webpack-reactcli配置

283 阅读5分钟

一、reactcli开发环境配置

效果:

动图.gif

1、目录结构

image.png

2、开始搭建

npm init -y生成package.json:

{
  "name": "react-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "npm run dev",
    "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "browserslist": [
    "last 2 version",
    "> 1%",
    "not dead"
  ],
  "devDependencies": {
    "@babel/core": "^7.17.10",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
    "babel-loader": "^8.2.5",
    "babel-preset-react-app": "^10.0.1",
    "cross-env": "^7.0.3",
    "css-loader": "^6.7.1",
    "eslint": "^8.41.0",
    "eslint-config-react-app": "^7.0.1",
    "eslint-webpack-plugin": "^3.1.1",
    "html-webpack-plugin": "^5.5.0",
    "less": "^4.1.3",
    "less-loader": "^10.2.0",
    "postcss-loader": "^6.2.1",
    "postcss-preset-env": "^7.5.0",
    "react-refresh": "^0.13.0",
    "sass": "^1.51.1",
    "sass-loader": "^12.6.0",
    "style-loader": "^3.3.1",
    "webpack": "^5.72.0",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.9.0"
  },
  "dependencies": {
    "react": "^18.1.0",
    "react-dom": "^18.1.0",
    "react-router-dom": "^6.3.0"
  }
}

babel.config.js

module.exports = {
  presets: ['react-app'] // 使用react官方预设,内部集成了babel-preset-react-app
};

.eslintrc.js

module.exports = {
  extends: ["react-app"], // 继承 react 官方规则
  parserOptions: {
    babelOptions: {
      presets: [
        // 解决页面报错问题
        ["babel-preset-react-app", false],
        "babel-preset-react-app/prod",
      ],
    },
  },
};

About/index.jsx

import React from 'react';

const About = () => {
  return (
    <h2>
      about<p className="p">段落2</p>
    </h2>
  );
};

export default About;

Home/index.jsx

import React from 'react';
import styles from './index.module.less'

const Home = () => {
  return (
    <h2 className={styles.title}>
      home
      <p className={styles.p}>段落23</p>
    </h2>
  );
};

export default Home;

Home/index.module.less

.title {
  color: aqua;
}
.p {
  color: yellow;
}

App.js

import React, { Suspense, lazy } from 'react';
import { Link, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import(/* webpackChunkName: 'Home' */ './pages/Home'));
const About = lazy(() =>
  import(/* webpackChunkName: 'About' */ './pages/About')
);

const App = () => {
  return (
    <div>
      <h1>App</h1>
      <ul>
        <li>
          <Link to="/home">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>
      <Suspense fallback={<div>loading...</div>}>
        <Routes>
          <Route path="/home" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </div>
  );
};

export default App;

main.js

import React from 'react';
import ReactDom from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const root = ReactDom.createRoot(document.querySelector('#app'));

root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

public/index.html

<!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" />
    <title>react 脚手架</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

webpack.dev.js

const path = require('path');
const EslintWebpackPlugin = require('eslint-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

// 返回loader数组
const getStyleLoaders = loaderName => {
  const loaders = [
    'style-loader',
    'css-loader',
    /*
      处理css兼容性问题,需要在package.json中设置browserslist属性
        "browserslist": [
          "last 2 version",
          "> 1%",
          "not dead"
        ]
    */
    {
      loader: 'postcss-loader',
      options: { postcssOptions: { plugins: ['postcss-preset-env'] } }
    },
    loaderName // undefined/less-loader/sass-loader/...
  ];
  return loaders.filter(Boolean);
};

module.exports = {
  entry: './src/main.js',
  output: {
    path: undefined,
    filename: 'static/js/[name].js',
    chunkFilename: 'static/js/[name].chunk.js',
    assetModuleFilename: 'static/media/[hash:10][ext][query]'
  },
  module: {
    rules: [
      {
        oneOf: [
          { test: /\.css$/, use: getStyleLoaders() },
          { test: /\.less$/, use: getStyleLoaders('less-loader') },
          { test: /\.s[ac]ss$/, use: getStyleLoaders('sass-loader') },
          {
            test: /\.(jpe?g|png|gif|webp|svg)$/,
            type: 'asset',
            parser: { dataUrlCondition: { maxSize: 10 * 1024 } } // 小于10kb的图片会被处理成base64
          },
          { test: /\.(woff2?|ttf)$/, type: 'asset/resource' },
          {
            test: /\.jsx?$/,
            include: path.resolve(__dirname, '../src'),
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 开启babel缓存
              cacheCompression: false, // 关闭缓存文件压缩
              plugins: ['react-refresh/babel'] // 激活js的hmr
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new EslintWebpackPlugin({
      context: path.resolve(__dirname, '../src'),
      exclude: 'node_modules',
      cache: true,
      cacheLocation: path.resolve(
        __dirname,
        '../node_modules/.cache/.eslintcache'
      )
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html')
    }),
    new ReactRefreshWebpackPlugin() // 激活js的hmr
  ],
  mode: 'development',
  devtool: 'cheap-module-source-map',
  optimization: {
    splitChunks: { chunks: 'all' }, // 代码分割
    runtimeChunk: { name: entrypoint => `runtime~${entrypoint.name}.js` } // 提取runtime文件
  },
  resolve: { extensions: ['.jsx', '.js', '.json'] }, // 自动补全文件扩展名
  devServer: {
    host: 'localhost',
    port: 3005,
    // open: true,
    // hot:true, // 默认已经开启
    historyApiFallback: true // 解决react路由刷新404问题
  }
};

二、reactcli生产环境配置

安装包(node版本:16.14.0)

cnpm i mini-css-extract-plugin css-minimizer-webpack-plugin -D // 提取css、css压缩
cnpm i image-minimizer-webpack-plugin imagemin imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D // 图片无损压缩
cnpm i copy-webpack-plugin -D // 将plugin目录下的资源拷贝到dist目录中

添加指令

    "prod": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js"

public

    <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />

webpack.prod.js

const path = require('path');
const EslintWebpackPlugin = require('eslint-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 提取css生成单独的css文件
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin'); // css压缩
const TerserWebpackPlugin = require('terser-webpack-plugin'); // js压缩
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin'); // 图片压缩
const CopyPlugin = require('copy-webpack-plugin'); // 将plugin目录下的资源拷贝到dist目录中
const { loader: miniCssLoader } = MiniCssExtractPlugin;

// 返回loader数组
const getStyleLoaders = loaderName => {
  const loaders = [
    miniCssLoader,
    'css-loader',
    /*
      处理css兼容性问题,需要在package.json中设置browserslist属性
        "browserslist": [
          "last 2 version",
          "> 1%",
          "not dead"
        ]
    */
    {
      loader: 'postcss-loader',
      options: { postcssOptions: { plugins: ['postcss-preset-env'] } }
    },
    loaderName // undefined/less-loader/sass-loader/...
  ];
  return loaders.filter(Boolean);
};

module.exports = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'static/js/[name].[contenthash:8].js',
    chunkFilename: 'static/js/[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'static/media/[hash:10][ext][query]',
    clean: true // 清除上一次的dist
  },
  module: {
    rules: [
      {
        oneOf: [
          { test: /\.css$/, use: getStyleLoaders() },
          { test: /\.less$/, use: getStyleLoaders('less-loader') },
          { test: /\.s[ac]ss$/, use: getStyleLoaders('sass-loader') },
          {
            test: /\.(jpe?g|png|gif|webp|svg)$/,
            type: 'asset',
            parser: { dataUrlCondition: { maxSize: 10 * 1024 } } // 小于10kb的图片会被处理成base64
          },
          { test: /\.(woff2?|ttf)$/, type: 'asset/resource' },
          {
            test: /\.jsx?$/,
            include: path.resolve(__dirname, '../src'),
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 开启babel缓存
              cacheCompression: false // 关闭缓存文件压缩
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new EslintWebpackPlugin({
      context: path.resolve(__dirname, '../src'),
      exclude: 'node_modules',
      cache: true,
      cacheLocation: path.resolve(
        __dirname,
        '../node_modules/.cache/.eslintcache'
      )
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html')
    }),
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css',
      chunkFilename: 'static/css/[name].[contenthash:8].chunk.css'
    }),
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, '../public'),
          to: path.resolve(__dirname, '../dist'),
          globOptions: { ignore: ['**/index.html'] } // 忽略index.html文件
        }
      ]
    })
  ],
  mode: 'production',
  devtool: 'source-map', // 包含行/列映射,打包速度慢
  optimization: {
    splitChunks: { chunks: 'all' }, // 代码分割
    runtimeChunk: { name: entrypoint => `runtime~${entrypoint.name}.js` }, // 提取runtime文件
    minimizer: [
      new CssMinimizerWebpackPlugin(),
      new TerserWebpackPlugin(),
      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' }
                    }
                  ]
                }
              ]
            ]
          }
        }
      })
    ]
  },
  resolve: { extensions: ['.jsx', '.js', '.json'] } // 自动补全文件扩展名
};

执行npm run prod,输出dist目录,如果直接打开,此时还看不到ico,可以使用serve dist或者anywhere -d dist

三、合并配置

webpack.config.js

