造轮子就是应用核心原理 + 周边功能的堆砌,所以学习成熟库的源码往往会受到非核心代码干扰,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 地址变化时(无论是用户触发还是自己的
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
了,他俩做的事情都是跳转,有如下区别:
- 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>
默认访问记录行为你也得花高成本补上,所以错误的设计方向会导致事倍功半,甚至无法实现。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=17937,转载请注明出处。
评论0