#08 实作篇 — 使用 Next.js 的各种 Data Fetching 方式实作小专案 ft. Github API

大家好!昨天实作了小小专案,也写了一篇短短的介绍文,那今天跟大家分享怎麽用 Next.js 的各种 data fetching functions 串 API 抓取资料然後做成一个专案~

不过设计就这样随便做,请见谅Q

这专案用 Radix UIPrimitivesStitches 做 styling

Day07

Features (功能)

开始之前,先跟大家分享这小专案的所有功能:

  • 看 Next.js 的 Github repository info (SSG)
  • 看某 Github user info (ISR)
  • 看某 Github repository info (SSR)

Setup

用 Next.js 的 create-next-app,在终端机输入以下指令建立专案:

npx create-next-app
# or
yarn create next-app

跑完上面的指令後进到该专案的资料夹,执行 npm run devyarn dev,打开浏览器浏览 http://localhost:3000,应该会看到以下这画面:

Next.js

耶~ 恭喜!我们可以开始了!

pages/_app.js

这是什麽?!之前的文章应该没有提到这档案呢?Next.js 使用 App 去做每个页面的初始化,不过我们可以自己做 custom App 做客制化。因为每个页面都会用这 Appinitialize,所以如果我们想加一些每一页必须有的 components 或 css,都可以加在这里:

// imports
import { globalCss } from "@stitches/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { Container } from "../components/container";

// 加 global CSS styles
const globalStyles = globalCss({
  "html, body": {
    padding: 0,
    margin: 0,
    fontFamily:
      "'Open Sans', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif",
  },
  a: { color: "inherit" },
  "*": { boxSizing: "border-box" },
});

// custom App
function MyApp({ Component, pageProps }) {
  globalStyles();

  const { pathname } = useRouter();

  return (
    <Container>
      <main>
        // 显示当页
        <Component {...pageProps} />
      </main>
      // 每一页会有这 footer
      <footer>
        <p>
          // 如果我们不在首页,让使用者看到 "Go back to Home"
          {pathname === "/" ? "You are at " : "Go back to "}
          <Link href="/">Home</Link>
        </p>
      </footer>
    </Container>
  );
}

// 记得用 export default 喔!
export default MyApp;

pages/index.js

Home (首页)
先从 Home (首页) 开始吧!所谓的首页是我们的 pages/index.js 档案,因为像这篇提到,pages/index.js 会对应到我们的 / 路径。在这页我们完全不用显示任何资料,也就是我们不需要用到任合 data fetching 的 functions。不过,我们放了一个连结去 Next 页 (/next),用 Link 做的:

<p>
  See what is <Link href="/next">Next</Link>?
</p>

除了连结之外,这页最重要的功能是 input!上方的 input 可以填 Github username,而下方的 input 是填 Github repository name。大家可以填填看,会观察到一件事,按钮一开始是 disabled 的状态,代表是不能点的 (或是点了没有用),等到有输入值之後,才能点喔:

// component state
const [user, setUser] = useState("");
const [repo, setRepo] = useState("");
// render inputs
<Flex css={{ marginBottom: 12 }}>
  <UserInput value={user} onChange={handleChange("user")} />
  // 当 user 没有值,按钮的 disabled 会等於 true
  <Button disabled={!user} onClick={handleClick("user")}>
    Go →
  </Button>
</Flex>
<Flex css={{ marginBottom: 12 }}>
  <RepoInput value={repo} onChange={handleChange("repo")} />
  // 当 user 或 repo 没有值,按钮的 disabled 会等於 true
  <Button disabled={!user || !repo} onClick={handleClick("repo")}>
    Go →
  </Button>
</Flex>

handleChange 会在每次 input 触发 change event 被执行,更新对应 state 的值。那 handleClick 是处理按按钮的事件,我们希望当我们点 UserInput 旁边的按钮而且 user state 有值,我们会被导去 User 页,怎麽导呢?用 useRouter 回传的 push method:

const { push } = useRouter();

const handleClick =
  (type = "user") =>
  () => {
    switch (type) {
      case "user":
        if (user) {
          push({ pathname: "/users/[user]", query: { user } });
        }
        break;
      case "repo":
        if (user && repo) {
          push({ pathname: "/repos/[user]/[repo]", query: { user, repo } });
        }
        break;
      default:
        break;
    }
  };

RepoInput 旁边的按钮需要加一个条件,就是 userrepo 都不能是空的,才能导去 Repo 页

pages/next.js

Next 页
Next 页是采用 Static Generation 产生出来的,也就是使用 getStaticProps 去抓取该页所需的内容。透过 props 去传递 data

// 在服务器端在 build time 执行
export async function getStaticProps() {
  // 抓取 vercel/next.js repository 的资料
  const res = await fetch("https://api.github.com/repos/vercel/next.js");
  const data = await res.json();

  // 回传该 page 所需的 props
  return {
    props: { data },
  };
}

Next 页会收到 props 而里面包含 data

