React Router 中實現嵌套路由和動態路由的示例

React Router 是一個用於 React 應用的路由庫,它提供瞭一種簡單的方式來將 URL 與組件匹配起來。React Router 實現瞭以下幾個主要的概念:

  • Router: 它提供瞭應用程序的基本路由功能。
  • Routes: 它定義瞭 URL 和組件之間的映射關系。
  • Link: 它提供瞭一種方便的方式來在應用程序中導航。
  • Switch: 它用於確保隻有一個路由能夠匹配當前的 URL。
  • createBrowserHistory: 它用於創建一個 HTML5 History API 的實例。

下面,我們將深入探討 React Router 的實現原理。我們將首先討論 Router 組件的實現,然後討論 Routes 組件的實現,最後討論 Link 組件的實現。

Router 組件的實現

Router 組件是 React Router 庫的核心組件,它提供瞭應用程序的基本路由功能。以下是 Router 組件的簡化版實現代碼:

const Router = ({ children }) => {
  const [location, setLocation] = useState(window.location.pathname);
  useEffect(() => {
    const handlePopState = () => setLocation(window.location.pathname);
    window.addEventListener('popstate', handlePopState);
    return () => window.removeEventListener('popstate', handlePopState);
  }, []);
  return <RouterContext.Provider value={{ location }}>{children}</RouterContext.Provider>;
};

在上面的代碼中,我們首先定義瞭一個 Router 組件。它接受一個 children 屬性,這個屬性是我們的應用程序的根組件。然後,我們使用 useState Hook 來創建瞭一個名為 location 的狀態變量。它將用於跟蹤當前的 URL。我們將使用 setLocation 函數來更新這個狀態變量。

接下來,我們使用 useEffect Hook 來註冊瞭一個監聽 popstate 事件的函數。當用戶點擊瀏覽器的“前進”或“後退”按鈕時,會觸發 popstate 事件。在這種情況下,我們會更新 location 狀態變量以反映新的 URL。

最後,我們使用 RouterContext.Provider 組件將 location 狀態變量傳遞給它的子組件。

Routes 組件的實現

Routes 組件用於定義 URL 和組件之間的映射關系。以下是 Routes 組件的簡化版實現代碼:

const Routes = ({ children }) => {
  const { location } = useContext(RouterContext);
  return children.find((child) => matchPath(location, child.props)) || null;
};

在上面的代碼中,我們首先定義瞭一個 Routes 組件。它接受一個 children 屬性,這個屬性是一個包含我們應用程序的所有路由的組件列表。然後,我們使用 useContext Hook 來獲取 location 變量,這個變量是從 Router 組件中傳遞過來的。

接下來,我們使用 find 函數在 children 列表中查找第一個匹配當前 URL 的路由。我們使用 matchPath 函數來比較當前 URL 和路由的 path 屬性。如果找到瞭匹配的路由,則返回這個路由對應的組件。否則,返回 null

matchPath 函數是一個用於比較 URL 和路由 path 屬性的函數。以下是 matchPath 函數的簡化版實現代碼:

const matchPath = (pathname, { path }) => {
  const segments = pathname.split('/').filter(Boolean);
  const parts = path.split('/').filter(Boolean);
  if (segments.length !== parts.length) return false;
  const params = {};
  for (let i = 0; i < parts.length; i++) {
    const isParam = parts[i].startsWith(':');
    if (isParam) {
      const paramName = parts[i].slice(1);
      const paramValue = segments[i];
      params[paramName] = paramValue;
    } else if (segments[i] !== parts[i]) {
      return false;
    }
  }
  return { params };
};

在上面的代碼中,我們首先定義瞭一個 matchPath 函數。它接受兩個參數:pathname 是當前 URL 的路徑部分,{ path } 是路由組件的 path 屬性。

然後,我們將 URL 和路由 path 屬性分別拆分成段。我們使用 filter(Boolean) 來過濾掉空的段。接著,我們比較 URL 的段數和路由的段數是否相等。如果它們不相等,則說明它們無法匹配,我們返回 false

