React กับการทำ Build Once, Deploy Anywhere

Piyawat Sakornsukkit
SET-IT-TEAM
Published in
3 min readSep 14, 2023
Photo by Lautaro Andreani on Unsplash

Build Once, Deploy Anywhere

ตามอุดมคติ ถ้าเราจะ build application docker image เราก็อยากจะ build ครั้งเดียวแล้วเอาไปใช้ได้กับทุกๆ environment ของเราทั้ง dev, uat, staging หรือ production

concept นี้เรียกว่า Build Once, Deploy Anywhere

ซึ่งจะช่วยให้เราแน่ใจว่า image ที่เรา build มาได้ผ่านการทดสอบมาแล้ว ก่อนที่จะขึ้น production

Challenge

ปกติแล้วถ้าอยากให้ build ครั้งเดียวแล้วเอาไปใช้ได้หลายที่ เราก็ควรจะแยกส่วนที่แตกต่างกันของแต่ละ environment ออกมาเป็น configuration

สำหรับ docker แล้ว ง่ายที่สุดก็คือ Environment Variable

สำหรับ React สามารถเรียกใช้ Environment Variable ได้จาก process.env.VAR1 และสั่ง yarn start ได้

แต่ถ้าเทียบกัน การ build code ให้เป็น static file แล้ว serve ด้วย webserver จะได้ performance ที่ดีกว่ามากๆ

ซึ่งการ build เป็น static file แล้วนำมารันที่ browser จะไม่สามารถอ่าน Environment Variable ที่อยู่ฝั่ง server ได้

ค้นหาวิธีซักพักก็เจอวิธีที่ถูกใจ https://blog.mikesir87.io/2021/07/build-once-deploy-everywhere-for-spas/

จึงจะมาลองทำดูซักหน่อย

เริ่มกันเลย

  1. เริ่มจากสร้าง react application ขึ้นมาตามปกติ
yarn create react-app test-react-build-once-deploy-anywhere --template typescript
cd test-react-build-once-deploy-anywhere

2. สร้าง file public/config.json

{
"VAR1": "hello world"
}

3. สร้างไฟล์ src/AppConfigContext.ts

import React from "react"

export interface AppConfig {
VAR1: string
}

export const AppConfigContext = React.createContext<AppConfig>({
VAR1: "default VAR1"
})

4. แก้ไข file src/App.tsx

เพิ่ม import

import { AppConfig } from './AppConfigContext';

เพิ่ม interface Props

interface Props {
config: AppConfig
}

เพิ่ม parameter ให้ function App

function App(props: Props) {
...

เพิ่ม tag AppConfigContext.Provider มาครอบ code ส่วน return

  return (
<AppConfigContext.Provider value={props.config}>
<div className="App-header">
...
</div>
</AppConfigContext.Provider>
);

5. แก้ไข file src/index.tsx

โดยเปลี่ยนจาก

root.render(
<React.StrictMode>
<App />
<React.StrictMode>
);

เป็น

fetch("/config.json")
.then(r => r.json())
.then(config => {
root.render(
<React.StrictMode>
<App config={config} />
</React.StrictMode>
);
});

6. ลองสร้าง component A ขึ้นมาเรียกใช้ VAR1

สร้างไฟล์ src/components/A.tsx

import { FC, useContext } from "react"
import { AppConfigContext } from "../AppConfigContext";

interface Props {}

export const A: FC<Props> = (props) => {
const { VAR1 } = useContext(AppConfigContext);
return <>{VAR1}</>
}

เอา A ไปเรียกใช้ใน src/App.tsx

import './App.css';
import { AppConfig, AppConfigContext } from './AppConfigContext';
import { A } from './components/A';

interface Props {
config: AppConfig
}

function App(props: Props) {
return (
<AppConfigContext.Provider value={props.config}>
<A />
</AppConfigContext.Provider>
);
}

export default App;

7. ลอง yarn start

จะเห็นค่าเดียวกับที่ใส่ไว้ใน public/config.json แสดงว่าอ่านค่าจากไฟล์ได้สำเร็จ

Build Image

สร้าง Dockerfile

FROM node:18.17.1-bullseye-slim AS builder
WORKDIR /app
COPY ./ ./

RUN yarn && yarn build

FROM nginx:1.21.1-alpine
COPY --from=builder /app/build /usr/share/nginx/html

จากนั้นสั่ง build image

docker build -t test-react-build-once-deploy-anywhere .

สร้าง testenv.config.json เพื่อทดสอบเปลี่ยนค่า VAR1

{
"VAR1": "hello world VAR1 has been changed"
}

ลองรัน container โดย mount เอา testenv.config.json เข้าไป

docker run — rm -v $(pwd)/testenv.config.json:/usr/share/nginx/html/config.json -p 81:80 test-react-build-once-deploy-anywhere

จะเห็นว่า VAR1 ที่ react อ่านได้จะเปลี่ยนไปตาม file config.json ท่ีเรา mount เข้าไปใน container นั่นเอง

หากต้องการ deploy บน kubernetes สามารถใช้ idea เดียวกันได้โดย mount config.json ด้วย ConfigMap

สรุป

เพียงเท่านี้เราก็สามารถ build image ของ React เพียงแค่ครั้งเดียว และใช้กับทุกๆ environment ได้แล้ว เย้ ~

--

--