原文:Using ES modules in browsers with import-maps
image.png

介绍

ES modules 已经在JS社区内被讨论很长时间了。主要的目标是给JS带来标准化的模块系统。当一些东西变成标准后,后续一般会跟着两个步骤。首先,是被EcmaScript标准委员会接受为标准并最终敲定;第二 浏览器开始实现并且支持它。但这一步会花费一些时间,并且会有一些兼容性问题。
好消息是大多数的浏览器已经开始支持ES modules,如下图:
image.png

给JS世界增加模块化,还需要很多的努力。例如:

  • Node.js实现了自己的模块系统
  • 打包和构建工具例如webpack、babel和Browserify也整合了模块化的用法。

一些模块化的定义已经被实现。两个使用较少的是:

  • AMD 或者异步模块定义
  • UMD 或者 通用模块定义

然而,广泛应用的是:

  • CommonJS 同时也是nodejs的模块化实现
  • ES modules ,原生JS支持的模块化标准

    为什么需要ES modules ?

    在JS中,和其他的编程语言类似,大多数的关注点是构建、管理和使用变量和函数。你可以使用这些来构建业务逻辑,并把最终的结果呈现给用户。然而,随着大量的变量、函数和文件的增长,可维护性变的很重要。例如,你不能通过修改变量而影响到代码的其他部分,即时他们有相同name。

在文件级别,我们可以通过作用域的处理,来解决这个问题,如下所示:

  1. // file.js
  2. var foo = "I'm global";
  3. var bar = "So am I";
  4. function () {
  5. var foo = "I'm local, the previous 'foo' didn't notice a thing";
  6. var baz = "I'm local, too";
  7. function () {
  8. var foo = "I'm even more local, all three 'foos' have different values";
  9. baz = "I just changed 'baz' one scope higher, but it's still not global";
  10. bar = "I just changed the global 'bar' variable";
  11. xyz = "I just created a new global variable";
  12. }
  13. }

如果在多个不同的文件都要这样的处理?

多数情况下,你会对所有的文件都做上述的处理。但这也会带来一些问题。不同文件的依赖和共享库会变的很重要。而且你需要时刻关注载入脚本文件的顺序,这也是一种隐式的文件依赖管理,如下所示:

  1. <script src="index1.js"></script>
  2. <script src="index2.js"></script>
  3. <script src="main.js"></script>

类似于上面的代码示例,文件index1.js引用了index2.js中的函数,但是函数并不会执行因为代码的执行流程还没有到达index2.js文件。除了依赖管理,用script标签引入文件还有其他的一些问题:

  • 较慢的执行时间,因为每一个Request都会阻塞线程。
  • 每一个脚本文件初始话http请求时都会造成性能问题。

当面对上述问题是,你可能想重构或者修改代码。但是每一次你修改代码,你都在担心会不会影响之前的函数。这时候模块化就出来拯救了。

ES modules 通常情况下,所谓的模块化是指定义了一组变量和函数,并把他们组合在一起,绑定在模块域下。这意味着你可以在同样的模块里引用变量,你也可以显示的导出和引入其他的模块。在这个架构下,如果有其他的模块被删除了或者代码执行问题,你可以知道问题出在哪里。

浏览器中的 ES modules

在Html中引入模块,需要在script标签添加type="module"属性,浏览器会知道把它解析成一个模块。

  1. // External Script
  2. <script type="module" src="./index.js"></script>
  3. // Inline Script
  4. <script type="module">
  5. import { main } from './index.js';
  6. // ...
  7. </script>

在这个例子中,浏览器会请求高层的script脚本,并把他们放在一个叫做 module map并且有一个唯一的引用。这种情况下,如果遇到其他的脚本指向了同样的引用,他就会跳到下一个脚本,所以每一个脚本只会被解析一次。
假设index.js内容如下所示:

  1. // index.js
  2. import { something } from './something.js'
  3. export const main = () => {
  4. console.log('do something');
  5. }
  6. //..

上述文件中,我们用exportimport暴露和使用依赖。当浏览器完成异步请求并且解析完依赖后,浏览器开始把嵌套的模块的引用从主脚本开始,直到遍历完所有的嵌套依赖,并把他们放到 module map里。
请求和解析模块,只是浏览器加载模块的第一步。

为什么和怎么使用 import-maps?

在加载模块的构建阶段,有两个初始化的步骤需要执行。

第一个步骤就是模块的解析,需要识别出模块的下载地址。第二个步骤是真正的下载文件的操作。这也是浏览器环境下和nodejs环境下模块处理最大的区别。由于Node.js可以直接访问文件系统,所以和浏览器的处理方式不一致。这也是为什么在nodejs中引入依赖可以用下面的写法:

  1. const _lodash = require('lodash');

在浏览器环境下,大多数时候你会如下写法:

  1. import * as _lodash from 'lodash';

在上述的例子中,Nodejs可以直接访问 ‘ lodash ’模块,是因为nodejs可以直接访问 filesystem。但是浏览器只接受URL的方式,因为浏览器获取模块的方式只能是通过网络下载。直到新的ES modules的提议被引入,叫做 import-maps,可以解决这个问题,并且提供统一的观感对于浏览器、构建器和打包器中模块的用法。

import-maps会根据导入的模块名称定义一个map结构,并且允许开发者提供裸的的导入标识例如 import 'jquery'

通过给script标签提供type="importmap"属性,你可以定义一个map结构,然后定义一系列最基本的导入名称和相对/绝对URL地址。如下所示,定义map结构:

  1. // index.html
  2. <script type="importmap">
  3. {
  4. "imports": {
  5. "lodash": "/node_modules/lodash-es/lodash.js"
  6. }
  7. }
  8. </script>

在定义完map结构之后,你就可以在代码的任何地方直接引入lodash

  1. import jQuery from 'jquery';

如果你没有使用import-maps,你不得不使用如下方式引入模块,但这和一些构建工具中定义模块的方式不一致。

  1. import jQuery from "/node_modules/jQuery/index.js";

使用import-maps可以和目前的依赖模块的使用保持一致。

在Nodejs中引入模块时,可以省略文件后缀,如下所示:

  1. // requiring something.js file
  2. const something = require('something');

在使用import-maps也可以省略文件后缀,

  1. {
  2. "imports": {
  3. "lodash/map": "/node_modules/lodash/map.js"
  4. }
  5. }

如上所示,当我们定义模块的名称而没有.js后缀时,可以通过如下两种方式引入模块,

  1. // Either this
  2. import map from "lodash/map"
  3. // Or
  4. import map from "lodash/map.js"

通常情况下,当我们引入npm包时,npm包中会包含一些子模块,如果你想引用子模块,你可以类似如下的写法:

  1. {
  2. "imports": {
  3. "lodash": "/node_modules/lodash/lodash.js",
  4. "lodash/": "/node_modules/lodash/"
  5. }
  6. }
  1. // You can directly import lodash
  2. import _lodash from "lodash";
  3. // or import a specific moodule
  4. import _shuffle from "lodash/shuffle.js";

参考:
https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
https://github.com/WICG/import-maps
https://www.sitepoint.com/understanding-es6-modules/