淺談 nodejs 中間層

nodejs的出現爲前端行業帶來了無限的可能性, 讓很多原來只負責客戶端開發的同學也慢慢開始接觸和使用服務器端技術.

雖然nodejs帶來了很多的好處, 但是它也存在自身的侷限性. 和那些傳統老牌的編程語言相比, 如JAVA,PHP.nodejs並不能成爲它們的替代品, 而且在可預估的未來, 也很難撼動那些老牌編程語言的地位.

目前nodejs主要有以下幾個應用場景.

本文主要講一講nodejs作爲中間層的一些實踐,查看下圖.

傳統的的開發模式由瀏覽器直接和Server層直接通信, 中間層的加入意味着在瀏覽器和Server層之間額外添加了一層.

原來客戶端直接向Server發送請求,Server層收到請求後經過計算處理將結果返回給瀏覽器.

如今瀏覽器將請求發送給node層,node層經過一輪處理後再向Server層發起請求.Server層處理完畢將響應結果返回給node層,node層最後將數據返回給瀏覽器.

因爲node層的出現,Server層可以只用關注業務本身, 而不必理會前端對字段的特殊要求。

node層可以向server層獲取數據, 再通過對數據的計算整合轉換成符合前端UI要求的數據格式. 另外整個應用如果採用微服務架構, 那麼Server層會有很多臺管理單獨業務模塊的服務器,node層就很好的適配了微服務的架構, 它可以向多臺服務器發起請求獲取到不同模塊的數據再整合轉化發送給前端.

下面着重介紹一下nodejs作爲中間層的部分實踐.

代理轉發

代理轉發在實際中有很多廣泛的應用. 瀏覽器首先將請求發送給node服務器, 請求收到後node服務器可以對請求做一些處理, 比如將原來的路徑變換一下, 請求頭的信息改變一下, 再把修改後的請求發送給遠程真實的服務器.

遠程服務器計算出響應結果再返回給node服務器,node服務器仍然可以對響應做選擇性處理再分返回給瀏覽器.

代理轉發可以解決前端日常開發中經常遇到的跨域問題, 另外它還屏蔽了遠程真實服務器的細節, 讓瀏覽器只與node服務器通信. 下面是簡單的實踐.

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();//創建應用
 
app.use("/api",createProxyMiddleware( //設置代理轉發
  { 
     target: 'http://www.xxx.com', //舉例隨便寫的地址
     changeOrigin: true,
     pathRewrite: function (path) { 
       return path.replace('/api''/server/api');
     }
  })
);

app.use("*",(req,res)=>{  //不是以'/api'開頭的路由全部返回"hello world"
  res.send("hello world");
})

app.listen(3000);

http-proxy-middleware是一個第三方依賴包, 可以非常方便設置代理轉發, 需要通過npm安裝.

如果當前訪問的路徑是以/api開頭, 那麼該請求就會被http-proxy-middleware攔截. 觀察http-proxy-middleware裏面配置的參數.

上面的案例意思很明顯, 假如當前瀏覽器訪問http://localhost:3000/api/list. 因爲這個路徑以/api開頭所以會被攔截, 從而觸發pathRewrite函數修改訪問路徑. 最終訪問路徑就變成了http://www.xxx.com/server/api/list, 然後就會向這個路徑發起請求, 得到響應後再返回給瀏覽器.

接口聚合

上面介紹的接口轉發在實踐中很少會單獨應用, 如果僅僅只是爲了轉發一下數據, 那還不如直接用nginx配置一下,轉發就搞定了.

如果接口聚合和接口轉發都需要, 那麼從代碼層面去解決還是優先考慮的方式.

接口聚合是什麼意思呢? 假設現在企業有兩個銷售體系, 一個是線上的電商平臺銷售, 另一個是線下實體店. 它們分別屬於不同的團隊運營, 維護着不同的數據系統.

如果當前請求只是想查詢一下電商平臺某款商品的信息, 只需要將接口轉發給電商平臺系統即可. 同理如果僅僅只是查詢線下實體店某一天的銷售業績, 可以直接把請求轉發給線下數據系統查詢, 再把響應數據返回. 上面介紹的插件http-proxy-middleware支持配置多個代理路徑, 詳細可查詢文檔.

現在有這麼一個需求, 目標是查詢本週某款商品在線上和線下銷售數據的對比. 那麼這個時候就需要node層向兩個遠程服務器發送請求分別獲取線上銷售數據和線下銷售數據, 將這兩部分數據聚合處理後再返回給前端. 簡單實踐如下.

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();//創建應用

//僞代碼
app.get("/getSaleInfo",async (req,res)=>{ 
   const online_data =  await getOnline(); //獲取線上數據
   const offline_data = await getOffline(); //獲取線下數據
   res.send(dataHanlder(online_data,offline_data)); //對數據處理後返回給前端
})

proxyHanlder(app);//僞代碼,將代理轉發的邏輯封裝起來

app.use("*",(req,res)=>{
  res.send("hello world");
})

app.listen(3000);

/getSaleInfo代表着將兩條數據聚合的自定義路由, 如果需要聚合數據的需求比較多, 這塊邏輯要單獨封裝到路由模塊中管理, 並且要寫在代理轉發的前面.

這樣就確保了需要轉發的接口就交給轉發的邏輯處理, 需要個性化處理數據的接口就單獨編寫路由操作數據.

數據緩存

緩存對於提升系統性能, 減小數據庫壓力起到了無足輕重的作用. 一般常用的緩存軟件是redis, 它可以被理解成數據存儲在內存當中的數據庫. 由於數據放在內存中, 讀寫速度非常快, 能極快的響應用戶的請求.

node層部署redis管理緩存數據, 可以提升整體應用性能. 但不是什麼數據都建議存放在redis中, 只有那些不經常變動的數據應該設置成緩存.

比如商品的信息數據, 瀏覽器對某個商品發起請求, 想查看該商品的詳情. 請求第一次到達node層,redis此時是空的. 那麼node開始請求server層得到響應結果, 此時在將響應結果返回給瀏覽器之前, 將該次請求的訪問路徑作爲key值, 響應結果作爲value存儲到redis中. 這樣之後再有相同的請求發來時, 先查看redis有沒有緩存該請求的數據, 如果緩存了直接將數據返回, 如果沒有緩存再去請求server層, 把上述流程再走一遍.

redis還可以對緩存數據設置過期時間和清除, 可以根據具體的業務操作. 簡單實踐如下.

const express = require('express');

const app = express();//創建應用

//僞代碼
app.use("*",(req,res,next)=>{
   const path = req.originalUrl; //獲取訪問路徑
   if(redisClient.getItem(path)){ //查看redis中有沒有緩存該條接口的數據
     res.send(redisClient.getItem(path)); // 返回緩存數據
   }else{
     next(); //不執行任何操作,直接放行  
   }
})


aggregate(app); //僞代碼,將接口聚合的邏輯封裝起來

proxyHanlder(app);//僞代碼,將代理轉發的邏輯封裝起來

app.use("*",(req,res)=>{
  res.send("hello world");
})

app.listen(3000);

接口限流

node做中間層可以對前端無節制的訪問做限制. 比如有些惡意的腳本循環訪問接口, 一秒鐘訪問幾十次增大了服務器的負載.

redis可以幫助我們實現這一功能. 用戶第一次訪問, 解析出本次請求的ip地址, 將ip作爲key值,value置爲0存到redis中.

用戶第二次訪問, 取出ip找到redis中對應的value, 然後自增1. 如果是相同的人重複大量訪問,value在短期內就自增到了很大的數字, 我們可以每次獲取這個數字判端是否超過了設定的預期標準, 超過則拒絕本次請求. 簡單實踐如下.

const express = require('express');

const app = express();//創建應用

//僞代碼
app.use("*",(req,res,next)=>{

  const ip = req.ip;

  let num = 0;

  if(redisClient.getItem(ip)){ //是否緩存了當前的ip字段
    num = redisClient.incr(ip); //每訪問一下,計數加1
  }else{
    redisClient.setItem(ip,0);
    redisClient.setExpireTime(5); //設置過期時間爲5秒,5秒後再獲取該ip爲空
  }

  if(num > 20){ 
    res.send("非法訪問");
  }else{
    next();//放行
  }

})

