diff --git a/1-js/13-modules/01-modules-intro/article.md b/1-js/13-modules/01-modules-intro/article.md new file mode 100755 index 0000000000..8535adb50b --- /dev/null +++ b/1-js/13-modules/01-modules-intro/article.md @@ -0,0 +1,379 @@ + +# 模块 (Modules) 简介 + +当我们的应用日益增大时,我们想要将应用分割成多个文件,即我们所说的“模块”。 +一个模块通常包含一些有用的函数类或者库。 + +很长一段时间,JavaScript 都没有语言级(language-level)模块语法。这是因为初始的脚本都很小且简单,所以没必要将其模块化。 + +但是不管怎样,到最后,脚本文件都会变的越来越复杂,所以 JavaScript 社区发明了许多方法将代码组织为模块——一种特殊的可以按需加载的库。 + +例如: + +- [AMD](https://en.wikipedia.org/wiki/Asynchronous_module_definition) — 最古老的模块化系统,最开始应用在 [require.js](http://requirejs.org/) 这个库中。 +- [CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1) — 为 Node.js 创建的模块化系统。 +- [UMD](https://github.com/umdjs/umd) — 另外一个模块化系统,建议作为通用的模块化系统,它与 AMD 和 CommonJS 都是兼容的。 + +现在这些都将成为过去,但是我们仍然能在一些旧的脚本中找到他们的踪迹。语言级的模块化系统在 2015 年的时候出现在标准中,从那时候起开始逐渐发展,现在已经得到了所有主流浏览器和 Node.js 的支持。 + +## 什么是模块? + +模块仅仅是一个文件,一个脚本而已,它就是这么简单。 + +用一些关键字比如 `export` 和 `import` 来交换模块之间的功能(functionality)或者从一个模块中调用另一个模块中的函数。 + +- `export` 关键字表示在当前模块之外可以访问的变量和功能。 +- `import` 关键字允许从其他模块中导入一些诸如函数之类的功能等等。 + +例如,我们有一个名为 `sayHi.js` 的文件导出一个函数: + +```js +// 📁 sayHi.js +export function sayHi(user) { + alert(`Hello, ${user}!`); +} +``` + +然后在其他的文件里导入并使用它: + +```js +// 📁 main.js +import {sayHi} from './sayHi.js'; + +alert(sayHi); // function... +sayHi('John'); // Hello, John! +``` + +在这个章节里,我们专注于语言本身,但是我们使用浏览器作为演示环境,那么就让我们开始来看看怎么在浏览器中使用模块的。 + +由于模块使用特殊的关键词和功能,所以我们必须通过使用属性 ` +``` + +在这里我们可以在浏览器里看到它,但是对于任何模块来说都是一样的。 + +### 模块级作用域(Module-level scope) + +每个模块都有自己的顶级作用域(top-level scope)。换句话说,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。 + +在下面的这个例子中,我们导入了两个脚本,`hello.js` 尝试使用从 `user.js` 中导入的 `user` 变量。 + +[codetabs src="scopes" height="140" current="index.html"] + +模块可以导出 `export` 想要从外部访问的内容,也可以导入 `import` 想要的内容。 + +所以,我们应该在 `hello.js` 中直接导入 `user.js`,而不是在 `index.html` 中导入。 + +这是正确导入的方法: + +[codetabs src="scopes-working" height="140" current="hello.js"] + +在浏览器中,每个 ` + + +``` + +如果我们真的需要创建一个窗口级别(window-level)的全局变量,我们可以显式地将它分配给 `window` 并以 `window.user` 来访问它。但是这样做需要你有足够充分的理由,否则就不要这样。 + +### 模块代码仅在第一次导入时解析 + +如果将一个模块导入到多个其他位置,则仅在第一次导入时解析其代码,然后将导出提供给所有导入的位置。 + +这具有很重要的后果。我们来看一下下面的例子: + +首先,如果执行一个模块中的代码带来一些副作用,比如显示一个消息,然后多次导入它但是只会显示一次,即第一次: + +```js +// 📁 alert.js +alert("Module is evaluated!"); +``` + +```js +// 从不同的文件导入相同模块 + +// 📁 1.js +import `./alert.js`; // Module is evaluated! + +// 📁 2.js +import `./alert.js`; // (nothing) +``` + +在日常开发中,顶级模块主要是用于初始化使用的。我们创建数据结构,预填充它们,如果我们想要可重用某些东西,只要导出即可。 + +下面是一个高级点的例子: + +我们假设一个模块导出了一个对象: + +```js +// 📁 admin.js +export let admin = { + name: "John" +}; +``` + +如果这个模块被导入到多个文件中,模块仅仅在第一次导入的时候解析创建 `admin` 对象。然后将其传入所有导入的位置。 + +所有导入位置都得到了唯一的 `admin` 对象。 + +```js +// 📁 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 导入相同的对象 +// 1.js 中对对象的修改,在 2.js 中是可访问的 +*/!* +``` + +所以,让我们重申一下:模块只执行一次。生成导出,然后在导入的位置共享同一个导出,当在某个位置修改了 `admin` 对象,在其他模块中是可以看到修改的。 + +这种行为对于需要配置的模块来说是非常棒的。我们可以在第一次导入时设置所需要的属性,然后在后面的导入中就可以直接使用了。 + +例如,下面的 `admin.js` 模块可能提供特定的功能,但是希望在外部可访问 `admin` 对象: + +```js +// 📁 admin.js +export let admin = { }; + +export function sayHi() { + alert(`Ready to serve, ${admin.name}!`); +} +``` + +现在,在 `init.js`——我们 app 的第一个脚本中,设置了 `admin.name`。现在每个位置都能看到它了,包括来自 `admin.js` 本身的调用。 + +```js +// 📁 init.js +import {admin} from './admin.js'; +admin.name = "Pete"; +``` + +```js +// 📁 other.js +import {admin, sayHi} from './admin.js'; + +alert(admin.name); // *!*Pete*/!* + +sayHi(); // Ready to serve, *!*Pete*/!*! +``` + +### import.meta + +`import.meta` 对象包含当前模块的一些信息。 + +它的内容取决于其所在环境,比如说在浏览器环境中,它包含脚本的链接,如果是在 HTML 中的话就是当前页面的链接。 + +```html run height=0 + +``` + +### 顶级 "this" 是 未定义(undefined)的 + +这是一个小功能,但为了完整性,我们应该提到它。 + +在一个模块中,顶级 `this` 是未定义的,而不是像非模块脚本中的全局变量。 + +```html run height=0 + + + +``` + +## 特定于浏览器的功能 + +与常规脚本相比,拥有 `type="module"` 标识的脚本有几个特定于浏览器的差异。 + +如果你是第一次阅读或者你不在浏览器中使用 JavaScript,你可能需要暂时略过这些内容。 + +### 模块脚本是延迟解析的 + +对于外部和内联模块脚本来说,它 **总是** 延迟解析的,就和 `defer` 属性一样(参见 [script-async-defer](info:script-async-defer))。 + +也就是说: + - 外部模块脚本 ` + +相较于普通脚本: + + + + +``` + +注意:上面的第二个脚本要先于前一个脚本执行,所以我们先会看到 `undefined`,然后才是 `object`。 + +这是因为模块脚本被延迟执行了,所以要等到页面加载结束才执行。而普通脚本就没有这个限制了,它会马上执行,所以我们先看到它的输出。 + +当使用模块脚本的时候,我们应该知道当 HTML 页面加载完毕的时候会显示出来,然后 JavaScript 在其后开始执行,所以用户会先于 JavaScript 脚本加载完成是看到页面内容。某些依赖于 JavaScript 的功能可能还不能正常工作。我们应该使用透明层或者 “加载指示”,或者其他方法以确保用户不会感到莫名其妙。 + +### 内联脚本是异步的 + +内联脚本和外部脚本都允许使用 ` +``` + +### 外部脚本 + +外部脚本相较于其他脚本有两个显著的差异: + +1. 具有相同 `src` 属性值的外部脚本仅运行一次: + ```html + + + + ``` + +2. 从其他域名获取的外部脚本需要加上 [CORS](mdn:Web/HTTP/CORS) 头。换句话说,如果一个模块脚本是从其他域名获取的,那么它所在的远端服务器必须提供 `Access-Control-Allow-Origin: *`(可能使用加载的域名代替 `*`)响应头以指明当前请求是被允许的。 + ```html + + + + ``` + + 这可以保证最基本的安全问题。 + +### 不允许裸模块("bare" modules) + +在浏览器中,必须给与 `import` 一个相对或者绝对的 URL。没有给定路径的模块被称作“裸”模块。`import` 中不允许使用这些模块。 + +例如,下面这个 `import` 是不允许的: +```js +import {sayHi} from 'sayHi'; // Error,“裸”模块 +// 模块必须提供路径,例如 './sayHi.js' +``` + +在具体环境有所不同,比如 Node.js 或者打包工具中是可以使用裸模块的,因为它们有自己的查找模块和钩子的方法。但是目前浏览器还不支持裸模块。 + +### 兼容性,"nomodule" + +旧时的浏览器不理解 `type="module"` 值。对于位置类型的脚本会被忽略掉。对于它们来说可以使用 `nomodule` 属性来提供后备: + +```html run + + + +``` + +如果我们使用打包工具,当脚本被打包进一个单一文件(或者几个文件),在这些脚本中,`import/export` 语句被特殊的打包函数处理后替代。因此最终打包好的脚本不包含任何 `import/export` 语句,它也不需要 `type="module"` 属性,我们仅像普通脚本一样使用就好了: + +```html + + +``` + +## 构建工具 + +在日常开发中,浏览器模块很少以原始形式使用,通常,我们用一些特殊工具,像 [Webpack](https://webpack.js.org/),将他们打包在一起,然后部署到服务器。 + +使用打包工具的一个好处是——它们对于如何解析模块给与了足够多的控制,比如允许使用裸模块,以及 CSS/HTML 模块等等。 + +这里列出了一些构建工具做的事情: + +1. 从一个打算放在 HTML 中的 ` diff --git a/1-js/13-modules/01-modules-intro/say.view/say.js b/1-js/13-modules/01-modules-intro/say.view/say.js new file mode 100755 index 0000000000..198a3be6da --- /dev/null +++ b/1-js/13-modules/01-modules-intro/say.view/say.js @@ -0,0 +1,3 @@ +export function sayHi(user) { + return `Hello, ${user}!`; +} diff --git a/1-js/13-modules/01-modules-intro/scopes-working.view/hello.js b/1-js/13-modules/01-modules-intro/scopes-working.view/hello.js new file mode 100755 index 0000000000..6c087ea81a --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes-working.view/hello.js @@ -0,0 +1,3 @@ +import {user} from './user.js'; + +document.body.innerHTML = user; // John diff --git a/1-js/13-modules/01-modules-intro/scopes-working.view/index.html b/1-js/13-modules/01-modules-intro/scopes-working.view/index.html new file mode 100755 index 0000000000..b78f759126 --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes-working.view/index.html @@ -0,0 +1,2 @@ + + diff --git a/1-js/13-modules/01-modules-intro/scopes-working.view/user.js b/1-js/13-modules/01-modules-intro/scopes-working.view/user.js new file mode 100755 index 0000000000..d289329c61 --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes-working.view/user.js @@ -0,0 +1 @@ +export let user = "John"; diff --git a/1-js/13-modules/01-modules-intro/scopes.view/hello.js b/1-js/13-modules/01-modules-intro/scopes.view/hello.js new file mode 100755 index 0000000000..714aafa1f1 --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes.view/hello.js @@ -0,0 +1 @@ +alert(user); // no such variable (each module has independent variables) diff --git a/1-js/13-modules/01-modules-intro/scopes.view/index.html b/1-js/13-modules/01-modules-intro/scopes.view/index.html new file mode 100755 index 0000000000..a87e96fdfb --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes.view/index.html @@ -0,0 +1,3 @@ + + + diff --git a/1-js/13-modules/01-modules-intro/scopes.view/user.js b/1-js/13-modules/01-modules-intro/scopes.view/user.js new file mode 100755 index 0000000000..12ec850d9a --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes.view/user.js @@ -0,0 +1 @@ +let user = "John";