Quiet
  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我

bajiu

  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我
Quiet主题
  • JavaScript

前端模块化之ESModule

bajiu
前端

2021-06-08 20:49:01

如何在浏览器和 Node.js 之中加载 ES6 模块,以及实际开发中经常遇到的一些问题(比如循环加载)

加载规则

浏览器加载 ES6 模块,也使用 <script> 标签,但是要加入 type="module" 属性

<script type="module" src="./foo.js"></script>

由于 type 属性设为 module ,所以浏览器知道这是一个 ES6 模块

浏览器对于带有 type="module" 的 <script> ,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了 <script> 标签的defer属性

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

如果网页有多个<script type="module">, 它们会按照在页面出现的顺序依次执行

<script> 标签的 async 属性也可以打开, 这时只要加载完成, 渲染引擎就会中断渲染立即执行. 执行完成后, 再恢复渲染

<script type="module" src="./foo.js" async></script>
  • 对于外部的模块脚本(上例是foo.js),有几点需要注意:
    代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中, 可以使用 import 命令加载其他模块 (.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用 export 命令输出对外接口
  • 模块之中,顶层的 this 关键字返回 undefined, 而不是指向 window. 也就是说, 在模块顶层使用this关键字,是无意义的
  • 同一个模块如果加载多次,将只执行一次

ES6 模块与 CommonJS 模块的差异

讨论 Node.js 加载 ES6 模块之前, 必须了解 ES6 模块与 CommonJS 模块完全不同

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
  • CommonJS 模块的 require() 是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段。

第二个差异是因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

CommonJS 模块输出的是值的拷贝, 也就是说, 一旦输出一个值, 模块内部的变化就影响不到这个值.

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import, 就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import 有点像 Unix 系统的 **”符号连接”**,原始值变了, import加载的值也会跟着变. 因此, ES6 模块是动态引用, 并且不会缓存值, 模块里面的变量绑定其所在的模块

Node.js 的模块加载方法

概述

JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。

CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是, CommonJS 模块使用require() 和 module.exports, ES6 模块使用 import 和 export

它们采用不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。

Node.js 要求 ES6 模块采用 .mjs 后缀文件名。也就是说,只要脚本文件里面使用 import 或者 export 命令,那么就必须采用 .mjs 后缀名. Node.js 遇到 .mjs 文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定**”use strict”**

如果不希望将后缀名改成 .mjs, 可以在项目的package.json 文件中, 指定 type 字段为 module

{
   "type": "module"
}

一旦设置了以后, 该目录里面的 JS 脚本, 就被解释用 ES6 模块

# 解释成 ES6 模块
$ node my-app.js

如果这时还要使用 CommonJS 模块, 那么需要将 CommonJS 脚本的后缀名都改成.cjs. 如果没有type字段, 或者type字段为commonjs, 则.js脚本会被解释成 CommonJS 模块

总结为一句话: .mjs文件总是以 ES6 模块加载, .cjs 文件总是以 CommonJS 模块加载, .js文件的加载取决于 package.json 里面 type 字段的设置

package.json 的 main 字段

package.json 文件有两个字段可以指定模块的入口文件: main 和 exports. 比较简单的模块, 可以只使用 main 字段, 指定模块加载的入口文件

// ./node_modules/es-module-package/package.json
{
  "type": "module",
  "main": "./src/index.js"
}

上面代码指定项目的入口脚本为./src/index.js, 它的格式为 ES6 模块。如果没有 type 字段, index.js就会被解释为 CommonJS 模块。

然后, import s命令就可以加载这个模块。

package.json 的 exports 字段

exports 字段的优先级高于 main 字段. 它有多种用法

(1) 子目录别名

package.json 文件的 exports 字段可以指定脚本或子目录的别名

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./submodule": "./src/submodule.js"
  }
}

上面的代码指定 src/submodule.js 别名为submodule ,然后就可以从别名加载这个文件.

import submodule from 'es-module-package/submodule';
// 加载 ./node_modules/es-module-package/src/submodule.js

CommonJS 模块加载 ES6 模块

CommonJS 的 require() 命令不能加载 ES6 模块, 会报错, 只能使用 import() 这个方法加载

(async () => {
  await import('./my-app.mjs');
})();

上面代码可以在 CommonJS 模块中运行

require() 不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层 await 命令,导致无法被同步加载

ES6 模块加载 CommonJS 模块

ES6 模块的 import 命令可以加载 CommonJS 模块, 但是只能整体加载, 不能只加载单一的输出项

// 正确
import packageMain from 'commonjs-package';

// 报错
import { method } from 'commonjs-package';

这是因为 ES6 模块需要支持静态代码分析,而 CommonJS 模块的输出接口是 module.exports, 是一个对象,无法被静态分析,所以只能整体加载。

循环加载

“循环加载” (circular dependency)指的是, a脚本的执行依赖b脚本, 而b脚本的执行又依赖a脚本

通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现

上一篇

下一篇

WebGL着色器语言三种变量( attribute 、uniform 和 varying )

©2024 By bajiu.