[專案紀錄] 記帳 web app 升級!實作登入功能 — 專案緣起&回顧篇
專案回顧與挑戰
Live Demo on Heroku (先註冊一個帳號就能使用囉)
這次的專案是在學習使用 Express.js 架設後端伺服器、做出基本 CRUD 功能的經典案例 to-do list 後,延伸出來的一個 side project。一個功能齊全的 web app 一定少不了讓使用者操作增、刪、查、改的功能,那麼除了這些基本功能之外還能夠再加入什麼元素,讓這個 web app 變得更加「個人化」呢?
前端的部分,或許是自訂的背景、夜晚模式、排版方式或顯示語言等等;後端的部分,就是加入「登入/登出/註冊」的功能了吧!只要有了這些功能,就能夠多人使用同一個網站,利用不同帳號分別記錄各自的資料、使用者只能看到自己的頁面,無法取用別的帳號資訊、也能透過帳密驗證機制保護自己資料的安全。說到安全、機密,那就是錢了吧!正好記帳軟體與 to-do list 是差不多的概念,都是 CRUD 的組合,那就來做一個記帳 web app 吧~
由於基本 CRUD 功能先前已經做完了,因此列出這次想要升級的部分:
希望打造功能
- 註冊功能
- 打造 登入/登出功能
- 驗證登入狀態,取得訪問權限
- (新增功能) 可以同時篩選月份與支出類別
而這邊也列出實作時使用到的新套件們:
- Mongoose — 使用 MongoDB 資料庫儲存、調用使用者資訊
- Passport.js — 打造驗證使用者機制
- Express-session — 打造驗證狀態機制
- bcrypt.js — 密碼加密
由於這篇主要是回顧專案緣起與過程,因此簡述幾個比較深刻的記憶點,主要集中在路由設計和畫面操作上,驗證設定等的挑戰可以在這系列文的其他篇看到。
相對能掌握的部分
對於路由的設計和路由彼此間的關係算是漸趨熟悉,例如要打造登入/登出/註冊功能,可以很快想到要設計出跟這些動作相對應的路由:
GET /users/register → 註冊頁面GET /users/login → 登入頁面POST /users/register → 註冊POST /users/login → 登入GET /users/logout → 登出
而這次也是第二次實作路由與瀏覽器回應請求的動作設計,發現了一個過去認為理所當然,但仔細一看還是不了解的問題:
res.render() 和 res.redirect 有何不同?
在寫路由的時候,常常會遇到這兩個乍看之下很相似的回應動作,如果傳入的路由/模板相同,例如 res.render(‘login’) 是渲染 login 頁面、res.redirect(‘/users/login’) 是重新導向 login 頁面,似乎都是在做同一件事情,差別在 res.render() 裡面要傳入的是 template 名稱,而 res.redirect() 中要傳入的是路由。
原來這兩個動作的目的完全不同,也造成了兩者使用情境上的差異,因為 redirect 的過程是這樣的:
- 使用者訪問
/login
. - 使用者提交表單資料
- 伺服器驗證使用者資料並開始一段新的 session
- 伺服器利用
res.redirect('/home')
(或是任何你想要的 URL) 來告訴瀏覽器,去訪問這個 '/home' 的 URL - 瀏覽器執行重新導向到 ‘/home’ 並提交新的 request 給 server
- server 這時候看到傳來的 request,所以利用
res.render('home')
這個方法,根據模板 home.hbs 去渲染這個畫面所需要的資料 - 瀏覽器顯示 render 後的結果
所以當我們透過登入頁面的表單取得使用者的資料後,應該要使用 res.redirect() 來攜帶登入資訊進入目標頁面,而不是單純 render 沒有收集過任何 data 的 目標頁面。
參考資料:
Should I use res.render or res.redirect with Express and EJS?
Proper usage of Express’ res.render() and res.redirect()
透過這次查資料釐清觀念,也讓我更加熟悉在什麼情境應該要使用什麼工具了。
花了最多時間的部分
這次卡最久的地方是打造 同時篩選月份與支出類別的功能。由於先前已經有支出類別的篩選功能,但沒想到只是加個同時再篩月份的功能,就變得這麼複雜。
希望在月份的下拉式選單中的選項,能夠只對應到目前此使用者所有記帳項目的月份就好,也就是說如果這個使用者的記帳項目都在 2019 年的 二月 到 四月,那麼下拉選單的選項就只會有 二~四 月的選項:
看了一下我原本寫的 支出類別 的下拉式選單程式碼,感覺沒救了,因為我當初不明原因沒有用樣板引擎的 {{#each}} 方法去自動產生資料庫中的選項,看在類別數目不多的份上直接硬刻:
但月份不能這麼做,沒有辦法硬刻且需要連動資料庫的資料,若將某個二月份唯一的記帳項目修改日期為 三月,那麼下拉選單中就不能有 二月 這個選項;另外也要排除掉不同年同月的情況…
於是!我想到的辦法就是將 record model 做修改,在 Schema 中硬是加入了 month 的欄位,讓我之後可以用樣本引擎產生選項時,可以直接調用這個變數:
雖然看起來真的是土法煉鋼的超可怕 legacy code,但勉強算是解決了問題,之後勢必要回來重構,絕對是有更好的做法,到時候再來寫一篇重構的紀錄。
這次花了很久時間才解決問題,前半部是因為要找出 想要操作的資料對象,以及怎麼讓它能夠被利用,所以拼命的 console.log() 印出每次 promise .then() 中 return 的物件,最後找到 record[i].month 並存入陣列中;後半部則是卡在不知道怎麼將陣列中重複的物件剃除,因為以原本的方式,如果 2020年二月 有三條記帳,那麼 202002 這個選項會被重複存入陣列三次,不知道怎麼解決。
於是求助 Google 大神,讓我找到了這篇文章:JavaScript取出”陣列-物件“重複/不重複值的方法,以及 MDN,找到了去除陣列-物件中重複值的方法 ─ .set(),而 MDN 的定義中提到 A value in the Set
may only occur once,說明了 set 中的值並不會重複。
這完全解決了重複選項的問題,先利用 .filter() 來定義篩選出新陣列的方式,再利用 .set() 的方法 set.add(),若該項目不存在就加入 set 中,已存在的也不會重複加入。因此我的月份下拉選單中,就不會同時有好幾個 2020 年二月 的選項了~
自省與回顧
這邊雖然只列舉出一兩個印象深刻的過程,但可想而知整個開發過程是充滿了困難,除了登入登出機制的 Passport.js 套件可以照著文件與網路上的教學文手把手設定之外,我想造成卡關與困難的主因還是出在自己對 JavaScript 語法的不熟悉上面。常常需要打造什麼樣的功能,能夠想像怎麼做,但不知道有那些方法可以使用,特別是陣列的操作,例如 .filter()、.forEach() 這些的完整運用方式我也不夠熟悉,所以想半天還是做不出來。
這些部份應該就是要回歸熟悉語言本身的基本功,去了解基礎知識,例如陣列、物件的性質,有哪些方法,操作方法後原本的陣列會被改動嗎?等等的問題,都是接下來要去好好回顧與鑽研的,日後開發時才能正確使用這些語言提供給我們的工具與特性。
這次的專案並不完美,有許多可以重構的部分,但也是因為看見自己一個月前的程式碼,再看看現在的自己已經可以優化某些部分,有感覺到自己似乎有多累積一點點知識,可以解決一些以前只能硬幹的問題。希望之後有辦法寫一個重構篇,將一些問題解決,也把一些 promise 語法優化成 async/await 的寫法,讓程式碼更易讀、容易維護!