造轮子就是应用核心原理 + 周边功能的堆砌,所以学习成熟库的源码往往会受到非核心代码干扰,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>
默认访问记录行为你也得花高成本补上,所以错误的设计方向会导致事倍功半,甚至无法实现。本文作者:前端小毛
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!