模块化是一种思想, 是将大工程拆成小的模块分治的思想.用来抽离公共代码,隔离作用域,避免变量冲突等。将一个复杂的系统分解为多个模块以方便编码。
目前最主流的模块化方案应该是 ECMAScript 2015 提出的模块化规范(也称“ES6 模块”),这个规范同时适用于 JavaScript 的前后端环境。
1、export命令用于规定模块的对外接口,基本语法为export xxx,暴露的接口可以是对象、函数、基本类型变量。另外可以使用export default xxx为模块指定默认输出,因为很多时候用户不知道要加载的模块的属性名。 2、 import命令用于输入其他模块提供的功能,基本语法为import xxx from xxx,其中第一个xxx为引入的模块的属性名,第二个xxx为模块的位置。如果不理解没关系,后面我们将看到具体的案例。
// a.js export var a = ''; setTimeout(() => a = 'a', 500); // b.js import { a } from './a.js' console.log(a) // '' setTimeout(() => console.log(a), 1000) // 'a' ES6 模块强制自动采用严格模式,所以说不管有没有“user strict”声明都是一样的,换言之,编写代码的时候不必再刻意声明了;虽然大部分主流浏览器支持 ES6 模块,但是和引入普通 JS 的方式略有不同,需要在对应 script 标签中将属性 type 值设置为“module”才能被正确地解析为 ES6 模块;在 Node.js 下使用 ES6 模块则需要将文件名后缀改为“.mjs”,用来和 Node.js 默认使用的 CommonJS 规范模块作区分。调用 import() 函数传入模块路径,得到一个 Promise 对象。
import(`./section-modules/${link.dataset.entryModule}.js`) .then(module => { module.loadPageInto(main); }) .catch(err => { main.textContent = err.message; }); 违反首部声明要求,那么就意味着可以在代码运行时按需加载模块,这个特性就可以用于首屏优化,根据路由和组件只加载依赖的模块。违反变量或表达式要求,则意味着可以根据参数动态加载模块。违反嵌入语句逻辑规则,可想象空间更大,比如可以通过 Promise.race 方式同时加载多个模块,选择加载速度最优模块来使用,从而提升性能。ES6 模块有两个重要特性一定要掌握,一个是值引用,另一个是静态声明。
1、值引用是指 export 语句输出的接口,与其对应的值是动态绑定关系。即通过该接口,可以取到模块内部实时的值,可以简单地理解为变量浅拷贝。 2、ES6 模块对于引用声明有严格的要求,首先必须在文件的首部,不允许使用变量或表达式,不允许被嵌入到其他语句中。所以下面 3 种引用模块方式都会报错。
// 必须首部声明 let a = 1 import { app } from './app'; // 不允许使用变量或表达式 import { 'a' + 'p' + 'p' } from './app'; // 不允许被嵌入语句逻辑 if (moduleName === 'app') { import { init } from './app'; } else { import { init } from './bpp'; }CommonJS 规定每个文件就是一个模块,有独立的作用域。每个模块内部,都有一个 module 对象,代表当前模块。通过它来导出 API,它有以下属性:
id 模块的识别符,通常是带有绝对路径的模块文件名;filename 模块的文件名,带有绝对路径;loaded 返回一个布尔值,表示模块是否已经完成加载;parent 返回一个对象,表示调用该模块的模块;children 返回一个数组,表示该模块要用到的其他模块;exports 表示模块对外输出的值。引用模块则需要通过 require 函数,它的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。
CommonJS 特性和 ES6 恰恰相反,它采用的是值拷贝和动态声明。值拷贝和值引用相反,一旦输出一个值,模块内部的变化就影响不到这个值了,可以简单地理解为变量深拷贝。
1、仍然使用上面的例子,改写成 CommonJS 模块,在 Node.js 端运行,控制台会打印两个空字符串。
// a.js var a = ''; setTimeout(() => a = 'a', 500); module.exports = a // b.js var a = require('./a.js') console.log(a) // '' setTimeout(() => console.log(a), 1000) // ''2、动态声明就很好理解了,就是消除了静态声明的限制,可以“自由”地在表达式语句中引用模块。
在 ES6 模块出现之前,AMD(Asynchronous Module Definition,异步模块定义)是一种很热门的浏览器模块化方案。
AMD 规范只定义了一个全局函数 define,通过它就可以定义和引用模块,它有 3 个参数:
define(id?, dependencies?, factory);第 1 个参数 id 为模块的名称,该参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字;如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。
第 2 个参数 dependencies 是个数组,它定义了所依赖的模块。依赖模块必须根据模块的工厂函数优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂函数中。
第 3 个参数 factory 为模块初始化要执行的函数或对象。如果是函数,那么该函数是单例模式,只会被执行一次;如果是对象,此对象应该为模块的输出值。
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { exports.verb = function() { return beta.verb(); } });它的重要特性就是异步加载。所谓异步加载,就是指同时并发加载所依赖的模块,当所有依赖模块都加载完成之后,再执行当前模块的回调函数。这种加载方式和浏览器环境的性能需求刚好吻合。
由于 AMD 并不是浏览器原生支持的模块规范,所以需要借助第三方库来实现,其中最有名的就是 RequireJS。它的核心是两个全局函数 define 和 require,define 函数可以将依赖注入队列中,并将回调函数定义成模块;require 函数主要作用是创建 script 标签请求对应的模块,然后加载和执行模块
var requirejs, require, define; (function (global, setTimeout) { ... define = function (name, deps, callback) { ... if (context) { context.defQueue.push([name, deps, callback]); context.defQueueMap[name] = true; } else { globalDefQueue.push([name, deps, callback]); } }; ... req.load = function (context, moduleName, url) { ... if (isBrowser) { node = req.createNode(config, moduleName, url); ... if (baseElement) { head.insertBefore(node, baseElement) } else { head.appendChild(node) } currentlyAddingScript = null; return node } }; ... }(this, (typeof setTimeout === 'undefined' ? undefined : setTimeout)));CMD(Common Module Definition,通用模块定义)是基于浏览器环境制定的模块规范。
CMD 定义模块也是通过一个全局函数 define 来实现的,但只有一个参数,该参数既可以是函数也可以是对象:
define(function(require, exports, module) { //... });第 1 个参数 require 是一个函数,通过调用它可以引用其他模块,也可以调用 require.async 函数来异步调用模块。 第 2 个参数 exports 是一个对象,当定义模块的时候,需要通过向参数 exports 添加属性来导出模块 API。 第 3 个参数 module 是一个对象,它包含 3 个属性:uri,模块完整的 URI 路径;dependencies,模块的依赖;exports,模块需要被导出的 API,作用同第二个参数 exports。
define(function(require, exports, module) { var add = require('math').add; exports.increment = function(val) { return add(val, 1); }; module.id = "increment"; });CMD 最大的特点就是懒加载,和上面示例代码一样,不需要在定义模块的时候声明依赖,可以在模块执行时动态加载依赖。当然还有一点不同,那就是 CMD 同时支持同步加载模块和异步加载模块。
用一句话来形容就是,它整合了 CommonJS 和 AMD 规范的特点。遵循 CMD 规范的代表开源项目是 sea.js ,它的实现和 requirejs 没有本质差别
UMD(Universal Module Definition,统一模块定义)其实并不是模块管理规范,而是带有前后端同构思想的模块封装工具。通过 UMD 可以在合适的环境选择对应的模块规范。比如在 Node.js 环境中采用 CommonJS 模块管理,在浏览器端且支持 AMD 的情况下采用 AMD 模块,否则导出为全局函数。
它的实现原理也比较简单:
1、先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式; 2、再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块; 3、若前两个都不存在,则将模块公开到全局(Window 或 Global)。
(function (root, factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports, module.exports = factory(); } else { root.returnExports = factory(); } }(this, function () { //。。。 return {}; }));