案例

注册类小功能,大家都非常熟悉,使用者填写手机号,密码,上传头像后,可以成为系统用户,那么问题来了,头像文件是和用户信息如手机号,密码一起提交还是单独提交?如果同时提交,那么应用内也存在文件上传服务,每个业务都有需要处理上传,业务耦合性大,有什么好的方案解决这个问题?这就是本章节需要解决的问题:如何处理附件存储问题。

本章节的主要内容是关于附件存储服务的设计

一、 附件服务设计的一般思路

附件存储一般有俩种思路,一是应用业务内直接上传,该方案简单直接,适用于上传附件业务场景比较少的小微应用。还有一种是建立专门的附件上传服务或者附件上传服务器,该方式可以极大地降低应用耦合度,值得认真研究。

二、 常用后端附件处理方式

后端处理方式包括如下几个方面,使用应用服务器上传附件,附件服务器专门上传附件,以及第三方云存储上传附件

2.1  应用服务器存储附件

应用服务器上传图片主要适用于中小微型应用,比如该案列中的应用,前端主要代码如下

<form class="mui-input-group" method="post" action="/attach/direct" enctype="multipart/form-data">
        <div class="mui-input-row" style="height:120px;">
                <input type="file" name="uploadfile" class="file"  >
                <label>上传头像</label>
                <div class="upload-wrap">
                <img src="/assets/image/logo.png">
                
                </div>
            </div>
        <div class="mui-input-row">
            <label>用户名</label>
        <input type="text" name="name" class="mui-input-clear" placeholder="请输入用户名">
        </div>
        <div class="mui-input-row">
            <label>密码</label>
            <input type="password" name="passwd" class="mui-input-password" placeholder="请输入密码">
        </div>
        <div class="mui-button-row">
            <button type="submit" class="mui-btn mui-btn-primary" >确认</button>
            <button type="button" class="mui-btn mui-btn-danger" >取消</button>
        </div>
    </form>


后端代码
//参数
func (ctrl *AttachCtrl) direct(ctx *gin.Context) {

   name := ctx.PostForm("name")
   passwd := ctx.PostForm("passwd")

   timestamp := strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
   filename := "/upload/" + timestamp + ".jpg"
   file, _, err := ctx.Request.FormFile("uploadfile")
   out, err := os.Create("." + filename)
   if err != nil {
      log.Fatal(err)

      ctx.HTML(http.StatusOK, "tpl/result.html", gin.H{"status": 200, "data": err, "msg": "文件存储出错"})
      return
   }
   defer out.Close()
   // Copy数据
   _, err = io.Copy(out, file)
   if err != nil {
      log.Fatal(err)
   }
   utils.Debug(err)
   user := new(models.User)
   userId, err := user.RegisterWithAvatar(name, passwd, filename)
   if err ==nil{
      ctx.HTML(http.StatusOK, "tpl/result.html", gin.H{"status": 200, "data": userId, "msg": err})
   }else{
      ctx.HTML(http.StatusOK, "tpl/result.html", gin.H{"status": 200, "data": userId, "msg": "添加成功"})
   }

}

这里注意,本后端代码使用go语言编译,具体逻辑还是比较清晰地

//获取用户名称

name := ctx.PostForm(“name”)
passwd := ctx.PostForm(“passwd”)//获取密码
file, _, err := ctx.Request.FormFile(“uploadfile”)//获取上传的文件的内容

out, err := os.Create(“.” + filename)//新建一个文件

_, err = io.Copy(out, file)//将上传的文件内容copy到目标文件

如上所有逻辑,应用逻辑与图片处理混合在一起,不利于模块化

一般地,我们将上传逻辑和应用逻辑分开,如下所示前端代码

<div class="mui-content">



            <div class="mui-input-row" style="height:120px;">
                    <input type="file" name="uploadfile" class="file" onchange="upload($(this))"  >
                    <label>上传头像</label>
                    <div class="upload-wrap">
                    <img src="/assets/image/logo.png" id="avatar" />
                    
                    </div>
                </div>
            <div class="mui-input-row">
                <label>用户名</label>
            <input type="text" id="mobile" class="mui-input-clear" placeholder="请输入用户名">
            </div>
            <div class="mui-input-row">
                <label>密码</label>
                <input type="password" id="passwd" class="mui-input-password" placeholder="请输入密码">
            </div>
            <div class="mui-button-row">
                <button type="submit" id="submit" class="mui-btn mui-btn-primary" >确认</button>
                <button type="button" class="mui-btn mui-btn-danger" >取消</button>
            </div>


