引自:http://chaoskeh.com/blog/why-its-hard-to-combo-seajs-modules.html
为什么 SeaJS 模块的合并这么麻烦
引子
最近看到身边很多同学开始抛弃传统的 <script>
而改用 SeaJS 这样的 JS 模块加载器了,这是件好事,也是一种趋势。
但是任何事物都有两面性,使用模块加载器虽然对于代码的可维护性带来了较大的提升,但是也引入了更多的复杂度,所以肯定会给某些方面带来麻烦——比如这篇文章要探讨的 JS 文件合并。
不少人知道 SeaJS 有个配套的文件压缩合并工具 Spm,可是这个工具似乎一直各种调整、跳票,而且目前版本的使用、配置也很复杂,很多人对此怨声载道,比如我见过有人提 issue 说: “我感觉 SeaJS 非常轻量、好用,可是那个 Spm 怎么搞的那么复杂呢?”
其实 Spm 的复杂,有一部分原因正是由于 SeaJS 造成的,请往下看
传统的 JS 合并
如果采用 <script>
标签的话,JS 合并非常简单,比如
<script src="/js/util.js"></script> <script src="/js/index.js"></script>
// util.js function add (a,b) { return a + b; } // index.js var c = add(1, 2); alert(c);
这时候要合并的话,只需要按照 html 上 JS 文件的引入顺序,将相应的文件拼合即可,这一步甚至可以通过一些工具来自动化实现,比如启用 nginx 的 concat 模块后可以这样
<script src="/js/??util.js,index.js"></script>
// 合并后的 JS function add(a,b) { return a + b; } var c = add(1,2); alert(c);
使用 SeaJS 后的合并
上面的例子改用 SeaJS 的话会是这样
<script src="/js/sea.js"></script> <script> seajs.use("/js/index"); </script>
// util.js define(function (require, exports) { exports.add = function (a, b) { return a + b; }; }); // index.js define(function (require) { var util = require('./util'); var c = util.add(1, 2); alert(c); });
这时候如果要做 JS 合并的话,该怎么弄呢?
很多人会觉得这有什么难搞的,就跟传统的方式一样呗,而且更简单了,因为我们都不用手工指定该怎么合并,只要通过对 index.js 的内容进行分析就可以了,有 require 关键词嘛
// 合并后的 JS,替换原来的 index.js define(function (require, exports) { exports.add = function (a, b) { return a + b; }; }); define(function (require) { var util = require('./util'); var c = util.add(1, 2); alert(c); });
现在请问,上面这个合并后的 index.js 如果通过 seajs.use 加载进来的话能正常执行吗?
答案是,在 seajs 1.3.1 版本下,可以正常执行,但是如果抓包的话,会发现浏览器在加载了这个合并后的 index.js 之后,还是会再加载同目录下的 util.js
所以如果你把这个新的 index.js 换个目录存放并且相应修改 seajs.use 的模块路径,那么会发现这个页面没能蹦出预期中的 “3”,因为那个新的目录下没有 util.js
也就是说,这个合并的策略并不能奏效
CMD 规范
这是为啥呢?其实也很简单,翻开 SeaJS 的 CMD 规范 ,开头就说了:一个模块就是一个文件。
换句话说,一个文件里面只能定义一个 CMD 模块,而刚才那个文件里面定义了两个,所以出现异常也不奇怪了。
再来分析一下刚才的例子,我们发现,当一个文件出现了多个 CMD 模块时,其实只有最后那个被 SeaJS 识别了,所以执行时依然需要去再加载 util.js 这个文件。
如果继续深入 SeaJS 源码的话,就知道,CMD 模块其实是“匿名”模块,也就是说开发者没有显式地指定该模块的 id,对于匿名的模块,SeaJS 会用这个 JS 文件的 URL 作为它的 id ,并缓存 id 与 模块之间的关系(你可以理解为“识别”)。
所以只有最后一个定义的 CMD 模块会被识别,因为前面定义的模块都被它覆盖了
Transport 格式
如果 SeaJS 只支持 CMD 模块的话,我们就没法实现 JS 文件的合并了,所以其实 SeaJS 还支持一种 Transport 格式
建议看看玉伯在知乎上的这个回答:CommonJS 的 Modules/Transport 和 Modules/Wrappings 规范有什么区别?
我摘录一下重点
SeaJS 里,推崇的 Modules/Wrappings 规范是 CMD 规范:
define(function(){ })
直接是由开发者手写的,写完后,可直接不经过任何构建工具就在浏览器上加载运行。
但 CMD 模块在正式上线前,依旧需要通过构建工具先转换为 Modules/Transport 格式:define("id", ["dep-1", "dep-2"], function(require, exports, module) { // source code })转换成 Transport 格式后,才能进一步压缩、合并等。
可以看到,Transport 格式其实就是加上了名字的 CMD 模块,SeaJS 在遇到这种模块时就直接通过定义的 id 来缓存模块了
看到这里,你可能会想,这步转换也没啥难的嘛,我们给文件里面两个模块分别加上 id 就 OK 了啊,比如分别叫做 util 和 index
define('util', [], function (require, exports) { exports.add = function (a, b) { return a + b; }; }); define('index', [], function (require) { var util = require('./util'); var c = util.add(1, 2); alert(c); });
实验一下你会发现,浏览器不会再发起对 util.js 的请求了,但是页面也没有蹦出“3”
这又是为啥呢?
firstModuleInPackage
问题好像越来越复杂,但是从 SeaJS 的角度来想,其实很简单:
当调用 seajs.use('/js/index') 时,如果对应的 JS 文件中有两个 Transport 格式的模块,哪一个模块才是调用者想要的(哪个才是 js/index)?
答案相信大家都能想到,根据模块的 id 呗!SeaJS 也正是这么做的,它会比较模块的 id 与 use() 方法的参数(其实是相应 JS 文件的 URL),选用匹配的那个
可是这个“匹配”的规则该怎么定义呢?比如设想这样一个稍微复杂的情况:
util.js 文件中的两个模块 id 分别叫做 text/util 和 util ,当调用 seajs.use('/js/util')
时,究竟哪个模块才是我们需要的呢?
我想这时候大家都会想到这个万无一失的方案:把模块 ID 转换为完整的 URL 再匹配!没错,这也是 SeaJS 的做法:
将所有的模块 id 都转为完整的 URL ,然后选取与当前这个 JS 文件的 URL 完全匹配的那个模块
转换的规则就不细说了,不过看到这里我们大概知道上一步为啥有问题了,因为上一步的 id 只是取了个名字,完全没有考虑 URL ,我们略作修改
// http://localhost/js/index.js 的内容 define('http://localhost/js/util', [], function (require, exports) { exports.add = function (a, b) { return a + b; }; }); define('http://localhost/js/index', [], function (require) { var util = require('./util'); var c = util.add(1, 2); alert(c); });
id 直接用完整的 URL ,这样都不需要转换,这时候执行一下,总算 OK 了
那么上一个例子里面是什么情况呢?这涉及到 SeaJS 中的 firstModuleInPackage 策略
简单来说,上面例子中,所有模块的 id 都与当前 JS 的 URL 不匹配,这时候 SeaJS 会使用文件中第一个模块,所以实际上页面中只是执行了那个 add 方法的定义
关于 firstModuleInPackage 可以看看这篇讨论,这个特性是 1.2.1 引入的,但是玉伯又决定在 2.0 里面去掉这个特性,改为如果没有匹配时不执行任何模块
结语
看到这里,相信大家应该大致了解 SeaJS 文件该如何合并了,它相比传统方式的 JS 合并要复杂许多,原因也不难理解
一是 SeaJS 引入了额外的复杂度,原来简单的文件合并方式不会奏效
二来 SeaJS 相比 RequireJS 简化了模块书写,导致合并时需要做模块格式的转换,比如自动加上 id
SeaJS 虽好,但是不可能没有缺点,当你在一个方面获得巨大的好处时,通常会在其他方面付出代价,所以我们在做选择时一定要做好权衡。
另外,希望这篇文章能让大家更理解 SeaJS 与 Spm,事情没有想象中的那么简单,少一些抱怨,多一些建设性意见!
相关推荐
gulp-seajs-transport插件用于在gulp流程中处理Sea.js的模块依赖和动态加载问题,而gulp-seajs-concat则用于将多个Sea.js模块文件合并为一个单一的JavaScript文件。 ### 项目文件组织结构 为了实现按需合并,我们...
这是自己编写的模仿seajs模块加载的模块加载器,用于学习交流之用。大致模仿seajs的模块化加载实现。
总结来说,`gulp`和`gulp-cmd-pack`的组合为Seajs项目的构建提供了强大支持,通过自动化处理模块依赖、合并和压缩,使得前端开发变得更加高效和便捷。在实际项目中,还可以根据需求与其他`gulp`插件结合,比如处理...
本文将详细介绍如何通过脚本将Seajs模块转换为ES Modules,并探讨这两种模块系统的差异。 1. Seajs模块与ES Modules的区别: - 引入方式:Seajs使用`seajs.use`或`seajs.require`来加载和依赖模块;而ESM使用`...
SeaJS 是一款专为Web端设计的JavaScript模块加载器,它的出现是为了解决JavaScript在浏览器环境中的组织和管理问题。随着Web应用的复杂度不断提升,JavaScript代码的组织和依赖管理变得至关重要,SeaJS 提供了一种...
SeaJS是一个遵循CommonJS规范的JavaScript模块加载框架,可以实现JavaScript的模块化开发及加载机制。
模块化是一种解决此问题的有效方法,它可以将大项目分解为多个独立的、可重用的组件,每个组件都有自己的职责和作用范围。SeaJS 提供了一种符合 CommonJS 规范的模块定义方式,使得开发者能够在浏览器环境中享受到...
这通过`data-seajs combine`属性实现,Seajs会自动检测并合并相同域名下的模块。 9. **Sea.js核心模块**:seajs-2.2.1中的`sea.js`是整个框架的核心,它实现了上述所有功能。通过阅读源码,我们可以深入了解模块...
Seajs是中国开源社区非常受欢迎的一款JavaScript模块加载器,它的出现为Web开发引入了CommonJS规范,使得前端开发更加模块化,便于代码管理和维护。Seajs 2.3.0是该库的一个稳定版本,提供了丰富的功能和优化。 一...
SeaJS 是一款轻量级的前端模块加载器,它遵循CommonJS规范,允许开发者按照模块化的方式编写和加载JavaScript代码。本文将深入探讨SeaJS如何实现模块的依赖加载以及模块API的导出。 首先,SeaJS的核心在于其对模块...
模块化是将代码按照功能划分为独立的单元,每个单元称为模块,模块之间通过接口进行通信。这样可以提高代码的可读性、可维护性和复用性。SeaJS提供了一个基于CMD(Common Module Definition)规范的模块化解决方案。...
Seajs是中国开源社区推出的一款浏览器端的模块加载器,它借鉴了CommonJS的规范,但针对浏览器环境进行了优化,使得JavaScript在浏览器端也能实现模块化的开发。本教程将带你快速了解并掌握Seajs的使用,让你在5分钟...
然而,jQuery作为一个广泛使用的库,它的类和插件通常不是为模块化设计的。本文将介绍如何将jQuery及其相关的类和插件封装成Seajs的模块,以便在Seajs环境中无缝使用。 首先,我们来看如何将jQuery本身封装成Seajs...
Seajs是中国开源社区推出的一款基于模块化开发的前端加载器,它借鉴了CommonJS的模块化思想,但针对浏览器环境进行了优化。Seajs的核心理念是让JavaScript模块化变得简单,帮助开发者解决在大型Web项目中代码组织、...
Seajs配合工具如Sea.js打包工具(sea-modules-builder)或Webpack、Rollup等现代构建工具,可以实现模块的预编译和合并。 7. **示例与Demo**:压缩包中的"demo源码.rar"包含了一些使用Seajs的实际示例代码,通过这些...
Seajs 是一个用于浏览器端的模块加载器,它遵循 CommonJS 规范,让 JavaScript 开发者能够在浏览器环境中实现模块化开发,提高代码的可维护性和复用性。Seajs 的核心理念是通过模块化解决 JavaScript 开发中的依赖...
本文介绍的是seajs模块之间依赖的加载以及模块的执行,下面话不多说直接来看详细的介绍。 入口方法 每个程序都有个入口方法,类似于c的main函数,seajs也不例外。系列一的demo在首页使用了seajs.use() ,这便是入口...
seajs是一种前端JavaScript模块加载框架,它的核心思想是利用CMD(Common Module Definition)规范来组织和管理前端模块。在使用seajs进行前端模块开发时,一个常见的问题是模块压缩和打包问题,尤其是当我们将代码...
基于seajs模块化的合并压缩###合并压缩前--Gruntfile.js--打包脚本--pagekage.json--依赖的npm配置--node-modules/--下载的npm--app/ //存放页面--src/ //打包前目录------seaConfig.js //Seajs配置文件------page/ ...
2. 合并模块时要考虑模块间的依赖顺序,防止因顺序错误导致的运行时错误。 3. 虽然模块合并可以减少HTTP请求,但过多的合并也可能导致文件过大,应适度调整合并策略。 4. 使用此插件时,记得更新HTML中的Seajs加载...