源码解读/241.精读《react-snippets - Router 源码》.md
造轮子就是应用核心原理 + 周边功能的堆砌,所以学习成熟库的源码往往会受到非核心代码干扰,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 要做哪些事情?
navigate Link 触发),渲染新 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 了,他俩做的事情都是跳转,有如下区别:
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 的判断值得学习。
从这个小轮子中可以学习到一下几个经验:
pushState 无法触发 popstate 那段,直接把 popstate 代码复用过来,或者自己造一个状态沟通就太 low 了,用浏览器 API 模拟事件触发,既轻量,又符合逻辑,因为你要做的就是触发 popstate 行为,而非只是更新渲染组件这个动作,万一以后再有监听 popstate 的地方,你的触发逻辑就能很自然的应用到那儿。Link 的实现是基于 <a> 标签拓展的,如果采用自定义 <span> 标签,不仅要补齐样式上的差异,还要自己实现 ctrl 后打开新 tab 的行为,甚至 <a> 默认访问记录行为你也得花高成本补上,所以错误的设计方向会导致事倍功半,甚至无法实现。讨论地址是:精读《react-snippets - Router 源码》· Issue #418 · dt-fe/weekly
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)