生命在於折騰,寫一個前端資訊推送服務

去年年末開始寫的一個小項目,斷斷續續作了些優化,在此簡單的記錄一下。javascript

源頭

起源是以前一直沒什麼機會接觸到 Node 項目,工做中接觸到的也僅限於用 Node 寫腳本,作一些小工具,與服務器上跑的 Node 服務相差甚遠。因此想寫一個在服務器上跑的 Node 小項目練手。前端

一直喜歡用 RSS 訂閱資訊這種方式,簡單高效,與其天天不定時地接收推送,打開各網站 App 來接收資訊,不如本身拿到主動權集中在同一時間段統一閱讀。這樣避免了天天不定時接受信息的焦慮堆積,可是又經常想不起來打開😅,過了一週打開 Reeder,發現累積的未讀資訊又爆炸了,人真是很難知足。java

因而決定本身搞個資訊推送服務吧,知足本身的核心訴求,每一個工做日早上 10 點微信推送 RSS 前端資訊的更新,這樣就能夠在天天抵達工位的時候舒舒服服瀏覽一下新鮮事,挑一些有用的存起來慢慢研讀。node

項目倉庫: github.com/Colafornia/…ios

推送大概長這樣:git

掃碼獲取推送服務:github

如今推送源主要是各廠的知乎專欄,大佬們的我的博客,掘金前端熱門文章,都是我本身的我的口味。docker

下面來說一下開發(與本身給本身加需求)歷程。數據庫

開始

最開始感受這個需求是很簡單的,具體操做能夠分解爲:npm

  1. 寫一個配置文件,把我想抓取的 RSS 源地址寫在裏面
  2. 找一個能解析 RSS 的 npm 包,遍歷配置文件裏的源,解析以後處理數據
  3. 僅篩出在過去 24 小時內更新的文章,把數據處理一下,彙總成一段字符串,用微信推送
  4. 以上寫出的腳本經過定時任務跑起來,done!

最後選擇了 rss-parser 做爲解析工具包,PushBear 做爲推送服務,node-schedule 任務調度工具寫出來了一版。

而後就發現本身知識的匱乏了,沒有考慮到腳本部署到服務器上時,進程守護的問題,因而研習了一波 pm2,完美完成任務。

過渡

項目寫到這裏實際上是能夠湊和用了,可是看起來很 low 很難受。主要問題有:

  1. 當時 RSS 源大概有四五十個,一次性遍歷解析全部的源常常會有超時或者出錯的
  2. RSS 源寫在配置文件裏,每次想添加、修改源都須要改代碼,很 low
  3. PushBear 這個推送服務只能存儲三天內的推送,三天前,一週前的推送內容都看不了,這也很難受
  4. 掘金的 RSS 源內容很少,也不是按照熱門程度排序的(也多是我姿式不對😅),不太符合要求

第一點稍微有點複雜,可能如今解決的方案依然很原始。出現第一個問題一是須要控制請求的併發數量,二是 RSS 源自己有必定的不穩定性。目前的解決方案是:

  1. 把抓取任務和推送任務分開,預留出能夠循環抓取三次的時間,後面兩次只抓取以前失敗的源
  2. asyncmapLimittimeout 方法設置最大併發數量和超時時間

大體代碼以下(有一些細節處理沒貼上來):

// 抓取定時器 ID
let fetchInterval = null;
// 抓取次數
let fetchTimes = 0;
function setPushSchedule () {
    schedule.scheduleJob('00 30 09 * * *', () => {
        // 抓取任務
        log.info('rss schedule fetching fire at ' + new Date());
        activateFetchTask();
    });

    schedule.scheduleJob('00 00 10 * * *', () => {
        // 發送任務
        log.info('rss schedule delivery fire at ' + new Date());
        let message = makeUpMessage();
        log.info(message);
        sendToWeChat(message);
    });
}
function activateFetchTask() {
  fetchInterval = setInterval(fetchRSSUpdate, 120000);
  fetchRSSUpdate();
}
function fetchRSSUpdate() {
    fetchTimes++;
    if (toFetchList.length && fetchTimes < 4) {
        // 若抓取次數少於三次,且仍存在未成功抓取的源
        log.info(`第${fetchTimes}次抓取,有 ${toFetchList.length} 篇`);
        // 最大併發數爲15,超時時間設置爲 8000ms
        return mapLimit(toFetchList, 15, (source, callback) => {
            timeout(parseRSS(source, callback), 8000);
        })
    }
    log.info('fetching is done');
    clearInterval(fetchInterval);
    return fetchDataCb();
}
複製代碼

這樣基本解決了 90% 以上的抓取問題,保證了腳本的穩定性。

針對 RSS 源寫在配置文件裏,每次想添加、修改源都須要改代碼的問題,解決方法很簡單,把源配置寫到 MongoDB 裏也就行了,有一些 GUI 軟件能夠直接在圖形界面來添加、修改數據。

爲了解決推送服務只能存儲三天內的推送,決定新增一個每週五的周抓取任務,抓取一週內的新文章,把內容做爲 issue 發到倉庫。也還算是一個解決方案。

針對掘金的 RSS 源問題,最後決定直接調用掘金的接口來取數據,這就能夠爲所欲爲按本身的需求來了,天天只抓取❤️點贊數在 70 以上的文章。

順便給抓取的文章時間範圍加了一個偏移值,避免篩掉質量好可是因爲剛剛發佈點贊較少的文章。感受本身棒棒噠~

function filterArticlesByDateAndCollection () {
    const threshold = 70;
    // articles 是已按❤️數由高到低排序的文章列表
    let results = articles.filter((article) => {
        // 偏移值五小時,避免篩掉質量好可是因爲剛剛發佈點贊較少的文章
        return moment(article.createdAt).isAfter(moment(startTime).subtract(5, 'hours'))
            && moment(article.createdAt).isBefore(moment(endTime).subtract(5, 'hours'))
            && article.collectionCount > threshold;
    });
    // 掘金文章最多收錄 8 篇,避免信息爆炸
    return results.slice(0, 8);
}
複製代碼

在這個期間也充分感覺到了日誌的重要性,在數據庫裏新增了一個表用來存天天的推送內容。

另外在 PushBear 上新添加了一個 Channel 來給本身推送日誌,天天在抓取任務完成後,先給我發送一下抓取到的內容,若是發現有任何問題,我能夠本身登服務器緊急修復一下(這麼想來仍是很 low 😅)。

升級

作完以上改動以後,腳本穩定地跑了快半年,這期間我也一直在忙着搬磚,沒什麼精力再來改造它。

一直沒作推廣,但某天忽然發現已經有了三十多個用戶在訂閱這個服務,因而良心發現,本着對用戶負責(也是本身有了新的想練習的技術👻),就又作了一次改造。

此時項目的問題有:

  1. 沒有文章去重,若是文章在知乎專欄發了,掘金也發了,做者我的博客也發了的話,就至關於會重複出現幾回
  2. 推送的時間間隔不精確,都是當前時間的過去 24 小時來篩的
  3. 腳本直連數據庫進行存取操做也不太好,感受這個形式作成 server,對外暴露 api 更合理(等哪天想寫個 RSS 閱讀器也就用上了)
  4. 每次代碼有更新,依賴有更新,都 ssh 上服務器而後 npm install 感受也不太專業,有提高空間(其實就是想用 docker 了)

1,2 問題很好解決,每次抓取以前先查一下日誌,上次推送的具體時間。每抓到新文章時,再與最近 7 天日誌裏的文章比對一下,重複的不放到抓取結果中,也就解決了。

對於問題 3,因而決定搭建 Koa Server,先把從 MongoDB 讀取推送源,存取推送日誌變成 api。

目錄結構以下,添加 ModelController。把 RSS 抓取腳本與掘金爬蟲放到 task 文件。

沒什麼難點,就能夠調用 api 來獲取 RSS 源了:

此時想到了一個重要問題,身份驗證。確定不能把全部 api 都隨意暴露出去,讓外界能夠任意調用,這也就至關於把數據庫都暴露出去了。

最終決定用 JSON Web Token(縮寫 JWT) 做爲認證方案,主要緣由是 JWT 適合一次性、短期的命令認證,目前個人服務僅限於服務器端的 api 調用,天天的使用時間也不長,無需簽發有效期很長的令牌。

Koa 有一個 jwt 的中間件

// index.js
app.use(jwtKoa({ secret: config.secretKey }).unless({
    path: [/^\/api\/source/, /^\/api\/login/]
}))
複製代碼

加上中間件後,除了 /api/source/api/login 接口就都須要通過 jwt 認證才能訪問了。

所以寫了一個 /api/login 接口,用於簽發令牌,拿到令牌以後,把令牌設置到請求頭裏就能夠經過認證了:

// api/base.js
// 用於封裝 axios
// http request 攔截器
import axios from 'axios';
const config = require('../config');
const Instance = axios.create({
    baseURL: `http://localhost:${config.port}/api`,
    timeout: 3000,
    headers: {
        post: {
            'Content-Type': 'application/json',
        }
    }
});
Instance.interceptors.request.use(
    (config) => {
        // jwt 驗證
        const token = config.token;
        if (token) {
            config.headers['Authorization'] = `Bearer ${token}`
        }
        return config;
    },
    error => {
        return Promise.reject(error);
    }
);
複製代碼

若是請求頭裏沒有正確的 token,則會返回 Authentication Error

至於問題 4,如今服務比較簡單,也只在一個機器上部署,手動登機器 npm install 問題還不大,若是機器不少,依賴項也複雜的話,很容易出問題,具體參見科普文:爲何不能在服務器上 npm install ?

因而決定基於 Docker 作構建部署。

FROM daocloud.io/node:8.4.0-onbuild
COPY package*.json ./ RUN npm install -g cnpm --registry=https://registry.npm.taobao.org RUN cnpm install RUN echo "Asia/Shanghai" > /etc/timezone RUN dpkg-reconfigure -f noninteractive tzdata COPY . . EXPOSE 3001
CMD [ "npm", "start", "$value1", "$value2", "$value3"] 複製代碼

用的比較簡單,主要就是負責安裝依賴,啓動服務。須要注意的主要有兩點:

  1. 國內拉去外網的鏡像很慢,像 Node 官方的鏡像我都拉了很久都沒拉下來,這樣的話推薦使用國內的鏡像,好比我用的 DaoCloud,還有阿里雲鏡像等等
  2. 因爲推送服務是對時間敏感的,基礎鏡像的時區並非國內時區,要手動設置一下

而後去 DaoCloud 等提供公有云服務的網站受權訪問 Github 倉庫,鏈接本身的主機,就能夠實現持續集成,自動構建部署咱們的鏡像了。具體步驟可參考基於 Docker 打造前端持續集成開發環境

daocloud

本次優化大概就到這裏了。接下來要作的多是提供一個推送歷史查看頁面,優先級不是很高,有時間再作吧(順便練習一下 Nginx)。

如今的實現方案可能仍是有很不合理的地方,歡迎提出建議。

相關文章
相關標籤/搜索