Skip to content

4.2.1 · Repository map + Bootstrap + Apollo Client + Routing/Auth gate

บทนี้รวม 4 หัวข้อที่ V1 แยกเป็น 4 บท (repository-map / bootstrap-build / apollo-checklinkurl / routing-auth-gate) เพราะเนื้อหาสัมพันธ์กันแน่น — เข้าใจโครงสร้าง repo ก่อน → ดู entry point ที่ boot → ดู Apollo Client ที่ต่อ backend → ดู routing ที่เปิด/ปิด auth gate


ภาพรวม repo — vtrc-rc-backoffice

vtrc-rc-backoffice เป็น backoffice หลักของระบบ VTRC (ต่างจาก vtrc-backoffice และ vtrc-backoffice-new ที่เป็น backoffice ของฟีเจอร์เฉพาะทำนอง module ใหม่) เปิดให้ admin/staff ของสำนักงานใหญ่และศูนย์บริการใช้งาน บริหาร user/role/news/policy/organization/career/course/leave/welfare/hospital/payment และอื่น ๆ

  • Stack: React 16.14 + CRA 3.4.3 + AntD v4.15 + MUI v5.11 (ใช้น้อย) + Apollo v2 (react-apollo@3.1) + Recoil 0.7 + react-router-dom 5
  • Branch: master (769 tracked files — branch develop มี 769 เท่ากันแต่ commits ต่างกัน)
  • Repo size: 871 ไฟล์ใน src/pages/, 104 ไฟล์ใน src/components/, src/graphql/queries.js 4,108 บรรทัด + mutations.js 2,208 บรรทัด
  • Production URL: https://vtrcbackoffice.redcross.or.th/ (และ https://rc-vtrcbackoffice.redcross.or.th/ สำหรับศูนย์บริการ)
  • Staging URL: https://uat-vtrcbackoffice.redcross.or.th/ (และ https://uat-rc-vtrcbackoffice.redcross.or.th/)
  • Firebase Hosting: https://backoffice-vtrc.web.app/ (ใช้ endpoint คนละตัว — api.vtrcapp.com/api-dev/graphql)

โครงสร้าง src/

src/
├── index.js                       33 บรรทัด    entry point (ApolloProvider + RecoilRoot)
├── serviceWorker.js               ~140 บรรทัด  CRA default (unregister)
├── config/
│   ├── apollo.js                  187 บรรทัด    Apollo Client + checkLinkURL + errorLink
│   ├── routes.js                2,551 บรรทัด    route table (route definitions + SiderLayout wiring)
│   ├── custom-antd.css          ~13,000 บรรทัด   AntD v4 override (compiled)
│   └── axios-bo.js, axios-pms.js, axios-ai.js, refreshToken.js, datasets*.js
├── containers/App/index.js         33 บรรทัด    root router + auth gate (overlaps routes.js)
├── graphql/
│   ├── queries.js                4,108 บรรทัด    121 queries (gql operation names)
│   ├── mutations.js              2,208 บรรทัด    94 mutations
│   ├── WorkForce/queries.js     ~1,500 บรรทัด    ~40 WorkForce-specific queries
│   ├── WorkForce/mutations.js   ~900 บรรทัด      ~30 WorkForce-specific mutations
│   └── index.js                                    re-export hub
├── pages/                         871 ไฟล์       feature screens (รวม `TheamCss` — typo)
├── components/                    104 ไฟล์       shell (SiderLayout, Header, Breadcrumb), form controls, modals
├── utils/                                       helpers (auth.js, apiKey.js, message.js, axios wrappers)
└── assets/                                      css (main.css, redesign.css), images, fonts

ไฟล์ legacy/backup — มากกว่า 75 ไฟล์ ห้ามแตะ

vtrc-rc-backoffice สะสมไฟล์ backup/snapshot จากการ deploy หลาย env และ refactor ที่ยังไม่ลบ — pattern เดียวกับ vtrc-web

รูปแบบตัวอย่าง
_bk.js, _bk-<date>.jsqueries_bk-061265.js, mutations_bk.js
_prd.js, _pentest.jssnapshot ของ env prod / pentest
_gql.js, _axios.js, _table.jssnapshot ของ refactor ทดลอง

กฎเหล็ก — (1) อย่าแก้ไฟล์ที่ลงท้ายด้วยรูปแบบ backup (2) อย่า copy โค้ดจากไฟล์เหล่านี้เป็นต้นแบบ (3) อย่าลบทิ้งเองโดยไม่ตรวจ git history ก่อน

วิธีตรวจว่าไฟล์เป็นของจริง — เช็คว่าไม่มี suffix ผิดปกติ + ถูก import จาก routes.js + ถ้าเป็น query/mutation ต้องอยู่ใน queries.js/mutations.js ตัวจริง


Entry point — src/index.js

vtrc-rc-backoffice/src/index.js:1-33

javascript
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';

// const Root = () => (
//   <ApolloProvider client={apolloClient} >
//     <App />
//   </ApolloProvider>
// )

class Root extends Component {
  render() {
    return (
      <ApolloProvider client={apolloClient} >
        <RecoilRoot>
          <App />
        </RecoilRoot>
      </ApolloProvider>
    );
  }
}

ReactDOM.render(<Root />, document.getElementById('root'))

serviceWorker.unregister()

บรรทัดที่สำคัญ:

  • ลำดับการ mount: ApolloProvider (หุ้มนอกสุด) → RecoilRootApp (root router)
  • Root เป็น class component ที่ commented-out function version อยู่ด้านบน (index.js:10-14) — ห้ามลบ comment นั้น ผู้พัฒนาเก็บไว้เป็น reference
  • ApolloProvider รับ apolloClient จาก default export ของ ./config/apollo — เป็น singleton ที่สร้างตอน import
  • RecoilRoot หุ้มไว้ใต้ Apollo ทำให้ useRecoilState/useSetRecoilState/useRecoilValue ใช้ได้ในทุก component ลูก

serviceWorker.unregister() — ผลกระทบ

CRA default — ไม่ลงทะเบียน service worker ทำให้ browser ไม่เก็บ cache ฝั่ง client — ผู้ใช้ต้อง refresh เพื่อรับเวอร์ชันใหม่เสมอ (ตรงข้ามกับแนว PWA) ผลคือ deploy ใหม่แล้ว user อาจเห็นโค้ดเก่าได้ถ้า browser cache ฝั่ง HTTP ยัง active (ดู nginx config ในส่วน Dockerfile)


App container — root router + auth gate

vtrc-rc-backoffice/src/containers/App/index.js:1-33

javascript
import React from 'react'
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'
import { withApollo } from 'react-apollo'
import { Queries, Mutations } from '../../graphql'
import { isLoggedIn } from '../../utils/auth'
import SiderLayout from '../../components/SiderLayout'
import Login from '../../pages/Login'
import TheamCss from '../../pages/TheamCss'
import PathLogin from '../../pages/PathLogin'
import NotFound from '../../pages/NotFound'
import '../../config/custom-antd.css'
import 'antd/dist/antd.css'
import '../../assets/css/main.css'
import '../../assets/css/redesign.css'

function App () {
	console.log = console.warn = console.error = () => { };
	// console.log('login')
	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>
	)
}

export default withApollo(App)

สิ่งที่ต้องระวังใน App/index.js

  1. console.log = console.warn = console.error = () => { }; — ปิด console ทุกชนิดใน production ทำให้ debug ด้วย console.* ไม่ได้ ต้องใช้ debugger หรือ React DevTools ผลกระทบคือ error ฝั่ง client ไม่ถูก log ที่ browser console ทำให้นักพัฒนาไม่เห็น error จริงในการ debug

  2. ไม่มี PrivateRoute component — auth gate เป็น conditional render: isLoggedIn() && <Route …/> คู่กับ !isLoggedIn() && <Redirect to="/login" /> pattern นี้ทำให้ routes ทั้งหมด mount/unmount ทีเดียวกันเมื่อสถานะ login เปลี่ยน

  3. isLoggedIn() คืน boolean — ตรวจว่ามีทั้ง token และ refreshToken ใน localStorage/sessionStorage (ดู src/utils/auth.js) ไม่ได้คืน JWT string และไม่ได้ validate token ฝั่ง client — เช็คแค่ "มีอยู่หรือไม่"

  4. TheamCss — typo ในชื่อจริง (ควรเป็น ThemeCss) ห้ามแก้เพราะ path และ import อ้างชื่อนี้ทั้งระบบ เป็นหน้า dev tool สำหรับ preview CSS

  5. PathLogin route /pathLogin/:token/:refreshTokenSSO bypass ที่รับ token ทั้งคู่ผ่าน URL path (ดูวิเคราะห์เชิงลึกใน บท 4.2.5 Security analysis)

  6. CSS importscustom-antd.css (override) → antd.css (AntD core) → main.cssredesign.css ลำดับสำคัญเพราะ cascade ฝั่ง browser

  7. withApollo(App) — inject client เข้า props ของ App ทำให้ component ลูกเรียก this.props.client.query()/.mutate() ได้ (pattern class component) ส่วน function component ใช้ useQuery/useMutation ตรง ๆ ผ่าน hook ของ react-apollo (ไม่ต้อง withApollo)


Apollo Client — src/config/apollo.js

vtrc-rc-backoffice/src/config/apollo.js:1-187 เป็นไฟล์เดียวที่บริหาร GraphQL connection ทั้งหมด — checkLinkURL() + authLink + errorLink + uploadLink + apolloFetch + apolloClient

checkLinkURL() — env-based endpoint resolution

vtrc-rc-backoffice/src/config/apollo.js:14-36

javascript
export const checkLinkURL = () => {
	if (window.location.href.includes("https://vtrcbackoffice.redcross.or.th/")
		|| window.location.href.includes("https://rc-vtrcbackoffice.redcross.or.th/")) {
		return "https://vtrcapi.redcross.or.th/api/graphql"

	} else if (window.location.href.includes("https://uat-vtrcbackoffice.redcross.or.th/")) {
		return "https://uat-vtrcapi.redcross.or.th/api-uat/graphql"
      // return "https://uat-vtrcapi.redcross.or.th/api/graphql";

	} else if (window.location.href.includes("https://uat-rc-vtrcbackoffice.redcross.or.th/")) {
		return "https://uat-vtrcapi.redcross.or.th/api-uat/graphql"
		// return "https://uat-vtrcapi.redcross.or.th/api-uat/graphql"
      // return "https://uat-vtrcapi.redcross.or.th/api/graphql";
	} else if (window.location.href.includes("http://localhost:3000/") || window.location.href.includes("http://localhost:3001/")) {
		return "https://uat-vtrcapi.redcross.or.th/api-uat/graphql"
      // return "https://uat-vtrcapi.redcross.or.th/api/graphql"
		// return "http://localhost:8081/api/graphql"
		// return "http://192.168.0.149:8081/api-uat/graphql"

	} else if (window.location.href.includes("https://backoffice-vtrc.web.app")) {
		return "https://api.vtrcapp.com/api-dev/graphql"
	}
}

5 env ที่ตรวจด้วย window.location.href.includes(...) ไม่ใช้ NODE_ENV:

OriginGraphQL endpointใช้โดย
https://vtrcbackoffice.redcross.or.th/https://vtrcapi.redcross.or.th/api/graphqlbackoffice prod หลัก
https://rc-vtrcbackoffice.redcross.or.th/https://vtrcapi.redcross.or.th/api/graphqlbackoffice prod ศูนย์บริการ (ใช้ backend ตัวเดียวกับด้านบน)
https://uat-vtrcbackoffice.redcross.or.th/https://uat-vtrcapi.redcross.or.th/api-uat/graphqlstaging หลัก
https://uat-rc-vtrcbackoffice.redcross.or.th/https://uat-vtrcapi.redcross.or.th/api-uat/graphqlstaging ศูนย์บริการ
http://localhost:3000/ หรือ http://localhost:3001/https://uat-vtrcapi.redcross.or.th/api-uat/graphqldev local (ชี้ที่ staging backend)
https://backoffice-vtrc.web.apphttps://api.vtrcapp.com/api-dev/graphqlFirebase Hosting (env dev แยก)

หมายเหตุสำคัญ — V1 ไม่ถูกต้อง: V1 เอกสารฉบับเดิมระบุว่ามี RSA encryption สำหรับ login ใน repo vtrc-rc-backoffice นี้ แต่การ grep JSEncrypt, setPublicKey, node-forge, jsencrypt, RSA, publicKey ใน src/ ของ repo นี้ ไม่พบ library RSA ใด ๆ — Login (pages/Login/index.js:55-66) ส่ง password: password เป็น plaintext ผ่าน Mutations.loginBackoffice ไม่มีการ encrypt ฝั่ง client (RSA encryption มีจริงใน vtrc-web และ vtrc-mobile แต่ไม่ใช่ repo นี้) — ความผิดพลาดนี้เกิดจาก V1 copy pattern จาก vtrc-web มาผิด ๆ

