Jan 13, 2025
在寻找个人博客解决方案的过程中,我发现市面上现有的博客系统要么过于简单,要么难以深度定制。虽然有许多一键部署的博客模板,但这些魔改套壳的方案往往无法满足个性化需求。作为一名开发者,我希望打造一个真正属于自己的博客平台,不仅用于知识分享和写作积累,更要通过这个过程
提升技术能力
。在学习
Next.tsx
和Nest.tsx
的过程中,我萌生了从零开始搭建博客系统的想法。这个项目不仅能满足我对博客平台的需求,还能作为一次Node.tsx
全栈开发的实践机会。本系列将详细介绍这个项目的开发历程和技术要点,希望能为有类似需求的开发者提供参考。
Next.tsx 是一个基于 React 的开源框架,旨在帮助开发者构建高性能、易于扩展的现代 Web 应用程序
在搭建博客系统的过程中,整体的安装步骤可以参考 Next.tsx 官方文档,根据自身需求进行定制。通过执行 npx create-next-app@latest
命令即可快速启动项目,创建符合需求的基础框架。接下来,本文将详细介绍在构建过程中涉及到的关键技术要点。
Next.js采用文件路由
,根据文件夹以及文件名约定来动态生成路由,舍去了传统router路由编写,大大方便了开发者开发。
简单看下本项目目录结构:
每个根路由的基本文件结构下一般都会layout.tsx
以及page.tsx
两个文件,对应布局以及具体页面内容
layout中依靠children
挂在page页面内容。
客户端组件:使用next/navigation
的useRouter
函数,用法同vue-router
tsx"use client" import { useRouter } from "next/navigation" const router = useRouter() router.push('/xxx') ...
服务端组件:使用next/navigation
的redirect
函数
tsximport { redirect } from "next/navigation" redirect('/xxx')
组件跳转:使用next/link的<Link>
组件
<Link>
是一个内置组件,它扩展了 HTML<a>
标签以提供路由之间的预取和客户端导航。这是 Next.js 中路由之间导航的主要和推荐方式。
<Link>
特性
Link
使用的是 Next.js 的客户端路由,点击链接时无需刷新整个页面,页面切换更快。Link
指向的页面(在页面视口内的链接),提升性能。tsx<Link href={url}></Link>
一般来说在 Next.js 中,如果是应用内导航,优先使用 Link
;如果是外部导航或与 SPA 优化无关的场景,使用原生 a
标签更为合适。
具体参考官网文档,本项目暂未涉及
Next.js中的动态路由需要涉及到一些特殊的文件夹命名格式(具体见下面整理)来设置,如上图posts目录下,存在[sort]这种的特殊目录结构,用于匹配单一路径段/posts/xxx
,如图所示,可以直接在page.tsx获取params的时候获取到所需要的id以及sort等参数。
例子:访问https://wanyue.me/posts/自动化工具/11
对应的params为{id:11,sort:"自动化工具"}
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 |
Next.js支持多种常用的网络请求,像axios
、fecth
等,本项目封装了axios进行网络数据请求,一般在服务端组件
中请求
tsxclass 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" }) } }
在服务端组件中正常在页面函数中请求即可
tsxconst posts = await getPostList(queryParams)
Next.js的数据渲染主要分为两块:服务端组件和客户端组件
Next.js组件默认服务端组件,服务端组件在服务器端渲染完成后,将生成的 HTML 直接发送到客户端。客户端仅接收 HTML,减轻了 JavaScript 的负担。
主要优势
三种不同的服务器渲染策略:
在Next.js中,要使用客户端组件的话需在顶部增加use client
由于服务端组件不具备一些api以及一些页面交互逻辑的能力,这时候一般需要用到客户端组件,例如:useState
、useEffet
......
两者具体使用场景可看官网介绍:何时使用
Next,js做了许多内置优化,大大增强了用户体验。
本次主要用到了以下几种优化:<Image>
、<Font>
、<Link>
、Metadata
<Image>
Next.js 图像组件扩展了 HTML<img>
元素,具有自动图像优化功能:
两种使用:本地图片、远程图片,使用方法同原生<img>
tsximport Image from 'next/image'; <Image className="rounded-full" src={"/images/avg.png"} //src分别挂在本地图片路径和远程图片路径即可 alt="头像" width={300} height={300} ></Image>
<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> ) }
Next.js 有一个元数据 API,可用于定义应用程序元数据。 您可以通过两种方式向应用程序添加元数据:
metadata
对象或动态generateMetadata
函数。layout.js``page.js
favicon.ico
、apple-icon.jpg
和icon.jpg
:用于网站图标和图标opengraph-image.jpg
和twitter-image.jpg
:用于社交媒体图片robots.txt
:提供搜索引擎抓取的说明sitemap.xml
:提供有关网站结构的信息您可以灵活地使用这些文件作为静态元数据,也可以在您的项目中以编程方式生成它们。
通过这两个选项,Next.js 将自动<head>
为您的页面生成相关元素。
本项目使用MetaData设置页面图标、页面标题、页面描述
页面图标即用favicon.ico
以及opengraph-image.jpg
静态标题,直接export metaData对象即可
tsximport type { Metadata } from "next" export const metadata: Metadata = { title: `文章` }
页面标题模版
在根布局中,更新metadata
对象以包含模板:
tsxexport 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
模板中的 将被替换为特定的页面标题。
动态标题,比如具体id的文章页面
tsxexport async function generateMetadata({ params }: Props): Promise<Metadata> { const post = await getPostById(params.id) return { title: post.title } }
本项目采用Tailwind Css结合css的形式控制系统样式
Tailwind CSS是一个实用优先的 CSS 框架,与 Next.js 配合得非常好。
Nextjs项目初始化的时候可以直接安装,也可以手动安装
shellnpm 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)) }
tsximport { 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" )}
市面上Next.js适用的UI组件库主要包括NextUI、Headless UI、Primereact、Mantine、Ant Design等,它们各具特色,分别适用于不同的开发需求和场景。NextUI 听名字就是专门为 Next.js 量身定制的一款UI组件库,官网组件风格符合现代化审美,故本次选用NextUI。
NextUI 将 TailwindCSS 的强大功能与 React Aria 相结合,提供完整的组件(逻辑和样式),由于 NextUI 使用 TailwindCSS 作为其样式引擎,因此两者适配性非常高。
安装:
shellpnpm add @nextui-org/react framer-motion
pnpm安装需在.npmrc
中加入代码,将nextui包提升至根目录node_modules
public-hoist-pattern[]=*@nextui-org/*
Tailwindcss配置
tsconst {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
tsximport { NextUIProvider } from "@nextui-org/react" export function Providers({ children }: { children: React.ReactNode }) { return ( <NextUIProvider> {children} </NextUIProvider> ) }
app根目录下添加Providers
tsxexport 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例子
tsximport { 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>
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>
Next.js需要通过@svgr/webpack
才能直接导入SVG作为组件使用
安装@svgr/webpack
bashpnpm install @svgr/webpack
在next.config.mjs
设置
tsconst nextConfig = { webpack(config) { config.module.rules.push({ test: /\.svg$/, use: ["@svgr/webpack"] }) // 针对 SVG 的处理规则 return config } }
即可直接导入使用
目前系统设置了明暗两套主题,主要是通过**next-themes
**实现
安装:
bashpnpm 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 ……
同React、Vue一样设置.env
文件
使用还是直接process.env.BOK_AUTHOR
具体可见:Next.js 如何优雅地渲染 Markdown 文件?
采用Vercel导入Github仓库,进行自动更新部署,再通过Vercerl代理到自己服务器上。
- [ ] SEO全文搜索
- [ ] 用户认证系统(OAuth 集成)
- [ ] 评论互动功能
- [ ] 性能优化
- [ ] SEO完善
- [ ] 国际化支持
本文主要介绍了Next.js博客前端的构建要点,涉及到Next.js小白学习的大部分基础要点,希望有所帮助~
代码地址: bok-next-client