likes
comments
collection
share

webpack - 手写一个plugin

作者站长头像
站长
· 阅读数 17

得不得奖的无所谓希望能强迫自己闯一关╮( ̄▽ ̄)╭,上次更文未通关,这次继续

前言

记录 实现webpack plugin 的学习总结 有误请多多指正,附上女神图保命 [手动狗头]

学习已完成

  • 1.什么是Plugin
  • 2.编写自定义plugin
  • 3.使用编写好的自定义plugin
  • 4.开发中常用的plugin介绍与使用

什么是Plugin

Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果,Plugin 的出现就是为了丰富 Webpack 的 API。

一个最基础的 Plugin 的代码是这样的:

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){}

  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    // 指定一个挂载到 compilation 的钩子,回调函数的参数为 compilation 。
    compiler.hooks.compilation.tap('BasicPlugin', (compilation) => {
      // 现在可以通过 compilation 对象绑定各种钩子
      compilation.hooks.optimize.tap('BasicPlugin', () => {
        console.log('资源已经优化完毕。');
      });
    });
  }
}

// 导出 Plugin
module.exports = BasicPlugin;

使用这个 Plugin 时,配置如下:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins:[
    new BasicPlugin({name:'tywd'}),
  ]
}

Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

这就是 Plugin 的工作原理,实际开发中还有很多细节,继续往下看。

编写自定义plugin

webpack 官方编写 plugin 介绍

plugin基本结构

一个最基本的 plugin 需要包含这些部分,在开发插件时需要注意:

  • 一个 JavaScript 类
  • 一个 apply 方法,apply 方法在 webpack 装载这个插件的时候被调用,并且会传入 compiler 对象。只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
  • 使用不同的 webpack 提供的 hooks 来指定自己需要发生的处理行为
  • 在异步调用时,异步的事件会附带两个参数,第二个参数为回调函数 callback,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。或者需要通过 return Promise 的方式。在下面会介绍 tapAsync 和 tapPromise

传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。

Compiler 和 Compilation

在开发 Plugin 时最常用的也是最重要的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;上面提到的 apply 方法传入的参数就是它。 在为 webpack 开发插件时,你可能需要知道每个钩子函数是在哪里调用的。想要了解这些内容,请在 webpack 源码中搜索 hooks.<hook name>.call

  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

Compiler 和 Compilation 提供了非常多的钩子供我们使用,这些方法的组合可以让我们在构建过程的不同时间获取不同的内容,具体可查看官方中文文档

webpack/api/compiler-hooks

webpack/api/compilation-hooks

事件流机制

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。

webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable。

Webpack 的 Tapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条 webapck 机制中,去改变 webapck 的运作,使得整个系统扩展性良好。

Tapable 也是一个小型的 library,是 Webpack 的一个核心工具。类似于 node 中的 events 库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。 Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件

//  广播事件
compiler.apply('event-name', params)
compilation.apply('event-name', params)

// 监听事件
compiler.plugin('event-name', function (params) {})
compilation.plugin('event-name', function (params) {})

同步与异步

plugin 的 hooks 是有同步和异步区分的 在同步的情况下,上面笔者使用 <hookName>.tap 的方式进行调用 而在异步 hook 内我们可以进行一些异步操作,并且有异步操作的情况下,请使用 tapAsync 或者 tapPromise 方法来告知 webpack 这里的内容是异步的

tapAsync

需要多传一个回调

class HelloPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('HelloPlugin', (compilation, callback) => {
      setTimeout(() => {
        console.log('async')
        callback()
      }, 1000)
    })
  }
}
module.exports = HelloPlugin

tapPromise

需要返回一个 Promise 对象并且让它在结束的时候 resolve

class HelloPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise('HelloPlugin', (compilation) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log('async')
          resolve()
        }, 1000)
      })
    })
  }
}
module.exports = HelloPlugin

编写 plugin 常用 API 参考

参考 github.com/tywd/webpac… 下的的构建

github.com/tywd/webpac… 查看如何使用

判断 Webpack 使用了哪些插件

具体代码参考 custom-plugins/basic-plugin.js hasHtmlWebpackPlugin 方法

compiler 事件钩子 done 和 failed

具体代码参考,代码中有详细的注释 custom-plugins/end-webpack-plugin.js

class EndWebpackPlugin {
    // 在构造函数中获取用户给该插件传入的配置
    constructor(doneCallback, failCallback) {
        // 存下在构造函数中传入的回调函数
        this.doneCallback = doneCallback;
        this.failCallback = failCallback;
    }

    apply(compiler) {
        compiler.hooks.done.tap('EndWebpackPlugin', (stats) => {
            this.doneCallback(stats); // 在 done 事件中回调 doneCallback
        })
        compiler.hooks.failed.tap('EndWebpackPlugin', (err) => {
            this.failCallback(err); // 在 failed 事件中回调 failCallback
        })
    }
}
// 导出 Plugin
module.exports = EndWebpackPlugin;

compiler 的 compilation 事件钩子 processAssets 和 emitAsset

具体代码参考,代码中有详细的注释 custom-plugins/filelist-plugin.js

// 一个简单的示例插件,生成一个叫做 assets.md 的新文件;文件内容是所有构建生成的文件的列表
// 参考 https://webpack.docschina.org/contribute/writing-a-plugin/#creating-a-plugin
class FileListPlugin {
    static defaultOptions = {
        outputFile: 'assets.md', // 输出的md文件名
    };
    // 需要传入自定义插件构造函数的任意选项
    //(这是自定义插件的公开API)
    constructor(options = {}) {
        // 在应用默认选项前,先应用用户指定选项
        // 合并后的选项暴露给插件方法
        // 记得在这里校验所有选项 可参考 basic-plugin.js 使用 validate 方法
        this.options = {
            ...FileListPlugin.defaultOptions,
            ...options
        };
    }
    apply(compiler) {
        const pluginName = FileListPlugin.name;
        const { webpack } = compiler;
        const { Compilation } = webpack;
        const { RawSource } = webpack.sources;
        compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
            compilation.hooks.processAssets.tap(
                {
                  name: pluginName,
                  stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
                },
                (assets) => {
                  const content =
                    '# In this build:\n\n' +
                    Object.keys(assets)
                      .map((filename) => `- ${filename}`)
                      .join('\n');
                      
                  compilation.emitAsset(
                    this.options.outputFile,
                    new RawSource(content)
                  );
                }
              );
        })
    }
}

module.exports = FileListPlugin

使用编写好的自定义plugin

参考上面 什么是Plugin

开发中常用的plugin

1. html-webpack-plugin

将一个页面模板打包到dist目录下,默认都是自动引入js or css

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>首页</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            template: './index.html',  // 以本地的index.html文件为基础模板
            filename: "index.html",  // 输出到dist目录下的文件名称
        }),
    ]
}

2. clean-webpack-plugin

用于每次打包dist目录删除

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
    plugins: [
        new CleanWebpackPlugin()
    ]
}

3. copy-webpack-plugin

用于将文件拷贝到某个目录下

const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
    plugins: [
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: "./main.js",
                    to: __dirname + "/dist/js",
                    toType: "dir"
                }
            ]
        })
    ]
}

上面配置中,将main.js拷贝到dist目录下的js里,toType默认是file,也可以设置为dir,因为我这dist目录下没有js目录。

4. mini-css-extract-plugin

都是将css样式提取出来,需要配合 css-loader

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                   MiniCssExtractPlugin.loader,
                   "css-loader"
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: "css/[name].css",
            chunkFilename: "css/[name].css"
        })
    ]
}

5. optimize-css-assets-webpack-plugin

用于压缩css样式

const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin")
module.exports = {
    plugins: [
        new OptimizeCssAssetsWebpackPlugin(),
    ]
}

6. webpack.HotModuleReplacementPlugin

开启热模块更新,无需安装,webpack内置

const Webpack = require("webpack")
module.exports = {
    plugins: [
        new Webpack.HotModuleReplacementPlugin()
    ]
}

8. webpack.DefinePlugin

用于注入全局变量,一般用在环境变量上。无需安装,webpack内置

