Alpha Camp 學期 2-3 結業心得

隨著時間的推移,在學習上總是會碰到各種不同的狀況,而這些狀況也提供我不同的學習感受。這篇著重於紀錄自己遇到、找出並排除問題的過程。

要解決問題,必須先正確的找出問題發生原因。

緣起:為短網址產生器加上重複檢查機制

一如上學期結束時對自己許的願,這學期在寫作業的同時會盡可能地做些優化挑戰,然後不可避免的進入無窮卡關迴圈……

近期卡了最久的就是短網址產生器了!! 作業的要求其實很簡單,只要可以產生英文字母或數字的隨機亂碼來當短網址即可。但是! 我突然想到:「萬一真的產生了相同的亂碼怎麼辦?」。依照我當下的執行邏輯,產生亂碼之後就馬上把它和原始網址加入互相對照的 index ,在沒有檢查的情況下,兩組原始網址導向同一個短網址也不是不可能發生。

於是,我決定了優化方向:「在亂數產生器 return 結果之前加上檢查機制」,只要發現相同亂碼就重新執行一次亂碼產生流程。然後……在這個優化過程被卡了整整 2 天 🤣

嘗試失敗1:同步與非同步

先來備註一下:這次用來存放 UrlIndex 資料的是 MongoDB。

原本的寫法如下(只放有問題部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 產生亂碼短網址 <file:root/utils/shortGenerator.js>
function generator (length) {
// 1. generate random code
// 2. Check if random url already exist
// => if exist, run "generator (length)"
// => if not yet exist, return result
UrlIndex.findOne({ shortenUrl: result })
.then(data => {
return data ? generator(length) : result
})
}

// 把短網址存入對照 index <file:root/app.js>
UrlIndex.create({
shortenUrl: generator(5),
originalUrl: req.body.url
})

在我的預想中, generator() 負責產生獨一無二的短網址,而 UrlIndex.create() 把取得的短網址拿去建立 index 。但是,看似沒問題的邏輯跳錯誤了! 好在目前遇到的大部分錯誤訊息都能正確敘述出問題的原因,它告訴我 shortenUrl 的值 undefined 。於是我去查查賦值 undefined 的原因,發現在我寫的流程裡最有可能的原因是:「若變數沒有提供初始值,則預設為 undefined 」。但不對啊~我不是有告訴 UrlIndex.create() shortenUrl 的值從 generator() 取得嗎? 為什麼會 undefined 呢?

前幾天因緣際會接觸到「非同步」相關知識的我想起了前輩對非同步的評語:

實戰常常遇到問題,一研究發現,怎麼又是你!

檢查了一下目前進度,確實啊~ UrlIndex.create()UrlIndex.findOne() 都是非同步語法,如果沒有處理好,根本無法控制完成的先後順序。於是,再度開始了 google 旅程,發現目前對於非同步執行順序最常使用兩種方式:

  • async/await
  • Promise.then()

因為對 async/await 值的傳遞方式還不太熟悉,因此決定先用 Promise.then() 的方式。 .then 的前面必須是個 Promise 物件,但我目前 return 的是亂碼「值」。幸好查到的資料告訴我, mongoose 5.0 開始就內建 Promise 語法,所以我可以把整個 UrlIndex.findOne() 包成 Promise 物件。最後,我把整個檢驗是否重複的過程直接 return ,順利解決了執行順序問題。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 產生亂碼短網址
function generator (length) {
// (前略)
return UrlIndex.findOne({ shortenUrl: result })
.then(data => {
return data ? generator(length) : result
})
}

ShortGenerator(randomLength)
.then(url => {
return urlData ? urlData : UrlIndex.create({
shortenUrl: url,
originalUrl: req.body.url
})
})

嘗試失敗2:mongoose 語法 return 值

接著,我想把產生的短網址結果呈現給使用者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.post('/', (req, res) => {
UrlIndex.findOne({ originalUrl: req.body.url })
.then(urlData => {
ShortGenerator(randomLength)
.then(url => {
return urlData ? urlData : UrlIndex.create({
shortenUrl: url,
originalUrl: req.body.url
})
.then(urlData => res.render('index', { urlBasic, urlData })) // 執行到這行出問題!!
.catch(err => console.log(err))
})
.catch(err => console.log(err))
})

這時候又跳錯誤了! 根據錯誤訊息查詢,發現是 handlebars 因為安全性問題,只接受乾淨的物件資料,通常可以用 .lean() 解決。把 urlData 印出來,看看到底取到什麼值? 嗯?? _id 裡面那個 new 是什麼? 看起來確定是回傳值的問題。
error_returnValueOfMongoose

於是我加上了 .lean() ,但它又報錯了!

1
2
3
4
.then(urlData => {
urlData = urlData.lean()
res.render('index', { urlBasic, urlData })
})

error_mongoose.lean

測試後發現:如果是已建立 index 的網址就沒問題;反之,如果是新建的就會報錯。那就表示兩者拿到的資料不一樣。仔細查閱了 mongoose 官方文件, UrlIndex.findOne() 回傳 Query、 UrlIndex.create() 回傳 Promise 物件……好吧! 接下來的問題就是如何取 UrlIndex.create() 的「值」?

在各種變換關鍵字後,終於在 stack overflow 找到救世主,可以用 toObject() 進行資料轉換。

1
2
3
4
.then(urlData => {
urlData = urlData.toObject()
res.render('index', { urlBasic, urlData })
})

大功告成! 一切終於都如我預期的運作了!!

成品程式碼: 在這裡

心得分享

寫出來真的很有成就感啊!! 除了這個作業優化外,其實前面的作業也有經過類似的優化煎熬過程。總結起來解決問題的方法分幾個部分:

  • 預防 – 仔細閱讀使用工具的官方文件、了解它的使用方法,正確的使用工具可以排除很多可能發生的問題。
  • 找出問題 – 知道問題發生原因才是解決的最快方法。就像前面分享的問題二,看起來是 handlebars 接受的資料類型問題,實際上是 mongoose 回傳值的原因。
  • 查找資料 – 在找出正確問題的前提下,盡可能下對關鍵字可以找到更接近需求的內容。

不過沒有一次找到目標資料也沒關係,多多閱讀的過程中也能吸收各種知識,也許會在未來的某一天成為解決其他問題的關鍵!

文章內容如有錯誤,歡迎留言討論!


本 Blog 上的所有文章除特别聲明外,均採用 CC BY-SA 4.0 協議 ,轉載請註明出處!