超详细webpack的plugin讲解,看不懂算我输,案例>原理>实践

前言

本篇文章为webpack系列文章的第三篇,主要内容是对webpack的plugin进行详细的讲解,从使用,到原理,再到自己开发一个plugin,对每个过程都会进行详细的分析介绍。如果你对webpack了解的还比较少,建议你先阅读以下往期文章。

如何使用webpack实现模块化打包

webpack的核心机制之loader的秘密

介绍

plugin是webpack的一个插件机制,它为项目的构建提供了更加广泛的能力,loader的作用只是实现各种资源的转换,使得任何资源都可以模块化的被打包。而plugin可以解决其他的更多的自动化打包工作,plugin的范围更大,作用也更强。

  • 可以自动打包生成html文件,并自动引入打包后的结果
  • 打包前清除原dist文件中的内容
  • 可以将我们需要的但是并没有引入静态资源一同打包到dist文件中
  • 对打包的结果进行特殊的处理
  • 压缩打包后的内容,对打包结果可以进行更细的自定义操作

  • plugin的作用还远不止这些,可以看出plugin的重要性,下面我们来看几个常用的plugin,然后去深入它的原理,最后自己写一个简单的plugin。

情景再现

情景1

通过往期的学习我们知道,每次打包后的内容默认会输出到dist文件夹中,如果在原dist文件夹中存在相同文件名,则会覆盖原文件,使得打包结果始终为最新的内容。但是有没有想过这样的场景,如果原dist中存在某些文件已经被我们遗弃,也就是说后面我们不需要这些文件了,但是他们还是存在于dist中,这种情况下我们还要去提取出来哪些使我们需要的文件,这个过程显然是很繁琐的。

那如何解决这个问题呢,如果我们在打包输出前清除掉dist中原来的内容,那么打包后的内容必然都是我们需要的。我们每次手动的去清理也比较麻烦,如果有个插件可以在我们打包前自动帮我们清理就好了。

clean-webpack-plugin这个插件就可以实现这个功能,它是一个第三方npm包,我们只需要安装就可以使用。

npm install clean-webpack-plugin

在webpack.config.js中引入,我们只需要用到它其中的CleanWebpackPlugin就行。

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

使用插件是在plugins属性中,它是一个数组,表示可以使用多个插件,我们只需要将上面的对象实例化后写在数组中即可。

module.exports = {
    mode: 'none',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
    },
    plugins:[
        new CleanWebpackPlugin(),
    ]
}

现在我们执行打包命令

可以看到在原dist中含有很多个文件,我这里打包后会自动的清理掉。

情景2

在我们打包完成后会生成结果文件,我们需要手动的在index.html中去引入,如果结果文件名改变了,还需要手动的去修改引入的文件名。如果有个插件可以在打包完成后自动的生成一个html文件,并自动引入script标签就好了。其实这些功能我们可以使用html-webpack-plugin插件来实现。

npm install clean-webpack-plugin

引入

const HtmlWebpackPlugin =  require('html-webpack-plugin')

配置

...
     plugins:[
        new HtmlWebpackPlugin(),
     ]
...

我们可以在实例化时传入配置参数,这个插件提供了很多灵活的配置项。

  • title:标题
  • meta:meta标签
  • filename:输出的文件名
  • template:使用已有模板,有时在index.html中含有一些其他的内容,可以选用该html文件为模板。
  • minify:压缩html文件
  • favicon:favicon的路径

  • 这里我只列出了其中的部分配置,更多详情可以去查阅该插件的说明文档。
new HtmlWebpackPlugin({
    title: '测试标题',
    meta:{
        keywords: 'webpack,plugin'
    },
    filename: 'webpack.html',
    template: 'index.html',
    minify:{   //压缩html文件
        caseSensitive: true,   //是否对大小写敏感,默认false         
        collapseBooleanAttributes: true,   //是否简写boolean格式的属性如:disabled="disabled" 简写为disabled  默认false           
        collapseWhitespace: true,  //是否去除空格,默认false           
        minifyCSS: true,  //是否压缩html里的css(使用clean-css进行的压缩) 默认值false;
        minifyJS: true,  //是否压缩html里的js(使用uglify-js进行的压缩)           
        preventAttributesEscaping: true,  //Prevents the escaping of the values of attributes            
        removeAttributeQuotes: true,  //是否移除属性的引号 默认false           
        removeComments: true,  //是否移除注释 默认false           
        removeCommentsFromCDATA: true,  //从脚本和样式删除的注释 默认false           
        removeEmptyAttributes: true,  //是否删除空属性,默认false           
        removeOptionalTags: false,  //  若开启此项,生成的html中没有 body 和 head,html也未闭合           
        removeRedundantAttributes: true,   //删除多余的属性      
        removeScriptTypeAttributes: true,  //删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false        
        removeStyleLinkTypeAttributes: true,   //删除style的类型属性, type="text/css" 同上
        useShortDoctype: true,  //使用短的文档类型,默认false
    },
    favicon: './public/logo.ico'
}),

在配置中我选用了自己的index.html作为模板,原有的index.html内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>wepack-plugins</title>
</head>
<body>
    <div>
        <span>我是原html中的内容</span>
    </div>
</body>
</html>

在index.js中动态加入了一个标签

var dom = document.createElement('h2')
dom.innerHTML = '我是外部新增的内容'
document.body.append(dom)

执行打包,可以在dist下面出现一个webpack.html文件,这是因为我们在配置中配置了输出文件名为webpack.html。在配置中还对html进行了压缩配置,所以看到的是被压缩后的结果。

我们取消html压缩配置,重新打包

可以看到我们的配置全部生效,包括标题、meta等信息。在浏览器中预览结果完全符合我们的预期。

情景3

最后再来介绍一个插件叫做copy-webpack-plugin,看名字就知道它与复制有关。我们在项目中会用到很多静态资源,但有些可能并没有被直接的使用,我们希望这些资源可以一同的被打包到dist中。

新建一个public文件夹,假如说这个文件夹中的所有内容我们都希望被打包。为了测试结果,在里面我们新建一个static.txt文件,这个文件我们没有在任何地方使用到。

它接收的是一个数组,因为可能存在多个不同的目录

const CopyWebpackPlugin = require('copy-webpack-plugin')
...
new CopyWebpackPlugin(['./public']),

执行打包

通过上面几个案例,是不是可以看出plugin的强大能力和它的重要地位了,总有一个插件可以满足我们的需求。有人会说了,万一我就是没有找到适合我的需求的插件呢?

这个世界上本是没有轮子的,没有轮子我们就要学会去造轮子,下面我们就来造一个自己的轮子。

要学会造轮子

在造轮子之前我们必须要知道它的原理,plugin相比loader还有一点很大的不同,loader只工作于模块的加载环节,而plugin即可可以作用于打包过程的每一个环节,有点像vue中的生命周期,我们可以在一个合适的周期进行相应的操作。webpack的插件机制就是我们常说的钩子机制,整个打包过程可以分为多个环节,为了便于插件的扩展,webpack机会在每个环节都提供了一个钩子,我们就可以利用这些钩子来造轮子。

webpack为我们提供了哪些hooks呢?

  • entry-option 初始化 option
  • run 开始编译
  • compile 真正开始的编译,在创建 compilation 对象之前
  • compilation 生成好了 compilation 对象
  • make 从 entry 开始递归分析依赖,准备对每个模块进行 build
  • after-compile 编译 build 过程结束
  • emit 在将内存中 assets 内容写到磁盘文件夹之前
  • after-emit 在将内存中 assets 内容写到磁盘文件夹之后
  • done 完成所有的编译过程
  • failed 编译失败的时候

  • 更多hooks请参考官方文档:https://www.webpackjs.com/api/compiler-hooks/
    现在我们知道了生产线,我们在生产线的不同阶段做不同的事情。但是我们还是不知道怎么造轮子啊。

    webpack要求我们的插件必须是一个函数,或者是一个包含apply的对象。一般来说我们都会定义一个类型,然后在这个类型中定义apply方法,最后再通过这个类型来创建一个实例对象去使用这个插件。
