最近在做 Mpx 多端开发(小程序 + Web + RN),官方文档能告诉你「能写什么」,但真正上手还是会卡在「实际项目里该怎么组织、怎么写才不出事」。这篇文章是我把文档和踩坑经验合在一起整理的结果,尽量只讲通用的东西,换任何一个 Mpx 项目应该都能对照着看。

一、Mpx 是什么

Mpx 是滴滴开源的小程序跨端框架。核心思路很简单:不推翻小程序原生写法,在上面做增强,让你用接近 Vue 的体验写代码,然后编译到微信/支付宝/百度等小程序、Web,以及 React Native。

原生小程序一个页面四个文件:wxmljswxssjson。Mpx 用 .mpx 单文件把它们收进一个文件里:

区块 对应原生 作用
<template> wxml 视图模板
<script> js 逻辑
<style> wxss 样式
<script name="json"> json 页面/组件配置

除了单文件,Mpx 还增强了这些能力(文档里都有,实际开发高频用到的):

  • 数据响应:datacomputedwatch,以及组合式 API(setupref
  • 模板增强: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 放可复用组件,这是最基础的。

按功能模块分:页面多了以后,会拆成多个子目录或分包,每个模块下面有自己的 pagescomponents,公共能力抽到顶层的 componentsutilsstore

应用入口 app.mpx:一般就两件事,createApp({}) 注册应用,json 区块里声明 pages 或分包。这里有个小坑:pages 不要写成空数组,否则分包配置可能出不来,微信开发者工具表现会很怪。

构建产物:跑 npm run watchnpm run build 之后,在 dist/wxdist/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-propwx: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 端页面默认不滚动。onReachBottomonPageScroll、下拉刷新在 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.requestmy.xxx,Web 和 RN 构建很容易挂。这是跨端项目的基本规矩。

5.2 生命周期

页面和组件的生命周期和小程序基本一致:onLoadonShowonReadyonHideonUnload 等。

组合式 API 里对应的是 onMountedonUnmounted 等。注意:输出 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')
    },
  },
})

页面或组件里通过 mapStatemapMutations 或直接 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 单位

优先 rpxpx,少用 rememfont-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 上可能不生效,改成 boldnormal 更稳。

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 watchnpm run watch:wx 产物在 dist/wx,用微信开发者工具导入
支付宝等 npm run watch:ali 产物在对应 dist 目录
跨端同时编译 npm run watch:cross 一次产出多个平台
Web 看项目是否配置了 web 模式 浏览器里先看布局和逻辑
RN (iOS/Android) 项目配置的 drn / rn 相关命令 需要 RN 开发环境,真机或模拟器调试

建议的调试顺序:

  1. 先在 Web 或微信小程序把页面逻辑和布局跑通
  2. 再上 RN 真机调样式和交互
  3. iOS 和 Android 各过一遍

卡在样式问题时,先确认是「CSS 写法 RN 不支持」还是「iOS/Android 表现差异」,两种排查方向不一样。

十一、踩坑备忘

  1. RN 页面滑不动 → 用 scroll-view,别指望页面级滚动
  2. 列表闪、顺序乱 → 检查 wx:key
  3. 改了 CSS 类名 → 同步改模板和 createSelectorQuery 里的选择器
  4. RN 的节点查询 → 用 #id.class,模板节点加 wx:ref
  5. 模板里调方法 → 改用 computed
  6. 直连 wx.xxx → 改成 mpx.xxx
  7. 条件编译写太多 → 维护成本高,优先找全端统一写法
  8. 文本省略只改了 RN → 小程序端省略号丢失
  9. Stylus 嵌套没展开 → RN 编译可能报不支持的选择器
  10. app.mpx 的 pages 是空数组 → 分包配置异常
  11. 只测一台真机 → 字体、阴影、安全区很容易漏

十二、小结

这篇主要整理了 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
Logo

一站式 AI 云服务平台

更多推荐