uniapp多端适配精要:条件编译与动态主题切换实战

2026-06-17 0 942

第一次用 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 端的原生导航栏、各端图片路径差异、插件兼容等。但只要掌握了条件编译和平台判断的核心思路,就能用组合的方式逐一化解。本文的案例虽然围绕阅读页面展开,但其适配策略几乎可以复用到任何需要多端同构的场景里。

uniapp多端适配精要:条件编译与动态主题切换实战
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uniapp多端适配精要:条件编译与动态主题切换实战 https://www.taomawang.com/web/uniapp/2165.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务