老系统升级与兼容改造实践
1. 背景与目标
公司原有业务系统采用 Node + jQuery 架构,长期运行后暴露出典型遗留系统问题:
- 技术栈老旧,工程化与类型体系薄弱;
- 前后端边界不清,页面脚本与业务逻辑耦合严重;
- 公共方法分散,重复代码多,变更成本高;
- 发布链路依赖人工流程,质量保障不足。
本次升级以“稳定优先、渐进迁移、可回滚”为原则,整体使用 Vue 3 + TypeScript + Vite + Pinia 搭建新中台外壳,在尽量不改动老业务逻辑的前提下,通过 iframe 承载历史页面,逐步完成能力替换与业务迁移。
核心目标如下:
- 建立统一登录与权限体系;
- 统一路由、菜单、接口层和全局状态;
- 保持关键业务连续可用,避免一次性重写导致的风险;
- 为后续模块化迁移提供标准化底座。
2. 总体方案与架构分层
2.1 双层架构
- 外层(新中台):
Vue 3 + TypeScript + Vite + Pinia- 承担登录鉴权、布局、菜单、动态路由、请求封装、全局异常处理等职责。
- 内层(老系统):
iframe承载历史页面- 继续承载高耦合存量业务,减少对老代码入侵改造。
该方案的价值在于:先统一“系统能力层”,再按业务价值与复杂度分批迁移页面,降低全量替换风险。
2.2 迁移阶段建议
可按以下节奏推进(可根据团队规模调整):
- 底座期(1-2 周):搭建脚手架、鉴权、路由、请求层、日志与监控。
- 并行期(2-6 周):新老页面并存,优先迁移高频/高收益页面。
- 收敛期(持续):下线低价值旧页,沉淀组件与规范,建立迁移验收机制。
3. 升级范围与关键改造点
3.1 登录与账户体系重构
- 以新框架重建登录、重置密码、会话续期流程;
- 统一错误码处理(如未登录、会话过期、权限不足);
- 避免在
localStorage明文存放敏感信息,优先采用HttpOnlyCookie(若后端条件允许)。
3.2 路由与权限(后端主导)
推荐由后端维护路由元数据与权限点,前端根据返回的路由树动态渲染菜单与页面,并通过路由守卫控制访问。
建议路由元数据至少包含:
path、name、component(或iframeUrl);meta.title、meta.icon、meta.roles、meta.keepAlive;- 页面级权限点(如
user:create、order:refund)。
3.3 基础能力统一封装
utils:日期、数值、字符串、安全转义、节流防抖等;hooks:加载态、弹窗、消息、分页筛选等可复用交互逻辑;config:系统常量、品牌配置、环境差异配置;store:用户信息、权限状态、全局 UI 状态;http:请求拦截、响应归一化、错误上报。
3.4 类型与接口契约治理
- 使用 TypeScript 描述接口响应结构与业务实体;
- 通过枚举/常量集中维护 API 路径,减少“魔法字符串”;
- 建议与后端约定统一响应结构(如
code/message/data)。
4. 环境配置与多环境支持
企业项目通常存在开发、测试、预发布、生产等多环境。接口域名、静态资源域名、iframe 宿主域名可能不同,建议通过 Vite .env 文件管理。
示例:
VITE_APP_API_URL=https://api.example.com
VITE_APP_APP_ID=123456
VITE_APP_IFRAME_HOST=https://legacy.example.com
代码中读取:
const apiUrl = import.meta.env.VITE_APP_API_URL;
const iframeHost = import.meta.env.VITE_APP_IFRAME_HOST;
注意事项:
- 仅以
VITE_前缀声明的变量会暴露到前端; - 不要将密钥、数据库密码等敏感信息写入前端环境变量;
- 为每个环境配置独立域名白名单与
CORS策略。
5. 基础能力封装实践
5.1 axios 请求层(示例)
以下示例强调两个实践:请求阶段统一注入上下文、响应阶段统一归一化与错误处理。
import axios, {
type AxiosInstance,
type InternalAxiosRequestConfig,
type AxiosResponse,
} from "axios";
import qs from "qs";
const instance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_APP_API_URL as string,
timeout: 60_000,
});
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
if (config.method === "get" && config.params) {
config.paramsSerializer = (params) =>
qs.stringify(params, { arrayFormat: "repeat" });
}
return config;
},
(error) => Promise.reject(error),
);
instance.interceptors.response.use(
(response: AxiosResponse) => response.data,
(error) => {
const status = error?.response?.status;
if (status === 401) {
// 可在此做统一登出与重定向
}
return Promise.reject(error?.response ?? error);
},
);
export interface IHttpResult<T> {
code: number;
message: string;
data: T;
success: boolean;
}
export const httpServer = {
get<T = unknown, U = unknown>(url: string, params?: U) {
return instance.request<IHttpResult<T>>({ url, method: "get", params });
},
post<T = unknown, U = unknown>(url: string, data?: U) {
return instance.request<IHttpResult<T>>({ url, method: "post", data });
},
postForm<T = unknown, U = unknown>(url: string, params?: U) {
return instance.request<IHttpResult<T>>({
url,
method: "post",
data: qs.stringify(params, { arrayFormat: "repeat" }),
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
});
},
};
5.2 API 枚举与函数封装(示例)
enum UserApi {
UploadImage = "/api/upload/image",
DeleteImage = "/api/upload/delete",
ImageList = "/api/upload/list",
}
import { httpServer } from "@/http";
export function getImageList<T, U>(params: U) {
return httpServer.get<T, U>(UserApi.ImageList, params);
}
6. iframe 并行阶段的体验优化
6.1 常见问题
简单使用 <iframe :src="url"> 在路由切换时会触发整页重载,导致:
- 重复请求静态资源与接口;
- 表单临时输入丢失;
- 页面滚动位置与交互上下文丢失。
6.2 优化策略
- 使用
v-show控制显隐,尽量避免卸载重建; - 维护
iframe实例缓存(如按路由path建立 Map); - 在切换前后同步滚动位置;
- 对高频页面可增加预加载策略(注意并发与内存上限)。
示例(简化):
<iframe
v-for="item in routes"
:key="item.path"
v-show="item.path === route.path"
:src="item.iframeUrl"
/>
7. 新老系统通信与图片预览兼容
老系统常以“新标签页打开图片”为默认行为;新系统可能希望统一为 Element Plus 预览组件。可通过 postMessage 做父子通信,但必须关注安全边界。
7.1 通信示例
// iframe 内部(老系统)
window.parent.postMessage(
{
command: "open-image",
data: { url },
},
"https://admin.example.com",
);
// 外层新系统
function handleMessage(event) {
const trustedOrigin = "https://legacy.example.com";
if (event.origin !== trustedOrigin) return;
if (event.data?.command === "open-image") {
const imageUrl = event.data?.data?.url;
// 1) 调用图片预览组件,或
// 2) 兜底 window.open(imageUrl, "_blank", "noopener,noreferrer")
}
}
window.addEventListener("message", handleMessage);
// 卸载时移除监听
window.removeEventListener("message", handleMessage);
7.2 安全建议
targetOrigin不要使用"*",应指定明确来源;- 接收消息时必须校验
event.origin和消息结构; - 对 URL 做协议与域名白名单校验,防止注入恶意链接;
- 统一定义消息协议版本(如
type,version,payload),降低联调歧义。
8. 风险控制与验收建议
8.1 风险清单
- 会话一致性风险:新老系统 token 刷新机制不一致;
- 权限漂移风险:菜单可见但接口无权,或反之;
- 性能风险:
iframe数量过多导致内存上涨; - 通信风险:
postMessage校验不足带来安全隐患。
8.2 验收维度
- 功能正确性:登录、登出、权限、关键业务流程;
- 体验一致性:切页速度、滚动位置恢复、图片预览行为;
- 稳定性:接口错误回退、白屏兜底、异常链路可观测;
- 可维护性:新增页面开发时间、重复代码减少比例、问题定位时长。
9. 结语
对遗留系统升级而言,“推倒重来”通常不是最优解。通过“新外壳 + 老页面承载 + 渐进迁移”的策略,可以在业务连续运行的前提下,逐步获得现代工程能力,并为后续彻底去遗留化奠定基础。
参考文献
- Vite 文档:环境变量与模式
https://vite.dev/guide/env-and-mode.html - Vue 3 官方文档
https://vuejs.org/ - Pinia 官方文档
https://pinia.vuejs.org/ - Axios 官方文档
https://axios-http.com/ qs仓库
https://github.com/ljharb/qs- MDN:
window.postMessage
https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage - Element Plus 官方文档
https://element-plus.org/ - OWASP Top 10
https://owasp.org/www-project-top-ten/
相关文章
GridView 宫格加载渲染优化
系统首页 GridView 宫格模块接口耗时不高,但首次进入总耗时接近 22s。复盘耗时分层定位过程、代码层面的瓶颈(约 2000 行、100 个 tab 重复节点)、优化手段与最终指标对比。
新商家系统性能优化实践
商家系统新版本上线后,团队持续针对构建效率与用户体验做了一轮系统性优化。本文记录核心方案与落地结果,供后续版本复用。
防篡改水印
在各类管理后台、SaaS 平台或内部系统中,页面水印已经是非常常见的能力:一方面在截图时携带账号、姓名、时间等信息,降低截图外传的风险;另一方面在上传图片或文档预览时,通过水印标记上传者与时间,满足审计和合规需求。
图片上传前的自定义水印实践
项目需要在图片上传前叠加动态水印。水印内容不是固定文案,而是由多个动态元素组成(如时间、地点、业务字段等)。
键盘弹起导致底部被顶起问题(H5 适配)
在移动端 H5 页面中,当用户聚焦 input、textarea 等可编辑元素、软键盘弹出时,常见表现包括:
企业微信 uni-app H5:OAuth 回退白屏与列表缓存
第三方 BI 报表项目,企业微信内嵌 H5,uni-app 编译,history 模式。主页面 BiLink 同时承担静默授权和列表展示,上线后碰到两个问题:授权完清掉 URL 参数,iOS 侧滑返回还是白屏;列表加了缓存以后,刷新行…
Series
team documents
4 / 22