Open fouber opened 9 years ago
写的不错,mark
真不错
@fouber 相关文章的链接就是这篇文章?
@jtHwong 还没写,只是占坑,可以理解为那些链接是目录大纲,我写好了一篇文章就更新一个链接
受益匪浅, 有时间好好了解一下fis。
不断实践,持续关注
相当好的一系列文章。必须赞 and mark.
不错不错
mark
前半部分,收获很多,对组件化开发,又有了新的认识!感谢分享
以前只知道fis突然从原来的task-based的前端构建工具转向为前端集成解决方案, 开发的语言也从PHP转向到Node , 现在总算清楚改变的由来. facebook真是前端的先行者啊.
个人觉得fis能做的, gulp都可以做, 而且gulp灵活性和可编程性大多了, 插件也更加成熟,好用得多。 不过赞楼主博客
@BenBBear
确切的来说,是fis做了一些gulp没关心,但其实工程真正需要的事——资源管理。
非要抬杠的话,我们确实可以说fis能做的事情gulp都能做,因为最起码我们可以把fis的代码都贴到gulpfile中实现fis的功能,搞定!
但我想争辩一下,关于fis做了gulp没关心的事情的问题。如果你认可本文中对前端工程发展阶段的总结,不知道你是否认可以下证据链:
前端工程第四阶段目标是组织大工程开发
→ 组件化成为工程分治手段
→ 组件化开发会造成资源碎片化
→ 资源管理成为第四阶段前端工程基础
前端其实从一开始就缺乏资源管理的能力,script标签和link标签的写法是有原罪的,资源加载须“可编程”,才能解决一个庞大网站的工程级性能优化问题,你不会天真的以为,这些都能被“构建解决”吧?
fis的构建,只是其功能的一半,那些什么压缩校验根本不是重点,构建的重点工作是建立资源表,到这里只是fis的半壁江山,剩下的是基于表的资源加载框架,彻底解决大工程的资源管理问题,我的下一篇文章会全面介绍各自前端业务场景(前端渲染、后端渲染、单页面应用、多页面应用、PC端、移动端等)下的资源管理策略,到时候欢迎讨论。
你应该还不明白fis在做什么,gulp又在做什么。
编译就不说了, gulp用glob pattern /**/*.ext
就可以达到 fis 匹配 *.ext
的目的,其他编译相关的,gulp更开放+秒懂。(也可以一份代码做很多事情,不用一个情况写一个很不一样的)
资源表gulp同样可以生成,有了资源表, 至少对于js来说, 和替换source code 中的file path是等价的,更何况css在部分框架下也是通过js加载(因为 fis 猫似也不能为js module load实现file path替换,所以都是要加载资源分配表,根据表来加载source), filepath替换作用主要是在css/html
中,我没有查过gulp有没有做替换的插件,但只对于html/css
来说是应该不难的。
至于文件copy/upload就更不用说了
一直在用gulp, 试用过fis, 记得用得时候感觉很多插件质量堪忧, 而且fis打印的信息很不友好,速度也慢, gulp出了错都知道怎么回事, fis却不同,当然自己可能也是不太会调试fis。 因此个人还是喜欢用gulp吧。
自己真的还差得很远,要多和您学习,期待新的博文! 工具类理解就行, 达到目的就好了哈 O(∩_∩)O
@BenBBear
难得有人来争论,一个个掰扯:
编译就不说了, gulp用glob pattern //.ext 就可以达到 fis 匹配 .ext的目的
gulp的glob是这个:
gulp.src(glob).pipe(foo);
含义是找到glob匹配的所有源码,做foo这件事
而fis与之可以类比的配置是:
fis.match(glob, {
foo: bar
})
fis的含义是:凡是匹配glob的文件,其文件对象多了一个属性 foo,值是 bar
,这意味着什么?比如当你设计开发规范的时候,我们可能会这样描述:
1. 以下划线开头的less文件是mixin,不输出为css文件
2. components目录下的js是模块化js,需要wrap
3. 所有js、css压缩,并加md5戳,但.min.的文件不压缩
4. ...
把以上架构设计,翻译成fis的match就是:
fis.match('**/_*.less', {
release: false
});
fis.match('components/**.js', {
wrap: true
});
fis.match('**.{js|css}', {
md5: true,
optimize: true
});
fis.match('**.min.*', {
optimize: false
});
fis的match其实是用来给文件添加属性的,所有文件都会经过各种插件,但插件的处理行为取决于这些追加文件属性,因此可以用这种方式,来直接配置开发规范,而不是像gulp那样,把整个开发规范写入到构建流程中,难以维护。
如果你熟悉gulp,可以大概考虑一下上面的那些开发规范怎么翻译成gulp的配置。
@fouber
恩,您说得对, fis的构建更加一目了然, 而且让人们书写标准,而不是繁杂的代码
。
@BenBBear
关于资源定位的替换,fis也是使用的文件的release属性,所以,fis允许你的开发目录和部署目录不一致,比如我们可能这样定义开发规范:
源码中,模块资源都放到components目录下
但却这样定义部署规范:
所有静态资源发布后,都生成到public目录下,访问路径是 /public/xxxx
所以,可以在fis中描述为:
fis.match('**.{js|css|png|gif}', {
release: '/public/$0' // $0指代原路径
})
这样,代码中的所有资源定位标识最后前面都会多了 /public/
这个层级。
资源定位的依据是文件属性,而文件属性又能通过match分配,这就是fis连接开发规范和部署规范的方式,gulp应该没有关心这个问题吧
恩恩
对组件化开发的思路印象深刻,好文得多拜读!
好文,mark
gulp的好处主要是架构和生态上的。把我在其他地方比较grunt和gulp的评论贴过来,供参考:
gulpjs有几个很好的地方。 第一是有一个基于流和管道的模型,符合unix工具组合的传统。基于这样的模型,使其能形成互相协作和有序竞争的插件生态。 第二是代码的写法比基于配置的grunt要简洁和一致。 第三有性能优势。当然这一点broccoli有不同意见。
这些优势非常明显,以至于在grunt已经那么火的情况下,gulp出来没多少时候就可以跟grunt分庭抗礼,并且绝大部分功能都有对应插件——这一点broccoli就比不上(刚出来时蛮火的,但发展势头似乎现在有些停滞)。
关于插件杂乱无章这个事情。
整个nodejs的生态就是这样的,同样的事情可能会有很多modules。 第一,有竞争是好事。 第二,相对来说gulp由于模型的限制和其社区的严苛到变态的要求(看看有多少插件被以不符合gulp的理念而被干掉),所以每个插件的功能都相对单一,其竞争是相对有序的(只拼单一功能),所以会很快收敛到一个最多几个共存。有几个共存的原因往往是因为其底层实现确实不同,这是很正常的。 第三,gulp确实存在一些问题(比如错误处理),我们使用中也遇到,不过这些问题应该在下一个大版本4.0会解决,尽管4.0迟迟不出来也是个问题,但是就目前而言我还是愿意再耐心等待下。
如果需要的是开箱即用,那么grunt也许还是一个ok的选择,但是对于我来说,开箱即用不是我的主要目标,所以会选择架构上看更好的gulp。
生态这事,我觉得其实三者都差不多,因为大部分情况下,就是简单的wrap已有的类库如uglify啥的,fis并不比gulp的插件生态差。
fis的劣势之一是国际化推动不够。
发自我的 iPhone
在 2015年9月8日,20:28,HE Shi-Jun notifications@github.com 写道:
gulp的好处主要是架构和生态上的。把我在其他地方比较grunt和gulp的评论贴过来,供参考:
gulpjs有几个很好的地方。 第一是有一个基于流和管道的模型,符合unix工具组合的传统。基于这样的模型,使其能形成互相协作和有序竞争的插件生态。 第二是代码的写法比基于配置的grunt要简洁和一致。 第三有性能优势。当然这一点broccoli有不同意见。
这些优势非常明显,以至于在grunt已经那么火的情况下,gulp出来没多少时候就可以跟grunt分庭抗礼,并且绝大部分功能都有对应插件——这一点broccoli就比不上(刚出来时蛮火的,但发展势头似乎现在有些停滞)。
关于插件杂乱无章这个事情。
整个nodejs的生态就是这样的,同样的事情可能会有很多modules。 第一,有竞争是好事。 第二,相对来说gulp由于模型的限制和其社区的严苛到变态的要求(看看有多少插件被以不符合gulp的理念而被干掉),所以每个插件的功能都相对单一,其竞争是相对有序的(只拼单一功能),所以会很快收敛到一个最多几个共存。有几个共存的原因往往是因为其底层实现确实不同,这是很正常的。 第三,gulp确实存在一些问题(比如错误处理),我们使用中也遇到,不过这些问题应该在下一个大版本4.0会解决,尽管4.0迟迟不出来也是个问题,但是就目前而言我还是愿意再耐心等待下。
如果需要的是开箱即用,那么grunt也许还是一个ok的选择,但是对于我来说,开箱即用不是我的主要目标,所以会选择架构上看更好的gulp。
— Reply to this email directly or view it on GitHub.
@hax
本文主要在描述以下事实,不知道你是否认同:
大型前端项目的开发与维护的工程痛点:
- 性能方面,需要对资源部署与加载做特殊处理
- 开发方面,模块化&组件化开发是基本分治手段
∵ 静态资源管理能够同时解决以上两个问题
∴ 静态资源管理是构建大型前端应用的重要基础
就是是否接受“静态资源管理是构建大型前端应用的重要基础”这个结论?
如果认同这种观点,我觉得gulp尽管其本身设计思路清晰,但在其设计目标中并没有针对这个重要问题给出支持,相比之下,webpack就是这方面的进步。诚然,gulp的“单纯”使得它具有无限的可能,理论上可以用来做任何事,当然也包括解决上述问题,但我觉得它本身其实对这件事是没有关注的,况且静态资源管理本身展开之后会有非常多的处理细节。
grunt/gulp可以类比make,而我们前端,还需要一个javac。
看来webpack作为一个纯前端的工具还是有所限制的,期待fis能打通前后端。
@fouber gulpjs本身发展到现在其实更像一个简单的任务协作框架。各个插件对虚拟文件树进行处理,完成自己的那个构建步骤。 显然框架本身不回答“静态资源管理”的问题,单一插件也管不了这个。
我明白这是fis的重点——提供针对前端工程所需的更high-level的抽象。但既然是高层抽象,对此达成一致其实更不容易。当然webpack和fis或者其他工具会有相同点,但既然是高层抽象,通常会牵涉更多层面,比如组件化,分歧点更多,遇到未预料的新变化的可能性更高。这是我对fis有所保留的重要原因。
另外一方面,我其实更期望fis或者这类方案是建立在gulp的生态上的,这样能享受gulp生态的好处。gulp之中其实一直存在这样的子系统。比如sourcemap的支持,比如cache的部分。我个人觉得那样的系统更健康。
@hax
我个人觉得那样的系统更健康。
我感觉这个讨论可以停止了,就如同要信主还是上帝还是佛祖一样,没多大意义;
赞~
@hax
task-based或者管道式处理工具都有一个潜意识:
文件与文件之间不存在构建依赖
而与资源管理相关的构建需求(定位和内嵌)经常会有文件间的构建依赖问题,这是fis无法建立在这里工具的生态中的根本原因。就如同gulp的设计理念使其无法把自己作为一个task融入到grunt的生态中一样。
@hax
诚如你所言,fis针对工程有一个抽象——定位、内嵌和依赖声明。
定位和内嵌纯靠构建,相比其他产品孰优孰劣的不重要。但对依赖的处理,业界主流做法是靠构建,fis希望将这件事拆成资源表+加载框架,然后工程优化在框架上发力。
我觉得方向是对的,但整件事确实很有难度,主要是框架是一种对业务的侵入,而且它要与团队的工程规范紧密配合,往往只有借鉴价值,没有复用价值。虽然工程上更有优势,但需要一定的功力来驾驭,相比傻瓜式的构建方案,在门槛方面实在是差很远。。。
fis要想获得认可,框架这部分必须做出生态才行,而这种生态其实很难搞,不但可复用性很低,而且涉及工程的核心部分,对质量的要求又极高,这应该是fis面临的最大挑战。
所以我写这些文章不会“言必及fis”,还是让更多人先接受这种工程理念,用什么工具实现都好,不迈过这个阶段fis仍然还是小众的。
@fouber 我不同意“task-based或者管道式处理工具有潜意识:文件与文件之间不存在构建依赖”。当然默认他们是没管这个事情。
特别需要注意的是,gulp的管道之间流转的是虚拟文件树,而不是单个文件!所以gulp框架本身是可以处理依赖的——只要插件想处理的话。
gulp的设计理念使其无法把自己作为一个task融入到grunt的生态中一样
并非不能在grunt中使用gulp,只是没有这个必要。而且grunt有很多设计上的缺陷。
所以gulp和fis的关系跟grunt和gulp的关系是非常不一样的。 实际上gulp这样的插件体系能更好的协作:
所以我个人认为fis这样的系统完全可以(也应该)基于gulp这样的架构的。
另外,我应该在很多场合说过,我认为未来还是要大量运用运行时(浏览器端)loader,并由服务器响应实时的部分构建,而不是在全部在部署前的静态构建做完。核心的想法是:将更多的优化决策推迟到运行时。在这样一种思路下,也许静态构建只需要处理更少更简单的任务,架构本身的复杂度也可以得到缓解。
@hax
我说的是构建依赖,不是资源关系依赖。
所谓构建依赖,是指我要处理一个文件内容的时候,先要对它所依赖的文件进行完整构建之后在处理这个文件的内容,这必然会打破task-based或者管道处理的工作流程,它不是一个单一方向的数据流动,而是一种对树结构的深度优先遍历过程。
进一步的各种处理插件利用依赖树的信息做出自己的处理,比如内嵌资源插件,将原本的引用依赖修改为嵌入依赖
比如这个内嵌资源插件,当我们在一个文件中要内嵌另一个文件时,应该让被嵌入文件先经过完整的构建,得到构建后内容再插入到当前位置才行。
比如我已经用gulp配置好了针对less文件的一系列操作:
gulp.src('**/*.less')
.pipe(less())
.pipe(csslint())
.pipe(csslint.reporter())
.pipe(minifycss())
.pipe(gulp.dest('src/css'));
当我们需要『在JS中内嵌less』的时候,内嵌插件遇到less文件,需要调用之前对less的一系列处理配置得到文件内容再内嵌才行,而在处理less的过程中,又可能遇到图片的内嵌标识,这个时候还需要调用对图片的处理配置得到内容再return,这也是我说的,为什么构建工具应该提供一个 compile(file, callback)
函数,使得构建流程不再是管道式,而是可递归的处理过程。
另外,我应该在很多场合说过,我认为未来还是要大量运用运行时(浏览器端)loader,并由服务器响应实时的部分构建,而不是在全部在部署前的静态构建做完。核心的想法是:将更多的优化决策推迟到运行时。在这样一种思路下,也许静态构建只需要处理更少更简单的任务,架构本身的复杂度也可以得到缓解。
恩,这个非常认同,这也是我们一直以来的做法和希望推广的(违反广告法)实践,写一两篇文章分享一下过去我们在运行时的优化实践,包括前端渲染和后端渲染,PC/移动,单页面/多页面等不同业务场景下的做法。
捡到货了,这真是“前端攻城”啊!
第二次拜读这篇文章。上次看完后对下面的结构有几点疑问,忘能得到大大指点。
除了技术,也很佩服大大文中的谦虚。😄
components
和pages
都有js文件,它们共用的js模块应该放到哪里?自己想通:前面提到“components
组和成pages
”,所有pages
中的js应该没有逻辑,主要是require('../components/*')
,所以pages
只是依赖components
。
components/config/
的应用场景,components
是很小的单位,觉得配置应该在app层或page层。(可能是你随便写的)apis.js
, routes.js
类似文件,放在app/base/**
是否合理?可以收藏Issue吗?
虽然还是菜鸟,但是看了还是有收获很大的感觉
@epooren
components和pages都有js文件,它们共用的js模块应该放到哪里?
共用的js模块也是一个模块,丢到components下就行了,比如我图中列出的『components/ajax』模块,就是一个pages和components都可能用到的公共模块,正如你所想,page负责组装,它需要的大部分逻辑都被封装到某个components下了。
不理解components/config/的应用场景,components是很小的单位,觉得配置应该在app层或page层。(可能是你随便写的)
这个例子举得也不算太随便吧。我们项目中经常会有一些文件用于维护一些全局的配置。由于js模块可以导出接口,形成闭包,配合模块化的机制,可以把一些全局的配置存储在这种模块中,供其他模块共享数据,因此有的时候我们会这样设计config:
var data = {
version: '1.0.1',
api: '/data',
...
};
exports.get = function(key){
return data[key];
};
exports.set = function(key, value){
data[key] = value;
};
在其他模块就可以这样使用:
var config = require('config');
var api = config.get('api');
...
$.getJSON(api + '/info', function(data){
console.log(data);
config.set('username', data.username);
});
单页应用,我习惯有apis.js, routes.js类似文件,放在app/base/**是否合理?
其实这些模块都可以放到components中,以模块的形式管理,资源放到其他地方去与放在components的区别主要区别就是components中的js是模块化管理的,模块化管理并不影响你的原始功能,开发体验上的差别可能是模块化的资源要用之前都要require一下,而非模块化资源可以在全局释放一些变量,在其后的js可以直接当做环境变量使用。
所以是可以把一些代码(比如jquery)放在app/lib或者app/base这样的目录下,作为非模块化资源使用,其中的取舍开发者自己把握就好了。
@milla 可以watch这个项目,就订阅了
行云流水,无卖弄。也算对前端的本分大致了解了一下
@searchpcc 你的头像太明显啦。。。。
有个这样的场景,
开始时创建了 components/header 组件,后来引用的页面需要对 header 进行扩展,可能是换下背景、换下颜色,这情况下怎样处理会好一些呢。
复制一份 components/header2 改 html 和 css ?会导致代码冗余重复。
还是在 components/header 下复制新的 header.html 然后加上一个 className 再扩展 css ?变得不像组件了。
又或者重构 components/header 提取 components/header-common 之类的。
想请教两个问题:
@tantion 比较合理的方式是:组件开发者把容易变化的部分对外暴露接口(api),组件使用者传入合适的数据去调用。 在css中来说,css变量比较适合这种情况。 考虑到兼容性问题,我觉得在页面中对组件的样式进行覆盖是可以接受的,反正css本来就是层叠样式表嘛,当然这个只针对背景、颜色这种简单的变化,如果两个组件差异太大,还是做成两个独立的组件比较好,否则不同的样式/逻辑耦合在一起,也是很麻烦。
说到继承的问题,我感觉除非是特别明显的可以用面向对象的继承关系去抽象的,比如“猫--》折耳猫”什么的。对于一般的UI组件来说,组合很多时候比继承要好。
@tantion
header和header2,以及组件粒度控制问题,我觉得统一可以回答为『BEM』,注意,很多人把BEM理解为CSS的命名规范,其实不是的,BEM更大的意义应该是粒度划分的方法论。
开始时创建了 components/header 组件,后来引用的页面需要对 header 进行扩展,可能是换下背景、换下颜色,这情况下怎样处理会好一些呢。
这种情况,就叫做『块的修饰』
比如我的header组件HTML源码可能是这样的:
<div class="header">
<h1 class="header__title">Hello World</h1>
</div>
其中.header就是『块』,.header__title就是块中的『元素』,OK,当你的需求是『对header组件换一下背景、换一下颜色』的时候,我们需要给header提供一种『修饰符』。
如果你的header组件的换肤是希望『在背景色和字体颜色两个维度分别给出可选项,然后组合效果』的话,你可以这样定义的组件修饰符:
<div class="header header_bg_red header_color_green">
<h1 class="header__title">Hello World</h1>
</div>
对应的样式写法大概就是:
.header { background-color: white; color: black; }
.header_bg_red { background-color: red; }
.header_color_green { color: green; }
而如果你的颜色和背景色并不是独立设置,而是配套组合为一个『主题』的时候,你的修饰符可能具有一定的语义:
<div class="header header_night">
<h1 class="header__title">Hello World</h1>
</div>
css略。
『修饰符』就是用来使组件呈现不同状态的,如果你希望在创建组件时能够指定修饰符,只要把组件html片段变成模板,把修饰符变成模板变量就行了:
<div class="header header_{{ theme }}">
<h1 class="header__title">{{ title }}</h1>
</div>
组件内部的颜色、字体等显示属性应该由组件自己定义好,然后开放为『theme』提供给组件调用者使用,这样比较便于后期的维护。但有些属性可能不是组件内部能决定的,比如组件的宽高,这是跟使用组件的父容器有关,这种情况下,可以如 @sapjax 所说,采用层叠方式在父组件中覆盖子组件部分样式。
还是以这个header为例,假设我们要在index页面中使用组件:
<div class="index">
...
{{ component $id="header" theme="night" title="Hello World" }}
...
</div>
这里的举例采用了某种后端模板引擎来组织组件HTML片段,当然我们也可以以React/Vue/Polymer等其他框架的方式在前端进行组织,这不是我们关注的重点,无论哪种方式组织,其最终在浏览器中可运行的结果都将是得到以下完整的HTML结构:
<div class="index">
...
<div class="header header_night">
<h1 class="header__title">Hello World</h1>
</div>
...
</div>
我们心里要知道『无论采用什么组件化方案,最终在浏览器中都将以上述代码运行』,所以,我们在index页面中使用header组件,就可以这样重新定义它的宽高属性了:
.index .header { width: 50%; height: 30px; }
组件组合是方便了,那扩展方面呢,怎么处理继承?
GUI软件中组件与组件之间一般都是组合,很少用到继承,这点在前端中尤其如此。考虑继承之前,先考虑这种扩展需求能不能用上述『修饰符』的思路提供,我想不到有什么特殊的例子在GUI中需要我们必须用继承。而且一个组件靠HTML定义结构,CSS描述展现,JS带来交互,当你要『继承』这样一个单元的时候,你也会发现很别扭,由HTML定义的结构根本没有『继承』可言,最多是挖坑-填坑的过程;CSS由于其层叠的工作机制,勉强算得上可继承,而针对HTML结构实现了一系列交互行为的JS也毫无继承的必要性可言。
既然『组合』和『继承』都能达到相同的工程目的,且对于UI组件没有很好的继承机制,那么相比之下,我们更倾向于用组合来解决问题。
除了赞还有什么话说。
我是前端新手,我想问一下,组件都是HTML文件,怎么在页面里面使用?
@ikarosu
有两大类组件组织的方式:『前端渲染』和『后端渲染』。
这是业界的普遍叫法,其实不准确,狭义的渲染就是指浏览器的HTML/CSS/JS的解析并最终绘制成像,都发生在浏览器中,而这里所说的『前端渲染』和『后端渲染』,其实是指『前端组装HTML』和『后端组装HTML』,这两种技术选型要分开来讲,说来话长,是我下一篇要介绍的主要内容,我到时候再展开吧。。。
@fouber 我们公司目前后台使用.net MVC来写的,适合用哪种方式? 我想知道最后写下来组件HTML的代码是怎么样的,页面代码又是怎么样的。是不是需要用到什么框架之类的东西?能不能写个demo让我稍微明白一点
<div v-class="styles.label"></div>
@import "z-index";
@import "color";
.label {
z-index: $label-z-index;
color: $green;
}
import template from './label.html';
import styles from './label.scss';
export default {
template,
data() {
return {
styles
}
}
};
目前我们在Vue这样用,使用了style-loader,css-loader,style-loader处理css局域问题,这样也做了组件化,复用性也比较不错。
目录结构也是label.js label.scss label.html 放在一个label文件夹下,别的项目可以直接拿过去用,异步加载的效果也不错,直接将js、css、html一起异步调用。而且js、css、html都可以单独被复用。
Nice,接下来打算把团队强化到第三阶段,并涉猎第四阶段相关的东西。 收藏了,再精读一次。
Thank for your great post.
@justqyx 求带 ~~
你好,切图仔。
不知道你的团队如何定义前端开发,据我所知,时至今日仍然有很多团队会把前端开发归类为产品或者设计岗位,虽然身份之争多少有些无谓,但我对这种偏见还是心存芥蒂,酝酿了许久,决定写一个系列的文章,试着从工程的角度系统的介绍一下我对前端,尤其是Web前端的理解。
只要我们还把自己的工作看作为一项软件开发活动,那么我相信读过下面的内容你也一定会有所共鸣。
前端,是一种GUI软件
现如今前端可谓包罗万象,产品形态五花八门,涉猎极广,什么高大上的基础库/框架,拽炫酷的宣传页面,还有屌炸天的小游戏……不过这些一两个文件的小项目并非是前端技术的主要应用场景,更具商业价值的则是复杂的Web应用,它们功能完善,界面繁多,为用户提供了完整的产品体验,可能是新闻聚合网站,可能是在线购物平台,可能是社交网络,可能是金融信贷应用,可能是音乐互动社区,也可能是视频上传与分享平台……
如此复杂的Web应用,动辄几十上百人共同开发维护,其前端界面通常也颇具规模,工程量不亚于一般的传统GUI软件:
尽管Web应用的复杂程度与日俱增,用户对其前端界面也提出了更高的要求,但时至今日仍然没有多少前端开发者会从软件工程的角度去思考前端开发,来助力团队的开发效率,更有甚者还对前端保留着”如玩具般简单“的刻板印象,日复一日,刀耕火种。
历史悠久的前端开发,始终像是放养的野孩子,原始如斯,不免让人慨叹!
前端工程的三个阶段
现在的前端开发倒也并非一无所有,回顾一下曾经经历过或听闻过的项目,为了提升其前端开发效率和运行性能,前端团队的工程建设大致会经历三个阶段:
第一阶段:库/框架选型
前端工程建设的第一项任务就是根据项目特征进行技术选型。
基本上现在没有人完全从0开始做网站,哪怕是政府项目用个jquery都很正常吧,React/Angularjs等框架横空出世,解放了不少生产力,合理的技术选型可以为项目节省许多工程量这点毋庸置疑。
第二阶段:简单构建优化
选型之后基本上就可以开始敲码了,不过光解决开发效率还不够,必须要兼顾运行性能。前端工程进行到第二阶段会选型一种构建工具,对代码进行压缩,校验,之后再以页面为单位进行简单的资源合并。
前端开发工程化程度之低,常常出乎我的意料,我之前在百度工作时是没有多少概念的,直到离开大公司的温室,去到业界与更多的团队交流才发现,能做到这个阶段在业界来说已然超出平均水平,属于“具备较高工程化程度”的团队了,查看网上形形色色的网页源代码,能做到最基本的JS/CSS压缩的Web应用都已跨入标准互联网公司行列,不难理解为什么很多前端团队对于前端工程构建的认知还仅停留在“压缩、校验、合并”这种程度。
第三阶段:JS/CSS模块化开发
分而治之是软件工程中的重要思想,是复杂系统开发和维护的基石,这点放在前端开发中同样适用。在解决了基本开发效率运行效率问题之后,前端团队开始思考维护效率,模块化是目前前端最流行的分治手段。
JS模块化方案很多,AMD/CommonJS/UMD/ES6 Module等,对应的框架和工具也一大堆,说起来很烦,大家自行百度吧;CSS模块化开发基本都是在less、sass、stylus等预处理器的import/mixin特性支持下实现的。
虽然这些技术由来已久,在如今这个“言必及React”的时代略显落伍,但想想业界的绝大多数团队的工程化落后程度,放眼望去,毫不夸张的说,能达到第三阶段的前端团队已属于高端行列,基本具备了开发维护一般规模Web应用的能力。
然而,做到这些就够了么?Naive!
第四阶段
当我们要开发一款完整的Web应用时,前端将面临更多的工程问题,比如:
这些无疑是一系列严肃的系统工程问题。
前面讲的三个阶段虽然相比曾经“茹毛饮血”的时代进步不少,但用于支撑第四阶段的多人合作开发以及精细的性能优化似乎还欠缺点什么。
到底,缺什么呢?
没有银弹
读过《人月神话》的人应该都听说过,软件工程 没有银弹。没错,前端开发同样没有银弹,可是现在是连™铅弹都没有的年月!(刚有了BB弹,摔)
前端历来以“简单”著称,在前端开发者群体中,小而美的价值观占据着主要的话语权,甚至成为了某种信仰,想与其他人交流一下工程方面的心得,得到的回应往往都是两个字:太重。
工程方案其实也可以小而美!只不过它的小而美不是指代码量,而是指“规则”。找到问题的根源,用最少最简单明了的规则制定出最容易遵守最容易理解的开发规范或工具,以提升开发效率和工程质量,这同样是小而美的典范!
2011年我有幸参与到 FIS 项目中,与百度众多大中型项目的前端研发团队共同合作,不断探索实践前端开发的工程化解决方案,13年离开百度去往UC,面对完全不同的产品形态,不同的业务场景,不同的适配终端,甚至不同的网络环境,过往的方法论仍然能够快速落地,为多个团队的不同业务场景量身定制出合理的前端解决方案。
这些经历让我明悟了一个道理:
分治的确是非常重要的工程优化手段。在我看来,前端作为一种GUI软件,光有JS/CSS的模块化还不够,对于UI组件的分治也有着同样迫切的需求:
如上图,这是我所信仰的前端组件化开发理念,简单解读一下:
其中第二项描述的就近维护原则,是我觉得最具工程价值的地方,它为前端开发提供了很好的分治策略,每个开发者都将清楚的知道,自己所开发维护的功能单元,其代码必然存在于对应的组件目录中,在那个目录下能找到有关这个功能单元的所有内部逻辑,样式也好,JS也好,页面结构也好,都在那里。
组件化开发具有较高的通用性,无论是前端渲染的单页面应用,还是后端模板渲染的多页面应用,组件化开发的概念都能适用。组件HTML部分根据业务选型的不同,可以是静态的HTML文件,可以是前端模板,也可以是后端模板:
基于这样的工程理念,我们很容易将系统以独立的组件为单元进行分工划分:
由于系统功能被分治到独立的模块或组件中,粒度比较精细,组织形式松散,开发者之间不会产生开发时序的依赖,大幅提升并行的开发效率,理论上允许随时加入新成员认领组件开发或维护工作,也更容易支持多个团队共同维护一个大型站点的开发。
结合前面提到的模块化开发,整个前端项目可以划分为这么几种开发概念:
以上5种开发概念以相对较少的规则组成了前端开发的基本工程结构,基于这些理念,我眼中的前端开发就成了这个样子:
组件的JS可依赖其他JS模块,
CSS可依赖其他CSS单元
综合上面的描述,对于一般中小规模的项目,大致可以规划出这样的源码目录结构:
如果项目规模较大,涉及多个团队协作,还可以将具有相关业务功能的页面组织在一起,形成一个子系统,进一步将整个站点拆分出多个子系统来分配给不同团队维护,针对这种情况后面我会单开文章详细介绍。
以上架构设计历经许多不同公司不同业务场景的前端团队验证,收获了不错的口碑,是行之有效的前端工程分治方案。
上面提到的模块化/组件化开发,仅仅描述了一种开发理念,也可以认为是一种开发规范,倘若你认可这规范,对它的分治策略产生了共鸣,那我们就可以继续聊聊它的具体实现了。
很明显,模块化/组件化开发之后,我们最终要解决的,就是模块/组件加载的技术问题。然而前端与客户端GUI软件有一个很大的不同:
前端应用没有安装过程,其所需程序资源都部署在远程服务器,用户使用浏览器访问不同的页面来加载不同的资源,随着页面访问的增加,渐进式的将整个程序下载到本地运行,“增量下载”是前端在工程上有别于客户端GUI软件的根本原因。
上图展示了一款界面繁多功能丰富的应用,如果采用Web实现,相信也是不小的体量,如果用户第一次访问页面就强制其加载全站静态资源再展示,相信会有很多用户因为失去耐心而流失。根据“增量”的原则,我们应该精心规划每个页面的资源加载策略,使得用户无论访问哪个页面都能按需加载页面所需资源,没访问过的无需加载,访问过的可以缓存复用,最终带来流畅的应用体验。
这正是Web应用“免安装”的魅力所在。
由“增量”原则引申出的前端优化技巧几乎成为了性能优化的核心,有加载相关的按需加载、延迟加载、预加载、请求合并等策略;有缓存相关的浏览器缓存利用,缓存更新、缓存共享、非覆盖式发布等方案;还有复杂的BigRender、BigPipe、Quickling、PageCache等技术。这些优化方案无不围绕着如何将增量原则做到极致而展开。
所以我觉得:
相信这种贯彻不会随着时间的推移而改变,在可预见的未来,无论在HTTP1.x还是HTTP2.0时代,无论在ES5亦或者ES6/7时代,无论是AMD/CommonJS/UMD亦或者ES6 module时代,无论端内技术如何变迁,我们都有足够充分的理由要做好前端程序资源的增量加载。
正如前面说到的,第三阶段前端工程缺少点什么呢?我觉得是在其基础架构中缺少这样一种“智能”的资源加载方案。没有这样的方案,很难将前端应用的规模发展到第四阶段,很难实现落地前面介绍的那种组件化开发方案,也很难让多方合作高效率的完成一项大型应用的开发,并保证其最终运行性能良好。在第四阶段,我们需要强大的工程化手段来管理”玩具般简单“的前端开发。
在我的印象中,Facebook是这方面探索的伟大先驱之一,早在2010年的Velocity China大会上,来自Facebook的David Wei博士就为业界展示了他们令人惊艳的静态网页资源管理和优化技术。
David Wei博士在当年的交流会上提到过一些关于Facebook的一些产品数据:
这是一个状态爆炸的问题,将所有状态乘起来,整个网站的资源组合方式会达到几百万种之多(去重之后统计大概有300万种组合方式)。支撑这么大规模前端项目运行的底层架构正是魏博士在那次演讲中分享的Static Resource Management System(静态资源管理系统),用以解决Facebook项目中有关前端工程的3D问题(Development,Deployment,Debugging)。
那段时间 FIS 项目正好遇到瓶颈,当时的FIS还是一个用php写的task-based构建工具,那时候对于前端工程的认知度很低,觉得前端构建不就是几个压缩优化校验打包任务的组合吗,写好流程调度,就针对不同需求写插件呗,看似非常简单。但当我们支撑越来越多的业务团队,接触到各种不同的业务场景时,我们深刻的感受到task-based工具的粗糙,团队每天疲于根据各种业务场景编写各种打包插件,构建逻辑异常复杂,隐隐看到不可控的迹象。
我们很快意识到把基础架构放到构建工具中实现是一件很愚蠢的事,试图依靠构建工具实现各种优化策略使得构建变成了一个巨大的黑盒,一旦发生问题,定位起来非常困难,而且每种业务场景都有不同的优化需求,构建工具只能通过静态分析来优化加载,具有很大的局限性,单页面/多页面/PC端/移动端/前端渲染/后端渲染/多语言/多皮肤/高级优化等等资源加载问题,总不能给每个都写一套工具吧,更何况这些问题彼此之间还可以有多种组合应用,工具根本写不过来。
Facebook的做法无疑为我们亮起了一盏明灯,不过可惜它并不开源(不是技术封锁,而是这个系统依赖FB体系中的其他方面,通用性不强,开源意义不大),我们只能尝试挖掘相关信息,网上对它的完整介绍还是非常非常少,分析facebook的前端代码也没有太多收获,后来无意中发现了facebook使用的项目管理工具phabricator中的一个静态管理方案Celerity,以及相关的说明,看它的描述很像是Facebook静态资源管理系统的一个mini版!
简单看过整个系统之后发现原理并不复杂(小而美的典范),它是通过一个小工具扫描所有静态资源,生成一张资源表,然后有一个PHP实现的资源管理框架(Celerity)提供了资源加载接口,替代了传统的script/link等静态的资源加载标签,最终通过查表来加载资源。
虽然没有真正看过FB的那套系统,但眼前的这个小小的框架给了当时的我们足够多的启示:
多么优雅的实现啊!
资源表是一份数据文件(比如JSON),是项目中所有静态资源(主要是JS和CSS)的构建信息记录,通过构建工具扫描项目源码生成,是一种k-v结构的数据,以每个资源的id为key,记录了资源的类别、部署路径、依赖关系、打包合并等内容,比如:
而资源加载框架则提供一些资源引用的API,让开发者根据id来引用资源,替代静态的script/link标签来收集、去重、按需加载资源。调用这些接口时,框架通过查表来查找资源的各项信息,并递归查找其依赖的资源的信息,然后我们可以在这个过程中实现各种性能优化算法来“智能”加载资源。
根据业务场景的不同,加载框架可以在浏览器中用JS实现,也可以是后端模板引擎中用服务端语言实现,甚至二者的组合,不一而足。
这种设计很快被验证具有足够的灵活性,能够完美支撑不同团队不同技术规范下的性能优化需求,前面提到的按需加载、延迟加载、预加载、请求合并、文件指纹、CDN部署、Bigpipe、Quickling、BigRender、首屏CSS内嵌、HTTP 2.0服务端推送等等性能优化手段都可以很容易的在这种架构上实现,甚至可以根据性能日志自动进行优化(Facebook已实现)。
因为有了资源表,我们可以很方便的控制资源加载,通过各种手段在运行时计算页面的资源使用情况,从而获得最佳加载性能。无论是前端渲染的单页面应用,还是后端渲染的多页面应用,这种方法都同样适用。
此外,它还很巧妙的约束了构建工具的职责——只生成资源表。资源表是非常通用的数据结构,无论什么业务场景,其业务代码最终都可以被扫描为相同结构的表数据,并标记资源间的依赖关系,有了表之后我们只需根据不同的业务场景定制不同的资源加载框架就行了,从此彻底告别一个团队维护一套工具的时代!!!
深耕静态资源加载框架可以带来许多收益,而且有足够的灵活性和健壮性面向未来的技术变革,这个我们留作后话。
总结
回顾一下前面提到过的前端工程三个阶段:
现在补充上第四阶段:
由于先天缺陷,前端相比其他软件开发,在基础架构上更加迫切的需要组件化开发和资源管理,而解决资源管理的方法其实一点也不复杂:
近几年来各种你听到过的各种资源加载优化策略大部分都可以在这样一套基础上实现,而这种优化对于业务来说是完全透明的,不需要重构的性能优化——这不正是我们一直所期盼的吗?正如魏小亮博士所说:我们可以把优秀的人集中起来去优化加载。
如何选型技术、如何定制规范、如何分治系统、如何优化性能、如何加载资源,当你从切图开始转变为思考这些问题的时候,我想说:
你好,工程师!
相关文章:(注: 以下文章还在占坑中, 作者还未完成)