咱们都晓得Nodejs遵循的是CommonJS
标准,当咱们require('moduleA')
时,模块是怎么通过名字或者门路获取到模块的呢?首先要聊一下模块援用、模块定义、模块标识三个概念。
1 CommonJS
标准
1.1 模块援用
模块上下文提供require()
办法来引入内部模块,看似简略的require函数, 其实外部做了大量工作。示例代码如下:
//test.js//引入一个模块到以后上下文中const math = require('math');math.add(1, 2);
1.2 模块定义
模块上下文提供了exports
对象用于导入导出以后模块的办法或者变量,并且它是惟一的导出进口。模块中存在一个module
对象,它代表模块本身,exports
是module的属性。一个文件就是一个模块,将办法作为属性挂载在exports上就能够定义导出的形式:
//math.jsexports.add = function () { let sum = 0, i = 0, args = arguments, l = args.length; while(i < l) { sum += args[i++]; } return sum;}
这样就可像test.js
里那样在require()之后调用模块的属性或者办法了。
相干nodejs进阶视频解说:进入学习
1.3 模块标识
模块标识就是传递给require()
办法的参数,它必须是合乎小驼峰命名的字符串,或者以.
、..
结尾的相对路径或者绝对路径,能够没有文件后缀名.js
.
2. Node的模块实现
在Node中引入模块,须要经验如下四个步骤:
- 路径分析
- 文件定位
- 编译执行
- 退出内存
2.1 路径分析
Node.js中模块能够通过文件门路或名字获取模块的援用。模块的援用会映射到一个js文件门路。 在Node中模块分为两类:
- 一是Node提供的模块,称为外围模块(内置模块),内置模块公开了一些罕用的API给开发者,并且它们在Node过程开始的时候就预加载了。
- 另一类是用户编写的模块,称为文件模块。如通过NPM装置的第三方模块(third-party modules)或本地模块(local modules),每个模块都会裸露一个公开的API。以便开发者能够导入。如
const mod = require('module_name')const { methodA } = require('module_name')
执行后,Node外部会载入内置模块或通过NPM装置的模块。require函数会返回一个对象,该对象公开的API可能是函数、对象或者属性如函数、数组甚至任意类型的JS对象。
外围模块是Node源码在编译过程中编译进了二进制执行文件。在Node启动时这些模块就被加载进内存中,所以外围模块引入时省去了文件定位和编译执行两个步骤,并且在路径分析中优先判断,因而外围模块的加载速度是最快的。文件模块则是在运行时动静加载,速度比外围模块慢。
这里列下node模块的载入及缓存机制:
1、载入内置模块(A Core Module)
2、载入文件模块(A File Module)
3、载入文件目录模块(A Folder Module)
4、载入node_modules里的模块
5、主动缓存已载入模块
1、载入内置模块
Node的内置模块被编译为二进制模式,援用时间接应用名字而非文件门路。当第三方的模块和内置模块同名时,内置模块将笼罩第三方同名模块。因而命名时须要留神不要和内置模块同名。如获取一个http模块
const http = require('http')
返回的http即是实现了HTTP性能Node的内置模块。
2、载入文件模块
绝对路径的
const myMod = require('/home/base/my_mod')
或相对路径的
const myMod = require('./my_mod')
留神,这里疏忽了扩展名.js
,以下是对等的
const myMod = require('./my_mod')const myMod = require('./my_mod.js')
3、载入文件目录模块
能够间接require一个目录,假如有一个目录名为folder,如
const myMod = require('./folder')
此时,Node将搜寻整个folder目录,Node会假如folder为一个包并试图找到包定义文件package.json。如果folder目录里没有蕴含package.json
文件,Node会假如默认主文件为index.js
,即会加载index.js
。如果index.js
也不存在, 那么加载将失败。
4、载入node_modules里的模块
如果模块名不是门路,也不是内置模块,Node将试图去当前目录的node_modules
文件夹里搜寻。如果当前目录的node_modules
里没有找到,Node会从父目录的node_modules
里搜寻,这样递归上来直到根目录。
5、主动缓存已载入模块
对于已加载的模块Node会缓存下来,而不用每次都从新搜寻。上面是一个示例
// modA.jsconsole.log('模块modA开始加载...')exports = function() { console.log('Hi')}console.log('模块modA加载结束')
//init.jsvar mod1 = require('./modA')var mod2 = require('./modA')console.log(mod1 === mod2)
命令行node init.js
执行:
模块modA开始加载...模块modA加载结束true
能够看到尽管require了两次,但modA.js依然只执行了一次。mod1和mod2是雷同的,即两个援用都指向了同一个模块对象。
优先从缓存加载
和浏览器会缓存动态js文件一样,Node也会对引入的模块进行缓存,不同的是,浏览器仅仅缓存文件,而nodejs缓存的是编译和执行后的对象(缓存内存) require()
对雷同模块的二次加载一律采纳缓存优先的形式,这是第一优先级的,外围模块缓存查看先于文件模块的缓存查看。
基于这点:咱们能够编写一个模块,用来记录长期存在的变量。例如:我能够编写一个记录接口拜访数的模块:
let count = {}; // 因模块是关闭的,这里实际上借用了js闭包的概念exports.count = function(name){ if(count[name]){ count[name]++; }else{ count[name] = 1; } console.log(name + '被拜访了' + count[name] + '次。');};
咱们在路由的 action
或 controller
里这样援用:
let count = require('count');export.index = function(req, res){ count('index');};
以上便实现了对接口调用数的统计,但这只是个demo,因为数据存储在内存,服务器重启后便会清空。真正的计数器肯定是要联合长久化存储器的。
在进入门路查找之前有必要形容一下module path
这个Node.js中的概念。对于每一个被加载的文件模块,创立这个模块对象的时候,这个模块便会有一个paths属性,其值依据以后文件的门路 计算失去。咱们创立modulepath.js
这样一个文件,其内容为:
// modulepath.jsconsole.log(module.paths);
咱们将其放到任意一个目录中执行node modulepath.js命令,将失去以下的输入后果。
[ '/home/ikeepstudying/research/node_modules','/home/ikeepstudying/node_modules','/home/node_modules','/node_modules' ]
2.2 文件定位
1.文件扩展名剖析
调用require()
办法时若参数没有文件扩展名,Node会按.js
、.json
、.node
的顺寻补足扩展名,顺次尝试。
在尝试过程中,须要调用fs模块阻塞式地判断文件是否存在。因为Node的执行是单线程的,这是一个会引起性能问题的中央。如果是.node
或者·.json·文件能够加上扩展名放慢一点速度。另一个窍门是:同步配合缓存。
2.目录剖析和包
require()
剖析文件扩展名后,可能没有查到对应文件,而是找到了一个目录,此时Node会将目录当作一个包来解决。
首先, Node在挡墙目录下查找package.json
,通过JSON.parse()
解析出包形容对象,从中取出main属性指定的文件名进行定位。若main属性指定文件名谬误,或者没有pachage.json
文件,Node会将index当作默认文件名。
简而言之,如果require
绝对路径的文件,查找时不会去遍历每一个node_modules
目录,其速度最快。其余流程如下:
1.从module path
数组中取出第一个目录作为查找基准。
2.间接从目录中查找该文件,如果存在,则完结查找。如果不存在,则进行下一条查找。
3.尝试增加.js
、.json
、.node
后缀后查找,如果存在文件,则完结查找。如果不存在,则进行下一条。
4.尝试将require
的参数作为一个包来进行查找,读取目录下的package.json
文件,获得main
参数指定的文件。
5.尝试查找该文件,如果存在,则完结查找。如果不存在,则进行第3条查找。
6.如果持续失败,则取出module path
数组中的下一个目录作为基准查找,循环第1至5个步骤。
7.如果持续失败,循环第1至6个步骤,直到module path中的最初一个值。
8.如果依然失败,则抛出异样。
整个查找过程非常相似原型链的查找和作用域的查找。所幸Node.js对门路查找实现了缓存机制,否则因为每次判断门路都是同步阻塞式进行,会导致重大的性能耗费。
一旦加载胜利就以模块的门路进行缓存
2.3 模块编译
每个模块文件模块都是一个对象,它的定义如下:
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; if(parent && parent.children) { parent.children.push(this); } this.filename = null; this.loaded = false; this.children = [];}
对于不同扩展名,其载入办法也有所不同:
.js
通过fs模块同步读取文件后编译执行。.node
这是C/C++编写的扩大文件,通过dlopen()
办法加载最初编译生成的文件.json
同过fs模块同步读取文件后,用JSON.pares()
解析返回后果
其余当作.js
每一个编译胜利的模块都会将其文件门路作为索引缓存在Module._cache
对象上。
json
文件的编译
.json
文件调用的办法如下:其实就是调用JSON.parse
//Native extension for .jsonModule._extensions['.json'] = function(module, filename) { var content = NativeModule.require('fs').readFileSync(filename, 'utf-8'); try { module.exports = JSON.parse(stripBOM(content)); } catch(err) { err.message = filename + ':' + err.message; throw err; }}
Module._extensions
会被赋值给require()
的extensions
属性,所以能够用:console.log(require.extensions)
;输入零碎中已有的扩大加载形式。
当然也能够本人减少一些非凡的加载:
require.extensions['.txt'] = function(){//code};。
然而官网不激励通过这种形式自定义扩展名加载,而是冀望先将其余语言或文件编译成JavaScript文件后再加载,这样的益处在于不讲繁缛的编译加载等过程引入Node的执行过程。
js
模块的编译 在编译的过程中,Node对获取的javascript文件内容进行了头尾包装,将文件内容包装在一个function中:
(function (exports, require, module, __filename, __dirname) { var math = require(‘math‘); exports.area = function(radius) { return Math.PI * radius * radius; }})
包装之后的代码会通过vm原生模块的runInThisContext()
办法执行(具备明确上下文,不净化全局),返回一个具体的function对象,最初传参执行,执行后返回module.exports
.
外围模块编译
外围模块分为C/C++
编写和JavaScript编写的两个局部,其中C/C++
文件放在Node我的项目的src目录下,JavaScript文件放在lib目录下。
1.转存为C/C++代码
Node采纳了V8附带的js2c.py工具,将所有内置的JavaScript代码转换成C++里的数组,生成node_natives.h头文件:
namespace node { const char node_native[] = { 47, 47, ..}; const char dgram_native[] = { 47, 47, ..}; const char console_native = { 47, 47, ..}; const char buffer_native = { 47, 47, ..}; const char querystring_native = { 47, 47, ..}; const char punycode_native = { 47, 47, ..}; ... struct _native { const char* name; const char* source; size_t source_len; } static const struct _native natives[] = { { "node", node_native, sizeof(node_native)-1}, { "dgram", dgram_native, sizeof(dgram_native)-1}, ... };}
在这个过程中,JavaScript代码以字符串模式存储在node命名空间中,是不可间接执行的。在启动Node过程时,js代码间接加载到内存中。在加载的过程中,js外围模块经验标识符剖析后间接定位到内存中。
2.编译js外围模块
lib目录下的模块文件也在引入过程中经验了头尾包装的过程,而后才执行和导出了exports对象。与文件模块的区别在于:获取源代码的形式(外围模块从内存加载)和缓存执行后果的地位。
js外围模块源文件通过process.binding('natives')
取出,编译胜利的模块缓存到NativeModule._cache
上。代码如下:
function NativeModule() { this.filename = id + '.js'; this.id = id; this.exports = {}; this.loaded = fales;}NativeModule._source = process.binding('natives');NativeModule._cache = {};
3 import
和require
简略的说一下import
和require
的本质区别
import
是ES6的模块标准,require
是commonjs的模块标准,具体的用法我不介绍,我只想说一下他们最根本的区别,import是动态(编译时)加载模块,require(运行时)是动静加载,那么动态加载和动静加载的区别是什么呢?
动态加载时代码在编译的时候曾经执行了,动静加载是编译后在代码运行的时候再执行,那么具体点是什么呢?
先说说import,如下代码
import { name } from 'name.js'// name.js文件export let name = 'jinux'export let age = 20
下面的代码示意main.js
文件里引入了name.js
文件导出的变量,在代码编译阶段执行后的代码如下:
let name = 'jinux'
这个是我本人了解的,其实就是间接把name.js
里的代码放到了main.js
文件里,好比是在main.js
文件中申明一样。
再来看看require
var obj = require('obj.js');// obj.js文件var obj = { name: 'jinux', age: 20}module.export obj;
require是在运行阶段,须要把obj对象整个加载进内存,之后用到哪个变量就用哪个,这里再比照一下import
,import
是动态加载,如果只引入了name,age是不会引入的,所以是按需引入,性能更好一点。
4 nodejs革除require缓存
开发nodejs利用时会面临一个麻烦的事件,就是批改了配置数据之后,必须重启服务器能力看到批改后的后果。
于是问题来了,挖掘机哪家强?噢,no! no! no!怎么做到批改文件之后,主动重启服务器。
server.js
中的片段:
const port = process.env.port || 1337;app.listen(port);console.log("server start in " + port);exports.app = app;
假设咱们当初是这样的, app.js的片段:
const app = require('./server.js');
如果咱们在server.js中启动了服务器,咱们进行服务器能够在app.js中调用
app.app.close()
然而当咱们从新引入server.js
app = require('./server.js')
的时候会发现并不是用的最新的server.js文件,起因是require的缓存机制,在第一次调用require('./server.js')
的时候缓存下来了。
这个时候怎么办?
上面的代码解决了这个问题:
delete require.cache[require.resolve('./server.js')];app = require('./server.js');