这是用户在 2024-10-10 11:17 为 https://www.joshwcomeau.com/blog/how-i-built-my-blog-v2/ 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Skip to content 跳到内容
JoshWComeau

How I Built My Blog
我如何建立我的博客
2024 “App Router” Edition
2024 “应用路由器” 版

Filed under 归档于
General 一般
on 
in
September 24th, 2024 2024 年 9 月 24 日.
Sep 2024.

Over the past few months, I’ve been working on a brand new version of this blog. A couple of weeks ago, I flipped the switch! Here’s a quick side-by-side:
在过去的几个月里,我一直在为这个博客制作全新的版本。几周前,我终于切换了!以下是一个快速的对比:

Blog Homepage (Old) 博客首页(旧版)

The old version of my blog, in Dark ModeThe old version of my blog, in Light Mode

Blog Homepage (New) 博客首页(新)

The new version of my blog, in Dark ModeThe new version of my blog, in Light Mode

From a design perspective, it hasn’t changed too much; I like to think that it’s a bit more refined, but the same general idea. Most of the interesting changes are under-the-hood, or hidden in the details. In this blog post, I want to share what the new stack looks like, and dig into some of those details!
从设计的角度来看,它没有太大变化;我喜欢认为它更精致了一些,但总体想法是一样的。大多数有趣的变化都是在表面下,或隐藏在细节中。在这篇博客文章中,我想分享一下新的技术堆栈是什么样的,并深入探讨其中的一些细节!

Over the years, my blog has become a surprisingly complex application. It’s over 100,000 lines of code, not counting the content. Migrating everything over was a big project, but super educational. I’ll share my honest thoughts on all of the new technology I used for this blog.
多年来,我的博客已经变成一个令人惊讶的复杂应用程序。它的代码超过100,000 行,不包括内容。迁移所有内容是一个大项目,但非常有教育意义。我会分享我对为这个博客所使用的所有新技术的真实想法。

If you’re planning on starting a blog yourself, or are thinking about using some of the technologies I’m using, this post will hopefully be quite helpful!
如果您打算自己开一个博客,或者正在考虑使用我所使用的一些技术,这篇文章希望能对您有所帮助!

Link to this headingThe core stack 核心堆栈

Let’s start with a quick list of the major technologies used by my blog:
让我们先快速列出一下我的博客所使用的主要技术:

Next.jsv15.0.0 (beta) v15.0.0(测试版)

React 反应v19.0.0 (beta) v19.0.0 (测试版)

MDXv3.0.1

MongoDBv6.5.0

This list probably seems like overkill for a blog, and a few people have asked me why I didn’t opt for a more “lightweight” alternative. There are a few reasons:
这个列表可能对于一个博客来说显得有些过于繁琐,一些人问我为什么不选择一个更“轻便”的替代方案。原因有几个:

  1. All of my blog posts are written using MDX, so I needed first-class MDX support.
    我所有的博客文章都是使用 MDX 编写的,因此我需要一流的 MDX 支持。
  2. My other main project, my course platform, uses Next.js. I wanted as little context-switching friction as possible.
    我的另一个主要项目,我的课程平台,使用 Next.js。我希望尽量减少上下文切换的摩擦。
  3. I wanted to get a bit more experience with the latest React features, things like Server Components and Actions.
    我想多了解一下最新的 React 特性,比如服务器组件和动作。

If it wasn’t for reasons 2 and 3, I probably would have given Astro(opens in new tab) a shot. I’ve also been curious about Remix(opens in new tab) for a long time! I think both are likely fantastic options.
如果不是因为理由 2 和 3,我可能会尝试一下 Astro(在新标签页中打开)。我也对 Remix(在新标签页中打开) 感到好奇很久了!我认为这两者都是非常棒的选择。

Link to this headingContent management 内容管理

I write blog posts using MDX. It’s probably the most critical part of the tech stack for me.
我使用 MDX 撰写博客文章。这可能是我技术栈中最关键的部分。

If you’re not familiar with MDX, it’s essentially a combination of Markdown and JSX. You can think of it as a superset of Markdown that provides an additional superpower: the ability to include custom React elements within the content.
如果你对 MDX 不熟悉,它本质上是 Markdown 和 JSX 的结合。你可以把它看作是 Markdown 的超集,提供了一个额外的超级功能:在内容中包含自定义的 React 元素。

With MDX, I can create interactive widgets and drop ‘em right in the middle of a blog post, like this:
使用 MDX,我可以创建交互式小部件,并将它们直接放置在博客文章的中间,就像这样:

600
Drag me! 拖我!

Taken from my blog post, An Interactive Guide to Flexbox.
取自我的博客文章,交互式 Flexbox 指南

This ability is crucial for the sorts of content I create. I didn't want to be limited by the standard set of Markdown elements (links, tables, lists…). With MDX, I can create my own elements! It feels so much more powerful than traditional Markdown, or rich-text content stored in a CMS.
这种能力对我创作的内容至关重要。我不想受到标准 Markdown 元素(链接、表格、列表等)的限制。有了 MDX,我可以创建自己的元素!这感觉比传统的 Markdown 或存储在内容管理系统中的富文本内容要强大得多。

You might be wondering: Why not go “full React”, and skip the Markdown part altogether? When I built the very first version of this blog, way back in 2017, that’s exactly what I did. Each blog post was a React component. There were two problems with this:
你可能会想:为什么不完全使用“React”,完全跳过 Markdown 部分呢?当我在 2017 年构建这个博客的第一个版本时,我就是这么做的。每篇博客文章都是一个 React 组件。这种做法有两个问题:

  1. The writing experience was awful. Having to wrap each paragraph in a <p>, for example, gets old really fast.
    写作体验糟糕透了。比如每个段落都要用 <p> 包裹起来,这真的很让人厌烦。
  2. There was no way to access the content as data. I couldn’t, for example, get a list of the 10 most recently updated blog posts, since each blog post was a chunk of code, not a database record or JSON object.
    无法将内容作为数据访问。例如,我无法获取最近更新的 10 篇博客文章的列表,因为每篇博客文章都是一段代码,而不是数据库记录或 JSON 对象。

