什么是nextjs

nextjs是一款基于react的全栈SSR框架,提供一系列自动配置react的工具,让我们可以专注于构建应用,简化配置流程

什么是SSR(服务端渲染)

SPA

说到服务端渲染,就要提到SPA(单页面应用程序),单页面应用程序的例子就是React和Vue,他们是通过将js下载到客户端运行,
进行编译、构建,然后创建出js虚拟DOM对象,再一次性挂载到index.html上,由于只是挂载到单个html上,比如:

SPA程序提供一个index.html文件,然后里面有一个指定了id的div标签
SPA程序提供一个index.html文件,然后里面有一个指定了id的div标签
然后在SPA的入口js中,将程序渲染并挂载到指定的div上
然后在SPA的入口js中,将程序渲染并挂载到指定的div上

其他修改都是基于它来进行,所以是单页面应用程序。
并且,SPA程序会将进行渲染和逻辑处理的js文件放在客户端,渲染的工作是交给客户端进行所以也叫CSR(客户端渲染)程序

而服务端渲染则是由服务器将HTML文件渲染后交给客户端的,根据不同请求返回对应的html文件。

对比

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

Setup Nextjs Project

git仓库

本文的教学内容将放入github仓库,可以结合着学习,不同分支将对应不同的章节

创建项目

1
2
3
4
5
npx create-next-app@latest next-course
# or
pnpm dlx create-next-app@latest next-course
# or
pnpm create next-app

或者使用我的开源项目来创建

功能1:文件路由系统

在版本 13 中,Next.js 引入了一个基于React Server Components构建的新App Router ,它支持共享布局、嵌套路由、加载状态、错误处理等。

Nextjs会自动识别app目录中的page.jsx/.tsx文件来创建页面,而父级或同级的layout.jsx/.tsx会作为页面布局进行渲染

来试一下

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

1
2
3
4
5
6
- app
+ blog
+ test
+ page.tsx
- page.tsx
- layout.tsx
jsx
1
2
3
4
5
6
7
8
9
// app/blog/test/page.tsx

export default function TestBlogPage() {
return (
<div>
Hi, there is a blog page!
</div>
)
}

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

jsx
1
2
3
4
5
6
7
8
9
10
11
// app/page.tsx

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

启动它:

1
2
3
npm run dev
# or
pnpm dev
ok, 因为默认css样式的原因,看起来有点怪
ok, 因为默认css样式的原因,看起来有点怪

修改一下globals.css里的内容,将背景变得好看点

1
2
3
4
5
:root {
/*--foreground-rgb: 0, 0, 0;*/
/*--background-start-rgb: 214, 219, 220;*/
/*--background-end-rgb: 255, 255, 255;*/
}

效果

首页
首页
点击 Go to test blog
点击 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,这是非常耗时且低效率的

当然,Nextjs为我们提供了名为动态路由的解决方案

它的文件路由映射是这样的:

file://site.dir/app/blog/[slug]/page.jsx -> site.example/blog/a, site.example/blog/b, site.example/blog/c

试一下

修改我们的目录结构

1
2
3
4
- app
- blog
+ [slug]
+ page.tsx

创建博客内容页面

jsx
1
2
3
4
5
6
7
8
9
10
// app/blog/[slug]/page.tsx

export default function BlogPage() {
return (
<div>
{/*生成随机数*/}
blog {Math.round(Math.random() * 10)}
</div>
)
}
访问blog/a到达了我们的[slug]/page
访问blog/a到达了我们的[slug]/page
如果你保留了刚刚的blog/test/page,会发现它依然生效
如果你保留了刚刚的blog/test/page,会发现它依然生效

布局

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

分析一下layout文件

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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语法?

让我们改变目录结构

1
2
3
4
5
6
- app
- ...
- contents
- mountain.md
- bird.md
- flower.md
mountain.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
![](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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
![](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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
![](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需要的依赖

1
2
3
npm i marked
# or
pnpm i marked

在代码中读取文件并解析

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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文本到页面上!

效果:

解决文字样式问题

你或许发现了, 我们的标题样式和普通文字是一样的, 这是因为tailwindcss清除了默认的css样式, 我们可以使用一个库来解决

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

注册为tailwind插件

1
2
3
4
5
6
7
8
// tailwind.config.ts
const config: Config = {
/// ...
plugins: [
require('@tailwindcss/typography')
],
};
export default config;

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

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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接口

Nextjs会将api目录下的文件解析为后端接口, 接收http请求并进行处理.

我们将读取和解析markdown文件的操作放到后端(api目录)去, 而将渲染的工作留在前端(app目录)

试一下

改变我们的目录结构

1
2
3
4
5
- app
+ api
+ blogs
+ [slug]
+ route.ts

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

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

编写处理请求的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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文件
访问不存在的md文件
访问正常存在的文件
访问正常存在的文件

在前端请求后端数据

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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文件
访问不存在的md文件

到这里, 我们就告一段落了, 为了防止文章过长, 我将其他内容放到了单独的篇章, 你可以在下面点击跳转阅读

下一步

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

你见过别人的网站可以切换为明亮和黑暗样式的主题吗, 我们也可以实现

使用prisma连接数据库

通过文件系统或缓存系统存储数据是一种选择, 但是使用数据库存储数据是更加常用的选择.

使用next-auth进行身份验证

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

使用tiptap作为富文本编辑器

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

如何通过代码定制tiptap? 查看我的最新文章(todo)


本站总访问量