腾讯前端开发校招一面面试题(被吊打版已老实)

6,843 阅读18分钟

前言

书接上文,经历刚刚20分钟手写6道代码题的磨练后,又是与面试官一个小时的口舌之战,好吧,其实是被面试官拷打的一个小时,已老实。不得不说腾讯的面试官就是犀利,问的问题不仅深而且广度还很广,也有亿亿点点难。

还没看过笔试题的大家可以先移步到这篇文章看看笔试题

腾讯前端开发校招一面笔试题前言 小编我自从360实习归来之后也是马不停蹄的开始了校招的找工作,没有停下学习的脚步,前端时 - 掘金 (juejin.cn)

正文

面试题的大概题目如下,可能有些忘记了

image-20250125185843688

首先就是开始自我介绍,简单介绍完了之后就开始问我问题了。

一、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)

  • 优化原理:对于静态属性(如 classstyle 等),Vue 3 会将其提升到渲染函数外部,避免每次渲染时重新计算。
  • 效果:减少不必要的属性计算和比对。提升渲染性能。
<div class="static-class" :class="dynamicClass">
  Content
</div>
  • class="static-class" 会被提升为静态属性,只有 dynamicClass 会在每次渲染时计算。

3. 补丁标志(Patch Flags)

  • 优化原理:Vue 3 在编译阶段为每个动态节点添加一个“补丁标志”(Patch Flag),用于标记节点的哪些部分需要更新(如 classstyleprops 等)。在运行时,Vue 会根据补丁标志只更新必要的部分,而不是全量比对。
  • 效果:减少虚拟 DOM 比对的开销。提升更新性能。
<div :class="dynamicClass" :style="dynamicStyle">
  {{ dynamicContent }}
</div>
  • 编译后,Vue 会为这个节点生成补丁标志,标记 classstyle 和文本内容需要更新。

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-ifv-else 划分为一个块,只比对块内的节点。

5. 事件侦听器缓存(Event Listener Caching)

  • 优化原理:Vue 3 会对模板中的事件侦听器进行缓存,避免每次渲染时重新创建。
  • 效果:减少事件侦听器的创建和销毁开销。提升渲染性能。
<button @click="handleClick">Click Me</button>
  • 编译后,handleClick 函数会被缓存,避免重复创建。

6. 动态节点标记(Dynamic Node Marking)

  • 优化原理:Vue 3 在编译阶段会标记动态节点(如 v-ifv-for 等),并在运行时跳过静态节点的比对。
  • 效果:减少虚拟 DOM 比对的开销。提升渲染性能。

7. Tree Shaking 支持

  • 优化原理:Vue 3 的模板编译器支持 Tree Shaking,只打包实际使用的功能。
  • 效果:减少最终打包体积。提升应用加载性能。

8. 更快的编译器

  • 优化原理:Vue 3 的模板编译器经过重写,性能显著提升。
  • 效果:加快编译速度。提升开发体验。

总结

Vue 3 在模板编译阶段的优化主要集中在以下几个方面:

  1. 静态节点和属性提升:减少重复创建和比对。
  2. 补丁标志:精准更新动态节点。
  3. 块树优化:缩小比对范围。
  4. 事件侦听器缓存:减少事件处理开销。
  5. 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-ifv-forv-model 等指令的部分。
<div :class="dynamicClass">
  <p>{{ dynamicContent }}</p>
</div>
  • :class="dynamicClass"{{ dynamicContent }} 都是动态变量。

3. Vue 3 如何识别静态变量和动态变量

(1)模板解析

Vue 3 的模板编译器会解析模板,生成抽象语法树(AST)。在解析过程中,编译器会标记每个节点的类型和属性。

(2)静态分析

编译器会对 AST 进行静态分析,识别出哪些部分是静态的,哪些部分是动态的。具体方法包括:

  1. 纯文本内容

    如果节点是纯文本(如 "Hello World"),则标记为静态。

  2. 静态属性

    如果属性值是固定的(如 class="static-class"),则标记为静态。

  3. 动态绑定

    如果属性值包含插值语法(如 {{ dynamicContent }})或指令(如 v-bind),则标记为动态。

  4. 指令分析

    对于 v-ifv-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>
静态分析结果
  1. 静态部分

    class="static-class":静态属性。

    <p>Static Content</p>:静态节点。

  2. 动态部分

    :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" 是静态节点,直接生成。
  • dynamicClassdynamicContent 是动态变量,标记为需要更新。

5. 如何手动标记静态变量

在某些情况下,开发者可以手动标记静态变量,以帮助编译器更好地优化。例如:

  • 使用 v-once 指令标记静态节点:

    <p v-once>Static Content</p>
    

    v-once 会告诉 Vue 这个节点是静态的,只会渲染一次。


总结

Vue 3 通过模板解析、静态分析和补丁标志,自动区分静态变量和动态变量:

  • 静态变量:不会变化的部分,可以被优化(如静态节点提升)。
  • 动态变量:可能变化的部分,需要在每次渲染时重新计算。

三、模板编译这一块具体是如何编译的?模板编译里AST抽象语法树了解吗

Vue 3 的模板编译过程是将模板字符串转换为渲染函数的过程,其中包括解析模板、生成抽象语法树(AST)、优化 AST 和生成渲染函数等步骤。以下是对模板编译过程的详细解析,以及 AST 的作用和生成过程。


模板编译的步骤

Vue 3 的模板编译过程可以分为以下几个步骤:

  1. 解析模板

    将模板字符串解析为抽象语法树(AST)。

    解析器会识别模板中的 HTML 标签、属性、文本内容、指令等。

  2. 优化 AST

    对生成的 AST 进行静态分析,标记静态节点和静态属性。

    通过静态节点提升、补丁标志等优化手段,减少运行时开销。

  3. 生成渲染函数

    将优化后的 AST 转换为可执行的渲染函数。

    渲染函数用于生成虚拟 DOM。


1. 解析模板并生成 AST

什么是 AST?

AST(Abstract Syntax Tree,抽象语法树)是模板的结构化表示。它是一个树形结构,每个节点代表模板中的一个元素、属性或文本内容。

AST 节点的结构

一个典型的 AST 节点可能包含以下属性:

  • type:节点类型(如 ElementTextExpression 等)。
  • tag:标签名(如 divp 等)。
  • attrs:属性列表(如 classstyle 等)。
  • children:子节点列表。
  • directives:指令列表(如 v-ifv-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)
  • 为动态节点添加补丁标志,标记节点的哪些部分需要更新(如 classstyleprops 等)。
  • 在运行时,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 的模板编译过程包括以下步骤:

  1. 解析模板:将模板字符串解析为 AST。
  2. 优化 AST:通过静态节点提升、补丁标志等优化手段,减少运行时开销。
  3. 生成渲染函数:将优化后的 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-4text-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 的对比

特性UnoCSSTailwind 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
(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 是一个模块打包工具,它的核心功能是通过 LoaderPlugin 对模块进行处理和扩展。虽然 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 的区别

特性LoaderPlugin
作用转换模块的源代码扩展 Webpack 的功能
运行时机在模块加载时运行在整个构建生命周期中运行
输入/输出接收源文件,返回转换后的内容不直接处理文件,而是操作构建过程
配置方式module.rules 中配置plugins 中配置
实现方式导出一个函数导出一个类,包含 apply 方法
典型应用处理 CSS、图片、字体等资源代码压缩、资源管理、环境变量注入等

4. 总结

  • Loader:用于转换模块的源代码。适合处理文件级别的任务(如 CSS、图片、字体等)。通过 module.rules 配置。
  • Plugin:用于扩展 Webpack 的功能。适合处理构建过程级别的任务(如代码压缩、资源优化等)。通过 plugins 配置。

八、pnpm跟npm有什么区别?包管理工具corepack了解吗?

pnpm和npm我也是了解的,但是corepack这个包管理工具确实不太熟悉

pnpmnpm 都是 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.jsonnpm 的生态系统。

    支持 npm 的大部分命令(如 installrunpublish 等)。

    可以无缝替换 npm

  • npm

    是 Node.js 的默认包管理工具,兼容性最好。


(6) Monorepo 支持
  • pnpm

    内置对 Monorepo 的支持,通过 pnpm-workspace.yaml 配置文件管理多个子项目。

    依赖共享和链接机制非常适合 Monorepo 场景。

  • npm

    需要借助第三方工具(如 lerna)来实现 Monorepo 支持。


(7) 生态和社区
  • pnpm

    社区规模较小,但增长迅速。

    在大型项目和 Monorepo 中越来越受欢迎。

  • npm

    是 Node.js 的官方包管理工具,社区和生态系统非常成熟。

总结

pnpm 和 npm 的对比
特性pnpmnpm
磁盘空间利用率高(通过硬链接节省空间)低(每个项目独立存储依赖)
安装速度快(依赖全局存储)较慢(每次都需要下载)
node_modules 结构扁平化 + 符号链接扁平化
严格性严格(避免幽灵依赖)较宽松(可能存在幽灵依赖)
Monorepo 支持内置支持需要第三方工具(如 lerna)
生态和社区较小但增长迅速非常成熟

Corepack 是什么?

(1) 核心功能

  • Corepack 是 Node.js 官方提供的一个包管理器管理工具,用于管理不同的 JavaScript 包管理器(如 npmyarnpnpm 等)。
  • 它的目标是简化包管理器的安装和使用,确保开发者可以在不同项目中使用一致的包管理器版本。

(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 中所有项目使用统一的依赖管理工具(如 npmyarnpnpm),确保依赖版本一致。
  • 通过工具(如 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高效依赖管理支持中等活跃
LernaJavaScript/TypeScript支持 npm/yarn依赖提升需要配置成熟
TurborepoJavaScript/TypeScript支持 npm/yarn/pnpm增量构建支持新兴
RushJavaScript/TypeScript支持 npm/yarn/pnpm严格依赖管理支持微软支持
Bazel多语言(JS/Java等)高效增量构建内置强大
Pnpm WorkspacesJavaScript/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 实例。
  • 每个缓存的键是组件的唯一标识(通常是 namekey),值是组件的 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> 的底层实现主要依赖于以下机制:

  1. 缓存对象:存储组件的 VNode 实例。
  2. LRU 算法:管理缓存数量,移除最久未使用的缓存。
  3. 生命周期钩子:通过 activateddeactivated 钩子管理组件的激活和停用。

十二、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:管理 localStoragesessionStorage

    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在本地开发的时候底层热更新的实现这一块有什么区别

WebpackVite 在本地开发时的热更新(HMR,Hot Module Replacement)实现机制有显著区别,主要体现在以下几个方面:


1. Webpack 的热更新实现

(1) 基本原理

  • Webpack 的热更新是基于 文件监听模块依赖图 实现的。
  • 当文件发生变化时,Webpack 会重新构建受影响的模块,并通过 WebSocket 将更新推送到浏览器。
  • 浏览器接收到更新后,替换旧的模块代码并重新执行相关逻辑。

(2) 具体流程

  1. 文件监听:Webpack 通过 webpack-dev-server 监听文件系统的变化。当文件发生变化时,触发重新构建。
  2. 依赖图构建:Webpack 会构建一个模块依赖图,记录模块之间的依赖关系。当某个模块发生变化时,Webpack 会找到所有依赖该模块的模块,并重新构建这些模块。
  3. HMR 更新推送:Webpack 通过 WebSocket 将更新的模块代码推送到浏览器。浏览器接收到更新后,替换旧的模块代码并执行 module.hot.accept 回调。
  4. 模块替换:浏览器根据更新信息替换模块,并重新执行相关逻辑(如重新渲染组件)。

(3) 优点:成熟稳定,生态丰富。支持复杂的模块依赖关系。

**(4) 缺点:**随着项目规模增大,构建速度变慢。热更新时可能需要重新构建整个依赖图,导致更新时间较长。


2. Vite 的热更新实现

(1) 基本原理

  • Vite 的热更新是基于 ESM(ES Modules)浏览器原生模块系统 实现的。
  • Vite 利用浏览器的原生 ESM 支持,直接在浏览器中加载模块,无需打包。
  • 当文件发生变化时,Vite 只需更新受影响的模块,并通过 WebSocket 通知浏览器重新加载。

(2) 具体流程

  1. 文件监听:Vite 通过 chokidar 监听文件系统的变化。当文件发生变化时,触发更新逻辑。
  2. 模块更新:Vite 会分析模块的依赖关系,找到受影响的模块。通过 WebSocket 通知浏览器重新加载这些模块。
  3. 浏览器重新加载:浏览器接收到更新通知后,重新请求受影响的模块。由于模块是通过 ESM 加载的,浏览器会直接使用新的模块代码。
  4. 模块替换:浏览器根据新的模块代码更新页面,无需重新加载整个应用。

(3) 优点

  • 基于 ESM,更新速度极快。
  • 无需重新构建整个应用,只需更新受影响的模块。
  • 开发服务器启动速度快,适合大型项目。

(4) 缺点

  • 依赖浏览器的 ESM 支持,兼容性较差(现代浏览器支持良好)。
  • 对 CommonJS 模块的支持需要额外处理。

Webpack 和 Vite 热更新的对比

特性WebpackVite
热更新机制基于文件监听和模块依赖图基于 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(如 processBuffer 等),这些 API 在浏览器环境中不可用。
  • 需要使用 polyfill 或替换库(如 process/browser)来解决兼容性问题。

优化 CommonJS 模块的使用**

(1) 使用 ESM 优先的库
  • 尽量选择支持 ESM 的库,避免使用纯 CommonJS 模块。
  • 许多流行的库(如 lodash-esrxjs)已经提供了 ESM 版本。
(2) 手动转换 CommonJS 模块
  • 如果必须使用 CommonJS 模块,可以手动将其转换为 ESM 格式。
  • 使用工具如 ESBuildBabel 进行转换。
(3) 配置 optimizeDeps
  • Vite 提供了 optimizeDeps 配置项,可以预构建 CommonJS 模块,以提升开发服务器的性能。

  • vite.config.js 中配置:

    import { defineConfig } from 'vite';
    export default defineConfig({
      optimizeDeps: {
        include: ['commonjs-module'], // 预构建 CommonJS 模块
      },
    });
    

小结

Vite 通过以下方式处理 CommonJS 模块:

  1. 开发环境:使用 ESBuild 动态将 CommonJS 模块转换为 ESM。
  2. 生产环境:使用 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)中的新语法特性。

  • 例如:

    findLastfindLastIndex:数组方法。

    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 模块,导出一个对象,包含 metacreate 两个属性:
    • meta:规则的元信息,如类型、文档、是否可修复等。
    • create:一个函数,返回一个对象,包含 AST 节点的监听器。

(2) 规则的实现

  • create 函数中,规则会监听特定的 AST 节点类型(如 VariableDeclarationFunctionDeclaration 等)。
  • 当 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) 检测过程

  1. ESLint 解析代码,生成 AST。
  2. 遍历 AST,发现 y 被声明但未使用。
  3. 调用 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.cssscript.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 状态码是服务器对客户端请求的响应结果的一种标准化表示。204206 是两个常见的 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:表示返回内容的类型。

  1. 204 和 206 的区别
状态码含义适用场景是否返回内容
204无内容表单提交、删除操作等
206部分内容分片下载、断点续传等

  1. 总结
  • 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. 什么情况下会触发预检请求?

以下条件满足任意一个时,浏览器会发送预检请求:

  1. 请求方法不是 GET、POST 或 HEAD
    • 例如:PUT、DELETE、PATCH。
  2. 请求头包含自定义头
    • 例如:AuthorizationX-Custom-Header
  3. 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-urlencodedmultipart/form-datatext/plain 作为 Content-Type

(2) 缓存预检请求

  • 通过设置 Access-Control-Max-Age 头,缓存预检请求的结果,减少重复的预检请求。

(3) 避免跨域请求

  • 如果可能,将 API 和前端部署在同一个域名下,避免跨域问题。

6. 总结

特性预检请求(OPTIONS)实际请求(GET/POST/PUT 等)
触发条件复杂请求(自定义头、非简单方法等)简单请求或预检请求通过后
请求方法OPTIONSGET、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 Forbidden405 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)。

预检请求:为跨域请求保驾护航(上)-CSDN博客

二十、讲一讲你对前端安全的了解,内容安全策略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 防护机制。
  • 避免内联脚本
    • 避免使用 innerHTMLeval,改用安全的 DOM 操作方法。

2. 跨站请求伪造(CSRF)

(1) 什么是 CSRF?

  • CSRF 攻击者诱使用户在不知情的情况下发送恶意请求,执行非预期的操作(如转账、修改密码)。

(2) 如何防御 CSRF?

  • 使用 CSRF Token:在表单或请求中添加随机生成的 CSRF Token,服务器验证 Token 的有效性。
  • 设置 SameSite Cookie:将 Cookie 的 SameSite 属性设置为 StrictLax,防止跨站请求携带 Cookie。
  • 验证请求来源:检查 RefererOrigin 头,确保请求来自可信的源。

3. 点击劫持(Clickjacking)

(1) 什么是点击劫持?

  • 攻击者通过透明的 iframe 覆盖在合法页面上,诱使用户点击隐藏的按钮或链接。

(2) 如何防御点击劫持?

  • 设置 X-Frame-Options 头:使用 X-Frame-Options: DENYX-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) 敏感数据保护

  • 避免在前端存储敏感数据:不要将敏感数据(如密码、令牌)存储在 localStoragesessionStorage 中。
  • 使用 HTTPS:确保所有数据传输都通过 HTTPS 加密。

(2) 密码安全

  • 前端加密:对用户密码进行哈希处理(如使用 bcrypt),但最终的安全依赖于后端。
  • 防止暴力破解:实施验证码、登录失败限制等措施。

7. 第三方依赖安全

(1) 依赖库安全

  • 定期更新依赖:使用工具(如 npm audityarn audit)检查依赖库的安全漏洞。
  • 使用可信的 CDN:确保第三方资源(如 jQuery、Bootstrap)来自可信的 CDN。

(2) 防止第三方脚本滥用

  • 限制第三方脚本权限:使用 CSP 限制第三方脚本的加载和执行。
  • 审核第三方脚本:确保第三方脚本不会引入安全风险。

8. 其他安全问题

(1) 开放重定向

  • 问题:攻击者利用开放重定向将用户引导到恶意网站。
  • 解决方案:验证重定向 URL 的合法性,避免使用用户输入的重定向地址。

(2) 错误信息泄露

  • 问题:错误信息中泄露敏感数据(如数据库信息)。
  • 解决方案:在生产环境中隐藏详细的错误信息,返回通用的错误提示。

9. 总结

安全问题防御措施
XSS输入过滤、CSP、避免内联脚本
CSRFCSRF 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-OnlyEnforce 的区别

特性Content-Security-Policy-Report-OnlyContent-Security-Policy(Enforce)
拦截行为不拦截,只报告拦截违规行为
适用场景测试和调试阶段生产环境
配置指令Content-Security-Policy-Report-OnlyContent-Security-Policy
上报机制通过 report-urireport-to 上报无上报机制(直接拦截)

5. report-urireport-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-urireport-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 中排除 nullundefined

  • 示例

    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>

  • 作用:获取函数类型 Tthis 参数类型。

  • 示例

    function greet(this: { name: string }) {
      console.log(`Hello, ${this.name}!`);
    }
    
    type GreetThis = ThisParameterType<typeof greet>;
    // 等价于:
    // type GreetThis = { name: string }
    

15. OmitThisParameter<T>

  • 作用:移除函数类型 Tthis 参数类型。

  • 示例

    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 中排除 nullundefined
ReturnType<T>获取函数类型 T 的返回值类型。
InstanceType<T>获取构造函数类型 T 的实例类型。
Parameters<T>获取函数类型 T 的参数类型元组。
ConstructorParameters<T>获取构造函数类型 T 的参数类型元组。
ThisParameterType<T>获取函数类型 Tthis 参数类型。
OmitThisParameter<T>移除函数类型 Tthis 参数类型。
Awaited<T>获取 Promise 类型 T 的解析值类型。

总结

这些题目就是前几天晚上腾讯面试的前端面试题了,面完我已经瘫了,悬着的心终于死了,还有有很多不足的地方,还是需要好好复习,努力总结,好好沉淀。