MDX solves both of these problems, and without really sacrificing anything. I still have the full power of React when I'm writing blog posts!
MDX 解决了这两个问题,并且几乎没有牺牲任何东西。当我写博客文章时,我仍然可以充分利用 React 的所有功能!

In terms of workflow, I edit my MDX files directly in VS Code and commit them as code. Article metadata (eg. title, publish date) is set in frontmatter at the top of the file. There are some drawbacks to this method (eg. I have to re-deploy the whole app to fix a typo), but I’ve found it’s the simplest option for me.
在工作流程方面,我直接在 VS Code 中编辑我的 MDX 文件并将其作为代码提交。文章元数据(例如标题、发布日期)在文件顶部的前置信息中设置。这种方法有一些缺点(例如,我必须重新部署整个应用程序以修复一个错字),但我发现这是对我来说最简单的选择。

There are several ways to use MDX with Next.js. I'm using next-mdx-remote(opens in new tab), mostly because it’s what I use on my course platform and I want the two projects to be as similar as possible. If you’re building a brand-new blog using Next.js, it’s probably worth giving the built-in MDX support(opens in new tab) a shot; it seems a lot more straightforward.
有几种方法可以在 Next.js 中使用 MDX。我使用 next-mdx-remote(在新标签页中打开),主要是因为这是我在我的课程平台上使用的,我希望这两个项目尽可能相似。如果你正在使用 Next.js 构建一个全新的博客,值得尝试一下 内置的 MDX 支持(在新标签页中打开);这似乎要简单得多。

Link to this headingStyling and CSS 样式和 CSS

The old version of my blog used styled-components, a CSS-in-JS library. As I’ve written about previously, styled-components isn’t fully compatible with React Server Components. So, for this new blog, I've switched to Linaria(opens in new tab), via the next-with-linaria integration(opens in new tab).
我博客的旧版本使用了 styled-components,这是一个 CSS-in-JS 库。正如我之前所写的,styled-components 与 React Server Components 并不完全兼容。因此,为了这个新博客,我切换到了 Linaria(在新标签页中打开),通过 next-with-linaria 集成(在新标签页中打开)

Here’s what it looks like:
这是什么样子的:

import { styled } from '@linaria/react';

const Wrapper = styled.div`
  background: red;
`;

Linaria is an awesome tool. It offers a familiar styled API, but instead of working its magic at runtime, it instead compiles to CSS modules. This means that there is no JS runtime involved, and as a result, it’s fully compatible with React Server Components!
Linaria 是一个很棒的工具。它提供了一个熟悉的 styled API,但它不是在运行时发挥作用,而是 编译为 CSS 模块。这意味着没有 JavaScript 运行时参与,因此它与 React 服务器组件完全兼容!

Now, getting Linaria to work with Next has been an uphill battle. I ran into a few weird issues. For example, when I import React in a file without actually using it, I get this bewildering error:
现在,让 Linaria 与 Next 一起工作一直是个艰难的挑战。我遇到了一些奇怪的问题。例如,当我在一个文件中导入 React,但实际上没有 使用 它时,我得到了这个令人困惑的错误:

EvalError: TextEncoder is not defined
EvalError: TextEncoder 未定义

/node_modules/.pnpm/@wyw-in-js+transform@0.4.1_typescript@5.4.5/node_modules/@wyw-in-js/transform/lib/module.js:223
/节点_modules/.pnpm/@wyw-in-js+transform@0.4.1_typescript@5.4.5/节点_modules/@wyw-in-js/transform/lib/module.js:223

    throw new EvalError(e.message, this.callstack.join('\n| '));
抛出新的 EvalError(e.message, this.callstack.join('\n| '));

The error messages / stack traces didn’t really help, so I solved most issues by walking backwards through my changes and/or deleting random things until the error disappeared. Fortunately, all of the issues I’ve found are consistent and predictable; it’s not one of those things where the error happens sometimes, or only in production.
错误信息/堆栈跟踪并没有真正提供帮助,因此我通过回溯我的更改和/或随机删除一些东西直到错误消失来解决大部分问题。幸运的是,我发现的所有问题都是一致且可预测的;这不是那种错误偶尔发生有时,或者仅在生产环境中出现的情况。

Once I learned all of its idiosyncracies, it’s been pretty smooth sailing, though there is one significant remaining issue. And it doesn’t have to do with Linaria at all, it has to do with how Next.js handles CSS modules.
一旦我了解了它的所有特性,一切就变得相对顺利,尽管仍然有一个重要的问题。这与 Linaria 完全无关,而是与 Next.js 如何处理 CSS 模块有关。

It’s too much of a detour to cover properly in this post, but to quickly summarize: Next.js “optimistically” bundles a bunch of CSS from unrelated routes, to improve subsequent navigation speed and guarantee the correct CSS order. This blog post, for example, loads 245kb of CSS, but it only uses 47kb.
这篇文章中很难详细展开,但可以快速总结一下:Next.js “乐观”地将来自不相关路由的大量 CSS 进行打包,以提高后续导航速度并保证正确的 CSS 顺序。比如,这篇博客加载了 245kb 的 CSS,但它实际上只使用了 47kb。
Both of these numbers are the full uncompressed values. The actual amount of data sent over the wire is smaller. There is an active discussion on Github(opens in new tab) about this, and it sounds like some upcoming config options could improve the situation.
在这个关于此事的活跃讨论在Github(在新标签页打开)上,听起来一些即将推出的配置选项可能会改善这种情况。

Given all of this, I can’t really recommend Linaria. It’s a wonderful tool, but it just isn’t battle-tested enough for it to be a prudent decision for most people/teams.
考虑到这一切,我真的不能推荐 Linaria。 这是一个很棒的工具,但对于大多数人/团队来说,它的可靠性还不足以做出明智的决策。

I'm currently most excited about Pigment CSS(opens in new tab), a zero-runtime CSS-in-JS tool being developed by the team behind Material UI. In the future, it will be the CSS library used by their popular MUI component library, which means it will quickly become one of the most battle-tested CSS libraries out there.
我目前最兴奋的事情是 Pigment CSS(在新标签页中打开),这是由 Material UI 团队开发的零运行时 CSS-in-JS 工具。未来,它将成为他们受欢迎的 MUI 组件库所使用的 CSS 库,这意味着它将迅速成为市场上经过充分测试的 CSS 库之一。