const path = require('path');
const EslintWebpackPlugin = require('eslint-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 提取css生成单独的css文件
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin'); // css压缩
const TerserWebpackPlugin = require('terser-webpack-plugin'); // js压缩
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin'); // 图片压缩
const CopyPlugin = require('copy-webpack-plugin'); // 将plugin目录下的资源拷贝到dist目录中
const { loader: miniCssLoader } = MiniCssExtractPlugin;
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

// 通过cross-env获取当前环境变量
const isProduction = process.env.NODE_ENV === 'production';

// 返回loader数组
const getStyleLoaders = loaderName => {
  const loaders = [
    isProduction ? miniCssLoader : 'style-loader',
    'css-loader',
    /*
      处理css兼容性问题,需要在package.json中设置browserslist属性
        "browserslist": [
          "last 2 version",
          "> 1%",
          "not dead"
        ]
    */
    {
      loader: 'postcss-loader',
      options: { postcssOptions: { plugins: ['postcss-preset-env'] } }
    },
    loaderName // undefined/less-loader/sass-loader/...
  ];
  return loaders.filter(Boolean);
};

module.exports = {
  entry: './src/main.js',
  output: {
    path: isProduction ? path.resolve(__dirname, '../dist') : undefined,
    filename: isProduction ? 'static/js/[name].[contenthash:8].js' : 'static/js/[name].js',
    chunkFilename: isProduction ? 'static/js/[name].[contenthash:8].chunk.js' : 'static/js/[name].chunk.js',
    assetModuleFilename: 'static/media/[hash:10][ext][query]',
    clean: true // 清除上一次的dist
  },
  module: {
    rules: [
      {
        oneOf: [
          { test: /\.css$/, use: getStyleLoaders() },
          { test: /\.less$/, use: getStyleLoaders('less-loader') },
          { test: /\.s[ac]ss$/, use: getStyleLoaders('sass-loader') },
          {
            test: /\.(jpe?g|png|gif|webp|svg)$/,
            type: 'asset',
            parser: { dataUrlCondition: { maxSize: 10 * 1024 } } // 小于10kb的图片会被处理成base64
          },
          { test: /\.(woff2?|ttf)$/, type: 'asset/resource' },
          {
            test: /\.jsx?$/,
            include: path.resolve(__dirname, '../src'),
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 开启babel缓存
              cacheCompression: false, // 关闭缓存文件压缩
              plugins: [!isProduction && 'react-refresh/babel'].filter(Boolean) // 开发环境需要激活js的hmr
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new EslintWebpackPlugin({
      context: path.resolve(__dirname, '../src'),
      exclude: 'node_modules',
      cache: true,
      cacheLocation: path.resolve(
        __dirname,
        '../node_modules/.cache/.eslintcache'
      )
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html')
    }),
    isProduction && new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css',
      chunkFilename: 'static/css/[name].[contenthash:8].chunk.css'
    }),
    isProduction && new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, '../public'),
          to: path.resolve(__dirname, '../dist'),
          globOptions: { ignore: ['**/index.html'] } // 忽略index.html文件
        }
      ]
    }),
    !isProduction && new ReactRefreshWebpackPlugin() // 开发环境需要激活js的hmr
  ].filter(Boolean),
  mode: isProduction ? 'production' : 'development',
  devtool: isProduction ? 'source-map' : 'cheap-module-source-map', // 包含行/列映射,打包速度慢
  optimization: {
    splitChunks: { chunks: 'all' }, // 代码分割
    runtimeChunk: { name: entrypoint => `runtime~${entrypoint.name}.js` }, // 提取runtime文件
    minimize: isProduction, // 控制minimizer的配置是否有效
    minimizer: [
      new CssMinimizerWebpackPlugin(),
      new TerserWebpackPlugin(),
      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' }
                    }
                  ]
                }
              ]
            ]
          }
        }
      })
    ]
  },
  resolve: { extensions: ['.jsx', '.js', '.json'] }, // 自动补全文件扩展名
  // 运行指令时需要添加serve才能用到devServer
  devServer: {
    host: 'localhost',
    port: 3005,
    // open: true,
    // hot:true, // 默认已经开启
    historyApiFallback: true // 解决react路由刷新404问题
  }
};

package.json

    "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.config.js",
    "prod": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js"

四、优化配置

1、antd定制主题色

安装antd

cnpm i antd@4.20.3

main.js

import 'antd/dist/antd.css';

App.jsx

import { Button } from 'antd';



      <Button type="primary">按钮</Button>

此时可以看到页面中一个蓝色的按钮

webpack.config.js判断是否为less-loader,如果是,加上less配置

    loaderName && {
      loader: loaderName,
      options:
        loaderName === 'less-loader'
          ? {
              lessOptions: {
                modifyVars: { '@primary-color': '#1DA57A' },
                javascriptEnabled: true
              }
            }
          : {}

main.js需要将css改为less

import 'antd/dist/antd.less';

重启,便可以看到按钮变成绿色的了

2、打包优化

目前node_modules会打包成一个文件,造成某个文件的体积过大,会导致加载偏慢

image.png 我们给代码分割做一些配置:

webpack.config.js

    splitChunks: {
      chunks: 'all',
      // 将node_modules中比较大的几个模块单独打包
      cacheGroups: {
        // layouts通常是admin项目的主体布局组件,所有路由组件都要使用的
        // 可以单独打包,从而复用
        // 如果项目中没有,请删除
        layouts: {
          name: 'layouts',
          test: path.resolve(__dirname, '../src/layouts'),
          priority: 40
        },
        antd: {
          test: /[\\/]node_modules[\\/]antd(.*)/,
          name: 'chunk-antd',
          priority: 30
        },
        react: {
          test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
          name: 'chunk-react',
          priority: 20,
          chunks: 'initial'
        },
        libs: {
          test: /[\\/]node_modules[\\/]/,
          name: 'chunk-libs',
          priority: 10, // 权重最小,优先级低
          chunks: 'initial'
        }
      }
    }
    
    
  ...
    
  performance: false // 关闭性能分析,提示速度