前端关于node的面试题,Node.js面试题

  前端关于node的面试题,Node.js面试题

  本文基于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声明的变量不是全局变量,只在当前模块中生效。像上面这样的全局全局对象在全局范围内,任何全局变量、函数或对象都是对象的属性值。

  

2.1 常见全局对象

  节点的通用全局对象如下:

  Class:bufferprocessconsolecearinterval,setIntervalclearTimeout,settimeoutglobalClass:BufferClass:Buffer可用于处理二进制和非Unicode编码的数据,原始数据存储在Buffer类实例化中。Buffer类似于整数数组,内存在V8堆的原始存储空间中分配给它。一旦创建了缓冲区实例,就不能更改其大小。

  process进程表示一个进程对象,它提供关于当前进程的信息和控制。包括在执行节点程序的过程中,如果需要传递一个参数,我们需要在流程内置对象中获取这个参数。例如,我们有以下文件:

  process.argv.forEach((val,index)={

  console . log(` $ { index }:$ { val } `);

  });当我们需要启动一个进程时,我们可以使用下面的命令:

  节点index.js参数.console控制台主要用于打印stdout和stderr,最常用的是日志输出:console.log清除控制台的命令是:console.clear如果需要打印某个函数的调用栈,可以使用命令console.trace

  clearInterval、setIntervalsetInterval用于设置计时器。语法格式如下:

  SetInterval(回调,延迟[,args]) ClearInterval用于清除计时器,回调每延迟毫秒重复一次。

  clearTimeout、setTimeout

  和setInterval一样,setTimeout主要用于设置延迟,clearTimeout用于清除设置的延迟。

  globalglobal是一个全局名称空间对象。前面提到的进程、控制台和setTimeout可以放在全局中,例如:

  console . log(process===global . process)//Output true

2.2 模块中的全局对象

  除了系统提供的全局对象,还有一些只是出现在模块中,看起来像是全局变量,如下图:

  _ _ Dirname _ _ FilenameExportModuleQuery__dirname_ _ Dirname主要用于获取当前文件所在的路径,不包括以下文件名。例如,在/Users/mjr中运行node example.js,打印结果如下:

  console . log(_ _ dirname);//print:/Users/mjr__filename_ _ filename用于获取当前文件的路径和文件名,包括以下文件名。例如,在/Users/mjr中运行node example.js,打印结果如下:

  console . log(_ _文件名);//Print:/users/mjr/example . jsexportsmodule . exports用于导出指定模块的内容,然后还可以使用require()来访问这些内容。

  exports.name=nameexports.age=age

  exports . say hello=say hello;requirerequire主要用于导入模块、JSON或本地文件。可以从node_modules导入模块。可以使用相对路径导入本地模块或者JSON文件,路径会根据__dirname定义的目录名或者当前工作目录进行处理。

  

三、谈谈对process的理解

  

3.1 基本概念

  我们知道,过程计算机系统中资源分配和调度的基本单元是操作系统结构的基础,是线程的容器。当我们启动一个js文件时,我们实际上启动了一个服务进程。每个进程都有自己独立的空间地址和数据栈,就像另一个进程不能访问当前进程的变量和数据结构一样。只有在数据通信之后,才能在进程之间共享数据。

  Process object是Node的全局变量,提供当前Node.js进程的信息,并对其进行控制。

  由于JavaScript是单线程语言,通过节点xxx启动一个文件后只有一个主线程。

  

3.2 常用属性和方法

  流程的常见属性如下:

  Process.env:环境变量,比如通过process.env.NODE_ENV获取不同环境项目的配置信息。process.nextTick:这是在讲EventLoop时经常提到的:process.pid:获取当前进程idprocess.ppid:当前进程对应的父进程process.cwd()。获取当前进程工作目录process.platform:获取操作系统平台process.uptime():当前进程的运行时间,例如:pm2 daemon的uptime值。流程事件:process.on(未捕获异常,cb)捕获异常信息,process.on(退出,CB ,CB)推出并监听。

  三个标准流:process.stdout标准输出、process.stdin标准输入和process.stderr标准错误输出。process.title:用于指定进程名,有时需要为进程指定一个名称

四、谈谈你对fs模块的理解

  

