react-router之跳转

1,217 阅读4分钟

挑选和创建router

react-router共支持5种router类型,一个项目只支持使用一种router类型。

  • 在web项目里,常用的有两种historyRouter、hashRouter。
  • memoryRouter(单元测试用?或者使用DOM history APIs进行路由管理的项目使用?没有考证过)
  • nativeRouter(React Native项目使用)

react-router中创建router有两种方式

  • createBrowerRouter
  • <BrowserRouter>

两者的区别在于:

  • <BrowserRouter>只能将router配置使用jsx、tsx形式以children的形式传入。
  • createBrowerRouter可以使用RouterObject的形式,也可以使用官方提供的createRoutesFromElements方法,将jsx转换为object。另外,createBrowerRouter支持react-router的一些data apis,如:lazy、loader等。
// createRoutesFromElements
const router = createBrowserRouter(
    createRoutesFromElements(
      <Route path="/" element={<Root />}>
        <Route path="dashboard" element={<Dashboard />} />
        {/* ... etc. */}
      </Route>
    )
);
// jsx
const router = createBrowserRouter([
    {
      path: "/",
      element: <Root />,
      children: [
        {
          path: "team",
          element: <Team />,
        },
      ],
    },
]);

createRoutesFromElements

// 使用递归处理route的层级结构,创建object对象,提取route里props,赋值为routeObject
export function createRoutesFromChildren(
  children: React.ReactNode,
  parentPath: number[] = []
): RouteObject[] {
  let routes: RouteObject[] = [];

  React.Children.forEach(children, (element, index) => {

    let treePath = [...parentPath, index];

    if (element.type === React.Fragment) {
      //  support React.Fragment
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children, treePath)
      );
      return;
    }

    let route: RouteObject = {
      id: element.props.id || treePath.join("-"),
      caseSensitive: element.props.caseSensitive,
      element: element.props.element,
      Component: element.props.Component,
      index: element.props.index,
      path: element.props.path,
      loader: element.props.loader,
      action: element.props.action,
      errorElement: element.props.errorElement,
      ErrorBoundary: element.props.ErrorBoundary,
      hasErrorBoundary:
        element.props.ErrorBoundary != null ||
        element.props.errorElement != null,
      shouldRevalidate: element.props.shouldRevalidate,
      handle: element.props.handle,
      lazy: element.props.lazy,
    };

    if (element.props.children) {
      route.children = createRoutesFromChildren(
        element.props.children,
        treePath
      );
    }
    routes.push(route);
  });
  return routes;
}

createBrowserRouter

web中常用的两种router,创建时都是调用的createRouter方法,执行initialize方法,区别是history的创建方法不同。

export function createBrowserRouter(
  routes: RouteObject[],
  opts?: DOMRouterOpts
): RemixRouter {
  return createRouter({
    basename: opts?.basename,
    future: {
      ...opts?.future,
      v7_prependBasename: true,
    },
    history: createBrowserHistory({ window: opts?.window }),
    hydrationData: opts?.hydrationData || parseHydrationData(),
    routes,
    mapRouteProperties,
    unstable_dataStrategy: opts?.unstable_dataStrategy,
    window: opts?.window,
  }).initialize();
}

export function createHashRouter(
  routes: RouteObject[],
  opts?: DOMRouterOpts
): RemixRouter {
  return createRouter({
    basename: opts?.basename,
    future: {
      ...opts?.future,
      v7_prependBasename: true,
    },
    history: createHashHistory({ window: opts?.window }),
    hydrationData: opts?.hydrationData || parseHydrationData(),
    routes,
    mapRouteProperties,
    unstable_dataStrategy: opts?.unstable_dataStrategy,
    window: opts?.window,
  }).initialize();
}

createBrowserHistory

该方法会创建history对象,将listen、push、replace、handlePop这几个方法赋值给history对象,并返回

createRouter

代码有省略

