本文主要介绍在「媒体内容产品 + 管理后台」混合型前端(以 Octans Web 为例)里,如何做 UI 基座长期选型:候选如何比、为何不再把强预设组件库当视觉底座,以及落地后的实际栈形态。结论面向同类 Vue 3 产品可复用,不绑定某一仓库路径。
适用场景是:既有海报墙、详情页、播放页这类内容型界面,又有高密度列表、批处理、设置页与用户治理这类控制台界面;技术栈大致为 Vue 3 + Vite,并希望主题与组件外观由自己主导。若项目只是标准 CRUD 中后台,权重会不同,不宜原样照搬。
产品形态约束
混合前端的难点不在「有没有好看的表格」,而在同一套 UI 基座要同时服务两种气质完全不同的页面。
以媒体库类产品为例,前端至少会同时承担:
| 页面族群 | 典型任务 | 对 UI 基座的要求 |
|---|---|---|
| 内容读取 | 海报墙、人物墙、详情页 | 深色优先、品牌感、大图与卡片节奏 |
| 播放 | 自定义 overlay、轨道菜单、侧栏设置 | 界面主导权在自己,不能被强预设壳绑死 |
| 库工作台 | 高密度列表、筛选、批处理 | 表格状态机、虚拟滚动、工具栏密度 |
| 控制台 | 设置、用户、库治理 | 表单、弹层、选择器、消息确认 |
因此选型不能只对「表单 + 表格」友好。至少要同时满足:
- 深色与自有主题是主路径,不是事后给 vendor 打补丁。
- 内容页 / 播放页要有自由度,看起来像内容产品,而不是企业后台皮肤。
- 后台能力仍要完整:表单、筛选、弹层、菜单、设置页不能缺胳膊少腿。
- 虚拟化是硬约束:海报墙与后台表都要能扛到十万级量级目标;测试库小不等于生产小。
- 数据权威在服务端:排序、筛选、分页由后端 API 完成;前端表格库只做状态机、渲染与虚拟化(manual / lazy 模式),不是把全量数据塞进浏览器再算。
还有一条常被低估的边界:Web UI 基座只负责把 Web 端做好。若多端路线是 Web 与 Android / Apple / Desktop 各自原生 UI,而不是「一个 Web 壳推全平台」,则 Quasar 一类「全应用壳 + 跨端运行时」的优势很难兑现,不应被高估。
最后是层级约束:页面层不应直接建立在 vendor 内部类名上;换一个强预设 styled suite,并不等于已经拥有设计系统。新基座必须允许形成项目自己的 ui/ 封装层,token 成为主题事实来源。
候选方向与评价维度
评价维度
本轮比较不考虑迁移成本,只按长期架构价值排序。权重可按产品自行调整,这里给出一组对混合型产品较稳的分配:
| 维度 | 权重(示意) | 关注点 |
|---|---|---|
| 自有主题控制权 | 高 | tokens、组件外观、视觉节奏是否由自己掌握 |
| 深色主题一等公民 | 高 | 暗色是主路径还是事后补丁 |
| 内容页与播放页自由度 | 高 | 海报墙 / 详情 / 播放是否像内容产品 |
| 后台能力完整度 | 高 | 表单、表格、筛选、弹层是否够用 |
| 与现有技术栈契合 | 中 | Vue 3、Vite、Tailwind 等是否顺手 |
| 代码整洁 / 低覆写压力 | 中 | 是否减少大规模 vendor DOM 覆写与 !important |
| 移动端窄屏适配 | 低~中 | 窄屏、触摸、响应式收敛 |
| 与全端矩阵一致性 | 中 | 是否符合「Web 做基线、原生各走平台 UI」 |
硬约束建议固定写死,避免选型讨论滑回「哪个库默认更好看」:
- 页面层不绑 vendor 内部类名。
- 不把「换一个 styled suite」误认为设计系统已完成。
- 播放页 UI 主导权留在自己(例如
hls.js+ 原生<video>+ 自有 overlay)。 - 必须允许形成自有
ui/边界。 - 表格运行在服务端权威(manual / lazy)模式下。
候选范围
常见候选可以分成几档来想:
| 路线 | 代表 | 一句话 |
|---|---|---|
| 强预设中后台库 | Element Plus、Vuetify、Arco Design Vue 等 | 控制台快,内容气质与主题主导权弱 |
| 现代工程工具库 | Naive UI 等 | 做控制台顺,内容页仍偏 suite 思路 |
| 全应用壳框架 | Quasar 等 | 壳与响应式强,战略价值依赖「整站围绕它组织」 |
| Headless + open-code | Reka UI + shadcn-vue 类 | 行为与可访问性在底层,外观落仓自有 |
| Unstyled 套件 + 现成 DataTable | PrimeVue Unstyled / Volt 等 | 主题可控,后台重组件面更完整 |
对「重组件」需求要先收敛,避免假分水岭:
- 磁盘路径浏览往往是「面包屑 + 当前层懒加载列表」,不是 Tree / Cascader 刚需。
- 真正硬的是虚拟化数据网格:虚拟化 grid(海报墙)+ 虚拟化 table(后台列表)。
因此第一档比较不宜只比「按钮和 Dialog 谁好看」,而应把 UI 原语 与 datagrid 内核 绑在一起看:
- Route A:
Reka UI(+ open-code recipe,如 shadcn-vue 思路)+@tanstack/vue-table+@tanstack/vue-virtual - Route B:
PrimeVueUnstyled / Volt + 其 DataTable 的 virtual scroller + lazy
页面适配度(定性)
| 候选 | 内容墙 / 详情 / 播放 | 控制台 / 列表 | 深色 + 自有主题 |
|---|---|---|---|
| Reka + open-code | 高 | 中高 | 高 |
| PrimeVue Unstyled / Volt | 高 | 高 | 高 |
| Naive UI | 中 | 高 | 中高 |
| Quasar | 中 | 高 | 中 |
| Element Plus 等强预设 | 低~中低 | 高 | 低~中 |
解读可以很直接:
- 第一档是 A / B 两条可维护路线,不是「有没有组件」的有无题。
- PrimeVue 路线的优势主要在现成虚拟化 DataTable;Reka 路线的优势在 open-code + 自有 datagrid 基础设施。
- Element Plus 继续擅长设置页与列表筛选,但不适合作为海报墙、详情页与播放页的长期视觉底座。
最终方向与理由
已落地主线
选型讨论之后,路线已经收敛并落地为当前运行时事实(表述为产品侧结论,不绑内部仓库路径):
| 层级 | 选择 | 职责 |
|---|---|---|
| 项目组件边界 | 自有 src/ui(或等价目录) | 原语、overlay、feedback、form foundation、data-grid、scroll-area、recipe |
| 行为与可访问性 | Reka UI | headless 交互、焦点、键盘、ARIA 等 |
| 数据网格 | TanStack Table + TanStack Virtual | 列状态、排序筛选状态机 + 虚拟化渲染 |
| 滚动区域 | OverlayScrollbars | 统一滚动条体验;原生滚动条 fallback 可清理 |
| 主题 | 自有 design token(如 --octans-*) | 唯一主题事实来源 |
Element Plus 不再是当前运行时依赖,也不再作为过渡期可借用组件集。下文写它,仅作历史背景:说明「为何离开 / 为何不作为长期主线」。
为何选 Route A,而不是对等的 Route B
两条第一档路线都合理,优先级取决于团队对 datagrid 归属 的态度:
更倾向 Route A(已选)时:
- 更看重 open-code、Tailwind-first、最低样式中间层。
- 希望组件源码落在自己仓库,而不是长期依赖第三方内部结构。
- 愿意把数据网格当成自己的长期基础设施维护,而不是完全交给套件。
- 表头交互、列状态、虚拟化策略是产品设计资产,要原样延续并继续演化,而不是寄生在现成 DataTable 骨架里。
- 若项目里已经有自建网格壳、路径浏览也不是 Tree 问题,A 与现状更贴近。
更倾向 Route B 时:
- 希望保留 code ownership,但对重组件更保守。
- 不想自己扛虚拟化 datagrid 底层复杂度,优先「现成 virtual scroller + lazy」。
- 在「完全自有主题」与「更完整的后台组件面」之间取更稳的平衡。
我们最终把 A 定为首选,而不是「A/B 并列第一」,核心不是 Reka「更轻」或「更好看」,而是共识偏向:
- 长期拥有自己的 datagrid 基础设施。
- 保留并演化自有表头交互、列状态与虚拟化策略。
- 避免页面层再绑定强预设成品组件库。
只有当目标明确改成「最短路径拿到现成十万级虚拟化表格」时,才应回切 B。
落地时需要同步钉死的几件事
选型文档若只写库名,落地时仍会漂。下面几条对 A 路线特别关键:
Datagrid 是基础设施,不是「某个 UI 库附带的表格」。
- 正式路线:
@tanstack/vue-table+@tanstack/vue-virtual。 - open-code 的 Data Table 样板可以当 V0 脚手架,不是长期边界。
- 样板默认常是客户端排序 / 筛选 / 分页;对齐服务端权威时,应在落仓时立即打开
manualPagination/manualSorting/manualFiltering,不要等接真实接口再改——否则早期「看起来能跑」会掩盖架构错位。
命令式全局服务不要一刀切成纯声明式。
历史上若已有大量 Message / MessageBox 式调用落在 request、router、store 等非组件上下文,全站改成纯 <Toast> / <Dialog> 组合,迁移成本与回归风险都不划算。更稳的做法是:
- 对外保留
toast.success()、confirm.open()一类命令式体验。 - 底层用自有 provider + headless 原语承接。
confirm返回Promise<boolean>;toast/ 全局loading返回可close()的句柄。
表单校验栈与 Form 原语要一起定。
若 recipe 默认围绕 vee-validate + zod,就直接定这条主线,避免三选一悬空。Form 字段注册、校验消息、aria-* 关联属于阻塞基建,不宜跳过。
图标与尺寸外壳要统一。
图标集可以继续用已有方案(例如 Phosphor),但应用项目级 Icon wrapper 接住尺寸、对齐与 currentColor,避免散落的 vendor 图标壳。
ui/ API 契约先行。
例如:值类控件 v-model;显隐统一 v-model:open;size 与 density 分开;slots 用短名。否则即使换掉旧库,新的内部组件层也会很快失控。
不选纯 Element Plus 长期主线的原因
需要先澄清:这不是因为 Element Plus「不支持深色」或「后台能力不够」。它在设置页、控制台、列表筛选上仍然很强,也适合大量标准中后台。
在混合内容产品里,问题会变成另一组事实:
默认气质偏企业后台
海报墙、人物墙、详情页、播放页需要的是内容产品节奏。强预设组件库的默认视觉与交互密度,更像管理后台。你可以覆写,但覆写本身会成为长期税。
使用面广 ≠ 深度绑定生态
历史上常见状态是:广泛使用按钮、表单、弹层、消息、下拉等基础控件层,而不是深度依赖 Table / Tree / 整套生态。 这意味着「换基座」的真实成本,往往不等于「重写整个后台框架」,而是「把基础控件层与主题接管权收回来」。
样式接管已经过重时,说明基座选错了层级
当项目出现系统性 html.dark .el-* 全局覆写、大量 :deep(.el-*) 与 !important、对 vendor 内部 DOM 结构重写时,问题通常不是「组件功能不够」,而是:
我们已经在系统性接管 vendor 的视觉实现,却仍把页面层建在 vendor 类名之上。
这种模式会持续侵蚀页面层与组件层边界:任何升级、DOM 结构调整或主题微调,都会牵动全局 CSS。继续加抽象壳包一层 Element Plus,再期待「以后平滑换库」,往往只会多一层假边界。
与自有主题目标冲突
若设计目标不是「轻度换肤」,而是自有主题系统(token / foundation / recipe / 业务域样式分层),选型核心就应改成:
哪个方案最适合让项目真正拥有自己的视觉系统?
在这个目标下,强预设 suite 的默认值与内部结构都是阻力。Element Plus 更适合被定义为历史过渡承载,而不是长期视觉底座——过渡结束后,就应从运行时依赖中退出。
对同类项目的可复用清单
若你也在做「内容墙 + 控制台」混合前端,可以按下面清单自检。
先写清产品约束
- 是否同时存在内容型页面与高密度管理页?
- 深色 / 品牌主题是主路径还是可选项?
- 播放或其它沉浸式页面是否必须自定义 UI?
- 数据是否服务端权威?表格是否必须 virtual + lazy / manual?
- 多端是「Web 壳全平台」还是「Web + 各端原生 UI」?
先砍假需求
- 路径选择是否其实是「面包屑 + 分层懒加载」,而不是 Tree?
- 所谓重组件清单里,有多少是真实页面入口,有多少是「库里有所以想要」?
- 虚拟化网格是否被明确写成硬约束(含量级目标)?
再比第一档路线
- A:headless + open-code + 自有 TanStack 网格内核——主题与交互资产可控,网格要自己养。
- B:Unstyled 套件 + 现成虚拟化 DataTable——后台面更快,网格归属在第三方。
- 是否把「UI 原语选型」和「datagrid 选型」绑在一起决策?
落地规则(与库名同等重要)
- 是否形成自有
ui/边界与依赖方向(ui不反向依赖业务页)? - token 是否为唯一主题事实来源?
- 新代码是否禁止直接绑 vendor 内部类名?
- 命令式 toast / confirm 是否有全局 provider 方案?
- 表单校验栈与 Form 原语是否同步定稿?
- open-code Data Table 样板是否立刻切到 manual 模式?
- 自有基础设施是否从第一天起有最小测试 / 无障碍守护(如 vitest + axe)?
不建议的做法
- 在强预设库上继续堆全局覆写,指望「以后再整理 CSS」。
- 先包一层「兼容壳」假装解耦,页面仍直接依赖 vendor DOM。
- 因库内建 Tree / Cascader 而回退已验证的分层浏览模式。
- 用跨端壳框架的优势,去解决其实不存在的「全平台统一 Web 壳」目标。
局限与后续
局限
- 自有 datagrid 有维护成本。 Route A 把十万级表格可用性从「组件整合问题」部分转成「自建内核问题」。表头交互、列冻结、列状态持久化、多列排序等,都要自己演化和回归。
- 后台组件面不会一夜之间齐全。 阻塞项(Dialog、Menu、Combobox、Form 家族等)要先落仓;DatePicker、Upload 等可等真实需求再进,避免一次造全库。
- 迁移期双栈最容易失控。 即便最终目标是清退旧库,过渡规则仍建议写死:新代码不新增旧库 import、不新增 vendor 内部类名覆写、z-index 用 token 统一。
- 性能数字要因设备校准。 例如「万级行滚动帧率 / 挂载时间」适合作为试点硬线,而不是终态教条;应用 production build 测,不要只在 dev 里主观感觉。
后续可继续做的事
- 把
ui/与 datagrid 的契约、token、无障碍基线固化成文档与 smoke。 - 内容页 recipe(海报墙、详情、播放周边)与控制台密度体系分开演进,共用同一 token 与原语。
- 列状态序列化、多列排序等能力,优先落在统一 state 形状上(A 路线通常更直接)。
- 若产品战略变成「整站围绕单一应用壳 / 跨端 Web 运行时」,再重新评估 Quasar 一类方案;那是战略变化,不是本轮权重微调。
核心结论(三句)
- 混合前端的 UI 选型,本质是主题主导权 + 内容页自由度 + 虚拟化网格归属,而不是「哪个中后台库组件更多」。
- 强预设组件库适合标准控制台;当项目已在系统性覆写 vendor 视觉时,应转向自有
ui边界 + headless 行为层,而不是继续加壳。 - 当前可落地的稳妥主线是:自有组件层 + Reka UI + TanStack Table/Virtual + 统一滚动条方案;Element Plus 仅作为历史过渡背景,不再作为运行时主线。
相关阅读
同系列还可对照:
- 媒体库播放排障:从会话创建到 HLS / DirectPlay / 字幕链路:播放控制面与交付链路;UI 只消费决策结果,不本地重算 session 生命周期。
- Octans Android 播放内核演进:从 libmpv 单内核到双内核实验:多端「原生各自 UI」策略下的 Android 播放内核取舍。
来源
本文改写自 Octans 项目前端 UI 基座选型相关决策与研究材料,结论按产品侧可复用表述整理,不绑定内部仓库路径。