🪤JavaScript 的模块管理
# 关于 JavaScript 模块
随着 JavaScript 中的脚本应用代码越来越多,可以将它 拆分成多个文件(模块 Module)。一个模块可以包含用来 特定目的的类 或者 函数库。
# 不同的模块系统
AMD : 基于 require.js 库实现的模块系统。
CommonJS: 为 Node.js 服务器创建的模块系统。
UMD: 建议作为通用的模块系统,兼容 AMD 和 CommonJS。
# 模块
一个模块 (Module)就是一个文件。(一个脚本)模块之间可以互相加载,使用特殊的指令 export
和 import
交换功能,从一个模块调用另一个模块的 函数。
export
(导出):用来标记 从当前模块 外部可以访问的变量和函数。import
(引入):允许 从其他模块 导入变量和函数。
🌰 例子:
- 在
sayHi.js
文件中 导出 一个函数:
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
2
3
- 在另一个文件(例如
main.js
)导入并且使用这个函数:
import { sayHi } from './sayHi.js'
console.log(sayHi) // function sayHi ...
sayHi('John')
2
3
4
🌰 例子 / 在浏览器中导入模块
由于模块支持特殊的关键字和功能,因此必须要通过使用 <script type="module">
特性来告诉浏览器,此脚本应该被当作模块来对待。
( sayHi.js
同上)
- 在 HTML 页面文件中:
<!DOCTYPE html>
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>
2
3
4
5
6
浏览器会自动获取并解析导入的模块(如果需要,还可以分析该模块的导入),然后运行该脚本。
注意在浏览器打开
file
协议的 HTML 文件读取模块不起作用。
# 模块中的核心语法 / 功能
在模块中:
给未声明的变量赋值不能使用,将产生错误。
每个模块都有自己的 顶级作用域。即 一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。
但是,可以通过将变量显式地分配给
window
的一个属性,使其成为窗口级别的全局变量。例如window.user = "John"
。这样所有的脚本都会看到它,无论脚本是否带有type="module"
。避免全局变量的使用!模块代码仅在第一次导入时被解析。如果同一个模块被导入到多个其他位置,那么它的代码只会执行一次,即在第一次被导入时。将其导出的内容提供给进一步的导入。
🌰 例子:
- 在一个模块
alert.js
中,导出一个 JavaScript 语句:
console.log("Module is evaluated!")
1- 在不同的文件中导入相同的模块:
// 📁 1.js import `./alert.js`; // Module is evaluated! // 📁 2.js import `./alert.js`; // (什么都不显示)
1
2
3
4
5所以,一般 顶层模块代码应该用于初始化,创建模块特定的内部数据结构。如果需要多次调用某些东西,应该将其以函数的形式导出。
🌰 例子 / 模块导出对象:
// 📁 admin.js export let admin = { name: "John" };
1
2
3
4如果这个模块被导入到多个文件中,模块仅在 第一次被导入时被解析,并创建
admin
对象,然后将其传入到所有的导入。即,所有的导入获得的都是同一个唯一的
admin
对象:// 📁 1.js import { admin } from './admin.js'; admin.name = "Pete"; // 📁 2.js import { admin } from './admin.js'; alert(admin.name); // Pete
1
2
3
4
5
6
7当在一个脚本中导入一个模块对象,并修改它;在另一个脚本中可以看到该修改,因为共享的是同一个 模块对象。
🌰 例子:
模块可以提供需要配置的通用功能,可以导出一个配置对象,期望外部代码可以对其进行赋值。
这是经典的模块使用方式:
- 模块导出一些配置方法,例如一个配置对象。
- 在第一次导入时,对其进行初始化,写入其属性。可以在应用顶级脚本中进行此操作。
- 进一步地导入使用模块。
admin.js
模块可能提供了某些功能(例如身份验证),但希望凭证可以从模块之外赋值到config
对象:
// 📁 admin.js export let config = { }; export function sayHi() { alert(`Ready to serve, ${config.user}!`); }
1
2
3
4
5
6- 在
init.js
中,导入了config
并设置了 设置了config.user
:
import { config } from './admin.js'; config.user = "Pete";
1
2- 在另一个脚本中导入
config
:
// 📁 another.js import { sayHi } from './admin.js'; sayHi(); // Ready to serve, Pete!
1
2
3
4这时可以正确显示被配置的内容。
- 在一个模块
import.meta
:该对象包含关于当前模块的信息。它的内容取决于其所在的环境。在浏览器环境中,它包含当前脚本的 URL,或者如果它是在 HTML 中的话,则包含当前页面的 URL。
🌰 例子 / 获取当前 HTML 的 URL:
<script type="module"> alert(import.meta.url); </script>
1
2
3在一个模块中 顶级
this
的值为undefined
(对比来看,非模块的this
是 全局对象)🌰 例子:
<script> alert(this); // window </script> <script type="module"> alert(this); // undefined </script>
1
2
3
4
5
6
7
# 模块的 导出 / 导入
# 在声明之前导出
可以通过在声明之前放置 export
来标记 任意声明 为导出,无论声明的是变量,函数还是类都可以。
🌰 例子:
// 导出数组
export let months = ['Jan', ... ]
// 导出常量
export const STANDARD_YEAR = 2048;
// 导出类
export class User {
constructor(name) {
this.name = name
}
}
2
3
4
5
6
7
8
9
10
11
12
提示
** 导出 class
/ function
后没有分号。** 在类或者函数前的 export
不会让它们变成 函数表达式 (opens new window)。尽管被导出了,但它仍然是一个函数声明。大部分 JavaScript 样式指南都不建议在函数和类声明后使用分号。
# 声明之后导出
🌰 例子 / 先声明函数再导出:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
export {sayHi, sayBye};
2
3
4
5
6
7
8
9
一般最后的导出放在代码末尾,防止遗漏。
# 导入
通常导入:把要导入的东西列在花括号
import {...}
中。🌰 例子:
// 📁 main.js import {sayHi, sayBye} from './say.js'; sayHi('John'); sayBye('John');
1
2
3
4
5将导入的内容为一个 对象。
🌰 例子:
import * as say from './say.js'; say.sayHi('John'); say.sayBye('John');
1
2
3
4
提示
最好明确列出 要导入的内容。
现代的构建工具(webpack (opens new window) 和其他工具)将模块打包到一起并对其进行优化,以加快加载速度并删除未使用的代码。
优化器(optimizer)就会检测到导入,并从打包好的代码中删除那些未被使用的函数,从而使构建更小。这就是所谓的「摇树(tree-shaking)」。
明确列出要导入的内容会使得名称较短:
sayHi()
而不是say.sayHi()
。导入的显式列表可以更好地概述代码结构:使用的内容和位置。它使得代码支持重构,并且重构起来更容易。
# 别名
使用 as
让导入和导出具有不同的名字。
import as *
:🌰 例子:
import {sayHi as hi, sayBye as bye} from './say.js'; hi('John'); // Hello, John! bye('John');
1
2
3
4export as *
:🌰 例子:
export {sayHi as hi, sayBye as bye};
1在导入时使用别名。
# export default
实际情况中有两种模块:
- 包含库或函数包的模块。
- 声明 单个实体 的模块。
大部分情况下,开发者倾向于使用第二种方式,以便每个「东西」都存在于它自己的模块中。
文件具有良好的命名,并且文件夹结构得当,那么代码导航会变得更容易。
模块的 默认导出语法: export default
放在要导出的实体前。
🌰 例子:
- 默认导出一个类
User
:
// 📁 user.js
export default class User {
constructor(name) {
this.name = name;
}
}
2
3
4
5
6
- 每个文件可能只有一个
export default
,所以导入不用{}
:
// 📁 main.js
import User from './user.js'; // 不需要花括号 {User},只需要写成 User 即可
new User('John');
2
3
4
由于每个文件最多只能有一个默认的导出,因此导出的实体可能没有名称。
🌰 例子:
// 导出类
export default class {
constructor() { ... }
}
// 导出函数
export default function(user) {
alert(`Hello, ${user}!`);
}
// 数组
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
2
3
4
5
6
7
8
9
10
11
12
13
对于不加
default
,又不给命名,这样的导出是错误的。
# default
在某些情况下, default
关键词被用于 引用 默认的导出。
🌰 例子:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
export {sayHi as default};
2
3
4
5
与
export default
的效果相同。
🌰 例子 / 当模块中又有 默认导出 又有别的一些导出时:
// 📁 user.js
export default class User {
constructor(name) {
this.name = name;
}
}
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
2
3
4
5
6
7
8
9
10
导入时:
// 📁 main.js
import {default as User, sayHi} from './user.js';
new User('John');
2
3
4
另一种方法, 将所有东西 *
作为对象导入:
// 📁 main.js
import * as user from './user.js';
let User = user.default; // 默认的导出
new User('John');
2
3
4
5
提示
对于命名的导出,在导入时一般使用正确相同的名称。
对于默认的导出,一般使用文件名相同的名称形式导入(别的命名也可以,但一般为了变量命名规范,要与文件名对应)。
🌰 例子:
import User from './user.js'; import LoginForm from './loginForm.js'; import func from '/path/to/func.js'; ...
1
2
3
4
所以即便是模块中只有一个实体导出,也一般使用命名导出,这也方便了 重新导出。
# 重新导出
重新导出 export ... from
允许导入内容,并立即将其导出(可能是用的是其他的名字)。
🌰 例子:
export {sayHi} from './say.js'; // 重新导出 sayHi
export {default as User} from './user.js'; // 重新导出 default
2
3
🌰 例子 / 实际应用例子:
当开发一个 package (一个包含大量模块的文件夹),其中一些功能是导出到外部的,并且其中一些模块仅仅是供其他 package 中的模块内部使用的 「辅助模块」。
文件结构:
auth/ index.js user.js helpers.js tests/ login.js providers/ github.js facebook.js ...
1
2
3
4
5
6
7
8
9
10
当想要使用其中的功能,应该只从 index.js
导入:
import {login, logout} from 'auth/index.js'
所以 index.js
出口文件 应该要导出希望包中提供的所有功能:
这样做的目的,其他使用包的开发者不应该干预其内部结构,不应该搜索包的文件夹中的文件。所以只在
auth/index.js
中导出必要的部分,并保持其他内容「不可见」。
// 📁 auth/index.js
// 导入 login/logout 然后立即导出它们
import {login, logout} from './helpers.js';
export {login, logout};
// 将默认导出导入为 User,然后导出它
import User from './user.js';
export {User};
...
2
3
4
5
6
7
8
9
10
使用 export ... from ...
可以简写这个过程:
// 📁 auth/index.js
export {login, logout} from './helpers.js'
export {default as User} from './user.js'
2
3
4
export ... from
与 import/export
相比的显着区别是 重新导出的模块在当前文件中不可用。所以在上面的 auth/index.js
中,不能使用重新导出的 login/logout
函数。
🌰 例子 / 重新导出 默认导出:
- 如果 有一个
user.js
脚本,其中写了export default class User
,并且想重新导出类User
:
// 📁 user.js
export default class User {
// ...
}
2
3
4
这时可能会遇到两个问题:
export User from './user.js'
无效。要重新导出默认导出,必须明确写出
export {default as User}
,就像上面的例子中那样。
export * from './user.js'
重新导出只导出了 命名的导出,但是忽略了默认的导出。
如果想要将 命名的导出和默认的导出都重新导出,那么需要两条语句:
export * from './user.js' // 重新导出命名的导出
export {default} from './user.js' // 重新导出默认的导出
2
所以 默认导出 给重新导出带来了不便。要重新导出最好命名。
# 动态导入
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>)
2
3
4
5
或者在 异步函数中使用 await
:
let module = await import(modulePath)
🌰 例子 / 完整例子:
- 若有模块
say.js
:
// 📁 say.js
export function hi() {
alert(`Hello`);
}
export function bye() {
alert(`Bye`);
}
2
3
4
5
6
7
8
- 那么可以这样动态导入:
let {hi, bye} = await import('./say.js');
hi();
bye();
2
3
4
- 如果模块
say.js
有默认导出:
// 📁 say.js
export default function() {
alert("Module loaded (export default)!");
}
2
3
4
- 动态导入需要在导入后使用
default
属性:
let obj = await import('./say.js');
let say = obj.default;
say();
2
3
或者:
let {default: say} = await import('./say.js');
提示
import()
只是一种特殊语法,不能将 import
复制到一个变量中,或者对其使用 call/apply
。因为它不是一个函数。
# 总结
- 一个模块就是一个文件。
- 模块具有自己的本地顶级作用域,并可以通过
import/export
交换功能。 - 模块代码只执行一次。导出仅创建一次,然后会在导入之间共享。
模块的导出:
- 在声明一个 class/function/… 之前:
export [default] class/function/variable ...
- 独立的导出:
export {x [as y], ...}
.
- 重新导出:
export {x [as y], ...} from "module"
export * from "module"
(不会重新导出默认的导出)。export {default [as y]} from "module"
(重新导出默认的导出)。
模块的导入:
- 导入命名的导出:
import {x [as y], ...} from "module"
- 导入默认的导出:
import x from "module"
import {default as x} from "module"
- 导入所有:
import * as obj from "module"
- 导入模块(其代码,并运行),但不要将其任何导出赋值给变量:
import "module"