Skip to content

Commit 33e9a77

Browse files
committed
241
1 parent fb492c7 commit 33e9a77

File tree

2 files changed

+131
-1
lines changed

2 files changed

+131
-1
lines changed

readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
前端界的好文精读,每周更新!
88

9-
最新精读:<a href="./前沿技术/240.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20useEvent%20RFC%E3%80%8B.md">240.精读《React useEvent RFC》</a>
9+
最新精读:<a href="./源码解读/241.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-snippets%20-%20Router%20%E6%BA%90%E7%A0%81%E3%80%8B.md">241.精读《react-snippets - Router 源码》</a>
1010

1111
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
1212

@@ -246,6 +246,7 @@
246246
- <a href="./源码解读/156.%20%E7%B2%BE%E8%AF%BB%E3%80%8Areact-intersection-observer%20%E6%BA%90%E7%A0%81%E3%80%8B.md">156. 精读《react-intersection-observer 源码》</a>
247247
- <a href="./源码解读/227.%20%E7%B2%BE%E8%AF%BB%E3%80%8Azustand%20%E6%BA%90%E7%A0%81%E3%80%8B.md">227. 精读《zustand 源码》</a>
248248
- <a href="./源码解读/229.%E7%B2%BE%E8%AF%BB%E3%80%8Avue-lit%20%E6%BA%90%E7%A0%81%E3%80%8B.md">229.精读《vue-lit 源码》</a>
249+
- <a href="./源码解读/241.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-snippets%20-%20Router%20%E6%BA%90%E7%A0%81%E3%80%8B.md">241.精读《react-snippets - Router 源码》</a>
249250

250251
### 商业思考
251252

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
造轮子就是应用核心原理 + 周边功能的堆砌,所以学习成熟库的源码往往会受到非核心代码干扰,[Router](https://github.com/ashok-khanna/react-snippets/blob/main/Router.js) 这个 repo 用不到 100 行源码实现了 React Router 核心机制,很适合用来学习。
2+
3+
## 精读
4+
5+
[Router](https://github.com/ashok-khanna/react-snippets/blob/main/Router.js) 快速实现了 React Router 3 个核心 API:`Router``navigate``Link`,下面列出基本用法,配合理解源码实现会更方便:
6+
7+
```tsx
8+
const App = () => (
9+
<Router
10+
routes={[
11+
{ path: '/home', component: <Home /> },
12+
{ path: '/articles', component: <Articles /> }
13+
]}
14+
/>
15+
)
16+
17+
const Home = () => (
18+
<div>
19+
home, <Link href="/articles">go articles</Link>,
20+
<span onClick={() => navigate('/details')}>or jump to details</span>
21+
</div>
22+
)
23+
```
24+
25+
首先看 `Router` 的实现,在看代码之前,思考下 `Router` 要做哪些事情?
26+
27+
- 接收 routes 参数,根据当前 url 地址判断渲染哪个组件。
28+
- 当 url 地址变化时(无论是用户触发还是自己的 `navigate` `Link` 触发),渲染新 url 对应的组件。
29+
30+
所以 `Router` 是一个路由渲染分配器与 url 监听器:
31+
32+
```tsx
33+
export default function Router ({ routes }) {
34+
// 存储当前 url path,方便其变化时引发自身重渲染,以返回新的 url 对应的组件
35+
const [currentPath, setCurrentPath] = useState(window.location.pathname);
36+
37+
useEffect(() => {
38+
const onLocationChange = () => {
39+
// 将 url path 更新到当前数据流中,触发自身重渲染
40+
setCurrentPath(window.location.pathname);
41+
}
42+
43+
// 监听 popstate 事件,该事件由用户点击浏览器前进/后退时触发
44+
window.addEventListener('popstate', onLocationChange);
45+
46+
return () => window.removeEventListener('popstate', onLocationChange)
47+
}, [])
48+
49+
// 找到匹配当前 url 路径的组件并渲染
50+
return routes.find(({ path, component }) => path === currentPath)?.component
51+
}
52+
```
53+
54+
最后一段代码看似每次都执行 `find` 有一定性能损耗,但其实根据 `Router` 一般在最根节点的特性,该函数很少因父组件重渲染而触发渲染,所以性能不用太担心。
55+
56+
但如果考虑做一个完整的 React Router 组件库,考虑了更复杂的嵌套 API,即 `Router``Router` 后,不仅监听方式要变化,还需要将命中的组件缓存下来,需要考虑的点会逐渐变多。
57+
58+
下面该实现 `navigate` `Link` 了,他俩做的事情都是跳转,有如下区别:
59+
60+
1. API 调用方式不同,`navigate` 是调用式函数,而 `Link` 是一个内置 `navigate` 能力的 `a` 标签。
61+
2. `Link` 其实还有一种按住 `ctrl` 后打开新 tab 的跳转模式,该模式由浏览器对 `a` 标签默认行为完成。
62+
63+
所以 `Link` 更复杂一些,我们先实现 `navigate`,再实现 `Link` 时就可以复用它了。
64+
65+
既然 `Router` 已经监听 `popstate` 事件,我们显然想到的是触发 url 变化后,让 `popstate` 捕获,自动触发后续跳转逻辑。但可惜的是,我们要做的 React Router 需要实现单页跳转逻辑,而单页跳转的 API `history.pushState` 并不会触发 `popstate`,为了让实现更优雅,我们可以在 `pushState` 后手动触发 `popstate` 事件,如源码所示:
66+
67+
```tsx
68+
export function navigate (href) {
69+
// 用 pushState 直接刷新 url,而不触发真正的浏览器跳转
70+
window.history.pushState({}, "", href);
71+
72+
// 手动触发一次 popstate,让 Route 组件监听并触发 onLocationChange
73+
const navEvent = new PopStateEvent('popstate');
74+
window.dispatchEvent(navEvent);
75+
}
76+
```
77+
78+
接下来实现 `Link` 就很简单了,有几个考虑点:
79+
80+
1. 返回一个正常的 `<a>` 标签。
81+
2. 因为正常 `<a>` 点击后就发生网页刷新而不是单页跳转,所以点击时要阻止默认行为,换成我们的 `navigate`(源码里没做这个抽象,笔者稍微优化了下)。
82+
3. 但按住 `ctrl` 时又要打开新 tab,此时用默认 `<a>` 标签行为就行,所以此时不要阻止默认行为,也不要继续执行 `navigate`,因为这个 url 变化不会作用于当前 tab。
83+
84+
```tsx
85+
export function Link ({ className, href, children }) {
86+
const onClick = (event) => {
87+
// mac 的 meta or windows 的 ctrl 都会打开新 tab
88+
// 所以此时不做定制处理,直接 return 用原生行为即可
89+
if (event.metaKey || event.ctrlKey) {
90+
return;
91+
}
92+
93+
// 否则禁用原生跳转
94+
event.preventDefault();
95+
96+
// 做一次单页跳转
97+
navigate(href)
98+
};
99+
100+
return (
101+
<a className={className} href={href} onClick={onClick}>
102+
{children}
103+
</a>
104+
);
105+
};
106+
```
107+
108+
这样的设计,既能兼顾 `<a>` 标签默认行为,又能在点击时优化为单页跳转,里面对 `preventDefault``metaKey` 的判断值得学习。
109+
110+
## 总结
111+
112+
从这个小轮子中可以学习到一下几个经验:
113+
114+
- 造轮子之前先想好使用 API,根据使用 API 反推实现,会让你的设计更有全局观。
115+
- 实现 API 时,先思考 API 之间的关系,能复用的就提前设计好复用关系,这样巧妙的关联设计能为以后维护减少很多麻烦。
116+
- 即便代码无法复用的地方,也要尽量做到逻辑复用。比如 `pushState` 无法触发 `popstate` 那段,直接把 `popstate` 代码复用过来,或者自己造一个状态沟通就太 low 了,用浏览器 API 模拟事件触发,既轻量,又符合逻辑,因为你要做的就是触发 `popstate` 行为,而非只是更新渲染组件这个动作,万一以后再有监听 `popstate` 的地方,你的触发逻辑就能很自然的应用到那儿。
117+
- 尽量在原生能力上拓展,而不是用自定义方法补齐原生能力。比如 `Link` 的实现是基于 `<a>` 标签拓展的,如果采用自定义 `<span>` 标签,不仅要补齐样式上的差异,还要自己实现 `ctrl` 后打开新 tab 的行为,甚至 `<a>` 默认访问记录行为你也得花高成本补上,所以错误的设计方向会导致事半功倍,甚至无法实现。
118+
119+
> 讨论地址是:[精读《react-snippets - Router 源码》· Issue #418 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/418)
120+
121+
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
122+
123+
> 关注 **前端精读微信公众号**
124+
125+
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
126+
127+
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
128+
129+

0 commit comments

Comments
 (0)