简介这是一份 Remax 跨平台小程序开发框架的完整资源包面向熟悉 React 并希望将同一套代码运行到微信、支付宝、头条等小程序平台的开发者。Remax 把真正意义上的 React 渲染到小程序环境中支持 React Hooks 与常用 React 生态采用静态编译方案让开发者不受原生小程序语法限制。资源包共 2000 个文件约 2.27MB包含 535 个 TypeScript 源文件、520 个 JavaScript 文件、153 个 TSX 组件、208 个 JSON 配置以及大量 AXML、ACSS 等小程序页面样式文件另有 Markdown 文档、EJS 模板和构建配置文件目录结构完整适合用作源码分析或二次开发参考。目前已有 413 人学习下载。通过该资源读者可以查看完整的 Remax 实现原理、多端适配层、类型定义和示例工程深入理解如何在小程序环境中复用 React 组件模型与 Hooks 机制。1. remax 的真面目你不是在写“小程序”你是在写 React如果你是一个写过三年 React 的老手第一次接手小程序需求大概率会被原生小程序那套Page({})、setData、模板语法给绕晕。更难受的是团队里可能同时要维护微信端、支付宝端和字节系小程序三份代码三个语法改一个字段要复制三次。remax 的切入点是反直觉的它不是在语法上“长得像 React”而是把一个真实的 React reconciler 跑在小程序运行时之上你的useState、useEffect、自定义组件全部是原汁原味的 React只不过渲染目标从 DOM 换成了小程序节点树。这意味着 React 生态里的状态管理库、组件逻辑、TS 类型推导都能直接搬进来同时产出多端代码。这套资源适合两类人一类是带着 React 技能栈想快速覆盖多端业务的前端工程师另一类是受够了“伪 React 语法糖”、想统一团队技术栈的架构负责人。它解决的核心问题不是“怎么写小程序”而是“怎么让小程序开发回归到一套早已成熟的心智模型”。2. 环境与工程模板从零跑通三端最小案例需要哪几样东西2.1 准备工程一条命令行生成的 baseline拿到 remax 资源后不要急着往现有项目里塞先单独建一个目录跑通最小闭环。我一般会直接生成一个基于 TypeScript 的模板因为小程序项目到后面页面多了纯 JS 的类型提示约等于没有改配置全靠脑子记翻车率极高。npx create-remax-app my-app --typescript cd my-app npm install npm run dev wechat第一行里的create-remax-app会拉一个标准模板带上remax、react、react-dom和对应平台的适配包。npm run dev wechat这步很关键它不会像普通 React 项目那样起一个本地开发服务器而是把源码实时编译成一个微信小程序工程目录你需要打开微信开发者工具把项目根目录指向编译产物目录。如果你要跑支付宝端就把命令换成npm run dev ali头条系是npm run dev toutiao。生成后的目录不复杂但每一层都需要理解src/app.ts定义全局运行时逻辑相当于小程序的App()src/app.config.ts是全局配置页面路由、窗口样式、tabBar 都在这里src/pages/index/每个页面一个文件夹内部是 React 组件和页面自己的配置dist/是编译产物开发者工具指向的就是它。我第一次跑的时候犯了个低级错误以为改了 src 后要手动重启 dev结果保存后dist一直不更新。后来看了下 remax 的构建实现它是文件监听模式保持npm run dev wechat常驻改完源码等一两秒编译产物会自动变化开发者工具里再点一下编译刷新就好。遇到产物不更新先别急着重启命令去node_modules/.cache看有没有残留缓存。2.2 理解构建产物三端是如何从同一份 React 代码变成小程序文件的很多人用 remax 最大的心理障碍是“黑匣子恐惧”觉得代码被框架转成什么都看不懂。其实它的编译链路可以拆成两步第一步Babel 把 TS/JSX 转成标准 JS第二步经过 remax 自定义的运行时适配层把 React 组件树的更新映射成小程序原生节点的增删改查。这个过程不是字符串层面的模板替换而是真实执行了 React 的虚拟 DOM 协调过程然后针对目标平台生成对应的setData调用。正因为是这个机制你能在 React 组件里用普通函数、类、渲染 prop甚至React.memo这些能力在传统转换型框架里很难实现因为他们连setState都要翻译成小程序逻辑。// src/pages/index/index.tsx import * as React from react; import { View, Text } from remax; export default function IndexPage() { const [count, setCount] React.useState(0); return ( View Text当前计数{count}/Text View onClick{() setCount(count 1)} style{{ padding: 20px, background: #f5f5f5 }} 点我加一 /View /View ); }这段代码里的View、Text不是小程序内置组件的 JSX 别名而是 remax 暴露的跨端组件封装。onClick在编译时会映射成当前平台的点击事件名称你在微信端收到的是bindtap支付宝端收到的是onTap但写代码的人完全不需要关心。参数说明上style接受的是 React 风格的对象写法驼峰属性会自动转换这一点和普通 React DOM 几乎没有差别。需要注意这并不代表你可以把小程序的wx.API 直接无脑换掉。网络请求、缓存、登录这些能力 remax 不强包装你得自己封装一个平台适配层最常见做法是写一个src/utils/platform.ts用环境判断切换平台 API。2.3 配置多端入口app.config 与页面配置全局配置这块最容易踩坑因为不同小程序的配置项名称不一样而且有些字段某一端不支持直接忽略框架也不会报错。remax 对配置文件的处理方式是支持.config.ts文件并且同一份配置可以写平台后缀例如app.config.wechat.ts、app.config.ali.ts优先级高于公共配置。// src/app.config.ts 公共配置 export default { pages: [pages/index/index, pages/detail/index], window: { navigationBarTitleText: 模拟项目X, navigationBarBackgroundColor: #ffffff, }, };// src/app.config.wechat.ts 微信端额外配置 export default { tabBar: { color: #999, selectedColor: #333, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/detail/index, text: 详情 }, ], }, };注意这里pages数组是两端共用的一旦某个页面只在支付宝端存在就必须在公共配置里保留否则构建产物的 app.json 会报缺少页面。window字段在微信端是全局窗口配置支付宝端会直接忽略不会报错。我通常的做法是公共配置文件只放所有端通用的页面列表和网络超时时间平台专属的导航栏样式、tabBar、分包配置全部放到带后缀的文件里。生产环境构建时建议用NODE_ENVproduction remax build platform产物会带压缩和 tree shaking。另外 remax 支持多端同时构建你可以开三个终端分别跑dev wechat、dev ali、dev toutiao每次改动源码三端产物同步更新这是定位跨端差异时最省时间的一招。3. 用真正的 React 写页面状态、生命周期和列表渲染的落地写法3.1 useState、useEffect 与小程序生命周期怎么对齐小程序原生开发里每个页面要自己在onLoad、onShow、onHide这些钩子里做数据加载和清理。remax 没有抛弃这些生命周期而是把它们封装成了 React 侧的 hooks同时保留useState的天然表达力。最常用的组合是usePageEvent监听页面级别的非渲染事件useState管理页面数据useEffect处理依赖响应的副作用。import * as React from react; import { View, Text } from remax; import { usePageEvent } from remax; export default function DetailPage() { const [id, setId] React.useState(); const [detail, setDetail] React.useState(null); // 页面首次进入时从 URL 参数解析数据 usePageEvent(onLoad, (query: Recordstring, string) { setId(query.id || ); }); React.useEffect(() { if (!id) return; // 模拟请求详情 setDetail({ name: 商品${id}, price: 99 }); }, [id]); return ( View {detail ? ( Text {detail.name}价格 {detail.price} 元 /Text ) : ( Text加载中/Text )} /View ); }这里usePageEvent第一个参数是事件名第二个是回调。参数的接收和原生小程序的options一致onLoad里的 query 对象就是页面跳转携带的参数。为什么使用useStateuseEffect而不是在onLoad里直接setDetail因为在 remax 里小程序页面的onLoad触发时机在 React 第一次渲染之后如果直接在里面调用setDetail有概率触发“渲染期间更新状态”的警告更规范的做法是把参数先存入 state再用useEffect响应它触发数据请求。这一套组合和 React Router 的 URL 参数管理模式几乎一致团队里 React 背景的人不需要重新学一套状态流。3.2 列表渲染与组件拆分从 map 到自定义组件通信列表页是跨端小程序最典型的场景。原生小程序列表用wx:for还要配wx:keyremax 里直接用数组map和 React 完全一样。需要指出的一个细节是remax 对key的处理虽然遵循 React 规则但它需要的是稳定唯一的字符串如果列表项是商品 ID直接传id就好不要因为偷懒用数组索引否则上拉加载更多时新增数据会触发全部列表刷新性能直接劣化。import * as React from react; import { View, Text, Image } from remax; import ProductCard from ../../components/ProductCard; interface Product { id: string; title: string; cover: string; price: number; } export default function ProductList() { const [products, setProducts] React.useStateProduct[]([]); React.useEffect(() { // 模拟接口返回 setProducts([ { id: p1, title: 无线耳机, cover: , price: 199 }, { id: p2, title: 蓝牙音箱, cover: , price: 299 }, ]); }, []); const handleTapProduct (product: Product) { // 跳转详情query 参数传给下一页 // 实际运行时使用当前平台的路由 API console.log(跳转商品, product.id); }; return ( View {products.map((item) ( ProductCard key{item.id} product{item} onTap{() handleTapProduct(item)} / ))} /View ); }自定义组件ProductCard接收一个product对象和一个onTap回调props 的传递不经过任何中间层就是原生 React 的函数传参。这里有一个和 Web React 不一样的点小程序的自定义组件默认有样式隔离父组件的 class 不会渗透到子组件内部组件内要自带完整样式如果确实希望子组件继承某些全局样式需要显式配置styleIsolation: apply-shared或设置options在 remax 里对自定义组件的config文件做平台相关处理时尤其要注意。3.3 hooks 在跨端场景下的边界副作用时机与页面事件跨端场景里最容易让人困惑的是“同一个 useEffect在不同端的触发时机不一样”。原因是 remax 要等页面级别的小程序生命周期稳定后才挂载 React 组件树微信端页面结构复杂时useEffect的触发会晚于该端的onReady事件。这就意味着如果某个副作用必须在小程序原生onReady之后再执行比如获取节点尺寸、操作 canvas不能只写在useEffect里必须配合usePageEvent。import * as React from react; import { usePageEvent } from remax; export default function CanvasPage() { const canvasRef React.useRefany(null); usePageEvent(onReady, () { // 此时原生 canvas 节点已渲染完成 const query canvasRef.current; if (query) { // 各端获取节点信息 API 不同由平台适配层封装 console.log(canvas ready); } }); React.useEffect(() { // 这里可以做纯数据初始化不依赖原生节点 }, []); return Canvas ref{canvasRef} style{{ width: 100%, height: 200px }} /; }这是 remax 与纯 Web React 最大的边界在浏览器里 DOM 节点一定和 React 渲染同步但在小程序里原生组件的挂载时机和 React 组件的 commit 时机不是同一时刻凡是依赖真实节点的操作都不能只靠useEffect。我的一个习惯是把“数据副作用”和“原生副作用”分开处理数据逻辑进useEffect原生命令式操作进usePageEvent对应的回调。这个小习惯能避免大量玄学问题。4. remax 常见问题排查三端翻车重灾区我踩过的 5 个坑4.1 样式隔离失效父组件 class 意外进入子组件现象在自定义组件里写了一个.card的类名结果页面其他部分的样式也被改掉或者反过来子组件样式怎么都被父组件覆盖。原因小程序各端对样式隔离的默认值不一致。微信端自定义组件默认隔离但页面可以作为“组件”放在某个父级时继承规则被打破支付宝端的默认隔离策略更宽松类名会互相穿透。remax 本身不修改这些端侧逻辑只负责把 className 编译成对应的节点 class。解决在自定义组件的.config.ts里显式声明样式隔离策略不要依赖默认值。微信端写export default { options: { styleIsolation: isolated, }, };支付宝端需要设置styleIsolation: true才能达到同样的隔离效果。整包资源里通常会带一份模板配置我建议直接在所有业务组件里统一加上避免上线后出现线上样式乱套的问题。4.2 列表加载后 setState 不生效页面一直白屏现象数据请求返回了console 里能打印出数据但页面 UI 始终不更新也没有报错。原因常见于直接在onLoad里调用setState而当时 React 组件尚未完成首轮渲染。remax 的多端运行时在微信端和支付宝端都曾出现过这种竞态setState发出去了但协调器还没准备好接收更新更新被静默丢弃。解决不要在任何小程序生命周期回调里直接setState。把生命周期回调里接到的参数存到useState再用useEffect监听派生数据请求。如果某些低版本端还是有问题可以加一个setTimeout兜底但这是不得已的临时方案最终还是要走useEffect链路。4.3 tabBar 页面来回切换状态丢失现象A 用户在 tab 页滚动到列表很深位置切到另一个 tab 再切回来页面回到顶部滚动位置和筛选条件全丢了。原因小程序 tabBar 页面默认不销毁但 remax 的 React 组件树会把页面状态保存在内存里如果页面组件里用了usePageEvent(onHide)去做数据清理或者父组件在页面切换时被条件渲染移除React 状态就会跟着销毁。解决公共状态提升到页面外层的 context 或全局 store 里tab 切换时只保存展示片段不要在onHide里重置数据。每次切 tab 的加载状态单独用一个visible字段控制而不是卸载页面组件。这个坑在原生小程序里也有但 remax 因为多了一层 React 协调处理不好更隐蔽。4.4 微信端运行正常支付宝端点击事件失效现象同一个组件在微信开发者工具里一切正常支付宝开发者工具里点按钮一点反应都没有控制台也不报错。原因绝大多数情况是事件名称差异。remax 虽然统一了onClick但某些自定义组件内部直接用了小程序原生的事件绑定比如onTap微信端兼容了tap支付宝端对tap的兼容处理在某些基础库版本上失效。解决全部事件都通过 remax 的onClick传不要自己拼onTap。如果是从老代码迁移过来的页面搜一下项目里有没有直接写onTap、bindtap的 JSX 属性统一替换掉。另外支付宝端的自定义组件事件在 props 里传函数时组件声明需要配置events字段remax 对此的处理不如微信端透明排查时先看dist产物里对应组件的 json 文件 events 是否完整。4.5 多端同时构建时产物互相污染现象开了dev wechat和dev ali两个终端改了几次代码后编译产物dist目录里出现微信端的 tabBar 配置跑到支付宝端的情况。原因这是一个很容易踩的工程配置坑。默认模板里dist是同一个目录多端同时写会导致产物互相覆盖。remax 的 CLI 支持指定输出目录但模板给的 script 没有区分。解决把 npm script 改成按平台输出到独立目录{ scripts: { dev:wechat: remax build wechat --output dist/wechat, dev:ali: remax build ali --output dist/ali, dev:toutiao: remax build toutiao --output dist/toutiao } }之后各端开发者工具分别指向对应平台目录相互之间完全不干扰。这个坑一旦碰上表现非常诡异你甚至会怀疑是状态管理库的 bug实际上是产物目录串了。从那以后我每次起步就在 npm script 里写死平台输出目录多端项目从来不给它一个共用 dist 的机会。5. 再进一步把 remax 工程改造成多端可复用组件库并验证产物5.1 从一个页面到一个组件库的拆分策略如果你只是把 remax 拿来做一个演示小程序那前面三章的写法已经足够。但真实业务里同一个团队往往要同时维护多个业务小程序比如一个面向 C 端的商品浏览、一个面向 B 端的门店管理。这时候最好把 remax 工程当成一个“组件库”来管理而不是一个个重复的页面集合。我的做法是在src/components下按业务域分组每个组件自带.config.ts和平台适配文件。组件内不要直接调用小程序平台 API而是通过src/utils/platform.ts里的封装函数。这个文件维护一份统一的接口签名内部用process.env.REMAX_PLATFORM判断当前编译平台再把调用转发到对应 API。这样组件代码完全不知道自己在哪个端跑换端不换代码。const request (url: string, options: RequestOptions) { const platform process.env.REMAX_PLATFORM; if (platform wechat) { return wx.request({ url, ...options }); } if (platform ali) { return my.request({ url, ...options }); } // 其他端类似 };这个封装的收益不是当下而是在几个月后新增一个端时你只需要在这个文件里加分支业务组件和小程序页面一行都不用改。很多同事一开始嫌麻烦不愿意做这层抽象等到双端调试时才发现凡是直接调平台 API 的代码全都要写双份 if-else后悔药只能自己吞。5.2 验证产物不能只看开发者工具还要查构建产物细节组件库跑通后验证环节是必不可少的。开发者工具编译通过并不代表产物是对的。我建议每次发布前做四步验证第一步检查dist对应平台目录下的app.json、页面配置和组件 json 的字段是否完整第二步把产物里的 JS 文件格式化搜索业务代码里是否混入了某个平台才能识别的全局变量比如wx.出现在支付宝产物里就是适配层没做好第三步用开发者工具的真机预览扫一遍关键页面不只是看着不报错还要看接口请求和 setData 的频率第四步检查包体积微信端主包大小限制很严格如果主包包含图片和长列表数据很容易踩 2MB 限制该拆分包就拆分包。拆分包的配置和原生小程序几乎一致在app.config.ts里加subpackages字段然后 remax 的构建会按src/pages下的路由结构自动分包。注意分包页面不能引用主包外的自定义组件否则构建会报错。组件库模式里公共组件应该放在src/components下且被主包引用各分包新增页面尽量用自己的业务组件。5.3 把资源落地为团队日常习惯最终这份 remax 资源包真正价值不在于跑通一个 demo而在于给团队一套可持续的多端开发基线。我会把模板里的 npm scripts、目录结构、平台封装文件、样式隔离配置都固化为团队项目的标准模板每次新起项目不再从零搭而是直接拷贝这份工程结构。同时要求所有成员在同一个工程里增加新端时必须先更新platform.ts再写业务页面避免平台 API 直接散落到业务层。从那以后我每次接手多端小程序项目都会强制走一遍这套流程先独立目录跑通三端构建再配置独立产物目录再写平台封装层最后才开始写业务代码。顺序反了后面调试成本会指数级上升。这套 remax 资源里已经包含了脚手架工程、配置示例和踩坑记录照着落地能少走很多弯路希望帮到你。本文还有配套的精品资源点击获取