Skip to content

前言

随着项目越来越大,不可避免的要进行模块化,模块化的目的是使得将功能分割到各个模块,需要用到什么功能的时候,就引入对应的模块。

模块化最基本的功能就是:

  • 模块导入导出
  • 避免命名冲突

避免命名冲突这个是模块自带的属性,了解即可,在模块内部和模块外部的变量使用系统的名字,也不会冲突。

最关键的模块的导入和导出。由于JS语言实现模块化的方案有多种,每一种模块化方案是不同的规范标准,因此其导入导出规则也不相同,我们需要掌握的就是常用模块化规范的导入导出规则。

常见模块化方案

ES6

ES6从js语法层面解决了模块化的问题。

ES6模块化可以运行在浏览器和服务端。

ES6模块化详细参考:Module 的语法

导出

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

js
// 报错
export 1;

// 报错
var m = 1;
export m;
// 报错
export 1;

// 报错
var m = 1;
export m;

正确的写法是下面这样。

js
// 写法一
export var m = 1;

// 写法二
var m = 1;
export { m };

// 写法三
var n = 1;
export { n as m };
// 写法一
export var m = 1;

// 写法二
var m = 1;
export { m };

// 写法三
var n = 1;
export { n as m };

同样的,function和class的输出,也必须遵守这样的写法。

js
// 报错
function f() {}
export f;

// 正确
export function f() {};

// 正确
function f() {}
export {f};
// 报错
function f() {}
export f;

// 正确
export function f() {};

// 正确
function f() {}
export {f};

导出别名

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

js
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

导入别名

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

js
import { lastName as surname } from "./profile.js";
import { lastName as surname } from "./profile.js";

模块整体加载

使用 * 代替所有导出:

js
import * as circle from "./circle";

console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));
import * as circle from "./circle";

console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));

export 与 import 的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

js
export { foo, bar } from "my_module";

// 可以简单理解为
import { foo, bar } from "my_module";
export { foo, bar };
export { foo, bar } from "my_module";

// 可以简单理解为
import { foo, bar } from "my_module";
export { foo, bar };

需要注意的是,写成一行以后,foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo和bar。

模块的接口改名和整体输出,也可以采用这种写法。

js
// 接口改名
export { foo as myFoo } from "my_module";

// 整体输出
export * from "my_module";
// 接口改名
export { foo as myFoo } from "my_module";

// 整体输出
export * from "my_module";

默认接口的写法如下。

js
export { default } from "foo";
export { default } from "foo";

具名接口改为默认接口的写法如下。

js
export { es6 as default } from './someModule';

// 等同于
import { es6 } from './someModule';
export default es6;
export { es6 as default } from './someModule';

// 等同于
import { es6 } from './someModule';
export default es6;

同样地,默认接口也可以改名为具名接口。

js
export { default as es6 } from "./someModule";
export { default as es6 } from "./someModule";

ES2020新增了import写法

js
export * as ns from "mod";

// 等同于
import * as ns from "mod";
export { ns };
export * as ns from "mod";

// 等同于
import * as ns from "mod";
export { ns };

import.meta

import.meta只能在模块内部使用,在模块外部使用会报错。

在node环境中使用ES模块化的时候,如果需要获取__dirname,可以像下面这样获取:

js
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));

IIFE 匿名函数自调用

主要是用在浏览器端,利用闭包的特性来保存私有变量,达到模块化的效果

js
(function (args) {
    // do something...
})(args);
(function (args) {
    // do something...
})(args);

CommonJS

参考文章:CommonJS规范

CommonJS规范主要应用于nodejs中,浏览器运行的话,需要借助其他工具(Browserify)

AMD

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。

AMD规范使用define方法定义模块,使用require方法导入模块,在浏览器中导入AMD模块,需要先引入require方法

html
<!-- index.html -->
<body>
    <!-- 引入require.js并指定js主文件的入口 -->
    <script
        data-main="main"
        src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"
    ></script>
</body>
<!-- index.html -->
<body>
    <!-- 引入require.js并指定js主文件的入口 -->
    <script
        data-main="main"
        src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"
    ></script>
</body>

对于AMD模块具体导出和导入的细节就不深究了,大部分情况下用不到。

CMD

CMD是由SeaJS定义的模块化方案,SeaJS是淘宝团队推出的JS框架。这是一个同步模块化规范。

与AMD比较,CMD也是运行在浏览器环境,不过模块导入的时机不同,CMD是在使用的地方用require导入,而AMD是在一开始就将要导入的模块写到require的第一个参数里面,即前置依赖。

UMD

特点是兼容AMD和CommonJS,而且兼容全局引入。

UMD原理:

  • 先判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块;
  • 再判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式;
  • 前两个都不存在,则将模块公开到全局(window 或 global)