|
3 | 3 | Describe: 多图片上传组件, 附有预览, 排序, 验证等功能 |
4 | 4 |
|
5 | 5 | todo: 支持 before-upload |
6 | | -todo: 支持动态图检测 |
7 | 6 | todo: 图像验证支持验证是否是动图 |
8 | 7 | todo: 文档编写 |
9 | | -todo: accept 严格性优化 |
10 | 8 | todo: jsDoc 编写 |
| 9 | +todo: 文件判断使用 serveWorker 优化性能 |
11 | 10 | --> |
12 | 11 |
|
13 | 12 | <template> |
@@ -91,6 +90,13 @@ todo: jsDoc 编写 |
91 | 90 | </template> |
92 | 91 |
|
93 | 92 | <script> |
| 93 | +import { |
| 94 | + getFileType, |
| 95 | + checkIsAnimated, |
| 96 | + isEmptyObj, |
| 97 | + createId, |
| 98 | +} from './utils' |
| 99 | +
|
94 | 100 | /** |
95 | 101 | * @typedef {Object<string, number, any>} LocalFileInfo 本地图像通过验证后构造的信息对象 |
96 | 102 | * @property {string} localSrc 本地图像预览地址 |
@@ -118,53 +124,6 @@ todo: jsDoc 编写 |
118 | 124 | const ONE_KB = 1024 |
119 | 125 | const ONE_MB = ONE_KB * 1024 |
120 | 126 |
|
121 | | -// 检测官方文档: https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern |
122 | | -/** 类型检测掩码集合 */ |
123 | | -const patternMask = [{ |
124 | | - name: 'image/x-icon', |
125 | | - mask: [0xFF, 0xFF, 0xFF, 0xFF], |
126 | | - byte: [0x00, 0x00, 0x01, 0x00], |
127 | | -}, { |
128 | | - name: 'image/x-icon', |
129 | | - mask: [0xFF, 0xFF, 0xFF, 0xFF], |
130 | | - byte: [0x00, 0x00, 0x02, 0x00], |
131 | | -}, { |
132 | | - name: 'image/bmp', |
133 | | - mask: [0xFF, 0xFF], |
134 | | - byte: [0x42, 0x4D], |
135 | | -}, { |
136 | | - name: 'image/gif', |
137 | | - mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], |
138 | | - byte: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], |
139 | | -}, { |
140 | | - name: 'image/gif', |
141 | | - mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], |
142 | | - byte: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], |
143 | | -}, { |
144 | | - name: 'image/webp', |
145 | | - mask: [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], |
146 | | - byte: [0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50], |
147 | | -}, { |
148 | | - name: 'image/png', |
149 | | - mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], |
150 | | - byte: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], |
151 | | -}, { |
152 | | - name: 'image/jpeg', |
153 | | - mask: [0xFF, 0xFF, 0xFF], |
154 | | - byte: [0xFF, 0xD8, 0xFF], |
155 | | -}] |
156 | | -
|
157 | | -/** 判断是否是空对象 */ |
158 | | -function isEmptyObj(data) { |
159 | | - if (!data) return true |
160 | | - return (JSON.stringify(data) === '{}') |
161 | | -} |
162 | | -
|
163 | | -/** 生成随机字符串 */ |
164 | | -function createId() { |
165 | | - return Math.random().toString(36).substring(2) |
166 | | -} |
167 | | -
|
168 | 127 | /** |
169 | 128 | * 创建项, 如不传入参数则创建空项 |
170 | 129 | * status 状态转换说明: |
@@ -246,106 +205,6 @@ function getRangeTip(prx, min, max, unit = '') { |
246 | 205 | return str |
247 | 206 | } |
248 | 207 |
|
249 | | -/** |
250 | | - * 检测是否是动图 |
251 | | - * 主要针对 Gif 和 Webp 两种格式 |
252 | | - * @param {File} file 需要检测的文件 |
253 | | - * @param {String} fileUrl 文件url |
254 | | - */ |
255 | | -async function checkIsAnimated({ file, fileUrl, fileType }) { |
256 | | - // 参数验证 |
257 | | - if (!file || !(file instanceof File)) { |
258 | | - console.error('isAnimated param check fail: param expected to be File object') |
259 | | - return false |
260 | | - } |
261 | | - // 如果不是 gif 和 webp, 默认作为非动图 |
262 | | - if (fileType !== 'image/webp' && fileType !== 'image/gif') { |
263 | | - return false |
264 | | - } |
265 | | -
|
266 | | - if (fileType === 'image/webp') { |
267 | | - return new Promise((resolve) => { |
268 | | - const request = new XMLHttpRequest() |
269 | | - request.open('GET', fileUrl, true) |
270 | | - request.addEventListener('load', () => { |
271 | | - resolve((request.response.indexOf('ANMF') !== -1)) |
272 | | - }) |
273 | | - request.send() |
274 | | - }) |
275 | | - } |
276 | | - if (fileType === 'image/gif') { |
277 | | - return new Promise((resolve) => { |
278 | | - const request = new XMLHttpRequest() |
279 | | - request.open('GET', fileUrl, true) |
280 | | - request.responseType = 'arraybuffer' |
281 | | - request.addEventListener('load', () => { |
282 | | - const arr = new Uint8Array(request.response) |
283 | | - // make sure it's a gif (GIF8) |
284 | | - if (arr[0] !== 0x47 || arr[1] !== 0x49 || arr[2] !== 0x46 || arr[3] !== 0x38) { |
285 | | - resolve(false) |
286 | | - return |
287 | | - } |
288 | | -
|
289 | | - // ported from php http://www.php.net/manual/en/function.imagecreatefromgif.php#104473 |
290 | | - // an animated gif contains multiple "frames", with each frame having a |
291 | | - // header made up of: |
292 | | - // * a static 4-byte sequence (\x00\x21\xF9\x04) |
293 | | - // * 4 variable bytes |
294 | | - // * a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?) |
295 | | - // We read through the file til we reach the end of the file, or we've found |
296 | | - // at least 2 frame headers |
297 | | - let frames = 0 |
298 | | - for (let i = 0, len = arr.length - 9; i < len && frames < 2; ++i) { |
299 | | - if (arr[i] === 0x00 && arr[i + 1] === 0x21 && arr[i + 2] === 0xF9 && arr[i + 3] === 0x04 && arr[i + 8] === 0x00 && (arr[i + 9] === 0x2C || arr[i + 9] === 0x21)) { |
300 | | - frames++ |
301 | | - } |
302 | | - } |
303 | | -
|
304 | | - // if frame count > 1, it's animated |
305 | | - resolve(frames > 1) |
306 | | - }) |
307 | | - request.send() |
308 | | - }) |
309 | | - } |
310 | | -} |
311 | | -
|
312 | | -/** |
313 | | - * 检测文件类型 |
314 | | - * 使用文件编码进行检测 |
315 | | - * 支持模式参看: patternMask 定义 |
316 | | - */ |
317 | | -async function getFileType(file) { |
318 | | - if (!(file instanceof File)) { |
319 | | - return 'unknown' |
320 | | - } |
321 | | - return new Promise((resolve) => { |
322 | | - const fileReader = new FileReader() |
323 | | - fileReader.onloadend = (e) => { |
324 | | - const header = (new Uint8Array(e.target.result)).slice(0, 20) |
325 | | - let type = 'unknown' |
326 | | -
|
327 | | - // eslint-disable-next-line arrow-body-style |
328 | | - const index = patternMask.findIndex((item) => { |
329 | | - // eslint-disable-next-line arrow-body-style |
330 | | - return item.mask.every((subItem, subI) => { |
331 | | - // subItem 掩码标志 |
332 | | - // item.byte[subI] 规范值 |
333 | | - // header[subI] 文件实际值 |
334 | | - // eslint-disable-next-line |
335 | | - return ((subItem & (header[subI] ^ item.byte[subI])) === 0) |
336 | | - }) |
337 | | - }) |
338 | | -
|
339 | | - if (index >= 0) { |
340 | | - type = patternMask[index].name |
341 | | - } |
342 | | -
|
343 | | - resolve(type) |
344 | | - } |
345 | | - fileReader.readAsArrayBuffer(file) |
346 | | - }) |
347 | | -} |
348 | | -
|
349 | 208 | /** for originUpload: 一次请求最多的文件数量 */ |
350 | 209 | const uploadLimit = 10 |
351 | 210 | /** for originUpload: 文件对象缓存 */ |
|
0 commit comments