likes
comments
collection
share

React Router V6.4 (Router 对象篇)

作者站长头像
站长
· 阅读数 66

history 的不足

在 history 篇中,我们知道了 history 封装三种模式 history:

  • browserHistory
  • hashHistory
  • memoeryHistory

但是实际上它完成的在单个 url 层面的管理。

在实际中项目中,可能有很多的 url 要跳转要管理,一个 history 明显就不够用(或者难以维护)。Router 的概念就被创造了出来,Router 其实就是一个管理 Route 路由的 “表”,方便我们书写管理路由代码。

Router 对象使用

可以使用 vite 创建一个项目,然后进行快速 api 测试,使用 vitest 或者 Jest 进行前端单元测试(测试很重要,能快速验证我们猜测,保证代码质量,尤其像 React Router 这种需要高质量代码的库)

  • 安装
pnpm i @remix-run/router

Router 类型

export interface Router {
  get basename(): RouterInit["basename"];
  get state(): RouterState;
  get routes(): AgnosticDataRouteObject[];
  initialize(): Router;
  subscribe(fn: RouterSubscriber): () => void;
  enableScrollRestoration(
    savedScrollPositions: Record<string, number>,
    getScrollPosition: GetScrollPositionFunction,
    getKey?: GetScrollRestorationKeyFunction
  ): () => void;
  navigate(to: number): void;
  navigate(to: To, opts?: RouterNavigateOptions): void;
  fetch(
    key: string,
    routeId: string,
    href: string,
    opts?: RouterNavigateOptions
  ): void;
  revalidate(): void;
  createHref(location: Location | URL): string;
  encodeLocation(to: To): Path;
  getFetcher<TData = any>(key?: string): Fetcher<TData>;
  deleteFetcher(key?: string): void;
  dispose(): void;
  _internalFetchControllers: Map<string, AbortController>;
  _internalActiveDeferreds: Map<string, DeferredData>;
}

Route 类型

export type AgnosticDataRouteObject =
  | AgnosticDataIndexRouteObject
  | AgnosticDataNonIndexRouteObject;
export type AgnosticDataIndexRouteObject = AgnosticIndexRouteObject & {
  id: string;
};
export type AgnosticIndexRouteObject = AgnosticBaseRouteObject & {
  children?: undefined;
  index: true;
};
type AgnosticBaseRouteObject = {
  caseSensitive?: boolean;
  path?: string;
  id?: string;
  loader?: LoaderFunction;
  action?: ActionFunction;
  hasErrorBoundary?: boolean;
  shouldRevalidate?: ShouldRevalidateFunction;
  handle?: any;
};

如果读者使用过 react-router-dom 发现 Route 类型中没有 element, 这并不属于 Router 中的内容, element 是 React 中的特性,属于 React-Router。

创建 Router 工厂函数

export function createRouter(init: RouterInit): Router {}

createRouter 接受 init 作为参数,返回一个 Router. 下面是 RouterInit 的类型

export interface RouterInit {
  basename?: string;
  routes: AgnosticRouteObject[];
  history: History;
  hydrationData?: HydrationState;
}

创建一个 router

  • 使用 browserHistory 和 routes 就可以使用工厂函数初始化一个 router
import { createRouter, createBrowserHistory } from "@remix-run/router";

const router = createRouter({
  history: createBrowserHistory(),
  routes: [
    {
      path: "/",
    },
    {
      path: "/about"
    },
  ],
}).initialize();

router.navigate("/base") // 如果在浏览器环境中,地址就会改变 /base。但是没有刷新页面

从 history 函数参数可以得知,router 也是可以支持多种模式: hash/browser/memory

createRouter 内部重要内容

1. 使用 convertRoutesToDataRoutes 将 init.routes 转换成 dataRoutes

在转换之前要明白两个概念:

  • indexRoute
  • PathOrLayoutRoute

convertRoutesToDataRoutes 主要作用就是加上 id (indexRoute) 或者 id+children(PathOrLayoutRoute)

  • 判断 indexRoute
function isIndexRoute(
  route: AgnosticRouteObject
): route is AgnosticIndexRouteObject {
  return route.index === true;
}
  • convertRoutesToDataRoutes 的内部实现
if (isIndexRoute(route)) {
  let indexRoute: AgnosticDataIndexRouteObject = { ...route, id };
  return indexRoute;
} else {
  let pathOrLayoutRoute: AgnosticDataNonIndexRouteObject = {
    ...route,
    id,
    children: route.children
      ? convertRoutesToDataRoutes(route.children, treePath, allIds)
      : undefined,
  };
  return pathOrLayoutRoute;
}

router 转换完成之前,会初始化一些属性

  • unlistenHistory
  • subscribers
  • savedScrollPositions
  • getScrollRestorationKey
  • getScrollPosition
  • initialScrollRestored

2. 初始化 match 对象 initialMatches

  • 先看 match 的类型定义
export interface AgnosticRouteMatch<
  ParamKey extends string = string,
  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
> {
  params: Params<ParamKey>;
  pathname: string;
  pathnameBase: string;
  route: RouteObjectType;
}
  • matchRoutes 匹配 routes 函数
export function matchRoutes(
  routes: RouteObjectType[],
  locationArg: Partial<Location> | string,
  basename = "/"
){return match}

let initialMatches = matchRoutes(
    dataRoutes,
    init.history.location,
    init.basename
);

matchRoutes 内部实现

let branches = flattenRoutes(routes);

let matches = null;
  for (let i = 0; matches == null && i < branches.length; ++i) {
    matches = matchRouteBranch<string, RouteObjectType>(
      branches[i],
      safelyDecodeURI(pathname)
    );
  }

  return matches;
  • RouteBranch 类型
interface RouteBranch<
  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
> {
  path: string;
  score: number;
  routesMeta: RouteMeta<RouteObjectType>[];
}
  • matchRouteBranch
function matchRouteBranch<
  ParamKey extends string = string,
  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
>(
  branch: RouteBranch<RouteObjectType>,
  pathname: string
): AgnosticRouteMatch<ParamKey, RouteObjectType>[] | null {
    let matches: AgnosticRouteMatch<ParamKey, RouteObjectType>[] = [];
    let match = matchPath(
      { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
      remainingPathname
    );
    
    /*...*/
    matches.push({
      params: matchedParams as Params<ParamKey>,
      pathname: joinPaths([matchedPathname, match.pathname]),
      pathnameBase: normalizePathname(
        joinPaths([matchedPathname, match.pathnameBase])
      ),
      route,
    });
    
    eturn matches;
}

这里 initialMatches 中 match 对象

定义 router.state 对象(注意:这里的 state 是比较复杂和重要)

let state: RouterState = {
    historyAction: init.history.action, // history 中携带的 action
    location: init.history.location, // history 中携带的 location
    matches: initialMatches, // 上面已经涉及了 initialMatches 对象
    initialized,
    navigation: IDLE_NAVIGATION,
    restoreScrollPosition: null,
    preventScrollReset: false,
    revalidation: "idle",
    loaderData: (init.hydrationData && init.hydrationData.loaderData) || {},
    actionData: (init.hydrationData && init.hydrationData.actionData) || null,
    errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,
    fetchers: new Map(),
  };

其中 initizalized

let initialized =
    !initialMatches.some((m) => m.route.loader) || init.hydrationData != null;
  • 定义 initialize 函数
function initialize() {
    // If history informs us of a POP navigation, start the navigation but do not update
    // state.  We'll update our own state once the navigation completes
    unlistenHistory = init.history.listen(
      ({ action: historyAction, location }) =>
        startNavigation(historyAction, location)
    );

    // Kick off initial data load if needed.  Use Pop to avoid modifying history
    if (!state.initialized) {
      startNavigation(HistoryAction.Pop, state.location);
    }

    return router;
}

返回 router 对象,监听 并开始导航

定义订阅函数 subscribe

实现很简单,在 subscribe Map 数据结构中添加函数,然后返回一个包含删除订阅的函数

// Subscribe to state updates for the router
  function subscribe(fn: RouterSubscriber) {
    subscribers.add(fn);
    return () => subscribers.delete(fn);
  }

定义 enableScrollRestoration 函数

function enableScrollRestoration(
    positions: Record<string, number>,
    getPosition: GetScrollPositionFunction,
    getKey?: GetScrollRestorationKeyFunction
) {
    savedScrollPositions = positions;
    getScrollPosition = getPosition;
    getScrollRestorationKey = getKey || ((location) => location.key);

    if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
      initialScrollRestored = true;
      let y = getSavedScrollPosition(state.location, state.matches);
      if (y != null) {
        updateState({ restoreScrollPosition: y });
      }
    }

    return () => {
      savedScrollPositions = null;
      getScrollPosition = null;
      getScrollRestorationKey = null;
    };
 }
 
 滚动本质是修改 history 的 state
function updateState(newState: Partial<RouterState>): void {
    state = {
      ...state,
      ...newState,
    };
    subscribers.forEach((subscriber) => subscriber(state));
}

定义 navigate 函数(用于修改 url,和后续跳转)

  • number 模式,本质是 go 方法
  • 对象模式
async function navigate(
    to: number | To,
    opts?: RouterNavigateOptions
  ): Promise<void> {
    if (typeof to === "number") {
      init.history.go(to);
      return;
    }

    let { path, submission, error } = normalizeNavigateOptions(to, opts);

    let location = createLocation(state.location, path, opts && opts.state);
    location = {
      ...location,
      ...init.history.encodeLocation(location),
    };

    let historyAction =
      (opts && opts.replace) === true || submission != null
        ? HistoryAction.Replace
        : HistoryAction.Push;
    let preventScrollReset =
      opts && "preventScrollReset" in opts
        ? opts.preventScrollReset === true
        : undefined;

    return await startNavigation(historyAction, location, {
      submission,
      pendingError: error,
      preventScrollReset,
      replace: opts && opts.replace,
    });
  }

对象模式下开始导航 startNavigation 函数/completeNavigation 函数

startNavigation 为 completeNavigation 函数所需要的属性:

  • location
  • options
    • matches
    • loaderData
    • errors

导航的本质根据不同的条件,调用 push/replace

function completeNavigation(
    location: Location,
    newState: Partial<Omit<RouterState, "action" | "location" | "navigation">>
  ): void {
      // 更新
      updateState({/**/})
      // 本质
      if (isUninterruptedRevalidation) {
    } else if (pendingAction === HistoryAction.Pop) {
    } else if (pendingAction === HistoryAction.Push) {
      init.history.push(location, location.state);
    } else if (pendingAction === HistoryAction.Replace) {
      init.history.replace(location, location.state);
    }
    // 后面是重置数据
  }

navigate 的用法:

router.navigate("/about-page");
router.navigate("/home", { replace: true });
router.navigate(-1);

let formData = new FormData();
formData.append("name", "m");
router.navigate("/about", {
  formMethod: "post",
  formData,
});

定义 fetch 函数

什么是 fetch

fetch 是一种不触发导航情况下调用 loader加载器/action 操作的机制

// Execute the loader for /page
router.fetch("key", "/page");

// Submit to the action for /page
let formData = new FormData();
formData.append(key, value);
router.fetch("key", "/page", {
  formMethod: "post",
  formData,
});

fetch 函数将 createRouter 中 state 对象 fetcher Map 数据结构保存到 history.state 中

fetcher 类型

