Skip to content

Commit 455cf18

Browse files
donaldshenlevy9527
authored andcommitted
feat!: 可以通过action直接请求上传接口,并移除key与secret的注入 (#119)
1 parent 252cf44 commit 455cf18

File tree

8 files changed

+217
-544
lines changed

8 files changed

+217
-544
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ module.exports = {
99
},
1010
extends: [
1111
'eslint:recommended',
12+
'plugin:jest/recommended',
1213
'plugin:vue/recommended',
1314
'plugin:prettier/recommended',
1415
'prettier/vue'
1516
],
16-
plugins: ['vue', 'prettier'],
17+
plugins: ['vue', 'prettier', 'jest'],
1718
rules: {
1819
'no-console': [
1920
'error',

docs/http-request.md renamed to docs/request.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
```vue
44
<template>
5-
<upload-to-ali multiple v-model="url" :http-request="myUpload" />
5+
<upload-to-ali multiple v-model="url" :request="myUpload" />
66
</template>
77
88
<script>
@@ -23,4 +23,4 @@ export default {
2323
}
2424
}
2525
</script>
26-
```
26+
```

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636
},
3737
"dependencies": {
3838
"@femessage/img-preview": "^1.2.0",
39-
"ali-oss": "^6.0.1",
40-
"image-compressor.js": "^1.1.4"
39+
"compressorjs": "^1.0.6",
40+
"crypto-browserify": "^3.12.0",
41+
"eslint-plugin-jest": "^23.1.1"
4142
},
4243
"devDependencies": {
4344
"@babel/core": "^7.4.3",

src/upload-to-ali.vue

Lines changed: 53 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,11 @@
8383
</template>
8484

8585
<script>
86-
import AliOSS from 'ali-oss'
8786
import ImgPreview from '@femessage/img-preview'
88-
import ImageCompressor from 'image-compressor.js'
87+
import Compressor from 'compressorjs'
8988
import DraggableList from './components/draggable-list.vue'
9089
import UploadItem from './components/upload-item.vue'
91-
import {encodePath} from './utils'
92-
93-
const imageCompressor = new ImageCompressor()
90+
import {getSignature} from './utils'
9491
9592
const oneKB = 1024
9693
@@ -108,20 +105,11 @@ export default {
108105
},
109106
props: {
110107
/**
111-
* 阿里云控制台创建的access key
112-
* 使用前请务必设置跨域 及 ACL
113-
* @link https://help.aliyun.com/document_detail/32069.html?spm=a2c4g.11186623.6.920.9ddd5557vJ6QU7
114-
*/
115-
accessKeyId: {
116-
type: String,
117-
default: process.env.OSS_KEY
118-
},
119-
/**
120-
* 阿里云控制台创建的access secret
108+
* 上传地址
121109
*/
122-
accessKeySecret: {
110+
action: {
123111
type: String,
124-
default: process.env.OSS_SECRET
112+
default: process.env.UPLOAD_ACTION
125113
},
126114
/**
127115
* 存储空间的名字
@@ -207,7 +195,7 @@ export default {
207195
validator: val => val > 0
208196
},
209197
/**
210-
* 图片压缩参数,请参考:https://www.npmjs.com/package/image-compressor.js#options
198+
* 图片压缩参数,请参考:https://www.npmjs.com/package/compressorjs#options
211199
*/
212200
compressOptions: {
213201
type: Object,
@@ -225,7 +213,7 @@ export default {
225213
})
226214
},
227215
/**
228-
* 是否开启预览功能,需要全局注册img-preview组件
216+
* 是否开启预览功能,需要全局注册 img-preview 组件
229217
*/
230218
preview: {
231219
type: Boolean,
@@ -274,50 +262,40 @@ export default {
274262
},
275263
276264
/**
277-
* 自定义上传, 使用此函数则不采用默认 AliOSS 上传行为
265+
* 自定义上传, 使用此函数则会覆盖默认的上传行为
278266
* 返回 Promise, 接收 resolve 参数为 url
279267
*/
280-
httpRequest: {
268+
request: {
281269
type: Function,
282-
async default(file) {
283-
const {name} = file
284-
//文件名-时间戳 作为上传文件key
285-
const pos = name.lastIndexOf('.')
286-
const key =
287-
pos === -1
288-
? `${name}-${Date.now()}`
289-
: `${name.slice(0, pos)}-${Date.now()}${name.slice(pos)}`
290-
const client = this.newClient()
291-
try {
292-
const res = await client.multipartUpload(
293-
this.dir + key,
294-
file,
295-
this.uploadOptions
296-
)
297-
// 协议无关
298-
let url
299-
// 上传时阿里 OSS 会对文件名 encode,但 res.name 没有 encode
300-
// 因此要 encode res.name,否则会因为文件名不同,导致 404
301-
const filename = encodePath(res.name)
302-
if (this.customDomain) {
303-
if (this.customDomain.indexOf('//') > -1)
304-
url = `${this.customDomain}/${filename}`
305-
else {
306-
url = `//${this.customDomain}/${filename}`
270+
default(file) {
271+
const formData = new FormData()
272+
;['bucket', 'region', 'customDomain', 'dir']
273+
.filter(key => this[key])
274+
.forEach(key => formData.append(key, this[key]))
275+
formData.append('file', file)
276+
277+
return new Promise((resolve, reject) => {
278+
const xhr = new XMLHttpRequest()
279+
xhr.responseType = 'json'
280+
xhr.onload = () => {
281+
if (xhr.status === 200) {
282+
resolve(xhr.response.payload.url)
283+
} else {
284+
reject(xhr.response)
307285
}
308-
} else {
309-
url = `//${this.bucket}.${this.region}.aliyuncs.com/${filename}`
310-
}
311-
return url
312-
} catch (error) {
313-
if (client.isCancel()) {
314-
/**
315-
* 上传操作被取消事件
316-
*/
317-
this.$emit('cancel')
318286
}
319-
throw error
320-
}
287+
xhr.onerror = reject
288+
const timestamp = Date.now()
289+
const sep = this.action.indexOf('?') > -1 ? '&' : '?'
290+
const url = `${this.action}${sep}_=${timestamp}`
291+
xhr.open('POST', url, true)
292+
293+
const signature = getSignature(location.origin, timestamp)
294+
xhr.setRequestHeader('x-upload-timestamp', timestamp)
295+
xhr.setRequestHeader('x-upload-signature', signature)
296+
297+
xhr.send(formData)
298+
})
321299
}
322300
}
323301
},
@@ -351,25 +329,6 @@ export default {
351329
}
352330
},
353331
methods: {
354-
newClient() {
355-
const missingKey = [
356-
'region',
357-
'bucket',
358-
'accessKeyId',
359-
'accessKeySecret'
360-
].find(k => !this[k])
361-
if (missingKey) {
362-
throw new Error(`必要参数不能为空: ${missingKey}`)
363-
}
364-
365-
// https://help.aliyun.com/document_detail/32069.html?spm=a2c4g.11186623.6.801.LllSVA
366-
return new AliOSS({
367-
region: this.region,
368-
bucket: this.bucket,
369-
accessKeyId: this.accessKeyId,
370-
accessKeySecret: this.accessKeySecret
371-
})
372-
},
373332
onDelete(url, index) {
374333
const result = this.multiple ? this.uploadList.filter(v => v !== url) : ''
375334
this.$emit('input', result)
@@ -447,18 +406,26 @@ export default {
447406
const max = this.multiple ? this.max : 1
448407
for (let i = 0; i < files.length && this.uploadList.length < max; i++) {
449408
// 尝试压缩文件
450-
const file = enableCompressRegex.test(files[i].type)
451-
? await imageCompressor.compress(files[i], this.compressOptions)
452-
: files[i]
453-
409+
let file = files[i]
410+
if (enableCompressRegex.test(file.type)) {
411+
const blob = await new Promise((resolve, reject) => {
412+
new Compressor(file, {
413+
...this.compressOptions,
414+
success: resolve,
415+
error: reject
416+
})
417+
})
418+
/* eslint-disable-next-line require-atomic-updates */
419+
file = new File([blob], file.name)
420+
}
454421
/**
455422
* 上传过程中
456423
* @property {string} name - 当前上传的图片名称
457424
*/
458425
this.$emit('loading', file.name)
459426
460427
try {
461-
const url = await this.httpRequest(file)
428+
const url = await this.request(file)
462429
if (typeof url !== 'string' || !/^(https?:)?\/\//.test(url)) {
463430
throw new Error(
464431
`\`Promise.resolve\` 接收的参数应该是超链接(url), 当前为 ${typeof url}.`
@@ -468,17 +435,10 @@ export default {
468435
currentUploads.push(url)
469436
} catch (error) {
470437
console.warn(error.message)
471-
if (error.code === 'ConnectionTimeoutError') {
472-
/**
473-
* 上传超时
474-
*/
475-
this.$emit('timeout')
476-
} else {
477-
/**
478-
* 上传失败
479-
*/
480-
this.$emit('fail')
481-
}
438+
/**
439+
* 上传失败
440+
*/
441+
this.$emit('fail')
482442
}
483443
}
484444

src/utils.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1-
export function encodePath(url) {
2-
return url
3-
.split('/')
4-
.map(str => encodeURIComponent(str))
5-
.join('/')
1+
import crypto from 'crypto-browserify'
2+
3+
/**
4+
* 签名函数参考
5+
* https://help.aliyun.com/document_detail/29442.html?spm=a2c4g.11186623.2.13.2c541605Ky1bxj
6+
*/
7+
export function getSignature(origin, timestamp) {
8+
const param = {
9+
origin,
10+
timestamp,
11+
signatureMethod: 'HMAC-SHA1'
12+
}
13+
const paramStr = Object.keys(param)
14+
.sort()
15+
.map(k => `${k}=${encodeURIComponent(param[k])}`)
16+
.join('&')
17+
const signStr = 'POST&%2F&' + encodeURIComponent(paramStr)
18+
19+
return crypto
20+
.createHmac('sha1', 'nonce')
21+
.update(signStr)
22+
.digest('base64')
623
}
724

825
export function getBasename(url = '') {

styleguide.config.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ const {VueLoaderPlugin} = require('vue-loader')
22
const path = require('path')
33
const glob = require('glob')
44
const env = Object.assign({}, require('dotenv').config().parsed, {
5-
OSS_KEY: process.env.OSS_KEY,
6-
OSS_SECRET: process.env.OSS_SECRET,
5+
UPLOAD_ACTION: process.env.UPLOAD_ACTION,
76
OSS_BUCKET: process.env.OSS_BUCKET,
87
OSS_REGION: process.env.OSS_REGION
98
})

test/utils.test.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {getBasename, encodePath} from '../src/utils'
1+
import {getBasename, getSignature} from '../src/utils'
22

33
describe('getBasename', () => {
44
test('仅存在文件名', () => {
@@ -23,9 +23,15 @@ describe('getBasename', () => {
2323
})
2424
})
2525

26-
describe('encodePath', () => {
27-
test('文件名带有特殊字符的文件地址', () => {
28-
const url = '//example+1-1563433997757.jpg'
29-
expect(encodePath(url)).toBe('//example%2B1-1563433997757.jpg')
26+
describe('getSignature', () => {
27+
test('用例一', () => {
28+
const origin = 'http://localhost:6060'
29+
const timestamp = 1575362612539
30+
expect(getSignature(origin, timestamp)).toBe('VBrn5MJFxMj5OPExLx8eXq1DCCc=')
31+
})
32+
test('用例二', () => {
33+
const origin = 'https://example.com'
34+
const timestamp = 1575362612539
35+
expect(getSignature(origin, timestamp)).toBe('iZj/mMZghtdi1jyk9zqG8mYiKPo=')
3036
})
3137
})

0 commit comments

Comments
 (0)