手动搭建并配置react项目(webpack5)
介绍
不使用脚手架,利用webpack,手动搭建react项目框架
1、项目创建
创建目录 react_wepack
2、webpack+ react基础架构
2.1 配置 webpack.dev.js
基础配置说明可参考这篇文章
配置 loader、plugin、eslint【见webpack.dev.js】
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ESLintPlugin = require('eslint-webpack-plugin');
const { DefinePlugin } = require('webpack');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');// 用来处理获取的样式
function getStyleLoaders(pre) {return [MiniCssExtractPlugin.loader,"css-loader",{loader: "postcss-loader",options: {postcssOptions: {// plugins: [["autoprefixer"]],plugins: ['postcss-preset-env'],//能解决大多数兼容性问题},},}, pre].filter(Boolean);
}module.exports = {mode: "development", // 开发模式entry: path.resolve(__dirname, "../src/main.js"), // 入口文件 相对路径// web serverdevtool: 'cheap-module-source-map', //build: slow rebuild: fast// 在dist中不会输出devServer: {static: {directory: path.resolve(__dirname, "../dist"), // 打包后的文件路径 directory:目录},open: true, //自动打开浏览器compress: true, //启动gzip压缩port: 9000, // 端口号hot: true,// 提供HMR功能,只更新某个模块,没有替换整个项目},output: {clean: true, // 清理 /dist 文件夹// filename: "js/main.js", // 打包后的文件名称filename: "js/[name].[contenthash:8].js", // 打包后的文件名称path: undefined,// 打包输出的其他文件名称chunkFilename: "js/[name].[contenthash:8].chunk.js",// 图片 等字体通过type: asset处理资源命名方式assetModuleFilename: 'media/[name].[contenthash:8][ext][query]',},cache: {type: 'filesystem',allowCollectingMemory: true,idleTimeout: 60000,compression: 'gzip',},// 开发环境不用处理optimization: {minimize: true, // 强制启用压缩// 对代码进行分割splitChunks: {chunks: 'all',},// runtimeChunk: 'single',minimizer: [new CssMinimizerPlugin(),]},plugins: [new HtmlWebpackPlugin({// 模版:以public/index.html为模板生成打包后的index.htmltemplate: path.resolve(__dirname, "../public/index.html"),// BASE_URL: process.env.BASE_URL || '/'}),new ESLintPlugin({// 配置哪些目录需要检查context: path.resolve(__dirname, './src'),exclude: 'node_modules',// 不写 默认也有cache: true,// 开启缓存npmcacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache'),// threads,// 开启多进程打包eslintPath:'eslint',//指定传统}),new MiniCssExtractPlugin({filename: "css/[name].[contenthash:8].css"}),new DefinePlugin({// window.ENV = 'production'ENV: JSON.stringify('development'),BASE_URL: '"../"' // 定义全局变量BASE_URL}),new CssMinimizerPlugin(),],// loader 加载器module: {rules: [// 每个文件只有一个loader配置处理{oneOf: [{test: /\.(gif|png|jpe?g)$/i,type: "asset",parser: {dataUrlCondition: {// 小于10kb的图片转成base64,减少请求数量// 缺点:体积会大一点maxSize: 10 * 1024, // 小于10kb},},generator: {// 输出图片的名称filename: "imgs/[name].[contenthash:8][ext]",},},{test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件// 对文件原封不动的输出type: "asset/resource",// generator: {// filename: "media/[name].[contenthash:8][ext]",// },},{test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体type: "asset/resource",// generator: {// filename: "fonts/[name].[contenthash:8][ext]"// }},// 复杂场景用{test: /\.jsx?$/,exclude: /node_modules/,use: [{loader: 'babel-loader',options: {presets: [['@babel/preset-react', { runtime: 'automatic' }]],cacheDirectory: true, // 开启babel缓存cacheCompression: false,// 关闭缓存文件压缩plugins: ['@babel/plugin-transform-runtime'],},},]},{test: /\.css$/,// MiniCssExtractPlugin.loader 最终会将css提取到单独的文件use: getStyleLoaders(), // 从右向左解析原则// use: ["style-loader", "css-loader"]},{test: /\.less$/,use: getStyleLoaders("less-loader"), // 从右向左解析原则// use: ["style-loader", "css-loader", "less-loader"]},{test: /\.s[ac]ss$/,use: getStyleLoaders("sass-loader")},{test: /\.styl$/,use: getStyleLoaders("stylus-loader")},]}],},// webpack解析加载块加载选项resolve:{extensions: ['.jsx','.js','.json',],}
};
2.2 新建eslintrc.js文件
module.exports = {extends: ["react-app"], // 继承 react 官方规则parserOptions: {babelOptions: {presets: [// 解决页面报错问题["babel-preset-react-app", false],"babel-preset-react-app/prod",],},},
};
2.3 新建babel.config.js文件
module.exports = {persets:["react-app"]
}
2.4 创建 package.json 文件
npm init -y
2.5 创建 react main.js App.jsx
- main.js
import react from 'react';
import ReactDom from 'react-dom/client';
import App from './App';const root= ReactDom.createRoot(document.getElementById('app'));
root.render(<App/>);
- App.jsx
import React from 'react';
const App = ()=>{return(<div><h1>Hello World</h1></div>)
}
3、安装依赖
npm install webpack webpack-cli webpack-dev-server --D
npm install mini-css-extract-plugin html-webpack-plugin eslint-webpack-plugin --D
npm install style-loader css-loader less-loader sass sass-loader postcss-loader postcss-preset-env stylus-loader --D
npm install babel-loader @babel/core babel-preset-react-app --Dnpm install eslit eslint-config-react-app --D
npm install react react-dom --S
4、配置package.json webpack打包入口
"scripts": {"test": "npm run dev","dev": "webpack serve --config ./config/webpack.dev.js"},
5、public index.html 创建一个id为app节点,以便挂载react组件
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="shortcut icon" href="favicon.ico" type="image/x-icon"><title>react- Cli</title>
</head>
<body><div id="app"></div>
</body>
</html>
5、 报错处理
- 报错一 eslint 版本太高
[eslint] Couldn't find FlatESLint, you might need to set eslintPath to 'eslint/use-at-your-own-risk'
解决方案: 换成低版本的eslint
- 报错二 提示有未使用的变量
NODE_ENV
orBABEL_ENV
Usingbabel-preset-react-app
requires that you specifyNODE_ENV
orBABEL_ENV
解决方案:
- 安装cross-env
npm install cross-env --D
- 修改package.json中的dev命令,定义环境变量
"dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js"
- 报错三’./App’ 未加后缀,代码不认识
ERROR in ./src/main.js 4:0-24
Module not found: Error: Can’t resolve ‘./App’ in ‘E:\study_2025\react-webpack5\src’
解决方案:配置resolve,告诉webpack,先用.jsx解析,再用.js解析,再用.json解析
// webpack解析加载块加载选项resolve:{extensions: ['.jsx','.js','.json',],}
- 报错四:
Module not found: Error: Can’t resolve 'E:\study_2025\react-webpack5\public\index.html
检查了webpack的配置文件,发现没有配置html-webpack-plugin也是正确的
看视频发现没有这一步的配置
自己手动创建了public目录和index.html文件
掉了第五步: 5、创建public目录和 index.html文件, 创建一个id为app节点,以便挂载react组件
<body><div id="app"></div>
</body>
- 异常五:页面启动后无报错,页面显示空白
原因: 在高版本中import react from ‘react’ 不用写,打包会报错
解决更新 babel.config.js 或 webpack.config.js:
// babel.config.js
module.exports = {presets: [['@babel/preset-react',{runtime: 'automatic', // 启用自动 JSX 转换importSource: 'react', // 默认值,可改为 'preact' 等其他库},],],
};
或者
// webpack.config.js
module: {rules: [{test: /\.(js|jsx)$/,use: {loader: 'babel-loader',options: {presets: [['@babel/preset-react', { runtime: 'automatic' }]]}}}]
}
我用的下面这种
6、最终的目录结构
react-webpack5
├── public/ # 手动创建此文件夹
│ ├── index.html # 主 HTML 模板
│ ├── favicon.ico # 网站图标
│
├── src/ # 源码目录
│ ├── main.js # 入口文件
│ └── App.jsx # 应用根组件
└── config
└──── webpack.dev.js # Webpack 开发配置文件
7、webpack优化
配置热更新
npm install @pmmmwh/react-refresh-webpack-plugin --D
在webpack.dev.js中配置热更新
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
module.exports = {module:{devServer: {···hot: true,// 开启HMR},rules:[{test: /\.jsx?$/,...options: {...plugins: ["react-refresh/babel", // 激活js的HMR ,仅用于开发环境],},},]}
}plugins: [new ReactRefreshWebpackPlugin(), // 激活js的HMR
]
8、配置react路由
- 安装react-router-dom
npm install react-router-dom --S
- 在main.js中引入react-router-dom
import { BrowserRouter } from 'react-router-dom'const root = ReactDom.createRoot(document.getElementById("app"));
root.render(<BrowserRouter><App /></BrowserRouter>);
- 在pages文件夹下创建 About.jsx 、Home.jsx 文件
- 在App.jsx中配置路由跳转
import { Routes, Route, Link } from 'react-router-dom';
import About from './pages/About';
import Home from './pages/Home';
const App = () => {return (<div><ul><li><Link to="/home">首页</Link></li><li><Link to="/about">关于</Link></li></ul><Routes><Route path="/home" element={<Home/>} /><Route path="/about" element={<About/>} /></Routes></div>)
}
export default App;
- 路由强制刷新出现Cannot GET /about
解决方案:
module.exports = {devServer: {...port: 9000, // 端口号hot: true,// 提供HMR功能, 只更新某个模块,没有替换整个项目historyApiFallback: true, // 解决前端路由刷新404问题},
9、若想让文件单独打包,可配置路由懒加载
import React, { lazy, Suspense } from 'react';
import { Routes, Route, Link } from 'react-router-dom';
const LayAbout = lazy(() => import('./pages/About'));
const LayHome = lazy(() => import('./pages/Home'))
// const Home = lazy(() => import(/* webpackChunkName: 'home' */ "./pages/Home"));
// const About = lazy(() => import(/* webpackChunkName: 'about' */ "./pages/About"));const App = () => {return (<div><ul><li><Link to="/home">首页</Link></li><li><Link to="/about">关于</Link></li></ul><Suspense fallback={<div>页面正在加载中...</div>}><Routes ><Route path="/home" element={<LayHome />} /><Route path="/about" element={<LayAbout />} /></Routes></Suspense></div>)
}
export default App;
10、webpack.dev 配置文件完整
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ESLintPlugin = require('eslint-webpack-plugin');
const { DefinePlugin } = require('webpack');
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");// 用来处理获取的样式
function getStyleLoaders(pre) {return [MiniCssExtractPlugin.loader,"css-loader",{loader: "postcss-loader",options: {postcssOptions: {// plugins: [["autoprefixer"]],plugins: ['postcss-preset-env'],//能解决大多数兼容性问题},},}, pre].filter(Boolean);
}module.exports = {mode: "development", // 开发模式entry: path.resolve(__dirname, "../src/main.js"), // 入口文件 相对路径// web serverdevtool: 'cheap-module-source-map', //build: slow rebuild: fast// 在dist中不会输出devServer: {static: {directory: path.resolve(__dirname, "../dist"), // 打包后的文件路径 directory:目录},open: true, //自动打开浏览器compress: true, //启动gzip压缩port: 9000, // 端口号hot: true,// 提供HMR功能, 只更新某个模块,没有替换整个项目},output: {clean: true, // 清理 /dist 文件夹// filename: "js/main.js", // 打包后的文件名称filename: "js/[name].[contenthash:8].js", // 打包后的文件名称path: undefined,// 打包输出的其他文件名称chunkFilename: "js/[name].[contenthash:8].chunk.js",// 图片 等字体通过type: asset处理资源命名方式assetModuleFilename: 'media/[name].[contenthash:8][ext][query]',},cache: {type: 'filesystem',allowCollectingMemory: true,idleTimeout: 60000,compression: 'gzip',},// 开发环境不用处理optimization: {minimize: true, // 强制启用压缩// 对代码进行分割splitChunks: {chunks: 'all',},},plugins: [new HtmlWebpackPlugin({// 模版:以public/index.html为模板生成打包后的index.htmltemplate: path.resolve(__dirname, "../public/index.html"),// BASE_URL: process.env.BASE_URL || '/'}),new ESLintPlugin({// 配置哪些目录需要检查context: path.resolve(__dirname, './src'),exclude: 'node_modules',// 不写 默认也有cache: true,// 开启缓存npmcacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache'),// threads,// 开启多进程打包eslintPath: 'eslint',//指定传统}),new MiniCssExtractPlugin({filename: "css/[name].[contenthash:8].css"}),new DefinePlugin({// window.ENV = 'production'ENV: JSON.stringify('development'),BASE_URL: '"../"' // 定义全局变量BASE_URL}),new CssMinimizerPlugin(),new ReactRefreshWebpackPlugin(), // 激活js的HMR],// loader 加载器module: {rules: [// 每个文件只有一个loader配置处理{oneOf: [{test: /\.(gif|png|jpe?g)$/i,type: "asset",parser: {dataUrlCondition: {// 小于10kb的图片转成base64,减少请求数量// 缺点:体积会大一点maxSize: 10 * 1024, // 小于10kb},},generator: {// 输出图片的名称filename: "imgs/[name].[contenthash:8][ext]",},},{test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件// 对文件原封不动的输出type: "asset/resource",// generator: {// filename: "media/[name].[contenthash:8][ext]",// },},{test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体type: "asset/resource",// generator: {// filename: "fonts/[name].[contenthash:8][ext]"// }},// 复杂场景用{test: /\.jsx?$/,exclude: /node_modules/,use: [{loader: 'babel-loader',options: {presets: [['@babel/preset-react', { runtime: 'automatic' }]],cacheDirectory: true, // 开启babel缓存cacheCompression: false,// 关闭缓存文件压缩plugins: ['@babel/plugin-transform-runtime',"react-refresh/babel", // 激活js的HMR],},},]},{test: /\.css$/,// MiniCssExtractPlugin.loader 最终会将css提取到单独的文件use: getStyleLoaders(), // 从右向左解析原则// use: ["style-loader", "css-loader"]},{test: /\.less$/,use: getStyleLoaders("less-loader"), // 从右向左解析原则// use: ["style-loader", "css-loader", "less-loader"]},{test: /\.s[ac]ss$/,use: getStyleLoaders("sass-loader")},{test: /\.styl$/,use: getStyleLoaders("stylus-loader")},]}],},// webpack解析加载块加载选项resolve: {extensions: ['.jsx', '.js', '.json',],}
};
生产环境配置
- 复制一份webpack.dev.js文件,并改名为webpack.config.prod.js
- 修改项
mode: "production", // 生产模式devtool: "source-map",
- 添加插件css-minimizer-webpack-plugin、mini-css-extract-plugin
- css 压缩
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');optimization: {...minimizer: [new CssMinimizerPlugin(),]},
- js 压缩
const TerserWebpackPlugin = require("terser-webpack-plugin");...
optimization: {...minimizer: [new TerserWebpackPlugin(),]
},
-
移除HMR功能
- 移除devServer
- ReactRefreshWebpackPlugin移除
- plugins: [“react-refresh/babel”,] // 激活js的HMR
-
对图片进行压缩
- 安装依赖
npm i image-minimizer-webpack-plugin --Dnpm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo --D
包不好下
new ImageMinimizerPlugin({minimizer: {implementation: ImageMinimizerPlugin.imageminGenerate,options: {plugins: [["gifsicle", { interlaced: true }],["jpegtran", { progressive: true }],["optipng", { optimizationLevel: 5 }],["svgo",{plugins: ["preset-default","prefixIds",{name: "sortAttrs",params: {xmlnsOrder: "alphabetical",},},],},],],},},}),
- 将public目录下的静态资源复制到dist目录下
// const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");new CopyPlugin({patterns: [{from: path.resolve(__dirname, "../public"),to: path.resolve(__dirname, "../dist"),globOptions: {// 忽略index.html文件ignore: ["**/index.html"],},},],}),
webpack 公共
- 引入antd 三方组件
- 安装
npm install antd --S
import { Button } from 'antd';<Button type="primary">按钮</Button>
webpack 修改某个主题的颜色
- 页面
import { Button } from 'antd';<Button type="primary">按钮</Button>
- 在 webpack.prod.js 文件中添加以下代码
const getStyleLoaders = (pre) => {return [...pre && {loader: pre,options:pre === "less-loader"? {// antd自定义主题配置// 主题色文档:https://ant.design/docs/react/customize-theme-cn#Ant-Design-%E7%9A%84%E6%A0%B7%E5%BC%8F%E5%8F%98%E9%87%8FlessOptions: {modifyVars: { "@primary-color": "red" },javascriptEnabled: true,},}: {},},].filter(Boolean);
};
- webpack 打包时部分包比较大,这个时候可以使用 webpack-bundle-analyzer 来分析打包后的包大小,然后进行优化。
optimization: {splitChunks: {chunks: "all",cacheGroups: {// react react-dom react-router-dom 一起打包成一个js文件react: {test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,name: "chunk-react",priority: 40, // 权重最大},// antd 单独打包antd: {test: /[\\/]node_modules[\\/]antd[\\/]/,name: "chunk-antd",priority: 30,},// 剩下node_modules单独打包libs: {test: /[\\/]node_modules[\\/]/,name: "chunk-libs",priority: 20,},},},}
点击查看项目地址