type FetcherStates<TData = any> = {
  Idle: {
    state: "idle";
    formMethod: undefined;
    formAction: undefined;
    formEncType: undefined;
    formData: undefined;
    data: TData | undefined;
  };
  Loading: {
    state: "loading";
    formMethod: FormMethod | undefined;
    formAction: string | undefined;
    formEncType: FormEncType | undefined;
    formData: FormData | undefined;
    data: TData | undefined;
  };
  Submitting: {
    state: "submitting";
    formMethod: FormMethod;
    formAction: string;
    formEncType: FormEncType;
    formData: FormData;
    data: TData | undefined;
  };
};

export type Fetcher<TData = any> =
  FetcherStates<TData>[keyof FetcherStates<TData>];

获取 fetcher

function getFetcher<TData = any>(key: string): Fetcher<TData> {
    return state.fetchers.get(key) || IDLE_FETCHER;
}
  • loadingFetcher
let loadingFetcher: FetcherStates["Loading"] = {
  state: "loading",
  formMethod: undefined,
  formAction: undefined,
  formEncType: undefined,
  formData: undefined,
  data: existingFetcher && existingFetcher.data,
};
state.fetchers.set(key, loadingFetcher);
updateState({ fetchers: new Map(state.fetchers) });
  • doneFetcher
 let doneFetcher: FetcherStates["Idle"] = {
  state: "idle",
  data: result.data,
  formMethod: undefined,
  formAction: undefined,
  formEncType: undefined,
  formData: undefined,
};
state.fetchers.set(key, doneFetcher);
updateState({ fetchers: new Map(state.fetchers) });

定义 getFetcher

function getFetcher<TData = any>(key: string): Fetcher<TData> {
    return state.fetchers.get(key) || IDLE_FETCHER;
}

定义 deleteFetcher

function deleteFetcher(key: string): void {
    if (fetchControllers.has(key)) abortFetcher(key);
    fetchLoadMatches.delete(key);
    fetchReloadIds.delete(key);
    fetchRedirectIds.delete(key);
    state.fetchers.delete(key);
}

定义 revalidate

重新验证所有当前的所有 loader

function revalidate() {
    interruptActiveLoads();
    updateState({ revalidation: "loading" });

    if (state.navigation.state === "submitting") {
      return;
    }

    if (state.navigation.state === "idle") {
      startNavigation(state.historyAction, state.location, {
        startUninterruptedRevalidation: true,
      });
      return;
    }

    startNavigation(
      pendingAction || state.historyAction,
      state.navigation.location,
      { overrideNavigation: state.navigation }
    );
  }

定义 createHref

(to: To) => init.history.createHref(to)

定义 encodeLocation

(to: To) => init.history.encodeLocation(to)

定义 dispose

清除

function dispose() {
    if (unlistenHistory) {
      unlistenHistory();
    }
    subscribers.clear();
    pendingNavigationController && pendingNavigationController.abort();
    state.fetchers.forEach((_, key) => deleteFetcher(key));
}

Jest 测试

Router 包使用了 Jest 进行测试,在前面的文章里面已经说明了测试最好使用 memoryHistory 模式 history 和 router 进行测试,或者集成 dom 环境。

如果想要更加深入理解 Router 对象,不妨阅读测试文件。可以自己写这些测试用例,提供自己测试能力和学习源码的理解能力。

小结

Router 是一个复杂的对象,Router 建立在 history 对象之上支持多种模式)。重要的概念包括:

  • routes (路由表)
  • state (state 对象的操作,包括做滚动存储)
  • navigate(跳转的功能)
  • fetch (fetch 无跳转情况下操作 loaders/actions)

文章推荐

下一步

有了 Router 的基础下就是深入理解 Router 和测试用例(这一章要自己动手实践,充分理解才能进入 React-Router 阶段)和 React 层面的封装(组件,hooks)

help

作者正在参加投票活动,如果文章真的能帮助到您,不妨发财的小手点一点,投一票是对作者最大鼓励丫。

转载自:https://juejin.cn/post/7180425765966577701
评论
请登录