Skip to content

7.2 · Bootstrap + Webpack build

บทนี้เล่าว่า vtrc-web เริ่มต้นที่ไฟล์ใด ผ่านขั้นตอนไหนก่อนถึงหน้าจอผู้ใช้ และสร้าง bundle สำหรับ production อย่างไร

ถ้า DevOps แจ้งว่า "หน้าขาว" หรือ "bundle ใหญ่เกิน" — บทนี้คือจุดเริ่มต้นสำหรับการตรวจสอบ


Entry point — src/index.js

1:93:vtrc-web/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();

ลำดับการทำงานตั้งแต่บรรทัดแรก:

  1. import moment/locale/th แล้ว moment.locale('th') (บรรทัด 10-12) ตั้ง locale ทั้งระบบเป็นไทย
  2. override longDateFormat ของ th (บรรทัด 36-45) ให้ LL แสดงเป็น D MMMM YYYY แบบไทย
  3. override format/year แบบ +543 ถูก comment ไว้ (บรรทัด 18-33) — การแปลง พ.ศ. ทำที่ helper utils/formatdate.js แทน
  4. ตรวจ OS + browser แล้วแปะ class บน <html> (บรรทัด 46-76) เช่น os-windows, os-macos, win-chrome, mac-chrome สำหรับเขียน CSS workaround
  5. ครอบ <App /> ด้วย <ApolloProvider> + <LocaleProvider locale={th_TH}> แล้ว render ลง #root
  6. 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

1:35:vtrc-web/src/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)

js
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)

js
{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

โหมดmodedevtoolbailTerserPlugin
production'production''source-map' (ถ้า shouldUseSourceMap)trueminify เปิด
development'development''cheap-module-source-map'falseminify ปิด

ที่มา — 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-nativereact-native-web (webpack.config.js:296)
  • ไม่มี @/ หรือ path alias อื่น — ทุก import ใช้ relative path

Plugin สำคัญ

Pluginที่มาหน้าที่
CleanWebpackPluginwebpack.config.jsล้าง build/ ก่อน build
HtmlWebpackPluginwebpack.config.jsinject bundle เข้า public/index.html
WorkboxWebpackPlugin.GenerateSWwebpack.config.js:612-627สร้าง service worker (แต่ฝั่งแอป unregister)
IgnorePlugin(/^\.\/locale$/, /moment$/)webpack.config.js:609ตัด moment locale ที่ไม่ใช้

public/index.html

32:41:vtrc-web/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 startscripts/start.js:1-167dev server + HMR เรียก configFactory('development')
npm run buildscripts/build.js:1-212production build เรียก configFactory('production')
npm testscripts/test.jsjest runner

ทั้ง start.js และ build.js require config/env ก่อนเริ่ม (start.js:15, build.js:15) เพื่อโหลด .env*

Build warning threshold

scripts/build.js:36-37:

js
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;   // 512 KB
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;    // 1 MB

bundle ปัจจุบันเกิน threshold ทั้งคู่ — ทุกครั้งที่ build จะมี warning รายงานขนาด รายละเอียดใน บท 6.9


Docker build — 2 stages

1:19:vtrc-web/Dockerfile
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_แบบแจ้งรายการเพื่อการหักลดหย่อนภาษี.pdf

Stage 1 — build bundle

  • ใช้ node:14.20 (legacy — Node 14 EOL เมษายน 2566)
  • npm install --legacy-peer-deps เพราะ AntD v3 ขัดกับ @ant-design/icons v4 ใน dependency tree
  • npm 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:

BranchPipeline
feature/cicd, develop, uat, prodbuild-front-end-productionbuild-dockerpush-image-to-registry

ขั้นตอนหลัก:

  1. build bundle (ใน node image)
  2. build Docker image
  3. 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       ← เฉพาะ prod

REACT_APP_* จะถูก expose ผ่าน DefinePlugin

ข้อเท็จจริงเกี่ยวกับ env

ไม่พบ .env ใน workspace — env-specific ทั้งหมดทำงานที่ runtime ผ่าน window.location.href ใน checkLinkURL() (ดู บท 6.3)

bundle ตัวเดียวใช้กับทุก env เปลี่ยนเฉพาะ origin ที่เปิด


เคล็ดลับตอนทำงานจริง

ตอน debug "หน้าขาว"

อย่าเพิ่งโทษ network ให้ตรวจสอบตามลำดับ:

  1. เปิด DevTools → Console ดู error ถ้าไม่เห็นอะไรเลย → อาจเป็นเพราะ App/index.js:15 ปิด console ไว้
  2. แสดงความคิดเห็น (comment) บรรทัด console.log = ... = () => {} ใน App/index.js:15 ชั่วคราว แล้ว reload
  3. ดู source map ใน DevTools → Sources เพื่อหาบรรทัดที่ error จริง
  4. ถ้าเป็น 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 ใช้ HOC withApollo ที่ deprecated ใน Apollo v3
  • AntD v3 ใช้ LocaleProvider + Form.create HOC ที่หายไปใน v4
  • React 17 เปลี่ยน event delegation ทำให้บาง component ทำงานผิดปกติ

ตอนเปลี่ยน endpoint

ห้ามเพิ่ม .env หรือ REACT_APP_* — ให้แก้ checkLinkURL() ใน src/config/apollo.js ตาม pattern เดิม ดูรายละเอียดใน บท 6.3


ขั้นตอนถัดไป

ไป บท 6.3 Apollo Client + checkLinkURL