共 84 道 场景题 面试题。答案默认折叠,便于先自行作答。

1. 现在大模型已经能高效生成界面代码,有人认为前端会被AI颠覆,也有人认为这只是工具革新。你更倾向哪种观点?你认为前端未来3-5年真正的机会点或转型方向是什么?

难度:3 · 类型:QA

题目要点

更倾向于“工具革新”而非“彻底颠覆”;大模型替代的是低层实现工作,而前端正在向工程化、系统设计以及 AI 应用方向演进,未来机会集中在高阶工程能力与“前端 + AI”结合的领域。

参考答案

更倾向于“工具革新”的判断,但需要补充一个前提:被替代的不是前端岗位,而是低抽象层的前端工作。大模型把“写代码”这一步极大压缩,但没有消解前端在系统中的职责。

从本质上看,前端的价值不在于把设计稿转成代码,而在于如何在浏览器这个受限运行时中构建复杂系统并保证体验、性能与稳定性。大模型擅长生成局部正确的代码,但在跨模块一致性、状态复杂性、长期可维护性等方面仍然依赖工程设计能力。


一、为什么更接近“工具革新”

大模型改变的是生产方式,而不是问题本身:

  • 代码生成 → 自动化:组件、页面、样板代码的产出成本趋近于零
  • 问题复杂度 → 上移:从“怎么写”转为“怎么设计、怎么约束、怎么演进”
  • 人机协作 → 常态化:开发流程变成“设计 + 约束 + 校验 + 修正”

这和当年从 jQuery 到框架、再到工程化工具的演进类似,本质是抽象层不断上移。


二、未来 3–5 年的核心变化

可以用一句话概括:前端从“实现层”走向“系统层 + AI 协同层”

1. 前端工程能力会被放大,而不是削弱

大模型生成的代码,如果没有约束,很容易出现:

  • 状态混乱(状态源不清晰)
  • 性能问题(重复渲染、无效请求)
  • 架构失控(组件边界混乱)

因此需要更强的工程能力去“约束 AI”:

  • 设计组件规范、状态模型、数据流
  • 统一代码风格与约束(lint / schema / contract)
  • 构建可持续演进的架构

本质上:人负责“系统正确性”,AI 负责“实现效率”


2. “前端 + AI”的应用能力成为新分水岭

未来不是“用 AI 写代码”,而是“用前端做 AI 产品”:

  • Prompt 设计与上下文管理
  • 流式响应(SSE / streaming UI)
  • Agent 工作流编排(多步骤任务)
  • 多模态交互(文本 / 图像 / 语音)

前端在这里的角色是:AI 能力的产品化承载层


3. 复杂交互与可视化价值上升

大模型擅长生成静态 UI,但对复杂交互的掌控仍有限:

  • 可视化(图表、数据大屏、编辑器)
  • 拖拽、画布、低代码平台
  • 高交互密度场景(设计工具、IDE 类产品)

这些场景需要:

  • 精细状态管理
  • 渲染性能优化(canvas / WebGL / 虚拟化)
  • 交互细节控制

这是短期内最难被替代的部分。


4. BFF 与全栈能力会成为标配

前端边界正在上移:

  • 接入 AI 服务(需要后端编排)
  • 数据聚合与接口适配(BFF 层)
  • 权限、缓存、边缘计算

未来前端更像:

“以浏览器为核心的全栈工程师”


5. “约束 AI”的能力比“写代码”更重要

未来的核心能力不是写函数,而是:

  • 如何定义清晰的接口与数据结构
  • 如何拆解问题给 AI(任务建模)
  • 如何验证 AI 输出(测试 / 类型 /监控)

本质上是从“coding”转向“spec + orchestration”。


三、值得投入的方向(优先级建议)

如果从职业发展角度看,可以重点关注:

第一优先级(长期壁垒)

  • 工程化体系(构建、性能、监控、稳定性)
  • 前端架构设计(组件体系、状态管理、微前端)

第二优先级(增长点)

  • AI 应用开发(Agent、SSE、上下文管理)
  • BFF / Node 中间层

第三优先级(差异化)

  • 可视化(ECharts / WebGL / Canvas)
  • 低代码 / 编辑器类系统

结论

这不是一次“岗位消失”,而是一次“能力重构”:

  • 低阶实现能力被快速替代
  • 中高阶工程与架构能力被放大
  • AI 应用能力成为新的增量空间

能持续发展的前端,通常具备三点:工程能力、系统设计能力、AI 协同能力

2. 项目中是否有必要自己封装请求库?

难度:2 · 类型:QA

题目要点

自建请求库的核心目的是统一协议规范、集中处理异常与鉴权、封装重试与缓存等增强能力、接入监控体系以及屏蔽多端差异;它是工程治理与基础设施建设行为,而非简单的功能封装。

参考答案

是否需要自建请求库,本质上不是技术炫技,而是工程治理问题。

在业务规模较小阶段,直接使用原生 fetchXMLHttpRequest 已经足够;但在中大型项目中,请求逐渐成为系统的基础设施。如果每个业务模块各自处理鉴权、错误提示、重试逻辑和埋点,上层代码会迅速失控。

自建请求库的核心价值在于统一规范与横切能力收敛

第一是统一协议层。包括基础 URL、鉴权 token 注入、公共 headers、参数序列化规则,以及接口返回格式的统一解构。没有统一抽象,接口风格一旦变化会导致大面积修改。

第二是统一异常处理。比如 401 刷新 token、403 权限跳转、接口错误码映射、全局错误提示等,这些逻辑属于横切关注点,分散在业务中会导致重复代码与维护成本上升。

第三是增强能力封装。包括请求取消、超时控制、重试策略、并发控制、节流合并、缓存策略、弱网兜底等。这些能力如果不抽象,很难在系统层面治理接口质量。

第四是可观测性建设。请求库可以统一接入监控系统,例如接入 Sentry 或其他 APM 平台,实现接口耗时、错误率、慢接口统计。若不集中封装,很难形成完整链路追踪。

第五是环境适配。Web、SSR、小程序等环境对请求实现不同,例如浏览器使用 fetch,小程序使用平台 API,通过统一抽象可以屏蔽底层差异。

当然,自建请求库不等于重复造轮子。通常是在成熟库(如 axios 或原生 fetch)之上做二次封装,而不是完全重写网络层。

从架构角度看,请求库属于“基础设施层”,它的作用是将横切逻辑从业务代码中剥离,提高一致性与可维护性,而不是单纯为了替代现有 API。

3. 如果在项目中需要使用到动态表单,你会怎么实现?

难度:4 · 类型:QA

题目要点

动态表单的设计核心在于将结构、状态与行为解耦。通过 Schema 描述字段结构,通过集中式 Form Store 管理表单状态,通过依赖或表达式机制实现字段联动,从而构建一个可配置、可扩展、可维护的表单系统。本质是将表单抽象为一个状态驱动的响应式系统,而不是简单的配置渲染。

参考答案

这个问题如果只是回答“根据配置渲染组件”,深度是不够的。

动态表单真正考察的是:如何把表单从“页面结构”抽象为“可驱动的状态机”

动态表单的核心不是 UI 渲染,而是三层解耦:

  • 表单结构描述
  • 表单状态管理
  • 表单行为控制

如果这三层没有拆开,项目后期一定会失控。


一、第一层:结构抽象 —— Schema 驱动

动态表单通常基于 Schema 描述结构,而不是写死 JSX / template。

一个最基础的 schema 可能是:

{
  name: "username",
  label: "用户名",
  component: "Input",
  props: { placeholder: "请输入用户名" },
  rules: [{ required: true }]
}

这个 Schema 本质上是“表单 DSL”。

关键设计点在于:

  1. 字段标识必须唯一(通常使用 name 作为路径)
  2. 组件类型与具体 UI 框架解耦
  3. 校验规则抽象为规则对象
  4. 支持嵌套结构(数组、对象)

复杂场景下,Schema 会支持:

  • 条件渲染
  • 动态枚举数据源
  • 表单联动表达式
  • 权限控制

二、第二层:状态管理 —— 数据中心化

动态表单的难点不在渲染,而在状态。

表单至少包含:

  • 当前值(values)
  • 校验状态(errors)
  • 触发状态(touched)
  • 可见性(visible)
  • 禁用状态(disabled)

如果每个字段自己维护状态,联动逻辑会非常混乱。

正确方式是:

所有字段状态集中存储在一个 Form Store 中。

可以类比 Redux / Zustand 的设计思想。

当字段值变化:

  1. 更新 Store
  2. 触发依赖计算
  3. 重新派发状态给相关字段

例如在 React 体系中,可以通过 context + reducer 实现。

成熟方案如:

  • Formily
  • React Hook Form

本质都在做一件事:构建一个可订阅的表单状态容器。


三、第三层:行为驱动 —— 联动与副作用

真正复杂的是字段联动。

例如:

  • 选择国家后,城市下拉框变化
  • 勾选复选框后,显示额外字段
  • 金额变化触发计算字段

常见实现方式有三种:

1. 命令式联动

直接写监听逻辑:

if (values.type === "company") {
  showField("taxId");
}

问题是强耦合,维护成本高。


2. 声明式表达式驱动

在 schema 中写规则:

{
  visible: "{{ $values.type === 'company' }}"
}

然后在渲染时解析表达式。

这种方式更可扩展,但需要表达式解析引擎。


3. 依赖收集机制

高级方案会建立字段依赖图。

字段 A 依赖字段 B 当 B 更新时,只通知 A 重新计算。

这本质是一个响应式系统。

例如:

  • Vue 的依赖收集机制
  • MobX 的 observable

如果项目规模较大,建议构建“字段依赖图”,否则复杂联动会退化为大量手写监听。


四、动态表单的几个关键设计点

1. 渲染层必须可替换

Schema 只描述结构,不应绑定具体 UI 库。

否则换组件库时成本巨大。


2. 支持异步数据源

例如下拉选项来自接口。

必须设计:

  • loading 状态
  • 请求缓存
  • 错误处理

3. 支持嵌套与数组字段

例如:

  • 表单中嵌套子表单
  • 动态增删行

这通常需要路径解析能力,例如:

user.address.city
users[0].name

4. 性能优化

大型表单常见问题是:

  • 全量重渲染
  • 每次输入触发整个表单更新

优化思路:

  • 字段级订阅
  • React.memo
  • 局部更新

否则 100+ 字段的表单体验会明显卡顿。


五、从工程视角总结设计逻辑

一个成熟的动态表单系统通常遵循:

  1. Schema 描述结构(配置驱动)
  2. 中央 Store 管理状态(状态驱动)
  3. 依赖系统驱动联动(响应式驱动)
  4. 渲染层与逻辑层解耦(可扩展)

如果缺少其中任意一层,系统复杂度会指数级上升。

4. 如果页面含 100 个第三方资源,你会如何处理?

难度:2 · 类型:QA

题目要点

当页面包含大量第三方资源时,应优先进行资源治理与分级,而不是一次性加载;通过延迟加载、按需加载和统一的资源管理层,避免首屏阻塞和重复引入;所有第三方脚本都应异步、非阻塞,并在必要时通过 iframe 或异常兜底进行风险隔离;最后结合缓存与监控机制,持续评估资源价值,防止第三方依赖失控增长。

参考答案

当一个页面同时引入约 100 个第三方资源时,问题的核心并不是“能否加载”,而是如何在不牺牲稳定性与首屏体验的前提下,控制风险、降低成本并保持可演进性。处理思路应从“识别—治理—调度—隔离—监控”几个层面展开。


一、先做治理,而不是盲目优化

第一步并不是技术手段,而是资源治理

在真实项目中,第三方资源通常来源复杂:统计、监控、广告、客服、A/B、埋点、富文本、地图、支付等。需要先做的是:

  • 明确每个资源的业务价值、使用频率和关键路径依赖
  • 区分强依赖(页面不可用)弱依赖(功能增强)
  • 标记是否影响首屏、是否阻塞渲染、是否可异步

只有完成这一步,后续的优化策略才有依据。


二、分级加载,避免首屏被“拖死”

100 个资源不可能同时进入首屏加载,应当进行加载分级

核心原则是:

  • 首屏只加载必要资源
  • 非关键资源全部延迟或条件加载

常见策略包括:

  • 关键路径资源使用同步或优先级较高的加载方式
  • 统计、监控、客服等统一延后到 DOMContentLoadedload
  • 低频功能按路由、按交互触发再加载

这样可以显著降低首屏的网络拥塞和解析压力。


三、统一资源调度,而不是散落在各处

当第三方资源达到数量级之后,最大的问题是不可控

工程上应避免:

  • 在业务代码中直接插 <script>
  • 各个模块各自加载 SDK

更合理的方式是:

  • 抽象一个第三方资源管理层
  • 统一做加载、缓存、去重和失败兜底
  • 对外暴露“能力接口”,而不是暴露脚本细节

这样做的好处是:

  • 防止重复加载同一 SDK
  • 可以集中做异常兜底
  • 可以随时替换或下线某个资源

四、尽量异步化与非阻塞化

对第三方资源的一个基本要求是:

不能阻塞 HTML 解析和主线程

常见手段包括:

  • 使用 async / defer 加载脚本
  • 动态 import() 或运行时插入 script
  • 将初始化逻辑拆分,避免脚本加载即执行重逻辑

对于不受控的第三方脚本,必须假设:

  • 它可能慢
  • 它可能抛异常
  • 它可能污染全局

因此要在加载方式上尽量降低侵入性。


五、隔离风险,防止“一个资源拖垮整个页面”

当第三方资源数量很多时,稳定性优先级高于性能

常见的隔离策略包括:

  • iframe 隔离高风险脚本(如广告、复杂编辑器)
  • try-catch 包裹初始化逻辑,避免异常冒泡
  • 对全局对象做存在性检测,防止未加载即调用

核心目标是:

任何一个第三方资源失败,都不应影响主流程


六、网络与缓存层面的优化

在网络层,可以进一步降低成本:

  • 尽量通过 CDN 加速第三方资源
  • 能本地托管的资源尽量本地托管,减少外部依赖
  • 合理利用浏览器缓存,避免重复下载

对于稳定不变的 SDK,优先选择:

  • 长缓存策略
  • 版本化文件名

七、持续监控与动态治理

当页面依赖大量第三方资源时,必须配套监控机制

  • 资源加载成功率
  • 加载耗时
  • 初始化失败率
  • 对首屏和交互的影响

通过监控数据,可以:

  • 定期下线低价值资源
  • 替换性能或稳定性差的 SDK
  • 动态调整加载时机

否则第三方资源会不断“只增不减”。

5. 在前端项目中,如何设计前端与后端的数据交互机制?请说明数据传输格式、接口设计原则以及如何优化性能。

难度:2 · 类型:QA

题目要点

前端与后端的数据交互需要从格式、契约和性能三个层面整体设计;数据传输通常以 JSON 为主,必要时引入二进制协议;接口设计强调资源化、稳定契约和统一错误语义;通过缓存、请求合并、字段裁剪和时机控制优化性能;最终目标是在需求演进和复杂场景下,依然保持交互机制的清晰、稳定和高效。

参考答案

在前端工程中,前后端的数据交互并不是简单的“发请求、收数据”,而是一个围绕可演进性、稳定性和性能的系统设计问题。一个良好的交互机制,能够在需求变化、并发提升和链路变复杂时,仍然保持可控。


一、数据传输格式的设计

1. 主流格式选择

在大多数业务系统中,JSON 仍然是默认且最优的选择

  • 语义清晰,易于调试
  • 与前端对象模型天然匹配
  • 工具链成熟,日志与监控友好

在对性能或带宽要求更高的场景,可以引入:

  • Protobuf / MessagePack 等二进制协议,用于高频、结构稳定的接口
  • 在前端侧通常封装为统一解码层,避免侵入业务代码

2. 字段设计与稳定性

数据结构设计时,需要优先考虑向前和向后兼容

  • 字段只增不删,废弃字段通过版本或标记处理
  • 明确区分必填字段与可选字段
  • 避免在接口中返回强业务语义的拼装文案,而是返回结构化数据

这样可以减少前后端强耦合带来的协作成本。


二、接口设计原则

1. 以资源为中心,而不是以页面为中心

接口应围绕“数据模型”设计,而不是围绕某个具体页面:

  • 接口语义稳定,可被多个页面或终端复用
  • 避免一个页面一个接口的短视设计

2. 明确请求与响应的契约

一个健壮的接口应明确以下内容:

  • 请求参数的类型、约束和默认值
  • 响应数据的结构和状态含义
  • 错误码与异常场景的可预期行为

在前端侧,通常通过 TypeScript 类型或 schema 约束来固化这一契约。


3. 错误与状态的统一规范

推荐将接口响应拆分为三层含义:

  • 传输层是否成功(HTTP 状态码)
  • 业务是否成功(业务 code)
  • 数据本身

这样前端可以清晰地区分网络问题、权限问题和业务异常,避免大量条件分支散落在业务代码中。


三、前端侧的数据交互架构

1. 请求层与业务层解耦

前端通常会抽象出统一的请求模块,负责:

  • 请求封装与参数序列化
  • 通用错误处理
  • 鉴权、重试、超时控制

业务层只关心“获取什么数据”,而不关心“如何请求”。


2. 数据缓存与状态管理

并非所有数据都需要每次重新请求:

  • 列表与配置类数据适合缓存
  • 可通过内存缓存、请求去重或状态管理工具实现
  • 对于读多写少的数据,可以引入 stale-while-revalidate 模式

四、性能优化策略

1. 减少请求数量

  • 接口合并或批量接口
  • 合理利用后端聚合接口
  • 页面级数据预加载

避免过度细粒度的接口拆分,导致请求瀑布。


2. 减少单次请求体积

  • 精简返回字段,避免返回前端不使用的数据
  • 支持字段裁剪(如 fields 参数)
  • 使用压缩(gzip / br)

3. 控制请求时机

  • 懒加载与按需请求
  • 用户可感知操作优先,其余请求延后
  • 对高频请求做防抖、节流或合并

4. 并发与顺序控制

  • 明确哪些请求可以并发,哪些存在依赖
  • 使用取消机制避免无效请求占用资源
  • 页面切换时主动中断未完成请求

五、在演进中的可维护性考虑

1. 接口版本与灰度

  • 接口通过版本号或能力字段演进
  • 前端可根据能力判断是否启用新逻辑
  • 避免一次性破坏式升级

2. 可观测性

  • 请求耗时、错误率可监控
  • 关键接口具备埋点与日志
  • 前端异常可与后端日志关联定位

6. 在即时通讯场景中,在同步消息时,如果出现网络延迟或部分设备未在线,你会如何确保消息的最终一致性?

难度:3.5 · 类型:QA

题目要点

即时通讯中的最终一致性依赖于服务端权威、全局唯一标识和 ACK 机制;实时推送解决实时性,增量拉取作为兜底补偿;离线和多设备通过游标同步和幂等合并收敛状态;前端允许短暂不一致,但始终以服务端事件流为最终结果,从而在网络不稳定和设备切换的情况下保证消息最终一致。

参考答案

在即时通讯场景下,网络延迟和设备离线是常态,而不是异常。因此消息系统的目标并不是“强实时一致”,而是在允许短暂不一致的前提下,保证最终一致性。前端在其中承担的是“有序消费与状态收敛”的角色,而不是一致性的裁判。


一、一致性的基本原则

1. 服务端作为唯一事实源(Single Source of Truth)

所有消息内容和状态的最终结果,必须以服务端为准,包括:

  • 消息是否成功送达
  • 消息顺序
  • 已读、撤回等状态

前端任何本地状态都只能被视为临时视图,需要随时被服务端事件修正。


二、消息同步的核心机制

1. 全局唯一标识与顺序保证

  • 每条消息由服务端分配全局唯一 messageId 或单调递增序号
  • 前端以此作为去重、排序和补偿的基础
  • 即使消息乱序到达,也能在数据层重排

这一步是最终一致性的前提条件。


2. ACK + 重传机制

发送与接收过程需要明确的确认语义:

  • 发送方在本地插入临时消息
  • 服务端返回 ACK(确认消息已持久化)
  • 若在超时时间内未收到 ACK,前端进入重试或失败状态
  • 重试以 messageId / clientId 去重,避免重复消息

ACK 机制确保“至少一次送达”,去重机制保证“最多一次展示”。


3. 增量拉取作为兜底路径

实时推送无法覆盖所有异常场景,因此必须存在拉取补偿机制

  • 前端维护最后确认的同步游标

  • 在以下场景触发拉取:

    • 网络重连
    • 页面刷新
    • 设备重新上线
  • 拉取从游标之后的消息,补齐缺失部分

只要拉取接口可靠,最终一致性就可以得到保证。


三、多设备与离线场景的处理

1. 离线设备的消息处理

  • 离线期间,消息在服务端持久化
  • 设备上线后,通过游标或时间戳拉取历史消息
  • 拉取结果与本地缓存合并,按 messageId 去重

前端不需要感知“设备离线多久”,只关心“最后同步到哪里”。


2. 多设备同时在线的状态收敛

例如已读状态:

  • 已读作为一种“状态事件”同步
  • 以时间戳或版本号判断新旧
  • 本地较旧的状态会被覆盖

通过“最后写入生效”的策略,避免设备间反复拉扯状态。


四、网络延迟与乱序的应对

1. 乱序到达

  • 消息按服务端序号排序
  • UI 层允许短暂乱序
  • 数据层最终保持有序

2. 延迟 ACK

  • 前端保持 pending 状态
  • 不因短时间未确认而回滚 UI
  • 超时后才触发重试或失败提示

五、工程层面的关键约束

1. 所有状态变化都可重放

消息、已读、撤回等都应被设计为事件流,而不是直接修改最终状态。这使得在重连或补偿时,可以通过事件回放恢复一致状态。


2. 幂等性设计

  • 同一消息或状态事件可被多次处理
  • 多次处理结果相同
  • 前端以 messageId / eventId 做幂等判断

这是最终一致性能够成立的技术基础。


六、前端在最终一致性中的角色定位

前端的职责不是“保证绝对正确”,而是:

  • 快速响应用户操作
  • 在不确定阶段给出合理中间态
  • 在服务端数据到达时主动收敛到正确状态

7. 开发一个即时通讯系统的前端页面时,你会如何设计消息的存储和同步机制?如何处理用户切换设备后的消息同步问题?

难度:3 · 类型:QA

题目要点

即时通讯前端的消息系统需要分层设计存储与同步机制;通过 messageId、游标和增量拉取保证消息顺序与一致性;实时连接负责推送,拉取机制负责补偿;跨设备登录时以服务端为权威,通过同步位点恢复消息状态;本地缓存只作为性能优化手段,而不是一致性来源。

参考答案

在即时通讯系统中,前端的核心挑战不在于“如何把消息渲染出来”,而在于如何在不可靠网络、跨设备登录以及高频更新的前提下,保证消息的一致性、顺序性和可恢复性

设计时需要把消息系统当成一个“有状态的分布式系统前端”,而不是普通列表页。


一、消息存储模型的设计

1. 消息的基础结构

每条消息在前端都应具备明确的“身份”和“生命周期”信息,而不仅是文本内容,典型字段包括:

  • 全局唯一的 messageId(由服务端生成)
  • 会话 id(单聊 / 群聊)
  • 发送方、时间戳
  • 消息类型与 payload
  • 本地状态(sending / sent / failed / read)

其中,messageId 与时间戳是顺序和去重的基础,前端不应依赖数组索引或本地生成的自增 id。


2. 本地存储分层

前端通常采用内存 + 持久化的双层结构:

  • 内存层: 用于当前会话渲染和高频更新,通常由状态管理工具维护,强调响应速度。
  • 持久化层: 使用 IndexedDB 或本地数据库,存储最近消息和会话元数据,用于刷新、重进页面或弱网恢复。

两层之间通过 messageId 进行同步,而不是通过 UI 状态直接耦合。


二、实时同步机制

1. 消息接收

  • 与服务端保持长连接(WebSocket 或类似机制)
  • 服务端推送消息事件
  • 前端按 messageId 去重后写入内存和本地存储
  • 按时间戳或服务端序号插入正确位置

这里需要强调: 前端不假设消息一定按顺序到达,而是显式做排序或位置校验。


2. 消息发送与确认

发送流程通常分为两步:

  1. 本地先插入一条“临时消息”(clientId)
  2. 服务端返回确认(server messageId)
  3. 用 server messageId 替换本地临时标识

这样可以保证输入即显现,同时避免因网络延迟影响体验。


三、增量拉取与断点续传

1. 游标(cursor)机制

前端需要维护一个已同步到的最大位置标识,例如:

  • 最大 messageId
  • 或最后一条已确认消息的时间戳

当出现以下情况时触发拉取:

  • 初次进入会话
  • WebSocket 断线重连
  • 页面刷新

请求形式通常是: “拉取从 cursor 之后的所有消息”。


2. 去重与合并策略

  • 以 messageId 作为唯一键
  • 本地已有的消息直接忽略
  • 状态不同步时,以服务端状态为准

四、跨设备登录与消息同步

这是即时通讯系统中最关键、也是最容易出问题的部分。

1. 多设备一致性的基本原则

  • 服务端是最终一致性的裁判
  • 前端不做“全量信任本地缓存”的假设
  • 本地存储只作为加速层

2. 新设备登录流程

当用户在新设备登录时,前端应:

  1. 获取账号级的最新同步位点(lastSyncCursor)
  2. 执行一次增量或分页拉取
  3. 建立实时连接
  4. 后续通过推送保持同步

如果是首次登录或长时间未登录,应支持分批拉取历史消息,避免一次性加载造成性能问题。


3. 已读状态与设备间同步

已读、撤回、删除等操作,本质上是消息的状态变更事件

  • 统一走事件流
  • 前端根据事件更新本地消息状态
  • 不直接依赖 UI 操作结果作为最终状态

五、异常与边界处理

1. 离线与弱网

  • 消息进入发送队列
  • 标记为 pending 状态
  • 网络恢复后自动重试
  • 服务端确认后更新状态

2. 顺序错乱与补偿

  • 接收到“未来消息”时,允许暂存
  • 后续通过拉取补齐中间消息
  • UI 层可允许短暂乱序,但数据层保持有序

六、工程层面的组织方式

  • 会话列表与消息列表分离
  • 消息状态与 UI 状态解耦
  • 所有同步行为都有明确的触发源(push / pull / retry)

8. 有一个需要支持10万条数据渲染的表格组件,你会如何设计和优化?

难度:3.5 · 类型:QA

题目要点

10 万条数据表格的核心是控制渲染规模;必须基于行(必要时加列)虚拟化,将 DOM 数量限制在可视区范围;数据处理与渲染解耦,重计算放在数据层甚至 Worker 中;通过稳定 key、行级 memo 和简化布局,确保滚动和交互始终流畅。

参考答案

问题的本质已经是如何控制渲染规模、更新成本和交互响应时间

设计时应当从数据层、渲染层和交互层协同入手,而不是寄希望于某一个单点优化。


一、整体设计原则

核心原则只有一个:任何时刻参与渲染和布局计算的 DOM 数量必须是可控的。 10 万条数据不可能全部进入真实 DOM,否则无论是首屏渲染、滚动还是重排都会不可接受。

因此,设计目标通常包括:

  • 首屏渲染在毫秒级完成
  • 滚动过程中不触发布局抖动
  • 排序、筛选等操作不阻塞主线程
  • 单行更新不导致整表重渲染

二、渲染层:虚拟化是前提,不是优化项

1. 行虚拟化(Windowing)

表格只渲染视口内 + buffer 区域的行,其余数据只存在于内存中。

基本思路是:

  • 通过 scrollTop 计算当前可见的起始索引和结束索引
  • 只渲染这一区间的数据
  • 用一个占位容器(totalHeight)模拟完整滚动高度

这一层可以自行实现,也可以基于成熟方案,例如 react-window / react-virtualized 的核心思想。

2. 固定高度 vs 动态高度

  • 固定行高:计算简单,性能最好,适合大多数业务表格
  • 动态行高:需要行高缓存 + 二分查找,复杂度明显上升,应尽量避免

在 10 万量级下,优先设计为固定行高。


三、列与布局层面的优化

1. 列虚拟化(必要时)

当列数非常多(例如 50+)时,需要同时做横向虚拟化,否则单行 DOM 过多同样会拖慢布局和绘制。

2. 减少布局复杂度

  • 避免 table 原生布局计算,优先使用 div + flex / grid
  • 避免每个单元格内存在复杂嵌套结构
  • 能静态的样式不依赖 JS 计算

四、数据与计算层优化

1. 排序、过滤、搜索不直接作用于渲染层

这些操作应当:

  • 在数据层完成
  • 返回新的数据引用
  • 再交由虚拟列表渲染

对于复杂规则:

  • 使用 Web Worker 做计算
  • 主线程只负责渲染结果

2. 结构共享与不可变数据

  • 行数据保持稳定引用
  • 未变更的行不重新创建对象
  • 配合 React.memo / useCallback,保证单行更新不扩散

五、React 组件级别的控制

1. 行级组件隔离

每一行都是独立组件,且具备明确的 memo 边界:

  • 行只依赖当前行数据
  • 列配置尽量外提,避免每次 render 生成新对象

2. key 的稳定性

  • key 必须来自稳定主键
  • 禁止使用 index 否则在虚拟化场景下会导致大量不必要的卸载与重建。

六、滚动与交互体验优化

1. 滚动事件处理

  • 使用容器 scroll,而非 window scroll
  • 通过 requestAnimationFrame 或同步计算,避免额外 setState 抖动

2. 选中、高亮等状态设计

  • 状态尽量用 Set / Map 结构存储
  • 判断逻辑在行内部完成
  • 避免将大规模选中状态直接挂在表格根组件

七、进一步的工程级优化(高阶)

  • 首屏分批渲染(Idle Callback)
  • 非关键列延迟挂载
  • 使用 Canvas 渲染只读表格(极端场景)
  • SSR 场景下只输出首屏数据

八、设计取舍与边界说明

需要明确的是: 支持 10 万条数据并不等于一次性操作 10 万条 DOM

合理的设计,是让用户“感觉在操作 10 万条数据”,而系统实际只处理几十到几百行的渲染工作。

9. 如果需要做图片懒加载,并且图片数量很多,你会怎么做呢?

难度:3 · 类型:QA

题目要点

大量图片懒加载应优先使用 IntersectionObserver 作为触发机制;图片极多时需结合虚拟列表控制 DOM 数量;通过占位图、响应式图片和合适格式降低单图加载成本;同时做好兼容和资源清理,确保整体性能和稳定性。

参考答案

在图片数量很多的场景下,懒加载的目标不只是“晚一点加载”,而是在保证首屏体验的前提下,把网络、解码和渲染成本控制在可预测范围内。设计时通常需要从加载触发机制、资源策略以及工程层面的兜底方案三个层次来考虑。

首先,在加载触发机制上,IntersectionObserver 是首选方案。它由浏览器在渲染管线中统一调度,避免了大量 scroll 监听和同步布局计算。当图片元素即将进入视口时,再将真实图片地址从占位属性写回 src,可以做到按需、精准加载。对于图片非常密集的页面,通常会设置一个正向的 rootMargin,例如提前 200~300px 触发加载,保证用户滚动到图片位置时资源已经完成下载和解码,从而避免白屏或抖动。

当图片数量达到几百甚至上千时,仅靠懒加载还不够,需要配合虚拟列表(windowing)。思路是只在 DOM 中保留视口附近的图片节点,其余图片直接卸载,避免 DOM 数量过大导致的布局和内存压力。这在瀑布流、相册、长列表中非常关键,常见做法是将 IntersectionObserver 与虚拟滚动结合:虚拟列表控制节点数量,Observer 只负责触发资源加载。

在资源策略上,需要进一步降低单张图片的成本。通常会为懒加载配合 低质量占位图(LQIP)或模糊占位,例如先加载一个极小的 base64 或 WebP 预览图,再在真实图片加载完成后无缝替换。同时,应当利用现代图片能力,根据设备和网络条件选择合适格式和尺寸,例如通过 srcset / sizes 或后端图片服务,避免在移动端加载过大的原图。对关键图片(首屏、即将进入视口的图片)可以适度提高优先级,其余图片保持低优先级下载。

工程层面还需要考虑兜底和兼容性。对于不支持 IntersectionObserver 的环境,可以退化为节流后的 scroll 方案,但应限制监听逻辑的复杂度,避免在每次滚动中遍历大量节点。另外,需要处理图片加载失败、重复触发加载以及组件卸载后的 observer 清理问题,防止内存泄漏和无效请求。

综合来看,一个可扩展的方案通常是:虚拟列表控制数量 + IntersectionObserver 精准触发 + 响应式图片与占位策略降低单图成本。这样既能保证滚动流畅,也能在图片规模很大时保持稳定的性能表现。

10. 怎么根据当前滚动位置,实现目录中对应节点自动高亮?

难度:3 · 类型:QA

题目要点

目录自动高亮的本质是监听正文滚动位置,判断当前阅读的标题节点。我通常优先使用 IntersectionObserver 监听标题进入视口,在性能和实现复杂度之间取得最佳平衡,同时配合锚点滚动和状态同步,保证目录与正文的双向联动。

参考答案

“目录中自动高亮”(TOC Active)本质是 根据当前滚动位置,判断用户正在阅读的标题节点,并同步更新目录状态。这是文档类产品、Markdown 阅读器中非常常见的能力。

下面从 设计思路 → 常见实现方案 → 工程细节与坑 三个层次说明。


一、核心设计思路

滚动驱动状态,状态驱动目录高亮

关键问题只有两个:

  1. 如何判断当前“激活”的标题?
  2. 如何高效监听滚动并更新状态?

二、实现方案一(推荐):IntersectionObserver

适用场景

  • 现代浏览器
  • 标题是标准 DOM(h1–h6)

实现思路

  • 监听所有标题元素
  • 哪个标题进入视口上方区域,即认为是当前阅读位置

基本实现示例

const headings = document.querySelectorAll('h1, h2, h3');

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        setActiveId(entry.target.id);
      }
    });
  },
  {
    rootMargin: '0px 0px -70% 0px',
    threshold: 0
  }
);

headings.forEach(h => observer.observe(h));

原理说明

  • rootMargin: -70%:让标题进入页面上半部分就触发
  • isIntersecting 表示元素进入“有效阅读区”
  • activeId 用于目录高亮

优点

  • 性能好(浏览器原生)
  • 不依赖 scroll 事件
  • 不需要手动计算高度

缺点

  • 老浏览器需 polyfill

三、实现方案二:scroll + getBoundingClientRect(传统方案)

实现思路

  1. 监听 scroll
  2. 找到距离顶部最近但未超出的标题

示例代码

const headings = [...document.querySelectorAll('h1,h2,h3')];

window.addEventListener('scroll', () => {
  let active = null;

  for (const h of headings) {
    const { top } = h.getBoundingClientRect();
    if (top <= 100) {
      active = h;
    } else {
      break;
    }
  }

  if (active) {
    setActiveId(active.id);
  }
});

优化点

  • requestAnimationFrame 节流
  • 提前缓存 headings
  • 滚动事件加 passive

优缺点

优点缺点
兼容性好性能较差
易理解手写逻辑多

四、实现方案三:基于文档模型(编辑器场景)

如果你是:

  • 使用 markdown-it
  • Slate / ProseMirror

可以直接从 AST / Node 结构获取标题位置

思路

  • 渲染时记录 headingId → offsetTop
  • 滚动时用 scrollTop + 二分查找

五、目录点击与滚动同步(反向联动)

点击目录 → 滚动正文

function scrollToHeading(id) {
  document.getElementById(id)?.scrollIntoView({
    behavior: 'smooth',
    block: 'start'
  });
}

注意点

  • 固定头部高度(navbar)
window.scrollTo({
  top: el.offsetTop - headerHeight,
  behavior: 'smooth'
});

六、工程级细节 & 常见坑

多个标题同时可见怎么办?

规则:

  • 取「最靠上的一个」
  • 或 IntersectionObserver 中按 boundingClientRect.top 排序

页面初始加载未触发 scroll?

  • 首次执行一次计算
  • 或 IntersectionObserver 自动触发

高亮抖动?

  • 增加缓冲区(rootMargin)
  • 或 debounce 状态更新

锚点重复?

  • 渲染 markdown 时生成 唯一 id
  • title + index 或 hash

七、React / Vue 实战建议

React

  • useEffect 注册 observer
  • activeId 放在 useState
  • 组件卸载时 disconnect

Vue

  • onMounted 注册
  • onUnmounted 清理
  • activeId 用 ref

八、推荐方案总结

场景推荐方案
普通文档IntersectionObserver
老浏览器scroll + rect
编辑器AST + offset

11. 如果需要完成一个协同文档的系统,你会如何进行架构设计呢

难度:4 · 类型:QA

题目要点

协同文档的核心在于并发一致性。

采用 CRDT 或 OT 保证多端编辑一致性,使用 WebSocket 实时同步操作,通过 快照 + 操作日志 做持久化,并在客户端做乐观更新、离线支持和光标协同,从而在一致性、性能和用户体验之间取得平衡。

参考答案

主要从 一致性模型、数据结构、同步协议、性能与扩展性 四个层面来设计。


一、总体架构分层

客户端(Web / App)

  • 文档编辑器(富文本 / 表格 / Markdown)
  • 本地状态管理(操作队列、版本号)
  • 冲突解决算法(OT / CRDT)
  • WebSocket 实时通信
  • 离线缓存与重连恢复

服务端

  • 协同引擎(OT / CRDT 处理)
  • 会话管理(在线用户、光标、选区)
  • 文档存储(快照 + 操作日志)
  • 权限与审计
  • 消息分发(广播变更)

存储层

  • 文档快照(Snapshot)
  • 操作日志(Op Log)
  • 历史版本 / 回滚

二、核心问题 1:并发编辑一致性

这是协同文档的技术核心

方案一:OT(Operational Transformation)

代表:Google Docs

思路

  • 所有编辑行为抽象成操作(insert / delete)
  • 服务端对并发操作进行 transform,保证顺序一致

优点

  • 算法成熟
  • 网络开销小

缺点

  • 实现复杂
  • 强依赖中心服务器
  • 对新类型文档扩展成本高

适合

  • 强中心化、文档模型相对稳定的系统

方案二:CRDT(Conflict-free Replicated Data Type)

代表:Yjs / Automerge / Figma

思路

  • 数据结构天然支持并发合并
  • 不需要复杂 transform
  • 最终一致性

优点

  • 天然支持离线
  • 无需中心化冲突解决
  • 客户端可直接合并

缺点

  • 数据结构复杂
  • 数据体积偏大
  • 实现成本高

适合

  • 多端、离线优先、弱中心化架构

👉 个人倾向:新项目优先 CRDT


三、核心问题 2:文档数据模型设计

文档抽象

Document {
  id
  content   // CRDT / AST / Delta
  version
  meta
}

操作抽象(以富文本为例)

Operation {
  type: insert | delete | format
  position
  value
  userId
  timestamp
}

存储策略

  • 快照 + 增量日志
  • 定期做 snapshot,避免回放全部操作
  • 支持版本回溯与历史对比

四、核心问题 3:实时同步机制

通信方式

  • WebSocket(主通道)
  • HTTP(初始化、历史数据)

同步流程(典型)

  1. 客户端编辑 → 生成操作
  2. 本地立即应用(Optimistic Update)
  3. 通过 WS 发送到服务端
  4. 服务端校验 / 合并
  5. 广播给其他客户端
  6. 客户端应用远端操作

重连 & 离线

  • 本地操作队列
  • 带版本号 / vector clock
  • 重连后补发 diff

五、核心问题 4:协同体验增强

光标与选区同步

  • 不进入文档主数据
  • 独立 channel 广播
  • 高频但可丢失

用户状态

  • 在线列表
  • 编辑中提示
  • 编辑锁(针对块级结构)

六、性能与扩展性设计

前端性能

  • 操作批量合并
  • 虚拟渲染(长文档)
  • diff 粒度控制(字符级 vs 块级)

后端扩展

  • 文档级 shard(同一文档落到同一实例)
  • Redis / 内存存活会话
  • Kafka / MQ 做变更广播

大文档优化

  • 分段 CRDT
  • 懒加载文档块
  • 局部同步

七、安全与权限

  • 文档级 / 块级权限
  • 操作校验(只读用户禁止 op)
  • 操作审计日志
  • 防止恶意刷操作

八、简化版技术选型示例

层级技术
编辑器Slate / ProseMirror
协同算法Yjs(CRDT)
通信WebSocket
后端Node.js / Go
存储PostgreSQL + Redis
消息Redis Pub/Sub / Kafka

12. 平常你是如何进行代码调试的?

难度:1 · 类型:QA

题目要点

  • 工具组合:灵活切换浏览器断点、编辑器调试和第三方抓包工具。
  • 性能视角:不只关注报错,更通过 Performance 和 Memory 分析运行质量。
  • 链路追踪:利用 Source Map 还原源码,利用框架插件追踪状态流转。
  • 环境覆盖:针对移动端真机和线上环境,建立专项的监控与代理方案。
参考答案

调试代码早已不再局限于简单的 console.log。在复杂的工程环境下,调试是一套由表及里、从工具链到运行机制的综合诊断过程。

1. 浏览器开发者工具(Chrome DevTools)的深度应用

这是前端调试的“主战场”,但我会使用一些更高阶的特性:

  • 条件断点(Conditional Breakpoints):在循环或者频繁触发的逻辑中,右键设置条件断点(例如 i === 5),避免无效的单步执行。
  • 日志点(Logpoints):在不希望污染源码、不触发重新编译的情况下,直接在 DevTools 里插入日志输出。
  • 性能分析(Performance Tab):针对页面卡顿,通过录制 Profile 观察火焰图,找出长任务(Long Task)和重排(Reflow)的根源。
  • 内存分析(Memory Tab):利用 Heap Snapshot 对比,排查闭包或未销毁的事件监听导致的内存泄漏。

2. 源码映射与编辑器集成(Source Maps & VS Code)

在处理经过压缩(Minified)的生产环境代码时,Source Map 是还原真相的关键。

  • VS Code Debugger:比起在浏览器里跳来跳去,我更倾向于直接在 VS Code 中配置 .vscode/launch.json。这样可以直接在编辑器内打断点,利用编辑器强大的上下文感知能力查看变量,实现“编写与调试”体验的无缝衔接。

3. 针对框架层面的专项调试

现代前端框架(React/Vue)有其独特的状态流转机制,通用工具往往难以直观追踪。

  • 专项插件:使用 React Developer Tools 观察组件树的 Fiber 结构和 Hooks 状态;使用 Vue Devtools 追踪 Pinia 状态的变化以及事件总线的触发。
  • 时间旅行(Time Travel):在状态管理库(如 Redux/Vuex)中,通过快照回溯定位是哪一次 Action 导致了状态异常。

4. 网络层与接口模拟(Network & Proxy)

当后端接口尚未就绪或环境受限时:

  • Charles / Whistle / Fiddler:作为代理抓包工具,用于截获并修改请求。特别是 Whistle,可以通过配置规则实现域名映射、注入调试脚本(如 Eruda/vConsole)到移动端页面。
  • Mock 服务:在开发阶段使用 MSW (Mock Service Worker) 拦截 Fetch 请求,模拟各种极端情况(如 500 错误、请求超时)。

5. 移动端真机调试

这是前端开发中的痛点。

  • Safari/Chrome 远程调试:通过 USB 连接,在桌面端浏览器控制真机中的 WebView。
  • 微端/内建工具:对于微信小程序或特定 App 内部的 H5,利用微信开发者工具的远程调试功能,或者在页面中植入 vConsole 这种轻量级面板查看日志。

6. 异常监控与日志溯源

对于线上偶发性问题:

  • Sentry / Fundebug:通过上报的堆栈信息(Stack Trace)定位报错行数。
  • 用户行为回放:分析用户操作路径,重现报错前的上下文。

13. 多个项目共用组件时,如何保证版本一致性?

难度:3 · 类型:QA

题目要点

  1. 使用私有 npm 仓库或 Monorepo 是最可靠的版本一致性方案。
  2. 建议统一依赖管理与版本策略(SemVer + CHANGELOG + 自动发布)。
  3. 临时可用 git 引用或 link 方式调试,但不宜长期依赖。
  4. 最终目标是保证「组件单一来源、版本透明可控、升级可追溯」。
参考答案

多个项目共用组件时,版本一致性问题往往是前端工程化中最棘手的部分之一。它不仅关系到依赖冲突与兼容性问题,更直接影响到团队协作、线上稳定性与版本回滚策略。解决的关键在于:明确组件的发布策略、依赖约束机制以及统一的版本管理体系

一、核心问题来源

  1. 多个项目独立安装依赖: 不同项目各自安装组件库版本,容易出现「A 项目用 1.0.0,B 项目用 1.1.2」的情况,导致体验不一致。

  2. 组件库频繁更新: 当组件库持续迭代时,如果没有明确的发布与更新策略,使用方容易“被动跟进”,甚至发生 breaking change。

  3. 多仓库开发: 在多仓场景下(每个项目独立仓库),难以保证所有项目能同时感知版本变化。

二、可行的解决方案

1. 使用私有 npm 仓库统一发布版本

