Skip to content

Commit afffaf8

Browse files
committed
feat(imgUpload): 分离通用方法
1 parent d49aeef commit afffaf8

File tree

2 files changed

+154
-149
lines changed

2 files changed

+154
-149
lines changed

src/components/base/upload-imgs/index.vue

Lines changed: 8 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
Describe: 多图片上传组件, 附有预览, 排序, 验证等功能
44
55
todo: 支持 before-upload
6-
todo: 支持动态图检测
76
todo: 图像验证支持验证是否是动图
87
todo: 文档编写
9-
todo: accept 严格性优化
108
todo: jsDoc 编写
9+
todo: 文件判断使用 serveWorker 优化性能
1110
-->
1211

1312
<template>
@@ -91,6 +90,13 @@ todo: jsDoc 编写
9190
</template>
9291

9392
<script>
93+
import {
94+
getFileType,
95+
checkIsAnimated,
96+
isEmptyObj,
97+
createId,
98+
} from './utils'
99+
94100
/**
95101
* @typedef {Object<string, number, any>} LocalFileInfo 本地图像通过验证后构造的信息对象
96102
* @property {string} localSrc 本地图像预览地址
@@ -118,53 +124,6 @@ todo: jsDoc 编写
118124
const ONE_KB = 1024
119125
const ONE_MB = ONE_KB * 1024
120126
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-
168127
/**
169128
* 创建项, 如不传入参数则创建空项
170129
* status 状态转换说明:
@@ -246,106 +205,6 @@ function getRangeTip(prx, min, max, unit = '') {
246205
return str
247206
}
248207
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-
349208
/** for originUpload: 一次请求最多的文件数量 */
350209
const uploadLimit = 10
351210
/** for originUpload: 文件对象缓存 */
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
2+
// 检测官方文档: https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
3+
/** 类型检测掩码集合 */
4+
const patternMask = [{
5+
name: 'image/x-icon',
6+
mask: [0xFF, 0xFF, 0xFF, 0xFF],
7+
byte: [0x00, 0x00, 0x01, 0x00],
8+
}, {
9+
name: 'image/x-icon',
10+
mask: [0xFF, 0xFF, 0xFF, 0xFF],
11+
byte: [0x00, 0x00, 0x02, 0x00],
12+
}, {
13+
name: 'image/bmp',
14+
mask: [0xFF, 0xFF],
15+
byte: [0x42, 0x4D],
16+
}, {
17+
name: 'image/gif',
18+
mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
19+
byte: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61],
20+
}, {
21+
name: 'image/gif',
22+
mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
23+
byte: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61],
24+
}, {
25+
name: 'image/webp',
26+
mask: [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
27+
byte: [0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50],
28+
}, {
29+
name: 'image/png',
30+
mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
31+
byte: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
32+
}, {
33+
name: 'image/jpeg',
34+
mask: [0xFF, 0xFF, 0xFF],
35+
byte: [0xFF, 0xD8, 0xFF],
36+
}]
37+
38+
/** 判断是否是空对象 */
39+
export function isEmptyObj(data) {
40+
if (!data) return true
41+
return (JSON.stringify(data) === '{}')
42+
}
43+
44+
/** 生成随机字符串 */
45+
export function createId() {
46+
return Math.random().toString(36).substring(2)
47+
}
48+
49+
50+
/**
51+
* 检测是否是动图
52+
* 主要针对 Gif 和 Webp 两种格式
53+
*/
54+
export async function checkIsAnimated({ file, fileUrl, fileType }) {
55+
// 参数验证
56+
if (!file || !(file instanceof File)) {
57+
console.error('isAnimated param check fail: param expected to be File object')
58+
return false
59+
}
60+
// 如果不是 gif 和 webp, 默认作为非动图
61+
if (fileType !== 'image/webp' && fileType !== 'image/gif') {
62+
return false
63+
}
64+
65+
if (fileType === 'image/webp') {
66+
return new Promise((resolve) => {
67+
const request = new XMLHttpRequest()
68+
request.open('GET', fileUrl, true)
69+
request.addEventListener('load', () => {
70+
resolve((request.response.indexOf('ANMF') !== -1))
71+
})
72+
request.send()
73+
})
74+
}
75+
if (fileType === 'image/gif') {
76+
return new Promise((resolve) => {
77+
const request = new XMLHttpRequest()
78+
request.open('GET', fileUrl, true)
79+
request.responseType = 'arraybuffer'
80+
request.addEventListener('load', () => {
81+
const arr = new Uint8Array(request.response)
82+
// make sure it's a gif (GIF8)
83+
if (arr[0] !== 0x47 || arr[1] !== 0x49 || arr[2] !== 0x46 || arr[3] !== 0x38) {
84+
resolve(false)
85+
return
86+
}
87+
88+
// ported from php http://www.php.net/manual/en/function.imagecreatefromgif.php#104473
89+
// an animated gif contains multiple "frames", with each frame having a
90+
// header made up of:
91+
// * a static 4-byte sequence (\x00\x21\xF9\x04)
92+
// * 4 variable bytes
93+
// * a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?)
94+
// We read through the file til we reach the end of the file, or we've found
95+
// at least 2 frame headers
96+
let frames = 0
97+
for (let i = 0, len = arr.length - 9; i < len && frames < 2; ++i) {
98+
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)) {
99+
frames++
100+
}
101+
}
102+
103+
// if frame count > 1, it's animated
104+
resolve(frames > 1)
105+
})
106+
request.send()
107+
})
108+
}
109+
}
110+
111+
/**
112+
* 检测文件类型
113+
* 使用文件编码进行检测
114+
* 支持模式参看: patternMask 定义
115+
*/
116+
export async function getFileType(file) {
117+
if (!(file instanceof File)) {
118+
return 'unknown'
119+
}
120+
return new Promise((resolve) => {
121+
const fileReader = new FileReader()
122+
fileReader.onloadend = (e) => {
123+
const header = (new Uint8Array(e.target.result)).slice(0, 20)
124+
let type = 'unknown'
125+
126+
// eslint-disable-next-line arrow-body-style
127+
const index = patternMask.findIndex((item) => {
128+
// eslint-disable-next-line arrow-body-style
129+
return item.mask.every((subItem, subI) => {
130+
// subItem 掩码标志
131+
// item.byte[subI] 规范值
132+
// header[subI] 文件实际值
133+
// eslint-disable-next-line
134+
return ((subItem & (header[subI] ^ item.byte[subI])) === 0)
135+
})
136+
})
137+
138+
if (index >= 0) {
139+
type = patternMask[index].name
140+
}
141+
142+
resolve(type)
143+
}
144+
fileReader.readAsArrayBuffer(file)
145+
})
146+
}

0 commit comments

Comments
 (0)