首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Webpack5 入门与实战:从零构建现代化前端工程

Webpack5 入门与实战:从零构建现代化前端工程

原创
作者头像
搜weiranit.fun
发布2026-08-11 15:09:20
发布2026-08-11 15:09:20
1270
举报

Webpack5 入门与实战:从零构建现代化前端工程

深度解析 Webpack5 核心机制,手写 Loader 与 Plugin,玩转代码分割、Tree Shaking、持久缓存与构建优化

1. 为什么仍需要 Webpack?

在 Vite、esbuild 等新型构建工具层出不穷的今天,Webpack 依然是生产级项目中最成熟、最稳定的模块打包器。它强大的可扩展性精细的构建控制以及庞大的生态,使其在复杂大型项目中无可替代。Webpack5 带来了持久化缓存模块联邦Tree Shaking 优化等重磅特性,让构建性能与运行时性能都迈上新台阶。

本文将从零搭建一个 React + TypeScript 项目,深入 Webpack5 的配置、优化、原理,并手写自定义 Loader 与 Plugin,助你真正掌握这项前端核心技能。

2. 初始化项目与核心依赖

代码语言:javascript
复制
mkdir webpack5-playground
cd webpack5-playground
npm init -y

安装核心依赖:

代码语言:javascript
复制
npm install webpack webpack-cli webpack-dev-server --save-dev
npm install react react-dom @types/react @types/react-dom --save
npm install typescript ts-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript babel-loader --save-dev
npm install html-webpack-plugin clean-webpack-plugin mini-css-extract-plugin css-loader style-loader sass sass-loader --save-dev
npm install terser-webpack-plugin css-minimizer-webpack-plugin --save-dev

创建基本目录结构:

代码语言:javascript
复制
src/
  index.tsx
  App.tsx
  index.html (模板)
public/
  favicon.ico
config/
  webpack.common.js
  webpack.dev.js
  webpack.prod.js

3. Webpack5 核心配置(Common / Dev / Prod 三环境)

3.1 公共配置

代码语言:javascript
复制
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  entry: './src/index.tsx',
  output: {
    filename: '[name].[contenthash:8].js',
    path: path.resolve(__dirname, '../dist'),
    publicPath: '/',
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
    alias: {
      '@': path.resolve(__dirname, '../src'),
    },
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: ['babel-loader'], // 通过 babel 转译
      },
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
      {
        test: /\.(png|jpe?g|gif|svg|webp)$/i,
        type: 'asset/resource', // Webpack5 内置资源模块
        generator: {
          filename: 'images/[name].[hash:8][ext]',
        },
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[hash:8][ext]',
        },
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      favicon: './public/favicon.ico',
      inject: true,
    }),
  ],
  // 缓存配置(Webpack5 持久化缓存)
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename], // 配置文件变化时重新构建缓存
    },
  },
};

3.2 Babel 配置

代码语言:javascript
复制
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "babel-plugin-transform-typescript-metadata"
  ]
}

3.3 开发环境

代码语言:javascript
复制
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-cheap-module-source-map',
  devServer: {
    port: 3000,
    hot: true,
    open: true,
    historyApiFallback: true,
    compress: true,
    static: {
      directory: path.join(__dirname, '../public'),
    },
  },
  module: {
    rules: [
      {
        test: /\.(scss|css)$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
    ],
  },
  optimization: {
    // 开发环境下不压缩,保留模块名便于调试
    minimize: false,
  },
});

3.4 生产环境 webpack.prod.js

代码语言:javascript
复制
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  devtool: 'source-map',
  output: {
    filename: '[name].[contenthash:12].js',
  },
  module: {
    rules: [
      {
        test: /\.(scss|css)$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash:12].css',
    }),
  ],
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true, // 去除 console
          },
        },
      }),
      new CssMinimizerPlugin(),
    ],
    // 代码分割(核心)
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          enforce: true,
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
          name: 'common',
        },
      },
    },
    runtimeChunk: 'single', // 将 runtime 代码单独提取
  },
  performance: {
    hints: 'warning',
    maxEntrypointSize: 512000,
    maxAssetSize: 512000,
  },
});

4. 深入 Loader:工作原理与手写实战

Loader 本质上是一个导出为函数的 Node.js 模块,它接收源文件内容,返回转换后的内容。Webpack5 支持同步/异步 Loader,以及 loader-utilsschema-utils 进行参数校验。

4.1 手写一个 markdown-loader:将 Markdown 转为 HTML 并注入 React 组件

代码语言:javascript
复制
// loaders/markdown-loader.js
const { marked } = require('marked');
const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');

const schema = {
  type: 'object',
  properties: {
    highlight: { type: 'boolean' },
  },
};

module.exports = function (source) {
  const options = getOptions(this);
  validate(schema, options, 'Markdown Loader');

  // 异步模式
  const callback = this.async();

  try {
    const html = marked(source, {
      highlight: options.highlight ? (code, lang) => {
        // 可接入 highlight.js
        return `<pre><code>${code}</code></pre>`;
      } : undefined,
    });

    // 返回一个 React 组件字符串,或者直接返回 HTML 字符串
    const result = `
      const React = require('react');
      module.exports = function MarkdownContent() {
        return React.createElement('div', {
          dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} }
        });
      };
    `;
    callback(null, result);
  } catch (err) {
    callback(err);
  }
};

使用:在 webpack.common.jsmodule.rules 中添加:

代码语言:javascript
复制
{
  test: /\.md$/,
  use: [
    'babel-loader', // 将 JSX 转译
    {
      loader: path.resolve(__dirname, '../loaders/markdown-loader.js'),
      options: { highlight: true },
    },
  ],
}

这样便可以在组件中直接 import Readme from './README.md' 并渲染。


5. 深入 Plugin:生命周期与手写实战

Plugin 通过 Webpack 的 Tapable 钩子系统介入构建流程,可以在不同阶段执行自定义逻辑。一个 Plugin 是包含 apply 方法的类。

5.1 手写 BuildTimePlugin:在构建完成时输出耗时

代码语言:javascript
复制
// plugins/BuildTimePlugin.js
class BuildTimePlugin {
  constructor(options = {}) {
    this.options = options;
  }

  apply(compiler) {
    const startTime = Date.now();

    compiler.hooks.done.tap('BuildTimePlugin', (stats) => {
      const endTime = Date.now();
      const duration = endTime - startTime;
      console.log(`\n⏱️  Build completed in ${duration}ms`);

      if (this.options.emitFile) {
        // 可以生成一个 JSON 文件记录构建信息
        const content = JSON.stringify({
          duration,
          timestamp: new Date().toISOString(),
          hash: stats.hash,
        });
        // 使用 compilation 的 emitAsset
        const compilation = stats.compilation;
        compilation.emitAsset(
          'build-meta.json',
          new compiler.webpack.sources.RawSource(content)
        );
      }
    });

    // 还可以监听其他钩子,比如 compilation 阶段
    compiler.hooks.compilation.tap('BuildTimePlugin', (compilation) => {
      compilation.hooks.buildModule.tap('BuildTimePlugin', (module) => {
        // 每个模块构建前触发
      });
    });
  }
}

module.exports = BuildTimePlugin;

使用:在 webpack.prod.js 中添加:

代码语言:javascript
复制
const BuildTimePlugin = require('../plugins/BuildTimePlugin');

plugins: [
  new BuildTimePlugin({ emitFile: true }),
  // ...其他插件
]

6. 性能优化:Webpack5 核心利器

6.1 持久化缓存(Persistent Caching)

Webpack5 默认开启内存缓存,但我们可以启用文件系统缓存,极大加速二次构建:

代码语言:javascript
复制
// 已在 common 中配置
cache: {
  type: 'filesystem',
  buildDependencies: {
    config: [__filename],
  },
  name: 'dev-cache', // 不同环境可分开
},

6.2 Tree Shaking 最佳实践

Webpack5 通过 sideEffectsusedExports 实现 Tree Shaking。确保 package.json 中声明:

代码语言:javascript
复制
{
  "sideEffects": false, // 或 ["*.css", "*.scss"]
}

在生产模式下,optimization.usedExports 默认为 true,Webpack 会标记未使用的导出并删除代码。配合 TerserPluginunused 选项可进一步压缩。

6.3 代码分割与动态导入

除了 splitChunks,我们还可以利用动态 import() 实现路由级别的懒加载。例如 React 中使用 React.lazy

代码语言:javascript
复制
const Dashboard = React.lazy(() => import('./pages/Dashboard'));

Webpack 会自动为 Dashboard 生成独立的 chunk,并在加载时异步请求。

6.4 减少构建体积:使用 babel-plugin-transform-import 实现按需加载

对于 Antd 等组件库,配置 babel 插件:

代码语言:javascript
复制
{
  "plugins": [
    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
  ]
}

7. 完整的 src/index.tsx 示例

代码语言:javascript
复制
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles/index.scss';

const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);

App.tsx

代码语言:javascript
复制
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./components/LazyComponent'));

const App: React.FC = () => (
  <div>
    <h1>Webpack5 实战</h1>
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  </div>
);

export default App;

8. 运行与构建脚本

package.json 中添加:

代码语言:javascript
复制
"scripts": {
  "start": "webpack serve --config config/webpack.dev.js",
  "build": "webpack --config config/webpack.prod.js",
  "analyze": "webpack --config config/webpack.prod.js --profile --json > stats.json"
}

使用 webpack-bundle-analyzer 分析包大小:

代码语言:javascript
复制
npm install webpack-bundle-analyzer --save-dev

在 prod 配置中添加:

代码语言:javascript
复制
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
// 条件触发
if (process.env.ANALYZE) {
  plugins.push(new BundleAnalyzerPlugin());
}

9. 模块联邦(Module Federation)—— Webpack5 杀手级特性

模块联邦允许多个独立应用在运行时共享模块,实现微前端风格的代码共享。

代码语言:javascript
复制
// 在 webpack.common.js 中
const { ModuleFederationPlugin } = require('webpack').container;

new ModuleFederationPlugin({
  name: 'hostApp',
  remotes: {
    remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
  },
  shared: {
    react: { singleton: true, eager: true },
    'react-dom': { singleton: true },
  },
});

这样 hostApp 可以直接使用 remoteApp 导出的组件,实现真正的动态加载。


10. 总结与最佳实践

  • 环境分离:始终区分开发/生产配置,利用 webpack-merge
  • 缓存优先:开启 filesystem 缓存,并合理配置 buildDependencies
  • 代码分割:合理利用 splitChunks 和动态 import,按需加载。
  • Tree Shaking:确保 sideEffects 正确配置,并检查是否有未使用的导出。
  • Loader 与 Plugin:理解执行时机,按需编写自定义扩展,避免过度工程。
  • Source Map:生产环境使用 source-maphidden-source-map 以便调试。
  • 性能监控:使用 webpack-bundle-analyzer 定期分析体积。

Webpack5 不是最快的构建工具,但它是最可控、最可靠的生产级打包方案。掌握其核心配置与优化思路,是每一位资深前端工程师的必修课。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Webpack5 入门与实战:从零构建现代化前端工程
    • 1. 为什么仍需要 Webpack?
    • 2. 初始化项目与核心依赖
    • 3. Webpack5 核心配置(Common / Dev / Prod 三环境)
      • 3.1 公共配置
      • 3.2 Babel 配置
      • 3.3 开发环境
      • 3.4 生产环境 webpack.prod.js
    • 4. 深入 Loader:工作原理与手写实战
      • 4.1 手写一个 markdown-loader:将 Markdown 转为 HTML 并注入 React 组件
    • 5. 深入 Plugin:生命周期与手写实战
      • 5.1 手写 BuildTimePlugin:在构建完成时输出耗时
    • 6. 性能优化:Webpack5 核心利器
      • 6.1 持久化缓存(Persistent Caching)
      • 6.2 Tree Shaking 最佳实践
      • 6.3 代码分割与动态导入
      • 6.4 减少构建体积:使用 babel-plugin-transform-import 实现按需加载
    • 7. 完整的 src/index.tsx 示例
    • 8. 运行与构建脚本
    • 9. 模块联邦(Module Federation)—— Webpack5 杀手级特性
    • 10. 总结与最佳实践
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档