一个基于 Babel 的 JSX 微信小程序开发框架,让你用 React 风格的 JSX 语法和 Hooks API 编写微信小程序。
- JSX 语法 — 用熟悉的 JSX 编写 WXML 模板,支持条件渲染、列表渲染、事件绑定
- Hooks API —
useState、useEffect、useContext、usePageEvent、useAppEvent等 React 风格 Hooks - API Promise 化 — 内置
promisify工具函数,将小程序回调 API 转换为 Promise,支持 async/await - 两种编程范式 — 支持函数式组件(Hooks)和 Options API(传统小程序 Page/Component 配置)
- 环境变量注入(编译时替换) — 零依赖、无运行时开销;支持
.env文件、系统RSMAX_*环境变量、rsmax.config.js的define三层来源,process.env.XXX编译时替换为字面量 - CSS Modules —
.module.less/.module.css/.module.scss自动局部作用域,class 名自动 hash - 样式预处理 — 内置 Less/Sass 支持,px 自动转 rpx(1px → 1rpx,按 750rpx 设计稿)
- 第三方 UI 库 — 自动识别并注册 Vant Weapp、TDesign MiniProgram、Ant Design Mini 组件
- npm 包支持 — ES6
import自动转为 CommonJSrequire() - 状态管理 — 类 Zustand 的轻量级状态管理
@rsmax/store,支持微信缓存持久化 - 国际化(i18n) — 基于 JS 模块的多语言支持,编译器按需加载,JSX 中
t('key')自动转为 WXML 数据绑定 - WXS 模块支持 — 通过
import引用外部.wxs文件,编译器自动注入 WXML 标签并复制文件 - 静态资源 —
public/目录下的文件直接复制到产物根目录,支持绝对路径引用 - 监听模式 —
rsmax dev监听文件变化,增量编译 - miniprogram_npm 保护 — 构建时自动保留微信开发者工具生成的
miniprogram_npm目录
- Node.js >= 20
- pnpm >= 10(本项目使用 pnpm workspace)
- 微信开发者工具
pnpm add rsmaxyour-project/
├── src/ # 源码目录
│ ├── app.js # App 入口
│ ├── app.json # 小程序配置
│ ├── app.wxss # 全局样式
│ ├── pages/
│ │ ├── index/
│ │ │ ├── index.jsx # 页面逻辑 + JSX 模板
│ │ │ ├── index.less # 页面样式(或 .wxss/.css/.scss)
│ │ │ ├── index.module.less # CSS Modules 样式
│ │ │ └── index.json # 页面配置(可选)
│ │ └── ...
│ └── components/ # 自定义组件
├── locales/ # 多语言文件目录(可选)
│ ├── zh-CN.js # 中文语言包
│ └── en.js # 英文语言包
├── public/ # 静态资源目录(可选,与 src/ 同级)
│ ├── icon.png # → dist/icon.png
│ └── images/
│ └── logo.png # → dist/images/logo.png
├── project.config.json # 微信开发者工具项目配置
├── package.json
└── rsmax.config.js # rsmax 配置(可选)
关键配置项(确保 npm 构建正确):
{
"miniprogramRoot": "dist/",
"setting": {
"es6": false,
"postcss": false,
"minified": false,
"packNpmManually": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./dist/"
}
]
}
}rsmax build <source> -o <output> [-m, --mode <mode>]将源码编译输出到 dist 目录。编译前会清空输出目录,但保留 miniprogram_npm。
选项:
| 参数 | 别名 | 说明 | 默认值 |
|---|---|---|---|
-o, --output <output> |
- | 输出目录 | dist |
-m, --mode <mode> |
- | 环境模式(development/production/test 等任意自定义),决定加载的 .env.<mode> 文件和注入的 process.env.NODE_ENV/MODE 值 |
production |
# 生产环境构建(默认)
rsmax build src -o dist
# 预发环境构建
rsmax build src -o dist -m stagingrsmax dev <source> -o <output> [-m, --mode <mode>]监听源文件变化,增量编译。
选项:
| 参数 | 别名 | 说明 | 默认值 |
|---|---|---|---|
-o, --output <output> |
- | 输出目录 | dist |
-m, --mode <mode> |
- | 环境模式,决定加载的 .env.<mode> 文件 |
development |
# 开发模式(默认 mode=development)
rsmax dev src -o dist
# 开发模式 + 使用预发环境接口
rsmax dev src -o dist --mode stagingrsmax clean [output]清理输出目录
import { useState, useEffect } from '@rsmax/runtime';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('mounted, count:', count);
return () => console.log('unmounted');
}, [count]);
const increment = () => setCount(count + 1);
return (
<view class="container">
<text>Count: {count}</text>
<button onClick={increment}>+1</button>
</view>
);
}export default {
data: {
todos: []
},
addTodo() {
this.setData({ todos: [...this.data.todos, 'New item'] });
},
render() {
return (
<view>
{this.data.todos.map(item => (
<text key={item}>{item}</text>
))}
<button onClick={this.addTodo}>Add</button>
</view>
);
}
};rsmax 完整支持微信小程序的分包加载机制,包括普通分包和独立分包(independent subpackages)。你只需按照微信小程序的标准规范在 app.json 中声明 subPackages(或 subpackages),rsmax 编译器会自动处理:
- 分包内页面/组件的 JSX 编译
- 运行时文件(
rsmax-runtime.js、rsmax-store.js、rsmax-i18n.js)的相对路径计算 - 普通分包复用主包运行时,不额外拷贝运行时文件
- 独立分包自动在分包根目录独立拷贝运行时文件
在 app.json 中声明分包:
{
"pages": [
"pages/index/index"
],
"subPackages": [
{
"root": "packageA",
"pages": [
"pages/detail/index",
"pages/list/index"
]
},
{
"root": "packageB",
"pages": [
"pages/home/index"
],
"independent": true
}
]
}src/
├── app.js
├── app.json
├── app.wxss
├── pages/
│ └── index/ # 主包页面
│ └── index.jsx
├── packageA/ # 普通分包
│ ├── pages/
│ │ ├── detail/
│ │ │ └── index.jsx
│ │ └── list/
│ │ └── index.jsx
│ └── components/ # 分包内自定义组件
│ └── badge/
│ └── index.jsx
└── packageB/ # 独立分包
└── pages/
└── home/
└── index.jsx
分包页面和组件的写法与主包完全一致,直接使用 Hooks / Options API 即可,无需做任何额外改动:
// src/packageA/pages/detail/index.jsx
import { useState, useEffect } from '@rsmax/runtime';
export default function Detail() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('subpackage page mounted');
});
return (
<view className="container">
<text>分包页面 count: {count}</text>
<button onClick={() => setCount(count + 1)}>+1</button>
</view>
);
}- 独立分包会在其分包根目录下自动独立拷贝一份
rsmax-runtime.js(以及 store/i18n 运行时文件),保证独立运行 - 独立分包中的组件/页面不应依赖主包资源(遵循微信小程序官方约束)
@rsmax/runtime、@rsmax/store、@rsmax/i18n的 import 仍然正常使用,编译器会自动处理路径
| 场景 | 运行时位置 | 运行时引用路径 |
|---|---|---|
| 主包页面 | 主包根目录 | ./rsmax-runtime.js(页面同根) |
| 普通分包页面 | 主包根目录(共享) | ../../../../rsmax-runtime.js(从分包页面回溯到主包) |
| 独立分包页面 | 分包根目录(独立拷贝) | ../../rsmax-runtime.js(回溯到分包根) |
| 分包内组件 | 与同包页面一致 | 根据所在包自动计算 |
Rsmax 提供零依赖、无运行时开销的轻量级变量注入方案。所有 process.env.XXX 在编译阶段被静态替换为字面量,小程序运行时无需加载 Dotenv 等任何库,打包体积和运行时性能均零损耗。
设计原则:编译时静态替换(类似 Vite 的
import.meta.env/ Webpack 的DefinePlugin),不是运行时读取。
优先级 1(最低) .env 文件(4 种类型,按加载顺序依次覆盖)
│
优先级 2 系统环境变量(RSMAX_ 前缀 + 白名单 NODE_ENV/ENV/MODE)
│
优先级 3(最高) rsmax.config.js 中的 define 配置
注意:CLI 的
--mode参数会强制注入process.env.NODE_ENV和process.env.MODE,优先级高于.env文件和系统环境变量,但低于define配置(即define可以覆盖一切)。
在项目根目录(与 rsmax.config.js 同级)创建 .env 系列文件,支持 4 种加载类型(后者覆盖前者):
| 文件名 | 说明 | 何时加载 |
|---|---|---|
.env |
默认配置,所有环境都会加载 | 始终加载 |
.env.local |
本地个人覆盖,不应提交到 git | 始终加载(优先级高于 .env) |
.env.<mode> |
指定环境的配置(如 .env.production) |
当 --mode <mode> 匹配时加载 |
.env.<mode>.local |
指定环境的本地个人覆盖 | 当 --mode <mode> 匹配时加载(优先级最高) |
.env 文件语法兼容 Dotenv 主流用法:
# 简单键值对
API_BASE=https://api.example.com
APP_NAME=我的小程序
# 支持引号包裹(单/双引号都可以),包含空格或特殊字符时推荐
MOTTO="Hello World"
SECRET_KEY='abc123'
# 支持 export 前缀(可与 shell source 命令兼容)
export DEBUG=true
# 支持 ${VAR} 和 $VAR 引用同一文件中前面的变量
HOST=localhost
PORT=8080
BASE_URL=http://${HOST}:${PORT}
FULL_URL=$BASE_URL/api
# 井号开头的行为注释(注释不能出现在行首以外除非前面有空格)
APP_TITLE=测试应用 # 这是行尾注释示例项目结构:
your-project/
├── .env # 公共默认
├── .env.local # 本地个人覆盖(建议加入 .gitignore)
├── .env.development # 开发环境
├── .env.development.local # 开发环境本地私钥
├── .env.production # 生产环境
├── .env.staging # 预发环境
├── src/
└── rsmax.config.js
系统环境变量在编译时从 process.env 读取,仅以下两类会被注入(避免将无关的系统变量意外注入到小程序包):
-
RSMAX_前缀 — 所有以RSMAX_开头的变量名会被原样注入(包含前缀):# 例:CI/CD 脚本中设置 RSMAX_DEPLOY_VERSION=$(git rev-parse --short HEAD) RSMAX_UPLOAD_TOKEN=xxxxxxxxxxxx
代码中直接使用
process.env.RSMAX_DEPLOY_VERSION、process.env.RSMAX_UPLOAD_TOKEN。 -
白名单 —
NODE_ENV、ENV、MODE三个常用变量(不带前缀也可注入)。
提示:为避免命名冲突和安全泄漏,推荐始终使用
RSMAX_前缀(除NODE_ENV/MODE外)。
在项目根目录 rsmax.config.js 中通过 define 字段注入(或覆盖)任意变量:
// rsmax.config.js
module.exports = {
// 组件映射等其他配置...
components: { /* ... */ },
// 编译时 Define 变量(优先级最高,可覆盖 .env 和系统环境变量)
define: {
// 支持 string / number / boolean / null / undefined / 可 JSON 序列化的对象数组
API_BASE: 'https://api.rsmax.dev',
TIMEOUT: 10000,
DEBUG: true,
ENABLE_MOCK: process.env.CI ? false : true,
FEATURE_FLAGS: {
enableNewUserGuide: true,
enableDarkMode: false
},
// 你甚至可以强制覆盖 NODE_ENV(极少需要)
// NODE_ENV: 'production'
}
};// pages/index/index.jsx
import { useEffect, useState } from '@rsmax/runtime';
export default function Home() {
const [userList, setUserList] = useState([]);
useEffect(async () => {
// 编译时替换为字面量:https://api.example.com/users
const resp = await wx.request({
url: process.env.API_BASE + '/users',
timeout: process.env.TIMEOUT
});
setUserList(resp.data);
}, []);
return (
<view>
<text>当前环境:{process.env.NODE_ENV}</text>
{process.env.DEBUG && <text class="tag-dev">调试模式</text>}
</view>
);
}const { API_BASE, DEBUG, TIMEOUT } = process.env;
// 解构后 API_BASE / DEBUG / TIMEOUT 都是已替换的字面量常量
console.log(API_BASE, DEBUG, TIMEOUT);无论是否配置,process.env.NODE_ENV 和 process.env.MODE 始终可用:
- 默认值(未指定时):
dev命令为development,build命令为production - 运行
rsmax dev src -m staging时,两者都是'staging' - 可用于常见的「环境判断」代码:
if (process.env.NODE_ENV === 'production') { // 生产环境才启用的统计上报 wx.reportMonitor('perf_page_load', duration); }
场景:开发/生产接口地址切换
# .env.development(开发环境)
API_BASE=https://dev-api.example.com
DEBUG=true
MOCK_ENABLED=true# .env.production(生产环境)
API_BASE=https://api.example.com
DEBUG=false
MOCK_ENABLED=false# 开发模式 → 自动加载 .env + .env.development + 对应 local 文件
rsmax dev src -o dist
# 生产构建 → 自动加载 .env + .env.production + 对应 local 文件
rsmax build src -o dist场景:CI/CD 中注入版本号
# Jenkins / GitHub Actions 等流水线脚本
export RSMAX_BUILD_VERSION="v$(cat package.json | grep version | head -1 | awk -F: '{print $2}' | sed 's/[\",]//g' | tr -d '[[:space:]]')-$(git rev-parse --short HEAD)"
rsmax build src -o dist -m production代码中直接读取:
console.log('构建版本:', process.env.RSMAX_BUILD_VERSION);
// 输出类似:构建版本:v1.2.3-a1b2c3d-
纯编译时替换,不支持动态拼接键名:
// ✅ 正确:静态确定的键名 const url = process.env.API_BASE; // ❌ 错误:运行时才知道访问哪个键(无法静态分析,不会被替换) const key = 'API_BASE'; const url = process.env[key]; // 该表达式不会被替换,会报错
-
对象/数组字面量会以
JSON.parse(...)形式注入,性能开销可忽略;如需更轻量可先在define中扁平化为多个标量。 -
不要在代码中注入密码、私钥等超高敏感信息:编译后的值是明文写在产物 JS 文件中的(与所有同类方案一致),任何用户都可以反编译看到。建议仅注入非敏感的配置(接口域名、功能开关、版本号等)。
const [count, setCount] = useState(0);
const [count, setCount] = useState(0, 'count'); // 指定 data keyuseEffect(() => {
// 副作用
return () => {
// 清理函数(onUnload 时执行)
};
}, [deps]); // 依赖数组,同 Reactconst ThemeContext = createContext('light');
const theme = useContext(ThemeContext);获取页面 onLoad 时传入的参数:
const query = useQuery(); // { id: '123', ... }监听页面生命周期事件:
usePageEvent('onShow', () => {
console.log('page show');
});
usePageEvent('onReachBottom', () => {
// 下拉加载更多
});支持的页面事件:onLoad、onShow、onReady、onHide、onUnload、onPullDownRefresh、onReachBottom、onShareAppMessage、onPageScroll 等。
监听 App 全局事件:
useAppEvent('onLaunch', () => {
console.log('app launched');
});组件内监听 lifetimes 事件:
useComponentEvent('attached', () => {});
useComponentEvent('detached', () => {});将微信小程序风格的回调 API(success/fail)转换为 Promise,方便使用 async/await 语法。
import { promisify } from '@rsmax/runtime';
export default function UserPage() {
const [userInfo, setUserInfo] = useState(null);
const handleGetUserInfo = async () => {
try {
// 将 wx.getUserInfo 转换为 Promise 风格
const wxGetUserInfo = promisify(wx.getUserInfo);
const res = await wxGetUserInfo();
setUserInfo(res.userInfo);
} catch (err) {
console.error('获取用户信息失败:', err);
}
};
const handleRequest = async () => {
try {
const wxRequest = promisify(wx.request);
const res = await wxRequest({
url: 'https://api.example.com/data',
method: 'GET'
});
console.log('请求结果:', res.data);
} catch (err) {
console.error('请求失败:', err);
}
};
return (
<view class="container">
{userInfo ? (
<text>欢迎, {userInfo.nickName}</text>
) : null}
<button onClick={handleGetUserInfo}>获取用户信息</button>
<button onClick={handleRequest}>发起请求</button>
</view>
);
}特性:
- 支持同时传入自定义的
success/fail回调,它们会在 Promise resolve/reject 之前被调用 - 无参数调用时默认使用空对象
{} - 保留其他选项参数(如
url、method、data等)
// 自定义回调与 Promise 共存
const wxGetStorage = promisify(wx.getStorage);
wxGetStorage({
key: 'token',
success: (res) => console.log('自定义回调:', res.data),
}).then(res => {
console.log('Promise resolve:', res.data);
});常用可 promisify 的微信 API:wx.request、wx.login、wx.getUserInfo、wx.getStorage、wx.setStorage、wx.chooseImage、wx.navigateTo、wx.scanCode 等。
框架内置了类似 Zustand 的轻量级状态管理库 @rsmax/store,支持通过微信小程序缓存(wx.setStorageSync/wx.getStorageSync)实现状态持久化。
1. 创建 Store
// src/stores/counter.js
import { create } from '@rsmax/store';
export const counterStore = create((set, get) => ({
count: 0,
increment: () => set({ count: get().count + 1 }),
decrement: () => set({ count: get().count - 1 }),
reset: () => set({ count: 0 }),
incrementBy: (n) => set({ count: get().count + n }),
}));2. 在组件中使用
使用 useStore hook 订阅状态,传入 selector 函数只订阅需要的字段,优化性能:
// src/pages/counter/index.jsx
import { useStore } from '@rsmax/runtime';
import { counterStore } from '../../stores/counter';
export default function CounterPage() {
const count = useStore(counterStore, (s) => s.count);
return (
<view class="container">
<text>Count: {count}</text>
<button onClick={() => counterStore.getState().increment()}>+1</button>
<button onClick={() => counterStore.getState().decrement()}>-1</button>
<button onClick={() => counterStore.getState().reset()}>重置</button>
</view>
);
}3. 在组件外访问/修改状态
// 任意 JS 文件中访问
import { counterStore } from './stores/counter';
// 获取状态
const currentCount = counterStore.getState().count;
// 直接更新状态
counterStore.setState({ count: 100 });
// 订阅状态变化
const unsub = counterStore.subscribe((state, prevState) => {
console.log('count changed:', state.count);
});
// 取消订阅
unsub();使用 persist 中间件将状态自动保存到微信小程序本地缓存,应用重启后自动恢复:
// src/stores/counter.js
import { create } from '@rsmax/store';
import { persist } from '@rsmax/store/middleware';
export const counterStore = create(
persist(
(set, get) => ({
count: 0,
increment: () => set({ count: get().count + 1 }),
decrement: () => set({ count: get().count - 1 }),
reset: () => set({ count: 0 }),
}),
{
name: 'counter-storage', // 缓存键名(必填)
// partialize: (state) => ({ count: state.count }), // 可选:只持久化部分字段
// version: 1, // 可选:版本号,用于数据迁移
// migrate: (persistedState, version) => { /* 迁移逻辑 */ }, // 可选:版本迁移函数
}
)
);persist 配置项:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string |
是 | 本地缓存键名 |
partialize |
(state) => Partial<State> |
否 | 筛选需要持久化的字段,默认持久化全部状态 |
version |
number |
否 | 版本号,配合 migrate 使用 |
migrate |
(persistedState, version) => State |
否 | 数据迁移函数,当版本号不匹配时调用 |
storage |
{ getItem, setItem, removeItem } |
否 | 自定义存储实现,默认使用 wx.setStorageSync/wx.getStorageSync |
每个 store 实例提供以下方法:
| 方法 | 说明 |
|---|---|
getState() |
获取当前状态 |
setState(partial, replace?) |
更新状态。partial 为对象时合并更新,为函数时接收当前状态返回新状态;replace=true 时替换整个状态 |
subscribe(listener) |
订阅状态变化,返回取消订阅函数 |
destroy() |
销毁 store,清除所有订阅 |
完整示例可查看 e2e 项目中的 store-demo 页面。
框架内置了基于 JS 模块的轻量级国际化方案 @rsmax/i18n,支持多语言切换、变量插值、嵌套键值,且通过编译器实现按需加载——只有使用了 @rsmax/i18n 的页面/组件才会引入运行时和语言包文件。
在项目根目录创建 locales/ 文件夹(也支持放在 src/locales/),放置 JS 语言包文件,文件名即为语言代码:
locales/
├── zh-CN.js # 简体中文
├── en.js # 英文
└── ja.js # 日文(可选)
每个语言包通过 module.exports 导出一个嵌套对象,支持点号路径访问,使用 {name} 语法标记变量插值位置:
// locales/zh-CN.js
module.exports = {
app: {
name: '我的应用'
},
home: {
title: '首页',
greeting: '你好,{name}!',
items: {
count: '共 {count} 条记录'
}
},
common: {
confirm: '确定',
cancel: '取消'
}
};// locales/en.js
module.exports = {
app: {
name: 'My App'
},
home: {
title: 'Home',
greeting: 'Hello, {name}!',
items: {
count: '{count} items total'
}
},
common: {
confirm: 'Confirm',
cancel: 'Cancel'
}
};在 app.js 中初始化 i18n,设置默认语言:
// src/app.js
import { initI18n } from '@rsmax/i18n';
initI18n({
locale: 'zh-CN', // 默认语言
fallbackLocale: 'zh-CN' // 兜底语言(当翻译缺失时使用)
});
App({
onLaunch() {
console.log('App launched');
}
});使用 useI18n() Hook 获取翻译函数和语言控制方法,在 JSX 中直接调用 t('key'):
// src/pages/index/index.jsx
import { useState } from '@rsmax/runtime';
import { useI18n, t, setLocale } from '@rsmax/i18n';
export default function HomePage() {
const { locale } = useI18n(); // 初始化 i18n,自动注入 data.__i18n
const [name] = useState('World');
const switchToZh = () => setLocale('zh-CN');
const switchToEn = () => setLocale('en');
return (
<view class="container">
{/* 基础翻译 */}
<text>{t('home.title')}</text>
{/* 变量插值 — 在 JSX 中使用 state 变量展示插值结果 */}
<text>{t('home.greeting', { name })}</text>
<text>{t('home.items.count', { count: 10 })}</text>
{/* 嵌套键访问 */}
<text>{t('common.confirm')}</text>
{/* 语言切换 */}
<button onClick={switchToZh}>中文</button>
<button onClick={switchToEn}>English</button>
<text>当前语言: {locale}</text>
</view>
);
}注意:由于 WXML 模板中无法执行 JavaScript 函数调用,JSX 中的
t('key')会在编译时被转换为{{__i18n['key']}}数据绑定,这意味着模板中t()的参数必须是字符串字面量,不能是变量或表达式。对于带变量插值的场景,可在 JS 逻辑中调用i18nT('key', params)计算结果后通过setState绑定到视图。
编译器会自动处理以下工作:
- 按需检测:编译每个 JS/JSX 文件时,检测是否
import/require了@rsmax/i18n。只有使用了 i18n 的文件才会触发运行时复制。 - 路径重写:将源码中的
import { t } from '@rsmax/i18n'自动重写为本地相对路径(如require('../../rsmax-i18n.js'))。 - 运行时复制:首次检测到 i18n 使用时,将
rsmax-i18n.js运行时复制到 dist 根目录。 - 语言包处理:扫描
locales/目录,将所有.js语言包直接复制到dist/locales/,并生成rsmax-i18n-locales.js模块。每个语言包用函数包裹实现懒加载——只有切换到对应语言时才会require对应的语言包文件。 - WXML 转换:JSX 中的
t('key')调用在编译阶段被转换为 WXML 的{{__i18n['key']}}数据绑定,模板中无需函数调用即可直接渲染翻译文本。
在 App 入口初始化全局 i18n 实例,返回 i18n 实例。
initI18n({
locale: 'zh-CN', // 默认语言,默认 'zh-CN'
fallbackLocale: 'en', // 兜底语言,翻译缺失时回退到此语言
messages: { // 可选:内联消息(无需 locales 文件)
'zh-CN': { hi: '你好' },
'en': { hi: 'Hello' }
}
});在组件/页面的 setup 函数中调用,返回 { t, locale, setLocale, addMessages }。调用后会:
- 自动将当前语言的扁平消息注入到
data.__i18n中 - 订阅语言切换事件,语言变化时自动调用
setData更新视图 - 页面卸载时自动取消订阅
const { t, locale, setLocale, addMessages } = useI18n();翻译函数,根据当前语言返回对应的文本。支持点号分隔的嵌套键名和 {name} 变量插值。
t('home.title'); // "首页"
t('home.greeting', { name: '张三' }); // "你好,张三!"
t('nonexistent.key'); // key 不存在时返回 key 本身注意:在 JSX 模板中直接使用
t('key')时,编译器会自动转换为数据绑定。在 JS 逻辑代码中(如事件处理函数、useEffect 中),t()作为普通函数调用正常工作。
切换当前语言。切换后所有已挂载的组件会自动更新翻译内容,返回 Promise。
setLocale('en').then(() => {
console.log('语言已切换');
});获取当前语言代码。
const current = getLocale(); // 'zh-CN'动态添加翻译消息(适用于从后端加载语言包的场景)。添加后如果是当前语言,会立即触发视图更新。
addMessages('fr', {
home: { title: 'Accueil' }
});获取全局 i18n 实例(主要用于非组件环境,如工具函数中)。
const i18n = getI18n();
console.log(i18n.t('home.title'));编译器实现了精确的按需加载:
- 未使用
@rsmax/i18n的项目:不会复制任何 i18n 相关文件到 dist 目录 - 部分页面使用:只有 import 了
@rsmax/i18n的文件会被重写引用路径,但运行时和语言包只需复制一次(到 dist 根目录) - 语言包懒加载:运行时不会一次性加载所有语言包,只有调用
setLocale()切换到某语言时,才会require对应的语言文件 - watch 模式:开发模式下 locales 目录新增/修改语言包文件会自动重新生成语言包模块
public/ 目录用于存放不需要编译处理的静态资源,这些文件会直接复制到 dist 产物的根目录,保持原有的目录结构。
public/ 支持两种放置位置(与 locales/ 目录一致):
- 项目根目录(与
src/同级):推荐方式,如public/icon.png→dist/icon.png - 源码目录内:放在
src/public/下,仅当项目根目录没有public/时生效
优先级:项目根目录的
public/优先于src/public/,两者同时存在时只使用根目录的。
将静态文件放入 public/ 目录:
public/
├── icon.png
├── logo.svg
├── sitemap.json
└── images/
└── banner.jpg
构建后会映射到:
dist/
├── icon.png
├── logo.svg
├── sitemap.json
└── images/
└── banner.jpg
使用绝对路径(以 / 开头)引用静态资源,符合小程序路径规范:
// 引用 public/icon.png
<image src="/icon.png" />
// 引用 public/images/banner.jpg
<image src="/images/banner.jpg" />在 wxss 中也可以使用绝对路径:
.header {
background-image: url('/images/banner.jpg');
}在 rsmax dev 监听模式下,public 目录中的文件变化会自动同步:
- 新增文件 → 自动复制到 dist
- 修改文件 → 自动更新
- 删除文件 → 自动从 dist 移除
- 新增/删除子目录 → 自动同步
- 项目根目录 public 和 src/public 均支持监听
<text>{message}</text>
<text>{`Hello, ${name}`}</text>{show ? <view>Visible</view> : null}
{show && <view>Visible</view>}{items.map(item => (
<view key={item.id}>{item.name}</view>
))}使用 JSX Fragment <>...</> 可以返回多个根级元素而不产生额外的包裹节点,这在使用 page-meta 时尤为重要:
export default function Index() {
return (
<>
<page-meta page-style="background-color: #f5f5f5;">
<navigation-bar title="首页" />
</page-meta>
<view className="container">
<text>页面内容</text>
</view>
</>
);
}Fragment 在编译后会被展开,不会产生任何包裹标签,子节点直接成为 WXML 的根级节点。
rsmax 原生支持微信小程序的 page-meta 和 navigation-bar 组件,用于动态修改页面属性(如背景色、导航栏样式等)。
重要:微信小程序要求
page-meta必须是页面模板中的第一个节点。rsmax 编译器会自动处理 WXS 标签的注入位置,确保page-meta始终位于最前。
基础用法:
export default function Page() {
return (
<>
<page-meta
page-style="background-color: #f5f5f5;"
root-font-size="16px"
background-text-style="dark"
>
<navigation-bar
title="我的页面"
background-color="#ffffff"
front-color="#000000"
/>
</page-meta>
<view className="container">
<text>Hello World</text>
</view>
</>
);
}动态绑定属性:
import { useState } from '@rsmax/runtime';
export default function Page() {
const [bgColor, setBgColor] = useState('#ffffff');
const [title, setTitle] = useState('首页');
return (
<>
<page-meta page-style={`background-color: ${bgColor};`}>
<navigation-bar title={title} />
</page-meta>
<view>
<button onClick={() => setBgColor('#f0f0f0')}>切换背景</button>
</view>
</>
);
}page-meta 支持的属性(kebab-case):
| 属性 | 说明 |
|---|---|
page-style |
页面根节点样式 |
root-font-size |
页面根元素字体大小 |
background-text-style |
下拉背景字体、loading 图的样式(dark/light) |
background-color |
窗口背景色 |
background-color-top |
顶部窗口背景色 |
background-color-bottom |
底部窗口背景色 |
scroll-top |
滚动位置(需设置 scroll-view 为页面滚动) |
page-style-open |
进入/离开动画期间页面样式 |
page-style-close |
离开动画期间页面样式 |
navigation-bar 支持的属性:
| 属性 | 说明 |
|---|---|
title |
导航栏标题 |
background-color |
导航栏背景色 |
front-color |
前景颜色(含标题、按钮),仅支持 #000000/#ffffff |
color-animation-duration |
颜色变化动画时长 |
color-animation-timing-func |
颜色变化动画 timing function |
loading |
是否显示导航栏加载动画 |
title-image |
导航栏图片地址(替代标题文字) |
支持的事件:
page-meta:onScroll(bindscroll)、onResize(bindresize)navigation-bar:无自定义事件
注意:
- 必须使用 Fragment
<>...</>将page-meta和页面内容包裹在一起,使page-meta成为根级第一个节点。- 一个页面只能有一个
page-meta。navigation-bar必须是page-meta的直接子节点。- 属性名使用 kebab-case 形式(如
page-style而非pageStyle),与微信官方文档一致。
page-container 用于在页面内弹出一个全屏覆盖层,类似弹窗/抽屉效果,支持从各个方向滑入。
import { useState } from '@rsmax/runtime';
export default function Page() {
const [showPopup, setShowPopup] = useState(false);
return (
<>
<page-meta>
<navigation-bar title="Page Container Demo" />
</page-meta>
<view className="container">
<button onClick={() => setShowPopup(true)}>打开弹窗</button>
</view>
<page-container
show={showPopup}
overlay={true}
position="bottom"
round={true}
onBeforeEnter={() => console.log('进入前')}
onEnter={() => console.log('进入中')}
onAfterEnter={() => console.log('进入后')}
onBeforeLeave={() => console.log('离开前')}
onLeave={() => console.log('离开中')}
onAfterLeave={() => setShowPopup(false)}
onClickOverlay={() => setShowPopup(false)}
>
<view className="popup-content">
<text>这是一个底部弹出层</text>
</view>
</page-container>
</>
);
}page-container 常用属性:
| 属性 | 说明 |
|---|---|
show |
是否显示容器 |
overlay |
是否显示遮罩层 |
position |
弹出位置:top/bottom/right/center |
round |
是否显示圆角 |
close-on-slide-down |
是否开启下滑关闭 |
overlay-style |
遮罩层自定义样式 |
custom-style |
弹出层自定义样式 |
duration |
动画时长(毫秒) |
支持的事件: onBeforeEnter、onEnter、onAfterEnter、onBeforeLeave、onLeave、onAfterLeave、onClickOverlay
注意:
- 一个页面只能有一个
page-container。- 组件支持嵌套在页面任意位置(无需像
page-meta那样必须在第一个位置)。- 属性名使用 kebab-case 形式。
root-portal 可以将子组件渲染到页面的根节点,类似于 React 的 createPortal,适合实现弹窗、Toast 等需要脱离文档流的组件。
export default function Page() {
return (
<view className="container">
<text>页面内容</text>
{/* 这个 view 会被渲染到页面根节点 */}
<root-portal>
<view className="global-toast">
<text>全局提示消息</text>
</view>
</root-portal>
</view>
);
}root-portal 属性:
| 属性 | 说明 |
|---|---|
enable |
是否启用 portal,默认 true |
注意:
root-portal可以在页面中多次使用。- 子组件会被挂载到页面根节点,不受父级样式和定位影响。
- 适合用于实现全局 Toast、Modal、Popup 等需要脱离层级限制的组件。
rsmax 支持微信小程序的自定义 TabBar。在 src/ 目录下创建 custom-tab-bar/ 目录,其中的 JSX 文件会被自动编译为 Component(而非 Page),JSON 配置中会自动添加 "component": true。
目录结构:
src/
├── app.jsx
├── app.json
├── pages/
│ └── index/
│ └── index.jsx
└── custom-tab-bar/
└── index.jsx ← TabBar 组件
app.json 配置:
{
"pages": ["pages/index/index", "pages/my/index"],
"tabBar": {
"custom": true,
"color": "#999999",
"selectedColor": "#07c160",
"backgroundColor": "#ffffff",
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/my/index", "text": "我的" }
]
}
}custom-tab-bar/index.jsx:
import { useState } from '@rsmax/runtime';
export default function CustomTabBar() {
const [selected, setSelected] = useState(0);
const tabs = [
{ pagePath: '/pages/index/index', text: '首页', icon: '/images/tab-home.png', selectedIcon: '/images/tab-home-active.png' },
{ pagePath: '/pages/my/index', text: '我的', icon: '/images/tab-my.png', selectedIcon: '/images/tab-my-active.png' }
];
function switchTab(e) {
const index = e.currentTarget.dataset.index;
const url = tabs[index].pagePath;
wx.switchTab({ url });
setSelected(index);
}
return (
<view className="tab-bar">
{tabs.map((tab, index) => (
<view
key={tab.pagePath}
className={"tab-item" + (selected === index ? " active" : "")}
data-index={index}
onClick={switchTab}
>
<image
className="icon"
src={selected === index ? tab.selectedIcon : tab.icon}
/>
<text className="text">{tab.text}</text>
</view>
))}
</view>
);
}图标文件放置在 public/ 目录下(与 src/ 同级),会被自动复制到小程序根目录:
project/
├── public/
│ └── images/
│ ├── tab-home.png ← 首页图标(未选中)
│ ├── tab-home-active.png ← 首页图标(选中)
│ ├── tab-my.png
│ └── tab-my-active.png
├── src/
│ ├── custom-tab-bar/
│ │ ├── index.jsx ← TabBar 组件
│ │ └── index.less ← 样式(支持 .wxss/.css/.less/.scss/.sass)
│ └── pages/
└── app.json
样式文件与 JSX 同名会被自动编译/复制(如 index.less → index.wxss),也可以在 JSX 中通过 import './index.less' 显式导入。支持 CSS Modules(.module.less 等):
import styles from './index.module.less';
export default function CustomTabBar() {
return (
<view className={styles.tabBar}>
<view className={styles.tabItem}>首页</view>
</view>
);
}注意:
custom-tab-bar/目录必须放在src/根目录下(与pages/同级)。- 需要在
app.json的tabBar字段中设置"custom": true。- TabBar 组件使用
Component构造器(非Page),支持useState、useEffect等 hooks。- 切换 Tab 需使用
wx.switchTab(),不能使用wx.navigateTo()。- TabBar 的选中状态需要在各页面
onShow中通过getTabBar()主动更新,具体参考微信官方文档。- 在 JSX 中引用静态资源图片时,使用以
/开头的绝对路径(如/images/tab-home.png),该路径相对于小程序根目录解析,可正确访问public/目录下的资源。public/images/tab-home.png编译后位于dist/images/tab-home.png,因此/images/tab-home.png能正确加载。
| JSX 事件 | 小程序事件 |
|---|---|
onClick / onTap |
bindtap |
onInput |
bindinput |
onChange |
bindchange |
onBlur |
bindblur |
onFocus |
bindfocus |
onConfirm |
bindconfirm |
onSubmit |
bindsubmit |
onLongPress |
bindlongpress |
onTouchStart |
bindtouchstart |
onTouchMove |
bindtouchmove |
onTouchEnd |
bindtouchend |
onScroll |
bindscroll |
onLoad |
bindload |
onError |
binderror |
自定义组件的事件:JSX 中写 bindtap、catchtap、bind:change 等可直接透传。
<view onClick={handleTap}>Click me</view>
<van-switch checked={on} bindchange={handleChange} />class 和 className 均支持:
<view class="container">...</view>
<view className={styles.wrapper}>...</view>
<view class={`item ${active ? 'active' : ''}`}>...</view>Vant 等 UI 库的 boolean 属性支持 JSX 简写:
<van-button plain disabled>按钮</van-button>
<van-button plain={true}>朴素</van-button>
<van-cell border={false}>无边框</van-cell>属性名自动从 camelCase 转为 kebab-case:loadingText → loading-text。
<view data-id={item.id} onClick={handleClick}>...</view>rsmax 支持通过 ES Module import 语法引用外部 .wxs 文件。编译器会自动:
- 将
.wxs文件复制到目标目录 - 从 JS 中移除
.wxs的 import 语句(WXS 运行在渲染层,不在 JS 逻辑层执行) - 在生成的 WXML 文件头部自动注入
<wxs src="..." module="..." />标签
使用方式:
// pages/index/index.jsx
import { useState } from '@rsmax/runtime';
import tools from './tools.wxs';
export default function Index() {
const [price] = useState(99.9);
return (
<view>
<text>{tools.formatPrice(price)}</text>
<text>{tools.toUpperCase('hello')}</text>
</view>
);
}对应的 WXS 文件(与页面放在同一目录):
// pages/index/tools.wxs
function formatPrice(price) {
return '¥' + price.toFixed(2);
}
function toUpperCase(str) {
return str.toUpperCase();
}
module.exports = {
formatPrice: formatPrice,
toUpperCase: toUpperCase
};编译后的 WXML:
<wxs module="tools" src="./tools.wxs" />
<view>
<text>{{tools.formatPrice(price)}}</text>
<text>{{tools.toUpperCase('hello')}}</text>
</view>支持 import 多个 WXS 模块:
import math from './math.wxs';
import str from './str.wxs';
// 在模板中分别调用 math.double(num), str.toUpperCase(name) 等注意:WXS 函数在 WXML 表达式中调用时,直接使用 import 的模块名访问即可(如
tools.formatPrice(price)),编译器会自动去除this.data.前缀。
.wxss、.css、.less、.scss 文件直接编译为同名 .wxss,class 名保持全局。
文件名包含 .module 的样式文件启用 CSS Modules(如 index.module.less):
import styles from './index.module.less';
<view class={styles.container}>
<text class={styles.title}>Hello</text>
</view>kebab-case 的 class 名在 JS 中以 camelCase 访问:
.section-title { font-size: 28px; }<text class={styles.sectionTitle}>Title</text>所有 px 单位自动转换为 rpx(1px → 1rpx,基于 750rpx 设计稿)。大写 PX 不转换。
.title {
font-size: 32px; /* → font-size: 32rpx */
border: 1PX solid; /* → border: 1px solid (不转换) */
}自动检测已安装的 UI 组件库,无需手动配置 usingComponents:
| 组件库 | npm 包名 | 标签前缀 |
|---|---|---|
| Vant Weapp | @vant/weapp |
van- |
| TDesign MiniProgram | tdesign-miniprogram |
t- |
| Ant Design Mini | antd-mini |
ant- |
只需在 package.json 中安装依赖即可使用:
pnpm add @vant/weapp<van-button type="primary" onClick={handleClick}>按钮</van-button>
<van-cell-group inset>
<van-cell title="单元格" value="内容" />
</van-cell-group>编译器会自动扫描 JSX 中使用的组件标签,生成对应的 usingComponents 配置。
在项目根目录创建 rsmax.config.js 可添加自定义组件映射:
module.exports = {
components: {
// 前缀映射:标签前缀 → npm 包名
'my': 'my-ui-lib', // <my-button> → my-ui-lib/button/index
// 自定义解析规则
'x': {
packageName: 'my-x-lib',
resolve(tagName) {
// x-image-upload → my-x-lib/image-upload/index
return `my-x-lib/${tagName.slice(2)}/index`;
}
},
// 精确映射:指定单个标签的组件路径
'custom-header': '/components/header/index'
}
};在 app.json 声明插件后,可通过 rsmax.config.js 配置插件组件的便捷映射,无需再手动写页面/组件 .json 的 usingComponents。
第一步:在 app.json 中声明插件(微信小程序标准方式):
{
"plugins": {
"myPlugin": {
"version": "1.0.0",
"provider": "wxidxxxxxxxxxx"
}
}
}第二步:在 rsmax.config.js 中配置组件映射,支持两种方式:
// rsmax.config.js
module.exports = {
components: {
// 方式一:精确映射——标签名 → plugin:// 完整路径
'hello-comp': 'plugin://myPlugin/hello-component',
'city-select': 'plugin://cityPlugin/select',
// 方式二:前缀映射(推荐,适合插件提供多个组件)
// 使用 <mp-xxx /> 自动映射为 plugin://myPlugin/xxx
'mp': { plugin: 'myPlugin' },
// 方式三:前缀映射 + 自定义 resolve(插件命名非标准时使用)
'txv': {
plugin: 'tencentvideo',
resolve(tagName) {
// txv-videoview → plugin://tencentvideo/videoview
return `plugin://tencentvideo/${tagName.slice(4)}`;
}
}
}
};第三步:在 JSX 中直接使用插件组件标签:
// 精确映射用法
export default function Index() {
return (
<view>
<hello-comp name="world" />
<mp-hello />
<mp-list data-source={list} />
</view>
);
}编译后会自动在页面 .json 的 usingComponents 中生成:
{
"usingComponents": {
"hello-comp": "plugin://myPlugin/hello-component",
"mp-hello": "plugin://myPlugin/hello",
"mp-list": "plugin://myPlugin/list"
}
}插件的 JS API(如 requirePlugin)按微信小程序官方方式直接调用即可,编译器不做拦截:
const myPlugin = requirePlugin('myPlugin');
myPlugin.someMethod();import dayjs from 'dayjs'; // → var dayjs = require('dayjs');
import { format } from 'lib'; // → var { format } = require('lib');
import 'polyfill'; // → require('polyfill');
const now = dayjs().format('YYYY-MM-DD');注意:使用 npm 包需在微信开发者工具中执行 工具 → 构建 npm。构建产物 miniprogram_npm 目录会被 rsmax 构建时自动保留,无需每次构建后重新执行。
推荐直接使用 JSX 编写自定义组件,编译器会自动处理组件注册、properties 声明和模板生成。
在 src/components/ 目录下创建组件文件夹,每个组件包含 .jsx 入口和可选的样式文件:
src/components/
└── header/
├── index.jsx # 组件逻辑 + JSX
└── index.module.less # CSS Modules 样式(可选)
// src/components/header/index.jsx
import styles from './index.module.less';
export default function DemoHeader({ title, subtitle }) {
return (
<view class={styles.header}>
<text class={styles.title}>{title}</text>
{subtitle ? <text class={styles.subtitle}>{subtitle}</text> : null}
</view>
);
}编译器自动完成:
- 生成
{"component": true}的 JSON 配置 - 从函数参数解构中提取 properties(
title、subtitle),支持默认值 - 生成 WXML 模板和 WXSS 样式(含 CSS Modules 哈希类名)
- 注入 Component 生命周期,支持所有 Hooks(useState/useEffect/useComponentEvent 等)
方式一:通过 rsmax.config.js 配置前缀映射(推荐)
在项目根目录 rsmax.config.js 中配置标签前缀到组件路径的映射:
// rsmax.config.js
module.exports = {
components: {
// <demo-xxx> 标签 → /components/xxx/index
'demo': {
resolve(tagName) {
const compName = tagName.replace(/^demo-/, '');
return `/components/${compName}/index`;
}
},
// 也支持精确映射单个标签
'demo-header': '/components/header/index'
}
};然后在页面 JSX 中直接使用标签:
// src/pages/index/index.jsx
export default function Index() {
return (
<view>
<demo-header title="Hello" subtitle="Welcome to Rsmax" />
</view>
);
}方式二:在页面 JSON 中手动注册
创建页面同名的 .json 文件,手动声明 usingComponents:
// src/pages/index/index.json
{
"usingComponents": {
"demo-header": "/components/header/index"
}
}注意:
createApp、createPage、createComponent是编译器内部函数,不要在代码中直接调用或导入。只需export default你的函数或对象即可,编译器会自动完成包装。
components/ 目录同时支持原生小程序组件(WXML + WXSS + JS 四件套),与 JSX 组件共存。原生组件的文件会被原样复制到 dist,编译器自动生成 {"component": true} 的 JSON 配置。
src/components/
└── badge/
├── index.js # 使用原生 Component() 构造器
├── index.wxml # WXML 模板
└── index.wxss # WXSS 样式(直接用 rpx 单位)
// src/components/badge/index.js
Component({
properties: {
value: { type: null, value: '' },
type: { type: String, value: 'normal' }
}
});<!-- src/components/badge/index.wxml -->
<view class="badge {{type === 'dot' ? 'badge-dot' : ''}}">
<text class="badge-text" wx:if="{{type !== 'dot'}}">{{value}}</text>
</view>原生组件在 JSX 页面中通过 rsmax.config.js 前缀映射引用,与 JSX 组件使用方式完全一致:
<demo-badge value="3"/>
<demo-badge value="99+"/>
<demo-badge type="dot"/>原生组件的
.js文件不要使用export default(否则会被编译器当作函数式组件转换),直接调用Component({...})即可。
也支持传统的对象配置写法,与原生小程序 Component 一致:
export default {
properties: {
title: String
},
data: {
count: 0
},
methods: {
increment() {
this.setData({ count: this.data.count + 1 });
}
},
render() {
return (
<view>
<text>{this.data.title}: {this.data.count}</text>
<button onClick={this.increment}>+1</button>
</view>
);
}
};rsmax-jsx 被设计为天然支持渐进式使用,你不需要一次性将整个原生小程序项目全部重写。原生小程序文件(.wxml/.wxss/原生 .js)和 rsmax-jsx 文件(.jsx/带 export default 的 .js)可以完美共存于同一个项目中,编译器会根据文件类型和内容自动选择合适的处理方式。
编译器的文件处理规则非常简单:
| 文件类型 | 处理方式 | 说明 |
|---|---|---|
.jsx |
编译为小程序页面/组件 | 总是走 rsmax 编译流程,生成对应的 .js/.wxml/.wxss |
.js + 有 export default |
编译为小程序页面/组件 | 按 Page/Component 处理,支持 render 方法或函数式组件 |
.js + 无 export default |
直接复制 / 转换为 CommonJS | 普通工具库原样保留,ES6 import/export 转 require/module.exports |
.wxml .wxss .wxs .json |
直接复制 | 原生文件原样输出到 dist |
| 图片、字体等静态资源 | 直接复制 | 原样保留 |
public/ 目录文件 |
复制到 dist 根目录 | 保持原目录结构 |
从最简单的页面开始,逐个将原生页面迁移到 JSX,其他页面保持原生不动。
迁移前(原生):
src/pages/legacy/
├── index.js ← Page({...})
├── index.wxml ← WXML 模板
├── index.wxss
└── index.json
迁移步骤:
- 删除
index.wxml - 将
index.js重命名为index.jsx(或保持.js后缀,改用export default) - 用 JSX 的
render()方法(或函数式组件)重写模板,逻辑代码大部分可复用
迁移后(JSX):
// src/pages/legacy/index.jsx
export default {
data: {
list: []
},
onLoad() {
// 原有逻辑代码基本不动
},
// 用 render() + JSX 替代原来的 index.wxml
render() {
return (
<view class="list">
{this.data.list.map(item => (
<view key={item.id} class="item">{item.title}</view>
))}
</view>
);
}
};app.json 中的页面路径无需修改,编译器仍会输出 pages/legacy/index.js 和对应的 .wxml。
先迁移小组件,积累经验后再迁移页面。JSX 编译后的组件与原生小程序组件完全兼容,可以被原生页面正常引用。
src/components/
├── old-card/ ← 保持原生组件
│ ├── index.js (Component({...}))
│ ├── index.wxml
│ └── index.wxss
└── new-button/ ← 新组件用 JSX
└── index.jsx
在原生页面中使用 JSX 编译的组件(和引用原生组件一模一样):
// pages/index/index.json
{
"usingComponents": {
"new-button": "/components/new-button/index"
}
}<!-- pages/index/index.wxml -->
<new-button type="primary" bind:click="handleClick">点击</new-button>同样,JSX 页面中也可以直接使用原生组件,无需做任何特殊处理。
如果团队暂时对函数式组件 + Hooks 的风格不熟悉,可以先用接近原生小程序的 Options API 写法过渡,之后再逐步重构为 Hooks 风格。
阶段 1:Options API(和原生小程序几乎一样):
export default {
data: { count: 0 },
onShow() {
console.log('page show');
},
increment() {
this.setData({ count: this.data.count + 1 });
},
render() {
return (
<view>
<text>{this.data.count}</text>
<button onClick={this.increment}>+1</button>
</view>
);
}
};阶段 2:函数式组件 + Hooks(待团队熟悉后再改):
import { useState, usePageEvent } from '@rsmax/runtime';
export default function Counter() {
const [count, setCount] = useState(0);
usePageEvent('onShow', () => {
console.log('page show');
});
const increment = () => setCount(count + 1);
return (
<view>
<text>{count}</text>
<button onClick={increment}>+1</button>
</view>
);
}两种写法的编译输出是等价的,可以在项目中长期共存。
如果项目使用了小程序分包机制,可以选择将新增功能的分包全部用 JSX 编写,已有分包保持原生不动。
// app.json
{
"pages": [
"pages/home/index", // 主包页面:保持原生
"pages/my/index" // 主包页面:保持原生
],
"subPackages": [
{
"root": "legacyPackage",
"pages": [...] // 旧分包:保持原生
},
{
"root": "newPackage",
"pages": [...] // ✅ 新分包:全部用 JSX
}
]
}分包内的页面和组件写法与主包完全一致,编译器会自动处理运行时的相对路径引用和独立分包的运行时拷贝。
在迁移过程中,两种写法之间的互操作是完全无缝的:
- ✅ 原生页面 → 引用 JSX 编译的自定义组件
- ✅ JSX 页面 → 引用原生自定义组件(在
.json中声明即可,或直接在 JSX 中使用已知前缀的 UI 库) - ✅ 原生页面的样式文件(
.wxss/.less/.scss)无需修改,直接被编译器识别 - ✅ 工具函数、Store 等 JS 模块可以在两种页面中完全共享
- ✅
app.js、app.json、app.wxss完全兼容原生写法
-
同一页面二选一:单个页面要么用「
index.js+index.wxml原生组合」,要么用「index.jsx」。不要在页面目录下同时存在.wxml和对应同名的.jsx(后者会生成自己的.wxml,发生覆盖)。 -
样式文件可先复用:页面级样式文件(如
index.wxss、index.less)在迁移后无需修改文件名,编译器会自动识别同名样式文件并编译/复制为index.wxss。 -
miniprogram_npm自动保护:构建过程中不会清空dist/miniprogram_npm,已有的第三方原生小程序 npm 包可正常使用。 -
app.json路径不变:无论页面是原生还是 JSX,app.json中pages和subPackages里的路径写法完全一样(都写pages/xxx/index,不带扩展名)。 -
原生
.wxs直接复制:如果你的 WXML 里用到了 WXS 文件,迁移后在 JSX 中可以改用import tools from './tools.wxs'的方式引用(编译器会自动注入<wxs>标签),也可以先不迁移 WXML,保留原生写法。
对于中大型原生小程序项目,推荐按以下顺序逐步迁移,风险最低:
① 工具函数/常量 → 无改动,直接复用
↓
② 新增公共组件 → 全部用 JSX 编写
↓
③ 样式文件 → 无需改动,直接复用
↓
④ 简单列表页面 → 先从纯展示页面练手
↓
⑤ 复杂表单页面 → 熟悉后迁移交互复杂的页面
↓
⑥ 页面主入口 → 最后迁移首页等核心页面
每个阶段都可以独立验证,发现问题随时回退(将 .jsx 改回 .js + .wxml 即可)。
-
构建 npm:首次使用第三方 npm 包或 UI 库后,需在微信开发者工具中执行「工具 → 构建 npm」。后续 rsmax build/dev 会自动保留
miniprogram_npm,无需重复构建。 -
px 单位:代码中写
px会被自动转为rpx(1:1,基于 750rpx 宽度设计稿)。大写PX不转换。 -
样式隔离:Vant 等第三方 UI 组件默认启用样式隔离,页面样式无法穿透组件内部。如需控制布局,对组件宿主元素设置 margin/display 即可。
-
小程序 API:
wx对象全局可用,如wx.navigateTo、wx.request等。
| 包 | 说明 |
|---|---|
rsmax |
CLI 入口,提供 build/dev/clean 命令 |
@rsmax/compiler |
编译器核心:JSX→WXML、JS 转换、样式编译、CSS Modules、组件解析、i18n 按需加载 |
@rsmax/runtime |
运行时:Hooks 实现(useState/useEffect/useContext/useStore 等)、Page/Component/App 包装器、promisify 工具函数 |
@rsmax/store |
类 Zustand 状态管理库,支持微信缓存持久化中间件 |
@rsmax/i18n |
国际化运行时:基于 JS 模块的多语言支持,含 useI18n Hook、语言切换、变量插值、懒加载 |
@rsmax/babel-plugin-jsx-to-wxml |
Babel 插件:JSX AST → WXML 字符串转换(含 t() → __i18n 数据绑定转换) |
@rsmax/babel-plugin-transform-js |
Babel 插件:转换 ES6 import、注入 rsmax runtime、处理 CSS Modules、store 和 i18n 路径重写 |
MIT