4.1 fs是什么

   fs(filesystem)是一个文件系统模块,提供本地文件的读写能力,基本基于POSIX文件操作命令。可以说所有对文件的操作都是通过fs核心模块实现的。

  在使用之前,您需要导入fs模块,如下所示:

  const fs=require( fs );

4.2 文件基础知识

  在计算机中,有一些关于文件的基本知识如下:

  权限模式将标志文件标识为fd

4.2.1 权限位 mode

  。

  权限被分配给文件所有者、文件所属的组和其他用户。类型分为读取、写入和执行,权限位为4、2和1,没有权限为0。例如,在linux中查看文件权限位的命令如下:

  drwxr-xr-x 1 panda Shen 197121 0 Jun 28 14:41 core

  -RW-R-R-1 panda Shen 197121 293 Jun 23 17:44 index . MD前十位中,D是文件夹,-是文件,后九位代表当前用户、用户所属的组和其他用户的权限位,除以三位,代表读(R)、写(W)和执行(X)

  

4.2.2 标识位

  标志表示文件的操作模式,如可读、可写、既可读又可写等。如下表所示:

  

4.2.3 文件描述 fd

  操作系统将为每个打开的文件分配一个名为文件描述符的数字ID。文件操作使用这些文件描述符来识别和跟踪每个特定的文件。

  窗口系统使用不同但相似的机制来跟踪资源。为了方便用户,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 );

  设buf=fs . read file sync( 1 . txt );

  let data=fs.readFileSync(1.txt , utf8 );

  console . log(buf);//缓冲区48 65 6c 6c 6f

  console.log(数据);//Hello第一个参数是读取文件的路径或文件描述符。第二个参数是options,默认值为null,包括编码(默认为null)和标志(标志位,默认为r),也可以直接传入编码。读取是一种异步读取方法。readFile的前两个参数与readFileSync的参数相同,最后一个参数是一个回调函数。函数中有两个参数err(错误)和data(数据)。该方法没有返回值,回调函数在成功读取文件后执行。

  const fs=require( fs );

  fs.readFile(1.txt , utf8 ,(err,data)={

  如果(!呃){

  console.log(数据);//你好

  }

  });

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(数据);helloworld的第一个参数是写入文件的路径或文件描述符。第二个参数是写入的数据,它的类型是String或Buffer。第三个参数是options,默认值为null,包括编码(encoding,默认为utf8)、标志(flag bit,默认为w)和模式(permission bit,默认为0o666),也可以直接传入编码。WriteFile表示异步写入。writeFile与writeFileSync的前三个参数相同,最后一个参数是回调函数。函数中有一个参数err (error),回调函数在文件成功写入数据后执行。

  const fs=require( fs );

  fs.writeFile(2.txt , Hello world ,err={

  如果(!呃){

  fs.readFile(2.txt , utf8 ,(err,data)={

  console.log(数据);//你好世界

  });

  }

  });

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 bit,默认为A)和模式(permission bit,默认为0o666),也可以直接传入编码。AppendFile代表异步追加写入。appendFile方法与appendFileSync的前三个参数相同,最后一个参数是一个回调函数。函数中有一个参数err (error),回调函数在文件成功追加数据后执行,如下图。

  const fs=require( fs );

  fs.appendFile(3.txt , world ,err={

  如果(!呃){

  fs.readFile(3.txt , utf8 ,(err,data)={

  console.log(数据);//你好世界

  });

  }

  });

4.3.4 创建目录

  创建目录有两种主要方法:mkdirSync和mkdir。其中,mkdirSync是同步创建的,参数是一个目录的路径,没有返回值。在创建目录的过程中,传入路径前面的所有文件目录都必须存在,否则会抛出异常。

  //假设你在A下已经有了文件夹A和文件夹B。

  Fs.mkdirSync(a/b/c)mkdir是异步创建的,第二个参数是回调函数,如下所示。

  fs.mkdir(a/b/c ,err={

  如果(!Err) console.log(成功创建);

  });

五、谈谈你对Stream 的理解

  

