1

背景

随着小程序业务的不断迭代,组件越来越多,导致组件规划不清晰、复用率较低、组件重复和代码混乱,以及还有其他如UI交互一致性和提升效率等需求。

对于组件库来说,实现功能重要,而清晰的文档则更加重要,不然因为团队沟通协调成本高,还是会造成各自为战的情况,不能很好的解决组件重复和代码混乱的问题。

抽离封装电动车Taro组件库以及组件库在线演示文档解决上述问题。

方案选择

在文档这一块,PC和H5都有比较成熟的方案框架,如dumi、VitePress等,但Taro这一块还没有成熟的方案框架,即使taro官方提供的taro-ui,组件也不够丰富,而且说明和示例不对应,增加了使用者的学习成本。

所以我们选择自己手动搭建组件库。

手动搭建还有一个优势,像dumi这种框架虽然成熟,但整体很重,里面封装了过多的功能,而且大部分代码是黑盒的。对于大多数团队,只需要使用其部分核心功能,然而做减法是困难且容易出错的。框架代码的黑盒也导致后续维护困难。而手动搭建虽然一开始工作比较多,但是后续维护非常容易。

主要模块

技术栈对于一套组件库,主要模块包括组件、文档、示例三大部分:

组件:es模块输出和按需加载是必须,rollup是最佳选择,组件编写方面,和业务项目保持一致,使用React,组件更适合使用hooks语法,然后ts,less这种就是常规项。

示例:这里是Taro组件库和常规组件库最大的区别,文档是运行在浏览器环境里的,所以想要组件demo可以展示,则需要使用Taro框架的打包h5功能。所以单独起一个项目用来跑demo代码,使用Taro React。至于怎么把文档里的代码引入到demo项目运行,则是使用webpack-chain自定义md文件的loader,自己写一个loader去实现。

文档:文档就是个静态页面,构建工具就选择vite,配置简单,编译速度快。markdown内容部分,使用react-markdown等插件渲染,根据路由读取不同文件即可。右侧预览部分使用iframe加载demo项目。

图片

目录结构


├─config
│   ├─vite // vite配置
│   └─rollup // rollup配置
├─dist // doc打包产物
├─docs-dist // 文档打包产物
├─packages
│   ├─demo // 组件Taro demo项目
│   ├─doc // api文档项目
│   └─ui // 组件库
├─tests
├─index.html // doc入口文件
└─package.json

开发dev流程&路由关系

以增加一个标签组件tag为例:在ui/components下新增tag文件夹,组件入口tag/index.tsx,文档文件tag/README.md,组件打包产物 /dist/tag/index.js;

doc项目增加对应的路由 /tag,路由渲染菜单到左侧,当路由切换到 /tag 时,通过路由组合出import url引入 tag/README.md 文件,通过vite框架的 ?raw引入方式,读取md文件中的内容为字符串传递给 react-markdown 组件,渲染成中间的文档页面;

demo项目独立运行,打包成h5页面,通过iframe嵌入到右侧,先增加与组件路由对应的页面文件,当doc的路由变化时,iframe的路由也同步变化,demo切换到对应的路由页面,然后就可以读取对应的 tag/README.md;

demo项目通过自定义markdwon-loader读取 tag/README.md 文件中写的示例代码,对于组件的引用,通过配置webpack路径别名的方式,把@hb/rent-taro-components 指向组件产物 /dist/tag/index.js;

demo项目通过自定义markdwon-loader 把当前文档中全部示例代码组合成一个可运行页面输出,即可实时预览到全部示例代码的运行。

图片

这套架构的优点

图片

  • 文档和demo在一个md文件输出,维护方便且灵活
  • 多个demo代码单独编写,互不影响,清晰简洁
  • 右侧demo项目独立,如有需要,可单独发布demo
  • 小程序各模块功能清晰无黑盒,后续维护容易

各模块说明

doc

doc目录结构

├─src
│   ├─components
│   │   ├─markdown-render // md文件渲染组件
│   │   └─... // 其他布局组件
│   ├─guides // 指南文档
│   ├─router
│   │   ├─comp-doc.ts // 组件路由
│   │   ├─guide-doc.ts // 指南路由
│   │   └─index.ts // 统一导出
│   ├─app.less
│   └─App.tsx // 主页面
└─main.tsx // 入口文件

router
comp-doc.ts:


import Button from '@ui/button/README.md?raw';
import Tag from '@ui/tag/README.md?raw';

const compRoutes = [
  {
    name: '基础组件',
    path: '/basic',
    children: [
      {
        name: '按钮',
        path: '/button',
        component: Button,
      },
      {
        name: '标签',
        path: '/tag',
        component: Tag,
      },
    ],
  },
];

export default compRoutes;

demo

demo目录结构
除了增加了一个自定义loader,其他文件结构和标准Taro项目完全一致。


├─config
│   ├─dev.js
│   ├─index.js // Taro webpack配置
│   ├─markdownloader.js // 自定义md文件loader
│   └─prod.js
├─src
│   ├─pages
│   │   └─basic // 文件夹路径与doc路由保持一致
│   │       ├─button
│   │       │   ├─index.config.ts
│   │       │   └─index.tsx
│   │       └─tag
│   ├─app.config.ts // 路由配置
│   ├─app.less
│   ├─app.ts
│   └─index.html
└─package.json

route&核心代码
app.config.ts:
除了 /index 部分,路由与doc路由一一对应。


export default {
  pages: [
    'pages/basic/button/index', // pages和index之间的部分对应doc路由
    'pages/basic/tag/index'
  ],
  ...
}

pages/basic/button/index.tsx:
只需要把 md文件当做组件引入并 export即可,md 经 markdown-loader 解析后就是一个可运行组件。


// 直接引入组件库中README.md作为demo组件,代码解析在自定义loader中完成
import Demo from '@components/button/README.md';

export default Demo;

config/index.js:
通过 webpackChain 自定义loader。


const config = {
  h5: {
    // ...
    webpackChain (chain, webpack) {
      chain.merge({
        module: {
          rule: {
            mdLoader: {
              test: /\.md$/,
              use: [
                {
                  loader: 'babel-loader',
                  options: {}
                },
                {
                  // 引入自定义loader
                  loader: `${path.join(__dirname, './markdownLoader.js')}`,
                  options: {}
                },
              ]
            }
          }
        }
      })
    }
    // ...
  }
}

ui

ts文件配置
通过rollup-plugin-typescript2配置对应的config文件。


import RollupTypescript from 'rollup-plugin-typescript2';
// ...
plugins: [
  // ...
  RollupTypescript({
    tsconfig: resolveFile('config/tsconfig.rollup.json'),
  }),
]

按需加载

  • 多入口打包
  • 通过rollup-plugin-postcss抽离样式文件
  • 通过rollup-plugin-copy直接移动less文件到dist(因为对于业务项目,不需要打包成css)

import RollupCopy from 'rollup-plugin-copy';
import RollupPostCss from 'rollup-plugin-postcss';

const inputD = {};
compFiles.forEach((item) => {
  const val = item.replace(cwd, '').replace('/packages/ui/src/components/', '').replace('/index.tsx', '');
  inputD[`${val}`] = item;
});

const config = {
  input: inputD,
  plugins: [
    RollupPostCss({
      use: [
        [
          'less',
          {
            javascriptEnabled: true,
          },
        ],
      ],
      extract: 'index.less',
      extensions: ['.css', '.less'],
      makeAbsoluteExternalsRelative: false,
    }),
    RollupCopy({
      verbose: true,
      targets: [{
        src: resolveFile('/packages/ui/src/components/*/*.less'),
        dest: resolveFile('/dist/styles'),
      }],
    }),
  ]
}

(本文作者:范翔宇)

图片

本文系哈啰技术团队出品,未经许可,不得进行商业性转载或者使用。非商业目的转载或使用本文内容,敬请注明“内容转载自哈啰技术团队”。

哈啰技术
89 声望51 粉丝

哈啰官方技术号,不定期分享哈啰的相关技术产出。