一、创建react项目 (node 版本 20)
这里我们使用react+js+react-router-dom 5+redux+axio
1.创建 vreact项目,安装创建项目 (这里我创建的是react19.2.0)
npm init vite@latest


启动项目
npm install
npm run dev

一般情况下,我们需要按照需要配置以下目录结构,如状态管理、路由、静态资源、组件、页面、工具包,这些都需要在src下有一席之地,按照我个人的习惯,我预先建立了比较常见的以下目录:

2.安装其他插件
2.1配置resolve.alias
//vite.congit.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve:{
alias:{
'@': path.resolve(__dirname, './src')
}
}
})
2.2安装npm i react-router-dom,,注意这里的版本是v5
npm install react-router-dom@7.10.1
在views中简单写两个页面,用来切换路由使用在router文件夹中编写路由文件index.js,导出路由模块
//home.jsx
import React from 'react';
import { useNavigate } from 'react-router-dom';
const Home = () => {
let navigate = useNavigate();
return (
<div style={{ padding: '20px' }}>
<h1>首页</h1>
<p>这是首页内容,用于路由跳转测试。</p>
<div onClick={() => navigate('/find')} style={{ cursor: 'pointer', color: 'blue', textDecoration: 'underline' }}>前往发现页面</div>
</div>
);
};
export default Home;
//find.jsx
import React from 'react';
import { useNavigate } from 'react-router-dom';
const Find = () => {
const navigate = useNavigate();
return (
<div style={{ padding: '20px' }}>
<h1>发现页面</h1>
<p>这是发现页面内容,用于路由跳转测试。</p>
<div onClick={() => navigate('/')} style={{ cursor: 'pointer', color: 'blue', textDecoration: 'underline' }}>返回首页</div>
</div>
);
};
export default Find;
在app.jsx中将路由模块导入
import { HashRouter, Routes, Route } from 'react-router-dom'
import { routes } from './router/index.jsx'
//app.jsx
import React from 'react'
import './App.css'
import { HashRouter, Routes, Route } from 'react-router-dom'
import { routes } from './router/index.jsx'
function App() {
return (
<>
<HashRouter>
<Routes>
{routes.map((router) => (
<Route key={router.path} {...router} />
))}
</Routes>
</HashRouter>
</>
)
}
export default App

2.3 状态管理,前端框架三剑客之一,这里我们将为我们的项目配置redux状态管理
安装以下依赖包npm install reduxnpm install react-reduxnpm install @reduxjs/toolkitnpm install redux-thunknpm install esbuild

创建一个Redux切片(Slice)并在store中进行导入和配置
切片是应用中单个功能的Redux reducer 逻辑和 action 的集合,在这里做了一个计数功能的切片以供学习使用,代码如下
//counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
// 初始状态
const initialState = {
value: 0,
};
// 创建slice
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// Redux Toolkit 允许我们在 reducers 中直接修改状态
// 它不会真正修改状态,而是使用 Immer 库来创建状态的不可变副本
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
// 导出 actions
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// 导出 reducer
export default counterSlice.reducer;
创建store
你可以在你的src目录中创建一个名为store的文件夹并在里面新建一个index.js文件,并把刚才的redux切片导入
在该文件中你需要有以下的结构
// index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
// 配置并创建store
export const store = configureStore({
reducer: {
// 这里可以添加多个reducer
counter: counterReducer,
},
});

把配置好的store与应用进行连接
在应用的main.jsx中添加如下代码
//main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
// Redux
import { Provider } from 'react-redux'
import { store } from './store/index.js'
createRoot(document.getElementById('root')).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
)
使用和修改store中的state
其实到上一步,redux已经在项目中配置好了,这一步我们主要是看看store是否配置成功和测试一下使用方法,随便找一个应用中的组件:
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'; //引入两个钩子函数
//在home.jsx中使用
import { increment, decrement, incrementByAmount } from '@/store/counterSlice'; //按需引入action
const Home = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const count = useSelector((state) => state.counter.value);
return (
<div style={{ padding: '20px' }}>
<h1>首页</h1>
<p>这是首页内容,用于路由跳转测试。</p>
<div onClick={() => navigate('/find')} style={{ cursor: 'pointer', color: 'blue', textDecoration: 'underline', marginBottom: '20px' }}>前往发现页面</div>
{/* Redux 计数器示例 */}
<div style={{ marginTop: '30px', padding: '20px', border: '1px solid #ccc', borderRadius: '8px', maxWidth: '300px' }}>
<h2>Redux 计数器</h2>
<p>计数: {count}</p>
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
</div>
</div>
);
};
export default Home;
二、页面实现适配
1. 安装npm install -D sass-embedded CSS预处理器
这里是:react中css写法以及用法大全

