# Nodejs初探
# 1、简介
nodejs是一个基于chrome V8引擎的js运行环境,使用了一个事件驱动、非阻塞式i/o的模型,使其轻量又高效。
# 1、安装
- 通过node.js 官⽹的⼆进制安装⽂件来安装
- 通过二进制编译方式来安装,打包上线的一些nodejs环境,eg:docker、linux不适合通过安装文件安装
# 2、版本切换
使用nvm和n工具:
nvm 的全称就是 node version manager,意思就是能够管理 node 版本的⼀个⼯具,它提供了⼀种直接通过shell 执⾏的⽅式来进⾏安装,原理:通过nvm切换环境变量里node命令指向的软件路径。
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
# 3、周边工具解析
- 性能优化:压力测试ab、webbench
- 内存泄露于debug:vsc工具或断点
- vscode加断点调试:run and debug
- node工具和插件
- node --inspect --inspect-brk xxx:生成websocket链接
- chrome插件:NIM的inspect进行调试和内存快照查看,输入chrome://inspect/#devices
- 轻量的js解析引擎:quickjss
- 服务端js运行环境:deno
- 基于 rust 和 typescript 开发⼀些上层模块
- ⽀持从 url 加载模块,同时⽀持 top level await 等特性
参考资料:nodejs调试 (opens new window)
# 2、commonjs模块
# 1、用法
- 引入模块:require("./index.js")
- 导出模块:module.exports={...} 或者 exports.key={}
- 注意:exports为module.exports的引用,不可使用exports直接赋值,模块无法导出,eg:exports={}
- 缓存:require值会缓存,通过require引用文件时,会将文件执行一遍后,将结果通过浅克隆的方式,写入全局内存,后续require该路径,直接从内存获取,无需重新执行文件
- 值拷贝:模块输出是值的拷贝,一但输出,模块内部变化后,无法影响之前的引用,而ESModule是引用拷贝。commonJS运行时加载,ESModule编译阶段引用
- CommonJS在引入时是加载整个模块,生成一个对象,然后再从这个生成的对象上读取方法和属性
- ESModule不是对象,而是通过export暴露出要输出的代码块,在import时使用静态命令的方法引用制定的输出代码块,并在import语句处执行这个要输出的代码,而不是直接加载整个模块
TIP
面试题:为何直接使用exports导出commonjs模块,eg:exports = “hello”
解析:commonjs模块是通过module.exports导出模块,exports为module.exports的引用,使用exports直接赋值导出模块,只会改变exports的引用不会导出commonjs模块。
// cjs正确导出用法
exports.key = 'hello world'
module.exports = "hello world"
//错误导出
exports = "hello world" //无法输出
//解析:
const obj = {
key: {}
}
obj.key = "hello world" //可改变obj
const key = obj.key
key.key1 = "hello world" //可改变obj
key = "hello world" //无法改变obj,改变了key的引用
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2、原理实现
使用fs、vm、path内置模块,以及函数包裹形式实现
const vm = require("vm")
const path = require("path")
const fs = require("fs")
/**
* commonjs的require函数:引入module
* @param {string} filename module的名称
*/
function customRequire(filename){
const pathToFile = path.resolve(__dirname, filename)
const content = fs.readFileSync(pathToFile, 'utf-8')
//使用函数包裹模块,执行函数
//注入形参:require、module、exports、__dirname、__filename
const wrapper = [
'(function(require, module, exports, __dirname, __filename){',
'})'
]
const wrappedContent = wrapper[0] + content + wrapper[1]
const script = new vm.Script(wrappedContent, {
filename: 'index.js'
})
const module = {
exports: {}
}
//转换为函数,类似eval,(funcion(require, module, exports){ xxx })
const result = script.runInThisContext();
//函数执行,引入模块,若内部有require继续递归
//exports为module.exports的引用
result(customRequire, module, module.exports);
return module.exports
}
global.customRequire = customRequire
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 3、源码解析
- 源码路径:/lib/internal/modules/cjs/
//loader.js
//require函数定义
1、Module.prototype.require:调用__load函数
2、Module.__load:_cache处理,调用load函数
3、Module.prototype.load函数:调用Module._extensions[extension](this, filename);
//不同的后缀通过定义不同的函数指定解析规则:以Module._extensions['.js']为例
4、Module._extensions['.js'] = function(module, filename) {
//读取缓存或者通过readFileSync读取内容
if (cached?.source) {
content = cached.source;
cached.source = undefined;
} else {
content = fs.readFileSync(filename, 'utf8');
}
//...
//调用compile解析
module._compile(content, filename);
}
5、Module.prototype._compile = function(content, filename){
//生成包裹函数:warpSafe获取函数字符串并使用vm执行生成执行函数
const compiledWrapper = wrapSafe(filename, content, this);
//执行函数
const exports = this.exports;
const thisValue = exports;
const module = this;
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {
//静态方法Reflect.apply(target, thisArgument, argumentsList)
//通过指定的参数列表发起对目标(target)函数的调用
result = ReflectApply(compiledWrapper, thisValue,
[exports, require, module, filename, dirname]);
}
return result;
}
6、function wrapSafe(filename, content, cjsModuleInstance) {
/* 生成包裹函数字符:
let wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
const wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});',
];*/
const wrapper = Module.wrap(content);
//获取包裹函数
return vm.runInThisContext(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: async (specifier) => {
const loader = asyncESM.ESMLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
- 利用源码扩展工具
- 后缀名扩展解析
- 切面编程
const Module = require('module')
//后缀解析扩展:.test后缀同.js后缀
Module._extensions['.test'] = Module._extensions['.js']
//切面编程:解析js模块前做打印处理
const prevFunc = Module._extensions['.js']
Module._extensions['.js'] = function(...args){
console.log('load script')
prevFunc.apply(prevFunc, args)
}
2
3
4
5
6
7
8
9
10
# 3、事件循环
参考资料:node事件循环6个阶段 (opens new window)、node事件循环面试
- 同步代码
- process.nextTick和promise.then() ,之后进入事件循环
- timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
- I/O callbacks 阶段 :处理一些上一轮循环中的少数未执行的 I/O 回调
- idle, prepare 阶段 :仅node内部使用
- poll 阶段 :获取新的I/O事件, 适当的条件下node将阻塞在这里
- check 阶段 :执行 setImmediate() 的回调
- close callbacks 阶段:执行 socket 的 close 事件回调
习题解析:
- new Promise(()=>{//同步执行}).then(()=>{//异步执行})
- async function test(){console.log() //同步 -> await test(0) //同步 -> console.log()//异步}
//习题1:
// 顺序不确定,只有两个语句,执行环境有差异
// 场景1: setTimeout 0 最少1ms,未推入timers队列中,执行结果为:setImmediate、setTimeout
// 场景2: setTimeout 0 已推入timers队列中,执行结果为:setTimeout、setImmediate
setTimeout(()=>{
console.log('setTimeout')
}, 0)
setImmediate(()=>{
console.log('setImmediate')
})
//习题2: 都在回调函数中,内容确定
//首轮事件循环setTimeout1的timers清空,执行至check阶段,先输出setImmediate
//第二轮事件循环setTimeout2
//最终输出:setTimeout1、setImmediate、setTimeout2
setTimeout(()=>{
setTimeout(()=>{
console.log('setTimeout2')
}, 0)
setImmediate(()=>{
console.log('setImmediate')
})
console.log('setTimeout1')
}, 0)
//习题3: 混合题
setTimeout(()=>{
console.log('timeout')
}, 0)
setImmediate(()=>{
console.log('immediate')
setImmediate(()=>{
console.log('immediate1')
})
new Promise(resolve => {
console.log(77)
resolve()
}).then(()=>{
console.log(88)
})
process.nextTick(function(){
console.log('nextTick1')
});
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(()=>{
console.log(8)
})
process.nextTick(function(){
console.log('nextTick2')
})
console.log('start')
// 第一轮:7 start - timeout | immediate 77 | 8 | nextTick2
// 第二轮:7 start nextTick2 8 timeout immediate 77 - immediate1 | 88 | nextTick1
// 第三轮:7 start nextTick2 8 timeout immediate 77 nextTick1 88 immediate1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 4、模块
# 1、底层依赖模块
V8 引擎:主要是 JS 语法的解析,有了它才能识别 JS语法
libuv:C 语⾔实现的⼀个⾼性能异步⾮阻塞 IO 库,⽤来实现 node.js 的事件循环
http-parser/llhttp:底层处理 http 请求,处理报⽂,解析请求包等内容
openssl:处理加密算法,各种框架运⽤⼴泛,底层依赖,无需js实现
zlib:处理压缩等内容
# 2、全局对象
- 全局对象:global,下挂如下对象和函数,使用时无需模块引入
- global
- Buffer、process、console、queueMicrotask
- setTimeout、clearTimeout、setInterval、setImmediate、clearInterval
- 模块中使用注入变量:
- __filename:当前文件名称带路径,eg:/Users/jian/workspace/cjs/index.js
- __dirname:当前文件夹名称,eg:/Users/jian/workspace/cjs/
- cjs:require, module, exports
//全局this指向global
//模块文件中this指向:module.exports的对象
/* 模块中才可以使用的变量:commonjs模块注入,非模块中使用报错
__dirname、__filename、exports、module、require */
//node命令行中
> console.log(__dirname)
// Uncaught ReferenceError: __dirname is not defined
> console.log(this)
/* <ref *1> Object [global] {
global: [Circular *1],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
}
}*/
//node模块中执行
console.log(this)
console.log(__filename)
console.log(__dirname)
/*
{}
/Users/jian/workspace/cjs/index.js
/Users/jian/workspace/cjs
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 1、process
- argv:返回一个数组,由命令行执行脚本时的各个参数组成。它的第一个成员总是node,第二个成员是脚本文件名,其余成员是脚本文件的参数
- env:返回一个对象,成员为当前 shell 的环境变量
- stdin:标准输入流
- stdout:标准输出流
- cwd():返回当前进程的工作目录
console.log(process.argv)
//[ '/usr/local/bin/node', '/Users/jian/workspace/cjs/index.js' ]
console.log(process.cwd()) ////Users/jian/workspace/cjs
process.stdout.write("Hello World!"); //Hello World!
2
3
4
# 3、常见内置模块
node.js 中最主要的内容,就是实现了⼀套 CommonJS的模块化规范,以及内置了⼀些常⻅的模块。
- fs:⽂件系统,能够读取写⼊当前安装系统环境中硬盘的数据
- path:路径系统,能够处理路径之间的问题
- crypto:加密相关模块,能够以标准的加密⽅式对我们的内容进⾏加解密
- dns:处理 dns 相关内容,例如我们可以设置 dns 服务器等等
- http: 设置⼀个 http 服务器,发送 http 请求,监听响应等等
- readline: 读取 stdin 的⼀⾏内容,可以读取、增加、删除我们命令⾏中的内容
- os:操作系统层⾯的⼀些 api,例如告诉你当前系统类型及⼀些参数
- vm: ⼀个专⻔处理沙箱的虚拟机模块,底层主要来调⽤ v8 相关 api 进⾏代码解析
# 1、path模块
/*
1、path.join([path1][, path2][, ...]):连接路径,主要用途在于,会正确使用当前系统的路径分隔符
2、path.resolve([from ...], to):用于返回绝对路径,类似于多次cd处理
3、path.dirname(p):返回文件夹
4、path.extname(p):返回后缀名
5、path.basename(p):返回路径中的最后一部分
*/
const path = require("path");
// 连接路径:/test/test1/2slashes/1slash
console.log('joint path : ' + path.join('/test', 'test1', '2slashes/1slash', 'tab', '..'));
// 转换为绝对路径:/Users/jian/workspace/cjs/main.js
const mainPath = path.resolve('main.js')
console.log('resolve : ' + mainPath);
// 路径中文件的后缀名:.js
console.log('ext name : ' + path.extname(mainPath));
// 路径最后名字:main.js
console.log('ext name : ' + path.basename(mainPath));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2、fs
参考资料:Node.js 高级进阶之 fs 文件模块学习 (opens new window)
文件常识
- 权限位mode:文件所有者/文件所属组/其他用户的rwx,eg:默认0o666,对应十进制438
- 标志位flag:文件的操作方式,r、w、s同步、+增加相反操作、x排他方式
- 文件描述符fd:识别和追踪文件
完整性读写文件操作
- fs.readFile(filename,[encoding],[callback(error,data)]
- fs.writeFile(filename,data,[options],callback)
- options:可选,为对象,{encoding, mode, flag}
- 默认编码为 utf8,模式为 0666(可读可写可操作),flag 为 'w'
- fs.unlink(filename, callback)
指定位置读写文件操作(高级文件操作)
- fs.open(path,flags,[mode],callback)
- fs.read(fd, buffer, offset, length, position, callback);
- fs.write(fd, buffer, offset, length, position, callback);
- fs.close(fd,callback)
//nodejs中最好使用绝对路径
const pathToFile = path.resolve(__dirname, './text')
//err first, 异步
fs.readFile(pathTofile, 'utf-8', function(err, result){
if(err){
console.log('e', e)
return err
}
console.log('result', result)
})
//同步文件
fs.readFileSync(pathToFile, 'utf-8')
//耦合promise封装
function read(path) {
return new Promise(function (resolve, reject) {
fs.readFile(path, { flag: 'r', encoding: 'utf-8' }, function (err, data) {
if (err) {
// 失败执行的内容
reject(err)
} else {
// 成功执行的内容
resolve(data)
}
})
})
}
//通用promise封装,高版本已内置该库
function promisify(func){
return function(...args){
return new Promise((resolve, reject)=>{
args.push(function(err, result){
if(err) return reject(err)
return resolve(result)
})
return func.apply(func, args)
/* 等价于
fs.readFile(path, { flag: 'r', encoding: 'utf-8' }, function (err, data) {
if (err) {
// 失败执行的内容
reject(err)
} else {
// 成功执行的内容
resolve(data)
}
}) */
})
}
}
const creadFileAsync = promisify(fs.readFile)
creadFileAsync(pathToFile, 'utf-8')
.then(content => {
console.log(content)
}).catch(e=>{
console.log('e', e)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 3、http
var http = require('http');
var fs = require('fs');
var url = require('url');
// 创建服务器
http.createServer( function (request, response) {
// 解析请求,包括文件名
var pathname = url.parse(request.url).pathname;
// 输出请求的文件名
console.log("Request for " + pathname + " received.");
// 从文件系统中读取请求的文件内容
fs.readFile(pathname.substr(1), function (err, data) {
if (err) {
console.log(err);
// HTTP 状态码: 404 : NOT FOUND
// Content Type: text/html
response.writeHead(404, {'Content-Type': 'text/html'});
}else{
// HTTP 状态码: 200 : OK
// Content Type: text/html
response.writeHead(200, {'Content-Type': 'text/html'});
// 响应文件内容
response.write(data.toString());
}
// 发送响应数据
response.end();
});
}).listen(8080);
// 控制台会输出以下信息
console.log('Server running at http://127.0.0.1:8080/');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 4、buffer
参考资料:buffer乱码问题 (opens new window)、buffer模块解析 (opens new window)
# 5、stream
参考资料:stream模块解析 (opens new window)
# 6、EventEmitter
大多数时候我们不会直接使用 EventEmitter,而是在对象中继承它,包括 fs、net、 http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。
EventEmitter
会按照监听器注册的顺序同步地调用所有监听器,所以必须确保事件的排序正确,且避免竞态条件。常见api
- addListener(event, listener)添加监听器
- removeListener(event, listener)移除监听器
- removeAllListeners([event])移除所有监听器
- on(event, listener)注册监听器
- off(eventName, listener)移除监听器
- once(event, listener)为指定事件注册一个单次监听器
- emit(event, [arg1], [arg2], [...])按监听器的顺序执行执行每个监听器,如果事件有注册监听返回 true,否则返回 false
错误处理:当
EventEmitter
实例出错时,应该触发'error'
事件。 这些在 Node 中被视为特殊情况。如果没有为'error'
事件注册监听器,则当'error'
事件触发时,会抛出错误、打印堆栈跟踪、并退出 Node.js 进程。作为最佳,应该始终为'error'
事件注册监听器。
const EventEmitter = require('events').EventEmitter;
var eventEmitter = new EventEmitter();
eventEmitter.on('event', function(a, b) {
console.log(a, b, this, this === eventEmitter);
// 打印: 普通函数,this指向eventEmitter实例
// a b MyEmitter {
// domain: null,
// _events: { event: [Function] },
// _eventsCount: 1,
// _maxListeners: undefined } true
});
eventEmitter.emit('event', 'a', 'b');
eventEmitter.on('event', (a, b) => {
//箭头函数,this指向module.exports
console.log(a, b, this, this === module.exports);
// 打印: a b {} true
});
eventEmitter.emit('event', 'a', 'b');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TIP
面试题:事件模型设计(使用:发布订阅模式)
class EventEmitter{
constructor(maxListeners){
this.events = {}
this.maxListeners = maxListners || Infinity
}
on(event, listener){
if(!this.events[event]){
this.events[event] = []
}
if(this.maxListener != Infinity
&& this.events[event].length > this.maxListeners){
console.warn(`${event} has reached max listeners.`)
return this
}
this.events[event].push(listener)
return this
}
off(event, listener){
if(!listener){
this.events[event] = null //无listener全部移除
}else{
this.events[event] = this.events[event].filter(item => item !== listener)
}
return this//链式调用
}
once(event, listener){
const func = (...args) => {
this.off(event, listener)
listener.apply(this, args)
}
this.on(event, func)
}
emit(event, ...args){
const listeners = this.events[event]
if(!listeners){
console.log("no listeners")
return this
}
listeners.forEach(cb => {
cb.apply(this, args)
})
return this
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48