Vue.js 開發實踐:實現精巧的無限加載與分頁功能

本篇文章是一篇Vue.js的教程,目標在於用一種常見的業務場景——分頁/無限加載,幫助讀者更好的理解Vue.js中的一些設計思想。與許多Todo List類的入門教程相比,更全面的展現使用Vue.js完成一個需求的思考過程;與一些構建大型應用的高階教程相比,又更專一於一些零碎細節的實現,方便讀者快速掌握、致用。css

需求分析

當一個頁面中信息量過大時(例如一個新聞列表中有200條新聞須要展現),就會產生問題,例如:html

  • 數據量過大,影響加載速度前端

  • 用戶體驗差,很難定位到以前本身看過的某篇文章vue

  • 擴展性差,若是200條變爲2000條或者更多node

因此常見的解決思路就是至底時加載數據或者分頁展現。無限加載的實現過程相似於:webpack

  1. ajax類方法獲取數據git

  2. 數據存入本地數組es6

  3. 數組中的每條數據對應插入一個HTML模板片斷中github

  4. 將HTML片斷append到節點中web

前端分頁的實現過程相似於:

  1. ajax類方法獲取數據

  2. 數據替換本地數組

  3. 數組中的每條數據對應插入一個HTML模板片斷中

  4. 清空節點後將HTML片斷append到節點中

每每修改或者維護代碼時,咱們會發現渲染HTML和插入部分是比較煩人的。由於咱們須要將HTML拼接成字符串,在對應的位置插入數據,每每就是一段很是長的字符串,以後想要加個class都費勁。es6的模板字符串讓這個狀況有所好轉,可是依然有瑕疵(例如實際編寫時沒法HTML代碼高亮)。

同時咱們還須要寫很多for或者forEach去循環數組,再命令式的append,若是這段代碼片斷有一些複雜的交互,可能還須要經過事件代理綁定一堆方法。

若是在完成這類業務時,你也遇到過上述的問題,那麼你就會發現Vue真是太coooooool了,let's vue!

新建一個Vue.js項目

強烈推薦使用vue-cli來新建一個項目。

一開始你可能會認爲用node.js和npm安裝一大堆庫,生成了一些你不太瞭解的目錄和配置文件,一寫代碼還會跳出一堆eslint的提示。可是這絕對物有所值,由於這樣的一個模板能夠幫你更好的理解Vue.js組織文件的思路,而且當你適應以後,你會發現這些條條框框極大地加快了你的開發效率。

在此次的教程中,咱們新建了一個名叫loadmore的項目,具體的新建項目流程能夠參照官網教程的安裝一節。

佈局頁面結構

爲了配合教程的逐步深刻,我先從完成加載更多功能入手。爲了和以後的分頁保持一致,個人頁面準備由兩部分組成,一是信息列表,二是底部的一個加載更多的按鈕,我將他們都放在App.vue這個根組件中。

<template>
  <div id="app">
    <list></list>
    <a class="button" @click="next" >GO NEXT</a>
  </div>
</template>

<script>
import List from './components/List'

export default {
  components: {
    List
  },
  data () {
    return {
      ...
    }
  },
  methods: {
    next () {
      ...
    }
  }
}
</script>

<style scoped>
  .button {
    display: block;
    width: 100%;
    background: #212121;
    color: #fff;
    font-weight: bold;
    text-align: center;
    padding: 1em;
    cursor: pointer;
    text-decoration: none;
  }
  .button span {
    margin-left: 2em;
    font-size: .5rem;
    color: #d6d6d6;
  }
</style>

