Nuxt.js é um framework de código aberto para aplicações web e é baseado em Vue.js, Node.js, Webpack e Babel.
Foi inspirado no Next.js, que tem estrutura e propósito semelhante, baseado em React.js.
Vue.js é um framework JavaScript criado por um desenvolvedor da Google, que trabalhava no time de desenvolvimento do Angular;
Tem um curva de aprendizado pequena: HTML, CSS e JavaScript;
É versátil, pois você pode trabalhar com o simples e adicionar outras bibliotecas próprias ou de terceiros;
Por fim, é performático, rápido e leve (20k).
webpack é um empacotador de módulo JavaScript de código aberto.
O webpack pega módulos com dependências e gera conteúdos estáticos representando esses módulos.
Também permite dividir seu código em múltiplos módulos para serem lidos sob demanda.
O Babel é um transcompilador JavaScript de código aberto, usado principalmente para converter o código ECMAScript 2015+ em uma versão compatível com versões anteriores do JavaScript que pode ser executada por mecanismos JavaScript mais antigos.
# usando yarn
yarn create nuxt-app notes-app
# usando npm
npm init nuxt-app notes-app
{
"name": "notes-app",
"version": "1.0.0",
"private": true,
"scripts": {
// inicia servidor de desenvolvimento
"dev": "nuxt",
// constroi a aplicação usando webpack
"build": "nuxt build",
// inicia o servidor de produção
"start": "nuxt start",
// gera um arquivo HTML para cada rota da aplicação
"generate": "nuxt generate"
},
}
export default {
head: {
title: 'notes-app',
htmlAttrs: {
lang: 'pt-br'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
}
O diretório pages contém a camada de visualização da aplicação. O Nuxt.js lê todos os arquivos .vue dentro desse diretório e automaticamente cria uma rota para ele dentro do servidor.
<template>
<div class="container">
<div class="row">
<div class="col-md-3">
<div class="card bg-warning my-3">
<div class="card-body">
<h5 class="card-title">Título da Nota</h5>
<p class="card-text">
Descrição da nota
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<style>
.card {
min-height: 15rem;
}
</style>
<script>
export default {
data() {
return {
notas: [
{
id: 1,
titulo: "Título da nota",
descricao: "Descrição da nota"
},
{
id: 1,
titulo: "Título da nota 2",
descricao: null
}
]
};
}
};
</script>
<template>
<div class="container">
<div class="row">
<div v-for="nota of notas" :key="nota.id" class="col-md-3">
<div class="card bg-warning my-3">
<div class="card-body">
<h5 class="card-title">{{ nota.titulo }}</h5>
<p v-if="nota.descricao" class="card-text">
{{ nota.descricao }}
</p>
</div>
</div>
</div>
</div>
</div>
</template>
O termo hooking descreve uma série de técnicas utilizadas para modificar ou melhorar o comportamento de um sistema operacional, aplicações ou outros componentes de software através da interceptação de chamadas de funções, mensagens ou eventos passados entre componentes de software.
Para carregar dados assíncronos, como dados vindos de uma API, utilizaremos o hook chamado fetch, que acontece após a criação da instância do componente.
Esse hook nos devolverá uma promise, por isso utilizaremos async/await.
export default {
//...
axios: {
baseURL: "https://localhost:4443"
},
};
yarn add @nuxtjs/axios
//ou
npm install @nuxtjs/axios
<script>
export default {
data() {
return {
notas: []
};
},
async fetch() {
this.notas = await this.$axios
.get("nota/usuario/3")
.then(res => res.json());
}
};
</script>
O hook fetch também nos permite acessar algumas propriedades durante e após o processamento dos dados, como o estado do processo que está sendo realizado.
<template>
<div class="container">
<p v-if="$fetchState.pending">Carregando...</p>
<p v-else-if="$fetchState.error">Ocorreu um erro :(</p>
<div class="row">
<div v-for="nota of notas" :key="nota.id" class="col-md-3">
<div class="card bg-warning my-3">
<div class="card-body">
<h5 class="card-title">{{ nota.titulo }}</h5>
<p v-if="nota.descricao" class="card-text">
{{ nota.descricao }}
</p>
</div>
</div>
</div>
</div>
</div>
</template>
O Nuxt possui um módulo próprio que facilita a realização requisições de autenticação.
Com ele podemos configurar autenticações próprias ou ainda autenticações provenientes de serviços como Google, GitHub, entre outros.
Para isso basta instalar a biblioteca e configurar o projeto.
export default {
//...
auth: {
strategies: {
local: {
endpoints: {
login: { url: "login", method: "post" },
user: { url: "usuario", method: "get", propertyName: false },
logout: false
}
}
}
}
};
yarn add @nuxtjs/auth
//ou
npm install @nuxtjs/auth
<script>
export default {
data() {
return {
email: null,
senha: null
};
},
methods: {
async login() {
try {
const response = await this.$auth.loginWith("local", {
data: {
email: this.email,
senha: this.senha
}
});
this.$router.push("/");
} catch (e) {
this.error = e.response.data.message;
}
}
}
};
</script>
<div class="container-fluid">
<div class="row">
<div class="col-md-7 d-flex vh-100 justify-content-center">
<div class="col-md-5 align-self-center">
<h1 class="text-center mb-5">Notes App</h1>
<p class="text-center">Informe os dados abaixo para acessar</p>
<b-form @submit.prevent="login">
<b-form-group>
<b-form-input
v-model="email"
type="email"
placeholder="E-mail"
required
></b-form-input>
</b-form-group>
<b-form-group>
<b-form-input
v-model="senha"
type="password"
placeholder="Senha"
required
></b-form-input>
</b-form-group>
<b-button block type="submit" variant="primary">Acessar</b-button>
</b-form>
</div>
</div>
<div class="col-md-5 vh-100 cover"></div>
</div>
</div>
<style>
.cover {
background: url("https://url.gratis/wftZc") center center;
}
</style>
const jwt = require('jsonwebtoken');
const { secret } = require('../config/security');
module.exports = (req, res, next) => {
const [type, token] = req.headers['authorization'].split(' ');
//...
};
router.get('/', async (req, res) => {
const [type, token] = req.headers['authorization'].split(' ');
const { id } = jwt.decode(token);
//...
});
auth.js
routes/usuario.js
app.use(
cors({
origin: ['http://localhost:3000'],
})
);
app.js
A biblioteca nuxtjs/auth se encarrega de obter os tokens de autenticação, assim como a interceptação das requisições enviando o token no cabeçalho.
Assim como temos um método para login, a biblioteca nuxtjs/auth também fornece um método para realizar o logout do usuário.
Quando executado, o método remove do localStorage o token obtido no processo de autenticação.
export default {
//...
auth: {
strategies: {
local: {
endpoints: {
login: { url: "login", method: "post" },
user: { url: "usuario", method: "get", propertyName: false },
logout: false
}
}
}
}
};
<template>
<div>
<b-navbar type="dark" variant="dark">
<b-navbar-brand href="#">Notes App</b-navbar-brand>
<b-navbar-toggle target="navbarNotes"></b-navbar-toggle>
<b-collapse id="navbarNotes" is-nav>
<b-navbar-nav class="ml-auto">
<b-nav-item-dropdown text="User Name" right>
<b-dropdown-item href="#" @click.prevent="logout"
>Sair</b-dropdown-item
>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<div class="container"> </div>
</div>
</template>
<script>
export default {
methods: {
async logout() {
await this.$auth.logout();
this.$router.push("/login");
}
}
};
</script>
Middlewares permitem que possamos escrever funções customizadas que serão executadas antes da renderização de uma página.
Podem ser configurados globalmente ou direto na página onde será utilizado.
export default function({ store, redirect }) {
if (store.state.auth.loggedIn) {
return redirect("/");
}
}
<script>
export default {
middleware: "auth",
//...
};
</script>
Layout é uma ferramenta ótima para alterar o visual de uma aplicação nuxt. Com ele você pode criar layouts distintos para cada tipo de página dentro de um projeto, como login, registro, início e perfil.
Por padrão aplicações vem com um layout padrão criado no diretório layouts. Se um layout não for especificado, a aplicação usará o layout default.vue para renderizar o conteúdo.
Para criar um novo layout, basta criar um arquivo .vue com o nome desejado no diretório layouts, utilizando a tag <nuxt/> onde será incorporado o conteúdo da página em que utilizaremos o layout.
<template>
<div>
<b-navbar type="dark" variant="dark">
<b-navbar-brand href="/">Notes App</b-navbar-brand>
<b-navbar-toggle target="notesBar"></b-navbar-toggle>
<b-collapse id="notesBar" is-nav>
<b-navbar-nav class="ml-auto">
<b-nav-item-dropdown :text="primeiroNome" right>
<b-dropdown-item href="perfil">Perfil</b-dropdown-item>
<b-dropdown-item href="#" @click.prevent="logout"
>Sair</b-dropdown-item
>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<div class="container">
<Nuxt />
</div>
</div>
</template>
<script>
export default {
computed: {
usuario() {
return this.$store.state.auth.user;
},
primeiroNome() {
const [nome] = this.usuario.nome.split(" ");
return nome;
}
},
methods: {
async logout() {
await this.$auth.logout();
this.$router.push("/login");
}
}
};
</script>
<script>
export default {
layout: "home",
//...
}
</script>
<template>
<div>
<div class="row">
<div class="col-12">
<h1 class="my-5">Meu Perfil</h1>
<b-card class="p-5">
<div class="row">
<div class="col-md-3 text-center">
</div>
<div class="col-md-9">
</div>
</div>
</b-card>
</div>
</div>
</div>
</template>
<div class="row">
<div class="col-md-3 text-center">
<b-img
center
rounded="circle"
src="https://picsum.photos/125/125/?image=1"
alt="Center image"
/>
<h5 class="mt-3">Nome do caboclo</h5>
</div>
<div class="col-md-9">
<!-- ... -->
</div>
</div>
<div class="row">
<div class="col-md-3 text-center">
<!-- ... -->
</div>
<div class="col-md-9">
<b-form @submit.prevent="save">
<b-form-group label-align="right" label-cols="2" label="Nome">
<b-form-input
v-model="nome"
type="text"
required
></b-form-input>
</b-form-group>
<!-- ... -->
<div class="row">
<div class="col-md-10 offset-md-2">
<b-button type="submit" variant="primary"
>Salvar Perfil</b-button
>
</div>
</div>
</b-form>
</div>
</div>
<script>
export default {
layout: "home",
computed: {
usuario() {
return this.$store.state.auth.user;
}
},
methods: {
async salvar() {
try {
let data = {
nome: this.nome,
email: this.email
};
if (this.senha) {
data = { ...data, senha: this.senha };
}
await this.$axios.put(`usuario`, data);
} catch (e) {
console.log(e);
}
}
}
};
</script>
<script>
export default {
methods: {
async salvar() {
try {
let data = {
nome: this.nome,
email: this.email
};
if (this.senha) {
data = { ...data, senha: this.senha };
}
const savedUser = await this.$axios.put(`usuario`, data);
await this.$auth.setUser(savedUser.data);
this.$router.push("/");
} catch (e) {
console.log(e);
}
}
}
};
</script>
Para melhorar a segurança de nossa API, vamos extinguir o parâmetro id da rota PUT do recurso usuário. Agora iremos recuperar o id do usuário com base no token que está trafegando nas requisições.
const jwt = require('jsonwebtoken');
const { secret } = require('../config/security');
module.exports = (req, res, next) => {
if (!req.headers['authorization']) res.status(401).send({ error: 'Token não informado' });
const [type, token] = req.headers['authorization'].split(' ');
jwt.verify(token, secret, (error) => {
if (error) return res.status(401).send({ error });
req.token = token;
next();
});
};
middlewares/auth.js
router.put('/', async (req, res) => {
try {
const { body, token } = req;
const { id } = jwt.decode(token);
const usuario = await controller.edit(Usuario, body, id);
res.send(usuario);
} catch (error) {
res.status(500).send({ error });
}
});
routes/usuario.js
const bcrypt = require('bcrypt');
const { saltRounds } = require('../config/security');
module.exports = function (sequelize, DataTypes) {
return sequelize.define(
'usuario',
{
//...
},
{
//...
hooks: {
beforeValidate: (usuario) => {
if(usuario.senha) usuario.senha = bcrypt.hashSync(usuario.senha, saltRounds);
},
},
//...
}
);
};
models/usuario.js
Construir uma página para registro dos novos usuários utilizando a funcionalidade layouts do nuxt, coletando Nome, E-mail e Senha.
Ao final, salvar no banco de dados os dados do novo usuário utilizando a API e redirecioná-lo para a página de login após ter finalizado o cadastro.
Para salvar, a rota POST do recurso usuários deverá ser reescrita para que possa ser carregada antes da validação de autenticação da API.
É um padrão de gerenciamento de estado/biblioteca para aplicações Vue.js. Com é possível centralizar o armazenamento de todos os componentes da aplicação aplicando regras que garantem que o estado só pode ser mutado de uma forma previsível.
Chamada de arvore de estado, é um objeto único que armazena o estado de toda aplicação como uma única fonte da verdade, o que facilita a obtenção de dados instantaneamente e de maneira fácil.
export const state = () => ({
list: []
});
A única forma de mudar o estado da aplicação é utilizando uma mutação. São semelhantes a eventos, definidas por um nome e alguns parâmetros, sendo o primeiro referente ao estado, onde realizaremos a modificação e o segundo o valor ou objeto com os dados com as mudanças no estado.
//...
export const mutations = {
SET(state, notas) {
state.list = notas;
}
};
As ações são responsáveis por solicitar mutações. Isso é feito utilizando a função commit, onde o primeiro parâmetro é o nome da mutação e o segundo o objeto ou valor a ser utilizado pela mutação.
Ações também podem realizar operações assíncronas, local ideal para realizar comunicações com o back-end.
//...
export const actions = {
async list({ commit }, usuarioId) {
try {
const { data } = await this.$axios.get(`nota/usuario/${usuarioId}`);
commit("SET", data);
return data;
} catch (e) {
throw e;
}
}
};
Uma ação pode ser executada por meio do método dispatch por meio da biblioteca global this.$store.
Ele é responsável por receber como parâmetro o nome do módulo/ação a ser realizada, assim como o valor a ser alterado no estado da aplicação.
<script>
export default {
layout: "home",
middleware: "auth",
async fetch() {
await this.$store.dispatch("nota/list", this.usuario.id);
},
computed: {
usuario() {
return this.$store.state.auth.user;
},
notas() {
return this.$store.state.nota.list;
}
}
};
</script>
Com base na estrutura adotada na pasta pages o Nuxt automaticamente configura as rotas da aplicação.
Algumas regras são importantes para entender a estrutura que será montada pelo framework.
pages/
--| perfil/
-----| index.vue
-----| historico.vue
--| index.vue
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'perfil',
path: '/perfil',
component: 'pages/perfil/index.vue'
},
{
name: 'perfil-historico',
path: '/perfil/historico',
component: 'pages/perfil/historico.vue'
}
]
}
Chamados de dinâmicas as rotas que podem receber parâmetros. Esses parâmetros são identificados por um undescorded no início do aquivo/diretório.
pages/
--| nota
-----| index.vue
-----| _id.vue
--| index.vue
diretório pages
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'nota',
path: '/nota',
component: 'pages/nota/index.vue'
},
{
name: 'nota',
path: '/nota/:id?',
component: 'pages/nota/_id.vue'
},
]
}
pages/
--| nota
-----| new.vue
-----| edit/
--------| _id/
-----------| index.vue
--| index.vue
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'nota-new',
path: '/nota/new',
component: 'pages/nota/new.vue'
},
{
name: 'nota-edit',
path: '/nota/edit/:id',
component: 'pages/nota/edit/_id/index.vue'
},
]
}
Caso prefira o roteamento tradicional, é possível fazer isso configurando a chave router nas configurações da aplicação.
export default {
//...
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'nota-new',
path: '/nota/new',
component: 'pages/nota/new.vue'
},
{
name: 'nota-edit',
path: '/nota/edit/:id',
component: 'pages/nota/edit.vue'
},
]
}
}
Componentes são instâncias reutilizáveis do Vue com um nome. Como toda instância Vue, eles também tem acesso a recursos como data, computed, watch, methods e gatilhos de ciclo de vida.
<template>
</template>
<script>
export default {
name: "meu-componente",
props: {
}
};
</script>
<style>
</style>
Props são atributos personalizáveis que você pode registrar em um componente. Quando um valor é passado para um atributo prop, ele torna-se uma propriedade daquela instância de componente.
<template>
<div class="card bg-warning my-5">
</div>
</template>
<script>
export default {
name: "n-nota",
props: {
id: {
type: [String, Number],
required: true
}
},
};
</script>
Você pode usar a diretiva v-model para criar interligações de mão dupla (two-way binding) entre os dados e elementos input, textarea e select de formulários. A diretiva automaticamente busca a maneira correta de atualizar o elemento com base no tipo de entrada.
Isso também é possível em componentes, utilizando a propriedade value no componente.
<template>
<b-input-group class="mb-2">
<b-input-group-prepend is-text>
<!-- -->
</b-input-group-prepend>
<b-form-input
class="bg-warning border-0 text-dark"
:class="{ concluida: value.concluida == 1 }"
placeholder="Novo Item"
v-model="value.descricao"
/>
</b-input-group>
</template>
<script>
export default {
name: "n-checklist-item",
props: {
value: Object
}
};
</script>
<template>
<input
:value="value"
@input="$emit('input', $event.target.value)"
/>
</template>
<script>
export default {
name: "n-checklist-item",
props: {
value: Object
}
};
</script>
Às vezes é útil emitir um valor específico com um evento. Por exemplo, quando nosso usuário pressionar a tecla enter, iremos emitir um evento para confirmar a inclusão do item ao checklist.
<template>
<b-input-group class="mb-2">
<b-input-group-prepend is-text>
<!-- -->
</b-input-group-prepend>
<b-form-input
class="bg-warning border-0 text-dark"
:class="{ concluida: value.concluida == 1 }"
placeholder="Novo Item"
v-model="value.descricao"
@keyup.enter.prevent="change"
/>
</b-input-group>
</template>
<script>
export default {
name: "n-checklist-item",
props: {
value: Object
},
methods: {
async change() {
this.$emit("changeItem", this.value);
}
}
};
</script>
<template>
<div>
<ul class="list-unstyled">
<li v-for="(item, index) of value" :key="index">
<n-checklist-item v-model="value[index]"></n-checklist-item>
</li>
<li>
<n-checklist-item
v-model="checklist"
@changeItem="adicionar()"
></n-checklist-item>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "n-checklist",
props: {
value: Array
},
data() {
return {
checklist: {
descricao: null,
concluida: 0
}
};
},
methods: {
adicionar() {
this.value.push(this.checklist);
this.checklist = {
descricao: null,
concluida: 0
};
}
}
};
</script>
Expressões dentro de templates são muito convenientes, mas são destinadas a operações simples. Colocar muita lógica neles pode fazer com que fiquem inchados e que a sua manutenção fique mais complicada.
Para realizar lógicas complexas utilizamos dados computados, que se parecem muito com propriedades definidas no data.
<script>
export default {
name: "n-nota",
data() {
return {
nota: {
id: null,
titulo: null,
descricao: null,
criadoEm: null,
atualizadoEm: null,
checklists: [],
tags: []
}
};
},
computed: {
numeroItensChecklist() {
return this.nota.checklists.length;
}
},
};
</script>
Enquanto dados computados são mais adequados na maioria dos casos, há momentos em que um observador personalizado é necessário. Por isso o Vue fornece uma maneira mais genérica para reagir a alterações de dados, o watch. Isto é particularmente útil quando se precisa executar operações assíncronas ou operações complexas antes de responder a uma alteração de dados.
<script>
export default {
data() {
return {
awaitingChange: false,
nota: { }
}
},
watch: {
nota: {
handler(value, oldValue) {
if (!this.awaitingChange) {
setTimeout(() => {
adicionar()
this.awaitingChange = false;
}, 1000);
}
this.awaitingChange = true;
},
deep: true
},
}
};
</script>
Construir um componente personalizado onde seja possível categorizar notas utilizando tags.
O componente deverá estar interligado com a propriedade tags do componente NNota.vue, e o controle dessas tags deverá estar dentro do componente construído por você.
PWA é uma metodologia de desenvolvimento considerada uma evolução híbrida entre aplicações web tradicionais e aplicativos móveis, combinando recursos modernos disponíveis nos navegadores e as vantagens de utilizar o celular.
O Nuxt possui um módulo para configurar uma PWA do zero. Com ela é possível:
yarn add --dev @nuxtjs/pwa
{
buildModules: [
'@nuxtjs/pwa',
]
}
sw.*
{
pwa: {
icon: {
source: "static/icon.png"
},
}
}
{
pwa: {
//...
meta: {
theme_color: "#fff"
},
}
}
{
pwa: {
//...
manifest: {
// Presente em telas de diálogo de instalação, WebStores
name: 'Notes App TADS',
// Tela iniciais do dispositivo, Novas abas
short_name: "Notes App"
// Recomendado pela Google
description: 'Aplicativo de Notas da Aula de Programação Web'
lang: 'pt-br',
// Equivalente a meta tag theme-color
theme_color: "#fff"
}
}
}
É uma coleção de bibliotecas Java Script para PWA's. Esse módulo adiciona suporte completo para aplicações offline.
Permite configurar por exemplo como sua aplicação irá realizar o cache das informações, rotas, etc.
O Nuxt já realiza algumas configurações padrões que podem ser sobrescritas.
Helpers são ajudantes presentes no Nuxt. O Ajudante $nuxt está presente no Nuxt para melhorar a experiência dos usuários.
<template>
<div>
<!--
...
-->
<div class="container">
<b-alert class="mt-5" variant="dark" show v-if="$nuxt.isOffline"
>Você está offline</b-alert
>
<Nuxt />
</div>
</div>
</template>
// Verifica se está desconectado
this.$nuxt.isOffline
window.$nuxt.isOffline
// Verifica se está conectado
this.$nuxt.isOnline
window.$nuxt.isOnline
O OneSignal é uma ferramenta para envio de mensagens de diversos tipos, como Mobile Push e Web Push.
Configurar o módulo OneSignal para PWAs Nuxt, permite integração rápida com essa ferramenta.
yarn add @nuxtjs/onesignal
{
//...
modules: [
//...
'@nuxtjs/onesignal'
]
}
{
//...
oneSignal: {
init: {
appId: 'YOUR_APP_ID',
allowLocalhostAsSecureOrigin: true,
cdn: true,
welcomeNotification: {
disable: true
}
}
}
}
O Nuxt oferece recursos que facilitam a otimização de páginas para motores de busca. As tags meta podem ser definidas de duas formas.
Muito útil para adicionar um título padrão e tag de descrição para fins de SEO ou para definir o viewport ou adicionar o favicon.
{
head: {
title: "Notes App",
htmlAttrs: {
lang: "pt-br"
},
bodyAttrs: {
class: "bg-light"
},
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
hid: "description",
name: "description",
content: "Sem lojas e instalações. Crie suas anotações mesmo offline"
}
],
link: [{ rel: "icon", type: "image/png", href: "/icon.png" }]
},
}
Também é possível adicionar títulos e meta para cada página usando o método dentro de sua tag de script em cada página utilizando o head.
<script>
export default {
head: {
title: 'Login',
meta: [
{
hid: 'description',
name: 'description',
content: 'Acesse o Notes App'
}
],
}
}
</script>
Configurar o head para todas as páginas do projeto:
Implementar as seguintes adequações na edição de Notas para quando a aplicação estiver offline:
Transições permitem aplicar regras de css como animações e transições.
v-enter: Inicia o estado de entrada. Aplicada antes do elemento ser inserido, removida depois de um frame.
v-enter-active: Ativa e termina o estado de entrada. Aplicada antes do elemento ser inserido, removida quando a transição/animação termina.
v-leave-active: Estado ativo de saída. Aplicada durante toda a fase de saída. Adicionada imediatamente quando a transição de saída é disparada, removida quando a transição/animação termina. Esta classe pode ser usada para definir a duração, atraso e a curva da transição de saída.
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
<script>
export default {
transition: "fade"
}
</script>
{
//...
pageTransition: "fade",
}