写过一篇 发表于 2025-2-16 00:38:02

Webpack和Vite插件的开发与使用

在当代开发中一样平常各公司都有自己的监控平台,对前端而言如果浏览器报错的话就可以通过埋点收集错误日志,再结合sourcemap文件可以资助我们定位到错误代码,资助我们排查问题。这里就记录一下之前在webpack和vite两个环境中的插件开发,可以在生产构建时将sourcemap上传到内部的文件服务器配合后续的监控日志来一起使用
Webpack插件开发

Compiler 和 Compilation

在插件开发中有两个概念比较告急,分别是Compiler和Compilation,他们是Plugin和Webpack之间的桥梁。他们的含义如下:


[*]Compiler对象包含了Webpack环境全部的配置信息,包含options、loaders、plugins等这些全部的信息,这个对象在Webpack启动的时间被实例化,是全局唯一的,可以把他看成是Webpack的实例
[*]Compilation 对象包含了当前模块资源以及编译生成资源尚有厘革的文件等相关信息。在webpack以开发模式运行时,每当检测到一个文件厘革,一个新的Compilation就会被创建。Compilation对象提供了很多事件回调给插件做扩展,通过Compilation也能读取到Compiler对象
两者的区别在于:Compiler代表了整个Webpack从启动到关闭的生命周期,而Compilation只是代表了一次新的编译。
事件流

Webpack 就像一条生产线,要颠末一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依靠关系,只有完成当前处理后才能交给下一个流程行止理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个体系扩展性很好。
Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。 Compiler 和 Compilation 都继续自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件。
webpack 插件钩子

每个webpack插件都是一个class,其中最终要的两个内容,一个是constructor,可以接受一个option参数,这个就是用户在使用这个插件时传入的参数,第二个就是apply方法,接受一个compiler这个实例,在webpack初始化时,会调用每个插件,执行其中的apply方法,我们就可以在apply这个方法中使用接受到的Compiler实例上提供的各种hook来监听我们需要的事件,在整个流水线工程中有很多事件钩子,如下图所示:
https://i-blog.csdnimg.cn/direct/6b565affadcc4751bb73549558331e24.png
我们可以监听我们需要的时机,参考所在Webpack插件钩子
在这里我们需要在webpack打包竣事的时间将构建结果中的.map文件上传到自己的文件服务器,所以需要订阅done这个事件钩子,在每次compilation完成时执行。
插件调用方式

在webpack插件中一共有同步和异步两种调用方式,同步调用是直接使用tap异步调用使用tapAsync尚有一种是tapPromise,简朴代码展示如下:
apply(compiler) {
// 同步钩子
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
    // 同步处理
    console.log('同步处理');
});
}
apply(compiler) {
// 异步钩子,使用回调函数
compiler.hooks.done.tapAsync('MyPlugin', (stats, callback) => {
    // 异步处理
    setTimeout(() => {
      console.log('异步处理完成');
      callback();
    }, 1000);
});
}
apply(compiler) {
// 异步钩子,返回 Promise
compiler.hooks.done.tapPromise('MyPlugin', (stats) => {
    return new Promise((resolve) => {
      setTimeout(() => {
      console.log('Promise 异步处理完成');
      resolve();
      }, 1000);
    });
});
}
这里我们需要将文件上传,所以明显是一个异步的调用,接纳的是tapAsync的调用方式,具体代码如下:
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const FormData = require('form-data');

// 将打包文件中的.map文件上传到指定服务器
class UploadSourceMapPlugin {
constructor(options) {
    this.options = options;
    if (!this.options.uploadUrl) {
      throw new Error('uploadUrl is required');
    }
}

apply(compiler) {
    compiler.hooks.done.tapAsync('UploadSourceMapPlugin', async (stats, callback) => {
      try {
      const outputPath = compiler.options.output.path;
      
      // 递归读取目录中的所有文件
      function getAllFiles(dir) {
          const files = fs.readdirSync(dir);
          let fileList = [];
         
          files.forEach(file => {
            const filePath = path.join(dir, file);
            const stat = fs.statSync(filePath);
            
            if (stat.isDirectory()) {
            fileList = fileList.concat(getAllFiles(filePath));
            } else {
            fileList.push(filePath);
            }
          });
         
          return fileList;
      }
      
      // 获取所有文件
      const allFiles = getAllFiles(outputPath);
      
      // 过滤出 .map 文件
      const sourceMapFiles = allFiles
          .filter(file => file.endsWith('.map'))
          .map(file => ({
            name: path.basename(file),
            path: file
          }));
      
      // 上传所有的 source map 文件
      for (const file of sourceMapFiles) {
          const filePath = file.path;
         
          if (fs.existsSync(filePath)) {
            await this.uploadFile(filePath, file.name);
            
            // 如果配置了上传后删除
            if (this.options.deleteAfterUpload) {
            fs.unlinkSync(filePath);
            console.log(`Deleted ${file.name} after upload`);
            }
          }
      }
      // 通知 webpack 异步操作已完成
      // webpack 可以继续执行后续步骤
      // 如果不调用 callback,webpack 的构建过程会一直等待
      callback();
      } catch (error) {
      console.error('Error in UploadSourceMapPlugin:', error);
      callback();
      }
    });
}

async uploadFile(filePath, fileName) {
    try {
      const formData = new FormData();
      formData.append('sourcemap', fs.createReadStream(filePath), fileName);
      
      const response = await axios.post(this.options.uploadUrl, formData, {
      headers: {
          ...formData.getHeaders(),
          'Authorization': this.options.token || ''// 可选的认证token
      }
      });
      
      console.log(`Successfully uploaded ${fileName}`);
      return response.data;
    } catch (error) {
      console.error(`Failed to upload ${fileName}:`, error.message);
      throw error;
    }
}

}

module.exports = UploadSourceMapPlugin;

这里有个小小的坑,就是在compiler中有个state对象也是文件列表,但是这里面只能获取到颠末Webpack编译之后的文件,所以最保险的还是自己递归遍历一下结果文件进行过滤。这里callback是上传竣事之后的回调,告诉webpack已近完成,否则webpack就会不停在这里等待
使用

最终使用这个插件也很简朴,我们一样平常主需要再生产环境使用,开发环境一样平常是没有必要将sourcemap上传上去的。通过下面的配置就可以使用我们开发的插件了
// 只在生产环境使用
    ...(process.env.NODE_ENV === 'production' ? [
      new UploadSourceMapPlugin({
      uploadUrl: 'https://file-server.com/upload',
      token: 'auth-token',// 可选的认证token
      deleteAfterUpload: true    // 是否在上传后删除本地文件
      })
    ] : []),
Vite

Vite 是一个当代的前端构建工具,因其快速、简朴的配置和优化的开发体验而广受欢迎。Vite是基于Rollup来的,速率非常快,如果开发的插件不带Vite特有的钩子一样平常都可以在Rollup中兼容使用。
Vite中的每个插件一样平常都是一个返回一个对象的函数,其中有name字段表插件名称,以及对应的钩子函数,在Vite中有通用钩子和Vite专属钩子。
https://i-blog.csdnimg.cn/direct/8047a50f82914c6bbf282063752cf130.png
参考所在:Vite插件钩子
这里我们在每次生产环境构建竣事时调用这个插件。我们可以用apply: ‘build’, 表示只在构建时调用这个插件,一样平常开始npm run dev时处于开发状态就不会调用这个插件。
具体代码如下:
import fs from 'fs'
import path from 'path'

export default function UploadSourceMapPlugin(options = {}) {
const {
    uploadUrl = '',    // 上传服务器地址
    headers = {},      // 自定义请求头
    deleteAfterUpload = true// 上传后是否删除本地map文件
} = options

if (!uploadUrl) {
    throw new Error('uploadUrl is required for UploadSourceMapPlugin')
}

return {
    name: 'vite-plugin-upload-sourcemap',
    apply: 'build',    // 仅在构建时应用
   
    async closeBundle() {
      const distDir = path.resolve('dist')
      const sourcemaps = []

      // 递归查找所有.map文件
      function findSourceMaps(dir) {
      const files = fs.readdirSync(dir)
      
      files.forEach(file => {
          const fullPath = path.join(dir, file)
          const stat = fs.statSync(fullPath)
         
          if (stat.isDirectory()) {
            findSourceMaps(fullPath)
          } else if (file.endsWith('.map')) {
            sourcemaps.push(fullPath)
          }
      })
      }

      findSourceMaps(distDir)

      // 上传所有sourcemap文件
      for (const mapFile of sourcemaps) {
      const formData = new FormData()
      formData.append('file', fs.createReadStream(mapFile))
      
      try {
          const response = await fetch(uploadUrl, {
            method: 'POST',
            body: formData,
            headers: {
            ...headers
            }
          })

          if (!response.ok) {
            throw new Error(`Upload failed for ${mapFile}`)
          }

          console.log(`Successfully uploaded: ${mapFile}`)

          // 如果配置了上传后删除,则删除本地map文件
          if (deleteAfterUpload) {
            fs.unlinkSync(mapFile)
            console.log(`Deleted local file: ${mapFile}`)
          }
      } catch (error) {
          console.error(`Error uploading ${mapFile}:`, error)
      }
      }
    }
}
}
其中的代码逻辑实在和webpack中基本保持划一。至此我们就开发了一个上传soucemap的两套插件可以分别在Webpack和Rollup中使用了。
参考文件

Webpack插件钩子
Vite插件钩子
Vite插件开发

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Webpack和Vite插件的开发与使用