中山网站快照优化公司,区块链网站建设,网站开发后端工资多少,火车头wordpress发布模块深入了解Nodejs模块机制
我们都知道Nodejs遵循的是CommonJS规范#xff0c;当我们require(moduleA)时#xff0c;模块是怎么通过名字或者路径获取到模块的呢#xff1f;首先要聊一下模块引用、模块定义、模块标识三个概念。
1 CommonJS规范
1.1 模块引用
模块上下文提供…深入了解Nodejs模块机制
我们都知道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.js
exports.add function () {
let sum 0, i 0, args arguments, l args.length; while(i l) { sum args[i];
}
return sum; }这样就可像test.js里那样在require()之后调用模块的属性或者方法了。
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.js
console.log(模块modA开始加载...)
exports function() {console.log(Hi) }
console.log(模块modA加载完毕)//init.js
var 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.jS
console.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 .json
Module._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对象整个加载进内存之后用到哪个变量就用哪个这里再对比一下importimport是静态加载如果只引入了nameage是不会引入的所以是按需引入性能更好一点。
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);小平果118
÷