Mpx 跨端开发实录(上):实际项目里 Mpx 是怎么用的
mpx探索一下
最近在做 Mpx 多端开发(小程序 + Web + RN),官方文档能告诉你「能写什么」,但真正上手还是会卡在「实际项目里该怎么组织、怎么写才不出事」。这篇文章是我把文档和踩坑经验合在一起整理的结果,尽量只讲通用的东西,换任何一个 Mpx 项目应该都能对照着看。
一、Mpx 是什么
Mpx 是滴滴开源的小程序跨端框架。核心思路很简单:不推翻小程序原生写法,在上面做增强,让你用接近 Vue 的体验写代码,然后编译到微信/支付宝/百度等小程序、Web,以及 React Native。
原生小程序一个页面四个文件:wxml、js、wxss、json。Mpx 用 .mpx 单文件把它们收进一个文件里:
| 区块 | 对应原生 | 作用 |
|---|---|---|
<template> |
wxml | 视图模板 |
<script> |
js | 逻辑 |
<style> |
wxss | 样式 |
<script name="json"> |
json | 页面/组件配置 |
除了单文件,Mpx 还增强了这些能力(文档里都有,实际开发高频用到的):
- 数据响应:
data、computed、watch,以及组合式 API(setup、ref) - 模板增强:
wx:model双向绑定、内联传参、wx:class/wx:style、动态组件 - 样式预处理:Stylus / Less / Sass
- 条件编译:按平台、按环境输出不同代码
- 状态管理:类似 Vuex 的 store
二、一个 Mpx 项目通常长什么样
不用记某个具体仓库的名字,大型 Mpx 项目的目录套路基本都类似:
src/
├── app.mpx # 应用入口
├── pages/ # 页面
│ └── index/
│ └── index.mpx
├── components/ # 公共组件
├── store/ # 状态管理(如果用)
└── utils/ # 工具函数
几个实际工程里常见的组织方式:
按页面和组件分:pages 放页面,components 放可复用组件,这是最基础的。
按功能模块分:页面多了以后,会拆成多个子目录或分包,每个模块下面有自己的 pages、components,公共能力抽到顶层的 components、utils、store。
应用入口 app.mpx:一般就两件事,createApp({}) 注册应用,json 区块里声明 pages 或分包。这里有个小坑:pages 不要写成空数组,否则分包配置可能出不来,微信开发者工具表现会很怪。
构建产物:跑 npm run watch 或 npm run build 之后,在 dist/wx、dist/ali 等目录里能看到编译后的小程序原生代码。日常开发改的是 .mpx 源文件,不要直接去改 dist 里的产物。
三、.mpx 文件怎么写
3.1 页面示例
<template>
<view class="page">
<view class="title">{{ title }}</view>
<view
wx:for="{{ list }}"
wx:key="id"
class="list-item"
wx:class="{{ { active: item.active } }}"
bindtap="handleTap(index)"
>
{{ item.text }}
</view>
</view>
</template>
<script>
import { createPage, ref, computed } from '@mpxjs/core'
createPage({
setup() {
const title = ref('标题')
const list = ref([
{ id: 1, text: '第一项', active: false },
{ id: 2, text: '第二项', active: true },
])
const handleTap = (index) => {
list.value[index].active = !list.value[index].active
}
return { title, list, handleTap }
},
})
</script>
<script name="json">
module.exports = {
navigationBarTitleText: '示例页面',
usingComponents: {},
}
</script>
<style lang="stylus">
.page
padding 32rpx
.list-item
padding 24rpx
margin-bottom 16rpx
.list-item.active
color #1677ff
</style>
3.2 组件示例
页面用 createPage,组件用 createComponent,别混。
<template>
<view class="btn" bindtap="handleClick">
<slot></slot>
</view>
</template>
<script setup>
const props = defineProps({
disabled: { type: Boolean, value: false },
})
const ctx = useContext()
const handleClick = () => {
if (props.disabled) return
ctx.triggerEvent('click')
}
</script>
<script type="application/json">
{
"component": true
}
</script>
<style lang="stylus">
.btn
padding 20rpx 40rpx
background #1677ff
color #fff
border-radius 8rpx
</style>
实际项目里选项式(createComponent({ data, methods }))和 <script setup> 两种写法都会遇到,功能一样,看团队习惯。
3.3 json 区块的两种写法
静态配置可以直接写 JSON:
<script type="application/json">
{
"usingComponents": {
"my-input": "../components/my-input"
}
}
</script>
需要按平台或环境动态生成时,用 js 写法:
<script name="json">
module.exports = {
navigationBarTitleText: '页面标题',
usingComponents: {
'my-input': '../components/my-input',
},
// 可以根据 __mpx_mode__ 做条件配置
}
</script>
json 增强的好处:能写注释、能写逻辑,比纯 JSON 文件灵活。组件注册也支持按需构建,没用到的组件不会被打包进去。
四、模板:实际开发要注意什么
4.1 动态 class 和 style
<!-- 不推荐:在 class 属性里拼字符串 -->
<view class="item {{ isActive ? 'active' : '' }}">
<!-- 推荐 -->
<view class="item" wx:class="{{ { active: isActive } }}">
<view wx:style="{{ { color: textColor, fontSize: '28rpx' } }}">
跨端(尤其 RN)项目里,后面这种写法几乎是硬性要求。
4.2 内联传参
Mpx 支持在模板里直接传参,不用再去 dataset 里绕:
<view wx:for="{{ list }}" wx:key="id">
<button bindtap="handleDelete(item, index)">删除</button>
</view>
methods: {
handleDelete(item, index) {
console.log(item, index)
},
}
4.3 双向绑定
<input wx:model="{{ keyword }}" />
<view>{{ keyword }}</view>
自定义组件也支持,通过 wx:model-prop 和 wx:model-event 指定绑定的属性和事件。
4.4 动态组件
<component is="{{ currentComp }}"></component>
currentComp 是字符串,对应 json 里 usingComponents 注册的组件名。切换 tab、切换展示形态时会用到。
4.5 列表渲染
<view wx:for="{{ list }}" wx:key="id" class="row">
{{ item.name }}
</view>
wx:key 不要省。输出 RN 时列表复用更严格,没 key 容易出现闪烁、顺序错乱。
4.6 模板里不要调普通方法
{{ }} 里写 {{ formatDate(time) }} 这种,Mpx 不支持(i18n 的 t() 是例外)。需要加工数据,用 computed 或在脚本里处理好再绑定。
4.7 输出 RN 时的滚动
小程序页面天然能滚,RN 端页面默认不滚动。onReachBottom、onPageScroll、下拉刷新在 RN 上行为和小程序不一样。
需要滚动区域时,用 scroll-view 包一层:
<scroll-view scroll-y style="height: 100vh">
<view wx:for="{{ list }}" wx:key="id">...</view>
</scroll-view>
「布局对了但滑不动」,十有八九是这个问题。
五、脚本:API、生命周期、条件编译
5.1 用 mpx.xxx,别直连 wx.xxx
跨端项目里,环境 API 统一走 @mpxjs/api-proxy:
import mpx from '@mpxjs/core'
mpx.request({ url: '/api/list' })
mpx.showToast({ title: '成功' })
mpx.setStorageSync('key', value)
代码里到处写 wx.request、my.xxx,Web 和 RN 构建很容易挂。这是跨端项目的基本规矩。
5.2 生命周期
页面和组件的生命周期和小程序基本一致:onLoad、onShow、onReady、onHide、onUnload 等。
组合式 API 里对应的是 onMounted、onUnmounted 等。注意:输出 RN 时,部分小程序专有生命周期不支持(比如分享朋友圈、Tab 点击之类),写之前查 Mpx 官方文档里 RN 平台的生命周期支持列表。
5.3 状态管理
项目大了以后组件通信不够用了,会上 store。Mpx 的 store 和 Vuex 很像:
// store/index.js
import { createStore } from '@mpxjs/core'
export default createStore({
state: { count: 0 },
mutations: {
increment(state) {
state.count++
},
},
actions: {
asyncIncrement({ commit }) {
commit('increment')
},
},
})
页面或组件里通过 mapState、mapMutations 或直接 this.$store 访问。复杂项目还可以拆成多个 store 模块。
5.4 条件编译
跨端开发里有些能力无法全平台统一,Mpx 在编译期做条件编译,比运行时 if (platform) 干净。
编译变量 __mpx_mode__ 常见值:
| 平台 | 值 |
|---|---|
| 微信小程序 | wx |
| 支付宝小程序 | ali |
| Web | web |
| iOS RN | ios |
| Android RN | android |
| 鸿蒙 RN | harmony |
脚本里:
if (__mpx_mode__ === 'wx') {
// 只在微信小程序执行的逻辑
}
if (__mpx_mode__ === 'ios' || __mpx_mode__ === 'android') {
// 只在 RN 执行的逻辑
}
模板里:
<!-- 方式一:wx:if -->
<view wx:if="{{ __mpx_mode__ === 'ios' }}">iOS 专用内容</view>
<!-- 方式二:@mode 后缀,只在指定平台输出该节点/属性 -->
<view @ios|android>只在 RN 输出</view>
<text numberOfLines@ios|android="{{ 1 }}">单行省略</text>
样式里:
/* @mpx-if (__mpx_mode__ === 'ios') */
.container { padding-bottom: 48rpx; }
/* @mpx-else */
.container { padding-bottom: 32rpx; }
/* @mpx-endif */
json 里:
module.exports = {
navigationStyle: __mpx_mode__ === 'wx' ? 'custom' : 'default',
}
使用原则:
- 能全端统一写的,就不要条件编译
- 真要用,只包最小必要片段
- 样式条件编译时,整条规则(选择器 + 声明)一起包,别留下空选择器,否则 PostCSS 可能报错
另外 Mpx 还支持文件级条件编译:同一个组件可以写 index.mpx(通用)和 index.ios.mpx(iOS 特化),编译器会按平台选文件。逻辑复杂、差异大的时候,拆文件比在一个文件里堆 if 清楚。
六、样式:跨端开发最耗时间的部分
小程序里很多 CSS 写法,RN 端没有直接对应。输出 RN 的项目,样式要单独过一遍。
6.1 选择器:尽量单类名
RN 端主要支持单类选择器。下面这些在 RN 上不行或不稳定:
| 不支持/慎用 | 替代方案 |
|---|---|
.parent .child 后代选择器 |
直接在子节点上加独立类名,如 .page-title |
.a.b 交集选择器 |
合并成一个类名,如 .btn-primary |
.item:first-child 等伪类 |
模板里 wx:class="{{ { 'first-item': index === 0 } }}" |
::before / ::after |
用真实的 <view> 节点代替 |
:active 点击态 |
用 hover-class + hover-stay-time |
改选择器的时候,模板、样式、脚本里 createSelectorQuery().select('.xxx') 引用的类名要一起改。
6.2 布局
用 Flexbox,别用 Grid 和 Float。RN 对 Flex 支持最好,这也是实际项目里的默认选择。
.container {
display: flex;
flex-direction: column;
}
.row {
display: flex;
flex-direction: row;
align-items: center;
}
6.3 隐藏元素
RN 上 display: none 行为和小程序不完全一样。隐藏但占位、或做动画切换时,常用:
.hidden {
width: 0;
height: 0;
overflow: hidden;
}
或者用模板指令 wx:show / wx:if 控制节点是否渲染。
6.4 文本省略
小程序写法:
.ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
RN 不认这三个属性组合,要在模板上加 RN 原生属性:
<text
class="ellipsis"
numberOfLines@ios|android="{{ 1 }}"
ellipsizeMode="tail"
>{{ content }}</text>
关键:小程序的 CSS 要保留,RN 的属性是额外补充,用条件编译分开。只留 RN 写法、删了小程序 CSS,小程序端省略号就没了。
6.5 1 像素边框
小程序 1rpx 边框在 retina 屏上很细。RN 上有时需要用框架提供的 hairlineWidth 等等效方案,同样建议条件编译,各端各写。
6.6 单位
优先 rpx 和 px,少用 rem、em。font-weight 尽量用 normal / bold,数值型字重在 RN 上支持不完整。
样式文件里的 /*use rpx*/、/*use px*/ 注释是告诉编译器怎么处理单位,别随手删。
6.7 Stylus / Less 嵌套
写 RN 的项目,预处理器的嵌套选择器要先展开成平铺的单类选择器,再检查 RN 兼容性。嵌套写法在源文件里看着方便,展开后可能是 RN 不支持的复合选择器。
七、iOS 和 Android 样式差异
同一份 Mpx 代码编译到 iOS 和 Android,真机效果经常有差别。这不是你写错了,是 RN 本身在两个系统上的渲染差异。
7.1 安全区
iOS 有刘海和底部 Home Indicator,Android 各厂商导航栏高度也不统一。底部列表、固定按钮经常要额外留 padding,常见写法:
/* @mpx-if (__mpx_mode__ === 'ios') */
.page-footer { padding-bottom: 48rpx; }
/* @mpx-else */
.page-footer { padding-bottom: 32rpx; }
/* @mpx-endif */
也有项目把安全区交给原生容器处理,Mpx 里只做微调。
7.2 字体
iOS 默认 PingFang,Android 回退系统字体。同一个 font-size: 28rpx,两端行高可能差 1~2px,按钮垂直居中尤其容易看出差别。验收必须两台真机都看,模拟器参考意义有限。
7.3 字重
font-weight: 500 在 RN 上可能不生效,改成 bold 或 normal 更稳。
7.4 阴影
box-shadow 在 iOS 和 Android 上效果不同,Android 有时还更耗性能。重要 UI 的阴影要两端分别看,别只对着 iOS 调。
7.5 渐变
linear-gradient 两端都能用,但复杂渐变(多色标、特殊角度)建议先在 RN 真机验证,别直接从 Web 设计稿复制 CSS。
7.6 图片加载
RN 端有时会给 <image> 加平台专有属性做性能优化,比如只在 iOS 开启某种缓存策略:
<image src="{{ url }}" enable-fast-image@ios="{{ true }}" />
这种写法要清楚:属性前面的 @ios 表示只在 iOS RN 产物里注入。复制别人的代码时,确认自己项目 Android 端走什么方案,别只优化了一端。
7.7 同组件按平台拆文件
差异大的时候,常见三种文件并存:
my-slider.mpx # 通用版(小程序 + Web)
my-slider.ios.mpx # iOS RN 特化
my-slider.android.mpx # Android RN 特化(如果需要)
脚本里也可能有平台分支:
const hitSlop = __mpx_mode__ === 'ios' ? 18 : 16
这种一般是在对触摸热区或像素对齐,不是随便写的 magic number。
八、输出 RN 时的混合开发
Mpx 模板能覆盖大部分 UI,但复杂手势、高性能动画有时必须直接用 React Native 的原生组件和库(Reanimated、Gesture Handler 等)。
通用做法:
第一步,单独建一个目录,封装 RN 原生组件的薄导出层:
// rn-components/animated-view.js
import Animated from 'react-native-reanimated'
export const AnimatedView = Animated.View
export { useAnimatedStyle } from 'react-native-reanimated'
第二步,在 .mpx 模板里通过条件编译替换标签名和属性:
<view
class="card"
style@ios|android="{{ cardStyle }}"
pointerEvents@ios|android="box-none"
mpxTagName@ios|android="AnimatedView"
>
<text>{{ title }}</text>
</view>
mpxTagName@ios|android:RN 端把<view>替换成 RN 原生组件style@ios|android:RN 端注入 RN 风格的 style 对象pointerEvents@ios|android:RN 专有属性@_wx|_ali:带下划线前缀表示该属性仍走 Mpx 的跨端转换
第三步,特化严重的场景直接写 .ios.mpx,在 script 里 import RN 库。这种文件只会在对应平台编译,不会污染小程序产物。
九、分包和按需加载
小程序项目大了以后必须分包。Mpx 里分包配置和小程序一致,在 app.mpx 的 json 或独立的 app.json 里声明 subPackages。
异步分包组件加载时,用户可能先看到空白。Mpx 提供 componentPlaceholder 做占位:
module.exports = {
usingComponents: {
'heavy-chart': '../components/heavy-chart',
},
componentPlaceholder: {
'heavy-chart': 'view', // 加载前先渲染一个 view 占位
},
}
这样分包组件还没下载完时,至少不会整块空白。
十、本地开发和调试
Mpx 项目的命令因脚手架和配置不同会有差异,但套路是固定的:
| 目标 | 常见命令 | 说明 |
|---|---|---|
| 微信小程序 | npm run watch 或 npm run watch:wx |
产物在 dist/wx,用微信开发者工具导入 |
| 支付宝等 | npm run watch:ali 等 |
产物在对应 dist 目录 |
| 跨端同时编译 | npm run watch:cross |
一次产出多个平台 |
| Web | 看项目是否配置了 web 模式 | 浏览器里先看布局和逻辑 |
| RN (iOS/Android) | 项目配置的 drn / rn 相关命令 | 需要 RN 开发环境,真机或模拟器调试 |
建议的调试顺序:
- 先在 Web 或微信小程序把页面逻辑和布局跑通
- 再上 RN 真机调样式和交互
- iOS 和 Android 各过一遍
卡在样式问题时,先确认是「CSS 写法 RN 不支持」还是「iOS/Android 表现差异」,两种排查方向不一样。
十一、踩坑备忘
- RN 页面滑不动 → 用
scroll-view,别指望页面级滚动 - 列表闪、顺序乱 → 检查
wx:key - 改了 CSS 类名 → 同步改模板和
createSelectorQuery里的选择器 - RN 的节点查询 → 用
#id或.class,模板节点加wx:ref - 模板里调方法 → 改用
computed - 直连
wx.xxx→ 改成mpx.xxx - 条件编译写太多 → 维护成本高,优先找全端统一写法
- 文本省略只改了 RN → 小程序端省略号丢失
- Stylus 嵌套没展开 → RN 编译可能报不支持的选择器
app.mpx的 pages 是空数组 → 分包配置异常- 只测一台真机 → 字体、阴影、安全区很容易漏
十二、小结
这篇主要整理了 Mpx 在实际项目里的通用做法:
- 用
.mpx单文件开发,结构是 template + script + style + json - 页面用
createPage,组件用createComponent,json 里注册子组件 - 跨端 API 走
mpx.xxx,差异大的逻辑用条件编译,能统一写的先统一写 - 输出 RN 时,样式约束最多:单类选择器、Flex 布局、文本省略和 1px 边框要双端分别处理
- iOS 和 Android 真机都要看,别只对着一个平台调
附录
- 官方文档:https://mpxjs.cn/
- GitHub:https://github.com/didi/mpx
- 建议 CSDN 标题:《Mpx 跨端开发实录(上):实际项目里模板、脚本与样式怎么写》
- 标签:Mpx、小程序、跨端、React Native
更多推荐




所有评论(0)