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
组件的实现方式非常简单。它使用 useState
和 useEffect
钩子来维护一个 match
状态,用于表示当前 URL 是否匹配了任何一个子 Route
组件。在 useEffect
钩子中,它遍历所有子元素,找到第一个与当前 URL 匹配的 Route
组件,然后设置 match
状态为 true
。在返回值中,它再次遍历所有子元素,找到第一个匹配的 Route
组件,然后返回它。如果没有匹配的 Route
组件,就返回 null
。
Switch
组件的作用是确保只有一个路由能够匹配当前的 URL。这样做的好处是可以避免多个路由同时匹配同一个 URL,从而导致页面出现多个组件的情况。例如,在下面的代码中,如果没有 Switch
组件,HomePage
和 AboutPage
两个组件都会渲染出来:
<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
对象,该对象包含 pathname
、search
和 hash
属性,分别对应当前 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
组件中,并使用 Link
、Switch
和 Route
组件来定义应用程序的导航规则。每当用户点击 Link
组件时,我们就可以使用 history.push
函数将新的 URL 添加到历史记录中,并触发 Router
组件中注册的 location
变化监听器。然后,Switch
组件会根据当前的 URL 匹配相应的 Route
组件,并渲染匹配的组件。这样,我们就实现了一个简单的路由系统。
希望这些代码示例和注解能够帮助你理解 React Router 的实现原理。当然,这只是一个简化版的实现,实际的 React Router 代码更加复杂,包含了很多额外的功能和性能优化,比如动态路由、代码分割、异步加载等等。如果你有兴趣深入了解 React Router 的实现原理,建议阅读官方文档和源代码。
总的来说,React Router 是一个非常强大和灵活的路由库,它为 React 应用程序提供了丰富的导航和 URL 管理功能,能够帮助我们构建复杂的单页应用和多页应用。
原文链接:https://juejin.cn/post/7229909033896869946 作者:武文File