作者:Craig Bucklere原文:Build a Blog with React and Next.js(sitepoint) 字數:4272 字 (非直譯,有添加部分) 閱讀: 10 分鐘大傢好,在用 React 和 Next.js做一個簡單的博客網站(上) 一篇文章裡,我們一起瞭解瞭什麼是 Next.js,並手工創建瞭一個簡單的 Next.js 項目,學會瞭如何基於模板創建簡單的頁面,本篇文章,我們繼續完善這個案例。一、基於MD文檔生成動態路由創建博客,自然少不瞭文章內容,如果我們每寫一篇文章,就創建一個 JSX 單頁面,這樣太不現實,費事費力又不容易維護,我們開發人員更喜歡使用 Markdown 文檔寫文檔。慶幸的是,Next.js 允許我們使用 Markdown 作為文章的數據源,基於文件名生成動態路由,並且實現文件內容的 HTML 靜態化。1、在編寫本功能時,最好停止 Next.js 服務(Ctrl | Cmd + C)。2、接下來,在項目的根目錄裡創建 articles 文件夾,把你的 Markdown 文件放置在這裡,例如:articles/article-01.md,MD 文檔格式如下所示:—
title: The first article
description: This is the first article.
date: 2020-10-01
—
This is an article post.
## Subheading
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
我們將文檔的標題名稱、文檔描述、創建日期放置在 — 之間,Front-matter 這個 npm 插件基於這個格式可以讀取上述相關信息提取文檔的標題、描述、創建日期。要將 MD 文檔格式化成網頁的形式,我們還需要安裝 remark 和 remark-html 這兩個npm 插件,安裝命令如下:npm i front-matter remark remark-html
3、安裝完成後,我們要實現讀取和格式化 MD 文檔的功能,接下來創建 lib/posts-md.js 工具函數文件。getFileIds(dir) 函數返回一個 MD 文件名的數組(不包含 .md 擴展名的文件名),示例代碼如下: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';
// return absolute path to folder
function absPath(dir) {
return (
path.isAbsolute(dir) ? dir : path.resolve(process.cwd(), dir)
);
}
// return array of files by type in a directory and remove extensions
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)));
}
獲取到文件名數組後,我們需要解析 MD 的具體內容,比如文件的標題、描述、創建日期、具體的內容HTML格式化等,示例代碼如下: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();
// date formatting
const date = matter.attributes.date || stat.ctime;
matter.attributes.date = date.toUTCString();
matter.attributes.dateYMD = dateformat.ymd(date);
matter.attributes.dateFriendly = dateformat.friendly(date);
// word count
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
};
}
你可能註意到我使用瞭日期格式化功能,其功能定義在 lib/dateformat.js 文件,示例代碼如下:// date formatting functions
const toMonth = new Intl.DateTimeFormat('en', { month: 'long' });
// format a date to 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')}` : '';
}
// format a date to DD MMMM, YYYY
export function friendly(date) {
return date instanceof Date
? `${date.getUTCDate()} ${toMonth.format(date)}, ${date.getUTCFullYear()}` : '';
}
4、Next.js 使用帶 [ ] 符號的特殊的文件名生成動態路由。接下來我們在 Pages 目錄下創建這個特殊的文件 pages/articles/[id].js, Next.js 使用id作為路由的參數,生成 /articles/article-01 的頁面路由。pages/articles/[id].js 這個文件裡實現Next.js 特有的 getStaticPaths() 函數功能(Static Generation),在項目構建時生成指定的路由路徑,比如這個案例將 articles 目錄下的 MD 文檔返回如下的數組格式,id 將匹配 pages/articles/[id].js 對應的 [id] 參數生成動態路由:[
{ params: { id: "article-01" } },
{ params: { id: "article-02" } },
{ params: { id: "article-03" } },
…
]
這個方法調用 lib/posts-md.js 文件裡讀取 getFileIds 文件路徑列表的方法,示例代碼如下:import { getFileIds, getFileData } from '../../lib/posts-md';
// post directory
const postsDir = 'articles';
// dynamic route IDs
export async function getStaticPaths() {
const
paths = (await getFileIds(postsDir))
.map((id) => ({ params: { id } }));
return {
paths,
fallback: false,
};
}
5、動態路由生成後,我們需要實現 MD 內容格式化渲染,我們實現Next.js 特有的異步方法 getStaticProps({ params }),在項目構建時調用這個函數(Static Generation),通過 id 參數調用 lib/posts-md.js 文件中 getFileData() 定義的方法,將 MD 文檔內容異步回傳至包含 postData 屬性的組件內部(第六點的代碼部分),示例代碼如下:// dynamic route content
export async function getStaticProps({ params }) {
return {
props: {
postData: await getFileData(postsDir, params.id),
},
};
}
6、拿到數據後,我們需要填充到組件的模板裡,以更友好的形式展現,我們在 pages/articles/[id].js 裡編寫JSX的相關代碼,將文章內容嵌套在上節組件模板內,示例代碼如下:import Layout from '../../components/layout';
import Head from 'next/head';
…
export default function Article({ postData }) {
// generate HTML from markdown content
const html = `
<h1>${ postData.title }</h1>
<p class="time"><time datetime="${ postData.dateYMD }">${ postData.dateFriendly }</time></p>
<p class="words">${ postData.wordcount }</p>
${ postData.html }
`;
return (
<Layout hero="phone.jpg">
<Head>
<title>{ postData.title }</title>
<meta name="description" content={ postData.description } />
</Head>
<article dangerouslySetInnerHTML={{ __html: html }} />
</Layout>
);
}
最後我們需要重啟 Next.js 服務,一切正常的話,你會發現所有的 MD 文檔可以通過 /articles/文件名的路徑在瀏覽器上查看, 例如 http://localhost:3000/articles/article-01 對應 /articles/article-01.md這個 MD 文檔,效果如下圖所示:二、創建博客列表頁有瞭博客相關的內容頁,我們需要建一個按照文檔創建時間倒序排列的博客列表頁1、首先我們在 lib/posts-md.js 文件裡,定義一個 getAllFiles() 方法獲取指定目錄下文件列表:將 MD 文檔的內容加載到數組裡移除沒有內容的文件按照文章的日期倒序排列// return sorted array of all posts for indexes
export async function getAllFiles(dir) {
const
now = dateformat.ymd(new Date()),
files = await getFileIds(dir),
data = await Promise.allSettled( files.map(id => getFileData(dir, id)) );
return data
.filter(md => md.value && md.value.dateYMD <= now)
.map(md => md.value)
.sort((a, b) => (a.dateYMD < b.dateYMD ? 1 : -1));
}
2、接下來我們新建一個博客列表頁 pages/articles/index.js,創建一個異步方法 getStaticProps(),在項目構件時,調用剛才我們編寫的方法 getAllFiles(),將文件列表內容返回組件的 postData 的屬性裡(第三點的代碼部分),示例代碼如下:import { getAllFiles } from '../../lib/posts-md';
const postsDir = 'articles';
// fetch array of all article posts
export async function getStaticProps() {
return {
props: {
postData: await getAllFiles(postsDir),
},
};
}
3、接下來在我們需要將博客列表的內容輸出到 pages/articles/index.js 頁面進行顯示,使用數組的 map() 方法迭代解析上述方法 postData 返回的內容,示例代碼如下:import Layout from '../../components/layout';
import Pagelink from '../../components/pagelink';
import Head from 'next/head';
export default function ArticleIndex({ postData }) {
return (
<Layout hero="phone.jpg">
<Head>
<title>Article index</title>
<meta name="description" content="A list of articles published on this site." />
</Head>
<h1>Article index</h1>
<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>
);
}
4、你可能註意到,上述代碼我引用瞭一個<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.dateymd }>{ props.datefriendly }</time></p>
<p>{ props.description }</p>
</article>
);
}
到這裡博客列表頁的功能就全部完成瞭,在瀏覽器輸入 http://localhost:3000/articles 預覽效果如下圖所示:所有的 MD 的文件都會羅列在此頁面,隨著內容的增加,你需要增加相關的邏輯進行分頁,這裡你就需要用到 getStaticPaths() 這個方法,並且需要此頁面改成 pages/articles/[index].js(註:index可以換成你想要的參數,但是需要和getStaticPaths 方法中的參數對應),在頁面構建時生成對應的頁面路由,你可以參照第一部分基於MD文檔生成動態路由這部分內容,具體的邏輯你可以考慮下怎麼實現,這裡就不再介紹瞭;三、創建網站導航為瞭讓用戶更方便瀏覽我們的博客網站,我們需要新建 components/navmenu.js 導航組件,用來實現網站導航的功能,由於功能簡單,這裡就不再解釋,示例代碼如下:import Link from 'next/link';
import Link from 'next/link';
// menu name and link
const menu = [
{ text: 'home', link: '/' },
{ text: 'about', link: '/about' },
{ text: 'articles', link: '/articles' }
];
// render menu
export default function Navmenu() {
// get current page route
const
router = useRouter(),
currentPage = router.pathname;
return (
<nav>
<ul>
{ menu.map(item => (
<Navlink
key={ item.link }
text={ item.text }
link={ item.link }
currentpage={ currentPage }
/>
))}
</ul>
</nav>
)
}
// render individual menu link
function Navlink({ 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>
);
}
}
導航組件完成後,我們將其引入 components/header.js 組件內,示例代碼如下:import Navmenu from './navmenu';
更新後的 JSX 代碼如下:…
<Navmenu />
<figure>
<img src={ hero } width="400" height="300" alt="decoration" />
</figure>
…
完成後,博客導航的效果如下圖所示:四、使用Sass為博客添加全局樣式到這裡,一個基於 MD 文檔的簡單博客網站到這裡就完成瞭,最後我們要為網站添加樣式,要不網站醜得實在看不下去。Next.js 可以使用 Sass, Less, PostCSS, Styled JSX, CSS modules、plain old CSS等多種方式為站點添加樣式,這裡我們使用 Sass 為站點添加樣式,這裡我們手工為項目安裝Sass:npm i sass
接下來我們可以為每個組件定義相關的樣式,然後將其合並在一個 styles/global.scss 文件裡,由於本篇文章重點講述Next.JS 的用法,這裡就不介紹如何編寫Sass,感興趣的同學可以點擊文末的閱讀原文下載本文的 Sass 樣式:// settings
@import '01-settings/_variables';
@import '01-settings/_mixins';
// reset
@import '02-generic/_reset';
// elements
@import '03-elements/_primary';
// layout
@import '04-layout/_site';
// components
@import '05-components/_header';
@import '05-components/_footer';
@import '05-components/_article';
最後我們需要將 styles/global.scss 引入到 pages/_app.js 這個特殊的文件裡,這樣網站所有的頁面都可以使用此樣式,示例代碼如下:import '../styles/global.scss';
export default function App({ Component, pageProps }) {
return <Component {…pageProps} />
};
最後我們重新啟動 Next.js 服務,你將會看到一個還算漂亮的博客首頁,如下圖所示:未完待續由於篇幅原因,今天的文章就到這裡,一個基於 MD 文檔的簡單博客網站就完成瞭,通過本篇文章我們學習瞭如何基於MD文檔生成動態路由,完成瞭文章內容頁、列表頁、導航功能,並為網站添加瞭漂亮的樣式。在下篇文章裡,我們為博客網站添加暗黑模式,基於接口數據渲染內容(服務端渲染),及如何編譯項目將博客網站部署到 Node.js 服務器上或純靜態化部署,最後會提供完整的項目源碼,敬請期待…