带你深入了解webpack插件原理,看完你就会写插件了

375 阅读5分钟

webpack是一个款非常流行的构建打包工具,它具有两个核心的功能loader、plugin。

webpack的loader有什么作用?

webpack默认只能理解JavaScript和JSON对于其他的文件类型,比如.vue.ts图片.css等文件是不能直接处理的。需通过特定的loader转换后才能处理。

webpack的plugin有什么用?

主要目的就是解决 loader 无法实现的事情,loader 只是用作于将特定的模块进行转换,而 pulgin 可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等。

webpack执行流程

模拟webpack执行流程如下 文件目录

image.png

// add.js
export default (a, b) => {
  return a + b
}

// minus.js
export default (a, b) => {
  return a - b
}
// index.js
import add from "./add.js"
import minus from './minus.js'

const sum = add(1, 2)
const division = minus(2, 1)
console.log(sum)
console.log(division)
//webpack.js 
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const {
  SyncHook,
  AsyncSeriesWaterfallHook,
} = require("tapable");
let depsGraph = {}, depsG = {}
// 构建依赖图
const getModuleInfo = (file) => {
  const body = fs.readFileSync(file, 'utf-8')
  const ast = parser.parse(body, {
    sourceType: 'module' //表示我们要解析的是ES模块
  });
  const deps = {}
  // 查找某一模块的依赖
  traverse(ast, {
    ImportDeclaration ({
      node
    }) {
      const dirname = path.dirname(file)
      const abspath = "./" + path.join(dirname,node.source.value)
      deps[node.source.value] = abspath
    }
  })
  // ast转es5
  const {
    code,
    map
  } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
    sourceMaps:true
  })
  depsGraph[file] = {
    deps,
    code
  }
  depsG[file] = {
    deps,
    // map
  }
  for (const key in deps) {
    if (Object.hasOwnProperty.call(deps, key)) {
      const el = deps[key];
      getModuleInfo(el)
    }
  }
}
// 新增代码
const bundle = (entry, compiler) => {
  getModuleInfo(entry)
  fs.writeFileSync('./code.js',JSON.stringify(depsGraph,null,2))
  compiler.hooks.deps.call(depsG)
  return `(function (graph) {
        function require(file) {
            function absRequire(relPath) {
                return require(graph[file].deps[relPath])
            }
            var exports = {}
            ;(function (require,exports,code) {
                eval(code)
            })(absRequire,exports,graph[file].code)
            return exports
        }
        require('${entry}')
    })(${JSON.stringify(depsGraph)})`
}
// 模拟webpack的Compiler
class Compiler {
  constructor(context, options = {}) {
    this.hooks = {
      start: new SyncHook([]),
      deps: new SyncHook(['deps']),
      emit: new AsyncSeriesWaterfallHook(['compilation']),
      afterEmit: new AsyncSeriesWaterfallHook(['compilation']),
      done: new SyncHook([])
    }
  }
}
const PluginName = 'HelloPlugin'
// 一个简单的webpack插件
class HelloPlugin {
  constructor() {

  }
  apply (compiler) {
    compiler.hooks.start.tap(PluginName, () => {
      console.log('build start...')
    })
    compiler.hooks.deps.tap(PluginName, deps => {
      console.log('deps:', deps)
    })
    compiler.hooks.emit.tapPromise(PluginName, (compilation) => {
      return new Promise((resolve, reject) => {
          // 在编译阶段可以对资源进行一下处理
        const prefix = `
        /*
        * @LastEditTime: 2022-10-20 10:53:23
        * @LastEditors: shangYin
        */
        `
        resolve(prefix + compilation)
        // resolve(compilation)
      })
    })
    compiler.hooks.afterEmit.tapAsync(PluginName, (compilation, cb) => {
      // console.log(compilation)
      cb()
    })
    compiler.hooks.done.tap(PluginName, () => {
      console.log('build done...')
    })
  }
}
// 模拟webpack编译打包
const webpack = (options = {}) => {
  const { entry, output, plugins = [] } = options

  const compiler = new Compiler()

  for (const plugin of plugins) {
    plugin.apply(compiler)
  }
  runBuild(compiler, entry, output)
}
// 模拟编译打包的流程
const runBuild = (compiler, entry, output) => {
  compiler.hooks.start.call()
  compiler.hooks.emit.promise(bundle(entry, compiler)).then(compilation => {
    fs.writeFileSync(output, compilation)
    compiler.hooks.afterEmit.callAsync(compilation, () => {
      compiler.hooks.done.call()
    })
  }).catch(e => e)
}
webpack({
  entry: './src/index.js',
  output: './bundle.js',
  plugins: [new HelloPlugin()]
})

执行node webpack即可进行打包,更新关于webpack插件的知识见下面

Tapable

在 Webpack 的编译过程中,本质上通过 Tapable 实现了在编译过程中的一种发布订阅者模式的插件 Plugin 机制。

在 Tapable 中所有注册的事件可以分为同步、异步两种执行方式:

  • 同步表示注册的事件函数会同步进行执行。
  • 异步表示注册的事件函数会异步进行执行。

  • 针对同步钩子来 tap 方法是唯一的注册事件的方法,通过 call 方法触发同步钩子的执行。
  • 异步钩子可以通过 taptapAsynctapPromise三种方式来注册,同时可以通过对应的 callcallAsyncpromise 三种方式来触发注册的函数。

详细见:github.com/webpack/tap…

plugin大致代码

class MyPlugin {
  constructor(options){
    this.options = options
  }
  apply(compiler){
    // doSomething...
  }
}

Compiler与Compilation

  • Compiler 在 Webpack 启动打包时创建,保存着本次打包的所有初始化配置信息包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

compiler-hooks

compiler钩子,当webpack编译器处理到特定时间时就会触发处于这个时间段钩子函数。

  • emit 【AsyncSeriesHook】输出 asset 到 output 目录之前执行。这个钩子 不会 被复制到子编译器。
  • afterEmit 【AsyncSeriesHook】输出 asset 到 output 目录之后执行。这个钩子 不会 被复制到子编译器。
  • entryOption 【SyncBailHook】 在 webpack 选项中的 entry 被处理过之后调用。
  • done 【AsyncSeriesHook】 在 compilation 完成时执行。这个钩子 不会 被复制到子编译器。

更多hooks见:webpack.docschina.org/api/compile…

compilation-hooks

Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

  • buildModule ****【SyncHook】在模块构建开始之前触发,可以用来修改模块。
  • succeedModule【SyncHook】模块构建成功时执行。

更多hooks见:webpack.docschina.org/api/compila…

清除无用文件插件实现

作用:清除项目当中无用的文件,支持文件目录、文件后缀筛选扫描,支持对无用文化的删除/移动/恢复操作。

思路:

  1. 获取所有用到的资源,如何获取?

通过以上的知识我们已经知道,webpack在打包构建有一个compilation对象。这个对象包含了此次编译资源的相关信息。获取已使用资源的文件的路径我们就可以之后那些执行被使用了。

  1. 获取扫描目录下所有文件,然后与第一步中的资源信息做对比,筛选出无用的文件。
  2. 对筛选出的无用文件进行删除/移动/恢复操作。

代码实现:juejin.cn/post/711337…

插件发布

  1. 注册一个npm账号。(注意registry地址 npm config set registry registry.npmjs.org/

  2. 执行npm login 进行登录。(OTP二次认证或Token方式验证)

  3. 如插件需要打包压缩操作,进行相关操作。

  4. 填写关于插件的package.json信息,如description、keywords、version等等。

  5. npm publish将你的插件发布到npm官网。