React 渲染到小程序的原理浅析(运行时转换)

38 阅读13分钟

React 渲染到小程序的原理浅析(运行时转换)

一、核心问题

React 和小程序使用不同的渲染机制:

  • React:使用虚拟 DOM(Virtual DOM),在浏览器中渲染为真实 DOM
  • 小程序:使用自己的组件系统(如微信小程序的 <view><text> 等),不能直接使用 DOM

问题:如何让 React 的虚拟 DOM 在小程序中渲染?


二、解决方案:基于 React Reconciler 的自定义渲染器

核心思想

React 提供了 Reconciler(协调器) 机制,允许开发者创建自定义渲染器。通过实现一个针对小程序的渲染器,可以将 React 的虚拟 DOM 直接渲染到小程序环境中。

React Reconciler 简介

React Reconciler 是 React 的核心机制,负责:

  • 管理组件的生命周期
  • 协调虚拟 DOM 的更新
  • 调用渲染器进行实际渲染

React 本身使用 Reconciler + DOM 渲染器(react-dom)来渲染到浏览器。我们可以实现一个 小程序渲染器 来替代 DOM 渲染器。

工作流程

React 代码 → React Reconciler → 自定义小程序渲染器 → 小程序数据结构 → 小程序渲染

关键:不是转换虚拟 DOM,而是直接实现一个渲染器,让 React Reconciler 调用我们的渲染器来渲染到小程序。


三、详细实现原理

3.1 React Reconciler 的工作机制

React Reconciler 会调用渲染器的方法来执行实际的渲染操作。渲染器需要实现以下接口:

// 渲染器需要实现的接口(简化版)
const renderer = {
  // 创建节点
  createInstance(type, props) {},

  // 创建文本节点
  createTextInstance(text) {},

  // 追加子节点
  appendChild(parent, child) {},

  // 插入子节点
  insertBefore(parent, child, before) {},

  // 移除子节点
  removeChild(parent, child) {},

  // 更新属性
  commitUpdate(instance, updatePayload) {},

  // 提交更新
  commitTextUpdate(textInstance, oldText, newText) {}
}

3.2 实现小程序渲染器

基于 React Reconciler 实现小程序渲染器:

import { Reconciler } from 'react-reconciler'

// 创建小程序渲染器配置
const hostConfig = {
  // 创建组件实例(对应小程序的组件)
  createInstance(type, props) {
    // 组件映射
    const componentMap = {
      View: 'view',
      Text: 'text',
      Button: 'button',
      Image: 'image'
    }

    const miniprogramType = componentMap[type] || type.toLowerCase()

    // 转换属性
    const miniprogramProps = {}
    if (props.className) {
      miniprogramProps.class = props.className
    }
    if (props.onClick) {
      miniprogramProps.bindtap = props.onClick
    }

    // 返回小程序组件实例
    return {
      type: miniprogramType,
      props: miniprogramProps,
      children: []
    }
  },

  // 创建文本节点
  createTextInstance(text) {
    return {
      type: 'text',
      text: String(text)
    }
  },

  // 追加子节点
  appendChild(parent, child) {
    if (!parent.children) {
      parent.children = []
    }
    parent.children.push(child)
  },

  // 插入子节点
  insertBefore(parent, child, before) {
    if (!parent.children) {
      parent.children = []
    }
    const index = parent.children.indexOf(before)
    parent.children.splice(index, 0, child)
  },

  // 移除子节点
  removeChild(parent, child) {
    if (parent.children) {
      const index = parent.children.indexOf(child)
      parent.children.splice(index, 1)
    }
  },

  // 更新属性
  commitUpdate(instance, updatePayload) {
    Object.assign(instance.props, updatePayload)
  },

  // 提交更新(批量更新完成后调用)
  commitTextUpdate(textInstance, oldText, newText) {
    textInstance.text = newText
  },

  // 获取父节点
  getParentInstance(instance) {
    return instance.parent
  },

  // 获取根容器
  getRootHostContext() {
    return {}
  },

  // 获取子节点上下文
  getChildHostContext() {
    return {}
  }
}

// 创建 Reconciler 实例
const reconciler = Reconciler(hostConfig)

// 渲染函数
export function render(element, container, callback) {
  const root = reconciler.createContainer(container, false, false)
  reconciler.updateContainer(element, root, null, callback)

  // 将渲染结果转换为小程序数据结构
  const nodes = convertToMiniProgramData(container)

  // 通过 setData 更新小程序
  container.page.setData({ nodes })
}

