[專案紀錄] 記帳 web app 升級!實作登入功能 — 專案緣起&回顧篇

I caught a code
I Caught a Code
Published in
8 min readMar 19, 2021

專案回顧與挑戰

以鳴人作為主題的記帳 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 功能先前已經做完了,因此列出這次想要升級的部分:

希望打造功能

  1. 註冊功能
  2. 打造 登入/登出功能
  3. 驗證登入狀態,取得訪問權限
  4. (新增功能) 可以同時篩選月份與支出類別

而這邊也列出實作時使用到的新套件們:

  1. Mongoose — 使用 MongoDB 資料庫儲存、調用使用者資訊
  2. Passport.js — 打造驗證使用者機制
  3. Express-session — 打造驗證狀態機制
  4. 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 的過程是這樣的:

  1. 使用者訪問 /login.
  2. 使用者提交表單資料
  3. 伺服器驗證使用者資料並開始一段新的 session
  4. 伺服器利用 res.redirect('/home') (或是任何你想要的 URL) 來告訴瀏覽器,去訪問這個 '/home' 的 URL
  5. 瀏覽器執行重新導向到 ‘/home’ 並提交新的 request 給 server
  6. server 這時候看到傳來的 request,所以利用 res.render('home') 這個方法,根據模板 home.hbs 去渲染這個畫面所需要的資料
  7. 瀏覽器顯示 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 年的 二月 到 四月,那麼下拉選單的選項就只會有 二~四 月的選項:

如果我只篩四月的話,Sasuke 的 dashboard 就只會顯示「宇智波家的房貸」這一項

看了一下我原本寫的 支出類別 的下拉式選單程式碼,感覺沒救了,因為我當初不明原因沒有用樣板引擎的 {{#each}} 方法去自動產生資料庫中的選項,看在類別數目不多的份上直接硬刻:

該找時間重構了...

但月份不能這麼做,沒有辦法硬刻且需要連動資料庫的資料,若將某個二月份唯一的記帳項目修改日期為 三月,那麼下拉選單中就不能有 二月 這個選項;另外也要排除掉不同年同月的情況…

於是!我想到的辦法就是將 record model 做修改,在 Schema 中硬是加入了 month 的欄位,讓我之後可以用樣本引擎產生選項時,可以直接調用這個變數:

routes/model/records.js 以新增項目為例,將月份與日期切割開來,與日期一併存入資料庫中
routes/model/home.js 將篩選過的項目的月份(物件) 存入 months 陣列中,再用 Set() 方法留下不重複的物件,存入 monthOptions 陣列中..
再回來樣板引擎中用陣列{{#each monthOptions}} 來渲染下拉式選單..

雖然看起來真的是土法煉鋼的超可怕 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 的寫法,讓程式碼更易讀、容易維護!

--

--