reactjs路由

367 阅读13分钟

目标

  • 掌握react-router使⽤⽅法;
  • 能使⽤react-router设计开发react应⽤;
  • 理解react-router关键源码实现。
  • 理解react-router和vue-router的实现差异,针对⾯试提出的问题能举⼀反三;

知识要点

1. 浏览器路由原理

react-router doc: reactrouterdotcom.fly.dev/docs/en/v6/…

文章:www.cnblogs.com/lguow/p/109…

总结:

是什么

在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)

实现目标

  1. 更改url 不引起页面的刷新
  2. 如何检测url修改了

实现路由的两种实现

hash

1.形式: url 中的has(#),及后面的那部分

2.改变hash不会引起页面的刷新

3.监听url的变化

history

  1. 形式:
  1. 不会引起页面的刷新 动作 pushState, replaceState, 改变url的path部分,不会引起页面的刷新
  1. 监听url的变化
  • popstate事件,触发包括浏览器的前进后退
  1. 不会触发popstate的动作
  1. pusshState,replaceState通过拦截调用来检测url变化
  2. 通过点击事件来检测url变化

api:

history.pushState() :

方法是将新状态添加到浏览器的历史记录中,也就是说还可以通过点击 "后退" 按钮,退到前一个页面;

history.replaceState() :

是用新的状态代替当前的历史状态,也就是说没有更多的历史记录,"后退" 按钮不能操作了,页面不能 "后退" 了

参数:

  • 状态对象(state<Object | Null>):** 一个 JavaScript 对象,该对象包含用于恢复当前文档所需的所有信息。可以是任何能够通过 JSON.stringify() 方法转换成相应字符串形式的对象,也可以是其他类似 Date、RegExp 这样特定的本地类型。
  • 标题(title<String | Null>):**浏览器可以使用它标识浏览历史记录中保存的状态,可以传一个空字符串,也可以传入一个简短的标题,标明将要进入的状态。
  • 地址(URL):**用来表示当前状态的位置。新的 URL 不一定是绝对路径;如果是相对路径,它将以当前 URL 为基准(类似 #reactlazy 这样的 hash);传入的 URL 与当前 URL 应该是同源的,否则 pushState() 会抛出异常。该参数是可选的;不指定的话则为文档当前 URL。

原生JS版前端路由实现

hash

<!DOCTYPE html>
<html>
 <head>
    <meta charset="utf-8">
    <title>hash route</title> 
 </head> 
 <script>
  window.addEventListener('DOMContentLoaded',onLoad)
  //监听路由变化
  window.addEventListener('hashchange',onHashChange)
  var routeView=null;
  function onLoad(){
      routeView = document.getElementById('routeView')
    var  windowLocation = document.getElementById('windowLocation');
    windowLocation.addEventListener('click',()=>{
        window.location.hash='#/windowLocation'
    })

      onHashChange()
  }
  function onHashChange(){
      switch (location.hash){
          case '#/home':
              routeView.innerHTML='HOME';
              return;
          case '#/about':
              routeView.innerHTML = 'About';
              return;
          case '#/windowLocation':
              routeView.innerHTML = 'windowLocation';
              return;
          default:
              return        
      }
  }
 </script>  
 <body>
<ul>
    <li><a href="#/home">home</a></li>
    <li><a href="#/about">about</a></li>
    <li><button id="windowLocation">windowLocation</button></li>
</ul>
  <div id="routeView"></div>
 </body>
</html>

history

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>history route</title>
</head>
<script>
    window.addEventListener('DOMContentLoaded', onLoad)
    //监听路由变化
    window.addEventListener('popstate', onPopstate)
    var routeView = null;
    function onLoad() {
        routeView = document.getElementById('routeView')
        onPopstate()
        // 拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
        var linkList = document.querySelectorAll('a[href]')
        linkList.forEach(el => el.addEventListener('click', function (e) {
            e.preventDefault()
            window.history.pushState(null, 'main', el.getAttribute('href'))
            onPopState()
        }))

        var windowLocation = document.getElementById('windowLocation');
        windowLocation.addEventListener('click', () => {
            window.location.pathname= '/windowLocation'
        })
    }
    function onPopstate() {
        switch (location.pathname) {
            case '/home':
                routeView.innerHTML = 'HOME';
                return;
            case '/about':
                routeView.innerHTML = 'About';
                return;
            case '/windowLocation':
                routeView.innerHTML = 'windowLocation';
                return;    
            default:
                return
        }
    }
</script>

<body>
    <ul>
        <li><a href="/home">home</a></li>
        <li><a href="/about">about</a></li>
        <li><button id="windowLocation">windowLocation</button></li>
    </ul>
    <div id="routeView"></div>
</body>

</html>
  1. React-Router 核⼼源码解析

route流程.PNG 我们经常使⽤的BrowserRouter和HashRouter主要依赖三个包:react-router-dom、react-router、

history。

  • react-router 提供react路由的核⼼实现,是跨平台的。
  • react-router-dom 提供了路由在web端的具体实现,与之同级的还有react-router-native,提供
  • react-native端的路由实现。
  • history是⼀个对浏览器history操作封装,抹平浏览器history和hash的操作差异,提供统⼀的
  • location对象给react-router-dom使⽤。
ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
  document.getElementById('root') )

2.1BrowserRouter实现

export function BrowserRouter({
  basename,
  children,
  window,
}: BrowserRouterProps) {
  let historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    // 如果之前没有创建过,则创建history对象,createBrowserHistory是history包中 实现的⽅法
    historyRef.current = createBrowserHistory({ window });
 }
  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location,
 });
  // history变化时重新渲染
  React.useLayoutEffect(() => history.listen(setState), [history]);
  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
 );
}

