写点什么

使用 React 和 Next.js 构建博客

作者:devpoint
  • 2022 年 1 月 22 日
  • 本文字数:10665 字

    阅读完需:约 35 分钟

使用 React 和 Next.js 构建博客

Next.js 是由 Vercel 创建和维护的基于 React 的应用程序框架。本教程将从零开始学习如何使用 Next.js 构建一个小型的博客网站:


  • 基本页面创建

  • Markdown 文件生成的动态路由

  • 静态生成(在构建时渲染)

  • 服务器端渲染(在请求时渲染)

  • 客户端水合作用(在浏览器中使用 React 组件)

  • 开发和构建过程,例如热加载

Next.js 适合博客吗?

本教程将通过创建一个简单的博客来展示 Next.js 功能,那么 Next.js 适合这样的博客的开发吗?先来了解一下一般博客都需要什么?



  • WordPress 是一个内容管理系统 (CMS),它为三分之一的网站提供支持,通过在每次请求时将可编辑的数据库内容渲染到 PHP 模板中来为页面提供服务。它非常适合定期更新的内容,但性能、安全性和数据备份需要一定的自定义设置。

  • 静态站点生成器 (SSG),例如 EleventyGatsby 创建预渲染文件,无需服务器端或数据库即可快速构建静态站点,在版本控制、性能和安全性都非常出色,但构建步骤和以开发人员为中心的过程可能会减慢发布速度,尤其是在大型网站上。


Next.js 是一个基于 React 的应用程序框架,它几乎没有特定于博客功能。但是,它可以提供了一种实现机制:


  1. 在可能的情况下,Next.js 生成静态内容,如 SSG,这些页面加载速度非常快,可以被搜索引擎快速收录,并且可以在任何有或没有 JavaScript 的设备上阅读。

  2. 在第一次加载后,Next.js 应用程序的行为类似于单页应用程序 (SPA),后续页面和代码会以渐进式下载,无需刷新整页。

  3. Next.js 为每个请求提供服务器端渲染 (SSR),为个人用户提供实时 CMS 更新或自定义内容变得更加容易。


如果网站可能会从基本博客迭代为更复杂的网站,例如在线商店、新闻聚合服务、社交媒体平台等,可以考虑使用 Next.js

开始

本教程正在构建的内容,可以在 GitHub 上找到完整的代码。可以通过在终端中输入以下命令,在 WindowsmacOSLinux 上下载、安装和启动它:


git clone git@github.com:QuintionTang/react-blog.gitcd react-blognpm inpm run dev
复制代码


然后在浏览器中输入 localhost:3000 打开主页。

Next.js 提供了一个 create-next-app 工具来快速开始使用应用程序模板。本教程将展示如何从头开始构建站点:如添加静态资源或者页面。

安装 Next.js 和 React

与其他 Node.js 或者 VUE 项目一样,首先创建一个目录并初始化 package.json 文件:


mkdir react-blogcd react-blognpm init
复制代码


然后安装 Next.jsReact 作为依赖项:


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 (        <>            <h1>Next.js 博客网站</h1>            <p>                这个博客网站将使用 <a 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 (        <>            <h1>Next.js 博客网站</h1>            <p>                这个博客网站将使用 <a 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>            <h1>Next.js 博客网站</h1>            <p>                这个博客网站将使用 <a 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 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>            <h1>Next.js 博客网站</h1>            <p>                这个博客网站将使用 <a 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>
<h1>关于我们</h1> <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-22tags:    - 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-matterremarkremark-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-DDexport function ymd(date) { return date instanceof Date ? `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart( 2, "0" )}-${String(date.getUTCDate()).padStart(2, "0")}` : "";}
// 格式化为 DD MMMM, YYYYexport 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 = `    <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 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>
<h1>博客列表</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> );}
/** * 获取所有文章文章的数组 * @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 linkconst 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} />;}
复制代码


最终效果如下:





发布于: 刚刚阅读数: 2
用户头像

devpoint

关注

细节的追求者 2011.11.12 加入

专注前端开发,用技术创造价值!

评论

发布
暂无评论
使用 React 和 Next.js 构建博客