溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何打造一套vue組件庫

發(fā)布時(shí)間:2020-06-25 18:51:06 來源:網(wǎng)絡(luò) 閱讀:625 作者:wx5d61fdc401976 欄目:開發(fā)技術(shù)

開篇
組件庫能幫我們節(jié)省開發(fā)精力,無需所有東西都從頭開始去做,通過一個(gè)個(gè)小組件拼接起來,就得到了我們想要的最終頁面。在日常開發(fā)中如果沒有特定的一些業(yè)務(wù)需求,使用組件庫進(jìn)行開發(fā)無疑是更便捷高效,而且質(zhì)量也相對更高的方案。

目前的開源組件庫有很多,不管是react還是vue的體系里都有很多非常優(yōu)秀的組件庫,比如我經(jīng)常使用的就有elementui和iview。當(dāng)然也還有其他的一些組件庫,他們的本質(zhì)其實(shí)都是為了節(jié)省重復(fù)造基礎(chǔ)組件這一輪子的過程。也有的公司可能會對自己公司的產(chǎn)品有特別的需求,不太愿意使用開源的組件庫的樣式,或者自己有一些公司內(nèi)部的業(yè)務(wù)項(xiàng)目需要用到,但開源項(xiàng)目無法滿足的組件需要沉淀下來的時(shí)候,自建一套組件庫就成為了一個(gè)作為業(yè)務(wù)驅(qū)動所需要的項(xiàng)目。

本文會從 ”準(zhǔn)備“ 和 ”實(shí)踐“ 兩個(gè)階段來闡述,一步步完成一個(gè)組件庫的打造。大致內(nèi)容如下:

準(zhǔn)備:主要講了搭建組件庫之前我們需要先提及一下一些基礎(chǔ)知識,為實(shí)踐階段做鋪墊。
實(shí)踐:有了一些基本概念,咱們就直接通過一個(gè)實(shí)踐案例來動手搭建一套基礎(chǔ)的組件庫。從做的過程中去感受組件庫的設(shè)計(jì)。
希望通過本文的分享以及包含的一個(gè)簡單的 實(shí)際操作案例,能讓你從組件庫使用者的角色向組件庫創(chuàng)造者的角色邁進(jìn)那么一小步,在日常使用組件庫的時(shí)候心里有個(gè)底,那我的目的也就達(dá)到了。

我們的案例地址是:arronkler.github.io/lime-ui/

對應(yīng)的 repo也就是:github.com/arronKler/l…

準(zhǔn)備 :打造組件庫之前你應(yīng)該知道些什么?
這一個(gè)章節(jié)主要是想先解析清楚一些在組件庫的建立中會用到的一些平時(shí)在業(yè)務(wù)概念中很少去關(guān)注的概念。我會分為工程和組件兩個(gè)方面來闡述,把我所知道的一些其中的技巧和坑點(diǎn)都交付出來,以幫助我們在實(shí)際去做的過程中可以有所準(zhǔn)備。

項(xiàng)目:做一個(gè)組件庫項(xiàng)目有哪些額外需要考慮的事?
做組件庫項(xiàng)目和常規(guī)業(yè)務(wù)項(xiàng)目肯定還是有一些事情是我們業(yè)務(wù)項(xiàng)目不怎么需要,但是類庫項(xiàng)目一般都會考慮的事,這一小節(jié)就是介紹說明一下,那些我們在做組件庫的過程中需要額外考慮的事。

組件測試
很多開發(fā)者平時(shí)業(yè)務(wù)項(xiàng)目都比較趕,然后就是一般業(yè)務(wù)項(xiàng)目中都不怎么寫測試腳本。但在做一個(gè)組件庫項(xiàng)目的過程中,最好還是有對應(yīng)的組件測試的腳本。至少有兩點(diǎn)好處:

自動化測試你寫的組件的功能特性
改動代碼不用擔(dān)心會影響之前的使用者。(測試腳本會告訴你有沒有出現(xiàn)未預(yù)料到的影響)
對于類庫型項(xiàng)目,我覺得第二點(diǎn)好處還是很重要的,這才能保證你在不斷推進(jìn)項(xiàng)目升級迭代的過程中,確保不會出現(xiàn)影響已經(jīng)在用你所創(chuàng)造的類庫的那些人,畢竟你要是升級一次讓他的項(xiàng)目出現(xiàn)大問題,那可真保不準(zhǔn)別人飯碗都能丟。(就像之前的antd的圣誕節(jié)雪花事件一樣)

由于我們是要寫vue的組件庫,這里推薦的測試工具集是 vue-test-utils 這套工具,vue-test-utils.vuejs.org/zh/ 。其中提供的各種測試函數(shù)和方法都能很好的滿足我們的測試需要。具體的安裝使用可以參見它的文檔。

我們這里主要想提的是 組件測試到底要測什么?

我們這里給到一張很直觀的圖,看到這張圖其實(shí)你應(yīng)該也清楚了這個(gè)問題的答案

button
這張圖來自視頻 www.youtube.com/watch?v=OIp… ,也是vue-test-util推薦的一個(gè)非常棒的演講,想要具體了解可以進(jìn)去看一下。

所以回過頭來,組件測試,實(shí)際需要我們不僅僅作為創(chuàng)造者的角度對組件的功能特性進(jìn)行測試。更要從使用者的角度來看,把組件當(dāng)做一個(gè)“黑盒子”,我們能給到它的是用戶的交互行為、props數(shù)據(jù)等,這個(gè)“黑盒子”也會對應(yīng)的反饋出一定的事件和渲染的視圖可以被使用者所捕獲和觀察。通過對這些位置的檢查,我們就能獲知一個(gè)組件的行為是否如我們所愿的去進(jìn)行著,確保它的行為一定是一致不出幺蛾子的。

另外還想提的一點(diǎn)偏的話題就是 契約精神。作為組件的使用者,我使用你的組件,等于咱們簽訂一個(gè)契約,這個(gè)組件的所有行為應(yīng)該是和你描述的是一致的,不會出現(xiàn)第三種意料之外的可能。畢竟對于企業(yè)項(xiàng)目來說,我們不喜歡surprise。antd的彩蛋事件也是給各位都提個(gè)醒,咱們搞技術(shù)可以這么玩也挺有創(chuàng)意,但是這種公用類庫,特別是企業(yè)使用的也比較多的,還是把創(chuàng)意收一收,講究契約,不講surprise。就算是自家企業(yè)內(nèi)部使用的組件庫,除非是業(yè)務(wù)上的人都是認(rèn)可的,否則也不要做這種危險(xiǎn)試探。

好的組件測試也是能夠幫助我們識別出那些我們有意或無意創(chuàng)造的surprise,有意的咱就不說了,就怕是那種無意中出現(xiàn)的surprise那就比較要命了,所以寫好組件測試還是挺有必要的。

文檔生成
一般來說,我們做一個(gè)類庫項(xiàng)目都會有對應(yīng)的說明文檔的,有的項(xiàng)目一個(gè)README.md 的文檔就夠了,有的可能需要在來幾個(gè) Markdown的文檔。對于組件庫這一類的項(xiàng)目來說,我們可以用文檔工具來輔助直接生成文檔。這里推薦 vuepress ,可以快速幫我們完成組件庫文檔的建設(shè)。(vuepress.vuejs.org/zh/guide/)

vuepress是一個(gè)文檔生成工具,默認(rèn)的樣式和vue官方文檔幾乎是一致的,因?yàn)閯?chuàng)造它的初衷就是想為vue和相關(guān)的子項(xiàng)目提供文檔支持。它內(nèi)置了 Markdown的擴(kuò)展,寫文檔的時(shí)候就是用 markdown來寫,最讓人省心的是你可以直接在 Markdown 文件中使用Vue組件,意味著我們的組件庫中寫的一個(gè)個(gè)組件,可以直接放到文檔里去用,展示組件的實(shí)際運(yùn)行效果。 我們的案例網(wǎng)站也就是通過vuepress來寫的,生成靜態(tài)網(wǎng)站后,用 gh-pages 直接部署到github上。

vuepress更好的一點(diǎn)在于你可以自定義其webpack配置和主題,意味著你可以讓你自己的文檔站點(diǎn)在開發(fā)階段有更多的功能特性的支持,同時(shí)可以把站點(diǎn)風(fēng)格改成自己的一套主題風(fēng)格。這就無需我們重頭開始去做一套了,對于咱們想要快速完成組件庫文檔建設(shè)這一需求來說,還是挺有效的。

不過這只是咱們要做的事情的一個(gè)輔助性的東西,所以具體的使用咱們在實(shí)踐階段再說明,這里就不贅述了。

自定義主題
自定義主題的功能對于一個(gè)開源類庫來說肯定還是挺有好處的,這樣使用者就可以自己使用組件庫的功能而在界面設(shè)計(jì)上使用自己的設(shè)計(jì)風(fēng)格。其實(shí)大部分組件庫的功能設(shè)計(jì)都是挺好挺完善的,所以一般來說中小型公司即使想要實(shí)現(xiàn)自己的一套組件風(fēng)格的東西,直接使用開源類庫如 element、iview或者基于react的Antd 所提供的功能和交互邏輯,然后在其上進(jìn)行主題定制基本就滿足需求了(除非你家設(shè)計(jì)師很有想法。。。)。

自定義主題的功能一般的使用方式是這樣的

通過主題生成工具。(制作者需要單獨(dú)做一個(gè)工具)
引入關(guān)鍵主題文件,覆蓋主題變量。(這種方式一般都需要適配制作者所使用的css預(yù)處理器)
對于第一種方式往往都是組件庫的制作者通過把生成組件樣式的那一套東西做成一個(gè)工具,然后提供給使用者去根據(jù)自己的需要來調(diào)整,最后生成一套特定的樣式文件,引入使用。

第二種方式,作為使用者來說,你主要做的其實(shí)是覆蓋了組件庫中的一些主題變量,因?yàn)榫唧w的組件的樣式文件不是寫死的固定樣式值,而是使用了定義好的變量,所以你的自定義主題就生效了。但是這也會引入一個(gè)小問題就是你必須適配組件庫的創(chuàng)造者所使用的樣式預(yù)處理器,比如你用iview,那你的項(xiàng)目就要能解析Less文件,你用ElementUI,你的項(xiàng)目就必須可以解析SCSS。

其實(shí)對于第一種方式也主要是以調(diào)整主題變量為主。所以當(dāng)咱們自己要做一套組件庫的時(shí)候,不難看出,一個(gè)核心點(diǎn)就是需要把主題變量文件和樣式文件拆開來,后面的就簡單了。

webpack打包
類庫項(xiàng)目的構(gòu)建這里提兩點(diǎn):

暴露入口
外部化依賴
先談第一點(diǎn) “暴露接口”。業(yè)務(wù)項(xiàng)目中,我們的整個(gè)項(xiàng)目通過webpack或其他打包工具打包成一個(gè)或多個(gè)bundle文件,這些文件被瀏覽器載入后就會直接運(yùn)行。但是一個(gè)類庫項(xiàng)目往往都不是單獨(dú)運(yùn)行的,而是通過暴露一個(gè) “入口”,然我在業(yè)務(wù)項(xiàng)目中去調(diào)用它。 在webpack配置文件里,可以通過定義 output 中的 library 和 libraryTarget 來控制我們要暴露的一個(gè) “入口變量” ,以及我們要構(gòu)建的目標(biāo)代碼。

這一點(diǎn)可以詳細(xì)參考webpack官方文檔: webpack.js.org/configurati…

module.exports = {
// other config
output: {
library: "MyLibName",
libraryTarget: "umd",
umdNamedDefine: true
}
}
復(fù)制代碼
再說一下 “外部化依賴”,我們做一個(gè)vue組件庫項(xiàng)目的時(shí)候,我們的組件都是依賴于vue的,當(dāng)我們組件庫項(xiàng)目中的某個(gè)地方引入了vue,那么打包的時(shí)候vue的運(yùn)行時(shí)也是會被一塊兒打包進(jìn)入最終的組件庫bundle文件的。這樣的問題在于,我們的vue組件庫是被vue項(xiàng)目使用的,那么項(xiàng)目中已經(jīng)有運(yùn)行時(shí)了,我們就沒必要在組件庫中加入運(yùn)行時(shí),這樣會多增加組件庫bundle的體積。使用webpack的 externals可以將vue依賴 "外部化"。

module.exports = {
// other config
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
}
}
復(fù)制代碼
按需加載
組件庫的按需加載功能還是很實(shí)用的, 這樣可以避免我們在使用組件庫的過程中把所有的用到和沒用到的內(nèi)容都打包到業(yè)務(wù)代碼中去,導(dǎo)致最后的bundle文件過大影響用戶體驗(yàn)。

在業(yè)務(wù)項(xiàng)目中我們的按需加載都是把需要按需加載的地方單獨(dú)生成為一個(gè)chunk,然后瀏覽器運(yùn)行我們的打包代碼的時(shí)候發(fā)現(xiàn)我們需要這一塊兒資源了,再發(fā)起請求獲取到對應(yīng)的所需代碼。

在組件庫里邊,我們就需要改變一下引入的方式,比如一開始我們引入一個(gè)組件庫的時(shí)候是直接將組件庫和樣式全部引入的。如下面這樣

import LimeUI from 'lime-ui' // 引入組件庫
import 'lime-ui/styles/index.css' // 引入整個(gè)組件庫的樣式文件

Vue.use(LimeUI)
復(fù)制代碼
那么,換成手動的按需加載的方式就是

import { Button } from 'lime-ui' // 引入button組件
import 'lime-ui/styles/button.css' // 引入button的樣式

Vue.component('l-button', Button) // 注冊組件
復(fù)制代碼
這種方式的確是按需引入的,但也一個(gè)不舒服的地方就是每次我們引入的時(shí)候都需要手動的引入組件和樣式。一般來說一個(gè)項(xiàng)目里面用到的組件少說也有十多個(gè),這就比較麻煩了。組件庫是怎么解決這個(gè)問題的呢?

通過babel插件的方式,將引入組件庫和組件樣式的模式自動化,比如antd、antd-mobile、material-ui都在使用的babel-plugin-import、還有ElementUI使用的 babel-plugin-component。在業(yè)務(wù)項(xiàng)目中配置好babel插件之后,它內(nèi)部就可以給你做一個(gè)這樣的轉(zhuǎn)換(這里以 babel-plugin-component)

// 原始代碼
import { Button } from 'components'

// 轉(zhuǎn)換代碼
var button = require('components/lib/button')
require('components/lib/button/style.css')
復(fù)制代碼
OK,那既然代碼可以做這樣的轉(zhuǎn)換的話,其實(shí)我們所要做的一點(diǎn)就是在我們打造組件庫的時(shí)候,把我們的組件庫的打包代碼放到對應(yīng)的文件目錄結(jié)構(gòu)之下就可以了。使用者可以選擇手動載入組件,也可以使用babel插件的方式優(yōu)化這一步驟。

babel-plugin-component 文檔: www.npmjs.com/package/bab…

babel-pluigin-import 文檔: www.npmjs.com/package/bab…

組件:比起日常的組件設(shè)計(jì),做組件庫你還需要知道些什么?
做組件庫中的組件的技巧和在項(xiàng)目中用到的還是有一些區(qū)別的,這一小節(jié)就是告訴大家,組件庫中的組件設(shè)計(jì),我們還應(yīng)該知道哪些必要的知識內(nèi)容。

組件通信:除了上下級之間進(jìn)行數(shù)據(jù)通信,還有什么?
我們常規(guī)用到的組件通信的方法就是通過 props 和 $emit 來進(jìn)行父組件和子組件之間的數(shù)據(jù)傳遞,如下面的示意圖中展示的那樣:父組件通過 props 將數(shù)據(jù)給子組件、子組件通過 $emit 將數(shù)據(jù)傳遞給父組件,頂多通過eventBus或Vuex來達(dá)到任意組件之間數(shù)據(jù)的相互通信。這些方法在常規(guī)的業(yè)務(wù)開發(fā)過程中是比較有效的,但是在組件庫的開發(fā)過程中就顯得有點(diǎn)力不從心了,主要的問題在于: 如何處理跨級組件之間的數(shù)據(jù)通信呢?

????
3??????
如果在日常項(xiàng)目中,我們當(dāng)然可以使用像 vuex 這樣的將組件數(shù)據(jù)直接 ”外包“ 出去的方式來實(shí)現(xiàn)數(shù)據(jù)的跨級訪問,但是vuex 始終是一個(gè)外部依賴項(xiàng),組件庫的設(shè)計(jì)肯定是不能讓這種強(qiáng)依賴存在的。下面我們就來說說兩個(gè)在組件庫項(xiàng)目中我們會用到的數(shù)據(jù)通信方式。

內(nèi)置的provide/inject
provide/inject 是vue自帶的可以跨級從子組件中獲取父級組件數(shù)據(jù)的一套方案。 這一對東西類似于react里面的 Context ,都是為了處理跨級組件數(shù)據(jù)傳遞的問題。

使用的時(shí)候,在子組件中的 inject 處聲明需要注入的數(shù)據(jù),然后在父級組件中的某個(gè)含有對應(yīng)數(shù)據(jù)的地方,提供子級組件所需要的數(shù)據(jù)。不管他們之間跨越了多少個(gè)組件,子級組件都能獲取到對應(yīng)的數(shù)據(jù)。(參考下面的偽代碼例子)

// 引用關(guān)系 CompA --> CompB --> CompC --> ... --> ChildComp

// CompA.vue
export default {
provide: {
theme: 'dark'
}
}

// CompB.vue
// CompC.vue
// ...

// ChildComp.vue
export default {
inject: ['theme'],
mounted() {
console.log(this.theme) // 打印結(jié)果: dark
}
}
復(fù)制代碼
不過provide/inject的方式主要是子組件從父級組件中跨級獲取到它的狀態(tài),卻不能完美的解決以下問題:

子級組件跨級傳遞數(shù)據(jù)到父級組件
父級組件跨級傳遞數(shù)據(jù)到子級組件
派發(fā)和廣播: 自制dispatch和broadcast功能
dispatch和broadcast可以用來做父子級組件之間跨級通信。在vue1.x里面是有dispatch和broadcast功能的,不過在vue2.x中被取消掉了。這里可以參考一下下面鏈接給出的v1.x中的內(nèi)容。

dispatch的文檔(v1.x):v1.vuejs.org/api/#vm-dis…

broadcast文檔(v1.x):v1.vuejs.org/api/#vm-bro…

根據(jù)文檔,我們得知

dispatch會派發(fā)一個(gè)事件,這個(gè)事件首先在自己這個(gè)組件實(shí)例上去觸發(fā),然后會沿著父級鏈一級一級的往上冒泡,直到觸發(fā)了某個(gè)父級中聲明的對這個(gè)事件的監(jiān)聽器后就停止,除非是這個(gè)監(jiān)聽器返回了true。當(dāng)然監(jiān)聽器也是可以通過回調(diào)函數(shù)獲取到事件派發(fā)的時(shí)候傳遞的所有參數(shù)的。這一點(diǎn)很像我們在DOM中的事件冒泡機(jī)制,應(yīng)該不難理解。

而broadcast就是會將事件廣播到自己的所有子組件實(shí)例上,一層一層的往下走,因?yàn)榻M件樹的原因,往下走的過程會遇到 “分叉”,也就可以看成是一條條的多個(gè)路徑。事件沿著每一個(gè)子路徑向下冒泡,每個(gè)路徑上觸發(fā)了監(jiān)聽器就停止,如果監(jiān)聽器返回的是true那就繼續(xù)向下再傳播。

簡單總結(jié)一下。dispatch派發(fā)事件往上冒泡,broadcast廣播事件往下散播,遇到處理對應(yīng)事件的監(jiān)聽器就處理,監(jiān)聽器沒有返回true就停止

需要注意的是,這里的派發(fā)和廣播事件都是 跨層級的 , 而且可以攜帶參數(shù),那也就意味著可以跨層級進(jìn)行數(shù)據(jù)通信。

dispatch
由于dispatch和broadcast在vue2.x中取消了,所以我們這里可以自己寫一個(gè),然后通過mixin的方式混入到需要使用到跨級組件通信的組件中。

方法內(nèi)容其實(shí)很簡單,這里就直接列代碼

// 參考自iview的實(shí)現(xiàn)
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;

if (name === componentName) {
  child.$emit.apply(child, [eventName].concat(params));
} else {
  broadcast.apply(child, [componentName, eventName].concat([params]));
}

});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;

  while (parent && (!name || name !== componentName)) {
    parent = parent.$parent;

    if (parent) {
      name = parent.$options.name;
    }
  }
  if (parent) {
    parent.$emit.apply(parent, [eventName].concat(params));
  }
},
broadcast(componentName, eventName, params) {
  broadcast.call(this, componentName, eventName, params);
}

}
};

復(fù)制代碼
其實(shí)這里的實(shí)現(xiàn)和vue1.x中的實(shí)現(xiàn)還是有一定的區(qū)別的:

dispatch沒有事件冒泡。找到哪個(gè)就直接執(zhí)行
設(shè)定了一個(gè)name參數(shù),只針對特定name的組件觸發(fā)事件
其實(shí)看懂了這里的代碼,你就應(yīng)該可以舉一反三想出 找尋任何一個(gè)組件的方法了,不管是向上還是向下找,無非就是循環(huán)遍歷和迭代處理,直到目標(biāo)組件出現(xiàn),然后調(diào)用它。 派發(fā)和廣播無非就是找到之后利用vue自帶的事件機(jī)制來發(fā)布事件,然后在具體組件中監(jiān)聽該事件并處理。

渲染函數(shù):它可以釋放javascript的能力
首先我們回顧一下一個(gè)組件是如何從寫代碼到被轉(zhuǎn)換成界面的。我們寫vue單文件組件的時(shí)候一般會有template、script和style三部分,在打包的時(shí)候,vue-loader會將其中的template模板部分先編譯成Vue實(shí)例中render選項(xiàng)所需要的構(gòu)建視圖的代碼。在具體運(yùn)行的時(shí)候,vue運(yùn)行時(shí)會使用$mount 進(jìn)行渲染,渲染好之后將其掛載到你提供的DOM節(jié)點(diǎn)下。

整個(gè)過程里面我們只日常關(guān)注最多的當(dāng)然就是template的部分,但是template其實(shí)只是vue提供的一個(gè)語法糖,只是讓我們寫代碼寫起來跟寫html一樣輕松,降低剛?cè)胧講ue的小伙伴的學(xué)習(xí)成本。React就沒有提供template的語法糖,而是使用的JSX來降低寫組件的復(fù)雜度。(vue能在react和angular兩大框架的壓力下異軍突起,簡潔易懂的模板語法是有一定促進(jìn)作用的,畢竟看起來更簡單)

通過上面我們回顧的內(nèi)容,其實(shí)我們也發(fā)現(xiàn)了,我們寫的template,最終都是javascript。這里template被編譯之后,給到了 render這個(gè)渲染函數(shù),在執(zhí)行渲染的時(shí)候vue就會執(zhí)行render中的操作來渲染我們的組件。

所以template是好,但如果你想要使用全部的javascript的能力,那就可以使用渲染函數(shù)

渲染函數(shù)&JSX (官方文檔):cn.vuejs.org/v2/guide/re…

日常寫業(yè)務(wù)組件,我們用template就挺OK的,不過當(dāng)遇到一些復(fù)雜情況,用 寫組件 --> 引入使用 --> 注冊組件 --> 使用組件 的方式就不好處理了,比如下面兩種情況:

通過代碼動態(tài)渲染組件
將組件渲染到其他位置
第一種情況是通過代碼動態(tài)渲染組件,比如運(yùn)營常常使用的活動h6頁面,每個(gè)活動都不一樣,每次要么都重新做一份,要么在原有的基礎(chǔ)上修改。但是這種修改的頁面結(jié)構(gòu)調(diào)整是很大的,每次都會是破壞性的,和重做其實(shí)沒區(qū)別。這樣的話,每次活動無論內(nèi)容如何,前端都要上手去寫代碼。但其實(shí)只需要在管理后臺做一個(gè)活動編輯器,編輯器的內(nèi)容直接轉(zhuǎn)化為render函數(shù)的代碼,然后通過配置下發(fā)到某個(gè)頁面上,承載頁拿到數(shù)據(jù)給到render函數(shù)執(zhí)行渲染。這樣就可以動態(tài)的根據(jù)管理后臺配置的方式來渲染組件內(nèi)容,每次的活動頁,運(yùn)營也可以通過編輯器自行生成。

第二種情況是要將組件渲染到不同位置。我們?nèi)粘憳I(yè)務(wù)組件基本就是寫一個(gè)組件,在需要的拿來使用。如果你只是在template中把組件寫進(jìn)去,那你的組件的內(nèi)容就都會作為當(dāng)前組件的子組件進(jìn)行渲染,所生成的DOM結(jié)構(gòu)也是在當(dāng)前的DOM結(jié)構(gòu)之下的。知道render之后,其實(shí)我們可以新建vue實(shí)例,動態(tài)渲染之后,手動掛載到任意的DOM位置上去。

import CompA from './CompA.vue'

let Instance = new Vue({
render(h) {
return h(CompA)
}
})

let component = Instance.$mount() // 執(zhí)行渲染
document.body.appendChild(component.$el) // 掛載到body元素下

復(fù)制代碼
我們使用的element里面的 this.$message 就用到了動態(tài)渲染,然后手動掛載到指定位置。

實(shí)踐:做一遍你就會了
這里先貼上我們的github地址,各位可以在做的過程中對照著看。github.com/arronKler/l…

建立一個(gè)工程化的項(xiàng)目
第一步,建立工程化結(jié)構(gòu)
這里就不廢話了,直接貼目錄結(jié)構(gòu)和解釋

|- assets/ # 存放一些額外的資源文件,圖片之類的
|- build/ # webpack打包配置
|- docs/ # 存放文檔
|- .vuepress # vuepress配置目錄
|- component # 組件相關(guān)的文檔放這里
|- README.md # 靜態(tài)首頁
|- lib/ # 打包生成的文件放這里
|- styles/ # 打包后的樣式文件
|- src/ # 在這里寫代碼
|- mixins/ # mixin文件
|- packages/ # 各個(gè)組件,每個(gè)組件是一個(gè)子目錄
|- styles/ # 樣式文件
|- common/ # 公用的樣式內(nèi)容
|- mixins/ # 復(fù)用的mixin
|- utils # 工具目錄
|- index.js # 打包入口,組件的導(dǎo)出
|- test/ # 測試文件夾
|- specs/ # 存放所有的測試用例
|- .npmignore
|- .gitignore
|- .babelrc
|- README.md
|- package.json
復(fù)制代碼
這里比較重要的目錄就是我們的src目錄,下面存放了我們的各個(gè)單一的組件和一套樣式庫,另外還有一些輔助的東西。我們寫文檔就是在 docs目錄下去寫。項(xiàng)目目錄最外層都是些常規(guī)的配置內(nèi)容,比如 .npmignore 和 .gitignore 這樣的文件我們都是很常見的,所以我就不具體細(xì)說這一部分了,要是有一定疑惑可以直接參見github上的源碼對照著看。

