前端工程与模块化框架

前端模块化框架肩负着 模块管理、资源加载两项重要的功能,这两项功能与工具、性能、业务、部署等工程环节都有着非常紧密的联系。因此,模块化框架的设计应该最高优先级考虑工程需要。

CMD
CMD 模块依赖声明方式:

define(function (require) { 
var a = require('./a'); 
var b = require('./b'); // more code ..})

CMD 依赖是就近声明,通过内部require
方法进行声明。但是因为是异步模块,加载器需要提前加载这些模块,所以模块真正使用前需要提取模块里面所有的依赖。无论是加载器即时提取,还是通过自动化工具预先提取,CMD 的这种依赖声明格式只能通过静态分析方式实现,这也正是 CMD 的弊端所在。
CMD 规范的弊端
不能直接压缩:require
是局部变量,意味着不能直接的通过压缩工具进行压缩,若require
这个变量被替换,加载器与自动化工具将无法获取模块的依赖。
模块书写有额外约定:路径参数不能进行字符串运算,不能使用变量代替,否则加载器与自动化工具无法正确提取路径。

规范之外的约定意味着更多的文档说明,除非它们也是规范中的一部分。
注:SeaJS 静态分析实现是把模块包toString()
后使用正则提取require
部分得到依赖的模块路径。
AMD
AMD 模块依赖声明方式:

define(['./a', './b'], function (a, b) { 
// more code ..
})

AMD 的依赖是提前声明。这种优势的好处就是依赖无需通过静态分析,无论是加载器还是自动化工具都可以很直接的获取到依赖,规范的定义可以更简单,意味着可能产生更强大的实现,这对加载器与自动化分析工具都是有利的。
AMD 规范的弊端
依赖提前声明在代码书写上不是那么友好
模块内部与 NodeJS 的 Modules 有一定的差异

关于第二点的问题需要特别说明下。其实无论是 CMD 还是 AMD 的异步模块,都无法与同步模块规范保持一致(NodeJS 的 Modules),只有谁比谁更像同步模块而已。AMD 要转换为同步模块,除了去掉define
函数的包裹外,需要在头部使用require
把依赖声明好,而 CMD 只需要去掉define
函数的包裹即可。
从规范上来说,AMD 更加简单且严谨,适用性更广,而在 RequireJS 强力的推动下,在国外几乎成了事实上的异步模块标准,各大类库也相继支持 AMD 规范。
但从 SeaJS 与 CMD 来说,也做了很多不错东西:1、相对自然的依赖声明风格 2、小而美的内部实现 3、贴心的外围功能设计 4、更好的中文社区支持

模块化框架在工程方面的缺点:

  • requirejs和seajs二者在加载上都有缺陷,就是模块的依赖要等到模块加载完成后,通过静态分析(seajs)或者deps参数(requirejs)来获取,这就为 合并请求 和 按需加载 带来了实现上的矛盾:

    • 要么放弃按需加载,把所有js合成一个文件,从而满足请求合并(两个框架的官方demo都有这样的例子);
    • 要么放弃请求合并,请求独立的模块文件,从而满足按需加载。
  • AMD规范在执行callback的时候,要初始化所有依赖的模块,而CMD只有执行到require的时候才初始化模块。所以用AMD实现某种if-else逻辑分支加载不同的模块的时候,就会比较麻烦了。考虑这种情况:

//AMD for SPArequire(['page/index', 'page/detail'], function(index, detail){
 //在执行回调之前,index和detail模块的factory均执行过了 switch(location.hash){
 case '#index':
 index(); 
 break; 
case '#detail': 
detail(); 
break;
 }});

在执行回调之前,已经同时执行了index和detail模块的factory,而CMD只有执行到require才会调用对应模块的factory。这种差别带来的不仅仅是性能上的差异,也可能为开发增加一点小麻烦,比如不方便实现换肤功能,factory注意不要直接操作dom等。当然,我们可以多层嵌套require来解决这个问题,但又会引起模块请求串行的问题。

结论:以纯前端方式实现模块化框架 不能 同时满足 按需加载,请求合并
和 依赖管理 三个需求。导致这个问题的根本原因是 纯前端方式只能在运行时分析依赖关系。

解决模块化管理的新思路
由于根本问题出在 运行时分析依赖,因此新思路的策略很简单:不在运行时分析依赖。这就要借助 构建工具做线下分析了,其基本原理就是:

利用构建工具在线下进行 模块依赖分析,然后把依赖关系数据写入到构建结果中,并调用模块化框架的 依赖关系声明接口 ,实现模块管理、请求合并以及按需加载等功能。

举个例子,假设我们有一个这样的工程:

project 
├ lib 
│ └ xmd.js #模块化框架
├ mods #模块目录
     │ ├ a.js
     │ ├ b.js 
     │ ├ c.js
     │ ├ d.js 
     │ └ e.js
