# webpack 项目配置

# 1. 介绍

搭建:

  1. react 项目脚手架
  2. vue 项目脚手架

# 2. react 脚手架

# 2.1. 项目结构

react-proj/
  config/
    webpack.config.js
    webpack.dev.js
    webpack.prod.js
  dist/
  node_modules/
  public/
    favicon.ico
    index.html
  src/
    pages/
      about/
        index.jsx
        index.scss
      home/
        index.jsx
        index.less
    App.jsx
    main.js
  .browserslistrc
  .eslintrc.js
  babel.config.js
  package.json
  package-lock.json

# 2.2. 安装

# webpack cli dev-server
npm i -D webpack webpack-cli webpack-dev-server

## "webpack": "^5.81.0", "webpack-cli": "^5.0.2", "webpack-dev-server": "^4.13.3"


# css
npm i -D style-loader css-loader 
npm i -D less-loader 
npm i -D sass sass-loader
npm i -D stylus-loader

npm i -D postcss-loader postcss postcss-preset-env

npm i -D mini-css-extract-plugin
npm i -D css-minimizer-webpack-plugin

## "style-loader": "^3.3.2", "css-loader": "^6.7.3",
## "less-loader": "^11.1.0",
## "sass": "^1.62.1", "sass-loader": "^13.2.2",
## "stylus-loader": "^7.1.0",

## "postcss": "^8.4.23", "postcss-loader": "^7.3.0", "postcss-preset-env": "^8.3.2",

## "mini-css-extract-plugin": "^2.7.5",
## "css-minimizer-webpack-plugin": "^5.0.0",


# js
npm i -D eslint-webpack-plugin eslint-config-react-app
npm i -D babel-loader @babel/core babel-preset-react-app
npm i -D terser-webpack-plugin

## "eslint-webpack-plugin": "^4.0.1", "eslint-config-react-app": "^7.0.1",
## "babel-loader": "^9.1.2", "@babel/core": "^7.21.5", "babel-preset-react-app": "^10.0.1",
## "terser-webpack-plugin": "^5.3.7",

# img min
npm i -D image-minimizer-webpack-plugin imagemin
npm i -D imagemin-gifsicle imagemin-svgo
npm i -D imagemin-jpegtran imagemin-optipng 

## "image-minimizer-webpack-plugin": "^3.8.2", "imagemin": "^8.0.1",
## "imagemin-gifsicle": "^7.0.0", "imagemin-svgo": "^10.0.1",
## "imagemin-jpegtran": "^7.0.0", "imagemin-optipng": "^8.0.0",


# html
npm i -D html-webpack-plugin

## "html-webpack-plugin": "^5.5.1",


# HMR of react component
npm i -D @pmmmwh/react-refresh-webpack-plugin react-refresh

## "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "react-refresh": "^0.14.0",


# others
npm i -D cross-env
npm i -D copy-webpack-plugin

## "cross-env": "^7.0.3",
## "copy-webpack-plugin": "^11.0.0",


# react
npm i react react-dom react-router-dom

## "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.11.0"

# 2.3. 其他配置

# 2.3.1. .browserslistrc

last 2 version
> 1%
not dead

# 2.3.2. .eslintrc.js

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

# 2.3.3. babel.config.js

module.exports = {
  /*
    使用 react 官方规则,包含:
      @babel/preset-env
      corejs
      @babel/plugin-transform-runtime
   */
  presets: ['react-app'],
};

# 2.3.4. HMR of react component

配置:

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  devServer: {
    hot: true,
  },
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        use: [
          { 
            loader: 'babel-loader', 
            options: { plugins: ['react-refresh/babel'] }
          },
        ],
      },
    ],
  },
  plugins: [new ReactRefreshWebpackPlugin()],
}

参考:react-refresh-webpack-plugin (opens new window)

# 2.3.5. historyApiFallback

配置:

module.exports = {
  //...
  devServer: {
    /*
      When using the HTML5 History API, 
      the index.html page will likely have to be served in place of any 404 responses. 
      Enable devServer.historyApiFallback by setting it to true:
      
      boolean = false object
     */
    historyApiFallback: true,
  },
}