/** 创建router对象,处理跳转 */
export function createRouter(init: RouterInit): Router {
    let mapRouteProperties: MapRoutePropertiesFunction;
    // 拍平routes结构
    let dataRoutes = convertRoutesToDataRoutes(
      init.routes,
      mapRouteProperties,
      undefined,
      manifest
    );
    let state: RouterState = {};
    // state变化时,会触发set里的的回调函数
    // 回调函数注册是通过subscribe方法
    // function subscribe(fn: RouterSubscriber) {
    //    subscribers.add(fn);
    //    return () => subscribers.delete(fn);
    // }
    let subscribers = new Set<RouterSubscriber>();
    // 匹配route
    let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
    let router: Router;
  
    // 初始化router的方法
    // 调用方式为 let router = createRouter(init).initialize();
    function initialize() {
      // 注册监听popstate,传入popstate事件监听回调函数,popstate事件触发的acion为POP
      unlistenHistory = init.history.listen(
        ({ action: historyAction, location, delta }) => {
          // 内部实际执行为startNavigation方法
          return startNavigation(historyAction, location);
        }
      );
      // 返回router对象
      return router;
    }
  
    // 更新state并通知state变化
    function updateState(newState: Partial<RouterState>,...): void {
      state = {
        ...state,
        ...newState,
      };
      // 遍历subscribers,并执行内部存储的回调方法
      [...subscribers].forEach((subscriber) =>
        subscriber(state, {...})
      );
    }
       
    // 根据传入的location、action执行跳转
    async function startNavigation(
      historyAction: HistoryAction,
      location: Location,
      opts?: {...}
    ): Promise<void> {
      pendingAction = historyAction;
      // 匹配路由
      let matches = matchRoutes(routesToUse, location, basename);
      // 未匹配到,返回404错误error boundary
      if (!matches) {
        let error = getInternalRouterError(404, { pathname: location.pathname });
        completeNavigation(...);
        return;
      }
      // 执行completeNavigation
      completeNavigation(location, {
        matches,
        ...getActionDataForCommit(pendingActionResult),
        loaderData,
        errors,
      });
    }
  
    // 完成navigation、更新state(historyAction/location/matches)
    function completeNavigation(location,newState): void {
      // 根据跳转类型,执行不同方法
      if (pendingAction === HistoryAction.Pop) {
        // popstate事件触发,不需要动作,因为事件触发说明,url已经发生了变更。
      } else if (pendingAction === HistoryAction.Push) {
        // 此处即执行window.history.pushState方法
        init.history.push(location, location.state);
      } else if (pendingAction === HistoryAction.Replace) {
        // 此处即执行window.history.replaceState方法
        init.history.replace(location, location.state);
      }
      // 更新state
      updateState(...props);
    }
  
    // 手动触发跳转方法 --useNavigate内部即执行此方法
    async function navigate(to: number | To | null,opts?: RouterNavigateOptions){
      // 传入数字,即执行window.history.go方法
      if (typeof to === "number") {
        init.history.go(to);
        return;
      }
      // 其他情况即执行startNavigation
      return await startNavigation(...props);
    }
    // 返回router对象
    router = {
      initialize,
      subscribe,
      navigate,
    };
    return router;
}

RouterProvider

// RouterProvider通过context,将各种router相关的变量传递给下层组件
export function RouterProvider({
    fallbackElement,
    router,
    future,
  }: RouterProviderProps): React.ReactElement {
    let [state, setStateImpl] = React.useState(router.state);
    
    // 执行updateState方法后会触发state的变更,从而触发重新渲染,渲染逻辑包含在DataRoutes中
    let setState = React.useCallback<RouterSubscriber>(
      (newState: RouterState) => {setStateImpl(newState);},
      [setStateImpl]
    );
    
    // 注册updateState变化后的回调方法,
    React.useLayoutEffect(() => router.subscribe(setState), [router, setState]);
  
    let navigator = React.useMemo((): Navigator => {
      return {
        createHref: router.createHref,
        encodeLocation: router.encodeLocation,
        go: (n) => router.navigate(n),
        push: (to, state, opts) =>
          router.navigate(to, {
            state,
            preventScrollReset: opts?.preventScrollReset,
          }),
        replace: (to, state, opts) =>
          router.navigate(to, {
            replace: true,
            state,
            preventScrollReset: opts?.preventScrollReset,
          }),
      };
    }, [router]);
  
    let basename = router.basename || "/";
  
    let dataRouterContext = React.useMemo(
      () => ({
        router,
        navigator,
        static: false,
        basename,
      }),
      [router, navigator, basename]
    );
  
    return (
      <>
        <DataRouterContext.Provider value={dataRouterContext}>
          <DataRouterStateContext.Provider value={state}>
            <Router
              basename={basename}
              location={state.location}
              navigationType={state.historyAction}
              navigator={navigator}
            >
                <DataRoutes
                  routes={router.routes}
                  future={router.future}
                  state={state}
                />
            </Router>
          </DataRouterStateContext.Provider>
        </DataRouterContext.Provider>
        {null}
      </>
    );
  }

总结

与跳转有关的逻辑,router需要做的事情简单来说有两个,即管理url和根据url渲染相关的组件。 url的变更分为两种类型,

  • 外部触发的url变更(外部相对于react-router而言),此类变更是通过监听popState事件,执行handlePop方法,即startNavigation->completeNavigation方法来达到的
  • react-router内部的方法触发的,比如useNavigate hook、Navigate组件等,此类变更是通过主动触发startNavigation方法来达到的,与外部触发的区别在于,主动触发的跳转需要维护url,同时不能触发页面的刷新,这里的实现即通过原生的history.pushState和history.replaceState方法达到的

github.com/remix-run/r… reactrouter.com/en/main/rou… developer.mozilla.org/en-US/docs/…