2023.03.09 - 2023.03.15 更新收集面试问题(45道题)【第4部分】
获取更多面试问题可以访问
github 地址: https://github.com/pro-collection/interview-question/issues
gitee 地址: https://gitee.com/yanleweb/interview-question/issues

目录:

  • 中级开发者相关问题【共计 2 道题】

    • 73.express middleware 工作原理是什么??【Nodejs】
    • 75.[vue]: 手写 vue 双向绑定?【web框架】
  • 高级开发者相关问题【共计 3 道题】

    • 69.前端如何实现即时通讯?【JavaScript】
    • 70.前端做错误监控?【JavaScript】
    • 72.TCP 和 UDP的区别?【网络】

73.express middleware 工作原理是什么??【Nodejs】

express middleware 工作原理是什么?

Express middleware 的工作原理是通过拦截 HTTP 请求,对请求进行处理,然后将请求传递给下一个中间件或应用程序的路由处理。在 Express 中,中间件可以是一个或多个函数,每个函数都可以对请求进行操作或响应,从而实现对请求的处理和过滤。

当 Express 应用程序接收到一个 HTTP 请求时,请求将首先被传递给第一个注册的中间件函数。这个中间件函数可以对请求进行操作,例如修改请求的头信息、检查请求是否包含有效的身份验证令牌等等。当这个中间件函数完成操作后,它可以选择将请求传递给下一个中间件函数,或者直接将响应返回给客户端。

如果中间件函数选择将请求传递给下一个中间件函数,它可以调用 next() 函数来将控制权传递给下一个中间件。这个过程可以一直持续到所有中间件函数都被执行完毕,最后将请求传递给应用程序的路由处理函数。

通过使用中间件,开发人员可以将应用程序的功能模块化,从而实现更好的代码组织和可维护性。同时,中间件还可以实现各种功能,例如身份验证、日志记录、错误处理等等,从而为应用程序提供更丰富的功能和更好的用户体验。

Express middleware 的设计模式是基于责任链模式。在责任链模式中,每个对象都有机会处理请求,并将其传递给下一个对象,直到请求被完全处理为止。在 Express 中,每个中间件函数都有机会对请求进行处理,并可以选择将请求传递给下一个中间件函数或应用程序的路由处理函数。

以下是一个简单的示例,演示如何使用 Express middleware 实现身份验证:

 1const express = require('express');
 2const app = express();
 3
 4// 定义一个中间件函数,用于验证用户身份
 5function authenticate(req, res, next) {
 6  const token = req.headers.authorization;
 7  if (token === 'secret-token') {
 8    // 如果令牌有效,则将控制权传递给下一个中间件函数
 9    next();
10  } else {
11    // 否则,返回 401 错误
12    res.status(401).send('Unauthorized');
13  }
14}
15
16// 注册中间件函数,用于验证用户身份
17app.use(authenticate);
18
19// 定义一个路由处理函数,用于返回受保护的资源
20app.get('/protected', (req, res) => {
21  res.send('Protected resource');
22});
23
24// 启动应用程序
25app.listen(3000, () => {
26  console.log('Server is running on port 3000');
27});

在上面的示例中,我们定义了一个名为 authenticate 的中间件函数,它用于验证用户的身份。在这个函数中,我们检查请求头中是否包含有效的身份验证令牌。如果令牌有效,则将控制权传递给下一个中间件函数或路由处理函数。否则,返回 401 错误。

然后,我们通过调用 app.use() 方法来注册这个中间件函数,以便在每个请求中都进行身份验证。最后,我们定义一个名为 /protected 的路由处理函数,它表示受保护的资源。只有在身份验证通过后,才能访问这个路由处理函数。

通过这个简单的示例,我们可以看到如何使用 Express middleware 实现基本的身份验证功能。中间件函数充当责任链中的一个环节,通过对请求进行处理和过滤,为应用程序提供更好的安全性和用户体验。

75.[vue]: 手写 vue 双向绑定?【web框架】

