# 前台重構 - 使用 Composition API 封裝頁碼邏輯

終於有點時間可以弄 OKR ,和公司的資深前端討論了一下,決定優先把前台報表頁的程式碼統一成一致的 style。
### 重構過程和思路
1. 因為每個同事寫報表都有自己的 coding style,但其實做的邏輯大多雷同,所以我同事寫了一個很厲害的 **vue-condition-watcher** 打算統一報表頁寫法,這樣之後出大事只維護核心的抽象邏輯即可。
2. 公司產品的後端是用 **django** 寫的,而分頁報表 API 用的是 **LimitOffsetPagination** ,它有兩個特別的 query - **limit** 跟 **offset**,這兩個 query 的功用如下:
- **limit** - 撈多少資料
- **offset** - 後端從 table 的哪個 index 撈取資料
3. 因為 vue-conditional-watcher 只針對 conditions 監聽,很難介入和辨別使用者的操作是以下何者:
- 切換頁碼 (只觸發改變 offset)
- 改變 filter 欄位 (觸發 filter 欄位改變且 offset 要歸零)
```javascript
const conditions = reactive({
// 頁碼控制欄位
limit: 20,
offset: 0,
// filter 欄位
createdAt: ['2020-03-30', '2020-03-31'],
transaction_type: '',
})
```
4. 決定寫一個代理去幫忙 vue-condition-watcher 判斷上面這件事,**核心的思想是把 vue-condition-watcher 的 conditions 解構失去 reactive 特性再重新用 ref 跟 reactive 包裝**,這樣決定何時更新 conditions 的主導權就會重回我們手上。
```javascript
// @/hooks/usePagination.js
export default function (
config,
queryOptions = {
sync: 'router',
/*
* 我們不能讓使用者可以透過 route query 更改 limit
* 因為可能會有瘋子一次打個超大數字直接讓後端爆炸
*/
ignore: ['limit'],
},
limit = 20
) {
const { proxy } = getCurrentInstance()
config.conditions.offset = +proxy.$route.query.offset || 0
config.defaultParams = config.defaultParams || {}
config.defaultParams.limit = limit
queryOptions.sync = queryOptions.sync || 'router'
if (!queryOptions.ignore.includes('limit')) queryOptions.ignore.push('limit')
const {
conditions: watcherConditions,
data,
loading,
refresh,
} = useConditionWatcher(config, { sync, ignore })
/*
* 核心 I:
* 解構並重新以 ref 和 reactive 建立活性
* 之後使用者 mutate 到的數據都會是這些代理變數
*/
const {
offset: parseOffset,
limit: parseLimit,
...parseConditions
} = watcherConditions
const offset = ref(parseOffset)
const conditions = reactive(parseConditions)
/*
* 頁碼相對於 offset 和 limit 的計算關係
* 拿來綁定給 element-ui 的 el-pagination
*/
const currentPage = computed({
get: () => offset.value / limit + 1,
set: (page) => {
offset.value = (page - 1) * limit
},
})
/*
* 核心 II:
* 由這些代理變數去改變實際的 watcherConditions 觸發打請求
*/
watch(offset, (newOffset) => {
watcherConditions.offset = newOffset
})
watch(
conditions,
(newConditions) => {
Object.keys(newConditions).forEach((key) => {
watcherConditions[key] = newConditions[key]
})
watcherConditions.offset = 0
offset.value = 0
},
{ deep: true }
)
return {
data,
loading,
refresh,
limit,
offset,
conditions,
currentPage,
}
}
```
### 使用方式
```htmlembedded
<template>
<div>
<el-form :inline="true" :model="conditions">
<el-form-item label="選擇日期">
<el-date-picker
v-model="conditions.createdAt"
size="mini"
type="daterange"
start-placeholder="開始日期"
end-placeholder="結束日期"
value-format="yyyy-MM-dd"
:disabled="loading"
>
</el-date-picker>
</el-form-item>
...
<el-pagination
v-if="totalCount > limit"
:current-page.sync="currentPage"
:page-size="limit"
layout="total, prev, pager, next"
:total="totalCount"
>
</el-pagination>
</div>
<template>
```
```javascript
import usePagination from '@/hooks/usePagination'
export default {
setup () {
const { data, loading, refresh, limit, currentPage, conditions } = usePagination({
fetcher: () => axios.get('xxx'),
conditions: {
createdAt: ['2021-03-29', '2021-03-30']
},
beforeFetch (conditions) {
conditions.created_at_after = conditions.createdAt[0]
conditions.created_at_before = conditions.createdAt[1]
delete conditions.createdAt
return conditions
},
afterFetch (data) {
totalCount.value = data.counts
}
})
return {
totalCount,
data,
loading,
refresh,
limit,
currentPage,
conditions
}
}
}
</script>
```
### 可以繼續拓展的方向
- 報表可能會更齊全,limit 可能有朝一日也會變成一個可控制變因。
- 可以對 afterFetch 用裝飾者模式去封裝有固定 format 的 response data (比如上面例子的 totalCount )
呃,然後我真的有點不確定這個 hook 要叫什麼名字好。<br/>
試著取名成`useAPIFetcher`、`useLimitOffset`、`useReport`過,但都覺得怪怪的。
### 參考
[Django - Pagination](https://www.django-rest-framework.org/api-guide/pagination/#limitoffsetpagination)<br/>
[Wiki - Proxy Pattern](https://en.wikipedia.org/wiki/Proxy_pattern)
###### tags: `Refactor` `JavaScript` `Vue` `element-ui`