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

背景

2023 年 6 月,当时还是 Meta 公司 React 团队成员的 Dan Abramov 在 Github reactwg/server-components repo[1] 的讨论区发布了一篇深入介绍 React 服务器组件的文章《RSC From Scratch. Part 1: Server Components》[2]

“从头学 React 服务器组件(RSC From Scratch)”是一个系列,就像你看到的,这个系列第 1 篇的主题是介绍服务器组件(Server Component)。由于篇幅实在太长,内容量又极其丰富,因此我计划将这篇文章拆分成 6 个短篇来翻译介绍,主要采用“改写”的方式进行翻译,以期达到比较好的传达效果。

有能力的同学,推荐直接阅读原文进行学习。地址位于这里:https://github.com/reactwg/server-components/discussions/5。

当然,学习之前需要大家有一定的 React 使用经验,而且这个系列并不着眼于介绍如何使用服务器组件,而是通过实现一个低配版本的服务器组件来讲解其原理,理解引入它的背景和目的。

回到过去

如果将时间的齿轮拨回到 2003 年,那时的 Web 开发还处在一个比较早期的阶段,设备和工具都还很简陋。那个时候,用 PHP 来开发一个博客网站,是一种时尚,也是一种潮流。

PHP 版本

在网上查找一些资料后,了解了 PHP 的一些基本语法,写出了类似下面这样一个网页。

<?php
  $author = "Jae Doe";
  $post_content = file_get_contents("./posts/hello-world.txt");
?>
<html>
  <head>
    <title>My blog</title>
  </head>
  <body>
    <nav>
      <a href="/">Home</a>
      <hr>
    </nav>
    <article>
      <?php echo htmlspecialchars($post_content); ?>
    </article>
    <footer>
      <hr>
      <p><i>(c) <?php echo htmlspecialchars($author); ?><?php echo date("Y"); ?></i></p>
    </footer>
  </body>
</html>

注意:为了方便阅读、理解,我们使用了在 HTML 5 中定义的一些语意化标签(<nav><article><footer>),我们假装它们当时已经存在就行了。

