5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

前提

  • 単一ファイルコンポーネントで実装。
  • 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する

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

5
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?