Django + React 電商專案練習 [14] - 分頁功能

Posted by Young on 2022-12-04
Estimated Reading Time 6 Minutes
Words 1.4k In Total

後端

  • 由於我們並不是使用 Django template 來渲染前端頁面,所以並不能單純的用 Django 的 Paginator 來分頁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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

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

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

1
2
3
4
5
6
7
8
9
10
11

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 頁開始

1
2
3
4
5
6
7
8
9
// 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 了

會報錯的寫法:

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

所有要分 pathnamesearch 分別賦值

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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});

1
2
3
4
// ProductListPage.js
...
</Table>
<Paginate pages={pages} page={page} isAdmin={true} />

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

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

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

error_2

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

1
2
3
4
5
6
7
8
9
10
11
12
// 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]);

若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~


留言版