Cara menggunakan react-native-render-html renderers

React adalah framework front-end populer yang digunakan untuk membuat single page application (SPA). Framework ini di-render dan dijalankan di sisi klien di browser. Namun, untuk alasan SEO atau kinerja, Anda mungkin perlu melakukan rendering beberapa bagian dari aplikasi React di sisi server. Di sinilah server-side rendering (SSR) berguna.

Tulisan ini memperkenalkan konsep dan mendemonstrasikan cara rendering aplikasi React dengan AWS Lambda. Untuk menerapkan solusi ini dan untuk menyediakan resource di AWS, saya menggunakan AWS Cloud Development Kit (CDK). AWS CDK adalah open source framework, yang membantu Anda mengurangi jumlah kode yang diperlukan untuk membuat otomatisasi deployment.

Gambaran Umum

Solusi ini menggunakan Amazon S3, Amazon CloudFront, Amazon API Gateway, AWS Lambda, dan Lambda @ Edge. Kombinasi ini menciptakan implementasi SSR yang sepenuhnya serverless, dan secara otomatis dapan scale sesuai dengan beban kerja. Solusi ini membahas tiga skenario.

Skenario 1 Aplikasi React statis yang di-hosting dalam bucket S3 dengan distribusi CloudFront di depan situs web. Backend dari aplikasi ini berjalan di belakang API Gateway dan diimplementasikan sebagai fungsi Lambda. Di sini, aplikasi sepenuhnya diunduh ke klien dan ditampilkan di browser web. Aplikasi ini kemudian mengirimkan permintaan ke backend.

Cara menggunakan react-native-render-html renderers

Skenario 2 Aplikasi React di-render dengan fungsi Lambda. Distribusi CloudFront dikonfigurasi untuk meneruskan permintaan dari path

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
4 ke endpoint API Gateway. Aplikasi ini memanggil fungsi Lambda tempat proses rendering terjadi. Saat me-render halaman yang diminta, fungsi Lambda memanggil API backend untuk mengambil data. Ini mengembalikan halaman HTML statis dengan semua data. Halaman ini dapat di-cache di CloudFront untuk mengoptimalkan permintaan selanjutnya.

Cara menggunakan react-native-render-html renderers

Skenario 3 Aplikasi React di-render dengan fungsi Lambda@Edge. Skenario ini serupa dengan skenario sebelumnya tetapi rendering terjadi di lokasi edge. Permintaan ke path

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
5 ditangani oleh fungsi Lambda@Edge. Ini mengirimkan permintaan ke backend dan mengembalikan halaman HTML statis.

Cara menggunakan react-native-render-html renderers

Demonstrasi

Aplikasi contoh berikut menunjukkan bagaimana skenario sebelumnya diimplementasikan dengan AWS CDK. Solusi ini membutuhkan:

  • Akun AWS
  • Node.js 12.x
  • AWS CDK 1.72+
  • Kredensial AWS untuk AWS CLI

Solusi ini melakukan deployment fungsi Lambda@Edge sehingga harus disediakan di AWS Region AS Timur (Virginia U.).

Untuk memulai, unduh dan konfigurasikan sampel:

Langkah 1 Dari sebuah terminal, clone repositori GitHub berikut:

 git clone https://github.com/aws-samples/react-ssr-lambda

Langkah 2 Berikan nama unik untuk S3 bucket, yang dibuat oleh stack dan digunakan untuk meng-hosting aplikasi React. Ubah placeholder

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
6 menjadi nama bucket Anda. Untuk menginstal solusi, jalankan:

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>

Langkah 3 Catat nilai-nilai berikut dari output:

  • SSRAppStack.CFURL – URL distribusi CloudFront. Akses ke root path
    cd react-ssr-lambda
    cd ./cdk
    npm install
    npm run build
    cdk bootstrap
    cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json
    
    cd ../simple-ssr
    npm install
    npm run build-all
    cd ../cdk
    cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
    7 akan mengembalikan aplikasi React yang disimpan di S3.
  • SSRAppStack.LambdaSSRURL – URL distribusi CloudFront
    cd react-ssr-lambda
    cd ./cdk
    npm install
    npm run build
    cdk bootstrap
    cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json
    
    cd ../simple-ssr
    npm install
    npm run build-all
    cd ../cdk
    cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
    4, yang mengembalikan halaman yang di-render oleh fungsi Lambda.
  • SSRAppStack.LambdaEdgeSSRURL – URL distribusi CloudFront
    cd react-ssr-lambda
    cd ./cdk
    npm install
    npm run build
    cdk bootstrap
    cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json
    
    cd ../simple-ssr
    npm install
    npm run build-all
    cd ../cdk
    cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
    5, yang mengembalikan halaman yang di-render oleh fungsi Lambda@Edge.

Cara menggunakan react-native-render-html renderers

Langkah 4 Di browser, buka setiap URL dari langkah 3. Anda melihat halaman yang sama dengan footer berbeda, yang menunjukkan bagaimana halaman tersebut ditampilkan.