不熟悉 PHP 的同学,也没有关系,我做个简单的说明。

  • 这个 .php 文件会被上传到服务器,使用浏览器访问时,这个文件会首先在服务中进行编译,具体说就是将 PHP 文件转成纯 HTML,然后发送回浏览器进行解析展示
  • <?php //... > 中的内容,就是会在服务器执行的 PHP 代码
  • 我们定义了两个变量 $author$post_content,表示博客作者,以及博文内容。其中 $post_content 的内容是从服务器本地的 hello-world.txt 文件中读取的(使用 PHP 内置函数 file_get_contents()
  • 我们还使用了内置函数 htmlspecialchars()$author$post_content 的内容中的特殊字符,转换成 HTML 实体,确保文本里的内容不会意外当做 HTML;另外,还使用了 date() 函数获取当前年份
  • echo xxx 用来将后面的返回值替换当前代码,并最终打印到生成的 HTML 页面上

我们假设 hello-world.txt 的内容如下。

Hi everyone! This is my first blog post. I <3 React.

最终,这个 .php 文件返回到浏览器的内容是这样的。

<html>
  <head>
    <title>My blog</title>
  </head>
  <body>
    <nav>
      <a href="/">Home</a>
      <hr>
    </nav>
    <article>
      Hi everyone! This is my first blog post. I <3 React.
    </article>

    <footer>
      <hr>
      <p><i>(c) Jae Doe, 2023</i></p>
    </footer>
  </body>
</html>

浏览器访问 http://locahost:8080/hello-world,看到的效果。

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

Node.js 版本

当然,如果是用 Node.js 来编写这个应用的话,是下面这样。

import { createServer } from 'http';
import { readFile } from 'fs/promises';
import escapeHtml from  'escape-html';

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>
          ${escapeHtml(postContent)}
        </article>
        <footer>
          <hr>
          <p><i>(c) ${escapeHtml(author)}${new Date().getFullYear()}</i></p>
        </footer>
      </body>
    </html>`

  );
}).listen(8080);

function sendHTML(res, html{
  res.setHeader("Content-Type""text/html");
  res.end(html);
}

注意:假设当时的 CD-ROM(“光盘只读存储器”) 上能顺利跑我们的 Node.js 引擎。

相比较于 PHP 来说,Node.js 的实现会更加简单。启动一个服务器后,直接以模板字符串的形式组织网页,并将内容以 HTML 格式发送回浏览器。效果与 PHP 实现一样。

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

(这里有一个_线上 demo_[3],点击查看)

这个时候,从未来过来的你,如果要把 React 编码范式带过来,你会以什么样的顺序添加功能?

发明 JSX!

回顾一下 Node.js 版本的代码实现,一个不太理想的地方是在操作 HTML 的时候。 我们使用纯字符串的形式合成最终的 HTML 代码,还要调用 escapeHtml(postContent) 确保文本里的内容不会意外当做 HTML。

这个时候我们会想到引入一种模板语言,将计算逻辑跟模板分离,提供一种方法来为文本和属性注入动态值,同时又能在模板内安全地转义文本内容,还支持使用某种条件判断和循环的语法。这也是 2003 年一些最流行的以服务器为中心的框架所采用的方法。

当然,有了 React 使用经验的我们,会想到这样做。

引入 JSX

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);

是不是有点 React 的味道了?我们的“模板”不再是字符串了。我们在 JavaScript 中引入了一个类似 XML 的语法子集(也就是 JSX),避免在使用字符串插值时,可能会出现的标记不匹配(如:没有写闭合标签)或忘记转义文本内容的问题。

在底层,JSX 会生成一个由对象组成的树结构,看起来像这样。

// Slightly simplified
{
  $$typeofSymbol.for("react.element"), // Tells React it's a JSX element (e.g. <html>)
  type'html',
  props: {
    children: [
      {
        $$typeofSymbol.for("react.element"),
        type'head',
        props: {
          children: {
            $$typeofSymbol.for("react.element"),
            type'title',
            props: { children'My blog' }
          }
        }
      },
      {
        $$typeofSymbol.for("react.element"),
        type'body',
        props: {
          children: [
            {
              $$typeofSymbol.for("react.element"),
              type'nav',
              props: {
                children: [{
                  $$typeofSymbol.for("react.element"),
                  type'a',
                  props: { href'/'children'Home' }
                }, {
                  $$typeofSymbol.for("react.element"),
                  type'hr',
                  propsnull
                }]
              }
            },
            {
              $$typeofSymbol.for("react.element"),
              type'article',
              props: {
                children: postContent
              }
            },
            {
              $$typeofSymbol.for("react.element"),
              type'footer',
              props: {
                /* ...And so on... */
              }              
            }
          ]
        }
      }
    ]
  }
}

JSX 节点类型说明

这个树结构种每一个节点类似这样:

{
    $$typeofSymbol.for("react.element"),
    type'head',
    props: {
     children: {}
   }
}

说明如下:

  • $$typeof 属性值固定为 Symbol.for("react.element"),表示这个元素结构是符合 React 节点规范的
  • type 表示标记名称,目前仅支持字符串类型,比如 "head""body""nav""a" 等等
  • props 则是用来表示标记上的 attribute,比如 idclassName 之类的,没有即为 null
  • props.children 是一个特殊属性,表示当前元素的所有子元素,可以是:
    • 一个字符串、数字(表示仅包含一个文本)
    • undefind/null/布尔(表示没有子元素,当前已经是叶子元素了)
    • 一个数组(表示包含多个子元素)
    • 一个对象(表示仅包含一个子元素)

虽然我们现在能成功使用这种树结构表示 JSX 了,但我们需要发送到浏览器的是 HTML,而不是 JSON 树(当然,不一定。不过,至少目前是这样)。

编写 JSX 渲染函数

由此,我们就需要编写一个函数,将 JSX 所代表的底层对象转换成 HTML 字符串。为此,我们需要对不同类型的节点(字符串、数字、数组或带子节点(有 .children 属性)的 JSX 节点)做判断分别处理,并最终转换为 HTML 片段。

我们将渲染函数定义成 renderJSXToHTML(jsx),来看下它的实现。

function renderJSXToHTML(jsx{
  if (typeof jsx === "string" || typeof jsx === "number") {
    // 是一个文本节点。转义处理之后可以直接放到 HTML 字符串里
    return escapeHtml(jsx);
  } else if (jsx == null || typeof jsx === "boolean") {
    // 表示一个空节点,返回一个空字符串就行
    return "";
  } else if (Array.isArray(jsx)) {
    // 处理多个子元素。递归调用 renderJSXToHTML() 函数,每个元素单独处理返回 HTML 字符串片段,并最终拼接到一起
    return jsx.map((child) => renderJSXToHTML(child)).join("");
  } else if (typeof jsx === "object") {
    // 渲染 React JSX 元素 (e.g. <div />).
    if (jsx.$$typeof === Symbol.for("react.element")) {
      // 拼接 HTML tag.
      let html = "<" + jsx.type;
      // 处理除 .children 外的所有其他 prop,映射成对应标记上的 attribute
      for (const propName in jsx.props) {
        if (jsx.props.hasOwnProperty(propName) && propName !== "children") {
          html += " ";
          html += propName;
          html += "=";
          html += escapeHtml(jsx.props[propName]); // 对 prop 值进行到 HTML 实体的转义
        }
      }
      html += ">";
      // 处理 .children prop
      html += renderJSXToHTML(jsx.props.children);
      html += "</" + jsx.type + ">";
      return html;
    } else throw new Error("Cannot render an object.");
  } else throw new Error("Not implemented.");
}

实现原理就是针对 JSX  节点所有可能的类型分别进行判断处理,最终返回一个表示 HTML 字符串的过程。

这里提供了一个线上 demo[4],打开看看,我们的 JSX 是如何被渲染 HTML 字符串提供出去的!

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

将 JSX 转换为 HTML 字符串通常称为“服务器端渲染(Server-Side Rendering,简称SSR)”。需要注意的是,RSC 和 SSR 是两个非常不同的东西(不过通常又会一起使用)。在本系列的文章中,我们从 SSR 开始,因为这是我们在服务器环境中,首先会想到并会做的一个尝试。这只是我们诸多过程里第一步,后面会看到实现上的明显差别。

总结

本文,我们首先回顾了 2000 年早期,使用 PHP 编写博客网页的例子。接着写出了同功能的 Node.js 版本。然后,依靠我们携带的 React 先进思想,引入了 JSX——这是一种比传统模板工具具有更好表现力的 JS 扩展,写起来也更便捷——让我们可以在 JS 种直接编写 HTML 标记。不过,JSX 底层的对象树结构表示并不能给浏览器直接使用,所以我们又引入了将 JSX 结构转换为 HTML 字符串的、一个简单版本的渲染函数 renderJSXToHTML()

有了 JSX,下一节我们将介绍组件,这是一种拆分 UI 界面的策略,能极大提升网页可维护性,实现页面功能的可插拔。当然,这就需要我们对现有的 renderJSXToHTML() 进行扩展来支持。

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

参考资料

[1]

reactwg/server-components repo: https://github.com/reactwg/server-components

[2]

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

[3]

线上 demo: https://codesandbox.io/p/sandbox/nostalgic-platform-kvog0r?file=%2Fserver.js

[4]

线上 demo: https://codesandbox.io/p/sandbox/recursing-kepler-yw7dlx?file=%2Fserver.js


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

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

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

(0)
小半的头像小半

相关推荐

发表回复

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