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

2023年 10月 27日 90.7k 0

1. 前言

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

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

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

2. 实现分析

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

图片图片

3. 整体架构设计

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

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

3.1单包是什么

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

► 优点

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

  • 组件完全耦合,必须把它作为一个整体统一发包,就算只改一个非常小的功能,都要对整个包发布更新
  • 搭建场景重复打包
  •       比如说Antd,它就是作为一个整体的包来尽进行管理的。选择使用单包架构的话,那么你就必须提供按需加载的能力,以降低使用者的成本,你可以考虑支持ESModules的Treeshaking的功能来实现按需加载的能力。当然你也可以选择另外一种方案,叫做"多包架构"。

    3.2多包是什么

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

    ► 优点

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

    ► 缺点

  • 维护成本高,每个组件都需要一套package配置。
  • 组件与组件之间物理隔离,对于相互依赖,公共代码抽象等场景,就只能通过NPM包引用的方式来实现。
  • 多依赖多版本问题
  • 常用逻辑片段/各个组件都需要的依赖和逻辑
  • 在这些场景下的开发统一发布,相对来说没那么方便,多包架构在业界称之为"Monorepo"。

    图片图片

    3.3结论

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

    4. 组件库构建

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

    图片

    4.1项目打包

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

    ►rollup使用流程

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

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

  • 实际上,我们自己写的代码在最下面。上面注入的大段代码 都是webpack自己的兼容代码,目的是自己实现require,modules.exports,export,让浏览器可以兼容cjs和esm语法
  • 可以理解为,webpack自己实现polyfill支持模块语法,rollup是利用高版本浏览器原生支持esm(所以rollup无需代码注入)
  •        具体细节rollup和webapck的源码实现差异在这里不做过多赘述,大家可以自己深入研究。

    ► 构建出 esm、cjs 格式

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

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

    相关文章

    Oracle如何使用授予和撤销权限的语法和示例
    Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
    下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
    社区版oceanbase安装
    Oracle 导出CSV工具-sqluldr2
    ETL数据集成丨快速将MySQL数据迁移至Doris数据库

    发布评论