11-8-2022

PocketBase & Vue 3 - De beste open source backend stack?

#code#backend#vue

Note

Deze blog post is op 30 novembber geupdate zodat de instructies passen bij de 0.8.0 versie

Wat is Pocketbase?

Wanneer je aan jouw volgende SaaS-startupidee werkt, moet je een beslissing nemen over de back-end. Je kijkt rond en vindt twee kant-en-klare oplossingen die passen bij jouw project: Google's Firebase en Supabase.

Beide zijn goede opties op basis van uw vereisten, maar ze hebben hun eigen voor- en nadelen. Firebase kan niet door zelf worden gehost, dus je moet Google betalen op basis van het gebruik van de verschillende Firebase-services (database, authenticatie en opslag etc.) en alle gegevens moeten op hun servers worden opgeslagen. Supabase biedt daarentegen veel van dezelfde functionaliteit en stelt je ook in staat om de oplossing zelf te hosten, zodat je volledig eigenaar bent van de techstack en alle gegevens die er doorheen stromen. Het zelf hosten van Supabase is echter niet zo eenvoudig als het klinkt, vooral als je niet bekend bent met Docker, PostgreSQL en het deployen van code.

Er is een derde alternatief: PocketBase

PocketBase is een open source-backend voor uw volgende SaaS of mobiele app in 1 bestand. Het is gebaseerd op SQLite en Go waardoor het klein, snel en portable is.

Het biedt out-of-the-box functionaliteiten zoals:

  • Realtime database
  • Authenticatie met zowel e-mail als wachtwoord en providers zoals Google, Facebook en GitHub
  • Bestandsopslag
  • Uitbreidbaarheid

Een minimale app bouwen

Om te laten zien hoe gemakkelijk het is om met PocketBase te werken, maken we in deze blog post een minimale Twitter-achtige app met Vue 3 als front-end framework. Laten we beginnen!

Maak een Vue 3 app

Open om te beginnen je terminal en voer het volgende command uit om het Vue 3 project te starten:

npm init vue@latest

Kies de volgende:

  • Typescript
  • Vue Router
  • Pinia
  • ESLint
  • Prettier
 Project name:  <your-project-name>
 Add TypeScript?  Yes
 Add JSX Support?  No
 Add Vue Router for Single Page Application development?  Yes
 Add Pinia for state management?  Yes
 Add Vitest for Unit testing?  No
 Add Cypress for both Unit and End-to-End testing?  No
 Add ESLint for code quality?  Yes
 Add Prettier for code formatting?  Yes
Scaffolding project in ./<your-project-name>...
Done.

Voor styling gaan we met Tailwind. Volg de officiële instructie hier om het aan uw project toe te voegen.

Voer nu de volgende commands uit om naar uw projectmap te gaan, de dependencies te installeren en de Vue dev server te starten:

> cd <your-project-name>
> npm install
> npm run dev

Top! Navigeer nu naar http://localhost:5173/ en je zou de Vue 3 app moeten zien. Tijd om verder te gaan naar PocketBase.

PocketBase instellen

Oké, ga naar de website van PocketBase en download de app die overeenkomt met je besturingssysteem. Ik gebruik de MacOS versie.

download-page

Na het downloaden pak je het bestand uit in de root van je Vue-project. Het zou er ongeveer zo uit moeten zien:

.
├── .node_modules
├── pocketbase_0.8.0_darwin_amd64 <-- Pocket base folder
├── public
├── src
├── .env
├── .eslintrc.cjs
├── .gitignore
├── env.d.ts
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── README.md
├── tailwind.config.js
├── tsconfig.config.json
├── tsconfig.json
└── vite.config.ts

Laten we PocketBase starten en naar de admin interface gaan. Voer het volgende command uit om de server te starten:

cd pocketbase_0.8.0_darwin_amd64/
./pocketbase serve

Navigeer nu naar http://localhost:8090/_/ en je zou het volgende scherm moeten zien waar je de admin-gebruiker kunt aanmaken:

admin-login-screen

Stel uw admin account in en klik op de knop create and login.

Gefeliciteerd, PocketBase is klaar voor gebruik!

Register & Login + Persistent LocalStorage store

Om onze PocketBase-back-end te verbinden met onze Vue-app, gaan we de officiële JS SDK gebruiken. Het stelt ons in staat om gemakkelijk de ingebouwde PocketBase-functies in onze app te gebruiken. Om de SDK te installeren, voert u het volgende command uit in de root folder van de Vue-app:

npm install pocketbase --save

Maak vervolgens een nieuw bestand met de naam index.ts onder src/pocketbase/ en voeg de volgende code toe om de SDK te importeren en te initialiseren:

// Import the PocketBase JS library
import PocketBase from "pocketbase";
// Init the PocketBase instance with the correct URL.
// By setting this in a .env file you can easily switch between development and production environments
const client = new PocketBase(import.meta.env.VITE_POCKETBASE_URL);
export default client;

Je ziet dat we hier ook een environment variable gebruiken om de PocketBase-URL te krijgen. Om dit mogelijk te maken, voeg een bestand met de naam .env (vergeet de punt in de bestandsnaam niet) toe aan de hoofdmap van uw Vue-project en voeg de volgende regel toe:

VITE_POCKETBASE_URL=http://localhost:8090

Omdat we TypeScript gebruiken, moeten we ook een injection key maken zodat we de PocketBase SDK in onze app kunnen injecteren, zodat we deze in onze verschillende componenten kunnen gebruiken. Maak een nieuw bestand met de naam injectionSymbols.ts onder src/symbols/. Voeg de volgende code toe:

import type { InjectionKey } from "vue";
import type PocketBase from "pocketbase";
export const pocketBaseSymbol: InjectionKey<PocketBase> = Symbol("PBClient");

Finally, open the main.ts file and add the lines shown below in order to import Pocketbase, the symbol we created and inject it in the app:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/index.css'
// Import the PocketBase JS library
import client from '@/pocketbase'; <- this line
// Import custom pocketBase type
import { pocketBaseSymbol } from "@/symbols/injectionSymbols" <- this line
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.provide(pocketBaseSymbol, client) <- this line
app.mount('#app')

Nu dat PocketBase werkt, laten we beginnen met de gebruikersregistratie en inlogfunctionaliteit. Voor dit voorbeeldproject houd ik het simpel en blijf ik bij aanmelden met e-mail en wachtwoord, maar weet dat PocketBase allerlei OAuth2-opties biedt:

pocketbase-oauth2-providers

Eerst moeten we ervoor zorgen dat we alle gebruikersinformatie verzamelen die we willen hebben. Het e-mailadres en wachtwoord zijn standaardvelden waar we ons geen zorgen over hoeven te maken, maar we willen ook een gebruikersnaam weergeven. PocketBase stelt een name en avatar-veld in, maar laten we die verwijderen. In plaats daarvan maken we een nieuw veld aan met de naam username, maken het verplicht en uniek.

users-table

Vervolgens willen we een nieuwe pagina maken met een registercomponent en een logincomponent. Maak eerst een nieuw bestand in de map Views en noem het HomeView.vue. Dit is de eerste pagina waarop iemand terechtkomt:

<script setup lang="ts">
import LoginComponent from "@/components/LoginComponent.vue";
import RegisterComponent from "@/components/RegisterComponent.vue";
</script>
<template>
    <div>
        <h1 class="mb-12 text-2xl">Login page</h1>
        <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
            <div>
                <LoginComponent />
            </div>
            <div>
                <RegisterComponent />
            </div>
        </div>
    </div>
</template>

Maak vervolgens een nieuwe component met de naam RegisterComponent.vue in de components folder. Hiermee kan iedereen een nieuw account voor onze app maken:

<script setup lang="ts">
import { inject, ref } from "vue";
import { pocketBaseSymbol } from "@/symbols/injectionSymbols";
import { useUserStore } from "@/stores/user";
import { useRouter } from "vue-router";
// Inject the PocketBase client
const $pb = inject(pocketBaseSymbol);
// Init the store
const userStore = useUserStore();
// Router composable
const router = useRouter();
// Reactive variables
const email = ref("");
const username = ref("");
const password = ref("");
const passwordConfirm = ref("");
// Function to create a new user
const createUser = async () => {
    try {
        if (validateInput()) {
            // Create new user
            const user = await $pb?.collection("users").create({
                username: username.value,
                email: email.value,
                password: password.value,
                passwordConfirm: passwordConfirm.value
            });
            if (user) {
                // Authenticate the user in order to set the username
                await authUser();
                // After succesfull user registration, redirect to dashboard
                router.push({ path: "/dashboard" });
            } else {
                console.log("Error");
            }
        } else {
            alert("Password doesn't match");
        }
    } catch (error) {
        console.log(error);
    }
};
// Function to authenticate the user based on email and password
const authUser = async () => {
    try {
        // Authenticate the user via email and password
        const userData = await $pb
            ?.collection("users")
            .authWithPassword(email.value, password.value);
        if (userData) {
            userStore.userID = userData.record.id;
            userStore.username = userData.record.profile?.username;
            userStore.userProfileID = userData.record.profile?.id!;
            router.push({ path: "/dashboard" });
        }
    } catch (error) {
        console.log(error);
    }
};
// Simple utility function to validate input. Easiliy extendable with additional checks if needed
const validateInput = () => {
    if (password.value !== passwordConfirm.value) {
        return false;
    } else {
        return true;
    }
};
</script>
<template>
    <h2 class="text-2xl font-bold">Register</h2>
    <form @submit.prevent="createUser">
        <div class="grid grid-cols-1 gap-6">
            <label class="block">
                <span>Username</span>
                <input type="text" class="mt-1 block w-full" v-model="username" />
            </label>
            <label class="block">
                <span>Email</span>
                <input type="email" class="mt-1 block w-full" v-model="email" />
            </label>
            <label class="block">
                <span>Password</span>
                <input type="password" class="mt-1 block w-full" v-model="password" />
            </label>
            <label class="block">
                <span>Repeat Password</span>
                <input
                    type="password"
                    class="mt-1 block w-full"
                    v-model="passwordConfirm"
                />
            </label>
        </div>
        <button
            type="submit"
            class="mt-4 text-white desktop-xl:text-2xl bg-black px-4 py-2 border-2 rounded border-black hover:bg-white dark:hover:bg-main-dark-bg hover:text-black"
        >
            Sign Up
        </button>
    </form>
</template>

Dit is best veel code, maar we kunnen het in drie delen opsplitsen:

  1. De functie createUser haalt de invoer uit het registerformulier en gebruikt deze om het account voor de gebruiker aan te maken. Het gebruikt ook de functie validateInput om ervoor te zorgen dat de wachtwoorden in beide wachtwoordvelden overeenkomen.
  2. Als de gebruiker succesvol is aangemaakt, wordt de functie authUser aangeroepen. Dit is nodig omdat alleen het registreren van een gebruiker geen geldig inlogtoken oplevert.
  3. Ten slotte wordt de gebruiker doorgestuurd naar de /dashboard-pagina.

De code is vrij basic zoals je kunt zien. Ik zou het niet aanraden om het een-op-een te gebruiken een productieomgeving. De error handling moet op zijn minst robuuster zijn. Maar als voorbeeld zal dit prima werken.

Het bestand LoginComponent.vue lijkt erg op het bestand RegisterComponent.vue. Het enige grote verschil is dat in de login-component de functie createUser niet wordt aangeroepen, aangezien dit alleen binnen de register-component gebeurt:

<script setup lang="ts">
import { useUserStore } from "@/stores/user";
import { pocketBaseSymbol } from "@/symbols/injectionSymbols";
import { inject, ref } from "vue";
import { useRouter } from "vue-router";
// Inject the PocketBase client
const $pb = inject(pocketBaseSymbol);
// Init the store
const userStore = useUserStore();
// Router composable
const router = useRouter();
// Local reactive variables
const email = ref("");
const password = ref("");
// Function to authenticate the user based on email and password
const authUser = async () => {
    try {
        // Authenticate the user via email and password
        const userData = await $pb
            ?.collection("users")
            .authWithPassword(email.value, password.value);
        if (userData) {
            userStore.userID = userData.record.id;
            userStore.username = userData.record.profile?.username;
            userStore.userProfileID = userData.record.profile?.id!;
            router.push({ path: "/dashboard" });
        }
    } catch (error) {
        console.log(error);
    }
};
</script>
<template>
    <h2 class="text-2xl font-bold">Login</h2>
    <form @submit.prevent="authUser">
        <div class="grid grid-cols-1 gap-6">
            <label class="block">
                <span>Email</span>
                <input type="email" class="mt-1 block w-full" v-model="email" />
            </label>
            <label class="block">
                <span>Password</span>
                <input type="password" class="mt-1 block w-full" v-model="password" />
            </label>
        </div>
        <button
            type="submit"
            class="mt-4 text-white desktop-xl:text-2xl bg-black px-4 py-2 border-2 rounded border-black hover:bg-white dark:hover:bg-main-dark-bg hover:text-black"
        >
            Login
        </button>
    </form>
</template>

Vergeet niet uw nieuwe HomeView.vue-pagina toe te voegen aan uw routerbestand:

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            path: '/',
            name: 'home',
            // route level code-splitting
            // this generates a separate chunk (About.[hash].js) for this route
            // which is lazy-loaded when the route is visited.
            component: () => import('../views/HomeView.vue')
        },
// Rest of the file omitted for brevity

Je ziet dat we de userStore in beide componenten gebruiken om wat gebruikersinformatie op te slaan, zodat we deze later in andere componenten kunnen gebruiken. Dit is een globale state beheerd door Pinia, die u aan uw project kunt toevoegen door een bestand met de naam user.ts aan src/stores/ toe te voegen en de volgende code toe te voegen:

import { defineStore } from "pinia";
import { ref } from "vue";
import { useLocalStorage } from "@vueuse/core";
export const useUserStore = defineStore("user", () => {
    // State variable
    // Using the useLocalStorage composable provided by VueUse in order to persist state during page reloads
    const userID = ref(useLocalStorage("userID", ""));
    const username = ref(useLocalStorage("username", ""));
    const userProfileID = ref(useLocalStorage("userProfileID", ""));
    // Actions
    function clear() {
        userID.value = "";
        username.value = "";
        userProfileID.value = "";
    }
    return { userID, username, userProfileID, clear };
});

U kunt nu nieuwe gebruikers registreren en laten inloggen, maar ze worden nergens naar doorgestuurd omdat de dashboardpagina nog niet bestaat. Laten we dat snel oplossen. Maak eerst een nieuwe view met de naam DashboardView.vue en voeg voorlopig de volgende tijdelijke code toe:

<script setup lang="ts"></script>
<template>
    <div>
        <h1>Dashboard</h1>
    </div>
</template>

en vergeet natuurlijk niet om de nieuwe dashboardpagina aan je routerbestand toe te voegen:

import client from "@/pocketbase";
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            path: "/",
            name: "home",
            // route level code-splitting
            // this generates a separate chunk (About.[hash].js) for this route
            // which is lazy-loaded when the route is visited.
            component: () => import("../views/HomeView.vue")
        },
        {
            path: "/dashboard",
            name: "dashboard",
            meta: { requiresAuth: true },
            // route level code-splitting
            // this generates a separate chunk (About.[hash].js) for this route
            // which is lazy-loaded when the route is visited.
            component: () => import("../views/DashboardView.vue")
        }
    ]
});
router.beforeEach((to, from) => {
    // Init the store within the beforeEach function as per the documentation:
    // https://pinia.vuejs.org/core-concepts/outside-component-usage.html#single-page-applications
    if (to.meta.requiresAuth && !client?.authStore.token) {
        return {
            path: "/"
        };
    }
});
export default router;

Mogelijk zie je iets anders aan de bovenstaande routercode. Namelijk het meta-object met het requiresAuth attribuut. Dit zorgt ervoor dat iemand niet zomaar naar de /dashboard-pagina kan gaan zonder in te loggen. Het werkt samen met de code onderaan, die de beforeEach functie gebruikt om te controleren of een bepaalde route het requiresAuth attribuut heeft en of de gebruiker een PocketBase-token heeft. Voor nu gaan we ervan uit dat iedereen die een token heeft succesvol is ingelogd. Houd er rekening mee dat u in een productieomgeving moet nadenken over het ongeldig maken van tokens.

Laten we tot slot wat navigatie toevoegen met extra controles op basis van de ingelogde status van de gebruiker. Maak een nieuwe component met de naam navbarComponent.vue en voeg de volgende code toe:

<script setup lang="ts">
import { useUserStore } from "@/stores/user";
import { pocketBaseSymbol } from "@/symbols/injectionSymbols";
import { inject } from "vue";
import { useRouter } from "vue-router";
// Init the store
const userStore = useUserStore();
// Inject the PocketBase client
const $pb = inject(pocketBaseSymbol);
// Router composable
const router = useRouter();
const logoutUser = () => {
    // Manual reset because Pinia using the composition API does not support the $reset function
    userStore.clear();
    // Remove the PocketBase token
    $pb?.authStore.clear();
    // Redirect to the login page
    router.push({ path: "/" });
};
</script>
<template>
    <nav
        class="sticky top-0 flex flex-wrap items-center justify-between py-3 bg-white z-10"
    >
        <div
            class="w-full relative flex justify-between py-1 lg:w-auto lg:static lg:block lg:justify-start"
        >
            <span aria-current="page" class="text-3xl font-extrabold">
                Pocketbase & Vue
            </span>
            <button type="button" class="lg:hidden">
                <svg
                    fill="none"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    viewBox="0 0 24 24"
                    stroke="currentColor"
                    class="h-8 w-8 fill-current text-black"
                >
                    <path d="M4 6h16M4 12h16M4 18h16"></path>
                </svg>
            </button>
        </div>
        <div
            class="lg:relative text-xl font-medium lg:flex lg:flex-grow items-center hidden"
        >
            <ul class="flex flex-row list-none ml-auto">
                <li v-if="$pb?.authStore.token" class="pr-5">
                    <router-link to="/feed"> Feed </router-link>
                </li>
                <li v-if="$pb?.authStore.token" class="pr-5">
                    <router-link to="/dashboard"> Dashboard </router-link>
                </li>
                <li v-if="$pb?.authStore.token" class="pr-5">
                    <span @click="logoutUser"> Logout </span>
                </li>
            </ul>
        </div>
    </nav>
</template>

Oké, dat was veel code. Eindelijk tijd om de resultaten te zien. Voer eerst het npm run dev command uit in de root map van uw Vue-project om de Vue dev-server te starten. Navigeer vervolgens in een ander terminalvenster naar uw PocketBase-map en voer ./pocketbase serve uit om de server te starten. Navigeer naar http://localhost:5173/ en je zou de volgende pagina moeten zien:

login-register-page

Tijd om onze allereerste gebruiker te registreren. Voer een gebruikersnaam, e-mailadres en wachtwoord in. Druk op de knop 'Aanmelden' en je zou op de dashboardpagina moeten belanden.

Gefeliciteerd, u kunt nu gebruikers registreren en ze laten inloggen en uitloggen!

first-registered-user

Verbeter het dashboard

Nu iemand kan registreren, inloggen en uitloggen is het tijd om wat meer functionaliteit toe te voegen. Gebruikers moeten posts kunnen toevoegen en ook kunnen zien wat ze in het verleden hebben gepost. Laten we beide functies aan het dashboard toevoegen.

We gaan de functies opsplitsen in afzonderlijke componenten en ze vervolgens samenbrengen in ons DashboardView.vue-bestand. Laten we beginnen met de nieuwe post-functie. Ga naar het PocketBase admin panel en klik op Collection en dan op de + New collection knop. Voeg de volgende velden toe:

  • titel
    • Type: Text
  • inhoud
    • Type: Text
  • gebruiker (dit wordt gebruikt om posts aan gebruikers te relateren)
    • Type: Gebruiker
  • gebruikersgegevens (Dit wordt gebruikt om gegevens op te halen over de gebruiker die het bericht heeft gemaakt) -Type: Relatie

Sla uw wijzigingen op en laten we verder gaan met de code.

create-user-post-collection

Sla uw wijzigingen op en laten we verder gaan met de code. Maak een nieuw bestand met de naam NewPostComponent.vue in de map Components en voeg de volgende code toe:

<script setup lang="ts">
import { ref } from "@vue/reactivity";
import { inject } from "@vue/runtime-core";
import { pocketBaseSymbol } from "@/symbols/injectionSymbols";
import { useUserStore } from "@/stores/user";
// Inject the PocketBase client
const $pb = inject(pocketBaseSymbol);
// Init the store
const userStore = useUserStore();
// Init emits
const emit = defineEmits(["newPostCreated"]);
// State
const title = ref("");
const content = ref("");
// Methods
const createPost = async () => {
    try {
        const postPayload = {
            title: title.value,
            content: content.value,
            user: userStore.userID,
            userdata: userStore.userProfileID
        };
        const response = await $pb?.collection("posts").create(postPayload);
        if (response) {
            emit("newPostCreated");
            title.value = "";
            content.value = "";
        }
    } catch (error) {
        console.log(error);
    }
};
</script>
<template>
    <h2 class="text-2xl font-bold">Create new post</h2>
    <form @submit.prevent="createPost">
        <div class="grid grid-cols-1 gap-6">
            <label class="block">
                <span>Title</span>
                <input type="text" class="mt-1 block w-full" v-model="title" />
            </label>
            <label class="block">
                <span>Content</span>
                <textarea class="mt-1 block w-full" v-model="content"></textarea>
            </label>
        </div>
        <button
            type="submit"
            class="mt-4 text-white desktop-xl:text-2xl bg-black px-4 py-2 border-2 rounded border-black hover:bg-white dark:hover:bg-main-dark-bg hover:text-black"
        >
            Submit
        </button>
    </form>
</template>

De code verschilt niet veel van onze register- en inlogcomponenten. We hebben weer een formulier waar gebruikers de titel en inhoud van hun posts kunnen invoeren en enige logica om het naar PocketBase te sturen. De functie createPost haalt de titel en inhoud uit het formulier maar ook de userID en userProfileID om het bericht aan de juiste gebruiker te koppelen. De gebruikersinformatie wordt opgehaald uit de Pinia store die wordt ingevuld wanneer iemand inlogt.

We laten ook de dashboardpagina weten, waar dit onderdeel zal leven, dat er een nieuwe post is gemaakt. De dashboardpagina kan vervolgens alle noodzakelijke acties uitvoeren, zoals het ophalen van het bericht.

Vervolgens hebben we een component nodig om een individuele post weer te geven. We kunnen dit gebruiken om een bericht weer te geven waar we maar willen in onze app. Om te beginnen, maakt u een nieuwe component met de naam IndividualPostComponent.vue en voegt u de volgende code toe:

<script setup lang="ts">
const props = defineProps({
    postData: Object
});
</script>
<template>
    <div class="mb-8 p-4 bg-blue-50 rounded">
        <h2 class="text-2xl font-bold">{{ props.postData?.title }}</h2>
        <span class="text-gray-400">{{
            props.postData?.expand.user.username
        }}</span>
        <p>{{ props.postData?.content }}</p>
    </div>
</template>

Dit onderdeel heeft verrassend weinig code nodig. We definiëren een prop (gegevens die kunnen worden doorgegeven van de ouder naar dit component) en gebruiken deze om de gegevens te krijgen die nodig zijn om het bericht weer te geven.

Laten we alles samenvoegen in de DashboardView.vue. Vervang de tijdelijke aanduidingscode die u daar nu hebt door het volgende:

<script setup lang="ts">
import NewpostComponent from "@/components/NewPostComponent.vue";
import IndividualPostComponent from "@/components/IndividualPostComponent.vue";
import { useUserStore } from "@/stores/user";
import { pocketBaseSymbol } from "@/symbols/injectionSymbols";
import { inject, onMounted, ref } from "vue";
// Inject the PocketBase client
const $pb = inject(pocketBaseSymbol);
// Init the store
const userStore = useUserStore();
// Local reactive variables
const posts = ref<any[]>([]);
// Get all the user's posts
const getOwnedPostList = async () => {
    try {
        const list = await $pb?.collection("posts").getFullList(200, {
            filter: `user = '${userStore.userID}'`,
            expand: "user"
        });
        if (list) {
            posts.value = list;
        }
    } catch (error) {
        console.log(error);
    }
};
onMounted(() => {
    getOwnedPostList();
});
</script>
<template>
    <div>
        <h1 class="mb-3 text-2xl">Dashboard</h1>
        <div class="grid grid-cols-2 gap-8">
            <div>
                <div v-for="post in posts">
                    <IndividualPostComponent :post-data="post" />
                </div>
            </div>
            <div>
                <NewpostComponent @new-post-created="getOwnedPostList" />
            </div>
        </div>
    </div>
</template>

Hier gebruiken we de twee componenten die we zojuist hebben gemaakt om het dashboard te vullen.

Als we de pagina mounten, roepen we de functie getOwnedPostList aan om alle posts die de gebruiker heeft gemaakt op te halen. Let op het attribuut expand: "user" dat we aan de payload hebben toegevoegd. Dit is zo dat PocketBase automatisch record relaties ophaalt en ons dus in staat stelt om de gebruikersgegevens op te halen die aan het profiel zijn gerelateerd zonder dat we een aanvullende query hoeven uit te voeren. Dan hebben we een v-for loop die alle posts die we hebben opgehaald doorloopt en de gegevens doorgeeft aan de IndividualPostComponent die zorgt voor de daadwerkelijke weergave van de berichtgegevens.

De NewpostComponent wordt gebruikt om het formulier weer te geven dat een gebruiker kan gebruiken om een nieuw bericht te maken. Omdat we een signaal uitzenden van het component wanneer een bericht wordt gemaakt, gebruiken we @new-post-created="getOwnedPostList" om het signaal op te vangen en alle posts opnieuw op te halen, waardoor de lijst wordt vernieuwd zonder de pagina opnieuw te hoeven laden.

API-regels

Nog een laatste ding voordat we een post kunnen maken, moeten we de API-regels van het gebruikersprofiel bijwerken, zodat we de gebruikersnaam kunnen ophalen die in onze posts moet worden weergegeven. Wat zijn API-regels denk je nu... uit de documentatie:

API-regels zijn uw Collection toegangsrechten en gegevensfilters.

Elke verzameling heeft 5 regels, die overeenkomen met de specifieke API-actie:

  • listRule
  • viewRule
  • createRule
  • updateRule
  • deleteRule

Elke regel kan worden ingesteld op:

  • "vergrendeld" - oftewel. null, wat betekent dat de actie alleen kan worden uitgevoerd door een geautoriseerde beheerder (dit is de standaard)
  • Lege string - iedereen kan de actie uitvoeren (beheerders, geautoriseerde gebruikers en gasten)
  • Niet-lege tekenreeks - alleen die Record(s) en gebruikers (al dan niet geautoriseerd) die voldoen aan de regelfilterexpressie kunnen deze actie uitvoeren

In ons geval moeten we de viewRule van het gebruikersprofiel instellen op een lege string, zodat iedereen de gebruikersprofielinformatie kan ophalen die is gekoppeld aan een posst (gebruikersnaam in ons geval).

user-profile-api-rules

De eerste post

Als je nu een post maakt, zou je zoiets als dit moeten zien:

dashboard

Geweldig! Gebruikers kunnen nu nieuwe posts maken en de volledige lijst zien van alle posts die ze hebben gemaakt.

Realtime feed maken

Laten we het laatste stukje van de puzzel toevoegen: een realtime feed van alle posts. Het doel is om een Twitter-achtige feed te hebben die automatisch wordt bijgewerkt zodra iemand een nieuw bericht maakt. Klinkt ingewikkeld, maar gelukkig ondersteunt PocketBase het abonneren op individuele rijen in de database (in dit geval individuele posts) of op een hele collection (alle posts, wat wij zullen gebruiken). Maak om te beginnen een nieuw bestand met de naam FeedView.vue in de map views en voeg de volgende code toe:

<script setup lang="ts">
import { pocketBaseSymbol } from "@/symbols/injectionSymbols";
import { inject, onMounted, onUnmounted, ref } from "vue";
import IndividualPostComponent from "../components/IndividualPostComponent.vue";
// Inject the PocketBase client
const $pb = inject(pocketBaseSymbol);
// Local reactive variables
const posts = ref({});
// Get all the user's posts
const getPostList = async () => {
    try {
        const list = await $pb?.collection("posts").getFullList(200, {
            expand: "user"
        });
        if (list) {
            posts.value = list;
        }
    } catch (error) {
        console.log(error);
    }
};
// Subcribe to the posts collection
const subscribeToAllPosts = async () => {
    await $pb?.realtime.subscribe("posts", async function (e) {
        await getPostList();
    });
};
// Unsubscribe fromt he posts collection
const unsubscribeToAllPosts = async () => {
    await $pb?.realtime.unsubscribe("posts");
};
onMounted(async () => {
    await getPostList();
    await subscribeToAllPosts();
});
onUnmounted(async () => {
    await unsubscribeToAllPosts();
});
</script>
<template>
    <div>
        <h1 class="mb-3 text-2xl">Feed</h1>
        <div class="grid grid-cols-3">
            <div v-for="post in posts" class="col-start-2 col-span-1">
                <IndividualPostComponent :post-data="post" />
            </div>
        </div>
    </div>
</template>

Zoals je kunt zien is de code niet al te ingewikkeld. We doen grofweg drie dingen:

Eerst gebruiken we de getPostList-functie om alle beschikbare posts op te halen, deze worden weergegeven zodra u op de feedpagina belandt.

Ten tweede abonneren we ons op de hele verzameling posts door de functie 'subscribeToAllPosts' te gebruiken. Dus als iemand nu een nieuw bericht maakt, wordt de functie getPostList opnieuw aangeroepen om de hele lijst bij te werken. Dit is niet super geoptimaliseerd omdat we niet echt alle posts willen ophalen, alleen de nieuwe die is toegevoegd. Ik heb dit als oefening voor de lezer gelaten.

Ten slotte noemen we de unsubscribeToAllPosts wanneer we de pagina verlaten om wat op te ruimen.

Ga je gang en start de Vue dev-server en de PocketBase-server, open twee verschillende vensters, log in met twee verschillende gebruikers en laat de ene een nieuw bericht maken en de andere op de feedpagina. Je zou nu het nieuwe bericht direct onderaan de feed moeten zien!

Conclusie

In deze blogpost hebben we gekeken naar PocketBase als alternatieve back-end in plaats van Firebase en Supabase. Hoewel de voorbeeld-Vue-app die we hebben gebouwd vrij eenvoudig is, lijkt het een zeer capabele tool te zijn voor de meeste SaaS-applicaties die er zijn.

De combinatie van autorisatie (inclusief OAuth2-providers), databasebeheer, bestandsopslag en realtime-mogelijkheden maken het het overwegen waard. Vooral als je het belangrijk vindt om alle gegevens die je gebruiker produceert te zelf te bezitten en het niet erg vindt om zelf te hosten.

Als je deze voorbeeld-app wilt uitbreiden, kun je de code voor dit project hier vinden op GitHub. Of als deze blog je heeft geïnspireerd en je iemand nodig hebt om met je samen te werken aan je volgende op PocketBase gebaseerde project, kun je hier contact met ons opnemen: Contact