JavaScript 模块:从 IIFE 到 CommonJS 再到 ES6 模块
我教过很多人 JavaScript 很久了。这门语言最常被低估的部分就是模块系统。这是有原因的。JavaScript 中的模块有着一段奇特而又不稳定的历史。在这篇文章中,我们将回顾这段历史,并学习过去的模块,以便更好地理解 JavaScript 模块如今的工作原理。
如果你更喜欢观看而不是阅读,这里有一个涵盖相同内容的视频。如果你喜欢,欢迎订阅。
在学习如何在 JavaScript 中创建模块之前,我们首先需要了解它们是什么以及它们存在的原因。现在看看你周围。你能看到的任何稍微复杂的东西,很可能都是由一些独立的部件组成的,这些部件组装在一起就形成了完整的整体。
我们以手表为例。
一块简单的腕表由数百个内部零件组成。每个零件都有特定的用途,并明确了与其他零件的相互作用。所有这些零件组合在一起,构成了腕表的整体。我不是腕表工程师,但我认为这种设计方法的好处显而易见。
可重用性
再看一下上面的图表。注意整个手表使用了多少相同的部件。通过以模块化为中心的高度智能的设计决策,他们能够在手表设计的不同方面重复使用相同的组件。这种部件重复使用的能力简化了制造流程,我猜想,这也会提高利润。
可组合性
这张图完美地展现了可组合性。通过为每个组件建立清晰的边界,他们能够将各个部件组合在一起,从而由微小而专注的部件构建出一块功能齐全的手表。
杠杆作用
想想制造过程。这家公司不是在制造手表,而是在制造组装成手表的各个部件。他们可以自己生产这些部件,也可以外包,并利用其他制造工厂,这都无关紧要。最重要的是,每个部件最终组装成一块手表——这些部件在哪里制造并不重要。
隔离
理解整个系统并非易事。由于手表由细小、专注的部件组成,因此每个部件都可以单独思考、制造或维修。这种独立性使得多个人可以独立地操作手表,而不会互相干扰。此外,如果其中一个部件坏了,你只需更换损坏的部件,而不必更换整块手表。
组织
组织性是每个独立部分之间交互方式清晰边界的副产品。有了这种模块化,组织性自然而然地发生,无需过多思考。
我们已经见证了模块化在手表等日常用品上带来的显著优势,那么在软件方面呢?事实证明,两者有着相同的理念和优势。就像手表的设计一样,我们应该将软件设计成不同的部分,每个部分都有特定的用途,并有明确的界限来规定它们如何与其他部分交互。在软件中,这些部分被称为模块。模块听起来可能与函数或 React 组件没什么区别。那么,模块究竟包含哪些内容呢?
每个模块都有三个部分 - 依赖项(也称为导入)、代码和导出。
imports
code
exports
依赖项(导入)
当一个模块需要另一个模块时,它可以import将该模块作为依赖项。例如,每当你想要创建一个 React 组件时,你都需要import该react模块。如果你想使用像 这样的库lodash,你也需要import该lodash模块。
代码
确定模块所需的依赖项后,下一部分是模块的实际代码。
出口
导出是模块的“接口”。从模块导出的任何内容,导入该模块的用户都可以使用。
说了足够多的高层次内容,让我们深入研究一些真实的例子。
首先,我们来看看 React Router。它有一个modules文件夹,方便快捷。这个文件夹里自然充满了……模块。那么在 React Router 中,“模块”的定义是什么呢?事实证明,在大多数情况下,React Router 会将 React 组件直接映射到模块。这很合理,而且通常也是在 React 项目中分离组件的方式。之所以有效,是因为即使你重新阅读上面的代码,将“模块”替换成“组件”,这些比喻仍然适用。
让我们看一下模块的代码MemoryRouter。现在先不要担心实际的代码,而是集中精力于模块的结构。
// imports
import React from "react";
import { createMemoryHistory } from "history";
import Router from "./Router";
// code
class MemoryRouter extends React.Component {
history = createMemoryHistory(this.props);
render() {
return (
<Router
history={this.history}
children={this.props.children}
/>;
)
}
}
// exports
export default MemoryRouter;
你会注意到,在模块的顶部,他们定义了导入的内容,或者说,为了使MemoryRouter模块正常工作,他们需要哪些其他模块。接下来,他们有自己的代码。在本例中,他们创建了一个名为 的新 React 组件MemoryRouter。然后在最底部,他们定义了导出的内容,MemoryRouter。这意味着,每当有人导入该MemoryRouter模块时,他们都会获得该MemoryRouter组件。
现在我们了解了什么是模块,让我们回顾一下手表设计的好处,看看如何通过遵循类似的模块化架构,将这些好处应用到软件设计中。
可重用性
模块可以最大限度地提高可重用性,因为模块可以被导入到任何需要它的模块中。此外,如果某个模块对其他程序有用,您可以将其创建一个包。一个包可以包含一个或多个模块,并且可以上传到NPM供任何人下载。react、lodash、 和jquery都是 NPM 包的示例,因为它们可以从 NPM 目录安装。
可组合性
由于模块明确定义了它们的导入和导出,因此它们易于组合。更重要的是,易于删除是优秀软件的标志。模块提高了代码的“可删除性”。
杠杆作用
NPM 仓库拥有世界上最大的免费可复用模块集合(确切地说超过 70 万个)。如果你需要某个特定的软件包,NPM 很可能能帮你搞定。
隔离
我们用来描述手表隔离性的文字在这里也非常贴切。“理解整个系统很困难。因为(你的软件)是由一些小型、专注的(模块)组成的,所以每个(模块)都可以单独思考、构建和修复。这种隔离允许多个人单独开发(应用程序),而不会互相干扰。此外,如果其中一个(模块)出现故障,你只需更换损坏的(模块),而不是更换整个(应用程序)。”
组织
模块化软件最大的好处或许在于其组织性。模块提供了一个自然的分离点。此外,正如我们即将看到的,模块还能防止污染全局命名空间,并避免命名冲突。
至此,您已经了解了模块的优势和结构。现在是时候真正开始构建它们了。我们将采用非常系统的方法。原因在于,正如前文所述,JavaScript 中的模块有着一段奇特的历史。尽管现在有一些“更新”的 JavaScript 模块创建方法,但一些较老的版本仍然存在,您会不时看到它们。如果我们在 2018 年就直接讨论模块,那对您来说就太不公平了。话虽如此,我们将回到 2010 年末。当时 AngularJS 刚刚发布,jQuery 风靡一时。企业终于开始使用 JavaScript 构建复杂的 Web 应用程序,而这种复杂性带来了对它的管理需求——通过模块。
创建模块的第一个直觉可能是按文件分离代码。
// users.js
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
// dom.js
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = window.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<ul id="users"></ul>
<input
id="input"
type="text"
placeholder="New User">
</input>
<button id="submit">Submit</button>
<script src="users.js"></script>
<script src="dom.js"></script>
</body>
</html>
完整代码可以在这里找到。
好的。我们已经成功地将我们的应用程序分离成它自己的文件。这是否意味着我们已经成功地实现了模块?不,绝对不是。从字面上看,我们所做的只是分离代码所在的位置。在 JavaScript 中创建新作用域的唯一方法是使用函数。我们声明的所有不在函数中的变量都存在于全局对象中。您可以通过window在控制台中记录对象来看到这一点。您会注意到我们可以访问,更糟糕的是,更改addUsers,users,getUsers,addUserToDOM。这基本上就是我们的整个应用程序。我们没有将代码分离成模块,我们所做的只是按物理位置将其分开。如果您是 JavaScript 新手,这可能会让您感到惊讶,但这可能是您对如何在 JavaScript 中实现模块的第一直觉。
那么,如果文件分离不能给我们带来模块,那什么能呢?记住模块的优势——可重用性、可组合性、可利用性、隔离性和可组织性。JavaScript 是否有原生特性可以用来创建我们自己的“模块”,并带来同样的好处?普通的函数怎么样?当你想到函数的好处时,它们与模块的好处完美契合。那么这将如何实现呢?如果我们不将整个应用程序放在全局命名空间中,而是公开一个对象,我们将其称为APP。然后,我们可以将应用程序运行所需的所有方法放在 下APP,这将防止我们污染全局命名空间。然后,我们可以将其他所有内容包装在一个函数中,以将其与应用程序的其余部分隔离开来。
// App.js
var APP = {}
// users.js
function usersWrapper () {
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
APP.getUsers = getUsers
}
usersWrapper()
// dom.js
function domWrapper() {
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = APP.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
}
domWrapper()
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<ul id="users"></ul>
<input
id="input"
type="text"
placeholder="New User">
</input>
<button id="submit">Submit</button>
<script src="app.js"></script>
<script src="users.js"></script>
<script src="dom.js"></script>
</body>
</html>
完整代码可以在这里找到。
现在,如果你查看window对象,它不再包含我们应用程序的所有重要部分,而只包含APP我们的包装函数usersWrapper和domWrapper。更重要的是,我们所有重要的代码(例如users)都无法修改,因为它们不再位于全局命名空间中。
让我们看看能否更进一步。有没有办法摆脱包装函数?注意,我们定义它们之后会立即调用它们。我们给它们命名的唯一原因就是为了能够立即调用它们。有没有办法立即调用匿名函数,这样我们就不用给它们命名了?事实证明是有的,而且它甚至还有一个更酷的名字——Immediately Invoked Function Expression或者IIFE简称。
当前执行函数
它看起来是这样的。
(function () {
console.log('Pronounced IF-EE')
})()
请注意,它只是一个用括号 () 包裹的匿名函数表达式。
(function () {
console.log('Pronounced IF-EE')
})
然后,就像任何其他函数一样,为了调用它,我们在它的末尾添加另一对括号。
(function () {
console.log('Pronounced IF-EE')
})()
现在让我们利用对 IIFE 的了解来摆脱丑陋的包装函数并进一步清理全局命名空间。
// users.js
(function () {
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
APP.getUsers = getUsers
})()
// dom.js
(function () {
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = APP.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
})()
完整代码可以在这里找到。
厨师之吻。现在,如果您查看该window对象,您会注意到我们向其中添加的唯一内容是APP,我们将其用作应用程序正常运行所需的所有方法的命名空间。
我们将此模式称为IIFE 模块模式。
IIFE 模块模式有什么好处?首先,我们避免了将所有内容都转储到全局命名空间。这有助于避免变量冲突,并使我们的代码更加私密。它有什么缺点吗?当然有。我们在全局命名空间中仍然有一项, 。如果其他库碰巧使用了相同的命名空间,我们就有麻烦了。其次,你会注意到文件中标签APP的顺序很重要。如果你没有按照现在的顺序排列脚本,应用程序就会崩溃。<script>index.html
尽管我们的解决方案并不完美,但我们正在不断进步。既然我们了解了 IIFE 模块模式的优缺点,那么如果我们要制定自己的模块创建和管理标准,它会具备哪些特性呢?
之前,我们分离模块的初衷是为每个文件创建一个新的模块。虽然 JavaScript 并非开箱即用,但我认为这是模块分离的明显起点。每个文件都是一个独立的模块。这样一来,我们唯一需要的功能就是让每个文件定义显式的导入(或依赖)和显式的导出,以便其他任何导入该模块的文件都能访问它们。
Our Module Standard
1) File based
2) Explicit imports
3) Explicit exports
现在我们已经了解了模块标准所需的功能,让我们深入研究一下 API。我们唯一需要定义的 API 是导入和导出。我们先从导出开始。为了简单起见,任何关于模块的信息都可以放在对象上module。然后,任何我们想从模块导出的内容都可以放在 上module.exports。像这样
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
module.exports.getUsers = 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]
}
}
现在我们已经搞清楚了模块导出是什么样子的,接下来我们需要搞清楚导入模块的 API 是什么样子的。为了保持这一点的简单性,我们假设有一个名为 的函数require。它将一个字符串路径作为第一个参数,并返回从该路径导出的任何内容。按照users.js上面的文件,导入该模块的代码如下:
var users = require('./users')
users.getUsers() // ["Tyler", "Sarah", "Dan"]
users.sortUsers() // ["Dan", "Sarah", "Tyler"]
users.firstUser() // ["Tyler"]
相当巧妙。通过我们的假设module.exports和require语法,我们保留了模块的所有优点,同时摆脱了 IIFE 模块模式的两个缺点。
你可能已经猜到了,这不是一个虚构的标准。它是真实存在的,叫做 CommonJS。
CommonJS 小组定义了一种模块格式,通过确保每个模块在其自己的命名空间中执行来解决 JavaScript 作用域问题。这是通过强制模块显式导出它想要暴露给“universe”的变量,以及定义正常工作所需的其他模块来实现的。
-Webpack 文档
如果您以前使用过 Node,那么 CommonJS 应该很熟悉。这是因为 Node(在大多数情况下)使用 CommonJS 规范来实现模块。因此,使用 Node,您可以使用前面提到的 CommonJSrequire和module.exports语法来获取开箱即用的模块。然而,与 Node 不同,浏览器不支持 CommonJS。事实上,浏览器不仅不支持 CommonJS,而且 CommonJS 本身就不是一个很好的浏览器解决方案,因为它是同步加载模块的。在浏览器领域,异步加载器才是王道。
总结一下,CommonJS 有两个问题。首先,浏览器无法理解它。其次,它会同步加载模块,这在浏览器中会带来糟糕的用户体验。如果我们能解决这两个问题,那就万事大吉了。既然 CommonJS 连浏览器都不支持,那我们花这么多时间讨论它还有什么意义呢?其实有一个解决方案,那就是模块打包器。
模块捆绑器
JavaScript 模块打包器的作用是检查你的代码库,查看所有导入和导出语句,然后智能地将所有模块打包成一个浏览器可以理解的单一文件。这样,你就不用把所有脚本都打包到 index.html 文件中,也不用担心它们的加载顺序了,只需bundle.js打包器自动生成的单一文件即可。
app.js ---> | |
users.js -> | Bundler | -> bundle.js
dom.js ---> | |
那么打包器究竟是如何工作的呢?这是一个很大的问题,我自己也不太明白,不过这是我们用Webpack(一个流行的模块打包器)运行简单代码后的输出。
完整的 CommonJS 和 Webpack 代码可以在这里找到。你需要下载代码,运行“npm install”,然后运行“webpack”。
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(
exports,
name,
{ enumerable: true, get: getter }
);
}
};
// define __esModule on exports
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string')
for(var key in value)
__webpack_require__.d(ns, key, function(key) {
return value[key];
}.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./dom.js");
})
/************************************************************************/
({
/***/ "./dom.js":
/*!****************!*\
!*** ./dom.js ***!
\****************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval(`
var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n
function addUserToDOM(name) {\n
const node = document.createElement(\"li\")\n
const text = document.createTextNode(name)\n
node.appendChild(text)\n\n
document.getElementById(\"users\")\n
.appendChild(node)\n}\n\n
document.getElementById(\"submit\")\n
.addEventListener(\"click\", function() {\n
var input = document.getElementById(\"input\")\n
addUserToDOM(input.value)\n\n
input.value = \"\"\n})\n\n
var users = getUsers()\n
for (var i = 0; i < users.length; i++) {\n
addUserToDOM(users[i])\n
}\n\n\n//# sourceURL=webpack:///./dom.js?`
);}),
/***/ "./users.js":
/*!******************!*\
!*** ./users.js ***!
\******************/
/*! no static exports found */
/***/ (function(module, exports) {
eval(`
var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n
function getUsers() {\n
return users\n}\n\nmodule.exports = {\n
getUsers: getUsers\n
}\n\n//# sourceURL=webpack:///./users.js?`);})
});
你会注意到这里面有很多神奇之处(如果你想知道具体是怎么回事,可以看看注释),但有一点很有趣,他们把所有代码都包裹在一个大的 IIFE 函数式中。所以他们找到了一种方法,只需利用我们古老的 IIFE 模块模式,就能获得模块系统的所有优点,而不用承担模块系统的缺点。
真正能证明 JavaScript 面向未来发展的是,它是一门充满活力的语言。JavaScript 标准委员会 TC-39 每年召开几次会议,讨论该语言的潜在改进。目前,模块对于编写可扩展、可维护的 JavaScript 至关重要,这一点应该已经非常清楚。早在 2013 年左右(或许更早),JavaScript 就需要一个标准化的内置模块处理解决方案。这开启了 JavaScript 原生模块实现的进程。
了解了现在的知识,如果你被要求为 JavaScript 创建一个模块系统,它会是什么样子?CommonJS 基本上做到了。和 CommonJS 一样,每个文件都可以是一个新模块,并且有清晰的方式定义导入和导出——显然,这就是关键所在。我们在使用 CommonJS 时遇到的一个问题是它同步加载模块。这对服务器来说很棒,但对浏览器来说却并非如此。我们可以做的一个改进是支持异步加载。另一个改进是require,既然我们讨论的是语言本身的新增功能,我们可以定义新的关键字,而不是函数调用。我们就用importand 吧export。
TC-39 委员会并没有再沿着“假设的、虚构的标准”这条路走太远,他们在创建“ES 模块”时也做出了完全相同的设计决策。ES 模块现在是 JavaScript 中创建模块的标准化方式。让我们来看看它的语法。
ES 模块
如上所述,为了指定应该从模块导出什么,您可以使用export关键字。
// 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
但是如果我们不想导入模块导出的所有内容怎么办?在这个例子中,如果我们想导入first但又不想导入怎么办last?这时你就可以使用所谓的named imports(它看起来像解构,但实际上不是)。
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;
}
使用default导出时,导入模块的方式会发生变化。现在,您无需使用*语法或命名导入,只需使用 即可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;
}
现在导入语法应该是什么样子的?在这种情况下,它应该符合你的预期。
import leftpad, { first, last } from './utils'
很巧妙,是吧?leftpad是default出口,而first和last只是常规出口。
ES 模块的有趣之处在于,由于它本身就是 JavaScript 的原生特性,现代浏览器无需使用打包器即可支持它们。让我们回顾一下本教程开头的简单用户示例,看看使用 ES 模块后会是什么样子。
完整代码可以在这里找到。
// users.js
var users = ["Tyler", "Sarah", "Dan"]
export default function getUsers() {
return users
}
// dom.js
import getUsers from './users.js'
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
现在到了最酷的部分。使用我们的 IIFE 模式,我们仍然需要在每个 JS 文件中引入一个脚本(并且按顺序引入)。使用 CommonJS 时,我们需要使用像 Webpack 这样的打包工具,然后将脚本引入到bundle.js文件中。使用 ES 模块,在现代浏览器中,我们只需要引入主文件(在本例中为dom.js),并type='module'在脚本标签页中添加一个属性即可。
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<ul id="users">
</ul>
<input id="input" type="text" placeholder="New User"></input>
<button id="submit">Submit</button>
<script type=module src='dom.js'></script>
</body>
</html>
摇树
CommonJS 模块和 ES 模块之间还有一个区别我们上面没有提到。
使用 CommonJS,您可以require在任何地方使用模块,甚至有条件地使用。
if (pastTheFold === true) {
require('./parallax')
}
由于 ES 模块是静态的,因此 import 语句必须始终位于模块的顶层。您无法有条件地导入它们。
if (pastTheFold === true) {
import './parallax' // "import' and 'export' may only appear at the top level"
}
做出此设计决策的原因是,通过强制模块静态化,加载器可以静态分析模块树,找出实际使用的代码,并从打包文件中移除未使用的代码。这可不是说说而已。换句话说,由于 ES 模块强制你在模块顶部声明 import 语句,打包器可以快速理解你的依赖关系树。理解了依赖关系树后,它就能找出哪些代码未被使用,并将其从打包文件中移除。这被称为Tree Shaking 或 Dead Code Elimination。
有一个关于动态导入的第三阶段提案,它将允许您通过 import() 有条件地加载模块。
我希望深入了解 JavaScript 模块的历史不仅能帮助您更好地欣赏 ES 模块,还能更好地理解它们的设计决策。
这最初发布在ui.dev上,是其高级 JavaScript课程的一部分。
文章来源:https://dev.to/tylermcginnis/javascript-modules-from-iifes-to-commonjs-to-es6-modules-33p1
后端开发教程 - Java、Spring Boot 实战 - msg200.com