//home.module.scss
.home{
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
h1{
font-size: 32px;
}
p{
font-size: 18px;
}
h2{
font-size: 18px;
}
}
2. 使用amfe-flexible和postcss-pxtorem实现适配
# 安装核心依赖 npm install amfe-flexible postcss-pxtorem autoprefixer postcss-import postcss-url --save-dev
a.在vite.config.js中进行配置
//vit.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import postcssImport from 'postcss-import';
import postcssUrl from 'postcss-url';
import autoprefixer from 'autoprefixer';
import pxtorem from 'postcss-pxtorem';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve:{
alias:{
'@': path.resolve(__dirname, './src')
}
},
// 配置 CSS
css: {
postcss: {
plugins: [
// 处理 @import 和 url() 路径
postcssImport(),
postcssUrl(),
// 自动添加浏览器前缀
autoprefixer({
overrideBrowserslist: [
'last 5 version',
'>1%',
'ie >=8'
]
}),
// px 转 rem(核心适配插件)
pxtorem({
rootValue: 192, // 1920设计稿除以10
minPixelValue: 1, // 最小转换值,1px及以上转换
unitPrecision: 6, // 转换后的小数位数
propList: ['*'], // 所有属性都转换
selectorBlackList: ['aaa-'], // 匹配不被转换的选择器
replace: true, // 直接替换而不是添加备用
mediaQuery: false, // 媒体查询中的px不转换
exclude: /node_modules/i // 排除 node_modules
})
]
}
},
server:{
proxy: {
'/DreamOne': {
target: 'http://10.3.4.174:8080/DreamOne/',
changeOrigin: true,
rewrite: (path) => {
return path.replace(/^/DreamOne/, '')
},
}
}
}
})
也可以单独维护
postcss.config.js,把他引入到vite.cofig.js中就行
// postcss.config.js
const postcssImport = require('postcss-import');
const postcssUrl = require('postcss-url');
const autoprefixer = require('autoprefixer');
const pxtorem = require('postcss-pxtorem');
module.exports = {
plugins: [
postcssImport(),
postcssUrl(),
autoprefixer({
overrideBrowserslist: [
'last 5 version',
'>1%',
'ie >=8'
]
}),
pxtorem({
rootValue: 192,
minPixelValue: 1,
unitPrecision: 6,
propList: ['*'],
selectorBlackList: ['aaa-'],
replace: true,
mediaQuery: false,
exclude: /node_modules/i
})
]
};
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
css: {
postcss: './postcss.config.js'
}
});
b.引入 amfe-flexible,在项目入口文件 或
src/main.jsx 中:
src/main.tsx
import 'amfe-flexible'; // 引入自适应方案
c.创建内联样式转换工具
由于内联样式不会被 PostCSS 处理,需要手动转换:
// src/utils/px2rem.js
/**
* 将px转换为rem(基于192设计稿)
* @param {number|string} px - px值
* @returns {string} rem值
*/
export const px2rem = (px) => {
if (typeof px === 'string') {
// 处理带单位的情况
const match = px.match(/^(d+(.d+)?)(px)?$/);
if (match) {
const num = parseFloat(match[1]);
return `${num / 192}rem`;
}
return px; // 如果不是纯数字或px单位,原样返回
}
return `${px / 192}rem`;
};
/**
* 批量转换样式对象中的px到rem
* @param {Object} style - 样式对象
* @param {Array} excludeProps - 排除的属性列表
* @returns {Object} 转换后的样式对象
*/
export const styleToRem = (style, excludeProps = []) => {
const result = {};
const pxProperties = [
'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight',
'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
'top', 'right', 'bottom', 'left',
'fontSize', 'lineHeight', 'letterSpacing',
'border', 'borderWidth', 'borderRadius', 'borderTopLeftRadius',
'borderTopRightRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius',
'gap', 'rowGap', 'columnGap',
'outline', 'outlineWidth',
'textIndent'
];
Object.keys(style).forEach(key => {
const value = style[key];
// 检查是否在排除列表中
if (excludeProps.includes(key)) {
result[key] = value;
return;
}
// 检查是否是数字
if (pxProperties.includes(key) && typeof value === 'number') {
result[key] = px2rem(value);
}
// 检查是否是带px的字符串
else if (pxProperties.includes(key) &&
typeof value === 'string' &&
/^-?d+(.d+)?px$/.test(value)) {
const pxValue = parseFloat(value);
result[key] = px2rem(pxValue);
}
// 其他情况直接赋值
else {
result[key] = value;
}
});
return result;
};
在home.jsx中使用
//home/home.jsx
import React from 'react';
import styles from './home.module.scss';
import { useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'; //引入两个钩子函数
import { increment, decrement, incrementByAmount } from '@/store/counterSlice'; //按需引入action
import { px2rem, styleToRem } from '@/utils/px2rem';
const Home = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const count = useSelector((state) => state.counter.value);
return (
<div className={styles.home}>
<h1>首页</h1>
<p>这是首页内容,用于路由跳转测试。</p>
<div onClick={() => navigate('/find')} style={{ cursor: 'pointer', color: 'blue', textDecoration: 'underline', marginBottom: px2rem(20), fontSize: px2rem(18) }}>前往发现页面</div>
{/* Redux 计数器示例 */}
<div style={{ marginTop: px2rem(30), padding: px2rem(20), border: '1px solid #ccc', borderRadius: px2rem(8), maxWidth: px2rem(300) }}>
<h2>Redux 计数器</h2>
<p>计数: {count}</p>
<div style={{ display: 'flex', gap: px2rem(10), marginBottom: px2rem(10) }}>
<button style={{ padding: px2rem(5), borderRadius: px2rem(4), border: '1px solid #ccc', fontSize: px2rem(16) }} onClick={() => dispatch(increment())}>+1</button>
<button style={{ padding: px2rem(5), borderRadius: px2rem(4), border: '1px solid #ccc', fontSize: px2rem(16) }} onClick={() => dispatch(decrement())}>-1</button>
<button style={{ padding: px2rem(5), borderRadius: px2rem(4), border: '1px solid #ccc', fontSize: px2rem(16) }} onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
</div>
</div>
);
};
export default Home;

三、axios的封装
安装
npm install axios qs umi-request js-base64
npm install antd
a.在utils创建config.js、dsfRequest.js、api2.js文件
这里注意import.meta.env.VITE_API_URL是.env环境变量中的会在第四点配置
//config.js
//配置服务端地址
const serverUrl = {
doPost: "/skytraffic/doPost",
doAction: "/skytraffic/doAction",
WebRoot: "/skytraffic"
}
export {
serverUrl
}
//dsfRequest.js
/**
* request 网络请求工具
* 更详细的 api 文档: https://github.com/umijs/umi-request
*/
import { extend } from 'umi-request';
//https://www.npmjs.com/package/js-base64
import { Base64 } from 'js-base64';
import { message } from 'antd'
import { serverUrl } from './config'
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
}
/**
* 异常处理程序
*/
const errorHandler = function(error) {
const { response } = error
if (response && response.status) {
const errorText = codeMessage[response.status] || response.statusText
const { status, url } = response
message.error(`请求错误 ${status}: ${url}`)
}
return response
}
/**
* 配置request请求时的默认参数
*/
const request = extend({
errorHandler, // 默认错误处理
credentials: 'include', // 默认请求是否带上cookie
})
// request拦截器, 改变url 或 options.
request.interceptors.request.use((url, options) => {
options.headers = options.headers || {};
options.headers.code = 'MOBILE';
let devApiUrl = process.env.VUE_APP_BASE_API
if (url.indexOf("?") > -1) {
//处理自动识别上传地址,带问号的都视为原请求方式
return ({
url: `${devApiUrl+serverUrl.WebRoot+(url.indexOf('/')==0?"":"/")+url}`,
options: options,
})
}
let method = ""
const lastIndex = url.lastIndexOf(".")
if (lastIndex > 0) {
const lastName = url.substr(lastIndex)
if (lastName == ".xml" || lastName == ".bl") {
method = "post"
} else {
method = "action"
}
} else {
method = "post"
}
options.data = options.data || {}
// const headers = options.headers;
// headers.code = 'MOBILE';
const requestData = createRequest(url, JSON.stringify(options.data))
const reqUrl = method.toLowerCase() == "post" ? serverUrl.doPost : serverUrl.doAction
// const { NODE_ENV } = process.env
return ({
url: `${devApiUrl+reqUrl}`,
options: { method: 'post', requestType: 'json', data: requestData, headers: options.headers },
})
})
// response拦截器, 处理response
request.interceptors.response.use((response, options) => {
//response.headers.append('code', 'MOBILE');
return response
})
// 克隆响应对象做解析处理
request.interceptors.response.use(async(response) => {
const data = await response.clone().json();
if (!data.Result) {
return response
}
if (data && data.Result && data.Result.Success) {
return data.Result.Data;
} else {
message.error(`请求错误===》` + data.Result.Data)
}
return response;
})
/**
* dsf框架请求后台组织xml格式进行base64编码啊
*/
const createRequest = function(path,
content,
requestformat,
responseformat,
options,
expro, enCode) {
requestformat = requestformat || 'JSON'
responseformat = responseformat || 'JSON'
content = content || ''
options = options || {}
let d = content
let b = ""
let isCache = false
let isHead = true
if (options) {
if (typeof(options) == "object") {
isCache = options.isCache || false
isHead = options.isHead != false ? true : false
} else {
isCache = true
}
}
if (isCache) {
b = `<Cache type='MEMORY' period='6000000000000'></Cache>`;
}
if (typeof(content) == "string" && content.indexOf("<Data>") < 0) {
if (enCode != false) {
if (requestformat == "JSON") {
//alert("test");
//let data = $("<Data></Data>").text(content);
//content = data.html();
}
//let bb = "";
}
d = `<Data>${content}</Data>`
}
expro = expro ? `exinfo="${expro}" ` : ``
let name = options.name || ""
let request = `<Request ${name ? 'name="' + name + '"' : ""} action="${path}" request="${requestformat}" response="${responseformat}" ${!isHead ? 'nohead="true"' : ''} ${expro}>${b + d}</Request>`
if (options.base64 != false) {
return Base64.encode(request)
} else {
return request
}
}
// Base64.encode('dankogai'); // ZGFua29nYWk=
// Base64.encode('小飼弾'); // 5bCP6aO85by+
// Base64.encodeURI('小飼弾'); // 5bCP6aO85by-
// Base64.decode('ZGFua29nYWk='); // dankogai
// Base64.decode('5bCP6aO85by+'); // 小飼弾
// // note .decodeURI() is unnecessary since it accepts both flavors
// Base64.decode('5bCP6aO85by-'); // 小飼弾
export default request
//api2.js
import axios from 'axios';
import request from '../utils/dsfRequest.js';
import qs from 'qs';
export const getJson = function (method) {
console.log(import.meta.env);
return new Promise((resolve, reject) => {
axios({
method: 'get',
url: method,
dataType: "json",
crossDomain: true,
cache: false
}).then(res => {
resolve(res)
}).catch(error => {
reject(error)
})
})
}
export const getServerData = function (url = "", params = {}) {
return new Promise((resolve, reject) => {
request(url, { data: params }).then(res => {
resolve(res)
}).catch(err => {
resolve(err)
})
})
}
export const http = {
get: function (url, params, options) {
if (import.meta.env.VITE_NODE_ENV == "production") {
url = import.meta.env.VITE_API_URL + url
}else{
url='/DreamWeb'+url
}
let opts = {
params: params,
headers: {},
paramsSerializer: function (params) {
return qs.stringify(params, {
arrayFormat: "repeat"
});
}
};
opts.transformRequest = [
function (data) {
let ret = "";
for (let it in data) {
ret +=
encodeURIComponent(it) + "=" + encodeURIComponent(data[it]) + "&";
}
return ret;
}
];
opts = Object.assign(opts, options || {});
let p = axios.get(url, opts);
return p;
},
post: function (url, params, options) {
if (import.meta.env.VITE_NODE_ENV == "production") {
url = import.meta.env.VITE_API_URL + url
}else{
url='/DreamWeb'+url
}
let configContentType =
options && options.headers && options.headers["Content-Type"] ?
options.headers["Content-Type"] :
"";
let opts = {
headers: {}
};
opts.transformRequest = [
function (data) {
let ret;
if (configContentType.includes("multipart/form-data")) {
ret = data;
} else if (configContentType.includes("application/json")) {
ret = JSON.stringify(data);
} else {
ret = "";
for (let it in data) {
ret +=
encodeURIComponent(it) + "=" + encodeURIComponent(data[it]) + "&";
}
}
return ret;
}
];
opts = Object.assign(opts, options || {});
let p = axios.post(url, params, opts);
return p;
}
}
b.在home页面中使用
注意这里的json文件需要再public文件中,要不打包的时候打不上
//home.jsx
import {useEffect} from 'react';
import styles from './home.module.scss';
import { useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'; //引入两个钩子函数
import { increment, decrement, incrementByAmount } from '@/store/counterSlice'; //按需引入action
import { px2rem, styleToRem } from '@/utils/px2rem';
import { getJson } from '@/utils/api2.js';
const Home = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const count = useSelector((state) => state.counter.value);
useEffect(()=>{
getJson('/test.json').then(res=>{
console.log(res,"++++")
})
},[])
return (
<div className={styles.home}>
<h1>首页</h1>
<p>这是首页内容,用于路由跳转测试。</p>
<div onClick={() => navigate('/find')} style={{ cursor: 'pointer', color: 'blue', textDecoration: 'underline', marginBottom: px2rem(20), fontSize: px2rem(18) }}>前往发现页面</div>
{/* Redux 计数器示例 */}
<div style={{ marginTop: px2rem(30), padding: px2rem(20), border: '1px solid #ccc', borderRadius: px2rem(8), maxWidth: px2rem(300) }}>
<h2>Redux 计数器</h2>
<p>计数: {count}</p>
<div style={{ display: 'flex', gap: px2rem(10), marginBottom: px2rem(10) }}>
<button style={{ padding: px2rem(5), borderRadius: px2rem(4), border: '1px solid #ccc', fontSize: px2rem(16) }} onClick={() => dispatch(increment())}>+1</button>
<button style={{ padding: px2rem(5), borderRadius: px2rem(4), border: '1px solid #ccc', fontSize: px2rem(16) }} onClick={() => dispatch(decrement())}>-1</button>
<button style={{ padding: px2rem(5), borderRadius: px2rem(4), border: '1px solid #ccc', fontSize: px2rem(16) }} onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
</div>
</div>
);
};
export default Home;

四、根据不同.env环境配置文件打包
这里以.env.prod文件为例
a.创建.env.prod文件
#.env.prod 文件
# 本地环境接口地址(这里是使用了代理,解决跨域问题)
#所在环境
VITE_NODE_ENV = 'production'
# 业务中台本地环境接口地址
VITE_API_URL = '/DreamWeb'
# 打包文件名
VITE_APP_NAME = 'dist_pc'
b.在vite.config.js中进行打包配置
//vite.config.js
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import postcssImport from 'postcss-import';
import postcssUrl from 'postcss-url';
import autoprefixer from 'autoprefixer';
import pxtorem from 'postcss-pxtorem';
const Timestamp = new Date().getTime();//随机时间戳
// https://vite.dev/config/
export default defineConfig(({mode})=>{
const env = loadEnv(mode, process.cwd());
console.log(env);
let fileName = 'dist'
// 兼容性,以防打包崩溃
fileName = env.VITE_APP_NAME
return {
base: '',//项目名称开发或生产环境服务的公共基础路径。
plugins: [react()],
resolve:{
alias:{
'@': path.resolve(__dirname, './src')
}
},
server:{
proxy: {
'/DreamOne': {
target: 'http://10.3.4.174:8080/DreamOne/',
changeOrigin: true,
rewrite: (path) => {
return path.replace(/^/DreamOne/, '')
},
}
}
},
build: {
outDir: fileName, // 打包后文件包名称
sourcemap: false,//控制是否生成源映射文件
target: ['ios11', 'es2015'],//指定目标浏览器和 JavaScript 版本。解释:这里使用了 ios11 和 es2015,表示你希望构建的代码在 iOS 11 及更高版本的浏览器中运行,并且使用 ES2015 的语法。这有助于优化构建输出,使其更适应目标环境。
rollupOptions: {
// 使用了模板字符串和 ${Timestamp} 变量,这是为了在文件名中添加构建时的时间戳,以避免浏览器缓存问题。这样每次构建都会生成带有新时间戳的文件名,确保文件更新后不受缓存影响
output: {
chunkFileNames: `static/js/[name].[hash]${Timestamp}.js`,//配置代码拆分后的 chunk 文件名规则。
entryFileNames: `static/js/[name].[hash]${Timestamp}.js`,//配置入口文件的输出文件名规则。
assetFileNames: `static/[ext]/[name].[hash]${Timestamp}.[ext]`,//配置静态资源文件的输出文件名规则。
}
}
},
// 配置 CSS
css: {
postcss: {
plugins: [
// 处理 @import 和 url() 路径
postcssImport(),
postcssUrl(),
// 自动添加浏览器前缀
autoprefixer({
overrideBrowserslist: [
'last 5 version',
'>1%',
'ie >=8'
]
}),
// px 转 rem(核心适配插件)
pxtorem({
rootValue: 192, // 1920设计稿除以10
minPixelValue: 1, // 最小转换值,1px及以上转换
unitPrecision: 6, // 转换后的小数位数
propList: ['*'], // 所有属性都转换 大写PX不会被转换
selectorBlackList: ['aaa-'], // 匹配不被转换的选择器
replace: true, // 直接替换而不是添加备用
mediaQuery: false, // 媒体查询中的px不转换
exclude: /node_modules/i // 排除 node_modules
})
]
}
},
}
})
c.配置打包命令以及打包
“build:prod”: “vite build –mode prod”,
这里就会根据env.prod配置文件进行打包

