首页投稿(暂停使用,暂停投稿)今日看点程序员

前端工程与模块化框架

2016-11-30  本文已影响143人  Www刘

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

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、更好的中文社区支持

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

//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" ]}
<!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>

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

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

再来讨论一下这种方案的缺点:
由于采用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都作为依赖声明标记来识别,这样工程性就比较完备了。

问题

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



每个子系统的静态资源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函数,会在运行时读取资源表来实现静态资源按需,资源表中也记录了子系统内代码的合并情况,可以在模板运行期间计算静态资源的最优组合(带宽、请求数等)
每个系统独立构建,只有运行时的交叉引用,不会出现整站构建的情况
上一篇 下一篇

猜你喜欢

热点阅读