源码字段与浏览器资源分开
Markdown 保存可持久资源地址,浏览器可能需要短期签名 URL、Blob 或内部资源接口。Image.configure({ resolve }) 负责把源码目标转换为 { url, release? },不是把凭证写进图片语法。
下面是完整的同源资源接入。它只接受 /assets/<id>,需要应用自行实现 GET /api/assets/:id,本网站不会向这个演示地址发送请求。
import { Image } from "@quirekit/msfe-image";
import { StarterKit } from "@quirekit/msfe-starter-kit";
/** A same-origin asset resolver; authentication remains in the host fetch policy. */
export const PrivateImages = Image.configure({
/** @param source - Canonical asset reference. @param signal - This image's lifetime. @returns One browser resource lease. */
async resolve(source, signal) {
const match = /^\/assets\/([a-zA-Z0-9_-]+)$/.exec(source);
if (!match) throw new Error("unsupported-asset-reference");
const response = await fetch(`/api/assets/${encodeURIComponent(match[1]!)}`, { signal, credentials: "same-origin" });
if (!response.ok) throw new Error(`asset-http-${response.status}`);
const blob = await response.blob();
if (!/^image\/(png|jpeg|webp|gif)$/.test(blob.type) || blob.size > 5_000_000) throw new Error("unsupported-image");
const url = URL.createObjectURL(blob);
return { url, release() { URL.revokeObjectURL(url); } };
},
});
/** Replace the preset's image owner; the host must implement GET /api/assets/:id. */
export const privateImageKit = StarterKit.configure({ image: PrivateImages });
privateImageKit 替换了 StarterKit 的 image 项,避免同名重复安装。源码中的 /assets/ 相对路径经过 host resolver 变成 Blob URL;资源释放时撤销该 URL。示例限制图片类型与 5 MB 大小,可按真实存储协议调整。
粘贴、拖入与上传
图片功能自带粘贴与拖入:剪贴板或拖放中只有图片文件时接管,存好后在落点插入 。上传期间落点由源码锚点保持,只读时不处理。图片存到哪里由 upload 决定:
import { Image, type ImageUploader } from '@quirekit/msfe-image'
const uploader: ImageUploader = {
async upload(file, { source, name, signal }) {
const body = new FormData()
body.append('file', file, name ?? 'image')
const response = await fetch('/api/assets', { method: 'POST', body, signal })
if (!response.ok) throw new Error('upload-failed')
return (await response.json()).url // 写入 Markdown 的持久地址
},
}
Image.configure({ upload: uploader })
- 提供
upload:上传完成后写入返回的地址;某张图片失败时不写入它,提示由宿主在upload里给出。source区分粘贴、拖入、对话框、Word 导入与 AI 等入口,可用于配额或命名。 - 省略
upload:PNG、JPEG、GIF、WebP 内联为data:地址,文档自包含但会变大;SVG 不内联,需要宿主上传。 upload: false:不处理粘贴与拖入,交给应用自己的扩展。
resolve 是可选的反向查询,导出 Word、PDF 时把存储地址取回为 data URI。
插入命令
const inserted = editor.commands.insertImage({
url: '/assets/diagram_42',
alt: '系统结构图',
title: '架构评审',
})
命令验证字段并编码为 Markdown。自己实现上传入口(如对话框)时,先捕获目标、上传文件、取得持久地址,再执行命令;长时间等待使用 映射锚点,取消或换文档后不应插入到新的光标位置。
内置 Image 在调用 resolver 之前就校验源码地址;不接受 asset: 等自定义协议。使用安全相对路径或 HTTPS 地址作为持久标识。若必须支持新协议,需要自己的字段校验和 codec,不能只换 resolver。
更换图片组件
Image.extend({ addNodeView() { return VueNodeViewRenderer(ImageView) } }) 可以继承字段 codec 和资源协议。组件通过 nodeView.status 处理加载、成功、错误,使用 nodeView.resource 中的结果,并通过 updateAttributes 或受控动作修改源码。
可增加标题表单、错误提示和设计系统操作按钮;不要从 .cm-content 移出图片范围或直接改 src 后声称已保存。组件显示资源改变与 Markdown 字段改变是两件不同的事。
链接与外部打开策略
链接功能的 Link.configure({ openLink }) 接管显式用户动作的打开策略;只在应用需要时打开安全链接。投影构建或图片加载不能自动触发外部导航。引用式链接/图片、相对地址和特殊协议需要按实际 codec 支持范围处理,不能把显示成功等同于任意格式都可精确写回。
验证
覆盖成功、HTTP 失败、非图片响应、体积超限、迟到响应、删除节点、只读及撤销;粘贴与拖入分别验证配置上传与内联两种情况。确认离开页面后没有遗留 object URL,源码始终保存持久 ID。进一步参见 资源生命周期 和 UI 与主题。