如果一个对象中有属性有方法,那么调用属性可以直接. 就可以调用,但是如果是调用方法的时候,是通过入参来决定key的值来调用的话,请用[]来表示:

 1<!DOCTYPE html>
 2<html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml">
 3  <head>
 4    <meta charset="UTF-8">
 5      <title>MVVM 双项绑定</title>
 6      <style>
 7        #app {
 8        text-align: center;
 9        margin-top: 100px;
10        color: #888;
11      }
12
13        h1 {
14        color: #aaa;
15      }
16
17        input {
18        padding: 0 10px;
19        width: 600px;
20        line-height: 2.5;
21        border: 1px solid #ccc;
22        border-radius: 2px;
23      }
24
25        .bind {
26        color: #766;
27      }
28
29        strong {
30        color: #05BC00;
31      }
32
33        button {
34        padding: 5px 10px;
35        border: 1px solid #777777;
36        border-radius: 5px;
37        background: #ffffff;
38        color: #777777;
39        cursor: pointer;
40
41      }
42      </style>
43  </head>
44  <body>
45    <div id="app">
46      <h1>Hi,MVVM</h1>
47      <input v-model="name" placeholder="请输入内容" type="text">
48        <h1 class="bind">{{name}} 's age is <strong>{{age}}</strong></h1>
49        <button v-on:click="sayHi">点击欢迎您</button>
50    </div>
51    <script>
52      function observe(data) {
53      //如果不是一个对象,直接终止程序
54      if (!data || typeof data !== 'object') {
55      return false;
56    }
57      for (let key in data) {
58      let val = data[key];
59      let subject = new Subject();
60      Object.defineProperty(data, key, {
61      enumerable: true,
62      configurable: true,
63      get: function () {
64      if (currentObserver) {
65      currentObserver.subscribeTo(subject)
66    }
67      return val
68    },
69      set: function (newVal) {
70      val = newVal;
71      subject.notify()
72    }
73    });
74      if (typeof val === 'object') {
75      observe(val)
76    }
77    }
78    }
79
80      let id = 0;
81      let currentObserver = null;
82
83      /**
84      * 订阅者对象
85      */
86      class Subject {
87      constructor() {
88      this.id = id++;
89      this.observers = []
90    }
91
92      addObserver(observer) {
93      this.observers.push(observer)
94    }
95
96      removeObserver(observer) {
97      let index = this.observers.indexOf(observer)
98      if (index > -1) {
99      this.observers.splice(index, 1)
100    }
101    }
102
103      notify() {
104      this.observers.forEach(observer => {
105      observer.update()
106    })
107    }
108    }
109
110      /**
111      * 观察者对象
112      */
113      class Observer {
114      constructor(vm, key, cb) {
115      this.subjects = {};
116      this.vm = vm;
117      this.key = key;
118      this.cb = cb;
119      this.value = this.getValue()
120    }
121
122      //如果新旧数据不相同,就直接调用cb方法
123      update() {
124      let oldVal = this.value;
125      let value = this.getValue();
126      if (value !== oldVal) {
127      this.value = value;
128      this.cb.bind(this.vm)(value, oldVal)
129    }
130    }
131
132      //添加观察者
133      subscribeTo(subject) {
134      if (!this.subjects[subject.id]) {       //如果当前换擦着中不存在这个当前id的一个对象,那么吧这个对象添加为观察者
135      subject.addObserver(this);
136      this.subjects[subject.id] = subject     //放在观察者对象中,根据自增id来区分
137    }
138    }
139
140      getValue() {
141      currentObserver = this;
142      let value = this.vm.$data[this.key];    //获取vm实例兑现中的data数据
143      currentObserver = null;
144      return value
145    }
146    }
147
148      /**
149      * 编译对象
150      */
151      class Compile {
152      constructor(vm) {
153      this.vm = vm; //vm对象
154      this.node = vm.$el; //获取挂载的元素dom
155      this.compile();//执行核心功能
156    }
157
158      compile() {
159      this.traverse(this.node);//传入的参数是挂载元素dom
160    }
161
162      traverse(node) {
163      if (node.nodeType === 1) {      //节点类型1:element元素
164      this.compileNode(node);     //触发节点事件 双向绑定和事件触发
165      node.childNodes.forEach(childNode => {
166      this.traverse(childNode);       // 递归调用,如果是有子节点,重新递归
167    })
168    } else if (node.nodeType === 3) {       // 节点类型3: 文本元素
169      this.compileText(node);     // 处理文本元素的编译
170    }
171    }
172
173      // 文本编译入口
174      compileText(node) {
175      let reg = /{{(.+?)}}/g;
176      let match;
177      while (match = reg.exec(node.nodeValue)) {      //获取到文本内容
178      let raw = match[0]
179      let key = match[1].trim()
180      node.nodeValue = node.nodeValue.replace(raw, this.vm.$data[key]);
181      new Observer(this.vm, key, function (val, oldVal) {     // 订阅者核心方法
182      node.nodeValue = node.nodeValue.replace(oldVal, val)
183    })
184    }
185    }
186
187      // 节点编译入口
188      compileNode(node) {
189      let attrs = [...node.attributes];//获取标签属性
190      attrs.forEach(attr => {
191      if (this.isModelDirective(attr.name)) { //截取是绑定数据的情况
192      this.bindModel(node, attr); //绑定数据
193    } else if (this.isEventDirective(attr.name)) { //截取是绑定事件的情况
194      this.bindEventHander(node, attr); //触发事件
195    }
196    })
197    }
198
199      /**
200       * 双向绑定数据
201       * @param node  标签节点
202       * @param attr  标签节点的属性名
203       */
204      bindModel(node, attr) {
205      let key = attr.value;// 获取到传递过来的属性的key的值
206      node.value = this.vm.$data[key]; //给节点绑定值,对应的值就是vm实例里面data对应key的值
207      new Observer(this.vm, key, function (newVal) {
208      node.value = newVal
209    });
210      node.oninput = (e) => { //监听节点的input事件
211      this.vm.$data[key] = e.target.value //过去输入框中输入的value值,把这个值放入到vm的data实例中去
212    }
213    }
214
215      /**
216       *
217       * @param node
218       * @param attr
219       */
220      bindEventHander(node, attr) {
221      let eventType = attr.name.substr(5); //获取节点属性,从第五个下标开始截取后面的字符串作为:key(事件类型)
222      let methodName = attr.value; //获取节点的属性的value
223      node.addEventListener(eventType, this.vm.$methods[methodName]);//通过事件类型,来触发事件,事件就是vm实例中方法
224    }
225
226      //赛选出传入的node属性是v-model的情况
227      isModelDirective(attrName) {
228      return attrName === 'v-model'
229    }
230
231      //赛选出传入的node属性是 v-on的情况
232      isEventDirective(attrName) {
233      return attrName.indexOf('v-on') === 0
234    }
235    }
236
237      class mvvm {
238      constructor(opts) {     //这里面的函数是实例化的时候执行的
239      this.init(opts);
240      observe(this.$data);
241      new Compile(this);      //变异当前对象
242    }
243
244      init(opts) {
245      if (opts.el.nodeType === 1) {
246      this.$el = opts.el
247    } else {
248      this.$el = document.querySelector(opts.el)
249    }
250
251      this.$data = opts.data || {};
252      this.$methods = opts.methods || {};
253      //把$data 中的数据直接代理到当前 vm 对象
254      for (let key in this.$data) {
255      Object.defineProperty(this, key, {
256      enumerable: true,
257      configurable: true,
258      get: () => {
259      return this.$data[key]
260    },
261      set: newVal => {
262      this.$data[key] = newVal
263    }
264    })
265    }
266      //让 this.$methods 里面的函数中的 this,都指向当前的 this,也就是 vm对象实例
267      for (let key in this.$methods) {
268      this.$methods[key] = this.$methods[key].bind(this);
269    }
270    }
271    }
272
273
274      /**
275      * 实例化MVVM对象, 主入口
276      * @type {mvvm}
277      */
278      let vm = new mvvm({
279      el: '#app',
280      data: {
281      name: 'YanLe',
282      age: 3
283    },
284      methods: {
285      sayHi: function () {
286      alert(`hi ${this.name}`)
287    }
288    }
289    });
290
291      let clock = setInterval(function () {
292      vm.age++;  //等同于 vm.$data.age
293
294      if (vm.age === 10) clearInterval(clock)
295    }, 1000)
296    </script>
297  </body>
298</html>

