前言
之前对于模块化开发了解的不多,但是在项目中又经常用到,每次使用module.exports和exports的时候总是搞不明白,但是经常看同事写这块写的可顺手了,所以就下定决心把这块原理搞明白
前端为什么需要模块化开发
模块化开发的好处有很多例如解决命名冲突、提高可复用性、提高代码可维护性等等,但是这里我想说其中的两点解决全局污染和依赖管理,下面上代码 首先在a.js中
function show(){
name = '我是a.js中的name'
}
那么在index.html
<script src="./a.js"></script>
<body>
<script>
name = "我是index.js中定义的name"
show();
console.log(name);
</script>
</body>
那么在控制台打印出的name是
我是a.js中定义的name 这只是其中的一个小例子,那么如果好多人开发不同的js,在引用的时候肯定会出现很多问题
那么还有我一个问题就是依赖管理,就是在script标签中引入的js是按排列的顺序执行的,那么排列在下面的js去调用已经执行完的js中的方法是无法调用的 那么通过以上例子我们就说一下前端两个模块化的方案 Commonjs和Es Module
Commonjs
特点
-
在commonsjs中每一个js文件都是一个单独的模块我们可以称之为module
-
该模块中,包含Commonjs规范的核心变量:exports、module.exports、require,在nodejs中还存在_filename和_dirname变量
-
exports和module.exports可以负责对模块中的内容进行导出
-
require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方模块库)中的内容
前面我们提到每个模块中都包含三个变量,三个变量的意思分别是
- module记录当前模块信息
- require引入模块的方法
- exports当前模块导出的属性
在编译过程中实际上commonjs对js中的代码进行了首位包装 例如在一个main.js中引入了index.js中的方法hello
(function(exports, require,module,_filename,_dirname){
const hello = require('./index.js')
module.exports = function say(){
return {
talk: hello(),
name: '前端开发'
}
}
}
)
我们称外面的函数为包装函数,那么也就是说在编译的时候exports,require,module其实是通过形参的方式传递到包装函数中的 那么包装函数又是什么样呢
function wrapper (script) {
return '(function (exports, require,module,_filename,_dirname){' +
script +
'\n})'
}
const modulefuntion = wrapper('
const hello = require('./index.js')
module.exports = function say(){
return {
talk: hello(),
name: '前端开发'
}
}
')
那么script就是我们在main.js模块中写的内容
runInThisContext(modulefunction)(module.exports, require,module,_filename,_dirname)
在模块加载的时候,会通过runInThisContext执行modulefunction,传入export,require, module等参数,这就是我们所说的加载流程
require的加载原理
我们先说两个概念module和Module module: 在node中每一个js文件都是一个module,module上保存了exports等信息之外,还有一个loaded表示该模块是否被已经被加载
- 为false表示还没有加载
- 为true 表示已经加载 Module: 以nodejs为例,整个系统运行之后,会用Module缓存每一个模块加载的信息 我们看一下require源码
// id 为路径标识符
function require(id) {
// 查找Module 上有没有已经加载的js对象
const cachedModule = Module._cache[id]
// 如果已经加载了那么直接取走缓存的exports对象
if(cachedModule) {
return cachedModule.exports
}
// 创建当前模块 module
const module = { exports: {}, loaded: false, ...}
//将module缓存到module 的缓存属性中,路径标识符作为id
Module._cache[id] = module
// 加载文件
runInthisContext(wrapper('module.exports = "123"'))(module.exports, require,module,_filename,_dirname)
// 加载完成
module.loaded = true
//返回值
return module.exports
}
那么通过上述require大概的源码我们可以总结出几点
- require 会接受一个参数 -文件标识符,然后分析定位,接下来会从Module上查找有没有缓存,如果有缓存,那么直接返回缓存的内容
- 如果没有缓存,会创建一个module对象,缓存到Module上,然后执行文件,加载完文件,将loaded属性设置为true,然后返回module.exports对象
- exports和module.exports持有相同的引用,因为最后导出的是module.exports,所以对exports进行赋值会导致exports操作的不再是module.exports的引用
exports和module.exports
从上述require原理实现中,我们知道了exports和module.exports持有相同的引用,因为最后导出的是module.exports
ES Module
导出export和导入import 所有通过export导出的属性,在import中可以通过解构的方式解构出来 默认导出export default export default anything 导入module的默认导出,anything可以是函数,属性方法,或者对象 那么具体esmodule需要注意的点我这里就不说了,比较基础