使用 JavaScript 製作分頁效果 - 順便造個輪子做成套件

Fri Nov 12 2021

前端開發

# 這篇是我在 2021-11-04 受邀在六角學院直播分享的主題

目標是在不使用任何框架與外部套件的情況下,用純粹的 JavaScript 打造出分頁的效果,而為了完整性、復用性的考量下,我幾乎是做了一個與 UI 解耦的分業管理套件了。

雖然有些地方準備不足,不過我已經盡力把我認為最實用的內容分享出來,也希望能為你帶來一些啟發。

# 綱要

  • 設計思路
  • 知識點補充
  • DOM render 處理
  • Pagination 處理
  • history API
  • 延伸討論
  • 問題與討論

# 頁面 Demo

# 設計思路

為什麼要需要分頁?


要達成分頁效果的常見做法:

  • 一次性取得所有資料
  • 批次獲取各頁面資料

# 分頁獲取的前提

  • API 有回傳資料總筆數

# API 資料取得

# OffStreetParkingCityAPI : 各縣市[路外]停車場資料

GET /v1/Parking/OffStreet/CarPark/City/{City}

取得指定[縣市]之停車場基本資料

https://tdx.transportdata.tw/api-service/swagger


# 狀態管理與頁面渲染


# 知識點補充

  • Event Bubbling
  • Dataset
  • ES module
  • 解構賦值
  • 展開運算子
  • 閉包 Closure

# Event Bubbling

paginationElement.addEventListener('click', e => {
  const target = e.target.dataset
  const currentTarget= e.currentTarget.dataset
})

e.target


e.currentTarget


# Dataset

<a
  href="?page=1"
  onclick="return false"
  data-action="setPage"
  data-value="1"
>
  1
</a>
paginationElement.addEventListener('click', e => {
  const { action, value } = e.target.dataset
  action === 'setPage' // true
  value === '1' // true
})

# ES module

<script type="module" src="./simple_all.js"></script>
// simple_all.js
import { getParkingApi } from './module/service.js'
// module/service.js
export function getBikeApi() {}
export function getParkingApi() {}
export default { getBikeApi, getParkingApi }

# 解構賦值

function foo({ a = 1, b = 1, c }) {}
foo({
  a: 10,
  c: () => {},
})

剛剛 Dataset 講到的範例

paginationElement.addEventListener('click', e => {
  const { action, value } = e.target.dataset
  action === 'setPage' // true
  value === '1' // true
})

# 展開運算子

Math.max(1, 2, 3) === Math.max([1, 2, 3]) //false
Math.max(1, 2, 3) === Math.max(...[1, 2, 3]) //true
const arr1 = [1, 2, 3]
const arr2 = arr1
arr1 === arr2 // true

const arr1 = [1, 2, 3]
const arr2 = [...arr1]
arr1 === arr2 // false

# 閉包 Closure

function foo(data) {
    const getData = () => data
    const setData = n => data = n
    return {getData, setData}
}

const { getData, setData } = foo(10)

getData() //10
setData(50)
getData() //50

# DOM render 處理

  • 建立渲染函式
  • 建立共用組件

建立渲染函式

export function renderFunction(bindDom, renderContent, contentData = []) {
  if (contentData.length) bindDom.innerHTML = updateElement([...contentData])

  function updateElement(newData) {
    contentData = newData
    bindDom.innerHTML = newData.map(renderContent).join('')
  }

  return updateElement
}

產生頁面結構的函式

export function createCard({ Telephone, CarParkName, Description, Address }) {
  return /*html*/ `
      <article class="flex flex-col shadow m-4 max-w-xs">
          <div class="bg-white flex flex-col justify-start p-6">
              <p class="text-blue-700 text-sm font-bold uppercase pb-4">${Telephone}</p>
              <h2 class="text-3xl font-bold hover:text-gray-700 pb-4">${CarParkName?.Zh_tw}</h2>
              <p class="text-sm pb-3">${Address}</p>
              <p class="uppercase text-gray-800 hover:text-black">${Description}</p>
          </div>
      </article>
    `
}

# 宣告式與命令式的程式設計

明明是簡單的 innerHTML,為什麼要多此一舉?


# Pagination 處理

  • 建立 create 函式儲存 Pagination 需要的狀態
  • create 函式回傳操作 Pagination 需要的方法

export default function createPagination({ pagesLength = 1, currentPage = 1, onChange }) {
  const setPage = n => {...}

  const setPagesLength = (newPagesLength, newCurrentPage) => {}

  const getPages = () => {...}

  const getCurrentPage = () => currentPage

  const nextPage = () => setPage(currentPage + 1)

  const previousPage = () => setPage(currentPage - 1)

  const firstPage = () => setPage(1)

  const lastPage = () => setPage(pagesLength)

  return {
    setPage,
    setPagesLength,
    getPages,
    getCurrentPage,
    nextPage,
    previousPage,
    firstPage,
    lastPage,
  }
}


# history API

讓換頁功能不只是虛有其表


# history.pushState

history.pushState(state, title [, url])
history.pushState({ page: 10 }, '', '?page=' + 10)

# 監聽使用者「上一頁」、「下一頁」

window.addEventListener('popstate', ({ state }) => {
  const { page = 1 } = state || {}
  updateElements(pagination.setPage(page))
})

# 延伸討論


  • 資料暫存
  • 資料更新
  • 每頁筆數
  • 搜尋篩選

# Pagination-UI-LESS 套件


# npm install

npm i pagination-ui-less
or
yarn add pagination-ui-less

# 使用 CDN

<script src="https://cdn.jsdelivr.net/npm/pagination-ui-less"></script>
paginationUiLess({
    pagesLength: 1,
    currentPage: 1,
    onChange: () => {}
})