模块介绍

当我们的应用程序变大时,我们想要把它分割成多个文件,也就是所谓的“模块”。一个模块可以包含一个用于特定目的的类或函数库。

很长一段时间以来,JavaScript都没有语言级的模块语法。这不是问题,因为最初的脚本很小很简单,所以没有必要。

但最终脚本变得越来越复杂,因此社区发明了各种方法来将代码组织到模块中,以及根据需要加载模块的特殊库。

  • AMD——最古老的模块系统之一,最初由require.js库实现。

  • CommonJS -为Node.js服务器创建的模块系统。

  • UMD -另一个模块系统,建议作为一个通用的,兼容AMD和CommonJS。

现在所有这些慢慢地成为历史的一部分,但我们仍然可以在古老的脚本中找到它们。

语言级模块系统于2015年出现在标准中,后来逐渐演变,现在所有主流浏览器和Node.js都支持它。因此,我们将从现在开始学习现代JavaScript模块。

什么是模块

模块只是一个文件。一个脚本就是一个模块。就这么简单。

模块可以相互加载,并使用特殊的指令导出和导入来交换功能,从一个模块调用另一个模块的函数:

  • export 关键字标签变量和函数,这些变量和函数应该可以从当前模块外部访问。

  • import 允许从其他模块导入功能。

例如,如果我们有一个文件sayHi.js导出一个函数:

// ???? sayHi.js
export function sayHi(user) {
  alert(`Hello, ${user}!`);
}

然后另一个文件可以导入并使用它:

// ???? main.js
import {sayHi} from './sayHi.js';

alert(sayHi); // function...
sayHi('John'); // Hello, John!

import指令通过相对于当前文件的path ./sayHi.js加载模块,并将导出的函数sayHi赋给相应的变量。

让我们在浏览器中运行这个示例。

由于模块支持特殊的关键字和特性,所以我们必须通过属性

sayHi.js

export function sayHi(user) {
  return `Hello, ${user}!`;
}

index.html

<!doctype html>
<script type="module">
  import {sayHi} from './say.js';

  document.body.innerHTML = sayHi('John');
</script>
核心模块功能

与“常规”脚本相比,模块有什么不同?

有一些核心特性,对浏览器和服务器端JavaScript都有效。

use strict

默认情况下,模块总是使用严格模式的。例如,给未声明的变量赋值会产生错误。

<script type="module">
  a = 5; // error
</script>
块级作用域

每个模块都有自己的顶级作用域。换句话说,一个模块中的顶级变量和函数在其他脚本中看不到。

在下面的例子中,导入了两个脚本,hello.js尝试使用user.js中声明的user变量:

user.js

let user = "John";

hello.js

alert(user); // no such variable (each module has independent variables)

index.html

<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>

模块应该导出它们希望从外部访问的内容,并导入它们需要的内容。

因此,我们应该将user.js导入到hello.js中,并从中获取所需的功能,而不是依赖全局变量。

这是正确的变体:

user.js

export let user = "John";

hello.js

import {user} from './user.js';

document.body.innerHTML = user; // John

index.html

<!doctype html>
<script type="module" src="hello.js"></script>

在浏览器中,每个<script type="module">对象都有独立的顶级作用域

<script type="module">
  // The variable is only visible in this module script
  let user = "John";
</script>

<script type="module">
  alert(user); // Error: user is not defined
</script>

如果我们真的需要创建一个窗口级全局变量,我们可以显式地将它分配给window,并作为window.user访问。但这是一个需要充分理由的例外。

模块代码只在第一次导入时才被求值

如果同一个模块被导入到其他多个位置,它的代码只在第一次执行,然后导出将被交给所有导入器。

这有重要的后果。让我们来看看他们的例子:

首先,如果执行一个模块代码会带来副作用,比如显示一条消息,那么多次导入它只会触发一次-第一次:

// ???? alert.js
alert("Module is evaluated!");
// Import the same module from different files

// ???? 1.js
import `./alert.js`; // Module is evaluated!

// ???? 2.js
import `./alert.js`; // (shows nothing)

在实践中,顶级模块代码主要用于初始化、内部数据结构的创建,如果我们想要某些东西可重用—导出它。

现在,一个更高级的例子。

比方说,一个模块导出了一个对象:

// ???? admin.js
export let admin = {
  name: "John"
};

如果从多个文件导入此模块,则只在第一次评估该模块,创建admin对象,然后传递给所有进一步的导入器。

所有的导入器都只有一个admin对象:

// ???? 1.js
import {admin} from './admin.js';
admin.name = "Pete";

// ???? 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete

// Both 1.js and 2.js imported the same object
// Changes made in 1.js are visible in 2.js

