当前位置: 首页 > article >正文

React 乐观更新(Optimistic UI):在网络波动环境下维持 React 状态与服务端最终一致性

欢迎来到“乐观 UI”的游乐场如何在网络波动中假装一切都很完美大家好我是你们的老朋友一个在 React 深渊里摸爬滚打多年的资深工程师。今天我们不聊那些虚头巴脑的架构图也不谈什么微前端、Serverless咱们来聊点“人性”的东西。具体来说咱们聊聊乐观更新。你有没有过这种经历你在电商网站上手指悬停在“加入购物车”按钮上心里默念“买买买”然后手指一按——好了购物车图标瞬间从 0 变成了 1。没有转圈圈没有“加载中”甚至没有一丝丝延迟。你心里那个爽啊觉得这网站简直神了。然后你淡定地继续浏览甚至觉得自己刚才那一手操作简直行云流水堪比魔术师。但是你有没有想过服务器那边发生了什么服务器可能还在打哈欠甚至可能因为网络波动正在给你发“请稍等”的信号。但你的浏览器早就替你决定了结果。这就是乐观更新的核心哲学先发制人甚至有点“欺骗”性质。今天我们就来扒一扒这个让用户体验起飞却让后端调试头秃的技术。我们不讲枯燥的定义我们直接上代码上实战上段子。第一章当“Loading”成为数字时代的噩梦在谈乐观更新之前我们必须先批判一下“悲观更新”。什么是悲观更新就是那种你点一下按钮它就给你转圈圈告诉你“正在努力连接宇宙中心请稍候 3 秒”。3 秒过去了还是转圈圈最后你刷新页面发现刚才的操作白费了。这种体验就像是你约了女神去吃饭发消息过去她回了个“嗯”然后你就站在原地等着时间一分一秒过去你心里的焦虑像杂草一样疯长。女神不回消息你就得在那傻等。这叫“等死”。而在 Web 开发中这种“等死”体验的罪魁祸首就是那个该死的 Loading 状态。用户点个赞等 2 秒用户发条评论等 3 秒用户提交个表单等 5 秒。如果你的应用里充满了这种“转圈圈”用户就会觉得你的应用很卡甚至觉得你的代码写得像便秘一样。于是乐观更新应运而生。它的口号是如果用户相信你会成功那你就先告诉他成功了。这不仅仅是关于速度这是关于掌控感。第二章乐观更新的本质——先吃蛋糕再付钱想象一下你去面包店买蛋糕。悲观模式传统模式你付钱然后面包师拿刀切蛋糕称重装袋再递给你。整个过程你都要盯着面包师心里祈祷他手别抖称别坏。如果你说“我要这块”他得先问老板“这能卖吗”然后老板问系统“有库存吗”系统问数据库“还有吗”。这一通下来你都快饿死了蛋糕凉了。乐观模式Optimistic UI你指着蛋糕说“我要这个”。面包师二话不说直接切下一块塞你手里收了钱然后转身对你说“先拿着我去确认一下后厨还有没有。如果没了你再把蛋糕吐出来。”这就是乐观更新的精髓。它假设请求会成功所以 UI 立即响应。如果真的成功了皆大欢喜如果失败了再进行回滚。在 React 中这意味着我们要在数据还没到服务器之前就更新本地状态。第三章原生 React 的“手动挡”实现虽然现在有很多库React Query, SWR帮我们搞定了一切但作为资深工程师我们必须知道底层是怎么运作的。这就像你知道怎么用筷子也知道怎么用叉子但偶尔你也得知道怎么直接上手抓饭吃。我们来看一个最简单的例子点赞。假设我们有一个帖子列表每个帖子都有一个likeCount。3.1 悲观版本的代码垃圾代码function Post({ post }) { const [likes, setLikes] useState(post.likeCount); const [loading, setLoading] useState(false); const handleLike async () { setLoading(true); // 开始转圈圈用户体验下降 try { // 发送请求 await api.likePost(post.id); // 成功了才更新 UI setLikes(prev prev 1); } catch (error) { // 失败了给个提示 console.error(点赞失败, error); } finally { setLoading(false); // 停止转圈圈 } }; return ( div classNamepost h3{post.title}/h3 p点赞数: {loading ? ... : likes}/p button onClick{handleLike} disabled{loading} {loading ? 处理中... : 点赞} /button /div ); }看到那个loading状态了吗看到那个disabled了吗看到那个...了吗这就是用户体验的杀手。用户感觉自己的操作被卡住了。3.2 乐观版本的代码真香代码现在我们把它改成乐观更新。function Post({ post }) { const [likes, setLikes] useState(post.likeCount); const handleLike async () { // 1. 立即更新 UI乐观部分 // 我们假装请求已经成功了 setLikes(prev prev 1); try { // 2. 发送请求 await api.likePost(post.id); // 如果请求成功什么都不用做UI 已经是新的了 } catch (error) { // 3. 如果请求失败回滚 // 把 UI 恢复到原来的样子 setLikes(prev prev - 1); // 给用户一点反馈比如 Toast 提示 showToast(哎呀点赞失败了网络好像抽风了); } }; return ( div classNamepost h3{post.title}/h3 p点赞数: {likes}/p button onClick{handleLike}点赞/button /div ); }看这就是区别。用户点击的瞬间数字就变了。没有转圈圈。那种“噌”的一下爽不爽第四章React Query —— 资深工程师的“作弊码”写原生代码虽然能理解原理但每次都要手动处理try/catch、手动处理loading、手动处理回滚太累了。而且容易出 bug。这时候TanStack Query (React Query)就闪亮登场了。它简直就是为乐观更新量身定制的。React Query 的useMutation钩子自带了处理乐观更新的机制。4.1 基础用法import { useMutation, useQueryClient } from tanstack/react-query; function LikeButton({ postId }) { const queryClient useQueryClient(); // 定义 mutation const mutation useMutation({ mutationFn: (postId) api.likePost(postId), // onMutate在请求发送前执行 // 这里就是我们的“乐观”操作发生的地方 onMutate: async (newPostId) { // 1. 取消正在进行的查询防止数据覆盖 await queryClient.cancelQueries({ queryKey: [posts, newPostId] }); // 2. 保存旧数据为了回滚 const previousPost queryClient.getQueryData([posts, newPostId]); // 3. 乐观更新本地缓存 queryClient.setQueryData([posts, newPostId], (old) { return { ...old, likes: old.likes 1 }; }); // 返回上下文供 onError 使用 return { previousPost }; }, // onError如果出错了恢复旧数据 onError: (err, newPostId, context) { queryClient.setQueryData([posts, newPostId], context.previousPost); }, // onSettled无论成功失败都重新获取数据保持最终一致性 onSettled: (data, error, newPostId) { queryClient.invalidateQueries({ queryKey: [posts, newPostId] }); } }); return ( button onClick{() mutation.mutate(postId)} disabled{mutation.isPending} 点赞 ({mutation.data?.likes || 0}) /button ); }看到没React Query 帮你处理了所有的脏活累活。onMutate是乐观更新的核心。它利用了 React Query 的缓存机制。React Query 把你的组件状态和服务器状态分离开来了。组件状态乐观更新修改的是这个。服务器状态等请求回来或者重新获取。4.2 为什么要用invalidateQueries注意看onSettled里的invalidateQueries。为什么我们不直接在成功后更新缓存而要重新获取这就是最终一致性的问题。乐观更新是一种“快照”。它假设成功了。但如果服务器因为并发问题比如两个人同时点赞拒绝了这个操作怎么办或者服务器因为 bug 返回了错误怎么办为了确保我们的 UI 和服务器数据绝对一致最终一致性我们通常在乐观更新之后触发一次数据的重新获取或者使用乐观更新后的数据作为缓存但这在复杂场景下很难保证。invalidateQueries会告诉 React Query“去服务器重新拉取一下最新数据覆盖掉我刚才那个可能错误的乐观更新。”这就像你去餐厅点菜。乐观更新是你先吃了。invalidateQueries就是服务员去厨房确认一下这道菜到底做好了没。如果厨房说没做好服务员就会把菜撤走重新给你上菜。第五章进阶场景——列表的“回滚”艺术乐观更新不仅仅是修改一个数字它还涉及到列表的增删改查。这里面有个大坑索引问题。场景删除列表中的某一项假设你有一个购物车你点击删除按钮。乐观更新逻辑立即从本地列表中移除该项。显示“删除成功”的 Toast。发送请求到服务器。如果失败把该项加回去。代码大概是这样的const mutation useMutation({ mutationFn: deleteItem, onMutate: async (itemId) { // 1. 取消查询 await queryClient.cancelQueries({ queryKey: [cart] }); // 2. 保存当前列表快照 const previousData queryClient.getQueryData([cart]); // 3. 乐观更新过滤掉要删除的项 queryClient.setQueryData([cart], (oldData) { return oldData.filter(item item.id ! itemId); }); // 返回上下文 return { previousData }; }, onError: (err, itemId, context) { // 4. 失败回滚恢复旧数据 queryClient.setQueryData([cart], context.previousData); showToast(删除失败商品还在购物车里); }, onSettled: () { // 5. 重新获取 queryClient.invalidateQueries({ queryKey: [cart] }); } });这里的关键点在于onError。如果你在乐观更新后没有保存previousData一旦出错你根本不知道原来的列表长什么样也就没法回滚了。这就是原子性操作在 React 中的体现。要么全部成功要么全部回滚。虽然网络请求失败了但我们在 UI 层面保证了这种原子性。第六章并发更新与竞态条件现在让我们来点更刺激的。React 18 引入了并发模式。想象一下用户手速极快连续点击了两次“点赞”按钮。悲观模式用户点一次 - Loading - 请求 A 发送 - 请求 B 发送 - 请求 A 成功UI 更新 - 请求 B 成功UI 再次更新可能重复或者覆盖 - 用户一脸懵逼。乐观模式 并发用户点一次 - 请求 A 发送 - UI 更新1。用户点一次 - 请求 B 发送 - UI 更新1。两个请求都成功了。UI 变成了 2。这是正确的。但是如果请求 B 失败了怎么办React Query 会自动处理这种竞态条件。它会丢弃过期的请求结果。const mutation useMutation({ mutationFn: (id) api.likePost(id), // React Query 默认会处理这种竞态问题 });但是如果我们手动实现乐观更新就需要特别注意。比如我们在onMutate里保存了previousPost但是用户在请求回来的过程中又点了两次这时候onError拿到的上下文可能已经不是最新的了。解决方案在onMutate中不仅要保存数据还要取消正在进行的查询。React Query 的cancelQueries已经帮我们做了这件事。如果我们手动管理状态就得手动用AbortController取消请求。const controller new AbortController(); const mutation useMutation({ mutationFn: () fetch(/api/like, { signal: controller.signal }), onMutate: async () { controller.abort(); // 取消其他正在进行的类似操作 // ... 乐观更新逻辑 } });这有点像是在高速公路上开车你变道了发送了请求必须把原来的车道锁住取消之前的请求否则后面的车新的请求会把你撞飞。第七章复杂表单的乐观更新乐观更新在表单里怎么用比如注册、修改资料。直接把整个表单状态都乐观更新那如果服务器要求必填项没填怎么办如果密码太短怎么办如果图片上传失败怎么办这时候我们不能盲目乐观。策略部分乐观更新只更新 UI 上已经拿到服务器反馈的字段比如头像上传成功后显示新头像。禁用按钮在提交表单期间禁用按钮防止重复提交。错误处理表单提交失败后把错误信息显示在对应字段旁边。function EditProfile({ user }) { const [form, setForm] useState(user); const [isUpdating, setIsUpdating] useState(false); const [error, setError] useState(null); const handleSubmit async (e) { e.preventDefault(); setIsUpdating(true); setError(null); try { // 乐观更新假设成功 // 实际上我们只是更新了本地状态 // React 本身就是声明式的所以 UI 会自动渲染新的表单值 await api.updateUser(form); // 成功后刷新整个用户信息 window.location.reload(); // 简单粗暴或者用 React Query 刷新 } catch (err) { // 失败了回滚 UI setForm(user); setError(更新失败请检查输入); } finally { setIsUpdating(false); } }; return ( form onSubmit{handleSubmit} input value{form.name} onChange{e setForm({...form, name: e.target.value})} disabled{isUpdating} / button disabled{isUpdating}保存/button {error span style{{color: red}}{error}/span} /form ); }在这个例子中我们乐观地更新了form状态。如果服务器返回 400 错误比如用户名已存在我们立即把form恢复成原来的user。用户会看到输入框里的内容瞬间变回了修改前的样子这给了用户很强的“失败反馈”。第八章保持“最终一致性”的哲学回到我们的主题在网络波动环境下维持 React 状态与服务端最终一致性。乐观更新是一种暂时性的状态。它把服务器的“真理”推迟到了最后。如果用户刷新页面或者打开浏览器的开发者工具或者被同事远程控制了电脑乐观更新的状态就会消失因为那只是内存里的假象。如何保证最终一致性乐观更新是 UI 的快照它是为了给用户看的不是为了给数据库看的。请求回来后的清洗无论是成功还是失败都要重新获取数据invalidateQueries。乐观更新不能替代数据验证前端乐观更新后服务器必须再次验证数据的合法性。如果服务器验证失败必须返回错误前端负责回滚。这就像你出门前收拾行李乐观更新觉得自己带齐了。但是当你到了机场安检人员服务器说“嘿你忘带身份证了。” 这时候你只能老老实实回家拿身份证回滚而不是站在机场大喊“我明明感觉我带了啊”第九章实战演练——一个完整的购物车案例为了让大家彻底理解我们来写一个稍微复杂点的购物车组件。需求用户点击“加入购物车”。购物车数量立即 1。显示一个绿色的“已添加”提示。如果请求失败数量 -1显示红色提示。如果请求成功提示消失。import { useMutation, useQueryClient } from tanstack/react-query; import { useState } from react; function ProductCard({ product }) { const queryClient useQueryClient(); const [toast, setToast] useState(null); // { message: string, type: success | error } const mutation useMutation({ mutationFn: (productId) api.addToCart(productId), onMutate: async (productId) { // 1. 取消查询防止冲突 await queryClient.cancelQueries({ queryKey: [cart] }); // 2. 获取旧数据 const previousCart queryClient.getQueryData([cart]); // 3. 乐观更新 queryClient.setQueryData([cart], (old) { const existingItem old.find(item item.id productId); if (existingItem) { return old.map(item item.id productId ? { ...item, quantity: item.quantity 1 } : item ); } else { return [...old, { ...product, quantity: 1 }]; } }); // 4. 显示“添加中”的 Toast乐观提示 setToast({ message: 已将 ${product.name} 加入购物车, type: success }); // 返回上下文 return { previousCart }; }, onError: (err, productId, context) { // 5. 失败回滚 queryClient.setQueryData([cart], context.previousCart); setToast({ message: 添加失败请重试, type: error }); // 3秒后清除错误提示 setTimeout(() setToast(null), 3000); }, onSuccess: () { // 6. 成功后延迟一点清除提示让用户看到成功 setTimeout(() setToast(null), 1000); }, onSettled: () { // 7. 无论成功失败重新获取数据虽然乐观更新已经改了但这是保险起见 queryClient.invalidateQueries({ queryKey: [cart] }); } }); const handleAddToCart () { mutation.mutate(product.id); }; return ( div classNameproduct-card h3{product.name}/h3 p${product.price}/p button onClick{handleAddToCart} disabled{mutation.isPending} {mutation.isPending ? 添加中... : 加入购物车} /button {toast ( div className{toast toast-${toast.type}} {toast.message} /div )} /div ); }在这个例子中我们使用了 React Query 的上下文机制。onMutate返回的context对象在onError中被重新赋值实现了完美的回滚。第十章不要在所有地方都使用乐观更新最后作为一个资深专家我得泼点冷水。乐观更新虽然好但不是万能药。什么时候不要用乐观更新需要服务器 ID 的情况比如创建一条新评论服务器返回了新的 ID。如果你乐观更新了列表但你还没拿到新 ID列表的渲染就会出错比如用索引渲染或者需要 ID 来定位。这时候你只能先显示一个占位符等 ID 到了再替换。副作用巨大的操作比如删除账户、修改密码。这种操作一旦出错后果很严重。用户可能会以为自己改成功了结果过两天发现密码没变或者账号被盗。这种情况下老老实实等待服务器响应可能更安全。数据结构极其复杂如果你的状态更新涉及深层的嵌套对象手动处理回滚的previousState会非常痛苦。这种时候使用一个状态管理库如 Redux配合中间件来处理乐观更新会更合适。结语做一名“乐观”的工程师好了同学们今天的讲座就到这里。我们讲了什么是乐观更新讲了如何用原生 React 实现讲了如何用 React Query 进阶讲了列表的回滚讲了并发竞态还写了一个完整的购物车案例。乐观更新不仅仅是一个技术技巧它是一种用户同理心。它告诉用户“我相信你的操作是有价值的我相信我们的系统是可靠的。”在网络波动的时代这种信任感是无价的。虽然我们作为工程师要时刻准备着处理错误和回滚但只要我们逻辑严密、代码稳健我们就可以放心大胆地让用户先享受成功的喜悦然后再去处理现实。记住不要做那个只会转圈圈的悲观主义者。要做一名快乐的、乐观的、让用户爽到飞起的 React 工程师现在去把你的 Loading 状态全部干掉吧

相关文章:

React 乐观更新(Optimistic UI):在网络波动环境下维持 React 状态与服务端最终一致性

欢迎来到“乐观 UI”的游乐场:如何在网络波动中假装一切都很完美大家好,我是你们的老朋友,一个在 React 深渊里摸爬滚打多年的资深工程师。今天我们不聊那些虚头巴脑的架构图,也不谈什么微前端、Serverless,咱们来聊点…...

prek内置钩子详解:20个零配置快速检查工具

prek内置钩子详解:20个零配置快速检查工具 【免费下载链接】prek ⚡ A Git hook manager written in Rust, designed as a drop-in alternative to pre-commit. 项目地址: https://gitcode.com/GitHub_Trending/pr/prek prek是一个用Rust编写的Git钩子管理器…...

SCons完整指南:从简单程序到复杂项目的构建自动化

SCons完整指南:从简单程序到复杂项目的构建自动化 【免费下载链接】scons SCons - a software construction tool 项目地址: https://gitcode.com/gh_mirrors/sc/scons SCons是一款功能强大的软件构建工具,它能够帮助开发者自动化从简单程序到复杂…...

ITK-SNAP医学图像分割:从新手到专家的实战指南

ITK-SNAP医学图像分割:从新手到专家的实战指南 【免费下载链接】itksnap ITK-SNAP medical image segmentation tool 项目地址: https://gitcode.com/gh_mirrors/it/itksnap 在医学影像分析领域,精确的分割技术是诊断、治疗规划和科学研究的基础。…...

EPLAN高手都在用的‘拖拽大法’:一个手势搞定符号库、项目打开和文件导入

EPLAN手势革命:用拖拽大法重构电气设计工作流 在智能手机普及的今天,我们早已习惯了用两根手指缩放照片、长按图标整理应用——这些手势操作让数字交互变得无比自然。但回到专业工程软件领域,多数人依然被困在层层菜单和右键选项中。EPLAN作为…...

用Simulink手把手搭建7自由度悬架模型:从方程到仿真的保姆级避坑指南

用Simulink手把手搭建7自由度悬架模型:从方程到仿真的保姆级避坑指南 在车辆动力学研究中,7自由度悬架模型是分析整车振动特性的黄金标准。不同于简单的四分之一车模型,它能同时捕捉车身垂向跳动、俯仰、侧倾以及四个车轮的独立运动&#xff…...

数字IC版图新手避坑指南:以加法器为例,解决DRC/LVS错误和仿真毛刺

数字IC版图设计实战:从加法器案例拆解DRC/LVS错误与仿真毛刺的根治方案 第一次在Cadence Virtuoso里完成加法器版图时,看着Calibre报出的237个DRC错误和LVS窗口里密密麻麻的mismatch提示,我对着屏幕发呆了半小时——那些教科书上轻描淡写的&q…...

别再只写#ifdef __cplusplus了!聊聊这个宏在C++11/17/20下的实战用法与坑

深入实战:__cplusplus宏在跨版本C项目中的高阶用法与避坑指南 如果你在跨版本C项目中遇到过这样的场景:明明代码在本地编译通过,却在CI服务器上莫名其妙报错;或者精心编写的头文件在C11和C17下表现不一致——那么这篇文章正是为你…...

别再混淆了!一文搞懂OpenHarmony NAPI中的同步、回调与Promise接口(附代码对比)

OpenHarmony NAPI接口设计实战:同步、回调与Promise的黄金选择法则 当你在OpenHarmony生态中封装一个底层功能时,总会面临这个灵魂拷问:该用同步接口、回调函数还是Promise?这个看似简单的选择背后,藏着性能、可维护性…...

避坑指南:沁恒CH582/CH583 Sleep模式下RTC唤醒的中断与主频那些事儿

沁恒CH582/CH583低功耗开发实战:Sleep模式与RTC唤醒的深度解析 当你在凌晨三点盯着调试器,发现设备明明配置了RTC唤醒却毫无反应时,那种挫败感我太熟悉了。去年在智能门锁项目中使用CH583时,我花了整整三天才搞明白为什么80MHz主频…...

Workrave终极指南:告别重复性劳损的完整解决方案

Workrave终极指南:告别重复性劳损的完整解决方案 【免费下载链接】workrave Workrave is a program that assists in the recovery and prevention of Repetitive Strain Injury (RSI). The program frequently alerts you to take micro-pauses, rest breaks and r…...

5步搞定明日方舟全自动化:MAA助手终极指南

5步搞定明日方舟全自动化:MAA助手终极指南 【免费下载链接】MaaAssistantArknights 《明日方舟》小助手,全日常一键长草!| A one-click tool for the daily tasks of Arknights, supporting all clients. 项目地址: https://gitcode.com/Gi…...

别再只用SysTick了!用GD32F103的TIMER1实现更灵活的1ms延时(附完整代码)

突破SysTick限制:GD32F103定时器高阶延时方案实战 在嵌入式开发中,精确的延时控制如同系统的心跳,而SysTick作为ARM内核标配的简易定时器,常被开发者当作默认选择。但当我们面对多任务调度、可变频率延时或复杂时序控制时&#xf…...

易语言中根据凸度求圆心角

在易语言中,根据凸度(Bulge)计算圆心角,其核心数学原理与通用公式 θ 4 * arctan(|bulge|) 完全一致 。实现的关键在于调用易语言的数学函数库,并遵循其特定的语法规则进行编码 。 一、核心计算函数实现 以下是一个…...

魔兽世界宏编程革命:如何用GSE告别技能卡顿的烦恼 [特殊字符]

魔兽世界宏编程革命:如何用GSE告别技能卡顿的烦恼 🎮 【免费下载链接】GSE-Advanced-Macro-Compiler GSE is an alternative advanced macro editor and engine for World of Warcraft. 项目地址: https://gitcode.com/gh_mirrors/gs/GSE-Advanced-Ma…...

HsMod插件终极指南:55项功能详解与快速上手教程

HsMod插件终极指南:55项功能详解与快速上手教程 【免费下载链接】HsMod Hearthstone Modification Based on BepInEx 项目地址: https://gitcode.com/GitHub_Trending/hs/HsMod HsMod是一款基于BepInEx框架开发的《炉石传说》增强插件,为玩家提供…...

Z-Image-Turbo Web服务日志调试:从backend/main.py异常堆栈定位LoRA加载失败

Z-Image-Turbo Web服务日志调试:从backend/main.py异常堆栈定位LoRA加载失败 1. 问题背景与场景 最近在部署基于Z-Image-Turbo的图片生成Web服务时,遇到了一个棘手的问题:服务在启动时能够正常加载基础模型,但在尝试加载laonans…...

WinClaw安全实战 10|5分钟微信接入指南:零代码远程操控电脑,AI助手随身带

摘要:前9篇专栏已让WinClaw具备Office自动化、浏览器控制、零脚本测试等强大本地能力,但“必须坐在电脑前操作”的物理限制始终存在。本文作为“多渠道消息对接与远程操控”模块开篇,聚焦微信ClawBot官方接入方案——通过“安装插件→启动服务→扫码绑定”三步零代码操作,5…...

Gemma-3-270m入门指南:从模型选择到提问技巧的完整新手教学

Gemma-3-270m入门指南:从模型选择到提问技巧的完整新手教学 想试试最新的AI对话模型却不知道从哪开始?Gemma-3-270m可能是你的完美选择。这个由谷歌打造的轻量级模型,虽然参数规模不大,但能力却相当出色,特别适合新手…...

Qwen3.5-2B低门槛部署指南:无Linux经验用户也能完成的5步流程

Qwen3.5-2B低门槛部署指南:无Linux经验用户也能完成的5步流程 1. 为什么选择Qwen3.5-2B Qwen3.5-2B是阿里云推出的轻量化多模态基础模型,属于Qwen3.5系列的小参数版本(20亿参数)。这个模型特别适合想要尝试AI能力但又担心硬件配…...

HTML怎么实现记住我功能_HTML checkbox保存登录状态【方法】

HTML checkbox 本身不能“记住我”,需用 JavaScript 结合 localStorage 持久化勾选状态,并将 remember 信号传给后端以发放长期 HttpOnly cookie,前端不可存密码,且须兼容 Safari 隐私模式与 WebView 限制。HTML checkbox 本身不能…...

制造业、质检类20种业务场景,SQL精写技巧

在制造业、质检类业务场景中,经常需要基于上限(USL)、下限(LSL)字段整合标准值,或通过实测值(如PH、温度等)判断是否超出上下限。本文结合实际业务需求,整理了20种Oracle…...

Golang Redis Pipeline如何用_Golang Redis Pipeline教程【完整】

...

Halcon模板匹配后怎么把结果画出来?手把手教你用vector_angle_to_rigid和affine_trans_contour_xld搞定轮廓显示

Halcon模板匹配结果可视化实战:从矩阵变换到轮廓绘制的完整指南 刚完成Halcon模板匹配的兴奋感还没消退,却发现匹配结果无法直观展示在图像上——这是许多视觉工程师遇到的第一个"拦路虎"。本文将带你深入理解vector_angle_to_rigid和affine_t…...

Blender-Python脚本(材质篇)

一.查询/创建/删除材质 for blender_material in bpy.data.materials:print(blender_material.name) bpy.data.materials.new(nametest_material) bpy.data.materials.remove(bpy.data.materials[0]) 二.给物体添加材质 for blender_material in bpy.data.materials:blender…...

从录音转文字到 AI 漫画生成:智在记录让知识真正 “活” 起来

在信息爆炸的当下,我们每天都在和海量录音、文字内容打交道:职场会议录音、线下培训讲解、服务行业须知科普、知识博主口播内容…… 但长期以来,我们始终面临两个核心痛点:纯语音内容检索难、复盘难,大段很难快速抓取核…...

【flutter for open harmony】第三方库 Flutter 鸿蒙实战:get_it 依赖注入 + 模块化架构优化,项目秒变企业级✨

🚀 Flutter 鸿蒙实战:get_it 依赖注入 模块化架构优化,项目秒变企业级✨ 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net👋 前言 哈喽各位小伙伴!我是持续深耕 Flutter 鸿蒙跨平台…...

山东大学软件学院创新项目实训 —— 基于UE与LLM的医患沟通模拟与评价系统(四)

文章目录 前言一、场景列表接口实现1、get_all_scenarios_with_scenes()2、新增接口3、测试验证传参版本 二、设计人物音色三、集成TTS语音合成功能1. 新建 app/services/tts_service.py2. 修改 endpoints.py3. 在 AudioResponse schema 里加 audio_path 字段4. 测试TTS 前言 …...

FPGA----完美解决mmcx: Timeout waiting for hardware interrupt.

1、问题的出现:我在SD卡中有boot的fat分区和rootfs的ext4分区,以前每次启动经常性的出现sd卡无法加载的情况,我尝试了各种办法,甚至包括修改内核,一直无法解决。2、问题的解决: 开机时不要插入jtag线。这是…...

Java基础·第7篇:Java抽象类——把共性的先做了,剩下的你自己看着办

文章目录引入抽象方法和抽象类抽象类的使用抽象类的特征最后的话引入 假设我们要写一个员工管理系统。所有员工都有姓名、工号、薪水这些属性,也都需要打卡上班。这些代码每个子类都一样,如果让每个子类都写一遍,太麻烦了。 但是"工作…...