news 2026/10/3 14:53:16

React中刷新页面Token丢失?Redux+LocalStorage方案详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React中刷新页面Token丢失?Redux+LocalStorage方案详解

刷新页面掉登录,这个坑做 React 的人几乎都踩过。明明登录成功了,token 也拿到手了,按一下 F5,瞬间被踢回登录页,接口跟着一片 401。更糟的是,这种问题往往不是必现的,偶尔出现一次,排查起来特别费劲。我自己在好几个项目里都遇到过这个场景,从最早的 React 16 + Redux 经典组合,到后来用 Redux Toolkit + TypeScript 的新项目,底层逻辑其实都一样:Redux store 是内存态,一刷新就归零。

这篇文章会围绕 React + Redux + LocalStorage 这套组合,把 token 持久化从登录写入、初始化恢复、请求注入、401 统一处理到 Hook 封装的完整链路讲清楚。不会只贴代码,重点是解释每一步为什么这么做、坑在哪里。适合正在写 React 中后台项目、或者刚接手一个"刷新就掉登录"老项目的同学参考,看完基本能自己动手把整套逻辑理顺。

1. 问题拆解:刷新页面后 Token 为什么消失

先别急着写 localStorage,得先把问题发生的机制搞清楚。好多同学卡在"为什么我 token 明明存了,刷新还是丢",本质上是没理解前后端各层状态的生命周期。

1.1 Redux store 的"内存态"本质

Redux 把整个应用的状态放在一个 JavaScript 对象树里,由 store 统一管理。这个对象树活在浏览器当前页面的运行时内存中。你路由跳转、组件挂载卸载、派发 action,所有状态都在这份内存里变来变去。可一旦你按下 F5,浏览器会销毁当前页面的整个运行时环境——JS 引擎重新启动,脚本重新加载,store 重新执行createStore或configureStore,state 回到初始化值。

打个比方,store 就像一块白板,你在上面写了"当前用户已登录",只要不擦掉,页面内的任意操作都能看到。但刷新页面相当于换了一块新的白板,原来写的东西全没了。Redux 从设计上就没打算替你跨页面保存数据,它解决的是页面内部状态混乱的问题,不是持久化问题。

所以问题不在 Redux 本身,而在于我们把登录态只放进了内存态里,没有同步落地到浏览器提供的持久化存储中。

1.2 Token 消失后的连锁反应

Token 一旦在刷新后丢失,后果不是"重新登录一次"那么简单,而是一连串的连锁反应:

  • 受保护的路由组件拿到isAuthenticated = false,直接重定向到登录页,用户刚才填了一半的表单全丢了;
  • 即使路由没拦,请求拦截器也拿不到 token,发出的请求不带Authorization头,后端返回 401;
  • 如果项目里做了 401 全局处理,比如自动跳登录页,用户会被莫名其妙地踢下线,体验极差;
  • 更隐蔽的是,很多项目在main.tsx或App.tsx里异步请求用户信息,token 没了,这个请求 401,用户信息渲染不出来,页面出现空白或报错。

这些问题往往不是单点故障,而是从状态恢复缺失开始,一层层往外扩散。理解了这一整条链路,才能明白为什么"刷新后恢复登录态"是必须优先解决的问题,而不是一个锦上添花的优化项。

2. 方案选型:为什么是 Redux + LocalStorage

登录态要跨刷新保存,方案其实不止一种。选 LocalStorage 不是因为它最好,而是因为它在"实现成本"和"适用场景"之间最平衡。下面按实际项目里的取舍标准拆一下。

2.1 几种前端存储方案横向对比

存储方案生命周期跨标签页容量安全性适用场景
内存变量页面刷新即销毁否不限最高(不进磁盘)临时状态
sessionStorage标签页关闭即销毁否约 5MB中会话级临时数据
localStorage手动清除才销毁是约 5MB中(可被 XSS 读取)持久化登录态、偏好设置
cookie可设置 Expires跨标签页共享约 4KB可设 HttpOnly服务端会话标识

