一句话总结:模块是一个JavaScript文件,文件名就是模块名,export定义了调用方能从模块中获得什么(变量,函数,类),调用方通过import(或require)引入要用到的内容。
本文探讨了ES6模块,展示了如何在转换器(transpiler)的帮助下使用它们。
几乎每种语言都有模块概念——将声明的功能包含在一个文件中的方法。通常,开发人员创建一个封装的代码库,负责处理相关任务。该库可以由应用程序或其他模块引用。
好处:
- 代码可以拆分为自包含功能的较小文件。
- 可以在任意数量的应用程序中共享相同的模块。
3 .理想情况下,模块不需要由另一个开发人员进行检查,因为它们已被证明有效。 - 引用模块的代码理解它是一种依赖。如果更改或移动模块文件,问题立即显而易见。
- 模块代码(通常)有助于消除命名冲突。模块1中的函数x()不能与模块2中的函数x()发生冲突。使用命名空间等选项,将调用变为module1.x()和module2.x()。
JavaScript中的模块在哪里?
几年前开始进行Web开发的任何人都会惊讶地发现JavaScript中没有模块的概念。无法直接引用或包含一个JavaScript文件。因此,开发人员采用了替代方案。
多个HTML <script>标签
HTML可以使用多个<script>标记加载任意数量的JavaScript文件:
<script src="lib1.js"></script>
<script src="lib2.js"></script>
<script src="core.js"></script>
<script>
console.log('inline code');
</script>
2018年平均每个网页使用了25个单独的脚本,但它不是一个实用的解决方案:
- 每个脚本都会启动一个新的HTTP请求,这会影响页面性能。HTTP/2在某种程度上缓解了这个问题,但它无法帮助其他域(如CDN)上引用的脚本。
- 每个脚本在运行时都会停止进一步处理。
- 依赖管理是一个手动过程。在上面的代码中,如果
lib1.js
引用了代码lib2.js
,代码将因为尚未加载而失败。这可能会打破进一步的JavaScript处理。 - 除非使用适当的模块模式,否则函数可以覆盖其早期的JavaScript库因使用全局函数名称或重写本机方法而臭名昭着。
脚本连接
解决多个<script>
标签问题的一种方法是将所有JavaScript文件连接成一个大的文件。这解决了一些性能和依赖关系管理问题,但它可能会导致手动构建和测试步骤。
模块装载机
诸如RequireJS和SystemJS之类的系统提供了一个库,用于在运行时加载和命名其他JavaScript库。需要时,使用Ajax方法加载模块。这些系统有所帮助,但对于较大的代码库或将标准<script>
标签添加到组合中的站点而言可能会变得复杂。
模块捆绑器,预处理器和Transpilers
Bundlers引入了编译步骤,因此在构建时生成JavaScript代码。处理代码以包含依赖项并生成单个ES5跨浏览器兼容的连接文件。流行的选项包括Babel,Browserify,webpack和更多一般任务运行者,如Grunt和Gulp。
JavaScript构建过程需要一些投入,但也收获好处:
- 处理是自动化的,因此人为错误的可能性较小。
- 进一步处理可以lint代码,删除调试命令,缩小生成的文件等。
- Transpiling允许你使用其他语法,如TypeScript或CoffeeScript。
ES6模块
上述选项引入了各种竞争模块定义格式。广泛采用的语法包括:
- CommonJS - Node.js中使用的语法module.exports和require语法
- 异步模块定义(AMD)
- 通用模块定义(UMD)
ES6(ES2015)中提出了单一的本机模块标准。
默认情况下,ES6模块中的所有内容都是私有的,并且以严格模式运行(不需要'use strict')。使用export
公开变量,函数和类。例如:
// lib.js
export const PI = 3.1415926;
export function sum(...args) {
log('sum', args);
return args.reduce((num, tot) => tot + num);
}
export function mult(...args) {
log('mult', args);
return args.reduce((num, tot) => tot * num);
}
// private function
function log(...msg) {
console.log(...msg);
}
或者,用单条export
语句的形式。例如:
// lib.js
const PI = 3.1415926;
function sum(...args) {
log('sum', args);
return args.reduce((num, tot) => tot + num);
}
function mult(...args) {
log('mult', args);
return args.reduce((num, tot) => tot * num);
}
// private function
function log(...msg) {
console.log(...msg);
}
export { PI, sum, mult };
然后用import
将项目从模块拉到另一个脚本或模块(要用花括号):
// main.js
import { sum } from './lib.js';
console.log( sum(1,2,3,4) ); // 10
这个例子中,lib.js与main.js在同一个文件夹。也可以使用绝对文件引用(以...开头/),相对文件引用(起始./或../)或完整URL。
一次可以导入多个项目:
import { sum, mult } from './lib.js';
console.log( sum(1,2,3,4) ); // 10
console.log( mult(1,2,3,4) ); // 24
导入时设置别名以解决命名冲突:
import { sum as addAll, mult as multiplyAll } from './lib.js';
console.log( addAll(1,2,3,4) ); // 10
console.log( multiplyAll(1,2,3,4) ); // 24
可以通过提供命名空间来导入所有公共项:
import * as lib from './lib.js';
console.log( lib.PI ); // 3.1415926
console.log( lib.add(1,2,3,4) ); // 10
console.log( lib.mult(1,2,3,4) ); // 24
导出模块由命名和默认(default)两种方式,前面都是命名方式。命名方式要求引入的时候必须要通过相同的命名进行引入,但是默认的没有要求。
// file test.js
let k; export default k = 12;
// some other file
import m from './test'; // 用变量m替代了变量k
console.log(m);
在浏览器中使用ES6模块
在撰写本文时,基于Chromium的浏览器(v63 +),Safari 11+和Edge 16+ 支持ES6模块。Firefox支持将在版本60中支持(它about:config
在v58 + 的标志后面)。
必须通过type="module"
在<script>
标记中设置属性来加载使用模块的脚本。例如:
<script type="module" src="./main.js"></script>
或内联:
<script type="module">
import { something } from './somewhere.js';
// ...
</script>
无论它们在页面或其他模块中引用了多少次,模块只被解析一次。
服务器端注意事项
必须使用MIME类型提供模块application/javascript
。大多数服务器会自动执行此操作,但要警惕动态生成的脚本或.mjs
文件(请参阅下面的Node.js部分)。
常规<script>
标记可以在其他域上获取脚本,但使用跨源资源共享(CORS)获取模块。因此,不同域上的模块必须设置适当的HTTP标头,例如Access-Control-Allow-Origin: *
。
最后,除非将crossorigin="use-credentials"
属性添加到<script>
标记并且响应包含标题,否则模块将不会发送cookie或其他标头凭据Access-Control-Allow-Credentials: true
。
模块执行推迟
<script defer>
属性会延迟脚本执行,直到文档加载并解析为止。模块(包括内联脚本)默认延迟。例:
<!-- runs SECOND -->
<script type="module">
// do something...
</script>
<!-- runs THIRD -->
<script defer src="c.js"></script>
<!-- runs FIRST -->
<script src="a.js"></script>
<!-- runs FOURTH -->
<script type="module" src="b.js"></script>
模块后备
不支持模块的浏览器不会运行type="module"脚本。可以通过浏览器的nomodule
属性给模块提供后备脚本。例如:
<script type="module" src="runs-if-module-supported.js"></script>
<script nomodule src="runs-if-module-not-supported.js"></script>
现在应该在浏览器中使用模块吗?(文章是18年4月写的)
浏览器支持正在增长,但转换到ES6模块可能还为时过早。目前,使用模块捆绑器创建一个可在任何地方工作的脚本可能更好。
在Node.js中使用ES6模块
当Node.js在2009年发布时,不能提供模块是不可想象的。它采用了CommonJS,这意味着可以开发Node包管理器npm。从那时起,使用量呈指数级增长。
CommonJS模块的编码方式与ES2015模块类似。使用module.exports
而不是export
:
// lib.js
const PI = 3.1415926;
function sum(...args) {
log('sum', args);
return args.reduce((num, tot) => tot + num);
}
function mult(...args) {
log('mult', args);
return args.reduce((num, tot) => tot * num);
}
// private function
function log(...msg) {
console.log(...msg);
}
module.exports = { PI, sum, mult };
用require
(而不是import)将模块拉入另一个脚本或模块:
const { sum, mult } = require('./lib.js');
console.log( sum(1,2,3,4) ); // 10
console.log( mult(1,2,3,4) ); // 24
require
还可以导入所有项目:
const lib = require('./lib.js');
console.log( lib.PI ); // 3.1415926
console.log( lib.add(1,2,3,4) ); // 10
console.log( lib.mult(1,2,3,4) ); // 24
ES6模块很容易在Node.js中实现吗?不。
ES6模块位于Node.js 9.8.0+中的标志后面,并且至少在版本10之前不会完全实现。虽然CommonJS和ES6模块具有相似的语法,但它们的工作方式基本不同:
- ES6模块已预先解析,以便在执行代码之前解析进一步的导入。
- CommonJS模块在执行代码时根据需要加载依赖项。
它在上面的示例中没有任何区别,但请考虑以下ES2015模块代码:
// ES2015 modules
// ---------------------------------
// one.js
console.log('running one.js');
import { hello } from './two.js';
console.log(hello);
// ---------------------------------
// two.js
console.log('running two.js');
export const hello = 'Hello from two.js';
ES2015的输出:
running two.js
running one.js
hello from two.js
使用CommonJS编写的类似代码:
// CommonJS modules
// ---------------------------------
// one.js
console.log('running one.js');
const hello = require('./two.js');
console.log(hello);
// ---------------------------------
// two.js
console.log('running two.js');
module.exports = 'Hello from two.js';
CommonJS的输出:
running one.js
running two.js
hello from two.js
执行顺序在某些应用程序中可能至关重要,如果ES2015和CommonJS模块混合在同一个文件中会发生什么?要解决此问题,Node.js将仅允许具有扩展名.mjs文件中的ES6模块。带.js扩展名的文件默认为CommonJS。这是一个简单的选项,它消除了很多复杂性,应该有助于编码和编辑。
你应该在Node.js中使用ES6模块吗?
ES6模块仅适用于Node.js v10以后(2018年4月发布)。转换现有项目不太可能带来任何好处,并且会使应用程序与早期版本的Node.js不兼容。
对于新项目,ES6模块提供了CommonJS的替代方案。语法与客户端编码相同,可以提供更容易的同构JavaScript路径,可以在浏览器或服务器上运行。
模块混战
标准化的JavaScript模块系统需要很多年才能到达,甚至更长时间才能实现,但问题已得到纠正。所有主流浏览器和2018年中期的Node.js都支持ES6模块,尽管在每个人都升级时应该会出现切换延迟。
立即学习ES6模块,为明天的JavaScript开发做好准备。