</div>

处理该逻辑分为俩个接口,一个为上传接口,返回文件路径,另一个为注册接口,提交的信息为文件的路径信息以及用户名密码等
  var data = {}
    $(function(){
        $("#submit").click(function(){
            data.mobile = $("#mobile").val()
            data.passwd = $("#passwd").val()
            $.post("/user/register",data).then(function(resp){
                alert(resp.msg)
              })
        })
    })
function upload(o){
    compressanduploadonefile(o,{}).then(function (resp) {
        $("#avatar").attr("src",resp.data)
        data.avatar = resp.data;
    })
}

function compressanduploadonefile(filedom,data){

    // 压缩图片需要的一些元素和对象
    var reader = new FileReader(),
            img = new Image();
    var that = this;
    if(typeof pickfile=="undefined"){
        function pickfile(f){

        }
    }

    // 选择的文件对象
    var file = filedom[0].files[0];;
    var deferred = $.Deferred();
    if(file.type.indexOf("image")==-1){
        deferred.resolve({"status":400,"msg":"当前文件类型暂不支持"})

        return deferred;
    }
    console.log(file)




    // 缩放图片需要的canvas
    var canvas = document.createElement('canvas');
    var context = canvas.getContext('2d');

    // base64地址图片加载完毕后
    img.onload = function () {
        // 图片原始尺寸
        var originWidth = this.width;
        var originHeight = this.height;


        // 最大尺寸限制
        var maxWidth = filedom.data("maxwidth")||400, maxHeight = filedom.data("maxheight")||400;
        // 目标尺寸
        var targetWidth = originWidth, targetHeight = originHeight;
        // 图片尺寸超过400x400的限制
        if (originWidth > maxWidth || originHeight > maxHeight) {
            if (originWidth / originHeight > maxWidth / maxHeight) {
                // 更宽,按照宽度限定尺寸
                targetWidth = maxWidth;
                targetHeight = Math.round(maxWidth * (originHeight / originWidth));
            } else {
                targetHeight = maxHeight;
                targetWidth = Math.round(maxHeight * (originWidth / originHeight));
            }
        }

        // canvas对图片进行缩放
        canvas.width = targetWidth;
        canvas.height = targetHeight;
        // 清除画布
        context.clearRect(0, 0, targetWidth, targetHeight);
        // 图片压缩
        context.drawImage(img, 0, 0, targetWidth, targetHeight);
        // canvas转为blob并上传
        var dataURL = canvas.toDataURL("image/jpeg");

        $.ajax({
            url:"/attach/uploadbase64",
            method:"post",
            data:{"base64data":dataURL}
        }).then(function(resp){
            deferred.resolve( resp)
        })

    };

    // 文件base64化,以便获知图片原始尺寸
    reader.onload = function(e) {
        img.src = e.target.result;
    };
    if (file.type.indexOf("image") == 0) {
        reader.readAsDataURL(file);
    }else{
        deferred.resolve({"status":400,"msg":"当前文件类型暂不支持"})
    }
    return deferred;

}

后端处理上传逻辑接口为
func (ctrl *AttachCtrl) uploadbase64(ctx *gin.Context) {
   base64data := ctx.PostForm("base64data")
   if strings.Contains(base64data, "base64,") {
      datastrarr := strings.Split(base64data, "base64,")
      base64data = datastrarr[1]
   }

   ddd, error := base64.StdEncoding.DecodeString(base64data) //成图片文件并把文件写入到buffer
   if error != nil {
      ctx.JSON(http.StatusOK, gin.H{"status": 400, "data": error, "msg": ""})
      return
   }
   timestamp := strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
   filename := "/upload/" + timestamp + ".jpg"
   err2 := ioutil.WriteFile("."+filename, ddd, 0666) //buffer输出到jpg文件中(不做处理,直接写到文件)
   if err2 == nil {
      ctx.JSON(http.StatusOK, gin.H{"status": 200, "data": filename, "msg": ""})
   } else {
      ctx.JSON(http.StatusOK, gin.H{"status": 400, "data": err2, "msg": "文件存储出错"})
   }
}

当然,如果我们不需要上传base64文件,我们也可以使用如下接口
func (ctrl *AttachCtrl) uploadfile(ctx *gin.Context) {

   timestamp := strconv.FormatInt(time.Now().UTC().UnixNano(), 10)

   suffix := ctx.PostForm("suffix")
   if suffix=="" {
      suffix = ".jpg"
   }
   file,_,err := ctx.Request.FormFile("uploadFile")

   filename := "/upload/" + timestamp + suffix
   out, err := os.Create("."+filename)
   if err != nil {

      ctx.JSON(http.StatusOK, gin.H{"status": 400, "data": err, "msg": "文件存储出错"})
      return
   }
   _,err = io.Copy(out,file)
   defer out.Close()

   // Copy数据
// _, err = io.Copy(out, file)
   if err != nil {
      log.Fatal(err)
   }
   ctx.JSON(http.StatusOK, gin.H{"status": 200, "data": filename, "msg": "上传成功了"})
}

2.2 自建图片服务器

主要有俩个方面,一方面是用户上传的流量,1张3M的和一张300kb的,显而易见,前者消耗的流量大得多。另一方面是降低用户浏览图片所耗费的流量。

自建图片服务器主要难点在于设计文件存储路径,这个我在 千万级流量H5应用涉及到的技能点-图片篇 4、如何设计图片文件存储的名称和路径策略 http://www.imwinlion.com/archives/28 有专门论述,在此不做详细阐述

2.3 使用阿里云存储

使用阿里云存储分为前端直传和后端上传,后端上传本文不做专门阐述,本文主要阐述前端直传,不适用官方demo,使用jquery,容易上手

html代码如下

 

<div class="mui-content">

        <ol>
            <li>基于jquery封装 </li>
            <li>可以运行在PC浏览器,手机浏览器,微信</li>

            <li>最关键的是,让你10分钟之内就能移植到你的系统,实现以上牛逼的功能!</li>
            <li>注意:此方法是直接在前端签名,有accessid/accesskey泄漏的风险</li>
            <li>注意一点,bucket必须设置了Cors(Post打勾),不然没有办法上传</li>
            <li>注意一点,把upload.js 里面的host/accessid/accesskey改成您上传所需要的信息即可</li>
        </ol>

            <div class="mui-input-row" style="height:120px;">
                    <input type="file" id="uploadfile" name="uploadfile" class="file"   >

                </div>

            <div class="mui-button-row">

                <button type="button" onclick="upload()" class="mui-btn mui-btn-danger" >上传</button>
            </div>


</div>

js代码如下
var policyText = {
    "expiration": "2020-01-01T12:00:00.000Z", //设置该Policy的失效时间,超过这个失效时间之后,就没有办法通过这个policy上传文件了
    "conditions": [
        ["content-length-range", 0, 1048576000] // 设置上传文件的大小限制
    ]
};

accessid= '6MKOqxGiGU4AUk44';
accesskey= 'ufu7nS8kS59awNihtjSonMETLI0KLy';
host = 'http://post-test.oss-cn-hangzhou.aliyuncs.com';
function upload(){
var file = document.getElementById("uploadfile").files[0]
var suffix = file.name.substr(file.name.lastIndexOf("."))
var policyBase64 = Base64.encode(JSON.stringify(policyText))
message = policyBase64
var bytes = Crypto.HMAC(Crypto.SHA1, message, accesskey, { asBytes: true }) ;
var signature = Crypto.util.bytesToBase64(bytes);
var multipart_params ={
    'Filename': '${filename}',//下载时候的文件名
    'key' : 'tmp/'+new Date().getTime()+suffix,//如果想源文件保存介意设置为${filename}
    'policy': policyBase64,
    'OSSAccessKeyId': accessid,
    'success_action_status' : '200', //让服务端返回200,不然,默认会返回204
    'signature': signature,
}


    var formData = new FormData()
    for(var i in multipart_params){
        formData.append(i,multipart_params[i])
    }
    formData.append("file",file)


    $.ajax({
                type: "POST",
                url: host,
                contentType: false, // 注意这里应设为false
                processData: false,
                data: formData  ,
                xhrFields:{'Access-Control-Allow-Origin':'*'}

            }
    ).done(
            function(data, textStatus, jqXHR){
                if(jqXHR.status==200){
                    alert("文件上传成功,网址为"+host +"/"+ multipart_params.key)
                    console.log("文件上传成功,网址为"+host +"/"+ multipart_params.key)
                }
            }
    )

}

三、效果展示

四、如何获取代码

加我好友jiepool-winlion,给我发微信红包

发表评论

电子邮件地址不会被公开。 必填项已用*标注