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来监听我们需要的事件,在整个流水线工程中有很多事件钩子,如下图所示:

我们可以监听我们需要的时机,参考地址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) {// 异步钩子,返回 Promisecompiler.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', // 可选的认证tokendeleteAfterUpload: true // 是否在上传后删除本地文件})] : []),
Vite
Vite 是一个现代的前端构建工具,因其快速、简单的配置和优化的开发体验而广受欢迎。Vite是基于Rollup来的,速度非常快,如果开发的插件不带Vite特有的钩子一般都可以在Rollup中兼容使用。
Vite中的每个插件一般都是一个返回一个对象的函数,其中有name字段表插件名称,以及对应的钩子函数,在Vite中有通用钩子和Vite专属钩子。

参考地址: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文件} = optionsif (!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插件开发
相关文章:
Webpack和Vite插件的开发与使用
在现代开发中一般各公司都有自己的监控平台,对前端而言如果浏览器报错的话就可以通过埋点收集错误日志,再结合sourcemap文件可以帮助我们定位到错误代码,帮助我们排查问题。这里就记录一下之前在webpack和vite两个环境中的插件开发࿰…...
HTTP的状态码
HTTP 状态码 当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(server header)用以响应浏览器的请求。 常见的HTTP状态码 …...
Python函数-装饰器
装饰器 写好的函数,不做任何修改,就可以改变执行内容,在其头或尾部加入新的流程代码本质上就是使用函数嵌套,在内部嵌套定义的函数中调用原函数,从而可读在前或后加入新的代码使用的关键: 将原函数作为参数…...
【数据可视化-17】基于pyecharts的印度犯罪数据可视化分析
🧑 博主简介:曾任某智慧城市类企业算法总监,目前在美国市场的物流公司从事高级算法工程师一职,深耕人工智能领域,精通python数据挖掘、可视化、机器学习等,发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…...
HTTP请求报文头和相应报文头
一、HTTP请求报文头 HTTP请求报文由请求行、请求头和请求体组成。请求头包含客户端向服务器发送的附加信息。 1.1 请求行 格式: 方法 请求URI HTTP/版本示例: GET /index.html HTTP/1.1 方法: 请求类型,如GET、POST、PUT、DELETE等。 请求URI: 请求的资源…...
19.4.9 数据库方式操作Excel
版权声明:本文为博主原创文章,转载请在显著位置标明本文出处以及作者网名,未经作者允许不得用于商业目的。 本节所说的操作Excel操作是讲如何把Excel作为数据库来操作。 通过COM来操作Excel操作,请参看第21.2节 在第19.3.4节【…...
BFS 走迷宫
#include<bits/stdc.h> using namespace std; int a[100][100],v[100][100];//访问数组 n,m<100 struct point {int x;int y;int step; }; queue<point> r;//申请队列 int dx[4]{0,1,0,-1};//四个方向 右下左上 int dy[4]{1,0,-1,0}; int main() { /* 5 4 1 …...
【Linux系统】—— 简易进度条的实现
【Linux系统】—— 简易进度条的实现 1 回车和换行2 缓冲区3 进度条的准备代码4 第一版进度条5 第二版进度条 1 回车和换行 先问大家一个问题:回车换行是什么,或者说回车和换行是同一个概念吗? 可能大家对回车换行有一定的误解࿰…...
Qt 中使用 SQLite 数据库的完整指南
SQLite 是一款轻量级、嵌入式的关系型数据库,无需独立的服务器进程,数据以文件形式存储,非常适合桌面和移动端应用的本地数据管理。Qt 通过 Qt SQL 模块提供了对 SQLite 的原生支持,开发者可以轻松实现数据库的增删改查、事务处理…...
数智化时代的工单管理:从流程驱动到数据驱动-亿发
在数智化时代,工单管理系统已从简单的任务分发工具演变为企业运营的智能中枢。传统工单系统关注流程的线性推进,而现代工单管理系统则强调数据的全生命周期管理,通过智能算法实现工单的自动分配、优先级判定和效能优化。这种转变不仅提升了运…...
Large Language Model Distilling Medication Recommendation Model
摘要:药物推荐是智能医疗系统的一个重要方面,因为它涉及根据患者的特定健康需求开具最合适的药物。不幸的是,目前使用的许多复杂模型往往忽视医疗数据的细微语义,而仅仅严重依赖于标识信息。此外,这些模型在处理首次就…...
floodfill算法系列一>被围绕的区域
目录 整体思想:代码设计:代码呈现: 整体思想: 代码设计: 代码呈现: class Solution {int m,n;int[] dx {0,0,-1,1};int[] dy {-1,1,0,0};public void solve(char[][] board) {m board.length;n board[…...
Redis 01 02章——入门概述与安装配置
一、入门概述 (1)是什么 Redis:REmote Dictionary Server(远程字典服务器)官网解释:Remote Dictionary Server(远程字典服务)是完全开源的,使用ANSIC语言编写遵守BSD协议,是一个高…...
windows基于cpu安装pytorch运行faster-whisper-large-v3实现语音转文字
1.创建虚拟环境 conda create -n faster-whisper python3.10 conda activate faster-whisper 2.安装cpu版本的pytorch pip3 install torch torchvision torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple 3.验证pytorch安装结果 (faster-whisper) H:\big-model\faste…...
AI大模型(如GPT、BERT等)可以通过自然语言处理(NLP)和机器学习技术,显著提升测试效率
在软件测试中,AI大模型(如GPT、BERT等)可以通过自然语言处理(NLP)和机器学习技术,显著提升测试效率。以下是几个具体的应用场景及对应的代码实现示例: 1. 自动生成测试用例 AI大模型可以根据需求文档或用户故事自动生成测试用例。 代码示例(使用 OpenAI GPT API): …...
【Prometheus】prometheus黑盒监控balckbox全面解析与应用实战
✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…...
CSS实现单行、多行文本溢出显示省略号(…)
在网页设计中,我们常常遇到这样的情况:文本内容太长,无法完全显示在一个固定的区域内。为了让界面看起来更整洁,我们可以使用省略号(…)来表示内容溢出。这不仅能提升用户体验,还能避免内容溢出…...
服务器中部署大模型DeepSeek-R1 | 本地部署DeepSeek-R1大模型 | deepseek-r1部署详细教程
0. 部署前的准备 首先我们需要足够算力的机器,这里我在vultr中租了有一张A16显卡一共16GB显存的服务器作为演示。部署的模型参数为14b的。如果需要部署满血版本671b的,需要更大的算力支持,这里由于是个人资金有限,就演示14b的部署…...
元学习之孪生网络Siamese Network
简介:元学习是一种思想,一般以神经网络作为特征嵌入的工具,实现对数据特征的提取,然后通过构造某种指标以引导优化器对模型参数进行优化。而最小化距离是最常见的学习目标,这就是熟知的度量学习,度量学习里…...
深入HBase——引入
引入 前面我们通过深入HDFS到深入MapReduce ,从设计和落地,去深入了解了大数据最底层的基石——存储与计算是如何实现的。 这个专栏则开始来看大数据的三驾马车中最后一个。 通过前面我们对于GFS和MapReduce论文实现的了解,我们知道GFS在数…...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
3.3.1_1 检错编码(奇偶校验码)
从这节课开始,我们会探讨数据链路层的差错控制功能,差错控制功能的主要目标是要发现并且解决一个帧内部的位错误,我们需要使用特殊的编码技术去发现帧内部的位错误,当我们发现位错误之后,通常来说有两种解决方案。第一…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...
LRU 缓存机制详解与实现(Java版) + 力扣解决
📌 LRU 缓存机制详解与实现(Java版) 一、📖 问题背景 在日常开发中,我们经常会使用 缓存(Cache) 来提升性能。但由于内存有限,缓存不可能无限增长,于是需要策略决定&am…...
jmeter聚合报告中参数详解
sample、average、min、max、90%line、95%line,99%line、Error错误率、吞吐量Thoughput、KB/sec每秒传输的数据量 sample(样本数) 表示测试中发送的请求数量,即测试执行了多少次请求。 单位,以个或者次数表示。 示例:…...
LLaMA-Factory 微调 Qwen2-VL 进行人脸情感识别(二)
在上一篇文章中,我们详细介绍了如何使用LLaMA-Factory框架对Qwen2-VL大模型进行微调,以实现人脸情感识别的功能。本篇文章将聚焦于微调完成后,如何调用这个模型进行人脸情感识别的具体代码实现,包括详细的步骤和注释。 模型调用步骤 环境准备:确保安装了必要的Python库。…...