Memahami aplikasi React contoh

Aplikasi dibuat oleh tool

import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
0. Anda dapat menjalankan dan menguji aplikasi ini secara lokal dengan menavigasi ke direktori
import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
1 dan menjalankan perintah
import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
2.

Aplikasi kecil ini terdiri dari dua komponen yang akan me-render daftar produk yang diterima dari backend. File

import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
3 mengirim permintaan, melakukan parsing hasilnya, dan meneruskannya ke komponen React.

import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;

Menambahkan server-side rendering

Untuk mendukung SSR, saya mengubah aplikasi sebelumnya menggunakan beberapa fungsi Lambda untuk implementasinya. Saat saya mengubah cara data diambil dari backend, saya menghapus kode ini dari

import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
3. Sebagai gantinya, data diambil di fungsi Lambda dan dimasukkan ke dalam aplikasi selama proses rendering.

File baru

import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
5 mencerminkan perubahan ini:

import React, { useState } from "react";
import ProductList from "./components/ProductList";

const SSRApp = ({ data }) => {
  const [result, setResult] = useState({ loading: false, products: data });
  return (
    <div>
      <ProductList result={result} />
    </div>
  );
};

export default SSRApp;

Selanjutnya, saya menerapkan logika SSR dalam fungsi Lambda tersebut. Untuk kesederhanaan, saya menggunakan metode

import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
6 bawaan React, yang mengembalikan
import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
7 HTML. Anda dapat menemukan file yang sesuai di
import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
8. Fungsi handler mengambil data dari backend, membuat komponen React, dan memasukkannya ke dalam template HTML. Ini mengembalikan respons ke API Gateway, yang merespons klien.

const handler = async function (event) {
  try {
    const url = config.SSRApiStack.apiurl;
    const result = await axios.get(url);
    const app = ReactDOMServer.renderToString(<SSRApp data={result.data} />);
    const html = indexFile.replace(
      '<div id="root"></div>',
      `<div id="root">${app}</div>`
    );
    return {
      statusCode: 200,
      headers: { "Content-Type": "text/html" },
      body: html,
    };
  } catch (error) {
    console.log(`Error ${error.message}`);
    return `Error ${error}`;
  }
};

Untuk me-render kode yang sama di Lambda@Edge, saya mengubah kode agar bisa memproses event CloudFront dan juga mengubah format respons. Fungsi ini mencari path khusus (

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
5). Semua logika lainnya tetap sama. Anda bisa melihat kode lengkapnya di simple-ssr/src/edge/index.js:

const handler = async function (event) {
  try {
    const request = event.Records[0].cf.request;
    if (request.uri === "/edgessr") {
      const url = config.SSRApiStack.apiurl;
      const result = await axios.get(url);
      const app = ReactDOMServer.renderToString(<SSRApp data={result.data} />);
      const html = indexFile.replace(
        '<div id="root"></div>',
        `<div id="root">${app}</div>`
      );
      return {
        status: "200",
        statusDescription: "OK",
        headers: {
          "cache-control": [
            {
              key: "Cache-Control",
              value: "max-age=100",
            },
          ],
          "content-type": [
            {
              key: "Content-Type",
              value: "text/html",
            },
          ],
        },
        body: html,
      };
    } else {
      return request;
    }
  } catch (error) {
    console.log(`Error ${error.message}`);
    return `Error ${error}`;
  }
};

Tool

import React, { useEffect, useState } from "react";
import ProductList from "./components/ProductList";
import config from "./config.json";
import axios from "axios";

const App = ({ isSSR, ssrData }) => {
  const [err, setErr] = useState(false);
  const [result, setResult] = useState({ loading: true, products: null });
  useEffect(() => {
    const getData = async () => {
      const url = config.SSRApiStack.apiurl;
      try {
        let result = await axios.get(url);
        setResult({ loading: false, products: result.data });
      } catch (error) {
        setErr(error);
      }
    };
    getData();
  }, []);
  if (err) {
    return <div>Error {err}</div>;
  } else {
    return (
      <div>
        <ProductList result={result} />
      </div>
    );
  }
};

export default App;
0 mengonfigurasi tool seperti Babel dan webpack untuk aplikasi React sisi klien. Namun, tool ini tidak dirancang untuk bekerja dengan SSR. Untuk membuat fungsi berfungsi seperti yang diharapkan, saya mentranspilasinya ke dalam format CommonJS sebagai tambahan untuk transpilasi file JSX React. Tool standar untuk tugas seperti ini adalah Babel. Untuk menambahkannya ke proyek ini, saya membuat file konfigurasi .babelrc.json dengan instruksi untuk mentranspilasi fungsi ke dalam format Node.js v12:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": 12
        }
      }
    ],
    "@babel/preset-react"
  ]
}

