上周把项目从微信小程序扩展到H5和安卓App,原以为要维护三套代码,结果发现uni-app的条件编译彻底解决了这个问题。不过群里还是有很多人搞不懂 #ifdef 和 #ifndef,甚至有人把条件编译写进css里结果没生效。今天我就把条件编译这块从头到尾捋一遍,全是实战经验,没有废话。
先说明:条件编译是uni-app独有的特性,它不是在运行时判断,而是在编译期就干掉不需要的代码。也就是说,你编译成微信小程序时,H5的代码块根本不会被打包进去,这样做不仅能减小包体积,还能避免运行时报错。这是不同于JS里写if判断的核心点。
基础语法:记住这几个指令就够日常用了
在注释中写条件编译,主要用 #ifdef 表示“如果存在平台”,#ifndef 表示“如果不存在平台”。可以写在js、css、template以及组件json里。写法有点差异,先看最常用的js。
// #ifdef H5
console.log('这是H5专属逻辑');
// #endif
// #ifndef MP-WEIXIN
console.log('除了微信小程序之外的平台都会执行');
// #endif
其中H5、MP-WEIXIN是官方预定义的平台标识。注意不要加引号,直接写就行。多平台用 || 连接,比如 // #ifdef H5 || MP-WEIXIN 表示H5或微信小程序。
在模板template里,条件编译写在注释中,格式如下:
<!-- #ifdef H5 -->
<view>只有在H5端才显示</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view>非微信小程序端显示</view>
<!-- #endif -->
在css里,需要用注释包裹样式:
/* #ifdef H5 */
.h5-only {
display: flex;
}
/* #endif */
/* #ifdef MP-WEIXIN */
.mp-only {
padding: 10px;
}
/* #endif */
有一点要特别注意:在css里进行条件编译时,必须把选择器完整包裹在注释里。如果你只包了一行声明,很可能编译后留下半吊子样式,我当时就被坑过。
文件级条件编译:不同平台用不同的文件
光靠代码块里的条件编译在复杂项目里依然难受。比如H5需要调微信JS-SDK,而小程序要调uni.login,逻辑完全不一样。这时候我更喜欢把不同代码放到不同文件里,利用uni-app的文件命名规则来区分。
官方支持在文件名后加平台后缀,例如:
index.h5.vue—— 仅H5会加载index.mp-weixin.vue—— 仅微信小程序会加载index.app.vue—— 仅App会加载
以前我总把这个规则忘记,后来项目多了才体会到它的威力。像一些使用了第三方H5 SDK的api,直接建一个sdk.h5.js和sdk.mp-weixin.js,然后在主代码里统一import sdk from '@/utils/sdk',uni-app会根据当前编译平台自动选择文件。这样做比在代码里到处写#ifdef清晰得多。
这里有个坑:如果你同时存在 index.vue 和 index.h5.vue,那么H5编译时会优先选择 index.h5.vue,而不是 index.vue。所以公共的逻辑仍然放回默认文件里,只有差异部分单独写后缀文件。
实战案例:封装一个平台差异的存储模块
最近在做跨端小项目,需要用到本地存储。H5可以用localStorage,小程序用uni.setStorageSync,老项目里可能还有App的plus.storage。用条件编译把它一次性封装好,以后直接调用同一套接口。
// src/utils/storage.js
export function setItem(key, value) {
// #ifdef H5
localStorage.setItem(key, JSON.stringify(value));
// #endif
// #ifndef H5
uni.setStorageSync(key, value);
// #endif
}
export function getItem(key) {
// #ifdef H5
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
// #endif
// #ifndef H5
return uni.getStorageSync(key);
// #endif
}
export function removeItem(key) {
// #ifdef H5
localStorage.removeItem(key);
// #endif
// #ifndef H5
uni.removeStorageSync(key);
// #endif
}
因为H5的localStorage只能存字符串,所以额外做了一次JSON序列化。而uni的storage处理非字符串会自动转换,所以小程序和App直接用原值就行。这种封装方式在项目里特别常见,避免了到处写分支。
页面json的条件编译:小程序独有的配置
小程序页面json里可以配置下拉刷新、导航栏样式等,有些属性在H5端没有意义。如果还想在同一份代码里维护,可以在json文件中使用条件编译注释。比如pages/index/index.json:
{
"navigationBarTitleText": "首页",
// #ifdef MP-WEIXIN
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
// #endif
}
但是注意,在json里写注释是不符合标准json规范的,好在uni-app的编译器会先剥离注释再解析。目前我在微信小程序上用了没问题,H5端因为没写这个属性也不会报错。不过还是建议大多数情况用pages.json里的条件编译,而不是页面内json文件。
pages.json里的条件编译:全局配置差异化
pages.json是uni-app的全局配置文件,里面也支持条件编译。比如我们小程序端需要设置全局导航栏颜色,H5端不需要,可以这样:
// pages.json 不允许直接写注释,但官方支持特殊写法
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#ffffff"
},
// #ifdef MP-WEIXIN
"networkTimeout": {
"request": 10000
},
// #endif
}
这个我没有实际用过,但看官方文档支持。唯一要注意的就是必须严格遵守JSON带注释的格式,不能把注释乱放。如果编译报错,优先检查注释是否位于键值对之间,而不是数组中间。
注意:条件编译只能用于uni-app体系内
有些同学在组件内部直接写requireocess.env.NODE_ENV去判断,其实这不算条件编译。由于uni-app在编译时已经确定了平台,你完全可以写死。但也别把原生条件编译和webpack的resolve.alias混为一谈,不然容易晕。我建议项目根目录的vue.config.js里仍然可以用webpack属性,但里面的逻辑会作用于所有端。跨端差异还是交给#ifdef吧。
还有一点,HBuilderX 3.x版本以后,条件编译支持在<script>里对import语句做注释包裹,但有一点需要注意——如果你使用vite构建(比如用CLI创建的项目),条件编译在编译过程中生效,所以import的代码也会被裁剪。如果你用npm包导入某个第三方库,在H5端用了一个版本,小程序端又用了另一个版本,最好的方式还是用文件级条件编译。
别踩这些坑:条件编译排错心得
《一》注释标记必须成对出现。少了#endif,编译阶段会报错,而且报错位置经常是文件末尾,很迷惑。
《二》html标签里的条件编译,只能包住<view>等uniapp组件,不能包住<text>的一部分。如果你试图在同一个标签中间切一刀,编译后会变得很奇怪。
《三》#ifdef和#ifndef后面必须跟空格再写平台。写成#ifdefH5是无效的,而且不容易发现。
《四》如果你同时在模板和script中使用了条件编译,两个区域的编译是独立处理的。比如你在script里定义了某个变量,但在H5端又被裁剪了,模板里H5分支却用了该变量,那么H5编译时就找不到变量。这就是所谓的“跨区域引用错误”,做的时候要把模板和逻辑一起考虑,建议把平台差异封装成同一个变量,比如:
const isH5 = !!(typeof window !== 'undefined'); // 不用条件编译也可以判断,但只在H5为true
但要记住,这只是一个普通的JS判断,不会裁掉代码。若你想让H5打包时不包含小程序SDK,还是要用条件编译或文件级隔离。
《五》在App端,由于App使用的可能是vue页面或nvue页面,nvue里条件编译同样可以用,但注意nvue的样式有限,不能用某些CSS功能。如果你自适应用了条件编译,最好分别测试。
组合使用:条件编译+环境变量
有时候单纯靠平台标识不够,比如还在区分测试环境和生产环境。这时候可以把process.env.NODE_ENV和条件编译一起用。比如测试环境的H5需要开启mock数据,生产环境H5使用线上API。
// #ifdef H5
const API_BASE = process.env.NODE_ENV === 'development' ? 'https://dev-api.com' : 'https://api.com';
// #endif
// #ifdef MP-WEIXIN
const API_BASE = 'https://api.com';
// #endif
注意小程序端不能使用process.env.NODE_ENV吗?其实在uni-app里通过webpack或vite注入也能用,但为了避免混淆,小程序端我直接写死。这样编译出来,小程序端只有一行URL,不会有环境分支。
最后送上一个我在实战中总结的“口诀”
“模板注释包标签,脚本注释包代码,样式注释包选择器,文件后缀最省心。”
只要记住了这几句,基本可以应付90%的跨端需求。我对条件编译的定位就是:它是解决“跨端差异”的最终武器,但也不要滥用。如果一个地方差异很小,比如只是某个样式值不同,完全可以用普通class加平台判断去搞定,过度的条件编译会让代码阅读困难。在团队协作中,最好约定好条件注释的用途和范围,避免每个人乱插。
现在这个项目已经跑了两周,H5端和小程序端共用一份代码仓库,改个逻辑两边同时生效,再也不用ctrl+c/v两遍了。条件编译真的是uni-app最值得花时间掌握的功能之一。

