webpack插件开发入门其实很简单

101 阅读3分钟

webpack构建/编译过程中有一些生命周期,webpack插件开发就是你可以调用这些生命周期的钩子,在某些生命周期去执行你需要的操作,例如生成文件,修改文件内容,删除文件。

生命周期的类型有tap,tapAsync 和 tapPromise,同步、异步钩子,具体的看官方文档

插件开发的步骤官方文档也写了比较清楚的解释,现在我这里做一个案例,在umi脚手架中去实现自己的webpack插件。

插件功能:把某个文件夹内的文件路径,转换成json格式文件。

  1. 新建plugins文件夹,里面新建source-path-plugin.js文件
  2. source-path-plugin.js文件里面写一个SourcePathPlugin类
source-path-plugin.js

class SourcePathPlugin {
  constructor({ sourcePath = 'src',  }) {
    this.sourcePath = sourcePath; //插件接收的参数
  }
}

module.exports = SourcePathPlugin;
  1. 根据官方文档,定义一个 apply 方法,以 compiler 为参数。
source-path-plugin.js

class SourcePathPlugin {
  constructor({ sourcePath = 'src',  }) {
    this.sourcePath = sourcePath; //插件接收的参数
  }
  apply(compiler) {
    // 指定一个挂载到 webpack 自身的事件钩子
    compiler.hooks.environment.tap('SourcePathPlugin', () => {
     //这里实现生成src路径json文件
    });
  }
}

module.exports = SourcePathPlugin;
  1. 接下用fs来读取文件和写入文件
source-path-plugin.js
class SourcePathPlugin {
  constructor({ sourcePath = 'src',  }) {
    this.sourcePath = sourcePath; //插件接收的参数
  }
  // 递归获取文件路径树数据
  static getFileTreeData(path, obj) {
    const files = fs.readdirSync(path, { withFileTypes: true });
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      if (file.name[0] == '.') {
        continue;
      }
      const filePath = `${path}/${file.name}`;
      if (file.isFile()) {
        obj[file.name] = filePath;
      }
      if (file.isDirectory()) {
        let childObj = {};
        SourcePathPlugin.getFileTreeData(filePath, childObj);
        obj[file.name] = childObj;
      }
    }
  }
  apply(compiler) {
    // 指定一个挂载到 webpack 自身的事件钩子
    compiler.hooks.environment.tap('SourcePathPlugin', () => {
     
      const fileTree = {};
      //获取src文件夹路径下的文件路径json,结果放在fileTree
      SourcePathPlugin.getFileTreeData(this.sourcePath, fileTree);
      // 当public文件夹中没有code文件夹时,创建code文件夹
      if (!fs.existsSync('public/code')) {
        fs.mkdirSync('public/code', { recursive: true });
      }
      // 在code文件夹中写入path.json文件
      const json = JSON.stringify(fileTree, null, 2);
      fs.writeFileSync('public/code/path.json', json);
    });
  }
}

module.exports = SourcePathPlugin;
  1. 插件已写好,接下来在webpack配置中使用插件。
webpack.config.js
const SourcePathPlugin = require('./plugins/source-path-plugin');//引入插件

memo.plugin('source-path-plugin').use(SourcePathPlugin, [
    {
      sourcePath: 'src',
    },
]);

到这里一个插件的开发和使用就完成了。

  1. 扩展插件的功能,在编译完成后对文件夹进行复制或删除。以下是完整代码
source-path-plugin.js
// 自定义 webpack plugin.

const fs = require('fs');
const path = require('path');

class SourcePathPlugin {
  constructor({ sourcePath = 'src', deletePath, copy }) {
    this.sourcePath = sourcePath;
    this.copy = copy;
    this.deletePath = deletePath;
  }
  // 获取文件路径树数据
  static getFileTreeData(path, obj) {
    const files = fs.readdirSync(path, { withFileTypes: true });
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      if (file.name[0] == '.') {
        continue;
      }
      const filePath = `${path}/${file.name}`;
      if (file.isFile()) {
        obj[file.name] = filePath;
      }
      if (file.isDirectory()) {
        let childObj = {};
        SourcePathPlugin.getFileTreeData(filePath, childObj);
        obj[file.name] = childObj;
      }
    }
  }

  // 复制文件夹内的文件
  static copyFolderSync(source, target) {
    // 检查目标文件夹是否存在,如果不存在,则创建
    if (!fs.existsSync(target)) {
      fs.mkdirSync(target);
    }
    // 读取源文件夹下的所有文件/文件夹
    const files = fs.readdirSync(source);
    for (let file of files) {
      let sourcePath = path.join(source, file);
      let targetPath = path.join(target, file);

      // 检查是文件还是文件夹
      if (fs.lstatSync(sourcePath).isDirectory()) {
        // 如果是文件夹,则递归复制
        copyFolderSync(sourcePath, targetPath);
      } else {
        // 如果是文件,则直接复制

        fs.copyFileSync(sourcePath, targetPath);
      }
    }
  }

  // 删除文件或文件夹
  static deleteFolderRecursive(directory) {
    if (fs.existsSync(directory)) {
      //  Node.js v14.0.0 ,recursive选项可删除文件夹内容
      fs.rmdirSync(directory, { recursive: true });
    } else {
      // 删除文件
      fs.unlinkSync(directory);
    }
  }

  apply(compiler) {
    // 生成src路径json文件
    compiler.hooks.environment.tap('SourcePathPlugin', () => {
      const fileTree = {};
      SourcePathPlugin.getFileTreeData(this.sourcePath, fileTree);
      if (!fs.existsSync('public/code')) {
        fs.mkdirSync('public/code', { recursive: true });
      }
      const json = JSON.stringify(fileTree, null, 2);
      fs.writeFileSync('public/code/path.json', json);
    });

    // 构建完后复制文件和删除文件
    compiler.hooks.done.tap('SourcePathPlugin', stats => {
      const environment = process.env.NODE_ENV;
      if (environment == 'production') {
        let arr = [];
        this.copy.map(item => {
          arr.push(SourcePathPlugin.copyFolderSync(item.from, item.to));
        });
        this.deletePath.map(item => {
          arr.push(SourcePathPlugin.deleteFolderRecursive(item));
        });
        Promise.all(arr);
      }
    });
  }
}

module.exports = SourcePathPlugin;
webpack.config.js
const SourcePathPlugin = require('./plugins/source-path-plugin');

memo.plugin('source-path-plugin').use(SourcePathPlugin, [
    {
      sourcePath: 'src',
      deletePath: ['dist/src/.umi', 'dist/src/.umi-production'],
    },
  ]);