从头学服务器组件#4:发明异步组件

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

  1. 发明 JSX[2]
  2. 发明组件[3]
  3. 添加路由[4]
  4. 发明异步组件(本文)

回顾

在上一篇文章《从头学服务器组件#3:添加路由》[5]中,我们为博客站点添加了路由,引入了博客首页。实现过程中,我们还将博客主页和详情页的共享布局提取成了单独的布局组件 BlogLayout

在结束的时候,我们总结了 2 个发现的问题。

  1. 具体来说 BlogIndexPageBlogPostPage 主体的内容和结构有些重复了
  2. 另外,获取数据的逻辑也重复了,暴露在了组件之外

下面,我们就着手解决。

抽象异步组件

抽象 Post 组件

关于 BlogIndexPageBlogPostPage 组件中重复的部分,我们可以提取出一个 Post 组件来。

function Post({ slug, content }// Someone needs to pass down the `content` prop from the file :-(
  return (
    <section>
      <h2>
        <a href={"/" + slug}>{slug}</a>
      </h2>
      <article>{content}</article>
    </section>

  )
}

这是我们首先会想到的提取方式。

不过,我们仍然需要从外部将博文内容(content)传递进来。目前,获取博文内容的逻辑还在组件外部(也就是下面框出来的 2 个地方)。

从头学服务器组件#4:发明异步组件

这两块逻辑是重复的,都使用了 readFile API 来获取博文内容。而之所以把这块逻辑放在外面,是因为获取内容是一个异步行为,目前我们的组件还不支持异步渲染。

注意:案例中的 readFile API 其实是模拟从数据库获取数据。虽然 readFile API 有同步版本,但并不符合真实场景,因此我们有意忽略了。另外,到目前为止,我们只针对服务器环境进行实现,因此我们选用readFile API 了来模拟数据的获取。

增加组件异步渲染支持

下面,让我们为组件增加异步支持。首先,移除 content prop,将 Post 组件从 function Post() {} 改成 function async Post() {},也就是改成 async 函数,这样就可以在组件内部使用 await readFile() 加载博文内容了。

async function Post({ slug }{
  let content;
  try {
    content = await readFile("./posts/" + slug + ".txt""utf8");
  } catch (err) {
    throwNotFound(err);
  }
  return (
    <section>
      <h2>
        <a href={"/" + slug}>{slug}</a>
      </h2>
      <article>{content}</article>
    </section>

  )
}

同理,我们将 BlogIndexPage 也改成 async 函数,内部通过 await readdir() 获取博文列表。

async function BlogIndexPage({
  const postFiles = await readdir("./posts");
  const postSlugs = postFiles.map((file) =>
    file.slice(0, file.lastIndexOf("."))
  );
  return (
    <section>
      <h1>Welcome to my blog</h1>
      <div>
        {postSlugs.map((slug) => (
          <Post key={slug} slug={slug} />
        ))}
      </div>
    </section>

  );
}

抽象 <Router> 组件

现在, PostBlogIndexPage 都改成在内部获取内容了。接着,再用 <Router> 组件来替换之前的 matchRoute() 方法。

// 新版
function Router({ url }{
  let page;
  if (url.pathname === "/") {
    page = <BlogIndexPage />;
  } else {
    const postSlug = sanitizeFilename(url.pathname.slice(1));
    page = <BlogPostPage postSlug={postSlug} />;
  }
  return <BlogLayout>{page}</BlogLayout>;
}

// 旧版,对比查看。
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);
    }
  }
}

会发现,代码上会简洁很多。最后,createServer 中将渲染任务委托给 <Router>

// 新版
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);

/
/ 旧版,对比查看。
createServer(async (req, res) => {
  try {
    const url = new URL(req.url, `http:/
/${req.headers.host}`);
    await sendHTML(res, <Router url={url} /
>);
  } catch (err) {
    console.error(err);
    res.statusCode = err.statusCode ?? 500;
    res.end();
  }
}).listen(8080);

renderJSXToHTML() 的异步渲染支持

组件现在换成 async 函数的写法了,但 renderJSXToHTML() 并没有对此做兼容。

else if (typeof jsx.type === "function") {
  const Component = jsx.type;
  const props = jsx.props;
  const returnedJsx = Component(props); // <--- 组件还是以同步方式处理的
  return renderJSXToHTML(returnedJsx);
else throw new Error("Not implemented.");

为了支持异步组件渲染,我们需要在调用组件时使用 await

    // ...
    const returnedJsx = await Component(props);
    // ...

如此,renderJSXToHTML() 也要改成 async 函数了(配合内部 await)。

async function renderJSXToHTML(jsx)  {
  // ...
}

这样修改后,树中的任何组件都可以是 async 的,而且也能“等待”最终 HTML 字符串的生成。

需要注意的是,最新的 BlogIndexPage 中只是获取了博文列表的标题(slug),具体博文内容还是在 Post 中获取的。

还有BlogPostPage ,改起来也挺简单。

// 从
function BlogPostPage({ postSlug, postContent }{
  return (
    <section>
      <h2>
        <a href={"/" + postSlug}>{postSlug}</a>
      </h2>
      <article>{postContent}</article>
    </section>

  );
}
// 改成 ->
function BlogPostPage({ postSlug }{
  return <Post slug={postSlug} />;
}

这样,就修改完所有的地方了。现在重新访问,查看效果(线上 demo 地址[6])。

主页:

从头学服务器组件#4:发明异步组件

详情页:

从头学服务器组件#4:发明异步组件

依然成功展示了,说明我们的修改是没问题的。

需要注意的是,await 实现方式并不理想,因为除非所有 HTML 字符串都成功了,否则永远不会发送回 HTML 给浏览器,也就是渲染过程本身是“阻塞”的。理想情况下,我们希望服务器负载会流式传输给浏览器,也就是一边生成内容,一边发送回浏览器。这个实现比较复杂,现在我们只用只关注数据流向,流式传输不会在“从头学服务器组件”这一系列中讨论,不过可以在这里简单讲讲概念。

关于流式传输,后续支持的过程中,不需要我们对组件本身进行任何更改。每个组件只使用 await 来等待自己的数据(这是不可避免的),但父组件不需要 await 子组件——即便子组件也是 async 的—— 这就是为什么 react 可以在子组件完成渲染前就能流出父组件的原因。

总结

引入了异步组件后,我们解决了渲染内容重复的问题,代码更加内聚,博文内容的获取统一安排在了 Post 组件中,整体代码也变得清爽很多。

到目前为止,我们已经实现了具备首页和详情页浏览功能的博客站点。不过,现在的页面渲染模式还很复古,页面与页面之间的 DOM 树和状态是隔离的——在主页和详情页之间切换时,都是一次全新的请求、响应、渲染页面的过程。而从最终的渲染结果来看,主页和详情页的主题布局是完全一样的,那么是否能够做到在主页和详情页之间切换时,布局 DOM 树的复用呢?简单说就是实现页面局部刷新的能力;又或者在某个共同的位置如果存在某个输入框数据,在不做任何处理的情况下,页面切换也会导致输入数据(即页面状态)的丢失,那又该如何应对呢?

这些都是可以解决的,也是下一篇要探讨“在导航中保留状态(preserve state on navigation)”的内容,就会讲到。

本文就先说到这里,再见。

参考资料

[1]

《RSC From Scratch. Part 1: Server Components》: https://Github.com/reactwg/server-components/discussions/5

[2]

发明 JSX: https://juejin.cn/post/7299745570812821558

[3]

发明组件: https://juejin.cn/post/7299849645207158795

[4]

添加路由: https://juejin.cn/post/7301558181058658345

[5]

《从头学服务器组件#3:添加路由》: https://juejin.cn/post/7301558181058658345

[6]

线上 demo 地址: https://codesandbox.io/p/sandbox/relaxed-pare-gicsdi?file=%2Fserver.js%3A41%2C15-42%2C5


原文始发于微信公众号(写代码的宝哥):从头学服务器组件#4:发明异步组件

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/243718.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!