大家好!昨天實作了小小專案,也寫了一篇短短的介紹文,那今天跟大家分享怎麼用 Next.js 的各種 data fetching functions 串 API 抓取資料然後做成一個專案~
不過設計就這樣隨便做,請見諒Q
這專案用 Radix UI 的 Primitives 和 Stitches 做 styling
開始之前,先跟大家分享這小專案的所有功能:
用 Next.js 的 create-next-app
,在終端機輸入以下指令建立專案:
npx create-next-app
# or
yarn create next-app
跑完上面的指令後進到該專案的資料夾,執行 npm run dev
或 yarn dev
,打開瀏覽器瀏覽 http://localhost:3000
,應該會看到以下這畫面:
耶~ 恭喜!我們可以開始了!
pages/_app.js
這是什麼?!之前的文章應該沒有提到這檔案呢?Next.js 使用 App
去做每個頁面的初始化,不過我們可以自己做 custom App
做客製化。因為每個頁面都會用這 App
被 initialize,所以如果我們想加一些每一頁必須有的 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 (首頁) 開始吧!所謂的首頁是我們的 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
旁邊的按鈕需要加一個條件,就是 user
和 repo
都不能是空的,才能導去 Repo 頁~
pages/next.js
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 頁的路徑是 /users/[user]
,user
為動態資訊,也就是使用 dynamic routes 的方法!在這裡我用的是 getStaticProps
和 getStaticPaths
,而且還加了 revalidate
和 fallback = '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 頁的路徑是 /repos/[user]/[repo]
,user
和 repo
為動態資訊,一樣是用 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)
祝大家明天上班上課愉快!
晚安 <3