3.3 使用自定义渲染器

// 在小程序页面中
import { render } from './miniprogram-renderer'

Page({
  data: {
    nodes: []
  },

  onLoad() {
    // 使用自定义渲染器渲染 React 组件
    render(<App />, {
      page: this,
      children: []
    })
  }
})

小程序使用 template 渲染:

<!-- 小程序 WXML -->
<template name="react-node">
  <block wx:for="{{nodes}}" wx:key="index">
    <!-- view 组件 -->
    <view wx:if="{{item.type === 'view'}}" class="{{item.props.class}}">
      <template is="react-node" data="{{nodes: item.children}}" />
    </view>

    <!-- text 组件 -->
    <text wx:elif="{{item.type === 'text'}}">{{item.text}}</text>

    <!-- button 组件 -->
    <button wx:elif="{{item.type === 'button'}}" bindtap="{{item.props.bindtap}}">
      <template is="react-node" data="{{nodes: item.children}}" />
    </button>
  </block>
</template>

<!-- 使用 template 渲染 -->
<template is="react-node" data="{{nodes}}" />

四、关键技术点详解

4.1 组件映射

问题:React 组件需要映射到小程序组件

实现

const componentMap = {
  View: 'view', // React View → 小程序 view
  Text: 'text', // React Text → 小程序 text
  Image: 'image', // React Image → 小程序 image
  Button: 'button', // React Button → 小程序 button
  ScrollView: 'scroll-view',
  Swiper: 'swiper'
  // ... 更多映射
}

4.2 属性转换

问题:React 属性名和小程序属性名不同

转换规则

const propMap = {
  className: 'class', // className → class
  onClick: 'bindtap', // onClick → bindtap
  onChange: 'bindchange', // onChange → bindchange
  onInput: 'bindinput', // onInput → bindinput
  style: 'style' // style 需要特殊处理
}

4.3 状态管理

原理:React Reconciler 会自动处理状态更新

当组件调用 setState 时:

  1. React Reconciler 检测到状态变化
  2. 重新协调虚拟 DOM(diff 算法)
  3. 调用渲染器的更新方法(commitUpdatecommitTextUpdate 等)
  4. resetAfterCommit 中统一调用 setData 更新小程序

实现

// React Reconciler 自动处理状态更新
// 当组件 setState 时,Reconciler 会:
// 1. 重新协调组件树
// 2. 调用 hostConfig.commitUpdate 更新实例
// 3. 调用 hostConfig.resetAfterCommit 提交更新
// 4. 在 resetAfterCommit 中调用 setData

4.4 事件处理

问题:React 事件需要转换为小程序事件

实现

// 事件处理映射
function convertEvent(eventName) {
  const eventMap = {
    onClick: 'bindtap',
    onChange: 'bindchange',
    onInput: 'bindinput',
    onSubmit: 'bindsubmit'
  }
  return eventMap[eventName] || eventName
}

// 事件处理函数需要绑定到小程序页面
function bindEvent(handler) {
  // 将 React 事件处理函数绑定到小程序页面
  return function (event) {
    // 转换小程序事件对象为 React 事件对象
    const reactEvent = convertMiniProgramEvent(event)
    handler(reactEvent)
  }
}

4.5 生命周期映射

问题:React 生命周期需要映射到小程序生命周期

映射关系

// React 生命周期 → 小程序生命周期
const lifecycleMap = {
  componentDidMount: 'onLoad', // 组件挂载 → 页面加载
  componentDidUpdate: 'onShow', // 组件更新 → 页面显示
  componentWillUnmount: 'onUnload' // 组件卸载 → 页面卸载
}

五、完整实现示例

5.1 基于 React Reconciler 的实现

import { Reconciler } from 'react-reconciler'

