uni-app 深色模式完整指南:从原理到华为白底白字 bug 排查
前言
最近在 SimbaSpeaks 项目里踩了一个很经典的坑: 一切开发测试都正常, 但把安装包发到华为手机上, 登录页的输入框里打的字完全看不见. 排查过程意外地复杂, 因为涉及到三层独立的"深色模式"机制互相作用. 把这次踩坑完整记录下来, 也顺手把"如何在 uni-app 里正确开启/关闭深色模式"系统性地讲一遍.
三层架构总览
uni-app 应用里到底有几层 "深色模式" 在起作用? 一开始我也以为是单一机制, 排查后才发现有三层独立生效:
下面逐层拆解.
Layer 1: uni-app 框架 DarkMode (官方机制)
这是 uni-app 官方文档 提供的深色模式接入方案. 它依赖三个东西同时存在:
manifest.json里开启darkmode: true- 指定
themeLocation指向一个theme.json theme.json里分别定义light和dark两个变量集合
1.1 各平台分别配置
uni-app 是跨端的, darkmode 需要每个目标平台都显式声明:
{
"app-plus": { "darkmode": true, "themeLocation": "theme.json" },
"app-harmony": { "darkmode": true, "themeLocation": "theme.json" },
"h5": { "darkmode": true, "themeLocation": "theme.json" },
"mp-weixin": { "darkmode": true, "themeLocation": "theme.json" }
}
1.2 theme.json 结构
{
"light": {
"navBgColor": "#f8f8f8",
"navTxtStyle": "black",
"bgColor": "#ffffff"
},
"dark": {
"navBgColor": "#292929",
"navTxtStyle": "white",
"bgColor": "#1f1f1f"
}
}
然后在 pages.json 里用 @变量名 引用:
{
"globalStyle": {
"navigationBarBackgroundColor": "@navBgColor",
"navigationBarTextStyle": "@navTxtStyle",
"backgroundColor": "@bgColor"
}
}
1.3 关键限制 (官方文档)
- iOS 13+ / Android 10+ 设备才支持
- 必须云端打包 (HBuilderX 自定义基座都不行)
- App 端需要先调用
plus.nativeUI.setUIStyle('auto')才能监听到主题切换
Layer 2: OS 强制 WebView 深色 (这次 bug 的真凶)
这是华为/小米等国产 ROM WebView 内置的"强制网页深色"功能, 跟 uni-app 完全无关:
- 华为 EMUI/HarmonyOS: 设置 → 显示 → 深色模式 (或电池省电模式) 会自动启用
- Android 10+: 由 app 主题里的
android:forceDarkAllowed属性控制
2.1 现象
开启后, WebView 会强行给页面里所有 <input>/<textarea>/<select> 应用深色样式:
input { color: rgba(0, 0, 0, 0.3) !important; }
如果你的页面里 .input 没有显式 color, 浏览器就用这个浅色; 同时 .card 是硬编码 background: #fff 不变, 结果就是 白底浅字 → 看不见.
2.2 跟 Layer 1 的区别
| 维度 | Layer 1 (uni-app) | Layer 2 (OS WebView) |
|---|---|---|
| 触发方 | 开发者主动配置 | OS 自动应用 |
| 作用范围 | 仅 theme.json 定义的变量 |
所有未显式 color 的元素 |
| 是否需要 theme.json | 是 | 否 |
| 是否影响 input | 不会 (除非变量引用到) | 会, 且不可控 |
这次 bug 完全是 Layer 2 引起的, 因为项目根本没配置
theme.json, Layer 1 实际上是 no-op.
Layer 3: CSS @media prefers-color-scheme
CSS 标准里的媒体查询, 完全由开发者决定怎么用:
/* 默认浅色 */
.card { background: white; color: black; }
/* 系统深色模式下生效 */
@media (prefers-color-scheme: dark) {
.card { background: #1b1b1b; color: #fff; }
}
兼容性: Chrome 76+ / Safari 12.1+ / Firefox 67+. uni-app 的 App WebView 是 Chrome 内核, 完全兼容.
完整方案: 修复华为白底白字 + 关闭 DarkMode
我们项目最终决定关闭 DarkMode (产品方向暂不支持), 但仍然需要修复华为的强制深色 bug. 完整方案需要三处同时配置:
3.1 Layer 1: 关闭 manifest darkmode
{
"app-plus": { "darkmode": false, ... },
"mp-weixin": { "darkmode": false, ... }
}
3.2 Layer 2: 添加 color-scheme meta (H5 入口)
index.html 的 <head> 里加:
<meta name="color-scheme" content="light">
这个 meta 告诉浏览器: "我的页面只支持 light theme, 请不要自动应用深色样式". 这是修复 Layer 2 最干净的办法, 不需要修改每个元素的 CSS.
3.3 Layer 2 + 3: 全局 :root color-scheme
App.vue 的全局 <style> 里加:
:root {
color-scheme: light;
}
跟 <meta> 等价, 但作为 CSS 标准属性在所有平台编译结果里都生效 (H5/小程序/App WebView).
3.4 完整改动清单
| 文件 | 改动 | 作用层 |
|---|---|---|
src/manifest.json |
app-plus.darkmode: false + mp-weixin.darkmode: false |
L1 |
index.html |
添加 <meta name="color-scheme" content="light"> |
L2 |
src/App.vue |
添加 :root { color-scheme: light; } |
L2 + L3 |
验证清单
修复后在华为真机上验证:
- 系统设置: 浅色 / 深色 / 跟随电池 三种模式切换
- input 输入文字: 始终深色清晰可见
- 占位符 (placeholder): 颜色不变
- 验证码图片: 正常显示
- 其他平台 (iOS / 小米 / 微信小程序) 行为一致
Chrome DevTools 可以用 Rendering 面板里的 Emulate CSS prefers-color-scheme: dark 模拟深色模式, 快速验证 L3 不会被意外触发.
反过来: 如果要开启深色模式怎么办?
对于从零开始的项目, 建议流程是:
- 先写完整的 L3 (
@media (prefers-color-scheme: dark)适配), 因为这是最底层的, 不依赖任何框架. - 再加 L1 (uni-app manifest + theme.json) 处理原生组件 (navigationBar, tabBar).
- 测试 L2 在华为等设备上的表现, 必要时再加
color-scheme: light dark;让浏览器允许深色.
如果跳过了 L3 直接做 L1, 就会出现我们这次遇到的 "页面硬编码浅色, 但 WebView 自动深色" 的撕裂感.
总结
- 深色模式不是单一开关, uni-app 里至少有三层互相独立的机制
- 华为白底白字 bug 的真凶是 Layer 2 (OS WebView 强制深色), 不是 Layer 1
color-scheme是修复 Layer 2 最干净的方式, 比给每个 input 加!important color优雅得多- 关掉 DarkMode 时也要每个目标平台都显式声明, 避免未来 build 时漏掉某个平台
参考: