Skip to content

Commit 0e57b52

Browse files
committed
Translate 1-js/13-modules/01-modules-intro/article.md
1 parent c6a17f6 commit 0e57b52

File tree

9 files changed

+401
-0
lines changed

9 files changed

+401
-0
lines changed

Diff for: 1-js/13-modules/01-modules-intro/article.md

+381
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
# 模块 (Modules) 简介
2+
3+
当我们的应用日益增大时,我们想要将应用分割成多个文件,即我们所说的“模块”。
4+
5+
一个模块通常包含一些有用的函数类或者库。
6+
7+
很长一段时间,JavaScript 都没有语言级(language-level)模块语法。这是因为初始的脚本都很小且简单,所以没必要将其模块化。
8+
9+
但是不管怎样,到最后,脚本文件都会变的越来越复杂,所以 JavaScript 社区发明了许多方法将代码组织为模块——一种特殊的可以按需加载的库。
10+
11+
例如:
12+
13+
- [AMD](https://en.wikipedia.org/wiki/Asynchronous_module_definition) -- 最古老的模块化系统,最开始应用在 [require.js](http://requirejs.org/) 这个库中。
14+
15+
- [CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1) -- 为 Node.js 创建的模块化系统。
16+
17+
- [UMD](https://github.com/umdjs/umd) -- 另外一个模块化系统,建议作为通用的模块化系统,它与 AMD 和 CommonJS 都是兼容的。
18+
19+
现在这些都将成为过去,但是我们仍然能在一些旧的脚本中找到他们的踪迹。语言级的模块化系统在 2015 年的时候出现在标准中,从那时候起开始逐渐发展,现在已经得到了所有主流浏览器和 Node.js 的支持。
20+
21+
## 什么是模块?
22+
23+
模块仅仅是一个文件,一个脚本而已,它就是这么简单。
24+
25+
用一些关键字比如 `export``import` 来交换模块之间的功能(functionality)或者从一个模块中调用另一个模块中的函数。
26+
27+
- `export` 关键字表示在当前模块之外可以访问的变量和功能。
28+
- `import` 关键字允许从其他模块中导入一些诸如函数之类的功能等等。
29+
30+
例如,我们有一个名为 `sayHi.js` 的文件导出一个函数:
31+
32+
```js
33+
// 📁 sayHi.js
34+
export function sayHi(user) {
35+
alert(`Hello, ${user}!`);
36+
}
37+
```
38+
39+
然后在其他的文件里导入并使用它:
40+
41+
```js
42+
// 📁 main.js
43+
import {sayHi} from './sayHi.js';
44+
45+
alert(sayHi); // function...
46+
sayHi('John'); // Hello, John!
47+
```
48+
49+
在这个章节里,我们专注于语言本身,但是我们使用浏览器作为演示环境,那么就让我们开始来看看怎么在浏览器中使用模块的。
50+
51+
由于模块使用特殊的关键词和功能,所以我们必须通过使用属性 `<script type="module">` 来告诉浏览器,脚本应该被当作 `模块` 来看待。
52+
53+
像这样:
54+
55+
[codetabs src="say" height="140" current="index.html"]
56+
57+
浏览器自动导入脚本并解析导入的模块,然后执行该脚本。
58+
59+
## 核心模块功能
60+
61+
模块相较于普通的脚本有什么区别呢?
62+
63+
下面有一些核心的功能,对于浏览器和服务端的 JavaScript 来说都是有效的。
64+
65+
### 默认使用 "use strict"
66+
67+
模块始终默认使用使用 `use strict`,例如,对一个未声明的变量赋值将会抛出错误。
68+
69+
```html run
70+
<script type="module">
71+
a = 5; // error
72+
</script>
73+
```
74+
75+
在这里我们可以在浏览器里看到它,但是对于任何模块来说都是一样的。
76+
77+
### 模块级作用域(Module-level scope)
78+
79+
每个模块都有自己的顶级作用域(top-level scope)。换句话说,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。
80+
81+
在下面的这个例子中,我们导入了两个脚本,`hello.js` 尝试使用从 `user.js` 中导入的 `user` 变量。
82+
83+
[codetabs src="scopes" height="140" current="index.html"]
84+
85+
模块可以导出 `export` 想要从外部访问的内容,也可以导入 `import` 想要的内容。
86+
87+
所以,我们应该在 `hello.js` 中直接导入 `user.js`,而不是在 `index.html` 中导入。
88+
89+
这是正确导入的方法:
90+
91+
[codetabs src="scopes-working" height="140" current="hello.js"]
92+
93+
在浏览器中,每个 `<script type="module">` 也存在独立的顶级范围的作用域。
94+
95+
```html run
96+
<script type="module">
97+
// 变量仅可在模块脚本内部可见
98+
let user = "John";
99+
</script>
100+
101+
<script type="module">
102+
*!*
103+
alert(user); // Error: user is not defined
104+
*/!*
105+
</script>
106+
```
107+
108+
如果我们真的需要创建一个窗口级别(window-level)的全局变量,我们可以显式地将它分配给 `window` 并以 `window.user` 来访问它。但是这样做需要你有足够充分的理由,否则就不要这样。
109+
110+
### 模块代码仅在第一次导入时解析
111+
112+
如果将一个模块导入到多个其他位置,则仅在第一次导入时解析其代码,然后将导出提供给所有导入的位置。
113+
114+
这具有很重要的后果。我们来看一下下面的例子:
115+
116+
首先,如果执行一个模块中的代码带来一些副作用,比如显示一个消息,然后多次导入它但是只会显示一次,即第一次:
117+
118+
```js
119+
// 📁 alert.js
120+
alert("Module is evaluated!");
121+
```
122+
123+
```js
124+
// 从不同的文件导入相同模块
125+
126+
// 📁 1.js
127+
import `./alert.js`; // Module is evaluated!
128+
129+
// 📁 2.js
130+
import `./alert.js`; // (nothing)
131+
```
132+
133+
在日常开发中,顶级模块主要是用于初始化使用的。我们创建数据结构,预填充它们,如果我们想要可重用某些东西,只要导出即可。
134+
135+
下面是一个高级点的例子:
136+
137+
我们假设一个模块导出了一个对象:
138+
139+
```js
140+
// 📁 admin.js
141+
export let admin = {
142+
name: "John"
143+
};
144+
```
145+
146+
如果这个模块是从多个文件中导入的,模块仅仅在第一次导入的时候解析创建 `admin` 对象。然后将其传入所有导入的位置。
147+
148+
所有导入位置都得到了唯一的 `admin` 对象。
149+
150+
```js
151+
// 📁 1.js
152+
import {admin} from './admin.js';
153+
admin.name = "Pete";
154+
155+
// 📁 2.js
156+
import {admin} from './admin.js';
157+
alert(admin.name); // Pete
158+
159+
*!*
160+
// 1.js 和 2.js 导入相同的对象
161+
// 1.js 中对对象的修改,在 2.js 中是可访问的
162+
*/!*
163+
```
164+
165+
所以,让我们重申一下:模块只执行一次。生成导出,然后在导入的位置共享同一个导出,当在某个位置修改了 `admin` 对象,在其他模块中是可以看到修改的。
166+
167+
这种行为对于需要配置的模块来说是非常棒的。我们可以在第一次导入时设置所需要的属性,然后在后面的导入中就可以直接使用了。
168+
169+
例如,下面的 `admin.js` 模块可能提供特定的功能,但是希望在外部可访问 `admin` 对象:
170+
171+
```js
172+
// 📁 admin.js
173+
export let admin = { };
174+
175+
export function sayHi() {
176+
alert(`Ready to serve, ${admin.name}!`);
177+
}
178+
```
179+
180+
现在,在 `init.js`——我们 app 的第一个脚本中,设置了 `admin.name`。现在每个位置都能看到它了,包括来自 `admin.js` 本身的调用。
181+
182+
```js
183+
// 📁 init.js
184+
import {admin} from './admin.js';
185+
admin.name = "Pete";
186+
```
187+
188+
```js
189+
// 📁 other.js
190+
import {admin, sayHi} from './admin.js';
191+
192+
alert(admin.name); // *!*Pete*/!*
193+
194+
sayHi(); // Ready to serve, *!*Pete*/!*!
195+
```
196+
197+
### import.meta
198+
199+
`import.meta` 对象包含当前模块的一些信息。
200+
201+
它的内容取决于其所在环境,比如说在浏览器环境中,它包含脚本的链接,如果是在 HTML 中的话就是当前页面的链接。
202+
203+
```html run height=0
204+
<script type="module">
205+
alert(import.meta.url); // 脚本链接 (在行内联本中就是当前页面的链接)
206+
</script>
207+
```
208+
209+
### 顶级 "this" 是 未定义(undefined) 的
210+
211+
这是一个小功能,但为了完整性,我们应该提到它。
212+
213+
在一个模块中,顶级 `this` 是未定义的,而不是像非模块脚本中的全局变量。
214+
215+
```html run height=0
216+
<script>
217+
alert(this); // window
218+
</script>
219+
220+
<script type="module">
221+
alert(this); // undefined
222+
</script>
223+
```
224+
225+
## 特定于浏览器的功能
226+
227+
与常规脚本相比,拥有 `type =module` 标识的脚本有几个特定于浏览器的差异。
228+
229+
如果你是第一次阅读或者你不在浏览器中使用 JavaScript,你可能需要暂时略过这些内容。
230+
231+
### 模块脚本是延迟解析的
232+
233+
对于外部和内联模块脚本来说,它 *总是* 延迟解析的,就和 `defer` 属性一样(参见 [script-async-defer](info:script-async-defer))。
234+
235+
也就是说:
236+
- 外部模块脚本 `<script type="module" src="...">` 不会阻塞 HTML 的解析,它们与其他资源并行加载。
237+
- 直到 HTML 文档完全解析渲染后(即使模块脚本比 HTML 先加载完成),模块脚本才会开始运行。
238+
- 执行脚本的相对顺序:在前面的先执行。
239+
240+
它的一个副作用是,模块脚本总是 “看见” 完全加载的 HTML 页面,包括在它们后面的 HTML 元素。
241+
242+
例如:
243+
244+
```html run
245+
<script type="module">
246+
*!*
247+
alert(typeof button); // object: 脚本可以 '看见' 下面的 button
248+
*/!*
249+
// 当脚本模块延迟时,脚本在整个页面加载完成之后才执行
250+
</script>
251+
252+
相较于普通脚本:
253+
254+
<script>
255+
*!*
256+
alert(typeof button); // Error: button is undefined, 脚本不能 “看到” 下面的元素
257+
*/!*
258+
// 普通脚本在剩余页面加载完成前就执行了
259+
</script>
260+
261+
<button id="button">Button</button>
262+
```
263+
264+
注意:上面的第二个脚本要先于前一个脚本执行,所以我们先会看到 `undefined`,然后才是 `object`
265+
266+
这是因为模块脚本被延迟执行了,所以要等到页面加载结束才执行。而普通脚本就没有这个限制了,它会马上执行,所以我们先看到它的输出。
267+
268+
当使用模块脚本的时候,我们应该知道当 HTML 页面加载完毕的时候会显示出来,然后 JavaScript 在其后开始执行,所以用户会先于 JavaScript 脚本加载完成是看到页面内容。某些依赖于 JavaScript 的功能可能还不能正常工作。我们应该使用透明层或者 “加载指示”,或者其他方法以确保用户不会感到莫名其妙。
269+
270+
### 内联脚本是异步的
271+
272+
内联脚本和外部脚本都允许使用 `<script async type="module">` 属性,当导入的模块被处理时,异步脚本会立即运行,于其他的脚本或者 HTML 文档无关。
273+
274+
例如,下面的脚本中有 `async` 属性,所以它不会等待其他任何加载完成就已经开始运行。
275+
276+
它导入(fetches `./analytics.js`)脚本,导入完成就开始运行,即使 HTML 文档还未解析完毕或者其他脚本仍在待处理的状态。
277+
278+
这对于不依赖任何其他东西的功能来说是非常棒的,比如计数器,广告和文档级的事件监听器。
279+
280+
```html
281+
<!-- 所有依赖都获取(analytics.js)脚本,然后运行 -->
282+
<!-- 不会等待 HTML 文档或者其他 <script> 标签 -->
283+
<script *!*async*/!* type="module">
284+
import {counter} from './analytics.js';
285+
286+
counter.count();
287+
</script>
288+
```
289+
290+
### 外部脚本
291+
292+
外部脚本相较于其他脚本有两个显著的差异:
293+
294+
1. 具有相同 `src` 属性值的外部脚本仅运行一次:
295+
```html
296+
<!-- my.js 脚本被加载,但它只运行一次 -->
297+
<script type="module" src="my.js"></script>
298+
<script type="module" src="my.js"></script>
299+
```
300+
301+
2. 从其他域名获取的外部脚本需要加上 [CORS](mdn:Web/HTTP/CORS) 头。换句话说,如果一个模块脚本是从其他域名获取的,那么它所在的远端服务器必须提供 `Access-Control-Allow-Origin: *`(可能使用加载的域名代替 `*`) 响应头以指明当前请求是被允许的。
302+
```html
303+
<!-- another-site.com 必须提供 Access-Control-Allow-Origin -->
304+
<!-- 否则, 脚本不会执行 -->
305+
<script type="module" src="*!*http://another-site.com/their.js*/!*"></script>
306+
```
307+
308+
这可以保证最基本的安全问题。
309+
310+
### 不允许裸模块("bare" modules)
311+
312+
在浏览器中,必须给与 `import` 一个相对或者绝对的 URL。没有给定路径的模块被称作 “裸” 模块。`import` 中不允许使用这些模块。
313+
314+
例如,下面这个 `import` 是不允许的:
315+
```js
316+
import {sayHi} from 'sayHi'; // Error, "裸" 模块
317+
// 模块必须提供路径, 例如 './sayHi.js'
318+
```
319+
320+
在具体环境有所不同,比如 Node.js 或者打包工具中是可以使用裸模块的,因为它们有自己的查找模块和钩子的方法。但是目前浏览器还不支持裸模块。
321+
322+
### 兼容性,"nomodule"
323+
324+
旧时的浏览器不理解 `type="module"` 值。对于位置类型的脚本会被忽略掉。对于它们来说可以使用 `nomodule` 属性来提供后备:
325+
326+
```html run
327+
<script type="module">
328+
alert("Runs in modern browsers");
329+
</script>
330+
331+
<script nomodule>
332+
alert("Modern browsers know both type=module and nomodule, so skip this")
333+
alert("Old browsers ignore script with unknown type=module, but execute this.");
334+
</script>
335+
```
336+
337+
如果我们使用打包工具,当脚本被打包进一个单一文件(或者几个文件),在这些脚本中,`import/export` 语句被替换成特殊的打包函数。因此最终打包好的脚本不包含任何 `import/export` 语句,它也不需要 `type="module"` 属性,我们仅像普通脚本一样使用就好了:
338+
339+
```html
340+
<!-- 假设我们从诸如 Webpack 这类的打包工具中获得了 "bundle.js" 脚本 -->
341+
<script src="bundle.js"></script>
342+
```
343+
344+
## 构建工具
345+
346+
在日常开发中,浏览器模块很少以原始形式使用,通常,我们用一些特殊工具,像 [Webpack](https://webpack.js.org/),将他们打包在一起,然后部署到服务器。
347+
348+
使用打包工具的一个好处是——它们对于如何解析模块给与了足够多的控制,比如允许使用裸模块,以及 CSS/HTML 模块等等。
349+
350+
这里列出了一些构建工具做的事情:
351+
352+
1. 从一个打算放在 HTML 中的 `<script type="module">` 主模块开始。
353+
2. 分析它的依赖:它的导入以及它的导入的导入。
354+
3. 用打包函数替换掉原生的 `import` 调用,生成一个(或者多个,这是可调的)具有所有模块的文件,这就是打包工具的工作。特殊的模块类型,比如 HTML/CSS 模块也是可以这样做的。
355+
4. 在这个过程中,可能会应用其他的转换或者优化:
356+
- 删除无法访问的代码
357+
- 删除未使用的导出("tree-shaking")
358+
- 开发中使用的如 `console``debugger` 这样的语句
359+
- 使用 [Babel](https://babeljs.io/) 可以将现代的,前沿的 JavaScript 语法转换为具有类似功能的旧语法
360+
- 最终生成压缩文件(删除无用空格,变量用短的名字替换)
361+
362+
也就是说,原生模块也是可以使用的。所以我们在这里不会使用 Webpack,你可以稍后再配置它。
363+
364+
## 总结
365+
366+
下面总结一下模块的核心概念:
367+
368+
1. 模块就是文件。浏览器需要使用 `<script type="module">` 属性以使 `import/export` 可用,这里有几点差别:
369+
- 默认是延迟解析的
370+
- 行内脚本是异步的
371+
- 加载外部不同源(domain/protocol/port)脚本时,必须提供 CORS 响应头
372+
- 重复的外部脚本会被忽略
373+
2. 模块有自己的本地顶级作用域,可以通过 `import/export` 交换功能
374+
3. 模块始终使用 `use strict`
375+
4. 模块代码只执行一次。导出的代码创建一次然后会在各导入之间共享
376+
377+
所以,通常来说,当我们使用模块的时候,每个模块实现特定功能并导出它。然后我们需要它的时候直接使用 `import` 导入即可。浏览器会自动加载和解析脚本。
378+
379+
在生产环境中,开发者经常基于性能或者其他原因而使用诸如 [Webpack](https://webpack.js.org) 这类的打包工具。
380+
381+
在下一章里,我们将会看到更多关于模块以及如何导入/导出的例子。

Diff for: 1-js/13-modules/01-modules-intro/say.view/index.html

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<!doctype html>
2+
<script type="module">
3+
import {sayHi} from './say.js';
4+
5+
document.body.innerHTML = sayHi('John');
6+
</script>

Diff for: 1-js/13-modules/01-modules-intro/say.view/say.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function sayHi(user) {
2+
return `Hello, ${user}!`;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {user} from './user.js';
2+
3+
document.body.innerHTML = user; // John

0 commit comments

Comments
 (0)