Vue.js始めるおれおれアドベントカレンダー2016 – 12日目
昨日のフォームのやつで「ファイルはアレだよ」という話になってたので、アレするのを試してみました。
コードは抜粋しか記載してないのでGitHubの方で確認してください。
ファイル情報の取得
- App.vue
<input @change="file_change" ref="file" type="file" multiple /> <ul v-if="form.files.length > 0"> <li v-for="(file, index) in form.files"> #{{index + 1}} <ul> <li>名前: {{file.name}}</li> <li>サイズ: {{file.size.toLocaleString()}} bytes</li> <li>種類: {{file.type}}</li> </ul> </li> </ul>
file_change (event) { const elFile = this.$refs.file const files = elFile.files form.commit('setFiles', files) }
- form.js
mutations: { setFiles (state, files) { state.files = Array.from(files).map(file => { const data = { name: file.name, size: file.size, type: file.type } state.files.push(data) }) } }
情報の所在
<input type="file">
の要素オブジェクトの files
プロパティに格納されてます。
change
イベントのタイミングで ref
を使って参照します。でもってStoreに格納しました。
情報の形式
elForm.files
は配列じゃなくてFileListなので、情報を配列に変換しました。
FileListをそのまま突っ込むと二度目以降の change
イベントが発火しなくなった。
あ、あと multiple
の有無によらずFileListなので、ひとつだけ選択してもらう場合は el.files[0]
でアクセスします。
ファイル情報を純粋なオブジェクトへ変換
この例では単純に state.files = Array.from(files)
だけでも間に合うんだけど、これから処理を足すので map()
も併せて使ってます。あ、でもFileオブジェクトを突っ込むのも良くないのかな。なら今回みたいにして正解だ。
表示
v-for
でくるくる出力。
というわけで、名前とか出すだけならこれで終わり。
画像を表示する
プレビュー機能を追加します。
state.files = Array.from(files).map(file => { const data = { name: file.name, size: file.size, type: file.type } if (file.type.startsWith('image/')) { data.previewImageSrc = window.URL.createObjectURL(file) } …
<li v-if="file.previewImageSrc"> <img :src="file.previewImageSrc" :alt="`${file.name}のプレビュー画像`" class="form-files-imagePreview" /> </li>
画像かどうかの判定
file.type
に種類が格納されていて、例えば image/png
とか text/html
とか。
これが "image/"
で開始していれば画像と判断します。
画像URLの作成
createObjectURL()
を使ってFileオブジェクトから特殊なURLを作成します。あんまりよくわかってない。
本当は使用後に revokeObjectURL()
で解放してやるのが良いらしい。
オブジェクト URL が不要になった場合にはこれらを逐一
window.URL.revokeObjectURL()
で削除するのが望ましいでしょう。ブラウザーは、文書がアンロードされた際にこれらのオブジェクト URL をメモリから解放します。しかし、パフォーマンスとメモリ使用を考慮し、明示的にアンロードできる安全な機会があるならば、そうするべきです。
テキストを表示する
ついでにテキストも。
state.files = Array.from(files).map(file => { const data = { name: file.name, size: file.size, type: file.type } … if (file.type.startsWith('text/')) { data.textContent = 'loading...' const reader = new window.FileReader() reader.onloadend = event => { const text = event.target.result data.textContent = text } reader.readAsText(file) } …
<li v-if="file.textContent"> ファイル冒頭: <pre>{{file.textContent | textPreview}}</pre> </li>
Vue.filter('textPreview', function (value, length = 128) { let result = value.slice(0, length) if (value.length > 128) { result += '…' } return result })
テキストかどうかの判定
画像と同じく "text/"
で開始するかどうかで判定しました。
ただし、 *.js
ファイルが application/javascript
だったり *.csv
が application/vnd.ms-excel
だったりもするので、もうちょっとうまくやった方が良いかも。
テキストの内容を取得
ファイルの内容を読み込んで表示します。
読み込みにはFileReaderを使います。
読込処理が終了すると readyState は DONE に変わり、loadend イベントが生じます。それと同時に result プロパティにはファイルの内容が文字列として格納されます。
だそうです。
カスタムフィルターで冒頭だけ表示
冒頭部分に必要なら "…"
を付けて抜き出してくれるフィルターを用意しました。
よくわからないこと
mutationの処理の中の非同期処理
良くないと聞いた。actionを通すとか?
まだそこらへん知らないので愚直にここに書いた。々ドキュメントを読もう……。
Bootstrapと <input type="file">
Vue関係ないんだけど。
ドキュメントの例だと何もしてない。
<div class="form-group"> <label for="exampleInputFile">File input</label> <input type="file" id="exampleInputFile"> <p class="help-block">Example block-level help text here.</p> </div>
こんなもんか?
あとファイル情報の表示はもうあきらめてあんなになりました。 Thumbnail コンポーネント(?)を使うと良さそうだろうか。でも三種類あるんだよなー。
おしまい
デザインェ……。