前言

本篇我们重点介绍以下四种模块加载规范:

  1. AMD
  2. CMD
  3. CommonJS
  4. ES6 模块

最后再延伸讲下 Babel 的编译和 webpack 的打包原理。

require.js

在了解 AMD 规范之前,我们先来看看 require.js 的使用方式。

项目目录为:

* project/* index.html* vender/* main.js* require.js* add.js* square.js* multiply.js

index.html 的内容如下:

<!DOCTYPE html>
<html><head><title>require.js</title></head><body><h1>Content</h1><script data-main="vender/main" src="vender/require.js"></script></body>
</html>

data-main="vender/main" 表示主模块是 vender 下的 main.js

main.js 的配置如下:

// main.js
require(['./add', './square'], function(addModule, squareModule) {console.log(addModule.add(1, 1))console.log(squareModule.square(3))
});

require 的第一个参数表示依赖的模块的路径,第二个参数表示此模块的内容。

由此可以看出,主模块依赖 add 模块square 模块

我们看下 add 模块add.js 的内容:

// add.js
define(function() {console.log('加载了 add 模块');var add = function(x, y) { return x + y;};return {      add: add};
});

requirejs 为全局添加了 define 函数,你只要按照这种约定的方式书写这个模块即可。

那如果依赖的模块又依赖了其他模块呢?

我们来看看主模块依赖的 square 模块square 模块的作用是求出一个数字的平方,比如输入 3 就返回 9,该模块依赖一个乘法模块,该乘法模块即 multiply.js 的代码如下:

// multiply.js
define(function() {console.log('加载了 multiply 模块')var multiply = function(x, y) { return x * y;};return {      multiply: multiply};
});

square 模块就要用到 multiply 模块,其实写法跟 main.js 添加依赖模块一样:

// square.js
define(['./multiply'], function(multiplyModule) {console.log('加载了 square 模块')return {      square: function(num) {return multiplyModule.multiply(num, num)}};
});

require.js 会自动分析依赖关系,将需要加载的模块正确加载。

requirejs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/requirejs

而如果我们在浏览器中打开 index.html,打印的顺序为:

加载了 add 模块
加载了 multiply 模块
加载了 square 模块
2
9

AMD

在上节,我们说了这样一句话:

requirejs 为全局添加了 define 函数,你只要按照这种约定的方式书写这个模块即可。

那这个约定的书写方式是指什么呢?

指的便是 The Asynchronous Module Definition (AMD) 规范。

所以其实 AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。

你去看 AMD 规范) 的内容,其主要内容就是定义了 define 函数该如何书写,只要你按照这个规范书写模块和依赖,require.js 就能正确的进行解析。

sea.js

在国内,经常与 AMD 被一起提起的还有 CMD,CMD 又是什么呢?我们从 sea.js 的使用开始说起。

文件目录与 requirejs 项目目录相同:

* project/* index.html* vender/* main.js* require.js* add.js* square.js* multiply.js

index.html 的内容如下:

<!DOCTYPE html>
<html>
<head><title>sea.js</title>
</head>
<body><h1>Content</h1><script src="vender/sea.js"></script><script>// 在页面中加载主模块seajs.use("./vender/main");</script>
</body></html>

main.js 的内容如下:

// main.js
define(function(require, exports, module) {var addModule = require('./add');console.log(addModule.add(1, 1))var squareModule = require('./square');console.log(squareModule.square(3))
});

add.js 的内容如下:

// add.js
define(function(require, exports, module) {console.log('加载了 add 模块')var add = function(x, y) { return x + y;};module.exports = {      add: add};
});

square.js 的内容如下:

define(function(require, exports, module) {console.log('加载了 square 模块')var multiplyModule = require('./multiply');module.exports = {      square: function(num) {return multiplyModule.multiply(num, num)}};});

multiply.js 的内容如下:

define(function(require, exports, module) {console.log('加载了 multiply 模块')var multiply = function(x, y) { return x * y;};module.exports = {      multiply: multiply};
});

跟第一个例子是同样的依赖结构,即 main 依赖 add 和 square,square 又依赖 multiply。

seajs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/seajs

而如果我们在浏览器中打开 index.html,打印的顺序为:

加载了 add 模块
2
加载了 square 模块
加载了 multiply 模块
9

CMD

与 AMD 一样,CMD 其实就是 SeaJS 在推广过程中对模块定义的规范化产出。

你去看 CMD 规范的内容,主要内容就是描述该如何定义模块,如何引入模块,如何导出模块,只要你按照这个规范书写代码,sea.js 就能正确的进行解析。

AMD 与 CMD 的区别

从 sea.js 和 require.js 的例子可以看出:

1.CMD 推崇依赖就近,AMD 推崇依赖前置。看两个项目中的 main.js:

// require.js 例子中的 main.js
// 依赖必须一开始就写好
require(['./add', './square'], function(addModule, squareModule) {console.log(addModule.add(1, 1))console.log(squareModule.square(3))
});
// sea.js 例子中的 main.js
define(function(require, exports, module) {var addModule = require('./add');console.log(addModule.add(1, 1))// 依赖可以就近书写var squareModule = require('./square');console.log(squareModule.square(3))
});

2.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。看两个项目中的打印顺序:

// require.js
加载了 add 模块
加载了 multiply 模块
加载了 square 模块
2
9
// sea.js
加载了 add 模块
2
加载了 square 模块
加载了 multiply 模块
9

AMD 是将需要使用的模块先加载完再执行代码,而 CMD 是在 require 的时候才去加载模块文件,加载完再接着执行。

感谢

感谢 require.js 和 sea.js 在推动 JavaScript 模块化发展方面做出的贡献。

CommonJS

AMD 和 CMD 都是用于浏览器端的模块规范,而在服务器端比如 node,采用的则是 CommonJS 规范。

导出模块的方式:

var add = function(x, y) { return x + y;
};module.exports.add = add;

引入模块的方式:

var add = require('./add.js');
console.log(add.add(1, 1));

我们将之前的例子改成 CommonJS 规范:

// main.js
var add = require('./add.js');
console.log(add.add(1, 1))var square = require('./square.js');
console.log(square.square(3));
// add.js
console.log('加载了 add 模块')var add = function(x, y) { return x + y;
};module.exports.add = add;
// multiply.js
console.log('加载了 multiply 模块')var multiply = function(x, y) { return x * y;
};module.exports.multiply = multiply;
// square.js
console.log('加载了 square 模块')var multiply = require('./multiply.js');var square = function(num) { return multiply.multiply(num, num);
};module.exports.square = square;

CommonJS 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/commonJS

如果我们执行 node main.js,打印的顺序为:

加载了 add 模块
2
加载了 square 模块
加载了 multiply 模块
9

跟 sea.js 的执行结果一致,也是在 require 的时候才去加载模块文件,加载完再接着执行。

CommonJS 与 AMD

引用阮一峰老师的《JavaScript 标准参考教程(alpha)》:

CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。

AMD规范则是非同步加载模块,允许指定回调函数。

由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。

但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范。

ES6

ECMAScript2015 规定了新的模块加载方案。

导出模块的方式:

var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;export {firstName, lastName, year};

引入模块的方式:

import {firstName, lastName, year} from './profile';

我们再将上面的例子改成 ES6 规范:

目录结构与 requirejs 和 seajs 目录结构一致。

<!DOCTYPE html>
<html><head><title>ES6</title></head><body><h1>Content</h1><script src="vender/main.js" type="module"></script></body>
</html>

注意!浏览器加载 ES6 模块,也使用 <script> 标签,但是要加入 type="module" 属性。

// main.js
import {add} from './add.js';
console.log(add(1, 1))import {square} from './square.js';
console.log(square(3));
// add.js
console.log('加载了 add 模块')var add = function(x, y) {return x + y;
};export {add}
// multiply.js
console.log('加载了 multiply 模块')var multiply = function(x, y) { return x * y;
};export {multiply}
// square.js
console.log('加载了 square 模块')import {multiply} from './multiply.js';var square = function(num) { return multiply(num, num);
};export {square}

ES6-Module 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/ES6

值得注意的,在 Chrome 中,如果直接打开,会报跨域错误,必须开启服务器,保证文件同源才可以有效果。

为了验证这个效果你可以:

cnpm install http-server -g

然后进入该目录,执行

http-server

在浏览器打开 http://localhost:8080/ 即可查看效果。

打印的顺序为:

加载了 add 模块
加载了 multiply 模块
加载了 square 模块
2
9

跟 require.js 的执行结果是一致的,也就是将需要使用的模块先加载完再执行代码。

ES6 与 CommonJS

引用阮一峰老师的 《ECMAScript 6 入门》:

它们有两个重大差异。

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第二个差异可以从两个项目的打印结果看出,导致这种差别的原因是:

因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

重点解释第一个差异。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

举个例子:

// 输出模块 counter.js
var counter = 3;
function incCounter() {counter++;
}
module.exports = {counter: counter,incCounter: incCounter,
};
// 引入模块 main.js
var mod = require('./counter');console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

counter.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。

但是如果修改 counter 为一个引用类型的话:

// 输出模块 counter.js
var counter = {value: 3
};function incCounter() {counter.value++;
}
module.exports = {counter: counter,incCounter: incCounter,
};
// 引入模块 main.js
var mod = require('./counter.js');console.log(mod.counter.value); // 3
mod.incCounter();
console.log(mod.counter.value); // 4

value 是会发生改变的。不过也可以说这是 "值的拷贝",只是对于引用类型而言,值指的其实是引用。

而如果我们将这个例子改成 ES6:

// counter.js
export let counter = 3;
export function incCounter() {counter++;
}// main.js
import { counter, incCounter } from './counter';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

这是因为

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

Babel

鉴于浏览器支持度的问题,如果要使用 ES6 的语法,一般都会借助 Babel,可对于 import 和 export 而言,只借助 Babel 就可以吗?

让我们看看 Babel 是怎么编译 import 和 export 语法的。

// ES6
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;export {firstName, lastName, year};
// Babel 编译后
'use strict';Object.defineProperty(exports, "__esModule", {value: true
});
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;exports.firstName = firstName;
exports.lastName = lastName;
exports.year = year;

是不是感觉有那么一点奇怪?编译后的语法更像是 CommonJS 规范,再看 import 的编译结果:

// ES6
import {firstName, lastName, year} from './profile';
// Babel 编译后
'use strict';var _profile = require('./profile');

你会发现 Babel 只是把 ES6 模块语法转为 CommonJS 模块语法,然而浏览器是不支持这种模块语法的,所以直接跑在浏览器会报错的,如果想要在浏览器中运行,还是需要使用打包工具将代码打包。

webpack

Babel 将 ES6 模块转为 CommonJS 后, webpack 又是怎么做的打包的呢?它该如何将这些文件打包在一起,从而能保证正确的处理依赖,以及能在浏览器中运行呢?

首先为什么浏览器中不支持 CommonJS 语法呢?

这是因为浏览器环境中并没有 module、 exports、 require 等环境变量。

换句话说,webpack 打包后的文件之所以在浏览器中能运行,就是靠模拟了这些变量的行为。

那怎么模拟呢?

我们以 CommonJS 项目中的 square.js 为例,它依赖了 multiply 模块:

console.log('加载了 square 模块')var multiply = require('./multiply.js');var square = function(num) { return multiply.multiply(num, num);
};module.exports.square = square;

webpack 会将其包裹一层,注入这些变量:

function(module, exports, require) {console.log('加载了 square 模块');var multiply = require("./multiply");module.exports = {square: function(num) {return multiply.multiply(num, num);}};
}

那 webpack 又会将 CommonJS 项目的代码打包成什么样呢?我写了一个精简的例子,你可以直接复制到浏览器中查看效果:

// 自执行函数
(function(modules) {// 用于储存已经加载过的模块var installedModules = {};function require(moduleName) {if (installedModules[moduleName]) {return installedModules[moduleName].exports;}var module = installedModules[moduleName] = {exports: {}};modules[moduleName](module, module.exports, require);return module.exports;}// 加载主模块return require("main");})({"main": function(module, exports, require) {var addModule = require("./add");console.log(addModule.add(1, 1))var squareModule = require("./square");console.log(squareModule.square(3));},"./add": function(module, exports, require) {console.log('加载了 add 模块');module.exports = {add: function(x, y) {return x + y;}};},"./square": function(module, exports, require) {console.log('加载了 square 模块');var multiply = require("./multiply");module.exports = {square: function(num) {return multiply.multiply(num, num);}};},"./multiply": function(module, exports, require) {console.log('加载了 multiply 模块');module.exports = {multiply: function(x, y) {return x * y;}};}
})

最终的执行结果为:

加载了 add 模块
2
加载了 square 模块
加载了 multiply 模块
9

参考

  • 《JavaScript 标准参考教程(alpha)》
  • 《ECMAScript6 入门》
  • 手写一个CommonJS打包工具(一)

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

ES6 系列之模块加载方案相关推荐

  1. ES6模块加载方案 CommonJS和AMD ES6和CommonJS

    目录 CommonJS CommonJS和AMD的对比 ES6和CommonJS 改成ES6 exports和module.exports CommonJS 每个文件就是一个模块,有自己的作用域.在一 ...

  2. ES6 模块加载export 、import、export default 、import() 语法与区别,笔记总结

    ES6模块加载export .import.export default .import() 语法与区别 在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种. ...

  3. ES6之Module 的加载实现(2)

    3.Node 加载 Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的.目前的解决方案是,将两者分开,ES6 模块和 CommonJS ...

  4. 悟空活动中台 - 基于 WebP 的图片高性能加载方案

    本文首发于 vivo互联网技术 微信公众号  链接: https://mp.weixin.qq.com/s/rSpWorfNTajtqq_pd7H-nw 作者:悟空中台研发团队 一.背景 移动端网页的 ...

  5. nodejs学习巩固笔记-nodejs基础,Node.js 高级编程(核心模块、模块加载机制)

    目录 Nodejs 基础 大前端开发过程中的必备技能 nodejs 的架构 为什么是 Nodejs Nodejs 异步 IO Nodejs 事件驱动架构 全局对象 全局变量之 process 核心模块 ...

  6. Android一键生成包含.dex的Jar及动态加载方案

    Android一键生成包含.dex的Jar及动态加载方案 背景:谈到动态加载相信很多小伙伴都会想到 热更新 及 动态加载dex 的技术,最近也因为项目重构的需求,折腾了下这方面的技术点,以前研究过但时 ...

  7. javascript模块化、模块加载器初探

    最常见网站的javascript架构可能是这样的: 一个底层框架文件,如jQuery 一个网站业务框架文件,包含整站公用业务模块类(如弹框.ajax封装等) 多个业务文件,包含每个具体页面有关系的业务 ...

  8. ES6之Module 的加载实现(3)

    4.循环加载 "循环加载"(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本 通常,"循环加载"表示存在强耦合 ...

  9. ES6之Module 的加载实现(1)

    1.浏览器加载 1.1传统方法: 在 HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本 默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎 ...

最新文章

  1. 数据库设计的三大范式
  2. flask 配置文件和学习资料
  3. android+apk+反编译和再签名打包,Android:apk反编译步骤,打包、签名和逆向工程经验总结...
  4. gsoap中的数据结构中不允许有野指针
  5. 【Linux 内核】Linux 内核源码目录说明 ① ( arch 目录 | block 目录 | certs 目录 | crypto 目录 | Documentation 目录 )
  6. Iveely搜索引擎二三题,用你的智慧来解决吧!
  7. 十多位全球技术专家,为你献上近十个小时的.Net微服务介绍
  8. Codeforces 1091E New Year and the Acquaintance Estimation Erdős–Gallai定理
  9. 【python】集合的定义与操作
  10. 在python语言中定义私有成员变量的方法是_Python在类中有“私有”变量吗?
  11. IE8变成IE7的显示方式
  12. Oracle快速入门(1)——ORACLE数据库简介
  13. 怎样用计算机做周计划表,在电脑桌面上制定每日工作日程计划表适合用哪一便签软件?...
  14. minmax()函数
  15. gst-example
  16. 华为薪资等级结构表_2018华为等级工资表一览
  17. JAVA语言编程练习--图形界面--简易登录界面
  18. 文本编辑工具 | Editplus_v5.5 +汉化包,用于java、C/C++的语言工具
  19. 羊皮卷的故事-第七章
  20. 桥牌坐庄训练bm2000 level3闯关记录——A8

热门文章

  1. mysql 按指定值排序
  2. freemark判断传过来的值为空和不为空及问号、感叹号用法
  3. 炒股的最终下场(搞笑趣图)
  4. SCPPO(十五):IIS配置文件节点加密
  5. 嬴彻科技这一年:“姚班”天才加盟、运力模式显现、已有商业化收入
  6. 开源的“谷歌AutoML杀手”来了
  7. 小米高管:已投大量精力研发手机AI芯片,造不造还没定
  8. 问题:'NoneType' object has no attribute 'encoding'
  9. OSS内文件如何设置为无时间限制的下载链接
  10. mysql双机热备的配置步骤