注意: 在React 18中, StrictMode 会在开发环境下对组件进行两次渲染以检测潜在问题,这导致 useEffect 钩子被执行两次,从而产生了两次网络请求。这种行为是React 18引入的新特性,只在开发环境下发生,生产环境不会有。
五、实现 Ant Design 的按需加载
Ant Design 5.x或者以上默认支持Tree Shaking,默认支持按需加载
这里需要定制主题或者样式兼容,你可以使用 取消默认的降权操作(请注意版本保持与 antd 一致)
@ant-design/cssinjs
在 Vite + React + Ant Design 中使用 CSS-in-JS(ant5.x.x及以上)
https://ant.design/docs/react/compatible-style-cn
npm install antd @ant-design/icons
# CSS-in-JS 相关
npm install @ant-design/cssinjs
//main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { StyleProvider } from '@ant-design/cssinjs';
import {ConfigProvider} from "antd";
import 'amfe-flexible'; // 引入自适应方案
import './index.css'
import '@/assets/css/base.css'
import App from './App.jsx'
// Redux
import { Provider } from 'react-redux'
import { store } from './store/index.js'
// 1. 恢复StrictMode :重新启用了React的严格模式,这有助于发现潜在问题
// 2. 移除重复代码 :删除了注释掉的重复 <App /> 组件
// 3. 优化缩进 :调整了代码缩进,使组件层级结构更清晰
// 当前的配置结构是正确的,遵循了Ant Design 6.x的最佳实践:
// - 使用 StyleProvider 来自定义样式优先级和哈希策略
// - 使用 ConfigProvider 来全局配置Ant Design组件
// - 使用 Provider 来提供Redux store
createRoot(document.getElementById('root')).render(
<StrictMode>
<StyleProvider hashPriority="high">
<ConfigProvider>
<Provider store={store}>
<App />
</Provider>
</ConfigProvider>
</StyleProvider>
</StrictMode>
)
修改ant组件样式使用:global()修饰符来确保样式能够正确应用到Ant组件内部的类名上,因为在CSS Modules中默认样式是作用域化

六、Vite旧版浏览器兼容插件指南: @vitejs/plugin-legacy
这是一个 Vite 插件,用于为旧版本的浏览器提供兼容性支持。它的主要作用是将现代 JavaScript 代码(例如,ES6+、async/await、模块化等)转换成较旧版本浏览器(如 IE11 或早期版本的 Safari、Chrome)能够理解和运行的代码。该插件的工作原理基于 Babel 和 Polyfill。
注意:
– 1. @vitejs/plugin-legacy 对应vite版本要求
在项目中package.json中查看 vite版本,一般只需要找对应的大版本就行;【eg: vite ^7 ==> @vitejs/plugin-legacy ^7】
-2. 使用 @vitejs/plugin-legacy 打包后,包体积增大
插件会自动为旧版本浏览器引入一些 Polyfill(例如 core-js 和 regenerator-runtime)。这些 Polyfill 是为了确保现代 JavaScript 特性能够在旧浏览器中正常工作。由于 Polyfill 涉及到许多额外的库和代码,它会增加最终打包文件的体积。
a.安装依赖
– 查看可用版本
npm view @vitejs/plugin-legacy versions –json
– 下载对应的版本xx
npm i @vitejs/plugin-legacy@x.x.x -D
– 必须安装 Terser,因为 plugin-legacy 使用 Terser 进行缩小
npm add -D terser
b.项目配置
//vite.config.js
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import postcssImport from 'postcss-import';
import postcssUrl from 'postcss-url';
import autoprefixer from 'autoprefixer';
import pxtorem from 'postcss-pxtorem';
import legacy from '@vitejs/plugin-legacy'
const Timestamp = new Date().getTime();//随机时间戳
// https://vite.dev/config/
export default defineConfig(({mode})=>{
const env = loadEnv(mode, process.cwd());
console.log(env);
let fileName = 'dist'
// 兼容性,以防打包崩溃
fileName = env.VITE_APP_NAME
return {
base: '',//项目名称开发或生产环境服务的公共基础路径。
plugins: [react(),
legacy({
// 设置需要兼容的目标浏览器版本
targets: ['ie >= 11', '> 1%', 'last 2 versions','chrome > 68'],
// 为传统包提供 polyfills
polyfills: ['es.promise.finally', 'es/map', 'es/set'],
modernPolyfills: false,
renderLegacyChunks: true,
// 在使用协议本地运行项目时file:,这也很有帮助,因为加载现代代码块type="module"可能会触发 CORS 限制。
// 为了避免此问题,只需设置renderModernChunks为false仅使用旧代码块即可
renderModernChunks: false,
// 如果你使用 `regenerator-runtime` (例如用了 async/await), 需要启用它
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
})
],
resolve:{
alias:{
'@': path.resolve(__dirname, './src')
}
},
server:{
proxy: {
'/DreamOne': {
target: 'http://10.3.4.174:8080/DreamOne/',
changeOrigin: true,
rewrite: (path) => {
return path.replace(/^/DreamOne/, '')
},
}
}
},
build: {
outDir: fileName, // 打包后文件包名称
sourcemap: false,//控制是否生成源映射文件
target: ['ios11', 'es2015'],//指定目标浏览器和 JavaScript 版本。解释:这里使用了 ios11 和 es2015,表示你希望构建的代码在 iOS 11 及更高版本的浏览器中运行,并且使用 ES2015 的语法。这有助于优化构建输出,使其更适应目标环境。
rollupOptions: {
// 使用了模板字符串和 ${Timestamp} 变量,这是为了在文件名中添加构建时的时间戳,以避免浏览器缓存问题。这样每次构建都会生成带有新时间戳的文件名,确保文件更新后不受缓存影响
output: {
chunkFileNames: `static/js/[name].[hash]${Timestamp}.js`,//配置代码拆分后的 chunk 文件名规则。
entryFileNames: `static/js/[name].[hash]${Timestamp}.js`,//配置入口文件的输出文件名规则。
assetFileNames: `static/[ext]/[name].[hash]${Timestamp}.[ext]`,//配置静态资源文件的输出文件名规则。
}
}
},
// 配置 CSS
css: {
postcss: {
plugins: [
// 处理 @import 和 url() 路径
postcssImport(),
postcssUrl(),
// 自动添加浏览器前缀
autoprefixer({
overrideBrowserslist: [
'last 5 version',
'>1%',
'ie >=8'
]
}),
// px 转 rem(核心适配插件)
pxtorem({
rootValue: 192, // 1920设计稿除以10
minPixelValue: 1, // 最小转换值,1px及以上转换
unitPrecision: 6, // 转换后的小数位数
propList: ['*'], // 所有属性都转换 大写PX不会被转换
selectorBlackList: ['aaa-'], // 匹配不被转换的选择器
replace: true, // 直接替换而不是添加备用
mediaQuery: false, // 媒体查询中的px不转换
exclude: /node_modules/i // 排除 node_modules
})
]
}
},
// 设置构建目标,legacy 插件会为传统浏览器生成相应的包
target: 'es2015', // 或 ‘es5’
minify: true, // 代码压缩
}
})
这里有优化压缩体积的方式:https://www.jb51.net/article/271663.htm
vite官方文档:开始 {#getting-started} | Vite中文网 (vitejs.cn)
redux安装配置:Redux 基础,第二节:应用的结构 | Redux 中文官网
一个统计前端技术走向的网站:2021 JavaScript Rising Stars
二、页面实现适配




