浅谈一下 webpack 以及 loader 和 plugin

话说,前端练习时长也快两年了,但是关于 webpack 的东西好像也没怎么研究过

一是没有这方面的需求:回想一下,关于 webpack 的配置相关工作,也就只有自己配置过一次 loader「使用 svg-sprite-loader、svgo-loader 优化 svg symbols」,还是摸着石头过河;

二是大部分的配置工作脚手架都已经做好了,这很可能导致一个问题,就是别人问你 webpack 相关的知识的时候,阿巴阿巴阿巴... ️

确实,大多数情况下,前端开发人员可能不需要深入了解 webpack,但了解 webpack 的基本概念和用法对于前端开发仍然是很有益的。话不多说,开搞!️

1. webpack

先让我们 一下 webpack 官网 的解释:

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

怎么感觉所有框架或工具的官网定义都不是那么的通俗易懂?

通俗点讲,就是当我们开发应用时,无论你用什么框架也好,都会在项目内部有一个入口文件,比如 Vue 和 React 项目的 `main.ts` 文件,其他模块的代码一般会分散在多个文件中,这些文件可能包含不同的功能、库或模块。为了能在浏览器中运行这些代码,我们需要将它们打包成一个或多个文件,比如我们平时打包出来的 `dist` 或 `build` 目录,这就是 webpack 的作用 ‍♂️

浅谈一下 webpack 以及 loader 和 plugin_第1张图片

webpack 的主要功能包括:

  1. 模块打包:webpack 将应用程序的各个模块作为输入,通过解析模块之间的依赖关系,将它们打包成一个或多个静态资源文件。➡️ `pnpm run build`

  2. 资源转换:webpack 支持加载各种类型的文件,并且可以通过加载器(Loaders)对它们进行转换。比如,可以使用 Babel-loader 将 ES6/ES7 的 JavaScript 代码转换为浏览器可识别的 ES5 代码。➡️ loader 加载器

  3. 插件系统:webpack 提供了丰富的插件系统,开发者可以使用插件来扩展和定制打包过程。比如,可以使用 UglifyJS 插件来压缩 JavaScript 代码,或者使用 HtmlWebpackPlugin 插件生成 HTML 文件。➡️ plugin 插件

此外,webpack 还提供了许多优化功能,如代码压缩、代码拆分、懒加载等,以优化应用性能

2. loader

loader,顾名思义,加载器。

webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱即用自带的能力。loader 让 webpack 拥有能够去处理其他类型的文件都能力,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。比如刚才提到的,可以使用 Babel-loader 将 ES6/ES7 的 JavaScript 代码转换为浏览器可识别的 ES5 代码。

一句话概括:loader 就是协助 webpack 打包处理特定的文件模块。

在更高层面,在 webpack 的配置中,loader 有两个属性:

  1. test 属性,识别出哪些文件会被转换。
  2. use 属性,定义出在进行转换时,应该使用哪个 loader。
// webpack.config.js
const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js',
  },
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
};

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息:

“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先 use(使用) raw-loader 转换一下。”

是不是还挺简单的?去看文档!️

下面简单看一下 webpack 常见的 loader

2.1 babel-loader

作用:将高级 JS 语法转化成低级语法 → 才能运行在 IE

webpack 只能打包处理部分高级 JS 语法,对于无法处理的需借助 babel-loader 打包,比如:

class Person {
    // 通过 static 关键字,为 Person 类定义了一个静态属性 info
    // webpack 无法打包处理“静态属性”这个高级语法
    static info = 'person info'
}

// 安装 babel-loader 相关的包
npm i babel-loader @babel/core @babel/plugin-proposal-class-properties

// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: [
	rules: [
		{
			test: /.js$/,            // 匹配的文件类型
			exclude: /node_modules/, // 排除项
			use: {                   // 对应要调用的loader
				loader: "babel-loader",
				options: { // 参数项
					// 声明一个babel插件,此插件用来转化class中的高级语法
					plugins:['@babel/plugin-proposal-class-properties']
				}
			}
		}
	]
]

2.2 ts-loader

作用:把 TS 转变成 JS,并提示类型错误

// 安装
// npm install ts-loader typescript --save-dev

// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
	rules: [
		// `.ts/.cts/.mts/.tsx` extension files will be handled by `ts-loader`
		{ test: /\.([cm]?ts|tsx)$/, loader: "ts-loader" }
	]
}

2.3 less/sass-loader、postcss-loader、css-loader、style-loader

  • less/sass-loader: 将 less/sass 转化成 css
  • postcss-loader: 优化 css (如:加前缀) → 最好放 css-loader 之前
  • css-loader: 将 css 转化成 JS 字符串
  • style-loader: 将 JS 字符串转化成 style 标签
// 安装 css 相关的 loader 的包
npm install style-loader css-loader less/sass-loader less/sass -D

// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
	rules: [
		{
			test : /.css$/,
			use : ['style-loader', 'css-loader', 'postcss-loader', 'less/sass-loader']
		} // 多个 loader 的调用顺序是:从后往前调用
	]
}

2.4 url-loader、file-loader

  • file-loader:一个简单的文件加载器,它会将源文件复制到输出目录,并返回文件的最终路径。它通常用于处理像图片、字体等文件类型,可以将这些文件复制到输出目录,并根据需要生成正确的 URL 地址供应用程序使用。

例如,在 webpack 配置中使用 file-loader 处理图片文件:

// 安装相关的 loader 的包
npm i file-loader

// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
  rules: [
    {
      test: /\.(png|jpg|gif)$/,
      use: [
        {
          loader: 'file-loader',
          options: {
            name: '[name].[hash].[ext]', // 输出文件名的格式
            outputPath: 'images/' // 输出文件的目录
          }
        }
      ]
    }
  ]
}

这个配置会将匹配到的图片文件复制到输出目录中的 images/ 目录,并生成一个对应的文件名。

  • url-loader:基于 file-loader 的封装,并增加了一些额外的功能。它可以根据文件大小将文件转换为 Data URL 或将其保留为文件,并返回相应的 URL 地址。这样做的好处是,对于小文件,可以将其转换为 Data URL,避免额外的网络请求,而对于大文件,则可以保留为文件。
// 安装相关的 loader 的包
npm i url-loader

// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
	rules: [
		{
			test : /.jpg|png|gif$/,
			use : { // 带参数项的 loader 可以通过对象的方式进行配置
				loader: "url-loader",
				options: {
                    limit: 10240, // limit 指定图片的大小,单位是字节(byte)
                    name: '[name].[hash].[ext]', // 输出文件名的格式
                    outputPath: 'images/' // 输出文件的目录
                }
			} // 只有 <= limit大小的图片,才会被转为 base64格式的图片
		}   // 配了 url-loader 在配置里面就不要再给图片配 file-loader 了
	]     // 因为 url-loader 默认会使用 file-loader 来处理图片的路径关系的
}

2.5 svg-sprite-loader、svgo-loader

  • svg-sprite-loader:官方解释是:一个用于创建 svg 雪碧图的 Webpack 加载器。这个加载器现在已经被 JetBrains 公司收录和维护了。通俗的讲:svg-sprite-loader 会把你引入的 svg 塞到一个个 symbol 中,合成一个大的 svg,最后将这个大的 svg 放入 body 中。symbol 的 id 如果不特别指定,就是你的文件名。
  • svgo-loader:是基于 SVG Optimizer 的一个加载器,而 SVG Optimizer 是一个基于node.js 的工具,用于优化 SVG 矢量图形文件,它可以删除和修改SVG元素,折叠内容,移动属性等。
// 安装相关的 loader 的包
npm i svg-sprite-loader svgo-loader --dev

// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
	rules: [
		{
			test : /\.svg$/,
			use : [
                { loader: 'svg-sprite-loader', options: {} },
                { loader: 'svgo-loader', options: {
                    plugins: [{
                        name: 'removeAttrs', // 必须指定name!
                        params: {attrs: 'fill'}
                    }]
                }
            ]
		}
	]
}

ps:对这个 loader 感兴趣的话可以参考 使用 svg-sprite-loader、svgo-loader 优化 svg symbols

3. plugin

plugin,顾名思义,插件。

通过安装和配置第三方插件,可以扩展 webpack 的能力,从而让 webpack 用起来更方便。

loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。

一句话概括:plugin 是用于扩展和定制 webpack  功能的工具。没用过浏览器插件吗?‍♂️

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建一个插件实例。

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件

module.exports = {
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
  plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};

在上面的示例中,html-webpack-plugin 为应用程序生成一个 HTML 文件,并自动将生成的所有 bundle 注入到此文件中。

是不是还挺简单的?去看文档!️

下面简单看一下 webpack 常见的 plugin

一些常用的 Webpack 插件:

  1. HtmlWebpackPlugin:用于生成 HTML 文件,并将打包生成的资源文件自动注入到 HTML 文件中。

  2. MiniCssExtractPlugin:用于将 CSS 代码提取为独立的文件,而不是内联到 JavaScript 文件中。

  3. CleanWebpackPlugin:用于清理输出目录中的旧文件,以便在每次构建之前保持输出目录的干净。

  4. OptimizeCSSAssetsPlugin:用于优化和压缩 CSS 代码。

  5. DefinePlugin:用于定义全局常量,可以在应用程序的代码中直接使用。

可以根据官网给出的步骤配置插件:

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

module.exports = {
  entry: './src/index.js',
  output: {
    path: 'dist',
    filename: 'bundle.js'
  },
  module: { rules: [ /* 添加 Loader 的规则 */ ] },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new MiniCssExtractPlugin({
      filename: 'styles.css'
    }),
    new CleanWebpackPlugin()
  ]
};

4. loader vs plugin

如果看完上面的解释,还是不知道 webpack 的 loader 和 plugin 的区别的话,那我们举个

假如你是一名厨师,你有一些食材(模块文件)需要处理,并且需要一些工具来做完这道菜。

Loader 就像你的各种厨房工具。例如,切菜刀、搅拌器、炉灶等,这些工具帮助你对食材进行加工和转换,以便制作出美味的菜肴。在 webpack 中 loader 的作用也是一样的,它们负责将不同类型的文件进行处理和转换,比如:将 ES6 代码转换为 ES5 代码,将 CSS 文件转换为浏览器可识别的样式等。

Plugin 则像你的特殊调料和烹饪技巧。假设你想给菜肴增添特殊的风味或实现特定的效果。你可能会使用辣椒酱增加辣味,柠檬汁增添酸味,或者使用烘烤技巧让菜表面金黄酥脆。在 webpack 中 plugin 的作用也是一样的,它们可以在构建过程中监听事件,并执行一些特殊的操作。例如,你可以使用 HtmlWebpackPlugin 生成一个带有引入资源的 HTML 文件,使用 UglifyJSPlugin 压缩和混淆 JavaScript 代码,或者使用 ExtractTextPlugin 将 CSS 提取为独立的文件。

总结来说,loader 是用于处理和转换文件的工具,类似于厨房中的各种工具,而 plugin 则是用于扩展和定制构建过程的工具,类似于特殊的调料和烹饪技巧。它们共同协作,使得 Webpack 能够处理各种文件类型、进行模块化开发,并通过插件机制进行灵活的定制和优化。

5. 自己写一个 plugin

Webpack 插件就是一个 JavaScript 对象,通过扩展或修改 webpack 的功能来实现特定的构建需求。它可以在 webpack 的构建过程中干预并做出相应的处理。基本的 webpack 插件结构如下:

class MyPlugin {
  constructor(options) {
    // 在构造函数中接收插件的配置选项
    this.options = options;
  }

  apply(compiler) {
    // 在 apply 方法中定义插件的逻辑
    // compiler 对象代表了完整的 webpack 环境配置
    // 可以通过 compiler 对象来访问 webpack 的各种钩子函数

    // 注册钩子函数,以在 webpack 构建过程中执行特定操作
    compiler.hooks.someHook.tap('MyPlugin', () => {
      // 在这里执行你的插件逻辑
    });
  }
}

这是一个最基本的 webpack 插件结构示例,webpack 插件的结构包括一个 apply 方法和一些钩子函数。apply 方法在插件被应用时被调用,接受一个 compiler 参数,该参数代表了完整的 webpack 环境配置。通过 compiler 对象,插件可以访问 webpack 的各种钩子函数并注册自己的逻辑。

钩子函数是 webpack 在构建过程中的特定时间点触发的函数。插件可以根据需求选择合适的钩子函数,并在这些函数中执行自定义的逻辑。例如,在构建开始前可以使用 beforeRunrun 钩子,在构建完成后可以使用 done 钩子。

下面是一些常用的 webpack 钩子函数:

  • beforeRun:在 webpack 构建启动之前执行。
  • run:在开始构建之前执行。
  • beforeCompile:在编译之前执行。
  • compile:在开始编译之前执行。
  • compilation:在每次新的编译创建之前执行。
  • emit:在生成资源并输出到输出目录之前执行。
  • afterEmit:在资源输出到输出目录之后执行。
  • done:在构建完成时执行。
  • 不止这些钩子吧?去看文档!

插件可以使用这些钩子函数来执行各种任务,如修改、添加、删除资源,生成额外的文件,提取公共代码,优化输出等等。

OK,举个

假如我们比较关心项目在构建完成后产出的文件的路径和大小,go!

const fs = require('fs');

class MyPlugin {
  constructor(options) {
    this.options = options;
  }

  apply(compiler) { // 构建时只会执行一次
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      const outputPath = stats.compilation.outputOptions.path;
      const outputFileName = stats.compilation.outputOptions.filename;

      const filePath = `${outputPath}/${outputFileName}`;
      const fileSize = fs.statSync(filePath).size;

      console.log(`Built file: ${filePath}`);
      console.log(`File size: ${fileSize} bytes`);
    });
  }
}

在上述示例中,我们在 webpack 构建完成后的 done 钩子中获取构建输出文件的路径和大小,并将其输出到控制台。

为什么说 apply 只会执行一次?

我在实践手写这个插件的时候,为了调试,在 apply 内部写了一个日志 log,我发现日志仅在第一次编译的时候可以执行,在热更新的时候,日志并没有打印,这是怎么回事?

还是刚才那个例子,apply 方法相当于你炒菜的点火动作,具体到了什么时机以及我们需要具体做什么,是需要提前注册具体的逻辑的。这里为了方便记忆,你也可以类比成 dom 的 addEventListener,具体的注册动作只发生一次,但是监听到事件之后是每次都会执行的!

最后,要使用此插件,还需要在 webpack 配置文件中引入并实例化它

const MyPlugin = require('./path/to/MyPlugin');

module.exports = {
  // ...其他配置项
  plugins: [
    new MyPlugin({
      // 插件的配置选项
    })
  ]
};

end

你可能感兴趣的:(webpack,前端,javascript)