本文主要阐述JavaScript模块化的发展和模块化的相关基础。本文翻译和参考自Tyler McGinnis
的《JavaScript Modules: From IIFEs to CommonJS to ES6 Modules》
JavaScript:从IIFEs,到CommonJS,再到ES6的Modules
模块的组成
一个模块其实可以3个部分:依赖项(也叫做“引入”)、代码、导出。
Dependencies (Imports):依赖项(也叫做“引入”):当一个模块需要另一个模块时,它可以将该模块作为依赖项导入。例如,每当要创建React组件时,都需要导入react模块。如果你想使用像lodash这样的库,你需要导入lodash模块。
Code:代码:在确定了模块所需的依赖项之后,下一部分是模块的实际代码。
Exports:导出:导出是模块的“接口”。无论您从模块导出的是什么,都可以从任何人那里导入该模块。
JS的模块发展史:
IIFE(立即执行函数)-》CommonJS -》ES6 Module
一、IIFE:
形式:
|
|
优点和缺点:
IIFE模块模式有哪些好处?首先,我们避免将所有内容转储到全局命名空间。这将有助于变量冲突并使我们的代码更加私密。它有任何缺点吗?确实如此。我们在全局命名空间APP上仍有1个项目。如果偶然的另一个库使用相同的命名空间,我们就遇到了麻烦。其次,你会注意到index.html文件中<script>
标签的顺序。如果您没有按照现在的确切顺序编写脚本,则应用程序将会中断。
二、CommonJS:
形式:
var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } module.exports.getUsers = getUsers
module.exports
实际上是一个对象,getUsers
是它的一个属性,因此可以这样写:
var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } module.exports = { getUsers: getUsers }
无论我们有多少方法,我们都可以将它们添加到exports
对象中:
// users.js var users = ["Tyler", "Sarah", "Dan"] module.exports = { getUsers: function () { return users }, sortUsers: function () { return users.sort() }, firstUser: function () { return users[0] } }
导入模块:
var users = require('./users') users.getUsers() // ["Tyler", "Sarah", "Dan"] users.sortUsers() // ["Dan", "Sarah", "Tyler"] users.firstUser() // ["Tyler"]
优点和缺点:
如果您以前使用过Node,CommonJS应该看起来很熟悉。原因是因为Node使用(大部分)CommonJS规范来实现模块。因此,使用Node,您可以使用之前看到的CommonJS require和module.exports语法获得开箱即用的模块。但是,与Node不同,浏览器不支持CommonJS。事实上,不仅浏览器不支持CommonJS,而且开箱即用,CommonJS不是一个很好的浏览器解决方案,因为它同步加载模块。在浏览器的领域,异步加载器是王道。
总而言之,CommonJS存在两个问题。首先,浏览器不理解它。其次,它同步加载模块,这在浏览器中将是一个糟糕的用户体验。如果我们能解决这两个问题,我们就会处于良好的状态。那么,如果它对浏览器来说甚至不好,那么花时间谈论CommonJS有什么意义呢?那么有一个解决方案,它被称为模块捆绑器。
题外话:模块捆绑器(Module Bundlers)
JavaScript模块捆绑器的作用是检查您的代码库,查看所有导入和导出,然后智能地将所有模块捆绑到一个浏览器可以理解的文件中。然后,不是在index.html文件中包含所有脚本,而是担心它们进入的顺序,而是包含捆绑器为您创建的单个bundle.js文件。
app.js ---> | | users.js -> | Bundler | -> bundle.js dom.js ---> | |
那么捆绑器实际上如何工作呢?这是一个非常大的问题,而且我自己并不完全了解,但这是通过Webpack(一种流行的模块捆绑器)运行我们的简单代码之后的输出。
三、ES6 Module
形式:
// utils.js // Not exported function once(fn, context) { var result return function() { if(fn) { result = fn.apply(context || this, arguments) fn = null } return result } } // Exported export function first (arr) { return arr[0] } // Exported export function last (arr) { return arr[arr.length - 1] }
现在,为了导入first
和last
,您有几个不同的选择。一种是导入从utils.js导出的所有内容:
import * as utils from './utils' utils.first([1,2,3]) // 1 utils.last([1,2,3]) // 3
但是,如果我们不想导入模块导出的所有内容呢?在这个例子中,如果我们想先导入但不是最后导入怎么办?这是你可以使用所谓的命名导入(它看起来像解构,但它不是)
import { first } from './utils' first([1,2,3]) // 1
ES模块的优点是,您不仅可以指定多个导出,还可以指定默认(default)导出:
// leftpad.js export default function leftpad (str, len, ch) { var pad = ''; while (true) { if (len & 1) pad += ch; len >>= 1; else break; } return pad + str; }
使用默认导出时,会更改导入该模块的方式。您只需使用 import name from './path'
,而不是使用*
语法或使用命名导入:
import leftpad from './leftpad'
现在,如果您有一个默认模块导出 default
,但也导出其他常规导出怎么办?好吧,你会按照你的期望去做。
// utils.js function once(fn, context) { var result return function() { if(fn) { result = fn.apply(context || this, arguments) fn = null } return result } } // regular export export function first (arr) { return arr[0] } // regular export export function last (arr) { return arr[arr.length - 1] } // default export export default function leftpad (str, len, ch) { var pad = ''; while (true) { if (len & 1) pad += ch; len >>= 1; else break; } return pad + str; }
以下导入代码中,leftpad
是默认导出,first
和 last
只是常规导出。
import leftpad, { first, last } from './utils'ES模块的有趣之处在于,它们现在是JavaScript原生的,现代浏览器支持它们而不使用捆绑器。 若在浏览器中使用CommonJS,我们需要使用像Webpack这样的捆绑器,然后在bundle.js文件中包含一个脚本。使用ES模块,在现代浏览器中,我们需要做的就是包含我们的主文件(在本例中为dom.js)并在脚本选项卡中添加一个type ='module'属性。
<script type=module src='dom.js'></script>
Tree Shaking
我们在上面没有介绍的CommonJS模块和ES模块之间还有一个区别。使用CommonJS,您可以在任何地方,甚至是有条件地使用模块。
if (pastTheFold === true) { require('./parallax') }
由于ES模块是静态的,因此import语句必须始终位于模块的顶层。您无法有条件地导入它们。因此下面的代码是错误的:
if (pastTheFold === true) { import './parallax' // "import' and 'export' may only appear at the top level" }
做出这个设计决策的原因是因为通过强制模块是静态的,加载器可以静态地分析模块树,找出实际使用的代码,并从捆绑中删除未使用的代码。换句话说,因为ES模块强制您在模块顶部声明import语句,所以Bundler可以快速理解您的依赖树。当它理解你的依赖树时,它可以看到没有使用的代码并从bundle中删除它。这称为Tree Shaking或Dead Code Elimination。
番外:CommonJS、AMD和CMD
CommonJS和AMD两者都是js模块规范,两者都用require方法引入模块。
commonjs是同步加载模块的。用require(模块名)来引入并执行模块。在定义模块时,是通过“module.exports = 对象/函数”来把模块暴露出去的,其中module表示当前模块,exports属性是对外的接口。而为了方便,还定义了一个exports变量,它指向module.exports。相当于在最前面加上一句var exports=module.exports。可以给exports变量添加对象:exports.对象名 等价于 module.exports.对象名。commonjs是同步的,而在服务器(nodeji)的模块又是在本地的,所以commonjs适用于服务器端的js。
AMD是异步记载模块的。通过require([模块1,模块2…],callback)来引入模块。其中第一个参数是一个数组,是要加载模块的集合,第二个参数是一个回调,只有成功加载前面数组中的模块,才会执行回调,回调中的内容是要使用模块进行的一些操作。amd通过defined关键字来定义模块。由于浏览器需要加载服务器的模块时,需要异步加载(不然会阻塞渲染),所以一般浏览器端使用amd规范。
CMD规范:是commonjs和amd的结合。即遵循amd规范,又遵循commonjs规范。与amd的区别是:cmd是延后执行,推崇依赖就近;而amd是前置执行,推崇依赖前置。