JS模块化之JavaScript模块化方案总结
创始人
2025-03-17 12:20:22
0次

 JS模块化之JavaScript 模块化方案总结

 

 

本文包含两部分,第一部分通过简明的描述介绍什么是 CommonJS、AMD、CMD、UMD、ES Module 以及它们的常见用法,第二部分则根据实际问题指出在正常的 webpack 构建过程中该如何指定打包配置中的模块化参数。

JavaScript 模块化方案

模块化这个话题在 ES6 之前是不存在的,因此这也被诟病为早期 JavaScript 开发全局污染和依赖管理混乱问题的源头。这类历史渊源和发展概述在本文将不会提及,因此感兴趣可以自行搜索 JavaScript 发展史进行了解。

直接进入正题,我们来看看常见的模块化方案都有哪些以及他们都有哪些内容。

CommonJS

CommonJS 的一个模块就是一个脚本文件,通过执行该文件来加载模块。CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。

我们见过这样的模块引用

  1. var myModule = require('module'); 
  2. myModule.sayHello(); 

这是因为我们把模块的方法定义在了模块的属性上:

  1. // module.js 
  2. module.exports.sayHello = function() { 
  3.  console.log('Hello '); 
  4. }; 
  5. // 如果这样写 
  6. module.exports = sayHello; 
  7. // 调用则需要改为 
  8. var sayHello = require('module'); 
  9. sayHello(); 

require 命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象(模块可以多次加载,但是在第一次加载时才会运行,结果被缓存),这个结果长成这样:

  1. { 
  2.  id: '...', 
  3.  exports: { ... }, 
  4.  loaded: true, 
  5.  ... 
  6. } 

Node.js 的模块机制实现就是参照了 CommonJS 的标准。但是 Node.js 额外做了一件事,即为每个模块提供了一个 exports 变量,以指向 module.exports,这相当于在每个模块最开始,写有这么一行代码:

  1. var exports = module.exports; 

CommonJS 模块的特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 独立性是模块的重要特点就,模块内部最好不与程序的其他部分直接交互。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

AMD

CommonJS 规范很好,但是不适用于浏览器环境,于是有了 AMD 和 CMD 两种方案。AMD 全称 Asynchronous Module Definition,即异步模块定义。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。除了和 CommonJS 同步加载方式不同之外,AMD 在模块的定义与引用上也有所不同。

  1. define(id?, dependencies?, factory); 

AMD 的模块引入由 define 方法来定义,在 define API 中:

  • id:模块名称,或者模块加载器请求的指定脚本的名字;
  • dependencies:是个定义中模块所依赖模块的数组,默认为 [“require”, “exports”, “module”],举个例子比较好理解,当我们创建一个名为 “alpha” 的模块,使用了require,exports,和名为 “beta” 的模块,需要如下书写(示例1);
  • factory:为模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值;
  1. // 示例1 
  2. define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { 
  3.  exports.verb = function() { 
  4.  return beta.verb(); 
  5.  // 或者 
  6.  return require("beta").verb(); 
  7.  } 
  8. }); 

如果模块定义不存在依赖,那么可以直接定义对象:

  1. define({ 
  2.  add: function(x, y){ 
  3.  return x + y; 
  4.  } 
  5. }); 

而使用时我们依旧通过 require 关键字,它包含两个参数,第一个数组为要加载的模块,第二个参数为回调函数:

  1. require([module], callback); 

举个例子:

  1. require(['math'], function (math) { 
  2.  math.add(2, 3); 
  3. }); 

CMD

CMD 全称为 Common Module Definition,是 Sea.js 所推广的一个模块化方案的输出。在 CMD define 的入参中,虽然也支持包含 id, deps 以及 factory 三个参数的形式,但推荐的是接受 factory 一个入参,然后在入参执行时,填入三个参数 require、exports 和 module:

  1. define(function(require, exports, module) { 
  2.  var a = require('./a'); 
  3.  a.doSomething(); 
  4.  var b = require('./b');  
  5.  b.doSomething(); 
  6.  ... 
  7. }) 

