【转】前端开发体系建设日记

原文转自:http://www.infoq.com/cn/articles/talk-front-end-integrated-solution-part2javascript

InfoQ编辑注:本文来自前端工程师张云龙的博客,由做者本人推荐至InfoQ进行分享。目前本系列已经发布了三个部分,本处分享的是第二部分,前端开发体系建设日记。建议在阅读本文前先阅读本文做者和其团队以前分享的《前端工程精粹》系列php


上周写了一篇 文章 介绍前端集成解决方案的基本理论,不少同窗看过以后大呼不过瘾。css

干货 fuck things 在哪里!html

 

本打算继续完善理论链,造成前端工程的知识结构。但鉴于现在的快餐文化,po主决定仍是先写一篇实战介绍,让你们看到前端工程体系能为团队带来哪些好处,调起你们的胃口再说。前端

ps: 写完才发现这篇文章真的很是很是长,涵盖了前端开发中的不少方面,但愿你们能有耐心看完,相信必定会有所斩获。。。java

2014年02月12日 - 晴

新到松鼠团队的次日,小伙伴 @nino 找到我说node

nino: 视频项目打算从新梳理一下,但愿能引入新的技术体系,解决现有的一些问题。

po主不由暗喜,好机会,这是我专业啊,蓝翔技校-前端集成解决方案学院-自动化系-打包学专业的文凭不是白给的,因而自信满满的对nino说,有什么需求尽管提!

nino: 个人需求并很少,就这么几条~~

  1. 模块化开发。最好能像写nodejs同样写js,很舒服。css最好也能来个模块化管理!
  2. 性能要好。模块那么多,得有按需加载,请求不能太多。
  3. 组件化开发。一个组件的js、css、模板最好都在一个目录维护,维护起来方便。
  4. 用 handlebars 做为前端模板引擎。这个模板引擎不错,logic-less(轻逻辑)。
  5. 用 stylus 写css挺方便,给我整一个。
  6. 图片base64嵌入。有些小图可能要以base64的形式嵌入到页面、js或者css中使用。嵌入以前记得压缩图片以减少体积。
  7. js/css/图片压缩应该都没问题吧。
  8. 要能与公司的ci平台集。工具里最好别依赖什么系统库,ci的机器未必支持。
  9. 开发体验要好。文件监听,浏览器自动刷新(livereload)一个都不能少。
  10. 咱们用nodejs做为服务器,本地要能预览,最好再能抓取线上数据,方便调试。

我倒吸一口凉气,但表面故做镇定的说:恩,确实很少,让咱们先来看看第一个需求。。。

还没等我说完,nino打断我说

nino: 桥豆麻袋(稍等),还有一个最重要的需求!

松鼠公司的松鼠浏览器你知道吧,恩,它有不少个版本的样子。
我但愿代码发布后能按照版本部署,不要彼此覆盖。

举个例子,代码部署结构多是这样的:

  release/
    - public/
      - 项目名
        - 1.0.0/
        - 1.0.1/
        - 1.0.2/
        - 1.0.2-alpha/
        - 1.0.2-beta/

让历史浏览器浏览历史版本,没事还能作个灰度发布,ABTest啥的,多好!

此外,咱们未来会有多个项目使用这套开发模式,但愿能共用一些组件或者模
块,产品也会公布一些api模块给第三方使用,因此共享模块功能也要加上。

总的来讲,还要追加两个部署需求:

  1. 按版本部署,采用非覆盖式发布
  2. 容许第三方引用项目公共模块

nino: 怎么样,不算复杂吧,这个项目很赶,3天搞定怎么样?

我凝望着会议室白板上的这些需求,正打算争辩什么,一扭头发现nino已经不见了。。。正在沮丧之际,小伙伴 @hinc 过来找我,跟他大概讲了一下nino的需求,正想跟他抱怨工期问题时,hinc却说

hinc: 恩,这正是咱们须要的开发体系,不过我这里还有一个需求。。。

  1. 咱们以前积累了一些业务能够共用的模块,放在了公司内的gitlab上,采用 component做为发布规范,能不能再加上这个组件仓库的支持?

3天时间,13项前端技术元素,靠谱么。。。

2014年02月13日 - 多云

