Home > Article > Web Front-end > Summarize and share some front-end interview questions based on Node.js (with analysis)
This article summarizes and shares some front-end interview questions based on Node.js (with analysis). I hope it will be helpful to everyone!
Node.js is an open source and cross-platform JavaScript runtime environment . Run the V8 JavaScript engine (the core of Google Chrome) outside the browser, leveraging techniques such as event-driven, non-blocking, and asynchronous input and output models to improve performance. We can understand it as: Node.js is a server-side, non-blocking I/O, event-driven JavaScript running environment. [Recommended study: "nodejs Tutorial"]
To understand Node, there are several basic concepts: non-blocking asynchronous and event-driven.
Node.js is suitable for The value of I/O-intensive applications is that when the application is running at its limit, the CPU usage is still relatively low, and most of the time is spent doing I/O hard disk memory reading and writing operations. The disadvantages are as follows:
For the third point, the common solution is to use Nnigx reverse proxy, open multiple processes to bind multiple ports, or open multiple processes to listen to the same port .
After becoming familiar with the advantages and disadvantages of Nodejs, we can see that it is suitable for the following application scenarios:
The specific usage scenarios are as follows:
In browser JavaScript, window is a global object, while the global object in Nodejs is global.
In NodeJS, it is impossible to define a variable in the outermost layer, because all user code belongs to the current module and is only available in the current module, but it can be passed to outside the module. Therefore, in NodeJS, variables declared with var are not global variables and only take effect in the current module. The global object like the above is in the global scope, and any global variable, function, or object is an attribute value of the object.
Common global objects of Node include the following:
Class:BufferClass:Buffer can be used To handle binary and non-Unicode encoded data, the original data is stored in the Buffer class instantiation. Buffer is similar to an integer array. Memory is allocated to it in the V8 heap original storage space. Once a Buffer instance is created, the size cannot be changed.
processprocess represents the process object, providing information and control about the current process. Including during the execution of the node program, if a parameter needs to be passed, we need to obtain this parameter in the process built-in object. For example, we have the following file:
process.argv.forEach((val, index) => { console.log(`${index}: ${val}`); });
When we need to start a process, we can use the following command:
node index.js 参数...
consoleconsole主要用来打印stdout和stderr,最常用的比如日志输出:console.log
。清空控制台的命令为:console.clear
。如果需要打印函数的调用栈,可以使用命令console.trace
。
clearInterval、setIntervalsetInterval用于设置定时器,语法格式如下:
setInterval(callback, delay[, ...args])
clearInterval则用于清除定时器,callback每delay毫秒重复执行一次。
clearTimeout、setTimeout
和setInterval一样,setTimeout主要用于设置延时器,而clearTimeout则用于清除设置的延时器。
globalglobal是一个全局命名空间对象,前面讲到的process、console、setTimeout等可以放到global中,例如:
console.log(process === global.process) //输出true
除了系统提供的全局对象外,还有一些只是在模块中出现,看起来像全局变量,如下所示:
__dirname__dirname主要用于获取当前文件所在的路径,不包括后面的文件名。比如,在/Users/mjr
中运行 node example.js
,打印结果如下:
console.log(__dirname); // 打印: /Users/mjr
__filename__filename用于获取当前文件所在的路径和文件名称,包括后面的文件名称。比如,在/Users/mjr
中运行 node example.js
,打印的结果如下:
console.log(__filename);// 打印: /Users/mjr/example.js
exportsmodule.exports 用于导出一个指定模块所的内容,然后也可以使用require() 访问里面的内容。
exports.name = name;exports.age = age; exports.sayHello = sayHello;
requirerequire主要用于引入模块、 JSON、或本地文件, 可以从 node_modules 引入模块。可以使用相对路径引入本地模块或JSON文件,路径会根据__dirname定义的目录名或当前工作目录进行处理。
我们知道,进程计算机系统进行资源分配和调度的基本单位,是操作系统结构的基础,是线程的容器。当我们启动一个js文件,实际就是开启了一个服务进程,每个进程都拥有自己的独立空间地址、数据栈,像另一个进程无法访问当前进程的变量、数据结构,只有数据通信后,进程之间才可以数据共享。
process 对象是Node的一个全局变量,提供了有关当前 Node.js 进程的信息并对其进行控制。 由于JavaScript是一个单线程语言,所以通过node xxx启动一个文件后,只有一条主线程。
process的常见属性如下:
进程事件: process.on(‘uncaughtException’,cb) 捕获异常信息、 process.on(‘exit’,cb)进程推出监听
fs(filesystem)是文件系统模块,该模块提供本地文件的读写能力,基本上是POSIX文件操作命令的简单包装。可以说,所有与文件的操作都是通过fs核心模块来实现的。
使用之前,需要先导入fs模块,如下:
const fs = require('fs');
在计算机中,有关于文件的基础知识有如下一些:
针对文件所有者、文件所属组、其他用户进行权限分配,其中类型又分成读、写和执行,具备权限位4、2、1,不具备权限为0。如在linux查看文件权限位的命令如下:
drwxr-xr-x 1 PandaShen 197121 0 Jun 28 14:41 core -rw-r--r-- 1 PandaShen 197121 293 Jun 23 17:44 index.md
在开头前十位中,d为文件夹,-为文件,后九位就代表当前用户、用户所属组和其他用户的权限位,按每三位划分,分别代表读(r)、写(w)和执行(x),- 代表没有当前位对应的权限。
标识位代表着对文件的操作方式,如可读、可写、即可读又可写等等,如下表所示:
操作系统会为每个打开的文件分配一个名为文件描述符的数值标识,文件操作使用这些文件描述符来识别与追踪每个特定的文件。
Window 系统使用了一个不同但概念类似的机制来追踪资源,为方便用户,NodeJS 抽象了不同操作系统间的差异,为所有打开的文件分配了数值的文件描述符。
在 NodeJS 中,每操作一个文件,文件描述符是递增的,文件描述符一般从 3 开始,因为前面有 0、1、2三个比较特殊的描述符,分别代表 process.stdin(标准输入)、process.stdout(标准输出)和 process.stderr(错误输出)。
由于fs模块主要是操作文件的,所以常见的文件操作方法有如下一些:
常用的文件读取有readFileSync和readFile两个方法。其中,readFileSync表示同步读取,如下:
const fs = require("fs"); let buf = fs.readFileSync("1.txt"); let data = fs.readFileSync("1.txt", "utf8"); console.log(buf); // <Buffer 48 65 6c 6c 6f> console.log(data); // Hello
readFile为异步读取方法, readFile 与 readFileSync 的前两个参数相同,最后一个参数为回调函数,函数内有两个参数 err(错误)和 data(数据),该方法没有返回值,回调函数在读取文件成功后执行。
const fs = require("fs"); fs.readFile("1.txt", "utf8", (err, data) => { if(!err){ console.log(data); // Hello } });
文件写入需要用到writeFileSync和writeFile两个方法。writeFileSync表示同步写入,如下所示。
const fs = require("fs"); fs.writeFileSync("2.txt", "Hello world"); let data = fs.readFileSync("2.txt", "utf8"); console.log(data); // Hello world
writeFile表示异步写入,writeFile 与 writeFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 err(错误),回调函数在文件写入数据成功后执行。
const fs = require("fs"); fs.writeFile("2.txt", "Hello world", err => { if (!err) { fs.readFile("2.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } });
文件追加写入需要用到appendFileSync和appendFile两个方法。appendFileSync表示同步写入,如下。
const fs = require("fs"); fs.appendFileSync("3.txt", " world"); let data = fs.readFileSync("3.txt", "utf8");
appendFile表示异步追加写入,方法 appendFile 与 appendFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 err(错误),回调函数在文件追加写入数据成功后执行,如下所示。
const fs = require("fs"); fs.appendFile("3.txt", " world", err => { if (!err) { fs.readFile("3.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } });
创建目录主要有mkdirSync和mkdir两个方法。其中,mkdirSync为同步创建,参数为一个目录的路径,没有返回值,在创建目录的过程中,必须保证传入的路径前面的文件目录都存在,否则会抛出异常。
// 假设已经有了 a 文件夹和 a 下的 b 文件夹 fs.mkdirSync("a/b/c")
mkdir为异步创建,第二个参数为回调函数,如下所示。
fs.mkdir("a/b/c", err => { if (!err) console.log("创建成功"); });
流(Stream)是一种数据传输的手段,是一种端到端信息交换的方式,而且是有顺序的,是逐块读取数据、处理内容,用于顺序读取输入或写入输出。在Node中,Stream分成三部分:source、dest、pipe。
其中,在source和dest之间有一个连接的管道pipe,它的基本语法是source.pipe(dest),source和dest就是通过pipe连接,让数据从source流向dest,如下图所示:
在Node,流可以分成四个种类:
在Node的HTTP服务器模块中,request 是可读流,response 是可写流。对于fs 模块来说,能同时处理可读和可写文件流可读流和可写流都是单向的,比较容易理解。而Socket是双向的,可读可写。
在Node中,比较的常见的全双工通信就是websocket,因为发送方和接受方都是各自独立的方法,发送和接收都没有任何关系。
基本的使用方法如下:
const { Duplex } = require('stream'); const myDuplex = new Duplex({ read(size) { // ... }, write(chunk, encoding, callback) { // ... } });
流的常见使用场景有:
流一个常见的使用场景就是网络请求,比如使用stream流返回文件,res也是一个stream对象,通过pipe管道将文件数据返回。
const server = http.createServer(function (req, res) { const method = req.method; // get 请求 if (method === 'GET') { const fileName = path.resolve(__dirname, 'data.txt'); let stream = fs.createReadStream(fileName); stream.pipe(res); } }); server.listen(8080);
文件的读取也是流操作,创建一个可读数据流readStream,一个可写数据流writeStream,通过pipe管道把数据流转过去。
const fs = require('fs') const path = require('path') // 两个文件名 const fileName1 = path.resolve(__dirname, 'data.txt') const fileName2 = path.resolve(__dirname, 'data-bak.txt') // 读取文件的 stream 对象 const readStream = fs.createReadStream(fileName1) // 写入文件的 stream 对象 const writeStream = fs.createWriteStream(fileName2) // 通过 pipe执行拷贝,数据流转 readStream.pipe(writeStream) // 数据读取完成监听,即拷贝完成 readStream.on('end', function () { console.log('拷贝完成') })
另外,一些打包工具,Webpack和Vite等都涉及很多流的操作。
Node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。
当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop),其运行原理如下图所示。
从左到右,从上到下,Node.js 被分为了四层,分别是 应用层、V8引擎层、Node API层 和 LIBUV层。
在Node中,我们所说的事件循环是基于libuv实现的,libuv是一个多平台的专注于异步IO的库。上图的EVENT_QUEUE 给人看起来只有一个队列,但事实上EventLoop存在6个阶段,每个阶段都有对应的一个先进先出的回调队列。
事件循环一共可以分成了六个阶段,如下图所示。
每个阶段对应一个队列,当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段,如下图所示。
前文说过,Node采用了事件驱动机制,而EventEmitter 就是Node实现事件驱动的基础。在EventEmitter的基础上,Node 几乎所有的模块都继承了这个类,这些模块拥有了自己的事件,可以绑定、触发监听器,实现了异步操作。
Node.js 里面的许多对象都会分发事件,比如 fs.readStream 对象会在文件被打开的时候触发一个事件,这些产生事件的对象都是 events.EventEmitter 的实例,用于将一个或多个函数绑定到命名事件上。
Node的events模块只提供了一个EventEmitter类,这个类实现了Node异步事件驱动架构的基本模式:观察者模式。
在这种模式中,被观察者(主体)维护着一组其他对象派来(注册)的观察者,有新的对象对主体感兴趣就注册观察者,不感兴趣就取消订阅,主体有更新会依次通知观察者,使用方式如下。
const EventEmitter = require('events') class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter() function callback() { console.log('触发了event事件!') } myEmitter.on('event', callback) myEmitter.emit('event') myEmitter.removeListener('event', callback);
在上面的代码中,我们通过实例对象的on方法注册一个名为event的事件,通过emit方法触发该事件,而removeListener用于取消事件的监听。
除了上面介绍的一些方法外,其他常用的方法还有如下一些:
EventEmitter其实是一个构造函数,内部存在一个包含所有事件的对象。
class EventEmitter { constructor() { this.events = {}; } }
其中,events存放的监听事件的函数的结构如下:
{ "event1": [f1,f2,f3], "event2": [f4,f5], ... }
然后,开始一步步实现实例方法,首先是emit,第一个参数为事件的类型,第二个参数开始为触发事件函数的参数,实现如下:
emit(type, ...args) { this.events[type].forEach((item) => { Reflect.apply(item, this, args); }); }
实现了emit方法之后,然后依次实现on、addListener、prependListener这三个实例方法,它们都是添加事件监听触发函数的。
on(type, handler) { if (!this.events[type]) { this.events[type] = []; } this.events[type].push(handler); } addListener(type,handler){ this.on(type,handler) } prependListener(type, handler) { if (!this.events[type]) { this.events[type] = []; } this.events[type].unshift(handler); }
移除事件监听,可以使用方法removeListener/on。
removeListener(type, handler) { if (!this.events[type]) { return; } this.events[type] = this.events[type].filter(item => item !== handler); } off(type,handler){ this.removeListener(type,handler) }
实现once方法, 再传入事件监听处理函数的时候进行封装,利用闭包的特性维护当前状态,通过fired属性值判断事件函数是否执行过。
once(type, handler) { this.on(type, this._onceWrap(type, handler, this)); } _onceWrap(type, handler, target) { const state = { fired: false, handler, type , target}; const wrapFn = this._onceWrapper.bind(state); state.wrapFn = wrapFn; return wrapFn; } _onceWrapper(...args) { if (!this.fired) { this.fired = true; Reflect.apply(this.handler, this.target, args); this.target.off(this.type, this.wrapFn); } }
下面是完成的测试代码:
class EventEmitter { constructor() { this.events = {}; } on(type, handler) { if (!this.events[type]) { this.events[type] = []; } this.events[type].push(handler); } addListener(type,handler){ this.on(type,handler) } prependListener(type, handler) { if (!this.events[type]) { this.events[type] = []; } this.events[type].unshift(handler); } removeListener(type, handler) { if (!this.events[type]) { return; } this.events[type] = this.events[type].filter(item => item !== handler); } off(type,handler){ this.removeListener(type,handler) } emit(type, ...args) { this.events[type].forEach((item) => { Reflect.apply(item, this, args); }); } once(type, handler) { this.on(type, this._onceWrap(type, handler, this)); } _onceWrap(type, handler, target) { const state = { fired: false, handler, type , target}; const wrapFn = this._onceWrapper.bind(state); state.wrapFn = wrapFn; return wrapFn; } _onceWrapper(...args) { if (!this.fired) { this.fired = true; Reflect.apply(this.handler, this.target, args); this.target.off(this.type, this.wrapFn); } } }
中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的。 在Node中,中间件主要是指封装http请求细节处理的方法。例如,在express、koa等web框架中,中间件的本质为一个回调函数,参数包含请求对象、响应对象和执行下一个中间件的函数,架构示意图如下。
通常,在这些中间件函数中,我们可以执行业务逻辑代码,修改请求和响应对象、返回响应数据等操作。
Koa是基于Node当前比较流行的web框架,本身支持的功能并不多,功能都可以通过中间件拓展实现。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助开发者快速而愉快地编写服务端应用程序。
Koa 中间件采用的是洋葱圈模型,每次执行下一个中间件都传入两个参数:
通过前面的介绍,我们知道了Koa 中间件本质上就是一个函数,可以是 async 函数,也可以是普通函数。下面就针对koa进行中间件的封装:
// async 函数 app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); // 普通函数 app.use((ctx, next) => { const start = Date.now(); return next().then(() => { const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); });
当然,我们还可以通过中间件封装http请求过程中几个常用的功能:
token校验
module.exports = (options) => async (ctx, next) { try { // 获取 token const token = ctx.header.authorization if (token) { try { // verify 函数验证 token,并获取用户相关信息 await verify(token) } catch (err) { console.log(err) } } // 进入下一个中间件 await next() } catch (err) { console.log(err) } }
日志模块
const fs = require('fs') module.exports = (options) => async (ctx, next) => { const startTime = Date.now() const requestTime = new Date() await next() const ms = Date.now() - startTime; let logout = `${ctx.request.ip} -- ${requestTime} -- ${ctx.method} -- ${ctx.url} -- ${ms}ms`; // 输出日志文件 fs.appendFileSync('./log.txt', logout + '\n') }
Koa存在很多第三方的中间件,如koa-bodyparser、koa-static等。
koa-bodyparserkoa-bodyparser 中间件是将我们的 post 请求和表单提交的查询字符串转换成对象,并挂在 ctx.request.body 上,方便我们在其他中间件或接口处取值。
// 文件:my-koa-bodyparser.js const querystring = require("querystring"); module.exports = function bodyParser() { return async (ctx, next) => { await new Promise((resolve, reject) => { // 存储数据的数组 let dataArr = []; // 接收数据 ctx.req.on("data", data => dataArr.push(data)); // 整合数据并使用 Promise 成功 ctx.req.on("end", () => { // 获取请求数据的类型 json 或表单 let contentType = ctx.get("Content-Type"); // 获取数据 Buffer 格式 let data = Buffer.concat(dataArr).toString(); if (contentType === "application/x-www-form-urlencoded") { // 如果是表单提交,则将查询字符串转换成对象赋值给 ctx.request.body ctx.request.body = querystring.parse(data); } else if (contentType === "applaction/json") { // 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body ctx.request.body = JSON.parse(data); } // 执行成功的回调 resolve(); }); }); // 继续向下执行 await next(); }; };
koa-statickoa-static 中间件的作用是在服务器接到请求时,帮我们处理静态文件,比如。
const fs = require("fs"); const path = require("path"); const mime = require("mime"); const { promisify } = require("util"); // 将 stat 和 access 转换成 Promise const stat = promisify(fs.stat); const access = promisify(fs.access) module.exports = function (dir) { return async (ctx, next) => { // 将访问的路由处理成绝对路径,这里要使用 join 因为有可能是 / let realPath = path.join(dir, ctx.path); try { // 获取 stat 对象 let statObj = await stat(realPath); // 如果是文件,则设置文件类型并直接响应内容,否则当作文件夹寻找 index.html if (statObj.isFile()) { ctx.set("Content-Type", `${mime.getType()};charset=utf8`); ctx.body = fs.createReadStream(realPath); } else { let filename = path.join(realPath, "index.html"); // 如果不存在该文件则执行 catch 中的 next 交给其他中间件处理 await access(filename); // 存在设置文件类型并响应内容 ctx.set("Content-Type", "text/html;charset=utf8"); ctx.body = fs.createReadStream(filename); } } catch (e) { await next(); } } }
总的来说,在实现中间件时候,单个中间件应该足够简单,职责单一,中间件的代码编写应该高效,必要的时候通过缓存重复获取数据。
JWT(JSON Web Token),本质就是一个字符串书写规范,作用是用来在用户和服务器之间传递安全可靠的,如下图。
在目前前后端分离的开发过程中,使用token鉴权机制用于身份验证是最常见的方案,流程如下:
Token,分成了三部分,头部(Header)、载荷(Payload)、签名(Signature),并以.
进行拼接。其中头部和载荷都是以JSON格式存放数据,只是进行了编码,示意图如下。
每个JWT都会带有头部信息,这里主要声明使用的算法。声明算法的字段名为alg,同时还有一个typ的字段,默认JWT即可。以下示例中算法为HS256:
{ "alg": "HS256", "typ": "JWT" }
因为JWT是字符串,所以我们还需要对以上内容进行Base64编码,编码后字符串如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
载荷即消息体,这里会存放实际的内容,也就是Token的数据声明,例如用户的id和name,默认情况下也会携带令牌的签发时间iat,通过还可以设置过期时间,如下:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
同样进行Base64编码后,字符串如下:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
签名是对头部和载荷内容进行签名,一般情况,设置一个secretKey,对前两个的结果进行HMACSHA25算法,公式如下:
Signature = HMACSHA256(base64Url(header)+.+base64Url(payload),secretKey)
因此,就算前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名也是不一致的。
通常,Token的使用分成了两部分:生成token和校验token。
借助第三方库jsonwebtoken,通过jsonwebtoken 的 sign 方法生成一个 token。sign有三个参数:
下面是一个前端生成token的例子:
const crypto = require("crypto"), jwt = require("jsonwebtoken"); // TODO:使用数据库 // 这里应该是用数据库存储,这里只是演示用 let userList = []; class UserController { // 用户登录 static async login(ctx) { const data = ctx.request.body; if (!data.name || !data.password) { return ctx.body = { code: "000002", message: "参数不合法" } } const result = userList.find(item => item.name === data.name && item.password === crypto.createHash('md5').update(data.password).digest('hex')) if (result) { // 生成token const token = jwt.sign( { name: result.name }, "test_token", // secret { expiresIn: 60 * 60 } // 过期时间:60 * 60 s ); return ctx.body = { code: "0", message: "登录成功", data: { token } }; } else { return ctx.body = { code: "000002", message: "用户名或密码错误" }; } } } module.exports = UserController;
在前端接收到token后,一般情况会通过localStorage进行缓存,然后将token放到HTTP 请求头Authorization 中,关于Authorization 的设置,前面需要加上 Bearer ,注意后面带有空格,如下。
axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); config.headers.common['Authorization'] = 'Bearer ' + token; // 留意这里的 Authorization return config; })
首先,我们需要使用 koa-jwt 中间件进行验证,方式比较简单,在路由跳转前校验即可,如下。
app.use(koajwt({ secret: 'test_token' }).unless({ // 配置白名单 path: [/\/api\/register/, /\/api\/login/] }))
使用koa-jwt中间件进行校验时,需要注意以下几点:
获取用户token信息的方法如下:
router.get('/api/userInfo',async (ctx,next) =>{ const authorization = ctx.header.authorization // 获取jwt const token = authorization.replace('Beraer ','') const result = jwt.verify(token,'test_token') ctx.body = result }
注意:上述的HMA256加密算法为单秘钥的形式,一旦泄露后果非常的危险。
在分布式系统中,每个子系统都要获取到秘钥,那么这个子系统根据该秘钥可以发布和验证令牌,但有些服务器只需要验证令牌。这时候可以采用非对称加密,利用私钥发布令牌,公钥验证令牌,加密算法可以选择RS256等非对称算法。
除此之外,JWT鉴权还需要注意以下几点:
Node作为一门服务端语言,性能方面尤为重要,其衡量指标一般有如下几点:
对于CPU的指标,主要关注如下两点:
这两个指标都是用来评估系统当前CPU的繁忙程度的量化指标。Node应用一般不会消耗很多的CPU,如果CPU占用率高,则表明应用存在很多同步操作,导致异步任务回调被阻塞。
内存是一个非常容易量化的指标。 内存占用率是评判一个系统的内存瓶颈的常见指标。 对于Node来说,内部内存堆栈的使用状态也是一个可以量化的指标,可以使用下面的代码来获取内存的相关数据:
// /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占用系统内存的比例 } } }
在Node中,一个进程的最大内存容量为1.5GB,因此在实际使用时请合理控制内存的使用。
硬盘的 IO 开销是非常昂贵的,硬盘 IO 花费的 CPU 时钟周期是内存的 164000 倍。内存 IO 比磁盘 IO 快非常多,所以使用内存缓存数据是有效的优化方法。常用的工具如 redis、memcached 等。
并且,并不是所有数据都需要缓存,访问频率高,生成代价比较高的才考虑是否缓存,也就是说影响你性能瓶颈的考虑去缓存,并且而且缓存还有缓存雪崩、缓存穿透等问题要解决。
关于性能方面的监控,一般情况都需要借助工具来实现,比如Easy-Monitor、阿里Node性能平台等。
这里采用Easy-Monitor 2.0,其是轻量级的 Node.js 项目内核性能监控 + 分析工具,在默认模式下,只需要在项目入口文件 require 一次,无需改动任何业务代码即可开启内核级别的性能监控分析。
Easy-Monitor 的使用也比较简单,在项目入口文件中按照如下方式引入。
const easyMonitor = require('easy-monitor'); easyMonitor('项目名称');
打开你的浏览器,访问 http://localhost:12333 ,即可看到进程界面,更详细的内容请参考官网
关于Node的性能优化的方式有如下几个:
每个版本的性能提升主要来自于两个方面:
在Node中,很多对象都实现了流,对于一个大文件可以通过流的形式发送,不需要将其完全读入内存。
const http = require('http'); const fs = require('fs'); // 错误方式 http.createServer(function (req, res) { fs.readFile(__dirname + '/data.txt', function (err, data) { res.end(data); }); }); // 正确方式 http.createServer(function (req, res) { const stream = fs.createReadStream(__dirname + '/data.txt'); stream.pipe(res); });
合并查询,将多次查询合并一次,减少数据库的查询次数。
// 错误方式 for user_id in userIds let account = user_account.findOne(user_id) // 正确方式 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 中,主要将内存分为新生代和老生代两代:
若新生代内存空间不够,直接分配到老生代。通过减少内存占用,可以提高服务器的性能。如果有内存泄露,也会导致大量的对象存储到老生代中,服务器性能会大大降低,比如下面的例子。
const buffer = fs.readFileSync(__dirname + '/source/index.htm'); app.use( mount('/', async (ctx) => { ctx.status = 200; ctx.type = 'html'; ctx.body = buffer; leak.push(fs.readFileSync(__dirname + '/source/index.htm')); }) ); const leak = [];
当leak的内存非常大的时候,就有可能造成内存泄露,应当避免这样的操作。
减少内存使用,可以明显的提高服务性能。而节省内存最好的方式是使用池,其将频用、可复用对象存储起来,减少创建和销毁操作。例如有个图片请求接口,每次请求,都需要用到类。若每次都需要重新new这些类,并不是很合适,在大量请求时,频繁创建和销毁这些类,造成内存抖动。而使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中,从而避免重读的初始化操作,从而提高框架的性能。
更多编程相关知识,请访问:编程视频!!
The above is the detailed content of Summarize and share some front-end interview questions based on Node.js (with analysis). For more information, please follow other related articles on the PHP Chinese website!