跳到正文
前端知识库
工程化

前端场景:跨端交互与发布治理

把 Canvas 高清绘制、移动端下拉刷新、跨上下文通信、国际化切换和 npm 包发布整理成可实现、可验证的场景题。

18 分钟场景题 · Canvas · 移动端 · Worker · 国际化 · npm · 发布

前端场景:跨端交互与发布治理

这篇文章处理几类很容易“只会背 API”的面试题。回答时始终从约束出发:目标是什么、状态如何变化、哪些边界会失败、如何验证结果。

1. Canvas 高清绘制、坐标与命中测试

1.1 CSS 尺寸和位图尺寸是两套坐标

canvasstyle.width/height 决定它在页面中的 CSS 尺寸,canvas.width/height 决定内部位图的像素数。默认位图尺寸通常是 300 × 150;在高 DPI 屏幕上如果只改 CSS 尺寸,浏览器会放大低分辨率位图,文字和线条就会发虚。

初始化时先得到 CSS 尺寸,再按 devicePixelRatio 放大 backing store,并把绘图上下文缩放回 CSS 坐标。这样业务模型仍可使用 CSS 像素,绘制时不必在每个坐标上手动乘倍数:

function resizeCanvas(canvas, ctx) {
  const rect = canvas.getBoundingClientRect()
  const dpr = Math.max(1, window.devicePixelRatio || 1)

  // 真实项目可根据最大纹理尺寸和内存预算设置上限,不能盲目放大。
  const pixelWidth = Math.round(rect.width * dpr)
  const pixelHeight = Math.round(rect.height * dpr)
  if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
    canvas.width = pixelWidth
    canvas.height = pixelHeight
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
  }

  return { width: rect.width, height: rect.height, dpr }
}

setTransform 比重复调用 scale 更容易保持幂等。窗口缩放、容器尺寸变化、系统显示缩放和横竖屏切换都可能改变尺寸,通常用 ResizeObserver 重新计算;重设 canvas.width 会清空上下文状态和像素,因此要先保存模型数据,再按新尺寸重绘,而不是把旧位图简单拉伸。

1.2 从指针坐标还原到模型坐标

事件中的 clientX/clientY 是视口坐标,getBoundingClientRect() 也是视口坐标。两者相减即可得到 CSS 像素坐标;因为上下文已经缩放到 CSS 坐标,命中测试不应再次乘 dpr

function pointInCanvas(event, canvas) {
  const rect = canvas.getBoundingClientRect()
  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

如果 Canvas 本身还应用了 CSS transform(例如缩放、旋转)或位于滚动容器中,不能只凭 offsetLeft 推算位置,应使用 DOMMatrix 的逆矩阵把视口坐标变换到绘图坐标。缩放画布时应明确“视觉坐标”和“模型坐标”两层:模型坐标保持稳定,视图矩阵只影响绘制和输入映射。

1.3 命中测试要以模型为准

不要把“鼠标落在 Canvas 元素上”当成命中某个图形。常见策略按场景选择:

场景 做法 取舍
图形很少 对模型的矩形、圆形或多边形做几何判断 实现简单,复杂图形要自己处理边界
路径形状 使用 Path2D 配合 isPointInPath/isPointInStroke 与绘制路径一致,但要维护路径对象
几万以上图形 先按网格、R-tree 或四叉树缩小候选集,再做精确判断 建索引有成本,适合频繁指针移动
可选座位/地图 建立“模型 ID -> 几何区域”的索引,渲染只是结果 便于键盘操作、回放和服务端校验

命中边界要定义清楚:线条可设置可交互的容差,重叠图形按 z-index 或业务优先级从后往前检查。拖拽时保存被命中的模型 ID,不要在每一帧重新猜测目标;指针离开、pointercancel 或组件卸载时清理捕获状态。

1.4 可访问性与敏感内容边界

Canvas 本身不是可访问的语义树。座位图、流程图等交互内容应同时提供 DOM 列表、键盘焦点和文本状态,至少让辅助技术知道“元素 ID、状态、可执行动作”。签名板要提供清除、撤销和键盘/按钮替代操作;支付码等敏感图像要控制缓存、日志和截图权限,Canvas 不是安全容器。

跨源图片绘制前设置正确的 crossOrigin 并让服务器返回允许的 CORS 头,否则 Canvas 会变成非 origin-clean,toBlob()/toDataURL() 读取像素时会抛出安全异常。导出大图优先 toBlob(),避免把整个二进制内容膨胀成 Base64 字符串;导出任务应可取消,并在完成后释放临时位图和对象 URL。

面试回答骨架

先区分 CSS 尺寸、位图尺寸和模型坐标
-> 按 DPR 设置 backing store,并在 resize 后重绘
-> 用 rect/逆矩阵把指针映射到模型坐标
-> 先空间索引、再几何命中,明确重叠和容差
-> 提供 DOM/键盘替代,并说明跨源和敏感数据边界

2. 下拉刷新、触摸事件与滚动冲突

2.1 先定义状态机和边界

一个可控的下拉刷新组件至少有以下状态:

idle -> pulling -> ready -> refreshing -> settling -> idle
                 \-> cancelled

只有在滚动容器确实处于顶部、手势主要向下、且没有横向滑动意图时才进入 pulling。达到阈值后显示 ready,松手才触发一次刷新;刷新期间忽略新的拖拽,成功或失败都要回到可继续交互的状态。用状态机而不是几个布尔值,可以避免“请求已完成但指示器仍在转”“快速松手触发两次”等竞态。

2.2 Pointer Events、passive 和 preventDefault

现代 Web 优先使用 Pointer Events,统一鼠标、触摸和触控笔,并在拖拽对象上使用 setPointerCapture 保持事件连续性。若必须使用 Touch Events,要明确监听器的 passive 选项:

element.addEventListener('touchmove', onTouchMove, { passive: false })

function onTouchMove(event) {
  if (!shouldTakeOverVerticalPull(event)) return
  event.preventDefault()
  updatePullDistance(event.touches[0].clientY)
}

浏览器默认把部分触摸滚动监听视为 passive,以便尽早开始滚动;passive 监听器中调用 preventDefault() 不会生效。不要全局注册非 passive 监听器:只有确认组件在顶部、方向锁定为垂直下拉并需要接管手势时,才在局部监听器中阻止默认行为。CSS touch-action 应先表达意图,例如 pan-x 允许横向滚动、由组件处理纵向拖拽;它不能代替状态判断。

2.3 嵌套滚动、边界回弹和原生行为

页面可能有外层窗口、内层列表和横向轮播。开始手势时记录起点,比较 dxdy 做方向锁定;查找实际滚动容器的 scrollTop,不能只检查 window.scrollY。内层列表未到顶部时应把手势交给内层滚动,到了顶部才允许外层下拉。overscroll-behavior-y: contain 可以减少滚动链和浏览器边界回弹,但不同浏览器的系统级“下拉刷新”行为仍需在目标设备上验证,不应假定 CSS 在所有 WebView 中一致。

距离反馈可使用阻尼函数(例如 distance = raw * 0.5),并设置最大拖拽距离,防止指示器把布局撑得过大。动画只改变 transform,避免拖动过程中反复修改布局属性;尊重 prefers-reduced-motion,为不使用手势的用户提供明确的“刷新”按钮或键盘操作。

2.4 取消、异常和清理

  • pointercancel、页面隐藏、方向切换和组件卸载都应结束当前手势并释放捕获。
  • 刷新请求使用 AbortController,超时或路由离开时取消;取消不应被当成业务失败提示。
  • 刷新成功后再更新列表和锚点,避免用户在等待期间看到跳动的内容。
  • 监听器、计时器和订阅必须成对清理;否则返回页面后可能一次拖拽触发多次刷新。

3. iframe、窗口和 Worker 的可靠通信

3.1 先选通道,再定义协议

postMessage 只提供消息传输,不提供业务级请求响应、鉴权或重试。跨上下文场景可按需求选通道:

需求 通道 关键边界
父子 iframe/窗口 window.postMessage 固定 targetOrigin,校验 originsource
同源多标签页 BroadcastChannel 无持久队列,页面关闭后消息丢失
主线程与 Dedicated Worker Worker.postMessage Worker 可终止,消息需可结构化克隆
需要独立双向端口 MessageChannel 通过 MessagePort 建立专用连接
多页面共享计算 SharedWorker 端口管理和生命周期更复杂

协议建议统一信封,至少有版本、唯一 ID、消息类型和截止时间:

type Envelope = {
  version: 1
  id: string
  type: string
  replyTo?: string
  sentAt: number
  deadline?: number
  payload?: unknown
  error?: { code: string; message: string }
}

接收方先验证来源和协议版本,再校验 type 对应的 payload schema,最后进入业务处理。不要直接把未知消息转成函数调用,也不要把 token、完整用户资料或未脱敏的 URL 广播给所有同源页面。

3.2 请求响应、超时和取消

发送请求时把 id -> {resolve, reject, timer} 放入 pending 表;收到 replyTo 时只完成对应请求。超时要删除 pending 项并向对端发送可选的 cancel 消息;对端必须把取消传递给 AbortController 或计算任务。所有入口都要处理 messageerror、Worker error、端口关闭和上下文销毁,否则 Promise 会永久悬挂。

function request(port, type, payload, signal, timeout = 5000) {
  const id = crypto.randomUUID()
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      pending.delete(id)
      reject(new DOMException('request timeout', 'TimeoutError'))
    }, timeout)

    pending.set(id, { resolve, reject, timer })
    port.postMessage({ version: 1, id, type, payload, sentAt: Date.now() })

    signal?.addEventListener('abort', () => {
      clearTimeout(timer)
      pending.delete(id)
      port.postMessage({ version: 1, id: crypto.randomUUID(), type: 'cancel', replyTo: id })
      reject(signal.reason)
    }, { once: true })
  })
}

示例省略了 schema 校验和端口关闭处理;生产代码还要避免同一个 id 重复完成,并限制单条消息大小、并发请求数和队列长度。Transferable(如 ArrayBufferMessagePortImageBitmap)可以避免复制大对象,但转移后发送方不再拥有原对象;需要共享可变内存时才考虑 SharedArrayBuffer,并先满足跨源隔离等部署前提。

3.3 握手、安全和版本演进

iframe 建立连接时可先发送随机 nonce,双方交换支持的协议版本和能力列表;之后每条消息都绑定已验证的 source、origin 和会话 ID。targetOrigin 不要写 *,接收端也不能只判断 event.origin 而忽略 event.source。Origin 校验是边界校验,不等于用户鉴权;敏感操作仍需服务端授权和一次性签名。

协议升级采用向后兼容字段和明确的 capability,未知字段忽略、未知类型拒绝并记录指标。需要顺序的业务要携带序列号或版本号;跨不同通道不能假定全局有序。Worker 任务可用任务 ID、取消标志和版本号防止旧结果覆盖新结果,关闭 Worker 后清空 pending 表并释放 ImageBitmap、ArrayBuffer 等资源。

4. 国际化资源与路由切换

4.1 语言选择和资源分层

把 locale 当成受约束的应用状态,而不是散落在组件里的字符串。常见优先级是:显式 URL/用户设置 > 已保存偏好 > 浏览器 navigator.languages/Accept-Language > 默认语言。最终值应经过 allowlist 规范化,例如将 zh-CNzh-cn 映射到同一个 canonical locale,并为 RTL 语言设置 dir="rtl"

资源按语言、命名空间和页面拆分,避免一个 JSON 包覆盖整个应用:

locales/
  zh-CN/common.json
  zh-CN/checkout.json
  en-US/common.json
  en-US/checkout.json

文案使用稳定 key 和 ICU 风格的复数、日期、数字占位,不要通过字符串拼接句子:

cart.items = {count, plural,
  =0 {购物车为空}
  one {# 件商品}
  other {# 件商品}
}

日期、时间、金额使用 Intl.DateTimeFormatIntl.NumberFormat 等 API,并显式传入时区和 currency;不要把服务端时间字符串直接按用户机器时区渲染成业务结论。

4.2 懒加载、回退和竞态

路由切换时只加载当前页面需要的 namespace。加载任务要带 locale + namespace 版本,并用请求代次或 AbortController 防止用户快速切换语言时旧资源覆盖新资源:

let localeRequest = 0

async function changeLocale(nextLocale) {
  const requestId = ++localeRequest
  const messages = await loadMessages(nextLocale)
  if (requestId !== localeRequest) return

  i18n.replaceMessages(nextLocale, messages)
  document.documentElement.lang = nextLocale
  document.documentElement.dir = isRtl(nextLocale) ? 'rtl' : 'ltr'
}

回退链应可观测且有限,例如 zh-Hant-TW -> zh-Hant -> zh-CN,不能静默吞掉关键缺失。缺 key 在开发和 CI 中应报警,生产可以显示安全占位并上报 key、locale、route;不要把用户输入或完整翻译内容写进日志。资源缓存要带版本号,发布新版本时避免旧 namespace 与新代码混用。

4.3 SSR、路由和状态同步

SSR 首屏必须使用与客户端相同的 locale 和资源版本,否则会发生水合不一致或先闪现默认语言。语言偏好若保存在 Cookie,服务端要校验 allowlist;若由 URL 决定,切换后应更新 history、canonical 链接和页面标题。多标签页切换语言可用 BroadcastChannel,收到消息后重新加载缺失 namespace,而不是广播整套资源。

路由是否翻译路径要单独决策:展示文案可以切换而不改变资源 ID;若 URL 也本地化,必须维护稳定的 route ID、反向映射、分享链接和 404 回退,不能用翻译后的标题当数据库主键。表单错误、无障碍标签、日期和方向性布局都要纳入回归测试。

5. npm 包的 exports、类型与发布闭环

5.1 公开契约要和产物一致

package.json 是消费者看到的 API 契约。exports 只暴露稳定入口,types 指向与运行时代码同版本的声明,sideEffects 必须描述真实的顶层副作用:

{
  "name": "@acme/ui",
  "version": "1.4.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./styles.css": "./dist/styles.css",
    "./package.json": "./package.json"
  },
  "files": ["dist", "README.md", "LICENSE"],
  "sideEffects": ["**/*.css"],
  "peerDependencies": { "react": ">=18" }
}

exports 存在时,消费者不能再依赖未公开的深层路径;这是有意的封装,不应为了兼容偶然路径而全部开放。条件导出的顺序、ESM/CJS 互操作和 Node/打包器支持矩阵需要用真实消费者验证。声明文件不能只“生成出来就算完成”:要检查泛型、事件类型、CSS 模块和子路径入口是否可被消费者正确推断。

sideEffects: false 不是性能开关。若模块注册全局行为、注入样式或执行 polyfill,错误标记会让打包器删除必要代码;若每个样式文件都是副作用,应精确列出 CSS,而不是把整个包标成有副作用导致 tree-shaking 失效。

5.2 构建、测试和包内容检查

发布前至少做四类检查:

  1. 产物检查npm pack --dry-run 查看文件清单,确认没有测试夹具、本地配置、源码地图敏感路径或未编译源码;检查 ESM、CJS、.d.ts 和 CSS 入口都存在。
  2. 消费者冒烟:在临时目录安装打包后的 tarball,分别用 Node、TypeScript 和目标 bundler 导入根入口及每个公开子路径。
  3. 行为矩阵:运行单元、类型、Lint、SSR/浏览器交互和按支持框架划分的兼容测试;组件库要检查键盘、焦点、主题和样式是否被 tree-shaking 错误删除。
  4. 依赖审计:运行锁文件一致性和漏洞扫描,确认框架放在 peerDependencies,运行时必需包没有误放到 devDependencies

构建应可复现:固定 Node/包管理器版本,CI 使用锁文件,产物由当前提交生成。把 dist 手工改后发布会破坏回滚和溯源;版本号、变更日志、Git tag 与构建产物要一一对应。

5.3 版本策略、registry 和回滚

语义化版本只是沟通工具,先定义“公共 API 的兼容性”:新增可选导出通常是 minor,修复是 patch,删除导出或改变默认行为是 major。弃用时保留迁移期和运行时提示,提示不能在生产高频刷屏。预发布版本使用独立 dist-tag,验证通过后再提升稳定标签。

发布到私有 registry 时显式配置 scope、registry、访问权限和 provenance 要求,CI 中使用短期凭据,不把 token 写进日志或包内。发布失败优先修复后发布新版本;不要依赖覆盖同一版本号,很多 registry 会禁止或缓存旧产物。真正的回滚通常是让消费者回到已知稳定版本、撤回 dist-tag 或在应用侧锁定版本,而不是删除历史包。

发布答题清单

公共入口和类型是否与 dist 一致?
-> exports 是否限制了深层路径,ESM/CJS 是否都由消费者冒烟验证?
-> sideEffects 是否准确,CSS/注册副作用是否会被误删?
-> npm pack 内容、依赖类别、锁文件和安全扫描是否通过?
-> 版本、变更日志、tag、registry 和回滚入口是否可追溯?

6. 请求并发池、超时预算与可观测性

6.1 并发池解决什么问题

浏览器、WebView 和服务端都可能有连接数、CPU 或下游 QPS 限制。请求层的并发池是应用级背压,不等同于浏览器的连接池:它应该按资源或业务隔离,例如 searchuploadanalytics 使用不同的上限,必要时再按用户或租户建立配额。不能把所有请求塞进一个全局队列,否则一个慢的批量任务会阻塞登录、保存等关键操作。

队列项至少包含任务函数、优先级、截止时间、取消信号和幂等信息。调度器只有在“未过期、未取消且当前槽位可用”时才启动任务;任务结束、抛错或取消都必须在 finally 中释放槽位。可以采用带上限的 FIFO 加优先级队列,并为低优先级任务保留配额,避免高频预加载把交互请求饿死。

type QueueTask<T> = {
  run: (signal: AbortSignal) => Promise<T>
  priority: number
  deadline: number
  signal?: AbortSignal
}

class RequestPool {
  private active = 0
  private queue: Array<QueueTask<unknown> & {
    resolve: (value: unknown) => void
    reject: (reason?: unknown) => void
  }> = []

  constructor(private readonly limit: number) {}

  add<T>(task: QueueTask<T>): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      this.queue.push({
        ...task,
        resolve: (value) => resolve(value as T),
        reject,
      })
      this.queue.sort((a, b) => b.priority - a.priority)
      this.drain()
    })
  }

  private drain() {
    while (this.active < this.limit && this.queue.length > 0) {
      const task = this.queue.shift()!
      if (task.signal?.aborted) {
        task.reject(task.signal.reason ?? new DOMException('request cancelled', 'AbortError'))
        continue
      }
      if (task.deadline <= Date.now()) {
        task.reject(new DOMException('queue deadline exceeded', 'TimeoutError'))
        continue
      }

      this.active += 1
      const remaining = Math.max(1, task.deadline - Date.now())
      const timeout = AbortSignal.timeout(remaining)
      task.run(timeout)
        .then(task.resolve, task.reject)
        .finally(() => {
          this.active -= 1
          this.drain()
        })
    }
  }
}

