drawui-react 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,205 @@
1
+ # 图片上传通道 · 集成指南
2
+
3
+ > 📌 本文件是 `packages/core/IMAGE_UPLOAD.md` 的**同步副本**,随 `drawui-react` 一起发布,安装后不必跳转即可阅读。内容改动请以 core 那份为准,改完同步覆盖本文件。
4
+
5
+ > 面向接入方(队友 / 业务方)。用于把 drawui 的「图片」按钮、粘贴、拖入文件统一接到自家上传服务(OSS / 自建后端 / 其它对象存储)。
6
+
7
+ ## 〇、这份文档在哪
8
+
9
+ | 场景 | 路径 |
10
+ | --- | --- |
11
+ | 本仓库(真源,改动以此为准) | `packages/core/IMAGE_UPLOAD.md` |
12
+ | 本仓库(同步副本) | `packages/react/IMAGE_UPLOAD.md` |
13
+ | 安装依赖后 | `node_modules/drawui-core/IMAGE_UPLOAD.md` / `node_modules/drawui-react/IMAGE_UPLOAD.md` |
14
+ | 在线 | <https://github.com/weishell/drawui/blob/main/packages/core/IMAGE_UPLOAD.md> |
15
+
16
+ > 两个 npm 包均已把该文档列入 `files`,`README.md` 里的链接使用的是包内相对路径,安装后同样点得开。
17
+
18
+ ## 一、能力定位
19
+
20
+ - **drawui 不内置任何对象存储 SDK**(不依赖 `ali-oss`、不预置 STS、不暴露任何 accessKey)。
21
+ - SDK 只负责「选文件 → 拿到一个 URL → 在画布上渲染图片」,至于 URL 从哪儿来,完全由接入方决定。
22
+ - 提供 **两条通道**,二选一或并存:
23
+
24
+ | 通道 | 形式 | 适用 |
25
+ | --- | --- | --- |
26
+ | **A. `ImageUploadConfig`** | 配 `{ url, method, fieldName, headers, fields, responseUrlPath, timeoutMs, maxSizeBytes }` | 队友有自己的中转接口(前端 → 队友后端 → OSS) |
27
+ | **B. `ImageUploader` 函数** | `(file: File) => Promise<string>`,函数内自行拿到 STS、签名直传 OSS | 前端直传 OSS / 自定义分片 / 自定义压缩 |
28
+
29
+ **优先级 B > A**(函数优先于配置)。
30
+
31
+ ## 二、硬性约定(已默认开启,接入方不需要写)
32
+
33
+ | 项 | 默认值 | 覆盖方式 |
34
+ | --- | --- | --- |
35
+ | **单张图片体积上限** | **30 MB**(超过直接拒绝,不会发出请求) | `ImageUploadConfig.maxSizeBytes` 自定义;写 `ImageUploader` 函数时需自己检查 |
36
+ | **上传超时** | **60 秒**(之前是 30 秒,已调大) | `ImageUploadConfig.timeoutMs` 自定义 |
37
+ | **失败重试** | 不重试,抛错后由调用方提示用户 | 自定义 `ImageUploader` 时自己实现 |
38
+ | **请求并发控制** | 不限制,多文件并发上传 | 自定义 `ImageUploader` 时自己实现 |
39
+ | **跨域** | 远端图默认 `crossOrigin='anonymous'` | `setImageCrossOrigin(null)` 关闭(注意会污染画布导致 PNG 导出失败) |
40
+
41
+ > 体积超限走错误码 `image.tooLarge:`(前端 toast 已自动用 `image.tooLarge` 文案显示)。
42
+
43
+ ## 三、怎么调(按场景)
44
+
45
+ ### 场景 1:队友有自己的上传中转接口(最常见)
46
+
47
+ ```tsx
48
+ import { CanvasBoard } from 'drawui-react'
49
+
50
+ <CanvasBoard
51
+ imageUpload={{
52
+ url: 'https://api.team.com/upload',
53
+ method: 'POST',
54
+ fieldName: 'file', // 后端接收字段名
55
+ headers: { Authorization: `Bearer ${token}` },
56
+ // responseUrlPath: 'data.url', // 响应 JSON 里 url 的路径;不填自动探测
57
+ // timeoutMs: 60000, // 默认 60s
58
+ // maxSizeBytes: 30 * 1024 * 1024, // 默认 30 MB
59
+ }}
60
+ />
61
+ ```
62
+
63
+ 后端返回任一形式即可(自动探测顺序:`url` → `data.url` → `data` → `data.src` → `location` → `path` → `src` → `href` → `fileUrl` → `imageUrl` → `data.fileUrl`):
64
+
65
+ ```jsonc
66
+ // 推荐:直接给 url
67
+ { "url": "https://cdn.example.com/foo.png" }
68
+
69
+ // 或者套了一层
70
+ { "code": 0, "data": { "url": "https://cdn.example.com/foo.png" } }
71
+ ```
72
+
73
+ 如果自动探测拿不准,**强烈建议显式配 `responseUrlPath: 'data.url'`**,避免 JSON 里其它字段(如 `userUrl`)被误识别。
74
+
75
+ ### 场景 2:前端直传 OSS(拿 STS 后 PutObject)
76
+
77
+ **必须用 `ImageUploader` 函数,不要用 `ImageUploadConfig.headers` 试图塞 OSS 签名 —— 浏览器会先发 CORS 预检,而 OSS 默认不回 `x-oss-*` 自定义头的预检响应。**
78
+
79
+ ```tsx
80
+ import OSS from 'ali-oss'
81
+ import { CanvasBoard } from 'drawui-react'
82
+
83
+ async function uploadToOSS(file: File): Promise<string> {
84
+ // 1. 跟你们后端拿 STS 临时凭证
85
+ const sts = await fetch('/api/oss/sts', { credentials: 'include' }).then(r => r.json())
86
+ // 期望字段:accessKeyId / accessKeySecret / securityToken / region / bucket / host / dir
87
+ const { accessKeyId, accessKeySecret, securityToken, region, bucket, host, dir } = sts
88
+
89
+ // 2. 客户端压缩一下,避免撞 60s 超时
90
+ const compressed = await compressIfNeeded(file, 30 * 1024 * 1024)
91
+
92
+ // 3. 直传 OSS
93
+ const oss = new OSS({
94
+ accessKeyId, accessKeySecret, stsToken: securityToken,
95
+ bucket, region, secure: true,
96
+ })
97
+ const filename = `${dir}/${Date.now()}_${compressed.name}`
98
+ await oss.put(filename, compressed, {
99
+ headers: { 'x-oss-object-acl': 'public-read' },
100
+ })
101
+
102
+ // 4. 必须返回公网可访问的 URL
103
+ return `${host}/${filename}`
104
+ }
105
+
106
+ <CanvasBoard imageUpload={uploadToOSS} />
107
+ ```
108
+
109
+ > STS 接口必须保证返回的 `host` + 文件路径能在浏览器里直接 GET 到,并且对应 OSS 桶回 **正确的 CORS 头**。
110
+
111
+ ### 场景 3:暂未接入上传服务
112
+
113
+ 不传 `imageUpload` 也能用 —— SDK 会走 `URL.createObjectURL(file)` 本地兜底,图片以 **`temporary: true`** 标记。**但**:刷新页面图片即丢失。SDK 会自动弹「已插入临时图片(未配置上传接口,刷新后会丢失)」toast 提示用户。
114
+
115
+ 接入方在保存数据前可调用 `editor.hasTemporaryImages()` 检测并提示用户:
116
+
117
+ ```ts
118
+ if (editor.hasTemporaryImages()) {
119
+ // 让用户先上传完再保存,或者保存时提醒刷新会丢
120
+ }
121
+ ```
122
+
123
+ ## 四、调用链速览(接入方排查用)
124
+
125
+ ```
126
+ Toolbar「图片」按钮 ─► onInsertImage(files)
127
+ │
128
+ ▼
129
+ CanvasBoard (imageUpload?: ImageUploadOption)
130
+ │
131
+ ▼
132
+ editor.insertImageFiles(files, at)
133
+ │
134
+ ▼
135
+ Editor.insertImageFile(file, at) ← packages/core/src/editor/Editor.ts
136
+ │
137
+ ┌─────────┴─────────┐
138
+ 配了 imageUploadOption 没配(默认)
139
+ │ │
140
+ ▼ ▼
141
+ runUploader(option, file) URL.createObjectURL(file)
142
+ ─► 拿远端 URL ─► blob:xxx(temporary:true)
143
+ │ │
144
+ └─────────┬───────────┘
145
+ ▼
146
+ loadImage(src) → ImageShape
147
+ ▼
148
+ src 入画布(不是 base64)
149
+ ```
150
+
151
+ 三条触发路径都走同一通道:
152
+
153
+ | 入口 | 代码位置(按符号定位,行号会漂移) |
154
+ | --- | --- |
155
+ | 工具栏「图片」按钮 | `Toolbar.tsx` → `handleImagePicked`(隐藏 `<input type="file">` 的 `onChange`) |
156
+ | 拖拽图片到画布 | `CanvasBoard.tsx` → `onDrop` → `editor.insertImageFiles(dropped, at)` |
157
+ | 粘贴图片 | `CanvasBoard.tsx` → 粘贴处理 → `editor.insertImageFiles(files)`(失败经 `imageError` UI 通道回流) |
158
+
159
+ ## 五、接 OSS 时最容易踩的 10 个坑
160
+
161
+ | # | 现象 | 根因 / 处理 |
162
+ | --- | --- | --- |
163
+ | 1 | 上传完图裂了 | OSS 桶没回 CORS 头。检查桶的「跨域设置」:允许源 `*`(或具体域名)、允许方法 `GET/HEAD/PUT/POST`、允许头 `*` |
164
+ | 2 | `ImageUploadConfig.headers` 加 `Authorization` 后浏览器报错 | 浏览器会先发 CORS 预检,队友接口没回 `Access-Control-Allow-Headers`。要么后端放开,要么改走 `ImageUploader` 函数自己 fetch |
165
+ | 3 | 60 秒超时 | 大图直传。压缩后再上传,或调大 `timeoutMs: 180000` |
166
+ | 4 | 一次选 20 张图全失败 | 并发数太高被 OSS 限流。自定义 `ImageUploader` 内做并发限流(如 p-limit) |
167
+ | 5 | 上传报错后用户重试不了 | SDK 不内置重试,在 `ImageUploader` 内自己加 |
168
+ | 6 | 刷新后图全没了 | 拿到的是 `blob:` 本地地址或返回的 URL 不公网可访问。`editor.hasTemporaryImages()` 帮你在保存前检测 |
169
+ | 7 | 客户端工具菜单「另存为图片」导出 PNG 失败 | OSS 没配 CORS 头,canvas 被污染。用 `setImageCrossOrigin(null)` 可暂解,但会失去 PNG/SVG 导出能力 |
170
+ | 8 | 自动探测 URL 拿错了字段 | 返回 JSON 里恰好有非图片含义的 `url` 字段。**显式配 `responseUrlPath`** |
171
+ | 9 | 拖拽多个文件只插入一张 | `CanvasBoard.tsx` 的 `onDrop` 已过滤非图片。检查 `file.type` 是否正确 |
172
+ | 10 | 大于 30 MB 静默失败 | 不静默,会抛 `image.tooLarge:`,前端 toast 用 `image.tooLarge` 文案显式提示用户。要调大用 `maxSizeBytes` |
173
+
174
+ ## 六、自检 checklist(上线前过一遍)
175
+
176
+ - [ ] OSS 桶已配 CORS
177
+ - [ ] STS 接口走自家鉴权(不依赖前端 cookie)
178
+ - [ ] 返回的图片 URL 公网可访问,浏览器 GET 200
179
+ - [ ] 上传 > 30 MB 的图被拒绝(toast 提示用户)
180
+ - [ ] 上传 > 60s 的请求会超时(确认是自己调的 60s 还是后端更快超时)
181
+ - [ ] 上传完 → 刷新页面 → 图片还在(验证不是 `blob:`)
182
+ - [ ] 保存文档前调用 `editor.hasTemporaryImages()` 提示用户
183
+ - [ ] `image.tooLarge` 文案在你们支持的语言里都翻译到位(默认带 7 种语言,扩展见 `packages/core/src/i18n.ts`)
184
+
185
+ ## 七、相关导出(来自 `drawui-core`)
186
+
187
+ ```ts
188
+ import {
189
+ Editor,
190
+ DEFAULT_IMAGE_MAX_SIZE_BYTES, // 30 * 1024 * 1024
191
+ type ImageUploadOption, // 配置 | 函数,二选一
192
+ type ImageUploadConfig, // url / method / headers / responseUrlPath / timeoutMs / maxSizeBytes ...
193
+ type ImageUploader, // (file: File) => Promise<string>
194
+ isUploaderFunction,
195
+ parseUploadResponse, // 解析后端响应文本 → URL(业务方自实现上传函数时可复用)
196
+ } from 'drawui-core'
197
+ ```
198
+
199
+ 核心文件:
200
+
201
+ - `packages/core/src/imageUpload.ts`:上传通道定义 + fetch 实现
202
+ - `packages/core/src/editor/Editor.ts`:`insertImageFile` / `insertImageFiles`(带 30 MB 闸门)
203
+ - `packages/core/src/imageStore.ts`:图片加载、缓存、跨域处理
204
+ - `packages/react/src/components/CanvasBoard.tsx`:`imageUpload` prop + toast 文案
205
+ - `packages/core/src/i18n.ts`:多语言文案(含新增的 `image.tooLarge`)