从0到1教你搭建前端团队的组件系统(高级进阶必备)

49,879

前言

随着vue/react这类以数据驱动为主的web框架的不断完善和壮大,越来越多的前端团队开始着手搭建内部的组件库。虽然目前市面上已经有很多功能强大且完善的组件库供我们使用,比如基于react的开源组件库ant-design,material,又比如基于vue的开源组件库elementUI,iView等。

我们在开发管理系统或者中台产品时,完全可以使用这种第三方库来开发,因为首先其服务的用户群体比较小众,一般是企业或者运营人员来使用,重点在于功能和业务,所以在B端产品比较适合;另一点就是设计要求相对于C端产品会低一些,因为B端产品或者管理系统风格统一简单反而会降低使用者的学习成本。所以对于上述情况,我们完全可以使用ant-design-pro或者element-admin-vue这类集成管理框架开开发。

我们使用第三方组件库搭建一个企业级应用是完全没有问题的,但是另一方面,随着我们对用户体验以及网站性能的要求越来越高,流量及金钱,速度即王道,对于专注于做C端的企业来说,尽可能的减少用户等待才能留住更多的用户,比如我们在某宝,某东上买一个商品,结果我们花了一分钟商品列表还没有出来(形容的有点夸张),这种情况下客户可有可能直接选择某拼了。很明显像ant-design和elementUI这样的组件不适合做C端产品,因为体积太大了,除非用高性能服务器或者其他方式弥补。所以说采用轻量级组件库对于C端项目来说有以下几点好处:

  • 打包体积小,高度可控
  • 采用内部组件库安全性更高,防止嵌入攻击
  • 构建和开发更灵活,且组合型更高

但是开发组件库还是需要投入时间和成本的,毕竟这东西不是每个团队都玩的起的。说了这么多,接下来我们就来分析和实现一个团队内部的组件库吧。

你将收获

  • 如何从0搭建一个组件库
  • 前端组件系统设计思路和模式
  • 组件库的划分及设计思路
  • 组件库的package.json文件配置说明
  • 将组件库部署到github并发布到npm上

正文

1. 开发组件库的几种方式

目前我们开发组件库的方式有很多,只要根据npm发包原则去配置就好了,我们可以用webpack自己大家一个library,也可以直接使用create-react-app/vue-cli3来快速改造一个组件库的脚手架,或者采用之前比较火且自动集成tree-shaking的rollup,这些方式都可以搭建我们组件库的脚手架。关于如何使用webpack4.0和rollup,可以参考笔者的以下几篇文章:

其实还有一种最快的方式就是直接去ant-design或者elementUI的github仓库,把代码copy下来改成自己的组件库脚手架,当然,这也不是随便就能改好的,如果想尝试这种方案,建议大家先学好typescript和webpack。

笔者这里采用的是目前比较流行的工具链umi,umi的father专门是提供组件库或者工具库打包的集成工具,我们只需要更改配置文件就能轻松搭建一款自带说明文档的组件库。笔者接下来会具来教大家如何使用它。

2. 前端组件系统设计思路和模式

以上是笔者画的一个简陋的分层图,我们可以看到最底层的是我们的基础视图组件,它是上层建筑的基石。对于一个包含很多子系统的复杂的项目系统来说,要想设计一个好的架构,第一步就是合理划分组件,组件的粒度拆成的足够细,这样才能最大限度的复用组件。

对于任何一个复杂系统来说,最重要的就是实现错综复杂的业务功能,但是不同模块或者子系统之间很多业务往往是相通的或者相似的,如果这个时候我们每个页面对于实现类似的业务场景都去重复去写一遍业务代码,那完全是没必要的,对于可维护性来说也是一种打击,所以基于这种场景我们的 业务组件 就很有必要出场了。我们可以把功能或者需求类似的有机体封装成一个业务组件,并对外暴露接口来实现灵活的可定制性,这样的话我们就可以再不同页面不同子系统中复用同样的逻辑和功能了。

同理,不同页面中往往有可能出现视觉或者交互完全相同或者类似的区块,为了提高可复用性和提高开发效率,我们往往会基于基础组件和业务组件再进行一次封装,让其成为一个独立的区块以便直接复用。

通过这样一层层封装,我们就逐渐搭建了一套完整的组件化系统,基于这种模式的开发往往也是一个好的前端架构的开始。但要注意一点就是高层次的组件一定会依赖低层次的组件,但是低层次的组件不可以包含高层次的组件。(听起来有点像rudex的单向数据流法则),他们的关系就好像下图:

3. 组件库的划分及设计思路

组件库的划分其实可以参考成熟组件库划分。由于业务组件和区块划分完全取决于不同公司的实际项目情况,这里不能形成一套统一的思维框架,所以我这里说的组件库划分主要指基础组件库的划分。我们先来看看antd的划分,它划分为:通用组件,布局组件,导航组件,数据录入和数据展示组件,反馈型组件和其他。elementUI的组件划分为:基础组件,表单组件,数据呈现组件,通知类组件,导航类组件和其他,这些分类发都是非常合理的划分,所以我们在设计组件库时也可以参考或者直接使用,具体总结如下:

  • 通用型组件: 比如Button, Icon等.
  • 布局型组件: 比如Grid, Layout布局等.
  • 导航型组件: 比如面包屑Breadcrumb, 下拉菜单Dropdown, 菜单Menu等.
  • 数据录入型组件: 比如form表单, Switch开关, Upload文件上传等.
  • 数据展示型组件: 比如Avator头像, Table表格, List列表等.
  • 反馈型组件: 比如Progress进度条, Drawer抽屉, Modal对话框等.
  • 其他业务类型

至于组件实现的设计思路,其实笔者之前也写过很多文章来做铺垫,第一要义就是需求,一切要从需求出发。不仅仅是react的组件设计,vue或者angular等都是类似的方法和思路,这里简单给大家举一个组件开发的例子—— 弹窗组件(Modal)的开发思路:

  1. 需求分析
  2. 功能设计及实现思路
  3. 健壮性与组件测试
    因为Modal设计是组件设计里一个很典型的案例,如果想学习具体实现细节,可以在读完本文之后移步手摸手实现一个轻量级可扩展的模态框(Modal)组件

4. 从0搭建一个组件库

这一步是文章的重点,我们将会了解到如何使用umi/father来搭建团队的组件库。至于umi这个前端集成解决方案,笔者从它的架构中受到了很多启发,并且基于umi+dva+react的前端开发流程应用非常广泛,感兴趣的朋友可以研究学习一下。

4.1 father介绍

官方介绍就一句话:基于rollup和docz的库打包工具。它的特点主要有:

  • 基于 docz 的文档功能
  • 基于 rollup 和 babel 的组件打包功能
  • 支持 TypeScript
  • 支持 cjs、esm 和 umd 三种格式的打包
  • esm 支持生成 mjs,直接为浏览器使用
  • 支持用 babel 或 rollup 打包 cjs 和 esm
  • 支持多 entry
  • 支持 lerna
  • 支持 css 和 less,支持开启 css modules
  • 支持 test
  • 支持用 prettier 和 eslint 做 pre-commit 检查

所以作为一个开箱即用的组件库打包工具,已经为我们省去了很多中间步骤,比如说组件的测试,不同环境下的模块打包,而且还支持ts和文档功能,我们只需要掌握babel和rollup的知识,就能用它轻松配置出一个强大的组件库脚手架。如果对docz不太了解的可以在docz官网学习使用非常简单,只要你对markdown和react熟悉,分分钟入门。

4.2 使用father搭建组件库

其实father的使用非常简单,首先我们先安装一下father:

npm install father -D

或者使用yarn安装:

yarn add father

接下来我们可以在package.json里配置如下脚本来使用:

# 打包库
$ father build

# 开发环境下启动文档服务
$ father doc dev

# 打包编译文档
$ father doc build

# 将文档部署到github
$ father doc deploy

# 组件库测试及测试覆盖率
$ father test
$ father test --coverage

这里拿笔者之前已经发布到npm的组件库xui——基于react的轻量级UI组件库来举例。 首先我们看看xui的package.json中的script脚本如何配置的:

"scripts": {
    "dev": "npx --max_old_space_size=8096 father doc dev --host 0.0.0.0",
    "build": "father build",
    "build:doc": "father doc build",
    "deploy": "father doc deploy"
  }

当我们执行npm run dev或者yarn dev时,father自动帮我们启动了doc服务器,我们接下来就能看到我们组件的测试文档了:

这些内容都是我们提前写好的mdx文档,语法类似于markdown,只不过增加了对react组件的编译支持,其实使用起来很简单,我们只需要把react组件包裹在Playground容器里就好了,具体使用可以参考以下方式:

---
name: Button
route: /Button
order: 3
sidebar: true
---

import { Playground } from 'docz'
import Button from './index'

# Button

#### 基本用法
<Playground>
    <Button>按钮</Button><br />
    <Button type="primary">按钮</Button><br />
    <Button type="warning">按钮</Button><br />
    <Button type="info">按钮</Button><br />
    <Button type="pure">按钮</Button><br />
    <Button type="primary" shape="circle">按钮的命运</Button><br />
</Playground>

头部的信息我需要介绍一下:

  • name 组件的名称,也就是显示在左部导航栏里的导航文本
  • route 组件页面的路由
  • order 组件在导航条中显示的顺序
  • siderbar 当前页面是否显示导航条

我们根据案例可以知道mdx里面可以使用es6的导入方式来引入组件或者变量,其实还有很多配置,这里就不一一举例了,感兴趣的可以到docz官网学习。以下是官网的截图:

接下来介绍我们最重要的部分,.fatherrc.js文件的配置。初始化father项目时会自动生成一个默认配置,但是大多数情况下我们都需要自定义配置,官网的配置参数也很多,可以找到适合自己团队的配置,这里我主要介绍xui组件库的配置细节。先看看配置代码:

// .fatherrc.js
const options = {
  entry: 'src/index.js',
  doc: {
    title: 'xu_ui',
    themeConfig: { mode: 'light' },
    base: '/xu_ui'
  },
  extraBabelPlugins: [
      ['babel-plugin-import', {
          libraryName: 'antd',
          libraryDirectory: 'es',
          style: true,
      }]
  ],
  // cssModules: true,
  extractCSS: true,
  lessInBabelMode: true,
  runtimeHelpers: true,
  esm: 'babel', 
  cjs: 'babel',
  autoprefixer: {
      browsers: ['ie>9', 'Safari >= 6'],
  }
};

export default options;
  • entry 主要用来定义组件库的入口位置,通常我们会放在src目录下,以下是xui项目中src的目录结构:
    大家可以参考以下,我们还可以在组件目录下加测试代码,这里就不举例了。
  • doc 主要用来配置文档的标题,主题色以及根路由
  • extraBabelPlugins 主要用来配置额外的babel插件,比如组件库的按需导入。xui虽然没用用到antd,但是大家如果有基于antd二次开发业务组件或者区块时,可以按照如上配置去按需导入第三方组件库,这样可以极大的降低代码体积
  • cssModules 是否开启css Module,这个按团队需求来定制,可以不用配置
  • extractCSS 是否将css抽离成单独的css文件,这个也是看组件库的体量,不过建议最好配置上
  • lessInBabelMode 在 babel 模式下做 less 编译,基于 gulp-less,默认不开启
  • runtimeHelpers 是否把 helper 方法提取到 @babel/runtime 里,推荐开启,能节约不少代码体积
  • esm 是否输出 esm 格式,以及指定 esm 格式的打包方式等,笔者这里使用babel的打包方式
  • cjs 是否输出 cjs 格式,以及指定 cjs 格式的打包方式等,笔者这里使用babel的打包方式
  • autoprefixer 主要用来配置打包后的组件对浏览器的兼容版本

通过以上的配置,我们就能愉快的开发组件啦。

4.3 编写组件说明文档

组件说明文档是让其他人了解组件库的关键环节,包括组件库的适用范围(pc端,移动端,轻量级还是重量级),兼容浏览器的版本,设计原则和背景,以及社区生态,使用方法等。所以组件库说明文档的编写也是非常重要的,大家具体可以参考antd或者element的说明文档,可以说是写的非常专业了。如下是antd的例子:

大家可以参考以上笔者整理的核心部分去写组件说明文档。

5. 组件库的package.json文件配置说明

如果你在为团队开发私有组件库,那么完全不需要在意接下来笔者写的内容,但是如果你要开发一个开源的,大家都能使用的组件库,一定要注意以下几点的编写:

  • name package的包名,让人一眼就能知道这个库是用来做什么的
  • description 库的描述,一个精准而具体的组件库的描述有利于人们在npm或者github上的搜索,有点类似于SEO的感觉,没错,这就是组件库的SEO
  • keywords 组件库的关键字说明,这一点也很重要,直接影响者用户对我们组件库的搜索排名
  • homepage 组件库的主页地址,更有利于用户了解组件库的用法,功能等 完整的在线例子可以参考: xui——基于react的轻量级UI组件库

6. 将组件库部署到github并发布到npm上

首先我们需要在package.json中配置github的地址:

"repository": {
    "type": "git",
    "url": "git+https://github.com:MrXujiang/xu_ui.git"
  }

大家可以复制以上代码改成自己的仓库地址。

其次我们需要登录进npm官网,如果你还没有账号可以直接申请一个,也很简单,然后在终端通过命令行登录。

接下来我们执行以下几个命令就能将自己的组件库打包发布到npm上了:

// 打包编译组件库
yarn build

// 编译组件库文档,该步骤可省略
yarn build:doc

// 部署组件库文档到github, 该步骤可省略
yarn deploy

// 发布到npm上
npm publish --access public

以上的yarn命令具体实现可参考xui的package.json,也可以自己配置。经过上述步骤我们就成功将自己的组件库发布到npm上了,是不是很简单呢?

笔者已经将实现过的组件发布到npm上了,大家如果感兴趣可以直接用npm安装后使用,方式如下:

npm i @alex_xu/xui

// 导入xui
import { 
  Button, Skeleton, Empty, Progress,
  Message, Tag, Switch, Drawer, Badge, Alert
} from '@alex_xu/xui'

该组件库支持按需导入,我们只需要在项目里配置babel-plugin-import即可,具体配置如下:

// .babelrc
"plugins": [
  ["import", { "libraryName": "@alex_xu/xui", "style": true }]
]

npm库截图如下:

组件系统与微前端架构初探

笔者本篇文章并不会将微前端架构的知识,但是既然涉及到组件库,就一定要形成一个知识闭环,笔者特意画了如下脑图,供前端朋友或者正准备走向微前端架构的团队一些参考:

最后

如果对于react/vue组件设计原理不熟悉的,可以参考我的之前写的组件设计系列文章:

笔者已经将组件库发布到npm上了, 大家可以通过npm安装的方式体验组件.

如果大家是vue技术栈,笔者还是极力推荐使用vue-cli来搭建组件库的,至于如何搭建vue组件库,笔者后期有时间会详细的出一篇文章来介绍。

如果想获取组件设计系列完整源码, 或者想学习更多H5游戏, webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们的技术群一起学习讨论,共同探索前端的边界。

更多推荐