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';
/**
* 检查一个对象是否空对象
* @param obj
* @returns {boolean}
*/
function isEmpty(obj) {
// 如果这个对象是 null, undefined 或者 0, 直接返回 true, 认为是空对象
if (!obj) return true;
// 如果这个对象有任何属性,则认为是非空对象
for (let key in obj) {
return false;
}
// 代码能走到这里,说明这是个空对象
return true;
}
/**
* 检测一个指令 action 是否是一个行为跟 push (压栈) 类似,
* 这样的 action 有两个 : NAVIGATE, PUSH
* @param action
* @returns {boolean}
*/
function behavesLikePushAction(action) {
// 如果 action 是 NAVIGATE 或者PUSH 返回 true, 否则返回 false
return (
action.type === NavigationActions.NAVIGATE ||
action.type === NavigationActions.PUSH
);
}
/**
* 导出 栈式路由器 创建函数
*
* 概念解释 :
* 1. 路由配置 : 包含多个路由配置项的一个对象
* 2. 路由配置项 : 路由名称(字符串) : 路由配置对象(至少包含一个名字为 screen 的属性)
* 3. 路由配置项中属性 screen 的值可以是一个 开发人员定义的屏幕组件或者也是一个 navigator
*
* @param routeConfigs 路由配置
* @param stackConfig 导航器配置
* @returns {*} 一个栈式路由器对象
*/
export default (routeConfigs, stackConfig = {}) => {
// Fail fast on invalid route definitions
// 检查路由配置,如果有问题直接抛出错误提示错误不再继续
// 什么样的路由配置认为是没问题的 ?
// 1. 必须有配置项
// 2. 每个配置项必须包含屏幕组件,也就是属性 screen 或者 属性方法 getScreen
// 3. 每个配置项中 screen 或者 getScreen 二者最多存在一个,不能两个同时设置
// 4. 如果是属性 screen, 其类型必须是 string 或者 function
validateRouteConfigMap(routeConfigs);
// 用于记录子路由的路由器信息:
// 1. 如果某个子路由是一个普通 React 组件,记录其路由器属性为 null
// 2. 如果某个子路由也是一个 navigator/router, 记录器路由器属性为该子路由的 router 属性
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) {
// 属性 screen 本身也是 navigator/router 的情况
// 如果 screen 中有 router 属性,说明它也是一个 navigator/router
// If it has a router it's a navigator.
childRouters[routeName] = screen.router;
} else {
// 属性 screen 是普通 React 组件的情况
// 如果 screen 中没有 router 属性,认为它是一个普通 React 组件
// If it doesn't have router it's an ordinary React component.
childRouters[routeName] = null;
}
});
// 从导航器参数中获取初始路由参数属性
const {initialRouteParams} = stackConfig;
// 确定当前路由器的初始路由名称:
// 使用指定的初始路由名称,如果没有指定初始路由名称,使用第一个路由配置项的路由作为初始路由
const initialRouteName = stackConfig.initialRouteName || routeNames[0];
// 记录初始路由对应的路由器信息,null或者一个嵌套的 navigator/router
const initialChildRouter = childRouters[initialRouteName];
const pathsByRouteNames = {...stackConfig.paths} || {};
let paths = [];
/**
* 基于 action 构建初始导航状态
* @param action
* @returns {{key: string, isTransitioning: boolean, index: number, routes: *[]}}
*/
function getInitialState(action) {
let route = {};
const childRouter = childRouters[action.routeName];
// This is a push-like action, and childRouter will be a router or null if we are responsible for this routeName
if (behavesLikePushAction(action) && childRouter !== undefined) {
// 1. 如果 action 是一个 push-like action 并且 action 目标路由由该路由器负责处理的情况
// (如果 action 对应的路由不是由该路由器负责,则 childRouter 这里应该是 undefined)
let childState = {};
// The router is null for normal leaf routes
// 1. 如果 childRouter 为 null , 表示目标路由是一个普通 React 组件,也就是一个一般功能屏幕;
// 2. 如果 childRouter 不为 null, 则从上面的逻辑可知它一定是另外一个 navigator (StackNavigator, TabNavigator
// 或者其他什么类型的 navigator)
if (childRouter !== null) {
// childRouter 是另外一个 navigator 的情况( navigator 嵌套的情况)
// 备注 :
// 1. 这里可以将一个 navigator 理解成一棵树,
// 2. 分支节点是一个嵌套的 navigator, 叶子节点是普通 screen/scene 组件,
// 3. 根节点或者分支节点上 navigator 可能是 StackNavigator,TabNavigator或者其他什么类型的 navigator
// 构造 childAction, 如果指定了 action.action 使用它,如果没指定,应用初始状态
const childAction =
action.action || NavigationActions.init({params: action.params});
// 获取分支节点路由器上应用 childAction 的状态
childState = childRouter.getStateForAction(childAction);
}
// 各种计算和准备都做完了,现在构造初始状态对象并返回
return {
key: 'StackRouterRoot',// 注意这里的 Stack,表明这是一个 栈式路由器,
isTransitioning: false,
index: 0,// index 属性用于记录当前路由,也就是当前屏幕,初始为 0
routes: [
{
params: action.params,
...childState,
key: action.key || generateKey(),
routeName: action.routeName,
},
],
};
}
// 2. 非 (action 是一个 push-like action 并且 action 目标路由由该路由器负责处理的情况) 的情况
if (initialChildRouter) {
// 如果初始路由对应的也是一个路由器,则计算出在它上面派发 navigate 到路由 initialRouteName
// 的 action 后它的状态,记录到 route 变量
route = initialChildRouter.getStateForAction(
NavigationActions.navigate({
routeName: initialRouteName,
params: initialRouteParams,
})
);
// 代码能走到这个分支的实际举例 :
// 顶层路由器是一个 StackNavigator, 顶层路由器的初始路由或者第一个子路由是一个嵌套的TabNavigator,
// 那么应用刚启动时,代码逻辑就会走这个分支
}
// 现在对 route 变量的内容做一个总结 :
// 1. 如果 initialChildRouter 是一个路由器, route 已经记录了由它计算出的目标信息,
// 2. 如果 initialChildRouter 为 null , route 保持为 {}
const params = (route.params || action.params || initialRouteParams) && {
...(route.params || {}),
...(action.params || {}),
...(initialRouteParams || {}),
};
const {initialRouteKey} = stackConfig;
route = {
...route,
...(params ? {params} : {}),
routeName: initialRouteName,
key: action.key || (initialRouteKey || generateKey()),
};
return {
key: 'StackRouterRoot',
isTransitioning: false,
index: 0,// index 属性用于记录当前路由,也就是当前屏幕,初始为 0
routes: [route],
};
}
// 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);
paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority);
// 构建 StackRouter 对象并返回
return {
/**
* 查找指定导航状态 state 当前路由对应的屏幕(screen)组件(React Component)
* 注意 : 如果当前路由也是一个navigator/router,会从它里面获取相应的屏幕组件
* @param state
* @returns {*} 目标路由对应的screen组件,找不到则抛出错误
*/
getComponentForState(state) {
const activeChildRoute = state.routes[state.index];
const {routeName} = activeChildRoute;
if (childRouters[routeName]) {
return childRouters[routeName].getComponentForState(activeChildRoute);
}
return getScreenForRouteName(routeConfigs, routeName);
},
/**
* 获取指定名称的路由对应的屏幕(screen)组件(React Component)
* 注意 : 本方法只招直接子路由,不递归查找
* @param routeName 目标路由名称
* @returns {*} 目标路由对应的screen组件,找不到则抛出错误
*/
getComponentForRouteName(routeName) {
return getScreenForRouteName(routeConfigs, routeName);
},
/**
* 基于当前导航状态 state, 执行 action, 计算执行后的导航状态并返回
* @param action 待执行 action
* @param state 当前状态
* @returns {*} 在状态 state 上执行 action 之后的 状态
*/
getStateForAction(action, state) {
// Set up the initial state if needed
if (!state) {
// 如果没有提供 state 参数,或者 state 参数为 null, 则直接构建初始状态并返回初始状态
return getInitialState(action);
}
// Check if the focused 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) {
// 如果当前状态中的当前路由是一个 navigator/router, 先尝试让它看看能不能处理
// 待执行的 action
const route = childRouter.getStateForAction(action, childRoute);
// 这里 route 分三种情况 :
// 1. null : 表明 childRouter 可以处理该 action , 但是处理完之后什么都不变,所以需要直接返回当前 state
// 2. 一个新的路由状态对象 : 表明 childRouter 可以处理该 action ,并且有了相应的输出(可能有状态变化,也可能没有)
// 3. undefined : 表明 childRouter 处理不了该 action ,
// 以上 1,2 两种情况都算是当前函数任务完成了,接下来需要返回,而3这种情况表明该函数的任务
// 还未被完成,代码逻辑继续
if (route === null) {
return state;
}
if (route && route !== childRoute) {
// 如果 route 存在并且和当前 childRoute 不同,说明当前路由(嵌套的
// 路由器)能处理指定 action ,那么就将这个状态更新到导航状态对象
// state 中相应的位置
return StateUtils.replaceAt(state, childRoute.key, route);
}
}
}
// 代码走到这里,说明待执行的 action 并没有被处理 : 当前子路由不是一个嵌套路由器,
// 或者当前子路由是一个嵌套路由器但是它处理不了待执行的 action, 所以下面继续处理
// 待执行的 action
// Handle explicit push navigation action. This must happen after the
// focused child router has had a chance to handle the action.
if (
behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined
) {
// 如果待执行 action 是 NAVIGATE/PUSH,并且目标路由是当前路由器对象的子路由屏幕组件,
// 则代码会走到这个分支
const childRouter = childRouters[action.routeName];
let route;
invariant(
action.type !== NavigationActions.PUSH || action.key == null,
'StackRouter does not support key on the push action'
);
// With the navigate action, the key may be provided for pushing, or to navigate back to the key
if (action.key) {
// 如果 action 中提供了 key,则这个信息可以被用来检测目标路由屏幕是否已经存在于导航栈中,如果存在于
// 导航栈中,则会将导航栈中该路由屏幕之上的路由屏幕都扔掉,并更新路由状态中当前路由屏幕的索引属性 index
// 为目标路由屏幕在导航栈中的索引,同时更新状态中的其他一些参数,并返回新状态,此函数任务结束
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;
}
// Remove the now unused routes at the tail of the routes array
const routes = state.routes.slice(0, lastRouteIndex + 1);
// 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,
};
}
}
if (childRouter) {
// 如果目标路由(当前路由器的子路由)不是一个屏幕组件,而是一个嵌套navigator/router,
// 则更新该嵌套路由器到其初始状态,并将该子路由器状态记录新的路由栈帧到 route
const childAction =
action.action || NavigationActions.init({params: action.params});
route = {
params: action.params,
// merge the child state in this order to allow params override
...childRouter.getStateForAction(childAction),
routeName: action.routeName,
key: action.key || generateKey(),
};
} else {
// 如果目标路由(当前路由器的子路由)是一个屏幕组件,基于此构建新的路由栈帧 route
route = {
params: action.params,
routeName: action.routeName,
key: action.key || generateKey(),
};
}
// 在当前路由状态中压入新的路由栈帧并返回它,当前函数任务结束
return {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
} else if (
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
// 如果待执行 action 是 PUSH,但目标路由不是当前路由器对象的子路由,
// 则代码会走到这个分支:什么都不做,返回原状态
// If we've made it this far with a push action, we return the
// state with a new identity to prevent the action from bubbling
// back up.
return {
...state,
};
}
// Handle navigation to other child routers that are not yet pushed
if (behavesLikePushAction(action)) {
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) {
const route = {
...routeToPush,
routeName: childRouterName,
key: action.key || generateKey(),
};
return StateUtils.push(state, route);
}
}
}
}
// Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
if (action.type === NavigationActions.POP_TO_TOP) {
// Refuse to handle pop to top if a key is given that doesn't correspond
// to this router
if (action.key && state.key !== action.key) {
return state;
}
// If we're already at the top, then we return the state with a new
// identity so that the action is handled by this router.
if (state.index === 0) {
return {
...state,
};
} else {
return {
...state,
isTransitioning: action.immediate !== true,
index: 0,
routes: [state.routes[0]],
};
}
return state;
}
// Handle replace action
if (action.type === NavigationActions.REPLACE) {
const routeIndex = state.routes.findIndex(r => r.key === action.key);
// Only replace if the key matches one of our routes
if (routeIndex !== -1) {
const childRouter = childRouters[action.routeName];
let childState = {};
if (childRouter) {
const childAction =
action.action ||
NavigationActions.init({params: action.params});
childState = childRouter.getStateForAction(childAction);
}
const routes = [...state.routes];
routes[routeIndex] = {
params: action.params,
// merge the child state in this order to allow params override
...childState,
routeName: action.routeName,
key: action.newKey || generateKey(),
};
return {...state, routes};
}
}
// Update transitioning state
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
state.isTransitioning
) {
return {
...state,
isTransitioning: false,
};
}
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 newStackActions = action.actions;
return {
...state,
routes: newStackActions.map(newStackAction => {
const router = childRouters[newStackAction.routeName];
let childState = {};
if (router) {
const childAction =
newStackAction.action ||
NavigationActions.init({params: newStackAction.params});
childState = router.getStateForAction(childAction);
}
return {
params: newStackAction.params,
...childState,
routeName: newStackAction.routeName,
key: newStackAction.key || generateKey(),
};
}),
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,
};
} else if (
backRouteIndex === 0 &&
action.type === NavigationActions.POP
) {
return {
...state,
};
}
}
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;
let decodedMatchResult;
try {
decodedMatchResult = decodeURIComponent(matchResult);
} catch (e) {
// ignore `URIError: malformed URI`
}
nextResult[paramName] = decodedMatchResult || matchResult;
return nextResult;
}, queryParams);
return NavigationActions.navigate({
routeName: matchedRouteName,
...(params ? {params} : {}),
...(nestedAction ? {action: nestedAction} : {}),
});
},
getScreenOptions: createConfigGetter(
routeConfigs,
stackConfig.navigationOptions
),
getScreenConfig: getScreenConfigDeprecated,
};
};
React Navigation源代码阅读 :routers/StackRouter.js
猜你喜欢
转载自blog.csdn.net/andy_zhang2007/article/details/80285965
今日推荐
周排行