最常见、最稳定的方案是:

  • 将公共组件抽离成独立 npm 包(例如 @org/ui
  • 在公司内部搭建私有 npm 仓库(如 Nexus、Verdaccio)
  • 所有项目通过包管理工具(npm / pnpm / yarn)引用同一个版本号

一旦组件库发布新版本,只需执行 npm publish 即可同步更新。 项目方可以通过 package.json 的版本锁定策略来控制是否升级(如 ~1.2.0 表示只允许补丁更新)。

优点:版本清晰、可追溯、可回滚。 缺点:组件调试周期略长,需先发布后验证。


2. 使用 Monorepo 管理多个项目与组件库

当团队的项目数量较多或组件依赖较深时,可采用 Monorepo 架构,通过工具(如 pnpm workspaceNxTurborepo)统一管理依赖。

Monorepo 的优势在于:

  • 所有项目与组件共享同一依赖树,自动保持版本一致。
  • 改动组件后可本地实时联调,不必发布到 npm。
  • 可统一执行构建、测试、版本发布命令,提升 CI/CD 效率。

发布时配合 changesetlerna version 等工具统一打版本。


3. 通过 Git 子模块或包引用方式共享代码

对于较轻量的团队,也可使用 Git 的 submodulesubtree 将组件库挂载到多个项目中。 或通过 npm link / pnpm link 的方式建立软链接,实现本地同步调试。

优点:简单易用,适合中小型团队。 缺点:版本变更需要手动同步更新,容易遗漏。


4. 建立统一的版本发布与变更管理机制

无论采用哪种技术架构,都应当有一致的「版本管理规范」,如遵守 语义化版本(SemVer)

  • MAJOR:存在破坏性变更
  • MINOR:新增功能,向后兼容
  • PATCH:修复问题

同时,建议:

  • 建立 CHANGELOG,明确每次改动的影响范围。
  • 在 CI/CD 阶段自动校验依赖版本(例如通过脚本检测各项目的组件版本号)。
  • 使用自动化发布工具(如 changesets)生成一致的版本号与变更日志。

5. 临时调试或灰度更新

在多项目共享组件但无法立即统一升级的场景,可通过:

  • 内部镜像(如 Git 直连 npm install git+ssh://...
  • 使用 alphabeta 分支进行灰度发布(如 1.2.0-beta.3
  • 通过 CI 工具(Jenkins、GitHub Actions)控制指定项目使用特定分支版本

这能让主线项目保持稳定,同时支持部分项目提前验证新版本。

三、经验总结

  1. 核心原则:组件库必须具备“单一真源”(Single Source of Truth),所有项目依赖的组件来自统一的版本源。
  2. 短期策略:可通过 Git 子模块或 npm link 保持调试阶段一致性。
  3. 长期方案:通过私有 npm + Monorepo + 语义化版本管理建立持续可控的版本体系。

14. 当需要更改组件库中组件的主题时,有哪些常见的方法?请以你熟悉的组件库为例,说明如何实现主题定制。

难度:3.5 · 类型:QA

题目要点

  1. 修改 Less / Sass 变量 → 全局主题定制,适合构建时静态修改。
  2. 使用组件库提供的配置 API(ConfigProvider、ThemeProvider 等) → 动态主题切换,安全可靠。
  3. 覆盖 CSS 样式 → 针对个别组件微调,但升级风险较高。
  4. CSS-in-JS 方案 → 组件级主题控制,可动态切换,但有额外依赖和性能开销。
参考答案

在前端项目中,当需要修改组件库的主题时,一般有几种常见方法:覆盖 CSS 变量、通过配置提供的主题 API、使用自定义样式或 CSS-in-JS。

以下以 Ant Design 为例说明实现方式:

1. 覆盖 Less 变量(Ant Design)

Ant Design 的样式是基于 Less 变量实现的,通过修改 Less 变量可以全局改变主题。

使用方法(Vite / Webpack + less-loader)

  1. 安装 Less 和相关 loader:
npm install less less-loader --save-dev
  1. 在构建工具中配置 less-loadermodifyVars
// vite.config.js 示例
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import less from 'less';

export default defineConfig({
  plugins: [react()],
  css: {
    preprocessorOptions: {
      less: {
        modifyVars: {
          '@primary-color': '#1DA57A',
          '@link-color': '#1890ff',
        },
        javascriptEnabled: true,
      },
    },
  },
});

效果

  • 改变 @primary-color 就会影响按钮、输入框、标签等组件的主色。
  • 可以通过修改其他 Less 变量调整边框圆角、字体大小等。

2. 使用 ConfigProvider 提供全局主题(Ant Design 4.0+)

Ant Design 提供了 ConfigProvider,可以动态设置主题样式或方向:

import { ConfigProvider, Button } from 'antd';

function App() {
  return (
    <ConfigProvider
      theme={{
        token: {
          colorPrimary: '#722ED1', // 覆盖主色
        },
      }}
    >
      <Button type="primary">主题按钮</Button>
    </ConfigProvider>
  );
}
  • 优点:支持在运行时动态切换主题。
  • 限制:只能修改组件库暴露的 token 或主题变量,精细定制有限。

3. 自定义 CSS / 覆盖样式

  • 可以通过增加自定义 CSS 或 Sass 文件覆盖组件默认样式。
  • 适合对个别组件做微调,而不改变全局主题。
/* 覆盖 Ant Design Button 样式 */
.ant-btn-primary {
  background-color: #ff4d4f;
  border-color: #ff4d4f;
}
  • 优点:灵活、直接,可针对某些特殊组件。
  • 缺点:全局维护较麻烦,升级组件库可能导致样式覆盖失效。

4. CSS-in-JS 或 styled-components

  • 对于使用 styled-components 或 Emotion 的项目,可以通过动态 props 修改主题样式:
import styled from 'styled-components';
import { Button } from 'antd';

const ThemedButton = styled(Button)`
  background-color: ${props => props.theme.primaryColor};
  border-color: ${props => props.theme.primaryColor};
`;

<ThemedButton theme={{ primaryColor: '#52c41a' }}>主题按钮</ThemedButton>
  • 优点:可以实现组件级别的主题切换,灵活性高。
  • 缺点:需要额外引入 CSS-in-JS 库,并增加运行时开销。

15. 如果你有一个业务组件库,希望打包输出为 esm + cjs + dts,有什么思路?

难度:3.5 · 类型:QA

题目要点

  1. 使用 vite.build 搭配 vite-plugin-dts 可以同时输出 ESM、CJS 和 .d.ts
  2. 保持 React、Vue 等为 external 避免重复打包。
  3. 配置 package.jsonmain/module/types 字段,清晰指向输出结果。
  4. 确保 tsconfig.json 中开启类型声明生成并指定输出路径。
  5. 实际打包后建议用真实项目验证加载和类型提示是否正常。
参考答案

打包成 ESM、CJS 和生成 TypeScript 的 .d.ts 类型声明文件,关键是合理配置构建工具。

我们可以选用 RollupVite(底层也是用 Rollup),下面以使用 Vite 构建组件库 为例,讲讲如何配置输出 ESM + CJS + DTS:


一、初始化项目结构

假设目录结构是:

/src
  Button/index.tsx
  index.ts  // 组件库入口

package.json
vite.config.ts
tsconfig.json

二、Vite 配置打包输出

vite.config.ts 示例:

import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import path from 'path';

export default defineConfig({
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'), // 组件库入口
      name: 'MyComponentLib',
      formats: ['es', 'cjs'],
      fileName: (format) => `my-component-lib.${format}.js`,
    },
    rollupOptions: {
      external: ['react', 'react-dom'], // 避免打包这些外部依赖
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
  plugins: [dts()],
});

重点解释下配置细节:

  • formats: ['es', 'cjs']:表示输出两种格式。
  • external: 保持 reactreact-dom 是外部依赖(否则会被打进 bundle)。
  • vite-plugin-dts:自动为库生成类型定义文件。

三、生成 .d.ts 类型文件

安装 vite-plugin-dts 插件:

pnpm add vite-plugin-dts -D

它会读取 tsconfig.json 并在打包时生成类型声明到 dist/ 目录。你也可以单独用 tsc 生成,但 vite-plugin-dts 更方便,支持合并等特性。


四、tsconfig.json 配置

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "declaration": true,
    "declarationDir": "dist/types",
    "emitDeclarationOnly": true,
    "jsx": "react-jsx",
    "outDir": "dist",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "strict": true
  },
  "include": ["src"]
}

确保开启 declarationemitDeclarationOnly,并设置好 include


五、打包命令配置

package.json 中配置构建命令:

{
  "main": "dist/my-component-lib.cjs.js",
  "module": "dist/my-component-lib.es.js",
  "types": "dist/types/index.d.ts",
  "scripts": {
    "build": "vite build"
  }
}
  • main 是 CJS 的入口
  • module 是 ESM 的入口
  • types 指向类型声明文件

六、验证输出

执行:

pnpm build

会输出:

  • dist/my-component-lib.es.js
  • dist/my-component-lib.cjs.js
  • dist/types/index.d.ts

此时组件库即可支持多种模块加载方式和类型提示。


其他建议

  • 如果库中包含样式文件(如 .scss),要通过插件或 Vite 自定义配置将样式打包或分离。
  • 发布前可以通过 npm pack 查看实际输出的文件内容。
  • 如果考虑 tree-shaking,建议每个组件也单独导出,便于用户按需引入。

16. 前端实现的倒计时为什么会有误差?有什么减小误差的解决方案吗?

难度:3 · 类型:QA

题目要点

  • 如果是普通倒计时(如 10s 倒计时):使用 Date.now() 修正
  • 如果是高精度倒计时(如游戏或精确到毫秒的倒计时):使用 requestAnimationFrame + performance.now()
  • 如果是多人同步倒计时(如直播、抢购):使用服务器时间同步
参考答案

前端倒计时会产生误差的主要原因是 setTimeoutsetInterval 的执行机制 以及 JavaScript 单线程的特性。以下是具体原因和优化方案:


1. 为什么前端倒计时会有误差?

(1) setTimeoutsetInterval 的执行机制

  • setTimeout(fn, delay) 并不会在精确的 delay 毫秒后执行 fn,而是至少 delay 毫秒后执行。
  • setInterval(fn, delay) 也是如此,它的下一次执行时间是当前任务执行完后才开始计算 delay,所以可能比预期时间更长。

(2) JavaScript 是单线程的

  • JS 是单线程的,如果主线程在执行其他任务(如渲染、事件回调等),定时器回调可能会被延迟执行。

(3) 浏览器的最小时间间隔限制

  • 大多数浏览器最小的定时器间隔是 4ms(在非活跃标签页或后台页面,可能会被调整到 1s 甚至更长)。

(4) 设备性能与负载

  • 如果设备负载高,主线程被占用,定时器可能会延迟执行,导致倒计时出现误差。

2. 如何减少误差?(优化方案)

方案 1:基于 Date.now()performance.now() 进行校正

核心思想

  • 记录开始时间,每次执行倒计时时,计算与实际过去时间的偏差,并动态调整。
function startCountdown(duration) {
  const startTime = Date.now(); // 记录开始时间
  const endTime = startTime + duration * 1000; // 计算结束时间

  function updateCountdown() {
    const now = Date.now();
    const remainingTime = Math.max(0, Math.floor((endTime - now) / 1000)); // 计算剩余时间

    console.log(remainingTime + " 秒");

    if (remainingTime > 0) {
      requestAnimationFrame(updateCountdown); // 使用 requestAnimationFrame 调整精度
    }
  }

  updateCountdown();
}

startCountdown(10); // 10秒倒计时

优势

  • 误差不会随时间累计,适合高精度倒计时。

方案 2:使用 requestAnimationFrame 代替 setInterval

核心思想

  • requestAnimationFrame 具有更高的执行精度,并且能在页面可见时保持稳定。
function countdown(duration) {
  let start = performance.now(); // 记录高精度时间
  let end = start + duration * 1000;

  function tick() {
    let now = performance.now();
    let remaining = Math.max(0, Math.floor((end - now) / 1000));

    console.log(remaining + " 秒");

    if (remaining > 0) {
      requestAnimationFrame(tick);
    }
  }

  tick();
}

countdown(10);

优势

  • performance.now() 提供更高精度(微秒级)。
  • 避免 setTimeout 的累积误差。

方案 3:服务端同步时间

核心思想

  • 前端倒计时误差可控,但如果需要高精准倒计时(如抢购、秒杀),建议从服务器获取精确时间,然后同步到前端。

步骤:

  1. 前端向服务器请求当前时间(如 timestamp = Date.now())。
  2. 后端返回服务器时间(如 serverTimestamp)。
  3. 计算前端和服务器的时间差delta = serverTimestamp - timestamp
  4. 倒计时基于服务器时间运行
    function startCountdown(duration, serverTimestamp) {
      const clientStartTime = Date.now();
      const endTime = serverTimestamp + duration * 1000;
    
      function updateCountdown() {
        const now = Date.now() + (serverTimestamp - clientStartTime); // 修正时间偏差
        const remainingTime = Math.max(0, Math.floor((endTime - now) / 1000));
    
        console.log(remainingTime + " 秒");
    
        if (remainingTime > 0) {
          requestAnimationFrame(updateCountdown);
        }
      }
    
      updateCountdown();
    }
    
    // 示例:假设服务器返回的当前时间是 1710000000000 毫秒
    startCountdown(10, 1710000000000);
    

优势

  • 适用于多人同步倒计时(如活动、直播等)。
  • 确保前端倒计时和服务器时间一致。

3. 方案对比总结

方案适用场景误差复杂度
setTimeout/setInterval普通倒计时(累积误差)(简单)
Date.now() 修正普通倒计时(误差可控)中等
requestAnimationFrame高精度倒计时极低(动画帧级别)中等
服务器同步时间需要多人同步几乎无误差

17. 对于 PC 端的 banner 组件,你会怎么适配超宽的屏幕?

难度:1.5 · 类型:QA

题目要点

  • 内容区域设置 max-width 居中是关键
  • 背景使用 cover/拉伸延展增强视觉体验
  • 保持视觉焦点在中间,避免 banner 被拉伸变形
参考答案

在 PC 端适配超宽屏幕的 banner 组件时,目标是让视觉中心不偏移,同时保证两边留白或延展自然:

  1. 限制内容区域最大宽度,居中显示

    .banner-inner {
      max-width: 1440px;
      margin: 0 auto;
    }
    
  2. 背景色或背景图延展至两侧

    .banner {
      background: url('banner-bg.jpg') center/cover no-repeat;
    }
    
  3. 左右背景延展,内容区域居中

    • 内容居中处理视觉聚焦
    • 背景可设置为拉伸图像渐变重复填充
  4. 媒体查询控制布局变化(如在超大屏时调整 padding/margin 等)

  5. 使用一些工具库,通过拾取 banner 图片的背景色,实现灵活过度和模块背景色填充

18. ChatGPT 的对话功能实现,为什么选择 SSE 协议而非 Websocket ?

难度:3.5 · 类型:QA

题目要点

ChatGPT 选择 SSE 协议是因为其简单、单向推送的特性非常适合对话类应用,避免了 WebSocket 双向通信的复杂性和开销。SSE 还具有良好的兼容性、自动重连机制和低开发维护成本,是在这个场景中的理想选择。

参考答案

ChatGPT 的对话功能实现选择 Server-Sent Events (SSE) 协议而非 WebSocket,主要是基于两者在使用场景和实现复杂度上的差异。

以下是原因分析:

1. 数据流单向性适合 SSE

  • SSE 是单向的:SSE 是由服务器主动推送消息到客户端的单向数据流协议,客户端只需要接收服务器传来的消息。这种模式适合像 ChatGPT 这样的应用场景,因为用户请求发送到服务器后,服务器只需要持续向客户端推送生成的对话数据。客户端没有必要频繁地向服务器发送数据(除了初次请求)。
  • WebSocket 是双向的:WebSocket 是全双工通信协议,允许客户端和服务器互相发送消息。虽然 WebSocket 功能更强大,但 ChatGPT 场景中并不需要客户端和服务器之间的高频双向通信。

2. 实现和维护复杂度

  • SSE 简单易用:SSE 只需要服务器推送消息,客户端可以通过标准的 EventSource API 轻松接收消息,并且基于 HTTP 协议实现。无需像 WebSocket 那样进行复杂的连接握手和状态管理。这使得 SSE 在实现和维护上相对简单。
  • WebSocket 较复杂:WebSocket 在建立连接时需要进行协议升级,并且要管理双向通信,增加了复杂度。尤其在某些代理、防火墙等网络环境中,WebSocket 的握手和长连接更容易遇到阻碍。

3. 兼容性和可靠性

  • SSE 通过 HTTP/1.1 实现:SSE 是基于 HTTP/1.1 的长连接协议,通常能够更好地穿透代理服务器、防火墙等网络设施。这在需要保证消息推送可靠性时非常重要。
  • WebSocket 需要协议升级:WebSocket 需要从 HTTP 升级到 WebSocket 协议 (wswss),某些网络环境可能会阻断这种升级过程,从而影响连接的可靠性。

4. 自动重连和消息重发

  • SSE 提供自动重连功能:如果网络中断或连接丢失,SSE 会自动尝试重连,且服务器可以通过 Last-Event-ID 实现消息重发,从而保证消息不会丢失。
  • WebSocket 重连复杂:WebSocket 需要自行实现重连逻辑和消息重发功能,增加了开发的复杂度和维护成本。

5. 使用场景的适配性

  • SSE 适合较低频的消息推送:SSE 适用于不需要高频交互、消息量适中但要求可靠推送的应用场景,如实时通知、数据流等。ChatGPT 的对话功能正符合这种情况,消息从服务器按需推送到客户端即可。
  • WebSocket 适合高频双向通信:WebSocket 更适合需要双向、低延迟、高频数据交互的应用场景,比如在线游戏、实时协作工具等。ChatGPT 的场景则不需要这种双向高频通信。

6. 资源效率和性能

  • SSE 使用较少的资源:SSE 是通过 HTTP 长连接传输数据,资源消耗较少,尤其是在只需要单向通信的场景中,避免了 WebSocket 双向通信带来的额外负载。
  • WebSocket 性能较优但资源消耗大:WebSocket 在高频双向通信时有更高的性能,但对于像 ChatGPT 这种场景,其双向通信能力未被充分利用,反而会增加资源开销。

19. 有一个新的前端项目需要部署到线上,你有什么思路?

难度:4 · 类型:QA

题目要点

前端项目的线上部署过程包括项目准备、构建、选择部署平台、上传文件、服务器配置、设置域名和 HTTPS、自动化部署(如有必要)和后续监控。根据项目的复杂度和需求,可以使用不同的工具和平台实现自动化和性能优化。

参考答案

以下是常规思路:

1. 项目准备阶段

  • 检查项目构建配置:确保项目的构建配置文件(如 webpack.config.jsvite.config.js)已准备好并针对生产环境进行了优化(如代码压缩、Tree Shaking 等)。
  • 环境变量:配置环境变量,确保生产环境下的 API 地址和其他配置项正确。
  • 依赖检查:确保所有依赖项在 package.json 中已定义并通过 npm install 正确安装。

2. 构建项目

  • 打包构建:使用构建命令(如 npm run build)生成生产环境的静态文件。
  • 打包产物检查:检查打包后的文件,确保体积、性能、加载顺序等方面都正常。检查生成的 index.html、CSS 和 JS 文件等是否按预期生成。
  • 也可使用流水线进行构建

3. 选择部署平台

  • 静态服务器(Nginx/Apache):如需要自托管,选择 Nginx 或 Apache 来托管静态文件。为单页面应用(SPA)做好路由回退配置。
  • 云服务平台:如 AWS S3、阿里云 OSS 等静态托管服务。
  • 部署平台:如 Vercel、Netlify,这类平台支持自动化部署,并提供 CI/CD 集成。

4. 上传构建文件

  • 自托管服务器
    • 通过 FTP、SCP 或 Rsync 上传打包后的静态文件到服务器上的指定目录。
    • 如果使用 Docker,可以通过 Docker 容器化前端应用,并将其部署到云服务器上。
  • 云服务平台
    • 使用对应的 SDK 或 CLI 工具(如 AWS CLI、OSS CLI)上传文件到云存储。
  • 平台自动化部署
    • 如使用 Vercel 或 Netlify,可连接 Git 仓库,设置自动化构建和部署。

5. 配置服务器

  • Nginx 配置:配置 Nginx 或其他服务器,指向项目的根目录,并处理 SPA 的路由问题。
  • 缓存优化:配置 HTTP 缓存策略(如 Cache-ControlETag),优化文件的缓存和加载速度。
  • CDN 配置:如果有全球用户,考虑使用 CDN 分发静态文件,提升加载速度。

6. 设置域名和 HTTPS

  • 绑定域名:确保域名已解析到服务器的 IP 地址或绑定到云平台。
  • HTTPS 支持:通过 Let’s Encrypt 或购买 SSL 证书,确保项目可以通过 HTTPS 访问。

7. 自动化部署(可选)

  • CI/CD 集成:集成 CI/CD 工具(如 GitHub Actions、Jenkins),自动完成构建、测试和部署流程。
  • 钩子配置:如使用 Vercel/Netlify 等平台,配置 Webhooks,确保每次代码变更后自动部署。

8. 测试和监控

  • 测试部署环境:在生产环境下进行全面的功能测试,确保所有 API 调用、页面加载和用户交互正常。
  • 性能监控:引入监控工具(如 Google Analytics、Sentry)监控性能和错误日志。
  • 定期更新和维护:定期检查项目性能、修复潜在问题,并根据业务需求进行定期更新。

示例:基于 Nginx 的部署流程

  1. 打包构建
    npm run build
    
  2. 上传文件到服务器
    scp -r ./build/* user@server_ip:/var/www/project
    
  3. Nginx 配置
    server {
        listen 80;
        server_name example.com;
        root /var/www/project;
        index index.html;
        location / {
            try_files $uri $uri/ /index.html;
        }
    }
    
  4. 重启 Nginx
    sudo systemctl restart nginx
    

20. 怎么在 react 技术栈的低代码平台中,实现组件的脚本注入?

难度:4 · 类型:QA

题目要点

在 React 技术栈的低代码平台中,脚本注入可以通过 new Function 来动态执行用户输入的 JavaScript 代码,并通过事件和状态管理来进行交互。虽然脚本注入提供了灵活的自定义能力,但安全性是关键,必须做好脚本的隔离和验证,防止 XSS 等安全风险。

参考答案

在使用 React 技术栈构建的低代码平台中,脚本注入可以让用户通过自定义 JavaScript 脚本来控制页面中的组件行为或逻辑。这在一些高级场景下非常有用,比如动态交互、组件间的复杂联动逻辑等。实现脚本注入的过程中,既要保证灵活性,也要考虑安全性。

以下是实现脚本注入的思路:

实现思路

  1. 动态脚本注入设计 通过为每个组件或页面元素提供一个可配置的脚本区域,允许用户输入 JavaScript 代码。平台会在特定的生命周期(如挂载、更新或事件触发时)执行这些脚本。脚本可以用来操作组件的 props 或状态。

  2. 通过事件触发执行 注入的脚本往往通过事件触发,如点击按钮、表单提交等。这时可以为组件提供事件回调的配置区域,在触发事件时执行用户注入的 JavaScript。

  3. 使用 evalnew Function 动态执行 React 中,JavaScript 代码的注入可以通过 evalnew Function 来动态执行用户的脚本。不过,这种方法可能存在安全风险(如 XSS 漏洞),需要通过严格的沙箱机制或者限制作用域来确保安全。

  4. 搭配状态管理 为用户注入的脚本提供访问组件状态的能力,通过 React 的 useStateuseContext 等方式将状态暴露给用户脚本,这样脚本可以在运行时动态修改组件的表现。

实现步骤

1. 配置脚本注入区域

在低代码平台中,用户可以为每个组件配置注入脚本。你可以提供一个输入框或者编辑器,用户可以输入 JavaScript 代码片段。例如,提供一个 beforeRenderScript 字段,用户可以在渲染之前执行某些自定义逻辑。

const [customScript, setCustomScript] = useState(``);

// 用户在 UI 中输入 JavaScript 代码
<textarea value={customScript} onChange={(e) => setCustomScript(e.target.value)} />

2. 脚本执行机制

使用 new Function 动态创建并执行用户脚本:

// 假设 customScript 是用户注入的 JavaScript 代码
function executeUserScript(script, props, state) {
  try {
    const scriptFn = new Function('props', 'state', script);
    return scriptFn(props, state); // 执行脚本,传入组件的 props 和 state
  } catch (error) {
    console.error("Script execution failed", error);
  }
}

3. 在组件生命周期中执行脚本

可以在 React 组件的生命周期钩子中执行用户脚本,比如在 useEffect 中执行:

useEffect(() => {
  if (customScript) {
    executeUserScript(customScript, props, state);
  }
}, [customScript, props, state]);

这样,当组件初始化或者更新时,用户的自定义脚本就会运行。

4. 处理事件注入

为组件暴露事件处理钩子,让用户可以通过自定义脚本在事件触发时执行动态逻辑:

const handleClick = () => {
  if (customScript) {
    executeUserScript(customScript, props, state);
  }
};

绑定到 React 组件的 onClick 事件:

<button onClick={handleClick}>Click me</button>

5. 状态与数据绑定

为了让用户脚本可以访问和修改组件状态,应该将 state 传入 executeUserScript 中,或者通过暴露 setter 函数让用户脚本直接更新 React 组件的状态:

const [value, setValue] = useState(0);

// 用户注入脚本,修改状态
const customScript = `
  if (props.shouldIncrease) {
    state.setValue(state.value + 1);
  }
`;

// 通过将 setValue 暴露给用户的脚本
executeUserScript(customScript, { shouldIncrease: true }, { value, setValue });

这样用户就能动态控制组件的行为。

安全性考虑

  1. 防止 XSS 攻击

    • 对于输入的脚本代码要有严格的验证,过滤掉恶意代码。
    • 可以通过在 iframe 中执行脚本或使用沙箱机制来隔离执行环境,避免直接修改页面 DOM。
    • 尽量避免直接使用 eval,可以使用 new Function 来控制传递的变量作用域。
  2. 沙箱环境

    • 可以通过限制用户脚本的作用域,确保用户只能访问合法的 propsstate,无法修改页面其他部分。
    • 或者可以通过 Proxy 或者 with 语句来控制用户代码执行时的环境,限制全局变量的访问。

完整示例

import React, { useState, useEffect } from 'react';

function CustomComponent() {
  const [value, setValue] = useState(0);
  const [customScript, setCustomScript] = useState('');

  // 用于执行用户脚本的函数
  const executeUserScript = (script, props, state) => {
    try {
      const scriptFn = new Function('props', 'state', script);
      return scriptFn(props, state);
    } catch (error) {
      console.error("Script execution error:", error);
    }
  };

  // 在组件初始化或更新时执行
  useEffect(() => {
    if (customScript) {
      executeUserScript(customScript, { shouldIncrease: true }, { value, setValue });
    }
  }, [customScript, value]);

  // 输入框让用户输入自定义脚本
  return (
    <div>
      <textarea
        value={customScript}
        onChange={(e) => setCustomScript(e.target.value)}
        placeholder="Enter your JavaScript code here"
      />
      <p>Value: {value}</p>
      <button onClick={() => setValue(value + 1)}>Increase Value</button>
    </div>
  );
}

export default CustomComponent;

21. 怎么实现表单项间的联动?

难度:3 · 类型:QA

题目要点

  • 状态管理:在框架(如 React、Vue)中,通过使用 useStatedata 来管理表单的状态,实现表单项之间的联动。
  • 事件监听:使用原生 JavaScript 可以通过事件监听的方式来手动实现表单的联动。
  • 禁用/启用:通过联动时动态调整表单项的禁用状态、可选内容、验证规则等,可以提升表单的交互体验。
参考答案

表单项的联动,是指一个表单字段的值或状态变化时,动态影响其他表单字段的值、显示或验证状态。

1. 基于状态管理的表单联动

使用状态管理来实现表单项联动是最常见的方法之一,特别是使用像 React、Vue 等框架时,可以利用其组件的响应式特性,动态更新表单项。

React 实现示例

import React, { useState } from 'react';

function Form() {
  const [formData, setFormData] = useState({
    country: '',
    city: '',
    postalCode: '',
  });

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value,
    });
  };

  // Example: Cities are dependent on the selected country
  const cities = formData.country === 'USA' ? ['New York', 'Los Angeles'] : ['Tokyo', 'Osaka'];

  return (
    <form>
      <label>
        Country:
        <select name="country" value={formData.country} onChange={handleInputChange}>
          <option value="">Select</option>
          <option value="USA">USA</option>
          <option value="Japan">Japan</option>
        </select>
      </label>

      <label>
        City:
        <select name="city" value={formData.city} onChange={handleInputChange}>
          <option value="">Select</option>
          {cities.map((city) => (
            <option key={city} value={city}>
              {city}
            </option>
          ))}
        </select>
      </label>

      <label>
        Postal Code:
        <input
          name="postalCode"
          value={formData.postalCode}
          onChange={handleInputChange}
          disabled={!formData.city} // Disable postal code field if no city is selected
        />
      </label>
    </form>
  );
}

export default Form;

思路

  1. 状态管理:通过 React 的 useState 进行表单状态的管理。所有表单数据保存在 formData 中。
  2. 联动逻辑:当 country 选择变化时,动态改变 city 下拉选项的内容。比如当 country 选择为 USA 时,城市选项动态变为纽约和洛杉矶。
  3. 禁用逻辑:邮政编码输入框的 disabled 属性依赖于城市是否选择,从而实现输入框禁用/启用的联动。

2. 使用事件监听进行表单联动

你也可以直接通过监听表单字段的 change 事件,在事件回调中实现表单项之间的联动。

实现示例

<form id="myForm">
  <label>
    Country:
    <select id="country">
      <option value="">Select</option>
      <option value="USA">USA</option>
      <option value="Japan">Japan</option>
    </select>
  </label>

  <label>
    City:
    <select id="city" disabled>
      <option value="">Select</option>
    </select>
  </label>

  <label>
    Postal Code:
    <input type="text" id="postalCode" disabled />
  </label>
</form>

<script>
  const country = document.getElementById('country');
  const city = document.getElementById('city');
  const postalCode = document.getElementById('postalCode');

  const cities = {
    USA: ['New York', 'Los Angeles'],
    Japan: ['Tokyo', 'Osaka'],
  };

  country.addEventListener('change', function () {
    const selectedCountry = country.value;
    city.innerHTML = '<option value="">Select</option>'; // 清空城市选项

    if (selectedCountry) {
      cities[selectedCountry].forEach((cityName) => {
        const option = document.createElement('option');
        option.value = cityName;
        option.textContent = cityName;
        city.appendChild(option);
      });

      city.disabled = false;
    } else {
      city.disabled = true;
    }

    postalCode.disabled = true; // 清空时禁用邮政编码
  });

  city.addEventListener('change', function () {
    if (city.value) {
      postalCode.disabled = false;
    } else {
      postalCode.disabled = true;
    }
  });
</script>

思路

  1. 事件监听:为 countrycity 下拉框添加 change 事件监听器。
  2. 联动逻辑:当 country 改变时,重新生成 city 的选项,并启用或禁用 citypostalCode
  3. 手动 DOM 操作:通过 DOM 操作动态添加或移除选项,实现表单字段之间的联动。

3. 使用 Vue 实现表单联动

Vue 的响应式数据绑定功能非常适合实现表单联动。

Vue 实现示例

<template>
  <form>
    <label>
      Country:
      <select v-model="formData.country" @change="updateCities">
        <option value="">Select</option>
        <option value="USA">USA</option>
        <option value="Japan">Japan</option>
      </select>
    </label>

    <label>
      City:
      <select v-model="formData.city" :disabled="cities.length === 0">
        <option value="">Select</option>
        <option v-for="city in cities" :key="city" :value="city">{{ city }}</option>
      </select>
    </label>

    <label>
      Postal Code:
      <input v-model="formData.postalCode" :disabled="!formData.city" />
    </label>
  </form>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        country: '',
        city: '',
        postalCode: '',
      },
      cities: [],
    };
  },
  methods: {
    updateCities() {
      const cityData = {
        USA: ['New York', 'Los Angeles'],
        Japan: ['Tokyo', 'Osaka'],
      };
      this.cities = cityData[this.formData.country] || [];
      this.formData.city = ''; // 重置城市选择
      this.formData.postalCode = ''; // 重置邮政编码
    },
  },
};
</script>

思路

  • Vue 的双向绑定:通过 v-model 绑定表单项的值,实现表单数据的双向绑定。
  • 响应式数据:通过改变 country 的值,自动更新 cities 列表,从而实现城市的联动。
  • 禁用属性:使用条件来禁用或启用某些表单项。

22. 现在要开发一个响应式的网站,怎么实现根据设备的尺寸和分辨率,加载不同尺寸的 banner?要求不使用 JavaScript

难度:3 · 类型:QA

题目要点

1. <picture> 元素

  • 优点
    • 支持根据媒体条件加载不同的图像,适应各种设备。
    • 语义清晰,便于维护和理解。
  • 缺点
    • 需要较多的 HTML 代码。
    • 对老旧浏览器的支持有限。

2. srcset 属性

  • 优点
    • 允许在同一 <img> 标签中定义多种图像,便于管理。
    • 根据设备的像素比加载合适的图片,优化性能。
  • 缺点
    • 需要理解 sizes 属性的用法,有一定学习曲线。
    • 对于复杂的布局可能不够灵活。

3. 媒体查询

  • 优点
    • 强大的 CSS 控制,适用于背景图像和其他元素。
    • 易于使用,不需要额外的 HTML 标签。
  • 缺点
    • 只能用于背景图,不能直接替换 <img> 标签的内容。
    • 可能会导致 CSS 复杂度增加,影响可维护性。

4. image-set()

  • 优点
    • 可以根据设备像素比选择合适的图像,提升清晰度。
    • 语法简洁,可以在 CSS 中直接使用。
  • 缺点
    • 浏览器支持情况不如其他方法广泛,可能影响兼容性。
    • 对于不同屏幕尺寸的适配能力有限。
参考答案

使用 <picture> 元素

<picture>
    <source media="(min-width: 1200px)" srcset="banner-large.jpg">
    <source media="(min-width: 600px)" srcset="banner-medium.jpg">
    <img src="banner-small.jpg" alt="Banner Image">
</picture>

使用多个 <source> 元素,根据屏幕的最小宽度来选择不同的图片。

使用 srcset 属性

如果只想使用 <img> 标签,可以这样做:

<img src="banner-small.jpg"
     srcset="banner-medium.jpg 600w,
             banner-large.jpg 1200w"
     sizes="(min-width: 1200px) 100vw,
            (min-width: 600px) 50vw,
            100vw"
     alt="Banner Image">

<img> 标签中使用 srcset 定义多个图片及其宽度,结合 sizes 属性指定如何选择图片。

是的,媒体查询和 image-set() 也可以用于实现响应式图片,下面是如何使用它们:

媒体查询

你可以使用 CSS 媒体查询来根据屏幕尺寸设置背景图像。例如:

.banner {
    background-image: url('banner-small.jpg');
}

@media (min-width: 600px) {
    .banner {
        background-image: url('banner-medium.jpg');
    }
}

@media (min-width: 1200px) {
    .banner {
        background-image: url('banner-large.jpg');
    }
}

通过不同的屏幕宽度定义样式,使得不同尺寸的背景图像在相应的设备上加载。

image-set()

如果你使用 CSS,可以利用 image-set() 来根据设备像素比加载不同的图片:

.banner {
    background-image: image-set(
        url('banner-small.jpg') 1x,
        url('banner-medium.jpg') 2x,
        url('banner-large.jpg') 3x
    );
}

根据设备的像素比自动选择合适的图像,提升图像清晰度和加载性能。

23. 如果想在小程序中嵌入 markdown 的文档,你有什么思路?

难度:3 · 类型:QA

题目要点

  • 转成 html 后,使用 rich-text 或者其他方式进行渲染
  • 使用第三方库渲染 markdown文档,比如 wxParsetowxml
参考答案

小程序不支持直接渲染 markdown,但是支持富文本的渲染,可以将文档转成 html 后进行渲染,或者使用一些第三方的库直接进行 markdown完成渲染。

1. 转成 html 后渲染

  • 步骤

    1. 使用 Markdown 解析库(如 markedmarkdown-it 等)将 Markdown 文本转换为 HTML。
    2. 使用小程序的 <web-view><rich-text> 组件,或者自定义的组件渲染转换后的 HTML。
  • 示例

    // 在 JavaScript 文件中
    import marked from 'marked';
    
    // 将 Markdown 转换为 HTML
    const markdownText = '# Hello World\nThis is a Markdown document.';
    const html = marked(markdownText);
    
    // 将转换后的 HTML 传递给小程序的页面或组件
    this.setData({ html });
    
    <!-- 在 WXML 文件中 -->
    <rich-text nodes="{{html}}" />
    

2. 使用第三方的组件库

可以使用第三方库,如 wxParsetowxml 进行渲染。这些库可以将 Markdown 文本解析为微信小程序可以理解的组件和数据。

以下是使用 wxParse 的基本步骤:

  1. 下载并导入 wxParse 到你的项目中。
  2. 在需要使用的页面的 JS 文件中,引入 wxParse.js
  3. 使用 wxParse.wxParse('markdown', 'md', markdownContent, this) 方法将 Markdown 文本解析为数据和组件。
  4. 在对应的 WXML 文件中,使用 <import src="wxParse.wxml"/> 导入 wxParse 的 WXML。
  5. 在需要显示 Markdown 的地方,使用 <template is="wxParse" data="{{wxParseData:markdown.nodes}}"/> 显示解析后的内容。

以下是使用 wxParse 渲染 Markdown 的代码示例:

// 引入 wxParse.js
var wxParse = require('../../wxParse/wxParse.js');

Page({
  data: {
    markdown: ''
  },
  onLoad: function(options) {
    var that = this;
    var markdownContent = '# 这是一个标题\n这是一段普通的文本';
    wxParse.wxParse('markdown', 'md', markdownContent, that);
  }
});
<!-- 引入 wxParse.wxml -->
<import src="../../wxParse/wxParse.wxml"/>
<view>
  <!-- 显示解析后的内容 -->
  <template is="wxParse" data="{{wxParseData:markdown.nodes}}"/>
</view>

24. 假如让你负责一个商城系统的开发,现在需要统计商品的点击量,你有什么样设计与实现的思路?

难度:4.5 · 类型:QA

题目要点

  1. 前端

    • 绑定点击事件,使用防抖或节流技术优化请求。
    • 将点击数据发送到后端。
  2. 后端

    • 设计 API 接口接收点击数据。
    • 使用高性能数据库或缓存系统存储点击量数据。
    • 优化数据处理性能,处理高并发请求。
  3. 数据展示

    • 提供管理后台和实时监控功能,帮助分析点击量数据。
参考答案

1. 需求分析

  • 功能:统计每个商品的点击量,并提供统计数据的接口。
  • 性能要求:处理高并发点击请求,保证系统的稳定性和实时性。
  • 数据存储:选择合适的存储方式来保存点击量数据,支持高效的读写操作。

2. 设计思路

前端

  1. 事件绑定

    • 在商品页面中,通过 JavaScript 绑定点击事件到商品元素(如按钮、图片等)。
    // 假设商品按钮有一个 class 为 'product-btn'
    document.querySelectorAll('.product-btn').forEach(button => {
      button.addEventListener('click', (event) => {
        const productId = event.target.getAttribute('data-product-id');
        // 调用函数发送点击事件到后端
        trackClick(productId);
      });
    });
    
    function trackClick(productId) {
      // 发送 AJAX 请求到后端
      fetch('/api/track-click', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ productId }),
      });
    }
    
  2. 优化

    • 使用防抖或节流技术,减少频繁的网络请求,避免因网络延迟造成的数据丢失。

后端

  1. 接口设计

    • 设计一个 API 接口来接收前端发送的点击数据。例如 /api/track-click
    // Express.js 示例
    const express = require('express');
    const app = express();
    app.use(express.json());
    
    app.post('/api/track-click', (req, res) => {
      const { productId } = req.body;
      // 调用数据处理函数
      trackProductClick(productId);
      res.status(200).send('OK');
    });
    
    function trackProductClick(productId) {
      // 实现点击量统计逻辑
      // 例如:保存到数据库或缓存系统
    }
    
  2. 数据存储

    • 使用高性能的数据库或缓存系统来存储点击量数据,例如 Redis、MongoDB、MySQL 等。
    • 对于高并发场景,考虑使用缓存(如 Redis)进行预处理,定期将缓存数据写入持久存储。
    const redis = require('redis');
    const client = redis.createClient();
    
    function trackProductClick(productId) {
      client.incr(`product:${productId}:clicks`, (err, newClickCount) => {
        if (err) {
          console.error('Error updating click count:', err);
        } else {
          console.log(`Product ${productId} click count updated to ${newClickCount}`);
        }
      });
    }
    
  3. 性能优化

    • 使用异步处理和队列来处理高并发请求。
    • 设计合理的缓存机制来减少数据库的读取压力。
    • 定期对数据进行归档和清理,以保持数据的高效管理。

3. 数据展示

  • 管理后台

    • 设计管理后台接口和页面,用于查看和分析商品的点击量数据。
    • 提供图表和报表功能,方便管理人员进行数据分析。
  • 实时监控

    • 实现实时数据监控功能,帮助跟踪用户的点击行为和趋势。

25. 前端如何实现即时通讯?

难度:4 · 类型:QA

题目要点

  • WebSocket:适用于需要低延迟和双向通信的场景。
  • 长轮询:适用于传统服务器和客户端通信。
  • SSE:适用于单向服务器推送数据。
  • Firebase:适合快速开发实时应用,提供简单的接口和即时数据同步。
  • WebRTC:用于音视频通讯或实时数据传输。
参考答案

实现即时通讯(IM)功能是现代 web 应用中的一个重要需求,通常包括聊天、通知、状态更新等功能。以下是实现即时通讯的常见技术和方法:

1. 使用 WebSocket

WebSocket 提供了一种在客户端和服务器之间建立持久连接的方式,适合实时双向通信。

  • 步骤

    1. 建立连接:客户端和服务器通过 WebSocket 协议建立连接。
    2. 数据交换:通过 WebSocket 连接,客户端和服务器可以随时发送和接收消息。
    3. 关闭连接:当通信结束时,关闭 WebSocket 连接。
  • 示例代码

    客户端

    const socket = new WebSocket('ws://yourserver.com/socket');
    
    socket.onopen = () => {
      console.log('WebSocket connection opened');
      socket.send('Hello Server');
    };
    
    socket.onmessage = (event) => {
      console.log('Message from server', event.data);
    };
    
    socket.onclose = () => {
      console.log('WebSocket connection closed');
    };
    

    服务器(Node.js 示例):

    const WebSocket = require('ws');
    const wss = new WebSocket.Server({ port: 8080 });
    
    wss.on('connection', (ws) => {
      ws.on('message', (message) => {
        console.log('Received:', message);
        ws.send('Hello Client');
      });
    });
    

2. 使用长轮询(Long Polling)

长轮询是一种传统的实现即时通讯的方式,通过不断的请求来模拟实时数据推送。

  • 步骤

    1. 发送请求:客户端发送请求到服务器,服务器在有新数据时才响应。
    2. 处理响应:客户端处理服务器响应的数据,并发送新的请求以等待更多数据。
    3. 循环:重复发送请求和处理响应的过程。
  • 示例代码

    客户端

    function pollServer() {
      fetch('/long-polling-endpoint')
        .then(response => response.json())
        .then(data => {
          console.log('Received data:', data);
          pollServer(); // 继续轮询
        });
    }
    
    pollServer();
    

    服务器(Node.js 示例):

    const express = require('express');
    const app = express();
    
    app.get('/long-polling-endpoint', (req, res) => {
      // 模拟延迟
      setTimeout(() => {
        res.json({ message: 'Hello Client' });
      }, 1000);
    });
    
    app.listen(3000, () => console.log('Server is running on port 3000'));
    

3. 使用 Server-Sent Events (SSE)

SSE 允许服务器推送实时更新到客户端。适用于单向从服务器到客户端的数据流。

  • 步骤

    1. 建立连接:客户端通过 EventSource 对象与服务器建立连接。
    2. 接收事件:客户端处理从服务器推送的事件。
    3. 关闭连接:当不再需要更新时,客户端关闭连接。
  • 示例代码

    客户端

    const eventSource = new EventSource('http://yourserver.com/sse');
    
    eventSource.onmessage = (event) => {
      console.log('Message from server:', event.data);
    };
    
    eventSource.onerror = (error) => {
      console.error('Error:', error);
    };
    

    服务器(Node.js 示例):

    const express = require('express');
    const app = express();
    
    app.get('/sse', (req, res) => {
      res.setHeader('Content-Type', 'text/event-stream');
      res.setHeader('Cache-Control', 'no-cache');
      res.setHeader('Connection', 'keep-alive');
    
      setInterval(() => {
        res.write(`data: ${JSON.stringify({ message: 'Hello Client' })}\n\n`);
      }, 1000);
    });
    
    app.listen(3000, () => console.log('Server is running on port 3000'));
    

4. 使用 Firebase Realtime Database

Firebase Realtime Database 提供了一种简便的方式来实现实时数据同步,适合快速开发即时通讯应用。

  • 步骤

    1. 配置 Firebase:在 Firebase 控制台创建项目,并获取配置数据。
    2. 初始化 Firebase:在客户端初始化 Firebase。
    3. 使用 Firebase 数据库:读取和写入实时数据。
  • 示例代码

    客户端

    <script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-database.js"></script>
    
    <script>
      const firebaseConfig = {
        apiKey: 'your-api-key',
        authDomain: 'your-auth-domain',
        databaseURL: 'your-database-url',
        projectId: 'your-project-id',
        storageBucket: 'your-storage-bucket',
        messagingSenderId: 'your-messaging-sender-id',
        appId: 'your-app-id'
      };
    
      // Initialize Firebase
      firebase.initializeApp(firebaseConfig);
    
      const database = firebase.database();
    
      // Listening for data changes
      database.ref('messages').on('value', (snapshot) => {
        console.log('Received data:', snapshot.val());
      });
    
      // Sending data
      function sendMessage(message) {
        database.ref('messages').push(message);
      }
    </script>
    

5. 使用 WebRTC

WebRTC 主要用于实现实时音视频通信,但也可以用于实时数据传输。

  • 步骤

    1. 建立 Peer Connection:客户端和服务器(或两个客户端)建立 WebRTC 连接。
    2. 传输数据:通过连接发送和接收数据。
    3. 处理媒体:处理音视频流(可选)。
  • 示例代码

    客户端

    const peerConnection = new RTCPeerConnection();
    
    // Handle incoming data
    peerConnection.ondatachannel = (event) => {
      const dataChannel = event.channel;
      dataChannel.onmessage = (event) => {
        console.log('Message from peer:', event.data);
      };
    };
    
    // Create data channel
    const dataChannel = peerConnection.createDataChannel('chat');
    dataChannel.onopen = () => {
      dataChannel.send('Hello Peer');
    };
    

26. 前端怎么做错误监控?

难度:3.5 · 类型:QA

题目要点

  • 捕获 JavaScript 错误:使用 window.onerrorwindow.addEventListener 进行全局错误捕获。
  • 使用专业工具:集成 Sentry、Rollbar、LogRocket 等工具提供丰富的监控功能。
  • 监控 AJAX 请求错误:拦截 XMLHttpRequestfetch 请求以捕获网络请求错误。
  • 记录用户行为:记录用户操作和上下文,辅助排查问题。
参考答案

前端错误监控是确保应用稳定性和用户体验的重要手段。以下是一些常见的前端错误监控方法:

1. 捕获 JavaScript 错误

方法

  • 使用 window.onerror 事件:捕获全局 JavaScript 错误。
  • 使用 window.addEventListener('error'):捕获未处理的错误和资源加载错误。
  • 使用 window.addEventListener('unhandledrejection'):捕获未处理的 Promise 拒绝错误。

示例代码

// 捕获 JavaScript 错误
window.onerror = function (message, source, lineno, colno, error) {
    console.log('Error captured:', { message, source, lineno, colno, error });
    // 发送错误信息到服务器
    sendErrorToServer({ message, source, lineno, colno, error });
    return true; // 防止浏览器默认处理
};

// 捕获未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', function (event) {
    console.log('Unhandled rejection:', event.reason);
    // 发送错误信息到服务器
    sendErrorToServer({ reason: event.reason });
});

// 发送错误信息到服务器
function sendErrorToServer(error) {
    fetch('/log-error', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(error)
    });
}

2. 使用监控工具

集成专业的前端错误监控工具可以提供更多功能,如自动错误捕获、用户上下文、异常堆栈跟踪等。

常用工具

  • Sentry:提供 JavaScript 错误捕获、性能监控、用户上下文等功能。
  • Rollbar:实时错误监控和日志记录,支持自定义错误报告。
  • LogRocket:记录用户会话、错误和性能数据。
  • New Relic:性能监控和错误捕获,集成各种前端和后端数据。

示例代码(以 Sentry 为例)

<script src="https://browser.sentry-cdn.com/7.7.0/bundle.tracking.min.js"></script>
<script>
  Sentry.init({ dsn: 'https://your-dsn@sentry.io/your-project-id' });

  // 捕获 JavaScript 错误
  try {
    // 可能抛出错误的代码
  } catch (error) {
    Sentry.captureException(error);
  }

  // 捕获 Promise 拒绝
  window.addEventListener('unhandledrejection', event => {
    Sentry.captureException(event.reason);
  });
</script>

3. 监控 AJAX 请求错误

方法

  • 拦截 AJAX 请求和响应:在发起请求前和收到响应后处理错误。

示例代码

// XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
    this.addEventListener('error', function () {
        console.log('XHR error:', url);
        // 发送错误信息到服务器
        sendErrorToServer({ type: 'XHR', url, status: this.status });
    });
    return originalOpen.apply(this, arguments);
};

// Fetch API
const originalFetch = window.fetch;
window.fetch = function (url, options) {
    return originalFetch(url, options).catch(error => {
        console.log('Fetch error:', url, error);
        // 发送错误信息到服务器
        sendErrorToServer({ type: 'Fetch', url, error });
        throw error; // 继续抛出错误
    });
};

4. 记录用户行为和上下文

方法

  • 记录用户操作:在发生错误时记录用户的操作历史和上下文,以便进行故障排查。
  • 集成工具:使用用户行为分析工具,如 LogRocket,提供用户会话回放和上下文信息。

示例代码

// 示例:记录用户行为
document.addEventListener('click', function (event) {
    console.log('User clicked:', event.target);
    // 发送行为数据到服务器
    sendUserActionToServer({ type: 'click', target: event.target.tagName });
});

27. 如何给自己团队的大型前端项目设计单元测试?

难度:4 · 类型:QA

题目要点

设计和实施单元测试时,选择合适的工具,规划测试策略,编写清晰可维护的测试用例,集成到开发流程中,并定期维护更新。这些步骤有助于确保大型前端项目的稳定性和可靠性。

参考答案

设计和实施单元测试对于大型前端项目至关重要,可以确保代码的稳定性和可靠性。以下是设计单元测试的一些关键步骤和最佳实践:

1. 选择合适的工具

  • 测试框架:如 Jest、Mocha、Jasmine。Jest 是一个流行的选择,因为它提供了功能全面的测试运行器和断言库。
  • 测试库:如 React Testing Library、Enzyme(用于 React 组件测试),或者 Vue Test Utils(用于 Vue 组件测试)。

2. 规划测试策略

  • 确定测试范围

    • 组件测试:测试组件的渲染、事件处理和状态管理。
    • 功能测试:测试业务逻辑和功能模块的正确性。
    • 边界情况和异常测试:确保处理各种边界条件和异常情况。
  • 编写测试用例

    • 正向测试:验证正常流程是否按预期工作。
    • 负向测试:验证在输入无效或异常情况时系统如何反应。

3. 编写可维护的测试

  • 模块化

    • 测试单元:每个测试用例应该测试单一功能,避免测试多个功能的交互。
    • 拆分测试:将复杂的组件拆分为多个小的测试单元,每个单元测试特定的功能。
  • 清晰的测试用例

    • 命名:确保测试用例名称清晰且描述性强,能明确测试的功能或场景。
    • 断言:使用具体的断言来验证测试结果是否符合预期。

4. 测试示例

  • React 组件测试示例(使用 Jest 和 React Testing Library)

    import { render, screen, fireEvent } from '@testing-library/react';
    import MyComponent from './MyComponent';
    
    test('renders the component with initial state', () => {
      render(<MyComponent />);
      expect(screen.getByText('Initial State')).toBeInTheDocument();
    });
    
    test('handles button click correctly', () => {
      render(<MyComponent />);
      fireEvent.click(screen.getByText('Click Me'));
      expect(screen.getByText('Clicked')).toBeInTheDocument();
    });
    
  • Vue 组件测试示例(使用 Jest 和 Vue Test Utils)

    import { mount } from '@vue/test-utils';
    import MyComponent from '@/components/MyComponent.vue';
    
    test('renders component with initial state', () => {
      const wrapper = mount(MyComponent);
      expect(wrapper.text()).toContain('Initial State');
    });
    
    test('handles button click correctly', async () => {
      const wrapper = mount(MyComponent);
      await wrapper.find('button').trigger('click');
      expect(wrapper.text()).toContain('Clicked');
    });
    

5. 集成到开发流程

  • CI/CD 集成

    • 在持续集成(CI)环境中自动运行单元测试,确保每次提交和合并都经过测试。
    • 使用工具如 GitHub Actions、Travis CI、CircleCI 等来自动化测试流程。
  • 代码覆盖率

    • 使用覆盖率工具(如 Jest 的内置覆盖率工具)来检测测试的覆盖范围,确保关键代码路径被测试到。

6. 维护和更新

  • 定期更新测试

    • 随着代码的变化,定期更新测试用例,确保测试与实际代码保持一致。
    • 移除不再适用的测试,添加新的测试用例以覆盖新的功能或变更。
  • 重构测试

    • 通过重构测试代码保持其可读性和可维护性。避免测试代码的重复和复杂性。

28. 如何做一个前端项目工程的自动化部署,有哪些规范和流程设计?

难度:4.5 · 类型:QA

题目要点

自动化部署前端项目涉及选择合适的工具、规范化流程、持续集成和部署、监控和通知,以及文档和培训。通过建立有效的自动化部署流程,可以提高开发效率,减少人工错误,确保代码的稳定性和质量。

参考答案

自动化部署前端项目是提高开发效率和代码质量的关键步骤。它包括从代码提交到生产环境部署的全过程。以下是如何设计一个前端项目工程的自动化部署流程,以及相关的规范和流程:

1. 选择和配置工具

  • 版本控制:使用 Git 作为版本控制系统,代码托管在 GitHub、GitLab 或 Bitbucket 上。
  • CI/CD 工具:选择合适的 CI/CD 工具,如 GitHub Actions、GitLab CI、Jenkins、CircleCI 或 Travis CI,用于自动化构建和部署过程。
  • 构建工具:使用构建工具如 Webpack、Vite、Parcel 进行代码打包和优化。
  • 发布工具:使用发布工具或平台如 Netlify、Vercel、AWS S3、Azure Blob Storage 来托管和部署前端应用。

2. 规范化流程

  1. 代码提交和审核

    • 代码规范:遵循统一的代码风格和规范,使用工具如 ESLint、Prettier 进行代码检查和格式化。
    • 代码审核:通过 Pull Request(PR)进行代码审核,确保代码质量和一致性。
  2. 持续集成(CI)

    • 自动化测试:在 CI 流程中运行单元测试、集成测试、端到端测试。使用工具如 Jest、Mocha、Cypress 进行测试。
    • 构建和打包:自动执行构建脚本,生成优化后的生产代码。使用构建工具如 Webpack 或 Vite 进行打包。
    • 代码质量检查:在 CI 流程中执行代码质量检查,确保代码符合质量标准。

    示例 GitHub Actions 配置文件

    name: CI Pipeline
    
    on:
      push:
        branches:
          - main
    
    jobs:
      build:
        runs-on: ubuntu-latest
    
        steps:
          - name: Checkout code
            uses: actions/checkout@v3
    
          - name: Install dependencies
            run: npm install
    
          - name: Run tests
            run: npm test
    
          - name: Build project
            run: npm run build
    
          - name: Deploy
            if: github.ref == 'refs/heads/main'
            run: npm run deploy
    
  3. 持续部署(CD)

    • 自动部署:在 CI 流程成功后,自动将构建生成的代码部署到生产环境。配置自动化部署脚本,确保构建结果无缝部署。
    • 环境配置:使用环境变量和配置文件管理不同环境(开发、测试、生产)的配置。
    • 回滚机制:设计和实施回滚机制,以便在部署失败时快速恢复到之前的稳定版本。

    示例 Netlify 配置

    [build]
      publish = "dist"
      command = "npm run build"
    
    [[redirects]]
      from = "/old-route"
      to = "/new-route"
      status = 301
    
  4. 监控和通知

    • 应用监控:使用工具如 Sentry、New Relic、LogRocket 监控应用性能和错误。
    • 通知机制:配置通知机制,及时接收构建、测试和部署状态的通知。例如,通过 Slack、Email 或其他消息渠道接收 CI/CD 流程的通知。
  5. 文档和培训

    • 文档:编写和维护部署流程、操作指南和故障排查文档,以便团队成员了解部署流程。
    • 培训:对团队成员进行自动化部署工具和流程的培训,确保每个人都能顺利使用和维护自动化部署系统。

3. 流程设计

  1. 开发阶段

    • 本地开发:开发人员在本地环境中进行开发和测试。
    • 代码提交:将代码提交到版本控制系统,触发 CI 流程。
  2. 持续集成阶段

    • 构建和测试:CI 工具自动构建和测试代码,生成测试报告。
    • 代码审核:进行代码审核,确保代码质量。
  3. 持续部署阶段

    • 构建优化:将构建结果部署到测试环境进行验证。
    • 生产部署:在测试通过后,将代码部署到生产环境。
  4. 后续监控

    • 性能监控:监控应用的性能和健康状态。
    • 问题修复:根据监控数据和用户反馈修复问题,进行迭代改进。

29. 怎么分析页面加载慢的原因?

难度:3 · 类型:QA

题目要点

  1. 网络面板:检查资源加载时间、请求时间和阻塞请求。
  2. 性能面板:记录和分析页面加载过程的性能数据,识别重绘和回流问题。
  3. 网络和服务器:检查网络速度、服务器性能和 CDN 配置。
  4. 前端资源:优化资源大小、延迟加载和按需加载策略。
  5. 前端代码:检查 JavaScript 和 CSS 性能,优化代码执行和渲染。
  6. 监控和调优:使用性能分析工具,持续监控和优化页面性能。
参考答案

分析页面加载慢的原因通常涉及多个方面,包括网络请求、资源加载、渲染过程等。

以下是一个系统化的分析步骤,可以帮助你找出页面加载慢的原因:

1. 使用浏览器开发者工具

网络面板(Network)

  1. 查看资源加载时间

    • 打开浏览器开发者工具的网络面板,查看每个资源(如 HTML、CSS、JS、图片)的加载时间。
    • 检查是否有资源加载缓慢或失败的情况。
  2. 分析请求时间

    • 观察每个请求的“Waiting (TTFB)”时间,了解服务器响应时间。
    • 检查资源的缓存策略,确保静态资源使用了合适的缓存控制。
  3. 识别阻塞请求

    • 查看是否有某些资源(如 JS 文件)阻塞了其他资源的加载。

性能面板(Performance)

  1. 记录性能分析

    • 使用性能面板记录页面加载过程的性能数据。
    • 分析关键时间节点(如 DOM 内容加载、页面加载完成、首次绘制等)。
  2. 分析重绘和回流

    • 识别是否有频繁的重绘和回流,这可能会影响页面渲染性能。
    • 检查布局、样式计算和绘制的时间。

2. 检查网络和服务器

  1. 网络速度

    • 测试页面在不同网络条件下的加载速度,检查是否有网络带宽问题。
  2. 服务器性能

    • 检查服务器的响应时间和负载情况。
    • 确保服务器配置和优化良好,以处理请求并提供响应。
  3. CDN 使用

    • 如果使用了 CDN,检查是否配置正确且有效地加速了资源的分发。

3. 分析前端资源

  1. 优化资源

    • 确保图片、视频等媒体资源经过压缩和优化。
    • 使用合适的格式和大小,以减少加载时间。
  2. 减少资源大小

    • 进行 JavaScript 和 CSS 的压缩和合并。
    • 删除不必要的库和模块,减少代码体积。
  3. 延迟加载

    • 实施按需加载和延迟加载策略(例如,懒加载图片和异步加载 JavaScript 文件)。

4. 检查前端代码

  1. JavaScript 性能

    • 检查 JavaScript 代码是否存在性能瓶颈,如大量的 DOM 操作、复杂的计算或无效的循环。
    • 使用 Web Workers 处理耗时操作,避免阻塞主线程。
  2. CSS 性能

    • 确保 CSS 不存在复杂的选择器或过多的层叠,这可能影响渲染性能。
    • 使用 will-change 属性来优化即将发生变化的元素。

5. 监控和调优

  1. 使用 Lighthouse

    • 使用 Lighthouse 工具分析页面性能,获取性能优化建议。
  2. 持续监控

    • 定期使用性能监控工具(如 Google PageSpeed Insights、WebPageTest)来检查和优化页面性能。

30. 如何监控前端页面的崩溃?

难度:3.5 · 类型:QA

题目要点

通过结合使用 window.onerrorunhandledrejectiontry...catch 和各种错误监控工具,可以有效地捕获和报告前端页面的崩溃和错误。这些方法帮助开发者及时发现和解决问题,提高用户体验。

参考答案

监控前端页面的崩溃通常涉及捕获和报告 JavaScript 错误、性能问题以及页面状态。

以下是一些常见的方法和工具来实现这些监控:

1. 使用 window.onerror

  • 定义window.onerror 是一个全局事件处理程序,用于捕获 JavaScript 执行时的错误。
  • 实现
    window.onerror = function(message, source, lineno, colno, error) {
      // 处理错误信息,例如发送到服务器
      console.error('Error captured:', { message, source, lineno, colno, error });
      // 可以通过 HTTP 请求将错误信息发送到日志服务器
    };
    

2. 使用 window.addEventListener('unhandledrejection')

  • 定义:捕获未处理的 Promise 拒绝(rejections)。
  • 实现
    window.addEventListener('unhandledrejection', function(event) {
      // 处理 Promise 拒绝,例如发送到服务器
      console.error('Unhandled rejection:', event.reason);
      // 可以通过 HTTP 请求将错误信息发送到日志服务器
    });
    

3. 使用 try...catch

  • 定义:在可能出现错误的代码块中使用 try...catch 捕获异常。
  • 实现
    try {
      // 可能会抛出错误的代码
    } catch (error) {
      // 处理错误,例如发送到服务器
      console.error('Caught error:', error);
      // 可以通过 HTTP 请求将错误信息发送到日志服务器
    }
    

4. 使用错误监控工具

  • Sentry:捕获前端错误并提供详细的堆栈跟踪和上下文信息。
  • New Relic:提供全面的前端性能监控和错误捕获。
  • Rollbar:实时捕获和报告 JavaScript 错误和异常。
  • LogRocket:记录用户会话并捕获前端错误。

5. 性能监控和日志

  • 浏览器开发者工具:使用 DevTools 监控网络请求、性能和资源使用。
  • Web Vitals:跟踪核心 Web Vitals 指标(如 LCP、FID、CLS)来发现性能问题。
  • Custom Logging:自定义日志记录功能,将应用程序状态和错误发送到日志服务器。

6. 网络请求监控

  • 自定义错误日志:在 JavaScript 错误处理程序中,通过 HTTP 请求将错误信息发送到远程服务器进行存储和分析。
  • 日志服务器:维护一个后端日志服务器,用于存储和分析前端错误和崩溃数据。

31. 实现 table header 吸顶, 有哪些实现方式?

难度:3 · 类型:QA

题目要点

  • position: sticky:简单且现代的 CSS 解决方案,易于实现,但兼容性需确认。
  • JavaScript 滚动监听:更灵活,可自定义,但需处理额外的滚动事件。
  • 第三方库:提供丰富的功能,适合需要更多特性的场景,但增加了依赖。
参考答案

实现 table header 吸顶的效果有几种方法,主要包括使用 CSS 的 position: sticky 属性、JavaScript 的滚动监听、以及使用第三方库。

以下是常见的实现方式:

1. 使用 position: sticky

  • 定义:CSS position: sticky 属性使元素在滚动到一定位置时固定在视口的某个位置。
  • 实现
    <style>
      .table-container {
        max-height: 400px; /* 限制表格容器的高度 */
        overflow-y: auto;  /* 启用垂直滚动条 */
      }
      table {
        width: 100%;
        border-collapse: collapse;
      }
      th {
        position: sticky;
        top: 0; /* 吸顶位置 */
        background: #f1f1f1; /* 背景色以遮盖滚动内容 */
        z-index: 1; /* 确保 th 在内容之上 */
      }
    </style>
    <div class="table-container">
      <table>
        <thead>
          <tr>
            <th>Header 1</th>
            <th>Header 2</th>
            <th>Header 3</th>
          </tr>
        </thead>
        <tbody>
          <!-- Table rows go here -->
        </tbody>
      </table>
    </div>
    

2. 使用 JavaScript 滚动监听

  • 定义:通过监听滚动事件,将表头的 position 设置为 fixed 来实现吸顶效果。
  • 实现
    <style>
      .table-container {
        max-height: 400px; /* 限制表格容器的高度 */
        overflow-y: auto;  /* 启用垂直滚动条 */
      }
      table {
        width: 100%;
        border-collapse: collapse;
      }
      th.fixed {
        position: fixed;
        top: 0; /* 吸顶位置 */
        background: #f1f1f1; /* 背景色以遮盖滚动内容 */
        z-index: 1; /* 确保 th 在内容之上 */
      }
    </style>
    <div class="table-container" id="tableContainer">
      <table>
        <thead>
          <tr id="header">
            <th>Header 1</th>
            <th>Header 2</th>
            <th>Header 3</th>
          </tr>
        </thead>
        <tbody>
          <!-- Table rows go here -->
        </tbody>
      </table>
    </div>
    
    <script>
      const tableContainer = document.getElementById('tableContainer');
      const header = document.getElementById('header');
    
      tableContainer.addEventListener('scroll', () => {
        if (tableContainer.scrollTop > 0) {
          header.classList.add('fixed');
        } else {
          header.classList.remove('fixed');
        }
      });
    </script>
    

3. 使用第三方库

  • 例子:可以使用像 DataTablesSticky Table Headers 这样的库,它们提供了更复杂的表头固定功能和其他表格功能。
  • 实现:通常只需要引入库和配置选项即可实现固定表头的效果。例如,使用 DataTables 的配置选项:
    $(document).ready(function() {
      $('#example').DataTable({
        scrollY: '400px',
        scrollCollapse: true,
        paging: false
      });
    });
    

32. 前端单页应用 History 路由模式, 需要如何配置 nginx?

难度:2 · 类型:QA

题目要点

为支持 History 路由模式的 SPA,需要在 Nginx 配置中使用 try_files 指令,将所有未匹配到实际文件的请求重定向到 index.html。这样,前端路由才能正确处理路径。

参考答案

前端单页应用使用 History 路由模式,需要将路由的控制交给前端,而不是后端处理,因此需要配置 nginx 让其将所有请求都指向 index.html 页面,让前端来处理路由。

具体配置可以参考以下 nginx 配置:

bashCopy codeserver {
    listen       80;
    server_name  example.com; # 域名或 IP 地址

    location / {
        root   /path/to/your/app; # 项目根目录
        index  index.html;
        try_files $uri $uri/ /index.html; # 将所有请求都指向 index.html 页面
    }
}

以上配置中,try_files 指令会尝试将请求映射到对应的文件,如果找不到,则会将请求转发到 /index.html 页面,由前端来处理路由。同时,root 指令需要指向前端应用的根目录。

33. 前端如何实现截图?

难度:2.5 · 类型:QA

题目要点

  • html2canvashtml2pdf:适用于将 HTML 元素转换为图像或 PDF 文件。
  • canvas API:用于绘制和截取自定义图像。
  • getDisplayMedia:适用于捕获屏幕内容,生成截图。
  • 浏览器扩展:提供简单的图形界面进行截图操作。
