探索組件庫(kù)設(shè)計(jì)的無限可能性
1. 前言
當(dāng)前端開發(fā)團(tuán)隊(duì)面臨業(yè)務(wù)的不斷增長(zhǎng)和項(xiàng)目數(shù)量的持續(xù)增加時(shí),兩個(gè)主要問題逐漸凸顯:1.業(yè)務(wù)組件復(fù)用的挑戰(zhàn);2. 代碼一致性和質(zhì)量維護(hù)問題。前端團(tuán)隊(duì)為了解決這些問題,通常會(huì)選擇構(gòu)建業(yè)務(wù)組件庫(kù)。其主要目標(biāo)是:
- 提高開發(fā)效率:開發(fā)人員可以在不同項(xiàng)目中復(fù)用組件,從而減少重復(fù)工作,提高開發(fā)效率。
- 保持一致的代碼實(shí)現(xiàn):可以確保在不同項(xiàng)目中使用相同的代碼實(shí)現(xiàn),避免了風(fēng)格不一致和質(zhì)量差異。
- 質(zhì)量保障:組件庫(kù)中的組件經(jīng)過嚴(yán)格驗(yàn)證和測(cè)試,能夠提供高質(zhì)量的代碼。
- 易于維護(hù)和升級(jí):作為獨(dú)立的模塊,業(yè)務(wù)組件庫(kù)更容易進(jìn)行維護(hù)和升級(jí),使開發(fā)人員能夠更專注于組件庫(kù)本身的質(zhì)量。
- 知識(shí)共享和技術(shù)積累:組件庫(kù)可以成為團(tuán)隊(duì)共享技術(shù)知識(shí)和經(jīng)驗(yàn)的平臺(tái),幫助提升整體的技術(shù)水平。
因此,構(gòu)建業(yè)務(wù)組件庫(kù)有助于解決業(yè)務(wù)組件復(fù)用、代碼統(tǒng)一性和質(zhì)量維護(hù)等問題,為不斷發(fā)展的業(yè)務(wù)環(huán)境提供了高效、統(tǒng)一和可維護(hù)的開發(fā)流程。
2. 實(shí)現(xiàn)分析
在構(gòu)建業(yè)務(wù)組件庫(kù)時(shí),需要深入調(diào)研和選擇適當(dāng)?shù)募夹g(shù)方案,驗(yàn)證方案的可行性,最終將各個(gè)解決方案集成到一起,以實(shí)現(xiàn)高效的組件庫(kù)開發(fā)和維護(hù)。下面我們將通過整體架構(gòu)、構(gòu)建、質(zhì)量監(jiān)控、站點(diǎn)搭建、組件質(zhì)量、組件SOP這六大模塊對(duì)我們的業(yè)務(wù)組件庫(kù)進(jìn)行分析。
圖片
3. 整體架構(gòu)設(shè)計(jì)
對(duì)于業(yè)務(wù)組件庫(kù)的整體架構(gòu)設(shè)計(jì)而言,核心問題是業(yè)務(wù)組件庫(kù)的代碼時(shí)如何來組織和管理。首先,我們把代碼倉(cāng)庫(kù)建好。業(yè)界一般會(huì)把同一類組件庫(kù)用單個(gè)倉(cāng)庫(kù)的形式維護(hù),并且把組件開發(fā)成NPM包的形式,這里的重點(diǎn)是,你要考慮把所有的組件打包成一個(gè)大的NPM包,還是分割是一個(gè)個(gè)獨(dú)立的小NPM包。不要小看這個(gè)問題,這兩種選擇會(huì)使倉(cāng)庫(kù)的目錄結(jié)構(gòu)有不小的差異,進(jìn)一步又會(huì)影響到后面組件的開發(fā),構(gòu)建,發(fā)布,實(shí)現(xiàn)的技術(shù)設(shè)計(jì)。
業(yè)界組件庫(kù)的架構(gòu)常見單包和多包兩種。單包適合簡(jiǎn)單場(chǎng)景,組件集中在一個(gè)庫(kù)中。多包則將組件分成獨(dú)立包,適應(yīng)多項(xiàng)目需求,增強(qiáng)靈活性和復(fù)用性。
3.1單包是什么
把所有的組件看成一個(gè)整體,一起打包發(fā)布。單個(gè)倉(cāng)庫(kù),單個(gè)包,統(tǒng)一維護(hù)統(tǒng)一管理。
? 優(yōu)點(diǎn)
- 可以通過相對(duì)路徑實(shí)現(xiàn)組件與組件之間的引用,公共代碼之間的引用
- 維護(hù)成本低,只維護(hù)一套package.json配置
? 缺點(diǎn)
- 組件完全耦合,必須把它作為一個(gè)整體統(tǒng)一發(fā)包,就算只改一個(gè)非常小的功能,都要對(duì)整個(gè)包發(fā)布更新
- 搭建場(chǎng)景重復(fù)打包
比如說Antd,它就是作為一個(gè)整體的包來盡進(jìn)行管理的。選擇使用單包架構(gòu)的話,那么你就必須提供按需加載的能力,以降低使用者的成本,你可以考慮支持ESModules的Treeshaking的功能來實(shí)現(xiàn)按需加載的能力。當(dāng)然你也可以選擇另外一種方案,叫做"多包架構(gòu)"。
3.2多包是什么
每個(gè)組件彼此獨(dú)立,單獨(dú)打包發(fā)布,單個(gè)倉(cāng)庫(kù)多個(gè)包,統(tǒng)一維護(hù)單獨(dú)管理。
? 優(yōu)點(diǎn)
組件發(fā)布靈活,并且大然支持按需使用
? 缺點(diǎn)
- 維護(hù)成本高,每個(gè)組件都需要一套package配置。
- 組件與組件之間物理隔離,對(duì)于相互依賴,公共代碼抽象等場(chǎng)景,就只能通過NPM包引用的方式來實(shí)現(xiàn)。
- 多依賴多版本問題
- 常用邏輯片段/各個(gè)組件都需要的依賴和邏輯
在這些場(chǎng)景下的開發(fā)統(tǒng)一發(fā)布,相對(duì)來說沒那么方便,多包架構(gòu)在業(yè)界稱之為"Monorepo"。
圖片
3.3結(jié)論
ZjDesign組件庫(kù)使用場(chǎng)景比較特殊,組件之間的依賴關(guān)系比較強(qiáng),互相會(huì)組合形式新的組件,所以在這里選用的單包開發(fā)模式進(jìn)行開發(fā)。單包開發(fā)模式可以減少我們開發(fā)維護(hù)成本,開發(fā)工作量的減少,提升組件之間的引用效率。
4. 組件庫(kù)構(gòu)建
當(dāng)你確定了整體架構(gòu)之后,就可以開始具體的功能點(diǎn)實(shí)現(xiàn)了。業(yè)務(wù)組件庫(kù)要求整體框架提供基礎(chǔ)的技術(shù)能力。
4.1項(xiàng)目打包
提到構(gòu)建工具,大家肯定一下會(huì)想到很多一堆工具:webapck、gulp、rollup等。網(wǎng)上有很多文章分析它們分別更適合哪些場(chǎng)景,webpack更適合打包組件庫(kù)、應(yīng)用程序之類的應(yīng)用,而rollup更適合打包純js的類庫(kù)。下面我們來對(duì)比一下webpack和rollup兩者的區(qū)別。
?rollup使用流程
- 無需考慮瀏覽器兼容問題,開發(fā)者寫esm代碼 -> rollup通過入口,遞歸識(shí)別esm模塊 -> 最終打包成一個(gè)或多個(gè)bundle.js -> 瀏覽器直接可以支持引入
- 需考慮瀏覽器兼容問題,可能會(huì)比較復(fù)雜,需要用額外的polyfill庫(kù),或結(jié)合webpack使用
打包成npm包:
- 開發(fā)者寫esm代碼 -> rollup通過入口,遞歸識(shí)別esm模塊 -> (可以支持配置輸出多種格式的模塊,如esm、cjs、umd、amd)最終打包成一個(gè)或多個(gè)bundle.js
- (開發(fā)者要寫cjs也可以,需要插件@rollup/plugin-commonjs)初步看來
- 很明顯,rollup 比較適合打包js庫(kù)(react、vue等的源代碼庫(kù)都是rollup打包的)或 高版本無需往下兼容的瀏覽器應(yīng)用程序(現(xiàn)在2022年了,時(shí)間越往后,遷移到rollup會(huì)越多,猜測(cè))
- 這樣打包出來的庫(kù),可以充分使用上esm的tree shaking,使源庫(kù)體積最小
?webpack和rollup打包比對(duì)
let foo = () => {
let x = 1;
if (false) {
console.log("never reached");
}
let a = 3;
return a;
};
let baz = () => {
var x = 1;
console.log(x);
post();
function unused() {
return 5;
}
return x;
let c = x + 3;
return c;
};
baz();
測(cè)試對(duì)比兩個(gè)打包工具對(duì)Dead Code的打包結(jié)果
打包對(duì)比結(jié)果:中間是源代碼,左邊是rollup打包結(jié)果,右邊是webpack打包結(jié)果對(duì)比。
圖片
webpack打包效果(有很多注入代碼)
- 實(shí)際上,我們自己寫的代碼在最下面。上面注入的大段代碼 都是webpack自己的兼容代碼,目的是自己實(shí)現(xiàn)require,modules.exports,export,讓瀏覽器可以兼容cjs和esm語(yǔ)法
- 可以理解為,webpack自己實(shí)現(xiàn)polyfill支持模塊語(yǔ)法,rollup是利用高版本瀏覽器原生支持esm(所以rollup無需代碼注入)
具體細(xì)節(jié)rollup和webapck的源碼實(shí)現(xiàn)差異在這里不做過多贅述,大家可以自己深入研究。
? 構(gòu)建出 esm、cjs 格式
選擇Rollup來打包組件庫(kù),需要有幾點(diǎn)注意:
- 配置包格式為 esm、cjs、umd
- external 掉vue,組件庫(kù)不建議將 Vue 打包進(jìn)去
rollup 配置如下:
{
input: file,
output: {
compact: true,
file: `lib/index.js`,
format: 'es',
name,
sourcemap: false,
globals: {
echarts: 'echarts',
vue: 'Vue'
}
},
external: [
'echarts', 'vue'
],
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production')
}),
vue({
css: false,
template: {
isProduction: true
},
modules: true,
styles: {
scoped: true,
trim: true
}
}),
postcss({
extract: true,
modules: false,
scoped: true,
sourceMap: false,
autoModules: true,
plugins: [
simplevars(),
nested(),
cssnano(),
base64({
extensions: ['.png', '.jpeg', '.jpg', '.gif'],
root: './assets/'
}),
autoprefixer({
add: true
})
],
extensions: ['.css', '.less'],
use: {
less: {
javascriptEnabled: true
}
}
}),
babel({
runtimeHelpers: true,
exclude: 'node_modules/**',
plugins: [
['@babel/plugin-proposal-optional-chaining', { loose: false }]
],
presets: [
['@babel/preset-env', { targets: '> 0.25%, not dead' }]
]
}),
url({
limit: 10 * 1024,
emitFiles: true
}),
progress(),
buble({
transforms: { forOf: false }
}),
uglify({
ie8: true
})
]
}
4.2 版本控制
組件庫(kù)發(fā)布版本號(hào)的管理是很重要的,如何來維護(hù)我們的版本號(hào)?只能動(dòng)手在package.json中修改嗎?其實(shí)可以在打包執(zhí)行命令的時(shí)候,通過命令及參數(shù)幫助我們實(shí)現(xiàn)自動(dòng)升級(jí)版本號(hào)的目的。比如我們?cè)诖驕y(cè)試環(huán)境包的時(shí)候可以使用(cross-env用來指定變量NODE_ENV的值)。
"scripts": {
"test": "npm version patch && cross-env NODE_ENV=testing node build/build.js"
}
下面我們來看看npm version命令具體的使用方式:npm采用了semver規(guī)范作為依賴版本管理方案。semver約定一個(gè)包的版本號(hào)必須包含3個(gè)數(shù)字。
MAJOR.MINOR.PATCH 意思是 主版本號(hào).小版本號(hào).修訂版本號(hào)
MAJOR 對(duì)應(yīng)大的版本號(hào)迭代,做了不兼容舊版的修改時(shí)要更新MAJOR版本號(hào)
MINOR 對(duì)應(yīng)小版本迭代,發(fā)生兼容舊版API的修改或功能更新時(shí),更新MINOR版本號(hào)
PATCH 對(duì)應(yīng)修訂版本號(hào),一般針對(duì)修復(fù)BUG的版本號(hào)
當(dāng)我們每次發(fā)布包的時(shí)候都需要升級(jí)版本號(hào):
"scripts": {
"rollup": "rollup -c rollup.config.js",
"publish:major": "npm version major && npm publish",
"publish:minor": "npm version minor && npm publish",
"publish:patch": "npm version patch && npm publish",
"publish:beta": "npm version prerelease --preid=beta && npm publish --tag=beta"
},
4.3發(fā)布
npm包發(fā)布使用之家npm進(jìn)行發(fā)布,發(fā)布流程如下:
1. 首先需要配置私有包,配置一次即可
$ npm config set @auto:ZjDesign http://xxxx.com/
2. 使用如下命令在私有倉(cāng)庫(kù)中添加用戶(配置一次即可)
npm adduser --registry http://xxxx.com/
3. 執(zhí)行打包命令
npm run rollup
4.私有包發(fā)布
npm publish --registry http://xxxx.com/
5. 組件搭建實(shí)例
首先看一下我們單個(gè)組件UI設(shè)計(jì)圖。從圖中可以看出,每個(gè)組件實(shí)例demo可以看成抽象五大模塊。1.組件的title+subtitle、2.組件描述、3.多個(gè)組件形態(tài)展示、4.設(shè)計(jì)原則與頁(yè)面布局、5.單個(gè)組件形態(tài)的代碼示例。
圖片
5.1組件demo整體目錄
圖片
index.zh-CN.md: 作為靜態(tài)數(shù)據(jù)的快速輸出,包含組件名稱、描述、設(shè)計(jì)原則和API。從.md格式文件中可以使用插件md(vite插件)解析出組件需要的數(shù)據(jù),這個(gè)在后面單獨(dú)講解實(shí)現(xiàn)細(xì)節(jié)。
圖片
單個(gè)組件類型文件:包含組件排序,title,描述,html。這個(gè)通過docs進(jìn)行數(shù)據(jù)的解析,具體解析后面進(jìn)行詳細(xì)講解。
圖片
5.2Docs插件
作用:將單例中的.vue文件中docs標(biāo)簽數(shù)據(jù)進(jìn)行格式處理,docs插件流程圖。
圖片
實(shí)現(xiàn)代碼:
export default (options: Options = {}): Plugin => {
const { root, markdown } = options
const vueToMarkdown = createVueToMarkdownRenderFn(root)
const markdownToVue = createMarkdownToVueRenderFn(root, markdown)
return {
name: 'vueToMdToVue',
async transform(code, id) {
if (id.endsWith('.vue') && id.indexOf('/demo/') > -1 && id.indexOf('index.vue') === -1) {
const res = vueToMarkdown(code, id)
return {
code: res.ignore ? res.vueSrc : (await markdownToVue(res.vueSrc, id)).vueSrc,
map: null
}
}
}
}
}
vueToMarkdown函數(shù)實(shí)現(xiàn):這里面使用了lru-cache進(jìn)行緩存處理,對(duì)于已經(jīng)解析完成的文件進(jìn)行跟蹤,這樣可以加快文檔展示。通過fetchCode方法對(duì)自定義標(biāo)簽內(nèi)容進(jìn)行獲取。
5.3 MarkDown插件
作用:將markdown文檔格式數(shù)據(jù)轉(zhuǎn)化成我們想要的vue格式化數(shù)據(jù)。
這里主要通過對(duì)第三方markdown-it,依據(jù)UI設(shè)計(jì)的要求進(jìn)行定制化的修改??梢灾С州斎雃moji,anchor,toc分別使用markdown-it-emoji、markdown-it-anchor、markdown-it-table-of-contents插件。
? md插件實(shí)現(xiàn)流程
1、定義插件導(dǎo)出,基于vite的Plugin進(jìn)行封裝:
import { createMarkdownToVueRenderFn } from './markdownToVue';
import type { MarkdownOptions } from './markdown/markdown';
import type { Plugin } from 'vite';
interface Options {
root?: string;
markdown?: MarkdownOptions;
}
export default (options: Options = {}): Plugin => {
const { root, markdown } = options;
const markdownToVue = createMarkdownToVueRenderFn(root, markdown);
return {
name: 'mdToVue',
async transform(code, id) {
if (id.endsWith('.md')) {
// transform .md files into vueSrc so plugin-vue can handle it
return { code: (await markdownToVue(code, id)).vueSrc, map: null };
}
},
};
};
2、markdownToVue核心思想實(shí)現(xiàn):
通過lru-cache進(jìn)行解析文檔的緩存處理,使用gray-matter對(duì)docs格式數(shù)據(jù)的解析,最后生成demo-box組件格式的vue文件。
export function createMarkdownToVueRenderFn(root: string = process.cwd(), options: MarkdownOptions = {}) {
const md = createMarkdownRenderer(options)
return async (src: string, file: string): Promise=> {
const relativePath = slash(path.relative(root, file))
const cached = cache.get(src)
if (cached) {
debug(`[cache hit] ${relativePath}`)
return cached
}
const start = Date.now()
const { content, data: frontmatter } = matter(src)
// eslint-disable-next-line prefer-const
let { html, data } = md.render(content)
// avoid env variables being replaced by vite
html = html.replace(/import\.meta/g, 'import.meta').replace(/process\.env/g, 'process.env')
// TODO validate data.links?
const pageData: PageData = {
title: inferTitle(frontmatter, content),
description: inferDescription(frontmatter),
frontmatter,
headers: data.headers,
relativePath,
content: escapeHtml(content),
html,
// TODO use git timestamp?
lastUpdated: Math.round(fs.statSync(file).mtimeMs)
}
const newContent = data.vueCode
? await genComponentCode(md, data, pageData)
: `${html}${fetchCode(content, 'style')}`
debug(`[render] ${file} in ${Date.now() - start}ms.`)
const result = {
vueSrc: newContent?.trim(),
pageData
}
cache.set(src, result)
return result
}
}
6. 組件沉淀-SOP
在開發(fā)組件并將其沉淀為組件庫(kù)時(shí),建立合適的SOP機(jī)制可以提高開發(fā)效率、保持一致性,并促進(jìn)團(tuán)隊(duì)合作。以下是從組件設(shè)計(jì)到溝通、開發(fā)、沉淀為組件庫(kù)的SOP機(jī)制:
圖片
? 組件設(shè)計(jì):
設(shè)計(jì)同學(xué)進(jìn)行界面設(shè)計(jì),定義組件統(tǒng)一規(guī)范。根據(jù)多個(gè)業(yè)務(wù)方進(jìn)行公共組件的提取,確定組件的用途、功能。
?評(píng)審:
設(shè)計(jì)同學(xué)和研發(fā)同學(xué)進(jìn)行組件設(shè)計(jì)UI的評(píng)審,研發(fā)同學(xué)定義組件的輸入和輸出,以及可能的配置項(xiàng)。并且進(jìn)行編寫組件的詳細(xì)需求文檔,包括示例代碼和用法示例。
? 開發(fā)階段:
根據(jù)組件設(shè)計(jì)和需求文檔,進(jìn)行組件的開發(fā)。使用規(guī)范的編碼風(fēng)格和最佳實(shí)踐。在開發(fā)過程中進(jìn)行單元測(cè)試和集成測(cè)試,確保組件的穩(wěn)定性和正確性。
? 文檔編寫:
編寫組件的文檔,包括組件的用途、API文檔、示例代碼等。提供詳細(xì)的使用說明,幫助其他開發(fā)人員使用組件。
? CODE-REVIEW:
使用版本管理工具(如Git)來管理組件的代碼。進(jìn)行代碼審查,確保代碼質(zhì)量和一致性。
? 測(cè)試與驗(yàn)收:
在真實(shí)項(xiàng)目中測(cè)試組件,確認(rèn)其在不同場(chǎng)景下的穩(wěn)定性和可用性。進(jìn)行驗(yàn)收測(cè)試,確保組件滿足預(yù)期要求。設(shè)計(jì)同學(xué)進(jìn)行UI驗(yàn)收。
? 發(fā)布:
根據(jù)版本號(hào)規(guī)則,發(fā)布組件庫(kù)的新版本。定期更新組件庫(kù),修復(fù)bug、添加新功能等。
7. 總結(jié)
目前,ZjDesign業(yè)務(wù)組件庫(kù)正在不斷豐富中。我們努力開發(fā)具有高擴(kuò)展性和低上手成本的組件。并且組件庫(kù)已有多個(gè)新項(xiàng)目接入,整體開發(fā)效率明顯提升,減少了重復(fù)開發(fā)。組件庫(kù)的搭建為團(tuán)隊(duì)提供了一個(gè)統(tǒng)一的技術(shù)平臺(tái),促進(jìn)了知識(shí)分享和合作。這一系列改進(jìn)加速了產(chǎn)品交付,并推動(dòng)了整體開發(fā)流程的提升。
作者簡(jiǎn)介
何彪
■ 主機(jī)廠事業(yè)部-技術(shù)部-數(shù)科技術(shù)團(tuán)隊(duì)
■2023年2月加入汽車之家,目前任職于主機(jī)廠事業(yè)部-技術(shù)部-數(shù)科技術(shù)團(tuán)隊(duì),主要負(fù)責(zé)數(shù)科前端業(yè)務(wù),組件庫(kù)搭建等工作。