DEV Community

Tobias Mesquita for Quasar Framework Brasil

Posted on • Edited on

1 1

QPANC - Parte 12 - Quasar - Serviços

QPANC são as iniciais de Quasar PostgreSQL ASP NET Core.

24 Injetar objetos nos componentes, stores e routes.

Para o correto funcionamento de uma aplicação SSR, alguns serviços precisam ser instanciados de maneira isolada, de forma, que um usuário não consiga acessar a uma instancia destinada a outro usuário, em resumo, todos os serviços devem está preferencialmente isoladas dentro do escopo do usuário.

O primeiro passo para conseguir este objetivo, é criar um utilitário que irá injetar estes serviços nos components, store e routes.

QPANC.App/src/boot/inject.js

import Vue from 'vue'

const mixins = []
const inject = function (bootCb) {
  return async function (ctx) {
    const { app, router, store } = ctx
    let boot
    if (typeof bootCb === 'function') {
      const response = bootCb(ctx)
      boot = response.then ? await response : response
    } else {
      boot = bootCb
    }

    for (const name in boot) {
      const key = `$${name}`
      if (mixins.indexOf(name) === -1) {
        mixins.push(name)
        Vue.mixin({
          beforeCreate () {
            const options = this.$options
            if (options[name]) {
              this[key] = options[name]
            } else if (options.parent) {
              this[key] = options.parent[key]
            }
          }
        })
      }
      app[name] = boot[name]
      store[key] = boot[name]
      router[key] = boot[name]
    }
  }
}

export default inject

Agora, um exemplo de utilização.:

Digamos que tenhamos um serviço que faz o print da mensagem Hello World no console.:

class DummieService {
  sayHello ({ name }) {
    console.log(`${name}: Hello Wolrd`)
  }
}

E agora queremos injetar uma instancia do serviço acima em todos os componentes, stores e routes, poderemos faze-lo em um arquivo de boot.

import inject from './inject'
import DummieService from 'services/dummie'

export default inject(({ Vue }) => {
  const dummie = new DummieService()
  return {
    dummie: dummie
  }
})

feito isto, poderemos acessar this.$dummie nos componentes e stores, assim como router.$dummie nos navigation guards, segue alguns exemplos.:

sample/page/dummie.js

export default {
  data () {
    const initialMsg = this.$dummie.sayHello({ name: 'me' })
    return {
      msg: initialMsg
    }
  },
  methods: {
    changeName ({ name }) {
      this.msg = this.$dummie.sayHello({ name })
    }
  }
}

sample/store/dummie.js

export default {
  namespaed: true,
  state () {
    return {
      name: 'me',
      msg: ''
    }
  },
  mutations: {
    name (state, value) { state.name = value },
    msg (state, value) { state.msg = value }
  },
  actions: {
    setMessage ({ commit }, name) {
      const msg = this.$dummie.sayHello({ name })
      commit('msg', msg)
    }
  },
  getters: {
    getMessage (state) {
      return this.$dummie.sayHello({ name: state.name })
    }
  }
}

sample/router/dummie.js

export default function (context) {
  return {
    path: '/dummie',
    beforeEnter (to, from, next) {
      const { $dummie } = context.router
      const msg = $dummie.sayHello({ name: to.params.name })
      if (msg.length > 50) {
        next('/hello-long-name')
      } else {
        next('/hello-short-name')
      }
    }
  }
}

25 Customizando os componentes do Quasar

Para esta tarefa, estarei usando a extensão @toby-mosque/utils, porém não irei mostrar um código equivalente sem o uso da extensão, pois envolve um transparent wrapper bem intricado, onde um pequeno deslize, pode levar a um bug difícil de rastrear ou a um comportamento indesejado.

Para esta demostração, estaremos personalizando apenas o QInput, mas podemos customizar qual quer componente, inclusive aqueles que são instalados através de extensões.

O primeiro passo, é criar um boot, aqui chamaremos ele de brand

quasar new boot brand

QPANC.App/quasar.config.js

module.exports = function (ctx) {
  return {
    boots: [
      'brand'
    ]
  }
}

QPANC.App/src/boot/inject.js

import { factory } from '@toby.mosque/utils'
import inject from './inject'
import { QInput } from 'quasar'

// "async" is optional
export default inject(({ Vue }) => {
  const brand = {}
  brand.input = Vue.observable({
    /*
    style: {
      'font-size': '12px'
    },
    class: {
      'custom-input': true
    },
    */
    props: {
      outlined: true
    }
  })

  factory.reBrand('q-input', QInput, brand.input)
  return {
    brand
  }
})

O objeto brand será injetado nos componentes e stores, então poderemos acessa-lo futuramente.

A propriedade input é um Vue.observable, estão qual quer alteração nele, será refletido para os componentes que fazem uso dele. input é apenas um nome, poderia ser qual quer coisa no lugar.

O factory.reBrand, é o responsável por injetar o brand.input em todos os q-input. Ele fará uso apenas das propriedades style, class e props, onde estas propriedades serão injetadas no style, class e props do respectivo componente, sendo que nenhum deles é obrigatório.

Caso execute a aplicação agora, verá que todos os inputs estarão com a propriedade :outlined="true"

Alt Text

Agora, vamos adaptar o exemplo acima, para usar o Dark Mode, onde os inputs deverão ser filled no modo dark e outlined no light

QPANC.App/src/boot/inject.js

import { factory } from '@toby.mosque/utils'
import inject from './inject'
import { QInput, Dark } from 'quasar'

