前端集成解决方案要求:
- 模块化开发。最好能像写nodejs一样写js,很舒服。css最好也能来个模块化管理!
- 性能要好。模块那么多,得有按需加载,请求不能太多。
- 组件化开发。一个组件的js、css、模板最好都在一个目录维护,维护起来方便。
- 用 handlebars 作为前端模板引擎。这个模板引擎不错,logic-less(轻逻辑)。
- 用 stylus 写css挺方便,给我整一个。
- 图片base64嵌入。有些小图可能要以base64的形式嵌入到页面、js或者css中使用。嵌入之前记得压缩图片以减小体积。
- js/css/图片压缩应该都没问题吧。
- 要能与公司的ci平台集。工具里最好别依赖什么系统库,ci的机器未必支持。
- 开发体验要好。文件监听,浏览器自动刷新(livereload)一个都不能少。
- 我们用nodejs作为服务器,本地要能预览,最好再能抓取线上数据,方便调试。
- 代码发布后能按照版本部署,不要彼此覆盖。
- 允许第三方引用项目公共模块
- 支持原有公用组件模块,采用component 作为发布规范
简单划分 - 规范
- 开发规范
- 模块化开发,js模块化,css模块化,像nodejs一样编码
- 组件化开发,js,css,handlebars维护在一起
- 部署规范
- 采用nodejs后端,基本部署规范应该参考 [express]
(http://expressjs.com/) 项目部署 - 按版本号做费覆盖式发布
- 公共模块可发布给第三方共享
- 采用nodejs后端,基本部署规范应该参考 [express]
- 开发规范
- 框架
- js模块化框架,支持请求合并,按需加载等性能优化点
- 工具
- 可以编译stylus为css
- 支持js、css、图片压缩
- 允许图片压缩后以base64编码形式嵌入到css、js或html中
- 与ci平台集成
- 文件监听、浏览器自动刷新
- 本地预览、数据模拟
- 仓库
- 支持component模块安装和使用
百度出品的 fis 就是一个能帮助快速搭建前端集成解决方案的工具,不幸的是,现在fis官网所介绍的 并不是 fis,而是一个叫 fis-plus 的项目,该项目并不像字面理解的那样是fis的加强版,而是在fis的基础上定制的一套面向百度前端团队的解决方案,以php为后端语言,跟smarty有较强的绑定关系,有着 19项
技术要素,密切配合百度现行技术选型。绝大多数非百度前端团队都很难完整接受这19项技术选型,尤其是其中的部署、框架规范,跟百度前端团队相关开发规范、部署规范、以及php、smarty等有着较深的绑定关系。
因此如果你的团队用的不是 php后端 && smarty模板&& modjs模块化框架
&& bingo框架的话,请查看 fis的文档,或许不会有那么多困惑。
ps: fis是一个构建系统内核,很好的抽象了前端集成解决方案所需的通用工具需求,本身不与任何后端语言绑定。而基于fis实现的具体解决方案就会有具体的规范和技术选型了。
0. 开发概念定义
前端开发体系设计第一步要定义开发概念。开发概念是指针对开发资源的分类概念。开发概念的确立,直接影响到规范的定制。比如,传统的开发概念一般是按照文件类型划分的,所以传统前端项目会有这样的目录结构:
- js:放js文件
- css:放css文件
- images:放图片文件
- html:放html文件
这样确实很直接,任何智力健全的人都知道每个文件该放在哪里。但是这样的开发概念划分将给项目带来较高的维护成本,并为项目臃肿埋下了工程隐患,理由是:
如果项目中的一个功能有了问题,维护的时候要在js目录下找到对应的逻辑修改,再到css目录下找到对应的样式文件修改一下,如果图片不对,还要再跑到images目录下找对应的开发资源。
images下的文件不知道哪些图片在用,哪些已经废弃了,谁也不敢删除,文件越来越多。
ps: 除非你的团队只有1-2个人,你的项目只有很少的代码量,而且不用关心性能和未来的维护问题,否则,以文件为依据设计的开发概念是应该被抛弃的。
以我个人的经验,更倾向于具有一定语义的开发概念。综合前面的需求,我为这个开发体系确定了3个开发资源概念: - 模块化资源:js模块、css模块或组件
- 页面资源:网站html或后端模板页面,引用模块化框架,加载模块
- 非模块化资源:并不是所有的开发资源都是模块化的,比如提供模块化框架的js本身就不能是一个模块化的js文件。严格上讲,页面也属于一种非模块化的静态资源。
ps: 开发概念越简单越好,前面提到的fis-plus也有类似的开发概念,有组件或模块(widget),页面(page),测试数据(test),非模块化静态资源(static)。有的团队在模块之中又划分出api模块和ui模块(组件)两种概念
1. 开发目录设计
基于开发概念的确立,接下来就要确定目录规范了。我通常会给每种开发资源的目录取一个有语义的名字,三种资源我们可以按照概念直接定义目录结构为:
project
- modules 存放模块化资源
- pages 存放页面资源
- static 存放非模块化资源
这样划分目录确实直观,但希望能使用component仓库资源,因此我决定将模块化资源目录命名为components,得到:
project
- components 存放模块化资源
- pages 存放页面资源
- static 存放非模块化资源
模块资源分为项目模块和公共模块,以及hinc提到过希望能从component安装一些公共组件到项目中,因此,一个components目录还不够,想到nodejs用node_modules作为模块安装目录,因此我在规范中又追加了一个 component_modules 目录,得到:
project
- component_modules 存放外部模块资源
- components 存放项目模块资源
- pages 存放页面资源
- static 存放非模块化资源
大多数项目采用nodejs作为后端,express是比较常用的nodejs的server框架,express项目通常会把后端模板放到 views 目录下,把静态资源放到 public下。为了迎合这样的需求,我将page、static两个目录调整为 views
和 public,规范又修改为:
project
- component_modules 存放外部模块资源
- components 存放项目模块资源
- views 存放页面资源
- public 存放非模块化资源
考虑到页面也是一种静态资源,而public这个名字不具有语义性,与其他目录都有概念冲突,不如将其与views目录合并,views目录负责存放页面和非模块化资源比较合适,因此最终得到的开发目录结构为:
project
- component_modules 存放外部模块资源
- components 存放项目模块资源
- views 存放页面以及非模块化资源
2. 部署目录设计
一个完整的部署结果应该是这样的目录结构:
由于还要部署一些可以被第三方使用的模块,public下只有项目名的部署还不够,应改把模块化文件单独发布出来,得到这样的部署结构:
由于 component_modules 这个名字太长了,如果部署到这样的路径下,url会很长,这也是一个优化点,因此最终决定部署结构为:
插一句,并不是所有团队都会有这么复杂的部署要求,这和松鼠团队的业务需求有关,但我相信这个例子也不会是最复杂的。每个团队都会有自己的运维需求,前端资源部署经常牵连到公司技术架构,因此很多前端项目的开发目录结构会和部署要求保持一致。这也为项目间模块的复用带来了成本,因为代码中写的url通常是部署后的路径,迁移之后就可能失效了。
解耦开发规范和部署规范是前端开发体系的设计重点。
3. 配置fis连接开发规范和部署规范
样例项目:
project
- views
- logo.png
- index.html
- fis-conf.js
- README.md
fis-conf.js是fis工具的配置文件,接下来我们就要在这里进行构建配置了。虽然开发规范和部署规范十分复杂,但好在fis有一个非常强大的 roadmap.path 功能,专门用于分类文件、调整发布结构、指定文件的各种属性等功能实现。
所谓构建,其核心任务就是将文件按照某种规则进行分类(以文件后缀分类,以模块化/非模块化分类,以前端/后端代码分类),然后针对不同的文件做不同的构建处理。
闲话少说,我们先来看一下基本的配置,在 fis-conf.js 中添加代码:
fis.config.set('roadmap.path', [ {
reg : '**.md', //所有md后缀的文件
release : false //不发布
}
]);
以上配置,使得项目中的所有md后缀文件都不会发布出来。release是定义file对象发布路径的属性,如果file对象的release属性为false,那么在项目发布阶段就不会被输出出来。
在fis中,roadmap.pah是一个数组数据,数组每个元素是一个对象,必须定义 reg
属性,用以匹配项目文件路径从而进行分类划分,reg属性的取值可以是路径通配字符串或者正则表达式。fis有一个内部的文件系统,会给每个源码文件创建一个 fis.File 对象,创建File对象时,按照roadmap.path的配置逐个匹配文件路径,匹配成功则把除reg之外的其他属性赋给File对象,fis中各种处理环节及插件都会读取所需的文件对象的属性值,而不会自己定义规范。有关roadmap.path的工作原理可以看这里 以及 这里。ok,让md文件不发布很简单,那么views目录下的按版本发布要求怎么实现呢?其实也是非常简单的配置:
fis.config.set('roadmap.path', [
{
reg : '**.md', //所有md后缀的文件
release : false //不发布
},
{
//正则匹配【/views/**】文件,并将views后面的路径捕获为分组1
reg : /^\/views\/(.*)$/i, //发布到 public/proj/1.0.0/分组1 路径下
release : '/public/proj/1.0.0/$1'
}
]);
roadmap.path数组的第二元素据采用正则作为匹配规则,正则可以帮我们捕获到分组信息,在release属性值中引用分组是非常方便的。正则匹配 + 捕获分组,成为目录规范配置的强有力工具:
在上面的配置中,版本号被写到了匹配规则里,这样非常不方便工程师在迭代的过程中升级项目版本。我们应该将版本号、项目名称等配置独立出来管理。好在roadmap.path还有读取其他配置的能力,修改上面的配置,我们得到:
fis的配置系统非常灵活,除了 文档 中提到的配置节点,其他配置用户可以随便定义使用。比如配置的roadmap是系统保留的,而name、version都是用户自己随便指定的。fis系统保留的配置节点只有6个,包括:
project(系统配置)
modules(构建流程配置)
settings(插件配置)
roadmap(目录规范与域名配置)
deploy(部署目标配置)
pack(打包配置)
完成第一份配置之后,我们来看一下效果。
cd project
fis release --dest ../release
进入到项目目录,然后使用fis release命令,对项目进行构建,用 --dest <path>
参数指定编译结果的产出路径,可以看到部署后的结果:
ps: fis release会将处理后的结果发布到源码目录之外的其他目录里,以保持源码目录的干净。
fis系统的强大之处在于当你调整了部署规范之后,fis会识别所有资源定位标记,将他们修改为对应的部署路径。
fis的文件系统设计决定了配置开发规范的成本非常低。fis构建核心有三个超级正则,用于识别资源定位标记,把用户的开发规范和部署规范通过配置完整连接起来,具体实现可以看这里。
不止html,fis为前端三种领域语言都准备了资源定位识别标记,更多文档可以看这里:在html中定位资源,在js中定位资源,在css中定位资源
接下来,我们修改一下项目版本配置,再发布一下看看效果:
fis.config.set('version', '1.0.1');
再次执行:
cd projectfis release --dest ../release
至此,我们已经基本解决了开发和部署直接的目录规范问题,这里我需要加快一些步伐,把其他目录的部署规范也配置好,得到一个相对比较完整的结果:
我构造了一个相对完整的目录结构,然后进行了一次构建,效果还不错:
不管部署规则多么复杂都不用担心,有fis强大的资源定位系统帮你在开发规范和部署规范之间建立联系,设计开发体系不在受制于工具的实现能力。
你可以尽情发挥想象力,设计出最优雅最合理的开发规范和最高效最贴合公司运维要求的部署规范,最终用fis的roadmap.path功能将它们连接起来,实现完美转换。
fis的roadmap功能实际上提供了项目代码与部署规范解耦的能力。
从前面的例子可以看出,开发使用相对路径即可,fis会在构建时会根据fis-conf.js中的配置完成开发路径到部署路径的转换工作。这意味着在fis体系下开发的模块将具有天然的可移植性,既能满足不同项目的不同部署需求,又能允许开发中使用相对路径进行资源定位,工程师再不用把部署路径写到代码中了。
模块化框架###
模块化框架是前端开发体系中最为核心的环节。
模块化框架肩负着模块管理、资源加载、性能优化(按需,请求合并)等多种重要职责,同时它也是组件开发的基础框架,因此模块化框架设计的好坏直接影响到开发体系的设计质量。
很遗憾的说,现在市面上已有的模块化框架都没能很好的处理模块管理、资源加载和性能优化三者之间的关系。这倒不是框架设计的问题,而是由前端领域语言特殊性决定的。框架设计者一般在思考模块化框架时,通常站在纯前端运行环境角度考虑,基本功能都是用原生js实现的,因此一个模块化开发的关键问题不能被很好的解决。这个关键问题就是依赖声明。
以 seajs 为例(无意冒犯),seajs采用运行时分析的方式实现依赖声明识别,并根据依赖关系做进一步的模块加载。比如如下代码:
define(function(require) {
var foo = require("foo"); //...
});
当seajs要执行一个模块的factory函数之前,会先分析函数体中的require
书写,具体代码在这里和这里,大概的代码逻辑如下:
Module.define = function (id, deps, factory) {
... //抽取函数体的字符串内容
var code = factory.toString(); //设计一个正则,分析require语句
var reg = /\brequire\s*\(([.*]?)\)/g;
var deps = []; //扫描字符串,得到require所声明的依赖
code.replace(reg, function(m, $1){
deps.push($1);
});
//加载依赖,完成后再执行factory ...
};
由于框架设计是在“纯前端实现”的约束条件下,使得模块化框架对于依赖的分析必须在模块资源加载完成之后才能做出识别。这将引起两个性能相关的问题:
1.require被强制为关键字而不能被压缩。否则factory.toString()的分析将没有意义。
2.依赖加载只能串行进行,当一个模块加载完成之后才知道后面的依赖关系。
第一个问题还好,尤其是在gzip下差不多多少字节,但是要配置js压缩器保留require函数不压缩。第二个问题就比较麻烦了,虽然seajs有seajs-combo插件可以一定程度上减少请求,但仍然不能很好的解决这个问题。举个例子,有如下seajs模块依赖关系树:
ps: 图片来源 @raphealguo
采用seajs-combo插件之后,静态资源请求的效果是这样的:
http://www.example.com/page.js
http://www.example.com/a.js,b.js
http://www.example.com/c.js,d.js,e.js
http://www.example.com/f.js
工作过程是
- 框架先请求了入口文件page.js
- 加载完成后分析factory函数,发现依赖了a.js和b.js,然后发起一个combo请求,加载a.js和b.js
- a.js和b.js加载完成后又进一步分析源码,才发现还依赖了c.js、d.js和e.js,再发起请求加载这三个文件
- 完成c、d、e的加载之后,再分析,发现f.js依赖,再请求
- 完成f.js的请求,page.js的依赖全部满足,执行它的factory函数。
虽然combo可以在依赖层级上进行合并,但完成page.js的请求仍需要4个。很多团队在使用seajs的时候,为了避免这样的串行依赖请求问题,会自己实现打包方案,将所有文件直接打包在一起,放弃了模块化的按需加载能力,也是一种无奈之举。
原因很简单以纯前端方式来实现模块依赖加载不能同时解决性能优化问题。
归根结底,这样的结论是由前端领域语言的特点决定的。前端语言缺少三种编译能力,前面讲目录规范和部署规范时其实已经提到了一种能力,就是“资源定位的能力”,让工程师使用开发路径定位资源,编译后可转换为部署路径。其他语言编写的程序几乎都没有web这种物理上分离的资源部署策略,而且大多具都有类似'getResource(path)'这样的函数,用于在运行环境下定位当初的开发资源,这样不管项目怎么部署,只要getResource函数运行正常就行了。可惜前端语言没有这样的资源定位接口,只有url这样的资源定位符,它指向的其实并不是开发路径,而是部署路径。
这里可以简单列举出前端语言缺少三种的语言能力:
**
资源定位的能力:使用开发路径进行资源定位,项目发布后转换成部署路径
依赖声明的能力:声明一个资源依赖另一个资源的能力
资源嵌入的能力:把一个资源的编译内容嵌入到另一个文件中
**
以后我会在完善前端开发体系理论的时候在详细介绍这三种语言能力的必要性和原子性,这里就暂时不展开说明了。
fis最核心的编译思想就是围绕这三种语言能力设计的。
要兼顾性能的同时解决模块化依赖管理和加载问题,其关键点在于
不能运行时去分析模块间的依赖关系,而要让框架提前知道依赖树。
了解了原因,我们就要自己动手设计模块化框架了。不要害怕,模块化框架其实很简单,思想、规范都是经过很多前辈总结的结果,我们只要遵从他们的设计思想去实现就好了。
参照已有规范,我定义了三个模块化框架接口:
**
模块定义接口:define(id, factory);
异步加载接口:require.async(ids, callback);
框架配置接口:require.config(options);
**
利用构建工具建立模块依赖关系表,再将关系表注入到代码中,调用require.config接口让框架知道完整的依赖树,从而实现require.async在异步加载模块时能提前预知所有依赖的资源,一次性请求回来。
以上面的page.js依赖树为例,构建工具会生成如下代码:
当执行require.async('page.js', fn);语句时,框架查询config.deps表,就能知道要发起一个这样的combo请求:
http://www.example.com/f.js,c.js,d.js,e.js,a.js,b.js,page.js
从而实现按需加载和请求合并两项性能优化需求。
根据这样的设计思路,我请 @hinc 帮忙实现了这个框架,我告诉他,deps里不但会有js,还会有css,所以也要兼容一下。hinc果然是执行能力非常强的小伙伴,仅一个下午的时间就搞定了框架的实现,我们给这个框架取名为 scrat.js,仅有393行。
前面提到fis具有资源依赖声明的编译能力。因此只要工程师按照fis规定的书写方式在代码中声明依赖关系,就能在构建的最后阶段自动获得fis系统整理好的依赖树,然后对依赖的数据结构进行调整、输出,满足框架要求就搞定了!fis规定的资源依赖声明方式为:在html中声明依赖,在js中声明依赖,在css中声明依赖。
接下来,我要写一个配置,将依赖关系表注入到代码中。fis构建是分流程的,具体构建流程可以看这里。fis会在postpackager阶段之前创建好完整的依赖树表,我就在这个时候写一个插件来处理即可。编辑fis-conf.js
我们准备一下项目代码,看看构建的时候发生了什么:
执行fis release查看命令行输出,可以看到consolog.log的内容为:
可以看到js和同名的css自动建立了依赖关系,这是fis默认进行的依赖声明。有了这个表,我们就可以把它注入到代码中了。我们为页面准备一个替换用的钩子,比如约定为FRAMEWORK_CONFIG
,这样用户就可以根据需要在合适的地方获取并使用这些数据。模块化框架的配置一般都是写在非模块化文件中的,比如html页面里,所以我们应该只针对views目录下的文件做这样的替换就可以。所以我们需要给views下的文件进行一个标记,只有views下的html或js文件才需要进行依赖树数据注入,具体的配置为:
我在views/index.html中写了这样的代码:
执行 fis release -d ../release之后,得到构建后的内容为:
在调用 require.async('components/foo/foo.js')
之际,模块化框架已经知道了这个foo.js依赖于bar.js、bar.css以及foo.css,因此可以发起两个combo请求去加载所有依赖的js、css文件,完成后再执行回调。
现在模块的id有一些问题,因为模块发布会有版本号信息,因此模块id也应该携带版本信息,从前面的依赖树生成配置代码中我们可以看到模块id其实也是文件的一个属性,因此我们可以在roadmap.path中重新为文件赋予id属性,使其携带版本信息:
重新构建项目,我们得到了新的结果:
所有id都会被修改为我们指定的模式,这就是以文件为中心的编译系统的威力。
以文件对象为中心构建系统应该通过配置指定文件的各种属性。插件并不自己实现某种规范规定,而是读取file对象的对应属性值,这样插件的职责单一,规范又能统一起来被用户指定,为完整的前端开发体系设计奠定了坚实规范配置的基础。
接下来还有一个问题,就是模块名太长,开发中写这么长的模块名非常麻烦。我们可以借鉴流行的模块化框架中常用的缩短模块名手段——别名(alias)——来降低开发中模块引用的成本。此外,目前的配置其实会针对所有文件生成依赖关系表,我们的开发概念定义只有components和component_modules目录下的文件才是模块化的,因此我们可以进一步的对文件进行分类,得到这样配置规范:
然后我们为一些模块id建立别名:
再次构建,在注入的代码中就能看到alias字段了:
这样,代码中的 require('foo');就等价于 require('proj/1.0.5/foo/foo.js')了。
还剩最后一个小小的需求,就是希望能像写nodejs一样开发js模块,也就是要求实现define的自动包裹功能,这个可以通过文件编译的 postprocessor 插件完成。配置为:
所有在components目录和component_modules目录下的js文件都会被包裹define,并自动根据roadmap.path中的id配置进行模块定义了。
回顾
剩下的几个需求中有些是fis默认支持的,比如base64内嵌功能,图片会先经过编译流程,得到压缩后的内容fis再对其进行base64化的内嵌处理。由于fis的内嵌功能支持任意文件的内嵌,所以,这个语言能力扩展可以同时解决前端模板和图片base64内嵌需求,比如我们有这样的代码:
project
- components
- foo
- foo.js
- foo.css
- foo.handlebars
- foo.png
无需配置,既可以在js中嵌入资源,比如 foo.js 中可以这样写:
//依赖声明
var bar = require('../bar/bar.js');
//把handlebars文件的字符串形式嵌入到js中
var text = __inline('foo.handlebars');
var tpl = Handlebars.compile(text);
exports.render = function(data){
return tpl(data);
};
//把图片的base64嵌入到js中
var data = __inline('foo.png');
exports.getImage = function(){
var img = new Image();
img.src = data;
return img;
};
编译后得到:
支持stylus也非常简单,fis在 parser 阶段处理非标准语言,这个阶段可以把非标准的js(coffee/前端模板)、css(less/sass/stylus)、html(markdown)语言转换为标准的js、css或html。处理之后那些文件还能和标准语言一起经历预处理、语言能力扩展、后处理、校验、测试、压缩等阶段。
所以,要支持stylus的编译,只要在fis-conf.js中添加这样的配置即可:
这样我们项目中的*.styl后缀的文件都会被编译为css内容,并且会在后面的流程中被当做css内容处理,比如压缩、csssprite等。
文件监听、自动刷新都是fis内置的功能,fis的release命令集合了所有编译所需的参数,
这些参数是可以随意组合的,比如我们想文件监听、自动刷新,则使用:
fis release -wL
压缩、打包、文件监听、自动刷新、发布到output目录,则使用:
fis release -opwLd output
构建工具不需要那么多命令,或者develop、release等不同状态的配置文件,应该从命令行切换编译参数,从而实现开发/上线构建模式的切换。
另外,fis是命令行工具,各种内置的插件也是完全独立无环境依赖的,可以与ci平台直接对接,并在各个主流操作系统下运行正常。
利用fis的内置的各种编译功能,我们离目标又近了许多:
剩下两个,我们可以通过扩展fis的命令行插件来实现。fis有11个编译流程扩展点,还有一个命令行扩展点。要扩展命令行插件很简单,只要我们将插件安装到与fis同级的node_modules目录下即可。比如:
node_modules - fis - fis-command-say
那么执行 fis say这个命令,就能调用到那个fis-command-say插件了。剩下的这个component模块安装,我就利用了这个扩展点,结合component开源的 component-installer 包,我就可以把component整合当前开发体系中,这里我们需要创建一个npm包来提供扩展,而不能直接在fis-conf.js中扩展命令行,插件代码我就不贴了,可以看 这里。
眼前我们有了一个差不多100行的fis-conf.js文件,还有几个插件,如果我把这样一个零散的系统交付团队使用,那么大家使用的步骤差不多是这样的:
安装fis,npm install -g fis
安装component安装用的命令行插件,npm insatll -g fis-command-component
安装stylus编译插件,npm install -g fis-parser-stylus
下载一份配置文件,fis-conf.js,修改里面的name、version配置
这种情况让团队用起来会有很多问题。首先,安装过程太过麻烦,其次如果项目多,那么fis-conf.js不能同步升级,这是非常严重的问题。grunt的gruntfile.js也是如此。如果说有一个项目用了某套grunt配置感觉很爽,那么下个项目也想用这套方案,复制gruntfile.js是必须的操作,项目用的多了,同步gruntfile的成本就变得非常高了。
因此,fis提供了一种“包装”的能力,它允许你将fis作为内核,包装出一个新的命令行工具,这个工具内置了一些fis的配置,并且把所有命令行调用的参数传递给fis内核去处理。
我准备把这套系统打包为一个新的工具,给它取名为 scrat,也是一只松鼠。这个新工具的目录结构是这样的:
将这个npm包发布出来,我们就有了一个全新的开发工具,这个工具可以解决前面说的13项技术问题,并提供一套完整的集成解决方案,而你的团队使用的时候,只有两个步骤:
- 安装这个工具,npm install -g scrat
- 项目配置只有两项,name和version
使用新工具的命令、参数几乎和fis完全一样:
scrat release [options]
scrat server start
scrat install <name@version> [options]
而scrat这个工具所内置的配置将变成规范文档描述给团队同学,这套系统要比grunt那种松散的构建系统组成方式更容易被多个团队、多个项目同时共享。
总结
不可否认,为大规模前端团队设计集成解决方案需要花费非常多的心思。
如果说只是实现一个简单的编译+压缩+文件监+听自动刷新的常规构建系统,基于fis应该不超过1小时就能完成一个,但要实践完整的前端集成解决方案,确实需要点时间。
如之前一篇 文章 所讲,前端集成解决方案有8项技术要素,除了组件仓库,其他7项对于企业级前端团队来说,应该都需要完整实现的。即便暂时不想实现,也会随着业务发展而被迫慢慢完善,这个完善过程是普适的。
对于前端集成解决方案的实践,可以总结出这些设计步骤:
- 设计开发概念,定义开发资源的分类(模块化/非模块化)
- 设计开发目录,降低开发、维护成本(开发规范)
- 根据运维和业务要求,设计部署规范(部署规范)
- 设计工具,完成开发目录和部署目录的转换(开发-部署转换)
- 设计模块化框架,兼顾性能优化(开发框架)
- 扩展工具,支持开发框架的构建需求(框架构建需求)
- 流程整合(开发、测试、联调、上线等流程接入)
我们可以看看业界已有团队提出的各种解决方案,无不以这种思路来设计和发展的:
- seajs开发体系,支付宝团队前端开发体系,以 spm 为构建和包管理工具
- fis-plus,百度绝大多数前端团队使用的开发体系,以fis为构建工具内核,以lights为包管理工具
- edp,百度ecomfe前端开发体系,以 edp 为构建和包管理工具
- modjs,腾讯AlloyTeam团队出品的开发体系
- yeoman,google出品的解决方案,以grunt为构建工具,bower为包管理工具
纵观这些公司出品的前端集成解决方案,深入剖析其中的框架、规范、工具和流程,都可以发现一些共通的影子,设计思想殊途同归,不约而同的朝着一种方向前进,那就是前端集成解决方案。尝试将前端工程孤立的技术要素整合起来,解决常见的领域问题。
或许有人会问,不就是写页面么,用得着这么复杂?
在这里我不能给出肯定或者否定的答复。
因为单纯从语言的角度来说,html、js、css(甚至有人认为css是数据结构,而非语言)确实是最简单最容易上手的开发语言,不用模块化、不用工具、不用压缩,任何人都可以快速上手,完成一两个功能简单的页面。所以说,在一般情况下,前端开发非常简单。
在规模很小的项目中,前端技术要素彼此不会直接产生影响,因此无需集成解决方案。
但正是由于前端语言这种灵活松散的特点,使得前端项目规模在达到一定规模后,工程问题凸显,成为发展瓶颈,各种技术要素彼此之间开始出现关联,要用模块化开发,就必须对应某个模块化框架,用这个框架就必须对应某个构建工具,要用这个工具,就必须对应某个包管理工具……这个时候,完整实践前端集成解决方案就成了不二选择。
当前端项目达到一定规模后,工程问题将成为主要瓶颈,原来孤立的技术要素开始彼此产生影响,需要有人从比较高的角度去梳理、寻找适合自己团队的集成解决方案。
所以会出现一些框架或工具在小项目中使用的好好的,一旦放到团队里使用就非常困难的情况。
不知道fouber是怎么处理业务模块与公共模块的。比如5位开发承担了站点的不同模块开发(可以理解为5个页面)。页面都调用了公共的登录模块,发布的时候是独自发布登录的模块呢?还是用一个公共的页面片(类似apache的include)。如果是独立发布,那登录模块改版,就涉及到5个人都要发。如果是用公共的页面片,看你的文章也没有体现出来。处理公共的模块与业务,一直觉得很头疼。编写上还好说,发布最头疼,除了要考虑同事间的协作,还得考虑性能
对于大型网站的维护,你这个需求其实很常见,而且也很迫切——我们希望系统由多个独立的子系统组织形成,由多个团队维护,每个团队的代码不用与其他团队的业务代码产生构建依赖,均能独立构建独立上线。
这些需求,也是通过基于表的静态资源管理实现的。在百度的做法:
1. 目录结构规划
site是站点目录,common、user、news、photo都是一个 子系统
(为例避免混淆,我们并不称它为模块,而是叫做子系统),每个这样的子系统就是一个 git/svn 仓库,由一个独立的团队维护,最终将所有子系统构建之后部署在一起,就得到了完整的站点。
以common子系统为例,其内部目录结构大致为:
每个子系统都有一个 fis-conf.js
是我们的构建工具配置,你可以把它理解成是 gulpfile/gruntfile,widget放的是UI组件或js/css模块,page放的是页面,lib放的是第三方类库以及一些非模块化资源。
每个子系统构建之后,会产生一张资源表,比如上面这个common子系统,构建之后会得到一个 common-map.json的文件,这个文件中记录了common子系统中所有js/css/模板的部署路径和依赖关系,我在其他文章中也时常介绍过。
然后,给你展示一下我们这个common子系统构建之后的目录结构:
每个子系统经过构建之后都会得到 static、template、map三个目录,三个目录中都以子系统名称命名了一个目录,里面存放了静态资源、模板和资源表。
这样,我们把所有子系统都构建一遍部署在一起后,就得到了这样的一个完整的站点部署结果:
做到了以下几点:
- 子系统的部署相互之间是不覆盖的
- 每个子系统都会发布产生static、template、map三个目录,其中static用于静态资源部署,可以单独拿出来部署到CDN源服务器,template和map要一起部署到webserver
当我们有了这样的开发和部署规范以后,我们才方便谈『如何跨子系统使用资源,去除构建依赖,并保证资源更新』。
这一切都是因为我们有资源表和基于表的资源管理框架!!!
由于每个子系统会产生一张表,记录了子系统内资源的依赖、打包(子系统内也可以实现资源合并打包,并记录在表)和发布信息,这样我们需要跨子系统调用资源的时候,就可以这样做(假设我们在photo子系统中需要使用common子系统和user子系统的资源):
在photo子系统的js文件中跨子系统依赖资源:
/** * 系统:photo *
文件:widget/header/header.js
*/
var $ = require('common:lib/jquery');
var login = require('user:widget/login');...
在photo子系统的css文件中跨子系统依赖资源(@require是fis自定义的依赖声明标识,用于构建工具识别):
/** * 系统:photo * 文件:widget/footer/footer.css * * @require common:lib/font-awesome * */.footer .fa { font-size: 12px;}
在photo子系统的后端模板文件中跨子系统调用组件(widget是扩展的模板引擎功能,类似include):
{*
系统:photo
文件:page/index/index.tpl*
}
<div class="photo-page-index">
<div class="photo-page-index_header">
{% widget id="common:widget/header" %}
</div>
<div class="photo-page-index_user-info">
{% widget id="user:widget/info" %}
</div>
</div>
我们修改了一下id的规范,在前面加上了 子系统名:
这样的标识,表示是跨子系统的资源调用,比如需要common子系统的header组件,其id就是 common:widget/header
。
代码设计成这样之后,最后的关键就是写资源管理框架了。
因为资源表中记录了资源的依赖关系,资源依赖关系会带上子系统的名称前缀,所以,资源管理框架只需根据这个id去不同的map文件中查找资源即可,一般对于这样的大型系统,我们都是把资源管理框架实现为服务端模板引擎的扩展,可以减少前端的负担,有关这个资源管理框架的实现思路介绍,我其实还没有完整发文介绍过,也是近期在酝酿的事情,并不复杂,只是要在这里全部展开多少有点不合适。
有了资源表、跨子系统资源依赖声明规范、资源管理框架和一个简单的构建工具之后,就完成你问题的要求了,构建依赖被消除的原因是资源管理框架让依赖变成了运行时的。模板在服务端拼装的过程中,资源管理框架实时读取map目录下的资源表,跨子系统找到资源的发布路径、打包合并情况,然后动态生成资源的加载代码,插入到html中;而每个子系统独立上线之后,会更新资源表,让新的部署立即生效,而不需要依赖构建解决资源更新问题。