因爲業務須要,近期團隊要搞一套本身的UI
組件庫,框架方面仍是Vue
。而業界已經有比較成熟的一些UI
庫了,好比ElementUI
、AntDesign
、Vant
等。css
結合框架Vue
,咱們選擇在ElementUI
基礎上進行改造。但造輪子
絕非易事,首先須要先去了解它整個但構建流程、目錄設計等。html
本文經過分析ElementUI
完整的構建流程,最後給出搭建一個完備的組件庫須要作的一些工做,但願對於想了解ElementUI
源碼或者也有搭建UI
組件庫需求的你,能夠提供一些幫助!前端
咱們先來看下ElementUI
的源碼的目錄結構。vue
Element UI
貢獻指南、issue
和PR
模板說完文件目錄,剩下還有幾個文件(常見的.babelrc
、.eslintc
這裏就不展開說明了),在業務代碼中是不常見的:
node
Element UI
提供了四種不一樣語言的,也是很貼心了webpack
打包時獲取組件的文件路徑。ElementUI
開發者對常見問題的解答。Element UI
使用的是MIT
協議Makefile
是一個適用於 C/C++
的工具,在擁有 make
環境的目錄下, 若是存在一個 Makefile
文件。 那麼輸入 make
命令將會執行 Makefile
文件中的某個目標命令。深刻了解構建流程前,咱們先來看下ElementUI
源碼的幾個比較主要的文件目錄,這對於後面研究ElementUI
的完整流程是有幫助的。webpack
一般咱們去看一個大型項目都是從package.json
文件開始看起的,這裏麪包含了項目的版本、入口、腳本、依賴等關鍵信息。git
我這裏拿出了幾個關鍵字段,一一的去分析、解釋他的含義。github
項目的入口文件web
import Element from 'element-ui'
時候引入的就是main
中的文件
lib/element-ui.common.js
是commonjs
規範,而lib/index.js
是umd
規範,這個我在後面的打包模塊會詳細說明。shell
指定npm publish
發包時須要包含的文件/目錄。
TypeScript
入口文件。
項目的線上地址
當你把一個包發佈到npm
上時,它同時應該也能夠在unpkg
上獲取到。也就是說,你的代碼既可能在NodeJs
環境也可能在瀏覽器環境
執行。爲此你須要用umd
格式打包,lib/index.js
是umd
規範,由webpack.conf.js
生成。
聲明樣式入口文件,這裏是lib/theme-chalk/index.css
,後面也會詳細說明。
開發、測試、生產構建,打包、部署,測試用例等相關腳本。scripts
算是package.json
中最重要的部分了,下面我會一一對其中的重要指令進行說明。
"bootstrap": "yarn || npm i"
安裝依賴, 官方推薦優先選用yarn
(吐槽一句:我剛開始沒看明白,想着bootstrap
不是以前用過的那個 ui 庫嗎 🤔,後來看了下,原來bootstrap
翻譯過來是引導程序
的意思,這樣看看也就大概理解了 🤣)
該指令主要用來自動化生成一些文件。
"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js"
這條指令較長,咱們拆開來看:
build/bin/iconInit.js
解析icon.scss
,把全部的icon
的名字放在icon.json
裏面 最後掛在Vue
原型上的$icon
上。
最後經過遍歷icon.json
,獲得了官網的這種效果:
build/bin/build-entry.js
根據components.json
文件,生成src/index.js
文件,核心就是json-templater/string
插件的使用。
咱們先來看下src/index.js
文件,他對應的是項目的入口文件,最上面有這樣一句:
/* Automatically generated by './build/bin/build-entry.js' */
也就是src/index.js
文件是由build/bin/build-entry.js
腳本自動構建的。咱們來看下源碼:
// 根據components.json生成src/index.js文件 // 引入全部組件的依賴關係 var Components = require("../../components.json"); var fs = require("fs"); // https://www.npmjs.com/package/json-templater 可讓string與變量結合 輸出一些內容 var render = require("json-templater/string"); // https://github.com/SamVerschueren/uppercamelcase 轉化爲駝峯 foo-bar >> FooBar var uppercamelcase = require("uppercamelcase"); var path = require("path"); // os.EOL屬性是一個常量,返回當前操做系統的換行符(Windows系統是\r\n,其餘系統是\n) var endOfLine = require("os").EOL; // 生成文件的名字和路徑 var OUTPUT_PATH = path.join(__dirname, "../../src/index.js"); var IMPORT_TEMPLATE = "import {{name}} from '../packages/{{package}}/index.js';"; var INSTALL_COMPONENT_TEMPLATE = " {{name}}"; // var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */ // ... // 獲取全部組件的名字,存放在數組中 var ComponentNames = Object.keys(Components); var includeComponentTemplate = []; var installTemplate = []; var listTemplate = []; ComponentNames.forEach((name) => { var componentName = uppercamelcase(name); includeComponentTemplate.push( render(IMPORT_TEMPLATE, { name: componentName, package: name, }) ); if ( [ "Loading", "MessageBox", "Notification", "Message", "InfiniteScroll", ].indexOf(componentName) === -1 ) { installTemplate.push( render(INSTALL_COMPONENT_TEMPLATE, { name: componentName, component: name, }) ); } if (componentName !== "Loading") listTemplate.push(` ${componentName}`); }); var template = render(MAIN_TEMPLATE, { include: includeComponentTemplate.join(endOfLine), install: installTemplate.join("," + endOfLine), version: process.env.VERSION || require("../../package.json").version, list: listTemplate.join("," + endOfLine), }); // 結果輸出到src/index.js中 fs.writeFileSync(OUTPUT_PATH, template); console.log("[build entry] DONE:", OUTPUT_PATH);
其實就是上面說的,根據components.json
,生成src/index.js
文件。
build/bin/i18n.js
根據 examples/i18n/page.json
和模版,生成不一樣語言的 demo
,也就是官網 demo 展現國際化的處理。
ElementUI
官網的國際化依據的模版是examples/pages/template
,根據不一樣的語言,分別生成不一樣的文件:
這裏面都是.tpl
文件,每一個文件對應一個模版,並且每一個tpl
文件又都是符合SFC
規範的Vue
文件。
咱們隨便打開一個文件:
export default { data() { return { lang: this.$route.meta.lang, navsData: [ { path: "/design", name: "<%= 1 >", }, { path: "/nav", name: "<%= 2 >", }, ], }; }, };
裏面都有數字標示了須要國際化處理的地方。
首頁全部國際化相關的字段對應關係存儲在examples/i18n/page.json
中:
最終官網展現出來的就是通過上面國際化處理後的頁面:
支持切換不一樣語言。
繞了一圈,回到主題:build/bin/i18n.js
幫咱們作了什麼呢?
咱們思考一個問題:首頁的展現是如何作到根據不一樣語言,生成不一樣的vue
文件呢?
這就是build/bin/i18n.js
幫咱們作的事情。
來看下對應的源碼:
"use strict"; var fs = require("fs"); var path = require("path"); var langConfig = require("../../examples/i18n/page.json"); langConfig.forEach((lang) => { try { fs.statSync(path.resolve(__dirname, `../../examples/pages/${lang.lang}`)); } catch (e) { fs.mkdirSync(path.resolve(__dirname, `../../examples/pages/${lang.lang}`)); } Object.keys(lang.pages).forEach((page) => { var templatePath = path.resolve( __dirname, `../../examples/pages/template/${page}.tpl` ); var outputPath = path.resolve( __dirname, `../../examples/pages/${lang.lang}/${page}.vue` ); var content = fs.readFileSync(templatePath, "utf8"); var pairs = lang.pages[page]; Object.keys(pairs).forEach((key) => { content = content.replace( new RegExp(`<%=\\s*${key}\\s*>`, "g"), pairs[key] ); }); fs.writeFileSync(outputPath, content); }); });
處理流程也很簡單:遍歷examples/i18n/page.json
,根據不一樣的數據結構把tpl
文件的標誌位,經過正則匹配出來,並替換成本身預先設定好的字段。
這樣官網首頁的國際化就完成了。
build/bin/version.js
根據package.json
中的version
,生成examples/versions.json
,對應就是完整的版本列表
處理樣式相關。
"build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",
一樣這一條也關聯了多個操做,咱們拆開來看。
這一步是根據components.json
,生成package/theme-chalk/index.scss
文件,把全部組件的樣式都導入到index.scss
。
實際上是作了一個自動化導入操做,後面每次新增組件,就不用手動去引入新增組件的樣式了。
咱們都知道ElementUI
在使用時有兩種引入方式:
import Vue from "vue"; import ElementUI from "element-ui"; import "element-ui/lib/theme-chalk/index.css"; import App from "./App.vue"; Vue.use(ElementUI); new Vue({ el: "#app", render: (h) => h(App), });
import Vue from "vue"; import { Pagination, Dropdown } from "element-ui"; import App from "./App.vue"; Vue.use(Pagination); Vue.use(Dropdown); new Vue({ el: "#app", render: (h) => h(App), });
對應兩種引入方式,Element
在打包時對應的也有兩種方案。
具體以下:將packages/theme-chalk
下的全部scss
文件編譯爲css
,當你須要全局引入時,就去引入index.scss
文件;當你按需引入時,引入對應的組件scss
文件便可。
這其中有一點,咱們須要思考下:如何把packages/theme-chalk
下的全部scss
文件編譯爲css
?
在平時的開發中,咱們打包、壓縮之類的工做每每都會交給webpack
去處理,可是,針對上面這個問題,咱們若是採用gulp
基於工做流去處理會更加方便。
gulp
相關的處理就在packages/theme-chalk/gulpfile.js
中:
"use strict"; const { series, src, dest } = require("gulp"); const sass = require("gulp-sass"); // 編譯gulp工具 const autoprefixer = require("gulp-autoprefixer"); // 添加廠商前綴 const cssmin = require("gulp-cssmin"); // 壓縮css function compile() { return src("./src/*.scss") // src下的全部scss文件 .pipe(sass.sync()) // 把scss文件編譯成css .pipe( autoprefixer({ // 基於目標瀏覽器版本,添加廠商前綴 browsers: ["ie > 9", "last 2 versions"], cascade: false, }) ) .pipe(cssmin()) // 壓縮css .pipe(dest("./lib")); // 輸出到lib下 } function copyfont() { return src("./src/fonts/**") // 讀取src/fonts下的全部文件 .pipe(cssmin()) .pipe(dest("./lib/fonts")); // 輸出到lib/fonts下 } exports.build = series(compile, copyfont);
通過處理,最終就會打包出對應的樣式文件
cp-cli
是一個跨平臺的copy
工具,和CopyWebpackPlugin
相似
這裏就是複製文件到lib/theme-chalk
下。
上面提到過屢次components.json
,下面就來了解下。
這個文件其實就是記錄了組件的路徑,在自動化生成文件以及入口時會用到:
{ "pagination": "./packages/pagination/index.js", "dialog": "./packages/dialog/index.js", "autocomplete": "./packages/autocomplete/index.js", // ... "avatar": "./packages/avatar/index.js", "drawer": "./packages/drawer/index.js", "popconfirm": "./packages/popconfirm/index.js" }
存放着組件庫的源碼和組件樣式文件。
這裏以Alert
組件爲例作下說明:
這裏main.vue
對應就是組件源碼,而index.js
就是入口文件:
import Alert from "./src/main"; /* istanbul ignore next */ Alert.install = function (Vue) { Vue.component(Alert.name, Alert); }; export default Alert;
引入組件,而後爲組件提供install
方法,讓Vue
能夠經過Vue.use(Alert)
去使用。
關於
install
能夠看
官方文檔
這裏面存放的就是全部組件相關的樣式,上面也已經作過說明了,裏面有index.scss
(用於全局引入時導出全部組件樣式)和其餘每一個組件對應的scss
文件(用於按需引入時導出對應的組件樣式)
說了半天,終於繞到了src
文件夾。
上面的packages
文件夾是分開去處理每一個組件,而src
的做用就是把全部的組件作一個統一處理,同時包含自定義指令、項目總體入口、組件國際化、組件 mixins、動畫的封裝和公共方法。
咱們主要來看下入口文件,也就是src/index.js
:
/* Automatically generated by './build/bin/build-entry.js' */ // 導入了packages下的全部組件 import Pagination from "../packages/pagination/index.js"; import Dialog from "../packages/dialog/index.js"; import Autocomplete from "../packages/autocomplete/index.js"; // ... const components = [ Pagination, Dialog, Autocomplete, // ... ]; // 提供了install方法,幫咱們掛載了一些組件與變量 const install = function (Vue, opts = {}) { locale.use(opts.locale); locale.i18n(opts.i18n); // 把全部的組件註冊到Vue上面 components.forEach((component) => { Vue.component(component.name, component); }); Vue.use(InfiniteScroll); Vue.use(Loading.directive); Vue.prototype.$ELEMENT = { size: opts.size || "", zIndex: opts.zIndex || 2000, }; Vue.prototype.$loading = Loading.service; Vue.prototype.$msgbox = MessageBox; Vue.prototype.$alert = MessageBox.alert; Vue.prototype.$confirm = MessageBox.confirm; Vue.prototype.$prompt = MessageBox.prompt; Vue.prototype.$notify = Notification; Vue.prototype.$message = Message; }; /* istanbul ignore if */ if (typeof window !== "undefined" && window.Vue) { install(window.Vue); } // 導出版本號、install方法(插件)、以及一些功能好比國際化功能 export default { version: "2.13.2", locale: locale.use, i18n: locale.i18n, install, Pagination, Dialog, Autocomplete, // ... };
文件開頭的:
/* Automatically generated by './build/bin/build-entry.js' */
其實在上面的scripts
的build/bin/build-entry.js
中咱們已經提到過:src/index.js
是由build-entry
腳本自動生成的。
這個文件主要作下如下事情:
packages
下的全部組件install
方法,把全部的組件註冊到Vue
上面,並在Vue
原型上掛載了一些全局變量和方法install
方法、變量、方法導出存放了 ElementUI
的組件示例。
其實從目錄結構,咱們不難看出這是一個完整獨立的Vue
項目。主要用於官方文檔的展現:
這裏咱們主要關注下docs
文件夾:Element
官網支持 4 種語言,docs
一共有 4 個文件夾,每一個文件夾裏面的內容基本是同樣的。
咱們能夠看到裏面所有都是md
文檔,而每個md
文檔,分別對應着官網組件的展現頁面。
其實如今各大主流組件庫文檔都是用採用
md
編寫。
咱們上面大體瞭解了源碼的幾個主要文件目錄,可是都比較分散。下面咱們從構建指令到新建組件、打包流程、發佈組件完整的看一下構建流程。
平時咱們都習慣將項目經常使用的腳本放在package.json
中的scripts
中。但ElementUI
還使用了Makefile
文件(因爲文件內容較多,這裏就選取了幾個作下說明):
.PHONY: dist test default: help # build all theme build-theme: npm run build:theme install: npm install install-cn: npm install --registry=http://registry.npm.taobao.org dev: npm run dev play: npm run dev:play new: node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS)) dist: install npm run dist deploy: @npm run deploy pub: npm run pub test: npm run test:watch // Tip: // make new <component-name> [中文] // 一、將新建組件添加到components.json // 二、添加到index.scss // 三、添加到element-ui.d.ts // 四、建立package // 五、添加到nav.config.json
我是第一次見,因此就去Google
下,網上對Makefile
對定義大概是這樣:
Makefile
是一個適用於C/C++
的工具,較早做爲工程化工具出如今UNIX
系統中, 經過make
命令來執行一系列的編譯和鏈接操做。在擁有make
環境的目錄下, 若是存在一個Makefile
文件。 那麼輸入make
命令將會執行Makefile
文件中的某個目標命令。
這裏我以make install
爲例簡要說明下執行流程:
make
命令, 在該目錄下找到 Makefile
文件。Makefile
文件中對應命令行參數的 install
目標。這裏的目標就是 npm install
咱們看下scripts
中的dev
指令:
"dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js",
首先npm run bootstrap
是用來安裝依賴的。
npm run build:file
在前面也有提到,主要用來自動化生成一些文件。主要是node build/bin/build-entry.js
,用於生成Element
的入口js
:先是讀取根目錄的components.json
,這個json
文件維護着Element
全部的組件路徑映射關係,鍵爲組件名,值爲組件源碼的入口文件;而後遍歷鍵值,將全部組件進行import
,對外暴露install
方法,把全部import
的組件經過Vue.component(name, component)
方式註冊爲全局組件,而且把一些彈窗類的組件掛載到Vue
的原型鏈上(這個在上面介紹scripts
相關腳本時有詳細說明)。
在生成了入口文件的src/index.js
以後就會運行webpack-dev-server
。
webpack-dev-server --config build/webpack.demo.js
這個前面也提過,用於跑Element
官網的基礎配置。
上面咱們提到了,Element
中還用了makefile
爲咱們編寫了一些額外的腳本。
這裏重點說一下 make new <component-name> [中文]
這個命令。
當運行這個命令的時候,其實運行的是 node build/bin/new.js
。
build/bin/new.js
比較簡單,備註也很清晰,它幫咱們作了下面幾件事:
一、新建的組件添加到components.json
二、在packages/theme-chalk/src
下新建對應到組件scss
文件,並添加到packages/theme-chalk/src/index.scss
中
三、添加到 element-ui.d.ts
,也就是對應的類型聲明文件
四、建立package
(咱們上面有提到組件相關的源碼都在package
目錄下存放)
五、添加到nav.config.json
(也就是官網組件
左側的菜單)
ElementUI
打包執行的腳本是:
"dist": "npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme",
下面咱們一一來進行分析:
"clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",
刪除以前打包生成文件。
根據components.json
生成入口文件src/index.js
,以及i18n
相關文件。這個在上面已經作過度析,這裏就再也不展開進行說明。
"lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet",
項目eslint
檢測,這也是如今項目必備的。
webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js
生成umd
格式的js
文件(index.js)
生成commonjs
格式的js
文件(element-ui.common.js),require
時默認加載的是這個文件。
以components.json
爲入口,將每個組件打包生成一個文件,用於按需加載。
"build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",
把src
目錄下的除了index.js
入口文件外的其餘文件經過babel
轉譯,而後移動到lib
文件夾下。
"build:umd": "node build/bin/build-locale.js",
生成umd
模塊的語言包。
"build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",
根據components.json
,生成package/theme-chalk/index.scss
。用gulp
構建工具,編譯scss
、壓縮、輸出css
到lib
目錄。
最後用一張圖來描述上述整個打包流程:
打包完成,緊跟着就是代碼的發佈了。Element
中發佈主要是用shell
腳本實現的。
Element
發佈一共涉及三個部分:
一、git 發佈
二、npm 發佈
三、官網發佈
發佈對應的腳本是:
"pub": "npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js && sh build/deploy-faas.sh",
運行 git-release.sh
進行git
衝突的檢測,這裏主要是檢測dev
分支是否衝突,由於Element
是在dev
分支進行開發的。
#!/usr/bin/env sh # 切換至dev分支 git checkout dev # 檢測本地和暫存區是否還有未提交的文件 if test -n "$(git status --porcelain)"; then echo 'Unclean working tree. Commit or stash changes first.' >&2; exit 128; fi # 檢測本地分支是否有誤 if ! git fetch --quiet 2>/dev/null; then echo 'There was a problem fetching your branch. Run `git fetch` to see more...' >&2; exit 128; fi # 檢測本地分支是否落後遠程分支 if test "0" != "$(git rev-list --count --left-only @'{u}'...HEAD)"; then echo 'Remote history differ. Please pull changes.' >&2; exit 128; fi # 經過以上檢查,表示代碼無衝突 echo 'No conflicts.' >&2;
dev
分支代碼檢測沒有衝突,接下來就會執行release.sh
腳本,合併dev
分支到master
、更新版本號、推送代碼到遠程倉庫併發布到npm
(npm publish)。
官網更新大體就是:將靜態資源生成到examples/element-ui
目錄下,而後放到gh-pages
分支,這樣就能經過github pages
的方式訪問。
到這裏ElementUI
的完整構建流程就分析完了。
經過對ElementUI
源碼文件和構建流程的分析,下面咱們能夠總結一下搭建一個完備的 ui 組件庫都須要作什麼工做。
目錄結構對於大型項目是尤爲重要的,合理清晰的結構對於後期的開發和擴展都是頗有意義的。ui
組件庫的目錄結構,我感受ElementUI
的就很不錯:
|-- Element |-- .babelrc // babel相關配置 |-- .eslintignore |-- .eslintrc // eslint相關配置 |-- .gitattributes |-- .gitignore |-- .travis.yml // ci配置 |-- CHANGELOG.en-US.md |-- CHANGELOG.es.md |-- CHANGELOG.fr-FR.md |-- CHANGELOG.zh-CN.md // 版本改動說明 |-- FAQ.md // 常見問題QA |-- LICENSE // 版權協議相關 |-- Makefile // 腳本集合(工程化編譯) |-- README.md // 項目說明文檔 |-- components.json // 組件配置文件 |-- element_logo.svg |-- package.json |-- yarn.lock |-- .github // 貢獻者、issue、PR模版 | |-- CONTRIBUTING.en-US.md | |-- CONTRIBUTING.es.md | |-- CONTRIBUTING.fr-FR.md | |-- CONTRIBUTING.zh-CN.md | |-- ISSUE_TEMPLATE.md | |-- PULL_REQUEST_TEMPLATE.md | |-- stale.yml |-- build // 打包 |-- examples // 示例代碼 |-- packages // 組件源碼 |-- src // 入口文件以及各類輔助文件 |-- test // 單元測試文件 |-- types // 類型聲明
參考大多數 UI
組件庫的作法,能夠將 examples
下的示例代碼組織起來並暴露一個入口,使用 webpack
配置一個 dev-server
,後續對組件的調試、運行都在此 dev-server
下進行。
UI
組件做爲高度抽象的基礎公共組件,編寫單元測試是頗有必要的。合格的單元測試也是一個成熟的開源項目必備的。
對於打包後的文件,統一放在 lib
目錄下,同時記得要在 .gitignore
中加上 lib
目錄,避免將打包結果提交到代碼庫中。
同時針對引入方式的不一樣,要提供全局引入
(UMD)和按需加載
兩種形式的包。
組件庫的文檔通常都是對外可訪問的,所以須要部署到服務器上,同時也需具有本地預覽的功能。
組件庫的某個版本完成開發工做後,須要將包發佈到 npm 上。發佈流程:
發佈後須要平常維護以前老版本,通常須要注意一下幾點:
1.若是以爲這篇文章還不錯,來個分享、點贊、在看三連吧,讓更多的人也看到~
2.關注公衆號前端森林,按期爲你推送新鮮乾貨好文。
3.特殊階段,帶好口罩,作好我的防禦。