作者简介:
李中凯
八年多工作经验 前端负责人,
擅长JavaScript/Vue。
掘金文章专栏:https://juejin.im/user/57c7cb8a0a2b58006b1b8666/posts
公众号:1024译站


模块化是大型前端项目的必备要素。JavaScript 从诞生至今,出现过各种各样的模块化方案,让我们一起来盘点下吧。

IIFE 模块

默认情况下,在浏览器宿主环境里定义的变量都是全局变量,如果页面引用了多个这样的 JavaScript 文件,很容易造成命名冲突。

// 定义全局变量
let count = 0;
const increase = () => ++count;
const reset = () => {count = 0;console.log("Count is reset.");
};// 使用全局变量
increase();
reset();

为了避免全局污染,可以用匿名函数包裹起来,这就是最简单的 IIFE 模块(立即执行的函数表达式):

// 定义 IIFE 模块
const iifeCounterModule = (() => {let count = 0;return {increase: () => ++count,reset: () => {count = 0;console.log("Count is reset.");}};
})();// 使用 IIFE 模块
iifeCounterModule.increase();
iifeCounterModule.reset();

IIFE 只暴露了一个全局的模块名,内部都是局部变量,大大减少了全局命名冲突。

每个 IIFE 模块都是一个全局变量,这些模块通常有自己的依赖。可以在模块内部直接使用依赖的全局变量,也可以把依赖作为参数传给 IIFE:

// 定义带有依赖的 IIFE 模块
const iifeCounterModule = ((dependencyModule1, dependencyModule2) => {let count = 0;return {increase: () => ++count,reset: () => {count = 0;console.log("Count is reset.");}};
})(dependencyModule1, dependencyModule2);

一些流行的库在早期版本都采用这模式,比如大名鼎鼎的 jQuery(最新版本也开始用 UMD 模块了,后面会介绍)。

还有一种 IIFE,在 API 声明上遵循了一种格式,就是在模块内部提前定义了这些 API 对应的变量,方便 API 之间互相调用:

// Define revealing module.
const revealingCounterModule = (() => {let count = 0;const increase = () => ++count;const reset = () => {count = 0;console.log("Count is reset.");};return {increase,reset};
})();// Use revealing module.
revealingCounterModule.increase();
revealingCounterModule.reset();

CommonJS 模块(Node.js 模块)

CommonJS 最初叫 ServerJS,是由 Node.js 实现的模块化方案。默认情况下,每个 .js 文件就是一个模块,模块内部提供了一个moduleexports变量,用于暴露模块的 API。使用 require 加载和使用模块。下面这段代码定义了一个计数器模块:

// 定义 CommonJS 模块: commonJSCounterModule.js.
const dependencyModule1 = require("./dependencyModule1");
const dependencyModule2 = require("./dependencyModule2");let count = 0;
const increase = () => ++count;
const reset = () => {count = 0;console.log("Count is reset.");
};exports.increase = increase;
exports.reset = reset;
// 或者这样:
module.exports = {increase,reset
};

使用这个模块:

// 使用 CommonJS 模块
const { increase, reset } = require("./commonJSCounterModule");
increase();
reset();
// 或者这样:
const commonJSCounterModule = require("./commonJSCounterModule");
commonJSCounterModule.increase();
commonJSCounterModule.reset();

在运行时,Node.js 会将文件内的代码包裹在一个函数内,然后通过参数传递exportsmodule变量和require函数。

// Define CommonJS module: wrapped commonJSCounterModule.js.
(function (exports, require, module, __filename, __dirname) {const dependencyModule1 = require("./dependencyModule1");const dependencyModule2 = require("./dependencyModule2");let count = 0;const increase = () => ++count;const reset = () => {count = 0;console.log("Count is reset.");};module.exports = {increase,reset};return module.exports;
}).call(thisValue, exports, require, module, filename, dirname);// Use CommonJS module.
(function (exports, require, module, __filename, __dirname) {const commonJSCounterModule = require("./commonJSCounterModule");commonJSCounterModule.increase();commonJSCounterModule.reset();
}).call(thisValue, exports, require, module, filename, dirname);

AMD 模块(RequireJS 模块)

AMD(异步模块定义)也是一种模块格式,由 RequireJS 这个库实现。它通过define函数定义模块,并接受模块名和依赖的模块名作为参数。

// 定义 AMD 模块
define("amdCounterModule", ["dependencyModule1", "dependencyModule2"], (dependencyModule1, dependencyModule2) => {let count = 0;const increase = () => ++count;const reset = () => {count = 0;console.log("Count is reset.");};return {increase,reset};
});

也用 require加载和使用模块:

require(["amdCounterModule"], amdCounterModule => {amdCounterModule.increase();amdCounterModule.reset();
});

跟 CommonJS 不同,这里的 requrie接受一个回调函数,参数就是加载好的模块对象。

AMD 的define函数还可以动态加载模块,只要给它传一个回调函数,并带上 require参数:

// Use dynamic AMD module.
define(require => {const dynamicDependencyModule1 = require("dependencyModule1");const dynamicDependencyModule2 = require("dependencyModule2");let count = 0;const increase = () => ++count;const reset = () => {count = 0;console.log("Count is reset.");};return {increase,reset};
});

AMD 模块还可以给define传递moduleexports,这样就可以在内部使用 CommonJS 代码:

// 定义带有 CommonJS 代码的 AMD 模块
define((require, exports, module) => {// CommonJS 代码const dependencyModule1 = require("dependencyModule1");const dependencyModule2 = require("dependencyModule2");let count = 0;const increase = () => ++count;const reset = () => {count = 0;console.log("Count is reset.");};exports.increase = increase;exports.reset = reset;
});// 使用带有 CommonJS 代码的 AMD 模块
define(require => {// CommonJS 代码const counterModule = require("amdCounterModule");counterModule.increase();counterModule.reset();
});

UMD 模块

UMD(通用模块定义),是一种支持多种环境的模块化格式,可同时用于 AMD 和 浏览器(或者 Node.js)环境。

兼容 AMD 和浏览器全局引入:

((root, factory) => {// 检测是否存在 AMD/RequireJS 的 define 函数if (typeof define === "function" && define.amd) {// 如果是,在 define 函数内调用 factorydefine("umdCounterModule", ["deependencyModule1", "dependencyModule2"], factory);} else {// 否则为浏览器环境,直接调用 factory// 导入的依赖是全局变量(window 对象的属性)// 导出的模块也是全局变量(window 对象的属性)root.umdCounterModule = factory(root.deependencyModule1, root.dependencyModule2);}
})(typeof self !== "undefined" ? self : this, (deependencyModule1, dependencyModule2) => {// 具体的模块代码let count = 0;const increase = () => ++count;const reset = () => {count = 0;console.log("Count is reset.");};return {increase,reset};
});

看起来很复杂,其实就是个 IIFE。代码注释写得很清楚了,可以看看。
下面来看兼容 AMD 和 CommonJS(Node.js)模块的 UMD:

