数据源与资源加载
Core 读取数据、解析素材路径并管理加载与清理。它保留原数据格式;Vega、Cassiopeia、Home Spot 分别负责自己的画面。本页先读取一个真实 JSON 和图片,剧情播放见 Vega 教程。浏览器使用已托管的 https://haneoka.org/embed/core.js,无需安装 npm。
1. 读取现成 JSON
Section titled “1. 读取现成 JSON”打开完整 promenade.json。它保存标题、背景路径和一行文字;garden.svg是对应的原创图片。下面的页内示例选择「HTTP JSON」并加载,即可看见数据。随后点「解析资源 URL」取得图片地址,或「读取资源字节」检查实际内容。这里读到的 lines 数据不是 Vega 可播放文档,转换见第 3 步。
修改 JSON 参数,然后点击加载。模块和请求只在点击后启动;输出包含真实响应与传输记录。
正在准备示例…
查看实际示例代码 import { createEmbedLoader, httpDataSource } from "https://haneoka.org/embed/core.js";const loader = createEmbedLoader({ source: httpDataSource({ url: "https://docs.haneoka.org/examples/promenade.json" }), locale: "fr",});const data = await loader.load();const imageUrl = await loader.resourceUrl(data.background);// 消费者使用完这个地址后,再 await loader.dispose()。HTTP 相对素材以配置的 JSON 所在目录为基准,重定向不改变这个基准;显式 assetsBase 可覆盖它。resourceUrl 取得地址,resourceBytes 读取实际字节。source/locale/resourceKey 等示例面板字段会转换成实际 factory,其中 resourceKey/resourceField 是此演示选择素材的字段,完整 SDK 参数见后面的表。
2. 真正选择本地 JSON 和图片
Section titled “2. 真正选择本地 JSON 和图片”分别保存 promenade.json 与 garden.svg。打开完整文件读取网页,先选择 JSON,再选择图片,点击「加载所选文件」。你会看见标题 Une promenade、原创庭园图片及实际图片字节数。关闭读取器会先移除图片,再销毁 loader 和它的 Blob URL;可以重新选择文件再加载。
将下面代码保存为 UTF-8 的 index.html 即可保留这个完整项目;本地 HTTP 打开方法见快速开始。目录为 index.html、promenade.json、garden.svg 三个文件。JSON 中 background 是素材键,文件映射以这个键指向你实际选择的 File,也支持合法 Unicode 路径。
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>读取本地 JSON 与图片</title><style>body{max-width:60rem;margin:auto;padding:1rem;font:1rem/1.5 sans-serif;color:CanvasText;background:Canvas}button,input{font:inherit}button,label{min-height:48px}label{display:block;margin:.5rem 0}img{width:320px;aspect-ratio:16/9;object-fit:contain}[hidden]{display:none!important}pre{white-space:pre-wrap}</style><p><a href="https://haneoka.org/">Haneoka</a></p><h1>读取本地 JSON 与图片</h1><p>选择 promenade.json 和 garden.svg,再一起加载。</p><label>JSON 文件 <input id="json-file" type="file" accept=".json,application/json"></label><label>图片文件 <input id="image-file" type="file" accept="image/*,.svg"></label><button id="load">加载所选文件</button><button id="cancel" disabled>取消</button><button id="close">关闭读取器</button><p id="status" role="status" aria-live="polite">先选择两个文件。</p><h2 id="title"></h2><img id="preview" hidden alt="从所选文件读取的图片"><pre id="output"></pre><script type="module">const by=id=>document.getElementById(id); let loader,controller,generation=0;async function close(){generation++;controller?.abort();by("preview").hidden=true;by("preview").removeAttribute("src");by("title").textContent="";by("output").textContent="";const old=loader;loader=undefined;await old?.dispose();by("cancel").disabled=true;}by("load").onclick=async()=>{ by("load").disabled=true;let current; try{ await close();current=++generation;controller=new AbortController();const signal=controller.signal;by("cancel").disabled=false;by("status").textContent="正在读取…"; const jsonFile=by("json-file").files[0],imageFile=by("image-file").files[0]; if(!jsonFile||!imageFile)throw new Error("先选择 JSON 和图片。"); const {createEmbedLoader,fileDataSource,fileResourceResolver}=await import("https://haneoka.org/embed/core.js"); signal.throwIfAborted();if(current!==generation)return; const files=new Map(); loader=createEmbedLoader({source:fileDataSource(jsonFile),locale:"fr",maxBytes:1048576,resolveResource:fileResourceResolver(files)}); const data=await loader.load({signal}); if(typeof data.background!=="string"||!data.background)throw new Error("JSON 需要 background 素材键。"); files.set(data.background,imageFile); const bytes=await loader.resourceBytes(data.background,{signal}); const url=await loader.resourceUrl(data.background,{signal}); if(current!==generation)return; by("title").textContent=String(data.title??"");by("output").textContent=JSON.stringify({data,imageBytes:bytes.length},null,2); by("preview").src=url;by("preview").hidden=false;await by("preview").decode(); if(current===generation)by("status").textContent="JSON 与所选图片已加载。"; }catch(error){if(current===generation){by("status").textContent=`读取失败: ${error.message}`;const failed=loader;loader=undefined;by("preview").removeAttribute("src");by("preview").hidden=true;await failed?.dispose().catch(()=>{});}} finally{if(current===generation){by("load").disabled=false;by("cancel").disabled=true;}}};by("cancel").onclick=()=>{controller?.abort();};by("close").onclick=async()=>{await close();by("load").disabled=false;by("status").textContent="读取器已关闭,可以再次加载。";};window.addEventListener("pagehide",()=>{void close();},{once:true});</script></html>分别选择 JSON 和所需素材。这里映射一张图片;多文件项目为每个声明的素材键及模型清单引用文件提供对应的 File。
3. 把本页的通用数据转换为剧情
Section titled “3. 把本页的通用数据转换为剧情”promenade.json 的格式是本页的数据读取例,不是 AdvStory。Vega 要求 commands 数组。对这份明确的 lines 格式,可以在读取 data 后创建:
const storyDocument = { localization: { locales: [data.language], defaultLocale: data.language }, commands: [ { command: 25, background: { url: data.background } }, ...data.lines.map(line => ({ command: 2, targetName: line.speaker, text: line.text })), ],};把 storyDocument 作为 mountStory 的 document,并把 assetsBase 指向同一素材目录;完整可保存剧情网页与加载步骤在 Vega。选择的 JSON 已是完整 AdvStory 时,直接读取它,不再做这个特定 lines 转换。decode 校验/转换实际数据,TypeScript 泛型只描述期望类型。
4. 读取 Haneoka 当前资料
Section titled “4. 读取 Haneoka 当前资料”在页内示例选「本站资料」,加载公开歌曲后解析 jacketUrl。适配器与消费者均可使用托管入口:
import { createEmbedLoader, haneokaDataSource } from "https://haneoka.org/embed/core.js";const loader = createEmbedLoader({ source: haneokaDataSource({ resource: "songs", id: "100070" }), server: "intl", locale: "en",});const song = await loader.load();const imageUrl = await loader.resourceUrl(song.jacketUrl);// 先结束消费者,再 await loader.dispose()。公开读取无需账号或 release。server 选择素材目录,locale 选择语言上下文,返回资料保留源提供的多语字段。source.assetsBase 默认是 API origin;资源基于它解析,显式 loader.assetsBase 可覆盖。repeat load 会再次读取当前资料。自有对象使用 memoryDataSource,选中的 File 使用 fileDataSource,更多签名和传输限制都在下方完整参考。
createEmbedLoader 全部参数
Section titled “createEmbedLoader 全部参数”| 字段 | 类型/必填/默认 | 作用、组合和错误 |
|---|---|---|
| source | 必填 DataSource<T> | 提供 load(context),每次 load 调用;可选 source.assetsBase 为素材目录,dispose 清理一次。数据格式应符合消费宿主。 |
| server | 可选 string,undefined | 原样传给 source/resolver;本站 adapter 默认intl并用公开客户端校验 slug,与语言独立。 |
| locale | 可选 string,en | 原样保留 fr/pt-BR 等作者语言;本站通过 Accept-Language 协商支持语言。 |
| assetsBase | 可选 string,source.assetsBase→无 | 覆盖 source base,绝对 HTTP(S) 目录,不含 query/fragment,补斜线;构造时非法 TypeError,无base相对key URL构造失败。 |
| resolveResource | 可选 (key,DataContext)=>string/URL/Blob 或其Promise,默认原key | 完整映射key,相对结果基于base;拒绝即资源失败,Map缺key不回落网络。 |
| fetcher | 可选 (Request)=>Promise<Response>,默认fetch | 接收cors、配置headers/credentials/signal的Request,文档和资源共同使用;媒体元素有独立加载器。拒绝传递为操作失败。 |
| credentials | omit/same-origin/include,omit | 跨域带cookie需服务允许origin与credentials;非法值由Request拒绝。 |
| headers | 可选 HeadersInit,无 | 共用头,source逐次header覆盖同名值;本站以locale设Accept-Language,校验遵循Fetch。 |
| maxBytes | 可选正safe integer,67108864 | 每项解码JSON/资源bytes预算,不是总场景/cache/URL预算。构造非法RangeError,超预算read RangeError(本站包装response_too_large)。 |
| onListenerError | 可选 (unknown)=>void,忽略 | 接收observer异常,自身异常隔离,不影响操作结果。 |
构造无signal/timeout/decode/自动重试cache选项;使用逐次signal、source decoder和宿主策略。resourceUrl仅解析,不下载,也不约束之后媒体元素的字节量。
全部 source factory
Section titled “全部 source factory”| Factory | 必填 | 可选/默认 | 路径与校验 |
|---|---|---|---|
| memoryDataSource(data) | 任意T | 无 | 返回同一值/对象identity,不复制校验,无默认素材目录。 |
| httpDataSource({url,decode?}) | 绝对HTTP(S) string/URL | decode(unknown)=>T,默认JSON | factory校验URL/protocol,base为配置URL目录;redirect不改变base。读取遵循signal/budget,JSON/decoder失败拒绝。 |
| fileDataSource(file,decode?) | File或Blob | decoder同上 | 无网络base,实际Blob bytes受预算/进度控制,坏JSON/decoder拒绝。 |
| fileResourceResolver(files) | ReadonlyMap<string,Blob> | 无 | Unicode完整key字面查找,不规范化;缺项Error。URL生成owned BlobURL,bytes直接读Blob。 |
| haneokaDataSource(options) | resource:string | 下述字段 | 当前公开目录,不填账号/release。 |
本站source全部字段:resource 使用manifest资源语法;id?:string 有值取entity,省略取index;view?:string 选manifest投影;apiBase?:string 默认 https://haneoka.org/api/v1/,绝对HTTP(S)目录不含query/fragment;decode? 校验index/entity,默认JSON值。语法/不存在等load错误沿客户端。source默认素材base是API的origin根,可由loader.assetsBase覆盖;server/locale来自loader。
自定义 source/resolver 的完整 context
Section titled “自定义 source/resolver 的完整 context”DataSource<T> 包含 load(context):T|Promise<T>、可选 assetsBase?:string、dispose():void|Promise<void>。loader销毁时清理一次并等待,cleanup失败拒绝dispose;拥有资源的source按单loader所有权使用。
| DataContext字段 | 类型 | 含义 |
|---|---|---|
| server | 可选string | 原样registry。 |
| locale | string | 作者语言或默认值。 |
| signal | AbortSignal | 当前操作取消,底层工作应跟随。 |
| fetcher | (Request)=>Promise<Response> | 配置后的传输。 |
| maxBytes | number | 有效解码预算,自定义解析也应遵守。 |
| progress | ({loaded:number,total?:number})=>void | 实际字节,未知总量省略total。 |
Resolver同步/异步返回string、URL、Blob。绝对HTTP/HTTPS/Blob/data保留origin,相对基于assetsBase,/开头用origin根;其他protocol TypeError,不猜SSR/页面base。
const source = { assetsBase: "https://author.example/project/", async load(context) { context.signal.throwIfAborted(); const response = await context.fetcher(new Request("https://author.example/project/story.json", { signal: context.signal })); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); // 接入自己的格式校验和预算读取。 }, dispose() { /* 回收source拥有的资源 */ },};const loader = createEmbedLoader({ source, locale: "pt-BR", resolveResource: key => key });handle 的全部属性和方法
Section titled “handle 的全部属性和方法”| 成员 | 参数/结果 | 生命周期 |
|---|---|---|
| locale | readonly string | 有效语言。 |
| server | readonly string/undefined | 显式server,adapterfallback前。 |
| disposed | readonly boolean | dispose开始即true。 |
| load(options={}) | 可选signal,Promise<T> | 每次独立source操作,key为$data。 |
| resourceUrl(key,options={}) | 必填string,可选signal;Promise<string> | 不下载,每个Blob复用一个URL直到销毁。 |
| resourceBytes(key,options={}) | key/可选signal;Promise<Uint8Array> | status/budget/progress真实读取。 |
| subscribe(listener) | 必填(event)=>void;返回unsubscribe | 后续事件,不重放;已销毁抛Error。 |
| cancel() | void | 取消当前全部操作,loader可重用。 |
| dispose() | 共用Promise<void> | 立即禁止新工作、abort、revoke、disposed事件、清监听、source清理一次;幂等,不能重用。 |
忽略signal的source也不能迟到ready/progress覆盖已取消操作;底层source仍应主动停止。cancel可继续,dispose结束loader。先销毁consumer,再撤销loader拥有的URL;作者自行创建的URL自行管理。
全部事件和错误
Section titled “全部事件和错误”| type | 字段 | 时机 |
|---|---|---|
| start/ready | operation:number,key:string | 开始/成功。 |
| progress | operation/key/loaded:number/total?:number | 实际解码字节,内存或仅URL无自动bytes进度;压缩或未知length不填total。 |
| error/cancelled | operation/key/error:unknown | 失败/取消。 |
| disposed | 仅type | 销毁,销毁后的取消错误不继续发出。 |
operation按loader增长,key为$data或原资源名。observer异常不拒绝操作,并发独立、不自动重试或去重;演示只保留32条显示记录,不改变SDK事件流。
EmbedHttpError(status,url) 扩展Error,带name/message/status/url。URL/base/protocol非法TypeError,预算RangeError,JSON SyntaxError,作者source/resolver/decode原错误保留。本站保留ApiClientError,预算为response_too_large及HTTPstatus,默认不可重试。abort拒绝signal.reason,通常AbortError/TimeoutError。dispose后调用为Error,sourcecleanup失败拒绝dispose。
branding子入口全部参数
Section titled “branding子入口全部参数”createHaneokaBranding(document,options={}) 在官方core facade和包/branding入口可用。document必填DOM Document;corner?:top-left/top-right/bottom-left/bottom-right,默认top-left。返回HTMLAnchorElement,不自动append,不请求图像。宿主在有定位的完整容器角落悬浮append,保留48px触控区域,清理时remove。链接固定为 Haneoka,_blank/noopener、原inlineSVG、48px触控与focus;可视embed自动管理,data-only core导入不碰DOM。