這里我們把需要使用到的類庫文件也先建立好

在 src/mixins 下創(chuàng)建一個(gè) emitter.js,寫入如下內(nèi)容,也就是我們的dispatch和broadcast的方法,之后的組件設(shè)計(jì)中會用到

function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;

if (name === componentName) {
  child.$emit.apply(child, [eventName].concat(params));
} else {
  broadcast.apply(child, [componentName, eventName].concat([params]));
}

});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;

  while (parent && (!name || name !== componentName)) {
    parent = parent.$parent;

    if (parent) {
      name = parent.$options.name;
    }
  }
  if (parent) {
    parent.$emit.apply(parent, [eventName].concat(params));
  }
},
broadcast(componentName, eventName, params) {
  broadcast.call(this, componentName, eventName, params);
}

}
};
復(fù)制代碼
然后在 src/utils 下新建一個(gè) assist.js 文件,寫下輔助性的函數(shù)

export function oneOf(value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) {
return true;
}
}
return false;
}
復(fù)制代碼
這兩個(gè)地方都是之后會使用到的,如果你需要其他的輔助內(nèi)容,也可以在這兩個(gè)文件所在的目錄下去建立。

第二步, 完善打包流程
目錄建好了,那就該填充血肉了,要打包一個(gè)組件庫項(xiàng)目,肯定是要先配置好我們的webpack,不然寫了源碼也沒法跑起來。所以我們先定位到 build目錄下,在build目錄下先建立三個(gè)文件

webpack.base.js 。存放基本的一些rules配置

webpack.prod.js 。整個(gè)組件庫的打包配置

gen-style.js 。單獨(dú)對樣式進(jìn)行打包

以下是具體的配置內(nèi)容

/ webpack.base.js /
const path = require('path');
const webpack = require('webpack');
const pkg = require('../package.json');
const VueLoaderPlugin = require('vue-loader/lib/plugin')

function resolve(dir) {
return path.join(__dirname, '..', dir);
}

module.exports = {
module: {
rules: [
{
test: /.vue$/,
loader: 'vue-loader',
options: {
loaders: {
css: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
],
less: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'less-loader',
options: {
sourceMap: true,
},
},
],
},
postLoaders: {
html: 'babel-loader?sourceMap'
},
sourceMap: true,
}
},
{
test: /.js$/,
loader: 'babel-loader',
options: {
sourceMap: true,
},
exclude: /node_modules/,
},
{
test: /.css$/,
loaders: [
{
loader: 'style-loader',
options: {
sourceMap: true,
},
},
{
loader: 'css-loader',
options: {
sourceMap: true,
},
}
]
},
{
test: /.less$/,
loaders: [
{
loader: 'style-loader',
options: {
sourceMap: true,
},
},
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'less-loader',
options: {
sourceMap: true,
},
},
]
},
{
test: /.scss$/,
loaders: [
{
loader: 'style-loader',
options: {
sourceMap: true,
},
},
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
]
},
{
test: /.(gif|jpg|png|woff|svg|eot|ttf)\??.$/,
loader: 'url-loader?limit=8192'
}
]
},
resolve: {
extensions: ['.js', '.vue'],
alias: {
'vue': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.DefinePlugin({
'process.env.VERSION': '${pkg.version}'
}),
new VueLoaderPlugin()
]
};
復(fù)制代碼
/
webpack.prod.js */
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.js');

process.env.NODE_ENV = 'production';

module.exports = merge(webpackBaseConfig, {
devtool: 'source-map',
mode: "production",
entry: {
main: path.resolve(dirname, '../src/index.js') // 將src下的index.js 作為入口點(diǎn)
},
output: {
path: path.resolve(
dirname, '../lib'),
publicPath: '/lib/',
filename: 'lime-ui.min.js', // 改成自己的類庫名
library: 'lime-ui', // 類庫導(dǎo)出
libraryTarget: 'umd',
umdNamedDefine: true
},
externals: { // 外部化對vue的依賴
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
})
]
});
復(fù)制代碼
/ gen-style.js /
const gulp = require('gulp');
const cleanCSS = require('gulp-clean-css');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
const autoprefixer = require('gulp-autoprefixer');
const components = require('./components.json')

function buildCss(cb) {
gulp.src('../src/styles/index.scss')
.pipe(sass())
.pipe(autoprefixer())
.pipe(cleanCSS())
.pipe(rename('lime-ui.css'))
.pipe(gulp.dest('../lib/styles'));
cb()
}

exports.default = gulp.series(buildCss)
復(fù)制代碼
OK,這里我們的webpack配置基本設(shè)置好了,webpack.base.js 中的配置就主要是一些loader和插件的配置,具體的出入口都是在 webpack.prod.js 中配置的。這里webpack.prod.js 合并了 webpack.base.js 中的配置項(xiàng)。關(guān)于 output.libary 和 externals ,閱讀了之前 “準(zhǔn)備” 階段的內(nèi)容的應(yīng)該不會陌生了。

另外還有 gen-style.js 這個(gè)文件是單獨(dú)使用了 gulp 來對樣式文件進(jìn)行打包操作的,我們這里選用的是 scss的語法,如果你想用less或其他的預(yù)處理器,也可以自行修改這里的文件和相關(guān)依賴。

不過這個(gè)配置肯定還沒有結(jié)束,首先我們需要安裝好這里的配置里使用到的各種loader和plugin。為了不漏掉安裝項(xiàng)和保持一致性,可以直接復(fù)制下面的配置內(nèi)容放到 package.json 下,通過 npm install 來進(jìn)行安裝。需要注意的是,這里的安裝完成之后,其實(shí)后面的一些內(nèi)容的依賴也都一并安裝好了。

"dependencies": {
"async-validator": "^3.0.4",
"core-js": "2.6.9",
"webpack": "^4.39.2",
"webpack-cli": "^3.3.7"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@vue/test-utils": "^1.0.0-beta.29",
"babel-loader": "^8.0.6",
"chai": "^4.2.0",
"cross-env": "^5.2.0",
"css-loader": "2.1.1",
"file-loader": "^4.2.0",
"gh-pages": "^2.1.1",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^7.0.0",
"gulp-clean-css": "^4.2.0",
"gulp-rename": "^1.4.0",
"gulp-sass": "^4.0.2",
"karma": "^4.2.0",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage": "^2.0.1",
"karma-mocha": "^1.3.0",
"karma-sinon-chai": "^2.0.2",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "^0.0.32",
"karma-webpack": "^4.0.2",
"less": "^3.10.2",
"less-loader": "^5.0.0",
"mocha": "^6.2.0",
"node-sass": "^4.12.0",
"rimraf": "^3.0.0",
"sass-loader": "^7.3.1",
"sinon": "^7.4.1",
"sinon-chai": "^3.3.0",
"style-loader": "^1.0.0",
"url-loader": "^2.1.0",
"vue-loader": "^15.7.1",
"vue-style-loader": "^4.1.2",
"vuepress": "^1.0.3"
},
復(fù)制代碼
另外,由于我們使用了babel,所以需要在項(xiàng)目的根目錄下設(shè)置一下 .babelrc 文件,內(nèi)容如下:

{
"presets": [[
"@babel/preset-env",
br/>[
"@babel/preset-env",
"loose": false,
"modules": "commonjs",
"spec": true,
"useBuiltIns": "usage",
"corejs": "2.6.9"
}
]
],
"plugins": ["@babel/plugin-transform-runtime",
br/>"@babel/plugin-transform-runtime",
}
復(fù)制代碼
當(dāng)然也不要忘記在package.json文件中寫上scripts簡化手動輸入命令的過程

{
"scripts": {
"build:style": "gulp --gulpfile build/gen-style.js",
"build:prod": "webpack --config build/webpack.prod.js",
}
}
復(fù)制代碼
第三步,建立文檔化工具
如果在上一步中未安裝了 vuepress ,可以通過 npm install vuepress --save-dev 來安裝,

然后在 package.json 中加入腳本,快速啟動

{
"scripts": {
// ...
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}
}
復(fù)制代碼
這個(gè)時(shí)候你可以在你的 docs/README.md 文件里寫點(diǎn)內(nèi)容,然后運(yùn)行 npm run docs:dev 就可以看到本地的文檔內(nèi)容了。需要打包的時(shí)候使用 npm run docs:build 就可以了。

如果我們的項(xiàng)目是要放到github上的,那么其實(shí)也可以一并將我們的文檔生成之后也放到github上去,利用github的pages功能讓這個(gè)本地的文檔在線運(yùn)行。(github pages托管我們的靜態(tài)頁面和資源)

可以運(yùn)行 npm install gh-pages --save-dev 安裝 gh-pages 這個(gè)可以幫我們一鍵部署github pages文檔的工具。它的工作原理就是將對應(yīng)的某個(gè)文件夾下的資源遷移到我們的當(dāng)前項(xiàng)目的gh-pages分支上,然后這個(gè)分支在push給了github之后,github就會將該分支內(nèi)的內(nèi)容服務(wù)起來。為了更好的使用它,我們可以在package.json中添加scripts

{
"scripts": {
// ...
"deploy": "gh-pages -d docs/.vuepress/dist",
"deploy:build": "npm run docs:build && npm run deploy",
}
}
復(fù)制代碼
這樣你就可以使用 npm run deploy 直接部署你的vuepress生成的靜態(tài)站點(diǎn),不過務(wù)必在部署之前運(yùn)行一下文檔的構(gòu)建程序。因此我們也添加了一條 npm run deploy:build 命令,使用這條命令就可以直接把文檔的構(gòu)建和部署直接一起解決。是不是很簡單呢?

不過為了我們能夠直接使用自己寫的組件,還需要對vuepress做一點(diǎn)點(diǎn)配置。在 docs/.vuepress目錄下新建一個(gè) enhanceApp.js 文件,寫入如下內(nèi)容,將我們的組件庫的入口和樣式注入進(jìn)去

import LimeUI from '../../src/index.js'
import "../../src/styles/index.scss"

export default ({
Vue,
options,
router
}) => {
Vue.use(LimeUI)
}
復(fù)制代碼
這個(gè)時(shí)候我們之后寫的組件就可以直接在文檔中使用了。

第四步,樣式構(gòu)建
先需要說明的是這里我們所使用的樣式預(yù)處理器的語法是scss。那么在“完善打包流程”這一小節(jié)中已經(jīng)將用gulp進(jìn)行打包的代碼給出了,不過有必要說明一下,我們又是如何去整合樣式內(nèi)容的。

首先,為了之后便于做按需加載,對于每個(gè)組件的樣式都是一個(gè)單獨(dú)的scss文件,寫樣式的時(shí)候,為了避免太多的層級嵌套,使用了BEM風(fēng)格的方式去書寫。

我們需要先在 src/styles目錄執(zhí)行如下命令生成一個(gè)基本的樣式文件

cd src/styles
mkdir common
mkdir mixins
touch common/var.scss # 樣式變量文件
touch common/mixins.scss
touch index.scss # 引入所有樣式
復(fù)制代碼
然后將對應(yīng)的 var.scss 和 mixins.scss 文件填充上一些基礎(chǔ)內(nèi)容

/ common/var.scss /

$--color-primary: #ff6b00 !default;
$--color-white: #FFFFFF !default;
$--color-info: #409EFF !default;
$--color-success: #67C23A !default;
$--color-warning: #E6A23C !default;
$--color-danger: #F56C6C !default;
復(fù)制代碼
/ mixins/mixins.scss /
$namespace: 'lime'; / 組件庫的樣式前綴 /

/ BEM
--------------------------
/
@mixin b($block) {
$B: $namespace+'-'+$block !global;

.#{$B} {@content;
br/>@content;
}
復(fù)制代碼
在mixins文件中我們聲明了一個(gè)mixin,用于幫助我們更好的去構(gòu)建樣式文件。

組件打造案例
上面的內(nèi)容設(shè)置好了, 咱們就可以開始具體去做一個(gè)組件試試了

簡單的button組件
這是做好之后的大致效果

button
OK,那我們建立基本的button組件相關(guān)的文件

cd src/packages
mkdir button && cd button
touch index.js
touch button.vue
復(fù)制代碼
寫入button.vue的內(nèi)容

<template>
<button class="lime-button" :class="{[lime-button-${type}]: true}" type="button">
<slot></slot>
</button>
</template>

<script>
import { oneOf } from '../../utils/assist';

export default {
name: 'Button',
props: {
type: {
validator (value) {
return oneOf(value, ['default', 'primary', 'info', 'success', 'warning', 'error']);
},
type: String,
default: 'default'
}
}
}
</script>
復(fù)制代碼
這里我們需要在 index.js 中導(dǎo)出這個(gè)組件

import Button from './button.vue'
export default Button
復(fù)制代碼
這樣單個(gè)的一個(gè)組件就完成了,之后你可以再多做幾個(gè)組件試試,不過有一點(diǎn)就是這些組件需要一個(gè)統(tǒng)一的打包入口,我們再webpack中已經(jīng)配置過了,那就是 src/index.js 這個(gè)文件,我們需要在這個(gè)文件里面將我們剛才寫的button組件以及你自己寫的其他組件都引入進(jìn)來,然后統(tǒng)一導(dǎo)出給webpack打包使用,具體代碼見下

import Button from './packages/button'

const components = {
lButton: Button,
}

const install = function (Vue, options = {}) {

Object.keys(components).forEach(key => {
Vue.component(key, components[key]);
});
}

export default install
復(fù)制代碼
可以看到的是index.js中我們最終導(dǎo)出的是一個(gè)叫install的函數(shù),這個(gè)函數(shù)其實(shí)就是Vue插件的一種寫法,便于我們在實(shí)際項(xiàng)目中引入的時(shí)候可以使用 Vue.use 的方式來自動安裝我們的整個(gè)組件庫。install接受兩個(gè)參數(shù),一個(gè)是Vue,我們把它用來注冊一個(gè)個(gè)的組件。還有一個(gè)是options,便于我們可以在注冊組件的時(shí)候傳入一些初始化參數(shù),比如默認(rèn)的按鈕大小、主題等信息,都可以通過參數(shù)的方式來設(shè)定。

然后我們可以在 src/styles目錄下新建一個(gè)button.scss 文件,寫入我們button對應(yīng)的樣式

/ button.scss /
@charset "UTF-8";
@import "common/var";
@import "mixins/mixins";

@include b(button) {
min-width: 60px;
height: 36px;
font-size: 14px;
color: #333;
background-color: #fff;
border-width: 1px;
border-radius: 4px;
outline: none;
border: 1px solid transparent;
padding: 0 10px;

&:active,
&:focus {
outline: none;
}

&-default {
color: #333;
border-color: #555;

&:active,
&:focus,
&:hover {
  background-color: rgba($--color-primary, 0.3);
}

}
&-primary {
color: #fff;
background-color: $--color-primary;

&:active,
&:focus,
&:hover {
  background-color: mix($--color-primary, #ccc);
}

}

&-info {
color: #fff;
background-color: $--color-info;

&:active,
&:focus,
&:hover {
  background-color: mix($--color-info, #ccc);
}

}
&-success {
color: #fff;
background-color: $--color-success;

&:active,
&:focus,
&:hover {
  background-color: mix($--color-success, #ccc);
}

}
}
復(fù)制代碼
最后我們還需要在 src/styles/index.scss 文件中將button的樣式引入進(jìn)去

@import "button";
復(fù)制代碼
為了簡單的實(shí)驗(yàn),你可以直接在 docs/README.md 文件下寫兩個(gè)button組件試試看

<template>
<l-button type="primary">Click me</l-button>
</template>
復(fù)制代碼
如果你想要得到和我在 arronkler.github.io/lime-ui/ 上一樣的效果,可以參考 github.com/arronKler/l… 項(xiàng)目中的 docs 目錄下的配置。如果想要更個(gè)性化的配置,可以查閱vuepress的官方文檔。

Notice提示組件
這個(gè)組件就要用到我們的動態(tài)渲染的相關(guān)的東西了。具體最后的使用方式是這樣的

this.$notice({
title: '提示',
content: this.content || '內(nèi)容',
duration: 3
})
復(fù)制代碼
效果類似于這樣

button
OK,我們先來寫一下這個(gè)組件的一個(gè)基本源碼

在 src/packages 目錄下新建notice文件夾,然后新建一個(gè) notice.vue 文件

<template>
<div class="lime-notice">
<div class="lime-noticemain" v-for="item in notices" :key="item.id">
<div class="lime-notice
title">{{item.title}}</div>
<div class="lime-notice__content">{{item.content}}</div>
</div>
</div>
</template>

<script>
export default {
data() {
return {
notices: []
}
},
methods: {
add(notice) {
let id = +new Date()
notice.id = id
this.notices.push(notice)

  const duration = notice.duration
  setTimeout(() => {
    this.remove(id)
  }, duration * 1000)
},
remove(id) {
  for(let i = 0; i < this.notices.length; i++) {
    if (this.notices[i].id === id) {
      this.notices.splice(i, 1)
      break;
    }
  }
}

}
}
</script>

復(fù)制代碼
代碼很簡單,其實(shí)就是聲明了一個(gè)容器,然后在其中通過控制 notices 的數(shù)據(jù)來展示和隱藏,接著我們在同一個(gè)目錄下新建一個(gè)notice.js 文件來做動態(tài)渲染

import Vue from 'vue'
import Notice from './notice.vue'

Notice.newInstance = (properties) => {
let props = properties || {}
const Instance = new Vue({
render(h) {
return h(Notice, {
props
})
}
})

const component = Instance.$mount()
document.body.appendChild(component.$el)

const notice = component.$children[0]

return {
add(_notice) {
notice.add(_notice)
},
remove(id) {

}

}
}

let noticeInstance

export default (_notice) => {
noticeInstance = noticeInstance || Notice.newInstance()
noticeInstance.add(_notice)
}
復(fù)制代碼
這里我們我們通過動態(tài)渲染的方式讓我們的組件可以直接掛在到body下面,而非歸屬于根掛載點(diǎn)之下。

然后在 src/styles 目錄下新建 notice.scss 文件,寫上我們的樣式文件

/ notice.scss /
@charset "UTF-8";
@import "common/var";
@import "mixins/mixins";

@include b(notice) {
position: fixed;
right: 20px;
top: 60px;
z-index: 1000;

&__main {
min-width: 100px;
padding: 10px 20px;
box-shadow: 0 0 4px #aaa;
margin-bottom: 10px;
border-radius: 4px;
}

&title {
font-size: 16px;
}
&
content {
font-size: 14px;
color: #777;
}
}
復(fù)制代碼
最后同樣的,也需要在 src/index.js 這個(gè)入口文件中對 notice做處理。完整代碼是這樣的。

import Button from './packages/button'
import Notice from './packages/notice/notice.js'

const components = {
lButton: Button
}

const install = function (Vue, options = {}) {

Object.keys(components).forEach(key => {
Vue.component(key, components[key]);
});

Vue.prototype.$notice = Notice;
}

export default install
復(fù)制代碼
我們可以看到我們再Vue的原型上掛上了我們的 $notice 方法,這個(gè)方法調(diào)用的時(shí)候就會觸發(fā)我們在 notice.js 文件中動態(tài)渲染組件的一套流程。這個(gè)時(shí)候我們就可以在 docs/README.md 文檔中測試著用了。

<script>
export default() {
mounted() {
this.$notice({
title: '提示',
content: this.content,
duration: 3
})
}
}
<script>
復(fù)制代碼
單獨(dú)打包樣式和組件
為了能支持按需加載的功能,我們除了將整個(gè)組件庫打包之外,還需要對樣式和組件單獨(dú)打包成單個(gè)的文件。這里我們需要做兩件事兒

打包單獨(dú)的css文件
打包單獨(dú)的組件內(nèi)容
對于第一點(diǎn),我們需要對 build/gen-style.js 文件做一下改造,加上buildSeperateCss任務(wù),完整代碼如下

// 其他之前的代碼...

function buildSeperateCss(cb) {
Object.keys(components).forEach(compName => {
gulp.src(../src/styles/${compName}.scss)
.pipe(sass())
.pipe(autoprefixer())
.pipe(cleanCSS())
.pipe(rename(${compName}.css))
.pipe(gulp.dest('../lib/styles'));
})

cb()
}

exports.default = gulp.series(buildCss, buildSeperateCss) // 加上 buildSeperateCss
復(fù)制代碼
對于第二點(diǎn),我們可以用一個(gè)新的webpack配置來處理,新建一個(gè) build/webpack.component.js 文件,寫入

const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.js');
const components = require('./components.json')
process.env.NODE_ENV = 'production';

const basePath = path.resolve(__dirname, '../')
let entries = {}
Object.keys(components).forEach(key => {
entries[key] = path.join(basePath, 'src', components[key])
})

module.exports = merge(webpackBaseConfig, {
devtool: 'source-map',
mode: "production",
entry: entries,
output: {
path: path.resolve(__dirname, '../lib'),
publicPath: '/lib/',
filename: '[name].js',
chunkFilename: '[id].js',
// library: 'lime-ui',
libraryTarget: 'umd',
umdNamedDefine: true
},
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
})
]
});

復(fù)制代碼
這里我們引用了build文件夾下的一個(gè)叫做 component.json 的文件,該文件是我自定義用來標(biāo)識我們的組件和組件路徑的,實(shí)際上你也可以通過腳本直接遍歷 src/packages目錄自動獲得這樣一些信息。這里只是簡單演示, build/component.json 的代碼如下

{
"button": "packages/button/index.js",
"notice": "packages/notice/notice.js"
}
復(fù)制代碼
所有的單獨(dú)打包流程配置好以后,我們就可以在 package.json 文件中再加上 scripts 命令

{
"scripts": {
// ...
"build:components": "webpack --config build/webpack.component.js",
"dist": "npm run build:style && npm run build:prod && npm run build:components",
}
}
復(fù)制代碼
OK,現(xiàn)在只需要運(yùn)行 npm run dist 命令,它就會自動去構(gòu)建完整的樣式內(nèi)容和各個(gè)組件單獨(dú)的樣式內(nèi)容,然后會打包一個(gè)完整的組件包和各個(gè)組件的單獨(dú)的包。

這里需要注意的一點(diǎn)就是你的package.json 文件中的這幾個(gè)字段需要做一下調(diào)整

{
"name": "lime-ui",
"version": "1.0.0",
"main": "lib/lime-ui.min.js",
//...
}
復(fù)制代碼
其中name表示別人使用了你的包的時(shí)候的包名,main字段很重要,表示別人直接引入你包的時(shí)候,入口文件是哪一個(gè)。這里因?yàn)槲覀僿ebpack打包后的文件是 lib/lime-ui.min.js 所以我們這樣去設(shè)置。

一切就緒后,你就可以運(yùn)行 npm run dist 打包你的組件庫,然后 npm publish 去發(fā)布你的組件庫了(發(fā)布前需要 npm login 登陸)

使用自己的組件庫
直接使用
我們可以用vue-cli 或其他工具另外生成一個(gè)demo項(xiàng)目,用這個(gè)項(xiàng)目去引入我們的組件庫。如果你的包還沒有發(fā)布出去,可以在你的組件庫項(xiàng)目目錄下 用 npm link 或者 yarn link的命令創(chuàng)建一個(gè)link(推薦使用yarn)

然后在你的demo目錄下使用 npm link package_name 或者 yarn link package_name 這里的package_name就是你的組件庫的包名,然后在你的demo項(xiàng)目的入口文件里

import Vue from vue
import LimeUI from 'lime-ui'
import 'lime-ui/lib/styles/lime-ui.css'
// 其他代碼 ...

Vue.use(LimeUI)
復(fù)制代碼
這樣設(shè)置好之后,我們創(chuàng)建的組件就可以在這個(gè)項(xiàng)目里使用了

按需加載
上面我們談的是全局載入的一種使用方法,那如何按需加載呢?其實(shí)我們之前也說過那么一點(diǎn)

先通過npm安裝好 babel-plugin-component 包,然后在你的demo項(xiàng)目的 .babelrc 文件中寫上這部分內(nèi)容

{
"plugins": [
["component", {
"libraryName": "lime-ui",
"libDir": "lib",
"styleLibrary": {
"name": "styles",
"base": false, // no base.css file
"path": "[module].css"
}
}]
]
}
復(fù)制代碼
這里的配置是要符合我們的lime-ui 的一個(gè)目錄結(jié)構(gòu)的,有了這個(gè)配置我們就可以進(jìn)行按需加載了,你可以像這樣做加載一個(gè)Button

import Vue from 'vue'
import { Button } from 'lime-ui'

Vue.component('a-button', Button)
復(fù)制代碼
可以看到的是,我們并沒有在這個(gè)位置加載任何樣式,因?yàn)?babel-plugin-component 已經(jīng)幫我們做了,不過因?yàn)槲覀冎辉诮M件庫的入口點(diǎn)里面設(shè)置了 install 方法用來注冊組件,所以這里我們按需引入的時(shí)候,就需要自己手動注冊了。

主題定制
前面的內(nèi)容做好之后,主題定制就比較簡單了,我們先在DEMO項(xiàng)目的入口文件同級目錄下創(chuàng)建一個(gè) global.scss 文件,然后在其中寫入類似下面這樣的代碼。

$--color-primary: red;
@import "~lime-ui/src/styles/index.scss";
復(fù)制代碼
然后在入口文件中把引入組件庫的方式改變一下

import Vue from vue
import LimeUI from 'lime-ui'
import './global.scss'
// 其他代碼 ...

Vue.use(LimeUI)
復(fù)制代碼
我們在入口文件中把對組件庫的樣式引入,改成引入我們自定義的global.scss文件。

其實(shí)這里就是覆蓋了我們在組件庫項(xiàng)目里 var.scss 里的變量的值,然后其余的組件基礎(chǔ)樣式還是使用了各自的樣式內(nèi)容,這樣就可以達(dá)到主題定制了。

結(jié)語
本文通過對組件庫的一些特性的介紹和一個(gè)實(shí)際的操作案例,闡述了打造一套組件庫的一些基礎(chǔ)的東西。希望能通過這樣的一次分享,讓我們不只是去使用組件庫,而是能知道組件庫的誕生過程和了解組件庫的一些內(nèi)部特性,幫助我們在日常使用的過程中能“心中有數(shù)”,當(dāng)出現(xiàn)問題或組件庫需求可能不滿足的時(shí)候有一個(gè)新的思考入手點(diǎn),那就足夠了。

引用參考

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI