造轮子就是应用核心原理 + 周边功能的堆砌,所以学习成熟库的源码往往会受到非核心代码干扰,Router 这个 repo 用不到 100 行源码实现了 React Router 核心机制,很适合用来学习。
精读
Router 快速实现了 React Router 3 个核心 API:Router、navigate、Link,下面列出基本用法,配合理解源码实现会更方便:
const App = () => (
<Router
routes={[
{ path: '/home', component: <Home /> },
{ path: '/articles', component: <Articles /> }
]}
/>
)
const Home = () => (
<div>
home, <Link href="/articles">go articles</Link>,
<span onClick={() => navigate('/details')}>or jump to details</span>
</div>
)首先看 Router 的实现,在看代码之前,思考下 Router 要做哪些事情?
接收 routes 参数,根据当前 url 地址判断渲染哪个组件。
当 url 地址变化时(无论是用户触发还是自己的
navigateLink触发),渲染新 url 对应的组件。
所以 Router 是一个路由渲染分配器与 url 监听器:
export default function Router ({ routes }) {
// 存储当前 url path,方便其变化时引发自身重渲染,以返回新的 url 对应的组件
const [currentPath, setCurrentPath] = useState(window.location.pathname);
useEffect(() => {
const onLocationChange = () => {
// 将 url path 更新到当前数据流中,触发自身重渲染
setCurrentPath(window.location.pathname);
}
// 监听 popstate 事件,该事件由用户点击浏览器前进/后退时触发
window.addEventListener('popstate', onLocationChange);
return () => window.removeEventListener('popstate', onLocationChange)
}, [])
// 找到匹配当前 url 路径的组件并渲染
return routes.find(({ path, component }) => path === currentPath)?.component
}最后一段代码看似每次都执行 find 有一定性能损耗,但其实根据 Router 一般在最根节点的特性,该函数很少因父组件重渲染而触发渲染,所以性能不用太担心。
但如果考虑做一个完整的 React Router 组件库,考虑了更复杂的嵌套 API,即 Router 套 Router 后,不仅监听方式要变化,还需要将命中的组件缓存下来,需要考虑的点会逐渐变多。
下面该实现 navigate Link 了,他俩做的事情都是跳转,有如下区别:
API 调用方式不同,
navigate是调用式函数,而Link是一个内置navigate能力的a标签。Link其实还有一种按住ctrl后打开新 tab 的跳转模式,该模式由浏览器对a标签默认行为完成。
所以 Link 更复杂一些,我们先实现 navigate,再实现 Link 时就可以复用它了。
既然 Router 已经监听 popstate 事件,我们显然想到的是触发 url 变化后,让 popstate 捕获,自动触发后续跳转逻辑。但可惜的是,我们要做的 React Router 需要实现单页跳转逻辑,而单页跳转的 API history.pushState 并不会触发 popstate,为了让实现更优雅,我们可以在 pushState 后手动触发 popstate 事件,如源码所示:
export function navigate (href) {
// 用 pushState 直接刷新 url,而不触发真正的浏览器跳转
window.history.pushState({}, "", href);
// 手动触发一次 popstate,让 Route 组件监听并触发 onLocationChange
const navEvent = new PopStateEvent('popstate');
window.dispatchEvent(navEvent);
}接下来实现 Link 就很简单了,有几个考虑点:
返回一个正常的
<a>标签。因为正常
<a>点击后就发生网页刷新而不是单页跳转,所以点击时要阻止默认行为,换成我们的navigate(源码里没做这个抽象,笔者稍微优化了下)。但按住
ctrl时又要打开新 tab,此时用默认<a>标签行为就行,所以此时不要阻止默认行为,也不要继续执行navigate,因为这个 url 变化不会作用于当前 tab。
export function Link ({ className, href, children }) {
const onClick = (event) => {
// mac 的 meta or windows 的 ctrl 都会打开新 tab
// 所以此时不做定制处理,直接 return 用原生行为即可
if (event.metaKey || event.ctrlKey) {
return;
}
// 否则禁用原生跳转
event.preventDefault();
// 做一次单页跳转
navigate(href)
};
return (
<a className={className} href={href} onClick={onClick}>
{children}
</a>
);
};这样的设计,既能兼顾 <a> 标签默认行为,又能在点击时优化为单页跳转,里面对 preventDefault 与 metaKey 的判断值得学习。
总结
从这个小轮子中可以学习到一下几个经验:
造轮子之前先想好使用 API,根据使用 API 反推实现,会让你的设计更有全局观。
实现 API 时,先思考 API 之间的关系,能复用的就提前设计好复用关系,这样巧妙的关联设计能为以后维护减少很多麻烦。
即便代码无法复用的地方,也要尽量做到逻辑复用。比如
pushState无法触发popstate那段,直接把popstate代码复用过来,或者自己造一个状态沟通就太 low 了,用浏览器 API 模拟事件触发,既轻量,又符合逻辑,因为你要做的就是触发popstate行为,而非只是更新渲染组件这个动作,万一以后再有监听popstate的地方,你的触发逻辑就能很自然的应用到那儿。尽量在原生能力上拓展,而不是用自定义方法补齐原生能力。比如
Link的实现是基于<a>标签拓展的,如果采用自定义<span>标签,不仅要补齐样式上的差异,还要自己实现ctrl后打开新 tab 的行为,甚至<a>默认访问记录行为你也得花高成本补上,所以错误的设计方向会导致事半功倍,甚至无法实现。
讨论地址是:精读《react-snippets - Router 源码》· Issue #418 · dt-fe/weekly
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)
本文通过对react-snippets中Router源码的精读,解释了如何实现React Router的核心API——、和。文章详细阐述了如何监听URL变化并根据当前URL渲染组件,以及如何实现和的区别。最后,总结了从源码学习到的设计原则,如根据API设计、逻辑复用和原生能力拓展。
1116

被折叠的 条评论
为什么被折叠?



