前端模块化:CommonJS vs ES6 Module

692 阅读3分钟

我正在参加「掘金·启航计划」

前言

Javascipt是在1995年由 Netscape 公司的Brendan Eich在 Netscape 浏览器上首次设计实现的。JS 最初并没有模块系统,这不利于开发大型的、复杂的前端项目。随着时间的推移,社区中催生出 CommonJS、AMD 等模块加载方案。再到后来,ES6 在语言标准的层面上实现了模块功能,旨在取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

笔者之前对于前端模块化仅有大致的了解,没有去系统化地梳理,平时大多是熟悉下 exports, module.exports, import, export 等命令的用法。因此,本文比较了前端模块化中的 CommonJS 和 ES6 Module 规范,用于学习和加深理解~

前端模块化的好处

前端模块化,好处不外乎以下几点:

  • 避免命名冲突,减少全局变量污染
  • 更好地分离代码并实现按需加载
  • 高复用性
  • 高可维护性

CommonJS

Node.js 是 CommonJS 规范的践行者。从v12.0.0开始,Node.js 也支持 ES modules规范,是通过在package.json中添加type: "module"实现的。

在 Node.js 中,每个文件被当作一个单独的模块,通过require引用模块,通过exportsmodule.exports导出模块中的变量。

举个例子:

// index.js
const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);

// circle.js:
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;

问题1:exportsmodule.export有什么区别呢?

exports指向module.exports,二者是等价的。但是当模块整体导出时,必须使用后者,且这会切断二者之间的联系。

module.exports = class Square {
  constructor(width) {
    this.width = width;
  }
  area() {
    return this.width ** 2;
  }
};
console.log(exports === module.exports); // false

ES6 Module

ES6 模块通过export命令显式地导出模块中的变量,通过import命令导入模块。

ES6 模块是编译时加载(静态加载),使得静态分析、tree-shaking 等成为了可能。此外。ES6 模块会自动采用 严格模式

举个例子:

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1994;

// var firstName = 'Michael';
// var lastName = 'Jackson';
// var year = 1994;
// export { firstName, lastName, year };

export default function () {
  console.log('foo');
}


// index.js
import fn, { firstName, lastName as last } from './profile.js'
// console.log('lastName: ' + lastName) // ReferenceError: lastName is not defined

import * as profile from './profile.js';
console.log(profile)

需要注意的是,使用as重命名后,原来的变量名 lastName 变成了 undefined 。

import()

动态 import()函数,支持动态加载模块,不需要依赖 type="module" 的 script 标签。

当我们希望按照一定的条件或者按需加载模块的时候,动态 import() 是非常有用的。

// 按需加载
button.addEventListener('click', event => {
  import('./xxx.js')
  .then(() => {
  })
  .catch(error => {
  })
});

// 条件加载
if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

// 解构赋值
import('./myModule.js')
.then(({export1, export2}) => {
});

import.meta

import.meta是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的 URL。

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

<script type="module">
import './index.mjs?someURLInfo=5';
</script>
// index.mjs
new URL(import.meta.url).searchParams.get('someURLInfo'); // 5

在ES6模块中,我们可以通过const require = createRequire(import.meta.url)自定义 require 函数,用于加载 Nodejs 模块:

import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')

参考