前端组件化及自制UI框架概述

2016 年 TopGeek 前端开发者大会

很荣幸能作为演讲嘉宾之一参加这个大会,能与 Hax、寸志等前端大拿同台也让我很兴奋和紧张。这是我在这次大会上的演讲内容,时间只有20分钟左右,所以关于模块化、组件化等只是简单带过,可以说是极其入门级的一场演讲,但毕竟是一次特殊的人生经历,还是有必要记录下来。

主要内容

大家好,我是来自 DaoCloud 的前端开发工程师朱静思。

我们 DaoCloud 是一家技术驱动型的公司,虽然核心技术是基于 Docker 的,但公司非常重视前端,我们在前端开发方面也在不断探索最适合我们的实践方式,今天我就来向大家分享一点我们在模块化、组件化、UI 设计方面的一些心得。

我们公司在过去的时间里先后推出了 Daocloud 云平台、Daovoice、Daocloud Enterprise 等一系列的产品,这样高速的迭代给前端开发带来了巨大的压力,同时也推动着我们不断革新。

我们的产品大多使用 AngularJs 作为框架,一开始并没有考虑组件化,直到有一天我们发现页面的业务逻辑都堆在了一起,所有控件都互相牵扯,冗长的代码看起来特别累,相似的功能点都需要重复写,一旦需要更改某个功能点时需要人工遍历所有涉及到的地方再一一修改,效率极低,并且还有极高的风险出 BUG,这才意识到仅仅靠框架的一部分约束还不够,随着越来越多的业务逻辑往前端堆积,模块化和组件化的脚步迫在眉睫了。

关于模块化:

分而治之 是软件工程中的重要思想,是复杂系统开发和维护的基石,这点放在前端开发中同样适用。在解决了基本开发效率、运行效率问题之后,前端团队开始思考维护效率,模块化是目前前端最流行的分治手段,简单来说,少量的代码总比一大坨代码好维护。

JS 模块化方案有很多:AMD/CommonJS/CMD 等等,但在去年我们迎来了 ES6 的到来,帮助我们从语言的层面解决了这一难题,我们也极力地推崇使用 ES6 规范进行开发,这标志着前端模块化成为了规范,是模块化大一统的趋势,我们在新的产品线中也都统一使用 ES6。

ES6 的优势:

AMD/CommonJS/CMD:“运行时加载”

ES6:“编译时加载”(效率高)

ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。比如,CommonJS模块就是对象,输入时必须查找对象属性。

由于ES6模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽JavaScript的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

除了静态加载带来的各种好处,ES6模块还有以下好处。

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持ES6模块格式。目前,通过各种工具库,其实已经做到了这一点。

  • 将来浏览器的新API就能用模块格式提供,不再必要做成全局变量或者navigator对象的属性。

  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

关于组件化:

光有JS/CSS的模块化还不够,对于UI组件的分治也有着同样迫切的需求。

这张图基本可以描述我们产品组件化的一个基本架构。

  1. 页面上的每个 独立的 可视/可交互区域视为一个组件;
  2. 每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护
  3. 由于组件具有独立性,因此组件与组件之间可以 自由组合
  4. 页面只不过是组件的容器,负责组合组件形成功能完整的界面;
  5. 当不需要某个组件,或者想要替换组件时,可以整个目录删除/替换。

我一开始认为,组件化的意义就在于复用,如果一个功能块不能被复用,那就没必要将它组件化,但在实践中我发现这种想法是很局限的,我们应该将分治的理念贯彻到底。我认为在组件化的过程中,复用和分治要一起综合考虑,最终能够达到:页面中所有可复用的功能块都完成组件化,剩下的功能块是为了分治而组件化。

这样我们的项目一目了然,各个模块职责清晰,各个组件功能独立。

组件通信

接下来问题来了,我们是怎样做到组件之间的通信呢?在Angular中有两种通知机制:基于事件系统的广播和基于脏检查的数据监听,在我们的前端业务场景中,更多的是考虑数据的一致性,再加上事件系统后期维护成本比较高,于是我们最终选择了数据共享和监听。我们在Service层做了一个数据池,所有组件都到这里拿取数据,由于引用类型都是传递地址,它们也就指向了同一个源,源头改了,各个组件的数据当然也就跟着变了。

与设计师的磨合之路

在前端组件化开展得如火如荼的时候,我们可能忘了还有一个角色能够很大程度上地影响组件化的步伐,那个人就是设计师。

一般而言,设计师们在设计产品的时候是直接设计一整张页面再交给工程师实现的,工程师同学将参考设计稿设计页面布局,实现业务逻辑后套上这张样式大皮,完成。

但我们的 Web 应用通常体量很大:多功能、多页面、多状态、多系统。这样设计师一张张出设计稿,程序员一页页实现,多累呀,而且同时在设计和实现的过程中两方还得同时面临需求的修改、用户操作产生的页面变动等等伤脑筋的问题。有时在前端看来是同一功能的组件,设计师在设计两张页面的时候却采用了不同的样式,给组件在前端的复用带来了麻烦;前端在写样式的时候没有达到像素级的完成度,设计师又不满意。这一件件破事儿堆积起来就造成了设计师与前端程序员之间巨大的鸿沟,不仅影响工作效率还可能影响同事关系。

DaoStyle

针对这样的问题,我们做了很多的思考,既然前端可以组件化,那么为什么设计不能原子化呢?

我们将能够复用的前端控件和公共样式都拆了出来,独立成一个我们 DaoCloud 自己的 UI 框架,一些基础逻辑功能和稳定的业务逻辑我们就都留在了产品自身作为公共模块调用。这样设计师只要专注于这些控件的设计就好了。

有朋友又要说,这有什么卵用呢?我们可以直接用 BootStrap、semantic、Flat UI 这些有名气的 UI 框架啊!但是这对我们来说是很有必要的!

我们也不是一上来就喊口号要做自己的UI框架的,我们一开始也是用Bootstrap的,但是在使用的过程中,总会遇到这样那样的问题让人头疼。

首先,这些 UI 框架里包含的都是最常见和通用的控件,当你的业务功能非常复杂时就需要对这些控件修修改改再堆砌一大堆的业务逻辑使得这个组件跟需求完美契合,这是需要花巨大的精力和时间的,而且这种情况出现的频率太高了。

其次,由于我们的产品是高度定制的,这些 UI 框架中不一定有我们需要的控件,这时候为了省事就要去引进各种第三方库啊、组件啊,越引越多后就导致项目变得很臃肿。

还有,对一个品牌来说,风格的统一是无比重要的,否则就会有很山寨的感觉,界面风格统一对提升品牌气质和影响力有着莫大的作用。而如果你完全使用以上 UI 框架给你设计好的样式,确实是达到风格统一了,但却失去了品牌特色,品牌特色能极大地提升你的品牌认知度。

最后,虽然我们的 Web 应用体量大,但实际上那些 UI 框架中的控件和样式我们只会用到极小的一部分,然而我们为此却要引入它一整套的库,很不划算啊。

于是现在还有人觉得这是件没有必要的事吗?但可能又有人会说,自己开发 UI 框架多费时间啊,我们还要赶项目进度呢!但这其实是个一劳永逸的事儿,当你在这个 UI 框架中添加完成一个控件,完美达到了设计师和老板的审美要求之后,你就可以拿着它在你的项目里到处用,甚至在多个项目里到处用啦。并且控制权在你手中,实现代码也熟悉,将来想怎么改就怎么改。

那么有哪些可以作为 UI 框架的一部分呢?我们觉得应该是那些复用性高、可脱离业务逻辑存在且设计师拍版说样式基本固定了的控件。

接下来,怎么去写控件呢?我们认为,在写控件之前,我们需要对业务需求、复用情况做综合考虑,决定暴露哪些接口,在设计的时候一定要尽量简化未来这个控件的使用方式,同时,如果市面上已经有很多类似的控件,就需要去参考它们的实现方式,往往能够弥补很多我们考虑不周全的地方。控件的功能是根据业务需求来定,但不能依赖业务逻辑,更不能把特定的业务逻辑集成到控件里,虽然我们的 UI 框架是为我们的产品量身打造的,但也要保持一定的通用性。

我们的 UI 框架是由我们的产品来推动的,往往这样写控件的效率是最高的,很多控件都是当我们需要用到时才去做,这样做完了还可以即刻使用上,再进行进一步的雕琢。原则上,我们推荐控件谁用谁写,因为在使用过程中可能发现控件写得不完善的地方,由于是自己写的,改起来也方便。

于是我们就做到了产品推动着 UI 框架的发展,UI 框架又促进了产品的发展,这样一个良性循环。同时设计师、程序员也握手言和,共同朝着一个目标迈进,达到了你好我好大家好的 Happy Ending。

说到现在还是着力在前端实现上,接下来请我们的设计总监介绍从设计的角度该如何实现原子化。

演讲 pdf