// "async" is optional
export default inject(({ Vue }) => {
  const brand = {}
  brand.input = Vue.observable({
    /*
    style: {
      'font-size': '12px'
    },
    class: {
      'custom-input': true
    },
    */
    props: {
      filled: Dark.isActive
      outlined: !Dark.isActive
    }
  })

  factory.reBrand('q-input', QInput, brand.input)
  return {
    brand
  }
})

O problema aqui, é que o Dark.isActive está sendo usado apenas como o valor inicial para o brand.input, porém quando o Dark.isActive é alterado, ele não é propagado para o brand.input.

Então, precisaremos de um watch no App.vue.

QPANC.App/src/App.vue

export default {
  name: 'App',
  watch: {
    '$q.dark.isActive' () {
      this.$brand.input.props.filled = this.$q.dark.isActive
      this.$brand.input.props.outlined = !this.$q.dark.isActive
    }
  }
}

E por fim, algumas prints.:

Alt Text
Alt Text

25 Persistência em Cookies

Como se trata de uma aplicação SSR, é natural que alguns dados serão persistidos no lado do cliente, coisas como o Token JWT, o tema e o idioma preferido.

Porém, alguns destes dados precisam ser acessados no lado do servidor, e como não podem ser recuperados de outra forma, teremos de usar Cookies.

O primeiro passo, será ativar o plugin responsável por ler os Cookies.

QPANC.App/quasar.config.js

module.exports = function (ctx) {
  return {
    framework: 
      plugins: [
        'Cookies'
      ]
    }
  }
}

Agora, iremos adicionar um plugin para o vuex, no caso o vuex-persistedstate

yarn add vuex-persistedstate

Agora, adicione o boot persist, e não deixe e adicionar ele ao quasar.config.js > boots, é vital que ele seja adicionado antes do boot do axios e do i18n.

quasar new boot persist

QPANC.App/quasar.config.js

module.exports = function (ctx) {
  return {
    boots: [
      'persist',
      'i18n',
      'axios'
    ]
  }
}

Antes de codificamos o boot persist, precisamos criar um pequeno serviço, que será responsável por detectar o idioma recomendado para o usuário.

QPANC.App/src/services/locales.js

const locales = ['en-us', 'pt-br']
const regions = {
  en: 'en-us',
  pt: 'pt-br'
}
const fallback = regions.en
const detectLocale = function () {
  if (process.env.CLIENT) {
    const locale = navigator.language.toLowerCase()
    if (locales.includes(locale)) {
      return locale
    }
    const region = locale.split('-')[0]
    if (region in regions) {
      return regions[region]
    }
    return regions.en
  } else {
    return fallback
  }
}

export {
  locales,
  regions,
  fallback,
  detectLocale
}

Agora, vamos ao boot:

QPANC.App/src/boot/persist.js

import { Cookies, Quasar } from 'quasar'
import createPersistedState from 'vuex-persistedstate'

const persistState = function ({ name, store, storage }) {
  createPersistedState({
    key: name,
    paths: [name],
    filter ({ type }) {
      return type.startsWith(name)
    },
    storage
  })(store)
}

export default function ({ store, ssrContext }) {
  const cookies = process.env.SERVER
    ? Cookies.parseSSR(ssrContext)
    : Cookies

  const cookieStorage = {
    getItem (key) {
      return JSON.stringify(cookies.get(key))
    },
    setItem (key, value) {
      cookies.set(key, value, { path: '/' })
    },
    removeItem (key) {
      cookies.remove(key, { path: '/' })
    }
  }

  persistState({ name: 'app', store, storage: cookieStorage })
  if (process.env.CLIENT) {
    // persistState({ name: 'local', store, storage: window.localStorage })
    store.commit('app/localeOs', detectLocale())
  }
}

Um pequeno detalhamento sobre o que está sendo feito:

persistState({ name: 'app', store, storage: cookieStorage })

Estamos instruindo o vuex-persistedstate à persistir todo o modulo app em um Cookie

if (process.env.CLIENT) {
  // persistState({ name: 'local', store, storage: window.localStorage })
}

Caso o comentário seja removido, estamos instruindo o vuex-persistedstate à persistir todo o modulo local no localStorage

if (process.env.CLIENT) {
  store.commit('app/localeOs', detectLocale())
}

Estamos atualizando o localeOs para que ele seja igual ao locale disponível mais próximo ao que é utilizado pelo browser, este vai ser o idioma da aplicação, caso o usuário também não especifique o localeUser.

Porém vale lembrar, que o código no cliente é executado após a execução no servidor, então, na primeira requisição, o servidor irá sempre utilizar a linguagem padrão, no caso, o inglês (isto é configurável em quasar.config.js > framework > lang).

Desta forma, na primeira requisição, o app será carregado usando a linguagem padrão, e irá alternar para a linguagem informada pelo browser após a conclusão do carregamento da pagina.

Agora, vamos criar o nosso modulo app

QPANC.App/src/store/app.js

import { factory } from '@toby.mosque/utils'

class AppStoreModel {
  constructor ({
    token = '',
    localeOs = '',
    localeUser = ''
  } = {}) {
    this.token = token
    this.localeOs = localeOs
    this.localeUser = localeUser
  }
}

const options = {
  model: AppStoreModel
}

export default factory.store({
  options,
  getters: {
    locale (state) {
      return state.localeUser || state.localeOs
    }
  }
})

export { options, AppStoreModel }

Como se trata de um modulo global, não esqueça de registra-lo no QPANC.App/src/store/index.js

QPANC.App/src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import app from './app'

Vue.use(Vuex)

export default function (context) {
  const Store = new Vuex.Store({
    modules: {
      app
    },
    strict: process.env.DEV
  })

  return Store
}

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay