上传头像

背景

在小米的面试中,最后一轮被问到了一个场景。即关于在 WebView 下开发一个用户上传头像的场景的完整流程。但是当时回答的好多细节都没有回答上来。

原因

  1. 没有使用过 WebView;
  2. 文件上传的功能做的不多;
  3. 文件上传的前后端实现都是用的插件,导致实现原理不清楚;

目标

  • 理清使用 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 中显示网页。

业务逻辑

  1. 用户打开个人资料页面,先默认显示灰色头像,即头像的 img 标签的 src 属性为 data:image/*;base64,**
  2. 前端向后端请求个人资料,后端鉴权通过后,将个人资料返回给前端;
  3. 如果个人资料中头像属性为空,则对头像不做任何操作;如果头像属性不为空,则将网页中头像的 img 标签的 src 属性的值修改为响应的头像属性;
  4. 用户点击头像,弹出对话框,包含拍照和选择相册两个选项;
  5. 用户点击选择相册以后,弹出相册选择界面;
  6. 只允许用户选择图片类型的文件;
  7. (可选)用户选择好以后,弹出裁剪界面;
  8. 操作完成后,压缩图片,并通过 form data 上传到后端服务器,前端显示操作中提示;
  9. 后端接收到表单以后,计算图片的 hash 值,在数据库中查询是否已经有存在相同头像,如果已经有相同头像,则在数据库中复制原有图片地址;如果没有,则把文件保存在被 nginx 反向代理的本地文件夹后,再将地址和 hash 值保存在数据库;
  10. 后端保存成功后,给前端返回一个状态值为 200 的响应,并将头像的完整地址作为响应的属性;
  11. 前端接收到响应以后,将网页中头像的 img 标签的 src 属性的值修改为响应的头像属性;

潜在问题

  • WebView 开发。没有接触过;
  • 文件命名规则。为了避免文件重名和恶意代码注入,需要使用一种生成唯一值的规则,作为文件的命名规范,还不能太长。之前使用过时间戳作为规则;
  • 服务器头像文件夹结构。为了避免降低文件查找的性能,需要建立不同层级的文件夹,规则可以按照年月日建立不同的文件夹保存文件;

开发步骤

既然 WebView 也是浏览器,那么应该按照最小化问题的原则,一步一步实现最终的目标。因此,决定按照以下步骤实现功能。

  1. 搭建后端服务器,然后使用 Postman 之类的工具测试;
  2. 电脑浏览器环境下,实现前端功能目标;
  3. 环境切换为真正的 WebView 环境,实现功能目标;

开发

后端搭建

关键点

  • [x] 后端支持提供静态文件;
  • [x] 接收 form data 类型表单,并保存成文件;
  • [x] 按照规则重命名;
  • [ ] 计算 hash 编码;

效果图

Postman 效果图

代码

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 环境

开发过程

  1. 使用 Xcode 建立一个简单工程;
  2. 将 WKWebView 控件拖入;
  3. 使用代码加载 url;
  4. 修改配置,允许 http 协议传输;

然后运行以后,一切正常,点击头像也能够调用相册。但是,噩梦就来了,无法正常选择图片并上传,找到了很多解决办法,但是因为对 swift 语言不熟悉,暂时没有办法测试。

IOS - WebView

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,472评论 25 707
  • 在Gooogle I/O 2013年的大会上面,展示的Volley库,已经成为android开发中最常用的处理和缓...
    优才学院阅读 8,121评论 2 9
  • 嗨!大家好!这是我第三次看到简书这个App了,起初还没有太在意,后面看到用的人越来越多,于是自己也下载一个,想知道...
    成为你自己onlyone阅读 253评论 0 1
  • ①飲み物 ジュース(アップルジュース、オレンジジュース、ピーチジュース、カルピス、コーラ)、コーヒー、ココア、お茶...
    一直都是干物女啊WWW阅读 559评论 4 1