
Hola a todos! En este artículo, consideraremos el desarrollo del frente de un simple blog de Vue usando todos los encantos de Vue, incluidos Vuex y Router. Y también hablemos sobre la estructura de la aplicación y trabajemos con el contenedor y el enrutador.
Primero, definiremos las etapas de creación del frente para la aplicación (en este caso, el blog):
- planificación
- esqueleto de aplicación
- creando modelos
- implementación de lógica de negocios
- agregar páginas y rutas
- agregar componentes
- ediciones
1. Planificación
Para empezar, escribiremos lo que contendrá nuestro SPA. Debe comenzar directamente desde las páginas, ya que esto es con lo que el usuario interactúa directamente (si es muy difícil redondearlo de manera similar a las pruebas TDD: primero describimos qué debe hacer la aplicación, cómo interactuará el usuario con ella y luego ya estamos involucrados en la implementación).
Entonces, ¿qué páginas serán:
- Inicio: presentará categorías populares, comentarios recientes y artículos
- Examinar categoría: una lista de artículos para una categoría específica.
- Ver noticias: directamente el contenido de las noticias y una lista de comentarios y otros artículos de la categoría
Si pones prototipos de páginas, entonces se ve así:
No hay nada claro, pero ese es el prototipo :-) Explicaré de inmediato por qué no se dibujó con un software de creación de prototipos: porque es mucho más rápido y fácil en papel, y a veces aún necesitas mover las manos, pero cuando se trata de cuándo necesitas firmar en algún lugar muy triste se vuelve.
Según las páginas que obtenemos, enumeramos los componentes:
- Lista de artículos
- Lista de categorías
- Lista de comentarios
- Formulario de comentarios
Nos ocuparemos de la optimización de los componentes ya durante la implementación de los componentes mismos, en esta etapa esto no es necesario.
Finalmente, habiendo descrito todos los puntos de interacción del usuario, describimos la esencia de nuestra aplicación:
- Artículo (título, contenido, lista de comentarios)
- Categoría (título, lista de noticias)
- Comentario (contenido)
Es importante tener en cuenta que se describe la esencia de la lógica de negocios (BL) y no la tabla base. Al desarrollar y planificar el frente de la parte, y la mayor parte del reverso (excluyendo solo la capa de datos), es necesario operar con entidades BL y no con "tablas". Además, las entidades deberían tener solo lo que se usa en la aplicación misma. Tocar el futuro es bueno, pero rara vez llega este futuro, y en el caso de que solo se establezca la funcionalidad utilizada y la estructura de la aplicación sea
ampliable , no hay problemas para agregar funcionalidad en el futuro y no hay nada superfluo en el momento actual.
2. Esqueleto de aplicación
Pasamos a crear la estructura. Ejecutamos los siguientes comandos en la consola:
npm install -g @vue/cli vue create vue-blog-habr -n -d -m npm cd vue-blog-habr
Estos comandos crean el proyecto vue-blog-habr en el directorio apropiado. Puede encontrar más información sobre vue-cli y los parámetros utilizados
aquí .
Y al final obtenemos la estructura estándar del proyecto:

Instale inmediatamente los paquetes que necesitamos:
npm install vue-router vuex axios bootstrap-vue sass-loader npm install --save-dev --unsafe-perm node-sass
Registre los 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')
Corregimos la estructura de directorios del proyecto de esta manera:

Descripción del directorio:
- api : contiene archivos responsables de la "comunicación" con el servidor
- activos : diversos recursos estáticos utilizados: imágenes, iconos, ...
- componentes : componentes de la aplicación, sin "páginas" utilizadas en el enrutador
- modelos : modelos de lógica de negocios, pueden saturarse con la funcionalidad del dominio utilizado en el frente
- páginas : componentes de página utilizados en el enrutador
- enrutador - enrutamiento de archivos
- servicios : servicios auxiliares que no están relacionados con la lógica empresarial. Por ejemplo, el servicio de visualización, que contiene un método para obtener las coordenadas de un elemento en una página
- store - archivos de almacenamiento Vuex
- estilos - archivos de estilo
Y ejecute el servidor de prueba:
npm run serve
El último comando inicia el servidor en el que se aplican todas las ediciones del proyecto en modo de tiempo de ejecución. Para acceder al navegador, vaya a:
localhost : 8080
3. Crear modelos
No hay una lógica complicada en nuestra aplicación, pero, sin embargo, debe crear modelos en cualquier caso.
Hay varias razones para esto:
- especificación de objetos : cualquier objeto arbitrario puede contener propiedades y métodos. Cuando se usan clases, sabemos qué propiedades y métodos tiene un objeto en particular
- tipificación de parámetros de componentes : se deduce de lo anterior: los componentes Vue controlan el tipo de propiedades de entrada
- conveniencia : en términos de clases, operamos con entidades del área temática, por eso el código se vuelve más claro. Las clases también proporcionan características adicionales como get / set / static
Todos los modelos se colocan en el directorio apropiado. La clase para el artículo se ve así:
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. Implementación de la lógica de negocios.
Inesperadamente, pero en esta etapa, la implementación de la lógica comercial NO es necesaria. Necesita crear un objeto de almacenamiento e inmediatamente dividirlo en 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, }, })
Y en el módulo en sí, describiremos más detalladamente todas las mutaciones / captadores / acciones utilizadas. Inicialmente, el repositorio se ve así:
src / store / modules / blog.js export default { state: {}, getters: {}, mutations: {}, actions: {}, }
Además, comenzamos un objeto para trabajar con la API a través del cual pasarán todas las solicitudes. En el momento de la implementación, el frente de la parte, el backend no es absolutamente necesario, por lo que puede usar datos 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) {
Lo que da el uso de vuex:
- pureza : los componentes no se convierten en objetos de Dios que "saben demasiado"
- consistencia : si tiene varios componentes que usan los mismos datos, cuando use el contenedor vuex cuando cambie los datos, estos (datos) se actualizarán en toda la aplicación, y no solo en el componente que inició la actualización
- conveniencia : es mucho más fácil y más conveniente acceder a la tienda que describir los tipos de parámetros y / o llamadas API en cada componente
- pruebas , aunque ¿quién lo hace?
5. Agregar páginas y rutas
Según la estructura planificada previamente, necesitamos crear 4 páginas: Índice, Categoría, Artículo, así como 404 páginas. En el directorio src / pages, agrega los archivos apropiados:
- Artículo.vue
- Category.vue
- Index.vue
- 404.vue
Si 404 páginas deben tener una página personal con un diseño individual, entonces el punto de entrada se puede cambiar de esta manera:
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>
El archivo de la página principal se ve así:
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>
Después de la implementación de las páginas, agregamos inmediatamente todos los captadores y acciones utilizados al repositorio vuex (sin implementación):
src / store / modules / blog.js export default { state: { articles: [], comments: [], categories: [],
Hay varios puntos a tener en cuenta:
- cuando necesita obtener datos, debe comunicarse con el estado. Si, después de recibir los datos, necesita hacer alguna manipulación de los datos, es mejor crear captadores para esto;
- cuando necesita hacer algo con datos, debe referirse a acciones, no a mutaciones. Una acción puede incluir varias mutaciones y realizar otras manipulaciones de datos, así como escribir en estado y no tener restricciones asincrónicas.
- No es necesario realizar solicitudes directas a la API / Rest. Al solicitar todos los datos a través de Vuex, esto garantizará el estado coherente de toda la aplicación (si el almacenamiento se realiza correctamente)
- En todos los enlaces y programas de navegación, debe referirse a las rutas con nombre. Esto le permitirá cambiar sin problemas las rutas sin editar los enlaces y la navegación del programa. Esto se trata naturalmente de enlaces dentro del SPA, las direcciones que no son procesadas por el enrutador deben especificarse como de costumbre
Después de crear y llenar las páginas, debe agregar las reglas apropiadas al enrutador:
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 los parámetros del enrutador se pueden encontrar en la documentación:
router.vuejs.org/ru/api/#constructor options-router
6. Agregar componentes
Después de implementar todas las páginas, obtenemos la siguiente lista de componentes, los principales:
- CategoryItem
- Artículo Artículo
- CommentItem
- CommentForm
Y auxiliar:
Como usamos modelos, cuando implementamos componentes, podemos usarlos 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>
Algunas palabras sobre la implementación de componentes en general:
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 utiliza el componente contenedor de ListItems. A primera vista, puede parecer redundante, ya que puede sobrevivir con la construcción v-for a medida que se hacen comentarios, pero el uso de ranuras reduce en gran medida el código utilizado y le permite reutilizar el mismo elemento en varios lugares.
Pero si mira la lista de artículos, se usa en dos páginas (Índice y Categoría) con exactamente la misma llamada. En esta situación, es la decisión correcta crear el componente ArticleItems y heredarlo 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>
En esta situación, la herencia permite no duplicar la descripción de los parámetros (propiedad de accesorios), se toma del componente padre. Más sobre herencia e impurezas:
ru.vuejs.org/v2/api/#extends ,
ru.vuejs.org/v2/guide/mixins.htmlDespués de crear un nuevo componente, también debe corregir los archivos de la página:
src / pages / Category.vue (era) <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 (se convirtió) <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>
También vale la pena prestar atención al formulario para agregar comentarios. Las solicitudes no se realizan directamente a la API, pero la acción de Vuex se realiza y la solicitud a la API ya está "dentro", se actualiza el modelo de artículo necesario y se actualiza la lista de comentarios. El código del componente en sí se ve así:
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>
Después de implementar todos los componentes y páginas, implementar stubs y agregar datos de prueba, en este caso particular, la parte frontal del blog está lista. Pero como suele suceder en la práctica, esto está lejos del final del proyecto, porque después de la finalización del trabajo, comienzan los cambios :-)
7. Ediciones
Supongamos que necesitamos cambiar la visualización de los artículos en la página de categoría: deben mostrarse en 2 columnas. Y en la página principal todo debería permanecer como está.
Agregamos una propiedad cols adicional al número de columnas en el componente ArticleItems.
src / components / ArticleItems.vue (se convirtió) <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>
En la llamada de componente en la página de categorías, agregue la propiedad deseada:
src / pages / Category.vue <ArticleItems :items="articles" :cols="2"></ArticleItems>
Luego, queríamos ir a la página de visualización de artículos, agregar enlaces a artículos vecinos (hacia adelante / hacia atrás). Para hacer esto, necesitará agregar 2 captadores y enlaces a la 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>
Y la implementación de los captadores mismos:
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; }, ...
Y finalmente, necesitamos cambiar la URL de la página del artículo, de "artículo-123" a "post-123". Dado que las rutas con nombre se usan en toda la aplicación, es suficiente cambiar solo el patrón de ruta:
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
- vuex.vuejs.org/en/guide
- cli.vuejs.org/en/guide
- router.vuejs.org/ru
PS
A pesar de que resultó ser una aplicación muy simple, la estructura está hecha de tal manera que puede cambiar fácilmente algo y / o agregar:
- Los componentes se ocupan exclusivamente de las imágenes, los datos se toman del repositorio en forma de modelos y no se realizan solicitudes directas de API.
- Los modelos contienen la lógica de negocios necesaria y todas las relaciones entre entidades. Tampoco acceda a la API.
- El almacenamiento (vuex) es el enlace de conexión de todos los componentes: los datos se solicitan de la API y se convierten en modelos, a los que luego acceden los componentes.
Visualmente, esto se puede representar de la siguiente manera:

Con este esquema, el trabajo en el proyecto se puede dividir de manera segura entre tres unidades:
- Mr. layout: se dedica a la implementación de páginas y componentes, y también describe qué campos deben contener modelos y qué captadores y acciones deben estar en el repositorio;
- Mr. Front: se dedica a la implementación de la lógica de negocios y los modelos de almacenamiento Vuex, así como en el enrutamiento y todo lo demás que no concierne al diseño de Mr.
- Mr. back: participa en la implementación de servicios API en el lado del backend.
Todas las unidades pueden funcionar independientemente unas de otras si todos los puntos en común entre ellos se han discutido previamente (en general, si hubo alguna planificación antes del trabajo en el proyecto).
O si no hay un equipo como tal, es lógico ir en orden de arriba a abajo, de modo que tan pronto como sea posible haya algo que ver (implementar MVP).
Repositorio con todas las fuentes:
github.com/irpsv/vue-blog-habr