Vue全家桶仿网易新闻全栈项目--前端篇

做为当前热门的js框架之一,以数据驱动和组件构建为核心的vue实在是使人着迷。用vue写个项目,是对这段学习时间的一个自我巩固与自我提高。我在写这个项目的过程当中,爬过不少坑,碰过不少壁,把他们分享出来,但愿能让各位看官老爷吸取一些有价值的东西。css

先挂个页面基本布局,里面的功能逻辑,咱们后面在谈。

项目启动前作个优化可好

咱们都知道,vue的产生核心之一在于开发大型单页应用,一个大型单页应用归根结底仍是一个html,若是不对项目进行一些优化的话,浏览器会将全部没有设置路由懒加载的组件,webpack打包生成的依赖js,页面样式文件所有加装完毕再将网页渲染出来。这是将致使一个很是可怕的白屏时间,带给用户的体验效果极差!咱们能够从几个方面减轻压力...html

vue全家桶外部引入

在webpack配置中能够设置externals(外部)参数,不对一些依赖进行打包,而已cdn的形势引入。配置也很简单,代码以下:前端

externals: {
    'vue': 'Vue',
    'vuex': 'Vuex',
    'vue-router': 'VueRouter'
  }
复制代码

json的key值为引入资源的名字,value值表示该模块提供给外部引用的名字,由对应的库自定。例如,vue为Vue,vue-router为VueRouter.
别忘了在index.html用script标签引入你以前定义在externals中的依赖哦。vue

路由懒加载

vue-router提供的路由懒加载可让组件按需加载,减轻加载压力。配置示例:node

{
          path: '/Headlines',
          name: 'Headlines',
          component (resolve) {
            require(['@/page/homeComponents/Headlines'], resolve)
          }
        },
        {
          path: '/Joke',
          name: 'Joke',
          component (resolve) {
            require(['@/page/homeComponents/Joke'], resolve)
          }
        },
        {
          path: '/City',
          name: 'City',
          component (resolve) {
            require(['@/page/homeComponents/City'], resolve)
          }
复制代码

不过不要过分使用路由懒加载,不然在切换的路由的时候可能会出现闪屏的状况哦...webpack

组件库按需引入

若是你使用了一些vue的ui框架,很是不推荐在main.js中直接将全部组件引入,而是引入的项目中须要的组件,这样不会形成资源浪费,也能减轻浏览器压力。(由于个人项目使用的是vant组件库因此以vant为例)ios

  • 不推荐
import Vue from 'vue';
import Vant from 'vant';
import 'vant/lib/index.css';

Vue.use(Vant);
复制代码
  • 推荐

使用babel-import插件
babel-plugin-import 是一款 babel 插件,它会在编译过程当中将 import 的写法自动转换为按需引入的方式。 在babelrc中加入以下配置:css3

// 在.babelrc 中添加配置
// 注意:webpack 1 无需设置 libraryDirectory
{
  "plugins": [
    ["import", {
      "libraryName": "vant",
      "libraryDirectory": "es",
      "style": true
    }]
  ]
}
复制代码

手动引入git

import Button from 'vant/lib/button';
import 'vant/lib/button/style';
复制代码

解决跨域问题

由于这是一个先后端分离的项目,因此在访问接口时会出现跨越问题。通常解决跨域问题三种方式:es6

  • 前端使用代理服务器解决跨域
  • 后端配置
  • 使用jsonp

jsonp只能处理get请求,因此在对一些开放形接口会使用jsonp,在前端开发过程当中使用proxy代理解决跨域问题居多。
直接在package.json文件中配置

"proxy":"http://localhost:3000"
复制代码

也能够在webpack中详细配置

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api1': { // 若是API中有这个字符串,那么就开始匹配代理,
        target: 'http://www.xxx.com/', // 将跨域前往的目标域名或IP地址
        pathRewrite: {'^/api' : ''}, // 路径重写,/api会被替换为空
        changeOrigin: true,     // target是域名的话,须要这个参数,
        secure: false,          // 设置支持https协议的代理
      },
      '/api2': {
          .....
      }
    }
  }
};
复制代码

首页

导航之间的联动

  • 顶部导航是一排超出隐藏的inline-block元素,点击能够切换首页的二级路由
  • 首页的二级路由装载在一个swiper里,切换swiper状态能够切换路由,swiper的移动也能够带动顶部导航移动
  • 隐藏设置能够修改顶部导航,也能够切换路由

三者受制于vuex的一个数组,和数组当前的激活索引,只须要改变数组,数组索引就能完成三者之间的联动

将首页须要的全局变量定义在vuex中,并加上相应的mutations和actions

const moduleHome = {
  state: {
    navbar: [
      {
        name: '头条',
        component: 'Headlines'
      },
      {
        name: '段子',
        component: 'Joke'
      },
      {
        name: '南昌',
        component: 'City'
      },
      {
        name: '笑话',
        component: 'Easetime'
      },
      {
        name: '图片',
        component: 'Picture'
      }
    ],
    moreList: [
      {
        name: '星座',
        component: 'Constellation'
      },
      {
        name: '音乐',
        component: 'Musi'
      },
      {
        name: '教育',
        component: 'Education'
      },
      {
        name: '佛学',
        component: 'Buddhism'
      }
    ],
    active: 0,
    caches: {
      headlines: []
    },
    page: {
      headlines: 0
    }
  },
  mutations: {
    changeActive (state, index) {
      state.active = index
    },
    resetActive (state) {
      state.active = 0
    },
    changeCache (state, opt) {
      let { arr, name } = opt
      state.caches[name] = arr
    },
    changePage (state, opt) {
      let { page, name } = opt
      state.page[name] = page
    },
    changeNavbar (state, navbar) {
      state.navbar = navbar
    },
    changeMoreList (state, moreList) {
      state.moreList = moreList
    }
  },
  actions: {
    changeCache: ({commit}, opt) => commit('changeCache', opt),
    changePage: ({commit}, opt) => commit('changePage', opt),
    changeActive: ({commit}, index) => commit('changeActive', index),
    resetActive: ({commit}) => commit('resetActive')
  }
}
复制代码

使用vuex

在使用vuex以前咱们要理解为何要使用它,用bus不行吗,固然使用bus也能够完成组件间的通讯。可是一旦共享状态的组件数量过多,使用bus经过传递的参数的办法未免过于繁琐,组件之间的状态同步须要用多个函数来维持,增大了维护代码的压力。
我把首页的状态抽成了一个module,官方文档对在module中怎么使用action,state,没有详细描述,它的用法以下:

//  导出方式
export default new Vuex.Store({
  modules: {
    home: moduleHome,
    global: moduleGlobal
  }
})
// 使用state
$store.state.home   // 能够看出就算将状态模块化,它其实也被隐
                       性包含与一个更大的state中
// 使用actions

对于actions的使用其实不管action在哪一个module中均可以被mapActions映射出来
因此用法和不用module是同样的(没想到吧)

import { mapActions } from 'vuex'
...mapActions([
      'changeCache',  // home中的action
      'changeLogin'  // global中的action
    ])
复制代码

顶部导航

顶部导航的渲染依赖于home(vuex中的module)的navbar数组,点击item能够切换navbar的激活状态active,若是导航有超出隐藏,若是active的增量绝对值大于等于2,顶部导航就会自动滚动

active: function (newVal, oldVal) {
      // 向右滑动事件导航跟踪处理
      if (newVal % 2 === 0 && newVal - oldVal === 1) {
        let interval = setInterval(() => {
          this.$refs['scroll'].scrollLeft += this.$refs['scroll'].offsetWidth * 0.2 / 10
          setTimeout(() => {
            clearInterval(interval)
          }, 100)
        }, 10)
        return
      }
      // 向左滑动事件导航跟踪处理
      if (newVal % 2 === 0 && newVal - oldVal === -1) {
        let interval = setInterval(() => {
          this.$refs['scroll'].scrollLeft -= this.$refs['scroll'].offsetWidth * 0.2 / 10
          setTimeout(() => {
            clearInterval(interval)
          }, 100)
        }, 10)
        return
      }
      // 点击事件处理
      if (Math.abs(newVal - oldVal) !== 1) {
        let interval = setInterval(() => {
          this.$refs['scroll'].scrollLeft += this.$refs['scroll'].offsetWidth * 0.2 * (newVal - oldVal) / 10
          setTimeout(() => {
            clearInterval(interval)
          }, 100)
        }, 10)
      }
    }
    // 使用interval模仿滚动动画
复制代码

swiper

以前提到个人项目使用的是vant组件库,可是vant中的swiper并无原生的swiper强大,这里我选择使用了原生swiper。一样对active监听,调用swiper的移动方法完成联动

mounted () {
    const that = this
    this.myswiper = new Swiper('.swiper-container',
      {
        // touchRatio: 0.8,
        watchSlidesProgress: true,
        observer: true,
        on: {
          slideChangeTransitionEnd: function () {
            that.changeActive(this.activeIndex)
            that.$router.push(that.contentArr[this.activeIndex].component)
            that.pushRoute(that.contentArr[this.activeIndex].component)
            that.shiftRoute()
          }
        }
      })
    const sWidth = this.$refs['s-con'].offsetWidth
    this.myswiper.setTranslate(-sWidth * this.active)
  }
 watch: {
    active: function (newVal) {
      const sWidth = this.$refs['s-con'].offsetWidth
      this.myswiper.setTranslate(-sWidth * newVal)
    }
  }
复制代码

在swiper滑动结束后,路由会进行跳转,在这里,有对路由变换的栈入,队列出的操做,是由于我对路由History的功能还不够知足,自我定义了一个栈来描述路由变换,咱们会在后面提到它的用法。
隐藏设置

这一段的逻辑相对复杂一些,直接点击个人栏目中的item会切换路由,按下编辑按钮,能够删除navbar中的item,长按item能够拖动换位,底部的更多栏目能够添加进navbar中,这里主要对拖动换位讲解一下:

  • 在item触发touchstart事件时设置一个延迟0.3秒的定时器,得到长按item的全部信息,包括位置,内容等。
  • 其实在这里我隐藏了一个item跟在最后,我称它为falseDom,touchmove时间触发时,这个falseDom,position:absolute,脱离文档流定位在,以前按下去的那个item上,而那个item设置不可视。
  • touchend时设置清除计时器(不足0.3s就触发其余事件),根据falseDom移动的位置,将一个拷贝了信息item插入到这个位置,并删除原位置item,事件结束,falseDom回归文档流排在最后并不可视。
dragStart (el) {
      this.timeout = setTimeout(() => {
        let index = el.target.dataset.index
        if (this.edited && index !== '0') {
          this.dragged = true
          this.moveText = this.list[index]
          this.falseDom.index = index
          this.falseDom.oldX = el.target.offsetLeft
          this.falseDom.oldY = el.target.offsetTop - 10
          this.falseDom.width = el.target.offsetWidth
          this.falseDom.height = el.target.offsetHeight
        }
      }, 300)
    },
    dragMove (el) {
      if (this.dragged) {
        this.dragIndex = el.target.dataset.index
        let draged = document.querySelector('.changed')
        if (draged) {
          this.trueDom = false
          this.list[this.dragIndex] = ''
        }
        let falseDom = document.querySelector('.falseDom')
        falseDom.style.left = el.changedTouches[0].pageX - el.target.offsetWidth / 2 + 'px'
        falseDom.style.top = el.changedTouches[0].pageY - el.target.offsetHeight / 2 + 'px'
        this.ready = true
      }
    },
    dragEnd (el) {
      clearTimeout(this.timeout)
      if (this.dragged) {
        this.dragged = false
        this.ready = false
        this.trueDom = true
        let falseDom = document.querySelector('.falseDom')
        let newX = parseFloat(falseDom.style.left)
        let newY = parseFloat(falseDom.style.top)
        let goX = parseInt((newX - this.falseDom.oldX) / (this.falseDom.width))
        let goY = parseInt((newY - this.falseDom.oldY) / this.falseDom.height)

        let newIndex = parseInt(this.dragIndex) + goY * 4 + goX
        if (newIndex <= 0) {
          this.list[this.dragIndex] = this.moveText
          return
        }
        console.log(goX, goY, newIndex)
        this.list = this.list.filter(text => text !== '')
        this.list.splice(newIndex, 0, this.moveText)
        return
        // console.log(this.list)
      }
      if (this.edited && el.target.dataset.index !== '0') {
        let index = el.target.dataset.index
        let deleteText = this.list[index].name
        let component = this.list[index].component
        this.list.splice(index, 1)
        this.moreList.push({name: deleteText, component})
        return
      }
      let index = el.target.dataset.index
      this.changeActive(index)
      this.$router.push(this.list[index].component)
      this.closeColumn()
    }
复制代码

这里的计算位置算法原理以下: 由于item每4个排一行,用新的left减去初始left除以item宽度就能算出,item在x轴位移的增量,同理算出y轴的增量,由于一行排4个,因此新的Index = oldIndex + goY * 4 + goX

头条栏目的一些细节

下拉刷新上拉加载

下拉刷新使用的是vant的PullRefresh组件,上拉加载使用的是vant的List组件(下拉加载的视频转gif怎么弄都太大了,最终没有上传)

在这里我在vuex中的home模块中定义了一个page状态用来存储分页状态,不过我在数据库里只存了两页,因此拿数据时一直交替page伪造无限滚动。我在项目中使用axios发送ajax请求,对于axios的使用,文档中讲解的十分详细,我就很少谈了。

在下拉刷新中,我使用并改造了vant的Notify组件。

<!--js-->
 Notify({
        message: '成功为您推荐5条新闻',
        className: 'notify',
        duration: 800
      })
<!--css-->
.notify
  top 2.4rem /* 90/37.5 */
  left 50%
  transform translateX(-50%)
  animation show .1s linear
  opacity 1
@keyframes show {
  0% {
    width 70%
    top 0
  }
  10%{
    width 100%
    top 2.4rem /* 90/37.5 */
  }
}
复制代码

硬件加速

细心的看官老爷们会发现个人css代码(这里使用的是stylus)中有一些不一样之处,我定义了一个opacity:1。由于这是一个暗示开启GPU加速的江湖黑话。

为何要使用硬件加速
咱们常常会发现一些css3的动画效果在移动端(甚至pc端)会有些卡顿,这是由于CSS的 animations, transforms以及transitions不会自动开启GPU加速,而是由浏览器的缓慢的软件渲染引擎来执行。为了让动画效果更佳流畅,咱们能够开启GPU加速来达到目的。

不过只有个别css属性能够启动硬件加速:

  • transform
  • opacity
  • filter

(使用transform开启硬件加速须要使用3d去小骗一下,如transform: translate3d(0, 0, 0),transform: translateZ(0), transform: rotateZ(360deg))

不过不要去迷恋硬件加速,过分使用它会给你的应用带来一些隐患

flexible

在这个项目中我使用了移动端适应插件flexible.js,它对移动端不一样屏幕分辨率的适应提供很好的帮助。它的做用原理是把屏幕宽度1/10定为1rem,我在谷歌浏览器预览效果时用的是iphone6/7/8因此在上面有个

top 2.4rem /* 90/37.5 */
复制代码

flexible的使用也很是简单只要在index.html中用cdn引入就好了...

图片加载

在图片没有加载完以前,使用一个灰色的div代替,图片Img,onload事件以后再显示出来。

<div src="" alt="" v-if="!includes(0) && !cache" class="img"></div>
<div src="" alt="" v-if="!includes(1) && !cache" class="img"></div>
<div src="" id='imgLast' alt="" v-if="!includes(2) && !cache" class="img"></div>
<img :src="item.img[0]" alt="" @load="load(0)" :data-src='item.img[0]' v-show="includes(0) || cache">
<img :src="item.img[1]" alt="" @load="load(1)" :data-src='item.img[1]' v-show="includes(1) || cache">
<img :src="item.img[2]" alt="" id="imgLast" @load="load(2)" :data-src='item.img[2]' v-show='includes(2) || cache'>
<!--js-->
load (num) {
      this.img.push(num)
    },
includes (num) {
  return this.img.includes(num)
}
复制代码

在template里面不能使用es6方法因此我在method中调用,思路就是,有一个图片就绪数组,当有图片onload时就把它的index传进数组,只有判断数组里是否有它,就能肯定遮挡层的去留。

作个缓存

这里的业务需求是看过的新闻,返回头条页面时要变暗表示已读。这里就须要作个缓存,而后告诉头条组件不要再发送请求了!

link (index) {
      this.linked = index
      let item = this.feedio[index]
      item.read = 1
      setTimeout(() => {
        this.$router.push({name: 'Arcticle', params: { item }})
        this.pushRoute('Arcticle')
        this.shiftRoute()
        // 只能传一个参数
        this.changeCache({
          arr: this.feedio,
          name: 'headlines'
        })
      }, 300)
复制代码

在这里咱们又看到了我以前提到的本身定义的路由栈了,这个路由栈只能保存两个路由,一个是上一次的路由,一个是当前路由。若是判断你的下一次路由和上一次路由名称相同,那就会使用缓存来代替get请求,这里传入了一个index表示阅读的新闻的位置,并把它的read属性标记为1,因此就能出现如上的已读效果。

这里我还对路由设置了0.3秒延迟,这是为了展现一个路由切换动画,若是直接用routerlink就不能看到那个像水同样像外弥漫的效果了,那个效果的原理以下:

.active
  animation link .3s ease forwards
@keyframes link {
  0% {
    width 60%
    border-radius 0
  }
  20% {
    width 100%
    border-radius 1.066667rem /* 40/37.5 */
  }
  100%{
    width 100%
    border-radius 0
  }
}
复制代码

这里的速度曲线用的是ease表示先加速后减速,就像一颗石子落入池塘,水纹扩散的速度也是先加速后减速的,至于那个曲线填满效果,是经过改变盒子的border-radius来实现的

文章页面

在这个项目中,点击item进入的文章页面是动态渲染出来的,咱们用postman测试一下接口,看下返回的数据(postman是一个测试接口工具)

能够看到有个返回的news中有个html属性,这个就是文章里面的html部分。至于为啥返回了html,又要从另外一个地方提及了。这个项目受众对象是观众,在这里观众是没有写文章的权利的,要写文章还须要一个做者端。做者端有一个markdown编辑器,做者使用md语法写好文章提交到服务器,在服务器中有md语法解析插件,会将md文本转化为html文本存在数据库里。

使用v-html就能把html直接渲染出来

<div id="article" v-html="article.html"></div>
复制代码

用个better-scroll

在写页面滚动时,我发现vue中的onscroll触发不了!找了许多资料,都不太理想,推荐方案都是用个addEventListener,可是我实在是不想破坏vue的统一性,因此用了bscroll插件,不过不得不说,bsroll也有不少坑,当时也被它整的我脑瓜子疼。

在这里有两个滚动需求,一个是当滚动超过做者后,做者信息上跳到头部,跟帖按钮变色拉长,在文章底部有个关闭限制,当上拉超过某个高度时能够关闭页面。

this.myscroll.on('scroll', pos => {
    this.scrollY = pos.y
    this.tipShow = true

    if (this.scrollY < -100) {
      this.show = true
    } else {
      this.show = false
    }
  })
 this.myscroll = new BScroll(this.$refs['bscrll'], {
    probeType: 3,
    pullUpLoad: true,
    click: true
  })
复制代码

头部那个变化的实现比较容易,bscroll的scroll事件能够不断监听当前滚动位置,不过为了实时监测,一点别忘了在初始化bscroll时定义probeType:3,至于bscroll的这些属性我就不细说了...

在实现底部的上拉关闭时,bscroll的事件就没有想象中的那么给力了。在这里我使用了他的pullingUp事件

this.myscroll.on('pullingUp', () => {
    this.maxY = this.scrollY
  })
复制代码

可是这个事件是否是设计的有点毒,它这个pullingUp只能触发一次,若是要再次触发,须要人为去调整他的finsh状态,而后,滚动一到底部它就会瞬间触发,说好的上拉呢,我还没拉它就触发了!真是气死噶人。

还有,能够滚动的高度居然不等于被包裹元素的offsetHeight,致使我须要在滚动到底部时,定义一个最大高度来获取它.而后上拉关闭原理以下:

if (this.maxY - this.scrollY >= 60 && this.maxY) {
    this.tipMsg = '释放关闭此页'
    this.tipIndex = 1
    this.close = true
} else {
    this.tipMsg = '上拉关闭此页'
    this.tipIndex = 0
    this.close = false
}
复制代码

这里bscroll的滚动高度是负值,因此我设置当上拉高度超过底部的60像素时,就能够实现关闭页面

moment工具

这里的19天前,使用了moment的fromNow方法。fromNow方法会返回一个与今日的时间差,它会经过时间间隔的大小来肯定使用什么时间单位。不过,它返回的是英文,好比''一天前''返回的是''a day ago'',须要咱们人为去修改它,我这里比较直接用的是if,else判断...

<!--translate.js-->
const translate = (time) => {
  if (time.includes('a ') || time.includes('an ')) {
    time = time.replace(/a |an /, '1')
  }
  if (time.includes('hour ') || time.includes('hours ')) {
    time = time.replace(/hour |hours /, '小时')
  }
  if (time.includes('day ') || time.includes('days ')) {
    time = time.replace(/day |days /, '天')
  }
  if (time.includes('month ') || time.includes('months ')) {
    time = time.replace(/month |months /, '月')
  }
  if (time.includes('years ') || time.includes('year ')) {
    time = time.replace(/years |year /, '年')
  }
  if (time.includes('ago')) {
    time = time.replace(/ago/, '前')
  }
  if (time.includes(' ')) {
    time = time.replace(/ /, '')
  }
  return time
}

module.exports = translate
<!--sunTime.js-->
const moment = require('moment')
const translate = require('./translate')
const sumTime = (time) => translate(moment(time, 'YYYY-MM-DD HH:mm:ss').fromNow())

module.exports = sumTime
复制代码

手撸一个图片预览

  • 事件委托

由于这些图片是v-html中渲染出来的,不是虚拟Dom,要取到图片的src须要使用到事件委托。

事件委托是利用事件冒泡实现一对多监听的方式,常见如ul,li,若是要给li绑定事件,则全部li都须要进行绑定,不利于代码的稳定性,而事件委托则是在ul中绑定事件,判断触发的target是否是须要绑定的元素,就能够完成一对多监听。

imageView (e) {
  let source = e.target
  if (source.nodeName === 'IMG') {
    if (!this.img.imgs) {
      let imgs = []
      let imgArr = this.$refs['article'].querySelectorAll(`img`)
      imgArr.forEach((img, index) => {
        img.setAttribute('index', index)
        imgs.push(img.src)
      })
      this.img.imgs = imgs
    }
    this.img.index = source.getAttribute('index')
    this.changeImg(this.img)
    this.viewImg()
  }
},
复制代码

咱们在App.vue中事先准备好了一个imgview的组件,而后它的渲染条件是判断vuex中的的一个imgView数组有没有值,先前我在事件委托时,将img的url赋值给了这个数组,因此它在事件发生后就会显示出来,至于里面的swiper效果,比较简单,这里就很少提了。

登陆/注册

登陆注册有意思的地方,就是我设定了当全部条件知足时,开始使用button才会从透明度0.6变成1,表示可用,其余的比较简单就不提了。

跟帖

只有登陆后才能跟帖,未登陆状态点击跟帖会自动弹出登陆界面。由于vue是单页应用,我结合LocalStorage,SessionStorage和vuex存储登陆状态。登陆完成后,在LocalStorage中存储userId,SessionStorage中存储md5加密后的密码,vuex中存一些头像,昵称等信息。

这里要提的点是,其实底部的写跟帖只是用来弹出textarea的一个disabled input,点击,textarea就会出现,在屏幕中容易touchmove,textarea就会隐藏,

这个输入框既能够发跟帖,也能对跟帖进行留言。

  • 直接点击写跟帖会调用的bscroll的scrollToElement方法,自动滚动到页面底部(这个方法接收一个$refs,从而滚动到该元素的位置)。
  • 点击回复时不会调用,而是在textarea中出现placeholder='回复: 回复人昵称'

这里在textarea中,给placeholder绑定了一个数据,只要判断是否有这个数据就能区分是发跟帖仍是回复跟帖。

关于点赞,在这里一些vue的初学者,可能会对一列用v-for动态渲染的元素,只改变其中某一个的样式,感到困扰,我这里提供我解决的方法。

<img :src="includes(index)?praisehover:praise" alt="" @click="praised(index)">
复制代码

这里的index是v-for的第二个参数,表明数组索引,只要在事件中把这个做为参数传入就能解决问题。

视频

nextTick

视频这里的页面结构以下:

<div class="video" :style="'background-image:url('+item.bgImg+')'" v-if='index!=active'>
  <div class="title">{{item.title}}</div>
  <div class="play-times">{{item.playTimes}}播放</div>
  <div class="img">
     <img src="../../assets/video/play.svg" alt="" class="icon" @touchend="play(index)">
  </div>
  <div class="time">{{item.time}}</div>
</div>
<div class="video" v-else>
  <video :src='item.video' ref='video' controls='controls' id='video'
  poster="../../assets/img/loading.gif"></video>
</div>
复制代码

视频和封面盒子用的是一个类名,直接用v-if,v-else判断,完成替换,但这里要提到的一个知识点就是nextTick。

定义: 在下次 DOM更新循环结束以后执行延迟回调。在修改数据以后当即使用这个方法,获取更新后的 DOM

nextTick涉及到vue的异步加载操做,会用在由于数据更新而变化的dom结构上,常见如v-if。由于数据驱动页面须要必定的时间,若是刚改变数据,就去操做与之新生成的dom,可能会获取不到,而没法完成操做。

只须要将操做放在nextTick的回调函数中,vue就会在dom完成渲染后执行...

play (index) {
  this.active = index
  this.$nextTick(() => {
    this.$refs.video[0].play()
  })
}
复制代码

这里,ref取的是一个数组,由于video在虚拟DOM中用v-for渲染,而我定义的ref都是video,因此,全部ref命名同样的DOM结构会存在一个数组中。可是实际上,用v-if控制,数组中只会出现被激活的video,因此是数组索引是0。

video标签的血与泪

细心的看官老爷,可能会发现,我使用的是touchend来播放视频,为啥没用click呢,为啥不是touchstart呢。由于click在移动端有些浏览器中,不能首次触发video.play(),点击事后,还要再点击控制条的播放才能播,这可真让人头大。

在查阅过不少资料中,获得的结果是,在触发事件后有延迟,video.play就可能执行不了,而click事件在移动端是有0.3秒延迟的。由于移动端会对,双击事件进行判断,控制页面缩放。

然而,我在meta里面禁用了页面缩放,click事件仍是不能播。终于在一篇资料中获得启发,video在移动端不支持自动播放,就算用js也不行,必定要引导用户在点击,触屏,或触屏滑动时再用js播放。

触屏?用click用习惯了,都忽略touch系列事件了。先改为touchstart,卡住了...

touch事件的执行顺序是touchstart->touchmove->touchend,touchstart触发太快,video还没到canplay阶段,用touchend就能够播放(不过个人视频源都比较小,我不知道换大的视频源会不会出现问题)。

写在最后

很是感谢看官老爷们能穿越这篇文章沙漠,来到最后。我在文章中用实战穿插知识点的分享方式,不知道有没有给各位在沙漠中见到绿光。最后源码奉上,在这个仓库中还有这个项目的Node后端,和正在开发的React做者端。若是有机会,我会在以后继续发表后端项目和React做者端...

相关文章
相关标签/搜索