webpack plugin原理以及自定义plugin

通过插件我们可以拓展webpack,加入自定义的构建行为,使webpack可以执行更广泛的任务。

plugin工作原理:

webpack工作就像是生产流水线,要通过一系列处理流程后才能将源文件转为输出结果,在不同阶段做不同的事,插件就是通过插入在某个阶段,执行一些特定的事情。

webpack通过tapable来组织这条复杂的生产线,webpack在执行的过程中会广播事件,插件只需要监听它关心的事件,就能加入到这条生产线中,改变运作。

代码角度来说:webpack在编译的过程中,会触发一系列的tapable钩子事件,插件要做的就是找到对应的钩子,往上面挂自己的任务,也就是注册事件,webpack在构建的时候,就会一起触发注册的事件。

webpack内部钩子函数

钩子函数的本质就是事件,为了方便我们直接接入和控制编译过程,webpack把编译过程中触发的各类关键事件封装成事件接口暴露了出来,这些就是钩子。

Tapable

为webpack提供了统一的插件接口,类型定义,是webpack的核心功能库。webpack目前有十种ooks。Tapable统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

tap:可以注册同步钩子和异步钩子

tapAsync:回调方式注册异步钩子

tapPromise:Promise方式注册异步钩子

plugin构建对象

Compiler 对象中保存着完整的webpack环境配置,每次启动webpack构件时,都只创建一次的对象,我们可以通过compiler获取到ebpack的主环境配置,比如:loader,plugin等。主要有以下属性(compiler 钩子 | webpack 中文文档 | webpack 中文文档 | webpack 中文网):

compiler.options 可以访问本次启动webpack时所有的配置文件,包括但不限于loader,entry,output,plugin等等完整配置信息。

compiler.inputFileSystem compiler.outputFileSystem 可进行文件操作,类似于node中fs模块。

compiler.hooks 可以注册Tapable的不同种类Hook,从而可以再compiler生命周期中植入不同的逻辑。

Compilation 对象代表一次资源的构建,compilation实例可以访问所有的模块以及他们的依赖。

一个compilation会构建依赖图中所有的模块进行编译。在编译阶段,模块会被加载(load),封存(seal),优化(optimize),分块(split),哈希(hash)和重新构建(restore)。主要有以下属性(compilation 钩子 | webpack 中文文档 | webpack 中文文档 | webpack 中文网):

compilation.modules 可以访问所有模块,打包的每一个文件都是一个模块。

compilation.chunks chunks即是多个modules组成而来的一个代码块。入口文件引入的资源组成一个chunk,通过代码分割的模块又是另外的chunk。

compilation.assets 可以访问本次打包生成所有文件的结果。

compilation.hooks 可以注册Tapable的不同种类Hook,用于在compilation编译模块阶段进行逻辑添加以及修改。

写法如下:

//1.webpack加载webpack.config.js中所有配置,此时就会new TestPlugin(),执行插件的constructor
//2.创建compiler对象
//3.遍历所有plugins中插件,调用插件的apply方法,
//4.再去执行剩下的编译流程(触发各个hooks事件)
class TestPlugin{
    constructor(){}
    apply(compiler){
        //文档可知environment是同步,所以使用tap注册
        compiler.hooks.environment.tap("TestPlugin",()=>{

        })
        //emit是异步串行,按照顺序执行完才往下走
        compiler.hooks.emit.tap("TestPlugin",(compilation)=>{
        })
        compiler.hooks.emit.tapAsync("TestPlugin",(compilation,callback)=>{
            setTimeout(()=>{
                callback() //callback执行后才继续往下走
            },1000)
        })
        compiler.hooks.emit.tapPromise("TestPlugin",(compilation)=>{
            return new Promise((resolve,reject)=>{
                setTimeout(()=>{
                    resolve()
                },1000)
            })
        })
        //make是异步并行
        compiler.hooks.make.tapAsync("TestPlugin",(compilation,callback)=>{
            //compilation钩子函数要在make钩子里触发
            compilation.hooks.seal.tap("TestPlugin",()=>{

            })
            setTimeout(()=>{
                callback() //callback执行后才继续往下走
            },3000)
        }),
        compiler.hooks.make.tapAsync("TestPlugin",(compilation,callback)=>{
            setTimeout(()=>{
                callback() //callback执行后才继续往下走
            },2000)
        }),
        compiler.hooks.make.tapAsync("TestPlugin",(compilation,callback)=>{
            setTimeout(()=>{
                callback() //callback执行后才继续往下走
            },1000)
        })
    }
}
module.exports=TestPlugin

