快速开始
本页从一个空的 Vite + TypeScript 项目开始,完成 Canvas 宿主创建、页面与主窗口实例化、应用挂载、首帧运行和资源释放。完成后,你会得到一个可以点击的最小 DirectSurface UI 应用。
启动链路
DirectSurface UI 不是把组件挂到普通 DOM 节点,而是把一棵 Render Tree 挂到已有的 HTMLCanvasElement:
<canvas id="app">
→ Application.mount('#app')
→ AppHost / RuntimeHost
→ MainWindow
→ RenderPage
→ Layout 与 Widgets
→ Canvas 2D 像素输出
业务应用通常只直接使用 Application 和返回的 AppHost。不要为了启动普通页面自行实例化底层 RuntimeHost。
1. 创建项目并安装
先把团队提供的实际版本 .tgz 文件复制到业务项目,例如 packages/ds-ui-0.1.0.tgz。文件名中的版本号以收到的制品为准;DirectSurface UI 不要求发布到公共 npm registry。
npm create vite@latest ds-ui-hello -- --template vanilla-ts
cd ds-ui-hello
npm install
npm install ./packages/ds-ui-0.1.0.tgz
如需让安装来源随项目一起固定,可以在 package.json 中写入:
{
"dependencies": {
"ds-ui": "file:./packages/ds-ui-0.1.0.tgz"
}
}
提交 package.json 后执行 npm install。无论 .tgz 放在本地目录还是由内部制品系统下载,业务代码都从 ds-ui 包入口导入,不应依赖包内路径。
2. 创建 Canvas 宿主
将项目根目录的 index.html 改为:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DirectSurface UI Hello</title>
</head>
<body>
<canvas id="app" aria-label="DirectSurface UI 应用"></canvas>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Application.mount() 只接收已经存在的 HTMLCanvasElement。它不会替业务创建 Canvas,也不能挂到普通 <div>。
3. 让 Canvas 填满宿主
创建 src/style.css:
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
}
body {
overflow: hidden;
}
#app {
display: block;
}
CSS 尺寸决定 Canvas 在页面中的显示区域;runtime 会根据逻辑视口和 DPR 同步实际像素尺寸。
4. 创建页面、主窗口并挂载应用
创建 src/main.ts:
import './style.css'import { Application, MedicalCompactLightTheme, RenderButton, RenderPage, RenderStackPanel, RenderText, RenderWindow, loadDirectSurfaceFonts, type AppHost,} from 'ds-ui' class HelloPage extends RenderPage { constructor() { const status = new RenderText('应用已经挂载', { role: 'secondary' }) const content = new RenderStackPanel({ orientation: 'vertical', spacing: 12, padding: 24, crossAxisAlignment: 'stretch', }) content.addChild(new RenderText('Hello DirectSurface UI', { role: 'title' })) content.addChild(status) content.addChild(new RenderButton({ label: '执行', onClick: () => { status.text = `最近点击:${new Date().toLocaleTimeString()}` }, })) super({ child: content }) }} class MainWindow extends RenderWindow { constructor() { super({ title: 'DirectSurface UI Hello', chrome: 'none', contentPadding: 0, contentSpacing: 0, }) this.setChildren([new HelloPage()]) }} let host: AppHost | undefined async function bootstrap(): Promise<void> { await loadDirectSurfaceFonts() const mainWindow = new MainWindow() host = Application.mount('#app').run(mainWindow, { theme: MedicalCompactLightTheme, })} function dispose(): void { host?.dispose() host = undefined} window.addEventListener('beforeunload', dispose, { once: true })const hot = (import.meta as ImportMeta & { hot?: { dispose(callback: () => void): void }}).hothot?.dispose(dispose) void bootstrap()
上面的源码是独立业务项目的完整入口;文档末尾的运行区会复用同一份页面工厂,并由文档站的共享 Application 承载,因此不会在同一个 Canvas 上重复挂载 runtime。
这里完成了最小应用的全部关键步骤:
new HelloPage()创建业务页面及其 Render Tree。new MainWindow()创建唯一的主窗口,并通过setChildren()接入页面。Application.mount('#app')找到宿主 Canvas。.run(mainWindow, options)创建 runtime、挂载主窗口、初始化主题并调度首帧。.run()返回的AppHost保存整个应用实例;退出、热更新或宿主卸载时调用host.dispose()。
Application.mount() 是推荐入口。不要写成 new Application().run(...),因为未经过 mount() 的 Application 没有 Canvas,调用 run() 会直接报错。
5. 运行
npm run dev
打开 Vite 输出的本地地址。正常情况下会看到浅色主界面、“Hello DirectSurface UI”和一个可点击按钮。
挂载参数和实例职责
const diagnosticsWindow = new RenderWindow({ title: '诊断示例', chrome: 'none',})const isDevelopment = location.hostname === 'localhost'const host = Application.mount('#app').run(diagnosticsWindow, { theme: MedicalCompactLightTheme, enableLayoutInspector: isDevelopment, enablePerformanceOverlay: isDevelopment, context: { currentUser: { id: 'u-1', name: 'Demo User' }, },})
| 对象 | 谁创建 | 主要职责 |
|---|---|---|
HTMLCanvasElement |
Vue、React、原生 HTML 或其他宿主 | 提供绘制目标并决定 DOM 生命周期。 |
HelloPage |
业务模块 | 组合布局、组件和业务交互。 |
MainWindow |
业务应用壳 | 承载应用根页面、工作区或导航。 |
Application |
框架静态入口 | 查找 Canvas 并启动应用。 |
AppHost |
Application.run() 返回 |
持有 runtime、应用上下文和浮层服务,并负责统一释放。 |
RuntimeHost |
框架内部创建 | 管理尺寸、事件、layout、paint、合成和帧调度。 |
如果 Canvas 位于 Vue、React 或微前端页面内,应在宿主的 mounted/effect 生命周期中执行 Application.mount(...).run(...),并在 unmounted/effect cleanup 中调用同一个 host.dispose()。Canvas DOM 仍由宿主框架负责创建和移除。
常见启动错误
Canvas element "#app" was not found
- 检查
index.html中是否存在<canvas id="app">。 - 确认启动代码在 Canvas 插入 DOM 后执行。
- 确认选择器指向 Canvas,而不是同名的
<div>。
页面存在但没有内容
- 确认主窗口调用了
setChildren([page])。 - 确认 Canvas 和父容器有非零宽高。
- 确认没有在启动后立即调用
host.dispose()。
热更新后出现重复事件或重复应用
保存 .run() 返回的 AppHost,并在 Vite HMR dispose、React effect cleanup 或 Vue onUnmounted 中调用 host.dispose()。
示例与公共 API 的边界
文档中的运行示例会包含演示数据和页面装配代码。它们用于说明公共 API 的组合方式,不会随 ds-ui 包一起导出;业务项目应按自己的服务、状态和模块边界重新组织这些代码。
最终运行效果
完成以上步骤后,应用应显示浅色主界面、“Hello DirectSurface UI”、当前挂载状态和一个可以点击的操作按钮。你可以先在下方直接交互,再使用“全屏运行”检查独立页面效果。
这个运行区与上面的业务入口使用同一个页面工厂,但由文档站已经存在的 Application 承载,避免在同一个 Canvas 上重复创建 runtime。点击“执行操作”后,状态文字应更新时间;这也同时验证了页面挂载、布局、绘制和事件链路。
下一步
- 创建第一个页面:继续学习页面、布局和页面注册。
- Runtime API:查看
ApplicationRunOptions、AppHost和运行时能力。 - 组件手册总览:按业务场景选择控件。
- 布局系统总览:掌握约束、滚动和常用布局组件。
- 应用上下文:注入登录态、服务和应用级依赖。