5.1 基本概念

  流是一种数据传输的手段,是一种端到端的信息交换方式,而且是顺序的。它逐块读取数据,处理内容,并用于顺序读取输入或写入输出。在节点中,流被分成三个部分:源、目标和管道。

  其中,source和dest之间有一个管道pipe连接,其基本语法是source.pipe(dest)。源和目标通过管道连接,因此数据可以从源流向目标,如下图所示:

  

5.2 流的分类

  在节点处,流可分为四类:

  可写流:可以写数据的流,比如fs.createWriteStream(),可以使用流将数据写入文件。可读流:可以读取数据的流,如fs.createReadStream(),可以从文件中读取内容。双工流:既可读又可写的流,例如net。套接字转换流:在数据写入和读取期间,可以修改或转换数据流。例如,在文件压缩操作中,压缩数据可以写入文件,解压缩数据可以从文件中读取。在Node的HTTP服务器模块中,请求是可读的流,响应是可写的流。对于fs模块,可读流和可写流是单向的,易于理解,可读写流既可以处理可读文件流,也可以处理可写文件流。套接字是双向的,可读写的。

  

5.2.1 双工流

  在Node中,最常见的全双工通信是websocket,因为发送方和接收方都是独立的方法,发送和接收之间没有关系。

  的基本用法如下:

  const { Duplex }=require( stream );

  const myDuplex=新双工({

  读取(大小){

  //.

  },

  写入(区块、编码、回调){

  //.

  }

  });

5.3 使用场景

  流的常见使用场景有:

  Get请求返回文件到客户端文件操作打包工具的一些底层操作

5.3.1 网络请求

   stream一个常见的使用场景是网络请求,比如使用stream stream返回文件。res也是一个流对象,通过管道返回文件数据。

  const server=http . create server(function(req,res) {

  const方法=req.method

  //获取请求

  if (method===GET) {

  const fileName=path . resolve(_ _ dirname, data . txt );

  let stream=fs.createReadStream(文件名);

  stream . pipe(RES);

  }

  });

  server . listen(8080);

5.3.2 文件操作

  文件读取也是流操作。创建一个可读的数据流readStream和一个可写的数据流writeStream,通过管道管道传输数据。

  const fs=require(fs )

  const path=require(path )

  //两个文件名

  const filename 1=path . resolve(_ _ dirname, data.txt )

  const filename 2=path . resolve(_ _ dirname, data-bak.txt )

  //读取文件的流对象

  const read stream=fs . create read stream(文件名1)

  //写入文件的流对象

  const write stream=fs . create write stream(文件名2)

  //通过管道复制,数据流

  readStream.pipe(writeStream)

  //数据读取完成监控,即复制完成。

  readStream.on(end ,function () {

  Console.log(“复制完成”)

  })另外,一些打包工具,如Webpack、Vite等,都涉及到很多流操作。

  

六、事件循环机制

  

6.1 什么是浏览器事件循环

   Node.js在主线程中维护一个事件队列。当接收到一个请求时,它将作为一个事件放入这个队列,然后继续接收其他请求。当主线程空闲时(当没有访问请求时),它开始循环事件队列,并检查队列中是否有要处理的事件。此时有两种情况:如果是非I/O任务,则亲自处理,通过回调函数返回上层调用;如果是I/O任务,从线程池中拿出一个线程来处理这个事件,指定一个回调函数,然后继续循环队列中的其他事件。

  当线程中的I/O任务完成后,将执行指定的回调函数,完成的事件将放在事件队列的末尾,等待事件循环。当主线程再次循环到事件时,会直接处理并返回到上层调用。这个过程称为事件循环,其工作原理如下图所示。

  从左到右,从上到下,Node.js分为四层,分别是应用层、V8引擎层、节点API层和LIBUV层。

  应用层: JavaScript交互层,也就是俗称的Node.js的模块比如http,FSV8引擎层:它使用V8引擎解析JavaScript语法,然后与下层API进行交互。Node API层:为上层模块提供系统调用,一般用C语言实现,与操作系统交互。LIBUV层:是跨平台底层包,实现事件循环、文件操作等。实现异步是Node.js的核心。在Node中,我们说的事件循环是基于libuv的,这是一个多平台的库,专注于异步IO。图中的EVENT_QUEUE给人的感觉是只有一个队列,但实际上EventLoop有六个阶段,每个阶段都有对应的FIFO回调队列。

  

6.2 事件循环的六个阶段

  事件周期可分为六个阶段,如下图所示。

  Timers阶段:这个阶段主要执行timer(setTimeout,setInterval)的回调。I/O事件回调阶段(I/O回调):执行推迟到下一次循环迭代的I/O回调,即上一次循环中没有执行的一些I/O回调。空闲阶段(Idle,prepare):仅在系统内使用。轮询阶段:检索新的I/O事件;执行与I/O相关的回调(在几乎所有情况下,除了关闭的回调函数,那些由timer和setImmediate()调度的回调函数),其余的cases节点将在适当的时间阻塞在这里。Check阶段:check):setImmediate()回调函数在这里执行关闭回调阶段:一些关闭的回调函数,比如socket.on(close ,),每个阶段对应一个队列。当事件循环进入某一阶段时,回调将在该阶段执行,直到队列耗尽或执行了最大数量的回调,然后进行下一步处理。

  

七、EventEmitter

  

7.1 基本概念

  如前所述,Node采用事件驱动机制,EventEmitter是Node事件驱动实现的基础。在EventEmitter的基础上,Node几乎所有的模块都继承了这个类。这些模块都有自己的事件,可以绑定和触发监听器,实现异步操作。

  Node.js中的很多对象都会分发事件。例如,当文件打开时,fs.readStream对象将触发一个事件。这些事件生成对象是事件的实例。EventEmitter,用于将一个或多个函数绑定到命名事件。

  

7.2 基本使用

   Node的events模块只提供了一个EventEmitter类,实现了Node异步事件驱动架构的基本模式:observer模式。

  在这种模式下,被观察者(主体)维护一组由其他对象发送(注册)的观察者。如果新对象对主题感兴趣,注册观察者,如果不感兴趣,取消订阅。如果有更新,主题将依次通知观察者。用法如下。

  const EventEmitter=require("事件")

  MyEmitter类扩展了EventEmitter {}

  const myEmitter=new MyEmitter()

  函数回调(){

  console.log(触发了事件事件!)

  }

  我的密特。开(事件,回调)

  我的密特。发出(“事件”)

  我的密特。移除侦听器( event ,回调);在上面的代码中,我们通过实例对象的在方法注册一个名为事件的事件,通过发射方法触发该事件,而移除监听器用于取消事件的监听。

  除了上面介绍的一些方法外,其他常用的方法还有如下一些:

  emitter.addListener/on(eventName, listener):添加类型为事件名的监听事件到事件数组尾部。emitter.prependListener(eventName, listener):添加类型为事件名的监听事件到事件数组头部。emitter.emit(eventName[, ...args]):触发类型为事件名的监听事件。emitter.removeListener/off(eventName, listener):移除类型为事件名的监听事件。emitter.once(eventName, listener):添加类型为事件名的监听事件,以后只能执行一次并删除。emitter.removeAllListeners([eventName]): 移除全部类型为事件名的监听事件010-10时10分事件发射器其实是一个构造函数,内部存在一个包含所有事件的对象。

  类别事件发射器{

  构造函数(){

  这个。事件={ };

  }

  }其中,事件存放的监听事件的函数的结构如下:

  {

  事件1: [f1,f2,f3],

  事件2: [f4,f5],

  .

  }然后,开始一步步实现实例方法,首先是发射,第一个参数为事件的类型,第二个参数开始为触发事件函数的参数,实现如下:

  发出(类型,args) {

  this.events[type].forEach((item)={

  Reflect.apply(item,this,args);

  });

  }实现了发射方法之后,然后依次实现on、addListener、prependListener这三个实例方法,它们都是添加事件监听触发函数的。

  在(类型,处理程序){

  如果(!this.events[type]) {

  这个。events[type]=[];

  }

  this.events[type].推送(处理程序);

  }

  addListener(类型,处理程序){

  this.on(类型,处理程序)

  }

  前置监听器(类型,处理程序){

  如果(!this.events[type]) {

  这个。events[type]=[];

  }

  this.events[type].未换档(处理程序);

  }移除事件监听,可以使用方法removeListener/on

  移除监听器(类型,处理程序){

  如果(!this.events[type]) {

  返回;

  }

  这个。事件[类型]=this。事件[类型].过滤器(item=item!==处理程序);

  }

  关(类型,处理程序){

  this.removeListener(类型,处理程序)

  }实现一次方法,再传入事件监听处理函数的时候进行封装,利用闭包的特性维护当前状态,通过解雇属性值判断事件函数是否执行过。

  一次(类型、处理程序){

  this.on(类型,这个._onceWrap(type,handler,this));

  }

  _onceWrap(类型、处理程序、目标){

  const state={ fired: false,handler,type,target} .

  const wrapFn=this ._一次包装。bind(状态);

  状态。wrap fn=wrap fn

  返回包装Fn

  }

  _onceWrapper(.args) {

  如果(!this.fired) {

  this.fired=true

  Reflect.apply(this.handler,this.target,args);

  this.target.off(this.type,this。wrap fn);

  }

  }下面是完成的测试代码:

  类别事件发射器{

  构造函数(){

  这个。事件={ };

  }

  在(类型,处理程序){

  如果(!this.events[type]) {

  这个。events[type]=[];

  }

  this.events[type].推送(处理程序);

  }

  addListener(类型,处理程序){

  this.on(类型,处理程序)

  }

  前置监听器(类型,处理程序){

  如果(!this.events[type]) {

  这个。events[type]=[];

  }

  this.events[type].未换档(处理程序);

  }

  移除监听器(类型,处理程序){

  如果(!this.events[type]) {

  返回;

  }

  这个。事件[类型]=this。事件[类型].过滤器(item=item!==处理程序);

  }

  关(类型,处理程序){

  this.removeListener(类型,处理程序)

  }

  发出(类型,args) {

  this.events[type].forEach((item)={

  Reflect.apply(item,this,args);

  });

  }

  一次(类型、处理程序){

  this.on(类型,这个._onceWrap(type,handler,this));

  }

  _onceWrap(类型、处理程序、目标){

  const state={ fired: false,handler,type,target}。

  const wrapFn=this。_ once wrapper . bind(state);

  state . wrap fn=wrap fn;

  返回wrapFn

  }

  _onceWrapper(.args) {

  如果(!this.fired) {

  this.fired=true

  Reflect.apply(this.handler,this.target,args);

  this.target.off(this.type,this . wrap fn);

  }

  }

  }

