Vue para o menor blog aka pequeno em todos os cânones



Olá pessoal! Neste artigo, consideraremos o desenvolvimento da frente de um blog simples do Vue, usando todos os encantos do Vue, incluindo o Vuex e o Router. E também vamos falar sobre a estrutura do aplicativo e trabalhar com o contêiner e o roteador.

Primeiro, definiremos os estágios da criação da frente do aplicativo (neste caso, o blog):

  1. planejamento
  2. esqueleto de aplicação
  3. criando modelos
  4. implementação da lógica de negócios
  5. adicionando páginas e rotas
  6. adicionando componentes
  7. edições

1. Planejamento


Para começar, escreveremos o que o nosso SPA conterá. Você deve começar diretamente das páginas, pois é com isso que o usuário interage diretamente (se for muito difícil arredondá-lo para os testes TDD - descrevemos primeiro o que o aplicativo deve fazer, como o usuário interagirá com ele e, em seguida, já estamos envolvidos na implementação).

Então, quais páginas serão:

  • Página inicial - contará com categorias populares, comentários e artigos recentes
  • Procurar categoria - uma lista de artigos para uma categoria específica
  • Visualizar notícias - diretamente o conteúdo das notícias e uma lista de comentários e outros artigos da categoria

Se você colocar protótipos de páginas, será algo assim:

Beleza absoluta


Nada está claro, mas esse é o protótipo :-) explicarei imediatamente por que não foi desenhado usando um software de prototipagem: porque no papel é muito mais rápido e fácil, e às vezes você ainda precisa mexer as mãos, mas quando se trata de quando você precisa assinar em algum lugar muito triste que se torne.

Com base nas páginas que obtemos, listamos os componentes:

  • Lista de Artigos
  • Lista de categorias
  • Lista de comentários
  • Formulário de comentário

Lidaremos com a otimização dos componentes já durante a implementação dos próprios componentes; nesse estágio, isso não é necessário.

Por fim, tendo descrito todos os pontos de interação do usuário, descrevemos a essência de nossa aplicação:

  • Artigo (título, conteúdo, lista de comentários)
  • Categoria (manchete, lista de notícias)
  • Comentário (conteúdo)

É importante observar que a essência da lógica de negócios (BL) é descrita, e não a tabela base. Ao desenvolver e planejar a frente da peça e a maior parte de trás (excluindo apenas a camada de dados), é necessário operar com entidades BL, e não com "tabelas". Além disso, as entidades devem ter apenas o que é usado no próprio aplicativo. Tocar o futuro é bom, mas muito raramente esse futuro chega e, quando apenas a funcionalidade usada é estabelecida e a estrutura do aplicativo é expansível , não há problemas em adicionar funcionalidades no futuro e não há nada supérfluo no momento.

2. Esqueleto de aplicação


Passamos a criar a estrutura. Executamos os seguintes comandos no console:

npm install -g @vue/cli vue create vue-blog-habr -n -d -m npm cd vue-blog-habr 

Esses comandos criam o projeto vue-blog-habr no diretório apropriado. Mais informações sobre o vue-cli e os parâmetros utilizados podem ser encontradas aqui .

E, no final, obtemos a estrutura padrão do projeto:



Instale imediatamente os pacotes que precisamos:

 npm install vue-router vuex axios bootstrap-vue sass-loader npm install --save-dev --unsafe-perm node-sass 

Registre os módulos usados:

src / main.js
 import App from './App.vue' import Vue from 'vue' import VueRouter from 'vue-router' import BootstrapVue from 'bootstrap-vue' import store from './store' import router from './router' Vue.config.productionTip = false Vue.use(VueRouter) Vue.use(BootstrapVue) new Vue({ store, router, render: h => h(App), }).$mount('#app') 


Corrigimos a estrutura de diretórios do projeto desta maneira:



Descrição do Diretório:

  • api - contém arquivos responsáveis ​​pela "comunicação" com o servidor
  • ativos - vários recursos estáticos usados: fotos, ícones, ...
  • componentes - componentes de aplicativos, sem "páginas" usadas no roteador
  • modelos - modelos de lógica de negócios, eles podem ser saturados com o funcional do domínio usado na frente
  • pages - componentes da página usados ​​no roteador
  • roteador - arquivos de roteamento
  • serviços - serviços auxiliares que não estão relacionados à lógica de negócios. Por exemplo, o serviço de exibição, que contém um método para obter as coordenadas de um elemento em uma página
  • store - arquivos de armazenamento Vuex
  • styles - arquivos de estilo

E execute o servidor de teste:

 npm run serve 

O último comando inicia o servidor no qual todas as edições do projeto são aplicadas no modo de tempo de execução. Para acessar o navegador, acesse: localhost : 8080

3. Criando modelos


Não há lógica complicada em nossa aplicação, mas, no entanto, você precisa criar modelos em qualquer caso.
Existem várias razões para isso:

  • especificação de objetos - qualquer objeto arbitrário, pode conter quaisquer propriedades e métodos. Quando classes são usadas, sabemos quais propriedades e métodos um determinado objeto possui
  • digitação dos parâmetros do componente - segue o anterior: Os componentes do Vue controlam o tipo de propriedades de entrada
  • conveniência - em termos de classes, operamos com entidades da área de atuação, por isso o código se torna mais claro. As classes também fornecem recursos adicionais, como get / set / static

Todos os modelos são colocados no diretório apropriado. A classe do artigo é assim:

src / models / Article.js
 export default class Article { constructor(id, title, content) { this.id = id; this.title = title; this.content = content; this.comments = []; } addComment(item) { this.comments.push(item); } static createFrom(data) { const {id, title, content} = data; return new this(id, title, content); } } 


4. Implementação da lógica de negócios


Inesperadamente, mas neste estágio, a implementação da lógica de negócios em si NÃO é necessária. Você precisa criar um objeto de armazenamento e imediatamente dividi-lo em módulos:

src / store / index.js
 import Vue from 'vue' import Vuex from 'vuex' import blog from './modules/blog' Vue.use(Vuex) export default new Vuex.Store({ modules: { blog, }, }) 


E no próprio módulo, descreveremos melhor todas as mutações / getters / ações usadas. Inicialmente, o repositório fica assim:

src / store / modules / blog.js
 export default { state: {}, getters: {}, mutations: {}, actions: {}, } 


Além disso, iniciamos um objeto para trabalhar com a API pela qual todas as solicitações serão transmitidas. No momento da implementação, na frente da peça, o back-end não é absolutamente necessário, para que você possa usar dados estáticos:

src / api / index.js
 import Article from '@/models/Article'; import Comment from '@/models/Comment'; import Category from '@/models/Category'; export default { getArticles() { const comments = this.getComments(); const items = [ { id: 1, title: ' 1', content: '  1', }, { id: 2, title: ' 2', content: '  2', }, { id: 3, title: ' 3', content: '  3', }, { id: 4, title: ' 4', content: '  4', }, { id: 5, title: ' 5', content: '  5', }, { id: 6, title: ' 6', content: '  6', }, ]; return items.map((item) => { const article = Article.createFrom(item); article.comments = comments.filter((comment) => comment.article_id == article.id); return article; }); }, getComments() { const items = [ { id: 1, article_id: 1, content: '   1', }, ]; return items.map((item) => Comment.createFrom(item)) }, getCategories() { const items = [ { id: 1, title: '', articles: [1,3,5], }, { id: 2, title: '', articles: [2,3,4], }, { id: 3, title: '', articles: [], }, ]; return items.map((item) => Category.createFrom(item)) }, addComment(comment) { if (comment) { //     } }, }; 


O que dá o uso do vuex:

  • pureza - componentes não se transformam em objetos de Deus que “sabem demais”
  • consistência - se você tiver vários componentes que usam os mesmos dados, ao usar o contêiner vuex ao alterar dados, eles (dados) serão atualizados em todo o aplicativo, e não apenas no componente que iniciou a atualização
  • conveniência - é muito mais fácil e conveniente acessar a loja do que descrever os tipos de parâmetros e / ou chamadas de API em cada componente
  • testes - embora quem o faça?

5. Adicionando páginas e rotas


Com base na estrutura planejada anteriormente, precisamos criar 4 páginas: Índice, Categoria, Artigo e 404 páginas. No diretório src / pages, adiciona os arquivos apropriados:

  • Article.vue
  • Category.vue
  • Index.vue
  • 404.vue

Se 404 páginas tiverem uma página pessoal com um design individual, o ponto de entrada poderá ser alterado desta maneira:

src / app.vue
 <template> <div id="app"> <template v-if="is404"> <router-view></router-view> </template> <template v-else> <Header></Header> <main> <b-container> <router-view></router-view> </b-container> </main> </template> </div> </template> <script> import '@/styles/index.scss'; import Header from '@/components/Header.vue'; export default { name: 'App', components: { Header, }, computed: { is404() { return this.$route.name === '404'; }, }, } </script> 


O arquivo da página principal fica assim:

src / pages / Index.vue
 <template> <b-row> <b-col md="8" lg="9"> <ListItems :items="lastArticles"> <template v-slot:default="props"> <ArticleItem :item="props.item"></ArticleItem> </template> <template v-slot:empty>    :) </template> </ListItems> </b-col> <b-col md="4" lg="3"> <ListItems :items="popularCategories" v-slot="props"> <router-link :to="getCategoryRoute(props.item)"> {{ props.item.title }} </router-link> </ListItems> <CommentItem v-for="(item, index) in lastComments" :key="index" :item="item"></CommentItem> </b-col> </b-row> </template> <script> import ListItems from '@/components/ListItems.vue' import ArticleItem from '@/components/ArticleItem.vue' import CommentItem from '@/components/CommentItem.vue' import { mapGetters, } from 'vuex' export default { name: 'Index', components: { ListItems, ArticleItem, CommentItem, }, data() { return {}; }, methods: { getCategoryRoute(item) { return { name: 'Category', params: { category_id: item.id, }, }; }, }, computed: { ...mapGetters([ 'lastArticles', 'lastComments', 'popularCategories', ]), }, created() { /** *     (  ) */ this.$store.dispatch('loadArticles'); this.$store.dispatch('loadComments'); this.$store.dispatch('loadCategories'); }, } </script> 


Após a implementação das páginas, adicionamos imediatamente todos os getters e ações usados ​​ao repositório vuex (sem implementação):

src / store / modules / blog.js
 export default { state: { articles: [], comments: [], categories: [], // activeArticle: null, activeCategory: null, }, getters: { lastArticles(state) { return []; }, lastComments(state) { return []; }, popularCategories(state) { return []; }, activeCategoryArticles(state) { return []; }, }, mutations: {}, actions: { async loadArticles({ commit, state }) {}, async loadComments({ commit, state }) {}, async loadCategories({ commit, state }) {}, async loadActiveCategory(context, id) {}, async loadActiveArticle(context, id) {}, async addComment({ commit }, payload) {}, }, } 


Há vários pontos a serem observados:

  • quando precisar obter dados, entre em contato com o estado. Se, depois de receber os dados, você precisar manipular os dados, é melhor criar getters para isso;
  • quando você precisa fazer algo com dados, precisa se referir a ações, não a mutações. Uma ação pode incluir várias mutações e executar outras manipulações de dados, além de gravar no estado e não ter restrições assíncronas
  • Não há necessidade de fazer solicitações diretas à API / Rest. Ao solicitar todos os dados pelo Vuex, isso garante o estado consistente de todo o aplicativo (se o próprio armazenamento for feito corretamente)
  • em todos os links e navegação do programa, você precisa consultar as rotas nomeadas. Isso permitirá que você altere as rotas sem problemas, sem editar os links e a navegação do programa. Naturalmente, trata-se de links dentro do SPA. Os endereços que não são processados ​​pelo roteador devem ser especificados normalmente.

Após criar e preencher as próprias páginas, você precisa adicionar as regras apropriadas ao roteador:

src / router / index.js
 import VueRouter from 'vue-router' import blog from './blog' export default new VueRouter({ mode: 'history', routes: [ { path: '/', name: 'Index', component: () => import('@/pages/Index.vue'), }, /** *   ,       *     *          */ ...blog, { path: '*', name: '404', component: () => import('@/pages/404.vue'), }, ] }) 


Todos os parâmetros do roteador podem ser encontrados na documentação: router.vuejs.org/ru/api/#constructor options-router

6. Adicionando componentes


Após implementar todas as páginas, obtemos a seguinte lista de componentes, os principais:

  • CategoryItem
  • ArticleItem
  • CommentItem
  • CommentForm

E auxiliar:

  • ListItems
  • Cabeçalho

Como usamos modelos, ao implementar componentes, podemos usá-los para tipificar parâmetros:

src / components / ArticleItem.vue
 <template> <b-card :title="item.title" class="article-item-card"> <router-link :to="getArticleRoute" class="card-link">  </router-link> </b-card> </template> <script> import Article from '@/models/Article'; export default { name: 'ArticleItem', props: { item: Article, }, computed: { getArticleRoute() { return { name: 'Article', params: { post_id: this.item.id, }, }; }, }, } </script> <style> .article-item-card { margin-bottom: 1rem; } </style> 


Algumas palavras sobre a implementação de componentes principalmente:

src / pages / Index.vue
 <template> <b-row> <b-col md="8" lg="9"> <ListItems :items="lastArticles"> <template v-slot:default="props"> <ArticleItem :item="props.item"></ArticleItem> </template> <template v-slot:empty>    :) </template> </ListItems> </b-col> <b-col md="4" lg="3"> <ListItems :items="popularCategories" v-slot="props"> <router-link :to="getCategoryRoute(props.item)"> {{ props.item.title }} </router-link> </ListItems> <CommentItem v-for="(item, index) in lastComments" :key="index" :item="item"></CommentItem> </b-col> </b-row> </template> <script> import ListItems from '@/components/ListItems.vue' import ArticleItem from '@/components/ArticleItem.vue' import CommentItem from '@/components/CommentItem.vue' import { mapGetters, } from 'vuex' export default { name: 'Index', components: { ListItems, ArticleItem, CommentItem, }, data() { return {}; }, methods: { getCategoryRoute(item) { return { name: 'Category', params: { category_id: item.id, }, }; }, }, computed: { ...mapGetters([ 'lastArticles', 'lastComments', 'popularCategories', ]), }, created() { /** *     (  ) */ this.$store.dispatch('loadArticles'); this.$store.dispatch('loadComments'); this.$store.dispatch('loadCategories'); }, } </script> 


Esta página usa o componente wrapper de ListItems. À primeira vista, pode parecer redundante, porque você pode se dar bem com a construção v-for à medida que os comentários são feitos, mas o uso de slots reduz bastante o código usado e permite reutilizar o mesmo elemento em vários lugares.

Mas se você olhar para a lista de artigos, ela será usada em duas páginas (Índice e Categoria) com exatamente a mesma chamada. Nessa situação, é a decisão correta criar o componente ArticleItems e herdá-lo de ListItems:

src / components / ArticleItems.vue
 <template> <ListItems :items="items"> <template v-slot:default="props"> <ArticleItem :item="props.item"></ArticleItem> </template> <template v-slot:empty>    :) </template> </ListItems> </template> <script> import ListItems from '@/components/ListItems.vue' import ArticleItem from '@/components/ArticleItem.vue' export default { name: 'ArticleItems', components: { ArticleItem, ListItems, }, extends: ListItems, } </script> 


Nessa situação, a herança permite não duplicar a descrição dos parâmetros (propriedade props), que é retirada do componente pai. Mais sobre herança e impurezas: ru.vuejs.org/v2/api/#extends , ru.vuejs.org/v2/guide/mixins.html

Depois de criar um novo componente, você também precisa corrigir os arquivos de página:

src / pages / Category.vue (was)
 <template> <div> <div v-if="category"> <h1> {{ category.title }} </h1> <ListItems :items="articles"> <template v-slot:default="props"> <ArticleItem :item="props.item"></ArticleItem> </template> <template v-slot:empty>    :) </template> </ListItems> </div> <div v-else>    </div> </div> </template> <script> import ListItems from '@/components/ListItems.vue' import ArticleItem from '@/components/ArticleItem.vue' import { mapActions, } from 'vuex' export default { name: 'Category', components: { ListItems, ArticleItem, }, computed: { categoryId() { return this.$route.params['category_id'] || null; }, category() { return this.$store.state.blog.activeCategory; }, articles() { return this.$store.getters.activeCategoryArticles; }, }, methods: { ...mapActions([ 'loadActiveCategory', ]), }, mounted() { this.loadActiveCategory(this.categoryId); }, } </script> 


src / pages / Category.vue (tornou-se)
 <template> <div> <div v-if="category"> <h1> {{ category.title }} </h1> <ArticleItems :items="articles"></ArticleItems> </div> <div v-else>    </div> </div> </template> <script> import ArticleItems from '@/components/ArticleItems.vue' import { mapActions, } from 'vuex' export default { name: 'Category', components: { ArticleItems, }, computed: { categoryId() { return this.$route.params['category_id'] || null; }, category() { return this.$store.state.blog.activeCategory; }, articles() { return this.$store.getters.activeCategoryArticles; }, }, methods: { ...mapActions([ 'loadActiveCategory', ]), }, mounted() { this.loadActiveCategory(this.categoryId); }, } </script> 


Você também deve prestar atenção ao formulário para adicionar comentários. As solicitações não são feitas diretamente na API, mas a ação Vuex é executada e a solicitação para a API já está "dentro", o modelo de Artigo necessário é atualizado e a lista de comentários é atualizada. O código do componente em si é assim:

src / components / CommentForm.vue
 <template> <form @submit.prevent="onSubmit"> <textarea class='form-control' v-model="content"></textarea> <br> <button type="submit" class="btn btn-primary"></button> </form> </template> <script> export default { name: 'CommentForm', props: { articleId: Number, }, data() { return { content: '', }; }, methods: { onSubmit() { if (this.content) { this.$store.dispatch('addComment', { content: this.content, article_id: this.articleId, }); this.content = ''; } }, }, } </script> 


Depois de implementar todos os componentes e páginas, implementar stubs e adicionar dados de teste, nesse caso em particular, a parte frontal do blog está pronta. Mas, como geralmente acontece na prática, isso está longe do final do projeto, porque após a conclusão do trabalho, as mudanças começam :-)

7. Edições


Suponha que precisamos alterar a exibição dos artigos na página da categoria: eles devem ser exibidos em 2 colunas. E na página principal tudo deve permanecer como está.

Adicionamos uma propriedade cols adicional ao número de colunas no componente ArticleItems.

src / components / ArticleItems.vue (tornou-se)
 <template> <ListItems :items="items" class="row"> <template v-slot:default="props"> <b-col :cols="itemCols"> <ArticleItem :item="props.item"></ArticleItem> </b-col> </template> <template v-slot:empty> <b-col>    :) </b-col> </template> </ListItems> </template> <script> import ListItems from '@/components/ListItems.vue' import ArticleItem from '@/components/ArticleItem.vue' export default { name: 'ArticleItems', components: { ArticleItem, ListItems, }, extends: ListItems, props: { cols: { type: Number, default: 1, }, }, computed: { itemCols() { return 12 / this.cols; }, }, } </script> 


Na chamada do componente na página de categorias, adicione a propriedade desejada:

src / pages / Category.vue
 <ArticleItems :items="articles" :cols="2"></ArticleItems> 


Em seguida, queríamos ir para a página de visualização de artigos, adicionar links aos artigos vizinhos (avançar / retroceder). Para fazer isso, você precisará adicionar 2 getters e links à página:

