Javascript 模块

在这篇文章中,我们将介绍标准的 JavaScript 模块,目前是如何在前端应用程序中使用的,以及未来我们可能会如何使用它们。
JavaScript 模块有时被称为 ESM,它代表 ECMAScript 模块。


image.png

什么是JavaScript模块?

JavaScript模块是构造 JavaScript 代码的一种方法。模块中的代码与其他模块中的代码是隔离的,并且不在全局范围内。

<script>

function hello() {

console.log("hello Bob");

}

</script>

<script>

function hello() {

console.log("hello Fred");

}

</script>

<script>

hello(); // outputs hello Fred

</script>


上面的代码定义了两个函数,没有使用模块,在全局作用域会产生冲突。


JavaScript 模块解决的另一个问题是不必担心 HTML 页面上脚本元素的顺序:

<script>

hello(); // 💥 - Uncaught ReferenceError: hello is not defined

</script>

<script>

function hello() {

console.log("hello");

}

</script>


在上面的示例中,定义 hello 函数的脚本元素需要放在调用 hello 函数的脚本元素之前。 如果有很多这样的 Javascript 文件,就很难管理了。

现在 JavaScript 模块通常是如何使用的?


JavaScript 模块语法是在 ES6 中引入的,通常在我们今天构建的应用程序中使用,如下所示:

import React from 'react';

...

export const HomePage = () => ...


上面的示例导入 React 模块并导出 HomePage 组件。


不过,这段代码并没有使用 JavaScript 模块。取而代之的是,Webpack 将其转换为非原生模块,而采用了 IIFE(立即调用函数表达式)来做 。
值得注意的是,Webpack 确实有一个实验性的 outputModule 特性,允许它以原生模块格式发布。希望 Webpack 5 中包含这个功能!

使用原生的 JavaScript 模块


要声明一个引用 JavaScript 模块代码的脚本元素,需要将类型属性设置为module:

<script type="module">

import { hello } from "/src/a.js";

hello();

</script>


这是 src 文件夹中 a.js 中的 JavaScript:

// /src/a.js

import { hellob } from "/src/b.js";

import { helloc } from "/src/c.js";

export function hello() {

hellob();

helloc();

}


因此,在 a.js 中的 hello 函数,调用了在 b.js 中调用 hellob,在 c.js 中调用 helloc。


这是来自 b.js 和 c.js 的 JavaScript:

// /src/b.js

export function hellob() {

console.log("hello b");

}

// /src/c.js

export function helloc() {

console.log("hello c");

}


请注意,我们需要提供要导入的文件的完整相对路径,并且还需要包含文件扩展名。
我们可能更习惯于一个简单的导入说明符,如下所示:

import { hello } from "a";


稍后我们将再次介绍原生的导入说明符。


还请注意,我们不必在 HTML 文件中声明所有模块。
浏览器在运行时会去解析它们。


需要注意的是,不能从普通脚本元素使用 JavaScript 模块。
例如,如果我们尝试不使用 type 属性,脚本元素将不会被执行:

<script>

// 💥 - 不能在模块外部使用import语句

import { hello } from "/src/a.js";

hello();

</script>

用 JavaScript 模块编写的代码在默认情况下以 严格模式 执行。
所以没有必要在代码顶部使用 use strict:

<script type="module">

let name = "Fred";

let name = "Bob"; // 💥 - Identifier 'name' has already been declared

</script>

JavaScript模块错误


让我们以前面的类似示例为例,其中有 JavaScript 模块 a,b,c。模块 a 依赖于模块 b 和 c。模块 b 和c没有依赖关系。


假设 c.js 中包含运行时错误:

export function helloc() {

consol.log("hello c"); // 💥 - Uncaught ReferenceError: consol is not defined

}


从 HTML 文件调用代码的方法如下:

<script type="module">

import { hello } from "/src/a.js";

hello();

</script>

在 a.js文件中:

import { hellob } from "/src/b.js";

import { helloc } from "/src/c.js";

export function hello() {

hellob();

helloc(); // 💥

hellob(); // never executed 从未执行

}


正如我们所预料的那样,第二次调用 hellob 时永远不会被调用。


如果c.js中的问题是编译错误:

// 注:错写了 function 这个单词

export functio helloc() {

console.log("hello c");

}

模块中没有代码被执行:

<script type="module">

// 💥 - Unexpected token 'export'

// no code is executed

import { hello } from "/src/a.js";

hello();

</script>

其他模块中的代码可以正常执行。[_注:再有一个script 设置 type 为 module,可以正常执行,并不会受报错模块的影响,因为每个模块是独立的,没依赖关系互不受影响]_
_

