To-Do Calendar - Day18 實作年曆日期圈選功能
這篇要來實作原子習慣追蹤的年曆日期圈選功能,包含打圈和取消打圈。
原子習慣追蹤圈選日期功能
- 開發流程:
- 根據前端傳來的參數 op 判斷要執行新增或移除日期,並將結果並返回前端
- 最後實作前端判斷圈選操作後呼叫對應的 API,然後顯示更新後返回的結果
設計 RESTful API
實作年曆日期圈選功能 原子習慣追蹤(圈選或取消年曆上的日期)
addPickedDay API
關於要使用哪個 HTTP 方法糾結了許久,考慮用 POST 是因為這功能有「新增」日期,考慮用 PUT 是因為這功能實際是「修改」原子習慣追蹤,考慮用 PATCH 是因為這功能實際是「部分修改」原子習慣追蹤,最後還是決定以對應到資料庫操作去設計,因為從 URI 來看,原子習慣追蹤才是主體資源,而這個新增的日期只是原子習慣追蹤的 pickedDays 陣列中的一個元素,為了更新一個屬性上傳整個 habitTracker Document 或上傳整個新的 pickedDays 陣列都不太好(資料傳輸量浪費 server 頻寬),所以最後採用的是 PATCH,只上傳要新增的日期,而 PATCH 請求的格式參考的是 JSON Patch。
- Request URL
PATCH http://localhost:8080/users/{userId}/habitTrackers?habitId=<habitId>&year=<year>
- Request Body
{ "op":"add", "path":"/pickedDays", "value":{"id": "2022-06-06", "date": "2022-06-05T16:00:00.000Z"} }
- Response Body
{ "id": "6294f0123d1b551c205e3b71", "habitId": "habit01", "year": "2022", "pickedDays":[ { "id": "2022-01-06", "date": "2022-01-05T16:00:00.000Z" }, { "id": "2022-01-07", "date": "2022-01-06T16:00:00.000Z" }, { "id": "2022-01-08", "date": "2022-01-07T16:00:00.000Z" }, { "id": "2022-01-09", "date": "2022-01-08T16:00:00.000Z" }, { "id": "2022-01-10", "date": "2022-01-09T16:00:00.000Z" }, { "id": "2022-06-04", "date": "2022-06-03T16:00:00.000Z" }, { "id": "2022-06-06", "date": "2022-06-05T16:00:00.000Z" } ], "createdTime": 1653405213436, "lastModifiedTime": 1653405213436 }
removePickedDay API
和 addPickedDay API 的 Request Body 差在 op 參數的值。
- Request URL
PATCH http://localhost:8080/users/{userId}/habitTrackers?habitId=<habitId>&year=<year>
- Request Body
{ "op":"remove", "path":"/pickedDays", "value":{"id": "2022-06-06", "date": "2022-06-05T16:00:00.000Z"} }
- Response Body
{ "id": "6294f0123d1b551c205e3b71", "habitId": "habit01", "year": "2022", "pickedDays":[ { "id": "2022-01-06", "date": "2022-01-05T16:00:00.000Z" }, { "id": "2022-01-07", "date": "2022-01-06T16:00:00.000Z" }, { "id": "2022-01-08", "date": "2022-01-07T16:00:00.000Z" }, { "id": "2022-01-09", "date": "2022-01-08T16:00:00.000Z" }, { "id": "2022-01-10", "date": "2022-01-09T16:00:00.000Z" }, { "id": "2022-06-04", "date": "2022-06-03T16:00:00.000Z" } ], "createdTime": 1653405213436, "lastModifiedTime": 1653405213436 }
實作 Controller 層
這次從 Controller 層開始實作
- 在 HabitTrackerController class 新增 patchHabitTracker 方法,返回類型為
ResponseEntity<HabitTracker>
- 在方法上加上
@PatchMapping
註解,表示前端要使用 PATCH 方法來請求 API - 在
@PatchMapping
註解括號中指定 url 路徑,並使用@RequestParam
註解去取得 url 路徑的參數 - 使用
@RequestBody
去接住前端傳來的參數,並使用@Valid
註解讓寫在 PatchRequest class 的驗證請求參數的註解生效Info如果 Spring Boot 版本是 2.3 之後的版本,要使用驗證請求參數的註解的話,是需要額外在 pom.xml 增加一組 dependency validation 設定。 - 接著實作 patchHabitTracker 方法
- 將前端傳過來的參數的值一一 set 到 habitTrackerQueryParams 的屬性
- call habitTrackerService 的 patchHabitTracker 方法
pom.xml PatchRequest class HabitTrackerController class
實作 Service 層
- 在 HabitTrackerService interface 宣告 patchHabitTracker 方法
- 接著再到 HabitTrackerServiceImpl class,實作 patchHabitTracker 方法
- 依 patchRequest 的 op 參數的值決定要 call habitTrackerDao 的哪個方法
HabitTrackerService interface HabitTrackerServiceImpl class
實作 Dao 層
- 在 HabitTrackerDao interface 宣告 addPickedDay 方法和 removePickedDay 方法
- 實作之前可先在 Query Console 測試 MongoDB 原生語法
- 接著再回到 HabitTrackerDaoImpl class,實作 addPickedDay 方法和 removePickedDay 方法
- addPickedDay 方法中會先檢查指定的 habitTracker Document 是否存在,如果尚未存在則以查詢條件建立一個,然後才去執行更新語法(使用前才會去建立使用者的 habitTracker Document)
Query Console(圈選日期)
Query Console(取消圈選日期)
HabitTrackerDao interface
HabitTrackerDaoImpl class(addPickedDay)
HabitTrackerDaoImpl class(removePickedDay)
- addPickedDay 方法中會先檢查指定的 habitTracker Document 是否存在,如果尚未存在則以查詢條件建立一個,然後才去執行更新語法(使用前才會去建立使用者的 habitTracker Document)
- 接著運行 Spring Boot 程式,使用 API Tester 測試一下效果
addPickedDay API
removePickedDay API
實作前端串接 API
- 在 src/axios/index.js 新增 apiPickedDayAdd 方法和 apiPickedDayRemove 方法,然後再 export 出去給外面的組件 import
export const apiPickedDayAdd = (userId, habitId, year, day) => instance.patch( `/users/${userId}/habitTrackers?habitId=${habitId}&year=${year}`, { op: "add", path: "/pickedDays", value: { id: day.id, date: day.date, }, } ); export const apiPickedDayRemove = (userId, habitId, year, day) => instance.patch( `/users/${userId}/habitTrackers?habitId=${habitId}&year=${year}`, { op: "remove", path: "/pickedDays", value: { id: day.id, date: day.date, }, } );
- 在 habitTracker.vue 中根據所選的日期是否已存在 pickedDays 陣列
- 如果尚未存在則呼叫 addPickedDay API
- 如果尚未存在則呼叫 removePickedDay API
- 最後顯示更新後的結果
import { apiHabitsQuery, apiHabitTrackerQuery, apiPickedDayAdd, apiPickedDayRemove } from "../../api/index.js"; ... onDayClick(day) { let self = this; let idx = self.pickedDays.findIndex((d) => d.id === day.id); if (idx >= 0) { apiPickedDayRemove(self.$store.state.userId, self.pickedHabit.habitId, self.pageYear, day).then( (res) => { this.pickedDays = res.data.pickedDays; } ) } else { apiPickedDayAdd(self.$store.state.userId, self.pickedHabit.habitId, self.pageYear, day).then( (res) => { this.pickedDays = res.data.pickedDays; } ) } }
運行結果
年曆日期圈選功能