Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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