网站首页 > 技术文章 正文
作者:大道至简
转发链接:https://mp.weixin.qq.com/s/M2HysrNBXp8D5Y6ys45L_A
前言
模块化是大型前端项目的必备要素。JavaScript 从诞生至今,出现过各种各样的模块化方案,让我们一起来盘点下吧。
IIFE 模块
默认情况下,在浏览器宿主环境里定义的变量都是全局变量,如果页面引用了多个这样的 JavaScript 文件,很容易造成命名冲突。
// 定义全局变量
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
// 使用全局变量
increase();
reset();
为了避免全局污染,可以用匿名函数包裹起来,这就是最简单的 IIFE 模块(立即执行的函数表达式):
// 定义 IIFE 模块
const iifeCounterModule = (() => {
let count = 0;
return {
increase: () => ++count,
reset: () => {
count = 0;
console.log("Count is reset.");
}
};
})();
// 使用 IIFE 模块
iifeCounterModule.increase();
iifeCounterModule.reset();
IIFE 只暴露了一个全局的模块名,内部都是局部变量,大大减少了全局命名冲突。
每个 IIFE 模块都是一个全局变量,这些模块通常有自己的依赖。可以在模块内部直接使用依赖的全局变量,也可以把依赖作为参数传给 IIFE:
// 定义带有依赖的 IIFE 模块
const iifeCounterModule = ((dependencyModule1, dependencyModule2) => {
let count = 0;
return {
increase: () => ++count,
reset: () => {
count = 0;
console.log("Count is reset.");
}
};
})(dependencyModule1, dependencyModule2);
一些流行的库在早期版本都采用这模式,比如大名鼎鼎的 jQuery(最新版本也开始用 UMD 模块了,后面会介绍)。
还有一种 IIFE,在 API 声明上遵循了一种格式,就是在模块内部提前定义了这些 API 对应的变量,方便 API 之间互相调用:
// Define revealing module.
const revealingCounterModule = (() => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
return {
increase,
reset
};
})();
// Use revealing module.
revealingCounterModule.increase();
revealingCounterModule.reset();
CommonJS 模块(Node.js 模块)
CommonJS 最初叫 ServerJS,是由 Node.js 实现的模块化方案。默认情况下,每个 .js 文件就是一个模块,模块内部提供了一个module和exports变量,用于暴露模块的 API。使用 require 加载和使用模块。下面这段代码定义了一个计数器模块:
// 定义 CommonJS 模块: commonJSCounterModule.js.
const dependencyModule1 = require("./dependencyModule1");
const dependencyModule2 = require("./dependencyModule2");
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
exports.increase = increase;
exports.reset = reset;
// 或者这样:
module.exports = {
increase,
reset
};
使用这个模块:
// 使用 CommonJS 模块
const { increase, reset } = require("./commonJSCounterModule");
increase();
reset();
// 或者这样:
const commonJSCounterModule = require("./commonJSCounterModule");
commonJSCounterModule.increase();
commonJSCounterModule.reset();
在运行时,Node.js 会将文件内的代码包裹在一个函数内,然后通过参数传递exports、module变量和require函数。
// Define CommonJS module: wrapped commonJSCounterModule.js.
(function (exports, require, module, __filename, __dirname) {
const dependencyModule1 = require("./dependencyModule1");
const dependencyModule2 = require("./dependencyModule2");
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
module.exports = {
increase,
reset
};
return module.exports;
}).call(thisValue, exports, require, module, filename, dirname);
// Use CommonJS module.
(function (exports, require, module, __filename, __dirname) {
const commonJSCounterModule = require("./commonJSCounterModule");
commonJSCounterModule.increase();
commonJSCounterModule.reset();
}).call(thisValue, exports, require, module, filename, dirname);
AMD 模块(RequireJS 模块)
AMD(异步模块定义)也是一种模块格式,由 RequireJS 这个库实现。它通过define函数定义模块,并接受模块名和依赖的模块名作为参数。
// 定义 AMD 模块
define("amdCounterModule", ["dependencyModule1", "dependencyModule2"],
(dependencyModule1, dependencyModule2) => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
return {
increase,
reset
};
});
也用 require加载和使用模块:
require(["amdCounterModule"], amdCounterModule => {
amdCounterModule.increase();
amdCounterModule.reset();
});
跟 CommonJS 不同,这里的 requrie接受一个回调函数,参数就是加载好的模块对象。
AMD 的define函数还可以动态加载模块,只要给它传一个回调函数,并带上 require参数:
// Use dynamic AMD module.
define(require => {
const dynamicDependencyModule1 = require("dependencyModule1");
const dynamicDependencyModule2 = require("dependencyModule2");
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
return {
increase,
reset
};
});
AMD 模块还可以给define传递module和exports,这样就可以在内部使用 CommonJS 代码:
// 定义带有 CommonJS 代码的 AMD 模块
define((require, exports, module) => {
// CommonJS 代码
const dependencyModule1 = require("dependencyModule1");
const dependencyModule2 = require("dependencyModule2");
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
exports.increase = increase;
exports.reset = reset;
});
// 使用带有 CommonJS 代码的 AMD 模块
define(require => {
// CommonJS 代码
const counterModule = require("amdCounterModule");
counterModule.increase();
counterModule.reset();
});
UMD 模块
UMD(通用模块定义),是一种支持多种环境的模块化格式,可同时用于 AMD 和 浏览器(或者 Node.js)环境。
兼容 AMD 和浏览器全局引入:
((root, factory) => {
// 检测是否存在 AMD/RequireJS 的 define 函数
if (typeof define === "function" && define.amd) {
// 如果是,在 define 函数内调用 factory
define("umdCounterModule", ["deependencyModule1", "dependencyModule2"], factory);
} else {
// 否则为浏览器环境,直接调用 factory
// 导入的依赖是全局变量(window 对象的属性)
// 导出的模块也是全局变量(window 对象的属性)
root.umdCounterModule = factory(root.deependencyModule1, root.dependencyModule2);
}
})(typeof self !== "undefined" ? self : this, (deependencyModule1, dependencyModule2) => {
// 具体的模块代码
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
return {
increase,
reset
};
});
看起来很复杂,其实就是个 IIFE。代码注释写得很清楚了,可以看看。下面来看兼容 AMD 和 CommonJS(Node.js)模块的 UMD:
(define => define((require, exports, module) => {
// 模块代码
const dependencyModule1 = require("dependencyModule1");
const dependencyModule2 = require("dependencyModule2");
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log("Count is reset.");
};
module.export = {
increase,
reset
};
}))(// 判断 CommonJS 里的 module 变量和 exports 变量是否存在
// 同时判断 AMD/RequireJS 的define 函数是否存在
typeof module === "object" && module.exports && typeof define !== "function"
? // 如果是 CommonJS/Node.js,手动定义一个 define 函数
factory => module.exports = factory(require, exports, module)
: // 否则是 AMD/RequireJS,直接使用 define 函数
define);
同样是个 IIFE,通过判断环境,选择执行对应的代码。
ES 模块(ES6 Module)
前面说到的几种模块格式,都是用到了各种技巧实现的,看起来眼花缭乱。终于,在 2015 年,ECMAScript 第 6 版(ES 2015,或者 ES6 )横空出世!它引入了一种全新的模块格式,主要语法就是 import和epxort关键字。来看 ES6 怎么定义模块:
// 定义 ES 模块:esCounterModule.js 或 esCounterModule.mjs.
import dependencyModule1 from "./dependencyModule1.mjs";
import dependencyModule2 from "./dependencyModule2.mjs";
let count = 0;
// 具名导出:
export const increase = () => ++count;
export const reset = () => {
count = 0;
console.log("Count is reset.");
};
// 默认导出
export default {
increase,
reset
};
浏览器里使用该模块,在 script标签上加上type="module",表明引入的是 ES 模块。在 Node.js 环境中使用时,把扩展名改成 .mjs。
// Use ES module.
//浏览器: <script type="module" src="esCounterModule.js"></script> or inline.
// 服务器:esCounterModule.mjs
import { increase, reset } from "./esCounterModule.mjs";
increase();
reset();
// Or import from default export:
import esCounterModule from "./esCounterModule.mjs";
esCounterModule.increase();
esCounterModule.reset();
浏览器如果不支持,可以加个兜底属性:
<script nomodule> alert("Not supported.");</script>
ES 动态模块(ECMAScript 2020)
2020 年最新的 ESCMA 标准11版中引入了内置的 import函数,用于动态加载 ES 模块。import函数返回一个 Promise,在它的then回调里使用加载后的模块:
// 用 Promise API 加载动态 ES 模块
import("./esCounterModule.js").then(({ increase, reset }) => {
increase();
reset();
});
import("./esCounterModule.js").then(dynamicESCounterModule => {
dynamicESCounterModule.increase();
dynamicESCounterModule.reset();
});
由于返回的是 Promise,那肯定也支持await用法:
// 通过 async/await 使用 ES 动态模块
(async () => {
// 具名导出的模块
const { increase, reset } = await import("./esCounterModule.js");
increase();
reset();
// 默认导出的模块
const dynamicESCounterModule = await import("./esCounterModule.js");
dynamicESCounterModule.increase();
dynamicESCounterModule.reset();
})();
各平台对import、export和动态import的兼容情况如下:
image.png
image.png
System 模块
SystemJS 是一个 ES 模块语法转换库,以便支持低版本的 ES。例如,下面的模块是用 ES6 语法定义的:
// 定义 ES 模块
import dependencyModule1 from "./dependencyModule1.js";
import dependencyModule2 from "./dependencyModule2.js";
dependencyModule1.api1();
dependencyModule2.api2();
let count = 0;
// Named export:
export const increase = function () { return ++count };
export const reset = function () {
count = 0;
console.log("Count is reset.");
};
// Or default export:
export default {
increase,
reset
}
如果当前的运行环境(比如旧浏览器)不支持 ES6 语法,上面的代码就无法运行。一种方案是把上面的模块定义转换成 SystemJS 库的一个 API, System.register:
// Define SystemJS module.
System.register(["./dependencyModule1.js", "./dependencyModule2.js"],
function (exports_1, context_1) {
"use strict";
var dependencyModule1_js_1, dependencyModule2_js_1, count, increase, reset;
var __moduleName = context_1 && context_1.id;
return {
setters: [
function (dependencyModule1_js_1_1) {
dependencyModule1_js_1 = dependencyModule1_js_1_1;
},
function (dependencyModule2_js_1_1) {
dependencyModule2_js_1 = dependencyModule2_js_1_1;
}
],
execute: function () {
dependencyModule1_js_1.default.api1();
dependencyModule2_js_1.default.api2();
count = 0;
// Named export:
exports_1("increase", increase = function () { return ++count };
exports_1("reset", reset = function () {
count = 0;
console.log("Count is reset.");
};);
// Or default export:
exports_1("default", {
increase,
reset
});
}
};
});
这样,import/export关键字就不见了。Webpack、TypeScript 等可以自动完成这样的转换(后面会讲)。
SystemJS 也支持动态加载模块:
// Use SystemJS module with promise APIs.
System.import("./esCounterModule.js").then(dynamicESCounterModule => {
dynamicESCounterModule.increase();
dynamicESCounterModule.reset();
});
Webpack 模块(打包 AMD,CJS,ESM)
Webpack 是个强大的模块打包工具,可以将 AMD、CommonJS 和 ES Module 格式的模块转换并打包到单个 JS 文件。
Babel 模块
Babel 是也个转换器,可将 ES6+ 代码转换成低版本的 ES。前面例子中的计数器模块用 Babel 转换后的代码是这样的:
// Babel.
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
function _interopRequireDefault(obj)
{ return obj && obj.__esModule ? obj : { "default": obj }; }
// Define ES module: esCounterModule.js.
var dependencyModule1 = _interopRequireDefault(require("./amdDependencyModule1"));
var dependencyModule2 = _interopRequireDefault(require("./commonJSDependencyModule2"));
dependencyModule1["default"].api1();
dependencyModule2["default"].api2();
var count = 0;
var increase = function () { return ++count; };
var reset = function () {
count = 0;
console.log("Count is reset.");
};
exports["default"] = {
increase: increase,
reset: reset
};
引入该模块的index.js将会转换成:
// Babel.
function _interopRequireDefault(obj)
{ return obj && obj.__esModule ? obj : { "default": obj }; }
// Use ES module: index.js
var esCounterModule = _interopRequireDefault(require("./esCounterModule.js"));
esCounterModule["default"].increase();
esCounterModule["default"].reset();
以上是 Babel 的默认转换行为,它还可以结合其他插件使用,比如前面提到的 SystemJS。经过配置,Babel 可将 AMD、CJS、ES Module 转换成 System 模块格式。
TypeScript 模块
TypeScript 是 JavaScript 的超集,可以支持所有 JavaScript 语法,包括 ES6 模块语法。它在转换时,可以保留 ES6 语法,也可以转换成 AMD、CJS、UMD、SystemJS 等格式,取决于配置:
{
"compilerOptions": {
"module": "ES2020", // None, CommonJS, AMD, System, UMD, ES6, ES2015, ES2020, ESNext.
}
}
TypeScript 还支持 module和namespace关键字,表示内部模块。
module Counter {
let count = 0;
export const increase = () => ++count;
export const reset = () => {
count = 0;
console.log("Count is reset.");
};
}
namespace Counter {
let count = 0;
export const increase = () => ++count;
export const reset = () => {
count = 0;
console.log("Count is reset.");
};
}
都可以转换成 JavaScript 对象:
var Counter;
(function (Counter) {
var count = 0;
Counter.increase = function () { return ++count; };
Counter.reset = function () {
count = 0;
console.log("Count is reset.");
};
})(Counter || (Counter = {}));
总结
以上提到的各种模块格式是在 JavaScript 语言演进过程中出现的模块化方案,各有其适用环境。随着标准化推进,Node.js 和最新的现代浏览器都开始支持 ES 模块格式。如果要在旧环境中使用模块化,可以通过 Webpack、Babel、TypeScript、SystemJS 等工具进行转换。
推荐JavaScript经典实例学习资料文章
《JavaScript正则深入以及10个非常有意思的正则实战》
《前端开发规范:命名规范、html规范、css规范、js规范》
《100个原生JavaScript代码片段知识点详细汇总【实践】》
《手把手教你深入巩固JavaScript知识体系【思维导图】》
《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》
《身份证号码的正则表达式及验证详解(JavaScript,Regex)》
《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》
《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《面试中教你绕过关于 JavaScript 作用域的 5 个坑》
作者:大道至简
转发链接:https://mp.weixin.qq.com/s/M2HysrNBXp8D5Y6ys45L_A
- 上一篇: 服务器的常用配置方法,简单总结(多图)
- 下一篇: 系统小技巧:软件卸载不了?这里办法多
猜你喜欢
- 2024-11-17 JavaScript 模块的构建以及对应的打包工具
- 2024-11-17 系统小技巧:软件卸载不了?这里办法多
- 2024-11-17 服务器的常用配置方法,简单总结(多图)
- 2024-11-17 仪表盘设计的 7 个阶段(仪表盘的设计原则)
- 2024-11-17 系统分析STM32的上电启动过程(stm32上电不启动)
- 2024-11-17 微软发力 Linux,挖来 Systemd 开发者 Lennart Poettering
- 2024-11-17 在 .NET 应用程序中运行 JavaScript,你会了吗?
- 2024-11-17 JavaScript初学者指南(javascript初级教程)
- 2024-11-17 参宿四未爆,“末日彗星”已近?(参宿四超新星爆发效果图)
- 2024-11-17 微前端落地:Systemjs模块化解决方案
- 标签列表
-
- content-disposition (47)
- nth-child (56)
- math.pow (44)
- 原型和原型链 (63)
- canvas mdn (36)
- css @media (49)
- promise mdn (39)
- readasdataurl (52)
- if-modified-since (49)
- css ::after (50)
- border-image-slice (40)
- flex mdn (37)
- .join (41)
- function.apply (60)
- input type number (64)
- weakmap (62)
- js arguments (45)
- js delete方法 (61)
- blob type (44)
- math.max.apply (51)
- js (44)
- firefox 3 (47)
- cssbox-sizing (52)
- js删除 (49)
- js for continue (56)
- 最新留言
-