Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从 CommonJS 到 Sea.js #269

Closed
lifesinger opened this issue Jul 10, 2012 · 14 comments
Closed

从 CommonJS 到 Sea.js #269

lifesinger opened this issue Jul 10, 2012 · 14 comments
Milestone

Comments

@lifesinger
Copy link
Member

CommonJS 是什么

CommonJS 是一个有志于构建 JavaScript 生态圈的组织。它有一个 邮件列表,很多开发者参与其中。 整个社区致力于提高 JavaScript 程序的可移植性和可交换性,无论是在服务端还是浏览器端。

CommonJS 模块是什么

JavaScript 并没有内置模块系统(反正现在没有,需要等到 ES6 的普遍支持,不知还需要多少年),于是 CommonJS 创造了自己的。 传统的 CommonJS 模块如下:

math.js

exports.add = function() {
  var sum = 0, i = 0, args = arguments, l = args.length;
  while (i < l) {
    sum += args[i++];
  }
  return sum;
};

increment.js

var add = require('math').add;
exports.increment = function(val) {
  return add(val, 1);
};

program.js

var inc = require('increment').increment;
var a = 1;
inc(a); // 2

CommonJS 与浏览器

仔细看上面的代码,您会注意到 require 是同步的。模块系统需要同步读取模块文件内容,并编译执行以得到模块接口。

然而, 这在浏览器端问题多多

浏览器端,加载 JavaScript 最佳、最容易的方式是在 document 中插入<script> 标签。但脚本标签天生异步,传统 CommonJS 模块在浏览器环境中无法正常加载。

解决思路之一是,开发一个服务器端组件,对模块代码作静态分析,将模块与它的依赖列表一起返回给浏览器端。 这很好使,但需要服务器安装额外的组件,并因此要调整一系列底层架构。

另一种解决思路是,用一套标准模板来封装模块定义:

define(function(require, exports, module) {

  // The module code goes here

});

这套模板代码为模块加载器提供了机会,使其能在模块代码执行之前,对模块代码进行静态分析,并动态生成依赖列表。

为了让静态分析可行,需要遵守一些简单的 规则

把上面例子中的模块封装起来,可得到:

math.js

define(function(require, exports, module) {
  exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
      sum += args[i++];
    }
    return sum;
  };
});

increment.js

define(function(require, exports, module) {
  var add = require('math').add;
  exports.increment = function(val) {
    return add(val, 1);
  };
});

program.js

define(function(require, exports, module) {
  var inc = require('increment').increment;
  var a = 1;
  inc(a); // 2
});

上面是一种封装方案,还有各种各样的封装方案,比如 AMD、Modules/Wrappings、CommonJS/Modules 2.0 等等模块定义规范。

Sea.js 的封装方案就是 CMD 规范:CMD 模块定义规范

CommonJS 与 Sea.js

从上面可以看出,Sea.js 的初衷是为了让 CommonJS Modules/1.1 的模块能运行在浏览器端,但由于浏览器和服务器的实质差异,实际上这个梦无法完全达成,也没有必要去达成。

更好的一种方式是,Sea.js 专注于 Web 浏览器端,CommonJS 则专注于服务器端,但两者有共通的部分。对于需要在两端都可以跑的模块,可以 有便捷的方案来快速迁移

目前 Sea.js 的模块,如果没有用到浏览器环境下的特有属性,可以很方便跑在 NodeJS 端。只要在入口文件处,引入 Sea.js 的 Node.js 版本即可:

// 让 Node 环境可以加载执行 CMD 模块
require('seajs');
var a = require('./a');

这样,a.js 就可以是一个用 define 包裹起来的 CMD 模块了。

CommonJS 的模块需要跑在浏览器端时,通过简单封装就行:

a.js

define(function(require, exports, module) {
   // a.js 原来的代码
});

这样 a.js 就可以在浏览器端通过 Sea.js 加载运行。当然前提是 a.js 没有利用到服务器特有属性和模块,比如 __dirnameprocess 等。

通过上面的方案,我们就实现了 CommonJS 与 Sea.js 两个生态圈的融合,可以彼此互通,让我们书写的 JavaScript 模块可移植,可在不同平台上运行。

@lifesinger
Copy link
Member Author

有任何问题,欢迎留言交流。
注意:已解决的问题,会在整理后删除掉。

@niceue
Copy link

niceue commented Apr 30, 2013

SeaJS貌似是继承了RequireJS的方案(兼容CommonJS),但又不属于AMD,而且也不同于CommonJS。

确实没见过CMD规范,国外只有CommonJS和AMD,如果说CMD是为了和CommonJS以及RequireJS加以区分,那么我建议像AMD一样在define上加上识别,所有符合AMD规范的Module Loader都会有define.amd标示,那么符合CMD规范的SeaJS是否也应该有define.cmd

加上识别的好处是很多第三方的模块可以做到通吃AMD和CMD以及CommonJS,放到哪个环境都可以运行。不用修改第三方插件就可以直接使用。

现在很多插件都在这样做了,但只会判断typeof define === 'function' && define.amd希望有一天这个判断可以变成下面这样:

if ( typeof define === 'function' && define.amd || define.cmd )

@lifesinger
Copy link
Member Author

@niceue CMD 的来历,可以看这篇文档: #588

define.cmd 曾经有过,但个人觉得加上并不好。应该尽量简单一致,而不是像 RequireJS 一样搞分裂。

@niceue
Copy link

niceue commented May 1, 2013

@lifesinger 嗯,我单是从第三方考虑了,就是因为现在这种模块加载器太多了,而且不乏一些使用量很多的工具,例如RequireJS和curl,define这个全局命名随处可见,要区分一个插件所处的环境,得有更多的判断。

seajs简单的外包一层就可以变身Sea模块,其实也是优势了。而且jquery插件适配器的配置更是靓点。

,SeaJS2.0,确实越来越好了

@hax
Copy link

hax commented Jun 14, 2013

@lifesinger 不是分裂的问题,而是如果没有标志,影响为seajs写兼容。仅仅用 typeof define == 'function' 判断CMD似乎过于霸道。

@antife-yinyue
Copy link
Contributor

@hax @lifesinger

这样可否?

if (typeof define === 'function' && define.cmd) {}

@lifesinger
Copy link
Member Author

写兼容其实很简单,typeof seajs !== "undefined" 以及 typeof requirejs !== "undefined",这样写,其实比 typeof define 以及 define.amd / cmd 更靠谱。

@hax
Copy link

hax commented Jun 18, 2013

通过判断seajs和requirejs是否存在虽然“靠谱”,但是不利于其他的兼容实现。这是为什么有define.amd。其他实现加上.amd表示它实现了AMD(当然是否实现完全无误是另一回事情)。如果我检查define.amd,表示我只预期一个 AMD 实现,而不依赖 requirejs 的特别特性。CMD也是一样的。目前至少司徒正美的mass号称兼容CMD(虽然他似乎也没有检测define.cmd)。

@lifesinger
Copy link
Member Author

@hax 现在问题是,大家对 AMD 的理解并不一致,即便有 define.amd,不同的实现,中间细节差异还不少。要不 CMD 要加下 define.cmd

define.amd 的初始用法,是一个 {},类似:

define.amd = {
  'jquery': true
}

支持 AMD 的组件类库,理论上都需要在 amd 对象中注册下,这太 BT 了。

目前大部分用 define.amd 是当布尔值用,非 AMD 的原意。

@lifesinger
Copy link
Member Author

我 out 了,查找了下最新的 AMD 规范:https://github.com/amdjs/amdjs-api/wiki/AMD

对 define.amd 的用法已更新。现在的定位不错。我去加一个 define.cmd

@whilefor
Copy link

有些地方不是很理解,希望能帮忙解释解释:
对模块代码作静态分析,静态分析是怎么运作的,怎么解决了对浏览器端对require方法的异步问题?

@sunmenghua
Copy link

@niceue
if ( typeof define === 'function' && define.amd || define.cmd )
=>
if ( typeof define === 'function' && ( define.amd || define.cmd ) )

@lynnic26
Copy link

lynnic26 commented Jul 16, 2020

和browserify有什么区别?读下来感觉实现原理是一样的,只不过sea.js有一套不同于CommonJS的规范

@johanazhu
Copy link

考古

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants