Vue项目剖析

作者: 可爱的煎饼 | 来源:发表于2019-11-14 22:22 被阅读0次

    博客地址

    zengzhengrong

    前言

    在github搜索了一些关于vue的前端源码,感觉这个demo组件少,内建自带api填充数据,以及在结构目录和设计模式可以用来参考,便打算clone下来探究探究
    项目来源:github
    要求:
    需要一定的Vue基础

    结构目录

    image

    components目录

    有5个组件:

    1. Blog为components目录的根组件,均为其余4个组件的父组件
    2. BlogFeed是用来显示blog列表的组件,包括用来执行过滤显示,对于本项目来说,他的过滤显示有三种情况:
      • 主界面(显示全部的blog封面列表)
      • 只显示同类或者同个作者的blog封面列表
      • blog详情(此时只显示该篇blog封面)
    3. BlogFooter显示页脚的组件
    4. BlogNav显示导航栏的组件
    5. 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并做数据处理的方式不同,需要将数据处理转变为创建可替换的数据访问层

    相关文章

      网友评论

        本文标题:Vue项目剖析

        本文链接:https://www.haomeiwen.com/subject/vjbdictx.html