toxic-johann / toxic-johann.github.io

my blog
6 stars 0 forks source link

【2016-04-23】由input标签引发的血案(一) #28

Open toxic-johann opened 7 years ago

toxic-johann commented 7 years ago

最近这两周压力比较大,手上这个项目遇到的坑还是挺多的。现在第一版的代码差不多完成,今晚算是空闲。因此先记录一下一些这些天遇到的bug。

个人认为multiple是一个很完美的属性。在元素<input type="file">,配上multipe和accept属性。就这样一个限定类型的多文件上传接口就出来了。

然而事情总是不是那么完美的。这次的主角是安卓。

惨无人道的全红

事实上下面还有段语句解释。

Not supported on Android 4.x and below, presumably an OS limitation. Only seems to work in Android 5.x for the Chrome browser.

就是说只有安卓5.0以上的版本才支持这个特性。我们知道安卓5.0出来已经有一段时间了。但是因为安卓特殊的厂商分发机制。到现在大部分机子还是处于4.x的程度。

其实这个特性在安卓上是曾经生效过的。但是因为某一个版本的问题,直接被废弃了。无论你如何选择都是只能提交一张。而web页面暂时没有办法对此作出反抗。除非你用hybrid之类的自由度比较大的框架。

因此这里我们降级采用了微信JSSDK的上传接口。

花开两朵,各表一枝。既然安卓和iOS分成了两个派别,那我们先谈一下iOS的处理。(真对不起,我手头没有winphone,而且两个系统已经搞死我了。windows phone用户不要怪我。)

一般来说,我们会采用change事件来监听网页里的input值选择,在type="file"的情境下我们也照样处理。这里的逻辑是这样的。

input取文件模拟流程

手机取出一个文件还是挺快的。但是,如果你选了很多个文件。这就很难说了。在性能比较差的手机或者系统里。他会把所有照片的路径和相关资料都打包好,再提交给页面。这就是图中红框所指的时间差。这个时间差甚至会长达十秒以上。

这个时候,如果手机性能不好的话。页面会表现为卡死。如果手机性能尚不至于十分差劲,页面会可以交互。

而红框这个时候,如果我们用事件交代,就是click事件后、change事件前。

这是我们遇到的第一个问题,我们“不知道”用户已经选择完毕了,就被推上了前台。

那既然我们什么都不知道,那就提前做好准备吧。在用户click的时候就作出我们已经在处理的现象,安抚一下用户吧。

因此我决定在click事件上就加上相关的页面交互表示。当我们web端被推向用户的时候,不至于手足无措。但是这就引来了下面这个问题。

万一用户按了取消事件呢?

这就是另一个坑了。一般在电脑上,有两种处理情况。

  1. 当用户cancel后,触发change,上传的file为0.
  2. 不触发change.

现在假设你在电脑上。

  1. file为0,我们好理解。在change事件上面绑上了0的排除项就可以了。
  2. 不通知的话,我们只需要把所有交互都放在change上面就好。那么你没选,就没触发change了。

但是,现在我们在手机端,而且根据上文,我们为了客户体验。在click上作了处理。

  1. 手机通知我file为0,谢天谢地,你还会通知我。那我把刚刚的提醒赶紧取消掉就好。
  2. 手机不通知我,坑爹了……

这就让我在click后处于一种十分尴尬的状态了。

究竟用户是在选择呢?取消选择了呢?还是选择完了手机在处理用户盯着我干瞪眼呢?

这个问题,我暂时想不到比较好的办法。我个人建把议决定权交还给用户。我们给可以让用户关掉的一个提示框。 待议。

iOS上的上传基本就是用form-data和filereader进行处理就好了。这里问题不大。

下面我们说android。

微信JSsdk中,chooseImage的最多数目是九个。如果你设定大于9的限制。在android里会显示依旧只能选9个,但是在iPhone里,你的确可以选很多个,不过该接口会返回一个错误。

令人庆幸的是,微信这个接口success、fail、cacel的接口一应俱全。让人十分满意。

localId是微信给的一个地址,应该是一个代理让你找寻本地文件。但是除此之外,就找不到其他资料了。

这个localId只能用于img展示,将其绑定到<img>的src属性上,就可以显示该图片。

但是这个localId无法用于filereader进行处理。

在stackoverflow上面找到这个处理方法。用于将img转化为dataurl。

一个是用xhr进行文件下载处理。

function toDataUrl(url, callback){
    var xhr = new XMLHttpRequest();
    xhr.responseType = 'blob';
    xhr.onload = function() {
      var reader  = new FileReader();
      reader.onloadend = function () {
          callback(reader.result);
      }
      reader.readAsDataURL(xhr.response);
    };
    xhr.open('GET', url);
    xhr.send();
}

但是微信的localId不支持这种ajax请求。

另一个是用canvas渲染后再进行提取。

