原文:Using ES modules in browsers with import-maps
介绍
ES modules 已经在JS社区内被讨论很长时间了。主要的目标是给JS带来标准化的模块系统。当一些东西变成标准后,后续一般会跟着两个步骤。首先,是被EcmaScript标准委员会接受为标准并最终敲定;第二 浏览器开始实现并且支持它。但这一步会花费一些时间,并且会有一些兼容性问题。
好消息是大多数的浏览器已经开始支持ES modules,如下图:
给JS世界增加模块化,还需要很多的努力。例如:
- Node.js实现了自己的模块系统
- 打包和构建工具例如webpack、babel和Browserify也整合了模块化的用法。
一些模块化的定义已经被实现。两个使用较少的是:
然而,广泛应用的是:
- CommonJS 同时也是nodejs的模块化实现
- ES modules ,原生JS支持的模块化标准
为什么需要ES modules ?
在JS中,和其他的编程语言类似,大多数的关注点是构建、管理和使用变量和函数。你可以使用这些来构建业务逻辑,并把最终的结果呈现给用户。然而,随着大量的变量、函数和文件的增长,可维护性变的很重要。例如,你不能通过修改变量而影响到代码的其他部分,即时他们有相同name。
在文件级别,我们可以通过作用域的处理,来解决这个问题,如下所示:
// file.jsvar foo = "I'm global";var bar = "So am I";function () {var foo = "I'm local, the previous 'foo' didn't notice a thing";var baz = "I'm local, too";function () {var foo = "I'm even more local, all three 'foos' have different values";baz = "I just changed 'baz' one scope higher, but it's still not global";bar = "I just changed the global 'bar' variable";xyz = "I just created a new global variable";}}
如果在多个不同的文件都要这样的处理?
多数情况下,你会对所有的文件都做上述的处理。但这也会带来一些问题。不同文件的依赖和共享库会变的很重要。而且你需要时刻关注载入脚本文件的顺序,这也是一种隐式的文件依赖管理,如下所示:
<script src="index1.js"></script><script src="index2.js"></script><script src="main.js"></script>
类似于上面的代码示例,文件index1.js引用了index2.js中的函数,但是函数并不会执行因为代码的执行流程还没有到达index2.js文件。除了依赖管理,用script标签引入文件还有其他的一些问题:
- 较慢的执行时间,因为每一个Request都会阻塞线程。
- 每一个脚本文件初始话http请求时都会造成性能问题。
当面对上述问题是,你可能想重构或者修改代码。但是每一次你修改代码,你都在担心会不会影响之前的函数。这时候模块化就出来拯救了。
ES modules 通常情况下,所谓的模块化是指定义了一组变量和函数,并把他们组合在一起,绑定在模块域下。这意味着你可以在同样的模块里引用变量,你也可以显示的导出和引入其他的模块。在这个架构下,如果有其他的模块被删除了或者代码执行问题,你可以知道问题出在哪里。
浏览器中的 ES modules
在Html中引入模块,需要在script标签添加type="module"属性,浏览器会知道把它解析成一个模块。
// External Script<script type="module" src="./index.js"></script>// Inline Script<script type="module">import { main } from './index.js';// ...</script>
在这个例子中,浏览器会请求高层的script脚本,并把他们放在一个叫做 module map并且有一个唯一的引用。这种情况下,如果遇到其他的脚本指向了同样的引用,他就会跳到下一个脚本,所以每一个脚本只会被解析一次。
假设index.js内容如下所示:
// index.jsimport { something } from './something.js'export const main = () => {console.log('do something');}//..
上述文件中,我们用export和import暴露和使用依赖。当浏览器完成异步请求并且解析完依赖后,浏览器开始把嵌套的模块的引用从主脚本开始,直到遍历完所有的嵌套依赖,并把他们放到 module map里。
请求和解析模块,只是浏览器加载模块的第一步。
为什么和怎么使用 import-maps?
在加载模块的构建阶段,有两个初始化的步骤需要执行。
第一个步骤就是模块的解析,需要识别出模块的下载地址。第二个步骤是真正的下载文件的操作。这也是浏览器环境下和nodejs环境下模块处理最大的区别。由于Node.js可以直接访问文件系统,所以和浏览器的处理方式不一致。这也是为什么在nodejs中引入依赖可以用下面的写法:
const _lodash = require('lodash');
在浏览器环境下,大多数时候你会如下写法:
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结构:
// index.html<script type="importmap">{"imports": {"lodash": "/node_modules/lodash-es/lodash.js"}}</script>
在定义完map结构之后,你就可以在代码的任何地方直接引入lodash。
import jQuery from 'jquery';
如果你没有使用import-maps,你不得不使用如下方式引入模块,但这和一些构建工具中定义模块的方式不一致。
import jQuery from "/node_modules/jQuery/index.js";
使用import-maps可以和目前的依赖模块的使用保持一致。
在Nodejs中引入模块时,可以省略文件后缀,如下所示:
// requiring something.js fileconst something = require('something');
在使用import-maps也可以省略文件后缀,
{"imports": {"lodash/map": "/node_modules/lodash/map.js"}}
如上所示,当我们定义模块的名称而没有.js后缀时,可以通过如下两种方式引入模块,
// Either thisimport map from "lodash/map"// Orimport map from "lodash/map.js"
通常情况下,当我们引入npm包时,npm包中会包含一些子模块,如果你想引用子模块,你可以类似如下的写法:
{"imports": {"lodash": "/node_modules/lodash/lodash.js","lodash/": "/node_modules/lodash/"}}
// You can directly import lodashimport _lodash from "lodash";// or import a specific mooduleimport _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/
