Webpack 5 从零搭建现代化前端项目

Created on

引言

在日常开发中,我们经常使用 Vue CLI、Create React App 等官方脚手架。这些工具虽然方便,但如果不了解其内部原理,在需要自定义配置时会感到困难。本文将带你从零开始,使用 Webpack 5 搭建一个现代化的前端项目,让你深入理解前端工程化的每个环节。

技术选型

初始化项目

创建项目目录

mkdir webpack5-project
cd webpack5-project
pnpm init

安装核心依赖

# 安装 Webpack 相关
pnpm add -D webpack@latest webpack-cli@latest webpack-merge@latest webpack-dev-server@latest

# 安装构建优化插件
pnpm add -D compression-webpack-plugin@latest terser-webpack-plugin@latest

项目目录结构

webpack5-project/
├── src/
│   ├── pages/
│   │   ├── page1/
│   │   │   ├── index.html
│   │   │   ├── index.js
│   │   │   └── style.css
│   │   └── page2/
│   │       ├── index.html
│   │       ├── index.js
│   │       └── style.css
│   ├── assets/
│   │   ├── images/
│   │   └── fonts/
│   └── utils/
├── dist/
├── config/
│   ├── webpack.common.js
│   ├── webpack.dev.js
│   └── webpack.prod.js
├── .eslintrc.js
├── .prettierrc.js
├── babel.config.js
├── postcss.config.js
└── package.json

开发环境配置

创建 config/webpack.dev.js

const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  mode: "development",

  // 开发环境使用 eval-cheap-module-source-map,构建速度快
  devtool: "eval-cheap-module-source-map",

  // 开发服务器配置
  devServer: {
    static: {
      directory: path.join(__dirname, "../dist"),
    },
    compress: true,
    port: 9000,
    hot: true,
    open: true,
    historyApiFallback: true, // 支持 HTML5 History API
    client: {
      overlay: {
        errors: true,
        warnings: false,
      },
    },
  },

  // 图片和字体配置
  module: {
    rules: [
      {
        test: /\.(png|svg|jpg|jpeg|gif|webp)$/i,
        type: "asset",
        generator: {
          publicPath: "/",
          filename: "images/[name][ext]",
        },
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024, // 8KB以下转base64
          },
        },
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: "asset/resource",
        generator: {
          publicPath: "/",
          filename: "fonts/[name][ext]",
        },
      },
    ],
  },

  // 开发环境优化
  optimization: {
    runtimeChunk: "single",
    moduleIds: "named",
    chunkIds: "named",
  },

  // 性能提示
  performance: {
    hints: false,
  },
});

生产环境配置

创建 config/webpack.prod.js

const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");
const TerserPlugin = require("terser-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");

module.exports = merge(common, {
  mode: "production",

  // 生产环境使用 source-map,便于排查问题
  devtool: "source-map",

  // 输出配置
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "js/[name].[contenthash:8].js",
    chunkFilename: "js/[name].[contenthash:8].chunk.js",
    assetModuleFilename: "assets/[name].[contenthash:8][ext]",
    clean: true, // 清理输出目录
  },

  // 图片和字体配置
  module: {
    rules: [
      {
        test: /\.(png|svg|jpg|jpeg|gif|webp)$/i,
        type: "asset",
        generator: {
          publicPath: "/dist/",
          filename: "images/[name].[contenthash:8][ext]",
        },
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          },
        },
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: "asset/resource",
        generator: {
          publicPath: "/dist/",
          filename: "fonts/[name].[contenthash:8][ext]",
        },
      },
    ],
  },

  // 生产环境优化
  optimization: {
    minimize: true,
    minimizer: [
      // 压缩 JavaScript
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // 移除 console
            drop_debugger: true, // 移除 debugger
            pure_funcs: ["console.log"], // 移除特定函数
          },
          format: {
            comments: false, // 移除注释
          },
        },
        extractComments: false,
      }),
    ],

    // 代码分割
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          priority: 10,
          reuseExistingChunk: true,
        },
        // 公共代码
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
          minSize: 0,
        },
      },
    },

    // 运行时代码单独打包
    runtimeChunk: {
      name: "runtime",
    },
  },

  plugins: [
    // Gzip 压缩
    new CompressionPlugin({
      test: /\.(js|css|html|svg)$/,
      algorithm: "gzip",
      threshold: 10240, // 大于10KB才压缩
      minRatio: 0.8,
      deleteOriginalAssets: false,
    }),
  ],

  // 性能提示
  performance: {
    hints: "warning",
    maxEntrypointSize: 512000, // 入口文件大小限制
    maxAssetSize: 512000, // 资源文件大小限制
  },
});

通用配置

创建 config/webpack.common.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const ESLintPlugin = require("eslint-webpack-plugin");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const chalk = require("chalk");

const isDevelopment = process.env.NODE_ENV !== "production";

module.exports = {
  // 入口配置
  entry: {
    page1: path.resolve(__dirname, "../src/pages/page1/index.js"),
    page2: path.resolve(__dirname, "../src/pages/page2/index.js"),
  },

  // 输出配置
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "js/[name].js",
    clean: true,
  },

  // 模块解析规则
  module: {
    rules: [
      // HTML
      {
        test: /\.(html|htm)$/i,
        loader: "html-loader",
        options: {
          minimize: !isDevelopment,
        },
      },

      // JavaScript/JSX
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            cacheDirectory: true, // 启用缓存
          },
        },
      },

      // CSS
      {
        test: /\.css$/i,
        use: [
          isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              importLoaders: 1,
              sourceMap: true,
            },
          },
          "postcss-loader",
        ],
      },

      // Sass/SCSS
      {
        test: /\.(sass|scss)$/i,
        use: [
          isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
              sourceMap: true,
            },
          },
          "postcss-loader",
          "sass-loader",
        ],
      },

      // Less
      {
        test: /\.less$/i,
        use: [
          isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
              sourceMap: true,
            },
          },
          "postcss-loader",
          {
            loader: "less-loader",
            options: {
              lessOptions: {
                javascriptEnabled: true,
              },
            },
          },
        ],
      },
    ],
  },

  // 路径解析配置
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "../src"),
      "@assets": path.resolve(__dirname, "../src/assets"),
      "@utils": path.resolve(__dirname, "../src/utils"),
    },
    extensions: [".js", ".jsx", ".json", ".css", ".scss", ".less"],
    // 减少模块搜索层级
    modules: [path.resolve(__dirname, "../node_modules")],
  },

  // 插件配置
  plugins: [
    // 进度条
    new ProgressBarPlugin({
      format: `  :msg [:bar] ${chalk.green.bold(":percent")} (:elapsed s)`,
      clear: false,
    }),

    // ESLint
    new ESLintPlugin({
      extensions: ["js", "jsx"],
      exclude: "node_modules",
      cache: true,
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
    }),

    // HTML 生成
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../src/pages/page1/index.html"),
      filename: "index.html",
      chunks: ["page1"],
      inject: "body",
      minify: !isDevelopment && {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true,
      },
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../src/pages/page2/index.html"),
      filename: "page2.html",
      chunks: ["page2"],
      inject: "body",
      minify: !isDevelopment && {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true,
      },
    }),

    // CSS 提取
    new MiniCssExtractPlugin({
      filename: isDevelopment
        ? "css/[name].css"
        : "css/[name].[contenthash:8].css",
      chunkFilename: isDevelopment
        ? "css/[id].css"
        : "css/[id].[contenthash:8].css",
    }),
  ],

  // 优化配置
  optimization: {
    minimizer: [
      "...", // 保留默认的 minimizer
      new CssMinimizerPlugin({
        minimizerOptions: {
          preset: [
            "default",
            {
              discardComments: { removeAll: true },
            },
          ],
        },
      }),
    ],
  },

  // 缓存配置
  cache: {
    type: "filesystem",
    cacheDirectory: path.resolve(__dirname, "../node_modules/.cache/webpack"),
  },

  // 统计信息
  stats: {
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false,
  },
};

ESLint + Prettier 配置

安装依赖

pnpm add -D eslint prettier eslint-config-prettier eslint-plugin-prettier

ESLint 配置

创建 .eslintrc.js

module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:prettier/recommended", // 必须放在最后
  ],
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
  },
  rules: {
    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
    "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
    "prettier/prettier": "error",
  },
};

Prettier 配置

创建 .prettierrc.js

module.exports = {
  // 一行最多 100 字符
  printWidth: 100,
  // 使用 2 个空格缩进
  tabWidth: 2,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 行尾需要有分号
  semi: true,
  // 使用单引号
  singleQuote: true,
  // 对象的 key 仅在必要时用引号
  quoteProps: "as-needed",
  // jsx 不使用单引号,而使用双引号
  jsxSingleQuote: false,
  // 末尾需要逗号
  trailingComma: "es5",
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // jsx 标签的反尖括号需要换行
  bracketSameLine: false,
  // 箭头函数,只有一个参数的时候,也需要括号
  arrowParens: "always",
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: "preserve",
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: "css",
  // vue 文件中的 script 和 style 内不用缩进
  vueIndentScriptAndStyle: false,
  // 换行符使用 lf
  endOfLine: "lf",
};

创建 .prettierignore

dist
node_modules
*.md

Babel 配置

安装依赖

pnpm add -D @babel/core @babel/preset-env babel-loader core-js@3

创建配置文件

创建 babel.config.js

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        // 按需引入 polyfill
        useBuiltIns: "usage",
        corejs: 3,
        // 目标浏览器
        targets: {
          chrome: "87",
          firefox: "78",
          safari: "14",
          edge: "88",
        },
      },
    ],
  ],
  plugins: [],
};

PostCSS 配置

安装依赖

pnpm add -D postcss postcss-loader postcss-preset-env autoprefixer

创建配置文件

创建 postcss.config.js

module.exports = {
  plugins: ["postcss-preset-env", "autoprefixer"],
};

创建 .browserslistrc

> 0.5%
last 2 versions
not dead

Git Hooks 配置

安装依赖

pnpm add -D husky lint-staged

初始化 husky

npx husky install
npx husky add .husky/pre-commit "npx lint-staged"

配置 lint-staged

package.json 中添加:

{
  "lint-staged": {
    "*.{js,jsx}": ["eslint --fix", "prettier --write"],
    "*.{css,scss,less}": ["prettier --write"],
    "*.{html,md,json}": ["prettier --write"]
  }
}

package.json 脚本配置

{
  "name": "webpack5-project",
  "version": "1.0.0",
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js",
    "build": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
    "build:analyze": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js --env analyze",
    "lint": "eslint --ext .js,.jsx src",
    "lint:fix": "eslint --ext .js,.jsx src --fix",
    "format": "prettier --write \"src/**/*.{js,jsx,css,scss,less,html}\"",
    "prepare": "husky install"
  },
  "devDependencies": {
    "@babel/core": "^7.23.0",
    "@babel/preset-env": "^7.23.0",
    "autoprefixer": "^10.4.16",
    "babel-loader": "^9.1.3",
    "chalk": "^4.1.2",
    "compression-webpack-plugin": "^10.0.0",
    "core-js": "^3.33.0",
    "cross-env": "^7.0.3",
    "css-loader": "^6.8.1",
    "css-minimizer-webpack-plugin": "^5.0.1",
    "eslint": "^8.52.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.1",
    "eslint-webpack-plugin": "^4.0.1",
    "html-loader": "^4.2.0",
    "html-webpack-plugin": "^5.5.3",
    "husky": "^8.0.3",
    "less": "^4.2.0",
    "less-loader": "^11.1.3",
    "lint-staged": "^15.0.2",
    "mini-css-extract-plugin": "^2.7.6",
    "postcss": "^8.4.31",
    "postcss-loader": "^7.3.3",
    "postcss-preset-env": "^9.2.0",
    "prettier": "^3.0.3",
    "progress-bar-webpack-plugin": "^2.1.0",
    "sass": "^1.69.5",
    "sass-loader": "^13.3.2",
    "style-loader": "^3.3.3",
    "terser-webpack-plugin": "^5.3.9",
    "webpack": "^5.89.0",
    "webpack-bundle-analyzer": "^4.10.1",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1",
    "webpack-merge": "^5.10.0"
  }
}

打包分析

安装依赖

pnpm add -D webpack-bundle-analyzer

配置分析工具

webpack.prod.js 中添加:

const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");

module.exports = merge(common, {
  // ... 其他配置
  plugins: [
    // 打包分析
    process.env.ANALYZE &&
      new BundleAnalyzerPlugin({
        analyzerMode: "static",
        openAnalyzer: true,
      }),
  ].filter(Boolean),
});

运行分析:

npm run build:analyze

性能优化建议

1. 使用 Tree Shaking

确保 package.json 中设置:

{
  "sideEffects": false
}

或指定有副作用的文件:

{
  "sideEffects": ["*.css", "*.scss"]
}

2. 使用 DLL 插件(可选)

对于不常变动的第三方库,可以使用 DLL 插件预编译。

3. 多线程构建

pnpm add -D thread-loader
{
  test: /\.js$/,
  use: [
    'thread-loader',
    'babel-loader'
  ]
}

4. 缩小构建目标

module.exports = {
  resolve: {
    modules: [path.resolve(__dirname, "node_modules")],
    extensions: [".js", ".jsx"], // 只包含必要的扩展名
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, "src"), // 只编译 src 目录
        exclude: /node_modules/,
      },
    ],
  },
};

常见问题

1. 开发服务器无法热更新

确保设置了 target: 'web' 在开发配置中。

2. CSS 模块化

{
  test: /\.module\.css$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        modules: {
          localIdentName: '[name]__[local]--[hash:base64:5]',
        },
      },
    },
  ],
}

3. 环境变量

使用 cross-env 设置跨平台环境变量,或使用 Webpack 的 DefinePlugin

const webpack = require("webpack");

plugins: [
  new webpack.DefinePlugin({
    "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
    "process.env.API_URL": JSON.stringify("https://api.example.com"),
  }),
];

总结

本文详细介绍了如何使用 Webpack 5 从零搭建一个完整的前端项目,包括:

  1. ✅ 开发环境和生产环境的配置分离
  2. ✅ 完整的代码规范工具链(ESLint + Prettier)
  3. ✅ 现代化的 JavaScript 编译(Babel)
  4. ✅ CSS 预处理和后处理(Sass/Less + PostCSS)
  5. ✅ Git 提交前的代码检查(husky + lint-staged)
  6. ✅ 性能优化(代码分割、压缩、缓存)
  7. ✅ 打包分析工具

通过这个配置,你可以构建一个高性能、可维护的现代化前端项目。建议根据实际项目需求进行调整和优化。

参考资源