function toDataUrl(url, callback, outputFormat){
    var img = new Image();
    img.crossOrigin = 'Anonymous';
    img.onload = function(){
        var canvas = document.createElement('CANVAS');
        var ctx = canvas.getContext('2d');
        var dataURL;
        canvas.height = this.height;
        canvas.width = this.width;
        ctx.drawImage(this, 0, 0);
        dataURL = canvas.toDataURL(outputFormat);
        callback(dataURL);
        canvas = null; 
    };
    img.src = url;
}

这里注意下,必须要设置跨域。不然你无法调用canvas的toDataURL方法。

不过我试了下,生成的图片都是空图片,应该还是有些问题。

鉴于时间愿意,我也放弃了,选择了一种我较为熟悉的做法。。

前端把图片上传至微信服务器,获取微信服务器mediaId,再在我服务器利用该id进行拉取。这个以后我再谈谈具体做法。

uploadImage不支持多张上传,而且必须要传完一张,再传下一张。如果你同时提请求,或者在他没有处理好的情况下,提出请求。他会不报错,不报warning直接忽略掉。

因此,这个必须要设置递归上传了。因此,我写了个异步递归方法。

recursion:function(...args){
    let me = this;
    return new Promise((resolve,reject)=>{
        let arr = args[0];
        let callback = args[1];
        let other = args.slice(2);
        // 判断处理队列是否还有需要处理的数据。
        if(Array.isArray(arr) && arr.length > 0){
            let tmp = arr.shift();
            let runArgs = [tmp].concat(other);
            let newArgs = [arr,callback].concat(other)
            // 执行需要我执行的函数,无论是否成功,执行结束后调用下一个函数进行执行。
            callback.apply(this,runArgs).then(()=>{
                me.recursion.apply(me,newArgs).then(()=>{
                    resolve();
                })
            },(...args)=>{
                console.log("recursion got reject",args);
                me.recursion.apply(me,newArgs).then(()=>{
                    resolve();
                })
            }).catch((...args)=>{
                console.log("recursion catach error",args);
                me.recursion.apply(me,newArgs).then(()=>{
                    resolve();
                })
            })
        } else {
            resolve()
        }
    });
}

使用起来也十分简单。

recursion(localIds,(each,self)=>{
    return self.uploadByWx(each,self)
},self).then(success=>{
    self.$emit("finished");
});

传入你需要的参数和调用的函数。然后监听结果就好。

那么这样子我们就基本完成了主需求了。下面谈一些其他方面的小问题。

现在十分流行下拉翻页等需求。因此,对于scorll效果,我们用的越来越多。鉴于本次项目需要的滚动不算特别复杂。因此我就直接自己编写了一个vue的指令。

;(function() {
    var vueScroll = {};
    vueScroll.install = function(Vue) {
        Vue.directive('scroll', {
            isFn : true,
            acceptStatement : true,
            bind : function() {
                 //bind callback
            },
            update : function(fn) {
                var self = this;

                if(typeof fn !== 'function') {
                    return console.error('The param of directive "v-scroll" must be a function!');
                }

                let positon = $(window).scrollTop();
                let direction = "";
                let delta = 20;

                $(window).scroll(function(...args){
                    let now = $(window).scrollTop();
                    if(now == ($(document).height() - $(window).height())){
                        args.push("bottom")
                        args.push($(self.el));
                        fn.apply(self,args);
                    }
                    if(direction != "down" && now-positon > delta){
                        direction = "down";
                        positon = now;
                        args.push("down")
                        args.push($(self.el));
                        fn.apply(self,args);
                    } else if(direction != "up" && positon-now > delta){
                        direction = "up";
                        positon = now;
                        args.push("up")
                        args.push($(self.el));
                        fn.apply(self,args);
                    } else if(Math.abs(now-positon)>delta){
                        positon = now;
                    }
                })
            },
            unbind : function() {},
        });
    };

    if (typeof exports == "object") {
        module.exports = vueScroll;
    } else if (typeof define == "function" && define.amd) {
        define([], function(){ return vueScroll })
    } else if (window.Vue) {
        window.vueScroll = vueScroll;
        Vue.use(vueScroll);
    }
})();

监听滚动到底部的方式还是比较简单,做一个位置的提醒就好了。

至于判断滑动方向,我想不是方向改变了的话,你当然不希望收到这么多通知啦。因此我这里做了个标志的缓存。如果方向边,只在第一次移动的时候进行提醒。

但是这里苹果做了个优化,直到最后滚动彻底停下来,苹果的浏览器才出发window的scroll函数。这里需要注意。因此你可以默认滚动到底部就等于往下翻等。这里自己需要稍加注意。

移动端五花八门,还是很多细节会有不一样。特别是腾讯自己开发的X5浏览器,很多行为都十分怪异,惨不忍睹。这些找时间总结一下。

另外这次还尝试了一下使用vuejs和众多微信的接口。这些找时间再谈。