# 前台重構 - 使用 Composition API 封裝頁碼邏輯 ![I love writing report](https://www.memecreator.org/static/images/memes/4796468.jpg =400x) 終於有點時間可以弄 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`