React SSR

React 服务端渲染(SSR) 是 JavaScript 在 BFF(为前端提供服务的后端)函数中一种常见的使用场景。 和在浏览器中渲染 HTML DOM 元素的方式不同,这种技术使用 React 框架在服务端就生成了 HTML 元素,以此来加快应用的加载速度。 在 Jamstack 应用中,这是使用 serverless 函数时一种较理想的方式。

在这篇文章中,我们将向你展示如何使用 WasmEdge 的 QuickJS 运行时来实现 React SSR 的能力。与 Docker + Linux + nodejs + v8 的方案相比,WasmEdge 要轻量得多(仅 1% 的占用),也更安全,能够提供更好的资源隔离和管理机制,并有着和非 JIT(同时是安全的)近似的性能。

本文将包含对静态渲染和流式渲染两种渲染方式的介绍。静态渲染相对容易理解和实现。而流式渲染则可以提供更好的用户体验,因为用户在浏览器前等待结果时,可以优先看到生成的部分内容。

静态渲染

本示例的源代码可以在 GitHub 仓库的 example_js/react_ssr 文件夹中找到。它展示了运行于 WasmEdge 的 JavaScript 应用程序,是如何编排 HTML 模板并将其渲染成 HTML 字符串的。

文件 component/Home.jsx 里是 React 的主页模板。

1. import React from 'react';
2. import Page from './Page.jsx';
3. class Home extends React.Component {
4. render() {
5. const { dataList = [] } = this.props;
6. return (
7. <div>
8. <div>This is home</div>
9. <Page></Page>
10. </div>
11. );
12. }
13. };

15. export default Home;

Home.jsx 中会包含 Page.jsx 提供的模板,作为页面的一部分。

1. import React from 'react';

3. class Page extends React.Component {
4. render() {
5. const { dataList = [] } = this.props;
6. return (
7. <div>
8. <div>This is page</div>
9. </div>
10. );
11. }
12. };

14. export default Page;

文件 main.js 会调用 React 将模板渲染成 HTML。

1. import React from 'react';
2. import {renderToString} from 'react-dom/server';

4. import Home from './component/Home.jsx';

6. const content = renderToString(React.createElement(Home));
7. console.log(content);

目录中的 rollup.config.js 文件和 package.json 文件用于把 React SSR 的所有依赖和组件打包成一个 WasmEdge 可用的 JavaScript 文件。你可以使用 npm 命令来进行构建,构建产物会输出到 dist/main.js 文件里。

1. npm install
2. npm run build

要运行这个例子,请在命令行中执行以下命令。你会看到,所有的模板成功合成了一个 HTML 字符串。

1. $ cd example_js/react_ssr
2. $ wasmedge --dir .:. ../../target/wasm32-wasi/release/wasmedge_quickjs.wasm dist/main.js
3. <div data-reactroot=""><div>This is home</div><div><div>This is page</div></div></div>

注意: 命令行里的 --dir .:. 会给 WasmEdge 读取本地文件系统下目录的权限,以此读取 dist/main.js 文件。

流式渲染

本示例的源代码可以在 GitHub 仓库的 example_js/react_ssr_stream 文件夹中找到。它展示了运行于 WasmEdge 的 JavaScript 应用程序,是如何流式地把 HTML 模板渲染成 HTML 字符串的。

文件 component/LazyHome.jsx 是 React 的主页模板。当外层的 HTML 渲染好并返回给用户 2s 之后,它才会开始“懒”加载内层的页面模板。

1. import React, { Suspense } from 'react';

3. async function sleep(ms) {
4. return new Promise((r, _) => {
5. setTimeout(() => r(), ms)
6. });
7. }

9. async function loadLazyPage() {
10. await sleep(2000);
11. return await import('./LazyPage.jsx');
12. }

14. class LazyHome extends React.Component {
15. render() {
16. let LazyPage1 = React.lazy(() => loadLazyPage());
17. return (
18. <html lang="en">
19. <head>
20. <meta charSet="utf-8" />
21. <title>Title</title>
22. </head>
23. <body>
24. <div>
25. <div> This is LazyHome </div>
26. <Suspense fallback={<div> loading... </div>}>
27. <LazyPage1 />
28. </Suspense>
29. </div>
30. </body>
31. </html>
32. );
33. }
34. }

36. export default LazyHome;

LazyPage.jsx 里就是内层的模板。只有在外层页面返回给用户 2s 之后,它才会被渲染。

1. import React from 'react';

3. class LazyPage extends React.Component {
4. render() {
5. return (
6. <div>
7. <div>
8. This is lazy page
9. </div>
10. </div>
11. );
12. }
13. }

15. export default LazyPage;

main.mjs 文件会启动一个异步的 HTTP 服务器,然后把 HTML 页面渲染成多段放入响应。当一个 HTTP 请求进来的时候,handle_client() 函数就会被调用来渲染 HTML,并以流的形式返回结果。

1. import * as React from 'react';
2. import { renderToPipeableStream } from 'react-dom/server';
3. import * as http from 'wasi_http';
4. import * as net from 'wasi_net';

6. import LazyHome from './component/LazyHome.jsx';

8. async function handle_client(s) {
9. let resp = new http.WasiResponse();
10. resp.headers = {
11. "Content-Type": "text/html; charset=utf-8"
12. }
13. renderToPipeableStream(<LazyHome />).pipe(resp.chunk(s));
14. }

16. async function server_start() {
17. print('listen 8001...');
18. let s = new net.WasiTcpServer(8001);
19. for (var i = 0; i < 100; i++) {
20. let cs = await s.accept();
21. handle_client(cs);
22. }
23. }

25. server_start();

目录中的 rollup.config.js 文件和 package.json 文件用于把 React SSR 的所有依赖和组件打包成一个 WasmEdge 可用的 JavaScript 文件。你可以使用 npm 命令来进行构建,构建产物会输出到 dist/main.js 文件里。

1. npm install
2. npm run build

要运行这个例子,请在命令行上执行以下命令来启动服务器。

1. cd example_js/react_ssr_stream
2. nohup wasmedge --dir .:. ../../target/wasm32-wasi/release/wasmedge_quickjs.wasm dist/main.mjs &

然后通过 curl 或浏览器发送一个 HTTP 请求。

1. curl http://localhost:8001

结果如下所示。该服务首先返回一个 HTML 页面,里面包含一个空的内层部分(即 loading 部分)。然后在 2s 后返回内层部分的 HTML 内容,以及将它显示出来的 JavaScript 代码。

1. % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
2. Dload  Upload   Total   Spent    Left  Speed

4. 0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
5. 100   211    0   211    0     0   1029      0 --:--:-- --:--:-- --:--:--  1024
6. 100   275    0   275    0     0    221      0 --:--:--  0:00:01 --:--:--   220
7. 100   547    0   547    0     0    245      0 --:--:--  0:00:02 --:--:--   245
8. 100  1020    0  1020    0     0    413      0 --:--:--  0:00:02 --:--:--   413

10. <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><title>Title</title></head><body><div><div> This is LazyHome </div><!--$?--><template id="B:0"></template><div> loading... </div><!--/$--></div></body></html><div hidden id="S:0"><template id="P:1"></template></div><div hidden id="S:1"><div><div>This is lazy page</div></div></div><script>function $RS(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)};$RS("S:1","P:1")</script><script>function $RC(a,b){a=document.getElementById(a);b=document.getElementById(b);b.parentNode.removeChild(b);if(a){a=a.previousSibling;var f=a.parentNode,c=a.nextSibling,e=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d)if(0===e)break;else e--;else"$"!==d&&"$?"!==d&&"$!"!==d||e++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;b.firstChild;)f.insertBefore(b.firstChild,c);a.data="$";a._reactRetry&&a._reactRetry()}};$RC("B:0","S:0")</script>

流式 SSR 的示例充分利用了 WasmEdge 独特的处理异步网络的能力以及对 ES6 module 的支持(rollup 打包的 JS 文件中包含了 ES6 的模块)。你可以在本书中阅读更多有关 异步网络 和 ES6 的知识。