场景

场景Scene是一种特殊的元素,对于所有的xr-frame 微信小程序组件,其最外层必须有一个xr-scene标签作为根元素,并且组件内只能有一个,以此作为整个组件的基础。

和一般仅用于组合的元素不同,场景有以下几个特别之处:

  1. 其下挂载的组件都是名为系统System的特殊组件,来驱动逻辑和渲染。
  2. 增加了一些方法,用于创建资源、查询等等。
  3. 每个元素、组件、资源都是属于当前被包在的xr-scene标签对应的元素中的。

在我们编写组件时,常常用到的那个this.scene,其实就是这个元素。

用途

一般我们需要用到的是场景的创建和查询能力。

创建资源可以使用类似:

// 脱离于资源加载工作流,创建属于这个场景的纹理资源
const texture = scene.createTexture(options);

// 直接创建一个元素,但注意要和`Shadow元素`一起使用
const el = scene.createElement(xrFrameSystem.XRNode);

的方式,更多的创建可见API文档。

同样在API文档中我们还可以看到有一些查询接口,比如上一节提到的scene.getElementById,以及scene.width这种获取画布信息的,scene.assets这种获取系统实例引用的,开发者可以按需使用。

获取场景实例

要使用场景需要先获取实例,在组件、加载器等中,我们可以直接用this.scene获取,而在用户微信小程序脚本中,则需要通过事件:

<xr-scene bind:ready="handleReady">
......
</xr-scene>
// 微信小程序脚本中绑定的事件
handleReady({detail}) {
  const scene = detail.value;
}

事件

场景元素提供了以下事件:

事件 参数 立即 wxml 时机
ready 场景第一次解析完毕
tick 数字,delta(ms) 一帧驱动开始
pause 场景暂停,一般是是压后台
resume 场景恢复,一般是从后台唤醒

事件

事件管理器从属于元素,提供给组件的设计者一个向使用者派发事件的手段。

事件有两种使用方式,一种是走传统的脚本逻辑,一种是用微信小程序wxml中的事件绑定。

脚本逻辑

在脚本中使用事件很简单,用一段代码例子来解释:

// 定义监听器,比如这个事件的参数类型是`number`
function handleTest(params: number, sender: XrFrame.Element) {
  console.log('test', params, sender);
}

// 添加事件监听器
el.event.add('test', handleTest);

// 移除事件监听器
el.event.remove('test', handleTest);

// 添加事件监听器,但会在一次触发后自动移除
el.event.addOnce('test', handleTest);

wxml绑定

wxml中进行事件绑定会稍微复杂一些,还是来看个例子:

<xr-scene bind:tick="handleTick">
</xr-scene>

这里我们给xr-scene标签绑定了tick事件,事件的handler被定义在组件的method内,和其他的微信小程序组件一致。

这里要注意的是如此绑定的事件获取事件参数的方法不同:

handleTick: function(event) {
  const {value, el} = event.detail;
}

这里取出的value就是事件的参数,el则是派发事件的那个元素。

派发事件

知道了如何监听事件,我们还需要知道如何派发,依照以上两种监听方式,事件的触发有不同参数:

el.event.trigger(type, event, immediately, toXML, bubbles);

immediately是指是否要立即触发,默认是true,如果为false,则会对当帧同名的事件进行合并,然后在当帧组件生命周期的驱动前派发。

toXML是说这个事件是否要派发到xml上,来允许开发者使用事件绑定,默认是true

bubbles是指事件是否要沿着xr-frame的节点树冒泡,同时也会影响在xml中的冒泡。

注意在Shadow元素的子孙元素中,如果要让事件能够在xml中被绑定,bubbles必须为true

元素

元素Element本身没有逻辑,其主要负责两部分——组件的聚合和属性代理。所以在阅读以下内容之前,请保证先阅读了组件的相关内容。

元素有两种使用方式,在xml中直接对应于标签,也可以在脚本中动态创建,但注意动态创建的时候,必须作为Shadow元素的子孙结点!!!

定制一个元素

让我们从定制一个元素开始,理解它的功能:

import XrFrame from 'XrFrame';
const xrFrameSystem = wx.getXrFrameSystem();

// 指定默认挂载的组件
const AutoRotateTouchableGLTFDefaultComponents: XrFrame.IEntityComponents = Object.assign({
  'mesh-shape': {},
  'auto-rotate': {}
}, xrFrameSystem.GLTFDefaultComponents);

// 指定一个映射,将`xml`上特定的属性,映射为组件的数据
const AutoRotateTouchableGLTFDataMapping: {[key: string]: string[];} = Object.assign({
  speed: ['auto-rotate', 'speed']
}, xrFrameSystem.GLTFDataMapping);

// 定义元素类
class XRAutoRotateTouchableGLTF extends xrFrameSystem.Element {
  // 指定默认组件集
  public readonly defaultComponents: XrFrame.IEntityComponents = AutoRotateTouchableGLTFDefaultComponents;
  // 指定默认数据映射
  public readonly dataMapping: {[key: string]: string[];} = AutoRotateTouchableGLTFDataMapping;
}

// 注册元素实现`XRAutoRotateTouchableGLTF`为名字`auto-rotate-touchable-gltf`
xrFrameSystem.registerElement('auto-rotate-touchable-gltf', XRAutoRotateTouchableGLTF);

在这个自定义元素中,我们首先指定了它初始化时就要挂载的组件,这其中有两部分,第一部分是xrFrameSystem.GLTFDefaultComponents,这其实就是xr-gltf组件默认挂载的组件集合,之后在其上追加了mesh-shape和上一节定制的auto-rotate组件。

其次我们指定了映射,还是分为了两部分,第一部分是xrFrameSystem.GLTFDataMapping,第二部分是追加的speed。来看看speed的值,数组第一个值为auto-rotate,就是挂载在这个元素上的组件名字,第二个值是speed,就是说要将写在标签上的speed属性直接映射到auto-rotate组件的数据speed上。

注意,所有的内置组件的映射,都是会将如isARCamera这样的驼峰,映射为is-ar-camera这样的小写加中划线分割的形式。 从这里我们可以看出,元素确实是用组合而非继承的方式实现的功能。最后我们继承自Element实现了元素,并用registerElement将其注册到了框架中。

使用这个元素

和组件一样,使用元素的方式同样有两种:

在wxml使用

首先是在wxml中使用,只需要按照注册时的名字使用就好:

<xr-auto-rotate-touchable-gltf id="artg" position="-2 0 0" model="gltf-damageHelmet" speed="1 0 0" bind:drag-shape="handleDrag" bind:touch-shape="handleTouchStart" bind:untouch-shape="handleTouchEnd" />

可以看到,这里确实直接写了speed属性。

组件的属性写法和数据映射是可以共存的,同时数据映射并不先要求组件存在。

手动使用

我们也可以在运行时来代码创建元素和添加到场景,但注意绝对不能将其添加到Shadow元素以外的元素!!!

为了避免开发者失误,创建和添加详见Shadow元素。

const node = scene.createElement(xrFrameSystem.XRNode, {
  position: '1 1 1'
});

node.setAttribute(position, '2 2 2');

这段代码通过createElement方法创建了一个元素,注意第二个初始化参数传入的是属性,其完全对应于标签上的名字,值也是字符串,接下来我们还通过setAttribute方法更新了其属性。

但建议不要使用setAttribute去更新属性,而是先获取组件,然后用组件中的setData方法来设置,省去字符串解析!

查找

有时候我们需要获得元素的实例,来进一步进行其他操作,以下几个方法可以帮助我们拿到它:

// 通过在`wxml`的元素上设置的`id`索引,比如上面例子中的`artg`,`id`是唯一的
scene.getElementById(id);

// 获取元素的第`i`个子元素
el.getChildAtIndex(i);

// 通过一个`filter`函数,获取第一个子元素或者子元素集
el.getChildByFilter(filter);
el.getChildrenByFilter(filter);

// 通过类获取子元素
el.getChildByClass(clz);

// 通过在`wxml`元素上设置的名字`name`获取子元素,`name`可以不唯一
el.getChildName(name);
el.getChildrenName(name);

事件

每个元素上都有一个事件管理器,来管理其下挂载的所有组件的事件派发,这个派发也可以触达到wxml的事件绑定,详见事件。

组件数据解析

在组件一章我们知道需要有一个解析器将xml中组件对应属性的字符串转换为组件需要的数据类型,对此,微信小程序框架提供了一套机制来处理。

数据解析器

