29

clipboard.png

17-3-2更新: 谢谢@mengdu 补充的关于图片预览的另一种更简单方法 URL.createObjectURL(),具体在文章里补充

之前用Vue做了一个基础的组件vue-img-inputer,下面就叫vii,记录下在开发过程中遇到的知识点(都算比较基础,具体代码不会贴太多,都可以在项目仓库里看到)。

上传文件很多项目都要用到,一些组件库里(ele/iview...)文件上传组件都是做成了标配,虽然viiuploader定位还是有些差别,但实现上都有几个共同要点:

  1. 样子要好看点

  2. 图片/文件选择后预览

  3. 实现拖拽选择文件

  4. 图片选择后执行某些动作(譬如uploader的上传等)

先上demo

注: 下面有些地方会有些啰嗦,请选择观看

基础

首先我们有个文件选择框,恩,长这样:

clipboard.png

好丑啊!!我们来让它变好看点:

第一个方法:修改自身CSS

这里有一个shadowDOM的概念,简单的来说就是我们经常用到的一些HTML标准组件(例如viedo,甚至滚动条)其实是由若干个更基础的DOM由浏览器封装成的,使得我们调用只要一个标签就够了,这类也就是WebComponent,这里具体不展开了。我们先来看下file-input的内部是如何的(chrome devtool不设置是看不到的):

clipboard.png

所以呢,这个隐藏在革命碉堡里的button就是我们直接修改file-input样式但是却去不掉丑按钮的元凶!!

解决思路的:我们要么把按钮移出视线,要么就用这个按钮修改其样式。

这里就修改下里面这个type=button的样式,只提供个思路,代码:

<input type="file"/>
<style>
    input {
        font-size: 0; /* 为了去掉‘未选择任何文件’这几个字,也可以随便弄到哪里*/
    }
    /* 注意不是直接input > input[type=button] 哦*/
    input::-webkit-file-upload-button {
        background: #efeeee;
        color: #333;
        border: 0;
        padding: 40px 100px;
        border-radius: 5px;
        font-size: 12px;
        box-shadow: 1px 1px 5px rgba(0,0,0,.1), 0 0 10px rgba(0,0,0,.12);
    }
</style>

有没有想到chrome修改滚动条样式呢?哈哈,其实是一个道理,现在file-input变这样了:

clipboard.png

好像挺简单!然而我们看到-webkit-就应该知道兼容性了,这种方法只有safari和chrome笑笑,其他GG,所以自然不能这么干。

第二个方法:给file-input找个替身

是这样,我们可以可以把file-input整个移出视线,再找个找几个元素,通过点击这些个元素来代理原file-input的点击,呼出文件选择框呢?

自然是可以的,label标注标签, 给label一个for属性指向input的唯一id,这样点击label就相当于点击input, 所以我们可以这么写:

<div class="box">
  <input id="id" type="file" />
  <label for="id"></label>
  <!-- other element-->
</div>
.box {
    position: relative;
}
input {
    position: absolute;
    left: -9999px;
}    
/* 使label充满整个box*/
label {
    position: absolute;
    top: 0;left: 0;right: 0;bottom: 0;
    z-index: 10; /* 这个z-index之后说到*/
}

这样子做之后,就有一个组件的影子了,其中

  • 因为label充满了整个box,所以点击box就可以选择文件

  • 同时有了box,可以往里面填充任何元素,譬如一个icon

<div class="box">
  <input id="id" type="file" />
  <label for="id"></label>
  <i class="iconfont">:)</i>
  <!--  ...发挥你的想象力-->
</div>

好了,基础基本上啰嗦完了,正式进入vue的实现(Vue 2.x):

文件选择的处理

这块讲文件数据的获取和处理:

v-model

如果问你vue里你想要组件绑定一个输入值的最粗暴的方式是什么?v-model啊!但是这条指令其实是一个语法糖:

<imgInputer v-model="target"></imgInputer>
<!-- 默认等同于下面几行-->
<imgInputer ref="x" :value="target"></imgInputer>  
<script>
    ...
    // 默认给这个组件对象绑定input事件!
    this.$refs.x.$on('input', value => {this.target = value})
    ...
</script>

所以文件选择传值的实现方式:

<template>
    <div>
      <input @change="handleFileChange" ref="inputer" .../>
      ...
    </div>
</template>
<script>
    ...
    props: {
        value: {
            // 绑定默认的value prop
            default: undefined
        },
    },
    ...
    // input的change回调第一个参数是event对象
    methods: {
        handleFileChange (e) {
            let inputDOM = this.$refs.inputer;
            // 通过DOM取文件数据
            this.file    = inputDOM.files[0];
            this.errText = '';
    
            let size = Math.floor(this.file.size / 1024);
            if (size > ...) {
                // 这里可以加个文件大小控制
                return false
            }
    
            // 触发这个组件对象的input事件
            this.$emit('input', this.file);
            
            // 这里就可以获取到文件的名字了
            this.fileName = this.file.name;
            
            // 这里加个回调也是可以的
            this.onChange && this.onChange(this.file, inputDOM.value);
      
        },
    }
    ...
</script>
<!-- 调用-->
<imgInputer v-model="target"></imgInputer>

这样选中的文件就会传给target了,接着说图片预览

图片预览

方法有三种:

  1. 选择文件后直上传然后得到网络url

  2. 用HTML5的File APIFileReader图片本地转成base64格式的url

  3. URL.createObjectURL(file)对象方法创建临时路径

然后将url路径赋值给一个img标签

我们这里肯定不会选第一种,所以只说2和3:

FileReader

照例贴MDN文档先,然后是代码:

