7.2 · Bootstrap + Webpack build
บทนี้เล่าว่า vtrc-web เริ่มต้นที่ไฟล์ใด ผ่านขั้นตอนไหนก่อนถึงหน้าจอผู้ใช้ และสร้าง bundle สำหรับ production อย่างไร
ถ้า DevOps แจ้งว่า "หน้าขาว" หรือ "bundle ใหญ่เกิน" — บทนี้คือจุดเริ่มต้นสำหรับการตรวจสอบ
Entry point — src/index.js
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 { LocaleProvider } from 'antd';
import th_TH from 'antd/lib/locale-provider/th_TH';
import moment from 'moment';
import 'moment/locale/th';
moment.locale('th');
// ... (moment override + OS/browser detection) ...
class Root extends Component {
render() {
return (
<ApolloProvider client={apolloClient}>
<LocaleProvider locale={th_TH}>
<App />
</LocaleProvider>
</ApolloProvider>
);
}
}
ReactDOM.render(<Root />, document.getElementById('root'));
serviceWorker.unregister();ลำดับการทำงานตั้งแต่บรรทัดแรก:
- import
moment/locale/thแล้วmoment.locale('th')(บรรทัด 10-12) ตั้ง locale ทั้งระบบเป็นไทย - override
longDateFormatของ th (บรรทัด 36-45) ให้LLแสดงเป็นD MMMM YYYYแบบไทย - override
format/yearแบบ +543 ถูก comment ไว้ (บรรทัด 18-33) — การแปลง พ.ศ. ทำที่ helperutils/formatdate.jsแทน - ตรวจ OS + browser แล้วแปะ class บน
<html>(บรรทัด 46-76) เช่นos-windows,os-macos,win-chrome,mac-chromeสำหรับเขียน CSS workaround - ครอบ
<App />ด้วย<ApolloProvider>+<LocaleProvider locale={th_TH}>แล้ว render ลง#root serviceWorker.unregister()(บรรทัด 93) ปิด CRA service worker — webpack ยัง generate SW ไว้ แต่ฝั่งแอปไม่ register จึงไม่ทำงาน
ข้อสังเกต
- ไม่มี
<ErrorBoundary>ระดับ root — runtime error ใน component ลูกจะทำให้หน้าขาวทันที <LocaleProvider>ของ AntD v3 ต่างจาก<ConfigProvider>ของ v4 ห้ามสับเปลี่ยนmoment.updateLocale('th', ...)(บรรทัด 36) เป็น global side effect — เปลี่ยน locale ของ moment ทุกจุดในแอป
Root component — containers/App/index.js
import React from 'react'
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'
import { isLoggedIn } from '../../utils/auth'
import Login from '../../pages/user/Login'
import ConfirmUserA1 from '../../pages/user/ConfirmUserA1'
import ConfirmUserA2 from '../../pages/user/ConfirmUserA2'
import SetPasswordsFirstTime from '../../pages/user/SetPasswordsFirstTime'
import SiderLayout from '../../components/user/SiderLayout'
import ForgetPassword from '../../pages/user/ForgetPassword'
import NotFound from '../../pages/user/NotFound'
function App() {
console.log = console.warn = console.error = () => { };
return (
<BrowserRouter>
<Switch>
<Route path="/login" exact component={Login} />
<Route path="/verify/user/A1" exact component={ConfirmUserA1} />
<Route path="/verify/user/A2" exact component={ConfirmUserA2} />
<Route path="/forgetpassword" exact component={ForgetPassword} />
<Route path="/passwords/first/time" exact component={SetPasswordsFirstTime} />
{isLoggedIn() && <Route path="/" component={SiderLayout} />}
{!isLoggedIn() && <Redirect to="/login" />}
<Route component={NotFound} />
</Switch>
</BrowserRouter>
)
}
export default Appส่วนสำคัญ 3 จุด
1. ปิด console ทั้งหมด (บรรทัด 15)
console.log = console.warn = console.error = () => { };เป็น global side effect ที่ทำให้ debug ฝั่ง production ได้ยาก — ทุก console.* ถูกทำให้เป็น no-op ตั้งแต่แอปเริ่ม ถ้าจะ debug ฝั่ง browser ต้อง comment บรรทัดนี้ชั่วคราว หรือใช้ debugger statement + DevTools
2. Auth gate ตัวเดียวคือ isLoggedIn() (บรรทัด 27-28)
{isLoggedIn() && <Route path="/" component={SiderLayout} />}
{!isLoggedIn() && <Redirect to="/login" />}isLoggedIn() จาก utils/auth.js:91-97 ตรวจว่ามี token + refreshToken ครบทั้งคู่ใน storage รายละเอียดใน บท 6.4
3. Public routes แยกต่างหาก (บรรทัด 20-26)
5 path ที่ไม่ต้อง login: /login, /verify/user/A1, /verify/user/A2, /forgetpassword, /passwords/first/time — เป็น onboarding + auth flow
Webpack config — CRA ejected
config/webpack.config.js:1-672 — เป็น CRA ที่ eject แล้ว แก้ไขได้ทุกอย่าง แต่ต้องรับผิดชอบการ upgrade เอง
โหมดการ build
| โหมด | mode | devtool | bail | TerserPlugin |
|---|---|---|---|---|
| production | 'production' | 'source-map' (ถ้า shouldUseSourceMap) | true | minify เปิด |
| development | 'development' | 'cheap-module-source-map' | false | minify ปิด |
ที่มา — config/webpack.config.js:134-141, 198-261
Entry + output
entry: paths.appIndexJs (webpack.config.js:158)
output: paths.appBuild + static/js/[name].[contenthash:8].js (prod)
static/js/bundle.js (dev)ไม่มี code splitting ระดับ route — splitChunks แยก vendor (chunks: 'all', webpack.config.js:265-268) แต่ไม่มี React.lazy ทำให้ทุก page อยู่ใน main bundle
Alias
react-native→react-native-web(webpack.config.js:296)- ไม่มี
@/หรือ path alias อื่น — ทุก import ใช้ relative path
Plugin สำคัญ
| Plugin | ที่มา | หน้าที่ |
|---|---|---|
CleanWebpackPlugin | webpack.config.js | ล้าง build/ ก่อน build |
HtmlWebpackPlugin | webpack.config.js | inject bundle เข้า public/index.html |
WorkboxWebpackPlugin.GenerateSW | webpack.config.js:612-627 | สร้าง service worker (แต่ฝั่งแอป unregister) |
IgnorePlugin(/^\.\/locale$/, /moment$/) | webpack.config.js:609 | ตัด moment locale ที่ไม่ใช้ |
public/index.html
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-LKXD2EBDZ4"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-LKXD2EBDZ4');
</script>
<title>VTRC Web App</title>Google Analytics (GA4 id G-LKXD2EBDZ4) ฝังตรงใน HTML template แทนที่จะใช้ library แยก — โหลดก่อน bundle
meta cache-control ตั้ง no-cache + expires: 0 + pragma: no-cache (index.html:6, 13-15) เพื่อบังคับให้ browser ดู bundle เวอร์ชันใหม่เสมอ
scripts/ — start + build
| สคริปต์ | ที่มา | หน้าที่ |
|---|---|---|
npm start | scripts/start.js:1-167 | dev server + HMR เรียก configFactory('development') |
npm run build | scripts/build.js:1-212 | production build เรียก configFactory('production') |
npm test | scripts/test.js | jest runner |
ทั้ง start.js และ build.js require config/env ก่อนเริ่ม (start.js:15, build.js:15) เพื่อโหลด .env*
Build warning threshold
scripts/build.js:36-37:
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; // 512 KB
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; // 1 MBbundle ปัจจุบันเกิน threshold ทั้งคู่ — ทุกครั้งที่ build จะมี warning รายงานขนาด รายละเอียดใน บท 6.9
Docker build — 2 stages
FROM node:14.20 AS stage-1
WORKDIR /usr/src/app
ADD . .
RUN npm install --legacy-peer-deps
RUN npm run build
FROM nginx:1.24.0 AS stage-2
COPY ./nginx-config/default.conf /etc/nginx/conf.d/
COPY --from=stage-1 usr/src/app/build/ /usr/share/nginx/html/build/
COPY --from=stage-1 usr/src/app/src/assets/images/07_ไฟล์แนบ_แนวปฏิบัติสำหรับผู้ได้รับอุบัติ.pdf /usr/share/nginx/html/build/assets/07_ไฟล์แนบ_แนวปฏิบัติสำหรับผู้ได้รับอุบัติ.pdf
COPY --from=stage-1 usr/src/app/src/assets/images/16_แบบแจ้งรายการเพื่อการหักลดหย่อนภาษี.pdf /usr/share/nginx/html/build/assets/16_แบบแจ้งรายการเพื่อการหักลดหย่อนภาษี.pdfStage 1 — build bundle
- ใช้
node:14.20(legacy — Node 14 EOL เมษายน 2566) npm install --legacy-peer-depsเพราะ AntD v3 ขัดกับ@ant-design/iconsv4 ใน dependency treenpm run buildสร้าง bundle ในbuild/
Stage 2 — nginx serve
nginx:1.24.0- copy
default.confจากnginx-config/ - copy
build/ไป/usr/share/nginx/html/build/ - copy PDF แนบ 2 ไฟล์ (ชื่อไทย) แยกต่างหาก เพราะเก็บใน
src/assets/images/ไม่ได้ถูก webpack processed
nginx config
nginx.conf:1-7 คอนฟิกหลัก, nginx-config/default.conf คอนฟิก server block:
- listen port 80
try_files $uri $uri/ /index.html— SPA fallback- root
/usr/share/nginx/html/build
CI/CD — bitbucket-pipelines.yml
bitbucket-pipelines.yml:1-180 — branch trigger:
| Branch | Pipeline |
|---|---|
feature/cicd, develop, uat, prod | build-front-end-production → build-docker → push-image-to-registry |
ขั้นตอนหลัก:
- build bundle (ใน node image)
- build Docker image
- push ไป GCP/Cloud Registry โดย tag ด้วย commit short hash +
:latest
env ทำงานอย่างไร
config/env.js:1-90 — CRA env loader มาตรฐาน โหลดตามลำดับ:
.env ← ทุก env
.env.local ← ทุก env (ไม่ commit)
.env.development ← เฉพาะ dev
.env.production ← เฉพาะ prodREACT_APP_* จะถูก expose ผ่าน DefinePlugin
ข้อเท็จจริงเกี่ยวกับ env
ไม่พบ .env ใน workspace — env-specific ทั้งหมดทำงานที่ runtime ผ่าน window.location.href ใน checkLinkURL() (ดู บท 6.3)
bundle ตัวเดียวใช้กับทุก env เปลี่ยนเฉพาะ origin ที่เปิด
เคล็ดลับตอนทำงานจริง
ตอน debug "หน้าขาว"
อย่าเพิ่งโทษ network ให้ตรวจสอบตามลำดับ:
- เปิด DevTools → Console ดู error ถ้าไม่เห็นอะไรเลย → อาจเป็นเพราะ
App/index.js:15ปิด console ไว้ - แสดงความคิดเห็น (comment) บรรทัด
console.log = ... = () => {}ในApp/index.js:15ชั่วคราว แล้ว reload - ดู source map ใน DevTools → Sources เพื่อหาบรรทัดที่ error จริง
- ถ้าเป็น runtime error ที่ไม่มี ErrorBoundary → ทุก component บนหน้าจะ unmount
ตอน build แล้วเจอ warning ขนาด bundle
warning threshold ตั้งไว้ที่ 512 KB (gzip) — bundle ปัจจุบันใหญ่กว่ามาก รายละเอียดและแนวทางแก้ใน บท 6.9
ตอน upgrade dependency
ห้าม upgrade React เป็น 17/18, Apollo เป็น v3, หรือ AntD ข้าม major โดยไม่ได้รับมอบหมาย — เป็น breaking change เพราะ:
react-apollo@3.1.5ใช้ HOCwithApolloที่ deprecated ใน Apollo v3- AntD v3 ใช้
LocaleProvider+Form.createHOC ที่หายไปใน v4 - React 17 เปลี่ยน event delegation ทำให้บาง component ทำงานผิดปกติ
ตอนเปลี่ยน endpoint
ห้ามเพิ่ม .env หรือ REACT_APP_* — ให้แก้ checkLinkURL() ใน src/config/apollo.js ตาม pattern เดิม ดูรายละเอียดใน บท 6.3