(define => define((require, exports, module) => {// 模块代码const dependencyModule1 = require("dependencyModule1");const dependencyModule2 = require("dependencyModule2");let count = 0;const increase = () => ++count;const reset = () => {count = 0;console.log("Count is reset.");};module.export = {increase,reset};
}))(// 判断 CommonJS 里的 module 变量和 exports 变量是否存在// 同时判断 AMD/RequireJS 的define 函数是否存在typeof module === "object" && module.exports && typeof define !== "function"? // 如果是 CommonJS/Node.js,手动定义一个 define 函数factory => module.exports = factory(require, exports, module): // 否则是 AMD/RequireJS,直接使用 define 函数define);

同样是个 IIFE,通过判断环境,选择执行对应的代码。

ES 模块(ES6 Module)

前面说到的几种模块格式,都是用到了各种技巧实现的,看起来眼花缭乱。终于,在 2015 年,ECMAScript 第 6 版(ES 2015,或者 ES6 )横空出世!它引入了一种全新的模块格式,主要语法就是 importepxort关键字。来看 ES6 怎么定义模块:

// 定义 ES 模块:esCounterModule.js 或 esCounterModule.mjs.
import dependencyModule1 from "./dependencyModule1.mjs";
import dependencyModule2 from "./dependencyModule2.mjs";let count = 0;
// 具名导出:
export const increase = () => ++count;
export const reset = () => {count = 0;console.log("Count is reset.");
};
// 默认导出
export default {increase,reset
};

浏览器里使用该模块,在 script标签上加上type="module",表明引入的是 ES 模块。在 Node.js 环境中使用时,把扩展名改成 .mjs

// Use ES module.
//浏览器: <script type="module" src="esCounterModule.js"></script> or inline.// 服务器:esCounterModule.mjs
import { increase, reset } from "./esCounterModule.mjs";
increase();
reset();
// Or import from default export:
import esCounterModule from "./esCounterModule.mjs";
esCounterModule.increase();
esCounterModule.reset();

浏览器如果不支持,可以加个兜底属性:

<script nomodule>alert("Not supported.");
</script>

ES 动态模块(ECMAScript 2020)

2020 年最新的 ESCMA 标准11版中引入了内置的 import函数,用于动态加载 ES 模块。import函数返回一个 Promise,在它的then回调里使用加载后的模块:

// 用 Promise API 加载动态 ES 模块
import("./esCounterModule.js").then(({ increase, reset }) => {increase();reset();
});import("./esCounterModule.js").then(dynamicESCounterModule => {dynamicESCounterModule.increase();dynamicESCounterModule.reset();
});

由于返回的是 Promise,那肯定也支持await用法:

// 通过 async/await 使用 ES 动态模块
(async () => {// 具名导出的模块const { increase, reset } = await import("./esCounterModule.js");increase();reset();// 默认导出的模块const dynamicESCounterModule = await import("./esCounterModule.js");dynamicESCounterModule.increase();dynamicESCounterModule.reset();
})();

各平台对importexport和动态import的兼容情况如下:

image.png

image.png

System 模块

SystemJS 是一个 ES 模块语法转换库,以便支持低版本的 ES。例如,下面的模块是用 ES6 语法定义的:

// 定义 ES 模块
import dependencyModule1 from "./dependencyModule1.js";
import dependencyModule2 from "./dependencyModule2.js";
dependencyModule1.api1();
dependencyModule2.api2();let count = 0;
// Named export:
export const increase = function () { return ++count };
export const reset = function () {count = 0;console.log("Count is reset.");
};
// Or default export:
export default {increase,reset
}

如果当前的运行环境(比如旧浏览器)不支持 ES6 语法,上面的代码就无法运行。一种方案是把上面的模块定义转换成 SystemJS 库的一个 API, System.register

// Define SystemJS module.
System.register(["./dependencyModule1.js", "./dependencyModule2.js"], function (exports_1, context_1) {"use strict";var dependencyModule1_js_1, dependencyModule2_js_1, count, increase, reset;var __moduleName = context_1 && context_1.id;return {setters: [function (dependencyModule1_js_1_1) {dependencyModule1_js_1 = dependencyModule1_js_1_1;},function (dependencyModule2_js_1_1) {dependencyModule2_js_1 = dependencyModule2_js_1_1;}],execute: function () {dependencyModule1_js_1.default.api1();dependencyModule2_js_1.default.api2();count = 0;// Named export:exports_1("increase", increase = function () { return ++count };exports_1("reset", reset = function () {count = 0;console.log("Count is reset.");};);// Or default export:exports_1("default", {increase,reset});}};
});

这样,import/export关键字就不见了。Webpack、TypeScript 等可以自动完成这样的转换(后面会讲)。

SystemJS 也支持动态加载模块:

// Use SystemJS module with promise APIs.
System.import("./esCounterModule.js").then(dynamicESCounterModule => {dynamicESCounterModule.increase();dynamicESCounterModule.reset();
});

Webpack 模块(打包 AMD,CJS,ESM)

Webpack 是个强大的模块打包工具,可以将 AMD、CommonJS 和 ES Module 格式的模块转换并打包到单个 JS 文件。

Babel 模块

Babel 是也个转换器,可将 ES6+ 代码转换成低版本的 ES。前面例子中的计数器模块用 Babel 转换后的代码是这样的:

// Babel.
Object.defineProperty(exports, "__esModule", {value: true
});
exports["default"] = void 0;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }// Define ES module: esCounterModule.js.
var dependencyModule1 = _interopRequireDefault(require("./amdDependencyModule1"));
var dependencyModule2 = _interopRequireDefault(require("./commonJSDependencyModule2"));
dependencyModule1["default"].api1();
dependencyModule2["default"].api2();var count = 0;
var increase = function () { return ++count; };
var reset = function () {count = 0;console.log("Count is reset.");
};exports["default"] = {increase: increase,reset: reset
};

引入该模块的index.js将会转换成:

// Babel.
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }// Use ES module: index.js
var esCounterModule = _interopRequireDefault(require("./esCounterModule.js"));
esCounterModule["default"].increase();
esCounterModule["default"].reset();

以上是 Babel 的默认转换行为,它还可以结合其他插件使用,比如前面提到的 SystemJS。经过配置,Babel 可将 AMD、CJS、ES Module 转换成 System 模块格式。

TypeScript 模块

TypeScript 是 JavaScript 的超集,可以支持所有 JavaScript 语法,包括 ES6 模块语法。它在转换时,可以保留 ES6 语法,也可以转换成 AMD、CJS、UMD、SystemJS 等格式,取决于配置:

{"compilerOptions": {"module": "ES2020", // None, CommonJS, AMD, System, UMD, ES6, ES2015, ES2020, ESNext.}
}

TypeScript 还支持 modulenamespace关键字,表示内部模块。

module Counter {let count = 0;export const increase = () => ++count;export const reset = () => {count = 0;console.log("Count is reset.");};
}namespace Counter {let count = 0;export const increase = () => ++count;export const reset = () => {count = 0;console.log("Count is reset.");};
}

都可以转换成 JavaScript 对象:

var Counter;
(function (Counter) {var count = 0;Counter.increase = function () { return ++count; };Counter.reset = function () {count = 0;console.log("Count is reset.");};
})(Counter || (Counter = {}));

总结

以上提到的各种模块格式是在 JavaScript 语言演进过程中出现的模块化方案,各有其适用环境。随着标准化推进,Node.js 和最新的现代浏览器都开始支持 ES 模块格式。如果要在旧环境中使用模块化,可以通过 Webpack、Babel、TypeScript、SystemJS 等工具进行转换。


作者简介:
李中凯
八年多工作经验 前端负责人,
擅长JavaScript/Vue。
掘金文章专栏:https://juejin.im/user/57c7cb8a0a2b58006b1b8666/posts
公众号:1024译站

本文已经获得李中凯老师授权转发,其他人若有兴趣转载,请直接联系作者授权。

史上最全的 JavaScript 模块化方案和工具相关推荐

  1. 史上最全的javascript知识点总结,浅显易懂。

    来源于:http://blog.csdn.net/qiushi_1990/article/details/40260471 一,认识javascript 1-1 为什么学习JavaScript 一). ...

  2. 史上最全Android性能优化方案解析

    Android中的性能优分为以下几个方面: 布局优化 网络优化 安装包优化 内存优化 卡顿优化 启动优化 -- 一.布局优化 布局优化的本质就是减少View的层级.常见的布局优化方案如下: 在Line ...

  3. 史上最全的 SQL 优化方案!建议收藏

    在进行MySQL的优化之前,必须要了解的就是MySQL的查询过程,很多查询优化工作实际上就是遵循一些原则,让MySQL的优化器能够按照预想的合理方式运行已. 图-MySQL查询过程 1 优化的哲学 注 ...

  4. intel(r)wireless-ac9462异常//笔记本电脑网络无法连接???吐血后总结 :一次解决,史上最全,N种方案

    intel®wireless-ac9462出现黄色感叹号 intel®wireless-ac9560出现黄色感叹号 笔记本联网出现异常 如图所示: 简单介绍:这种情况是无线网卡驱动出现问题 解决方案一 ...

  5. 史上最全的Schnorr签名方案和BLS签名方案的全面对比

    前言 Schnorr 签名算法最初是由德国密码学家 Claus Schnorr 于 2008 年提出的,而来自区块链协议公司 Blockstream 的密码学家 Gregory Maxwell.Pie ...

  6. 开发者福利:史上最全Android 开发和安全系列工具

    取证工具 bandicoot - 一个Python工具箱,用于分析手机元数据.它提供了一个完整,易于使用的环境,数据科学家分析手机元数据.只需几行代码,加载数据集,可视化数据,执行分析和导出结果. A ...

  7. 史上最全前端学习路线图(视频+工具+书籍+资源)

    很多人在学习编程的路上都会收藏一些资料,但是真正用起来的却很少,究其原因可能就是不够系统,资料不全.所以下面我也为大家整理了最全的前端学习教程+工具+书籍,就不用你一个个收藏了,看这篇就够了!!! 2 ...

  8. 【转】史上最全Android 开发和安全系列工具

    取证工具 bandicoot - 一个Python工具箱,用于分析手机元数据.它提供了一个完整,易于使用的环境,数据科学家分析手机元数据.只需几行代码,加载数据集,可视化数据,执行分析和导出结果. A ...

  9. 史上最全的10类常用软件测试工具全都在这(共60+款工具)

    目前软件测试按类型划分比较细致,对应的功能软件也都比较广泛.本文介绍最常用的软件测试工具,希望对大家有所帮助. 我们将常用的测试工具分为10类. 一. 测试管理工具 二.接口测试工具 三.性能测试工具 ...

最新文章

  1. 在64位机上PLSQL连oracle11g问题:SQL*Net not properly installed和ORA-12154:TNS:无法处理服务名...
  2. 从 Gzip 压缩 SVG 说起 — 论如何减小资源文件的大小
  3. linux 命令行 过滤,利用linux命令行工具进行文本行过滤
  4. DevOps Troubleshooting(1)-运维思想
  5. html 制作条形图,Highcharts 基本条形图
  6. PowerDesigner的逆向工程.
  7. Oracle FGA审计记录的清理步骤
  8. 【算法竞赛学习】金融风控之贷款违约预测-模型融合
  9. HashOperations
  10. 拦截器原理多个拦截器执行顺序
  11. LeetCode 1059. All Paths from Source Lead to Destination
  12. vue3.0引入ant-design-vue报错 export ‘default‘ (imported as ‘Vue‘) was not found in ‘vue‘
  13. 感受野-Receptive Field的理解
  14. pytorch基础---李博文记录索引
  15. powerpoint预览_如何调整PowerPoint模板的大小
  16. 安卓linux远程桌面连接,用SSH连接你的android手机
  17. 又到招聘季,说说网络招聘的那些坑!!!
  18. 为仿真器添加eCos多线程调试支持,GDBServer Extender 0.0.1 使用说明
  19. 【自动控制原理】知识点
  20. 《冰雪奇缘》造雪花的技术,被MIT用来开发了一只软体机器人

热门文章

  1. Excel制作九九乘法表
  2. ib网卡命令_一种IB网卡连通配置方法、装置、终端及存储介质与流程
  3. vc2008 使用mysql_real_connect_VC远程连接MYSQL失败原因收集:mysql_real_connect()
  4. 【宝藏级】PyEcharts 超详细的使用指南
  5. 软件建模与分析——G003-185-03
  6. c++实验4——求1000以内所有偶数的和
  7. 通俗易懂:I2c总线
  8. java jettison_Java常用Json库性能对比
  9. 小程序自定义分享卡片
  10. 如何使用Babel实现polyfill