Vue pour le plus petit aka petit blog sur tous les canons



Bonjour à tous! Dans cet article, nous considérerons le développement de la face avant d'un simple blog Vue utilisant tous les charmes de Vue dont Vuex et Router. Et parlons également de la structure de l'application et travaillons avec le conteneur et le routeur.

Dans un premier temps, nous définirons les étapes de création du front pour l'application (dans ce cas, le blog):

  1. planification
  2. squelette d'application
  3. création de modèles
  4. mise en œuvre de la logique métier
  5. ajout de pages et d'itinéraires
  6. ajout de composants
  7. modifications

1. Planification


Pour commencer, nous noterons ce que contiendra notre SPA. Vous devez commencer directement à partir des pages, car c'est ce avec quoi l'utilisateur interagit directement (s'il est très difficile de l'arrondir de manière similaire aux tests TDD - nous décrivons d'abord ce que l'application doit faire, comment l'utilisateur va interagir avec elle, puis nous sommes déjà engagés dans la mise en œuvre).

Alors quelles pages seront:

  • Accueil - il présentera des catégories populaires, des commentaires et des articles récents
  • Parcourir la catégorie - Une liste d'articles pour une catégorie spécifique.
  • Afficher les actualités - directement le contenu des actualités et une liste de commentaires et d'autres articles de la catégorie

Si vous mettez des prototypes de pages, cela ressemble à ceci:

Beauté totale


Rien n'est clair, mais c'est le prototype :-) Je vais tout de suite expliquer pourquoi il n'a pas été dessiné à l'aide d'un logiciel de prototypage: parce que c'est beaucoup plus rapide et plus facile sur papier, et parfois il faut quand même bouger les mains, mais quand vient vous devez signer quelque part très triste ça devient.

Sur la base des pages que nous obtenons, nous listons les composants:

  • Liste d'articles
  • Liste des catégories
  • Liste de commentaires
  • Formulaire de commentaires

Nous traiterons déjà de l'optimisation des composants lors de la mise en œuvre des composants eux-mêmes, à ce stade ce n'est pas nécessaire.

Enfin, après avoir décrit tous les points d'interaction utilisateur, nous décrivons l'essence de notre application:

  • Article (titre, contenu, liste des commentaires)
  • Catégorie (titre, liste de nouvelles)
  • Commentaire (contenu)

Il est important de noter que l'essence de la logique métier (BL) est décrite, et non la table de base. Lors du développement et de la planification du recto de la pièce, et de la majeure partie du verso (à l'exclusion uniquement de la couche de données), il est nécessaire d'opérer avec des entités BL, et non avec des «tables». De plus, les entités ne devraient avoir que ce qui est utilisé dans l'application elle-même. Toucher l'avenir est bien, mais très rarement cet avenir vient, et dans le cas où seules les fonctionnalités utilisées sont définies et la structure de l'application est extensible , il n'y a aucun problème à ajouter des fonctionnalités à l'avenir et il n'y a rien de superflu à l'heure actuelle.

2. Squelette d'application


Nous passons à la création de la structure. Nous exécutons les commandes suivantes dans la console:

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

Ces commandes créent le projet vue-blog-habr dans le répertoire approprié. Pour plus d'informations sur vue-cli et les paramètres utilisés, cliquez ici .

Et à la fin, nous obtenons la structure de projet standard:



Installez immédiatement les packages dont nous avons besoin:

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

Enregistrez les modules utilisés:

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') 


Nous corrigeons la structure du répertoire du projet de cette manière:



Description du répertoire:

  • api - contient les fichiers responsables de la "communication" avec le serveur
  • actifs - diverses ressources statiques utilisées: images, icônes, ...
  • composants - composants d' application, sans "pages" utilisées dans le routeur
  • modèles - modèles de logique métier, ils peuvent être saturés de la fonctionnalité du domaine utilisé en façade
  • pages - composants de page utilisés dans le routeur
  • routeur - routage de fichiers
  • services - services auxiliaires qui ne sont pas liés à la logique métier. Par exemple, le service Display, qui contient une méthode pour obtenir les coordonnées d'un élément sur une page
  • store - Fichiers de stockage Vuex
  • styles - fichiers de style

Et exécutez le serveur de test:

 npm run serve 

La dernière commande démarre le serveur sur lequel toutes les modifications de projet sont appliquées en mode d'exécution. Pour accéder au navigateur, accédez à: localhost : 8080

