本文为稀土技术社区首发签约文章,30 天内制止转载,30 天后未获授权制止转载,侵权必究!

假如你看过某个现代 JavaScript 库的代码,一定会困惑其复杂的模块适配,下图现在干流的 JavaScript 库模块适配计划,截取自我的新书《现代 JavaScript 库开发:技术、原理与实战》。

一文搞懂 JavaScript 模块化

本文梳理 JavaScript 模块化的历史和现状,不仅介绍不同模块体系是什么,而是深入介绍不同模块体系诞生的原因和处理的问题,阅读本文将为你解开许多 JavaScript 模块化的疑问。

传统 JavaScript

JavaScript 诞生之初,是一门在浏览器运用的脚本语言,并没有供给模块体系,站在其时的视角来看,的确也并不需求模块体系。

在浏览器中,假如想引证一个脚本文件,只需求运用 script 标签引进即可,十分简略直观,如下所示即可可入 jQuery。

一文搞懂 JavaScript 模块化

script 的方法,并没有处理依靠的问题,依靠关系的处理,需求咱们手动保证引进的次序问题,2013 年我曾写过一个较为复杂的绘图程序Painter,手动维护依靠关系,如下图所示,从前让我十分苦楚。

一文搞懂 JavaScript 模块化

JavaScript 缺少模块带来两个问题,一个是封装的问题,一个是依靠的管理问题,关于怎么支撑模块的问题,浏览器社区和 Node.js 社区别离给出了不同的探索和计划,下面介绍其间影响比较大的模块体系。

Node.js 模块计划

Node.js 是浏览器之外的另一个运行时,其创建之初,为了弥补 JavaScript 缺失模块的问题,其带来了commonjs 标准,在 Node.js 中模块是强制的,commonjs 的模块定义和运用示例如下,需求留意外面的 define 在 Node.js 中是主动增加的,不需求写。

define(function (require, exports, module) {
    //运用event 模块
    var ec = require("event");
});

跟着 Node.js 的发展,commonjs 影响力也越来越大,社区中许多库都供给了 commonjs 的引进方法,在当时这个时间点(2024-03-11),社区中仍然存在很多仅支撑 commonjs 的库。

浏览器模块计划

当浏览器社区考虑引进模块体系时,发现 commonjs 并不适合浏览器,这是因为 commonjs 是为同步导入规划的模块体系,在 Node.js 中引进一个模块是经过文件体系,同步十分合理,也十分简略。但在浏览器环境中,都是根据网络加载 js 文件的,需求规划一套异步加载标准。

其间最杰出的异步加载标准是AMD(Asynchronous Module Definition),假如要运用 AMD 模块,还需求加载器,其间 RequireJS 是运用最为广泛的 AMD 模块加载器。

AMD 标准中定义模块的方法如下:

define(["beta"], function (beta) {
  bata.***//调用模块
});

笔者早年间写的变色方块小游戏,便是运用 RequireJS 作为加载器的,其源代码中只加载一个进口文件。

一文搞懂 JavaScript 模块化

其依靠的其他模块都经过 RequireJS 异步导入,示例如下:

一文搞懂 JavaScript 模块化

现在 AMD 现已很少运用,仅作为了解即可,但在其时 AMD 也有许多用户,许多 JS 库都供给了 AMD 的引进方法。

分裂的社区

在 AMD 和 commonjs 双雄并存的时代,不得不面临一个巨大的问题,许多 JS 库,都只供给一种引进方法,这让社区别裂开来,怎么在一种模块中运用另一种模块的库,成了模块加载器的急需处理的问题。

AMD 怎么给 Node.js 运用

RequireJS 供给了在 Node.js 中运用 AMD 模块的计划,其运用方法如下所示:

一文搞懂 JavaScript 模块化

为了完成这个功用,给 RequireJS 中增加了冗余代码,其部分源码完成如下所示,即便今日来看,RequireJS 的完成也颇为巧妙。

一文搞懂 JavaScript 模块化

commonjs 怎么给浏览器运用

那么很多的 commonjs 模块怎么让浏览器运用呢?最早开始探索的先驱是# Browserify,其经过预编译的方法,将 commonjs 编译为传统 script 的方法,其运用方法如下所示:

一文搞懂 JavaScript 模块化

Browserify 的这种方法被后来的东西学习并发扬光大,今日咱们常用的东西都是根据这种方法,比如 webpack,rollup,pracel,vite 等。

怎么交融 AMD 和 commonjs?

两套模块体系的另一个困扰来自库开发者,我到底该供给哪个模块给咱们运用呢?有什么方法能够交融 AMD 和 commonjs 呢?

这个问题终究被 UMD 处理,UMD的全称是 Universal Module Definition。和它姓名的意思一样,这种标准基本上能够在任何一个模块环境中作业。

UMD 的规划十分精巧,其支撑传统 JavaScript,AMD 和 commonjs,关于传统 JavaScript,它设置支撑了相似 jQuery 中的 noConflict 方法,一段典型的 UMD 代码如下所示:

(function (root, factory) {
  var Data = factory(root);
  if ( typeof define === 'function' && define.amd) {
    // AMD
    define('data', function() {
      return Data;
    });
  } else if ( typeof exports === 'object') {
    // Node.js
    module.exports = Data;
  } else {
    // Browser globals
    var _Data = root.Data;
    Data.noConflict = function () {
      if (root.Data === Data) {
        root.Data = _Data;
      }
      return Data;
    };
    root.Data = Data;
  }
}(this, function (root) {
	var Data = ...
	//自己的代码
	return Data;
}));

其实故事到此本该就结束了,一些首要问题,现已基本处理了,没想到半路杀出个程咬金——ESM,对 AMD 和 commonjs 完成了降维打击。

ESM

2015 年 ECMAScript6 发布,也被称为 ECMAScript2015,其为 JavaScript 语言带来了原生的模块体系 ECMAScript6 Module,下文咱们简称为 ESM。

网上有许多文章介绍 ESM,ESM 的科普不是本文的重点,这儿不再展开介绍,commonjs 和 ESM 的引证模块的对比如下:

// commonjs
let { stat, exists, readfile } = require("fs");
// ESM
import { stat, exists, readFile } from "fs";

打包东西怎么支撑 ESM

ES6 尽管带来了 ESM,但并未供给实际的运用方法,并没有环境支撑 ESM,为了运用 ESM 需求凭借打包东西,最早支撑 ESM 的打包东西应该是 rollup,rollup 给的计划是库供给两个进口,一个是 esm,一个是 commonjs,这样让库一起在 rollup 和其他打包东西中运用。

rollup 主张库中增加module字段,来标记 ESM 的进口文件,rollup 有一篇文档详细介绍这个字段 pkg.module,值得一提的是因为不同东西的原因,完成同样的诉求,存在两个字段,一个是module,一个是jsnext:main

假如库中存在如下字段,rollup 会加载module字段文件,其他打包东西则加载main

一文搞懂 JavaScript 模块化

后来 webpack 也供给了支撑,其是经过增加装备来完成的,mainFields中的main前面增加module装备即可,如下所示:

一文搞懂 JavaScript 模块化

你或许会猎奇最前面的browser字段是啥,那就持续往下看吧。

怎么处理 Node.js 和浏览器差异的问题

一个库一起支撑 Node.js 和浏览器,理想很夸姣,然后实际却或许遇到挑战,因为环境的不一致问题,同一个库,在不同环境中,或许存在不同的完成方法。

举个比如,咱们了解的 axios 库,其功用是供给发送请求的友爱接口,一起支撑在 Node.js 和浏览器中运用,其在浏览器中根据 xhr 完成,但在 Node.js 因为没有 xhr,其根据 http 模块完成。

关于这个问题,能够经过供给两个 npm 包的方法来处理,但这并不优美,库的开发者或许希望只供给一个 npm 包。还有一种处理方法,便是在一个 npm 包中,写分支代码,但这种方法会让浏览器环境中多出来 Node.js 中的代码,尽管能够经过打包东西,避免将 http 模块打包进来,但仍然有冗余代码。

为了处理这个问题,package-browser-field-spec诞生了,其经过在 package.json 中增加 browser 字段的方法,来区别不同的环境,关于浏览器环境来说,打包东西会主动引证 browser 字段的内容。

其运用方法如下所示,即支撑整个进口替换,也支撑部分文件的替换。

一文搞懂 JavaScript 模块化

一文搞懂 JavaScript 模块化

webpack 关于 browser 的支撑也经过增加装备的方法,上面咱们看到 webpack 装备中的 browser 字段,便是完成这个功用的。

浏览器怎么支撑 ESM

除了凭借打包东西,浏览器也对 ESM 供给了原生支撑,其经过给 script 标签增加type="module"属性的方法,来区别传统加载,还是模块化加载。

举个比如,咱们有main.jshello.js两个文件,其间main.js依靠hello.js,内容如下所示:

一文搞懂 JavaScript 模块化

一文搞懂 JavaScript 模块化

假如经过传统 script 标签直接加载存在importexport的 js 文件会报错,如下所示:

一文搞懂 JavaScript 模块化

只需简略增加type="module"即可,示例如下,现在咱们运用 vite 在 dev 形式下,便是根据浏览器原生 ESM 加载模块的。

一文搞懂 JavaScript 模块化
如下

Node.js 怎么支撑 ESM

Node.js 对 ESM 的支撑比较崎岖,其间完成计划也修改过,导致其比较复杂,Node.js 从 18 版别开始供给了较为安稳的 ESM 支撑。

Node.js 支撑 ESM 的挑战是,怎么兼容很多的存量 commonjs 模块,Node.js 供给了两种方法,一种是经过后缀名区别,一种是经过给 package.json 增加 type 字段来区别。

在一个 npm 包中,能够一起存在这种量状况,归纳起来,能够分为如下状况,.mjs是 ESM,.cjs是 commonjs,.js要看 package.json 的type字段。

  • .mjs
  • .js
    • package.json 没有 type
    • package.json type=commonjs
    • package.json type=module
  • .cjs

有一点需求留意,在 Node.js 中 ESM 中能够引证 commonjs,在 commonjs 中不能引证 ESM,假如想了解背面的原因,以及更多细节,能够检查 Node.js 官方的文档:package 包模块

现在 webpack 中也支撑.mjs,能够经过如下装备来完成:

一文搞懂 JavaScript 模块化

exports

那么一个库,怎么给旧版别 Node.js 供给 commonjs,给新版别 Node.js 供给 ESM 呢,Node.js 供给的答案是引进新的 exports 字段,exports 是从头规划的接口,其支撑咱们前面提到的全部功用,比如 browser。

下面是 exports 的示例:

  • 关于不支撑 exports 的环境,会持续读取 main 字段
  • 支撑 exports,但不支撑 ESM 的环境,会运用 require 字段
  • 支撑 exports,且支撑 ESM 的环境,会运用 import 字段

一文搞懂 JavaScript 模块化

exports 本身的规则也比较复杂,假如想正确运用 exports 主张仔细阅读标准,下面是东西库 axios 的的 exports 装备,其间 types 是给 typescript 运用的,browser 是支撑咱们前面提到的 browser 功用。

一文搞懂 JavaScript 模块化

现在 webpack 也支撑 exports 导入,能够经过如下装备来完成:

一文搞懂 JavaScript 模块化

双包问题

关于 Node.js 来说,支撑 ESM 并不简略,这儿题一个双包的问题,假如咱们的 npm 包经过前面的 exports 字段,供给了 ESM 和 commonjs 两种进口,在实际运用中,两种进口或许会被一起运用,导致咱们的代码被履行两头。

举个比如,咱们的包是 A,项目中运用 A 的 ESM,项目依靠另一个包 B,B 依靠 A 的 commonjs 时,就会存在双包的问题。

假如咱们的包是无副作用的代码,则履行两次问题不到,假如是希望单例的包,这样则会造成严重问题。

处理双包的问题,现在有两个思路:

一种是保守方法,因为 ESM 能够引进 commonjs,在 commonjs 中完成功用代码,在 ESM 中供给一个包装代码,调用咱们的 commonjs 即可。

一种是急进方法,只供给 ESM 的包,放弃支撑 commonjs 的旧环境。

Deno

仔细观察你会发现,Node.js 加载 ESM 是根据文件途径,而咱们的浏览器是根据 URL,这并不一致,世界上也并不只有 Node.js,比如 Deno 就只支撑 URL 加载,如下所示:

一文搞懂 JavaScript 模块化

那么 Deno 怎么运用咱们的 npm 包呢,答案是经过unpkgesm这种的平台来完成。

不过这要在咱们的 package.json 中增加新的字段,如下所示,unpkg 会主动识别咱们的字段,并供给相应的替换和包装功用:

一文搞懂 JavaScript 模块化

需求留意,在 Node 17 今后,也支撑经过 URL 来加载 ESM。

总结 & JavaScript 库的破局

本文介绍了 JavaScript 中首要模块计划和其背面的原因,希望对您有协助,在日后的作业中看到任何模块,都不再困扰。

关于 JavaScript 库开发者来说,上面介绍的内容都需求掌握,在实际开发中,或许需求一起支撑上面的这些模块体系,或者按自己的库的特性,选择支撑部分。能够看到还是比较费事和繁琐的,为此我专门开发了jslib-base,支撑 10 秒快速搭建一个新库的基础框架,其间现已内置了本文提到的一切模块。

参阅文章