From 46aaf67a7118b99fe4be4403c3fb67457ab9b93d Mon Sep 17 00:00:00 2001 From: Tran Anh Tuan Date: Thu, 20 Nov 2025 17:10:44 +0700 Subject: [PATCH] refactor(.umirc.ts): change proxy and request in dev mode and production mode --- .umirc.ts | 21 +++--- README.md | 33 ++++++++ config/{proxy.ts => proxy_dev.ts} | 4 +- config/proxy_prod.ts | 59 +++++++++++++++ config/{Request.ts => request_dev.ts} | 0 config/request_prod.ts | 105 ++++++++++++++++++++++++++ src/app.tsx | 6 +- tsconfig.json | 24 +++--- 8 files changed, 227 insertions(+), 25 deletions(-) rename config/{proxy.ts => proxy_dev.ts} (85%) create mode 100644 config/proxy_prod.ts rename config/{Request.ts => request_dev.ts} (100%) create mode 100644 config/request_prod.ts diff --git a/.umirc.ts b/.umirc.ts index 755bc67..793bbc0 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -1,8 +1,11 @@ import { defineConfig } from '@umijs/max'; -import proxy from './config/proxy'; -const { REACT_APP_ENV = 'dev' } = process.env as { - REACT_APP_ENV: 'dev' | 'test' | 'prod'; -}; +import proxyDev from './config/proxy_dev'; +import proxyProd from './config/proxy_prod'; +const envConfig = process.env as { REACT_APP_ENV?: 'dev' | 'test' | 'prod' }; +const rawEnv = envConfig.REACT_APP_ENV; +const isProdBuild = process.env.NODE_ENV === 'production'; +const resolvedEnv = isProdBuild && !rawEnv ? 'prod' : rawEnv || 'dev'; +const proxyConfig = isProdBuild ? proxyProd : proxyDev; export default defineConfig({ antd: {}, @@ -17,16 +20,16 @@ export default defineConfig({ title: false, baseSeparator: '-', // support for Vietnamese and English - locales: [ - ['vi-VN', 'Tiếng Việt'], - ['en-US', 'English'], - ], + // locales: [ + // ['vi-VN', 'Tiếng Việt'], + // ['en-US', 'English'], + // ], }, favicons: ['/logo.png'], layout: { title: '2025 Sản phẩm của Mobifone v1.0', }, - proxy: proxy[REACT_APP_ENV], + proxy: proxyConfig[resolvedEnv], routes: [ { title: 'Login', diff --git a/README.md b/README.md index 3dab1f3..9f785e6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,36 @@ # README `@umijs/max` 模板项目,更多功能参考 [Umi Max 简介](https://umijs.org/docs/max/introduce) + +## Language Switching + +- Lang toggle components (`src/components/switch/LangSwitches.tsx`, `src/components/LanguageSwitcher/index.tsx`) call Umi `setLocale(newLocale, false)` so content re-renders instantly without refreshing the page. +- Both components listen for the browser `languagechange` event to keep their UI state in sync when another part of the app switches languages. +- Strings are translated via `useTranslation`/`useIntl`; ensure new labels have entries in `src/locales/en-US.ts` and `src/locales/vi-VN.ts`. + +## Proxy & Request Profiles + +- Project uses two sets of configs under `config/`: + - `proxy.ts` + `Request.ts` for local development API targets. + - `proxy_prod.ts` + `request_prod.ts` for production-like targets. +- The runtime selects configs automatically: + - `pnpm run dev` / any dev server run ⇒ `NODE_ENV=development` ⇒ uses dev proxy/request. + - `pnpm run build` (and other production builds) ⇒ `NODE_ENV=production` ⇒ uses prod proxy/request. +- Override the environment by exporting `REACT_APP_ENV` before running commands (values: `dev`, `test`, `prod`). Example: + +```bash +# Use test upstreams in a dev session +REACT_APP_ENV=test pnpm run dev + +# Build with testing endpoints +NODE_ENV=production REACT_APP_ENV=test pnpm run build +``` + +- `config/proxy_prod.ts` resolves targets dynamically based on the current hostname; adjust rules there if deployment hosts change. +- `config/request_prod.ts` prepends the API base URL from `ApiConfigService`, while the dev file leaves relative paths untouched. + +## Verifying Builds + +- After switching language, confirm UI updates immediately and check console for `languagechange` events. +- For proxy changes, inspect network requests in devtools to ensure they hit the expected environment. +- Run `pnpm run build && pnpm serve` (or your hosting preview) to validate prod settings before deploying. diff --git a/config/proxy.ts b/config/proxy_dev.ts similarity index 85% rename from config/proxy.ts rename to config/proxy_dev.ts index ea18c12..7114440 100644 --- a/config/proxy.ts +++ b/config/proxy_dev.ts @@ -1,4 +1,4 @@ -const proxy: Record = { +const proxyDev: Record = { dev: { '/api': { target: 'http://192.168.30.103:81', @@ -21,4 +21,4 @@ const proxy: Record = { }, }; -export default proxy; +export default proxyDev; diff --git a/config/proxy_prod.ts b/config/proxy_prod.ts new file mode 100644 index 0000000..5777317 --- /dev/null +++ b/config/proxy_prod.ts @@ -0,0 +1,59 @@ +// Hàm lấy IP từ hostname hiện tại (chỉ hoạt động runtime) +const getCurrentIP = () => { + if (typeof window !== 'undefined') { + const hostname = window.location.hostname; + // Nếu là localhost hoặc IP local, trả về IP mặc định + if ( + hostname === 'localhost' || + hostname.startsWith('192.168.') || + hostname.startsWith('10.') + ) { + console.log('Host name: ', hostname); + + return hostname; + } + // Nếu là domain, có thể cần map sang IP tương ứng + return hostname; + } + return process.env.REACT_APP_API_HOST || '192.168.30.102'; // fallback từ env +}; + +// Hàm tạo proxy config động +const createDynamicProxy = () => { + const currentIP = getCurrentIP(); + const isLocalIP = + currentIP.startsWith('192.168.') || + currentIP.startsWith('10.') || + currentIP === 'localhost'; + + return { + dev: { + '/api': { + target: `http://${currentIP}:81`, + changeOrigin: true, + }, + }, + test: { + '/test': { + target: isLocalIP + ? `http://${currentIP}:81` + : 'https://test-sgw-device.gms.vn', + changeOrigin: true, + secure: !isLocalIP, + }, + }, + prod: { + '/test': { + target: isLocalIP + ? `http://${currentIP}:81` + : 'https://prod-sgw-device.gms.vn', + changeOrigin: true, + secure: !isLocalIP, + }, + }, + }; +}; + +const proxyProduct: Record = createDynamicProxy(); + +export default proxyProduct; diff --git a/config/Request.ts b/config/request_dev.ts similarity index 100% rename from config/Request.ts rename to config/request_dev.ts diff --git a/config/request_prod.ts b/config/request_prod.ts new file mode 100644 index 0000000..e16f082 --- /dev/null +++ b/config/request_prod.ts @@ -0,0 +1,105 @@ +import { ROUTE_LOGIN } from '@/constants'; +import { apiConfig } from '@/services/ApiConfigService'; +import { getToken, removeToken } from '@/utils/localStorageUtils'; +import { history, RequestConfig } from '@umijs/max'; +import { message } from 'antd'; + +const codeMessage = { + 200: 'The server successfully returned the requested data。', + 201: 'New or modified data succeeded。', + 202: 'A request has been queued in the background (asynchronous task)。', + 204: 'Data deleted successfully。', + 400: 'There is an error in the request sent, the server did not perform the operation of creating or modifying data。', + 401: 'The user does not have permission (token, username, password is wrong) 。', + 403: 'User is authorized, but access is prohibited。', + 404: 'The request issued was for a non-existent record, the server did not operate。', + 406: 'The requested format is not available。', + 410: 'The requested resource is permanently deleted and will no longer be available。', + 422: 'When creating an object, a validation error occurred。', + 500: 'Server error, please check the server。', + 502: 'Gateway error。', + 503: 'Service unavailable, server temporarily overloaded or maintained。', + 504: 'Gateway timeout。', +}; + +// Runtime configuration +export const handleRequestConfig: RequestConfig = { + // Unified request settings + timeout: 20000, + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + // Error handling: umi@3's error handling scheme. + errorConfig: { + // Error throwing + errorThrower: (res: any) => { + console.log('Response from backend:', res); + const { success, data, errorCode, errorMessage, showType } = res; + if (!success) { + const error: any = new Error(errorMessage); + error.name = 'BizError'; + error.info = { errorCode, errorMessage, showType, data }; + throw error; // Throw custom error + } + }, + // Error catching and handling + errorHandler: (error: any) => { + if (error.response) { + const { status, statusText, data } = error.response; + + // Ưu tiên: codeMessage → backend message → statusText + const errMsg = + codeMessage[status as keyof typeof codeMessage] || + data?.message || + statusText || + 'Unknown error'; + + message.error(`❌ ${status}: ${errMsg}`); + if (status === 401) { + removeToken(); + history.push(ROUTE_LOGIN); + } + } else if (error.request) { + message.error('🚨 No response from server!'); + } else { + message.error(`⚠️ Request setup error: ${error.message}`); + } + }, + }, + + // Request interceptors + requestInterceptors: [ + (url: string, options: any) => { + console.log('URL Request:', url, options); + + // Nếu URL không phải absolute URL, thêm base URL + let finalUrl = url; + if (!url.startsWith('http')) { + const baseUrl = apiConfig.getApiBaseUrl(); + finalUrl = `${baseUrl}${url}`; + } + + const token = getToken(); + return { + url: finalUrl, + options: { + ...options, + headers: { + ...options.headers, + ...(token ? { Authorization: `${token}` } : {}), + }, + }, + }; + }, + ], + + // Unwrap data from backend response + // responseInterceptors: [ + // (response) => { + // const res = response.data as ResponseStructure; + // if (res && res.success) { + // // ✅ Trả ra data luôn thay vì cả object + // return res.data; + // } + // return response.data; + // }, + // ], +}; diff --git a/src/app.tsx b/src/app.tsx index 35546ff..aec37a6 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,7 +1,8 @@ import { LogoutOutlined } from '@ant-design/icons'; import { history, RunTimeLayoutConfig, useIntl } from '@umijs/max'; import { Dropdown } from 'antd'; -import { handleRequestConfig } from '../config/Request'; +import { handleRequestConfig as devRequestConfig } from '../config/request_dev'; +import { handleRequestConfig as prodRequestConfig } from '../config/request_prod'; import UnAccessPage from './components/403/403Page'; import LanguageSwitcher from './components/LanguageSwitcher'; import { ROUTE_LOGIN } from './constants'; @@ -158,4 +159,5 @@ export const layout: RunTimeLayoutConfig = (initialState) => { }; }; -export const request = handleRequestConfig; +const isProdBuild = process.env.NODE_ENV === 'production'; +export const request = isProdBuild ? prodRequestConfig : devRequestConfig; diff --git a/tsconfig.json b/tsconfig.json index e0c7e17..36a627c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,15 @@ { "extends": "./src/.umi/tsconfig.json", - "compilerOptions": { - "target": "es2017", - "lib": ["dom", "es2017"], - "module": "esnext", - "moduleResolution": "node", - "jsx": "react-jsx", - "esModuleInterop": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src"] + // "compilerOptions": { + // "target": "es2017", + // "lib": ["dom", "es2017"], + // "module": "esnext", + // "moduleResolution": "node", + // "jsx": "react-jsx", + // "esModuleInterop": true, + // "skipLibCheck": true, + // "strict": true, + // "forceConsistentCasingInFileNames": true, + // }, + // "include": ["src"] }