React กับการทำ Build Once, Deploy Anywhere
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/
จึงจะมาลองทำดูซักหน่อย
เริ่มกันเลย
- เริ่มจากสร้าง 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 ได้แล้ว เย้ ~