// 小程序渲染器配置
const hostConfig = {
  // 组件映射表
  componentMap: {
    View: 'view',
    Text: 'text',
    Button: 'button',
    Image: 'image'
  },

  // 创建组件实例
  createInstance(type, props, rootContainerInstance) {
    const miniprogramType = this.componentMap[type] || type.toLowerCase()

    const instance = {
      type: miniprogramType,
      props: this.convertProps(props),
      children: [],
      parent: null
    }

    return instance
  },

  // 创建文本节点
  createTextInstance(text, rootContainerInstance) {
    return {
      type: 'text',
      text: String(text),
      parent: null
    }
  },

  // 转换属性
  convertProps(props) {
    const miniprogramProps = {}

    // className → class
    if (props.className) {
      miniprogramProps.class = props.className
    }

    // 事件转换
    Object.keys(props).forEach(key => {
      if (key.startsWith('on')) {
        const eventMap = {
          onClick: 'bindtap',
          onChange: 'bindchange',
          onInput: 'bindinput'
        }
        const eventName = eventMap[key] || key.toLowerCase()
        miniprogramProps[eventName] = props[key]
      }
    })

    return miniprogramProps
  },

  // 追加子节点
  appendChild(parentInstance, child) {
    child.parent = parentInstance
    parentInstance.children.push(child)
  },

  // 插入子节点
  insertBefore(parentInstance, child, beforeChild) {
    child.parent = parentInstance
    const index = parentInstance.children.indexOf(beforeChild)
    parentInstance.children.splice(index, 0, child)
  },

  // 移除子节点
  removeChild(parentInstance, child) {
    const index = parentInstance.children.indexOf(child)
    parentInstance.children.splice(index, 1)
    child.parent = null
  },

  // 更新属性
  commitUpdate(instance, updatePayload, type, oldProps, newProps) {
    const newMiniprogramProps = this.convertProps(newProps)
    Object.assign(instance.props, newMiniprogramProps)
  },

  // 提交文本更新
  commitTextUpdate(textInstance, oldText, newText) {
    textInstance.text = newText
  },

  // 获取父节点
  getParentInstance(instance) {
    return instance.parent
  },

  // 获取根容器上下文
  getRootHostContext() {
    return {}
  },

  // 获取子节点上下文
  getChildHostContext(parentHostContext, type) {
    return {}
  },

  // 准备更新(提交阶段前)
  prepareForCommit(containerInfo) {
    // 可以在这里做一些准备工作
  },

  // 重置提交后的状态
  resetAfterCommit(containerInfo) {
    // 渲染完成后,将结果转换为小程序数据
    const nodes = this.convertToMiniProgramData(containerInfo)
    containerInfo.page.setData({ nodes })
  },

  // 转换为小程序数据结构
  convertToMiniProgramData(container) {
    function convertNode(node) {
      if (node.type === 'text') {
        return { type: 'text', text: node.text }
      }

      return {
        type: node.type,
        props: node.props,
        children: node.children ? node.children.map(convertNode) : []
      }
    }

    return container.children.map(convertNode)
  }
}

// 创建 Reconciler 实例
const reconciler = Reconciler(hostConfig)

// 渲染函数
export function render(element, container, callback) {
  const root = reconciler.createContainer(container, false, false)
  reconciler.updateContainer(element, root, null, callback)
}

5.2 使用示例

// 小程序页面
import { render } from './miniprogram-renderer'
import App from './App'

Page({
  data: {
    nodes: []
  },

  onLoad() {
    // 使用自定义渲染器渲染 React 组件
    render(<App />, {
      page: this,
      children: []
    })
  }
})

六、完整实现流程详解(从 React 到小程序显示)

6.1 核心关联机制:数据流

关键理解:WXML 和 React 的关联是通过数据绑定实现的,而不是直接关联。

React 组件
  ↓ (React Reconciler 渲染)
渲染器转换
  ↓ (生成小程序数据结构)
setData 更新
  ↓ (小程序数据更新)
WXML 数据绑定
  ↓ ({{nodes}} 渲染)
小程序显示

6.2 详细流程步骤

步骤 1:编写 React 组件
// src/pages/index/App.jsx
import React from 'react'

export default function App() {
  return (
    <View className="container">
      <Text>Hello World</Text>
    </View>
  )
}
步骤 2:在小程序页面中调用渲染器
// miniprogram/pages/index/index.js
import { render } from '../../src/renderer/miniprogram-renderer'
import App from '../../src/pages/index/App'

Page({
  data: {
    nodes: [] // 这个 nodes 就是连接 React 和 WXML 的桥梁
  },

  onLoad() {
    // 调用渲染器,将 React 组件转换为数据结构
    render(App, {
      page: this, // 传入小程序页面实例,用于调用 setData
      children: []
    })
  }
})
步骤 3:渲染器内部处理流程
// src/renderer/miniprogram-renderer.js
export function render(element, container, callback) {
  // 1. React Reconciler 开始工作
  const root = reconciler.createContainer(container, false, false)

  // 2. 更新容器,触发渲染流程
  reconciler.updateContainer(element, root, null, callback)

  // 注意:实际的转换和 setData 在 resetAfterCommit 中完成
}