一觉醒来,轻松了许多,但还有任务在身,不敢有半点怠慢。整理一下昨天的需求,咱们来作一个简单的划分。

  • 规范
    • 开发规范
      • 模块化开发,js模块化,css模块化,像nodejs同样编码
      • 组件化开发,js、css、handlebars维护在一块儿
    • 部署规范
      • 采用nodejs后端,基本部署规范应该参考 express 项目部署
      • 按版本号作非覆盖式发布
      • 公共模块可发布给第三方共享
  • 框架
    • js模块化框架,支持请求合并,按需加载等性能优化点
  • 工具
    • 能够编译stylus为css
    • 支持js、css、图片压缩
    • 容许图片压缩后以base64编码形式嵌入到css、js或html中
    • 与ci平台集成
    • 文件监听、浏览器自动刷新
    • 本地预览、数据模拟
  • 仓库
    • 支持component模块安装和使用

这样一套规范、框架、工具和仓库的开发体系,服从我以前介绍的 前端集成解决方案 的描述。前端界天天都团队在设计和实现这类系统,它们实际上是有规律可循的。百度出品的 fis 就是一个能帮助快速搭建前端集成解决方案的工具。使用fis我应该能够在3天以内完成这些任务。

ps: 这不是一篇关于fis的软文,若是这样的一套系统基于grunt实现相信会有很是大量的开发工做,3天完成几乎是不可能的任务。

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

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

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

言归正传,让咱们基于 fis 开始实践这套开发体系吧!

0. 开发概念定义

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

  • js:放js文件
  • css:放css文件
  • images:放图片文件
  • html:放html文件

这样确实很直接,任何智力健全的人都知道每一个文件该放在哪里。可是这样的开发概念划分将给项目带来较高的维护成本,并为项目臃肿埋下了工程隐患,理由是:

  1. 若是项目中的一个功能有了问题,维护的时候要在js目录下找到对应的逻辑修改,再到css目录下找到对应的样式文件修改一下,若是图片不对,还要再跑到images目录下找对应的开发资源。
  2. images下的文件不知道哪些图片在用,哪些已经废弃了,谁也不敢删除,文件愈来愈多。

ps: 除非你的团队只有1-2我的,你的项目只有不多的代码量,并且不用关心性能和将来的维护问题,不然,以文件为依据设计的开发概念是应该被抛弃的。

以我我的的经验,更倾向于具备必定语义的开发概念。综合前面的需求,我为这个开发体系肯定了3个开发资源概念:

  • 模块化资源:js模块、css模块或组件
  • 页面资源:网站html或后端模板页面,引用模块化框架,加载模块
  • 非模块化资源:并非全部的开发资源都是模块化的,好比提供模块化框架的js自己就不能是一个模块化的js文件。严格上讲,页面也属于一种非模块化的静态资源。

ps: 开发概念越简单越好,前面提到的fis-plus也有相似的开发概念,有组件或模块(widget),页面(page),测试数据(test),非模块化静态资源(static)。有的团队在模块之中又划分出api模块和ui模块(组件)两种概念。

1. 开发目录设计

基于开发概念的确立,接下来就要肯定目录规范了。我一般会给每种开发资源的目录取一个有语义的名字,三种资源咱们能够按照概念直接定义目录结构为:

project
  - modules       存放模块化资源
  - pages         存放页面资源
  - static        存放非模块化资源

这样划分目录确实直观,但结合前面hinc说过的,但愿能使用component仓库资源,所以我决定将模块化资源目录命名为components,获得:

project
  - components    存放模块化资源
  - pages         存放页面资源
  - static        存放非模块化资源

而nino又提到过模块资源分为项目模块和公共模块,以及hinc提到过但愿能从component安装一些公共组件到项目中,所以,一个components目录还不够,想到nodejs用node_modules做为模块安装目录,所以我在规范中又追加了一个 component_modules 目录,获得:

project
  - component_modules    存放外部模块资源
  - components           存放项目模块资源
  - pages                存放页面资源
  - static               存放非模块化资源

nino说过从此大多数项目采用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. 部署目录设计

托nino的福,我们的部署策略将会很是复杂,根据要求,一个完整的部署结果应该是这样的目录结构:

release
  - public
    - 项目名
      - 1.0.0    1.0.0版本的静态资源都构建到这里
      - 1.0.1    1.0.1版本的静态资源都构建到这里
      - 1.0.2    1.0.2版本的静态资源都构建到这里
      ...
  - views
    - 项目名
      - 1.0.0    1.0.0版本的后端模板都构建到这里
      - 1.0.1    1.0.1版本的后端模板都构建到这里
      - 1.0.2    1.0.2版本的后端模板都构建到这里
      ...

因为还要部署一些能够被第三方使用的模块,public下只有项目名的部署还不够,应改把模块化文件单独发布出来,获得这样的部署结构:

