import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import { NavigationContainer } from '@react-navigation/native' import { createNativeStackNavigator, NativeStackNavigationOptions } from '@react-navigation/native-stack' import { BackBehavior } from '@react-navigation/routers/src/TabRouter' import { CardStyleInterpolators, createStackNavigator, StackNavigationOptions } from '@react-navigation/stack' import { camelCase } from 'lodash' import React from 'react' import { StyleProp, ViewStyle } from 'react-native' import { navigationRef } from './rootNavigation' import { getCurrentJumpUrl, getTabInitRoute, getTabItemConfig, getTabVisible, handleUrl, setTabConfig } from './utils/index' import BackButton from './view/BackButton' import HeadTitle from './view/HeadTitle' import CustomTabBar from './view/TabBar' import { TabOptions } from './view/TabBarItem' interface WindowConfig { pageOrientation?: 'auto' | 'portrait' | 'landscape' pullRefresh?: 'YES' | 'NO' | boolean allowsBounceVertical?: 'YES' | 'NO' navigationBarBackgroundColor?: string navigationBarTextStyle?: 'white' | 'black' navigationStyle?: 'default' | 'custom' navigationBarTitleText?: string backgroundTextStyle?: 'dark' | 'light' enablePullDownRefresh?: boolean onReachBottomDistance?: number } interface ITabBarItem { pagePath: string // 是 页面路径,必须在 pages 中先定义 text: string // 是 tab 上按钮文字 iconPath?: string // 否 图片路径 selectedIconPath?: string // 否 选中时的图片路径 } interface ITabBar { color?: string selectedColor?: string backgroundColor?: string borderStyle?: 'black' | 'white' list: ITabBarItem[] position?: 'bottom' | 'top' custom?: boolean } interface PageItem { name: string component: any pagePath: string } interface RNConfig { initialRouteName?: string linking?: string[] screenOptions?: StackNavigationOptions | NativeStackNavigationOptions tabOptions?: TabOptions tabBarOptions?: Record tabProps?: { backBehavior?: BackBehavior lazy?: boolean detachInactiveScreens?:boolean sceneContainerStyle?: StyleProp } stackProps?: { keyboardHandlingEnabled?:boolean headerMode?: 'float' | 'screen' detachInactiveScreens?:boolean } useNativeStack?: boolean } export interface RouterConfig { pages: PageItem[] tabBar?: ITabBar window?: WindowConfig linkPrefix?: string[] rnConfig?: RNConfig initParams?:Record // 原生启动传递的参数 initPath?: string // 原生启动时传入的参数路径 entryPagePath?: string // 默认启动路径 } export interface RouterOption{ onReady?: (options)=> void onUnhandledAction?: (options)=> void } export function createRouter (config: RouterConfig, options:RouterOption) { if (config?.tabBar?.list?.length) { return createTabNavigate(config, options) } else { return createStackNavigate(config, options) } } // 初始化路由相关,入口组件,onLaunch,onShow export function getInitOptions (config) { const initRouteName = getInitRouteName(config) const initParams = getInitParams(config, initRouteName) const initPath = config.pages.find(p => p.name === initRouteName)?.pagePath return { path: initPath, query: initParams, } } function getTabNames (config: RouterConfig) { let tabNames: string[] = [] const tabBar = config?.tabBar if (!tabBar) return tabNames tabNames = tabBar.list.map(item => { const pagePath = item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}` return camelCase(pagePath) }) return tabNames } function getPageList (config: RouterConfig) { // pageList 去除tabbar的页面 const tabBar = config.tabBar const pageList = config.pages if (!tabBar) return pageList const tabNames = getTabNames(config) return pageList.filter(item => tabNames.indexOf(item.name) === -1) } function getTabItemOptions (item, index: number) { return { tabBarLabel: getTabItemConfig(index, 'tabBarLabel') || item.text, tabBarBadge: getTabItemConfig(index, 'tabBarBadge'), tabBarVisible: getTabVisible(), tabBarTestID: `tabbar-${index}` } } function getHeaderView (title: string, color: string, props: any) { return } // screen配置的内容 function getStackOptions (config: RouterConfig) { const windowOptions = config.window || {} const title = '' const headColor = windowOptions.navigationBarTextStyle || 'white' const bgColor = windowOptions.navigationBarBackgroundColor || '#000000' const headerTitleAlign: 'left' | 'center' = 'center' const defaultOptions = { title: title, headerShown: windowOptions.navigationStyle !== 'custom', headerTitle: (props) => getHeaderView(title, headColor, props), headerStyle: { backgroundColor: bgColor, shadowOffset: { width: 0, height: 0 }, borderWidth: 0, elevation: 0, shadowOpacity: 1, borderBottomWidth: 0 }, headerTintColor: headColor, cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, cardStyle: { elevation: 1 }, headerBackTitleVisible: false, headerTitleAlign, // eslint-disable-next-line react/display-name headerBackImage: ({ tintColor }) => { return } } const rnConfig = config.rnConfig || {} const screenOptions = rnConfig?.screenOptions || {} return Object.assign({}, defaultOptions, screenOptions) } function getTabItem (config: RouterConfig, tabName: string) { const tabBar = config.tabBar const pageList = config.pages if (!tabBar) return pageList let tabItem: PageItem = { name: '', component: {}, pagePath: '' } pageList.forEach(item => { if (item.name === tabName) { tabItem = item } }) return tabItem } let initRoute function getInitRouteName (config: RouterConfig) { if (initRoute) return initRoute const initPath = config.initPath || '' const rn = config.rnConfig || {} if (initPath) { initRoute = handleUrl(initPath).pageName } else if (rn?.initialRouteName) { initRoute = camelCase(rn.initialRouteName) } else if (config.entryPagePath) { const entryPagePath = config.entryPagePath.startsWith('/') ? config.entryPagePath : `/${config.entryPagePath}` initRoute = config.pages.find(p => p.pagePath === entryPagePath)?.name } else { initRoute = config.pages[0].name } return initRoute } function getInitTabRoute (config: RouterConfig) { const pageList = config.pages const tabNames = getTabNames(config) const initPath = config.initPath || '' let initTabName = '' if (initPath) { // 优先原生传入的路由 const route = handleUrl(initPath).pageName for (let i = 0; i < tabNames.length; i++) { if (route === tabNames[i]) { initTabName = tabNames[i] break } } } if (!initTabName) { for (let i = 0; i < pageList.length; i++) { const item = pageList[i] if (tabNames.indexOf(item.name) !== -1) { initTabName = item.name break } } } return initTabName } function getInitParams (config, pageName) { let params: any = {} const initRouteName = getInitRouteName(config) if (initRouteName === pageName) { const initPath = config.initPath || '' params = handleUrl(initPath).params params = Object.assign({}, params, config.initParams) } return params } function createTabStack (config: RouterConfig, parentProps: any, screenOptions) { const Tab = createBottomTabNavigator() const tabBar = config.tabBar const rnConfig = config.rnConfig const tabList: any = [] const tabOptions: TabOptions = rnConfig?.tabOptions || {} tabBar?.list.forEach((item, index) => { const defaultOptions = Object.assign({}, { tabBarVisible: config.tabBar?.custom ? false : getTabVisible() }, getTabItemOptions(item, index)) const tabItemOptions = Object.assign({}, defaultOptions, tabOptions, { headerShown: false, title: item.text }) setTabConfig('tabBarVisible', tabItemOptions.tabBarVisible) const path = item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}` const tabName = camelCase(path) const tabPage: PageItem = getTabItem(config, tabName) as PageItem const initParams = getInitParams(config, tabName) tabList.push() }) const borderColorMap = { black: '#000000', white: '#ffffff' } // 允许传入色值、black、white、默认 #000000 const borderTopColor = tabBar?.borderStyle ? (borderColorMap[tabBar?.borderStyle] || tabBar?.borderStyle) : '#000000' const userTabBarOptions = rnConfig?.tabBarOptions || {} // tabbarOptions const tabBarOptions = Object.assign({ backBehavior: 'none', activeTintColor: tabBar?.selectedColor || '#3cc51f', inactiveTintColor: tabBar?.color || '#7A7E83', activeBackgroundColor: tabBar?.backgroundColor || '#ffffff', inactiveBackgroundColor: tabBar?.backgroundColor || '#ffffff', style: { backgroundColor: tabBar?.backgroundColor, borderTopColor } }, userTabBarOptions) const tabNames = getTabNames(config) const tabProps = config.rnConfig?.tabProps || {} const tabInitRouteName = getTabInitRoute() || getInitTabRoute(config) || tabNames[0] return createTabBar(props, tabOptions, tabBarOptions)} initialRouteName={tabInitRouteName} screenOptions={screenOptions} >{tabList} } function createTabBar (props, tabOptions: TabOptions, tabBarOptions) { return } function getLinkingConfig (config: RouterConfig) { const prefixes = config?.rnConfig?.linking || config.linkPrefix || [] const screens: Record = {} const pageList = getPageList(config) pageList.forEach(item => { const path: string = item.pagePath.startsWith('/') ? item.pagePath.substr(1) : item.pagePath screens[`${item.name}`] = path }) let tabScreen: Record = {} if (config.tabBar) { const tabs: Record = {} const tabBarList = config.tabBar?.list || [] tabBarList.forEach((item) => { const tabPath = item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}` const tabName = camelCase(tabPath) tabs[`${tabName}`] = tabPath }) tabScreen = { tabNav: { path: '', screens: tabs } } } return { prefixes: prefixes, config: { screens: { ...tabScreen, ...screens } } } } function defaultOnUnhandledAction (action) { // @ts-ignore if (process.env.NODE_ENV === 'production') { return } const payload: Record | undefined = action.payload let message = `The action '${action.type}'${ payload ? ` with payload ${JSON.stringify(action.payload)}` : '' } was not handled by any navigator.` switch (action.type) { case 'NAVIGATE': case 'PUSH': case 'REPLACE': case 'JUMP_TO': if (payload?.name) { const pageName = getCurrentJumpUrl() ?? payload?.name message += `\n\nDo you have a screen '${pageName}'?\n\nIf you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.` } else { message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.` } break case 'GO_BACK': case 'POP': case 'POP_TO_TOP': message += `\n\nIs there any screen to go back to?` break case 'OPEN_DRAWER': case 'CLOSE_DRAWER': case 'TOGGLE_DRAWER': message += `\n\nIs your screen inside a Drawer navigator?` break } message += `\n\nThis is a development-only warning and won't be shown in production.` console.error(message) } function handlePageNotFound (action, options) { const routeObj:Record = action?.payload ?? {} if (routeObj?.name) { options?.onUnhandledAction && options?.onUnhandledAction({ path: getCurrentJumpUrl() ?? routeObj?.name, query: routeObj?.params ?? {} }) } // 监听了onUnhandledAction,导航默认打印错误就不执行了, 把源码中默认打印加一下😭 defaultOnUnhandledAction(action) } function createTabNavigate (config: RouterConfig, options: RouterOption) { const Stack = config.rnConfig?.useNativeStack ? createNativeStackNavigator() : createStackNavigator() const pageList = getPageList(config) const linking = getLinkingConfig(config) const stackProps = config.rnConfig?.stackProps const screenOptions = getStackOptions(config) return handlePageNotFound(action, options)} > {(props) => createTabStack(config, props, screenOptions)} {pageList.map(item => { const initParams = getInitParams(config, item.name) return })} } function createStackNavigate (config: RouterConfig, options:RouterOption) { const Stack = config.rnConfig?.useNativeStack ? createNativeStackNavigator() : createStackNavigator() const pageList = getPageList(config) if (pageList.length <= 0) return null const linking = getLinkingConfig(config) const stackProps = config.rnConfig?.stackProps const screenOptions = getStackOptions(config) return handlePageNotFound(action, options)} > {pageList.map(item => { const initParams = getInitParams(config, item.name) return })} }