从头学服务器组件#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) 来解决。

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

相关推荐
百万蹄蹄向前冲35 分钟前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5811 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路1 小时前
GeoTools 读取影像元数据
前端
ssshooter2 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友2 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry3 小时前
Jetpack Compose 中的状态
前端
dae bal3 小时前
关于RSA和AES加密
前端·vue.js
柳杉3 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog4 小时前
低端设备加载webp ANR
前端·算法
LKAI.4 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi