概要
目的
CSVアップロードしてVueの変数に格納して使いたい!
よく使うし、外部モジュール化しておいて必要な時にファイル読み込んできて使いたい!
ということで、外部からインポートして使えるCSV読込コンポーネントを作成しました。
JavaScript、非同期覚えたてなところがあるので、おかしいところあればご指摘いただけると嬉しいです。
こちらを参考にさせていただきました。
- Vue.jsでCSVをインポート
- JavaScript, Python, Ruby で多次元配列を zip で作ってループする場合のメモ
- 【JavaScript】FileAPIからCSVを読み込んでみる
コード
<template>
<div class="container">
<form action="">
<input @change="csvUpload" type="file">
</form>
</div>
</template>
<script>
import csvUpload from '../lib/csvUpload'; //libフォルダに格納してます
export default {
data(){
return {
csvJson: {}
}
},
methods:{
async csvUpload(event){
const res = await csvUpload.csvUpload(event)
if(res[0]){
this.csvJson = res[1]
} else{
alert(res[1]);
}
}
}
}
</script>
<style>
省略
</style>
function checkFileReader(){
return window.File && window.FileReader && window.FileList && window.Blob
}
export default {
csvUpload(event){
return new Promise(function(resolve,reject){
// ブラウザチェック
if(!checkFileReader()){
reject([ false, 'FileAPI非対応のブラウザです。'])
}
const file = event.target.files[0]
const reader = new FileReader()
// ファイル読込完了
const mailJson = []
const loadFunc = () => {
const lines = reader.result.split('\n')
const header = lines[0].split(',')
lines.shift()
// 一行ずつ読込
lines.forEach(eachLine => {
const elements = eachLine.split(',')
if(elements.length != header.length){
return
}
// 一列ずつ読込
const line = {}
const zip = (array1, array2) => array1.map((_, i) => [array1[i], array2[i]])
zip(header, elements).forEach(eachElement => {
line[eachElement[0]] = eachElement[1]
})
mailJson.push(line)
})
resolve([ true, mailJson])
}
reader.onload = loadFunc
reader.onerror = () => {
reject([ false, 'ファイルの読込に失敗しました'])
}
reader.readAsText(file)
})
}
}
詳細
外部コンポーネントの読込
import csvUpload from '../lib/csvUpload'; //libディレクトリに格納してます
libディレクトリに格納しているcsvUpload.jsを読み込んでいます。
メソッドを定義する
methods:{
async csvUpload(event){
const res = await csvUpload.csvUpload(event)
if(res[0]){
this.csvJson = res[1]
} else{
alert(res[1]);
}
}
}
今回は、csvUploadというメソッドを準備し、そのメソッドの中で外部モジュールを実行しています。
csvUpload.csvUploadはPromiseを返す関数になっています。
awaitをつけることで、Promiseが実行された結果をresで受け取ることができます。
無事にファイル読み込みできたら[ true, ファイルオブジェクト]、エラーが出たら[ false, エラーメッセージ ]が返ってくるようにしています。
ここでは、vueのdataに定義したcsvJsonという変数に格納されるようにしています。
フォーム部品をつくる
<form action="">
<input @change="csvUpload" type="file">
</form>
@change
でメソッドを指定すると、ファイル選択ボタンが押されたときに先ほど定義したcsvUploadメソッドが実行されます。
CSVの変換処理
ここから、呼び出してきたcsvUploadモジュールで何をしているか説明していきます。
戻り値はPromise
export default {
csvUpload(event){
return new Promise(function(resolve,reject){
処理内容
}
}
}
Promiseオブジェクトを戻してやることによって、呼び出し元でasync,awaitが使えるようになります。
Promise内では、resolve、rejectが関数にとってのreturnみたいな形で働きます。
Promiseを呼び出すときにawaitをつけてやることで、resolve,rejectでの戻り値をちゃんと受け取れるんですね。
ファイル形式のチェック
MIMEタイプで判定しようと思いましたが、単純に「text/csv」とかだけじゃないみたいなので、あきらめました。
ファイル容量のチェック
簡単にできそうなので、あとで付け足したいと思います。
FileAPI・FileReaderによるファイルの読込
reader.onload = loadFunc
reader.onerror = () => {
reject([ false, 'ファイルの読込に失敗しました'])
}
reader.readAsText(file)
FileReader.readAsText() メソッドは非同期処理です。ファイルの読込が完了しないまま、次の処理に進んでしまいます。
ファイルの読込が完了したか判断するには、onloadイベントを使い、ファイルの読み込みにエラーが起きたか判断するには、onerrorイベントを使います。
onload,onerrorは、そのイベントが起きた時に何を実行するか、関数を指定しておく形になります。
ここで詰まりました。
onloadで関数指定しますけど、returnつかったら戻り値受け取れないじゃん!!ってなりました。
結果的に、Promiseで囲ってから、onloadに指定した関数からresolveでPromiseを解決するという手段をとりました。
終わりに
せっかくresolveとrejectを使っているのに、実行完了のときは[true,obj],失敗の時は[false,err]を返すのって、スマートじゃないですよね。resolveとrejectの判定について勉強不足なので、これからどうやって書いていったら良いか考えてみたいと思います。
あと、csvUpload.csvUploadの部分、モジュール名と関数名が同じで、続けて書くという書き方気に入ってないんですけど、どういう書き方するとスマートになるか教えてください・・・
また、forEachの外に定義した配列を、forEachで変えていくのも良くなさそうです。この辺もまだやり方わかりませんが、勉強したいと思います。
このへんちょっとずつ直していって安心して使っていただけるようなモジュールを作りたいです。
他にもおかしいところあったらぜひご指摘くださいm(__)m