Quasar Framework — a SSR+PWA app with dynamic data.

Tobias Mesquita
11 min readOct 6, 2019

--

Note: This article was originally released on Dev.to.

Table Of Contents

1 Introduction

We’ll build an SSR app that will manage a small CRUD, but the whole CRUD will work offline. To be able of to do that, we’ll use PouchDB to persist everything at the client’s browser. Then, on the server side, we’ll directly query the CouchDB.

We’ll use a Quasar app extension that will help us to create the stores and the pages we’ll need. If you want to read more about app extensions, check the follow link: Quasar — Utility Belt App Extension to speedup the development of SSR and offline first apps.

2 CouchDb

Our first step, is to install a CouchDb Instance. Go to CouchDb Home Page and follow the instructions.

The exact step-by-steps to install CouchDB will depend of your OS. If you’re on Windows, it will be as simple as next > next > finish wizard. If you're on Linux, you'll need to execute some commands in your terminal. That will take some time, but you should be used to it.

To check if everything is working as expected, you would access: http://localhost:5984/_utils, a page like the below one will appear.

3 Quasar Project

First of all, I really recommend you to use yarn to manage your local packages and npm for the global ones, but you're free to use your preferred package manager.

Our first step is make sure the @quasar/cli is installed and up-to-date, so even if you already have the cli installed, please run the follow command.

$ npm i -g @quasar/cli@latest

We can now create a new project, run the following command:

$ quasar create quasar-offline

here is what I selected:

? Project name (internal usage for dev) quasar-offline
? Project product name (official name; must start with a letter if you will build mobile apps) Quasar App
? Project description A Quasar Framework app
? Author Tobias de Abreu Mesquita <tobias.mesquita@gmail.com>
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)ESLint, Vuex, Axios, Vue-i18n
? Pick an ESLint preset Standard
? Cordova id (disregard if not building mobile apps) org.cordova.quasar.app
? Should we run `npm install` for you after the project has been created? (recommended) yarn

Besides the Vuex feature, you aren’t bound to any of those options, so feel free to select what you might already do normally.

4 Preparing

4.1 Utility Belt App Extension

$ quasar ext add "@toby-mosque/utils"

4.2 Installing dependencies

Since we’re planning to use the PouchDB to persist everything at the client-side, we need to install the required packages.

$ yarn add pouchdb pouchdb-find relational-pouch worker-pouch

4.3 Setup

We need to do a few small changes in the project (ok, we’ll do a workaround/macgyver).

Edit your ./babel.config.js to look like:

Open your ./quasar.conf.js and extend the webpack with the follow line:

Here a simplified view of the ./quasar.conf.js.

5 Configuring PouchdDb

5.1 Creating a Boot File

Following the Quasar’s philosophy, in order to configure anything, you would create a boot with that single responsibility.

$ quasar new boot pouchdb/index

You need to register the boot file in the ./quasar.conf.js

5.2 Installing the PouchDb plugins

We’ll install the pouchdb’s plugins in a separated file:

Create ./src/boot/pouchdb/setup.js and modify it to look like this:

Now, edit the ./src/boot/pouchdb/index.js

What are we doing here? We need a slightly different behavior when the code is running at the client-side when compared to the server-side.

When at the server-side, the app will query the CouchDb instance directly.
When at the client-side, the app will rely only on the local database and sync whenever a connection is available.

5.3 Configuring your database schema

One of the common mistakes what devs do when starting with PouchDb/CouchDb, is create a table for each doc type (based on personal experience), but soon they will figure out that this isn't a good idea. Each database needs a dedicated connection in order to sync properly.

To solve that problem, we will persist everything in a single table. Personally, I believe it is easy to think about the data in a relational way, so we’ll use a PouchDB plugin to abstract that: relational-pouch

We already registered the plugin in the previous step, but we still need to configure the database schema. Again, we’ll do that in a separate file:

Create ./src/boot/pouchdb/create.js and modify it to look like this:

One more time, edit the ./src/boot/pouchdb/index.js

5.4 Seeding the database

Now, let’s seed our database with some data. We’ll do that only at the server-side. And again, we’ll do that in a separate file:

In order to generate our data (for this article), we’ll use FakerJS

yarn add faker

Create ./src/boot/pouchdb/seed.js and modify it to look like this:

