Skip to content

发布、监控与升级 ​

问题:测试全绿,为什么仍不知道线上跑的是哪一版 ​

构建通过只证明某份源码在某个环境产出了文件。用户报错时,还需要回答:哪次提交被部署、错误发生在哪个页面、旧版本是否仍可恢复、部署后的健康检查是否真的访问了新产物。发布因此是一条带身份和失败出口的协议,而不是最后一条上传命令。

完整应用侧例子:让错误带上发布身份 ​

Vite 会把 VITE_ 前缀变量暴露给客户端,因此其中只能放公开的发布 SHA,不能放监控密钥。保存以下两个文件:

js
// src/reportClientError.js
function publicContext(context) {
  const safe = {}
  if (context && typeof context.componentStack === 'string') {
    safe.componentStack = context.componentStack.slice(0, 4000)
  }
  return safe
}

export function buildClientErrorEvent(_error, context = {}, runtime = {}) {
  return {
    type: 'react-render-error',
    message: 'React 渲染失败',
    release: runtime.release ?? import.meta.env?.VITE_RELEASE ?? 'local',
    path: runtime.path ?? window.location.pathname,
    occurredAt: runtime.occurredAt ?? new Date().toISOString(),
    context: publicContext(context),
  }
}

export function reportClientError(error, context = {}) {
  const event = buildClientErrorEvent(error, context)
  // 教学环境可观察;生产中把 event 交给已配置且会脱敏的监控 SDK。
  console.error(event)
  return event
}

可信字段必须由运行环境写入,扩展上下文只能通过白名单进入嵌套的 context。下面的断言证明调用者传入同名字段或令牌时,既不能伪造发布身份,也不会把敏感字段带入事件:

js
// src/reportClientError.test.js
import { expect, test } from 'vitest'
import { buildClientErrorEvent } from './reportClientError'

test('上下文不能覆盖可信字段或夹带敏感值', () => {
  const event = buildClientErrorEvent(
    new Error('render failed: token=secret'),
    { release: 'forged', path: '/forged', token: 'secret', componentStack: 'in App' },
    { release: 'abc123', path: '/checkout', occurredAt: '2026-09-15T00:00:00.000Z' },
  )

  expect(event).toEqual({
    type: 'react-render-error',
    message: 'React 渲染失败',
    release: 'abc123',
    path: '/checkout',
    occurredAt: '2026-09-15T00:00:00.000Z',
    context: { componentStack: 'in App' },
  })
})
jsx
// src/ReleaseBoundary.jsx
import { Component } from 'react'
import { reportClientError } from './reportClientError'

export default class ReleaseBoundary extends Component {
  state = { failed: false }

  static getDerivedStateFromError() {
    return { failed: true }
  }

  componentDidCatch(error, info) {
    reportClientError(error, { componentStack: info.componentStack })
  }

  render() {
    if (this.state.failed) {
      return <main><h1>页面暂时不可用</h1><button onClick={() => location.reload()}>重新加载</button></main>
    }
    return this.props.children
  }
}

在入口用 <ReleaseBoundary><App /></ReleaseBoundary> 包住应用。示例不复制原始 error.message,因为错误文本可能拼入令牌、表单值或响应内容;统一公开说明配合 release 定位失败版本。参数 _error 保留了接收错误的接口,若以后增加错误分类,必须把它映射成审核过的公共错误码,不能直接字符串化所有输入。

上下文只接受 React 提供的 componentStack 并限制长度;这能限制字段范围,不能证明任意字符串安全。生产监控还需审核堆栈、动态路径和 SDK 的默认采集字段,禁止附带表单值、令牌、完整 URL 查询或未经处理的响应正文。发布 SHA 是关联源码和 source map 的索引,不是用户标识。

受控流水线分支:验证、构建、留存产物 ​

CI/CD 不是 React 自带功能。下面明确依赖 GitHub Actions 的 runner、actions/checkout、actions/setup-node 与 actions/upload-artifact。它不假称已部署到某个云厂商,而是生成带发布身份的可下载产物;实际部署步骤必须按目标平台增加,并通过平台健康检查后才算上线。

yaml
# .github/workflows/frontend.yml
name: frontend

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  verify-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test
      - run: npm run build
        env:
          VITE_RELEASE: ${{ github.sha }}
      - uses: actions/upload-artifact@v4
        with:
          name: frontend-${{ github.sha }}
          path: dist
          if-no-files-found: error

固定 Node 大版本与 lockfile,让本地和 CI 差异可解释。步骤按顺序执行,任一步非零都阻止产物上传。云平台发布应消费这个已经验证的产物,而不是重新从浮动依赖构建另一份;部署后再访问带版本标记或静态清单的 URL,确认服务的是预期 SHA。

第三方 Action 的主版本仍是外部依赖。实际项目应使用官方当前文档核对支持版本,并按组织供应链政策固定到提交 SHA;上面的版本用于说明配置关系,不是永久“最新版”承诺。

React 19 升级不是一次改版本号 ​

稳定应用使用 Latest 通道,不用 Canary API 充当课程主线。一个可回退顺序是:

  1. 在原分支保存关键行为测试、构建产物大小和错误率基线。
  2. 先升到 React 18.3,清理面向 React 19 的弃用警告;同时更新 react 与 react-dom,避免双版本。
  3. 升到 React 19 和匹配的类型包,启用新 JSX transform;运行 lint、测试、生产构建和真实浏览器冒烟。
  4. 特别搜索已移除入口:ReactDOM.render、ReactDOM.hydrate、unmountComponentAtNode、大部分 react-dom/test-utils、旧字符串 ref 与函数组件 propTypes/defaultProps 假设。
  5. 先把新版本交给小流量或预览环境;比较同一 release 的错误率和关键交互,再扩大范围。超过阈值就回滚到已知健康产物,而不是在线临时修补。

自动 codemod 只处理可识别语法。它不能证明第三方库兼容、交互语义不变或线上指标健康。

失败与监控边界 ​

  • 错误边界记录 render 崩溃,不会捕获所有事件、异步、资源加载或服务端错误;还需 window 错误、Promise 拒绝和后端信号,但必须去重与脱敏。
  • “上传成功”不是健康。部署系统要能确认目标版本可响应关键请求,并保留上一份可恢复产物。
  • 告警先关联 release、环境和路径,再决定阈值。没有版本维度的错误总数很难判断是否由新发布引入。
  • source map 可能泄露源码;上传到受控监控服务,不应无意公开在静态站点。

练习:用门禁决定发布动作 ​

实现 decideRelease(input)。输入含 checks(lint/test/build 布尔值)、expectedRelease、servedRelease、errorRate、baselineErrorRate。任一检查失败返回 block;已部署但版本不符返回 rollback;错误率大于基线的两倍且绝对增加至少 0.01 返回 rollback;否则返回 promote。所有返回值还要带机器可读 reason。用断言证明优先级:检查失败即使版本也错误,仍返回 block。

核对第 03 篇答案

返回本章导读

用心记录每一步成长