7.3 实现原理

  

八、中间件

  中间件是介于应用系统和系统软件之间的一种软件。它利用系统软件提供的基本服务(功能)将应用系统的各个部分或网络上的不同应用连接起来,可以达到资源共享、功能共享的目的。

  在Node中,中间件主要是指封装http请求细节的方法。比如在express、koa等web框架中,中间件的本质是一个回调函数,参数包括请求对象、响应对象和执行下一个中间件的函数。架构图如下。

  通常,在这些中间件功能中,我们可以执行业务逻辑代码,修改请求和响应对象,并返回响应数据。

  

8.1 基本概念

   Koa是一个流行的基于节点的web框架。本身支持的功能不多,所有功能都可以通过中间件扩展。KO没有捆绑任何中间件,而是提供了一种优雅的方法,帮助开发者快速愉快地编写服务器端应用。

  Koa中间件采用洋葱圈模型,每次执行下一个中间件时传入两个参数:

  Ctx:封装请求和响应的变量next:进入下一个要执行的中间件的函数。通过前面的介绍,我们知道Koa中间件本质上是一个函数,可以是异步函数,也可以是常用函数。中间件为koa封装如下:

  //异步函数

  app.use(async (ctx,next)={

  const start=date . now();

  wait next();

  const ms=date . now()-start;

  console . log(` $ { CTX . method } $ { CTX . URL }-$ { ms } ms `);

  });

  //通用函数

  app.use((ctx,next)={

  const start=date . now();

  返回下一个()。然后(()={

  const ms=date . now()-start;

  console . log(` $ { CTX . method } $ { CTX . URL }-$ { ms } ms `);

  });

  });当然,我们也可以通过中间件封装http请求流程中的几个常用功能:

  token校验

  module . exports=(options)=async(CTX,next) {

  尝试{

  //获取令牌

  const令牌=ctx.header.authorization

  if (token) {

  尝试{

  verify函数对令牌进行验证,获取用户的相关信息。

  等待验证(令牌)

  } catch (err) {

  console.log(错误)

  }

  }

  //转到下一个中间件

  等待下一个()

  } catch (err) {

  console.log(错误)

  }

  }日志模块

  const fs=require(fs )

  module . exports=(options)=async(CTX,next)={

  const startTime=Date.now()

  const requestTime=新日期()

  等待下一个()

  const ms=date . now()-start time;

  let logout=` $ { CTX . request . IP }-$ { request time }-$ { CTX . method }-$ { CTX . URL }-$ { ms } ms `;

  //输出日志文件

  fs.appendFileSync(。/log.txt ,注销 \n )

  } KOA中有很多第三方中间件,比如koa-bodyparser、koa-static等。

  

