背景
在小米的面試中,最后一輪被問到了一個場景。即關于在 WebView 下開發一個用戶上傳頭像的場景的完整流程。但是當時回答的好多細節都沒有回答上來。
原因
- 沒有使用過 WebView;
- 文件上傳的功能做的不多;
- 文件上傳的前后端實現都是用的插件,導致實現原理不清楚;
目標
- 理清使用 WebView 上傳頭像的業務邏輯;
- 理清技術細節;
- 實現 Demo;
分析
關于 WebView
什么是 WebView
Android WebView is a system for the Android operating system
(OS) that allows Android apps to display content from the web directly inside an application.
There are two ways to view web content on an Android device: though a traditional web browser or through an Android application that includes WebView in the layout. If a developer wants to add browser functionality to an application, she can include the WebView library and create an instance of a WebView class; this essentially embeds a browser within the app to do things like render web pages and execute JavaScript. WebView is powerful because it not only provides the app with an embedded browser, it also allows the developer's app to interact with web pages and other web apps.
簡而言之,對于 Android 來說,WebView 就是一個內置瀏覽器組件,開發者可以調用該組件來在 App 中顯示網頁。
業務邏輯
- 用戶打開個人資料頁面,先默認顯示灰色頭像,即頭像的
img
標簽的src
屬性為data:image/*;base64,**
; - 前端向后端請求個人資料,后端鑒權通過后,將個人資料返回給前端;
- 如果個人資料中頭像屬性為空,則對頭像不做任何操作;如果頭像屬性不為空,則將網頁中頭像的
img
標簽的src
屬性的值修改為響應的頭像屬性; - 用戶點擊頭像,彈出對話框,包含拍照和選擇相冊兩個選項;
- 用戶點擊選擇相冊以后,彈出相冊選擇界面;
- 只允許用戶選擇圖片類型的文件;
- (可選)用戶選擇好以后,彈出裁剪界面;
- 操作完成后,壓縮圖片,并通過 form data 上傳到后端服務器,前端顯示操作中提示;
- 后端接收到表單以后,計算圖片的 hash 值,在數據庫中查詢是否已經有存在相同頭像,如果已經有相同頭像,則在數據庫中復制原有圖片地址;如果沒有,則把文件保存在被 nginx 反向代理的本地文件夾后,再將地址和 hash 值保存在數據庫;
- 后端保存成功后,給前端返回一個狀態值為 200 的響應,并將頭像的完整地址作為響應的屬性;
- 前端接收到響應以后,將網頁中頭像的
img
標簽的src
屬性的值修改為響應的頭像屬性;
潛在問題
- WebView 開發。沒有接觸過;
- 文件命名規則。為了避免文件重名和惡意代碼注入,需要使用一種生成唯一值的規則,作為文件的命名規范,還不能太長。之前使用過時間戳作為規則;
- 服務器頭像文件夾結構。為了避免降低文件查找的性能,需要建立不同層級的文件夾,規則可以按照年月日建立不同的文件夾保存文件;
開發步驟
既然 WebView 也是瀏覽器,那么應該按照最小化問題的原則,一步一步實現最終的目標。因此,決定按照以下步驟實現功能。
- 搭建后端服務器,然后使用 Postman 之類的工具測試;
- 電腦瀏覽器環境下,實現前端功能目標;
- 環境切換為真正的 WebView 環境,實現功能目標;
開發
后端搭建
關鍵點
- [x] 后端支持提供靜態文件;
- [x] 接收 form data 類型表單,并保存成文件;
- [x] 按照規則重命名;
- [ ] 計算 hash 編碼;
效果圖
代碼
const path = require('path')
const express = require('express')
const formidable = require('formidable')
const cors = require('cors')
const app = express()
app.use(cors())
app.use('/static', express.static(path.join(__dirname, 'public')))
app.post('/api/v1/avatar', (req, res) => {
if (req.url === '/api/v1/avatar' && req.method.toLowerCase() === 'post') {
const form = new formidable.IncomingForm()
form.uploadDir = './public/avatars'
form.keepExtensions = true
form.parse(req, (err, fields, files) => {
if (err) {
console.error(err)
return res.status(500).json({
message: '服務器發生錯誤!'
})
}
const filename = path.basename(files['avatar']['path'])
return res.json({
success: true,
path: '/static/avatars/' + filename
})
})
}
})
app.listen(3000, () => console.log('Avatar back end service starts!'))
電腦瀏覽器環境
關鍵點
- [x] 文件上傳 HTML5 API;
- [x] 驗證文件類型和大小;
- [x] 隱藏掉原生
<input type="file">
元素,點擊頭像即可上傳文件;
效果圖
代碼
import { Component } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { Uploader } from './uploader.service';
@Component({
selector: 'app-root',
template: `
<div style="text-align:center">
<div style="margin:50px 0">
<label for="avatar"><img width="300" alt="avatar" class="img-thumbnail" [src]="avatarSrc"></label>
<input type="file" id="avatar" name="avatar" accept="image/*"
(change)="onChangeAvatar($event)" placeholder="更換頭像" style="visibility:hidden">
</div>
</div>
`
})
export class AppComponent {
private rootEndPoint: string = 'http://localhost:3000'
public avatarSrc = this.sanitizer.bypassSecurityTrustUrl('data:image/svg+xml;base64,\
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgM\
jUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMT\
IzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzM\
wMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiI\
C8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wx\
MS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==');
constructor (
private sanitizer: DomSanitizer,
private uploader: Uploader
) {}
private getFormValue (file: any): FormData {
const formData = new FormData();
formData.append('avatar', file);
return formData;
}
public onChangeAvatar (event: any) {
if (event.target.files.length === 0) {
return;
}
const file = event.target.files[0];
// 檢查文件格式和大小是否滿足要求
if (file.size > 1024 * 1024) {
window.alert('文件格式不規范!')
return;
}
this.uploader
.upload(this.getFormValue(file))
.subscribe(
path => this.avatarSrc = this.sanitizer.bypassSecurityTrustUrl(this.rootEndPoint + path),
errorMessage => console.log(errorMessage)
);
}
}
IOS WebView 環境
開發過程
- 使用 Xcode 建立一個簡單工程;
- 將 WKWebView 控件拖入;
- 使用代碼加載 url;
- 修改配置,允許 http 協議傳輸;
然后運行以后,一切正常,點擊頭像也能夠調用相冊。但是,噩夢就來了,無法正常選擇圖片并上傳,找到了很多解決辦法,但是因為對 swift 語言不熟悉,暫時沒有辦法測試。