我正在参加「掘金·启航计划」
前言
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引用模块,通过exports和module.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:exports和module.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')