浏览器应用启动与客户端插件装配
浏览器入口创建 AppWebEntry,在注入完成后建立模块系统与 Cordis 上下文,预取首批插件,再交由 bootClient 装配客户端入口并调用 mountClient 挂载界面。入口 · 启动内核
Purpose and Scope
本文聚焦 apps/web 浏览器入口到 AppWebEntry 的启动顺序、桌面载体注入就绪门、模块装载配置、插件预取、失败展示及释放。插件内部如何实现 bootClient、具体渲染器如何实现 mountClient、服务端如何生成注入表,以及桌面宿主的完整协议均属于相邻主题;本文只讨论这些边界在启动链上的调用关系。由于本页没有读取这些函数的实现,不推断它们内部的插件排序或渲染细节。boot.ts
Overview
apps/web 是基于 @deepseek-ai/dsh-client-web shell 的 Vite 应用入口;包脚本提供 dev、build、watch 和独立的 preview 构建/服务命令。package.json · package.json。普通网页直接调用 entry.run();检测到 dshDesktopBoot 时,入口先异步获取注入数据和流地址,加载脚本后放行同一条启动流程。main.ts
Architecture
AppWebEntry 保留容器、可选模块传输替身、启动页,以及运行中产生的 Context、模块系统与 manifest;构造时立即创建 BootPage,但直到 run() 才加载插件和挂载 UI。boot.ts
启动路径与装配次序
浏览器入口和桌面载体
入口先检查 #root 是否存在,缺失时抛出 web app: missing #root;随后构造 AppWebEntry。桌面模式必须存在 __DSH_BOOT_READY__ deferred,否则抛出明确的就绪门错误。desktop.ready() 返回注入表与 streamBaseUrl;入口设置 __DSH_TRANSPORT__ = { ownsHost: true, streamBaseUrl },通过向 document.head 追加 script 元素逐个加载 applyIndexInjections 请求的脚本,成功后 gate.resolve(),失败时 gate.reject(error)。entry.run(...) 不等待这段注入逻辑完成,而由 run() 自己等待门的 promise,避免读取尚未注入的启动全局量。main.ts · boot.ts
const el = document.getElementById('root')
if (el === null) throw new Error('web app: missing #root')
const entry = new AppWebEntry(el)Source: main.ts
1await applyIndexInjections(injections, src => new Promise<void>((resolve, reject) => {
2 const script = document.createElement('script')
3 script.src = src
4 script.onload = () => { resolve() }
5 script.onerror = () => { reject(new Error(`desktop web: failed to load ${src}`)) }
6 document.head.append(script)
7}))
8gate.resolve()Source: main.ts
装载器、预取与渲染交接
run() 等待可选的就绪门;不存在门时不等待。随后检查 window.__ModuleLoader__,以 __DSH_BOOT__、getStaticModules()、可选传输 loadBundle 和构造函数传入的 seams 创建模块系统;展开顺序使显式 seams 覆盖预注入的传输钩子。读取 modules.manifest 后,先发起立即层预取,再创建 Cordis Context、设置启动页插件总数,等待预取,最后调用 bootClient。启动回调按名称写入状态;有宿主的失败处理器时,失败状态不会写到启动页。插件启动结束后安装窗口拖拽区域监测,再以同一 ctx 与容器调用 mountClient。boot.ts
1this.modules = moduleLoader.create({
2 boot: win.__DSH_BOOT__,
3 staticModules: getStaticModules(),
4 ...transport?.loadBundle === undefined ? {} : { loadBundle: transport.loadBundle },
5 ...this.seams,
6})
7this.manifest = this.modules.manifestSource: boot.ts
1const prefetching = this.prefetchImmediateTier()
2const ctx = new Context()
3this.ctx = ctx
4this.page.setTotal(this.manifest.plugins.length)
5await prefetching
6await bootClient({
7 ctx,
8 modules: this.modules,
9 manifest: this.manifest,
10 onEntryState: (name, state) => {
11 if (onFailure === undefined || state !== 'failed') this.page.setState(name, state)
12 },
13})Source: boot.ts
Core Flow
预取仅处理 manifest.plugins 中 immediately 为真的行,并对这些行并行调用 modules.prefetch(row.id);预取错误在该阶段被吞掉,源码注释说明正式 Loader import 将重试并报告 bundle 失败。这里不应将预取完成理解为插件已经激活。boot.ts
Usage Examples
下例是入口的实际启动调用:桌面模式传递失败报告函数,普通网页模式不传。函数内部捕获初始化的同步异常,桌面宿主存在时通过 failed() 异步报告;无宿主则重新抛出原始原因。main.ts · main.ts
1void entry.run(desktop === undefined ? undefined : reportFailure)
2} catch (reason) {
3 reportFailure(reason)
4}Source: main.ts
下面是 AppWebEntry 内部的高级用法:插件装配之后,先启用拖拽区域监测再移交渲染器;销毁时逆向停止监测、释放 Cordis fiber,并释放启动页。调用方若管理入口生命周期,可调用公开的 dispose()。boot.ts
this.stopDragRecall = installWindowDragRecall({ document: this.container.ownerDocument })
await mountClient(ctx, this.container)Source: boot.ts
1async dispose(): Promise<void> {
2 this.stopDragRecall?.()
3 this.stopDragRecall = undefined
4 const ctx = this.ctx
5 this.ctx = undefined
6 if (ctx !== undefined) await ctx.fiber.dispose()
7 this.page.dispose()
8}Source: boot.ts
配置与启动输入
这里的运行时输入来自全局量、构造参数和 manifest,而非该入口中的环境变量配置;以下列出源码中可核实的含义。main.ts · boot.ts
| 输入 | 类型/缺省 | 用途 |
|---|---|---|
#root | DOM 元素;必需 | AppWebEntry 的容器;不存在即抛错。 |
dshDesktopBoot | 可选,缺省 undefined | 启用桌面注入流程,提供 ready() 和 failed(message)。 |
__DSH_BOOT_READY__ | 可选全局 deferred;桌面模式必需 | run() 等待其 promise;桌面注入完成后 resolve。 |
window.__ModuleLoader__ | 必需的模块装载 facade | 创建模块系统;缺失时 run() 进入失败处理。 |
__DSH_BOOT__ | 注入的启动数据 | 透传给 moduleLoader.create;此处未定义其内部字段。 |
__DSH_TRANSPORT__.loadBundle | 可选 | 用于自带 bundle 字节的传输;显式 seams.loadBundle 可覆盖。 |
seams | 可选 BootSeams | 构造时提供的模块传输替身,源码注明供 jsdom 测试替换。 |
manifest.plugins[].immediately | 来自模块系统 manifest | 标记要提前并行预取的插件条目。 |
上述行分别对应入口全局与 DOM 检查、模块系统创建和预取筛选。
API Reference
| API | 参数 | 返回/语义 | 可观察的失败 |
|---|---|---|---|
new AppWebEntry(container: HTMLElement, seams?: BootSeams) | container:挂载 DOM;seams:可选 loadBundle 替身 | 构造启动页,尚不开始加载。 | 构造函数中未声明显式异常。 |
run(onFailure?: (reason: unknown) => void): Promise<void> | 可选的宿主失败报告回调 | 等待注入、装配插件并挂载;捕获内部错误后显示启动页错误或调用回调,因此通常通过 resolve 完成失败报告。 | catch 内调用 console.error;若所提供回调自身抛错,源码未再捕获。 |
dispose(): Promise<void> | 无 | 停止 drag recall,等待 ctx.fiber.dispose(),释放 BootPage。 | 未在此方法内设置异常捕获。 |
签名及执行路径见 boot.ts 与 boot.ts。BootSeams 只抽取 ClientModuleCreateOptions 的 loadBundle 字段,不应推断其它注入接口。boot.ts
故障、并发与运维注意
- 注入失败:桌面脚本
onerror拒绝该脚本的 Promise,注入链随后拒绝就绪门;run()的等待抛错并进入自身catch,由可选宿主回调或启动页承接。main.ts · boot.ts - 缺少依赖:缺
#root在入口同步失败;桌面模式缺就绪门也在入口失败;缺模块装载 facade 则在run()内失败。三种错误路径不同,排查时先区分失败发生在创建入口前还是run()内。main.ts · boot.ts - 预取并发与容错:
Promise.all并行预取标为immediately的插件,预取失败不阻断这一阶段;正式装载的失败由下游 loader 报告,不在这里提前判定。boot.ts - 生命周期:
run()的异常处理并未自动调用dispose();复用/清理入口时须注意dispose()才停止 drag recall 并释放 fiber。源码没有展示并发调用run()或与dispose()交错的协调机制,不应假定其可重入。boot.ts
Related Links
更深入的服务端注入生成、桌面文档生成和插件装配内部实现应参阅对应的独立主题;本页对未读取的实现不作行为承诺。