fouber / blog

没事写写文章,喜欢的话请点star,想订阅点watch,千万别fork!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

前端开发体系建设日记

fouber opened this issue · comments

前端开发体系建设日记

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

干货 fuck things 在哪里!

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

ps: 写完才发现这篇文章真的非常非常长,涵盖了前端开发中的很多方面,希望大家能有耐心看完,相信一定会有所斩获。。。

2014年02月12日 - 晴

新到松鼠团队的第二天,小伙伴 @nino 找到我说

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两个目录调整为 viewspublic,规范又修改为:

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

在上面的配置中,版本号被写到了匹配规则里,这样非常不方便工程师在迭代的过程中升级项目版本。我们应该将版本号、项目名称等配置独立出来管理。好在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> 参数指定编译结果的产出路径,可以看到部署后的结果:

fis release

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

得到:

资源定位替换2

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

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模块依赖关系树:

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是数据结构,而非语言)确实是最简单最容易上手的开发语言,不用模块化、不用工具、不用压缩,任何人都可以快速上手,完成一两个功能简单的页面。所以说,在一般情况下,前端开发非常简单。

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

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

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

所以会出现一些框架或工具在小项目中使用的好好的,一旦放到团队里使用就非常困难的情况。

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

满足一切前端开发需求,诚不欺我也。

4天解决如此多的开发需求,给力

为何没有上后端静态资源管理以及后端模板的组件化能力呢?是时间问题还是有的新的思考?

真的很长,晚上回去慢慢看。

@hefangshi

不同的公司要采用不同的技术选型,以松鼠团队现状来看,这样的选型是比较合适的,后面会做更深入的探索。

mark 再看

@fouber 我们有一套基于 Ruby 的集成方案 Linner 在团队实践一年了, 思路一致.

在使用上追求简洁, 易用. 尤其体现在配置上.

@SaitoWu

不错,赞一个,每个团队都会实践自己的解决方案,相信思路上也会一致,因为这个过程是普适的。

工程化还是很复杂的一件事情

牛逼~

写得很好,赞一个

好专业啊

赞~

写的真好 赞一个 战斗力真强

mark一下。

NB

给力!!学习了!!!

So, sexy !

= =。。。好厉害,好多新东西都没办法用到。。。所以也不知道怎么学。。。学的东西,是应为在使用,学不到,是因为,没在用。

@yyman001

早晚会用到,到时再去了解也不迟

commented

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

总结的很好

工程化的思考除了需要过硬的编程能力,更多需要的是对人和工程本身的深刻洞察。大赞!

commented

太牛逼~

赞 Fis
关于按需加载有个问题,请教下。
我理解,按需加载其实是按照 Page 的粒度进行的,比 Module 更高一层。理论上讲,Page 会比 Module 少很多。
基于这种想法的按需加载方案:
1、增加两个(js + css)列表(排序防止不用顺序不命中缓存)记录已加载的 Page(直接 require.async 的 Module)。
2、require 新的 Page 时将 1 中的列表拼接发送给服务器。
3、服务器解析依赖,返回对应的 Module 以及依赖的内容。

这样可以:
1、不再需要将所有依赖关系发送给客户端,减小文件大小。
2、请求链接更短(期望值),更清晰。

稍微增加服务器处理,有 Cache 情况下可以无视。

这种方案是不是更好呢?

不愧是国内工业化前端第一人!!服!

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

說得好!

@ZoomZhao

了解你的意思了,这也是一种不错的机制,需要把依赖关系表产出给静态资源服务器,然后去diff差异。并不一定是page,只要require.async把自己被调用过的参数记录下来,然后再发起加载请求的时候带上就好了,在后端做diff,非常机智!

大局观啊!我用grunt做前端构建工具时,就把项目文件按照css,html,js,image来划分的。后来才发现,特码的,我写的组件,css,js,图片太分散了。外人怎么引用啊?

@litson 不会看了半天还是只看到fis吧。。。工具不重要,如果用grunt能实现也挺好。问题是工具之外的过程、步骤

写的好详细,点赞

佩服云龙,用fis基本上不用去关注构建的任何问题了,只需关注业务本身,开发效率提升不少!继续推广,普及fis,不同公司在fis的基础上都会很快搭建出一个符合个性化的前端集成解决方案!