// 记得把 page component 当 default 的 export 喔!
export default function Next({ data }) {
  // 这里的 data 就是 getStaticProps 回传的~
  return <RepoCard data={data} />;
}

pages/users/[user].js

User 页
User 页的路径是 /users/[user]user 为动态资讯,也就是使用 dynamic routes 的方法!在这里我用的是 getStaticPropsgetStaticPaths,而且还加了 revalidatefallback = 'blocking',让页面会不断更新也不断产生 (生成),所以这页是采用 Incremental Static Regeneration 的模式:

// 抓取该 page 所需的资料
export async function getStaticProps(context) {
  // 跟 Github 拿 user 的资料
  const res = await fetch(
    // context.params.user 就是路径中的 [user]
    `https://api.github.com/users/${context.params.user}`
  );
  const data = await res.json();

  return {
    props: { data },
    revalidate: 24 * 60 * 60, // 至少 24 小时後服务器会重新抓取资料而重新生成该页
  };
}

// 抓取这 dynamic route 该产生的 paths
export async function getStaticPaths() {
  // 抓取 Github 的所有 users
  const res = await fetch("https://api.github.com/users");
  const data = await res.json();
  // 因为几个 user 代表几个 page,我不想要一次产生这麽多页面,所以只挑前 1000 名
  const paths = data.slice(0, 1000).map((u) => ({ params: { user: u.login } }));

  // 回传该在 build time 被产生的 paths,不在这清单里的页面,会采取 "blocking" 方式
  return { paths, fallback: "blocking" };
}

现在我们来看看User 页的 component:

// 记得把 page component 当 default 的 export
export default function User({ data }) {
  // 当 Github API 回传 Not Found 错误
  if ("message" in data && data.message === "Not Found") {
    // 应该要用 Next.js 的 404 page,可是我先坐在这里Q
    return (
      <Center column>
        <h3>404 Not Found</h3>
        <p>Try other user</p>
      </Center>
    );
  }

  return (
    <>
      <Head>
        <title>{data.name || "A user"} | 2021 iTHome Day 07</title>
        <meta name="description" content="2021 iTHome Day 07 by Jade" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <UserCard data={data} />
    </>
  );
}

pages/repos/[user]/[repo].js

Repo 页
Repo 页的路径是 /repos/[user]/[repo]userrepo 为动态资讯,一样是用 dynamic routes,不过这页是采用 getServerSideProps 实做出来的,也就是 Server-side Rendering:

// 在服务器端每次收到请求时会执行
export async function getServerSideProps(context) {
  const res = await fetch(
   // context.params.user 就是路径中的 [user]
   // context.params.repo 就是路径中的 [repo]
   `https://api.github.com/repos/${context.params.user}/${context.params.repo}`
  );
  const data = await res.json();

  return {
    props: { data },
  };
}

Repo 页 component 其实长得跟 User 页 很像 (很懒Q),只差在显示的资料喔:

// 记得把 page component 当 default 的 export~
export default function Repo({ data }) {
  // 当 Github API 回传 Not Found 错误
  if ("message" in data && data.message === "Not Found") {
    // 应该要用 Next.js 的 404 page,可是我先坐在这里Q
    return (
      <Center column>
        <h3>404 Not Found</h3>
        <p>Try other repo or user</p>
      </Center>
    );
  }

  return (
    <>
      <Head>
        <title>
          {data.name || "A repo"} by {data.owner.login || "someone"} | 2021
          iTHome Day 07
        </title>
        <meta name="description" content="2021 iTHome Day 07 by Jade" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <RepoCard data={data} />
    </>
  );
}

小结

哇!写完了~ 这个小专案应该有用到前几天学的东东,大家觉得如何呢?有没有什麽问题?欢迎发问喔~
目前还没有办法提供完整的 code,不过有任何问题都可以问我! (希望我回答得出来Q)
祝大家明天上班上课愉快!

Live Demo

晚安 <3

看更多


<<:  【Day 08】- 有着资料清洗功能的 Requests-HTML

>>:  【第九天 - 数字型 SQL注入】

【Day 08】List 介绍!

前言 list 是 Python 中最常见的资料类型,有许多的应用都会用到 list 喔! 今天会先...

[Day04] JavaScript - ES6 模板字符串 (Template Literal)

ES6 除了新增了上篇的let & const之外,也提供了新的模版字符串(Template...

【29】遇到不平衡资料(Imbalanced Data) 时 使用 SMOTE 解决实验

Colab连结 今天要介绍处理不平衡资料的方法叫 SMOTE (Synthetic Minority...

[Day 14] .Net 非同步概念整理

前言 本来只是想简单带过 .Net , 但不知不觉就写了一大堆, 觉得内容有些混乱, 就在今天花些时...

【第十七天 - 文件读取漏洞】

Q1. 什麽是文件读取漏洞? 骇客可以透过一些手段读取无授权的档案,时常作为资讯收集的一种手段,例如...