示例只展示调度边界,生产实现还应让队列项在“过期/已取消”时调用 reject,并组合调用方信号与超时信号(如 AbortSignal.any,或手动转发 abort)。不要把一个已被调用方取消的任务继续占用槽位;也不要在队列等待期间就开始计算网络超时。

6.2 用绝对截止时间管理超时

把超时写成一个孤立的 setTimeout(5000) 很容易在排队、重试和刷新 Token 后超预算。更可靠的做法是请求开始时生成一个绝对 deadline,把剩余时间传给每个阶段:

总预算 8s
  排队等待 0.8s
  首次请求最多 4s
  退避 0.5s
  Token 刷新最多 1.5s
  重放请求只能使用剩余 1.2s

每次开始尝试前计算 remaining = deadline - now;若小于最小可用窗口就直接结束,并把原因标成 deadline_exceeded。重试只对幂等请求或带服务端幂等键的写请求开放;每次尝试释放并发槽位后再退避,不能让睡眠中的重试占着池子。超时、主动取消、网络错误和 HTTP 业务错误要使用不同的结果码,避免监控把用户主动离开误报成服务故障。

队列也要有容量上限和拒绝策略。超过上限时可丢弃可重建的预加载、返回 queue_full,或让调用方选择降级;不能无限堆积 Promise 和请求体。上传、下载等长任务应按字节数或带宽预算另行限制,不能只看请求个数。

6.3 可观测字段和验证

每个逻辑请求使用稳定的 requestId,每次重试有递增的 attempt;跨服务调用再携带不含敏感信息的 traceId。至少记录:

  • poolKey、优先级、queueWaitMsinFlight、队列长度和拒绝数;用户/租户维度应使用脱敏或哈希后的标识。
  • deadlineMsattempt、退避时长、durationMs、HTTP 状态或网络错误码,以及 outcome(success、timeout、cancel、queue_full、http_error)。
  • 请求方法、规范化 URL 模板、资源大小和响应字节数;不要记录 Authorization、完整查询参数或请求体。

验证时分别压测单资源突发、多个资源互相影响、队列满、超时后重试、调用方取消和页面卸载。关注 P95/P99 排队等待与端到端耗时,而不是只看平均响应时间;如果池的上限变化,必须能从发布版本和配置快照还原当时的行为。

7. ESLint/tsconfig 继承、增量迁移与 CI 基线

7.1 共享约束,局部覆盖

大型仓库应把“默认约束”和“运行环境差异”分开。TypeScript 可维护一个根 tsconfig.base.json,再由应用、Node 脚本和测试配置通过 extends 继承;每个子配置只声明自己的 includeoutDir、JSX 或环境类型。项目较大时可用 Project References 把包拆成可独立构建的图,避免一个配置把浏览器和 Node 全局类型混在一起。

ESLint 也应有一个版本化的共享配置(现代 ESLint 可用 flat config 或发布 shareable config),在根部统一 parser、规则、忽略目录和插件版本,再按包/文件类型添加窄范围覆盖。不要把 dist、生成代码和 vendor 目录靠大量 /* eslint-disable */ 隐藏;忽略规则和源码范围应在配置中可审查。tsc 类型检查与 ESLint 规则是两条不同的质量线,不能用其中一个代替另一个。

一个常见的目录边界如下:

tsconfig.base.json       # 严格度、模块解析、共享路径
tsconfig.app.json        # 浏览器/JSX 入口
tsconfig.node.json       # 构建脚本和 Node 类型
packages/*/tsconfig.json # 包的 include、outDir、references
eslint.config.js         # 共享规则和忽略范围

配置本身也要固定在 lockfile 和 CI 的 Node/TypeScript/ESLint 版本中;升级配置工具时先看规则和模块解析的行为变化,再批量改代码。

7.2 老项目的增量迁移

不要一次把数万条历史错误混入每个 PR。先建立基线报告,记录文件、规则/诊断码、责任域和豁免期限;新提交必须“不能新增错误”,迁移工作再按包或目录逐步收敛。迁移顺序通常是:

  1. 先统一解析器、路径和生成目录,消除配置噪声。
  2. 打开低风险格式/规则并修复自动可修复项,再处理 noImplicitAny、严格空值等语义风险。
  3. 对 JS 目录先用 allowJs,必要时按边界启用 checkJs;新文件直接使用严格配置,旧文件按迁移清单逐步纳入。
  4. 每个包迁移完成后收紧 include、删除临时豁免并更新基线,避免基线成为永久垃圾场。

类型迁移要把外部输入(HTTP、Storage、Schema、环境变量)放在运行时校验边界;不要用 any@ts-ignore 或大量非空断言把错误从报告里抹掉。确有第三方声明缺陷时,使用带链接和到期日期的局部声明补丁,并让 CI 统计豁免数量。

7.3 CI 阻断级别和修复基线

CI 至少分三层:

层级 例子 默认策略
必须阻断 配置解析失败、类型错误、Lint error、单元测试失败、构建失败 PR 不能合并
受控告警 新增 Lint warning、覆盖率轻微下降、bundle 预算接近阈值 有预算和责任人,超过阈值才阻断
观察指标 全仓历史错误、非关键包迁移进度、缓存命中率 记录趋势,不用一次性阻塞所有团队

“只检查改动文件”适合迁移初期的增量门禁,但发布分支仍应跑受影响包的完整类型检查、测试和构建;否则跨包契约可能在合并后才暴露。CI 缓存键应包含 lockfile、配置文件、编译器版本和目标包,配置变更必须使缓存失效。

基线报告要可再生、可审计:每条豁免注明原因、负责人、创建提交和截止日期;CI 比较本次报告与基线,新增项直接失败,修复项从基线移除。每月或每个里程碑统计剩余项和平均修复时间,达到门槛后删除兼容开关。这样既不会用“全仓归零”阻断迁移,也不会把临时容忍变成永久标准。

8. 统一的场景题答题与验证模板

这几类题可以用同一套结构回答:

成功标准 -> 约束/能力检测 -> 状态机与数据模型
-> 关键 API 和协议 -> 失败、取消、兼容与安全边界
-> 可观测字段 -> 最小复现和回归验证 -> 后续演进

验证不应停留在“看起来能用”:Canvas 用不同 DPR、缩放和触摸输入验证坐标;下拉刷新用嵌套滚动、pointercancel 和 reduced-motion 验证;通信协议注入未知来源、乱序、重复和超时消息;i18n 用缺 key、慢网络、SSR 和 RTL 验证;npm 包用真实消费者安装 tarball 验证入口、类型和 tree-shaking。这样才能把资料中的题目转化为可迁移的工程能力。

高频追问

为什么 Canvas 不能直接截图 DOM?

drawImage 接受图像、视频或另一个 Canvas 等可绘制源,不会把任意 DOM 树按样式布局转换成位图。DOM 截图需要浏览器或专门渲染器逐项处理字体、伪元素、跨源资源和布局;Canvas 只适合绘制已经准备好的图像数据。(深入阅读:Canvas 坐标与绘制边界

passive: false 是否能解决所有下拉刷新冲突?

不能。它只决定监听器是否可以阻止默认行为;还要判断滚动边界、手势方向、touch-action、嵌套容器和系统级回弹,并处理取消和无障碍替代操作。(深入阅读:Pointer Events 与默认行为

postMessage 校验了 origin 就安全吗?

不够。还要校验 event.source、会话/nonce、消息 schema、权限和重放边界。消息通道安全只解决传输来源,不能替代服务端授权。(深入阅读:握手、安全与版本演进

sideEffects: false 为什么可能导致样式消失?

打包器会据此删除未被显式引用的模块。如果样式导入或全局注册依赖模块执行副作用,错误标记会把它当成可删除代码。应精确列出 CSS 或其他确实有副作用的文件,并用消费者包冒烟测试验证。(深入阅读:包入口与副作用声明