src / pages / Article.vue
 <template> <b-row v-if="article"> <b-col md="8" lg="9"> <h1> {{ article.title }} </h1> <p class="mb-4"> {{ article.content }} </p> <table class="table table-bordered"> <tbody> <tr> <td class="w-50"> <router-link v-if="prevArticle" :to="getArticleRoute(prevArticle)"> {{ prevArticle.title }} </router-link> </td> <td class="text-right"> <router-link v-if="nextArticle" :to="getArticleRoute(nextArticle)"> {{ nextArticle.title }} </router-link> </td> </tr> </tbody> </table> <CommentForm :articleId="article.id"></CommentForm> <CommentItem v-for="(item, index) in article.comments" :key="index" :item="item"></CommentItem> </b-col> <b-col md="4" lg="3"> <CommentItem v-for="(item, index) in lastComments" :key="index" :item="item"></CommentItem> </b-col> </b-row> </template> <script> import CommentForm from '@/components/CommentForm.vue'; import CommentItem from '@/components/CommentItem.vue'; import { mapActions, mapGetters, } from 'vuex' export default { name: 'Article', components: { CommentForm, CommentItem, }, computed: { ...mapGetters([ 'lastComments', 'nextArticle', 'prevArticle', ]), articleId() { return this.$route.params['post_id'] || null; }, article() { return this.$store.state.blog.activeArticle; }, }, methods: { ...mapActions([ 'loadComments', 'loadActiveArticle', ]), getArticleRoute(item) { return { name: 'Article', params: { post_id: item.id, }, }; }, }, mounted() { this.loadComments(); this.loadActiveArticle(this.articleId); }, watch: { articleId(value) { this.loadActiveArticle(value); }, }, } </script> 


E a implementação dos próprios getters:

src / store / modules / blog.js
 ... prevArticle(state) { let prevItem = null; if (state.activeArticle) { state.articles.forEach((item, index) => { if (item.id == state.activeArticle.id) { prevItem = state.articles[index-1] || null; } }); } return prevItem; }, nextArticle(state) { let nextItem = null; if (state.activeArticle) { state.articles.forEach((item, index) => { if (item.id == state.activeArticle.id) { nextItem = state.articles[index+1] || null; } }); } return nextItem; }, ... 


E, finalmente, precisamos alterar o URL da página do artigo, de "article-123" para "post-123". Como as rotas nomeadas são usadas em todo o aplicativo, basta alterar apenas o padrão da rota:

src / router / blog / index.js
 export default [ { path: '/cat-:category_id', name: 'Category', component: () => import('@/pages/Category.vue'), }, { path: '/post-:post_id', name: 'Article', component: () => import('@/pages/Article.vue'), }, ]; 



Literatura útil


  1. vuex.vuejs.org/en/guide
  2. cli.vuejs.org/en/guide
  3. router.vuejs.org/ru


PS


Apesar de ser uma aplicação muito simples, a estrutura é feita de forma que você pode alterar facilmente algo e / ou adicionar:

  • Os componentes lidam exclusivamente com o visual, os dados são retirados do repositório na forma de modelos e as solicitações diretas de API não são feitas.
  • Os modelos contêm a lógica comercial necessária e todos os relacionamentos entre entidades. Também NÃO acesse a API.
  • O armazenamento (vuex) é o link de conexão de todos os componentes: os dados são solicitados a partir da API e convertidos em modelos, que são acessados ​​pelos componentes.

Visualmente, isso pode ser representado da seguinte maneira:


Com esse esquema, o trabalho no projeto pode ser dividido com segurança entre três unidades:

  • Sr. layout - está envolvido na implementação de páginas e componentes e também descreve quais campos devem conter modelos e quais getters e ações devem estar no repositório;
  • Mr. Front - está envolvido na implementação da lógica de negócios e dos modelos de armazenamento Vuex, bem como no roteamento e tudo o mais que não se refere ao layout do Sr.;
  • Sr. back - está envolvido na implementação de serviços de API no lado de back-end.

Todas as unidades podem trabalhar independentemente uma da outra se todo o terreno comum entre elas tiver sido discutido anteriormente (em geral, se houver algum planejamento antes do trabalho no projeto).

Ou, se não houver uma equipe como tal, é lógico ir de cima para baixo, para que, o mais rápido possível, haja algo para assistir (implemente o MVP).

Repositório com todas as fontes: github.com/irpsv/vue-blog-habr

Source: https://habr.com/ru/post/pt483064/


All Articles