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 コンポーネント(?)を使うと良さそうだろうか。でも三種類あるんだよなー。

おしまい

デザインェ……。