从这张表能看出几个关键点。sessionStorage 的问题是标签页隔离——你在这个标签页登录了,开新标签页还得重新登录,用户会觉得莫名其妙。cookie 的问题是容量小且有 CSRF 风险,虽然可以用 HttpOnly 缓解,但在纯前端 SPA 项目里操作起来麻烦。localStorage 容量足够,跨标签页共享,API 简单到只有getItem/setItem/removeItem,配合 Redux 做状态恢复非常直接。

2.2 LocalStorage 的安全边界与妥协

说实话,把 token 放 localStorage 并不是绝对安全的方案。它的最大软肋是XSS 攻击:如果项目里不小心引入了第三方脚本,或者有dangerouslySetInnerHTML渲染不可信内容,攻击者就能执行脚本读取 localStorage 里的 token,然后拿这个 token 冒充用户调用接口。

但现实是,大部分中后台项目里的 token 不是业务敏感数据本身,而是一个访问凭证。真正敏感的数据放在服务端,接口会做权限校验。所以 localStorage 存 token 是行业里极其普遍的做法,属于"可接受的妥协"。

如果对安全性有更高要求,可以退一步做分层:access token 留在内存里,refresh token 放在 HttpOnly cookie 里。刷新页面后先拿 refresh token 换新的 access token,再写回内存。这样 XSS 拿不到 refresh token,token 续签也不会因为页面刷新而中断。不过这套方案的复杂度会明显上升,需要后端配合,适合对安全有硬性要求的项目。如果当前项目规模不大、主要面向内部系统,先做好 localStorage + 统一拦截处理,已经能解决 90% 的问题。

3. 完整落地:Redux + LocalStorage 持久化实现

方案定下来之后,动手实现。这一整节讲的是标准链路,每一步我都标注了为什么要这么做,方便你在自己项目里对应着改。

3.1 先封装一个干净的 token 存储工具

很多人图省事,直接在业务代码里到处写localStorage.getItem('token')。一开始没什么,等你要换 key 名前缀、加版本号、或者在测试里 mock 存储的时候,就会发现处处都是散落的字符串,改起来想哭。

我习惯第一步先封装一个独立模块,所有存储操作都走这个模块:

// utils/tokenStorage.js const TOKEN_KEY = 'myapp_access_token'; const USER_KEY = 'myapp_user_info'; export const tokenStorage = { getToken() { try { return localStorage.getItem(TOKEN_KEY); } catch (e) { return null; } }, setToken(token) { try { localStorage.setItem(TOKEN_KEY, token); } catch (e) { // 隐私模式或配额超限时静默失败 } }, removeToken() { localStorage.removeItem(TOKEN_KEY); }, getUserInfo() { try { const raw = localStorage.getItem(USER_KEY); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } }, setUserInfo(info) { localStorage.setItem(USER_KEY, JSON.stringify(info)); }, clear() { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY); }, };

几个设计细节解释一下:

  • key 名加项目前缀。中后台项目经常和多套系统共用同一个域名下的存储空间,token这种通用 key 很容易互相覆盖。加个myapp_前缀成本极低,能避免一类很诡异的 bug;
  • JSON 序列化统一处理。用户信息是对象,直接存进去只会得到[object Object],封装层里统一处理JSON.stringify和JSON.parse,业务侧不用关心这些;
  • try/catch 包裹。localStorage 在某些隐私模式下会直接抛异常,不加保护的话,一个getItem就能让整个应用白屏;
  • 只暴露方法,不暴露 key。这样后续改 key 名、迁移存储位置,只动这个文件就行。

3.2 登录成功后写入 store 和 localStorage

登录流程的典型做法是用 Redux Toolkit 的createAsyncThunk发登录请求,拿到 token 后同时写入 store 和 localStorage。写入 store 是为了当前页面立刻能用,写入 localStorage 是为了下一次刷新能恢复。

