这是我参与更文挑战的第7天,活动详情查看: 更文挑战
注:以下是个人理解、如有不对还望指正!
前言、一个又菜又爱折腾的菜鸡H~在一个写bug的晚上、突然翻到的webpack官网、突然想实现一个插件加深印象、说干就干的性格那就直接过了两星期才去行动、突然一想写个什么插件好呢、太难的估计得花很多时间、太容易的没意思(膨胀)、那就模仿个热门插件CopyWebpackPlugin吧、几乎必备插件使用简单、也出名、ok开干!
loader和plugin区别
- loader : 文件加载器、告诉webpack如何转化和处理某一类型文件、并引入到打包处的文件当中
- plugin : 自定义打包过程的方式、webpack提供了非常多的生命周期钩子我们可以在钩子函数里吗做一些我们想处理的逻辑、比如我想在输出文件的时候添加一段js、在或者我想在指定文件当中删除某些代码、都是可以的
学习webpack插件语法
plugins: [
new CopyWebpackPlugin({
from:'/src/public/',
to:'public'
})
]
复制代码
可以看到plugin数组的CopyWebpackPlugin使用了关键字 new 而且还有参数传入所以他应该是一个构造函数
- compiler对象
构造函数定义一个apply方法、等插件执行的时候、webpack会去执行我们插件的apply方法并且传入一个compiler对象、compiler对象里面有个hooks属性、存放着生命周期个个环节的钩子、我们马上需要用到的一个叫 thisCompilation(初始化 compilation 时调用,在触发 compilation 事件之前调用)
- Compilation 对象
我把他叫做文件实例对象、compiler对象更多的是做一些生命周期的钩子作用、比如配置加载完、配置初始化一些、而Compilation对象里面大多做一些文件内容的处理
第一个plugin(webpack提供)
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('webpack 构建过程开始!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
复制代码
上面案例、定义了一个类、里面有一个apply方法、然后得到一个compiler实例、注册实例上的run钩子函数的回调、
一个插件必须有一个apply 方法、webpack会去执行插件的apply方法、并传入一个compiler、我们几乎所有的事情都是在apply方法去操作、compiler你可以理解为webpack的实例
钩子类型和效果示例
- hooks类型
类型名称 | 描述 |
---|---|
SyncHook | 同步钩子,不能处理异步任务 |
SyncBailHook | 同步钩子,return非空时,阻止向下执行 |
SyncWaterfallHook | 同步钩子,支持将返回值透传到下一个钩子中 |
SyncLoopHook | 同步钩子,支持将返回值透传到下一个钩子中,返回非空时,重复执行方法 |
AsyncParallelHook | 异步并行钩子,返回非空时,阻止向下执行,直接执行回调 |
AsyncSeriesHook | 异步串行钩子 |
AsyncSeriesBailHook | 异步串行钩子,返回非空时,阻止向下执行,直接执行回调 |
AsyncSeriesLoopHook | 支持异步串行 && 并行的钩子,返回非空时,重复执行 |
AsyncSeriesWaterfallHook | 异步串行的钩子,下一步依赖上一步返回的值 |
1、同步的方法,推荐直直接使用 tap 进行注册方法,
2、异步的方案,tabAsync 通过执行 callback 方法实现回调,
3、返回的是一个 Promise,推荐使用 tapPromise 进行方法的注册
- 示例一
SyncHook串行同步执行,不关心事件处理函数的返回值,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数
compiler.hooks.environment.tap('test', (context, entry) => {
console.log(11111111)
})
compiler.hooks.environment.tap('test', (context, entry) => {
console.log(2222222)
})
compiler.hooks.environment.tap('test', (context, entry) => {
console.log(3333333)
})
// 输出 11111111 2222222 3333333
复制代码
- 示例二
SyncBailHook 串行同步执行,多个注册事件时、如果有一个函数有返回值、就不会继续往下一个事件执行
compiler.hooks.entryOption.tap('test', (context, entry) => {
console.log(11111111)
})
compiler.hooks.entryOption.tap('test', (context, entry) => {
console.log(222222)
return 1
})
compiler.hooks.entryOption.tap('test', (context, entry) => {
console.log(3333333)
})
// 输出 11111111 2222222
复制代码
学习完基础部分后我们对整个插件的流程就熟悉了一些、下面我们开始着手我们的主角copyWebpackPlugin插件
开始copyWebpackPlugin插件
目录结构
CopyWebpackPlugin/globby.js (用于处理文件、并设置忽略文件)
const fs = require('fs');
const path = require('path');
async function globby( absoluteFrom,options ){
const { ignore = [] } = options;
absoluteFrom = absoluteFrom.indexOf('/') === 0 ? absoluteFrom.substring(1) : absoluteFrom;
try{
const result = await fs.readdirSync(absoluteFrom)
.filter( filte => !ignore.includes(filte) )
.map( file => { return { file_path:path.join( absoluteFrom,file ),file_name:file } } );
return result || []
}catch(err){
console.log(err)
}
}
module.exports = globby;
复制代码
schema.json (参数校验、依赖于schema-utils)
{
"type":"object",
"properties":{
"from":{
"type":"string"
},
"to":{
"type":"string"
},
"ignore":{
"type":"array"
}
},
"additionalProperties":false
}
复制代码
CopyWebpackPlugin/index.js (插件代码)
//需要安装 schema-utils -> yarn add schema-utils
const { validate } = require('schema-utils');
const globby = require('./globby');
const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const schema = require('./schema.json');
const { RawSource } = webpack.sources;
class CopyWebpackPlugin{
constructor(options = {}){
//验证options是否符合规范
validate(schema,options,{
name:'CopyWebpackPlugin'
})
this.options = options;
}
apply( compiler ){
//初始化compilation
compiler.hooks.thisCompilation.tap('CopyWebpackPlugin',( compilation ) =>{
//添加资源的hooks
compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async ( cb ) =>{
//把from的资源复制到to中
const { from,ignore } = this.options;
//补齐
const to = this.options.to ? this.options.to : '.';
//获取运行指令的目录
const context = compiler.options.context;
//将输出路径变成绝对路径
const absoluteFrom = path.isAbsolute( from ) ? from : path.join( context,from );
//globby(要处理的文件夹),第二个参数是忽略的文件
const paths = await globby( absoluteFrom ,{ ignore } );
//读取文件内容
const files = await Promise.all(
paths.map( async file_info =>{
const { file_path,file_name } = file_info;
const file_data = await fs.readFileSync( file_path );
let filename = path.join(to,file_name)
return { file_data, filename }
})
)
//生成webpack资源
const assets = files.map( file =>{
const { file_data,filename } = file;
const source = new RawSource(file_data);
return{ source, filename }
})
//添加到compilation输出出去
assets.forEach( asset =>{
const { filename, source} = asset;
compilation.emitAsset(filename,source)
})
//插件执行完毕
cb()
})
})
}
}
module.exports = CopyWebpackPlugin;
复制代码
使用
plugins: [
//把src/pulic/下的内容复制到打包文件下的public下
new CopyWebpackPlugin({
from:'/src/public/',
to:'public'
}),
]
复制代码
总结
webpack提高很多钩子、学习起来相对繁琐一些、学之前先去看下tapable 、tapable是webpack核心框架、基于事件,或者叫做发布订阅模式,或观察者模式,webpack的整个生命周期及其开放的自定义插件系统都离不开tapable的支持、理解了后、我们只需要对着文档然后找出合适的事件段钩子做逻辑处理就ok啦!