您好,欢迎访问一九零五行业门户网

总结分享一些基于Node.js的前端面试题(附解析)

本篇文章给大家总结分享一些基于node.js的前端面试题(附解析),希望对大家有所帮助!
一、node基础概念1.1 node是什么node.js 是一个开源与跨平台的 javascript 运行时环境。在浏览器外运行 v8 javascript 引擎(google chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能。我们可以理解为:node.js 就是一个服务器端的、非阻塞式i/o的、事件驱动的javascript运行环境。【推荐学习:《nodejs 教程》】
理解node,有几个基础的概念:非阻塞异步和事件驱动。
非阻塞异步: nodejs采用了非阻塞型i/o机制,在做i/o操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执行操作。例如,在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。事件驱动: 事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数。比如,读取一个文件,文件读取完毕后,就会触发对应的状态,然后通过对应的回调函数来进行处理。
1.2 node的应用场景及存在的缺点1.2.1 优缺点node.js适合用于i/o密集型应用,值的是应用在运行极限时,cpu占用率仍然比较低,大部分时间是在做 i/o硬盘内存读写操作。缺点如下:
不适合cpu密集型应用只支持单核cpu,不能充分利用cpu可靠性低,一旦代码某个环节崩溃,整个系统都崩溃对于第三点,常用的解决方案是,使用nnigx反向代理,开多个进程绑定多个端口,或者开多个进程监听同一个端口。
1.2.1 应用场景在熟悉了nodejs的优点和弊端后,我们可以看到它适合以下的应用场景:
善于i/o,不善于计算。因为nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个线程。大量并发的i/o,应用程序内部并不需要进行非常复杂的处理。与 wesocket 配合,开发长连接的实时交互应用程序。具体的使用场景如下:
用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序。基于web、canvas等多人联网游戏。基于web的多人实时聊天客户端、聊天室、图文直播。单页面浏览器应用程序。操作数据库、为前端和移动端提供基于json的api。二、node全部对象在浏览器 javascript 中,window 是全局对象, 而 nodejs 中的全局对象则是 global。
在nodejs里,是不可能在最外层定义一个变量,因为所有的用户代码都是当前模块的,只在当前模块里可用,但可以通过exports对象的使用将其传递给模块外部。所以,在nodejs中,用var声明的变量并不属于全局的变量,只在当前模块生效。像上述的global全局对象则在全局作用域中,任何全局变量、函数、对象都是该对象的一个属性值。
2.1 常见全局对象node常见的全局对象有如下一些:
class:bufferprocessconsoleclearinterval、setintervalcleartimeout、settimeoutglobalclass:bufferclass:buffer可以用来处理二进制以及非unicode编码的数据,在buffer类实例化中存储了原始数据。buffer类似于一个整数数组,在v8堆原始存储空间给它分配了内存,一旦创建了buffer实例,则无法改变大小。
processprocess表示进程对象,提供有关当前过程的信息和控制。包括在执行node程序的过程中,如果需要传递参数,我们想要获取这个参数需要在process内置对象中。比如,我们有如下一个文件:
process.argv.foreach((val, index) => { console.log(`${index}: ${val}`);});
当我们需要启动一个进程时,可以使用下面的命令:
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
2.2 模块中的全局对象除了系统提供的全局对象外,还有一些只是在模块中出现,看起来像全局变量,如下所示:
__dirname__filenameexportsmodulerequire__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定义的目录名或当前工作目录进行处理。
三、谈谈对process的理解3.1 基本概念我们知道,进程计算机系统进行资源分配和调度的基本单位,是操作系统结构的基础,是线程的容器。当我们启动一个js文件,实际就是开启了一个服务进程,每个进程都拥有自己的独立空间地址、数据栈,像另一个进程无法访问当前进程的变量、数据结构,只有数据通信后,进程之间才可以数据共享。
process 对象是node的一个全局变量,提供了有关当前 node.js 进程的信息并对其进行控制。由于javascript是一个单线程语言,所以通过node xxx启动一个文件后,只有一条主线程。
3.2 常用属性和方法process的常见属性如下:
process.env:环境变量,例如通过 `process.env.node_env 获取不同环境项目配置信息process.nexttick:这个在谈及 eventloop 时经常为会提到process.pid:获取当前进程idprocess.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:用于指定进程名称,有的时候需要给进程指定一个名称四、谈谈你对fs模块的理解4.1 fs是什么fs(filesystem)是文件系统模块,该模块提供本地文件的读写能力,基本上是posix文件操作命令的简单包装。可以说,所有与文件的操作都是通过fs核心模块来实现的。
使用之前,需要先导入fs模块,如下:
const fs = require('fs');
4.2 文件基础知识在计算机中,有关于文件的基础知识有如下一些:
权限位 mode标识位 flag文件描述为 fd4.2.1 权限位 mode
针对文件所有者、文件所属组、其他用户进行权限分配,其中类型又分成读、写和执行,具备权限位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),- 代表没有当前位对应的权限。
4.2.2 标识位标识位代表着对文件的操作方式,如可读、可写、即可读又可写等等,如下表所示:
4.2.3 文件描述 fd操作系统会为每个打开的文件分配一个名为文件描述符的数值标识,文件操作使用这些文件描述符来识别与追踪每个特定的文件。
window 系统使用了一个不同但概念类似的机制来追踪资源,为方便用户,nodejs 抽象了不同操作系统间的差异,为所有打开的文件分配了数值的文件描述符。
在 nodejs 中,每操作一个文件,文件描述符是递增的,文件描述符一般从 3 开始,因为前面有 0、1、2三个比较特殊的描述符,分别代表 process.stdin(标准输入)、process.stdout(标准输出)和 process.stderr(错误输出)。
4.3 常用方法由于fs模块主要是操作文件的,所以常见的文件操作方法有如下一些:
文件读取文件写入文件追加写入文件拷贝创建目录4.3.1 文件读取常用的文件读取有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
第一个参数为读取文件的路径或文件描述符。第二个参数为 options,默认值为 null,其中有 encoding(编码,默认为 null)和 flag(标识位,默认为 r),也可直接传入 encoding。readfile为异步读取方法, readfile 与 readfilesync 的前两个参数相同,最后一个参数为回调函数,函数内有两个参数 err(错误)和 data(数据),该方法没有返回值,回调函数在读取文件成功后执行。
const fs = require("fs");fs.readfile("1.txt", "utf8", (err, data) => { if(!err){ console.log(data); // hello }});
4.3.2 文件写入文件写入需要用到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
第一个参数为写入文件的路径或文件描述符。第二个参数为写入的数据,类型为 string 或 buffer。第三个参数为 options,默认值为 null,其中有 encoding(编码,默认为 utf8)、 flag(标识位,默认为 w)和 mode(权限位,默认为 0o666),也可直接传入 encoding。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 }); }});
4.3.3 文件追加写入文件追加写入需要用到appendfilesync和appendfile两个方法。appendfilesync表示同步写入,如下。
const fs = require("fs");fs.appendfilesync("3.txt", " world");let data = fs.readfilesync("3.txt", "utf8");
第一个参数为写入文件的路径或文件描述符。第二个参数为写入的数据,类型为 string 或 buffer。第三个参数为 options,默认值为 null,其中有 encoding(编码,默认为 utf8)、 flag(标识位,默认为 a)和 mode(权限位,默认为 0o666),也可直接传入 encoding。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 }); }});
4.3.4 创建目录创建目录主要有mkdirsync和mkdir两个方法。其中,mkdirsync为同步创建,参数为一个目录的路径,没有返回值,在创建目录的过程中,必须保证传入的路径前面的文件目录都存在,否则会抛出异常。
// 假设已经有了 a 文件夹和 a 下的 b 文件夹fs.mkdirsync("a/b/c")
mkdir为异步创建,第二个参数为回调函数,如下所示。
fs.mkdir("a/b/c", err => { if (!err) console.log("创建成功");});
五、谈谈你对stream 的理解5.1 基本概念流(stream)是一种数据传输的手段,是一种端到端信息交换的方式,而且是有顺序的,是逐块读取数据、处理内容,用于顺序读取输入或写入输出。在node中,stream分成三部分:source、dest、pipe。
其中,在source和dest之间有一个连接的管道pipe,它的基本语法是source.pipe(dest),source和dest就是通过pipe连接,让数据从source流向dest,如下图所示:
5.2 流的分类在node,流可以分成四个种类:
可写流:可写入数据的流,例如 fs.createwritestream() 可以使用流将数据写入文件。可读流: 可读取数据的流,例如fs.createreadstream() 可以从文件读取内容。双工流: 既可读又可写的流,例如 net.socket。转换流: 可以在数据写入和读取时修改或转换数据的流。例如,在文件压缩操作中,可以向文件写入压缩数据,并从文件中读取解压数据。在node的http服务器模块中,request 是可读流,response 是可写流。对于fs 模块来说,能同时处理可读和可写文件流可读流和可写流都是单向的,比较容易理解。而socket是双向的,可读可写。
5.2.1 双工流在node中,比较的常见的全双工通信就是websocket,因为发送方和接受方都是各自独立的方法,发送和接收都没有任何关系。
基本的使用方法如下:
const { duplex } = require('stream');const myduplex = new duplex({ read(size) { // ... }, write(chunk, encoding, callback) { // ... }});
5.3 使用场景流的常见使用场景有:
get请求返回文件给客户端文件操作一些打包工具的底层操作5.3.1 网络请求流一个常见的使用场景就是网络请求,比如使用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);
5.3.2 文件操作文件的读取也是流操作,创建一个可读数据流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等都涉及很多流的操作。
六、事件循环机制6.1 什么是浏览器事件循环node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 i/o 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 i/o 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。
当线程中的 i/o 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (event loop),其运行原理如下图所示。
从左到右,从上到下,node.js 被分为了四层,分别是 应用层、v8引擎层、node api层 和 libuv层。
应用层: 即 javascript 交互层,常见的就是 node.js 的模块,比如 http,fsv8引擎层: 即利用 v8 引擎来解析javascript 语法,进而和下层 api 交互node api层: 为上层模块提供系统调用,一般是由 c 语言来实现,和操作系统进行交互 。libuv层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 node.js 实现异步的核心 。在node中,我们所说的事件循环是基于libuv实现的,libuv是一个多平台的专注于异步io的库。上图的event_queue 给人看起来只有一个队列,但事实上eventloop存在6个阶段,每个阶段都有对应的一个先进先出的回调队列。
6.2 事件循环的六个阶段事件循环一共可以分成了六个阶段,如下图所示。
timers阶段:此阶段主要执行timer(settimeout、setinterval)的回调。i/o事件回调阶段(i/o callbacks):执行延迟到下一个循环迭代的 i/o 回调,即上一轮循环中未被执行的一些i/o回调。闲置阶段(idle、prepare):仅系统内部使用。轮询阶段(poll):检索新的 i/o 事件;执行与 i/o 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setimmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。检查阶段(check):setimmediate() 回调函数在这里执行关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on('close', ...)每个阶段对应一个队列,当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段,如下图所示。
七、eventemitter7.1 基本概念前文说过,node采用了事件驱动机制,而eventemitter 就是node实现事件驱动的基础。在eventemitter的基础上,node 几乎所有的模块都继承了这个类,这些模块拥有了自己的事件,可以绑定、触发监听器,实现了异步操作。
node.js 里面的许多对象都会分发事件,比如 fs.readstream 对象会在文件被打开的时候触发一个事件,这些产生事件的对象都是 events.eventemitter 的实例,用于将一个或多个函数绑定到命名事件上。
7.2 基本使用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用于取消事件的监听。
除了上面介绍的一些方法外,其他常用的方法还有如下一些:
emitter.addlistener/on(eventname, listener) :添加类型为 eventname 的监听事件到事件数组尾部。emitter.prependlistener(eventname, listener):添加类型为 eventname 的监听事件到事件数组头部。emitter.emit(eventname[, ...args]):触发类型为 eventname 的监听事件。emitter.removelistener/off(eventname, listener):移除类型为 eventname 的监听事件。emitter.once(eventname, listener):添加类型为 eventname 的监听事件,以后只能执行一次并删除。emitter.removealllisteners([eventname]): 移除全部类型为 eventname 的监听事件。7.3 实现原理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); } }}
八、中间件8.1 基本概念中间件(middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的。在node中,中间件主要是指封装http请求细节处理的方法。例如,在express、koa等web框架中,中间件的本质为一个回调函数,参数包含请求对象、响应对象和执行下一个中间件的函数,架构示意图如下。
通常,在这些中间件函数中,我们可以执行业务逻辑代码,修改请求和响应对象、返回响应数据等操作。
8.2 koakoa是基于node当前比较流行的web框架,本身支持的功能并不多,功能都可以通过中间件拓展实现。 koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助开发者快速而愉快地编写服务端应用程序。
koa 中间件采用的是洋葱圈模型,每次执行下一个中间件都传入两个参数:
ctx :封装了request 和 response 的变量next :进入下一个要执行的中间件的函数通过前面的介绍,我们知道了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等。
8.3 koa中间件koa-bodyparserkoa-bodyparser 中间件是将我们的 post 请求和表单提交的查询字符串转换成对象,并挂在 ctx.request.body 上,方便我们在其他中间件或接口处取值。
// 文件:my-koa-bodyparser.jsconst 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 转换成 promiseconst 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鉴权9.1 jwt是什么jwt(json web token),本质就是一个字符串书写规范,作用是用来在用户和服务器之间传递安全可靠的,如下图。
在目前前后端分离的开发过程中,使用token鉴权机制用于身份验证是最常见的方案,流程如下:
服务器当验证用户账号和密码正确的时候,给用户颁发一个令牌,这个令牌作为后续用户访问一些接口的凭证。后续访问会根据这个令牌判断用户时候有权限进行访问。token,分成了三部分,头部(header)、载荷(payload)、签名(signature),并以.进行拼接。其中头部和载荷都是以json格式存放数据,只是进行了编码,示意图如下。
9.1.1 header每个jwt都会带有头部信息,这里主要声明使用的算法。声明算法的字段名为alg,同时还有一个typ的字段,默认jwt即可。以下示例中算法为hs256:
{ "alg": "hs256", "typ": "jwt" }
因为jwt是字符串,所以我们还需要对以上内容进行base64编码,编码后字符串如下:
eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9
9.1.2 payload载荷即消息体,这里会存放实际的内容,也就是token的数据声明,例如用户的id和name,默认情况下也会携带令牌的签发时间iat,通过还可以设置过期时间,如下:
{ "sub": "1234567890", "name": "john doe", "iat": 1516239022}
同样进行base64编码后,字符串如下:
eyjzdwiioiixmjm0nty3odkwiiwibmftzsi6ikpvag4grg9liiwiawf0ijoxnte2mjm5mdiyfq
9.1.3 signature签名是对头部和载荷内容进行签名,一般情况,设置一个secretkey,对前两个的结果进行hmacsha25算法,公式如下:
signature = hmacsha256(base64url(header)+.+base64url(payload),secretkey)
因此,就算前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名也是不一致的。
9.2 设计实现通常,token的使用分成了两部分:生成token和校验token。
生成token:登录成功的时候,颁发token。验证token:访问某些资源或者接口时,验证token。9.2.1 生成 token借助第三方库jsonwebtoken,通过jsonwebtoken 的 sign 方法生成一个 token。sign有三个参数:
第一个参数指的是 payload。第二个是秘钥,服务端特有。第三个参数是 option,可以定义 token 过期时间。下面是一个前端生成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;})
9.2.2 校验token首先,我们需要使用 koa-jwt 中间件进行验证,方式比较简单,在路由跳转前校验即可,如下。
app.use(koajwt({ secret: 'test_token'}).unless({ // 配置白名单 path: [/\/api\/register/, /\/api\/login/]}))
使用koa-jwt中间件进行校验时,需要注意以下几点:
secret 必须和 sign 时候保持一致。可以通过 unless 配置接口白名单,也就是哪些 url 可以不用经过校验,像登陆/注册都可以不用校验。校验的中间件需要放在需要校验的路由前面,无法对前面的 url 进行校验。获取用户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鉴权还需要注意以下几点:
payload部分仅仅是进行简单编码,所以只能用于存储逻辑必需的非敏感信息。需要保护好加密密钥,一旦泄露后果不堪设想。为避免token被劫持,最好使用https协议。十、node性能监控与优化10.1 node优化点node作为一门服务端语言,性能方面尤为重要,其衡量指标一般有如下几点:
cpu内存i/o网络10.1.1 cpu对于cpu的指标,主要关注如下两点:
cpu负载:在某个时间段内,占用以及等待cpu的进程总数。cpu使用率:cpu时间占用状况,等于 1 - 空闲cpu时间(idle time) / cpu总时间。这两个指标都是用来评估系统当前cpu的繁忙程度的量化指标。node应用一般不会消耗很多的cpu,如果cpu占用率高,则表明应用存在很多同步操作,导致异步任务回调被阻塞。
10.1.2 内存指标内存是一个非常容易量化的指标。 内存占用率是评判一个系统的内存瓶颈的常见指标。 对于node来说,内部内存堆栈的使用状态也是一个可以量化的指标,可以使用下面的代码来获取内存的相关数据:
// /app/lib/memory.jsconst 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占用系统内存的比例 } }}
rss:表示node进程占用的内存总量。heaptotal:表示堆内存的总量。heapused:实际堆内存的使用量。external :外部程序的内存使用量,包含node核心的c++程序的内存使用量。在node中,一个进程的最大内存容量为1.5gb,因此在实际使用时请合理控制内存的使用。
10.13 磁盘 i/o硬盘的 io 开销是非常昂贵的,硬盘 io 花费的 cpu 时钟周期是内存的 164000 倍。内存 io 比磁盘 io 快非常多,所以使用内存缓存数据是有效的优化方法。常用的工具如 redis、memcached 等。
并且,并不是所有数据都需要缓存,访问频率高,生成代价比较高的才考虑是否缓存,也就是说影响你性能瓶颈的考虑去缓存,并且而且缓存还有缓存雪崩、缓存穿透等问题要解决。
10.2 如何监控关于性能方面的监控,一般情况都需要借助工具来实现,比如easy-monitor、阿里node性能平台等。
这里采用easy-monitor 2.0,其是轻量级的 node.js 项目内核性能监控 + 分析工具,在默认模式下,只需要在项目入口文件 require 一次,无需改动任何业务代码即可开启内核级别的性能监控分析。
easy-monitor 的使用也比较简单,在项目入口文件中按照如下方式引入。
const easymonitor = require('easy-monitor');easymonitor('项目名称');
打开你的浏览器,访问 http://localhost:12333 ,即可看到进程界面,更详细的内容请参考官网
10.3 node性能优化关于node的性能优化的方式有如下几个:
使用最新版本node.js正确使用流 stream代码层面优化内存管理优化10.3.1 使用最新版本node.js每个版本的性能提升主要来自于两个方面:
v8 的版本更新node.js 内部代码的更新优化10.3.2 正确使用流在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);});
10.3.3 代码层面优化合并查询,将多次查询合并一次,减少数据库的查询次数。
// 错误方式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]
10.3.4 内存管理优化在 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这些类,并不是很合适,在大量请求时,频繁创建和销毁这些类,造成内存抖动。而使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中,从而避免重读的初始化操作,从而提高框架的性能。
更多编程相关知识,请访问:编程视频!!
以上就是总结分享一些基于node.js的前端面试题(附解析)的详细内容。
其它类似信息

推荐信息