It’s still early days, but once they release their version 1.0, I plan on trying to switch. Hopefully by then, Next.js has fixed the bundling issue with CSS Modules. 🤞
现在还比较早,但一旦他们发布 1.0 版本,我计划尝试切换。希望到那时,Next.js 能够解决与 CSS Modules 的打包问题。🤞

Link to this headingCode snippets 代码片段

Code snippets look very different on the new blog, thanks to a custom-designed syntax theme! Here’s a before/after:
代码片段在新的博客上看起来非常不同,这要归功于定制设计的语法主题!这是一个前后对比:

Code Snippets (Old) 代码片段(旧)

The old version of my code snippets, in Dark Mode

Code Snippets (New) 代码片段(新)

The new version of my code snippets, in Dark Mode

If you’d like to use this theme in your IDE, you can download the JSON files (dark, light). I haven’t tested it, but it uses the same grammar as VSCode and other editors, so it should work.
如果您想在您的 IDE 中使用此主题,可以下载 JSON 文件(暗色亮色)。我还没有测试过,但它使用与 VSCode 和其他编辑器相同的语法,因此它应该可以工作。

Link to this headingThe magic of static 静电的魔力

I'm using Shiki(opens in new tab) for managing the syntax highlighting. While not specifically built for React, Shiki is designed to work at compile-time, making it a perfect fit for React Server Components. This is surprisingly exciting.
我正在使用 Shiki(在新标签页中打开) 来管理语法高亮。虽然并不是专为 React 构建的,Shiki 设计为在编译时工作,使其非常适合 React 服务器组件。 这让人感到意外的兴奋。

In my old blog, I was using Prism, a typical client-side syntax highlighting library. Because all of the code gets included in the JavaScript bundle, several sacrifices have to be made:
在我旧的博客中,我使用了 Prism,这是一个典型的客户端语法高亮库。因为所有代码都包含在 JavaScript 包中,所以必须做出一些妥协:

  • We have to be very conservative about the number of languages we support, since each additional language will add kilobytes to our bundle.
    我们必须对我们支持的语言数量保持非常保守,因为每增加一种语言都会增加我们的软件包的字节数。
  • The syntax highlighting logic is lean, much simpler than the syntax highlighting inside IDEs like VS Code. This gives theme creators less control over the end result, and means we can't share themes between IDE and Prism.
    语法高亮逻辑简洁,比 VS Code 等 IDE 内部的语法高亮简单得多。这使得主题创建者对最终结果的控制减少,也意味着我们无法在 IDE 和 Prism 之间共享主题。

With the minimal set of built-in languages, Prism winds up being 26kb minified and gzipped(opens in new tab), which is incredibly small for a syntax highlighter, but still a substantial addition to the bundle.
在最小的内置语言集下,Prism 最终被 压缩到 26kb 并且 进行了 gzip 压缩(在新标签页中打开),对于一个语法高亮器来说,这个体积不可思议地小,但仍然是对包的实质性补充。

With Shiki, it adds 0kb to the JavaScript bundle, it uses the same industry-standard TextMate grammar as VS Code, and it can support dozens of languages at no additional cost.
使用 Shiki,它对 JavaScript 包的大小没有影响,它使用与 VS Code 相同的行业标准 TextMate 语法,并且可以在无需额外费用的情况下支持数十种语言。

This means that when I want to include a Haskell snippet, as I did in a random blog post I wrote years ago, it will be fully syntax-highlighted:
这意味着当我想要包含一个 Haskell 代码片段时,正如我在我几年前写的一篇随机博客文章中所做的那样,它将完全高亮显示语法:

pe58 = n
  where
  a p q = scanl (+) p $ iterate (+ 8) q
  b = [[x,y,z] | (x,(y,z)) <- zip (a 3 10) $ zip (a 5 12) (a 7 14)]
  c = zip (scanl1 (+) . map (length . filter isPrime) $ b) (iterate (+ 4) 5)
  [(n,_)] = take 1 $ dropWhile (\(_,(a,b)) -> 10*a > b) $ zip [3,5..] c

Shiki is a joy to work with as a developer. It’s incredibly flexible and extensible. For example, I created my own “annotation” logic, so that I can highlight specific lines of code:
Shiki 作为开发者合作非常愉快。它极具灵活性和可扩展性。例如,我创建了自己的“注释”逻辑,这样我就可以突出特定的代码行:

function someRandomFunction() {
  // These two lines are highlighted! You can tell by the
  // background color, and the little bump on the left.

  return 42;
}

On my old blog, syntax highlighting didn't work properly for CSS-in-JS. My template strings would be treated as a standard string, rather than a bit of injected CSS within JS:
在我旧的博客上,CSS-in-JS 的语法高亮显示无法正常工作。我的模板字符串会被视为标准字符串,而不是 JS 中注入的 CSS。

With Shiki, I was able to reuse the syntax-highlighting logic that the styled-components VSCode Extension(opens in new tab) provides. And so now, my styled-components are highlighted correctly:
通过 Shiki,我能够重用 styled-components VSCode 扩展(在新标签页中打开) 提供的语法高亮逻辑。因此,现在我的 styled-components 被正确高亮显示:

const FunkyButton = styled.button`
  position: absolute;
  background: linear-gradient(
    to bottom,
    red,
    gold
  );

  @media (min-width: 24rem) {
    &:focus {
      background: gold;
    }
  }
`;

export default FunkyButton;

As much as I love Shiki, it does have some tradeoffs.
虽然我很喜欢敷纪,但确实有一些权衡。

Because it uses a more powerful syntax-highlighting engine, it’s not as fast as other options. I was originally rendering these blog posts “on demand”, using standard Server Side Rendering rather than static compile-time HTML generation, but found that Shiki was slowing things down quite a bit, especially on pages with multiple snippets. This problem can be solved either by switching to static generation or with HTTP caching.
由于它使用更强大的语法高亮引擎,因此速度不如其他选项。我最初是“按需”渲染这些博客文章,使用标准的服务器端渲染,而不是静态编译时的 HTML 生成,但发现 Shiki 显著减慢了速度,尤其是在多个代码片段的页面上。这个问题可以通过切换到静态生成或使用 HTTP 缓存来解决。