const pluginName = 'myplugin'
module.exports =  class myplugin {
    apply(){}
}

这个apply方法接收一个叫compiler的参数对象,这个对象是webpack工作中最核心的对象,包含了此次打包构建的所有配置信息,我们就可以通过这个对象去注册钩子函数。

const pluginName = 'myplugin'
module.exports =  class myplugin {
    apply(compiler){
        compiler.hooks.run.tap(pluginName, () =>{
            {
                console.log('开始编译');
            }
        })
    }
}

我们想在run阶段输出‘开始编译’这句话,在webpack.config.js中引入并配置

const myplugin = require('./myplugin')
...
plugins:[
  new myplugin()
]
...

在这里插入图片描述
在控制台可以看到在开始阶段输出了内容,说明我们的plugin生效了,这只是测试,接下来我们就来实现点功能。

我们在使用node或者development模式下,打包后的js文件中,前面会有许多这样的注释符,看起来很不舒服。

我们可以自己来造个轮子让webpack打包后的内容中没有这些东西。

第一步,我们要找到合适的环节执行我们要进行的操作。通过查看API文档,可以找到emit这个hooks很符合我们的场景,它在生成资源到output目录之前执行,也就是在还没有输出打包文件时执行。

我们的思路是这样的,在即将输出文件的前面,获取到要输出的文件内容,找到以js为后缀的文件,然后去掉里面的注释符,最后再重新将处理后的内容替换原来要输出的内容。

./remove-comments-plugin

const pulginName = 'RemoveCommentsPlugin'
module.exports = class RemoveCommentsPlugin {
    apply(compiler) {
        compiler.hooks.emit.tap(pulginName, (compilation) => {
            for (const name in compilation.assets) {
                if (name.endsWith('js')) {
                    let contents = compilation.assets[name].source()
                    let noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
                    compilation.assets[name] = {
                        source: () => noComments,
                        size: () => noComments.length
                    }
                }
            }
        })
    }
}

它接收一个叫 compilation 的参数对象,这个对象可以理解为本次运行打包的上下文,它包含了所有打包过程中产生的结果。它的assets属性中包含的就是所有打包后将要输出的文件,source()方法可以获取到文件内的内容。

这里我们遍历所有即将要输出的文件,使用endsWith(‘js’)方法匹配到js文件(这是ES6的一个方法),然后使用正则将所有的注释符替换为空字符串,最后将修改后的内容覆盖掉原来的内容。这里要注意一点,在改变文件内容后需要重新计算文件的大小,否则size的值可能会与实际值不匹配。

const RemoveCommentsPlugin from './remove-comments-plugin'
...
plugins:[
  new RemoveCommentsPlugin()
]
...

查看打包后的结果,已经不存在注释符。

对于有些插件需要传递配置参数,这个也很简单,我们只需要在构造函数中进行接收即可。

const pulginName = 'RemoveCommentsPlugin'
module.exports = class RemoveCommentsPlugin {
    constructor(params){
        this.config = {}
        for (const key in params) {
            if (this.config.hasOwnProperty(key)) {
                this.config[key] = params[key]
            }
        }
    }
    apply(compiler) {
        compiler.hooks.emit.tap(pulginName, (compilation, params) => {
            for (const name in compilation.assets) {
                if (name.endsWith('js')) {
                    let contents = compilation.assets[name].source()
                    let noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
                    compilation.assets[name] = {
                        source: () => noComments,
                        size: () => noComments.length
                    }
                }
            }
        })
    }
}

每次都不知道怎么写结语,这都不重要,重要到的可以学到东西,学以致用就够了。
最后觉得我的文章对你有所帮助的话,希望可以收藏、点赞、关注,你们的支持是我最大的动力
欢迎访问我的个人网站www.dengzhanyong.com
我的个人公众号:【前端筱园】与CSDN同步更新,快来关注我吧,不错过我的每一篇推送。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章