关于compiler以及compilation的调试:

首行断点

"scripts": {
    "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
  }

在TestPlugin中debugger

class TestPlugin{
    constructor(){}
    apply(compiler){
        debugger
        console.log(compiler);
        //文档可知environment是同步,所以使用tap注册
        compiler.hooks.environment.tap("TestPlugin",()=>{

        })
        ...

运行指令:npm run debug,启动后打开浏器控制台,点击node标志,进入调试模式

 点击下一个断点,就是我们的debugger的地方,可以看到compiler的内容,当然也包括compilation。


这里以下都是生产环境:

自定义bannerWebpackPlugin:给打包后文件添加注释:

需要使用compiler.hooks.afterCompile钩子函数,在compilation 结束和封印之后触发

class BannerWebpackPlugin{
    constructor(options={}){
        this.options = options
    }
    apply(compiler){
        compiler.hooks.afterCompile.tapAsync("BannerWebpackPlugin",(compilation,callback)=>{
            debugger
            // 1.获取即将输出的资源文件compiler.assets
            // 2.只保留js和css资源
            const extension = ['css','js']
            //assets文件都是 文件路径:文件内容的格式 所以做一下处理
            const assets = Object.keys(compilation.assets).filter(assetPath=>{
                let splitted = assetPath.split(".")
                return extension.includes(splitted[splitted.length-1])
            })
            // 3.遍历资源添加注释
            let prefix = `/*
            *author:${this.options.author}
            */`
            assets.forEach(asset=>{
                //找到原文件内容
                const source = compilation.assets[asset].source()
                //内容添加注释
                const content = prefix + source

                compilation.assets[asset]={
                    //调用source方法,返回内容
                    source(){
                        return content
                    },
                    //返回资源大小
                    size(){
                        return content.length
                    }
                }
            })
            callback()
        })
    }
}
module.exports = BannerWebpackPlugin
plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
    // new TestPlugin(),
    new BannerWebpackPlugin({
      author:"胖虎"
    })
  ],

自定义cleanWebpackPlugin:

我们希望每次打包以后都能够清空上次的打包内容。

我们在compiler.hooks.emit钩子函数触发,即将输出资源前清空,通过文件操作outputFileSystem操作文件

class CleanWebpackPlugin {
    constructor(){}
    apply(compiler){
        //获取打包目录
        const outputpath = compiler.options.output.path
        const fs = compiler.outputFileSystem
        compiler.hooks.emit.tapAsync("CleanWebpackPlugin",(compilation,callback)=>{
            //清空内容
            this.removeFiles(fs,outputpath)
            callback()
        })
    }
    removeFiles(fs,filePath){
        debugger
        console.log(filePath);
        // 想要删除打包目录下的所有文件,需要先删除这个包下的所有资源,然后再删除这个目录
        // 获取当前目录下所有的资源
        const files = fs.readdirSync(filePath)
        //遍历一个一个删除,判断文件还是文件夹,文件直接删除,文件夹递归
        files.forEach(file => {
            const path = `${filePath}/${file}`
            const fileStat = fs.statSync(path)
            if(fileStat.isDirectory()){
                this.removeFiles(fs,path)
            }else{
                fs.unlink(path,()=>{})
            }
        });
    }
}

module.exports=CleanWebpackPlugin
plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
    // new TestPlugin(),
    new BannerWebpackPlugin({
      author:"胖虎"
    }),
    new CleanWebpackPlugin()
  ],

自定义analyze-webpack-plugin, 分析webpack打包资源大小并输出分析文件。

