前端模块化

首先为什么需要模块化?模块化解决了什么问题?

其实在其他语言中,模块往往是语言标准的一部分。模块最基本的一个作用就是隔离命名空间,避免出现命名冲突。

所以在js非模块化开发的时候变量命名特别需要注意。所以js的模块化发展是必然的。

那前端的模块化有哪些规范呢?

前端的模块化一直在发展,历史中形成规范其实挺多的。

一个是CommonJS规范,也就是cjs,用于写nodejs。

一个是ESM规范,我们前端的项目现在都是用这个。

一个是UMD规范,一般我们给别人用npm包的时候要按照这个规范打包,它其实是兼容AMD和CommonJS。

还要一个是iife,一般是前端应用打包时候用的,打出来的js立即执行。

CommonJS和ES Module的区别

模块的静态加载(依赖提前加载)和动态加载(依赖适时加载)

ESM的模块写法是在文件的头部把依赖全部import进来,显然ESM属于静态加载,也就是依赖提前加载好。

CommonJS我们可以在代码任何位置使用requie加载依赖,显然属于动态加载,也就是依赖适时加载。

// a.mjs
console.log("execute a.mjs")
import b from "./b.mjs"
// b.mjs
console.log("execute b.mjs")
export default "B"

输出
execute b.mjs
execute a.mjs
由于a依赖b,所以b会提前加载执行,然后才执行a的代码

// a.js
console.log("execute a.js")
var b = require("./b.js")
// b.js
console.log("execute b.js")
module.exports = "B"

输出
execute a.js
execute b.js
commonjs属于适时加载,先执行a,然后碰到require(b),再加载b执行

循环依赖问题

无论什么模块化规范,都逃不过循环依赖的问题。因为依赖加载的原理不同,所以ESM和CommonJS在循环依赖的表现上也会有所不同。

我们先来看看CommonJS的代码

// a.js
const b = require("./b.js")
module.exports = function(){
  console.log("in a.js function")
}
b();
// b.js
const a = require("./a.js")
module.exports = function () {
    console.log("in b.js function")
}
a()

// 运行 node a.js 会报错,a() is not a function
首先加载a.js,遇到require(b),这时候去加载b.js, b里遇到require(a),这时候去加载a, 但是由于a里面exports还没执行,所以a是找不到的,所以就会报错a不是一个函数。 但是相同的代码在ESM里却可以执行,因为ESM是依赖提前加载。

// a.mjs
import b from "./b.mjs"
export default function () {
    console.log("in a.mjs function")
}
b()
// b.mjs
import a from "./A.mjs"
a()
export default function () {
    console.log("in b.mjs function")
}

运行 node a.mjs,输出
in a.mjs function
in b.mjs function

首先扫描a.mjs文件中import/export语句,确定a.mjs导出一个函数,并且import语句依赖于b.mjs 然后再扫描b.mjs,确定b.mjs导出一个函数,并且import语句依赖a.mjs,由于a.mjs已经处理过了,所以可以直接加载,整个依赖关系就加载好了。

ES Module可以做treeShaking

esm模块规范可以做treeShaking,commonjs则不行