所以,让我们重申一下——这个模块只执行一次。导出将生成,然后它们将在导入器之间共享,因此,如果管理对象发生了更改,其他模块将看到这一点。

这样的行为允许我们在第一次导入时配置模块。我们可以设置它的属性一次,然后在进一步导入时,它就准备好了。

例如,admin.js模块可能提供某些功能,但希望凭据从外部进入admin对象:

// ???? admin.js
export let admin = { };

export function sayHi() {
  alert(`Ready to serve, ${admin.name}!`);
}

在init.js 在应用程序的第一个脚本中,我们设置admin.name。然后所有人都会看到它,包括从admin.js内部调用:

// ???? init.js
import {admin} from './admin.js';
admin.name = "Pete";

另一个模块也可以看到admin.name:

// ???? other.js
import {admin, sayHi} from './admin.js';

alert(admin.name); // Pete

sayHi(); // Ready to serve, Pete!
import.meta

导入的对象。元包含关于当前模块的信息。

它的内容取决于环境。在浏览器中,它包含脚本的url,或者当前网页的url,如果在HTML中:

<script type="module">
  alert(import.meta.url); // script url (url of the html page for an inline script)
</script>
In a module, “this” is undefined

这是一个小特性,但是为了完整性,我们应该提到它。

在模块中,这是未定义的顶层。

与非模块脚本相比,它是一个全局对象:

<script>
  alert(this); // window
</script>

<script type="module">
  alert(this); // undefined
</script>
浏览器 特定功能

与常规的脚本相比,使用type="module"的脚本还有一些特定于浏览器的差异。

如果您是第一次阅读,或者您没有在浏览器中使用JavaScript,那么您可能想要跳过这一部分。

模块脚本被延迟
<script type="module">
  alert(typeof button); // object: the script can 'see' the button below
  // as modules are deferred, the script runs after the whole page is loaded
</script>

Compare to regular script below:

<script>
  alert(typeof button); // button is undefined, the script can't see elements below
  // regular scripts run immediately, before the rest of the page is processed
</script>

<button id="button">Button</button>

请注意:第二个脚本实际上在第一个脚本之前运行!首先是undefined,然后是object。

这是因为模块被延迟了,所以我们等待文档被处理。常规脚本立即运行,所以我们首先看到它的输出。

当使用模块时,我们应该注意HTML页面在加载时显示,JavaScript模块在加载后运行,所以用户可能在JavaScript应用程序准备好之前看到页面。有些功能可能还不能工作。我们应该设置“加载指示符”,否则将确保访问者不会被混淆。

异步在内联脚本上工作

对于非模块脚本,async属性只对外部脚本有效。异步脚本在准备好后立即运行,独立于其他脚本或HTML文档。

对于模块脚本,它也适用于内联脚本。

例如,下面的内联脚本是异步的,所以它不等待任何东西。

它执行导入(fetch ./analytics.js)并在准备好时运行,即使HTML文档还没有完成,或者其他脚本仍在等待中。

这对于不依赖于任何东西的功能来说是很好的,比如计数器、广告、文档级事件侦听器。

<!-- all dependencies are fetched (analytics.js), and the script runs -->
<!-- doesn't wait for the document or other <script> tags -->
<script async type="module">
  import {counter} from './analytics.js';

  counter.count();
</script>
外部脚本

有type="module"的外部脚本有两个不同:

具有相同src的外部脚本只运行一次:

<!-- the script my.js is fetched and executed only once -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>

从另一个来源(例如另一个站点)获取的外部脚本需要CORS头,如“获取:跨来源请求”章节所述。换句话说,如果一个模块脚本是从另一个来源获取的,远程服务器必须提供一个头部Access-Control-Allow-Origin允许获取。

<!-- another-site.com must supply Access-Control-Allow-Origin -->
<!-- otherwise, the script won't execute -->
<script type="module" src="http://another-site.com/their.js"></script>
不允许出现裸模块

在浏览器中,import必须获得一个相对URL或绝对URL。没有任何路径的模块称为“裸”模块。这样的模块是不允许导入的。

例如,此导入无效:

import {sayHi} from 'sayHi'; // Error, "bare" module
// the module must have a path, e.g. './sayHi.js' or wherever the module is
Compatibility, “nomodule”

旧的浏览器不理解type="module"。未知类型的脚本将被忽略。对于它们,可以使用nomodule属性提供回退:

<script type="module">
  alert("Runs in modern browsers");
</script>

<script nomodule>
  alert("Modern browsers know both type=module and nomodule, so skip this")
  alert("Old browsers ignore script with unknown type=module, but execute this.");
</script>