# 前端工程与模块化框架
一直酝酿着写一篇关于模块化框架的文章,因为模块化框架是前端工程中的 ``最为核心的部分`` 。本来又想长篇大论的写一篇完整且严肃的paper,但看了 [@糖饼](https://github.com/aui) 在 [div.io](http://div.io/) 的一篇文章 《[再谈 SeaJS 与 RequireJS 的差异](http://div.io/topic/430)》觉得可以借着这篇继续谈一下,加上最近spm3发布,在seajs的官网上又引来了一场 [口水战](https://github.com/seajs/seajs/issues/454) ,我并不想参与到这场论战中,各有所爱的事情不好评论什么,但我想从工程的角度来阐述一下已知的模块化框架相关的问题,并给出一些新的思路,~~其实也不新啦,都实践了2多年了~~。
> 前端模块化框架肩负着 ``模块管理``、``资源加载`` 两项重要的功能,这两项功能与工具、性能、业务、部署等工程环节都有着非常紧密的联系。因此,模块化框架的设计应该最高优先级考虑工程需要。
基于 [@糖饼](https://github.com/aui) 的文章 《[再谈 SeaJS 与 RequireJS 的差异](http://div.io/topic/430)》,我这里还要补充一些模块化框架在工程方面的缺点:
1. requirejs和seajs二者在加载上都有缺陷,就是模块的依赖要等到模块加载完成后,通过静态分析(seajs)或者deps参数(requirejs)来获取,这就为 ``合并请求`` 和 ``按需加载`` 带来了实现上的矛盾:
* 要么放弃按需加载,把所有js合成一个文件,从而满足请求合并(两个框架的官方demo都有这样的例子);
* 要么放弃请求合并,请求独立的模块文件,从而满足按需加载。
2. AMD规范在执行callback的时候,要初始化所有依赖的模块,而CMD只有执行到require的时候才初始化模块。所以用AMD实现某种if-else逻辑分支加载不同的模块的时候,就会比较麻烦了。考虑这种情况:
```javascript
//AMD for SPA
require(['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`` 的源码内容为:
```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的书写规范):
```javascript
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');
};
});
```
## 具体实现过程
1. 用工具在下线对工程文件进行扫描,得到依赖关系表:
```json
{
"a" : [ "b", "c" ],
"b" : [ "d" ]
}
```
2. 工具把依赖表构建到页面或者脚本中,并调用模块化框架的配置接口,``index.html``的构建结果为:
```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>
```
3. 模块化框架根据依赖表加载资源,比如上述例子,入口需要加载a、e两个模块,查表得知完整依赖关系,配合combo服务,可以发起一个合并后的请求:
http://www.example.com/??d.js,b.js,c.js,a.js,e.js
## 先来看一下这种方案的优点
1. 采用类似CMD的书写规范(同步require函数声明依赖),可以在执行到require语句的时候才调用模块的factory。
1. 虽然采用CMD书写规范,但放弃了运行时分析依赖,改成工具输出依赖表,因此 ``依赖分析完成后可以压缩掉require关键字``
1. 框架并没有严格依赖工具,它只是约定了一种数据结构。不使用工具,人工维护 ``require.config({...})`` 相关的数据也是可以的。对于小项目,文件全部合并的情况,更加不需要deps表了,只要在入口的require.async调用之前加载所有模块化的文件,依赖关系无需额外维护
1. 构建工具设计非常简单,而且可靠。工作就是扫描模块文件目录,得到依赖表,JSON序列化之后插入到构建代码中
1. 由于框架预先知道所有模块的依赖关系,因此可以借助combo服务实现``请求合并``,而不用等到一级模块加载完成才能知道后续的依赖关系。
1. 如果构建工具可以自动包装define函数,那么整个系统开发起来会感觉跟nodejs非常接近,比较舒服。
## 再来讨论一下这种方案的缺点:
由于采用require函数作为依赖标记,因此如果需要变量方式require,需要额外声明,这个时候可以实现兼容AMD规范写法,比如
```javascript
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都作为依赖声明标记来识别,这样工程性就比较完备了。
但不管怎样, ``线下分析始终依靠了字面量信息``,所以开发上可能会有一定的局限性,但总的来说瑕不掩瑜。
> 希望本文能为前端模块化框架的作者带来一些新的思路。没有必要争论规范,工程问题才是最根本的问题。
- 前端篇
- 常用知识点
- 表单处理
- 前后端分离
- 提供模板渲染工具
- 页面优化
- css3动画部分
- 前端工程与模块化框架
- 服务器XML标签用法
- 微信JSSDK
- 小技巧
- 纯CSS实现自适应正方形
- 通用媒体查询
- css 黑科技
- H5性能优化方案
- 10个最常见的 HTML5
- 常见坑
- 资源收集
- 前端组件化开发实践
- 应用秒开计划
- AJAX API部分
- 静态资源处理优化
- 后端篇
- 微信对接与管理
- 微信消息处理
- API插件开发
- Plugin开发
- 后端插件开发
- 组件开发
- XML标签开发
- RESTFUL设计
- Admin GUI
- 设计篇
- 设计规范
- 微信开发库v.js
- 使用方法
- 微信JSSDK集成
- 调试面板使用
- 插件-http功能
- 插件-layer弹出层
- 插件-music 音乐播放器
- 插件-store 本地存储
- 插件 emitter 事件管理器
- 插件-shake 摇动功能
- 插件-lazyload 延迟加载
- 插件-t 模板渲染
- 插件-ani 动画功能
- 插件-is 类型侦测器
- 插件-ease 缓动函数库
- 插件-os 设备检测
- 插件 $ 类Jquery插件
- 插件-md5 散列计算
- 插件-svg动画loading
- 后台页面成功GUI
- 列表渲染List
- 表单生成Config
- 树状列表Tree
- 排序操作Sort
- Js 风格指南
- Vuep
- 内置动画库
- 组件库
- 内置插件库
- PSD自动切图