本库用于快速开始一个白板应用,基于 white-web-sdk、@netless/window-manager 和 netless-app 实现。
从0.3.22版本开始, fastboard 集成了@netless/appliance-plugin插件,以便于提供更优性能及更丰富的教具功能
从0.3.22版本开始, fastboard 新增全打包文件, @netless/fastboard/full
or @netless/fastboard-react/full
, 用于解决内外依赖冲突问题.
npm add @netless/fastboard @netless/window-manager white-web-sdk @netless/appliance-plugin
注意:
@netless/appliance-plugin
是开启 性能优化版本 才需要安装。
npm add @netless/fastboard @netless/appliance-plugin
注意: 全打包方式引用,则
@netless/window-manager
、white-web-sdk
可以不用安装。而 @netless/appliance-plugin 是开启性能优化版本 才需要安装。
@netless/window-manager
、white-web-sdk
、@netless/appliance-plugin
是 peerDependency,如果你不清楚 peerDependency 是什么意思,可以阅读 《为什么使用 peerDependency ?》。
// 全打包方式引用
import { createFastboard, createUI } from "@netless/fastboard/full";
// 分包引用
import { createFastboard, createUI } from "@netless/fastboard";
async function main() {
const fastboard = await createFastboard({
// [1]
sdkConfig: {
appIdentifier: "whiteboard-appid",
region: "cn-hz", // "cn-hz" | "us-sv" | "sg" | "in-mum" | "eu"
},
// [2]
joinRoom: {
uid: "unique_id_for_each_client",
uuid: "room-uuid",
roomToken: "NETLESSROOM_...",
// (可选)
userPayload: {
nickName: "喵喵",
},
},
// [3] (可选)
managerConfig: {
cursor: true,
// (可选), 开启appliance-plugin, 从0.3.22开始
supportAppliancePlugin: true,
},
// [4] (可选)
netlessApps: [],
// [5] (可选), 开启appliance-plugin, 从0.3.22开始
enableAppliancePlugin: {
...
},
});
const container = createContainer();
const ui = createUI(fastboard, container);
// .....
// 关闭 Fastboard UI
ui.destroy();
// .....
// 退出 Fastboard (从白板房间断开)
fastboard.destroy();
}
function createContainer() {
const container = document.createElement("div");
// 白板元素必须具有可见的大小
Object.assign(container.style, {
height: "400px",
border: "1px solid",
background: "#f1f2f3",
});
document.body.appendChild(container);
return container;
}
main().catch(console.error);
[1] 关于 SDK 更多配置请看 构造 WhiteWebSDK
[2] 加入房间更多配置请看 构造 Room 与 Player 对象
[3] 配置 WindowManager 请看 WindowManager.mount()
关于窗口最小化后显示的小图标,可以通过 CSS 覆盖样式的方式修改它的位置:
.telebox-collector {
right: 20px;
bottom: 40px;
}
先安装 @netless/fastboard-react,再使用里面提供的 <Fastboard />
组件。
npm add @netless/fastboard-react @netless/window-manager white-web-sdk react react-dom @netless/appliance-plugin
注意:
@netless/appliance-plugin
是开启 性能优化版本 才需要安装。
npm add @netless/fastboard-react react react-dom @netless/appliance-plugin
注意: 全打包方式引用,则
@netless/window-manager
、white-web-sdk
可以不用安装。而 @netless/appliance-plugin 是开启性能优化版本 才需要安装。
@netless/window-manager
、white-web-sdk
、@netless/appliance-plugin
、react
、react-dom
是 peerDependency,如果你不清楚 peerDependency 是什么意思,可以阅读 《为什么使用 peerDependency ?》。
// 全打包方式引用
import { useFastboard, Fastboard } from "@netless/fastboard-react/full";
// 分包引用
import { useFastboard, Fastboard } from "@netless/fastboard-react";
import React from "react";
import { createRoot } from "react-dom/client";
function App() {
const fastboard = useFastboard(() => ({
sdkConfig: {
appIdentifier: "whiteboard-appid",
region: "cn-hz", // "cn-hz" | "us-sv" | "sg" | "in-mum" | "eu"
},
joinRoom: {
uid: "unique_id_for_each_client",
uuid: "room-uuid",
roomToken: "NETLESSROOM_...",
},
// 开启 appliance-plugin 插件
managerConfig: {
cursor: true,
supportAppliancePlugin: true,
}
// 开启 appliance-plugin 插件
enableAppliancePlugin: {
...
},
}));
// 白板元素必须具有可见的大小
return (
<div
style={{
height: "400px",
border: "1px solid",
background: "#f1f2f3",
}}
>
<Fastboard app={fastboard} />
</div>
);
}
createRoot(document.getElementById("app")).render(<App />);
await fastboard.insertImage(fileUrl);
其中 fileUrl
是该图片文件的 CDN 地址。本库并不包含任何上传、保存文件的逻辑和功能。
fastboard.undo();
fastboard.redo();
fastboard.moveCamera({ centerX: 0, centerY: 0, scale: 1 });
fastboard.moveCameraToContain({ originX: -300, originY: -200, width: 600, height: 400 });
fastboard.setAppliance("pencil");
fastboard.setAppliance("shape", "triangle");
fastboard.setStrokeWidth(2);
fastboard.setStrokeColor([r, g, b]);
如果你需要自己开发插件,请参考文档:如何开发自定义窗口插件 Netless App
除了 Fastboard 内置的一些官方 app,你也可以定义并使用自己编写的 app。在进入房间(createFastboard
)前需要按以下操作先在各客户端注册 app:
import { register } from "@netless/fastboard";
import MyApp from "my-app";
register({ kind: MyApp.kind, src: MyApp });
或者你也可以在 createFastboard
时设置 netlessApps
:
createFastboard({
..., // 其他配置
netlessApps: [MyApp],
});
接着在房间内调用以下接口插入 app:
fastboard.manager.addApp({ kind: MyApp.kind });
// 插入 PDF/PPT/PPTX 至主白板
const appId = await fastboard.insertDocs("文件名.pptx", conversionResponse);
其中 conversionResponse
是 转码 结果。
注意: 如果你使用的是 projector 转码服务,也可以使用以下方式插入:
const appId1 = await fastboard.insertDocs({ fileType: "pdf", scenePath: `/pdf/${response.uuid}`, scenes: [ { name: "1", ppt: { width: 714, height: 1010, src: images[1].url } }, { name: "2", ppt: { width: 714, height: 1010, src: images[2].url } }, ], title: "filename.pdf", }); const appId2 = await fastboard.insertDocs({ fileType: "pptx", scenePath: `/pptx/${response.uuid}`, taskId: response.uuid, title: "filename.pptx", // 默认为 "https://convertcdn.netless.link/dynamicConvert" url: response.prefix, });
注意: 该功能需要你把以下依赖升级到对应版本以上
@netless/app-slide
≥ 0.2.50@netless/window-manager
≥ 0.4.66
// PDF / 静态转码的文档
const dispose = fastboard.manager.onAppEvent("DocsViewer", event => {
if (event.type === "pageStateChange") console.log(event.value);
});
// PPTX / 动态转码的文档
const dispose = fastboard.manager.onAppEvent("Slide", console.log);
onExitRoom(() => dispose());
上面的 event
对象结构如下:
{
"kind": "Slide",
"appId": "Slide-aa1840ba",
"type": "pageStateChange",
"value": {
"index": 0,
"length": 12
}
}
该方法返回一个取消监听的函数。
import { dispatchDocsEvent } from "@netless/fastboard";
dispatchDocsEvent(fastboard, "nextPage"); // prevPage, nextStep, prevStep
dispatchDocsEvent(fastboard, "jumpToPage", { page: 2 });
默认情况下会发送事件给当前焦点所在的文档,如果需要指定文档,可以传入 appId
:
dispatchDocsEvent(fastboard, "nextPage", { appId });
import { register, SlideApp, addSlideHooks } from "@netless/fastboard";
register({
kind: SlideApp.kind,
src: SlideApp,
appOptions: {
// ... 自定义渲染参数
// 引入 TypeScript 类型 SlideOptions 以获得提示
},
addHooks: addSlideHooks,
});
const appId = await fastboard.insertMedia("文件名.mp3", fileUrl);
其中 fileUrl
是该媒体文件的 CDN 地址。本库并不包含任何上传、保存文件的逻辑和功能。
const appId = await fastboard.manager.addApp({
kind: "Monaco",
options: { title: "Code Editor" },
});
const appId = await fastboard.manager.addApp({
kind: "Countdown",
options: { title: "Countdown" },
});
Note
GeoGebra 使用 GPLv3 协议并且仅允许非商业的免费使用。如果要商用,请先阅读 GeoGebra 的许可协议:https://www.geogebra.org/license
const appId = await fastboard.manager.addApp({
kind: "GeoGebra",
options: { title: "GeoGebra" },
});
const appId = await fastboard.manager.addApp({
kind: "Plyr",
options: { title: "YouTube" },
attributes: {
src: "https://www.youtube.com/embed/bTqVqk7FSmY",
provider: "youtube",
},
});
const appId = await fastboard.manager.addApp({
kind: "EmbeddedPage",
options: { title: "Google Docs" },
attributes: {
src: "https://docs.google.com/document/d/1bd4SRb5BmTUjPGrFxU2V7KI2g_mQ-HQUBxKTxsEn5e4/edit?usp=sharing",
},
});
注意: EmbeddedPage 使用
<iframe>
来显示外部资源,由于浏览器限制,您最好不要嵌套使用 iframe(即 iframe 内还有 iframe,通常来说浏览器会对第二层 iframe 增加十分多的限制或者根本无法使用)。
更多 app 请看 netless-app。
也可以参考文档:如何开发自定义窗口插件 Netless App。
通过 enableAppliancePlugin
及 managerConfig.supportAppliancePlugin
配置项开启 appliance-plugin 插件,以提升性能以及提供新的白板功能, 也可以参考文档:appliance-plugin了解更多内容。
注意: 开启使用性能优化版本,需要安装
@netless/appliance-plugin
。
// 直接通过raw-loader引入
import fullWorkerString from '@netless/appliance-plugin/dist/fullWorker.js?raw';
import subWorkerString from '@netless/appliance-plugin/dist/subWorker.js?raw';
const fullWorkerBlob = new Blob([fullWorkerString], {type: 'text/javascript'});
const fullWorkerUrl = URL.createObjectURL(fullWorkerBlob);
const subWorkerBlob = new Blob([subWorkerString], {type: 'text/javascript'});
const subWorkerUrl = URL.createObjectURL(subWorkerBlob);
// CDN 引入, 需要部署到自己的CDN服务器上, 它必须遵守 同源策略 。
const subWorkerUrl = "https://cdn.jsdelivr.net/npm/@netless/appliance-plugin@latest/dist/subWorker.js";
const fullWorkerUrl = "https://cdn.jsdelivr.net/npm/@netless/appliance-plugin@latest/dist/fullWorker.js";
function App() {
const fastboard = useFastboard(() => ({
sdkConfig: {
...
},
joinRoom: {
...
},
// 开启 appliance-plugin 插件, 和windowManager 配置
managerConfig: {
supportAppliancePlugin: true
},
// 开启 appliance-plugin 插件
enableAppliancePlugin: {
cdn: {
fullWorkerUrl,
subWorkerUrl,
}
}
}));
....
}
- 首先必需保证在安卓\ios\web,三端都开启 appliance-plugin 配置. appliance-plugin 开启后绘制的笔记在未开启的白板上不会显示.
- 在开启 appliance-plugin 插件后, 之前白板上旧绘制的内容会显示,但是无法操作和升级成新的笔记. 所以为了不影响体验,请在一个无任何历史数据的白板上使用。同理插件关闭后, 新绘制的内容会丢失。
- 只有浏览器对 web API offscreenCanvas 的完全支持,才能体验到更加的性能及丰富的教具功能体验。
Fastboard 为了上手快,不支持高度定制化。不过有一些轻量化配置:
// vanilla js
const ui = createUI(fastboard, container);
ui.update({ config: { ...ui_config } });
// react
<Fastboard app={fastboard} config={{ ...ui_config }} />;
上面的 ui_config
长这样:
{
toolbar: {
enable: true,
placement: 'left',
items: ['pencil', 'eraser'],
apps: { enable: true },
},
redo_undo: { enable: true },
zoom_control: { enable: true },
page_control: { enable: true },
}
例如,你可以像这样隐藏缩放栏:
// vanilla js
ui.update({ config: { zoom_control: { enable: false } } });
// react
<Fastboard app={fastboard} config={{ zoom_control: { enable: false } }} />;
或者改变工具栏上的按钮:
可用配置项:
clicker
、selector
、pencil
、text
、shapes
、eraser
、clear
、hand
、laserPointer
.
const toolbar_items = ["pencil", "eraser"];
// vanilla js
ui.update({ config: { toolbar: { items: toolbar_items } } });
// react
<Fastboard app={fastboard} config={{ toolbar: { items: toolbar_items } }} />;
也可以自己实现相关组件,请参考:如何为 fastboard 实现 UI以及如何自定义fastboard UI
白板本身有基于指令的录制功能,Fastboard 也为此实现了类似的组件和接口:
const player = await replayFastboard(...)
const ui = createReplayUI(player, container);
const player = useReplayFastboard(() => ({...}))
return <ReplayFastboard player={player} />
上面的 player
实例和原生的视频播放器类似,也有 play()
seek()
pause()
等方法。
如果要让白板和其他视频播放器同步进度条,请参考库 @netless/sync-player 。
你会因为以下情况收到异常回调:
- 因为断网等无法连上白板服务器
- 因为网络不稳自动进入重连状态
- 房间被封禁
注意,白板内部自带重试逻辑,用户无需手写,如果白板真的断开连接,你必须直接退出房间。
请参考以下代码适当地处理这些异常情况:
try {
fastboard = await createFastboard({
sdkConfig: {
onWhiteSetupFailed(error) {
console.error("Failed to find the whiteboard server", error);
},
},
joinRoom: {
callbacks: {
onPhaseChanged(phase) {
if (phase === "reconnecting") console.log("Whiteboard connection lost, reconnecting...");
},
onDisconnectWithError(error) {
console.error("Failed to connect to whiteboard server", error);
},
onKickedWithReason(reason) {
console.log("You're kicked by", reason);
// 正常退出房间
leaveRoom();
},
},
},
});
} catch (error) {
console.error("Failed to join whiteboard room", error);
}
React 组件的写法应该类似,这里不赘述。
MIT @ netless