第一次在掘金髮文章,有點囉裡囉嗦,大家見諒。
當前大部分UI框架設計的Webpack配置都相對複雜,例如Element、Ant Design Vue和Muse-UI等Vue元件庫。例如Element,為了實現業務層面的兩種引入形式(完整引入和按需引入),以及丟擲一些可供業務層面通用的utils
、i18n
等,Webpack配置變得非常複雜。為了簡化UI框架的設計難度,這裡介紹一種簡單的UI框架設計,在此之前先簡單介紹一下Element的構建流程,以便對比新的UI框架設計。
一般元件庫的設計者將引入形式設計成完整引入和按需引入兩種形式:完整引入的開發相對便利,針對一些大型業務或者對於打包體積不是特別注重的業務,按需引入開發的顆粒度相對精細,可以減少業務的打包體積。
設計的UI框架實踐專案的github地址是ziyi2/vue-cli3-lerna-ui,包括了preset.json
、自己設計的Vue CLI外掛以及自己設計的一系列UI元件(和生成的UI框架示例稍有不同),如果覺得整體結構有不合理的或者考慮不夠全面的地方,歡迎大家提issue,這樣我也可以對它進行完善。如果大家感興趣,希望大家能夠Star一下,這裡拜謝大家了!
Element
首先了解Element
的構建流程,檢視Element2.7.0
版本package.json
的npm 指令碼:
// 其中的`node build/bin/build-entry.js` 生成Webpack構建入口
"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",
// 構建css樣式
"build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",
// 構建commonjs規範的`utils`
"build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",
// 構建umd模組的語言包
"build:umd": "node build/bin/build-locale.js",
// 清除構建資料夾`lib`
"clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",
// 總體構建
"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",
// 執行eslint校驗
"lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet"
複製程式碼
這裡重點關注Element的構建指令碼,忽略測試、釋出、啟動開發態除錯頁面、構建演示頁面等指令碼。
npm run dist
與Element
構建相關的npm指令碼繁多,但是總體構建指令碼是dist
:
"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"
複製程式碼
&&
是繼發執行,只有當前任務成功,才能執行下一個任務。
總體構建指令碼包含了以下按順序執行的指令碼命令
npm run clean
- 清除構建資料夾lib
npm run build:file
- 其中的node build/bin/build-entry.js
生成Webpack構建入口npm run lint
- 執行eslint校驗webpack --config build/webpack.conf.js
- 構建umd總檔案webpack --config build/webpack.common.js
- 構建commonjs2總檔案webpack --config build/webpack.component.js
- 構建commonjs2元件(提供按需引入)npm run build:utils
- 構建commonjs的utils
(供commonjs2總檔案、commonjs2元件以及業務使用)npm run build:umd
- 構建umd語言包npm run build:theme
- 構建css樣式
如果對於對於
umd
、commonjs2
、amd
等模組定義不是特別清晰,可參考Webpack文件模組定義系統。
執行npm run dist
後會在當前根目錄生成新的lib
資料夾,包含以下構建內容:
lib
├── directives # commonjs指令(這裡歸為utils)
├── locale # commonjs國際化(commonjs語言包和API)
├── mixins # commonjs mixins(這裡歸為utils)
├── theme-chalk # css 樣式檔案
├── transitions # commonjs transitions(這裡歸為utils)
├── umd # umd語言包
├── utils
├── alert.js # commonjs元件
├── aside.js
├── ...
├── element-ui.common.js # commonjs2總檔案
├── ...
├── index.js # umd總檔案
├── ...
複製程式碼
從Element官方文件的使用指南結合lib
可以看出,Element
為我們提供了以下能力:
- 1、CDN引入(umd 總檔案)
- 2、npm包完整引入(丟擲commonjs2總檔案)
- 3、按需引入(丟擲commonjs2的所有UI元件)
- 4、支援國際化
- 5、提供
utils
方法(官方文件沒有說明,但事實上業務可以使用)
CDN引入的umd總檔案一般是全量構建的,不會有依賴問題,但是commonjs2模組的檔案需要在業務層面再次使用Webpack構建。例如需要在業務層面支援國際化和提供utils的功能,那麼就不能將國際化和提供utils的程式碼bundle到commonjs2總檔案或commonjs2的所有UI元件中(每一個元件都bundleutils
的方法或者國際化API顯然是不合理的),如果需要在業務層面支援按需引入的功能,那麼不建議將所有UI元件的原始碼bundle到commonjs2總檔案中,這樣便可以實現層層引用,對外丟擲功能的同時在業務層面可以防止Webpack二次打包,從而導致引入兩遍甚至多遍相同的程式碼的問題。
在元件庫中開發時,為了構建commonjs2模組的檔案,需要對各個
utils
、元件等引入的路徑做出強約定,這樣不僅產生的Webpack配置會變得很難維護,對於開發者的開發也需要做出一定的規範限制。
接下來分析一下各個指令碼的構建功能。
npm run build:file
build:file
指令碼是自動生成一些原始碼檔案的指令碼:
"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",
複製程式碼
其中與構建相關的指令碼是node build/bin/build-entry.js
,主要用於生成Webpack構建的入口原始檔src/index.js
:
// 註釋說明該檔案由build-entry.js指令碼自動生成
/* Automatically generated by './build/bin/build-entry.js' */
import Pagination from '../packages/pagination/index.js';
// ... 這裡省略大部分元件引入
import TimelineItem from '../packages/timeline-item/index.js';
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
const components = [
Pagination,
// ... 這裡省略大部分元件
TimelineItem,
CollapseTransition
];
const install = function(Vue, opts = {}) {
locale.use(opts.locale);
locale.i18n(opts.i18n);
components.forEach(component => {
Vue.component(component.name, component);
});
Vue.use(Loading.directive);
Vue.prototype.$ELEMENT = {
size: opts.size || '',
zIndex: opts.zIndex || 2000
};
Vue.prototype.$loading = Loading.service;
// ...
};
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
export default {
version: '2.7.0',
locale: locale.use,
i18n: locale.i18n,
install,
CollapseTransition,
Loading,
Pagination,
// ... 這裡省略大部分元件
TimelineItem
};
複製程式碼
在元件的開發過程中如果元件較多,建議使用指令碼自動生成構建入口檔案。
npm run lint
構建之前使用lint
指令碼對構建的原始碼檔案進行eslint
校驗:
"lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet",
複製程式碼
Element
對eslint
做了嚴格控制,一旦eslint
報錯那麼dist
總體構建指令碼執行停止,整體構建失敗。這裡的eslint
校驗可以使用eslint-loader進行處理(如果你希望eslint
校驗失敗也可以進行構建可以檢視Errors and Warning)。
webpack --config build/webpack.conf.js
webpack --config build/webpack.conf.js
指令碼用於構建umd總檔案,執行該指令碼最終會在lib
下生成index.js
檔案:
lib
├── index.js # umd 總檔案
複製程式碼
webpack.conf.js
配置如下:
// build/webpack.conf.js
// ...忽略
module.exports = {
mode: 'production',
// 指定入口檔案src/index.js,該入口檔案由`build:file`指令碼自動生成
entry: {
app: ['./src/index.js']
},
output: {
// 在lib檔案中生成
path: path.resolve(process.cwd(), './lib'),
// 生成lib/index.js
filename: 'index.js',
// 生成umd模組
libraryTarget: 'umd',
// src/index.js檔案採用export default語法丟擲,因此需要設定libraryExport
// 否則引入的UI元件庫需要使用.default才能引用到丟擲的物件
// if your entry has a default export of `MyDefaultModule`
// var MyDefaultModule = _entry_return_.default;
// 這裡踩過坑,所以說明一下,不配置的話遇到的問題是引入的UI元件庫沒法解構
libraryExport: 'default',
},
resolve: {
extensions: ['.js', '.vue', '.json'],
// 'element-ui': path.resolve(__dirname, '../')
// alias中的'element-ui'作為npm包丟擲後指向了業務專案node_modules所在的npm包路徑
alias: config.alias
},
externals: {
// 構建只排除vue
// umd模組通過CDN形式引入,因此將所有的元件、utils、i18n等構建在內
// umd模組沒有按需引入功能
vue: config.vue
},
// ...忽略
};
複製程式碼
構建檔案lib/index.js
主要的功能是用於CDN形式引入專案,並且無法做到按需載入,產生的體積非常大,對於簡單的應用可能不適用。
webpack --config build/webpack.common.js
webpack --config build/webpack.common.js
指令碼用於構建commonjs2總檔案,執行該指令碼最終會在lib
下生成element-ui.common.js
檔案:
lib
├── element-ui.common.js # commonjs2 總檔案
複製程式碼
由於該檔案需要在業務層面再次使用Webpack構建,因此考量的方面較多。在分析Webpack配置之前,再次回顧一下Element
能為我們做什麼:
- 1、完整引入(丟擲commonjs2總檔案)
- 2、按需引入(丟擲commonjs2的所有UI元件)
- 3、支援國際化(commonjs2)
- 4、提供
utils
方法(commonjs2,當然官方沒有對外說明)
webpack --config build/webpack.common.js
指令碼主要用於構建完整引入功能,同時為了可以在業務層面丟擲按需引入、支援國際化等功能,構建element-ui.common.js
時需要將UI元件、支援國際化、utils方法的原始碼排除。
webpack.common.js
配置如下:
// build/webpack.common.js
// ...忽略
module.exports = {
mode: 'production',
entry: {
app: ['./src/index.js']
},
output: {
path: path.resolve(process.cwd(), './lib'),
publicPath: '/dist/',
filename: 'element-ui.common.js',
chunkFilename: '[id].js',
libraryExport: 'default',
library: 'ELEMENT',
// 生成commonjs2模組
libraryTarget: 'commonjs2'
},
resolve: {
extensions: ['.js', '.vue', '.json'],
// 'element-ui': path.resolve(__dirname, '../')
alias: config.alias,
modules: ['node_modules']
},
// 這裡用於排除UI元件、支援國際化、utils方法的原始碼,這些原始碼需要額外的指令碼進行構建
externals: config.externals,
optimization: {
// commonjs2無須壓縮處理
minimize: false
},
// ...忽略
};
複製程式碼
重點需要關注一下config.externals
屬性,列印輸出該變數的值:
[{
vue: 'vue',
// 排除所有UI元件的原始碼
'element-ui/packages/option':'element-ui/lib/option',
// ...
// 排除國際化的原始碼
'element-ui/src/locale': 'element-ui/lib/locale',
// 排除utils方法的原始碼
'element-ui/src/utils/vue-popper': 'element-ui/lib/utils/vue-popper',
'element-ui/src/mixins/emitter': 'element-ui/lib/mixins/emitter',
'element-ui/src/transitions/collapse-transition': 'element-ui/lib/transitions/collapse-transition'
// ...
},
// var nodeExternals = require('webpack-node-externals');
// nodeExternals()
[Function]
];
複製程式碼
externals屬性可以將一些特定的依賴從輸出的bundle中排除,例如在開發態中元件之間有依賴關係,element-ui/packages/pagination
中引入element-ui/packages/option
元件:
pagecages/pagination/src/pagination.js
// pagination元件中需要用到option元件
import ElOption from 'element-ui/packages/option';
// ...
複製程式碼
Webpack構建後,可以發現在element-ui.common.js
中並沒有將element-ui/packages/option
元件打包在內,而只是更改了它的引入路徑element-ui/lib/option
(在實現按需引入功能時會用webpack --config build/webpack.component.js
指令碼構建出該檔案)。
// lib/element-ui.common.js
module.exports = require("element-ui/lib/option");
複製程式碼
因此以上列出的config.externals
屬性的key
和value
可以排除UI元件、支援國際化、utils方法功能的程式碼。
config.externals
屬性的最後一個值是[Function]
,是由webpack-node-externals生成的。這裡解釋一下webpack-node-externals
的作用:
Webpack allows you to define externals - modules that should not be bundled. When bundling with Webpack for the backend - you usually don't want to bundle its node_modules dependencies. This library creates an externals function that ignores node_modules when bundling in Webpack.
例如在Elment
元件庫開發中需要依賴deepmerge
,那麼Webpack構建的時候不需要將該依賴bundle到element-ui.common.js
中,而是將其新增到Element
元件庫(作為npm包釋出)的dependencies
,這樣通過npm安裝Element
的同時也會安裝它的依賴deepmerge
,從而使得element-ui.common.js
通過require("deepmerge")
的形式引入該依賴不會報錯。
這裡列出element-ui.common.js
排除的一些程式碼:
// 排除utils原始碼(utils原始碼會通過`npm run build:utils`指令碼構建)
module.exports = require("element-ui/lib/utils/dom");
// 排除vue
module.exports = require("vue");
// 排除國際化原始碼(國際化原始碼會通過`npm run build:utils`指令碼構建)
module.exports = require("element-ui/lib/locale");
// 需要注意和Vue相關的JSX依賴(Vue CLI3系統構建的包也會有一個該功能的依賴)
module.exports = require("babel-helper-vue-jsx-merge-props");
// 排除一些Elment元件使用的其他依賴
module.exports = require("throttle-debounce/throttle");
// 排除UI元件原始碼(UI元件原始碼會通過`webpack --config build/webpack.component.js`指令碼構建)
module.exports = require("element-ui/lib/option");
複製程式碼
需要注意
Element
釋出的npm包入口檔案就是element-ui.common.js
,可以通過package.json中的main
欄位資訊檢視。
webpack --config build/webpack.component.js
webpack --config build/webpack.component.js
指令碼用於構建commonjs2的UI元件(提供按需引入功能),執行該指令碼最終會在lib
下生成所有Element
支援的UI元件(同時這些檔案也會被element-ui.common.js
總入口檔案引用):
lib
├── alert.js # commonjs 元件
├── aside.js
├── button.js
├── ...
複製程式碼
檢視build/webpack.component.js
配置:
// ...忽略
const Components = require('../components.json');
// Components是所有元件的構建入口列表
// {
// "pagination": "./packages/pagination/index.js",
// ...
// "timeline-item": "./packages/timeline-item/index.js"
// }
const webpackConfig = {
mode: 'production',
// 多入口
entry: Components,
output: {
path: path.resolve(process.cwd(), './lib'),
publicPath: '/dist/',
filename: '[name].js',
chunkFilename: '[id].js',
libraryTarget: 'commonjs2'
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: config.alias,
modules: ['node_modules']
},
// 排除其他UI元件、支援國際化、utils的原始碼,這些原始碼會額外構建
externals: config.externals,
},
// ...忽略
};
複製程式碼
構建單個元件和構建總體入口檔案element-ui.common.js
的Webpack配置類似,需要將utils
、locale
以及其他一些依賴排除。
npm run build:utils
build:utils
指令碼主要用於構建commonjs的utils
(提供國際化以及utils
功能):
"build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",
複製程式碼
可以發現該命令並不是通過Webpack進行多檔案構建,而是通過Babel直接進行轉義處理(Webpack構建會產生額外的Webpack程式碼,並且配置繁瑣,Babel轉義處理構建的程式碼非常乾淨),將src
目錄下除了Webpack構建入口檔案src/index.js
以外的所有其他檔案進行轉義處理。執行該指令碼最終會在lib
下生成所有的utils
檔案:
lib
├── directives # commonjs 指令
├── locale # commonjs 國際化API和語言包
├── mixins # commonjs 混入
├── transitions # commonjs 過度動畫
├── utils # commonjs 工具方法
複製程式碼
生成的這些工具方法會被lib
下的element-ui.common.js
和各個元件引用,同時在業務層面也可以引用這些工具方法。檢視.babelrc
檔案的配置資訊:
{
"presets": [
[
"env",
{
"loose": true,
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}
],
"stage-2"
],
"plugins": ["transform-vue-jsx"],
"env": {
// cross-env BABEL_ENV=utils
"utils": {
"presets": [
[
"env",
{
// 鬆散模式,更像人手寫的ES5程式碼
"loose": true,
// es6轉成commonjs
"modules": "commonjs",
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}
],
],
"plugins": [
["module-resolver", {
"root": ["element-ui"],
"alias": {
// 類似於Webpack的externals功能
// 將原始碼的引入路徑更改成目的碼的引入路徑
"element-ui/src": "element-ui/lib"
}
}]
]
},
"test": {
"plugins": ["istanbul"]
}
}
}
複製程式碼
utils
檔案原始碼之間互相引用的路徑是element-ui/src
,轉義成目的碼後互相之間的引用路徑是element-ui/lib
,因此需要有類似於Webpack的externals
的功能去更改目的碼的引用路徑,進行Babel轉義時外掛babel-plugin-module-resolver可以實現該功能。
npm run build:theme
build:theme
指令碼主要用於構建UI元件的css樣式:
"build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",
複製程式碼
這裡主要關注 gulp build --gulpfile packages/theme-chalk/gulpfile.js
指令碼,該指令碼使用Gulp構建工具構建css樣式檔案,Glup構建多檔案樣式會非常簡單。最終將當前構建的packages/theme-chalk/lib
目錄下的內容拷貝到lib/theme-chalk
目錄下供外部業務使用:
lib
├── theme-chalk # css 樣式檔案
│ ├── fonts # icons
│ ├── alert.css # 按需引入的元件樣式
│ ├── ... # 按需引入的元件樣式
│ └── index.css # 完整引入樣式
複製程式碼
檢視gulpfile.js
檔案:
'use strict';
const { series, src, dest } = require('gulp');
const sass = require('gulp-sass');
const autoprefixer = require('gulp-autoprefixer');
const cssmin = require('gulp-cssmin');
function compile() {
return src('./src/*.scss')
// sass轉化成css
.pipe(sass.sync())
// Parse CSS and add vendor prefixes to rules by Can I Use
// css瀏覽器相容處理
.pipe(autoprefixer({
browsers: ['ie > 9', 'last 2 versions'],
cascade: false
}))
// 壓縮css
.pipe(cssmin())
.pipe(dest('./lib'));
}
function copyfont() {
return src('./src/fonts/**')
.pipe(cssmin())
.pipe(dest('./lib/fonts'));
}
exports.build = series(compile, copyfont);
複製程式碼
Vue CLI 3 & Lerna
構建整個Element
元件庫的指令碼繁多,構建的程式碼之間互相還有引用關係,對於開發的引用路徑也會產生一定的約束。因此設計類似於Element
的UI框架相對開發者而言需要一定的開發門檻。
這裡基於Vue CLI 3的開發/構建目標/庫能力以及Lerna工具設計了一個UI框架,這個UI框架整合了以下特點:
- 1、結構特點:每個UI元件都是一個npm包,多語言、工具和樣式都是自成體系的npm包,可被業務或UI元件靈活引用,同時天然按需載入。
- 2、配置特點:如果需要進行構建處理,那麼每個npm包可單獨進行構建配置,配置變得更加簡單。結合Vue CLI3的構件庫能力,對於簡單UI元件的構建幾乎可以做到webpack零配置,當然需要特殊的webpack loader除外。
- 3、釋出特點:元件庫的版本迭代可以更快,不需要進行整體構建,每個元件可單獨快速釋出
PATCH
或MINOR
版本。
這裡設定業務層面需要進行webpack構建處理,因此可以對UI框架的元件不進行構建處理,當然如果UI元件的設計需要特殊的webpack loader處理除外,否則業務層面需要做額外的webpack配置。當然不構建處理是相對於一定的使用場景的,不構建處理可能也會產生額外的一些問題。
這個UI框架的設計也會有一些缺陷:
- 1、沒有完整引入功能(也可以進行整體構建,但是這裡不推薦)
- 2、不提供UMD模組
- 3、業務層面引入繁瑣(可以出額外的引入工具,簡化業務中的UI元件引入)
Vue CLI 3
構建庫
為了簡化UI框架的webpack配置,這裡將Vue CLI 3作為開發的容器引入,借用Vue CLI 3的構建庫功能(構建web-components-元件功能應該更合適,這裡沒有進行驗證),幾乎可以做到UI元件構建的零配置。通過審查專案的-webpack-配置能力,可以檢視Vue CLI 3為我們預先設定的通用webpack配置(幾乎可以滿足大部分的UI元件構建)。
外掛體系
這裡使用Vue CLI 3的外掛和Preset功能開發了幾個外掛,以便於快速構建起步的UI設計框架,具體的preset.json配置如下:
{
"useConfigFiles": true,
"router": true,
"routerHistoryMode": true,
"vuex": false,
"cssPreprocessor": "less",
"plugins": {
"@vue/cli-plugin-babel": {},
"@vue/cli-plugin-eslint": {
"lintOn": ["save", "commit"]
},
"@ziyi2/vue-cli-plugin-ui-base": {},
"@ziyi2/vue-cli-plugin-ui-cz": {},
"@ziyi2/vue-cli-plugin-ui-lint": {}
}
}
複製程式碼
這裡採用了官方設計的@vue/cli-plugin-babel和@vue/cli-plugin-eslint外掛,同時自己設計了額外的三個外掛來支援整個新的UI框架的起步:
@ziyi2/vue-cli-plugin-ui-base
:UI框架基礎外掛,生成Monorepo結構的原始碼目錄(加入Lerna管理工具),生成基礎通用的webpack配置(在VUE CLI 3的webpack配置上進行再配置,VUE CLI3提供了vue.config.js
檔案供開發者進行webpack再配置),提供了幾個基礎UI元件的示例(僅參考價值)。@ziyi2/vue-cli-plugin-ui-cz
: UI框架的cz介面卡外掛,加入了cz-customizable、commitlint、conventional-changelog,用於生成Angular規範的Git提交說明、檢測提交說明是否符合規範以及自動生成UI框架的升級日誌等。@ziyi2/vue-cli-plugin-ui-lint
:UI框架的lint-staged外掛,程式碼提交前會執行Eslint校驗,校驗不通過則不允許提交辣雞程式碼。
這三個外掛已經發布在npm的倉庫裡,如果是已有的Vue CLI 3專案,可直接通過
vue add @ziyi2/ui-cz
等命令進行安裝使用,外掛原始碼地址ziyi2/vue-cli3-lerna-ui/plugins,如果想學習設計Vue CLI 3外掛,可參考外掛開發指南,不過官方文件可能不夠詳細,建議參考官方設計的外掛或者別人設計的優秀外掛。
Lerna
Lerna是一個Monorepo管理工具,使所有的元件(npm包)設計都整合在一個git倉庫裡,同時可以利用yarn的workspace特性,模擬釋出的元件環境,從而使元件的開發和測試變得簡單,不需要多次進行元件的釋出測試(這裡用Lerna進行Vue CLI外掛開發也非常方便)。
同時Lerna還整合了版本釋出工具,可以快速的對UI框架進行版本釋出。
UI元件各自修復問題或者新增功能可以各自快速釋出
PATCH
和MINOR
版本,如果UI元件整體有非相容性更新,可以利用Lerna進行MAJOR
版本釋出,更多關於版本釋出規範可檢視語義化版本。
UI框架實踐
利用Vue CLI 3的遠端Preset,這裡將自己設計的UI框架分享給大家進行實踐使用:
// 可能獲取會有點慢,大家耐心等待
vue create --preset ziyi2/vue-cli3-lerna-ui my-project --packageManager yarn
複製程式碼
如果報錯
unable to get local issuer certificate
,可以設定git config --global http.sslVerify false
。
如果遠端確實獲取preset.json失敗,可以採用本地的方式,將preset.json配置複製下來,放入新建的preset.json
檔案,執行以下命令生成UI框架:
vue create --preset ziyi2/vue-cli3-lerna-ui my-project --packageManager yarn
複製程式碼
執行後的生成過程如下:
指令碼命令
// 啟動開發服務
"serve": "vue-cli-service serve",
// 生成靜態資源
"build": "vue-cli-service build",
// Eslint校驗
"lint": "vue-cli-service lint",
// 安裝和連結Lerna repo的依賴
"bootstrap": "lerna bootstrap",
// 更新升級日誌
"cz:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md",
// 構建
"lib": "lerna run lib"
複製程式碼
如果需要利用GitHub Pages釋出靜態資源,可以新增命令
"deploy": "npm run build && gh-pages -d dist"
,需要安裝gh-page
依賴。
啟動
進入專案目錄,使用yarn serve
命令啟動開發態檢視
這裡給出了國際化、選擇器、警告以及按鈕等UI設計示例。
構建
執行lerna run lib
後(構建可以配合npm run lint
校驗,校驗不通過則構建失敗),Lerna工具會對每一個npm包執行lib
指令碼:
這裡分別對utils
、btn
、theme
包進行了構建處理,其中btn
採用了Vue CLI 3預設的構建庫配置。
Monorepo結構
UI框架生成並構建後的Monorepo結構如下:
.
├── packages # workspaces
│ ├── alert # 警告(不構建)
│ │ ├── alert.vue # 元件原始碼
│ │ ├── index.js # npm包入口檔案
│ │ └── package.json # npm包描述檔案
│ ├── btn # 按鈕
│ │ ├── lib # 目標檔案
│ │ │ └── lib.common.js # npm包入口檔案
│ │ ├── btn.vue # 元件原始碼
│ │ ├── index.js # 構建入口檔案
│ │ ├── package.json # npm包描述檔案(需要vue cli的開發態依賴)
│ │ └── vue.config.js # 構建配置檔案
│ ├── locale # 國際化
│ │ ├── lang # 語言包
│ │ │ ├── enjs # 英文
│ │ │ └── zh_CN.js # 中文
│ │ ├── mixins # 各個元件呼叫的國際化API
│ │ ├── src # 原始碼
│ │ ├── index.js # npm包入口檔案
│ │ ├── alert.vue # 元件原始碼
│ │ ├── index.js # npm包入口檔案
│ │ └── package.json # npm包描述檔案
│ ├── select # 選擇器(類似於alert)
│ ├── theme # 樣式
│ │ ├── lib # 目標檔案
│ │ │ ├── alert.css # 警告樣式
│ │ │ ├── btn.css # 按鈕樣式
│ │ │ ├── index.css # 總體樣式
│ │ │ └── select.css # 選擇器樣式
│ │ ├── src # 原始檔
│ │ │ ├── utils # 通用方法和變數
│ │ │ ├── alert.less # 警告樣式
│ │ │ ├── btn.less # 按鈕樣式
│ │ │ ├── index.less # 總體樣式
│ │ │ └── select.less # 選擇器樣式
│ │ ├── gulpfile.js # 構建配置檔案
│ │ └── package.json # npm包描述檔案
│ └── utils # 工具方法
│ ├── lib # 目標檔案(這裡也可以採用lodash的方式,去掉lib資料夾這一層)
│ ├── src # 原始檔
│ ├── babel.config.js # 構建配置檔案
│ └── package.json # npm包描述檔案
├── public # 公共資源目錄
├── src # 開發態目錄
├── .browserslistrc # UI框架目標瀏覽器配置
├── .cz-config.js # cz定製化提交說明配置
├── .gitignore # git忽略配置
├── .lintstagedrc # lint-staged配置
├── babel.config.js # vue cli的babel配置
├── lerna.json # lerna配置
├── package.json # vue cli容器描述檔案(容器不是npm包)
├── postcss.config.js # postcss配置
├── README.md # 說明
└── vue.common.js # 通用的元件構建配置檔案
複製程式碼
這裡重點說明src
檔案,src
檔案可以根據開發需要自行選定方案:
- 1、使用預設的CLI服務進行開發和UI框架Demo演示,這裡UI框架採用原生的
.vue
檔案形式進行Demo演示,如果想使用.md
檔案進行演示,可以採用vue-markdown-loader。 - 2、使用Vue 驅動的靜態網站生成器VuePress,這個目前不是很穩定。
釋出
釋出完全可以按照語義化版本進行:
- 1、各自npm包可以使用
npm publish
快速釋出MINOR
和PATCH
版本。 - 2、如果某個npm包有非相容性更新,那麼可以使用
lerna publish
釋出MAJOR
版本。
使用Lerna工具釋出的npm包建議採用scope的形式釋出,UI框架示例沒有給出Demo,如果想採用scope形式釋出可以檢視ziyi2/vue-cli3-lerna-ui,需要在每個npm包的package.json
中做額外的配置,具體可檢視vue-cli3-lerna-ui/plugins/vue-cli-plugin-ui-base/package.json的publishConfig
欄位資訊。
如果
lerna publish
釋出失敗,例如報403
錯誤等,可以使用lerna publish from-package
命令釋出,具體檢視官方說明Lerna/publish/usage。
總結
對比Element的UI框架設計,採用Vue CLI 3 & Lerna的形式可以簡化UI框架的配置,使各個UI元件的構建配置互相獨立,對於簡單的UI元件可以利用Vue CLI 3的預設webpack配置。同時採用Monorepo的設計結構(Why is Babel a monorepo?),配合Lerna工具,可以使得UI框架修復問題和釋出新功能的響應能力變得更快。
生成UI框架實踐專案的github地址是ziyi2/vue-cli3-lerna-ui,包括了preset.json
、自己設計的Vue CLI外掛以及自己設計的一系列UI元件(和生成的UI框架示例稍有不同),如果覺得整體結構有不合理的或者考慮不夠全面的地方,歡迎大家提issue,這樣我也可以對它進行完善。如果大家感興趣,希望大家能夠Star一下,這裡拜謝大家了!
參考連結
- Element - github
- npm scripts 使用指南 - 阮一峰
- umd - github
- Element官方文件
- eslint - eslint文件
- Module Definition Systems - Webpack文件
- eslint-loader - github
- webpack-node-externals - github
- Externals - Webpack文件
- babel-plugin-module-resolver - github
- Gulp - Gulp文件
- Vue CLI 3/開發/構建目標/庫 - Vue CLI文件
- Vue CLI 3/開發/webpack相關/審查專案的webpack配置 - Vue CLI文件
- Vue CLI 3/基礎/外掛和Preset - Vue CLI文件
- @vue/cli-plugin-babel - Vue CLI Plugin
- @vue/cli-plugin-eslint - Vue CLI Plugin
- cz - Git提交說明工具
- cz-customizable - Cz介面卡,自定義說明
- commitlint - Cz介面卡,提交說明檢測
- conventional-changelog - Cz介面卡,生成日誌
- lint-staged - 程式碼提交稽核工具
- 外掛開發指南 - Vue CLI文件
- 語義化版本 - 版本釋出規範
- Vue CLI3/基礎/CLI服務 - Vue CLI文件
- Why is Babel a monorepo?