参考答案

下面是一些常见的实现方案:

1. 使用 html2canvas

  • 描述html2canvas 是一个流行的 JavaScript 库,可以将 HTML 元素渲染成画布(Canvas),从而实现截图。
  • 使用方法
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.5.0-beta4/html2canvas.min.js"></script>
    <script>
      html2canvas(document.body).then(canvas => {
        document.body.appendChild(canvas); // 将截图的 Canvas 添加到页面中
      });
    </script>
    

2. 使用 html2pdf

  • 描述html2pdf 是基于 html2canvas 的扩展库,用于将 HTML 元素转换为 PDF 文件。
  • 使用方法
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.2/html2pdf.bundle.min.js"></script>
    <script>
      const element = document.getElementById('capture');
      html2pdf().from(element).save('screenshot.pdf');
    </script>
    

3. 使用 canvas API

  • 描述:使用 canvas API 可以手动绘制和截图页面的部分内容。
  • 使用方法
    <canvas id="myCanvas" width="500" height="500"></canvas>
    <script>
      const canvas = document.getElementById('myCanvas');
      const ctx = canvas.getContext('2d');
      // 绘制图像或文本到 Canvas 上
      ctx.fillText('Hello, World!', 10, 50);
      // 获取截图数据 URL
      const dataURL = canvas.toDataURL();
      // 将数据 URL 作为图像显示
      const img = new Image();
      img.src = dataURL;
      document.body.appendChild(img);
    </script>
    

4. 使用 navigator.mediaDevices.getDisplayMedia

  • 描述getDisplayMedia API 可以捕获整个屏幕或应用窗口的内容,适用于录屏或截图。
  • 使用方法
    <script>
      async function captureScreen() {
        try {
          const stream = await navigator.mediaDevices.getDisplayMedia({
            video: { mediaSource: 'screen' }
          });
          const [track] = stream.getVideoTracks();
          const imageCapture = new ImageCapture(track);
          const bitmap = await imageCapture.grabFrame();
          document.body.appendChild(bitmap); // 添加截图到页面中
        } catch (err) {
          console.error('Error: ', err);
        }
      }
      captureScreen();
    </script>
    

5. 使用浏览器扩展

  • 描述:有些浏览器扩展(如 Chrome 的 “Full Page Screen Capture”)可以用于截图整个网页。
  • 使用方法:安装相关扩展,按照扩展的使用说明进行操作。

34. Redux 和 Vuex 的设计思想是什么?

难度:2 · 类型:QA

题目要点

  • Redux 的设计思想强调单一数据源、不可变状态、纯函数、单向数据流和中间件。它的设计理念主要来自于函数式编程,并且适用于复杂和大型的状态管理需求。

  • Vuex 的设计思想则更加贴近 Vue.js 的生态系统,强调集中式存储、状态和变更的分离、同步 mutation 和支持插件。它的设计更适合 Vue.js 应用的开发模式和需求。

虽然 Redux 和 Vuex 在设计上有很多相似之处,如集中式存储和单向数据流,但它们的实现细节和理念也有各自的特点和适用场景。

参考答案

Redux 和 Vuex 都是用于状态管理的库,分别用于 React 和 Vue 框架。它们的设计思想在许多方面有相似之处,但也有各自的独特之处。

下面是对 Redux 和 Vuex 设计思想的详细比较:

Redux

设计思想

  1. 单一数据源

    • Redux 强调应用的所有状态都集中在一个单一的 store 中。这个设计简化了状态的管理和调试。
  2. 状态不可变性

    • Redux 采用不可变数据结构,每次状态的变化都会创建一个新的状态对象,而不是直接修改原有状态。这有助于追踪状态的变化并实现高效的 UI 渲染。
  3. 纯函数(Reducers)

    • 状态的更新由纯函数(称为 reducers)负责。纯函数意味着相同的输入始终产生相同的输出,不依赖于外部状态或副作用。
  4. 单向数据流

    • Redux 遵循单向数据流的原则。数据流动的方向是:action -> reducer -> store -> 视图。通过这种方式,可以明确跟踪数据的变化和流动。
  5. 中间件

    • Redux 支持中间件的机制,可以在 dispatchreducer 之间插入逻辑。这对于处理异步操作和其他副作用很有用。
  6. 可预测的状态管理

    • Redux 的设计使得状态变化变得可预测。状态变化的逻辑集中在 reducers 中,可以通过 action 和 reducer 组合跟踪状态的变化。

适用场景

  • 适用于大型应用或复杂状态管理场景。适合需要高度可控和可预测状态的应用。

Vuex

设计思想

  1. 集中式存储

    • Vuex 提供集中式的状态管理,所有组件的状态都存储在一个全局的 store 中,保证应用的状态集中管理。
  2. 状态、变更和行动分离

    • Vuex 将状态(state)、变更(mutations)、和行动(actions)进行明确的分离。状态是存储的状态,变更通过同步的 mutation 进行,异步操作通过 actions 进行。
  3. Mutation 必须是同步的

    • Vuex 强调 mutation 函数必须是同步的,所有状态的更改都必须通过 mutation,这保证了状态更改的可追踪性和调试性。
  4. 状态可追踪

    • 通过 Vuex 的 store 结构,可以清晰地跟踪状态的变化和应用的状态。
  5. Getter 函数

    • Vuex 提供 getter 函数,允许计算状态的派生数据。它类似于 Vue 的计算属性,用于从 store 的状态派生出新的数据。
  6. 插件系统

    • Vuex 支持插件,可以扩展 Vuex 的功能,例如日志记录、持久化存储等。

适用场景

  • 适用于 Vue.js 应用,特别是中大型应用。适合需要集中式状态管理的场景。

35. 怎么实现同一个链接,PC 访问是 web 应用,而手机打开是一个 H5 应用?

难度:2 · 类型:QA

题目要点

  1. 服务器端检测:根据 User-Agent 处理不同设备的请求。
  2. 客户端检测:在客户端 JavaScript 中根据 navigator.userAgent 进行判断并重定向。
  3. 响应式设计:使用 CSS 媒体查询在同一 URL 上适配不同设备。
  4. 框架支持:使用支持设备适配的前端框架或库。
参考答案

要实现同一个链接在 PC 上打开 Web 应用,在手机上打开 H5 应用,可以利用用户代理检测和条件渲染。以下是常见的实现方法:

1. 服务器端检测用户代理

在服务器端,根据用户的请求头中的 User-Agent 字段来判断是 PC 还是手机访问,并返回不同的应用内容。

示例(Node.js/Express)

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  const userAgent = req.headers['user-agent'];
  const isMobile = /mobile/i.test(userAgent);

  if (isMobile) {
    // 返回 H5 应用
    res.sendFile(__dirname + '/path/to/mobile/index.html');
  } else {
    // 返回 Web 应用
    res.sendFile(__dirname + '/path/to/pc/index.html');
  }
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

2. 客户端检测用户代理

在客户端使用 JavaScript 检测用户设备类型,然后动态加载适合的内容或重定向到不同的页面。

示例(客户端 JavaScript)

if (/mobile/i.test(navigator.userAgent)) {
  // 重定向到 H5 应用
  window.location.href = 'https://m.example.com';
} else {
  // 继续显示 Web 应用
}

3. 使用响应式设计

通过响应式设计来适配不同设备,所有内容都可以在同一 URL 上访问,但会根据设备的屏幕尺寸和特性显示不同的内容。

示例(CSS 媒体查询)

/* 手机样式 */
@media only screen and (max-width: 600px) {
  .web-app {
    display: none;
  }
  .h5-app {
    display: block;
  }
}

/* PC 样式 */
@media only screen and (min-width: 601px) {
  .web-app {
    display: block;
  }
  .h5-app {
    display: none;
  }
}

4. 使用框架或库

有些前端框架和库支持根据设备类型动态加载不同的内容。你可以利用这些工具进行设备适配。

示例(React)

import React from 'react';
import { useMediaQuery } from 'react-responsive';

const App = () => {
  const isMobile = useMediaQuery({ query: '(max-width: 600px)' });

  return (
    <div>
      {isMobile ? <MobileApp /> : <PCApp />}
    </div>
  );
};

36. 如何在前端团队快速落地代码规范

难度:3.5 · 类型:QA

题目要点

通过选择合适的代码规范,配置相关工具,进行培训和文档编写,实施反馈机制,并保持自动化和规范维护,你可以在前端团队中快速有效地落地代码规范。这将有助于提高代码质量,减少错误,并提升团队的开发效率。

参考答案

在前端团队中快速落地代码规范可以显著提升代码质量和团队效率。以下是一些步骤和建议,帮助你在团队中有效地实施代码规范:

1. 确定规范

  1. 选择标准化的规范

    • 选择广泛认可的代码规范,如 Airbnb、Google 或 Standard 的 JavaScript 风格指南。
    • 可以根据项目需求和团队情况自定义规则。
  2. 定义团队规范

    • 根据选择的标准,定义团队特有的规则,例如缩进风格、变量命名规则、代码组织方式等。

2. 工具配置

  1. 使用代码格式化工具

    • Prettier:自动格式化代码,确保一致的代码风格。
    • 配置 .prettierrc 文件以定义格式化规则。
  2. 使用静态代码分析工具

    • ESLint:检测和修复 JavaScript 和 TypeScript 代码中的潜在问题。
    • 配置 .eslintrc 文件以定义 linting 规则。
    • 可以与 Prettier 集成,确保格式和规范一致。
  3. 设置版本控制钩子

    • 使用工具如 HuskyLint-staged,在每次提交代码之前自动运行格式化和 lint 检查。
    • 配置 pre-commit 钩子以确保提交的代码符合规范。

3. 培训与文档

  1. 培训团队成员

    • 组织代码规范培训,确保每个团队成员理解和掌握规范。
    • 分享学习资源和示例代码,帮助团队成员快速适应。
  2. 编写规范文档

    • 创建并维护代码规范文档,详细描述团队的代码风格和最佳实践。
    • 将文档放在团队可以轻松访问的地方,如项目 Wiki 或共享文档库。

4. 实施与反馈

  1. 实施阶段

    • 在新代码中实施规范,并逐步引入到现有代码库。
    • 确保所有团队成员在编写新代码时遵循规范。
  2. 定期审查与反馈

    • 定期进行代码审查,确保代码规范得到了遵守。
    • 鼓励团队成员提供反馈,调整规范以适应实际工作中的问题。
  3. 工具集成

    • 将规范工具集成到 CI/CD 流程中,自动检测代码规范是否符合标准。

5. 自动化与维护

  1. 自动化测试

    • 将格式化和 lint 检查集成到构建和测试流程中,确保每次构建都符合规范。
  2. 持续更新

    • 定期更新规范工具和规则,以适应新的技术和最佳实践。
    • 维护文档和培训资料,确保它们与当前的代码规范保持一致。

37. react 中怎么实现下拉菜单场景,要求点击区域外能关闭下拉组件

难度:3.5 · 类型:QA

题目要点

涉及以下几个步骤:

参考答案

涉及以下几个步骤:

  1. 创建下拉菜单组件
  2. 监听点击事件
  3. 判断点击事件是否在下拉菜单外部

步骤说明

1. 创建下拉菜单组件

import React, { useState, useRef, useEffect } from 'react';

const Dropdown = () => {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef(null);

  const toggleDropdown = () => {
    setIsOpen(!isOpen);
  };

  // 监听点击事件
  useEffect(() => {
    const handleClickOutside = (event) => {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
        setIsOpen(false);
      }
    };

    document.addEventListener('mousedown', handleClickOutside);
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, []);

  return (
    <div ref={dropdownRef}>
      <button onClick={toggleDropdown}>
        Toggle Dropdown
      </button>
      {isOpen && (
        <div className="dropdown-menu">
          <p>Menu Item 1</p>
          <p>Menu Item 2</p>
          <p>Menu Item 3</p>
        </div>
      )}
    </div>
  );
};

export default Dropdown;

2. 监听点击事件

  • useEffect 钩子中,添加一个全局的 mousedown 事件监听器,用于检测点击是否发生在下拉菜单外部。
  • handleClickOutside 函数会检查点击事件的目标是否在下拉菜单外部,如果是则关闭下拉菜单。

3. 判断点击事件是否在下拉菜单外部

  • 使用 useRef 钩子获取下拉菜单组件的引用(dropdownRef)。
  • handleClickOutside 函数中使用 dropdownRef.current.contains(event.target) 来判断点击的目标是否在下拉菜单的 DOM 结构内。

注意事项

  • 确保 ref 正确设置在包含下拉菜单的最外层容器上。
  • 在组件卸载时移除事件监听器,以避免内存泄漏。
  • 在大型应用中,可以考虑使用更复杂的事件处理库或工具来处理全局点击事件。

通过上述步骤,你可以实现点击区域外关闭下拉菜单的功能。

38. 如何解决 node_modules 中,同一个包安装了多个不同版本的问题?

难度:2 · 类型:QA

题目要点

  • 使用工具(如 npm dedupeyarn resolutions)来减少版本冲突。
  • 手动合并或升级依赖包。
  • 考虑使用 pnpm 作为包管理工具,优化包的处理方式。
  • 在可能的情况下,使用 peerDependencies 来减少版本冲突的发生。
参考答案

node_modules 中遇到同一个包的多个版本问题,通常是由于不同依赖包对该包有不同的版本要求。解决这个问题有几个常用的方法:

1. 使用 npmyarn 的解析策略

1.1 使用 npmdedupe

  • npm dedupe 会尝试减少 node_modules 中重复的包版本,使得可能的依赖只会使用一个版本。

    npm dedupe
    

1.2 使用 yarnresolutions

  • yarn 提供了 resolutions 字段,可以强制使用某个版本的依赖包。

    // package.json
    {
      "resolutions": {
        "package-name": "desired-version"
      }
    }
    
    • 使用 yarn install 时,yarn 会使用 resolutions 中指定的版本。

2. 合并和升级

2.1 手动合并依赖

  • 查找所有 package.json 文件中依赖的版本,尝试将它们合并到一致的版本。然后更新 package-lock.jsonyarn.lock 文件。

2.2 升级依赖

  • 如果可能,升级所有依赖到最新版本,以减少版本冲突。例如,使用 npm-check-updates 工具来批量更新依赖。

    npx npm-check-updates -u
    npm install
    

3. 使用 pnpm 作为包管理工具

  • pnpm 使用一种不同的策略来管理包,使得 node_modules 目录中的依赖可以被高效共享,减少版本冲突。

    npm install -g pnpm
    pnpm install
    

4. 定制依赖包版本

4.1 使用 package.jsonpeerDependencies

  • 如果你的项目是一个库或插件,使用 peerDependencies 来指定你支持的包版本,而不是直接依赖它们。这样可以避免在 node_modules 中安装多个版本。

4.2 使用 resolutions (对于 npm 7+)

  • npm 7+ 版本中,也可以使用类似 yarnresolutions 解决版本冲突。需要在 package.json 中设置 overrides

    // package.json
    {
      "overrides": {
        "package-name": "desired-version"
      }
    }
    

39. 如何解决页面请求接口大规模并发问题

难度:3 · 类型:QA

题目要点

解决大规模并发请求问题,结合前端和后端的优化策略,包括请求合并、节流防抖、并发控制、缓存机制等,可以有效降低请求负载,提高系统性能和用户体验。

参考答案

主要包括以下几种策略:

1. 使用防抖和节流

  • 防抖(Debouncing):在一段时间内只执行最后一次请求,适用于用户输入场景。

    function debounce(func, delay) {
      let timer;
      return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => func.apply(this, args), delay);
      };
    }
    
  • 节流(Throttling):限制在一定时间内执行请求,适用于限制频繁的请求。

    function throttle(func, limit) {
      let lastFunc;
      let lastRan;
      return function (...args) {
        const context = this;
        if (!lastRan) {
          func.apply(context, args);
          lastRan = Date.now();
        } else {
          clearTimeout(lastFunc);
          lastFunc = setTimeout(function () {
            if ((Date.now() - lastRan) >= limit) {
              func.apply(context, args);
              lastRan = Date.now();
            }
          }, limit - (Date.now() - lastRan));
        }
      };
    }
    

2. 请求合并

  • 请求去重:避免同一请求被重复发起,通过设置请求唯一标识来防止重复请求。

    const pendingRequests = new Map();
    
    async function fetchData(url, options) {
      const key = `${url}_${JSON.stringify(options)}`;
      if (pendingRequests.has(key)) {
        return pendingRequests.get(key);
      }
    
      const requestPromise = fetch(url, options).finally(() => {
        pendingRequests.delete(key);
      });
      pendingRequests.set(key, requestPromise);
      return requestPromise;
    }
    
  • 批量请求:将多个请求合并为一个请求,通过接口支持批量请求功能来减少请求数量。

3. 后端处理

  • 负载均衡:使用负载均衡器将请求分发到多个服务器,避免单台服务器过载。

  • 缓存:在后端使用缓存(如 Redis)存储重复请求的数据,减少数据库压力。

  • 限流:在后端实施限流策略,控制每秒处理的请求数量,防止系统过载。

4. 异步处理

  • 并发控制:限制并发请求的数量,通过实现并发队列来控制请求的并发数。

    class Queue {
      constructor(limit) {
        this.queue = [];
        this.activeCount = 0;
        this.limit = limit;
      }
    
      add(fn) {
        return new Promise((resolve, reject) => {
          this.queue.push(() => fn().then(resolve).catch(reject));
          this.processQueue();
        });
      }
    
      processQueue() {
        if (this.activeCount < this.limit && this.queue.length > 0) {
          const fn = this.queue.shift();
          this.activeCount++;
          fn().finally(() => {
            this.activeCount--;
            this.processQueue();
          });
        }
      }
    }
    

5. 前端缓存

  • 使用浏览器缓存:设置缓存头(如 Cache-ControlETag)来缓存请求结果,减少重复请求。

  • 本地存储:使用 localStoragesessionStorage 来缓存数据,避免重复请求。

6. 使用 Web Workers

  • Web Workers:将一些计算密集型的请求处理任务放到 Web Worker 中,避免阻塞主线程,提升用户体验。

40. 如何设计一套统计全站请求耗时的工具

难度:3 · 类型:QA

题目要点

通过前端拦截请求、后端日志记录与分析、以及数据可视化工具,可以有效地统计和监控全站请求耗时。

参考答案

以下是一个解决方案的示例:

1. 需求分析

  • 统计请求时间:记录每个请求的发起和完成时间,计算耗时。
  • 统计请求数量:记录总请求数量,分类型统计(如 GET、POST)。
  • 错误监控:监控失败的请求及其错误信息。
  • 展示统计信息:提供统计数据的可视化界面,如图表、表格等。
  • 报警机制:在请求超时或异常时发出警报。

2. 实现步骤

前端实现

  1. 拦截请求

    使用 XMLHttpRequestfetch 的拦截器,捕获请求和响应。

    // 对 XMLHttpRequest 的封装
    (function() {
      const originalSend = XMLHttpRequest.prototype.send;
      XMLHttpRequest.prototype.send = function(...args) {
        const xhr = this;
        const startTime = performance.now();
        xhr.addEventListener('loadend', () => {
          const endTime = performance.now();
          const duration = endTime - startTime;
          logRequest(xhr.responseURL, xhr.method, duration, xhr.status);
        });
        originalSend.apply(xhr, args);
      };
    })();
    
    // 对 fetch 的封装
    (function() {
      const originalFetch = window.fetch;
      window.fetch = function(...args) {
        const startTime = performance.now();
        return originalFetch(...args).then(response => {
          const endTime = performance.now();
          const duration = endTime - startTime;
          logRequest(response.url, args[1]?.method || 'GET', duration, response.status);
          return response;
        });
      };
    })();
    
  2. 记录日志

    实现 logRequest 函数,将请求信息记录到服务器端。

    function logRequest(url, method, duration, status) {
      navigator.sendBeacon('/log', JSON.stringify({
        url,
        method,
        duration,
        status,
        timestamp: new Date().toISOString()
      }));
    }
    
  3. 显示统计数据

    通过发送定期的请求,将收集的数据发送到服务器,并在服务器端生成统计报告。

后端实现

  1. 数据存储

    选择合适的存储方式(如数据库、日志文件),存储请求日志。

    const express = require('express');
    const app = express();
    const fs = require('fs');
    
    app.use(express.json());
    
    app.post('/log', (req, res) => {
      const logEntry = req.body;
      fs.appendFile('request_logs.json', JSON.stringify(logEntry) + '\n', (err) => {
        if (err) {
          console.error('Failed to write log:', err);
        }
      });
      res.status(200).send('Logged');
    });
    
    app.listen(3000, () => {
      console.log('Server running on port 3000');
    });
    
  2. 数据分析

    实现数据分析和可视化工具,生成统计报告和图表。

    // 数据分析示例(伪代码)
    const logs = readLogs('request_logs.json');
    const stats = analyzeLogs(logs);
    renderStatsToDashboard(stats);
    
  3. 报警机制

    根据设置的阈值(如请求耗时、错误率)进行报警。

    function checkAlerts(stats) {
      if (stats.averageDuration > THRESHOLD_DURATION) {
        sendAlert('High average request duration');
      }
      if (stats.errorRate > THRESHOLD_ERROR_RATE) {
        sendAlert('High error rate');
      }
    }
    

数据可视化

  1. 前端展示

    使用图表库(如 Chart.js、D3.js)展示统计数据。

    <canvas id="request-chart"></canvas>
    <script>
      const ctx = document.getElementById('request-chart').getContext('2d');
      const chart = new Chart(ctx, {
        type: 'line',
        data: {
          labels: ['Jan', 'Feb', 'Mar', 'Apr'], // Example labels
          datasets: [{
            label: 'Request Duration',
            data: [30, 40, 35, 50], // Example data
            borderColor: 'rgba(75, 192, 192, 1)',
            borderWidth: 1
          }]
        }
      });
    </script>
    
  2. 报告生成

    生成详细的性能报告,并提供下载或在线查看的功能。

41. 如果用户说 web 应用感觉很反应慢或者卡顿,该如何排查?

难度:3 · 类型:QA

题目要点

综合使用浏览器工具、代码审查、资源优化、用户反馈和监控工具,可以有效地排查和解决 web 应用的性能问题。

参考答案

可以通过以下步骤进行排查:

1. 性能分析

  • 浏览器开发者工具:使用 Chrome DevTools 或其他浏览器的开发者工具,查看性能面板中的记录,分析 CPU 和内存使用情况。
    • Performance:查看长时间的任务、回流和重绘情况,识别瓶颈。
    • Memory:检查内存使用情况和垃圾回收,查找内存泄漏。
    • Network:监控网络请求时间,查看是否有资源加载缓慢的情况。

2. 网络请求

  • 检查加载时间:使用网络面板查看请求的响应时间和大小,找出慢请求。
  • 分析请求依赖:确保第三方服务和 API 响应迅速且稳定,减少请求次数和大小。

3. 代码审查

  • 长时间运行的脚本:查找和优化执行时间较长的 JavaScript 代码,使用 Web Workers 处理耗时任务。
  • 频繁的 DOM 操作:减少 DOM 操作的次数和复杂度,批量更新 DOM。
  • 事件处理:优化事件处理程序,避免高频率的事件触发(如滚动、输入)。

4. 资源优化

  • 资源大小:检查图片、字体和其他资源的大小,压缩和优化资源。
  • 异步加载:使用懒加载和异步加载技术,避免阻塞渲染。

5. 前端性能优化

  • 缓存:利用浏览器缓存和 CDN 加速静态资源加载。
  • 代码拆分:使用 Webpack 或其他构建工具进行代码拆分,减少初始加载时间。
  • CSS 和 JS 合并:合并 CSS 和 JS 文件,减少 HTTP 请求次数。

6. 用户反馈

  • 重现问题:询问用户操作的具体步骤和使用环境,尝试重现问题。
  • 环境差异:检查不同设备、浏览器和网络环境下的表现,确保兼容性。

7. 使用监控工具

  • 实时监控:使用性能监控工具(如 New Relic、Datadog)收集实时性能数据。
  • 错误跟踪:配置错误监控工具(如 Sentry),捕获和分析 JavaScript 错误和异常。

8. 回归测试

  • 版本回退:测试最近的更改,查看是否新版本引入了性能问题。
  • 逐步回退:逐步回退更改,以确定性能问题的根源。

42. 站点一键换肤的实现方式有哪些?

难度:3 · 类型:QA

题目要点

实现站点一键换肤功能,可以通过以下几种方式:

参考答案

实现站点一键换肤功能,可以通过以下几种方式:

1. 切换 CSS 样式表

方式:

  • 通过 JavaScript 动态切换不同的 CSS 样式表文件。

实现步骤:

  1. <head> 标签中引入多个样式表,使用不同的 id 来标识。

    <link id="theme-style" rel="stylesheet" href="default-theme.css">
    
  2. 使用 JavaScript 动态更改 href 属性来切换样式表。

    function changeTheme(theme) {
      const linkElement = document.getElementById('theme-style');
      linkElement.href = theme + '-theme.css';
    }
    

2. 使用 CSS 变量

方式:

  • 定义 CSS 变量来控制主题样式,通过 JavaScript 动态修改这些变量的值。

实现步骤:

  1. 在 CSS 文件中定义主题变量。

    :root {
      --primary-color: #3498db;
      --background-color: #ecf0f1;
    }
    
    .dark-theme {
      --primary-color: #2c3e50;
      --background-color: #34495e;
    }
    
  2. 使用 JavaScript 动态切换主题类。

    function changeTheme(theme) {
      document.documentElement.className = theme;
    }
    

3. 动态加载样式

方式:

  • 利用 JavaScript 动态加载不同的样式文件或 CSS 规则。

实现步骤:

  1. 创建多个样式文件,并使用 JavaScript 动态加载。

    function loadTheme(theme) {
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = theme + '-theme.css';
      document.head.appendChild(link);
    }
    
  2. 移除当前样式以避免重复加载。

    function changeTheme(theme) {
      const existingLink = document.querySelector('link[href*="-theme.css"]');
      if (existingLink) {
        existingLink.remove();
      }
      loadTheme(theme);
    }
    

4. 使用 JavaScript 插件

方式:

  • 利用现成的 JavaScript 插件或库来实现换肤功能。

实现步骤:

  1. 使用插件,例如 ThemeSwitcher 或类似的工具,根据插件文档进行配置。

  2. 插件通常提供了简单的 API 来更换主题。

    $('#theme-switcher').on('change', function() {
      const selectedTheme = $(this).val();
      themeSwitcher.change(selectedTheme);
    });
    

5. 存储用户设置

方式:

  • 保存用户选择的主题,并在用户重新访问时应用相同的主题。

实现步骤:

  1. 在用户选择主题时,将其存储到 localStoragesessionStorage

    function setTheme(theme) {
      localStorage.setItem('theme', theme);
      changeTheme(theme);
    }
    
  2. 在页面加载时,读取存储的主题并应用。

    document.addEventListener('DOMContentLoaded', function() {
      const savedTheme = localStorage.getItem('theme') || 'default';
      changeTheme(savedTheme);
    });
    

43. 说说你对 package.json 中配置项的了解

难度:1.5 · 类型:QA

题目要点

package.json 是 Node.js 和前端项目中的核心配置文件,定义了项目的元数据、依赖、脚本等信息。

参考答案

package.json 是 Node.js 和前端项目中的核心配置文件,定义了项目的元数据、依赖、脚本等信息。

以下是一些常见的 package.json 配置项及其作用:

1. name

  • 定义:项目名称。
  • 作用:用于标识项目,必须是唯一的。如果你发布到 npm,名称将成为包的标识符。

2. version

  • 定义:项目的版本号。
  • 作用:表示项目的版本,通常遵循 SemVer 语义化版本规范(如 1.0.0)。

3. description

  • 定义:项目的简短描述。
  • 作用:提供有关项目的简要说明。

4. main

  • 定义:项目的入口文件。
  • 作用:指定 Node.js 模块的主文件,默认为 index.js。当使用 require 导入模块时,会加载该文件。

5. scripts

  • 定义:定义可以通过 npm run &lt;script&gt; 命令运行的脚本。

  • 作用:用于自动化任务,如测试、构建和启动项目。常见的脚本包括 starttestbuild 等。

    "scripts": {
      "start": "node index.js",
      "test": "mocha test/",
      "build": "webpack --config webpack.config.js"
    }
    

6. dependencies

  • 定义:项目运行时所需的依赖模块。

  • 作用:列出项目在生产环境中需要的 npm 包。安装项目时,这些依赖会被下载和安装。

    "dependencies": {
      "express": "^4.17.1",
      "lodash": "^4.17.21"
    }
    

7. devDependencies

  • 定义:项目开发时所需的依赖模块。

  • 作用:列出项目在开发和测试过程中需要的 npm 包,如编译器、测试框架等。

    "devDependencies": {
      "webpack": "^5.38.1",
      "babel-loader": "^8.2.2"
    }
    

8. peerDependencies

  • 定义:项目与其他包的兼容版本范围。

  • 作用:指定项目所兼容的其他包的版本范围,用于插件和库的兼容性。

    "peerDependencies": {
      "react": "^17.0.2"
    }
    

9. engines

  • 定义:指定项目需要的 Node.js 版本或其他运行环境。

  • 作用:确保项目在特定版本的 Node.js 上运行,避免不兼容问题。

    "engines": {
      "node": ">=14.0.0"
    }
    

10. license

  • 定义:项目的许可证类型。
  • 作用:指明项目的开源许可证类型,如 MITApache-2.0GPL-3.0 等。

11. author

  • 定义:项目作者的名称和联系信息。
  • 作用:提供有关项目作者的信息。

12. contributors

  • 定义:项目的贡献者列表。
  • 作用:列出为项目做出贡献的人员。

13. repository

  • 定义:项目的代码仓库信息。

  • 作用:指定项目的 Git 仓库 URL,方便其他人访问源代码。

    "repository": {
      "type": "git",
      "url": "https://github.com/username/repo.git"
    }
    

14. keywords

  • 定义:项目相关的关键词。

  • 作用:用于描述项目的关键词,以便搜索引擎和 npm 上更容易找到。

    "keywords": [
      "express",
      "node",
      "web"
    ]
    

15. files

  • 定义:要包含在发布包中的文件列表。

  • 作用:限制发布到 npm 的文件,排除不需要的文件。

    "files": [
      "lib/",
      "dist/"
    ]
    

16. browserslist

  • 定义:指定支持的浏览器版本范围。

  • 作用:用于工具(如 Babel、Autoprefixer)来根据支持的浏览器生成兼容代码。

    "browserslist": [
      "> 1%",
      "last 2 versions",
      "IE 10"
    ]
    

这些配置项在 package.json 文件中定义了项目的基本信息、依赖、脚本等内容,有助于项目的开发、构建、发布和维护。

44. 怎么实现扫码登录?

难度:3 · 类型:QA

题目要点

扫码登录涉及生成二维码、验证用户凭证、用户确认登录及通知客户端等步骤。确保过程中的每一步都有合适的安全措施,以保护用户数据和防止未经授权的访问。

参考答案

扫码登录是一种常见的身份验证方式,允许用户通过扫描二维码来登录应用。其基本实现流程如下:

1. 生成二维码

  1. 用户请求扫码登录

    • 用户点击“扫码登录”按钮。
  2. 生成登录二维码

    • 服务器生成一个唯一的登录凭证(如临时的唯一标识符或会话 ID)。
    • 生成一个包含该凭证的二维码,并将其显示给用户。二维码可以使用图像生成库或 API(如 qrcode 库)生成。
    const qr = require('qrcode');
    const uuid = require('uuid');
    
    const uniqueToken = uuid.v4();
    qr.toDataURL(`https://yourapp.com/login?token=${uniqueToken}`, function (err, url) {
      if (err) throw err;
      // Send the URL to the client to display the QR code
    });
    

2. 扫码和登录验证

  1. 扫描二维码

    • 用户使用手机扫描二维码。
  2. 验证登录请求

    • 移动端应用将扫描到的二维码中的链接(包含的凭证)发送到服务器。
    • 服务器验证凭证的有效性(如检查凭证是否有效、是否过期)。
    app.get('/login', async (req, res) => {
      const token = req.query.token;
      // Verify token and handle login process
    });
    

3. 用户确认登录

  1. 显示登录请求

    • 服务器将扫码请求与用户账户绑定,可能会要求用户确认登录(例如在后台管理系统中)。
  2. 用户确认

    • 用户在后台确认登录请求。

4. 通知移动端

  1. 通知登录结果

    • 一旦用户确认登录,服务器将登录成功的状态通知移动端应用。
  2. 更新用户状态

    • 移动端应用接收到登录成功的通知后,更新用户的登录状态并重定向到应用的主界面。
    app.post('/login/confirm', async (req, res) => {
      const { token } = req.body;
      // Handle login confirmation and update user session
      // Notify mobile app about login success
    });
    

5. 客户端处理

  1. 接收通知

    • 客户端(如 Web 应用)收到登录成功的通知后,更新应用状态并重定向用户。
  2. 更新用户界面

    • 确保用户能够顺利进入应用并显示相应的用户信息。

45. 怎么在前端页面中添加水印?

难度:3.5 · 类型:QA

题目要点

  • CSS 方法:简单易用,适合静态内容,兼容性好。
  • Canvas 方法:灵活,可以创建动态水印,但涉及到绘图,可能对性能有影响。
  • 背景图像:简单但不易于调整文本位置和样式。
  • JavaScript 动态生成:灵活,可操作 DOM,适合动态内容。
参考答案

在前端页面中添加水印可以通过以下几种方法实现:

1. 使用 CSS 实现

使用 CSS 伪元素和 background 属性来添加水印。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Watermark Example</title>
<style>
  .watermarked {
    position: relative;
  }
  .watermarked::before {
    content: "Watermark";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(-30deg);
    font-size: 3rem;
    color: rgba(0, 0, 0, 0.1);
    pointer-events: none;
    z-index: 1000;
  }
</style>
</head>
<body>
<div class="watermarked">
  <!-- Your content here -->
  <p>Some content with a watermark.</p>
</div>
</body>
</html>

2. 使用 Canvas

通过在 canvas 上绘制水印来实现。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Watermark Example</title>
</head>
<body>
<canvas id="watermarkCanvas" width="600" height="400"></canvas>
<script>
  const canvas = document.getElementById('watermarkCanvas');
  const ctx = canvas.getContext('2d');

  // Draw background
  ctx.fillStyle = '#fff';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Draw watermark text
  ctx.font = '48px Arial';
  ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.save();
  ctx.translate(canvas.width / 2, canvas.height / 2);
  ctx.rotate(-Math.PI / 6);
  ctx.fillText('Watermark', 0, 0);
  ctx.restore();
</script>
</body>
</html>

3. 使用 HTML 的 background 属性

将水印作为背景图像设置到页面的某个容器上。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Background Watermark Example</title>
<style>
  .watermarked {
    background-image: url('watermark.png');
    background-repeat: no-repeat;
    background-size: 200px 100px;
    background-position: center;
  }
</style>
</head>
<body>
<div class="watermarked">
  <!-- Your content here -->
  <p>Some content with a watermark.</p>
</div>
</body>
</html>

4. 使用 JavaScript 动态生成水印

通过 JavaScript 在 DOM 中添加水印元素。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Watermark Example</title>
<style>
  .watermark {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    pointer-events: none;
    z-index: 9999;
    text-align: center;
    font-size: 3rem;
    color: rgba(0, 0, 0, 0.1);
    transform: rotate(-30deg);
  }
</style>
</head>
<body>
<div id="content">
  <!-- Your content here -->
  <p>Some content with a watermark.</p>
</div>
<script>
  const watermark = document.createElement('div');
  watermark.className = 'watermark';
  watermark.textContent = 'Watermark';
  document.body.appendChild(watermark);
</script>
</body>
</html>

46. 如何将JavaScript代码解析成抽象语法树(AST)?

难度:1 · 类型:QA

题目要点

  • 选择解析工具:如 Babel、Acorn 或 Esprima。
  • 生成 AST:使用工具的解析方法。
  • 理解 AST 结构:了解常见节点类型和结构。
  • 操作 AST:遍历、修改和分析 AST。
  • 将 AST 转回代码:使用工具将 AST 转换回 JavaScript 代码。

通过以上步骤,你可以将 JavaScript 代码解析成 AST,并基于 AST 进行进一步的分析和操作。

参考答案

将 JavaScript 代码解析成抽象语法树(AST)是实现代码分析、转译、优化等任务的关键步骤。AST 是一种抽象的、结构化的表示代码语法的树形结构,能够帮助我们理解和操作 JavaScript 代码的结构。以下是将 JavaScript 代码解析成 AST 的一般步骤及相关工具:

1. 选择一个解析工具

有几个流行的工具可以将 JavaScript 代码解析成 AST,包括:

  • Babel:一个广泛使用的 JavaScript 编译器,它可以将现代 JavaScript 代码转换成兼容性更好的版本,并生成 AST。
  • Acorn:一个轻量级的 JavaScript 解析器,专注于解析代码并生成 AST。
  • Esprima:一个高性能的 JavaScript 解析器,用于生成 AST。

2. 使用工具生成 AST

以下是使用 Babel、Acorn 和 Esprima 生成 AST 的示例:

Babel 示例

Babel 是一个流行的 JavaScript 编译器,具有强大的插件系统和 AST 生成能力。

// 安装 Babel 解析器
// npm install @babel/parser

const parser = require('@babel/parser');

const code = `const a = 1;`;
const ast = parser.parse(code);

console.log(JSON.stringify(ast, null, 2));
  • 解释:使用 @babel/parserparse 方法将 JavaScript 代码解析为 AST。

Acorn 示例

Acorn 是一个轻量级的 JavaScript 解析器。

// 安装 Acorn
// npm install acorn

const acorn = require('acorn');

const code = `const a = 1;`;
const ast = acorn.parse(code);

console.log(JSON.stringify(ast, null, 2));
  • 解释:使用 acorn.parse 方法将 JavaScript 代码解析为 AST。

Esprima 示例

Esprima 是一个广泛使用的 JavaScript 解析器。

// 安装 Esprima
// npm install esprima

const esprima = require('esprima');

const code = `const a = 1;`;
const ast = esprima.parseScript(code);

console.log(JSON.stringify(ast, null, 2));
  • 解释:使用 esprima.parseScript 方法将 JavaScript 代码解析为 AST。

3. 理解 AST 结构

AST 是一个树状结构,每个节点表示代码中的一种结构或元素。常见的 AST 节点类型包括:

  • Program:表示整个程序。
  • VariableDeclaration:表示变量声明。
  • FunctionDeclaration:表示函数声明。
  • Expression:表示表达式,如算术运算、函数调用等。
  • IfStatementForStatement 等:表示各种控制流语句。

4. 操作 AST

生成 AST 后,可以对其进行各种操作,如:

  • 遍历:访问 AST 中的每个节点,通常使用库如 babel-traverse
  • 修改:在 AST 上进行修改,然后将其重新转换为代码。
  • 分析:基于 AST 分析代码的结构和行为。

5. 将 AST 转回代码

修改 AST 后,可以使用工具将其转换回 JavaScript 代码,例如 Babel 的 @babel/generator

const generator = require('@babel/generator').default;

const { code } = generator(ast);
console.log(code);
  • 解释:使用 @babel/generatordefault 方法将 AST 转换回 JavaScript 代码。

总结

47. token过期后,页面如何实现无感刷新?

难度:3 · 类型:QA

题目要点

通过使用刷新 token 机制和请求拦截器,可以实现 token 过期后的无感刷新,确保用户体验不受影响。如果刷新 token 失败,则通常需要用户重新登录。整个过程对用户是透明的,最大限度地提升了用户体验。

参考答案

当用户的身份验证 token 过期时,可以通过以下方法实现页面的无感刷新(即用户无感知的情况下自动刷新 token 并继续操作),通常是针对基于 JWT(JSON Web Token)的身份验证流程。

实现步骤

  1. 设置刷新 token 机制

    • 短生命周期的访问 token:访问 token 应该有较短的有效期,以确保较高的安全性。
    • 长生命周期的刷新 token:刷新 token 有较长的有效期,用于在访问 token 过期后获取新的访问 token
  2. 拦截请求

    • 使用拦截器(如 Axios 拦截器)来监控所有的 HTTP 请求。每次请求时,检查 token 是否存在且有效。
    • 在拦截器中处理 401 Unauthorized 响应,这是 token 过期的常见标志。
  3. 尝试获取新 token

    • 当收到 401 响应时,拦截器会自动发起刷新 token 的请求。使用保存的刷新 token 通过专门的 API 来获取新的访问 token
    • 如果刷新成功,更新存储中的 token(如 localStoragesessionStorage),并重试原来的请求。
  4. 无感刷新 token 并重发请求

    • 如果刷新 token 成功,则将新的访问 token 附加到原始请求的头部,然后重新发起该请求。用户在这个过程中不会感知到 token 的刷新和请求的重试。
  5. 处理刷新失败

    • 如果刷新 token 失败(如刷新 token 也过期了),则通常需要让用户重新登录。此时可以重定向到登录页面或显示提示信息。

示例代码(使用 Axios 拦截器)

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  headers: {
    'Content-Type': 'application/json',
  },
});