// 在 hostConfig.resetAfterCommit 中:
resetAfterCommit(containerInfo) {
  // 3. 将渲染结果转换为小程序数据结构
  const nodes = this.convertToMiniProgramData(containerInfo)

  // 4. 通过 setData 更新小程序页面数据
  // 这一步是关键!将数据写入小程序的 data.nodes
  containerInfo.page.setData({ nodes })
}

转换后的数据结构示例

// 对于 <View><Text>Hello World</Text></View>
// 转换后的 nodes 数据结构:
;[
  {
    type: 'view',
    props: { class: 'container' },
    children: [
      {
        type: 'text',
        text: 'Hello World'
      }
    ]
  }
]
步骤 4:WXML 通过数据绑定渲染
<!-- miniprogram/pages/index/index.wxml -->
<template name="react-node">
  <block wx:for="{{nodes}}" wx:key="index">
    <!-- 当 item.type === 'view' 时,渲染 view 组件 -->
    <view wx:if="{{item.type === 'view'}}" class="{{item.props.class}}">
      <!-- 递归渲染子节点 -->
      <template is="react-node" data="{{nodes: item.children}}" />
    </view>

    <!-- 当 item.type === 'text' 时,渲染 text 组件 -->
    <text wx:elif="{{item.type === 'text'}}">{{item.text}}</text>
  </block>
</template>

<!-- 使用 template 渲染,nodes 来自 Page 的 data -->
<template is="react-node" data="{{nodes}}" />

关键点

  • {{nodes}} 绑定的是小程序 Page 的 data.nodes
  • setData({ nodes }) 执行后,WXML 会自动重新渲染
  • WXML 通过 wx:ifwx:elif 判断节点类型,递归渲染子节点

6.3 完整数据流示例

假设 React 组件是:

<View className="container">
  <Text>Hello World</Text>
</View>

流程 1:React Reconciler 渲染

React.createElement('View', { className: 'container' },
  React.createElement('Text', null, 'Hello World')
)

流程 2:渲染器转换

createInstance('View', { className: 'container' })
  → { type: 'view', props: { class: 'container' }, children: [] }

appendChild(viewInstance, textInstance)
  → viewInstance.children = [{ type: 'text', text: 'Hello World' }]

流程 3:转换为小程序数据

nodes = [
  {
    type: 'view',
    props: { class: 'container' },
    children: [{ type: 'text', text: 'Hello World' }]
  }
]

流程 4:setData 更新

this.setData({ nodes })
// 小程序 data.nodes 被更新

流程 5:WXML 渲染

<!-- WXML 读取 data.nodes -->
<template is="react-node" data="{{nodes}}" />

<!-- 渲染结果 -->
<view class="container">
  <text>Hello World</text>
</view>

6.4 为什么需要构建工具?

问题:小程序不支持直接运行 JSX 和 ES6+ 语法

解决方案:使用构建工具(如 Webpack、Rollup)将 React 代码编译为小程序可用的代码

src/pages/index/App.jsx (JSX + ES6)
  ↓ (Babel 编译)
src/pages/index/App.js (ES5)
  ↓ (打包)
miniprogram/pages/index/index.js (小程序可运行)

6.5 完整 Demo 项目

我已经创建了一个完整的可执行 demo,位于 docs/react-to-miniprogram-demo/ 目录。

项目结构

react-to-miniprogram-demo/
├── src/                          # React 源码
│   ├── components/               # React 组件
│   ├── pages/index/App.js        # Hello World 组件
│   └── renderer/                 # 渲染器
├── miniprogram/                  # 小程序目录
│   ├── pages/index/
│   │   ├── index.js             # 调用渲染器
│   │   ├── index.wxml           # 数据绑定渲染
│   │   └── index.wxss           # 样式
│   └── app.js
└── package.json

关键文件说明

  1. src/pages/index/App.js:React 组件,返回 <View><Text>Hello World</Text></View>
  2. src/renderer/miniprogram-renderer.js:渲染器,将 React 转换为小程序数据
  3. miniprogram/pages/index/index.js:小程序页面,调用 render(App, container),触发 setData({ nodes })
  4. miniprogram/pages/index/index.wxml:通过 {{nodes}} 数据绑定渲染

执行流程

1. 小程序页面加载 (onLoad)
   ↓
2. 调用 render(App, container)
   ↓
3. React Reconciler 渲染组件
   ↓
4. 调用 hostConfig.createInstance/createTextInstance
   ↓
5. 调用 hostConfig.appendChild 构建节点树
   ↓
6. 调用 hostConfig.resetAfterCommit
   ↓
7. convertToMiniProgramData 转换为小程序数据
   ↓
8. container.page.setData({ nodes }) 更新数据
   ↓
9. WXML 通过 {{nodes}} 自动重新渲染
   ↓
10. 显示 "Hello World"

数据流示例

// React 组件
<View className="container">
  <Text>Hello World</Text>
</View>

// 渲染器转换后的数据结构
nodes = [
  {
    type: 'view',
    props: { class: 'container' },
    children: [
      { type: 'text', text: 'Hello World' }
    ]
  }
]

// setData 更新
this.setData({ nodes })

// WXML 渲染
<view class="container">
  <text>Hello World</text>
</view>

七、实际使用指南

6.1 安装依赖

首先需要安装 React 和 React Reconciler:

npm install react react-reconciler
# 或
yarn add react react-reconciler

6.2 项目结构

推荐的项目结构:

miniprogram-project/
├── src/                          # React 源码目录
│   ├── components/               # React 组件
│   │   ├── View.jsx
│   │   ├── Text.jsx
│   │   └── Button.jsx
│   ├── pages/                    # React 页面组件
│   │   └── index/
│   │       └── App.jsx
│   └── renderer/                 # 渲染器代码
│       └── miniprogram-renderer.js
├── miniprogram/                  # 小程序目录
│   ├── pages/
│   │   └── index/
│   │       ├── index.js         # 小程序页面逻辑
│   │       ├── index.wxml       # 小程序模板
│   │       ├── index.wxss       # 小程序样式
│   │       └── index.json       # 小程序配置
│   └── app.js
└── package.json

6.3 创建渲染器

创建 src/renderer/miniprogram-renderer.js

import React from 'react'
import Reconciler from 'react-reconciler'

// 小程序渲染器配置
const hostConfig = {
  // 组件映射表
  componentMap: {
    View: 'view',
    Text: 'text',
    Button: 'button',
    Image: 'image',
    ScrollView: 'scroll-view',
    Swiper: 'swiper'
  },

  // 创建组件实例
  createInstance(type, props, rootContainerInstance) {
    const miniprogramType = this.componentMap[type] || type.toLowerCase()

    const instance = {
      type: miniprogramType,
      props: this.convertProps(props),
      children: [],
      parent: null
    }

    return instance
  },

  // 创建文本节点
  createTextInstance(text, rootContainerInstance) {
    return {
      type: 'text',
      text: String(text),
      parent: null
    }
  },

  // 转换属性
  convertProps(props) {
    const miniprogramProps = {}

    // className → class
    if (props.className) {
      miniprogramProps.class = props.className
    }

    // style 处理
    if (props.style) {
      if (typeof props.style === 'string') {
        miniprogramProps.style = props.style
      } else {
        // 对象转字符串
        miniprogramProps.style = Object.keys(props.style)
          .map(key => `${key}: ${props.style[key]}`)
          .join('; ')
      }
    }

    // 事件转换
    Object.keys(props).forEach(key => {
      if (key.startsWith('on')) {
        const eventMap = {
          onClick: 'bindtap',
          onChange: 'bindchange',
          onInput: 'bindinput',
          onSubmit: 'bindsubmit',
          onScroll: 'bindscroll'
        }
        const eventName = eventMap[key] || key.replace('on', 'bind').toLowerCase()
        miniprogramProps[eventName] = props[key]
      } else if (key !== 'className' && key !== 'style' && key !== 'children') {
        // 其他属性直接传递
        miniprogramProps[key] = props[key]
      }
    })

    return miniprogramProps
  },

  // 追加子节点
  appendChild(parentInstance, child) {
    child.parent = parentInstance
    if (!parentInstance.children) {
      parentInstance.children = []
    }
    parentInstance.children.push(child)
  },

  // 插入子节点
  insertBefore(parentInstance, child, beforeChild) {
    child.parent = parentInstance
    if (!parentInstance.children) {
      parentInstance.children = []
    }
    const index = parentInstance.children.indexOf(beforeChild)
    parentInstance.children.splice(index, 0, child)
  },

  // 移除子节点
  removeChild(parentInstance, child) {
    if (parentInstance.children) {
      const index = parentInstance.children.indexOf(child)
      parentInstance.children.splice(index, 1)
    }
    child.parent = null
  },

  // 更新属性
  commitUpdate(instance, updatePayload, type, oldProps, newProps) {
    const newMiniprogramProps = this.convertProps(newProps)
    Object.assign(instance.props, newMiniprogramProps)
  },

  // 提交文本更新
  commitTextUpdate(textInstance, oldText, newText) {
    textInstance.text = newText
  },

  // 获取父节点
  getParentInstance(instance) {
    return instance.parent
  },

  // 获取根容器上下文
  getRootHostContext() {
    return {}
  },

  // 获取子节点上下文
  getChildHostContext(parentHostContext, type) {
    return {}
  },

  // 准备更新(提交阶段前)
  prepareForCommit(containerInfo) {
    // 可以在这里做一些准备工作
  },

  // 重置提交后的状态
  resetAfterCommit(containerInfo) {
    // 渲染完成后,将结果转换为小程序数据
    const nodes = this.convertToMiniProgramData(containerInfo)
    containerInfo.page.setData({ nodes })
  },

  // 转换为小程序数据结构
  convertToMiniProgramData(container) {
    function convertNode(node) {
      if (node.type === 'text') {
        return { type: 'text', text: node.text }
      }

      return {
        type: node.type,
        props: node.props || {},
        children: node.children ? node.children.map(convertNode) : []
      }
    }

    return container.children ? container.children.map(convertNode) : []
  },

  // 其他必需的接口
  shouldSetTextContent() {
    return false
  },

  finalizeInitialChildren() {
    return false
  },

  getPublicInstance(instance) {
    return instance
  },

  prepareUpdate() {
    return true
  },

  clearContainer() {
    // 清空容器
  }
}