└ index.html #入口页面

工程中,index.html的源码内容为:

<!doctype html>
...
<script src="lib/xmd.js"></script> <!-- 模块化框架 -->
<script>
 //等待构建工具生成数据替换 `__FRAMEWORK_CONFIG__' 变量 require.config(__FRAMEWORK_CONFIG__);
</script>
<script> 
//用户代码,异步加载模块
 require.async(['a', 'e'], function(a, e){
 //do something with a and e.
 });
</script>...

工程中,mods/a.js 的源码内容为(采用类似CMD的书写规范):

define('a', function(require, exports, module){ 
console.log('a.init'); 
var b = require('b');
var c = require('c');
 exports.run = function(){
 //do something with b and c.
 console.log('a.run'); 
};
});

具体实现过程

  • 用工具在下线对工程文件进行扫描,得到依赖关系表:
{
 "a" : [ "b", "c" ], 
 "b" : [ "d" ]}
  • 工具把依赖表构建到页面或者脚本中,并调用模块化框架的配置接口,index.html的构建结果为:
<!doctype html>
...
<script src="lib/xmd.js"></script> <!-- 模块化框架 -->
<script>
 //构建工具生成的依赖数据
 require.config({
 "deps" : { 
"a" : [ "b", "c" ],
 "b" : [ "d" ]
 } 
});
</script>
<script> 
//用户代码,异步加载模块
 require.async(['a', 'e'], function(a, e){ 
//do something with a and e.
 });
</script>
  • 模块化框架根据依赖表加载资源,比如上述例子,入口需要加载a、e两个模块,查表得知完整依赖关系,配合combo服务,可以发起一个合并后的请求:

http://www.example.com/??d.js,b.js,c.js,a.js,e.js

先来看一下这种方案的优点

  • 采用类似CMD的书写规范(同步require函数声明依赖),可以在执行到require语句的时候才调用模块的factory。

  • 虽然采用CMD书写规范,但放弃了运行时分析依赖,改成工具输出依赖表,因此 依赖分析完成后可以压缩掉require关键字

  • 框架并没有严格依赖工具,它只是约定了一种数据结构。不使用工具,人工维护 require.config({...})相关的数据也是可以的。对于小项目,文件全部合并的情况,更加不需要deps表了,只要在入口的require.async调用之前加载所有模块化的文件,依赖关系无需额外维护

  • 构建工具设计非常简单,而且可靠。工作就是扫描模块文件目录,得到依赖表,JSON序列化之后插入到构建代码中

  • 由于框架预先知道所有模块的依赖关系,因此可以借助combo服务实现请求合并,而不用等到一级模块加载完成才能知道后续的依赖关系。

  • 如果构建工具可以自动包装define函数,那么整个系统开发起来会感觉跟nodejs非常接近,比较舒服。

再来讨论一下这种方案的缺点:
由于采用require函数作为依赖标记,因此如果需要变量方式require,需要额外声明,这个时候可以实现兼容AMD规范写法,比如

define('a', ['b', 'c'], function(require, exports, module){ 
console.log('a.init');
 var name = isIE ? 'b' : 'c'; 
var mod = require(name);
 exports.run = function(){ 
//do something with mod. 
console.log('a.run');
 };})

只要工具把define函数中的 deps
参数,或者factory内的require都作为依赖声明标记来识别,这样工程性就比较完备了。

问题

  • 但是在网购型系统里,很可能顶部的购物车、支付模块等,不是来自本系统,而是来自其他业务部门,这些东西却非要集成在一个页面里,它们的公共项就很难处理。所以我理解阿里把模块拆得这么碎,然后用看上去很怪异的方式,在nginx那边搞combiner来合并,然后也正是为此,可能js会有乱序,必须晚期依赖。
    答:
    对于大型系统,都不是整站构建的,而是按业务拆成了很多个子系统,每个产品会产生一张资源表,跨业务的依赖会引入对应产品库的表,每个业务子系统是独立构建上线的。举个例子:

每个子系统独立构建,并产生独立的表,线上部署的大致效果为:



每个子系统的静态资源id结构为: 系统名:资源id,比如common系统下的jquery代码,其id为 common:lib/jquery/jquery-2.0.2.js,所有的依赖关系可以记录在模板或模板所引用的js中的,模板中提供了静态资源管理和加载的接口,比如user子系统中希望使用message系统下的资源,其代码为(在user.git下的widget/user-info/user-info.php):



模板中的import函数,会在运行时读取资源表来实现静态资源按需,资源表中也记录了子系统内代码的合并情况,可以在模板运行期间计算静态资源的最优组合(带宽、请求数等)
每个系统独立构建,只有运行时的交叉引用,不会出现整站构建的情况
  • 楼主对Browserify这个解决方案怎么看?


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容