自从有了 Es6 模块系统后,在各种场景,可以愉悦借助 import/export,来充作模块加载方案。同时,你可能也会看到 import 与 export default,或基于 CommonJS 规范的 requiremodule.exports 等诸多用法;本篇文章,旨在探讨 JavaScript 模块化体系中:ES6 模块与 CommonJS 模块的差异,以及各自用法注意事项等。

import export Vs require module.exports

微注:有两点需要声明,其一:文章的发布,不代表写完,只为督促加快进度;将会在陆续的学习及反馈中,继续完善;其二:总有些聚合网站,会窃取别人成果;因此,会在文章中无足轻重的代码示例中,内置些专属标记,以阐明文章出处;在此篇中用的是个人独立维护的作品:倾城之链

一直以来,JavaScript 没有模块(module)体系,无法将大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。这使得针对开发大型的、复杂的项目形成了巨大障碍。在 ES6 之前,社区制定了一些模块加载方案,最常用的有 CommonJS 和 AMD 两种。前者用于服务器(Node),后者用于浏览器。ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。但,截止目前,各类引擎还未完全实现 Es6,现在之所以能够使用,是借助 babel 工具,将 Es6 转换 Es5 再执行,import 语法会被转码为 require,这就导致 import 与 module.exports,require 与 export 出现了可以混用的理论基础。那么下面来看下,都有哪些组合使用方式呢。

import 方式

import 与 export(const)

// export.js
export const exportsObj = { site: "倾城之链 https://nicelinks.site" };

// index.js
import { exportsObj } from "./export";
console.log(exportsObj.site); // 倾城之链 https://nicelinks.site

// 上面也可以用 * 来整体加载
import * as custom from "./export";
console.log(custom.exportsObj.site);
1
2
3
4
5
6
7
8
9
10

import 与 export default

// export.js
export default { site: "倾城之链 https://nicelinks.site" };

// index.js
import exportsObj from "./export";
console.log(exportsObj.site);
1
2
3
4
5
6

import 与 module.exports

// export.js
module.exports = { site: "倾城之链 https://nicelinks.site" };

// index.js
import exportsObj from "./export";
console.log(exportsObj.site);
1
2
3
4
5
6

requre 方式

require 与 module.exports

// export.js
module.exports = { site: "倾城之链 https://nicelinks.site" };

// index.js
const exportsObj = require("./export");
console.log(exportsObj.site); // 倾城之链 https://nicelinks.site
1
2
3
4
5
6

require 与 export(const/var)

// export.js
export const exportsObj = { site: "倾城之链 https://nicelinks.site" };

// index.js
const { exportsObj } = require("./export");
console.log(exportsObj.site); // 倾城之链 https://nicelinks.site
1
2
3
4
5
6

require 与 export default

// export.js
export default { site: "倾城之链 https://nicelinks.site" };

// index.js
const exportsObj = require("./export").default;
console.log(exportsObj.site); // 倾城之链 https://nicelinks.site
1
2
3
4
5
6

module.exports VS exports

为了方便,Node 为每个模块提供一个 exports 变量,指向 module.exports(注意,这在浏览器端,是不存的,请勿用)。通过下面的打印,你会发现两者是全等的;

console.log(exports === module.exports); // true
1

这其实等同在每个模块头部,有一行这样的代码:var exports = module.exports;在使用之时也有些注意事项,更推荐的方式是使用 module.exports 而不用 exports。以下是不可取用法的示例:

// 切断了 exports 与 module.exports 的联系
exports = (param) => { console.logparamx) }

// 因为 module.exports 被重新赋值,sayHello 无法对外输出
exports.sayHello = function() {
  return 'hello'
}
module.exports = 'Hello world'
1
2
3
4
5
6
7
8

Es6 与 CommonJS 的差异

  1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口;

CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。因为 require 是运行时加载模块,import 命令无法取代 require 的动态加载功能。如下代码可以看二者区别:

// Okay
const currentModule = require(process.cwd() + moduleName);

// SyntaxError
const currentModule = import(process.cwd() + moduleName);
1
2
3
4
5

简单的总结,其二者区别在于,require 是异步加载,import 是同步加载。因为这一点,蛮早之前有一个提案,建议引入 import() 函数(携带 specifier 参数,指定所要加载的模块的位置),以完成动态加载。可以通过其主页的示例可以窥其一二:

