面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 01:22

React Query 性能优化的常见瓶颈和解决方案有哪些?

React Query 最大性能陷阱是组件过度渲染:query 数据变化时所有订阅组件都重渲染。解法是用 select 提取组件关心的子字段,select 返回值用浅比较去重,相等则跳过渲染。第二陷阱是缓存策略不当:staleTime 控制数据何时标记为过期触发重新请求(默认0即立即过期),gcTime(v5 前叫 cacheTime)控制未使用的缓存何时被垃圾回收(默认5分钟)。不常变的数据应设较长 staleTime 避免无谓请求。第三是 queryKey 设计:key 变化就触发新请求,key 中包含频繁变化的值(如时间戳)会导致缓存失效。queryKey 应稳定且分层:['users', 'list', { status: 'active' }]。追问staleTime 和 gcTime 有什么区别?staleTime 决定数据是否需要重新获取(过期前用缓存,过期后下次挂载或窗口聚焦时 refetch);gcTime 决定缓存数据在内存中保留多久,观察者归零后开始倒计时,到期彻底删除。一个管新鲜度,一个管生命周期。select 怎么避免不必要的渲染?select 每次查询都会执行,但只有返回值与上次浅比较不同时才触发渲染。返回新对象/数组每次都是新引用会失效,需确保返回原始值或用结构化分享的子集。useInfiniteQuery 如何优化?每页数据独立缓存,默认所有页面变化都触发渲染。可用 select 只取当前视口需要的数据;配合虚拟滚动(如 react-virtual)避免渲染长列表;getNextPageParam 返回 undefined 自动停止加载。如何实现乐观更新?用 useMutation 的 onMutate 在请求发出前乐观修改缓存(queryClient.setQueryData),onError 时用 onMutate 保存的快照回滚,onSettled 时 invalidate 相关 query 保证最终一致。写段代码// select + staleTime 减少渲染和请求const { data: name } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id), staleTime: 5 * 60 * 1000, select: (data) => data.name,});
服务端阅读 05月29日 01:22

SSH 连接失败如何排查?常见原因有哪些?

SSH 故障按报错分四类:Connection refused 说明目标端口未监听(sshd 未运行或防火墙阻断);Connection timeout 说明网络不可达(路由/防火墙丢弃包);Permission denied 是认证失败(密钥不匹配或权限错误);Host key verification failed 是 knownhosts 中记录的指纹与服务器当前密钥不一致。排查第一步始终是 ssh -vvv 看详细握手过程,日志会精确显示卡在哪个阶段。密钥权限是最常见的低级错误:私钥必须 600,.ssh 目录 700,authorizedkeys 600,权限过宽 sshd 会拒绝认证。追问ssh -vvv 输出中各阶段分别对应什么?三个 v 递增详细度。-v 显示连接建立、密钥交换、认证尝试;-vv 增加配置解析和 IO 细节;-vvv 再增加包级调试。重点关注 debug1: Authentications that can continue 和 debug1: Offering public key 两行,前者看服务端支持哪些认证方式,后者看客户端尝试了哪些密钥。连接频繁断开怎么解决?通常是 NAT/防火墙空闲连接超时淘汰。客户端配置 ServerAliveInterval 60(每60秒发心跳),服务端配置 ClientAliveInterval 300。autossh 可自动重连断开的会话。known_hosts 冲突一定是重装系统吗?不一定。IP 被复用、服务器更新了主机密钥、或中间人攻击都会导致。确认安全后用 ssh-keygen -R host 删除旧记录,不可盲目忽略否则失去 MITM 保护。如何不用密码只允许密钥登录?服务端 /etc/ssh/sshd_config 设置 PasswordAuthentication no 和 PubkeyAuthentication yes,改完 sshd -t 验证语法再 systemctl restart sshd。写段代码# 客户端 SSH 配置 ~/.ssh/configHost myserver HostName 10.0.0.1 User deploy Port 2222 IdentityFile ~/.ssh/deploy_key ServerAliveInterval 60
服务端阅读 05月29日 01:22

Cookie 和 Session 有什么区别?何时用 Cookie,何时用 Session?

