react-routerbeta了两年半,出来了。 来吧,展示!!!

此次的变化,更新了一些内置组件,react组件和history包分开管理。交给了具体的平台去选择,只要实现具体的接口就好。比如单个使用Router组件不行

一些变化:

  1. Route和lInk组件是在Routes相对的,由context实现。这使得代码更加精简。 路由的匹配是根据最佳匹配度来决定的,也就是score 匹配度根据路由的嵌套层级,和index来计算
  2. history在reactRouter中移除,由navgigate代替,本质上还是history对象。
  3. route组件的三种渲染方式统一变为element。是ReactElement类型,直接传递props元信息更方便。
    • :id 动态参数路由
    • * 通配符
    • 移除了正则匹配
    • caseSensitive 区分大小写
    • index 为主路由,如果为true,route组件不能存在children属性,index选项只是增加了匹配度(2)
  4. 支持嵌套路由, 增加Outlet组件,和同样效果的useOutlet hook. outlet 也是基于context,会显示当前路由上下文下,与路由最匹配的组件
  5. 对于跳转路由相关,增加useHref 命类似于cd 也是基于context, 一个.. 从当前路由向上取一位
  6. history由navigate替代,功能合并为一个函数
  7. 一个新的匹配规则

score

score的计算是根据当前路由的嵌套路由,和一些参数来决定的

  1. const paramRe = /^:\w+$/;
  2. const dynamicSegmentValue = 3;
  3. const indexRouteValue = 2;
  4. const emptySegmentValue = 1;
  5. const staticSegmentValue = 10;
  6. const splatPenalty = -2;
  7. const isSplat = (s: string) => s === "*";
  8. function computeScore(path: string, index: boolean | undefined): number {
  9. // 当前路由以/分割
  10. let segments = path.split("/");
  11. // 默认根据当前路由的嵌套层级决定的匹配度
  12. let initialScore = segments.length;
  13. // 存在通配符减少匹配度
  14. if (segments.some(isSplat)) {
  15. initialScore += splatPenalty;
  16. }
  17. // index属性, 一个bool值, 会增加路由的匹配度
  18. if (index) {
  19. initialScore += indexRouteValue;
  20. }
  21. // 计算匹配度,根据我们的定义的路由(Route)组件计算所有的匹配度
  22. return segments
  23. .filter(s => !isSplat(s))
  24. .reduce(
  25. (score, segment) =>
  26. score +
  27. (paramRe.test(segment)
  28. ? dynamicSegmentValue
  29. : segment === ""
  30. ? emptySegmentValue
  31. : staticSegmentValue),
  32. initialScore
  33. );
  34. }

score匹配度计算完毕以后,会把所有定义的路由信息数组拍平并排序

interface RouteMatch<ParamKey extends string = string> {

  params: Params<ParamKey>;

  pathname: string;

  pathnameBase: string;
    // route的porps
  route: RouteObject;
}


function flattenRoutes(
  routes: RouteObject[],
  branches: RouteBranch[] = [],
  parentsMeta: RouteMeta[] = [],
  parentPath = ""
): RouteBranch[] {

  routes.forEach((route, index) => {
    let meta: RouteMeta = {
      relativePath: route.path || "",
      caseSensitive: route.caseSensitive === true,
      childrenIndex: index
    };
        // 连接父级路由, 收集路由下所有的routeMeta
    let routesMeta = parentsMeta.concat(meta);

    if (route.children && route.children.length > 0) {
        // 递归展开所有子组件
      flattenRoutes(route.children, branches, routesMeta, path);
    }

    if (route.path == null && !route.index) {
      return;
    }

    branches.push({ path, score: computeScore(path, route.index), routesMeta });
  });

  return branches;
}
// 排序
function rankRouteBranches(branches: RouteBranch[]): void {
  branches.sort((a, b) =>
    a.score !== b.score
      ? b.score - a.score // Higher score first
      : compareIndexes(
        a.routesMeta.map(meta => meta.childrenIndex),
        b.routesMeta.map(meta => meta.childrenIndex)
      )
  );
}


 function matchRoutes(
  routes: RouteObject[],
  locationArg: Partial<Location> | string,
  basename = "/"
): RouteMatch[] | null {

     // ...


  // 拍平所有定义的routes,也就是
  /*
      [
        {
        path:"home",
          element:ReactElement,
        children: [
            // ... {}
        ]
      },
      {
        path:"about",
          element:ReactElement
      }
    ]
  */
  let branches = flattenRoutes(routes);
  // 根据匹配度来排序
  rankRouteBranches(branches);

  let matches = null;
  // 第一个branch匹配到以后停止循环
  for (let i = 0; matches == null && i < branches.length; ++i) {
    matches = matchRouteBranch(branches[i], routes, pathname);
  }

  return matches;
}