参考:

# 2.3.6. 拷贝 public 目录

配置:

const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
  // ...
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, '../public'),
          to: path.resolve(__dirname, '../dist'),
          globOptions: {
            ignore: [
              // 不拷贝 index.html, 避免与 HtmlWebpackPlugin 冲突
              '**/index.html',
            ]
          }
        },
      ],
    }),
  ]
};

参考:

# 2.4. 报错

# 2.4.1. NODE_ENV

错误信息:

Using `babel-preset-react-app` requires that you specify `NODE_ENV` or `BABEL_ENV` environment variables.

解决方案:

  1. 安装 cross-env

    npm i -D cross-env
    
  2. 在 npm scripts 中设置临时环境变量 NODE_ENV=development

    {
      "scripts": {
        "start": "npm run dev",
        "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js"
      }
    } 
    

# 2.4.2. 省略 .jsx 后缀不识别

错误信息:

Module not found: Error: Can't resolve './App' in 'D:\dev\GitHub\blog\codes\frontend\webpack\guide\react-proj\src'

解决方案:

  • 在 webpack.config.js 中设置

    module.exports = {
      //...
      resolve: {
        // default:  ['.js', '.json', '.wasm']
        extensions: ['.js', '.jsx', '.json', '.wasm'],
      },
    };
    

# 2.5. 开发配置

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');

const getStyleLoaders = (preProcessor) => {
  return [
    'style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env',
          ],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
  mode: 'development',
  devtool: 'cheap-module-source-map',
  devServer: {
    host: 'localhost',
    port: '3000',
    open: true,
    hot: true,
    historyApiFallback: true,
  },

  entry: path.resolve(__dirname, '../src/main.js'),

  output: {
    path: path.resolve('../dist'),
    filename: 'static/js/[name].js',
    chunkFilename: 'static/js/[name].chunk.js',
    assetModuleFilename: 'static/media/[name].[hash][ext][query]',
  },

  resolve: {
    extensions: ['.js', '.jsx', '.json', '.wasm'],
  },

  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.jsx?$/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                  cacheCompression: false,
                  plugins: [ 'react-refresh/babel' ],
                }
              }
            ],
          },
          { test: /\.css$/, use: getStyleLoaders() },
          { test: /\.less$/, use: getStyleLoaders('less-loader') },
          { test: /\.s[ac]ss$/, use: getStyleLoaders('sass-loader') },
          { test: /\.styl$/, use: getStyleLoaders('stylus-loader') },
          {
            test: /\.(png|jpe?g|gif|webp|svg)$/,
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024
              }
            },
          },
          {
            test: /\.(ttf|woff2?|mp4|mp3|avi)$/,
            type: 'asset/resource',
          },
        ]
      },
    ]
  },

  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, '../src'),
    }),

    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'),
    }),

    new ReactRefreshWebpackPlugin(),
  ],

  optimization: {
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}.js`,
    },
  },
};

npm scripts:

{
  "scripts": {
    "start": "npm run dev",
    "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js"
  }
}

# 2.6. 生产配置

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");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require('terser-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const CopyPlugin = require("copy-webpack-plugin");

const getStyleLoaders = (preProcessor) => {
  return [
    MiniCssExtractPlugin.loader,
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env',
          ],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
  mode: 'production',
  devtool: 'source-map',

  entry: path.resolve(__dirname, '../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/[name].[hash][ext][query]',
    clean: true,
  },

  resolve: {
    extensions: ['.js', '.jsx', '.json', '.wasm'],
  },

  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.jsx?$/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                  cacheCompression: false,
                }
              }
            ],
          },
          { test: /\.css$/, use: getStyleLoaders() },
          { test: /\.less$/, use: getStyleLoaders('less-loader') },
          { test: /\.s[ac]ss$/, use: getStyleLoaders('sass-loader') },
          { test: /\.styl$/, use: getStyleLoaders('stylus-loader') },
          {
            test: /\.(png|jpe?g|gif|webp|svg)$/,
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024
              }
            },
          },
          {
            test: /\.(ttf|woff2?|mp4|mp3|avi)$/,
            type: 'asset/resource',
          },
        ]
      },
    ]
  },

  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, '../src'),
    }),

    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, 避免与 HtmlWebpackPlugin 冲突
              '**/index.html',
            ]
          }
        },
      ],
    }),

  ],

  optimization: {
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}.js`,
    },
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserPlugin(),
      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' } } ] }],
            ],
          },
        },
      }),

    ],
  },
};

npm scripts:

{
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js"
  }
}

# 2.7. 合并配置

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');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const isProd = process.env.NODE_ENV === 'production';

const getStyleLoaders = (preProcessor) => {
  return [
    isProd ? MiniCssExtractPlugin.loader : 'style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env',
          ],
        },
      },
    },
    preProcessor && {
      loader: preProcessor,
      options: preProcessor === 'less-loader' ?
        {
          lessOptions: { // If you are using less-loader@5 please spread the lessOptions to options directly
            modifyVars: {
              'primary-color': '#1DA57A',
              'link-color': '#1DA57A',
              'border-radius-base': '2px',
            },
            javascriptEnabled: true,
          },
        } : {}
    },
  ].filter(Boolean);
};

module.exports = {
  mode: isProd ? 'production' : 'development',
  devtool: isProd ? 'source-map' : 'cheap-module-source-map',

  devServer: {
    host: 'localhost',
    port: '3000',
    open: true,
    hot: true,
    historyApiFallback: true,
  },

  entry: path.resolve(__dirname, '../src/main.js'),

  output: {
    path: isProd ? path.resolve(__dirname, '../dist') : undefined,
    filename: isProd ? 'static/js/[name].[contenthash:8].js' : 'static/js/[name].js',
    chunkFilename: isProd ? 'static/js/[name].[contenthash:8].chunk.js' : 'static/js/[name].chunk.js',
    assetModuleFilename: 'static/media/[name].[hash][ext][query]',
    clean: isProd,
  },

  resolve: {
    extensions: ['.js', '.jsx', '.json', '.wasm'],
  },

  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.jsx?$/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                  cacheCompression: false,
                  plugins: [
                    !isProd && 'react-refresh/babel'
                  ].filter(Boolean),
                }
              }
            ],
          },
          {test: /\.css$/, use: getStyleLoaders()},
          {test: /\.less$/, use: getStyleLoaders('less-loader')},
          {test: /\.s[ac]ss$/, use: getStyleLoaders('sass-loader')},
          {test: /\.styl$/, use: getStyleLoaders('stylus-loader')},
          {
            test: /\.(png|jpe?g|gif|webp|svg)$/,
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024
              }
            },
          },
          {
            test: /\.(ttf|woff2?|mp4|mp3|avi)$/,
            type: 'asset/resource',
          },
        ]
      },
    ]
  },

  plugins: [
    !isProd && new ReactRefreshWebpackPlugin(),

    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, '../src'),
    }),

    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'),
    }),

    isProd && new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css',
      chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
    }),

    isProd && new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, '../public'),
          to: path.resolve(__dirname, '../dist'),
          globOptions: {
            ignore: [
              // 不拷贝 index.html, 避免与 HtmlWebpackPlugin 冲突
              '**/index.html',
            ]
          }
        },
      ],
    }),

  ].filter(Boolean),

  // 关闭性能分析,提升打包速度
  // performance: false,

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // react react-dom react-router-dom
        react: {
          priority: 40,
          test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
          name: "lib-react",
        },
        // antd
        antd: {
          priority: 30,
          test: /[\\/]node_modules[\\/]antd[\\/]/,
          name: "lib-antd",
        },
        // others
        others: {
          priority: 20,
          test: /[\\/]node_modules[\\/]/,
          name: "lib-others",
        },
      },
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}.js`,
    },

    // 控制是否要进行压缩
    minimize: isProd,

    minimizer: [
      new CssMinimizerPlugin(),
      new TerserPlugin(),
      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' } } ] }],
            ],
          },
        },
      }),

    ],
  },
};

npm scripts:

{
  "scripts": {
    "start": "npm run dev",
    "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.config.js",
    "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js",
  }
}

# 2.8. 优化

# 2.8.1. antd 自定义主题

安装:

npm i antd@4

## "antd": "^4.24.9",

配置:

const getStyleLoaders = (preProcessor) => {
  return [
    isProd ? MiniCssExtractPlugin.loader : 'style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env',
          ],
        },
      },
    },
    preProcessor && {
      loader: preProcessor,
      options: preProcessor === 'less-loader' ?
        {
          lessOptions: { // If you are using less-loader@5 please spread the lessOptions to options directly
            modifyVars: {
              'primary-color': '#1DA57A',
              'link-color': '#1DA57A',
              'border-radius-base': '2px',
            },
            javascriptEnabled: true,
          },
        } : {}
    },
  ].filter(Boolean);
};

参考:

# 2.8.2. 拆分 node_modules 里面的包

配置:

module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // react react-dom react-router-dom
        react: {
          test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
          name: "lib-react",
          priority: 40,
        },
        // antd
        antd: {
          test: /[\\/]node_modules[\\/]antd[\\/]/,
          name: "lib-antd",
          priority: 30,
        },
        // others
        libs: {
          test: /[\\/]node_modules[\\/]/,
          name: "lib-others",
          priority: 20,
        },
      },
    },
  }
}

# 2.8.3. 关闭性能分析

module.exports = {
  // ...

  // 关闭性能分析,提升打包速度
  performance: false,
}

# 3. vue 脚手架

# 3.1. 项目结构

vue3-proj/
  config/
    webpack.config.js
    webpack.dev.js
    webpack.prod.js
  dist/
  node_modules/
  public/
    index.html
    favicon.ico
  src/
    pages/
      About.vue
      Home.vue
    router/
      index.js
    styles/
      element/
        index.scss
    App.vue
    main.js
  .browserslistrc
  .eslintrc.js
  babel.config.js
  package.json
  package-lock.json

# 3.2. 安装

# webpack cli dev-server
npm i -D webpack webpack-cli webpack-dev-server

## "webpack": "^5.81.0", "webpack-cli": "^5.0.2", "webpack-dev-server": "^4.13.3"


# css
npm i -D style-loader css-loader 
npm i -D less-loader 
npm i -D sass sass-loader
npm i -D stylus-loader

npm i -D postcss-loader postcss postcss-preset-env

npm i -D mini-css-extract-plugin
npm i -D css-minimizer-webpack-plugin

## "style-loader": "^3.3.2", "css-loader": "^6.7.3",
## "less-loader": "^11.1.0",
## "sass": "^1.62.1", "sass-loader": "^13.2.2",
## "stylus-loader": "^7.1.0",

## "postcss": "^8.4.23", "postcss-loader": "^7.3.0", "postcss-preset-env": "^8.3.2",

## "mini-css-extract-plugin": "^2.7.5",
## "css-minimizer-webpack-plugin": "^5.0.0",

# js
npm i -D eslint-webpack-plugin @babel/eslint-parser eslint-plugin-vue
npm i -D babel-loader @babel/core @vue/cli-plugin-babel
npm i -D terser-webpack-plugin

## "eslint-webpack-plugin": "^4.0.1", "@babel/eslint-parser": "^7.21.3", "eslint-plugin-vue": "^9.11.0",
## "babel-loader": "^9.1.2", "@babel/core": "^7.21.5", "@vue/cli-plugin-babel": "^5.0.8",
## "terser-webpack-plugin": "^5.3.7",

# img min
npm i -D image-minimizer-webpack-plugin imagemin
npm i -D imagemin-gifsicle imagemin-svgo
npm i -D imagemin-jpegtran imagemin-optipng 

## "image-minimizer-webpack-plugin": "^3.8.2", "imagemin": "^8.0.1",
## "imagemin-gifsicle": "^7.0.0", "imagemin-svgo": "^10.0.1",
## "imagemin-jpegtran": "^7.0.0", "imagemin-optipng": "^8.0.0",


# html
npm i -D html-webpack-plugin

## "html-webpack-plugin": "^5.5.1",


# others
npm i -D cross-env
npm i -D copy-webpack-plugin

## "cross-env": "^7.0.3",
## "copy-webpack-plugin": "^11.0.0",


# vue
npm i -D vue-loader vue-template-compiler vue-style-loader

## "vue-loader": "^17.1.0", "vue-style-loader": "^4.1.3", "vue-template-compiler": "^2.7.14",


npm i vue vue-router

## "vue": "^3.2.47", "vue-router": "^4.1.6"

# 3.3. 其他配置

# 3.3.1. .browserslistrc

last 2 version
> 1%
not dead

# 3.3.2. .eslintrc.js

module.exports = {
  root: true,
  env: {
    node: true,
  },
  // eslint-plugin-vue
  extends: ['plugin:vue/vue3-essential', 'eslint:recommended'],
  parserOptions: {
    parser: '@babel/eslint-parser',
  },
};

# 3.3.3. babel.config.js

module.exports = {
  presets: ['@vue/cli-plugin-babel/preset'],
};

# 3.3.4. vue-loader

安装:

npm i -D vue-loader vue-template-compiler vue-style-loader

配置:

const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      // this will apply to both plain `.css` files
      // AND `<style>` blocks in `.vue` files
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    // make sure to include the plugin for the magic
    new VueLoaderPlugin()
  ]
}

参考:

# 3.3.5. 业务级环境变量

说明:

  • cross-env 设置的环境变量是给 webpack 用的,在构建代码中使用
  • __VUE_OPTIONS_API__, __VUE_PROD_DEVTOOLS__ 这种在业务代码中使用的环境变量,需要用 webpack 插件注入

配置:

const { DefinePlugin } = require("webpack");

module.exports = {
  // ...
  plugins: [
    new DefinePlugin({
      // 解决 vue3 页面警告的问题
      __VUE_OPTIONS_API__: true,    // 选项式 API
      __VUE_PROD_DEVTOOLS__: false, // 生产环境是否启用 devtools
    }),
  ]
}

参考:

# 3.4. 开发配置

webpack.dev.js:

const path = require("path");

const ESLintWebpackPlugin = require('eslint-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const { DefinePlugin } = require("webpack");
const { VueLoaderPlugin } = require('vue-loader');

const getStyleLoaders = (preProcessor) => {
  return [
    'vue-style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env',
          ],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
  mode: 'development',
  devtool: 'cheap-module-source-map',
  devServer: {
    host: 'localhost',
    port: '3000',
    open: true,
    hot: true,
    historyApiFallback: true,
  },

  entry: path.resolve(__dirname, '../src/main.js'),

  output: {
    path: path.resolve('../dist'),
    filename: 'static/js/[name].js',
    chunkFilename: 'static/js/[name].chunk.js',
    assetModuleFilename: 'static/media/[name].[hash][ext][query]',
  },

  resolve: {
    extensions: ['.js', '.vue', '.json', '.wasm'],
  },

  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        oneOf: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                  cacheCompression: false,
                }
              }
            ],
          },
          { test: /\.css$/, use: getStyleLoaders() },
          { test: /\.less$/, use: getStyleLoaders('less-loader') },
          { test: /\.s[ac]ss$/, use: getStyleLoaders('sass-loader') },
          { test: /\.styl$/, use: getStyleLoaders('stylus-loader') },
          {
            test: /\.(png|jpe?g|gif|webp|svg)$/,
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024
              }
            },
          },
          {
            test: /\.(ttf|woff2?|mp4|mp3|avi)$/,
            type: 'asset/resource',
          },
        ]
      },
    ]
  },

  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, '../src'),
    }),

    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'),
    }),

    new VueLoaderPlugin(),

    new DefinePlugin({
      // 解决 vue3 页面警告的问题
      __VUE_OPTIONS_API__: true,    // 选项式 API
      __VUE_PROD_DEVTOOLS__: false, // 生产环境是否启用 devtools
    }),
  ],

  optimization: {
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}.js`,
    },
  },
};