通过执行该构造方法,可以得到模块向外提供的接口。在与 AMD 比较上存在两个主要的不同点(来自玉伯回答):

  1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
  2. CMD 推崇依赖就近,AMD 推崇依赖前置。

如果说的不清楚,那么我们直接看上面的代码用 AMD 该怎么写:

  1. define(['./a', './b'], function(a, b) {  
  2.  a.doSomething(); 
  3.  b.doSomething(); 
  4.  ... 
  5. }) 

UMD

UMD,全称 Universal Module Definition,即通用模块规范。既然 CommonJs 和 AMD 风格一样流行,那么需要一个可以统一浏览器端以及非浏览器端的模块化方案的规范。

直接来看看官方给出的 jQuery 模块如何用 UMD 定义的代码:

  1. (function (factory) { 
  2.  if (typeof define === 'function' && define.amd) { 
  3.  // AMD. Register as an anonymous module. 
  4.  define(['jquery'], factory); 
  5.  } else if (typeof module === 'object' && module.exports) { 
  6.  // Node/CommonJS 
  7.  module.exports = function( root, jQuery ) { 
  8.  if ( jQuery === undefined ) { 
  9.  // require('jQuery') returns a factory that requires window to 
  10.  // build a jQuery instance, we normalize how we use modules 
  11.  // that require this pattern but the window provided is a noop 
  12.  // if it's defined (how jquery works) 
  13.  if ( typeof window !== 'undefined' ) { 
  14.  jQuery = require('jquery'); 
  15.  } 
  16.  else { 
  17.  jQuery = require('jquery')(root); 
  18.  } 
  19.  } 
  20.  factory(jQuery); 
  21.  return jQuery; 
  22.  }; 
  23.  } else { 
  24.  // Browser globals 
  25.  factory(jQuery); 
  26.  } 
  27. }(function ($) { 
  28.  $.fn.jqueryPlugin = function () { return true; }; 
  29. })); 

UMD的实现很简单:

  • 先判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块;
  • 再判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式;
  • 前两个都不存在,则将模块公开到全局(window 或 global);

ES Modules

当然,以上说的种种都是社区提供的方案,历史上,JavaScript 一直没有模块系统,直到 ES6 在语言标准的层面上,实现了它。其设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。而 ES Modules 不是对象,而是通过 export 命令显式指定输出的代码。

ES Modules 的模块化能力由 export 和 import 组成,export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。我们可以这样定义一个模块:

  1. // 第一种方式 
  2. export var firstName = 'Michael'; 
  3. export var lastName = 'Jackson'; 
  4. export var year = 1958; 
  5. // 第二种方式 
  6. var firstName = 'Michael'; 
  7. var lastName = 'Jackson'; 
  8. var year = 1958; 
  9. export { firstName, lastName, year }; 

然后再这样引入他们:

  1. import { firstName, lastName, year } from 'module'; 
  2. import { firstName as newName } from 'module'; 
  3. import * as moduleA from 'module'; 

除以上两种命令外,还有一个 export default 命令用于指定模块的默认输出(一个模块只能有一个默认输出)。如果使用了 export default 语法,在 import 时则可以任意命名。由于 export default 命令的本质是将后面的值,赋给 default 变量,所以也可以直接将一个值写在 export default 之后。当然,引用方式也存在多种:

  1. import { default as foo } from 'module'; 
  2. import foo from 'module'; 

需要注意的是 Modules 会自动采用严格模式,且 import 命令具有提升效果,会提升到整个模块的头部,首先执行。

延伸阅读 JavaScript 模块的循环加载

webpack 打包输出配置

说完理论,来看看实际项目中遇到的问题。当我们开发完一个 JavaScript 模块必然要经历打包的流程,而在 webpack 配置中,通过指定 output 选项就可以告诉 webpack 如何输出 bundle, asset 以及其他载入的内容。那么如何实现不同环境可兼容的构建呢?

  • import:通过 ES Modules 规范语法进入引入;
  • 变量:作为一个全局变量,比如通过 script 标签来访问;
  • this:通过 this 对象访问;
  • window:在浏览器中通过 window 对象访问;
  • UMD:在 AMD 或 CommonJS 通过 require 引入后访问;

