開篇
組件庫能幫咱們節省開發精力,無需全部東西都從頭開始去作,經過一個個小組件拼接起來,就獲得了咱們想要的最終頁面。在平常開發中若是沒有特定的一些業務需求,使用組件庫進行開發無疑是更便捷高效,並且質量也相對更高的方案。javascript
目前的開源組件庫有不少,無論是react仍是vue的體系裏都有不少很是優秀的組件庫,好比我常用的就有elementui和iview。固然也還有其餘的一些組件庫,他們的本質其實都是爲了節省重複造基礎組件這一輪子的過程。也有的公司可能會對本身公司的產品有特別的需求,不太願意使用開源的組件庫的樣式,或者本身有一些公司內部的業務項目須要用到,但開源項目沒法知足的組件須要沉澱下來的時候,自建一套組件庫就成爲了一個做爲業務驅動所須要的項目。css
本文會從 」準備「 和 」實踐「 兩個階段來闡述,一步步完成一個組件庫的打造。大體內容以下:html
準備:主要講了搭建組件庫以前咱們須要先說起一下一些基礎知識,爲實踐階段作鋪墊。
實踐:有了一些基本概念,我們就直接經過一個實踐案例來動手搭建一套基礎的組件庫。從作的過程當中去感覺組件庫的設計。
但願經過本文的分享以及包含的一個簡單的 實際操做案例,能讓你從組件庫使用者的角色向組件庫創造者的角色邁進那麼一小步,在平常使用組件庫的時候內心有個底,那個人目的也就達到了。前端
咱們的案例地址是:arronkler.github.io/lime-ui/vue
對應的 repo也就是:github.com/arronKler/l…java
準備 :打造組件庫以前你應該知道些什麼?
這一個章節主要是想先解析清楚一些在組件庫的創建中會用到的一些平時在業務概念中不多去關注的概念。我會分爲工程和組件兩個方面來闡述,把我所知道的一些其中的技巧和坑點都交付出來,以幫助咱們在實際去作的過程當中能夠有所準備。node
項目:作一個組件庫項目有哪些額外須要考慮的事?
作組件庫項目和常規業務項目確定仍是有一些事情是咱們業務項目不怎麼須要,可是類庫項目通常都會考慮的事,這一小節就是介紹說明一下,那些咱們在作組件庫的過程當中須要額外考慮的事。react
組件測試
不少開發者平時業務項目都比較趕,而後就是通常業務項目中都不怎麼寫測試腳本。但在作一個組件庫項目的過程當中,最好仍是有對應的組件測試的腳本。至少有兩點好處:webpack
自動化測試你寫的組件的功能特性
改動代碼不用擔憂會影響以前的使用者。(測試腳本會告訴你有沒有出現未預料到的影響)
對於類庫型項目,我以爲第二點好處仍是很重要的,這才能保證你在不斷推動項目升級迭代的過程當中,確保不會出現影響已經在用你所創造的類庫的那些人,畢竟你要是升級一次讓他的項目出現大問題,那可真保不許別人飯碗都能丟。(就像以前的antd的聖誕節雪花事件同樣)git
因爲咱們是要寫vue的組件庫,這裏推薦的測試工具集是 vue-test-utils 這套工具,vue-test-utils.vuejs.org/zh/ 。其中提供的各類測試函數和方法都能很好的知足咱們的測試須要。具體的安裝使用能夠參見它的文檔。
咱們這裏主要想提的是 組件測試到底要測什麼?
咱們這裏給到一張很直觀的圖,看到這張圖其實你應該也清楚了這個問題的答案
button
這張圖來自視頻 www.youtube.com/watch?v=OIp… ,也是vue-test-util推薦的一個很是棒的演講,想要具體瞭解能夠進去看一下。
因此回過頭來,組件測試,實際須要咱們不只僅做爲創造者的角度對組件的功能特性進行測試。更要從使用者的角度來看,把組件當作一個「黑盒子」,咱們能給到它的是用戶的交互行爲、props數據等,這個「黑盒子」也會對應的反饋出必定的事件和渲染的視圖能夠被使用者所捕獲和觀察。經過對這些位置的檢查,咱們就能獲知一個組件的行爲是否如咱們所願的去進行着,確保它的行爲必定是一致不出幺蛾子的。
另外還想提的一點偏的話題就是 契約精神。做爲組件的使用者,我使用你的組件,等於我們簽定一個契約,這個組件的全部行爲應該是和你描述的是一致的,不會出現第三種意料以外的可能。畢竟對於企業項目來講,咱們不喜歡surprise。antd的彩蛋事件也是給各位都提個醒,我們搞技術能夠這麼玩也挺有創意,可是這種公用類庫,特別是企業使用的也比較多的,仍是把創意收一收,講究契約,不講surprise。就算是自家企業內部使用的組件庫,除非是業務上的人都是承認的,不然也不要作這種危險試探。
好的組件測試也是可以幫助咱們識別出那些咱們有意或無心創造的surprise,有意的咱就不說了,就怕是那種無心中出現的surprise那就比較要命了,因此寫好組件測試仍是挺有必要的。
文檔生成
通常來講,咱們作一個類庫項目都會有對應的說明文檔的,有的項目一個README.md 的文檔就夠了,有的可能須要在來幾個 Markdown的文檔。對於組件庫這一類的項目來講,咱們能夠用文檔工具來輔助直接生成文檔。這裏推薦 vuepress ,能夠快速幫咱們完成組件庫文檔的建設。(vuepress.vuejs.org/zh/guide/)
vuepress是一個文檔生成工具,默認的樣式和vue官方文檔幾乎是一致的,由於創造它的初衷就是想爲vue和相關的子項目提供文檔支持。它內置了 Markdown的擴展,寫文檔的時候就是用 markdown來寫,最讓人省心的是你能夠直接在 Markdown 文件中使用Vue組件,意味着咱們的組件庫中寫的一個個組件,能夠直接放到文檔裏去用,展現組件的實際運行效果。 咱們的案例網站也就是經過vuepress來寫的,生成靜態網站後,用 gh-pages 直接部署到github上。
vuepress更好的一點在於你能夠自定義其webpack配置和主題,意味着你可讓你本身的文檔站點在開發階段有更多的功能特性的支持,同時能夠把站點風格改爲本身的一套主題風格。這就無需咱們重頭開始去作一套了,對於我們想要快速完成組件庫文檔建設這一需求來講,仍是挺有效的。
不過這只是我們要作的事情的一個輔助性的東西,因此具體的使用我們在實踐階段再說明,這裏就不贅述了。
自定義主題
自定義主題的功能對於一個開源類庫來講確定仍是挺有好處的,這樣使用者就能夠本身使用組件庫的功能而在界面設計上使用本身的設計風格。其實大部分組件庫的功能設計都是挺好挺完善的,因此通常來講中小型公司即便想要實現本身的一套組件風格的東西,直接使用開源類庫如 element、iview或者基於react的Antd 所提供的功能和交互邏輯,而後在其上進行主題定製基本就知足需求了(除非你家設計師頗有想法。。。)。
自定義主題的功能通常的使用方式是這樣的
經過主題生成工具。(製做者須要單獨作一個工具)
引入關鍵主題文件,覆蓋主題變量。(這種方式通常都須要適配製做者所使用的css預處理器)
對於第一種方式每每都是組件庫的製做者經過把生成組件樣式的那一套東西作成一個工具,而後提供給使用者去根據本身的須要來調整,最後生成一套特定的樣式文件,引入使用。
第二種方式,做爲使用者來講,你主要作的實際上是覆蓋了組件庫中的一些主題變量,由於具體的組件的樣式文件不是寫死的固定樣式值,而是使用了定義好的變量,因此你的自定義主題就生效了。可是這也會引入一個小問題就是你必須適配組件庫的創造者所使用的樣式預處理器,好比你用iview,那你的項目就要能解析Less文件,你用ElementUI,你的項目就必須能夠解析SCSS。
其實對於第一種方式也主要是以調整主題變量爲主。因此當我們本身要作一套組件庫的時候,不難看出,一個核心點就是須要把主題變量文件和樣式文件拆開來,後面的就簡單了。
webpack打包
類庫項目的構建這裏提兩點:
暴露入口
外部化依賴
先談第一點 「暴露接口」。業務項目中,咱們的整個項目經過webpack或其餘打包工具打包成一個或多個bundle文件,這些文件被瀏覽器載入後就會直接運行。可是一個類庫項目每每都不是單獨運行的,而是經過暴露一個 「入口」,然我在業務項目中去調用它。 在webpack配置文件裏,能夠經過定義 output 中的 library 和 libraryTarget 來控制咱們要暴露的一個 「入口變量」 ,以及咱們要構建的目標代碼。
這一點能夠詳細參考webpack官方文檔: webpack.js.org/configurati…
module.exports = {
// other config
output: {
library: "MyLibName",
libraryTarget: "umd",
umdNamedDefine: true
}
}
複製代碼
再說一下 「外部化依賴」,咱們作一個vue組件庫項目的時候,咱們的組件都是依賴於vue的,當咱們組件庫項目中的某個地方引入了vue,那麼打包的時候vue的運行時也是會被一起打包進入最終的組件庫bundle文件的。這樣的問題在於,咱們的vue組件庫是被vue項目使用的,那麼項目中已經有運行時了,咱們就不必在組件庫中加入運行時,這樣會多增長組件庫bundle的體積。使用webpack的 externals能夠將vue依賴 "外部化"。
module.exports = {
// other config
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
}
}
複製代碼
按需加載
組件庫的按需加載功能仍是很實用的, 這樣能夠避免咱們在使用組件庫的過程當中把全部的用到和沒用到的內容都打包到業務代碼中去,致使最後的bundle文件過大影響用戶體驗。
在業務項目中咱們的按需加載都是把須要按需加載的地方單獨生成爲一個chunk,而後瀏覽器運行咱們的打包代碼的時候發現咱們須要這一起資源了,再發起請求獲取到對應的所需代碼。
在組件庫裏邊,咱們就須要改變一下引入的方式,好比一開始咱們引入一個組件庫的時候是直接將組件庫和樣式所有引入的。以下面這樣
import LimeUI from 'lime-ui' // 引入組件庫
import 'lime-ui/styles/index.css' // 引入整個組件庫的樣式文件
Vue.use(LimeUI)
複製代碼
那麼,換成手動的按需加載的方式就是
import { Button } from 'lime-ui' // 引入button組件
import 'lime-ui/styles/button.css' // 引入button的樣式
Vue.component('l-button', Button) // 註冊組件
複製代碼
這種方式的確是按需引入的,但也一個不舒服的地方就是每次咱們引入的時候都須要手動的引入組件和樣式。通常來講一個項目裏面用到的組件少說也有十多個,這就比較麻煩了。組件庫是怎麼解決這個問題的呢?
經過babel插件的方式,將引入組件庫和組件樣式的模式自動化,好比antd、antd-mobile、material-ui都在使用的babel-plugin-import、還有ElementUI使用的 babel-plugin-component。在業務項目中配置好babel插件以後,它內部就能夠給你作一個這樣的轉換(這裏以 babel-plugin-component)
// 原始代碼
import { Button } from 'components'
// 轉換代碼
var button = require('components/lib/button')
require('components/lib/button/style.css')
複製代碼
OK,那既然代碼能夠作這樣的轉換的話,其實咱們所要作的一點就是在咱們打造組件庫的時候,把咱們的組件庫的打包代碼放到對應的文件目錄結構之下就能夠了。使用者能夠選擇手動載入組件,也可使用babel插件的方式優化這一步驟。
babel-plugin-component 文檔: www.npmjs.com/package/bab…
babel-pluigin-import 文檔: www.npmjs.com/package/bab…
組件:比起平常的組件設計,作組件庫你還須要知道些什麼?
作組件庫中的組件的技巧和在項目中用到的仍是有一些區別的,這一小節就是告訴你們,組件庫中的組件設計,咱們還應該知道哪些必要的知識內容。
組件通訊:除了上下級之間進行數據通訊,還有什麼?
咱們常規用到的組件通訊的方法就是經過 props 和 $emit 來進行父組件和子組件之間的數據傳遞,以下面的示意圖中展現的那樣:父組件經過 props 將數據給子組件、子組件經過 $emit 將數據傳遞給父組件,頂多經過eventBus或Vuex來達到任意組件之間數據的相互通訊。這些方法在常規的業務開發過程當中是比較有效的,可是在組件庫的開發過程當中就顯得有點力不從心了,主要的問題在於: 如何處理跨級組件之間的數據通訊呢?
ç¸å
³å¾ç
若是在平常項目中,咱們固然可使用像 vuex 這樣的將組件數據直接 」外包「 出去的方式來實現數據的跨級訪問,可是vuex 始終是一個外部依賴項,組件庫的設計確定是不能讓這種強依賴存在的。下面咱們就來講說兩個在組件庫項目中咱們會用到的數據通訊方式。
內置的provide/inject
provide/inject 是vue自帶的能夠跨級從子組件中獲取父級組件數據的一套方案。 這一對東西相似於react裏面的 Context ,都是爲了處理跨級組件數據傳遞的問題。
使用的時候,在子組件中的 inject 處聲明須要注入的數據,而後在父級組件中的某個含有對應數據的地方,提供子級組件所須要的數據。無論他們之間跨越了多少個組件,子級組件都能獲取到對應的數據。(參考下面的僞代碼例子)
// 引用關係 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) // 打印結果: dark
}
}
複製代碼
不過provide/inject的方式主要是子組件從父級組件中跨級獲取到它的狀態,卻不能完美的解決如下問題:
子級組件跨級傳遞數據到父級組件
父級組件跨級傳遞數據到子級組件
派發和廣播: 自制dispatch和broadcast功能
dispatch和broadcast能夠用來作父子級組件之間跨級通訊。在vue1.x裏面是有dispatch和broadcast功能的,不過在vue2.x中被取消掉了。這裏能夠參考一下下面連接給出的v1.x中的內容。
dispatch的文檔(v1.x):v1.vuejs.org/api/#vm-dis…
broadcast文檔(v1.x):v1.vuejs.org/api/#vm-bro…
根據文檔,咱們得知
dispatch會派發一個事件,這個事件首先在本身這個組件實例上去觸發,而後會沿着父級鏈一級一級的往上冒泡,直到觸發了某個父級中聲明的對這個事件的監聽器後就中止,除非是這個監聽器返回了true。固然監聽器也是能夠經過回調函數獲取到事件派發的時候傳遞的全部參數的。這一點很像咱們在DOM中的事件冒泡機制,應該不難理解。
而broadcast就是會將事件廣播到本身的全部子組件實例上,一層一層的往下走,由於組件樹的緣由,往下走的過程會遇到 「分叉」,也就能夠當作是一條條的多個路徑。事件沿着每個子路徑向下冒泡,每一個路徑上觸發了監聽器就中止,若是監聽器返回的是true那就繼續向下再傳播。
簡單總結一下。dispatch派發事件往上冒泡,broadcast廣播事件往下散播,遇處處理對應事件的監聽器就處理,監聽器沒有返回true就中止
須要注意的是,這裏的派發和廣播事件都是 跨層級的 , 並且能夠攜帶參數,那也就意味着能夠跨層級進行數據通訊。
dispatch
因爲dispatch和broadcast在vue2.x中取消了,因此咱們這裏能夠本身寫一個,而後經過mixin的方式混入到須要使用到跨級組件通訊的組件中。
方法內容其實很簡單,這裏就直接列代碼
// 參考自iview的實現
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); }
}
};
複製代碼
其實這裏的實現和vue1.x中的實現仍是有必定的區別的:
dispatch沒有事件冒泡。找到哪一個就直接執行
設定了一個name參數,只針對特定name的組件觸發事件
其實看懂了這裏的代碼,你就應該能夠觸類旁通想出 找尋任何一個組件的方法了,無論是向上仍是向下找,無非就是循環遍歷和迭代處理,直到目標組件出現,而後調用它。 派發和廣播無非就是找到以後利用vue自帶的事件機制來發布事件,而後在具體組件中監聽該事件並處理。
渲染函數:它能夠釋放javascript的能力
首先咱們回顧一下一個組件是如何從寫代碼到被轉換成界面的。咱們寫vue單文件組件的時候通常會有template、script和style三部分,在打包的時候,vue-loader會將其中的template模板部分先編譯成Vue實例中render選項所須要的構建視圖的代碼。在具體運行的時候,vue運行時會使用$mount 進行渲染,渲染好以後將其掛載到你提供的DOM節點下。
整個過程裏面咱們只平常關注最多的固然就是template的部分,可是template其實只是vue提供的一個語法糖,只是讓咱們寫代碼寫起來跟寫html同樣輕鬆,下降剛入手vue的小夥伴的學習成本。React就沒有提供template的語法糖,而是使用的JSX來下降寫組件的複雜度。(vue能在react和angular兩大框架的壓力下異軍突起,簡潔易懂的模板語法是有必定促進做用的,畢竟看起來更簡單)
經過上面咱們回顧的內容,其實咱們也發現了,咱們寫的template,最終都是javascript。這裏template被編譯以後,給到了 render這個渲染函數,在執行渲染的時候vue就會執行render中的操做來渲染咱們的組件。
因此template是好,但若是你想要使用所有的javascript的能力,那就可使用渲染函數。
渲染函數&JSX (官方文檔):cn.vuejs.org/v2/guide/re…
平常寫業務組件,咱們用template就挺OK的,不過當遇到一些複雜狀況,用 寫組件 --> 引入使用 --> 註冊組件 --> 使用組件 的方式就很差處理了,好比下面兩種狀況:
經過代碼動態渲染組件
將組件渲染到其餘位置
第一種狀況是經過代碼動態渲染組件,好比運營經常使用的活動h5頁面,每一個活動都不同,每次要麼都從新作一份,要麼在原有的基礎上修改。可是這種修改的頁面結構調整是很大的,每次都會是破壞性的,和重作其實沒區別。這樣的話,每次活動不管內容如何,前端都要上手去寫代碼。但其實只須要在管理後臺作一個活動編輯器,編輯器的內容直接轉化爲render函數的代碼,而後經過配置下發到某個頁面上,承載頁拿到數據給到render函數執行渲染。這樣就能夠動態的根據管理後臺配置的方式來渲染組件內容,每次的活動頁,運營也能夠經過編輯器自行生成。
第二種狀況是要將組件渲染到不一樣位置。咱們平常寫業務組件基本就是寫一個組件,在須要的拿來使用。若是你只是在template中把組件寫進去,那你的組件的內容就都會做爲當前組件的子組件進行渲染,所生成的DOM結構也是在當前的DOM結構之下的。知道render以後,其實咱們能夠新建vue實例,動態渲染以後,手動掛載到任意的DOM位置上去。
import CompA from './CompA.vue'
let Instance = new Vue({
render(h) {
return h(CompA)
}
})
let component = Instance.$mount() // 執行渲染
document.body.appendChild(component.$el) // 掛載到body元素下
複製代碼
咱們使用的element裏面的 this.$message 就用到了動態渲染,而後手動掛載到指定位置。
實踐:作一遍你就會了
這裏先貼上咱們的github地址,各位能夠在作的過程當中對照着看。github.com/arronKler/l…
創建一個工程化的項目
第一步,創建工程化結構
這裏就不廢話了,直接貼目錄結構和解釋
|- assets/ # 存放一些額外的資源文件,圖片之類的
|- build/ # webpack打包配置
|- docs/ # 存放文檔
|- .vuepress # vuepress配置目錄
|- component # 組件相關的文檔放這裏
|- README.md # 靜態首頁
|- lib/ # 打包生成的文件放這裏
|- styles/ # 打包後的樣式文件
|- src/ # 在這裏寫代碼
|- mixins/ # mixin文件
|- packages/ # 各個組件,每一個組件是一個子目錄
|- styles/ # 樣式文件
|- common/ # 公用的樣式內容
|- mixins/ # 複用的mixin
|- utils # 工具目錄
|- index.js # 打包入口,組件的導出
|- test/ # 測試文件夾
|- specs/ # 存放全部的測試用例
|- .npmignore
|- .gitignore
|- .babelrc
|- README.md
|- package.json
複製代碼
這裏比較重要的目錄就是咱們的src目錄,下面存放了咱們的各個單一的組件和一套樣式庫,另外還有一些輔助的東西。咱們寫文檔就是在 docs目錄下去寫。項目目錄最外層都是些常規的配置內容,好比 .npmignore 和 .gitignore 這樣的文件咱們都是很常見的,因此我就不具體細說這一部分了,要是有必定疑惑能夠直接參見github上的源碼對照着看。
這裏咱們把須要使用到的類庫文件也先創建好
在 src/mixins 下建立一個 emitter.js,寫入以下內容,也就是咱們的dispatch和broadcast的方法,以後的組件設計中會用到
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); }
}
};
複製代碼
而後在 src/utils 下新建一個 assist.js 文件,寫下輔助性的函數
export function oneOf(value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) {
return true;
}
}
return false;
}
複製代碼
這兩個地方都是以後會使用到的,若是你須要其餘的輔助內容,也能夠在這兩個文件所在的目錄下去創建。
第二步, 完善打包流程
目錄建好了,那就該填充血肉了,要打包一個組件庫項目,確定是要先配置好咱們的webpack,否則寫了源碼也無法跑起來。因此咱們先定位到 build目錄下,在build目錄下先創建三個文件
webpack.base.js 。存放基本的一些rules配置
webpack.prod.js 。整個組件庫的打包配置
gen-style.js 。單獨對樣式進行打包
如下是具體的配置內容
/ 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()
]
};
複製代碼
/ 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 做爲入口點
},
output: {
path: path.resolve(dirname, '../lib'),
publicPath: '/lib/',
filename: 'lime-ui.min.js', // 改爲本身的類庫名
library: 'lime-ui', // 類庫導出
libraryTarget: 'umd',
umdNamedDefine: true
},
externals: { // 外部化對vue的依賴
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
}
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
})
]
});
複製代碼
/ 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)
複製代碼
OK,這裏咱們的webpack配置基本設置好了,webpack.base.js 中的配置就主要是一些loader和插件的配置,具體的出入口都是在 webpack.prod.js 中配置的。這裏webpack.prod.js 合併了 webpack.base.js 中的配置項。關於 output.libary 和 externals ,閱讀了以前 「準備」 階段的內容的應該不會陌生了。
另外還有 gen-style.js 這個文件是單獨使用了 gulp 來對樣式文件進行打包操做的,咱們這裏選用的是 scss的語法,若是你想用less或其餘的預處理器,也能夠自行修改這裏的文件和相關依賴。
不過這個配置確定尚未結束,首先咱們須要安裝好這裏的配置裏使用到的各類loader和plugin。爲了避免漏掉安裝項和保持一致性,能夠直接複製下面的配置內容放到 package.json 下,經過 npm install 來進行安裝。須要注意的是,這裏的安裝完成以後,其實後面的一些內容的依賴也都一併安裝好了。
"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"
},
複製代碼
另外,因爲咱們使用了babel,因此須要在項目的根目錄下設置一下 .babelrc 文件,內容以下:
{
"presets": [br/>[
"@babel/preset-env",
{
"loose": false,
"modules": "commonjs",
"spec": true,
"useBuiltIns": "usage",
"corejs": "2.6.9"
}
]
],
"plugins": [br/>"@babel/plugin-transform-runtime",
]
}
複製代碼
固然也不要忘記在package.json文件中寫上scripts簡化手動輸入命令的過程
{
"scripts": {
"build:style": "gulp --gulpfile build/gen-style.js",
"build:prod": "webpack --config build/webpack.prod.js",
}
}
複製代碼
第三步,創建文檔化工具
若是在上一步中未安裝了 vuepress ,能夠經過 npm install vuepress --save-dev 來安裝,
而後在 package.json 中加入腳本,快速啓動
{
"scripts": {
// ...
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}
}
複製代碼
這個時候你能夠在你的 docs/README.md 文件裏寫點內容,而後運行 npm run docs:dev 就能夠看到本地的文檔內容了。須要打包的時候使用 npm run docs:build 就能夠了。
若是咱們的項目是要放到github上的,那麼其實也能夠一併將咱們的文檔生成以後也放到github上去,利用github的pages功能讓這個本地的文檔在線運行。(github pages託管咱們的靜態頁面和資源)
能夠運行 npm install gh-pages --save-dev 安裝 gh-pages 這個能夠幫咱們一鍵部署github pages文檔的工具。它的工做原理就是將對應的某個文件夾下的資源遷移到咱們的當前項目的gh-pages分支上,而後這個分支在push給了github以後,github就會將該分支內的內容服務起來。爲了更好的使用它,咱們能夠在package.json中添加scripts
{
"scripts": {
// ...
"deploy": "gh-pages -d docs/.vuepress/dist",
"deploy:build": "npm run docs:build && npm run deploy",
}
}
複製代碼
這樣你就可使用 npm run deploy 直接部署你的vuepress生成的靜態站點,不過務必在部署以前運行一下文檔的構建程序。所以咱們也添加了一條 npm run deploy:build 命令,使用這條命令就能夠直接把文檔的構建和部署直接一塊兒解決。是否是很簡單呢?
不過爲了咱們可以直接使用本身寫的組件,還須要對vuepress作一點點配置。在 docs/.vuepress目錄下新建一個 enhanceApp.js 文件,寫入以下內容,將咱們的組件庫的入口和樣式注入進去
import LimeUI from '../../src/index.js'
import "../../src/styles/index.scss"
export default ({
Vue,
options,
router
}) => {
Vue.use(LimeUI)
}
複製代碼
這個時候咱們以後寫的組件就能夠直接在文檔中使用了。
第四步,樣式構建
先須要說明的是這裏咱們所使用的樣式預處理器的語法是scss。那麼在「完善打包流程」這一小節中已經將用gulp進行打包的代碼給出了,不過有必要說明一下,咱們又是如何去整合樣式內容的。
首先,爲了以後便於作按需加載,對於每一個組件的樣式都是一個單獨的scss文件,寫樣式的時候,爲了不太多的層級嵌套,使用了BEM風格的方式去書寫。
咱們須要先在 src/styles目錄執行以下命令生成一個基本的樣式文件
cd src/styles
mkdir common
mkdir mixins
touch common/var.scss # 樣式變量文件
touch common/mixins.scss
touch index.scss # 引入全部樣式
複製代碼
而後將對應的 var.scss 和 mixins.scss 文件填充上一些基礎內容
/ 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;
複製代碼
/ mixins/mixins.scss /
$namespace: 'lime'; / 組件庫的樣式前綴 /
/ BEM
-------------------------- /
@mixin b($block) {
$B: $namespace+'-'+$block !global;
.#{$B} {br/>@content;
}
}
複製代碼
在mixins文件中咱們聲明瞭一個mixin,用於幫助咱們更好的去構建樣式文件。
組件打造案例
上面的內容設置好了, 我們就能夠開始具體去作一個組件試試了
簡單的button組件
這是作好以後的大體效果
button
OK,那咱們創建基本的button組件相關的文件
cd src/packages
mkdir button && cd button
touch index.js
touch button.vue
複製代碼
寫入button.vue的內容
<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>
複製代碼
這裏咱們須要在 index.js 中導出這個組件
import Button from './button.vue'
export default Button
複製代碼
這樣單個的一個組件就完成了,以後你能夠再多作幾個組件試試,不過有一點就是這些組件須要一個統一的打包入口,咱們再webpack中已經配置過了,那就是 src/index.js 這個文件,咱們須要在這個文件裏面將咱們剛纔寫的button組件以及你本身寫的其餘組件都引入進來,而後統一導出給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
複製代碼
能夠看到的是index.js中咱們最終導出的是一個叫install的函數,這個函數其實就是Vue插件的一種寫法,便於咱們在實際項目中引入的時候可使用 Vue.use 的方式來自動安裝咱們的整個組件庫。install接受兩個參數,一個是Vue,咱們把它用來註冊一個個的組件。還有一個是options,便於咱們能夠在註冊組件的時候傳入一些初始化參數,好比默認的按鈕大小、主題等信息,均可以經過參數的方式來設定。
而後咱們能夠在 src/styles目錄下新建一個button.scss 文件,寫入咱們button對應的樣式
/ 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); }
}
}
複製代碼
最後咱們還須要在 src/styles/index.scss 文件中將button的樣式引入進去
@import "button";
複製代碼
爲了簡單的實驗,你能夠直接在 docs/README.md 文件下寫兩個button組件試試看
<template>
<l-button type="primary">Click me</l-button>
</template>
複製代碼
若是你想要獲得和我在 arronkler.github.io/lime-ui/ 上同樣的效果,能夠參考 github.com/arronKler/l… 項目中的 docs 目錄下的配置。若是想要更個性化的配置,能夠查閱vuepress的官方文檔。
Notice提示組件
這個組件就要用到咱們的動態渲染的相關的東西了。具體最後的使用方式是這樣的
this.$notice({
title: '提示',
content: this.content || '內容',
duration: 3
})
複製代碼
效果相似於這樣
button
OK,咱們先來寫一下這個組件的一個基本源碼
在 src/packages 目錄下新建notice文件夾,而後新建一個 notice.vue 文件
<template>
<div class="lime-notice">
<div class="lime-noticemain" v-for="item in notices" :key="item.id">
<div class="lime-noticetitle">{{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>
複製代碼
代碼很簡單,其實就是聲明瞭一個容器,而後在其中經過控制 notices 的數據來展現和隱藏,接着咱們在同一個目錄下新建一個notice.js 文件來作動態渲染
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)
}
複製代碼
這裏咱們咱們經過動態渲染的方式讓咱們的組件能夠直接掛在到body下面,而非歸屬於根掛載點之下。
而後在 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;
}
}
複製代碼
最後一樣的,也須要在 src/index.js 這個入口文件中對 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
複製代碼
咱們能夠看到咱們再Vue的原型上掛上了咱們的 $notice 方法,這個方法調用的時候就會觸發咱們在 notice.js 文件中動態渲染組件的一套流程。這個時候咱們就能夠在 docs/README.md 文檔中測試着用了。
<script>
export default() {
mounted() {
this.$notice({
title: '提示',
content: this.content,
duration: 3
})
}
}
<script>
複製代碼
單獨打包樣式和組件
爲了能支持按需加載的功能,咱們除了將整個組件庫打包以外,還須要對樣式和組件單獨打包成單個的文件。這裏咱們須要作兩件事兒
打包單獨的css文件
打包單獨的組件內容
對於第一點,咱們須要對 build/gen-style.js 文件作一下改造,加上buildSeperateCss任務,完整代碼以下
// 其餘以前的代碼...
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
複製代碼
對於第二點,咱們能夠用一個新的webpack配置來處理,新建一個 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"'
})
]
});
複製代碼
這裏咱們引用了build文件夾下的一個叫作 component.json 的文件,該文件是我自定義用來標識咱們的組件和組件路徑的,實際上你也能夠經過腳本直接遍歷 src/packages目錄自動得到這樣一些信息。這裏只是簡單演示, build/component.json 的代碼以下
{
"button": "packages/button/index.js",
"notice": "packages/notice/notice.js"
}
複製代碼
全部的單獨打包流程配置好之後,咱們就能夠在 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",
}
}
複製代碼
OK,如今只須要運行 npm run dist 命令,它就會自動去構建完整的樣式內容和各個組件單獨的樣式內容,而後會打包一個完整的組件包和各個組件的單獨的包。
這裏須要注意的一點就是你的package.json 文件中的這幾個字段須要作一下調整
{
"name": "lime-ui",
"version": "1.0.0",
"main": "lib/lime-ui.min.js",
//...
}
複製代碼
其中name表示別人使用了你的包的時候的包名,main字段很重要,表示別人直接引入你包的時候,入口文件是哪個。這裏由於咱們webpack打包後的文件是 lib/lime-ui.min.js 因此咱們這樣去設置。
一切就緒後,你就能夠運行 npm run dist 打包你的組件庫,而後 npm publish 去發佈你的組件庫了(發佈前須要 npm login 登錄)
使用本身的組件庫
直接使用
咱們能夠用vue-cli 或其餘工具另外生成一個demo項目,用這個項目去引入咱們的組件庫。若是你的包尚未發佈出去,能夠在你的組件庫項目目錄下 用 npm link 或者 yarn link的命令建立一個link(推薦使用yarn)
而後在你的demo目錄下使用 npm link package_name 或者 yarn link package_name 這裏的package_name就是你的組件庫的包名,而後在你的demo項目的入口文件裏
import Vue from vue
import LimeUI from 'lime-ui'
import 'lime-ui/lib/styles/lime-ui.css'
// 其餘代碼 ...
Vue.use(LimeUI)
複製代碼
這樣設置好以後,咱們建立的組件就能夠在這個項目裏使用了
按需加載
上面咱們談的是全局載入的一種使用方法,那如何按需加載呢?其實咱們以前也說過那麼一點
先經過npm安裝好 babel-plugin-component 包,而後在你的demo項目的 .babelrc 文件中寫上這部份內容
{
"plugins": [
["component", {
"libraryName": "lime-ui",
"libDir": "lib",
"styleLibrary": {
"name": "styles",
"base": false, // no base.css file
"path": "[module].css"
}
}]
]
}
複製代碼
這裏的配置是要符合咱們的lime-ui 的一個目錄結構的,有了這個配置咱們就能夠進行按需加載了,你能夠像這樣作加載一個Button
import Vue from 'vue'
import { Button } from 'lime-ui'
Vue.component('a-button', Button)
複製代碼
能夠看到的是,咱們並無在這個位置加載任何樣式,由於 babel-plugin-component 已經幫咱們作了,不過由於咱們只在組件庫的入口點裏面設置了 install 方法用來註冊組件,因此這裏咱們按需引入的時候,就須要本身手動註冊了。
主題定製
前面的內容作好以後,主題定製就比較簡單了,咱們先在DEMO項目的入口文件同級目錄下建立一個 global.scss 文件,而後在其中寫入相似下面這樣的代碼。
$--color-primary: red;
@import "~lime-ui/src/styles/index.scss";
複製代碼
而後在入口文件中把引入組件庫的方式改變一下
import Vue from vue
import LimeUI from 'lime-ui'
import './global.scss'
// 其餘代碼 ...
Vue.use(LimeUI)
複製代碼
咱們在入口文件中把對組件庫的樣式引入,改爲引入咱們自定義的global.scss文件。
其實這裏就是覆蓋了咱們在組件庫項目裏 var.scss 裏的變量的值,而後其他的組件基礎樣式仍是使用了各自的樣式內容,這樣就能夠達到主題定製了。
結語
本文經過對組件庫的一些特性的介紹和一個實際的操做案例,闡述了打造一套組件庫的一些基礎的東西。但願能經過這樣的一次分享,讓咱們不僅是去使用組件庫,而是能知道組件庫的誕生過程和了解組件庫的一些內部特性,幫助咱們在平常使用的過程當中能「心中有數」,當出現問題或組件庫需求可能不知足的時候有一個新的思考入手點,那就足夠了。
引用參考