浏览器支持


所有的现代浏览器都支持原生模块,但不幸的是,IE不支持。
但是,有一种方法可以让我们在支持原生模块的浏览器上使用它们,并为不支持它们的浏览器提供一种退路。
使用 script 元素上的 nomodule 属性来实现这一点:


[注:设置了 nomodule,在支持原生模块的浏览器中不执行,可用于在不支持模块化JavaScript的旧浏览器中提供回退脚本]

<!--支持原生模块浏览器执行-->

<script type="module"></script>

<!--不支持原生模块浏览器执行-->

<script nomodule></script>


Rollup,可以很好地输出 ES 模块文件和非 ES 模块文件:

export default [{

...

output: {

file: 'bundle-esm.js',

format: 'es'

}

},{

...

output: {

file: 'bundle.js',

format: 'iife'

}

}];

瀑布流式


让我们看看一个示例,其中有引用来自 CDN 的模块:

<script type="module">

import intersection from "https://cdn.jsdelivr.net/npm/lodash-es@4.17.15/intersection.min.js";

console.log(intersection([2, 1], [2, 3]));

</script>


模块依赖于其他模块,而这些模块又依赖于其他模块。因此,在执行脚本元素之前,会下载并解析所有依赖项。


image.png

预加载模块


JavaScript 模块可以预加载使用 modulepreload 资源提示:

<link

rel="modulepreload"

href="https://cdn.jsdelivr.net/npm/lodash-es@4.17.15/intersection.js"

/>

即在其他模块下载之前先下载并解析此模块:
image.png
目前只有 Chrome 和 Edge 支持预加载模块。Firefox 和 Safari 将对模块正常下载。

动态导入


动态导入 是在运行时可以根据不同条件在其中导入代码:

<script type="module">

if (new Date().getSeconds() < 30) {

import("/src/a.js").then(({ helloa }) =>

helloa()

);

} else {

import("/src/b.js").then(({ hellob }) =>

hellob()

);

}

</script>


这对于某些使用率较低的大型模块很有用。这也可以减少浏览器中应用程序的内存占用。

使用导入映射说明符


回到我们如何在import语句中引用模块:

import { hello } from "/src/a.js";

import intersection from "https://cdn.jsdelivr.net/npm/lodash-es@4.17.15/intersection.min.js


如果我们仔细想想,除非指定模块的完整路径,否则浏览器怎么知道在哪里找到它呢?
所以,语法是有意义的,即使我们不习惯它。


有一种方法可以将导入说明符与一个被提议的称为导入映射(import-maps)的特性一起使用。
这是一个在特殊的 importmap 脚本元素中定义的映射,需要在引用模块的脚本元素之前定义:

<script type="importmap">

{

"imports": {

"b": "/src/b.js",

"lowdash-intersection": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.15/intersection.min.js"

}

}

</script>


为每个依赖模块提供一个纯导入说明符名称。然后可以在 import 语句中使用定义好的说明符:

<script type="module">

import { hellob } from "b";

hellob();

import intersection from "lowdash-intersection";

console.log(intersection([2, 1], [2, 3]));

</script>


目前,导入映射在浏览器中不可用。但是,此功能可通过以下实验性标记在 Chrome 中使用:chrome:// flags /#enable-experimental-web-platform-features
**

依赖项必须发布的是ES模块


重要的一点是,库必须发布为原生模块格式,以便开发者将库用作原生模块使用。不幸的是,目前这种情况并不常见。例如,React尚未发布为原生模块。

原生模块相对于非原生模块的好处


与 IIFE 模块之类的模块相比,原生模块的一个好处是需要下载到浏览器、解析然后执行,相对来说代码更少。
原生模块也可以并行地、异步地下载和解析。因此,原生模块对于大型依赖树可能执行得更快。
此外,预加载模块可能意味着用户可以更快地与页面交互,因为这些代码是从主线程中解析出来的。


除了性能上的提高之外,新的浏览器特性还可能构建在模块之上,因此使用原生模块是一种验证未来的代码。

结束语

当前最流行的浏览器都可以使用本机模块,并且可以为IE提供备份。Rollup已经可以以这种格式发布了,而且Webpack支持似乎正在进行中。现在,我们所需要的是更多的库开始以原生模块发布。

github博客地址:https://github.com/WYseven/bl...。
如果对你有帮助,请关注【前端技能解锁】:
前端技能解锁

以上是 Javascript 模块 的全部内容, 来源链接: utcz.com/a/41692.html

回到顶部