一、前言
传统方式下,JS 若想引入其它 JS 文件时,通常使用 <script>
语法来完成,然而引入的 JS 往往易于造成命名污染,为了解决这问题,模块化
开发的概念逐渐浮现。
本文将以完整的 Demo 将各大模块模块的概念和写法进行阐述与演示,希望对你有帮助。
二、AMD
2.1 介绍
AMD
就是为了解决命名污染而研发的,同时还支持按需加载,是第一个引入 模块化
开发的规范插件,要想使用 AMD
语法得借助一款插件 RequireJS
。
注意,AMD 只适用于浏览器,虽然也支持 Node,但不如 Node 自家的 CJS,后面会讲。
2.2 使用
-
目录结构
-
引入
requirejs.js
插件
// index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script data-main="app.js" src="https://requirejs.org/docs/release/2.3.6/r.js"></script>
</head>
<body><div id="app">Hello,world</div>
</body>
</html>
参数解释:data-main
代表 JS 入口文件,当 src 加载完插件后,会立刻执行 app.js
。
- 在
app.js
入口文件内进行一些 AMD 配置
// app.js
requirejs.config({baseUrl: 'modules/', // 引入模块的基路径。
});
- 使用
define
定义模块
// modules/tools.js
define('tools', function() {const getDeviceType = function() {return 'Android'}return {getDeviceType: getDeviceType,}
})
- 使用
require
加载模块
// app.js
requirejs.config({baseUrl: 'modules/',
});
// 根据上述配置的 baseUrl + 'tools' 去加载这个模块。
require(['tools'], function(tools) {console.log('tools', tools)
})
效果:
小结:define && require
= AMD,更多高级 API 和配置可参考官方。
三、CommonJS
3.1 介绍
CommonJS
也常被称为 CJS
,与 ADMI
一样属于模块化语法,不过它是用来兼容后端 Nodejs
语言,庆幸的是,CJS
在 Node.js 中已内置,开箱即用,无需引入插件。
3.2 使用
-
案例结构
-
使用
exports.module
定义模块
// modules/tools.js
const getDeviceType = () => {return 'Android'
}exports.module = {getDeviceType,
}
- 使用
require
加载模块
const tools = require('./modules/tools')
const app = () => {console.log('tools', tools)
}
app();
- 执行 node
小结:exports.module && require
= CJS
提示:文件后缀也可以是
.cjs
,Node 会自动识别。
四、UMD
4.1 介绍
UMD
是 AMD
+ CommonJS
的结合体,同时还兼容了 script 标签引入,对组件库或框架库来说,解决了以前一套代码无法多端使用的难题。UMD
模块可借助 Rollup
工具来完成。
4.2 使用
- 安装
rollup
npm i rollup -g
-
案例结构
modules/tools.js
模块代码如下:const getDeviceType = () => {return 'Android' } export { getDeviceType }
注意:代码采用的是 ESM 写法,后面会讲到。
-
通过
rollup
将文件打包成 UMD 产物rollup ./modules/tools.js --file ./modules/tools_umd.js --format umd --name=tools
解释:
--file
表示自定义输出产物的目录和文件名。--format
表示文件的转换格式,这里我们转成umd
即可。打包后的tools_umd.js
将放在modules/
目录下,产物内容如下:--name
表示兼容 script 标签,将数据挂载到指定的全局对象变量中,后续通过window[name]
来获取模块。
构建完的产物如下:
(function (global, factory) {typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :typeof define === 'function' && define.amd ? define(['exports'], factory) :(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.tools = {}));
})(this, (function (exports) { 'use strict';const getDeviceType = () => {return 'Android'};exports.getDeviceType = getDeviceType;
}));
- 在浏览器引入 UMD 产物
- 首先在
index.html
中引入 AMD 插件requirejs
,并执行app-web.js
,如下:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script data-main="app-web.js" src="https://requirejs.org/docs/release/2.3.6/r.js"></script>
</head><body><div id="app">Hello,world</div>
</body>
</html>
- 在
app-web.js
中引入加载刚刚打包好的 UMD 模块:
requirejs.config({baseUrl: 'modules/',
});
require(['tools_umd'], function (tools) {console.log('tools', tools)
})
效果:
- 在浏览器通过
script
标签引入 UMD 产物
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><!-- <script data-main="app-web.js" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script> --><script src="./modules/tools_umd.js"></script>
</head><body><div id="app">Hello,world</div>
</body>
</html>
效果:
- 在 Node 引入 UMD 产物
const tools = require('./modules/tools_umd.js')
const app = () => {console.log('Tools are: ', tools)
}
app()
效果:
- 在浏览器中通过 ESM 引入 UMD 产物:
由于 UMD 仅兼容 AMD/CJS
,可使用 rollup 将 format 设置 esm 版本即可:
rollup ./modules/tools.js --file ./modules/tools_esm.js --format esm
本质上和原先的
tools.js
代码没区别:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><!-- <script data-main="app-web.js" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script> --><!-- <script src="./modules/tools_umd.js"></script> --><script type="module" src="app.js"></script>
</head><body><div id="app">Hello,world</div>
</body></html>
// app.js
import { getDeviceType } from './modules/tools_esm.js';
const app = () => {console.log('Tool method:', getDeviceType)
}
app()
小结:
- UMD =
AMD
+CJS
+Script 标签
- UMD 需要借助打包工具如
Webpack/Rollup
- 除了
Webpack/Rollup
工具,还可以结合其它工具如 Babel,可以将最新的 ES 语法转成低版本的语法,当然这是题外话,对 Babel 话题感兴趣的可参考:JS & 介绍 Babel 的使用及 presets & plugins 的概念
五、ESM
5.1 介绍
由于浏览器始终得借助于 ADM
或 UMD
来进行模块化开发,官方实在看不下去,决定让浏览器内置一套模块化机制,俗称 ESM
,目的就是减少 AMD
或 UMD
的依赖实现统一规范,它与 CJS 一样,现代浏览器已经内置好了。
由于 ESM 技术较新,一些旧浏览器无法兼容,因此现在大多项目要借助 Webpack/Rollup 等构建工具将 ESM 打包成兼容的语法;也就说,我们可以在项目中写 ESM 代码,但在运行期间,代码会以兼容语法的方式来执行。
5.2 使用
其实在前面的 UMD
章节第 7 点我们已经使用过 ESM 语法了,除了 export
导出方式,还有其它导出的方式 export default
,它可以导出完整的对象,接下来将基于此语法作为演示。
提示:浏览器使用 ESM 的前提条件是给 script 标签加上
type="module"
另外,这里我们不再借助 Rollup 构建转换工具,现代浏览器本身就支持 ESM。
- 案例结构:
- 原来的
index.html
内容不变:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script type="module" src="app.js"></script>
</head>
<body><div id="app">Hello,world</div>
</body>
</html>
- 改造
app.js
// app.js
import tools from './modules/tools.js';
const app = () => {console.log('Tools are:', tools)
}
app()
- 改造
modules/tools.js
,使用export default
方式导出
// modules/tools.js
const getDeviceType = () => {return 'Android'
}
const getDeviceVersion = () => {return '1.0.0'
}
const tools = {getDeviceType,getDeviceVersion
}
export default tools
效果:
5. export
与 export default
支持混合使用
一个 JS 文件中,
export
可以有无限个,但export default
只能有一个。
// modules/tools.js
...
export { getDeviceType, getDeviceVersion }
export default tools
// app.js
import tools from './modules/tools.js';
import { getDeviceType } from './modules/tools.js';
const app = () => {console.log('Tools are:', tools)console.log('Method is:', getDeviceType)
}
app()
六、Nodejs 如何使用 ESM?
6.1 介绍
ESM
语法已经成为现代模块化标准规范,Nodejs
从 12.0.0
版本就开始支持 ESM
,前提条件是,文件名后缀必须为 .mjs
,若想省略 .mjs
,可在 package.json
设置 type: "module"
即可;
提示:若
A.js
想引入B.mjs
,也要在package.json
设置"type": "module"
,否则 node 运行时也会报错,如果是A.mjs
引入B.mjs
则不用配置。
6.2 使用
-
案例结构
-
改造
tools.mjs
,代码如同上述的 ESM
const getDeviceType = () => {return 'Android'
}const getDeviceVersion = () => {return '1.0.0'
}const tools = {getDeviceType,getDeviceVersion
}export { getDeviceType, getDeviceVersion }export default tools
- 改造
app.js
import tools from './modules/tools.mjs'
const app = () => {console.log('Tools are:', tools)
}
app()
- 在
node-esm
跟目录下执行npm init -y
,将会生成package.json
,里面我们只需新增"type": "module"
即可
{"name": "node-esm","version": "1.0.0","description": "","main": "app.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"keywords": [],"author": "","license": "ISC","type": "module"
}
提示:我们可以把前面的
tools.mjs
换成tools.js
,因为我们已经设定了"type: "module"
,但为了方便演示,这里就不变更了。
- 运行
node app.js
效果:
注意:一个 .js 文件不可能同时存在 ESM 或者 CJS 两种语法,也就是你的项目只能选择其中一种模块化语法来开发,否则执行时会抛出错误。像前端常见的 Vite 脚手架,我们之所以能够写 ESM 语法而不用在当前项目设定
module
的原因是, vite 内部的package.json
早已设定好"type": "module"
,通过 vite 来执行项目时会自动解析我们写的ESM 代码。
----------------------------------------------------------------结尾----------------------------------------------------------------
完整 demo 已放到 github 。
参考文献:
https://requirejs.org/docs/api.html#usage
https://adostes.medium.com/authoring-a-javascript-library-that-works-everywhere-using-rollup-f1b4b527b2a9
https://blog.logrocket.com/how-to-use-ecmascript-modules-with-node-js/