release
  - public
    - component_modules   模块化资源都部署到这个目录下
      - module_a
        - 1.0.0
          - module_a.js
          - module_a.css
          - module_a.png
        - 1.0.1
        - 1.0.2
        ...
    - 项目名
      - 1.0.0    1.0.0版本的静态资源都构建到这里
      - 1.0.1    1.0.1版本的静态资源都构建到这里
      - 1.0.2    1.0.2版本的静态资源都构建到这里
      ...
  - views
    - 项目名
      - 1.0.0    1.0.0版本的后端模板都构建到这里
      - 1.0.1    1.0.1版本的后端模板都构建到这里
      - 1.0.2    1.0.2版本的后端模板都构建到这里
      ...

因为 component_modules 这个名字太长了,若是部署到这样的路径下,url会很长,这也是一个优化点,所以最终决定部署结构为:

release
  - public
    - c                   模块化资源都部署到这个目录下
      - 公共模块
        - 版本号
      - 项目名
        - 版本号
    - 项目名
      - 版本号             非模块化资源都部署到这个目录下
  - views
    - 项目名
      - 版本号             后端模板都构建到这个目录下

插一句,并非全部团队都会有这么复杂的部署要求,这和松鼠团队的业务需求有关,但我相信这个例子也不会是最复杂的。每一个团队都会有本身的运维需求,前端资源部署常常牵连到公司技术架构,所以不少前端项目的开发目录结构会和部署要求保持一致。这也为项目间模块的复用带来了成本,由于代码中写的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.config.set('roadmap.path', [
    {
        reg : '**.md',   //全部md后缀的文件
        release : false  //不发布
    },
    {
        reg : /^\/views\/(.*)$/i,
        //使用${xxx}引用fis.config的其余配置项
        release : '/public/${name}/${version}/$1'
    }
]);

//项目配置,将name、version独立配置,统管全局
fis.config.set('name', 'proj');
fis.config.set('version', '1.0.0');

fis的配置系统很是灵活,除了 文档 中提到的配置节点,其余配置用户能够随便定义使用。好比配置的roadmap是系统保留的,而name、version都是用户本身随便指定的。fis系统保留的配置节点只有6个,包括:

  1. project(系统配置)
  2. modules(构建流程配置)
  3. settings(插件配置)
  4. roadmap(目录规范与域名配置)
  5. deploy(部署目标配置)
  6. 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 project
fis release --dest ../release

获得:

至此,咱们已经基本解决了开发和部署直接的目录规范问题,这里我须要加快一些步伐,把其余目录的部署规范也配置好,获得一个相对比较完整的结果:

fis.config.set('roadmap.path', [
    {
        //md后缀的文件不发布
        reg : '**.md',
        release : false
    },
    {
        //component_modules目录下的代码,因为component规范,已经有了版本号
        //我将它们直接发送到public/c目录下就行了
        reg : /^\/component_modules\/(.*)$/i,
        release : '/public/c/$1'
    },
    {
        //项目模块化目录没有版本号结构,用全局版本号控制发布结构
        reg : /^\/components\/(.*)$/i,
        release : '/public/c/${name}/${version}/$1'
    },
    {
        //views目录下的文件发布到【public/项目名/版本】目录下
        reg : /^\/views\/(.*)$/,
        release : '/public/${name}/${version}/$1'
    },
    {
        //其余文件就不属于前端项目了,好比nodejs的后端代码
        //不处理这些文件的资源定位替换(useStandard: false)
        //也不用对这些资源进行压缩(useOptimizer: false)
        reg : '**',
        useStandard : false,
        useOptimizer : false
    }
]);

fis.config.set('name', 'proj');
fis.config.set('version', '1.0.2');

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

无论部署规则多么复杂都不用担忧,有fis强大的资源定位系统帮你在开发规范和部署规范之间创建联系,设计开发体系不在受制于工具的实现能力。

你能够尽情发挥想象力,设计出最优雅最合理的开发规范和最高效最贴合公司运维要求的部署规范,最终用fis的roadmap.path功能将它们链接起来,实现完美转换。

fis的roadmap功能实际上提供了项目代码与部署规范解耦的能力。

从前面的例子能够看出,开发使用相对路径便可,fis会在构建时会根据fis-conf.js中的配置完成开发路径到部署路径的转换工做。这意味着在fis体系下开发的模块将具备自然的可移植性,既能知足不一样项目的不一样部署需求,又能容许开发中使用相对路径进行资源定位,工程师再不用把部署路径写到代码中了。

愉快的一天就这么过去了,睡觉!

2014年02月14日 - 阴转多云

每到周五老是很是惬意的感受,无论这一周多么辛苦,周五就是一个解脱,更况且今天仍是个特别的日子——情人节!