69.前端如何实现即时通讯?【JavaScript】

短轮询的原理很简单,每隔一段时间客户端就发出一个请求,去获取服务器最新的数据,一定程度上模拟实现了即时通讯。

  • 优点:兼容性强,实现非常简单
  • 缺点:延迟性高,非常消耗请求资源,影响性能

comet有两种主要实现手段, 一种是基于 AJAX 的长轮询(long-polling)方式, 另一种是基于 Iframe 及 htmlfile 的流(streaming)方式,通常被叫做长连接。

具体两种手段的操作方法请移步 Comet技术详解:基于HTTP长连接的Web端实时通信技术

  • 长轮询优缺点:

    • 优点:兼容性好,资源浪费较小
    • 缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护
  • 长连接优缺点:

    • 优点:兼容性好,消息即时到达,不发无用请求
    • 缺点:服务器维护长连接消耗资源

SSE(Server-Sent Event,服务端推送事件)是一种允许服务端向客户端推送新数据的HTML5技术。

  • 优点:基于HTTP而生,因此不需要太多改造就能使用,使用方便,而websocket非常复杂,必须借助成熟的库或框架
  • 缺点:基于文本传输效率没有websocket高,不是严格的双向通信,客户端向服务端发送请求无法复用之前的连接,需要重新发出独立的请求

