Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

Vue.js + TypeScript で IndexedDB にCSVデータを入れる

こんにちは、フロントエンド寄りのエンジニアの @1derful です。

2018年モバイルファクトリーアドベントカレンダー 15日目の記事です。
前日は mattak さんの「フリーランスになって変化したこと」でした。

はじめに

フロントエンド分野の技術は移り変わりが激しいですね。 とはいえ、流行の速いトレンドもあれど、W3Cが策定したWeb標準技術はメンテナンスが止まらない限り使い続けられます。 そこで、Vue.jsTypeScript などの新しい技術に FileReaderIndexedDB の古くからあるWeb APIを織り交ぜて使ってみたいと思います。

まず開発環境を作ります

今回は Vue CLI を使います。 Vue.js の迅速な開発環境構築のため、TypeScript から JavaScript に変換するトランスパイラや、各モジュールを束ねるバンドラのインストールを一括で行い、雛形をスキャフォールディングしてくれるツール群です。 また、TypeScript を公式サポートしていますので Vue CLI の設定時に TypeScript 使用するよう選択します。

f:id:i1derful:20181214101228p:plain
Vue CLIでの今回の記事の設定

それでは、

  1. Vue CLI をインストール
  2. Dexie.js をインストール
  3. PapaParse をインストール
  4. PapaParse の型定義ファイルをインストール*1
  5. プロジェクトを作る

の手順で行います。

ちなみに、IndexedDB はクライアントサイドで使える NoSQL のデータベースです。
IndexedDB API をそのまま使うと複雑な作りになるのでシンプルに扱える Dexie.js を使うことにします。他にも IndexedDB のライブラリはあるので、使い比べてみるのがよいと思います。

そして、CSV のパースを自力で行うこともできますが、CSVライブラリがあるので楽するために使いましょう。 そこでメンテナンスが継続されていて、使用方法が簡単で思った形に整形しやすい PapaParse を使うことにしました。

それでは、以下のコマンドでインストールしてプロジェクトを立ち上げます。

yarn add @vue/cli --dev
yarn add dexie --dev
yarn add papaparse --dev
yarn add @types/papaparse --dev
vue create csv-hangar
vue serve

結果、以下のバージョンの環境が作れました。

node: 11.4.0
vue-cli: 3.2.1
typescript: 3.2.2

実際にコードを書く

 <!-- Bootstrapを使ってます -->
<template>
  <div class='excel-uploader'>
    <div class='input-group-btn'>
      <label class='btn btn-primary'>
        アップロード
        <input type='file' accept='.csv,text/csv' @change='handleUpload' class='file-upload'>
      </label>
    </div>
  </div>
</template>

f:id:i1derful:20181214104003p:plain
味気ないのでロゴを付けてみました

それでは Dexie を使って IndexedDB にデータベースを作っていきます。

@Component
export default class CSVUploader extends Vue {
  /** data: メンバ変数 */
  public csv: string = '';
  public db: AppDatabase = new Dexie('AppDatabase') as AppDatabase;

  /** Vueのライフサイクルフック */
  public created(): void {
    /** テーブル定義 */
    this.db.version(1).stores({
      customer: '++id',    // auto_increment
    });

    /** デフォルトとしてダミーのリストをIndexedDBに追加 */
    this.db.customer.bulkAdd([
      { name : '名無しの権兵衛', age: 67, email: 'foo@foo.foo' },
      { name : 'ジョン・ドウ',     age: 32, email: 'bar@bar.bar' },
    ]);
  }

ここで Chrome をお使いならば Developer Tool を開きます。 Developer Tool の Application タブ から IndexedDB を見てみると、命名指定した「AppDatabase」が作られているのがわかります。 無事に「customer」テーブルも作成されているのが確認できました。

f:id:i1derful:20181214112038p:plain
ブラウザ立ち上げ時にダミーが入ります

以下でアップロード時の処理を追加していきます。

  /** methods: CSVアップロード時の処理 */
  public handleUpload(ev: HTMLElementEvent<HTMLInputElement>): void {
    const files = ((ev.target as any) as HTMLInputElement).files!;
    if (!files.length) {
        return;
    }
    this.createCSV(files[0]);
  }

  /** methods: CSVをパース */
  public createCSV(file: File): void {
    const reader = new FileReader();
    const vm = this;
    reader.onload = (ev: ProgressEvent): void => {
      const csv: string = (ev.target as any).result;
      const collection: any = Papa.parse(csv, { header: true });
      vm.addRecord(collection);
    };
    reader.readAsText(file);
  }

  /** methods: IndexdDBに追加 */
  public addRecord(collection: any): void {
    this.db.customer.bulkAdd(collection.data);
  }
}

以下のようなCSVファイルをアップロードしてみます。

name,age,email
山田太郎,35,hoge@hoge.hoge
ジョン・スミス,42,fuga@fuga.fuga
イワン・イワノヴィッチ・イワノフ,21,piyo@piyo.piyo

アップロード後に再びDeveloper Toolを開きます。
更新されていない場合は、Developer Toolを再起動するといいです。

f:id:i1derful:20181214112955p:plain
CSVのレコードが追加されているのがわかります

型定義ファイル

TypeScript で作る際は、以下の型定義ファイルをインポートする必要があります。

import { Component, Vue } from 'vue-property-decorator';
import { AppDatabase, HTMLElementEvent } from '../libs/definitions';

前者は Vue.js 上で TypeScript を使うのに必ず必要なデコレータです。
Vue CLI のインストールで TypeScript を選択すると、自動でインストールされます。

後者は、自作するしかないので以下に内容を示します。

/**
   IndexedDB
*/
import Dexie from 'dexie';

export type DexieDatabase = {[P in keyof Dexie]: Dexie[P]};
/** データベースの型定義 */
export interface AppDatabase extends DexieDatabase {
  customer: Dexie.Table<ICustomer, number>;
}
/** テーブルの型定義 */
export interface ICustomer {
  id?: number;
  name: string;
  age: number;
  email: string;
}

/**
  Event
*/
export interface HTMLElementEvent<T extends HTMLElement> extends Event {
  target: T;
}

TypeScript 2.1 · TypeScript 等を参考にしています。
HTMLElementEvent は定義されておらず TypeScript のコンパイラでコケますので定義します。


それでは駆け足でしたが、お元気で!
明日は tenmihi です。お楽しみに!!

*1:TypeScript のコンパイラに型を教えてあげないとビルドできないので、型定義ファイルが必要になります。