Netless 互动白板支持自定义插件。你可以在网上找到开源的自定义插件代码,将其集成到自己的应用中。你也可以根据自己的业务,自行开发自定义插件。
Netless 开源了一些自定义插件的代码,插件的开源代码可在如下地址找到。
如上两个插件可以在白板中展示一个播放器,以播放视频、音频文件。
你可以阅读开源项目的 README.md
文档来了如何将它们集成到自己的项目中。
自定义插件运行在 Web 浏览器上,需要基于 JavaScript 开发。此外,你还需要对 React 和 MobX 有一定了解。Netless 自持两种插件:组件插件、不可见插件。前者需要用代码描述它的外观,可以作为组件插入到白板中。后者仅仅利用白板同步、广播状态,没有自己的外观视图。
自行开发组件插件,需要定一个结构符合 Plugin
定义的 object
。
import React from "react";
type Plugin<C = {}, T = any> = {
// 组件类型,该组件的唯一识别符。应该取一个独特的名字,以和其他组件区分。
readonly kind?: string;
// 组件的外观定义
readonly render: React.ComponentType<PluginProps<C, T>>;
// 初始化 attributes 的默认值
readonly defaultAttributes?: T;
// 碰撞检测,以定义哪些位置可以被选择工具选中
readonly hitTest?: (plugin: PluginInstance<C, T>, x: number, y: number, selectorRadius: number) => boolean;
// 是否拦截组件上的事件
readonly willInterruptEvent?: (plugin: PluginInstance<C, T>, event: NativeEvent) => boolean;
};
其中 render
是一个 React 组件,你可以写成形如如下形式(或 React 中的其他等价形式)。
import React from "react";
import {observable} from "mobx";
import {CNode} from "white-react-sdk";
@observable
class MyPlugin extends React.Component {
render() {
var cnode = this.props.cnode;
var plugin = this.props.cnode;
return (
<CNode context={cnode}>
<div>hello world</div>
</CNode>
);
}
}
从上面的代码可以看出,我们从 this.props
中取到了两个重要的成员 cnode
、plugin
。
其中 cnode
包含组件渲染的上下文信息,该成员一定要和 CNode
搭配使用。你的 render
方法返回的 ReactNode
节点的最外层必须是 <CNode context={cnode}>
,否则,组件插件的视图将无法正常渲染。<CNode>
等效于 <div>
,你可以为期添加 <div>
才有的属性,如 className
、style
等。
plugin
是操作组件插件的实例,基于它你可以进行很多特定的操作。它的类型是 PluginInstance
,定义如下。
interface PluginInstance<C, A> {
// 组件插件的类型唯一识别符
readonly kind: string;
// 该组件实例的唯一识别服(房间内唯一)
readonly identifier: Identifier;
// 自定义上下文
readonly context?: C;
// 实例的左上角在世界坐标系中的 x 轴坐标
readonly originX: number;
// 实例的左上角在世界坐标系中的 y 轴坐标
readonly originY: number;
// 实例在世界坐标系中的宽
readonly width: number;
// 实例在世界坐标系中的高
readonly height: number;
// 实例是否能被选中
readonly selectable: boolean;
// 实例是否正在被拖拽
readonly isDraging: boolean;
// 实例是否正被拖着改变大小
readonly isResizing: boolean;
// 实例是否正在播放(若是实时房间,则恒为 true)
readonly isPlaying: boolean;
// 实例当前播放到的时间戳(若为实时房间,则恒为 0)
readonly playerTimestamp: number;
// 实例当前的播放速率倍率(若为实时房间,则恒为 1.0)
readonly playbackSpeed: number;
// 实例的属性,结构自定义
attributes: A;
// 修改属性中的部分字段
putAttributes(attributes: Partial<A>): void;
// 删除属性中的部分字段
removeAttributeKeys<K extends keyof A>(...keys: K[]): void;
// 修改实例的描述
update(description: PluginDescription<A>): void;
// 将该实例从白板中删除
remove(): void;
}
其中 plugin.attributes
是最重要的属性,一般而言,它是一个包含自定义字段的 object
。当你通过调用 plugin.putAttributes
来修改,整个房间都能得到通知,如果别人通过调用 plugin.putAttributes
来修改,你也能得到通知。换而言之,plugin.attributes
在房间内是同步的。你可以通过在 render
中直接读取它的字段来渲染需要的图形,MobX 会在 plugin.attributes
发生变化时自动调整你的视图。
此外,isDraging
、isResizing
、isPlaying
、playerTimestamp
、playbackSpeed
也会被 MobX 监听。你在 render
中直接读取,MobX 会在它们改变时自动调整视图。
之前提到,你需要自行构造一个符合 Plugin
类型定义的对象。例如,如下代码代码可做到构造一个 plugin
对象。
var plugin = Object.freeze({
kind: "my-plugin",
render: MyPlugin,
});
你可以通过如下方法将 plugin
注册到白板中。
import {createPlugins} from "white-web-sdk";
var whiteWebSdk = new WhiteWebSdk({
appIdentifier: "$APP_IDENTIFIER",
plugins: createPlugins({"my-plugin": plugin}),
});
在加入实时房间后,你可以通过如下方式,在白板中插入一个组件插件实例。注意,可能被插入的插件实例必须提前准备好。即房间内所有人都必须注册这些可能用到的插件。
insertPlugin(kind: string, description?: PluginDescription): Identifier;
其中 kind
是组件的唯一识别符。description
用于描述组件在白板中的情况,类型为 PluginDescription
,定义如下。
type PluginDescription<A = any> = {
// 组件的左上角在世界坐标系中的 x 轴坐标
readonly originX?: number;
// 组件的左上角在世界坐标系中的 y 轴坐标
readonly originY?: number;
// 组件在世界坐标系中的宽
readonly width?: number;
// 组件在世界坐标系中的高
readonly height?: number;
// 组件能否被选择工具选中
readonly selectable?: boolean;
// 初始化 attributes
readonly attributes?: A;
};
该方法会返回一个 identifier
,作为创建的组件实例的句柄。这是一个在房间内唯一的组件实例识别符。有了这个句柄,我们可以通过如下代码来删除房间中的组件实例。
removePlugin(identifier: Identifier): boolean;
此外,我们还可以通过如下代码来修改组件实例的描述。
updatePlugin(identifier: Identifier, description: PluginDescription): boolean;
通过如下代码,可以获取某个组件实例的 attributes
属性。
getPluginAttributes(identifier: Identifier): any | undefined;
通过如下代码,可以获取某个组件实例在白板中的位置信息。
getPluginRectangle(identifier: string): Rectangle | undefined;
export declare type Rectangle = {
readonly originX: number;
readonly originY: number;
readonly width: number;
readonly height: number;
};
不可见插件全局唯一,只能建立一个实例。它没有视图,只有一个会在全房间自动同步的 attributes
状态。为了定义一个不可见插件,你需要定义一个 class
,其结构如下。
import {InvisiblePlugin, InvisiblePluginContext} from "white-web-sdk";
type MyAttributes = {
// 自定义 attributes 的结构
};
class MyInvisiblePlugin extends InvisiblePlugin<MyAttributes> {
// 组件类型,该组件的唯一识别符。应该取一个独特的名字,以和其他组件区分。
static readonly kind: string = "my-invisible-plugin";
new (context: InvisiblePluginContext) {
// 初始化代码
}
static onCreate(plugin: MyInvisiblePlugin) {
// 在房间中构造出 plugin 实例的回调
}
static onDestroy(plugin: MyInvisiblePlugin) {
// 在房间中构造出 plugin 实例的回调
}
}
该 class
在构造时,会得到类型为 InvisiblePluginContext
的上下文对象,其定义如下。
type InvisiblePluginContext = {
// 组件类型
readonly kind: string;
// displayer 对象,实时房间中为 room 对象,回放时为 player 对象
readonly displayer: Displayer;
};
该对象的成员 displayer
可能是 room
对象,也可能是 player
对象。具体到底是哪个,可以使用如下代码判断。
import {isRoom, isPlayer} from "white-web-sdk";
if (isRoom(displayer)) {
// 是 room 对象
} else if (isPlayer(displayer)) {
// 是 player 对象
}
由于 class
继承 InvisiblePlugin
,因此我们同时也继承了它的方法和成员变量,具体 InvisiblePlugin
的定义如下。
abstract class InvisiblePlugin<A extends Object> {
// displayer 对象
readonly displayer: Displayer;
// 属性
readonly attributes: A;
// 回调监听节点
readonly callbacks: Callbacks<InvisibleCallbacks<A>>;
// 构造函数
constructor(context: InvisiblePluginContext);
// 修改属性的特定字段
setAttributes(attributes: Partial<A>): void;
// 从房间删除实例
destroy(): void;
// 覆写它,可以监听到 attributes 的变化
onAttributesUpdate?(attributes: A): void;
// 覆写它,可以监听到实例从房间删除的回调
onDestroy?(): void;
}
你还可以通过 this.callbacks
对象注册回调函数以监听事件。其类型 InvisibleCallbacks
定义如下。
type InvisibleCallbacks<A extends Object> = {
// 监听 attributes 的变化
readonly onAttributesUpdate: (attributes: A) => void;
// 监听到实例从房间删除的回调
readonly onDestroy: () => void;
};
this.callbacks
的使用方法和 room.callbacks
与 player.callbacks
类似。可参考《回调方法|构造 Room 与 Player 对象》。
完成定义后,你需要在构造 WhiteWebSdk
实例时,将你定义的 class
传进去。
var whiteWebSdk = new WhiteWebSdk({
appIdentifier: "$APP_IDENTIFIER",
invisiblePlugins: [MyInvisiblePlugin],
});
之后,在加入实时房间后,可以调用 room
的如下方法来创建不可见组件实例。
createInvisiblePlugin<K extends string, A extends Object, P extends InvisiblePlugin<A>>(
pluginClass: InvisiblePluginClass<K, A, P>,
attributes: A): Promise<InvisiblePlugin<A>>;
其中第一参数 pluginClass
是我们自己实现的 class
对象,第二参数 attributes
是初始化属性。执行完该方法后,房间内会创建一个pluginClass
的一个实例,并返回。这个操作会影响整个房间,也就是说,房间内只需一个人调用该方法,就可以为整个房间创建一个人人皆可共用的不可见组件实例。
在加入实时房间或正在回放录像时间,我们可以调用 room
或 player
的如下方法来获取已创建的不见组件实例。
getInvisiblePlugin<A extends Object>(kind: string): InvisiblePlugin<A> | null;