3. Création de modèles


Il n'y a pas de logique compliquée dans notre application, mais vous devez néanmoins créer des modèles dans tous les cas.
Il y a plusieurs raisons à cela:

  • spécification des objets - tout objet arbitraire, peut contenir toutes les propriétés et méthodes. Lorsque des classes sont utilisées, nous savons quelles propriétés et méthodes un objet particulier a
  • typage des paramètres des composants - découle de la précédente: les composants Vue contrôlent le type des propriétés d'entrée
  • commodité - en termes de classes, nous opérons avec des entités du domaine, à cause de cela le code devient plus clair. Les classes fournissent également des fonctionnalités supplémentaires telles que get / set / static

Tous les modèles sont placés dans le répertoire approprié. La classe de l'article ressemble à ceci:

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. Mise en œuvre de la logique métier


De façon inattendue, mais à ce stade, la mise en œuvre de la logique métier elle-même n'est PAS nécessaire. Vous devez créer un objet de stockage et le diviser immédiatement en modules:

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, }, }) 


Et dans le module lui-même, nous décrirons plus en détail toutes les mutations / getters / actions utilisées. Initialement, le référentiel ressemble à ceci:

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


De plus, nous commençons un objet pour travailler avec l'API à travers lequel toutes les demandes passeront. Au moment de la mise en œuvre, à l'avant de la pièce, le backend n'est absolument pas nécessaire, vous pouvez donc utiliser des données statiques:

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) { //     } }, }; 


Ce qui donne l'utilisation de vuex:

  • pureté - les composants ne se transforment pas en objets de Dieu qui «en savent trop»
  • cohérence - si vous avez plusieurs composants qui utilisent les mêmes données, alors lorsque vous utilisez le conteneur vuex lors de la modification des données, ils (les données) seront mis à jour dans toute l'application, et pas seulement dans le composant qui a lancé la mise à jour
  • commodité - il est beaucoup plus facile et plus pratique d'accéder au magasin que de décrire les types de paramètres et / ou d'appels d'API dans chaque composant
  • tests - bien qui le fait?

5. Ajout de pages et d'itinéraires


Sur la base de la structure précédemment prévue, nous devons créer 4 pages: index, catégorie, article, ainsi que 404 pages. Dans le répertoire src / pages, ajoute les fichiers appropriés:

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

Si 404 pages doivent avoir une page personnelle avec un design individuel, le point d'entrée peut être modifié de cette manière:

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> 


Le fichier de la page principale ressemble à ceci:

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> 


Après l'implémentation des pages, nous ajoutons immédiatement tous les getters et actions utilisés au référentiel vuex (sans implémentation):

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) {}, }, } 


Il y a plusieurs points à noter:

  • lorsque vous avez besoin d'obtenir des données, vous devez contacter l'état. Si, après avoir reçu les données, vous devez manipuler les données, il est préférable de créer des getters pour cela;
  • lorsque vous devez faire quelque chose avec des données, vous devez vous référer à des actions, pas à des mutations. Une action peut inclure plusieurs mutations et effectuer d'autres manipulations de données ainsi que l'écriture dans l'état et ne pas avoir de restrictions asynchrones
  • Pas besoin de faire des demandes directes à l'API / Rest. Lorsque vous demandez toutes les données via Vuex, cela garantira l'état cohérent de l'ensemble de l'application (si le stockage lui-même est effectué correctement)
  • dans tous les liens et la navigation du programme, vous devez vous référer aux itinéraires nommés. Cela vous permettra de modifier sans difficulté les itinéraires eux-mêmes sans modifier les liens et la navigation du programme. Il s'agit naturellement de liens à l'intérieur du SPA, les adresses qui ne sont pas traitées par le routeur doivent être spécifiées comme d'habitude

Après avoir créé et rempli les pages elles-mêmes, vous devez ajouter les règles appropriées au routeur:

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'), }, ] }) 


Tous les paramètres du routeur se trouvent dans la documentation: router.vuejs.org/ru/api/#constructor options-router

6. Ajout de composants


Après avoir implémenté toutes les pages, nous obtenons la liste suivante des composants, les principaux:

  • CategoryItem
  • ArticleItem
  • CommentItem
  • CommentForm

Et auxiliaire:

  • ListItems
  • En-tête

Puisque nous utilisons des modèles, lors de l'implémentation de composants, nous pouvons les utiliser pour typifier les paramètres:

src / composants / 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> 


Quelques mots sur l'implémentation des composants sur le principal:

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> 


Cette page utilise le composant wrapper de ListItems. À première vue, cela peut sembler redondant, car vous pouvez vous débrouiller avec la construction v-for au fur et à mesure des commentaires, mais l'utilisation des emplacements réduit considérablement le code utilisé et vous permet de réutiliser le même élément à plusieurs endroits.

Mais si vous regardez la liste des articles, elle est utilisée sur deux pages (Index et Catégorie) avec exactement le même appel. Dans cette situation, c'est la bonne décision de créer le composant ArticleItems et de l'hériter de ListItems:

src / composants / 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> 


Dans cette situation, l'héritage permet de ne pas dupliquer la description des paramètres (propriété props), elle est reprise du composant parent. En savoir plus sur l'hérédité et les impuretés: ru.vuejs.org/v2/api/#extends , ru.vuejs.org/v2/guide/mixins.html

Après avoir créé un nouveau composant, vous devez également corriger les fichiers d'échange:

src / pages / Category.vue (était)
 <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 (devenu)
 <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> 


Il convient également de prêter attention au formulaire d'ajout de commentaires. Les demandes ne sont pas adressées directement à l'API, mais l'action Vuex est effectuée et la demande à l'API est déjà «à l'intérieur», le modèle d'article nécessaire est mis à jour et la liste des commentaires est mise à jour. Le code du composant lui-même ressemble à ceci:

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> 


Après avoir implémenté tous les composants et pages, implémenté des stubs et ajouté des données de test, dans ce cas particulier, la partie avant du blog est prête. Mais comme cela se passe généralement dans la pratique, c'est loin de la fin du projet, car après l'achèvement des travaux, les changements commencent :-)

7. Modifications


Supposons que nous devions modifier l'affichage des articles sur la page de catégorie: ils doivent être affichés sur 2 colonnes. Et sur la page principale, tout doit rester tel quel.

Nous ajoutons une propriété cols supplémentaire au nombre de colonnes dans le composant ArticleItems.

src / components / ArticleItems.vue (devenu)
 <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> 


Dans l'appel de composant sur la page des catégories, ajoutez la propriété souhaitée:

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


Ensuite, nous avons voulu aller à la page de visualisation des articles, ajouter des liens vers les articles voisins (avant / arrière). Pour ce faire, vous devrez ajouter 2 getters et liens vers la page:

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> 


Et la mise en œuvre des getters eux-mêmes:

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; }, ... 


Et enfin, nous devons changer l'URL de la page de l'article, de «article-123» à «post-123». Étant donné que les routes nommées sont utilisées dans toute l'application, il suffit de modifier uniquement le modèle de route:

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'), }, ]; 



Littérature utile


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


PS


Malgré le fait que cela s'est avéré être une application très simple, la structure est faite de telle manière que vous pouvez facilement changer quelque chose et / ou ajouter:

  • Les composants traitent exclusivement des visuels, les données sont extraites du référentiel sous forme de modèles et aucune demande d'API directe n'est effectuée.
  • Les modèles contiennent la logique métier nécessaire et toutes les relations entre les entités. N'accédez PAS non plus à l'API.
  • Le stockage (vuex) est le lien de connexion de tous les composants: les données sont demandées à l'API et converties en modèles, auxquels les composants accèdent ensuite.

Visuellement, cela peut être représenté comme suit:


Avec ce schéma, le travail sur le projet peut être réparti en toute sécurité entre trois unités:

  • M. mise en page - est engagé dans la mise en œuvre des pages et des composants, et décrit également quels champs doivent contenir des modèles, et quels getters et actions doivent être dans le référentiel;
  • M. Front - est engagé dans la mise en œuvre de la logique métier et des modèles de stockage Vuex, ainsi que dans le routage et tout ce qui ne concerne pas la disposition de M.;
  • M. back - est engagé dans la mise en œuvre de services API côté backend.

Toutes les unités peuvent travailler indépendamment les unes des autres si tout terrain d'entente entre elles a été discuté précédemment (en général, s'il y avait une planification avant de travailler sur le projet).

Ou s'il n'y a pas d'équipe en tant que telle, il est logique d'aller dans l'ordre de haut en bas, afin que dès que possible il y ait quelque chose à regarder (déployer MVP).

Référentiel avec toutes les sources: github.com/irpsv/vue-blog-habr

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


All Articles