原文:http://blog.csdn.net/sinat_17775997/article/details/58585142
之前用Vue做了一個基礎的組件 vue-img-inputer ,下面就叫 vii
,記錄下在開發過程中遇到的 知識點 (都算比較基礎,具體代碼不會貼太多,都可以在 項目 倉庫里看到)。
上傳文件很多項目都要用到,一些組件庫里(ele/iview...) 文件上傳組件
都是做成了標配,雖然 vii
和 uploader
定位還是有些差別,但實現上都有幾個共同要點:
-
樣子要好看點
-
圖片/文件選擇后預覽
-
實現拖拽選擇文件
-
圖片選擇后執行某些動作(譬如uploader的上傳等)
先上 demo
注: 下面有些地方會有些啰嗦,請選擇觀看
基礎
首先我們有個文件選擇框,恩,長這樣:
好丑啊!!我們來讓它變好看點:
第一個方法:修改自身CSS
這里有一個 shadowDOM 的概念,簡單的來說就是我們經常用到的一些HTML標准組件(例如viedo
,甚至 滾動條
)其實是由若干個更基礎的DOM由瀏覽器封裝成的,使得我們調用只要一個標簽就夠了,這類也就是 WebComponent
,這里具體不展開了。我們先來看下file-input的內部是如何的(chrome devtool不 設置 是看不到的):
所以呢,直接給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變這樣了:
好像挺簡單!然而我們看到 -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了,接着說圖片預覽
圖片預覽
思路有兩種:
-
選擇文件后直上傳然后得到網絡url
-
用HTML5的
File API
的FileReader
圖片本地轉成base64格式的url
然后將url賦值給一個img標簽
我們這里肯定選擇第二種,所以先介紹下:
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 var 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+, 移動端可以快樂的使用。
預覽就這么完成了,下一個我們來說拖拽!
拖拽選擇
瀏覽器拖拽事件
首先,放 DragEVent
的 MDN文檔 ,重點是下面四個事件及解釋:
-
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" .../>
最后
-
暫時就這么多了,完整的 源碼在這里
-
有任何講的不對不好的地方請大力指正!