0%

Vue 筆記 - 取得 Token 並開始倒數計時至登出

本篇為專案遇到倒數計時的功能開發筆記,主要也釐清自己在工作上思路邏輯似乎沒有很清楚,給自己一個重新思考本次專案的練習,透過【coding 第六講:要怎麼練習程式思考?】的建議方式,來做本篇記錄。

會依照文中建議的幾個方式並稍微修改符合我的需求來陳述:

  1. 清楚描述問題
  2. 拆解問題
  3. 把問題簡化
  4. 構思(評估)問題
  5. 組合解法
  6. 問:然後呢?

需求

此功能為退票功能,在退票流程中要符合 WCAG 規範提醒使用者登入到期時間,所以需要有一個倒數計時器來提醒。

清楚描述問題

  • 輸入票號會進入退票流程,主要為身分驗證頁到明細頁面。
  • 因需符合 WCAG 規範,故要添加於 40 分鐘 token 到期通知,用彈窗提醒視障與聽障使用者。
  • 於 30 分鐘要顯示溫馨提示。
  • 於 40 分鐘前要顯示溫馨提醒,並跳轉回退票頁面,因串接後端 API 有時間差,故開發時會提早一點時間先跳彈窗,避免後端過期,導致前端頁面操作出現問題。

把問題簡化

  • 在指定頁面才倒數計時。
  • 已知倒數時間總長為 40 分鐘,故在 30 分鐘時要顯示提示,在 40 分鐘前要顯示提醒。

構思(評估)問題

  • 這次開發僅針對退票流程頁面,並不會影響到專案的其他功能與程式碼。

組合解法

  • 本次專案使用 Nuxt2 開發,使用 <nuxt-child/> 的嵌套去抓取退票資料夾下的所有檔案。
  • 取得退票 token。
  • 將 token 解碼(decode)取得 expireTime。
  • 在指定流程頁面才能倒數計時。
  • 設定開始倒數計時。
  • 設定結束倒數計時。
  • 30 分鐘的提示彈窗。
  • 40 分鐘提醒彈窗。
  • 已知呼叫 API 為非同步,故會設定提前時間先跳出提醒彈窗。
  • 提醒彈窗跳出後跳回退票首頁。

程式開發說明

取得退票 Token

  • 從 Vuex 中的 state 取得 token。
  • token 只要取一次就好,所以使用 computed 取值。
  • 透過 decode 把 token 的 expireTime 取出,並且乘 1000,意旨時間單位改成瀏覽器看得懂的最小單位(毫秒)。
  • 最後用 try catch 方法包起來,確保可以正確取得時間。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
computed: {
...mapState('Ticket', {
vxRefundOrderToken: 'refundOrderToken',
}),
expireTime() {
try {
const { vxRefundOrderToken } = this;
const { exp } = this.$decodeBase64(vxRefundOrderToken.split('.')[1]);
return exp * 1000; // orderToken 內的 exp 以秒為單位
} catch (err) {
return null;
}
},
},

可用此decode base64 工具在依照自己的需求,看看所得到的 token 內容。

指定頁面

  • 退票流程會有以下五個流程,退票首頁、身分驗證、驗證碼、明細、完成等,除了退票首頁跟完成業外,都要持續的倒數計時。
  • 故要取得目前頁面。
  • 知道現在在哪一頁。
  • 開始計時與結束計時。

先判斷我要倒數的頁面,這邊就是除了退票首頁跟完成頁外,都要倒數計時;當然也可以用正向判斷,然後要改成 ||,就可以正確判斷,用反向判斷的原因只是可以少寫一行而已。

1
2
3
4
5
6
7
8
methods:{
beginTimerWithPages() {
return (
this.currRouterName !== 'pages-online-refund' &&
this.currRouterName !== 'ticket-refund-completed'
);
},
}

取得目前頁面,使用 this.$nuxt.route.name 取得目前頁面名稱,透過開發人員工具可以看到取得頁面會出現這樣的字串,例如:page-online-refund___TW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
computed: {
...mapState('Ticket', {
vxRefundOrderToken: 'refundOrderToken',
vxTips: 'tips',
}),
// 取得目前頁面
currentRouterName() {
return this.$route.name.split('___')[0];
},
expireTime() {
try {
const { vxRefundOrderToken } = this;
const { exp } = this.$decodeBase64(vxRefundOrderToken.split('.')[1]);
return exp * 1000; // orderToken 內的 exp 以秒為單位
} catch (err) {
return null;
}
},
},
  • 取得頁面名稱後,需要知道現在在哪一頁,因為頁面是動態切換的,所以可以使用 wacth 監聽頁面的路由名稱。
  • 監聽到指定路由時開始倒數計時。
1
2
3
4
5
6
7
8
9
10
watch: {
currentRouterName: {
handler() {
if (this.beginTimerWithPages()) {
this.timerStart();
}
},
immediate: true,
},
},

倒數計時開始

  • 我要做倒數計時了,怎麼開始?
  • 首先要有判斷什麼時候開始倒數。
  • 目前我有未來的時間 expireTime,所以我要用這個時間減掉現在時間,就是接下來要倒數的時間,也就是我從現在開始往後推 40 分鐘要完成退票。
  • 有開始就有結束,先設定 timerStart()timerEnd() 兩個函式。
  • 連續性的時間就要想到 setInterval,會連續執行不間斷,會回傳一個數值。
  • data 用一個 sid 的變數記錄此數值。
  • 所以判斷就會是若沒有 sid 的值以及有未來時間時,就要開始倒數。
1
2
3
4
5
6
data() {
return {
sid: null, // setInterval 儲存的編號(拿來判斷是否逾時)
homepageUrl: '/pages/online-refund',
};
},
  • 因為沒有 sid,所以就要去存取 sid。
  • 用 timer 變數記錄每一次的時間差,得到時間差後要先除以 1000,把毫秒轉換成秒,因為會得到小數點,所以在用 parseInt 轉成整數。
  • 並且在 setInterval 是持續進行的,所以會在讀取到指定時間以及在此時間內若監測到非指定頁面要停止計時。
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
timerStart() {
// 什麼時候開始倒數
// 當 sid 沒有紀錄以及取得 expireTime 時
// 使用時間要想到 setInterval 來執行
if (!this.sid && this.expireTime) {
this.$log('開始倒數計時');
const now = Date.now();
// 因為 expireTime 是未來時間,所以要減去現在時間,取得時間差
// 因瀏覽器時間最小單位為毫秒,所以要除以 1000,轉換單位成秒
// 因取出來不是整數,故用 parseInt 轉為整數
// 設定 sid 為 setInterval 的回傳值
this.sid = setInterval(() => {
// 抓取每一秒的時間差
const timer = parseInt((this.expireTime - now) / 1000);

// 結束前 20 秒
if (timer <= 20) {
// 停止計時
//跳出提醒彈窗
} else if (timer < 10 * 60 && !this.vxTips) {
// 剩下十分鐘的提示彈窗
}

// 如果不是指定頁面就停止計時
if (!this.beginTimerWithPages()) {
//停止計時
}
}, 1000);
}
},
  • 最重要的倒數條件:當 30 分鐘與 40 分鐘前要各跳一次提示跟提醒。
  • 因為單位是秒,當 30 分鐘要跳提示時,換句話說就是 time 小於 10 分鐘,也就是 10 分鐘*60 秒 = 600 秒,當 600 秒要跳提示。
  • 因為 API 呼叫有時間差的關係,故提前 20 秒(30 秒太長,10 秒太短)跳出結束的提醒彈窗。
  • 這邊要顧慮到不能先把倒數 600 秒寫在前面,因為如果現在倒數 500 秒時,就已經符合小於 600 秒的條件,之後就不會在跳小於 20 秒的條件。也就是說:
1
2
3
4
5
if (timer <= 20) {
// 20 以內才會跑這段
} else if (timer < 10 * 60 && !this.vxTips) {
// 500 秒時會先跑這段
}

如果寫反:

1
2
3
4
5
if (timer < 10 * 60) {
// 500 秒會先跑這段
} else if (timer <= 20) {
// 前面已經符合條件,這段都不會再跑了
}
  • 此時又想到,那如果十分鐘已經提示過了,應該要記錄,才不會之後又跳出一次。
  • 所以可從 state 取出 tips 的變數當作儲存提示過的紀錄,若有就在提示彈窗出現後給 true,代表出現過了。
  • 已經完成倒數時間的判斷,再來就是把彈窗方法放到對應的判斷中。
    停止計時
    的呼叫判斷為,如果發現 sid 有值,就用 clearInterval 清除 sid,並且把 sid 清空,這樣下次重新登入時才會重新計時。

10 分鐘彈窗
剩下十分鐘跳出彈窗後,也記錄有提示過了。

1
2
3
4
5
show10minsLeftPopup() {
alert('剩下十分鐘');
// 紀錄已經提示過
this[actions.TICKET_SET_REFUND_TIPS](true);
},

結束彈窗
時間已到,把 token 清空,之後登入會重新取得 token。

1
2
3
4
showExpireTimePopup() {
alert('登入逾時,請重新登入');
// 因為已經登出,token 已經過期,要把 token 清空
this.$store.commit(`Ticket/${mutations.TICKET_SET_REFUND_TOKEN}`, '');

看起來應該寫完了,可以進行測試了,但先等等…記得要先問自己一下:然後呢?

然後呢?

避免瀏覽器跳轉頁面仍然持續執行 timerEnd(),使用前端框架的生命週期註銷 timerEnd(),這樣離開指定頁面時就能確保不會在執行 timerEnd() 了。

1
2
3
destroyed() {
this.timerEnd();
},

完整程式碼

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
data() {
return {
sid: null, // setInterval 儲存的編號(拿來判斷是否逾時)
homepageUrl: '/pages/online-refund',
};
},
methods: {
...mapActions('Ticket', [actions.TICKET_SET_REFUND_TIPS]),
// 倒數計時
timerStart() {
// 什麼時候開始倒數
// 當 sid 沒有紀錄以及取得 expireTime 時
// 使用時間要想到 setInterval 來執行
if (!this.sid && this.expireTime) {
this.$log('開始倒數計時');
const now = Date.now();
// 因為 expireTime 是未來時間,所以要減去現在時間,取得時間差
// 因 JS 是毫秒計,所以要除以 1000,轉換單位成秒
// 因取出來不是整數,故用 parseInt 轉為整數
// 設定 sid 為 setInterval 的回傳值
this.sid = setInterval(() => {
// 要寫在 setInterval 內,否則不會倒數
const timer = parseInt((this.expireTime - now) / 1000);
// 如果 timer 小於30 分鐘,要顯示提示彈窗
// 如果 timer 剩下 20 秒要顯示提醒彈窗
/**
* ? 為什麼不能把 10 分鐘寫在前面,因為如果 10 分鐘的條件成立,就不會進入 20 秒的條件
*/
if (timer <= 20) {
this.timerEnd(); // 先清除 setInterval 避免重複跳溫馨提醒
this.showExpirePopup();
// 剩下 10 分鐘前顯示溫馨提示
} else if (timer < 10 * 60 && !this.vxTips) {
this.show10minsLeftPopup();
}

// 如果不是指定頁面就停止計時
if (!this.beginTimerWithPages()) {
this.timerEnd();
}
}, 1000);
}
},
// 停止倒數計時
timerEnd() {
// 如果 sid 有紀錄,就停止計時
if (this.sid) {
this.$log('停止倒數計時');
clearInterval(this.sid);
this.sid = null; // 清空 sid,下次要重新計時
}
},
// 哪些頁面要倒數計時
// 當前頁面是 verification-code 或 verification 或 detail 時,開始倒數計時
// return boolean
beginTimerWithPages() {
return (
this.currRouterName !== 'booking-manage-my-trip-online-refund' &&
this.currRouterName !== 'ticket-refund-completed'
);
},
showExpireTimePopup() {
alert('登入逾時,請重新登入');
// 因為已經登出,token 已經過期,要把 token 清空
this.$store.commit(`Ticket/${mutations.TICKET_SET_REFUND_TOKEN}`, '');
},
show10minsLeftPopup() {
alert('剩下十分鐘');
// 紀錄已經提示過
this.$store.commit(`Ticket/${mutations.TICKET_SET_REFUND_TIPS}`, true);
},

小結

這次雖然是用參考其他頁面相同功能的程式碼練習寫出來的專案,但以前看程式碼都大概看看,並沒有完全了解,導致當開始要解決問題時,並不知道如何下手,也不知其寫這段的原因如何?整理一下利用文章方法的心得:

清楚描述問題

透過清楚地描述問題,可以釐清自己對於需求與實際問題的理解是否有落差,透過文字的確比較能把問題逐一列出,並抓到癥結點。

拆解問題

通常客戶或業務單位提出的問題都很攏統,只能把一個大問題拆分成許多可能會遇到的小問題,若拆成小問題仍然還有很多問題,那就在拆成更小的問題,把問題變成一個單位來解決。

把問題簡化

簡化問題也是檢視自己對於問題的理解,看看理解是否與符合需求。

構思(評估)問題

縱向的收到問題,但還要橫向的看問題是否會產生副作用,會不會影響到專案其他面向或是產生其他衍伸的問題。

組合解法

問題拆分成小單位後,此時就會進入寫程式的過程,先寫基本功能,並且會有延伸知識要去查找資料與吸收,再來就是測試與優化,測試可用暴力測試一些極端性的操作,看看會不會出現沒有預設到的狀況。

問:然後呢?

這個我覺得更關鍵,一直問自己然後呢? 就會思考完成後會不會有下一步真的很重要,因為目前程式碼是否具有彈性可以擴充或可以共用,具有高內聚低耦合的特性。

心得

透過以上思考方向,在開始開發程式真的會比較有效率,希望透過這次練習可以讓自己在程式開發的思維上有所進步。