---
title: Render
summary: "介绍渲染脚本（RJS）的使用方法，用于高频 Canvas 绘图和图形渲染场景。"
questions:
  - 什么是渲染脚本（RJS），它的主要应用场景有哪些？
  - Render 实例对象提供了哪些方法？
  - 如何通过 getCanvasById 获取 canvas 对象？
  - getSystemInfo() 返回的系统信息包含哪些字段？
  - eventChannel 实例对象的作用是什么？如何实现 RJS 间通信？
  - 为什么禁止跨页面或组件文件使用渲染脚本？
  - Render 函数的注册格式有哪些要求？
  - RJS 与逻辑层之间的数据交互有什么限制？
  - 在哪个生命周期中执行 canvas 绘制最合适？为什么？
  - RJS 支持哪些图形库插件？
---

# Render Script

**渲染脚本**，可用于处理高频的绘图需求，可以提高视图的动画渲染性能。文件名后缀为 `.rjs`, 提供 `Render()` 函数进行声明一个渲染脚本模块，与页面或组件结合使用。主要应用场景 canvas 图表渲染，webGL 图形渲染等。

- 渲染函数注册 `Render()` 必须在 `*.rjs` 中调用，必须调用且只能调用一次。不然会出现无法预期的效果。
- 渲染脚本仅提供 `api` 对 `canvas` 进行操作。
- 在智能小程序中，您可以通过 `ty.createCanvasContext` 来绘制 `canvas`，但是该操作需要依赖逻辑层到视图层通信。如果您需要频繁地绘制 `canvas`，那么渲染脚本可能更适合您。
- 渲染脚本环境有独立的执行环境， `window` `document` `localStorage` 等全局对象将会是一个空对象，您可以依据实际情况对全局对象进行适配。
- 如果您需要在`Ray`中使用，请查看[Ray RJS](/cn/miniapp/develop/ray/framework/render)

> 此功能要求 Tuya MiniApp IDE 版本大于 **0.3.0**
> eventChannel 要求基础库版本大于 2.18.0

## 实例对象

Render 对象实例化后，可通过 `this.instance` 进行逻辑编写。

- `instance`

  - `callMethod(name: string, ...args: any[]): void`: 调用关联页面或组件实例中的方法。
  - `getCanvasById(id: string): Promise<HTMLCanvas | null>` 通过`canvas id`获取`canvas`对象。返回一个[Canvas](/cn/miniapp/develop/miniapp/api/canvas/RJS-Canvas/Canvas)对象。

  ```js
  // index.rjs
  export default Render({
    async init(id) {
      const canvas = await this.instance.getCanvasById(id);
      // 获取到页面中 ID 的 canvas 节点
    },
  });
  ```

  - `getSystemInfo(): SystemInfo` 获取系统相关信息。返回一个对象，包含以下内容。

    | 变量名       | 备注                  | 类型   |
    | ------------ | --------------------- | ------ |
    | screenWidth  | 屏幕宽度              | number |
    | screenHeight | 屏幕高度              | number |
    | navbarHeight | 顶部导航栏高度        | number |
    | tabbarHeight | 底部 tab 栏高度       | number |
    | platform     | 设备类型：android/ios | string |
    | statusHeight | 状态栏高度            | number |
    | pixelRatio   | 像素比                | number |
    | orientation  | 屏幕状态（横竖屏）    | string |

  - `getBoundingClientRectById(id: string): Rect` 获取对应节点相关信息，包含以下内容。

    | 变量名 | 类型   |
    | ------ | ------ |
    | left   | number |
    | right  | number |
    | top    | number |
    | bottom | number |
    | width  | number |
    | height | number |

  - `createWorker(path: string): Worker`: 创建worker对象。

    | 方法名                                  | 备注               |
    | --------------------------------------- | ------------------ |
    | postMessage(...args: any[])             | 向 worker 发送消息 |
    | onMessage(fn: (...args: any[]) => void) | 监听 worker 消息   |
    | terminate(): void                       | 关闭 worker        |

  - `eventChannel`: eventChannel实例对象，用来进行 RJS 间或 RJS 和 SJS 的事件通信。 **需要注意 事件名唯一，避免事件冲突。 要求基础库版本大于 2.18.0**

例如： `page1/index.rjs` 与 `page2/index.rjs` 使用 `this.instance.eventChannel` 进行通信

```js
 // page1: index.rjs
  export default Render({
    init(id) {
      this.instance.getCanvasById(id).then((canvas) => {
        // 获取到页面中 ID 的 canvas 节点
        // ...
      });
      this.instance.eventChannel.emit('eventName', { data: +new Date  });
    },
  });
```

```js
// page2: index.rjs
export default Render({
  foo(e){
    // ...
    console.log(e)
  },
  init(id) {
    this.instance.eventChannel.on('eventName', foo);
  },
  // 在canvas销毁时取消监听
  destory(){
     this.instance.eventChannel.off('eventName', foo);
  }
});
```

## 图形库插件

- [F2](https://antv-f2.gitee.io/api/f2)
- [ECharts](https://echarts.apache.org/zh/index.html)
- [Three.js](https://threejs.org/)

使用方法请查看[插件系统](/cn/miniapp/develop/miniapp/framework/plugin/intro)

## 注意事项

### 1. 一致性

渲染脚本作为页面 和 组件的一部分，等同于 `index.json` 与 `index.tyss` 的关系。一个 `Page` 或一个 `Component` 只有一个渲染脚本。

- 禁止跨页面文件或组件文件使用渲染脚本，否则会构建失败。如需复用 `rjs`，可通过抽象 `js` 文件进行逻辑拆分。

```js
// 构建失败
import MyRender from '../other/index.rjs';
Page({
  onLoad() {
    this.render = new MyRender(this);
  },
});
```

- 渲染函数实例化时与页面实例或组件实例进行关联

```js
import MyRender from './index.rjs';
Page({
  onLoad() {
    this.render = new MyRender(this);
  },
});
```

### 2. 独立运行环境

- 渲染脚本支持引入其他 js 模块或三方工具包。渲染脚本有独立的执行环境，如果您引用的三方库使用访问了无法访问的对象，需要对该全局对象进行适配。

### 3. 渲染函数注册格式

- `Render` 函数入参必须声明一个对象字面量，不可传入一个变量。

```js
// 支持
export default Render({ draw() {} });
// 不支持
const config = { draw() {} };
export default Render(config);
```

- 必须默认导出 `Render` 函数

```js
export default Render({...})
// 或
module.exports = Render({...})
```

### 4. 交互数据需可序列化

- 执行渲染脚本中方法时，入参数据必须为可序列化的数据内容。

```js
// index.js
import Render from './index.rjs';
Page({
  onLoad: function () {
    this.render = new Render(this);
  },
  onReady() {
    console.log(render.test({ fn: function () {} })); // fn 不可序列化
  },
});
```

- 在渲染脚本需要调用实例方法，可使用 `this.instance.callMethod('method', 'arg1', 'arg2')`，其中 `method` 是实例方法名，`arg1`, `arg2` 是入参数据。
- 逻辑层调用 `RJS` 是一个事件，因此并不存在 `return`，数据交互请使用 `callMethod`。

```js
// index.rjs
export default Render({
  test() {
    this.instance.callMethod('testRjs', 'text'); // 正确
    return 'test'; // 错误
  },
});
```

```js
// index.js
import Render from './index.rjs';
Page({
  onLoad: function () {
    this.render = new Render(this);
  },
  testRjs: function (arg1) {
    console.log('接收到了RJS的参数, 仅支持可序列化参数', arg1); // args = test
  },
  onReady() {
    console.log(render.test()); // 页面实例无法直接获取 return 的数据
  },
});
```

### 5. 执行绘制 canvas 生命周期

在执行 `canvas` 绘制应在页面的 `onReady` 生命周期、或组件的 `ready` 生命周期，在其他生命周期中执行可能会出现 canvas 节点高度获取不正确，导致绘制异常的情况。

- 页面

```js
// index.js
import Render from './index.rjs';
Page({
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (query) {
    this.render = new Render(this);
  },
  onReady() {
    // 在此生命周期执行绘制
    this.render.draw();
  },
});
```

- 组件

```js
// index.js
import Render from './index.rjs';
Component({
  lifetimes: {
    created: function () {
      this.render = new Render(this);
    },
    ready: function (e) {
      // 在此生命周期执行绘制
      this.render.draw();
    },
  },
});
```

### 6. 支持 Promise 函数返回值

通过方式可简化与页面/组件实例进行通信的成本，避免频繁使用 `this.instance.callMethod`

- 基础库要求 `>= 2.21.0`
- Tuya MiniApp IDE `>= 0.7.1`

```js
import Render from './index.rjs';
Page({
  onLoad: async function (query) {
    this.render = new Render(this);
    const result = await this.render.sum(1, 2);
  }
});
```

```js
// index.rjs
export default Render({
  async sum(a, b) {
    return a + b
  }
});
```

## 示例代码

### 1.绘制 canvas 动画

```xml
<!-- index.tyml -->
<canvas type="2d" canvas-id="canvas"></canvas>
```

```js
// index.js
import Render from './index.rjs';
let render;
Page({
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function () {
    render = new Render(this);
  },

  onReady() {
    render.renderCar();
  },
});
```

```js
// index.rjs
export default Render({
  position: {
    x: 150,
    y: 150,
    vx: 2,
    vy: 2,
  },
  x: -100,

  async renderCar() {
    let canvasCar = await this.instance.getCanvasById('canvas');
    let width = canvasCar.width;
    let height = canvasCar.height;

    let ctx = canvasCar.getContext('2d');

    let dpr = 1;
    canvasCar.width = width * dpr;
    canvasCar.height = height * dpr;
    ctx.scale(dpr, dpr);

    let renderLoop = () => {
      this.render(canvasCar, ctx);
      canvasCar.requestAnimationFrame(renderLoop);
    };
    canvasCar.requestAnimationFrame(renderLoop);

    let img = canvas.createImage();
    img.onload = () => {
      this._img = img;
    };
    img.src = './car.png';
  },

  render(canvas, ctx) {
    ctx.clearRect(0, 0, 300, 300);
    this.drawBall(ctx);
    this.drawCar(ctx);
  },

  drawBall(ctx) {
    let p = this.position;
    p.x += p.vx;
    p.y += p.vy;
    if (p.x >= 300) {
      p.vx = -2;
    }
    if (p.x <= 7) {
      p.vx = 2;
    }
    if (p.y >= 300) {
      p.vy = -2;
    }
    if (p.y <= 7) {
      p.vy = 2;
    }

    function ball(x, y) {
      ctx.beginPath();
      ctx.arc(x, y, 5, 0, Math.PI * 2);
      ctx.fillStyle = '#1aad19';
      ctx.strokeStyle = 'rgba(1,1,1,0)';
      ctx.fill();
      ctx.stroke();
    }

    ball(p.x, 150);
    ball(150, p.y);
    ball(300 - p.x, 150);
    ball(150, 300 - p.y);
    ball(p.x, p.y);
    ball(300 - p.x, 300 - p.y);
    ball(p.x, 300 - p.y);
    ball(300 - p.x, p.y);
  },

  drawCar(ctx) {
    if (!this._img) return;
    if (this.x > 350) {
      this.x = -100;
    }
    ctx.drawImage(this._img, this.x++, 150 - 25, 100, 50);
    ctx.restore();
  },

  async renderImageData() {
    let canvas3 = await this.instance.getCanvasById('canvas3');
    let ctx = canvas3.getContext('2d');
    let imgData = ctx.createImageData(100, 100);
    for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
      imgData.data[i + 0] = 255;
      imgData.data[i + 1] = 0;
      imgData.data[i + 2] = 0;
      imgData.data[i + 3] = 155;
    }
    ctx.putImageData(imgData, 50, 50);
  },
});
```

### 2.绘制 ImageData 和 Path2D

```xml
<!-- index.tyml -->
<canvas
  type="2d"
  canvas-id="canvas3"
  style="width: 300px; height: 300px;border: 1px solid orange"
></canvas>
<canvas
  type="2d"
  canvas-id="canvas4"
  style="width: 300px; height: 300px;border: 1px solid blue"
></canvas>
```

```js
// index.js
import Render from './index.rjs';
let render;
Page({
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (query) {
    render = new Render(this);
  },

  onReady() {
    render.renderImageData();
    render.renderPath2D();
  },
});
```

```js
// index.rjs
export default Render({
  async renderImageData() {
    let canvas3 = await this.instance.getCanvasById('canvas3');
    let ctx = canvas3.getContext('2d');
    let imgData = ctx.createImageData(100, 100);
    for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
      imgData.data[i + 0] = 255;
      imgData.data[i + 1] = 0;
      imgData.data[i + 2] = 0;
      imgData.data[i + 3] = 155;
    }
    ctx.putImageData(imgData, 50, 50);
  },

  async renderPath2D() {
    let canvas4 = await this.instance.getCanvasById('canvas4');
    let ctx = canvas4.getContext('2d');
    let path1 = canvas4.createPath2D();
    path1.rect(10, 10, 100, 100);
    let path2 = canvas4.createPath2D(path1);
    path2.moveTo(220, 60);
    path2.arc(170, 60, 50, 0, 2 * Math.PI);
    ctx.stroke(path2);
  },
});
```

### 3.绘制图表

```xml
<!-- tyml -->
<canvas canvas-id="f2" class="chart" />
<button bindtap="draw">渲染图表</button>
```

```js
// index.js
import Render from './index.rjs';
let render;
Page({
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function () {
    render = new Render(this);
  },

  onReady() {
    this.draw();
  },

  draw() {
    render.draw([
      { genre: 'Sports', sold: Math.floor(Math.random() * 500) },
      { genre: 'Strategy', sold: Math.floor(Math.random() * 500) },
      { genre: 'Action', sold: Math.floor(Math.random() * 500) },
      { genre: 'Shooter', sold: Math.floor(Math.random() * 500) },
      { genre: 'Other', sold: Math.floor(Math.random() * 500) },
    ]);
  },
});
```

```js
// index.rjs
import F2 from '@antv/f2';
let chart;
export default Render({
  position: {
    x: 150,
    y: 150,
    vx: 2,
    vy: 2,
  },
  x: -100,
  async draw(data) {
    if (chart) {
      chart.clear(); // 清除
      chart.interval().position('genre*sold').color('genre');
      // Step 2: 载入数据源
      chart.source(data);
      // Step 4: 渲染图表
      chart.render();
    } else {
      let canvas = await this.instance.getCanvasById('f2');
      // Step 1: 创建 Chart 对象
      chart = new F2.Chart({
        el: canvas,
        pixelRatio: this.instance.getSystemInfo().pixelRatio || 2, // 指定分辨率
      });

      // Step 2: 载入数据源
      chart.source(data);

      // Step 3：创建图形语法，绘制柱状图，由 genre 和 sold 两个属性决定图形位置，genre 映射至 x 轴，sold 映射至 y 轴
      chart.interval().position('genre*sold').color('genre');

      // Step 4: 渲染图表
      chart.render();
    }
  },
});
```

### 4.绘制 lottie 动画 & 调用逻辑层方法

- 如需使用 lottie，请在 `index.rjs` 中引入[lottie-miniapp](https://github.com/landn172/lottie-miniapp)

```xml
<!-- tyml -->
<canvas type="2d" canvas-id="canvas5"></canvas>
```

```js
// index.js
import Render from './index.rjs';
let render;
Page({
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (query) {
    render = new Render(this);
  },
  animationPlay: function (arg1, arg2) {
    console.log('绘制动画', arg1, arg2);
  },
  onReady() {
    render.renderLottie();
  },
});
```

```js
// index.rjs
import { lottieData } from './lottie-data';
import lottie from 'lottie-miniapp';
export default Render({
  async renderLottie() {
    let canvas5 = await this.instance.getCanvasById('canvas5');
    let canvasContext = canvas5.getContext('2d');
    lottie.loadAnimation({
      renderer: 'canvas', // 只支持canvas
      loop: true,
      autoplay: true,
      animationData: lottieData,
      // path: animationPath,
      rendererSettings: {
        // 这里需要填 canvas
        canvas: canvas5,
        context: canvasContext,
        clearCanvas: true,
      },
    });
    this.instance.callMethod('animationPlay', 'arg1', 'arg2');
  },
});
```

## 示例仓库

### 1. 使用UChart绘制图表

<DemoBlock
  githubUrl="https://github.com/Tuya-Community/tuya-miniapp-demo/tree/master/uchart"
  qrCodeUrl="/images/qrCode/uchart.png"
  lang="zh">
</DemoBlock>

### 2. 获取图片像素点

<DemoBlock
  githubUrl="https://github.com/Tuya-Community/tuya-miniapp-demo/tree/master/getImageData"
  qrCodeUrl="/images/qrCode/getImageData.png"
  lang="zh">
</DemoBlock>

### 3.RJS插件系统

<DemoBlock
  githubUrl="https://github.com/Tuya-Community/tuya-miniapp-demo/tree/master/rjsPlugin"
  qrCodeUrl="/images/qrCode/rjsPlugin.png"
  lang="zh">
</DemoBlock>

### 4.Canvas图形库

<DemoBlock
  githubUrl="https://github.com/Tuya-Community/tuya-miniapp-demo/tree/master/canvasLibExample"
  qrCodeUrl="/images/qrCode/canvasLib.png"
  lang="zh">
</DemoBlock>