数据解析器IDataValueHandler就是用来将属性字符串转换成特定类型的数据的,其具体定义如下:

interface IDataValueHandler<TDataValue> {
  create(value: string, defaultValue: any, scene: Scene): TDataValue;
}

可见主要是一个create方法,其接受一个字符串的value,一个组件schema中定义的默认值defaultValue和场景引用scene,返回解析后的值。一般我们会如此注册一个解析器:

registerDataValue('number', {create: (value: string, defaultValue: any, scene: Scene) => {
  return value === undefined ? defaultValue : parseFloat(value));
}});

这里注册了number类型的解析器,可以看到如果没有传入值则返回默认值,否则用parseFloat转换。

除了registerDataValue方式注册的数据类型外,还有一类特殊类型的数据,它们就是资源

特殊类型-资源

资源数据和number这样的普通数据有些不同,其注册一般不使用registerDataValue,而是在registerAssetLoader时定义的。如何定义可以参考资源加载器的内容,一般来讲资源数据使用时填写的值都是资源ID,比如你要使用纹理资源:

<xr-asset-load type="texture" asset-id="waifu" src="/assets/textures/waifu.jpg" />
<xr-asset-material asset-id="simple-mat" uniforms="u_baseColorMap: waifu" />

我们先加载了一张id为waifu的纹理,然后在下面材质的uniform的u_baseColorMap属性使用了它。

资源类型的数据在schema中描述时,默认值是资源的id 注意资源类型的数据还会影响到组件的onAddonUpdate生命周期,微信小程序框架会在每一次资源数据更新后,先等待引用的资源加载完成,才会进入这两个生命周期

内置数据类型

微信小程序框架内置了一些数据类型:

非资源数据

类型 例子 转换 说明
string henshin: KuugaAgitoRyuki FaziBladeHibikiKabutoDenOkiva DecadeWOOOFourzeWizardGaim DriveGhostExaidbuildGrandZio ‘KuugaAgitoRyuki FaziBladeHibikiKabutoDenOkiva DecadeWOOOFourzeWizardGaim DriveGhostExaidbuildGrandZio’ 字符串,不作任何处理
number truth:42 42 数字,会转成float,支持其他进制如0xff0b11
boolean yiyandingzhen:false false 布尔,当写’false’时为false,否则均为true(包括不写值)
array producer:wowaka neru kurogaki [‘wowaka’,’neru’,’kurogaki’] 字符串数组,用空格分割
number-array idolOffice:7 6 5 [7,6,5] 数字数组,用空格分割
color rem:0.57 0.75 1 1 [0.57,0.75,1,1] 颜色,rgba,也可以使用#fff这种方式
map cat:10,dog:8,fox:6 [[‘cat’,10],[‘dog’,8],[‘fox’,6]] 映射形式是key:value,用,分割
dict camp:瞬光:混乱善良,roam:中立善良,xinyi:绝对中立 {瞬光:’混乱善良’,roam:’中立善良’,xinyi:’绝对中立’} 字典,和map类型写起来一样,转换结果不同
transform target:homo nodeId为homo的Transform组件引用 变换,可以用于索引标记过nodeId的变换组件

资源数据

资源数据可见内置的各种资源:效果,图片,纹理,材质,几何数据,模型,渲染目标,帧动画。

组件

组件Component用于实现xr-frame中所有的逻辑,以生命周期拉驱动。它们在wxml中对应于每个标签上的属性,比如<xr-element transform="position: 1 1 1;" />,就是在xr-element标签上挂在了一个transform组件。组件的聚合构成了后面章节的元素,也就是wxml中对应的标签。

组件的构成主要分为两部分——数据定义和生命周期。

让我们定制一个组件

通过定制一个简单的组件,我们可以对组件的机制有比较清晰的了解:

// 这里只包括类型信息
import XrFrame from 'XrFrame';
// 这里是实例
const xrFrameSystem = wx.getXrFrameSystem();

// 自定义组件的数据接口
export interface IAutoRotateData {
  speed: number[];
}

// 自定义组件的`schema`,详见后面论述
const AutoRotateSchema: XrFrame.IComponentSchema = {
  speed: {type: 'number-array', defaultValue: [1, 1, 1]}
}

// 定义组件
class AutoRotate extends xrFrameSystem.Component<IAutoRotateData> {
  public readonly schema: XrFrame.IComponentSchema = AutoRotateSchema;

  private _speedX: number = 1;
  private _speedY: number = 1;
  private _speedZ: number = 1;

  // 所挂载的`element`被挂载到场景时触发的回调。
  public onAdd(parent: XrFrame.Element, data: IAutoRotateData): void {
    this._processData(data);
  }

  // 数据更新时触发的回调。
  public onUpdate(data: IAutoRotateData, preData: IAutoRotateData): void {
    this._processData(data);
  }

  // 渲染每帧触发的回调。
  public onTick(delta: number, data: IAutoRotateData) {
    const trs = this.el.getComponent(xrFrameSystem.Transform);

    // 如果没有挂载到一个`Node`节点,则不生效。
    if (!trs) {
      return;
    }

    // 其实这里实现有点问题,不过是例子也无伤大雅了(逃
    trs.rotation.x += this._speedX * 0.1;
    trs.rotation.y += this._speedY * 0.1;
    trs.rotation.z += this._speedZ * 0.1;
  }

  // 所挂载的`element`从父节点`parent`被移除时,或者自己从`element`上呗移除时,触发的回调。
  // 一般用于消除功能的运作。
  public onRemove(parent: XrFrame.Element, data: IAutoRotateData): void {

  }

  // 从被挂载的`element`上被移除,或是`element`被销毁时,触发的回调。
  // 一般用于释放持有的资源。
  public onRelease(data: IAutoRotateData) {

  }

  private _processData(data: IAutoRotateData) {
    this._speedX = data.speed?.[0] !== undefined ? data.speed[0] : 1;
    this._speedY = data.speed?.[1] !== undefined ? data.speed[1] : 1;
    this._speedZ = data.speed?.[2] !== undefined ? data.speed[2] : 1;
  }
}

// 最后将组件注册进框架,名为`auto-rotate`
xrFrameSystem.registerComponent('auto-rotate', AutoRotate);

通过以上代码,我们定制了一个为挂载的元素提供旋转功能的组件。

首先可以看到的是IAutoRotateDataAutoRotateSchema,接口是为了通过泛型给定义的组件提供类型推断,而真正作用于运行时逻辑的,则是那个schema,其类型定义为:

interface IComponentSchema {
  [key: string]: {type: string, defaultValue?: any}
}

schema中,我们定义了组件每个数据的类型type和默认值defaultValue。这是由于所有组件在xml中对应的属性的值都是字符串,所以需要告诉微信小程序框架如何将这些字符串转换成最终的值,这一部分详见组件数据解析。

在数据定义后,我们继承了xrFrameSystem.Component派生了一个子类,它有一些生命周期,这就是组件具体逻辑实现的载体。我们在onAddonUpdate的时候去更新旋转速度数据,然后在每帧onTick的时候去修改挂载元素的transform组件中的旋转。注意还有onRemoveonRelease两个生命周期,但由于本组件没有额外资源需要处理,所以不需要执行任何操作。

完成了逻辑的实现后,我们最后一步调用了xrFrameSystem.registerComponent将这个组件注册到了微信小程序框架中,并给它了一个名字auto-rotate,接下来就可以正常使用了。

使用这个组件

使用组件的方式有两种:

在wxml中使用

首先是在wxml中使用,我们只需要按照注册的名字和数据类型定义将其写在元素标签上即可:

<xr-node auto-rotate="speed: 2 3 1;" />

如此这个节点便会自动旋转,速度为{x: 2, y: 3, z: 1}

组件还可以通过代理的方式来简化使用,详见元素中的数据代理部分。

手动使用

我们也可以在运行时来手动添加组件或设置它的数据,但注意一定不要和xml中冲突

要手动使用组件,我们需要先拿到元素的实例引用,详见场景中获取元素引用相关的描述,拿到了引用后便可以执行操作:

// 添加组件,并给一个可选的初始值
el.addComponent(AutoRotate, {speed: [2, 3, 1]});

// 获取组件并更新数据,注意`setData`包括`getData`都是通用方法,但有些组件有自己的实现,比如`transform.position`。
el.getComponent(AutoRotate).setData({speed: [1, 1, 1]});

// 手动移除组件
el.removeComponent(AutoRotate);

架构设计

