带你深入了解 Module
模块介绍
当我们的应用程序变大时,我们想要把它分割成多个文件,也就是所谓的“模块”。一个模块可以包含一个用于特定目的的类或函数库。
很长一段时间以来,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>