8.2 · Bootstrap + CRA build
บทนี้อธิบายว่าแอปเริ่มที่ไฟล์ใด ลำดับการ mount อย่างไร และ CRA สร้าง bundle อย่างไร
Entry point
CRA เริ่มที่ src/index.js (default convention) — เป็นไฟล์เดียวกับที่ public/index.html เสียบผ่าน <script src="%PUBLIC_URL%/static/js/bundle.js"> (สร้างโดย webpack ของ CRA)
import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import { ApolloProvider } from 'react-apollo'
import App from './containers/App'
import * as serviceWorker from './serviceWorker'
import apolloClient from './config/apollo'
import { RecoilRoot } from 'recoil';
class Root extends Component {
render() {
return (
<ApolloProvider client={apolloClient} >
<RecoilRoot>
<App />
</RecoilRoot>
</ApolloProvider>
);
}
}
ReactDOM.render(<Root />, document.getElementById('root'))
serviceWorker.unregister()ลำดับการ mount
ApolloProviderหุ้มด้วยapolloClient(default export จากsrc/config/apollo.js) — component ทุกตัวเข้าถึง client ผ่านwithApolloHOC หรือ hook ของreact-apolloRecoilRootหุ้มApp— ทำให้useRecoilState/useSetRecoilState/useRecoilValueใช้ได้ (module WorkForce, PMS)App(src/containers/App/index.js) mount ที่#root— CSS หลัก (antd.css,main.css,redesign.css,custom-antd.css) ถูก import ที่นี่ ไม่ใช่ที่index.jsserviceWorker.unregister()— CRA default ไม่ลงทะเบียน service worker
serviceWorker.unregister
CRA default — ไม่ลงทะเบียน service worker ทำให้แอปไม่เก็บ cache ฝั่ง browser — ผู้ใช้ต้อง refresh เพื่อรับเวอร์ชันใหม่เสมอ (ตรงข้ามกับแนว PWA)
App container — root router + auth gate
function App () {
console.log = console.warn = console.error = () => { };
return (
<BrowserRouter>
<Switch>
<Route path="/login" exact component={Login} />
<Route path="/TheamCss" exact component={TheamCss} />
<Route path="/pathLogin/:token/:refreshToken" exact component={PathLogin}/>
{isLoggedIn() && <Route path="/" component={SiderLayout} />}
{!isLoggedIn() && <Redirect to="/login" />}
<Route component={NotFound} />
</Switch>
</BrowserRouter>
)
}สิ่งที่ต้องระวังใน App/index.js
console.log = console.warn = console.error = () => {};— ปิด console ทุกชนิดใน production ทำให้ debug ด้วยconsole.*ไม่ได้ ต้องใช้debuggerหรือ React DevTools- ไม่มี
PrivateRoutecomponent — auth gate คือ conditional render:isLoggedIn() && <Route …/>คู่กับ!isLoggedIn() && <Redirect to="/login" /> isLoggedIn()คืน boolean — ตรวจว่ามีทั้งtokenและrefreshTokenใน localStorage/sessionStorage (ดู บท 7.4) ไม่ได้คืน JWT string- โฟลเดอร์/หน้า
TheamCss— typo ในชื่อจริง ห้ามแก้เพราะ path และ import อ้างชื่อนี้
CRA build — ภาพรวม
vtrc-rc-backoffice/package.json ใช้ react-scripts@3.4.3 (CRA ไม่ eject) — script ทั้งหมดมาจาก CRA default:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}สิ่งที่ CRA ทำตอน npm run build
- Webpack (ฝังใน react-scripts) อ่าน
public/index.htmlและsrc/index.js - Babel transpile JSX/ES2020+ → ES5 (browserslist จาก
package.json) - PostCSS/Autoprefixer จัดการ CSS
- แยก bundle:
static/js/main.<hash>.js— โค้ดฝั่ง appstatic/js/<chunk>.<hash>.chunk.js— lazy chunk (เกือบไม่มี เพราะไม่ค่อยมีReact.lazy)static/css/main.<hash>.css— CSS รวมstatic/media/*— assets
- สร้าง
build/index.htmlพร้อมแทรก<script>และ<link>ที่มี hash
ข้อจำกัดของ CRA 3.4.3
- webpack 4 (ล้าสมัยแล้ว — CRA 5 ใช้ webpack 5)
- ไม่สามารถกำหนด webpack config เองได้ โดยตรง ต้องใช้
rescripts/cracoหรือ eject (เลือกไม่ eject) - ไม่รองรับ Module Federation
- tree-shaking อ่อน — ทำให้ bundle ใหญ่ในบางส่วน
ไฟล์ config ของ CRA
| ไฟล์ | หน้าที่ |
|---|---|
package.json | dependencies, scripts, browserslist, eslintConfig |
.env | ค่า REACT_APP_* สำหรับ microservice บางตัว (เช่น PMS, AI recruitment) — ไม่มี REACT_APP_API_URL และไม่ได้ขับ GraphQL |
public/manifest.json | PWA metadata |
public/index.html | template ของ HTML |
Dockerfile | multi-stage: node:16 build → nginx:1.24.0 serve |
nginx/default.conf | SPA try_files → /index.html |
ไม่มี jsconfig.json ใน repo — import ใช้ relative path เป็นหลัก
browserslist
จาก package.json:
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}เป้าหมายครอบคลุมเบราว์เซอร์ที่มีส่วนแบ่ง > 0.2% ทั่วโลก ไม่รวม "dead" และไม่รวม Opera Mini
Dockerfile — multi-stage + nginx
FROM node:16 AS stage-1
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --legacy-peer-deps
COPY . .
RUN CI=false GENERATE_SOURCEMAP=false NODE_OPTIONS=--max-old-space-size=4096 npm run build
FROM nginx:1.24.0 AS stage-2
COPY ./nginx/default.conf /etc/nginx/conf.d/
COPY --from=stage-1 /usr/src/app/build/ /usr/share/nginx/html/build/ขั้นตอนใน Docker
- stage-1 — base
node:16ลง dependency ด้วยnpm install --legacy-peer-depsแล้วnpm run build(ปิด source map, เพิ่ม heap 4GB) - stage-2 — base
nginx:1.24.0คัดลอกnginx/default.confและ static จากbuild/ไปที่/usr/share/nginx/html/build/ - nginx listen port 80 (ไม่ใช่ 3000) — SPA fallback ผ่าน
try_files $uri $uri/ /index.html
ข้อระวังเรื่อง Docker
- ไม่ใช้
serve -s— production image เป็น nginx ไม่ใช่ Node + serve COPY . .คัดลอกbuild/ที่มีอยู่แล้วมาด้วย ถ้า repo มีbuild/commit ไว้ ทำให้ image ใหญ่และ build ช้า- Firebase Hosting (
backoffice-vtrc.web.app) เป็นอีกช่องทาง deploy ที่checkLinkURL()รองรับแยกต่างหาก
ขนาด bundle — ทำไมใหญ่
CRA ไม่มี built-in bundle analyzer แต่จากการประเมิน package.json dependencies:
| หมวด | Library | ขนาดประมาณการ |
|---|---|---|
| UI | antd@4.15 (full import) | ~1.2 MB |
| UI | @mui/*@5.11 (ใช้น้อยมาก) | ~300 KB |
| State | recoil@0.7 | ~50 KB |
| GraphQL | react-apollo@3.1 + apollo-client + apollo-link-* | ~250 KB |
| Date | moment@2.29 (with locale) | ~300 KB |
| Charts | chart.js@3 + react-chartjs-2 | ~250 KB |
| Report | stimulsoft-reports-js | ~5 MB |
| Excel | xlsx@0.18, exceljs | ~800 KB |
@react-pdf/renderer, react-pdf, pdfjs-dist | ~2 MB | |
| Misc | lodash, axios, crypto-js, qrcode, … | ~1 MB |
รวมประมาณ 11-13 MB (gzip ~3-4 MB) เพราะ:
- ไม่มี code splitting —
React.lazy+Suspenseแทบไม่มี - AntD v4 import แบบ full
- MUI v5 อยู่ใน dependencies แต่ใช้แค่ไม่กี่ไฟล์
- Stimulsoft ใหญ่มากแต่ใช้เฉพาะหน้า report
.env — ไม่ขับ GraphQL
ไฟล์ .env ที่ root ของ repo มีตัวแปรเช่น:
REACT_APP_END_POINT_RECRUITMENT_API_ATTACHFILEREACT_APP_END_POINT_EXPORTREACT_APP_AI_RECRUITMENT_APIREACT_APP_PMS_API_END_POINTREACT_APP_STIMUL_CONN_STRINGREACT_APP_PRIVATE_KEY
ไม่มี REACT_APP_API_URL และใน src/ ไม่มีโค้ดที่อ่าน REACT_APP_API_URL
GraphQL endpoint ของ Apollo หลักตัดสินด้วย checkLinkURL() ใน src/config/apollo.js ตาม window.location.href เท่านั้น (ดู บท 7.3)
บาง axios client (เช่น axios-pms.js, axios-ai.js) อ่าน REACT_APP_* ของตัวเอง — คนละเรื่องกับ GraphQL
npm scripts ที่ใช้จริง
| Script | ใช้ทำอะไร |
|---|---|
npm start | dev server ที่ http://localhost:3000 (hot reload) |
npm run build | สร้าง build/ สำหรับ deploy |
npm test | รัน Jest (แทบไม่มี test จริง) |
npm run eject | eject CRA (ห้ามกดโดยไม่จำเป็น) |
เปรียบเทียบกับ vtrc-web (ดู Volume 8 บท 6.2)
| ด้าน | vtrc-rc-backoffice | vtrc-web |
|---|---|---|
| Build system | CRA 3.4.3 (ไม่ eject) | Webpack 4 (eject แล้ว) |
| Config ได้ | ไม่ได้ (ต้อง eject/craco) | ได้เต็ม |
| Node ใน Docker (build) | 16 | 14 |
| Static serve ใน Docker | nginx:1.24.0 | (ดู Volume 6) |
console.* ปิด | ใช้ | ใช้ |
| Bundle | ~11-13 MB | ~12 MB |
| Code splitting | แทบไม่มี | แทบไม่มี |
| jsconfig absolute | ไม่มี | (ดู Volume 6) |