# 16. Moudule模块
ES6之前,js没有module体系。社区制定了一些模块的加载方案,最主要有两种
- 用于服务器:CommonJS (node里面 module.exports 与 require)
- 用于浏览器:AMD (浏览器引入 require.js,define定义,require引入) http://javascript.ruanyifeng.com/tool/requirejs.html
ES6实现了模块功能,完全可以取代CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案
// CommonJS模块require
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
ES6 模块编译时就能确定模块的依赖关系,及输入输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。 ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
# export 命令
模块功能主要由两个命令构成:export和import。
- export命令 用于规定模块对外接口
- import命令 用于输入其他模块提供的功能 一个模块就是一个独立的文件,该文件内部所有变量,外部无法获取。如果希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
export function multiply(x, y) {
return x * y
}
// 可以使用as关键字
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
注意不能直接 export 变量或函数,需要用 {}结构,或者直接在定义(初始化)时导出
export 1; // 报错
// 报错
var m = 1
export m
// 正确写法
export var m = 1
// 正确写法2
var m = 1
epxort {m}
// 正确写法3
var n = 1;
export {n as m}
// 导出函数
function f() {}
export f // 报错
// 正确写法
export function f() {}
// 正确写法2
function f() {}
exporf {f}
注意事项
// 1. export输出的接口,与对应的值时动态绑定关系。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// 上面代码输出变量foo,值为bar,500 毫秒之后变成baz。
// 2. export 不能放到函数里面。
function foo() {
export default 'bar' // SyntaxError
}
foo()
# import 命令
// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
// improt里 as用法
import { lastName as surname } from './profile.js';
// 注意事项:
// 1. import的变量是只读的,不能直接赋值
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only; 错误
a.foo = 'hello'; // 合法操作
// 2.improt 会提升到整个模块的头部,首先执行
foo();
import { foo } from 'my_module';
// 3.只执行一次
import 'lodash';
import 'lodash';
// 4.
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
# 模块的整体加载
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
// main.js
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
// main.js 另一种导入方法
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
// 导入的circle不允许改变
import * as circle from './circle';
// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};
# export default 命令
// 1. 可以输出匿名函数 2. import时可以直接引入,不需要结构 {}
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
// 3. 输出非匿名函数也是可以的
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者写成
function foo() {
console.log('foo');
}
export default foo;
// 4. 本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
// 5.
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
// 6.
// 正确
export default 42;
// 报错
export 42;
// 7. export default也可以用来输出类。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
// 8. 同时输入默认方法和其他接口
// loadsh.js 如下
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
import _, { each, forEach } from 'lodash';
# export 与 import 的复合写法
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
// 2.
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
// 3.
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
# import() 函数
按需加载,运行到这一句,就会加载指定的模块。按需加载import() 返回一个Promise对象
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
适用场景
// 1. 按需加载
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
// 2. 条件加载
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
// 3.动态的模块路径,根据f()返回结果,加载不能的模块
import(f()).then(...);
// 4.
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),]).then(([module1, module2, module3]) => {
···
});
// 5.
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
# Moudle的加载实现
# 浏览器加载
<!-- 1. 传统方法 -->
<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
// module code
</script>
<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>
<!-- 异步加载 -->
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
<!-- defer是“渲染完再执行” -->
<!-- async是“下载完就执行” -->
ES6 type="module" 表示导入一个ES6 模块
<script type="module" src="./foo.js"></script>
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
<!-- async 也可以 -->
<script type="module" src="./foo.js" async></script>
<!-- ES6模块也允许内嵌在网页中 -->
<script type="module">
import utils from "./utils.js";
// other code
</script>
# ES6模块与CommonJS模块的差异
- CommonJS 模块输出的是一个值得拷贝,ES6模块输出的是值得引用
- CommonJS 模块是运行时加载,ES6模块是编译时输出接口
// CommonJS模块
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
// ES6 模块
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
# ES6模块加载CommonJS模块
import 加载 module.exports 导出的变量、函数
// a.js
module.exports = {
foo: 'hello',
bar: 'world'
};
// 等同于
export default {
foo: 'hello',
bar: 'world'
};
// 写法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法三
import * as baz from './a';
// baz = {
// get default() {return module.exports;},
// get foo() {return this.default.foo}.bind(baz),
// get bar() {return this.default.bar}.bind(baz)
// }
// b.js
module.exports = null;
// es.js
import foo from './b';
// foo = null;
import * as bar from './b';
// bar = { default:null };
// 不能这样写,因为类似于export default,不能结构赋值
// 不正确
import { readFile } from 'fs';
# CommonJS模块加载ES6模块
CommonJS模块加载ES模块,不能使用require,而要使用import()函数
// es.mjs
let foo = { bar: 'my-default' };
export default foo;
// cjs.js
const es_namespace = await import('./es.mjs');
// es_namespace = {
// get default() {
// ...
// }
// }
console.log(es_namespace.default);
// { bar:'my-default' }
// 例子2
// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};
// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get foo() {return foo;}
// get bar() {return foo;}
// get f() {return f;}
// get c() {return c;}
// }
# 模块循环加载
a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本,待后续研究...
// a.js
var b = require('js')
// b.js
var a = require('a')
CommonJS模块无论加载对少次,只会运行一次,以后再加载,都是第一次运行的结果,除非清除缓存 require命令第一次加载该脚本,就会执行整个脚本,在内存中生成一个对象, 以后需要用到这个模块时,就会到exports属性里面取值,