一个仓库,如何同时承载 PC 后台与移动端
“既要做 PC 后台,又要做移动端”,很容易把团队带向两个极端。
一种做法是复用同一批页面,再用响应式 CSS 勉强适配不同屏幕;另一种做法是拆成两个前端项目,把页面、请求和登录态都维护两遍。前者常常牺牲体验,后者则会让相同的业务规则在两个仓库里逐渐分叉。
在一个教务业务前端中,我们选择了中间路线:一个仓库、同一套认证与业务 API、两套独立的体验层。 桌面端使用 Element Plus,移动端使用 NutUI;两端共享请求、状态、权限和国际化等基础能力,但不试图共享页面布局。
这篇文章拆解一个更具体的问题:当 PC 后台和移动端都要长期演进时,怎样让共仓成为有边界的工程选择,而不只是一次偶然的目录安排?“把所有组件做成自适应”不在本文讨论范围内。
本文是「教务前端架构实践」系列的一篇。系列以一个已脱敏的 Vue 3 教务管理前端为背景,分别讨论双端体验、导航与权限、会话治理和复杂业务交互。
先明确边界:共享业务能力,不共享体验层
双端共仓最容易出现的误解,是认为同一个仓库就应该尽量复用 UI。实际上,后台表格、侧边导航和复杂表单,与手机上的工作台、顶部导航和触控输入,通常面对的是不同的使用环境。
这个项目把两端页面与共享基础设施分开维护:
- 桌面端页面位于
src/views,由src/layout提供后台布局壳; - 移动端页面位于
src/mobile,拥有自己的Login.vue、Layout和业务模块,路由以/mobile为前缀; - 请求拦截器与 Pinia 状态,以及权限、国际化和领域 API 仍在共享基础设施层中维护。
flowchart TB
S[共享基础设施<br/>请求 · 状态 · 权限 · 国际化 · API] --> P[PC 体验层]
S --> M[移动体验层]
P --> PL[桌面 Layout + src/views]
P --> EP[Element Plus]
M --> ML[Mobile Layout + src/mobile]
M --> NU[NutUI]
我们刻意把边界放在这里:同一份业务事实可以共享,怎么呈现则由终端决定。课程、审批任务和成绩数据仍然走同一套 API;到了 PC 端,它们可以进入表格和侧边栏,到了移动端,则要服从窄屏与触控的页面结构。
它更接近一个模块化单体,既不采用微前端,也不独立发布;共享成本较低,调用链较短,但两端仍处于同一依赖图和交付节奏中。
目录与 Layout 先把两套页面结构隔开
在构建规则介入之前,最基础的隔离来自目录与页面外壳。
桌面端的后台布局负责侧边栏、头部及主内容区和页签等结构;移动端则由独立的 Mobile Layout 承担顶部栏、返回入口、通知与菜单区域,并通过 router-view 渲染移动端子路由。移动端登录页也作为独立入口存在,不直接套用后台 Layout。
这个分法不复杂,但省掉了最麻烦的一件事:两端不必在同一个 DOM 结构里争夺样式和交互规则。
桌面端新增一个管理页面,开发者进入 src/views 和对应的后台布局约定;移动端增加一个工作台入口,开发者进入 src/mobile 和移动路由。目录不只是文件分类,还告诉团队“这里的尺寸、组件和导航假设是什么”。
路由也延续这一边界。移动路由集中在自己的模块中,以 /mobile 开头,并挂载到移动 Layout 下。这样,登录后的重定向、返回行为和页面缓存都可以按移动场景处理,不必让桌面端布局承担例外分支。
两套组件库可以共存,前提是导入与样式有明确规则
项目使用 unplugin-auto-import 自动引入 Vue、Vue Router 与 Pinia,以及 Element Plus 的 API;使用 unplugin-vue-components 根据模板组件名解析组件。
在组件解析阶段,Element Plus 使用官方 ElementPlusResolver,NutUI 则使用自定义的 NutUIResolver。前者识别 Element Plus 组件;后者识别以 Nut 开头的组件名,并同时生成对应的样式副作用导入路径。页面只需写出所用组件,Resolver 决定该从哪个组件库加载它及其样式。
1 | // 简化后的 Resolver 组合示意 |
这样做能减少重复的 import。业务页面声明自己用了什么组件,构建工具补齐组件和样式依赖;桌面页面不必反复手写 Element Plus 的导入,移动页面也不必在每个文件里维护 NutUI 样式清单。两套组件库的接入方式因此保持一致。
Element Plus 的主题变量还通过 Vite 的 SCSS additionalData 全局注入,所有 SCSS 文件都能访问统一的主题变量。但这份一致性只服务桌面端主题,移动端并不会因此复用桌面布局的样式。
这里有一个容易被忽略的边界:自动导入降低的是使用成本,不会自动替团队做架构判断。一个共享组件究竟应使用 Element Plus、NutUI,还是保持无 UI 库依赖,仍需由它服务的终端决定。
语言与依赖也应按共享边界组织
双端共享的还有国际化和构建产物策略。项目提供中文、英文、德文三套入口语言包,并按业务模块聚合文案;语言切换属于全局能力,不需要让两套体验层各自维护词条。构建时又把 Axios、Element Plus、图标、语言包等 8 类依赖显式拆入 manualChunks,让体积较大的共享依赖拥有稳定的缓存边界。
这里的原则和 UI 一样:能跨端复用的基础能力集中维护,但不要因为共仓就把所有资源打进同一个入口。语言包和依赖分包解决的是加载与缓存问题;页面层的体验隔离,仍然得靠目录、Layout 和构建规则来守。
一个实用的判断方式是拆开“数据与交互”来看。比如课程信息卡可以共享课程数据的类型、请求和状态处理;但若 PC 端需要在表格行内展示操作入口,移动端需要更大的触控区域和纵向信息层级,就应分别实现展示组件。只有当两端的 DOM 结构、交互方式和尺寸语义都相近时,复用无 UI 库依赖的基础组件才更划算。
真正的隔离点在构建期:只转换该转换的 px
双端共仓中,尺寸单位往往比组件逻辑更容易造成事故。
该项目在 Vite 中配置了 postcss-px-to-viewport:以 375 为基准视口,将移动端源码和 NutUI 包内的 px 转换为 vmin。相对地,Element Plus、桌面端的 src/views、桌面 Layout、通用组件、NProgress 与 index.html 被显式排除在转换范围外。
这条规则是一份构建期契约。
1 | // 简化后的策略示意 |
这份配置的效果很单纯:桌面端与 Element Plus 保持原有的像素语义,只有移动体验层按视口单位计算。
这一点比在全局开启单位转换更稳妥。全局转换看起来省事,但可能把后台表格、弹窗、图标或第三方组件意外缩放;编译通常不会失败,某个页面却会在特定宽度下悄悄错位。
不过,路径匹配也意味着这条边界依赖约定:移动页面被放进 src/mobile 之外的目录,就拿不到单位转换;本应保持桌面像素语义的组件落在未排除的位置,则会被错误转换。这类风险必须被显式管理。
共仓能省下什么,又会把什么问题留给团队
这套方案的优势比较直接:两端共享认证与请求错误处理、Token 生命周期、用户状态和领域 API,相同的业务规则不用维护两份实现;同时,桌面端与移动端仍能各自选择合适的导航、组件和布局。我们没有把“页面复用率”当成产品体验的目标。
不过,共仓只是把成本集中起来,并没有让它消失。
两端共享同一次依赖升级与发布节奏。NutUI 的组件结构变化可能要求同步维护自定义 Resolver;Element Plus 主题调整也需要维护相应的 SCSS 变量映射。更重要的是,目录本身是构建规则的一部分:新成员若不知道 src/mobile 的路径意味着单位转换,就可能在不知情时破坏视觉边界。
当然,不是随便两个端都适合放进同一个仓库。业务模型、认证方式和交付节奏高度一致,又确实需要独立 UI 体验,这样的产品才更适合共仓;如果两端已经需要独立团队、独立发布或完全不同的运行时约束,继续共仓未必是成本最低的选择。
把目录约定升级为可验证的约束
既然双端边界部分依赖路径规则,下一步就不该只靠口头约定。
如果继续维护这个仓库,我会优先补上三类约束,并让它们在 CI 中实际执行:
- 写清共享组件策略:明确共享组件是否允许携带终端尺寸、是否可以直接依赖某一个组件库,以及何时应拆成双端实现。
- 校验构建边界:检查移动页面是否仍位于
src/mobile,并核对 PostCSS 的 include/exclude 规则,避免新增目录后静默失去单位转换。 - 做视觉回归验证:为关键移动页面、桌面表格和跨端公共组件保留固定视口截图,在变更中比较截图差异,确保“构建成功”不被误认为“视觉正确”。
这些工作不会让双端共仓变得更简单;它们只是让已经存在的复杂度更早暴露出来。这个方案能否长期可靠,取决于团队能否把“文件放在哪里”从个人经验变成可检查的工程规则。
结语:共仓的目标不是统一 UI,而是控制重复
PC 与移动端的差异是真实存在的,强行统一 UI 往往会把差异藏进越来越多的条件分支里。这个项目的选择是承认两端体验层独立,同时把真正应该共享的基础能力保留在一个仓库内。
回头看,这条边界落在几处具体的工程安排上:目录决定页面归属,Layout 决定页面结构,组件 Resolver 分管两端的组件库,PostCSS 规则守住尺寸单位,而最后这道,正是最容易出事故的地方。
当边界被写进目录、配置和检查之后,一个仓库承载两套体验,既能控制重复,也能给两端留下各自演进的空间。
系列文章:大型业务前端里,路由为什么不只是导航|企业后台的登录态治理:如何处理 Token 刷新风暴与跨标签同步|从领域组件到考试入口:教务前端中的复杂交互抽象