JavaScript 模块基础:ES Modules、CJS 与 ESM 的混用

2021-03-2311:09:32WEB前端开发Comments2,371 views字数 10785阅读模式

JavaScript 模块的发展史开始讲起,到最新的 ES Modules 的,重新认识一下模块。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

1、JavaScript 模块发展史

1.1 Vanilla JS(1995~2009)

JavaScript 被开发出来的时候,是没有模块标准的,因为 JavaScript 的设计初衷就是作为一个 toy script,在浏览器中做一些简单的交互。但是随着互联网的高速发展,人们已经不再满足于简单的交互,而代码的复杂度也日益增长,维护难度也越来越高。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

那么维护指的是维护什么呢?指的是维护变量。因为随着项目不断迭代,多人协同开发是不可避免的。在 JS 初期所有变量都写在全局作用域上,那么很可能出现的问题是什么呢?变量的覆盖、篡改和删除,这是一个很头疼的问题。很可能突然有一天你的功能报错了,就是因为你的某个变量被另一位开发者所删除了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

所以对于模块的引入初衷是为了解对变量的控制。当然还有其他的好处,例如对代码的封装、复用等等。 JavaScript 模块基础:ES Modules、CJS 与 ESM 的混用 那么初期在没有模块标准的支持下,开发者们是如何实现类似模块的效果呢?有 2 种方式。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

1.1.1 Object Literal Pattern(对象字面量)

使用 JS 内置的对象对变量进行控制:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

function Person(name) {
  this.name = name;
}

Person.prototype.talk = function () {
  console.log("my name is", this.name);
};

const p = new Person("anson");
p.talk();
复制代码

这样就可以通过 new Person 的方式把变量都控制在对象内部。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

1.1.2 IIFE(Immediately Invoked Function Expression)

我们知道在 JavaScript 中有作用域(Scope)的概念,在作用域内的变量,只在作用域内可见。在 ES6 之前,作用域只有 2 种,分别是:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

上面提到了对变量的控制,那么肯定是把变量的作用范围控制的越小越好,所以毫无疑问把变量写在函数内是最好的办法。但是,这又引发了另一个问题,函数中的变量要如何提供给外部使用呢? JavaScript 模块基础:ES Modules、CJS 与 ESM 的混用 这个问题在初期并没有很好的解决方法,你必须把变量暴露到全局作用域中,例如经典的 jQuery。 JavaScript 模块基础:ES Modules、CJS 与 ESM 的混用 而开发者们通常会使用 IIFE 去实现:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

// lib.js
(function() {
  const base = 10;
  this.sumDOM = function(id) {
    // 依赖 jQuery
    return base    $(id).text();
  }
})();
复制代码

在 HTML 中引入 lib.js文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

// index.html
<html>
  <head>
    <script src="/path/to/jquery.js">script>
    <script src="/path/to/lib.js">script>
  head>
  <body>
    <script>
      window.sumDOM(20);
    script>
  body>
html>
复制代码

但是 IIFE 有几个问题:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

  • 至少一个变量污染全局作用域;
  • 模块之间的依赖关系模糊,不明确(lib.js 不能直观看出依赖 jquery.js);
  • 加载顺序无法保证,不好维护(必须确保 jquery.js 必须在 lib.js 前加载完成,否则会报错)。

JavaScript 模块基础:ES Modules、CJS 与 ESM 的混用 所以,JavaScript 非常需要一个模块标准来解决上述问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

1.2 Non-Native Module Format & Module Loader(2009~2015)

由于模块能为我们解决上述问题,所以开发者尝试着自己去设计一些非原生模块标准如 CommonJSAMD (Asynchronous Module Definition)UMD (Universal Module Definition),然后搭配对应的 Module Loader 如 cjs-loader、RequireJSSystemJS 可以实现模块的效果,我们下面过一下几个流行的非原生模块标准。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

1.2.1 CommonJS (CJS)

2009 年,来自 Mozilla 的工程师 Kevin 提出了为运行在浏览器以外的 JavaScript 建立一个模块标准 CommonJS,主要应用在服务端如 Node.js。因为使用效果不错,随后也被用在浏览器的模块开发中,但由于浏览器并不支持 CommonJS,所以代码需要通过 Babel 等 transpiler 转换为 ES5 才能在浏览器上运行。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

CommonJS 的特征是使用 require 来导入依赖,exports 来导出接口。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

// lib.js
module.exports.add = function add() {};

// main.js
const { add } = require("./lib.js");
add();
复制代码

1.2.2 AMD

因为 CommonJS 设计初衷是应用在服务端的,所以模块的加载执行也都是同步的(因为本地文件的 IO 很快)。但是同步的方式运用到浏览器就不友好了,因为在浏览器中模块文件都是通过网络加载的,单线程阻塞在模块加载上,这是不可接受的。所以在 2011 年有人提出了 AMD,对 CommonJS 兼容的同时支持异步加载。 JavaScript 模块基础:ES Modules、CJS 与 ESM 的混用 AMD 的特征是使用 define(deps, callback) 来异步加载模块。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

// Calling define with a dependency array and a factory function
define([\'dep1\', \'dep2\'], function (dep1, dep2) {
    //Define the module value by returning a value.
    return function () {};
});
复制代码

1.2.3 UMD

因为 CommonJS 和 AMD 的流行,随后又有人提出了 UMD 的模块标准,UMD 通过对不同的环境特性进行检测,对 AMD、CommonJS 和 Global Variable 三种格式兼容。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

// UMD
(function (root, factory) {
  if (typeof define === \'function\' && define.amd) {
    // AMD
    define([\'jquery\', \'underscore\'], factory);
  } else if (typeof exports === \'object\') {
    // Node, CommonJS-like
    module.exports = factory(require(\'jquery\'), require(\'underscore\'));
  } else {
    // Browser globals (root is window)
    root.returnExports = factory(root.jQuery, root._);
  }
}(this, function ($, _) {
  //    methods
  function a(){};    //    private because it\'s not returned (see below)
  function b(){};    //    public because it\'s returned
  function c(){};    //    public because it\'s returned
  //    exposed public methods
  return {
    b: b,
    c: c
  }
}));
复制代码

因为 UMD 的兼容性好,不少库都会提供 UMD 的版本。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

1.3 ESM(2015~now)

随着 ECMAScript 的逐渐规范化、标准化,终于在 2015 年发布了 ES6(ES 2015),在这次版本更新中,制定了 JS 模块标准即 ES Modules,ES Modules 使用 import 声明依赖,export 声明接口。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/21093.html

// lib.mjs
const lib = function() {};
export default lib;

// main.js
import lib from \'./lib.mjs\';
复制代码

截止到 2018 年,大部分主流浏览器都已经支持 ES Modules,在 HTML 中通过为 :?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定