Vue UI组件库开发经验漫谈

UI组件是对一组相关的交互和样式的封装,提供了简单的调用方式和接口,让开发者能很便捷地使用组件提供的功能来实现业务需求。

我在一个名为Admin UI的Vue UI组件库(GitHub地址:https://github.com/BboyAwey/admin-ui)上投入了大量时间。该库到目前版本发展到3.x,几乎全部出自我手。它的优劣请先搁置不问,但至少,我从中积累和学习到的经验足够回答一个问题:如何开发一个Vue 组件库。不过即使你是React的使用者,也可以参考本文给出的经验,因为如果你打算编写一个React UI组件库,你将不得不面对几乎完全一样的问题。

这篇文章也是我在公司的一次技术分享的内容。我在这里主要只探讨思路,尽量不去涉及具体实现。并且我对这些问题的解决思路也不尽然是完全合理的,如有错漏请读者斧正。

1 组织你的项目

当你开始着手组件库的开发时,第一件事可能就是建立一个项目,因为是Vue 组件库,你很可能会使用其官方推荐的vue-cli工具来生成一个项目。

1.1 合适的文件结构

当项目生成后,你很快就发现这个项目模版的文件结构用于业务开发非常合适,但并不那么适合组件库的开发。这时你很可能会在其src文件夹下用你的组件库名称建立一个文件夹来存放你的组件库代码。但这个时候你还并不清楚所有需要做的事情,你并没有继续调整文件结构。

我们暂且将你的组件库就命名为admin-ui,方便后续行文

当你真正开始编写第一个组件时,你肯定会首先编写一个用来展示正在开发中的组件的页面,并在其上对其进行测试。所以你又在src文件夹中新建了一个examples文件夹用来存放你的示例代码。

这时你的文件结构看起来就像这样:

Vue UI组件库开发经验漫谈_第1张图片
组件库文件夹和示例页面文件夹

1.2 为组件库编写或生成使用文档

很可能一段时间后,你为每个组件都编写了一个示例页,甚至其中一些示例页本身已经做的很棒了。如果有一天组件库主要开发完成了,这堆实例页也就没什么用了。于是你可能会对这些示例页进行完善,将每个组件的特性和接口列表甚至使用示例代码放到它们的示例页上,然后将其部署在一台服务器上方便你的用户随时查看。它们很幸运地没有被浪费,而是都变成了组件库的使用文档了!

1.3 组件库本身开发文档的管理

然后你可能意识到一个人的力量之薄弱,你会邀请其它开发者参与到你的项目中,然而尽管你在使用vue-cli生成项目时已经开启了ESlint,但多人协同开发一套完整的UI库仅仅依靠代码风格的统一是远远不够的。你们可能需要建立开发文档,将各种约定和设计,以及需要共享的其它信息发布在其中。所以你又在项目的根目录中新建了一个documentation目录,并在其中使用GitBook生成了一个文档,同时同步到了GitBook服务器,以便你的伙伴们即使没有同步它们也能在线查看。

Vue UI组件库开发经验漫谈_第2张图片
使用gitbook建立的组件开发者文档

1.4 各种安装方式的支持

你现在已经可以开始开发你的组件了,在编写第一个组件的时候,你意识到你现在编写的这个项目本质上是你的组件库的使用文档,如果这算是一个业务项目,那么它是直接将你的组件库源码放置到了自己的源码中来使用。但如果其它业务项目组使用了你的组件库,他们目前只能像你现在这样把源码拷贝到他们的项目中使用,并且每次你的组件库升级了,他们都需要通过再次进行拷贝来进行升级,这种情况显然是你所不能接受的。你开始考虑你的用户们怎么安装你的组件库了。

你首先想到的是最流行的安装方式:npm。如果直接将你的组件库发布到npm上,你的用户将能通过它或者yarn非常方便地进行安装和升级。但目前你的组件库刚刚起步,你并不希望马上开源。于是你向公司申请了一个你们公司自己搭建的gitlab中的仓库,然后在你的组件库所在的那个文件夹git init 初始化了一个git项目,并将其同步到你申请的那个仓库中。这时,公司的同事们已经可以通过

npm install admin-ui git+ssh://admin-ui-git-location.git --save

来安装你的组件库了。

然后你能想到的第二种安装方式就是CDN。你的用户们通过在他们的页面内联


来使用你的组件库,这时就涉及到如何打包你的组件库了。在这种场景下,你需要将你的组件库打包为一整个admin-ui.js这样的js文件来使用。关于打包我们将在下一节继续讨论。

当然,最后一种安装方式就是直接使用源码了,将你的组件库直接放到项目源码中进行引用。

1.5 打包和发布你的组件库

确定了需要支持的安装方式,你可能已经意识到,现在你的项目中有两部分需要打包和发布:

  • 你的组件库(这是重点)
  • 你的组件库的使用文档(也就是项目本身)

你首先想到的是使用文档打包起来很方便,因为这个项目目前的打包配置直接就能将你编写的所有示例页面打包好,你唯一要做的就是运行

npm run build

但关键问题在于你的组件库admin-ui。它目前是作为你这个项目的一部分源码存在的。所以你不得不开始思考如何对这部分代码进行单独打包。

当然,你也可以不对你的组件库进行打包,而是直接将其源码发布为一个npm包,但那样的话,用户在使用它的时候就需要依赖打包工具来对你的代码进行打包。而类似vue-cli这类工具生成的项目,默认情况下是不会打包来自node_modules文件夹下的代码,用户必须修改构建配置手动指定需要打包的代码位置,这很不方便

于是你在对照着build.jswebpack.prod.conf.jsprod.env.js,在项目根目录下的buildconfig文件夹中分别新增了publish.jswebpack.publish.conf.jspublish.env.js文件,并查阅了webpack文档,去掉了不需要的一些功能配置,设置好了对你的组件库进行打包的配置。

你期望将打包后的代码就放在你的组件库文件夹内,并命名为dist,这时你的组件库的源文件就需要移动到src目录下。

Vue UI组件库开发经验漫谈_第3张图片
组件库源码与打包后的代码并存

webpack在对代码进行打包时需要指定入口文件,这时你发现你的组件库本身还没有出口文件。

1.6 全量加载和按需加载

你在组件库的src文件夹下新建了一个index.js文件,它引入并输出了所有的组件。

import Button from './components/button'
import Icon from './components/icon'
// ...省略的代码...
export {
  Button,
  Icon
  // ...省略的代码...
}

到这里,你或许会干脆将组件库本身的文件结构也一并规划好:

Vue UI组件库开发经验漫谈_第4张图片
组件库本身文件结构

在这种输出格式下,你的用户可以通过

import { Button } from 'admin-ui'

来从组件库中获得Button组件。然而这仅仅是对这种格式的支持(这并不是按需加载),用户还需要能够进行全量加载,也就是一次引入所有组件并全部自动注册好。所以你在index.js中将所有的组件都挂载到adminUi对象上,然后再在该对象上挂载install()方法用于支持Vue.use(),最后直接输出这个对象。现在你的index.js看起来像这样:

import Button from './components/button'
import Icon from './components/icon'
// ...省略的代码...
export {
  Button,
  Icon
  // ...省略的代码...
}

const adminUi = {
  Button,
  Icon,
  // ...省略的代码...
}

adminUi.install = function (Vue, options = {}) {
  Vue.component('cu-button', Button)
  Vue.component('cu-icon', Icon)
  // ...省略的代码,你也可以用循环来写...
}
export default adminUi

install()方法中可以做很多事情,除了注册组件,很可能你也会在其中进行一些实例方法的挂载

这时你的用户可以通过

import adminUi from 'adminUi'
Vue.use(adminUi)

来进行全量加载。

接下来就是按需加载。你发现如果仅仅是通过你的index.js入口文件去加载某个组件,其它组件虽然没有被用户引入,但仍旧被编译到了用户的代码中去了。所以你不得不考虑新的方式。既然不能从单一入口加载,是否可以为每个组件指定一个加载点呢?你希望你的用户能够通过类似

import Button from 'admin-ui/button'

这样的方式来加载单个组件,这样就不存在多余的组件了。所以你意识到,每个组件还需要单独进行打包。以每个组件的出口文件(可能也是个index.js,这里你应该意识到每个组件的文件结构保持一致能带来好处)为打包入口,将每个组件都打包为一个单独的模块放置到dist中的lib文件夹下。这时,按需加载就被支持了。

Vue UI组件库开发经验漫谈_第5张图片
打包后的组件库的文件结构

我并未讨论具体的webpack配置,一是因为本文主要讨论思路而不是具体实现,二是这个话题如果要深入讨论需要更多篇幅,三是webpack本身配置非常复杂而我并不算熟练。

然后你愉快地尝试了一下打包,但沮丧地发现,不管是以组件库本身的出口文件为入口,还是在对每个组件进行单独打包的时候,结果除了一个.js文件,还会有一个.css文件。你的用户不管是全量加载,还是按需加载,在引入.js文件时还要引入对应的.css文件。在全量加载时,由于只加载一次,这似乎不是什么大问题。但如果是按需加载,因为要引入多次,这就有些麻烦了。

分离的css文件是出于性能考虑,css文件可以被浏览器缓存,同时组件本身渲染时不需要再生成css了

解决方案有两种,一种是推荐用户使用babel-plugin-component,另一种是打包后的组件本身不再提供css文件,而是全局引入全量加载的那个css文件。两种做法都可以,但我使用的是后者。

有两个原因,首先组件们的样式集合起来体积并不大,压缩打包后控制在60KB以内(这其中绝大部分都是font-awesome的样式代码,组件的所有样式不超过5kb);其次由于使用了font-awesome,如果每个组件单独引入自己的样式,依赖了font-awesome的组件们就会出现重复的样式。

2 设计一个主题系统

当你的组件库被用于不同的项目中,或者某个项目需要换肤功能时,不可避免地,你需要在你的组件库中设计一个主题系统。

2.1 确定主题系统功能边界

首先需要明确,你的主题系统功能的边界。在我看来,影响一个管理类后台系统风格的因素主要有三种:

  • 颜色(这是最主要的)
  • 阴影
  • 圆角

所以不妨先将你的主题系统边界就设定为这三种因素。

2.2 选择合适的实现方案

然后你开始思考可行的主题系统实现思路:

  • 特殊格式的字符串替换
  • 主题文件预编译
  • 样式类

特殊格式的字符串替换无疑是最简便的,开发时当遇到需要被主题系统控制的样式时,在css中直接使用特使格式的字符串,在运行时进行替换即可。比如:

div {
  color: $$primary$$;
}

运行时被你的脚本替换成:

div {
  color: #00f;
}

这种方案的优点是开发时非常便捷,基本不影响开发体验,甚至还有提升。在传统的jquery时代问题不大,但就Vue项目而言,存在“替换时机”问题。你大可以在项目初始化后将页面中所有