携手创作,共同成长!这是我参与「日新计划 8 月更文挑战」的第5天,点击查看活动详情

Webpack 之 来,手写一个简单常用插件clean-webpack-plugin

webpack是目前最流行的构建工具,当前比较主流的React、Vue(2)前端框架都内置了webpack进行javaScript模块编译。

前言

loader和plugin是webpack最核心的两个概念,最近正在学习plugin的知识,为了加深对plugin的理解,尝试仿写一个功能简单的plugin — clean-webpack-plugin

完整代码在文末

前置知识

需要对node内置模块有一定的了解,如:fs、path模块 需要了解webpack Plugins的APIPlugins

clean-webpack-plugin

clean-webpack-plugin:用于清空/删除构建文件夹的内容。

为什么要用?

当每次执行项目打包命令时,都会在指定路径中生成打包后的代码文件:

// webpack配置:
module.exports = {
  mode: 'development',
  output: {
    path: __dirname + '/dist',
    filename: '[name]-[chunkhash:5].js'
  },
}

打包结果:

Webpack 之 来,手写一个简单常用插件clean-webpack-plugin

每次修改了文件并重新打包后,都会生成不同的js文件:

Webpack 之 来,手写一个简单常用插件clean-webpack-plugin

这样就导致最终构建出来的代码文件夹“不干净”,所以就有了我们的clean-webpack-plugin插件,每次执行打包时先清空/删除构建文件夹,再将编译出来的资源添加到构建文件夹,确保构建文件夹中的内容都是最新的。

怎么实现?

了解了clean-webpack-plugin是干什么的再去实现它就会相对容易些了,代码实现:

class CleanWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('CleanWebpackPlugin', (compilation, callback) => {
      // 获取webpack - output的配置项
      const outputOpts = compiler.options.output;
      // 删除目标文件夹
      removeDir(outputOpts.path);
      callback();
    })
  }
}

1、通过Plugins使用clean-webpack-plugin的方式也能看出来,clean-webpack-plugin暴露了一个CleanWebpackPlugin

2、apply:apply方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象(详情可以了解一下webpack的编译过程)。

3、compiler.hooks.emit.tapAsync: 在webpack编译的生命周期钩子中注册回调函数。emit指的是在输出构建资源到output目录之前执行。更多compiler钩子

4、compiler.options.output:获取webpack config中的output配置项

5、removeDir:自定义函数,用node内置模块fs、path清空构建目录内容,代码:

function removeDir(outputPath, exclude) {
   // 文件夹路径存在
   if(fs.existsSync(outputPath)) {
     // 读取文件夹内容
     fs.readdirSync(outputPath).forEach(file =>{
       const dirPath = path.join(outputPath, file);
       if (fs.statSync(dirPath).isDirectory()) {
         // 目标文件夹中还存在文件夹,则递归删除
         removeDir(dirPath, exclude)
       } else {
         // 文件直接删除
         fs.unlinkSync(dirPath);
       }
     });
     // 删除目标文件夹
     fs.rmdirSync(outputPath);
   }
}

这样就实现了一个丐版的clean-webpack-plugin插件。

为什么是丐版?

查看clean-webpack-plugin源码仓库会发现他还提供了很多可选配置项,而本文实现的插件并不支持这些配置。为了摆脱丐版这个称号,那就支持配置吧!

可选配置: 允许在创建clean-webpack-plugin实例时传递一个Object进行配置:

Options: {
    // 配置构建文件夹中哪些文件不需要清空/删除
    exclude: {
        type: Array,
        default: []
    },
    // 没了( ̄▽ ̄)"
}

在webpack.config.js文件中添加配置:

    plugins: [
        new CleanWebpackPlugin({ exclude:['main-85e85.js'] })
      ]

改造cleanWebpackPlugin.js

    // 
    class CleanWebpackPlugin {
        // 接收创建实例时传递的配置
      constructor(options) {
          // 混合配置,若未进行任何配置,则使用默认值
        this.options = { exclude: [], ...options };
      }
      apply(compiler) {
        compiler.hooks.emit.tapAsync('CleanWebpackPlugin', (compilation, callback) => {
          // 获取webpack - output的配置项
          const outputOpts = compiler.options.output;
          // 删除目标文件夹时传递配置项(配置项多时可以传递这个options,removeDir收参进行相应修改)
          removeDir(outputOpts.path, this.options.exclude);
          callback();
        })
      }
    }

修改removeDir方法

function removeDir(outputPath, exclude) {
  if(fs.existsSync(outputPath)) {
    fs.readdirSync(outputPath).forEach(file =>{
      const dirPath = path.join(outputPath, file);
      // 判断当前文件是否不需要删除
      if(exclude.length && !exclude.includes(file)) {
        if (fs.statSync(dirPath).isDirectory()) {
          removeDir(dirPath, exclude)
        } else {
          fs.unlinkSync(dirPath);
        }
      }
    });
    // 添加配置项后就需要多一步这个操作: 再次读取文件夹内容,内容为空则删除文件夹
    if(!fs.readdirSync(outputPath).length) {
      fs.rmdirSync(outputPath);
    }
  }
}

结语

至此就实现了一个可配置的mini版clean-webpack-plugin插件了。

完整代码

webpack.config.js

const { CleanWebpackPlugin } = require('./plugins/CleanWebpackPlugin');
module.exports = {
  mode: 'development',
  devtool: 'source-map',
  output: {
    path: __dirname + '/dist',
    filename: '[name]-[chunkhash:5].js'
  },
  plugins: [
    new CleanWebpackPlugin({ exclude:['main-85e85.js'] })
  ]
}

clean-webpack-plugin

const fs = require('fs');
const path = require('path');
function removeDir(outputPath, exclude) {
  // 文件夹路径存在
  if(fs.existsSync(outputPath)) {
    // 读取文件夹内容,
    fs.readdirSync(outputPath).forEach(file =>{
      const dirPath = path.join(outputPath, file);
      if(exclude.length && !exclude.includes(file)) {
        if (fs.statSync(dirPath).isDirectory()) {
          // 目标文件夹中还存在文件夹,则递归删除
          removeDir(dirPath, exclude)
        } else {
          // 文件直接删除
          fs.unlinkSync(dirPath);
        }
      }
    });
    // 再次读取文件夹内容,如果内容为空则删除文件夹
    if(!fs.readdirSync(outputPath).length) {
      fs.rmdirSync(outputPath);
    }
  }
}
class CleanWebpackPlugin {
  constructor(options) {
    this.options = { exclude: [], ...options };
  }
  apply(compiler) {
    compiler.hooks.emit.tapAsync('CleanWebpackPlugin', (compilation, callback) => {
      // 获取webpack - output的配置项
      const outputOpts = compiler.options.output;
      // 删除目标文件夹
      removeDir(outputOpts.path, this.options.exclude);
      callback();
    })
  }
}
module.exports = {
  CleanWebpackPlugin
}