一直酝酿着写一篇关于模块化框架的文章,因为模块化框架是前端工程中的最为核心的部分
。本来又想长篇大论的写一篇完整且严肃的paper,但看了@糖饼在DIV.IO的一篇文章 《再谈 SeaJS 与 RequireJS 的差异》觉得可以借着这篇继续谈一下,加上最近spm3发布,在seajs的官网上又引来了一场口水战,我并不想参与到这场论战中,各有所爱的事情不好评论什么,但我想从工程的角度来阐述一下已知的模块化框架相关的问题,并给出一些新的思路,其实也不新啦,都实践了2多年了。
前端模块化框架肩负着
模块管理
、资源加载
两项重要的功能,这两项功能与工具、性能、业务、部署等工程环节都有着非常紧密的联系。因此,模块化框架的设计应该最高优先级考虑工程需要。
基于@糖饼的文章 《再谈 SeaJS 与 RequireJS 的差异》,我这里还要补充一些模块化框架在工程方面的缺点:
- requirejs和seajs二者在加载上都有缺陷,就是模块的依赖要等到模块加载完成后,通过静态分析(seajs)或者deps参数(requirejs)来获取,这就为
合并请求
和按需加载
带来了实现上的矛盾: - AMD规范在执行callback的时候,要初始化所有依赖的模块,而CMD只有执行到require的时候才初始化模块。所以用AMD实现某种if-else逻辑分支加载不同的模块的时候,就会比较麻烦了。考虑这种情况:
//AMD for SPA require(['page/index', 'page/detail'],0)"> function(index detail){ //在执行回调之前,index和detail模块的factory均执行过了switchlocation.hashcase'#index': index();break;'#detail' }});
在执行回调之前,已经同时执行了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__' 变量config__FRAMEWORK_CONFIG__);</script>//用户代码,异步加载模块async'a''e'a e//do something with a and e.}); ...
mods/a.js的源码内容为(采用类似CMD的书写规范):
define(require exports module consolelog'a.init'var b ='b' c 'c'run (){//do something with b and c.'a.run'};});
具体实现过程
- 用工具在下线对工程文件进行扫描,得到依赖关系表:
{"a"["b""c""b""d"]}
//构建工具生成的依赖数据({"deps"</script>
-
模块化框架根据依赖表加载资源,比如上述例子,入口需要加载a、e两个模块,查表得知完整依赖关系,配合combo服务,可以发起一个合并后的请求:
先来看一下这种方案的优点
- 采用类似CMD的书写规范(同步require函数声明依赖),可以在执行到require语句的时候才调用模块的factory。
- 虽然采用CMD书写规范,但放弃了运行时分析依赖,改成工具输出依赖表,因此
依赖分析完成后可以压缩掉require关键字
- 框架并没有严格依赖工具,它只是约定了一种数据结构。不使用工具,人工维护
require.config({...})
相关的数据也是可以的。对于小项目,文件全部合并的情况,更加不需要deps表了,只要在入口的require.async调用之前加载所有模块化的文件,依赖关系无需额外维护 - 构建工具设计非常简单,而且可靠。工作就是扫描模块文件目录,得到依赖表,JSON序列化之后插入到构建代码中
- 由于框架预先知道所有模块的依赖关系,因此可以借助combo服务实现
请求合并
,而不用等到一级模块加载完成才能知道后续的依赖关系。 - 如果构建工具可以自动包装define函数,那么整个系统开发起来会感觉跟nodejs非常接近,比较舒服。
再来讨论一下这种方案的缺点:
由于采用require函数作为依赖标记,因此如果需要变量方式require,需要额外声明,这个时候可以实现兼容AMD规范写法,比如
[ name isIE ?'b' mod name//do something with mod.})
只要工具把define函数中的deps
参数,或者factory内的require都作为依赖声明标记来识别,这样工程性就比较完备了。
但不管怎样,线下分析始终依靠了字面量信息
,所以开发上可能会有一定的局限性,但总的来说瑕不掩瑜。
原文地址 :http://div.io/topic/439
原文链接:https://www.f2er.com/javaschema/284298.html