在這個過程當中,咱們根據Vue的設計思想有了以下思路:

  1. 在信息列表中,咱們會完成咱們上文中提到的幾個步驟,而這些步驟都只和信息列表自己有關,與Next按鈕間惟一的聯繫就是Next點擊後須要觸發信息列表去獲取,而這能夠經過props傳遞。因此咱們把列表及其自身業務邏輯、樣式都放在List.vue這個組件中。

  2. 咱們爲按鈕定義了一些基本的樣式,可是咱們用的css選擇器就是一個.button類名,可能會和別的組件中的.button樣式衝突,因此咱們加入了一個scoped屬性,讓App.vue中的style樣式只做用於這個組件內部。
    注意:scoped並不會影響css的做用優先級,使用scoped不表明不會被外部樣式表覆蓋。

  3. 咱們想引入一些基礎樣式,好比reset.css。若是在項目中使用了sass之類的語言,那麼能夠將對應的外部sass文件放在assets文件夾中,經過import引入。普通的css能夠直接寫在一個不加scoped屬性的組件中,可是若是你肯定這個樣式表不會被頻繁改動,那麼也能夠做爲第三方靜態資源引入index.html中。例如這個例子中,我在index.html中加入了:

<link rel="stylesheet" href="./static/reset.css">

效果:

clipboard.png

完成List.vue

目前咱們主要的業務邏輯都是圍繞信息列表展開的,也就是咱們建立的List.vue。

首先,咱們須要獲取目標數據,我選用了cnodejs.org社區的API做爲例子進行編寫。若是你也想用一個封裝好的ajax庫的話,應該這麼作:

引入第三方JS庫

將目標JS庫文件放在static文件夾中,例如我選擇的是reqwest.js,而後在index.html先引入。

<script src="./static/reqwest.min.js"></script>

而後在build配置文件夾中,修改webpack.base.conf.js,export externals屬性:

externals: {
  'reqwest': 'reqwest'
}

這樣咱們在咱們的項目中,就能夠隨時加載第三方庫了。

import reqwest from 'reqwest'

寫個API接口

在這個例子中,咱們只須要調用文章列表這一個接口,可是實際項目中,可能你須要調用不少接口,而這些接口又會在多個組件中被用到。那麼調用接口的邏輯四散在各個組件中確定是很差的,想象一下對方的url發生了變化,你就得在無數個組件中一個個檢查是否要修改。

因此我在src文件夾中新建了一個api文件夾,用於存放各種API接口。當前例子中,要獲取的是新聞列表,因此新建一個news.js文件:

import reqwest from 'reqwest'

const domain = 'https://cnodejs.org/api/v1/topics'

export default {
  getList (data, callback) {
    reqwest({
      url: domain,
      data: data
    })
    .then(val => callback(null, val))
    .catch(e => callback(e))
  }
}

這樣咱們就擁有了一個獲取新聞列表的API:getList。

編寫組件

咱們用一個<ol>做爲新聞列表,內部的每個<li>就是一條新聞,其中包括標題、時間和做者3個信息。

在data中,咱們用一個名爲list的數組來儲存新聞列表的數據,一開始固然是空的。咱們再在data中設置一個名爲limit的值,用來控制每頁加載多少條數據,做爲參數傳給getList這個API。

所以咱們的template部分是這樣的(加入了一些style美化樣式):

<template>
  <ol>
    <li v-for="news of list">
      <p class="title">{{ news.title }}</p>
      <p class="date">{{ news.create_at }}</p>
      <p class="author">By: {{ news.author.loginname }}</p>
    </li>
  </ol>
</template>

<style scoped>
  ol {
    margin-left: 2rem;
    list-style: outside decimal;
  }
  li {
    line-height: 1.5;
    padding: 1rem;
    border-bottom: 1px solid #b6b6b6;
  }
  .title {
    font-weight: bold;
    font-size: 1.3rem;
  }
  .date {
    font-size: .8rem;
    color: #d6d6d6;
  }
</style>

以後咱們顯然須要使用getList來獲取數據,不過先想一想咱們會在哪幾個地方使用呢?首先,咱們須要在組件開始渲染時自動獲取一次列表,填充基礎內容。其次,咱們在每次點擊APP.vue中的Next按鈕時也須要獲取新的列表。

因此咱們在methods中定義一個get方法,成功獲取到數據後,就把獲取的數組拼接到當前list數組後,從而實現了加載更多。

沿着這個思路,再想一想get方法須要的參數,一個是包含了page和limit兩個屬性的對象,另外一個是回調函數。回調函數咱們已經說過,只須要拼接數組便可,所以只剩下最後一個page參數還沒設置。

在初始化的時候,page的值應該爲1,默認是第一頁內容。以後page的值只由Next按鈕改變,因此咱們讓page經過props獲取App.vue中傳來的page值。

最後則是補充get方法觸發的條件。一是在組件的生命週期函數created中調用this.get()獲取初始內容,另外一是在page值變化時對應獲取,因此咱們watch了page屬性,當其變化時,調用this.get()。

最後List.vue的script長這樣:

<script>
import news from '../api/news'

export default {
  data () {
    return {
      list: [],
      limit: 10
    }
  },
  props: {
    page: {
      type: Number,
      default: 1
    }
  },
  created () {
    this.get()
  },
  watch: {
    page (val) {
      this.get()
    }
  },
  methods: {
    get () {
      news.getList({
        page: this.page,
        limit: this.limit
      }, (err, list) => {
        if (err) {
          console.log(err)
        } else {
          list.data.forEach((data) => {
            const d = new Date(data.create_at)
            data.create_at = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
          })
          this.list = this.list.concat(list.data)
        }
      })
    }
  }
}
</script>

同時咱們將App.vue中的<list>修改成:

<list :page="page"></list>

再爲page在App.vue中添加一個初始值以及對應的方法next:

data () {
  return {
    page: 1
  }
},
methods: {
  next () {
    this.page++
  }
}

這樣咱們就已經完成了加載更多的功能。

圖片描述

改寫爲分頁

由於以前咱們的思路很是清晰,代碼結構也很明瞭,因此改寫起來會很是簡單,只須要將List.vue中拼接數組改成賦值數組就能夠了:

// 常規loadmore
// this.list = this.list.concat(list.data)
// 分頁
this.list = list.data

就這麼簡單的一行就完成了功能的改變,這就是Vue.js中核心的數據驅動視圖的威力。固然,接下來咱們還要作點更cooooool的。

添加功能

由於分頁替換了原來的數組,因此僅僅一個Next按鈕不夠用了,咱們還須要一個Previous按鈕返回上一頁。一樣的,也給Previous按鈕綁定一個previous方法,除了用this.page--改變page的值之外,還須要對this.page === 1的邊界條件進行一個判斷。

同時爲了方便知道咱們當前的頁數,在按鈕中,加入{{ page }}顯示頁數。

<a class="button" @click="next" >GO NEXT<span>CURRENT:{{page}}</span></a>

transition動畫

編寫和完善功能的過程當中,已經充分體現了Vue.js清晰和便利的一面,接下來繼續看看其它好用的功能,首先就是transition動畫。

爲了展現transition的威力,首先我找到了一個模仿的對象:lavalamp.js(Demo地址)。

在Demo中能夠看到頁面以一種很是優雅的動畫過渡完成了切換內容的過程,其自己是用JQuery+CSS動畫完成的,我準備用Vue.js進行改寫。

首先學習了一下原做者的實現思路之後,發現是將一個div做爲loader,position設定爲fixed。當翻頁時,根據點擊的按鈕不一樣,loader從頂部或者底部擴展高度,達到100%。數據加載完畢後,再摺疊高度,最終隱藏。

那麼初步的思路以下:

  1. 添加一個loader,最小高度與按鈕一致,背景同爲黑色,讓過渡顯得更天然。

  2. loader高度須要達到一個屏幕的高度,因此設置html和body的height爲100%。

  3. 須要有一個值,做爲loader是否顯示的依據,我定爲finish,其默認值值爲true,經過給loader添加v-show="!finish"來控制其顯示。

  4. 在next和previous方法中添加this.finish = false觸發loader的顯示。

  5. 在App.vue和List.vue創建一個雙向的props屬性綁定至finish,當List.vue中的get方法執行完畢後,經過props將App.vue中的finish設定爲true,隱藏loader。

  6. 給loader添加一個transition。因爲動畫分爲頂部展開和底部展開兩種,因此使用動態的transition爲其指定正確的transition名稱。

  7. 新增一個值up,用於判斷動畫從哪一個方向開始,其默認值爲false。在previous方法中,執行this.up = true,反之在next方法中,則執行this.up = false。

根據思路,寫出的loader應該是這樣的(style等樣式設定在最後統一展現):

<div id="loader" v-show="!finish" :transition="up? 'up-start':'down-start'">
  <span>Loading</span>
</div>

能夠看到我設定了up-start和down-start兩種transition方式,對應的css動畫代碼以下:

.down-start-transition {
    bottom: 0;
    height: 100%;
  }
  .down-start-enter {
    animation: expand .5s 1 cubic-bezier(0, 1, 0, 1) both;
  }
  .down-start-leave {
    animation: collapse .5s 1 cubic-bezier(0, 1, 0, 1) both;
    top: 0;
    bottom: auto;
  }
  .up-start-transition {
    top: 0;
    height: 100%;
  }
  .up-start-enter {
    animation: expand .5s 1 cubic-bezier(0, 1, 0, 1) both;
  }
  .up-start-leave {
    animation: collapse .5s 1 cubic-bezier(0, 1, 0, 1) both;
    top: auto;
    bottom: 0;
  }
  @keyframes expand {
    0% {
      height: 3em;
      transform: translate3d(0, 0, 0);
    }
    100% {
      height: 100%;
      transform: translate3d(0, 0, 0);
    }
  }
  @keyframes collapse {
    0% {
      height: 100%;
      transform: translate3d(0, 0, 0);
    }
    100% {
      height: 3em;
      transform: translate3d(0, 0, 0);
    }
  }

設置了expand和collapse兩個animation,再在transition的各個生命週期鉤子中作對應的綁定,就達到了和lavalamp.js相接近的效果。

爲了保證動畫能執行完整,在List.vue的get方法執行完以後,還使用了一個setTimeout定時器讓finish延時0.5秒變爲true。

優化體驗

動畫效果完成以後,實際使用時發現lavalamp.js還有個巧妙地設計,就是點擊Previous後,頁面前往底部,反之點擊Next後則前往頂部。

實現後者並不複雜,在next方法中加入如下一行代碼調整位置便可:

document.body.scrollTop = 0

previous前往底部則略微複雜一點,由於獲取到數據以後,頁面高度會發生改變,若是在previous中執行scrollTop的改變,有可能會出現新的內容填充後高度變長,頁面不到底的狀況。

因此我watch了finish的值,僅當點擊按鈕爲previous且finish變化爲false至true時前往底部,代碼以下:

watch: {
  finish (val, oldVal) {
    if (!oldVal && val && this.up) {
      document.body.scrollTop = document.body.scrollHeight
    }
  }
}

前端路由

完成以上內容以後,發現不論翻到第幾頁,一旦刷新,就會回到第一頁。vue-router就是爲解決這類問題而生的。

首先咱們引入VueRouter,方式能夠參考上文中的「引入第三方JS庫」。而後在main.js對路由規則進行一些配置。

咱們的思路包括:

  1. 咱們須要在url上反映出當前所處的頁數。

  2. url中的頁數應該與全部組件中的page值保持一致。

  3. 點擊Next和Previous按鈕要跳轉到對應的url去。

  4. 在這個例子中咱們沒有router-view。

所以main.js的配置以下:

import Vue from 'vue'
import App from './App'
import VueRouter from 'VueRouter'

Vue.use(VueRouter)

const router = new VueRouter()
router.map({
  '/page/:pageNum': {
    name: 'page',
    component: {}
  }
})

router.redirect({
  '/': '/page/1'
})

router.beforeEach((transition) => {
  if (transition.to.path !== '/page/0') {
    transition.next()
  } else {
    transition.abort()
  }
})

router.start(App, 'app')

首先定義了一個名爲page的具名路徑。以後將全部目標路徑爲'/',也就是初始頁的請求,重定向到'/page/1'上保證一致性。最後再在每次路由執行以前作一個判斷,若是到了'/page/0'這樣的非法路徑上,就不執行transition.next()。

根據以前的思路,在App.vue中,獲取路由對象的參數值,賦值給page。同時給兩個按鈕添加對應的v-link。

最終的demo地址
Github倉庫

相關文章
相關標籤/搜索