mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-02-13 09:39:18 +08:00
473 lines
15 KiB
JavaScript
473 lines
15 KiB
JavaScript
import pathToRegexp from 'path-to-regexp';
|
|
|
|
import NavigationActions from '../NavigationActions';
|
|
import createConfigGetter from './createConfigGetter';
|
|
import getScreenForRouteName from './getScreenForRouteName';
|
|
import StateUtils from '../StateUtils';
|
|
import validateRouteConfigMap from './validateRouteConfigMap';
|
|
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
|
|
import invariant from '../utils/invariant';
|
|
import { generateKey } from './KeyGenerator';
|
|
|
|
function isEmpty(obj) {
|
|
if (!obj) return true;
|
|
for (let key in obj) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export default (routeConfigs, stackConfig = {}) => {
|
|
// Fail fast on invalid route definitions
|
|
validateRouteConfigMap(routeConfigs);
|
|
|
|
const childRouters = {};
|
|
const routeNames = Object.keys(routeConfigs);
|
|
|
|
// Loop through routes and find child routers
|
|
routeNames.forEach(routeName => {
|
|
const screen = getScreenForRouteName(routeConfigs, routeName);
|
|
if (screen && screen.router) {
|
|
// If it has a router it's a navigator.
|
|
childRouters[routeName] = screen.router;
|
|
} else {
|
|
// If it doesn't have router it's an ordinary React component.
|
|
childRouters[routeName] = null;
|
|
}
|
|
});
|
|
|
|
const { initialRouteParams } = stackConfig;
|
|
|
|
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
|
|
|
|
const initialChildRouter = childRouters[initialRouteName];
|
|
const pathsByRouteNames = { ...stackConfig.paths } || {};
|
|
let paths = [];
|
|
|
|
// Build paths for each route
|
|
routeNames.forEach(routeName => {
|
|
let pathPattern =
|
|
pathsByRouteNames[routeName] || routeConfigs[routeName].path;
|
|
let matchExact = !!pathPattern && !childRouters[routeName];
|
|
if (pathPattern === undefined) {
|
|
pathPattern = routeName;
|
|
}
|
|
const keys = [];
|
|
let re, toPath, priority;
|
|
if (typeof pathPattern === 'string') {
|
|
// pathPattern may be either a string or a regexp object according to path-to-regexp docs.
|
|
re = pathToRegexp(pathPattern, keys);
|
|
toPath = pathToRegexp.compile(pathPattern);
|
|
priority = 0;
|
|
} else {
|
|
// for wildcard match
|
|
re = pathToRegexp('*', keys);
|
|
toPath = () => '';
|
|
matchExact = true;
|
|
priority = -1;
|
|
}
|
|
if (!matchExact) {
|
|
const wildcardRe = pathToRegexp(`${pathPattern}/*`, keys);
|
|
re = new RegExp(`(?:${re.source})|(?:${wildcardRe.source})`);
|
|
}
|
|
pathsByRouteNames[routeName] = { re, keys, toPath, priority };
|
|
});
|
|
|
|
paths = Object.entries(pathsByRouteNames);
|
|
/* $FlowFixMe */
|
|
paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority);
|
|
|
|
return {
|
|
getComponentForState(state) {
|
|
const activeChildRoute = state.routes[state.index];
|
|
const { routeName } = activeChildRoute;
|
|
if (childRouters[routeName]) {
|
|
return childRouters[routeName].getComponentForState(activeChildRoute);
|
|
}
|
|
return getScreenForRouteName(routeConfigs, routeName);
|
|
},
|
|
|
|
getComponentForRouteName(routeName) {
|
|
return getScreenForRouteName(routeConfigs, routeName);
|
|
},
|
|
|
|
getStateForAction(action, state) {
|
|
// Set up the initial state if needed
|
|
if (!state) {
|
|
let route = {};
|
|
if (
|
|
action.type === NavigationActions.NAVIGATE &&
|
|
childRouters[action.routeName] !== undefined
|
|
) {
|
|
return {
|
|
isTransitioning: false,
|
|
index: 0,
|
|
routes: [
|
|
{
|
|
routeName: action.routeName,
|
|
params: action.params,
|
|
type: undefined,
|
|
key: `Init-${generateKey()}`,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
if (initialChildRouter) {
|
|
route = initialChildRouter.getStateForAction(
|
|
NavigationActions.navigate({
|
|
routeName: initialRouteName,
|
|
params: initialRouteParams,
|
|
})
|
|
);
|
|
}
|
|
const params = (route.params ||
|
|
action.params ||
|
|
initialRouteParams) && {
|
|
...(route.params || {}),
|
|
...(action.params || {}),
|
|
...(initialRouteParams || {}),
|
|
};
|
|
route = {
|
|
...route,
|
|
routeName: initialRouteName,
|
|
key: `Init-${generateKey()}`,
|
|
...(params ? { params } : {}),
|
|
};
|
|
// eslint-disable-next-line no-param-reassign
|
|
state = {
|
|
isTransitioning: false,
|
|
index: 0,
|
|
routes: [route],
|
|
};
|
|
}
|
|
|
|
// Check if a child scene wants to handle the action as long as it is not a reset to the root stack
|
|
if (action.type !== NavigationActions.RESET || action.key !== null) {
|
|
const keyIndex = action.key
|
|
? StateUtils.indexOf(state, action.key)
|
|
: -1;
|
|
const childIndex = keyIndex >= 0 ? keyIndex : state.index;
|
|
const childRoute = state.routes[childIndex];
|
|
invariant(
|
|
childRoute,
|
|
`StateUtils erroneously thought index ${childIndex} exists`
|
|
);
|
|
const childRouter = childRouters[childRoute.routeName];
|
|
if (childRouter) {
|
|
const route = childRouter.getStateForAction(action, childRoute);
|
|
if (route === null) {
|
|
return state;
|
|
}
|
|
if (route && route !== childRoute) {
|
|
return StateUtils.replaceAt(state, childRoute.key, route);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle explicit push navigation action
|
|
if (
|
|
action.type === NavigationActions.NAVIGATE &&
|
|
childRouters[action.routeName] !== undefined
|
|
) {
|
|
const childRouter = childRouters[action.routeName];
|
|
let route;
|
|
|
|
// The key may be provided for pushing, or to navigate back to the key
|
|
if (action.key) {
|
|
const lastRouteIndex = state.routes.findIndex(
|
|
r => r.key === action.key
|
|
);
|
|
if (lastRouteIndex !== -1) {
|
|
// If index is unchanged and params are not being set, leave state identity intact
|
|
if (state.index === lastRouteIndex && !action.params) {
|
|
return state;
|
|
}
|
|
const routes = [...state.routes];
|
|
// Apply params if provided, otherwise leave route identity intact
|
|
if (action.params) {
|
|
const route = state.routes.find(r => r.key === action.key);
|
|
routes[lastRouteIndex] = {
|
|
...route,
|
|
params: {
|
|
...route.params,
|
|
...action.params,
|
|
},
|
|
};
|
|
}
|
|
// Return state with new index. Change isTransitioning only if index has changed
|
|
return {
|
|
...state,
|
|
isTransitioning:
|
|
state.index !== lastRouteIndex
|
|
? action.immediate !== true
|
|
: undefined,
|
|
index: lastRouteIndex,
|
|
routes,
|
|
};
|
|
}
|
|
}
|
|
const key = action.key || generateKey();
|
|
if (childRouter) {
|
|
const childAction =
|
|
action.action || NavigationActions.init({ params: action.params });
|
|
route = {
|
|
params: action.params,
|
|
...childRouter.getStateForAction(childAction),
|
|
key,
|
|
routeName: action.routeName,
|
|
};
|
|
} else {
|
|
route = {
|
|
params: action.params,
|
|
key,
|
|
routeName: action.routeName,
|
|
};
|
|
}
|
|
return {
|
|
...StateUtils.push(state, route),
|
|
isTransitioning: action.immediate !== true,
|
|
};
|
|
}
|
|
|
|
if (
|
|
action.type === NavigationActions.COMPLETE_TRANSITION &&
|
|
state.isTransitioning
|
|
) {
|
|
return {
|
|
...state,
|
|
isTransitioning: false,
|
|
};
|
|
}
|
|
|
|
// Handle navigation to other child routers that are not yet pushed
|
|
if (action.type === NavigationActions.NAVIGATE) {
|
|
const childRouterNames = Object.keys(childRouters);
|
|
for (let i = 0; i < childRouterNames.length; i++) {
|
|
const childRouterName = childRouterNames[i];
|
|
const childRouter = childRouters[childRouterName];
|
|
if (childRouter) {
|
|
// For each child router, start with a blank state
|
|
const initChildRoute = childRouter.getStateForAction(
|
|
NavigationActions.init()
|
|
);
|
|
// Then check to see if the router handles our navigate action
|
|
const navigatedChildRoute = childRouter.getStateForAction(
|
|
action,
|
|
initChildRoute
|
|
);
|
|
let routeToPush = null;
|
|
if (navigatedChildRoute === null) {
|
|
// Push the route if the router has 'handled' the action and returned null
|
|
routeToPush = initChildRoute;
|
|
} else if (navigatedChildRoute !== initChildRoute) {
|
|
// Push the route if the state has changed in response to this navigation
|
|
routeToPush = navigatedChildRoute;
|
|
}
|
|
if (routeToPush) {
|
|
return StateUtils.push(state, {
|
|
...routeToPush,
|
|
key: generateKey(),
|
|
routeName: childRouterName,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (action.type === NavigationActions.SET_PARAMS) {
|
|
const key = action.key;
|
|
const lastRoute = state.routes.find(route => route.key === key);
|
|
if (lastRoute) {
|
|
const params = {
|
|
...lastRoute.params,
|
|
...action.params,
|
|
};
|
|
const routes = [...state.routes];
|
|
routes[state.routes.indexOf(lastRoute)] = {
|
|
...lastRoute,
|
|
params,
|
|
};
|
|
return {
|
|
...state,
|
|
routes,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (action.type === NavigationActions.RESET) {
|
|
// Only handle reset actions that are unspecified or match this state key
|
|
if (action.key != null && action.key != state.key) {
|
|
// Deliberately use != instead of !== so we can match null with
|
|
// undefined on either the state or the action
|
|
return state;
|
|
}
|
|
const resetAction = action;
|
|
|
|
return {
|
|
...state,
|
|
routes: resetAction.actions.map(childAction => {
|
|
const router = childRouters[childAction.routeName];
|
|
if (router) {
|
|
return {
|
|
...childAction,
|
|
...router.getStateForAction(childAction),
|
|
routeName: childAction.routeName,
|
|
key: generateKey(),
|
|
};
|
|
}
|
|
const route = {
|
|
...childAction,
|
|
key: generateKey(),
|
|
};
|
|
delete route.type;
|
|
return route;
|
|
}),
|
|
index: action.index,
|
|
};
|
|
}
|
|
|
|
if (
|
|
action.type === NavigationActions.BACK ||
|
|
action.type === NavigationActions.POP
|
|
) {
|
|
const { key, n, immediate } = action;
|
|
let backRouteIndex = state.index;
|
|
if (action.type === NavigationActions.POP && n != null) {
|
|
// determine the index to go back *from*. In this case, n=1 means to go
|
|
// back from state.index, as if it were a normal "BACK" action
|
|
backRouteIndex = Math.max(1, state.index - n + 1);
|
|
} else if (key) {
|
|
const backRoute = state.routes.find(route => route.key === key);
|
|
backRouteIndex = state.routes.indexOf(backRoute);
|
|
}
|
|
if (backRouteIndex > 0) {
|
|
return {
|
|
...state,
|
|
routes: state.routes.slice(0, backRouteIndex),
|
|
index: backRouteIndex - 1,
|
|
isTransitioning: immediate !== true,
|
|
};
|
|
}
|
|
}
|
|
return state;
|
|
},
|
|
|
|
getPathAndParamsForState(state) {
|
|
const route = state.routes[state.index];
|
|
const routeName = route.routeName;
|
|
const screen = getScreenForRouteName(routeConfigs, routeName);
|
|
const subPath = pathsByRouteNames[routeName].toPath(route.params);
|
|
let path = subPath;
|
|
let params = route.params;
|
|
if (screen && screen.router) {
|
|
const stateRoute = route;
|
|
// If it has a router it's a navigator.
|
|
// If it doesn't have router it's an ordinary React component.
|
|
const child = screen.router.getPathAndParamsForState(stateRoute);
|
|
path = subPath ? `${subPath}/${child.path}` : child.path;
|
|
params = child.params ? { ...params, ...child.params } : params;
|
|
}
|
|
return {
|
|
path,
|
|
params,
|
|
};
|
|
},
|
|
|
|
getActionForPathAndParams(pathToResolve, inputParams) {
|
|
// If the path is empty (null or empty string)
|
|
// just return the initial route action
|
|
if (!pathToResolve) {
|
|
return NavigationActions.navigate({
|
|
routeName: initialRouteName,
|
|
});
|
|
}
|
|
|
|
const [pathNameToResolve, queryString] = pathToResolve.split('?');
|
|
|
|
// Attempt to match `pathNameToResolve` with a route in this router's
|
|
// routeConfigs
|
|
let matchedRouteName;
|
|
let pathMatch;
|
|
let pathMatchKeys;
|
|
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
for (const [routeName, path] of paths) {
|
|
const { re, keys } = path;
|
|
pathMatch = re.exec(pathNameToResolve);
|
|
if (pathMatch && pathMatch.length) {
|
|
pathMatchKeys = keys;
|
|
matchedRouteName = routeName;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We didn't match -- return null
|
|
if (!matchedRouteName) {
|
|
// If the path is empty (null or empty string)
|
|
// just return the initial route action
|
|
if (!pathToResolve) {
|
|
return NavigationActions.navigate({
|
|
routeName: initialRouteName,
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Determine nested actions:
|
|
// If our matched route for this router is a child router,
|
|
// get the action for the path AFTER the matched path for this
|
|
// router
|
|
let nestedAction;
|
|
let nestedQueryString = queryString ? '?' + queryString : '';
|
|
if (childRouters[matchedRouteName]) {
|
|
nestedAction = childRouters[matchedRouteName].getActionForPathAndParams(
|
|
pathMatch.slice(pathMatchKeys.length).join('/') + nestedQueryString
|
|
);
|
|
if (!nestedAction) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// reduce the items of the query string. any query params may
|
|
// be overridden by path params
|
|
const queryParams = !isEmpty(inputParams)
|
|
? inputParams
|
|
: (queryString || '').split('&').reduce((result, item) => {
|
|
if (item !== '') {
|
|
const nextResult = result || {};
|
|
const [key, value] = item.split('=');
|
|
nextResult[key] = value;
|
|
return nextResult;
|
|
}
|
|
return result;
|
|
}, null);
|
|
|
|
// reduce the matched pieces of the path into the params
|
|
// of the route. `params` is null if there are no params.
|
|
const params = pathMatch.slice(1).reduce((result, matchResult, i) => {
|
|
const key = pathMatchKeys[i];
|
|
if (key.asterisk || !key) {
|
|
return result;
|
|
}
|
|
const nextResult = result || {};
|
|
const paramName = key.name;
|
|
nextResult[paramName] = matchResult;
|
|
return nextResult;
|
|
}, queryParams);
|
|
|
|
return NavigationActions.navigate({
|
|
routeName: matchedRouteName,
|
|
...(params ? { params } : {}),
|
|
...(nestedAction ? { action: nestedAction } : {}),
|
|
});
|
|
},
|
|
|
|
getScreenOptions: createConfigGetter(
|
|
routeConfigs,
|
|
stackConfig.navigationOptions
|
|
),
|
|
|
|
getScreenConfig: getScreenConfigDeprecated,
|
|
};
|
|
};
|