本文包含两部分,第一部分经过简明的描述介绍什么是 CommonJS、AMD、CMD、UMD、ES Module 以及它们的常见用法,第二部分则根据实际问题指出在正常的 webpack 构建过程当中该如何指定打包配置中的模块化参数。javascript
模块化这个话题在 ES6 以前是不存在的,所以这也被诟病为早期 JavaScript 开发全局污染和依赖管理混乱问题的源头。这类历史渊源和发展概述在本文将不会说起,所以感兴趣能够自行搜索 JavaScript 发展史进行了解。java
直接进入正题,咱们来看看常见的模块化方案都有哪些以及他们都有哪些内容。jquery
CommonJS 的一个模块就是一个脚本文件,经过执行该文件来加载模块。CommonJS 规范规定,每一个模块内部,module
变量表明当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports
)是对外的接口。加载某个模块,实际上是加载该模块的 module.exports
属性。webpack
咱们见过这样的模块引用:web
var myModule = require('module'); myModule.sayHello();
这是由于咱们把模块的方法定义在了模块的属性上:数组
// module.js module.exports.sayHello = function() { console.log('Hello '); }; // 若是这样写 module.exports = sayHello; // 调用则须要改成 var sayHello = require('module'); sayHello();
require
命令第一次加载该脚本时就会执行整个脚本,而后在内存中生成一个对象(模块能够屡次加载,可是在第一次加载时才会运行,结果被缓存),这个结果长成这样:浏览器
{ id: '...', exports: { ... }, loaded: true, ... }
Node.js 的模块机制实现就是参照了 CommonJS 的标准。可是 Node.js 额外作了一件事,即为每一个模块提供了一个 exports 变量,以指向 module.exports,这至关于在每一个模块最开始,写有这么一行代码:缓存
var exports = module.exports;
CommonJS 模块的特色:异步
全部代码都运行在模块做用域,不会污染全局做用域。ide
独立性是模块的重要特色就,模块内部最好不与程序的其余部分直接交互。
模块能够屡次加载,可是只会在第一次加载时运行一次,而后运行结果就被缓存了,之后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。
CommonJS 规范很好,可是不适用于浏览器环境,因而有了 AMD 和 CMD 两种方案。AMD 全称 Asynchronous Module Definition,即异步模块定义。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。除了和 CommonJS 同步加载方式不一样以外,AMD 在模块的定义与引用上也有所不一样。
define(id?, dependencies?, factory);
AMD 的模块引入由 define 方法来定义,在 define API 中:
id:模块名称,或者模块加载器请求的指定脚本的名字;
dependencies:是个定义中模块所依赖模块的数组,默认为 ["require", "exports", "module"],举个例子比较好理解,当咱们建立一个名为 "alpha" 的模块,使用了require,exports,和名为 "beta" 的模块,须要以下书写(示例1);
factory:为模块初始化要执行的函数或对象。若是为函数,它应该只被执行一次。若是是对象,此对象应该为模块的输出值;
// 示例1 define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { exports.verb = function() { return beta.verb(); // 或者 return require("beta").verb(); } });
若是模块定义不存在依赖,那么能够直接定义对象:
define({ add: function(x, y){ return x + y; } });
而使用时咱们依旧经过 require 关键字,它包含两个参数,第一个数组为要加载的模块,第二个参数为回调函数:
require([module], callback);
举个例子:
require(['math'], function (math) { math.add(2, 3); });
CMD 全称为 Common Module Definition,是 Sea.js 所推广的一个模块化方案的输出。在 CMD define 的入参中,虽然也支持包含 id, deps 以及 factory 三个参数的形式,但推荐的是接受 factory 一个入参,而后在入参执行时,填入三个参数 require、exports 和 module:
define(function(require, exports, module) { var a = require('./a'); a.doSomething(); var b = require('./b'); b.doSomething(); ... })
经过执行该构造方法,能够获得模块向外提供的接口。在与 AMD 比较上存在两个主要的不一样点(来自玉伯回答):
对于依赖的模块,AMD 是提早执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改为能够延迟执行(根据写法不一样,处理方式不一样)。CMD 推崇 as lazy as possible.
CMD 推崇依赖就近,AMD 推崇依赖前置。
若是说的不清楚,那么咱们直接看上面的代码用 AMD 该怎么写:
define(['./a', './b'], function(a, b) { a.doSomething(); b.doSomething(); ... })
UMD,全称 Universal Module Definition,即通用模块规范。既然 CommonJs 和 AMD 风格同样流行,那么须要一个能够统一浏览器端以及非浏览器端的模块化方案的规范。
直接来看看官方给出的 jQuery 模块如何用 UMD 定义的代码:
(function (factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['jquery'], factory); } else if (typeof module === 'object' && module.exports) { // Node/CommonJS module.exports = function( root, jQuery ) { if ( jQuery === undefined ) { // require('jQuery') returns a factory that requires window to // build a jQuery instance, we normalize how we use modules // that require this pattern but the window provided is a noop // if it's defined (how jquery works) if ( typeof window !== 'undefined' ) { jQuery = require('jquery'); } else { jQuery = require('jquery')(root); } } factory(jQuery); return jQuery; }; } else { // Browser globals factory(jQuery); } }(function ($) { $.fn.jqueryPlugin = function () { return true; }; }));
UMD的实现很简单:
先判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块;
再判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式;
前两个都不存在,则将模块公开到全局(window 或 global);
固然,以上说的种种都是社区提供的方案,历史上,JavaScript 一直没有模块系统,直到 ES6 在语言标准的层面上,实现了它。其设计思想是尽可能的静态化,使得编译时就能肯定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时肯定这些东西。好比,CommonJS 模块就是对象,输入时必须查找对象属性。而 ES Modules 不是对象,而是经过 export
命令显式指定输出的代码。
ES Modules 的模块化能力由 export
和 import
组成,export
命令用于规定模块的对外接口,import
命令用于输入其余模块提供的功能。咱们能够这样定义一个模块:
// 第一种方式 export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958; // 第二种方式 var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; export { firstName, lastName, year };
而后再这样引入他们:
import { firstName, lastName, year } from 'module'; import { firstName as newName } from 'module'; import * as moduleA from 'module';
除以上两种命令外,还有一个 export default
命令用于指定模块的默认输出(一个模块只能有一个默认输出)。若是使用了 export default
语法,在 import 时则能够任意命名。因为 export default
命令的本质是将后面的值,赋给 default
变量,因此也能够直接将一个值写在 export default
以后。固然,引用方式也存在多种:
import { default as foo } from 'module'; import foo from 'module';
须要注意的是 Modules 会自动采用严格模式,且 import 命令具备提高效果,会提高到整个模块的头部,首先执行。
延伸阅读 JavaScript 模块的循环加载
说完理论,来看看实际项目中遇到的问题。当咱们开发完一个 JavaScript 模块必然要经历打包的流程,而在 webpack 配置中,经过指定 output 选项就能够告诉 webpack 如何输出 bundle, asset 以及其余载入的内容。那么如何实现不一样环境可兼容的构建呢?
import
:经过 ES Modules 规范语法进入引入;
变量:做为一个全局变量,好比经过 script 标签来访问;
this
:经过 this 对象访问;
window
:在浏览器中经过 window 对象访问;
UMD:在 AMD 或 CommonJS 经过 require
引入后访问;
output
中有一个属性叫作 libraryTarget
,被用来指定如何暴露你的模块的属性。你能够这样尝试赋值给一个变量或者指定对象的属性:
// 加载完成后将模块赋值给一个指定变量(默认值) { libraryTarget: 'var', ... } // 赋值为指定对象的一个属性,好比 `this` 或者 `window` { libraryTarget: "this", // libraryTarget: "window", ... } // 一样的,如果指定 commonjs,那么即可以将模块分配给 exports,这也意味着能够用于 CommonJS 环境: { libraryTarget: "commonjs", ... }
若是须要更完整的模块化 bundle,以确保和各模块系统兼容,那么能够这样尝试:
// 内容分配给 module.exports 对象,用于 CommonJS 环境 { libraryTarget: 'commonjs2', ... } // 暴露为 AMD 模块,经过特定属性引入 { libraryTarget: 'amd', ... } // 全部模块系统兼容的万金油,能够在 CommonJS, AMD 环境下运行,或将模块导出到 global 下的变量 { libraryTarget: 'umd', ... }
所以,若是只看 output 内容,那么个人一个 webpack 生产环境配置能够写成这样:
module.exports = { output: { // webpack 如何输出结果的相关选项 path: path.resolve(__dirname, "dist"), filename: 'index.js', library: 'hijiangtao', umdNamedDefine: true, libraryTarget: 'umd', }, }
(完)
回复“加群”与大佬们一块儿交流学习~