Help us understand the problem. What is going on with this article?

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

前提

  • 単一ファイルコンポーネントで実装。
  • zipファイルの解凍には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のページ

やること

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])の説明にも
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する

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away