如果它們的段數相等,則說明它們可能是匹配的。接著,我們創建一個空對象 params,它將用於存儲 URL 參數的鍵值對。然後,我們遍歷路由的每個段,如果這個段是一個參數(即以冒號開頭),則將對應的 URL 段存儲到 params 對象中。否則,如果這個段不是參數且與 URL 的對應段不相等,則說明它們無法匹配,我們返回 false

最後,如果 URL 和路由能夠匹配,則返回一個包含 URL 參數的對象。否則,返回 false

Link 組件的實現

Link 組件用於在應用程序中導航。以下是 Link 組件的簡化版實現代碼:

const Link = ({ to, ...rest }) => (
  <a href={to} onClick={(event) => {
    event.preventDefault();
    history.push(to);
  }} {...rest} />
);

在上面的代碼中,我們首先定義瞭一個 Link 組件。它接受一個 to 屬性,這個屬性是一個指向我們想要導航到的 URL 的字符串。接著,我們使用 preventDefault 函數阻止默認的鏈接行為,並使用 history.push 函數將 URL 添加到歷史記錄中。最後,我們將其他傳遞給 Link 組件的屬性通過 spread 運算符傳遞給 <a> 元素。

Switch組件的實現

Switch 組件是 React Router 中非常重要的一部分,它用於確保隻有一個路由能夠匹配當前的 URL。下面是 Switch 組件的簡化版實現代碼:

const Switch = ({ children }) => {
  const [match, setMatch] = useState(false);
  useEffect(() => {
    // 遍歷所有子元素,找到第一個與當前 URL 匹配的 Route 組件
    React.Children.forEach(children, (child) => {
      if (!match && React.isValidElement(child) && child.type === Route) {
        const { path, exact, strict, sensitive } = child.props;
        const match = matchPath(window.location.pathname, {
          path,
          exact,
          strict,
          sensitive,
        });
        if (match) {
          setMatch(true);
        }
      }
    });
  }, [children, match]);
  // 返回第一個匹配的 Route 組件
  return React.Children.toArray(children).find((child) => {
    return match && React.isValidElement(child) && child.type === Route;
  }) || null;
};

這個 Switch 組件的實現方式非常簡單。它使用 useStateuseEffect 鉤子來維護一個 match 狀態,用於表示當前 URL 是否匹配瞭任何一個子 Route 組件。在 useEffect 鉤子中,它遍歷所有子元素,找到第一個與當前 URL 匹配的 Route 組件,然後設置 match 狀態為 true。在返回值中,它再次遍歷所有子元素,找到第一個匹配的 Route 組件,然後返回它。如果沒有匹配的 Route 組件,就返回 null

Switch 組件的作用是確保隻有一個路由能夠匹配當前的 URL。這樣做的好處是可以避免多個路由同時匹配同一個 URL,從而導致頁面出現多個組件的情況。例如,在下面的代碼中,如果沒有 Switch 組件,HomePageAboutPage 兩個組件都會渲染出來:

<Route path="/" exact component={HomePage} />
<Route path="/about" component={AboutPage} />

而加上 Switch 組件之後,隻會渲染第一個匹配的路由,因此隻有 HomePage 組件會被渲染。

<Switch>
  <Route path="/" exact component={HomePage} />
  <Route path="/about" component={AboutPage} />
</Switch>

createBrowserHistory 函數實現

下面是一個簡化版的 createBrowserHistory 函數,它可以用於創建一個支持 HTML5 歷史記錄 API 的瀏覽器 history 對象:

在這裡,我們引入瞭 history 對象。history 對象是一個管理應用程序歷史記錄的 JavaScript 對象,它可以用於導航和監聽 URL 的變化。在 React Router 中,history 對象可以通過使用 useHistory Hook 或將 history 對象作為 props 傳遞給組件來獲取。

const createBrowserHistory = () => {
  let listeners = [];
  let location = {
    pathname: window.location.pathname,
    search: window.location.search,
    hash: window.location.hash,
  };
  const push = (pathname) => {
    window.history.pushState({}, '', pathname);
    location = { ...location, pathname };
    listeners.forEach(listener => listener(location));
  };
  window.addEventListener('popstate', () => {
    location = {
      pathname: window.location.pathname,
      search: window.location.search,
      hash: window.location.hash,
    };
    listeners.forEach(listener => listener(location));
  });
  return {
    get location() {
      return location;
    },
    push,
    listen(listener) {
      listeners.push(listener);
      return () => {
        listeners = listeners.filter(l => l !== listener);
      };
    },
  };
};

在上面的代碼中,我們首先定義瞭一個 createBrowserHistory 函數,它用於創建一個支持 HTML5 歷史記錄 API 的瀏覽器 history 對象。該函數返回一個對象,其中包含三個方法:get location()push(pathname)listen(listener)

get location() 方法返回當前 location 對象,該對象包含 pathnamesearchhash 屬性,分別對應當前 URL 的路徑部分、查詢參數和哈希部分。

push(pathname) 方法用於將指定的 pathname 添加到歷史記錄中,並觸發所有已註冊的監聽器。

listen(listener) 方法用於註冊一個 location 變化監聽器,並返回一個函數,該函數用於取消該監聽器的註冊。

在 React Router 中,我們可以使用 createBrowserHistory 函數創建一個瀏覽器 history 對象,並將其作為 Router 組件的 history 屬性傳遞。這樣,我們就可以在整個應用程序中使用相同的 history 對象,以便實現統一的 URL 管理和導航行為。

下面是一個簡化版的 Router 組件的實現,它使用瞭 createBrowserHistory 函數創建瞭一個瀏覽器 history 對象,並將其作為 Router 組件的 history 屬性傳遞給子組件:

const Router = ({ children }) => {
  const [location, setLocation] = useState(history.location);
  useEffect(() => {
    const unlisten = history.listen((newLocation) => {
      setLocation(newLocation);
    });
    return () => {
      unlisten();
    };
  }, []);
  return (
    <RouterContext.Provider value={{ location }}>
      {children}
    </RouterContext.Provider>
  );
};
const App = () => (
  <Router>
    <div>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Switch>
        <Route path="/about">
          <About />
        </Route>
        <Route path="/">
          <Home />
        </Route>
      </Switch>
    </div>
  </Router>
);

在上面的代碼中,我們首先定義瞭一個 Router 組件,它接受一個 children 屬性,這個屬性包含瞭所有的子組件。在 Router 組件中,我們使用 useState Hook 來跟蹤當前的 location 對象,並使用 useEffect Hook 來註冊一個 history 變化監聽器。每當 history 發生變化時,我們就可以更新 location 狀態,並將其傳遞給所有的子組件。

Router 組件中,我們還使用瞭一個 RouterContext 上下文,用於向子組件傳遞 location 狀態。我們可以通過在子組件中使用 useContext Hook 來訪問 location 狀態,從而實現根據 URL 渲染不同的組件的功能。

App 組件中,我們將所有的子組件包裹在 Router 組件中,並使用 LinkSwitchRoute 組件來定義應用程序的導航規則。每當用戶點擊 Link 組件時,我們就可以使用 history.push 函數將新的 URL 添加到歷史記錄中,並觸發 Router 組件中註冊的 location 變化監聽器。然後,Switch 組件會根據當前的 URL 匹配相應的 Route 組件,並渲染匹配的組件。這樣,我們就實現瞭一個簡單的路由系統。

希望這些代碼示例和註解能夠幫助你理解 React Router 的實現原理。當然,這隻是一個簡化版的實現,實際的 React Router 代碼更加復雜,包含瞭很多額外的功能和性能優化,比如動態路由、代碼分割、異步加載等等。如果你有興趣深入瞭解 React Router 的實現原理,建議閱讀官方文檔和源代碼。

總的來說,React Router 是一個非常強大和靈活的路由庫,它為 React 應用程序提供瞭豐富的導航和 URL 管理功能,能夠幫助我們構建復雜的單頁應用和多頁應用。

到此這篇關於React Router 中如何實現嵌套路由和動態路由的文章就介紹到這瞭,更多相關React Router 嵌套路由和動態路由內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: