業務場景
微信端專案是基於Vux + Axios構建的,關於圖片上傳的業務場景有以下幾點需求:
1、單張圖片上傳(如個人頭像,實名認證等業務)
2、多張圖片上傳(如某類工單記錄)
3、上傳圖片時期望能按指定尺寸壓縮處理
4、上傳圖片可以從相簿中選擇或者直接拍照
遇到的坑
採用微信JSSDK上傳圖片
在之前開發的專案中(mui + jquery),有使用過微信JSSDK的介面上傳圖片,本想應該能快速遷移至此專案。事實證明程式設計沒有簡單的事:
1、按指定尺寸壓縮圖片
JSSDK提供的介面wx.chooseImage 是不能指定圖片壓縮尺寸的,只能在後端的介面通過localId獲取圖片時,再轉換成指定的尺寸。
2、微信JSSDK的介面許可權驗證
只要是單頁面應用專案,微信JSSDK注入許可權驗證都會有這個坑,而這個與路由模式(hash 或 history)也有關聯。有關此坑, 後續會再次寫文總結。參考解決方案[微信JSSDK] 解決SDK注入許可權驗證 安卓正常,IOS出現config fail
經過權衡考慮網頁可能需要在微信以外的瀏覽器上也能上傳檔案,顧後來放棄了採用微信JSSDK介面上傳圖片的方式。
android版微信,input onchange事件不觸發
這個坑,圈內有很多人踩過了。在PC端測試是正常的,釋出之後,微信端上傳時能選擇檔案,但之後沒有任何效果。日誌跟蹤,後臺的api都未呼叫,由此判斷是input的onchange事件未被觸發。
解決方案, 更改input的 accept屬性:
<input ref="file" type="file" accept="image/jpeg,image/png" @change="selectImgs" />
將以上程式碼更改為:
<input ref="file" type="file" accept="image/*" @change="selectImgs" />
如果不允許從相簿中選擇,只能拍照,增加capture="camera":
<input ref="file" type="file" accept="image/*" capture="camera" @change="selectImgs" />
(注:如果場景支援從相簿選擇或拍照,測試發現某些機型拍照後返回到了主頁。哈哈,也有可能是其他因素引起的問題,未做深究了)
使用Lrz.js壓縮圖片
目前手機拍照的圖片檔案大小一般在3-4M,如果在上傳時不做壓縮處理會相當浪費流量並且佔用伺服器的儲存空間(期望上傳原圖的另做討論)。如果能夠在前端壓縮處理,那肯定是最理想的方案。而lrz.js則提供了前端圖片檔案的壓縮方案,並且可以指定尺寸壓縮。實測:3M左右的圖片檔案,按寬度450px尺寸壓縮上傳後的檔案大小在500kb左右,上傳時間2s以內。
其核心原始碼,如下:
selectImgs () {
let file = this.$refs.file.files[0]
lrz(file, { width: 450, fieldName: 'file' }).then((rst) => {
var xhr = new XMLHttpRequest()
xhr.open('POST', 'http://xxx.com/upload')
xhr.onload = () => {
if (xhr.status === 200 || xhr.status === 304) {
// 無論後端丟擲何種錯誤,都會走這裡
try {
// 如果後端跑異常,則能解析成功, 否則解析不成功
let resp = JSON.parse(xhr.responseText)
console.log('response: ', resp)
} catch (e) {
this.imageUrl = xhr.responseText
}
}
}
// 新增引數
rst.formData.append('folder', 'wxAvatar') // 儲存的資料夾
rst.formData.append('base64', rst.base64)
// 觸發上傳
xhr.send(rst.formData)
return rst
})
}
單個圖片上傳元件完整程式碼,如下(注: icon圖示使用的是svg-icon元件):
<template>
<div class="imgUploader">
<section v-if="imageUrl"
class="file-item ">
<img :src="imageUrl"
alt="">
<span class="file-remove"
@click="remove()">+</span>
</section>
<section v-else
class="file-item">
<div class="add">
<svg-icon v-if="!text"
class="icon"
icon-class="plus" />
<span v-if="text"
class="text">{{text}}</span>
<input type="file"
accept="image/*"
@change="selectImgs"
ref="file">
</div>
</section>
</div>
</template>
<script>
import lrz from 'lrz'
export default {
props: {
text: String,
// 壓縮尺寸,預設寬度為450px
size: {
type: Number,
default: 450
}
},
data () {
return {
img: {
name: '',
src: ''
},
uploadUrl: 'http://ff-ff.xxx.cn/UploaderV2/Base64FileUpload',
imageUrl: ''
}
},
watch: {
imageUrl (val, oldVal) {
this.$emit('input', val)
},
value (val) {
this.imageUrl = val
}
},
mounted () {
this.imageUrl = this.value
},
methods: {
// 選擇圖片
selectImgs () {
let file = this.$refs.file.files[0]
lrz(file, { width: this.size, fieldName: 'file' }).then((rst) => {
var xhr = new XMLHttpRequest()
xhr.open('POST', this.uploadUrl)
xhr.onload = () => {
if (xhr.status === 200 || xhr.status === 304) {
// 無論後端丟擲何種錯誤,都會走這裡
try {
// 如果後端跑異常,則能解析成功, 否則解析不成功
let resp = JSON.parse(xhr.responseText)
console.log('response: ', resp)
} catch (e) {
this.imageUrl = xhr.responseText
}
}
}
// 新增引數
rst.formData.append('folder', this.folder) // 儲存的資料夾
rst.formData.append('base64', rst.base64)
// 觸發上傳
xhr.send(rst.formData)
return rst
})
},
// 移除圖片
remove () {
this.imageUrl = ''
}
}
}
</script>
<style lang="less" scoped>
.imgUploader {
margin-top: 0.5rem;
.file-item {
float: left;
position: relative;
width: 100px;
text-align: center;
left: 2rem;
img {
width: 100px;
height: 100px;
border: 1px solid #ececec;
}
.file-remove {
position: absolute;
right: 0px;
top: 4px;
width: 14px;
height: 14px;
color: white;
cursor: pointer;
line-height: 12px;
border-radius: 100%;
transform: rotate(45deg);
background: rgba(0, 0, 0, 0.5);
}
&:hover .file-remove {
display: inline;
}
.file-name {
margin: 0;
height: 40px;
word-break: break-all;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.add {
width: 100px;
height: 100px;
float: left;
text-align: center;
line-height: 100px;
font-size: 30px;
cursor: pointer;
border: 1px dashed #40c2da;
color: #40c2da;
position: relative;
background: #ffffff;
.icon {
font-size: 1.4rem;
color: #7dd2d9;
vertical-align: -0.25rem;
}
.text {
font-size: 1.2rem;
color: #7dd2d9;
vertical-align: 0.25rem;
}
}
}
input[type="file"] {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: 1px solid #000;
opacity: 0;
}
</style>
後端圖片儲存處理
後端api對圖片的處理,是必不可少的環節,需要將前端提交過來的base64字串轉換成圖片格式,並存放至指定的資料夾,介面返回圖片的Url路徑。各專案後端對圖片的處理邏輯都不一致,以下方案僅供參考(我們使用asp.net MVC 構建了獨立的檔案儲存站點)。
其核心原始碼,如下:
/// <summary>
/// 圖片檔案base64上傳
/// </summary>
/// <param name="folder">對應資料夾位置</param>
/// <param name="base64">圖片檔案base64字串</param>
/// <returns></returns>
public ActionResult Base64FileUpload(string folder, string base64)
{
var context = System.Web.HttpContext.Current;
context.Response.ClearContent();
// 因為前端呼叫時,需要做跨域處理
context.Response.AddHeader("Access-Control-Allow-Origin", "*");
context.Response.AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
context.Response.AddHeader("Access-Control-Allow-Headers", "content-type");
context.Response.AddHeader("Access-Control-Max-Age", "30");
if (context.Request.HttpMethod.Equals("OPTIONS"))
{
return Content("");
}
var resultStr = base64.Substring(base64.IndexOf(",") + 1);//需要去掉頭部資訊,這很重要
byte[] bytes = Convert.FromBase64String(resultStr);
var fileName = Guid.NewGuid().ToString() + ".png";
if (folder.IsEmpty()) folder = "folder";
//本地上傳
string root = string.Format("/Resource/{0}/", folder);
string virtualPath = root + fileName;
string path = Server.MapPath("~" + virtualPath);
//建立資料夾
if (!Directory.Exists(Path.GetDirectoryName(path)))
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
}
System.IO.MemoryStream ms = new System.IO.MemoryStream(bytes);//轉換成無法調整大小的MemoryStream物件
System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(ms);
bitmap.Save(path, System.Drawing.Imaging.ImageFormat.Png);//儲存到伺服器路徑
ms.Close();//關閉當前流,並釋放所有與之關聯的資源
return Content(Net.Url + virtualPath); //返回檔案路徑
}
結語
由於專案實際情況,上述的方案中還存在諸多未完善的點:
1、多張圖片上傳,還是採用的與單張圖片相同的介面處理, 更為完善的方案是,前端的多圖上傳元件只繫結一個關聯Id,即可通過實現上傳和將圖片列表查詢展示(注:該功能在微信端未實現)。
2、後端圖片上傳的介面,未做嚴格的安全校驗,更為完善的方案是,每個上傳的場景,都應該限制檔案型別,限制檔案大小,以及檔案資料來源校驗(注: 如軟體需要按二級等保標準測評,則後端介面會檢測通不過)。
3、上傳元件,未顯示上傳進度,體驗性稍差。
正如前文所述,出於專案實際情況考慮,只是簡單實現圖片壓縮上傳功能,如要支援更多的場景,還得細細雕琢。