几乎每一种语言都有一个概念的模块——包括功能在一个文件在另一个声明。通常,开发人员封装了一段代码负责处理相关的任务,供其它应用程序或其他模块引用。

好处:

1、代码可以分裂成更小的独立功能的文件。

2、模块可以在多个应用程序之间共享。

3、理想情况下,一个模块公开出来代表以及经过验证过的。

4、解决命名冲突

JavaScript中的模块究竟是什么?

几年前有人开始web开发会震惊地发现没有JavaScript模块的概念。

因此,开发者采取一些替代方案:

script 标签

早期的写法:

<script src="lib1.js"></script>
<script src="lib2.js"></script>
<script src="core.js"></script>
<script>
console.log('inline code');
</script>

然而,这不是一个理想的解决方案:

1、每个脚本需要建立一个新的HTTP请求,影响页面的性能。HTTP/2在一定程度上缓解这个问题,但这并不能帮助脚本引用CDN等其他领域

2、每一个脚本运行时会阻止页面的渲染,直到脚本执行完成。

3、依赖关系管理是一个手动过程。在上面的代码中,如果lib1.js 引用了lib2.js代码。js代码可能会失败,如果它加载失败。这可能进一步中断javascript线程。

4、早期的JavaScript库是臭名昭著的全局变量污染。

Script combo

将所有JavaScript文件合并到一个大文件。这解决了一些性能和依赖关系管理问题,但也带来了一些新的问题:

1、需要引入构建流程

2、线上不好调试

ES Module

业界常用的模块规范:

  • CommonJS: Node.js 中使用module.exports和require语法
  • AMD(Asynchronous Module Definition)
  • UMD(Universal Module Definition)

ES2015推出了原生模块标准:

在默认情况下ES6模块内所有东西都是私有的,  运行在严格模式下(但不需要'use strict')。通过 exports 导出变量,函数或类。

// 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 };

main.js 引入lib.js需要使用import:

// main.js
import { sum } from './lib.js';

console.log( sum(1,2,3,4) ); // 10
相对路径采用./或../; 绝对路径以/开头

为防止命名冲突,可以引入别名:

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

浏览器中使用ES6模块

我们可以在这里查看浏览器对ES6模块的支持情况。

标准写法如下:

<script type="module" src="./main.js"></script>

内联写法:

<script type="module">
  import { something } from './somewhere.js';
  // ...
</script>

不管模块被引用多少次,最终都只会编译一次。

服务器设置

MIME类型必须是:  application/javascript;

模块文件在跨域引用的情况下开启CORS:

Access-Control-Allow-Origin: *

模块延迟执行

defer 会在document下载和解析完成后才执行:

<!-- 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>

尽管浏览器的发展迭代速度很快,但是使用ES Module还是太早了。目前, 最好的方案是使用打包工具将我们的ES6代码转成成ES5代码,这样就能在主流的浏览器运行。

Node中的应用

CommonJS提供类似的方式写一个ES2015模块。

module.exports :

// 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来导入模块:

const { sum, mult } = require('./lib.js');

console.log( sum(1,2,3,4) );  // 10
console.log( mult(1,2,3,4) ); // 24

而CommonJS和ES6模块语法相似, 他们是完全不同的工作方式:

1、ES6在编译阶段即完成所有模块导入,提升运行时性能。

2、CommonJS模块在执行代码时根据需要加载依赖项。

我们分别用两种模块方案实现上面的例子:

// 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';

输出结果:

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

执行顺序在某些应用中是很重要的,如果在一个应用中,混合了两种模块化写法不可预测的。Node.js运行在.mjs中使用ES2015,.js中使用CommonJS。这会降低开发的复杂度,也有助于代码检查和编辑器处理。

ES6模块仅适用于Node.js v10以后的版本(2018年4月发布)。老项目改用ES6 Module不太可能带来任何好处,而且会使应用程序与Node.js的早期版本不兼容。

对于新项目,ES6模块提供了CommonJS的替代方案。语法与客户端编码相同,并且可能提供一种更容易的同构JavaScript路由,这种JavaScript可以在浏览器或服务器上运行。

模块之争

一个标准化的JavaScript模块系统需要花许多年,甚至更长的时间才能实现。所有主流浏览器和Node.js从2018年年中开始支持ES6模块,虽然在每个人都升级的时候应该会有一个切换延迟。

今天学习ES6模块,为明天的JavaScript开发做准备。