十年•杭研技术秀 | 网易蜂巢的工业化前端架构(上)

社区编辑2018-05-15 16:39

2016年对于网易杭州研究院(以下简称“杭研”)而言是重要的 – 成立十周年之际,杭研正式推出了网易云产品。“十年•杭研技术秀”系列文章,由杭研研发团队倾情奉献,为您展示杭研那些有用、有趣的技术实践经验,涵盖云计算、大前端、信息安全、运维、QA、大数据、人工智能等领域,涉及分布式架构、容器、深度学习等前沿技术。正是这些宝贵的经验,造就了今天高品质的网易云产品。本文的分享来自网易蜂巢团队,阐述工业化前端架构的整体思路。





近两年前端领域风云变幻,各种技术栈层出不穷。React、Angular2、Vue2逐步三分天下,Webpack + Babel + ES6模式如日中天,PostCSS大有取代SASS、LESS、Stylus的趋势,还有HTTP2、WebComponents、WebAssembly、函数式编程等新概念不断涌现。让我们这些前端工程师应接不暇、感叹不已:“今年一个技术还没学会,明年可能就不用学了。”

回顾前端发展历史,按照@xufei《前端开发技术的发展》一文中的观点,大概可以分为三个阶段:

刀耕火种:典型特征是服务器语言(ASP、JSP、PHP)为主,结合简单的CSS和JS代码片段。是最早期的WebPage模式;


手工工场:典型特征是Ajax的出现,它使得WebApp模式成为可能。这期间jQuery、Prototype和Mootools几个库占主导地位,并出现了初步的模块加载方式;


工业革命:典型特征是各种MV*框架的不断出现,各种开发模式的不断演进,各种自动化工具的不断革新,各种标准的不断确立。
2015年ES6标准的确立标志着前端领域正式进入了蒸汽时代。但离流水线作业的电气时代还有一段路要走。

相对于去年剧烈的变革,今年的前端生态有所缓和。乘着工业化浪潮,我们网易蜂巢产品(以下简称蜂巢https://c.163.com/)前端组本着面向未来、稳步推进的原则,也对项目架构进行了优化改进,并确立了新的发展方向。

本文以蜂巢产品下的蜂巢系统、镜像中心、数据库助手、ICP备案系统和域名系统五个单页系统(SPA)为例,阐述我们组工业化前端架构的整体思路。

本文认为关于前端架构的工业化,主要应该从模块化、组件化、规范化和自动化四大方面考虑,下面一一展开。

模块化

“分工产生效能。”


模块化是“刀耕火种”和“手工工场”的分界线。它的作用是将一个大文件拆分成相互依赖的小文件,再进行统一的拼装和加载,这为多人协作提供了可能。


JS的模块化


在ES6之前,JavaScript一直没有模块系统,这对开发大型复杂的前端工程造成了巨大的障碍。

对此社区制定了一些模块加载方案,如CommonJS、AMD和CMD等,某些框架也会有自己模块系统,如Angular1、我们公司的NEJ等。总之一个比一个实现得丑,最后甚至还发展出了为了兼容几种规范的变态级UMD:


(function(root, factory) {

if (typeof exports === ‘object‘)

module.exports = factory();

else if (typeofdefine === ‘function‘ && define.amd)

define(factory);

else

root[library] = factory();

})(this,function () {

//module …

});



幸运的是,现在ES6已经在语言层面上,规定了模块系统,而且使用起来相当方便,完全可以取代现有的CommonJS和AMD规范。


当然在2016年选择ES6不算尝鲜,各种生态已经相当成熟(如图1),唯一要考虑的因素就是兼容性要求。



截止至2016年11月,各厂浏览器对ES6的支持率


如果你想使用Babel:

IE6、7就不用考虑了(貌似我们公司有项目还在支持);
IE8在项目中不能使用export * from ‘xxx’这个功能,但你不能保证使用的依赖库中没有用到,具体请参考使你的 React 应用兼容 IE8(https://github.com/xcatliu/react-ie8);
IE9+只需吃个babel-polyfill就能完美使用。


我们的蜂巢是技术型产品,只要兼容到IE9+即可,所以上ES6毫无压力。至于打包工具,目前来说最好的非Webpack莫属。因此,

技术选型:Webpack + Babel + ES6


CSS的模块化



虽然SASS、LESS、Stylus等预处理器实现了CSS的文件拆分,但没有解决CSS模块化的核心问题:选择器的私有化问题(全局污染问题)。


按道理,一个模块化的文件应该要隐藏内部作用域,只暴露少量接口给使用者。而按照目前预处理器的方式,导入一个CSS模块后,已存在的样式有被覆盖的风险。虽然重写样式是CSS的一个优势,但这并不利于多人协作。



为了避免全局选择器的冲突,各厂都制定了自己的CSS命名风格:

Bootstrap风格;
Semantic UI风格;
我们公司的NEC风格 (http://nec.netease.com/standard);



但这毕竟是弱约束。选择器随着项目的增长变得越多越复杂,然后项目组里再来个新人带入自己的风格,就更加混乱了。



所以我很赞同知乎上一个人的观点:

与其费尽心思地告诉别人要遵守某种规则,以规避某种痛苦,倒不如从工具层面就消灭这种痛苦。



从工具层面,社区又创造出Shadow DOM、CSS in JS和CSS Modules三种解决方案。

Shadow DOM是WebComponents的标准。它能解决全局污染问题,但也使样式彻底私有化了,造成外部无法重写,损失了灵活性;
CSS in JS是彻底抛弃CSS,使用JS或JSON来写样式。这种方法很激进,不能利用现有的CSS技术,而且处理伪类等问题比较困难;
CSS Modules仍然使用CSS,只是让JS来管理依赖。它能够最大化地结合CSS生态和JS模块化能力,目前来看是最好的解决方案。


目前蜂巢产品中使用的是我们公司@郑海波(https://www.zhihu.com/people/leeluolee)写的MCSS(https://github.com/leeluolee/mcss),一个比SASS好用的预处理器。

考虑到从预处理器迁移为CSS Modules的成本较高,这块内容暂时不做调整。不过有意向在新的项目中实践PostCSS + CSS Modules。

技术选型:MCSS



组件化



“现代化生产的分工协作要求工业部件遵循互换性原则。”



当前最火的React带头掀起了组件化浪潮,随后Vue、Polymer、Angular2等各种组件化框架/类库如雨后春笋般出现,我们公司@郑海波写的RegularJS(https://github.com/regularjs/regular)也是组件化框架的一面旗帜。



那么,究竟什么是组件化?



组件化的概念



首先,组件化≠模块化。好多人对这两个概念有些混淆。



模块化只是在语言层面上,对代码的拆分;而组件化是基于模块化,在设计层面上,对UI(用户界面)的拆分。



从UI拆分下来的每个包含模板(HTML)+样式(CSS)+逻辑(JS)功能完备的结构单元,我们称之为组件。



组件化除了要处理组件这种本身的封装,还要处理组件之间的逻辑(JS)继承,样式(CSS)扩展和模板(HTML)嵌套等关系。



因此,组件化实际上是一种按照模板(HTML)+样式(CSS)+逻辑(JS)三位一体的形式对面向对象的进一步抽象。


然后,我们再思考一个问题:为什么类React框架/类库是组件化的,而之前的框架/类库,如jQuery、Angular1等也有很多配套的组件,就不是组件化的呢?


React提倡的理念能给出答案:

Keep Simple. Everything can be a component.


这句话就是说页面上所有的东西都是组件。页面是个大型组件,可以拆成若干个中型组件,然后中型组件还可以再拆,拆成若干个小型组件,小型组件也可以再拆,直到拆成DOM元素为止。DOM元素可以看成是浏览器自身的组件,作为组件的基本单元。

这其实是一种分治(分而治之)思想。



传统前端框架/类库的思想是先组织DOM,然后把某些可复用的逻辑封装成组件来操作DOM,是DOM优先;而组件化框架/类库的思想是先按顶层设计来构思组件,然后用DOM这种基本单元结合相应逻辑来实现组件,是组件优先。这是两者本质的区别。


再看看客户端框架,像WinForm、WPF、Android等,它们从诞生的那天起就是组件化的。当年写客户端的日子比较舒服,就是因为没有类似DOM这样的繁琐操作。而前端领域发展曲折,传统框架只是面向WebPage的,随着Web业务日益复杂化和多元化,面向WebPage逐渐转成面向WebApp,传统框架已经不能很好的解决问题了,因此才从客户端框架经验中引入了组件化思想。


综上,组件化是一种在设计层面上,对项目中UI进行整体的疏理和拆解,再按照HTML+CSS+JS三位一体的面向对象来进行封装的过程或思想。


按照组件化思想,我们改变了原来按照HTML、CSS、JS文件分开管理的策略,采取一个组件一个目录的原则。单个组件目录结构如下:

component/

demo/

index.md # 组件文档

test/

spec.js # 单元测试

index.mcss # 组件样式

index.js # 组件逻辑

index.rgl # 组件模板

index.json # 组件信息


从理念上,我很认同Vue的*.vue这种单文件结构;但在实践上,我认为用一个目录来表示组件,功能性和扩展性会更强一些(比如要增加单元测试和组件文档等功能),而且多文件在编辑器中分栏编辑更加方便。


组件的分类


按照组件的代码组成,组件可以分为:HTML、HTML+CSS、JS、HTML+JS、HTML+CSS+JS。


本文认为缺失JS的结构单元也是组件,比如仅有HTML+CSS的结构单元称为CSS组件。


按照组件的通用性(复用性),组件可以分为以下三类:

通用组件:不同产品间可以复用的组件。比如ListView(https://github.com/regular-ui/ui-listview),既可以在蜂巢中使用,也可以在别的产品中使用;
通用业务组件:仅在同一产品中可以复用的组件。比如按照蜂巢视觉,对ListView进行了样式扩展的RepoCards,可以在蜂巢中多处使用,且能进一步扩展,但只能在蜂巢中使用;
业务组件:不可以复用的组件。比如具体场景中使用的组件,一次性的业务模块等。

蜂巢组件通用性示例


本文认为组件的一个设计原则是:在不增加组件配置复杂度的情况下,尽可能的提高组件通用性。

本文未结束,敬请期待下篇。