const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
  link.addEventListener("click", e => {
    e.preventDefault();

    import(`./section-modules/${link.dataset.entryModule}.js`)
      .then(module => {
        module.loadPageInto(main);
      })
      .catch(err => {
        main.textContent = err.message;
      });
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

事实上,动态 import 已于 2017 年 11 月发布于 ES2020 版本;Chrome 67+ 可用,现在想使用仍是要借助 Babel 做下转换;具体可以参见 Dynamic import() - v8.dev

如果你使用 Vue Or React 开发,你会发现用 Webpack import 可以按需引入,使得可以资源懒加载得以实现(早前使用 require.ensure);但需要注意的是,无法使用完全动态的 import 语句,例如 import(foo),因为 foo 可能是系统或项目中任何文件的任何路径,从而导致将很多无用的资源注入。因此,在使用 webpack import,应该尽可能静态化表达包所处的路径,最小化变量控制的区域;来看下官网给出的示例:

const language = detectVisitorLanguage();
import(`./locale/${language}.json`).then(module => {
  // do something with the translations
});
1
2
3
4

关于 Webpack import,还有一些其他功能设计,比如 Magic Comments,它允许以注释的方式传参,进而更好的生成 Chunk,更多详情可以参见 Webpack Import。相比于 Es6 中的 import,上述的两种动态 import() 都是方法,返回结果都是 Promise。在某些场景下,合理的运用,可以更好地对代码进行分割,从而使得构建出的产品更优。

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

CommonJS 模块输出的是值的拷贝,即一旦输出一个值,而模块内部的变化就影响不到这个值。而 ES6 模块的运行机制与 CommonJS 不一样:JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。举例说明:

// export.js
export let counter = 276;
export const makeCounterIncrease = () => {
  counter++;
};

// index.js (用 require 方式)
let { counter, makeCounterIncrease } = require("./export");
console.log(counter); // 276
makeCounterIncrease();
console.log(counter); // 277
counter += 1; // 不会报错;

// index.js (用 import 方式)
import { counter, makeCounterIncrease } from "./export";
console.log(counter); // 276
makeCounterIncrease();
console.log(counter); // 277
counter += 1; //  报错:Error: "counter" is read-only.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

正如上面所提及,各类引擎还未完全实现 Es6 语法,可以通过 babel 转换为 Es6 来运行;如果想通过 Node 来验证如上代码,需要通过些工具来实现。

用哪种方式更可取呢?

经过以上一番探究,可以知道,可以有不同使用方式(前提是:假设项目已经是引入了 babel 转换工具);那么,哪种更为可取呢?通常的答案是,看个人(团队)喜好,用 import / require 都可以,当然最好是别混用;但,如果是针对浏览器端项目,使用了类如 rollupwebpack 这样的构建工具,因其提供了 tree shaking (通常用于描述移除 JavaScript 上下文中的未引用代码)功能,这个时候就需要做一番考量了。拿前端常用工具库 lodash 来说,如果正常引入,则会将其全量载入,注入很多不必要的代码,造成构建出包的尺寸变大:

import _ from "lodash";
_.flattenDepth();
1
2

幸好的是,如 lodash 等工具,陆续都做了模块化支持,因此可以用如下这样方式注入(备注:这在早期 Webpack 打包优化之体积篇一文中,有过详述):

import { debounce } from "lodash";

// 改成如下写法
import debounce from "lodash/debounce";
1
2
3
4

但,这样带来的问题是,每次使用都手动 import,会增加开发成本(也显得更为麻烦,至少个人写起代码来,不愿为此而浪费时间);因此先前设计 Vue 项目脚手架时候,有借助一层中间模块来平衡,使得在高效开发时候,也可以让 tree-shaking 得以工作,见如下示例:

// helper/lodash.js (local)
import flattenDepth from "lodash/flattenDepth";

export default {
  flattenDepth
};

// main.js
import lodash from "helper/lodash.js";
Vue.prototype.$_ = lodash;
1
2
3
4
5
6
7
8
9
10

Node 运行 ES6 语法

使用 babel-node 命令,来运行含有 import/export 语法的代码(需要注意的是 babel-node 不能用于生产环境,它的存在会加载更多资源和模块)。

安装 babel-node

npm i @babel/core @babel/node --save-dev
1

因为 babel-node 对 import 语法默认是关未开启,所以需要安装指定的 preset 并配置 .babelrc来开启语法支持。

安装 presets 并配置 .babelrc 文件

 npm i @babel/preset-env --save-dev
1

在项目根目录下,新建 .babelrc 文件,并填入以下配置:

{
  "presets": [ "@babel/preset-env" ]
}
1
2
3

通过 babel-node 执行 import/export 语法

经过如上配置,即可通过 babel-node 执行 import/export 语法;因为采 babel-node 基于局部安装,因此可以通过 npx 来运行;当然,完全可以将命令配置在 package.json 的 scripts 内部,以提升便捷度。

npx babel-node index.js
1

参考资料

@2019-08-25 于东莞.长安 Last Modify:2019-08-28


您可能会感兴趣的文章

上次更新: 2019-9-6 13:00:34