1. 前言
当前端开发团队面临业务的不断增长和项目数量的持续增加时,两个主要问题逐渐凸显:1.业务组件复用的挑战;2. 代码一致性和质量维护问题。前端团队为了解决这些问题,通常会选择构建业务组件库。其主要目标是:
- 提高开发效率:开发人员可以在不同项目中复用组件,从而减少重复工作,提高开发效率。
- 保持一致的代码实现:可以确保在不同项目中使用相同的代码实现,避免了风格不一致和质量差异。
- 质量保障:组件库中的组件经过严格验证和测试,能够提供高质量的代码。
- 易于维护和升级:作为独立的模块,业务组件库更容易进行维护和升级,使开发人员能够更专注于组件库本身的质量。
- 知识共享和技术积累:组件库可以成为团队共享技术知识和经验的平台,帮助提升整体的技术水平。
因此,构建业务组件库有助于解决业务组件复用、代码统一性和质量维护等问题,为不断发展的业务环境提供了高效、统一和可维护的开发流程。
2. 实现分析
在构建业务组件库时,需要深入调研和选择适当的技术方案,验证方案的可行性,最终将各个解决方案集成到一起,以实现高效的组件库开发和维护。下面我们将通过整体架构、构建、质量监控、站点搭建、组件质量、组件SOP这六大模块对我们的业务组件库进行分析。
图片
3. 整体架构设计
对于业务组件库的整体架构设计而言,核心问题是业务组件库的代码时如何来组织和管理。首先,我们把代码仓库建好。业界一般会把同一类组件库用单个仓库的形式维护,并且把组件开发成NPM包的形式,这里的重点是,你要考虑把所有的组件打包成一个大的NPM包,还是分割是一个个独立的小NPM包。不要小看这个问题,这两种选择会使仓库的目录结构有不小的差异,进一步又会影响到后面组件的开发,构建,发布,实现的技术设计。
业界组件库的架构常见单包和多包两种。单包适合简单场景,组件集中在一个库中。多包则将组件分成独立包,适应多项目需求,增强灵活性和复用性。
3.1单包是什么
把所有的组件看成一个整体,一起打包发布。单个仓库,单个包,统一维护统一管理。
► 优点
► 缺点
比如说Antd,它就是作为一个整体的包来尽进行管理的。选择使用单包架构的话,那么你就必须提供按需加载的能力,以降低使用者的成本,你可以考虑支持ESModules的Treeshaking的功能来实现按需加载的能力。当然你也可以选择另外一种方案,叫做"多包架构"。
3.2多包是什么
每个组件彼此独立,单独打包发布,单个仓库多个包,统一维护单独管理。
► 优点
组件发布灵活,并且大然支持按需使用
► 缺点
在这些场景下的开发统一发布,相对来说没那么方便,多包架构在业界称之为"Monorepo"。
图片
3.3结论
ZjDesign组件库使用场景比较特殊,组件之间的依赖关系比较强,互相会组合形式新的组件,所以在这里选用的单包开发模式进行开发。单包开发模式可以减少我们开发维护成本,开发工作量的减少,提升组件之间的引用效率。
4. 组件库构建
当你确定了整体架构之后,就可以开始具体的功能点实现了。业务组件库要求整体框架提供基础的技术能力。
4.1项目打包
提到构建工具,大家肯定一下会想到很多一堆工具:webapck、gulp、rollup等。网上有很多文章分析它们分别更适合哪些场景,webpack更适合打包组件库、应用程序之类的应用,而rollup更适合打包纯js的类库。下面我们来对比一下webpack和rollup两者的区别。
►rollup使用流程
打包成npm包:
►webpack和rollup打包比对
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();
测试对比两个打包工具对Dead Code的打包结果
打包对比结果:中间是源代码,左边是rollup打包结果,右边是webpack打包结果对比。
图片
webpack打包效果(有很多注入代码)
具体细节rollup和webapck的源码实现差异在这里不做过多赘述,大家可以自己深入研究。
► 构建出 esm、cjs 格式
选择Rollup来打包组件库,需要有几点注意:
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 版本控制
组件库发布版本号的管理是很重要的,如何来维护我们的版本号?只能动手在package.json中修改吗?其实可以在打包执行命令的时候,通过命令及参数帮助我们实现自动升级版本号的目的。比如我们在打测试环境包的时候可以使用(cross-env用来指定变量NODE_ENV的值)。
"scripts": {
"test": "npm version patch && cross-env NODE_ENV=testing node build/build.js"
}
下面我们来看看npm version命令具体的使用方式:npm采用了semver规范作为依赖版本管理方案。semver约定一个包的版本号必须包含3个数字。
MAJOR.MINOR.PATCH 意思是 主版本号.小版本号.修订版本号
MAJOR 对应大的版本号迭代,做了不兼容旧版的修改时要更新MAJOR版本号
MINOR 对应小版本迭代,发生兼容旧版API的修改或功能更新时,更新MINOR版本号
PATCH 对应修订版本号,一般针对修复BUG的版本号
当我们每次发布包的时候都需要升级版本号:
"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发布
npm包发布使用之家npm进行发布,发布流程如下:
1. 首先需要配置私有包,配置一次即可
$ npm config set @auto:ZjDesign http://xxxx.com/
2. 使用如下命令在私有仓库中添加用户(配置一次即可)
npm adduser --registry http://xxxx.com/
3. 执行打包命令
npm run rollup
4.私有包发布
npm publish --registry http://xxxx.com/
5. 组件搭建实例
首先看一下我们单个组件UI设计图。从图中可以看出,每个组件实例demo可以看成抽象五大模块。1.组件的title+subtitle、2.组件描述、3.多个组件形态展示、4.设计原则与页面布局、5.单个组件形态的代码示例。
图片
5.1组件demo整体目录
图片
index.zh-CN.md: 作为静态数据的快速输出,包含组件名称、描述、设计原则和API。从.md格式文件中可以使用插件md(vite插件)解析出组件需要的数据,这个在后面单独讲解实现细节。
图片
单个组件类型文件:包含组件排序,title,描述,html。这个通过docs进行数据的解析,具体解析后面进行详细讲解。
图片
5.2Docs插件
作用:将单例中的.vue文件中docs标签数据进行格式处理,docs插件流程图。
图片
实现代码:
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函数实现:这里面使用了lru-cache进行缓存处理,对于已经解析完成的文件进行跟踪,这样可以加快文档展示。通过fetchCode方法对自定义标签内容进行获取。
5.3 MarkDown插件
作用:将markdown文档格式数据转化成我们想要的vue格式化数据。
这里主要通过对第三方markdown-it,依据UI设计的要求进行定制化的修改。可以支持输入emoji,anchor,toc分别使用markdown-it-emoji、markdown-it-anchor、markdown-it-table-of-contents插件。
► md插件实现流程
1、定义插件导出,基于vite的Plugin进行封装:
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核心思想实现:
通过lru-cache进行解析文档的缓存处理,使用gray-matter对docs格式数据的解析,最后生成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
在开发组件并将其沉淀为组件库时,建立合适的SOP机制可以提高开发效率、保持一致性,并促进团队合作。以下是从组件设计到沟通、开发、沉淀为组件库的SOP机制:
图片
► 组件设计:
设计同学进行界面设计,定义组件统一规范。根据多个业务方进行公共组件的提取,确定组件的用途、功能。
►评审:
设计同学和研发同学进行组件设计UI的评审,研发同学定义组件的输入和输出,以及可能的配置项。并且进行编写组件的详细需求文档,包括示例代码和用法示例。
► 开发阶段:
根据组件设计和需求文档,进行组件的开发。使用规范的编码风格和最佳实践。在开发过程中进行单元测试和集成测试,确保组件的稳定性和正确性。
► 文档编写:
编写组件的文档,包括组件的用途、API文档、示例代码等。提供详细的使用说明,帮助其他开发人员使用组件。
► CODE-REVIEW:
使用版本管理工具(如Git)来管理组件的代码。进行代码审查,确保代码质量和一致性。
► 测试与验收:
在真实项目中测试组件,确认其在不同场景下的稳定性和可用性。进行验收测试,确保组件满足预期要求。设计同学进行UI验收。
► 发布:
根据版本号规则,发布组件库的新版本。定期更新组件库,修复bug、添加新功能等。
7. 总结
目前,ZjDesign业务组件库正在不断丰富中。我们努力开发具有高扩展性和低上手成本的组件。并且组件库已有多个新项目接入,整体开发效率明显提升,减少了重复开发。组件库的搭建为团队提供了一个统一的技术平台,促进了知识分享和合作。这一系列改进加速了产品交付,并推动了整体开发流程的提升。
作者简介
何彪
■ 主机厂事业部-技术部-数科技术团队
■2023年2月加入汽车之家,目前任职于主机厂事业部-技术部-数科技术团队,主要负责数科前端业务,组件库搭建等工作。