前提
- 単一ファイルコンポーネントで実装。
- 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内のデータを表示させる。
サンプルコード
<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文の外で使いたい場合。
サンプルコードの
...
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()
の処理が終わらないまま、レンダリングされます。
そのため、一部のデータで画像が表示されないという現象がおきます。
...
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する
もう少し良い方法があったり、ここ改善すると良さげ!というのがあれば、ご教示くださいませ。