背景
这周开始尝试阅读 Next.js 源码,有难度,但也很有意思。
对我来说,一个能够极大提高效率的技巧就是:每次先抛出一个自己感兴趣的小问题(多是关于实现原理的细节),然后阅读源码,尝试回答这个问题。解决疑问之后,再放下源码,抛出下一个感兴趣的问题(很可能是解决了上一个问题所引发的新问题),再阅读源码,如此循环往复。
这样的好处是,每次阅读源码都有一个清晰且易实现的目标(解答当前问题),不易陷入无关细节。如此反复数次,解决一个又一个问题,对实现原理的理解自然层层加深了。
首先从 Next.js 的 Link 与 Router 着手,因为感觉这一部分比较直观,好上手;而且还因为我刚写了一篇 一箭双雕:浅谈 Next.js 页面导航(page navigation)原理,和这个关系也比较大。
本篇涉及的源码文件:
packages/next/client/link.tsx # Link 的实现
注:本系列文章基于 Next.js v12.0.4 版本。
热身:a 链接 的 preventDefault
第一个问题非常简单。在上一篇文章 一箭双雕:浅谈 Next.js 页面导航(page navigation)原理 中我提到,Next.js 中每个 <Link />
下都有一个 <a>
元素,用户正常点击的时候 <a>
不会生效,但在 JS 禁用的时候(例如对于不允许 JS 的爬虫来说) <a>
就能派上用场了。
下面通过源码印证一下,正常情况下 Next.js 真的会让 <a>
失效。
我们平常使用的 <Link />
组件,其定义在 packages/next/client/link.tsx
的第 109 行。这里略去大部分无关代码,截取其中最重要的一段做注释:
代码片段 1:Link
// packages/next/client/link.tsx
// 第 109 行
function Link(props: React.PropsWithChildren<LinkProps>) {
// 前面代码略,从第 277 行开始
const childProps: {
onMouseEnter?: React.MouseEventHandler
onClick: React.MouseEventHandler
href?: string
ref?: any
} = {
ref: setRef,
// 这里是重点
onClick: (e: React.MouseEvent) => {
// 如果 child 本身有 onClick handler,则触发
if (child.props && typeof child.props.onClick === 'function') {
child.props.onClick(e)
}
// 正常情况下会执行 linkClicked
if (!e.defaultPrevented) {
linkClicked(e, router, href, as, replace, shallow, scroll, locale)
}
},
}
// 后面代码略
}
大家知道 ,涉及站内链接时,Next.js 官方文档上推荐的用法是:
<Link href="/some/route">
<a>Some text</a>
</Link>
那么上面源码中的 child
就对应这里的 <a>
。上面源码中的第一个逻辑是,“如果 child 本身有 onClick
handler,则触发”,不过在正常情况下,child 应该不会有 onClick
handler。
上面源码中的第二个逻辑就比较有意思了:如果 !e.defaultPrevented
,则执行 linkClicked
。 搜了一下 MDN 对 defaultPrevented 的解释,其实就是如果之前执行过 e.preventDefault()
,e.defaultPrevented
就会返回 true
,反之返回 false
。
那么这里的意思就是:如果之前没有执行过 e.preventDefault()
,则执行 linkClicked
。下面来看看完整的 linkClicked
函数:
代码片段 2:linkClicked
// packages/next/client/link.tsx
// 第 77 行
function linkClicked(
e: React.MouseEvent,
router: NextRouter,
href: string,
as: string,
replace?: boolean,
shallow?: boolean,
scroll?: boolean,
locale?: string | false
): void {
const { nodeName } = e.currentTarget
// 如果是站外链接,什么都不做,浏览器会根据 <a> 来跳转
if (nodeName === 'A' && (isModifiedEvent(e) || !isLocalURL(href))) {
// ignore click for browser’s default behavior
return
}
// 如果是站内链接,阻止浏览器默认的 <a> 跳转
e.preventDefault()
// avoid scroll for urls with anchor refs
if (scroll == null && as.indexOf('#') >= 0) {
scroll = false
}
// 这里切换 route
// replace state instead of push if prop is present
router[replace ? 'replace' : 'push'](href, as, {
shallow,
locale,
scroll,
})
}
可以看到,对于站内链接,linkClicked
确实使用 e.preventDefault()
阻止了浏览器默认的 <a>
跳转,然后调用 router
的相应方法,实现了 client-side navigation。
Link 下能否不加 a 链接?
下面来看另一个小问题:如上所述,官方文档推荐的用法是在 <Link />
下加入 <a>
,这就实现了 Next.js ”使用 JS 实现 client-side navigation、在 JS 禁用时通过浏览器默认的 <a>
实现跳转“ 这样的一箭双雕的效果:
<Link href="/some/route">
<a>Some text</a>
</Link>
那么,如果我们不按照官方文档的建议加入 <a>
会怎么样呢?比如像下面这样写:
<Link href="/some/route">
Some text
</Link>
源码中对这一部分也做了相应处理:
代码片段 3:Link
// packages/next/client/link.tsx
// 第 227 行,同样是在 Link 函数内
// 如果 children 是 string 类型,则将其转换为 <a> 链接
if (typeof children === 'string') {
children = <a>{children}</a>
}
所以,其实在 <Link />
下不用 <a>
链接,只用 text 也行。当然了,好的做法还是按照官方文档,在 <Link />
下使用 <a>
链接。
小结
这是 Next.js 实现原理系列 的第一篇文章,回答了两个非常简单的小问题。
下一篇文章会继续研究 Link 和 Router,看看 Next.js 是如何基于 Link 进行 prefetch 的: