Motyw
Store'y Pinia
Wybór między store'em a Vue Query
Pinia czy Vue Query — decyzja, od której zależy reszta implementacji:
| Rodzaj danych | Miejsce przechowywania |
|---|---|
| Rekordy CRUD z serwera: listy, szczegóły, zapis | Vue Query |
| Słowniki i konfiguracja pobierane raz, używane w wielu miejscach | Pinia |
| Stan sesji użytkownika, czyli token i uprawnienia | Pinia |
| Stan interfejsu dzielony między komponentami, na przykład otwarte menu czy bieżący katalog | Pinia |
| Stan pojedynczego komponentu | zwykły ref w komponencie |
Danych CRUD nie należy duplikować w store
Przechowywanie listy artykułów w Pinii oznacza konieczność samodzielnej obsługi odświeżania, unieważniania oraz stanów ładowania, czyli ręcznego odtworzenia tego, co zapewnia Vue Query.
Rejestracja
Plik src/stores/index.js tworzy wyłącznie instancję Pinii, bez żadnych pluginów:
js
// src/stores/index.js
export default defineStore((/* { ssrContext } */) => {
const pinia = createPinia();
return pinia;
});Store'y modułów nie podlegają auto-rejestracji i importowane są jawnie:
js
import { useAuthStore } from "modules/auth/stores/auth";
const authStore = useAuthStore();Store'y występujące w projekcie
| Store (id) | Plik | Odpowiedzialność |
|---|---|---|
auth | modules/auth/stores/auth.js | Sesja, token, currentUser, uprawnienia; opisany w Autentykacji |
core/locale | modules/core/stores/core.js | Język interfejsu, język treści oraz lista dostępnych języków |
core/navigation-drawer-side-menu | modules/core/stores/core.js | Informacja o rozwiniętym podmenu w nawigacji |
blog/category | modules/blog/stores/category.js | Drzewo kategorii wraz ze zmianą kolejności |
media/disk | modules/media/stores/disk.js | Dyski, defaultDisk, currentDiskName oraz metoda init() |
media/directory | modules/media/stores/directory.js | Drzewo katalogów i bieżący katalog |
media/file | modules/media/stores/file.js | Kontekst listy plików (additionalParamsWithDirectory) |
menu/menu | modules/menu/stores/menu.js | Typy menu oraz elementów możliwych do podlinkowania |
content-builder | modules/contentBuilder/stores/content-builder.js | Typy sekcji i sectionables |
Konwencja identyfikatorów
Identyfikator store'a przyjmuje postać <moduł>/<nazwa>, na przykład media/disk czy blog/category. Wyjątkiem są auth i content-builder, czyli moduły posiadające pojedynczy store.
Dwa style zapisu
W kodzie występują oba style oferowane przez Pinię. Wybierając jeden, kieruj się tym, co przeważa w danym module.
Options store, dominujący w projekcie
js
// src/modules/media/stores/disk.js
export const useMediaDiskStore = defineStore("media/disk", {
state: () => ({
disks: [],
currentDisk: null,
}),
getters: {
defaultDisk: (state) => state.disks.find((d) => d.is_default),
currentDiskName: (state) => state.currentDisk?.name,
},
actions: {
async fetchDisks() {
const { data } = await api.get("media/disks");
this.disks = data.results;
return data.results;
},
async init() {
if (!this.disks.length) {
const results = await this.fetchDisks();
this.currentDisk = results.find((d) => d.is_active && d.is_default);
return results;
}
},
},
});Wzorzec metody init()
Metoda init() pobiera dane wyłącznie wtedy, gdy store pozostaje pusty. Pozwala to wywoływać ją z wielu miejsc bez ryzyka zdublowanych żądań. Plik MasterLayout.vue korzysta z niej w funkcji preFetch.
Setup store
js
// src/modules/core/stores/core.js
export const useNavigationDrawerSideMenuStore = defineStore(
"core/navigation-drawer-side-menu",
() => {
const item = useLocalStorage("core/navigation-drawer-side-menu/item", {});
function setItem(newItem) {
if (item.value?.key !== newItem?.key) {
item.value = newItem;
return item.value;
}
item.value = null; // ponowne kliknięcie zamyka
}
return { item, setItem };
},
);Persystencja
W projekcie nie zastosowano pluginu persystencji — każdy store zapisuje stan samodzielnie:
| Mechanizm | Store | Przechowywane dane |
|---|---|---|
LocalStorage (Quasar) | auth | token, lastEmail, wasLoggedIn |
Cookies (Quasar) | core/locale | locale, content_locale, ważność 5 lat |
useLocalStorage (VueUse) | core/navigation-drawer-side-menu | rozwinięta pozycja menu |
Store języka stanowi przykład zapisu w trzy miejsca jednocześnie:
js
// src/modules/core/stores/core.js
setLocale(locale) {
const lang = this.getLocaleByCode(locale);
if (lang) {
Cookies.set("locale", locale, { expires: 365 * 5 }); // 1. ciasteczko
api.setLocale(locale); // 2. parametr w axiosie
setQuasarLang(lang.iso); // 3. pakiet językowy Quasara
this.locale = locale;
}
}Lista dostępnych języków jest zaszyta w kodzie
Tablice locales i contentLocales w pliku modules/core/stores/core.js mają charakter statyczny; obecnie zawierają wyłącznie pl, a wpis en pozostaje zakomentowany. Dodanie języka wymaga edycji tego pliku oraz utworzenia katalogu tłumaczeń, co opisują Tłumaczenia.
Store korzystający z endpointów modułu
Store nie musi wywoływać api bezpośrednio — może sięgać po warstwę endpointów:
js
// src/modules/blog/stores/category.js
import { getCategoriesTree, reorderCategories } from "modules/blog/api/endpoints/category";
export const useBlogCategoryStore = defineStore("blog/category", {
state: () => ({ categoryTree: [] }),
actions: {
async fetchCategoryTree(params) {
const { data } = await getCategoriesTree({
...params,
order_by: params?.order_by || "order",
order_direction: params?.order_direction || "ASC",
});
this.categoryTree = data.results;
return data;
},
async reorder(sortedCategories) {
return reorderCategories({ tree: sortedCategories });
},
},
});To zarazem przykład uzasadnionego użycia Pinii do danych z serwera: drzewo kategorii przestawia się metodą przeciągnij i upuść, więc komponent potrzebuje lokalnie mutowalnej kopii, a nie cache przeznaczonego tylko do odczytu.
Store zależny od innego store'a
Wywołanie useXStore() wewnątrz gettera albo akcji jest w pełni poprawne:
js
// src/modules/media/stores/file.js
getters: {
additionalParamsWithDirectory(state) {
const directoryStore = useMediaDirectoryStore();
return {
directory: directoryStore.currentDirectory.path,
...state.additionalParams,
};
},
},Wywołania useXStore() na poziomie modułu są niedozwolone
Wywołanie umieszczone poza getterem lub akcją wykona się w momencie importu pliku, czyli zanim Pinia zostanie zainstalowana, co przerywa działanie aplikacji. Musi więc znaleźć się wewnątrz funkcji.