xr-frame使用的架构属于ECS的一种,对应于xml中,Entity就是元素,Component就是属性,System就是场景的顶层组件。这样的设计在严谨的范式下,给开发者提供了灵活的扩展能力,理论上所有的内置功能都可以由开发者在外部自己实现。

ECS

开发者在xml中写的标签,都对应于框架里的元素Element的子类。元素本身不包含逻辑,它的逻辑全部都是被代理到其拥有的组件Component实现的。

而这个组件,在xml中对应的是元素上的属性,比如<xr-element transform="position: 1 1 1;" />,就是说在名为Element的元素上挂载了名为transform的组件。当然为了简化使用,我们还提供了一种属性代理的策略,详见Element的文档。

注意,虽然开发者会发现我们提供了手动给元素添加子节点的功能,但大部分情况下这是非法的!!!当然,出于某些场景的性能和灵活性考虑,我们提供了Shadow Element来满足开发者的需求。 在所有元素的子类中,有一个是最为特殊的,它就是场景Scene,对应于xml中的xr-scene标签。设计上来讲,每个xr-frame微信小程序组件顶层都必须是一个xr-scene元素,其他的元素都必须被包含在其中。这是因为场景负责整个框架的调度、各个系统的挂载以及所有资源的管理。

系统,就是挂载于场景下的各个顶层组件,它们负责每个模块的管理,按照顺序驱动各个模块的运行。目前而言,所有的内置功能由以下几个系统和对应的组件们构成。

逻辑系统

逻辑系统负责驱动所有元素以及组件的生命周期,保证它们按顺序进行,开发者一般只需要关心组件的生命周期即可,详见Component。

节点系统

节点系统负责管理场景中所有3D节点的可见性,详见Node。

渲染系统

渲染系统负责整个场景的渲染,也是最复杂的一个系统,它将Geometry、Effect、Material、Mesh、Texture、Light和Camera等组织起来,送入管线进行渲染。

渲染管线从设计上是可定制的,未来将会逐步开放给开发者。

资源系统

资源系统管理整个场景的资源(注意资源是每个场景专属的),一般通过AssetsAssetsLoad组件以及对应的元素在xml中使用,也提供手动管理的接口。同时还支持开发者自定义Loader来增加新的资源类型。

动画系统

动画系统管理所有的动画,其主要实现是借由每个元素的Animator组件来管理下面的所有AnimationAnimation本身被设计成可扩展的,框架内置了两种Animation来方便开发者简单使用:帧动画Keyframe和gltf动画。

物理系统

物理系统目前仅管理场景中的轮廓,以及触发轮廓点击事件。未来计划加入真实物理效果模拟,包括碰撞体和刚体等。

AR系统

AR系统将xr-frame微信小程序AI系统关联起来,让AR变得十分简单易用。主系统和ARTracker组件协作,让开发者用几行xml就能实现平面跟踪、2D/3DMarker识别等功能。

限制和展望

限制:

  1. 最低要求客户端iOS8.0.29、安卓8.0.30及以上,推荐稳定版在iOS8.0.36、安卓8.0.35及以上。
  2. 基础库最低2.27.1及以上,推荐2.32.0及以上。
  3. 开发工具需要最新版本,建议Nightly版本
  4. 微信小程序全局同一时刻只能存在一个xr-frame组件,否则可能会发生异常。
  5. 同一个xr-frame组件只能存在一个xr-scene,并且必须为顶层。
  6. 目前不支持和微信小程序传统标签比如<view>混写。
  7. 目前不支持wxml自动补全,真机调试需要特别注意,见真机调试文档。

同时未来还会追加更多的能力,在未来的规划中,我们还会着重致力于:

  1. XR-FRAME内置特色的UI组件,让开发者可以在XR-FRAME组件中写UI,来实现一套酷炫的UI系统。
  2. AR/VR能力持续增强,支持眼睛设备。
  3. 交互手段进一步强化,物理碰撞、触发等功能(已完成,待发布)。
  4. 工具能力强化,包括标签属性自动补全等。

想要进一步了解整个框架的架构,请看架构一节。

示例

上面的例子只是一部分简略代码,我们提供了丰富的示例并开源在xr-frame-demo,可以直接扫码体验:

demo

以下是部分示例需要用到的扫描资源:

典型案例 -> 扫描图片视频

典型案例 -> 扫描透视模型

典型案例 -> 扫描微信球

AR案例 -> 2DMarker

AR案例 -> OSD Marker

特性

比起目前的Canvas组件,xr-frame有以下显著的优势:

🍰 高度整合,上手简单

提供xml的方式来描述3D场景,并集成了AR、物理、动画、粒子、后处理等等系统,上手简单,符合微信小程序开发规范。

🌈 渲染效果好

内置完整的PBR效果、环境光照、阴影,原生支持glTF资源,提供的xr-frame-toolkit可以快速通过全景图生成环境数据。

⚡️ 高性能

混合方案,渲染性能逼近原生,xr-frame-toolkit可以对外部glTF文件进行优化,来提高加载性能,还有完善的缓存机制保证二次进入的加载性能。

🧱 易扩展

扩展性强,从资源到组件元素,皆可以由高阶用户自己定制,未来还可以暴露内部的渲染管线定制能力。

概述

本文适合有初步了解的开发者阅读,入门请参见指南

⚠️ xr-frame在基础库v2.32.0开始基本稳定,发布为正式版,但仍有一些功能还在开发,请见限制和展望。

xr-frame是一套微信小程序官方提供的XR/3D应用解决方案,基于混合方案实现,性能逼近原生、效果好、易用、强扩展、渐进式、遵循微信小程序开发标准:

<xr-scene>
  <xr-assets>
    <!-- 加载一个GLTF模型 -->
    <xr-asset-load type="gltf" asset-id="gltf-model" src="..." />
  </xr-assets>
  <xr-env env-data="..." />

  <xr-node>
    <!-- 将一个GLTF模型渲染在AR场景中 -->
    <xr-ar-tracker ...>
      <xr-gltf model="gltf-model" ...></xr-gltf>
    </xr-ar-tracker>
    <xr-camera is-ar-camera ...></xr-camera>
  </xr-node>

  <xr-node>
    <!-- 场景光照 -->
    <xr-light type="ambient" ... />
    <xr-light type="directional" ... />
  </xr-node>
</xr-scene>

特性

比起目前的Canvas组件,xr-frame有以下显著的优势:

🍰 高度整合,上手简单

提供xml的方式来描述3D场景,并集成了AR、物理、动画、粒子、后处理等等系统,上手简单,符合微信小程序开发规范。

🌈 渲染效果好

内置完整的PBR效果、环境光照、阴影,原生支持glTF资源,提供的xr-frame-toolkit可以快速通过全景图生成环境数据。

⚡️ 高性能

混合方案,渲染性能逼近原生,xr-frame-toolkit可以对外部glTF文件进行优化,来提高加载性能,还有完善的缓存机制保证二次进入的加载性能。

🧱 易扩展

扩展性强,从资源到组件元素,皆可以由高阶用户自己定制,未来还可以暴露内部的渲染管线定制能力。

示例

上面的例子只是一部分简略代码,我们提供了丰富的示例并开源在xr-frame-demo,可以直接扫码体验:

demo

以下是部分示例需要用到的扫描资源:

典型案例 -> 扫描图片视频

典型案例 -> 扫描透视模型

典型案例 -> 扫描微信球

AR案例 -> 2DMarker

AR案例 -> OSD Marker

限制和展望

限制:

  1. 最低要求客户端iOS8.0.29、安卓8.0.30及以上,推荐稳定版在iOS8.0.36、安卓8.0.35及以上。
  2. 基础库最低2.27.1及以上,推荐2.32.0及以上。
  3. 开发工具需要最新版本,建议Nightly版本
  4. 微信小程序全局同一时刻只能存在一个xr-frame组件,否则可能会发生异常。
  5. 同一个xr-frame组件只能存在一个xr-scene,并且必须为顶层。
  6. 目前不支持和微信小程序传统标签比如<view>混写。
  7. 目前不支持wxml自动补全,真机调试需要特别注意,见真机调试文档。

同时未来还会追加更多的能力,在未来的规划中,我们还会着重致力于:

  1. XR-FRAME内置特色的UI组件,让开发者可以在XR-FRAME组件中写UI,来实现一套酷炫的UI系统。
  2. AR/VR能力持续增强,支持眼睛设备。
  3. 交互手段进一步强化,物理碰撞、触发等功能(已完成,待发布)。
  4. 工具能力强化,包括标签属性自动补全等。

想要进一步了解整个框架的架构,请看架构一节。