本文转载自微信公众号「新钛云服」,作者方章和 翻译 。转载本文请联系新钛云服公众号。
模块在每个编程语言中都能找到。它是一种能在一个代码块中引入另一个代码的模块功能的方法。这些模块是开发人员开发的具有特定功能的代码,可以在项目其他地方重复使用。模块化为你提供了一些好处,比如代码的可复用性和模块化。
如果你之前一直在使用JavaScript开发代码,你就会知道早期的JavaScript没有这样的模块功能。开发者为了将js文件加载到他们的页面中,不得不使用HTML <script>标签。直到后来,几种模块定义规范才开始出现。
- CommonJS — Node.js中使用的module.export和require语法
- Asynchronous Module Definition (AMD)
- Universal Module Definition (UMD)
- ES Modules
首先,我们先来看下为什么需要这些模块化定义的规范
为什么我们需要模块化
每当你思考程序是如何工作的,其实它们所做的就是变量管理。它为变量赋值,修改变量,将两个变量组合在一起等等。但是当变量的数量随着你的应用程序的规模而增加时,你会发现你的代码管理起来会非常麻烦。
解决这个问题的方法是只需要考虑几个变量。JavaScript实现这一目标的方式被称为作用域。由于JavaScript中的作用域定义方式导致函数不能访问其他函数中定义的变量。虽然这使得你的变量不能被其他函数访问,但这又引起了另一个问题--你很难在不同函数之间共享变量。解决这个问题就是将它们定义在全局作用域上。
虽然这种方法可以解决问题,但并不推荐。你的脚本标签应该按照正确的顺序,你必须确保没有人改变这个顺序。如果顺序确实发生了变化,你的应用程序将抛出一个错误。这使得代码管理变得很棘手。你永远不知道什么会破坏什么。任何函数都可以访问,所以你不知道哪些函数依赖于哪些脚本。
另一个问题是,在该全局范围内的每一部分代码都可以被其它代码改变。这将允许恶意的和非恶意的代码访问甚至修改你的全局变量,无论是否有恶意。
所以需要引入模块来帮助克服这些问题。
模块化是如何优雅的解决这个问题的
模块可以让你更好地组织和管理变量和函数。通常情况下,属于同一功能的函数和变量会被放在一个模块中。这就把这些变量放到了模块作用域中。模块作用域可以用来在模块中的函数之间共享变量。
这也使得变量也可以为其他模块所用。他们可以明确的说哪些变量、类或函数应该对外部模块可用。这就是所谓的导出。一旦你有了一个导出,其他模块就可以明确地说它们依赖于该变量、类或函数。由于这种明确的关系,你将知道如果你删除一个模块,哪些模块将被破坏。
一旦你能够导入和导出变量和函数,你就可以更容易地将你的代码分割和分解成可以独立工作的代码块。你以后可以通过使用这些模块来构建你的应用程序,类似于用乐高积木来构建。
为了实现这个超级有用的功能,已经多次尝试用JavaScript添加模块功能。
现有的模块化系统
CommonJS
CommonJS是NodeJS一直在使用的。使用Node,你会得到CommonJS的module.exports和require并可以直接使用。但是,与Node不同的是,浏览器并不支持CommonJS。此外,CommonJS会同步加载模块,因此对于浏览器来说,它不是一个最佳的解决方案。你可以使用Webpack或Browserify等打包程序来解决这个问题。
- // filename: bar.js
- // dependencies
- var $ = require('jquery');
- // methods
- function myFunction(){};
- // exposed public method (single)
- module.exports = myFunction;
Asynchronous Module Definition (AMD)
AMD诞生于一群不喜欢CommonJS发展方向的开发者。事实上,AMD在发展初期就从CommonJS中分裂出来了。AMD和CommonJS的主要区别在于AMD是异步加载模块的。这在浏览器中非常受欢迎,因为启动时间对于良好的用户体验至关重要。
- // filename: bar.js
- define(['jquery'], function ($) {
- // methods
- function myFunction(){};
- // exposed public methods
- return myFunction;
- });
由于CommonJS和AMD在各自的领域相当流行,所以需要一个 "通用 "的模式来支持两种风格。但事实证明,UMD又乱又丑。虽然它确实同时支持AMD和CommonJS,也支持老式的 "全局 "变量定义。
Universal Module Definition (UMD)
什么是ES模块
众所周知,JavaScript缺乏一个标准模块定义规范。因此,在ES6中提出了一个单一的、原生的模块标准。import和export指令允许程序在不运行代码的情况下导入导出,从而建立一个完整的模块依赖关系。
其语法格式还是简单好用的,并且兼容浏览器中的同步和异步操作模式。ES模块在浏览器中很快就可以使用,但在Node.js中,要想出一个向后兼容并能实现增量升级的解决方案就有点难了。在Node.js中,原生的ES模块在实验性模块标志后面长期可用。
下面以ES6模块为例。
JavaScript
- //------ library.js ------
- export const sqrt = Math.sqrt;
- export function square(x) {
- return x * x;
- }
- export function diagonal(x, y) {
- return sqrt(square(x) + square(y));
- }
- //------ main.js ------
- import { square, diagonal } from 'library';
- console.log(square(13)); // 169
- console.log(diagonal(12, 5)); // 13
- const app = document.getElementById("app");
- app.innerHTML = "<h1>Demo App for ES Modules</h1>";
- const input = document.getElementById("num");
- input.addEventListener("change",displaySquare);
- function displaySquare(){
- var sqrOutput = document.getElementById("sqr");
- sqrOutput.value = square(input.value);
- }
HTML
- <HTML>
- <head>
- <title>ES Modules Demo</title>
- </head>
- <body>
- <script type="module" src="./main.js" ></script>
- <div id="app"></div>
- <label>Input</label>
- <input id="num" type="number" placeholder="Enter number here"/>
- <br>
- <label>Output</label>
- <input id="sqr" type="number" disabled style="margin- top:20px"/>
- </body>
- </HTML>
如上图所示,在HTML文件中,你需要在脚本标签中指定type="模块",浏览器才会将其视为ECMAScript模块。
兼容性
为了向后兼容,你可以在脚本标签中包含nomodule(其中加载的JS文件是单个打包文件)。支持ES模块的浏览器会知道忽略这一点。这个解决方案即使在最老的浏览器中也能使用。Willem的回答已经很好的解释了这个问题。
在上面的方案中,我们会在HTML中加入这样的内容。
- <script type="module" src="./main.js" >
- </script>
- <script nomodule src="./fallback.js" >
- </script>
如果你是在本地测试,你将需要在服务器上运行这个,因为你会遇到CORS问题。请在这里阅读更多信息。模块以绝对或相对引用导入,必须以"/"、"./"或"./"开头。
注意:
动态导入
最新的ES2020版本确实带有动态导入功能。要动态导入模块,导入关键字可以作为函数调用。当以这种方式使用时,它会返回一个promise。
- import('/modules/library.js')
- .then((module) => {
- // Do something with the module.
- });
- //or using await
- let module = await import('/modules/library.js');
关于es模块的详细兼容性可以参考这里 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#import以及https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#export
您是否应该选择使用ES模块?
对于浏览器来说,ES模块是新的标准。可以开箱即用的异步模块加载功能,你可以获得更快的启动时间以更好的性能。虽然您可以在浏览器中使用CommonJS与一些额外的插件,但强烈建议您切换到ES模块,因为它们是浏览器中的原生模块。
ES原生模块允许您获得单个模块的加载,而不是单个打包文件。这是相当有用的,它减少了加载数据的大小。浏览器对原生模块的兼容性也很重要,因为它决定了原生的ES模块是否会被实现,或者我们是否会回退到我们的模式,它将加载一个单一的文件。当你得到一个单一的bundle文件时,其中一个问题是,当你的应用程序变得更大时,bundle js文件的大小也会增加,从而影响启动时间和性能。你可以通过使用代码拆分来避免这个问题,这是现代打包器(如webpack)中的一个功能。但在某些情况下,我们可能会选择模块打包器,如webpack而不是ES模块。如果你有CSS、图像、字体等资产,甚至是XML、CSV等数据文件,你可能会选择webpack解决方案,因为webpack提供了文件打包功能。
你还应该考虑到浏览器对HTTP2的支持。当你使用本地模块时,你的浏览器会单独加载这些模块。但在HTTP2的帮助下,我们可以用一个连接同时服务多个请求,而不是发送多个HTTP请求。根据CanIUse的数据,96.49%的浏览器使用HTTP2。
但是当你开发一个应用程序时,即使是剩下的3.51%也应该满足,那么你可能会想改用webpack。这是因为如果你坚持使用原生的ES模块,你的应用程序将需要发送几个HTTP请求来加载每个单独的模块。
在Node中,情况就完全不同了。由于该功能仍被标记为实验性的,所以你最好坚持使用CommonJS。不过你还是可以尝试一下ES模块。你可以在这里查看上面例子的源代码。你也可以在这里查看实时演示。我希望你明白什么是ES模块,以及为什么需要它们。