DEV Community

Tobias Mesquita for Quasar Framework Brasil

Posted on • Updated on

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
}

Discussion (0)