HTML、CSS、JavaScript
css
- 盒模型 box-sizing
- 每个 HTML 元素都可以看作是一个矩形的盒子,这个盒子由四个部分组成:内容区域(content)、内边距(padding)、边框(border) 和 外边距(margin)。以下是盒模型的详细介绍:
- CSS 盒模型有两种模式:标准盒模型 和 IE 盒模型(也称为怪异模式盒模型)。
- 标准盒模型(content-box)
- 默认模式。
- 元素的
width
和height
仅表示内容区域的尺寸。 - 总宽度 =
width
+padding
+border
+margin
- 总高度 =
height
+padding
+border
+margin
- IE 盒模型(border-box)
- 元素的
width
和height
包含内容区域、内边距和边框的尺寸。 - 总宽度 =
width
+margin
- 总高度 =
height
+margin
- 通过
box-sizing: border-box;
设置。
- 元素的
- 标准盒模型(content-box)
- 水平垂直居中实现方式
- flex: 0 1 auto 分别代表了什么
- 实际上是
flex-grow
、flex-shrink
和flex-basis
这三个属性值的缩写- flex-grow(第一个值,这里是 0)
- 含义:定义了弹性项目的放大比例,即当弹性容器有多余空间时,该项目将按照这个比例来分配多余的空间。
- 取值解释:取值为 0 表示该弹性项目不会主动去占用弹性容器内的多余空间。无论弹性容器剩余多少空间,这个项目都不会变大。如果所有弹性项目的 flex-grow 值都为 0,那么多余的空间将不会分配给任何项目。
- flex-shrink
- 定义了弹性项目的缩小比例,即当弹性容器的空间不足以容纳所有项目时,该项目将按照这个比例来缩小自身大小
- 取值为
1
表示该弹性项目会参与缩小。当弹性容器空间不足时,项目会根据flex-shrink
的值和自身的初始大小按比例缩小,以适应容器的空间。如果所有项目的flex-shrink
值都为1
,那么它们会按相同的比例缩小。
- flex-basis
- 定义了弹性项目在分配多余空间或缩小之前的初始大小。
- 取值为
auto
表示弹性项目的初始大小由其内容决定,或者由width
(对于水平方向的弹性容器)或height
(对于垂直方向的弹性容器)属性来确定。如果没有设置width
或height
属性,那么项目会根据其内容自动调整大小。
- flex-grow(第一个值,这里是 0)
- 实际上是
- 对回流和重绘的理解
- 回流是指浏览器重新计算元素的几何属性(如尺寸、位置),并重新构建页面布局的过程。回流通常发生在以下情况:
- 页面首次加载时。
- 浏览器窗口大小改变时。
- 元素的尺寸、位置、内容发生变化时(如修改
width
、height
、padding
、margin
等属性)。 - 添加或删除可见的 DOM 元素时。
- 激活 CSS 伪类(如
:hover
)。 - 计算
offsetWidth
、offsetHeight
等布局属性时。
- 回流的特点:
- 回流是代价高昂的操作,因为它会触发整个或部分页面的重新布局。
- 回流范围可以是局部的(只影响部分元素)或全局的(影响整个页面)。
- 回流是指浏览器重新计算元素的几何属性(如尺寸、位置),并重新构建页面布局的过程。回流通常发生在以下情况:
- 重绘是指浏览器根据元素的样式属性(如颜色、背景、边框等)重新绘制元素的过程。重绘通常发生在以下情况:
- 元素的样式发生变化,但不影响布局时(如修改
color
、background-color
、visibility
等属性)。 - 回流一定会触发重绘,因为布局变化后需要重新绘制。
- 重绘的特点:
- 重绘的代价比回流低,因为它不涉及布局计算。
- 如果只修改不影响布局的属性,可以避免回流,只触发重绘。
- 元素的样式发生变化,但不影响布局时(如修改
js
判断一个对象是不是数组的 方法
使用
Array.isArray()
方法- js
const arr = [1, 2, 3] const obj = { key: 'value' } console.log(arr instanceof Array) // 输出: true console.log(obj instanceof Array) // 输出: false
使用
Object.prototype.toString.call()
方法Array.prototype.isPrototypeOf()
typeof null 返回值
typeof null
返回"object"
是一个历史遗留问题,需要注意区分。instanceof
用于检查对象是否是某个构造函数的实例,会沿着原型链查找。typeof
适用于所有数据类型,而instanceof
仅适用于对象类型。
有没有了解过类数组或者伪数组, 即arraylike这个概念, 以及如何把他们转换成真正的数组
- 常见的类数组对象包括:
arguments
对象(函数参数列表)。- DOM 元素集合(如
document.getElementsByTagName
返回的NodeList
)。 - 字符串(字符串可以通过索引访问字符,但字符串是不可变的)。
- 类数组(Array-like)是指具有以下特征的对象:
- 具有
length
属性:表示对象的元素个数。 - 通过索引访问元素:可以通过
[0]
、[1]
等方式访问元素。 - 不是数组:没有数组的方法(如
push
、pop
、forEach
等)。
- 具有
- 转换方式
Array.from()
是 ES6 提供的方法,可以将类数组或可迭代对象转换为真正的数组。- 扩展运算符(
...
) - 通过
Array.prototype.slice
方法将类数组转换为数组
- 常见的类数组对象包括:
双等号, 三等号和Object.is的区别
Object.is()
方法的比较规则与三等号类似,但在处理特殊值时有所不同:- 对于
NaN
,Object.is(NaN, NaN)
返回true
,而NaN === NaN
返回false
。 - 对于正负零,
Object.is(+0, -0)
返回false
,而+0 === -0
返回true
。 - 三等号在比较时不会进行类型转换,只有当两个值的类型相同且值也相等时,才会返回
true
。
- 对于
对事件循环的理解
是 JavaScript 实现异步编程的核心机制,它使得 JavaScript 能够在单线程环境下高效地处理异步任务(如 I/O 操作、定时器、网络请求等)
事件循环的核心是 任务队列 和 事件循环机制。JavaScript 引擎通过事件循环不断地从任务队列中取出任务并执行。
任务队列
任务队列分为两种:
- 宏任务队列(MacroTask Queue):
- 包含的任务:
setTimeout
、setInterval
、setImmediate
(Node.js)、I/O 操作、UI 渲染等。 - 每次事件循环会从宏任务队列中取出一个任务执行。
- 包含的任务:
- 微任务队列(MicroTask Queue):
- 包含的任务:
Promise.then
、MutationObserver
、process.nextTick
(Node.js)等。 - 每次事件循环会清空微任务队列中的所有任务。
- 包含的任务:
- 宏任务队列(MacroTask Queue):
事件循环的执行顺序
- 从宏任务队列中取出一个任务执行。
- 执行完当前宏任务后,清空微任务队列中的所有任务。
- 如果需要,进行 UI 渲染。
- 重复上述过程。
- 以下是事件循环的具体步骤:
- 执行同步代码:
- 同步代码是立即执行的,不会被放入任务队列。
- 处理微任务:
- 每当一个宏任务执行完毕后,事件循环会立即清空微任务队列中的所有任务。
- 处理宏任务:
- 从宏任务队列中取出一个任务执行。
- 执行完毕后,再次检查微任务队列并清空。
- UI 渲染:
- 如果需要,浏览器会进行 UI 渲染。
- 重复循环:
- 重复上述过程,直到任务队列为空。
- 执行同步代码:
事件循环的应用场景
定时器
setTimeout
和setInterval
是宏任务,它们的回调函数会被放入宏任务队列。- 定时器的延迟时间并不是精确的,而是至少延迟指定时间。
Promise
Promise.then
是微任务,会在当前宏任务执行完毕后立即执行。- 微任务的优先级高于宏任务。
异步 I/O
- 如
fetch
、XMLHttpRequest
等异步操作的回调函数会被放入宏任务队列。
- 如
UI 渲染
- 浏览器会在事件循环的适当时机进行 UI 渲染。
Node.js 的事件循环与浏览器类似,但有一些额外的阶段:
- Timers:处理
setTimeout
和setInterval
的回调。 - Pending Callbacks:执行系统操作的回调(如 TCP 错误)。
- Idle, Prepare:内部使用。
- Poll:检索新的 I/O 事件,执行 I/O 回调。
- Check:处理
setImmediate
的回调。 - Close Callbacks:处理关闭事件的回调(如
socket.on('close')
- Timers:处理
ts
简单说下interface和type的区别
interface
:使用interface
关键字来定义,语法较为直观,主要用于定义对象类型。可以使用
extends
关键字进行扩展,实现接口的继承,从而创建新的接口。可以对同一个接口进行多次定义,TypeScript 会自动将这些定义合并。
主要用于定义对象类型,不能直接定义基本类型、联合类型或交叉类型(虽然可以通过扩展来组合类型,但不如类型别名直接)。
interface
:可以被类实现(implements
),用于约束类的结构。- js
interface Shape { area(): number; } class Circle implements Shape { constructor(private radius: number) {} area() { return Math.PI * this.radius * this.radius; } }
interface
:不能直接用于创建映射类型。
使用
type
关键字定义类型别名,可以定义各种类型,包括基本类型、联合类型、交叉类型等。- 使用交叉类型(
&
)来实现类似扩展的效果。 - 类型别名不允许重复定义,重复定义会导致编译错误。
- 可以定义基本类型、联合类型、交叉类型等,非常灵活。
- 类型别名也可以用于类的实现,但在一些复杂类型的场景下,
interface
会更具语义化。 type
:可以方便地用于创建映射类型,例如:
- 使用交叉类型(
列举一下过往使用过的工具类型,比如required, readonly
Required<T>
作用:将类型
T
中的所有可选属性转换为必选属性。Readonly<T>
- 将类型
T
中的所有属性转换为只读属性,意味着这些属性一旦被赋值,就不能再被修改。 - ts
interface Point { x: number y: number } type ReadonlyPoint = Readonly<Point> const readonlyPoint: ReadonlyPoint = { x: 1, y: 2 } // 下面这行代码会报错,因为 x 属性是只读的 // readonlyPoint.x = 3;
- 将类型
Partial<T>
- 将类型
T
中的所有属性转换为可选属性。 - tsx
interface Todo { title: string description: string } type PartialTodo = Partial<Todo> const partialTodo: PartialTodo = { title: 'New Todo' }
- 将类型
Pick<T, K>
- 从类型
T
中选取一组属性K
来构造一个新的类型。 - ts
interface Todo { title: string description: string completed: boolean } type PickTodo = Pick<Todo, 'title' | 'completed'> const pickTodo: PickTodo = { title: 'New Todo', completed: false, }
- 从类型
Omit<T, K>
- 从类型
T
中移除一组属性K
来构造一个新的类型。 - ts
interface Todo { title: string description: string completed: boolean } type OmitTodo = Omit<Todo, 'description'> const omitTodo: OmitTodo = { title: 'New Todo', completed: false, }
- 从类型
Exclude<T, U>
- 从类型
T
中排除可以赋值给类型U
的那些类型,返回一个新的类型。 - ts
type T = 'a' | 'b' | 'c' type U = 'b' type Excluded = Exclude<T, U> // Excluded 的类型为 'a' | 'c'
- 从类型
Extract<T, U>
- 从类型
T
中提取可以赋值给类型U
的那些类型,返回一个新的类型。 - ts
type T = 'a' | 'b' | 'c' type U = 'b' | 'd' type Extracted = Extract<T, U> // Extracted 的类型为 'b'
- 从类型
扩展
- 前端缓存机制
- 说一下客户端缓存 服务端缓存 http缓存 浏览器缓存各自的特点
- 客户端缓存是指将数据存储在客户端(如浏览器)中,以减少对服务器的请求。
- 存储位置:浏览器内存或本地存储(如
localStorage
、sessionStorage
、IndexedDB
)。 - 数据类型:静态资源(如 HTML、CSS、JS)、API 响应数据等。
- 生命周期:
localStorage
:永久存储,除非手动清除。sessionStorage
:会话级别存储,关闭标签页后失效。IndexedDB
:持久化存储,适合大量结构化数据。
- 适用场景:
- 缓存静态资源。
- 缓存 API 响应数据,减少重复请求。
- 存储位置:浏览器内存或本地存储(如
- 服务端缓存是指将数据存储在服务器端,以减少数据库查询或重复计算。
- 存储位置:服务器内存或缓存服务(如 Redis、Memcached)。
- 数据类型:数据库查询结果、计算结果、静态资源等。
- 生命周期:由缓存策略决定,可以设置过期时间或手动清除。
- 适用场景:
- 缓存频繁访问的数据(如热门文章、用户信息)。
- 缓存计算结果(如排行榜、统计数据)。
- HTTP 缓存是指通过 HTTP 协议实现的缓存机制,利用 HTTP 头字段控制资源的缓存行为。
- 存储位置:浏览器缓存或代理服务器缓存。
- 数据类型:静态资源(如 HTML、CSS、JS、图片)。
- 缓存策略:
- 强缓存:通过
Cache-Control
和Expires
头字段控制。Cache-Control: max-age=3600
:资源在 3600 秒内有效。Expires: Wed, 21 Oct 2023 07:28:00 GMT
:资源在指定时间前有效。
- 协商缓存:通过
Last-Modified
和ETag
头字段控制。Last-Modified
:资源的最后修改时间。ETag
:资源的唯一标识符。
- 强缓存:通过
- 适用场景:
- 缓存静态资源,减少重复下载。
- 浏览器缓存是指浏览器将资源存储在本地,以便快速加载。
- 存储位置:浏览器内存或磁盘。
- 数据类型:静态资源(如 HTML、CSS、JS、图片)。
- 缓存策略:
- 内存缓存(Memory Cache):快速访问,但容量有限。
- 磁盘缓存(Disk Cache):容量较大,但访问速度较慢。
- 适用场景:
- 缓存静态资源,减少重复下载。
- 客户端缓存是指将数据存储在客户端(如浏览器)中,以减少对服务器的请求。
- 说一下客户端缓存 服务端缓存 http缓存 浏览器缓存各自的特点
- http2 的特点
- 二进制分帧
- 原理:HTTP/2 将所有传输的信息分割为更小的消息和帧,并采用二进制格式编码。在 HTTP/2 中,同域名下所有通信都在单个连接上完成,这个连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成,不同类型的帧携带不同类型的数据。
- 优势:二进制分帧使得协议解析更高效,并且在性能和扩展性上有显著提升,解决了 HTTP/1.x 文本协议解析复杂的问题。
- 多路复用
- 原理:在一个 TCP 连接上可以同时发送多个请求和响应,并且这些请求和响应可以交错进行,互不影响。每个请求和响应都有自己的数据流 ID,通过数据流 ID 来区分不同的请求和响应。
- 优势:避免了 HTTP/1.x 中由于串行请求导致的 “队头阻塞” 问题,大大提高了传输效率,充分利用网络带宽。
- 头部压缩
- 原理:HTTP/2 使用 HPACK 算法对请求和响应的头部进行压缩。它会维护一个静态表和一个动态表,静态表包含了常见的头部字段和值,动态表会根据实际传输的头部信息进行动态更新。当发送头部时,如果头部字段和值已经存在于表中,只需要发送对应的索引,从而减少了头部数据的传输量。
- 优势:减少了头部信息的传输开销,尤其对于移动网络等带宽有限的场景,能显著提高传输效率。
- http报文头部有哪些字段? 有什么意义 ?
- 请求头
User - Agent
- 意义:用于标识客户端的类型和版本信息,服务器可以根据该字段判断客户端的类型(如浏览器、移动应用等),并返回合适的内容。例如,不同的浏览器对某些 HTML、CSS 特性的支持可能不同,服务器可以根据
User - Agent
字段返回适配的页面。
- 意义:用于标识客户端的类型和版本信息,服务器可以根据该字段判断客户端的类型(如浏览器、移动应用等),并返回合适的内容。例如,不同的浏览器对某些 HTML、CSS 特性的支持可能不同,服务器可以根据
Accept
- 意义:告诉服务器客户端能够接受的响应内容类型。例如,
Accept: text/html,application/xhtml+xml,application/xml;q = 0.9,*/*;q = 0.8
表示客户端优先接受 HTML、XHTML 和 XML 格式的响应,对于其他格式的响应接受度较低。
- 意义:告诉服务器客户端能够接受的响应内容类型。例如,
Accept - Encoding
- 意义:表示客户端支持的内容编码方式,如
gzip
、deflate
等。服务器可以根据该字段对响应内容进行压缩,以减少传输数据量。
- 意义:表示客户端支持的内容编码方式,如
Cookie
- 意义:用于在客户端和服务器之间传递会话信息。服务器可以通过
Set - Cookie
响应头字段设置 Cookie,客户端在后续请求中会携带这些 Cookie,以便服务器识别用户身份和维护会话状态。
- 意义:用于在客户端和服务器之间传递会话信息。服务器可以通过
Referer
- 意义:表示请求的来源页面的 URL。服务器可以根据该字段了解用户是从哪个页面跳转过来的,有助于进行流量分析和防止恶意请求。
- 响应头
Content - Type
- 意义:指示响应内容的类型,如
text/html
、application/json
等。客户端根据该字段来正确解析响应内容。
- 意义:指示响应内容的类型,如
Content - Length
- 意义:表示响应内容的长度(字节数)。客户端可以根据该字段判断是否已经接收到完整的响应内容。
Set - Cookie
- 意义:用于服务器向客户端设置 Cookie。可以设置 Cookie 的名称、值、过期时间、路径等信息,以便在后续请求中识别用户。
Cache - Control
- 意义:用于控制缓存策略,如
no - cache
表示不使用缓存,max - age = 3600
表示缓存的有效时间为 3600 秒。
- 意义:用于控制缓存策略,如
Location
- 意义:通常在重定向响应(状态码为 3xx)中使用,指示客户端需要重定向到的新 URL。
- 请求头
- 二进制分帧
- https加密原理,中间人攻击知道吗
- 对称加密和非对称加密结合
- 对称加密:使用相同的密钥进行加密和解密,加密和解密速度快,但密钥的分发和管理存在安全风险。
- 非对称加密:使用一对密钥,即公钥和私钥。公钥可以公开,用于加密数据;私钥只有所有者持有,用于解密数据。非对称加密的安全性高,但加密和解密速度相对较慢。
- HTTPS 加密过程
- 客户端向服务器发送请求:客户端向服务器发送请求,包含客户端支持的加密算法、SSL/TLS 版本等信息。
- 服务器响应:服务器选择一个加密算法和 SSL/TLS 版本,并向客户端发送证书(包含服务器的公钥)和协商的加密算法等信息。
- 客户端验证证书:客户端验证服务器证书的有效性,包括证书的颁发机构、有效期等。如果证书有效,客户端会生成一个会话密钥(对称密钥),并使用服务器的公钥对会话密钥进行加密。
- 客户端发送加密的会话密钥:客户端将加密后的会话密钥发送给服务器。
- 服务器解密会话密钥:服务器使用自己的私钥解密客户端发送的加密会话密钥,得到会话密钥。
- 数据传输:客户端和服务器使用会话密钥进行对称加密通信,确保数据在传输过程中的保密性和完整性。
- 中间人攻击
- 原理:中间人攻击(Man - in - the - Middle Attack,MITM)是一种攻击方式,攻击者在客户端和服务器之间拦截通信,并伪装成客户端和服务器与双方进行通信。攻击者可以窃取、篡改或伪造通信数据,而客户端和服务器可能无法察觉。
- 防范措施
- 证书验证:客户端在建立 HTTPS 连接时,要严格验证服务器证书的有效性,确保证书是由受信任的证书颁发机构(CA)颁发的,并且证书中的域名与服务器的实际域名一致。
- SSL/TLS 协议升级:使用最新版本的 SSL/TLS 协议,因为新版本通常修复了旧版本中存在的安全漏洞,提高了加密的安全性。
- HSTS(HTTP 严格传输安全):服务器可以通过设置 HSTS 头字段,强制客户端只能使用 HTTPS 进行通信,避免客户端被重定向到 HTTP 连接,从而防止中间人通过拦截 HTTP 请求进行攻击。
- 对称加密和非对称加密结合
前端工程化工具(如ESLint、Prettier、Commitlint、Husky)
写过工具吗 发布过npm包吗
pnpm 实现原理 以及 monorepo 架构
- pnpm 通过硬链接和符号链接优化了依赖管理,减少了磁盘占用和安装时间。
- Monorepo 通过单一仓库管理多个项目,提升了代码复用性和开发效率。
- 结合 pnpm 和 Monorepo 工具(如 Lerna、Nx),可以构建高效、可维护的大型项目。
实现原理
pnpm(Performant NPM)是一个高效的包管理工具,旨在解决 npm 和 Yarn 在磁盘空间和安装速度上的问题。其核心原理是通过硬链接和符号链接来共享依赖,从而减少磁盘占用和提升安装速度。
- 硬链接
- 硬链接是文件系统中指向同一文件内容的多个路径。pnpm 将所有依赖包存储在全局存储中,然后在项目中通过硬链接引用这些包。
- 多个项目共享同一份依赖,节省磁盘空间。
- 安装速度快,因为不需要重复下载和复制文件。
- 符号链接是指向另一个文件或目录的快捷方式。pnpm 使用符号链接将项目的
node_modules
中的依赖指向全局存储中的实际文件。- 保持
node_modules
的扁平结构,避免依赖冲突。 - 支持多版本依赖共存。
- 保持
- 依赖隔离
- pnpm 为每个项目创建一个独立的
node_modules
目录,并通过符号链接将依赖指向全局存储。 - 避免依赖冲突,支持多版本共存。
- 保持项目的依赖结构清晰。
- pnpm 为每个项目创建一个独立的
- 工作流程
- 安装依赖:
- 检查全局存储中是否已存在依赖包。
- 如果存在,则通过硬链接引用;如果不存在,则下载并存储到全局存储中。
- 创建符号链接:
- 在项目的
node_modules
中创建符号链接,指向全局存储中的依赖包。
- 在项目的
- 依赖隔离:
- 每个项目都有独立的
node_modules
,避免依赖冲突。
- 每个项目都有独立的
- 安装依赖:
monorepo
Monorepo 是一种将多个项目或包存储在同一个代码仓库中的开发模式。它广泛应用于大型项目或库的开发中,能够提升代码复用性和开发效率。
Monorepo 的特点
- 单一仓库:所有项目或包存储在同一个代码仓库中。
- 共享依赖:多个项目可以共享相同的依赖,减少重复安装。
- 统一构建:可以通过统一的工具链(如 Lerna、Nx)管理构建、测试和发布。
- 代码复用:可以轻松共享工具函数、组件和配置。
Monorepo 的优势
- 代码复用性高:共享代码和依赖,减少重复开发。
- 开发效率高:统一工具链和配置,简化开发流程。
- 依赖管理方便:通过工具(如 pnpm、Lerna)统一管理依赖版本。
- 协作更方便:所有代码在一个仓库中,便于团队协作和代码审查。
Monorepo 的挑战
- 仓库体积大:所有项目存储在一个仓库中,可能导致仓库体积过大。
- 构建复杂度高:需要统一的工具链管理构建和测试。
- 权限管理复杂:需要精细的权限控制,避免误操作。
pnpm 与 Monorepo 的结合
pnpm 是 Monorepo 的理想选择,因为它通过硬链接和符号链接优化了依赖管理,减少了磁盘占用和安装时间。以下是 pnpm 在 Monorepo 中的应用:
(1)安装依赖
- 使用
pnpm install
安装所有项目的依赖。 - 依赖存储在全局存储中,通过硬链接引用。
(2)递归命令
使用
pnpm -r
执行递归命令。示例:
bash
复制
pnpm -r run build # 递归构建所有项目 pnpm -r run test # 递归测试所有项目
(3)工作空间(Workspace)
使用
pnpm-workspace.yaml
配置 Monorepo 的工作空间。示例:
yaml
复制
packages: - 'packages/*' - 'apps/*'
什么是灰度
灰度发布(Gray Release),也称为金丝雀发布(Canary Release),是一种在软件系统更新过程中,逐步将新功能或新版本的软件推向部分用户或部分服务器的发布策略。在灰度发布期间,新功能或新版本会在小范围内进行测试和验证,收集反馈和数据,评估其稳定性和性能,然后再根据结果决定是否逐步扩大推广范围,最终覆盖到所有用户。
注意事项
在灰度发布过程中,需要建立有效的监控机制,实时收集用户的反馈和数据,如页面加载时间、用户操作行为、错误日志等。如果发现新前端版本存在严重问题,应及时回滚到旧前端版本,确保用户体验不受影响。可以通过配置管理系统或者手动操作来实现回滚。例如,在发现新前端版本的某个功能出现大量报错时,通过修改配置文件或者执行脚本,将所有用户的流量重新导向旧前端版本。
基于用户特征的灰度发布
原理
根据用户的特定特征(如用户 ID、用户角色、地理位置等)来决定用户是否能够访问新的前端版本。这种方式可以精准地控制哪些用户能够体验新功能。
实现步骤
- 确定用户特征规则:例如,设定用户 ID 为偶数的用户可以访问新前端版本。
- 在前端代码中获取用户特征信息:可以从用户登录信息、cookie 或者后端接口中获取用户特征。
- 根据特征信息进行版本分发:根据获取的用户特征信息,判断用户是否符合访问新前端版本的规则。+
基于流量比例的灰度发布
按照一定的流量比例将用户分配到新前端版本和旧前端版本。例如,将 10% 的用户流量导向新前端版本,90% 的用户流量导向旧前端版本。
基于版本号的灰度发布
原理
通过管理前端版本号,根据用户使用的版本号来决定是否升级到新的前端版本。这种方式适用于需要逐步推进版本更新的场景。
实现步骤
- 记录用户当前版本号:可以将用户当前使用的前端版本号存储在 cookie 或者本地存储中。
- 检查是否有新版本:在用户访问页面时,从后端接口获取最新的前端版本号,并与用户当前版本号进行比较。
- 根据比较结果进行版本分发:如果有新版本,且符合升级规则,则引导用户升级到新前端版本。
Node.js Express.js
- 具备使用Express.js框架成功实施多个项目的能力。
用的什么数据库 以及高速缓存
用的什么ORM框架
熟悉nest 追问 ioc控制反转 DI 依赖注入 设计模式
MVC 架构 和MVVM 区别
nest rxjs 库用法 以及原理
nest 微服务 gRPC MQ 以及网关
express 中间件原理
基本概念
- 在 Express 里,中间件(Middleware)是一个函数,它可以访问请求对象(
req
)、响应对象(res
),以及应用程序请求 - 响应循环中的下一个中间件函数(通常用next
表示)。中间件函数的主要作用是处理请求、修改请求和响应对象,或者结束请求 - 响应循环。 - Express 应用程序是由一系列中间件函数组成的处理链。当有请求到达时,Express 会按照中间件函数的注册顺序依次执行它们,每个中间件函数可以选择继续将控制权传递给下一个中间件(通过调用
next()
函数),或者直接结束请求 - 响应循环(通过发送响应)。
- 在 Express 里,中间件(Middleware)是一个函数,它可以访问请求对象(
中间件函数的基本形式
- js
const express = require('express'); const app = express(); // 中间件函数 const myMiddleware = (req, res, next) => { // 可以在这里对请求进行处理,比如添加请求头信息 req.customData = 'This is custom data added by middleware'; // 调用 next() 函数将控制权传递给下一个中间件 next(); }; // 使用中间件 app.use(myMiddleware); // 路由处理函数 app.get('/', (req, res) => { // 可以访问中间件添加的自定义数据 console.log(req.customData); res.send('Hello, World!'); }); const port = 3000; app.listen(port, () => { console.log(`Server is running on port ${port}`); });
常用中间件
内置
express.static
作用:用于托管静态文件,如 HTML、CSS、JavaScript、图片等。它会根据请求的路径在指定的目录中查找对应的文件并返回给客户端。
- js
const express = require('express'); const app = express(); // 托管 public 目录下的静态文件 app.use(express.static('public')); const port = 3000; app.listen(port, () => { console.log(`Server is running on port ${port}`); });
xpress.json
- 作用:解析请求体中的 JSON 数据,并将解析后的数据存储在
req.body
中。
- 作用:解析请求体中的 JSON 数据,并将解析后的数据存储在
express.urlencoded
- 作用:解析 URL 编码的请求体数据,并将解析后的数据存储在
req.body
中。通常用于处理表单提交的数据。
- 作用:解析 URL 编码的请求体数据,并将解析后的数据存储在
第三方中间件
morgan
- 作用:HTTP 请求日志中间件,用于记录每个请求的详细信息,如请求方法、请求路径、响应状态码、响应时间等。这对于调试和监控应用程序非常有帮助。
cors
作用:处理跨域资源共享(CORS)问题。在前后端分离的开发中,由于浏览器的同源策略,不同源的前端应用无法直接访问后端 API,使用
cors
中间件可以允许跨域请求。- js
const express = require('express'); const cors = require('cors'); const app = express(); // 使用 cors 中间件允许跨域请求 app.use(cors()); app.get('/api/data', (req, res) => { res.json({ message: 'This is some data' }); }); const port = 3000; app.listen(port, () => { console.log(`Server is running on port ${port}`); });
helmet
作用:帮助设置 HTTP 头部,增强应用程序的安全性。它可以防止一些常见的安全漏洞,如跨站脚本攻击(XSS)、点击劫持等。
- js
const express = require('express'); const helmet = require('helmet'); const app = express(); // 使用 helmet 中间件增强安全性 app.use(helmet()); app.get('/', (req, res) => { res.send('Secure Express App'); }); const port = 3000; app.listen(port, () => { console.log(`Server is running on port ${port}`); });
Express 如何处理异步请求,避免回调地狱?
- Express 处理异步请求时,推荐使用
async/await
代替传统的 回调函数 或then/catch
,并用try/catch
捕获错误。
在 Express 里实现 JWT 认证
- 可以使用
jsonwebtoken
进行用户身份验证。
Express 中实现 CORS 跨域
- 可以使用
cors
中间件来自动处理跨域。
node 大量日志怎么处理的(缓冲队列/采样率降低等)
- 缓冲队列的核心思想是将日志先存储在内存中的队列里,当队列达到一定的长度或者经过了特定的时间间隔后,再批量将这些日志写入到存储介质(如文件、数据库)中。这样做的好处是减少了频繁 I/O 操作带来的性能开销,因为批量写入比频繁的单条写入更加高效。
- 高并发写入场景:当应用面临大量并发的日志写入请求时,频繁的磁盘 I/O 操作会成为性能瓶颈。缓冲队列可以将这些写入请求暂存起来,然后批量写入磁盘,减少了 I/O 操作的次数,从而提高性能。例如,一个高流量的 Web 服务器,每秒可能会产生大量的访问日志,如果每条日志都立即写入磁盘,会严重影响服务器的响应性能,使用缓冲队列可以有效缓解这个问题。
- 磁盘 I/O 性能有限:如果应用运行的服务器磁盘 I/O 性能较差,频繁的单条日志写入会导致磁盘负载过高,甚至出现性能抖动。缓冲队列通过批量写入的方式,降低了磁盘的 I/O 频率,提高了磁盘的利用率。
- 需要完整日志记录:在某些业务场景下,需要完整记录所有的日志信息,以便进行后续的审计、故障排查等工作。例如,金融系统的交易日志、医疗系统的患者数据操作日志等,这些日志数据的完整性至关重要,不能采用采样率降低的方法,而缓冲队列可以在保证数据完整性的前提下,提高日志写入的性能。
- 数据一致性要求高:缓冲队列可以确保日志数据按照产生的顺序进行批量写入,保证了数据的一致性。在一些对数据顺序敏感的场景中,如分布式系统的日志同步,缓冲队列是更好的选择。
- 采样率降低是指并非记录每一条日志,而是按照一定的规则有选择性地记录部分日志。例如,每记录 10 条日志中的 1 条,或者根据特定的条件(如请求的响应时间超过某个阈值)来决定是否记录日志。这样可以在不损失太多重要信息的前提下,显著减少日志的记录量。
- 日志写入性能达到极限:当即使使用了缓冲队列,日志的写入性能仍然无法满足需求时,采样率降低是一种有效的解决方案。通过减少日志的记录量,可以直接降低日志写入的压力,提高系统的整体性能。例如,在一些实时数据处理系统中,每秒会产生海量的日志数据,即使使用缓冲队列也难以处理,此时降低采样率可以显著减轻系统负担。
- 允许一定数据损失:如果业务场景对日志数据的完整性要求不是非常严格,允许一定程度的数据损失,那么可以考虑使用采样率降低的方法。例如,一些统计分析类的日志,如用户行为统计日志,偶尔丢失一些日志记录对整体的统计结果影响不大,此时降低采样率可以减少日志存储和处理的成本。
- 日志分级与过滤。将日志按照重要程度分为不同的级别,如
debug
、info
、warn
、error
等。在不同的环境下,可以根据实际需求过滤掉一些不必要的日志级别。例如,在生产环境中,只记录warn
和error
级别的日志,这样可以减少日志的产生量。 - 日志分割与归档。随着日志量的不断增加,单个日志文件会变得越来越大,这不仅会影响文件的读写性能,还会给日志的管理和分析带来困难。日志分割与归档就是按照一定的规则(如按时间、文件大小)将日志文件分割成多个小文件,并对旧的日志文件进行归档存储。
- 日志聚合工具(如 Elasticsearch、Logstash、Kibana 组成的 ELK 栈,或者 Graylog 等)可以收集、存储和分析大量的日志数据。这些工具通常具有分布式存储、高效的查询和分析功能,能够处理大规模的日志数据。
node 与其他语言有什么区别,其优劣势是什么。
- Node.js 是一个基于 V8 引擎 的 JavaScript 运行时,最适合构建高并发、I/O 密集型的应用,如 Web 服务器、实时通信、微服务、前端 SSR 渲染 等。
- 高并发性能
- 基于事件驱动(Event Loop)+ 非阻塞 I/O
- 单线程处理 多个请求,避免创建线程的开销
- 适合 API 服务器、WebSocket 实时通信
- CPU 密集型任务性能较差
- 单线程适合 I/O 密集任务,但 不擅长处理 CPU 密集型任务
- 计算量大的任务可能会 阻塞主线程
node 的V8 引擎的垃圾回收(GC)机制
管理 内存分配 和 回收不再使用的对象。
V8 将 内存(Heap) 主要分为两大区域:
- 新生代(Young Generation)
- 存放短生命周期的小对象
- 临时变量、函数局部变量
- Scavenge 算法(复制 + 标记清除)。
- 新生代堆内存 分成两部分(From 空间 和 To 空间)。
- GC 过程:
- 存活的对象被复制到 To 空间,垃圾对象被回收。
- 完成后交换 From 和 To 空间(复制 + 交换)。
- 高效(只回收新生代,速度快)
- ❌ 仅适用于小对象(大对象频繁复制效率低)
- 老生代(Old Generation)
- 存放生命周期长的大对象
- 复杂对象、全局变量、缓存
- Mark-Sweep & Mark-Compact(老生代 GC)
- 针对老生代(Old Generation),采用 Mark-Sweep & Mark-Compact(标记-清除 & 标记-整理)。
- GC 过程:
- 标记(Mark):遍历所有对象,标记 仍在使用 的对象。
- 清除(Sweep):回收未标记的对象,释放内存。
- 整理(Compact)(部分情况下):防止内存碎片化,移动对象,使内存连续。
- 新生代(Young Generation)
V8 采用增量标记(Incremental Marking)优化 GC,将一次性标记变为分步标记,减少 STW 时间。
并行垃圾回收:让 GC 在 JavaScript 运行时 间歇性执行,降低停顿时间。
Webpack与Vite进行项目构建与优化。
webpack,vite,rollup的区别
Webpack 是一个功能强大且高度可定制的模块打包工具,它将一切文件(如 JavaScript、CSS、图片等)都视为模块,并通过各种 loader 和 plugin 对这些模块进行处理和打包。Webpack 采用的是打包优先的理念,会在构建时将所有模块递归地解析和打包成一个或多个文件,适合处理复杂的项目结构和大量的资源依赖。
- 支持各种类型的模块,包括 CommonJS、AMD、ES 模块等,并且可以通过 loader 处理不同类型的文件,如 CSS、图片、字体等。Webpack 可以处理复杂的模块依赖关系,对模块进行代码分割、懒加载等操作。
- 拥有庞大的插件生态系统,几乎可以满足任何项目的需求。通过各种插件,Webpack 可以实现代码压缩、代码分割、CSS 提取、环境变量注入等功能。
Vite 是一个基于原生 ES 模块的构建工具,它的设计理念是快速冷启动和即时热更新。Vite 在开发阶段利用浏览器原生的 ES 模块支持,直接在浏览器中加载模块,而不需要像 Webpack 那样进行预打包,只有在生产环境才进行打包优化。这种方式使得开发服务器启动速度极快,尤其适合开发大型项目。
- 主要基于原生 ES 模块,在开发阶段直接使用浏览器的 ES 模块加载机制。对于非 ES 模块的文件,Vite 也可以通过插件进行处理。Vite 在生产环境下会将代码打包成适合生产的格式。
Rollup 专注于 JavaScript 模块的打包,它的设计理念是简洁高效,强调对 ES 模块的支持。Rollup 主要用于打包 JavaScript 库,它会分析模块之间的依赖关系,将代码进行 Tree - Shaking(去除未使用的代码),生成干净、简洁的包,适合构建体积小、依赖少的库。
- 专注于 ES 模块的打包,对 ES 模块的支持非常好,能够进行有效的 Tree - Shaking,去除未使用的代码。Rollup 也支持一些其他模块格式,但主要还是以 ES 模块为主。
- 主要用于打包 JavaScript 库,能够生成体积小、性能高的库文件。对于需要发布到 npm 等平台的 JavaScript 库,Rollup 是一个很好的选择。
webpack 分包 webpack 打包优化 webpack打包原理 以及 HMR 原理
Vite原理 中间件原理 是否写过vite插件
Vite 的核心原理基于 ES Module(ESM)+ Rollup + Koa,它的核心流程可以分为:
- 开发阶段:利用 原生 ESM,通过 依赖预构建(esbuild) 和 按需编译 来加速热更新,避免了 Webpack 传统的 Bundle 过程。
- 构建阶段:Vite 采用 Rollup 进行生产构建,最终输出优化后的静态资源。
Vite 开发服务器基于 Koa,实现了类似 Express/Koa 的中间件机制,包括:
- 静态资源中间件:拦截并返回
/public
下的静态资源。 - 模块解析中间件:拦截
import
语句,转换为 ESM 形式,并处理别名、路径等。 - HMR(热更新)中间件:监听文件变更,推送 WebSocket 消息,触发前端模块热替换。
Vite 插件的 钩子机制(如 config
, transform
, configureServer
等),可以基于不同阶段做自定义扩展。
插件
Mock 数据插件:拦截
/api/
请求,返回本地 JSON 数据,避免开发时依赖后端。实现方式:
拦截
/api/
请求,读取本地 JSON 或 TS/JS 文件,返回数据。支持热更新,当 Mock 文件变更时自动刷新 API 响应。
支持动态配置,可通过
config
决定是否启用 Mock。
自动导入插件:自动导入 Vue 组件、工具函数,类似 unplugin-auto-import
,提高开发效率。
HMR 相关优化:监听某些特定文件的变更,实现更精细的热更新逻辑。
钩子
config(config, env)
- 作用:修改 Vite 配置(在
vite.config.ts
解析前)。 - 使用场景:自动启用 Mock、动态修改
base
路径等。
- 作用:修改 Vite 配置(在
handleHotUpdate(ctx)
作用:监听文件变更,控制 HMR 逻辑。
使用场景:优化 JSON 热更新、局部刷新 Vue 组件等。
configureServer(server)
作用:在开发服务器启动时,注入 Koa 中间件。
使用场景:Mock API、WebSocket 通信、文件监听等。
Rollup 兼容钩子
resolveId(source, importer)
- 作用:自定义模块解析路径,适合别名或虚拟模块。
- 使用场景:扩展
@
解析、Mock 虚拟文件等。
transform(code, id)
- 作用:修改文件内容,如 JSX 编译、Vue SFC 处理等。
- 使用场景:对 TS/JS 代码做 AST 转换。
esBuild 常用命令 以及 为什么这么快
常用命令
- 基础构建命令
- esbuild src/index.ts --bundle --outfile=dist/bundle.js
--bundle
:将所有依赖打包成一个文件。--outfile=dist/bundle.js
:输出文件路径。
- esbuild src/index.ts --bundle --outfile=dist/bundle.js
- 监听文件变更(Watch Mode)
- esbuild src/index.ts --bundle --outfile=dist/bundle.js --watch
- 开启开发服务器
- esbuild src/index.ts --bundle --outfile=dist/bundle.js --serve=8080
- 代码压缩(Minify)
- esbuild src/index.ts --bundle --minify --outfile=dist/bundle.js
- 指定es版本
- esbuild src/index.ts --bundle --target=esnext --outfile=dist/bundle.js
- 生成sourcemap
- esbuild src/index.ts --bundle --sourcemap --outfile=dist/bundle.js
为什么 esBuild 这么快
esBuild 之所以比 Webpack、Rollup 等传统打包工具快 10~100 倍
采用 Go 语言编写
- Go 语言 是一个 高性能、并发友好 的编程语言,比 JavaScript 执行效率高得多。
- 传统工具(Webpack/Rollup)基于 JavaScript(单线程) 运行,而
esbuild
利用 Go 的并发模型,充分利用 CPU 多核能力。
esbuild
在 多个 CPU 核心上同时进行解析、转换、压缩,而 Webpack/Rollup 主要是单线程执行。
例如,Webpack 在构建大项目时 按模块串行解析,而 esbuild
直接 并发解析整个依赖树,大幅减少等待时间。
避免 AST 二次解析
- 传统工具(Webpack、Babel)构建流程:
- 读取源码 → 2. 解析成 AST → 3. 转换 AST → 4. 再生成代码
esbuild
直接在 解析时就完成转换,避免了 AST 解析-转换-再解析 的性能开销。
内置高效的 Tree Shaking
esbuild
直接基于 AST 进行 高效 Tree Shaking,避免了 Rollup/Webpack 在构建阶段的额外处理,提升构建速度。
** 直接操作二进制数据**
- Webpack 处理
.js/.css
时,需要 加载 AST 解析器(如 Babel、Terser),这些都是 JS 代码,运行开销大。 esbuild
直接 在 Go 里操作二进制数据,避免了额外的 AST 解析开销。
乾坤 微前端
是什么
- 微前端是一种将大型前端应用拆分成多个小型、自治的前端应用,并将它们组合在一起的架构模式。它借鉴了后端微服务的思想,旨在解决传统单体前端应用在开发、维护和团队协作方面的问题。
核心功能
- 主应用和子应用可以使用不同的前端技术栈(如 React、Vue、Angular 等)进行开发,相互之间不会产生技术栈的耦合。
- 支持以 HTML 作为入口加载子应用,简化了子应用的接入方式,无需关心子应用的打包配置。
- 通过 CSS 沙箱机制,确保子应用的样式不会影响主应用和其他子应用,避免样式冲突。
- Shadow DOM:将子应用的 DOM 节点包裹在 Shadow DOM 中,Shadow DOM 有自己独立的样式作用域,子应用的样式只会作用于 Shadow DOM 内部,不会影响外部的主应用和其他子应用。
- Scoped CSS:在子应用加载时,乾坤会为子应用的样式添加唯一的标识,使得子应用的样式只对自身的 DOM 节点生效,避免样式冲突。
- 提供两种 JS 沙箱模式(快照沙箱和代理沙箱),隔离子应用的全局变量和事件,防止子应用对主应用的全局环境造成污染。
- 快照沙箱(SnapshotSandbox):适用于不支持
Proxy
的浏览器。在子应用挂载时,记录当前全局变量的快照;在子应用卸载时,通过对比快照恢复全局变量的状态,从而实现全局变量的隔离。 - 代理沙箱(ProxySandbox):利用
Proxy
对象拦截子应用对全局变量的读写操作。在子应用内部,对全局变量的读写操作都通过代理对象进行,不会影响主应用的全局环境。
- 快照沙箱(SnapshotSandbox):适用于不支持
- 支持对子应用进行预加载,提高子应用的加载速度,提升用户体验。
- 乾坤的预加载是在主应用空闲时,提前加载子应用的资源(如 HTML、CSS、JS 等)到浏览器缓存中。当用户访问子应用时,由于资源已经在缓存中,无需再次从服务器请求,从而提高子应用的加载速度。
- 提升用户体验,减少用户等待子应用加载的时间,特别是对于一些资源较大的子应用,预加载能显著改善应用的响应性能。可以通过在主应用中配置
preload
选项来开启预加载功能。
主应用和子应用之间如何进行通信
- props 传递:主应用在注册子应用时,可以通过
props
属性向子应用传递数据。子应用可以在生命周期钩子函数中接收这些数据。 - 事件总线:可以使用自定义的事件总线对象,主应用和子应用都可以向事件总线发布和订阅事件,从而实现数据的传递和通信。
window.postMessage
:利用浏览器的window.postMessage
API 进行跨窗口通信,主应用和子应用可以通过该 API 发送和接收消息。
数据缓存
为什么数据会丢
- 当子应用从主应用中卸载时,子应用内部的状态数据可能会丢失。
- 主应用与子应用之间的数据传递过程中,如果通信机制出现问题,也可能导致数据丢失。
数据持久化
- 使用浏览器的
localStorage
或sessionStorage
来存储数据。在子应用卸载前将关键数据存储到本地,在子应用重新挂载时再从本地读取数据。 - 对于大量结构化数据的存储,
IndexedDB
是一个更好的选择。它是一种基于数据库的存储方式,支持事务操作和索引查询。
- 使用浏览器的
实操
- 主应用可以作为数据的集中管理中心,在子应用卸载时将子应用的数据缓存到主应用中,在子应用重新挂载时再将数据传递给子应用。
- 子应用在挂载时检查主应用是否有缓存的数据,如果有则恢复数据。
资源版本如何控制
文件名加哈希值:在构建过程中,为资源文件名添加哈希值,当资源内容发生变化时,哈希值也会改变,从而使浏览器认为是新的资源,避免使用旧的缓存。例如,使用 Webpack 的
[contenthash]
占位符。在引用资源时,通过查询参数添加版本号,每次更新资源时更新版本号。
如何自动刷新缓存
可以在主应用中实现自动检测资源更新的机制,当检测到资源更新时,自动刷新页面。
- js
// 检测资源更新 function checkForUpdates() { // 发送请求获取最新版本信息 fetch('version.json') .then((response) => response.json()) .then((data) => { const currentVersion = localStorage.getItem('appVersion') if (data.version !== currentVersion) { localStorage.setItem('appVersion', data.version) location.reload() } }) } // 定时检测 setInterval(checkForUpdates, 60 * 1000) // 每分钟检测一次
子应用状态管理
- 在微前端架构(如 乾坤 qiankun)中,子应用通常需要 独立管理自身的状态,同时可能需要 与主应用或其他子应用共享状态。这时候就涉及 状态管理工具 的选择,比如 Redux、MobX、Vuex 和 Pinia。
- 子应用是否需要共享全局状态?
- Qiankun 提供了
initGlobalState
API,可以在主应用和子应用之间同步状态
- Qiankun 提供了
- 子应用之间的状态是否需要独立?
- 子应用监听全局状态
onGlobalStateChange
,并在状态变化时做相应处理 - 子应用修改全局状态
- actions.setGlobalState({ user: { id: 123, name: "Alice" }, theme: "dark", });
- 子应用监听全局状态
- 如何避免状态污染?
- 使用
namespace
进行隔离- 不要让所有子应用修改同一个
globalState
对象,而是每个子应用只修改自己的部分:
- 不要让所有子应用修改同一个
- 子应用只监听自己关心的状态
- 避免监听整个
globalState
,只监听自己关心的部分:
- 避免监听整个
- 尽量让子应用自己的状态与主应用解耦,子应用内部状态尽量使用自己的 Vuex / Pinia / Redux,不直接依赖
globalState
。 - 子应用之间避免直接修改 Vuex / Pinia / Redux
- 主应用只提供数据,不主动改动子应用的状态,子应用在需要时主动同步。
- 使用
- 子应用和主应用是否是同一个框架(如 Vue or React)?
- Redux mobx vuex pinia
- Redux 适合大型 React 项目,因为 Redux 通过 单一状态树 可以更好地管理全局共享状态。
- 适用于 多子应用 需要共享用户信息、权限、主题等全局数据的场景。
- Mobx 适合小型项目,主要用于 响应式场景,可以减少 Redux 的繁琐样板代码。
- 通过
makeObservable()
让子应用的 store 响应主应用的全局状态变化。 - 也可以将 mobx store 放入 window 对象 供子应用访问。
- 通过
- Qiankun 不建议直接共享 Vuex,因为 Vuex 基于单例模式,多个子应用共享 Vuex 可能会导致状态污染。
- 使用 Qiankun 提供的
actions
机制进行数据同步
- 使用 Qiankun 提供的
- Pinia 推荐在 Vue3 项目使用,因为 Pinia 支持 Vue3 的 Composition API,并且相比 Vuex 更轻量级、性能更好。
- 可以直接使用 Qiankun 的
actions
来同步状态,或者 将 Pinia Store 绑定到主应用的全局变量。
- 可以直接使用 Qiankun 的
- Redux 适合大型 React 项目,因为 Redux 通过 单一状态树 可以更好地管理全局共享状态。
为什么不用iframe做微前端
- iframe 通信问题
- 通过
postMessage
或window
对象,代码复杂且容易出错。 - iframe 内的应用无法直接访问外部的全局变量或状态管理工具(如 Redux、Vuex)。
- 通过
- 资源问题
- 每个 iframe 都会创建一个独立的浏览器上下文,导致内存占用高。
- 页面加载速度慢,因为 iframe 需要重新加载 HTML、CSS 和 JavaScript。
- 体验问题
- iframe 内外应用的 URL 无法同步,用户刷新页面时可能丢失状态。
- iframe 的样式与外部页面隔离,可能导致样式不一致或布局问题。
- 弹窗、遮罩层等 UI 组件无法覆盖整个页面。
React
登录鉴权 角色 权限 jwt token
- JWT怎么定义的,由什么部分组成
- JSON Web Token(JWT)是一种用于在网络应用中安全传输信息的开放标准(RFC 7519)。它是一种紧凑且自包含的方式,以 JSON 对象的形式在各方之间安全地传输声明。JWT 通常用于在客户端和服务器之间传递身份验证信息,比如在用户登录后,服务器生成一个 JWT 并返回给客户端,客户端在后续请求中携带这个 JWT,服务器通过验证 JWT 来确认请求的合法性和用户身份。
- JWT 由三部分组成,这三部分之间用点(
.
)分隔,格式为Header.Payload.Signature
。以下是对每个部分的详细解释:
集成AI
- api key 接口调用
数据缓存、列举一下过往开发过程中常用的React hooks
useMemo
缓存计算结果原理:
useMemo
是 React 的一个 Hook,它接收一个计算函数和一个依赖项数组。只有当依赖项数组中的值发生变化时,才会重新执行计算函数并返回新的结果;否则,会返回上一次缓存的结果。昂贵的计算:对于那些计算成本较高的操作,如复杂的数学计算、数据转换等,使用
useMemo
可以避免在每次渲染时都进行重复计算。对象或数组的创建:当需要创建对象或数组,并且这些对象或数组的内容在依赖项不变的情况下不会改变时,使用
useMemo
可以避免每次渲染都创建新的对象或数组实例。- ts
import React, { useMemo } from 'react'; function ExpensiveComponent({ a, b }) { const result = useMemo(() => { // 模拟一个耗时的计算 console.log('Performing expensive calculation...'); return a + b; }, [a, b]); return <div>The result is: {result}</div>; } export default ExpensiveComponent;
useCallback
缓存函数原理:
useCallback
也是一个 Hook,它用于缓存函数。它接收一个函数和一个依赖项数组,只有当依赖项数组中的值发生变化时,才会返回一个新的函数;否则,会返回上一次缓存的函数。这在将函数作为 props 传递给子组件时非常有用,可以避免不必要的重新渲染。传递回调函数:当需要将回调函数作为 props 传递给子组件,并且子组件依赖于函数的引用相等性来进行性能优化(如使用
React.memo
)时,使用useCallback
。事件处理函数:在处理事件时,如果事件处理函数比较复杂,且依赖于组件的某些状态或 props,可以使用
useCallback
来避免每次渲染都创建新的函数实例。- ts
import React, { useCallback } from 'react'; function ParentComponent() { const handleClick = useCallback(() => { console.log('Button clicked!'); }, []); return <ChildComponent onClick={handleClick} />; } function ChildComponent({ onClick }) { return <button onClick={onClick}>Click me</button>; } export default ParentComponent;
props和state最大区别是什么
数据来源和流向
props
- 来源:
props
是从父组件传递给子组件的数据。父组件可以在使用子组件时,通过属性的形式将数据传递给子组件。 - 流向:数据是单向流动的,只能从父组件流向子组件。子组件不能直接修改
props
,如果子组件需要修改props
中的数据,需要通过回调函数通知父组件,由父组件来修改数据。
- 来源:
state
- 来源:
state
是组件内部管理的数据,由组件自己创建和维护。每个组件都可以有自己独立的state
。 - 流向:
state
的数据变化只影响当前组件及其子组件。组件可以通过setState
(类组件)或useState
的更新函数(函数组件)来修改state
,从而触发组件的重新渲染。
- 来源:
数据的作用范围
props
:主要用于在组件之间传递数据和配置信息,使得组件可以根据不同的props
呈现不同的状态或行为。例如,一个Button
组件可以通过props
接收color
、size
等属性来改变按钮的外观。state
:用于管理组件内部的状态,如表单输入的值、列表的展开或折叠状态等。这些状态是与组件自身的交互和逻辑相关的,不需要外部组件知道具体的实现细节。数据的可变性
props
:props
是只读的,子组件不能直接修改props
的值。这是为了保证数据的单向流动和可预测性,使得组件之间的数据流更加清晰。state
:state
是可变的,组件可以根据用户交互、异步操作等情况修改state
的值。修改state
会触发组件的重新渲染,更新组件的 UI。
React组件之间常用的通信方式
父组件向子组件通信
- 这是最常见的通信方式,通过
props
来实现。父组件在使用子组件时,将数据作为属性传递给子组件,子组件通过props
对象接收这些数据。
- 这是最常见的通信方式,通过
子组件向父组件通信通常是通过回调函数实现。父组件将一个回调函数作为
props
传递给子组件,子组件在需要时调用这个回调函数并传递数据。兄弟组件之间的通信可以借助共同的父组件来实现。一个兄弟组件通过回调函数将数据传递给父组件,父组件再将数据传递给另一个兄弟组件。
当组件嵌套层级较深时,使用
props
层层传递数据会变得很繁琐,这时可以使用 React 的 Context API。Context 提供了一种在组件树中共享数据的方式,而不必显式地通过props
一层一层传递。对于大型应用,状态管理库可以帮助管理全局状态,实现组件之间的通信。以 Redux 为例,它通过一个单一的 store 来存储应用的状态,组件可以通过
connect
函数(在 React - Redux 中)或 Hooks(如useSelector
和useDispatch
)来获取和修改状态。- mobx 相较于 redux 的优点 为什么选用 mobx
- Redux 需要定义 actions、reducers 和 dispatch 逻辑,而 MobX 主要依赖 observable state 和 computed values,不需要写一堆冗余的 action 和 reducer 代码。
- MobX 基于 观察者模式(Observer Pattern),组件会自动监听 state 变化,而 Redux 需要手动
connect
或使用useSelector
订阅 state 变化。 - Redux 的状态是 不可变的(Immutable),需要通过 dispatch action 改变 state,而 MobX 允许 直接修改状态。
- Redux 本身不支持异步操作,需要借助中间件(如
redux - thunk
、redux - saga
等)来处理异步 action。这增加了代码的复杂度和学习成本。
- Redux 本身不支持异步操作,需要借助中间件(如
- MobX 允许声明 computed values,当依赖数据发生变化时,computed value 也会自动更新,而 Redux 需要自己写
useMemo
或reselect
来优化。 - Redux 适合大型项目,尤其是需要严格的时间旅行调试(time-travel debugging)的项目。
- MobX 更加灵活,适用于小型项目,或者需要 高性能的复杂 UI 状态管理(如表单、动画、游戏等)。
- mobx 相较于 redux 的优点 为什么选用 mobx
事件总线是一种发布 - 订阅模式的实现,通过一个全局的事件总线对象,组件可以发布事件和订阅事件,从而实现组件之间的通信。
什么导致了useState有时的表现是同步, 有时是异步
- 与 React 的批量更新机制以及调用
useState
的上下文环境有关。 - React 的事件处理函数(如
onClick
、onChange
等)和生命周期函数中,React 会对useState
的更新进行批量处理,以提高性能。React 会将多个状态更新合并为一次更新,从而减少不必要的渲染。因此,在这些情况下,useState
的更新是异步的,即不会立即更新状态并重新渲染组件。 - 在上述示例中,点击按钮调用
handleClick
函数时,两次调用setCount
并不会立即更新count
的值,而是将更新操作放入队列中,等事件处理函数执行完毕后,再统一进行状态更新和组件渲染。所以在handleClick
函数中打印的count
仍然是旧的值。- 减少渲染次数:如果每次调用
setCount
都立即更新状态并重新渲染组件,会导致不必要的性能开销。通过批量更新,可以将多个状态更新合并为一次渲染,提高性能。
- 减少渲染次数:如果每次调用
- 在
setTimeout
、Promise
等异步回调函数中,React 无法对useState
的更新进行批量处理,因为这些回调函数是在 React 的事件循环之外执行的。所以在这些情况下,useState
的更新是同步的,即会立即更新状态并触发组件的重新渲染。 - 为了确保在批量更新时状态更新的准确性,可以使用函数式更新。函数式更新接收一个函数作为参数,该函数的参数是前一个状态值,返回值是新的状态值。
函数式组件和类组件的区别
在类组件中如何判断是否需要重新渲染
在类组件中,React 默认会在 state 或 props 发生变化时重新渲染。如果想要优化性能,避免不必要的渲染,可以使用以下几种方法:
shouldComponentUpdate
shouldComponentUpdate
允许你手动控制组件是否应该重新渲染。
如果类组件的 props 和 state 是浅比较(shallow compare)可优化的,可以直接使用
React.PureComponent
,它内部会自动实现shouldComponentUpdate
,只在数据变化时触发重新渲染。componentDidUpdate
结合setState
处理状态变化有时候我们需要 根据 state 或 props 变化来决定是否更新,可以在
componentDidUpdate
里进行手动判断
二开 dumi
- 样式设计 全局样式
Vue3
Vue源码调度机制
Diff算法
Vue2
双端 Diff + 递归对比
双端对比(SameVnode 规则)
- 先从 头部 和 尾部 同时对比新旧节点,尽可能减少移动操作。
- 如果头部或尾部匹配,就直接复用或更新,否则进入完整 Diff 逻辑。
逐层递归对比
- 如果是 同类型节点(tag 相同),继续深度遍历子节点。
- 如果是 不同类型节点,则直接删除旧节点,创建新节点。
key 的作用
- Vue 2 使用
key
来优化 列表更新,通过key
生成 映射表(map),加速查找并减少节点移动。
- Vue 2 使用
问题
- 递归对比导致性能损耗,层级过深时 栈溢出风险。
- 对非 keyed 列表 Diff 性能差,可能引发 大量 DOM 操作。
Vue3
- 主要改进包括
采用 “静态标记 + Block Fragment”
- Vue 3 在编译阶段 会给 VNode 打上 静态标记(PatchFlag),避免不必要的对比:
- 静态节点直接跳过 Diff,不再递归比对整个子树。
- 只对动态节点进行更新,减少
patch
调用次数。 - diff 减少了 70%~90% 的计算量,提升渲染性能。
- Vue 3 在编译阶段 会给 VNode 打上 静态标记(PatchFlag),避免不必要的对比:
最长递增子序列(LIS)优化列表对比
先找到 无需移动的最长递增子序列(LIS)。
仅移动 不在 LIS 里的节点,而不是 Vue 2 的逐个比对。
Vue 3 的 Diff 是 “扁平化 + 深度优先”
- Vue 3 取消 Vue 2 递归 Diff,采用 非递归的深度优先遍历,避免栈溢出问题。
- 这样大幅减少 VNode 递归创建的开销,提升渲染效率。
- 主要改进包括
追问 最长递增子序列算法
在 Vue3 的 Diff 算法 中,最长递增子序列(LIS)用于优化列表节点移动:
- Vue 2 直接 逐个对比并移动,可能 多次 DOM 变更。
- Vue 3 找出不需要移动的子序列,只对 需要变更的部分进行操作,减少 DOM 操作次数,提高性能。
指在一个序列中,找到 最长的子序列,且子序列中的元素是 递增 的,并且 相对顺序不能改变。
输入: [10, 9, 2, 5, 3, 7, 101, 18]
LIS: [2, 3, 7, 18] (最长递增子序列长度 = 4)
复杂度
二分查找 + 贪心(O(n log n))
优化思路:
- 贪心策略:维护一个 最小的递增子序列
- 尽量让 序列结尾的数尽可能小,这样才能接更多的元素。
- 使用二分查找
- 维护一个 数组 tails[],其中
tails[i]
代表 长度为 i+1 的递增子序列的最小结尾。 - 遍历
nums
,对tails
进行 二分查找:- 若
nums[i]
比 tails 最右边的元素大,直接 push 进tails
; - 若
nums[i]
可以替换 tails 内某个元素(通过二分查找找到位置),用nums[i]
替换 第一个大于等于它的元素。
- 若
- 维护一个 数组 tails[],其中
- 最终
tails.length
就是 LIS 的长度。
响应式原理 追问 为什么使用位运算
在 Vue3 响应式系统中,Vue 通过位运算 进行依赖追踪优化,主要用于 标记依赖、提高触发效率。
1️⃣ 使用位运算标记 "副作用函数类型"
Vue3 需要对 不同的依赖类型 进行分类,例如:
GETTER
依赖(渲染相关)WATCH
依赖(副作用)COMPUTED
依赖(缓存计算)
nextTick执行机制
vue-router 原理 hash 和 history
vue从data改变到页面渲染的过程。
Vue 2.x 流程
- 数据劫持:Vue 2 使用
Object.defineProperty()
方法对data
对象中的所有属性进行劫持,为每个属性创建 getter 和 setter。当这些属性的值被读取时触发 getter,被修改时触发 setter。 - 依赖收集:在组件渲染过程中,Vue 会触发
data
中属性的 getter。此时,Vue 会记录下哪些组件依赖了这些属性,这个过程就是依赖收集。每个属性都有一个对应的Dep
(依赖)对象,用于存储依赖它的组件的Watcher
对象。 - 数据变更触发 setter:当
data
中的某个属性值发生改变时,会触发其 setter。在 setter 中,会通知该属性对应的Dep
对象。 - 通知
Watcher
:Dep
对象会遍历存储的所有Watcher
对象,并调用它们的update
方法。Watcher
是用来监听数据变化并更新视图的对象,每个组件都有一个对应的Watcher
。 - 虚拟 DOM 更新:
Watcher
的update
方法会触发组件的重新渲染。Vue 首先会根据新的数据生成新的虚拟 DOM 树。 - 虚拟 DOM 对比:Vue 使用
diff
算法对比新旧虚拟 DOM 树,找出差异部分。diff
算法通过比较新旧节点的属性、子节点等,确定需要更新的最小范围。 - 真实 DOM 更新:根据
diff
算法的结果,只对真实 DOM 中发生变化的部分进行更新,从而提高渲染效率。
- 数据劫持:Vue 2 使用
Vue 3.x 流程
- 响应式系统:Vue 3 使用
Proxy
对象来实现响应式数据。Proxy
可以劫持整个对象,相比Object.defineProperty()
更加灵活,能够监听对象属性的新增和删除。 - 依赖收集与触发更新:和 Vue 2 类似,在组件渲染时进行依赖收集,当数据变化时触发更新。不过,Vue 3 的依赖收集和更新机制在性能和实现上更加优化。
- 虚拟 DOM 与渲染:Vue 3 的虚拟 DOM 实现也有所改进,它采用了静态提升、PatchFlag 等技术,进一步提高了虚拟 DOM 的对比和更新效率。静态提升会将模板中的静态部分提取出来,避免每次渲染时都重新创建;PatchFlag 会标记动态节点,让
diff
算法只关注这些动态节点,减少不必要的比较。
- 响应式系统:Vue 3 使用
怎么看待组件层级嵌套很多层
优点
- 模块化和可维护性:多层级嵌套可以将复杂的界面拆分成多个小的组件,每个组件负责单一的功能或界面部分。这样可以提高代码的模块化程度,使得每个组件的逻辑更加清晰,便于开发和维护。
- 复用性:可以将通用的组件提取出来,在不同的层级中复用,减少代码重复。
- 职责分离:不同层级的组件可以承担不同的职责,例如,父组件负责数据的获取和管理,子组件负责具体的展示和交互,有利于团队协作开发。
缺点
- 性能问题:组件层级过深会增加虚拟 DOM 的对比和更新的复杂度,导致渲染性能下降。每次数据更新时,可能需要遍历更多的组件来找到需要更新的部分。
- 调试困难:多层级嵌套会使组件之间的通信变得复杂,当出现问题时,很难定位是哪个组件出现了错误。
- 代码可读性降低:过多的层级嵌套会使代码结构变得复杂,增加理解代码的难度。
怎么看待virtual dom
- 实现原理
- Vue 2:Vue 2 的虚拟 DOM 是基于对象的实现,使用 JavaScript 对象来表示真实 DOM 节点。通过递归遍历这些对象,进行
diff
算法比较和更新。 - Vue 3:Vue 3 的虚拟 DOM 实现进行了优化,采用了更高效的数据结构和算法。它引入了静态提升、PatchFlag 等技术,提高了虚拟 DOM 的对比和更新效率。
- Vue 2:Vue 2 的虚拟 DOM 是基于对象的实现,使用 JavaScript 对象来表示真实 DOM 节点。通过递归遍历这些对象,进行
- 性能表现
- Vue 2:在大型项目或复杂组件树中,由于
Object.defineProperty()
的限制和虚拟 DOM 对比的复杂度,性能可能会受到一定影响。 - Vue 3:通过
Proxy
实现响应式数据,以及虚拟 DOM 的优化,Vue 3 在性能上有显著提升,尤其是在初始渲染和更新时的速度更快。
- Vue 2:在大型项目或复杂组件树中,由于
前端监控系统,通过分析DOM元素和拦截XHR请求 sqlite
监听dom 的方式
MutationObserver 创建并返回一个新的
MutationObserver
它会在指定的 DOM 发生变化时被调用。- ts
const observer = new MutationObserver((mutationsList) => { mutationsList.forEach((mutation) => { console.log('DOM 变化:', mutation) // 可以上报给服务器 }) }) // 监听整个 body observer.observe(document.body, { childList: true, // 监听子节点的变化(新增/删除) attributes: true, // 监听属性变化 subtree: true, // 监听整个子树 })
如何在前端拦截 XMLHttpRequest
或 fetch
请求,收集 API 调用数据并上报?
重写
XMLHttpRequest.prototype.open/send
和window.fetch
来拦截请求- ts
// 1. 拦截 XHR const originalOpen = XMLHttpRequest.prototype.open const originalSend = XMLHttpRequest.prototype.send XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._url = url // 记录 URL this._method = method return originalOpen.apply(this, [method, url, ...rest]) } XMLHttpRequest.prototype.send = function (body) { console.log('拦截 XHR 请求:', this._method, this._url, body) return originalSend.apply(this, arguments) } // 2. 拦截 fetch const originalFetch = window.fetch window.fetch = function (...args) { console.log('拦截 fetch 请求:', args) return originalFetch.apply(this, args) }
如何定期清理 SQLite 日志数据,避免数据库过大
使用定时任务
- js
setInterval( () => { db.run( `DELETE FROM logs WHERE timestamp < datetime('now', '-7 days')`, (err) => { if (err) console.error('清理旧日志失败:', err) else console.log('清理了 7 天前的日志') } ) }, 1000 * 60 * 60 * 24 ) // 每天清理一次
Nuxt
预渲染机制 常用api
- 预渲染是指在构建时生成静态 HTML 文件,而不是在用户请求时动态生成页面。Nuxt 的预渲染机制分为两种模式:
- 静态站点生成(SSG):
- 在构建时生成所有页面的静态 HTML 文件。
- 适合内容不频繁变化的网站(如博客、文档站点)。
- 服务端渲染(SSR):
- 在用户请求时动态生成 HTML 文件。
- 适合内容频繁变化的网站(如电商、社交网络)。
- 静态站点生成(SSG):
- 预渲染的工作流程
- 构建阶段:
- Nuxt 根据路由配置生成所有页面的静态 HTML 文件。
- 每个页面的 HTML 文件包含初始数据(通过
asyncData
或fetch
获取)。
- 部署阶段:
- 将生成的静态文件部署到 CDN 或服务器。
- 请求阶段:
- 用户访问页面时,直接返回预渲染的 HTML 文件。
- 客户端激活后,页面变为交互式 SPA。
- 构建阶段:
useFetch。useAsyncData。fetch
图片 资源预加载
Pm2 部署
- ecosystem.config
- 日志文件配置
- 轮询日志 删除
移动端
移动端适配
- 做了哪些功能 rem vw vh。media query
- 如何在当前Web页面判断用户使用的设备
navigator.userAgent
是一个包含了浏览器和操作系统信息的字符串。不同的设备和浏览器在这个字符串中会有特定的标识,通过对这个字符串进行解析和匹配,就可以判断用户使用的设备类型。userAgent
可以被用户或开发者修改,因此通过它进行判断并不绝对可靠。- 随着新设备和浏览器的不断出现,需要不断更新匹配规则以保证判断的准确性。
window.screen
对象包含了屏幕的相关信息,如屏幕的宽度和高度。通过获取屏幕的尺寸,并结合常见设备的屏幕尺寸范围,可以大致判断用户使用的设备类型。- 媒体查询可以根据设备的特性(如屏幕宽度、高度、方向等)应用不同的 CSS 样式。通过 JavaScript 动态获取媒体查询的匹配结果,可以判断当前设备的类型。
- 混合开发中H5和原生APP的通信方式有哪些, 如何在H5页面中调用相机
- 通信方式
- H5 通过 JSBridge 发送请求,原生 APP 监听并执行对应的功能。
- 双向通信,H5 可以调用原生,也可以接收原生回调。
- 需要 WebView 支持
addJavascriptInterface
(Android)和WKScriptMessageHandler
(iOS)
- H5 页面跳转到特定格式的
URL
,原生 APP 监听这个 URL 并解析参数。- H5 调用原生 APP 方法,比如唤起支付、打开相机等。
- 需要提前在 APP 端注册 URL Scheme
- WebView 监听
postMessage
- H5 通过
postMessage
发送数据,原生 APP 监听 WebView 的消息。
- H5 通过
- H5 通过 JSBridge 发送请求,原生 APP 监听并执行对应的功能。
- 使用相机
- 直接使用
<input type="file">
- 使用
navigator.mediaDevices.getUserMedia()
- H5 + WebRTC 方案
- window.jsBridge.callCamera();
- 直接使用
- 通信方式
- 前端的哪个API可以让前端的某个元素滚动到可视区域-scrollIntoView()
scrollIntoView
是一个标准的 DOM API,在所有现代浏览器中都有很好的支持,包括旧版本的浏览器。scrollIntoViewIfNeeded
是 WebKit 内核浏览器(如 Safari)引入的非标准方法,虽然在 Chrome、Opera 等浏览器中也有支持,但在 Firefox 等浏览器中需要使用polyfill
才能正常工作。
js bridge 封装
移动端300ms延时的原因? 如何处理?
在早期的移动端浏览器中,为了实现双击缩放(double-tap to zoom)功能,引入了 300ms 的点击延时。当用户在移动端屏幕上点击一次时,浏览器并不能立刻确定这是一次单击操作还是双击操作的第一次点击。因此,浏览器会等待大约 300ms 来判断是否会有第二次点击,如果在这 300ms 内没有第二次点击,才会触发单击事件。
- 通过设置
<meta>
标签的user-scalable
属性为no
,禁止用户对页面进行缩放操作。当页面不支持缩放时,浏览器就不需要等待 300ms 来判断是否为双击缩放,从而消除点击延时。- 这种方法会完全禁止用户缩放页面,对于一些需要缩放功能的页面(如地图、图片展示等)不适用。
- FastClick 是一个专门用于解决移动端 300ms 点击延时的库。它会在检测到
touchstart
事件时,模拟一个click
事件并立即触发,从而绕过浏览器的 300ms 等待时间。 - 直接使用
touchstart
、touchend
等触摸事件来替代click
事件,因为触摸事件会在手指触碰屏幕或离开屏幕时立即触发,不会有 300ms 的延时。- 使用触摸事件需要处理一些额外的问题,如滑动时的误触发、不同设备的触摸行为差异等。
- 从 Chrome 32、Safari 9 等现代浏览器开始,在设置了合适的
viewport
元标签(如width=device-width
)且页面没有缩放功能时,会自动消除 300ms 的点击延时。<meta name="viewport" content="width=device-width, initial-scale=1.0">
- 通过设置
项目中的难点