BrowserRouter包装了,并创建了history对象,监听history变化。

Router:

export function Router({
  basename: basenameProp = "/",
  children = null,
  location: locationProp,
  navigationType = NavigationType.Pop,
  navigator,
  static: staticProp = false,
}: RouterProps): React.ReactElement | null {
  
  let basename = normalizePathname(basenameProp);
  let navigationContext = React.useMemo(
   () => ({ basename, navigator, static: staticProp }),
   [basename, navigator, staticProp]
 );
  if (typeof locationProp === "string") {
    locationProp = parsePath(locationProp);
 }
  let {
    pathname = "/",
    search = "",
    hash = "",
    state = null,
    key = "default",
 } = locationProp;
  let location = React.useMemo(() => {
    let trailingPathname = stripBasename(pathname, basename);
    if (trailingPathname == null) {
      return null;
   }
    return {
      pathname: trailingPathname,
      search,
      hash,
      state,
      key,
   };
    // 当location中有如下变化时重新⽣成location对象,触发⻚⾯重新渲染
 }, [basename, pathname, search, hash, state, key]);
   if (location == null) {
    return null;
 }
  // const LocationContext = React.createContext<LocationContextObject>
(null!);
  /**
   创建了全局的context,⽤于存放history对象
 */
  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ location, navigationType }}
      />
    </NavigationContext.Provider>
 );
}

Router处理了history对象,将其⽤NavigationContext包裹,使得下层⼦组件都可以访问到这个

history。

Routes:

export function Routes({
  children,
  location,
}: RoutesProps): React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}
export function useRoutes(
  routes: RouteObject[],
  locationArg?: Partial<Location> | string
): React.ReactElement | null {
  let { matches: parentMatches } = React.useContext(RouteContext);
  let routeMatch = parentMatches[parentMatches.length - 1];
  let parentParams = routeMatch ? routeMatch.params : {};
  let parentPathname = routeMatch ? routeMatch.pathname : "/";
  let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
  let parentRoute = routeMatch && routeMatch.route;
  let locationFromContext = useLocation();
  let location;
  if (locationArg) {
    let parsedLocationArg =
      typeof locationArg === "string" ? parsePath(locationArg) :
locationArg;
    location = parsedLocationArg;
 } else {
    location = locationFromContext;
 }
  let pathname = location.pathname || "/";
  let remainingPathname =
    parentPathnameBase === "/"
      ? pathname
     : pathname.slice(parentPathnameBase.length) || "/";
  let matches = matchRoutes(routes, { pathname: remainingPathname });
  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
 );
}
export function createRoutesFromChildren(
  children: React.ReactNode
): RouteObject[] {
  let routes: RouteObject[] = [];
  React.Children.forEach(children, (element) => {
    if (!React.isValidElement(element)) {
      return;
   }
    if (element.type === React.Fragment) {
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children)
     );
      return;
   }
    let route: RouteObject = {
      caseSensitive: element.props.caseSensitive,
      element: element.props.element,
      index: element.props.index,
      path: element.props.path,
   };
    if (element.props.children) {
      route.children = createRoutesFromChildren(element.props.children);
   }
    routes.push(route);
 });
  return routes; }

Routes通过Route⼦组件⽣成路由列表,通过location中的pathname匹配组件并渲染。

通过以上代码,我们基本理解了react-router如何感知history中的pathname变化,并渲染对应组件。

但我们具体是如何操作history变化的呢?

我们在回到最上⾯的createBrowserHistory 和 history.listen ⽅法,看看history对象是怎么被创建和改

变的:

export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
  let { window = document.defaultView! } = options;
  let globalHistory = window.history;
      
  // 获取当前history中的index和location对象
  function getIndexAndLocation(): [number, Location] { }
  let blockedPopTx: Transition | null = null;
  
  // 处理返回上⼀⻚
  function handlePop() {}
  // 监听浏览器的popState事件
  window.addEventListener(PopStateEventType, handlePop);
  
  // 操作history对象
  function applyTx(nextAction: Action) {
    action = nextAction;
   [index, location] = getIndexAndLocation();
    listeners.call({ action, location });
 }
  // push操作
  function push(to: To, state?: any) {}
  // replace操作
  function replace(to: To, state?: any) {}
  // 前进或后退n⻚
  function go(delta: number) {}
  let history: BrowserHistory = {
    get action() {
      return action;
   },
    get location() {
      return location;
   },
    createHref,
    push,
    replace,
    go,
    back() {
         go(-1);
   },
    forward() {
      go(1);
   },
    listen(listener) {
      return listeners.push(listener);
   },
    block(blocker) {
      let unblock = blockers.push(blocker);
      if (blockers.length === 1) {
        window.addEventListener(BeforeUnloadEventType,
promptBeforeUnload);
     }
      return function () {
        unblock();
        if (!blockers.length) {
          window.removeEventListener(BeforeUnloadEventType,
promptBeforeUnload);
       }
     };
   },
 };
    return history; }

createBrowserHistory创建了⼀个标准的history对象,以及对history对象操作的各⽅法,且操作变更

后,通过listen⽅法将变更结果回调给外部。

getIndexAndLocation实现:

function getIndexAndLocation(): [number, Location] {
    let { pathname, search, hash } = window.location;
    let state = globalHistory.state || {};
    return [
      state.idx,
      readOnly<Location>({
        pathname,
        search,
        hash,
        state: state.usr || null,
        key: state.key || "default",
     }),
   ];
 }

push实现:

function push(to: To, state?: any) {
    let nextAction = Action.Push;
    let nextLocation = getNextLocation(to, state);
    function retry() {
      push(to, state);
   }
    if (allowTx(nextAction, nextLocation, retry)) {
      let [historyState, url] = getHistoryStateAndUrl(nextLocation, index
+ 1);
      try {
        // 操作浏览器的history
        globalHistory.pushState(historyState, "", url);
     } catch (error) {
        window.location.assign(url);
     }
      // 处理history对象并回调
      applyTx(nextAction);
      /**
       function applyTx(nextAction: Action) {
         action = nextAction;
         [index, location] = getIndexAndLocation();
         listeners.call({ action, location });
       }
     */
   }
 }

问: 我们在代码中都是 const location = useLocation(); location.push("/") 这样的⽅式使⽤push的,

那上⾯这个push⽅法到底是怎么跟useLocation关联的呢?

还记得Router中有这么⼀段代码吗?

const LocationContext = React.createContext<LocationContextObject> (null!);

我们的history对象创建后会被Router注⼊进⼀个LocationContext的全局上下⽂中。

useLocation实际就是包裹了这个上下⽂对象。

export function useLocation(): Location {
  return React.useContext(LocationContext).location; }

总结⼀下,BrowserRouter核⼼实现包含三部分:

创建history对象,提供对浏览器history对象的操作。

创建Router组件,将创建好的history对象注⼊全局上下⽂。

Routes组件,遍历⼦组件⽣成路由表,根据当前全局上下⽂history对象中的pathname匹配当前激

活的组件并渲染。

HashRouter和BrowserRouter原理类似,只是监听的浏览器原⽣history从pathname变为hash,这⾥不

再赘述。

3. react-router和vue-router的差异

  • 路由类型

React:

borwserRouter

hashRouter

memoryRouter

Vue:

history

hash

abstract

memoryRouter和abstract作⽤类似,都是在不⽀持浏览器的环境中充当fallback

中通过 props.history 实现 路由拦截; 如果是 函数式组件,在函数⽅法中通过 props.history 或者

useHistory 返回的 history 对象 实现 路由拦截。

  • 使⽤⽅式

路由拦截的实现不同:

vue router 提供了 全局守卫、路由守卫、组件守卫 供我们实现 路由拦截。

react router 没有提供类似 vue router 的 守卫 供我们使⽤,不过我们可以 在组件渲染过程中⾃⼰

实现路由拦截。 如果是 类组件, 我们可以在 componentWillMount 或者 getDerivedStateFromProps

  • ****实现差异

hash 模式的实现不同:

react router 的 hash 模式,是基于 window.location.hash(window.location.replace) 和

hashchange 实现的。当通过 push ⽅式 跳转⻚⾯时,直接修改 window.location.hash,然后渲染⻚

⾯; 当通过 replace ⽅式 跳转⻚⾯时,会先构建⼀个 修改 hash 以后的临时 url,然后使⽤这个临时

url 通过 window.location.replace 的⽅式 替换当前 url,然后渲染⻚⾯;当 激活历史记录导致 hash 发

⽣变化时,触发 hashchange 事件,重新 渲染⻚⾯。

vue router 的 hash 模式,是先通过 pushState(replaceState) 在浏览器中 新增(修改)历史记录,然

后渲染⻚⾯。 当 激活某个历史记录 时,触发 popstate 事件,重新 渲染⻚⾯。 如果 浏览器不⽀持

pushState,才会使⽤ window.location.hash(window.location.replace) 和 hashchange 实现。

history 模式不⽀持 pushState 的处理⽅式不同

使⽤ react router 时,如果 history 模式 下 不⽀持 pushState,会通过 重新加载⻚⾯

(window.location.href = href)的⽅式实现⻚⾯跳转。

使⽤ vue router 时,如果 history 模式下不⽀持 pushState,会根据 fallback 配置项 来进⾏下⼀步

处理。 如果 fallback 为 true, 回退到 hash 模式;如果 fallback 为 false, 通过重新加载⻚⾯的⽅式实

现⻚⾯跳转。

懒加载实现过程不同

vue router在路由懒加载 过程中,会先去获取懒加载⻚⾯对应的js⽂件。等 懒加载⻚⾯对应的js⽂

件加载并执⾏完毕,才会开始渲染懒加载⻚⾯。

react router在 路由懒加载 过程中, 会先去获取懒加载⻚⾯对应的js⽂件,然后渲染 loading ⻚

⾯。 等懒加载⻚⾯对应的js⽂件加载并执⾏完毕,触发更新,渲染懒加载⻚⾯。

4. React-Router的使用场景

Auth

  • 如何组织路由
  • 如何进行路由拦截
  • 如何创建全局context, 提供Provider, 拿取值
  • 如何使用useNavigate 来进行跳转控制
<AuthProvider>//返回一个配置context的Provider组件,用于提供user信息,登录,登出的方法
      <h1>Auth Example</h1>
      <Routes>
        <Route element={<Layout />}>//Layout为编写link的组件,
          <Route path="/" element={<PublicPage />} /> //path对应路由路径, element对应组件
          <Route path="/login" element={<LoginPage />} />
          <Route
            path="/protected"
            element={
              <RequireAuth> //进行路由拦截,来决定是否渲染ProtectedPage
                <ProtectedPage />
              </RequireAuth>
            }
          />
        </Route>
      </Routes>
    </AuthProvider>


//Layout
function Layout() {
  return (
    <div>
      <AuthStatus />

      <ul>
        <li>
          <Link to="/">Public Page</Link> //跳转链接编写
        </li>
        <li>
          <Link to="/protected">Protected Page</Link>
        </li>
      </ul>
      <Outlet /> //路由匹配后的组件渲染的位置
    </div>
  );
}

