博客地址
前言
在github搜索了一些关于vue的前端源码,感觉这个demo组件少,内建自带api填充数据,以及在结构目录和设计模式可以用来参考,便打算clone下来探究探究
项目来源:github
要求:
需要一定的Vue基础
结构目录
components目录
有5个组件:
- Blog为components目录的根组件,均为其余4个组件的父组件
- BlogFeed是用来显示blog列表的组件,包括用来执行过滤显示,对于本项目来说,他的过滤显示有三种情况:
- 主界面(显示全部的blog封面列表)
- 只显示同类或者同个作者的blog封面列表
- blog详情(此时只显示该篇blog封面)
- BlogFooter显示页脚的组件
- BlogNav显示导航栏的组件
- BlogPost显示blog详情的组件
接下来从路由开始讲起
路由配置
路由的配置文件都放在router/index.js文件里
- router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Blog from '../components'
Vue.use(Router)
export default new Router({
mode: 'history',
linkActiveClass: 'active',
routes: [{
path: '/',
name: 'feed',
component: Blog
}, {
path: '/by/:author',
name: 'author',
props: true,
component: Blog
}, {
path: '/read/:post',
name: 'post',
props: true,
component: Blog
}]
})
三个路由均使用了相同的组件Blog,Blog组件可以调度其子组件的显示
1.主页面路由path: '/'
命名为feed,显示站点根目录,没有过滤任何blog
2.第二个path: '/by/:author'
是根据作者过滤blog列表并根据author变量作为props传入Blog组件中实现过滤
3.第三个path: '/read/:post'
跟上面一样,BlogFeed组件的blog列表只有一个blog,然后显示单个blog的内容详情的BlogPost组件
挂载组件
在src/main.js
import Vue from 'vue'
import App from './App'
import router from './router'
import * as resources from './resources'
import resource from './plugins/resource'
import deviceQueries from './plugins/device-queries'
import Pace from 'pace-progress'
Vue.config.productionTip = false
Vue.use(resource, {
resources,
endpoint: '/static/api'
})
Vue.use(deviceQueries, {
phone: 'max-width: 567px',
tablet: 'min-width: 568px',
mobile: 'max-width: 1024px',
laptop: 'min-width: 1025px',
desktop: 'min-width: 1280px',
monitor: 'min-width: 1448px'
})
new Vue({
router,
render: h => h(App),
mounted() {
Pace.start()
Pace.on('hide', () => {
document.dispatchEvent(new Event('app.rendered'));
})
}
}).$mount('#app')
在这里我们的Vue实例,为整个项目的根实例,components目录的Blog组件也是它的子组件(实例)
1.首先我们先引入了几个插件:router、resource插件以及api接口资源resources、device-queries(设备自适应)、pace-progress(进度条加载)
2.配置resource插件
Vue.use(resource, {
resources,
endpoint: '/static/api'
})
resource插件在/src/plugins目录里,这里得一个参数是设置本地api路径,以便resources目录文件接口调用本地资源,endpoint是将Blog*.js文件里path的路径结合来获取static里的数据
3.同样配置device-queries插件
Vue.use(deviceQueries, {
phone: 'max-width: 567px',
tablet: 'min-width: 568px',
mobile: 'max-width: 1024px',
laptop: 'min-width: 1025px',
desktop: 'min-width: 1280px',
monitor: 'min-width: 1448px'
})
设置参数就是每个设备的最大宽度
4.配置路由被写成单独的js文件了,直接引入router文件即可
5.创建Vue实例,并挂载router路由和pace-progress插件,最后将实例化后的VUe实例挂载到index.html
main.js的Vue实例将会挂载到
根目录下的index.html
<body>
<div id="app"></div>
</body>
而路由所挂载的组件都在这<router-view id="app"/>
个标签中显示,在这个项目里,各个路由都挂载的组件都是Blog组件,所以这个标签将显示Blog组件
src/App.vue:
<template>
<router-view id="app"/>
</template>
<script>
export default { name: 'app' }
</script>
<style lang="scss">
@import './sass/app';
</style>
这里的id="app"
将会在组件中最上层的标签添加id=app这个属性。
接下来开始了解项目组件
Blog组件
Blog组件是其他4个组件的父组件,也是本项目全部路由所挂载的组件,根据不同的url动态调度Blog组件以至调度各个子组件
源码:
<template>
<main class="blog" :class="{ 'blog--reading': this.post }">
<blog-nav :content="content" :filters="filters" :navs="navs"/>
<blog-feed :filters="filters"/>
<blog-post :post="post"/>
<blog-footer/>
</main>
</template>
<script>
import BlogNav from './BlogNav'
import BlogFeed from './BlogFeed'
import BlogPost from './BlogPost'
import BlogFooter from './BlogFooter'
export default {
name: 'blog',
components: { BlogNav, BlogFeed, BlogPost, BlogFooter },
resource: 'Blog',
props: {
post: String,
author: String
},
data() {
return {
navs: 0,
title: '',
labels: {
post: '',
author: ''
}
}
},
computed: {
content() {
return { title: this.title, labels: this.labels }
},
filters() {
let filters = {}
console.log(this.post)
console.log(this.author)
if (this.post) filters.post = this.post
if (this.author) filters.author = this.author
return filters
}
},
watch: {
'$route.name' (to, from) {
if (to !== from) this.navs++
}
},
mounted() {
this.$getResource('blog')
.then(x => {
// use pace hook to know when rendering is ready
})
}
}
</script>
组件模版:
<template>
<main class="blog" :class="{ 'blog--reading': this.post }">
<blog-nav :content="content" :filters="filters" :navs="navs"/>
<blog-nav :filters="filters"/>
<blog-post :post="post"/>
<blog-footer/>
</main>
</template>
用一个main标签包含了其他四个组件,并设置了一个动态class'blog--reading': this.post
如果是有post即是从路由传来的blog标题名则添加'blog--reading'的样式
blog-nav、blog-nav、blog-post三个组件都设置了父组件属性绑定,用于将数据传递给子组件
逻辑部分:
<script>
import BlogNav from './BlogNav'
import BlogFeed from './BlogFeed'
import BlogPost from './BlogPost'
import BlogFooter from './BlogFooter'
export default {
name: 'blog',
components: { BlogNav, BlogFeed, BlogPost, BlogFooter },
resource: 'Blog',
props: {
post: String,
author: String
},
data() {
return {
navs: 0,
title: '',
labels: {
post: '',
author: ''
}
}
},
computed: {
content() {
console.log(this.labels)
return { title: this.title, labels: this.labels }
},
filters() {
let filters = {}
console.log(this.post)
console.log(this.author)
if (this.post) filters.post = this.post
if (this.author) filters.author = this.author
console.log(filters)
return filters
}
},
watch: {
'$route.name' (to, from) {
if (to !== from) this.navs++
console.log(this.navs)
}
},
mounted() {
console.log(this.$route.name)
console.log(this.filters)
this.$getResource('blog')
.then(x => {
// use pace hook to know when rendering is ready
})
}
}
</script>
1.首先引入了四个组件
2.在实例部分我们设定了Blog组件的name属性这个属性用于使用模版标签。
并在components属性中配置四个子组件,以便在父组件模版中使用。
resource属性是用于获取本地api接口文件resources中index所输出的对应接口,这里对应的resources/Blog.js。如果不填写这个属性,则无法在生命周期函数中获取api中的数据。
props属性接受路由传进来的参数,并验证他的数据类型是否是string
3.在data()方法中,我们定义navs、title、labels对象,navs用于计数,传递到nav组件会用到,title和labels都是从Blog.js获取数据的属性,只是在重新在组件中定义,以用this获取数据。
resources/Blog.js:
export default {
blog() {
return {
path: '/blog.json',
resolve: (response, mappers) => {
let blog = response.results[0]
return mappers.merge({
title: blog.title,
labels: {
post: blog.post_label,
author: blog.author_label
}
})
}
}
}
}
title: blog.title获取网站的标题
labels: {...}这里有两个,如果是在blog详情页面则显示post的返回标签,如果是author则是显示分类时候的返回标签。
这里数据在
static/api/blog.json:
{
"results": [{
"title": "In Plain Vue",
"post_label": "Exit reading mode",
"author_label": "View all authors"
}]
}
计算属性computed
content函数
content() {
// 因模版使用了计算属性方法,则优先渲染DOM,计算属性会优先处理,这个弹窗会出现两次
// 第一次没有在mounted生命周期函数中获取数据,弹出一个空字符串(这里在data中定义)
// 第二次在执行生命周期函数mounted时获取到数据,重新执行了一遍计算属性的方法,这次弹出就含有字符的字符串
alert(this.labels.post)
console.log(this.labels)
return { title: this.title, labels: this.labels }
}
这个计算属性绑定到blog-nav组件,用于该组件接受绑定数据,返回了网站的标题和显示的labels,如何选择labels的post还是author,会根据filters方法来判断。
filters方法:
filters() {
let filters = {}
if (this.post) filters.post = this.post
if (this.author) filters.author = this.author
return filters
}
1.声明了一个只在该函数为作用于的一个空对象filters
2.然后判断是否有路由传递post属性或者是
author属性,有的话将在filters将设置属性(只可能是含有一个属性),最后将返回filters
watch监听属性
用于监听参数变化后所执行的函数
watch: {
'$route.name' (to, from) {
if (to !== from) this.navs++
console.log(this.navs)
}
}
这里我们监听route.name
,即路由命名空间,跳转不同的路由,不同的路由有不一样命名空间,参数是新的值和旧的值,如果新旧值不同则说明路由变化过,nav计数就加一,这个用于确定返回的上一次路由,后面在nav组件会提到。
mounted生命周期函数
mounted() {
this.$getResource('blog')
.then(x => {
// use pace hook to know when rendering is ready
})
}
1.这里会先到获取Blog.js输出函数blog(),该函数获取api数据,以至可以使用this.title和this.labels,这两个属性值发生改变此时会重新执行计算属性,然后在执行then后续的逻辑。
Blog讲解组件到此为止,该组件主要是计算属性和生命周期函数的配合获取数据,因优先处理DOM,计算属性会比mounted周期函数先执行,此时还是空对象,到后面执行了mounted函数获取到api数据,重新执行了计算属性,以调度了各个组件的执行。
BlogNav组件
模版
<template>
<nav class="nav">
<h1 class="nav__title">
<router-link to="/">{{ content.title }}</router-link>
</h1>
<transition-group tag="menu" name="nav__item" class="nav__menu">
<li v-for="label in labels" class="nav__item" :key="label" @click="navBack">
<i class="nav__item--icon"></i>
<span class="nav__item--label">{{ label }}</span>
</li>
</transition-group>
</nav>
</template>
1.网站标题设定了路由连接,指定了首页路由
2.```<transition-group></transition-group>``这个是Vue的内置组件,用于其内容变化列表过度,具体可以查看Vue官方介绍
3.该列表用v-for循环了li标签,同时v-on绑定绑定了方法navBack,后面会提到这个两个
逻辑部分:
<script>
export default {
name: 'blog-nav',
props: {
navs: Number,
content: Object,
filters: {
type: Object,
default: () => {}
}
},
computed: {
labels() {
console.log(Object.keys(this.filters)
.map(filter => this.content.labels[filter]))
return Object.keys(this.filters)
.map(filter => this.content.labels[filter])
}
},
methods: {
navBack() {
if (this.navs && !this.filters.author) this.$router.go(-1)
else this.$router.push('/')
}
}
}
</script>
计算属性computed
computed: {
labels() {
return Object.keys(this.filters)
.map(filter => this.content.labels[filter])
}
},
1.计算属性的labels方法用于模版循环,根据父组件传过来的filters来判断返回显示哪个字符串内容
2.在父组件获取的filters中获取键名,执行map方法,以键名为参数执行函数获取在content中labels的值,返回一个数组
methods属性
navBack方法
methods: {
navBack() {
if (this.navs && !this.filters.author) this.$router.go(-1)
else this.$router.push('/')
}
1.该方法绑定了在显示labels循环,用于判断返回的路由
2.navs不为0同时不在author路由时,执行返回到上一个路由
3.在这项目中只有一种情况,即从author路由进入post路由时候,返回时执行到上个路由(author路由)
BlogNav组件很简单,设计了api接口,并将接口数据稍加处理便用于模版中,逻辑很简单,接下来是比较复杂的BlogFeed组件
BlogFeed组件
用于显示blog列表,无论在哪个路由,blog列表都会有(blog个数根据是否过滤),只是在不同路由在显示数量有所不同。
组件模版:
<template>
<transition-group tag="ul" :name="transition" class="blog__feed">
<li v-for="post in feed" class="preview" :key="post.id">
<figure class="preview__figure" :class="figureClass" :style="getBgImg(post.image)">
<transition name="v--fade">
<figcaption v-if="!reading || $device.phone" class="preview__details">
<router-link class="preview__title"
:to="`/read/${post.id}`"
@click.native="scrollTo(0, 220, scrollDelay)">
{{ post.title }}
</router-link>
<div class="preview__meta">
<time class="preview__published">
{{ prettyDate(post.published) }}
</time>
<router-link class="preview__author"
:to="`/by/${kebabify(post.author)}`"
@click.native="scrollTo(0, 220, scrollDelay)">
{{ post.author }}
</router-link>
</div>
</figcaption>
</transition>
</figure>
</li>
</transition-group>
</template>
1.这里同样也使用了transition-group组件,并使用tag属性真实渲染为ul,过度效果前缀名name属性绑定了一个方法,因为需要动态改变它的过度效果,根据组件状态不同,过度也不同。
2.这里li循环了feed方法,feed方法在计算属性中,后面会讲到
3.用了figure和figcaption标签,作为封面和标题,figcaption用一个过度组件transition包含住
4.v-if="!reading || $device.phone"
在figcaption添加了v-if,当阅读详情时(路由有post)不显示标题,但在手机模式下显示标题。
5.标题figcaption含有两个路由,分别到post和author
逻辑部分:
<script>
import { scrollTo, kebabify, prettyDate } from '../helpers'
export default {
name: 'blog-feed',
resource: 'BlogFeed',
/*用于获取本地数据,resource插件所需要的属性,
该值会执行到resources文件夹的js脚本,
而js脚本会定向到api的数据,不添加发生错误mounted的getResource函数*/
props: {
filters: {
type: Object,
default: () => {}
}
},
data() {
return {
posts: [],
transition: 'preview-appear'
}
},
computed: {
reading() { return this.filters.post },
scrollDelay() { return (this.$device.phone) ? 0 : 560 },
figureClass() {
return { 'preview__figure--mobile': this.$device.phone && this.reading }
},
feed() {
const filterBy = {
post: (filter, { id }) => filter === id,
author: (filter, { author }) => filter === this.kebabify(author)
}
/*过滤列表为0时即为显示文章列表*/
if (!Object.keys(this.filters).length) return this.posts
/*post为数组的元素,是回调函数的参数,会遍历每个元素检验其回调函数逻辑正确
回调函数会返回处理参数的过后的逻辑正确性(即通过回调函数)
返回true表示保留该元素,false则不保留*/
return this.posts.filter(post => {
/* Object.keys(object) 获取对象的所有keys*/
/* every() 方法测试数组的所有元素是否都通过了指定函数的测试*/
return Object.keys(this.filters).every(filter => {
return filterBy[filter](this.filters[filter], post)
})
})
}
},
methods: {
scrollTo,
kebabify,
prettyDate,
getBgImg(src) {
return { backgroundImage: `url(${src})` }/*在字符串引入变量*/
},
stackPosts(posts) {
let interval
const stack = () => {/* 声明一个局部变量,变量是个箭头函数*/
this.posts.push(posts.shift())/*arry.shift()删除数组第一个元素并返回该元素*/
if (!posts.length) {
this.transition = 'preview'
clearInterval(interval)
}
}
interval = setInterval(stack, 125)
}
},
mounted() {
this.$getResource('feed')
.then(posts => {
// posts为static/api下的feed.json文件
/* filters对象没有属性时执行stackPosts,即为显示文章列表*/
if (!Object.keys(this.filters).length) {
this.stackPosts(posts)
}else {
this.posts = posts
this.transition = 'preview'
}
})
}
}
</script>
1.引入了三个辅助函数,scrollTo, kebabify, prettyDate分别用于 滚动定位,格式化post.author名以跟author路由参数一样和日期格式化函数
三个文件都在src/helpers.js
import animateScroll from 'scrollto-with-animation'
export const scrollTo = (pos, duration = 600, delay = 0) => new Promise(resolve => {
setTimeout(() => {
animateScroll(document.documentElement, 'scrollTop', pos, duration, 'easeInOutSine', resolve)
}, delay)
})
export const kebabify = (words) =>
words
.toLowerCase()
.replace(' ', '-')
export const prettyDate = (date) =>
new Date(date)
.toString()
.split(' ')
.slice(0, 4)
.join(' ')
.replace(/( \d+)$/, ',$1')
2.设定组件模版名和确定resource接口BlogFeed
3.设定api接口filters
4.定义一个posts数组用来接受api的数据
5.定义一个动态过渡效果transition
计算属性computed
这里只讲一个比较重要方法
feed:
feed() {
const filterBy = {
post: (filter, { id }) => filter === id,
author: (filter, { author }) => filter === this.kebabify(author)
}
/*过滤列表为0时即为显示文章列表*/
if (!Object.keys(this.filters).length) return this.posts
/*post为数组的元素,是回调函数的参数,会遍历每个元素检验其回调函数逻辑正确
回调函数会返回处理参数的过后的逻辑正确性(即通过回调函数)
返回true表示保留该元素,false则不保留*/
return this.posts.filter(post => {
/* Object.keys(object) 获取对象的所有keys*/
/* every() 方法测试数组的所有元素是否都通过了指定函数的测试*/
return Object.keys(this.filters).every(filter => {
return filterBy[filter](this.filters[filter], post)
})
})
}
1.定义一个对象filterBy,用于通过此对象中的函数来返回正确需要显示的post
2.若没有传入路由参数,则返回全部的posts
3.返回一个经过过滤的posts数组,posts的每个元素只有通过测试的回调函数返回true,才能正确的被显示,返回false的会被过滤掉。这个feed方法经过多次回调最后只是过滤掉任何ID不匹配的帖子。这是一个非常好的实现方式,之后我们要添加过滤项(例如category),我们只需要在Blog组件的filters方法接收相应的路由参数和filterBy对象中添加属性,然后添加判断接受的路由参数和post中的数据是否吻合,即可实现过滤,非常轻松快捷。
methods属性
methods: {
scrollTo,
kebabify,
prettyDate,
getBgImg(src) {
return { backgroundImage: `url(${src})` }/*在字符串引入变量*/
},
stackPosts(posts) {
let interval
const stack = () => {/* 声明一个局部变量,变量是个箭头函数*/
this.posts.push(posts.shift())/*arry.shift()删除数组第一个元素并返回该元素*/
if (!posts.length) {
this.transition = 'preview'
clearInterval(interval)
}
}
1.getBgImg绑定了style属性, 设定css样式背景图片
2.stackPosts,后面生命周期函数会调用他,此方法用来以对接收来的posts中的每个元素逐个显示,用到setInterval方法反复调用了stack,shift方法逐个从posts中传入到组件中的posts数组,一旦组件中的posts数组发生变化计算属性就立刻执行,我们可以在feed方法在
if (!Object.keys(this.filters).length) {console.log(this.posts.length); return this.posts}
中添加来测试,console的数字是逐个增加直到api的posts为0停止并最后用clearInterval清除反复调用任务
生命周期函数mounted
mounted() {
this.$getResource('feed')
.then(posts => {
// posts为static/api下的feed.json文件
/* filters对象没有属性时执行stackPosts,即为显示文章列表*/
if (!Object.keys(this.filters).length) {
console.log(Object.keys(this.filters).length)
console.log(this.posts)
this.stackPosts(posts)
} else {
this.posts = posts
this.transition = 'preview'
}
})
}
同样是在resources/BlogFeed.js中出书的feed方法中获取数据,then的参数是获取到的数据,并用一个箭头函数执行获取数据后的操作,这里没有获取到路由参数的情况下将调用stackPosts,否则直接将posts赋给组件中的posts。
接下来就是最后一次组件-BlogPost,BlogFooter没有什么代码,这个省略过去
BlogPost组件
模版:
<template>
<transition name="post">
<article v-if="allReady" class="post">
<header class="post__header">
<h2 class="post__title">{{ title }}</h2>
<h3 class="post__meta">by <router-link class="post__author"
:to="`/by/${kebabify(author)}`">{{ author }}</router-link>
<span class="post__sep"></span>
<time>{{ prettyDate(published) }}</time>
</h3>
<blockquote class="post__subtitle">{{ description }}</blockquote>
</header>
<section class="post__body rte" v-html="content"></section>
<footer class="post__footer">
<vue-disqus v-if="commentsReady" shortname="vue-blog-demo"
:key="post" :identifier="post" :url="`https://vue-blog-demo.netlify.com/read/${post}`"/>
</footer>
</article>
</transition>
</template>
1.也同样在article标签添加了过度效果
2.article标签显示条件绑定了allReady方法
3.header标签包含了title、author路由、published和description
4.用v-html属性,将blog的content以标准html的方式渲染
5.footer标签添加了disqus评论模块
逻辑部分:
<script>
import VueDisqus from 'vue-disqus/VueDisqus'
import { kebabify, prettyDate } from '../helpers'
export default {
name: 'blog-post',
resource: 'BlogPost',
components: { VueDisqus },
props: { post: String },
data() {
return {
title: '',
author: '',
content: '',
published: '',
description: '',
commentsReady: false,
ready: false
}
},
computed: {
allReady() {
return this.ready && this.post;
}
},
watch: {
post(to, from) {
if (to === from || !this.post) return;
//this.commentsReady = false
this.$getResource('post', to)
.then(this.showComments)
.then(() => {
this.ready = true;
});
}
},
methods: {
kebabify,
prettyDate,
showComments() {
// This is injected by prerender-spa-plugin on build time, we don't prerender disqus comments.
// 运行build时,将注入预渲染插件,渲染的路由为各个post,可以在bulid配置文件中查看详细配置,不会预渲染disqus
if (window.__PRERENDER_INJECTED &&
window.__PRERENDER_INJECTED.prerendered) {
return;
}
// 定时方法:一秒后运行this.commentsReady = true
setTimeout(() => {
this.commentsReady = true
}, 1000)
}
},
//该生命周期函数用于在路由post页面刷新时可调用api
mounted() {
if (!this.post) {
this.ready = true;
return;
}
this.$getResource('post', this.post)
.then(this.showComments)
.then(() => {
this.ready = true;
});
}
}
</script>
1.引入了vue的disqus组件和一些格式化方法
2.同样的设置组件标签名和resource的api接口,设计父组件传来的api,还有将组件传进该实例。
3.设定该组件的api获取的资源属性,到后面$getResource
会将aip属性赋给组件的属性来完成替换,还有两个重要布尔属性,用来确定在满足何等条件时显示post和comments
计算属性computed
computed: {
allReady,() {
return this.ready && this.post;
}
},
allReady,绑定了article标签,返回true时才显示article的所有内容。
一旦ready或者post的值有改变,立刻刷新allReady方法
监听属性watch
watch: {
post(to, from) {
if (to === from || !this.post) return;
//this.commentsReady = false
this.$getResource('post', to)
.then(this.showComments)
.then(() => {
this.ready = true;
});
}
}
监听post参数,如果新旧值一样或者没有post参数,则直接return。个人认为这里并不需要判断新旧值是否一样,直接判断是否处在post路由即可,处在post路由则执行api接口方法获取资源,获取成功后执行showComments方法,然后执行this.ready = true;
将article标签显示出来
methods属性
methods: {
kebabify,
prettyDate,
showComments() {
// This is injected by prerender-spa-plugin on build time, we don't prerender disqus comments.
// 运行build时,将注入预渲染插件,渲染的路由为各个post,可以在bulid配置文件中查看详细配置,不会预渲染disqus
if (window.__PRERENDER_INJECTED &&
window.__PRERENDER_INJECTED.prerendered) {
return;
}
// 定时方法:一秒后运行this.commentsReady = true
setTimeout(() => {
this.commentsReady = true
}, 1000)
}
},
这里showComments方法会在api获取数据成功时执行,一秒后将会将commentsReady设定会true,至于预渲染插件则会在build时才启动。
生命周期函数mounted
mounted() {
if (!this.post) {
// this.ready = true;
return;
}
this.$getResource('post', this.post)
.then(this.showComments)
.then(() => {
this.ready = true;
});
}
该生命周期函数用于在路由post页面刷新时可调用api,不在post路由时直接return,个人认为将ready设置true也没有必要的,有人可能疑问问什么要获取两次api,你可以尝试一下将mounted函数注视掉,你会发现从feed路由进去post路由是完全没有问题的,但是你刷新一下就不会显示article的内容,原因很简单:重新刷新post路由页面,监听函数不会执行所以我们要在生命周期函数中获取api
至此可以说该项目的组件我给梳理了一遍
总结
项目难度不大,只有几个组件,但是里面设计模式真的是非常cool,尤其是过滤posts的BlogFeed组件,好的设计模式,在后续开发中可以更加快速的添加功能,而且也更加节省运算资源。在本地api设计中也有非常炫酷的操作,在api获取时使用了方法,该方法中处理了一些数据的显示(添加html标签)和API的数据结构定义(在组件业务中无需将api获取的数据属性赋值到组件属性)在组件使用数据时更加得心应手,与一般在vue组件内部调用api并做数据处理的方式不同,需要将数据处理转变为创建可替换的数据访问层