Skip to content

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 有几点核心原因:

  1. 真正的技术栈无关:qiankun 要求子应用必须导出 bootstrap/mount/unmount 生命周期,意味着旧项目必须改造。Wujie 只需要一个 URL 就能接入,不管是 Vue2/Vue3/React/jQuery 都能直接加载,零改造成本。
  2. 双层隔离方案:Wujie 用 WebComponent 做样式隔离 + iframe 做 JS 沙箱隔离,不需要 qiankun 那种 eval + Proxy 形式,天然避免了 Function/eval 的兼容性问题。
  3. 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-corewujie-vue3 是直接 Fork 源码进来改的——比如修复了多个子应用路由跳转时事件重名的问题(给每个子应用分配独立的 eventName)、修复了 innerWidth/innerHeight 在 resize 时不更新的问题。如果用 npm 包,每次修 bug 要等官方发版,改源码可以快速迭代。

Q2: WujieSubApp 通用组件封装

面试官可能问:"封装的 WujieSubApp 组件做了哪些能力?"

回答

把 Wujie 的接入封装成了一个 <WujieSubApp> 通用组件,任何子应用只需一个标签就能挂载。做了以下几层能力:

1. 动态入口配置:支持多种方式指定子应用入口路由:

  • entryRoute prop 直接传入
  • 路由 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. 用户信息透传:把主应用的 usernamecurrentRolerolesaccessToken 注入到子应用的 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) 反过来让主应用跳转

设计要点

  1. 事件命名空间隔离:每个子应用有独立的 eventName,防止事件串扰
  2. 主题同步的时序:暗黑模式切换时用 setTimeout(() => bus.$emit(...), 0) 把 emit 放到微任务队列,确保 startViewTransition 动画先拿到更新后的 DOM 再通知子应用
  3. 子应用未激活时的处理:如果子应用还没加载就触发了 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 更新菜单渲染数据

路由级(前端双重校验)

  1. 菜单过滤filterNoPermissionTree 会从 useUserStore().currentRole.key 取当前角色,和路由的 meta.roles 数组做交集匹配——没有权限的路由不出现在菜单里
  2. 路由守卫拦截router.beforeEach 里判断 to.meta.roles 是否包含当前角色 → 不包含就重定向到 403 页面。即使有人手动输入 URL,也进不了无权限的页面

按钮级

  • 后端返回路由数据时,在 meta.auths 里附带该页面允许的按钮权限码,如 ['btn.add', 'btn.edit', 'btn.delete']
  • 前端封装 v-auth 自定义指令:
typescript
// 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 天)
  • accessTokenexpires 存在 Cookie 里(随过期时间自动销毁)
  • refreshToken 和用户信息存在 sessionStorage 里(关闭浏览器销毁)

请求拦截器

  1. 请求白名单放行(/login/refreshToken/getUserInfo,以及排除项 loginLogs
  2. 不在白名单的请求 → 检查 accessToken 是否过期(expires - Date.now() <= 0
  3. 过期 → 判断是否正在刷新中(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-1light-9(9 级浅色变体)和 --el-color-primary-dark-1/-2(2 级深色变体)。暗黑模式下用 darken 函数生成深色变体,日间用 lighten 函数。通过 @pureadmin/themetoggleThemescopeName: layout-theme-${theme} 加载对应主题的 CSS。

ViewTransition 动画: 暗黑模式切换用了 Chrome 的 startViewTransition API,实现从点击位置扩散的圆形裁剪动画:

typescript
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 项)

  1. 重新加载:销毁 keepAlive 缓存 → 重新渲染(先 deleteadd,间隔 100ms)
  2. 关闭当前标签页:从 multiTags splice 掉,自动跳转到右侧或左侧相邻标签页
  3. 关闭左侧标签页:批量 splice 左侧
  4. 关闭右侧标签页:批量 splice 右侧
  5. 关闭其他标签页:保留当前 + 首页,删掉其余
  6. 关闭全部标签页:清空到只剩首页
  7. 整体页面全屏:浏览器 Fullscreen API
  8. 内容区全屏:隐藏侧边栏 pureSetting.hiddenSideBar toggle

标签页缓存(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) 精确清除

typescript
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() 全部清除

typescript
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 方案。其他系统跳转到主应用时带上三个必需参数:usernamerolesaccessToken

核心流程(src/utils/sso.ts

  1. IIFE 自执行:页面加载时立即执行,解析 URL 参数
  2. 参数校验getQueryMap(location.href) 提取所有 query 参数 → 校验是否包含 must = ['username', 'roles', 'accessToken'] 三个必需参数 → 三个都满足才判定为 SSO 请求
  3. 获取用户信息:用 accessToken/getUserInfo 补全用户详情(头像、昵称等)
  4. 存储登录态setToken(params) 把 Token 和用户信息存到 Cookie + sessionStorage
  5. 清理 URLhandleSingleSiteOn() 删除 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 源码中的 instanceofisCollapsed 检查。

5. popper.js 定位异常: Element Plus 的 Popover/Dropdown 等组件基于 popper.js,在 Wujie 的 shadow DOM 里绝对定位会偏移。通过 cssBeforeLoaders 强制 body { position: relative !important; } 修正。

6. 其余 polyfillDocFullscreenPluginEventTargetPluginLocationHrefPluginWindowSizePluginWindowMessagePluginSelectionPlugin ——全部是 wujie-polyfill 官方提供的补丁。

追问:"这些插件哪些是主动降级时不需要的?"

InstanceofPluginDocFullscreenPluginEventTargetPluginLocationHrefPluginWindowSizePluginWindowMessagePluginSelectionPlugin 这 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 前缀:

  • developmenthttp://192.168.2.112:8163/test-api
  • testhttps://emi.goomaker.com/test-api
  • productionhttps://emi.goomaker.com/prod-api
  • demo / trial 同理

局域网 IP 管理:开发时后端 API 可能跑在不同同事的电脑上。把 IP 地址抽到 ip.json 里(已 gitignore),每个人配置自己的 { "local": "192.168.x.x" }baseUrlApihttp://${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+ 个独立子系统整合为一个协同管控平台。

"遇到的最大难点?"

  1. Wujie 源码 bug 修复:Wujie 当时版本还比较早期,实际落到项目里暴露了很多问题——多子应用路由事件串扰、innerWidth 不更新、Vite4 样式丢失、popper.js 定位异常……很多问题在官方 issue 里都没解决,只能自己 Fork 源码逐行调试修复。印象最深的是多子应用 routeChange 事件串扰,排查了两天才发现是事件名命名空间的问题,最后给每个子应用分配独立的 eventName 解决。

  2. Token 刷新 + 请求队列:无感刷新 Token 的经典难题——多个请求同时发现 Token 过期,如何让它们只触发一次刷新而不是多次?加了 isRefreshing 锁 + requests 队列,后面来的请求先入队,等刷新完成后统一重发。响应拦截器里 401 还要兜底重试。这个逻辑调了好几版才稳定。

  3. Wujie 沙箱环境和 iframe 降级的兼容矩阵:不同插件在"正常/降级"两种模式下有不同的表现——有的需要跳过,有的需要调整逻辑。16 个插件的兼容矩阵调试非常繁琐。

"有什么遗憾?"

  1. Wujie Fork 的版本维护成本高——官方更新后要手动 merge,容易产生冲突。后续应该把通用修复提 PR 到上游。
  2. 标签页持久化和动态路由存在潜在冲突——如果标签页对应的菜单权限被撤销,恢复标签页会出现 403。目前只是建议用户关闭持久化,没有彻底的解决方案。
  3. 子应用的打包部署流程没有统一自动化——每个子应用需要单独打包、单独部署到对应目录,容易出错。应该用 CI/CD 统一管理。

"跟其他项目有什么关联?"

这个平台是公司智能制造产品线的集成入口。EMI 智慧 SOP 看板、EMI 数字孪生平台、智能回潮率检测仪系统都是通过这个平台接入的。技术栈和工程化体系是统一的——Vue3 + TypeScript + Vite + Pinia + Element Plus。我在这个项目中建立的 Wujie 微前端方案和权限体系,后续所有子系统都在复用。

"这个项目有多少人参与?"

前端是我一个人独立负责架构和开发,后端有 2 个 Java 同事配合。子应用由不同的业务团队分别开发,我负责制定子应用接入规范、提供 WujieSubApp 组件和开发文档。整个项目从需求分析、技术选型、架构设计到核心开发、联调部署都是我在主导。

上一篇:06-EMI数字孪生平台 下一篇:08-EMI云数据平台(数据大屏)

前端开发工程师 · 面试准备