Comments ที่ต้องระวัง

  • apollo.js:21, apollo.js:25-26, apollo.js:29-31 — มี commented-out endpoint หลายตัว (uat-vtrcapi.redcross.or.th/api/graphql ไม่มี -uat, http://localhost:8081/api/graphql, http://192.168.0.149:8081/...) — เหล่านี้คือ endpoint ทดลอง ห้าม uncomment โดยไม่ตรวจกับทีม backend

vtrc-rc-backoffice/src/config/apollo.js:48-65

javascript
const authLink = setContext((_, { headers }) => {
	const { token } = getToken()
	const API_DEVICE_KEY = apiKeyNews()
	if (token) {
		return {
			headers: {
				...headers,
				Authorization: `Bearer ${token}`,
				apiKey: API_DEVICE_KEY
			}
		}
	}
	return {
		headers: {
			apiKey: API_DEVICE_KEY
		}
	}
})
  • getToken() (จาก utils/auth.js) — อ่าน token จาก localStorage/sessionStorage
  • apiKeyNews() (จาก utils/apiKey.js) — คืน device key ที่ map กับ APP_TYPE=WEB_ADMIN (หรือค่าที่ custom สำหรับ backoffice)
  • header apiKey ส่งทุก request แม้จะยังไม่ login — backend (vtrc-api) ใช้ค่านี้ resolve APP_TYPE ก่อนประมวลผล resolver
  • header Authorization: Bearer {token} ส่งเฉพาะเมื่อ token มีอยู่ — ทำให้หน้า login (Mutations.loginBackoffice) ทำงานได้โดยไม่ต้องส่ง bearer token

vtrc-rc-backoffice/src/config/apollo.js:67-169 ยาว 102 บรรทัดเพราะจัดการ error code 8+ แบบ:

extensions.codeActionRedirect?
TOKEN_EXPIREDrefresh ผ่าน Queries.refreshTokenBO แล้ว retry operationไม่ (ลอง refresh ก่อน)
TOKEN_INVALIDalert "พบข้อผิดพลาด กรุณาติดต่อผู้ดูแลระบบ" → clearAllStorage()/login
INTERNAL_SERVER_ERRORalert → clearAllStorage()/login
SESSION_EXPIRED, SESSION_INVALIDalert + clearAllStorage()/login
CANNOT_ACCESSalert + clearAllStorage() (delay 1s)/login
AUTH_USER_OR_PASS_INVALIDalert message จาก backendไม่ (อยู่หน้า login)
AUTH_LIMITalert + clearAllStorage() (delay 1s)/login
USER_OR_ORG_WRONGalert message จาก backendไม่
DATA_USEDalert message จาก backendไม่
PROFILE_NOT_FOUNDalert + clearAllStorage() (delay 1s)/login
NOT_SSO_ID(ทั้ง block ถูก comment ไว้ — ไม่ทำอะไรเลย)ไม่
(default extensions.code อื่น ๆ)alert message จาก backendไม่

Token refresh logic — apollo.js:67-105

javascript
const errorLink = onError(({ graphQLErrors, operation, forward }) => {
	if (graphQLErrors && graphQLErrors[0].extensions.code === 'TOKEN_EXPIRED') {
		return new Observable(async observer => {
			const oldHeaders = operation.getContext().headers
			const { errors, data } = await apolloClient.query({
				query: Queries.refreshTokenBO,
				variables: {
					refreshToken: getToken().refreshToken,
					token: getToken().token
				},
			});
			console.log('result refreshTokenBO : appllo :', { errors, data })
			if (!errors) {
				setToken(data.refreshTokenBO)
				operation.setContext({
					headers: {
						...oldHeaders,
						Authorization: `Bearer ${data.refreshTokenBO.accessToken}`
					}
				})
				const subscriber = {
					next: observer.next.bind(observer),
					error: observer.error.bind(observer),
					complete: observer.complete.bind(observer)
				}
				forward(operation).subscribe(subscriber)
			} else if (errors && errors[0].extensions.code === 'TOKEN_INVALID') {
				alertMessage("error", "พบข้อผิดพลาด กรุณาติดต่อผู้ดูแลระบบ")
				clearAllStorage()
				window.location.href = '/login'
			}
		})
	}
	// ... ระบบอื่น ๆ

จุดที่น่าสนใจ:

  • refresh ใช้ query ชื่อ refreshTokenBO (มี suffix BO = BackOffice) ไม่ใช่ refreshToken ธรรมดา — backend แยก flow refresh ระหว่าง user-side (refreshToken) และ backoffice (refreshTokenBO)
  • หลัง refresh สำเร็จ เรียก setToken(data.refreshTokenBO) เก็บ token ใหม่ แล้ว set context ใหม่ก่อน forward(operation) retry
  • ถ้า refresh เองก็ fail ด้วย TOKEN_INVALID → logout ทันที
  • console.log('result refreshTokenBO : appllo :', ...) ที่ apollo.js:78 — ทำงานจริง แต่ถูกปิดผลกระทบโดย App/index.js:17 (console.log = () => {}) ทำให้ไม่แสดงใน browser console
  • ไม่มี retry limit — ถ้า refresh loop (token หมด → refresh → token หมด → refresh → ...) ไม่มีกลไกตัด ต้องพึ่ง browser abort หรือ user ปิด tab

apolloClient — singleton + defaultOptions

vtrc-rc-backoffice/src/config/apollo.js:171-187

javascript
const apolloClient = new ApolloClient({
	link: ApolloLink.from([errorLink, authLink, uploadLink]),
	cache: new InMemoryCache()
})

apolloClient.defaultOptions = {
	watchQuery: {
		fetchPolicy: 'no-cache',
		errorPolicy: 'ignore'
	},
	query: {
		fetchPolicy: 'no-cache',
		errorPolicy: 'all'
	}
}

export default apolloClient

สิ่งที่ต้องระวัง:

  • fetchPolicy: 'no-cache' — ทุก query ดึงจาก backend ทุกครั้ง ไม่ใช้ cache ฝั่ง Apollo ทำให้ UI อัปเดตเสมอแต่ network load สูง (เหมาะกับ backoffice ที่ต้องการข้อมูลใหม่)
  • errorPolicy: 'ignore' สำหรับ watchQuery — error จาก GraphQL ไม่ทำให้ component crash แต่ก็ไม่เข้า error ของ useQuery ด้วย — error ถูก handle ที่ errorLink เท่านั้น
  • errorPolicy: 'all' สำหรับ query — error เข้าทั้ง data และ error ของ promise
  • link order: errorLink → authLink → uploadLink — errorLink อยู่นอกสุดเพื่อ catch error ทั้งหมด, authLink แทรก header ก่อน uploadLink ส่ง request จริง

vtrc-rc-backoffice/src/config/apollo.js:38-46

javascript
const apolloFetch = createApolloFetch({ uri: checkLinkURL() })
const uploadLink = createUploadLink({ uri: checkLinkURL() })

apolloFetch.use(({ request, options }, next) => {
	options.headers = {
		apiKey: apiKeyNews(),
	};
	next();
});
  • apolloFetch — ใช้สำหรับ apolloClient.query({ fetchPolicy: 'no-cache' }) บาง flow (เช่น refresh token) ที่ต้องการ raw fetch ไม่ผ่าน Apollo cache
  • uploadLink — จาก apollo-upload-client รองรับ file upload ผ่าน GraphQL multipart
  • apolloFetch เพิ่ม apiKey header เท่านั้น ไม่ได้เพิ่ม Authorization — ใช้สำหรับ query ที่ไม่จำเป็นต้อง login (เช่น refreshToken เอง)

Routing — src/config/routes.js + SiderLayout wiring

routes.js ยาว 2,551 บรรทัด — เป็นไฟล์ใหญ่ที่สุดใน src/config/ มีทั้ง import statements, route definitions (<Route path=... component={...} />), helper functions และ SiderLayout wiring

vtrc-rc-backoffice/src/config/routes.js:1-40 (import statements — เห็นการ mix module เก่าใหม่):

javascript
import React from "react";
import { Route, Switch } from "react-router-dom";
import NotFound from "../pages/NotFound";
import News from "../pages/News";
import FormNews from "../pages/News/form";
import Organization from "../pages/Organization";
import Formorganization from "../pages/Organization/form";
import Career from "../pages/Career";
import FormCareer from "../pages/Career/form";
import Manageuser from "../pages/ManageUser";
import ManageuserMenu from "../pages/ManageUser/menu";
import Course from "../pages/Course";
import CategoryCourse from "../pages/WorkForce/CategoryCourse";
import Prefix from "../pages/WorkForce/Prefix";
import FormCourse from "../pages/Course/form";
import Notification from "../pages/Notification";
import Policy from "../pages/Policy";
import PolicyForm from "../pages/Policy/form";
import PolicyCategory from "../pages/Policy/category";

import HospitalSearchForm from "../pages/HospitalHr/Search";
import HospitalForm from "../pages/HospitalHr/Form";
// ... 2,511 บรรทัดเพิ่มเติม

โครงสร้าง routes.js

ไฟล์แบ่งเป็น 4 ส่วนใหญ่ ๆ:

  1. Imports (~400 บรรทัด) — import ทุก page component ที่จะ render ใน SiderLayout
  2. Route table (~1,200 บรรทัด) — <Route exact path="/news" component={News} /> ลำดับตาม feature
  3. Helper functions (~500 บรรทัด) — function สำหรับ render route ตาม role/menu permission (checkMenuPermission, renderRouteIfAccessible, etc.)
  4. SiderLayout wiring (~400 บรรทัด) — mapping route → menu item → icon → breadcrumb

ลำดับ route ที่สำคัญ

RouteComponentหมวด
/news, /news/form/:id?News, FormNewsNews/announcement
/organization, /organization/form/:id?Organization, FormorganizationOrganization master
/career, /career/form/:id?Career, FormCareerCareer master
/manageuser, /manageuser/menu/:idManageuser, ManageuserMenuUser/role CRUD
/course, /course/form/:id?Course, FormCourseCourse master
/notificationNotificationNotification blast
/policy, /policy/form/:id?, /policy/categoryPolicy, PolicyForm, PolicyCategoryPolicy document
/hospitalhr/search, /hospitalhr/form/:id?, /hospitalhr/withdraw/...HospitalSearchForm, HospitalForm, HospitalWithdraw*Hospital HR
/hospitalpaid/payments/...HospitalPaidPayments, etc.Hospital payment
/payment/paid, /payment/cash, /payment/exportpayment/...PaymentPagePaid, PaymentPagCash, PaymentPagExportPayment*Payment workflow
/WorkForce/...(many)WorkForce module (เริ่มปี 2022)
/pms/...(many)PMS module
/recruitment/...(many)Recruitment module

PathLogin — SSO bypass route

vtrc-rc-backoffice/src/containers/App/index.js:24

javascript
<Route path="/pathLogin/:token/:refreshToken" exact component={PathLogin}/>

route นี้รับ token และ refreshToken ผ่าน URL path (ไม่ใช่ query string) สำหรับ SSO จากระบบอื่น — component PathLogin อ่าน params, เก็บลง localStorage, redirect ไป / ทำให้ isLoggedIn() คืน true

ความเสี่ยงด้าน security: token ทั้งคู่ปรากฏใน browser history, server access log, referer header (ถ้ามี link ออก) — severity S3 ใน บท 4.2.5

TheamCss — typo route

vtrc-rc-backoffice/src/containers/App/index.js:23

javascript
<Route path="/TheamCss" exact component={TheamCss} />

TheamCss (ควรเป็น ThemeCss) เป็น dev tool สำหรับ preview CSS — ไม่มี auth gate (mount ก่อน isLoggedIn() check) ผู้พัฒนาใช้ตรวจ styling โดยไม่ต้อง login ห้ามเผยแพร่ URL นี้เพราะ bypass auth


CRA build — Dockerfile + nginx

vtrc-rc-backoffice/Dockerfile:1-17

dockerfile
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

  1. stage-1 — base node:16 ลง dependency ด้วย npm install --legacy-peer-deps (จำเป็นเพราะ peer dep conflict ระหว่าง react-apollo + @apollo/client + MUI v5) แล้ว npm run build ปิด source map (GENERATE_SOURCEMAP=false) เพิ่ม heap 4GB (NODE_OPTIONS=--max-old-space-size=4096) เพราะ Webpack ของ CRA 3.4.3 out-of-memory ถ้าไม่เพิ่ม
  2. stage-2 — base nginx:1.24.0 คัดลอก nginx/default.conf และ static จาก build/ ไปที่ /usr/share/nginx/html/build/
  3. 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 ช้า — build/ ที่ commit อยู่ใน repo (จากการ deploy manual ครั้งเดือนที่แล้ว) มีขนาด ~130 MB
  • CI=false — ปิด CRA warning-as-error ที่ CRA ทำตอน CI env (ไม่งั้น warning เรื่อง <forge> หรือ source map จะ fail build)
  • Firebase Hosting (backoffice-vtrc.web.app) เป็นอีกช่องทาง deploy ที่ checkLinkURL() รองรับแยกต่างหาก (ดูตาราง env ด้านบน)

npm scripts

vtrc-rc-backoffice/package.json (scripts section):

json
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
}

script ทั้งหมดมาจาก CRA default — ไม่มี custom script


Bundle size — ทำไมใหญ่

CRA ไม่มี built-in bundle analyzer แต่จากการประเมิน package.json dependencies:

หมวดLibraryขนาดประมาณการ
UIantd@4.15 (full import)~1.2 MB
UI@mui/*@5.11 (ใช้น้อย)~300 KB
Staterecoil@0.7~50 KB
GraphQLreact-apollo@3.1 + apollo-client + apollo-link-* + apollo-upload-client + apollo-cache-inmemory + apollo-fetch~280 KB
Datemoment@2.29 (with locale)~300 KB
Chartschart.js@3 + react-chartjs-2~250 KB
Reportstimulsoft-reports-js~5 MB
Excelxlsx@0.18, exceljs~800 KB
PDF@react-pdf/renderer, react-pdf, pdfjs-dist~2 MB
Cryptocrypto-js~120 KB
Misclodash, axios, qrcode, react-router-dom, ...~1 MB

รวมประมาณ 11-13 MB (gzip ~3-4 MB) เพราะ:

  1. ไม่มี code splittingReact.lazy + Suspense แทบไม่มี Stimulsoft ถูก load ตั้งแต่ page load แม้ user จะไม่ได้ใช้ report
  2. AntD v4 import แบบ full — ไม่ได้ใช้ babel-plugin-import สำหรับ tree-shake
  3. MUI v5 อยู่ใน dependencies แต่ใช้แค่ไม่กี่ไฟล์ — น่าจะเริ่มทดลองแต่ยังไม่ migrate
  4. Stimulsoft ใหญ่มาก (~5 MB) แต่ใช้เฉพาะหน้า report — ควรแยก chunk ด้วย React.lazy(() => import('./StimulsoftReport'))

.env — ไม่ขับ GraphQL

vtrc-rc-backoffice/.env (committed — เป็น debt S2 ใน บท 4.2.5) มีตัวแปรเช่น:

  • REACT_APP_END_POINT_RECRUITMENT_API_ATTACHFILE
  • REACT_APP_END_POINT_EXPORT
  • REACT_APP_AI_RECRUITMENT_API
  • REACT_APP_PMS_API_END_POINT
  • REACT_APP_STIMUL_CONN_STRING
  • REACT_APP_PRIVATE_KEY

ไม่มี REACT_APP_API_URL และใน src/ ไม่มีโค้ดที่อ่าน REACT_APP_API_URL

GraphQL endpoint ของ Apollo หลักตัดสินด้วย checkLinkURL() ใน src/config/apollo.js ตาม window.location.href เท่านั้น (ดูตาราง env ด้านบน)

axios client สำหรับ microservice (axios-pms.js, axios-ai.js, axios-bo.js) อ่าน REACT_APP_* ของตัวเอง — คนละเรื่องกับ GraphQL


browserslist

vtrc-rc-backoffice/package.json (browserslist):

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" (IE, Opera Mini ฯลฯ)


เปรียบเทียบกับ vtrc-web

ด้านvtrc-rc-backofficevtrc-web
Build systemCRA 3.4.3 (ไม่ eject)Webpack 4 (eject แล้ว)
Webpack configdefault ของ CRAcustom ใน config/webpack.config.dev.js
Node ใน Docker (build)1614
Static serve ใน Dockernginx:1.24.0(ดู vtrc-web ch.6)
console.* ปิดใช้ (App/index.js:17)ใช้
Bundle~11-13 MB~12 MB
Code splittingแทบไม่มีแทบไม่มี
LoginMutations.loginBackoffice (plaintext, no RSA)Mutations.login + RSA encrypt (JSEncrypt)
RSA encryptionไม่มีมี (utils/rsa.js, JSEncrypt)
Apollo Clientapollo-client@2.x + react-apollo@3.1 + apollo-link-*เดียวกัน
StateRecoil 0.7 + class component this.state (mix)class component this.state เกือบล้วน
AntDv4.15 + custom override 13K บรรทัดv3.x + custom override 21K บรรทัด
MUIv5.11 (ใช้น้อย)ไม่มี
GraphQL queries121 queries + 94 mutations (รวม WorkForce module แยก)ดู vtrc-web ch.2
SSO bypass route/pathLogin/:token/:refreshTokenไม่มี
Routingreact-router-dom 5 + routes.js 2,551 บรรทัดreact-router-dom 5 + routes.js 1,410 บรรทัด
jsconfig.json absolute importไม่มีมี

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

ไป บท 4.2.2 GraphQL catalog + Component patterns + Recoil