\\n </Card>\\n </>\\n );\\n}\\n\\nexport async function generateStaticParams() {\\n const posts = getAllPosts([\\"slug\\"]);\\n return posts.map((post) => ({\\n slug: post.slug,\\n }));\\n}
可以看见,使用 RSC 后的页面相比于不使用 RSC 的页面大幅减少了代码长度。
但正因 RSC 是在 node 环境运行的,所以我们不能在 RSC 使用浏览器的 api,也无法使用 useState
等 api,也就是说,RSC 不能直接响应用户的交互。而当我们需要调用浏览器api时,可以通过在组件代码前加上 \\"use client\\";
将组件更改为客户端组件,就能调用浏览器api。
值得一提的是,我们可以在RSC中引入客户端组件:
<div>\\n This is a RSC\\n <ClientComponent />\\n</div>
但我们不能在客户端组件内引入RSC:
<ClientComponent>\\n <ServerComponent />\\n</ClientComponent>
但我们依然有办法在客户端组件内直接调用RSC。前文提到,RSC在渲染过程中需要node的参与,但是返回结果是相当于静态的,所以我们可以通过Props的方式向客户端组件传入RSC的返回值实现:
<ServerComponent>\\n const res = <AnotherSC />\\n <ClientComponent aprop={res} />\\n</ServerComponent>
在 Next.js 13 中,Next.js 新增了一种新的路由方式:App Router
。它基于 React Server Components
开发,支持共享布局,加载状态,嵌套路由,错误处理等。其中对我而言最重要的更新就是嵌套路由。
App Router 将路由拆分为布局与内容两部分,其中布局支持嵌套,大大减少了代码冗余。例如我们要实现 AB 两个页面,二者都包含一个导航栏和一个侧边栏,B 页面在以上的基础下又包含其自身所需要的布局,即需要布局嵌套,在 Pages Router
中,我们需要这样:
// A.tsx\\nexport default function A() {\\n return (\\n <LayoutA>\\n <Content />\\n </LayoutA>\\n );\\n}
// B.tsx\\nexport default function B() {\\n return (\\n <LayoutA>\\n <LayoutB>\\n <Content />\\n </LayoutB>\\n </LayoutA>\\n );\\n}
只有两层情况就已经如此糟糕,倘若有三层,四层.......代码的可读性将大大降低。但在 App Router 中,我们可以这样做:
// app/a/layout.tsx\\nexport default RootLayout({ children }: { children: ReactNode }) {\\n return (\\n <html lang=\\"zh-CN\\">\\n <body>\\n <NavBar />\\n <div>\\n <LayoutA>{children}</LayoutA>\\n </div>\\n </body>\\n </html>\\n )\\n}
// app/a/b/layout.tsx\\nexport default LayoutB({ children }: { children: ReactNode }) {\\n return <LayoutA>{children}</LayoutA>\\n}
此时,b 页面的布局会自动继承 a 的布局,并在其基础上新增其自身布局,代码会十分简洁。
除了语法更加简洁,我们也可以通过AppRouter实现共享布局。
在「使用 Next.js 重构我的博客」一文中我提到,我将博客核心所使用的 CMS 从 Hexo
迁移到自研的基于文件的 CMS,在构建时使用 Next.js Pages Router
提供的 getStaticProps
等一系列 api 在构建时从本地获取文章并渲染,但在我迁移博客到 Next.js App Router
时这样的做法无法通过构建,原因是 App Router 并不支持使用 getStaticProps
等 api 获取数据。同时,在构建时渲染 Markdown 也会导致构建速度很慢,在考虑之下,我决定放弃 SSG,迈向 SSR。
但是,如果在每次访问都渲染一次文章,就会导致服务器压力激增,客户端访问速度直线上升。显然,这种做法是极其不明智的。好在,React 18 中提供了一个 cache
方法,被 cache
包裹的方法,在传参不变的情况下不会执行方法,而是直接返回缓存值,例如:
import { cache } from \\"react\\";\\n\\nconst add = (a: number, b: number) => {\\n return a + b;\\n};\\n\\nconst cacheAdd = cache(add);\\n\\ncacheAdd(1, 2); // 3\\ncacheAdd(1, 2); // 3
如果参数不变,多次调用 cacheAdd
方法,并不会执行 add
方法,而是会直接返回缓存值。上面的代码也可以写成这样:
import { cache } from \\"react\\";\\n\\nconst add = cache((a: number, b: number) => {\\n return a + b;\\n});\\n\\nadd(1, 2); // 3\\nadd(1, 2); // 3
接下来的事情就简单了,只需要在读取和渲染文章的方法外包裹 cache
就能提高性能:
export const getPostBySlug = cache((slug: string, fields: string[] = []) => {\\n // ...\\n});
\\n顺便提一下,本人参与开发的评论系统「retalk」也大量使用了缓存提高性能
但现在依然有一个问题,现在服务端在收到请求后,会根据请求路径中的 slug 查找文章,并读取文章内容,但当文件不存在时,node 的 fs api 就会抛出错误,使 getPostBySlug
方法没有返回任何内容,进而导致服务端返回 500,所以我们需要在 getPostBySlug
方法中检测 slug 是否存在,若不存在则返回空对象而不是没有返回值:
if (!fs.existsSync(fullPath)) {\\n return items;\\n}
在调用时检测返回对象是否包含 slug:
if (!post.title) {\\n return notFound();\\n}
notFound()
是 Next.js 提供的方法,可以手动返回 404。
这时,我们再尝试访问不存在的文章,服务端会返回 404,而非 500。
在 Pages Router 中,我们可以在页面中返回 Head
组件自定义页面的元数据:
import Head from \\"next/head\\";\\n\\nexport default function PageA() {\\n return (\\n <Head>\\n <title>Your Title</title>\\n </Head>\\n );\\n}
而在 App Router 中,我们可以通过导出 metadata
的方法设置元数据:
import { Metadata } from \\"next\\";\\n\\nexport const metadata: Metadata = {\\n title: \\"Your Title\\",\\n};
\\n简介等属性同理
除了静态导出,我们还可以通过导出 generateMetadata
函数实现动态设置元数据:
export async function generateMetadata({\\n params,\\n}: {\\n params: { slug: string };\\n}): Promise<Metadata> {\\n const post = getPostBySlug(params.slug, [\\"title\\", \\"content\\"]);\\n if (!post.title) {\\n return {\\n title: `404 - ${config.name}`,\\n };\\n }\\n return {\\n title: `${post.title} - ${config.name}`,\\n description: post.content.slice(0, 200),\\n };\\n}
在以往的版本中,我使用 remark
完成 Markdown 的渲染,但我逐渐发现,remark 逐渐无法满足我的需求,所以更换到 marked
。marked 支持自定义渲染器,可以更方便的修改渲染逻辑。
本站的 markdown 渲染被封装到了一个 markdownToHtml
函数,所以更改渲染器十分方便:
export default async function markdownToHtml(markdown: string) {\\n const renderer = new marked.Renderer();\\n\\n renderer.code = function (code, language) {\\n // 添加hljs类和data-language属性\\n let lang = language ? language.toUpperCase() : \\"\\";\\n if (!language) {\\n lang = \\"TEXT\\";\\n }\\n if (language == \\"\\") {\\n language = \\"plaintext\\";\\n }\\n const highlightedCode = highlightjs(code, language);\\n return `<pre class=\\"hljs language-${lang}\\" data-language=\\"${lang}\\">\\n <code>${highlightedCode}</code>\\n </pre>`;\\n };\\n return marked.parse(markdown, { mangle: false, headerIds: false, renderer });\\n}
\\n完整的渲染器代码较长,此处仅展示部分代码,可能无法单独使用
React 无法直接将 html 嵌入到组件作为子元素使用,但提供了属性 dangerouslySetInnerHTML
供开发者显示 html 字符串,所以在这里可以这样写:
export default async function Post({ params }: { params: { slug: string } }) {\\n const post = getPostBySlug(params.slug, [\\n \\"title\\",\\n \\"date\\",\\n \\"slug\\",\\n \\"cover\\",\\n \\"content\\",\\n ]);\\n if (!post.title) {\\n return notFound();\\n }\\n const content = await markdownToHtml(post.content || \\"\\");\\n return (\\n <>\\n <Card title={post.title} cover={post.cover} label={post.date.toString()}>\\n <div>\\n <div>{post.desc}</div>\\n <div dangerouslySetInnerHTML={{ __html: content }} />\\n </div>\\n </Card>\\n </>\\n );\\n}
以往版本中,正文部分所使用的代码高亮主题是由 github-markdown-css
提供的,我逐渐发现其设计不满足我的需求,便进行了自定义。
代码高亮配色我觉得没有必要更改,但是 highlight.js
提供的主题不支持深色模式,我就不得不手写配色。我将包含深色模式的配色放到了全局变量中:
:root {\\n --pre: #fafafa;\\n --pre-comment: #6a737d;\\n --pre-string: #032f62;\\n --pre-literal: #032f62;\\n --pre-keyword: #d73a49;\\n --pre-function: #6f42c1;\\n --pre-deleted: #24292e;\\n --pre-class: #22863a;\\n --pre-property: #005cc5;\\n --pre-namespace: #6f42c1;\\n --pre-punctuation: #24292e;\\n}\\n\\n@media (prefers-color-scheme: dark) {\\n :root {\\n --pre-comment: #757575;\\n --pre-string: #977cdc;\\n --pre-literal: #c64640;\\n --pre-keyword: #77b7d7;\\n --pre-function: #86d9ca;\\n --pre-deleted: #fff;\\n --pre-class: #dfab5c;\\n --pre-property: #77b7d7;\\n --pre-namespace: #86d9ca;\\n --pre-punctuation: #fff;\\n }\\n}
然后在 highlight.js 提供的默认主题基础上做修改,将颜色从固定值改为变量:
.hljs {\\n color: var(--text);\\n background: var(--pre);\\n}\\n.hljs-doctag,\\n.hljs-keyword,\\n.hljs-meta .hljs-keyword,\\n.hljs-template-tag,\\n.hljs-template-variable,\\n.hljs-type,\\n.hljs-variable.language_ {\\n color: var(--pre-keyword);\\n}\\n.hljs-title,\\n.hljs-title.class_,\\n.hljs-title.class_.inherited__,\\n.hljs-title.function_ {\\n color: var(--pre-function);\\n}
\\n此处因篇幅原因只展示部分
原有代码块并没有直接显示语言,容易产生歧义。出现了以下设计方案:
显然第一种更显眼并具有设计感。
具体实现我使用了 before
伪类的方式,通过 attr(data-language)
读取自定义渲染器所输入的语言名称:
return `<pre class=\\"hljs language-${lang}\\" data-language=\\"${lang}\\">\\n <code>${highlightedCode}</code>\\n </pre>`;
pre::before {\\n color: var(--text-l);\\n opacity: 0.25;\\n content: attr(data-language);\\n font-size: 1.625rem;\\n font-weight: 700;\\n position: absolute;\\n right: 0.5rem;\\n}
至此完成了代码块的修改。
传统的css在使用重复布局时回产生大量冗余,例如以下三个类:
.a {\\n padding: 1.125rem;\\n color: pink;\\n}\\n\\n.b {\\n padding: 1.125rem;\\n color: skyblue;\\n}\\n\\n.c {\\n margin: 1.125rem;\\n color: pink;\\n}
可以看见,pading: 1.125rem
与 color: pink
被在css中出现了多次,现在这个css文件共有6个属性。我们可以将它们拆分,封装成4个类:
.p-125 {\\n pading: 1.125rem;\\n}\\n\\n.color-pink {\\n color: pink;\\n}\\n\\n.color-skyblue {\\n color: skyblue;\\n}\\n\\n.m-125 {\\n margin: 1.125rem;\\n}
在html中我们可以直接使用这些类的组合实现与第一种方式相同的效果,这就是原子化css。使用原子化设计的css只出现了4个属性。不难看出,原子化css能够减少css的体积。
在实际使用中,我们往往会使用已经绑定好的原子化css库,例如tailwindcss等。
但是,这种做法会导致代码中有一大串的类名,显然不够优雅。所以,我使用了 style9
,实现atomic css in js,您可以打开devtools查看效果。
以往版本的博客使用单栏设计,只留出中间一栏展示所有信息,这样做实现简单,但会导致比较单调。在新博客的设计中,我采用了「双飞翼」布局,即三栏布局:
将布局拆分为 Sidebar
与 Content
,使用Next App Router可以实现路由跳转只加载 Content
。具体实现使用 CSS Flex
布局。
如你所见,本站现在的所有组件都是卡片,基础是一个具有 title
,label
,content
,size
,cover
等众多属性的组件:
以此保证样式的统一。
深色模式能够提高用户体验,为了实现深色模式,我为深色模式单独设计了配色,并通过 @media (prefers-color-scheme: dark)
实现根据系统设置自动切换。
以往的友情链接是静态储存的,这样做难免会有局限性,在新版博客中,我将友情链接数据迁移到了GitHub仓库,并通过 Chuqi CDN
实时获取友情链接信息。
\\n你想与101互换友情链接吗,那就看看下面的步骤吧
.tk
,.ml
等免费域名(不包括付费购买的域名)*.github.io
,*.gitee.io
等域名{\\n \\"name\\": \\"Redish101 Blog\\",\\n \\"desc\\": \\"人文 科技 白日梦\\",\\n \\"icon\\": \\"https://blog.redish101.top/favicon.ico\\",\\n \\"link\\": \\"https://blog.redish101.top\\"\\n}
\\n
如果你满足前置条件,就可以开始提交。
感谢阅读
","description":"本文该渲染由 reblog 前端生成,可能存在排版问题,最佳体验请前往:https://blog.redish101.top/article/migrate-my-blog-to-nextjs-app-router-and-react-server-component 在今年三月初,我使用 Next.js 重构了我的博客。现在,随着 Next.js AppRouter 的稳定,我又将博客从 Next.js Pages Router 迁移到了 Next.js 13 AppRouter 与 React Server Components,同时…","guid":"https://blog.redish101.top/article/migrate-my-blog-to-nextjs-app-router-and-react-server-component","author":null,"authorUrl":null,"authorAvatar":null,"insertedAt":"2025-02-03T12:29:04.577Z","publishedAt":"2023-07-13T05:03:10.540Z","media":[{"url":"https://jsd.onmicrosoft.cn/gh/Redish101/cdn@src/img/20230712213628.png","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://jsd.onmicrosoft.cn/gh/Redish101/cdn@src/img/20230713131357.png","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://jsd.onmicrosoft.cn/gh/Redish101/cdn@src/img/20230713131238.png","type":"photo","width":0,"height":0,"blurhash":""}],"categories":null,"attachments":null,"extra":null,"language":null,"feeds":{"type":"feed","id":"73194030027496448","url":"https://blog.redish101.top/feed","title":"Redish101 Blog","description":"学生,人,活的","siteUrl":"https://blog.redish101.top/","image":null,"errorMessage":null,"errorAt":null,"ownerUserId":"73160515783752704"}},{"feedId":"73194030027496448","id":"109216320717168642","title":"使用Fiber Starter开始一个Golang Web后端项目","url":"https://blog.redish101.top/article/fiber-starter","content":"本文该渲染由 reblog 前端生成,可能存在排版问题,最佳体验请前往:https://blog.redish101.top/article/fiber-starter
Fiber是一个轻量级的Golang Web框架。
不同于Java Web开发,开始一个Golang Web框架是很繁琐的,你需要: 设计目录结构、制作许多简单但繁琐的小工具..........如果项目更复杂些,你还需要: docker、docker-compose、make......... 这个初始化的过程往往耗费时间。为了解决这个问题,我把自己的起手模板稍微修改了下,并公开到Github:Redish101/fiber-starter,帮助大家解决这个问题。
这个项目包含了基于Git的版本信息获取,Make构建、Docker&Docker Compose、CLI等实用的功能。
import { remark } from \\"remark\\";\\nimport html from \\"remark-html\\";\\n\\nexport default async function markdownToHtml(markdown: string) {\\n const result = await remark().use(html).process(markdown);\\n return result.toString();\\n}
XHU4YmY3XHU1NzI4XHU2M2E3XHU1MjM2XHU1M2YwXHU2MjY3XHU4ODRjXHUwMDZiXHUwMDY1XHUwMDc5XHU1MWZkXHU2NTcw
├── Makefile # Make配置文件(本项目使用Make管理构建)\\n├── bin # 构建输出目录\\n│ └── fiber-starter\\n├── cmd # CLI\\n│ ├── root.go\\n│ └── server.go\\n├── config # 一些配置文件\\n│ ├── app.go # 应用程序相关\\n│ └── version.go # 版本信息,会在构建时通过Git获取\\n├── docker-compose.yml # Docker Compose 配置文件\\n├── dockerfile # Dockerfile\\n├── go.mod\\n├── go.sum\\n├── internal # 核心部分\\n│ ├── handler # handler\\n│ │ └── home.go # 默认的demo\\n│ ├── server # 服务器相关操作\\n│ │ ├── route.go\\n│ │ └── server.go\\n│ └── utils # 零碎的小工具\\n│ └── res.go # 格式化相应\\n└── main.go
Github提供了方便的模板功能,进入本项目仓库,点击右上角的Use this template
点击Create a new repository
,在接下来的页面内更改仓库信息,完成后,点击下方Create repository from template
。
你需要在修改这几个文件,完成初始化:
修改文件开头的部分变量:
修改第一行的包名,与上一步在Makefile中设置的一致。
修改包名后,你需要使用ide的替换功能,将目录内所有的github.com/Redish101/fiber-starter
替换为你修改的包名。
\\n如果不需要Docker,请忽略此项
将21行的/bin/fiber-starter
更改为在Makefile中设置的BIN_NAME
。
修改最后一行的fiber-starter
,设置为在Makefile中设置的BIN_NAME
\\n如果不需要docker-compose,请忽略此项
根据需求更改。
根据需要,修改第五行的返回数据格式。
修改AppName
,可以选择性的删除后面的版本号。
handler应存放在internal/handler
,每个handler应该是这样的格式:
package handler\\n\\nimport (\\n \\"github.com/Redish101/fiber-starter/internal/utils\\"\\n \\"github.com/gofiber/fiber/v2\\"\\n)\\n\\nfunc Home(c *fiber.Ctx) error {\\n return utils.Res(c, true, \\"Fiber Starter成功启动\\", nil)\\n}
其中utils.Res
函数是标准化响应工具,需要传入这几个参数:
完成后,应当在internal/server/route.go
中注册路由。在initRoutes
函数末尾增加路由注册代码,它看起来应该像这样:
app.Get(\\"/\\", handler.Home)
其中\\"/\\"
为路径,handler.Home
为Handler函数。
运行make dev
。
运行make build-debug
。使用本命令构建的可执行文件可以使用gdb进行调试,且版本号为dev-xxxxxx
。
运行make all
。使用本命令构建的可执行文件不可以使用gdb进行调试,且版本号为最新的标签。
运行make build-docker
。
这篇文章就到这里了,如果觉得项目还不错的话就点点Star吧,谢谢了。
本文该渲染由 reblog 前端生成,可能存在排版问题,最佳体验请前往:https://blog.redish101.top/article/blog-v5
在这两者之间,我对React的使用更加熟练些,而且我认为使用TSX开发React应用的体验是愉悦的,所以选择React。
这两者都是十分优秀的React框架,但我一直无法成功配置Gatsby环境,而且考虑到应用以后可能使用服务端渲染,所以选择Next.js。
博客目前并没有一些复杂的功能需要使用SSR实现,为了节省性能,选择了Next.js的SSG(以后会计划开发管理后台,所以以后可能会更换为SSR)。
为了提高界面主题的美观,降低实现的难度,我设计了较为简单(简陋)的ui,并做了移动端适配,尽量让移动端的用户能有较好的体验。
最开始,我准备效仿苏卡卡,使用hexo管理文章,但在进行一段时间的开发后,发现我对hexo api的了解无法满足使用。最后,我选择将文章储存为Markdown文件,并在每次更新后将其渲染为静态页面。
在众多Markdown渲染库中,我选择使用比较简单易用的 remark
将markdown渲染为html:
import { remark } from \\"remark\\";\\nimport html from \\"remark-html\\";\\n\\nexport default async function markdownToHtml(markdown: string) {\\n const result = await remark().use(html).process(markdown);\\n return result.toString();\\n}
搞定正文的渲染,还有一个问题,由于并没有使用数据库文章信息,所以需要将文章信息放到 front matter
内,再在构建时解析,转换为js能够读取的数据格式,并储存到页面的 props
中,供前端使用。在处理 front-matter
中,我选择了 gray-matter
。
在读取Markdown并处理后,需要将文章数据传递给前端,供前端使用,但我并没有使用SSR,所以无法做到获取实时的文章数据,但得益于强大的Next.js,我们可以通过 getStaticProps
,getStaticPaths
在执行构建时获取数据,储存到props中,例如文章详情页的数据可以这样获取:
export async function getStaticPaths() {\\n const posts = getAllPosts([\\"slug\\"]);\\n\\n return {\\n paths: posts.map((post) => {\\n return {\\n params: {\\n slug: post.slug,\\n },\\n };\\n }),\\n fallback: false,\\n };\\n}\\n\\nexport async function getStaticProps({ params }: any) {\\n const post = getPostBySlug(params.slug, [\\"title\\", \\"date\\", \\"slug\\", \\"content\\"]);\\n const content = await markdownToHtml(post.content || \\"\\");\\n\\n return {\\n props: {\\n post: {\\n ...post,\\n content,\\n },\\n },\\n };\\n}
这样在前端就可以十分方便的使用数据:
export default function Post(props: props) {\\n const router = useRouter();\\n const post = props.post;\\n const title = `${post.title} | Redish101 Blog`;\\n if (!router.isFallback && !post?.slug) {\\n return <Error404 />;\\n }\\n return (\\n <>\\n <Head>\\n <title>{title}</title>\\n </Head>\\n <PostBody title={post.title} date={post.date} content={post.content} />\\n </>\\n );\\n}
在众多css in js库中,我选择了 griffel
,它可以自动生成随机的类名,对于我这种起名困难党算是个福音,其次,它使用起来也十分的顺畅,只需要使用 makeStyles()
定义样式,即可通过 useStyles()
使用样式。使用它定义样式,在编码过程中ide会给出效果较好的代码提示:
在开发的过程中节约了很多时间,生成的随机类名可以很好的避免类名重复导致的错误:
在新版博客的首页,我将原来固定的副标题替换为从一言api获取一句质量较高的话作为副标题,数据的获取与文章数据的获取一样,都使用 getStaticProps
在构建时获取,所以一言的更新频率完去取决于我的更新频率。
网站的部署有以下几个选择:服务器部署,vercel,netlify。前面说过,本站是静态网站,所以如果选择服务器部署,在每次内容更新后都需要上传到服务器,浪费时间,即使使用ci,服务器由于地域原因也无法从GitHub拉取网站源码进行构建,所以率先出局。在vercel和netlify中我选择vercel,一是使用熟练,二是对Next.js有较好的支持,若是以后更改为ssr也很方便。
Nextjs SSG网站的性能明显是要好于动态博客的,而且构建速度也比之前用Hexo的时候更快。而且自己造的轮子,自己肯定更熟悉,改起来也方便。