从头学服务器组件#3:添加路由

这是"从头学服务器组件"系列的第 3 篇文章。这个系列的文章的来自于 Dan Abramov 所写的《RSC From Scratch. Part 1: Server Components》这篇长文,为了方便理解和学习,我将这篇长文拆分成了一个系列进行讲解。

  1. 发明 JSX
  2. 发明组件
  3. 添加路由(本文)

回顾

在上一篇文章《从头学服务器组件#2:发明组件》中,我们将博客详情页的 UI 拆分成 BlogPostPageFooter 两个组件,并通过修改 renderJSXToHTML() 函数实现,增加了对组件渲染的支持。

在这之后,我们考虑进一步为我们博客站点增加页面,引入博客主页(或叫索引页,Index Page)来展示所有博文列表。为此,我们需要增加路由功能。

定义路由

现在,访问 /hello-world 地址能看到博客详情页。我们再添加一个根路由(/)用来展示所有博博客列表,也就是博客索引页。为此,我们需要增加一个新的 BlogIndexPage 组件,与 BlogPostPage 共享布局,但包含不同内容。

提取布局组件 BlogLayout

目前,BlogPostPage 组件代表整个页面,从 <html> 标记开始写起,我们需要将这块与 BlogIndexPage 共享的页面结构(页眉和页脚)从 BlogPostPage 中提取出来,创建表示布局的 BlogLayout 组件。

jsx 复制代码
function BlogLayout({ children }) {
  const author = "Jae Doe";
  return (
    <html>
      <head>
        <title>My blog</title>
      </head>
      <body>
        <nav>
          <a href="/">Home</a>
          <hr />
        </nav>
        <main>
          {children}
        </main>
        <Footer author={author} />
      </body>
    </html>
  );
}

修改 BlogPostPage 组件,只包含我们希望插入到布局里的内容。

jsx 复制代码
// 新版
function BlogPostPage({ postSlug, postContent }) {
  return (
    <section>
      <h2>
        <a href={"/" + postSlug}>{postSlug}</a>
      </h2>
      <article>{postContent}</article>
    </section>
  );
}

旧版(作为对比),点击查看

jsx 复制代码
function BlogPostPage({ postContent, author }) {
  return (
    <html>
      <head>
        <title>My blog</title>
      </head>
      <body>
        <nav>
          <a href="/">Home</a>
          <hr />
        </nav>
        <article>
          {postContent}
        </article>
        <Footer author={author} />
      </body>
    </html>
  );
}

下面是 <BlogPostPage> 嵌套在 <BlogLayout> 中的外观:

添加一个新的 BlogIndexPage 组件,它会展示 ./posts/*.txt 下的每个博客文件。

jsx 复制代码
function BlogIndexPage({ postSlugs, postContents }) {
  return (
    <section>
      <h1>Welcome to my blog</h1>
      <div>
        {postSlugs.map((postSlug, index) => (
          <section key={postSlug}>
            <h2>
              <a href={"/" + postSlug}>{postSlug}</a>
            </h2>
            <article>{postContents[index]}</article>
          </section>
        ))}
      </div>
    </section>
  );
}

./posts 下目前只有一个 hello-world.txt 文件,我们再增加一个 vacation-time.txt 文件,内容如下:

rust 复制代码
It's me again! Haven't posted in a while because vacation.

<BlogIndexPage> 嵌套在 <BlogLayout> 里,就有了跟 BlogPostPage 一样的页眉页脚了。

增加路由导航支持

最后,修改服务器处理程序,依据 URL 提供不同的页面、加载数据、并在布局组件中呈现对应的页面。

jsx 复制代码
import { createServer } from "http";
import { readFile, readdir } from "fs/promises";
import escapeHtml from "escape-html";
import sanitizeFilename from "sanitize-filename";

createServer(async (req, res) => {
  try {
    const url = new URL(req.url, `http://${req.headers.host}`);
    // Match the URL to a page and load the data it needs.
    const page = await matchRoute(url);
    // Wrap the matched page into the shared layout.
    sendHTML(res, <BlogLayout>{page}</BlogLayout>);
  } catch (err) {
    console.error(err);
    res.statusCode = err.statusCode ?? 500;
    res.end();
  }
}).listen(8080);

async function matchRoute(url) {
  if (url.pathname === "/") {
    // We're on the index route which shows every blog post one by one.
    // Read all the files in the posts folder, and load their contents.
    const postFiles = await readdir("./posts");
    const postSlugs = postFiles.map((file) => file.slice(0, file.lastIndexOf(".")));
    const postContents = await Promise.all(
      postSlugs.map((postSlug) =>
        readFile("./posts/" + postSlug + ".txt", "utf8")
      )
    );
    return <BlogIndexPage postSlugs={postSlugs} postContents={postContents} />;
  } else {
    // We're showing an individual blog post.
    // Read the corresponding file from the posts folder.
    const postSlug = sanitizeFilename(url.pathname.slice(1));
    try {
      const postContent = await readFile("./posts/" + postSlug + ".txt", "utf8");
      return <BlogPostPage postSlug={postSlug} postContent={postContent} />;
    } catch (err) {
      throwNotFound(err);
    }
  }
}

function throwNotFound(cause) {
  const notFound = new Error("Not found.", { cause });
  notFound.statusCode = 404;
  throw notFound;
}

这里通过 matchRoute() 方法、依据 URL 获取主页或详情页数据。如果 url.pathname 值为 '/',就表示是渲染首页,否则按照详情页渲染。

现在,再次访问博客站点,查看效果(线上 demo 地址)。

主页:

详情页:

至此,我们就完成了多路由组件渲染支持。

总结

本文,我们为博客站点增加了博客主页来展示所有博文列表,为此修改了服务器部分的实现,增加了根路由(/)匹配逻辑,抽象在 matchRoute() 方法中。在这个过程中,我们还将博客主页和详情页的共享布局提取成了单独的布局组件 BlogLayout

不过,现在的代码有点冗长和重复,具体来说 BlogIndexPageBlogPostPage 主体的内容和结构重复了,这里可以考虑将这块重复的内容提取成一个组件。

另外,获取数据的逻辑也重复了,还暴露在了组件外部。那么,如果把获取博文内容的代码放在组件内部,不就解决这个问题了吗?

实际上,这些问题都可以通过下一篇要介绍的 异步函数(async components) 来解决。

本文就先说到这,下一篇再见。

相关推荐
西陵11 分钟前
Nx带来极致的前端开发体验——借助CDD&TDD开发提效
前端·javascript·架构
叹一曲当时只道是寻常12 分钟前
vue中添加原生右键菜单
javascript·vue.js
小磊哥er22 分钟前
【前端工程化】前端工作中的业务规范有哪些
前端
旷世奇才李先生27 分钟前
Next.js 安装使用教程
开发语言·javascript·ecmascript
ᥬ 小月亮32 分钟前
webpack基础
前端·webpack
YongGit1 小时前
探索 AI + MCP 渲染前端 UI
前端·后端·node.js
慧一居士1 小时前
<script setup>中的setup作用以及和不带的区别对比
前端
RainbowSea2 小时前
NVM 切换 Node 版本工具的超详细安装说明
java·前端
读书点滴2 小时前
笨方法学python -练习14
java·前端·python
Mintopia2 小时前
四叉树:二维空间的 “智能分区管理员”
前端·javascript·计算机图形学