简介:这是一份面向React开发者的Remax跨平台小程序框架源码包,专注于解决React生态无法直接用于微信、支付宝、头条等小程序开发的问题。Remax将React运行时完整引入小程序环境,组件渲染、生命周期与Hooks等能力均可按React习惯使用,再通过静态编译把同一套代码转换到多端,适合想复用React技能栈、又需兼顾多平台交付的初中高级前端开发者。压缩包共2000个文件,大小约2.27MB,以TypeScript、JavaScript和TSX源码为主,同时包含JSON配置、AXML/ACSS平台文件、MD说明文档、EJS构建模板等,完整覆盖源码实现、类型定义、示例工程与构建配置。目前已有411人学习下载。借助这套源码与文档,可以深入理解Remax在运行机制、多端适配、编译链路及TypeScript类型封装上的设计,既可直接指导实际项目开发,也能作为小程序框架二次改造或原理分析的学习蓝本。 写小程序跨端方案的文章不少,但大部分讲透原理的文章是真的少。我这个项目用 Remax 做了一款覆盖微信、支付宝、字节系小程序的生产级应用,从搭建到上线踩了不少坑。这篇文章不聊虚的,直接拆解 Remax 的核心机制、关键配置和实战经验,给准备选型或者已经入坑的朋友一些参考。
1. 为什么选择 Remax:React 运行时直出小程序
先说结论:Remax 和 Taro 2.x 这类编译时方案有本质区别。Taro 2.x 是将 JSX 编译成小程序原生语法,写的是 React 风格的代码,但运行时并没有 React 参与。Remax 走的是另一条路——它将 React 直接跑在小程序环境里,通过自定义 reconciler 让 React 的虚拟 DOM 树与小程序的自定义组件树建立映射关系。换句话说,在 Remax 里,你写的 React 组件就是真正的小程序组件,React 的完整生命周期、Hooks、Context 都是真实可用的,而不是被阉割过的模拟实现。
用一句话向团队解释这个差异:Taro 2.x 像是把中式菜谱翻译成西式做法,虽然也能做出中国菜的味道,但用的工具和流程都是西式的;Remax 则直接把中国厨师请进了西餐厅,厨师的技法、习惯统统保留,只是最后装盘方式按餐厅规矩来。
选择 Remax 还有一个现实层面的原因:React 生态实在太庞大了。团队里有不少同事对 Vue 更熟,但项目核心成员都是 React 背景,使用 Remax 可以直接复用已有 React 组件和业务逻辑层代码,不需要再学一套 DSL。对需要快速迭代、多端发布的小程序项目来说,这个成本优势非常明显。
2. 核心原理拆解:从 React Fiber 到小程序自定义组件
2.1 reconciler 与 host component 的映射逻辑
Remax 的核心思路并不复杂。React 官方提供了一套react-reconciler,允许你自定义“宿主环境”,也就是让 React 的虚拟 DOM 可以渲染到任意目标上。Remax 做的就是实现一套小程序的 host config:当 React 渲染出一个<view>组件时,Remax 会创建一个小程序自定义组件并挂载到组件树里。
这个过程的简化代码大致是这样:
import ReactReconciler from "react-reconciler"; const hostConfig = { createInstance(type, newProps, rootContainerInstance) { // 在小程序里创建一个自定义组件/元素实例 const element = createMiniProgramElement(type, newProps); return element; }, appendInitialChild(parentInstance, child) { // 将子节点挂载到父节点 parentInstance.appendChild(child); }, commitUpdate(instance, updatePayload, type, prevProps, nextProps) { // 处理属性更新,最终会调用 setData 更新到视图层 instance.setProps(nextProps); }, // ... 其他必要生命周期方法 }; export const reconciler = ReactReconciler(hostConfig);关键点在于commitUpdate:React 协调完成后,Remax 会把差异同步给小程序,小程序再通过setData渲染。这也就是为什么 Remax 应用有时候会被评为“性能一般”,因为多了一层虚拟 DOM 到小程序组件的映射和更新队列处理。但实际优化后,体感几乎没有差异。
2.2 真的能跑 React Hooks 和 Context 吗
能。这是 Remax 和编译时方案最大的区别。因为 React 运行时是真实存在于小程序里的,useState、useEffect、useContext都是原生的 React API,不需要编译成小程序生命周期函数。
以 Hooks 为例:
import React, { useState, useEffect } from "react"; import { View, Text } from "remax/wechat"; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { console.log("count 变了:", count); }, [count]); return ( <View> <Text>当前计数:{count}</Text> <Text onTap={() => setCount(count + 1)}>加一</Text> </View> ); }这段代码在小程序里跑,useState的更新会触发 Remax 的数据同步流程,最终更新到页面。它不需要被编译成Page({ data: { count: 0 } })的形式,也不需要开发者手动处理setData调用。这个开发体验和 Web React 几乎没有区别。
2.3 各小程序平台差异怎么抹平的
Remax 提供了一套统一的组件规范,底层按平台适配。你在代码里写<View>、<Text>、<Image>,Remax 会按当前编译平台映射成微信的wx-view或支付宝的ali-view。这部分逻辑封装在remax/<platform>的入口包里。
不过要注意的是:统一组件是“最大公约数”策略,各平台的独特组件和 API 还是得靠条件编译或平台文件区分。比如微信的open-data、支付宝的contact-button,这些只能按平台写。Remax 支持创建Component.wechat.jsx、Component.alipay.jsx这类后缀名文件,构建时自动选择对应文件。这是官方推荐的方式。
3. 项目搭建与关键配置全记录
3.1 初始化项目的两种姿势
Remax 官方脚手架目前维护得不错,创建项目非常快:
# 使用 create-remax 脚手架 npx create-remax@latest my-remax-app # 进入项目并安装依赖 cd my-remax-app npm install # 启动微信小程序开发模式 npm run dev脚手架会询问你要支持哪些平台、是否需要 TypeScript。如果你用 VS Code 开发,建议直接选 TypeScript 模板,配合类型提示能大大减少低级错误。
另一种姿势是手动在现有项目里集成 Remax。这种适合改造存量 H5 项目或已有 React 项目。你需要安装remax、react、react-dom等依赖,然后在remax.config.js里配置入口和平台。手动集成要处理构建配置、全局样式、路由等琐碎问题,不推荐新手一开始就这么干。
3.2 remax.config.js 里的核心配置项
一个典型的配置长这样:
module.exports = { // 项目入口,默认 src 目录 root: "src", // 构建目标平台 targets: ["wechat", "alipay"], // 是否启用 CSS Modules cssModules: true, // 开发阶段的代理配置 devServer: { proxy: { "/api": { target: "http://localhost:3000", changeOrigin: true, }, }, }, };cssModules我强烈建议开启。小程序项目的样式作用域问题很严重,原生写法一不小心就全局污染。开启 CSS Modules 后,每个组件的样式都是隔离的,配合classNames这样的工具库可以优雅处理多类名拼接。
此外,如果你需要修改小程序原生配置,比如app.json的窗口背景色、页面注册列表,可以在src/app.config.js里导出一个对象。Remax 构建时会将这个对象转成对应平台的小程序全局配置。这种“以 JS 配置驱动原生配置”的模式很符合 React 习惯。
3.3 微信开发者工具的正确打开方式
Remax 开发模式下会生成一个dist目录,微信开发者工具里导入这个目录就能预览。这里有个常见坑:微信开发者工具默认开启 ES6 转 ES5,但 Remax 构建出来的代码已经经过 Babel 处理,你再让开发者工具转一次会有概率出现语法不兼容的报错。正确做法是在开发者工具的“本地设置”里,把“ES6 转 ES5”关闭,同时把“增强编译”关闭,避免二次处理。
另外,如果你的代码里用到较新的 JavaScript 特性,需要确认 Remax 的 Babel 配置是否包含对应的 preset。脚手架默认支持到 ES2020 附近,如果项目里有更高的语法需求,自行在babel.config.js里补@babel/preset-env的useBuiltIns按需 polyfill。
4. 从 Web React 转到 Remax 的实战经验
4.1 组件库与事件绑定要点
Remax 的组件事件绑定时要注意两点:事件名的差异和事件对象的结构。在微信小程序里,点击事件是onTap,但在 Remax 里你既可以用onTap也可以用onClick(Remax 会自动映射)。源码内部有事件别名映射表,不用死记平台差异。
事件对象也有讲究。小程序的事件对象和 Web 的SyntheticEvent不完全一样。比如event.currentTarget.dataset在 Web 里是dataset,在小程序里也能用,但如果你需要在事件里取到很多自定义参数,更推荐提前绑定:
<View onTap={() => handleDetail(item.id)}>查看</View>闭包绑定性能略差,但代码可读性高。列表项特别多时,改用数据驱动的方式传递参数,减少闭包创建频率。这是项目卡顿后的一个性能优化点。
4.2 状态管理选型:Redux Toolkit 为核心
跨页面的数据共享必须用状态管理库。Remax 官方没有强制方案,你可以在项目里用 Redux Toolkit、Mobx,甚至自带的小程序全局变量getApp().globalData。
我的建议是:业务规模中等以上直接上 Redux Toolkit,配合react-redux使用。因为 Remax 的多平台编译能力决定了你的业务层代码最好 Agnostic,Redux 作为跨端状态管理方案非常成熟,将来这套状态逻辑迁移到 Web 或 React Native 也能复用,把状态层和视图层解耦,对后续扩展来说容错率更高。
Redux Toolkit 的createSlice写起来非常简洁:
import { createSlice } from "@reduxjs/toolkit"; const cartSlice = createSlice({ name: "cart", initialState: { items: [] }, reducers: { addItem(state, action) { state.items.push(action.payload); }, removeItem(state, action) { state.items = state.items.filter( (item) => item.id !== action.payload.id ); }, }, }); export const { addItem, removeItem } = cartSlice.actions; export default cartSlice.reducer;日常开发里,页面渲染、网络请求都从 Redux 里读取,写起来比逐个 setData 省心得多。
4.3 生命周期钩子处理技巧
小程序页面的onShow、onHide、onReachBottom等生命周期,在 Remax 里通过usePageEvent这种 hook 绑定:
import { usePageEvent } from "remax"; export default function Home() { usePageEvent("onShow", () => { // 页面显示时拉取最新数据 refreshList(); }); usePageEvent("onReachBottom", () => { // 触底加载更多 loadMore(); }); return <View>...</View>; }这个 hook 的设计很优雅,一个页面多个函数可以调用多次usePageEvent监听同一种事件,不会互相覆盖。对于需要在不同生命周期里加载数据的场景,比原生Page({...})的方式更符合 React 逻辑:让每个组件自己负责自己的数据获取,不要所有逻辑都堆在页面顶层。
小程序列表页常用的onPullDownRefresh下拉刷新也可以这样处理。但要记得,小程序原生的enablePullDownRefresh配置项需要在page.config.js里配置,否则事件不触发。
4.4 样式编写:从 CSS Modules 到设计系统
开启 CSS Modules 后,样式文件的写法需要注意:类名引用要用styles.xxx的方式,不能用字符串直接拼。局部类名和全局类名混用的情况,在项目里很常见。
我的习惯是:所有视觉基础变量(颜色、间距、字号)定义在src/styles/variables.css,所有组件通过:global引用这些变量,或者引入 CSS 自定义属性。这样换主题时只需改全局样式文件。
原子化 CSS 方案在小程序里也可以跑,比如windicss或tailwindcss,但需要额外配置 PostCSS 插件。我建议慎重:小程序包体积限制很严格,原子化 CSS 会带来大量无用样式。真想要一套设计规范,不如自己建一个小组件库(Button、Card、Tag、Loading),比引整个 UI 框架更轻量可控。
5. 跨平台适配与常见问题排查实录
5.1 “我编译出 4 个平台的包,但支付宝 UI 偏了”——平台差异排查思路
多端发布后,样式差异几乎是必然会遇到的问题。排查思路先分清是样式加载失败还是组件映射异常。我遇到过支付宝小程序不支持某些 CSS 属性(例如部分旧的position: sticky兼容问题),微信里表现正常,支付宝直接失效。
解决办法是:优先按平台文件拆分,而不是用 CSS hack。
src/components/List.jsx # 公共逻辑 src/components/List.wechat.jsx # 微信端特有实现 src/components/List.alipay.jsx # 支付宝端特有实现构建时 Remax 会自动按当前平台选择对应的文件后缀,这是最干净的平台差异处理方式。所有平台共用的业务逻辑写在无后缀文件里,平台特有细节按组件粒度拆分。千万不要写大段if (process.env.REMAX_PLATFORM === 'wechat')做逻辑判断,维护成本会指数级增长。
另一个高频坑是路由参数解析。微信的query参数在页面配置里是options.id,支付宝某些场景下是options.foo,但你用usePageQuery时两个平台的返回结构并不完全一样。我习惯在封装的usePageQueryhook 里做个字符串化处理,所有平台统一返回字符串类型的 key-value 对象。
5.2 “setData 数据量太大,页面疯狂卡顿”——性能优化方案
Remax 的运行时方案有一个天然劣势:组件树结构越复杂,setData的数据量越大。因为 Remax 需要维护一棵“虚拟组件树”来驱动小程序渲染,当页面状态更新时,它要把整个节点的 diff 结果同步到视图层。大量调用setData会导致线程通信阻塞。
实践下来,三个优化手段最有效:
第一,减小组件粒度。一个大页面拆成多个独立组件,每次更新只触发部分组件的 diff。相当于减少每次 setData 的 payload 大小。
第二,避免频繁更新全局状态。比如拖动进度条、高频轮询请求,不要一帧一个 setData。把更新节流到 16fps 已经够用:
let timer = null; function throttledSetState(fn) { if (timer) return; timer = setTimeout(() => { timer = null; fn(); }, 100); }第三,用memo包裹纯展示组件。只要 props 不变,React 就不会重新协调这个子树,也就不会触发额外 setData。这一点是 React 性能优化在小程序端的自然迁移,效果很明显。
5.3 “进入页面白屏,控制台也没有报错”——生命周期时机排查
白屏问题在小程序里很难查。一个常见的场景是:在usePageEvent('onLoad')里请求数据,请求完成回调里更新状态、渲染页面,但小程序首屏已经渲染过一次。如果setData时机不对,页面可能会出现先空白、再显示内容的情况。
我的做法是在全局加一个isReady开关,数据还没有返回之前显示骨架屏组件,返回后展示真实内容,而不是让页面裸奔。另外记得所有异步更新在页面卸载后要做清理:
useEffect(() => { let mounted = true; fetchData().then((res) => { if (mounted) { setList(res.data); } }); return () => { mounted = false; }; }, []);如果项目里用到多个定时器或者长连接,也务必在页面销毁时清理。Remax 页面销毁时并不会自动销毁所有定时器,这一点和 Web 端 SPA 的 HMR 机制不一样,需要手动解除引用。
5.4 “接口返回数据后,页面没有更新”——状态更新穿透性问题
这种问题在用过react-redux的情况下比较容易排查:Reducer 是否写对了、createSlice的 reducer 是否嵌套太深。最常见的是用了 Immer 的 draft 写法,但在 mutation 时不小心用了展开运算符覆盖掉整个 state,导致引用变了但页面组件没有重新渲染。
排到组件层面,我会在页面顶层输出console.log('render', pageData),先确认页面是否进入渲染流程;如果进入了但数据没更新,再去查样式选择器是不是被缓存,比如 CSS Modules 类名失效导致界面看起来没变化。
如果是团队协作出问题,大概率是有人在组件里直接修改了 redux 里的引用类型数据,导致 React/redux 无法检测到变化。强制团队遵守 immutable 更新规范,或者统一走createSlice的 reducer,能省掉交流成本。
6. 构建部署流程与脚手架设计心得
6.1 多环境构建配置
小程序项目至少需要区分开发、测试、生产三个环境。每个环境有各自的appid、接口域名和第三方推送凭证。在 Remax 里我习惯用.env文件管理:
# .env.development APP_BASE_URL=https://dev-api.example.com APP_APPID=wxdev123 # .env.production APP_BASE_URL=https://api.example.com APP_APPID=wxprod456然后在remax.config.js里按环境注入参数。一个小技巧是:在页面里通过process.env.APP_BASE_URL访问这些变量,这样构建时会自动替换成对应环境的值,方便多环境发布时不必手动改代码。
6.2 自动化 CI/CD 链路
小程序发布流程较长,建议引入 CI/CD。我的做法是:代码推送到 main 分支,触发 Jenkins 构建,按npm run build:wechat生成dist目录;然后通过微信官方命令行工具把产物上传到微信后台,形成预览二维码。
有条件的团队可以接微信的“小程序代码上传”接口,但官方接口需要appid和私钥。没有条件就简化成:构建成功后,把dist包给测试人员导入微信开发者工具,手动上传。
发布前一定要跑自动化测试,特别是涉及支付、登录的主链路。小程序端自动化用miniprogram-automator可以驱动微信开发者工具做点击和断言。虽然配置过程有一点点繁琐,但上线前能自动跑一遍核心流程,对稳定性提升是非常明显的。
6.3 包体积优化经验谈
微信小程序的包体限制是主包不超过 2MB,整个项目所有分包不超过 20MB。Remax 自带 React 运行时,天生比原生小程序大不少,因此分包策略很关键。
常用策略:
- 把体积大的页面拆到分包目录,主包只保留启动页、首页、TabBar 页面。
- 图片资源尽量全部走 CDN,少放本地图片。
- 第三方依赖按需引入,比如
lodash用lodash-es并配置 tree-shaking,减小最终 bundle。 - 定期检查 bundle 构成,可以用
webpack-bundle-analyzer分析。
某个版本里我不小心引入了 echarts 全量包,直接导致主包体积超过限制。换成按需模块引用后,体积从 1.8MB 降到了 400KB。遇到体积超限别慌,先分析构成,再对症下药。
7. 一个完整的购物车模块实现示例
这一节用一个贴近业务的购物车模块,把前面说到的技术点串起来。假设我们的小程序是一个简单的电商应用,购物车需要支持跨页面读取、修改数量、删除商品,并在 TabBar 上显示徽标。
7.1 使用 Redux Toolkit 管理购物车
import { createSlice } from "@reduxjs/toolkit"; const cartSlice = createSlice({ name: "cart", initialState: { items: [], totalCount: 0, totalPrice: 0, }, reducers: { addItem(state, action) { const index = state.items.findIndex( (item) => item.id === action.payload.id ); if (index >= 0) { state.items[index].count += 1; } else { state.items.push({ ...action.payload, count: 1 }); } cartSlice.caseReducers.calculate(state); }, decreaseItem(state, action) { const index = state.items.findIndex( (item) => item.id === action.payload.id ); if (index >= 0) { state.items[index].count -= 1; if (state.items[index].count <= 0) { state.items.splice(index, 1); } } cartSlice.caseReducers.calculate(state); }, clearCart(state) { state.items = []; state.totalCount = 0; state.totalPrice = 0; }, calculate(state) { state.totalCount = state.items.reduce( (sum, item) => sum + item.count, 0 ); state.totalPrice = state.items.reduce( (sum, item) => sum + item.count * item.price, 0 ); }, }, }); export const { addItem, decreaseItem, clearCart } = cartSlice.actions; export default cartSlice.reducer;calculate被复用了多次,这种在 createSlice 内部用caseReducers调用的模式写起来很顺手。注意:不要在 reducer 里做异步操作,异步逻辑要放到 dispatch 之前的业务代码里。
7.2 连接组件与全局状态
import React from "react"; import { View, Text, Button } from "remax/wechat"; import { useSelector, useDispatch } from "react-redux"; import { addItem, decreaseItem } from "@/store/cartSlice"; export default function CartPage() { const { items, totalPrice } = useSelector((state) => state.cart); const dispatch = useDispatch(); return ( <View className="cart-page"> {items.map((item) => ( <View className="cart-item" key={item.id}> <Text>{item.name}</Text> <Text>{item.price} 元</Text> <Button onTap={() => dispatch(decreaseItem(item))}>-</Button> <Text>{item.count}</Text> <Button onTap={() => dispatch(addItem(item))}>+</Button> </View> ))} <View className="checkout-bar"> <Text>合计:{totalPrice} 元</Text> </View> </View> ); }useSelector按需订阅、按引用比较触发更新,在小程序这种高频交互的场景下比connect高阶组件更精准。每减少一次不必要的渲染,就减少一次setData,体感就更快一点。
7.3 在 TabBar 上展示购物车数量徽标
TabBar 的徽标不能直接用 React 渲染,因为它是原生的tabBar配置项。一般要用wx.setTabBarBadge这个 API:
import { usePageEvent } from "remax"; usePageEvent("onShow", () => { const totalCount = useSelector((state) => state.cart.totalCount); if (totalCount > 0) { wx.setTabBarBadge({ index: 2, text: String(Math.min(totalCount, 99)), }); } else { wx.removeTabBarBadge({ index: 2 }); } });你会发现,Remax 与原生 API 完全兼容,只是外面包了一层 React 数据闭环。要调微信特有 API 时直接写wx.全局对象即可。这个能力让团队里两个只用过原生小程序的同事也很有安全感。
8. 经验总结与避坑指南清单
最后把这段时间实战踩坑换来的经验浓缩成一份清单。这份清单适用于准备上 React 技术栈做小程序,以及已经在 Remax 项目里挣扎的团队。
| 场景 | 建议做法 | 踩坑经验 |
|---|---|---|
| 选型 | 团队精通 React,多端发布,代码复用优先 | 别选编译时方案,遇到 React 新特性会很痛苦 |
| 项目初始化 | 用脚手架,开启 TypeScript,开启 CSS Modules | 手动集成容易漏配置,排查成本高 |
| 状态管理 | Redux Toolkit 或 Mobx,按模块拆分 | 别在 reducer 里做异步,保持纯函数 |
| 样式方案 | CSS Modules + 全局变量文件 | 类名默认 scoped,全局类名要主动加 :global |
| 平台差异 | 按文件后缀拆分,少写运行时判断 | 共用逻辑无后缀,平台特例按后缀覆盖 |
| 性能优化 | 拆组件、memo 包裹、setData 节流 | 高频 setData 是卡顿主因 |
| 生命周期 | usePageEvent + useEffect,卸载前清理 | 定时器和订阅不清理会内存泄漏或状态错乱 |
| 发布 | 多环境配置、CI/CD 上传、核心链路自动化测试 | 记得关微信开发者工具的二次 ES6 编译 |
| 包体积 | 按需引入三方库、图片 CDN、主包分包 | 定期用 bundle analyzer 检查依赖 |
再补一个容易踩的坑:Remax 不推荐硬改dist目录下的代码,因为每次 build 都会覆盖。真要改原生行为,要么通过配置项,要么别用 Remax。
这套方案跑了大半年,线上稳定,迭代效率确实比原生小程序的“页面逻辑堆叠”模式高一个档次。如果你玩过 Remax,或者有其他小程序跨端方案的经验,也欢迎多交流,互相补补课。
本文还有配套的精品资源,点击获取