第一次用 uniapp 同时生成微信小程序和H5时,我在一个简单的按钮样式上卡了很久。小程序里圆角用rpx刚刚好,到了H5却显得过大;H5上想要一个阴影效果,小程序却不支持某些CSS属性。后来才发现,工具本身已经提供了非常细腻的应对策略,只是当时我还没掌握条件编译和平台判断的正确姿势。
本文将带着你从零实现一个“阅读器”页面,它需要在微信小程序、H5 和 App 上保持一致的观感,同时支持夜间/白天主题切换。过程中会密集用到条件编译、CSS 变量、uni.getSystemInfoSync() 平台判断、以及页面布局的兼容写法。全部代码都能直接在 uniapp 项目中运行。
项目骨架与需求拆解
先明确我们要做什么:一个文章阅读界面,顶部标题栏在不同端有不同的高度(小程序需避开胶囊按钮,H5用传统导航),中间是正文内容,底部有切换主题的悬浮按钮。夜间模式下背景变深、文字变浅,且这一选择需要持久化存储。
在 uniapp 中创建项目后,页面目录会包含 .vue 单文件。我们先建立 pages/reader/reader.vue。
第一步:利用 CSS 变量搭建可切换主题
不用 uni.getStorageSync 来控制主题类名,因为那样会让每个组件都引入判断逻辑。我们把主题色定义在 CSS 变量里,在 App.vue 的全局样式中设置默认值:
/* App.vue 的 style 块(不 scoped) */
page {
--bg-color: #ffffff;
--text-color: #333333;
--bar-bg: #f8f8f8;
--shadow: 0 2px 10px rgba(0,0,0,0.05);
}
/* 夜间模式变量覆盖 */
page.dark {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
--bar-bg: #2a2a2a;
--shadow: 0 2px 10px rgba(0,0,0,0.3);
}
然后,我们在 reader.vue 中使用这些变量,免去反复写条件样式的痛苦。背景色用 var(--bg-color),文字用 var(--text-color)。主题切换只需要在 page 元素上添加或移除 dark 类。
第二步:条件编译处理不同端的顶部栏
微信小程序有原生的导航栏,一般我们会配置 navigationStyle: "custom" 来自定义,但胶囊按钮会占据右上角,所以标题区域需要留出安全距离。H5 则没有胶囊,可以直接用简单的 view 做导航栏。App 端情况类似 H5。
uniapp 的条件编译可以针对不同平台编写不同代码块。在模板中:
<template>
<view class="reader-page">
<!-- 顶部栏:条件编译 -->
<!-- #ifdef MP-WEIXIN -->
<view class="nav-bar-wx">
<view class="title">阅读</view>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="nav-bar-h5">
<view class="back-btn" @click="goBack">返回</view>
<view class="title">阅读</view>
</view>
<!-- #endif -->
<scroll-view class="content" scroll-y>
<text class="article">{{ articleText }}</text>
</scroll-view>
<view class="theme-toggle" @click="toggleTheme">
{{ isDark ? '☀️' : '🌙' }}
</view>
</view>
</template>
上面的 #ifdef MP-WEIXIN 表示仅当编译为微信小程序时,才包含其包裹的视图;否则使用 #ifndef 对应的 H5/App 栏。这样做的好处是,不用在运行时做平台判断,编译后各端代码就是精简的,不会有冗余 DOM。
第三步:动态获取胶囊信息适配导航栏高度
小程序端的自定义导航栏需要知道胶囊按钮的位置,才能让标题居中且不被遮挡。在 onLoad 中获取:
<script>
export default {
data() {
return {
articleText: '这是文章正文...',
isDark: false,
navBarHeight: 44 // 默认值
};
},
onLoad() {
// #ifdef MP-WEIXIN
const menuButtonInfo = uni.getMenuButtonBoundingClientRect();
const systemInfo = uni.getSystemInfoSync();
// 导航栏高度 = 胶囊底部到状态栏底部的距离 + 胶囊高度 + 一些边距
this.navBarHeight = menuButtonInfo.bottom + menuButtonInfo.height + 8;
// #endif
// 读取本地主题存储
const savedTheme = uni.getStorageSync('app_theme');
if (savedTheme === 'dark') {
this.isDark = true;
this.applyTheme(true);
}
},
methods: {
toggleTheme() {
this.isDark = !this.isDark;
this.applyTheme(this.isDark);
uni.setStorageSync('app_theme', this.isDark ? 'dark' : 'light');
},
applyTheme(dark) {
// 直接操作 page 元素的 class
const page = document.querySelector('page') || document.body;
if (dark) {
page.classList.add('dark');
} else {
page.classList.remove('dark');
}
},
goBack() {
uni.navigateBack();
}
}
}
</script>
这里用到了 uni.getMenuButtonBoundingClientRect() 来同步胶囊位置,它只在小程序端有效,所以用条件编译包裹,避免 H5 端报错。H5 的导航栏高度直接用 CSS 定高即可。
第四步:样式隔离与平台特有细节
同样的,CSS 也可以条件编译。在 <style> 块中,你可以用 /* #ifdef MP-WEIXIN */ 注释来区分平台样式:
<style scoped>
.reader-page {
background: var(--bg-color);
color: var(--text-color);
min-height: 100vh;
}
/* 小程序导航栏:动态高度 */
/* #ifdef MP-WEIXIN */
.nav-bar-wx {
height: v-bind(navBarHeight) px;
background: var(--bar-bg);
box-sizing: border-box;
padding-top: var(--status-bar-height);
}
/* #endif */
/* H5/App 导航栏:固定高度 */
/* #ifndef MP-WEIXIN */
.nav-bar-h5 {
height: 44px;
background: var(--bar-bg);
display: flex;
align-items: center;
}
/* #endif */
.content {
padding: 20rpx; /* rpx 在各端自动换算 */
}
.theme-toggle {
position: fixed;
bottom: 30px;
right: 30px;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: var(--bar-bg);
box-shadow: var(--shadow);
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
}
</style>
这样,小程序端的导航栏高度是动态计算的,而其他端则是固定 44px。CSS 变量确保了主题色自然渗透到所有元素。
第五步:处理主题切换的兼容性
在 H5 和 App 端,document.querySelector('page') 可能拿不到元素。更稳妥的做法是利用 uniapp 的 uni.setPageStyle 或者直接在根组件上维护一个 class。不过实际测试中,直接给 document.documentElement 添加 class 在 H5 端最为可靠。因此调整 applyTheme:
applyTheme(dark) {
const root = document.documentElement || document.querySelector('html');
if (dark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}
而在小程序端,document 不存在,上述代码会报错。所以需要用条件编译把这段逻辑包裹起来,或者改用小程序支持的 uni.setPageStyle 来动态修改页面背景色。但为了统一,我们可以把主题切换做成全局状态,在 App.vue 中监听:
// App.vue 中
onLaunch() {
const theme = uni.getStorageSync('app_theme');
if (theme === 'dark') {
// 设置全局样式变量
uni.setPageStyle({
style: {
'--bg-color': '#1a1a1a',
'--text-color': '#e0e0e0'
}
});
}
}
不过 uni.setPageStyle 只能设置当前页面样式,而非全局。更佳实践是将 dark 类挂载到 page 元素上,在小程序端可以通过 this.$mp.page 获取页面实例并添加 class。经过实际摸索,以下方法在小程序、H5 均可运行:
// 在组件中
setThemeClass(dark) {
const pages = getCurrentPages();
const page = pages[pages.length - 1];
if (page) {
const pageElement = page.$el || page;
if (dark) {
pageElement.classList.add('dark');
} else {
pageElement.classList.remove('dark');
}
}
}
但为了降低复杂度,本文案例采用条件编译分开处理:H5靠操作 documentElement,小程序靠 uni.setPageStyle 动态写入变量。两种方式都能达到效果。
完整可运行代码整合
下面给出 reader.vue 的整合版本,去除冗余注释,可一键拷贝使用:
<template>
<view class="reader-page">
<!-- #ifdef MP-WEIXIN -->
<view class="nav-bar-wx" :style="{ paddingTop: statusBarHeight + 'px', height: navBarHeight + 'px' }">
<view class="title">阅读</view>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="nav-bar-h5">
<view class="back-btn" @click="goBack">← 返回</view>
<view class="title">阅读</view>
</view>
<!-- #endif -->
<scroll-view scroll-y class="content">
<text>{{ articleText }}</text>
</scroll-view>
<view class="theme-btn" @click="toggleTheme">
{{ isDark ? '☀️' : '🌙' }}
</view>
</view>
</template>
<script>
export default {
data() {
return {
articleText: '这是一段示例文章。...',
isDark: false,
statusBarHeight: 0,
navBarHeight: 44
};
},
onLoad() {
// 获取状态栏高度(所有端通用)
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
// 小程序额外获取胶囊高度
// #ifdef MP-WEIXIN
const menuButton = uni.getMenuButtonBoundingClientRect();
this.navBarHeight = menuButton.bottom + menuButton.height + 8;
// #endif
// 读取主题
const theme = uni.getStorageSync('app_theme');
if (theme === 'dark') {
this.isDark = true;
this.applyTheme(true);
}
},
methods: {
toggleTheme() {
this.isDark = !this.isDark;
uni.setStorageSync('app_theme', this.isDark ? 'dark' : 'light');
this.applyTheme(this.isDark);
},
applyTheme(dark) {
// #ifdef H5
const root = document.documentElement;
root.classList.toggle('dark', dark);
// #endif
// #ifdef MP-WEIXIN
uni.setPageStyle({
style: dark
? { '--bg-color': '#1a1a1a', '--text-color': '#e0e0e0', '--bar-bg': '#2a2a2a' }
: { '--bg-color': '#ffffff', '--text-color': '#333333', '--bar-bg': '#f8f8f8' }
});
// #endif
},
goBack() {
uni.navigateBack();
}
}
}
</script>
<style scoped>
/* 全局变量依赖 App.vue 中的定义,此处直接使用 */
.reader-page {
background: var(--bg-color);
color: var(--text-color);
min-height: 100vh;
position: relative;
}
/* #ifdef MP-WEIXIN */
.nav-bar-wx {
background: var(--bar-bg);
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
}
/* #endif */
/* #ifndef MP-WEIXIN */
.nav-bar-h5 {
height: 44px;
background: var(--bar-bg);
display: flex;
align-items: center;
padding: 0 12px;
}
.back-btn {
position: absolute;
left: 12px;
}
/* #endif */
.title {
font-size: 18px;
}
.content {
padding: 20rpx;
line-height: 1.8;
}
.theme-btn {
position: fixed;
bottom: 40rpx;
right: 40rpx;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: var(--bar-bg);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
}
</style>
总结
uni-app 的条件编译远不止 ifdef 这么简单,它能在模板、脚本、样式甚至配置文件里精细地切割平台差异,同时保持一套代码的完整性。配合 CSS 变量和动态主题,跨端项目的维护成本会明显降低。
实际项目中,你可能还会遇到更多边界情况,比如 App 端的原生导航栏、各端图片路径差异、插件兼容等。但只要掌握了条件编译和平台判断的核心思路,就能用组合的方式逐一化解。本文的案例虽然围绕阅读页面展开,但其适配策略几乎可以复用到任何需要多端同构的场景里。

