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. };
  14. export default Home;

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

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

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

  1. import React from 'react';
  2. import {renderToString} from 'react-dom/server';
  3. import Home from './component/Home.jsx';
  4. const content = renderToString(React.createElement(Home));
  5. 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';
  2. async function sleep(ms) {
  3. return new Promise((r, _) => {
  4. setTimeout(() => r(), ms)
  5. });
  6. }
  7. async function loadLazyPage() {
  8. await sleep(2000);
  9. return await import('./LazyPage.jsx');
  10. }
  11. class LazyHome extends React.Component {
  12. render() {
  13. let LazyPage1 = React.lazy(() => loadLazyPage());
  14. return (
  15. <html lang="en">
  16. <head>
  17. <meta charSet="utf-8" />
  18. <title>Title</title>
  19. </head>
  20. <body>
  21. <div>
  22. <div> This is LazyHome </div>
  23. <Suspense fallback={<div> loading... </div>}>
  24. <LazyPage1 />
  25. </Suspense>
  26. </div>
  27. </body>
  28. </html>
  29. );
  30. }
  31. }
  32. export default LazyHome;

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

  1. import React from 'react';
  2. class LazyPage extends React.Component {
  3. render() {
  4. return (
  5. <div>
  6. <div>
  7. This is lazy page
  8. </div>
  9. </div>
  10. );
  11. }
  12. }
  13. 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';
  5. import LazyHome from './component/LazyHome.jsx';
  6. async function handle_client(s) {
  7. let resp = new http.WasiResponse();
  8. resp.headers = {
  9. "Content-Type": "text/html; charset=utf-8"
  10. }
  11. renderToPipeableStream(<LazyHome />).pipe(resp.chunk(s));
  12. }
  13. async function server_start() {
  14. print('listen 8001...');
  15. let s = new net.WasiTcpServer(8001);
  16. for (var i = 0; i < 100; i++) {
  17. let cs = await s.accept();
  18. handle_client(cs);
  19. }
  20. }
  21. 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
  3. 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
  4. 100 211 0 211 0 0 1029 0 --:--:-- --:--:-- --:--:-- 1024
  5. 100 275 0 275 0 0 221 0 --:--:-- 0:00:01 --:--:-- 220
  6. 100 547 0 547 0 0 245 0 --:--:-- 0:00:02 --:--:-- 245
  7. 100 1020 0 1020 0 0 413 0 --:--:-- 0:00:02 --:--:-- 413
  8. <!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 的知识。