Webpack 详解 | 豆包MarsCode AI刷题

7 阅读7分钟

前言

在现代前端开发中,Webpack 已成为不可或缺的工具之一。它通过模块化打包的方式,将我们分散的代码、资源整合成高效的输出。然而,Webpack 的强大不仅体现在它的打包能力上,更多是通过灵活的 Loader 和 Plugin 机制,赋予开发者无限扩展的可能性。

在这篇文章中,我们将深入了解 Webpack 的 Loader 和 Plugin。我们将从它们的基本概念和工作原理入手,分析它们在构建流程中的作用。随后,我会手把手展示如何实现一个自定义的 HtmlWebpackPlugin,帮助你更好地理解 Webpack 插件的开发方式。

Webpack是什么?

概念:

官方文档定义: Webpack是一个用于现代 JavaScript 应用程序的 静态模块打包工具,基于 Node.js 开发出的前端工具。

我们来拆分一下这段话的重点:

  • 打包: 将多个分散的代码文件和资源整合成一个或多个文件的过程
  • 静态资源: 比如常见的 Css 文件、JS 、Images、字体文件、模板文件 。这一类属于都静态资源。

所以我们再来总结文档中定义的这段话:

Webpack 是一个用于 JS 应用程序中众多静态资源文件整合在一起的工具。

为什么要整合静态资源?

在网页中,如果静态资源过多,会产生一些性能以及依赖管理的问题:

  • 网页加载速度变慢
  • 处理复杂的依赖关系

所以,在我们开发大型的应用程序,一个合适的打包工具是必不可少,它可以帮助我们解决许多性能上、资源上、依赖上的许多问题。

Webpack基本使用

配置 Webpack 必不可缺的俩个概念:entryoutput ,接下来我们来展示这俩个概念分别如何作用。

entry

指定一个入口文件:

Webpack会从指定的这个入口文件开始,递归解析应用程序中的依赖模块,最后打包成一个或者多个 bundle.js 文件

entry 通常在 webpack.config.js 中配置,代码如下:

// webpack.config.js

module.exports = {
    entry: './src/index.js'
};

这里指定的入口文件为 src 目录下的 index.js 文件,不过 entry 默认值一般都为 src/index.js ,你可以自定义配置它的入口文件。

output

指定一个输出文件:

Wenpack 会指定一个打包后输出的文件,它定义打包后的文件输出在哪,以及输出的文件名称是什么。

output 也在 webpack.config.js 中配置,代码如下:

module.exports = {
    output: {
        filename: 'bundle.js', 
        path: __dirname + '/dist'  
    }
};

这里指定的输出文件在 当前路径下的 dist 目录下,输出的文件名称是 bundle.js,你也可以自定义配置它的输出文件。

Webpack的核心

Wevpack俩个核心概念:LoaderPlugin 是其强大灵活性的基础,它们分别用于处理文件转换和扩展 Webpack 的功能。

Loader

概念:

Webpack处理模块文件的转换器:例如将 TS 转换成 JS 文件,less 转换成 CSS 文件。

转换原理:

Webpack 默认只识别 JavaScript 和 JSON 文件,其他类型文件无法被打包,如:TS Scss CSS 文件等,Loader 通过转换这些文件,将它们转换成 Webpack 可以处理的模块,然后再去打包。

主要作用:

  • 模块解析前的预处理
  • 将一种文件类型转换成另一种文件类型

使用:

loader 在 Webpack 配置文件中的 module.rules 进行配置。 通过 test 属性指定要处理的文件类型,通过 use 属性指定要使用的 loader 转换器。

代码示例:

module.exports = {
  module: {
    rules: [

      {
        test: /\.js$/,
        use: 'babel-loader', // 将ES6+转换成ES5
      },

      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
        // style-loader 将 css 文件插入到 HTML 中的 <style> 标签中
        // css-loader 用于解析 CSS 文件中的 @import 和 url() 语句
      },

      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'], 
        // sass-loader 将 scss 文件转换成 css 文件
      },

      {
        test: /\.(png|jpg|gif)$/,
        use: 'file-loader', // 处理图像文件
      },
    ],
  },
};

Plugin

概念:

Plugin 是一个 Webpack 功能扩展器,在 Webpack 打包周期中执行一些复杂的操作。

工作原理:

Webpack 在构建项目时会触发一系列钩子,Plugin 可以挂载到这些钩子上,在特定期间执行特定的行为。

主要作用:

  • HtmlWebpackPlugin: 自动生成 HTML 文件,并将打包后的 JS 文件自动引入到 HTML 中。
  • CleanWebpackPlugin: 在每次构建前清理输出目录(如 dist/)。
  • MiniCssExtractPlugin: 将 CSS 提取到独立的文件中,实现更好的缓存和页面加载速度。

以上是一些常用插件的功能,用于更好的构建项目,优化项目。

使用:

Plugin 在 Webpack 配置文件中的 plugins 数组进行配置。

使用 new 操作符来生成构造函数。

代码示例:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }), // 在template指定路径下生成该html文件,并注入打包后的资源

    new CleanWebpackPlugin(), // 在每次构建之前清理输出目录

    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production'),
    }), // 定义环境变量
  ],
};

实现一个 htmlWebpackPlugin

通过上面的概念以及代码示例,我们基本已经了解了 Webpack 部分功能以及核心概念,那么我们将会实践,自己实现一个插件:htmlWebpackPlugin

配置要求:

首先是我们需要使用到的依赖:

"devDependencies": {
    "html-minifier": "^4.0.0",
    "webpack": "^5.93.0",
    "webpack-cli": "^5.1.4"
}

执行构建的命令:

"scripts": {
    "build": "webpack"
}

webpack.config.js 配置

const path = require('path');
 // 提前创建好一个 htmlWebpackPlugin.js 文件
const HtmlWebpackPlugin = require('./htmlWebpackPlugin')
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
     plugins: [
        new HtmlWebpackPlugin({
            // ... 这里是一些参数配置
        })
    ]
};

参数要求:

  • title: 用来指定 HTML 标题。
  • filename: 用来指定生成的 HTML 文件名称。
  • template: 用来指定 HTML 模板文件,根据该模板,插入打包后的 js 或 css 文件。
  • inject: 指定资源的插入位置, 或者 中。
  • meta: 指定在 HTML 中生成 标签。
  • minify: 配置生成的 HTML 文件中压缩和优化选项。

现在所有的参数以及罗列出来,我们可以去配置 htmlWebpackPlugin 的参数了。

代码示例:

const path = require('path');
 // 提前创建好一个 htmlWebpackPlugin.js 文件
const HtmlWebpackPlugin = require('./htmlWebpackPlugin')
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
     plugins: [
        new HtmlWebpackPlugin({
            title: 'My Webpack App',
            filename: 'index.html',
            template: 'src/template.html',
            inject: 'head',
            meta: {
                viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no',
                author: 'Shr',
            },
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true,
                removeEmptyAttributes: true,
                removeOptionalTags: true,
            },
        })
    ]
};

代码实现:

HtmlWebpackPlugin 的配置中,可以通过 minify 属性来设置 HTML 压缩选项,这些选项会传递给 html-minifier ,以控制生成 HTML 文件的压缩方式,所以我们现在对 minify 的处理需要引入 html-minifier 来处理。

const path = require('path')
const fs = require('fs')
const htmlMinifier = require('html-minifier').minify

大多数的 Webpack 插件都是定义一个类来实现的,我们在调用 htmlWebpackPlugin 时也是采用了 new 操作符来创建一个实例对象,所以我们这里来定义一个类:

class HtmlWebpackPlugin {
    // 这里接受options选项作为参数
    constructor(options) {
        this.options = options; // 参数包含标题/文件名
    }
}
module.exports = HtmlWebpackPlugin 

实现 apply 方法:apply 方法用于注册 Webpack 提供的钩子,在特定期间执行特定操作。

// 注册插件
apply(compiler) {
    // 指定一个挂载到 webpack 自身的事件钩子。
    compiler.hooks.emit.tapAsync(
      // 钩子名
      'SimpleHtmlWebpackPlugin',
      // 钩子函数
      (compilation, callback) => {
        // 调用generateHtml方法生成html内容
        // 传入compilation对象作为参数
        const html = this.generateHtml(compilation);
        // 生成html文件输出路径
        compilation.assets[this.options.filename || 'index.html'] = {
          source: () => html, // 返回html内容
          size: () => html.length // 返回html长度
        };
        // 使用tapAsync方法绑定插件,必须调用callback指定回调函数
        // 表示处理完成
        callback();
      });
}

实现 generateHtml :生成 html 内容

generateHtml(compilation) {
    let html
    // template:用于文件系统读取模板文件/templateContent:直接从参数值中提供HTML模板内容
    // 这里用文件中template模板
    if (this.options.templateContent) {
      html = this.options.templateContent;
    } else if (this.options.template) {
      const templatePath = path.resolve(this.options.template);
      html = fs.readFileSync(templatePath, 'utf-8');
    } else {
      html = this.defaultTemplate();
    }
    // title
    if (this.options.title) {
      html = html.replace('<title>', `<title>${this.options.title}`)
    }
    const assets = Object.keys(compilation.assets); // 输出所有文件名称
    const scripts = assets.map(asset => `<script src="${asset}"></script>`).join('\n');
    // inject
    if (this.options.inject === 'body') {
      html = html.replace('</body>', `</body>\n${scripts}`)
    } else if (this.options.inject === 'head') {
      html = html.replace('</head>', `${scripts}\n</head>`)
    }
    // meta
    if (this.options.meta) {
      const metaTag = Object.entries(this.options.meta)
        .map(([name, content]) => `<meta name="${name}" content="${content}">`).join('\n')
      html = html.replace('<head>', `<head>\n${metaTag}`)
    }
    // minify
    if (this.options.minify) {
      html = htmlMinifier(html, this.options.minify)
    }
    return html
}

完整代码:

const path = require('path')
const fs = require('fs')
const htmlMinifier = require('html-minifier').minify
// js类
class HtmlWebpackPlugin {
  // 这里接受options选项作为参数
  constructor(options) {
    this.options = options; // 参数包含标题/文件名
  }
  // 注册插件
  // 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
  apply(compiler) {
    // 指定一个挂载到 webpack 自身的事件钩子。
    compiler.hooks.emit.tapAsync(
      // 钩子名
      'SimpleHtmlWebpackPlugin',
      // 钩子函数
      (compilation, callback) => {
        // 调用generateHtml方法生成html内容
        // 传入compilation对象作为参数
        const html = this.generateHtml(compilation);
        // 生成html文件输出路径
        compilation.assets[this.options.filename || 'index.html'] = {
          source: () => html, // 返回html内容
          size: () => html.length // 返回html长度
        };
        // 使用tapAsync方法绑定插件,必须调用callback指定回调函数
        // 表示处理完成
        callback();
      });
  }

  generateHtml(compilation) {
    let html
    // template:用于文件系统读取模板文件/templateContent:直接从参数值中提供HTML模板内容
    // 这里用文件中template模板
    if (this.options.templateContent) {
      html = this.options.templateContent;
    } else if (this.options.template) {
      const templatePath = path.resolve(this.options.template);
      html = fs.readFileSync(templatePath, 'utf-8');
    } else {
      html = this.defaultTemplate();
    }
    // title
    if (this.options.title) {
      html = html.replace('<title>', `<title>${this.options.title}`)
    }
    const assets = Object.keys(compilation.assets); // 输出所有文件名称
    const scripts = assets.map(asset => `<script src="${asset}"></script>`).join('\n');
    // inject
    if (this.options.inject === 'body') {
      html = html.replace('</body>', `</body>\n${scripts}`)
    } else if (this.options.inject === 'head') {
      html = html.replace('</head>', `${scripts}\n</head>`)
    }
    // meta
    if (this.options.meta) {
      const metaTag = Object.entries(this.options.meta)
        .map(([name, content]) => `<meta name="${name}" content="${content}">`).join('\n')
      html = html.replace('<head>', `<head>\n${metaTag}`)
    }
    // minify
    if (this.options.minify) {
      html = htmlMinifier(html, this.options.minify)
    }
    return html
  }
}
module.exports = HtmlWebpackPlugin;

现在我们的 htmlWebpackPlugin 就已经实现了,有大多数参数项还没有实现,这只是一个基本的例子。