
大家好! 在本文中,我们将考虑使用Vue的所有魅力(包括Vuex和Router)开发一个简单的Vue博客的前端。 另外,我们还讨论应用程序的结构以及如何使用容器和路由器。
首先,我们将定义为应用程序创建前端的阶段(在本例中为博客):
- 策划
- 应用框架
- 建立模型
- 业务逻辑的实现
- 添加页面和路线
- 添加组件
- 编辑
1.规划
首先,我们将写下SPA包含的内容。 您应该直接从页面开始,因为这是用户直接与之交互的内容(如果像TDD测试那样将其四舍五入非常困难-我们首先描述应用程序应该做什么,用户将如何与之交互,然后我们已经在进行实现了)。
那么什么页面将是:
- 主页-它将包含热门类别,最新评论和文章
- 浏览类别-特定类别的文章列表
- 查看新闻-直接查看新闻内容,评论列表以及该类别中的其他文章
如果放置页面原型,则它看起来像这样:
尚不清楚,但这只是原型:-)我将立即解释为什么不使用原型制作软件绘制它的原因:因为在纸上它更快,更容易,有时您仍然需要动手,但是当涉及到什么时候您需要在变得非常悲伤的地方签名。
根据获得的页面,我们列出了以下组件:
我们将在实现组件本身的过程中就已经对组件进行了优化,在这个阶段这是没有必要的。
最后,在描述了所有用户交互点之后,我们描述了应用程序的本质:
- 文章(标题,内容,评论列表)
- 类别(标题,新闻列表)
- 评论(内容)
重要的是要注意,描述的是业务逻辑(BL)的本质,而不是基表。 在开发和计划零件的前部以及大部分后部(仅包括数据层)时,有必要使用BL实体而不是“表”进行操作。 此外,实体应仅具有应用程序本身中使用的内容。 接触未来是好的,但很少有这种未来出现,并且在仅放下已使用的功能并且应用程序结构
可扩展的情况下 ,将来添加功能就没有问题,并且当前没有多余的东西。
2.应用框架
我们继续创建结构。 我们在控制台中执行以下命令:
npm install -g @vue/cli vue create vue-blog-habr -n -d -m npm cd vue-blog-habr
这些命令在适当的目录中创建vue-blog-habr项目。 有关vue-cli及其使用的参数的更多信息,请
参见此处 。
最后,我们得到了标准的项目结构:

立即安装我们需要的软件包:
npm install vue-router vuex axios bootstrap-vue sass-loader npm install --save-dev --unsafe-perm node-sass
注册使用的模块:
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')
我们通过以下方式纠正项目目录结构:

目录说明:
- api-包含负责与服务器“通信”的文件
- 资产 -各种二手静态资源:图片,图标,...
- 组件 -应用程序组件,在路由器中不使用“页面”
- 模型 -业务逻辑模型,它们可以被前面使用的域功能所饱和
- pages-路由器中使用的页面组件
- 路由器 -路由文件
- 服务 -与业务逻辑无关的辅助服务。 例如,显示服务,其中包含一种用于获取页面上元素的坐标的方法
- store -Vuex存储文件
- 样式 -样式文件
并运行测试服务器:
npm run serve
最后一条命令启动在运行时模式下应用了所有项目编辑的服务器。 要访问浏览器,请访问:
localhost :8080
3.创建模型
我们的应用程序中没有复杂的逻辑,但是无论如何,您都需要创建模型。
这有几个原因:
- 对象规范 -任何任意对象,可以包含任何属性和方法。 使用类时,我们知道特定对象具有哪些属性和方法
- 组件参数的键入 -从上一步开始:Vue组件控制输入属性的类型
- 便利性 -就类而言,我们与主题领域的实体合作,因此代码变得更加清晰。 类还提供其他功能,例如获取/设置/静态
所有模型都放在适当的目录中。 本文的类如下所示:
src /模型/ 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.业务逻辑的实现
出乎意料的是,但是在此阶段,不需要执行业务逻辑本身。 您需要创建一个存储对象,并立即将其分为模块:
src /商店/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, }, })
在模块本身中,我们将进一步描述所使用的所有变异/获取器/动作。 最初,存储库如下所示:
src /商店/模块/ blog.js export default { state: {}, getters: {}, mutations: {}, actions: {}, }
另外,我们启动了一个与API一起使用的对象,所有请求都将通过该对象传递。 在实施时,绝对不需要后端,因此您可以使用静态数据:
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) {
是什么让vuex得以使用:
- 纯度 -组件不会变成“知道太多”的上帝对象
- 一致性 -如果您有多个使用相同数据的组件,那么在使用vuex容器更改数据时,它们(数据)将在整个应用程序中进行更新,而不仅仅是在启动更新的组件中进行更新
- 便利 -与描述每个组件中的参数类型和/或API调用相比,访问商店要容易得多,也更方便
- 测试 -尽管是谁做的?
5.添加页面和路线
根据先前计划的结构,我们需要创建4页:索引,类别,文章以及404页。 在src / pages目录中,添加适当的文件:
如果404页应具有带有个性化设计的个人页,则可以通过以下方式更改入口点:
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>
主页文件如下所示:
src /页面/ 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>
页面实现后,我们立即将所有使用的getter和操作添加到vuex存储库(不执行):
src /商店/模块/ blog.js export default { state: { articles: [], comments: [], categories: [],
有几点要注意:
- 当需要获取数据时,需要联系状态。 如果在接收数据后需要对数据进行一些操作,则最好为此创建吸气剂;
- 当您需要对数据进行某些处理时,您需要引用操作,而不是突变。 一个动作可以包括多个突变并执行其他数据操作以及写入状态且没有异步限制
- 无需直接向API / Rest发送请求。 通过Vuex请求所有数据时,这将确保整个应用程序的状态一致(如果存储本身正确完成)
- 在所有链接和程序导航中,您需要引用命名的路线。 这将使您无需编辑链接和程序导航即可轻松更改路线本身。 这自然是关于SPA内部的链接,必须照常指定路由器未处理的地址
在创建并填充页面本身之后,您需要向路由器添加适当的规则:
src /路由器/ 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'), }, ] })
所有路由器参数都可以在文档中找到:
router.vuejs.org/ru/api/#constructor options-router
6.添加组件
实施完所有页面后,我们将获得以下主要组件列表:
- 分类项
- ArticleItem
- CommentItem
- 评论表
和辅助:
由于我们使用模型,因此在实现组件时,可以使用它们来表示参数:
src /组件/ 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>
关于主要组件实现的几句话:
src /页面/ 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>
此页面使用ListItems的包装器组件。 乍一看,这似乎是多余的,因为您可以在进行注释时使用v-for构造,但是使用插槽会大大减少所使用的代码,并允许您在多个位置重用同一元素。
但是,如果您查看文章列表,则会在两个页面(索引和类别)上使用完全相同的调用来使用它。 在这种情况下,正确的决定是创建ArticleItems组件并从ListItems继承它:
src /组件/ 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>
在这种情况下,继承不允许重复参数的描述(props属性),它取自父组件。 有关继承和杂质的更多信息:
ru.vuejs.org/v2/api/#extends,ru.vuejs.org/v2/guide/mixins.html创建新组件后,还需要修复页面文件:
src /页面/ Category.vue(是) <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 /页面/ Category.vue(成为) <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>
还要注意添加评论的表格。 不会直接向API发出请求,但是会执行Vuex操作,并且对API的请求已经在“内部”,更新了必需的Article模型,并更新了注释列表。 组件本身的代码如下所示:
src /组件/ 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>
在实现了所有组件和页面,实现了存根并添加了测试数据之后,在这种情况下,博客的前端已准备就绪。 但是,正如通常在实践中发生的那样,这离项目的结束还很遥远,因为在工作完成之后,更改就开始了:-
7.编辑
假设我们需要更改类别页面上文章的显示:文章应显示在2列中。 在主页上,所有内容都应保持原样。
我们向ArticleItems组件中的列数添加了一个额外的cols属性。
src /组件/ ArticleItems.vue(成为) <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>
在类别页面上的组件调用中,添加所需的属性:
src /页面/ Category.vue <ArticleItems :items="articles" :cols="2"></ArticleItems>
然后,我们想转到文章查看页面,添加到相邻文章的链接(前进/后退)。 为此,您需要向页面添加2个getter和链接:
src /页面/ 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>
以及吸气剂本身的实现:
src /商店/模块/ 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; }, ...
最后,我们需要将文章页面的URL从“ article-123”更改为“ post-123”。 由于已在整个应用程序中使用了已命名的路由,因此仅更改路由模式就足够了:
src /路由器/博客/ 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'), }, ];
有用的文献
- vuex.vuejs.org/en/guide
- cli.vuejs.org/en/guide
- router.vuejs.org/ru
聚苯乙烯
尽管事实证明这是一个非常简单的应用程序,但该结构的制作方式使您可以轻松更改和/或添加:
- 这些组件专门处理视觉效果,数据以模型的形式从存储库中获取,并且不发出直接API请求。
- 模型包含必要的业务逻辑以及实体之间的所有关系。 也不要访问API。
- 存储(vuex)是所有组件的连接链接:从API请求数据并将其转换为模型,然后由组件访问。
在视觉上,这可以表示如下:

使用此方案,可以将项目的工作安全地划分为三个单元:
- layout先生-从事页面和组件的实现,还描述了哪些字段应包含模型,以及哪些getter和action应在存储库中;
- Front先生-从事业务逻辑和Vuex存储模型的实现,以及路由和与布局先生无关的所有其他工作;
- back先生-在后端侧从事API服务的实现。
如果先前已经讨论了它们之间的所有共同点(通常,如果在进行该项目之前有任何计划),则所有单元可以彼此独立地工作。
或者,如果没有这样的团队,从上到下按顺序进行是合乎逻辑的,这样就可以尽快注意一些事情(推出MVP)。
包含所有资源的存储库:
github.com/irpsv/vue-blog-habr