Shiki is also memory-hungry; I ran into an issue with Node running out of memory(opens in new tab), and had to refactor to make sure I wasn't spawning multiple Shiki instances.
Shiki 也很耗内存;我遇到 节点内存不足的问题内存(在新标签中打开),并且不得不重构以确保我没有生成多个 Shiki 实例。

The biggest issue, however, is that sometimes I need syntax highlighting on the client. For example, in my Gradient Generator, the snippet changes based on how the user edits the shadows:
然而,最大的问题是,有时我需要在客户端上进行语法高亮。例如,在我的渐变生成器中,代码片段会根据用户编辑阴影的方式而变化:

There’s no way to generate this at compile-time, since the code is dynamic!
在编译时无法生成这个,因为代码是动态的!

For these cases, I have a second Shiki highlighter. This one is lighter, only supporting a small handful of languages. And it isn’t included in my standard bundles, I'm lazy-loading it with next/dynamic(opens in new tab). Since the syntax highlighting itself is slower, I'm using useDeferredValue to keep the rest of the app fast.
对于这些情况,我有一个第二个 Shiki 语法高亮器。这个更轻,只支持少数几种语言。并且它不包括在我的标准捆绑包中,我正在通过 next/dynamic(在新标签页中打开)懒加载它。由于语法高亮本身较慢,我正在使用useDeferredValue来保持应用程序的其余部分快速。

The trickiest part was that I needed both a static Server Component as well as a dynamic Client Component, in order for SSR to work correctly. I secretly swap between them on the client, after everything has loaded.
最棘手的部分是我需要两者,既有静态服务器组件,又有动态客户端组件,以便 SSR 能够正确工作。在所有内容加载完毕后,我在客户端偷偷切换它们。

Link to this headingCode playgrounds 代码游乐场

In addition to code snippets, I also have code playgrounds, little Codepen-style editors:
除了代码片段,我还有代码游乐场,类似 Codepen 的小型编辑器:

Code Playground  代码游乐场

import React from 'react';
import range from 'lodash.range';

import styles from './PrideFlag.module.css';
import { COLORS } from './constants';

function PrideFlag({
variant = 'rainbow', // rainbow | rainbow-original | trans | pan
width = 200,
numOfColumns = 10,
staggeredDelay = 100,
billow = 2,
}) {
const colors = COLORS[variant];

const friendlyWidth =
Math.round(width / numOfColumns) * numOfColumns;

const firstColumnDelay = numOfColumns * staggeredDelay * -1;

return (
<div className={styles.flag} style={{ width: friendlyWidth }}>
{range(numOfColumns).map((index) => (
<div
key={index}
className={styles.column}
style={{
'--billow': index * billow + 'px',
background: generateGradientString(colors),
animationDelay:
firstColumnDelay + index * staggeredDelay + 'ms',
}}
/>
))}
</div>
);
}

function generateGradientString(colors) {
const numOfColors = colors.length;
const segmentHeight = 100 / numOfColors;

const gradientStops = colors.map((color, index) => {
const from = index * segmentHeight;
const to = (index + 1) * segmentHeight;

return `${color} ${from}% ${to}%`;
});

return `linear-gradient(to bottom, ${gradientStops.join(', ')})`;
}

export default PrideFlag;

Taken from my blog post, Animated Pride Flags.
取自我的博客文章,动态骄傲旗帜

For React playgrounds, I use Sandpack(opens in new tab), a wonderful editor created by the folks at CodeSandbox. I’ve previously written about how I make use of Sandpack, and all of that stuff is still relevant.
对于 React 游乐场,我使用 Sandpack(在新标签页中打开),这是 CodeSandbox 团队创建的一个奇妙编辑器。我之前写过我如何使用 Sandpack,所有这些内容仍然是相关的。

For static HTML/CSS playgrounds, I'm using my own fork of agneym's Playground(opens in new tab). Sandpack does support static templates, but they rely on Service Workers, which are sometimes blocked by browser privacy settings, leading to broken user experiences.
对于静态 HTML/CSS 操作区,我正在使用我自己分支的 agneym 的 Playground(在新标签页中打开)。Sandpack 确实 支持静态模板,但它们依赖于服务工作者,这有时会被浏览器的隐私设置阻止,从而导致用户体验不佳。

Link to this headingInteractive widgets 互动小部件

Lots of folks have asked me how I build the interactive demos in my posts like this:
很多人问我如何在我的帖子中制作这样的互动演示:

Are you sure? 你确定吗?

This action cannot be undone.
此操作无法撤消。

0%
Include Elevation: 包含海拔:

Taken from my blog post, Designing Beautiful Shadows in CSS.
来自我的博客文章,在 CSS 中设计美丽的阴影

I never quite know how to answer this question 😅. I don’t use any specific libraries or packages for this, it’s all standard web development stuff. I built my own reusable <Demo> component which provides the shell and a suite of controls, and I compose it for each individual widget.
我从来不知道如何回答这个问题 😅。我没有使用任何特定的库或包,这些都是标准的网页开发内容。我构建了自己的可重用 <Demo> 组件,它提供了外壳和一系列控件,然后我为每个单独的小部件组合它。

That said, there are a couple of generic tools that help. I use React Spring(opens in new tab) to smoothly interpolate between values in a fluid, organic fashion. And I use Framer Motion(opens in new tab) for layout animations.
话虽如此,有几个通用工具可以帮助我。我使用React Spring(在新标签中打开)以流畅、自然的方式在值之间进行插值。并且我使用Framer Motion(在新标签中打开)进行布局动画。

It feels indulgent to have two separate animation libraries, especially since neither is tiny ( 19.4kb(opens in new tab) and 44.6kb(opens in new tab), respectively). I include React Spring as a core library and dynamically import Framer Motion when needed.
拥有两个独立的动画库感觉有些奢侈,特别是因为它们的体积都不小( 19.4kb(在新标签页中打开) 44.6kb(在新标签页中打开),分别)。我将 React Spring 作为核心库,并在需要时动态导入 Framer Motion。

Truthfully, though, Framer Motion should be able to do everything that React Spring can do, so if I had to pick a “desert island” animation library, it would probably be Framer Motion.
老实说,Framer Motion 应该能够做 React Spring 能做的所有事情,所以如果我必须选择一个“荒岛”动画库,那可能就是 Framer Motion。

Link to this headingDatabase stuff 数据库内容

If you’re reading this on desktop, you might’ve seen this little fella off to the side:
如果你在桌面上阅读这个,你可能会看到旁边这个小家伙:

55,911

It’s a like button! Which is kind of silly… social networks use like buttons to inform their algorithm about which pieces of content to surface. This blog has no discovery algorithm, so it serves no purpose other than being cute.
这是一个喜欢按钮!这有点傻……社交网络使用喜欢按钮来告诉他们的算法哪些内容应该被推荐。这个博客没有发现算法,因此除了可爱之外没有其他目的。

Each visitor can click the button up to 16 times, and the data is stored in MongoDB. The database record looks something like:
每位访客最多可以点击按钮 16 次,数据存储在 MongoDB 中。数据库记录的样子如下:

{
  "slug": "promises",
  "categorySlug": "javascript",
  "hits": 123456,
  "likesByUser": {
    "abc123": 16,
    "def456": 4,
    "ghi789": 16,
    // ...
  }
}

The IDs are generated based on the user’s IP address, hashed using a secret salt to preserve anonymity. This blog is deployed on Vercel, and Vercel provides the user’s IP through a header.
这些 ID 是基于用户的 IP 地址生成的,使用一个秘密盐进行哈希以保持匿名性。这个博客部署在 Vercel 上,Vercel 通过一个头部提供用户的 IP。

Originally I used IDs generated on the client and stored in localStorage, but legendary sleuth Jane Manchun Wong showed me why that was a bad idea(opens in new tab) by spamming the API endpoint and generating tens of thousands of likes. 😅
最初我使用的是在客户端生成并存储在 localStorage 中的 ID,但传奇侦探 Jane Manchun Wong 向我展示了 为什么这主意很糟糕 (在新标签中打开),因为它通过垃圾邮件发送 API 端点并生成了成千上万的点赞。😅

One of my favourite things about Next.js is that you don’t need a separate Node.js backend. The logic for liking posts is dealt with in a Route Handler(opens in new tab), which functions almost exactly like an Express endpoint.
我最喜欢 Next.js 的其中一件事就是不需要单独的 Node.js 后端。喜欢帖子逻辑的处理是在一个 路由 处理程序(在新标签页中打开) 中进行的,它的功能几乎与 Express 端点完全相同。

Link to this heading“The Details” 细节

Link to this headingElement cohesion 元素凝聚力

I spent an unreasonable amount of time on contextual styles, making sure that my generic “LEGO brick” components composed nicely together.
我花费了不合理的时间在上下文样式上,确保我的通用“乐高砖”组件能很好地组合在一起。

For example, I have an <Aside> component for sidenotes, and a <CodeSnippet> component (discussed earlier). Check out what happens when we put a <CodeSnippet> inside an <Aside>:
例如,我有一个<Aside>组件用于旁注,还有一个<CodeSnippet>组件(之前讨论过)。看看当我们把一个<CodeSnippet>放在一个<Aside>里面时发生了什么:

Compare it to a code snippet not inside a sidenote:
将其与一个不在旁注中的代码片段进行比较:

function findLargestNum(nums: Array<number>) {
  if (nums.length === 1) {
    return nums[0];
  }

  return Math.max(...nums);
}

Instead of having a transparent background and gray outline, the CodeSnippet inside the Aside gets a brown background. Other details, like the annotations and the “Copy to Clipboard” button, also have custom colors.

Instead of having a light blue background, the CodeSnippet inside the Aside gets a golden background. Other details, like the annotations and the “Copy to Clipboard” button, also have custom colors.
而不是使用浅蓝色背景,CodeSnippetAside 内部获得金色背景。其他细节,如注释和“复制到剪贴板”按钮,也有自定义颜色。

I created custom colors for all four Aside variants (info, success, warning, error), for each color theme (light, dark). Code snippets also receive different margin/padding when they’re within an Aside, and this changes based on the viewport size, as well as whether or not they’re the final child in the container. It gets quite complicated, considering all of the possible combinations!
我为所有四种Aside变体(信息、成功、警告、错误)创建了自定义颜色,适用于每种颜色主题(浅色、深色)。代码片段在Aside内时也会得到不同的边距/填充,并且这会根据视口大小以及它们是否是容器中的最后一个子元素而变化。考虑到所有可能的组合,这变得相当复杂!

This is just one example, too. Lots of other components have “adaptive” styles that change depending on their context, to make sure everything feels cohesive. It was a ton of work, but I find the result super satisfying. 😄
这只是一个例子。许多其他组件也有“自适应”样式,根据它们的上下文而变化,以确保一切感觉协调。这花费了大量的工作,但我发现结果非常令人满意。😄

Link to this headingThe rainbow 彩虹

On the desktop homepage, you might’ve noticed that there’s a big new rainbow:
在桌面主页上,您可能已经注意到有一个新的大彩虹:

Screenshot of my blog’s homepage showing a colorfun rainbow behind my 3D mascot

This rainbow responds to your cursor, segments bending towards it like iron shavings reacting to a magnet.
这个彩虹会对你的光标作出反应,像铁屑对磁铁一样向它弯曲。

There’s an extra little easter egg as well: if you hover over the rainbow for a few seconds, a little “edit” button appears. Clicking it opens the 🌈 Rainbow Configurator.
还有一个小彩蛋:如果你将鼠标悬停在彩虹上几秒钟,会出现一个小的“编辑”按钮。点击它会打开🌈 彩虹配置器

A control panel with several sliders and controls for changing the parameters of the rainbow

Here’s the twist: you’re not just changing the rainbow on your device, you’re changing it for everybody. Each change is immediately broadcast around the world, rainbows shooting through network cables and wifi signals so that we can all enjoy the rainbow you’ve designed. 💖
这是个转折:你不仅仅是在改变你设备上的彩虹,你是在为所有人改变它。每一次变化都会立即传遍全球,彩虹穿过网络电缆和 wifi 信号,让我们都能享受到你设计的彩虹。 💖

This is made possible by PartyKit(opens in new tab), a fabulous modern tool created by the illustrious Sunil Pai. It uses WebSockets so that the changes are lightning-fast. I can’t say enough good things about PartyKit. The developer experience is world-class.
这是通过 PartyKit(在新标签中打开)实现的,这是一款由杰出的 Sunil Pai 创造的绝妙现代工具。它使用 WebSockets,以便更改速度极快。我对 PartyKit 的赞美之词不胜枚举。开发者体验堪称世界一流。

One thing I failed to consider is how chaotic it would be with hundreds of people trying to edit the rainbow at the same time 😅. When I first launched the new blog, I received several bug reports from people thinking that the rainbow was glitching out, not aware that other people were wrestling over the controls. Things have calmed down now, but I should still find a way to make this clearer.
我没有考虑到的一个问题是,几百个人试图同时编辑彩虹会有多混乱😅。当我第一次推出新博客时,我收到了一些来自人们的 bug 报告,他们以为彩虹出现了故障,却不知道其他人在争抢控制权。现在情况已经平静下来,但我仍然应该找到一种方法,让这一点更清楚。

Link to this headingView Transitions 视图过渡

When navigating between pages, there should be a subtle cross-fade animation. If the header is in a new location, it should slide into place:
在页面之间导航时,应有微妙的交叉淡出动画。如果标题位于新位置,它应该滑入到位:

This uses the very-powerful View Transitions API(opens in new tab). It isn’t yet supported in all browsers, but I think it’s a neat little progressive enhancement.
这使用了非常强大的 视图过渡 API(在新标签页中打开)。目前并不是所有浏览器都支持,但我认为这是一个不错的小进步增强。

This API works by capturing virtual screenshots of the UI right before a transition, and manipulating that screenshot and the real UI, sliding and fading things around to create the illusion that two separate elements on two separate pages are the same.
该 API 通过在过渡之前捕获 UI 的虚拟截图,然后操纵该截图和真实的 UI,滑动和淡化元素,以创造出在两个不同页面上两个独立元素相同的错觉。

It’s honestly pretty tricky to work with; I think the API design is great, but the underlying problem space is just so complicated, there’s no way to avoid some complexity. Expect to run into little quirks, like things not maintaining their aspect ratio, or text being glitchy.
老实说,这很难处理;我认为 API 设计得很好,但底层问题空间实在太复杂,没办法避免一些复杂性。准备好遇到一些小问题,比如某些东西不保持其宽高比,或者文字出现故障。

I’ve found Jake Archibald’s content super helpful for wrapping my mind around View Transitions. For example, his article on handling aspect-ratio changes(opens in new tab).
我发现杰克·阿奇博尔德的内容对我理解视图过渡非常有帮助。例如,他关于处理纵横比变化(在新标签页中打开)的文章。

Getting it to work within the Next.js App Router was a bit of a challenge. I used the use-view-transitions(opens in new tab) package, and created a low-level Link component that wraps around next/link. You can check it out in the Sources pane if you’re curious!
在 Next.js 应用路由中使其工作有点挑战。我使用了 use-view-transitions(在新标签页中打开) 包,并创建了一个低级 Link 组件,将 next/link 包裹起来。如果你感兴趣,可以在 Sources 窗格中查看!

My blog finally has a search feature! You can access it by clicking the magnifying glass in the header.
我的博客终于有了搜索功能!您可以通过点击标题中的放大镜来访问它。

I'm using Algolia(opens in new tab) to do all the hard stuff, like fuzzy matching. At some point, I may feed all of my blog post data to an AI agent and make a chatbot, but for now, basic search seems to do the trick.
我正在使用 Algolia(在新标签页中打开)来处理所有复杂的事情,比如模糊匹配。在某个时候,我可能会将我所有的博客文章数据输入到一个 AI 代理中并制作一个聊天机器人,但现在,基本搜索似乎足够用了。

One cute little detail: clicking the “trash” icon will clear the search term, but I set it up so that it isn’t instantaneous. I wanted it to seem like the trash can was gobbling up each character. 😄
一个可爱的小细节:点击“垃圾桶”图标会清除搜索词,但我设置成不即时清除。我想让它看起来像垃圾桶在吞噬每个字符。😄

Link to this headingModern outline icons 现代轮廓图标

At first glance, the icons on this site seem pretty much like the old icons, but they’ve been refined. Many of them have new micro-interactions!
乍一看,这个网站上的图标 似乎 和旧图标差不多,但它们已经被优化了。许多图标都有新的微交互!

My process for this involves starting with the icons from Feather Icons(opens in new tab), since they fit my aesthetic well. Then, I either pick apart or reconstruct their SVG so that I can animate independent parts.
我的过程涉及从Feather Icons(在新标签页中打开)开始,因为它们非常符合我的美学。然后,我要么拆解它们的 SVG,要么重构它,以便我可以对独立部分进行动画处理。

For example, I have an arrow bullet that stretches out on hover:
例如,我有一个在悬停时伸展的箭头图标:

I started by grabbing the SVG code for Feather Icons’ ArrowRight, and turning it into JSX. The final code looks something like this:
我开始抓取 Feather Icons 的 ArrowRight 的 SVG 代码,并将其转换为 JSX。最终代码大致如下:

import { useSpring, animated } from 'react-spring';

const SPRING_CONFIG = {
  tension: 300,
  friction: 16,
};

function IconArrowBullet({
  size = 20,
  isBooped = false,
}: Props) {
  const shaftProps = useSpring({
    x2: isBooped ? 23 : 18,
    config: SPRING_CONFIG,
  });
  const tipProps = useSpring({
    points: isBooped
      ? '17 6 24 12 17 18'
      : '12 5 19 12 12 19',
    config: SPRING_CONFIG,
  });

  return (
    <svg
      fill="none"
      width={size / 16 + 'rem'}
      height={size / 16 + 'rem'}
      viewBox="0 0 24 24"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      xmlns="http://www.w3.org/2000/svg"
    >
      <animated.line
        x1="5"
        y1="12"
        y2="12"
        {...shaftProps}
      />
      <animated.polyline {...tipProps} />
    </svg>
  );
}

export default IconArrowBullet;

Like a real arrow, this icon is composed of a shaft and a tip, made with an SVG line and polyline. Using React Spring, I change the x/y values for some of the points when it’s booped. This was a process of trial and error, moving individual points until it felt right.
像真正的箭头一样,这个图标由一个箭杆和一个箭头组成,使用 SVG linepolyline 制作。当它被 轻触 时,我使用 React Spring 更改某些点的 x/y 值。这是一个反复试验的过程,移动单个点直到感觉正确。

