从零开始,打造属于自己的个性博客(Next.js 前端篇)

Jan 13, 2025

#Next.js 前端篇

在寻找个人博客解决方案的过程中,我发现市面上现有的博客系统要么过于简单,要么难以深度定制。虽然有许多一键部署的博客模板,但这些魔改套壳的方案往往无法满足个性化需求。作为一名开发者,我希望打造一个真正属于自己的博客平台,不仅用于知识分享和写作积累,更要通过这个过程提升技术能力

在学习 Next.tsxNest.tsx 的过程中,我萌生了从零开始搭建博客系统的想法。这个项目不仅能满足我对博客平台的需求,还能作为一次Node.tsx全栈开发的实践机会。本系列将详细介绍这个项目的开发历程和技术要点,希望能为有类似需求的开发者提供参考。

相关博客地址

一、Next.js简介

Next.tsx 是一个基于 React 的开源框架,旨在帮助开发者构建高性能、易于扩展的现代 Web 应用程序


二、为什么选用Next.js

  • 服务端渲染(SSR)与 SEO 优化:Next.tsx 提供的服务端渲染能力,使得页面内容在服务器端渲染后返回客户端,有助于提升首屏加载速度和搜索引擎优化(SEO)效果,尤其适用于需要良好 SEO 表现的博客系统。
  • App Router 路由系统:Next.tsx 采用的全新 App Router 路由系统提供了更清晰、灵活的路由配置方式,使得开发过程更加高效。
  • 活跃的社区和完善的官方文档:Next.tsx 拥有一个活跃的开发者社区,并且提供了详尽的官方文档,能帮助开发者快速上手和解决开发中遇到的问题。
  • React 技术栈的同步学习:作为一个基于 React 的框架,Next.tsx 与 React 的技术栈高度契合,开发者可以同步学习。

三、关键技术要点

在搭建博客系统的过程中,整体的安装步骤可以参考 Next.tsx 官方文档,根据自身需求进行定制。通过执行 npx create-next-app@latest 命令即可快速启动项目,创建符合需求的基础框架。接下来,本文将详细介绍在构建过程中涉及到的关键技术要点。

1、App Router

Next.js采用文件路由,根据文件夹以及文件名约定来动态生成路由,舍去了传统router路由编写,大大方便了开发者开发。

1.1.基本路由构建

简单看下本项目目录结构:

image-20250102095517883

每个根路由的基本文件结构下一般都会layout.tsx以及page.tsx两个文件,对应布局以及具体页面内容

layout中依靠children挂在page页面内容。

image-20250102094751761

1.2.路由跳转

  • 客户端组件:使用next/navigationuseRouter函数,用法同vue-router

    tsx
    "use client" import { useRouter } from "next/navigation" const router = useRouter() router.push('/xxx') ...
  • 服务端组件:使用next/navigationredirect函数

    tsx
    import { redirect } from "next/navigation" redirect('/xxx')
  • 组件跳转:使用next/link的<Link>组件

    <Link>是一个内置组件,它扩展了 HTML<a>标签以提供路由之间的预取和客户端导航。这是 Next.js 中路由之间导航的主要和推荐方式。

    <Link>特性

    • 客户端路由Link 使用的是 Next.js 的客户端路由,点击链接时无需刷新整个页面,页面切换更快。
    • 预加载优化: Next.js 会自动预加载 Link 指向的页面(在页面视口内的链接),提升性能。
    tsx
    <Link href={url}></Link>

    一般来说在 Next.js 中,如果是应用内导航,优先使用 Link;如果是外部导航或与 SPA 优化无关的场景,使用原生 a 标签更为合适。

  • history原生

    具体参考官网文档,本项目暂未涉及

1.3.动态路由以及路由传参

Next.js中的动态路由需要涉及到一些特殊的文件夹命名格式(具体见下面整理)来设置,如上图posts目录下,存在[sort]这种的特殊目录结构,用于匹配单一路径段/posts/xxx,如图所示,可以直接在page.tsx获取params的时候获取到所需要的id以及sort等参数。

例子:访问https://wanyue.me/posts/自动化工具/11对应的params为{id:11,sort:"自动化工具"}

image-20250102095953906

1.4.文件约定整理

1️⃣文件名格式和功能整理

文件名功能描述
page.tsx页面内容文件,定义路由的主渲染内容。
layout.tsx布局文件,为当前及其子路由提供共享布局。
error.tsx错误边界文件,捕获页面或子路由的运行时错误并显示自定义错误界面。
loading.tsx加载状态文件,定义页面或子路由的数据加载时显示的占位内容。
default.tsx默认子页面内容,当某个路由路径未匹配具体页面时显示的内容。
not-found.tsx自定义 404 页面,用于处理路径未匹配的情况。
global-error.tsx全局错误边界文件,用于捕获整个应用的运行时错误(需要放在 app 根目录下)。
head.tsx自定义 <head> 标签内容,例如动态设置页面标题和元信息(在 App Router 模式中已被弃用,改用 metadata API)。
template.tsx动态布局文件,为路由提供动态布局切换功能。
route.tsx定义 API 路由,用于提供服务端接口(在 App Router 中的路由定义方式,与 Pages Router 的 api 不同)。
middleware.tsx定义全局中间件,用于处理请求的预处理,例如认证或重定向。

2️⃣特殊格式和功能整理

格式功能描述示例路由
[param]动态路由,匹配单一路径段/blog/:slug
[[...param]]可选动态路由,路径可空或多段匹配/docs/*
[...param]匹配所有路径段,至少一段/catch-all/*
@folder逻辑分组,不参与路由生成不生成路由
(folder)逻辑嵌套,不影响路由路径/login
_folder忽略文件夹,不参与路由或构建不生成路由,不可导入
segment@condition匹配特定条件的动态路由/product/electronics

2、数据请求

Next.js支持多种常用的网络请求,像axiosfecth等,本项目封装了axios进行网络数据请求,一般在服务端组件中请求

tsx
class Request { private instance: AxiosInstance constructor(config: AxiosRequestConfig) { this.instance = axios.create(config) // 请求拦截器 this.instance.interceptors.request.use( async (config: InternalAxiosRequestConfig) => { // 默认用户鉴权 if (config.url?.includes("auth")) return config const token = await getToken() config.headers["Authorization"] = "Bearer " + token return config }, (error: AxiosError) => { return Promise.reject(error) } ) // 响应拦截器 this.instance.interceptors.response.use( (response: AxiosResponse) => { return response.data }, (error: AxiosError) => { return Promise.reject(error) } ) } // 公共方法 fetchData<T>(options: AxiosRequestConfig): Promise<T> { return new Promise((resolve, reject) => { this.instance .request<any, T>(options) .then((res) => { resolve(res) }) .catch((err) => { reject(err) }) }) } get<T>(options: AxiosRequestConfig): Promise<T> { return this.fetchData<T>({ ...options, method: "GET" }) } post<T>(options: AxiosRequestConfig): Promise<T> { return this.fetchData({ ...options, method: "POST" }) } put<T>(options: AxiosRequestConfig): Promise<T> { return this.fetchData({ ...options, method: "PUT" }) } delete<T>(options: AxiosRequestConfig): Promise<T> { return this.fetchData({ ...options, method: "DELETE" }) } }

在服务端组件中正常在页面函数中请求即可

tsx
const posts = await getPostList(queryParams)

3、数据渲染

Next.js的数据渲染主要分为两块:服务端组件客户端组件

3.1.服务端组件

Next.js组件默认服务端组件,服务端组件在服务器端渲染完成后,将生成的 HTML 直接发送到客户端。客户端仅接收 HTML,减轻了 JavaScript 的负担。

主要优势

  • 安全性大大提高,可将涉密的数据直接保存在服务器上,不会暴露给客户端。
  • 首屏加载速率提高(SSR),我们可以生成 HTML 以允许用户立即查看页面,而无需等待客户端下载、解析和执行呈现页面所需的 JavaScript。

...更多优点

三种不同的服务器渲染策略:

3.2.客户端组件

在Next.js中,要使用客户端组件的话需在顶部增加use client

由于服务端组件不具备一些api以及一些页面交互逻辑的能力,这时候一般需要用到客户端组件,例如:useStateuseEffet......

两者具体使用场景可看官网介绍:何时使用


4、内置优化组件

Next,js做了许多内置优化,大大增强了用户体验。

本次主要用到了以下几种优化:<Image><Font><Link>Metadata

4.1.图像优化<Image>

Next.js 图像组件扩展了 HTML<img>元素,具有自动图像优化功能:

  • 尺寸优化:使用 WebP 和 AVIF 等现代图像格式,自动为每台设备提供正确尺寸的图像。
  • 视觉稳定性:加载图像时自动防止布局偏移。
  • 更快的页面加载:仅当图像进入视口时,才使用本机浏览器延迟加载来加载图像,并带有可选的模糊占位符。
  • 资产灵活性:按需调整图像大小,即使对于存储在远程服务器上的图像也是如此

两种使用:本地图片、远程图片,使用方法同原生<img>

tsx
import Image from 'next/image'; <Image className="rounded-full" src={"/images/avg.png"} //src分别挂在本地图片路径和远程图片路径即可 alt="头像" width={300} height={300} ></Image>

4.2.字体优化<Font>

如果需要获取和加载字体文件,则在项目中使用自定义字体可能会影响性能

Next.js 会自动优化应用程序中的字体next/font。它会在构建时下载字体文件并将其与您的其他静态资产一起托管。这意味着当用户访问您的应用程序时,不会有额外的字体网络请求,从而影响性能。

可以加载谷歌字体,也可以加载本地字体

tsx
// 谷歌字体 import { Inter } from 'next/font/google'; export const inter = Inter({ subsets: ['latin'] }); import '@/app/ui/global.css'; import { inter } from '@/app/ui/fonts'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body className={`${inter.className} antialiased`}>{children}</body> </html> ); } // 本地字体 import localFont from 'next/font/local' const myFont = localFont({ src: "../../public/fonts/LXGWWenKaiMonoScreen.ttf" }) export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( <html lang="en" className="light"> <body className={`${myFont.className} relative m-0 h-full overflow-y-auto overflow-x-hidden p-0 text-default-700`} > <>{children}</> </body> </html> ) }

4.3.MetaData

Next.js 有一个元数据 API,可用于定义应用程序元数据。 您可以通过两种方式向应用程序添加元数据:

  • 基于配置:在或文件中导出静态metadata对象或动态generateMetadata函数layout.js``page.js
  • 基于文件:Next.js 有一系列专门用于元数据目的的特殊文件:
    • favicon.icoapple-icon.jpgicon.jpg:用于网站图标和图标
    • opengraph-image.jpgtwitter-image.jpg:用于社交媒体图片
    • robots.txt:提供搜索引擎抓取的说明
    • sitemap.xml:提供有关网站结构的信息

您可以灵活地使用这些文件作为静态元数据,也可以在您的项目中以编程方式生成它们。

通过这两个选项,Next.js 将自动<head>为您的页面生成相关元素。

本项目使用MetaData设置页面图标页面标题页面描述

页面图标即用favicon.ico以及opengraph-image.jpg

页面标题

静态标题,直接export metaData对象即可

tsx
import type { Metadata } from "next" export const metadata: Metadata = { title: `文章` }

页面标题模版

在根布局中,更新metadata对象以包含模板:

tsx
export const metadata: Metadata = { title: { template: `%s | ${process.env.NEXT_PUBLIC_BOK_NAME}`, default: process.env.NEXT_PUBLIC_BOK_NAME as string }, description: process.env.NEXT_PUBLIC_BOK_NAME }

%s模板中的 将被替换为特定的页面标题。

image-20250108155027128

动态标题,比如具体id的文章页面

tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> { const post = await getPostById(params.id) return { title: post.title } }

5、系统样式

5.1.样式-TailwindCss

本项目采用Tailwind Css结合css的形式控制系统样式

Tailwind CSS是一个实用优先的 CSS 框架,与 Next.js 配合得非常好。

Nextjs项目初始化的时候可以直接安装,也可以手动安装

shell
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p

导入Tailwind样式

css
// global.css @tailwind base; @tailwind components; @tailwind utilities;

推荐两个Tailwind插件

  • prettier-plugin-tailwindcss

    tailwindcss格式化插件,搭配使用更佳

  • tailwind-merge

    用于合并tailwindcss变量语句,搭配clsx使用

    ts
    // @/lib/helper import clsx from "clsx" import { twMerge } from "tailwind-merge" export const clsxm = (...args: any[]) => { return twMerge(clsx(args)) }
    tsx
    import { clsxm } from "@/lib/helper" className={clsxm( "text-default-400 hover:text-default-700 items-center gap-1 cursor-pointer bg-white shadow-lg rounded-full border border-solid border-[#eee]", isAtTop ? "hidden" : "flex" )}

5.2.组件库-NextUI

市面上Next.js适用的UI组件库主要包括NextUI、Headless UI、Primereact、Mantine、Ant Design等,它们各具特色,分别适用于不同的开发需求和场景。NextUI 听名字就是专门为 Next.js 量身定制的一款UI组件库,官网组件风格符合现代化审美,故本次选用NextUI。

NextUI 将 TailwindCSS 的强大功能与 React Aria 相结合,提供完整的组件(逻辑和样式),由于 NextUI 使用 TailwindCSS 作为其样式引擎,因此两者适配性非常高。

安装:

shell
pnpm add @nextui-org/react framer-motion

pnpm安装需在.npmrc中加入代码,将nextui包提升至根目录node_modules

public-hoist-pattern[]=*@nextui-org/*

Tailwindcss配置

ts
const {nextui} = require("@nextui-org/react"); /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", "./src/ui/**/*.{js,ts,jsx,tsx,mdx}", "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, darkMode: "class", plugins: [nextui({ prefix: "nextui", addCommonColors: true })], };

设置NextUI Provider

tsx
import { NextUIProvider } from "@nextui-org/react" export function Providers({ children }: { children: React.ReactNode }) { return ( <NextUIProvider> {children} </NextUIProvider> ) }

app根目录下添加Providers

tsx
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( <Providers> <Header></Header> <main className="relative z-[1] pt-[4.5rem] min-h-[calc(100vh-4.5rem)] "> <Toaster position="bottom-right" /> {children} </main> <Footer></Footer> </Providers> ) }

即可使用NextUI组件

Button例子

tsx
import { Button } from "@nextui-org/react" <Button disabled={copied} onPress={onCopy} isIconOnly size="sm" fullWidth variant="light" > {copied ? ( <CopiedIcon className="h-6 w-6" /> ) : ( <CopyIcon className="h-6 w-6" /> )} </Button>

5.3.动画库-framer-motion

NextUI自身采用framer-motion实现组件动画等,本项目也采用framer-motion以此学习

NextUI安装的时候已经一并安装framer-motion

使用方法比较简单,看自身动画需要去参考官方文档即可

例子:

tsx
<motion.div whileHover={{ scale: 1.2 }} whileTap={{ scale: 0.8 }} className={commonClassName} > <Icon /> </motion.div>

5.4.SVG图标导入

Next.js需要通过@svgr/webpack才能直接导入SVG作为组件使用

安装@svgr/webpack

bash
pnpm install @svgr/webpack

next.config.mjs设置

ts
const nextConfig = { webpack(config) { config.module.rules.push({ test: /\.svg$/, use: ["@svgr/webpack"] }) // 针对 SVG 的处理规则 return config } }

即可直接导入使用


6、主题切换

目前系统设置了明暗两套主题,主要是通过**next-themes**实现

安装:

bash
pnpm add next-themes

添加全局Provider包装组件

tsx
"use client" import { NextUIProvider } from "@nextui-org/react" import { ThemeProvider as NextThemesProvider } from "next-themes" export function Providers({ children }: { children: React.ReactNode }) { return ( <NextUIProvider> <NextThemesProvider attribute="class" defaultTheme="light"> {children} </NextThemesProvider> </NextUIProvider> ) }

通过defaultTheme设置默认样式

设置主题切换组件ThemeSwitcher

tsx
"use client" import * as lodash from "lodash" import { useTheme } from "next-themes" import { useCallback, useEffect, useState } from "react" import Morning from "../../public/svgs/太阳.svg" import Night from "../../public/svgs/月亮.svg" enum Themes { "DARK" = "dark", "LIGHT" = "light" } const debouncedChangeTheme = lodash.debounce((fn: () => void) => fn(), 200) export function ThemeSwitcher() { const [mounted, setMounted] = useState(false) const { theme, setTheme } = useTheme() const changeAnimation = useCallback(() => { const morningDom = document.getElementsByClassName( "morning" )[0] as HTMLElement const nightDom = document.getElementsByClassName("night")[0] as HTMLElement const ANIMATION_CLASSES = ["-translate-y-full", "opacity-0"] if (theme === Themes.LIGHT) { nightDom.classList.add(...ANIMATION_CLASSES) morningDom.classList.remove(...ANIMATION_CLASSES) } else { nightDom.classList.remove(...ANIMATION_CLASSES) morningDom.classList.add(...ANIMATION_CLASSES) } }, [theme]) const changeTheme = () => { switch (theme) { case Themes.DARK: setTheme(Themes.LIGHT) break case Themes.LIGHT: setTheme(Themes.DARK) break default: setTheme(Themes.LIGHT) } changeAnimation() } const callbackRef = useCallback( (ref: HTMLDivElement | null) => { if (!ref) return changeAnimation() }, [changeAnimation] ) useEffect(() => { setMounted(true) }, []) if (!mounted) return null return ( <div className="p-2"> <div className="relative flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-solid bg-gradient-to-b from-orange-400 to-yellow-400 p-2 dark:from-[#39598a] dark:to-[#79d7ed]" onClick={() => debouncedChangeTheme(changeTheme)} > <div ref={callbackRef} className="morning absolute h-6 w-6 transform duration-1000 ease-in-out" > <Morning className="h-full w-full"></Morning> </div> <div className="night absolute h-6 w-6 transform duration-1000 ease-in-out"> <Night className="h-full w-full"></Night> </div> </div> </div> ) }

通过使用nest-themes中的useTheme钩子可直接修改主题

修改后结合TailwindCss的colors可实现颜色自动切换

例如全局文字颜色:text-default-700

也可以通过dark:***来设置,比如全局明暗高亮色

ts
// tailwind.config.ts theme: { extend: {}, colors: { ...{ highlight: { light: "#61B9AF", dark: "pink" } } } },
css
文字高亮:text-highlight-light dark:text-highlight-dark; 背景高亮:bg-highlight-light dark:bg-highlight-dark ……

7、环境变量

同React、Vue一样设置.env文件

image-20250109143707419

使用还是直接process.env.BOK_AUTHOR


8、MarkDown渲染

具体可见:Next.js 如何优雅地渲染 Markdown 文件?


9、自动化部署

采用Vercel导入Github仓库,进行自动更新部署,再通过Vercerl代理到自己服务器上。


四、未来规划

近期计划

- [ ] SEO全文搜索

- [ ] 用户认证系统(OAuth 集成)

- [ ] 评论互动功能

长期规划

- [ ] 性能优化

- [ ] SEO完善

- [ ] 国际化支持


五、小结

本文主要介绍了Next.js博客前端的构建要点,涉及到Next.js小白学习的大部分基础要点,希望有所帮助~

代码地址: bok-next-client

目录