From 6a6ab8e40e7f8ca9988762da9a37f58d75d1d714 Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 30 Jul 2026 15:34:27 +0800 Subject: [PATCH 01/13] fix(core): preserve RN initial page params --- .../mpx2rn/references/rn-script-reference.md | 4 ++-- docs-vitepress/guide/rn/application-api.md | 2 +- packages/core/src/platform/createApp.ios.js | 21 ++++++++----------- .../platform/patch/getDefaultOptions.ios.js | 6 +++++- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 8324a60915..44ddbf6560 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -150,7 +150,7 @@ | `pageLifetimes.show` | 组件 | 所在页面展示或重新获得焦点时触发,与页面 `onShow` 时机对齐。 | | `pageLifetimes.hide` | 组件 | 所在页面隐藏或失焦时触发,与页面 `onHide` 时机对齐。 | | `pageLifetimes.resize` | 组件 | 所在页面可视区域尺寸变化时触发,与页面 `onResize` 时机对齐。 | -| `onLoad` | 页面 | 页面创建后调用,**两个参数** `(rawQuery, decodedQuery)`。 | +| `onLoad` | 页面 | 页面创建后调用,**两个参数** `(rawQuery, decodedQuery)`;冷启动首次首页会保留 `parseAppProps` 返回的原始 `initialParams`,并与路由参数合并。 | | `created` | 组件 | 组件实例刚创建,RN 由 `MpxProxy` 在实例建立阶段调度,此时不宜依赖完整视图。 | | `attached` | 组件 | 组件进入节点树,RN 对齐为挂载流程中的对应阶段,详见 `docs-vitepress/guide/basic/lifecycle.md` 映射表。 | | `ready` | 组件 | 组件布局完成、可与视图交互,RN 对应 React 挂载后的就绪时机,与页面 `onReady` 同属一套内置映射。 | @@ -753,7 +753,7 @@ Mpx.config.rnConfig = { | 配置项 | 说明 | | --- | --- | | `projectName` | 由构建注入到 RN 入口,与 `AppRegistry.registerComponent` 相关(偏构建侧)。 | -| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由。 | +| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由;`initialParams` 会原样传给冷启动首次首页的 `onLoad`,并在初始路由实例存续期间作为应用 `onLaunch` / `onShow` 的 `query`。 | | `onStateChange` | 导航 state 变化时回调。 | | `disablePageTransition` | 为 `true` 时禁用 RN 页面转场动画,框架内部映射为 `animation: "none"`。 | | `disableAppStateListener` | 为 `true` 时不注册 `AppState` 监听(避免与宿主 App 重复)。 | diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index c16e0e29ca..abe5363567 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -417,7 +417,7 @@ createComponent({ 用于获取初始路由配置的函数,参数为 RN 根组件接收到的参数 - initialRouteName: 首页路径,例如 pages/index -- initialParams: 将作为 首页onLoad 与 应用onLaunch 的参数,例如 \{ a: 1 \} +- initialParams: 冷启动时将原样作为首次首页 `onLoad` 的参数,并在初始路由实例存续期间作为应用 `onLaunch` / `onShow` 的 `query`,例如 \{ a: 1 \}。首次首页同时存在路由参数时会进行合并,路由参数优先 在需要将 RN 应用嵌入到现有的 NA 应用中时,NA 可能会向 RN 的根组件传递 props,此时可在 parseAppProps 中接受 props 并进行处理和透传到页面 diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index 216ba809f8..8dd28c1731 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -52,7 +52,7 @@ export default function createApp (options) { const pagesMap = currentInject.pagesMap || {} const firstPage = currentInject.firstPage const Stack = createNativeStackNavigator() - const getPageScreens = (initialRouteName, initialParams) => { + const getPageScreens = () => { return Object.entries(pagesMap).map(([key, item]) => { const pageConfig = Object.assign({}, global.__mpxPageConfig, global.__mpxPageConfigsMap[key]) const headerLayout = ({ navigation, children }) => { @@ -72,14 +72,6 @@ export default function createApp (options) { const getComponent = () => { return item.displayName ? item : callWithErrorHandling(item, null, 'require page script') } - if (key === initialRouteName) { - return createElement(Stack.Screen, { - name: key, - getComponent, - initialParams, - layout: headerLayout - }) - } return createElement(Stack.Screen, { name: key, getComponent, @@ -120,7 +112,7 @@ export default function createApp (options) { const current = state.routes[state.index] options = { path: current.name, - query: current.params, + query: current.key === global.__mpxInitialRouteKey ? global.__mpxInitialRunParams : current.params, scene: 0, shareTicket: '', referrerInfo: {} @@ -178,9 +170,10 @@ export default function createApp (options) { const state = navigation.getState() Mpx.config.rnConfig.onStateChange?.(state) const current = state.routes[state.index] + global.__mpxInitialRouteKey = current.key const options = { path: current.name, - query: current.params, + query: global.__mpxInitialRunParams, scene: 0, shareTicket: '', referrerInfo: {}, @@ -211,6 +204,10 @@ export default function createApp (options) { }, []) const { initialRouteName, initialParams } = initialRouteRef.current + if (!global.__mpxAppHotLaunched) { + global.__mpxInitialRouteKey = null + global.__mpxInitialRunParams = initialParams + } const navScreenOpts = { headerShown: false } @@ -230,7 +227,7 @@ export default function createApp (options) { initialRouteName, screenOptions: navScreenOpts }, - ...getPageScreens(initialRouteName, initialParams) + ...getPageScreens() ) ) ) diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index c1ca16706a..f5cf2191c7 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -296,12 +296,16 @@ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps }) } + let loadParams if (type === 'page') { const props = propsRef.current instance.route = props.route.name global.__mpxPagesMap = global.__mpxPagesMap || {} global.__mpxPagesMap[props.route.key] = [instance, props.navigation] setFocusedNavigation(props.navigation) + if (!global.__mpxAppHotLaunched && global.__mpxInitialRunParams) { + loadParams = Object.assign({}, global.__mpxInitialRunParams) + } set(global.__mpxPageSizeCountMap, pageId, global.__mpxSizeCount) // App onLaunch 在 Page created 之前执行 if (!global.__mpxAppHotLaunched && global.__mpxAppOnLaunch) { @@ -315,7 +319,7 @@ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps if (type === 'page') { const props = propsRef.current const decodedQuery = {} - const rawQuery = props.route.params || {} + const rawQuery = Object.assign({}, loadParams, props.route.params || {}) if (isObject(rawQuery)) { for (const key in rawQuery) { try { From ea64b6d3f779d1517d7cce01afa6927ebf913efa Mon Sep 17 00:00:00 2001 From: xuegan Date: Sat, 5 Sep 2026 15:26:45 +0800 Subject: [PATCH 02/13] fix(core): use initial state for RN launch params --- .../mpx2rn/references/rn-script-reference.md | 4 +- docs-vitepress/guide/rn/application-api.md | 2 +- .../__tests__/common/createApp.ios.spec.js | 190 ++++++++++++++++++ packages/core/src/platform/createApp.ios.js | 14 +- .../platform/patch/getDefaultOptions.ios.js | 6 +- 5 files changed, 202 insertions(+), 14 deletions(-) create mode 100644 packages/core/__tests__/common/createApp.ios.spec.js diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 44ddbf6560..f6f27e91a6 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -150,7 +150,7 @@ | `pageLifetimes.show` | 组件 | 所在页面展示或重新获得焦点时触发,与页面 `onShow` 时机对齐。 | | `pageLifetimes.hide` | 组件 | 所在页面隐藏或失焦时触发,与页面 `onHide` 时机对齐。 | | `pageLifetimes.resize` | 组件 | 所在页面可视区域尺寸变化时触发,与页面 `onResize` 时机对齐。 | -| `onLoad` | 页面 | 页面创建后调用,**两个参数** `(rawQuery, decodedQuery)`;冷启动首次首页会保留 `parseAppProps` 返回的原始 `initialParams`,并与路由参数合并。 | +| `onLoad` | 页面 | 页面创建后调用,**两个参数** `(rawQuery, decodedQuery)`;RN 根组件初始化时,首个页面实例会收到 `parseAppProps` 返回的 `initialParams`,后续创建的同路径页面实例不会自动继承。 | | `created` | 组件 | 组件实例刚创建,RN 由 `MpxProxy` 在实例建立阶段调度,此时不宜依赖完整视图。 | | `attached` | 组件 | 组件进入节点树,RN 对齐为挂载流程中的对应阶段,详见 `docs-vitepress/guide/basic/lifecycle.md` 映射表。 | | `ready` | 组件 | 组件布局完成、可与视图交互,RN 对应 React 挂载后的就绪时机,与页面 `onReady` 同属一套内置映射。 | @@ -753,7 +753,7 @@ Mpx.config.rnConfig = { | 配置项 | 说明 | | --- | --- | | `projectName` | 由构建注入到 RN 入口,与 `AppRegistry.registerComponent` 相关(偏构建侧)。 | -| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由;`initialParams` 会原样传给冷启动首次首页的 `onLoad`,并在初始路由实例存续期间作为应用 `onLaunch` / `onShow` 的 `query`。 | +| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由;`initialParams` 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`。后续创建的同路径页面实例不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准。 | | `onStateChange` | 导航 state 变化时回调。 | | `disablePageTransition` | 为 `true` 时禁用 RN 页面转场动画,框架内部映射为 `animation: "none"`。 | | `disableAppStateListener` | 为 `true` 时不注册 `AppState` 监听(避免与宿主 App 重复)。 | diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index abe5363567..5e34fba241 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -417,7 +417,7 @@ createComponent({ 用于获取初始路由配置的函数,参数为 RN 根组件接收到的参数 - initialRouteName: 首页路径,例如 pages/index -- initialParams: 冷启动时将原样作为首次首页 `onLoad` 的参数,并在初始路由实例存续期间作为应用 `onLaunch` / `onShow` 的 `query`,例如 \{ a: 1 \}。首次首页同时存在路由参数时会进行合并,路由参数优先 +- initialParams: 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`,例如 \{ a: 1 \}。后续通过路由 API 再次打开同路径页面时不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准 在需要将 RN 应用嵌入到现有的 NA 应用中时,NA 可能会向 RN 的根组件传递 props,此时可在 parseAppProps 中接受 props 并进行处理和透传到页面 diff --git a/packages/core/__tests__/common/createApp.ios.spec.js b/packages/core/__tests__/common/createApp.ios.spec.js new file mode 100644 index 0000000000..bf6c312242 --- /dev/null +++ b/packages/core/__tests__/common/createApp.ios.spec.js @@ -0,0 +1,190 @@ +import createApp from '../../src/platform/createApp.ios' +import transferOptions from '../../src/core/transferOptions' +import Mpx from '../../src/index' + +jest.mock('../../src/core/transferOptions', () => ({ + __esModule: true, + default: jest.fn() +})) + +jest.mock('../../src/platform/patch/builtInKeysMap', () => ({ + __esModule: true, + default: {} +})) + +jest.mock('@mpxjs/utils', () => ({ + makeMap: (keys) => keys.reduce((map, key) => { + map[key] = true + return map + }, {}), + spreadProp: (options) => options, + getFocusedNavigation: jest.fn(), + hasOwn: (target, key) => Object.prototype.hasOwnProperty.call(target, key), + callWithErrorHandling: jest.fn() +}), { virtual: true }) + +jest.mock('../../src/convertor/mergeLifecycle', () => ({ + mergeLifecycle: () => ({ + app: ['onLaunch', 'onShow', 'onHide', 'onError', 'onUnhandledRejection'] + }) +})) + +jest.mock('../../src/platform/patch/lifecycle/index', () => ({ + LIFECYCLE: {} +})) + +jest.mock('../../src/index', () => { + const Mpx = function () {} + Mpx.config = { rnConfig: {} } + return { + __esModule: true, + default: Mpx + } +}) + +jest.mock('../../src/observer/reactive', () => ({ + reactive: (value) => value +})) + +jest.mock('../../src/observer/watch', () => ({ + watch: jest.fn() +})) + +jest.mock('react', () => ({ + createElement: (type, props, ...children) => ({ + type, + props: Object.assign({}, props, { children }) + }), + memo: (component) => component, + useRef: (value) => ({ current: value }), + useEffect: jest.fn() +}), { virtual: true }) + +jest.mock('react-native', () => ({ + AppState: { + addEventListener: jest.fn() + } +}), { virtual: true }) + +jest.mock('../../src/platform/export/inject', () => ({ + initAppProvides: jest.fn() +})) + +jest.mock('../../src/platform/env/navigationHelper', () => ({ + NavigationContainer: 'NavigationContainer', + createNativeStackNavigator: () => ({ + Navigator: 'StackNavigator', + Screen: 'StackScreen' + }), + SafeAreaProvider: 'SafeAreaProvider', + GestureHandlerRootView: 'GestureHandlerRootView' +}), { virtual: true }) + +jest.mock('@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-nav', () => 'MpxNav', { virtual: true }) + +describe('RN createApp initial params', () => { + const globalKeys = [ + '__mpxAppCbs', + '__mpxAppHotLaunched', + '__mpxAppLaunched', + '__mpxAppOnLaunch', + '__mpxEnterOptions', + '__mpxInitialRouteKey', + '__mpxInitialRunParams', + '__mpxLaunchOptions', + '__mpxOptionsMap', + '__mpxPageConfig', + '__mpxPageConfigsMap', + '__mpxPagesMap', + '__navigationHelper', + 'getApp', + 'getCurrentPages', + 'setAppHide', + 'setAppShow' + ] + + beforeEach(() => { + global.__mpxAppCbs = { + show: [], + hide: [], + error: [], + rejection: [] + } + global.__mpxPageConfig = {} + global.__mpxPageConfigsMap = { + 'pages/index': {} + } + global.__navigationHelper = {} + Mpx.config.rnConfig = {} + }) + + afterEach(() => { + jest.clearAllMocks() + globalKeys.forEach((key) => { + delete global[key] + }) + }) + + it('stores initial params on the initial route state instead of Screen defaults', () => { + const initialParams = { a: 1 } + const onLaunch = jest.fn() + const onStateChange = jest.fn() + const IndexPage = () => null + IndexPage.displayName = 'IndexPage' + transferOptions.mockReturnValue({ + rawOptions: { onLaunch }, + currentInject: { + moduleId: 'app', + firstPage: 'pages/index', + pagesMap: { + 'pages/index': IndexPage + } + } + }) + Mpx.config.rnConfig = { + parseAppProps: () => ({ + initialRouteName: 'pages/index', + initialParams + }), + onStateChange + } + + createApp({}) + const tree = global.__mpxOptionsMap.app({}) + const navigationContainer = tree.props.children[0] + const stackNavigator = navigationContainer.props.children[0] + const indexScreen = stackNavigator.props.children[0] + + expect(navigationContainer.props.initialState).toEqual({ + routes: [{ + name: 'pages/index', + params: initialParams + }] + }) + expect(indexScreen.props).not.toHaveProperty('initialParams') + expect(global.__mpxInitialRouteKey).toBeUndefined() + expect(global.__mpxInitialRunParams).toBeUndefined() + + const resetRouteParams = Object.assign({}, indexScreen.props.initialParams, { b: 2 }) + expect(resetRouteParams).toEqual({ b: 2 }) + + const state = { + index: 0, + routes: [{ + key: 'pages/index-1', + name: 'pages/index', + params: initialParams + }] + } + global.__mpxAppOnLaunch({ + getState: () => state + }) + + expect(onStateChange).toHaveBeenCalledWith(state) + expect(onLaunch).toHaveBeenCalledWith(expect.objectContaining({ + path: 'pages/index', + query: initialParams, + isLaunch: true + })) + }) +}) diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index 8dd28c1731..2a8d42f5be 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -112,7 +112,7 @@ export default function createApp (options) { const current = state.routes[state.index] options = { path: current.name, - query: current.key === global.__mpxInitialRouteKey ? global.__mpxInitialRunParams : current.params, + query: current.params, scene: 0, shareTicket: '', referrerInfo: {} @@ -170,10 +170,9 @@ export default function createApp (options) { const state = navigation.getState() Mpx.config.rnConfig.onStateChange?.(state) const current = state.routes[state.index] - global.__mpxInitialRouteKey = current.key const options = { path: current.name, - query: global.__mpxInitialRunParams, + query: current.params, scene: 0, shareTicket: '', referrerInfo: {}, @@ -204,9 +203,11 @@ export default function createApp (options) { }, []) const { initialRouteName, initialParams } = initialRouteRef.current - if (!global.__mpxAppHotLaunched) { - global.__mpxInitialRouteKey = null - global.__mpxInitialRunParams = initialParams + const initialState = { + routes: [{ + name: initialRouteName, + params: initialParams + }] } const navScreenOpts = { headerShown: false @@ -219,6 +220,7 @@ export default function createApp (options) { null, createElement(NavigationContainer, { + initialState, onStateChange, onUnhandledAction }, diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index f5cf2191c7..c1ca16706a 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -296,16 +296,12 @@ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps }) } - let loadParams if (type === 'page') { const props = propsRef.current instance.route = props.route.name global.__mpxPagesMap = global.__mpxPagesMap || {} global.__mpxPagesMap[props.route.key] = [instance, props.navigation] setFocusedNavigation(props.navigation) - if (!global.__mpxAppHotLaunched && global.__mpxInitialRunParams) { - loadParams = Object.assign({}, global.__mpxInitialRunParams) - } set(global.__mpxPageSizeCountMap, pageId, global.__mpxSizeCount) // App onLaunch 在 Page created 之前执行 if (!global.__mpxAppHotLaunched && global.__mpxAppOnLaunch) { @@ -319,7 +315,7 @@ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps if (type === 'page') { const props = propsRef.current const decodedQuery = {} - const rawQuery = Object.assign({}, loadParams, props.route.params || {}) + const rawQuery = props.route.params || {} if (isObject(rawQuery)) { for (const key in rawQuery) { try { From dd1b6903857317d72ccd1501ec890bdeb44f49c2 Mon Sep 17 00:00:00 2001 From: xuegan Date: Sat, 5 Sep 2026 15:43:38 +0800 Subject: [PATCH 03/13] test(core): define perf globals for Jest --- packages/core/jest.config.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/jest.config.json b/packages/core/jest.config.json index 04f974ba52..d27215cd75 100644 --- a/packages/core/jest.config.json +++ b/packages/core/jest.config.json @@ -2,5 +2,9 @@ "transform": { "^.+\\.(js|jsx)?$": ["babel-jest", { "rootMode": "upward" }] }, - "testEnvironment": "jsdom" + "testEnvironment": "jsdom", + "globals": { + "__mpx_perf__": false, + "__mpx_perf_framework__": false + } } From 4f14d3761ceb11cd564ad1e76588ed8d4192c164 Mon Sep 17 00:00:00 2001 From: xuegan Date: Wed, 9 Sep 2026 14:10:33 +0800 Subject: [PATCH 04/13] fix(core): handle invalid RN initial routes --- .../mpx2rn/references/rn-script-reference.md | 2 +- docs-vitepress/guide/rn/application-api.md | 2 +- .../__tests__/common/createApp.ios.spec.js | 55 +++++++++++++++++-- packages/core/src/platform/createApp.ios.js | 11 +++- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 97638a3a02..3d869c6b46 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -717,7 +717,7 @@ Mpx.config.rnConfig = { | 配置项 | 说明 | | --- | --- | | `projectName` | 由构建注入到 RN 入口,与 `AppRegistry.registerComponent` 相关(偏构建侧)。 | -| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由;`initialParams` 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`。后续创建的同路径页面实例不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准。 | +| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由;`initialParams` 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`。后续创建的同路径页面实例不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准。`initialRouteName` 未注册时,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),回退至应用首页,并丢弃该错误路由的 `initialParams`。 | | `onStateChange` | 导航 state 变化时回调。 | | `disablePageTransition` | 为 `true` 时禁用 RN 页面转场动画,框架内部映射为 `animation: "none"`。 | | `disableAppStateListener` | 为 `true` 时不注册 `AppState` 监听(避免与宿主 App 重复)。 | diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index 5e34fba241..dde6607ef0 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -416,7 +416,7 @@ createComponent({ 用于获取初始路由配置的函数,参数为 RN 根组件接收到的参数 -- initialRouteName: 首页路径,例如 pages/index +- initialRouteName: 首页路径,例如 pages/index。若该路径未注册,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),回退至应用首页,并丢弃该错误路由的 `initialParams` - initialParams: 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`,例如 \{ a: 1 \}。后续通过路由 API 再次打开同路径页面时不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准 在需要将 RN 应用嵌入到现有的 NA 应用中时,NA 可能会向 RN 的根组件传递 props,此时可在 parseAppProps 中接受 props 并进行处理和透传到页面 diff --git a/packages/core/__tests__/common/createApp.ios.spec.js b/packages/core/__tests__/common/createApp.ios.spec.js index bf6c312242..e64091caac 100644 --- a/packages/core/__tests__/common/createApp.ios.spec.js +++ b/packages/core/__tests__/common/createApp.ios.spec.js @@ -1,6 +1,7 @@ import createApp from '../../src/platform/createApp.ios' import transferOptions from '../../src/core/transferOptions' import Mpx from '../../src/index' +import { error } from '@mpxjs/utils' jest.mock('../../src/core/transferOptions', () => ({ __esModule: true, @@ -20,7 +21,8 @@ jest.mock('@mpxjs/utils', () => ({ spreadProp: (options) => options, getFocusedNavigation: jest.fn(), hasOwn: (target, key) => Object.prototype.hasOwnProperty.call(target, key), - callWithErrorHandling: jest.fn() + callWithErrorHandling: jest.fn(), + error: jest.fn() }), { virtual: true }) jest.mock('../../src/convertor/mergeLifecycle', () => ({ @@ -125,18 +127,21 @@ describe('RN createApp initial params', () => { }) }) - it('stores initial params on the initial route state instead of Screen defaults', () => { + it('uses initialState for a non-first Screen instead of Navigator and Screen defaults', () => { const initialParams = { a: 1 } const onLaunch = jest.fn() const onStateChange = jest.fn() + const HomePage = () => null + HomePage.displayName = 'HomePage' const IndexPage = () => null IndexPage.displayName = 'IndexPage' transferOptions.mockReturnValue({ rawOptions: { onLaunch }, currentInject: { moduleId: 'app', - firstPage: 'pages/index', + firstPage: 'pages/home', pagesMap: { + 'pages/home': HomePage, 'pages/index': IndexPage } } @@ -153,7 +158,7 @@ describe('RN createApp initial params', () => { const tree = global.__mpxOptionsMap.app({}) const navigationContainer = tree.props.children[0] const stackNavigator = navigationContainer.props.children[0] - const indexScreen = stackNavigator.props.children[0] + const indexScreen = stackNavigator.props.children[1] expect(navigationContainer.props.initialState).toEqual({ routes: [{ @@ -161,6 +166,11 @@ describe('RN createApp initial params', () => { params: initialParams }] }) + expect(stackNavigator.props.children.map(screen => screen.props.name)).toEqual([ + 'pages/home', + 'pages/index' + ]) + expect(stackNavigator.props).not.toHaveProperty('initialRouteName') expect(indexScreen.props).not.toHaveProperty('initialParams') expect(global.__mpxInitialRouteKey).toBeUndefined() expect(global.__mpxInitialRunParams).toBeUndefined() @@ -187,4 +197,41 @@ describe('RN createApp initial params', () => { isLaunch: true })) }) + + it('reports and falls back when the initial route is not registered', () => { + const initialParams = { fromMissing: true } + const IndexPage = () => null + IndexPage.displayName = 'IndexPage' + transferOptions.mockReturnValue({ + rawOptions: {}, + currentInject: { + moduleId: 'app', + firstPage: 'pages/index', + pagesMap: { + 'pages/index': IndexPage + } + } + }) + Mpx.config.rnConfig = { + parseAppProps: () => ({ + initialRouteName: 'pages/missing', + initialParams + }) + } + + createApp({}) + const tree = global.__mpxOptionsMap.app({}) + const navigationContainer = tree.props.children[0] + const stackNavigator = navigationContainer.props.children[0] + + expect(error).toHaveBeenCalledTimes(1) + expect(error).toHaveBeenCalledWith('Initial page [pages/missing] is not registered, fallback to [pages/index].') + expect(navigationContainer.props.initialState).toEqual({ + routes: [{ + name: 'pages/index', + params: {} + }] + }) + expect(stackNavigator.props).not.toHaveProperty('initialRouteName') + }) }) diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index ebf90537aa..a478f2b7a1 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -1,6 +1,6 @@ import transferOptions from '../core/transferOptions' import builtInKeysMap from './patch/builtInKeysMap' -import { makeMap, spreadProp, getFocusedNavigation, hasOwn, callWithErrorHandling } from '@mpxjs/utils' +import { makeMap, spreadProp, getFocusedNavigation, hasOwn, callWithErrorHandling, error } from '@mpxjs/utils' import { mergeLifecycle } from '../convertor/mergeLifecycle' import { LIFECYCLE } from '../platform/patch/lifecycle/index' import Mpx from '../index' @@ -204,7 +204,13 @@ export default function createApp (options) { } }, []) - const { initialRouteName, initialParams } = initialRouteRef.current + const initialRoute = initialRouteRef.current + if (!hasOwn(pagesMap, initialRoute.initialRouteName)) { + error(`Initial page [${initialRoute.initialRouteName}] is not registered, fallback to [${firstPage}].`) + initialRoute.initialRouteName = firstPage + initialRoute.initialParams = {} + } + const { initialRouteName, initialParams } = initialRoute const initialState = { routes: [{ name: initialRouteName, @@ -228,7 +234,6 @@ export default function createApp (options) { }, createElement(Stack.Navigator, { - initialRouteName, screenOptions: navScreenOpts }, ...getPageScreens() From 510bd4dc26687f825ff88a4da16adea62a25cfc8 Mon Sep 17 00:00:00 2001 From: xuegan Date: Wed, 9 Sep 2026 16:03:30 +0800 Subject: [PATCH 05/13] fix(core): clarify RN initial route fallback message --- packages/core/__tests__/common/createApp.ios.spec.js | 2 +- packages/core/src/platform/createApp.ios.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/__tests__/common/createApp.ios.spec.js b/packages/core/__tests__/common/createApp.ios.spec.js index e64091caac..852f2fecba 100644 --- a/packages/core/__tests__/common/createApp.ios.spec.js +++ b/packages/core/__tests__/common/createApp.ios.spec.js @@ -225,7 +225,7 @@ describe('RN createApp initial params', () => { const stackNavigator = navigationContainer.props.children[0] expect(error).toHaveBeenCalledTimes(1) - expect(error).toHaveBeenCalledWith('Initial page [pages/missing] is not registered, fallback to [pages/index].') + expect(error).toHaveBeenCalledWith('The initial page [pages/missing] is not registered in the application. Mpx will fall back to the first page [pages/index].') expect(navigationContainer.props.initialState).toEqual({ routes: [{ name: 'pages/index', diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index a478f2b7a1..6cf00d103c 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -206,7 +206,7 @@ export default function createApp (options) { const initialRoute = initialRouteRef.current if (!hasOwn(pagesMap, initialRoute.initialRouteName)) { - error(`Initial page [${initialRoute.initialRouteName}] is not registered, fallback to [${firstPage}].`) + error(`The initial page [${initialRoute.initialRouteName}] is not registered in the application. Mpx will fall back to the first page [${firstPage}].`) initialRoute.initialRouteName = firstPage initialRoute.initialParams = {} } From 01397ab3b3646c6660f6086654cc13030acde897 Mon Sep 17 00:00:00 2001 From: xuegan Date: Thu, 10 Sep 2026 19:11:54 +0800 Subject: [PATCH 06/13] refactor(core): validate RN initial route during app props parsing --- packages/core/src/platform/createApp.ios.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index 6cf00d103c..eb3099c112 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -168,6 +168,13 @@ export default function createApp (options) { initialRouteRef.current.initialRouteName = initialRouteName || initialRouteRef.current.initialRouteName initialRouteRef.current.initialParams = initialParams || initialRouteRef.current.initialParams + const initialRoute = initialRouteRef.current + if (!hasOwn(pagesMap, initialRoute.initialRouteName)) { + error(`The initial page [${initialRoute.initialRouteName}] is not registered in the application. Mpx will fall back to the first page [${firstPage}].`) + initialRoute.initialRouteName = firstPage + initialRoute.initialParams = {} + } + global.__mpxAppOnLaunch = (navigation) => { const state = navigation.getState() Mpx.config.rnConfig.onStateChange?.(state) @@ -204,13 +211,7 @@ export default function createApp (options) { } }, []) - const initialRoute = initialRouteRef.current - if (!hasOwn(pagesMap, initialRoute.initialRouteName)) { - error(`The initial page [${initialRoute.initialRouteName}] is not registered in the application. Mpx will fall back to the first page [${firstPage}].`) - initialRoute.initialRouteName = firstPage - initialRoute.initialParams = {} - } - const { initialRouteName, initialParams } = initialRoute + const { initialRouteName, initialParams } = initialRouteRef.current const initialState = { routes: [{ name: initialRouteName, From 34ebc050d4406e2076ccd2fad2258b4ead9ead17 Mon Sep 17 00:00:00 2001 From: xuegan Date: Thu, 10 Sep 2026 20:46:45 +0800 Subject: [PATCH 07/13] test(core): simplify RN initial params tests --- .../__tests__/common/createApp.ios.spec.js | 178 +++--------------- 1 file changed, 29 insertions(+), 149 deletions(-) diff --git a/packages/core/__tests__/common/createApp.ios.spec.js b/packages/core/__tests__/common/createApp.ios.spec.js index 852f2fecba..fe6a8e125b 100644 --- a/packages/core/__tests__/common/createApp.ios.spec.js +++ b/packages/core/__tests__/common/createApp.ios.spec.js @@ -1,48 +1,17 @@ -import createApp from '../../src/platform/createApp.ios' -import transferOptions from '../../src/core/transferOptions' -import Mpx from '../../src/index' -import { error } from '@mpxjs/utils' - -jest.mock('../../src/core/transferOptions', () => ({ - __esModule: true, - default: jest.fn() -})) +global.__mpx_mode__ = 'ios' -jest.mock('../../src/platform/patch/builtInKeysMap', () => ({ - __esModule: true, - default: {} -})) +const createApp = require('../../src/platform/createApp.ios').default +const transferOptions = require('../../src/core/transferOptions') +const Mpx = require('../../src/index') +const { error } = require('@mpxjs/utils') -jest.mock('@mpxjs/utils', () => ({ - makeMap: (keys) => keys.reduce((map, key) => { - map[key] = true - return map - }, {}), - spreadProp: (options) => options, - getFocusedNavigation: jest.fn(), - hasOwn: (target, key) => Object.prototype.hasOwnProperty.call(target, key), - callWithErrorHandling: jest.fn(), - error: jest.fn() -}), { virtual: true }) +jest.mock('../../src/core/transferOptions', () => jest.fn()) -jest.mock('../../src/convertor/mergeLifecycle', () => ({ - mergeLifecycle: () => ({ - app: ['onLaunch', 'onShow', 'onHide', 'onError', 'onUnhandledRejection'] - }) -})) - -jest.mock('../../src/platform/patch/lifecycle/index', () => ({ - LIFECYCLE: {} +jest.mock('@mpxjs/utils', () => Object.assign({}, jest.requireActual('@mpxjs/utils'), { + error: jest.fn() })) -jest.mock('../../src/index', () => { - const Mpx = function () {} - Mpx.config = { rnConfig: {} } - return { - __esModule: true, - default: Mpx - } -}) +jest.mock('../../src/index', () => ({ config: { rnConfig: {} }, prototype: {} })) jest.mock('../../src/observer/reactive', () => ({ reactive: (value) => value @@ -62,11 +31,7 @@ jest.mock('react', () => ({ useEffect: jest.fn() }), { virtual: true }) -jest.mock('react-native', () => ({ - AppState: { - addEventListener: jest.fn() - } -}), { virtual: true }) +jest.mock('react-native', () => ({}), { virtual: true }) jest.mock('../../src/platform/export/inject', () => ({ initAppProvides: jest.fn() @@ -85,78 +50,36 @@ jest.mock('../../src/platform/env/navigationHelper', () => ({ jest.mock('@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-nav', () => 'MpxNav', { virtual: true }) describe('RN createApp initial params', () => { - const globalKeys = [ - '__mpxAppCbs', - '__mpxAppHotLaunched', - '__mpxAppLaunched', - '__mpxAppOnLaunch', - '__mpxEnterOptions', - '__mpxInitialRouteKey', - '__mpxInitialRunParams', - '__mpxLaunchOptions', - '__mpxOptionsMap', - '__mpxPageConfig', - '__mpxPageConfigsMap', - '__mpxPagesMap', - '__navigationHelper', - 'getApp', - 'getCurrentPages', - 'setAppHide', - 'setAppShow' - ] + const onLaunch = jest.fn() beforeEach(() => { - global.__mpxAppCbs = { - show: [], - hide: [], - error: [], - rejection: [] - } + jest.clearAllMocks() + global.__mpxOptionsMap = {} global.__mpxPageConfig = {} - global.__mpxPageConfigsMap = { - 'pages/index': {} - } - global.__navigationHelper = {} + global.__mpxPageConfigsMap = {} Mpx.config.rnConfig = {} - }) - - afterEach(() => { - jest.clearAllMocks() - globalKeys.forEach((key) => { - delete global[key] - }) - }) - - it('uses initialState for a non-first Screen instead of Navigator and Screen defaults', () => { - const initialParams = { a: 1 } - const onLaunch = jest.fn() - const onStateChange = jest.fn() - const HomePage = () => null - HomePage.displayName = 'HomePage' - const IndexPage = () => null - IndexPage.displayName = 'IndexPage' transferOptions.mockReturnValue({ rawOptions: { onLaunch }, currentInject: { moduleId: 'app', firstPage: 'pages/home', pagesMap: { - 'pages/home': HomePage, - 'pages/index': IndexPage + 'pages/home': () => null, + 'pages/index': () => null } } }) - Mpx.config.rnConfig = { - parseAppProps: () => ({ - initialRouteName: 'pages/index', - initialParams - }), - onStateChange - } + }) + function renderApp (initialRouteName, initialParams) { + Mpx.config.rnConfig.parseAppProps = () => ({ initialRouteName, initialParams }) createApp({}) - const tree = global.__mpxOptionsMap.app({}) - const navigationContainer = tree.props.children[0] + return global.__mpxOptionsMap.app({}).props.children[0] + } + + it('uses initialState for a non-first Screen instead of Navigator and Screen defaults', () => { + const initialParams = { a: 1 } + const navigationContainer = renderApp('pages/index', initialParams) const stackNavigator = navigationContainer.props.children[0] const indexScreen = stackNavigator.props.children[1] @@ -166,31 +89,12 @@ describe('RN createApp initial params', () => { params: initialParams }] }) - expect(stackNavigator.props.children.map(screen => screen.props.name)).toEqual([ - 'pages/home', - 'pages/index' - ]) expect(stackNavigator.props).not.toHaveProperty('initialRouteName') expect(indexScreen.props).not.toHaveProperty('initialParams') - expect(global.__mpxInitialRouteKey).toBeUndefined() - expect(global.__mpxInitialRunParams).toBeUndefined() - - const resetRouteParams = Object.assign({}, indexScreen.props.initialParams, { b: 2 }) - expect(resetRouteParams).toEqual({ b: 2 }) - const state = { - index: 0, - routes: [{ - key: 'pages/index-1', - name: 'pages/index', - params: initialParams - }] - } global.__mpxAppOnLaunch({ - getState: () => state + getState: () => Object.assign({ index: 0 }, navigationContainer.props.initialState) }) - - expect(onStateChange).toHaveBeenCalledWith(state) expect(onLaunch).toHaveBeenCalledWith(expect.objectContaining({ path: 'pages/index', query: initialParams, @@ -199,39 +103,15 @@ describe('RN createApp initial params', () => { }) it('reports and falls back when the initial route is not registered', () => { - const initialParams = { fromMissing: true } - const IndexPage = () => null - IndexPage.displayName = 'IndexPage' - transferOptions.mockReturnValue({ - rawOptions: {}, - currentInject: { - moduleId: 'app', - firstPage: 'pages/index', - pagesMap: { - 'pages/index': IndexPage - } - } - }) - Mpx.config.rnConfig = { - parseAppProps: () => ({ - initialRouteName: 'pages/missing', - initialParams - }) - } - - createApp({}) - const tree = global.__mpxOptionsMap.app({}) - const navigationContainer = tree.props.children[0] - const stackNavigator = navigationContainer.props.children[0] + const navigationContainer = renderApp('pages/missing', { fromMissing: true }) expect(error).toHaveBeenCalledTimes(1) - expect(error).toHaveBeenCalledWith('The initial page [pages/missing] is not registered in the application. Mpx will fall back to the first page [pages/index].') + expect(error).toHaveBeenCalledWith('The initial page [pages/missing] is not registered in the application. Mpx will fall back to the first page [pages/home].') expect(navigationContainer.props.initialState).toEqual({ routes: [{ - name: 'pages/index', + name: 'pages/home', params: {} }] }) - expect(stackNavigator.props).not.toHaveProperty('initialRouteName') }) }) From 95d29693a3b86c727dcf5fa76db4afbf32158303 Mon Sep 17 00:00:00 2001 From: xuegan Date: Thu, 10 Sep 2026 21:10:09 +0800 Subject: [PATCH 08/13] fix(core): validate only explicit RN initial routes --- .../core/__tests__/common/createApp.ios.spec.js | 17 +++++++++++------ packages/core/src/platform/createApp.ios.js | 14 ++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/core/__tests__/common/createApp.ios.spec.js b/packages/core/__tests__/common/createApp.ios.spec.js index fe6a8e125b..1ac13cda06 100644 --- a/packages/core/__tests__/common/createApp.ios.spec.js +++ b/packages/core/__tests__/common/createApp.ios.spec.js @@ -77,26 +77,31 @@ describe('RN createApp initial params', () => { return global.__mpxOptionsMap.app({}).props.children[0] } - it('uses initialState for a non-first Screen instead of Navigator and Screen defaults', () => { + it.each([ + ['pages/index', 'pages/index'], + [undefined, 'pages/home'] + ])('uses initialState when initialRouteName is %s', (initialRouteName, expectedRouteName) => { const initialParams = { a: 1 } - const navigationContainer = renderApp('pages/index', initialParams) + const navigationContainer = renderApp(initialRouteName, initialParams) const stackNavigator = navigationContainer.props.children[0] - const indexScreen = stackNavigator.props.children[1] + expect(error).not.toHaveBeenCalled() expect(navigationContainer.props.initialState).toEqual({ routes: [{ - name: 'pages/index', + name: expectedRouteName, params: initialParams }] }) expect(stackNavigator.props).not.toHaveProperty('initialRouteName') - expect(indexScreen.props).not.toHaveProperty('initialParams') + stackNavigator.props.children.forEach(screen => { + expect(screen.props).not.toHaveProperty('initialParams') + }) global.__mpxAppOnLaunch({ getState: () => Object.assign({ index: 0 }, navigationContainer.props.initialState) }) expect(onLaunch).toHaveBeenCalledWith(expect.objectContaining({ - path: 'pages/index', + path: expectedRouteName, query: initialParams, isLaunch: true })) diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index eb3099c112..8312978ab5 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -164,17 +164,15 @@ export default function createApp (options) { firstRef.current = false } if (!global.__mpxAppHotLaunched) { - const { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {} + let { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {} + if (initialRouteName && !hasOwn(pagesMap, initialRouteName)) { + error(`The initial page [${initialRouteName}] is not registered in the application. Mpx will fall back to the first page [${firstPage}].`) + initialRouteName = firstPage + initialParams = {} + } initialRouteRef.current.initialRouteName = initialRouteName || initialRouteRef.current.initialRouteName initialRouteRef.current.initialParams = initialParams || initialRouteRef.current.initialParams - const initialRoute = initialRouteRef.current - if (!hasOwn(pagesMap, initialRoute.initialRouteName)) { - error(`The initial page [${initialRoute.initialRouteName}] is not registered in the application. Mpx will fall back to the first page [${firstPage}].`) - initialRoute.initialRouteName = firstPage - initialRoute.initialParams = {} - } - global.__mpxAppOnLaunch = (navigation) => { const state = navigation.getState() Mpx.config.rnConfig.onStateChange?.(state) From bd8c1e8645f0c51957ae97301b3e1cdc8c42e2b6 Mon Sep 17 00:00:00 2001 From: xuegan Date: Thu, 10 Sep 2026 21:12:33 +0800 Subject: [PATCH 09/13] fix(core): discard invalid RN initial route configs --- .../skills/mpx2rn/references/rn-script-reference.md | 2 +- docs-vitepress/guide/rn/application-api.md | 2 +- packages/core/__tests__/common/createApp.ios.spec.js | 4 ++-- packages/core/src/platform/createApp.ios.js | 11 +++++------ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 3d869c6b46..223b76ec47 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -717,7 +717,7 @@ Mpx.config.rnConfig = { | 配置项 | 说明 | | --- | --- | | `projectName` | 由构建注入到 RN 入口,与 `AppRegistry.registerComponent` 相关(偏构建侧)。 | -| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由;`initialParams` 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`。后续创建的同路径页面实例不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准。`initialRouteName` 未注册时,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),回退至应用首页,并丢弃该错误路由的 `initialParams`。 | +| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由;`initialParams` 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`。后续创建的同路径页面实例不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准。传入的 `initialRouteName` 未注册时,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),丢弃本次返回的 `initialRouteName` 和 `initialParams`,保留已有初始配置(默认是应用首页和空参数)。 | | `onStateChange` | 导航 state 变化时回调。 | | `disablePageTransition` | 为 `true` 时禁用 RN 页面转场动画,框架内部映射为 `animation: "none"`。 | | `disableAppStateListener` | 为 `true` 时不注册 `AppState` 监听(避免与宿主 App 重复)。 | diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index dde6607ef0..450bfca15d 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -416,7 +416,7 @@ createComponent({ 用于获取初始路由配置的函数,参数为 RN 根组件接收到的参数 -- initialRouteName: 首页路径,例如 pages/index。若该路径未注册,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),回退至应用首页,并丢弃该错误路由的 `initialParams` +- initialRouteName: 首页路径,例如 pages/index。若传入的路径未注册,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),丢弃本次返回的 `initialRouteName` 和 `initialParams`,保留已有初始配置(默认是应用首页和空参数) - initialParams: 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`,例如 \{ a: 1 \}。后续通过路由 API 再次打开同路径页面时不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准 在需要将 RN 应用嵌入到现有的 NA 应用中时,NA 可能会向 RN 的根组件传递 props,此时可在 parseAppProps 中接受 props 并进行处理和透传到页面 diff --git a/packages/core/__tests__/common/createApp.ios.spec.js b/packages/core/__tests__/common/createApp.ios.spec.js index 1ac13cda06..d8d863e89c 100644 --- a/packages/core/__tests__/common/createApp.ios.spec.js +++ b/packages/core/__tests__/common/createApp.ios.spec.js @@ -107,11 +107,11 @@ describe('RN createApp initial params', () => { })) }) - it('reports and falls back when the initial route is not registered', () => { + it('reports and ignores an unregistered initial route and its params', () => { const navigationContainer = renderApp('pages/missing', { fromMissing: true }) expect(error).toHaveBeenCalledTimes(1) - expect(error).toHaveBeenCalledWith('The initial page [pages/missing] is not registered in the application. Mpx will fall back to the first page [pages/home].') + expect(error).toHaveBeenCalledWith('The initial page [pages/missing] is not registered in the application. Mpx will ignore this initial route configuration.') expect(navigationContainer.props.initialState).toEqual({ routes: [{ name: 'pages/home', diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index 8312978ab5..f220dc43bf 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -164,14 +164,13 @@ export default function createApp (options) { firstRef.current = false } if (!global.__mpxAppHotLaunched) { - let { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {} + const { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {} if (initialRouteName && !hasOwn(pagesMap, initialRouteName)) { - error(`The initial page [${initialRouteName}] is not registered in the application. Mpx will fall back to the first page [${firstPage}].`) - initialRouteName = firstPage - initialParams = {} + error(`The initial page [${initialRouteName}] is not registered in the application. Mpx will ignore this initial route configuration.`) + } else { + if (initialRouteName) initialRouteRef.current.initialRouteName = initialRouteName + if (initialParams) initialRouteRef.current.initialParams = initialParams } - initialRouteRef.current.initialRouteName = initialRouteName || initialRouteRef.current.initialRouteName - initialRouteRef.current.initialParams = initialParams || initialRouteRef.current.initialParams global.__mpxAppOnLaunch = (navigation) => { const state = navigation.getState() From bc087d69a14249a2f32fa906eba98977670a1e27 Mon Sep 17 00:00:00 2001 From: xuegan Date: Thu, 10 Sep 2026 21:13:26 +0800 Subject: [PATCH 10/13] docs: revert initial route documentation sync --- .agents/skills/mpx2rn/references/rn-script-reference.md | 2 +- docs-vitepress/guide/rn/application-api.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 223b76ec47..3d869c6b46 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -717,7 +717,7 @@ Mpx.config.rnConfig = { | 配置项 | 说明 | | --- | --- | | `projectName` | 由构建注入到 RN 入口,与 `AppRegistry.registerComponent` 相关(偏构建侧)。 | -| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由;`initialParams` 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`。后续创建的同路径页面实例不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准。传入的 `initialRouteName` 未注册时,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),丢弃本次返回的 `initialRouteName` 和 `initialParams`,保留已有初始配置(默认是应用首页和空参数)。 | +| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由;`initialParams` 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`。后续创建的同路径页面实例不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准。`initialRouteName` 未注册时,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),回退至应用首页,并丢弃该错误路由的 `initialParams`。 | | `onStateChange` | 导航 state 变化时回调。 | | `disablePageTransition` | 为 `true` 时禁用 RN 页面转场动画,框架内部映射为 `animation: "none"`。 | | `disableAppStateListener` | 为 `true` 时不注册 `AppState` 监听(避免与宿主 App 重复)。 | diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index 450bfca15d..dde6607ef0 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -416,7 +416,7 @@ createComponent({ 用于获取初始路由配置的函数,参数为 RN 根组件接收到的参数 -- initialRouteName: 首页路径,例如 pages/index。若传入的路径未注册,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),丢弃本次返回的 `initialRouteName` 和 `initialParams`,保留已有初始配置(默认是应用首页和空参数) +- initialRouteName: 首页路径,例如 pages/index。若该路径未注册,Mpx 会通过统一错误处理上报(配置了 `Mpx.config.errorHandler` 时会触发该回调),回退至应用首页,并丢弃该错误路由的 `initialParams` - initialParams: 作为 RN 根组件初始化时首个路由实例的参数,会传入该页面的 `onLoad`,并作为初始化阶段应用 `onLaunch` / `onShow` 的 `query`,例如 \{ a: 1 \}。后续通过路由 API 再次打开同路径页面时不会自动继承;应用再次展示时,`onShow` 参数中的 `query` 以当前路由实例的参数为准 在需要将 RN 应用嵌入到现有的 NA 应用中时,NA 可能会向 RN 的根组件传递 props,此时可在 parseAppProps 中接受 props 并进行处理和透传到页面 From 68771f5bfa8de6193060638be8192423eee6f79d Mon Sep 17 00:00:00 2001 From: xuegan Date: Thu, 10 Sep 2026 21:17:41 +0800 Subject: [PATCH 11/13] style(core): expand initial route assignment conditionals --- packages/core/src/platform/createApp.ios.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index f220dc43bf..32f959c26f 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -168,8 +168,12 @@ export default function createApp (options) { if (initialRouteName && !hasOwn(pagesMap, initialRouteName)) { error(`The initial page [${initialRouteName}] is not registered in the application. Mpx will ignore this initial route configuration.`) } else { - if (initialRouteName) initialRouteRef.current.initialRouteName = initialRouteName - if (initialParams) initialRouteRef.current.initialParams = initialParams + if (initialRouteName) { + initialRouteRef.current.initialRouteName = initialRouteName + } + if (initialParams) { + initialRouteRef.current.initialParams = initialParams + } } global.__mpxAppOnLaunch = (navigation) => { From 1306613a075c0b9b464af0a3fec66de14acd9f56 Mon Sep 17 00:00:00 2001 From: xuegan Date: Thu, 10 Sep 2026 21:21:03 +0800 Subject: [PATCH 12/13] refactor(core): handle valid initial routes first --- packages/core/src/platform/createApp.ios.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index 32f959c26f..21e2d9412f 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -165,15 +165,15 @@ export default function createApp (options) { } if (!global.__mpxAppHotLaunched) { const { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {} - if (initialRouteName && !hasOwn(pagesMap, initialRouteName)) { - error(`The initial page [${initialRouteName}] is not registered in the application. Mpx will ignore this initial route configuration.`) - } else { + if (!initialRouteName || hasOwn(pagesMap, initialRouteName)) { if (initialRouteName) { initialRouteRef.current.initialRouteName = initialRouteName } if (initialParams) { initialRouteRef.current.initialParams = initialParams } + } else { + error(`The initial page [${initialRouteName}] is not registered in the application. Mpx will ignore this initial route configuration.`) } global.__mpxAppOnLaunch = (navigation) => { From 3a815d7017485eca98141f52ed4ee6df983ca9d6 Mon Sep 17 00:00:00 2001 From: xuegan Date: Thu, 10 Sep 2026 21:24:32 +0800 Subject: [PATCH 13/13] refactor(core): restore invalid initial route guard first --- packages/core/src/platform/createApp.ios.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index 21e2d9412f..32f959c26f 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -165,15 +165,15 @@ export default function createApp (options) { } if (!global.__mpxAppHotLaunched) { const { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {} - if (!initialRouteName || hasOwn(pagesMap, initialRouteName)) { + if (initialRouteName && !hasOwn(pagesMap, initialRouteName)) { + error(`The initial page [${initialRouteName}] is not registered in the application. Mpx will ignore this initial route configuration.`) + } else { if (initialRouteName) { initialRouteRef.current.initialRouteName = initialRouteName } if (initialParams) { initialRouteRef.current.initialParams = initialParams } - } else { - error(`The initial page [${initialRouteName}] is not registered in the application. Mpx will ignore this initial route configuration.`) } global.__mpxAppOnLaunch = (navigation) => {