しかたらむーの てっくぶろ

組み込みソフトの古参エンジニアがモダンなフロントエンドエンジニアに転生 その進化の記録です

Vue.js + Typescript で LocalStorageを使ったToDoアプリを作ってみる

はじめに

WebアプリのフロントエンドをSPAで開発していると、デスクトップのネイティブアプリの様にファイルの読み書きができるといいなぁって思うことがちょくちょくあります。

HTML5から、WebStorageというAPIが採用され、フロントエンドでもネイティブアプリのファイルIOの様な感覚でデータの永続化が可能になりました。
WebStorageにはsessionStorageLocalStorageの2種類がありますが、永続化という点ではLocalStorageになります。(sessionStorageはブラウザを閉じるとデータが消えてしまう為)

今回はこのLocalStorageの使い方を紹介してみたいと思います。

出来上がりはこんな感じです!

Vueなどモダンなフレームの記事によくあるToDoアプリを題材にLocalStorageqを使ってみたいと思います。 せっかくなので、登録したToDoを永続化させて、使えるアプリにしてみたいと思います。
いつものとおり、出来上がりイメージは次の様な感じです。

f:id:Shikataramuno:20190318142714p:plain

見た目は何も変わり映えしませんが、ブラウザを閉じても、PCを再起動しても登録したToDoがクリアされずに表示されます。

今回のコードはここです。 GitHub - Shikataramuno/vue-ts-localStorageDB at Blogアップ

以前に紹介したスクロールリストのコードをベースにTodoをLocalStorageで永続化する様に改造する方針でやってみましたので、以前の記事も参考にみていただければと思います。
techblo.shikataramuno.com

LocalStorage どう使うの?

LocalStorageはURLのオリジンごとにデータを永続化でき、しかも仕様上の容量は最低 5MB と画像の永続化などでなければ、実用上十分な容量が確保されています。
実装上はWindowオブジェクトのlocalStorageプロパティでアクセスでき、以下APIが定義されています。

API 操作
getItem(key:string) keyの値を取得する
setItem(key:string, value:string) keyでvalueを書き込み
removeItem(key:string) keyの値を削除する。
clear このドメインのlocalStorage全体を削除する。

注意する点としては、APIで渡すデータはいずれもDOMString型であることくらいです。
JSONオブジェクトそのままでは記録できません(serializeすれば別ですが)ので、JSON.stringifyJSON.parseでオブジェクトとstringを型変換してやる必要があります。

LocalStorage を見てみる

LocalStorageデベロッパーツールで、その中身を確認できます。
Application -> Local Storage -> ドメイン と辿れば、現在のLocalStorageの中身が表示されます。

f:id:Shikataramuno:20190318151447p:plain

コンポーネント&クラス構成

では、ToDoアプリに行きましょう。
今回、LocalStorageでデータを永続化させるので、最初にコンポーネント構成と責務分担を簡単に設計してみました。 全体の構成をUMLのクラス図チックに書いてみるとこんな感じです。(厳格なクラス図ではありませんが、そのあたりはご容赦いただき雰囲気を掴んでいただければと思います)

f:id:Shikataramuno:20190318202504p:plain

  • UIコンポーネント
    TodoList.vueです。出来上がりイメージにあるUIを構築します。
    createdのLifeCycleメソッドでTodosのインスタンスを取得します。以降はこのインスタンスを経由してLocalStorageを読み書きします。

  • ToDoクラス
    'Todo.ts'です。Todoのモデルとなるクラスで、分類を示すタグ、todoの内容、id番号、完了状況をメンバとして定義しています。

  • ToDosクラス
    Todos.tsです。LocalStorageを使う上で肝となるクラスで、ここにLocalStorageへのアクセスをカプセル化します。
    Todoのcollectionを保持するのと同時にLocalStorageへのアクセスを一元化するためにSingletonパターンを採用してみました。 その他は、CRUDのメソッドをpublic定義しています。

  • SortOrdersクラス
    列ごとのソートオーダーを保持するクラスです。

さぁ作ってみましょう

っということでプロジェクトの作成から行きます。

プロジェクトの定義

プロジェクト名を vue-ts-localstorage-dbとしてvue create コマンドでプロジェクトを作成します。

作成時のオプションや可視性(publicやらprivateやら)を省略するための設定、 デバッグ文を出力できるようにするための設定などは以下の記事を参考にしてください。

techblo.shikataramuno.com

LocalStorageへのCRUD

今回はLocalStorageの使い方がメインなのでUIコンポーネントの詳細は割愛します。(すみません) LocalStorageへのアクセスはTodos.tsカプセル化しています。実装コードは以下のとおりです。

import Todo from './Todo';

const ToDosKeyWord: string = 'Todos';
const IdKeyWord: string = 'id';
export default class Todos {

  static getInstance(): Todos {
    console.log('MemberList.getInstance');
    if (!this.instance) {
      console.log('call constructor');
      this.instance = new Todos();
    }
    return this.instance;
  }

  private static instance: Todos;
  private todos: Todo[] = [];
  private id: number = 1;

  constructor() {
    console.log('Todos class constructor');
    if (IdKeyWord in localStorage) {
      this.id = JSON.parse(localStorage.getItem(IdKeyWord) as string) as number;
    } else {
      localStorage.setItem(IdKeyWord, JSON.stringify(this.id));
    }
    if (ToDosKeyWord in localStorage) {
      const objs: Todo[] = JSON.parse(localStorage.getItem(ToDosKeyWord) as string) as Todo[];
      objs.forEach((obj: Todo) => {
        const todo: Todo = new Todo(obj.id, obj.tag, obj.todo, obj.complete);
        this.todos.push(todo);
      });
    } else {
      localStorage.setItem(ToDosKeyWord, JSON.stringify(this.todos));
    }
  }

  getTodos(): Todo[] {
    return this.todos.slice();
  }

  update(todo: Todo): void {
    const index: number = this.todos.findIndex((td: Todo) => {
      return td.id === todo.id;
    });
    this.todos[index] = todo;
    localStorage.setItem(ToDosKeyWord, JSON.stringify(this.todos));
  }

  addTodo(todo: Todo): void {
    todo.id = this.id;
    this.todos.push(todo);
    localStorage.setItem(ToDosKeyWord, JSON.stringify(this.todos));
    this.id++;
    localStorage.setItem(IdKeyWord, JSON.stringify(this.id));
  }

  delete(target: Todo): void {
    this.todos = this.todos.filter((todo: Todo) => {
      return todo.id !== target.id;
    });
    localStorage.setItem(ToDosKeyWord, JSON.stringify(this.todos));
  }
}

まずは、7~14行目のgetInstance()メソッドでsingletonパターンを実装しています。
次に20~36行目のコンストラクタでLocalStorageを検索し永続化データを読み込んでいます。

23行目、28行目でgetItem()LocalStorageから読み込んだデータからJSON.parse()でオブジェクトを再構築しています。 これは先に述べたとおり、string型でしか記録できないためです。

今回は、Todoに付与するユニークなIDとTodo配列の二つのキーで永続化しています。 Todo配列をそのまま永続化するのはちょっと乱暴に思えるかもしれませんが、Todoを1件ずつ書き込むと検索、削除でパフォーマンスが低下すると考えたからです。

全てのTodoを一括して永続化し、検索などはメモリ上の配列を対象に行う方式としました。この辺りはデータの容量やアクセスの頻度など対象アプリによりきちんと設計する必要がある部分ですね

38行目のgetTodos() ではLocalStorageから読み込んだTodo配列のコピーを返します。
42行目からの update() では、メモリ上の配列を更新し、配列ごとsetItem()で上書きしています。
50行目からの addTodo() では、メモリ上の配列に1件追加(push)し、同じく上書きしています。
58行目からの delete() ではメモリ上の配列をフィルタ(削除対象のID以外のTodo)し、上書きしています。

何れもsetItem()の第二引数でJSON.stringfy()を使ってオブジェクトをstringに変換して記録していまが、これもstring型でしか記録できないための処理です。

出来上がり!

npm run serve で動かしてみましょう。 「やらなぁ~あかんこと」の追加のところにタグとTodoを入力し、「追加」ボタンクリックしてください。Todoがリストに追加されると思います。

いくつか追加してから
ブラウザと閉じて、再度開いてみてください。
                 きちんとデータが記録されていると思います。(やった!)

謝辞

せっかく永続化するのだから、少しでも使えそうなサンプルと思いTodoを題材にLocalStorageを試してみました。 ネイティブアプリのファイルアクセスみたいに気軽にアクセスできる点はGood!な感じでした。 シンプルなわりに長い文書になってしまいましたが、最後まで読んで下さりありがとうございました。 何かのお役に立てれば幸いです。