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

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

相关推荐
Smile_Gently1 小时前
前端:最简单封装nmp插件(组件)过程。
前端·javascript·vue.js·elementui·vue
nihui1236 小时前
Uniapp 实现顶部标签页切换功能?
javascript·vue.js·uni-app
luckycoke8 小时前
小程序立体轮播
前端·css·小程序
一 乐8 小时前
高校体育场管理系统系统|体育场管理系统小程序设计与实现(源码+数据库+文档)
前端·javascript·数据库·spring boot·高校体育馆系统
懒羊羊我小弟8 小时前
常用Webpack Loader汇总介绍
前端·webpack·node.js
shengmeshi8 小时前
vue3项目img标签动态设置src,提示:ReferenceError: require is not defined
javascript·vue.js·ecmascript
BillKu8 小时前
vue3中<el-table-column>状态的显示
javascript·vue.js·elementui
祈澈菇凉8 小时前
ES6模块的异步加载是如何实现的?
前端·javascript·es6
我爱学习_zwj9 小时前
4.从零开始学会Vue--{{组件通信}}
前端·javascript·vue.js·笔记·前端框架
顾比魁9 小时前
XSS盲打:当攻击者“盲狙”管理员
前端·网络安全·xss