【模块化系列】Nodejs模块化的原理
一、前言
node的应用是模块组成的,Node遵循commonjs的模块规范,用来隔离每个模块的作用域,使每一个模块在自身的命名空间中执行。
commonjs的主要内容:
模块必须通过module.exports导出对外的变量或接口,通过require()来导入其他模块的输出到当前模块作用域中。
commonjs模块特点:
1、所有代码运行在当前模块作用域中,不会污染全局作用域。
2、模块同步加载,根据代码的顺序加载。
3、模块可以多次加载,只会在第一次加载时运行一次,然后运行结果会被缓存,以后再加载,就直接从缓存中读取结果,如若想要模块再次运行,必须清除缓存。
我们先来 看一下简单的例子
编写一个demo-exports.js
let name = 'saucxs';
let getName = function (name) {
console.log(name)
};
module.exports = {
name: name,
getName: getName
}
我们再来编写一个demo-require.js
let person = require('./demo-export')
console.log(person, '-------------') // { name: 'saucxs', getName: [Function: getName] }
console.log(person.name, '===========') // saucxs
person.getName('gmw'); // gmw
person.name = 'updateName'
console.log(person, '22222222') // { name: 'updateName', getName: [Function: getName] }
console.log(person.name, '3333333') // updateName
person.getName('gmw') // gmw
node demp-require.js,结果如上图所示。
二、module对象
commonjs规范,每一个文件就是一个模块,每一个模块中都有一个module对象,这个对象就指向当前模块。module对象具有如下属性:
(1)id:当前模块id。
(2)exports:表示当前模块暴露给外部的值。
(3)parent:是一个对象,表示调用当前模块的模块。
(4)children:是一个对象,表示当前模块调用的模块。
(5)filename:模块的绝对路径
(6)paths:从当前模块开始查找node_modules目录,然后依次进入到父目录,查找父目录下的node_modules目录;依次迭代,直到根目录下的node_modules目录。
(7)loaded:一个布尔值,表示当前模块是否被完全加载。
我们来看一下栗子
module.js
module.exports = {
name: 'saucxs',
getName: function (name) {
console.log(name)
}
}
console.log(module)
node module.js
1、module.exports
我们知道了module对象有一个exports属性,该属性用来对外暴露变量,方法或者整个模块。当其他文件需要require该模块的时候,实际上读取的是module对象中的exports属性。
2、exports对象
既然都有了module.exports就能满足所有的需求,为啥还有一个exports对象呢?
我们现在来看一下两者的关系:
(1)exports对象和module.exports都是引用类型变量,指向同一个内存地址,在node中,两者一开始都是指向一个空对象的。
exports = module.exports = {}
(2)其次,exports对象是通过形参的方式传入,直接赋值给形参的引用,但是并不能改变作用域外的值。
var module = {
exports: {}
};
var exports = module.exports;
function change(exports) {
/*为形参exports添加属性name,会同步到外部的module.exports对象*/
exports.name = 'saucxs'
/*在这里修改wxports的引用,并不会影响到module.exports*/
exports = {
age: 18
}
console.log(exports) // {age: 18}
}
change(exports);
console.log(module.exports); // {exports: {name: 'saucxs'}}
分析上述代码:
直接给exports赋值,会改变当前模块内部的形参exports的对象应用。说明当前的exports对象已经跟外部的module.exports对象没有任何关系,所以改变exports对象不会影响到module.exports。
注意:module.exports就是为了解决上述exports直接赋值的问题,会导致抛出不成功的问题而产生的。
//这些操作都是合法的
exports.name = 'saucxs';
exports.getName = function(){
console.log('saucxs')
};
//相当于下面的方式
module.exports = {
name: 'saucxs',
getName: function(){
console.log('saucxs')
}
}
//或者更常规的写法
let name = 'saucxs';
let getName = function(){
console.log('saucxs')
}
module.exports = {
name: name,
getName: getName
}
这样就可以不用每次都把要抛出对象或者方法直接赋值给exports属性,直接采用对象字面量的方式更加方便。
三、require方法
require是模块的引入规则,通过exports或者module.exports抛出一个模块,通过require方法传入模块标识符,然后node根据一定的规则引入该模块,我们就可以使用模块中定义的方法和属性。
(一)node中引入模块的机制
1、在node中引入模块,需要经历3个步骤:
(1)路径分析
(2)文件定位
(3)编译执行
2、在node中,模块分为两种:
(1)node提供的模块,例如http模块,fs模块等,称为核心模块。核心模块在node源代码编译过程中就有编译了二进制文件,在node进程启动的时候,部分核心模块就直接加载进内存中,因此这部分模块是不用经历上述的(2),(3)步骤,而且在路径分析中优先判断,因此加载速度是最快的。
(2)用户自己编写的模块,称为文件模块。文件模块是需要按需加载的,需要经历上述的三个步骤,速度较慢。
3、优先从缓存中加载
浏览器会缓存静态脚本文件以提高页面性能一样,Node对引入过的模块也会进行缓存。与浏览器不同的是:Node缓存的是编译执行之后的对象而不是静态文件。我们举个例子看一下
requireA.js
console.log('模块requireA开始加载...')
exports = function() {
console.log('Hi')
}
console.log('模块requireA加载完毕')
init.js
var mod1 = require('./requireA')
var mod2 = require('./requireA')
console.log(mod1 === mod2)
执行node init.js
虽然我们两次引入requireA这个模块,但是模块中的代码其实只执行了一遍。并且mod1和mod2指向了同一个模块。
4、module._load源码
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};
对应的流程如下:
(二)路径分析和文件定位
1、路径分析
模块标识符分析:
(1)核心模块,如http,fs模块。
(2)以 . 或者 ../ 开始的相对路径文件模块。
(3)以 / 开始的绝对路径模块。
(4)非路径形式的文件模块。
分析:
(1)核心模块:优先级仅次于缓存,加载速度最快。如果自定义模块和核心模块名称相同,加载会失败。若想成功,必须修改自定义模块的名称或者换个路径。
(2)路径形式的文件模块:以 . 或者 .. 或者 / 开始的标识符,都会被当做文件模块来处理。加载过程中,require方法将路径转换为真实的路径,加载速度仅次于核心模块。
(3)非路径形式的自定义模块:这是一种特殊的文件模块,可能是一个文件或者包的形式。查找这类模块的策略类似于js作用域链,node会逐个尝试模块路径中的路径,知道找到目标文件为止。
注意:这是node定位文件模块的具体文件的时候的查找策略,具体表现为一个路径的组成的数组。
可以在REPL环境中输出Module对象,查看其path属性的方式查看上述数组,文章开始的paths数组:
2、文件定位
(1)文件拓展名分析
require()分析的标识符可以不包含扩展名,node会按.js、.node、.json的次序补足扩展名,依次尝试
(2)目标分析和包
如果在扩展名分析的步骤中,查找不到文件而是查找到相应目录,此时node会将目录当做包来处理,进行下一步分析查找当前目录下package.json中的main属性指定的文件名,若查找不成功则依次查找index.js,index.node,index.json。
如果目录分析的过程中没有定位到任何文件,则自定义模块会进入下一个模块路径继续查找,直到所有的模块路径都遍历完毕,依然没找到则抛出查找失败的异常。
(3)参考源码
在Module._load方法的内部调用了Module._findPath这个方法,这个方法是用来返回模块的绝对路径的,源码如下:
Module._findPath = function(request, paths) {
// 列出所有可能的后缀名:.js,.json, .node
var exts = Object.keys(Module._extensions);
// 如果是绝对路径,就不再搜索
if (request.charAt(0) === '/') {
paths = [''];
}
// 是否有后缀的目录斜杠
var trailingSlash = (request.slice(-1) === '/');
// 第一步:如果当前路径已在缓存中,就直接返回缓存
var cacheKey = JSON.stringify({request: request, paths: paths});
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
}
// 第二步:依次遍历所有路径
for (var i = 0, PL = paths.length; i < PL; i++) {
var basePath = path.resolve(paths[i], request);
var filename;
if (!trailingSlash) {
// 第三步:是否存在该模块文件
filename = tryFile(basePath);
if (!filename && !trailingSlash) {
// 第四步:该模块文件加上后缀名,是否存在
filename = tryExtensions(basePath, exts);
}
}
// 第五步:目录中是否存在 package.json
if (!filename) {
filename = tryPackage(basePath, exts);
}
if (!filename) {
// 第六步:是否存在目录名 + index + 后缀名
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
}
// 第七步:将找到的文件路径存入返回缓存,然后返回
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// 第八步:没有找到文件,返回false
return false;
};
(三)清除缓存
根据上述的模块引入机制我们知道,当我们第一次引入一个模块的时候,require的缓存机制会将我们引入的模块加入到内存中,以提升二次加载的性能。但是,如果我们修改了被引入模块的代码之后,当再次引入该模块的时候,就会发现那并不是我们最新的代码,这是一个麻烦的事情。如何解决呢?
require(): 加载外部模块
require.resolve():将模块名解析到一个绝对路径
require.main:指向主模块
require.cache:指向所有缓存的模块
require.extensions:根据文件的后缀名,调用不同的执行函数
解决办法:
//删除指定模块的缓存
delete require.cache[require.resolve('/*被缓存的模块名称*/')]
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
然后我们再重新require进来需要的模块就可以了。
如若转载,请注明出处:sau交流学习社区-power by saucxs(程新松)(/page/777.html)
- 记录一次nginx故障:recv() failed (104: Connection reset by peer) while reading response header from upstream,
- 微信小程序初体验,入门练手项目--通讯录,部署上线(二)
- 微信小程序初体验,入门练手项目--通讯录,后台是阿里云服务器(一)
- thinkjs性能优化之nginx配置
- 开学AI登场!南京高校用人脸识别查考勤管理学生,还敢逃课、玩手机?姚AI变脸热到爆!开源换脸工具FakeSwap今登GitHub排行榜第二位,ICLR飞升,IJCAI降级:清华的新版AI顶会评级引发学术圈热议,意外!已获6700万美元投资的自动驾驶LiDAR创业公司关门了
标签云
图文推荐
“深度学习”这十年:52篇大神级论文再现AI荣与光,12岁上大学、31岁成最年轻IEEE Fellow,前百度总裁张亚勤加盟清华,全球女性福音!DeepHealth深度学习模型检测乳腺癌完胜5名放射科医师,读博无门,就业碰壁,孤独当了7个月“民科”后,我的论文中了顶会Spotlight
saucxs 2020-01-03 10:04:09
1200 万部手机、500 亿实时位置数据,上帝视角还原特朗普全天行踪,自动驾驶玩漂移 斯坦福最新研究:《头文字D》无人车版来了,印度裔又要统领美国科学界?特朗普提名他领导美国国家科学基金会,GitHub排名第一!免费最强“抢票神器”在手,程序员抢票再不用跪求加速包
saucxs 2019-12-25 10:08:05
蝉联AI专业全球冠军!清华力压CMU再夺CSRankings高校AI排行王座,波士顿动力CEO:不排斥军方订单,就想你粗暴地对待机器狗,亚马逊Alexa突然发疯,读着论文教主人自杀:你活着地球人口会过剩,图神经网络的ImageNet?斯坦福大学等开源百万量级OGB基准测试数据集
saucxs 2019-12-24 09:57:45
2020 AAAI Fellow名单出炉!深度学习先驱Bengio和LeCun双双入选,李世石最后一战不敌AI“韩豆”!唯一战胜过AlphaGo的男人正式退役,GAN和PS合体会怎样?东京大学图像增强新研究:无需配对图像,增强效果还可解释,2019年最受关注的100项研究,AI“复活”蒙娜丽莎排第一
saucxs 2019-12-23 09:58:10
这家AI公司用面具破解中国人脸识别系统!微信、支付宝、火车站无一幸免,OpenAI公开Dota 2论文:胜率99.4%,「手术」工具连续迁移训练,9岁神童大学毕业前突然退学,转赴美国攻读电气工程博士,Reddit热议:15岁高中生用神经网络建立生命进化“新宇宙”
saucxs 2019-12-17 09:45:30
- 东方财富
- saucxs博客园
- weekly周报
- loveBook
- 前端博客
- 技术交流学习社区
- saucxs
- segmentfault
- saucxs的博客
- haorooms
- luckyscript
- 东方财富期货
- 峰云就她了
- binlive
- songEagle
- 且听风吟
- (TX)Heying Ye
- CG Online Judge
- Chris's Blog
- vueRouter
- 龙恩0707
- 鑫空间,鑫生活
- 木易杨
- MDN-js
- Cherry's Blog
- InterviewMap
- 安安哥的小戏台
- xiaojun1994-cat
- JavaScript-Garden
- junruchen-daily
- 秋天爱美丽
- web-summary-daily
- w3cplus-tb
- 潜行者m
- 于江水
- KieSun-git
- cnodejs
- aerminBlog
- fex_bd
- gwuhaolin-git
- Deboy's Blog
- react-china
- react.docschina
- Vuejs社区
- vuejsDoc
- cdnSocketIo
- shuaihuaCC
- colorize
- stazhu
- 前端学习
- IBM Dev社区
- linux运维笔记
- webpackDoc
- sayskyGithub
- 前端里
- 996.ICU
- electronjs
- CS-Notes
- mqyqingfengBlog
- beego官方
- 编码规范
- 阮一峰个人网站
- sessionstack
- damonareGoUp
- 咀嚼之味
- fe-interview
- xd-tayde/blog
- css-tricks
- ghmagical个人中心
- markyunp6
- hellogithub
- 霜序廿
- rin部落
- 建站素材
- 前端资源网
- CSS开发手册
- web技术交流
- 程新松个人网站
- wangEditor
- 小松博客
- 交流学习社区