聊聊 JavaScript 的几种模块系统

2022-12-21 19:59:57 浏览数 (1)

大家好,我是前端西瓜哥,今天我们来聊聊 JavaScript 的模块系统。

模块系统

模块系统是什么?简单来说,其实就是我们在一个文件里写代码,声明一些可以导出的字段,然后另一个文件可以将其导入并使用。

模块化的优点:

  1. 文件里声明的变量会被隔离,不会暴露到全局,可以有效解决以往变量污染全局空间的问题;
  2. 更容易看出代码之间的依赖关系,看文件头的的导入代码就知道;
  3. 方便多人协作,各自开发自己的模块,而不冲突;
  4. 不用担心文件引入的顺序;
  5. 方便以文件为单位做单元测试;

模块化解决了变量污染、代码维护、依赖顺序问题。

CommonJS

CommonJS,或者叫 CJS,是 nodejs 选择的模块化标准。

导出模块的写法:

代码语言:javascript复制
// 集中一起导出
module.exports = {
  userName: '前端西瓜哥',
  // ...
}

// 或分散导出
module.exports.userName = '前端西瓜哥';

// 或
exports.userName = '前端西瓜哥';

每个文件都可以访问到一个 module 对象,其下的 exports 属性是一个空对象,你可以给它加上属性,module.exports 将作为可以导出的代码部分。

exports 是一个别名,它指向 module.exports,用它能够少打一点字。

然后是导入模块的写法:

代码语言:javascript复制
const Setting = require('./user');

// 或用解构写法,直接提取属性
const { userName } = require('./user');

// 或不使用任何导出内容,但希望指定对应模块文件的副作用(如给全局注入变量)
require('./user');

require 方法能够找到对应模块文件,提取出它的 module.exports 对象,引入到当前模块中。关于 require 怎么找到模块的过程,也是一篇长篇大论,有机会另开一篇文章再讲解了。

nodejs 现在最新版也支持用 ES Modules 的方式了,需要在 package.json 上加上 "type": "module"

ES Modules

ES Modules,或者叫 ESM,是 ES6 引入的新特性,是模块系统的真正标准。

在 script 标签下设置 type="module",可以开启模块化加载。 当然实际上生产环境我们是不会这么做的,只是用 ES Modules 写代码,然后打包,用传统的模式运行。

导出模块的写法:

代码语言:javascript复制
// 集中一起导出
export {
  userName: '前端西瓜哥',
  // ...
}

// 或分散指定
export const userName = '前端西瓜哥';

// 一种特殊的导出:默认指定
export default user;

ES Modules 中,export 不是一个对象,准确来说都不是变量,而是新引入的关键字,用于指定要导出的变量。

然后是导入模块的写法:

代码语言:javascript复制
// 导入需要的变量
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 标准已过时,不必花太多精力学习,简单了解即可。

AMD,是 Asynchronous Module Definition 的缩写。这是一种异步的模块加载方案,是 ES Module 发布前的一种浏览器模块化方案。

CommonJS 不适合浏览器端,因为它的模块加载是同步的,浏览器需要请求模块文件,是异步的。

AMD 的特点是 依赖前置,即所有的依赖模块要在开头指定好。

实现 AMD 规范的典型库是 require.js,require.js 的使用如下。

导出模块写法为:

代码语言:javascript复制
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 方法为主模块,也就是程序入口。

代码语言:javascript复制
require(["a", "b"], function(util) {
  // a.js 和 b.js 加载完会执行这个函数
})

CMD

CMD 也过时了,甚至比 AMD 还小众,同样简单了解即可。

CMD,全称 Common Module Definition,也是浏览器模块化的一种方案,和 AMD 是同时代的产物。

但和 AMD 不同的是,它的特点是 依赖就近,在具体的用到某个模块的地方引入即可,接近 CommonJS。

代表库是 sea.js,我们看看语法。

模块写法:

代码语言:javascript复制
define(function(require, exports, module) {
  // 导入 a 模块
 var a = require('./a');
  a.doSomething();
  
  // 导出当前模块
 module.exports = {
    name: 'b',
    start: function() {};
  };
});

主模块这样写:

代码语言:javascript复制
seajs.use(['./a', './b'], function(a, b) {
  a.doSomething();
  b.start();
});

写法很像 Commonjs 了。

ES Modules 和 CommonJS 的区别

  1. Commonjs 模块在 运行时 加载,ESM 在 编译时 确定依赖关系;
  2. require 可以在代码的任何地方使用,比如在条件语句内,因为它是运行时同步加载的。import 需要写在模块文件最外层,不能在其他任何作用域内,且 import 会做提升;
  3. require 永远是同步加载代码。import 一般也是同步的,但也能做动态加载,此时则是异步的。如 import('lang/zh.js').then(module) => { /* 获得语言包对象 */ }
  4. CommonJS 的导出是值拷贝,而 ESM的导出是值引用。比如 a.js 导出一个值为 1 的 count,b.js 导入 a.js 拿到 count,然后 a.js 中的定时器修改了 count 的值为 2,然后在 b.js 再打印 count 的值。你会发现 Commonjs 下,count 还是 1,而在 ESM 下则变成了 2(说明编译时做了一些处理)。
  5. ESM import 的变量是只读的,等价于用 const 声明;CommonJS 则随意用声明关键字,var、let、const 都可以。
  6. CommonJS 可以导入 json 文件,ESM 不可以(实际上我们使用打包工具,通过转换器支持各种文件的导入);

UMD

模块标准这么多,需要一个个构建不同的模块文件可太麻烦了。如果我只希望发布一份代码,就让它运行在不同的模块系统中,有办法吗?

可以考虑用 UMD(Universal Module Definition),它能够同时在 CommonJS、AMD 运行,如果都不是,则会暴露到全局环境中。

下面是 webpack(5.74.0)设置 output.libraryTarget"umd" 后的打包结果的部分代码。

代码语言:javascript复制
(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。

我是前端西瓜哥,欢迎关注我,学习更多前端知识。


0 人点赞