最近学习了 Webpack 5 之后,想自己搭建个项目练练手,于是就搭建了一个基于 Webpack 5 的 React 的脚手架。
脚手架配置了相关模块,集成了常用功能,便于自己以后 React 新项目的搭建,开箱即用!
仓库地址:「Github」
React
react-router-dom
Typescript
redux
less
、sass
eslint
git commit
前规范检测commitlint
dayjs
antd
,配置了样式按需引入、自定义主题react hooks
库ahooks
项目的整体目录结构如下所示,其中为了测试可用性,添加了一些简单的组件和页面,可自行更改。
│ .babelrc // Babel配置
│ .commitlintrc.js // commitlint配置
│ .eslintrc.js // eslint配置
│ .gitignore // git忽略文件列表
│ package.json
│ README.md
│ tsconfig.json // typescript配置
│ yarn.lock
│
├─public // 全局文件
│ │ index.html // 模板
│ │
│ └─assets // 不需要动态导入的资源
│ index.css
│ index.jpg
│ index.js
│
├─scripts // 脚本
│ │ antd-theme.js // antd自定义主题配置
│ │ constant.js // webpack相关的常量
│ │ env.js // 环境变量
│ │
│ └─config
│ webpack.common.js // 开发环境+生产环境的公共配置
│ webpack.dev.js // 开发环境webpack配置
│ webpack.prod.js // 生产环境webpack配置
│
└─src
│ App.scss
│ App.tsx
│ index.tsx // 入口文件
│
├─components // 组件
│ └─ErrorBoundary // 错误边界
│ index.tsx
│
├─pages // 页面(写了一些页面测试)
│ ├─Admin
│ │ index.tsx
│ │
│ └─Home
│ index.tsx
│
├─redux // redux相关
│ │ actions.ts
│ │ constant.ts
│ │ interface.ts
│ │ store.ts
│ │
│ └─reducers
│ count.ts
│ index.ts
│
└─types // 模块声明
asset.d.ts
style.d.ts
主要看scripts
下的内容。还配置了git husky
,用于在提交commit
前自动检测commit
规范性。
{
"name": "my-react",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "cross-env NODE_ENV=development webpack-dev-server --config ./scripts/config/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./scripts/config/webpack.prod.js"
},
"dependencies": {
// ...
},
"browserslist": [
">0.2%",
"not dead",
"ie >= 9",
"not op_mini all"
],
"husky": {
"hooks": {
"commit-msg": "commitlint --config .commitlintrc.js -e"
}
}
}
导出环境变量。
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
isDevelopment,
isProduction,
};
导出根路径、HOST、POST。
const path = require('path');
const ROOT_PATH = path.resolve(__dirname, '../');
const SERVER_HOST = 'localhost';
const SERVER_PORT = 8080;
module.exports = {
ROOT_PATH,
SERVER_HOST,
SERVER_PORT,
};
const path = require('path');
const WebpackBar = require('webpackbar');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
const { ROOT_PATH } = require('../constant');
const { isDevelopment, isProduction } = require('../env');
const { myAntd } = require('../antd-theme');
const getCssLoaders = () => {
const cssLoaders = [
// 开发模式使用style-loader,生产模式MiniCssExtractPlugin.loader
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
// 模块化类名,防止重复
localIdentName: '[local]--[hash:base64:5]',
},
sourceMap: isDevelopment,
},
},
];
// 加css前缀的loader配置
const postcssLoader = {
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
isProduction && [
'postcss-preset-env',
{
autoprefixer: {
grid: true,
},
},
],
],
},
},
};
// 生产模式时,才需要加css前缀
isProduction && cssLoaders.push(postcssLoader);
return cssLoaders;
};
const getAntdLessLoaders = () => [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: isDevelopment,
},
},
{
loader: 'less-loader',
options: {
sourceMap: isDevelopment,
lessOptions: {
// antd 自定义主题
modifyVars: myAntd,
javascriptEnabled: true,
},
},
},
];
module.exports = {
entry: {
index: path.resolve(ROOT_PATH, './src/index'),
},
plugins: [
// html模板
new HtmlWebpackPlugin({
template: path.resolve(ROOT_PATH, './public/index.html'),
filename: 'index.html',
inject: 'body',
}),
// 打包显示进度条
new WebpackBar(),
// webpack打包不会有类型检查,强制ts类型检查
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.resolve(ROOT_PATH, './tsconfig.json'),
},
}),
// 复制不用动态导入的资源
new CopyWebpackPlugin({
patterns: [
{
context: 'public',
from: 'assets/*',
to: path.resolve(ROOT_PATH, './build'),
toType: 'dir',
globOptions: {
dot: true,
gitignore: true,
ignore: ['**/index.html'], // **表示任意目录下
},
},
],
}),
// 自动删除上一次打包的产物
new CleanWebpackPlugin(),
// 将antd中的moment.js替换为day.js
new AntdDayjsWebpackPlugin(),
],
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: getCssLoaders(),
},
{
test: /\.less$/,
exclude: /node_modules/,
use: [
...getCssLoaders(),
{
loader: 'less-loader',
options: {
sourceMap: isDevelopment,
},
},
],
},
{
test: /\.less$/,
exclude: /src/,
use: getAntdLessLoaders(),
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
...getCssLoaders(),
{
loader: 'sass-loader',
options: {
sourceMap: isDevelopment,
},
},
],
},
{
test: /\.(tsx?|js)$/, // ts\tsx\js
loader: 'babel-loader',
options: { cacheDirectory: true }, // 缓存公共文件
exclude: /node_modules/,
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
// 自动选择导出为单独文件还是url形式
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024,
},
},
},
{
test: /\.(eot|svg|ttf|woff|woff2?)$/,
// 分割为单独文件,并导出url
type: 'asset/resource',
},
],
},
// 路径配置别名
resolve: {
alias: {
'@': path.resolve(ROOT_PATH, './src'),
},
// 若没有写后缀时,依次从数组中查找相应后缀文件是否存在
extensions: ['.tsx', '.ts', '.js', '.json'],
},
// 缓存
cache: {
// 基于文件系统的持久化缓存
type: 'filesystem',
buildDependencies: {
// 当配置文件发生变化时,缓存失效
config: [__filename],
},
},
};
const path = require('path');
const { merge } = require('webpack-merge');
const webpack = require('webpack');
const common = require('./webpack.common');
const { ROOT_PATH, SERVER_HOST, SERVER_PORT } = require('../constant');
module.exports = merge(common, {
target: 'web', // 解决热更新失效
mode: 'development',
devtool: 'eval-cheap-module-source-map',
output: {
path: path.resolve(ROOT_PATH, './build'),
filename: 'js/[name].js',
},
devServer: {
host: SERVER_HOST,
port: SERVER_PORT,
compress: true, // gzip压缩
open: true, // 自动打开默认浏览器
hot: true, // 启用服务热替换配置
client: {
logging: 'warn', // warn以上的信息,才会打印
overlay: true, // 当出现编译错误或警告时,在浏览器中显示全屏覆盖
},
// 解决路由跳转404问题
historyApiFallback: true,
},
plugins: [
// 引入热替换
new webpack.HotModuleReplacementPlugin(),
],
optimization: {
minimize: false,
minimizer: [],
// 代码分割
splitChunks: {
chunks: 'all',
minSize: 0,
},
},
});
const path = require('path');
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const common = require('./webpack.common');
const { ROOT_PATH } = require('../constant');
module.exports = merge(common, {
target: 'browserslist',
mode: 'production',
devtool: false,
output: {
path: path.resolve(ROOT_PATH, './build'),
filename: 'js/[name].[contenthash:8].js',
// 资源
assetModuleFilename: 'assets/[name].[contenthash:8].[ext]',
},
plugins: [
// 生产模式使用了MiniCssExtractPlugin.loader,则需要使用MiniCssExtractPlugin
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css',
}),
// 查看打包体积大小,启用一个本地服务器
new BundleAnalyzerPlugin(),
],
// 专门存放优化打包的配置
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin(),
// JS压缩
new TerserPlugin({
extractComments: false, // 去除所有注释
terserOptions: {
compress: { pure_funcs: ['console.log'] }, // 去除所有console.log函数
},
}),
],
// 代码分割
splitChunks: {
chunks: 'all',
minSize: 0,
},
},
});
在webpack.dev.js
添加一项,任何请求都会返回index.html
文件,解决单页面应用的路由跳转问题。
devServer: {
// ...
historyApiFallback: true,
}
先全局安装node-gyp
:
npm install -g node-gyp
再到项目根目录下,yarn
继续安装即可。
安装babel-plugin-import
,在.babelrc
文件的plugins
下,添加一项:
{
"plugins": [
["import", {
"libraryName": "antd",
"libraryDirectory": "es",
"style": true // `style: true` 会加载 less 文件
}]
]
}
正常使用即可,无需再引入样式:
import React from 'react';
import { Button } from 'antd';
import { useTitle } from 'ahooks';
const Admin: React.FC = () => {
useTitle('Admin');
return <Button type='primary'>按钮</Button>;
};
export default Admin;
当css-loader
配置了模块化引入时,如下所示:
// ...
{
loader: 'css-loader',
options: {
modules: {
// 模块化类名,防止重复
localIdentName: '[local]--[hash:base64:5]',
},
sourceMap: isDevelopment,
},
}
// ...
发现 antd 的样式不显示了。原因是模块化也应用于node_modules
中的文件,把 antd 中引入的样式也作了模块化,但是引入的组件还是正常的类名,所以显示不出。
解决办法是,将自己写的业务代码与第三方库的代码配置分开,因为之前 antd 按需加载配置时,配置了"style": true
,加载less
,所以要单独配置下less
,只在业务代码中开启module
:
module.exports = {
// ...
module: {
rules: [
{
test: /\.less$/,
exclude: /node_modules/, // 排除第三方库代码
use: [
...getCssLoaders(), // 正常配置
{
loader: 'less-loader',
options: {
sourceMap: isDevelopment,
},
},
],
},
{
test: /\.less$/,
exclude: /src/, // 排除业务代码
use: getAntdLessLoaders(), // 不开启module
},
// ...
],
},
// ...
};
处理less
。注意排除业务代码,不开启module
:
// antd自定义主题配置
const myAntd = {
'primary-color': '#1DA57A',
'link-color': '#1DA57A',
'border-radius-base': '8px',
};
module.exports = {
myAntd,
};
const { myAntd } = require('../antd-theme');
//...
const getAntdLessLoaders = () => [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: isDevelopment,
},
},
{
loader: 'less-loader',
options: {
sourceMap: isDevelopment,
lessOptions: {
// antd 自定义主题
modifyVars: myAntd,
javascriptEnabled: true,
},
},
},
];
//...
{
test: /\.less$/,
exclude: /src/,
use: getAntdLessLoaders(),
}
//...
本文记录自己所学,若有不妥,欢迎批评指出~