从头学服务器组件#2:发明组件

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

回顾

在上一篇文章《从头学服务器组件#1:发明 JSX》中,我们为实现的博客站点引入了 JSX 这一个 JS 的语法扩展,并编写了将 JSX 树结构转换成 HTML 字符串的渲染函数 renderJSXToHTML()

在这之后,我们就要考虑 UI 界面的拆分了。我们可以把一张页面看成是由多个模块组成,每个模块各自独立,又能自由组合。在实际开发中,我们使会用组件(Components) 这一结构来实现这些模块。

组件

不管你的代码是运行在客户端还是服务端,对页面 UI 进行拆分都是必要的。我们采用函数组件的方式来表示这一个个页面模块------给它们起名,并通过函数参数方式,将props 信息传递给组件。

先看下之前的实现。

javascript 复制代码
createServer(async (req, res) => {
  const author = "Jae Doe";
  const postContent = await readFile("./posts/hello-world.txt", "utf8");
  sendHTML(
    res,
    <html>
      <head>
        <title>My blog</title>
      </head>
      <body>
        <nav>
          <a href="/">Home</a>
          <hr />
        </nav>
        <article>
          {postContent}
        </article>
        <footer>
          <hr />
          <p><i>(c) {author}, {new Date().getFullYear()}</i></p>
        </footer>
      </body>
    </html>
  );
}).listen(8080);

拆分组件

那么按照我们最新的构想,我们将博客详情页的内容拆分成 2 个组件:BlogPostPageFooter(按照约定,组件首字母大写)。

javascript 复制代码
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>
  );
}

function Footer({ author }) {
  return (
    <footer>
      <hr />
      <p>
        <i>
          (c) {author} {new Date().getFullYear()}
        </i>
      </p>
    </footer>
  );
}

再用 <BlogPostPage postContent={postContent} author={author} /> 替换原来位置上的代码。

javascript 复制代码
createServer(async (req, res) => {
  const author = "Jae Doe";
  const postContent = await readFile("./posts/hello-world.txt", "utf8");
  sendHTML(
    res,
    <BlogPostPage
      postContent={postContent}
      author={author}
    />
  );
}).listen(8080);

现在,如果不对 renderJSXToHTML() 函数不做任何修改,运行程序,发现最终生成的 HTML 字符串是有问题的。

html 复制代码
<!-- 并非是有效的 HTML... -->
<function BlogPostPage({postContent,author}) {...}>
</function BlogPostPage({postContent,author}) {...}>

问题出在之前实现的 renderJSXToHTML() 函数------只考虑了 jsx.type 是字符串的情况(例如 "html""footer""p")。

javascript 复制代码
if (jsx.$$typeof === Symbol.for("react.element")) {
  // Existing code that handles HTML tags (like <p>).
  let html = "<" + jsx.type;
  // ...
  html += "</" + jsx.type + ">";
  return html;
} 

但是在这里,我们的组件标记在编译后,jsx.type 的值是组件函数本身,也就是 BlogPostPage,所以执行 "<" + jsx.type + ">" 会直接把函数源代码打印出来了。因此,我们还需要扩展 renderJSXToHTML()功能,增加当 jsx.type 的值为函数时的处理逻辑。

增加组件渲染支持

这块实现相对来说不是很复杂:判断 jsx.type 是函数后,表示正在处理函数组件,调用函数组件,并将 jsx.props 作为参数传入,得到返回的 JSX 结构,再带入 renderJSXToHTML() 函数,转换成最终的 HTML 字符串就行了。

下面是代码实现。

javascript 复制代码
if (jsx.$$typeof === Symbol.for("react.element")) {
  if (typeof jsx.type === "string") { // 是像 <div> 这样的 HTML 标记吗?
    // 处理 HTML 标记 (比如:<p>).
    let html = "<" + jsx.type;
    // ...
    html += "</" + jsx.type + ">";
    return html;
  } else if (typeof jsx.type === "function") { // 是像 <BlogPostPage> 这样的组件吗?
    // 使用 props 作为参数调用组件函数,得到返回的 JSX 结构,并转换成 HTML 字符串
    const Component = jsx.type;
    const props = jsx.props;
    const returnedJsx = Component(props);
    return renderJSXToHTML(returnedJsx); 
  } else throw new Error("Not implemented.");
}

添加完这块判断后,renderJSXToHTML() 函数在碰到 <BlogPostPage author="Jae Doe" /> 这类 JSX 元素后,调用 BlogPostPage() 函数,并将 { author: "Jae Doe" } 参数传递给它,会得到由所有 HTML 标记构成的 JSX 结构,然后将这个结构再一次传递回 renderJSXToHTML(),就得到最终的 HTML 字符串了。

仅仅需要这些操作,我们就完成了对组件渲染的支持。

打开这里的线上 demo查看,发现渲染结果跟之前的一样,说明成功了。

总结

本节我们实现了 UI 页面的拆分,将博客详情页拆分成 BlogPostPageFooter 两个组件,并通过修改 renderJSXToHTML() 函数实现,增加了对组件渲染的支持,并最终得到了跟之前一样的渲染效果。

现在,我们的博客应用只有详情页,比较单调,接下来我们再引入博客主页,或叫索引页(Index Page),来展示所有的博文列表。为此,我们需要增加路由功能。

到此为止,下一篇再说,再见!

相关推荐
折果44 分钟前
如何在vue项目中封装自己的全局message组件?一步教会你!
前端·面试
不死鸟.亚历山大.狼崽子1 小时前
Syntax Error: Error: PostCSS received undefined instead of CSS string
前端·css·postcss
汪子熙1 小时前
Vite 极速时代的构建范式
前端·javascript
叶常落1 小时前
[react] js容易混淆的两种导出方式2025-08-22
javascript
跟橙姐学代码1 小时前
一文读懂 Python 的 JSON 模块:从零到高手的进阶之路
前端·python
前端小巷子1 小时前
Vue3的渲染秘密:从同步批处理到异步微任务
前端·vue.js·面试
nightunderblackcat2 小时前
新手向:用FastAPI快速构建高性能Web服务
前端·fastapi
小码编匠2 小时前
物联网数据大屏开发效率翻倍:Vue + DataV + ECharts 的标准化模板库
前端·vue.js·echarts
欧阳天风3 小时前
分段渲染加载页面
前端·fcp
艾小码3 小时前
TypeScript在前端的实践:类型系统助力大型应用开发
前端·typescript