Appearance
2025最新NodeJs面试题
原文链接:backup/node-2025.md
1. 服务端渲染 SSR
服务端渲染是数据与模板组成的 html,即 html=数据+模板。将组件或页面通过服务器生成 html 字符串,再发送到浏览器,最后将静态标记混合为客户端上完全交互的应用程序。页面没使用服务渲染,当请求页面时,返回的 body 里为空,之后执行 js 将 html 结构注入到 body 里,结合 css 显示出来。
SSR 的优势
对 SEO 友好
所有的模板、图片等资源都存在服务器端
一个 html 返回所有数据
减少 http 请求
响应快、用户体验好、首屏渲染快
更利于 SEO
不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本(google 除外,据说 google 可以运行js)。使用 React 或者其他 MVVM 框架之后,页面大多数 DOM 元素都是在客户端根据 js 动态生成,可供爬虫抓取分析的内容大大减少。另外,浏览器爬虫不会等待我们的数据抓取完成之后在抓取页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行 JS 脚本的最终 html,网络爬虫就可以抓取到完整页面的信息。
更利于首屏渲染
首屏的渲染是 node 发送过来的 html 字符串,并不依赖于 js 文件了,使用客户就会更快的看到页面的内容,尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。
SSR 的局限
服务端压力较大
本来是通过客户端完成渲染,现在统一到服务端 node 服务去做。尤其是高并发访问的情况,会大量占用服务端 CPU 资源
• 开发条件受限
在服务端渲染中,react只会执行到 componentDidMount 之前的生命周期钩子,因此项目引用的第三方的库也不可用其他生命周期钩子,这对引用库的选择产生了很大的限制;
学习成本相对较高
除了对 webpack、MVVM 框架要熟悉,还需要掌握 node、koa2 等相关技术。相对于客户端渲染,项目构建、部署过程更为复杂。
时间耗时比较
数据请求
由服务端请求首屏数据,而不是客户端请求首屏数据,这就是快的一个主要原因。服务端在内网进行请求,数据响应速度快。客户端在不同网络环境进行数据请求,并且外网 http 请求开销大,导致时间差。
• html 渲染
服务端渲染是先向后端服务器请求数据,然后生成完整首屏 html 返回给浏览器;而客户端渲染时是等js 代码下载、加载、解析完成之后在请求数据渲染,等待的过程页面是什么都没有的,就是用户所看到的白屏。就是服务端渲染不需要等待 js 代码下载完成并请求数据,就可以返回一个已有完整数据的首屏页面。
2. 项目中有没有涉及到 Cluster,说一下你的理解
node 天生就是单线程,在多线程语言对于服务重启以及服务降级,都可以使用其他线程进行监控,当主线程的服务发送服务退出命令后,其他线程就会立即启动进行服务平滑切换和降级,但是 node 却因为天生单线程,所以无法开始多线程去监听服务退出任务。
nodeJS 是单线程运行的,这也是经常被吐槽的点,针对这一点,node 推出了 cluster 这个模块,用于创建多进程的 node 应用。
cluster 可以做下面的事情:
发送重启信号给 Master 线程
- 可以根据 cpu 核心数起对应的 n 个新服务并开始监听服务,理论上可以无数个服务,但是一般来说还是根据 os 的核心数去起服务。
master 线程可以等待旧服务
同时还能杀掉旧服务
node 平缓降级与重启的小例子
javascript
// 一个简单带有cluster自动重启的app.js
const cluster = require("cluster");
const cpuNums = require("os").cpus().length;
const http = require("http");
if (cluster.isMaster) {
// 是否在主线程
for (var i = 0; i < cpuNums; i++) {
cluster.fork(); // 有多少个cpu就分多少个cluster出来
cluster.on("exit", function (worker, code, signal) {
console.log('线程id为 ${worker.process.pid} 退出');
});
cluster.on("listening", function (worker, code, signal) {
console.log('线程id为 ${worker.process.pid} 停止服务');
});
process.on("SIGUSR2", function () {
// 接收kill -SIGUSR2 $pid
// 保存旧 worker 的列表, cluster.workers 是个 map
var oldWorkers = Object.keys(cluster.workers).map(function (idx) {
return cluster.workers[idx];
});
// 重新起服务
cluster.fork();
// 当新服务起来之后, 关闭所有的旧 worker
cluster.once("listening", function (worker) {
oldWorkers.forEach(function (worker) {
// disconnect 会停止接收新请求, 等待旧请求结束后再结束进程
worker.disconnect();
});
});
});
} else {
http
.createServer(function (req, res) {
res.end(123);
});
.listen(8080);
console.log('你的线程id为 ${process.pid}');
}
简易的线程关闭自动重启的一个过程。但是工程上面可以使用 pm2 进行服务切换降级,以及对服务更新;我们还可以直接在全局捕错中间件进行 process.exit() 事件,进行发送 SIGUSR2 事件,可以自定义启动参数;这个平缓降级主要的两个点:一个是当主线程死掉的时候,正在进来的 request
和正在出去的 response 如何切换,但是这些 pm2 都帮我们做了,另外我们在工程还需要对线程进行自定义的捕错,不然,会遗漏一些不可预知的错误
Cluster 能给 node 带来性能上很大的提升,让 node 以前让人诟病的单线程也得到解决。同时,Cluster 的多核利用,也能让我们发挥想象力,把它用在其他地方,比如编译打包,比如大数据处理等3.为什么用 gulp 打包 node?
高效的 gulp
gulp 内部使用的是 node 流机制,流自然就是不需要说了,是一种相当高效且不占内存的一种数据格式,它并不会过多的占用 node 的堆内内存,而且所占内存的最大值在 30m 以内,在流向最后一个消费者才会写入磁盘中,在打包过程中,并不会占用磁盘空间或者是内存。
node+gulp 打造高效开发体验面向未来开发
nodejs的痛点---es的支持度,由于es版本发布是相当快的,以及一些标准也是很超前的,但是node作为运行在服务端的js代码,对于这些es版本支持的新特性是不会更新这么快的。首当其冲的就是node对es的支持。在node历史长流中,要到2019年11月21日的13.2版本才正式支持ESModule特效,在13.2版本中的Stability0.1.2中,在生产模式只能使用Stability 2稳定版的api,而且在这个版本中nodejs默认启用对ES模块的实验支持,就是说,这里已经是默认允许我们在任何环境使用实验中的api了,当然即使这些实验中的api不稳定。在以往的Nodejs8.9.0之后的版本中需要在启动项目时需要制定特定的参数--experimental-modules,开启对es支持以及对实验性api支持。
主流浏览器都能通过 <script type="module"> 标签支持 ECMAScript 模块(ES modules)。各种项目 npm 包都使用了 ES 模块编写,并且可以通过 <script type="module"> 在浏览器中直接使用。支持导入映射(import maps)即将登陆 Chrome。import map 将让浏览器支持 node.js 风格的包名导入。基于这个问题,在 node 开发中引入一个中间处理器以支持我们面向未来开发的方式。
打包工具的对比 gulp/Rollup/webpack
首先对于 node 程序的打包要求,这里简单说几点:
在打包后需支持最新的 es 版本
打包编译速度不能太慢,这样对开发进度可控
打包后的文件结构不能发生变化,如有需要可发生一点点变化
配置文件友好
首先第一排除的就是 webpack,因为 webpack 配置起来是比较麻烦的,而且还有很多插件需要版本兼容;在第三点中,打包后文档结构不能发生变化,这样就可以放弃 Rollup 了,因为 Rollup 号称将
所有小文件打包成一个大的 lib 或者是 bin 文件,就是 Rollup 是适合多人共同开发一个库,比较出名的 vue 就是使用 Rollup 打包工具进行打包的。
那么这里选择 gulp 的理由是:
第一,编译速度快;
• 第二,开发并不占用很多的电脑内存空间;
- 第三,保持文件结构不变,同时 gulp 还有 Rollup 同样的功能,可以生成 cjs 或者是 mjs 格式的 js 文件。
4. 说一下 koa-body 原理
原理
koa-body中间件作用是将 Post 等请求的请求体携带的数据解析到ctx.request.body中。基本原理是先利用type-is---ctx.js 函数根据请求的 content-type,判断出请求的数据类型,然后根据不同类型用co-body(请求体解析)和 formidable(数据类型是 multipart,文件上传解析)来解析,拿到解析结果以后放到ctx.request.body或者ctx.request.files里面。
使用方式
javascript
const Koa = require("koa");
const koaBody = require("koa-body");
const app = new Koa();
app.use(koaBody());
app.use((ctx) => {
ctx.body = `Request Body: ${JSON.stringify(ctx.request.body)}`
});
app.listen(3000);koa-body 先处理一堆参数,然后用type-is这个包判断请求的数据类型,然后根据不同类型用 co-body 和 formidable 来解析,取到解析结果以后放到 body 或者 files 里面。
type-is:引用了 mime-types 和 media-type,但是最终起作用的还是 mime-db 这个包,type-is 就是去匹配请求类型是什么(通过 content-type 来判断,而不是请求内容),我们也可以用 ctx.is 来判断请求的类型。在 type-is 里有一个函数叫 hasbody,get 请求在这个函数的判断下被认为没有 body,所以 get 请求获取到的结果都是空对象。
co-body:代码结构很简单,提供了json、form、text这三种格式的解析,主要依赖的是inflation和raw-body,处理了一些参数(主要是encoding和limit参数),用inflation和raw-body解析。inflation比较简单,根据content-encoding的类型,做不同的操作,如果是gzip和deflate,调用zlib.Unzip解压缩,如果是identity,直接返回输入值。raw-body,它做的事情是stream解析。
formidable:如果我们的数据类型是被 type-is 判断为 multipart,那么就会调用 formidable 来进行解析,formidable 本身也提供了很多种格式的解析,有 json,multipart,urlencoded 等。
5. master 挂了的话,pm2 如何处理?
Node.js 原生集群模式
工作原理
集群模块会创建一个 master 主线程,然后复制任意多份程序并启动,这叫做 工作线程。
工作线程通过 IPC 频道进行通信并且使用了 Round-robin algorithm 算法进行工作调度以此实现负载均衡。
Round-robin 调度策略主要是 master 主线程负责接收所有的连接并派发给下面的各个工作线程。
代码的例子:
javascript
var cluster = require("cluster");
var http = require("http");
var os = require("os");核数
javascript
var numCPUs = os.cpus().length;
if (cluster.isMaster) {
// Master:
// Let's fork as many workers as you have CPU cores
// 创建多个工作线程
for (var i = 0; i < numCPUs; ++i) {
cluster.fork();
}
} else {
// Worker:
// Let's spawn a HTTP server
// (Workers can share any TCP connection. In this case its a HTTP server)
}
21 http
22 .createServer(function (req, res) {
23 res.writeHead(200);
24 res.end("hello world");
25 })
26 .listen(8080);
27 }
28
你可以不受 CPU 核心限制的创建任意多个工作线程。
用原生方法有些麻烦而且还需要处理,如果某个工作线程挂掉了等额外的逻辑。
#### $ pm^{2} $ 的方式
pm2 内置了处理上述的逻辑,不用再写这么多繁琐的代码了
1 pm2 start app.js -i 4
2
3 -i 表示实例程序的个数---就是工作线程。如果i为∅表示,会根据当前CPU核心数创建4
5 这样的一行代码就可以啦!
1. 保持程序不中断运行 如果有任何工作线程意外挂掉了,pm2 会立即重启他们,当前你可以在任何时候重启,只需要 pm2 restart all
2. 实时调整集群数量 你可以使用命令 pm2 scale ``<appName>`` ``<n>`` 调整你的线程数量,如pm2 scale app +3 会在当前基础上加 3 个工作线程
3. 在生产环境中让程序永不中断 PM2 $ \underline{\text{reload ``<appName>``}} $ 命令一个接一个的重启工作线程,在新的工作线程启动后才结束老的工作线程。
这种方式可以保持你的 Node 程序始终是运行状态。即使在生产环境下部署了新的代码补。
也可以使用 gracefulReload 命令达到同样的目的,它不会立即结束工作线程,而是通过 IPC 向它发送关闭信号,这样它就可以关闭正在进行的连接,还可以在退出之前执行一些自定义任务,这种方式更优雅。
```javascript
process.on("message", function (msg) {
if (msg === "shutdown") {
close_all_connections();
delete_cache();
server.close();
process.exit(0);
}
}
);9 6. 上传文件的 content-type 是什么,node 如何拿到上传的文件内容(不适用第三方插件),文件内容是一次性传输过去的么?
上传文件的 content-type 使用 multipart/form-data
如何拿到上传的文件内容
http 模块的 createServer(request, response) 传入请求对象的 request,其实已经实现了 ReadableStream 接口,这个信息流可以被监听或者与其它流进行对接。我们可以监听 data 和 end 事件从而把数据给取出来。
文件的内容不是一次性的传过来,是以流的方式传输
获取到上传文件的内容代码如下:
javascript
const http = require("http");
let fileData = "";
http.createServer((request, response) => {
request
.on("error", (err) => {
console.error(err);
})
.on("data", (chunk) => {
fileData += chunk;
})
.on("end", () => {
console.log(fileData);
});
});
}).listen(8080);7. npm2 和 npm3+ 有什么区别?
npm2 所有项目依赖是 嵌套关系。而 npm3 为了改进嵌套过多、套路过深的情况,会将所有依赖放在第二层依赖中(所有的依赖只嵌套一次,彼此平行,也就是平铺的结构)
npm2 依赖安装的时候比较简单,直接按照包依赖的树形结构下载填充本地目录结构,也就是说每个包都会将该包的依赖组织到当前包所在的 node_modules 目录中。npm3 则会对依赖安装进行了改造,采用 扁平结构 的思路来组织依赖包的目录结构。具体的就是 npm install 的过程时:按照 package.json 里依赖的顺序依次解析,遇到新的包就把它放在第一级目录,后面如果遇到一级目录已经存在的包,会先判断该版本,如果版本一样则忽略,否则会按照 npm2 的方式依次挂在依赖包目录下。
8. 介绍下 pm2,pm2 依据什么重启服务?
pm2 是一个带有负载均衡功能的 node 应用的进程管理器。我们都知道 node.js 是单进程执行的,当程序出现错误死掉之后需要能够自动,这时候就需要 pm2 了,当然进程管理工具有很多,例如 forever 等等;
主要特性
• 启动多子进程,充分使用 CPU
• 子进程之间负载均衡
0 秒重启
• 界面友好
• 提供进程交互接口
依据什么重启服务
pm2 采用 $ \underline{\text{心跳检测}} $ 查看子进程是否处于活跃状态,每隔数秒向子进程发送心跳包,子进程如果不回复,那么调用 kill 杀死这个进程,然后再重新 $ \underline{\text{cluster.fork()}} $ 一个新的进程,子进程可以监听到错误事件,这个时候可以发送消息给主进程,请求杀死自己,并且主进程此时重新调用 $ \underline{\text{cluster.fork()}} $ 一个新的子进程
拥有的能力
• 日志管理:两种日志,pm2 系统日志与管理的进程日志,默认会把进程的控制台输出记录到日志
- 负载均衡:pm2 可以通过创建共享同一服务器端口的多个子进程来扩展你的应用程序。这样做还允许以零秒停机时间重新启动应用程序
• 终端监控:可以在终端中监控应用程序并检查应用程序运行状况(CPU 使用率、使用的内存、请求/分钟等等)
• SSH 部署:自动部署,避免逐个在所有服务器中进行 ssh
• 静态服务:支持静态服务器功能
- 支持开发调试模式:非后台运行,pm2-dev start
<appName>
常用命令
启动服务
javascript
pm2 start ``<script_file|config_file>`` [options] 启动指定应用• 启动一个 node 程序
1 pm2 start app.js 启动 app.js 应用
启动进程并指定应用的程序名
1 pm2 start app.js --name 程序名启动应用并设置 name
- 添加进程监视 监听模式启动,当文件发生变化,自动重启
1 pm2 start app.js --name 程序名 --watch(指定程序名的情况下) 2 3 pm2 start app.js --watch(未指定程序名的情况下)
列出所有进程
1 pm2 list 简写成 pm2 ls
从进程列表中删除进程
1 pm2 delete [appname] | id 2 3 pm2 delete app 指定进程名删除 4 5 pm2 delete 0 指定进程 id 删除 6 7 如果修改了应用配置行为,需要先删除应用,重新启动后方才会生效,如修改脚本入口文件
• 删除进程列表中所有进程
1 pm2 delete all (关闭并删除应用)
- 查看某个进程具体情况
1 pm2 describe app
查看进程的资源消耗情况
1 pm2 monit(监控各个应用进程 cpu 和 memory 使用情况)
• 重启进程
1 pm2 restart app.js 同时杀死并重启所有进程,短时间内服务不可用,生成环境后慎用2 3 pm2 restart all 重启所有进程 4 5 pm2 reload app.js 重新启动所有进行程,0 秒重启,始终保持至少一个进程在运行6 7 pm2 gracefulReload all 以群集横式重新加载所有应用程序8
这种方