從 0 開始做 Side Project

前言

身為一個 junior 前端工程師,為了求職或者為了滿足自己的成就感,或多或少都會想自己寫一個 「side project」 (以下皆以玩具代稱) ,用說的很容易,實際上想要動手卻不知道從何開始,因此想要稍微紀錄並分享一下我是如何一步步打造屬於自己的玩具。


選定目標

常常看到很多大神們在社群上公開自己的作品,進而激發自身潛在慾望。

「啊,這個東西好酷啊,不曉得怎麼做的,好想也寫一個玩具」諸如此類的想法。

但往往真正要動手的時候腦袋一片空,不曉得怎麼挑選一個主題來做。

關於玩具「主題」的選定,來源或許有以下:

  • 最近流行的話題
    • 口罩
    • 新冠肺炎
    • clubhouse
  • 源自於別人推坑
  • 延伸、拓展自己有興趣的事物
  • 解決自己、或他人遭遇的痛點

不管挑哪個做都好,最重要的還是得讓自己有興趣,不然就沒戲唱了。

想著想著,最後挑了第四個「解決自己、或他人遭遇的痛點」

我不常逛 Dcard ,頻率大概一星期一、二次吧。

但是我會去某些版專門看藍頭 (男生) 或粉頭 (女生) 發的文,然而 Dcard 並沒有針對性別過濾這樣的功能。

雖然看起來理由有點薄弱,不過只要能說服自己就是好理由!

很多時候驅使我們做任何事,理由往往不需要很冠冕堂皇。

所以如果跟我一樣不太知道要做些什麼,也是蠻推薦從解決日常生活中痛點起手。

也許你可能會說「想做的東西大家都做過了」,但是這個痛點不見得別人有很好解決掉,而這個工具是為了解決這個獨有的痛點產生的,這麼想是不是就有做的價值了 XDD

最後我幫這個小工具取名為 「Dcard helper

…我知道很沒創意啦 QQ


建立儀式感 (就職後掌握的開發流程)

不曉得大家有沒有一個狀況,就是當在做任何事情之前一定要做某件事,若跳過感覺就怪怪的,諸事不順這樣。

我自己是有這樣的情況,而且發現透過進行「特殊的」儀式會讓自己更有自信、更好的累積成就感。

好啦,不賣關子,說的好像什麼邪教儀式似的 XD

其實只是透過一些行為培養、累積自己的信心,促使自己持之以恆。

進入職場後,在工作上我們運用了 Gitlab 做了以下這些事情:

  • 利用 Gitlab 的 Issue 做紀錄、並且紀錄開發時數
  • 遵循 Gitlab Flow 進行開發
  • 寫 CHANGELOG 、 README
  • Commit 的結尾附上 Issue 的編號方便追蹤
  • 幫那些 Issue 打上美美的標籤 (?!)
  • 推上 Gitlab 的 Code 一定要通過 test 才能進行後續的 Code Review 環節,都通過才由 Leader 進行 Merge

而這些事情都是我剛入職前沒有掌握到的,我認為做了這些事情能讓整體 Code 的品質上升。

這些儀式雖然一開始做起來很麻煩,但累積起來會讓我莫名有成就感,而且也方便他人日後觀看我是如何完成這個玩具的。

另外 README 、 CHANGELOG 的目的是為了加深當初為什麼自己要做這玩具的想法以及紀錄玩具在每個 Issue 間的變化

當然每個人的想法不一樣,這是我自己測出來如何讓自己堅持下去的方式,分享給各位參考囉。


事前準備 - 框架的挑選

有了想法之後,接下來就可以開始進行技術層面的粗估,像是要用什麼前端要用什麼框架、後端要怎麼做、接哪個資料庫等等…。

到這邊可能就會開始有人開始不知所措,不知道要怎麼選。

就以我的例子來舉例,「我想要一個能針對 Dcard 發文者性別篩選的網站」,用最直白的話來描述我想做的玩具大概是這樣。

也就是說,為了達成這個目標可能我需要:

  • 前端 UI ,呼叫 API 渲染畫面
    • 是否使用框架加速開發 (Angular / Vue / React / Other…) 或純 Javascript
      • 公司是使用 Angular 進行開發,所以自然的我也比較熟這個框架
    • 是否使用 CSS framework (Bootstrap / Tailwinds / Material / Other..) 或手刻
  • 後端 Server
    • 使用什麼技術搭建 Web Server
    • 是否需要搭配資料庫 (MySQL / PostgreSQL / MongoDB / Other…)
      • 我需要有個地方能存放爬蟲爬取的資料最終選了 MongoDB
        • 這邊就是 SQL 與 NoSQL 的取捨 (關聯式資料庫 VS 非關聯式資料庫)
        • 優缺點網上有許多前輩分析過了,大家可以下關鍵字搜尋
        • 一方面沒用過、一方面達成這個目標資料庫不需要太嚴謹也沒關係,所以選 MongoDB
          • 其實本來想用 Firestore ,但我怕如果之後要給別人玩流量大概很快就炸了?
    • 使用 TypeScript 進行開發 (非必要)
      • 因為工作使用 Angular ,早已習慣 TypeScript 的存在,看不到型別、「點」不出東西對我來說是很痛苦的事情
  • 爬蟲
    • 要用什麼技術來爬 Dcard 的資料
      • 我熟悉的後端與研究是 nodejs ,那我也是第一次接觸爬蟲,隨意看了一下就挑了 puppeteer
      • 卡斯伯老師曾經有介紹

就是說可以逐層抽絲剝繭的推敲出這個玩具大概的輪廓,當然初期可能推估出來的東西是錯誤的。

但至少有整理出東西,這樣也方便跟較有經驗的開發者討論,對方也比較好針對問題點回覆,簡單來說就是「要先有自己的想法,別人才有辦法跟你討論

動手吧 - 首先是最小可執行的產出

想法有了、也釐清可能會使用那些技術實作了,接下來就是動手的時刻了!

啊然後勒…?

這時候不妨再回想一下自己當初訂下的需求、目標。

以這個玩具為例:

  • 有個網站能呼叫 API 渲染畫面
  • 有個伺服器能提供 API 給前端呼叫,然後這個 API 的資料從 mongoDB 中取出
  • 有支爬蟲程式爬取 Dcard 的資料,並且存到 mongoDB

這樣推敲下來順序就是 「Dcard 爬蟲程式」 > 「Web server」 > 「網站 UI」,

不過我通常會從畫面去開需要那些 API ,所以順序我會改成 「Dcard 爬蟲程式」 > 「Web server」 > 「網站 UI」

所以第一步就是從爬蟲程式起手,我應該先了解 puppeteer 如何使用。

玩具剛開始的時候,會建議先訂一個小小的目標藉以培養自己的信心,若以這個玩具為例:

  • step1. 能順利運行 puppeteer 並前往特定網址截下一張圖
  • step2. 前往特定網址後能取得資料並以 console.log 印出
  • step3. 將資料於本地儲存以 .json 的形式

於是,我們就有了一個真的可以運作的爬蟲程式,隨著每次的運行應該會對這隻程式有一些想法,接下來就是按你所想的去優化它,好比說:

  • 串接資料庫將其存入
  • 研究如何翻頁然後反覆地爬取資料
  • 優化程式碼
  • …自由發揮

最後,這隻爬蟲程式就完成了,接著就可以往下個階段前進。

然後呢 - 儀式感帶來的好處

前面提到我很享受這些「儀式感」,一方面除了帶給我成就感之外,也能如實的反應我的開發情況:

  • 能得知這個 issue 花了多少時間去處理
  • 透過 CHANGELOG ,我能知道這個專案的變化
  • 透過 issue 能記錄當初開發時的想法,方便日後追蹤

比方說這個 issue

  • 之後如果有人想了解我這個玩具開發的脈絡,就可以透過 issue 了解
  • 也可以發現這個 issue 我花了 6 小時
  • 未來若是發現 code 改壞了,也可以透過 commit 來追是什麼時候改壞的

最重要的是做這些事情會讓我感受到,這個玩具是隨著我每次的 issue 完成,逐步成長的,自然會更有動力想完成它。

閒聊

最後,稍微閒聊一下在做這個玩具時,遇到那些問題、後來是怎麼避開(無視)的。

爬蟲部分

這部分我是初次使用 puppeteer 進行爬蟲,也遇到了一些奇怪但不知道怎麼解只好繞開的情形,如果有其他大大對這部分有心得的話還請不吝分享給小弟,感恩。

於是為了練練手也沒什麼想法,就很直接寫了一隻爬 google 搜尋結果的程式 example-crawler

爬是能爬啦,不過多爬幾次就會被關切是不是機器人,但至少是個有趣的體驗。

Cloudflare 防護

在我開始進行 Dcard 爬蟲程式的撰寫前,有稍微的 google 一下有沒有相關知識可以科普,於是發現也有不少前輩做過類似的事情,但好像沒有我想做的事 (太小眾了) ,
但意外地發現 Dcard 可以呼叫 API 就好,不見得真的要爬實際網站內容。

於是我就開了一個 issue 去記錄這件事情,然後開始我的嘗試。

一開始想說既然都有 API 了,是不是不用爬蟲也可以直接呼叫這支 API 之類的,寫好之後發現實際呼叫後的內容跟預期的不太一樣,原來是 Dcard 有使用 Cloudflare 防護。

後來還是乖乖地回來使用 puppeteer 繞開這件事情, puppeteer 背後其實是控制 Chrome 或 Chromium 瀏覽器來做我們想做的事情。

我想請 puppeteer 做的事情,就很像手動把 Dcard 的 API 貼到瀏覽器的網址列一樣,透過這個方式,我迴避掉了第一個難關。

但後來發現這個 Cloudflare 防護其實不只這麼簡單,要是爬蟲發出的請求沒有隨機的等待時間、或是根本沒有等待時間,不用多久就會被 Cloudflare 擋下來要求證明你不是機器人了 XDD

這部分我也想知道有沒有辦法教 puppeteer 去回答那些問題。

重構

程式寫著寫著,隨著 issue 一個個的被我關閉,也慢慢的發現程式碼的重複片段有點高。

可以看到我這個時候的 commit ,像是爬文章 (post)、爬看板 (forum)、爬熱門文章 (popular-post) ,裡面重複做的事情相當多,大致像是:

  • 建立一個 headless 模式的 Chromium 並且開啟新分頁
  • 前往某個網址 (這裡是填入各式資源的 API)
  • 將內容爬下來處理
  • 判斷有沒有下一頁,有就繼續前面的行為、沒有就結束

然而這裡要分享的不是如何重構,而是重構的時間點。

上面打的是我已經完成爬取文章、看板、熱門文章的爬蟲程式後綜觀整體才釐清的東西。

其實在更早在寫這部分程式的時候多少就有一點很「冗餘」的感覺。

當時有試著想要邊寫邊進行重構,但其實有些時候很多東西都還沒寫完,很難綜觀全局去優化。

也因為這樣花了比平常更多的時間去完成,而且重構的部分也不了了之。

後來乾脆等東西都寫完了,再開個重構的 issue 專心處理還比較省事。

所以我是建議如果功力不夠深厚,還是不要貿然嘗試邊寫邊優化、重構,不如等到能看清全貌了再動手會比較省事、省時。

最後這部分是運用了 class 的繼承去處理掉重複性質過高的程式碼。

怪異的問題 - 為什麼畫面截圖會導致崩潰?

進行爬蟲的時候,可能會需要處理一些錯誤情況,而這個情況也是我偶然發現的。

我寫的程式在進入錯誤處理的區塊時,會將當前的頁面截圖並保存圖片,方便我排查錯誤。

但我後來發現只要進入到截圖這個部分,程式就會卡住沒有回應,直到我關閉無頭模式 (headless) 後才發現原來是崩潰 (crash) 了!

網上搜了一些資料、甚至開新的專案以最簡短的程式嘗試模擬錯誤情況,就沒有遇到這樣的問題。

仔細觀察自己寫的程式,沒有發現明顯的錯誤,甚至也不知道成因的情況下,我選擇繞路走。

我把進入錯誤處理時的動作改成了截取前的頁面並存成 PDF ,雖說這樣就避開了,但心裡還是有點納悶的。

而且不是完美的解,畢竟存成 PDF 官方有說只有在無頭模式開啟時能用。

希望這部分有研究的大大們能夠指點小弟看看問題出在哪,感恩感恩。

爬蟲的時間與排程

目前我的爬蟲策略是這樣 (第一次執行程式):

  • 先爬取所有看板,把資料存入對應的集合 (Collection)
    • 例如看板存到 _forums 、熱門看板就是 _popular_forums
  • 逐步去爬每個看板,進到每個看板把文章都爬過一遍,爬完的看板就把對應的集合清空放入新的資料

初期我是先採取這樣的策略,確保資料的完整性。

但很快地就發現一個問題,那就是爬太久了~~

記得那時候是 228 連假時開始爬的, Dcard 那個時候的看板數量有 4XX 個吧 (含隱藏、校版)

每個發出的請求,我都隨機間隔 1 ~ 5 秒,猜猜爬完 Dcard 我需要花多久 ?

為了記錄這些時間,我寫了一隻小程式去紀錄時數、也把爬每個版需要花多少時間記錄下來方便自己分析。

Dcard 看板文章全爬蟲時數

總時數大概是落在 52.19 小時左右,那時候我有手動中斷一次可能圖片的看板總數與總時數不是最精確的,但這個數據還是相當值得參考。

問題來了,現在狀況是爬蟲不會辨識內容,只會從頭爬到尾。

如果說是初次運行,為了資料的完整性,徹底地從頭爬到尾需要這些時間是合理的。

但問題是之後如果要更新資料,即便我另外取得了熱門看板的 TOP 10 看板,按目前這個從頭爬到尾的爬蟲機制來看,想必還是很久的。

