前后台共用页面这件事几乎每个做 Web 开发的团队都会撞上。商城前台要展示订单详情后台运营也要看同一个订单内容站点前台要渲染文章详情后台编辑也要预览同一篇文章。做两套视觉和交互稍微一漂移就变成两套完全不相关的东西硬塞在一起后台的低频表格和前台的高流量页面又互相拖累。这篇文章我就用实际做过的项目来拆一拆前后台要共用页面核心难点到底在哪以及怎么设计出一套能真正“共享”而不是“共用个名字”的开发方案。先说结论共享的关键不是把两套代码硬合并而是把“可变的部分”和“不可变的部分”干净地切分开。你真正想共享的是那套业务逻辑和核心交互而不是布局、皮肤、权限边界这些天然不同的东西。搞清楚这个主次后面所有的技术选型都顺理成章。围观的读者里如果你正在为独立的后台管理端和前台电商站点反复改同一块业务代码或者刚接手一个“前后台不分离的老项目”想重构这篇文章值得看完。我会把方案拆成四条路线讲清楚各自的取舍再把实操过程中最容易翻车的细节拎出来说。1. 先搞清楚一个核心问题什么页面真的该共享动手写代码之前我建议你先花半天把所有页面过一遍给它们打标签前台独有、后台独有、两边共用。这一步很多人偷懒跳过结果就是把一堆其实完全不一样的页面硬凑在一起然后为了兼容互相妥协最后谁都用得不痛快。1.1 适合共享的典型场景我做过一个电商项目最典型的共享页面是订单详情。前台用户要看自己的订单后台客服要看某个用户反馈过来的订单信息本质上是一样的商品列表、金额明细、物流信息、售后状态。差异只在于入口、操作按钮和展示密度。还有一类是内容预览页。运营在后台编辑商品图文详情或者活动页面保存之后需要立刻看真实效果。这种情况下后台内嵌一个前台同款渲染组件比后台自己在 iframe 里随便做个预览框要靠谱得多。因为渲染逻辑只有一个副本所见即所得是真的“所得”。我把适合共享的页面特征整理成了三条判断标准数据模型和状态流转完全一致比如订单、工单、内容详情前后台操作的是同一份数据核心交互模式一致比如表单填写、列表筛选、详情查看只是入口和权限不同视觉差异可以通过配置层收敛而不是需要彻底重写交互结构比如前台做卡片流、后台做表格流但单条商品信息的展示元素可以复用。满足这三条的页面才有共享的价值。反过来如果判断标准里命中了两条否定项比如数据都不一致、交互完全不同那说明这页面只是“长得有点像”硬共享是给自己挖坑。1.2 打死也不要共享的部分共享不是万能的。有些东西看着能省事实际上会变成巨型耦合点我挨个说。登录态与权限模型不能共享。前台用户的登录凭证、后台管理员的登录凭证通常走两套身份体系就算都叫 token有效期、刷新策略、权限粒度都不一样。强行共用一个认证上下文你会陷入“这个按钮前台用户到底能不能看到”的永无休止的争论中。后台专属的低频操作组件不能共享。比如批量改价、审核流、权限分配表格这些组件天然绑定后台的交互范式前台根本不会用。把它们塞进共享组件库里只会让前台 bundle 体积变大首屏速度被拖累。布局和导航必须分开。前台有顶栏、底部、侧边栏商品分类后台有左侧菜单、面包屑、工作台。这些属于“外层结构”如果你把这个也共享了等于把两套完全不同的信息架构绑在一起改任何一个都会影响另一个。提示一个实操时很有用的判断法——把页面截图给一个不了解项目的人看如果他一秒内能说出“这是前台还是后台”那说明这页面不适合大范围共享如果他自己都分不清那才是共享的好候选。2. 四条主流路线各自的取舍和适配场景确定了哪些页面要共享接下来就是技术路线问题。下面这四条路线我都实际用过也见过别的团队用各有各的脾气。2.1 路线一抽独立组件库用 npm 包分发这是最朴素也最容易上手的方案把订单详情、商品卡片、表单校验这些跨端复用的东西抽到一个独立的组件库项目里发布到私有 npm 仓库前台和后台各自安装引用。优点非常直观代码只有一个维护源前后台各自的版本号可以独立升级互不阻塞。我用这个方案做过一个商品卡片的共享封装前台展示商品用后台选商品弹窗也用效果很干净。但这方案有个硬伤组件库和页面之间始终隔着一层抽象一旦页面复杂到一定程度比如订单详情里有十几块区块联动你会发现组件库为了迎合两边的不同需求props 越加越多没过几个月就膨胀成一个什么都管、什么都管不好的“上帝组件”。所以要给组件库设一条红线从业务中提炼出来的纯 UI 和通用逻辑才放进去和具体页面绑定的状态拉取逻辑尽量留在各自应用里。2.2 路线二Monorepo 多包结构共享代码仓库Monorepo 这几年非常流行。它的核心是用一个仓库管理多个应用和多个共享包借助 Workspace 能力本地开发时直接引用源码省掉每次改共享包都要先构建再发版的流程。我后来的项目基本都选了 Monorepo。等于把前台站点、后台管理、共享 UI 包、共享业务逻辑包放在同一个代码仓库里各自独立构建部署本地联调时又是即时生效的。这方案最大的受益点是“改动跨包时非常顺滑”。比如我改了共享的订单详情核心逻辑前台和后台的引用方可以立刻在本地测到效果然后一起提测不会出现“npm 包装完了、后台还在等新版本”的尴尬。规划和分工做扎实之后Monorepo 是把“共享”这件事在工程层面落实得最彻底的方案。要注意的是Monorepo 不等于没有代价仓库体积会变大CI 如果没有做增量构建每次跑全量测试会等到怀疑人生团队如果没有统一的代码规范多包之间会迅速长出各种风格差异。工具链上流行的包管理方案都支持 Workspace登录和权限这块在两端的隔离我会在第 3 章里细讲。2.3 路线三微前端与模块联邦运行时共享如果你的前后台是两个独立部署的老应用历史包袱很重谁也不想把代码塞进同一个仓库那可以考虑微前端。其中模块联邦这一支方案能做到构建时互相不依赖、运行时按需加载共享模块。直观感受是后台界面依然跑在自己的应用里但订单详情模块是从前台应用运行时“借”过来的反过来前台也可以在某个 H5 活动页临时拉取后台的数据看板模块。两边各自的版本继续独立迭代共享部分通过联邦协议动态解析。这个方案灵活是灵活但复杂度也是四条路线里最高的。做模块联邦的前提是团队的工程化能力足够成熟因为排查一个跨应用的运行时报错难度通常比排查单应用内的问题大一个量级。依赖版本冲突、沙箱隔离不彻底、共享模块的通信协议设计全是细节活。我见过不止一个团队被微前端搞得线上问题频发所以如果你不是真的面临多团队并行维护独立应用的现实压力我不太建议一上来就上这个方案。2.4 路线四同一套页面代码按环境切换双入口这也是一条很实际的路线尤其适合页面数量不多、结构相对固定的场景。它不搞什么包管理直接在同一个应用里维护一套代码通过运行时环境标志走不同的布局、权限和数据源。比如订单详情页我用同一个 React 组件通过一个appType参数决定前台模式渲染用户侧的操作按钮和移动端风格后台模式加载管理员操作栏和桌面端密度。路由也是同一套路由表只是在构造路由时根据环境标记启用不同的一级布局。这条路线最大的好处是快非常适合“先跑起来再优化”的阶段。但反面问题也很明显随着条件分支变多代码会变得越来越不可预测。你永远不知道当前页面的某个样式是appType admin才生效还是user才生效测试也要每改一处跑两遍。当项目稍微上了规模我建议把这种模式作为过渡而不是终态。我把这四种方案的取舍整理成一张表方便你结合自身情况选型方案上手难度共享粒度隔离性适合场景npm 组件库低组件/函数高版本独立团队较小共享范围明确Monorepo中组件/页面/业务逻辑中仓库内统一前后台同团队维护需频繁联调模块联邦高整块模块/页面高应用级隔离多团队独立维护历史包袱重双入口最低整页低分支耦合原型期/小型项目过渡方案3. 实操环节从零搭一个可共享的订单详情页方案聊完我拿一个相对典型的 Monorepo 场景来走一遍完整实操。假设我们有三个包web前台商城、admin后台管理、shared共享包现在要做订单详情页。3.1 第一步划分共享边界决定哪些代码放到 shared动手前最忌讳的是“把整个页面代码拖进 shared”。我在真实项目里会把页面拆成三层数据层订单状态枚举、金额计算、状态流转判断、API 请求的封装这一层和 UI 完全无关是共享价值最高的部分组件层商品明细列表、金额明细区块、物流时间线、售后状态标签这些都是纯展示加少量交互适合做成受控组件放到共享包页面壳层整个订单详情页的布局结构、顶部操作栏、路由定义这一层差异最大留在各自应用里组装。我用共享包的目录结构举个例子shared/ ├── src/ │ ├── api/ // 订单接口封装 │ ├── domain/ // 状态枚举、金额计算、流转逻辑 │ ├── components/ // 商品明细、金额区块、时间线等展示组件 │ ├── hooks/ // 订单数据拉取、倒计时等通用 hooks │ └── index.ts ├── package.json注意一个边界细节数据层的 API 封装不要直接内置请求实例。我在早期的项目里直接在共享包里写死了请求地址和鉴权头结果前台和后台的鉴权方式不一样回调惨不忍睹。正确的做法是共享包只提供“传入请求函数返回封装好的接口方法”这种依赖注入模式。3.2 第二步设计组件接口契约用 props 避免脏逻辑共享组件最怕的就是一个组件没有清晰的边界什么都往里塞。我建议给每个共享组件定义好它的输入输出并且把判定规则定死组件内部只负责渲染和响应用户行为所有数据由外部传入。订单详情里最核心的商品明细列表设计成受控组件interface OrderItemListProps { items: OrderItem[]; renderActions?: (item: OrderItem) ReactNode; density?: comfortable | compact; }商品怎么展示是组件的事但“这条商品能不能退款、要不要显示退款按钮”这种业务判断由外部通过renderActions传入这样前台传退款入口后台传改价入口组件本身不用知道两边业务差异。这看起来是一件小事却是决定共享组件生命周期长短的关键。一旦允许组件内部直接发请求、直接判断权限它就会和某一个端的业务耦合下一次共享就变成噩梦。3.3 第三步做主题差异化一套结构两套皮视觉差异是共享页面躲不开的坎。比如前台的订单详情是移动优先、大按钮、卡片化后台要的是信息密度、紧凑表格。我的做法是共享组件统一用布局层和内容层分离的方式来处理主题。布局是指间距、尺寸、显示密度内容是指字段的展示顺序和重点。用 CSS 变量来定义主题 token// shared 组件内部只消费变量 .order-card { padding: var(--order-card-padding, 16px); border-radius: var(--order-card-radius, 8px); } // 前台应用设置 :root { --order-card-padding: 20px; --order-card-radius: 12px; } // 后台应用设置 :root { --order-card-padding: 8px; --order-card-radius: 2px; }这样前台和后台能各自调整视觉细节但组件本身只有一个实现。除了 CSS 变量另一个技巧是把“展示密度”做成组件的一个受控 prop比如densitycomfortable时显示大图、densitycompact时缩略图加小字。两种模式内部共用一套布局逻辑不是两套组件。我在实际项目里发现主题差异化这件事越往后越重要。最开始可能只是边框粗细不一样后面前台做一次大改版后台想保持不变如果你的主题层设计得不好就会被迫把共享组件拆回去一切打回原形。3.4 第四步权限差异放入口不放组件内部订单详情页前台用户只在“自己的订单”里查看后台客服可以看任意订单前台用户能申请售后后台客服能执行审核。这些都是权限差异处理不好就变成组件里到处写if (isAdmin)。我的原则是共享组件永远不做权限判断权限判断发生在各自应用的入口与数据请求层。前台在进入页面前拦截确保当前用户只能访问属于这个用户的订单后台在路由守卫里校验管理员权限然后把“有权限的数据”传给共享组件。共享组件拿到什么就渲染什么只遵循 props 约定。这个原则落到实处的好处是共享组件里一行isAdmin都不会出现前后台业务完全通过数据层和入口层隔离。将来就算后台的权限体系换了一套彻底重做的共享组件也完全不用动。代码上我给前后台各自的路由守卫留了配置位// web 端的路由配置 { path: /order/:id, element: OrderDetailPage /, loader: async ({ params }) { const order await getOrder(params.id, { scope: self }); return { order }; } } // admin 端的路由配置 { path: /orders/:id, element: OrderDetailPage /, loader: async ({ params }) { const order await getOrder(params.id, { scope: any }); return { order, enableAdminOps: true }; } }页面壳层组装也都是同一个OrderDetailPage但传给它的 props 里已经带了不同的操作权限数组和数据范围组件内部只需要统一渲染。3.5 第五步本地联调和构建部署的衔接Monorepo 里共享包的改动要能即时出现在前后台引用方必须依赖包管理工具的 Workspace 能力。在具体配置文件里声明共享包的依赖时直接指向 workspace:// web/package.json { dependencies: { shared: workspace:* } }这样本地开发时web直接引用的就是shared的源码改了不重构不发布马上生效。但要注意线上构建之前shared包需要先构建成产物否则发布到 CI 环境会因为缺构建文件而挂掉。我习惯在构建脚本里把顺序写清楚{ scripts: { build: npm run build:shared npm run build:web, build:shared: cd packages/shared npm run build, build:web: cd apps/web npm run build } }这一步看起来不起眼但实际线上发布翻车经常就翻在构建顺序上。4. 那些年踩过的坑前后台共享页面的高频事故这一章我特意放在后面写因为几乎每条都是靠“踩了坑花了时间排查最后骂骂咧咧修好”换来的。早点看到能省不少加班时间。4.1 配置了共享组件但两边路由冲突了第一个坑出现在路由和资源加载层面。如果共用组件但没共用路由表问题不大如果用了同一条路由路径那就出事了。我见过一个项目前台有个/order路由后台也有个/order路由同时在一个域名下加载时页面直接被空白接管控制台全是路由匹配警告。解决办法是要么前后台各自独立域名/独立应用路由永远互不感知要么在共享层用一个命名空间前缀后台路由统一加admin前缀。最怕的是“半共享”状态——路由表从一个文件导出两边各引一半冲突了谁也说不清是谁的责任。4.2 状态管理共享后数据串台的魔幻事故我做一个共享表单组件的时候图省事把 axios 实例和登录态都放在共享包里。结果后台管理员登录后切到前台页面前台居然带着后台的 token 去请求数据因为共享模块里缓存了最新的请求头。这个坑的教训就是共享代码可以共享业务逻辑但永远不要共享 IO 相关的有状态实例。请求实例、缓存实例、登录态存储必须在各自应用内初始化共享包只能接收实例作为参数。我在共享包里已经把数据请求都改成了依赖注入模式// shared 包内 export const createOrderApi (requestFn: RequestFn) ({ getOrder: (id: string) requestFn(/orders/${id}), updateOrder: (id: string, data: PartialOrder) requestFn(/orders/${id}, { method: PUT, body: data }), });前台传前台自己的请求函数后台传后台的两者互不污染。这个改动彻底解决了数据串台的问题也是我每次写共享包必遵守的基线。4.3 样式隔离做不到位后台被前台拖下水由于共享组件用了 CSS 变量和同一个类名体系前台临时的样式实验如果不小心改到了全局变量后台样式马上跟着变。有一次前台运营为了做大促活动给根节点加了红色主题后台页面当晚就变成红色主题客服系统直接“过年”。避免这个问题有两个办法。第一个是共享组件里尽量使用带作用域的样式方案比如 CSS Modules、CSS-in-JS。第二个是后台应用在根节点固定自己的主题变量并在挂载时强制指定一层作用域不让前台泄漏进来// admin 入口 ThemeProvider theme{adminTheme} App / /ThemeProvider这属于“自保式设计”共享组件本身不改但各端应用用一层壳把自己的样式边界划清楚。只要共享组件不写全局覆盖样式这个方案就能稳住。4.4 版本迭代带来回归测试翻倍共享组件改一次前台后台都要回归。一开始觉得共享省事后来发现每次改个文案都牵动两端测试成本翻倍。这个坑本质不是方案的问题而是“共享内容的粒度不合理”的问题。我把共享组件的回归策略定为共享组件的变更都要在独立的组件测试环境里过一遍并且制定一份跨端回归清单。比如订单详情我会在共享包内维护一套标准的 mock 数据前端后台共用同一份 mock 数据跑集成测试最大程度减少测试环境偏差。另外要养成一个好习惯共享包的破坏性变更必须显式提升主版本号并在变更日志里写明影响范围。否则后台团队升级依赖时根本不知道这个改动会影响自己的订单审核页线上出问题也就难以避免。4.5 共享组件变成“上帝组件”的失控过程这是最隐蔽的坑没有明显报错但项目会像温水煮青蛙一样变慢。线索是这样的某一天共享组件里多了一个可选 props负责处理一种只有后台才有的边缘情况再过一个月又多了一个 props处理另一种前台独有的样式。这个组件渐渐变成所有人都不敢往里加需求、更不敢动的“大泥球”。我自己对抗这个问题的办法是给共享包每个组件维护一个“支持能力清单”每加一个 props 必须有前后台两端的真实场景支撑不然就打回。如果发现一个 props 只为一个端服务优先考虑把它从组件里剥出去变成那个端自己的包装层。保持共享组件的纯粹性比多封装几行代码重要得多。5. 给不同情况的朋友一条实操建议聊到这里我有几句掏心窝的话。如果你是刚起步的项目前后台页面不多直接选双入口或者小范围 npm 组件库先把业务跑通最重要不要为了“共享”上一个复杂架构结果反而拖慢交付。如果项目已经成型前后台同团队维护Monorepo 是投入产出比最好的选择。它带来的共享体验是最完整的从数据层到组件层都能共用同时部署仍然是解耦的。如果你们是多个团队并行维护的老应用被迫要共享模块联邦可以解决燃眉之急但要在团队工程化能力足够的前提下再碰。别只看到它灵活的那一面要多想想跨应用的排障成本。我个人经验里最重要的一条是共享的是核心隔离的是边界。数据逻辑、核心组件可以大胆共享权限、路由、布局、请求实例这些边界一定要各自收好。边界划得越清晰共享就越省心边界模糊的时候才是项目复杂度暴涨的开始。最后再分享一个小技巧刚开始做共享页面前先把“共享范围”写成一页纸的文档把哪些组件共享、哪些路由独立、哪些数据域隔离列得明明白白然后贴到团队文档里。这个动作花不了半小时但能避免后面一半的争论和返工。等真出了问题再回来翻这篇文章你会感谢当初那个愿意提前花半小时把事情想清楚的决定。