@mpxjs/core
Version:
mpx runtime core
277 lines (261 loc) • 9.78 kB
JavaScript
import transferOptions from '../core/transferOptions'
import builtInKeysMap from './patch/builtInKeysMap'
import { makeMap, spreadProp, getFocusedNavigation, hasOwn } from '@mpxjs/utils'
import { mergeLifecycle } from '../convertor/mergeLifecycle'
import { LIFECYCLE } from '../platform/patch/lifecycle/index'
import Mpx from '../index'
import { reactive } from '../observer/reactive'
import { watch } from '../observer/watch'
import { createElement, memo, useRef, useEffect } from 'react'
import * as ReactNative from 'react-native'
import { initAppProvides } from './export/inject'
import { NavigationContainer, createNativeStackNavigator, SafeAreaProvider, GestureHandlerRootView } from './env/navigationHelper'
import { innerNav } from './env/nav'
const appHooksMap = makeMap(mergeLifecycle(LIFECYCLE).app)
function getPageSize (window = ReactNative.Dimensions.get('window')) {
return window.width + 'x' + window.height
}
function filterOptions (options, appData) {
const newOptions = {}
Object.keys(options).forEach(key => {
if (builtInKeysMap[key]) {
return
}
if (!appHooksMap[key]) {
appData[key] = options[key]
} else {
newOptions[key] = options[key]
}
})
return newOptions
}
export default function createApp (options) {
const appData = {}
// app选项目前不需要进行转换
const { rawOptions, currentInject } = transferOptions(options, 'app', false)
initAppProvides(rawOptions.provide, rawOptions)
const defaultOptions = filterOptions(spreadProp(rawOptions, 'methods'), appData)
// 在页面script执行前填充getApp()
global.getApp = function () {
return appData
}
// 模拟小程序appInstance在热启动时不会重新创建的行为,在外部创建跟随js context的appInstance
const appInstance = Object.assign({}, appData, Mpx.prototype)
defaultOptions.onShow && global.__mpxAppCbs.show.push(defaultOptions.onShow.bind(appInstance))
defaultOptions.onHide && global.__mpxAppCbs.hide.push(defaultOptions.onHide.bind(appInstance))
defaultOptions.onError && global.__mpxAppCbs.error.push(defaultOptions.onError.bind(appInstance))
defaultOptions.onUnhandledRejection && global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(appInstance))
defaultOptions.onAppInit && defaultOptions.onAppInit()
const pagesMap = currentInject.pagesMap || {}
const firstPage = currentInject.firstPage
const Stack = createNativeStackNavigator()
const getPageScreens = (initialRouteName, initialParams) => {
return Object.entries(pagesMap).map(([key, item]) => {
const pageConfig = Object.assign({}, global.__mpxPageConfig, global.__mpxPageConfigsMap[key])
const headerLayout = ({ navigation, children }) => {
return createElement(GestureHandlerRootView,
{
style: {
flex: 1
}
},
createElement(innerNav, {
pageConfig: pageConfig,
navigation
}),
children
)
}
const getComponent = () => {
return item.displayName ? item : item()
}
if (key === initialRouteName) {
return createElement(Stack.Screen, {
name: key,
getComponent,
initialParams,
layout: headerLayout
})
}
return createElement(Stack.Screen, {
name: key,
getComponent,
layout: headerLayout
})
})
}
global.__mpxOptionsMap = global.__mpxOptionsMap || {}
const onStateChange = (state) => {
Mpx.config.rnConfig.onStateChange?.(state)
if (global.__navigationHelper.lastSuccessCallback) {
global.__navigationHelper.lastSuccessCallback()
global.__navigationHelper.lastSuccessCallback = null
}
}
const onUnhandledAction = (action) => {
const payload = action.payload
const message = `The action '${action.type}'${payload ? ` with payload ${JSON.stringify(action.payload)}` : ''} was not handled by any navigator.`
if (global.__navigationHelper.lastFailCallback) {
global.__navigationHelper.lastFailCallback(message)
global.__navigationHelper.lastFailCallback = null
}
}
const appState = reactive({ state: '' })
// TODO hideReason 暂未完全模拟
// 0用户退出小程序
// 1进入其他小程序
// 2打开原生功能页
// 3其他
watch(() => appState.state, (value) => {
if (value === 'show') {
let options = appState.showOptions
delete appState.showOptions
if (!options) {
const navigation = getFocusedNavigation()
if (navigation) {
const state = navigation.getState()
const current = state.routes[state.index]
options = {
path: current.name,
query: current.params,
scene: 0,
shareTicket: '',
referrerInfo: {}
}
} else {
options = {}
}
}
global.__mpxAppCbs.show.forEach((cb) => {
cb(options)
})
} else if (value === 'hide') {
const reason = appState.hideReason ?? 3
delete appState.hideReason
global.__mpxAppCbs.hide.forEach((cb) => {
cb({
reason
})
})
}
}, { sync: true })
const onAppStateChange = (currentState) => {
const navigation = getFocusedNavigation()
if (currentState === 'active') {
appState.state = 'show'
if (navigation && hasOwn(global.__mpxPageStatusMap, navigation.pageId)) {
global.__mpxPageStatusMap[navigation.pageId] = 'show'
}
} else if (currentState === 'inactive' || currentState === 'background') {
appState.hideReason = 3
appState.state = 'hide'
if (navigation && hasOwn(global.__mpxPageStatusMap, navigation.pageId)) {
global.__mpxPageStatusMap[navigation.pageId] = 'hide'
}
}
}
global.__mpxAppLaunched = false
global.__mpxOptionsMap[currentInject.moduleId] = memo((props) => {
const firstRef = useRef(true)
const initialRouteRef = useRef({
initialRouteName: firstPage,
initialParams: {}
})
if (firstRef.current) {
// 热启动情况下,app会被销毁重建,将__mpxAppHotLaunched重置保障路由等初始化逻辑正确执行
global.__mpxAppHotLaunched = false
// 热启动情况下重置__mpxPagesMap避免页面销毁函数未及时执行时错误地引用到之前的navigation
global.__mpxPagesMap = {}
firstRef.current = false
}
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
global.__mpxAppOnLaunch = (navigation) => {
const state = navigation.getState()
Mpx.config.rnConfig.onStateChange?.(state)
const current = state.routes[state.index]
const options = {
path: current.name,
query: current.params,
scene: 0,
shareTicket: '',
referrerInfo: {},
isLaunch: true
}
global.__mpxEnterOptions = options
if (!global.__mpxAppLaunched) {
global.__mpxLaunchOptions = options
defaultOptions.onLaunch && defaultOptions.onLaunch.call(appInstance, options)
}
appState.showOptions = options
appState.state = 'show'
global.__mpxAppLaunched = true
global.__mpxAppHotLaunched = true
}
}
useEffect(() => {
const changeSubscription = ReactNative.AppState.addEventListener('change', (state) => {
// 外层可能会异常设置此配置,因此加载监听函数内部
if (Mpx.config.rnConfig.disableAppStateListener) return
onAppStateChange(state)
})
let count = 0
let lastPageSize = getPageSize()
const resizeSubScription = ReactNative.Dimensions.addEventListener('change', ({ window }) => {
const pageSize = getPageSize(window)
if (pageSize === lastPageSize) return
lastPageSize = pageSize
const navigation = getFocusedNavigation()
if (navigation && hasOwn(global.__mpxPageStatusMap, navigation.pageId)) {
global.__mpxPageStatusMap[navigation.pageId] = `resize${count++}`
}
})
return () => {
appState.hideReason = 0
appState.state = 'hide'
changeSubscription && changeSubscription.remove()
resizeSubScription && resizeSubScription.remove()
}
}, [])
const { initialRouteName, initialParams } = initialRouteRef.current
const navScreenOpts = {
headerShown: false,
statusBarTranslucent: true,
statusBarBackgroundColor: 'transparent'
}
return createElement(SafeAreaProvider,
null,
createElement(NavigationContainer,
{
onStateChange,
onUnhandledAction
},
createElement(Stack.Navigator,
{
initialRouteName,
screenOptions: navScreenOpts
},
...getPageScreens(initialRouteName, initialParams)
)
)
)
})
global.getCurrentPages = function () {
const navigation = getFocusedNavigation()
if (navigation) {
return navigation.getState().routes.map((route) => {
return global.__mpxPagesMap[route.key] && global.__mpxPagesMap[route.key][0]
}).filter(item => item)
}
return []
}
// 用于外层业务用来设置App的展示情况
global.setAppShow = function () {
onAppStateChange('active')
}
global.setAppHide = function () {
onAppStateChange('inactive')
}
}