6

在这篇文章中,我们将介绍标准的 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.jsc.jsJavaScript

// /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 依赖于模块 bc。模块 bc没有依赖关系。


假设 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>


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


image.png

预加载模块


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

<link
  rel="modulepreload"
  href="https://cdn.jsdelivr.net/npm/lodash-es@4.17.15/intersection.js"
/>

即在其他模块下载之前先下载并解析此模块:
image.png
目前只有 ChromeEdge 支持预加载模块。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...
如果对你有帮助,请关注【前端技能解锁】:

前端技能解锁
)


戎马
2.4k 声望346 粉丝

前端码农一枚,上班一族,爱文学一本。ส็็็็็็็็็็็็็็ ส้้้้้้้้้้้้้้้้้้้。