cacheData(app)//僞代碼.緩存接口數據

aggregate(app); //僞代碼,將接口聚合的邏輯封裝起來

proxyHanlder(app);//僞代碼,將代理轉發的邏輯封裝起來

app.use("*",(req,res)=>{
  res.send("hello world");
})

app.listen(3000);

在應用的前面設置一層限流中間件, 每次訪問來臨先判端是否緩存過. 第一次訪問肯定沒有緩存, 就將當前ip對應的值設置爲0並添加過期時間爲5秒鐘. 下一次相同的用戶再訪問時就會將value自增1.

最後的效果就達到了5秒內調用接口的次數超過20次便拒絕訪問.

日誌操作

系統沒有日誌, 相當於人沒有雙眼. 日誌可以幫助我們發現分析定位線上系統出現的錯誤. 另外通過日誌數據也可以進行統計計算得出某些結論和趨勢.

node層能夠承擔起管理日誌的功能, 以接口訪問日誌爲例. 在系統中新建一個日誌文件夾, 每次有請求訪問時, 首先解析請求的路徑、當前的訪問時間以及攜帶的參數和終端數據信息. 然後在日誌文件夾創建一個txt文件存放當天日誌情況, 將上述數據和該請求的響應結果組合成一條記錄插入txt文件中. 下一次訪問繼續走上面流程往txt文件添加訪問日誌. 像上面介紹的代理轉發, 插件http-proxy-middleware支持配置如何返回響應結果, 那麼在相應的事件函數鉤子裏就可以同時得到請求和響應, 有了這兩塊數據就可以存放到日誌中.

這裏還能制定很多的配置策略. 可以選擇一天一個日誌文本, 如果訪問量巨大也可以選擇一個小時一個日誌文本, 依據實際情況而定.

另外隨着時間的延長, 日誌文件夾的文件內容會越來越多. 這就需要編寫linux操作系統定時任務來遷移和備份這些日誌數據.

日誌操作簡單實踐如下.

//僞代碼
app.use("/getList",async (req,res)=>{
  const list = await getProductList(); //獲取商品數據
  const { 訪問時間,訪問路徑,參數 } = req;
  logger.log('info',`${訪問時間}-${訪問路徑和參數}:${list}`);//將數據存儲到日誌文件中 
  res.send(list);//將結果返回給客戶端
})

結尾

中間層另外還可以做很多其他事情, 比如監控、鑑權和服務器端渲染 (ssr). 這部分由於內容比較多可以單獨成章, 網絡上也有大量如何實踐的文章, 可搜索查閱學習.

其實上面所談到的所有功能其他編程語言都可以做到, 這也成爲了很多人質疑是否需要在架構上額外再加一層的顧慮.

添加nodejs中間層, 對於前端同學來說肯定是好消息. 因爲它能讓前端承擔更多的工作任務, 讓前端的業務比重變大. 另外後端從此只需要關注自身業務, 前端繼續幹着自己擅長的事, 從整體上是能提升開發效率.

但從宏觀角度上看, 架構額外增加一層勢必會造成整個應用性能上的損耗, 另外在部署, 測試層面都會增大運維成本.

當下前後端分離已經成爲了主流的開發模式, 很多類型的應用需要 seo 的支持以及首屏加載速度, 因此服務器端渲染不可或缺. 前端項目目前大多采用reactvue框架開發, 如果用nodejs承擔服務器端渲染的任務, 那麼可以確保一套代碼既可以做客戶端渲染也能支持服務器端渲染, 而這些工作都可以讓前端程序員獨立來完成. 服務器端渲染技術非常重要, 後面會開一個小節單獨講解.

綜上來看,nodejs做中間層最有價值的功能是服務器端渲染和接口數據聚合. 如果企業應用數量較少業務簡單還沒有規模化, 不建議添加中間層, 那樣反而讓簡單的事情變得複雜.

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Dv6bENmNin7_TPwayFX0qQ