// 创建 Reconciler 实例
const reconciler = Reconciler(hostConfig)

// 渲染函数
export function render(element, container, callback) {
  const root = reconciler.createContainer(container, false, false)
  reconciler.updateContainer(element, root, null, callback)
}

// 卸载函数
export function unmount(container) {
  const root = reconciler.getContainerForHostContainer(container)
  if (root) {
    reconciler.updateContainer(null, root, null)
  }
}

6.4 创建 React 组件

创建 src/components/View.jsx

import React from 'react'

export function View({ children, className, style, onClick, ...props }) {
  return React.createElement(
    'View',
    {
      className,
      style,
      onClick,
      ...props
    },
    children
  )
}

创建 src/components/Text.jsx

import React from 'react'

export function Text({ children, className, style, ...props }) {
  return React.createElement(
    'Text',
    {
      className,
      style,
      ...props
    },
    children
  )
}

创建 src/components/Button.jsx

import React from 'react'

export function Button({ children, onClick, className, ...props }) {
  return React.createElement(
    'Button',
    {
      onClick,
      className,
      ...props
    },
    children
  )
}

6.5 创建 React 页面组件

创建 src/pages/index/App.jsx

import React, { useState } from 'react'
import { View } from '../../components/View'
import { Text } from '../../components/Text'
import { Button } from '../../components/Button'

export default function App() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
  }

  return (
    <View className="container">
      <Text className="title">Hello React MiniProgram!</Text>
      <Text className="count">计数: {count}</Text>
      <Button onClick={handleClick} className="btn">
        点击增加
      </Button>
    </View>
  )
}

6.6 在小程序页面中使用

创建 miniprogram/pages/index/index.js

import { render } from '../../src/renderer/miniprogram-renderer'
import App from '../../src/pages/index/App'

Page({
  data: {
    nodes: []
  },

  onLoad() {
    // 使用自定义渲染器渲染 React 组件
    render(
      App,
      {
        page: this,
        children: []
      },
      () => {
        console.log('渲染完成')
      }
    )
  },

  onUnload() {
    // 页面卸载时可以清理资源
  }
})

6.7 小程序模板文件

创建 miniprogram/pages/index/index.wxml

<!-- 小程序 WXML -->
<template name="react-node">
  <block wx:for="{{nodes}}" wx:key="index">
    <!-- view 组件 -->
    <view wx:if="{{item.type === 'view'}}" class="{{item.props.class}}" style="{{item.props.style}}">
      <template is="react-node" data="{{nodes: item.children}}" />
    </view>

    <!-- text 组件 -->
    <text wx:elif="{{item.type === 'text'}}" class="{{item.props.class}}">{{item.text}}</text>

    <!-- button 组件 -->
    <button wx:elif="{{item.type === 'button'}}"
            class="{{item.props.class}}"
            bindtap="{{item.props.bindtap}}">
      <template is="react-node" data="{{nodes: item.children}}" />
    </button>

    <!-- image 组件 -->
    <image wx:elif="{{item.type === 'image'}}"
           src="{{item.props.src}}"
           class="{{item.props.class}}"
           mode="{{item.props.mode}}" />

    <!-- scroll-view 组件 -->
    <scroll-view wx:elif="{{item.type === 'scroll-view'}}"
                 class="{{item.props.class}}"
                 scroll-y="{{item.props.scrollY}}"
                 bindscroll="{{item.props.bindscroll}}">
      <template is="react-node" data="{{nodes: item.children}}" />
    </scroll-view>
  </block>
</template>

<!-- 使用 template 渲染 -->
<view class="react-root">
  <template is="react-node" data="{{nodes}}" />
</view>

6.8 小程序样式文件

创建 miniprogram/pages/index/index.wxss

.react-root {
  width: 100%;
  min-height: 100vh;
}

.container {
  padding: 20px;
}

.title {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 20px;
}

.count {
  font-size: 18px;
  margin-bottom: 20px;
}

.btn {
  background-color: #007aff;
  color: white;
  padding: 10px 20px;
  border-radius: 4px;
}

6.9 构建配置

如果需要使用 JSX,需要配置 Babel:

创建 babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current'
        }
      }
    ],
    [
      '@babel/preset-react',
      {
        pragma: 'React.createElement',
        pragmaFrag: 'React.Fragment'
      }
    ]
  ]
}

6.10 实际开发注意事项

  1. 事件处理:小程序的事件对象和 React 的事件对象不同,需要在渲染器中转换
  2. 样式处理:小程序的样式支持有限,某些 CSS 特性可能不支持
  3. 性能优化:避免频繁的 setData,可以使用防抖或节流
  4. 组件库:可以使用现有的 React 组件库,但需要确保组件映射正确
  5. 状态管理:可以使用 Redux、MobX 等状态管理库
  6. 路由:需要自己实现路由系统,或使用小程序的路由 API

6.11 使用现有框架

实际上,不建议自己实现,推荐使用成熟的框架:

  • Remax:基于 React Reconciler 的小程序框架
  • Taro:支持 React 的多端框架(编译时转换)

这些框架已经处理了大部分兼容性问题,可以直接使用。


七、核心原理总结

7.1 工作流程

  1. 创建自定义渲染器:基于 React Reconciler 实现小程序渲染器
  2. React Reconciler 协调:React 使用 Reconciler 管理组件生命周期和更新
  3. 渲染器方法调用:Reconciler 调用渲染器的方法(createInstance、appendChild 等)
  4. 转换为小程序数据:在 resetAfterCommit 中将渲染结果转换为小程序数据结构
  5. 小程序渲染:通过 setData 更新小程序,使用 template 渲染

7.2 关键技术

  • React Reconciler:React 提供的协调器机制,允许创建自定义渲染器
  • 渲染器接口实现:实现 hostConfig 中的各种方法(createInstance、appendChild 等)
  • 组件映射:React 组件 → 小程序组件(在 createInstance 中实现)
  • 属性转换:React 属性 → 小程序属性(className → class,在 convertProps 中实现)
  • 事件转换:React 事件 → 小程序事件(onClick → bindtap)
  • 状态同步:Reconciler 自动处理 setState,在 resetAfterCommit 中调用 setData

7.3 优缺点

优点

  • ✅ 真正的 React:可以使用所有 React 特性
  • ✅ 灵活性高:完全兼容 React 生态
  • ✅ 开发体验好:纯 React 开发体验

缺点

  • ❌ 性能较差:需要运行时转换,性能不如原生
  • ❌ 包体积大:需要包含 React 和适配层代码
  • ❌ 兼容性问题:部分 React 特性可能无法完美适配