nextjs入门

发布于:2025-06-21 ⋅ 阅读:(11) ⋅ 点赞:(0)

Next.js入门 - v13

什么是Next.js

Next.js是一款基于React的全栈SSR框架,提供一系列自动配置React的工具,让我们可以专注于构建应用,简化配置流程。
Next.js官网 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

什么是SSR(服务端渲染)

SPA

说到服务端渲染,就要提到SPA(单页面应用程序),单页面应用程序的例子就是React和Vue,他们是通过将js下载到客户端运行,进行编译、构建,然后创建出js虚拟DOM对象,再一次性挂载到index.html上,由于只是挂载到单个html上,比如:
SPA程序提供一个index.html文件,然后里面有一个指定了id的div标签
然后在SPA的入口js中,将程序渲染并挂载到指定的div上
其他修改都是基于它来进行,所以是单页面应用程序。并且,SPA程序会将进行渲染和逻辑处理的js文件放在客户端,渲染的工作是交给客户端进行所以也叫CSR(客户端渲染)程序。
而服务端渲染则是由服务器将HTML文件渲染后交给客户端的,根据不同请求返回对应的html文件。

对比

  • 性能
    CSR将渲染的任务交给客户端,对于性能不好的设备来说,将会变得卡顿,但是相对的,减轻了服务器的负担。而SSR则相反,在请求高峰期需要处理大量请求,这将会是巨大的压力,但是可以减轻客户端的压力。
  • SEO友好性
    客户端渲染在渲染前是很难获取到网页具体信息的,对于SEO来说并不友好,而服务端渲染则可以将渲染好的文件交给搜索引擎爬虫,此时页面内容基本完整,利于引擎算法进行分析识别。
  • 首次加载时间
    CSR需要将js下载到客户端进行渲染,所以在js下载到客户端之前有可能遭遇到一段时间的页面空白(首屏渲染问题)。而SSR在服务端先渲染好HTML,省去了这个步骤,并且减少了客户端压力,所以可以更快地向用户展示内容。

Setup Nextjs Project

Git仓库

本文的教学内容将放入GitHub仓库,可以结合着学习,不同分支将对应不同的章节。
课程仓库 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建项目

npx create-next-app@latest next-course
# or
pnpm dlx create-next-app@latest next-course
# or 
pnpm create next-app

创建项目截图
或者使用我的开源项目来创建:
rust-init-cli 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
rust-cli-panel 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

功能1:文件路由系统

在版本13中,Next.js引入了一个基于React Server Components构建的新App Router,它支持共享布局、嵌套路由、加载状态、错误处理等。
Next.js路由文档 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
Next.js会自动识别app目录中的page.jsx/.tsx文件来创建页面,而父级或同级的layout.jsx/.tsx会作为页面布局进行渲染。

来试一下

假设我们要创建一个博客网站,站点中的博客的访问路径为site.example/blog/test/,那么,让我们来创建这样的目录文件结构:

- app
    + blog
        + test
            + page.tsx
    - page.tsx
    - layout.tsx
// app/blog/test/page.tsx
export default function TestBlogPage() {
    return (
        <div>
            Hi, there is a blog page!
        </div>
    )
}

app/page.tsx中添加指向它的链接:

// app/page.tsx
export default function Home() {
    return (
        <div>
            <a href={'/blog/test'}>
                Go to test blog
            </a>
        </div>
    );
}

启动它:

npm run dev
# or 
pnpm dev

访问本地服务器
页面截图
因为默认CSS样式的原因,看起来有点怪。修改一下globals.css里的内容,将背景变得好看点:

:root {
    /*--foreground-rgb: 0, 0, 0;*/
    /*--background-start-rgb: 214, 219, 220;*/
    /*--background-end-rgb: 255, 255, 255;*/
}

效果:
首页截图
点击Go to test blog截图
Ok, 你完成了这个挑战!😎

route映射到URL

从刚才的例子我们可以看出,文件到路由的映射关系为:
file://site.dir/app/a/b/page.jsx -> site.example/a/b

动态路由

你或许注意到了,现在的URL是硬编码的,如果我们需要10个不同的blog页面(比如blog/test2, blog/abc, blog/start),那使用这种方式将要手动创建10个不同的xxx/page.js/jsx/ts/tsx,这是非常耗时且低效率的。
当然,Next.js为我们提供了名为动态路由的解决方案。
它的文件路由映射是这样的:
file://site.dir/app/blog/[slug]/page.jsx -> site.example/blog/a, site.example/blog/b, site.example/blog/c

试一下

修改我们的目录结构:

- app
    - blog
        + [slug]
            + page.tsx

创建博客内容页面:

// app/blog/[slug]/page.tsx
export default function BlogPage() {
    return (
        <div>
            {/*生成随机数*/}
            blog {Math.round(Math.random() * 10)}
        </div>
    )
}

访问blog/a截图
如果保留了刚刚的blog/test/page,会发现它依然生效

布局

Next.js提供layout文件,提供布局功能,它是在多个路由之间共享的UI。在导航时,布局保留状态、保持交互性并且不重新渲染。布局也可以嵌套。
分析一下layout文件:

import type {Metadata} from "next";
import {Inter} from "next/font/google";
import "./globals.css";
// 这个是字体,暂时不用管
const inter = Inter({subsets: ["latin"]});
// 提供页面的元数据
export const metadata: Metadata = {
    title: "Create Next App",
    description: "Generated by create next app",
};
export default function RootLayout({
                                       children,
                                   }: Readonly<{
    children: React.ReactNode;
}>) {
    return (
        <html lang="en">
        <body className={inter.className}>
        {/*children就是子目录里的内容(包含子目录的layout和page)*/}
        {children}
        </body>
        </html>
    );
}
让我们添加navbar和footbar
// components/footbar.tsx
export default function Footbar() {
    return (
        <footer className={'bg-gray-200 border-b border-b-gray-300 border-t border-t-gray-300 sticky h-20 w-full'}>
            footbar
        </footer>
    )
}
// components/navbar.tsx
export default function Navbar() {
    return (
        <nav className={'bg-green-200 border-b border-b-gray-300 sticky w-full h-20'}>
            navbar
        </nav>
    )
}

你或许还没学过TailwindCSS?看这篇文章(todo)
将它们添加到layout.tsx:

// app/layout.tsx
/// 
export default function RootLayout({
                                       children,
                                   }: Readonly<{
    children: React.ReactNode;
}>) {
    return (
        <html lang="en">
        <body className={inter.className}>
        <Navbar/>
        {/*children就是子目录里的内容(包含子目录的layout和page)*/}
        {children}
        <Footbar/>
        </body>
        </html>
    );
}

效果:
布局效果截图

提供博客内容

OK, 你现在已经有了可以匹配不同名称的动态的路由,以及创建了navbar和footbar,但是,我们希望页面能呈现更多内容。

编写Markdown

现在很多笔记软件都支持Markdown语法,这也是我本人比较喜欢的一种方式,而现在也有很多npm库支持将Markdown文件内容解析为HTML富文本串。

学习Markdown语法?
查看wiki或者等待我的新文章(todo) 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
让我们改变目录结构:

- app
    - ...
- contents
    - mountain.md
    - bird.md
    - flower.md
# mountain.md
![](https://cdn.pixabay.com/photo/2022/10/24/12/20/mountains-7543273_1280.jpg)
# Title: Summit Reflections: A Mountain's Mirror to the Soul
Climbing mountains is more than a physical endeavor; it's a journey of the spirit, where each peak reveals a facet of inner strength. As I scaled the rugged trails, the grind of my boots against rock echoed the resilience required to surmount life's challenges. The ascent, steep and demanding, taught me endurance. With each step, I found a metaphor for the effort needed to achieve one's dreams.
The view from the summit was not just a vista of vast landscapes but a perspective on the infinite possibilities life offers. It reminded me that after every great effort comes a broad expanse of opportunity. The descent, often overlooked, was no less instructive. It spoke of humility and caution; a reminder that what goes up must come down with grace.
This mountain experience distilled life into a simple yet profound truth: the journey matters as much as the destination. It's in the climb that we discover our mettle and in the view that we savor our triumphs.
(_create by AI_)
# flower.md
![](https://cdn.pixabay.com/photo/2023/03/19/05/31/flower-7861942_960_720.jpg)
# Title: Blossoming Insights: A Whiff of Flowers
As I meandered through the garden, the air was thick with the sweet perfume of blooming flowers. Each petal, a tender brushstroke on nature's canvas, painted a picture of grace and resilience. The flowers, in their silent language, spoke of beauty that survives amidst the harshest conditions.
A delicate rose, its petals softer than silk, nodded gently in the breeze. It reminded me that even the most stunning forms can emerge from thorny paths. In the blossoms, I saw a reflection of life's inherent beauty and the fortitude to flourish despite challenges.
The garden, with its kaleidoscope of colors, became a sanctuary where every flower told a story of transformation and growth. My spirits were lifted by this quiet symphony of scents and hues, a testament to nature's power to inspire and replenish the soul.
(_create by AI_)
# bird.md
![](https://cdn.pixabay.com/photo/2024/01/19/18/08/bird-8519547_1280.jpg)
# Title: Capturing the Charm of Feathered Friends
Today's venture into the serene woods was a delightful encounter with nature's delicate treasures. As I wandered through the dappled sunlight under the canopy, my camera was my faithful companion, ready to freeze moments in time.
A soft trill caught my attention, leading me to a vibrant avian presence. A tiny bird, with feathers arrayed in hues of blue and green, perched gracefully on a branch. It seemed almost aware of its own charm, bobbing and turning, as if posing for an unseen audience.
I snapped a sequence of shots, each click capturing a different angle of this natural splendor. The bird, in its innocence, carried on with its song, unaware of the beauty it bestowed upon my day.
As I left the woods, my heart felt lighter, and my camera held a piece of joy that I will cherish. These moments of connection with nature are what truly nourish the soul.
(_create by AI_)

读取并显示

安装解析Markdown需要的依赖:

npm i marked
# or
pnpm i marked

在代码中读取文件并解析:

// app/blog/[slug]/page.tsx
import {readFile} from "node:fs/promises";
import {marked} from "marked";
// 服务端组件可以使用async
export default async function BlogPage({
                                           params,
                                           searchParams,
                                       }: {
    params: { slug: string } // 接收url参数: (/blog/[slug] -> slug)
    searchParams: {}
}) {
    const text = await readFile(`./contents/${params.slug}.md`, 'utf8')
    const html = marked(text)
    return (
        <div>
            <div dangerouslySetInnerHTML={{__html: html}}></div>
        </div>
    )
}

很好,现在我们成功解析渲染了Markdown文本到页面上!
效果:
Markdown渲染效果截图

解决文字样式问题

你或许发现了,我们的标题样式和普通文字是一样的,这是因为TailwindCSS清除了默认的CSS样式,我们可以使用一个库来解决。
Next.js官方文档 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

npm i -save-dev @tailwindcss/typography
# or
pnpm i -save-dev @tailwindcss/typography

注册为Tailwind插件:

// tailwind.config.ts
const config: Config = {
    /// ...
    plugins: [
        require('@tailwindcss/typography')
    ],
};
export default config;

然后使用为组件添加prose类名:

// app/blog/[slug]/page.tsx
import {readFile} from "node:fs/promises";
import {marked} from "marked";
export default async function BlogPage({
                                           params,
                                           searchParams,
                                       }: {
    params: { slug: string } // 接收url参数: (/blog/[slug] -> slug)
    searchParams: {}
}) {
    const text = await readFile(`./contents/${params.slug}.md`, 'utf8')
    const html = marked(text)
    return (
        // flex flex-row justify-center -> 内容居中
        <div className={'w-screen flex flex-row justify-center'}>
            {/* prose让文本中的标题有对应的样式 */}
            <div className={'prose'} dangerouslySetInnerHTML={{__html: html}}></div>
        </div>
    )
}

重新启动服务,再次访问页面:
重新启动后的效果截图

功能2:提供API接口

Next.js会将api目录下的文件解析为后端接口,接收HTTP请求并进行处理。
我们将读取和解析Markdown文件的操作放到后端(api目录)去,而将渲染的工作留在前端(app目录)。

试一下

改变我们的目录结构:

- app
  + api
      + blogs
          + [slug]
              + route.ts 

此时文件系统和URL的对应关系是:

app/api/blogs/[slug]/route.ts -> site.example.com/api/blogs/a, site.example.com/api/blogs/b, ...

编写处理请求的代码:

// api/blogs/route.ts
import {NextRequest, NextResponse} from "next/server";
import {readFile} from "node:fs/promises";
import {marked} from "marked";
// 接收GET请求
export async function GET(req: NextRequest) {
    // 解析url
    let slug = req.url.slice(
        req.url.lastIndexOf('/') + 1,
        req.url.length
    )
    let html
    try {
        // 读取md文件
        const text = await readFile(`./contents/${slug}.md`, 'utf8')
        html = marked(text)
    } catch (err) {
        console.error(err)
        // 错误返回
        return NextResponse.json({error: err})
    }
    // 返回html内容
    return NextResponse.json({html})
}
// 接收POST请求
export async function POST(req: NextRequest) {
    return NextResponse.json({})
}

看看结果:
访问不存在的md文件截图
访问正常存在的文件截图
在前端请求后端数据:

// app/blog/[slug]/page.tsx
// 服务端组件可以使用async
export default async function BlogPage({
                                           params,
                                           searchParams,
                                       }: {
    params: { slug: string } // 接收url参数: (/blog/[slug] -> slug)
    searchParams: {}
}) {
    // 请求后端数据
    let res = await fetch(`http://localhost:3000/api/blogs/${params.slug}`, {
        method: "GET"
    })
    let json = await res.json()
    let html
    if (json.error)
        html = "Ooh! Something went wrong"
    else
        html = json.html
    return (
        // flex flex-row justify-center -> 内容居中
        <div className={'w-screen flex flex-row justify-center'}>
            {/* prose让文本中的标题有对应的样式 */}
            <div className={'prose'} dangerouslySetInnerHTML={{__html: html}}></div>
        </div>
    )
}

结果:
正常访问截图
访问不存在的md文件截图
到这里,我们就告一段落了,为了防止文章过长,我将其他内容放到了单独的篇章,你可以在下面点击跳转阅读。

下一步

使用next-themes进行日夜主题切换

你见过别人的网站可以切换为明亮和黑暗样式的主题吗,我们也可以实现。
Next-themes %%
博客文章 %%

使用Prisma连接数据库

通过文件系统或缓存系统存储数据是一种选择,但是使用数据库存储数据是更加常用的选择。
Prisma 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 %%

使用NextAuth进行身份验证

身份验证是应用程序常见的功能,使用NextAuth可以免去自己编写登录注册页的麻烦,专注于实现身份验证逻辑。
NextAuth %%

使用Tiptap作为富文本编辑器

Tiptap是一款现代化的无头富文本编辑器,可以让我们轻松编写好看的页面内容。

如何通过代码定制Tiptap? 查看我的最新文章(todo)
Tiptap官方文档 %%