更何況是 TOP10 的看板,那個文章總數大概會嚇死人。


於是我的做法是這樣,由於文章的 API 不帶任何 QueryString 的話預設是從新到舊,這部分我沒有排序過,因此資料庫也是按這個順序儲存。

因此資料庫的第一筆會是那個時間點下該看板文章的第一筆。

就是說我只要在開始文章爬蟲之前,查一下資料庫看第一筆的 createdAt 是幾月幾號,比方說是 2021-02-08 15:48:36 這樣好了,

在進行爬蟲的時候,就去檢查這次爬到的資料內每一筆的 createdAt 有沒有在 2021-02-08 15:48:36 之後,

倘若開始有 2021-02-08 15:48:36 之前的資料出現就剔除,並把其餘符合的資料存入,中斷該看板的爬蟲。

但這樣可能還是會有問題,比方說如果有人同時壓秒發文之類的、或者其他沒考慮到的問題。

我想到的配合方案就是定時跑 TOP10 熱門看板的爬蟲,然後再安排時間跑完整看板文章的爬蟲去補正。

所以就是還需要去研究一下排程的部分,如果想讓這部分的機制更完善的話啦。

但撇開這些來說,我當初想做的事情已經完成了,這邊只是讓它更好而已。

前端部分

如前面所述,是以 Angular + Angular Material + Bootstrap 構建的,而因為玩具性質的關係,這次前端所佔的技術相對少,較著重在爬蟲部分,所以倒也沒遇到太大問題。

Decorator 與 Viewblocker

會想要使用 Decorator 是因為在工作上有使用到,覺得這東西其實蠻方便的,而且很適合用來搭配 Viewblocker 。

Viewblocker 就是進行非同步操作時,會有各式各樣的 loading 畫面來遮住部分視野的東西。

我寫了一支 Viewblock Service 去控制 Viewblocker 的開關。

這部分一開始踩了蠻多的雷,因為 Decorator 算是 TypeScript 提供的語法糖, Angular 的生命週期本身是不會控管的。

而問題就出在這個 Decorator 需要取得 Viewblocker Service 的 Instance 才有辦法進行 Viewblocker 的開啟與關閉控制。

因此我需要在 Viewblocker Service 被 new 出來的時候,把 Instance 透過靜態 (static) 方法給出去。

所以一開始我一直撞牆的原因是 Viewblocker Service 太慢被 new 出來,導致 Decorator 拿不到 Instance ,

於是我把 Viewblocker Service 拉到 AppComponent 內進行依賴注入,這樣就能在 AppComponent 被建立時確保後面用到的 Viewblocker Service 也被建立。

為了 UX 只好再次打掉 Viewblocker

上面一個問題解決之後,很快的又遇到一個地雷。

通常 Viewblocker 不能遮住選單列、導覽列 (Nav bar) ,試想如果今天使用者點到某個頁面 loading 超久,但又被蓋住全螢幕,逼得使用者只能重新整理,這樣 UX 體驗肯定很差。

因此需要確保 Viewblocker 不能遮住選單列、導覽列。

而我就是踩到這個地雷, Viewblocker 元件放置的位置會蓋到手機模式的側選單。

最後是使用 Angular Material 提供的 CDK - portal 把這個問題解掉,因為我把 Viewblocker 變成動態的插入到需要的地方,
這樣就能很好的避開不能被 Viewblocker 蓋到的地方。

可以看到我在 AppLayoutComponentngOnInit 中設置 Viewblocker 要蓋到哪裡。

雛型大概是這個樣子 (還沒串 API)

結尾

呼…總算是寫完了。

當初在準備要寫的時候,就有預感這一篇肯定會寫很久而且篇幅有點長。

畢竟我不是一直都在寫這個玩具,有時候工作比較忙或者倦怠一上來就不想動了。

不過也幸虧自己的這個開發習慣,不管從什麼時候開始都能夠快速掌握目前開發到哪邊。

其實這一篇應該只適合給新手參考,有些程度的工程師大概會覺得這整篇都是廢話 XDDD

不過還是希望能幫助那些想要嘗試寫自己玩具的新手,那怕是一點點心路歷程也好,最終促使這篇文的產生。

因為工作忙碌、回家就犯懶…這個玩具老實說我也還沒有寫完,這篇文出去之後會慢慢的把它完成,說不定也是變相的鞭策自己 (?

一些踩雷心得也會補在閒聊那個區塊。

Live Demo 的話,可能會再等一陣子,大概是等我滿意了才會想租個空間放上去吧 XDD

最後最後,感謝我的好室友在我鬼打牆的時候給我幫助。

有錯的話鞭小力一點,我怕痛 QQ



Gitlab

#六角學院

0%