Next.js
是由 Vercel 建立和維護的基于 React 的應用程式架構。本教程将從零開始學習如何使用
Next.js
建構一個小型的部落格網站:
- 基本頁面建立
- 從
檔案生成的動态路由Markdown
- 靜态生成(在建構時渲染)
- 伺服器端渲染(在請求時渲染)
文章涉及的代碼倉庫位址:https://github.com/QuintionTang/react-blog
Next.js 适合部落格嗎?
本教程将通過建立一個簡單的部落格來展示
Next.js
功能,那麼
Next.js
适合這樣的部落格的開發嗎?先來了解一下一般部落格都需要什麼?
- WordPress 是一個内容管理系統 (CMS),它為三分之一的網站提供支援,通過在每次請求時将可編輯的資料庫内容渲染到 PHP 模闆中來為頁面提供服務。它非常适合定期更新的内容,但性能、安全性和資料備份需要一定的自定義設定。
- 靜态站點生成器 (SSG),例如 Eleventy 或 Gatsby 建立預渲染檔案,無需伺服器端或資料庫即可快速建構靜态站點,在版本控制、性能和安全性都非常出色,但建構步驟和以開發人員為中心的過程可能會減慢釋出速度,尤其是在大型網站上。
Next.js
是一個基于 React 的應用程式架構,它幾乎沒有特定于部落格功能。但是,它可以提供了一種實作機制:
- 在可能的情況下,
生成靜态内容,如 SSG,這些頁面加載速度非常快,可以被搜尋引擎快速收錄,并且可以在任何有或沒有 JavaScript 的裝置上閱讀。Next.js
- 在第一次加載後,
應用程式的行為類似于單頁應用程式 (SPA),後續頁面和代碼會以漸進式下載下傳,無需重新整理整頁。Next.js
-
為每個請求提供伺服器端渲染 (SSR),為個人使用者提供實時 CMS 更新或自定義内容變得更加容易。Next.js
如果網站可能會從基本部落格疊代為更複雜的網站,例如線上商店、新聞聚合服務、社交媒體平台等,可以考慮使用
Next.js
。
開始
本教程正在建構的内容,可以在 GitHub 上找到完整的代碼。可以通過在終端中輸入以下指令,在
Windows
、
macOS
或
Linux
上下載下傳、安裝和啟動它:
git clone [email protected]:QuintionTang/react-blog.git
cd react-blog
npm i
npm run dev
然後在浏覽器中輸入
localhost:3000
打開首頁。
從頭開始建構
Next.js
提供了一個
create-next-app
工具來快速開始使用應用程式模闆。本教程将展示如何從頭開始建構站點:如添加靜态資源或者頁面。
安裝 Next.js 和 React
與其他
Node.js
或者 VUE 項目一樣,首先建立一個目錄并初始化
package.json
檔案:
mkdir react-blog
cd react-blog
npm init
然後安裝
Next.js
和
React
作為依賴項:
npm install next react react-dom --save
添加開發建構腳本設定,如下所示,在
package.json
檔案的
scripts
屬性中添加:
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
建立第一個頁面
Next.js
有一個基于檔案系統的路由器。在項目的
pages
目錄中建立的任何 React 元件檔案都會自動呈現為一個頁面。
要建立一個頁面,需要在
pages
目錄中添加一個
index.js
檔案。将以下代碼添加到
./pages/index.js
檔案中,傳回
JSX
代碼的功能性 React 元件:
export default function Home() {
return (
<>
Next.js 部落格網站
<p>
這個部落格網站将使用 <a rel="nofollow" href="https://nextjs.org/">Next.js</a>。
</p>
</>
);
}
JSX 必須在單個包含元素(例如)中傳回。
<div>
表示法定義了一個文檔片段,是以不需要額外的容器。
<> ... </>
要啟動
Next.js
開發伺服器,從項目根目錄在終端中運作
npm run dev
(可以使用
npx next dev
),然後在浏覽器中打開
http://localhost:3000/
:
Next.js
已經确定頁面可以預渲染,是以它在開發模式下顯示一個閃電圖示。
可以在自動路由的頁面目錄中建立類似的檔案,如下:
-
用于呈現部落客要pages/index.js
-
呈現一個pages/about.js
頁面/about
增加連結
在 JSX 代碼中使用标準 HTML
<a>
标簽建立指向另一個頁面的超連結。如果該頁面位于同一個
Next.js
站點内,浏覽器将會重新整理整個頁面。可以使用
next/link
中的
<Link>
元件實作頁面跳轉。在根頁面
/index.js
上建立指向
/about
頁面的連結,代碼如下:
import Link from "next/link";
export default function Home() {
return (
<>
Next.js 部落格網站
<p>
這個部落格網站将使用 <a rel="nofollow" href="https://nextjs.org/">Next.js</a>。
</p>
<p>
更多内容請點選{" "}
<Link href="/about">
<a>關于我們...</a>
</Link>
</p>
</>
);
}
當點選
關于我們...
連結時,
Next.js
将使用
Ajax
請求下載下傳
/about
的内容一次并緩存,然後再頁面中呈現。
增加 <head> 元素
可以使用
next/head
中的
<Head>
元件來更改頁面标題和元标記,如下:
import Head from "next/head";
import Link from "next/link";
export default function Home() {
return (
<>
<Head>
<title>Next.js網站</title>
<meta
name="description"
content="這是一個由 Next.js 驅動的網站"
/>
</Head>
Next.js 部落格網站
<p>
這個部落格網站将使用 <a rel="nofollow" href="https://nextjs.org/">Next.js</a>。
</p>
<p>
更多内容請點選{" "}
<Link href="/about">
<a>關于我們...</a>
</Link>
</p>
</>
);
}
點選浏覽器檢視源代碼,可以看到相關 HTML 标簽。
增加靜态資源
public
目錄用于存放靜态資源,如圖示、
robots.txt
或其它更新頻率低的檔案。可以增加自己的檔案或從初始項目存儲庫複制
favicon.ico
和圖像子目錄。
建立模闆
Next.js
使用 React 元件來實作模闆化,接下來項目根目錄下建立一個新的
components
檔案夾,然後添加
layout.js
來定義一個新的
<Layout>
元件:
import Header from "./header";
import Footer from "./footer";
export default function Layout({ children, title }) {
return (
<>
<Header title={title} />
<main>{children}</main>
<Footer />
</>
);
}
任何使用此元件的頁面都會傳遞一個
props
對象,該對象包含作為子值
children
的内容。
<Layout>
還将引用了另外兩個元件,分别是
component/header.js
中的
<Header>
,主要呈現一個
<header>
,包含首頁連結、内聯 SVG Logo和 預設為
/images/header.jpg
的圖像:
import Link from "next/link";
export default function Header({ title }) {
const headerImg = "/images/" + (title || "header.jpg");
return (
<header>
<p className="logo">
<Link href="/">
<a>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
width="50"
height="50"
>
<path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z"></path>
</svg>
Next.js 部落格
</a>
</Link>
</p>
<figure>
<img
src={headerImg}
height="80"
alt="decoration"
/>
</figure>
</header>
);
}
第二個元件是
component/footer.js
中的
<Footer>
,呈現
<footer>
内容,其中包含指向 GitHub 存儲庫和内聯 SVG 的連結:
export default function Footer() {
return (
<footer>
<p className="github">
<a rel="nofollow" href="https://github.com/QuintionTang/react-blog">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
width="50"
height="50"
>
<path d="M256 32C132.3 32 32 134.9 32 261.7a229.3 229.3 0 00153.2 217.9 17.6 17.6 0 003.8.4c8.3 0 11.5-6.1 11.5-11.4l-.3-39.1a102.4 102.4 0 01-22.6 2.7c-43.1 0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1 1.4-14.1h.1c22.5 2 34.3 23.8 34.3 23.8 11.2 19.6 26.2 25.1 39.6 25.1a63 63 0 0025.6-6c2-14.8 7.8-24.9 14.2-30.7-49.7-5.8-102-25.5-102-113.5 0-25.1 8.7-45.6 23-61.6a84.6 84.6 0 012.2-60.8 18.6 18.6 0 015-.5c8.1 0 26.4 3.1 56.6 24.1a208.2 208.2 0 01112.2 0c30.2-21 48.5-24.1 56.6-24.1a18.6 18.6 0 015 .5 84.6 84.6 0 012.2 60.8 90.3 90.3 0 0123 61.6c0 88.2-52.4 107.6-102.3 113.3 8 7.1 15.2 21.1 15.2 42.5 0 30.7-.3 55.5-.3 63 0 5.4 3.1 11.5 11.4 11.5a19.4 19.4 0 004-.4A229.2 229.2 0 00480 261.7C480 134.9 379.7 32 256 32z" />
</svg>
https://github.com/QuintionTang/react-blog
</a>
</p>
</footer>
);
}
接下來更新
pages/index.js
,将使用自定義的
<Layout>
元件:
import Layout from "../components/layout";
import Head from "next/head";
import Link from "next/link";
export default function Home() {
return (
<Layout>
<Head>
<title>Next.js網站</title>
<meta
name="description"
content="這是一個由 Next.js 驅動的網站"
/>
</Head>
Next.js 部落格網站
<p>
這個部落格網站将使用 <a rel="nofollow" href="https://nextjs.org/">Next.js</a>。
</p>
<p>
更多内容請點選{" "}
<Link href="/about">
<a>關于我們...</a>
</Link>
</p>
</Layout>
);
}
然後對
pages/about.js
和建立的任何其他頁面執行相同操作。
import Layout from "../components/layout";
import Head from "next/head";
export default function Home() {
return (
<Layout title="share.png">
<Head>
<title>關于我們</title>
</Head>
關于我們
<p>
DevPoint 是 WEB
開發的分享中心,用自己的熱情來分享網際網路的點滴,以此激勵自己加強學習提升自我。
</p>
</Layout>
);
}
更新後的效果如下:
使用動态路由檢視部落格内容
使用 JSX 建立内容并不是特别實用,尤其是對于正常部落格文章,對于開發者比較喜歡 Markdown 的方式寫部落格。Next.js 可以使用任何來源建立頁面。這些可以在建構時靜态生成,并使用動态路由将資料映射到 URL。
在繼續之前,先來建立一個文章目錄,用于存放部落格的 Markdown 檔案。例如:
articles/article-01.md
:
---
title: 使用 React 和 Next.js 建構部落格
description: Next.js 是由 Vercel 建立和維護的基于 React 的應用程式架構。本教程将從零開始學習如何使用 Next.js 建構一個小型的部落格網站。
date: 2022-01-22
tags:
- HTML
- CSS
- REACT
---
使用 React 和 Next.js 建構部落格
## 摘要
Next.js 是由 Vercel 建立和維護的基于 React 的應用程式架構。本教程将從零開始學習如何使用 Next.js 建構一個小型的部落格網站。
本教程将通過建立一個簡單的部落格來展示 Next.js 功能,那麼 Next.js 适合這樣的部落格的開發嗎?先來了解一下一般部落格都需要什麼?
- WordPress 是一個内容管理系統 (CMS),它為三分之一的網站提供支援,通過在每次請求時将可編輯的資料庫内容渲染到 PHP 模闆中來為頁面提供服務。它非常适合定期更新的内容,但性能、安全性和資料備份需要一定的自定義設定。
- 靜态站點生成器 (SSG),例如 Eleventy 或 Gatsby 建立預渲染檔案,無需伺服器端或資料庫即可快速建構靜态站點,在版本控制、性能和安全性都非常出色,但建構步驟和以開發人員為中心的過程可能會減慢釋出速度,尤其是在大型網站上。
Next.js 是一個基于 React 的應用程式架構,它幾乎沒有特定于部落格功能。但是,它可以提供了一種實作機制:
1. 在可能的情況下,Next.js 生成靜态内容,如 SSG,這些頁面加載速度非常快,可以被搜尋引擎快速收錄,并且可以在任何有或沒有 JavaScript 的裝置上閱讀。
2. 在第一次加載後,Next.js 應用程式的行為類似于單頁應用程式 (SPA),後續頁面和代碼會以漸進式下載下傳,無需重新整理整頁。
3. Next.js 為每個請求提供伺服器端渲染 (SSR),為個人使用者提供實時 CMS 更新或自定義内容變得更加容易。
如果網站可能會從基本部落格疊代為更複雜的網站,例如線上商店、新聞聚合服務、社交媒體平台等,可以考慮使用 Next.js。
内容的模闆以
---
來定義部落格的标題、釋出時間等中繼資料,
---
後面的為部落格的正文。接下來需要安裝解析内容的依賴,包括:
front-matter
、
remark
和
remark-html
,執行一下指令:
npm install front-matter remark remark-html --save
要讀取和解析 Markdown 檔案,需要添加相關邏輯,代碼所在檔案
libs/posts-md.js
。
import { promises as fsp } from "fs";
import path from "path";
import fm from "front-matter";
import { remark } from "remark";
import remarkhtml from "remark-html";
import * as dateformat from "./dateformat";
const fileExt = "md";
// 擷取檔案夾相對路徑
function absPath(dir) {
return path.isAbsolute(dir) ? dir : path.resolve(process.cwd(), dir);
}
/**
* 擷取檔案夾中 Markdown 檔案名清單,以數組形式傳回
* @param {*} dir
* @returns
*/
export async function getFileIds(dir = "./") {
const loc = absPath(dir);
const files = await fsp.readdir(loc);
return files
.filter((fn) => path.extname(fn) === `.${fileExt}`)
.map((fn) => path.basename(fn, path.extname(fn)));
}
/**
* 擷取單個 Markdown 檔案的内容
* @param {*} dir
* @param {*} id
* @returns
*/
export async function getFileData(dir = "./", id) {
const file = path.join(absPath(dir), `${id}.${fileExt}`),
stat = await fsp.stat(file),
data = await fsp.readFile(file, "utf8"),
matter = fm(data),
html = (await remark().use(remarkhtml).process(matter.body)).toString();
// 日期格式化
const date = matter.attributes.date || stat.ctime;
matter.attributes.date = date.toUTCString();
matter.attributes.dateYMD = dateformat.ymd(date);
matter.attributes.dateFriendly = dateformat.friendly(date);
// 計數
const roundTo = 10,
readPerMin = 200,
numFormat = new Intl.NumberFormat("en"),
count = matter.body
.replace(/\W/g, " ")
.replace(/\s+/g, " ")
.split(" ").length,
words = Math.ceil(count / roundTo) * roundTo,
mins = Math.ceil(count / readPerMin);
matter.attributes.wordcount = `${numFormat.format(
words
)} words, ${numFormat.format(mins)}-minute read`;
return {
id,
html,
...matter.attributes,
};
}
以上代碼涉及日期格式化代碼,檔案路徑
libs/dateformat.js
。
// 時間格式化
const toMonth = new Intl.DateTimeFormat("en", { month: "long" });
// 格式化為 YYYY-MM-DD
export function ymd(date) {
return date instanceof Date
? `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(
2,
"0"
)}-${String(date.getUTCDate()).padStart(2, "0")}`
: "";
}
// 格式化為 DD MMMM, YYYY
export function friendly(date) {
return date instanceof Date
? `${date.getUTCDate()} ${toMonth.format(
date
)}, ${date.getUTCFullYear()}`
: "";
}
Next.js 使用包含在
[
id
]
中的辨別符的檔案名來識别動态(生成)路由。建立一個名為
pages/articles/[id].js
的檔案:
Next.js
将使用
id
作為參數在
/articles/article-01
等路由處生成頁面,即部落格的詳情頁路由。
pages/articles/[id].js
中定義函數
getStaticPaths
,該函數傳回建構時要呈現的路徑資訊。
/**
* 擷取部落格路徑資訊
* @returns [ { params: { id: 'article-01' } } ]
*/
export async function getStaticPaths() {
const paths = (await getFileIds(postsDir)).map((id) => {
return { params: { id } };
});
console.log(paths);
return {
paths,
fallback: false,
};
}
設定 fallback: false
會在找不到路徑時出現 404 頁面。
接下來建立函數
getStaticProps
,函數在建構時擷取特定 ID 的資料以進行靜态生成。它在
params
對象中傳遞了一個
id
屬性,調用
libs/posts-md.js
中的
getFileData()
函數解析 Markdown 檔案。
/**
* 解析路由擷取詳細内容
* @param {*} param0
* @returns
*/
export async function getStaticProps({ params }) {
return {
props: {
postData: await getFileData(postsDir, params.id),
},
};
}
pages/articles/[id].js
除了解析部落格内容外,還需将内容導出為一個 React 元件,元件将
postData
渲染到前面建立的模闆中:
export default function Article({ postData }) {
// 解析markdown内容
const html = `
${postData.title}
<p class="time"><time datetime="${postData.dateYMD}">${postData.dateFriendly}</time></p>
<p class="words">${postData.wordcount}</p>
${postData.html}
`;
return (
<Layout title="share.png">
<Head>
<title>{postData.title}</title>
<meta name="description" content={postData.description} />
</Head>
<article dangerouslySetInnerHTML={{ __html: html }} />
</Layout>
);
}
dangerouslySetInnerHTML
屬性確定 HTML 不被編碼。
建立部落格清單頁
建立檔案
pages/articles/index.js
,這個頁面需要實作的功能是解析部落格清單,并傳回為一個 React 元件。在實作這個頁面功能之前,先來建立一個連結元件
Pagelink
。
Pagelink
元件實作部落格清單中單篇部落格的布局,建立檔案
components/pagelink.js
,代碼如下:
import Link from "next/link";
export default function Pagelink(props) {
const link = `/${props.postsdir}/${props.id}`;
return (
<article>
<h2>
<Link href={link}>
<a>{props.title}</a>
</Link>
</h2>
<p className="time">
釋出時間:
<time dateTime={props.datefriendly}>{props.dateymd}</time>
</p>
<p>{props.description}</p>
</article>
);
}
完成單個部落格布局後,來看看部落格清單頁,代碼如下:
import { getAllFiles } from "../../libs/posts-md";
import Layout from "../../components/layout";
import Pagelink from "../../components/pagelink";
import Head from "next/head";
const postsDir = "articles";
export default function ArticleIndex({ postData }) {
return (
<Layout title="share.png">
<Head>
<title>部落格清單</title>
<meta
name="description"
content="A list of articles published on this site."
/>
</Head>
部落格清單
<aside className="pagelist">
{postData.map((post) => (
<Pagelink
key={post.id}
postsdir={postsDir}
id={post.id}
title={post.title}
description={post.description}
dateymd={post.dateYMD}
datefriendly={post.dateFriendly}
/>
))}
</aside>
</Layout>
);
}
/**
* 擷取所有文章文章的數組
* @returns
*/
export async function getStaticProps() {
return {
props: {
postData: await getAllFiles(postsDir),
},
};
}
建立導航
一個完整的部落格站點,需要一個導航菜單,友善内容切換。接下來建立一個導航元件,建立檔案
components/navs.js
,導出一個
<Navs>
元件,代碼如下:
import { useRouter } from "next/router";
import Link from "next/link";
// menu name and link
const menu = [
{ text: "網站首頁", link: "/" },
{ text: "關于我們", link: "/about" },
{ text: "部落格清單", link: "/articles" },
];
export default function Navs() {
const router = useRouter(),
currentPage = router.pathname;
return (
<nav>
<ul>
{menu.map((item) => (
<NavItem
key={item.link}
text={item.text}
link={item.link}
currentpage={currentPage}
/>
))}
</ul>
</nav>
);
}
function NavItem({ text, link, currentpage }) {
if (link === currentpage) {
return (
<li className="active">
<strong>{text}</strong>
</li>
);
} else {
return (
<li>
<Link href={link}>
<a>{text}</a>
</Link>
</li>
);
}
}
下面将
Navs
元件加入到元件 Header 中,代碼如下:
import Link from "next/link";
import Navs from "./navs"; // 導航菜單
export default function Header({ title }) {
const headerImg = "/images/" + (title || "cover.png");
return (
<header>
<p className="logo">
<Link href="/">
<a>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
width="50"
height="50"
>
<path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z"></path>
</svg>
Next.js 部落格
</a>
</Link>
</p>
<Navs />
<figure>
<img src={headerImg} width="400" alt="decoration" />
</figure>
</header>
);
}
到此一個簡單的部落格站點功能已經實作。
添加樣式
Next.js 提供了一系列樣式選項,包括 Sass、Less、PostCSS、Styled JSX、CSS 子產品和普通的CSS。 Sass 易于使用,因為不需要任何配置,按照依賴:
npm install sass --save
根目錄下建立檔案夾
styles
,所有的樣式檔案都放在這個檔案夾下。樣式的入口未見為
master.scss
。
然後在檔案夾
pages
下建立檔案
_app.js
,将樣式檔案導入,完整代碼如下:
import "../styles/master.scss";
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />;
}
最終效果如下: