type
status
date
slug
summary
tags
category
icon
password
原文:
关于作者:
以下是译文:
我是如何建立我的博客
2024 “App Router” 版
在过去的几个月里,我一直在为这个博客编写一个全新的版本。几周前,我按下了开关!这是一个快速的并排:
(旧)
(新)
从设计的角度来看,它并没有太大变化;我喜欢认为它更精致一些,但总体思路相同。大多数有趣的变化都是在幕后,或者隐藏在细节中。在这篇博文中,我想分享新堆栈的外观,并深入研究其中的一些细节!
多年来,我的博客已经成为一个非常复杂的应用程序。它超过 100,000 行代码,这还不包括内容。迁移所有内容是一个大项目,但非常具有教育意义。我将分享我对我用于此博客的所有新技术的诚实想法。
如果您打算自己创建一个博客,或者正在考虑使用我正在使用的一些技术,那么这篇文章希望对您有所帮助!
核心堆栈
让我们从我的博客使用的主要技术的快速列表开始:
技术栈 | 版本 |
v15.0.0 (beta) | |
v19.0.0 (beta) | |
v3.0.1 | |
v6.1.0 | |
v1.17.7 | |
v2.13.8 | |
v9.7.3 | |
v11.2.10 | |
v6.5.0 | |
v5.6.2 | |
v0.0.108 |
对于博客来说,这个列表似乎有点矫枉过正,有些人问我为什么不选择更“轻量级”的替代方案。有几个原因:
- 我所有的博客文章都是使用 MDX 编写的,因此我需要一流的 MDX 支持。
- 我的另一个主要项目,我的课程平台,使用 Next.js。我希望尽可能少地进行上下文切换。
- 我想获得更多关于最新 React 功能的经验,比如 Server Components 和 Actions。
Pages Router 与 App Router
您可能知道,Next.js 最近推出了一个全新的路由系统,即 App Router。这是对路由和渲染在 React 中如何工作的根本重新构想,是 React 和 Next.js 核心团队多年工作的结果。
这个博客的上一个版本也是用 Next.js 构建的,但它使用的是旧的 Pages Router。对于我的新博客,我专门使用 App Router。
比较和对比这两种方法真的很有趣。在这篇文章的后面,我将分享我对这个新方向的看法,以及它是否值得迁移。
内容管理
我使用 MDX 撰写博客文章。对我来说,这可能是技术堆栈中最关键的部分。
如果您不熟悉 MDX,它本质上是 Markdown 和 JSX 的组合。你可以把它看作是 Markdown 的超集,它提供了一个额外的超能力:在内容中包含自定义 React 元素的能力。
使用 MDX,我可以创建交互式小部件,并将它们放在博客文章的中间,如下所示:
这种能力对于我创建的内容类型至关重要。我不想被标准的 Markdown 元素集(链接、表格、列表......使用 MDX,我可以创建自己的元素!它感觉比传统的 Markdown 或存储在 CMS 中的富文本内容强大得多。
你可能想知道:为什么不去 “完整的 React”,完全跳过 Markdown 部分呢?当我在 2017 年构建这个博客的第一个版本时,这正是我所做的。每篇博文都是一个 React 组件。
这有两个问题:
- 写作经历很糟糕。例如,必须将每个段落括在
<p>
中,这很快就会过时。
- 无法将内容作为数据进行访问。例如,我无法获取最近更新的 10 篇博客文章的列表,因为每篇博客文章都是一段代码,而不是数据库记录或 JSON 对象。
MDX 解决了这两个问题,而且没有真正牺牲任何东西。当我写博客文章时,我仍然拥有 React 的全部功能!
在工作流程方面,我直接在 VS Code 中编辑我的 MDX 文件,并将它们作为代码提交。文章元数据(例如标题、发布日期)在文件顶部的 frontmatter 中设置。这种方法有一些缺点(例如。我必须重新部署整个应用程序以修复拼写错误),但我发现这对我来说是最简单的选择。
有几种方法可以将 MDX 与 Next.js 一起使用。我正在使用 next-mdx-remote,主要是因为它是我在课程平台上使用的工具,我希望这两个项目尽可能相似。如果你正在使用 Next.js 构建一个全新的博客,那么可能值得尝试一下内置的 MDX 支持;这似乎要简单得多。
从 MDX v1 迁移到 v3
此博客的旧版本也使用了 MDX,但它使用的是版本 1。作为博客重建的一部分,我借此机会更新到最新版本 v3。
老实说,迁移有时😅非常令人沮丧。在 v1 中“正常工作”的东西在 v2/v3 中变得不受支持。在某些情况下,我能够通过安装 remark/rehype 插件来恢复旧的行为,但大多数时候我要么不得不拼凑自己的解决方案,要么手动编辑我的 MDX 以匹配新格式。
这最终是一项相当大的任务,有时令人沮丧,因为我觉得新版本没有改进。我更喜欢在 v1 中做出的权衡。😬
也就是说,某些东西在新版本中肯定更好,我的大部分抱怨都是主观的/品味问题。MDX 仍然是我所知道的用于创建交互式内容的最佳解决方案。如果我今天要启动一个全新的项目,我仍然会选择 MDX v3。
样式和 CSS
我的博客的旧版本使用了 styled-components,一个 CSS-in-JS 库。正如我之前所写的,styled-components 与 React 服务器组件并不完全兼容。所以,在这个新博客中,我通过 next-with-linaria 集成切换到了 Linaria.
这是它的样子:
Linaria 是一个很棒的工具。它提供了一个熟悉
styled
API,但它不是在运行时发挥它的魔力,而是编译为 CSS 模块。这意味着不涉及 JS 运行时,因此,它与 React 服务器组件完全兼容!现在,让 Linaria 与 Next 合作是一场艰苦的战斗。我遇到了一些奇怪的问题。例如,当我在文件中导入 React 而没有实际使用它时,我会收到这个令人困惑的错误:
错误消息/堆栈跟踪并没有真正帮助,因此我通过倒退我的更改和/或删除随机内容来解决大多数问题,直到错误消失。幸运的是,我发现的所有问题都是一致且可预测的;它不是那种有时会发生错误的事情,或者只在生产中发生错误。
一旦我了解了它的所有特质,它就相当顺利了,尽管还有一个重要的问题仍然存在。它与 Linaria 完全无关,它与 Next.js 处理 CSS 模块的方式有关。
这篇文章中无法正确介绍这太绕道了,但快速总结一下:Next.js“乐观地”捆绑了一堆来自不相关路由的 CSS,以提高后续导航速度并保证正确的 CSS 顺序。例如,这篇博文加载了 245kb 的 CSS,但它只使用了 47kb。Github上对此进行了热烈的讨论,听起来一些即将推出的配置选项可以改善这种情况。
鉴于所有这些,我真的不能推荐 Linaria。这是一个很棒的工具,但它还没有经过足够的实战测试,因此对于大多数人/团队来说,它是一个谨慎的决定。
目前,我对 Pigment CSS感到最兴奋,这是一款由 Material UI 背后的团队开发的零运行时 CSS-in-JS 工具。将来,它将成为他们流行的 MUI 组件库使用的 CSS 库,这意味着它将很快成为目前最久经考验的 CSS 库之一。
现在还处于早期阶段,但一旦他们发布 1.0 版,我打算尝试切换。希望到那时,Next.js 已经解决了 CSS 模块的捆绑问题。🤞
为什么不使用 Tailwind?
在过去的几年里,Tailwind 已成为 React 应用程序最流行的样式工具。我肯定可以通过切换到 Tailwind 来避免所有这些令人困惑的问题吗?
这对我来说不是正确的解决方案有几个原因:
• 我有大约 1500 个样式化组件,因此我需要一个提供简单迁移路径的工具。我不会手动重写数千行 CSS。
• Tailwind 根本不支持代码拆分,这意味着您的整个应用程序使用单个 CSS 文件,在每个路由上加载。因为每个声明都是它自己的重用类,所以这对大多数项目来说不是一个大问题,但这取决于你如何使用 CSS。我倾向于使用 CSS 变量做很多自定义操作,这可能会导致问题。
• 使用 Tailwind 对我来说是一种令人恼火的经历,我希望我从事的项目很有趣。Tailwind 不是我的菜。
- 我有大约 1500 个样式化组件,因此我需要一个提供简单迁移路径的工具。我不会手动重写数千行 CSS。
- Tailwind 根本不支持代码拆分,这意味着您的整个应用程序使用单个 CSS 文件,在每个路由上加载。因为每个声明都是它自己的重用类,所以这对大多数项目来说不是一个大问题,但这取决于你如何使用 CSS。我倾向于使用 CSS 变量做很多自定义操作,这可能会导致问题。
- 使用 Tailwind 对我来说是一种令人恼火的经历,我希望我从事的项目很有趣。Tailwind 不是我的菜。
代码片段
由于定制设计的语法主题,代码片段在新博客上看起来非常不同!这是之前/之后:
(旧)
(新)
新的编码字体
您可能已经注意到,旧博客和此博客之间的编码字体发生了变化!
在尝试了十几种不同的选项后,我选择了 Connary Fagen 的 Cartograph CF.这是一种非常异想天开的字体。我特别喜欢草书斜体字:
Cartograph CF 是一种付费字体。你可以直接从其铸造厂购买 Cartograph CF,但通过 Font Bros 购买可能更便宜。
静态的魔力
我正在使用 Shiki 来管理语法高亮显示。虽然不是专门为 React 构建的,但 Shiki 被设计为在编译时工作,使其非常适合 React 服务器组件。这出乎意料地令人兴奋。
在我的旧博客中,我使用的是 Prism,这是一个典型的客户端语法高亮库。因为所有代码都包含在 JavaScript 包中,所以必须做出一些牺牲:
- 我们必须非常保守地对待我们支持的语言数量,因为每增加一种语言都会为我们的 bundle 增加 KB。
- 语法高亮逻辑很精简,比 VS Code 等 IDE 中的语法高亮要简单得多。这使得主题创建者对最终结果的控制较少,并且意味着我们不能在 IDE 和 Prism 之间共享主题。
使用最少的内置语言集,Prism 最终会缩小 26kb 并压缩,这对于语法高亮器来说非常小,但仍然是捆绑包的重要补充。
使用 Shiki,它将 0kb 添加到 JavaScript 捆绑包中,它使用与 VS Code 相同的行业标准 TextMate 语法,并且可以支持数十种语言,而无需额外费用。
这意味着,当我想包含 Haskell 代码段时,就像我几年前在一篇随机写的博客文章中所做的那样,它将完全以语法突出显示:
作为开发人员,与 Shiki 合作是一种乐趣。它非常灵活且可扩展。例如,我创建了自己的 “annotation” 逻辑,以便我可以突出显示特定的代码行:
在我的旧博客上,语法高亮对 CSS-in-JS 无法正常工作。我的模板字符串将被视为标准字符串,而不是 JS 中注入的一点 CSS:
借助 Shiki,我能够重用styled-components VSCode Extension 提供的语法高亮逻辑。所以现在,我的 styled-components 被正确地高亮了:
尽管我很喜欢 Shiki,但它确实有一些权衡。
因为它使用更强大的语法高亮引擎,所以它不如其他选项快。我最初是 “按需” 渲染这些博客文章,使用标准的服务器端渲染而不是静态编译时 HTML 生成,但发现 Shiki 的速度减慢了很多,尤其是在具有多个片段的页面上。这个问题可以通过切换到静态生成或使用 HTTP 缓存来解决。
Shiki 也很渴望记忆;我遇到了 Node 内存不足的问题,不得不重构以确保我没有生成多个 Shiki 实例。
但是,最大的问题是有时我需要在客户端上突出显示语法。例如,在我的 Gradient Generator 中,代码片段会根据用户编辑阴影的方式而变化:
没有办法在编译时生成它,因为代码是动态的!
对于这些情况,我有第二个 Shiki 荧光笔。这个版本更轻量级,仅支持少数几种语言。而且它不包含在我的标准捆绑包中,我使用 next/dynamic延迟加载它。由于语法高亮显示本身的速度较慢,因此我使用 useDeferredValue 来保持应用程序的其余部分快速运行。
最棘手的部分是,我需要一个静态服务器组件和一个动态客户端组件,以便 SSR 正常工作。在所有内容加载完成后,我在客户端上秘密地在它们之间切换。
代码 playground
除了代码片段之外,我还有代码游乐场,即 Codepen 风格的小编辑器:
对于 React Playground,我使用 Sandpack,这是由 CodeSandbox 的人们创建的一个很棒的编辑器。我之前写过关于我如何使用 Sandpack 的文章,所有这些东西仍然相关。
对于静态 HTML/CSS Playground,我使用的是我自己的 agneym Playground分支。Sandpack 确实支持静态模板,但它们依赖于 Service Worker,而 Service Worker 有时会被浏览器隐私设置阻止,从而导致用户体验中断。
交互式小部件
很多人问我如何在我的帖子中构建交互式演示,如下所示:
摘自我的博客文章 Designing Beautiful Shadows in CSS。
我从来不知道如何回答这个问题😅。我没有为此使用任何特定的库或包,都是标准的 Web 开发内容。我构建了自己的可重用
<Demo>
组件,该组件提供了 shell 和一套控件,并为每个单独的小部件编写了它。也就是说,有几个通用工具可以提供帮助。我使用 React Spring以流畅、有机的方式在值之间平滑地插值。我使用 Framer Motion来制作布局动画。
不过,说实话,Framer Motion 应该能够完成 React Spring 能做的所有事情,所以如果我必须选择一个 “荒岛 desert island” 动画库,那可能是 Framer Motion。
据库内容
如果你在桌面上阅读这篇文章,你可能已经在旁边看到了这个小家伙:
这是一个赞按钮!这有点愚蠢......社交网络使用 Like 按钮来通知他们的算法要显示哪些内容。这个博客没有发现算法,所以除了可爱之外没有任何用途。
每个访问者最多可以单击该按钮 16 次,并且数据存储在 MongoDB 中。数据库记录如下所示:
这些 ID 是根据用户的 IP 地址生成的,并使用秘密盐进行哈希处理以保持匿名性。这个博客部署在 Vercel 上,Vercel 通过一个 header 提供用户的 IP。
最初,我使用的是在客户端上生成并存储在 localStorage 中的 ID,但传奇侦探 Jane Manchun Wong 通过向 API 端点发送垃圾邮件并生成数以万计的赞,向我展示了为什么这是一个坏主意😅
我最喜欢 Next.js 的一点是你不需要单独的 Node.js 后端。点赞帖子的逻辑在 Route Handler 中处理,其功能几乎与 Express 端点完全相同。
为什么不是 Server Actions? 解决这个问题的更现代的方法是使用Server Action我用它们做了实验,老实说,我认为这比得不偿失。😅 公平地说,我对 fetch + Route Handler 解决方案非常满意,所以我确实有一种 “不要修复没有损坏的东西” 的心态。如果我花更多的时间使用它们,我很可能会看到曙光。 Server Actions 也仍处于非常早期的阶段,因此我将拭目以待,看看社区如何利用它们,以及它们如何发展。
“细节”
单元内聚
我在上下文样式上花费了不合理的时间,以确保我的通用“乐高积木”组件可以很好地组合在一起。
例如,我有一个用于旁注的
<Aside>
组件,以及一个 <CodeSnippet>
组件(前面讨论过)。看看当我们将 <CodeSnippet>
放入 <Aside>
中时会发生什么:将其与不在旁注中的代码片段进行比较:
Aside
中的 CodeSnippet
没有透明背景和灰色轮廓,而是棕色背景。其他详细信息(如注释和“复制到剪贴板”按钮)也具有自定义颜色。我为每个颜色主题(浅色、深色)的所有四个
Aside
变体(info、success、warning、error)创建了自定义颜色。当代码片段位于 Aside 中时,它们也会收到不同的 margin/padding,并且这会根据视口大小以及它们是否是容器中的最后一个子对象而变化。考虑到所有可能的组合,它变得相当复杂!这也只是一个例子。许多其他组件都有 “adaptive” 样式,这些样式会根据它们的上下文而变化,以确保一切都感觉有凝聚力。这需要大量的工作,但我发现结果非常令人满意。😄
探索 和我之前的博客一样,这个博客是闭源的。造成这种情况的原因有很多,在 “为什么我的博客是闭源的” 中列举了。 也就是说,如果您想深入研究代码,仍然有希望!我在此项目上启用了 sourcemap,这意味着您可以通过浏览器 devtool 的 Sources 窗格浏览原始的未缩小代码:
视图过渡
在页面之间导航时,应该有一个微妙的交叉淡化动画。如果标头位于新位置,则应滑入到位:
这使用了非常强大的 视图转换 API .并非所有浏览器都支持它,但我认为这是一个简洁的渐进式增强功能。
此 API 的工作原理是在过渡之前捕获 UI 的虚拟屏幕截图,并操纵该屏幕截图和真实 UI,滑动和淡化内容,以产生两个不同页面上的两个单独元素相同的错觉。
老实说,这很棘手;我认为 API 设计很棒,但底层问题空间太复杂了,无法避免一些复杂性。预计会遇到一些小怪癖,例如事物无法保持其纵横比,或者文本出现故障。
我发现 Jake Archibald 的内容对我理解 View Transitions 非常有帮助。例如,他关于处理纵横比更改的文章.
让它在 Next.js App Router 中运行有点挑战。我使用了 use-view-transitions包,并创建了一个包裹
next/link
的低级 Link
组件。如果您好奇,可以在 Sources 窗格中查看它!Framer Motion 的终结?
Framer Motion 的强大功能是能够进行“布局过渡”。乍一看,View Transitions API 似乎解决了同样的问题!这是否意味着我们不应该费心学习 Framer Motion?
根据我的经验,视图过渡非常适合页面过渡,但它们在处理较小的动画(如微交互)方面表现不佳。它们无法正常处理中断;如果新的过渡在上一个过渡结束之前开始,则元素将传送到新位置。
视图过渡是工具箱中值得保留的一个很好的新工具,但我真的不认为它会取代任何东西。
搜索
我的博客终于有了搜索功能了!您可以通过单击标题中的放大镜来访问它。
我正在使用 Algolia来完成所有困难的事情,比如模糊匹配。在某些时候,我可能会将所有博客文章数据提供给 AI 代理并制作聊天机器人,但就目前而言,基本搜索似乎可以解决问题。
一个可爱的小细节:点击“垃圾桶”图标会清除搜索词,但我把它设置成不是瞬间的。我希望它看起来像是垃圾桶吞噬了每个角色。😄
现代轮廓图标
乍一看,这个网站上的图标似乎很像旧图标,但它们已经被提炼了。他们中的许多人都有新的微交互!
我的过程包括从 Feather Icons中的图标开始,因为它们非常符合我的审美。然后,我要么拆开要么重建他们的 SVG,这样我就可以为独立的部分制作动画。
例如,我有一个在悬停时伸出的箭头项目符号:
我首先获取 Feather Icons 的
ArrowRight
的 SVG 代码,并将其转换为 JSX。最终代码如下所示:就像真正的箭头一样,这个图标由一个轴和一个尖端组成,由 SVG
线
和折线
制成。使用 React Spring,我在 booped 时更改了一些点的 x/y 值。这是一个反复试验的过程,不断调整各个观点,直到感觉正确为止。这个网站上的许多图标都被赋予了类似的微交互。我什至为其中一个图标计划了一个特别的复活节彩蛋,但我没有在发布前及时完成。😮
SVG 动画
我的第三门课程将全部是关于异想天开的动画,我们几乎肯定会介绍 SVG 以及如何使用它们做这样的事情。现在还处于非常早期的阶段,但您可以加入我的时事通讯以跟踪其发展并了解它何时可用!
App router vs. Pages router
正如我之前提到的,新博客的最大变化之一是从 Pages Router 切换到 App Router。我知道很多人都在考虑做出同样的改变,所以我想分享我的经验,以帮助您做出决定。
老实说,我的经历有点好坏参半😅。让我们从好东西开始。
心智模型很棒。“Server Components” 范式感觉比
getServerSideProps
自然得多。肯定有一个学习曲线,但我很快就掌握了它的窍门。除了改进的人体工程学外,新系统还更加强大。例如:在 Pages 路由器中,只有顶级路由组件可以做后端工作,而现在,任何 Server Component 都可以。Server Components 的另一个好处是,我们不再需要将每个 React 组件都包含在我们的客户端 bundle 中。这意味着 bundle 中完全省略了 “static” 组件。这也意味着我们可以使用更强大的服务器专用库,如 Shiki,因为我们知道我们不必担心 bundle 膨胀。
从理论上讲,这应该会带来一些相当显着的性能优势,但这不是我的实际经验。事实上,我的新博客的性能比我的旧博客略差:
(旧)
(新)
不过,这有很多注意事项:
- 这并不是真正的同类比较,因为我在新博客中添加了大量新功能和细节。这不是直接的 1:1 迁移。
- 一个很大的促成因素是 CSS 捆绑问题 我之前提到过。如果您不使用 CSS Modules(或编译为 CSS Modules 的工具),则不会遇到此问题。
- 因为我使用 React Spring 进行了很多交互,所以很多原本静态的组件最终需要成为客户端组件。我实际上没有那么多 Server Components。
- 我很有可能错过了提高性能的机会,或者在实现中犯了错误。
看着数字很容易灰心丧气,但是当我限制我的 CPU/网络并进行并排比较时,我无法真正分辨出区别。我有点担心较低的 Lighthouse 分数对 SEO 的影响,但我认为如果 Next 团队解决了 CSS 捆绑问题,它最终应该大致相同。
虽然我们讨论的是性能缓慢的话题,但使用 App Router 的开发服务器要慢得多。随着我的博客发展壮大,情况变得越来越糟。以下是当前的统计数据:
- 使用 Pages Router,我的开发服务器需要 7-12 秒才能启动,具体取决于缓存的状态。使用 App Router 时,这些数字已激增至 30-60+ 秒。
- Pages Router 中的热重载感觉是瞬间的;当我从编辑器切换到浏览器时,我的更改已经在那里。使用 App Router 时,只需 1 到 5 秒。
- 有时,页面加载会随机花费很长时间,如下所示:
这很痛苦😬。当我转而在我的课程平台(仍然使用 Pages Router)上工作时,感觉就像呼吸了一股新鲜空气。
我应该注意:因为我正在使用 Linaria,所以我不得不选择退出 Turbopack,这是他们基于 Rust 的现代 Webpack 替代品。启用 Turbopack 后,开发性能可能不是问题。但我怀疑我们中的很多人都会处于同样的情况,我们需要 Webpack 来打包某个包或其他包,这不应该这么慢;Pages 路由器使用 Webpack,而且它很活泼!
好消息是,Next.js 团队已经意识到了这类问题,并已将开发性能作为优先事项。App Router 仍处于起步阶段,肯定会有一些成长的烦恼。我非常有信心 Next.js 团队会解决这个问题(这个团队很棒,他们已经解决了我提出的许多问题!App Router 可能已被标记为“稳定”,但老实说,它对我来说仍然感觉很新生。
React Server Components 和 App Router 背后的愿景令人鼓舞。对于所有关于 React 社区“重塑”PHP 的 Twitter 笑话,我真的认为 Meta/Vercel 做了一些真正了不起的事情,一旦他们解决了所有问题,它肯定会成为构建 React 应用程序的最佳方式。但今天,感觉我们坚定地处于“早期采用者”的领域。
我很高兴将我的博客迁移到 App Router(当 CSS 问题得到解决😅时,我会感觉更好),但我也不急于迁移我的课程平台。