Next.js 实现原理系列(1):Link 基础

背景

这周开始尝试阅读 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 的:

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值