html5今日看点程序员

前端开发体系建设

2016-11-29  本文已影响445人  Www刘

前端集成解决方案要求:

百度出品的 fis 就是一个能帮助快速搭建前端集成解决方案的工具,不幸的是,现在fis官网所介绍的 并不是 fis,而是一个叫 fis-plus 的项目,该项目并不像字面理解的那样是fis的加强版,而是在fis的基础上定制的一套面向百度前端团队的解决方案,以php为后端语言,跟smarty有较强的绑定关系,有着 19项
技术要素,密切配合百度现行技术选型。绝大多数非百度前端团队都很难完整接受这19项技术选型,尤其是其中的部署、框架规范,跟百度前端团队相关开发规范、部署规范、以及php、smarty等有着较深的绑定关系。

因此如果你的团队用的不是 php后端 && smarty模板&& modjs模块化框架
&& bingo框架的话,请查看 fis的文档,或许不会有那么多困惑。

ps: fis是一个构建系统内核,很好的抽象了前端集成解决方案所需的通用工具需求,本身不与任何后端语言绑定。而基于fis实现的具体解决方案就会有具体的规范和技术选型了。

0. 开发概念定义

前端开发体系设计第一步要定义开发概念。开发概念是指针对开发资源的分类概念。开发概念的确立,直接影响到规范的定制。比如,传统的开发概念一般是按照文件类型划分的,所以传统前端项目会有这样的目录结构:

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属性值中引用分组是非常方便的。正则匹配 + 捕获分组,成为目录规范配置的强有力工具:


fis的配置系统非常灵活,除了 文档 中提到的配置节点,其他配置用户可以随便定义使用。比如配置的roadmap是系统保留的,而name、version都是用户自己随便指定的。fis系统保留的配置节点只有6个,包括:

project(系统配置)
modules(构建流程配置)
settings(插件配置)
roadmap(目录规范与域名配置)
deploy(部署目标配置)
pack(打包配置)

完成第一份配置之后,我们来看一下效果。

cd project
fis release --dest ../release

进入到项目目录,然后使用fis release命令,对项目进行构建,用 --dest <path>
参数指定编译结果的产出路径,可以看到部署后的结果:

=
我构造了一个相对完整的目录结构,然后进行了一次构建,效果还不错:

当执行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


我们准备一下项目代码,看看构建的时候发生了什么:

可以看到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-conf.js不能同步升级,这是非常严重的问题。grunt的gruntfile.js也是如此。如果说有一个项目用了某套grunt配置感觉很爽,那么下个项目也想用这套方案,复制gruntfile.js是必须的操作,项目用的多了,同步gruntfile的成本就变得非常高了。
因此,fis提供了一种“包装”的能力,它允许你将fis作为内核,包装出一个新的命令行工具,这个工具内置了一些fis的配置,并且把所有命令行调用的参数传递给fis内核去处理。
我准备把这套系统打包为一个新的工具,给它取名为 scrat,也是一只松鼠。这个新工具的目录结构是这样的:


将这个npm包发布出来,我们就有了一个全新的开发工具,这个工具可以解决前面说的13项技术问题,并提供一套完整的集成解决方案,而你的团队使用的时候,只有两个步骤:

使用新工具的命令、参数几乎和fis完全一样:

scrat release [options]
scrat server start
scrat install <name@version> [options]

而scrat这个工具所内置的配置将变成规范文档描述给团队同学,这套系统要比grunt那种松散的构建系统组成方式更容易被多个团队、多个项目同时共享。
总结
不可否认,为大规模前端团队设计集成解决方案需要花费非常多的心思。

如果说只是实现一个简单的编译+压缩+文件监+听自动刷新的常规构建系统,基于fis应该不超过1小时就能完成一个,但要实践完整的前端集成解决方案,确实需要点时间。
如之前一篇 文章 所讲,前端集成解决方案有8项技术要素,除了组件仓库,其他7项对于企业级前端团队来说,应该都需要完整实现的。即便暂时不想实现,也会随着业务发展而被迫慢慢完善,这个完善过程是普适的。
对于前端集成解决方案的实践,可以总结出这些设计步骤:

我们可以看看业界已有团队提出的各种解决方案,无不以这种思路来设计和发展的:

纵观这些公司出品的前端集成解决方案,深入剖析其中的框架、规范、工具和流程,都可以发现一些共通的影子,设计思想殊途同归,不约而同的朝着一种方向前进,那就是前端集成解决方案。尝试将前端工程孤立的技术要素整合起来,解决常见的领域问题。
或许有人会问,不就是写页面么,用得着这么复杂?

在这里我不能给出肯定或者否定的答复。
因为单纯从语言的角度来说,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三个目录,三个目录中都以子系统名称命名了一个目录,里面存放了静态资源、模板和资源表。

这样,我们把所有子系统都构建一遍部署在一起后,就得到了这样的一个完整的站点部署结果:



做到了以下几点:

当我们有了这样的开发和部署规范以后,我们才方便谈『如何跨子系统使用资源,去除构建依赖,并保证资源更新』。

这一切都是因为我们有资源表和基于表的资源管理框架!!!

由于每个子系统会产生一张表,记录了子系统内资源的依赖、打包(子系统内也可以实现资源合并打包,并记录在表)和发布信息,这样我们需要跨子系统调用资源的时候,就可以这样做(假设我们在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中;而每个子系统独立上线之后,会更新资源表,让新的部署立即生效,而不需要依赖构建解决资源更新问题。

@fouber,感谢原文作者

上一篇 下一篇

猜你喜欢

热点阅读