感谢你愿意贡献。本文是给人类协作者的指南;AI Agent 协作者请优先阅读 AGENTS.md。
pnpm + turbo 单仓多包:
packages/
core/ # ★ 发布到 npm 的 react-zmage 库
home/ # 演示站源码 (Vite)
sandbox-r17/ # ┐
sandbox-r18/ # ├ R17/18/19 真实 npm 消费者集成测试 (npm pack)
sandbox-r19/ # ┘
docs/ # 演示站构建产物 (GitHub Pages)
类型与默认值的 single-source-of-truth:
packages/core/src/types/global.ts— 所有 props 类型packages/core/src/types/default.ts— 所有默认值与预设packages/core/src/index.ts— 运行时导出
需要 pnpm 9.x 与 Node.js 18+。
git clone https://github.com/Caldis/react-zmage
cd react-zmage
pnpm install
pnpm dev # 默认演示站 (R17 + Vite SPA), http://127.0.0.1:8080修改 packages/core 后 home 应用会通过 workspace 链接热更新。
pnpm dev:csr-r17 # CSR · Vite SPA · React 17 (:8080)
pnpm dev:csr-r18 # CSR · Vite SPA · React 18 (:8080)
pnpm dev:csr-r19 # CSR · Vite SPA · React 19 (:8080)
pnpm dev:ssr-r19 # SSR · Express + Vite + renderToString · R19 (:8090)
pnpm dev:nextjs # RSC · Next.js 15 App Router · R19 (:8095)每个 demo 顶部固定一个 ContextBanner 显示当前实际加载的 React 版本与渲染模式,便于在切换不同环境时确认上下文。GUI / 动画 / 视觉验收只能由人来做——CI 与 Agent 仅负责构建/类型/运行时 smoke 不出错。
| 命令 | 用途 |
|---|---|
pnpm test |
core 单元测试 (vitest + @testing-library/react, jsdom 环境) |
pnpm build |
产出 dist/ (tsup 出 cjs/esm/iife + tsc 出 .d.ts) |
pnpm lint |
eslint + stylelint |
pnpm -w run check |
完整跨版本程序化测试:build → pack → 重装 → R17/R18/R19 sandbox 跑 strict tsc + 在真实 Node 跑 renderToString smoke + Next.js sandbox 跑 next build |
任何会改动 packages/core/src 或 packages/core/package.json 的 PR,合入前必须能通过 pnpm -w run check。
CI 会在 GitHub Actions 上分别跑 build job 和 sandbox-matrix job (R17/R18/R19 三组并行)。
- TypeScript,所有源码已
.tsx/.ts化 - ESLint + Stylelint 见仓库根的
.eslintrc.js/.stylelintrc.js - 现有代码以 React 类组件为主(
React.Component<P, S>+ arrow methods)。新增内部组件可以是函数组件,但不要在同一个 PR 里做大规模 class→function 迁移——这是 2.0 级别的改动 - 缩进 2 空格,不带分号风格被现有代码混用,跟随就近文件
- 导入顺序:libs → components → utils → types(参考
Zmage.tsx顶部)
下面这些是历史 PR 已经修过的坑,新代码必须保持,否则会回退到已知 bug:
-
所有异步操作的句柄必须可取消并在
componentWillUnmount中取消。requestAnimationFrame/setTimeout/setInterval都要存为实例属性,卸载时显式 cancel。否则 React StrictMode 双 mount/unmount 会泄漏监听器,并触发"setState on unmounted component"警告。- 参考:
Browser.tsx的initRaf/unInitTimer、Image.tsx的pendingRafHandles、Zmage.callee.tsx的inBrowsingRaf/outBrowsingTimer
- 参考:
-
unInit({force: true})必须同步执行清理副作用,不要把enableScroll/unlockTouchInteraction/removeEventListener等放在setState → setTimeout → setState链尾——卸载路径下 setState 会被丢弃,副作用永远跑不到。 -
packages/core/src/types/global.ts不能改回.d.ts。tsc 默认不 emit 源.d.ts;改回去会让 dist 缺关键类型,下游消费者所有 prop 推断退化为any。 -
公共组件类型保持 callable + statics 形态,不要用
ForwardRefExoticComponent交叉。当前ReactZmageComponent = ((props) => JSX.Element | null) & { browsing, wrapper, ... }是有意为之,绕开了@types/react@18+的ReactPortal回归与defaultProps引发的 prop 推断丢失。改回ForwardRefExoticComponent会破坏 R18/R19 消费者的严格 JSX 检查。 -
react-dom/client必须通过运行时require获取,不能静态import。静态 import 会让 R16/17 消费方 bundler 报 "Module not found"。当前Zmage.callee.tsx的resolveMountAdapter()用require + try/catch平滑降级。 -
tsup.config.ts的external必须包含react、react-dom、react-dom/client。少一个会让 R18+ 的 mount 适配器无法在打包后被消费方解析。
详细技术背景可看对应的 PR commit message:
c38a936PR-2 — StrictMode 清理54ccaf2PR-1 — .d.ts pipeline + 类型 refactor46c744ePR-3 — R18/19 mount adapter
新建 Issue。请尽量提供:
- React 版本(16/17/18/19)
- react-zmage 版本
- 浏览器与桌面/移动环境
- 最小可复现代码(CodeSandbox / StackBlitz 链接最佳)
- 期望 vs 实际行为
与"未规划但有意义"的方向也欢迎以 Issue 起讨论。已知规划见 ROADMAP.md。
- Fork → 建分支(建议
feat/xxx/fix/xxx/docs/xxx) - 改完代码 →
pnpm test→pnpm -w run check都过 - 提交(见下方约定)
- Push → 在 GitHub 上发起 PR 到
master - PR 描述请覆盖:
- 改动动机(解决什么问题 / 实现什么需求)
- 方案要点(关键设计决策与权衡)
- 验证方式(跑了哪些测试 / 在什么环境下手动验证)
- 如果触及架构不变量,明确说明仍然遵守
使用类似 Conventional Commits 的简化形式:
<type>: <subject>
<body explaining why and what>
<type> 取值:
feat— 新功能fix— 修 bugdocs— 文档chore— 构建 / 工具 / 依赖refactor— 不改外部行为的重构test— 测试相关perf— 性能优化
Body 描述建议包含为什么而不仅仅是做了什么——README/ AGENTS.md 已经描述 what,commit message 应该解释 why。
跨多文件的功能性改动可以走 PR 编号(参考最近的 PR-0 .. PR-4 命名风格)。