5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Vue3】Piniaを用いた状態管理の永続化とその解除【Pinia】

Last updated at Posted at 2024-01-31

はじめに

Pinia(vuexも?)は状態管理を行うのに非常に便利なツールですが、通常は画面リロードでせっかく保持した状態が消えてしまいます。
それだと何かと不都合が多そうなので、よしなに永続化したい!でも、ログアウトしたときとかには永続化を解除したい!ということで奮闘した記録です。

Vuexは永続化解除の方法までググれば出てくるのになぜかPiniaは出てこないので、誰かの参考になれば嬉しいですね。(蓋を開けてみれば別にググるまでもないくらい簡単だったので、そりゃ情報なんてないわ
インストール(npm i ~~)に関しては一切省略します。

TS勢の方は、私の下手くそなJSをよしなにTSに置き換えてみてください。

Piniaのセットアップ

Vue公式が推すくらいなので、Vueを触っていれば聞いたことがない方は少なさそうな状態管理ライブラリです。ピーニャって読むんですかね。アイコン可愛すぎんか。
セットアップはOptionAPI / CompositionAPIお好きにどうぞ。
私はVue2自体をまったく知らないので、CompositionAPI風味に書いています。

stores/userStoreSample.js
import { ref } from 'vue';
import { defineStore } from 'pinia';

export const useUserStoreSample = defineStore('userSample', () => {
    // state
    const userId = ref('')
    const userName = ref('')
    const userEmail = ref('')

    // getters
    const getUserId = () => {
        return userId.value
    }

    const getUserName = () => {
        return userName.value
    }
    // 本題に関係ないのでsetters含め以下略

    return {
        userId,
        userName,
        // 以下略
    }
})

stateを書いてgetterを書いて(setterは省略しましたがあるものとして)、いたって普通にuserStoreをセットアップしました。

この状態でもコンポーネントから呼び出せばstoreとして機能し、状態管理をuserStoreSampleが担ってくれます。SPA想定の作りであれば、画面遷移してもuserStoreSampleが有効であればいつでもstoreを介して状態を取得することができます。

が、画面リロードでstoreの中身がふっ飛びます。 Pinia含む状態管理ライブラリは通常、リロードによる状態管理を保証しません。(理由は知らない)

作ってる側としては「そういうもんだよな〜」って理解すればいいでしょうが、ユーザーからすればそうはいきません。なんか上手くいかなかったりしたら「とりあえずリロードするかぁ」ってなりません?私はなります。
そのような条件下でせっかく保持した状態が飛んでしまったら使い勝手が悪すぎるので、リロードをしても状態を保持できるようにしましょう。永続化です。

永続化

今回は、「Pinia 永続化」でググるとすぐに検索に引っかかるこちらのライブラリを使います。Piniaのpluginです。ライブラリを追加せずともPiniaの機能の中で実現できるというのは内緒の話。

ライブラリを追加したら、先程のdefineStoreを書き換えます。

stores/userStoreSample.js(永続化処理追加)
import { ref } from 'vue';
import { defineStore } from 'pinia';

export const useUserStoreSample = defineStore('userSample', () => {
    // state
    const userId = ref('')
    const userName = ref('')
    const userEmail = ref('')

    // getters
    const getUserId = () => {
        return userId.value
    }

    const getUserName = () => {
        return userName.value
    }
    // 本題に関係ないのでsetters含め以下略

    return {
        userId,
        userName,
        // 以下略
    },
    // 以下、永続化のために追加した記述
    {
        persist: {
            storage: sessionStorage,
        },
    },
})

以上です。Piniaを導入するときのようにmain.jsに手を加える必要もありません。この記述を追加するだけで永続化が実現され、リロードしてもstoreが吹っ飛ぶことはありません。やったね。

保管するストレージを明示的にセッションストレージを指定していますが、特に指定をしなければローカルストレージに保存されます。セキュリティ的にローカルストレージの方が脆弱っぽいので、今回はセッションストレージを選択しました。

ブラウザの検証機能でセッションストレージを見るとこんな感じ。
スクリーンショット 2024-02-01 0.02.41.png
無事、セッションストレージにstoreの情報が格納できました。

でも…なんか気持ち悪くないですか?
セキュリティ的に難があるからっていう理由でセッションストレージに情報を入れたわけですが、こんな生のJSON突っ込んだだけではセキュリティも何もあったものではない気がします。ローカルストレージでもセッションストレージでも、同じようにブラウザから簡単に覗けるわけだし、チョットワカル悪い人が見たら、悪いことやり放題な特級呪物に見えてしまいます。

そして何より、まるでイケてない。

あーこいつこんなデータをセッションで持たせたいんだねふーん…ww みたいなのが透けてしまうのがとても嫌。ということでせめて暗号化してやろうと思ったので次、暗号化。

暗号化

この段階で猛者の諸氏はとっくにお気づきのことと思いますが、そもそもPiniaの永続化の仕組みを乱雑に言ってしまえば

  1. defineStoreで宣言したstateをストレージに持たせる
  2. (リロードした場合など含む)state情報をストレージから取り寄せてstoreが再度持つ
    という仕組みなわけです。実際、先の永続化ライブラリのデフォルトの働きとして

1. JSON.stringifyでstateをJSON文字列にシリアライズしたものをストレージに保管
2. JSON.parseでストレージからデシリアライズしてstoreに返す
ことを行っています。なので、ここの仕組みをいじってやります。今回はcrypto-jsというライブラリを使いました。

stores/userStoreSample.js(暗号化処理追加)
import { ref } from 'vue'
import { defineStore } from 'pinia'
import CryptoJS from 'crypto-js' // 暗号化処理をするライブラリを追加

export const useUserStoreSample = defineStore('userSample', () => {
    // state
    const userId = ref('')
    const userName = ref('')
    const userEmail = ref('')

    // getters
    const getUserId = () => {
        return userId.value
    }

    const getUserName = () => {
        return userName.value
    }
    // 本題に関係ないのでsetters含め以下略

    return {
        userId,
        userName,
        // 以下略
    },
    {
        persist: {
            storage: sessionStorage,
            // シリアライズ・デシリアライズの処理をJSONではなくAESを使った暗号処理に変更
            serializer: {
                deserialize: (str) => {
                    const decrypted = CryptoJS.AES.decrypt(str, 'user')
                    const decryptedData = decrypted.toString(CryptoJS.enc.Utf8)
                    return JSON.parse(decryptedData)
                },
                serialize: (state) => {
                    return CryptoJS.AES.encrypt(JSON.stringify(state), 'user').toString()
                },
            }
        },
    },
})

先程述べた、永続化ライブラリのシリアライズ周りの設定をJSONではなくAESで暗号化してやりました。暗号化の処理自体は下記の参考リンクにCryptoJSに関する記事を載せておりますので、そちらと公式を参考にしてください。別にAESでなくてもできます。
なお、永続化ライブラリのserializerの項目に詳しいですが、serializeの引数は当該ストアの状態を、deserializeの引数はストレージから取得したvalue(文字列)を取るようになっています。

この処理を追加してあげると、先程と同じ情報をストレージに格納したときにこうなります。(長いから途中で切ってます)
スクリーンショット 2024-02-01 0.37.13.png
ちゃんと暗号化されています。これなら悪いこともされなさそうです。何よりちょっとだけイケてる気がする。

crypto-jsについて補足
ここまで書いてから発覚したのですが、このライブラリは保守すら止まっている状況のようです。
NodeJSやブラウザでCryptoモジュールが導入され、もはやCryptoモジュールのラッパーにしかならんだろ…というのが理由なんだとか。
その辺りに懸念がある場合はcyrpto-jsを用いず、代替手段で実装を行ってください。

永続化の解除

これが私自身一番知りたいことだったのですが、別にローカルストレージあるいはセッションストレージにPiniaのstateを逃がしているだけの話なので、解除したいタイミングで
sessionStorage.removeItem()とかlocalStorage.removeItem()するだけです。ググりまくった時間を返せ。

最後に

「Pinia 永続化 解除」で延々とググっても情報が出ない理由がよく分かりました。だってブラウザのストレージに逃がしてるだけなんだもの。。
ということで、新しくライブラリを導入するときはしっかりドキュメントを読み漁ろうねってことを再認識させられたいい機会でした。永続化ライブラリ側で、デフォルトのシリアライズの設定を暗号化してくれれば楽なのに。

参考リンク

偉大なるライブラリ一覧

永続化をするにあたってのヒントをくださった先人様たち
@salt1998(sasaki shiori) 様

zenn みほりん 様

暗号化処理についてのヒントをくださった先人様たち
独学プログラマ 様

@aiiro_engineer(りょうた) 様

ローカルストレージかセッションストレージかで悩んでヒントをくださった先人様
テイクユアタイム あかり 様

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?