你应该听过 CommonJS、AMD、CMD、UMD、JS 模块化等等,这篇文章讲的就是这些名称都是什么意思,为什么会出现JS模块化,以及他们分别是如何实现JS模块化的。
最早,我们就是直接在 script 标签里写 JS 代码的
<script>
function add(a, b) {
console.log(a + b)
}
add(1, 2)
</script>
这时候就有几个问题,一个是代码复用,一个是全局作用域污染,还有就是可维护性问题。
随着代码的增加,这些问题越来越严重。所以首先出现了命名空间和匿名闭包 IIFE 模式,对代码进行封装,并通过提供外部方法来对它们进行访问。
var namespace = {}
namespace.add = function(a, b) {
console.log(a + b)
}
namespace.add(1, 2)
// IIEF
var utils = (function() {
var module = {}
module.multiply = function(a, b) {
console.log(a * b)
}
return module
}())
utils.multiply(1,2)
大多数流行的 JS 库,都使用这种模式,比如 jQuery,所有的函数都在一个全局对象 $ 中。然而,这还是至少需要一个全局变量,而且开发人员需要知道正确的依赖顺序,比如需要控制在 jQuery 加载完成后才能运行带有 $ 的代码。
<script src='./jQurey.js'></script>
<script src='./main.js'></script>
CommonJS
09年 CommonJS(或者称作 CJS)规范推出,在 NodeJS 中实现。主要方法是 exports 和 require。
// utils.js 文件
function add(a, b) {
console.log(a + b)
}
module.exports.add = add
// main.js 文件
var add = require('./utils').add
add(1, 2)
CJS 出来以后,服务端的模块概念已经形成,很自然地,大家就想要客户端模块。但是 CJS 是同步的,服务端读取本地硬盘可以很快同步加载完成,但是浏览器同步读取服务器端的模块可能需要很长的时间,浏览器将会处于”假死”状态。所以出现异步加载 js 文件的 AMD。
AMD
AMD 是异步模块定义(Asynchronous Module Definition)。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
//utils.js
define([], function() {
return {
add: function(a, b) {
console.log(a + b)
}
}
})
// main.js 文件
require(['./utils'], function(utils) {
utils.add(1, 2)
})
AMD 依赖 RequireJS,例如多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载,就是 RequireJS 解决的问题。引用模块的时候,将模块名放在数组中作为 reqiure() 的第一参数。如果定义的模块本身也依赖其他模块,同样将依赖模块放在 define() 的第一参数的数组中。
CMD
CMD(Common Module Definition)是玉伯在开发 SeaJS的时候提出来的,SeaJS 要解决的问题和 RequireJS 一样。不同于 AMD 的依赖前置,CMD 是就近依赖。
// AMD
require(['./utils', 'a', 'b'], function(utils) {
console.log(1)
// 还没有用到 utils a b 等模块,但是 AMD 已经初始化了所有模块
console.log(2)
utils.add(1, 2)
})
//CMD
define(function(require, exports, module){
console.log(1)
if(false) {
var utils = require('./utils') // 需要时再 require,不执行就不会加载
utils.add(1, 2)
}
})
但是在 AMD 也是支持依赖就近,也就是 CMD 这样的写法的,所以,RequireJS 中,以上两种方式都能执行。不过,RequireJS 官方文档中,默认都是采用依赖前置的写法。
UMD
再说 UMD,通用模块定义(Universal Module Definition),比如你写了一段代码或者写了一个库,在服务器端和浏览器端都会用到,难道要维护 CJS 和 AMD 两套代码吗,这时候,UMD 就来了,它其实就是帮你判断应该用 AMD 还是 commonJS,是哪个就用哪个方式来定义模块,都不是的话就挂到全局对象上。
// utils.js 文件同上
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
//AMD
define(['utils'], factory)
} else if (typeof exports === 'object') {
//CommonJS
var utils = require('utils')
module.exports = factory(utils)
} else {
root.result = factory(root.utils)
}
}(this, function(utils) {
utils.add(1, 2)
}))
ES6
最后 ES6(ES2015)自带的模块化,这个大家应该就比较熟悉了,使用 import 和 export 关键字来导入和导出模块。不熟悉 ES6 的赶快去阮老师那里补课。
export const utils = {
add: function(a, b) {
console.log(a + b)
}
}
// main.js 文件
import { utils } from "./utils"
utils.add(1, 2)
这里再说一下 CommonJS 和 ES6 的区别,第一个
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块输出的是一个值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。比如
// utils.js 文件
var count = 0
function add(a, b) {
console.log(a + b)
count++
}
module.exports = {add, count}
// main.js 文件
var utils = require('./utils')
utils.add(1, 2)
console.log(utils.count) // 0
虽然执行 add 函数使模块中的 count++ 但是引入 utils.count 时就是 0,模块内部改变并不会导致引入的值的变化。
ES6 模块输出的是值的引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
到上面代码最后一行时候才会去模块中读取 utils.count 的值,这时候的输出就是 1 了,大家可以试一下。
第二个区别是
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
尾声
我们在开发过程中可能对模块化如何实现的关注比较少,因为各种工具都可以帮我们转换成各种需要的模块化方式,比如 TS 可以生成各种模块加载系统使用的代码。不过看完这篇文章,有没有大概了解了 JS 模块化呢
如果你喜欢看视频的话,可以看这里哦
参考
https://juejin.im/post/5b5069f56fb9a04fb136de33
https://www.html.cn/archives/7628
https://es6.ruanyifeng.com/#docs/module-loader
https://juejin.im/post/5e3985396fb9a07cde64c489
https://typescript.bootcss.com/modules.html
https://juejin.im/post/5c17ad756fb9a049ff4e0a62
你应该听过 CommonJS、AMD、CMD、UMD、JS 模块化等等,这篇文章讲的就是这些名称都是什么意思,为什么会出现JS模块化,以及他们分别是如何实现JS模块化的。
最早,我们就是直接在 script 标签里写 JS 代码的
这时候就有几个问题,一个是代码复用,一个是全局作用域污染,还有就是可维护性问题。
随着代码的增加,这些问题越来越严重。所以首先出现了命名空间和匿名闭包 IIFE 模式,对代码进行封装,并通过提供外部方法来对它们进行访问。
大多数流行的 JS 库,都使用这种模式,比如 jQuery,所有的函数都在一个全局对象 $ 中。然而,这还是至少需要一个全局变量,而且开发人员需要知道正确的依赖顺序,比如需要控制在 jQuery 加载完成后才能运行带有 $ 的代码。
CommonJS
09年 CommonJS(或者称作 CJS)规范推出,在 NodeJS 中实现。主要方法是 exports 和 require。
CJS 出来以后,服务端的模块概念已经形成,很自然地,大家就想要客户端模块。但是 CJS 是同步的,服务端读取本地硬盘可以很快同步加载完成,但是浏览器同步读取服务器端的模块可能需要很长的时间,浏览器将会处于”假死”状态。所以出现异步加载 js 文件的 AMD。
AMD
AMD 是异步模块定义(Asynchronous Module Definition)。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD 依赖 RequireJS,例如多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载,就是 RequireJS 解决的问题。引用模块的时候,将模块名放在数组中作为 reqiure() 的第一参数。如果定义的模块本身也依赖其他模块,同样将依赖模块放在 define() 的第一参数的数组中。
CMD
CMD(Common Module Definition)是玉伯在开发 SeaJS的时候提出来的,SeaJS 要解决的问题和 RequireJS 一样。不同于 AMD 的依赖前置,CMD 是就近依赖。
但是在 AMD 也是支持依赖就近,也就是 CMD 这样的写法的,所以,RequireJS 中,以上两种方式都能执行。不过,RequireJS 官方文档中,默认都是采用依赖前置的写法。
UMD
再说 UMD,通用模块定义(Universal Module Definition),比如你写了一段代码或者写了一个库,在服务器端和浏览器端都会用到,难道要维护 CJS 和 AMD 两套代码吗,这时候,UMD 就来了,它其实就是帮你判断应该用 AMD 还是 commonJS,是哪个就用哪个方式来定义模块,都不是的话就挂到全局对象上。
ES6
最后 ES6(ES2015)自带的模块化,这个大家应该就比较熟悉了,使用 import 和 export 关键字来导入和导出模块。不熟悉 ES6 的赶快去阮老师那里补课。
这里再说一下 CommonJS 和 ES6 的区别,第一个
CommonJS 模块输出的是一个值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。比如
虽然执行 add 函数使模块中的 count++ 但是引入 utils.count 时就是 0,模块内部改变并不会导致引入的值的变化。
ES6 模块输出的是值的引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
到上面代码最后一行时候才会去模块中读取 utils.count 的值,这时候的输出就是 1 了,大家可以试一下。
第二个区别是
因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
尾声
我们在开发过程中可能对模块化如何实现的关注比较少,因为各种工具都可以帮我们转换成各种需要的模块化方式,比如 TS 可以生成各种模块加载系统使用的代码。不过看完这篇文章,有没有大概了解了 JS 模块化呢
如果你喜欢看视频的话,可以看这里哦
参考
https://juejin.im/post/5b5069f56fb9a04fb136de33
https://www.html.cn/archives/7628
https://es6.ruanyifeng.com/#docs/module-loader
https://juejin.im/post/5e3985396fb9a07cde64c489
https://typescript.bootcss.com/modules.html
https://juejin.im/post/5c17ad756fb9a049ff4e0a62