// store/authSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { loginApi } from '../api/auth'; import { tokenStorage } from '../utils/tokenStorage'; export const login = createAsyncThunk( 'auth/login', async ({ username, password }) => { const data = await loginApi({ username, password }); return data; // 形如 { token, userInfo } } ); const authSlice = createSlice({ name: 'auth', initialState: { token: null, userInfo: null, isAuthenticated: false, loading: false, }, reducers: { setAuth(state, action) { state.token = action.payload.token; state.userInfo = action.payload.userInfo || null; state.isAuthenticated = Boolean(action.payload.token); }, clearAuth(state) { state.token = null; state.userInfo = null; state.isAuthenticated = false; }, }, extraReducers: (builder) => { builder .addCase(login.pending, (state) => { state.loading = true; }) .addCase(login.fulfilled, (state, action) => { state.token = action.payload.token; state.userInfo = action.payload.userInfo || null; state.isAuthenticated = true; state.loading = false; // 直接在这里持久化,避免业务代码里重复写 tokenStorage.clear(); tokenStorage.setToken(action.payload.token); if (action.payload.userInfo) { tokenStorage.setUserInfo(action.payload.userInfo); } }) .addCase(login.rejected, (state) => { state.loading = false; }); }, }); export const { setAuth, clearAuth } = authSlice.actions; export default authSlice.reducer;

这里有个值得养成的习惯:写入持久化放在异步请求的 fulfilled 回调里,而不是登录组件的.then()里。原因很简单:状态更新、持久化这两件事和 UI 无关,属于业务状态流转的一部分,放进 reducer 生命周期里,任何地方调用dispatch(login(...))都能得到一致的行为,不会出现"这里记得存、那里忘了存"的情况。

3.3 应用初始化时恢复登录态

这是"刷新不掉登录"最关键的一步。应用每次启动,store 创建后立刻从 localStorage 里读取 token,派发setAuth把状态填回去。

// store/index.js import { configureStore } from '@reduxjs/toolkit'; import authReducer, { setAuth } from './authSlice'; import { tokenStorage } from '../utils/tokenStorage'; const store = configureStore({ reducer: { auth: authReducer, }, }); // 关键:在 store 创建后立即恢复登录态 const token = tokenStorage.getToken(); const userInfo = tokenStorage.getUserInfo(); if (token) { store.dispatch(setAuth({ token, userInfo })); } export default store;

这一步背后的逻辑是:store 的初始化不只是空状态,而应该是"从持久层恢复"的状态。把恢复逻辑放在store/index.js这个模块的顶层执行,能保证在任何组件渲染之前,isAuthenticated就已经是正确值。路由守卫、页面组件第一次读取状态时拿到的就是恢复后的登录态,不会先渲染一帧"未登录"再跳转。

关于用户信息的恢复有一点要特别注意:localStorage 里的旧用户信息可能是过期的,比如用户资料改了、权限变了。所以有些项目选择只恢复 token,然后发一个getUserInfo请求拉最新资料。我自己的经验是:先从 localStorage 恢复一份缓存用着,让首屏先渲染,同时异步请求最新用户信息,请求回来后用新数据覆盖 store 和 localStorage。这样既保证了页面不闪白,又不会用陈旧数据太久。

路由守卫侧的做法是:用一个React.memo包裹的路由组件,加载状态为loading时渲染空白或 loading 页,等isAuthenticated确定后再决定放行还是重定向:

// router/RequireAuth.jsx import { Navigate } from 'react-router-dom'; import { useSelector } from 'react-redux'; export default function RequireAuth({ children }) { const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); const loading = useSelector((state) => state.auth.loading); if (loading) { return <div>加载中...</div>; } if (!isAuthenticated) { return <Navigate to="/login" replace />; } return children; }

3.4 请求拦截器统一注入 token 并处理 401

Token 恢复只是第一步,后续每个接口请求都得带上 token。用 Axios 拦截器做统一注入,是避免每个api.get(...)都手动传 header 的最优解。

// api/client.js import axios from 'axios'; import { tokenStorage } from '../utils/tokenStorage'; import store from '../store'; import { clearAuth } from '../store/authSlice'; const api = axios.create({ baseURL: '/api', timeout: 15000, }); // 请求拦截器:注入 token api.interceptors.request.use( (config) => { const token = tokenStorage.getToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // 响应拦截器:401 统一处理 api.interceptors.response.use( (response) => response, (error) => { if (error.response && error.response.status === 401) { // 清理本地状态 tokenStorage.clear(); store.dispatch(clearAuth()); // 避免重复跳转:判断当前不在 /login 再跳 if (!window.location.pathname.startsWith('/login')) { window.location.href = '/login'; } } return Promise.reject(error); } ); export default api;

这个拦截器有三个容易踩的细节:

  • token 从 localStorage 读取,而不是从 store 读取。原因在于模块加载顺序和闭包问题:拦截器在 store 更新前就可能被调用,从 localStorage 读取永远拿得到最新值;
  • 401 清理时要判断当前路由。如果不判断,登录页本身发出的登录请求如果返回 401,会被重定向到 /login,形成自我跳转,页面会闪一下。判断一下能省掉这个异常体验;
  • 跳转用window.location.href还是navigate?这里要分场景:如果是同应用路由,用navigate更顺滑,能保留页面状态;如果是 token 失效需要强制重新登录,我倾向于window.location.href直接整页刷新,把内存里可能残留的状态彻底清干净,避免"跳过去了但内存里还有旧数据"的尴尬。

3.5 退出登录的清理顺序

退出登录看起来就是清数据,但顺序有讲究。一种常见的 bug 是:先清 store,后清 localStorage,结果清 store 时组件监听到了状态变化,触发了什么副作用,又去读 localStorage,读到了还没删掉的 token,导致状态反复横跳。

我的做法是固定顺序:先调后端登出接口(如果有)→ 清理 localStorage → 再 dispatch clearAuth。清理 localStorage 的动作要放在 dispatch 之前,这样从清除的那一刻起,任何副作用都拿不到旧 token。

export const logout = createAsyncThunk('auth/logout', async (_, { dispatch }) => { try { await logoutApi(); } finally { tokenStorage.clear(); dispatch(clearAuth()); } });

4. Hook 使用中的坑与避坑指南

状态恢复解决了,接下来是 Hook 层面的坑。实际项目里,很多"刷新后 token 消失"的诡异现象,根因往往不是存储方案的问题,而是 useEffect、useSelector 用得不对,导致状态被错误覆盖或依赖链断裂。

4.1 闭包陷阱:useEffect 里读到了过期 token

这是我见过最多的坑。一个典型的场景:页面加载时判断 token 是否有效,无效就跳登录页。

// 错误示例 const token = useSelector((state) => state.auth.token); useEffect(() => { if (token && isTokenExpired(token)) { logout(); } }, []);

问题在于useEffect的回调只会在挂载时执行一次,它捕获的是挂载那一刻的token值。如果 token 的初始化恢复发生在 useEffect 执行之后(比如异步恢复),回调里拿到的token就是null,判断逻辑直接失效。即使恢复同步完成了,如果后续 token 变了(比如刷新了 token),这个 useEffect 也感知不到。

正确做法是把判断放进状态流转里,而不是组件生命周期里:

// 推荐:把 token 有效性判断交给拦截器 // 组件里只需要订阅最终状态 const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); useEffect(() => { if (isAuthenticated) { // 拉取业务数据 } }, [isAuthenticated]);

核心原则是:不要把"状态判断"写在空依赖数组的 useEffect 里,让它依赖真实的订阅值。如果你确实需要监听某个变量的变化,就要把它写进依赖数组。如果你担心依赖数组导致重复执行,问题大概率出在依赖值不稳定(比如每次 render 都生成新对象),而不是不该监听。

4.2 useEffect 执行时机与无限渲染循环

另一个高频坑是 useEffect 配合 useSelector 导致的无限循环。看这段代码:

const userInfo = useSelector((state) => state.auth.userInfo); useEffect(() => { if (!userInfo) { dispatch(fetchUserInfo()); } }, [userInfo, dispatch]);

这段代码的本意是用户信息为空时拉取一次。看起来很合理,但有个隐藏问题:useSelector的默认比较方式是引用相等。如果 reducer 返回了新的对象引用(比如state.userInfo = action.payload每次都是新对象),那么每次 dispatch 后userInfo都会变,useEffect 又依赖它,于是:dispatch fetch → userInfo 更新 → effect 触发 → 判断 userInfo 不为空不再 fetch。看起来没问题,但如果 fetch 接口返回的数据每次都不同,或者 reducer 里做了某种 transform,userInfo 永远不会稳定,就会无限循环。

更隐蔽的版本是 selector 返回一个新派生对象:

// 千万别这样写 const authInfo = useSelector((state) => ({ token: state.auth.token, isAuthenticated: state.auth.isAuthenticated, }));

每次 redux store 更新,即使token和isAuthenticated都没变,authInfo也都是新对象,导致所有依赖authInfo的 useEffect 每次都执行、所有 memo 组件每次都重渲染。正确做法是用useSelector的浅比较特性,分开选择基本类型值,或者用createSelector做记忆化:

import { createSelector } from '@reduxjs/toolkit'; const selectAuthInfo = createSelector( (state) => state.auth.token, (state) => state.auth.isAuthenticated, (token, isAuthenticated) => ({ token, isAuthenticated }) ); const authInfo = useSelector(selectAuthInfo);

4.3 自定义 Hook 统一封装登录态逻辑

与其在每个组件里散落各种useSelector和useDispatch,不如封装一个自定义 Hook,把登录态相关逻辑收敛到一起。这是我的项目里实际在用的一个简化版:

// hooks/useAuth.js import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { login as loginThunk, logout as logoutThunk } from '../store/authSlice'; import { tokenStorage } from '../utils/tokenStorage'; export function useAuth() { const dispatch = useDispatch(); const token = useSelector((state) => state.auth.token); const userInfo = useSelector((state) => state.auth.userInfo); const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); const loading = useSelector((state) => state.auth.loading); const login = useCallback( (params) => { return dispatch(loginThunk(params)).unwrap(); }, [dispatch] ); const logout = useCallback(() => { return dispatch(logoutThunk()).unwrap(); }, [dispatch]); const updateToken = useCallback( (newToken) => { tokenStorage.setToken(newToken); dispatch(setAuth({ token: newToken, userInfo })); }, [dispatch, userInfo] ); return { token, userInfo, isAuthenticated, loading, login, logout, updateToken, }; }

封装带来的好处是:业务组件里不再直接依赖 Redux 的 API,后续要切换状态管理方案(比如换 Zustand),只需要改这一个 Hook。而且login返回 Promise,组件里可以很方便地做表单 loading、错误提示,不用自己再监听 loading 状态。

5. 常见问题排查实录与实用经验

最后这部分是我的"踩坑实录",也是每次处理此类问题时的排查清单。按表格整理出来,方便以后遇到类似问题直接对着查。

5.1 高频问题速查表

现象可能原因排查方向解决方案
刷新后立刻跳登录页初始化恢复逻辑缺失或写在组件里检查 store 创建后是否读取 localStorage 并 dispatch在store/index.js顶层执行恢复逻辑
刷新后能进页面,但接口全部 401请求拦截器没有统一注入 token检查 request interceptor 是否从存储读取确认注入Authorization,从 localStorage 读取
登录成功后立即刷新,token 仍有,但用户信息没恢复没有持久化用户信息或恢复逻辑不完整检查 localStorage 里是否有 userInfo增加setUserInfo和恢复逻辑
多个标签页登录状态不同步没有监听 storage 事件在另一个标签页登录后,当前页状态未更新加storage事件监听并 dispatch setAuth
Token 过期后接口 401,但页面没有反应响应拦截器没有统一处理 401检查 response interceptor401 时清理状态并跳登录
清除 localStorage 后 store 里还有旧状态清理顺序不对检查 logout 逻辑先清存储再 dispatch clearAuth
组件里 token 更新了但界面不刷新useSelector 选择了派生对象导致重渲染异常检查是否返回新对象用 createSelector 或分开选择基础值
无限请求用户信息接口useEffect 依赖了新对象检查依赖数组和 selector用 useCallback 和记忆化 selector

5.2 几个值得长期坚持的设计习惯

排查过太多这类问题之后,我总结出几个"如果一开始就做好,后面能少走很多弯路"的习惯。

第一,坚持单一数据源原则。token 的"真值"在 localStorage,store 是它的镜像。任何地方修改 token,都必须先操作 localStorage,再同步派发 action。不要出现"某些地方改 store、某些地方改 localStorage"的双写混乱,否则一旦顺序错位,刷新后状态必然对不上。

第二,key 名带上项目标识和版本号。比如myapp_v2_access_token。上线新版本时如果存储结构变了,可以通过改版本号来避免旧数据残留导致解析失败。很多诡异的"登录不了"问题,排查到最后都是新旧数据结构不兼容。

第三,用 storage 事件同步多标签页。用户经常在多个标签页间切换。这个标签页退出了,另一个标签页还停留在登录状态。加一段全局监听:

window.addEventListener('storage', (e) => { if (e.key === 'myapp_access_token') { if (e.newValue) { store.dispatch(setAuth({ token: e.newValue, userInfo: null })); } else { store.dispatch(clearAuth()); } } });

注意storage事件只在其他标签页修改 localStorage 时触发,当前标签页自己改不触发,所以不会造成多余渲染。这段代码放在main.jsx或store/index.js里即可。

第四,凡是操作 localStorage 的地方,都做好异常兜底。隐私模式、存储配额、测试环境都可能让setItem抛异常。不加 try/catch 的话,一个存储异常能让整个功能不可用,这不该发生。

5.3 token 续签和失效时间的一个处理思路

最后补充一个和持久化强相关但容易被忽略的点:token 过期了怎么办。很多项目只做了"过期后 401 → 跳登录页",用户体验就是干坐了一会儿,突然被踢下线。稍微好一点的做法是 request 拦截器里判断 token 是否将过期,快过期时用 refresh token 静默续签,成功就自动重放原请求。

这个逻辑实现起来不复杂,核心是"用一个队列缓存待重放的请求,防止同时多个请求各自刷新 token 造成重复请求":

