JavaScript模块化(ES Module/CommonJS/AMD/CMD)

1模块化历史

1.1前言

参照前端模块化开发的价值

1.2无模块化

每次说到JavaScript都会想到Brendan Eich花了十来天就发明了它,那就是JS的鸿蒙时期,混沌初开。
就像当年在校初学前端时写的代码,没有那么多的套路,就是从上往下码代码,没有想着去声明函数神马的,甚至多少代码都写在一个JS里。现在想来真是惨不忍睹。虽然本人入坑前端距那个鸿蒙时代实在久远,但是据各种典籍记载,那时候的代码风格就差不多这样子,从上往下一直堆着就好了。

var a = 0;
if (xxx) {
  // 省略100L
}
document.getElementById('id').onclick = function(event) {
  // 省略若干行
}
......

1.3模块化冒泡

每个行业都有梗,现在和同事聊天有时候还会吐槽十几年前的老网站,真是有幸见过。前辈的聊天更有意思了,当年的登录居然是写死在前端代码里,就像这样子

if (username === 'xxxxx' && password === 'xxxxxx') {
  // 登录成功
}

是不是觉得很无语。当年的前端都是静态页面,没有现在这样子通过ajax和后端交互神马的,内容更是丰富多彩,更新及时。
前端代码愈发庞大,那么自然而然会暴露很多问题。
无非就俩个:

  • 命名冲突
  • 文件依赖

1.3.1命名冲突解决

  1. java风格的namespace,这个很好理解,在此不赘诉,缺点的话,自行想象,不堪
  2. 自执行函数(内部变量不可见不被污染)
// jQuery式的匿名自执行函数
// 缺点是增添了全局变量、依赖需要提前提供
(function(root) {
    root.jQuery = window.$ = jQuery; // 挂载到window之上
})(window)
// 普通自执行函数
// 缺点就是暴露了全局变量,而且随着模块的增加,全局变量会很多
module = function() {
    function module() {

    }
    return module;
}()
  1. YUI3的沙箱机制,这个表示不晓得。

1.3.2文件依赖解决

这个没有解决方法,乖乖自行保证顺序和不缺漏吧

<script src="https://cdn.bootcss.com/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdn.bootcss.com/backbone.js/1.3.3/backbone-min.js"></script>

1.4CommonJS

1.4.1前言

随着前端的发展,到node.js被创,js可以用来写server端代码。
做过后端的同学肯定知道没有模块化怎么能忍呢?
我们通过上节所得,我们可以得出以下几点需待解决

  • 模块代码安全,不可被污染也不可污染别人,沙箱呀
  • 把模块接口暴露出去(得优雅呀,不能增添全局变量)
  • 这个依赖顺序管理

1.4.2发展

这个还真没有经历了解过。度娘一番,幸好看到seajs下的一个issues
大致就是大牛很牛,推出了Modules/1.0规范
之后为了推广到浏览器端,大牛产生分歧,分为三大流派:

  • Modules/1.x 流派(Modules/Transport
    通过工具转换现有的CommonJS)
  • Modules/Async 流派(自立门派)
  • Modules/2.0 流(Modules/Wrappings
    对1.0的升级)

这里说下为什么不能用在浏览器

  • 服务端代码在硬盘,加载模块时间几乎忽略不计。浏览器端就不成了。
  • 模块引用未被function,所以暴露在了全局之下。

1.4.3番外(AMD、CMD)

AMD是 RequireJS 在推广过程中对模块定义的规范化产出。
CMD是 SeaJS 在推广过程中对模块定义的规范化产出。

1.5ES Module

这个是ECMA搞得一套。和之前的区别在于人家是官方的,根正苗红,上文的是社区搞得,野生。

2 ES Module/CommonJS/AMD/CMD差异

2.1 ES Module与CommonJS的差异

编译时和运行时

首先说下编译时和运行时。JavaScript有俩种声明方法(声明变量和声明方法)。var/const/let和function
编译时,对于声明变量会在内存中开辟一块内存空间并指向变量名,且指向变量名,赋值为undefined。对于函数声明会一样的开启空间。不过赋值为声明的函数体。PS:无论顺序如何,都会先声明变量
运行时,执行变量初始化之类的。

// 源码
var a = 3;
function f() {
    return 'f';
}

// 编译时
var a = undefined;
var f = function() {
    return 'f';
}
// 运行时
a = 3;

CommonJS模块是对象,是运行时加载,运行时才把模块挂载在exports之上(加载整个模块的所有),加载模块其实就是查找对象属性。
ES Module不是对象,是使用export显示指定输出,再通过import输入。此法为编译时加载,编译时遇到import就会生成一个只读引用。等到运行时就会根据此引用去被加载的模块取值。所以不会加载模块所有方法,仅取所需。

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
    详情参见

2.2CommonJS与AMD/CMD的差异

AMD/CMD是CommonJS在浏览器端的解决方案。CommonJS是同步加载(代码在本地,加载时间基本等于硬盘读取时间)。AMD/CMD是异步加载(浏览器必须这么干,代码在服务端)

2.3AMD与CMD的差异

  • AMD是提前执行(RequireJS2.0开始支持延迟执行,不过只是支持写法,实际上还是会提前执行),CMD是延迟执行
  • AMD推荐依赖前置,CMD推荐依赖就近

3 用法

3.1 CommonJS

// 导出使用module.exports,也可以exports。exports指向module.exports;即exports = module.exports
// 就是在此对象上挂属性
// commonjs
module.exports.add = function add(params) {
    return ++params;
}
exports.sub = function sub(params) {
    return --params;
}

// 加载模块使用require('xxx')。相对、绝对路径均可。默认引用js,可以不写.js后缀
// index.js
var common = require('./commonjs');
console.log(common.sub(1));
console.log(common.add(1));

3.2 AMD/RequireJS

  • 定义模块:define(id?, dependencies?, factory)
  • 加载模块:require([module], factory)
// a.js
// 依赖有三个默认的,即"require", "exports", "module"。顺序个数均可视情况
// 如果忽略则factory默认此三个传入参数
// id一般是不传的,默认是文件名
define(["b", "require", "exports"], function(b, require, exports) {
    console.log("a.js执行");
    console.log(b);
// 暴露api可以使用exports、module.exports、return
    exports.a = function() {
        return require("b");
    }
})
// b.js
define(function() {
    console.log('b.js执行');
    console.log(require);
    console.log(exports);
    console.log(module);
    return 'b';
})
// index.js
// 支持Modules/Wrappings写法,注意dependencies得是空的,且factory参数不可空
define(function(require, exports, module) {
    console.log('index.js执行');
    var a = require('a');
    var b = require('b');
})
// index.js
require(['a', 'b'], function(a, b) {
    console.log('index.js执行');
})

3.3 CMD/SeaJS

SeaJS平时没有到,不过了解了下,丰富用法看CMD定义规范

  • 定义模块:define(factory)
// a.js
// require, exports, module参数顺序不可乱
// 暴露api方法可以使用exports、module.exports、return
// 与requirejs不同的是,若是未暴露,则返回{},requirejs返回undefined
define(function(require, exports, module) {
    console.log('a.js执行');
    console.log(require);
    console.log(exports);
    console.log(module);
})
// b.js
// 
define(function(require, module, exports) {
    console.log('b.js执行');
    console.log(require);
    console.log(exports);
    console.log(module);
})
// index.js
define(function(require) {
    var a = require('a');
    var b = require('b');
    console.log(a);
    console.log(b);
})

定义模块无需列依赖,它会调用factory的toString方法对其进行正则匹配以此分析依赖。预先下载,延迟执行

3.4 ES Module

输出/export

// 报错1
export 1;
// 报错2
const m = 1;
export m;

// 接口名与模块内部变量之间,建立了一一对应的关系
// 写法1
export const m = 1;
// 写法2
const m = 1;
export { m };
// 写法3
const m = 1;
export { m as module };

PS:这个有点不是很明白,大致理解就是不能直接导出变量,但是可以导出声明(函数、变量声明)。这里的接口理解是export之后的变量,它和变量建立了映射关系。总的而言,export之后只能接声明或者语句

输入/import

基本用法

// 类似于对象解构
// module.js
export const m = 1;
// index.js
// 注意,这里的m得和被加载的模块输出的接口名对应
import { m } from './module';
// 若是想为输入的变量取名
import { m as m1 }  './module';
// 值得注意的是,import是编译阶段,所以不能动态加载,比如下面写法是错误的。因为'a' + 'b'在运行阶段才能取到值,运行阶段在编译阶段之后
import { 'a' + 'b' } from './module';
// 若是只是想运行被加载的模块,如下
// 值得注意的是,即使加载两次也只是运行一次
import './module';
// 整体加载
import * as module from './module';

PS:CommonJS和ES Module是可以写一起的,但是最好不要。毕竟一个是编译阶段一个是运行阶段。就在项目中入过坑,自行体会。

赋值

首先输入的模块变量是不可重新赋值的,它只是个可读引用,不过却可以改写属性

// 单例
// module.js
export const a = {};
// module2.js
export { a } from './module';
import { a as a1 } from './module';
import { a } from './module2';
a1.e = 3;
console.log(a1) // { e: 3 }
console.log(a) // { e: 3 }

输出/export default

// module.js
// 其实export default就是export { xxx as default }
const m = 1;
export default m;
===
export { m as default }
// index.js
// 对应的输入也得相应变化
import module from './module';
===
import { default as module } from './module';

还记得之前export小结处的俩报错么?如下写法正确,因为提供了default接口

// 写法1
export default 1;
// 写法2
const m = 1;
export default m;
// 错误写法
export default const m = 1;

PS:export default只能一次

复合写法

可用于模块间继承。比如在a模块写下如下,那么a模块不就有了./module的方法了

export { a } from './module';
export { a as a1 } from './module';
export * from './module';

动态加载/import()

因为编译时加载,所以不能动态加载模块。不过幸好有import()方法。
这家伙返回值是一个Promise对象,所以then、catch你开心就好

// 普通写法
import('./module').then(({ a }) => {})
// async、await
const { a } = await import('./module');

4 番外自实现

var MyModules = (function(){
    var modules = [];
    function define(name, deps, cb) {
        deps.forEach(function(dep, i) {
            deps[i] = modules[dep];
        });
        modules[name] = cb.apply(cb, deps);
    }
    function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();
MyModules.define('add', [], function() {
    return function(a, b) {
            return a + b;
        };
})
MyModules.define('foo', ['add'], function(add) {
    var a = 3;
    var b = 4;
    return {
        doSomething: function() {
            return add(a, b) + a;;
        }
    };
})
var add = MyModules.get('add');
var foo = MyModules.get('foo');
console.log(add(1, 2));
console.log(foo.doSomething());
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容