Lots of the icons on this site are given similar micro-interactions. I even have one more special easter egg planned for one of the icons, something I didn’t quite finish in time for the launch. 😮
这个网站上的很多图标都有类似的微交互。我甚至为其中一个图标计划了一个特别的彩蛋,只是我没有及时完成以便在发布时使用。😮

Link to this headingAccessibility 无障碍性

In “The Surprising Truth About Pixels and Accessibility”, I show how using the rem unit for media queries is more accessible. It ensures that our layout adapts gracefully if the user cranks up their browser’s default font size.
“关于像素和无障碍的惊人真相” 中,我展示了如何使用 rem 单位进行媒体查询更加无障碍。它确保我们的布局能够优雅地适应用户增加浏览器默认字体大小的情况。

Every now and then, a reader would notice that my actual blog used pixel-based media queries. I wasn’t even practicing what I was preaching! What a hypocrite!
每隔一段时间,读者会注意到 我的实际博客 使用了 基于像素 的媒体查询。 我甚至没有实践我所宣扬的内容! 真是个伪君子!

When I first built the previous version of my blog, I wasn’t aware that rem-based media queries were more accessible; I discovered it while building my course platform. Retrofitting my blog to use rem-based media queries was a big job, and I didn’t want to wait until that was done to share what I had learned!
当我第一次构建我博客的旧版本时,我并不知道基于 rem 的媒体查询更具可访问性;我是在构建我的课程平台时发现的。将我的博客改造为使用基于 rem 的媒体查询是一项大工程,我不想等到完成之后再分享我所学到的知识!

And so, whenever someone emailed me about this, I would share this rationale, but I would still feel quite embarrassed about it. 😅
所以,每当有人给我发邮件询问这个,我都会分享这个理由,但我仍然会感到很尴尬。😅

Needless to say, this new blog uses rem-based media queries throughout. I’ve learned a lot about accessibility over the years (including through my own short-term disability), and I’ve applied everything I’ve learned to this new blog.
不用说,这个新博客在整个过程中都使用了基于 rem 的媒体查询。多年来,我对无障碍性学到了很多(包括通过我自己< a id=0>短期残疾),我已经将我所学到的一切应用到这个新博客中。

Of course, I’m always still learning, so if you spot anything inaccessible on this blog, please do let me know!
当然,我仍然在不断学习,所以如果你发现这个博客上有什么无法访问的内容,请 告诉我

Link to this headingApp router vs. Pages router
应用路由器与页面路由器

As I mentioned earlier, one of the biggest changes with the new blog was switching from the Pages Router to the App Router. I know lots of folks are considering making the same switch, so I wanted to share my experience, to help inform your decision.
正如我之前提到的,新的博客带来的最大变化之一是从页面路由器切换到应用路由器。我知道很多人正在考虑进行同样的切换,因此我想分享我的经验,以帮助您做出决定。

Honestly, my experience was a bit of a mixed bag 😅. Let’s start with the good stuff.
老实说,我的经历有点复杂😅。让我们先从好的开始。

The mental model is wonderful. The “Server Components” paradigm feels much more natural than getServerSideProps. There’s definitely a learning curve, but I got the hang of it pretty quickly. In addition to the improved ergonomics, the new system is more powerful. For example: in the Pages router, only the top-level route component could do backend work, whereas now, any Server Component can.
心理模型非常出色。“服务器组件”范式比getServerSideProps自然得多。确实有一个学习曲线,但我很快就掌握了。除了改善的人体工程学,新系统也更强大。例如:在页面路由器中,只有最顶层的路由组件可以进行后端工作,而现在,任何服务器组件都可以。

Another benefit with Server Components is that we no longer need to include each and every React component in our client-side bundles. This means that “static” components are omitted entirely from the bundles. It also means we can use more-powerful server-exclusive libraries like Shiki, knowing that we don’t have to worry about bundle bloat.
服务器组件的另一个好处是,我们不再需要在客户端捆绑包中包含每一个 React 组件。这意味着“静态”组件在捆绑包中被完全省略。它还意味着我们可以使用更强大的仅限服务器的库,例如 Shiki,因为我们不必担心捆绑包的膨胀。

In theory, that should lead to some pretty significant performance benefits, but that hasn’t really been my experience. In fact, the performance of my new blog is slightly worse than my old blog:
理论上,这 应该 会带来一些相当显著的性能提升,但这并不是我真实的体验。事实上,我的新博客的性能 稍差 于我的旧博客:

Lighthouse Report (Old) 灯塔报告(旧版)

Lighthouse report showing a performance score of 88

Lighthouse Report (New) 灯塔报告(新)

Lighthouse report showing a performance score of 88

There are a ton of caveats to this though:
不过,这里有很多注意事项

  1. It’s not really an apples-to-apples comparison, since I added a bunch of new features and details to the new blog. It wasn’t a straight 1:1 migration.
    这并不是真正的对比,因为我在新博客中添加了许多新功能和细节。这并不是简单的 1:1 迁移。
  2. A big contributing factor is the CSS bundling issue(opens in new tab)
    一个重要的因素是 CSS 打包 问题(在新标签中打开)
    I mentioned earlier. If you don’t use CSS Modules (or a tool that compiles to CSS Modules), you won’t run into this issue.
    我之前提到过。如果你不使用 CSS 模块(或编译成 CSS 模块的工具),你就不会遇到这个问题。
  3. Because I sprinkle so many interactions around using React Spring, a lot of otherwise-static components wound up needing to become Client Components. I don’t actually have that many Server Components.
    因为我在使用 React Spring 时进行了如此多的交互,一个 很多 本来静态的组件最终需要变成客户端组件。实际上,我并没有那么多服务器组件。
  4. It’s very possible that I’ve missed opportunities to improve performance, or have made mistakes in my implementation.
    很可能我错过了提高表现的机会,或者在实施过程中犯了错误。

