CommonJS规范
1.什么是模块化
在ECMScript2015之前 Node只支持 CommonJS的规范 ,在ECMScript2016+ Node开始支持ESM规范
- Node中对CommonJS进行了支持和实现,让我们在开发Node的过程中可以方便进行模块化开发
- 在Node中每个js文件都一个单独的模块
- 这个模块中包括CommonJS规范的核心变量:
exportsmoudule.exportsrequire
- 模块的核心就是导入导出
exports和modules.exports可以负责对模块中的内容进行导出- require 函数可以帮助我们导入其它模块(
Node内置模块,自定义模块,第三方模块)中的内容
2. 模块化的使用
exports和require的使用
- 在
bar.js文件中
const name = 'zs'
const age = '10'
function sayHello(name){
console.log(`hello ${name}`)
}
exports.name = name
exports.age = age
exports.sayHello = sayHello
- 在
index.js文件中
const bar = require('./bar.js')
console.log(bar) // {name:'zs',age:10,sayHello:Function}
- 上面完成了什么样的操作呢?
- 意味着index.js中的bar变量等于exports对象
- 也就是require 各种查找方式,最终找到exports这个对象
- 并且将这个exports对象赋值给bar这个变量
- bar就是exports对象
module.expots的使用
-
有了exports为啥还有module.exports?它俩到底是啥关系?
-
我们去查看CommonJS规范能找到我们想要答案
- 在Node中真正导出其实不是exports而是module.exports
-
但是为什么,exports也可以导出呢?
-
其实就是类似于这样
const module = { exports:{} } let exports = module.exports- exports其实是module.exports的引用
-
-
还是在刚刚的
bar.js文件中const name = 'zs' const age = '10' function sayHello(name){ console.log(`hello ${name}`) } // 我们其实可以这样 module.expotrs = { name, age, sayHello } -
在
index.js文件中const bar = require('./bar.js') console.log(bar) // {name:'zs',age:10,sayHello:Function}
内部执行原理
修改了exports导出的属性会发生什么?
-
在
bar.js文件中const info = { name: "张三", age: 18, foo: function() { console.log("foo函数~") } } setTimeout(() => { info.name = '李四' console.log(info.name) // 这里修改了 info对象里面name属性 },1000) module.exports = info -
在
index.js文件中
const {info} = require('./bar.js')
console.log(info) // {name:'李四',age:18,foo:Function}
bar.js修改了 info对象里面name属性, 那么 index.js文件中的 info对象name属性的值也会修改,因为他俩的内存地址指向是同一个
在index.js修改了exports 导出属性 会发生什么?
-
在
bar.js文件中const info = { name: "张三", age: 18, foo: function() { console.log("foo函数~") } } setTimeout(() => { console.log(info.name) // 此时打印什么呢? }, 1500); module.exports = info -
在
index.js文件中
const {info} = require('./bar.js')
setTimeout(() => {
info.name = '赵六';
}, 1000);
index.js文件修改了 info对象里面name属性, 那么 bar.js文件中的 info对象name属性的值也会修改,因为他俩的内存地址指向是同一个
修改module.exports引用,那么exports还有用吗?
-
在
bar.js文件中const name = "why" const age = 18 function sum(num1, num2) { return num1 + num2 } exports.name = name exports.age = age exports.sum = sum // 此时我修改 module.exports的引用 重新赋值 module.exports = {} -
在
index.js文件中const bar = require('./bar.js') console.log('bar',bar) // 会打印什么呢? -
结论是会输出
{},为什么呢?-
因为在CommonJS规范中 exports是module.exports的引用 当 module.exports没有引用exports时
exports导出的东西将会失效,只会导出module.exports引用
-
-
我们可以在
index.js文件中测试,在没修改exports和module.exports引用时console.log( exports=== module.exports) // true -
在Node实现CommonJS导出源码中
module.exports = {} // 创建一个对象 exports = module.exports // 赋值给exports exports=== module.exports // true 此时exports是全等于module.exports的
3.require细节
require查找规则
require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象
那么require查找规则是什么呢?比如我写了一个require(x)
情况一:Node的内置(核心)模块
const path = require("path")
const fs = require("fs")
// 第三方包
const axios = require('axios')
path.resolve()
path.extname()
fs.readFile()
- 直接返回内置模块,并且停止查找
情况二:自定义模块 路径 ./ 或 ../ 或 /(根目录)开头的
- 第一步将
x当做一个 文件去查找- 如果有后缀名,就按照后缀名去查找
- 如果没有后追梦,会按照以下顺序查找
- 直接查找文件
x - 查找
x.js文件 - 查找
x.json文件 - 查找
x.Node文件
- 直接查找文件
- 第二步没有找到对应文件,将
x当做目录- 查找目录下面的
index文件- 查找
x/index.js文件 - 查找
x/index.json文件 - 查找
x/index.Node文件
- 查找
- 查找目录下面的
- 如果没有找到,就直接报错:not found
情况三:下载的Node_modules第三方包
-
第三方包查找规则如下:
-
首先Node会检查模块名是不是Node里面的核心模块(例如:http,fs)
-
如果不是一个核心模块 查找Node_modules目录:
-
Node会在当前目录的
Node_modules目录查找 -
如果当面目录没有找到,会在父目录
Node_modules继续查找 -
如果父目录没有找到,会在父目录的父目录的
Node_modules继续查找 -
如果父级顶层目录的
Node_modules没有 就会去全局Node_modules进行查找 -
如果全局也没有 就直接报错:not found
-
在Node中打印下面代码 就是Node查找规则
console.log(module.paths);
-
-
require加载过程
-
模块在被第一次引入时,模块中的js代码会被运行一次
-
模块被多次引入时,最终只加载(运行)一次
-
为什么只会加载一次呢?
- 这是因为每个模块的对象module 都有一个属性:loaded
- loaded:false就是没被加载,loaded:true就是被加载出来 会缓存,
- loaded:true 的时候当我们去引入文件的时候 不会在运行
-
在
bar.jsconst name = '张三' cosnt age = 18 console.log('bar.js中代码执行') module.exports = { name, age } -
在
index.jsconsole.log('start,index.js') require('./bar.js') require('./bar.js') require('./bar.js') console.log('end,index.js') //最终输出结果 start,index.js bar.js中代码执行 end,index.js
-
-
如果模块中出现了循环引用怎么办呢?加载顺序是什么呢?
-
这种结构其实是图结构
-
图结构有两种遍历过程:
深度优先搜索和广度优先搜索 -
Node 采用的遍历算法是:
深度优先搜索main>aaa>ccc>ddd>eee>bbb在 main.js文件中 console.log("main") require("./aaa") require("./bbb") ------------- 在 aaa.js 文件中 console.log("aaa") require("./ccc") ------------- 在 bbb.js 文件中 console.log("bbb") require("./ccc") require("./eee") ------------- 在 ccc.js 文件中 console.log("ccc") require("./ddd") ------------- 在 ddd.js 文件中 console.log("ddd") require("./eee") ------------- 在 eee.js 文件中 console.log("eee") // 最终代码执行结果 main aaa ccc ddd eee bbb // bbb文件执行完成只会又去引入ccc和eee文件, // 但是ccc和eee文件被执行过一次多次require是无效的, // 所以执行到bbb就没有打印结果了
-
-
4. CommonJS规范的缺点
-
CommonJS加载模块是同步的
-
同步意味着只有等模块加载完毕 才能执行后面代码,如果当前模块非常耗时会有问题
-
不能用于浏览器
-
浏览器加载js是从服务器将文件下载下来,之后加载运行
-
采用同步意味着后续js代码无法正常运行,比如 DOM操作 AJAX请求 都会进行阻塞
-
当然现在webpack,vite搭建的项目使用CommonJS是另外一回事
-
在ECMAScript2015之前浏览器的模块化是AMD和CMD, 在ECMScript2015开始支持ESM规范
-
目前浏览器和Node已经支持ESM规范,另一方面借助于webpack、vite等工具可以实现
CommonJS与ESM代码的转换