当前位置:   article > 正文

史上最全的 JavaScript 模块化方案和工具_javascript帮助分析模块工具

javascript帮助分析模块工具

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


 

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

IIFE 模块

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

  1. // 定义全局变量
  2. let count = 0;
  3. const increase = () => ++count;
  4. const reset = () => {
  5.     count = 0;
  6.     console.log("Count is reset.");
  7. };
  8. // 使用全局变量
  9. increase();
  10. reset();

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

  1. // 定义 IIFE 模块
  2. const iifeCounterModule = (() => {
  3.     let count = 0;
  4.     return {
  5.         increase: () => ++count,
  6.         reset: () => {
  7.             count = 0;
  8.             console.log("Count is reset.");
  9.         }
  10.     };
  11. })();
  12. // 使用 IIFE 模块
  13. iifeCounterModule.increase();
  14. iifeCounterModule.reset();

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

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

  1. // 定义带有依赖的 IIFE 模块
  2. const iifeCounterModule = ((dependencyModule1, dependencyModule2=> {
  3.     let count = 0;
  4.     return {
  5.         increase: () => ++count,
  6.         reset: () => {
  7.             count = 0;
  8.             console.log("Count is reset.");
  9.         }
  10.     };
  11. })(dependencyModule1, dependencyModule2);

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

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

  1. // Define revealing module.
  2. const revealingCounterModule = (() => {
  3.     let count = 0;
  4.     const increase = () => ++count;
  5.     const reset = () => {
  6.         count = 0;
  7.         console.log("Count is reset.");
  8.     };
  9.     return {
  10.         increase,
  11.         reset
  12.     };
  13. })();
  14. // Use revealing module.
  15. revealingCounterModule.increase();
  16. revealingCounterModule.reset();

CommonJS 模块(Node.js 模块)

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

  1. // 定义 CommonJS 模块: commonJSCounterModule.js.
  2. const dependencyModule1 = require("./dependencyModule1");
  3. const dependencyModule2 = require("./dependencyModule2");
  4. let count = 0;
  5. const increase = () => ++count;
  6. const reset = () => {
  7.     count = 0;
  8.     console.log("Count is reset.");
  9. };
  10. exports.increase = increase;
  11. exports.reset = reset;
  12. // 或者这样:
  13. module.exports = {
  14.     increase,
  15.     reset
  16. };

使用这个模块:

  1. // 使用 CommonJS 模块
  2. const { increase, reset } = require("./commonJSCounterModule");
  3. increase();
  4. reset();
  5. // 或者这样:
  6. const commonJSCounterModule = require("./commonJSCounterModule");
  7. commonJSCounterModule.increase();
  8. commonJSCounterModule.reset();

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

  1. // Define CommonJS module: wrapped commonJSCounterModule.js.
  2. (function (exports, require, module, __filename, __dirname) {
  3.     const dependencyModule1 = require("./dependencyModule1");
  4.     const dependencyModule2 = require("./dependencyModule2");
  5.     let count = 0;
  6.     const increase = () => ++count;
  7.     const reset = () => {
  8.         count = 0;
  9.         console.log("Count is reset.");
  10.     };
  11.     module.exports = {
  12.         increase,
  13.         reset
  14.     };
  15.     return module.exports;
  16. }).call(thisValue, exports, require, module, filename, dirname);
  17. // Use CommonJS module.
  18. (function (exports, require, module, __filename, __dirname) {
  19.     const commonJSCounterModule = require("./commonJSCounterModule");
  20.     commonJSCounterModule.increase();
  21.     commonJSCounterModule.reset();
  22. }).call(thisValue, exports, require, module, filename, dirname);

AMD 模块(RequireJS 模块)

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

  1. // 定义 AMD 模块
  2. define("amdCounterModule", ["dependencyModule1""dependencyModule2"], 
  3.       (dependencyModule1, dependencyModule2=> {
  4.     let count = 0;
  5.     const increase = () => ++count;
  6.     const reset = () => {
  7.         count = 0;
  8.         console.log("Count is reset.");
  9.     };
  10.     return {
  11.         increase,
  12.         reset
  13.     };
  14. });

也用 require加载和使用模块:

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

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

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

  1. // Use dynamic AMD module.
  2. define(require => {
  3.     const dynamicDependencyModule1 = require("dependencyModule1");
  4.     const dynamicDependencyModule2 = require("dependencyModule2");
  5.     let count = 0;
  6.     const increase = () => ++count;
  7.     const reset = () => {
  8.         count = 0;
  9.         console.log("Count is reset.");
  10.     };
  11.     return {
  12.         increase,
  13.         reset
  14.     };
  15. });

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

  1. // 定义带有 CommonJS 代码的 AMD 模块
  2. define((require, exports, module) => {
  3.     // CommonJS 代码
  4.     const dependencyModule1 = require("dependencyModule1");
  5.     const dependencyModule2 = require("dependencyModule2");
  6.     let count = 0;
  7.     const increase = () => ++count;
  8.     const reset = () => {
  9.         count = 0;
  10.         console.log("Count is reset.");
  11.     };
  12.     exports.increase = increase;
  13.     exports.reset = reset;
  14. });
  15. // 使用带有 CommonJS 代码的 AMD 模块
  16. define(require => {
  17.     // CommonJS 代码
  18.     const counterModule = require("amdCounterModule");
  19.     counterModule.increase();
  20.     counterModule.reset();
  21. });

UMD 模块

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

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

  1. ((root, factory=> {
  2.     // 检测是否存在 AMD/RequireJS 的 define 函数
  3.     if (typeof define === "function" && define.amd) {
  4.         // 如果是,在 define 函数内调用 factory
  5.         define("umdCounterModule", ["deependencyModule1""dependencyModule2"], factory);
  6.     } else {
  7.         // 否则为浏览器环境,直接调用 factory
  8.         // 导入的依赖是全局变量(window 对象的属性)
  9.         // 导出的模块也是全局变量(window 对象的属性)
  10.         root.umdCounterModule = factory(root.deependencyModule1, root.dependencyModule2);
  11.     }
  12. })(typeof self !== "undefined" ? self : this, (deependencyModule1, dependencyModule2=> {
  13.     // 具体的模块代码
  14.     let count = 0;
  15.     const increase = () => ++count;
  16.     const reset = () => {
  17.         count = 0;
  18.         console.log("Count is reset.");
  19.     };
  20.     return {
  21.         increase,
  22.         reset
  23.     };
  24. });

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

  1. (define => define((require, exports, module) => {
  2.     // 模块代码
  3.     const dependencyModule1 = require("dependencyModule1");
  4.     const dependencyModule2 = require("dependencyModule2");
  5.     let count = 0;
  6.     const increase = () => ++count;
  7.     const reset = () => {
  8.         count = 0;
  9.         console.log("Count is reset.");
  10.     };
  11.     module.export = {
  12.         increase,
  13.         reset
  14.     };
  15. }))(// 判断 CommonJS 里的 module 变量和 exports 变量是否存在
  16.     // 同时判断 AMD/RequireJS 的define 函数是否存在
  17.     typeof module === "object" && module.exports && typeof define !== "function"
  18.         ? // 如果是 CommonJS/Node.js,手动定义一个 define 函数
  19.             factory => module.exports = factory(require, exports, module)
  20.         : // 否则是 AMD/RequireJS,直接使用 define 函数
  21.             define);

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

ES 模块(ES6 Module)

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

  1. // 定义 ES 模块:esCounterModule.js 或 esCounterModule.mjs.
  2. import dependencyModule1 from "./dependencyModule1.mjs";
  3. import dependencyModule2 from "./dependencyModule2.mjs";
  4. let count = 0;
  5. // 具名导出:
  6. export const increase = () => ++count;
  7. export const reset = () => {
  8.     count = 0;
  9.     console.log("Count is reset.");
  10. };
  11. // 默认导出
  12. export default {
  13.     increase,
  14.     reset
  15. };

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

  1. // Use ES module.
  2. //浏览器: <script type="module" src="esCounterModule.js"></script> or inline.
  3. // 服务器:esCounterModule.mjs
  4. import { increase, reset } from "./esCounterModule.mjs";
  5. increase();
  6. reset();
  7. // Or import from default export:
  8. import esCounterModule from "./esCounterModule.mjs";
  9. esCounterModule.increase();
  10. esCounterModule.reset();

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

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

ES 动态模块(ECMAScript 2020)

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

  1. // 用 Promise API 加载动态 ES 模块
  2. import("./esCounterModule.js").then(({ increase, reset }) => {
  3.     increase();
  4.     reset();
  5. });
  6. import("./esCounterModule.js").then(dynamicESCounterModule => {
  7.     dynamicESCounterModule.increase();
  8.     dynamicESCounterModule.reset();
  9. });

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

  1. // 通过 async/await 使用 ES 动态模块
  2. (async () => {
  3.     // 具名导出的模块
  4.     const { increase, reset } = await import("./esCounterModule.js");
  5.     increase();
  6.     reset();
  7.     // 默认导出的模块
  8.     const dynamicESCounterModule = await import("./esCounterModule.js");
  9.     dynamicESCounterModule.increase();
  10.     dynamicESCounterModule.reset();
  11. })();

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

image.png

 

image.png

System 模块

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

  1. // 定义 ES 模块
  2. import dependencyModule1 from "./dependencyModule1.js";
  3. import dependencyModule2 from "./dependencyModule2.js";
  4. dependencyModule1.api1();
  5. dependencyModule2.api2();
  6. let count = 0;
  7. // Named export:
  8. export const increase = function () { return ++count };
  9. export const reset = function () {
  10.     count = 0;
  11.     console.log("Count is reset.");
  12. };
  13. // Or default export:
  14. export default {
  15.     increase,
  16.     reset
  17. }

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

  1. // Define SystemJS module.
  2. System.register(["./dependencyModule1.js""./dependencyModule2.js"], 
  3.                 function (exports_1, context_1) {
  4.     "use strict";
  5.     var dependencyModule1_js_1, dependencyModule2_js_1count, increase, reset;
  6.     var __moduleName = context_1 && context_1.id;
  7.     return {
  8.         setters: [
  9.             function (dependencyModule1_js_1_1) {
  10.                 dependencyModule1_js_1 = dependencyModule1_js_1_1;
  11.             },
  12.             function (dependencyModule2_js_1_1) {
  13.                 dependencyModule2_js_1 = dependencyModule2_js_1_1;
  14.             }
  15.         ],
  16.         execute: function () {
  17.             dependencyModule1_js_1.default.api1();
  18.             dependencyModule2_js_1.default.api2();
  19.             count = 0;
  20.             // Named export:
  21.             exports_1("increase", increase = function () { return ++count };
  22.             exports_1("reset"reset = function () {
  23.                 count = 0;
  24.                 console.log("Count is reset.");
  25.             };);
  26.             // Or default export:
  27.             exports_1("default", {
  28.                 increase,
  29.                 reset
  30.             });
  31.         }
  32.     };
  33. });

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

SystemJS 也支持动态加载模块:

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

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

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

Babel 模块

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

  1. // Babel.
  2. Object.defineProperty(exports, "__esModule", {
  3.     valuetrue
  4. });
  5. exports["default"= void 0;
  6. function _interopRequireDefault(obj) 
  7.          { return obj && obj.__esModule ? obj : { "default": obj }; }
  8. // Define ES module: esCounterModule.js.
  9. var dependencyModule1 = _interopRequireDefault(require("./amdDependencyModule1"));
  10. var dependencyModule2 = _interopRequireDefault(require("./commonJSDependencyModule2"));
  11. dependencyModule1["default"].api1();
  12. dependencyModule2["default"].api2();
  13. var count = 0;
  14. var increase = function () { return ++count; };
  15. var reset = function () {
  16.     count = 0;
  17.     console.log("Count is reset.");
  18. };
  19. exports["default"= {
  20.     increase: increase,
  21.     resetreset
  22. };

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

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

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

TypeScript 模块

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

  1. {
  2.     "compilerOptions": {
  3.         "module""ES2020"// None, CommonJS, AMD, System, UMD, ES6, ES2015, ES2020, ESNext.
  4.     }
  5. }

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

  1. module Counter {
  2.     let count = 0;
  3.     export const increase = () => ++count;
  4.     export const reset = () => {
  5.         count = 0;
  6.         console.log("Count is reset.");
  7.     };
  8. }
  9. namespace Counter {
  10.     let count = 0;
  11.     export const increase = () => ++count;
  12.     export const reset = () => {
  13.         count = 0;
  14.         console.log("Count is reset.");
  15.     };
  16. }

都可以转换成 JavaScript 对象:

  1. var Counter;
  2. (function (Counter) {
  3.     var count = 0;
  4.     Counter.increase = function () { return ++count; };
  5.     Counter.reset = function () {
  6.         count = 0;
  7.         console.log("Count is reset.");
  8.     };
  9. })(Counter || (Counter = {}));

总结

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


 

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/709822
推荐阅读
相关标签
  

闽ICP备14008679号