[关闭]
@hanting003 2016-11-09T16:47:26.000000Z 字数 5915 阅读 1219

饿了么基于Vue 2.0的通用组件库开发之路

Element 是由饿了么UED设计、饿了么大前端开发的一套基于 Vue 2.0 的桌面端组件库。今天我们要分享的就是开发 Element 的一些心得。

一、设计目的

大部分项目起源都是源于业务方的需求,Element 也是一样。随着公司的业务发展,内部开始衍生出很多后台系统,UED 部门也接到越来越多的设计需求。分析这整个过程,我们发现如下问题:

于是我们决定:

二、设计阶段

下面简单说一下设计 Element 经历的几个阶段。

设计的目的是为了业务服务。第一步我们从内部系统开始入手,了解公司内部在使用的各种后台系统,将其组件抽象剥离,寻找共性特征。

总结了公司不同系统不同组件的使用情况后,我们打算从业务组件入手,因为这部份是由公司特殊需求衍生的解决方案。解决了这些棘手的问题,也能给其他后台产品带来好的设计引导。

到这一步,我们开始寻找公司内部的开发团队,并在这时才得知不同团队里使用着不同的前端框架,有 Vue、React、Angular 等。

大前端作为独立的前端团队,有能力开发底层的工具去服务不同业务,并且 Vue 也是一套年轻且发展方向很好的一个技术栈。UED 与大前端的合作一拍即合。

跟大前端接触后,才发现最开始的方向并不正确,因为业务变化过快,即使有通用的业务组件,也很难跟上需求的变化,而基础组件才是所有开发团队都需要的通用组件。这时候我们开始把方向调整为基础组件的设计。

前期的设计工作主要是由交互设计师进行设计,等确认完所有组件的功能和交互后,开始进行视觉阶段,这中间包括制定颜色、字体等各类规范,也同时进行主体网站的设计。

输出 UI Kit 文件,统一设计规范

第一版网站设计,此处的「特殊组件」即业务组件。

设计过程简单来说就经历了这几个阶段,如还有问题可以继续交流,下面进入开发阶段。

三、开发目的

四、开发流程

进入开发阶段后,在总体架构方面我们做了一些尝试,下面按照时间顺序分享给大家:

1.如何与设计师进行配合

经过项目初期开发和设计的磨合,我们提炼了一套组件开发流程:

(1)根据交互稿和视觉稿进行开发,期间与设计师保持沟通
(2)开发完成后自测,之后提交设计师验收
(3)设计师提出修改意见,根据意见进行修改
(4)完成组件开发,为网站编写例子和文档

2.如何管理多组件项目

在开发之初,我们就在思考如何降低组件的耦合度,确保组件可以独立工作。这样的目的是可以保证组件可以依赖其他组件、让用户只加载其中几个组件甚至在安装时只安装需要的组件。最先想到的做法是一个组件单独一个仓库,而组件库项目就是把组件作为依赖引入。

但是由于人手不足,这样的机制导致开发太耗时间,每个组件都需要单独维护和打包,同时还要维护组件库项目的各依赖的版本号。我们只能另寻方案。后来参考了 babel 项目的管理方式:所有子项目放在 packages/ 目录里,一个子项目可以当作一个独立的仓库。通过 lerna 来管理子项目的依赖和发布。

结合自身项目的特点以及 babel 的这套机制,我们重构了目录结构:组件可单独作为一个项目放在 packages/,共用函数放在 src/ 里。最后的打包结果会将整个组件打包成一个文件、组件分别打包成独立文件,同时发布时还将发布组件库和独立组件,满足不同用户的使用需求。

3.如何解决自定义主题

开发一套组件库就离不开定制主题的需求。类名要足够友好,尽量避免存在样式层级嵌套,这样在直接覆盖样式或者单独写一套主题都会方便许多。所以我们采用 BEM 的方式管理类名,同时尽可能将属性值用变量代替,维护一份变量文件便于直接修改变量就能定制一套主题。

考虑到不同用户的使用习惯,我们没有选用 Less 或 Sass 之类的有各自风格的预处理器,而是选用了更接近未来标准的 CSS4 风格的语法,用 PostCSS 和整合了 postcss-bem 和 postcss-cssnext 等插件的 postcss-salad 开发。

为了降低用户自定义主题的上手成本,我们还提供了命令行工具指导用户快速自定义一套主题。

4.如何提供一份直观的文档

文档不仅是让用户看起来直观,也要让编写者写起来直观。所以最简单的方式是用 Markdown 写文档。但是就会产生另一个问题:如何在文档里写可运行的示例?常规的做法是把文档写在 Vue 文件里,这样就可以在里面调用其他组件,但是这样就违背了写「直观」文档的初衷。

经过几番尝试,结合 Vue 的特点。我们写了一套处理 Markdown 文件的 webpack loader,可以将 Markdown 转成 Vue 文件,不仅降低了文档的维护成本,同时也将文档里运行组件示例变成可能。

5.多语言官网如何配置和管理

Element 在立项之初其实并没有考虑国际化的问题。项目开源之后,我们陆续收到了一些外国开发者的反馈,希望能够增加英文文档。不久之后,国内的一个翻译团队主动联系到了我们,为 Element 贡献了整套英文文档。

有了英文文档就需要有英文网站,这就需要对官网的现有结构进行修改和升级;同时为了面向未来,需要官网能够兼容除英语外的其他多语言。为此我们做了以下工作:

(1)路由

官网的路由是根据一个记录了导航信息的 json 文件自动生成的。因此需要在这个 json 文件中添加对应于其他语言的字段,并且根据新的数据结构修改路由生成的逻辑。

(2)页面

官网中除了文档外,还有一些介绍性质的页面。这些页面中文字比较多,如果人工管理每种语言的页面,若需要修改则必须去每个页面相应的位置进行编辑,有些繁琐。我们的做法是:每个页面对应一个模板,模板中的文字全部抽取到一个语言配置文件中,并且写了一个脚本生成最终的页面。这样在需要修改时,只需在语言配置文件中编辑对应的字段即可。

(3)网站组件

对于 headerfooter 等通用的页面组件,我们采取了和上面类似的策略。但由于组件内的文字较少,于是没有再使用模板,而是通过路由判断应该显示何种语言。

中英文网站的显示效果

至此,我们也逐渐完善了技术栈。用 ES2015 和 CSS4 作开发语言、Lerna 负责管理组件、用 Karma 搭配 Mocha 和 Chai 等工具在 Travis CI 里做持续集成测试,最后用 Markdown 结合 Vue 写文档。我们甚至还在 CI 里实现了自动部署网站和推送主题仓库代码等功能,提升了不少开发效率。

6.开发过程中遇到的问题
具体到组件层面,在开发的过程中不可避免地会遇到一些问题。下面是我们的一些应对策略,希望能够抛砖引玉,引发大家的思考和讨论。

在 Vue 2.0 中,用于父子组件间事件通信的 $dispatch$broadcast 被移除了。官方的考虑是,基于组件树结构的事件流方式让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。但是类似 Element 这样的组件库有几个特点:首先,父子组件间互相通信的场景非常常见,比如在一个带有验证功能的表单里,每个表单项在 changeblur 时需要通知表单组件进行校验;其次,组件的结构相对来说比较固定。

出于以上考虑,我们实现了简化版的 dispatchbroadcast ,并把它们包装成了一个 mixin ,方便在需要时调用。其中的 dispatch 代码如下:

  1. dispatch(componentName, eventName, params) {
  2. var parent = this.$parent || this.$root;
  3. var name = parent.$options.componentName;
  4. while (parent && (!name || name !== componentName)) {
  5. parent = parent.$parent;
  6. if (parent) {
  7. name = parent.$options.componentName;
  8. }
  9. }
  10. if (parent) {
  11. parent.$emit.apply(parent, [eventName].concat(params));
  12. }
  13. }

可以看出,我们的实现需要在调用时传入 componentName (在各个组件中定义),这样就确保了事件只会在正确的组件中触发。

在 Vue 2.0 中的自定义组件上使用 v-on 只会监听自定义事件(即组件用 $emit 触发的事件)。如果要监听原生事件,必须使用 .native 修饰符:

  1. <mt-component @click.native="handleClick"></my-component>

这样一来,很多不太熟悉 Vue 2.0 语法的用户会发现给 Element 的组件绑定原生事件总是不生效。事实上,我们从开源以来收到的 issue 里被问得最多的一个问题是:如何给 Button 组件绑定 click 事件?

事实上我们只需要添加一行代码就能解决问题,但是关于是否需要让用户可以直接监听原生事件这件事在我们内部有两种不同的观点:一边认为应该遵循 Vue 的设计思想,原生事件要加 native;另一边认为 button 最常用的就是 click 事件,帮助用户做这件事可以降低学习成本。后来我们专门咨询了尤雨溪本人,他的观点是,对于一些组件的常用事件,可以允许用户直接监听原生事件,同时在文档中说明哪些事件可以直接监听,哪些事件需要加 .native 修饰符。最后我们决定从易用性的角度出发,让用户在使用 Button 组件时可以监听原生 click 事件,因为对于桌面端来说,Button 在绝大部分场景下都是需要监听点击事件的。 现在的 Button 支持以下两种写法:

  1. <el-button @click.native="handleClick">Click Me!</el-button>
  2. <el-button @click="handleClick">Click Me!</el-button>

在历次迭代中,我们会尽量保持 API 的一致。但是在一些万不得已的情况下,需要对 API 作出一些更新。对于老版本的用户而言,如果使用了被移除的 API,升级到新版后会出现一些意料之外的报错信息。为了友好地帮助用户尽快找到报错的来源,我们编写了一个 mixin ,当组件的 API 发生变化时,在组件中引入这个 mixin 并列出变化前后的字段名即可。

mixin 的核心代码为:

  1. const { props, events } = this.getMigratingConfig();
  2. const { data, componentOptions } = this.$vnode;
  3. const definedProps = data.attrs || {};
  4. const definedEvents = componentOptions.listeners || {};
  5. for (let propName in definedProps) {
  6. if (definedProps.hasOwnProperty(propName) && props[propName]) {
  7. console.warn(`[Element Migrating][Attribute]: ${props[propName]}`);
  8. }
  9. }
  10. for (let eventName in definedEvents) {
  11. if (definedEvents.hasOwnProperty(eventName) && events[eventName]) {
  12. console.warn(`[Element Migrating][Event]: ${events[eventName]}`);
  13. }
  14. }

引用了这个 mixin 的组件需要在 methods 中添加一个名为 getMigratingConfig 的方法,返回一个包含发生变化的 API 字段名和对应提示信息的对象:

  1. getMigratingConfig() {
  2. return {
  3. props: {
  4. 'selection-mode': 'Table: selection-mode has been removed.'
  5. },
  6. events: {
  7. cellclick: 'Table: cellclick has been renamed to cell-click.'
  8. }
  9. };
  10. }

五、issue 处理方式

我们选择使用 Tower 来配合 GitHub 进行 issue 的追踪和处理。首先在 Tower 上建立几个清单:Plan、Design、Develop 和 Release。随后具体的操作流程如下:

六、总结

Element 从立项至今已经走过了五个月的时间。总的来说,这段时间就是一个不断发现问题和解决问题的过程,也是每个参与者自身成长的过程。开发时 Vue 2.0 正处于 RC 阶段,我们随着它的版本迭代踩到了不少坑,同时也给 Vue 提了一些 issue,并且都得到了 Vue 团队的处理。在此向Vue 团队的专业精神表示感谢。

自从 9 月开源以来,在社区的帮助下,Element 逐渐成熟,我们也在今天发布了它的第一个正式版本。希望越来越多的人能够参与进来,和我们一起把 Element 做得更好。

七、参考资料

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注