webpack - loader、plugin原理

一、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 调试一样使用
    在这里插入图片描述
  • 10
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值