function useAuth() {
  return React.useContext(AuthContext);//获取全局context
}

function AuthStatus() {
  let auth = useAuth();////获取全局context,用于任何组件的地方获取登录信息
  let navigate = useNavigate();//获取触navigate用于触发跳转

  if (!auth.user) {
    return <p>You are not logged in.</p>;
  }

  return (
    <p>
      Welcome {auth.user}!{" "}
      <button
        onClick={() => {
          auth.signout(() => navigate("/"));
        }}
      >
        Sign out
      </button>
    </p>
  );
}

//RequireAuth
function RequireAuth({ children }: { children: JSX.Element }) {
  let auth = useAuth();
  let location = useLocation();

  if (!auth.user) {
    // Redirect them to the /login page, but save the current location they were
    // trying to go to when they were redirected. This allows us to send them
    // along to that page after they login, which is a nicer user experience
    // than dropping them off on the home page.
    return <Navigate to="/login" state={{ from: location }} replace />;//用于跳转
  }

  return children;//渲染子组件的东西
}

基础

  • 首次默认渲染的组件
  • 通配路由path配置
<Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />//首次默认渲染组件
          <Route path="about" element={<About />} />
          <Route path="dashboard" element={<Dashboard />} />

          {/* Using path="*"" means "match anything", so this route
                acts like a catch-all for URLs that we don't have explicit
                routes for. */}
          <Route path="*" element={<NoMatch />} />//找不到路径渲染的组件
        </Route>
      </Routes>

自定义过滤link

  • 定义和提取url的query, 和path的参数
  • 如何使用useSearchParams提取query
  • 如何使用useParams提取path参数
  • 使用useMemo缓存依赖存的值
<Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<SneakerGrid />} />
          <Route path="/sneakers/:id" element={<SneakerView />} />//带path 参数的路由
          <Route path="*" element={<NoMatch />} />
        </Route>
      </Routes>

//
<Link
      to={`/?brand=${brand}`}//设置查询query
      {...props}
      style={{
        ...props.style,
        color: isActive ? "red" : "black",
      }}
    >
      {children}
    </Link>

//提取query
let [searchParams] = useSearchParams();
  let brand = searchParams.get("brand");

//提取path参数
 let { id } = useParams<"id">();

//使用Memo缓存依赖存的值
 const sneakers = React.useMemo(() => {
    if (!brand) return SNEAKERS;
    return filterByBrand(brand);
  }, [brand]);

自定义link

  • 使用useResolvedPath来解析to ,此挂钩根据当前位置的路径名解析pathname给定to值中的位置。
  • 使用useMatch,返回相对于当前位置的给定路径的路线匹配数据。
function Layout() {
  return (
    <div>
      <nav>
        <ul>
          <li>
            <CustomLink to="/">Home</CustomLink>//自定义封装的link
          </li>
          <li>
            <CustomLink to="/about">About</CustomLink>
          </li>
        </ul>
      </nav>

      <hr />

      <Outlet />
    </div>
  );
}

function CustomLink({ children, to, ...props }) {//提取参数
  let resolved = useResolvedPath(to);//此挂钩根据当前位置的路径名解析pathname给定to值中的位置
  let match = useMatch({ path: resolved.pathname, end: true });//返回相对于当前位置的给定路径的路线匹配数据。

  return (
    <div>
      <Link
        style={{ textDecoration: match ? "underline" : "none" }}
        to={to}
        {...props}
      >
        {children}
      </Link>
      {match && " (active)"}
    </div>
  );
}

定义query parsing

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

function Home() {
  let [pizza, setPizza] = useQueryParam<Pizza>("pizza");
    let pizza: Pizza = {
      toppings: formData.getAll("toppings") as string[],
      crust: formData.get("crust") as string,
      extraSauce: formData.get("extraSauce") === "on",
    };
    setPizza(pizza, { replace: true });
  }
    
  function useQueryParam<T>(
  key: string
): [T | undefined, (newQuery: T, options?: NavigateOptions) => void] {
  let [searchParams, setSearchParams] = useSearchParams();
  let paramValue = searchParams.get(key);

  let value = React.useMemo(() => JSURL.parse(paramValue), [paramValue]);

  let setValue = React.useCallback(
    (newValue: T, options?: NavigateOptions) => {
      let newSearchParams = new URLSearchParams(searchParams);
      newSearchParams.set(key, JSURL.stringify(newValue));
      setSearchParams(newSearchParams, options);
    },
    [key, searchParams, setSearchParams]
  );

  return [value, setValue];
}

