[Side-Project] Re:從零開始撰寫 巴哈論壇閱讀器 (ReGamer)
起因於原本習慣待在 BBS 論壇,但是巴哈的場外版實在太有趣,所以常常也會掛在上面看文章。但我希望能有一點類 BBS 的閱讀器,於是就從零開始撰寫一個符合我需求的 App 。
這篇文章會著重於開發 side-project 常常會遇到的問題,以及我自己的想法,怎麼解決的思路,想跟大家分享。
Demo
目錄
- 設定目標以及開始
- 代理伺服器 & 分析 API pattern
- Cross-origin Block
- 使用前端路由 React-router
- 持續性狀態 Redux-persist
- 最後 — 打包 Electron
設定目標以及開始
從使用者的角度來看,我希望能有一個界面單純的閱讀器,但它不能失去巴哈的感覺,卻能帶有 BBS 的好處,所以這會是我的目標。
- 熱門看板推薦
- 搜尋看板的功能
- 不過多載入圖片
- 文章 GP 值
從開發者的角度來看,我希望能透過一次 side-project 的經驗,練習到 React 以及簡單的狀態管理,以加速未來類似專案的流程與時間(未來有個 boilerplate)
- 前端路由的分配
- 狀態管理的嘗試
那我們該如何開始?
網頁版的巴哈哈拉版並非像 Dcard 那樣直接使用 restful API(可以看看我的另一個專案 — realdennis/v-dcard)。
沒有辦法直接透過瀏覽器的 Network panel 分析 API 的 pattern,我的猜測是網頁版使用 php 的 MVC 架構渲染。
這時候我們有兩個可行的方式:
- 直接讓使用者發起請求把整個 html 拉下來
- 想辦法找出其他使用 API 的地方
(1.) 的做法未嘗不行,就是變成像是爬蟲再將資料可視化,需要 html parser 的幫助把重要資料抽取,如果在前端的話可以透過 Browser API ,但仍然需要 data cleaner 的邏輯幫助。
可以參考這篇 — [Crawler] 使用Puppeteer爬取PTT的網頁
這次我們選擇的是方法 (2.) ,在這個智慧型手機橫行的年代,我們的對象目標如果有做成官方的 App ,他們的開發者肯定也會仰賴於 API 的呼叫,我們來看看巴哈的 App 吧。
巴哈論壇 App & API 分析
我們來看看 App 提供的功能有:
- 熱門看板
- 搜尋看板
- 版內文章
- 文章內容
很幸運的是,上述全部都是 無限滾動瀑布流 ,這意味著手機 App 的開發者可能擁有一個不錯的 API 可以做到這件事。
那麼我們來撈取資料吧!因為是在於手機的 App 上,所以我們需要在電腦上假設一個代理伺服器,在電腦上查看一下到底呼叫了什麼鬼。
這裡使用的是 mitmproxy ,而且它是用 Python 開發的(不瞞各位說,我也是隱性的 Py 教徒)。
只是個工具,我就長話短說,這次的重點不在拆解封包,而是簡單觀察 API pattern,我猜肯定是滿滿的 GET 請求。
- mitmproxy 開啟代理 (假設8888 port )
- 手機在同個區網下 (代理設定電腦IP 8888 port)
註:iPhone 有時候代理失效的話,可以先進入飛航模式再出來。
來看看 API 長什麼樣子吧
- filter 設定 api.gamer.com
- 觀察 query string
- 查看回傳的 json 格式
我們發現 API 透過幾個不同的 php 所回傳:
熱門看板 → v1/hot_board.php
搜尋看板 → v1/search_board.php
看板文章 → v1/B.php
文章內容 → v3/C.php
至於 v1 / v3 應該為內部開發的版本更迭。
註:page都是從1開始、bsn為看板號、snA為文章號、index為20一個區間
Cross Origin Block
嘗試在前端串接外部的 API 都會遇到 CORB 的問題,主要是 API 只能同個 origin (同個protocol、domain、port) 才能拿回 response。
這裡有兩種解決方式:
- 使用代理伺服器繞過 COR 的限制
- 本地請求、不透過瀏覽器過去。
這邊我們把使用 Electron 包裹的 desktop App 以及 Cordova 包裹的 mobile app,在 Electron 裡頭的 fetch 請求就可以不帶瀏覽器請求頭。
這裡使用 Electron v4。
這裡有 Electron 的 boilerplate,可以直接 clone 下來玩玩看。 — electron/electron-quick-start
前端路由 — React router
為什麼我們需要前端路由?
答:我們的 App 需要有幾個視圖,分別是熱門、搜尋、我的最愛、看板內、文章。在單頁面程式 (single page application) 中,我們需要透過 router 的幫助,讓我們在對應的路由狀態,掛起對應的組件。
最簡單的結構如下:
<App>
<Router>
<Route 組件=熱門看板>
<Route 組件=搜尋看板>
<Route 組件=我的最愛>
<Route 組件=看板內部>
<Route 組件=文章內部>
因為這次的 Project 主要想練習 React ,使用採用的是 react-router-dom,如果你想透過 Vue 開發,當然也可以使用 vue-router。
我們設定路由位置以及對應的組件。
將對應的資料映射到每個組件的功能
以下拿看板內文章作為舉例:
{
"bsn": 60076,
"snA": 4549786,
"top": 8,
"renum": 67,
"gp": 33,
"ctime": "12-23 22:32",
"del": 0,
"mindflag": 0,
"daren": 0,
"reply_timestamp": 1545575543,
"locked": 0,
"dreason": "",
"title": "【公告】場外精華好文推薦區《誠徵精華組員》",
"thumbnail": "",
"summary": "大家好唷!我是糖果,是負責精華區的所有相關事宜,從第三屆小組開始,精華區將由我跟精華小組的組員一同為大家服務。場外文章流"
},
回傳的內容很詳細,包括文章的看板號、文章號、gp 值、發文時間等等。為了簡化 App 界面,渲染的部分我們只渲染 gp、文章標題,組件範例如下。Link 為 react-router 的跳轉元素,最後會渲染為<a>。
<Link to={{pathname: `/article/${article.bsn}/${article.snA}`}} > <p className="gp" style={{color:numToColor(article.gp)}}>{article.gp}</p> <p className="title">{article.title}</p>
持續性狀態 — 我的最愛
如果使用者把看板加入我的最愛,這意味著使用者希望下次打開時,可以透過我的最愛的快捷進入對應看板。可是前端頁面的狀態一般來說在重新整理時全部都會被初始化,我們希望能將狀態保存。
最直觀的做法是把資訊存入瀏覽器提供的 localstorage Web API,這邊我們透過 redux-persist 把我們的 redux 狀態存入 localStorage。
如果不使用 redux 或 vuex 來保存組件狀態的話,可以直接操作 localstorage ,亦有針對此 API 封裝的庫,比如 vue-ls 之類的,可以查看這個直接操作localstoage的範例 — [分享] Tarobot — 塔羅牌 & 線上抽牌。
最後 — 打包成跨平台應用
這裡我們使用 electron-builder
,打包為 Windows & MacOS 可用的執行檔案。
將我們的 React 專案 build 起來後,透過配置 Electron 文件 (main.js),告訴 Electron 要怎麼呈現程式,比如長寬高、frame是否顯示。
通常來說,如果你的在開發環境可能為了 hot reload ,而 Electron 透過 localhost:port
連過去的話,你得先把 CORB 的限制關掉,最後打包成靜態文件後,則希望你把 CORB 打開,此時你的主程式在 index.html 的跨站請求不會被擋住。
這是為了程式的安全,如果你的程式會連到外部頁面,你不會希望把用戶暴露在危險環境下。
尾聲
這類的 side project 可以練習簡單的框架操作,比如路由或是狀態管理。這個程式沒有任何廣告啦,趕快下載下來玩玩看吧。
另外程式本身大小並不大,但是包裹 Electron 的硬傷,就是會突然肥大起來(約莫40–50mb),但這種 solution 可以一次性開發多端應用,Electron 可以打包桌面端(Mac、Windows、Linux),Cordova則能包裹移動端(Android、iOS)。