Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/mpx2rn/references/rn-script-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
| `pageLifetimes.show` | 组件 | 所在页面展示或重新获得焦点时触发,与页面 `onShow` 时机对齐。 |
| `pageLifetimes.hide` | 组件 | 所在页面隐藏或失焦时触发,与页面 `onHide` 时机对齐。 |
| `pageLifetimes.resize` | 组件 | 所在页面可视区域尺寸变化时触发,与页面 `onResize` 时机对齐。 |
| `onLoad` | 页面 | 页面创建后调用,**两个参数** `(rawQuery, decodedQuery)`。 |
| `onLoad` | 页面 | 页面创建后调用,**两个参数** `(rawQuery, decodedQuery)`;RN 根组件初始化时,首个页面实例会收到 `parseAppProps` 返回的 `initialParams`,后续创建的同路径页面实例不会自动继承。 |
| `created` | 组件 | 组件实例刚创建,RN 由 `MpxProxy` 在实例建立阶段调度,此时不宜依赖完整视图。 |
| `attached` | 组件 | 组件进入节点树,RN 对齐为挂载流程中的对应阶段,详见 `docs-vitepress/guide/basic/lifecycle.md` 映射表。 |
| `ready` | 组件 | 组件布局完成、可与视图交互,RN 对应 React 挂载后的就绪时机,与页面 `onReady` 同属一套内置映射。 |
Expand Down Expand Up @@ -717,7 +717,7 @@ Mpx.config.rnConfig = {
| 配置项 | 说明 |
| --- | --- |
| `projectName` | 由构建注入到 RN 入口,与 `AppRegistry.registerComponent` 相关(偏构建侧)。 |
| `parseAppProps` | `(props) => { initialRouteName?, initialParams? }`,解析外层传入 App 根组件的初始路由。 |
| `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 重复)。 |
Expand Down
4 changes: 2 additions & 2 deletions docs-vitepress/guide/rn/application-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,8 @@ createComponent({

用于获取初始路由配置的函数,参数为 RN 根组件接收到的参数

- initialRouteName: 首页路径,例如 pages/index
- initialParams: 将作为 首页onLoad 与 应用onLaunch 的参数,例如 \{ a: 1 \}
- 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 并进行处理和透传到页面

Expand Down
122 changes: 122 additions & 0 deletions packages/core/__tests__/common/createApp.ios.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
global.__mpx_mode__ = 'ios'

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('../../src/core/transferOptions', () => jest.fn())

jest.mock('@mpxjs/utils', () => Object.assign({}, jest.requireActual('@mpxjs/utils'), {
error: jest.fn()
}))

jest.mock('../../src/index', () => ({ config: { rnConfig: {} }, prototype: {} }))

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', () => ({}), { 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 onLaunch = jest.fn()

beforeEach(() => {
jest.clearAllMocks()
global.__mpxOptionsMap = {}
global.__mpxPageConfig = {}
global.__mpxPageConfigsMap = {}
Mpx.config.rnConfig = {}
transferOptions.mockReturnValue({
rawOptions: { onLaunch },
currentInject: {
moduleId: 'app',
firstPage: 'pages/home',
pagesMap: {
'pages/home': () => null,
'pages/index': () => null
}
}
})
})

function renderApp (initialRouteName, initialParams) {
Mpx.config.rnConfig.parseAppProps = () => ({ initialRouteName, initialParams })
createApp({})
return global.__mpxOptionsMap.app({}).props.children[0]
}

it.each([
['pages/index', 'pages/index'],
[undefined, 'pages/home']
])('uses initialState when initialRouteName is %s', (initialRouteName, expectedRouteName) => {
const initialParams = { a: 1 }
const navigationContainer = renderApp(initialRouteName, initialParams)
const stackNavigator = navigationContainer.props.children[0]

expect(error).not.toHaveBeenCalled()
expect(navigationContainer.props.initialState).toEqual({
routes: [{
name: expectedRouteName,
params: initialParams
}]
})
expect(stackNavigator.props).not.toHaveProperty('initialRouteName')
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: expectedRouteName,
query: initialParams,
isLaunch: true
}))
})

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 ignore this initial route configuration.')
expect(navigationContainer.props.initialState).toEqual({
routes: [{
name: 'pages/home',
params: {}
}]
})
})
})
6 changes: 5 additions & 1 deletion packages/core/jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
"transform": {
"^.+\\.(js|jsx)?$": ["babel-jest", { "rootMode": "upward" }]
},
"testEnvironment": "jsdom"
"testEnvironment": "jsdom",
"globals": {
"__mpx_perf__": false,
"__mpx_perf_framework__": false
}
}
34 changes: 20 additions & 14 deletions packages/core/src/platform/createApp.ios.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -54,7 +54,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 }) => {
Expand All @@ -74,14 +74,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,
Expand Down Expand Up @@ -173,8 +165,16 @@ export default function createApp (options) {
}
if (!global.__mpxAppHotLaunched) {
const { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {}
initialRouteRef.current.initialRouteName = initialRouteName || initialRouteRef.current.initialRouteName
initialRouteRef.current.initialParams = initialParams || initialRouteRef.current.initialParams
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
}
}

global.__mpxAppOnLaunch = (navigation) => {
const state = navigation.getState()
Expand Down Expand Up @@ -213,6 +213,12 @@ export default function createApp (options) {
}, [])

const { initialRouteName, initialParams } = initialRouteRef.current
const initialState = {
routes: [{
name: initialRouteName,
params: initialParams
}]
}
const navScreenOpts = {
headerShown: false
}
Expand All @@ -224,15 +230,15 @@ export default function createApp (options) {
null,
createElement(NavigationContainer,
{
initialState,
onStateChange,
onUnhandledAction
},
createElement(Stack.Navigator,
{
initialRouteName,
screenOptions: navScreenOpts
},
...getPageScreens(initialRouteName, initialParams)
...getPageScreens()
)
)
)
Expand Down
Loading