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做者端...

相關文章
相關標籤/搜索