const Webpack = require("webpack")
module.exports = {
    plugins: [
        new Webpack.DefinePlugin({
          STR: JSON.stringify("蛙人"),
          "process.env": JSON.stringify("dev"),
          name: "蛙人"
        })
    ]
}

上面配置中,DefinePlugin接收一个对象,里面的key值对应一个value值,这个value值是一个代码片段,可以看上面name那个,会报错 蛙人 is not defined,这里需要注意,value值必须是一个变量或代码片段。

9. webpack.ProvidePlugin

用于定义全局变量,如100个页面都引入vue,每个页面都引入只会增加工作量,直接在webpackProvide挂载一个变量就行,不用再去一一引入。无需安装,webpack内置

const Webpack = require("webpack")
module.exports = {
    plugins: [
        new Webpack.ProvidePlugin({
            "Vue": ["vue", "default"] 
        })
    ]
}

上面配置中,ProvidePlugin接收一个对象,key值是使用的变量,value值第一个参数是Vue模块,第二个参数默认取Es Module.default的属性。import默认引入进来是一个 Es Module的对象,里面有default这个属性就是实体对象

10. webpack.SplitChunksPlugin

以下两插件均为 Webpack 内置,无需安装。

  • webpack4.0之前使用 webpack.optimize.CommonsChunkPlugin
  • webpack4.0之后使用 optimization.SplitChunks

optimization.SplitChunks 配置

// main.js
import Vue from "vue"
console.log(Vue)
import("./news")
// news.js
import Vue from "vue"
console.log(Vue)
// webpack.config.js
module.exports = {
    mode: "development",
    entry: {
        main: "./main.js"
    },
    output: {
        filename: "[name].js",
        path: __dirname + "/dist"
    },
    optimization: {
        splitChunks: {
            chunks: "all" // all是对所有的chunk都生效,默认只对async异步有效。
        }
    },
}

splitChunks默认情况下也有自动提取,默认要求如下:

  • 被提取的模块来自node_module目录
  • 模块大于30kb
  • 按需加载时请求资源最大值小于等于5
  • 首次加载时并行请求最大值小于等于3

11. webpack.IgnorePlugin

用于过滤打包文件,减少打包体积大小。无需安装,webpack内置

const Webpack = require("webpack")
module.exports = {
    plugins: [
        new Webpack.IgnorePlugin(/.\/lib/, /element-ui/)
    ]
}

12. uglifyjs-webpack-plugin

用于压缩js文件,针对webpack4版本以上。

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
	optimization: {
        minimizer: [
            new UglifyJsPlugin({
                test: /\.js(\?.*)?$/i,
                exclude: /node_modules/
            })
        ]
    }
}

13. imagemin-webpack-plugin

图片压缩

const ImageminPlugin =  require('imagemin-webpack-plugin').default
module.exports = {
    plugins: [
        new ImageminPlugin({
             test: /\.(jpe?g|png|gif|svg)$/i 
        })
    ]
}

这个插件最好使用 cnpm i -D imagemin-webpack-plugin, npm 好像会有些依赖拉不到,以至于运行时出错

14. VueLoaderPlugin

Vue文件转换

const { VueLoaderPlugin} = require('vue-loader'); // 来自于vue-loader
module.exports = {
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
    ]
}

15. webpack-bundle-analyzer

打包分析

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
    plugins: [
      new BundleAnalyzerPlugin({
        analyzerPort: 9091,
        generateStatsFile: false
      })
    ]
}

15. friendly-errors-webpack-plugin

美化控制台,良好的提示错误。终端打印较美观

const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
module.exports = {
	plugins: [
		new FriendlyErrorsWebpackPlugin({
			compilationSuccessInfo: {
          notes: ['蛙人你好,系统正运行在http://localhost:' + devServer.port]
      },
      clearConsole: true,
		})
	],
}

写在最后

参考文章

# 深入浅出的webpack

代码地址

github.com/tywd/webpac…

以上的方式总结只是自己学习总结,有其他方式欢迎各位大佬评论 渣渣一个,欢迎各路大神多多指正,不求赞,只求监督指正( ̄. ̄) 有关文章经常被面试问到可以帮忙留下言,小弟也能补充完善完善一起交流学习,感谢各位大佬(~ ̄▽ ̄)~