正好在梳理工程上的一些问题,lz此文真是雪中送炭那,给了不少启示,十分感谢~

长见识了

是啊,我也是这样想的,看了这篇文章受益不少

发自我的 iPad

在 2014年4月30日,10:39,xiaomingming notifications@github.com 写道:

大局观啊!我用grunt做前端构建工具时,就把项目文件按照css,html,js,image来划分的。后来才发现,特码的,我写的组件,css,js,图片太分散了。外人怎么引用啊?


Reply to this email directly or view it on GitHub.

component_modules目录存放外部模块资源的意思是这里存放一些公共的模块,例如公共组件等公共资源吗?
如果是这样的话,每新建一个项目都要将所有的公共资源拉到本地来?那这些公共资源又是怎样维护的?

通过compoment或bower。

发自我的 iPad Mini

在 2014年5月2日,16:31,Dongming Ji notifications@github.com 写道:

component_modules目录存放外部模块资源的意思是这里存放一些公共的模块,例如公共组件等公共资源吗?
如果是这样的话,每新建一个项目都要将所有的公共资源拉到本地来?那这些公共资源又是怎样维护的?


Reply to this email directly or view it on GitHub.

@xiaoji121

我提供了一个命令行工具,叫

scrat install name@version

新建项目,用户可以创建一个 component.json 的文件,内容为:

{
    "dependencies" : {
        "xxxx" : "1.0.0"
    }
}

然后在项目下执行scrat install就能自动在项目中拉取过来了,而真正的代码是维护在其他仓库中的。

是不是每个项目的工程里面都会有一份公共模块的拷贝,发布时这些公共模块也会作为项目的一部分随项目代码一同发布到线上?

在 2014年5月2日 下午5:16,张云龙 notifications@github.com写道:

@xiaoji121 https://github.com/xiaoji121

我提供了一个命令行工具,叫

scrat install name@version

新建项目,用户可以创建一个 component.json 的文件,内容为:

{
"dependencies" : {
"xxxx" : "1.0.0"
}}

然后在项目下执行scrat install就能自动在项目中拉取过来了,而真正的代码是维护在其他仓库中的。


Reply to this email directly or view it on GitHubhttps://github.com//issues/2#issuecomment-42007425
.

commented

不明觉厉,我是来点赞的

涨姿势。

松鼠浏览器的项目来了:这工期还是太长了,能不能再压一下

其实我是来围观的。顺便发一下,这个前端知识收集:
https://github.com/jikeytang/front-end-collect

建立alias那段fis-conf.js代码有一行错误:map.alias[match[1]] = id; 应改为 map.alias[match[1]] = file.id; 不然就报错了。很少见到这么完整的讲清楚前端流程的文章,写的超棒。

@cospring 多谢提醒,马上修正

好文!翻来覆去看了将近十遍,终于理清一些东西!赞龙兄!哈哈!

@zhaowx

写的太长了。。。所以要反复阅读么。。。

按照这个开发日记,自己也做下来了。现在剩下合并请求(combo )这块还有些迷茫。
在这个blog中只是提到了在得到依赖关系表之后可以将请求合并,但并木有什么实现办法啊。
我看了下seajs的,有一个seajs-combo插件,也需要配合服务器开启combo功能。 咱这边能详细一些么,可以实施一下!
谢龙兄!

@zhaowx

我在文档中提到过seajs-combo的问题,这个你可以再看看那部分的说明。

...
第一个问题还好,尤其是在gzip下差不多多少字节,但是要配置js压缩器保留require函数不压缩。第二个问题就比较麻烦了,虽然seajs有seajs-combo插件可以一定程度上减少请求,但仍然不能很好的解决这个问题。举个例子,有如下seajs模块依赖关系树:
...

文章中上面那段文字后面我比较详细的叙述了关于模块化框架的设计思路。值得注意的是,业内目前没有设计的非常好的,兼顾了性能的模块化框架,因此我们通常要自己动手实践,其依据就是我在文章中说到的原理了。

这里有一个框架: https://github.com/scrat-team/scrat.js/blob/master/scrat.js

就是这篇文档中提到的一个模块化框架的具体实现,你可以参考一下。其核心实现是借助工具分析生成的依赖关系表来设计模块化框架的接口,你可以看看下面的代码,这样的接口改怎么实现:

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
});

后面我会再单独写文章详细介绍模块化框架的设计原则。这部分是前端集成解决方案的关键地方

嗯,谢谢龙兄这么详细的回复!
我使用了scrat.js,现在也得到了这个关系表。那接下来要是想进行请求的合并,那就需要自己动手实践了,龙兄应该是这个意思吧?
那么在得到完整的依赖关系表之后,就可以按照seajs-combo的思路进行请求合并?

@zhaowx

哦,这里确实没有说一个细节,就是我加了一个配置

fis release -p 的时候,生成的结果是:

require.config({
    "combo": true,
    "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"
        ]
    }
});

不加-p参数的时候,就没有那个combo参数了,当然这个功能并不是fis内置的,而是我写的插件实现的,你可以看这里:

https://github.com/scrat-team/scrat/blob/a22737eeba668970cc0bc976b91e04690660f286/index.js#L80

你可以自己根据框架的要求和设计思路自己实现。

然后框架发现有这个combo标记,就会把依赖的资源的请求变成一个combo的url,当然处理combourl的响应问题还是要对应的服务端支持的,这个你可以看这里:

https://github.com/scrat-team/project/blob/master/index.js#L20-L40

对对对,就是这个!
大赞!

赞+1,一口气看完总体上感觉就是目前团队内部想要整体实现的一个前端集成解决方案(PS:请原谅我偷偷地把内部文档“前端框架解决方案"也更正过来了)……

开发目录结构这块有一些疑问,想请教一下 @fouber :

ps: 除非你的团队只有1-2个人,你的项目只有很少的代码量,而且不用关心性能和未来的维护问题,否则,以文件为依据设计的开发概念是应该被抛弃的。

我们团队的项目较多,由于历史原因,静态文件目录结构都是按照Js, Css, Images统一把静态文件管理在static.xxx.xxx,APP(包括页面模板)都是放在类似app.xxx.xxx这里,这样做的主要原因是觉得更方便的管理静态资源,提高重复利用,也有利于日后的CDN处理。

但是,按照龙兄所说到的这种目录结构,管理相对来说个人觉得更复杂了,而且像上面有说到的,共用资源的调用也是不方便了……

呵呵,以上纯属个人愚见,有说得不对的地方请多多指教~

@xiaoji121 @fouber
借着小鸡的问题再问一下。

假如有一个公共库,用来提供项目需要的所有组件,这时有三个产品插件,都会使用到公共库中的组件,假如按照npm/aralejs/scrat这种软件市场的install模式的话,势必会造成公共资源在三个插件中都有一份copy,而这三个插件最终都会部署到同一个手机主客户端系统中,造成安装包的臃肿。

这是我们真实的案例,目前我们采用的是将这个公共库直接放入到手机主客中,其他业务线插件直接引用,不再有install的概念。

请问你对这种场景有什么好的建议么?多谢。

@LeoYuan

这个问题是有取舍的。

虽然在某个时间点上看,是有三个产品插件,共用了一个公共库,但当公共库升级的时候,很快就会出现以下两种情况:

  1. 部署这个公共库的唯一最新版本,三个产品插件立刻使用它。
  2. 部署这个公共库的两个版本,需要升级的插件升级依赖,不需要的使用旧版本。

方案1有一个比较高的工程风险,使得升级公共库的时候要非常谨慎,而且有着比较高的回归测试成本。方案2会产生冗余,跟install方案没有本质区别。

我经常对做工程设计的同学说的一句话就是:

两害之中取其轻

工程设计通常没有完美方案,很多环节都是在寻找平衡点,所以针对这种场景的两害风险与回归测试成本 vs. 性能上的冗余代价 的取舍,这是从业务特征来的,没有统一的定论。

从你的描述中我大概能感觉到你们是把一个完整的产品拆分成多个产品线来开发的。所以可共享部分比较多,而且产品与产品之间页面跳转比较频繁,用户不是独立访问每一个产品的,所以这种情况下共享的收益就比较大。

而我们是多个产品,彼此之间访问独立,没有多少跳转的情况,所以共享的收益较小,而冗余的收益比较大(因为减少了风险)。

多谢 @fouber 如此详尽的回答, 分析过程真的很到位,赞!
其中至少两点让我觉得特别赞,

方案2会产生冗余,跟install方案没有本质区别。

从长期维护的角度看待这个问题,而不仅仅是停留在当前时刻上。

工程设计通常没有完美方案,很多环节都是在寻找平衡点,两害之中取其轻。

醍醐灌顶

想请教个问题,如果我想调试fis的代码(甚至假设要自己写fis插件),应该怎么弄呢?
试了好几种方式,但总的说来都是要node --debug-brk {fis安装目录}/fis.js 。然后用node-inspector之类的调试器监听
但这样的话程序运行路径就不会是要打包的项目路径了。整个程序会中途退出
调试fis和调试其它Node程序不一样的一点就是它是要依赖当前运行目录的…而Node调试又要指定fis.js所在的运行目录……
难道只能用最原始的console?真心求教

@kpaxqin 建议试试看webstorm,调试的时候working directory设置成文件目录,javascript file path设置成fis路径即可。node-inspector的性能比较差,调试FIS比较吃力。

@kpaxqin 没有用过node的--debug-brk模式,但是我们自己开发用的是IDE,比如phpstorm、webstorm,都能对fis进行调试。

@fouber @hefangshi 感谢回答
我用Phpstorm试了下,在要编译项目的working copy 下设置external libraries 到fis目录,再配置debug config中node的javascript file路径到fis.js,设置要执行的参数release -Dpm
点击debug后在console里看到实际执行的是
"D:\Program Files\nodejs\node.exe" --debug-brk=49628 C:\Users\Administrator\AppData\Roaming\npm\node_modules\fis\fis.js release -Dpm

能进入fis.js里的断点,但是在执行release.js之前就报错
process disconnected unexpectedly
最终也没有打包输出

是我哪儿理解错了吗?

@kpaxqin javascript file路径不是fis.js,而是bin/fis这个文件

不太了解你说的external libraries目录设置到FIS下面是干什么,可能不太需要,但是working directory是需要设置到待编译项目下的

@hefangshi 非常感谢,在phpstorm下已经弄成功了
核心问题出在我之前是设置到fis.js,正确的应该是bin/fis

external libraries设到fis下是为了在当前项目中打开fis的源码以便打断点,或者有其它做法?

按照console里打印的命令,试了下连接node-inspector(更习惯chrome的调试界面和快捷键),还是和之前一样的问题(调到一半报端口错误),估计是node-inspector的问题了

@kpaxqin 明白了,external libraries加上了是比较方便,可以直接导航过去

大赞

已经收录,谢谢。《免费的计算机编程类中文书籍》 https://github.com/justjavac/free-programming-books-zh_CN

巧豆麻豆

很好,作者是百度的前端开发吧。个人觉得这种框架最好能剥离公司标签。

@thankwsx

本人已离职,而且尽量剥离了公司的标签,采用fis作为核心工具,是因为相同的功能用grunt实现几乎是不可能的。。。

@fouber 呃,离开百度了啊。有方向吗?要不要到我这里玩玩?

前端的架构,对于数据填充我一直有疑惑,目前常见的两条路
1,ajax结合restful
2,后端模板
前者在复杂的页面的情况下请求会非常多,除非后台提供复合接口(但这样就把 接口与展现耦合了)
后者不利于前后端解耦,而且往往会束缚前端的技术空间,项目变大的时候维护难度就会滚雪球

想请教下@fouber 的看法。

@hax

已经入职松鼠公司了。。。

@kpaxqin

  1. RESTFul的情况,一般会多一层复合接口API,就是某种UI接入层,这种做法确实在展现全页面的时候为了性能而放弃了对REST的坚持。工程为性能让步,这是很常见的做法,没有办法。不过其实让步的并不是非常多,仅在全页面展现的时候才做让步。RESTFul的另一个问题是请求要多一次,相比模板直接渲染,还是差一些的。通过loading界面来让用户“感觉”快一些。
  2. 选用后端模板,必须要坚持一个原则,就是 一定要让前端工程师写后端模板绝对不能把分工划分为【前端工程师写静态页面→后端工程师把它变成模板】。而且,要在后端模板中实现静态资源管理及其接口,相关思路,可以看一下这个demo: https://github.com/fouber/static-resource-management-system-demo

我个人是非常推崇方案二的,但不绝对,还是要根据具体的业务场景来选择。在松鼠团队我最终选型了方案一,因为业务的数据要求高度统一,而且移动端比较倾向于SPA类型的产品,方案一的亲和力比较强。

@fouber
【必须要坚持一个原则,就是 一定要让前端工程师写后端模板】
一针见血,让后端工程师套模板的弊端已经见识到不少了。
但是前端一旦开始写模板,那么相应的Url路由(页面管理)、模板工具类等需求也会随之而来,同时,由于需要向不同的模板页面输出数据,后端依然不能彻底专注于业务
给我的感觉就是:似乎在前后端结合这个问题上,物理界限和工程界限两者有一定冲突
淘宝这篇文章的思路有点意思:
http://ued.taobao.org/blog/2014/04/full-stack-development-with-nodejs/
和您在上面的看法也有一个共同点:传统后端工作中有一部分其实应该交给前端

再回头看看基于restful服务的SPA,某种程度上说,是否可以理解为把controller层直接搬到了浏览器端,统一了物理界限和工程界限
而基于后端模板的项目,为了提高项目的效率和可维护性,目前的趋势是让前端跨越浏览器这道物理界限,完全控制view层,甚至controller层

基于RESTFul的SPA是比较清晰的架构,但是有两个弊端,就是SEO和性能优化。首次全页面渲染总是要多一个数据请求,而且SEO不友好。

前端工程师接手整个UI层不是什么问题,在熊掌公司也这么实践的,fis团队接管了后端框架中的模板引擎模块,路由到没有接管,因为模板渲染并不关心路由问题。controller确实要分成两大类,一类是RESTFul的API处理,另一类是页面渲染的数据拼装处理,第二类使得后端工程师不能专注业务,而是根据页面展现组装模板数据。

目前前后端架构的设计确实还没有一个很好的方案出来,前后端衔接的ui层是一个“脏地带”,理想架构应该能同时满足前端友好、后端友好、seo友好、性能友好,目前来看两者都有缺陷:

条件 SPA项目 传统模板
后端友好 ★★★★★ ★★★☆☆
前端友好 ★★★★★ ★★☆☆☆
SEO友好 ☆☆☆☆☆ ★★★★★
性能友好 ★★★☆☆ ★★★★★

淘宝的那篇文章比较著名,不过我觉得nodejs的功效不应该被放大,服务端应用开发和浏览器应用开发是两个完全不同的概念,语言相同不会带来多少好处。

至于其他说什么用nodejs的话可以让“模板前后端都能渲染”,这纯属胡扯,虽然技术上可行,但设计一个这样的开发体系是很困难的。另外有一些说可以“让js在前后端都能跑”也是扯淡,看过某些所谓前后端能跑的js,其实都是这样的代码结构:

if(runAtServer){
    //do something in server
} else {
    //do something in browser
}

是的,输出到前端的代码携带了一坨不需要的逻辑分支,这对于要求低带宽的前端来说好矬逼,而且很容易暴露server端的敏感信息。


说跑题了,总结一下:

  1. RESTFul和传统模板引擎各有优缺点,自己权衡吧
  2. UI接入层是一个“脏地带”,前端想搞好,应该接管这里
  3. 接管UI要做理性的技术选型,别看着亲切就选了,容易自坑

@fouber 感谢回复,收益良多
看那篇文章的时候我觉得让前端跨越物理障碍,接管UI层确实是个很好的思路
之前有个SPA项目,被这个脏地带拖累得很惨,现在的项目是维护期的传统模板项目,也觉得开发不够清爽
看那文章我也确实觉得异样,仅仅是语言程度的相通,nodejs方案的说服力似乎并不充分。碍于这方面实践经验尚浅,难以找到问题所在

Restful 不是万能,但是以我的理解,并不存在“前者在复杂的页面的情况下请求会非常多,除非后台提供复合接口(但这样就把 接口与展现耦合了)”的问题。Rest相比RPC本来就是大颗粒度数据,你把Rest当成RPC使用才有复合接口的问题。至于说接口和展现耦合的问题,我认为多数情况其实是Rest抽象没做好。

@fouber 的图里比较的不是 Restful 和传统模板,而是典型的 SPA 和典型的后端模板生成的方案。其中性能这项,各有优点和缺点,难以简单比较啊。

但基本意思到位的。故我认为理想的方案是新一代模板,或者说表现层描述语言,直接结合两者优点。

淘宝那个 nodejs 的方案的关键点其实不是 nodejs,而是获得了一种把本来后端做的事情干掉的可能。这才是最重要的。因此在这点上语言相同是非常重要的,因为前端有这个技能和话语权。很多事情,不是单单技术问题啊。

@hax

恩,可能概念上确实有一些模糊的描述。我理解的RESTFul,是指以资源为单位的细粒度接口,但是页面展现的时候,可能要读取多种资源的数据,那么就有合并这些数据请求的需要。

表格中对比的确实更想说是SPA和传统模板的对比,性能部分是考虑传统模板可以一次性渲染出整个html页面,而SPA第一步先请求一个html骨架,然后发起数据请求,这个过程总是没有传统模板有优势。理想的模板方案应该同时满足前后端友好、seo友好、性能友好。4年前FB在velocity上提出的Quickling方案,我觉得目前接近这个要求,但是实现成本略高。

淘宝的那篇文章,关键点不在nodejs,表示赞同。干掉后端的部分事情是有必要的,表示赞同。话语权问题,表示赞同,但还是要提醒大家,不要盲从,在有话语权的前提下,选型可以从容一些。

@hax
【Rest相比RPC本来就是大颗粒度数据,你把Rest当成RPC使用才有复合接口的问题】
粒度是整个restful的基础,决定最终接口的可用性,也是比较容易出问题的地方

我理解的“合适”的粒度,一定是与展现无关的,类似传统j2ee架构中的service层
这样一来在面对复杂页面,多种数据、业务组合时,必然会有针对UI合并请求的需要
还有些项目要求把rest同时暴露给第三方开发者,抽象时的粒度会更小

我目前只实际接触过一个restful项目,理解尚浅,有什么不对的地方还望斧正

@kpaxqin 合适的粒度与展现无关正是一个误解。REST的一个要点在于对资源的抽象,而此资源不可直接对应于业务逻辑的数据。比较而言,其实更接近“展现”,但是是展现的model,而不是像我们一般写页面的时候会包含许多与该页面几乎无关的非常次要的内容。

出现针对UI合并的请求,不是说不可能出现,但是如果大量出现,其实说明这个对resource的抽象出现了偏差,很可能过分暴露了数据细节。

其实理解REST的关键是在于始终记得,传统的web正是非常REST的,只不过它用的是HTML而不是JSON或者XML之类。所以如果要做一个好的资源抽象,只要想象你还是写页面,但是写完全干净的一个页面,而不用考虑由产品、视觉、技术限制等因素所附加的要素。

文章,评论全部看完,收获良多!

尽然看完了,不错~

@byszhao

真有耐心。。。

nice 32 个顶呱呱

非常赞,简单、易懂。学习了如何从零开始构建适合业务的解决方案

我震惊了! 默默回首再看一遍。。。

重新看了一遍,确实是干货好文。

看完一遍留言,收益颇多,给了我一个方向,马上再来第二遍回味下,然后实践起来了

👍 虽然只看到一半,先评论再看。

评论里都放干货,过分了龙哥

commented

可以。。。

本鸟已嚼文3遍

想问个很菜的问题,开发目录与部署目录分离之后,开始的时候没法看到效果,必须等构建完成后才能看到效果,构建期间要等,哪怕构建很快也是有个时间差问题,大家是怎么解决这个构建时间问题的?

既然已经是部署和开发了,想看效果可以在开发目录里看,不需要每次都看部署的效果吧

发自我的 iPhone

在 2015年2月1日,下午12:29,liulangxiong notifications@github.com 写道:

想问个很菜的问题,开发目录与部署目录分离之后,开始的时候没法看到效果,必须等构建完成后才能看到效果,构建期间要等,哪怕构建很快也是有个时间差问题,大家是怎么解决这个构建时间问题的?


Reply to this email directly or view it on GitHub.

@liulangxiong 有缓存,再次构建只会处理和改动文件相关的文件。

commented

拜读了,满分的好文啊,给了非常多的启示!!!

又读了一遍, 每回都有更深的感受.