[Side-Project] Re:從零開始撰寫 巴哈論壇閱讀器 (ReGamer)

realdennis
9 min readDec 28, 2018

--

起因於原本習慣待在 BBS 論壇,但是巴哈的場外版實在太有趣,所以常常也會掛在上面看文章。但我希望能有一點類 BBS 的閱讀器,於是就從零開始撰寫一個符合我需求的 App 。

這篇文章會著重於開發 side-project 常常會遇到的問題,以及我自己的想法,怎麼解決的思路,想跟大家分享。

realdennis / re-gamer

Demo

目錄

  1. 設定目標以及開始
  2. 代理伺服器 & 分析 API pattern
  3. Cross-origin Block
  4. 使用前端路由 React-router
  5. 持續性狀態 Redux-persist
  6. 最後 — 打包 Electron

設定目標以及開始

從使用者的角度來看,我希望能有一個界面單純的閱讀器,但它不能失去巴哈的感覺,卻能帶有 BBS 的好處,所以這會是我的目標。

  • 熱門看板推薦
  • 搜尋看板的功能
  • 不過多載入圖片
  • 文章 GP 值

從開發者的角度來看,我希望能透過一次 side-project 的經驗,練習到 React 以及簡單的狀態管理,以加速未來類似專案的流程與時間(未來有個 boilerplate)

  • 前端路由的分配
  • 狀態管理的嘗試

那我們該如何開始?

網頁版的巴哈哈拉版並非像 Dcard 那樣直接使用 restful API(可以看看我的另一個專案 — realdennis/v-dcard)。

沒有辦法直接透過瀏覽器的 Network panel 分析 API 的 pattern,我的猜測是網頁版使用 php 的 MVC 架構渲染。

這時候我們有兩個可行的方式:

  1. 直接讓使用者發起請求把整個 html 拉下來
  2. 想辦法找出其他使用 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 請求。

  1. mitmproxy 開啟代理 (假設8888 port )
  2. 手機在同個區網下 (代理設定電腦IP 8888 port)
最簡單的方式查看電腦的 IP & 在手機的 WiFi 區手動設定代理

註: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

cross origin block

嘗試在前端串接外部的 API 都會遇到 CORB 的問題,主要是 API 只能同個 origin (同個protocol、domain、port) 才能拿回 response。

這裡有兩種解決方式:

  1. 使用代理伺服器繞過 COR 的限制
  2. 本地請求、不透過瀏覽器過去。

這邊我們把使用 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)。

--

--