什么是模块化
对于一个复杂的应用程序,将其所有代码按照一定的规则拆分到几个互相独立的文件中。这些文件通过对外暴露一些数据或调用方法,与外部完成整合。
这样一来,每个文件彼此独立,开发者更容易开发和维护代码,模块之间又能够互相调用和通信。
早期的模块化
通过立即执行函数(IIFE) ,我们构造一个私有作用域,再通过闭包,将需要对外暴露的数据和接口输出,我们称之为 IIFE 模式。
const module = (function() {
let foo = 'bar '
const fnl = function() {
// ...
}
const fn2 = function fn2 () {
// ...
}
return {
fn1,
fn2
}
})()
ES模块
静态特性
ES 模块的设计思想是尽量静态化,这样能保证在编译时就确定模块之间的依赖关系,每个模块的输入和输出都是确定的。
将 ES 模块设计成静态的, 一个明显的优势是,通过静态分析,即通过分析作用域,推导出变量和导入依赖变量之间的引用关系。如果导入的变量没有被引用,我们便可以通过 tree shaking 手段减少代码体积,进而提升 web 加载速度。
与其他模块方案的区别
CommonJS 和 AMD 模块无法保证在编译时就确定这些内容,它们都只能在运行时确定。这也是 ES 模块和其他模块规范最显著的差别。
第二个差别在于, CommonJS 模块输出的是一个值的拷贝, ES 模块输出的是值的引用。
在浏览器中快速使用 ES 模块
只需要在 script 标签上添加 type="module” 属性。
模块代码仅在第一次导入时被解析
假设一个模块导出了一个对象:
// 📁 admin.js
export let admin = {
name: "John"
};
如果这个模块被导入到多个文件中,模块仅在第一次被导入时被解析,并创建 admin 对象,然后将其传入到所有的导入。
// 📁 1.js
import { admin } from './admin.js';
admin.name = "Pete";
// 📁 2.js
import { admin } from './admin.js';
alert(admin.name); // Pete
// 1.js 和 2.js 引用的是同一个 admin 对象
// 在 1.js 中对对象做的更改,在 2.js 中也是可见的
因此,顶层模块代码应该用于初始化,创建模块特定的内部数据结构。如果我们需要多次调用某些东西 —— 我们应该将其以函数的形式导出。
模块语法
export
- 直接导出
// 导出变量
export let months = 'Mar';
- 统一导出
// 📁 say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
export {sayHi, sayBye}; // 导出变量列表
- 导出时命名 export as
// 📁 say.js
...
export {sayHi as hi, sayBye as bye};
- 默认导出 export default
// 📁 user.js
export default class User { // 只需要添加 "default" 即可
constructor(name) {
this.name = name;
}
}
每个文件只有一个 export default,并且将其导入不再需要花括号:
// 📁 main.js
import User from './user.js'; // 不需要花括号 {User},只需要写成 User 即可
new User('John');
这里建议减少使用 export default 导出,一方面是因为 export default 会导出整体对象结果,不利于通过 tree shaking 进行分析;另一方面是因为 export default 导出的结果可以随意命名变量,不利于团队统一管理。
import
- 直接导入
把要导入的东西列在花括号 import {...} 中:
// 📁 main.js
import {sayHi, sayBye} from './say.js';
sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!
- 导入所有
使用 import * as <obj> 将所有内容导入为一个对象,例如:
// 📁 main.js
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
- 命名导入 import as
// 📁 main.js
import {sayHi as hi, sayBye as bye} from './say.js';
hi('John'); // Hello, John!
bye('John'); // Bye, John!
动态导入
import('./say.js') 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式。
例如,如果我们有以下模块 say.js:
// 📁 say.js
export function hi() {
alert(`Hello`);
}
export function bye() {
alert(`Bye`);
}
那么,可以想像下面这样进行动态导入:
let {hi, bye} = await import('./say.js');
hi();
bye();
上面这段代码在使用 webpack 进行构建时,会打包到两个 bundle 中。
事实上,webpack 的优化之一:代码分割和上面这种异步加载本质上都是构建时进行代码分割,再在必要时进行加载。