output 中有一个属性叫做 libraryTarget,被用来指定如何暴露你的模块的属性。你可以这样尝试赋值给一个变量或者指定对象的属性:

  1. // 加载完成后将模块赋值给一个指定变量(默认值) 
  2. { 
  3.  libraryTarget: 'var', 
  4.  ... 
  5. } 
  6. // 赋值为指定对象的一个属性,比如 `this` 或者 `window` 
  7. { 
  8.  libraryTarget: "this", 
  9.  // libraryTarget: "window", 
  10.  ... 
  11. } 
  12. // 同样的,若是指定 commonjs,那么便可以将模块分配给 exports,这也意味着可以用于 CommonJS 环境: 
  13. { 
  14.  libraryTarget: "commonjs", 
  15.  ... 
  16. } 

如果需要更完整的模块化 bundle,以确保和各模块系统兼容,那么可以这样尝试:

  1. // 内容分配给 module.exports 对象,用于 CommonJS 环境 
  2. { 
  3.  libraryTarget: 'commonjs2', 
  4.  ... 
  5. } 
  6. // 暴露为 AMD 模块,通过特定属性引入 
  7. { 
  8.  libraryTarget: 'amd', 
  9.  ... 
  10. } 
  11. // 所有模块系统兼容的万金油,可以在 CommonJS, AMD 环境下运行,或将模块导出到 global 下的变量 
  12. { 
  13.  libraryTarget: 'umd', 
  14.  ... 
  15. } 

因此,如果只看 output 内容,那么我的一个 webpack 生产环境配置可以写成这样:

  1. module.exports = { 
  2.  output: { 
  3.  // webpack 如何输出结果的相关选项 
  4.  path: path.resolve(__dirname, "dist"), 
  5.  filename: 'index.js', 
  6.  library: 'hijiangtao', 
  7.  umdNamedDefine: true, 
  8.  libraryTarget: 'umd', 
  9.  }, 
  10. } 

 

相关内容

热门资讯

PHP新手之PHP入门 PHP是一种易于学习和使用的服务器端脚本语言。只需要很少的编程知识你就能使用PHP建立一个真正交互的...
网络中立的未来 网络中立性是什... 《牛津词典》中对“网络中立”的解释是“电信运营商应秉持的一种原则,即不考虑来源地提供所有内容和应用的...
各种千兆交换机的数据接口类型详... 千兆交换机有很多值得学习的地方,这里我们主要介绍各种千兆交换机的数据接口类型,作为局域网的主要连接设...
粉嫩如何诠释霸道 东芝M805... “霸道粉”是个什么玩意东芝M805拿过来的时候,笔者扑哧笑了,不是笑这款笔记本,而是笑这款产品的颜色...
如何利用交换机和端口设置来管理... 在网络管理中,总是有些人让管理员头疼。下面我们就将介绍一下一个网管员利用交换机以及端口设置等来进行D...
什么是大数据安全 什么是大数据... 在《为什么需要大数据安全分析》一文中,我们已经阐述了一个重要观点,即:安全要素信息呈现出大数据的特征...
只需三步,让你掌握阿尔法路由器... 请参照以下阿尔法路由器设置方法,通过浏览器进入及设置宽带路由器。经过这三个步骤的设置,基本上能够熟悉...
如何更省钱? 近期特价热门硒鼓... 如今激光打印设备已经成为商务或企业的绝对主力办公设备,但采购者往往忽视了激光打印设备所用的耗材,相对...
全面诠释网络负载均衡 负载均衡的出现大大缓解了服务器的压力,更是有效的利用了资源,提高了效率。那么我们现在来说一下网络负载...
如何允许远程连接到MySQL数... [[277004]]【51CTO.com快译】默认情况下,MySQL服务器仅侦听来自localhos...