在这篇文章中,咱们将介绍规范的 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.jsimport { 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.jsexport function hellob() {  console.log("hello b");}
// /src/c.jsexport 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>


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



预加载模块


JavaScript 模块能够预加载应用 modulepreload 资源提醒:

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

即在其余模块下载之前先下载并解析此模块:

目前只有 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...。
如果对你有帮忙,请关注【前端技能解锁】: