从头学服务器组件#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),来展示所有的博文列表。为此,我们需要增加路由功能。

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

相关推荐
DT——1 小时前
Vite项目中eslint的简单配置
前端·javascript·代码规范
学习ing小白3 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
真的很上进4 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er4 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063714 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl4 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码4 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347544 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
喝旺仔la4 小时前
VSCode的使用
java·开发语言·javascript
ch_s_t4 小时前
新峰商城之分类三级联动实现
前端·html