共计 4818 个字符,预计需要花费 13 分钟才能阅读完成。
在这篇文章中,咱们将介绍规范的 JavaScript 模块,目前是如何在前端应用程序中应用的,以及将来咱们可能会如何应用它们。
JavaScript 模块有时被称为 ESM,它代表 ECMAScript 模块。
什么是 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" src="app.js"></script>
<!-- 不反对原生模块浏览器执行 -->
<script nomodule src="classic-app-bundle.js"></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>
模块依赖于其余模块,而这些模块又依赖于其余模块。因而,在执行脚本元素之前,会下载并解析所有依赖项。
预加载模块
JavaScript 模块能够预加载应用 modulepreload 资源提醒:
<link
rel="modulepreload"
href="https://cdn.jsdelivr.net/npm/lodash-es@4.17.15/intersection.js"
/>
即在其余模块下载之前先下载并解析此模块:
目前只有 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…。
如果对你有帮忙,请关注【前端技能解锁】: