Day05 - 使用 Link 实作换页

Link

在了解了 Next.js 的三种路由方式後,接下来就让我们来聊聊怎麽在 component 切换页面吧!

在 Next.js 中切换页面是用 client-side routing 的方式,其体验会很像是 SPA (Single Page Application),而不是像传统的 SSR 在切换页面时画面都会有重新载入的感觉。

Next.js 提供了 <Link /> 这个 component,让我们可以在 Next.js 做到像是 SPA 的体验。还记得 Next.js 有三种路由的方式,分别为 static routes、dynamic routes、catch all routes,对於这三种不同的路由方式,在定义 <Link /> 都大同小异。

import Link from "next/link";

function Home() {
  return (
    <ul>
      <li>
        <Link href="/post">
          <a>切换至 pages/post/index.tsx</a>
        </Link>
      </li>
      <li>
        <Link href="/post/123">
          <a>切换至 pages/post/[postId].tsx</a>
        </Link>
      </li>
      <li>
        <Link href="/post/2021/12/31">
          <a>切换至 pages/post/[...date].tsx</a>
        </Link>
      </li>
    </ul>
  );
}

export default Home;

以上方的范例来说, <Link /> 传入的 href 都是对应一个 page 的 url:

  • /post 对应的是 pages/post.tsxpages/post/index.tsx
  • /post/123 对应的是 pages/post/[postId].tsx
  • /post/2021/12/31 对应的是 pages/post/[...date].tsx

物件形式的 href

href 不仅仅可以传入 url 字串也能够接受物件形式的 url 物件,我们透过 TypeScript 的型别定义来看看 href 可以传入的数值有哪些:

type Url = string | UrlObject;

interface UrlObject {
  auth?: string | null | undefined;
  hash?: string | null | undefined;
  host?: string | null | undefined;
  hostname?: string | null | undefined;
  href?: string | null | undefined;
  pathname?: string | null | undefined;
  protocol?: string | null | undefined;
  search?: string | null | undefined;
  slashes?: boolean | null | undefined;
  port?: string | number | null | undefined;
  query?: string | null | ParsedUrlQueryInput | undefined;
}

从 Next.js 的型别定义中,我们可以看到 UrlObject 接受的参数非常多种,能够用非常弹性的方式定义路由的 url。

我们用上面比较简单来的例子改写成 UrlObject

<li>
  <Link
    href={{
      pathname: "/post/[postId]",
      query: { postId: "123" },
    }}
  >
    <a>切换至 pages/post/[postId].tsx</a>
  </Link>
</li>
<li>
  <Link
    href={{
      pathname: "/post/[...date]",
      query: { date: ["2021", "12", "31"] },
    }}
  >
    <a>切换至 pages/post/[...date].tsx</a>
  </Link>
</li>

可藉由 pathname 以相似 page 的方式传入 url,并在 query 传入可以从 useRouter() 中拿到的值:

  • /post/[postId] 对应的是 pages/post/[postId].tsx
  • /post/[...date] 对应的是 pages/post/[...date].tsx

动态生成 href

既然 href 都能够接受 stringUrlObject 两种定义路由的方式,当然也能够接受动态生成 url 的方式。

例如贴文列表里面包含了许多贴文的连结,因此可以用 map 来生成许多的 <Link />

import Link from "next/link";

const Posts = () => {
  const posts = [{ id: "1" }, { id: "2" }, { id: "3" }];
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/post/${post.id}`}>
            <a>Post id: {post.id}</a>
          </Link>
        </li>
      ))}
    </ul>
  );
};

export default Posts;

但有一点必须要注意的是, <Link /> 这个 component 只能接受里面有一个 child node,而且 child node 只能是 string 或是 <a> ,如上方的范例,假设把 <a> 拿掉以後 Next.js 便会报错。

因为对於 react 来说, Post id: {post.id} 是会分成两个 node,所以 Next.js 便无法顺利的编译。

Imperatively routing

在许多情况下 <Link /> 已经够用,但是也许有些使用情境,我们需要延迟切换页面的事件,例如在点击按钮时想要触发 google analytics 的事件後,再触发换页。

所以,我们就可以用 useRouter 中的 router.push 来帮助我们达成这件事:

import { useRouter } from "next/router";

import ga from "../lib/ga";

const Posts = () => {
  const posts = [{ id: "1" }, { id: "2" }, { id: "3" }];
  const router = useRouter();

  const handleRouteChange = (post_url: string) => {
    ga.event({ action: "click_post_link", params: { post_url } });
    router.push(post_url);
  };

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <button onClick={() => handleRouteChange(`/post/${post.id}`)}>
            <a>Post id: {post.id}</a>
          </button>
        </li>
      ))}
    </ul>
  );
};

export default Posts;

Shallow routing

Shallow routing 是一种用於同一个 page 的路由,你能够改变 url 上的 query string,但是不执行 getServerSidePropsgetStaticPropsgetInitialProps 里面的程序,此外还会保留 page 中的状态。

要使用 shallow routing 需要用到 useRouter() ,在 router.push 的第三个参数加上 { shallow: true }

router.push(`/?counter=123`, undefined, { shallow: true });

以下是一个简单的范例,点击「add counter」的按钮後,会新增一个随机的数字,同时修改 url 上的 counter - query string,而 useEffect 会监听 router.query.counter ,在改变时储存到 counters 状态中。

import { useState, useEffect } from "react";
import { useRouter } from "next/router";

// 目前 url 为 '/'
function Page() {
  const router = useRouter();
  const [counters, setCounters] = useState<number[]>([]);

  const handleClick = () => {
    const counter = Math.round(Math.random() * 100);
    router.push(`/?counter=${counter}`, undefined, { shallow: true });
  };

  useEffect(() => {
    if (router.query.counter) {
      setCounters((prev) => [
        ...prev,
        parseInt(router.query.counter as string),
      ]);
    }
  }, [router.query.counter]);

  return (
    <div>
      <ul>
        {counters.map((counter) => (
          <li key={counter}>{counter}</li>
        ))}
      </ul>
      <button onClick={handleClick}>add counter</button>
    </div>
  );
}

export default Page;

你可能会踩到 Shallow routing 的雷 ?

Shallow routing 有一个限制是「只能在同一个 url 上切换」,像是以上方的范例来说,我们在在 url 上加上一段 query string — /?counter=${counter} ,这样的操作是合法的。以下示范一个不合法的操作:

router.push(`/products?counter=${counter}`, undefined, { shallow: true });

这个操作会将目前的页面转移到 /product,等於 { shallow: true } 变成是一段没有用的参数。

Reference


<<:  D06 - Web Serial API 初体验

>>:  Day20:[排序演算法]Selection Sort - 选择排序法

[Day7] struct 结构体

今天突然整个不知道要写什麽 @@ 一定是礼拜六要上课的关系 ## 今天呢 就来讲讲有关於 Rust ...

[ JS个人笔记 ] Event Loop事件循环—DAY11

理解js单执行绪&非同步运行机制 由於js为单执行绪,也就是一次只处理一件事情并依序执行,但...

【Day 9】梯度下降法(Gradient Descent) --- Tip 2, 3

Tip 2:随机梯度下降法(Stochastic Gradient Descent) 提升训练速度 ...

Day18 - 轻前端 Vue - 复杂型别 object

先说明一下 我用轻前端 Vue 的目的,不是把整个网站都改用轻前端,而是为了把复杂的 js 取值、给...

给自己安装一台游戏主机

之前一直用的E5 2680v3打了鸡血,全核3.3G,打游戏不太好使毕竟目前的游戏大部分吃的单核高主...