Cookie 存在客户端浏览器,每次请求自动携带,上限约 4KB,可被用户查看和篡改;Session 存在服务端(内存/Redis/数据库),客户端只持有 Session ID(通常通过 Cookie 传递),数据大小无硬性限制,安全性更高。核心区别是状态存储位置:Cookie 是客户端状态,Session 是服务端状态。选择依据:非敏感偏好数据(主题、语言)用 Cookie;登录态、权限等敏感数据用 Session。Session 的局限在于服务端需存储状态,分布式场景需用 Redis 等集中式存储共享 Session,否则粘性Session限制了水平扩展。JWT 是第三条路:将状态编码进 Token 本身,服务端无状态验证签名即可,但 Token 体积大且无法主动失效。追问Session ID 被窃取会怎样?如何防范?攻击者可用截获的 Session ID 冒充用户(Session 劫持)。防范:Cookie 标记 HttpOnly+Secure,绑定 IP/UA 校验,使用 SameSite 属性,Session 定期轮换 ID,设置合理超时。分布式系统中 Session 如何共享?三种方案:1) 粘性 Session(Nginx ip_hash),简单但不利于负载均衡;2) 集中存储(Redis/Memcached),最常用;3) Session 复制(Tomcat 集群广播),数据量大时性能差。JWT 相比 Session 的优劣?优势:无状态,服务端不存数据,天然支持分布式。劣势:无法主动注销(需黑名单机制又回到有状态),Token 体积大于 Session ID,续期机制复杂(双 Token 方案)。Cookie 被禁用怎么办?Session ID 可通过 URL 参数传递(;jsessionid=xxx),但会暴露在日志和 Referer 中,安全性差。更推荐在登录页提示用户启用 Cookie。写段代码// Express 中 Session 存入 Redisconst session = require('express-session');const RedisStore = require('connect-redis');app.use(session({ store: new RedisStore({ client: redisClient }), secret: 'your-secret', resave: false, cookie: { httpOnly: true, secure: true, maxAge: 3600000 }}));
服务端阅读 05月29日 01:21

OpenCV.js 在移动端和 Web 应用中有哪些最佳实践?

OpenCV.js 在移动端和 Web 应用中有广泛的应用,但需要考虑性能、兼容性和用户体验。以下是移动端和 Web 应用的最佳实践:1. 移动端优化策略响应式设计class MobileImageProcessor { constructor() { this.isMobile = this.detectMobile(); this.processingSize = this.getOptimalSize(); } detectMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } getOptimalSize() { if (this.isMobile) { // 移动端使用较小尺寸 return { width: Math.min(window.innerWidth, 640), height: Math.min(window.innerHeight, 480) }; } else { // 桌面端可以使用较大尺寸 return { width: 1280, height: 720 }; } } resizeImage(src) { let dst = new cv.Mat(); try { cv.resize(src, dst, new cv.Size(this.processingSize.width, this.processingSize.height)); return dst; } catch (error) { console.error('Resize error:', error); return src.clone(); } }}触摸事件处理class TouchHandler { constructor(canvasId) { this.canvas = document.getElementById(canvasId); this.setupTouchEvents(); } setupTouchEvents() { let startX, startY; this.canvas.addEventListener('touchstart', (e) => { e.preventDefault(); const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; }); this.canvas.addEventListener('touchmove', (e) => { e.preventDefault(); const touch = e.touches[0]; const deltaX = touch.clientX - startX; const deltaY = touch.clientY - startY; // 处理触摸移动 this.handleTouchMove(deltaX, deltaY); startX = touch.clientX; startY = touch.clientY; }); this.canvas.addEventListener('touchend', (e) => { e.preventDefault(); this.handleTouchEnd(); }); } handleTouchMove(deltaX, deltaY) { // 实现触摸移动逻辑 console.log(`Touch move: ${deltaX}, ${deltaY}`); } handleTouchEnd() { // 实现触摸结束逻辑 console.log('Touch end'); }}2. PWA(渐进式 Web 应用)集成Service Worker 缓存 OpenCV.js// sw.jsconst CACHE_NAME = 'opencv-pwa-v1';const urlsToCache = [ '/', '/index.html', 'https://docs.opencv.org/4.8.0/opencv.js'];self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME) .then((cache) => cache.addAll(urlsToCache)) );});self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request) .then((response) => { if (response) { return response; } return fetch(event.request); }) );});离线支持class OfflineImageProcessor { constructor() { this.isOnline = navigator.onLine; this.setupOfflineSupport(); } setupOfflineSupport() { window.addEventListener('online', () => { this.isOnline = true; console.log('Back online'); }); window.addEventListener('offline', () => { this.isOnline = false; console.log('Gone offline'); }); } async processImage(image) { if (!this.isOnline) { // 离线模式:使用本地处理 return this.processLocally(image); } else { // 在线模式:可以选择使用云端处理 return this.processWithFallback(image); } } processLocally(image) { let src = cv.imread(image); let dst = new cv.Mat(); try { cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.Canny(dst, dst, 50, 100); return dst; } finally { src.delete(); } } processWithFallback(image) { try { // 尝试云端处理 return this.processCloud(image); } catch (error) { console.warn('Cloud processing failed, falling back to local'); return this.processLocally(image); } }}3. 性能监控和优化实时性能监控class PerformanceMonitor { constructor() { this.metrics = { fps: 0, frameTime: 0, memoryUsage: 0 }; this.frameCount = 0; this.lastTime = performance.now(); this.startMonitoring(); } startMonitoring() { setInterval(() => { this.updateMetrics(); this.displayMetrics(); }, 1000); } updateMetrics() { const currentTime = performance.now(); const deltaTime = currentTime - this.lastTime; this.metrics.fps = Math.round(this.frameCount * 1000 / deltaTime); this.metrics.frameTime = deltaTime / this.frameCount; if (performance.memory) { this.metrics.memoryUsage = Math.round(performance.memory.usedJSHeapSize / 1024 / 1024); } this.frameCount = 0; this.lastTime = currentTime; } recordFrame() { this.frameCount++; } displayMetrics() { console.table(this.metrics); } getMetrics() { return { ...this.metrics }; }}自适应质量调整class AdaptiveQualityProcessor { constructor() { this.quality = 1.0; this.monitor = new PerformanceMonitor(); this.adjustQuality(); } adjustQuality() { setInterval(() => { const metrics = this.monitor.getMetrics(); if (metrics.fps < 20) { // 性能差,降低质量 this.quality = Math.max(0.5, this.quality - 0.1); console.log(`Reducing quality to ${this.quality}`); } else if (metrics.fps > 50 && this.quality < 1.0) { // 性能好,提高质量 this.quality = Math.min(1.0, this.quality + 0.1); console.log(`Increasing quality to ${this.quality}`); } }, 2000); } processImage(src) { let dst = new cv.Mat(); const size = new cv.Size( Math.round(src.cols * this.quality), Math.round(src.rows * this.quality) ); try { cv.resize(src, dst, size); this.monitor.recordFrame(); return dst; } finally { // dst 由调用者负责释放 } }}4. 电池优化电池状态感知class BatteryAwareProcessor { constructor() { this.batteryLevel = 1.0; this.isCharging = false; this.setupBatteryListener(); } setupBatteryListener() { if ('getBattery' in navigator) { navigator.getBattery().then((battery) => { this.batteryLevel = battery.level; this.isCharging = battery.charging; battery.addEventListener('levelchange', () => { this.batteryLevel = battery.level; this.adjustProcessing(); }); battery.addEventListener('chargingchange', () => { this.isCharging = battery.charging; this.adjustProcessing(); }); }); } } adjustProcessing() { if (this.batteryLevel < 0.2 && !this.isCharging) { // 低电量且未充电,降低处理强度 this.setProcessingMode('low'); } else if (this.batteryLevel > 0.5 || this.isCharging) { // 电量充足或正在充电,正常处理 this.setProcessingMode('normal'); } } setProcessingMode(mode) { console.log(`Setting processing mode to: ${mode}`); // 根据模式调整处理参数 }}5. Web Worker 集成后台图像处理// 主线程class WorkerImageProcessor { constructor() { this.worker = new Worker('image-processor-worker.js'); this.pendingTasks = new Map(); this.taskId = 0; } processImage(imageData) { return new Promise((resolve, reject) => { const taskId = this.taskId++; this.pendingTasks.set(taskId, { resolve, reject }); this.worker.postMessage({ taskId, imageData, operation: 'edge-detection' }, [imageData.data.buffer]); }); } processVideoFrame(videoElement) { const canvas = document.createElement('canvas'); canvas.width = videoElement.videoWidth; canvas.height = videoElement.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(videoElement, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); return this.processImage(imageData); }}// image-processor-worker.jsself.onmessage = function(e) { const { taskId, imageData, operation } = e.data; try { let src = cv.matFromImageData(imageData); let dst = new cv.Mat(); switch (operation) { case 'edge-detection': cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.Canny(dst, dst, 50, 100); break; case 'blur': cv.GaussianBlur(src, dst, new cv.Size(15, 15), 0); break; } const result = new ImageData( new Uint8ClampedArray(dst.data), dst.cols, dst.rows ); self.postMessage({ taskId, result }, [result.data.buffer]); src.delete(); dst.delete(); } catch (error) { self.postMessage({ taskId, error: error.message }); }};6. 移动端特定优化摄像头访问优化class MobileCameraHandler { constructor() { this.stream = null; this.constraints = this.getOptimalConstraints(); } getOptimalConstraints() { const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent); if (isMobile) { return { video: { facingMode: 'environment', // 使用后置摄像头 width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 30 } }, audio: false }; } else { return { video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 60 } }, audio: false }; } } async startCamera() { try { this.stream = await navigator.mediaDevices.getUserMedia(this.constraints); return this.stream; } catch (error) { console.error('Camera access error:', error); // 降级方案 if (this.constraints.video.width.ideal > 640) { this.constraints.video.width.ideal = 640; this.constraints.video.height.ideal = 480; return this.startCamera(); } throw error; } } stopCamera() { if (this.stream) { this.stream.getTracks().forEach(track => track.stop()); this.stream = null; } }}7. 完整的移动端应用示例class MobileCVApp { constructor() { this.processor = new MobileImageProcessor(); this.camera = new MobileCameraHandler(); this.battery = new BatteryAwareProcessor(); this.monitor = new PerformanceMonitor(); this.isRunning = false; } async init() { await this.camera.startCamera(); this.setupUI(); } setupUI() { const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); video.srcObject = this.camera.stream; video.onloadedmetadata = () => { canvas.width = video.videoWidth; canvas.height = video.videoHeight; this.startProcessing(); }; } startProcessing() { this.isRunning = true; this.processFrame(); } processFrame() { if (!this.isRunning) return; const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); let src = cv.imread(video); let dst = new cv.Mat(); try { // 根据电池状态调整处理 if (this.battery.batteryLevel < 0.2) { cv.resize(src, src, new cv.Size(src.cols / 2, src.rows / 2)); } // 图像处理 cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.Canny(dst, dst, 50, 100); cv.imshow(canvas.id, dst); this.monitor.recordFrame(); requestAnimationFrame(() => this.processFrame()); } finally { src.delete(); dst.delete(); } } stop() { this.isRunning = false; this.camera.stopCamera(); }}// 使用const app = new MobileCVApp();app.init();总结移动端和 Web 应用中使用 OpenCV.js 需要考虑:性能优化:降低处理分辨率,使用 Web Worker用户体验:响应式设计,触摸事件处理资源管理:电池优化,内存管理离线支持:PWA 集成,Service Worker 缓存兼容性:检测设备能力,提供降级方案监控和调试:实时性能监控,自适应质量调整通过这些最佳实践,可以在移动端和 Web 应用中提供流畅的 OpenCV.js 体验。
服务端阅读 05月29日 01:21

Session Cookie 和 Persistent Cookie 有什么区别?

Session Cookie 不设 Expires 和 Max-Age 属性,浏览器将其存在内存中,关闭浏览器即消失;Persistent Cookie 设有明确的过期时间,存储在磁盘上,过期前跨会话持久存在。服务端通过 Set-Cookie 响应头控制类型:不设过期属性是 Session Cookie,设 Max-Age 或 Expires 则为 Persistent Cookie。安全层面两者都应标记 HttpOnly 和 Secure,Persistent Cookie 因长期驻留磁盘更易被窃取,敏感数据应避免持久化。SameSite 属性(Strict/Lax/None)对两者同样适用,防止 CSRF 攻击。追问浏览器关闭后 Session Cookie 一定被清除吗?不一定。现代浏览器有恢复会话功能(如 Chrome 的继续浏览上次打开的网页),会将 Session Cookie 写入磁盘以便恢复,实际表现等同于 Persistent Cookie。依赖关闭即清除做安全假设是不可靠的。Max-Age 和 Expires 有什么区别?Max-Age 是相对秒数,从收到 Cookie 时起算;Expires 是绝对时间点,依赖客户端时钟。Max-Age 优先级更高(RFC 6265),两者都不设则为 Session Cookie。Session Cookie 能存多少数据?和 Persistent Cookie 一样受 4KB 限制,区别仅在于生命周期,不在容量。大量数据应改用 localStorage 或 IndexedDB。如何强制 Session Cookie 在标签页关闭时清除?无法可靠实现。可用的替代方案:用 sessionStorage(标签页级)或服务端维护有效期并主动使 Session 失效。写段代码// 服务端设置两种 Cookieres.setHeader('Set-Cookie', [ 'sid=abc123; HttpOnly; Secure; SameSite=Lax', // Session Cookie 'theme=dark; Max-Age=31536000; HttpOnly; SameSite=Lax' // Persistent Cookie]);
服务端阅读 05月29日 01:21

OpenCV.js 在实际项目中有哪些应用场景?

OpenCV.js 在实际开发中有很多应用场景,以下是几个典型的实战案例:1. 网页端图像编辑器功能实现class ImageEditor { constructor(canvasId) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.originalImage = null; this.currentImage = null; } loadImage(file) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { this.canvas.width = img.width; this.canvas.height = img.height; this.ctx.drawImage(img, 0, 0); this.originalImage = cv.imread(this.canvas); this.currentImage = this.originalImage.clone(); resolve(); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); } applyFilter(filterType) { let temp = new cv.Mat(); try { switch(filterType) { case 'grayscale': cv.cvtColor(this.currentImage, temp, cv.COLOR_RGBA2GRAY); cv.cvtColor(temp, this.currentImage, cv.COLOR_GRAY2RGBA); break; case 'blur': cv.GaussianBlur(this.currentImage, temp, new cv.Size(15, 15), 0); temp.copyTo(this.currentImage); break; case 'sharpen': let kernel = cv.matFromArray(3, 3, cv.CV_32FC1, [ 0, -1, 0, -1, 5, -1, 0, -1, 0 ]); cv.filter2D(this.currentImage, temp, -1, kernel); temp.copyTo(this.currentImage); kernel.delete(); break; case 'edge': cv.cvtColor(this.currentImage, temp, cv.COLOR_RGBA2GRAY); cv.Canny(temp, temp, 50, 100); cv.cvtColor(temp, this.currentImage, cv.COLOR_GRAY2RGBA); break; } cv.imshow(this.canvas.id, this.currentImage); } finally { temp.delete(); } } adjustBrightness(value) { let temp = new cv.Mat(); try { this.currentImage.convertTo(temp, -1, 1, value); temp.copyTo(this.currentImage); cv.imshow(this.canvas.id, this.currentImage); } finally { temp.delete(); } } reset() { this.currentImage = this.originalImage.clone(); cv.imshow(this.canvas.id, this.currentImage); } download() { const link = document.createElement('a'); link.download = 'edited-image.png'; link.href = this.canvas.toDataURL(); link.click(); }}2. 实时人脸检测和识别class FaceDetector { constructor(videoId, canvasId) { this.video = document.getElementById(videoId); this.canvas = document.getElementById(canvasId); this.faceCascade = new cv.CascadeClassifier(); this.isRunning = false; } async init() { // 加载人脸检测模型 await this.loadModel('haarcascade_frontalface_default.xml'); // 启动摄像头 const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); this.video.srcObject = stream; await this.video.play(); this.canvas.width = this.video.videoWidth; this.canvas.height = this.video.videoHeight; } async loadModel(url) { return new Promise((resolve, reject) => { this.faceCascade.load(url); resolve(); }); } start() { this.isRunning = true; this.detect(); } stop() { this.isRunning = false; } detect() { if (!this.isRunning) return; let src = new cv.Mat(); let gray = new cv.Mat(); let faces = new cv.RectVector(); try { // 读取视频帧 src = cv.imread(this.video); // 转灰度 cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); // 检测人脸 this.faceCascade.detectMultiScale(gray, faces, 1.1, 3, 0); // 绘制人脸框 for (let i = 0; i < faces.size(); ++i) { let face = faces.get(i); let point1 = new cv.Point(face.x, face.y); let point2 = new cv.Point(face.x + face.width, face.y + face.height); cv.rectangle(src, point1, point2, [255, 0, 0, 255], 2); // 添加标签 cv.putText(src, `Face ${i + 1}`, new cv.Point(face.x, face.y - 10), cv.FONT_HERSHEY_SIMPLEX, 0.5, [0, 255, 0, 255], 1); } cv.imshow(this.canvas.id, src); requestAnimationFrame(() => this.detect()); } finally { src.delete(); gray.delete(); faces.delete(); } }}3. OCR 文字识别class OCRProcessor { constructor() { this.tesseract = null; } async init() { // 初始化 Tesseract.js this.tesseract = Tesseract.createWorker({ logger: m => console.log(m) }); await this.tesseract.loadLanguage('eng'); await this.tesseract.initialize('eng'); } async preprocessImage(imageElement) { let src = cv.imread(imageElement); let gray = new cv.Mat(); let binary = new cv.Mat(); let denoised = new cv.Mat(); try { // 转灰度 cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); // 降噪 cv.medianBlur(gray, denoised, 3); // 二值化 cv.threshold(denoised, binary, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU); // 显示预处理结果 const canvas = document.getElementById('preprocessedCanvas'); cv.imshow(canvas.id, binary); return binary; } finally { src.delete(); gray.delete(); denoised.delete(); } } async recognizeText(imageElement) { // 预处理图像 const processed = await this.preprocessImage(imageElement); // 转换为 ImageData const canvas = document.getElementById('preprocessedCanvas'); const imageData = canvas.toDataURL('image/png'); // OCR 识别 const { data: { text } } = await this.tesseract.recognize(imageData); processed.delete(); return text; } async cleanup() { await this.tesseract.terminate(); }}4. 实时二维码扫描class QRScanner { constructor(videoId, canvasId) { this.video = document.getElementById(videoId); this.canvas = document.getElementById(canvasId); this.isScanning = false; } async start() { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); this.video.srcObject = stream; await this.video.play(); this.canvas.width = this.video.videoWidth; this.canvas.height = this.video.videoHeight; this.isScanning = true; this.scan(); } scan() { if (!this.isScanning) return; let src = new cv.Mat(); let gray = new cv.Mat(); let edges = new cv.Mat(); try { src = cv.imread(this.video); cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); cv.Canny(gray, edges, 50, 150); // 查找轮廓 let contours = new cv.MatVector(); let hierarchy = new cv.Mat(); cv.findContours(edges, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE); // 检测二维码 for (let i = 0; i < contours.size(); i++) { let contour = contours.get(i); let area = cv.contourArea(contour); if (area > 1000) { // 绘制轮廓 cv.drawContours(src, contours, i, [0, 255, 0, 255], 2); // 提取二维码区域 let rect = cv.boundingRect(contour); let qrCode = src.roi(rect); // 使用 jsQR 库解码 const imageData = new ImageData( new Uint8ClampedArray(qrCode.data), qrCode.cols, qrCode.rows ); const code = jsQR(imageData.data, imageData.width, imageData.height); if (code) { console.log('QR Code:', code.data); // 触发回调 this.onQRCodeDetected(code.data); } qrCode.delete(); } } cv.imshow(this.canvas.id, src); requestAnimationFrame(() => this.scan()); } finally { src.delete(); gray.delete(); edges.delete(); } } stop() { this.isScanning = false; } onQRCodeDetected(data) { // 重写此方法处理二维码数据 console.log('QR Code detected:', data); }}5. 实时视频滤镜class VideoFilter { constructor(videoId, canvasId) { this.video = document.getElementById(videoId); this.canvas = document.getElementById(canvasId); this.currentFilter = 'none'; } async start() { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); this.video.srcObject = stream; await this.video.play(); this.canvas.width = this.video.videoWidth; this.canvas.height = this.video.videoHeight; this.process(); } setFilter(filterName) { this.currentFilter = filterName; } process() { let src = new cv.Mat(); let dst = new cv.Mat(); try { src = cv.imread(this.video); switch(this.currentFilter) { case 'grayscale': cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.cvtColor(dst, dst, cv.COLOR_GRAY2RGBA); break; case 'sepia': this.applySepia(src, dst); break; case 'cartoon': this.applyCartoon(src, dst); break; case 'emboss': this.applyEmboss(src, dst); break; default: src.copyTo(dst); } cv.imshow(this.canvas.id, dst); requestAnimationFrame(() => this.process()); } finally { src.delete(); dst.delete(); } } applySepia(src, dst) { let kernel = cv.matFromArray(3, 3, cv.CV_32FC1, [ 0.272, 0.534, 0.131, 0.349, 0.686, 0.168, 0.393, 0.769, 0.189 ]); cv.transform(src, dst, kernel); kernel.delete(); } applyCartoon(src, dst) { let gray = new cv.Mat(); let edges = new cv.Mat(); let color = new cv.Mat(); cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); cv.medianBlur(gray, gray, 7); cv.Canny(gray, edges, 50, 150); cv.cvtColor(edges, edges, cv.COLOR_GRAY2RGBA); cv.bilateralFilter(src, color, 9, 250, 250); cv.bitwise_and(color, edges, dst); gray.delete(); edges.delete(); color.delete(); } applyEmboss(src, dst) { let kernel = cv.matFromArray(3, 3, cv.CV_32FC1, [ -2, -1, 0, -1, 1, 1, 0, 1, 2 ]); cv.filter2D(src, dst, -1, kernel); kernel.delete(); }}这些实战案例展示了 OpenCV.js 在不同场景下的应用,开发者可以根据具体需求选择合适的实现方案。
服务端阅读 05月29日 01:21

GORM 的 AutoMigrate 功能如何使用?

AutoMigrate 根据 Go 结构体的 tag 自动创建表、添加缺失的列和索引,是纯增量操作——不会删除列、不会修改列类型、不会重命名列。这意味着一旦某列被创建,即使结构体中删除了该字段,数据库中仍会保留。对于列类型变更(如 string 改为 text),AutoMigrate 静默跳过。生产环境不应依赖 AutoMigrate,应使用 golang-migrate 等版本化迁移工具,AutoMigrate 仅适合开发和快速原型阶段。追问AutoMigrate 检测到列类型不匹配时会怎样?会报错吗?如何手动删除一列?db.Migrator().DropColumn() 在生产环境有什么风险?AutoMigrate 对已存在的表修改索引的行为是什么?多个服务同时启动时 AutoMigrate 会冲突吗?如何保证迁移安全?db.Migrator().HasTable() 和 db.Migrator().HasColumn() 在实际迁移逻辑中怎么用?写段代码type User struct { gorm.Model Name string `gorm:"size:100;not null"` Email string `gorm:"uniqueIndex"`}// 只做增量,不会删除旧列db.AutoMigrate(&User{})// 手动操作需用 Migratordb.Migrator().DropColumn(&User{}, "OldField")
服务端阅读 05月29日 01:21

OpenCV.js 开发中常见问题及解决方案有哪些?

OpenCV.js 开发中最常见的问题有三个:一是 WASM 加载失败,cv 对象 undefined,原因是 opencv.js 文件约 8MB 加载慢或 CDN 不稳定,解决方案是配置多个 CDN 备用并监听 cv.onRuntimeInitialized 回调确认就绪;二是内存泄漏,浏览器长时间运行变卡,根因是 cv.Mat 通过 WASM 堆分配内存不受 JS GC 管理,必须在 try-finally 中调用 mat.delete(),视频循环中更要复用 Mat 对象而非每帧新建;三是跨域图像无法处理,canvas 被 tainted 后 cv.imread() 报错,需在 img 标签设置 crossOrigin='Anonymous' 或通过服务端代理。此外,模型文件(如 Haar Cascade XML)在浏览器端无法直接读本地路径,需 fetch 下载为 ArrayBuffer 再加载。追问cv.onRuntimeInitialized 和 Module.onRuntimeInitialized 有什么区别?前者是 OpenCV.js 的回调,后者是 Emscripten 底层回调。推荐用 cv.onRuntimeInitialized,它在 OpenCV API 完全可用时触发,而 Module 版本可能在 WASM 编译完成但 JS 绑定未就绪时就触发。delete 一个已 delete 的 Mat 会怎样?会抛出异常。安全做法是 delete 后将变量设为 null,或封装一个 safeDelete(mat) 函数先判断再调用。视频循环中更好的做法是复用 Mat 而非反复创建删除。如何排查 OpenCV.js 的内存泄漏?用 Chrome DevTools 的 Memory 面板做堆快照对比,关注 WASM 堆增长。也可在代码中用 cv.getBuildInformation() 确认版本,用 performance.memory(Chrome)监控 JS 堆外内存变化。opencv.js 文件太大怎么优化加载?可自行编译精简版,通过 opencvcontrib 的 buildjs.py 脚本用 -DBUILD_LIST 指定只编译需要的模块(如 core,imgproc),体积可从 8MB 降到 2-3MB。也可开启 WASM 流式编译(Streaming instantiation)加速。canvas tainted 的具体报错是什么?怎么彻底避免?报错为 'The canvas has been tainted by cross-origin data'。根本方案:所有外部图片设 crossOrigin 属性、服务端返回正确 CORS 头、避免在 canvas 中绘制未授权跨域资源。一旦 tainted 无法逆转,只能重建 canvas。写段代码function onOpenCvReady() { let src = cv.imread('canvasInput'); let dst = new cv.Mat(); try { cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.imshow('canvasOutput', dst); } finally { src.delete(); dst.delete(); }}cv.onRuntimeInitialized = onOpenCvReady;
服务端阅读 05月29日 01:20

GORM 中的软删除(Soft Delete)是如何工作的?

GORM 软删除通过 gorm.DeletedAt 字段实现:模型包含该字段后,db.Delete() 不会执行 DELETE,而是 UPDATE SET deleted_at=NOW();查询时 GORM 自动追加 WHERE deleted_at IS NULL 过滤已删除记录。要用 Unscoped() 查询包含已删除的记录,用 Unscoped().Delete() 执行真正的硬删除。软删除的最大坑是唯一约束——已软删除的记录仍占据唯一索引位置,导致无法插入同值新记录,需用复合唯一索引 (email, deleted_at) 解决。追问db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users) 和 db.Unscoped().Find(&users) 结果有何区别?软删除记录如何恢复?恢复时唯一约束冲突怎么处理?关联查询(Preload)中软删除的记录会被过滤吗?如何加载已删除的关联?为什么不推荐在生产环境依赖软删除做数据审计?应该用什么替代方案?自定义软删除字段(如 is_deleted bool)时 GORM 还会自动过滤吗?写段代码type User struct { gorm.Model Email string `gorm:"uniqueIndex:idx_email_deleted"`}// 复合唯一索引解决软删除冲突// db.Unscoped().Delete(&user) // 硬删除// db.Unscoped().Where("id = ?", id).Update("deleted_at", nil) // 恢复
服务端阅读 05月29日 01:20

OpenCV.js 如何进行机器学习任务?

OpenCV.js 的机器学习能力有限,主要提供传统算法(KNN、SVM、决策树、RTrees、Boost、MLP),不支持训练深度学习模型。实际开发中更常用的方式是通过 DNN 模块加载预训练模型做推理,支持 Caffe、TensorFlow、ONNX 等格式的模型。训练流程通过 cv.ml.KNearest / cv.ml.SVM.create() 等创建模型,调用 train() 方法用 Mat 格式的特征和标签训练,再用 predict() 推理。但 OpenCV.js 不适合做复杂 ML 任务,浏览器端做 ML 推理更推荐 TensorFlow.js 或 ONNX Runtime Web,OpenCV.js 的 ML 模块更适合小规模分类等轻量场景。追问OpenCV.js 能训练深度学习模型吗?不能。OpenCV.js 的 DNN 模块只支持前向推理,不包含反向传播。要在浏览器训练深度学习模型需用 TensorFlow.js 等框架。OpenCV.js 的 MLP 也只是传统浅层网络。DNN 模块支持哪些模型格式?支持 Caffe(.caffemodel + .prototxt)、TensorFlow(.pb)、ONNX(.onnx)、Darknet(.weights + .cfg)。加载用 cv.readNetFromONNX() 等方法,模型文件需通过 fetch 下载到浏览器。KNN 和 SVM 在 OpenCV.js 中哪个更实用?小数据集简单分类用 KNN 更方便(无需调参),SVM 在特征空间复杂时效果更好但需调核函数和超参。两者都不适合大规模数据,浏览器内存有限。如何在浏览器端提取图像特征用于 ML?常用方法:cv.calcHist() 计算直方图特征、HOG 描述子(cv.HOGDescriptor)、或用 ORB 提取局部特征再聚合。更高级的特征提取建议用 DNN 模块加载预训练 CNN 做特征提取。OpenCV.js ML 模块最大的局限是什么?三点:一是不支持 GPU 加速,纯 CPU/WASM 运行速度慢;二是训练数据必须全部加载到内存,浏览器内存限制制约了数据规模;三是模型无法持久化保存,每次刷新页面需重新训练。写段代码let svm = cv.ml.SVM.create();svm.setType(cv.ml.SVM_C_SVC);svm.setKernel(cv.ml.SVM_LINEAR);svm.train(trainData, cv.ml.ROW_SAMPLE, labels);let result = svm.predict(testSample);console.log('class:', result);svm.clear();