一、loader
- loader 就是一个函数,函数里面要将处理过的文件内容 return 出去
- 当 webpack 解析资源时,会调用相应的 loader 去处理
- loader 接收三个参数:
- content:文件内容,map:SourceMap,meta:别的 loader 传递的数据
1、手写一个简单的 loader
webpack.js 中配置 loader
module: {
rules:[
{
test: /\.js$/, // 匹配以 .js 结尾的文件
loader: './loaders/test-loader.js' // 处理匹配到的文件用的 loader
}
]
},
loaders/test-loader.js
// 同步 loader
/**
* loader 就是一个函数,函数里面要将处理过的文件内容 return 出去
* 当 webpack 解析资源时,会调用相应的 loader 去处理
* loader 接收三个参数:
* content:文件内容,map:SourceMap,meta:别的 loader 传递的数据
*/
// module.exports = function(content, map, meta) {
// // content = 'console.log("12")' // 修改 content
// return content
// }
// 同步 loader
module.exports = function (content, map, meta) {
/**
* 第一个参数:err 代表是否有错误
* 第二个参数:content 是处理后的内容
* 第三个参数:map 是 source-map 继续传递source-map
* 第四个参数:meta 是给下一个 loader 传递的参数
*/
this.callback(null, content, map, meta)
}
// 异步 loader
module.exports = function (content, map, meta) {
const callback = this.async()
setTimeout(() => {
console.log('test2')
callback(null, content, map, meta)
}, 1000)
}
以上代码的作用:通过 test-loader.js 去处理匹配到以 .js 结尾的文件。
2、raw loader
raw loader 中,拿到的 contetn 是一个 Buffer 数据
// 方法1:
// module.exports = function (content) {
// console.log(content)
// return content
// }
// // raw loader 接收到的 content 是 Buffer 数据
// module.exports.raw = true
const { truncate } = require("fs")
// 方法2:
function rawLoader(content) {
console.log(content)
return content
}
rawLoader.raw = true
module.exports = rawLoader
3、pitch loader
如果 webpack.config.js 中处理某些文件使用了多个 pitch loader =》pitch_loader1.js、pitch_loader2.js、pitch_loader3.js,那么 loader 执行顺序是:
pitch_loader1 中的 pitch =》
pitch_loader2 中的 pitch =》
pitch_loader3 中的 pitch =》
pitch_loader3 中的 normal 方法 =》
pitch_loader2 中的 normal 方法 =》
pitch_loader1 中的 normal 方法
如果某一个 loader 的pitch中使用了 return,那么该 loader 的 normal 方法和该 loader 之后的 loader.js文件 中的 normal 方法 和 pitch 都不会再执行,从该 loader 的前一个 loader 的 normal 方法开始执行,
比如 pitch_loader2.js 中的 pitch 使用 了 return,那么执行顺序是:
pitch_loader1 中的 pitch =》
pitch_loader2 中的 pitch =》
pitch_loader1 中的 normal 方法
// loader
module.exports = function (content) {
return content
}
// pitch
module.exports.pitch = function () {
console.log('pitch loader')
// return ''
}
4、loader API
5、自定义 loader
1)定义一个清除文件内容中 console.log(XXX) 的 loader
module.exports = function (content) {
// 清除文件内容中 console.log(xxx)
return content.replace(/console\.log\(.*\);?/g, '')
}
2)定义一个 给打包出书文件添加注释的loader
webpack.config.js
module.exports = {
module: {
rules:[
{
test: /\.js$/,
loader: './loaders/author_loader/index.js',
options: {
author: 'zhangsan'
}
}]
}
}
author_loader/index.js
const schema = require('./schema')
module.exports = function (content) {
// 获取 webpack.config.js 中配置的对象
const options = this.getOptions(schema)
let prefix = `
/*
* Author: ${options.author}
*/
`
return `${prefix}${content}`
}
author_loader/schema.json
{
"type": "object",
"properties": {
"author": {
"type": "string"
}
},
"additionalProperties": false
}
二、plugins
webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换为输出结果。这条生产线上的每个处理流程的职责都是单一的,多个流程之间存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
---- 【深入浅出 Webpack】
站在代码逻辑的角度就是:webpack 在编译代码过程中,会触发一系列 Tapable
钩子事件,插件所做的,就是找到对应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。
1、创建一个简单的 plugins
webpack.config.js
const TestPlugin = require('./plugins/test-plugin')
...
plugins: [
new TestPlugin()
],
...
test-plugin.js
const { resolve } = require("path")
/**
* 1、webpack 加载 webpack.config.js 中所有配置,此时就会 new TestPlugin(),执行插件的 constructor
* 2、webpack 创建 compiler 对象
* 3、遍历所有 plugins 中的插件,调用插件的 apply 方法
* 4、执行剩下编译流程(出发各个 hooks 事件)
*/
class TestPlugin {
constructor() {
console.log('TestPlugin constructor')
}
apply(compiler) {
console.log('TestPlugin apply')
// environment 是同步钩子,所以需要 tab 注册
compiler.hooks.environment.tap("TestPlugin", () => {
console.log("TestPlugin environment")
})
// emit 是异步串行钩子
compiler.hooks.emit.tap("TestPlugin", (compilation) => {
console.log("TestPlugin emit 111")
})
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("TestPlugin emit 222")
callback()
}, 2000)
})
compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => {
return new Promise(() => setTimeout(() => {
console.log("TestPlugin emit 333")
resolve()
}, 1000))
})
// make 是异步并行钩子
compiler.hooks.make.tapAsync("TestPlugin",(compilation, callback)=>{
setTimeout(() => {
console.log("TestPlugin make 111")
callback()
}, 3000)
})
}
}
module.exports = TestPlugin
2、定义一个 给打包出书文件添加注释的 plugin
思路:
- 在打包输入前添加注释:需要使用
compiler.hooks.emit
钩子,它是打包输出前触发 - 如何让获取打包输出的资源:
compilation.assets
可以获取所有即将输出的资源文件
webpack.config.js
const AuthorWebpackPlugin = require('./plugins/author-webpack-plugin')
...
plugins: [
new AuthorWebpackPlugin({
author: "王武"
})
],
...
author-webpack-plugin.js
/**
* 插件功能:给所有的 css、js 文件内容前,增加前缀 anthor
*/
class AuthorWebpackPlugin {
constructor(options) {
// 获取 webpack.config.js 使用插件时传入的配置项
this.options = options
}
apply(compiler) {
compiler.hooks.emit.tap("AuthorWebpackPlugin", (compilation) => {
// 该插件处理 css、js 文件资源
const extensions = ["css", "js"]
// 1、获取即将输出的资源文件对象:compilation.assets(是一个对象,key 是包含当前文件名的文件路径,value 是文件内容相关信息)
// 2、过滤只保留 js 和 css 文件
const assets = Object.keys(compilation.assets).filter(assetPath => {
// 通过 . 对文件路径进行切分
const splitted = assetPath.split(".")
// 获取文件后缀
const extension = splitted[splitted.length - 1]
// 判断是否是 js 或者 css 文件
return extensions.includes(extension)
})
const prefix = `/*
* Author: ${this.options.author}
*/
`
// 遍历所有 js、css 资源添加注释
assets.forEach(asset => {
// 获取原文件内容
const source = compilation.assets[asset].source()
// 在原文件内容的基础上添加前缀
const content = prefix + source
// 修改资源
compilation.assets[asset] = {
// 最终资源输出时,调用 source 方法,source 返回的值时资源的具体内容
source() {
return content
},
// 资源大小
size() {
return content.length
}
}
})
})
}
}
module.exports = AuthorWebpackPlugin
3、定义一个 清除上次打包内容的 plugin
作用:在 webpack 打包输出前,将上次打包内容情况,类似于 webpack.config.js 中 output 的 clean: true
开发思路:
- 如何在打包输出前执行:需要使用
complier.hooks.emit
钩子,它是打包输出前触发 - 如何清空上次打包内容:
· 获取打包输出目录:通过complier
对象
· 通过文件操作情况内容:通过complier.outputFileSystem
webpack.config.js
const CleanWebpackPlugin = require("./plugins/clean-webpack-plugin")
...
plugins: [
new CleanWebpackPlugin()
],
...
clean-webpack-plugin.js
class CleanWebpackPlugin {
apply(complier) {
// 2、获取打包输出的目录
const outputPath = complier.options.output.path
// 获取 fs 模块处理文件
const fs = complier.outputFileSystem
// 1、注册钩子:在打包输出之前 emit
complier.hooks.emit.tap("CleanWebpackPlugin", (compilation) => {
// 3、通过 fs 删除打包输出目录下的所有文件
this.removeFiles(fs, outputPath)
})
}
removeFiles(fs, filePath) {
// 法一:递归删除文件和目录
fs.rmSync(filePath, {recursive: true})
// 法二:删除文件(空目录不会被删除)
/*
// 删除打包输出目录下所有的资源,需要先将目录下的资源删除,才能删除这个目录
// 1、读取当前目录下所有资源(这里读取到的只有第一层文件和文件夹)
const files = fs.readdirSync(filePath)
// 2、遍历 files 一个个删除
files.forEach(file => {
// 2.1 遍历所有资源,判断时文件夹还是文件
const path = `${filePath}/${file}`
// 获取资源信息
const fileStat = fs.statSync(path)
if(fileStat.isDirectory()) {
// 2.2 如果是文件夹,就要递归删除下面所有文件
this.removeFiles(fs, path)
} else {
// 2.3 如果是文件,就直接删除
fs.unlinkSync(path)
}
})
*/
}
}
module.exports = CleanWebpackPlugin
拓展:浏览器控制台调试 node
场景:编写 plugin 文件时,要查看 compiler
或者 compilation
- 1、node 文件中,增加
debugger
test-plugin.js
class TestPlugin {
constructor() {
console.log('TestPlugin constructor')
}
apply(compiler) {
debugger
console.log('TestPlugin apply')
}
}
module.exports = TestPlugin
- 2、package.json 文件中的 scripts 属性中新增
debug
命令"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
package.json
{
...
"scripts": {
"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
},
...
}
- 3、终端执行命令
npm run debug
- 4、打开谷歌浏览器控制台,左上角会显示一个 node 图标
- 5、点击图标,就可以进行断点调试,调试按钮跟正常 js 调试一样使用