Saya juga menyertakan semua dependensi. Saya menggunakan tool frontend populer yakni webpack, yang juga dapat bekerja dengan fungsi Lambda. Tool ini hanya menambahkan dependensi yang diperlukan dan meminimalkan ukuran paket. Untuk tujuan ini, saya membuat konfigurasi untuk kedua fungsi tersebut. Anda dapat menemukannya di file webpack.edge.js dan webpack.server.js:

const path = require("path");

module.exports = {
  entry: "./src/edge/index.js",

  target: "node",

  externals: [],

  output: {
    path: path.resolve("edge-build"),
    filename: "index.js",
    library: "index",
    libraryTarget: "umd",
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: "babel-loader",
      },
      {
        test: /\.css$/,
        use: "css-loader",
      },
    ],
  },
};

Hasil dari menjalankan webpack adalah satu file untuk setiap build. Saya menggunakan file ini untuk mendeploy fungsi Lambda dan Lambda@Edge. Untuk mengotomatiskan proses build, saya menambahkan beberapa skrip ke

import React, { useState } from "react";
import ProductList from "./components/ProductList";

const SSRApp = ({ data }) => {
  const [result, setResult] = useState({ loading: false, products: data });
  return (
    <div>
      <ProductList result={result} />
    </div>
  );
};

export default SSRApp;
1.

"build-server": "webpack --config webpack.server.js --mode=development",
"build-edge": "webpack --config webpack.edge.js --mode=development",
"build-all": "npm-run-all --parallel build build-server build-edge"

Jalankan proses build dengan perintah

import React, { useState } from "react";
import ProductList from "./components/ProductList";

const SSRApp = ({ data }) => {
  const [result, setResult] = useState({ loading: false, products: data });
  return (
    <div>
      <ProductList result={result} />
    </div>
  );
};

export default SSRApp;
2.

Melakukan deployment aplikasi

Setelah proses build aplikasi berhasil, saya akan melakukan deployment ke AWS Cloud. Saya menggunakan AWS CDK sebagain pendekatan infrastructure as code. Kode terletak di cdk/lib/ssr-stack.ts.

Pertama, saya membuat bucket S3 untuk menyimpan konten statis dan saya meneruskan nama bucket sebagai parameter. Untuk memastikan hanya CloudFront yang dapat mengakses bucket S3 saya, saya menggunakan konfigurasi origin access identity:

const mySiteBucketName = new CfnParameter(this, "mySiteBucketName", {
      type: "String",
      description: "The name of S3 bucket to upload react application"
    });

const mySiteBucket = new s3.Bucket(this, "ssr-site", {
      bucketName: mySiteBucketName.valueAsString,
      websiteIndexDocument: "index.html",
      websiteErrorDocument: "error.html",
      publicReadAccess: false,
      //only for demo not to use in production
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });

new s3deploy.BucketDeployment(this, "Client-side React app", {
      sources: [s3deploy.Source.asset("../simple-ssr/build/")],
      destinationBucket: mySiteBucket
    });

const originAccessIdentity = new cloudfront.OriginAccessIdentity(
      this,
      "ssr-oia"
    );
    mySiteBucket.grantRead(originAccessIdentity);

Saya men-deploy fungsi Lambda dari direktori build dan mengonfigurasi integrasi dengan API Gateway. Saya juga mencatat nama domain API Gateway untuk digunakan nanti dalam distribusi CloudFront.

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
0

Saya mengkonfigurasi fungsi Lambda@Edge. Penting untuk membuat versi fungsi secara eksplisit untuk digunakan dengan CloudFront:

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
1

Terakhir, saya mengonfigurasi distribusi CloudFront untuk berkomunikasi dengan semua sumber:

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
2

Template sekarang siap untuk di-deploy. Pendekatan ini memungkinkan Anda menggunakan kode ini di pipeline Continuous Integration and Continuous Delivery / Deployment (CI / CD) untuk mengotomatiskan deployment aplikasi SSR Anda. Selain itu, Anda dapat membuat konstruksi CDK untuk menggunakan kembali kode ini dalam aplikasi yang berbeda.

Pembersihan

Untuk menghapus semua sumber daya yang digunakan dalam solusi ini, jalankan:

cd react-ssr-lambda
cd ./cdk
npm install
npm run build
cdk bootstrap
cdk deploy SSRApiStack --outputs-file ../simple-ssr/src/config.json

cd ../simple-ssr
npm install
npm run build-all
cd ../cdk
cdk deploy SSRAppStack --parameters mySiteBucketName=<your bucket name>
3

Kesimpulan

Tulisan ini mendemonstrasikan dua cara Anda dapat mengimplementasikan dan menerapkan solusi untuk rendering sisi server di aplikasi React, dengan menggunakan Lambda atau Lambda@Edge.

Saya juga menunjukkan cara menggunakan alat sumber terbuka dan AWS CDK untuk mengotomatiskan pembuatan dan penerapan aplikasi semacam itu.

Untuk lebih banyak sumber belajar untuk aplikasi serverless, kunjungi Serverless Land.


Tulisan ini diterjemahkan dari tulisan Building server-side rendering for React in AWS Lambda oleh Roman Boiko, Solutions Architect.