let isRefreshing = false; let pendingQueue = []; async function refreshTokenAndRetry(error) { const { config } = error; if (!isRefreshing) { isRefreshing = true; try { const newToken = await requestRefreshToken(); // 调用刷新接口 tokenStorage.setToken(newToken); store.dispatch(setAuth({ token: newToken, userInfo: store.getState().auth.userInfo })); pendingQueue.forEach((cb) => cb(newToken)); pendingQueue = []; config.headers.Authorization = `Bearer ${newToken}`; return api(config); } catch (refreshError) { pendingQueue.forEach((cb) => cb(null)); pendingQueue = []; tokenStorage.clear(); store.dispatch(clearAuth()); window.location.href = '/login'; return Promise.reject(refreshError); } finally { isRefreshing = false; } } return new Promise((resolve, reject) => { pendingQueue.push((newToken) => { if (newToken) { config.headers.Authorization = `Bearer ${newToken}`; resolve(api(config)); } else { reject(error); } }); }); }

这段代码完整落地需要后端配合提供 refresh_token 接口。如果你的后端暂不支持,也可以先用"401 时清状态跳登录"的简化方案,但心里要清楚:真正的用户友好方案是静默续签,而不是让用户重新登录。

刷新丢 token 这个问题,说起来就一句话——内存状态没有持久化——但真正落地时牵扯到登录写入、初始化恢复、请求注入、异常清理、Hook 依赖管理一整条链路。我个人在实际项目里的体会是:先把存储和状态之间的映射关系彻底理清楚,再动手写代码,比什么技巧都管用。理清了这条线,你再去接 Zustand、接 Vue 的 Pinia,或者换成 sessionStorage,本质都是一样的套路,只是 API 变了而已。

最后分享一个小建议:做完持久化改造后,别只在"页面刷新"这一个场景下验证,多试几个场景——登录后快速刷新、多个标签页同时操作、token 手动改坏、清掉 localStorage 再刷新。这几个场景能帮你把状态恢复和清理逻辑中的边角漏网全部揪出来。改完之后,你会发现"F5 后面露出的獠牙"其实并不可怕,真正的问题永远藏在状态生命周期和存储生命周期没有对齐的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 14:50:16

Comsol两相流与流固耦合仿真:从建模思路到工程实践全解析

做仿真这些年&#xff0c;我有个很深的体会&#xff1a;两相流是CAE里最“磨人”的一类问题。界面怎么捕捉、网格怎么更新、压力怎么不振荡、质量怎么守恒&#xff0c;每一步都是在跟数值稳定性较劲。Comsol 作为多物理场耦合的老牌选手&#xff0c;在两相流和流固耦合交叉的场…

作者头像 李华
网站建设 2026/10/3 14:50:13

小型语言模型微调实战:LoRA参数高效微调全流程与避坑指南

小型语言模型的微调这件事&#xff0c;我从去年开始断断续续折腾了快一年。最开始的想法很简单&#xff1a;手里有一批垂直领域的问答数据&#xff0c;想让它按照我的语料风格来回答问题&#xff0c;而不是每次都要在提示词里塞一大堆背景信息。但真正动手之后才发现&#xff0…

作者头像 李华
网站建设 2026/10/3 14:48:42

Python连接MySQL入门:从环境配置到pymysql实战

自从开始写 Python 学习记录这个系列&#xff0c;我就一直在想第一篇应该从哪讲起。后来发现&#xff0c;很多人卡住的第一关根本不是语法&#xff0c;而是“工具没装好、数据库连不上”。搜了一圈 python 安装教程、mysql 安装配置教程、navicat for mysql&#xff0c;再对照自…

作者头像 李华
网站建设 2026/10/3 14:48:37

MMC子模块电容电压均压控制:从排序算法到样机调试的完整实践指南

做了快三年的MMC仿真和样机调试&#xff0c;坦白说第一次把子模块电容电压均压控制策略写进控制器时&#xff0c;我低估了这个看似简单的逻辑有多容易被细节击穿。说出来一句话——让桥臂上几十上百个直流电容的电压乖乖聚在额定值附近。但真把这套逻辑放进最近电平逼近调制&am…

作者头像 李华
网站建设 2026/10/3 14:48:35

风储VSG并网仿真全解析:从原理建模到波形调参实战

做风电并网仿真的人&#xff0c;大概率绕不开一件事&#xff1a;新能源占比高了以后&#xff0c;电网的惯量和阻尼被稀释得很厉害。风储VSG&#xff0c;也就是把虚拟同步发电机算法用在风储联合系统上&#xff0c;这几年在Simulink仿真里特别火。简单说&#xff0c;就是用储能配…

作者头像 李华
网站建设 2026/10/3 14:48:05

大数据场景下数据复制实战指南:distcp调优与跨集群同步策略

大数据时代&#xff0c;数据体量早就不是按GB算了&#xff0c;动不动就是几十TB到PB级别的集群。在这种规模下&#xff0c;“数据复制”四个字听起来简单&#xff0c;做起来是真要命的活。相信不少朋友都经历过这种场景&#xff1a;业务方一句“把A集群的数据同步到B集群”&…

作者头像 李华