也还是在compiler.hooks.emit钩子函数触发

class AnalyzeWebpackPlugin {
    constructor() { }
    apply(compiler) {
        compiler.hooks.emit.tapAsync("AnalyzeWebpackPlugin", (compilation, callback) => {
            const assets = Object.entries(compilation.assets)
            let content = `|资源名称|资源大小|
|---|---|`
            assets.forEach(([filename, file]) => {
                content += `\n|${filename}|${Math.floor(file.size()/1024)+"kb"}|`
            })
            // 生成一个md文件
            compilation.assets['analyze.md']={
                source(){
                    return content
                },
                size(){
                    return content.size
                }
            }
            callback()
        })
    }
}
module.exports=AnalyzeWebpackPlugin
plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
    // new TestPlugin(),
    new BannerWebpackPlugin({
      author:"胖虎"
    }),
    new CleanWebpackPlugin(),
    new AnalyzeWebpackPlugin()
  ],

效果:

 自定义inline-chunk-webpack-plugin

webapck打包生成的runtime文件太小了,额外发请求性能不太好,将其内联到js中,从而减少请求数量,需要借助html-webpack-plugin来实现,将runtime内联注入到index.html中。下面是html-webpack-plugin原理图。

 html-webpack-plugin有6个生命周期函数,我们在alterAssetTagGroups中(已经将文件分好组)来找到runtime文件,变成inline script标签。在compiler.hooks.compilation钩子触发。

生成runtime文件:

optimization: {
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}.js`,
    },
  },
const HtmlWebpackPlugin = require('safe-require')('html-webpack-plugin')
class InlineChunkWebpackPlugin {
    constructor(tests) { 
        this.tests=tests
    }
    apply(compiler) {
        compiler.hooks.compilation.tap("InlineChunkWebpackPlugin", (compilation, compilationParams) => {
            // 获取HtmlWebpackPlugin的钩子,注册
            HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tap("InlineChunkWebpackPlugin", (assets) => { //assets是获取的资源包含headTags以及bodyTags
                //     headTags: [
                //       {
                //         tagName: 'script',
                //         voidTag: false,
                //         meta: [Object],
                //         attributes: [Object]
                //       },
                //       {
                //         tagName: 'script',
                //         voidTag: false,
                //         meta: [Object],
                //         attributes: [Object]
                //       }
                //     ],
                //     bodyTags: [],
                // 我们需要修改:
                //     {
                //         tagName: 'script',
                //         innerHTML: runtime文件内容,
                //         closeTag: true
                //       }
                assets.headTags = this.getInlineChunk(assets.headTags, compilation.assets)
                assets.bodyTags = this.getInlineChunk(assets.bodyTags, compilation.assets)

            })
            // 删除已经被注入的文件
            HtmlWebpackPlugin.getHooks(compilation).afterEmit.tap("InlineChunkWebpackPlugin", (outputname,plugin) => { //assets是获取的资源包含headTags以及bodyTags
                Object.keys(compilation.assets).forEach(filePath=>{
                    if(this.tests.some(test=>test.test(filePath))){
                        delete compilation.assets[filePath]
                    }
                })

            })
        })

    }
    getInlineChunk(tags, assets) {
        return tags.map(tag => {
            if (tag.name != "script") return tag
            const filePath = tag.attributes.src
            if (!filePath) return tag
            if (!this.tests.some(test=>{test.test(filePath)})) return tag
            return {
                tagName: 'script',
                innerHTML: assets[filePath].source(),
                closeTag: true
            }
        });
    }
}
module.exports = InlineChunkWebpackPlugin
plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
    // new TestPlugin(),
    new BannerWebpackPlugin({
      author:"胖虎"
    }),
    new CleanWebpackPlugin(),
    new AnalyzeWebpackPlugin(),
    new InlineChunkWebpackPlugin([/runtime(.*)\.js$/g]) //可自定义输入文件正则来删除已注入的文件
  ],

猜你喜欢

转载自blog.csdn.net/m0_59962790/article/details/130465766
今日推荐