match

对定义的路由信息处理完成以后, 开始匹配路由,包括父级


// 这里通过没有给branch的routesMeta来决定是否能匹配到相应的pathname
// 如果路由信息存在一个没有匹配的就return -> null
// routesMeta最后一项是定义的route自己的路由信息,前面的都是父级路由信息
// 只有从头到尾的全部匹配,才能表示完整的路由信息
function matchRouteBranch<ParamKey extends string = string>(
  branch: RouteBranch,
  routesArg: RouteObject[],
  pathname: string
): RouteMatch<ParamKey>[] | null {
  let routes = routesArg;
  // 取出定义的路由信息
  let { routesMeta } = branch;

  let matchedParams = {};
  let matchedPathname = "/";
  let matches: RouteMatch[] = [];

  for (let i = 0; i < routesMeta.length; ++i) {
    let meta = routesMeta[i];
    let end = i === routesMeta.length - 1;

    let remainingPathname =
      matchedPathname === "/"
        ? pathname
        : pathname.slice(matchedPathname.length) || "/";
    // 这里根据页面的路由生成正则,并把url上的参数取处理
    // 和匹配路由组件没关系
    let match = matchPath(
      { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
      remainingPathname
    );
        // 存在不匹配项返回空
    if (!match) return null;

    Object.assign(matchedParams, match.params);

    // 取出路由定义, childrenIndex 在拍平时会把路由的索引保存下来
    let route = routes[meta.childrenIndex];

    // 这里把所有的路由分别取出, 比如嵌套路由,会把本级路由下的路由全部取出
    // /about
    // /about/about2
    // /about/about2/about3
    // 但是每一个路由的参数会和页面的路由匹配,匹配参数
    matches.push({
      params: matchedParams,
      pathname: joinPaths([matchedPathname, match.pathname]),
      pathnameBase: joinPaths([matchedPathname, match.pathnameBase]),
      route
    });

    if (match.pathnameBase !== "/") {
      matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
    }

    routes = route.children!;
  }

  return matches;
}


function matchPath<ParamKey extends string = string>(
  pattern: PathPattern | string,
  pathname: string
): PathMatch<ParamKey> | null {
     // init...


  // 生成正则
  let [matcher, paramNames] = compilePath(
    pattern.path,
    pattern.caseSensitive,
    pattern.end
  );

  let match = pathname.match(matcher);
  if (!match) return null;

  // 匹配到的路由
  let matchedPathname = match[0];
  let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");

  // 匹配到的参数
  let captureGroups = match.slice(1);
  // 根据路由的定义,替换参数
  // 比如 /about/:id   -> about/123 & params = { id: 123 }
  let params: Params = paramNames.reduce<Mutable<Params>>(
    (memo, paramName, index) => {

      if (paramName === "*") {
        let splatValue = captureGroups[index] || "";
        pathnameBase = matchedPathname
          .slice(0, matchedPathname.length - splatValue.length)
          .replace(/(.)\/+$/, "$1");
      }

      memo[paramName] = safelyDecodeURIComponent(
        captureGroups[index] || "",
        paramName
      );
      return memo;
    },
    {}
  );

  return {
    params,
    pathname: matchedPathname,
    pathnameBase,
    pattern
  };
}

// 这里会用到relativePath\caseSensitive\end. 
// 根据这几个参数动态计算出相应的正则
// 同时会收集到路由上所有的参数名称
function compilePath(
  path: string,
  caseSensitive = false,
  end = true
): [RegExp, string[]] {


  let paramNames: string[] = [];
  // 根据路由动态生成正则
  let regexpSource =
    "^" +
    path
      .replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
      .replace(/^\/*/, "/") // Make sure it has a leading /
      .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
      .replace(/:(\w+)/g, (_: string, paramName: string) => {
        // 匹配到的所有参数名称
        paramNames.push(paramName);
        return "([^\\/]+)";
      });

  if (path.endsWith("*")) {
    paramNames.push("*");
    regexpSource +=
      path === "*" || path === "/*"
        ? "(.*)$" // Already matched the initial /, just match the rest
        : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
  } else {
    regexpSource += end
      ? "\\/*$" // When matching to the end, ignore trailing slashes
      : // Otherwise, at least match a word boundary. This restricts parent
      // routes to matching only their own words and nothing more, e.g. parent
      // route "/home" should not match "/home2".
      "(?:\\b|$)";
  }
    //生成正则对象,是否区分大小写
  let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");

  return [matcher, paramNames];
}

完毕

  1. 第一步我们执行flattenRoutes 收集routesMeta到一个数组中,这里会把一个路由下面所有的路由定义全部取出,也就是嵌套路由。 执行computeScore 计算匹配度
  2. 执行rankRouteBranches 根据匹配度来排序,匹配度高的在前
  3. 循环,执行matchRouteBranch 根据每一个branch和最新的pathname调用matchPath, 函数根据每一个路由的path生成正则, 由pathname去匹配, 并取出参数。 matchRouteBranch 函数找到第一个匹配项的数组,找到以后停止。 匹配时只有所有的routesMeta 全部匹配时才算匹配

render

第一步

function useRoutes(
  routes: RouteObject[],
  locationArg?: Partial<Location> | string
): React.ReactElement | null {
  // 省略匹配的故事
  // ...

  // 对路由信息做最后的处理
  return _renderMatches(
    matches &&
    matches.map(match =>
      Object.assign({}, match, {
        params: Object.assign({}, parentParams, match.params),
        pathname: joinPaths([parentPathnameBase, match.pathname]),
        pathnameBase:
          match.pathnameBase === "/"
            ? parentPathnameBase
            : joinPaths([parentPathnameBase, match.pathnameBase])
      })
    ),
    parentMatches
  );
}

第二步
这里反着来, branchs是匹配度高的在前,这里渲染时会渲染匹配低的也就是父级路由,然后才是子级路由

function _renderMatches(
  matches: RouteMatch[] | null,
  parentMatches: RouteMatch[] = []
): React.ReactElement | null {

  if (matches == null) return null;

  return matches.reduceRight((outlet, match, index) => {
     // match.route.element为空就渲染当前层级的outlet,也就是value属性的outlet
    return (
      <RouteContext.Provider
        children={
          match.route.element !== undefined ? match.route.element : <Outlet />
        }
        value={{
          outlet,
          matches: parentMatches.concat(matches.slice(0, index + 1))
        }}
      />
    );
  }, null as React.ReactElement | null);
}

outlet 和useOutlet

export function Outlet(_props: OutletProps): React.ReactElement | null {
  return useOutlet();
}

export function useOutlet(): React.ReactElement | null {
  return React.useContext(RouteContext).outlet;
}

outlet拿到的就是最近的一个context的outleft,也就是我们当前路由匹配的branch

Router

router组件改了,之前默认存在路由为browser模式的路由器,现在没了由router-dom实现

interface RouterProps {
  basename?: string;
  children?: React.ReactNode;
  location: Partial<Location> | string;
  navigationType?: NavigationType;
  // histoty 
  navigator: Omit<
  History,
  "action" | "location" | "back" | "forward" | "listen" | "block"
>;
  static?: boolean;
}


 interface BrowserRouterProps {
  basename?: string;
  children?: React.ReactNode;
  window?: Window;
}

function BrowserRouter({
  basename,
  children,
  window
}: BrowserRouterProps) {
  let historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    // 和history包结合
    historyRef.current = createBrowserHistory({ window });
  }

  let history = historyRef.current;

  let [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });

  React.useLayoutEffect(() => history.listen(setState), [history]);

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
}

// dom 包中提供的Link组件
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
  function LinkWithRef(
    { onClick, reloadDocument, replace = false, state, target, to, ...rest },
    ref
  ) {
    let href = useHref(to);

    let internalOnClick = useLinkClickHandler(to, { replace, state, target });

    function handleClick(
      event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
    ) {
      if (onClick) onClick(event);
      if (!event.defaultPrevented && !reloadDocument) {
        internalOnClick(event);
      }
    }

    return (
      // eslint-disable-next-line jsx-a11y/anchor-has-content
      <a
        {...rest}
        href={href}
        onClick={handleClick}
        ref={ref}
        target={target}
      />
    );
  }
);

browserRouter组件props中没有关于history的参数,以为着在router-dom中不能再干预router的工作,比如react中实现路由导航。 但是可以基于router组件来改造

link组件也是使用的useHrefhook来实现的navlink是基于link组件来改造的。那么React-router-dom这个包就没用了,比如自定义的路由导航可以自己实现,那么link组件同样也可以。