去年年末開始寫的一個小項目,斷斷續續作了些優化,在此簡單的記錄一下。javascript
起源是以前一直沒什麼機會接觸到 Node 項目,工做中接觸到的也僅限於用 Node 寫腳本,作一些小工具,與服務器上跑的 Node 服務相差甚遠。因此想寫一個在服務器上跑的 Node 小項目練手。前端
一直喜歡用 RSS 訂閱資訊這種方式,簡單高效,與其天天不定時地接收推送,打開各網站 App 來接收資訊,不如本身拿到主動權集中在同一時間段統一閱讀。這樣避免了天天不定時接受信息的焦慮堆積,可是又經常想不起來打開😅,過了一週打開 Reeder,發現累積的未讀資訊又爆炸了,人真是很難知足。java
因而決定本身搞個資訊推送服務吧,知足本身的核心訴求,每一個工做日早上 10 點微信推送 RSS 前端資訊的更新,這樣就能夠在天天抵達工位的時候舒舒服服瀏覽一下新鮮事,挑一些有用的存起來慢慢研讀。node
項目倉庫: github.com/Colafornia/…ios
推送大概長這樣:git
掃碼獲取推送服務:github
如今推送源主要是各廠的知乎專欄,大佬們的我的博客,掘金前端熱門文章,都是我本身的我的口味。docker
下面來說一下開發(與本身給本身加需求)歷程。數據庫
最開始感受這個需求是很簡單的,具體操做能夠分解爲:npm
最後選擇了 rss-parser 做爲解析工具包,PushBear 做爲推送服務,node-schedule 任務調度工具寫出來了一版。
而後就發現本身知識的匱乏了,沒有考慮到腳本部署到服務器上時,進程守護的問題,因而研習了一波 pm2,完美完成任務。
項目寫到這裏實際上是能夠湊和用了,可是看起來很 low 很難受。主要問題有:
第一點稍微有點複雜,可能如今解決的方案依然很原始。出現第一個問題一是須要控制請求的併發數量,二是 RSS 源自己有必定的不穩定性。目前的解決方案是:
mapLimit
和 timeout
方法設置最大併發數量和超時時間大體代碼以下(有一些細節處理沒貼上來):
// 抓取定時器 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 😅)。
作完以上改動以後,腳本穩定地跑了快半年,這期間我也一直在忙着搬磚,沒什麼精力再來改造它。
一直沒作推廣,但某天忽然發現已經有了三十多個用戶在訂閱這個服務,因而良心發現,本着對用戶負責(也是本身有了新的想練習的技術👻),就又作了一次改造。
此時項目的問題有:
npm install
感受也不太專業,有提高空間(其實就是想用 docker
了)1,2 問題很好解決,每次抓取以前先查一下日誌,上次推送的具體時間。每抓到新文章時,再與最近 7 天日誌裏的文章比對一下,重複的不放到抓取結果中,也就解決了。
對於問題 3,因而決定搭建 Koa Server,先把從 MongoDB 讀取推送源,存取推送日誌變成 api。
目錄結構以下,添加 Model
與 Controller
。把 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"] 複製代碼
用的比較簡單,主要就是負責安裝依賴,啓動服務。須要注意的主要有兩點:
而後去 DaoCloud 等提供公有云服務的網站受權訪問 Github 倉庫,鏈接本身的主機,就能夠實現持續集成,自動構建部署咱們的鏡像了。具體步驟可參考基於 Docker 打造前端持續集成開發環境。
本次優化大概就到這裏了。接下來要作的多是提供一個推送歷史查看頁面,優先級不是很高,有時間再作吧(順便練習一下 Nginx)。
如今的實現方案可能仍是有很不合理的地方,歡迎提出建議。