懒加载 lazy-loading

  • 使用React.lazy() 和React.sequence去实现懒加载
const About = React.lazy(() => import("./pages/About")); //使用lazy去包裹回调
const Dashboard = React.lazy(() => import("./pages/Dashboard"));

<Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route
            path="about"
            element={
              <React.Suspense fallback={<>...</>}>
                <About />
              </React.Suspense>
            }
          />
        </Route>
      </Routes>

渲染modal

  • 使用自带的Dialog组件

  • 使用useNavigate 编程式导航

  • 使用可选的第二个参数传递一个To值(与 相同的类型)或{ replace, state }

  • 在历史堆栈中传递您想要进入的增量。例如,navigate(-1)相当于点击后退按钮。

  <Routes location={state?.backgroundLocation || location}>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="gallery" element={<Gallery />} />
          <Route path="/img/:id" element={<ImageView />} />
          <Route path="*" element={<NoMatch />} />
        </Route>
      </Routes>

      {/* Show the modal when a `backgroundLocation` is set */}
      {state?.backgroundLocation && (
        <Routes>
          <Route path="/img/:id" element={<Modal />} />
        </Routes>
      )}


function Modal() {
  let navigate = useNavigate();
  let { id } = useParams<"id">();
  let image = getImageById(Number(id));
  let buttonRef = React.useRef<HTMLButtonElement>(null);

  function onDismiss() {
    navigate(-1);
  }

  if (!image) return null;

  return (
    <Dialog
      aria-labelledby="label"
      onDismiss={onDismiss}
      initialFocusRef={buttonRef}
    >
      <div
        style={{
          display: "grid",
          justifyContent: "center",
          padding: "8px 8px",
        }}
      >
        <h1 id="label" style={{ margin: 0 }}>
          {image.title}
        </h1>
        <img
          style={{
            margin: "16px 0",
            borderRadius: "8px",
            width: "100%",
            height: "auto",
          }}
          width={400}
          height={400}
          src={image.src}
          alt=""
        />
        <button
          style={{ display: "block" }}
          ref={buttonRef}
          onClick={onDismiss}
        >
          Close
        </button>
      </div>
    </Dialog>
  );
}

multil - app的使用

  • 使用express,和vite来结合搭建app,来转换路由对应的app的index.html文件,该文件会下载对应app的react app 启动文件

package.json启动命令配置

"scripts": {
  "dev": "cross-env NODE_ENV=development node server.js",
  "build": "vite build",
  "start": "cross-env NODE_ENV=production node server.js",
  "debug": "node --inspect-brk server.js"
  },

server.js 服务器启动脚本

let path = require("path");
let fsp = require("fs/promises");
let express = require("express");

let isProduction = process.env.NODE_ENV === "production";

async function createServer() {
  let app = express();//创建一个express实例
  /**
   * @type {import("vite").ViteDevServer}
   */
  let vite;

  if (!isProduction) {
    vite = await require("vite").createServer({//创建一个 vite server
      root: process.cwd(),
      server: { middlewareMode: "ssr" },
    });

    app.use(vite.middlewares);//放进express 作为打包中间间使用
  } else {
    app.use(require("compression")());
    app.use(express.static(path.join(__dirname, "dist")));
  }

  app.use("*", async (req, res) => {//定义所有路由都要经过的中间件
    let url = req.originalUrl;

    // Use a separate HTML file for the "Inbox" app.
    let appDirectory = url.startsWith("/inbox") ? "inbox" : "";
    let htmlFileToLoad;

    if (isProduction) {
      htmlFileToLoad = path.join("dist", appDirectory, "index.html");//拼接index文件路径
    } else {
      htmlFileToLoad = path.join(appDirectory, "index.html");
    }

    try {
      let html = await fsp.readFile(//提取文件对应内容
        path.join(__dirname, htmlFileToLoad),
        "utf8"
      );

      if (!isProduction) {
        html = await vite.transformIndexHtml(req.url, html);//使用vite进行转换
      }

      res.setHeader("Content-Type", "text/html");
      return res.status(200).end(html);//通过response进行返回
    } catch (error) {
      if (!isProduction) vite.ssrFixStacktrace(error);
      console.log(error.stack);
      return res.status(500).end(error.stack);
    }
  });

  return app;
}