It’s easy to get disheartened looking at numbers, but when I throttle my CPU/network and do side-by-side comparisons, I can’t really tell the difference. I’m a bit concerned about the SEO impact of a lower Lighthouse score, but I think if the Next team addresses the CSS bundling issue, it should wind up being roughly equivalent.
看数字让人容易感到沮丧,但当我限制 CPU/网络并进行并排比较时,我实在分不出差别。我有点担心较低的 Lighthouse 分数对 SEO 的影响,但我认为如果 Next 团队解决了 CSS 捆绑问题,最终应该大致相当。

While we’re on the topic of slow performance, the development server is much slower with the App Router. It’s gotten worse and worse as my blog has grown. Here are the current stats:
在我们谈到性能慢的问题时,开发服务器在使用应用路由器时变得更慢了。 随着我的博客不断增长,这种情况变得越来越糟。以下是当前的统计数据:

  • With the Pages Router, my dev server would take 7-12 seconds to boot up, depending on the state of the cache. With the App Router, these numbers have ballooned to 30-60+ seconds.
    使用 Pages Router,我的开发服务器启动时间为 7-12 秒,具体取决于缓存的状态。使用 App Router,这个时间膨胀到了 30-60 秒以上。
  • Hot reloading in the Pages Router felt instantaneous; By the time I tabbed from the editor to the browser, my change was there. With the App Router, it takes anywhere from 1 to 5 seconds.
    在页面路由器中,热重载感觉是瞬时的;当我从编辑器切换到浏览器时,我的更改已经生效。使用应用程序路由器,则需要 1 到 5 秒的时间。
  • Sometimes, a page load will just randomly take forever, like this:
    有时,页面加载会莫名其妙地非常慢,就像这样:
Terminal screenshot showing a 92 second compile time

It’s pretty painful 😬. When I switch gears to work on my course platform (which still uses the Pages Router), it feels like a breath of fresh air.
这很痛苦 😬。当我切换到我的课程平台工作(仍然使用 Pages Router)时,感觉就像一阵清新的空气。

I should note: because I’m using Linaria, I’ve had to opt out of Turbopack, their modern Rust-based alternative to Webpack. It’s possible that dev performance is not an issue with Turbopack enabled. But I suspect that lots of us will be in the same situation, where we need Webpack for some package or other, and it shouldn’t be this slow; the Pages router used Webpack, and it was zippy!
我应该提到:因为我在使用 Linaria,我必须选择不使用 Turbopack,它是他们基于 Rust 的现代 Webpack 替代品。启用 Turbopack 后,开发性能可能不是问题。但我怀疑我们很多人会处于同样的情况,某些包需要 Webpack,而它 不应该 这么慢;Pages 路由器使用了 Webpack,速度很快!

The good news is that the Next.js team is aware of these sorts of issues and have made dev performance a priority. The App Router is still in its infancy, and there are bound to be some growing pains. I have a lot of confidence that the Next.js team will fix this stuff (the team is awesome and they’ve already addressed many of the issues I’ve brought up!). The App Router may have been marked as “stable”, but honestly it still feels pretty nascent to me.
好消息是 Next.js 团队意识到了这些问题,并将开发性能作为优先事项。 App Router 仍处于起步阶段,必然会有一些成长的痛苦。我非常有信心 Next.js 团队会解决这些问题(团队很棒,他们已经解决了我提到的许多问题!)。虽然 App Router 被标记为“稳定”,但老实说,这在我看来仍然感觉相当初级。

The vision behind React Server Components and the App Router is inspiring. For all of the Twitter jokes about the React community “reinventing” PHP, I really do think that Meta/Vercel have done something truly remarkable, and once they work out all of the kinks, it will definitively become the best way to build React applications. But today, it feels like we’re firmly in “early adopter” territory.
React 服务器组件和应用路由的愿景令人鼓舞。尽管关于 React 社区“重新发明” PHP 的 Twitter 笑话很多,我真的认为 Meta/Vercel 做了一些真正了不起的事情,一旦他们解决了所有问题,这将无疑成为构建 React 应用程序的最佳方式。但是今天,我们似乎仍处于“早期采用者”的阶段。

I’m glad to have migrated my blog to the App Router (and I'll feel even better about it when the CSS issues are resolved 😅), but I'm also in no rush to migrate my course platform.
我很高兴把我的博客迁移到应用路由器(在 CSS 问题解决后我会更开心 😅),但我也不急于迁移我的课程平台。

Link to this headingA foundation to build on
一个可以建立的基础

I’ve been teaching React for something like 7 years now. I started teaching at a local coding bootcamp, developing their React curriculum and working with students one-on-one. I’ve published 22 articles on React through this blog. And I’ve created The Joy of React(opens in new tab), a comprehensive online course that digs into how React works and how to use it effectively.
我已经教授 React 大约 7 年了。我在一个当地的编码训练营开始教学,开发他们的 React 课程,并与学生进行一对一的指导。我通过这个博客发布了 22 篇文章 关于 React。此外,我创建了 《React 的乐趣React(在新标签页中打开),这是一个深入探讨 React 工作原理以及如何有效使用的综合在线课程。

This course is focused on the core mechanisms of React, but the final module is all about the Next.js App Router and React Server Components. In fact, the final project has you build an interactive MDX-based blog. 😄
本课程专注于 React 的核心机制,但最后一模块完全围绕 Next.js 应用路由器和 React 服务器组件。事实上,最终项目要求您构建一个互动的基于 MDX 的博客。😄

It looks like this: 看起来是这样的:

Screenshot of the final project from The Joy of React, a blog quite a bit like this oneScreenshot of the final project from The Joy of React, a blog quite a bit like this one

It’s not the most complex thing we cover in the course, but it is one of the most practical. And the best part is that you can use it as the foundation for your actual blog! This isn’t just a contrived course project, it can become your own real-world home base on the internet. 😄
这不是我们课程中最复杂的内容,但它是最实用的一部分。而且最好的部分是你可以将其 作为你实际博客的基础! 这不仅仅是一个人为设计的课程项目,它可以成为你在互联网上的真实基地。😄

You can learn more here:
您可以在这里了解更多信息:

Last updated on 最后更新于

September 24th, 2024 2024 年 9 月 24 日

# of hits 点击次数

Hi friend! Hope I didn’t startle you. Can I tell you about my newsletter?
嗨,朋友!希望我没有吓到你。我可以告诉你关于我的新闻通讯吗?