Skip to content

fix(runner): 站内导航保留 query string,?api= 不再被抹掉 - #3594

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3578-preserve-api-query
Aug 7, 2026
Merged

fix(runner): 站内导航保留 query string,?api= 不再被抹掉#3594
yinlianghui merged 1 commit into
mainfrom
claude/issue-3578-preserve-api-query

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3578

问题

packages/runner/src/App.tsxhandleNavigate 原先是 window.history.pushState({}, '', to) —— to 是侧栏导航项给的裸路径,于是地址栏上的 query string 在第一次点击后就没了。

loader 被 useMemo(..., []) memo 住,所以当前会话看起来一切正常;真正的代价在会话之外:

  • F5params.get('api') 读到 null → 退回 LocalBundleLoader,而 src/app-data/ 在正常安装里是空的(gitignore),于是页面变成 Page not found
  • 复制地址栏分享 → 对方拿到的是不带后端的 URL。

而且失败现场不指认根因:没有任何提示说「API 基址丢了」,控制台只打印 📦 Using Local Bundle Loader

裁定与做法

按 ADR-0054 C3「可寻址状态放 URL」——「当前指向哪个后端」是典型的可寻址状态(会被分享、刷新后期待还在)——采用方向 1:导航时把当前 query string 一并带走

保留整个 query,还是只保留 api?先量了再定

参数谁读的file:line
apirunner 自己packages/runner/src/App.tsx:45-46(git greppackages/runner/src 下的唯一一处 query 读取)
__debug / __debug_schema / __debug_perf / __debug_data / __debug_expr / __debug_events / __debug_registryrunner 渲染的树里的依赖包packages/core/src/utils/debug.ts:39-50(parseDebugFlags)、packages/core/src/utils/debug.ts:86(isDebugEnabled)、packages/react/src/hooks/useDebugMode.ts:51

所以 api不是今天唯一有语义的参数 —— runner 依赖 @object-ui/react / @object-ui/core,那套 debug 开关同样直接读 window.location.search,同样被裸路径 pushState 抹掉。只搬 api 会把第二类参数留在原地继续丢,是同一种静默丢失,只是爆炸半径小一点。故保留整个 query string —— 也最少惊讶,并且以后谁再开始读 query 都自动受益。

改在哪

只改 handleNavigate 一处。读取侧经测量无需改动:currentPath 的初值(App.tsx:39)和 popstate handler(App.tsx:80)都只读 location.pathname,不受 query 影响;loader 的 useMemo 依旧是挂载时读一次 —— 本 PR 修的正是「刷新时它还能读到」。

新增的 withPreservedQuery(to, currentSearch):

  • 常见情形(to 是裸路径):把当前 query string 原样接上,不做重编码,地址栏保持用户输入的样子。
  • 无 query 时:原样返回 to,不会多出一个空的 ?
  • to 自带 query:它自己的参数保留且同名优先,其余当前参数并在其后 —— 绝不拼出 path?a=1?b=2 这种畸形 URL(这条分支会经 URLSearchParams 重建,值按标准做百分号编码,是同一批参数的规范拼法)。

测试

新增 packages/runner/src/App.navigation.test.tsx,4 条,驱动真实组件(stub 掉 loader,因为 src/app-data/ 是 gitignore 的),所以对未修改的源码也有意义。

反向验证(先预测方向,再跑):预测 3 红 1 绿 —— 三条「保留」断言在旧代码下必红,而「无 query 时不多加 ?」那条在旧代码下必绿(裸路径天然满足它),它防的是新逻辑过度施加,不是回归探测器。在未修改App.tsx 上实跑,与预测完全一致:

 ❯ |dom| packages/runner/src/App.navigation.test.tsx (4 tests | 3 failed)
× carries `?api=` across a sidebar navigation …
Expected: "/customers?api=https://backend.example.com/api"
Received: "/customers"
× carries the WHOLE query string, not just `api` …
× merges into a target that declares its own query …
Expected: "/orders?tab=open&api=…" Received: "/orders?tab=open"
Tests 3 failed | 1 passed (4)

改完后全绿:

$ pnpm exec vitest run packages/runner --maxWorkers=2
Test Files 3 passed (3)
Tests 13 passed (13)
$ pnpm --workspace-concurrency=2 --filter '@object-ui/runner' type-check
> tsc --noEmit && tsc -p tsconfig.test.json
(exit 0)
$ pnpm --workspace-concurrency=2 --filter '@object-ui/runner' lint
✖ 23 problems (0 errors, 23 warnings) # 全部既有;App.tsx 那条 React Compiler 警告是原有的 `useMemo`,只是行号被上移的 helper 顶下去了
$ node scripts/check-control-bytes.mjs
✅ check-control-bytes: OK (scanned 3631 tracked text file(s); skipped 85 binary)

文档同步

PR #3581 本小时刚把这个行为如实写成注意事项;修复落地后那段话就变成在描述一个已修复的缺陷,故一并改掉:

  • content/docs/utilities/runner.mdx —— 「Read once, at mount」那条 caveat 重写为新行为(query 跟着走、刷新/分享可用;整串 query 而不只是 api;目标自带 query 时的合并规则)。「挂载时读一次」这半句仍然属实(不刷新地改地址栏不生效),予以保留。
  • packages/runner/README.md:91-92 —— 同一句缺陷描述在包 README 里还有一份(「the caveat that in-app navigation drops ?api= from the address bar」),同步改掉。这一处超出了派单的文件面(派单只点了 mdx),但它和 mdx 那句是同一个事实的两份拷贝,留着就是在同一个 PR 里发布一句已知为假的话。请 review 时确认;要退回随时可以只 revert 这两行。

越界说明

packages/runner/tsconfig.test.json 加了一行 "types": ["vite/client"]。原因不是顺手清理:新测试是 runner 第一个 importApp.tsx 的测试,把 src/lib/MetadataLoader.ts 拉进了 tsconfig.test.json 的 program,而该文件用了 import.meta.glob;tsconfig.test.json extends 的是 tsconfig(不是 Vite 应用,不带 vite/client),于是 tsc -p tsconfig.test.json 报 3 条 TS2339: Property 'glob' does not exist on type 'ImportMeta'。这是本 PR 引入的,不是既有问题(把新测试文件挪走后 type-check 立刻 exit 0,已实测)—— 修法与 packages/{layout,plugin-calendar,plugin-report,types}/tsconfig.test.json 既有的 types 声明同一形状。

Changeset

.changeset/runner-preserve-query-on-navigate.md —— @object-ui/runner: patch(用户可见行为修复)。


Generated by Claude Code

`handleNavigate` 原先 `pushState({}, '', to)` 推的是侧栏给的裸路径,
地址栏上的 query 在第一次点击后就没了。memo 住的 loader 让当前会话
看起来正常,但 F5 或把 URL 分享出去时 `params.get('api')` 读到 null,
静默退回(正常安装里为空的)LocalBundleLoader,页面变成 Page not found,
而现场没有任何东西指认「API 基址丢了」。
按 ADR-0054 C3「可寻址状态放 URL」,导航时把当前 query string 一并带走。
保留的是**整个** query,不是只挑 `api`:runner 自己只读 `api`
(`App.tsx`),但它渲染的树里 `@object-ui/core` 的 `parseDebugFlags` /
`@object-ui/react` 的 `useDebugMode` 同样直接读 `window.location.search`
(`?__debug`、`?__debug_schema` 等),只搬 `api` 会把第二类参数留在原地
继续丢 —— 同一种静默丢失,只是爆炸半径小一点。
目标路径若自带 query 则原样保留、同名参数以它为准,其余当前参数并在其后,
绝不拼出 `path?a=1?b=2` 这种畸形 URL。读取侧无需改动:`currentPath` 初值
与 popstate 都只读 `location.pathname`,不受 query 影响。
Fixes#3578
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 7, 2026 3:01pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-CVz6ARkO.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)480.72KB105.64KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)137.51KB35.11KB
fields (index.js)230.87KB56.83KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.03KB28.88KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.79KB57.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)186.61KB49.34KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.54KB1.30KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationtests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runner 的站内导航把 ?api= 从地址栏抹掉:刷新/分享该 URL 会静默退回空的本地打包加载器

2 participants

@yinlianghui@claude