在之前傳統的前端頁面開發方式時,存在協同困難,可複用性差的問題,致使開發和維護都不是一件簡單的事。而組件化思想的提出,以及Vue、React等MV*框架的快速流行,讓咱們開始嘗試用組件化的思想去開發。因爲筆者最近在研究組件庫的搭建,故撰文記之。
組件化思想讓咱們把頁面劃分爲一個個組件,組件內部維護本身的UI展現、交互邏輯,這樣將能夠大大提升代碼的複用性以及可維護性。css
本文將着重介紹組件庫搭建過程當中的準備工做,包括定義合理的項目結構、組件庫打包構建、實現按需加載以及組件庫所須要完善的其餘工做等,但願對讀者有所幫助~(本文將以vue組件庫爲案例敘述)前端
首先你須要爲你的組件庫定義一個合理的項目結構,合理的項目結構對後期的代碼維護和管理十分有幫助。你也能夠先使用vue-cli初始化一個項目結構,這裏筆者爲了對組件庫單獨分割出來管理,在根目錄下定義一個components文件夾,組件樣式統一放置於該文件夾下的theme-chalk下,項目主要結構以下:vue
能夠看到,文件結構中有兩個關鍵點,一個是組件庫入口,一個是單組件入口,組件庫入口須要對項目組件進行註冊,同時暴露對象中要含有install方法用於被Vue.use的時候調用。組件庫入口文件以下:node
import helloworld from "../components/helloworld/index.js"; import test from "../components/test/index.js"; import { version } from '../package.json'; const components = { helloworld, test } const install = function (Vue) { if (install.installed) return; Object.keys(components).forEach(key => { Vue.component(components[key].name, components[key]); }) install.installed = true; }; if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } export { helloworld, test } export default { version, install };
其實作的事情很簡單,把組件讀取進來後,進行統一的Vue.component註冊,以後暴露install方法便可。同理,componentA文件夾下的index.js,僅針對A組件作處理,以下:webpack
import helloworld from "./src/main.vue" helloworld.install = function(Vue) { Vue.component(helloworld.name, helloworld); }; export default helloworld;
聰明的你可能已經察覺到了,前面講樣式單獨分離出來到theme-chalk下管理,是爲何?爲何這裏又沒有引入樣式呢?git
一、不嵌套在vue組件的style標籤中書寫樣式,方便對組件樣式的單獨打包,對於按需加載十分有意義;github
二、單獨分離出樣式組件進行管理,方便後續對組件進行換膚;web
三、單獨分離出樣式文件,方便統一管理。vue-cli
所以,若是你也要對本身的組件提取爲組件庫的話,強烈建議將樣式單獨分離出來處理。npm
項目結構和入口文件都定義好了,接下來就要考慮打包構建,讓咱們的項目跑起來了。
開發環境只須要配置好webpack,利用devserver即可以開始調試,若是你是用vue-cli初始化項目的話,直接npm run serve便可。
在正式發佈部署的時候,問題開始逐漸變得複雜起來,不一樣於以往的項目,你只須要配置好一份webpack.config.js打包文件,區分開發和發佈環境,就能夠進行打包構建了,可是組件庫或者第三方庫不同,你須要將打包構建的結果提供給開發者使用,做爲一個貼心的庫提供者,你須要考慮如下問題:
一、你可能須要提供不一樣模塊類型的包:commonjs、umd、es模塊;
二、你須要對各組件單獨打包處理,方便用戶按需加載;
三、因爲須要實現按需加載,不避免的,你須要對樣式也單獨打包處理;
四、提供打包壓縮後的.min.js文件
因爲這是一個第三方庫,而且這是一個組件庫,使得咱們對打包這件事變得束手束腳,咱們須要針對不一樣的狀況進行打包,提供豐富的打包產物給用戶,讓用戶想要啥有啥。爲了明確咱們打包構建所須要的產物,筆者畫了一張示意圖:
或許這樣,你會更明白咱們的打包任務是什麼,相比以往的webpack一把梭,已經梭不了了,在咱們的打包產物中,存在es模塊,而webpack自己打包不支持導出es模塊,因此最終的打包構建咱們只能藉助於rollup了。(你可能爲問,爲何咱們要執着於打包出es模塊的包?其實,大部分的第三庫都已經利用rollup進行打包,除了es模塊將成爲將來的緣由之外,es模塊對於tree-shaking有極大意義,後文將介紹利用其進行按需加載)接下里咱們剖析一下每一個任務要怎麼完成(按照圖中所標序號):
一、構建任務一:導出組件庫總包,利用rollup。若是你不稀罕es模塊的導出的話,請選擇webpack一把梭,這裏爲了提供es模塊的導出,向主流看齊,咱們選擇用rollup進行打包。
二、構建任務二:對各組件單獨打包,利用webpack。還記得咱們在第一節中爲每一個組件預留了一個index.js的入口嗎?沒錯,這就是爲了按需加載埋下的伏筆,本覺得咱們能夠用rollup就進行打包的,可是rollup的entry只支持string形式,若是我有100個組件,難道要執行一百次rollup指令嗎...不要緊,對於單獨打包,仍是webpack香,經過webpack的entry配置hash對象,即可以對各個組件進行單獨打包。(這個時候就沒法導出es模塊了,可是不要緊,畢竟只是單個組件的引入,不treeshaking了唄hh)
三、構建任務三與構建任務四:對樣式統一打包和單獨打包,利用gulp。因爲咱們須要對css文件單獨打包,不管是rollup仍是webpack都不能把打包入口指定爲css文件,因此咱們只能藉助gulp來打包css了。
最後打包文件是如下幾個:
└── build ├── gulp.css.js // 針對css的文件處理打包 ├── rollup.config.js // 利用rollup進行最終產物打包 ├── webpack.dev.js // 開發模式配置 本地起dev server └── webpack.component.js // 利用webpack對各組件單獨打包
webpack.component.js配置以下:
... // 讀取components文件夾下的全部文件 const fs = require('fs'); const items = fs.readdirSync('./components'); const dirs = items.filter(item => { return fs.statSync(path.resolve('./components', item)).isDirectory() }) const entryHash = {} if(dirs.length > 0){ dirs.forEach(ele=>{ // css不做處理 if(ele !== "theme-chalk"){ entryHash[ele] = `./components/${ele}/index.js` } }) } // 不打包第三方模塊內容 var externals = [Object.assign({ vue: 'vue' }), nodeExternals()]; module.exports = { ... entry: entryHash, ... externals:externals, ... optimization: { minimize: false, } }
大部分是常規的配置,可是這裏注意一點,在實踐過程當中,筆者對一個簡單邏輯的componentA進行單獨打包,結果打包出4000+行的代碼,一度感受到生命的絕望,實際上是由於沒有設置external,把第三庫的內容也打包進去了。因爲咱們約定components路徑下存放組件,因此直接經過讀取文件夾下的文件名來建立hash對象,而不須要本身手動維護,可謂一勞永逸!
其餘都是常規的配置,針對js和css單獨打包,配置好rollup和gulp的配置文件,因爲篇幅有限,這裏再也不展現rollup、gulpfile的配置。有興趣的能夠戳個人github看看配置。https://github.com/handsomeguy/oleiwa-demo
最後,咱們的package.json中是這樣的配置的:
"gulp": "npx gulp css && npx gulp all", "rollupbuild:es": "npx rollup --config ./build/rollup.config.js", "rollupbuild:umd": "format=umd npx rollup --config ./build/rollup.config.js", "rollupbuild:min": "minify=true npx rollup --config ./build/rollup.config.js", "build:comp": "npx webpack --config ./build/webpack.component.js", "build": "npm run gulp && npm run rollupbuild:es && npm run rollupbuild:umd && npm run rollupbuild:min && npm run build:comp", "serve": "vue-cli-service serve",
後編譯指的是在發佈npm依賴包的時候,不進行編譯構建,跟隨npm包把源碼也一塊兒發出去,以後讓用戶直接引用未編譯的源文件,自行打包編譯。業界提倡後編譯的典範即是cube-ui。
後編譯帶來的既有好處也有壞處。
優勢:
一、共用公共依賴。
二、bebal轉碼只有一次,減小代碼量。
三、方便換膚功能實現。(直接針對源碼sass編譯)
缺點:
一、用戶的打包配置要兼容,甚至須要額外作配置。
二、配置頗有多是侵入式的,對於用戶的接入成本過大。
筆者我的認爲,大部分人都會傾向於選擇易於接入的組件庫,我的並不推薦後編譯,可是後編譯其實仍是有它的做用的,例如後面咱們要介紹的換膚功能,其實本質就是一種後編譯,只不過咱們只針對css文件作了後編譯,你須要暴露出一個源碼的scss文件入口。
跟大部分的組件庫同樣,咱們只須要這樣,即可以全量引入咱們的組件庫:(注意樣式文件單獨引入)
import oleiwa from "@tencent/oleiwa"; import "@tencent/oleiwa/dist/css/index.css"; Vue.use(oleiwa)
按需引入這個問題,其實不單出如今組件庫中,大部分的第三方庫都會面臨這個問題,用戶只須要其中一部分的功能,你要怎麼幫助他剔除無用的模塊,做爲庫提供者,你須要作的就是細分模塊,讓用戶能只引入本身須要的功能模塊。工具庫中的lodash即是一個很好的栗子。回到正題,咱們要怎麼讓咱們組件庫可以按需引入?
最直接、最粗暴的方式,即是直接指定組件路徑和樣式路徑。
import helloworld from "@tencent/oleiwa/dist/helloworld.js" import "@tencent/oleiwa/dist/css/base.css" import "@tencent/oleiwa/dist/css/helloworld.css" Vue.use(helloworld)
能夠看到,經過直接指定路徑的方式,咱們須要再手動引入css樣式,並且還不能落了base.css這個樣式文件。
第一種方式,簡單粗暴,可是你必定不但願你的用戶寫着又臭又長的路徑,嘴裏一邊咒罵:這哪一個XX寫的組件庫。因此還有一種hack的方式,幫助咱們來實現按需引入,利用plugin的方式,對引入路徑作替換,幫助咱們引入須要的組件以及樣式。
目前業界的處理方式:
其本質都是在編譯階段,針對引用路徑作替換。例如:
import { Button } from 'components'
將被替換成如下代碼:
var button = require('components/lib/button') require('components/lib/button/style.css')
固然,路徑並非固定的,以babel-plugin-component爲例,它容許咱們對lib和樣式文件地址進行配置。
可是因爲配置能力有限,咱們的組件必須放置在lib路徑下,(實際咱們打包在dist路徑下,路徑可修改)可是組件不容許再嵌套一層路徑了,因此咱們須要把咱們的打包配置稍做修改,將每一個組件的打包結果都導出到dist路徑下。
最後,用戶使用起來的時候,只須要安裝好plugin依賴,配置babel.config.js文件以下,即可以實現按需引入:
module.exports = { presets: ["@vue/app"], "plugins": [["component", { libraryName: "@tencent/oleiwa", libDir:"dist", styleLibraryName:"css", }]] };
使用以下(注意:按需引入狀況下,插件自己會默認幫你導入base.css,因此你也不用擔憂base文件的問題):
import {helloworld} from "@tencent/oleiwa"; Vue.use(helloworld)
sideEffects是webpack4新增的一個特性,須要咱們在package.json中進行配置,其主要做用是告訴webpack咱們這個包有沒有反作用。什麼是反作用,簡單的說就是其導出的模塊是否對其之外的模塊或變量形成影響。例如是否修改了window上的屬性,是否複寫了原生對象 Array, Object 方法,是否修改了其自己所導出的其餘模塊等,若是你還想了解更多,能夠戳這裏。
其實早些時候,還有這樣一篇文章,你的treeshaking並無什麼卵用 [](https://zhuanlan.zhihu.com/p/...
有興趣能夠點進去看一下,主要講的是因爲babel轉碼的緣由,致使最後編譯後的代碼存在了反作用(getter和setter致使),最後致使咱們不能對第三方庫有效的tree-shaking,最後做者提出的方案是在業務中先進行tree-shaking以後再進行轉碼,同時提供了相關插件。這也是咱們爲何最初要用rollup來打包一個es模塊的文件,爲了方便tree-shaking時判斷哪些變量或模塊能夠直接剔除,除此之外,藉助import和export可以更好的發揮tree-shaking的功效。(注:webpack2.X開始和rollup都會感應package.json配置文件中的module屬性,來優先加載es模塊的包,所以你首先須要爲你的包配置此字段)
接下來咱們嘗試對oleiwa包進行sideEffect的配置:
{ ... "main": "dist/oleiwa.umd.js", "module": "dist/oleiwa.es.js", "sideEffects": false }
因爲咱們使用的時候是經過import {componentA} from "@tencent/oleiwa"的形式引入的,因此咱們的入口文件處還設置了各個組件的export,即export { componentA,componentB ,...}
原本開開心心的配置完,按照import {helloworld} from "@tencent/oleiwa"來引用,按理說應該生效了,可是最後打包的結果卻沒有剔除其餘組件?問題出在了哪裏?讓咱們回顧一下剛剛的index.js入口文件:
.. //code const install = function(Vue){ .. //code } if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } export { helloworld, test } export default { version, install };
咱們對組件單獨暴露一個屬性,同時export default裏附帶install方法,安裝所有組件,最初筆者覺得多是install的執行邏輯致使webpack不敢對其tree-shaking,因而把if部分的判斷去掉了,可是最後發現,仍是把test組件打包進來了。其實執行的時候window爲undefined,install就已經沒有執行了,因此並不會影響到shaking,關鍵的問題在於最後的export default,筆者刪掉export default 的代碼後,最後實現了按需引入,打包出來的bundle剔除了test組件。緣由是tree-shaking能夠針對單獨的export作處理,可是對export default裏export出來的對象沒法進行shaking,因此若是你要使用tree-shaking,請使用export的方式暴露你的變量。
簡單總結一下,若是你要利用sideEffects和tree-shaking來實現按需加載,須要確保如下幾點:
一、利用rollup打包,導出es模塊;
二、配置package.json文件,若是你確保模塊沒有反作用,可直接把sideEffects設置爲false,同時,指定module入口;
三、導出時使用export,而非export default;
四、用戶在實際開發中須要使用webpack4.x 或 rollup進行打包。
前面介紹了項目結構初始化、打包構建以及如何實現按需加載等,大體的組件庫架子已經搭好了,你已經能夠開始愉快開心的開發你的組件庫了,接下里要介紹的是組件庫還能夠進行完善的其餘工做,包括換膚功能的實現、組件庫的類型定義以及組件庫的單元測試。
大部分的組件庫,element-ui、cube-ui、iview等都容許你對UI主題進行定製,其原理十分簡單,還記得咱們最開始爲咱們的樣式文件定義了一個index.scss的總入口嗎,只須要把這個入口暴露給用戶,讓用戶再進行額外的設置便可。換膚功能的實現本質即是一種後編譯,經過將編譯前的源碼暴露給用戶,讓用戶在開發過程當中去編譯。
用戶只須要安裝好sass-loader,自定義一個user.scss文件,引入咱們的總入口文件便可:
@import '@tencent/oleiwa/components/theme-chalk/index.scss'; // Here are the variables to cover, such as: @primary-color: #8c0776;
(注:你能夠將變量的定義放置於base.css中,供全部組件css共用)
沒有作類型定義的組件庫,是沒有格調的組件庫,爲了你的用戶可以開心愉快地使用你的組件庫,你要爲你的各個組件定義好類型,方便用戶使用。在這以前你須要在package.json裏定義好類型校驗的入口:
"typings": "types/index.d.ts",
參照Element-ui的實現,咱們能夠這樣設計類型定義文件的結構:
└── types ├── index.d.ts // 類型定義總入口 ├── oleiwa-ui.d.ts // 類型定義入口,在這裏import其餘的組件定義 ├── component.d.ts // 定義組件基類 └── helloworld.d.ts // helloworld組件的類型定義
因爲咱們的組件庫並不是直接的業務組件,因此咱們須要更多關注的是組件交互和渲染的UI測試,而組件庫須要提供給用戶使用,因此完備的單元測試頗有必要,針對組件的單元測試主要能夠細分一下幾類:
一、組件渲染,快照對比
二、props傳遞
三、回調函數執行
四、document.createEvent模擬事件觸發,檢測核心交互邏輯
一個簡單的栗子:
import { expect } from "chai"; import { shallowMount } from "@vue/test-utils"; import HelloWorld from "@/components/HelloWorld.vue"; describe("HelloWorld.vue", () => { it("renders props.msg when passed", () => { const msg = "new message"; const wrapper = shallowMount(HelloWorld, { propsData: { msg } }); expect(wrapper.text()).to.include(msg); }); });
接下來,咱們只須要爲每一個組件寫好單元測試,放置在tests/unit文件夾下統一管理便可。執行單元測試:
npm run test:unit
開發完了你的組件庫,怎麼也得教你的用戶怎麼使用吧,若是你想偷懶的話,能夠直接用vuese,快速根據你的組件,生成API文檔,其本質是經過AST分析你的文件,提取props、events等參數。
具體使用:安裝好vuese後,配置.vueserc以下:
{ "include": [ "./components/**/*.vue" ], "title": "oleiwa-doc", "genType": "docute" }
執行npx vuese gen便可,簡直方便到爆炸。
固然,若是你不知足於這個的話,可使用markdown-it來書寫本身的文檔,業界最廣泛的方式都是基於此。同時,爲了不demo和code分離,維護兩份代碼,你能夠實現本身的demo-block組件,將本身的 vue 組件插入文檔中,有興趣的話能夠戳如下連接:
最後,一個組件庫的架子,就被咱們這樣手把手的搭起來了。回顧一下咱們學習了啥:
一、爲咱們的組件庫定義好項目結構,以及定義入口文件;
二、因爲組件庫不一樣於普通的應用,因此在打包構建上咱們要針對性的處理,統一打包和單獨打包,css和js各自單獨打包,導出的js文件打包要提供umd、es模塊支持;
三、在前面的項目結構,以及咱們的打包構建基礎上,讓咱們爲組件庫實現按需加載成爲了可能,而且討論了按需加載實現的幾種方式;
四、關於組件庫所須要完善的其餘工做,包括換膚、類型定義以及爲你的組件庫作單元測試;
五、生成組件庫文檔,可使用vuese一把梭,也能夠和業界同樣,採用markdown-it來書寫文檔。
至此,一個組件庫的搭建工做到此結束,可是這只是第一步而已,接下來你須要豐富你的組件庫組件,實現更多的功能,相似於動畫、內置icon等!組件庫之路,道阻且長。