Vue.jsでzipファイルを解凍（JSZipを利用）し、中身をブラウザ表示させる。

# 前提
* 単一ファイルコンポーネントで実装。
* zipファイルの解凍には[JSZip](https://stuk.github.io/jszip/)を利用。
* csvのパースは、axiosを使いAPI（golang製）を経由(csvの中身がSJISという条件があるが、今回は略)。

## zipファイルの中身

### ディレクトリ構造
以下のようなディレクトリ構造のzipファイルを解凍する。

```
upload_dir
├── images
│   ├── Thumbs.db
│   ├── item01.jpg
│   ├── item02.png
│   ├── item03.gif
│   ...
│   ...
│   └── image10.jpg
└── upload.csv
```
  
### csvファイルの中身

```
商品ID,商品名,商品説明文
item01,item_name_1,これはitem01の商品説明です。
item02,item_name_2,これはitem02の商品説明です。
item03,item_name_3,これはitem03の商品説明です。
```

## JSZipをインストール

npmでインストールするか、package.jsonなどで管理している場合は、そこに記述。
参考：[npmのJSZipのページ](https://www.npmjs.com/package/jszip)

# やること
csv内の商品IDとimagesディレクトリ内の画像ファイルの名前を照らし合わせて、ブラウザに画像とcsv内のデータを表示させる。


# サンプルコード

``` vue.js
<template lang="pug">
div
  input#file-upload(type="file" @change="select" accept="application/zip")
  
  table
    tr
      th 商品ID
      th 商品画像
      th 商品説明
    tr(v-for="data in uploadData")
      td {{ data.item_id }}
      td: img(:src="data.img_url")
      td {{ data.description }}

</template>
<script>
import JSZip from 'jszip'

export default {
  data () {
    return {
      this.showData = []
      ...
    }
  },
  methods: {
    let file = e.target.files[0]

    let formData = new FormData()
    formData.append('zip', file)
    
    // csvのパースは、サーバーサイドで行う。
    // 今回は大胆にzipファイルごとサーバーサイドに投げて処理。csvファイルだけをパースして返すようにしています。
    const axiosPromise = this.$axios.post(
      `${process.env.API_URL}/api/unzip/csv`,
       formData
    )
    
    // 画像ファイル名(path)から、拡張子以外のファイル名を取得するためのregexpをグローバルに設定
    const regexpImgFile = /upload_dir\/images\/item\d*\.(jpg|png|gif)/
    const regexpImgName = /(item\d*)\.(jpg|png|gif)/ // 後方参照させて拡張子前を取得できるようにする。

    // JSZipの各関数からの返り値は、Proiseオブジェクトになっている。
    const loadedZip = JSZip.loadAsync(file)

    // zipファイルから画像を取り出す。
    const imgFiles = loadedZip.then(zip => {
      return zip.file(regexpImgFile)
    })

    let imgDataMap = new Map()
    const imgPromise = imgFiles.then(files => {
      files.map(zipf => {
        const imgFile = regexpImgFile.exec(zipf.name)
        const imgName = regexpImgName.exec(imgFile)[1] // 後方参照を使うと返り値がArrayで返却されるのでindex指定して拡張子より前を取得
        imgDataMap.set(imgName, zipf.async('blob')) // [img_name => PromiseObj]というMapをsetしていく
      })
      return imgDataMap
    })

    Promise.all([axiosPromise, imgPromise]).then(([axiosRes, imgRes]) => {
      const showData = axiosRes.data // APIからのレスポンスはjsonの配列を想定。apiResDataの中身は、[json,json...]のような状態になっている。

      let imgPromises = []
      for (const i in showData) {
        // APIからのレスポンスデータのキー名はitem_idとして値に「item01」などが入っていることとする。
        let imgBlobPromise = imgRes.get(showData[i].item_id)

        const imgPromise = imgBlobPromise.then(imgBlob => {
          const imgUrl = URL.createObjectURL(imgBlob) //  Blobからimgタグで使う為のurlを生成。
          showData[i]['img_url'] = imgUrl // 'img_url'というキーでimgUrlを格納
        })
        imgPromises.push(imgPromise)
      }

      // 上記のforの中で、画像のurlを取得するのに値がPriomiseで返ってきてしまうので、
      // 全Promiseを配列化し、その配列の中身が全部処理が終わってから、showDataを格納する。
      // これをしないと、見た目上、表示データが欠損する。
      Promise.all(imgPromises).then(() => {
        this.showData = showData
      })
    })
  }
</script>
```

# 個人的なポイント
## for文内で返されるPromise Objectを配列化して、Promise.all()で処理を直列化する。

公式の[ZipObject#async(type[, onUpdate])](https://stuk.github.io/jszip/documentation/api_zipobject/async.html)の説明にも
`Return a Promise of the content in the asked type.`
と書いてある通り、返り値はPromiseのオブジェクトとなります。

ですので、`ZipObject.async('blob')`などを使った場合、以降は`Promise.then()`の形でthenの中で処理をしなければなりません。

これで困るのが、for文内で`Promise.then()`をしていて、そこで得た値をfor文の外で使いたい場合。

サンプルコードの

``` vue.js
...
for (const i in showData) {
  let imgBlobPromise = imgDataMap.get(showData[i].item_id)
  const imgPromise = imgBlobPromise.then(imgBlob => {
    const imgUrl = URL.createObjectURL(imgBlob)
    showData[i]['img_url'] = imgUrl
  })
  ...
}
...
```
この部分。

このまま`Promise.All([promises])`で解決せず、for文の外で`this.showData`に格納して、HTML側で`v-for`を使ってレンダリングしようとすると、for文内の`imgBlobPromise.then()`の処理が終わらないまま、レンダリングされます。
そのため、一部のデータで画像が表示されないという現象がおきます。

``` vue.js
...
for (const i in showData) {
  let imgBlobPromise = imgDataMap.get(showData[i].item_id)
  const imgPromise = imgBlobPromise.then(imgBlob => {
    const imgUrl = URL.createObjectURL(imgBlob)
    showData[i]['img_url'] = imgUrl
  })
  ...
}
this.showData = showData // これをレンダリングしようとすると、データ欠損しているように表示される。
...
```

`console.log(showData)`で確認してみても、ブラウザコンソールでは、img_urlに値が入っているように見えるので、余計混乱します。


この辺りは、以下を参照して解決しました(助かった...)。
参考：[for文の中で非同期関数を使いたいときでも慌てずPromiseする](https://qiita.com/progre/items/5666f4a333cc2d032d15)

もう少し良い方法があったり、ここ改善すると良さげ！というのがあれば、ご教示くださいませ。