Node.js Express如何在用户关闭页面时终止router.post后续代码执行
Express 接口处理逻辑在用户提前关页时的中断方案
问题场景
用户未等待接口返回数据就提前关闭页面时,需要中断对应router.post处理逻辑中的后续代码执行,避免无效资源消耗。项目基于Express搭建,现有代码如下:
后端 APP.JS
var express = require('express'); var expressSession = require('express-session'); var redis = require('redis'); var connectRedis = require('connect-redis'); var body_parser = require('body-parser'); var compression = require('compression'); var RedisStore = connectRedis(expressSession); var io = require('socket.io'); var data = require('./routes/data.js'); var page = require('./routes/page.js'); var app = express(); app.use(compression()); app.set('view engine', 'ejs'); app.set('views', __dirname + '/pages'); app.use('/', data); app.use('/page', page); io.on('connection', function(socket){ socket.request.session; // 取session socket.id; // 取socket id soc.on('user-abort', function(id){ id.specificUser; // 需要关闭该用户关联连接,终止对应所有同步执行的代码 }); });
路由文件 PAGE.JS
router.post('/page', function(req, res, next) { // 渲染页面返回对应数据 res.render('page'); req.session // 此处获取用户id var data = 'data'; // 模拟分段向用户发送数据的循环逻辑 function sendData(){ console.log("data"); setTimeout(sendData, 1000); } setTimeout(sendData, 1000); // 待解决:用户退出页面时,如何终止当前路由里的后续代码执行 }); module.exports = router;
前端页面 PAGE.EJS
window.onbeforeunload = function(e) { socket.emit('abort', {id : specificUser}, function(){ // 用户退出页面回调 }); };
当前已完成前端页面关闭监听,可通过Socket.io向服务端发送携带用户标识的abort事件,需要补全服务端逻辑,收到中断信号后终止对应用户请求在router.post中未执行完的代码(比如示例中的循环定时sendData任务)。
实现方案
核心思路很简单:给每个用户的请求维护一个异步任务清单,不管是定时器、IO请求还是其他异步操作,都把终止句柄存在清单里,收到中断信号或者检测到连接断开时,批量执行清理即可。
- 全局任务映射表初始化
在APP.JS顶层初始化一个Map,用来存储用户标识和对应异步任务句柄的映射关系:// 放在app.js全局作用域 global.userTaskMap = new Map(); - 路由层改造
所有生成的异步任务都要把终止句柄存入对应清单,同时加上HTTP连接断开的兜底清理逻辑——不要完全依赖前端的unload事件,浏览器崩溃、强杀进程时不会触发前端事件,而HTTP层的close事件是连接断开时原生触发的,可靠性最高:router.post('/page', function(req, res, next) { // 替换成你项目里实际的用户唯一标识,和前端传的specificUser保持一致 const userId = req.session.userId; res.render('page'); // 初始化当前用户的任务列表 if (!global.userTaskMap.has(userId)) { global.userTaskMap.set(userId, []); } const taskList = global.userTaskMap.get(userId); function sendData(){ // 执行逻辑前先判断连接状态,已经断开就直接退出 if (res.writableEnded || res.destroyed) return; console.log("data"); // 把定时器句柄存入任务列表 const loopTimer = setTimeout(sendData, 1000); taskList.push(loopTimer); } // 首次触发的定时器也存入列表 const firstTimer = setTimeout(sendData, 1000); taskList.push(firstTimer); // 兜底:HTTP连接断开时自动清理所有任务 res.on('close', () => { taskList.forEach(timer => clearTimeout(timer)); global.userTaskMap.delete(userId); }); }); module.exports = router; - Socket.io中断逻辑补全
修复原代码里soc的笔误(应该是当前连接的socket实例),收到abort事件时校验权限后清理对应用户的任务:io.on('connection', function(socket){ // 取当前socket连接绑定的用户id const currentUserId = socket.request.session.userId; socket.on('user-abort', function(payload){ const targetUserId = payload.id.specificUser; // 加权限校验,防止恶意用户伪造请求终止他人任务 if (targetUserId !== currentUserId) return; const taskList = global.userTaskMap.get(targetUserId); if (taskList) { taskList.forEach(timer => clearTimeout(timer)); global.userTaskMap.delete(targetUserId); } }); });
补充说明
- 如果路由逻辑里有数据库查询、文件读写、外部接口调用这类异步操作,优先使用支持中断的调用方式:比如Node.js 15+原生
fetch支持AbortSignal、Mongoose查询也可传入abort信号,把对应的abort触发句柄也存入任务列表,就能实现全链路的逻辑终止。 - 任务清理完成后一定要记得从Map里删除对应用户的记录,避免内存泄漏。
内容的提问来源于stack exchange,提问作者proto
相关产品推荐
相关产品推荐

