大家好,我是前端西瓜哥,今天我们来聊聊 JavaScript 的模块系统。
模块系统是什么?简单来说,其实就是我们在一个文件里写代码,声明一些可以导出的字段,然后另一个文件可以将其导入并使用。
模块化的优点:
模块化解决了变量污染、代码维护、依赖顺序问题。
CommonJS,或者叫 CJS,是 nodejs 选择的模块化标准。
导出模块的写法:
// 集中一起导出module.exports = { userName: '前端西瓜哥', // ...}// 或分散导出module.exports.userName = '前端西瓜哥';// 或exports.userName = '前端西瓜哥';
每个文件都可以访问到一个 module 对象,其下的 exports 属性是一个空对象,你可以给它加上属性,module.exports 将作为可以导出的代码部分。
exports 是一个别名,它指向 module.exports,用它能够少打一点字。
然后是导入模块的写法:
const Setting = require('./user');// 或用解构写法,直接提取属性const { userName } = require('./user');// 或不使用任何导出内容,但希望指定对应模块文件的副作用(如给全局注入变量)require('./user');
require 方法能够找到对应模块文件,提取出它的 module.exports 对象,引入到当前模块中。关于 require 怎么找到模块的过程,也是一篇长篇大论,有机会另开一篇文章再讲解了。
nodejs 现在最新版也支持用 ES Modules 的方式了,需要在 package.json 上加上 "type": "module"。
ES Modules,或者叫 ESM,是 ES6 引入的新特性,是模块系统的真正标准。
在 script 标签下设置 type="module",可以开启模块化加载。
当然实际上生产环境我们是不会这么做的,只是用 ES Modules 写代码,然后打包,用传统的模式运行。
导出模块的写法:
// 集中一起导出export { userName: '前端西瓜哥', // ...}// 或分散指定export const userName = '前端西瓜哥';// 一种特殊的导出:默认指定export default user;
ES Modules 中,export 不是一个对象,准确来说都不是变量,而是新引入的关键字,用于指定要导出的变量。
然后是导入模块的写法:
// 导入需要的变量import { userName } from './user.js';// 导入 export default 对应的变量,这时候不需要花括号import user from './user.js';// 在命名冲突时,可以给导入的变量换个名字import user as otherUser from './user.js';// 将 export 组装成对象,包括 export default,对应 default 属性名import * as obj from './user.js'
根据标准,导入的模块名是要提供 .js 后缀名的,因为实际上的 ES Module 是会请求一个 url 的,url 必须精准。
不过我们通常会使用打包工具,可以配置一下将其省略。比如 webpack,我们可以设置 resolve.extensions 配置项来设置后缀不存在时,拼上什么后缀去查找。
AMD 标准已过时,不必花太多精力学习,简单了解即可。
AMD,是 Asynchronous Module Definition 的缩写。这是一种异步的模块加载方案,是 ES Module 发布前的一种浏览器模块化方案。
CommonJS 不适合浏览器端,因为它的模块加载是同步的,浏览器需要请求模块文件,是异步的。
AMD 的特点是 依赖前置,即所有的依赖模块要在开头指定好。
实现 AMD 规范的典型库是 require.js,require.js 的使用如下。
导出模块写法为:
define(["./cart", "./inventory"], function (cart, inventory) { // 从 cart.js 和 inventory.js 拿到对应的导出内容 // 然后通过返回值指定当前模块的导出内容 return { color: "blue", size: "large", addToCart: function () { inventory.decrement(this); cart.add(this); }, };});
需要用 define 函数,第一参数为依赖的其他模块,然后第二个参数函数可以拿到这些模块导出的内容,然后这个函数的返回值就是当前文件的导出内容。
require 方法为主模块,也就是程序入口。
require(["a", "b"], function(util) { // a.js 和 b.js 加载完会执行这个函数})
CMD 也过时了,甚至比 AMD 还小众,同样简单了解即可。
CMD,全称 Common Module Definition,也是浏览器模块化的一种方案,和 AMD 是同时代的产物。
但和 AMD 不同的是,它的特点是 依赖就近,在具体的用到某个模块的地方引入即可,接近 CommonJS。
代表库是 sea.js,我们看看语法。
模块写法:
define(function(require, exports, module) { // 导入 a 模块 var a = require('./a'); a.doSomething(); // 导出当前模块 module.exports = { name: 'b', start: function() {}; };});
主模块这样写:
seajs.use(['./a', './b'], function(a, b) { a.doSomething(); b.start();});
写法很像 Commonjs 了。
模块标准这么多,需要一个个构建不同的模块文件可太麻烦了。如果我只希望发布一份代码,就让它运行在不同的模块系统中,有办法吗?
可以考虑用 UMD(Universal Module Definition),它能够同时在 CommonJS、AMD 运行,如果都不是,则会暴露到全局环境中。
下面是 webpack(5.74.0)设置 output.libraryTarget 为 "umd" 后的打包结果的部分代码。
(function (root, factory) { // 判断是否为 commonjs if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); // 判断是否为 requirejs else if(typeof define === 'function' && define.amd) define([], factory); else { var a = factory(); for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; }})(self, () => { // 模块内容 return { user: '前端西瓜哥' }})
原理也不复杂,就是先通过判断不同模块系统提供的全局变量是否存在,且类型符合预期,就能知道是 CommonJS 还是 requirejs,然后使用它们对应的语法。都不匹配,那就暴露到全局。
虽然但是,UMD 无法支持 ES Modules,因为它的 import 不是变量,而是一个关键字,是编程语言层面的语法,在其他模块系统中存在会报错,且 import 只能在模块最外边使用。
模块系统太多,让我们有不小的心智负担,但正统在 ES Modules,nodejs 现在也完全支持 ES Modules 了。但因为历史问题,我们还是要保留 CommonJS。
我是前端西瓜哥,欢迎关注我,学习更多前端知识。