Webpack随笔

203 阅读9分钟

webpack随笔

本文主要记录一些关于 webpack 的版本变动、常用 API 配置和使用、性能优化,供自己以后查漏补缺,也欢迎同道朋友交流学习。

引言

Webpack 是一款广泛应用于现代前端开发中的静态模块打包工具。它主要用于将 JavaScript 应用程序的各种模块(包括JavaScript 文件、CSS 样式表、图像以及其他资源文件等)按照依赖关系进行高效的组织、处理和打包,最终输出为一个或多个优化过的静态资源文件( bundles )。这一过程包括编译转换压缩等,以便于浏览器能够快速、有效地加载和执行。

核心特点和功能

  1. 模块打包:通过分析相互依赖的模块关系,将所有模块打包成一个或几个较小的、按需加载的包( bundles )。
  2. 加载器(Loaders):支持加载预处理非 JS 模块,比如将 CSS/Less/Sass图片TypeScript等转换为 JS 模块,使得它们能够被浏览器理解并执行。
  3. 插件(Plugins):提供了强大的插件系统,允许开发者执行范围更广的任务,比如优化压缩资源管理环境变量注入等。
  4. 代码拆分(Code Splitting):支持将代码 拆分 成多个小包,从而实现 按需加载,提高应用的加载速度和运行效率。
  5. 开发服务器(Development Server):内置的开发服务器( webpack-dev-server )提供了实时重载( Live reloading )和 热模块替换( Hot Module Replacement, HMR )功能,使得开发过程更加高效便捷。
  6. 生产环境优化:通过一系列配置和插件,Webpack 可以对输出的代码进行压缩、去除死代码等操作,进一步提升应用性能。

应用场景

Webpack 适用于多种前端开发场景,以下是一些典型的应用场景:

  1. 代码压缩与优化:可以自动压缩和混淆 JS、CSS 代码,减小文件体积,提升网页加载速度。
  2. 编译现代语言特性:它支持使用 ES6TSSCSS 等现代前端开发语言,并将其转换为向后兼容的 JS 和CSS。
  3. 模块化处理:支持模块化开发,能有效管理和打包,使得代码结构更加清晰,易于维护。
  4. 资源管理:它能够处理图片、字体等静态资源,通过加载器自动转换、压缩,并根据需求将其作为模块引入,简化资源引用过程。
  5. 代码拆分与懒加载:通过代码拆分功能,Webpack 可以根据应用的实际需要动态加载代码,提升首屏加载速度和用户体验。
  6. 环境配置:根据不同的环境(如开发环境、测试环境、生产环境)自动生成对应的配置,方便开发者进行调试和优化。
  7. 自动刷新与热模块替换:集成 webpack-dev-server 支持实时自动刷新浏览器页面或热模块替换( HMR ),大大提升了开发效率。
  8. 第三方库集成:可以轻松集成 Vue.jsReactAngular 等前端框架及 Antdelement-uimint-ui等组件库,便于框架和库的管理和优化。
  9. HASH缓存:通过添加 HASH 值到文件名,实现浏览器缓存策略,确保用户在有新版本发布时获取到最新的资源。

替代工具(如Rollup、Parcel等)的比较

在前端构建工具领域,WebpackRollupParcel 各有特色,适用于不同的开发场景和需求。以下是这三者的简要比较:

工具特点优势劣势
Webpack是一个高度可配置且功能强大的模块打包工具,适合大型项目和复杂应用。它支持广泛的加载器和插件系统,能够处理各种资源类型,同时提供代码拆分、懒加载、热模块替换等高级功能。高度定制化,强大的生态系统,丰富的插件和加载器支持配置复杂,有一定学习曲线;在未优化配置时构建速度可能较慢。
Rollup是一个快速、简洁的模块打包器,专注于 JS 模块打包。输出代码体积小,优化能力强;配置相对简单、专注于ES模块打包,适合构建库。对非 JS 资源的支持较弱,需要依赖外部插件;在处理大型应用或非标准模块系统时可能不够灵活。
Parcel强调零配置,开箱即用,自动处理资源类型,包括代码转换、优化、压缩等,使得开发者可以快速启动项目。零配置,内置了大多数功能,如热模块替换、代码拆分等;自动处理资源类型。可配置性和灵活性较低,不太适用大型项目

选择工具

选择哪种工具取决于项目的需求、团队的经验以及对构建速度、配置复杂度、代码优化程度等因素的考量。

  • Webpack 适合大型项目和需要高度定制化的场景。
  • Rollup 更适合库的打包和追求极致代码体积的项目。
  • Parcel 则是快速原型开发和追求快速上手体验的理想选择。

版本变动

Webpack其版本的迭代伴随着许多重要特性的引入和改进。以下主要介绍下 Webpack 4Webpack 5 的变动概览:

Webpack 4

  • 更简化配置:继续简化配置过程,许多选项被赋予了更合理的默认值,使得小型项目甚至可以不用配置文件直接运行。
  • 分离CLI:将命令行界面( CLI )从核心包中分离出来,需要单独安装 webpack-cli。这使得核心更轻量,也便于 CLI 独立更新。
  • 更快的构建速度:通过更好的内部优化, 极大地提高了构建速度,特别是在增量构建上。
  • Node.js v4 不再支持:由于依赖于 ES6 语法,Webpack 4 不再支持 Node.js v4
  • 默认使用production模式:如果没有指定 mode,Webpack 4 默认以 production 模式运行,这带来了诸如代码压缩等优化。

Webpack 5

  • 持续性能优化:后续版本继续在性能上进行优化,包括更快的启动时间和更少的内存使用。
  • 模块联邦(Module Federation):引入了模块联邦特性,这是一种无需中心化服务器即可实现微前端架构的方案,允许模块在运行时动态共享。
  • 移除老旧特性:逐步移除了某些过时或不推荐使用的特性,鼓励开发者采用更现代化的实践。
  • 安全性增强:增强了对加载器和插件的安全性检查,增加了对不安全依赖的警告或错误提示。

全局安装

你可以通过 npm(Node.js的包管理器)来安装 Webpack。在项目的根目录下,运行以下命令:

npm install webpack webpack-cli --save-dev

## 生成 `webpack.config.js`
npx webpack-cli init

Webpack基础配置

webpack.config.js

webpack.config.js 文件它导出一个对象,该对象包含了Webpack的配置选项。以下是一个基本的配置示例:

const path = require('path');

module.exports = {
  entry: './src/index.js', // 入口文件
  output: {
    filename: 'bundle.js', // 输出文件名
    path: path.resolve(__dirname, 'dist') // 输出目录
  },
  module: {
    rules: [
      {
        test: /\.js$/, // 处理.js文件
        exclude: /node_modules/, // 排除node_modules目录
        use: {
          loader: 'babel-loader', // 使用babel-loader
          options: {
            presets: ['@babel/preset-env'] // 使用Babel预设
          }
        }
      }
    ]
  }
};

编写入口文件

在你的项目中创建一个入口文件,Webpack将从这个文件开始打包。例如,在src目录下创建index.js

// src/index.js
console.log('Hello, Webpack!');

运行Webpack

在配置文件设置完毕后,你可以通过以下命令来运行Webpack:

npx webpack --config webpack.config.js

这将使用你的配置文件来打包应用程序,并将输出文件生成到 dist 目录下。

自动化构建过程

为了自动化构建过程,你可以在 package.json 中添加一个脚本来运行 Webpack:

"scripts": {
  "build": "webpack --config webpack.config.js"
}

然后,你可以通过运行npm run build来启动构建过程。这就是 Webpack 安装和基本配置的步骤。根据你的项目需求,你可能还需要安装和配置其他 loaderplugin

模块和依赖管理

模块和依赖管理是 Webpack 的核心功能之一,它允许开发者将应用程序分解成多个小的、可重用的模块,然后由 Webpack 负责将这些模块及其依赖项打包成一个或多个 bundle。以下是模块和依赖管理的基本概念和步骤:

模块

Webpack 使用自己的模块解析算法来找到每个模块的依赖项。它支持多种模块格式,包括ES6、CommonJS、AMD等。

使用ES6模块ES6 引入了模块的原生语法,使用 importexport 关键字来处理模块的导入和导出:

// export.js
export const myFunction = () => {
  console.log('@@@');
};

// import.js
import { myFunction } from './export.js';
myFunction();

CommonJS模块:在 Node.js 中,CommonJS 是模块的另一种形式,使用 module.exportsrequire 来导出和导入模块:

// module.js
module.exports = function () {
  console.log('@@@');
};

// app.js
const myModule = require('./module');
myModule();

配置 Webpack 的 resolve

Webpack的resolve配置选项允许你控制模块如何被解析。你可以设置文件扩展名、别名(alias)等:

// webpack.config.js
module.exports = {
  // ...
  resolve: {
    extensions: ['.js', '.json'], // 指定Webpack应该使用哪些扩展名来解析模块
    alias: {
      utils: path.resolve(__dirname, 'src/utils/') // 设置别名,简化模块路径
    }
  }
};

依赖管理

Webpack本身只能处理JavaScript和JSON文件。对于其他类型的文件(如CSS、图片等),你需要使用相应的loader:

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/,
        type: 'asset/resource'
      }
    ]
  }
};

Loaders

在 Webpack 中,Loaders 是一种转换器,它们在模块被添加到依赖图之前对模块进行转换。Loaders 允许你在构建过程中预处理文件,例如将样式表、图片、字体等资源文件转换为合适的模块,或者将现代 JS 代码转换为浏览器兼容的格式。

配置 Loaders

在 Webpack 配置文件中,module.rules 数组用于定义规则,每个规则可以包含以下属性:

  • test:正则表达式,用于匹配文件路径。
  • use:指定loader或loader链。
  • includeexclude:指定包含和排除的目录。
  • options:传递给loader的选项。

通过合理配置Loaders,你可以使Webpack处理各种类型的文件,从而构建一个功能丰富的应用程序。

  1. 样式配置:使用 style-loadercss-loadersass-loaderless-loader
  2. 图片和字体配置:使用 file-loaderurl-loader
  3. ES6+配置:使用 babel-loader
  4. HTML文件配置:使用 html-loader
  5. JSON文件配置:使用 json-loader
  6. 缓存loader:使用 cache-loader

自定义 Loader

自定义Loader可以接受查询参数、source maps、访问 Loader 上下文 和 getOptions 获取配置。

  • 获取用户配置:Loader 可以通过调用 this.getOptions() 来获取 options 对象,该对象包含了用户在Webpack配置文件中为 Loader 设置的所有选项。
  • 灵活性和配置:用户可以根据不同场景或需求调整 Loader 的行为,而无需修改Loader本身的代码。
  • 简化配置:相比于通过查询参数传递选项,使用getOptions可以让配置更加清晰和结构化。
export default function (source, map, meta) {
  console.log('@@@ source', source);
  console.log('@@@ map', map);
  console.log('@@@ meta', meta);
  const options = this.getOptions();
  console.log('@@@ options', options);

  // 对资源进行一些转换
  return transformSource(source);
}

Plugins

Webpack 插件 Plugins 是扩展 Webpack 功能的重要方式,它们在 Webpack 构建流程中的特定时刻执行,能够执行范围广泛的任务,从打包优化、资源管理到环境变量注入等。以下是几个常用的 Webpack 插件及其功能概述:

  • HtmlWebpackPlugin:自动生成 HTML 文件,并插入编译后的 JS 和 CSS 文件。
  • CleanWebpackPlugin:在构建之前清理输出目录。
  • MiniCssExtractPlugin:将 CSS 提取到单独的文件中,而不是内联到 JS 中。
  • UglifyJsPlugin / TerserPlugin:压缩 JS 代码,移除无用代码,减小文件体积。
  • DefinePlugin:用于环境变量的配置,如区分开发环境和生产环境。
  • CopyWebpackPlugin:复制文件或目录到构建目录中,常用于静态资源如图片、字体文件的复制。
  • HotModuleReplacementPlugin:启用模块热替换 HMR,在开发时无需刷新整个页面即可更新修改过的模块。
  • BundleAnalyzerPlugin:可视化分析打包后资源的大小,帮助识别体积过大的模块。
  • SplitChunksPlugin:拆分代码,将公共代码提取到单独的 chunk 中,减少重复代码。
  • ProvidePlugin:自动加载某些模块,而无需显式 import

通过这些插件,你可以扩展Webpack的功能,优化构建过程,并提高开发和生产环境的效率。

自定义插件

自定义 Webpack 插件的应用场景广泛且多样,涵盖了前端项目构建和优化的多个方面。

自定义插件需要继承自 webpack.Plugin 类,并实现 apply 方法。apply 方法接收一个 compiler 对象作为参数,它是 Webpack 编译过程的核心。webpack 插件由以下组成:

  • 一个 JS 命名函数或 JS 类。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子。
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。
const webpack = require('webpack');

// 一个 JavaScript 类
class MyCustomPlugin {
  // 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
  apply(compiler) {
    // 指定一个挂载到 webpack 自身的事件钩子。
    compiler.hooks.emit.tapAsync(
      'MyCustomPlugin',
      (compilation, callback) => {
        console.log('这是一个示例插件!');
        console.log(
          '这里表示了资源的单次构建的 `compilation` 对象:',
          compilation
        );

        // 用 webpack 提供的插件 API 处理构建过程
        compilation.addModule(/* ... */);

        callback();
      }
    );
  }
}

module.exports = MyCustomPlugin;

模式(Mode)

在 Webpack 中,mode 是一个配置选项,用于指定构建的目标环境,这直接影响到 Webpack 的优化程度、代码分割策略、是否生成 source map 以及一些内置插件的行为。Webpack 主要有三种模式:

  1. development(开发模式):

    • 优化:在开发模式下,Webpack 的优化主要是为了提高开发效率,例如,它会使用 cheap-module-source-map 来生成source map,这有助于在浏览器中快速定位错误,但不会精确到每一行。
    • 速度:构建速度相对较快,因为不进行代码压缩和最小化处理。
    • 模块热替换(HMR, Hot Module Replacement):支持模块热替换,可以在不刷新页面的情况下替换、添加或删除模块,极大地提升了开发体验。
    • 警告:Webpack 会显示更详细的警告信息,帮助开发者在开发阶段发现并解决问题。
  2. production(生产模式):

    • 优化:此模式下,Webpack 会进行一系列性能优化,包括代码压缩、死代码消除(Tree Shaking)、最小化资源文件等,以减小最终输出文件的大小。
    • 速度:虽然生产构建比开发构建慢,因为它执行了更多的优化步骤,但这换来的是更小的文件体积和更快的加载速度。
    • source map:默认使用 production 的 source map,这种 source map 在体积上更小,但只提供行映射,适合生产环境下的错误追踪。
    • 安全性:某些插件(如 TerserPlugin)在生产模式下会开启额外的安全性选项,如禁用 eval()
  3. none(无模式):

    • 当没有明确指定mode时,Webpack 4及以上版本默认进入 production 模式,而 Webpack 3及以下版本则没有这一概念。
    • 在此模式下,Webpack 不会应用任何内置的环境相关的默认配置,意味着所有优化和特性都需要手动配置。

设置 mode 的方式很简单,在Webpack的配置文件中加入:

module.exports = {
  mode: 'production', // 或 'development' 或 'none'
  // 其他配置...
};

代码优化

减少第三方库的大小

使用工具如webpack-bundle-analyzer来分析包内容,移除不必要的第三方库代码。

CSS压缩

利用css-loaderminimize选项或MiniCssExtractPlugin来压缩CSS文件。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  plugins: [new MiniCssExtractPlugin()],
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
};

代码拆分和懒加载

Webpack 允许你将代码拆分成多个 bundle,并且可以实现懒加载,即按需加载模块:

// webpack.config.js
module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: 'all',
      // ... 
    }
  }
};

使用 ProvidePlugin 自动加载模块

ProvidePlugin 可以自动加载某些模块,而无需显式 import

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  // ...
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery'
    })
  ]
};

动态导入

Webpack 支持动态导入( Dynamic Imports ),这允许你在运行时根据需要加载模块:

// Some condition to load the module
if (someCondition) {
  import('./module.js').then(module => {
    module.default();
  });
}

Tree Shaking

利用 Tree Shaking 功能移除未使用的代码。确保代码模块使用ES6模块导出(export/import)。

利用缓存

通过为生成的文件名添加内容哈希(使用[contenthash]),可以利用浏览器缓存来提高加载速度。

使用Content Security Policy (CSP)

通过CSP减少XSS攻击的风险,并可能提高性能。

__webpack_nonce__ = 'c29tZSBjb29sIHN0cmluZyB3aWxsIHBvcCB1cCAxMjM=';

使用PreloadWebpackPlugin

这个插件可以帮助你预加载重要的模块,减少首屏加载时间。

const PreloadWebpackPlugin = require('preload-webpack-plugin');

module.exports = {
  // ... 其他配置 ...
  plugins: [
    new PreloadWebpackPlugin({
      rel: 'preload', // 可以是 'preload' 或 'prefetch'
      as: 'script', // 资源类型,可以是 'script' 或 'font' 等
      include: 'asyncChunks', // 预加载异步chunk
      fileBlacklist: [/\.(mp4|webm)$/] // 排除某些文件
    }),
    // ... 其他插件 ...
  ]
};

使用HappyPack或ThreadLoader

对于大型项目,使用多线程来加速构建过程。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          "thread-loader",
          // 耗时的 loader (例如 babel-loader)
        ],
      },
    ],
  },
};

高级配置

多入口与多出口配置

对于大型项目,可能需要从多个入口点开始构建,每个入口对应一个应用程序或库的不同部分。可以通过配置entry为对象或数组来实现:

module.exports = {
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

HASH

HASH 主要用于为输出的文件添加文件指纹( file fingerprinting ),以此来实现浏览器缓存的长期有效性,同时确保当文件内容发生变化时,浏览器能够获取到最新的文件。Webpack 提供了三种主要的哈希策略,分别是:hashchunkhashcontenthash。这些哈希值都是基于文件内容生成的 MD5 或其他哈希算法计算得出的,以确保文件内容的微小变化都会导致哈希值的显著不同。

  1. hash (全局哈希): hash 是基于整个打包配置生成的。这意味着,只要 Webpack 配置不变,无论哪个模块发生变化,所有输出的文件哈希值都会相同。这适用于那些不希望因单个文件变动而影响所有文件缓存的场景,但通常不是最佳实践,因为它降低了缓存的粒度。

  2. chunkhash (块哈希): chunkhash 是基于每个入口点(entry chunk)及其依赖生成的。当某个入口点或其直接依赖发生改变时,该入口点相关的所有输出文件的哈希值才会改变。这对于实现高效的长期缓存非常有用,因为每个独立的代码块可以独立更新,不会影响到未更改的代码块的缓存。

  3. contenthash (内容哈希): contenthash 是基于文件内容生成的,通常用于资源文件,如样式表、图片等。这意味着即使在相同的入口点下,如果资源内容变化,输出的文件名也会变化,提供最细粒度的缓存更新。这对于确保只有内容真正改变的资源被重新加载非常有用。

在配置中,你可以通过占位符(如[hash][chunkhash][contenthash])来使用这些哈希值:

module.exports = {
  output: {
    filename: '[name].[chunkhash].js', // 对于入口点相关的chunk
    chunkFilename: '[id].[contenthash].js', // 对于非入口点chunk,如动态导入的模块
    assetModuleFilename: '[name].[contenthash][ext]', // 对于资源文件
  },
}

附录