Skip to content

Instantly share code, notes, and snippets.

@tpmccallum
Last active May 16, 2022 00:05
Show Gist options
  • Save tpmccallum/83e30629b620685f5e2ca56e3a9c2808 to your computer and use it in GitHub Desktop.
Save tpmccallum/83e30629b620685f5e2ca56e3a9c2808 to your computer and use it in GitHub Desktop.
How to implements server-side rendering (SSR) using WebAssembly (Wasm)

System

First we update the system (Ubuntu 20)

sudo apt-get update
sudo apt install build-essential
sudo apt-get install npm
npm install svg-url-loader --save-dev
sudo apt install mlocate
npm i g webpack@4.44.2

Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

WASI target

rustup target add wasm32-wasi

WasmEdge CLI

curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash
source $HOME/.wasmedge/env

WasmEdge QuickJS

git clone https://github.com/second-state/wasmedge-quickjs.git
cd wasmedge-quickjs/
cargo build --target wasm32-wasi --release

React Blog Software - fetch

The following GitHub repository is the code used on a YouTube tutorial series called Full React Tutorial by The Net Ninja

cd ~
git clone https://github.com/iamshaunjp/Complete-React-Tutorial.git
git checkout lesson-32

React Blog Software - configure

cd ~/Complete-React-Tutorial/dojo-blog
vi src/index.js

Replace the text render on line 6 of the index.js file with the text hydrate. The result should look like the following

ReactDOM.hydrate(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

Add the following line of code to the src/App.js file

vi src/App.js
import React from 'react';

Create a new directory called server

mkdir server

Create a new file in that server directory called index.js

vi server/index.js

Populate the contents of the new index.js file with the following code.

import * as React from 'react';
import ReactDOMServer from 'react-dom/server';
import * as std from 'std';
import * as http from 'wasi_http';
import * as net from 'wasi_net';

import App from '../src/App.js';

async function handle_client(cs) {
  print('open:', cs.peer());
  let buffer = new http.Buffer();

  while (true) {
    try {
      let d = await cs.read();
      if (d == undefined || d.byteLength <= 0) {
        return;
      }
      buffer.append(d);
      let req = buffer.parseRequest();
      if (req instanceof http.WasiRequest) {
        handle_req(cs, req);
        break;
      }
    } catch (e) {
      print(e);
    }
  }
  print('end:', cs.peer());
}

function enlargeArray(oldArr, newLength) {
  let newArr = new Uint8Array(newLength);
  oldArr && newArr.set(oldArr, 0);
  return newArr;
}

async function handle_req(s, req) {
  print('uri:', req.uri)

  let resp = new http.WasiResponse();
  let content = '';
  if (req.uri == '/') {
    const app = ReactDOMServer.renderToString(<App />);
    content = std.loadFile('./build/index.html');
    content = content.replace('<div id="root"></div>', `<div id="root">${app}</div>`);
  } else {
    let chunk = 1000; // Chunk size of each reading
    let length = 0; // The whole length of the file
    let byteArray = null; // File content as Uint8Array
    
    // Read file into byteArray by chunk
    let file = std.open('./build' + req.uri, 'r');
    while (true) {
      byteArray = enlargeArray(byteArray, length + chunk);
      let readLen = file.read(byteArray.buffer, length, chunk);
      length += readLen;
      if (readLen < chunk) {
        break;
      }
    }
    content = byteArray.slice(0, length).buffer;
    file.close();
  }
  let contentType = 'text/html; charset=utf-8';
  if (req.uri.endsWith('.css')) {
    contentType = 'text/css; charset=utf-8';
  } else if (req.uri.endsWith('.js')) {
    contentType = 'text/javascript; charset=utf-8';
  } else if (req.uri.endsWith('.json')) {
    contentType = 'text/json; charset=utf-8';
  } else if (req.uri.endsWith('.ico')) {
    contentType = 'image/vnd.microsoft.icon';
  } else if (req.uri.endsWith('.png')) {
    contentType = 'image/png';
  }
  resp.headers = {
    'Content-Type': contentType
  };

  let r = resp.encode(content);
  s.write(r);
}

async function server_start() {
  print('listen 8002...');
  try {
    let s = new net.WasiTcpServer(8002);
    for (var i = 0; ; i++) {
      let cs = await s.accept();
      handle_client(cs);
    }
  } catch (e) {
    print(e);
  }
}

server_start();

Create a new file called .babelrc.json

vi .babelrc.json
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ]
}

Create a new file called webpack.server.js and populate it with the following code

vi webpack.server.js
const path = require('path');
module.exports = {
  entry: './server/index.js',
  externals: [
    {"wasi_http": "wasi_http"},
    {"wasi_net": "wasi_net"},
    {"std": "std"}
  ],
  output: {
    path: path.resolve('server-build'),
    filename: 'index.js',
    chunkFormat: "module",
    library: {
      type: "module"
    },
  },
  experiments: {
    outputModule: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ["css-loader"]
      },
      {
        test: /\.svg$/,
        use: ["svg-url-loader"]
      }
    ]
  }
};

Find out where the wasmedge_quickjs.wasm file is located on your system.

sudo updatedb
locate wasmedge_quickjs.wasm

The above command will return a file path (something similar to what is shown below); copy the text which your terminal presented to you because you will be using it in the next task

/home/ubuntu/wasmedge-quickjs/target/wasm32-wasi/release/wasmedge_quickjs.wasm

Open the package.json file and add the following two lines to the scripts section (ensure that you use the proper location for the wasmedge_quickjs.wasm file)

"dev:build-server": "NODE_ENV=development webpack --config webpack.server.js --mode=development",
"dev:start-server": "wasmedge --dir .:. /home/ubuntu/wasmedge-quickjs/target/wasm32-wasi/release/wasmedge_quickjs.wasm ./server-build/index.js"

Build and start the blog software

npm install
npm run build
npx browserslist@latest --update-db
npm run dev:build-server

If you receive a message like the one below, please type yes and press enter

Do you want to install 'webpack-cli' (yes/no):

Run the server

npm run dev:start-server

The above will result in the following output

npm run dev:start-server

> dojo-blog@0.1.0 dev:start-server /home/ubuntu/Complete-React-Tutorial/dojo-blog
> wasmedge --dir .:. /home/ubuntu/wasmedge-quickjs/target/wasm32-wasi/release/wasmedge_quickjs.wasm ./server-build/index.js

listen 8002...
@DarumaDocker
Copy link

I encountered this error: Invariant Violation: Browser history needs a DOM and found this solution:
https://stackoverflow.com/questions/43058684/react-router-4-browser-history-needs-a-dom

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment