前言
书接上文,经历刚刚20分钟手写6道代码题的磨练后,又是与面试官一个小时的口舌之战,好吧,其实是被面试官拷打的一个小时,已老实。不得不说腾讯的面试官就是犀利,问的问题不仅深而且广度还很广,也有亿亿点点难。
还没看过笔试题的大家可以先移步到这篇文章看看笔试题
腾讯前端开发校招一面笔试题前言 小编我自从360实习归来之后也是马不停蹄的开始了校招的找工作,没有停下学习的脚步,前端时 - 掘金 (juejin.cn)
正文
面试题的大概题目如下,可能有些忘记了
首先就是开始自我介绍,简单介绍完了之后就开始问我问题了。
一、vue3在框架层面在模板编译的时候做了哪些优化
面试官:请你说说vue3在框架层面模板编译的时候做了哪些事情做了哪些优化
Vue 3 在模板编译阶段进行了多项优化,旨在提升性能、减少运行时开销,并改善开发体验。
1. 静态节点提升(Static Node Hoisting)
- 优化原理:在编译阶段,Vue 3 会识别模板中的静态节点(即不会变化的节点),并将其提升到渲染函数外部,生成一次性的静态节点树。在后续渲染中,直接复用静态节点,避免重复创建和比对。
- 效果:减少虚拟 DOM 的创建和比对开销。提升渲染性能,尤其是在大型应用中。
<div>
<p>Static Content</p>
<p>{{ dynamicContent }}</p>
</div>
- 编译后,
<p>Static Content</p>
会被提升为静态节点,只在初始化时创建一次。
2. 静态属性提升(Static Props Hoisting)
- 优化原理:对于静态属性(如
class
、style
等),Vue 3 会将其提升到渲染函数外部,避免每次渲染时重新计算。 - 效果:减少不必要的属性计算和比对。提升渲染性能。
<div class="static-class" :class="dynamicClass">
Content
</div>
class="static-class"
会被提升为静态属性,只有dynamicClass
会在每次渲染时计算。
3. 补丁标志(Patch Flags)
- 优化原理:Vue 3 在编译阶段为每个动态节点添加一个“补丁标志”(Patch Flag),用于标记节点的哪些部分需要更新(如
class
、style
、props
等)。在运行时,Vue 会根据补丁标志只更新必要的部分,而不是全量比对。 - 效果:减少虚拟 DOM 比对的开销。提升更新性能。
<div :class="dynamicClass" :style="dynamicStyle">
{{ dynamicContent }}
</div>
- 编译后,Vue 会为这个节点生成补丁标志,标记
class
、style
和文本内容需要更新。
4. 块树优化(Block Tree Optimization)
- 优化原理:Vue 3 将模板划分为多个“块”(Block),每个块包含一组动态节点。在更新时,Vue 只会比对块内的动态节点,而不是整个模板。
- 效果:减少虚拟 DOM 比对的范围。提升更新性能。
<div>
<p v-if="condition">Dynamic Content 1</p>
<p v-else>Dynamic Content 2</p>
</div>
- 编译后,Vue 会将
v-if
和v-else
划分为一个块,只比对块内的节点。
5. 事件侦听器缓存(Event Listener Caching)
- 优化原理:Vue 3 会对模板中的事件侦听器进行缓存,避免每次渲染时重新创建。
- 效果:减少事件侦听器的创建和销毁开销。提升渲染性能。
<button @click="handleClick">Click Me</button>
- 编译后,
handleClick
函数会被缓存,避免重复创建。
6. 动态节点标记(Dynamic Node Marking)
- 优化原理:Vue 3 在编译阶段会标记动态节点(如
v-if
、v-for
等),并在运行时跳过静态节点的比对。 - 效果:减少虚拟 DOM 比对的开销。提升渲染性能。
7. Tree Shaking 支持
- 优化原理:Vue 3 的模板编译器支持 Tree Shaking,只打包实际使用的功能。
- 效果:减少最终打包体积。提升应用加载性能。
8. 更快的编译器
- 优化原理:Vue 3 的模板编译器经过重写,性能显著提升。
- 效果:加快编译速度。提升开发体验。
总结
Vue 3 在模板编译阶段的优化主要集中在以下几个方面:
- 静态节点和属性提升:减少重复创建和比对。
- 补丁标志:精准更新动态节点。
- 块树优化:缩小比对范围。
- 事件侦听器缓存:减少事件处理开销。
- Tree Shaking 支持:减少打包体积。
这些优化使得 Vue 3 在运行时性能、更新效率和开发体验上都有了显著提升。
二、如何区分静态变量还是动态变量,如何识别
在 Vue 3 的模板编译阶段,区分静态变量和动态变量是非常重要的,因为静态变量可以被优化(如静态节点提升),而动态变量需要在每次渲染时重新计算。以下是 Vue 3 如何区分和识别静态变量与动态变量的方法:
1. 静态变量的特征
静态变量是指在模板中不会变化的部分,通常包括:
- 纯文本内容(如
"Hello World"
)。 - 静态属性(如
class="static-class"
)。 - 没有绑定动态数据的 HTML 标签或属性。
<div class="static-class">
<p>Static Content</p>
</div>
class="static-class"
和"Static Content"
都是静态变量。
2. 动态变量的特征
动态变量是指在模板中可能变化的部分,通常包括:
- 使用插值语法绑定的数据(如
{{ dynamicContent }}
)。 - 使用
v-bind
或:
绑定的属性(如:class="dynamicClass"
)。 - 使用
v-if
、v-for
、v-model
等指令的部分。
<div :class="dynamicClass">
<p>{{ dynamicContent }}</p>
</div>
:class="dynamicClass"
和{{ dynamicContent }}
都是动态变量。
3. Vue 3 如何识别静态变量和动态变量
(1)模板解析
Vue 3 的模板编译器会解析模板,生成抽象语法树(AST)。在解析过程中,编译器会标记每个节点的类型和属性。
(2)静态分析
编译器会对 AST 进行静态分析,识别出哪些部分是静态的,哪些部分是动态的。具体方法包括:
-
纯文本内容:
如果节点是纯文本(如
"Hello World"
),则标记为静态。 -
静态属性:
如果属性值是固定的(如
class="static-class"
),则标记为静态。 -
动态绑定:
如果属性值包含插值语法(如
{{ dynamicContent }}
)或指令(如v-bind
),则标记为动态。 -
指令分析:
对于
v-if
、v-for
等指令,编译器会分析其表达式,判断是否是动态的。
(3)标记补丁标志(Patch Flags)
对于动态节点,Vue 3 会为其添加补丁标志(Patch Flags),用于标记节点的哪些部分需要更新。例如:
1
表示文本内容需要更新。2
表示class
需要更新。4
表示style
需要更新。
4. 示例分析
<div class="static-class" :class="dynamicClass">
<p>Static Content</p>
<p>{{ dynamicContent }}</p>
</div>
静态分析结果
-
静态部分:
class="static-class"
:静态属性。<p>Static Content</p>
:静态节点。 -
动态部分:
:class="dynamicClass"
:动态属性。{{ dynamicContent }}
:动态文本。
生成的渲染函数
function render() {
return (
_openBlock(),
_createBlock("div", { class: ["static-class", dynamicClass] }, [
_createVNode("p", null, "Static Content"),
_createVNode("p", null, _toDisplayString(dynamicContent), 1 /* TEXT */),
])
);
}
"Static Content"
是静态节点,直接生成。dynamicClass
和dynamicContent
是动态变量,标记为需要更新。
5. 如何手动标记静态变量
在某些情况下,开发者可以手动标记静态变量,以帮助编译器更好地优化。例如:
-
使用
v-once
指令标记静态节点:<p v-once>Static Content</p>
v-once
会告诉 Vue 这个节点是静态的,只会渲染一次。
总结
Vue 3 通过模板解析、静态分析和补丁标志,自动区分静态变量和动态变量:
- 静态变量:不会变化的部分,可以被优化(如静态节点提升)。
- 动态变量:可能变化的部分,需要在每次渲染时重新计算。
三、模板编译这一块具体是如何编译的?模板编译里AST抽象语法树了解吗
Vue 3 的模板编译过程是将模板字符串转换为渲染函数的过程,其中包括解析模板、生成抽象语法树(AST)、优化 AST 和生成渲染函数等步骤。以下是对模板编译过程的详细解析,以及 AST 的作用和生成过程。
模板编译的步骤
Vue 3 的模板编译过程可以分为以下几个步骤:
-
解析模板:
将模板字符串解析为抽象语法树(AST)。
解析器会识别模板中的 HTML 标签、属性、文本内容、指令等。
-
优化 AST:
对生成的 AST 进行静态分析,标记静态节点和静态属性。
通过静态节点提升、补丁标志等优化手段,减少运行时开销。
-
生成渲染函数:
将优化后的 AST 转换为可执行的渲染函数。
渲染函数用于生成虚拟 DOM。
1. 解析模板并生成 AST
什么是 AST?
AST(Abstract Syntax Tree,抽象语法树)是模板的结构化表示。它是一个树形结构,每个节点代表模板中的一个元素、属性或文本内容。
AST 节点的结构
一个典型的 AST 节点可能包含以下属性:
type
:节点类型(如Element
、Text
、Expression
等)。tag
:标签名(如div
、p
等)。attrs
:属性列表(如class
、style
等)。children
:子节点列表。directives
:指令列表(如v-if
、v-for
等)。static
:是否是静态节点。
示例
<div class="container">
<p>{{ message }}</p>
</div>
生成的 AST 可能如下:
{
type: 'Element',
tag: 'div',
attrs: [
{ name: 'class', value: 'container' }
],
children: [
{
type: 'Element',
tag: 'p',
attrs: [],
children: [
{
type: 'Expression',
content: 'message'
}
],
static: false
}
],
static: false
}
2. 优化 AST
在生成 AST 后,Vue 3 会对 AST 进行优化,主要包括以下内容:
(1)静态节点提升
- 识别静态节点(如纯文本、静态属性等),并将其提升到渲染函数外部。
- 静态节点只需要在初始化时创建一次,后续渲染时直接复用。
(2)补丁标志(Patch Flags)
- 为动态节点添加补丁标志,标记节点的哪些部分需要更新(如
class
、style
、props
等)。 - 在运行时,Vue 会根据补丁标志只更新必要的部分,而不是全量比对。
(3)块树优化
- 将模板划分为多个“块”(Block),每个块包含一组动态节点。
- 在更新时,Vue 只会比对块内的动态节点,而不是整个模板。
3. 生成渲染函数
优化后的 AST 会被转换为渲染函数。渲染函数是一个 JavaScript 函数,用于生成虚拟 DOM。
<div class="container">
<p>{{ message }}</p>
</div>
生成的渲染函数可能如下:
function render() {
return (
_openBlock(),
_createBlock("div", { class: "container" }, [
_createVNode("p", null, _toDisplayString(message), 1 /* TEXT */),
])
);
}
_openBlock
和_createBlock
是 Vue 3 的内部函数,用于创建块。_createVNode
用于创建虚拟 DOM 节点。_toDisplayString
用于将动态数据转换为字符串。
4. 模板编译的最终结果
模板编译的最终结果是一个渲染函数,它可以在运行时被调用,生成虚拟 DOM。虚拟 DOM 会被进一步用于更新真实 DOM。
总结
Vue 3 的模板编译过程包括以下步骤:
- 解析模板:将模板字符串解析为 AST。
- 优化 AST:通过静态节点提升、补丁标志等优化手段,减少运行时开销。
- 生成渲染函数:将优化后的 AST 转换为可执行的渲染函数。
AST 是模板编译的核心数据结构,它以一种树形结构表示模板的内容和结构。通过 AST,Vue 3 能够高效地分析模板、优化性能,并生成渲染函数。
四、unocss了解过吗
面试官:我看你简历上有写css相关的一个Tailwindcss,那你有了解vue的一个相关的作者也发起过一个unocss有了解过吗?
麻了,没想到面试官不问我Tailwindcss,竟然我问unocss?!还好我之前看过一些来了解过一点。
UnoCSS 是一个高性能的原子化 CSS 引擎,旨在通过按需生成 CSS 来减少样式文件的体积,并提升开发体验。它的设计理念类似于 Tailwind CSS,但在性能和灵活性上做了进一步优化。
1. UnoCSS 的核心特性
(1) 按需生成 CSS
- UnoCSS 会根据实际使用的样式类名,动态生成 CSS 文件。
- 避免了传统 CSS 框架中未使用样式的冗余。
(2) 高性能
- UnoCSS 的生成速度极快,适合大型项目。
- 通过静态分析和缓存机制,减少重复计算。
(3) 原子化 CSS
- 提供大量原子化的工具类(如
m-4
、text-center
),可以直接在 HTML 或 JSX 中使用。 - 通过组合工具类,快速构建 UI。
(4) 高度可定制
- 支持自定义规则、主题和预设。
- 可以与其他 CSS 框架(如 Tailwind CSS、Windi CSS)无缝集成。
(5) 框架无关
- 支持多种前端框架,包括 Vue、React、Svelte 等。
2. UnoCSS 的工作原理
(1) 静态分析
- UnoCSS 会扫描项目中的源代码,提取使用的样式类名。
- 通过静态分析,确定需要生成的 CSS 规则。
(2) 按需生成
- 根据提取的类名,动态生成对应的 CSS 规则。
- 生成的 CSS 文件只包含实际使用的样式。
(3) 缓存机制
- UnoCSS 会缓存生成的 CSS 规则,避免重复计算。
- 在开发模式下,缓存可以显著提升构建速度。
3. UnoCSS 的使用
(1) 安装
npm install unocss
(2) 配置
在项目根目录下创建 uno.config.js
文件:
import { defineConfig } from 'unocss';
export default defineConfig({
// 自定义规则
rules: [
['m-4', { margin: '1rem' }],
['text-center', { 'text-align': 'center' }],
],
// 预设
presets: [
// 内置预设
require('@unocss/preset-uno')(),
// 其他预设(如 Tailwind CSS)
require('@unocss/preset-wind')(),
],
});
(3) 集成到构建工具
-
Vite:在
vite.config.js
中集成 UnoCSS:import UnoCSS from 'unocss/vite'; export default { plugins: [UnoCSS()], };
-
Webpack:使用
unocss-webpack-plugin
:const UnoCSS = require('unocss/webpack').default; module.exports = { plugins: [UnoCSS()], };
(4) 在代码中使用在 HTML 或 JSX 中直接使用工具类:
<div class="m-4 text-center">Hello, UnoCSS!</div>
4. UnoCSS 的优势
(1) 极小的 CSS 体积
- 按需生成 CSS,避免了未使用样式的冗余。
- 生成的 CSS 文件通常只有几 KB。
(2) 极快的构建速度
- 静态分析和缓存机制使得构建速度极快。
- 在开发模式下,几乎感觉不到构建延迟。
(3) 灵活的定制能力
- 支持自定义规则、主题和预设。
- 可以与其他 CSS 框架无缝集成。
(4) 框架无关
- 支持多种前端框架,适用性广泛。
5. UnoCSS 与 Tailwind CSS 的对比
特性 | UnoCSS | Tailwind CSS |
---|---|---|
按需生成 | 是 | 是(需要配置 PurgeCSS) |
性能 | 极快 | 较快 |
定制能力 | 高度可定制 | 可定制 |
预设 | 支持多种预设(如 Uno、Wind) | 内置预设 |
框架支持 | 支持多种框架(Vue、React、Svelte 等) | 主要支持 React、Vue |
社区生态 | 较新,生态正在发展 | 成熟,生态丰富 |
6. 总结
- UnoCSS 是一个高性能的原子化 CSS 引擎,通过按需生成 CSS 减少样式文件体积。
- 它支持高度定制和多种前端框架,适合对性能和灵活性要求较高的项目。
- 如果你喜欢 Tailwind CSS 的开发体验,但希望获得更好的性能和更小的 CSS 体积,UnoCSS 是一个很好的选择。
五、介绍一下promise.all()、promise.allSetted()、promise.race()
这三者的区别我之前和大家说过啦,大家可以看看我的这篇文章
promise的方法总结Promise 是 JavaScript 中用于处理异步操作的对象。它表示一个异步操作的最终完成 - 掘金 (juejin.cn)
六、nginx如何进行流量按比例转发
听到这题我麻了,这我是真不太清楚啊,面试完1后在网上了解了一下:
在 Nginx 中,可以通过 加权轮询(Weighted Round Robin) 或 split_clients 模块来实现 流量按比例转发。以下是两种常见的实现方式:
1. 加权轮询(Weighted Round Robin)
加权轮询是 Nginx 默认支持的负载均衡策略之一。通过为不同的后端服务器分配不同的权重,可以实现流量按比例转发。
(1) 配置示例
http {
upstream backend {
server backend1.example.com weight=3; # 权重为 3
server backend2.example.com weight=2; # 权重为 2
server backend3.example.com weight=1; # 权重为 1
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
}
(2) 说明
weight
参数表示权重,权重越高,分配的流量越多。- 上述配置中,
backend1
会接收 50% 的流量(3/(3+2+1)),backend2
会接收 33.3% 的流量,backend3
会接收 16.7% 的流量。
(3) 适用场景
- 适用于简单的流量按比例转发需求。
- 权重是静态的,无法根据请求内容动态调整。
2. 使用 split_clients
模块
split_clients
是 Nginx 的一个模块,可以根据变量的值将流量按比例分配到不同的后端服务器。
(1) 配置示例
http {
# 定义流量分配比例
split_clients "${remote_addr}${http_user_agent}" $backend {
50% backend1; # 50% 的流量转发到 backend1
30% backend2; # 30% 的流量转发到 backend2
* backend3; # 剩余的 20% 流量转发到 backend3
}
upstream backend1 {
server backend1.example.com;
}
upstream backend2 {
server backend2.example.com;
}
upstream backend3 {
server backend3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://$backend;
}
}
}
(2) 说明
split_clients
根据${remote_addr}${http_user_agent}
的值生成一个哈希值,并根据比例分配流量。- 上述配置中:
- 50% 的流量会转发到
backend1
。 - 30% 的流量会转发到
backend2
。 - 剩余的 20% 流量会转发到
backend3
。
- 50% 的流量会转发到
(3) 适用场景
- 适用于需要根据请求内容动态分配流量的场景。
- 可以根据客户端 IP、User-Agent 等变量进行流量分配。
3. 使用 map
模块
map
模块也可以实现流量按比例转发,类似于 split_clients
,但更加灵活。
(1) 配置示例
http {
# 定义流量分配比例
map $remote_addr $backend {
default backend3; # 默认转发到 backend3
~^1\.1\.1\.1 backend1; # 特定 IP 转发到 backend1
~^2\.2\.2\.2 backend2; # 特定 IP 转发到 backend2
}
upstream backend1 {
server backend1.example.com;
}
upstream backend2 {
server backend2.example.com;
}
upstream backend3 {
server backend3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://$backend;
}
}
}
(2) 说明
-
map
模块根据${remote_addr}
的值匹配规则,并将流量转发到对应的后端服务器。 -
上述配置中:
IP 为
1.1.1.1
的请求会转发到backend1
。IP 为
2.2.2.2
的请求会转发到backend2
。其他请求会转发到
backend3
。
(3) 适用场景
- 适用于需要根据特定条件(如 IP、Header 等)动态分配流量的场景。
4. 总结
方法 | 特点 | 适用场景 |
---|---|---|
加权轮询 | 简单易用,静态权重分配 | 简单的流量按比例转发需求 |
split_clients | 根据变量值动态分配流量 | 需要动态分配流量的场景 |
map | 根据条件(如 IP、Header)动态分配流量 | 需要复杂条件匹配的场景 |
根据具体需求选择合适的流量分配方式,可以灵活地实现流量按比例转发。
如果有大佬觉得我有讲的不对的地方大家可以帮我指正指正,可以在评论区跟我讲讲,虚心请教。
七、Webpack中plugin 插件和loader原理和区别
这题我还是比较了解的
Webpack 是一个模块打包工具,它的核心功能是通过 Loader 和 Plugin 对模块进行处理和扩展。虽然 Loader 和 Plugin 都是 Webpack 的扩展机制,但它们的作用和实现原理有显著区别。
Loader
(1) 作用
- Loader 用于对模块的源代码进行转换。
- 它可以将非 JavaScript 文件(如 CSS、图片、字体等)转换为 Webpack 能够处理的模块。
(2) 工作原理
- Loader 是一个函数,接收源文件内容作为输入,返回转换后的内容。
- Webpack 在解析模块时,会根据配置的 Loader 对文件进行处理。
(3) 示例
-
处理 CSS 文件:
module.exports = { module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'], // 使用 style-loader 和 css-loader }, ], }, };
css-loader
将 CSS 文件转换为 JavaScript 模块。style-loader
将 CSS 插入到 DOM 中。 -
自定义 Loader:
// my-loader.js module.exports = function(source) { return source.replace('foo', 'bar'); // 将 "foo" 替换为 "bar" };
(4) 特点
- 链式调用:多个 Loader 可以串联使用,按从右到左的顺序执行。
- 单一职责:每个 Loader 只完成一种转换任务。
- 同步/异步:Loader 可以是同步的,也可以是异步的。
Plugin
(1) 作用
- Plugin 用于扩展 Webpack 的功能。
- 它可以在 Webpack 的构建生命周期中注入钩子,实现更复杂的任务(如打包优化、资源管理、环境变量注入等)。
(2) 工作原理
- Plugin 是一个类,包含一个
apply
方法。 - Webpack 在启动时会调用
apply
方法,并传入compiler
对象,Plugin 可以通过compiler
对象监听事件或修改构建过程。
(3) 示例
-
使用 HtmlWebpackPlugin:
const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', // 使用模板生成 HTML 文件 }), ], };
-
自定义 Plugin:
class MyPlugin { apply(compiler) { compiler.hooks.done.tap('MyPlugin', (stats) => { console.log('构建完成!'); }); } } module.exports = { plugins: [new MyPlugin()], };
(4) 特点
- 生命周期钩子:Plugin 可以通过 Webpack 的生命周期钩子执行任务。
- 全局操作:Plugin 可以访问 Webpack 的整个构建过程,修改输出结果。
- 功能强大:Plugin 可以实现复杂的任务,如代码压缩、资源优化等。
3. Loader 和 Plugin 的区别
特性 | Loader | Plugin |
---|---|---|
作用 | 转换模块的源代码 | 扩展 Webpack 的功能 |
运行时机 | 在模块加载时运行 | 在整个构建生命周期中运行 |
输入/输出 | 接收源文件,返回转换后的内容 | 不直接处理文件,而是操作构建过程 |
配置方式 | 在 module.rules 中配置 | 在 plugins 中配置 |
实现方式 | 导出一个函数 | 导出一个类,包含 apply 方法 |
典型应用 | 处理 CSS、图片、字体等资源 | 代码压缩、资源管理、环境变量注入等 |
4. 总结
- Loader:用于转换模块的源代码。适合处理文件级别的任务(如 CSS、图片、字体等)。通过
module.rules
配置。 - Plugin:用于扩展 Webpack 的功能。适合处理构建过程级别的任务(如代码压缩、资源优化等)。通过
plugins
配置。
八、pnpm跟npm有什么区别?包管理工具corepack了解吗?
pnpm和npm我也是了解的,但是corepack这个包管理工具确实不太熟悉
pnpm 和 npm 都是 Node.js 的包管理工具
pnpm 和 npm 的区别
(1) 磁盘空间利用率
-
pnpm:
使用 硬链接(hard link) 和 符号链接(symlink) 的方式存储依赖包。
所有项目的依赖包都会链接到一个全局的存储目录(
~/.pnpm-store
),避免了重复下载和存储。显著节省磁盘空间,尤其是在多个项目使用相同依赖时。
-
npm:
每个项目都会在
node_modules
中完整地下载和存储依赖包。如果多个项目使用相同的依赖,每个项目都会有一份独立的副本,导致磁盘空间浪费。
(2) 安装速度
-
pnpm:
由于依赖包是从全局存储中链接到项目,安装速度通常比 npm 更快。
尤其是在依赖包已经存在于全局存储时,安装几乎是瞬间完成的。
-
npm:
每次安装依赖时都需要下载并解压包,速度相对较慢。
(3) node_modules
结构
-
pnpm:
使用 扁平化 + 符号链接 的结构。
每个依赖包只会存在于一个地方(全局存储),项目中的
node_modules
只包含符号链接。避免了依赖重复和幽灵依赖(phantom dependencies)问题。
-
npm:
使用 扁平化结构,所有依赖包会被提升到
node_modules
的根目录。可能导致依赖冲突和幽灵依赖问题(即未在
package.json
中声明的依赖被错误地使用)。
(4) 严格性
-
pnpm:
更加严格,确保只有
package.json
中声明的依赖可以被访问。避免了幽灵依赖问题,提高了项目的可维护性和稳定性。
-
npm:
由于扁平化结构,未在
package.json
中声明的依赖可能被错误地访问,导致潜在的问题。
(5) 兼容性
-
pnpm:
完全兼容
package.json
和npm
的生态系统。支持
npm
的大部分命令(如install
、run
、publish
等)。可以无缝替换
npm
。 -
npm:
是 Node.js 的默认包管理工具,兼容性最好。
(6) Monorepo 支持
-
pnpm:
内置对 Monorepo 的支持,通过
pnpm-workspace.yaml
配置文件管理多个子项目。依赖共享和链接机制非常适合 Monorepo 场景。
-
npm:
需要借助第三方工具(如
lerna
)来实现 Monorepo 支持。
(7) 生态和社区
-
pnpm:
社区规模较小,但增长迅速。
在大型项目和 Monorepo 中越来越受欢迎。
-
npm:
是 Node.js 的官方包管理工具,社区和生态系统非常成熟。
总结
pnpm 和 npm 的对比
特性 | pnpm | npm |
---|---|---|
磁盘空间利用率 | 高(通过硬链接节省空间) | 低(每个项目独立存储依赖) |
安装速度 | 快(依赖全局存储) | 较慢(每次都需要下载) |
node_modules 结构 | 扁平化 + 符号链接 | 扁平化 |
严格性 | 严格(避免幽灵依赖) | 较宽松(可能存在幽灵依赖) |
Monorepo 支持 | 内置支持 | 需要第三方工具(如 lerna) |
生态和社区 | 较小但增长迅速 | 非常成熟 |
Corepack 是什么?
(1) 核心功能
- Corepack 是 Node.js 官方提供的一个包管理器管理工具,用于管理不同的 JavaScript 包管理器(如
npm
、yarn
、pnpm
等)。 - 它的目标是简化包管理器的安装和使用,确保开发者可以在不同项目中使用一致的包管理器版本。
(2) 主要用途
- 统一管理包管理器:Corepack 允许你轻松地在项目中切换和使用不同的包管理器,而无需手动安装或配置。
- 确保版本一致性:通过 Corepack,你可以为项目指定特定的包管理器版本,避免因版本不一致导致的问题。
(3) 使用示例
-
启用 Corepack:
corepack enable
-
激活特定包管理器:
corepack prepare pnpm@latest --activate
-
在项目中使用指定版本的包管理器: 在
package.json
中指定:{ "packageManager": "pnpm@7.0.0" }
Corepack 的作用
- Corepack 是一个包管理器管理工具,用于简化包管理器的安装和使用。
- 它可以帮助开发者在不同项目中使用一致的包管理器版本,避免版本冲突。
九、单体仓库monorepo解决了哪些问题?单体仓库有哪些工具及它们的对比优劣势
单体仓库(Monorepo) 是一种将多个项目或包存储在同一个代码仓库中的开发模式。它解决了传统多仓库(Multi-Repo)模式中的许多问题,特别是在代码共享、依赖管理、版本控制和开发效率方面。
1. 代码共享与复用
(1) 问题
- 在多仓库模式下,共享代码需要通过发布包或复制代码的方式,导致维护困难。
- 不同仓库之间的代码复用成本高,容易产生重复代码。
(2) 解决
- Monorepo 中所有项目共享同一个代码库,可以轻松引用和复用代码。
- 通过模块化设计,将公共代码提取为共享库,减少重复代码。
2. 依赖管理
(1) 问题
- 在多仓库模式下,不同项目可能使用不同版本的依赖,导致冲突和重复安装。
- 依赖版本不一致可能导致构建失败或运行时错误。
(2) 解决
- Monorepo 中所有项目使用统一的依赖管理工具(如
npm
、yarn
、pnpm
),确保依赖版本一致。 - 通过工具(如 Lerna、Nx)实现依赖的提升和共享,减少重复安装。
3. 版本控制
(1) 问题
- 在多仓库模式下,跨仓库的版本控制和发布流程复杂。
- 需要手动同步多个仓库的版本号,容易出错。
(2) 解决
- Monorepo 中可以通过工具(如 Lerna、Changesets)统一管理版本和发布流程。
- 在一个提交中完成跨项目的版本更新,确保一致性。
4. 开发效率
(1) 问题
- 在多仓库模式下,切换项目和配置开发环境耗时。
- 每个仓库需要单独配置构建工具、测试工具等,增加了开发成本。
(2) 解决
- Monorepo 中所有项目共享相同的开发环境,提升开发效率。
- 通过工具(如 Nx、Turborepo)实现增量构建和测试,减少构建时间。
5. 跨项目重构
(1) 问题
- 在多仓库模式下,跨项目重构需要同步多个仓库,容易出错。
- 重构后的代码需要分别提交和发布,增加了复杂性。
(2) 解决
- Monorepo 中可以在一个提交中完成跨项目重构,确保一致性。
- 通过工具(如 Nx、Bazel)实现跨项目的依赖分析和重构支持。
6. 代码一致性
(1) 问题
- 在多仓库模式下,不同仓库可能使用不同的代码风格、工具和配置,导致代码不一致。
- 维护统一的代码风格和配置成本高。
(2) 解决
- Monorepo 中可以通过统一的配置文件和工具(如 ESLint、Prettier)确保代码一致性。
- 所有项目共享相同的代码风格和配置,减少维护成本。
7. CI/CD 集成
(1) 问题
- 在多仓库模式下,每个仓库需要单独配置 CI/CD 流水线,增加了维护成本。
- 跨仓库的构建和测试流程复杂,容易出错。
(2) 解决
- Monorepo 中可以通过统一的 CI/CD 配置管理所有项目的构建和测试流程。
- 通过工具(如 Nx、Turborepo)实现增量构建和测试,提升 CI/CD 效率。
8. 团队协作
(1) 问题
- 在多仓库模式下,团队成员需要频繁切换仓库,增加了沟通和协作成本。
- 不同仓库的权限管理和代码审查流程复杂。
(2) 解决
- Monorepo 中所有项目共享同一个代码库,简化了团队协作流程。
- 通过统一的权限管理和代码审查工具,提升团队协作效率。
9. 总结
问题 | Monorepo 的解决方案 |
---|---|
代码共享与复用 | 所有项目共享同一个代码库,轻松引用和复用代码。 |
依赖管理 | 统一依赖管理工具,确保依赖版本一致。 |
版本控制 | 统一管理版本和发布流程,确保一致性。 |
开发效率 | 共享开发环境,提升开发效率。 |
跨项目重构 | 在一个提交中完成跨项目重构,确保一致性。 |
代码一致性 | 统一配置文件和工具,确保代码一致性。 |
CI/CD 集成 | 统一 CI/CD 配置,提升构建和测试效率。 |
团队协作 | 简化协作流程,提升团队协作效率。 |
通过采用 Monorepo,可以有效解决多仓库模式下的许多问题,提升代码共享、依赖管理、版本控制和开发效率。
1. 主流 Monorepo 工具
工具 | 语言支持 | 包管理器集成 | 依赖管理 | 构建工具集成 | 学习曲线 | 社区生态 |
---|---|---|---|---|---|---|
Nx | 多语言(JS/TS等) | 支持 npm/yarn/pnpm | 高效依赖管理 | 支持 | 中等 | 活跃 |
Lerna | JavaScript/TypeScript | 支持 npm/yarn | 依赖提升 | 需要配置 | 低 | 成熟 |
Turborepo | JavaScript/TypeScript | 支持 npm/yarn/pnpm | 增量构建 | 支持 | 低 | 新兴 |
Rush | JavaScript/TypeScript | 支持 npm/yarn/pnpm | 严格依赖管理 | 支持 | 高 | 微软支持 |
Bazel | 多语言(JS/Java等) | 无 | 高效增量构建 | 内置 | 高 | 强大 |
Pnpm Workspaces | JavaScript/TypeScript | 仅 pnpm | 高效依赖管理 | 需要配置 | 低 | 新兴 |
2. 工具详细介绍及对比
(1) Nx
- 特点:支持多语言(JavaScript、TypeScript、Angular、React、Node.js 等)。内置代码生成器和依赖图可视化工具。支持增量构建和测试,性能优秀。与主流框架(如 Angular、React)深度集成。
- 优势:功能全面,适合大型项目。增量构建和缓存机制显著提升构建速度。社区活跃,文档丰富。
- 劣势:配置复杂,学习曲线较高。对小型项目可能显得过于重量级。
(2) Lerna
- 特点:专注于 JavaScript/TypeScript 项目。支持依赖提升(hoisting),减少重复依赖。与 npm/yarn 集成良好。
- 优势:简单易用,适合中小型项目。社区成熟,生态丰富。
- 劣势:缺乏增量构建支持,性能较差。依赖管理不够严格,可能导致幽灵依赖问题。
(3) Turborepo
- 特点:专注于 JavaScript/TypeScript 项目。支持增量构建和缓存,性能优秀。与 npm/yarn/pnpm 集成良好。
- 优势:轻量级,配置简单。增量构建和缓存机制显著提升构建速度。
- 劣势:功能相对较少,适合中小型项目。社区和生态仍在发展中。
(4) Rush
- 特点:由微软开发,适合大型企业级项目。支持严格的依赖管理和版本控制。内置增量构建和链接机制。
- 优势:严格依赖管理,避免幽灵依赖。适合超大型 Monorepo。
- 劣势:配置复杂,学习曲线高。社区相对较小。
(5) Bazel
- 特点:由 Google 开发,支持多语言(JavaScript、Java、C++ 等)。高效的增量构建和缓存机制。适合超大型项目。
- 优势:构建性能极佳,适合跨语言项目。强大的扩展性和灵活性。
- 劣势:配置复杂,学习曲线陡峭。对 JavaScript/TypeScript 生态支持较弱。
(6) Pnpm Workspaces
- 特点:基于
pnpm
的 Monorepo 支持。使用硬链接和符号链接管理依赖,节省磁盘空间。配置简单,适合中小型项目。 - 优势:依赖管理高效,节省磁盘空间。与
pnpm
无缝集成。 - 劣势:功能较少,缺乏增量构建支持。仅支持
pnpm
,生态相对较小。
3. 工具选择建议
场景 | 推荐工具 | 理由 |
---|---|---|
中小型 JavaScript 项目 | Lerna / Turborepo | 简单易用,配置少,适合快速上手。 |
大型 JavaScript 项目 | Nx / Rush | 功能全面,支持增量构建和严格依赖管理,适合复杂项目。 |
跨语言项目 | Bazel | 支持多语言,构建性能优秀,适合超大型项目。 |
磁盘空间敏感项目 | Pnpm Workspaces | 依赖管理高效,节省磁盘空间。 |
企业级项目 | Rush / Nx | 严格依赖管理,适合团队协作和大型项目。 |
4. 总结
Monorepo 解决的问题
- 代码共享、依赖管理、版本控制、开发效率和跨项目重构。
工具对比
- Nx:功能全面,适合大型项目。
- Lerna:简单易用,适合中小型项目。
- Turborepo:轻量级,性能优秀,适合中小型项目。
- Rush:严格依赖管理,适合企业级项目。
- Bazel:跨语言支持,构建性能极佳。
- Pnpm Workspaces:依赖管理高效,节省磁盘空间。
根据项目规模、技术栈和团队需求选择合适的工具,可以显著提升 Monorepo 的开发效率和维护性。
十、单体仓库依赖的管理方式以及包的发布等是怎么处理的
在 单体仓库(Monorepo) 中,依赖管理和包的发布是核心问题之一。由于多个项目或包共享同一个代码库,如何高效地管理依赖、避免冲突,以及如何发布包,是 Monorepo 需要解决的关键问题。以下是 Monorepo 中依赖管理和包发布的常见方式:
依赖管理
(1) 依赖提升(Hoisting)
-
问题:在 Monorepo 中,多个包可能依赖同一个第三方库,如果每个包都独立安装依赖,会导致重复安装和版本冲突。
-
解决方案:使用工具(如 Lerna、Nx、pnpm)将共同的依赖提升到 Monorepo 的根目录。
通过
node_modules
的符号链接或硬链接机制,减少重复安装。使用 Lerna 的
hoist
功能:lerna bootstrap --hoist
使用 pnpm 的 Workspace 功能:
pnpm install
(2) 依赖共享
-
问题:Monorepo 中的多个包可能需要共享某些内部工具或库。
-
解决方案:将共享代码提取为独立的包,并在 Monorepo 中引用。
使用工具(如 Nx、Turborepo)管理包之间的依赖关系。
-
示例:在
packages/shared
中定义共享代码,其他包通过package.json
引用:{ "dependencies": { "shared": "workspace:*" } }
(3) 依赖版本一致性
-
问题:不同包可能依赖不同版本的第三方库,导致冲突。
-
解决方案:
使用工具(如 Lerna、Rush)统一管理依赖版本。
在 Monorepo 的根目录中定义统一的依赖版本。
-
示例:使用 Lerna 的
fixed
模式:{ "version": "fixed" }
包的发布
(1) 版本管理
-
问题:在 Monorepo 中,多个包可能需要独立发布,但版本号需要保持一致或按需更新。
-
解决方案:使用工具(如 Lerna、Changesets)管理包的版本号。支持独立版本(Independent Mode)或统一版本(Fixed Mode)。
-
示例:
使用 Lerna 的独立版本模式:
lerna version --conventional-commits
使用 Changesets 管理版本:
changeset add changeset version
(2) 发布流程
-
问题:在 Monorepo 中,发布多个包需要确保依赖关系和版本号的一致性。
-
解决方案:
- 使用工具(如 Lerna、Nx、Rush)自动化发布流程。
- 支持增量发布(只发布有变化的包)。
-
示例:使用 Lerna 发布包:
lerna publish
使用 Nx 发布包:
nx run-many --target=publish
(3) 私有包管理
-
问题:Monorepo 中的某些包可能是私有的,不需要发布到公共注册表。
-
解决方案:在
package.json
中设置"private": true
,避免发布私有包。使用私有注册表(如 Verdaccio)管理私有包。
-
示例:在
package.json
中标记私有包:{ "private": true }
小结
依赖管理
- 依赖提升:通过工具(如 Lerna、pnpm)减少重复安装。
- 依赖共享:将共享代码提取为独立包。
- 版本一致性:统一管理依赖版本。
包发布
- 版本管理:使用工具(如 Lerna、Changesets)管理版本号。
- 发布流程:自动化发布流程,支持增量发布。
- 私有包管理:标记私有包,使用私有注册表。
十一、vue中 keep-alive底层是如何实现的?
<keep-alive>
是 Vue.js 中用于缓存组件的一个内置组件。它的底层实现主要依赖于 Vue 的组件生命周期和虚拟 DOM 机制。以下是 <keep-alive>
的底层实现原理和关键机制:
核心功能
<keep-alive>
的主要功能是缓存不活动的组件实例,而不是销毁它们。当组件再次被激活时,直接从缓存中恢复,避免重新渲染和挂载。
实现原理
(1) 缓存机制
<keep-alive>
内部维护了一个缓存对象(cache
),用于存储被缓存的组件实例。- 缓存对象的键是组件的
name
选项或组件的tag
,值是组件的 VNode(虚拟 DOM 节点)。
(2) 生命周期钩子
-
<keep-alive>
通过 Vue 的生命周期钩子来管理组件的激活和停用:activated
:当组件被激活时触发。deactivated
:当组件被停用时触发。
(3) 组件切换
-
当组件切换时,
<keep-alive>
会检查缓存中是否存在该组件的实例:如果存在,则直接从缓存中恢复组件实例,并触发
activated
钩子。如果不存在,则创建新的组件实例,并将其缓存起来。
源码解析
以下是 <keep-alive>
的核心实现逻辑(简化版):
export default {
name: 'keep-alive',
abstract: true, // 抽象组件,不会出现在父子组件链中
props: {
include: [String, RegExp, Array], // 需要缓存的组件
exclude: [String, RegExp, Array], // 不需要缓存的组件
max: [String, Number], // 最大缓存数量
},
created() {
this.cache = Object.create(null); // 缓存对象
this.keys = []; // 缓存键的数组(用于 LRU 算法)
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted() {
this.$watch('include', (val) => {
pruneCache(this, (name) => matches(val, name));
});
this.$watch('exclude', (val) => {
pruneCache(this, (name) => !matches(val, name));
});
},
render() {
const slot = this.$slots.default;
const vnode = getFirstComponentChild(slot); // 获取第一个子组件
const componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
const name = getComponentName(componentOptions);
const { include, exclude } = this;
// 检查是否需要缓存
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode;
}
const { cache, keys } = this;
const key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key;
// 如果缓存中存在,则直接使用缓存的实例
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// 使用 LRU 算法调整缓存顺序
remove(keys, key);
keys.push(key);
} else {
// 否则缓存新的实例
cache[key] = vnode;
keys.push(key);
// 如果超出最大缓存数量,则移除最久未使用的缓存
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true; // 标记为 keep-alive 组件
}
return vnode || (slot && slot[0]);
},
};
关键机制
(1) 缓存对象
cache
是一个对象,用于存储组件的 VNode 实例。- 每个缓存的键是组件的唯一标识(通常是
name
或key
),值是组件的 VNode。
(2) LRU 算法
- 当缓存数量超过
max
时,<keep-alive>
会使用 LRU(Least Recently Used) 算法移除最久未使用的缓存。 keys
数组用于记录缓存键的使用顺序。
(3) 组件激活与停用
- 当组件被缓存时,Vue 会调用
deactivated
钩子。 - 当组件从缓存中恢复时,Vue 会调用
activated
钩子。
使用示例
<template>
<keep-alive :include="['ComponentA', 'ComponentB']" :max="10">
<component :is="currentComponent" />
</keep-alive>
</template>
<script>
export default {
data() {
return {
currentComponent: 'ComponentA',
};
},
};
</script>
小结
<keep-alive>
的底层实现主要依赖于以下机制:
- 缓存对象:存储组件的 VNode 实例。
- LRU 算法:管理缓存数量,移除最久未使用的缓存。
- 生命周期钩子:通过
activated
和deactivated
钩子管理组件的激活和停用。
十二、vueUse这个hooks库了解吗
VueUse 是一个基于 Vue 3 的 Composition API 的工具库,提供了大量实用的 Hooks(类似于 React 的 Hooks),用于简化 Vue 开发中的常见任务。它由 Anthony Fu 开发和维护,已经成为 Vue 生态中最受欢迎的实用工具库之一。
VueUse 的核心特性
(1) 丰富的 Hooks:VueUse 提供了 200+ 个 Hooks,涵盖了状态管理、DOM 操作、网络请求、动画、设备 API 等多个领域。
(2) 基于 Composition API:VueUse 完全基于 Vue 3 的 Composition API 设计,支持 Vue 3 和 Vue 2.7+。所有 Hooks 都是响应式的,可以无缝集成到 Vue 组件中。
(3) 轻量级:VueUse 采用按需引入的方式,只加载使用的 Hooks,减少打包体积。核心库非常轻量,适合各种规模的项目。
(4) 高度可定制:每个 Hook 都提供了丰富的配置选项,满足不同的使用场景。支持自定义扩展,可以根据需求编写自己的 Hooks。
(5) 跨平台支持:VueUse 不仅支持浏览器环境,还支持 Node.js、React Native 等平台。
VueUse 的常用 Hooks
(1) 状态管理
-
useStorage
:管理localStorage
或sessionStorage
。import { useStorage } from '@vueuse/core'; const count = useStorage('count', 0);
-
useCounter
:创建一个计数器。import { useCounter } from '@vueuse/core'; const { count, inc, dec } = useCounter();
(2) DOM 操作
-
useMouse
:跟踪鼠标位置。import { useMouse } from '@vueuse/core'; const { x, y } = useMouse();
-
useElementSize
:获取元素的尺寸。import { useElementSize } from '@vueuse/core'; const { width, height } = useElementSize(ref);
(3) 网络请求
-
useFetch
:简化网络请求。import { useFetch } from '@vueuse/core'; const { data, error } = useFetch('https://api.example.com/data');
(4) 设备 API
-
useGeolocation
:获取用户的地理位置。import { useGeolocation } from '@vueuse/core'; const { coords } = useGeolocation();
-
useClipboard
:操作剪贴板。import { useClipboard } from '@vueuse/core'; const { text, copy } = useClipboard();
(5) 动画
-
useTransition
:创建平滑的过渡动画。import { useTransition } from '@vueuse/core'; const output = useTransition(0, { duration: 1000, transition: [0.75, 0, 0.25, 1], });
十三、webpack和vite在本地开发的时候底层热更新的实现这一块有什么区别
Webpack 和 Vite 在本地开发时的热更新(HMR,Hot Module Replacement)实现机制有显著区别,主要体现在以下几个方面:
1. Webpack 的热更新实现
(1) 基本原理
- Webpack 的热更新是基于 文件监听 和 模块依赖图 实现的。
- 当文件发生变化时,Webpack 会重新构建受影响的模块,并通过 WebSocket 将更新推送到浏览器。
- 浏览器接收到更新后,替换旧的模块代码并重新执行相关逻辑。
(2) 具体流程
- 文件监听:Webpack 通过
webpack-dev-server
监听文件系统的变化。当文件发生变化时,触发重新构建。 - 依赖图构建:Webpack 会构建一个模块依赖图,记录模块之间的依赖关系。当某个模块发生变化时,Webpack 会找到所有依赖该模块的模块,并重新构建这些模块。
- HMR 更新推送:Webpack 通过 WebSocket 将更新的模块代码推送到浏览器。浏览器接收到更新后,替换旧的模块代码并执行
module.hot.accept
回调。 - 模块替换:浏览器根据更新信息替换模块,并重新执行相关逻辑(如重新渲染组件)。
(3) 优点:成熟稳定,生态丰富。支持复杂的模块依赖关系。
**(4) 缺点:**随着项目规模增大,构建速度变慢。热更新时可能需要重新构建整个依赖图,导致更新时间较长。
2. Vite 的热更新实现
(1) 基本原理
- Vite 的热更新是基于 ESM(ES Modules) 和 浏览器原生模块系统 实现的。
- Vite 利用浏览器的原生 ESM 支持,直接在浏览器中加载模块,无需打包。
- 当文件发生变化时,Vite 只需更新受影响的模块,并通过 WebSocket 通知浏览器重新加载。
(2) 具体流程
- 文件监听:Vite 通过
chokidar
监听文件系统的变化。当文件发生变化时,触发更新逻辑。 - 模块更新:Vite 会分析模块的依赖关系,找到受影响的模块。通过 WebSocket 通知浏览器重新加载这些模块。
- 浏览器重新加载:浏览器接收到更新通知后,重新请求受影响的模块。由于模块是通过 ESM 加载的,浏览器会直接使用新的模块代码。
- 模块替换:浏览器根据新的模块代码更新页面,无需重新加载整个应用。
(3) 优点
- 基于 ESM,更新速度极快。
- 无需重新构建整个应用,只需更新受影响的模块。
- 开发服务器启动速度快,适合大型项目。
(4) 缺点
- 依赖浏览器的 ESM 支持,兼容性较差(现代浏览器支持良好)。
- 对 CommonJS 模块的支持需要额外处理。
Webpack 和 Vite 热更新的对比
特性 | Webpack | Vite |
---|---|---|
热更新机制 | 基于文件监听和模块依赖图 | 基于 ESM 和浏览器原生模块系统 |
更新速度 | 较慢(需重新构建依赖图) | 极快(仅更新受影响的模块) |
开发服务器启动速度 | 较慢(需打包所有模块) | 极快(无需打包,直接加载模块) |
生态支持 | 成熟,插件和工具丰富 | 较新,生态正在快速发展 |
兼容性 | 支持所有模块格式(ESM、CommonJS) | 主要支持 ESM,CommonJS 需额外处理 |
适用场景 | 中小型项目,兼容性要求高 | 大型项目,现代浏览器环境 |
小结
- Webpack 的热更新基于文件监听和模块依赖图,适合需要兼容性和复杂构建逻辑的项目,但随着项目规模增大,更新速度会变慢。
- Vite 的热更新基于 ESM 和浏览器原生模块系统,更新速度极快,适合现代浏览器环境的大型项目。
十四、vite是如何处理commonjs模块格式的
Vite 是一个基于 ESM(ES Modules) 的现代前端构建工具,它的设计初衷是为了充分利用浏览器的原生 ESM 支持,从而实现快速的开发服务器启动和热更新。然而,在实际项目中,我们仍然会遇到大量的 CommonJS 模块(尤其是在使用一些旧的 npm 包时)。Vite 通过以下方式处理 CommonJS 模块:
CommonJS 模块的处理方式
(1) 开发环境
在开发环境中,Vite 会通过 ESBuild 将 CommonJS 模块转换为 ESM 格式,以便浏览器能够正确加载。
- ESBuild 是一个极快的 JavaScript 打包工具,支持将 CommonJS 模块转换为 ESM。
- Vite 在开发服务器启动时,会动态地将 CommonJS 模块转换为 ESM,并通过 HTTP 请求提供给浏览器。
假设有一个 CommonJS 模块 cjs-module.js
:
// cjs-module.js
module.exports = {
hello: 'world',
};
Vite 会将其转换为 ESM 格式:
// 转换后的 ESM 模块
export default {
hello: 'world',
};
浏览器通过 ESM 加载该模块:
import cjsModule from '/path/to/cjs-module.js';
console.log(cjsModule.hello); // 输出 'world'
(2) 生产环境
在生产环境中,Vite 使用 Rollup 进行打包。Rollup 同样支持将 CommonJS 模块转换为 ESM。
- Rollup 会分析模块的依赖关系,并将 CommonJS 模块打包为 ESM 格式。
- 通过
@rollup/plugin-commonjs
插件,Rollup 可以处理 CommonJS 模块的转换。
在 vite.config.js
中,Vite 默认会启用 @rollup/plugin-commonjs
插件:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
plugins: [
// 默认启用 @rollup/plugin-commonjs
],
},
},
});
Vite 对 CommonJS 模块的限制
尽管 Vite 可以处理 CommonJS 模块,但由于其设计理念是基于 ESM 的,因此在使用 CommonJS 模块时需要注意以下限制:
(1) 动态导入
- CommonJS 模块不支持原生的动态导入(
import()
)。 - 如果需要在 Vite 中使用动态导入,建议将模块转换为 ESM 格式。
(2) 命名导出
- CommonJS 模块通常使用
module.exports
导出默认值,而 ESM 支持命名导出(export
)。 - 在 Vite 中,CommonJS 模块的命名导出需要通过
require
语法或转换工具处理。
(3) 兼容性问题
- 某些 CommonJS 模块可能依赖于 Node.js 特有的 API(如
process
、Buffer
等),这些 API 在浏览器环境中不可用。 - 需要使用 polyfill 或替换库(如
process/browser
)来解决兼容性问题。
优化 CommonJS 模块的使用**
(1) 使用 ESM 优先的库
- 尽量选择支持 ESM 的库,避免使用纯 CommonJS 模块。
- 许多流行的库(如
lodash-es
、rxjs
)已经提供了 ESM 版本。
(2) 手动转换 CommonJS 模块
- 如果必须使用 CommonJS 模块,可以手动将其转换为 ESM 格式。
- 使用工具如 ESBuild 或 Babel 进行转换。
(3) 配置 optimizeDeps
-
Vite 提供了
optimizeDeps
配置项,可以预构建 CommonJS 模块,以提升开发服务器的性能。 -
在
vite.config.js
中配置:import { defineConfig } from 'vite'; export default defineConfig({ optimizeDeps: { include: ['commonjs-module'], // 预构建 CommonJS 模块 }, });
小结
Vite 通过以下方式处理 CommonJS 模块:
- 开发环境:使用 ESBuild 动态将 CommonJS 模块转换为 ESM。
- 生产环境:使用 Rollup 和
@rollup/plugin-commonjs
插件打包 CommonJS 模块。
尽管 Vite 可以处理 CommonJS 模块,但为了充分发挥其性能优势,建议尽量使用 ESM 格式的模块。如果必须使用 CommonJS 模块,可以通过配置 optimizeDeps
或手动转换来优化兼容性。
十五、js模块化管理打包的模式有哪些
JavaScript 的模块化管理和打包模式经历了多次演进,从最初的脚本标签到现代的模块化标准,再到各种打包工具的出现。以下是 JavaScript 模块化管理和打包的主要模式及其发展历程:
模块化模式
(1) 无模块化(Script Tag)
-
特点:通过
<script>
标签直接引入 JavaScript 文件。所有变量和函数都挂载到全局作用域(window
对象)。 -
问题:容易造成全局变量污染。依赖关系难以管理。
<script src="a.js"></script> <script src="b.js"></script>
(2) IIFE(立即执行函数表达式)
-
特点:使用立即执行函数隔离作用域,避免全局污染。通过函数参数传递依赖。
-
问题:依赖关系需要手动管理。不适合大型项目。
// a.js var moduleA = (function() { return { hello: function() { console.log('Hello from A'); }, }; })(); // b.js var moduleB = (function(moduleA) { return { hello: function() { moduleA.hello(); console.log('Hello from B'); }, }; })(moduleA);
(3) CommonJS
-
特点:主要用于 Node.js 环境。使用
require
导入模块,module.exports
导出模块。同步加载模块。 -
问题:不适合浏览器环境(同步加载会导致性能问题)。
// a.js module.exports = { hello: function() { console.log('Hello from A'); }, }; // b.js const moduleA = require('./a'); module.exports = { hello: function() { moduleA.hello(); console.log('Hello from B'); }, };
(4) AMD(Asynchronous Module Definition)
-
特点:异步加载模块,适合浏览器环境。使用
define
定义模块,require
加载模块。主要实现库:RequireJS。 -
问题:语法复杂,配置繁琐。
// a.js define(function() { return { hello: function() { console.log('Hello from A'); }, }; }); // b.js define(['./a'], function(moduleA) { return { hello: function() { moduleA.hello(); console.log('Hello from B'); }, }; });
(5) UMD(Universal Module Definition)
-
特点:兼容 CommonJS、AMD 和全局变量模式。适用于多种环境。
-
问题:代码冗余,不够优雅。
(function(root, factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.moduleA = factory(); } })(this, function() { return { hello: function() { console.log('Hello from A'); }, }; });
(6) ES Modules(ESM)
-
特点:JavaScript 官方模块化标准。使用
import
导入模块,export
导出模块。静态加载,支持 Tree Shaking。 -
问题:需要现代浏览器或打包工具支持。
// a.js export function hello() { console.log('Hello from A'); } // b.js import { hello } from './a'; export function helloB() { hello(); console.log('Hello from B'); }
十六、eslint9的新特性你知道哪些,性能具体做了哪些优化?eslint是怎么做到用配置规则去检测代码的异常?
ESLint v9 的新特性
(1) 支持 ES2023 新语法
-
ESLint v9 将支持最新的 ECMAScript 标准(如 ES2023)中的新语法特性。
-
例如:
findLast
和findLastIndex
:数组方法。Hashbang Grammar
:支持 Shebang(#!
)语法。
(2) 改进的 TypeScript 支持
-
进一步优化对 TypeScript 的支持,包括:
更好的类型推断。
支持最新的 TypeScript 版本(如 TypeScript 5.x)。
修复与 TypeScript 插件相关的已知问题。
(3) 新的规则和规则改进
- 引入新的规则以覆盖更多的代码质量和风格问题。
- 对现有规则进行改进,提供更多的配置选项和更精确的检查。
(4) 更好的插件系统
- 改进插件 API,使插件开发更加灵活和高效。
- 提供更多的钩子(hooks)和扩展点,支持更复杂的自定义逻辑。
(5) 配置文件格式改进
- 支持更灵活的配置文件格式(如 JSON、YAML、JavaScript)。
- 提供更直观的配置选项,简化配置文件的编写。
ESLint v9 的性能优化
(1) 更快的解析器
- 优化默认解析器(如
espree
)的性能,减少解析时间。 - 支持更高效的 AST(抽象语法树)生成和遍历。
(2) 增量构建
- 引入增量构建机制,只重新检查发生变化的文件,而不是整个项目。
- 显著提升大型项目的检查速度。
(3) 并行检查
- 支持多线程或并行检查,充分利用多核 CPU 的性能。
- 通过并行化处理,减少检查时间。
(4) 缓存机制
- 引入更智能的缓存机制,避免重复检查未变化的代码。
- 缓存结果可以在多次运行之间共享,进一步提升性能。
(5) 减少内存占用
- 优化内存管理,减少 ESLint 运行时的内存占用。
- 通过更高效的数据结构和算法,降低资源消耗。
其他改进
(1) 更好的错误报告
- 提供更详细的错误信息和修复建议。
- 支持更友好的错误格式(如 Markdown、HTML)。
(2) 改进的 CLI 体验
- 提供更直观的命令行界面(CLI),支持更多的命令和选项。
- 改进的交互式模式,方便用户快速配置和运行 ESLint。
(3) 更丰富的文档
- 提供更详细的官方文档和示例,帮助用户更好地理解和使用 ESLint。
- 改进的社区支持,提供更多的教程和最佳实践。
总结
特性/优化 | 描述 |
---|---|
新语法支持 | 支持 ES2023 和 TypeScript 的最新特性。 |
规则改进 | 引入新规则,改进现有规则。 |
插件系统 | 提供更灵活的插件 API 和扩展点。 |
配置文件格式 | 支持更灵活的配置文件格式,简化配置。 |
性能优化 | 更快的解析器、增量构建、并行检查、缓存机制和减少内存占用。 |
错误报告 | 提供更详细的错误信息和修复建议。 |
CLI 体验 | 改进的命令行界面和交互式模式。 |
文档和社区 | 提供更详细的文档和社区支持。 |
ESLint v9 的发布将进一步增强其作为 JavaScript/TypeScript 代码检查工具的功能和性能,帮助开发者编写更高质量的代码。
ESLint 是一个用于检测 JavaScript 代码中潜在问题和风格问题的工具。它通过配置规则(rules)来分析代码,并报告不符合规则的代码。ESLint 如何利用配置规则检测代码异常的详细过程:
1. ESLint 的工作流程
(1) 代码解析
- ESLint 使用 解析器(Parser) 将源代码转换为 AST(抽象语法树)。
- 默认的解析器是
espree
(基于acorn
),但也可以配置其他解析器(如@babel/eslint-parser
用于支持 Babel 语法)。
(2) 规则应用
- ESLint 根据配置的规则(rules)遍历 AST,检查代码是否符合规则。
- 每条规则是一个函数,接收 AST 节点作为参数,检查节点的属性是否符合规则。
(3) 报告问题
- 如果代码违反了规则,ESLint 会生成一个 问题(issue),包括问题的类型、位置和描述。
- 问题会被输出到控制台或保存到文件中。
2. 规则的工作原理
(1) 规则的定义
- 每条规则是一个 JavaScript 模块,导出一个对象,包含
meta
和create
两个属性:meta
:规则的元信息,如类型、文档、是否可修复等。create
:一个函数,返回一个对象,包含 AST 节点的监听器。
(2) 规则的实现
- 在
create
函数中,规则会监听特定的 AST 节点类型(如VariableDeclaration
、FunctionDeclaration
等)。 - 当 ESLint 遍历 AST 时,会调用对应的监听器函数,检查节点的属性是否符合规则。
(3) 示例:自定义规则
以下是一个简单的自定义规则,用于检查变量名是否为小写:
module.exports = {
meta: {
type: 'suggestion', // 规则类型
docs: {
description: 'Enforce lowercase variable names', // 规则描述
},
schema: [], // 规则配置选项
},
create(context) {
return {
// 监听 VariableDeclarator 节点
VariableDeclarator(node) {
const variableName = node.id.name;
if (variableName !== variableName.toLowerCase()) {
// 报告问题
context.report({
node,
message: 'Variable name should be lowercase.', // 错误信息
});
}
},
};
},
};
3. 规则的配置
(1) 配置文件
- ESLint 的规则通过配置文件(如
.eslintrc.js
)进行配置。 - 每条规则可以配置为以下值:
"off"
或0
:关闭规则。"warn"
或1
:将规则视为警告。"error"
或2
:将规则视为错误。
(2) 示例配置
module.exports = {
rules: {
'no-unused-vars': 'error', // 禁止未使用的变量
'indent': ['error', 2], // 强制使用 2 个空格缩进
'quotes': ['error', 'single'], // 强制使用单引号
},
};
4. ESLint 的核心组件
(1) 解析器(Parser)
- 将源代码转换为 AST。
- 默认解析器是
espree
,但可以配置其他解析器(如@babel/eslint-parser
)。
(2) 规则(Rules)
- 定义代码检查的逻辑。
- ESLint 内置了大量规则,也支持自定义规则。
(3) 插件(Plugins)
- 扩展 ESLint 的功能,提供额外的规则和配置。
- 例如,
eslint-plugin-react
提供了 React 相关的规则。
(4) 配置(Configuration)
- 定义规则的启用状态和配置选项。
- 支持多种配置文件格式(如
.eslintrc.js
、.eslintrc.json
)。
(5) 处理器(Processors)
- 用于处理非 JavaScript 文件(如 Markdown、Vue 单文件组件)。
5. ESLint 的检测过程
(1) 初始化
- 加载配置文件(如
.eslintrc.js
)。 - 加载插件和解析器。
(2) 解析代码
- 使用解析器将源代码转换为 AST。
(3) 遍历 AST
- 根据配置的规则,遍历 AST 节点。
- 调用规则的监听器函数,检查节点是否符合规则。
(4) 报告问题
- 如果代码违反规则,生成问题并输出。
6. 示例:ESLint 检测未使用的变量
(1) 代码
let x = 10;
let y = 20;
console.log(x);
(2) 规则配置
module.exports = {
rules: {
'no-unused-vars': 'error',
},
};
(3) 检测过程
- ESLint 解析代码,生成 AST。
- 遍历 AST,发现
y
被声明但未使用。 - 调用
no-unused-vars
规则的监听器,报告问题。
(4) 输出
2:5 - 'y' is assigned a value but never used. (no-unused-vars)
7. 总结
- ESLint 通过解析代码为 AST,并应用规则来检测代码异常。
- 规则是 ESLint 的核心,每条规则监听特定的 AST 节点,检查代码是否符合规则。
- 配置文件和插件 提供了灵活的规则管理和扩展能力。
- ESLint 的工作流程 包括解析代码、遍历 AST、应用规则和报告问题。
通过理解 ESLint 的工作原理,你可以更好地配置规则、编写自定义规则,以及优化代码检查流程。
十七、讲一讲对http2的了解,http2中服务器推送的能力了解吗,浏览器是怎么接收怎么处理的?
HTTP/2 是 HTTP 协议的第二个主要版本,于 2015 年发布。它旨在解决 HTTP/1.1 的局限性,提升 Web 性能。以下是 HTTP/2 的核心特性,以及 服务器推送(Server Push) 的工作原理和浏览器处理方式。
1.1主要问题:带宽跑不满 1.1 对带宽的利用率低下原因:
- 1.TCP的慢启动:tcp协议采用了一个由慢到快的传输加速度
- 2.同时开启多个TCP链接,这些链接之间相互竞争带宽,影响关键资源的下载速度
- 3、http队头阻塞问题
2.0里提出来的解决方案:
- 多路复用 1、不允许同时建立多个TCP连接,一个域名只能建立一个tcp连接 2、多路复用的流程:采用二进制分帧层1,将所有的请求处理为一帧一帧的请求,每一帧请求都带上独特的ID编号,服务端根据id区分拼接完整请求体,并以同样的方式返回响应
1. HTTP/2 的核心特性
(1) 二进制帧层
- HTTP/2 将请求和响应分解为二进制帧(Frame),而不是 HTTP/1.1 的文本格式。
- 帧是 HTTP/2 的最小通信单位,包含帧头(Header)和帧体(Payload)。
- 二进制帧更高效,解析速度更快。
(2) 多路复用(Multiplexing)
- HTTP/2 允许在同一个 TCP 连接上并行发送多个请求和响应。
- 解决了 HTTP/1.1 的队头阻塞(Head-of-Line Blocking)问题,提升了并发性能。
(3) 头部压缩(Header Compression)
- 使用 HPACK 算法压缩 HTTP 头部,减少数据传输量。
- 重复的头部字段只需发送一次,后续请求通过索引引用。
(4) 服务器推送(Server Push)
- 服务器可以在客户端请求之前,主动将资源推送到客户端。
- 减少客户端请求的往返时间(RTT),提升页面加载速度。
(5) 流优先级(Stream Prioritization)
- 客户端可以为请求设置优先级,服务器根据优先级处理请求。
- 确保关键资源(如 CSS、JavaScript)优先加载。
2. HTTP/2 服务器推送的工作原理
(1) 什么是服务器推送?
- 服务器推送允许服务器在客户端明确请求之前,主动将资源推送到客户端。
- 例如,当客户端请求 HTML 文件时,服务器可以主动推送与该 HTML 文件相关的 CSS、JavaScript 文件。
(2) 服务器推送的优势
- 减少客户端请求的往返时间(RTT)。
- 提前加载关键资源,提升页面加载速度。
(3) 服务器推送的实现
- 服务器在响应客户端请求时,通过 PUSH_PROMISE 帧通知客户端即将推送的资源。
- 客户端可以选择接受或拒绝推送的资源。
3. 浏览器如何处理服务器推送
(1) 接收推送资源
- 当服务器发送
PUSH_PROMISE
帧时,浏览器会检查缓存。- 如果资源已经在缓存中,浏览器会拒绝推送。
- 如果资源不在缓存中,浏览器会接受推送,并将资源存储在缓存中。
(2) 使用推送资源
- 当浏览器解析 HTML 文件时,如果发现需要某个资源(如 CSS、JavaScript),会优先使用服务器推送的资源。
- 如果推送的资源未被使用,浏览器会将其丢弃。
(3) 推送资源的缓存
- 推送的资源会被存储在浏览器的缓存中,供后续请求使用。
- 如果资源已经过期,浏览器会重新请求。
4. 服务器推送的示例
(1) 服务器端配置
以 Nginx 为例,配置 HTTP/2 服务器推送:
server {
listen 443 ssl http2;
server_name example.com;
location / {
http2_push /style.css;
http2_push /script.js;
root /var/www/html;
}
}
(2) 客户端请求
-
客户端请求
index.html
:GET /index.html HTTP/2
-
服务器响应并推送
style.css
和script.js
:HTTP/2 200 OK Link: </style.css>; rel=preload; as=style Link: </script.js>; rel=preload; as=script <html> <link rel="stylesheet" href="/style.css"> <script src="/script.js"></script> </html>
5. 服务器推送的注意事项
(1) 推送的资源可能被浪费
- 如果客户端已经缓存了资源,推送的资源会被拒绝。
- 如果推送的资源未被使用,会增加带宽消耗。
(2) 推送的资源需要合理选择
- 只推送关键资源(如 CSS、JavaScript),避免推送不必要的资源。
- 使用
rel=preload
提示浏览器资源的优先级。
(3) 推送的资源需要缓存
- 推送的资源应设置合适的缓存策略,避免重复推送。
6. 小结
HTTP/2 的核心特性
- 二进制帧层、多路复用、头部压缩、服务器推送、流优先级。
服务器推送的工作原理
- 服务器通过
PUSH_PROMISE
帧主动推送资源。 - 浏览器根据缓存状态决定是否接受推送。
浏览器处理服务器推送
- 检查缓存,接受或拒绝推送资源。
- 使用推送资源,存储在缓存中供后续请求使用。
服务器推送的优势
- 减少客户端请求的往返时间,提升页面加载速度。
注意事项
- 推送的资源可能被浪费,需要合理选择和缓存。
通过合理使用 HTTP/2 的服务器推送功能,可以显著提升 Web 应用的性能。
十八、http状态码中204和206是什么意思
HTTP 状态码是服务器对客户端请求的响应结果的一种标准化表示。204 和 206 是两个常见的 HTTP 状态码,分别表示不同的响应状态。以下是它们的详细解释:
1. 204 No Content(无内容)
(1) 含义
- 204 表示服务器成功处理了请求,但不需要返回任何实体内容。
- 通常用于以下场景:请求成功,但客户端不需要更新当前页面。表单提交成功,但不需要跳转或刷新页面。
(2) 示例场景
- 表单提交: 用户提交表单后,服务器成功处理了数据,但不需要返回新页面。
- 删除操作: 删除资源成功后,服务器不需要返回被删除的资源内容。
(3) 响应示例
HTTP/1.1 204 No Content
Date: Mon, 23 Oct 2023 12:00:00 GMT
Server: Apache
2. 206 Partial Content(部分内容)
(1) 含义
- 206 表示服务器成功处理了部分 GET 请求。
- 通常用于以下场景:客户端请求资源的某一部分(如分片下载或断点续传)。服务器返回请求范围内的内容。
(2) 示例场景
- 分片下载:客户端请求文件的某一部分(如视频文件的某个时间段)。
- 断点续传:客户端从上次中断的地方继续下载文件。
(3) 响应示例
HTTP/1.1 206 Partial Content
Date: Mon, 23 Oct 2023 12:00:00 GMT
Server: Apache
Content-Range: bytes 0-499/2000
Content-Length: 500
Content-Type: video/mp4
[文件的部分内容]
Content-Range
:表示返回的内容范围(如bytes 0-499/2000
表示返回前 500 字节,文件总大小为 2000 字节)。Content-Length
:表示返回内容的长度。Content-Type
:表示返回内容的类型。
- 204 和 206 的区别
状态码 | 含义 | 适用场景 | 是否返回内容 |
---|---|---|---|
204 | 无内容 | 表单提交、删除操作等 | 否 |
206 | 部分内容 | 分片下载、断点续传等 | 是 |
- 总结
- 204 No Content:表示请求成功,但不需要返回内容。适用于不需要更新客户端页面的场景。
- 206 Partial Content:表示请求成功,但只返回部分内容。适用于分片下载或断点续传的场景。
十九、预检请求是什么?
预检请求(Preflight Request) 是浏览器在发送某些 跨域 HTTP 请求 之前,自动发起的一种 OPTIONS 请求。它的目的是确保服务器允许实际的跨域请求。预检请求是 CORS(跨域资源共享) 机制的一部分,用于保护服务器和客户端的安全。
1. 为什么需要预检请求?
(1) 跨域请求的安全限制
- 浏览器遵循 同源策略(Same-Origin Policy),默认禁止跨域请求。
- 为了支持安全的跨域请求,CORS 机制被引入。
(2) 复杂请求的预检
- 对于 简单请求(如 GET、POST 请求,且使用特定的 Content-Type),浏览器会直接发送请求。
- 对于 复杂请求(如 PUT、DELETE 请求,或使用自定义的 HTTP 头),浏览器会先发送预检请求,确认服务器是否允许实际请求。
2. 什么情况下会触发预检请求?
以下条件满足任意一个时,浏览器会发送预检请求:
- 请求方法不是 GET、POST 或 HEAD。
- 例如:PUT、DELETE、PATCH。
- 请求头包含自定义头。
- 例如:
Authorization
、X-Custom-Header
。
- 例如:
- Content-Type 不是以下值之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain
3. 预检请求的工作流程
(1) 浏览器发送预检请求
- 浏览器发送一个 OPTIONS 请求 到服务器,包含以下头信息:
Origin
:请求的来源(协议 + 域名 + 端口)。Access-Control-Request-Method
:实际请求的 HTTP 方法(如 PUT、DELETE)。Access-Control-Request-Headers
:实际请求的自定义头(如Authorization
)。
(2) 服务器响应预检请求
- 服务器需要返回以下头信息:
Access-Control-Allow-Origin
:允许的源(如*
或具体的域名)。Access-Control-Allow-Methods
:允许的 HTTP 方法(如GET, POST, PUT
)。Access-Control-Allow-Headers
:允许的自定义头(如Authorization
)。Access-Control-Max-Age
:预检请求的缓存时间(单位:秒)。
(3) 浏览器发送实际请求
- 如果预检请求通过,浏览器会发送实际的跨域请求。
- 如果预检请求失败,浏览器会阻止实际请求,并抛出 CORS 错误。
4. 预检请求示例
(1) 客户端代码
fetch('https://api.example.com/data', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value',
},
body: JSON.stringify({ key: 'value' }),
});
(2) 预检请求(OPTIONS)
OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://www.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, X-Custom-Header
(3) 服务器响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400
(4) 实际请求(PUT)
PUT /data HTTP/1.1
Host: api.example.com
Origin: https://www.example.com
Content-Type: application/json
X-Custom-Header: value
{"key":"value"}
(5) 服务器响应实际请求
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Content-Type: application/json
{"status":"success"}
5. 如何减少预检请求?
(1) 使用简单请求
- 尽量使用 GET、POST 方法,并避免自定义头。
- 使用
application/x-www-form-urlencoded
、multipart/form-data
或text/plain
作为Content-Type
。
(2) 缓存预检请求
- 通过设置
Access-Control-Max-Age
头,缓存预检请求的结果,减少重复的预检请求。
(3) 避免跨域请求
- 如果可能,将 API 和前端部署在同一个域名下,避免跨域问题。
6. 总结
特性 | 预检请求(OPTIONS) | 实际请求(GET/POST/PUT 等) |
---|---|---|
触发条件 | 复杂请求(自定义头、非简单方法等) | 简单请求或预检请求通过后 |
请求方法 | OPTIONS | GET、POST、PUT 等 |
目的 | 确认服务器是否允许实际请求 | 发送实际的跨域请求 |
响应头 | Access-Control-Allow-* 系列头 | 实际数据 |
预检请求是 CORS 机制的重要组成部分,用于确保跨域请求的安全性。通过合理配置服务器响应头,可以减少预检请求的频率,提升性能。
预检请求(Preflight Request) 的响应状态码通常是 200 OK。这是因为预检请求的目的是确认服务器是否允许实际的跨域请求,而不是实际处理请求。服务器通过返回 200 OK 状态码和相关的 CORS 头信息,告诉浏览器是否允许后续的实际请求。
1. 预检请求的响应状态码
(1) 成功响应
- 如果服务器允许跨域请求,预检请求会返回 200 OK 状态码,并包含以下 CORS 头信息:
Access-Control-Allow-Origin
:允许的源(如*
或具体的域名)。Access-Control-Allow-Methods
:允许的 HTTP 方法(如GET, POST, PUT
)。Access-Control-Allow-Headers
:允许的自定义头(如Authorization
)。Access-Control-Max-Age
:预检请求的缓存时间(单位:秒)。
(2) 失败响应
- 如果服务器不允许跨域请求,预检请求可能会返回 403 Forbidden 或 405 Method Not Allowed 等状态码。
- 浏览器会根据预检请求的响应决定是否发送实际请求。
2. 预检请求的响应示例
(1) 成功响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
(2) 失败响应
HTTP/1.1 403 Forbidden
3. 小结
场景 | 状态码 | 说明 |
---|---|---|
预检请求成功 | 200 OK | 服务器允许跨域请求,返回 CORS 头信息。 |
预检请求失败 | 403/405 等 | 服务器不允许跨域请求,浏览器会阻止实际请求。 |
预检请求的响应状态码通常是 200 OK,表示服务器允许跨域请求。如果服务器不允许跨域请求,可能会返回其他状态码(如 403 或 405)。
二十、讲一讲你对前端安全的了解,内容安全策略CSP中有个属性不做拦截只做上报了解吗
前端安全是 Web 开发中非常重要的一部分,涉及保护用户数据、防止恶意攻击以及确保应用程序的可靠性。以下是一些常见的前端安全问题及其解决方案:
1. 跨站脚本攻击(XSS)
(1) 什么是 XSS?
- XSS 攻击者通过在网页中注入恶意脚本,窃取用户数据或劫持用户会话。
- 分为三种类型:
- 存储型 XSS:恶意脚本存储在服务器上(如评论区)。
- 反射型 XSS:恶意脚本通过 URL 参数反射到页面中。
- DOM 型 XSS:恶意脚本通过修改 DOM 实现攻击。
(2) 如何防御 XSS?
- 输入过滤和转义:
- 对用户输入的数据进行过滤和转义,确保特殊字符(如
<
,>
,&
)不会被解析为 HTML 或 JavaScript。 - 使用工具库(如
DOMPurify
)对 HTML 进行净化。
- 对用户输入的数据进行过滤和转义,确保特殊字符(如
- 设置 HTTP 头:
- 使用
Content-Security-Policy
(CSP)限制脚本加载来源。 - 设置
X-XSS-Protection
头启用浏览器的 XSS 防护机制。
- 使用
- 避免内联脚本:
- 避免使用
innerHTML
或eval
,改用安全的 DOM 操作方法。
- 避免使用
2. 跨站请求伪造(CSRF)
(1) 什么是 CSRF?
- CSRF 攻击者诱使用户在不知情的情况下发送恶意请求,执行非预期的操作(如转账、修改密码)。
(2) 如何防御 CSRF?
- 使用 CSRF Token:在表单或请求中添加随机生成的 CSRF Token,服务器验证 Token 的有效性。
- 设置 SameSite Cookie:将 Cookie 的
SameSite
属性设置为Strict
或Lax
,防止跨站请求携带 Cookie。 - 验证请求来源:检查
Referer
或Origin
头,确保请求来自可信的源。
3. 点击劫持(Clickjacking)
(1) 什么是点击劫持?
- 攻击者通过透明的 iframe 覆盖在合法页面上,诱使用户点击隐藏的按钮或链接。
(2) 如何防御点击劫持?
- 设置 X-Frame-Options 头:使用
X-Frame-Options: DENY
或X-Frame-Options: SAMEORIGIN
防止页面被嵌入 iframe。 - 使用 CSP 的 frame-ancestors 指令:设置
Content-Security-Policy: frame-ancestors 'self'
,限制页面嵌入。
4. 内容安全策略(CSP)
(1) 什么是 CSP?
- CSP 是一种安全机制,用于限制页面中可以加载的资源(如脚本、样式、图片)。
(2) 如何配置 CSP?
-
在 HTTP 头中设置
Content-Security-Policy
,例如:Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline';
-
常用指令:
default-src
:默认资源加载策略。script-src
:限制脚本加载来源。style-src
:限制样式加载来源。img-src
:限制图片加载来源。
5. HTTP 严格传输安全(HSTS)
(1) 什么是 HSTS?
- HSTS 强制浏览器使用 HTTPS 连接,防止中间人攻击(MITM)。
(2) 如何启用 HSTS?
-
在 HTTP 头中设置
Strict-Transport-Security
,例如:Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-
参数:
max-age
:HSTS 的有效期(单位:秒)。includeSubDomains
:包含子域名。preload
:提交到浏览器的 HSTS 预加载列表。
6. 数据安全
(1) 敏感数据保护
- 避免在前端存储敏感数据:不要将敏感数据(如密码、令牌)存储在
localStorage
或sessionStorage
中。 - 使用 HTTPS:确保所有数据传输都通过 HTTPS 加密。
(2) 密码安全
- 前端加密:对用户密码进行哈希处理(如使用
bcrypt
),但最终的安全依赖于后端。 - 防止暴力破解:实施验证码、登录失败限制等措施。
7. 第三方依赖安全
(1) 依赖库安全
- 定期更新依赖:使用工具(如
npm audit
、yarn audit
)检查依赖库的安全漏洞。 - 使用可信的 CDN:确保第三方资源(如 jQuery、Bootstrap)来自可信的 CDN。
(2) 防止第三方脚本滥用
- 限制第三方脚本权限:使用 CSP 限制第三方脚本的加载和执行。
- 审核第三方脚本:确保第三方脚本不会引入安全风险。
8. 其他安全问题
(1) 开放重定向
- 问题:攻击者利用开放重定向将用户引导到恶意网站。
- 解决方案:验证重定向 URL 的合法性,避免使用用户输入的重定向地址。
(2) 错误信息泄露
- 问题:错误信息中泄露敏感数据(如数据库信息)。
- 解决方案:在生产环境中隐藏详细的错误信息,返回通用的错误提示。
9. 总结
安全问题 | 防御措施 |
---|---|
XSS | 输入过滤、CSP、避免内联脚本 |
CSRF | CSRF Token、SameSite Cookie、验证请求来源 |
点击劫持 | X-Frame-Options、CSP 的 frame-ancestors |
CSP | 配置 Content-Security-Policy 头 |
HSTS | 启用 Strict-Transport-Security 头 |
数据安全 | 避免存储敏感数据、使用 HTTPS |
第三方依赖安全 | 定期更新依赖、使用可信的 CDN |
通过采取这些安全措施,可以有效提升前端应用的安全性,保护用户数据和隐私。
内容安全策略(Content Security Policy,CSP)中有一个特殊的指令可以用来 只报告违规行为而不拦截, Content-Security-Policy-Report-Only
。
1. Content-Security-Policy-Report-Only
的作用
- 只报告,不拦截:当配置了
Content-Security-Policy-Report-Only
头时,浏览器会检测 CSP 规则的违规行为,但不会阻止这些行为。违规行为会被上报到指定的 URL,方便开发者分析和调试。 - 适用场景:在开发和测试阶段,逐步引入 CSP 规则时,可以使用
Report-Only
模式,观察规则的影响。在生产环境中,可以先启用Report-Only
模式,确保 CSP 规则不会意外拦截合法内容。
2. 如何配置 Content-Security-Policy-Report-Only
(1) 基本语法
Content-Security-Policy-Report-Only: <策略指令>; report-uri <上报URL>
<策略指令>
:定义 CSP 规则(如default-src 'self'
)。report-uri
:指定违规行为的上报 URL。
(2) 示例
以下是一个 Content-Security-Policy-Report-Only
的配置示例:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report-endpoint
- 规则:只允许从同源加载脚本。
- 上报 URL:违规行为会上报到
/csp-report-endpoint
。
3、违规报告的内容
当浏览器检测到 CSP 违规行为时,会向 report-uri
指定的 URL 发送一个 POST 请求,请求体是一个 JSON 对象,包含以下信息:
(1) 示例报告
{
"csp-report": {
"document-uri": "https://example.com/page.html",
"referrer": "https://referrer.com/",
"violated-directive": "script-src 'self'",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self'; report-uri /csp-report-endpoint",
"blocked-uri": "https://malicious.com/script.js",
"line-number": 42,
"column-number": 21,
"source-file": "https://example.com/script.js",
"status-code": 200,
"script-sample": "alert('XSS')"
}
}
(2) 字段说明
字段 | 说明 |
---|---|
document-uri | 发生违规的页面 URL。 |
referrer | 页面的来源 URL。 |
violated-directive | 被违反的 CSP 指令。 |
effective-directive | 实际生效的 CSP 指令。 |
original-policy | 完整的 CSP 策略。 |
blocked-uri | 被拦截的资源 URL。 |
line-number | 违规代码的行号(如果可用)。 |
column-number | 违规代码的列号(如果可用)。 |
source-file | 违规代码所在的文件 URL。 |
status-code | 页面的 HTTP 状态码。 |
script-sample | 违规脚本的示例代码(如果可用)。 |
4、Report-Only
与 Enforce
的区别
特性 | Content-Security-Policy-Report-Only | Content-Security-Policy (Enforce) |
---|---|---|
拦截行为 | 不拦截,只报告 | 拦截违规行为 |
适用场景 | 测试和调试阶段 | 生产环境 |
配置指令 | Content-Security-Policy-Report-Only | Content-Security-Policy |
上报机制 | 通过 report-uri 或 report-to 上报 | 无上报机制(直接拦截) |
5. report-uri
和 report-to
的区别
-
report-uri
:传统的上报方式,指定一个 URL 接收违规报告。Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report-endpoint
-
report-to
:-
新的上报方式,使用 Reporting API 定义上报端点。
-
需要先在
Report-To
头中定义上报组。Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-report-endpoint"}]} Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint
-
6. 小结
Content-Security-Policy-Report-Only
用于只报告 CSP 违规行为而不拦截,适合在测试和调试阶段使用。- 通过
report-uri
或report-to
可以接收违规报告,帮助开发者分析和优化 CSP 规则。 - 在生产环境中,建议在充分测试后切换到
Content-Security-Policy
,启用实际的拦截功能。
通过合理使用 Report-Only
模式,可以逐步引入 CSP 规则,避免对用户体验造成影响。
二十一、TS的内置工具类型有哪些
TypeScript 提供了许多内置的工具类型(Utility Types),用于简化类型操作和增强类型系统的表达能力。
1. Partial<T>
-
作用:将类型
T
的所有属性变为可选。 -
示例:
interface User { name: string; age: number; } type PartialUser = Partial<User>; // 等价于: // type PartialUser = { // name?: string; // age?: number; // }
2. Required<T>
-
作用:将类型
T
的所有属性变为必选。 -
示例:
interface User { name?: string; age?: number; } type RequiredUser = Required<User>; // 等价于: // type RequiredUser = { // name: string; // age: number; // }
3. Readonly<T>
-
作用:将类型
T
的所有属性变为只读。 -
示例:
interface User { name: string; age: number; } type ReadonlyUser = Readonly<User>; // 等价于: // type ReadonlyUser = { // readonly name: string; // readonly age: number; // }
4. Record<K, T>
-
作用:创建一个对象类型,其键为
K
,值为T
。 -
示例:
type UserRoles = Record<string, boolean>; // 等价于: // type UserRoles = { // [key: string]: boolean; // }
5. Pick<T, K>
-
作用:从类型
T
中选取指定的属性K
。 -
示例:
interface User { name: string; age: number; email: string; } type UserNameAndAge = Pick<User, 'name' | 'age'>; // 等价于: // type UserNameAndAge = { // name: string; // age: number; // }
6. Omit<T, K>
-
作用:从类型
T
中排除指定的属性K
。 -
示例:
interface User { name: string; age: number; email: string; } type UserWithoutEmail = Omit<User, 'email'>; // 等价于: // type UserWithoutEmail = { // name: string; // age: number; // }
7. Exclude<T, U>
-
作用:从类型
T
中排除可以赋值给U
的类型。 -
示例:
type T = 'a' | 'b' | 'c'; type U = 'a' | 'b'; type Result = Exclude<T, U>; // 等价于: // type Result = 'c'
8. Extract<T, U>
-
作用:从类型
T
中提取可以赋值给U
的类型。 -
示例:
type T = 'a' | 'b' | 'c'; type U = 'a' | 'b'; type Result = Extract<T, U>; // 等价于: // type Result = 'a' | 'b'
9. NonNullable<T>
-
作用:从类型
T
中排除null
和undefined
。 -
示例:
type T = string | number | null | undefined; type Result = NonNullable<T>; // 等价于: // type Result = string | number
10. ReturnType<T>
-
作用:获取函数类型
T
的返回值类型。 -
示例:
function getUser() { return { name: 'Alice', age: 30 }; } type User = ReturnType<typeof getUser>; // 等价于: // type User = { // name: string; // age: number; // }
11. InstanceType<T>
-
作用:获取构造函数类型
T
的实例类型。 -
示例:
class User { name: string; constructor(name: string) { this.name = name; } } type UserInstance = InstanceType<typeof User>; // 等价于: // type UserInstance = User
12. Parameters<T>
-
作用:获取函数类型
T
的参数类型元组。 -
示例:
function greet(name: string, age: number) { console.log(`Hello, ${name}! You are ${age} years old.`); } type GreetParams = Parameters<typeof greet>; // 等价于: // type GreetParams = [string, number]
13. ConstructorParameters<T>
-
作用:获取构造函数类型
T
的参数类型元组。 -
示例:
class User { constructor(public name: string, public age: number) {} } type UserConstructorParams = ConstructorParameters<typeof User>; // 等价于: // type UserConstructorParams = [string, number]
14. ThisParameterType<T>
-
作用:获取函数类型
T
的this
参数类型。 -
示例:
function greet(this: { name: string }) { console.log(`Hello, ${this.name}!`); } type GreetThis = ThisParameterType<typeof greet>; // 等价于: // type GreetThis = { name: string }
15. OmitThisParameter<T>
-
作用:移除函数类型
T
的this
参数类型。 -
示例:
function greet(this: { name: string }) { console.log(`Hello, ${this.name}!`); } type GreetWithoutThis = OmitThisParameter<typeof greet>; // 等价于: // type GreetWithoutThis = () => void
16. Awaited<T>
-
作用:获取
Promise
类型T
的解析值类型。 -
示例:
type PromiseResult = Awaited<Promise<string>>; // 等价于: // type PromiseResult = string
小结
工具类型 | 作用 |
---|---|
Partial<T> | 将类型 T 的所有属性变为可选。 |
Required<T> | 将类型 T 的所有属性变为必选。 |
Readonly<T> | 将类型 T 的所有属性变为只读。 |
Record<K, T> | 创建一个对象类型,其键为 K ,值为 T 。 |
Pick<T, K> | 从类型 T 中选取指定的属性 K 。 |
Omit<T, K> | 从类型 T 中排除指定的属性 K 。 |
Exclude<T, U> | 从类型 T 中排除可以赋值给 U 的类型。 |
Extract<T, U> | 从类型 T 中提取可以赋值给 U 的类型。 |
NonNullable<T> | 从类型 T 中排除 null 和 undefined 。 |
ReturnType<T> | 获取函数类型 T 的返回值类型。 |
InstanceType<T> | 获取构造函数类型 T 的实例类型。 |
Parameters<T> | 获取函数类型 T 的参数类型元组。 |
ConstructorParameters<T> | 获取构造函数类型 T 的参数类型元组。 |
ThisParameterType<T> | 获取函数类型 T 的 this 参数类型。 |
OmitThisParameter<T> | 移除函数类型 T 的 this 参数类型。 |
Awaited<T> | 获取 Promise 类型 T 的解析值类型。 |
总结
这些题目就是前几天晚上腾讯面试的前端面试题了,面完我已经瘫了,悬着的心终于死了,还有有很多不足的地方,还是需要好好复习,努力总结,好好沉淀。