npm scripts:

{
  "scripts": {
    "start": "npm run dev",
    "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js"
  }
}

# 3.5. 生产配置

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");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require('terser-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const CopyPlugin = require("copy-webpack-plugin");

const { DefinePlugin } = require("webpack");
const { VueLoaderPlugin } = require('vue-loader');

const getStyleLoaders = (preProcessor) => {
  return [
    MiniCssExtractPlugin.loader,
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env',
          ],
        },
      },
    },
    preProcessor,
  ].filter(Boolean);
};

module.exports = {
  mode: 'production',
  devtool: 'source-map',

  entry: path.resolve(__dirname, '../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/[name].[hash][ext][query]',
    clean: true,
  },

  resolve: {
    extensions: ['.js', '.vue', '.json', '.wasm'],
  },

  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        oneOf: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                  cacheCompression: false,
                }
              }
            ],
          },
          { test: /\.css$/, use: getStyleLoaders() },
          { test: /\.less$/, use: getStyleLoaders('less-loader') },
          { test: /\.s[ac]ss$/, use: getStyleLoaders('sass-loader') },
          { test: /\.styl$/, use: getStyleLoaders('stylus-loader') },
          {
            test: /\.(png|jpe?g|gif|webp|svg)$/,
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024
              }
            },
          },
          {
            test: /\.(ttf|woff2?|mp4|mp3|avi)$/,
            type: 'asset/resource',
          },
        ]
      },
    ]
  },

  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, '../src'),
    }),

    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, 避免与 HtmlWebpackPlugin 冲突
              '**/index.html',
            ]
          }
        },
      ],
    }),

    new VueLoaderPlugin(),

    new DefinePlugin({
      // 解决 vue3 页面警告的问题
      __VUE_OPTIONS_API__: true,    // 选项式 API
      __VUE_PROD_DEVTOOLS__: false, // 生产环境是否启用 devtools
    }),
  ],

  optimization: {
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}.js`,
    },
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserPlugin(),
      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' } } ] }],
            ],
          },
        },
      }),

    ],
  },
};

npm scripts:

{
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js"
  }
}

# 3.6. 合并配置

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");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require('terser-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const CopyPlugin = require("copy-webpack-plugin");

const { DefinePlugin } = require("webpack");
const { VueLoaderPlugin } = require('vue-loader');

const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
const ElementPlus = require('unplugin-element-plus/webpack')

const isProd = process.env.NODE_ENV === 'production';

const getStyleLoaders = (preProcessor) => {
  return [
    isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env',
          ],
        },
      },
    },
    preProcessor && {
      loader: preProcessor,
      options: preProcessor === "sass-loader" ?
        {
          additionalData: `@use "@/styles/element/index.scss" as *;`,
        } : {}
    },
  ].filter(Boolean);
};

module.exports = {
  mode: isProd ? 'production' : 'development',
  devtool: isProd ? 'source-map' : 'cheap-module-source-map',

  devServer: {
    host: 'localhost',
    port: '3000',
    open: true,
    hot: true,
    historyApiFallback: true,
  },

  entry: path.resolve(__dirname, '../src/main.js'),

  output: {
    path: isProd ? path.resolve(__dirname, '../dist') : undefined,
    filename: isProd ? 'static/js/[name].[contenthash:8].js' : 'static/js/[name].js',
    chunkFilename: isProd ? 'static/js/[name].[contenthash:8].chunk.js' : 'static/js/[name].chunk.js',
    assetModuleFilename: 'static/media/[name].[hash][ext][query]',
    clean: isProd,
  },

  resolve: {
    extensions: ['.js', '.vue', '.json', '.wasm'],
    alias: {
      '@': path.resolve(__dirname, '../src'),
    },
  },

  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          cacheDirectory: path.resolve(__dirname, "../node_modules/.cache/vue-loader"),
        },
      },
      {
        oneOf: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                  cacheCompression: false,
                }
              }
            ],
          },
          { test: /\.css$/, use: getStyleLoaders() },
          { test: /\.less$/, use: getStyleLoaders('less-loader') },
          { test: /\.s[ac]ss$/, use: getStyleLoaders('sass-loader') },
          { test: /\.styl$/, use: getStyleLoaders('stylus-loader') },
          {
            test: /\.(png|jpe?g|gif|webp|svg)$/,
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024
              }
            },
          },
          {
            test: /\.(ttf|woff2?|mp4|mp3|avi)$/,
            type: 'asset/resource',
          },
        ]
      },
    ]
  },

  plugins: [
    new ESLintWebpackPlugin({
      context: path.resolve(__dirname, '../src'),
    }),

    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'),
    }),

    isProd && new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css',
      chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
    }),

    isProd && new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, '../public'),
          to: path.resolve(__dirname, '../dist'),
          globOptions: {
            ignore: [
              // 不拷贝 index.html, 避免与 HtmlWebpackPlugin 冲突
              '**/index.html',
            ]
          }
        },
      ],
    }),

    new VueLoaderPlugin(),

    new DefinePlugin({
      // 解决 vue3 页面警告的问题
      __VUE_OPTIONS_API__: true,    // 选项式 API
      __VUE_PROD_DEVTOOLS__: false, // 生产环境是否启用 devtools
    }),

    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
    ElementPlus({
      useSource: true,
    }),
  ].filter(Boolean),

  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        vue: {
          test: /[\\/]node_modules[\\/]vue(.*)?[\\/]/,
          name: "lib-vue",
          priority: 40,
        },
        elementPlus: {
          test: /[\\/]node_modules[\\/]element-plus[\\/]/,
          name: "lib-element-plus",
          priority: 30,
        },
        others: {
          test: /[\\/]node_modules[\\/]/,
          name: "lib-others",
          priority: 20,
        },
      },
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}.js`,
    },

    minimize: isProd,
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserPlugin(),
      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' } } ] }],
            ],
          },
        },
      }),

    ],
  },
};

npm scripts:

{
  "scripts": {
    "start": "npm run dev",
    "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.config.js",
    "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js",
  }
}

# 3.7. 优化

# 3.7.1. element-plus

安装:

npm i element-plus
npm i -D unplugin-vue-components unplugin-auto-import
npm i -D unplugin-element-plus

## "element-plus": "^2.3.4",
## "unplugin-vue-components": "^0.24.1", "unplugin-auto-import": "^0.15.3",
## "unplugin-element-plus": "^0.7.1",

全部引入:

import { createApp } from 'vue';

import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

import App from './App';

createApp(App)
  .use(ElementPlus)
  .mount('#app');

按需引入:

const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  plugins: [
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ]
};

定制主题:

  1. 创建 src/styles/element/index.scss 文件

    @forward 'element-plus/theme-chalk/src/common/var.scss' with (
      $colors: (
        'primary': (
          'base': green,
        ),
      )
    );
    
  2. 修改 webpack.config.js

    
    module.exports = {
      resolve: {
        alias: {
          '@': path.resolve(__dirname, '../src'),
        },
      },
    }
    
  3. 修改 sass-loader 配置

    const getStyleLoaders = (preProcessor) => {
      return [
        isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
        'css-loader',
        {
          loader: 'postcss-loader',
          options: {
            postcssOptions: {
              plugins: [
                'postcss-preset-env',
              ],
            },
          },
        },
        preProcessor && {
          loader: preProcessor,
          options: preProcessor === "sass-loader" ?
            {
              additionalData: `@use "@/styles/element/index.scss" as *;`,
            } : {}
        },
      ].filter(Boolean);
    };
    
  4. 使用 plugin

    const ElementPlus = require('unplugin-element-plus/webpack')
    
    module.exports = {
      plugins: [
        ElementPlus({
          useSource: true,
        }),
      ]
    }
    

参考:

# 3.7.2. 拆分 node_modules

module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        vue: {
          test: /[\\/]node_modules[\\/]vue(.*)?[\\/]/,
          name: "lib-vue",
          priority: 40,
        },
        elementPlus: {
          test: /[\\/]node_modules[\\/]element-plus[\\/]/,
          name: "lib-element-plus",
          priority: 30,
        },
        others: {
          test: /[\\/]node_modules[\\/]/,
          name: "lib-others",
          priority: 20,
        },
      },
    },
  }
}

# 3.7.3. vue-loader 缓存

module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          cacheDirectory: path.resolve(__dirname, "../node_modules/.cache/vue-loader"),
        },
      },
    ]
  }
}

# 4. 总结

当项目越来越大,可以考虑进一步优化,参考上一篇文章(“webpack高级”)

本章目录