基于Taro 3的图片上传组件实现

前言

在19年写过一篇关于图片上传组件实现的文章(已删除),但是考虑到Taro的版本变更,及当年技术的不成熟,代码上有许多错漏及让人疑惑的地方,所以对原来的版本进行了一个查漏补缺,并将完整的代码贴出来。

本例只做一个简单的基于Taro的图片上传组件的实现,使用Taro来做跨端处理,这里主要以微信小程序以为主,其他端还没有测试,感兴趣的同学可以自己尝试,做简单兼容即可,更多的功能逻辑也可以在此基础上自行扩展(比如样式优化,扩展组件属性及回调函数等)。

原理

在Taro的基础上实现图片上传及预览功能,这里主要是调用三个基础API

  1. 选择图片API: Taro.chooseImage
  2. 上传文件API: Taro.uploadFile
  3. 图片预览API: Taro.previewImage

实现

注1: 请务必保证自己的Taro已安装,详情教程请见: Taro官方文档
注2: 本例使用Taro版本号为3.3.6(当前最新版本,若非3.x版本库,请审慎考虑,酌情拷贝),理论上本例主体部分代码可以在Taro 2上运行(除了引入taro部分代码需要变动之外)
注3: 本例使用react语法,组件使用class写法(使用其他写法的请自行调整)

废话不多说,直接上代码

目录结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# src目录
│ app.config.js
│ app.js
│ app.scss
│ index.html
│ tree.txt

├─assets
│ └─images
│ close.png
│ plus.png

├─components
│ └─ImageUpload
│ index.jsx
│ index.scss

└─pages
└─index
index.config.js
index.jsx
index.scss
组件代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/* src/components/ImageUpload/index.jsx */
import { PureComponent } from 'react'
import Taro from '@tarojs/taro'
import { View, Image } from '@tarojs/components'
// 关闭及新增的图标(图片请自行网上搜索,或者让UI出图)
import closePng from '../../assets/images/close.png'
import plusPng from '../../assets/images/plus.png'
import './index.scss'

export default class ImageUpload extends PureComponent {
static defaultProps = {
imageFiles: [], // 图片列表
maxLength: 4, // 最大可上传图片数
onChange() { }, // 图片改变事件回调
onImageClick() { }, // 图片点击事件回调
}

constructor(props) {
super(props)
this.picUrls = []
this.state = {
uploadUrl: process.env.BASE_URL + '/api/upload', // 图片上传url
token: '', // 鉴权token
uploadCurrent: 0 // 当前上传图片的index
}
}

componentDidMount() {
// 获取服务端token(token主要用来做校验,如果不需要校验或者已经有token,则可以去除)
this.getToken()
}

// 获取上传token
async getToken() {
try {
const res = await Taro.request({
url: process.env.BASE_URL + '/api/token', // 获取上传token的后台接口
method: 'GET'
})
if (res) {
this.setState({
token: res.data?.token // 获取从后台传回的token
})
}
} catch (err) {
// do something
console.log(err)
}
}

// 选择图片
chooseImage() {
const { imageFiles, maxLength } = this.props
const limit = maxLength - imageFiles.length
Taro.chooseImage({
count: limit,
success: (res) => {
this.uploadImage(res.tempFilePaths)
}
})
}

// 上传图片(这里涉及到多图一起上传的处理)
uploadImage(tempFilePaths) {
const { uploadCurrent, uploadUrl, token } = this.state
const { onChange, imageFiles } = this.props
Taro.uploadFile({
url: uploadUrl,
filePath: tempFilePaths[uploadCurrent],
name: 'file',
formData: { 'token': token },
success: (res) => {
if (res.errMsg === "uploadFile:ok") {
const imageUrl = process.env.BASE_URL + res.data.src
this.picUrls.push(imageUrl)
} else {
console.error('图片上传失败')
}
},
fail: () => {
console.error('图片上传失败')
},
complete: () => {
if (uploadCurrent === tempFilePaths.length - 1) { // 图片全部上传完成
const totalFiles = imageFiles.concat(this.picUrls)
onChange(totalFiles)
this.picUrls = []
this.setState({
uploadCurrent: 0
})
} else { // 图片没有全部上传完成
this.setState({
uploadCurrent: uploadCurrent + 1
}, () => {
this.uploadImage(tempFilePaths)
})
}
}
})
}

// 点击上传
handlePlusClick() {
this.chooseImage()
}

// 删除图片
handleImageDelete(index) {
const { imageFiles, onChange } = this.props
const copyFiles = JSON.parse(JSON.stringify(imageFiles))
copyFiles.splice(index, 1)
onChange(copyFiles)
}

render() {
const { onImageClick, imageFiles, maxLength } = this.props
return (
<View className='image-upload'>
{imageFiles.map((item, index) => (
<View
key={item + index}
className='image-upload__item'
>
<Image
className='image-upload__image'
src={item}
mode='aspectFill'
onClick={onImageClick.bind(this, item)}
></Image>
<Image
onClick={this.handleImageDelete.bind(this, index)}
className='image-upload__close'
src={closePng}
></Image>
</View>
))}
{imageFiles.length < maxLength && <View className='image-upload__item'>
<View
className='image-upload__plus'
onClick={this.handlePlusClick.bind(this)}
>
<Image
className='image-upload__plus--image'
src={plusPng}
></Image>
</View>
</View>}
</View>
)
}
}

组件基础样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* src/components/ImageUpload/index.scss */
.image-upload {
padding: 20px;
box-sizing: border-box;
display: flex;
&__item {
position: relative;
flex: 1;
max-width: 25%;
}
&__image {
width: 126px;
height: 126px;
display: block;
border-radius: 8px;
}
&__close {
position: absolute;
right: 18px;
top: -36px;
width: 36px;
height: 36px;
padding: 20px;
}
&__plus {
display: flex;
width: 126px;
height: 126px;
border: 1px solid #ccc;
border-radius: 8px;
box-sizing: border-box;
align-items: center;
justify-content: center;
&--image {
width: 63px;
height: 63px;
display: block;
}
}
}

页面内使用组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* src/pages/index/index.jsx */
// ...
constructor(props) {
super(props)
this.state = {
imageFiles: []
}
}

// 图片改变监听
handleChange(files) {
this.setState({
imageFiles: files
})
}
// 图片点击监听(这里是图片点击之后调起图片预览接口)
handleImageClick(currentUrl) {
Taro.previewImage({
current: currentUrl,
urls: this.state.imageFiles
})
}

render() {
const { imageFiles } = this.state
return (
<View className='index'>
<ImageUpload
imageFiles={imageFiles}
onChange={this.handleChange.bind(this)}
onImageClick={this.handleImageClick.bind(this)}
/>
</View>
)
}
// ...

附录

  1. process.env.BASE_URL 为api前缀,需要配置在配置文件中
  2. 本例为远程获取token然后携带token进行图片上传(上传七牛云的类型),如果已经有了token则不需要获取