Criando um chat em 25 minutos com NuxtJS + Vuetify + Firebase
Criando um chat em 25 minutos com NuxtJS + Vuetify + Firebase
NuxtJS como já sabemos é um framework incrível feito com VueJS. Ele facilita muito o trabalho e o objetivo dele é de trabalharmos o máximo nos arquivos *.vue
Vamos aprender agora como construir uma aplicação de chat em tempo real com ele, mais o framework de UI Vuetify e o Firebase com seu Realtime Database.
Dependências
Inicialmente, nosso package.json começa com:
{
"name": "chat",
"dependencies": {
"@nuxtjs/firebase": "^7.4.1",
"firebase": "^8.2.6",
"nuxt": "^2.14.12"
},
"devDependencies": {
"@nuxtjs/vuetify": "^1.11.3"
}
}Temos então as dependências:
@nuxtjs/firebase- Módulo doNuxtpara ajudar na integração com os serviços do Firebasefirebase- O Firebase em si, que é usado pelo@nuxtjs/firebasenuxt- Não preciso dizer nada né?@nuxtjs/vuetify- Por último mas não menos importante, nosso Framework de UIVuetify. Lembrando que com NuxtJS você pode usar qualquer framework de UI, onpx create-nuxt-app <appname>te dá um lista de opções.
Roda aí o npm ci pra instalar as dependências.
ci e não install? Porque o ci instala a partir do package-lock.json. Isso evita que o npm tente atualizar os pacotes definidos no package.json. É útil para builds automáticos em Integração Contínua. Fica a dica.Scripts
Bem, como é pra ser simples, vamos usar apenas o comando nuxt por enquanto. Pra saber dos outros comandos leia aqui.
{
...
"scripts": {
"dev": "nuxt"
}
...
}nuxt.config.js
O Nuxt usa um arquivo onde definimos algumas configurações para ele. Ele funciona sem esse arquivo, mas muito provavelmente você terá que utilizar.
export default {
ssr: false, // Não será Server Side Render
target: 'static',
modules: [
'@nuxtjs/firebase'
],
buildModules: [
'@nuxtjs/vuetify'
],
firebase: {
config: {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
},
services: {
auth: {
initialize: {
onAuthStateChangedMutation: 'auth/SET_USER'
},
},
database: true
}
}
}Sobre porque temos ssr: false só checar aqui
databaseURL após criar o banco. As configurações do Firebase são obrigatórias para o módulo @nuxtjs/firebase. Por motivos óbvios né.modules e buildModules
Nem todo módulo do Nuxt precisa ser executado em runtime pela aplicação, alguns bastam ser executados no build. Temos nesse caso o módulo @nuxtjs/vuetify. Já o módulo @nuxtjs/firebase ele é executado em runtime, pois ele inicializa o Firebase para ser utilizado pela nossa aplicação.
Firebase Services
No nosso arquivo nuxt.config.js, criamos uma seção firebase onde definimos algumas configurações para o módulo @nuxtjs/firebase.
export default {
...
firebase: {
config: {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
},
services: {
database: true,
auth: {
initialize: {
onAuthStateChangedMutation: 'auth/SET_USER'
},
}
}
}
...
}Observe que além do config, que é onde definimos as configurações de conexão com Firebase. temos também a services, que definimos quais serviços vão ser inicializados pelo módulo. No caso vamos usar o database e o auth. Pra ativar um serviço basta passar true como foi no caso do database, mas se precisar de alguma configuração extra, podemos incluir como foi o caso do auth.
Com onAuthStateChangedMutation dizemos para o módulo chamar uma mutation em caso de alteração do auth no Firebase. Assim, não precisamos configurar nada diretamente, o módulo faz pra gente. É possível usar uma action também. Veja aqui como.
Pra saber todos os serviços só ver aqui.
Layouts
Pois bem, módulos configurados. Vamos para nossos layouts. Vamos criar 2 layouts. Um será o padrão e outro usado apenas na página de login.
Esse será o layout padrão:
<template>
<v-app>
<v-app-bar app>
<v-avatar>
<img :src="$store.state.auth.user.photoURL" />
</v-avatar>
<v-btn text @click="logout()">Sair</v-btn>
</v-app-bar>
<v-main app>
<nuxt />
</v-main>
</v-app>
</template>
<script>
export default {
methods: {
logout() {
this.$fire.auth.signOut()
this.$router.replace('/')
}
}
}
</script>Observe que já estamos usando a store com $store.state.auth.user.photoURL. Mas não se preocupe que vamos definir isso mais tarde.
Observe também que usamos o $router. O Nuxt já vem integrado com o Vue Router.
A props app passada para v-app-bar e v-main servem para o Vuetify identificar componentes do layout e manipular corretamente. Veja mais detalhes aqui
O componente <nuxt /> é responsável por indicar no nosso layout, onde as rotas serão renderizadas.
Para o nosso layout de login:
<template>
<v-app>
<v-main app>
<nuxt />
</v-main>
</v-app>
</template>Pages
Agora vamos para as nossas páginas ou rotas se preferir chamar assim.
A primeira será a nossa home que na verdade será a página de login. Então criamos o diretório pages e dentro dele o arquivo index.vue.
Home
<template>
<v-container fill-height>
<v-row justify="center">
<v-btn color="primary" x-large dark @click="login"> Login </v-btn>
</v-row>
</v-container>
</template>
<script>
export default {
layout: 'login',
methods: {
async login() {
const provider = new this.$fireModule.auth.GoogleAuthProvider();
await this.$fire.auth.signInWithPopup(provider);
},
},
watch: {
'$store.state.auth.user'() {
if (this.$store.state.auth.user) {
this.$router.replace('/chats');
}
},
},
beforeMount() {
if (this.$store.state.auth.user) {
this.$router.replace('/chats');
}
},
};
</script>A página contém só um botão pra fazer login. E temos um watch para quando o usuário estiver logado, substituir a página atual pela página que lista os chats.
E claro, temos também o beforeMount para quando estiver acessando ou dando refresh na página na rota / e já exista usuário logado, substituímos a página com a página de chats. Isso é só pra evitar um usuário logado de acessar a página de login, já que isso não faria sentido.
$fire vs $fireModule
Existem maneiras diferentes de acessar certos recursos do Firebase pelo módulo @nuxtjs/firebase. Pra saber qual recurso usar de acordo com a documentação do Firebase, só dar uma olhada aqui
Layout
Observe que definimos explicitamente o layout para a página do login com layout: 'login'. Para as demais páginas o Nuxt vai usar o layout default.
Chats
Essa página vai listar os chats criados e também dar a possibilidade de criar um novo chat.
<template>
<v-container>
<v-row no-gutters>
<v-text-field v-model.trim="chatName" hide-details outlined class="mr-2"></v-text-field>
<v-btn color="primary" x-large :disabled="chatName === ''" @click="newChat">Novo chat</v-btn>
</v-row>
<v-row>
<v-col>
<v-card>
<v-list>
<v-list-item v-for="(chat, index) of chats" :key="index" :to="`/chat/${chat.id}`">
<v-list-item-content>
{{ chat.name }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
middleware: 'auth',
fetch() {
const chats = this.$fire.database.ref('chats')
chats.on('child_added', snapshot => {
this.chats.push({
id: snapshot.key,
...snapshot.val()
})
})
this.$on('hook:beforeDestroy', () => {
chats.off()
})
},
data() {
return {
chats: [],
chatName: ''
}
},
methods: {
newChat() {
const ref = this.$fire.database.ref('chats').push()
ref.set({
name: this.chatName
})
this.chatName = ''
}
}
}
</script>trim
Uma das coisas legais que o Vue tem é o modificador trim nos v-model. Isso faz com que automaticamente o Vue já faça o trim do input, removendo os espaços nas bordas da string. Ajuda com o problema de submeter strings vazias através de um input. Existe também o modificador number que faz com que '4' se torne um 4. Dá uma olhada aqui pra saber mais.
fetch
Isso é um hook do Nuxt, ele ajuda a definir ações de fetch para todo e qualquer componente, seja página ou não. Ele cria indicadores do estado, quando retornamos uma promise no fetch, com $fetchState como $fetchState.pending ou $fetchState.error pra ajudar a criar loadings ou mensagens de erro. Pra saber mais só olhar aqui
$on('hook:beforeDestroy')
No caso do Firebase, é necessário "desligar" certos listeners quando saímos de uma página caso contrário, continuarão funcionando. Pra evitar salvar uma instância de um ref no data e aumentar o consumo de memória pois o Vue acabaria por tornar reativo o ref, podemos usar o hook:beforeDestroy. É o mesmo que usarmos o beforeDestroy() do componente, com a diferença de que não vou precisar usar o data() pra salvar uma referência de algo pra usar no beforeDestroy() do componente.
Chat
A nossa última página, o chat em si. Pra entender o nome do arquivo, sugiro ler esse artigo
<template>
<v-container fill-height>
<v-row>
<v-col>
<v-card>
<v-toolbar color="primary" dark>
<v-btn icon @click="$router.back()">
<v-icon> mdi-chevron-left </v-icon>
</v-btn>
</v-toolbar>
<v-list max-height="400px" min-height="400px">
<v-list-item-group>
<v-list-item v-for="(message, index) of messages" :key="index">
<v-avatar color="secondary" size="32" class="mr-2">
<img :src="message.photo" v-if="message.photo" />
<span v-else>{{ message.author[0] }}</span>
</v-avatar>
<span>
<b>{{ message.author }}:</b> {{ message.message }}
</span>
</v-list-item>
</v-list-item-group>
</v-list>
<v-divider> </v-divider>
<form @submit.prevent="sendMessage">
<v-card-actions class="pa-5">
<v-row>
<v-text-field
v-model.trim="message"
class="mr-2"
hide-details
outlined
full-width
placeholder="Digite sua mensagem"
>
</v-text-field>
<v-btn
x-large
color="primary"
:disabled="message === ''"
type="submit"
>Enviar</v-btn
>
</v-row>
</v-card-actions>
</form>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
middleware: 'auth',
fetch() {
this.room = this.$fire.database.ref(
`/chats/${this.$route.params.id}/messages`
);
this.room.on("child_added", (snapshot) => {
this.messages.unshift(snapshot.val());
this.message = "";
});
this.$on("hook:beforeDestroy", () => {
this.room.off();
});
},
data() {
return {
room: null,
messages: [],
message: "",
};
},
methods: {
sendMessage() {
this.room.push({
author: this.$store.state.auth.user.displayName,
photo: this.$store.state.auth.user.photoURL,
message: this.message,
});
},
},
};
</script>Usamos um <form> porque é mais conveniente apertar <ENTER> pra enviar mensagem. E usamos o modificador .prevent no @submit pra obviamente prevenir o comportamento padrão do <form> quando ele é submetido.
Middleware
Pois bem, construídas as páginas, vamos ao nosso middleware. Ele será responsável por permitir ou não o acesso a uma página onde fica definido o seu uso.
export default ({ store, redirect }) => {
if (!store.state.auth.user) {
redirect('/')
}
}Ele é bem simples. Um middleware no Nuxt recebe um contexto como parâmetro, nós descontruímos esse contexto para só trabalhar com o store do Vuex e com a função de redirect.
Agora nas nossas páginas, você deve ter percebido que definimos o uso desse nosso middleware apenas passando: middleware: 'auth':
<template>
...
</template>
<script>
export default {
middleware: 'auth',
...
}
</script>Store
Por último, vamos criar nosso store do Vuex. Não há nada complicado aqui, basta criar um arquivo auth.js dentro do diretório store que o Nuxt passará a usar o Vuex e já criará um módulo Vuex com namespace auth (que é o mesmo nome do arquivo)
export const state = () => ({
user: null
})
export const mutations = {
SET_USER(state, { authUser }) {
if (authUser) {
const { displayName, photoURL } = authUser
state.user = {
displayName,
photoURL
}
} else {
state.user = null
}
}
}No nosso módulo, definimos o state e uma mutation.
Essa mutation é usada pelo @nuxtjs/firebase lembra? Configuramos ele pra usar essa mutation quando houver alteração no auth do firebase.
Executar
Isso é tudo, agora só rodar npm run dev e ver nossa aplicação funcionando.
Repositório
O código dessa aplicação se encontra no GitHub.