# 後端

  • 由於我們並不是使用 Django template 來渲染前端頁面,所以並不能單純的用 Django 的 Paginator 來分頁
@api_view(['GET'])
def getProducts(request):
    query = request.query_params.get('keyword')
    # print("query:", query)
    if query is None:
        query = ''
        products = Product.objects.all()
        page = request.query_params.get('page')
        paginator = Paginator(products, 2)  # 每頁顯示 2 個產品
        try:
            products = paginator.page(page)
        except PageNotAnInteger:  # 如果 page 不是整數,則預設為 1
            products = paginator.page(1)
        except EmptyPage:
            products = paginator.page(paginator.num_pages)
        if page is None:
            page = 1
        page = int(page)
        print("page:", page)
    else:
        products = Product.objects.filter(name__icontains=query)

    serializer = ProductSerializer(products, many=True)
    # return Response(serializer.data)
    return Response({'products': serializer.data, 'page': page, 'pages': paginator.num_pages})

目前的 Response 直接測試應會出現 products.map is not a function 的錯誤,因為目前後端返回的 API 內容已變成:

productmap_error

{
    "products": [...],  // 產品列表
    "page": 1,          // 當前頁碼
    "pages": 5          // 總頁數
}

不再是單純的products,所以才需要更新 productListReducer


export const productListReducer = (state = { products: [] }, action) => {
 // 列出所有產品
 switch (action.type) {
  case PRODUCT_LIST_REQUEST: // 請求中
   return { loading: true, products: [] };
  case PRODUCT_LIST_SUCCESS: // 成功
   return { loading: false, products: action.payload }; // 請求到資料了=loading結束
  ...
 }
};

為了測試分頁功能,需要暫時更改 productListReducer 的內容讓期能保存後端返回的完整資料結構,如 products、page 和 pages

export const productListReducer = (state = { products: [] }, action) => {
 // 列出所有產品
 switch (action.type) {
  case PRODUCT_LIST_REQUEST: // 請求中
   return { loading: true, products: [] };
  case PRODUCT_LIST_SUCCESS: // 成功
   return { loading: false, products: action.payload.products, page: action.payload.page, pages: action.payload.pages }; // 請求到資料,loading 結束
  case PRODUCT_LIST_FAIL: // 錯誤
   return { loading: false, error: action.payload };
  default:
   return state;
 }
};

目前這樣進度直接訪問網站的話應只會出現 2 個產品,因為後端設定每頁只顯示 2 個產品:

only_two_products

而目前只能手動在網址中輸入 ?page=2 來查看其他頁面的產品,所以接下來要在前端實現分頁功能

:?page=

# 前端

# 搜尋欄元件

這邊只需稍更改 navigate 的路由參數,確保每次搜尋產品時,自動從第 1 頁開始

// SearchBar.js
const submitHandler = (e) => {
 e.preventDefault();
 if (keyword.trim()) {
  navigate(`/?keyword=${keyword}&page=1`); // 這邊 ?keyword 不用加任何其他東西,否則 HomePage 的 useLocation 印不出 keyword
 } else {
  navigate(window.location.pathname); // 若沒有輸入關鍵字,則停留在當前頁面
 }
};

# 頁數按鈕元件

components 資料夾創建新元件 Paginate.js,此元件共會傳進四個參數:

  • pages:總頁數
  • page:當前頁碼
  • keyword:搜尋關鍵字,預設空字串,不一定每次都會帶有搜尋關鍵字
  • isAdmin:是否為管理員

現在 react-router-bootstrap 不允許將查詢參數(?keyword=...&page=...)直接作為 pathname 了

會報錯的寫法:

<LinkContainer
 to={`/?keyword=${keyword}&page=${x + 1}`,}
>

所有要分 pathnamesearch 分別賦值

另外,這邊有一個大重點就是當使用 <LinkContainer> 來包裹 <Pagination.Item> 時,<Pagination.Item>active 會衝突,導致每個頁碼都會標記為 active 狀態 ,但是用 href 又會有刷新頁面的問題。

所以這邊使用的方法是製作一個 handleClick 函數,當點擊頁碼時,透過 useNavigate 來改變路由,就能達到不重整頁面的情況下改變路由

// Paginate.js
import React from "react";
import { Pagination } from "react-bootstrap";
import { useNavigate } from "react-router-dom";

const Paginate = ({ pages, page, keyword = "" }) => {
 const navigate = useNavigate();

 if (keyword) {
  keyword = keyword.split("?keyword=")[1]?.split("&")[0] || "";
 }

 page = Number(page) || 1;

 const handleClick = (x) => {
  navigate(`/?keyword=${keyword}&page=${x}`);
 };

 return (
  <Pagination>
   {[...Array(pages).keys()].map((x) => (
    <Pagination.Item key={x + 1} active={x + 1 === page} onClick={() => handleClick(x + 1)}>
     {x + 1}
    </Pagination.Item>
   ))}
  </Pagination>
 );
};

export default Paginate;
  • 用展開運算子(Spread operator) [...Array()Iterator 轉換為普通的陣列,因為.keys() 會返回 Iterator,這樣才能進行 map() 運算。

📌 總結
方法 結果 備註
Array(pages).keys() Array Iterator {} Iterator,不是陣列
[...Array(pages).keys()] [0, 1, 2, 3, 4] 用 ... 展開成陣列
Array.from(Array(pages).keys()) [0, 1, 2, 3, 4] 直接轉換成陣列
👉 結論:因為 .keys() 回傳的是 Iterator,不能直接當作陣列使用,因此需要 ... 或 Array.from() 來展開它!

# 產品管理列表(Admin 權限專用)

Paginate 元件直接加入到後,並將 isAdmin 設為 true,此時若直接點擊頁碼會發現跳回首頁了,這是因為 Paginate 元件的 handleClick 函數中目前是只導向 navigate(/?keyword={keyword}&page={x});

// ProductListPage.js
...
</Table>
<Paginate pages={pages} page={page} isAdmin={true} />

所以回來修改判斷式,當 isAdmintrue 時,導向 /admin/productlist,否則就維持原本路由

// Paginate.js
...
const handleClick = (x) => {
 navigate(`${isAdmin ? "/admin/productlist" : "/"}/?keyword=${keyword}&page=${x}`);
};

修好之後再回去按頁碼,會發現儘管路由有正常改變,但換頁時頁面卻沒有重新渲染、沒有動靜

error_2

這是因為在目前 ProductListPage 中,沒有給 dispatch(listProducts()); 傳入 keyword,修改程式多監聽 keyword 的變化

// ProductListPage.js
let keyword = useLocation().search;
...
useEffect(() => {
 dispatch({ type: PRODUCT_CREATE_RESET }); // 進入頁面先重置新增產品狀態
 if (successCreate) {
  // 若成功新增產品,導向到該產品編輯頁面
  navigate(`/admin/product/${createdProduct.id}/edit`);
 } else {
  dispatch(listProducts(keyword));
 }
}, [dispatch, navigate, successDelete, successCreate, createdProduct, keyword]);