参照:
Node.js 是一个开源与跨平台的 JavaScript 运行时环境
在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能
可以理解为 Node.js 就是一个服务器端的、非阻塞式I/O的、事件驱动的JavaScript运行环境
特点:
- 非阻塞异步
- 事件驱动
优缺点:
优点:
- 处理高并发场景性能更佳
- 适合I/O密集型应用,值的是应用在运行极限时,CPU占用率仍然比较低,大部分时间是在做 I/O硬盘内存读写操作
因为Nodejs是单线程,带来的缺点有:
- 不适合CPU密集型应用
- 只支持单核CPU,不能充分利用CPU
- 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃
全局对象:
- Class:Buffer
- process
- console
- clearInterval、setInterval
- clearTimeout、setTimeout
- global
进程相关 process
常见属性方法
- process.env:环境变量,例如通过 `process.env.NODE_ENV 获取不同环境项目配置信息
- process.nextTick:这个在谈及
EventLoop时经常为会提到 - process.pid:获取当前进程id
- process.ppid:当前进程对应的父进程
- process.cwd():获取当前进程工作目录,
- process.platform:获取当前进程运行的操作系统平台
- process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值
- 进程事件: process.on(‘uncaughtException’,cb) 捕获异常信息、 process.on(‘exit’,cb)进程推出监听
- 三个标准流: process.stdout 标准输出、 process.stdin 标准输入、 process.stderr 标准错误输出
- process.title 指定进程名称,有的时候需要给进程指定一个名称
process.nextTick()
我们知道NodeJs是基于事件轮询,在这个过程中,同一时间只会处理一件事情
在这种处理模式下,process.nextTick()就是定义出一个动作,并且让这个动作在下一个事件轮询的时间点上执行
Node 事件循环机制
在Node中,同样存在宏任务和微任务,与浏览器中的事件循环相似
微任务对应有:
- next tick queue:process.nextTick
- other queue:Promise的then回调、queueMicrotask
宏任务对应有:
- timer queue:setTimeout、setInterval
- poll queue:IO事件
- check queue:setImmediate
- close queue:close事件
其执行顺序为(优先级):
- next tick microtask queue
- other microtask queue
- timer queue
- poll queue
- check queue
- close queue
模块化相关
在Node中每一个js文件都是一个单独的模块,模块中包括CommonJS规范的核心变量:exports、module.exports、require。
传参方式
require方法接收一下几种参数的传递:
- 原生模块:http、fs、path等
- 相对路径的文件模块:./mod或../mod
- 绝对路径的文件模块:/pathtomodule/mod
- 目录作为模块:./dirname
- 非原生模块的文件模块:mod
文件查找的优先级
优先查询缓存模块和内置模块,让后去根据路径进行查找
- 缓存的模块优先级最高
- 如果是内置模块,则直接返回,优先级仅次缓存的模块
- 如果是绝对路径 / 开头,则从根目录找
- 如果是相对路径 ./开头,则从当前require文件相对位置找
- 如果文件没有携带后缀,先从js、json、node按顺序查找
- 如果是目录,则根据 package.json的main属性值决定目录下的入口文件,默认情况为 index.js
- 如果文件为第三方模块,则会引入 node_modules 文件,如果不在当前仓库文件中,则自动从上级递归查找,直到根目录
场景题
说说对中间件概念的理解,如何封装 node 中间件?
中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的
在NodeJS中,中间件主要是指封装http请求细节处理的方法
例如在express、koa等web框架中,中间件的本质为一个回调函数,参数包含请求对象、响应对象和执行下一个中间件的函数
在这些中间件函数中,我们可以执行业务逻辑代码,修改请求和响应对象、返回响应数据等操作
比如koa是基于NodeJS当前比较流行的web框架,本身支持的功能并不多,功能都可以通过中间件拓展实现。通过添加不同的中间件,实现不同的需求,从而构建一个 Koa 应用
如何实现JWT鉴权
如何工作(工作流程)?组成部分是什么?
流程如下:
- 服务器当验证用户账号和密码正确的时候,给用户颁发一个令牌,这个令牌作为后续用户访问一些接口的凭证
- 后续访问会根据这个令牌判断用户时候有权限进行访问
Token,分成了三部分,头部(Header)、载荷(Payload)、签名(Signature),并以.进行拼接。其中头部和载荷都是以JSON格式存放数据,只是进行了编码
header
每个JWT都会带有头部信息,这里主要声明使用的算法。声明算法的字段名为alg,同时还有一个typ的字段,默认JWT即可。以下示例中算法为HS256
{ "alg": "HS256", "typ": "JWT" }
因为JWT是字符串,所以我们还需要对以上内容进行Base64编码,编码后字符串如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload
载荷即消息体,这里会存放实际的内容,也就是Token的数据声明,例如用户的id和name,默认情况下也会携带令牌的签发时间iat,通过还可以设置过期时间,如下:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
同样进行Base64编码后,字符串如下:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Signature
签名是对头部和载荷内容进行签名,一般情况,设置一个secretKey,对前两个的结果进行HMACSHA25算法,公式如下:
Signature = HMACSHA256(base64Url(header)+.+base64Url(payload),secretKey)
一旦前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名不一致
如何实现文件上传?说说你的思路
因为浏览器限制,浏览器不能直接操作文件系统的,需要通过浏览器所暴露出来的统一接口,由用户主动授权发起来访问文件动作,然后读取文件内容进指定内存里,最后执行提交请求操作,将内存里的文件内容数据上传到服务端,服务端解析前端传来的数据信息后存入文件里
对于文件上传,我们需要设置请求头为content-type:multipart/form-data
multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件
至于使用multipart/form-data,是因为文件是以二进制的形式存在,其作用是专门用于传输大型二进制数据,效率高
在服务器中,比如采用koa2中间件的形式解析上传的文件数据,分别有下面两种形式:
- koa-body
- koa-multer
分页功能实现
基本属性
{
"totalCount": 1836, // 总的条数
"totalPages": 92, // 总页数
"currentPage": 1 // 当前页数
"data": [ // 当前页的数据
{
...
}
]
后端逻辑:
- 获取用户参数页码数page和每页显示的数目 pageSize ,其中page 是必须传递的参数,pageSize为可选参数,默认为10
- 编写 SQL 语句,利用 limit 和 OFFSET 关键字进行分页查询
- 查询数据库,返回总数据量、总页数、当前页、当前页数据给前端
router.all('/api', function (req, res, next) {
var param = '';
// 获取参数
if (req.method == "POST") {
param = req.body;
} else {
param = req.query || req.params;
}
if (param.page == '' || param.page == null || param.page == undefined) {
res.end(JSON.stringify({ msg: '请传入参数page', status: '102' }));
return;
}
const pageSize = param.pageSize || 10;
const start = (param.page - 1) * pageSize;
const sql = `SELECT * FROM record limit ${pageSize} OFFSET ${start};`
pool.getConnection(function (err, connection) {
if (err) throw err;
connection.query(sql, function (err, results) {
connection.release();
if (err) {
throw err
} else {
// 计算总页数
var allCount = results[0][0]['COUNT(*)'];
var allPage = parseInt(allCount) / 20;
var pageStr = allPage.toString();
// 不能被整除
if (pageStr.indexOf('.') > 0) {
allPage = parseInt(pageStr.split('.')[0]) + 1;
}
var list = results[1];
res.end(JSON.stringify({ msg: '操作成功', status: '200', totalPages: allPage, currentPage: param.page, totalCount: allCount, data: list }));
}
})
})
});
Node性能如何进行监控以及优化?
Node作为一门服务端语言,性能方面尤为重要,其衡量指标一般有如下:
- CPU
- 内存
- I/O
- 网络
CPU:
主要分成了两部分:
- CPU负载:在某个时间段内,占用以及等待CPU的进程总数
- CPU使用率:CPU时间占用状况,等于 1 - 空闲CPU时间(idle time) / CPU总时间
这两个指标都是用来评估系统当前CPU的繁忙程度的量化指标
Node应用一般不会消耗很多的CPU,如果CPU占用率高,则表明应用存在很多同步操作,导致异步任务回调被阻塞
内存
内存是一个非常容易量化的指标。 内存占用率是评判一个系统的内存瓶颈的常见指标。 对于Node来说,内部内存堆栈的使用状态也是一个可以量化的指标
在Node中,一个进程的最大内存容量为1.5GB。因此我们需要减少内存泄露
// /app/lib/memory.js
const os = require('os');
// 获取当前Node内存堆栈情况
const { rss, heapUsed, heapTotal } = process.memoryUsage();
// 获取系统空闲内存
const sysFree = os.freemem();
// 获取系统总内存
const sysTotal = os.totalmem();
module.exports = {
memory: () => {
return {
sys: 1 - sysFree / sysTotal, // 系统内存占用率
heap: heapUsed / headTotal, // Node堆内存占用率
node: rss / sysTotal, // Node占用系统内存的比例
}
}
}
磁盘 I/O
硬盘的IO 开销是非常昂贵的,硬盘 IO 花费的 CPU 时钟周期是内存的 164000 倍
内存 IO比磁盘IO 快非常多,所以使用内存缓存数据是有效的优化方法。常用的工具如 redis、memcached等
并不是所有数据都需要缓存,访问频率高,生成代价比较高的才考虑是否缓存,也就是说影响你性能瓶颈的考虑去缓存,并且而且缓存还有缓存雪崩、缓存穿透等问题要解决
监控常用方法:
如何优化Node.js 脚本?
-
使用最新版本Node.js
-
正确使用流 Stream
在
Node中,很多对象都实现了流,对于一个大文件可以通过流的形式发送,不需要将其完全读入内存
const http = require('http');
const fs = require('fs');
// bad
http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', function (err, data) {
res.end(data);
});
});
// good
http.createServer(function (req, res) {
const stream = fs.createReadStream(__dirname + '/data.txt');
stream.pipe(res);
});
-
代码层面优化
合并查询,将多次查询合并一次,减少数据库的查询次数
// bad
for user_id in userIds
let account = user_account.findOne(user_id)
// good
const user_account_map = {} // 注意这个对象将会消耗大量内存。
user_account.find(user_id in user_ids).forEach(account){
user_account_map[account.user_id] = account
}
for user_id in userIds
var account = user_account_map[user_id]
-
内存管理优化
基于V8新旧分代机制,使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中。每次用到该对象时,就取对象池空闲的对象,并对它进行初始化操作,从而提高框架的性能