iT邦幫忙

2021 iThome 鐵人賽

DAY 8
1
Modern Web

製作你的無程式碼(No-code)個人網頁 ft. Next.js, SWR, 串 Youtube, IG, Github, Notion API ~系列 第 8

#08 實作篇 — 使用 Next.js 的各種 Data Fetching 方式實作小專案 ft. Github API

  • 分享至 

  • xImage
  •  

大家好!昨天實作了小小專案,也寫了一篇短短的介紹文,那今天跟大家分享怎麼用 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

看更多


上一篇
#07 簡介篇 — 使用 Next.js 的各種 Data Fetching 方式實作小專案 ft. Github API
下一篇
#09 No-code 之旅 — 怎麼在 Client-side 抓取資料?SWR 簡介
系列文
製作你的無程式碼(No-code)個人網頁 ft. Next.js, SWR, 串 Youtube, IG, Github, Notion API ~30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言