Now call the seed when the boot is running at the server-side:

5.5 Sync the database

Finally, we need to sync the data between the remote and the local databases.

When the app starts, before anything, we will try to do a complete replication. To make that task more clear, we’ll wrap the replication method inside a promise:

We’ll verify if the app is online and try to do a complete replication (remember, the client has to be online for this action). If something goes wrong, it is because the client is offline or the CouchDB, but that wouldn’t prevent the user from accessing the system.

After that, we’ll start the live replication and track any changes.

Now your boot file would look like this:

5.6 How your project would look like?

6 CouchDb

6.1 Accessing the CouchDb from the App

If you try to run your app, you’ll notice than CouchDB is refusing any connection from the client-side. Here you have two options; configure your app to act as a reverse proxy of the CouchDB, or configure the CORS of your CouchDb instance.

6.1.1 Alternative 1 — Configuring the CORS

Open the Fauxton (http://localhost:5984/_utils), go into the configurations, CORS, and enable it.

6.1.2 Alternative 2 — Reverse Proxy

Install the follow package

yarn add --dev http-proxy-middleware

Edit your ./src-ssr/extention.js to look like this:

Edit your boot file:

6.1.3 Silver Bullet

You don’t know what alternative to pick? Use the reverse proxy, since that will give to you more freedom.

6.2 Testing the Access

Run your app:

$ quasar dev -m ssr

Now check your console. If you see a list with 100 persons, everything is running as expected.

7 Centralized Data

7.1 Store

Since this is an SSR app, we don’t want to query the whole database at the server-side, but would be a good idea to query the domain entities. We’ll handle the job and company entities as being our domain entities (since they are used in all routes).

Our first step, is create a store (using Vuex) to hold the both collections:

src/store/database.js

src/store/index.js

7.2 Emitting Events

Since our data is being synced with a remote database in real-time, the CRUD operations will happen outside of our store. Because of that, we need to track them and emit events to update our centralized store every time that happens.

In order to do that, we need to modify the boot file: ./src/boot/pouchdb/index.js

7.3 Explanation

let’s imagine that someone updated a person, in that case the change object will look like:

In order to properly index the docs, the relational-pouch plugin modifies the id before of save, appending the type of doc and the type of the key (2 means the key is a string). sWe need break it down in order to get the type of the doc and your id.

Now, we will emit 2 events to inform the app that some document got updated.

  1. The first one, is meant to inform components who hold a collection of records, the event name is the type.
  2. The second one, is meant to inform components who hold the details of a specific record, the event name is the record id (that is unique across the app).

Our last step, is update the centralized store. We will dispatch an action that will update the store:

8 Setting the Framework

Let’s configure the framework to use the preFetch and auto discovery the components. Set the config > preFetch to true and config > framework > all to 'auto'. Here a simplified view of the ./quasar.conf.js

9 Listing the People

We already have some data working and the syncing process is configured. Let’s create some pages. But first, we need to update the src/router/routes.js file to look like.:

9.1 Configuring the Route

9.2 Creating a View

Now, create the src/pages/People/Index.vue file to look like this:

9.3 Adding a State Container and an Empty Page

We need to create src/pages/People/Index.vue.js. Out first step will be create a state container and an empty page:

If you’re worried that the remove action didn't commit anything, that is intentional. Since we'll be listening for changes, as soon a person gets deleted (no matter who, where and/or when), it will be reflected at the state container.

9.4 Listening for Changes

In order to listen for any changes at the people collection, we’ll need to update the mounted and destroyed hooks, and enable/disable some events listeners.

Doing this, every time when a person gets created, updated or deleted, the state container will be updated, regardless of the origin of the modification.

9.5 Table and Columns

Since we’re using a table to display the people, we will need to configure our columns, six in total (firstName, lastName, email, job, company, actions).

But, the job and company fields didn't hold the descriptions, but ids, we'll need to map them to your respective descriptions. We'll need to edit the computed properties to look like:

Now, we’ll create the columns definitions inside the data hook

9.6 Actions

It’s time to configure our actions. To be exact, our unique action: delete a person. We’ll edit our methods hook to look like this:

9.7 Screenshots

10 Editing a Person

10.1 Creating a View

Create the src/pages/Person/Index.vue file, and edit it to look like this:

10.2 Adding a State Container and an Empty Page

We need to create src/pages/Person/Index.vue.js, our first step will be create a state container and a empty page:

Again, don’t worry with the save. The lack of a commit is intentional, since we'll be listening for changes. As soon as the current person gets modified (no matter who, where and/or when) the page will be notified.

10.3 Listening for Changes

In order to listen for any changes to the current person, we’ll need to update the mounted and destroyed hooks, and enable/disable some event listeners.

But unlike what we did before, we’ll only notify the application and let the user decide what they want to do.

Doing this, every time the current person gets updated or deleted, the user will then be notified, regardless of the origin of the modification.

10.4 Data Sources

Like before, the job and company fields didn't hold the descriptions, but ids. But now we need the entire collection of jobs and companies in order to fetch the QSelect options.:

10.5 Actions

Now, it’s the time to write our save method. We’ll edit our methods hook to look like:

10.6 Screenshots

11 Wrapping the PouchDB instance with a Worker

Until now, all DB operations are being made in the main thread, that includes queries, updates, deletes, sync, etc.

If you have a large database and you’re creating or updating documents often, your UI can suffer from constant blocking, that will result in a poor user experience.

Anyway, I really recommend you move any DB operations to a separate thread. to achieve that you’ll need this package:

yarn add worker-pouch

11.1 Web Worker

This is the basic setup. Your first step is to verify if the worker adapter is configured. Just open the src/boot/pouchdb/setup.js and look for:

Our second step, is to configure the local database to use the worker adapter. Just open src/boot/pouchdb/input.js and replace:

with

Done, for now, all our DB operations are now in a separated worker thread.

11.2 Shared Worker

The biggest problem with the synchronous process is if you had multiple browser tabs opened, they will all access a single instance of the LocalStorage. If you update a document in one of the tabs, the others tabs will not be notified.

If you want all of your tabs notified, you’ll need to use a SharedWorker. In this case, you'll have only one worker for all the tabs.

TODO: waiting https://github.com/GoogleChromeLabs/worker-plugin/pull/42 to be merged.

11.3 Service Worker

Besides the name of this article, until now our app isn’t a PWA. Let’s change that. Open the ./quasar.conf.js and set the ssr > pwa to true.

Now, the workbox is configured and our app has a Service Worker, but we haven’t great control over it, anyway we can change that. Open your ./quasar.conf.js and configure your pwa > workboxPluginMode to be InjectManifest:

Now, we need to edit the ./src-pwa/custom-service-worker.js to look like this:

In order to move the DB operations into the Service Worker, we need to configure the webpack, so it'll be able to transpile some dependencies.

yarn add --dev serviceworker-webpack-plugin

Edit ./quasar.conf.js one more time:

Now, create the ./src-pwa/pouchdb-service-worker.js and edit your content to be like:

Finally, modify the ./src-pwa/custom-service-worker.js in order to import the worker-pouch related scripts and register them:

We need to modify our ./src/boot/pouchdb/index.js so the local pouchdb instance points to the Service Worker:

If you check your network tab, it should now look like:

11.4 Silver Bullet

You don’t know what worker to pick? Use the SharedWorker, since that didn't have drawbacks over the DedicatedWorker and the ServiceWorker will not stay active after the app is closed.

12 Syncing when the App is closed

That is just a Overview

The Service Worker will stay active only while the app is open. Even if we move the DB operations to run inside the Service Worker the sync will stop as soon the app is closed.

To let the DB be synced even when the app is closed, we’ll need to turn our server in a push-server using the web-push, after that, we need to sign the clients to the push server.

After the push is configured, we can configure a cron job to send a push periodically (like each 30 minutes), and the client will start the sync process every time it receives a notification.

13 Repository

You can check the final project here:
https://gitlab.com/TobyMosque/quasar-couchdb-offline

Interested in Quasar? Here are some more tips and information:

More info: https://quasar.dev
GitHub: https://github.com/quasarframework/quasar
Newsletter: https://quasar.dev/newsletter
Getting Started: https://quasar.dev/start
Chat Server: https://chat.quasar.dev/
Forum: https://forum.quasar.dev/
Twitter: https://twitter.com/quasarframework
Donate: https://donate.quasar.dev

--

--