昨天主要解决了开发概念、开发目录规范、部署目录规范以及初步的fis-conf.js配置。今天要进行前端开发体系设计的关键任务——模块化框架。

模块化框架是前端开发体系中最为核心的环节。

模块化框架肩负着模块管理、资源加载、性能优化(按需,请求合并)等多种重要职责,同时它也是组件开发的基础框架,所以模块化框架设计的好坏直接影响到开发体系的设计质量。

很遗憾的说,如今市面上已有的模块化框架都没能很好的处理模块管理、资源加载和性能优化三者之间的关系。这倒不是框架设计的问题,而是由前端领域语言特殊性决定的。框架设计者通常在思考模块化框架时,一般站在纯前端运行环境角度考虑,基本功能都是用原生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插件以后,静态资源请求的效果是这样的:

  1. http://www.example.com/page.js
  2. http://www.example.com/a.js,b.js
  3. http://www.example.com/c.js,d.js,e.js
  4. http://www.example.com/f.js

工做过程是

  1. 框架先请求了入口文件page.js
  2. 加载完成后分析factory函数,发现依赖了a.js和b.js,而后发起一个combo请求,加载a.js和b.js
  3. a.js和b.js加载完成后又进一步分析源码,才发现还依赖了c.js、d.js和e.js,再发起请求加载这三个文件
  4. 完成c、d、e的加载以后,再分析,发现f.js依赖,再请求
  5. 完成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.config({
    deps : {
        'page.js' : [ 'a.js', 'b.js' ],
        'a.js'    : [ 'c.js' ],
        'b.js'    : [ 'd.js', 'e.js' ],
        'c.js'    : [ 'f.js' ],
        'd.js'    : [ 'f.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

//postpackager插件接受4个参数,
//ret包含了全部项目资源以及资源表、依赖树,其中包括:
//   ret.src: 全部项目文件对象
//   ret.pkg: 全部项目打包生成的额外文件
//   reg.map: 资源表结构化数据
//其余参数暂时不用管
var createFrameworkConfig = function(ret, conf, settings, opt){
    //建立一个对象,存放处理后的配置项
    var map = {};
    //依赖树数据
    map.deps = {};
    //遍历全部项目文件
    fis.util.map(ret.src, function(subpath, file){
        //文件的依赖数据就在file对象的requires属性中,直接赋值便可
        if(file.requires && file.requires.length){
            map.deps[file.id] = file.requires;
        }
    });
    console.log(map.deps);
};
//在modules.postpackager阶段处理依赖树,调用插件函数
fis.config.set('modules.postpackager', [createFrameworkConfig]);

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

执行fis release查看命令行输出,能够看到consolog.log的内容为:

{
    deps: {
        'components/bar/bar.js': [
            'components/bar/bar.css'
        ],
        'components/foo/foo.js': [
            'components/bar/bar.js',
            'components/foo/foo.css'
        ]
    }
}

能够看到js和同名的css自动创建了依赖关系,这是fis默认进行的依赖声明。有了这个表,咱们就能够把它注入到代码中了。咱们为页面准备一个替换用的钩子,好比约定为__FRAMEWORK_CONFIG__,这样用户就能够根据须要在合适的地方获取并使用这些数据。模块化框架的配置通常都是写在非模块化文件中的,好比html页面里,因此咱们应该只针对views目录下的文件作这样的替换就能够。因此咱们须要给views下的文件进行一个标记,只有views下的html或js文件才须要进行依赖树数据注入,具体的配置为:

fis.config.set('roadmap.path', [
    {
        reg : '**.md',
        release : false
    },
    {
        reg : /^\/component_modules\/(.*)$/i,
        release : '/public/c/$1'
    },
    {
        reg : /^\/components\/(.*)$/i,
        release : '/public/c/${name}/${version}/$1'
    },
    {
        reg : /^\/views\/(.*)$/,
        //给views目录下的文件加一个isViews属性标记,用以标记文件分类
        //咱们能够在插件中拿到文件对象的这个值
        isViews : true,
        release : '/public/${name}/${version}/$1'
    },
    {
        reg : '**',
        useStandard : false,
        useOptimizer : false
    }
]);

var createFrameworkConfig = function(ret, conf, settings, opt){
    var map = {};
    map.deps = {};
    fis.util.map(ret.src, function(subpath, file){
        if(file.requires && file.requires.length){
            map.deps[file.id] = file.requires;
        }
    });
    //把配置文件序列化
    var stringify = JSON.stringify(map, null, opt.optimize ? null : 4);
    //再次遍历文件,找到isViews标记的文件
    //替换里面的__FRAMEWORK_CONFIG__钩子
    fis.util.map(ret.src, function(subpath, file){
        //有isViews标记,而且是js或者html类文件,才须要作替换
        if(file.isViews && (file.isJsLike || file.isHtmlLike)){
            var content = file.getContent();
            //替换文件内容
            content = content.replace(/\b__FRAMEWORK_CONFIG__\b/g, stringify);
            file.setContent(content);
        }
    });
};
fis.config.set('modules.postpackager', [createFrameworkConfig]);

//项目配置
fis.config.set('name', 'proj');     //将name、version独立配置,统管全局
fis.config.set('version', '1.0.3');

我在views/index.html中写了这样的代码:

<!doctype html>
<html>
<head>
    <title>hello</title>
</head>
<body>
    <script type="text/javascript" src="scrat.js"></script>
    <script type="text/javascript">
        require.config(__FRAMEWORK_CONFIG__);
        require.async('components/foo/foo.js', function(foo){
            //todo
        });
    </script>
</body>
</html>

执行 fis release -d ../release 以后,获得构建后的内容为:

<!doctype html>
<html>
<head>
    <title>hello</title>
</head>
<body>
    <script type="text/javascript" src="/public/proj/1.0.3/scrat.js"></script>
    <script type="text/javascript">
        require.config({
            "deps": {
                "components/bar/bar.js": [
                    "components/bar/bar.css"
                ],
                "components/foo/foo.js": [
                    "components/bar/bar.js",
                    "components/foo/foo.css"
                ]
            }
        });
        require.async('components/foo/foo.js', function(foo){
            //todo
        });
    </script>
</body>
</html>

在调用 require.async('components/foo/foo.js') 之际,模块化框架已经知道了这个foo.js依赖于bar.js、bar.css以及foo.css,所以能够发起两个combo请求去加载全部依赖的js、css文件,完成后再执行回调。

如今模块的id有一些问题,由于模块发布会有版本号信息,所以模块id也应该携带版本信息,从前面的依赖树生成配置代码中咱们能够看到模块id其实也是文件的一个属性,所以咱们能够在roadmap.path中从新为文件赋予id属性,使其携带版本信息:

fis.config.set('roadmap.path', [
    {
        reg : '**.md',
        release : false,
        isHtmlLike : true
    },
    {
        reg : /^\/component_modules\/(.*)$/i,
        //追加id属性
        id : '$1',
        release : '/public/c/$1'
    },
    {
        reg : /^\/components\/(.*)$/i,
        //追加id属性,id为【项目名/版本号/文件路径】
        id : '${name}/${version}/$1',
        release : '/public/c/${name}/${version}/$1'
    },
    {
        reg : /^\/views\/(.*)$/,
        //给views目录下的文件加一个isViews属性标记,用以标记文件分类
        //咱们能够在插件中拿到文件对象的这个值
        isViews : true,
        release : '/public/${name}/${version}/$1'
    },
    {
        reg : '**',
        useStandard : false,
        useOptimizer : false
    }
]);

从新构建项目,咱们获得了新的结果:

<!doctype html>
<html>
<head>
    <title>hello</title>
</head>
<body>
    <img src="/public/proj/1.0.4/logo.png"/>
    <script type="text/javascript" src="/public/proj/1.0.4/scrat.js"></script>
    <script type="text/javascript">
        require.config({
            "deps": {
                "proj/1.0.4/bar/bar.js": [
                    "proj/1.0.4/bar/bar.css"
                ],
                "proj/1.0.4/foo/foo.js": [
                    "proj/1.0.4/bar/bar.js",
                    "proj/1.0.4/foo/foo.css"
                ]
            }
        });
        require.async('proj/1.0.4/foo/foo.js', function(foo){
            //todo
        });
    </script>
</body>
</html>

you see?全部id都会被修改成咱们指定的模式,这就是以文件为中心的编译系统的威力。

以文件对象为中心构建系统应该经过配置指定文件的各类属性。插件并不本身实现某种规范规定,而是读取file对象的对应属性值,这样插件的职责单一,规范又能统一块儿来被用户指定,为完整的前端开发体系设计奠基了坚实规范配置的基础。

接下来还有一个问题,就是模块名太长,开发中写这么长的模块名很是麻烦。咱们能够借鉴流行的模块化框架中经常使用的缩短模块名手段——别名(alias)——来下降开发中模块引用的成本。此外,目前的配置其实会针对全部文件生成依赖关系表,咱们的开发概念定义只有components和component_modules目录下的文件才是模块化的,所以咱们能够进一步的对文件进行分类,获得这样配置规范:

fis.config.set('roadmap.path', [
    {
        reg : '**.md',
        release : false,
        isHtmlLike : true
    },
    {
        reg : /^\/component_modules\/(.*)$/i,
        id : '$1',
        //追加isComponentModules标记属性
        isComponentModules : true,
        release : '/public/c/$1'
    },
    {
        reg : /^\/components\/(.*)$/i,
        id : '${name}/${version}/$1',
        //追加isComponents标记属性
        isComponents : true,
        release : '/public/c/${name}/${version}/$1'
    },
    {
        reg : /^\/views\/(.*)$/,
        isViews : true,
        release : '/public/${name}/${version}/$1'
    },
    {
        reg : '**',
        useStandard : false,
        useOptimizer : false
    }
]);

而后咱们为一些模块id创建别名:

var createFrameworkConfig = function(ret, conf, settings, opt){
    var map = {};
    map.deps = {};
    //别名收集表
    map.alias = {};
    fis.util.map(ret.src, function(subpath, file){
        //添加判断,只有components和component_modules目录下的文件才须要创建依赖树或别名
        if(file.isComponents || file.isComponentModules){
            //判断一下文件名和文件夹是否同名,若是同名则创建一个别名
            var match = subpath.match(/^\/components\/(.*?([^\/]+))\/\2\.js$/i);
            if(match && match[1] && !map.alias.hasOwnProperty(match[1])){
                map.alias[match[1]] = file.id;
            }
            if(file.requires && file.requires.length){
                map.deps[file.id] = file.requires;
            }
        }
    });
    var stringify = JSON.stringify(map, null, opt.optimize ? null : 4);
    fis.util.map(ret.src, function(subpath, file){
        if(file.isViews && (file.isJsLike || file.isHtmlLike)){
            var content = file.getContent();
            content = content.replace(/\b__FRAMEWORK_CONFIG__\b/g, stringify);
            file.setContent(content);
        }
    });
};
fis.config.set('modules.postpackager', [createFrameworkConfig]);

再次构建,在注入的代码中就能看到alias字段了:

require.config({
    "deps": {
        "proj/1.0.5/bar/bar.js": [
            "proj/1.0.5/bar/bar.css"
        ],
        "proj/1.0.5/foo/foo.js": [
            "proj/1.0.5/bar/bar.js",
            "proj/1.0.5/foo/foo.css"
        ]
    },
    "alias": {
        "bar": "proj/1.0.5/bar/bar.js",
        "foo": "proj/1.0.5/foo/foo.js"
    }
});

这样,代码中的 require('foo'); 就等价于 require('proj/1.0.5/foo/foo.js');了。

还剩最后一个小小的需求,就是但愿能像写nodejs同样开发js模块,也就是要求实现define的自动包裹功能,这个能够经过文件编译的 postprocessor 插件完成。配置为:

//在postprocessor对全部js后缀的文件进行内容处理:
fis.config.set('modules.postprocessor.js', function(content, file){
    //只对模块化js文件进行包装
    if(file.isComponents || file.isComponentModules){
        content = 'define("' + file.id + 
                  '", function(require,exports,module){' +
                  content + '});';
    }
    return content;
});

全部在components目录和component_modules目录下的js文件都会被包裹define,并自动根据roadmap.path中的id配置进行模块定义了。

最煎熬的一天终于过去了,睡一觉,拥抱一下周末。

2014年02月15日 - 超晴

周末的天气很是好哇,一觉睡到中午才起,这么好的天气写码岂不是很loser?!

2014年02月16日 - 小雨

竟然浪费了一天,剩下的时间很少了,今天要抓紧啊!!!

让咱们来回顾一下已经完成了哪些工做:

  • 规范
    • 开发规范
      • 模块化开发,js模块化,css模块化,像nodejs同样的模块化开发
      • 组件化开发,js、css、handlebars维护在一块儿
    • 部署规范
      • 采用nodejs后端,基本部署规范应该参考 express 项目部署
      • 按版本号作非覆盖式发布
      • 公共模块可发布给第三方共享
  • 框架
    • js模块化框架,支持请求合并,按需加载等性能优化点
  • 工具
    • 能够编译stylus为css
    • 支持js、css、图片压缩
    • 容许图片压缩后以base64编码形式嵌入到css、js或html中
    • 与ci平台集成
    • 文件监听、浏览器自动刷新
    • 本地预览、数据模拟
  • 仓库
    • 支持component模块安装和使用

剩下的几个需求中有些是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;
};

编译后获得:

define("proj/1.0.5/foo/foo.js", function(require,exports,module){
//依赖声明
var bar =  require('proj/1.0.5/bar/bar.js');
//把handlebars文件的字符串形式嵌入到js中
var text = "<h1>{{title}}</h1>";
var tpl = Handlebars.compile(text);
exports.render = function(data){
    return tpl(data);
};

//把图片的base64嵌入到js中
var data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoA...';
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中添加这样的配置便可:

//依赖开源的stylus包
var stylus = require('stylus');
//编译插件只负责处理文件内容
var stylusParser = function(content, file, conf){
    return stylus(content, conf).render();
};
//配置编译流程,styl后缀的文件通过编译插件函数处理
fis.config.set('modules.parser.styl', stylusParser);
//告诉fis,styl后缀的文件,被当作css处理,编译后后缀也是css
fis.config.set('roadmap.ext.styl', 'css');

这样咱们项目中的*.styl后缀的文件都会被编译为css内容,而且会在后面的流程中被当作css内容处理,好比压缩、csssprite等。

文件监听、自动刷新都是fis内置的功能,fis的release命令集合了全部编译所需的参数,

  fis release -h

  Usage: release [options]

  Options:

    -h, --help             output usage information
    -d, --dest <names>     release output destination
    -m, --md5 [level]      md5 release option
    -D, --domains          add domain name
    -l, --lint             with lint
    -t, --test             with unit testing
    -o, --optimize         with optimizing
    -p, --pack             with package
    -w, --watch            monitor the changes of project
    -L, --live             automatically reload your browser
    -c, --clean            clean compile cache
    -r, --root <path>      set project root
    -f, --file <filename>  set fis-conf file
    -u, --unique           use unique compile caching
    --verbose              enable verbose output

这些参数是能够随意组合的,好比咱们想文件监听、自动刷新,则使用:

fis release -wL

压缩、打包、文件监听、自动刷新、发布到output目录,则使用:

fis release -opwLd output

构建工具不须要那么多命令,或者develop、release等不一样状态的配置文件,应该从命令行切换编译参数,从而实现开发/上线构建模式的切换。

另外,fis是命令行工具,各类内置的插件也是彻底独立无环境依赖的,能够与ci平台直接对接,并在各个主流操做系统下运行正常。

利用fis的内置的各类编译功能,咱们离目标又近了许多:

  • 规范
    • 开发规范
      • 模块化开发,js模块化,css模块化,像nodejs同样的模块化开发
      • 组件化开发,js、css、handlebars维护在一块儿
    • 部署规范
      • 采用nodejs后端,基本部署规范应该参考express项目部署
      • 按版本号作非覆盖式发布
      • 公共模块可发布给第三方共享
  • 框架
    • js模块化框架,支持请求合并,按需加载等性能优化点
  • 工具
    • 能够编译stylus为css
    • 支持js、css、图片压缩
    • 容许图片压缩后以base64编码形式嵌入到css、js或html中
    • 与ci平台集成
    • 文件监听、浏览器自动刷新
    • 本地预览、数据模拟
  • 仓库
    • 支持component模块安装和使用

剩下两个,咱们能够经过扩展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文件,还有几个插件,若是我把这样一个零散的系统交付团队使用,那么你们使用的步骤差很少是这样的:

  1. 安装fis,npm install -g fis
  2. 安装component安装用的命令行插件,npm insatll -g fis-command-component
  3. 安装stylus编译插件,npm install -g fis-parser-stylus
  4. 下载一份配置文件,fis-conf.js,修改里面的name、version配置

这种状况让团队用起来会有不少问题。首先,安装过程太过麻烦,其次若是项目多,那么fis-conf.js不能同步升级,这是很是严重的问题。grunt的gruntfile.js也是如此。若是说有一个项目用了某套grunt配置感受很爽,那么下个项目也想用这套方案,复制gruntfile.js是必须的操做,项目用的多了,同步gruntfile的成本就变得很是高了。

所以,fis提供了一种“包装”的能力,它容许你将fis做为内核,包装出一个新的命令行工具,这个工具内置了一些fis的配置,而且把全部命令行调用的参数传递给fis内核去处理。

我准备把这套系统打包为一个新的工具,给它取名为 scrat,也是一只松鼠。这个新工具的目录结构是这样的:

scrat
  - bin
    - scrat
  - node_modules
    - fis
    - fis-parser-handlebars
    - fis-lint-jshint
    - scrat-command-install
    - scrat-command-server
    - scrat-parser-stylus
  - index.js
  - package.json

其中,index.js的内容为:

//require一下fis模块
var fis = module.exports = require('fis');

//声明命令行工具名称
fis.cli.name = 'scrat';

//定义插件前缀,容许加载scrat-xxx-xxx插件,或者fis-xxx-xxx插件,
//这样能够造成scrat本身的插件系统
fis.require.prefixes = [ 'scrat', 'fis' ];

//把前面的配置都写在这里统一管理
//项目中就不用再写了
fis.config.merge({...});

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

  1. 安装这个工具,npm install -g scrat
  2. 项目配置只有两项,name和version

使用新工具的命令、参数几乎和fis彻底同样:

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

而scrat这个工具所内置的配置将变成规范文档描述给团队同窗,这套系统要比grunt那种松散的构建系统组成方式更容易被多个团队、多个项目同时共享。

熬了一个通宵,基本算是完成了。。。

2014年02月17日 - 多云

终于到了周一,交付了一个新的开发工具——scrat,及其使用 文档

然而,过去的三天,为了构造这套前端开发体系,都写了哪些代码呢?

  • 基于fis的一套规范及插件配置,274行;
  • scrat install命令行插件,用于安装component模块,74行;
  • scrat server命令行插件,用于启动nodejs的服务器,203行
  • 编译stylus的插件,10行
  • 编译handlebars的插件,6行
  • 一个模块化框架 scrat.js,393行

一共 960行 代码,用了4人/天。

总结

不能否认,为大规模前端团队设计集成解决方案须要花费很是多的心思。

若是说只是实现一个简单的编译+压缩+文件监+听自动刷新的常规构建系统,基于fis应该不超过1小时就能完成一个,但要实践完整的前端集成解决方案,确实须要点时间。

如以前一篇 文章 所讲,前端集成解决方案有8项技术要素,除了组件仓库,其余7项对于企业级前端团队来讲,应该都须要完整实现的。即使暂时不想实现,也会随着业务发展而被迫慢慢完善,这个完善过程是普适的。

对于前端集成解决方案的实践,能够总结出这些设计步骤:

  1. 设计开发概念,定义开发资源的分类(模块化/非模块化)
  2. 设计开发目录,下降开发、维护成本(开发规范)
  3. 根据运维和业务要求,设计部署规范(部署规范)
  4. 设计工具,完成开发目录和部署目录的转换(开发-部署转换)
  5. 设计模块化框架,兼顾性能优化(开发框架)
  6. 扩展工具,支持开发框架的构建需求(框架构建需求)
  7. 流程整合(开发、测试、联调、上线等流程接入)

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

  • seajs开发体系,支付宝团队前端开发体系,以 spm 为构建和包管理工具
  • fis-plus,百度绝大多数前端团队使用的开发体系,以fis为构建工具内核,以lights为包管理工具
  • edp,百度ecomfe前端开发体系,以 edp 为构建和包管理工具
  • modjs,腾讯AlloyTeam团队出品的开发体系
  • yeoman,google出品的解决方案,以grunt为构建工具,bower为包管理工具

纵观这些公司出品的前端集成解决方案,深刻剖析其中的框架、规范、工具和流程,均可以发现一些共通的影子,设计思想异曲同工,不约而同的朝着一种方向前进,那就是前端集成解决方案。尝试将前端工程孤立的技术要素整合起来,解决常见的领域问题。

或许有人会问,不就是写页面么,用得着这么复杂?

在这里我不能给出确定或者否认的答复。

由于单纯从语言的角度来讲,html、js、css(甚至有人认为css是数据结构,而非语言)确实是最简单最容易上手的开发语言,不用模块化、不用工具、不用压缩,任何人均可以快速上手,完成一两个功能简单的页面。因此说,在通常状况下,前端开发很是简单。

在规模很小的项目中,前端技术要素彼此不会直接产生影响,所以无需集成解决方案。

但正是因为前端语言这种灵活松散的特色,使得前端项目规模在达到必定规模后,工程问题凸显,成为发展瓶颈,各类技术要素彼此之间开始出现关联,要用模块化开发,就必须对应某个模块化框架,用这个框架就必须对应某个构建工具,要用这个工具,就必须对应某个包管理工具……这个时候,完整实践前端集成解决方案就成了不二选择。

当前端项目达到必定规模后,工程问题将成为主要瓶颈,原来孤立的技术要素开始彼此产生影响,须要有人从比较高的角度去梳理、寻找适合本身团队的集成解决方案。

因此会出现一些框架或工具在小项目中使用的好好的,一旦放到团队里使用就很是困难的状况。

前端入门虽易工程不易,且行写珍惜!

相关文章
相关标签/搜索