在前端專案的開發中,往往會根據業務需求,沉澱出一些專案內的UI元件/功能模組(以下通稱元件) 等;這些元件初期只在同一個專案中被維護,並被該專案中的不同頁面或模組複用,此時的元件逐步被完善,是一個只聚焦於功能和健壯性的成長期。
隨著業務的發展,原來的專案可能不得不產生裂變,變成幾個相似但各有不同的專案 -- 比如在初始專案中積累經驗後,需要推廣到相似的業態上或根據不同大客戶的需求進行定製,這種情況下往往很難理想化的保持各專案大版本或者後續發展進度的同步,只能逐漸各自發展。這時那些在一開始顯得八面玲瓏的“可複用元件”,往往就需要手忙腳亂的在各個專案中分頭維護,或是出現了意想不到的問題,需要重新規劃了。
本文以 Vue 技術棧的前端專案為例,嘗試簡單的探討一種抽象提取跨專案可複用元件的方法。
可複用元件的常見現狀
- 元件的複用侷限在單個專案中
- 一次開發,n 次復
用制 - 專案的裂變讓問題成倍放大,每個修正/改動要同步 n 次
- 兄弟專案的依賴庫可能相似但不同,或版本差距較大
- 單元測試環境或版本的不同也讓元件的複用帶來問題
關於同一元件在不同專案中的區別方面,以一個二次封裝 element-ui 中 el-date-picker 的 DateRange.vue
元件舉例:
所在專案 | 基礎元件庫 | 發現的代表性問題 |
---|---|---|
A | element-ui@v1.x |
|
B | ||
C | ||
D | ||
E | element-ui@v2.x |
|
F |
由於種種原因,幾個專案依賴的 UI 庫相似但並不相同;且專案體量過大、維護的團隊不同等等,都讓統一基礎元件庫變得?乎不可能~ ? 你太美,這就很尷尬了嘛~
如何收斂維護點?
- 僅以例子中的幾個專案來說,維護點就在 6 個,工作量×6
- 如果 收斂到一個統一的庫 中,則維護點變為 2 個,僅需區分基礎版本庫的差別
- 而大部分較簡單的元件,基礎元件庫的版本不同並不會造成差異的,或是根本沒有引用 element-ui 元件庫的簡單元件,則維護點直接能縮減到 1 個
什麼樣的元件是通用的?
- 足夠抽象,不包含業務邏輯,或擴充套件性足夠好
- 儘量不包含
$t
、$router
等和專案環境有關的依賴 - 有覆蓋率足夠高的單元測試
- 有必要的文件,或通過單元測試描述了足夠完整的功能
- 最好也提供可執行的例子
釋出到 npm
在某一個具體專案內,對元件只需引用其原始碼即可;
對於跨專案的通用元件庫,一種方法是在各專案內部維護一個指向元件庫原始碼的子模組(git 的 subtree 或 submodule),但這種方法維護比較麻煩,故不常用。
另一種我們比較習慣的方式是通過 npm 安裝後直接引用元件的註冊名稱(package.json
中的 name
)。
當然如果自己的元件多少還是關乎業務邏輯、對外部的專案其實也沒那麼通用,而公司內部又維護有 npm 的映象,那麼選擇將其釋出到這個內部環境中也是可以的。
釋出 npm 元件的主要步驟:
在 npmjs.com 上註冊使用者,或通過命令列:
npm adduser
複製程式碼
釋出前確認登入:
npm login
複製程式碼
釋出前手動更改 package.json ,或用命令列更新專案版本號,注意每次釋出的版本號不能相同:
npm version x.x.x
複製程式碼
執行釋出:
npm publish
複製程式碼
直接在命令列中開啟專案主頁檢視:
npm home [name]
複製程式碼
更多的命令參見官方的完整文件: docs.npmjs.com/cli-documen…
另外需要注意的是,正確配置 package.json 裡的 repository
欄位,可以在元件的 npm 主頁上顯示程式碼倉庫的連結。
用 rollup 而不是 webpack 打包元件
本例中選擇了 rollup 作為打包工具:
- webpack 雖然功能強大,但配置複雜、生成的程式碼冗餘較多
- rollup 更適用於庫、元件等型別原始碼的編譯
- rollup 基於外掛擴充套件打包功能,且配置相對簡單
- rollup 的配置項和 webpack 高度相似,便於遷移和適應
一套基本的配置
假設元件庫結構規劃如下:
├─.babelrc
├─.eslintignore
├─.eslintrc.js
├─.gitignore
├─CHANGELOG.md
├─jest.config.js
├─package.json
├─README.md
├─postcss.config.js
├─rollup.config.js
├─dist/
├─example/
├─node_modules/
├─src/
├─__mocks__/
└─__tests__/
複製程式碼
最小化的 npm scripts 如下:
// package.json
"scripts": {
"build": "rollup --config"
},
複製程式碼
較基礎的 rollup 配置如下:
// rollup.config.js
import path from 'path';
import json from 'rollup-plugin-json';
import { uglify } from 'rollup-plugin-uglify';
import alias from 'rollup-plugin-alias';
import vue from 'rollup-plugin-vue';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
import nodeGlobals from 'rollup-plugin-node-globals';
import bundleSize from 'rollup-plugin-filesize';
import { eslint } from 'rollup-plugin-eslint';
import pkg from './package.json';
const pathResolve = p => path.resolve(__dirname, p);
const extensions = ['.js', '.vue'];
module.exports = {
input: 'src/index.js',
output: {
file: 'dist/bundle.min.js',
format: 'umd',
name: 'MyComponents',
globals: {
vue: 'Vue',
echarts: 'echarts',
lodash: 'lodash'
},
sourcemap: true
},
external: Object.keys(pkg.dependencies),
plugins: [
resolve({
extensions,
browser: true
}),
eslint({
extensions,
exclude: ['**/*.json'],
cache: true,
throwOnError: true
}),
bundleSize(),
commonjs(),
nodeGlobals(),
vue({
template: {
isProduction: !process.env.ROLLUP_WATCH,
compilerOptions: { preserveWhitespace: false }
},
css: true
}),
babel({
exclude: 'node_modules/**'
}),
alias({
'@': pathResolve('src')
}),
json(),
uglify()
]
};
複製程式碼
關於該配置,簡要說明如下:
- 上例中外掛的順序是重要的
- node-globals 外掛會將 process 等變數注入打包後的檔案
- eslint 外掛會在打包之前檢查語法,並且基本能複用平時專案中的 .eslintrc.js 配置檔案
- bundleSize 外掛用來在打包後顯示目標檔案的體積
- vue 外掛中的 css 欄位,表示是否將內嵌樣式打包到目標 js 中
- 繼續使用 babel,而不是也經常和 rollup 搭配的更輕量的 buble 來編譯 ES6 程式碼,目的也是和 jest 複用
- json 元件解決原始碼中可能會直接匯入 json 檔案的情況
- external 配置的意思是:package.json 中 dependencies 包含的依賴,都不被打包到元件中,而是需要在具體專案中安裝
相關的語法轉換和語法檢查配置:
// .babelrc
{
"presets": [["env", { "modules": false }]],
"env": {
"test": {
"presets": [["env", { "targets": { "node": "current" } }]]
}
}
}
複製程式碼
// .eslintignore
__tests__/*
*.css
複製程式碼
// .eslintrc.js
module.exports = {
extends: [
"airbnb-base",
'plugin:vue/essential'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': 'error',
'space-before-function-paren': 'off',
'no-underscore-dangle': 'off',
'no-param-reassign': 'off',
'func-names': 'off',
'no-bitwise': 'off',
'prefer-rest-params': 'off',
'no-trailing-spaces': 'off',
'comma-dangle': 'off',
'quote-props': 'off',
'consistent-return': 'off',
'no-plusplus': 'off',
'prefer-spread': 'warn',
semi: 'warn',
indent: 'warn',
'no-tabs': 'warn',
'no-unused-vars': 'warn',
quotes: 'warn',
'no-void': 'off',
'no-nested-ternary': 'off',
'import/no-unresolved': 'off',
'no-return-assign': 'warn',
'linebreak-style': 'off',
'prefer-destructuring': 'off',
'no-restricted-syntax': 'warn'
},
parserOptions: {
parser: 'babel-eslint'
}
}
複製程式碼
配置單元測試環境
維護點收斂到了一個庫中,需要注意的是,相應的風險也高度集中了,可謂一損俱損一榮俱榮?。
所以單元測試也愈發重要起來,庫裡的元件或模組,凡是有條件的(比如 Vue 中的 directives 就沒那麼好做單元測試,但 filters 純函式很容易),想要讓各個專案的開發者小夥伴們放心大膽的統一引用,就應該無條件的買一送一,搭配完善的單元測試。
這裡以 jest 為例,列舉其主要配置:
// jest.config.js
module.exports = {
modulePaths: [
'<rootDir>/src/'
],
moduleFileExtensions: [
'js',
'json',
'vue'
],
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\.jsx?$': 'babel-jest'
},
transformIgnorePatterns: [
'/node_modules/'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': '<rootDir>/__mocks__/emptyMock.js'
},
snapshotSerializers: [
'jest-serializer-vue'
],
collectCoverage: true,
collectCoverageFrom: [
'<rootDir>/src/**/*.{js,vue}',
'!**/node_modules/**'
],
coveragePathIgnorePatterns: [
'<rootDir>/__tests__/'
],
coverageReporters: [
'text'
]
};
複製程式碼
其中 emptyMock.js
是用來在用例中忽略對樣式的引用的:
// __mocks__/emptyMock.js
module.exports = {};
複製程式碼
對應的 npm scripts:
"scripts": {
// ...
"test": "jest"
},
"pre-commit": [
"test"
],
複製程式碼
這裡用 pre-commit 包實現了提交前先進行單元測試的鉤子功能。
關於 Vue 單元測試的更多內容請參考這篇文章。
預覽元件實際效果
光說不練假把式,雖然靜態語法也檢查了、單元測試也跑通了,還是眼見為實比較踏實,對其他開發者也比較直觀;藉助 rollup-plugin-serve 等外掛,可以執行起一個最小配置的瀏覽器執行環境,人肉看看元件的實際表現。
在 npm scripts 中設定環境引數,分別對完全通用的元件,及適用於特定型別專案的元件啟動 demo 頁面服務:
"scripts": {
// ...
"dev:common": "rollup --watch --config rollup.config.dev.js --environment PROJ_ENV:common",
"dev:A": "rollup --watch --config rollup.config.dev.js --environment PROJ_ENV:A",
"dev:B": "rollup --watch --config rollup.config.dev.js --environment PROJ_ENV:B",
},
複製程式碼
適當修改 rollup 原配置,增加單獨的 rollup.config.dev.js,根據環境引數啟動服務:
import serve from 'rollup-plugin-serve';
import postcss from 'rollup-plugin-postcss';
import baseConfig, {
pathResolve,
browserGlobals
} from './rollup.config.base';
...
const PORT = 3001;
const PROJECT = process.env.PROJ_ENV;
export default {
input: pathResolve(`example/${PROJECT}/main.js`),
output: {
file: pathResolve(`example/${PROJECT}/dist/example.bundle.${PROJECT}.js`),
format: 'umd',
name: 'exampleApp',
globals: browserGlobals,
sourcemap: false
},
plugins: [
postcss(),
...baseConfig.plugins,
serve({
port: PORT,
contentBase: [
pathResolve(`example/${PROJECT}`)
]
})
]
};
複製程式碼
這裡假設簡單粗暴的都把元件引用到 App.vue 中,暫不考慮分路由等情況,對應的 example 目錄的結構可能如下:
+---A
| | App.vue
| | index.html
| | main.js
| |
| \---dist
| example.bundle.A.js
|
+---B
| | App.vue
| | index.html
| | foo.css
| | main.js
| |
| \---dist
| example.bundle.B.js
|
\---common
| App.vue
| index.html
| main.js
|
+---dist
| example.bundle.common.js
|
\---fonts
複製程式碼
總結
同時維護幾個同質化的前端專案時,不可避免的涉及到一些較通用的 UI元件/功能模組 的情況,將其集結後釋出到 npm 上,並輔以完善的單元測試和可執行的 demo 展示、必要的文件,就能將維護元件的工作量大大減輕。
--End--
搜尋 fewelife 關注公眾號
轉載請註明出處