前天学习了Link组件源码, 今天再来看一下与之相似的NavLink

一、更新


[2019-4-21]

Changed

  • 改进文章排版👌

二、前言


经过上一篇文章Link的战火洗礼, 可以了解到, Link作为沟通react-routerreact之间的桥梁, 经历click -> url变化 -> history(onListen) -> Switch -> Route render的过程, 据此, 有关Link的面试题便可迎刃而解.

正式开始分析之前, 先来回想一下我们平时是如何使用NavLink的?

PS: 传入对应的activeStyle或者activeClass, 点击之后, 如果对应的组件成功渲染, 则该NavLink的样式也会改变.

带着平时使用的思路去分析源码, 会又事半功倍的效果.

OK, 正式开始对NavLink的探索.

三、细说


先来看一下NavLink的大致结构, 可以看到, 其实NavLink是一个functional组件, functional相较于classes的优势很多:

  • 高度可拓展
  • 易于优化重构
  • 渲染性能较优

react v16.7之后的hooks加持之下, 可用性更是飞步提升.

接着来看一下NavLink接收了哪些props?

1
2
3
4
5
6
7
8
9
10
11
12
13
function NavLink({
"aria-current": ariaCurrent = "page",
activeClassName = "active",
activeStyle,
className: classNameProp,
exact,
isActive: isActiveProp,
location,
strict,
style: styleProp,
to,
...rest
}) {...}

对应的props的功能如下表所示:

aria-current 残障人士专用, 可参考这里
activeClassName 匹配时添加的类名
activeStyle 匹配时添加的样式
className NavLink的类名, 会附加到Link
exact 是否实行完全匹配
strict 是否实行严格匹配
isActive 自行计算高亮条件
location 与当前to进行比较的location, 默认是context.location
style 默认样式
to 跳转的url
…rest 额外参数

接着, 可以看到下面:

1
2
3
4
const path = typeof to === "object" ? to.pathname : to;

// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
const escapedPath = path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");

值得注意的是, 这里的excapedPath很奇怪, 一直想不通为什么要进行这个操作? 怀着好奇的心理点开注释的链接, 可以看到:

excapedPath

该函数会将字符串中的特殊字符进行转义处理, 那么这到底有咩用? 假设有一个特殊的path序列是这样的:

1
const path: string = '/user\duan/profile/se+cr*et';

用户想传递该path, 该path中含有特殊字符, 那么escaped会将转义字符再此进行转义, 便于传递.

接着往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
return (
<Route
path={escapedPath}
exact={exact}
strict={strict}
location={location}
children={({ location, match }) => {
const isActive = !!(isActiveProp
? isActiveProp(match, location)
: match);

const className = isActive
? joinClassnames(classNameProp, activeClassName)
: classNameProp;
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;

return (
<Link
aria-current={(isActive && ariaCurrent) || null}
className={className}
style={style}
to={to}
{...rest}
/>
);
}}
/>
);

通过一个Route组件包裹, 根据其的match判断是否active, 然后在Route中返回对应的Link组件, 将处理后的rest等参数传递给Link组件.

这一步, 我刚开始看了好几遍都没看懂, 很难理解, 难点在于为什么要用一个Route??? 它的本质是为了获取match, 根据match匹配成功与否, 进行动态添加样式, 理解了这点, 就没啥可说的了.

另外, 还有一种思路 —— 通过获取LinkinnerRef, 然后通过matchPath自行比对location.pathname === to, 根据判断的结果, 直接操作DOM, 这么做也是可以的, 不过值得思考的是, 这不正好违背了Route组件的设计原则:

PS: 匹配路径规则, 渲染组件

既然有更专业的Route来帮我们作这件事, 何乐而不为呢? 为什么还要再次比对呢? 得不偿失.

🆗, NavLink的源码思路解析大致已经完成, 接下来继续完善自己的yyg-react-router-dom库.

四、实践


老样子, 首先定义我们所需的一切props, 具体的props可以看这里.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/yyg-react-router-dom/components/NavLink.tsx
export interface extends React.HtmlHTMLAttributes<HTMLAnchorElement> {
to: string | LocationDescriptorObject<{
[key: string]: any,
}>;
activeClassName?: string;
activeStyle?: React.HtmlHTMLAttributes<HTMLAnchorElement>;
exact?: boolean;
strict?: boolean;
sensitive?: boolean;
isActive?: (
data: {
location: Location,
match: IStaticMatchParams,
},
) => boolean;
location?: Location;
ariaCurrent?: ARIA_CURRENT;
children?: React.ReactChild;
};

接着, 到了下面, 根据判断match是否为null来断定path是否匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
return (
...
<Route
children={(p) => {
const isActive = !!(p.match);
const activeStyles = isActive ? activeStyle : {};
const activeClassNames = isActive ? activeClassName : '';

return (
<Link
{...rests}
to={path}
style={activeStyles}
className={activeClassNames}
>{children}</Link>
);
}}
>
);

注意! 上面有一个值得注意的点, 那就是Routechildrenrender是不同的, 两者的具体差别可以看这里, 所以, 这里只能使用children来渲染.

五、测试


完成了自己的yyg-react-router-dom库, 可以来做一个简单的测试.

PS: 当然, 测试用例是随意的

打开App.tsx, 修改render中的测试逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//src/App.tsx
...
return (
<div>
<BrowserRouter>
<Switch>
<Route
path={"/test"}
component={One}
/>
</Switch>
</BrowserRouter>
</div>
);
...

然后, 在vscode的指引下, 进入One.tsx组件, 同样修改render中的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
return (
<div>
<h1>Page One</h1>
<ul>
<li>
<Link
to="/test/home"
>
Home
</Link>
</li>
<li>
<NavLink
to={{
pathname: '/test/product',
state: {
id: '19980808',
name: 'duan',
},
}}
activeStyle={{
color: 'red',
}}
>Product</NavLink>
</li>
<li>
<NavLink
to="/test/contact"
activeStyle={{
color: 'red',
}}
>
Contact
</NavLink>

</li>
</ul>
<Two render={({ point }) => (
<div>
<div>{point.x}</div>
<div>{point.y}</div>
</div>
)} />

<div>
<Route exact path="/test/home" component={Three} />
<Route exact path="/test/product" component={Four} />
<Route exact path="/test/contact" component={Five} />
</div>
</div>
);

可以看到, 总共有三个锚点, 第一个为Link, 为了与NavLink的效果加以区分, 然后在最下方放置需要router-view, 也就是需要渲染的组件. 大致结构与在项目中使用的差不多的…

接着, 在浏览器中可以看到效果:

测试NavLink

点击Home按钮, 视图变了, 但是链接颜色没变化? 当然了, 因为这只是普通的Link组件, 你想它有啥效果? 接着看下面两个, prefect, 两个锚点样式随着url变化而改变.

此时, 对于NavLink的测试工作已经完美结束了

六、源码


源码已上传, 点这里

七、总结


PS: 路漫漫其修远兮, 吾必反复求其知

最后, 一张思维脑图来结束今天的源码学习

NavLink思维图解