1 ES6模块化
1.1 ES6基本介绍
ES6 模块是 ECMAScript 2015(ES6)引入的标准模块系统,广泛应用于浏览器环境下的前端开发。Node.js环境主要使用CommonJS规范。ESM使用import和export来实现模块化开发从而解决了以下问题:
- 全局作用域下的变量命名冲突问题,模块化变量不会暴露在全局。
- 解决了依赖管理混乱的问题。
- 模块化提高了代码的可读性和维护性
1.2 导出方法
首先创建Index.html文件,再分别创建user.js和article.js以及index.js,index.js将导入user和article内部的变量和方法,user和articel分别导出需要暴露的函数和变量,在index.html中引入index.js。
user.js文件代码如下所示:
const name = "User 1"
const getData = () => {
const res = "Data from User 1"
return res
}
const getAge = () => {
return 30
}
article.js初始代码如下所示:
const name = "Article 1"
const getData = () => {
const res = "Data from Article 1"
return res
}
const getColunmn = () => {
return 3
}
index.html如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
1.2.1 分别导出
在需要导出的变量前加上export即可导出该变量,user.js如下所示:
export const name = "User 1"
export const getData = () => {
const res = "Data from User 1"
return res
}
1.2.2 按需命名导出
将需要导出的变量放入一个对象中,统一使用export {}导出,article文件代码如下所示。
export {name, getData}
1.2.3 默认导出
使用export default 变量 实现默认导出,注意,一个模块只能有一个默认导出,默认导出的变量将会视为一个整体,在使用imprt * 导出时存放在default属性下,如下所示。
const getAge = () => {
return 30
}
export default getAge
export default getAge = () => {
// return 30
// }
1.3 导入方法
在index.js进行导入前,需要将index.js引入index.html的body中,加入如下语句:
<body>
<script src="index.js" type="module"></script>
</body>
注意:在js文件中使用了import后需要将type="javascript/text"变为type="module",表明以内的是一个js模块,否则报错,此时模块内的变量和方程在浏览器控制台内就无法访问,隔离了变量。
1.3.1 全局导入
使用import * as 别名 from "./user.js"来引入全部导出的变量,如下所示:
import * as user from './user.js';
import * as article from './article.js';
console.log(user)
console.log(article)
在控制台打印结果如下所示:
可以看到分别导出和按需命名导出变量都在该对象上,按需导出的内容存储在对象的default属性上。
1.3.2 命名导入
命名导入使用形如import {name, getData} from "./user.js"来导入,类似解构赋值,如果变量重名可以使用as 别名来解决命名冲突问题,如下所示:
import {name as username, getData as getUserdata} from './user.js';
import {name, getData} from './article.js';
console.log(username)
console.log(getUserdata())
console.log(name)
console.log(getData())
可以看到控台输出如下所示:
1.3.3 默认导入
默认导入不需要解构赋值(形如 import user<-任意名称 from "./user.js"),可以以任意不冲突变量名来导入,如下所示:
import getAge from './user.js';
import anyName from './article.js';
console.log(getAge()) // output: 3
console.log(anyName.getData()) // output: 30
1.3.4 混合导入
混合导入可以同时导入默认导出和命名导出的内容,如下所示:
import user, {name, getData} from './user.js';
console.log(user)
console.log(name)
console.log(getData())
1.4 浏览器环境使用CommonJS规范-使用Browserify
将CommonJS模块文件转换为浏览器可识别的格式有许多方法,常用的工具有:ESbuild、Babel、Browserify等。本文将介绍Browserify,可以将 CommonJS 模块转换为浏览器可用的格式,只需要转换后在浏览器中引入即可,使用流程如下所示:
1. 安装Browserify
使用下面的命令安装Browserify:
npm install --save-dev browserify
2. 准备CommonJS规范的导入和导出文件
修改user.js文件内容如下所示:
const name = "User 1"
const getData = () => {
const res = "Data from User 1"
return res
}
const getAge = () => {
return 30
}
module.exports = {
name,
getData,
getAge
}
修改index.js文件内容如下所示:
const user = require('./user.js');
console.log(user)
3. 使用工具打包编写好的CommonJS模块文件
运行如下命令将index.js打包未可以识别的bundle.js:
npx browserify index.js -o bundle.js
4. 引入打包后的文件bundle.js到html文件当中
<script src="bundle.js" type="module"></script>
即可在控制台中查看输出。
1.5 注意事项
1.5.1 默认导出和命名导出的差异
命名导出:
- 可以在一个模块中导出多个内容。
- 导入时需要使用大括号 {} 并匹配导出的名称。
- 更适合导出多个功能或工具。
默认导出:
- 每个模块只能有一个默认导出。
- 导入时可以使用任意名称,不需要大括号。
- 适合导出一个主要的功能或对象。
1.5.2 模块的静态结构与运行时动态加载运行
在ESM中模块的依赖关系在编译时就已经决定了,模块之间的依赖关系和加载顺序是静态的,带来一下好处:
- 性能优化:构建阶段进行代码分析和优化,预加载、并行加载;
- 提前的错误检测:问题可以在编译阶段就提前发现
- 静态分析:准确的类型检查和自动补全
同时模块也可以通过事件进行动态的添加并执行(一次),在dynamic_code.js文件中添加console.log("content loaded"),然后再script中添加如下代码:
<body>
<button id="btn">Click</button>
<script>
const btn = document.getElementById('btn');
btn.addEventListener('click', async () => {
await import('./dynamic_code.js')
});
</script>
</body>
同时需要注意到import返回的是一个Promise对象,需要使用then或者await来等待加载完成状态。
1.5.3 导出变量的修改变化
我们先来看一个例子,来猜一猜它的输出:
function test(){
let sum = 0
function increment(){
sum ++
}
return {sum, increment}
}
let {sum, increment} = test()
console.log(sum) // 0
increment()
console.log(sum) // 0
解构赋值得到的只是一个拷贝,并不是原始的引用,所以increment增加的是闭包环境内的sum对象,和赋值得到的sum没有关系。但是如果是返回的是引用对象(数组和对象),那么就不同了:
function test(){
const obj = {sum:0}
function increment(){
obj.sum ++
}
return {obj, increment}
}
let {obj, increment} = test()
console.log(obj.sum) // 0
increment()
console.log(obj.sum) // 1
像obj.sum一样在ESM中导出的变量进行修改是会修改原始模块中的变量的值,如果意外修改了模块内的变量,且模块变量被多个其它模块所共用可能造成意外的错误。如下所示是一个ESM中的修改影响原始模块变量的例子:
首先编写count.js文件,导出let声明的sum和方程increment:
let sum = 0
function increment(){
sum ++
}
export {sum, increment}
在index.js中导入sum和increment函数,在网页控制台中查看输出:
import { sum, increment } from "./count";
console.log(sum); // 0
increment();
increment();
increment();
console.log(sum); // 3
可以看到输出0和3,对导出变量的修改会影响模块内的原始值,所以为了让模块内的变量不被修改,需要将导出的变量使用const 声明,如下所示。:
const sum = 0
const increment = () =>{
sum ++
}
export {sum, increment}
2 CommonJS模块
2.1 CommonJS基本介绍
CommonJS 是 Node.js 最初采用的模块系统,基于 require 和 module.exports以及exports 实现动态模块加载。尽管ESM才是官方的模块化标准,但是CommonJS是现在官方认可且Node.js环境中广泛使用的模块化规范。
2.2 导出导出方法
在CommonJS中导出的内容相当于挂载在一个对象上,exports.属性名相当于在对象上添加属性,module.exports相当于修改整个导出的对象。
2.2.1 exports简化导出-require导入
分别编写index.js和user.js及运行的输出如下所示,使用exports.变量名挂载到导出对象上,require( 'filepath')获得导入对象:
// index.js
const user = require('./user');
console.log(user); // output: { name: 'User 1', age: 30, getData: [Function (anonymous)] }
// user.js
exports.name = "User 1"
exports.age = 30
exports.getData = () => {
const res = "Data from User 1"
return res
}
导入的时候可以使用{ }进行赋值的解构,如果有重名使用ES6的重命名语法,如下所示:
const {name:username} = require('./user');
console.log(username);
2.2.2 module.exports整体导出
使用module.exports整体导出对象,导入方式不变,如下所示:
// user.js
const name = "User 1"
const age = 30
const getData = () => {
const res = "Data from User 1"
return res
}
module.exports = {
username: name,
age,
getData
}
2.2.3 混合导出存在的问题
首先不能使用exports = {}来进行导出,如果混合使用exports.变量名和module.exports = {}来进行导出,会以最后的module.exports值进行确认,如下所示:
const name = "User 1"
const age = 30
const getData = () => {
const res = "Data from User 1"
return res
}
exports.a = 1
module.exports = {
username: name,
age,
getData
}
exports.b = 4
module.exports = {
username: "Usernawm",
age,
getData,
b:32
}
exports.c = 3
导出的对象如下所示:
{ username: 'Usernawm', age: 30, getData: [Function: getData], b: 32 }
2.3 实现原理
CommonJS的原理实际是将每一个模块封装在一个函数中,module、exports和require都是函数传递的参数,我们使用console.log(arguments.callee.toString())来查看,输出内容如下。
function (exports, require, module, __filename, __dirname) {
exports.name = "User 1"
exports.age = 30
exports.getData = () => {
const res = "Data from User 1"
return res
}
console.log(arguments.callee.toString())
}
2.4 Node.js环境中的ESM和CommonJS中的兼容问题
1. 包配置修改-type字段
通过 package.json 中的 "type" 字段也可以指定整个包的模块类型,"module" 指的就是ESM。
// package.json
{
"type": "module"
}
2. 文件扩展名设置
Node.js 使用文件扩展名(.mjs 表示 ESM,.cjs 表示 CommonJS)来区分模块类型。
3. 默认导出和命名导出的处理
在 CommonJS 中,module.exports 可以是任何类型(对象、函数、类等),而 ESM 的默认导出是一个单一的值。 当从 CommonJS 模块导入到 ESM 时,整个 module.exports 对象被视为默认导出。
// CommonJS 模块
module.exports = {
add: (a, b) => a + b
};
// ESM 导入
import math from './math.js';
console.log(math.add(2, 3)); // 正常工作
反之,从 ESM 导入到 CommonJS 时,需要通过 default 属性访问默认导出。
// ESM 模块
export default function log(message) {
console.log(message);
}
// CommonJS 导入
const log = require('./logger.mjs').default;
log('Hello, Mixed Modules!');