背景
- SPAではないAngularアプリで、ある機能を実装していて、画面をまたぐ保存領域が欲しくなった
- Cookieで実装しようとしたが、容量が足りない
- localStorageを使うことにした
localStorageとは
The localStorage property allows you to access a local Storage object. localStorage is similar to sessionStorage. The only difference is that, while data stored in localStorage has no expiration time, data stored in sessionStorage gets cleared when the browsing session ends—that is, when the browser is closed.
とのこと。
私の言葉で書くと、ローカルストレージとはブラウザ内でキーバリューペアでデータを保存できる領域です。
Cookieより大きいものだとセッションストレージとローカルストレージがありますが、セッションストレージはブラウザを閉じたら消えるもの、ローカルストレージは(ユーザーが削除しない限りは)ずっと残るものという感じです。前に趣味でnekobitoというMarkdownエディタを作ったときはこれを使ってました。
ブラウザ対応
- http://caniuse.com/#search=localStorage を見るとIE8からSupportedだ!使える!(そもそもIE8自体は既に切り捨ててますが)
Angular2でlocalStorageを使うServiceの実装
何か面倒なことがあるかと思いきや、特に気をつけることはありませんでした。一つだけ、保存するときJSON.stringify, 取り出すときにJSON.parseすることくらいでした。
const MY_STORAGE_KEY = 'my_storage_key';
interface IMyItem {
id: number;
hoge: string;
fuga: number;
}
@Injectable()
export class MyStorageService {
// データの取り出し
fetch(): IMyItem[] {
return JSON.parse(localStorage.getItem(MY_STORAGE_KEY)) || [];
}
// 全削除
clear(): void {
localStorage.removeItem(MY_STORAGE_KEY);
}
// 保存
add(myItem: IMyItem): void {
const items = this.fetch().concat(myItem);
localStorage.setItem(MY_STORAGE_KEY, JSON.stringify(items));
}
// 1件削除
delete(myItem: IMyItem): void {
const items = this.fetch();
const filteredItems = items.filter((_item) => {
return _item.id !== myItem.id;
});
localStorage.setItem(MY_STORAGE_KEY, JSON.stringify(filteredItems));
}
}
localStorageをsubscribeする
上のコードで、localStorageをCRUD操作することはできました。
次に、localStorageの値の変更を監視して、各コンポーネントの表示に反映させたくなりました。
ここでは例として、本棚のためのコンポーネントと、本をリスト形式で一覧するためのコンポーネント、2種類あるとして、このどちらもがBookStorageというServiceを通して管理するlocalStorageの領域の変更を監視したい、というケースで書きます。
- BookShelfContainer
- BookListContainer
これに対して私は、以下のようなコードを書いて対処しました。
@Injectable()
export class BookStorageService {
private booksObserver: any;
books$: Observable<IBook[]>;
constructor() {
this.books$ = new Observable(observer => {
this.booksObserver = observer;
}).share();
}
// 保存
add(book: IBook): void {
const books = this.fetch().concat(myItem);
localStorage.setItem(BOOK_STORAGE_KEY, JSON.stringify(books));
this.booksObserver.next(this.fetch());
}
// その他のCRUD操作でも同様(操作後にthis.fetch()の値をnextする)
}
IBook[]のObservableをServiceのプロパティに持ち、それを使う側のコンポーネントでsubscribeすれば、BookListContainerからでもBookShelfConntainerからでもlocalStorageの内容を監視して、画面に反映することができます。
export class BooksListContainer implements OnInit {
constructor(private bookStorageService: BookStorageService) { }
ngOnInit() {
this.bookStorageService.subscribe((_books) => {
this.books = _books;
});
}
}
こうすることで、BookListContainerからlocalStorageの本のデータを更新したときも、BookshelfContainerから本のデータを更新した時も、どちらのビューにもそれが反映されます。
ただ、このやり方が役に立つのは、SPAではないウェブサイトで、1画面にlocalStorageの最新のデータを監視する2つのContainer(Component)が存在し、それらがどちらもデータ更新のたびにそれを表示に反映したい、という限られた場面だとは思います。そうでなければ、一つのStoreのような場所から、localStorageとつなげばいいのでしょう。