模块化
一、什么是模块
模块通常指的是一个独立的可重用软件组件,它包含了一组相关的函数、方法、类、变量等,能够实现特定的功能或提供特定的服务。
在编程中,可以将程序的功能划分成不同的模块,每个模块负责不同的任务。JavaScript 的模块化是一种将代码组织到独立的、可重用的、可维护的单元中的方式,使代码更易于管理和复用。模块化可以帮助开发者将代码分解成更小的部分,这样更容易理解和维护,同时也可以防止命名冲突和全局污染。在 JavaScript 中,有多种模块化的标准和实现,如 CommonJS、AMD、ES6 等。其中 ES6 的模块化已经成为 JavaScript 的官方标准,可以直接在浏览器和 Node.js 中使用。
初始阶段,直接导入
文件分离是最基础的模块化
- 直接加载,解析到标签立刻 pending,并且下载并执行。
- defer 解析到标签的时候会异步下载, 下载完成后开始执行。(在执行到加了 defer 的 script 时会推入下载进程,然后再去执行下一个标签。待到这个 script 的外链下载完毕时,需要看 DOM 是否渲染完毕了,如果渲染完毕了则执行 defer script 的内容,然后触发 DOMContentLoaded 的回调。多个 defer 等待所有 defer 下载完依次执行。)
- async 解析到标签的时候会异步下载,下载完成后立刻执行,阻塞继续解析(下载完成后不解析,先执行,执行完成后继续解析),执行完成之后在继续向下执行。(在执行到加了 async 的 script 时会先下载,然后再去执行下一个标签。待到这个 script 的外链下载完毕时,如果 DOM 正在渲染则暂停,执行 async script 的内容。多个 async 先下载完的先执行 async 和 DOMContentLoaded 无任何绑定关系。)
问题导向 => 浏览器渲染原理,同步异步原理,模块化加载解析。
<script src="xxx.js" type="text/javascript"></script>
<script src="xxx.js" type="text/javascript" defer></script>
<script src="xxx.js" type="text/javascript" async></script>
存在问题:变量之间相互污染,不利于大型项目的开发
IIFE(立刻执行函数,作用:作用域隔离)
通过立即执行函数实现作用域把控,如下:
let count = 0;
// 代码块1
const inscease = () => ++count;
// 代码块2
const reset = () => {
count = 0;
};
inscease();
reset();
利用函数的独立作用域:
(() => {
let count = 0;
// ...
})();
通过 定义函数 + 立即执行 得到 独立的空间,初步实现了一个最底层的最简单的模块。
下面将定义一个简单的模块化:
// 外部依赖依赖外部的传参处理
// dependencyModule1, dependencyModule2 代表外部依赖
const module = ((dependencyModule1, dependencyModule2) => {
let count = 0;
// dependencyModule1...
// 代码块...
// dependencyModule2...
// 代码块...
return {
inscease: () => ++count,
reset: () => {
count = 0;
},
};
})(dependencyModule1, dependencyModule2);
module.inscease();
module.reset();
// 揭示模式:返回的是能力 => 使用方的传参 + 本身的逻辑实现 + 其他依赖
const module = ((dependencyModule1, dependencyModule2) => {
let count = 0;
// dependencyModule1, dependencyModule2 代表外部依赖
// 代码块1
const inscease = () => {
// dependencyModule1...
// ...
};
// 代码块2
const reset = () => {
// dependencyModule2...
// ...
};
return {
inscease,
reset,
};
})(dependencyModule1, dependencyModule2);
// 例如:jquery 模块化处理方案
$("xxx").attr("title", "xxx");
面试问题导向:
- 深入模块化实现
- 转向框架 jquery, vue, react 模块化组件实现细节,以及框架原理特性。
- 设计模式 - 注重模块化的设计
CJS(commonjs)
- CommonJS 是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为 ServerJS,后来为了体现它的广泛性,修改为 CommonJS,也可简称为 CJS。
- 所有代码都运行在模块作用域,不会污染全局作用域;
- 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就会被缓存,以后再加载,将直接读取缓存结果。若想模块再次运行,必须清除缓存;
- 模块加载顺序,按照其在代码中出现的顺序;
特征:
- 通过 modules + export 向外部暴露接口。
- 通过 require 方式去调用其他模块
const dependencyModule1 = require('dependencyModule1')
const dependencyModule2 = require('dependencyModule2')
let count = 0;
// 代码块1
const inscease = () => {
// dependencyModule1...
// ...
};
// 代码块2
const reset = () => {
// dependencyModule2...
// ...
};
// 暴露出去
export.inscease = inscease;
export.reset = reset;
// 或者
module.exports = {
inscease,
reset
}
调用方式
const { inscease, reset } = require("dep.js");
inscease();
reset();
优缺点:
- 优点: CJS 从服务侧的角度解决了依赖全局污染的问题,但是相对于 IIFE 在写法上也实现了在写法上主观感受的模块化。
- 缺点: 针对服务端 => 存在异步的问题 => AMD模块化
AMD (经典实现框架:require.js)
AMD 是 Asynchronous Module Definition(异步模块定义)的缩写;
- CommonJS 规范加载模块是同步的,即加载完成,才能执行后面的操作。
- AMD 规范则是非同步加载模块,允许指定回调函数。
- 由于 Node.js 主要用于服务器编程,模块文件一已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用服务端变成。但是,如果是浏览器环境,就需要先从服务器将文件下来,之后再加载运行,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范。 事实上 AMD 的规范还要早于 CommonJS,但是 CommonJS 目前依然在被使用,而 AMD 使用少了。
特性:支持通过异步加载以及允许定制回调函数
// 通过define定义一个模块,然后通过require加载
define(id, [depends], callback);
require([module], callback);
define("amdModule", [dependencyModule1, dependencyModule2], (
dependencyModule1,
dependencyModule2
) => {
let count = 0;
// 代码块1
const inscease = () => {
// dependencyModule1...
// ...
};
// 代码块2
const reset = () => {
// dependencyModule2...
// ...
};
export.inscease = inscease;
export.reset = reset;
});
// 加载
require(['amdModule'], amdModule => {
amdModule.inscease()
amdModule.reset()
})
// 内部加载模块, 异步加载
define(['amdModule'], [], require => {
const dependencyModule1 = require('dependencyModule1')
const dependencyModule2 = require('dependencyModule2')
amdModule.inscease()
amdModule.reset()
})
面试导向:
- 如何区分 CJS 和 AMD 或者是普通 js => - 通过 module 判断是否是普通 js,通过 define 判断是否是 AMD
- 手写兼容 CJS 和 AMD 模块
(define(['amdModule'], [], require => {
const dependencyModule1 = require('dependencyModule1')
const dependencyModule2 = require('dependencyModule2')
amdModule.inscease()
amdModule.reset()
}))(
// 目标:一次去定位区分CJS和AMD
// 1. CJS factory
// 2. module module.exports
// 3. define
typeof module === 'Object'
&& module.exports
&& typeof define !== 'function'
? // 是CJS
factory => module.exports = factory(require, exports, module, args...)
: // 是AMD
define
)
优点:解决了服务端,客户端异步动态依赖的问题 缺点:会有引入成本,没有考虑按需加载
umd 的实现:
(function (root, factory) {
if (typeof module === "object" && typeof module.exports === "object") {
console.log("是commonjs模块规范,nodejs环境");
module.exports = factory();
} else if (typeof define === "function" && define.amd) {
console.log("是AMD模块规范,如require.js");
define(factory);
} else if (typeof define === "function" && define.cmd) {
console.log("是CMD模块规范,如sea.js");
define(function (require, exports, module) {
module.exports = factory();
});
} else {
console.log("没有模块环境,直接挂载在全局对象上");
root.umdModule = factory();
}
})(this, function () {
return {
name: "我是一个umd模块",
};
});
CMD(阿里:按需加载,代表框架:sea.js)依赖就近原则
CMD 是 Common Module Definition(通用模块定义)的缩写; CMD 规范同样专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD 规范整合了 CommonJS 和 AMD 规范的特点。
define('module', (require, exports, modules) => {
let module1 = require('module1'),
if (xxx) {
// 代码块
return ...
}
const dependencyModule1 = require('dependencyModule1')
// 代码块
})
优点:按需加载,依赖就近 缺点:依赖打包,编译阶段完成,扩大了模块内的体积,内部拆分,空间换性能
ESM
- 目前可以通过 esmodule 或者 commonjs 实现模块化。两者的区别是 esmodule 需要浏览器支持才能用。webpack 是打包出来变成普通的 js 文件,基本上大多数地方都可以直接用。
- ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
import dependencyModule1 from "dependencyModule1";
import dependencyModule2 from "dependencyModule2";
// 代码块1
const inscease = () => {
// dependencyModule1...
// ...
};
// 代码块2
const reset = () => {
// dependencyModule2...
// ...
};
export default { inscease, reset };
问题:如何处理异步依赖:
// es 方式
const dependsModule = async () => {
return await dependsModule.init();
};
// 原生方式
import xxx from "xxx";
// es11,类似代码中的cdn远程加载
import("XXX").then((dependModules) => {
dependModules.init();
});
ES6 模块与 CommonJS 模块的差异:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用;
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
优点:通过一种最统一的形态整合了所有的 js 的模块化
总结:
CommonJS 规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了 AMD CMD 解决方案;
AMD 规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD 规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;
CMD 规范与 AMD 规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在 Node.js 中运行;
UMD 为同时满足 CommonJS, AMD, CMD 标准的实现;
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案;