这是"从头学服务器组件"系列的第 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 个组件:BlogPostPage
和 Footer
(按照约定,组件首字母大写)。
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 页面的拆分,将博客详情页拆分成 BlogPostPage
和 Footer
两个组件,并通过修改 renderJSXToHTML()
函数实现,增加了对组件渲染的支持,并最终得到了跟之前一样的渲染效果。
现在,我们的博客应用只有详情页,比较单调,接下来我们再引入博客主页,或叫索引页(Index Page),来展示所有的博文列表。为此,我们需要增加路由功能。
到此为止,下一篇再说,再见!