Websocket是一个全新的、独立的协议,基于TCP协议,与http协议兼容、却不会融入http协议,仅仅作为html5的一部分,其作用就是在服务器和客户端之间建立实时的双向通信。

  • 优点:真正意义上的实时双向通信,性能好,低延迟
  • 缺点:独立与http的协议,因此需要额外的项目改造,使用复杂度高,必须引入成熟的库,无法兼容低版本浏览器

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理,创建有效的离线体验。

错误分类:即时运行错误(代码错误)、资源加载错误

即时运行错误: try…catch window.onerror

资源加载错误:
1)、object.onerror
2)、performance.getEntries()
3)、Error事件捕获
performance.getEntries()这个是可以获取到所有的家已经加载的资源

Error事件捕获使用方式:

 1window.addEventListener('error',function(e){
 2    console.log('捕获',e)
 3},true)

跨域是可以捕获的:
1)、在script标签添加crossorigin属性
2)、在js响应头添加Access-Control-Allow-Origin:*;

上报错误:ajax通信方式上报、通过Image对象上报,非常简单
(new Image()).src=‘http://baidu.com/test/sdflijsd?=sdlfkj’;

1、TCP/IP是个协议组,可分为三个层次:网络层、传输层和应用层。

在网络层有IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。
在传输层中有TCP协议与UDP协议。
在应用层有FTP、HTTP、TELNET、SMTP、DNS等协议。
因此,HTTP本身就是一个协议,是从Web服务器传输超文本到本地浏览器的传送协议。

TCP 是基于 TCP 协议实现的网络文本协议,属于传输层。
UDP 是和TCP 对等的,属于传输层,UDP 和 TCP 有重要的区别。

2、HTTP协议是建立在请求/响应模型上的。

首先由客户建立一条与服务器的TCP链接,并发送一个请求到服务器,请求中包含请求方法、URI、协议版本以及相关的MIME样式的消息。 服务器响应一个状态行,包含消息的协议版本、一个成功和失败码以及相关的MIME式样的消息。
HTTP/1.0为每一次HTTP的请求/响应建立一条新的TCP链接,因此一个包含HTML内容和图片的页面将需要建立多次的短期的TCP链接。一次TCP链接的建立将需要3次握手。
另外,为了获得适当的传输速度,则需要TCP花费额外的回路链接时间(RTT)。 每一次链接的建立需要这种经常性的开销,而其并不带有实际有用的数据,只是保证链接的可靠性, 因此HTTP/1.1提出了可持续链接的实现方法。HTTP/1.1将只建立一次TCP的链接而重复地使用它传输一系列的请求/响应消息,因此减少了链接建立的次数和经常性的链接开销。

三次握手的过程:
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;ACK:确认字符(Acknowledgement)
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

3、结论:虽然HTTP本身是一个协议,但其最终还是基于TCP的。

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

HTTPS:(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。 即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。

优点:协议较成熟,应用广泛、基于TCP/IP,拥有TCP优点、研发成本很低,开发快速、开源软件较多,nginx,apache,tomact等 缺点:无状态、无连接、只有PULL模式,不支持PUSH、数据报文较大 特性:基于TCP/IP应用层协议、无状态,无连接、支持C/S模式、适用于文本传输

TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂,我们这里只做简单、形象的介绍,你只要做到能够理解这个过程即可。 我们来看看这三次对话的简单过程:主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”, 这是第一次对话;主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话; 主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。 三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据。

TCP:(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂。 建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。

优点:可靠性 、全双工协议、开源支持多、应用较广泛、面向连接、研发成本低、报文内容不限制(IP层自动分包,重传,不大于1452bytes)
缺点:操作系统:较耗内存,支持连接数有限、设计:协议较复杂,自定义应用层协议、网络:网络差情况下延迟较高、传输:效率低于UDP协议特性: 面向连接、可靠性、全双工协议、基于IP层、OSI参考模型位于传输层、适用于二进制传输

三次握手 与 四次挥手 当客户端和服务器通过三次握手建立了TCP连接以后,当数据传送完毕,肯定是要断开TCP连接的啊。那对于TCP的断开连接,这里就有了神秘的“四次挥手”。
1.第一次挥手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
2.第二次挥手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我也没有数据要发送了,可以进行关闭连接了;
3.第三次挥手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入CLOSE_WAIT状态;
4.第四次挥手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
至此,TCP的四次挥手就这么愉快的完成了。

UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去! UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。 比如,我们经常使用“ping”命令来测试两台主机之间TCP/IP通信是否正常,其实“ping”命令的原理就是向对方主机发送UDP数据包, 然后对方主机确认收到数据包,如果数据包是否到达的消息及时反馈回来,那么网络就是通的。 例如,在默认状态下,一次“ping”操作发送4个数据包(如图2所示)。大家可以看到,发送的数据包数量是4包, 收到的也是4包(因为对方主机收到后会发回一个确认收到的数据包)。这充分说明了UDP协议是面向非连接的协议, 没有建立连接的过程。正因为UDP协议没有连接的过程,所以它的通信效果高;但也正因为如此,它的可靠性不如TCP协议高。 QQ就使用UDP发消息,因此有时会出现收不到消息的情况。

UDP:UDP是一个无连接协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据, 并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
优点:操作系统:并发高,内存消耗较低、传输:效率高,网络延迟低、传输模型简单,研发成本低
缺点:协议不可靠、单向协议、开源支持少、报文内容有限,不能大于1464bytes、设计:协议设计较复杂、网络:网络差,而且丢数据报文
特性:无连接,不可靠,基于IP协议层,OSI参考模型位于传输层,最大努力交付,适用于二进制传输

场景TCPUDP
是否连接面向连接面向非连接
传输可靠性可靠不可靠
应用场合传输大量数据少量数据
速度
个人笔记记录 2021 ~ 2025