ES6模块简介
本文主要介绍ES6模块相关知识,包括模块简介,导入导出等。
什么是模块
一个模块就是一个文件,一个脚本就是一个模块。
模块可以相互加载,通过export和import指令来交换功能,从而可以从一个模块调用另外一个模块的功能。
- export 关键字标记了可以从当前模块外部访问的变量和函数。
- import 关键字允许从其他模块导入功能。
示例
// 📁 say.js
function sayHello(user) {
alert("Hello, "+user+"!");
}
function sayBye(user) {
alert("Bye, "+user+"!");
}
// 模块导出变量列表
export {sayHello, sayBye};
// 📁 app.js
// 模块导入
import {sayHello} from './say.js';
//使用
sayHello("zht");
模块特性
- 始终使用 “use strict”严格模式。
- 模块的作用域相关独立,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。
- 模块代码仅在第一次导入时被解析运行。
示例1
// 📁 run_only_once.js alert("Run only once!");
// 在不同的文件中导入相同的模块 // 📁 app1.js import `./run_only_once.js`; // Run only once! // 📁 app2.js import `./run_only_once.js`; // (什么都不显示)
示例2
// 📁 admin.js export let admin = { name:"" }; export function sayHello() { alert(`Hello, ${admin.name}!`); }
// app1.js 和 app2.js 导入的是同一个对象 // 📁 app1.js import {admin} from './admin.js'; admin.name = "zht"; // 📁 app2.js import {admin} from './admin.js'; alert(admin.name); // zht
我们可以在首次导入时设置模块。我们只需要设置其属性一次,然后在进一步的导入中就都可以直接使用了。
// 📁 init.js import {admin} from './admin.js'; admin.name = "zht";
另一个模块也可以看到 admin.name
// 📁 other.js import {admin,sayHello} from './admin.js'; alert(admin.name);//zht; sayHello();//Hello, zht;
- 在一个模块中,“this” 是 undefined
在浏览器中使用模块
浏览器可以通过 <script type="module">标识的脚本来使用模块(可用于测试,一般不会用于生产环境)。
模块脚本特性
- 模块脚本是延迟的,与defer对外部脚本和内联脚本的影响相同。
- 下载外部模块脚本 <script type="module" src="..."> 不会阻塞 HTML 的处理,它们会与其他资源并行加载。
- 模块脚本会等到 HTML 文档完全准备就绪(即使它们很小并且比 HTML 加载速度更快),然后才会运行。
- 保持脚本的相对顺序:在文档中排在前面的脚本先执行。
- Async适用于内联脚本(inline script)。
对于非模块脚本,async 特性(attribute)仅适用于外部脚本。
- 具有相同 src 的外部脚本仅运行一次。
- 不允许裸模块
在浏览器中,import 必须给出相对或绝对的 URL 路径。没有任何路径的模块被称为裸模块。在 import 中不允许这种模块。
某些环境(例如Node.js/Webpack),允许没有任何路径的裸模块,因为它们有自己的查找模块的方法和钩子(hook)来对它们进行微调。但是浏览器尚不支持裸模块。
构建工具
在实际开发中,浏览器模块很少被以“原始”形式进行使用。通常,我们会使用一些特殊工具,例如 Webpack,将它们打包在一起,然后部署到生产环境的服务器。
构建流程
- 从HTML中的 <script type="module"> “主”模块开始。
- 分析它的依赖:它的导入,以及它的导入的导入等。
- 使用所有模块构建一个文件(或者多个文件,这是可调的),并用打包函数替代原生的 import 调用,以使其正常工作。还支持像 HTML/CSS 模块等“特殊”的模块类型。
- 在处理过程中,可能会应用其他转换和优化。
- 删除未访问的代码
- 删除未使用的导出
- 删除特定于开发的像 console 和 debugger 这样的语句。
- 可以使用 Babel 将前沿的现代的 JavaScript 语法转换为具有类似功能的旧的 JavaScript 语法。
- 压缩生成的文件(删除空格,用短的名字替换变量等)。
模块导出
有如下导出方式
- 在声明一个 class/function/… 之前
//export [default] class/function/variable ... // 导出数组 export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // 导出 const 声明的变量 export const MODULES_BECAME_STANDARD_YEAR = 2015; // 导出类 export class User { constructor(name) { this.name = name; } }
- 导出与声明分开
// 📁 say.js function sayHello(user) { alert("Hello, "+user+"!"); } function sayBye(user) { alert("Bye, "+user+"!"); } export {sayHello, sayBye}; // 导出变量列表
从技术上讲,我们也可以把 export 放在函数上面。
- 通过as对导出重命名
export {sayHello as hello, sayBye as bye};
import {hello, bye} from './say.js'; hello("zht");//Hello, zht! bye("zht");//Bye, zht!
- 默认导出
模块提供了一个特殊的默认导出export default语法,用来标识一个模块只做一件事。
有两种默认导出方式
- export default
// 📁 user.js export default class User { // 只需要添加 "default" 即可 constructor(name) { this.name = name; } }
- export {sayHello as default};
function sayHello(user) { alert(`Hello, ${user}!`); } // 就像我们在函数之前添加了 "export default" 一样 export {sayHello as default};
默认导出和一般的命名导出,有以下区别
- 默认的导出,导入时不需要花括号。
import User from './user.js'; // 不需要花括号 {User},只需要写成 User 即可 new User('zht');
- 每个文件只能有一个默认导出,而命名导出可以有多个。
- 默认导出可以没有名称,而命名导出必须要有名称。
export default class { // 没有类名 constructor() { ... } }
export default function(user) { // 没有函数名 alert(`Hello, ${user}!`); }
// 导出单个值,而不使用变量 export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
如果不用default,以下导出会报错
export class { // Error!(非默认的导出需要名称) constructor() {} }
- 默认导出,导入时可以自由选择名称,而命名导出不能选择名称
//export default class User... import User from './user.js'; // 有效 import MyUser from './user.js'; // 也有效
//export class User... import {User} from './user.js'; // 有效 import {MyUser} from './user.js'; // 无效
一个模块默认导出和命名导出可同时存在。
// 📁 user.js export default class User { constructor(name) { this.name = name; } } export function sayHello(user) { alert(`Hello, ${user}!`); }
导入默认的导出以及命名的导出的方法如下
import {default as User, sayHello} from './user.js'; new User('zht');
- export default
- 重新导出(Re-export)
有两种方式实现模块的重新导出
- import & export
// 导入sayHello 然后立即导出它们 import {sayHello, sayBye} from './say.js'; export {sayHello, sayBye}; // 将默认导出导入为 User,然后导出它 import User from './user.js'; export {User};
- export ... from ...
export {sayHello, sayBye} from './say.js'; // 重新导出 sayHello和sayBye export {default as User} from './user.js'; // 将user默认导出以User重新导出
export ... from ...是以上import & export的简写
重新导出可用于组织代码和对外公开功能,通过一个主文件index.js可对外公开整个package(内部包含大量模块文件)中各个子模块的功能。
- import & export
- 重新导出默认导出
重新导出时,默认导出需要单独处理。
// user.js export default class User { // ... }
//export default class User... export * from './user.js'; // 重新导出只导出了命名的导出,但是忽略了默认的导出。 export {default} from './user.js'; // 重新导出默认的导出 export User from './user.js'; // 无效,必须明确写出 export {default as User}
模块导入
有如下导入方式
- 导入的东西列在花括号 import {...} 中
import {sayHello,sayBye} from './say.js'; sayHello('zht');
- 将所有内容导入为一个对象
import * as say from './say.js'; say.sayHello('zht'); say.sayBye('zht');
这种全部导入的方式看起来比较便捷,但是明确导入的内容更利于优化。
- 构建工具(Webpack)将模块打包到一起时,会对其进行优化,并从打包好的代码中删除那些未被使用的函数,从而使构建更小,而采用全部导入的方式则不会有此精简操作。
- 明确列出要导入的内容会使得名称较短:sayHello() 而不是 say.sayHello()。
- 利于代码重构。
- 通过as对导入重命名
import {sayHello as hello, sayBye as bye} from './say.js'; import {default as User} from './user.js';//默认导出重命名 hello('zht'); // Hello, zht! bye('zht'); // Bye, zht!
模块动态导入
请注意,import/export必须放在顶层代码块中,在 {...} 中的 import/export 语句无效。
以下代码是无效的。
if (something) {
import {sayHello} from "./say.js"; // 异常: 导入必须在顶层代码块中
}
如果需要动态地按需导入模块的话,可以通过import() 表达式。
import(module) 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式。
例如:
let modulePath = prompt("Which module to load?");
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, e.g. if no such module>)
在异步函数中,我们可以使用 let module = await import(modulePath)。
例如:
// 📁 say.js
export function hello() {
alert(`Hello`);
}
export function bye() {
alert(`Bye`);
}
export default function() {//默认导出
alert("Module loaded (export default)!");
}
async function load() {
let say = await import('./say.js');
say.hello(); // Hello!
say.bye(); // Bye!
say.default(); // Module loaded (export default)! 模块对象的 default 属性使用默认导出
}
动态导入在浏览器常规脚本中工作,不需要 script type="module"特性。
参考资料: Modules, introduction