探索组件库设计的无限可能性

数据库 其他数据库
目前,ZjDesign业务组件库正在不断丰富中。我们努力开发具有高扩展性和低上手成本的组件。并且组件库已有多个新项目接入,整体开发效率明显提升,减少了重复开发。

1. 前言

当前端开发团队面临业务的不断增长和项目数量的持续增加时,两个主要问题逐渐凸显:1.业务组件复用的挑战;2. 代码一致性和质量维护问题。前端团队为了解决这些问题,通常会选择构建业务组件库。其主要目标是:

  • 提高开发效率:开发人员可以在不同项目中复用组件,从而减少重复工作,提高开发效率。
  • 保持一致的代码实现:可以确保在不同项目中使用相同的代码实现,避免了风格不一致和质量差异。
  • 质量保障:组件库中的组件经过严格验证和测试,能够提供高质量的代码。
  • 易于维护和升级:作为独立的模块,业务组件库更容易进行维护和升级,使开发人员能够更专注于组件库本身的质量。
  • 知识共享和技术积累:组件库可以成为团队共享技术知识和经验的平台,帮助提升整体的技术水平。

因此,构建业务组件库有助于解决业务组件复用、代码统一性和质量维护等问题,为不断发展的业务环境提供了高效、统一和可维护的开发流程。

2. 实现分析

在构建业务组件库时,需要深入调研和选择适当的技术方案,验证方案的可行性,最终将各个解决方案集成到一起,以实现高效的组件库开发和维护。下面我们将通过整体架构、构建、质量监控、站点搭建、组件质量、组件SOP这六大模块对我们的业务组件库进行分析。

图片图片

3. 整体架构设计

对于业务组件库的整体架构设计而言,核心问题是业务组件库的代码时如何来组织和管理。首先,我们把代码仓库建好。业界一般会把同一类组件库用单个仓库的形式维护,并且把组件开发成NPM包的形式,这里的重点是,你要考虑把所有的组件打包成一个大的NPM包,还是分割是一个个独立的小NPM包。不要小看这个问题,这两种选择会使仓库的目录结构有不小的差异,进一步又会影响到后面组件的开发,构建,发布,实现的技术设计。

业界组件库的架构常见单包和多包两种。单包适合简单场景,组件集中在一个库中。多包则将组件分成独立包,适应多项目需求,增强灵活性和复用性。

3.1单包是什么

把所有的组件看成一个整体,一起打包发布。单个仓库,单个包,统一维护统一管理。

► 优点

  1. 可以通过相对路径实现组件与组件之间的引用,公共代码之间的引用
  2. 维护成本低,只维护一套package.json配置

► 缺点

  1. 组件完全耦合,必须把它作为一个整体统一发包,就算只改一个非常小的功能,都要对整个包发布更新
  2. 搭建场景重复打包

      比如说Antd,它就是作为一个整体的包来尽进行管理的。选择使用单包架构的话,那么你就必须提供按需加载的能力,以降低使用者的成本,你可以考虑支持ESModules的Treeshaking的功能来实现按需加载的能力。当然你也可以选择另外一种方案,叫做"多包架构"。

3.2多包是什么

每个组件彼此独立,单独打包发布,单个仓库多个包,统一维护单独管理。

► 优点

    组件发布灵活,并且大然支持按需使用

► 缺点

  1. 维护成本高,每个组件都需要一套package配置。
  2. 组件与组件之间物理隔离,对于相互依赖,公共代码抽象等场景,就只能通过NPM包引用的方式来实现。
  3. 多依赖多版本问题
  4. 常用逻辑片段/各个组件都需要的依赖和逻辑

在这些场景下的开发统一发布,相对来说没那么方便,多包架构在业界称之为"Monorepo"。

图片图片

3.3结论

ZjDesign组件库使用场景比较特殊,组件之间的依赖关系比较强,互相会组合形式新的组件,所以在这里选用的单包开发模式进行开发。单包开发模式可以减少我们开发维护成本,开发工作量的减少,提升组件之间的引用效率。

4. 组件库构建

当你确定了整体架构之后,就可以开始具体的功能点实现了。业务组件库要求整体框架提供基础的技术能力。

图片

4.1项目打包

提到构建工具,大家肯定一下会想到很多一堆工具:webapck、gulp、rollup等。网上有很多文章分析它们分别更适合哪些场景,webpack更适合打包组件库、应用程序之类的应用,而rollup更适合打包纯js的类库。下面我们来对比一下webpack和rollup两者的区别。

►rollup使用流程

  1. 无需考虑浏览器兼容问题,开发者写esm代码 -> rollup通过入口,递归识别esm模块 ->  最终打包成一个或多个bundle.js -> 浏览器直接可以支持引入
  2. 需考虑浏览器兼容问题,可能会比较复杂,需要用额外的polyfill库,或结合webpack使用

打包成npm包:

  1. 开发者写esm代码 -> rollup通过入口,递归识别esm模块 -> (可以支持配置输出多种格式的模块,如esm、cjs、umd、amd)最终打包成一个或多个bundle.js
  2. (开发者要写cjs也可以,需要插件@rollup/plugin-commonjs)初步看来
  3. 很明显,rollup 比较适合打包js库(react、vue等的源代码库都是rollup打包的)或 高版本无需往下兼容的浏览器应用程序(现在2022年了,时间越往后,迁移到rollup会越多,猜测)
  4. 这样打包出来的库,可以充分使用上esm的tree shaking,使源库体积最小

►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打包效果(有很多注入代码)

  1. 实际上,我们自己写的代码在最下面。上面注入的大段代码 都是webpack自己的兼容代码,目的是自己实现require,modules.exports,export,让浏览器可以兼容cjs和esm语法
  2. 可以理解为,webpack自己实现polyfill支持模块语法,rollup是利用高版本浏览器原生支持esm(所以rollup无需代码注入)

       具体细节rollup和webapck的源码实现差异在这里不做过多赘述,大家可以自己深入研究。

► 构建出 esm、cjs 格式

选择Rollup来打包组件库,需要有几点注意:

  1. 配置包格式为 esm、cjs、umd
  2. external 掉vue,组件库不建议将 Vue 打包进去

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月加入汽车之家,目前任职于主机厂事业部-技术部-数科技术团队,主要负责数科前端业务,组件库搭建等工作。

责任编辑:武晓燕 来源: 之家技术
相关推荐

2023-09-27 22:53:04

图像处理是开源Java

2016-09-21 09:16:55

Qlik

2023-01-13 21:13:31

AI人工智能医疗

2011-04-20 10:07:15

2022-12-30 07:40:12

DeepKitTypeScript静态类型

2021-09-29 18:59:42

戴尔

2012-06-04 13:28:51

AndroidChrome OS

2011-04-18 13:43:42

2021-02-20 12:04:51

比特币区块链美元

2018-03-02 11:38:11

2013-03-19 11:13:14

Google广告SXSW

2020-05-15 13:42:03

物联网人工智能军事物联网

2019-04-15 10:30:38

程序员技能开发者

2020-08-11 09:38:40

微信苹果美国

2011-04-18 13:47:59

ECC私钥

2019-04-22 08:57:46

硅谷996ICU

2018-11-26 09:48:57

服务器异常宕机

2023-07-11 18:34:05

AI 医疗人工智能

2017-07-21 16:40:29

网易云场景专属云

2023-11-24 12:14:55

PythonVSCode
点赞
收藏

51CTO技术栈公众号