<template>
    <div ref="box">
      ...
      <input ... />
      // 给个img来承担预览工作就行了
      <img :src="dataUrl" />
      ...
    </div>
</template>
<sctipt>
    data () {
        return {
            // 转base64码后的data字段
            dataUrl: ''
        }
    },
    methods: {,
        imgPreview (file) {
            let self = this;
            // 看支持不支持FileReader
            if (!file || !window.FileReader) return;
    
            if (/^image/.test(file.type)) {
                // 创建一个reader
                let reader = new FileReader();
                // 将图片将转成 base64 格式
                reader.readAsDataURL(file);
                // 读取成功后的回调
                reader.onloadend = function () {
                    self.dataUrl = this.result;
                }
            }
        },
        handleFileChange (e) {
            ...
            this.file = inputDOM.files[0];
            ...
            // 在获取到文件对象进行预览就行了!
            this.imgPreview(this.file);
            ...
        }
    }
</script>

当然了,这东西的兼容性有点捉鸡: IE10+, 移动端可以快乐的使用。

clipboard.png

URL.createObjectURL

文档在这,这个方法其实很直观,唯一需要注意的是对临时路径的销毁,来看下代码:

...
imgPreview (file) {
    let self = this;
    // 看支持不支持FileReader
    if (!file || !URL.createObjectURL) return;

    if (/^image/.test(file.type)) {
        // 创建一个reader
        let this.dataUrl = URL.createObjectURL(file)
    }
},
handleFileChange (e) {
    // 每次重新选择都需要进行对上一次的销毁
    this.dataUrl && URL.revokeObjectURL(dataUrl)
    ...
    this.file = inputDOM.files[0];
    ...
    // 在获取到文件对象进行预览就行了!
    this.imgPreview(this.file);
    ...
}
...

代码一下子少了几行直观了不少哈哈哈,兼容性也是IE10+, 移动端安卓4.0+,safari6.0+

预览就这么完成了,下一个我们来说拖拽!

拖拽选择

拖拽说白了就是一个事件监听,drop事件,我们从头开始说起

浏览器拖拽事件

首先,放DragEVentMDN文档,重点是下面四个事件及解释:

  • dragenter
    当拖动的元素或选择文本输入有效的放置目标时,会触发此事件。

  • dragleave
    当拖动的元素或文本选择离开有效的放置目标时,会触发此事件。

  • dragover
    当将元素或文本选择拖动到有效放置目标(每几百毫秒)上时,会触发此事件。

  • drop
    当在有效放置目标上放置元素或选择文本时触发此事件。


以及dataTransfer对象:在拖放交互期间传输的数据。
获取方法: event.dataTransfer

为什么要关注着几个呢?因为浏览器是自身监听这几个拖放事件的!!譬如你把图片或者pdf拖进浏览器里。浏览器是会试图打开这个文件的,所以我们要干掉默认行为,很简单e.preventDefault()

...
methods: {
    preventDefaultEvent (eventName) {
        document.addEventListener(eventName, function (e) {
            e.preventDefault();
        }, false)
    },
},
mounted () {
    // 阻止浏览器默认的拖拽时事件,测试阻止这几个就够了,不放心就全阻止一遍吧
    ['dragleave', 'drop', 'dragenter', 'dragover'].forEach(e => {
        this.preventDefaultEvent(e);
    });
}
...

做完这一步,我们只需监听目标上的drop事件就行了,稍微改造下代码:

<template>
    <div ref="box">
      ...
    </div>
</template>
<script>
    ...
    addDropSupport () {
        let BOX = this.$refs.box;
        BOX.addEventListener('drop', (e) => {
            e.preventDefault();
            this.errText = '';
            // 上面给的MDN文档里有讲到dataTransfer承载拖拽数据
            let fileList = e.dataTransfer.files; // 其实这就是文件对象列表了
            // 总得拖一个文件吧
            if (fileList.length === 0) {
                return false
            }
            // 格式限制
            if (fileList[0].type.indexOf('image') === -1) {
                this.errText = '请选择图片文件';
                return false;
            }
            // 这次限制下只能拖一个文件
            if (fileList.length > 1) {
                this.errText = '暂不支持多文件';
                return false
            }
            this.handleFileChange(null, fileList[0]);
        })
    },
    // 加入第二个参数
    handleFileChange (e, FILE) { 
        // 数据赋值改动,这样就兼容两种选择方式了
        this.file = FILE || inputDOM.files[0];
    }
    ...
</script>

其实到这里重要的点都讲了,接下来说些其他的

上传

  • uploader的话选择完图片在handleFileChange里直接执行个请求上传

  • 在父组件里获取值该怎么传怎么传

其他一些东西

  • 当页面中需要多个inputer时,同一个input的id会冲突,所以不指定的情况下需要个唯一id:

<template>
    ...vue
    <input :id="inputId" ... />
    ...
</template>
<script>
...
methods: {
    gengerateID () {
        let nonstr = Math.random().toString(36).substring(3, 8);
        if (!document.getElementById(nonstr)) {
            return nonstr
        } else {
            return this.gengerateID()
        }
    },
},
mounted () {
    this.inputId = this.id || this.gengerateID();
}    
...
</script>
  • input原本可以指定接收的文件格式,会在选择框出来的时候默认无法选择非指定格式的文件

<!-- accept属性-->
<input accept="image/*,video/*;" .../>
  • 移动端允许拍照选择

<!-- capture属性-->
<input capture="video" .../>

最后

  • 暂时就这么多了,完整的源码在这里

  • 有任何讲的不对不好的地方请大力指正!

  • 顺便打下广告,喜欢就不妨star下vue-img-inputer吧!


你看上去很美味得样子
492 声望22 粉丝

啊?