createServer().then((app) => {
  app.listen(3000, () => {
    console.log("HTTP server is running at http://localhost:3000");
  });
});

Home app 的主要配置

<Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="about" element={<About />} />
        <Route path="*" element={<NoMatch />} />
      </Route>
    </Routes>

//<Layout>
function Layout() {
  return (
       <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            {/* Use a normal <a> when linking to the "Inbox" app so the browser
                does a full document reload, which is what we want when exiting
                this app and entering another so we execute its entry point in
                inbox/main.jsx. */}
            <a href="/inbox">Inbox</a>
          </li>
        </ul>
      </nav>

      <hr />

      <Outlet />
  );
}

//indox app
 <ul>
          <li>
            {/* Using a normal link here will cause the browser to reload the
                document when following this link, which is exactly what we want
                when transitioning to the "Home" app so we execute its entry
                point (see home/main.jsx). */}
            <a href="/">Home</a>
          </li>
          <li>
            <a href="/about">About</a>
          </li>
          <li>
            <Link to="/">Inbox</Link>
          </li>
        </ul>

使用RouteObjects来配置路由

export default function App() {
  let routes: RouteObject[] = [
    {
      path: "/",
      element: <Layout />,
      children: [
        { index: true, element: <Home /> },
        {
          path: "/courses",
          element: <Courses />,
          children: [
            { index: true, element: <CoursesIndex /> },
            { path: "/courses/:id", element: <Course /> },
          ],
        },
        { path: "*", element: <NoMatch /> },
      ],
    },
  ];

  // The useRoutes() hook allows you to define your routes as JavaScript objects
  // instead of <Routes> and <Route> elements. This is really just a style
  // preference for those who prefer to not use JSX for their routes config.
  let element = useRoutes(routes);

  return (
    <div>
      <h1>Route Objects Example</h1>
      {element}
    </div>
  );
}

SSR服务端渲染

官方 reactrouterdotcom.fly.dev/docs/en/v6/…

sever.js

let path = require("path");
let fsp = require("fs/promises");
let express = require("express");

let root = process.cwd();
let isProduction = process.env.NODE_ENV === "production";

function resolve(p) {
  return path.resolve(__dirname, p);
}

async function createServer() {
  let app = express();
  /**
   * @type {import('vite').ViteDevServer}
   */
  let vite;

  if (!isProduction) {
    vite = await require("vite").createServer({
      root,
      server: { middlewareMode: "ssr" },
    });

    app.use(vite.middlewares);
  } else {
    app.use(require("compression")());
    app.use(express.static(resolve("dist/client")));
  }

  app.use("*", async (req, res) => {
    let url = req.originalUrl;

    try {
      let template;
      let render;

      if (!isProduction) {
        template = await fsp.readFile(resolve("index.html"), "utf8");
        template = await vite.transformIndexHtml(url, template);
        render = await vite
          .ssrLoadModule("src/entry.server.tsx")
          .then((m) => m.render);//提取服务端返回的内容
      } else {
        template = await fsp.readFile(
          resolve("dist/client/index.html"),
          "utf8"
        );
        render = require(resolve("dist/server/entry.server.js")).render;
      }
      console.log(render(url))
      let html = template.replace("<!--app-html-->", render(url));//替换
      res.setHeader("Content-Type", "text/html");
      return res.status(200).end(html);
    } catch (error) {
      if (!isProduction) {
        vite.ssrFixStacktrace(error);
      }
      console.log(error.stack);
      res.status(500).end(error.stack);
    }
  });

  return app;
}

createServer().then((app) => {
  app.listen(3000, () => {
    console.log("HTTP server is running at http://localhost:3000");
  });
});

客户端

import * as React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";

import "./index.css";
import App from "./App";

ReactDOM.hydrate(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("app")
);

server端

import * as React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";

import App from "./App";

export function render(url: string) {
  return ReactDOMServer.renderToString(
    <React.StrictMode>
      <StaticRouter location={url}>
        <App />
      </StaticRouter>
    </React.StrictMode>
  );
}