起因
起初没有模块化概念之前,编写多个js文件时,必须按照顺序引入所需要的js文件
<script src="jqery.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script>
这样的缺点是
- 全局污染,每个js文件都会向全局作用域外暴露自己的变量和函数。比如:
jqery.js会在全局环境暴露$、jqery函数,a.js也在全局中暴露同样名称的函数,覆盖了jqery.js全局函数,那么b.js就无法在访问到jqery.js暴露的函数 - 加载顺序,因为每个模块的执行存在关联性,并且执行和加载js模块都会阻塞js执行线程,所以js的加载顺序必须固定,比如:
a.js需要访问jqery.js的方法,那么jqery.js一定要在a.js前面加载运行
闭包
在js中,不止全局作用域,还有函数作用域,通过闭包的方式减少向外暴露的变量和方法数量
// a.js
(function (win) {
win.aModule = {
data: [1, 2, 3],
};
})(window);
// b.js
(function (win, aModule) {
win.bModule = {
data: aModule.data.concat(5, 6),
};
})(window, window.aModule);
全局污染得到缓解,但是加载顺序的问题还是无法解决
commonJS
node提供的一套模块化方案,这里需要注意require加载是同步的,也就是在运行到require代码时,才会去加载其他包,并且执行也是同步的,但是这里需要注意每个模块式会被加载一次,以后加载运行都是直接使用缓存
// a.js
module.exports = {
data: [1, 2, 3],
};
// b.js
const { data } = require("./a.js");
module.exports = {
data: data.concat([5, 6]),
};
// index.js
const aModule = require("./a.js");
const bModule = require("./b.js");
console.log(aModule.data); // [1, 2, 3]
console.log(bModule.data); // [1, 2, 3, 5, 6]
解决了全局污染和加载顺序的问题,那commonJS可以使用在浏览器端吗?
显然时不能的,在使用require记载模块时会阻塞整个程序,在node端完全不是问题,这是因为运行在后端,但是在浏览器端肯定时不行的,如果浏览器加载很多模块,那白屏或者浏览器卡死的时间就会非常的长,而且网速还是不可预知的
AMD
AMD规范其实是仿照commonJS来的,上面说过之所以在浏览器不能直接使用commonJS规范,是因为commonJS是同步加载同步运行的,但是在浏览器端实现同步加载时不现实的
那么AMD案件出现了,他提出了异步加载并,已经是先加载好每个模块并且执行回去模块值之后,再去运行业务代码
// a.js
define(function () {
return {
data: [1, 2, 3],
};
});
// b.js
define(["a"], function (aModule) {
return {
data: aModule.data.concat([5, 6]),
};
});
// index.js
require.config({
paths: {
a: "./a",
b: "./b",
},
});
require(["a", "b"], function (aModule, bModule) {
console.log(aModule.data); // [1, 2, 3]
console.log(bModule.data); // [1, 2, 3, 5, 6]
});
<!-- 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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
<script src="index.js"></script>
</body>
</html>
实现一个AMD其实也很简单,就是先根据配置加载模块并且运行(require.config.paths就是预先加载模块的配置),在去执行业务代码的回调函数
CMD
上面在讲AMD提到一个现象,就是AMD在加载模块时,还会先运行先运行模块里面的代码,得到模块值,也就是提前执行、依赖前置
CMD觉得可以提前加载模块,但是不应该提前运行模块代码,而应该在使用到这个模块的时候才运行模块代码,也就是延迟执行、依赖就近
但是这里需要注意,无论是AMD还是CMD,都需要提前加载模块,区别在于什么时候执行模块
在AMD是通过执行查找加载模块,而在CMD因为不会预先执行,就会通过正则匹配的方式加载模块
// a.js
define(function () {
return {
data: [1, 2, 3],
};
});
// b.js
define(function (require) {
const aModule = require("./a.js");
return {
data: aModule.data.concat([5, 6]),
};
});
// index.js
define(function (require) {
const aModule = require("./a.js");
const bModule = require("./b.js");
console.log(aModule.data); // [1, 2, 3]
console.log(bModule.data); // [1, 2, 3, 5, 6]
});
<!-- 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>
<script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.0.3/sea.js"></script>
<script type="text/javascript">
seajs.use('./index.js');
</script>
</body>
</html>
自定义模块
引入路径
导入自定义模块必须使用相对路径,以./、../开头
const name = "李白"
const age = 18
const running = function () {
console.log("我是" + this.name + ",我跑的很快");
}
module.exports = {
name,
age,
running
}
const my = require("./my.js")
console.log(my.name);
my.running()
省略后缀
在导入后缀为 js、json文件时,可以省略省略后缀
const my = require("./my")
const config = require("./config")
console.log(my.name);
my.running()
console.log(config.basePath);
但是在引入在同名的 js 和 json文件时,省略后缀会优先引入 js 文件
在导入 C++ 书写的 node后缀文件也可以省略后缀
导入文件的执行方式
JS文件
正常执行,需要 module.exports 语句导出数据
// my.js
const name = "李白"
const age = 18
const running = function () {
console.log("我是" + this.name + ",我跑的很快");
}
module.exports = {
name,
age,
running
}
const my = require("./my.js")
console.log(my); // { name: '李白', age: 18, running: [Function: running] }
console.log(my.name); // 李白
JSON文件
不需要 module.exports 语句导出数据,并且会自动转为对象
{
"basePath":"./module"
}
const config = require("./config.json")
console.log(config) // { basePath: './module' }
console.log(config.basePath); // ./module
其他后缀或无后缀
当做 JS 文件进行执行,比如 .babel 、.eslintrc
// .babel
module.exports = {
presets: [
["@babel/preset-env"]
]
}
const babel = require("./.babel")
console.log(babel) // { presets: [ [ '@babel/preset-env' ] ] }
导入文件夹模块执行流程
esModuel 和 commonJS 规范导入模块搜索顺序完全一致
引入模块时入口文件查找顺序
如果文件夹没有package.json配置或者package.json没有相对应的入口配置,就会使用当前文件夹index.js或者index.ts
main:一般常用的入口,也是保底入口,如果没有其他入口配置就会根据这个入口的配置,在浏览器和node中都可以使用
modlue:EsModule规范的入口
browser:浏览器环境的入口
- 浏览器环境使用
import:browser>modlue>main> 默认 - node端使用
require:main> 默认
exports
node在16版本之后提出新的的入口配置,会直接覆盖前面的入口配置
{
"main": "index.main.ts",
"module": "index.module.ts",
"browser": "index.browser.ts",
"exports":{
".":{
"require": "./index.module.ts"
}
},
}
比如说,上面的配置,在浏览器使用import导包,exports选项没有import入口配置,虽然有main、module、browser选项,但是都不会采用,会直接使用默认入口
"exports":{
".":{
"require": "./index.module.ts"
},
"./style":{
"import":"./style.import.css",
"require":"./style.require.css"
}
},
exports还可以配置其他目录引入的情况,但需要主要,其他目录引入的规则,必须包在node_modules才有效
简要源码
const path = require("node:path")
const fs = require("node:fs")
const cacheModule = new Map()
function myRequire(filePath) {
// 1. 拼接文件模块绝对路径
const absolutePath = path.resolve(__dirname, filePath)
// 2. 检查缓存是否存在模块结果
if(cacheModule.has(absolutePath)){
return cacheModule.get(absolutePath)
}
// 3. 读取文件内容
const code = fs.readFileSync(absolutePath).toString()
// 4. 包装函数
const wrapper = Function('global', 'require', 'module', '__filename', '__dirname', `
const exports = module.exports;
${code};
return module.exports;
`)
// 5. 执行包装函数
const module = {
exports: {}
}
const global = {}
const result = wrapper(global, myRequire, module, absolutePath, path.dirname(absolutePath))
// 6. 缓存模块结果
cacheModule.set(absolutePath, result)
// 7. 返回模块结果
return result
}
const my = myRequire("./my.js")
esModel
上面介绍的都是民间开发自己研究出来的,并不具有系统性、规范性
所以在Es6时,Es规范提出了esModel规范
esModel和commonJS一样使用同步加载依赖包,和commonJS不同的是返回值一个引用地址
// a.js
const data = [1, 2, 3];
export default {
data,
};
//b.js
import aModule from "./a.js";
const data = aModule.data.concat([4, 5]);
export default {
data,
};
// index.js
import aModule from "./a.js";
import bModule from "./b.js";
console.log(aModule.data); // [1, 2, 3]
console.log(bModule.data); // [1, 2, 3, 5, 6]
<!-- 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>
<script type="module" src="index.js"></script>
</body>
</html>