8.2 koa

  koa-bodyparserKOA-body parser中间件就是把我们通过表单提交的post请求和查询字符串转换成对象挂在ctx.request.body上,方便我们在其他中间件或者接口获取值。

  //File: my-koa-bodyparser.js

  const query string=require( query string );

  module . exports=function body parser(){

  返回异步(ctx,next)={

  等待新的承诺((解决,拒绝)={

  //存储数据的数组

  设data arr=[];

  //接收数据

  ctx.req.on(data ,data=data arr . push(data));

  //合并数据并成功使用Promise。

  ctx.req.on(end ,()={

  //获取请求数据的json类型或形式

  let Content Type=CTX . get( Content-Type );

  //获取数据缓冲区格式

  let data=Buffer.concat(dataArr)。toString();

  if(content type=== application/x-www-form-urlencoded ){

  //如果是表单提交,将查询字符串转换成对象,并赋给ctx.request.body

  CTX . request . body=query string . parse(data);

  } else if(content type=== appl action/JSON ){

  //如果是json,把字符串格式的对象转换成对象,赋给ctx.request.body

  CTX . request . body=JSON . parse(data);

  }

  //执行成功的回调

  resolve();

  });

  });

  //继续向下执行。

  wait next();

  };

  };koa-staticKOA-静态中间件用来帮助我们在服务器收到请求时处理静态文件,比如。

  const fs=require( fs );

  const path=require( path );

  const mime=require( mime );

  const { promisify }=require( util );

  //将统计和访问转换为承诺

  const stat=promisify(fs . stat);

  const access=promisify(fs . access)

  module.exports=函数(目录){

  返回异步(ctx,next)={

  //将访问的路由视为绝对路径。这里使用join是因为它可能是/

  设realPath=path.join(dir,CTX . path);

  尝试{

  //获取stat对象

  let statObj=await stat(real path);

  //如果是文件,设置文件类型,直接响应内容;否则,将index.html作为文件夹查找。

  if (statObj.isFile()) {

  ctx.set(Content-Type ,` $ { mime . gettype()};charset=utf8 `);

  CTX . body=fs . createreadstream(realPath);

  }否则{

  let filename=path.join(realPath, index . html );

  //如果在这个文件中不存在,在catch中执行next,交给其他中间件处理。

  等待访问(文件名);

  //有设置文件类型和响应内容。

  ctx.set(Content-Type , text/html;charset=utf8 );

  ctx.body=fs.createReadStream(文件名);

  }

  } catch (e) {

  wait next();

  }

  }

  }一般来说,实现中间件的时候,单个中间件要足够简单,职责单一,中间件的代码要写得高效。必要时,可以通过缓存重复获取数据。

  

8.3 Koa中间件

  

九、如何设计并实现JWT鉴权

   JWT(JSON Web Token),本质上是一个字符串编写规范,它的作用是在用户和服务器之间传递安全性和可靠性,如下图所示。

  目前,在前端分离的开发过程中,使用令牌认证机制进行认证是最常见的方案,其流程如下:

  当服务器验证用户的账号和密码正确时,它会向用户颁发一个令牌,作为后续用户访问某些接口的凭证。后续访问将根据此令牌确定用户何时具有访问权限。令牌分为三个部分:报头、有效载荷和签名,它们是用.头和有效负载以JSON格式存储,但是只进行编码,如下图所示。

  

9.1 JWT是什么

  每个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.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占用系统内存的比例

   }

   }

  }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的前端面试题(附解析)的详细内容,更多请关注我们其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: