开发 Tauri 程序的时候,有时候需要在本地存储一些数据,并希望借助 pinia 管理以实现响应式,则可以参考本篇文章。
项目源码可前往 sofast-tauri2-practice 查看。
useStorageAsync
vueuse 中有一个特别实用的函数 useStorageAsync,可以很方便地将浏览器的 LocalStorage 交给 pinia 管理以实现响应式。
同时,它还提供了一个 StorageLikeAsync 接口,用于实现自定义的存储,而 Tauri 则可以通过实现这个接口来实现响应式本地存储。
typescript
interface StorageLikeAsync {
getItem: (key: string) => Awaitable<string | null>;
setItem: (key: string, value: string) => Awaitable<void>;
removeItem: (key: string) => Awaitable<void>;
}
如果需要序列化,需要实现 SerializerAsync 接口:
typescript
interface SerializerAsync<T> {
read: (raw: string) => Awaitable<T>;
write: (value: T) => Awaitable<string>;
}
tauri-fs
Tauri2 需要用到 fs 插件来管理本地文件系统,需要先添加 fs 插件:
bash
pnpm tauri add fs
同时,几乎所有插件的权限在前端默认都是禁止的,需要手动在 src-tauri/capabilities
目录下的权限 json 文件中开启我们需要用到的权限:
json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
"fs:default",
"fs:allow-write-text-file",
"fs:allow-read-text-file",
"fs:allow-exists",
"fs:allow-remove",
"fs:allow-mkdir",
{
"identifier": "fs:scope",
"allow": [
"$APPDATA",
"$APPDATA/*"
]
}
]
}
实现
接下来只需要实现一下 StorageLikeAsync 接口以及 SerializerAsync 接口即可:
typescript
class JsonLocalStorage implements StorageLikeAsync {
getItem(key: string): Awaitable<string | null> {
return new Promise(async resolve => {
const path = key + '.json';
// 不存在直接返回 null
// 如果返回的是 null,useStorageAsync 默认会调用一次 setItem
if (!(await exists(path, {baseDir: BaseDirectory.AppData}))) {
resolve(null)
return
}
resolve(await readTextFile(path, {baseDir: BaseDirectory.AppData}))
})
}
removeItem(key: string): Awaitable<void> {
return remove(key + '.json', {baseDir: BaseDirectory.AppData})
}
setItem(key: string, value: string): Awaitable<void> {
return writeTextFile(key + '.json', value, {baseDir: BaseDirectory.AppData})
}
}
class JsonSerializer implements SerializerAsync<object> {
read(raw: string): Awaitable<object> {
return JSON.parse(raw);
}
write(value: object): Awaitable<string> {
return JSON.stringify(value);
}
}
然后再稍微封装一下:
typescript
const jsonLocalStorage = new JsonLocalStorage()
const jsonSerializer = new JsonSerializer()
export const useJsonLocalStorage = <T>(key: string, initialValue: MaybeRefOrGetter<T>) => {
return useStorageAsync<T>(key, initialValue, jsonLocalStorage, {serializer: jsonSerializer as any})
}
使用
接下来,我们只需要在 pinia 中使用即可:
typescript
import {defineStore} from "pinia";
import {useJsonLocalStorage} from "./storage.ts";
export const useStore = defineStore('main', () => {
const state = useJsonLocalStorage<{
count: number
}>('state', {count: 0})
return {
state
}
})
示例
接下来就可以写个简单的示例测试一下了:
typescript
<template>
<div>
count is {{ store.state.count }}
<a @click="handlerAdd" href="">add</a>
</div>
</template>
<script setup lang="ts">
import {useStore} from "../store/index.store.ts";
const store = useStore()
const handlerAdd = (e: MouseEvent) => {
e.preventDefault()
store.state.count++
}
</script>
退出程序重新打开,依然是 2:
避坑
权限问题
Tauri 2 对于权限要求非常严格且细致,在前端使用某个 Tauri 插件的 api 的时候,一定要检查是否开启相关权限,可以前往官方文档或者对应仓库下面的 permissions/autogenerated/reference.md
文件查看相关权限以及说明,如 fs
可前往:
同时,fs
插件有个比较重要的权限是 scope
,配置允许访问的文件有哪些,默认是空的,如果不配置,就算你开启了各种读写权限,依然无法正常读写,所以可以像上面那样配置一个 scope
:
json
{
"identifier": "fs:scope",
"allow": [
"$APPDATA",
"$APPDATA/*"
]
}
这里配置 $APPDATA
和 $APPDATA
两个后面会讲到。
写文件需要目录存在
Tauri 在写文件的时候,需要文件所在的目录是存在的,否则就会报错。
具体表现为 writeTextFile
或者 writeFile
的时候,如果传入的 path
不存在,则会报错。
而我们这里用到了 {baseDir: BaseDirectory.AppData}
这个选项,所以,需要在写文件的时候先确保 $APPDATA
这个目录是存在的,否则就会报错。
这边我选择的是在软件启动的时候判断是否存在并创建它:
typescript
onMounted(() => {
exists("", {baseDir: BaseDirectory.AppData}).then(exist => {
if (!exist) {
mkdir("", {baseDir: BaseDirectory.AppData})
}
})
})
值得注意的是,由于 mkdir
传入的是空串,也就是创建 $APPDATA
这个目录,所以需要确保 $APPDATA
这个目录是有权限的,于是需要配置 $APPDATA
的 scope
权限。