// 请求拦截器,添加 token
apiClient.interceptors.request.use(config => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`;
  }
  return config;
}, error => {
  return Promise.reject(error);
});

// 响应拦截器,处理 401 错误并尝试刷新 token
apiClient.interceptors.response.use(response => {
  return response;
}, async error => {
  const originalRequest = error.config;
  if (error.response.status === 401 && !originalRequest._retry) {
    originalRequest._retry = true;
    try {
      const refreshToken = localStorage.getItem('refreshToken');
      const response = await axios.post('https://api.example.com/refresh-token', {
        refreshToken: refreshToken,
      });
      const newToken = response.data.accessToken;
      localStorage.setItem('accessToken', newToken);
      originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
      return apiClient(originalRequest); // 重试原来的请求
    } catch (refreshError) {
      // 刷新 token 失败,可能需要重新登录
      console.log('Refresh token failed:', refreshError);
      // 可以在这里重定向到登录页面
    }
  }
  return Promise.reject(error);
});

export default apiClient;

48. web 应用中如何对静态资源加载失败的场景做降级处理?

难度:3 · 类型:QA

题目要点

对静态资源加载失败进行降级处理可以通过备用资源、动态加载、服务工作者和功能降级等策略来实现。选择合适的方法可以提升应用的稳定性和用户体验,确保即使在资源加载失败时,用户也能获得良好的体验。

参考答案

在 Web 应用中,对静态资源加载失败的场景进行降级处理是确保应用稳定性和用户体验的关键。以下是一些常见的策略和方法:

1. 使用备用资源

  • 图片

    • <img> 标签中使用 onerror 事件处理程序,当图片加载失败时,替换为备用图片。
    <img src="main-image.jpg" alt="Image" onerror="this.src='fallback-image.jpg';">
    
  • CSS

    • 对于 CSS 文件,可以在 <link> 标签中设置备用 CSS 文件,但这种方式不如 JavaScript 的处理直接。通常建议使用 JavaScript 进行处理。
    <link rel="stylesheet" href="main.css" onerror="this.href='fallback.css';">
    

2. JavaScript 动态加载

  • 动态脚本加载

    • 使用 JavaScript 动态加载资源并处理加载失败情况。例如,可以使用 fetchXMLHttpRequest 加载 JavaScript 文件或其他资源,并在失败时加载备用资源。
    function loadScript(url, fallbackUrl) {
        const script = document.createElement('script');
        script.src = url;
        script.onerror = () => {
            script.src = fallbackUrl;
            document.head.appendChild(script);
        };
        document.head.appendChild(script);
    }
    
    loadScript('main-script.js', 'fallback-script.js');
    
  • 动态样式表加载

    • 类似地,可以动态加载样式表并处理加载失败。
    function loadStylesheet(url, fallbackUrl) {
        const link = document.createElement('link');
        link.rel = 'stylesheet';
        link.href = url;
        link.onerror = () => {
            link.href = fallbackUrl;
        };
        document.head.appendChild(link);
    }
    
    loadStylesheet('main-styles.css', 'fallback-styles.css');
    

3. 利用Service Workers

  • 缓存和离线处理
    • 使用 Service Workers 来缓存静态资源,并提供离线访问或备用资源。在资源加载失败时,服务工作者可以提供缓存中的版本或备用资源。
    // Example service worker script
    self.addEventListener('install', (event) => {
        event.waitUntil(
            caches.open('my-cache').then((cache) => {
                return cache.addAll([
                    '/main.css',
                    '/fallback.css',
                    '/main.js',
                    '/fallback.js'
                ]);
            })
        );
    });
    
    self.addEventListener('fetch', (event) => {
        event.respondWith(
            caches.match(event.request).then((response) => {
                return response || fetch(event.request).catch(() => {
                    return caches.match('/fallback.css'); // Or fallback.js
                });
            })
        );
    });
    

4. Graceful Degradation

  • 功能降级
    • 设计时考虑功能降级,确保核心功能在静态资源加载失败时仍然可用。例如,提供基本功能和备用界面,即使某些样式或脚本没有加载成功。

5. 用户提示

  • 在静态资源加载失败时,向用户显示提示信息或错误页面,以告知他们发生了问题并提供解决方案或备用操作。

49. 如何搭建一套灰度系统?

难度:3.5 · 类型:QA

题目要点

  • 目标明确:灰度系统的核心目标是降低发布风险和收集用户反馈。
  • 架构支持:灰度发布依赖于系统架构的设计,微服务和支持多版本并存的架构非常关键。
  • 监控与回滚:实时监控和快速回滚机制是灰度发布成功的关键。
  • 工具与流程:结合 CI/CD、监控和 A/B 测试工具,打造自动化、可扩展的灰度发布流程。
参考答案

搭建一套灰度发布系统涉及多个方面的技术和流程,目的是在发布新版本时,能够逐步、部分地将新功能或改动推送给用户,以降低发布的风险。

以下是搭建的一般步骤和要点:

1. 明确灰度发布的需求和目标

  • 降低风险:避免一次性发布导致的全局性错误影响所有用户。
  • 收集反馈:逐步推出新功能,观察用户行为和收集反馈,及时进行调整。
  • 验证性能和稳定性:在小范围内测试新版本的性能和稳定性。

2. 架构设计

  • 服务分层:将应用分成多个服务或模块,每个服务独立发布,方便单独进行灰度发布。
  • 支持多版本并存:确保系统能同时运行多个版本的新旧功能,以便不同用户访问到不同版本。

3. 用户分组策略

  • 按用户分组:根据用户的特征(如地域、设备类型、用户等级等)或随机分配,决定哪些用户先接收到新版本。
  • 流量分配:通过配置逐步增加新版本的流量比例。例如,先让 5% 的用户使用新版本,然后观察反馈,逐步增加到 10%、20% 等。

4. 灰度发布系统的功能

  • 流量调度:能够动态调整不同版本的流量占比,通常由一个流量调度模块控制。
  • 用户分组管理:可以管理用户分组,并将用户分配到对应的版本。
  • 监控和日志收集:实时监控系统的性能指标(如请求响应时间、错误率、资源使用情况等),并收集用户行为日志。
  • 自动回滚:当检测到新版本出现问题时,系统可以自动回滚到稳定的旧版本。
  • A/B 测试:结合 A/B 测试工具,进行功能对比测试,进一步细化灰度策略。

5. 工具与技术栈

  • 负载均衡器:使用负载均衡器(如 Nginx、HAProxy)进行流量分配,决定哪些请求应该分配到新版本。
  • 微服务架构:使用 Kubernetes、Docker 等技术支持微服务架构,方便独立部署和灰度发布。
  • CI/CD 工具:结合 Jenkins、GitLab CI/CD、GitHub Actions 等工具进行自动化构建、测试和部署。
  • 监控系统:使用 Prometheus、Grafana、ELK 等工具进行系统监控和日志分析。
  • A/B 测试工具:如 Google Optimize、Optimizely,结合灰度发布进行用户体验的差异化测试。

6. 实施灰度发布

  • 部署基础设施:搭建好灰度发布的基础设施,包括流量调度、监控、日志收集等模块。
  • 制定灰度策略:根据业务需求,制定详细的灰度发布策略,包括用户分组、流量占比、回滚条件等。
  • 逐步推进:从小规模用户开始,逐步扩大灰度范围,并观察各项指标以确保系统稳定。
  • 持续监控与反馈:在整个灰度发布期间持续监控系统表现,收集用户反馈,及时作出调整或回滚。

7. 回滚策略

  • 快速回滚机制:在发布过程中出现问题时,灰度系统应能快速回滚到上一稳定版本。
  • 保持数据一致性:确保新版本的数据格式与旧版本兼容,回滚时不会导致数据丢失或不一致。

8. 总结与优化

  • 记录发布过程:详细记录每次灰度发布的过程、问题和解决方案,为后续发布积累经验。
  • 优化灰度系统:根据发布过程中的经验,不断优化灰度系统,提升系统的健壮性和可操作性。

50. 前端项目里,一般对请求 request 做哪些统一的封装?

难度:1.5 · 类型:QA

题目要点

  • 统一配置:集中管理基础配置,如 baseURL 和 headers。
  • 请求拦截器:在请求发出前处理如添加认证 token。
  • 响应拦截器:处理响应数据和错误。
  • 错误处理:集中处理错误信息。
  • 请求方法封装:简化常见请求类型的调用。
  • 统一响应格式:确保响应数据格式一致。
  • 请求超时和重试:处理超时和重试机制。
参考答案

在前端项目中,对请求(request)的统一封装可以提高代码的可维护性、复用性,并简化对请求的管理。

以下是一些常见的请求封装做法:

1. 统一配置

目的:集中管理所有请求的基础配置,如 baseURL、请求头、超时设置等。

import axios from 'axios';

// 创建 Axios 实例
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' }
});

export default apiClient;

2. 请求拦截器

目的:在请求发出之前,统一处理请求的设置,如添加认证 token、处理请求参数等。

apiClient.interceptors.request.use(
  config => {
    // 添加认证 token
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  error => Promise.reject(error)
);

3. 响应拦截器

目的:统一处理响应数据和错误,简化处理逻辑,如格式化数据、全局错误处理等。

apiClient.interceptors.response.use(
  response => response.data, // 只返回数据部分
  error => {
    // 处理 HTTP 错误
    console.error('API Error:', error.response || error.message);
    return Promise.reject(error);
  }
);

4. 错误处理

目的:集中处理所有请求错误,例如展示用户友好的错误信息。

function handleError(error) {
  if (error.response) {
    // 请求已发出,但服务器响应状态码不在 2xx 范围内
    console.error('Server Error:', error.response.data);
  } else if (error.request) {
    // 请求已发出,但没有收到响应
    console.error('Network Error:', error.request);
  } else {
    // 其他错误
    console.error('Error:', error.message);
  }
}

// 在响应拦截器中调用
apiClient.interceptors.response.use(
  response => response.data,
  error => {
    handleError(error);
    return Promise.reject(error);
  }
);

5. 请求方法的封装

目的:创建通用的方法来处理特定的请求类型(GET、POST、PUT、DELETE),简化请求调用。

const api = {
  get(url, params) {
    return apiClient.get(url, { params });
  },
  post(url, data) {
    return apiClient.post(url, data);
  },
  put(url, data) {
    return apiClient.put(url, data);
  },
  delete(url, params) {
    return apiClient.delete(url, { params });
  }
};

export default api;

6. 统一的请求响应格式

目的:保持响应数据格式的一致性,通常是将响应数据封装在一个特定的结构中,便于处理。

apiClient.interceptors.response.use(
  response => {
    // 例如:{ code: 200, data: ..., message: ... }
    if (response.data.code === 200) {
      return response.data.data;
    } else {
      throw new Error(response.data.message || 'Unknown error');
    }
  },
  error => {
    handleError(error);
    return Promise.reject(error);
  }
);

7. 请求超时和重试机制

目的:处理请求超时的情况,和自动重试机制。

import axiosRetry from 'axios-retry';

// 配置 Axios 重试
axiosRetry(apiClient, { retries: 3, retryDelay: axiosRetry.exponentialDelay });

// 在拦截器中设置超时
apiClient.defaults.timeout = 15000;

51. 如何封装一个请求,让其多次调用的时候,实际只发起一个请求的时候,返回同一份结果?

难度:3.5 · 类型:QA

题目要点

  • 单一请求:确保相同请求的多次调用返回相同的 Promise,避免发出多个网络请求。
  • 缓存管理:请求完成后,删除缓存,避免长时间缓存未清理。
  • 错误处理:处理请求失败情况,并清理缓存以便重新请求。
参考答案

封装一个请求使其在多次调用时只发起一次请求,并返回相同结果,通常是通过请求去重(debouncing)来实现的。这种功能对于避免重复的网络请求、提高性能和减少不必要的负载非常有用。

同时,我们需要确保在请求完成之前,对相同请求的重复调用都会共享相同的请求 Promise。避免出现连续发出相同的请求,在第一个请求尚未完成时,那么可能会发出多个请求的情况。

可以通过以下步骤来实现这个功能:

1. 使用一个缓存机制

我们可以使用 JavaScript 对象或 Map 来缓存已经发起的请求,并在 subsequent 请求中返回缓存的结果。缓存的关键是确保相同的请求参数对应同一个缓存条目。

2. 创建请求缓存封装

以下是一个基于 axios 的请求去重的封装示例:

import axios from 'axios';

// 请求缓存
const requestCache = new Map();

async function fetchData(url, params) {
  // 生成缓存 key
  const cacheKey = `${url}?${new URLSearchParams(params).toString()}`;

  // 检查缓存中是否已有数据
  if (requestCache.has(cacheKey)) {
    return requestCache.get(cacheKey);
  }

  // 创建请求 Promise
  const requestPromise = axios.get(url, { params })
    .then(response => {
      // 请求成功,存储结果
      requestCache.delete(cacheKey); // 请求完成后,移除缓存
      return response.data;
    })
    .catch(error => {
      // 请求失败,清除缓存
      requestCache.delete(cacheKey);
      throw error;
    });

  // 存储请求 Promise
  requestCache.set(cacheKey, requestPromise);

  // 返回 Promise
  return requestPromise;
}

export default fetchData;

注意事项:

  • 缓存请求 Promise:每个请求的 Promise 被缓存到 requestCache 中。后续的相同请求会返回这个缓存的 Promise。

  • 请求完成后移除缓存:请求成功或失败后,删除缓存,以防止缓存中的 Promise 长时间存在,避免内存泄漏。

  • 请求失败处理:如果请求失败,清理缓存并抛出错误,以便后续调用可以重新发起请求。

3. 使用请求缓存

使用封装好的 fetchData 函数来发起请求。多次调用相同的请求 URL 和参数只会发起一次网络请求,并返回相同的结果。

import fetchData from './fetchData';

// 使用示例
fetchData('https://api.example.com/data', { id: 1 })
  .then(data => console.log(data))
  .catch(error => console.error(error));

// 再次调用相同的请求
fetchData('https://api.example.com/data', { id: 1 })
  .then(data => console.log(data)) // 共享相同的请求结果
  .catch(error => console.error(error));

52. 你参与过哪些前端基建方面的建设?

难度:4 · 类型:QA

题目要点

前端基建涵盖了从项目架构、构建工具配置到开发环境、自动化流程和安全等多个方面。良好的前端基建可以提升开发效率、代码质量和系统性能,为项目的长期维护和扩展打下坚实的基础。

参考答案

可以从以下方面进行回答:

1. 项目架构

  • 目录结构:定义项目文件和目录的组织结构,确保项目的可维护性和可扩展性。
  • 代码规范:统一代码风格和规范,使用 ESLint、Prettier 等工具进行代码检查和格式化。
  • 模块化:采用模块化设计,将代码拆分为独立的模块,提高可重用性和可维护性。
  • 脚手架:自动生成项目的基础结构,包括目录结构、配置文件、示例代码等。

2. 构建工具和配置

  • 构建工具:使用工具如 Webpack、Vite、Rollup 等进行项目的构建和打包。
  • 配置管理:配置构建工具以支持各种功能,如代码分割、热重载、环境变量等。
  • 优化:配置代码压缩、缓存策略、Tree Shaking 等,提升构建产物的性能和效率。

3. 开发环境

  • 开发服务器:设置本地开发服务器,支持热重载和调试功能。
  • 环境配置:管理开发、测试、生产环境的配置和变量。

4. 包管理

  • 依赖管理:使用 npm、Yarn 或 pnpm 等工具管理项目依赖,确保依赖版本一致性。
  • 发布管理:管理和发布自定义的 npm 包或组件库。

5. 自动化流程

  • CI/CD:配置持续集成(CI)和持续部署(CD)流程,自动化构建、测试和部署。
  • 测试:集成单元测试、集成测试和端到端测试工具,如 Jest、Cypress 等,确保代码质量。

6. 代码质量

  • 静态分析:使用 ESLint、TSLint 等工具进行代码静态检查。
  • 测试覆盖率:监控测试覆盖率,确保关键代码路径被充分测试。

7. 文档

  • 代码文档:编写和维护项目文档,包括 API 文档、开发指南和使用说明。
  • 自动化文档生成:使用工具如 Storybook、JSDoc 等生成组件库和 API 文档。

8. 组件库

  • 设计系统:构建和维护一套一致的设计系统和组件库,提高开发效率和界面一致性。
  • 共享组件:创建和管理共享组件,促进代码复用。

9. 性能优化

  • 前端性能:优化页面加载速度、响应时间和渲染性能。
  • 网络请求:管理和优化网络请求策略,减少请求次数和数据传输量。

10. 安全

  • 安全最佳实践:遵循前端安全最佳实践,如防范 XSS 和 CSRF 攻击。
  • 敏感数据保护:确保敏感数据不被暴露或滥用。

53. web 网页如何禁止别人移除水印

难度:4 · 类型:QA

题目要点

使用 MutationObserver 这个较新的浏览器API,来检测DOM元素的删除事件、以及DOM元素属性的修改事件。

参考答案

防止DOM被删除

为了防止水印被删除,可以利用 MutationObserver API 监听 DOM 变化。MutationObserver 可以监控 DOM 树的变化并触发回调函数。回调函数可以用于检测水印是否被移除,并采取相应的措施进行恢复。

以下是一个示例代码,演示了如何使用 MutationObserver 监听 DOM 变化并检测水印的删除:

// 目标节点
const targetNode = document.body;

// 创建 MutationObserver 实例
const observer = new MutationObserver(mutationsList => {
    mutationsList.forEach(mutation => {
        // 检查是否有子节点被删除
        if (mutation.removedNodes.length > 0) {
            // 检查被删除的节点是否为水印
            // 如果是,则重新插入水印元素
            // targetNode.appendChild(watermarkElement);
        }
    });
});

// 配置 MutationObserver
const config = { childList: true, subtree: true };

// 开始观察目标节点
observer.observe(targetNode, config);

在上述代码中,我们创建了一个 MutationObserver 实例,并通过 observe 方法绑定到目标节点。在回调函数中,使用 mutation.removedNodes 检测子节点删除情况。如果发现水印被删除,可以在此处重新插入水印元素。

需要注意的是,MutationObserver 是现代浏览器的特性,可能不兼容老旧浏览器。因此,实际应用中应考虑浏览器兼容性。

此外,为了确保水印能迅速恢复,可以在检测到水印被删除时立即执行插入操作。

防止DOM被隐藏

除了防止DOM被删除,还要考虑DOM被隐藏的情况。

要检测到水印DOM被设置为 display: none 隐藏,可以通过 MutationObserver 观察元素的属性变化,而不是仅仅关注子节点的删除。监听 attributes 类型的变化,以检测到 display 样式属性的改变。

以下示例展示了如何监控 display 属性的变化:

// 目标节点(假设水印元素是一个特定的节点)
const watermarkElement = document.querySelector('.watermark');

// 创建 MutationObserver 实例
const observer = new MutationObserver(mutationsList => {
    mutationsList.forEach(mutation => {
        if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
            // 检查水印的 display 属性是否被设置为 none
            if (getComputedStyle(watermarkElement).display === 'none') {
                // 如果水印被隐藏,重新显示水印
                watermarkElement.style.display = 'block';
            }
        }
    });
});

// 配置 MutationObserver
const config = { attributes: true, subtree: true, attributeFilter: ['style'] };

// 开始观察目标节点
observer.observe(document.body, config);

说明

  1. 目标节点:在代码中,watermarkElement 代表水印元素。请确保选择器正确。
  2. MutationObserver 实例:观察属性变化 (attributes) 和特定的属性 style
  3. 属性变化检测:在回调函数中,使用 getComputedStyle 检查 display 属性的值。如果水印被设置为 display: none,则将其恢复为 display: block

54. 你是怎么理解前端架构的?

难度:3 · 类型:QA

题目要点

前端架构涉及多个方面,从代码组织、构建工具、性能优化到状态管理、路由、安全、测试等。一个好的前端架构能够提升开发效率、保证代码质量、优化用户体验,并支持团队的协作和持续交付。设计前端架构时,需要综合考虑应用的需求、团队的实际情况以及技术的特点,以建立一个高效、可维护的前端系统。

参考答案

前端架构是指在前端开发中,设计和构建应用的整体结构和策略。这包括代码组织、工具选择、性能优化、团队协作等多个方面。良好的前端架构有助于提高开发效率、确保代码质量、优化用户体验。

以下与前端架构相关的一些要点:

1. 代码组织

  • 模块化:将代码拆分成独立的模块或组件,提高可维护性和可重用性。采用前端框架(如 React、Vue、Angular)通常会自然支持组件化。
  • 目录结构:设计合理的目录结构,以便于代码管理和团队协作。常见的结构包括按功能划分(如 components/services/views/)、按类型划分等。

2. 构建工具和流程

  • 构建工具:使用工具如 Webpack、Vite、Rollup 来打包、编译和优化前端资源。这些工具支持模块打包、代码分割、热重载等功能。
  • 自动化:配置自动化流程(CI/CD),包括构建、测试、部署等,确保代码的持续集成和交付。

3. 性能优化

  • 加载性能:优化资源的加载时间,例如通过代码分割、懒加载、图片压缩等技术。
  • 运行性能:优化页面的响应速度和渲染性能,减少 JavaScript 执行时间、优化渲染流程等。
  • 缓存策略:使用合适的缓存策略,如 HTTP 缓存、Service Worker 缓存等,提升应用的离线体验和响应速度。

4. 状态管理

  • 状态管理库:在复杂的应用中,使用状态管理库(如 Redux、Vuex、MobX)来集中管理应用的状态,确保状态一致性。
  • 本地状态和全局状态:合理划分本地状态(组件内部状态)和全局状态(应用级别状态),根据需要选择合适的管理方式。

5. 路由管理

  • 客户端路由:使用路由库(如 React Router、Vue Router)来管理应用的导航和路由。
  • 路由设计:设计合理的路由结构,确保用户体验流畅且符合应用需求。

6. 安全

  • 前端安全:防范常见的安全问题,如跨站脚本攻击(XSS)、跨站请求伪造(CSRF)。使用工具和最佳实践来保护应用安全。
  • 数据保护:确保敏感数据在传输和存储过程中得到适当的保护。

7. 测试

  • 单元测试:对单个组件或功能进行测试,确保它们的正确性。使用测试框架如 Jest、Mocha。
  • 集成测试:测试组件之间的交互和集成,验证系统的整体功能。
  • 端到端测试:模拟用户行为,测试整个应用的功能和性能。使用工具如 Cypress、Selenium。

8. 用户体验

  • 响应式设计:确保应用在不同设备和屏幕尺寸上都能良好显示。
  • 可访问性:关注应用的可访问性,确保所有用户,包括有特殊需求的用户,都能顺利使用应用。

9. 团队协作

  • 代码审查:实施代码审查流程,保证代码质量和团队协作。
  • 文档:编写和维护项目文档,帮助团队成员了解项目结构、规范和最佳实践。

55. 用户访问页面白屏了, 原因是啥, 如何排查?

难度:4 · 类型:QA

题目要点

用户访问页面白屏可能由多种原因引起,以下是一些可能的原因和排查步骤:

参考答案

白屏处理办法

用户访问页面白屏可能由多种原因引起,以下是一些可能的原因和排查步骤:

  1. 网络问题:用户的网络连接可能存在问题,无法正确加载页面内容。可以要求用户检查网络连接,或者自己尝试在不同网络环境下测试页面的加载情况。

  2. 服务端问题:服务器未正确响应用户请求,导致页面无法加载。可以检查服务器的状态、日志和错误信息,查看是否有任何异常。同时,可以确认服务器上的相关服务是否正常运行。

  3. 前端代码问题:页面的前端代码可能存在错误或异常,导致页面无法正常渲染。可以检查浏览器的开发者工具,查看是否有任何错误信息或警告。同时,可以尝试将页面的JavaScript、CSS和HTML代码分离出来进行单独测试,以确定具体的问题所在。

  4. 浏览器兼容性问题:不同浏览器对于某些代码的支持可能不一致,导致页面在某些浏览器中无法正常加载。可以尝试在不同浏览器中测试页面的加载情况,同时使用浏览器的开发者工具检查是否有任何错误或警告。

  5. 第三方资源加载问题:页面可能依赖于某些第三方资源(如外部脚本、样式表等),如果这些资源无法加载,可能导致页面白屏。可以检查网络请求是否正常,是否有任何资源加载失败的情况。

  6. 缓存问题:浏览器可能在缓存中保存了旧版本的页面或资源,导致新版本无法加载。可以尝试清除浏览器缓存,或者通过添加随机参数或修改文件名的方式强制浏览器重新加载页面和资源。

  7. 其他可能原因:页面白屏问题还可能由于安全策略(如CSP、CORS等)限制、跨域问题、DNS解析问题等引起。可以使用浏览器的开发者工具检查网络请求和错误信息,查找可能的问题。

在排查问题时,可以根据具体情况逐步进行排查,并结合浏览器的开发者工具、服务器日志等工具来辅助定位问题所在,并且可以与用户进行进一步沟通以获取更多信息。如果问题无法解决,可以寻求专业的技术支持或咨询。

56. SPA首屏加载速度慢的怎么解决

难度:3 · 类型:QA

题目要点

SPA的首屏渲染,通常都受限于我们代码大小、分包策略、网络、缓存等因素,因此提高首屏渲染速度,重点就是分析&解决上面几点。

  • 确保我们代码包尽量小,使用 tree-shaking 等技术,移除未使用的代码
  • 合理的分包策略,避免某的包过大、也需要避免拆成很多的小文件
  • 网络方面,主要就是利用CDN加速等手段,大大缩短静态资源的下载时间
  • 缓存,可以认为是网络的延伸,最快的网络请求,就是么有网络请求 ,如果我们能保证大多数资源包都直接走浏览器本地缓存,那不管是对我们服务器资源节省、还是提高首屏渲染速度,都是有极大的帮助
参考答案

单页面应用(SPA)首屏加载速度慢的问题可能由多种因素造成。以下是一些优化首屏加载速度的常见方法:

1. 代码分割(Code Splitting)

  • 描述:将代码拆分成多个小块,只加载当前页面所需的代码。

  • 实现:使用 Webpack 或其他打包工具进行动态导入,按需加载代码模块。

  • 示例:使用 React.lazySuspense 进行组件级别的代码分割。

    import React, { Suspense, lazy } from 'react';
    
    const LazyComponent = lazy(() => import('./LazyComponent'));
    
    function App() {
        return (
            <Suspense fallback={<div>Loading...</div>}>
                <LazyComponent />
            </Suspense>
        );
    }
    

2. 懒加载(Lazy Loading)

  • 描述:只在需要时加载资源(如图片、组件)。

  • 实现:使用 IntersectionObserver 或第三方库(如 react-lazyload)。

  • 示例

    import React from 'react';
    
    const LazyImage = React.lazy(() => import('./LazyImage'));
    
    function App() {
        return (
            <React.Suspense fallback={<div>Loading...</div>}>
                <LazyImage />
            </React.Suspense>
        );
    }
    

3. 减少初始加载资源

  • 描述:减少首屏渲染时需要加载的资源量(如 JS、CSS 文件)。
  • 实现:合并和压缩 CSS 和 JS 文件,删除未使用的 CSS,减少 HTTP 请求数量。

4. 服务器端渲染(SSR)

  • 描述:将页面的初始内容在服务器端生成,以减少客户端的渲染时间。
  • 实现:使用框架(如 Next.js、Nuxt.js)支持 SSR,优化首屏加载时间。

5. 使用 Service Workers

  • 描述:使用 Service Workers 缓存资源和页面,加速后续的访问。

  • 实现:在应用中注册 Service Worker,缓存静态资源和 API 请求。

    if ('serviceWorker' in navigator) {
        window.addEventListener('load', () => {
            navigator.serviceWorker.register('/service-worker.js')
                .then(registration => {
                    console.log('ServiceWorker registration successful:', registration);
                })
                .catch(error => {
                    console.log('ServiceWorker registration failed:', error);
                });
        });
    }
    

6. 优化数据请求

  • 描述:优化初始数据请求,减少请求时间。
  • 实现:将请求数据分为初始必要的数据和后续加载的数据,减少首屏渲染时的数据请求。

7. CDN

静态资源走CDN,缩短下载时间。

8. 使用性能分析工具

  • 描述:分析应用性能,找到瓶颈。
  • 实现:使用 Chrome DevTools、Lighthouse 等工具来分析和优化首屏加载时间。

通过结合这些方法,可以显著改善 SPA 的首屏加载速度,提高用户体验。

57. React 如何实现 vue 中 keep-alive 的功能?

难度:3.5 · 类型:QA

题目要点

在 React 中实现类似于 Vue 中 keep-alive 的功能,可以使用组件状态和 React 的生命周期方法来控制组件的挂载和卸载。

参考答案

在 React 中实现类似于 Vue 中 keep-alive 的功能,可以使用组件状态和 React 的生命周期方法来控制组件的挂载和卸载。

以下是一种实现方式:

1. 使用状态管理组件

创建一个 KeepAlive 组件,用于存储和管理被“缓存”的组件。

import React, { useState } from 'react';

// KeepAlive 组件
const KeepAlive = ({ children, name }) => {
  const [cache, setCache] = useState({});

  // 保存组件
  const saveCache = () => {
    setCache((prev) => ({
      ...prev,
      [name]: children,
    }));
  };

  // 恢复组件
  const getCachedComponent = () => {
    return cache[name] || children;
  };

  // 组件挂载时保存
  React.useEffect(() => {
    saveCache();
  }, [children]);

  return <>{getCachedComponent()}</>;
};

// 示例用法
const App = () => {
  const [activeComponent, setActiveComponent] = useState('ComponentA');

  return (
    <div>
      <button onClick={() => setActiveComponent('ComponentA')}>Component A</button>
      <button onClick={() => setActiveComponent('ComponentB')}>Component B</button>

      <KeepAlive name={activeComponent}>
        {activeComponent === 'ComponentA' ? <ComponentA /> : <ComponentB />}
      </KeepAlive>
    </div>
  );
};

const ComponentA = () => <div>Component A</div>;
const ComponentB = () => <div>Component B</div>;

export default App;

2. 实现逻辑

  • 状态管理KeepAlive 组件使用一个状态 cache 来存储被缓存的组件。
  • 保存和恢复:在组件挂载时保存当前子组件到缓存中;每次渲染时,检查缓存并返回之前的组件,避免重新渲染。
  • 使用示例:通过按钮切换 activeComponent 的状态,展示不同的组件,同时保留它们的状态。

58. 怎么使用 webpack,将 JS 文件中的 css 提取到单独的样式文件中?

难度:2 · 类型:QA

题目要点

通过配置 Webpack 和使用 MiniCssExtractPlugin,可以方便地将 JS 文件中的 CSS 提取到单独的文件中。

参考答案

在前端项目中使用 Webpack 提取 JS 文件中的 CSS,可以通过以下步骤实现:

1. 安装必要的依赖

首先,需要安装以下依赖:

npm install --save-dev webpack webpack-cli style-loader css-loader mini-css-extract-plugin

2. 配置 Webpack

在 Webpack 配置文件 webpack.config.js 中,配置 MiniCssExtractPlugin 来提取 CSS。

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: './src/index.js', // 入口文件
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css$/, // 匹配 CSS 文件
        use: [
          MiniCssExtractPlugin.loader, // 提取 CSS
          'css-loader', // 处理 CSS
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'styles.css', // 输出的 CSS 文件名
    }),
  ],
};

3. 在 JavaScript 中引入 CSS

在你的 JS 文件中,引入 CSS 文件:

import './styles.css'; // 引入 CSS 文件

console.log('Hello, World!');

4. 运行 Webpack

使用以下命令构建项目:

npx webpack --config webpack.config.js

5. 输出结果

构建完成后,生成的 dist 文件夹中会包含 bundle.jsstyles.css,CSS 被成功提取到单独的文件中。

59. 为什么在技术选型中选择了 webpack?

难度:3 · 类型:QA

题目要点

选择 Webpack 作为构建工具,主要是因为它强大的模块化支持、灵活的配置能力、丰富的插件生态以及优秀的性能优化功能。这些特性使得 Webpack 成为现代前端开发中不可或缺的工具,能够有效提升开发效率和应用性能。

参考答案

选择 Webpack 作为构建工具的原因,通常从以下几个方面作答:

1. 强大的模块化支持

  • CommonJS 和 ES Module: Webpack 原生支持多种模块系统(如 CommonJS、AMD 和 ES6 模块),可以灵活处理不同类型的模块。

2. 代码分割

  • 优化加载速度: Webpack 提供了代码分割的功能,可以将应用分割成多个更小的代码块(chunks),实现按需加载,提高应用性能。

3. 丰富的插件生态

  • 扩展性: Webpack 拥有丰富的插件和 Loader,可以处理各种资源(如 CSS、图片、字体等),并实现各种构建优化(如压缩、Tree Shaking 等)。

4. 热模块替换 (HMR)

  • 开发体验: Webpack 提供热模块替换功能,使得在开发过程中可以实时更新页面而不需要完全刷新,提高开发效率和体验。

5. 优化功能

  • Tree Shaking: Webpack 支持树摇优化,能够移除未使用的代码,减小最终打包文件的体积。
  • 压缩和优化: 提供多种内置的压缩和优化策略,可以显著提高生产环境下的性能。

6. 自定义配置

  • 灵活性: Webpack 允许开发者通过配置文件进行高度定制,能够满足各种复杂的项目需求。

7. 社区支持和文档

  • 活跃的社区: Webpack 拥有庞大的用户社区,提供了大量的教程、示例和插件,使得学习和解决问题变得更加容易。

8. 兼容性和稳定性

  • 广泛的使用场景: Webpack 已被广泛应用于各类项目,经过了多次迭代和优化,具有良好的稳定性和兼容性。

60. 如果公司要做一个低代码平台,你会怎么进行架构设计?

难度:4.5 · 类型:QA

题目要点

低代码平台的架构设计核心在于组件化、可配置化和扩展性,前端提供可视化搭建工具、组件库和预览功能;中台负责业务逻辑、数据处理、流程控制;后端保证数据存储和系统性能。最终通过灵活的插件系统、安全设计、性能优化和自动化运维实现整个系统的稳定、高效运行。

参考答案

设计一个低代码平台的架构需要综合考虑灵活性、可扩展性、性能、安全性等因素,旨在帮助用户通过拖拽、可视化界面配置等方式快速构建应用,同时又具备高度的可扩展性和稳定性。

以下是一些思路:

1. 总体架构思路

一个低代码平台可以划分为以下几个主要模块:

  • 前端界面层:提供用户交互的可视化界面,支持组件拖拽、属性配置、数据绑定等操作。
  • 中台服务层:负责处理数据流、权限、业务逻辑等,提供 API 和服务支持。
  • 后端支撑层:处理数据存储、流程引擎、权限控制、扩展机制等。
  • 扩展模块:为高级用户提供插件、扩展和自定义代码的能力。

2. 前端架构

前端是用户使用低代码平台的核心,主要任务是提供拖拽式可视化界面和动态配置能力。

2.1. 基于组件化的设计

低代码平台中的 UI 组件是核心,用户通过拖拽组件来搭建应用,因此前端架构要高度组件化。

  • 组件库:基础组件库,包括表单组件、布局组件、按钮、图表等。每个组件都有配置项、事件、样式等属性。
  • 拖拽引擎:负责管理组件的拖拽、位置调整和动态添加。可以基于 react-dndVue Drag-and-Drop 等库实现。
  • 属性面板:展示当前组件的可配置属性(如样式、数据源等),并提供动态配置的能力。
  • 事件机制:为组件设置事件处理程序,支持点击、提交等行为,并与后端 API 交互。

2.2. 动态渲染与预览

用户在可视化搭建应用时,可以实时预览效果,因此需要实现动态渲染引擎:

  • 页面配置化渲染:将用户搭建的页面结构和组件配置转化为 JSON,使用配置化的方式渲染页面。
  • 实时预览:用户在编辑器中所见即所得,支持实时预览和切换不同设备视图(如移动端、桌面端)。

2.3. 插件系统

平台需提供灵活的插件机制,允许开发者扩展组件库或功能模块。比如:导入第三方组件库、扩展事件处理器等。

3. 中台服务架构

中台负责连接前端的配置和业务逻辑,提供处理应用逻辑的能力,包括数据处理、权限管理、业务流程等。

3.1. API 网关

中台可以基于 API 网关进行架构设计,处理所有的 API 请求,并提供统一的鉴权、日志和监控功能。

3.2. 流程引擎

低代码平台需要支持工作流或流程设计,用户可以通过拖拽节点创建业务流程:

  • 流程节点:包括条件节点、操作节点(如调用 API、写入数据库)、事件节点等。
  • 流程引擎:控制流程的执行顺序、条件判断,类似于 BPM(业务流程管理)。

3.3. 数据集成与管理

平台需提供数据集成功能,允许用户通过拖拽或配置连接外部数据源:

  • 数据源连接:支持 REST API、GraphQL、数据库等多种数据源类型。
  • 数据绑定:前端组件需要通过可视化方式绑定数据源,例如通过下拉选择绑定 API 返回的数据。

3.4. 权限管理

中台负责提供权限控制,确保不同用户可以访问不同的数据和功能。

  • 角色权限:基于角色进行权限控制,用户根据权限可以操作不同的页面、数据和功能。
  • 数据权限:对于敏感数据,平台需提供细粒度的数据权限控制。

4. 后端支撑

后端负责平台的稳定运行、数据存储、权限控制等。

4.1. 数据存储

选择合适的数据存储方案,根据业务需求使用多种数据库:

  • 关系型数据库:存储应用配置、用户信息等结构化数据。
  • 非关系型数据库:例如 MongoDB,适合存储页面 JSON 配置、用户行为记录等非结构化数据。

4.2. 缓存系统

为了加快页面的加载速度和减少数据库的压力,可以使用缓存系统(如 Redis)存储高频访问的数据和配置。

4.3. 微服务架构

后端架构可以采用微服务拆分的方式,将不同的功能模块(如用户管理、日志监控、权限控制等)独立成微服务,方便后续扩展和维护。

5. 扩展与定制化

为了满足不同用户的需求,平台需提供扩展能力:

  • 自定义组件:用户可以开发自己的组件,上传到平台并进行使用。
  • 脚本扩展:支持用户编写自定义脚本,实现更复杂的交互和逻辑控制。
  • 模板机制:允许用户保存配置好的应用为模板,供其他人复用。

6. 安全性设计

平台的安全性是重点考虑的方面,需要从以下几个层面保障系统的安全:

  • 身份认证与授权:采用 OAuth 或 JWT 等认证机制,确保每个用户都经过身份验证。
  • 数据加密:对于用户数据,需采用传输层和存储层加密的方式保护敏感信息。
  • 操作日志:记录用户的操作行为,方便问题追溯和监控。

7. 性能优化

低代码平台要处理大量的配置化数据,性能优化显得尤为重要:

  • 按需加载:对于组件库和插件系统,支持按需加载,避免一次性加载所有组件。
  • 数据缓存:通过缓存机制,避免重复的请求和数据处理。
  • 静态资源优化:前端通过 Webpack、Vite 等工具优化静态资源加载,减少打包体积,提高页面加载速度。

8. CI/CD 和运维部署

低代码平台的持续集成与交付(CI/CD)是确保开发和部署高效进行的基础:

  • 自动化测试:确保平台的稳定性,进行单元测试、集成测试。
  • 自动化部署:通过 Kubernetes 或 Docker 实现自动化部署,支持横向扩展和多环境部署。

61. 低代码平台的页面数据结构该怎么设计?

难度:4 · 类型:QA

题目要点

低代码平台的页面数据结构核心设计是以组件化为基础,通过灵活的props传递属性、样式、事件,同时具备扩展性来支持自定义组件与动态数据绑定。通过合理设计数据结构,可以支持复杂的页面布局、动态渲染和良好的性能表现。

参考答案

低代码平台中的页面数据结构设计至关重要,它承载了页面的所有元素、布局、属性、样式、交互等内容。这一数据结构应该简洁、灵活、可扩展,以支持不同的页面需求、动态渲染、版本控制、状态管理等场景。

页面数据结构的设计原则

  1. 结构化清晰:数据结构应该明确反映出页面的布局、组件的嵌套关系等。
  2. 灵活扩展:支持多种组件类型和功能的动态扩展。
  3. 性能优化:在渲染时高效解析和转换为实际的页面。
  4. 组件化:以组件为基础的设计,组件可以是基础组件、布局组件、容器组件等。
  5. 可维护性:支持后续版本的平滑升级,适应多种需求。

基础结构设计

页面数据结构可以用树状结构表示页面的组件层级关系。每个页面由一个根节点开始,每个节点代表页面中的一个元素或组件。每个组件可以有其属性、样式、事件及其子组件。下面是页面数据结构的一个基本模型。

通用页面数据结构

{
  "id": "page-root",
  "type": "Page",
  "props": {
    "title": "My Page",
    "style": {
      "backgroundColor": "#fff",
      "padding": "20px"
    }
  },
  "children": [
    {
      "id": "header",
      "type": "Header",
      "props": {
        "title": "Welcome",
        "style": {
          "color": "#333",
          "fontSize": "20px"
        }
      },
      "events": {
        "onClick": "handleHeaderClick"
      }
    },
    {
      "id": "content",
      "type": "Container",
      "props": {
        "style": {
          "display": "flex",
          "flexDirection": "row",
          "justifyContent": "space-between"
        }
      },
      "children": [
        {
          "id": "left-section",
          "type": "Text",
          "props": {
            "text": "Left Content",
            "style": {
              "width": "50%",
              "color": "#555"
            }
          }
        },
        {
          "id": "right-section",
          "type": "Image",
          "props": {
            "src": "/path/to/image.jpg",
            "alt": "Image",
            "style": {
              "width": "50%"
            }
          }
        }
      ]
    },
    {
      "id": "footer",
      "type": "Footer",
      "props": {
        "style": {
          "padding": "10px",
          "backgroundColor": "#f0f0f0"
        }
      },
      "children": []
    }
  ]
}

关键字段解释

  1. id: 每个节点都有一个唯一的 id,用于标识组件,方便操作和查找。
  2. type: 组件的类型,代表具体的组件,如 HeaderTextImage 等。每个 type 对应不同的组件逻辑。
  3. props: 组件的属性,包括样式、内容、功能等。如 style 用于定义样式,textsrc 等用于设置具体内容。
  4. children: 组件的子节点数组,表示当前组件下嵌套的子组件。通过 children 实现组件树的嵌套结构。
  5. events: 组件的事件处理配置,映射到具体的事件处理函数。

设计思路详细解析

1. 组件类型与 props

每个节点的 type 决定它是什么组件,而 props 决定了组件的属性和行为。例如,对于文本组件(Text),它的 props 可能包括 textstyle;对于图片组件(Image),可能包括 srcalt 属性。每个组件类型都会有相应的属性结构。

{
  "id": "header",
  "type": "Header",
  "props": {
    "title": "Welcome to the page",
    "style": {
      "color": "#333",
      "backgroundColor": "#fff"
    }
  }
}

2. 组件的层次结构(children)

页面通常是由多个组件嵌套而成,children 字段代表子组件。每个节点都可以有自己的 children,组成树状的结构。嵌套结构使得复杂页面布局可以通过递归解析渲染。

{
  "id": "content",
  "type": "Container",
  "children": [
    {
      "id": "text-left",
      "type": "Text",
      "props": {
        "text": "This is a text block"
      }
    },
    {
      "id": "image-right",
      "type": "Image",
      "props": {
        "src": "/path/to/image.png"
      }
    }
  ]
}

3. 样式和布局

使用 props.style 定义每个组件的样式。样式支持 CSS-like 的属性,如 widthpaddingflexDirection 等。基于这些样式可以实现布局、排版和响应式设计。

{
  "id": "container",
  "type": "Container",
  "props": {
    "style": {
      "display": "flex",
      "flexDirection": "row",
      "justifyContent": "center"
    }
  },
  "children": [...]
}

4. 事件系统

通过 events 字段,定义组件的交互事件。例如,onClickonChange 等事件可以在 props 中指定为回调函数。

{
  "id": "button",
  "type": "Button",
  "props": {
    "text": "Click Me"
  },
  "events": {
    "onClick": "handleButtonClick"
  }
}

在实现过程中,事件函数可能映射到预定义的回调函数或自定义的脚本。

5. 高级功能

5.1. 条件渲染

在某些场景下,组件可能需要根据特定条件决定是否渲染。可以通过 condition 属性实现条件渲染:

{
  "id": "promo-banner",
  "type": "Banner",
  "props": {
    "text": "Special Promotion"
  },
  "condition": "user.isLoggedIn"
}
5.2. 动态数据绑定

支持动态数据绑定是低代码平台的核心需求之一。通过数据模型,可以为组件绑定数据源:

{
  "id": "user-info",
  "type": "Text",
  "props": {
    "text": "{{user.name}}"
  }
}

通过 {{}} 模板语法,数据在渲染时会自动替换为实际值。

5.3. 状态管理

每个组件可以与全局状态(如 Redux、Vuex)进行绑定,也可以内部维护自己的状态。通过 props 动态传入状态并更新。

{
  "id": "counter",
  "type": "Counter",
  "props": {
    "count": "{{state.count}}"
  }
}

性能优化设计

  • 按需渲染:仅当用户实际配置或拖拽时才渲染组件,避免页面首次加载过慢。
  • 缓存机制:支持对组件的配置和状态进行缓存,以便用户在切换页面或重新编辑时能够快速加载。
  • 虚拟 DOM:使用 React/Vue 这种虚拟 DOM 技术,可以在组件更新时减少不必要的 DOM 操作。

扩展性设计

页面数据结构应该支持扩展,以满足用户添加自定义组件、脚本、事件等需求。

  • 自定义组件:允许用户上传自定义组件并添加到页面中。
  • 脚本注入:提供接口允许用户为页面中的某些组件注入自定义 JavaScript 脚本。

62. 低代码平台怎么设计渲染模块?

难度:4 · 类型:QA

题目要点

低代码平台的渲染模块需要做到:

  • 动态解析用户配置:基于配置化的 JSON 数据渲染出实际页面。
  • 组件化设计:每个页面模块都是组件,具备属性、事件、样式等动态绑定能力。
  • 性能优化:通过按需加载、懒加载、虚拟滚动等优化页面渲染性能。
  • 多设备支持:提供多设备预览能力,确保跨平台一致性。
  • 扩展性强:支持自定义组件和脚本,满足不同用户的扩展需求。
参考答案

在低代码平台中,渲染模块的设计至关重要,因为它直接负责将用户通过可视化界面拖拽和配置生成的页面,动态地渲染为可以在浏览器中展示的 UI 界面。一个良好的渲染模块设计需要兼顾灵活性、高性能和可扩展性。

以下是一些思路:

1. 总体设计思路

渲染模块主要负责将用户配置的数据(通常是 JSON 格式)转化为可视的页面。核心设计包含以下几部分:

  • 页面配置结构解析
  • 组件化渲染
  • 属性、样式、事件的动态绑定
  • 性能优化(如按需渲染、懒加载等)
  • 动态更新和重渲染
  • 多设备预览

2. 页面配置结构解析

用户通过拖拽或配置生成的页面数据通常会存储为 JSON 或其他类似格式的结构。渲染模块首先需要解析这个配置数据,并动态生成对应的 UI 组件。

2.1. 页面配置数据结构

通常,页面的配置数据可以表示为以下形式:

{
  "id": "root",
  "type": "div",
  "props": {
    "style": {
      "display": "flex",
      "flexDirection": "column"
    }
  },
  "children": [
    {
      "id": "header",
      "type": "Header",
      "props": {
        "title": "My Page"
      }
    },
    {
      "id": "content",
      "type": "Content",
      "props": {
        "text": "This is the content"
      }
    }
  ]
}

每个节点代表一个页面的部分,包含以下关键属性:

  • type:组件的类型(如 div、自定义组件 Header 等)。
  • props:传递给组件的属性(如样式、事件等)。
  • children:子组件数组,表示嵌套的组件结构。

2.2. 解析逻辑

渲染模块需要解析配置文件并根据 type 渲染对应的组件。使用递归的方式处理嵌套的子组件。

function renderComponent(config) {
  const { type, props, children } = config;

  // 动态创建组件
  const Component = componentMap[type] || type;

  return (
    <Component {...props}>
      {children && children.map(child => renderComponent(child))}
    </Component>
  );
}

通过将 type 映射到实际的 React/Vue 组件,递归渲染每一个组件。

3. 组件化渲染

低代码平台的渲染模块核心在于基于组件化的设计思路。平台中的组件可以分为以下几类:

  • 基础组件:如按钮、输入框、表单、布局等。
  • 自定义组件:平台允许用户扩展组件库,甚至上传自己的业务组件。
  • 布局组件:如 GridFlexBox,帮助实现响应式布局。
  • 容器组件:如 ModalCard 等,用于包含其他组件。

通过建立完善的组件库,渲染模块可以支持用户从不同的库中选择并组合组件。每个组件可以拥有不同的属性(如 props)和样式配置。

4. 动态绑定属性、样式与事件

渲染的页面不只是静态展示,还需要根据用户的配置动态绑定属性、样式和事件。

4.1. 动态属性绑定

每个组件的 props 是动态传入的,通常会在用户配置面板中编辑后实时生效。例如,用户可以配置按钮的 onClick 事件、输入框的 placeholder 等属性。配置的数据被实时解析并传递给组件。

4.2. 样式绑定

组件的样式可以通过用户在可视化界面配置的样式信息动态绑定。比如,用户可以在拖拽界面选择按钮的背景颜色、字体大小等。渲染模块会根据配置动态生成 CSS 样式并应用到组件上。

<button style={{ backgroundColor: config.props.style.backgroundColor }}>
  {config.props.text}
</button>

4.3. 事件绑定

事件也是通过配置动态绑定的。用户在界面配置某个事件(如 onClick)后,渲染模块会将该事件通过 props 传递给组件,并进行绑定。

<Component
  {...props}
  onClick={handleClick}
/>

5. 性能优化

由于低代码平台生成的页面组件数量可能较多,性能优化至关重要,确保页面在渲染时不卡顿,尤其是复杂的页面。

5.1. 按需加载

对于大型组件库或第三方依赖,渲染模块应该支持按需加载。基于 Webpack 的 Code Splitting 或者基于 Vue/React 的异步组件加载,可以大大减少首屏加载时间。

5.2. 懒加载与虚拟滚动

如果页面中有大量的元素或组件,可以引入懒加载或虚拟滚动技术,只有当用户滚动到某个区域时才进行渲染。

5.3. Diff 算法与局部更新

为优化页面更新性能,平台应采用类似于 React 的 Diff 算法,比较新旧组件树的差异,只有当某个组件发生变化时才局部更新。

6. 动态更新与重渲染

用户在使用低代码平台时,会不断调整页面结构或修改组件属性,因此需要支持动态更新和重渲染。

  • 双向绑定:用户修改配置后,渲染模块能够实时反映修改的内容,提供 “所见即所得” 的体验。
  • 局部渲染:在用户拖拽组件或修改属性时,只对修改的部分组件重新渲染,而不是整个页面刷新。

7. 多设备预览

为了适配不同设备(如手机、平板、桌面),渲染模块需要提供多种视图模式,帮助用户快速切换视图,模拟不同设备的效果。

  • 响应式设计:布局组件应该支持响应式设计,适应不同的屏幕尺寸。
  • 设备模拟器:提供模拟器功能,用户可以在设计阶段查看不同设备上的页面效果。

8. 扩展能力

渲染模块应支持扩展能力,允许高级用户自定义组件或编写自定义逻辑。

  • 自定义组件库:高级用户可以上传自己的业务组件库,通过平台的组件渲染引擎进行展示。
  • 动态脚本执行:平台允许用户编写自定义的 JavaScript 或逻辑,并将这些逻辑注入到渲染过程中,实现复杂的业务需求。

63. 在前端应用中,怎么进行系统权限的设计?

难度:3 · 类型:QA

题目要点

在前端应用中,进行系统权限的设计是为了保证不同用户在不同权限级别下能够访问相应的资源和功能。权限设计通常涉及 用户身份管理权限控制前后端协作 以及 安全性 考虑。以下是设计思路:

参考答案

在前端应用中,进行系统权限的设计是为了保证不同用户在不同权限级别下能够访问相应的资源和功能。权限设计通常涉及 用户身份管理权限控制前后端协作 以及 安全性 考虑。以下是设计思路:

1. 权限模型的设计

权限模型通常可以采用以下几种方式进行管理:

  • 基于角色的访问控制(RBAC,Role-Based Access Control): 这是最常见的权限设计模式。用户被分配一个或多个角色,每个角色对应一组权限。前端可以根据用户的角色显示不同的页面或组件。

  • 基于资源的访问控制: 每个资源(如菜单、页面、API)都设置特定的权限。根据用户是否具备访问某个资源的权限来控制可见性和操作。

  • 基于属性的访问控制(ABAC,Attribute-Based Access Control): 权限是根据用户属性(如部门、地域、职位)来控制的,可以实现更细粒度的权限管理。

2. 权限管理设计步骤

2.1. 权限系统初始化

  • 用户身份认证: 首先通过登录认证系统(OAuth、JWT、Session等)获取用户的身份信息和权限信息。用户登录后,后端返回 token(JWT)或者权限数据,前端需要保存用户的登录态。

  • 获取用户权限信息: 登录后,前端需要从服务器拉取用户的权限信息。通常权限信息包括:

    • 用户角色
    • 用户具体的权限点(可操作的页面、功能)

    权限信息可以通过 API 获取,也可以包含在登录返回的 token 中。

2.2. 路由权限控制

  • 路由守卫: 前端的路由系统需要和权限系统集成,来防止用户访问未授权的页面。比如在 Vue 或 React 中使用路由守卫,来在用户每次访问页面时,检查是否有权限进入。

    • 在 Vue 中可以使用 beforeEach 路由守卫:

      router.beforeEach((to, from, next) => {
        const userRoles = getUserRoles(); // 获取用户角色
        if (to.meta.roles && !userRoles.includes(to.meta.roles)) {
          next('/no-access'); // 无权限访问,重定向到错误页面
        } else {
          next(); // 有权限,继续访问
        }
      });
      
    • 在 React 中,可以通过 PrivateRoute 组件来封装路由检查:

      const PrivateRoute = ({ component: Component, roles, ...rest }) => (
        <Route
          {...rest}
          render={props =>
            hasPermission(roles) ? (
              <Component {...props} />
            ) : (
              <Redirect to="/no-access" />
            )
          }
        />
      );
      

2.3. 组件和元素级别的权限控制

除了路由级别的权限控制,有时需要在页面内进行细粒度的权限控制,比如某个按钮、表单项是否可见或可操作。

  • 基于权限判断是否渲染组件: 在渲染组件时,可以基于用户的权限动态控制显示或隐藏:

    const DeleteButton = () => {
      if (!hasPermission('delete-item')) {
        return null; // 无权限,不渲染按钮
      }
      return <button>Delete</button>;
    };
    
  • 基于权限动态禁用元素: 甚至可以根据权限来控制元素的状态,比如禁用某些功能:

    <button disabled={!hasPermission('edit-item')}>Edit</button>
    

2.4. API 请求的权限控制

前端可以根据权限,决定用户能否执行某些请求。如果用户无权限,可以不显示相关按钮或直接阻止请求:

  • 发起 API 请求前的权限校验: 在发起请求之前,先检查当前用户是否具备权限:

    if (!hasPermission('create-item')) {
      alert('You do not have permission to create items.');
      return;
    }
    api.createItem(data); // 发起请求
    
  • 后端校验权限: 由于前端的权限是可见的,最终权限控制还需在后端校验,防止用户通过绕过前端限制直接发起请求。后端需验证用户的角色和权限,确保用户有权执行请求。

3. 权限缓存和更新

前端在登录后,通常会将权限信息存储在内存(如 ReduxVuex 中)或者本地存储(如 localStoragesessionStorage)中,减少不必要的权限拉取请求。

  • 缓存权限: 可以将用户的权限信息在首次登录后存储起来,后续直接读取缓存,减少 API 请求:

    const userPermissions = JSON.parse(localStorage.getItem('user-permissions'));
    
  • 权限更新: 如果权限发生变动(比如管理员更新了用户角色),前端需要重新拉取权限数据,并更新缓存:

    api.updatePermissions().then(newPermissions => {
      localStorage.setItem('user-permissions', JSON.stringify(newPermissions));
    });
    

4. 安全性考虑

  • 防止前端绕过: 前端权限控制是用户体验层面的,但真正的权限校验必须由后端完成,防止用户通过修改前端代码或发送非法请求来获得未经授权的访问。

  • 避免敏感数据泄漏: 在权限不足的情况下,前端不应该加载任何与该权限相关的资源,避免泄漏敏感数据或功能。

5. 权限设计示例

权限配置结构

  • 用户信息:

    {
      "userId": "123",
      "roles": ["admin", "editor"],
      "permissions": ["view-dashboard", "edit-post", "delete-post"]
    }
    
  • 路由配置:

    const routes = [
      { path: '/dashboard', component: Dashboard, meta: { roles: ['admin', 'editor'] } },
      { path: '/admin', component: Admin, meta: { roles: ['admin'] } },
      { path: '/no-access', component: NoAccess }
    ];
    
  • 权限校验函数:

    function hasPermission(requiredPermissions) {
      const userPermissions = getUserPermissions(); // 从缓存或 API 获取用户权限
      return requiredPermissions.every(permission => userPermissions.includes(permission));
    }
    

6. 权限设计的前后端协作

前端通过 角色或权限 来控制页面显示和功能,而后端则要根据每个用户的权限返回不同的数据或响应。例如,某些 API 接口可能只允许管理员访问。

64. 如果让你实现一个前端日志埋点 SDK,你会有什么样的设计思路?

难度:3.5 · 类型:QA

题目要点

实现一个前端日志埋点 SDK 需要考虑数据采集、上报、存储、重试等问题,并兼顾性能优化与用户体验。同时,还需要保证 SDK 的扩展性,以便未来支持更多功能或更复杂的场景。通过合理的设计,可以为产品带来有价值的用户行为数据与性能分析。

参考答案

实现一个前端日志埋点 SDK 是为了能够有效地跟踪和记录用户行为、性能数据以及错误日志,帮助开发者进行数据分析和故障排查。一个好的埋点 SDK 设计需要兼顾灵活性性能可扩展性可靠性

以下是一些设计思路,涵盖核心模块和功能的实现。

1. 基本功能设计

核心目标

  • 用户行为埋点:记录点击、页面访问、表单提交等用户操作。
  • 性能数据采集:收集页面加载时间、资源请求耗时等性能指标。
  • 错误日志记录:捕捉和记录 JavaScript 错误或异常。
  • 数据上报:将埋点数据发送到服务器或日志管理系统。

2. SDK 的架构设计

可以将 SDK 分为以下几个模块:

  1. 初始化模块

    • SDK 需要在页面加载时进行初始化,指定相关配置(如 API 服务器地址、环境配置、采样率等)。
    • 配置项可以允许动态定制,比如开关某些类型的日志记录、设置自定义属性等。
    • 提供全局 init() 方法来接收配置,并完成 SDK 的初始化。
  2. 事件捕获模块

    • 自动捕获:通过 DOM 事件代理机制(addEventListener)来监听点击事件、表单提交、页面跳转等,自动采集用户行为。
    • 自定义埋点:允许开发者通过 SDK 提供的接口主动记录埋点数据。例如 trackEvent() 方法,用于记录自定义事件及其相关信息。
    • 性能埋点:利用浏览器的 Performance API,采集页面加载时间、资源加载时长、DOM 渲染时间等数据。
    • 错误日志捕获:通过监听 window.onerrorwindow.unhandledrejection 捕捉 JavaScript 运行时错误和未处理的 Promise 异常。
  3. 数据存储与缓冲模块

    • 队列机制:埋点数据不应立即上报服务器,避免频繁发送请求。可以将捕获的数据暂存到队列中,并在达到一定数量或定时触发时统一发送。
    • 持久化存储:为应对网络波动或断线情况,SDK 需要将未成功发送的数据暂时保存在浏览器的 LocalStorage 或 SessionStorage 中,并在网络恢复时重新尝试发送。
  4. 上报模块

    • 数据批量上报:提供队列机制,每隔一段时间或达到一定数量后,将埋点数据批量上报至后端日志服务器。
    • 上报策略:支持通过 POSTGET 请求上报数据。可根据日志量及网络状态选择合适的上报方式。
    • 上报时机:可以在页面卸载前(beforeunload 事件)进行最后一次批量上报,确保在用户离开页面时捕获到的日志不会丢失。
    • 可靠性:在上报失败时,支持自动重试机制,并记录上报状态。
  5. 数据格式化与压缩模块

    • 数据格式:通常以 JSON 格式发送埋点数据,包含时间戳、事件类型、页面信息、用户 ID 等信息。
    • 数据压缩:为减小网络传输量,SDK 可以将数据压缩后再发送。比如采用 gzip 等压缩算法。
  6. 插件机制

    • 可扩展性:SDK 可以设计为模块化或插件化,允许用户根据需要加载特定功能模块(如性能监控、错误捕获、页面行为捕获)。
    • 第三方集成:可以提供 API 支持与第三方工具(如 Google Analytics、Mixpanel)集成,方便开发者将数据上报至不同平台。

3. SDK API 设计

// 初始化 SDK,传入配置
LoggerSDK.init({
  apiUrl: 'https://logserver.com/track',  // 日志上报的地址
  appId: 'my-app-id',                     // 应用 ID
  env: 'production',                      // 当前环境:开发、测试、生产
  samplingRate: 0.1,                      // 采样率,1 为全量记录,0.1 为 10% 采样
  autoTrack: true,                        // 是否自动捕获用户行为
  captureErrors: true                     // 是否捕获 JS 错误
});

// 自定义事件埋点
LoggerSDK.trackEvent({
  event: 'button_click',
  elementId: 'submit-button',
  label: '提交按钮点击'
});

// 手动记录页面加载性能
LoggerSDK.trackPerformance();

// 手动记录自定义错误
LoggerSDK.trackError({
  errorType: 'network',
  message: 'Failed to fetch data',
  url: '/api/data'
});

4. 性能优化

  • 懒加载 SDK:通过异步脚本加载,确保 SDK 不影响页面的首屏渲染。
  • 最小化 SDK 体积:使用 Webpack、Rollup 等工具对 SDK 进行打包压缩,减少加载时间。
  • 延迟执行:初始化和数据上报都可以异步进行,避免阻塞页面的其他功能。
  • 采样机制:对埋点进行采样,减少不必要的埋点上报压力。

5. 具体埋点类型

1. 页面浏览埋点

  • 记录页面加载、跳转、刷新行为。
  • 统计页面停留时间。

2. 点击事件埋点

  • 捕获页面上特定元素的点击行为。
  • 统计按钮点击、链接点击等操作。

3. 表单事件埋点

  • 捕获用户在表单中的输入、提交行为。

4. 性能数据埋点

  • 通过 Performance API 获取页面加载时间、首屏时间、资源加载时间等关键性能指标。

5. 错误日志埋点

  • 捕获 JS 运行错误和未处理 Promise 错误。
  • 捕捉并上报网络请求失败、资源加载错误等。

6. 安全性与隐私保护

  • 数据加密:上报的埋点数据应使用 HTTPS 传输,确保安全性。
  • 隐私合规:避免收集用户隐私信息(如敏感的用户数据),并确保 SDK 合规于 GDPR、CCPA 等隐私保护法规。

7. 数据展示与分析

提供后台管理系统或接口,支持:

  • 实时展示:提供实时展示埋点数据的能力,如点击次数、错误次数等。
  • 数据报表:生成定期的行为报表,帮助分析用户行为模式。
  • 告警机制:当系统出现大量错误时,提供告警通知(如通过邮件或第三方平台)。

65. 在项目中封装组件时,你会有些什么样的原则?

难度:3 · 类型:QA

题目要点

封装组件时遵循单一职责、高内聚低耦合、参数可配置性、样式隔离、良好的错误处理和性能优化等原则,确保组件可复用、易维护、性能高并具备清晰的文档。

参考答案

在封装组件时,可以遵循以下几个核心原则,以确保组件的复用性、可维护性和可靠性:

1. 单一职责原则

  • 每个组件只负责一个功能或任务,保持组件的单一性。这样可以让组件更加独立,易于测试和复用,减少互相依赖带来的复杂性。

2. 可复用性

  • 设计组件时尽量做到通用化,避免硬编码与业务逻辑耦合。通过 propsslots 等传入自定义内容,使组件在不同场景下都能复用。

3. 高内聚、低耦合

  • 内部实现尽可能封装,暴露的接口应简单、清晰。组件应依赖较少的外部资源和上下文环境,以便能在不同项目或页面中独立使用。

4. 参数可配置性

  • 提供灵活的 props 以满足不同需求,同时设置合理的默认值,提升易用性。避免 props 过多或过于复杂,以保持组件接口的简洁性。

5. 明确的输入输出

  • 确保组件的输入 (props) 和输出 (events) 清晰且文档化,使组件行为在外部清晰可控。尽量使用 TypeScript 定义组件的 propsevents 类型,保证类型安全。

6. 隔离样式

  • 组件样式应尽量隔离,避免影响全局样式。使用 CSS ModulesscopedCSS-in-JS 等方式来实现样式隔离,同时为组件提供自定义样式的入口,以便在不同场景中灵活调整。

7. 无状态化

  • 优先使用无状态组件(纯组件),让父组件通过 props 控制组件的状态。对于需要内部状态的组件,确保状态变化对外透明,并提供更新接口或事件,使父组件可以追踪和控制。

8. 良好的错误处理

  • 组件内部应具备基本的错误处理机制,避免出现不明错误或崩溃。同时在设计 API 时,应对传入的 props 做必要的校验,提供易懂的错误信息。

9. 清晰的文档

  • 为组件提供详细的文档说明,包括使用方法、传入的 props、事件回调、样式定制等,帮助其他开发者快速理解和使用组件。

10. 性能优化

  • 通过合理使用 memoizationshouldComponentUpdateReact.memo 或 Vue 的 computedwatch 等优化渲染性能,避免不必要的重新渲染,提升组件效率。

66. 前端系统中,怎么统一处理登录态过期的场景?

难度:1.5 · 类型:QA

题目要点

通过拦截请求、Token 刷新、用户超时退出、原页面重定向等策略,前端可以统一、友好地处理登录态过期场景,避免用户频繁登录,同时提高系统的安全性和用户体验。

参考答案

统一处理登录态过期的场景可以通过以下几个步骤实现:

1. 拦截请求响应,检查状态码

  • 实现思路:在发送请求或接收响应时进行拦截,通过 HTTP 状态码或后端自定义的错误码判断用户的登录状态是否过期。例如,后端可以返回 401(Unauthorized)状态码,表示需要重新登录。
  • 具体实现:使用 Axios 或 Fetch 进行请求拦截,例如 Axios 提供的 interceptors 可以在所有请求或响应的入口处检查状态,遇到 401 统一跳转登录。

示例代码(使用 Axios 拦截器):

import axios from 'axios';
import { redirectToLogin } from './auth'; // 定义重定向到登录页的函数

axios.interceptors.response.use(
  response => response, // 正常响应
  error => {
    if (error.response && error.response.status === 401) {
      // 登录态过期处理
      redirectToLogin();
    }
    return Promise.reject(error);
  }
);

2. 封装统一的请求方法

  • 实现思路:封装一个通用的 request 方法,用于管理所有 API 请求,并在该方法中统一处理登录态过期问题。当遇到过期状态码时,集中处理跳转逻辑或刷新令牌。
  • 具体实现:封装后的 request 方法可以统一管理拦截器逻辑,并添加登录态过期的逻辑判断。

示例代码:

async function request(url, options) {
  try {
    const response = await fetch(url, options);
    if (response.status === 401) {
      redirectToLogin(); // 重定向到登录
      return;
    }
    return await response.json();
  } catch (error) {
    console.error('请求失败:', error);
  }
}

3. 使用 Token 续期机制

  • 实现思路:采用 Token 刷新机制,在前端存储 accessTokenrefreshToken,如果 accessToken 过期,则调用刷新接口获取新的 accessToken,避免用户频繁跳转到登录页。
  • 具体实现:当遇到 401 错误时,可以调用刷新接口更新 accessToken。若刷新失败,再跳转至登录页。

示例代码:

axios.interceptors.response.use(
  response => response,
  async error => {
    const { response } = error;
    if (response.status === 401) {
      try {
        const refreshResponse = await axios.post('/refresh-token', {
          refreshToken: localStorage.getItem('refreshToken')
        });
        localStorage.setItem('accessToken', refreshResponse.data.accessToken);
        error.config.headers['Authorization'] = `Bearer ${refreshResponse.data.accessToken}`;
        return axios(error.config);
      } catch (refreshError) {
        redirectToLogin();
      }
    }
    return Promise.reject(error);
  }
);

4. 用户操作超时自动登出

  • 实现思路:设置用户不活跃超时时间,如果用户在指定时间内无操作,则触发登出操作,清除登录态并跳转到登录页面。
  • 具体实现:可以在全局监听 mousemovekeydown 等事件,重置倒计时。倒计时结束触发登录态过期逻辑。

示例代码:

let timeoutId;
const logoutAfterTimeout = () => {
  clearTimeout(timeoutId);
  timeoutId = setTimeout(() => {
    alert('登录超时,请重新登录');
    redirectToLogin();
  }, 30 * 60 * 1000); // 30 分钟超时
};

window.addEventListener('mousemove', logoutAfterTimeout);
window.addEventListener('keydown', logoutAfterTimeout);

5. 重定向至原页面

  • 实现思路:用户被重定向至登录页后,登录成功应自动跳转回之前试图访问的页面。
  • 具体实现:记录用户访问的页面 URL,作为参数传递给登录页。登录后根据该 URL 重定向回原页面,提升用户体验。

示例代码:

function redirectToLogin() {
  const currentPath = window.location.pathname;
  window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
}

67. 常见的登录鉴权方式有哪些?

难度:3 · 类型:QA

题目要点

不同的鉴权方式适合不同场景,选择适合的方案需要考虑系统架构、安全性、用户体验以及成本。常见的 Web 应用通常使用 JWTCookie + Session,而企业级应用偏向 SSO 或 SAML 等统一认证协议。

参考答案
  • 原理:用户登录后,服务器创建一个 Session,将用户信息存储在服务器端,同时生成一个唯一的 SessionID,并将其以 Cookie 的形式发送给客户端。客户端之后的每次请求都携带该 Cookie,服务器用 SessionID 确认用户身份。
  • 优点:服务器管理会话状态,安全性较高;可以设置短期有效期,适合敏感信息。
  • 缺点:会消耗服务器内存,难以扩展;需跨域配置。

2. JWT(JSON Web Token)

  • 原理:登录成功后,服务器生成一个加密的 JWT,该令牌包含用户信息或标识,发送给客户端。之后的请求,客户端携带该 JWT,服务器通过验证 JWT 的签名来确认用户身份。
  • 优点:无状态、可跨域,适合分布式架构;可携带信息,支持用户角色验证。
  • 缺点:令牌较长,增加请求负担;一旦泄露容易被滥用。

3. OAuth

  • 原理:OAuth 是一种授权协议,用户通过第三方平台(如 Google、Facebook)登录,将权限授予该平台来访问某些资源。OAuth 2.0 使用 Access Token 访问受保护资源,Refresh Token 则用于获取新的 Access Token
  • 优点:用户体验好,适用于第三方应用授权登录;支持多级权限。
  • 缺点:流程复杂,难以实现;Token 可能泄露或失效。

4. SAML(Security Assertion Markup Language)

  • 原理:SAML 是基于 XML 的单点登录协议,通常用于企业系统中。用户请求通过 SAML 协议在多个系统之间共享认证状态,允许用户在不重新登录的情况下访问其他应用。
  • 优点:适用于企业内部系统间的统一登录;支持跨域。
  • 缺点:复杂度高,基于 XML 的消息传递效率低。

5. SSO(Single Sign-On)

  • 原理:单点登录允许用户使用一个登录凭据访问多个应用或系统。常用的实现方案包括基于 OAuth、SAML 或 CAS(Central Authentication Service)。
  • 优点:提高用户体验,避免多次登录;适用于多系统集成。
  • 缺点:一旦账户泄露,多个系统都会受到威胁。

6. 基于短信或邮件的 OTP(一次性密码)

  • 原理:用户登录时接收一个一次性密码(通常通过短信或邮件发送),有效期较短。用户输入正确的 OTP 后,即可完成登录。
  • 优点:安全性高,适合敏感操作的二次验证。
  • 缺点:需要借助短信或邮件服务,成本较高。

7. 指纹、面部识别等生物识别

  • 原理:利用设备上的生物识别传感器进行身份认证,例如指纹、面部识别等。通常用于设备级别的解锁,或结合其他鉴权方式进行身份确认。
  • 优点:用户体验佳、难以复制;适合移动设备的安全验证。
  • 缺点:设备依赖性强;难以在 web 应用中普遍使用。

68. 如果需要使用 JS 执行 100 万个任务,如何保证浏览器不卡顿?

难度:3.5 · 类型:QA

题目要点

为了保证浏览器不卡顿,核心思路是避免长时间占用主线程,让浏览器有机会执行渲染和其他任务。

具体的方法有:

  1. 使用 分块处理空闲时间调度
  2. 利用 Web Workers 将任务移到子线程。
  3. 结合 Promise异步操作 处理任务。
参考答案

1. 使用分块处理(Chunking)

将 100 万个任务分成小块,逐块处理,每块处理完成后将控制权交还给浏览器,利用空闲时间继续处理。

实现方式:setTimeoutsetInterval

function processInChunks(tasks, chunkSize = 100) {
  function processChunk() {
    const chunk = tasks.splice(0, chunkSize);
    chunk.forEach(task => task());
    if (tasks.length > 0) {
      setTimeout(processChunk, 0); // 让出主线程
    }
  }
  processChunk();
}

// 示例
const tasks = Array.from({ length: 1000000 }, (_, i) => () => console.log(i));
processInChunks(tasks);

2. 使用浏览器空闲时间:requestIdleCallback

requestIdleCallback 可以利用浏览器的空闲时间执行任务,避免占用主线程的关键资源时间。

function processWithIdleCallback(tasks) {
  function processChunk(deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0) {
      const task = tasks.shift();
      task();
    }
    if (tasks.length > 0) {
      requestIdleCallback(processChunk);
    }
  }
  requestIdleCallback(processChunk);
}

// 示例
const tasks = Array.from({ length: 1000000 }, (_, i) => () => console.log(i));
processWithIdleCallback(tasks);

3. 使用 Web Workers

将任务放到 Web Worker 中执行,避免阻塞主线程。

主线程代码:

const worker = new Worker("worker.js");
worker.postMessage(1000000); // 发送任务数量
worker.onmessage = (e) => {
  console.log(e.data); // 接收 worker 处理结果
};

Worker 脚本(worker.js):

onmessage = (e) => {
  const tasks = e.data;
  for (let i = 0; i < tasks; i++) {
    // 模拟任务
  }
  postMessage("All tasks completed!");
};

4. 批处理和任务调度器

创建自定义任务调度器,根据优先级和剩余时间动态分配任务。

class Scheduler {
  constructor() {
    this.tasks = [];
  }

  add(task) {
    this.tasks.push(task);
  }

  run(chunkSize = 100) {
    const execute = () => {
      const chunk = this.tasks.splice(0, chunkSize);
      chunk.forEach(task => task());
      if (this.tasks.length > 0) {
        setTimeout(execute, 0);
      }
    };
    execute();
  }
}

// 示例
const scheduler = new Scheduler();
for (let i = 0; i < 1000000; i++) {
  scheduler.add(() => console.log(i));
}
scheduler.run();

5. 使用 async/await 与微任务

利用 Promiseawait 将任务切分到微任务队列中,减少对主线程的持续占用。

async function processTasks(tasks) {
  for (let i = 0; i < tasks.length; i++) {
    tasks[i]();
    if (i % 100 === 0) {
      await new Promise(resolve => setTimeout(resolve, 0)); // 让出主线程
    }
  }
}

// 示例
const tasks = Array.from({ length: 1000000 }, (_, i) => () => console.log(i));
processTasks(tasks);

69. 对于分页的列表,怎么解决快速翻页场景下的竞态问题?

难度:3 · 类型:QA

题目要点

解决分页列表快速翻页场景下的竞态问题,需要根据项目需求选择合适的方案:

  • 如果需要实时性强且无需保存历史请求结果,优先使用唯一标识控制(方案 2)
  • 如果页面频繁访问同一页,优先使用请求缓存(方案 3)
  • 如果用户交互频繁,建议结合 防抖机制(方案 4) 减少无效请求。
  • 如果需要更精细的控制,可以结合后端能力实现优先级处理。

通过这些方案,可以确保分页列表在快速翻页场景下始终显示用户期望的数据。

参考答案

在分页列表中,快速翻页可能导致竞态问题(race condition),即较早发起的请求返回数据覆盖了后发起请求的数据,导致页面显示错误的内容。

竞态问题的产生场景

  1. 用户快速翻页,比如从第 1 页直接跳到第 10 页。
  2. 第 1 页的请求先发起,但第 10 页的请求后发起。
  3. 如果第 1 页的请求响应比第 10 页的请求响应更快,页面可能会显示第 1 页的数据,而非用户期望的第 10 页。

解决竞态问题的方案

1. 通过唯一标识取消过时的请求

使用请求的唯一标识(如页码)来取消过时的请求,避免页面加载不需要的数据。

实现方法

借助 axios 提供的 CancelToken 或 Fetch 的 AbortController:

let cancelToken;

function fetchPage(pageNumber) {
  if (cancelToken) {
    cancelToken.cancel("Request canceled due to new request.");
  }
  cancelToken = axios.CancelToken.source();

  return axios.get(`/api/data?page=${pageNumber}`, {
    cancelToken: cancelToken.token,
  });
}

2. 仅渲染最后一次请求的数据

通过记录当前的有效请求标识,仅允许最后一次请求返回的数据更新页面。

实现方法

在发送请求时生成标识,并在请求返回时比对:

let currentRequestId = 0;

function fetchPage(pageNumber) {
  const requestId = ++currentRequestId;

  return axios.get(`/api/data?page=${pageNumber}`).then((response) => {
    if (requestId === currentRequestId) {
      // 只处理最后一次请求的响应
      renderData(response.data);
    }
  });
}

3. 缓存请求结果

对于快速翻页场景,可能存在页码反复访问的情况。通过缓存请求结果,可以直接使用已有数据,而不是重复发送请求。

实现方法

使用一个 Map 存储页码和对应数据:

const cache = new Map();

async function fetchPage(pageNumber) {
  if (cache.has(pageNumber)) {
    return renderData(cache.get(pageNumber));
  }

  const response = await axios.get(`/api/data?page=${pageNumber}`);
  cache.set(pageNumber, response.data);
  renderData(response.data);
}

4. 限制请求频率

使用防抖或节流机制,减少过于频繁的请求发起。

实现方法

使用防抖函数控制请求触发:

function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const fetchPageDebounced = debounce(fetchPage, 300);

5. 后端支持分页请求的取消或优先级控制

如果后端能够识别请求的优先级,可以通过返回特定状态码(如 409 Conflict)告知前端忽略掉过时的请求。

70. 如何检测网页空闲状态(即一定时间内无操作)?

难度:3 · 类型:QA

题目要点

总结

  • 使用常见的用户交互事件来监听用户的活动,如 mousemovekeydown 等。
  • 结合 setInterval 来计算用户空闲时间。
  • requestIdleCallback 可用于在浏览器空闲时执行任务,提升性能。

PS:chrome浏览器其实提供了一个Idle DetectionAPI,来实现网页空闲状态的检测,但是这个API还是一个实验性特性,并且Firefox与Safari不支持。

参考答案

可以使用 JavaScript 来监听用户的活动(如鼠标、键盘等事件),并结合 定时器 来判断一段时间内是否没有任何操作,从而判断网页是否处于空闲状态。

1. 监听用户活动

通常,我们会监听以下几种常见的用户交互事件:

  • mousemove:鼠标移动
  • keydown:键盘按下
  • scroll:滚动
  • click:点击事件

这些事件可以帮助我们检测用户是否在与页面交互。

2. 设置空闲检测机制

使用 setTimeoutsetInterval 配合上述事件,来判断在一段时间内(例如 5 秒)用户是否没有任何操作。若超时,判定为空闲状态。

示例代码

let idleTime = 0; // 记录用户空闲的时间
let idleThreshold = 5 * 60 * 1000; // 5分钟,单位为毫秒

// 增加事件监听器,监听常见用户操作
const resetIdleTimer = () => {
  idleTime = 0; // 用户操作时,重置空闲时间
};

// 监听用户活动
document.addEventListener('mousemove', resetIdleTimer);
document.addEventListener('keydown', resetIdleTimer);
document.addEventListener('scroll', resetIdleTimer);
document.addEventListener('click', resetIdleTimer);

// 定时器检查是否空闲
setInterval(() => {
  idleTime += 1000; // 每秒增加空闲时间

  if (idleTime >= idleThreshold) {
    console.log('User is idle for 5 minutes');
    // 在这里执行用户空闲时的逻辑
  }
}, 1000);

工作原理

  • resetIdleTimer:每次用户操作时(如移动鼠标、按键等),都会重置空闲时间为 0。
  • setInterval:每秒检查一次 idleTime,如果空闲时间达到设置的阈值(idleThreshold),则认为用户处于空闲状态。

3. 空闲状态后的处理

当检测到空闲状态时,你可以执行一些操作,例如:

  • 显示提示信息:例如弹出提示用户是否继续浏览或注销用户。
  • 自动登出:例如用户空闲超过一定时间后,自动注销用户或跳转到登录页。
  • 播放动画:例如展示空闲状态的动画或隐藏某些元素。

4. 高级优化:使用 requestIdleCallback

如果浏览器支持 requestIdleCallback,可以用它来检测页面是否处于空闲状态。requestIdleCallback 是一种优化的方式,可以在浏览器空闲时执行某些任务,不会阻塞主线程。

let idleTime = 0;
let idleThreshold = 5 * 60 * 1000; // 5分钟,单位为毫秒

const checkIdleState = (deadline) => {
  // 如果页面有空闲时间,增加空闲时间
  if (deadline.timeRemaining() > 0 || deadline.didTimeout) {
    idleTime += 1000;

    if (idleTime >= idleThreshold) {
      console.log('User is idle for 5 minutes');
      // 在这里执行用户空闲时的逻辑
    }
  }

  // 下一帧继续检测
  requestIdleCallback(checkIdleState, { timeout: 1000 });
};

// 初始启动空闲检测
requestIdleCallback(checkIdleState, { timeout: 1000 });

71. 前端应用上线后,怎么通知用户刷新当前页面?

难度:3.5 · 类型:QA

题目要点

通知用户刷新页面的方式主要有以下几种:

  1. Service Worker:利用 Service Worker 在后台检查资源的更新,发现新版本时通知用户刷新页面。
  2. WebSocket:通过 WebSocket 实时推送更新通知,适用于更复杂的系统。
  3. 版本检测 API:定期请求服务器接口检查版本信息,发现不一致时弹出提示。
  4. 轮询:定时轮询服务器,检查是否有新版本,适用于简单场景。
参考答案

在前端应用上线后,通知用户刷新页面通常是为了让他们能够获取到最新的资源或功能。常见的做法是通过 Service Workers 或者 WebSocket 等方式监测版本变化,并在检测到版本更新时弹出提示,提醒用户刷新页面。

以下是几种常见的实现方法:

1. 利用 Service Worker 实现页面自动刷新通知

如果应用使用了 PWA(渐进式Web应用) 或者 Service Worker,可以利用 Service Worker 来监听资源缓存的变化,当应用的资源有更新时,通知用户刷新页面。

  • 步骤:

    1. 使用 Service Worker 来缓存静态资源(如 JS、CSS 文件)。
    2. 当有新的版本的文件推送到服务器时,Service Worker 会检测到这些文件的变化。
    3. 在检测到新版本时,Service Worker 可以向前端发送一个消息,通知用户刷新页面。
  • 示例代码:

    service-worker.js

    self.addEventListener('install', (event) => {
      self.skipWaiting();  // 强制等待中的 Service Worker 激活
    });
    
    self.addEventListener('activate', (event) => {
      event.waitUntil(
        caches.keys().then((cacheNames) => {
          return Promise.all(
            cacheNames.map((cacheName) => {
              return caches.delete(cacheName);  // 清除旧的缓存
            })
          );
        })
      );
    });
    
    self.addEventListener('fetch', (event) => {
      event.respondWith(
        caches.match(event.request).then((cachedResponse) => {
          if (cachedResponse) {
            return cachedResponse;
          }
          return fetch(event.request);
        })
      );
    });
    

    前端通知用户刷新页面

    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/service-worker.js')
        .then(registration => {
          registration.onupdatefound = () => {
            const installingWorker = registration.installing;
            installingWorker.onstatechange = () => {
              if (installingWorker.state === 'installed') {
                if (navigator.serviceWorker.controller) {
                  // 新版本可用,通知用户刷新页面
                  alert('新版本的应用可用,点击刷新页面');
                }
              }
            };
          };
        })
        .catch(error => console.error('Service Worker 注册失败:', error));
    }
    
  • 优点:

    • 自动缓存和更新资源,用户在后台自动更新资源。
    • 如果在安装阶段检测到新版本,可以在没有刷新页面的情况下提前通知用户。

2. WebSocket 或 长轮询通知更新

通过 WebSocket 或 HTTP 长轮询,可以实现实时的版本检测,当服务器上有新版本发布时,直接通知客户端用户刷新页面。

  • 步骤:

    1. 在应用启动时,建立一个 WebSocket 连接,持续监听是否有新的版本发布。
    2. 当检测到新版本时,通过 WebSocket 推送消息给客户端。
    3. 客户端收到消息后,弹出提示,通知用户刷新页面。
  • 示例代码:

    const socket = new WebSocket('ws://your-server-url');
    
    socket.addEventListener('message', function (event) {
      const data = JSON.parse(event.data);
      if (data.action === 'UPDATE_AVAILABLE') {
        // 当检测到新版本时,通知用户刷新页面
        if (confirm('新版本已发布,是否刷新页面?')) {
          window.location.reload();
        }
      }
    });
    
  • 优点:

    • 实时通知,不需要轮询。
    • 可以灵活地向特定用户推送消息,适用于有多版本管理的复杂应用。

3. 版本检查与自动刷新

另一种简单的方式是通过 API 轮询请求服务器的版本信息,检查当前应用是否为最新版本。如果有新版本,则显示通知,提示用户刷新页面。

  • 步骤:

    1. 客户端可以定期请求一个版本检测接口,比较当前版本和服务器上最新的版本。
    2. 如果版本不一致,弹出提示让用户刷新页面。
  • 示例代码:

    // 假设我们有一个接口来获取当前最新的版本
    setInterval(() => {
      fetch('/api/version')
        .then(response => response.json())
        .then(data => {
          const currentVersion = '1.0.0';  // 当前客户端的版本
          if (data.version !== currentVersion) {
            alert('新版本的应用可用,点击刷新页面');
          }
        })
        .catch(error => console.error('无法获取版本信息:', error));
    }, 60000); // 每 1 分钟检查一次
    
  • 优点:

    • 实现简单,不依赖复杂的技术(如 WebSocket)。
    • 可以与其他版本控制系统结合使用。

4. 后台定时轮询

另一个简单的实现是定时轮询检查前端代码版本,当检测到前后端版本不一致时,显示刷新提示。

  • 示例代码:
    const checkForUpdates = () => {
      fetch('/api/check-for-updates')
        .then(response => response.json())
        .then(data => {
          if (data.updateAvailable) {
            alert('新版本上线了,请刷新页面!');
          }
        })
        .catch(error => console.log('检查更新失败', error));
    };
    
    // 每 30 秒检查一次
    setInterval(checkForUpdates, 30000);
    

72. 如何从 0 到 1 搭建前端基建?

难度:4 · 类型:QA

题目要点

搭建前端基建是一个包含多个步骤的过程。简要总结:

  1. 项目初始化:选择适合的技术栈、初始化项目。
  2. 代码质量控制:配置 ESLint、Prettier、Git Hook,确保代码质量。
  3. 构建工具和优化:选择适合的构建工具(Vite、Webpack),配置代码分割、缓存、压缩等。
  4. 自动化构建与部署:配置 CI/CD 流程和 Docker 容器化部署。
  5. 测试:配置单元测试、E2E 测试,确保代码健壮性。
  6. 监控与性能优化:使用性能分析工具、错误追踪工具,确保应用性能和可靠性。

搭建前端基建的目标是为开发提供一个高效、可扩展、稳定的开发环境,并确保应用上线后的可靠性与性能。

参考答案

从 0 到 1 搭建前端基建,意味着你要为一个新的项目或者团队,构建一个健全、可扩展的前端开发环境和工具链。以下是一个从零开始搭建前端基础设施的步骤,涵盖了项目初始化、代码质量控制、开发流程优化、构建部署等方面。

1. 项目初始化

搭建前端基建的第一步是初始化项目,通常可以选择适合团队和项目需求的工具和框架。

1.1 选择技术栈

  • 前端框架:React、Vue、Angular 或者是简单的 Vanilla JS。
  • 状态管理:Redux、Pinia、Vuex 等。
  • UI 库:Ant Design、Material-UI、Bootstrap 等。

1.2 初始化项目

选择适合的脚手架工具进行初始化,如:

  • 使用 ViteWebpackCreate React App 初始化项目。
  • 如果项目需要 Vue,可以使用 Vue CLIVite
  • 如果是 Angular 项目,使用 Angular CLI
# React + Vite 项目初始化
npm create vite@latest my-app --template react

# Vue 3 + Vite 项目初始化
npm create vite@latest my-app --template vue

# Angular 项目初始化
ng new my-app

1.3 安装常用依赖

  • 开发依赖:如 Babel、TypeScript、ESLint、Prettier。
  • UI 组件库:如 Ant Design、Material-UI、Tailwind CSS。
  • 路由:如 React Router、Vue Router。
  • 状态管理:如 Redux、Pinia。
# 安装基础依赖
npm install react-router-dom @reduxjs/toolkit axios
npm install --save-dev typescript eslint prettier

2. 代码质量控制

搭建前端基建时,确保代码质量的可维护性非常重要。这需要配置一些开发工具和规范。

2.1 配置 ESLint 和 Prettier

  • ESLint 用于检查代码风格、语法错误等,Prettier 用于自动格式化代码。
# 安装 ESLint 和 Prettier
npm install --save-dev eslint prettier eslint-plugin-react eslint-config-prettier

在项目中配置 .eslintrc.js.prettierrc 文件。

// .eslintrc.js 示例
module.exports = {
  parser: "@babel/eslint-parser",
  extends: ["eslint:recommended", "plugin:react/recommended", "prettier"],
  rules: {
    "react/prop-types": "off"
  }
};
// .prettierrc 示例
{
  "singleQuote": true,
  "semi": false,
  "trailingComma": "all"
}

2.2 设置 Git Hook

使用 Git Hook 来在提交时执行 lint 检查和代码格式化。例如,可以使用 Huskylint-staged 来实现。

# 安装 Husky 和 lint-staged
npm install --save-dev husky lint-staged

package.json 中配置:

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.js": "eslint --fix",
    "*.css": "prettier --write"
  }
}

3. 代码构建与优化

配置前端构建工具,确保代码在开发和生产环境中都能高效运行。

3.1 选择构建工具

  • Webpack:功能强大,适合大型项目,可以处理 JS、CSS、图片、字体等资源的打包。
  • Vite:更现代、更快速,适合中小型项目,支持热更新和快速构建。

对于 Vite 项目,可以直接使用默认配置。

# 安装 Vite
npm install --save-dev vite

对于 Webpack 项目,可以选择合适的配置(如 React、Vue 配置等)。

3.2 配置 Babel 转译

Babel 是现代 JavaScript 转译工具,可以将 ES6+ 的代码转换为兼容性更好的 ES5 代码,确保在旧版浏览器中也能运行。

# 安装 Babel 和相关插件
npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader

配置 .babelrcbabel.config.js

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

3.3 配置代码分割

代码分割可以提升加载性能,减少初次加载的包体积。使用 Webpack 或 Vite 的内建功能进行配置。

在 Webpack 中:

optimization: {
  splitChunks: {
    chunks: "all"
  }
}

在 Vite 中,通常默认启用了代码分割,直接使用即可。

3.4 配置缓存和压缩

为生产环境启用缓存、Gzip 或 Brotli 压缩,减小静态资源文件大小。

在 Webpack 中,可以使用 webpack-bundle-analyzercompression-webpack-plugin 插件。

npm install --save-dev webpack-bundle-analyzer compression-webpack-plugin

4. 自动化构建与部署

为了实现持续集成和持续部署(CI/CD),可以使用 GitHub Actions、GitLab CI、Jenkins 等工具。

4.1 配置 CI/CD

例如,使用 GitHub Actions 设置自动化流程:

  • 自动运行测试。
  • 构建生产环境的代码。
  • 将构建后的代码部署到服务器或云平台(如 Netlify、Vercel、AWS、GCP 等)。

4.2 配置 Docker

如果项目需要容器化部署,可以创建一个 Dockerfiledocker-compose.yml 文件,将项目打包成 Docker 镜像进行部署。

# 基于 Node 镜像构建
FROM node:16

# 设置工作目录
WORKDIR /app

# 复制依赖文件并安装
COPY package*.json ./
RUN npm install

# 复制项目代码
COPY . .

# 运行构建命令
RUN npm run build

# 启动应用
CMD ["npm", "start"]

5. 测试框架和工具

搭建前端基建时,务必配置自动化测试工具,确保代码质量。

5.1 单元测试

使用 JestReact Testing Library(React 项目)或 MochaChai 来进行单元测试。

# 安装 Jest 和 React Testing Library
npm install --save-dev jest @testing-library/react @testing-library/jest-dom

5.2 E2E 测试

使用 CypressPlaywright 来进行端到端(E2E)测试。

# 安装 Cypress
npm install --save-dev cypress

5.3 测试覆盖率

使用 IstanbulJest 自带的覆盖率功能,确保代码的测试覆盖率。

# 使用 Jest 进行测试覆盖率
jest --coverage

6. 监控和性能优化

上线后,需要监控应用的性能和用户的体验。

6.1 性能分析

使用 LighthouseWeb Vitals 来进行性能分析。

npm install --save-dev web-vitals

6.2 错误追踪

集成 SentryLogRocket 等工具,追踪生产环境中的错误。

73. 在表单校验场景中, 如何实现页面视口滚动到报错的位置

难度:3 · 类型:QA

题目要点

在表单校验场景中,当用户提交表单并发生验证错误时,通常需要将页面滚动到第一个报错的位置,以便用户能够清楚地看到并修正错误。

参考答案

在表单校验场景中,当用户提交表单并发生验证错误时,通常需要将页面滚动到第一个报错的位置,以便用户能够清楚地看到并修正错误。

实现思路

  1. 在表单提交时进行验证:通过监听表单的提交事件或触发验证事件,进行表单的字段校验。
  2. 查找第一个错误的位置:遍历表单字段,找到第一个校验失败的字段,获取其位置。
  3. 滚动页面到错误位置:使用浏览器的 scrollIntoView() 方法将该字段滚动到视口可见区域。

具体实现步骤

  1. 定义一个表单:使用 HTML 创建表单,配合各个输入字段和校验提示。
  2. 验证表单:通过 JavaScript 校验表单字段的值。
  3. 滚动到第一个错误字段:当发现错误时,使用 scrollIntoView() 滚动到对应的输入框。

示例代码

假设有一个表单,包含多个输入框,需要进行简单的非空验证:

<form id="myForm">
  <div class="form-group">
    <label for="name">Name</label>
    <input type="text" id="name" name="name">
    <span class="error-message" id="nameError"></span>
  </div>

  <div class="form-group">
    <label for="email">Email</label>
    <input type="email" id="email" name="email">
    <span class="error-message" id="emailError"></span>
  </div>

  <div class="form-group">
    <label for="age">Age</label>
    <input type="number" id="age" name="age">
    <span class="error-message" id="ageError"></span>
  </div>

  <button type="submit">Submit</button>
</form>

JavaScript 代码

// 表单校验
function validateForm(event) {
  event.preventDefault();  // 阻止表单提交,避免页面刷新

  // 清空之前的错误信息
  const errorMessages = document.querySelectorAll('.error-message');
  errorMessages.forEach(msg => msg.textContent = '');

  // 获取表单字段
  const name = document.getElementById('name');
  const email = document.getElementById('email');
  const age = document.getElementById('age');

  let isValid = true;
  let firstErrorElement = null;

  // 校验每个字段
  if (name.value.trim() === '') {
    isValid = false;
    document.getElementById('nameError').textContent = 'Name is required.';
    firstErrorElement = firstErrorElement || name;  // 记录第一个错误字段
  }

  if (email.value.trim() === '') {
    isValid = false;
    document.getElementById('emailError').textContent = 'Email is required.';
    firstErrorElement = firstErrorElement || email;
  }

  if (age.value.trim() === '') {
    isValid = false;
    document.getElementById('ageError').textContent = 'Age is required.';
    firstErrorElement = firstErrorElement || age;
  }

  // 如果有错误,滚动到第一个错误字段
  if (!isValid && firstErrorElement) {
    firstErrorElement.scrollIntoView({
      behavior: 'smooth', // 平滑滚动
      block: 'center' // 滚动到视口中间
    });
  }

  return isValid;
}

// 绑定表单提交事件
const form = document.getElementById('myForm');
form.addEventListener('submit', validateForm);

代码解释

  1. 表单校验函数 validateForm:当表单提交时,首先阻止默认行为,防止页面刷新。然后,检查每个输入框的值是否为空。如果某个字段验证失败,就显示对应的错误信息,并记录第一个错误元素。

  2. scrollIntoView() 方法:如果校验失败,并且找到了第一个错误字段,通过 scrollIntoView() 方法将该元素滚动到视口中间。这里使用了 smooth 选项来实现平滑滚动效果。

  3. firstErrorElement:用于记录第一个报错的元素,确保滚动到第一个错误的位置。

  4. 错误信息显示:每个输入框旁边都有一个 span 元素用于显示相应的错误信息。

其他补充

  1. 滚动位置优化

    • 你可以调整 scrollIntoView()block 参数,以控制滚动时的对齐方式。常见的值有:
      • start:元素顶部与视口顶部对齐。
      • center:元素与视口中心对齐。
      • end:元素底部与视口底部对齐。
  2. 支持多个错误提示

    • 如果需要支持多个错误提示(例如,表单中的多个字段可能同时出错),你可以扩展逻辑,依次显示所有字段的错误信息,并滚动到第一个错误的字段。
  3. 兼容性

    • scrollIntoView() 方法在现代浏览器中得到了广泛的支持,但如果你需要兼容更旧的浏览器(如 IE),可能需要使用其他方法(例如:通过 window.scroll() 实现)。
  4. 自定义验证规则

    • 你可以根据业务需求扩展验证规则,如正则校验邮箱格式、密码强度检查等。

74. 前端渲染和后端渲染分别有什么优缺点, 为什么现在的技术大方向,又逐渐往“后端渲染”方向靠了呢?

难度:3 · 类型:QA

题目要点

  • 前端渲染(CSR) 更适合交互性强、后端接口稳定、数据动态变化频繁的应用(如单页应用)。
  • 后端渲染(SSR) 更适合对 SEO 和首屏加载速度有高要求的应用,尤其是当页面的内容较为静态时(例如博客、电商等)。

随着技术的发展,很多现代框架(如 Next.jsNuxt.js)已经允许开发者使用 SSR + CSR 混合渲染,即在首屏使用 SSR 加速渲染,在后续页面交互中使用 CSR 提供流畅的用户体验。因此,随着对 SEO首屏加载速度 的关注增加,SSR 成为越来越多前端应用的主流选择。

参考答案

前端渲染与后端渲染的优缺点

前端渲染(Client-Side Rendering,CSR)和后端渲染(Server-Side Rendering,SSR)是两种常见的网页渲染方式,每种方式在性能、SEO、开发体验等方面都有不同的优缺点。

1. 前端渲染(CSR)

前端渲染是指将 HTML、CSS 和 JavaScript 一起发送给浏览器,浏览器负责渲染页面。JavaScript 运行在浏览器中,负责获取数据、生成 HTML 和动态更新页面。

优点:
  • 更好的用户体验

    • 前端渲染的应用通常会更流畅,因为页面加载后,用户和应用之间的交互不会频繁依赖后端。
    • 使用单页应用(SPA)框架(如 React、Vue、Angular)时,页面的切换速度更快,因为后续的页面加载只是局部更新,而不需要整个页面刷新。
  • 前端控制更灵活

    • 开发者可以自由控制前端路由、动画效果等,不需要频繁请求后端数据。
    • 在前端进行业务逻辑处理、状态管理和交互,减少了后端的负担。
  • 服务器负担较轻

    • 服务器主要负责数据接口,不需要处理页面渲染,减轻了服务器的压力。
缺点:
  • 首屏渲染速度较慢

    • 首次加载时,浏览器需要下载和解析 JavaScript 文件,执行代码,获取数据并渲染页面。这意味着在 JavaScript 文件下载和执行之前,用户无法看到有效内容,导致首屏加载慢。
  • SEO 不友好

    • 搜索引擎爬虫可能无法有效抓取由 JavaScript 动态渲染的内容。尽管现代的搜索引擎可以渲染 JavaScript,但效果和效率不如直接获取 HTML 内容。
  • 对用户设备要求较高

    • 前端渲染会依赖浏览器的性能,如果用户的设备较差,渲染性能可能会受到影响,造成卡顿或不流畅。

2. 后端渲染(SSR)

后端渲染是指服务器负责生成整个 HTML 页面,客户端只需接收 HTML 页面并显示出来。页面的内容在服务器端生成后,再通过 HTTP 响应返回给浏览器。

优点:
  • 首屏渲染速度快

    • 页面内容由服务器渲染,浏览器直接接收完整的 HTML,用户可以尽早看到页面内容,极大地改善了首屏加载速度。
  • SEO 优化

    • 由于服务器返回的是完整的 HTML,搜索引擎爬虫能够更容易抓取和索引页面内容,提升页面的 SEO 性能。
  • 较低的设备要求

    • 由于页面渲染工作是在服务器完成的,客户端(即用户的浏览器)不需要执行大量的 JavaScript 代码,因此即使是在性能较低的设备上,页面的加载和渲染也会更流畅。
缺点:
  • 服务器负担重

    • 每次请求都需要从服务器获取完整的页面,因此服务器需要渲染页面并处理请求。对于高流量的应用,这会增加服务器的负担。
  • 用户体验较差

    • 每次用户切换页面时,都需要重新向服务器发送请求并渲染页面,造成页面闪烁或延迟,尤其在无缓存的情况下,用户的互动体验会受到影响。
  • 开发难度较大

    • 对于一些复杂的交互和动态内容,后端渲染需要更多的配置和处理,开发相对复杂,尤其是在保持前后端一致性时。

现在的技术趋势:为什么越来越多的项目选择后端渲染(SSR)?

近年来,随着 SEO用户体验 需求的增加,以及技术的不断发展,很多现代应用逐渐倾向于使用 SSR(后端渲染),或者将 SSRCSR(前端渲染)相结合(即 混合渲染)。具体原因包括:

1. 首屏加载速度要求提高

  • 随着 用户体验(UX)要求的提高,尤其是在移动端和低带宽环境下,首屏加载速度变得至关重要。SSR 能够更快地渲染页面,用户可以在页面完全渲染之前看到有效内容,这对于提升用户体验、降低跳出率和提高转化率非常有帮助。

2. SEO 的重要性

  • SEO 仍然是许多网站(尤其是电商、媒体和内容网站)增长的重要驱动力。虽然现代搜索引擎在某种程度上支持 JavaScript 渲染,但 SSR 依然是实现良好 SEO 的首选方式。服务器直接返回的 HTML 让爬虫可以更高效地抓取和索引页面内容。

3. 用户设备和网络的多样性

  • 移动设备和低性能设备的普及使得前端渲染可能成为瓶颈。浏览器的 JavaScript 执行能力和设备的处理能力决定了前端渲染的流畅性,而使用 SSR 可以减轻客户端的压力,让即便在性能较低的设备上也能获得较好的体验。

4. 技术进步:混合渲染

  • 现代的技术栈,如 ReactVueNext.jsNuxt.js 等框架,已经提供了 SSR + CSR 混合渲染 的方案。通过服务器渲染首屏,然后在客户端接管后续的交互,使得用户体验和 SEO 都得到优化。这种做法能够兼顾 SSR 和 CSR 的优点:
    • SSR 提升首屏加载速度和 SEO。
    • CSR 提供更快的后续页面切换,减少了重复的网络请求和页面渲染。

5. Server-Side Rendering (SSR) + Static Site Generation (SSG)

  • 一些现代框架,如 Next.jsNuxt.js,支持 静态站点生成(SSG),即提前在构建时渲染 HTML 页面(通过 SSR 或其他方式)。这可以极大提高页面的加载速度和 SEO 性能,同时仍能保证一定的动态内容。

6. 实时数据和动态渲染的融合

  • 对于需要实时数据的应用,SSR 可以在首次加载时渲染静态内容,后续通过 客户端渲染 动态更新页面内容。这使得开发者可以通过 混合渲染 来优化页面加载性能和交互体验。

75. 如果你有一个内容站点,怎么防止其他人爬虫?

难度:3.5 · 类型:QA

题目要点

防止爬虫是一项综合性的任务,涉及多种策略的结合使用。常见的方法包括:

  • robots.txt 文件用户代理检测:基本的反爬虫措施。
  • 验证码:确保用户是人类。
  • IP 阻止请求频率限制:阻止恶意访问。
  • 动态内容和 JavaScript 渲染:让爬虫难以抓取页面内容。
  • 反向代理和 WAF:通过防火墙和代理增强安全性。
  • 行为分析:监控用户行为,识别爬虫。

单一的措施很难做到完全防止爬虫,最好结合多种策略来提高防护的效果。同时,也要注意平衡用户体验与防爬措施之间的关系。

参考答案

防止爬虫访问一个内容站点是一个复杂的任务,通常需要多层次的策略来防止不当的爬取行为。爬虫往往会绕过一些简单的限制,但通过一些技术手段和策略,还是可以有效地减少爬虫的访问。

以下是一些常见的方法和实践,可以保护网站免受爬虫攻击:

1. robots.txt 文件

robots.txt 是一种向爬虫指示哪些内容可以或不可以抓取的文件。虽然这并不能防止爬虫访问(因为爬虫可以选择忽视它),但是可以告诉合规的爬虫哪些页面不应该抓取。

  • 优点:简单易用。
  • 缺点:不强制执行,恶意爬虫可以忽视该文件。

示例:

User-agent: *
Disallow: /admin/
Disallow: /private/

2. 用户代理检测 (User-Agent)

通过检测请求的 User-Agent 头部来识别是否是常见的爬虫或自动化工具。如果请求的 User-Agent 是爬虫的标志,可以将其拦截。

  • 优点:能检测一些常见的爬虫。
  • 缺点:用户代理是可以伪造的,因此这种方法并不完全可靠。

代码示例:

const userAgent = req.headers['user-agent'];
const bannedUserAgents = ['Googlebot', 'Bingbot', 'bot', 'crawler'];

if (bannedUserAgents.some(agent => userAgent.includes(agent))) {
    res.status(403).send('Forbidden');
}

3. IP 阻止

如果发现某个 IP 或 IP 段频繁访问,或者是来自于一个已知的爬虫源,可以通过 IP 阻止来限制访问。

  • 优点:能有效阻止来自特定 IP 地址的访问。
  • 缺点:爬虫可以通过代理或 VPN 伪造 IP 地址,无法完全避免。

4. 验证码 (CAPTCHA)

当检测到大量请求来自同一来源时,或者当用户行为异常时,可以要求输入验证码(如 Google 的 reCAPTCHA)来验证请求者是否为人类。

  • 优点:非常有效阻止自动化爬虫。
  • 缺点:影响用户体验,尤其是在频繁访问的场景下。

示例:

<form action="/submit" method="POST">
    <!-- reCAPTCHA Widget -->
    <div class="g-recaptcha" data-sitekey="your-site-key"></div>
    <button type="submit">Submit</button>
</form>

5. 动态内容生成

动态生成页面内容而不是静态化页面内容,也就是避免直接将 HTML 内容暴露在静态页面中。使用 JavaScript 渲染页面,爬虫如果没有执行 JavaScript,可能无法抓取到页面内容。

  • 优点:爬虫抓取困难,尤其是不能执行 JavaScript 的爬虫。
  • 缺点:现代的爬虫(如 Googlebot)可以渲染 JavaScript 内容,但这仍然能增加爬虫的抓取难度。

6. 请求频率限制 (Rate Limiting)

通过限制同一 IP 地址在一定时间内的请求次数,来防止爬虫进行过于频繁的抓取。可以使用令牌桶算法或漏斗算法来控制流量。

  • 优点:可以有效防止暴力爬取。
  • 缺点:正常用户也可能会受到一定的影响(例如在频繁操作时被限制)。

代码示例(使用 Express 中间件):

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute window
  max: 100, // Limit each IP to 100 requests per windowMs
  message: "Too many requests from this IP, please try again after a minute"
});

app.use(limiter);

7. 反向代理和 WAF(Web 应用防火墙)

使用反向代理服务器(如 Nginx、Cloudflare、AWS WAF 等)来检测和拦截恶意请求。Web 应用防火墙 (WAF) 可以通过规则阻止爬虫访问特定的 URL 或者异常的请求。

  • 优点:高效且灵活,能够适应不断变化的爬虫策略。
  • 缺点:需要配置和维护。

8. JavaScript 混淆和动态化

通过对页面中的数据进行混淆或动态加载,使得爬虫抓取困难。例如,数据可以通过 AJAX 请求从后端获取,或者通过 WebSocket 动态推送。

  • 优点:增加了爬虫的抓取难度。
  • 缺点:复杂化了开发和调试过程,也可能影响用户体验。

9. Referer 检查

检查请求头中的 Referer 字段,以确保请求来源于你的站点,而不是第三方站点。恶意爬虫可能不发送正确的 Referer

  • 优点:简单有效。
  • 缺点:用户可能会禁用 Referer,导致正常用户也受影响。

10. 文件和数据分离

将敏感数据或动态内容通过 API 进行提供,而不是将数据嵌入到 HTML 页面中。这可以使得爬虫更难获取信息。API 请求需要进行认证和权限校验。

11. 验证码和滑动验证

对于一些动态页面或需要用户交互的页面,可以采用滑动验证码、行为分析验证码等方式增加难度,确保请求来自人类用户。

12. 行为分析

通过监控用户的行为(如鼠标移动、点击、滚动等),来判断请求是否为真实用户。异常的、无行为的访问可以被认为是爬虫并被拦截。

  • 优点:能够有效区分机器与人类用户。
  • 缺点:可能影响用户体验,需要在不影响体验的前提下进行优化。

通过为用户设置会话和 Cookie 来识别其是否是爬虫。爬虫通常没有浏览器的会话管理和 Cookie 支持,因此可以根据这些信息识别异常行为。

76. 业务需要实现前端项目的灰度发布,你会怎么设计?

难度:4 · 类型:QA

题目要点

前端灰度发布的核心在于 用户分流版本管理,可以通过反向代理、前端逻辑、CDN 或 AB 测试平台实现。结合监控和回滚机制,可以有效降低上线风险,保障系统稳定性和用户体验。

参考答案

前端项目的灰度发布是一种逐步上线的策略,允许新版本的功能仅在特定的用户群体中逐步推广,以减少大规模发布的风险。

以下是实现灰度发布的关键思路和步骤:

1. 实现灰度发布的核心思路

  1. 用户分组

    • 根据特定规则(如用户 ID、IP 地址、地理位置等)对用户进行分组,将一部分用户标记为灰度用户。
  2. 版本控制

    • 同时维护多个前端版本,例如旧版本(稳定版)和新版本(灰度版)。
  3. 动态加载

    • 根据用户分组决定加载哪个版本的前端代码。
  4. 实时监控

    • 在灰度发布过程中,监控用户的行为和系统性能,及时发现问题。

2. 实现灰度发布的具体方法

(1)通过反向代理实现(推荐)

步骤:

  1. 使用 Nginx 或其他反向代理服务器,将不同的用户请求转发到不同的前端版本。
  2. 配置灰度规则:
    • 基于 Cookie、Header、用户 ID 或 URL 参数来进行分流。
  3. 不同版本部署在不同路径或域名下,例如:
    • https://example.com/v1(旧版本)
    • https://example.com/v2(灰度版本)

Nginx 配置示例

map $cookie_user_group $backend {
    default v1;
    "gray" v2;
}

server {
    location / {
        proxy_pass http://$backend;
    }
}

优点

  • 易于实施,灵活性高。
  • 支持全量和灰度用户共存。

缺点

  • 需要运维支持配置。

(2)通过前端代码控制

步骤:

  1. 在前端逻辑中引入灰度规则,根据用户属性动态加载不同版本。
  2. 使用动态加载工具(如 import()SystemJS),实现灰度版本的按需加载。

代码示例:

function loadFrontendVersion() {
  const userGroup = getCookie('user_group'); // 获取用户组信息

  if (userGroup === 'gray') {
    // 加载灰度版本
    import('/gray-version/main.js').then((module) => module.init());
  } else {
    // 加载稳定版本
    import('/stable-version/main.js').then((module) => module.init());
  }
}

loadFrontendVersion();

优点

  • 前端代码自主管理,无需依赖后端。
  • 可动态调整灰度规则。

缺点

  • 用户资源文件可能重复下载,增加带宽和性能开销。

(3)基于 CDN 的灰度发布

步骤:

  1. 使用 CDN 提供的分流能力,通过 URL 参数、Cookie 或 Header 决定静态资源的加载路径。
  2. 灰度用户加载新的资源路径,例如:
    • https://cdn.example.com/stable/main.js(旧版本)
    • https://cdn.example.com/gray/main.js(灰度版本)

CDN 配置示例

  • 设置回源规则,将灰度用户的请求路由到新的资源文件。
  • 配合浏览器缓存策略,减少重复加载。

优点

  • 利用 CDN 的高性能和分发能力。
  • 适合大规模用户场景。

缺点

  • 需要配置 CDN 分流规则。

(4)结合 AB 测试平台

  1. 使用 AB 测试工具(如 Google Optimize、LaunchDarkly)控制灰度策略。
  2. 平台会根据设定的规则将用户分流到不同的版本。
  3. 收集用户行为数据,分析效果。

优点

  • 自动化程度高。
  • 提供详细的用户行为分析。

缺点

  • 成本较高。

3. 灰度发布中的关键问题

  1. 灰度规则

    • 用户分组规则可以基于:
      • 用户 ID 的哈希值取模。
      • 地区或网络环境。
      • 登录状态或特定 Header。
  2. 数据一致性

    • 如果前后端存在数据交互,需确保旧版本和新版本兼容相同的接口和数据结构。
  3. 监控与回滚

    • 灰度发布过程中,实时监控系统性能、用户行为和错误日志。
    • 遇到问题时,快速回滚到稳定版本。
  4. 用户体验

    • 避免频繁切换版本导致用户体验不一致。
    • 灰度发布可以逐步扩大灰度范围。

4. 示例应用场景

  • 移动端更新:根据用户设备或版本号分配灰度策略。
  • 新功能上线:仅允许部分用户尝试新功能。
  • 性能优化验证:逐步测试新代码的性能改进效果。

77. 一般怎么做代码重构?

难度:3.5 · 类型:QA

题目要点

代码重构核心思路

  1. 分析现有问题(性能、可维护性、代码风格)
  2. 小步优化(拆分组件、封装 Hook、移除冗余代码)
  3. 使用现代技术(React Hooks、React Query、TypeScript)
  4. 提升代码质量(ESLint、Husky、Prettier、测试)
  5. 性能优化(memo、lazy loading、代码拆分)
  6. 持续迭代(Code Review、A/B 测试)

如果是老项目,每次重构可以先从 关键业务模块 开始,逐步推广到整个项目

参考答案

代码重构是一项持续优化的过程,通常涉及 提升代码质量、优化架构、提高可维护性

一般的重构步骤如下:


1. 分析和评估

🛠 代码重构前,先搞清楚:

  • 代码哪些地方难维护?(如耦合严重、重复代码多)
  • 性能是否存在瓶颈?
  • 是否符合 SOLID 原则?(面向对象设计原则)
  • 是否有更好的设计模式可以优化?

工具辅助分析:

  • ESLint / Prettier → 代码风格检查
  • Webpack Bundle Analyzer → 代码体积分析
  • React DevTools / Vue DevTools → 组件层级优化
  • Lighthouse / Performance API → 性能分析

2. 确定重构范围

  • 小步重构:避免一次性改动太大,导致不可控的 Bug。
  • 优先级排序:先改影响大的部分(如核心功能、关键 API),然后再处理小的优化点。

示例

  • 糟糕的代码:多个组件重复使用相同的 fetch 逻辑
  • 优化方向:抽离 useFetch Hook / API 层
  • 改动范围:先改 1~2 个组件,验证后再推广

3. 代码拆分与优化

✅ 提取公共逻辑

问题:多个组件都在 useEffect 里写了相同的 API 请求
优化:封装成 自定义 Hook

// ✅ 抽离公共逻辑
const useFetchData = (url: string) => {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch(url).then(res => res.json()).then(setData);
  }, [url]);
  return data;
};
// 🎯 组件使用
const UserProfile = () => {
  const data = useFetchData('/api/user');
  return <div>{data?.name}</div>;
};

✅ 拆分大型组件

问题:单个组件代码太长,难以维护
优化:按照 单一职责原则 进行拆分

// 🚫 糟糕的写法:组件职责过多
const ProfilePage = () => {
  return (
    <div>
      <UserInfo />
      <UserPosts />
      <UserComments />
    </div>
  );
};
// ✅ 拆分成更小的组件
const ProfilePage = () => (
  <div>
    <UserInfo />
    <UserPosts />
    <UserComments />
  </div>
);

✅ 移除不必要的状态

问题:状态滥用,例如 Redux 里存放临时 UI 状态
优化:尽量用 useState 本地管理,避免全局状态污染

// 🚫 不好的做法:Redux 里存 UI 状态
const showModal = useSelector(state => state.ui.showModal);
// ✅ 只在组件内部管理
const [showModal, setShowModal] = useState(false);

4. 代码规范 & 工具集成

1️⃣ 使用 ESLint + Prettier 统一代码风格

{
  "extends": ["eslint:recommended", "plugin:react/recommended", "prettier"],
  "rules": {
    "no-console": "warn",
    "react/prop-types": "off"
  }
}

2️⃣ 使用 TypeScript 提高可维护性

type User = {
  id: number;
  name: string;
};

const getUser = async (): Promise<User> => {
  return { id: 1, name: 'Alice' };
};

3️⃣ 使用 Husky + lint-staged 进行 Git Hook 代码校验

npx husky add .husky/pre-commit "npx lint-staged"
{
  "lint-staged": {
    "*.ts": ["eslint --fix", "prettier --write"]
  }
}

5. 逐步替换老旧技术

1️⃣ 移除 class 组件,改为 Function Component + Hooks

// 🚫 旧写法(Class 组件)
class Counter extends React.Component {
  state = { count: 0 };
  render() {
    return <button onClick={() => this.setState({ count: this.state.count + 1 })}>{this.state.count}</button>;
  }
}
// ✅ 新写法(Hooks)
const Counter = () => {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
};

2️⃣ 使用 React Query 取代 useEffect + useState

// 🚫 旧方式:手写 fetch 请求
useEffect(() => {
  fetch('/api/data').then(res => res.json()).then(setData);
}, []);
// ✅ 现代方式:使用 React Query
const { data } = useQuery('fetchData', () => fetch('/api/data').then(res => res.json()));

6. 性能优化

1️⃣ 避免不必要的渲染

  • 使用 memo 优化纯组件
  • 使用 useCallback & useMemo 避免不必要的重新计算
const MemoizedComponent = React.memo(({ value }) => {
  console.log('组件渲染');
  return <div>{value}</div>;
});

2️⃣ 代码拆分(Code Splitting)

  • 按需加载路由
  • 使用 React.lazy + Suspense
const LazyComponent = React.lazy(() => import('./Component'));

<Suspense fallback={<div>Loading...</div>}>
  <LazyComponent />
</Suspense>;

3️⃣ 减少 re-render(优化 Context)

const UserContext = createContext();
const UserProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  return <UserContext.Provider value={{ user, setUser }}>{children}</UserContext.Provider>;
};
const MemoizedUser = () => {
  const { user } = useContext(UserContext);
  return <div>{user?.name}</div>;
};

7. 持续迭代,保证质量

  • 代码 review:团队讨论、减少主观决策
  • 单元测试 / 集成测试:确保重构后功能正常
  • 灰度发布 / A/B 测试:降低上线风险
# 运行 Jest 单元测试
npm test

78. 请求失败会弹出一个 toast,如何保证批量请求失败时, 只弹出一个 toast?

难度:2 · 类型:QA

题目要点

在批量请求失败的场景下,可以通过以下方法确保只弹出一个 toast

  1. 防抖:适用于短时间内多次触发相同事件的场景。
  2. 标志位:适用于需要明确控制 toast 显示频率的场景。
  3. 统一错误处理:适用于批量请求的场景,逻辑清晰。
  4. 队列和单例模式:适用于需要按顺序显示消息的场景。
参考答案

方法 1: 使用防抖(Debounce)

防抖可以确保在一定时间内只触发一次 toast。如果多个请求在短时间内失败,防抖会将这些失败的请求合并,只弹出一个 toast

let toastTimeout = null;

function showToast(message) {
    if (toastTimeout) clearTimeout(toastTimeout); // 清除之前的定时器
    toastTimeout = setTimeout(() => {
        // 实际显示 toast 的逻辑
        console.log(message);
        toastTimeout = null; // 重置定时器
    }, 300); // 设置防抖时间为 300ms
}

// 模拟批量请求失败
function handleFailedRequests() {
    showToast("请求失败,请稍后重试");
    showToast("请求失败,请稍后重试");
    showToast("请求失败,请稍后重试");
}

handleFailedRequests(); // 只会弹出一个 toast

总结: 防抖通过在短时间内合并多次触发,确保只执行一次 toast 显示逻辑。适用于短时间内多次触发相同事件的场景。


方法 2: 使用标志位(Flag)

通过设置一个标志位,确保在第一次请求失败时弹出 toast,并在一定时间内忽略后续的失败请求。

let isToastShown = false;

function showToast(message) {
    if (!isToastShown) {
        console.log(message); // 实际显示 toast 的逻辑
        isToastShown = true;

        // 重置标志位,例如 5 秒后允许再次弹出 toast
        setTimeout(() => {
            isToastShown = false;
        }, 5000); // 5 秒后重置
    }
}

// 模拟批量请求失败
function handleFailedRequests() {
    showToast("请求失败,请稍后重试");
    showToast("请求失败,请稍后重试");
    showToast("请求失败,请稍后重试");
}

handleFailedRequests(); // 只会弹出一个 toast

总结: 标志位方法通过控制 toast 的显示状态,确保在一定时间内只弹出一个 toast。适用于需要明确控制 toast 显示频率的场景。


方法 3: 批量请求的统一错误处理

如果批量请求是通过 Promise.all 或类似的方式发起的,可以在统一的错误处理逻辑中弹出 toast

function showToast(message) {
    console.log(message); // 实际显示 toast 的逻辑
}

async function handleBatchRequests() {
    const requests = [
        fetch('/api/request1').catch(() => {}),
        fetch('/api/request2').catch(() => {}),
        fetch('/api/request3').catch(() => {}),
    ];

    try {
        await Promise.all(requests);
    } catch (error) {
        // 统一处理错误
        showToast("部分请求失败,请稍后重试");
    }
}

handleBatchRequests(); // 只会弹出一个 toast

总结: 通过统一错误处理,可以在批量请求失败时只弹出一个 toast。适用于批量请求的场景,且逻辑清晰。


方法 4: 使用队列和单例模式

toast 显示逻辑封装为一个单例,并使用队列管理多个失败请求的消息,确保每次只显示一个 toast

class ToastManager {
    constructor() {
        this.queue = [];
        this.isShowing = false;
    }

    showToast(message) {
        this.queue.push(message);
        if (!this.isShowing) {
            this.displayNextToast();
        }
    }

    displayNextToast() {
        if (this.queue.length > 0) {
            this.isShowing = true;
            const message = this.queue.shift();
            console.log(message); // 实际显示 toast 的逻辑

            // 模拟 toast 消失后的回调
            setTimeout(() => {
                this.isShowing = false;
                this.displayNextToast();
            }, 3000); // 假设 toast 显示 3 秒
        }
    }
}

const toastManager = new ToastManager();

// 模拟批量请求失败
function handleFailedRequests() {
    toastManager.showToast("请求失败,请稍后重试");
    toastManager.showToast("请求失败,请稍后重试");
    toastManager.showToast("请求失败,请稍后重试");
}

handleFailedRequests(); // 会依次弹出 toast,但不会同时弹出多个

总结: 通过队列和单例模式,可以确保 toast 依次显示,避免同时弹出多个 toast。适用于需要按顺序显示消息的场景。

79. 如果想将一个前端项目中的 px 全部转为 rem,你有什么思路

难度:2 · 类型:QA

题目要点

  1. 自动化工具(如 PostCSS 插件)能够高效地将所有 px 转换为 rem,适合大规模项目。
  2. 通过 Sass 或 Less 自定义函数,也可以实现自动化转换,适用于有预处理器的项目。
参考答案

对于大型项目,手动转换显然不现实,可以使用一些自动化工具来处理 pxrem 的转换。

  • PostCSS 插件:使用 postcss-pxtorem 插件来自动将 px 转换为 rem

    安装:

    npm install postcss-pxtorem --save-dev
    

    配置 postcss.config.js

    module.exports = {
      plugins: {
        'postcss-pxtorem': {
          rootValue: 16,    // 根元素字体大小
          propList: ['*'],  // 需要转换的属性,* 表示所有
          minPixelValue: 2  // 小于 2px 的不会被转换
        }
      }
    }
    

    这样,所有 px 单位将根据根元素的字体大小自动转换为 rem

  • CSS 预处理器方案(如 Sass 或 Less)

还可以使用 Sass 或 Less 结合一些函数来进行转换。例如,使用 Sass 自定义函数来实现 pxrem 的转换:

$base-font-size: 16px;

@function px-to-rem($px) {
  @return $px / $base-font-size * 1rem;
}

body {
  font-size: px-to-rem(16px);  // 转换为 rem
}

80. 对于富文本编辑器中的划词交互(鼠标滑动选择一组字符, 对该组字符进行操作),你有什么实现思路?

难度:3.5 · 类型:QA

题目要点

  1. 捕获用户选择的文本:使用 window.getSelection() 获取选中的文本,监听 mouseupselectionchange 事件。
  2. 处理文本格式化:通过 document.execCommand() 或自定义实现,如使用 range.surroundContents() 等方法对选中文本进行格式化操作。
  3. 更新内容:通过 contenteditable 实现动态内容编辑,捕获选中文本后进行更新。
  4. 扩展操作:可以支持多种操作如加粗、斜体、链接等,并通过自定义工具栏提供更多的交互方式。
参考答案

富文本编辑器是一个比较小众的领域,本题难度较高,适合有相关项目经验的同学。

主要涉及以下几个步骤:捕获用户选择的文本、处理用户交互、更新编辑器内容

下面是实现这个功能的一些思路:

1. 捕获用户的文本选择

在用户通过鼠标滑动选择文本时,我们可以监听浏览器的原生事件,如 selectionchangemouseup 事件,以捕获用户选择的文本。

使用 window.getSelection() 获取选中的文本:

document.addEventListener('mouseup', () => {
  const selection = window.getSelection();
  const selectedText = selection.toString(); // 获取选中的文本
  console.log(selectedText);
});
  • window.getSelection() 返回一个 Selection 对象,其中包含了用户当前选中的文本信息。
  • toString() 方法可以提取选中的文本内容。

2. 处理选中文本的格式化操作

当用户选中某些文本时,通常会希望对其进行格式化操作(如加粗、斜体、添加链接等)。为了实现这一功能,可以提供一些按钮(例如加粗、斜体等),并监听按钮点击事件来执行相应操作。

示例:加粗选中的文本

const boldButton = document.querySelector('#bold-btn');

boldButton.addEventListener('click', () => {
  const selection = window.getSelection();
  const selectedText = selection.toString();

  if (selectedText) {
    document.execCommand('bold'); // 执行加粗操作
  }
});
  • document.execCommand() 是一个简单的命令接口,允许执行常见的文本格式化操作(如加粗、斜体、下划线等)。
  • 注意:execCommand() 目前已经被废弃,部分浏览器可能不再支持它。对于现代富文本编辑器,可以考虑使用更先进的 API,如 document.createRange()ContentEditable

3. 更新编辑器内容

对于富文本编辑器,内容通常是动态更新的,可能是在 contenteditable 元素中编辑内容。在处理选中文本后,你需要更新编辑器中的内容。

使用 contenteditable 进行编辑:

<div contenteditable="true" id="editor">
  这是一段可以编辑的文本。
</div>
<button id="bold-btn">加粗</button>

当用户选中某段文本并点击加粗按钮时,我们可以执行如下操作:

const editor = document.getElementById('editor');
const boldButton = document.getElementById('bold-btn');

boldButton.addEventListener('click', () => {
  const selection = window.getSelection();
  const range = selection.getRangeAt(0); // 获取选中的文本范围

  const span = document.createElement('span');
  span.style.fontWeight = 'bold';

  range.surroundContents(span); // 将选中的文本包裹在加粗的 span 中
});
  • window.getSelection() 捕获选中的文本。
  • getRangeAt(0) 获取选区。
  • surroundContents() 方法将选中的文本包裹在新的元素中(如 span)。

4. 处理多种文本操作

富文本编辑器通常需要支持多种文本操作,如加粗、斜体、添加链接、字体颜色等。这些操作都可以通过类似的方法实现:

// 加粗
document.execCommand('bold');

// 斜体
document.execCommand('italic');

// 添加链接
const selectedText = window.getSelection().toString();
if (selectedText) {
  const url = prompt('输入链接地址:');
  document.execCommand('createLink', false, url);
}

为了实现这些操作,你可以通过监听各个工具栏按钮的点击事件来执行相应的操作。

5. 自定义和拓展功能

  • 自定义菜单:你可以创建自定义的工具栏、浮动菜单,或右键菜单来处理选中的文本。例如,在用户选中文本后,浮动菜单显示出可供选择的操作(如加粗、插入链接、字体颜色等)。
  • 插件化设计:你可以设计插件来扩展富文本编辑器的功能,例如表格、代码块、图片等,提供更加丰富的编辑体验。
  • 对选中区域的样式处理:可以通过 CSS 来进一步美化选中的区域,增加视觉效果,例如高亮显示选中的文本,或者在鼠标滑动时显示提示操作按钮等。

6. 性能优化

在处理选中的文本时,要注意性能:

  • 频繁的 DOM 操作(如 selectionchange)可能会导致性能问题,尤其是对于大文本区域时。你可以通过防抖(debounce)或节流(throttle)来减少触发的频率。
  • 在编辑器中插入大量内容时,可以使用虚拟化技术来提高渲染性能。

81. 如何在浏览器中实现 PDF 文件的预览?

难度:2 · 类型:QA

题目要点

  • 简单的嵌入方式:使用 &lt;embed&gt;&lt;iframe&gt; 标签直接嵌入 PDF 文件,适合简单预览。
  • 复杂的交互需求:使用 PDF.js,可以实现多页显示、缩放、旋转等交互功能,适合需要更高自定义的场景。
  • 第三方库或插件:可以使用现成的解决方案如 PDFObjectreact-pdf,便于快速集成。
参考答案

以下是几种实现思路:

1. 使用 HTML 的 &lt;embed&gt; 标签

&lt;embed&gt; 标签可以用来嵌入 PDF 文件,直接在浏览器中显示预览。大多数现代浏览器都内置了对 PDF 文件的支持。

示例代码:

<embed src="path/to/your/file.pdf" type="application/pdf" width="600" height="400">
  • src:PDF 文件的路径。
  • type:指定文件类型为 application/pdf,这样浏览器会知道应该如何处理该文件。
  • widthheight:指定预览区域的大小。

优点:实现简单,兼容性较好。

缺点:功能相对简单,无法进行交互操作(如跳转页面、放大缩小等)。

2. 使用 &lt;iframe&gt; 标签

你可以使用 &lt;iframe&gt; 标签嵌入 PDF 文件,浏览器会自动加载 PDF 文件并显示预览。

示例代码:

<iframe src="path/to/your/file.pdf" width="600" height="400"></iframe>
  • src:PDF 文件路径。
  • widthheight:指定 iframe 的大小,控制预览区域。

优点:与 &lt;embed&gt; 类似,简单易用,且支持浏览器内置的 PDF 阅读器。

缺点:功能有限,交互性较弱。

3. 使用 PDF.js

PDF.js 是 Mozilla 提供的一个开源库,允许在网页中渲染和查看 PDF 文件。它可以让你自定义 PDF 的渲染,并提供更多的交互功能,例如翻页、缩放等。

步骤:

  1. 引入 PDF.js 库:
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.min.js"></script>
  1. 创建一个 <canvas> 元素来渲染 PDF 页面。

  2. 使用 PDF.js 加载并渲染 PDF 文件。

示例代码:

<canvas id="pdf-canvas" width="600" height="400"></canvas>

<script>
  // 加载 PDF 文件
  const url = 'path/to/your/file.pdf';

  // 使用 PDF.js 加载 PDF 文件
  pdfjsLib.getDocument(url).promise.then(function (pdf) {
    console.log('PDF loaded');

    // 获取第一页
    pdf.getPage(1).then(function (page) {
      console.log('Page loaded');

      const scale = 1.5; // 设置缩放比例
      const viewport = page.getViewport({ scale: scale });

      // 获取渲染上下文
      const canvas = document.getElementById('pdf-canvas');
      const context = canvas.getContext('2d');

      // 设置 canvas 的尺寸
      canvas.width = viewport.width;
      canvas.height = viewport.height;

      // 渲染 PDF 页面
      const renderContext = {
        canvasContext: context,
        viewport: viewport
      };
      page.render(renderContext);
    });
  });
</script>

步骤解释

  1. 使用 pdfjsLib.getDocument(url) 加载 PDF 文件。
  2. 使用 pdf.getPage(1) 获取第一页,并使用 <canvas> 渲染该页面。
  3. 可以通过 scale 参数设置渲染的缩放比例。

优点:功能强大,支持多页、缩放、旋转等多种交互功能。

缺点:实现较复杂,需要引入 PDF.js 库,适合需求较高的场景。

4. 使用第三方插件或组件

如果不想自己实现 PDF 渲染,可以使用一些现成的第三方组件或插件,它们提供了丰富的功能和配置选项。

  • react-pdf:如果你使用 React,可以使用这个库来加载和显示 PDF 文件。
  • PDFObject:这是一个简单的 JavaScript 库,用于嵌入 PDF 文件到网页中,支持更多的自定义选项。

示例:使用 PDFObject

<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.2.6/pdfobject.min.js"></script>
<div id="pdf-container" style="width: 600px; height: 400px;"></div>

<script>
  PDFObject.embed("path/to/your/file.pdf", "#pdf-container");
</script>

优点:使用简单,能够实现一些自定义功能。

缺点:与自定义方案相比,灵活性略低。

5. 使用浏览器的内置 PDF 阅读器

如果不想使用额外的库,浏览器本身通常都自带 PDF 阅读器。只需使用 &lt;embed&gt;&lt;iframe&gt; 标签嵌入 PDF 文件,浏览器会自动使用内置的 PDF 阅读器显示文件。或者直接在新标签页中打开 pdf 文件实现显示。

82. 做前端错误监控时,有什么办法能将报错的源码地址,包括代码行数也进行上报吗?

难度:3.5 · 类型:QA

题目要点

  1. 基础捕获:利用 error 事件获取行列号,但生产环境代码通常被压缩
  2. Source Map:必须与构建流程结合,且需保证安全性
  3. 上下文增强:通过动态获取源码片段辅助诊断
  4. 错误去重:根据文件名+行号+错误类型生成唯一指纹
  5. 性能权衡:Source Map 解析会增加后端处理开销

推荐方案

  • 中小项目:直接使用 Sentry/Fundebug 等成熟方案
  • 大型项目:自建 Source Map 解析服务 + ELK 存储分析
  • 敏感场景:开发环境保留 Source Map,生产环境通过 CI 管道关联
参考答案

可以结合 Source Map 实现。

一、基础错误捕获

1. 全局错误监听

window.addEventListener('error', (event) => {
  const { filename, lineno, colno, error } = event;
  reportError({
    file: filename,  // 报错文件URL
    line: lineno,    // 行号
    column: colno,   // 列号
    stack: error?.stack // 错误堆栈
  });
});

2. Promise 异常捕获

window.addEventListener('unhandledrejection', (event) => {
  const stack = event.reason?.stack;
  extractSourceLocation(stack); // 从堆栈解析源码位置
});

二、关键方法

1. Source Map 解析

适用场景:生产环境压缩代码定位

// 示例堆栈解析
function parseStack(stack) {
  // 错误堆栈示例:
  // "at MyComponent (https://example.com/static/js/main.4a2b3c.js:45:12)"
  const match = /at\s+.+\s\(?(.+):(\d+):(\d+)\)?/.exec(stack);
  return match ? {
    file: match[1],
    line: parseInt(match[2]),
    column: parseInt(match[3])
  } : null;
}

配套工作流

  1. 构建时生成 Source Map 文件
  2. 将 Source Map 存储在受限访问的服务器
  3. 通过错误上报的行列号反向查询源码

2. 构建配置示例(Webpack)

// webpack.config.js
module.exports = {
  devtool: 'hidden-source-map', // 不暴露sourcemap的配置
  output: {
    sourceMapFilename: '[file].map[query]',
    devtoolModuleFilenameTemplate: 'webpack:///[resource-path]'
  }
};

三、具体代码定位方案

1. AST 代码标记

在编译阶段注入代码位置信息:

// Babel插件示例
const { declare } = require('@babel/helper-plugin-utils');

module.exports = declare((api) => {
  return {
    visitor: {
      FunctionDeclaration(path) {
        const { loc } = path.node;
        path.addComment('leading', `loc:${loc.start.line}:${loc.start.column}`);
      }
    }
  };
});

2. 错误上下文捕获

function captureCodeContext(sourceUrl, line, range = 5) {
  // 通过fetch获取源文件(需配置CORS)
  return fetch(sourceUrl)
    .then(res => res.text())
    .then(code => {
      const lines = code.split('\n');
      return lines.slice(Math.max(0, line - range), line + range);
    });
}

四、生产环境方案

1. 安全策略

措施说明
Source Map 访问控制仅限内网访问或添加Auth验证
敏感信息脱敏上报前过滤API密钥等敏感内容
数据加密对行列号等关键信息加密传输

2. 上报数据格式示例

{
  "error": "TypeError: Cannot read property 'x' of undefined",
  "location": {
    "file": "https://cdn.example.com/static/js/chunk-abc123.js",
    "line": 45,
    "column": 12,
    "compiled": true,
    "context": ["  const data = response.json();", "  return data.x.y; // Error here"]
  },
  "env": {
    "userAgent": "Chrome/103.0",
    "url": "/product/123"
  }
}

五、开源方案推荐

1. Sentry 的实现方式

Sentry.init({
  dsn: 'YOUR_DSN',
  integrations: [
    new Sentry.Integrations.TryCatch({
      XMLHttpRequest: true
    }),
  ],
  beforeSend(event) {
    // 增强位置信息
    return enrichWithSourceMap(event);
  }
});

2. Fundebug 的源码定位

fundebug.init({
  apikey: 'YOUR_KEY',
  filters: {
    isFix: function(data) {
      // 自动解析sourcemap
      return data.sourceMap === true;
    }
  }
});

83. 设计一个技术方案,能在排查问题时,还原用户的操作流程

难度:4 · 类型:QA

题目要点

  • 操作流程还原的关键在于“行为埋点 + DOM 快照 + 日志上下文绑定”;
  • 通过 rrweb 实现精简化录屏,通过 sessionId 实现日志链路打通;
  • 需关注性能、隐私和合规边界,做到既能排查问题又不引发新风险;
  • 上层可以构建可视化 replay 平台,提高研发和运营的问题定位效率。
参考答案

还原用户操作流程的核心目标是可追溯性与可重现性,需要在不影响用户体验的前提下,采集关键交互行为、上下文环境和异常信息,为后续问题定位和复现提供支撑。

下面是一个适用于中大型前端项目的通用技术方案设计:

一、目标

  • 能还原用户“从进入页面开始,到出现问题”这一段时间内的操作轨迹;
  • 数据精简但有用,不能造成性能和隐私风险;
  • 提供可视化或日志化的重放手段,辅助前端或测试人员复现问题;
  • 可与异常上报(如 Sentry)联动,关联问题发生前的操作上下文。

二、方案核心组件

1. 行为埋点

记录用户的关键行为,如:

  • 页面访问(PV)、停留时长、跳转路径;
  • 元素点击(带事件上下文,如点击了哪个按钮);
  • 表单输入、提交(脱敏处理);
  • 滚动行为、Tab 切换、页面缩放等;
  • 自定义事件(如打开弹窗、调用 API、组件挂载等)。

数据结构通常包括:

{
  type: 'click' | 'input' | 'page_view' | ...,
  timestamp: 1620000000000,
  eventTarget: '#submitBtn',
  page: '/order/confirm',
  payload: { ... } // 自定义内容
}

2. 操作记录快照

采用轻量化的 DOM 变更记录方案,如:

  • 使用 rrweb 录制用户行为和 DOM 状态;
  • 降低频率,仅记录关键步骤(如页面切换、表单提交前);
  • 支持回放功能,用于“视觉上”还原操作流程。

例如:

rrweb.record({
  emit(event) {
    buffer.push(event);
  }
})

3. 日志上下文关联

将用户操作日志与错误日志或接口请求失败进行上下文绑定:

  • 每次操作打上 sessionId / traceId;
  • 异常发生时,采集最近一段时间内的操作轨迹;
  • 发送到日志中心或接入平台(如 ELK、Sentry、LogService)。

4. 前端异常监控系统

结合用户行为日志,通过 Sentry、TrackJS 等平台收集错误信息,并挂载:

  • 用户标识(userId / deviceId)
  • 操作序列摘要(最近 20 次操作事件)
  • 环境信息(浏览器 UA、系统版本、网络状态)

5. 本地缓冲与上报机制

  • 使用内存和 localStorage 作为日志缓存队列;
  • 每隔一定时间或达到数量阈值批量上报;
  • 避免频繁请求、提高性能和兼容性。

三、隐私与合规性考量

  • 明确脱敏字段:如手机号、密码、身份证等不采集或做脱敏处理;
  • 用户授权机制:重要采集需明确隐私策略;
  • 提供退出追踪或匿名化功能;
  • 对于敏感页面(如支付页)禁止行为录制。

四、运维与分析工具链对接

  • 前端日志汇总到日志平台(如 ELK、Sentry);
  • 行为数据用于 BI 或问题复现平台;
  • 可开发 replay 工具:以 sessionId 为入口查看某用户的操作路径和 DOM 快照;
  • 开发后台对接前端行为流分析系统,用于用户行为还原和转化漏斗优化。

示例场景

用户反馈:“我点击提交按钮没反应”。技术方案提供的回溯路径可能是:

  • 操作序列:页面加载 → 填写表单 → 点击提交 → JS 报错;
  • 快照还原:确认用户确实点击了提交 → 查看提交前是否有前置错误或表单校验失败;
  • 日志信息:接口请求未发出,定位可能是事件绑定失效或阻断;
  • 解决方案:复现场景并修复逻辑问题。

84. 什么是单点登录,以及如何进行实现?

难度:2.5 · 类型:QA

题目要点

  • 单点登录(SSO) 允许用户在多个应用程序中仅登录一次,提升了用户体验和安全性。
  • 实现方式 包括使用 SAML、OAuth 2.0、OpenID Connect 和 JWT 等协议。
  • 关键步骤 包括设置身份认证提供者、配置服务提供者、实现登录流程、验证和授权。
参考答案

一、是什么

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一

SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作

当一个系统成功登录以后,passport将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport发起认证

上图有四个系统,分别是Application1Application2Application3、和SSO,当Application1Application2Application3需要登录时,将跳到SSO系统,SSO系统完成登录,其他的应用系统也就随之登录了

举个例子

淘宝、天猫都属于阿里旗下,当用户登录淘宝后,再打开天猫,系统便自动帮用户登录了天猫,这种现象就属于单点登录

二、如何实现

同域名下的单点登录

cookiedomin属性设置为当前域的父域,并且父域的cookie会被子域所共享。path属性默认为web应用的上下文路径

利用 Cookie 的这个特点,没错,我们只需要将Cookie domain属性设置为父域的域名(主域名),同时将 Cookie path属性设置为根路径,将 Session ID(或 Token)保存到父域中。这样所有的子域应用就都可以访问到这个Cookie

不过这要求应用系统的域名需建立在一个共同的主域名之下,如 tieba.baidu.commap.baidu.com,它们都建立在 baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登录

不同域名下的单点登录(一)

如果是不同域的情况下,Cookie是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 Web 服务

用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 token 写入 Cookie(注意这个 Cookie 是认证中心的,应用系统是访问不到的)

应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心

由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了

如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录

如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统

应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了

此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法

不同域名下的单点登录(二)

可以选择将 Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将LocalStorage的数据传递给服务端

这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID (或 Token )放在响应体中传递给前端

单点登录完全可以在前端实现。前端拿到 Session ID (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage

关键代码如下:

// 获取 token
var token = result.data.token;

// 动态创建一个不可见的iframe,在iframe中加载一个跨域HTML
var iframe = document.createElement("iframe");
iframe.src = "http://app1.com/localstorage.html";
document.body.append(iframe);
// 使用postMessage()方法将token传递给iframe
setTimeout(function () {
    iframe.contentWindow.postMessage(token, "http://app1.com");
}, 4000);
setTimeout(function () {
    iframe.remove();
}, 6000);

// 在这个iframe所加载的HTML中绑定一个事件监听器,当事件被触发时,把接收到的token数据写入localStorage
window.addEventListener('message', function (event) {
    localStorage.setItem('token', event.data)
}, false);

前端通过 iframe+postMessage() 方式,将同一份 Token 写入到了多个域下的 LocalStorage 中,前端每次在向后端发送请求之前,都会主动从 LocalStorage 中读取Token并在请求中携带,这样就实现了同一份 Token 被多个域所共享

此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域

三、流程

单点登录的流程图如下所示:

  • 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数

  • sso认证中心发现用户未登录,将用户引导至登录页面

  • 用户输入用户名密码提交登录申请

  • sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌

  • sso认证中心带着令牌跳转会最初的请求地址(系统1)

  • 系统1拿到令牌,去sso认证中心校验令牌是否有效

  • sso认证中心校验令牌,返回有效,注册系统1

  • 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源

  • 用户访问系统2的受保护资源

  • 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数

  • sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌

  • 系统2拿到令牌,去sso认证中心校验令牌是否有效

  • sso认证中心校验令牌,返回有效,注册系统2

  • 系统2使用该令牌创建与用户的局部会话,返回受保护资源

用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话

用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心

全局会话与局部会话有如下约束关系:

  • 局部会话存在,全局会话一定存在
  • 全局会话存在,局部会话不一定存在
  • 全局会话销毁,局部会话必须销毁