EMI协同管控平台 — 面试问答
项目时间:2023年6月 - 2023年12月 | 企业级集成中台,系统模块化程度提升60%+ 技术栈:Vue3 + TypeScript + Wujie(腾讯微前端框架)+ Vite + Pinia + Element Plus + TailwindCSS + ECharts 线上地址:https://emi.goomaker.com
Q1: Wujie 微前端架构设计
面试官可能问:"为什么选 Wujie 而不是 qiankun?Wujie 的沙箱隔离是怎么实现的?"
回答:
选 Wujie 而不是 qiankun 有几点核心原因:
- 真正的技术栈无关:qiankun 要求子应用必须导出
bootstrap/mount/unmount生命周期,意味着旧项目必须改造。Wujie 只需要一个 URL 就能接入,不管是 Vue2/Vue3/React/jQuery 都能直接加载,零改造成本。 - 双层隔离方案:Wujie 用 WebComponent 做样式隔离 + iframe 做 JS 沙箱隔离,不需要 qiankun 那种
eval+ Proxy 形式,天然避免了Function/eval的兼容性问题。 - shadow DOM 降级:在不支持 shadow DOM 的环境下(比如某些旧浏览器)也能正常运行。
隔离原理:
- 样式隔离:子应用被渲染到一个 WebComponent(自定义元素)内部,WebComponent 的 Shadow DOM 天然隔离 CSS——子应用的样式不会泄漏到主应用,主应用的样式也不会影响子应用。
- JS 沙箱:子应用跑在 iframe 内的独立 Window 上下文里,全局变量
window.a = 1只在子应用内有效,不会污染主应用的window。同时通过proxy把 iframe 的document等关键对象代理到主应用的 DOM 树上,让子应用"以为自己跑在正常页面里"。
追问:"无界和无界子应用打包为什么放在项目里而不是 npm 包?"
因为对 Wujie 的源码做了定制修改。项目里 src/components/wujie/wujie-core 和 wujie-vue3 是直接 Fork 源码进来改的——比如修复了多个子应用路由跳转时事件重名的问题(给每个子应用分配独立的 eventName)、修复了 innerWidth/innerHeight 在 resize 时不更新的问题。如果用 npm 包,每次修 bug 要等官方发版,改源码可以快速迭代。
Q2: WujieSubApp 通用组件封装
面试官可能问:"封装的 WujieSubApp 组件做了哪些能力?"
回答:
把 Wujie 的接入封装成了一个 <WujieSubApp> 通用组件,任何子应用只需一个标签就能挂载。做了以下几层能力:
1. 动态入口配置:支持多种方式指定子应用入口路由:
entryRouteprop 直接传入- 路由
query.to参数透传(如?to=dashboard/overview) - 兜底默认
/ - 子应用切换后可通过
isRemoveQueryAfterJump清除 URL 上的 query 参数,保持 URL 干净
2. 多环境自动适配:entryHost 优先取 prop,没有则取环境变量。生产构建时自动拼接子应用名称 + 环境后缀(如 /sub-app-name/prod),开发环境不拼接,通过 Vite proxy 转发。
3. 优雅降级:三层降级策略:
- 手动降级:设置面板里开关控制,切换后全局子应用降级为 iframe 模式
Proxy检测:浏览器不支持window.Proxy时自动降级CustomElementRegistry检测:不支持 WebComponent 时自动降级- 降级到 iframe 后标签页的
v-show保活机制依然生效
4. 用户信息透传:把主应用的 username、currentRole、roles、accessToken 注入到子应用的 window.$wujie.props.userParams 里,子应用不用重复登录。
5. 路由跳转桥接:主应用把 router.push 封装成 jump 函数注入子应用,子应用通过 window.$wujie.props.jump('/xxx') 就能跳转到主应用路由,不用关心跨应用路由细节。
追问:"为什么每个子应用要有独立的 eventName?"
这是 Wujie 的一个坑。如果所有子应用共用同一个 bus 事件名(如 routeChange),A 子应用触发跳转时 B 子应用也会收到事件并尝试跳转,导致 B 子应用报错或跳到错误路由。所以给每个子应用分配独立的 eventName = ${name}RouteChange,子应用在 serverConfig.json 里配置对应的 eventBusPrefix。
追问:"子应用开发环境入口怎么处理的?"
子应用在本地开发时跑在 localhost:xxxx,通过 Vite 的 proxy 配置转发。环境变量 VITE_SUB_APP_HOST 设为子应用的开发地址,Wujie 加载子应用时走这个地址,跨域问题由 Vite proxy 解决。生产环境子应用被打包部署到同一域名下的不同子目录,Wujie 直接 fetch HTML 不需要跨域。
Q3: Wujie Bus 主子应用通信
面试官可能问:"主子应用之间怎么通信的?"
回答:
通过 Wujie 的 bus 事件总线实现主子应用双向通信。主要传递以下信息:
主题同步:
- 暗黑模式切换 → 主应用
bus.$emit('toggle-sub-dark-mode', isDark)→ 所有子应用收到后同步切换 - 主题色切换 → 主应用
bus.$emit('set-sub-theme-color', { color, themeColor })→ 子应用同步更新 Element Plus 主题色 - 全屏切换 → 主应用
bus.$emit('main-toggle-fullscreen', isFullscreen)→ 子应用同步全屏状态
路由跳转:
- 主应用通过
bus.$emit('${subAppName}RouteChange', route)通知子应用跳转到指定路由 - 子应用通过
window.$wujie.props.jump(route)反过来让主应用跳转
设计要点:
- 事件命名空间隔离:每个子应用有独立的
eventName,防止事件串扰 - 主题同步的时序:暗黑模式切换时用
setTimeout(() => bus.$emit(...), 0)把 emit 放到微任务队列,确保startViewTransition动画先拿到更新后的 DOM 再通知子应用 - 子应用未激活时的处理:如果子应用还没加载就触发了 bus 事件,Wujie 会打印"事件订阅数量为空"的警告。解决方案是子应用激活后在 store 里设置 flag,主应用判断 flag 存在才触发
追问:"为什么不用 postMessage 而是用 bus?"
Wujie 的 bus 底层也是基于 postMessage 封装的,但它提供了事件化的 API($emit/$on/$off),用起来更像 Vue 的事件总线。而且 bus 对 iframe 的 contentWindow 做了封装,不需要手动获取 iframe 引用再 postMessage。但如果子应用不是通过 Wujie 接入的(比如独立部署的外部 iframe),就需要用原生 postMessage。
Q4: RBAC 动态权限方案
面试官可能问:"权限控制是怎么设计的?菜单权限和按钮权限分别怎么做的?"
回答:
三级权限体系:菜单级 → 路由级 → 按钮级。
菜单级(后端):
- 后端基于 RBAC 模型,每个用户绑定一个或多个角色,每个角色有对应的菜单权限
- 前端调
/getAsyncRoutes接口,后端根据当前用户的currentRole.key返回该角色可见的路由树 - 前端把路由树通过
addAsyncRoutes动态注入 Vue Router →handleWholeMenus更新菜单渲染数据
路由级(前端双重校验):
- 菜单过滤:
filterNoPermissionTree会从useUserStore().currentRole.key取当前角色,和路由的meta.roles数组做交集匹配——没有权限的路由不出现在菜单里 - 路由守卫拦截:
router.beforeEach里判断to.meta.roles是否包含当前角色 → 不包含就重定向到 403 页面。即使有人手动输入 URL,也进不了无权限的页面
按钮级:
- 后端返回路由数据时,在
meta.auths里附带该页面允许的按钮权限码,如['btn.add', 'btn.edit', 'btn.delete'] - 前端封装
v-auth自定义指令:
// directives/auth/index.ts
export const auth: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
if (value) {
!hasAuth(value) && el.parentNode?.removeChild(el);
}
},
};- 用法:
<el-button v-auth="['btn.delete']">删除</el-button>——没有权限时直接从 DOM 移除该按钮 hasAuth函数从router.currentRoute.value.meta.auths取当前路由的按钮权限列表,精确匹配
追问:"角色切换后怎么让权限立即生效?"
角色切换时调用 useUserStore().switchRole() 告诉后端切换了角色 → 后端返回新角色的路由树 → 前端 resetRouter() 清掉旧动态路由 → initRouter() 重新加载 → 权限即时生效。同时 X-User-Info 请求头里的 currentRole 也同步更新,后端 API 也能精确识别新角色。
追问:"同一用户多角色怎么处理?"
用户登录后调 getUserRoleList 拿到所有角色列表,在顶部导航栏的角色切换器里展示。切换角色时前端切换 currentRole,后端同步切换。当前选中的角色存 sessionStorage,刷新页面保持。如果发现当前角色不在最新角色列表中(可能被管理员撤销了该角色),自动回退到第一个可用角色并提示用户 7 秒后刷新。
Q5: Token 无感刷新机制
面试官可能问:"Token 过期了怎么做到用户无感知刷新?"
回答:
在 Axios 拦截器里实现了完整的 Token 无感刷新 + 失败请求重试队列。
Token 存储方案:
accessToken(短期,如 2 小时)+refreshToken(长期,如 30 天)accessToken和expires存在 Cookie 里(随过期时间自动销毁)refreshToken和用户信息存在 sessionStorage 里(关闭浏览器销毁)
请求拦截器:
- 请求白名单放行(
/login、/refreshToken、/getUserInfo,以及排除项loginLogs) - 不在白名单的请求 → 检查
accessToken是否过期(expires - Date.now() <= 0) - 过期 → 判断是否正在刷新中(
isRefreshing锁):- 不在刷新中:调
handRefreshToken({ refreshToken })→ 拿到新 token → 更新请求头 → 依次执行等待队列中的回调 - 正在刷新中:
retryOriginalRequest(config)把当前请求加入队列,等刷新完成后统一重发
- 不在刷新中:调
响应拦截器:
- 处理 401 状态码:如果拦截器里没兜住的请求返回 401,重新走一遍刷新逻辑
- 刷新失败或没有 refreshToken → 提示"登录已过期" → 执行登出
追问:"为什么用 Cookie + sessionStorage 而不是全放 Cookie?"
Cookie 有大小限制(4KB),用户信息和角色列表数据量大。而且 Cookie 会自动附带在每个请求上,角色信息没必要每次都发。sessionStorage 只在当前标签页有效,切换标签页不共享,更安全。accessToken 放 Cookie 是因为它需要在每次请求中携带(通过 Authorization: Bearer xxx 头),读取方便。
追问:"X-User-Info 请求头是干什么的?"
每次请求自动带上当前选中的角色 currentRole.key,后端可以精准知道请求来自哪个角色的用户。SSO 登录进来时 currentRole === "unknown",拦截器会在下次请求时自动从 sessionStorage 补全这个值,确保后端能正确识别。
Q6: 主题系统与 ViewTransition
面试官可能问:"主题系统怎么设计的?暗黑模式切换动画怎么做的?"
回答:
主题架构:
- 10 套预设主题色:道奇蓝(默认)、亮白色、猩红色、橙红色、金色、绿宝石、酸橙绿、深粉色、深紫罗兰色、科技绿色
- 暗黑模式:独立开关,日/夜间切换
- 3 种布局模式:垂直(vertical,侧边栏在左边)、水平(horizontal,顶部导航栏)、混合(mix,大屏侧边栏 + 小屏可折叠)
- 辅助功能:灰色模式(色盲友好)、色弱模式
Element Plus 主题色注入: setEpThemeColor(color) 通过 CSS 变量注入,动态设置 --el-color-primary-light-1 到 light-9(9 级浅色变体)和 --el-color-primary-dark-1/-2(2 级深色变体)。暗黑模式下用 darken 函数生成深色变体,日间用 lighten 函数。通过 @pureadmin/theme 的 toggleTheme 按 scopeName: layout-theme-${theme} 加载对应主题的 CSS。
ViewTransition 动画: 暗黑模式切换用了 Chrome 的 startViewTransition API,实现从点击位置扩散的圆形裁剪动画:
const transition = document.startViewTransition(letsSwitch);
transition.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
];
document.documentElement.animate(
{
clipPath: dataTheme.value ? clipPath : clipPath.reverse(),
},
{
duration: 700,
easing: "cubic-bezier(0.645, 0.045, 0.355, 1)",
pseudoElement: dataTheme.value
? "::view-transition-new(root)"
: "::view-transition-old(root)",
},
);
});以鼠标点击位置为圆心,向外扩散切换,700ms 动画。
追问:"主题切换后子应用怎么同步?"
通过 Wujie Bus 实时同步:
- 暗黑模式 →
bus.$emit('toggle-sub-dark-mode', isDark) - 主题色 →
bus.$emit('set-sub-theme-color', { color, themeColor }) - 全屏 →
bus.$emit('main-toggle-fullscreen', isFullscreen)
三个事件确保了主应用和 6+ 个子应用的视觉一致性。
追问:"为什么不支持亮白色在暗黑模式下?"
亮白色主题在暗黑模式下会导致文字和背景都变白,完全不可用。所以 showThemeColors 里直接过滤掉 themeColor === 'light' && isDark.value 的选项,从源头防止用户选错。
Q7: 标签页管理引擎
面试官可能问:"标签页怎么管理的?右键菜单有哪些功能?"
回答:
标签页数据流:
- 标签页数据存
useMultiTagsStore(Pinia Store),响应式管理 - 路由跳转时
router.beforeEach+handleTags('push')自动添加标签 - 开闭/刷新标签时通过
handleTags('splice')删除
右键菜单(8 项):
- 重新加载:销毁 keepAlive 缓存 → 重新渲染(先
delete再add,间隔 100ms) - 关闭当前标签页:从
multiTagssplice 掉,自动跳转到右侧或左侧相邻标签页 - 关闭左侧标签页:批量 splice 左侧
- 关闭右侧标签页:批量 splice 右侧
- 关闭其他标签页:保留当前 + 首页,删掉其余
- 关闭全部标签页:清空到只剩首页
- 整体页面全屏:浏览器 Fullscreen API
- 内容区全屏:隐藏侧边栏
pureSetting.hiddenSideBartoggle
标签页缓存(keepAlive): keepAlive 只支持二级缓存,所以路由在初始化时通过 formatTwoStageRoutes 把三级及以上路由全部拍成二级结构。cachePageList 管哪些页面要被缓存,切换标签页时自动添加/删除。
追问:"动态路由可打开的最大数量 dynamicLevel 是什么?"
有些业务场景里,同一个路由(如订单详情)可能打开多个标签页(不同订单号)。dynamicLevel 限制同一路径最多同时打开几个标签页——超过上限时替换最早打开的那个,防止用户无节制打开几十个标签页导致内存爆炸。
追问:"标签页持久化怎么做的?"
multiTagsCache 开启后,标签页数据存到 LocalStorage(${responsiveStorageNameSpace()}tags)。下次打开浏览器时自动恢复上次的标签页。关闭持久化后直接清除 LocalStorage 里的标签数据。但这个选项风险较大——如果标签页对应的路由权限变了,恢复时会出问题,所以默认关闭。
Q8: iframe 保活与性能优化
面试官可能问:"iframe 标签页怎么避免重复加载?"
回答:
核心策略:v-show 替代 v-if。
传统的 iframe 标签页切换用 v-if,每次切回来 iframe 都重新创建 DOM → 重新加载 HTML → 重新执行 JS,性能极差。这个项目里改用 v-show——所有打开的 iframe 的 DOM 一直存在,只是 display: none 隐藏。切标签页时瞬间显示,不需要重新加载,性能提升 70%。
激活列表管理缓存生命周期:
frameView.vue 组件内部维护激活状态。关闭标签页时 splice 触发 emitter.emit('clearIframeKeepAliveCache', { path }),appMain 里通过 v-show 控制显隐。emitter(基于 mitt 的轻量事件总线)负责跨组件通信——关闭标签页 → 通知 appMain 隐藏对应 iframe。
追问:"iframe 内存会不会越来越大?"
确实会有这个问题。所以关闭标签页时会 emit clearIframeKeepAliveCache 事件,appMain 收到后彻底移除该 iframe 的 DOM,不再保留在 DOM 树中。用户主动关闭的标签页会释放内存。只有当前已打开的所有标签页对应的 iframe 才保持在 DOM 中,不会无限增长。
追问:"如果是 Wujie 子应用而不是 iframe 呢?"
Wujie 子应用本身就有 alive 保活机制,切标签页时 Wujie 内部不会销毁 iframe 沙箱。但关闭标签页时需要通过 refreshApp(name) 清除 Wujie 沙箱缓存(idToSandboxMap.clear()),释放内存。
Q9: 沙箱缓存管理
面试官可能问:"怎么管理 Wujie 子应用的缓存?"
回答:
Wujie 用 idToSandboxMap(一个 Map 对象)存储已激活子应用的沙箱数据。项目里封装了两个工具函数来精确管理缓存:
refreshApp(name) 精确清除:
const SUB_FRAME = document.querySelector(`iframe[name=${name}]`);
const SUB_IDMAP = SUB_FRAME.contentWindow.__WUJIE?.inject?.idToSandboxMap;
SUB_IDMAP.clear();通过 name 属性找到子应用对应的 iframe → 进入它的 contentWindow → 拿到 Wujie 注入的沙箱 Map → clear() 清空。精确清除单个子应用缓存,不需要整页刷新。
refreshAllApp() 全部清除:
const ALL_SUB_IFRAME = document.querySelectorAll("iframe[data-wujie-flag]");
ALL_SUB_IFRAME.forEach((v) =>
v.contentWindow.__WUJIE?.inject?.idToSandboxMap?.clear(),
);通过 data-wujie-flag 属性选中所有 Wujie 管理的 iframe → 逐个清空沙箱缓存。
使用场景:
- 切换降级/正常模式时自动调用
refreshAllApp() - 子应用出现异常(白屏、样式错乱)时手动调用
refreshApp() - 用户登出时通过
window.location.reload()全量清理
追问:"Wujie 没有内置的 refreshApp 方法吗?"
没有。Wujie 官方没有暴露清除单个子应用沙箱的 API——要么全杀(window.location.reload()),要么不管。所以自己封装了这个能力,填补了生态空白。
Q10: 单点登录(SSO)
面试官可能问:"单点登录是怎么实现的?"
回答:
基于 URL 参数的前端 SSO 方案。其他系统跳转到主应用时带上三个必需参数:username、roles、accessToken。
核心流程(src/utils/sso.ts):
- IIFE 自执行:页面加载时立即执行,解析 URL 参数
- 参数校验:
getQueryMap(location.href)提取所有 query 参数 → 校验是否包含must = ['username', 'roles', 'accessToken']三个必需参数 → 三个都满足才判定为 SSO 请求 - 获取用户信息:用
accessToken调/getUserInfo补全用户详情(头像、昵称等) - 存储登录态:
setToken(params)把 Token 和用户信息存到 Cookie + sessionStorage - 清理 URL:
handleSingleSiteOn()删除 URL 上的敏感参数,用window.location.replace(newUrl)替换历史记录——用户按"后退"不会回到带 Token 的 URL,避免 Token 泄漏
追问:"怎么防止误触发 SSO?"
严格的参数校验——必须是 3 个参数且全都是 must 列表里的值,缺一个都不算 SSO。防止普通页面跳转(如带了其他业务参数的 URL)被误判为 SSO 导致无限循环。
追问:"SSO 后 currentRole 是 unknown 怎么办?"
SSO 进来的用户,setToken 只存了基本的 username/roles/accessToken,currentRole 默认取 roles[0]。但 Axios 拦截器的 X-User-Info 头初始化为 "unknown"。在下一次发请求时,拦截器检测到 "unknown" → 自动从 sessionStorage 补全 currentRole.key → 更新 X-User-Info 请求头。用户完全无感知。
Q11: Wujie 插件与兼容性修复
面试官可能问:"项目中做了哪些 Wujie 的 bug 修复和兼容性处理?"
回答:
在 plugins.ts 里集成了大量修复插件,解决 Wujie 环境下各第三方库的兼容问题:
1. Element Plus 控制台 warn 修复: Wujie 环境下 Element Plus 的 instanceof 检查会失败(iframe 的 Window 和主应用的 Window 是两个不同的上下文)。引入 InstanceofPlugin 修复。
2. Vite4 子应用样式丢失: Wujie 处理 <style> 标签时,Vite4 的样式插入逻辑和 Wujie 的 shadow DOM 有冲突。通过 patchElementHook 拦截 <style> 的 insertAdjacentElement,重定向到 iframe 的 document.head.appendChild。
3. innerWidth/innerHeight 修复: Wujie 只在初始化时同步一次 innerWidth/innerHeight,后续主应用窗口 resize 时子应用不会更新。通过 jsBeforeLoaders 重写 innerWidth/innerHeight 的 getter,让它们始终返回 parent[prop](即主应用的当前值)。
4. wangEditor 兼容: wangEditor 在 Wujie 环境下检查 Selection/DataTransfer 会失败。通过 jsBeforeLoaders 重定义这两个属性——降级模式用 iframeWindow 的值,正常模式用主应用的值。同时通过 jsLoader 按行替换 wangEditor 源码中的 instanceof 和 isCollapsed 检查。
5. popper.js 定位异常: Element Plus 的 Popover/Dropdown 等组件基于 popper.js,在 Wujie 的 shadow DOM 里绝对定位会偏移。通过 cssBeforeLoaders 强制 body { position: relative !important; } 修正。
6. 其余 polyfill:DocFullscreenPlugin、EventTargetPlugin、LocationHrefPlugin、WindowSizePlugin、WindowMessagePlugin、SelectionPlugin ——全部是 wujie-polyfill 官方提供的补丁。
追问:"这些插件哪些是主动降级时不需要的?"
InstanceofPlugin、DocFullscreenPlugin、EventTargetPlugin、LocationHrefPlugin、WindowSizePlugin、WindowMessagePlugin、SelectionPlugin 这 7 个在降级为 iframe 模式后不需要——因为 iframe 本身就是完整的浏览器环境。代码里通过 !props.degrade && !defaultDegrade && 条件判断,降级时自动跳过这些插件,减少不必要的开销。
Q12: 多环境部署与动态配置
面试官可能问:"多环境部署和配置怎么管理的?"
回答:
多环境构建:5 种构建模式 —— development(本地)、test(测试)、production(正式)、demo(演示)、trial(试用)。Vite 通过 --mode 参数控制,每种 mode 有不同的环境变量。
API 地址适配(src/api/utils.ts): baseUrlApi(url) 函数根据 process.env.NODE_ENV 返回不同环境的 API 前缀:
development→http://192.168.2.112:8163/test-apitest→https://emi.goomaker.com/test-apiproduction→https://emi.goomaker.com/prod-apidemo/trial同理
局域网 IP 管理:开发时后端 API 可能跑在不同同事的电脑上。把 IP 地址抽到 ip.json 里(已 gitignore),每个人配置自己的 { "local": "192.168.x.x" },baseUrlApi 里 http://${ip.local}:8848 动态拼接。不需要每次切后端就改代码。
动态全局配置(public/serverConfig.json): 项目启动时 getServerConfig() 通过 axios 拉取 serverConfig.json,合并到 app.config.globalProperties.$config 上。配置项包括:
Title:页面标题Layout:默认布局模式Theme:默认主题色DarkMode:默认暗黑模式SidebarStatus:侧边栏默认展开/折叠EpThemeColor:默认 Element Plus 主题色FixedHeader:固定头部Grey/Weak:灰度/色弱模式ShowLogo/ShowModel/MultiTagsCache等
所有配置都可以通过部署时替换 serverConfig.json 来动态调整,不用重新构建。
追问:"子应用的多环境怎么对齐?"
子应用入口 HOST 也按环境拼接:生产环境子应用部署在 emi.goomaker.com/${subAppName}/prod/,测试环境 .../${subAppName}/test/。WujieSubApp 组件的 entryHost 属性自动拼接 ${props.name}/${env} 后缀。子应用需要独立打包对应环境的静态文件。
行为面试常见题快速答
"为什么做这个项目?"
公司的业务系统越来越多——数字化供应链、浆染化料系统、数据中心、自动称料大屏、VSCM 供应链、回潮率检测仪、水份检测仪、越南财务系统……每个子系统是独立的 Vue 项目,各自有登录、各自有权限、各自有主题。用户需要打开多个标签页分别登录,体验极差。老板要求做一个统一门户,把所有子系统整合到一个平台里。
我从零搭建这套基于 Wujie 微前端架构的中台系统,实现了单点登录、统一权限、统一主题、跨系统通信,把 6+ 个独立子系统整合为一个协同管控平台。
"遇到的最大难点?"
Wujie 源码 bug 修复:Wujie 当时版本还比较早期,实际落到项目里暴露了很多问题——多子应用路由事件串扰、innerWidth 不更新、Vite4 样式丢失、popper.js 定位异常……很多问题在官方 issue 里都没解决,只能自己 Fork 源码逐行调试修复。印象最深的是多子应用
routeChange事件串扰,排查了两天才发现是事件名命名空间的问题,最后给每个子应用分配独立的eventName解决。Token 刷新 + 请求队列:无感刷新 Token 的经典难题——多个请求同时发现 Token 过期,如何让它们只触发一次刷新而不是多次?加了
isRefreshing锁 +requests队列,后面来的请求先入队,等刷新完成后统一重发。响应拦截器里 401 还要兜底重试。这个逻辑调了好几版才稳定。Wujie 沙箱环境和 iframe 降级的兼容矩阵:不同插件在"正常/降级"两种模式下有不同的表现——有的需要跳过,有的需要调整逻辑。16 个插件的兼容矩阵调试非常繁琐。
"有什么遗憾?"
- Wujie Fork 的版本维护成本高——官方更新后要手动 merge,容易产生冲突。后续应该把通用修复提 PR 到上游。
- 标签页持久化和动态路由存在潜在冲突——如果标签页对应的菜单权限被撤销,恢复标签页会出现 403。目前只是建议用户关闭持久化,没有彻底的解决方案。
- 子应用的打包部署流程没有统一自动化——每个子应用需要单独打包、单独部署到对应目录,容易出错。应该用 CI/CD 统一管理。
"跟其他项目有什么关联?"
这个平台是公司智能制造产品线的集成入口。EMI 智慧 SOP 看板、EMI 数字孪生平台、智能回潮率检测仪系统都是通过这个平台接入的。技术栈和工程化体系是统一的——Vue3 + TypeScript + Vite + Pinia + Element Plus。我在这个项目中建立的 Wujie 微前端方案和权限体系,后续所有子系统都在复用。
"这个项目有多少人参与?"
前端是我一个人独立负责架构和开发,后端有 2 个 Java 同事配合。子应用由不同的业务团队分别开发,我负责制定子应用接入规范、提供 WujieSubApp 组件和开发文档。整个项目从需求分析、技术选型、架构设计到核心开发、联调部署都是我在主导。
上一篇:06-EMI数字孪生平台 下一篇:08-EMI云数据平台(数据大屏)