本文干貨滿滿,介紹了用BFF層(Back-end For Front-end)中間層提升性能的整體解決方案和思路,涉及前期技術調研,聚合業務分析,聚合方法,驗收,最后向同學們普及node、koa基礎知識,以及如何通過中間層做優化等一系列的內容,形成 發起問題——解決問題——復盤學習推廣 的完整閉環。
業務介紹

業務背景
我們負責的系統,既有內部各細分領域的功能分支,也有大量的功能交集,展示和交互要求極高。
- 后端微服務化,使得數據更加碎片化,請求數量增加,創建更多連接,且數據包大,傳輸耗費帶寬。
- 微前端技術的應用,使前端文件也變得碎片化。
最終導致:
- 頁面有效內容的展示發生延遲。
- 並且功能交互產生卡頓。
通過以下截圖,可以看到,一個頁面的請求數可輕松超過100個,其中接口類的請求有30個。

中間層目標
減少請求數量,以及縮小數據包體積,可以有效提升用戶體驗。
想要減少請求,可以建設BFF層。
通過建設BFF層,提升用戶體驗:
- 提升加載性能,更快地呈現核心內容。
- 提升交互的流暢度,更快地響應用戶操作。
驗收目標:聚合后,頁面性能,TTI提升10%
中間層在架構中的位置:(server層—web server層)

我在此次迭代中做的工作:
- node,koa技術調研
- 用koa框架搭建web server中間層
- 系統需要聚合業務和接口調研,以及文檔整理
- 出具所有聚合接口方案
- 分配到人進行接口聚合
- 配合后端和運維,域名轉發處理
- 性能監控,對比,與測試配合驗收

聚合接口注意事項:
接口聚合在開發時要注意的原則:
a. 盡量聚合同一視圖層級的請求
b. 在邏輯有相關性,參數相同的接口,比較適合做聚合
c. 不要把過慢的接口和其他正常接口放在一起,會影響整體性能
最終效果,通過我們的努力,BFF層聚合接口以后,請求時間和接口數降低,達到了預期的目標。
原理是,node采用事件驅動、異步編程,為網絡服務而設計。Node.js非阻塞模式的IO處理給Node.js帶來在相對低系統資源耗用下的高性能與出眾的負載能力,非常適合用作依賴其它IO資源的中間層服務。用戶,特別是電腦配置特別低的用戶,通過這波優化,他們體驗會更好(cpu資源降低,請求時長縮小),用服務端資源換用戶的時間和空間,最終提升了用戶體驗。
在此次項目完成以后,進行了前端分享,本次分享的目的:
- 讓大家對node不再陌生,了解bff層構成
- 能上手利用node,koa參與一些性能優化,
- 提升個人的技術能力和技術視野
nodeJS介紹

前言
Node.js 是 JavaScript 后端開發語言。從誕生之初就備受關注,到如今說到最火的后端 Web 開發,Node 說自己是第二,沒有人敢說他是第一。
前端有必要進行技術賦能,使用node對業務和團隊進行提升,包括以下幾個方面的提升:
1、 一切為了用戶——提升用戶體驗
2、職責明確——提高開發團隊生產力
3、團隊賦能——提高前端團隊的想象和成長空間
以下內容,是對node介紹。
一、概念
Node.js 是一個開源與跨平台的 JavaScript 運行時環境。
在瀏覽器外運行 V8 JavaScript 引擎(Google Chrome 的內核),利用事件驅動、非阻塞和異步輸入輸出模型等技術提高性能。
可以理解為 Node.js 就是一個服務器端的、非阻塞式I/O的、事件驅動的JavaScript運行環境。
非阻塞異步
Nodejs采用了非阻塞型I/O機制(基本原理結合服務器演進史,詳細看思維導圖),在做I/O操作的時候不會造成任何的阻塞,當完成之后,以時間的形式通知執行操作。
例如在執行了訪問數據庫的代碼之后,將立即轉而執行其后面的代碼,把數據庫返回結果的處理代碼放在回調函數中,從而提高了程序的執行效率。
事件驅動
事件驅動就是當進來一個新的請求的時,請求將會被壓入一個事件隊列中,然后通過一個循環來檢測隊列中的事件狀態變化,如果檢測到有狀態變化的事件,那么就執行該事件對應的處理代碼,一般都是回調函數。
比如讀取一個文件,文件讀取完畢后,就會觸發對應的狀態,然后通過對應的回調函數來進行處理。
能做什么?
1、提供數據給瀏覽器展示
2、保存用戶提交過來的數據
3、數據統計與分析
服務器Node.js和瀏覽器js的區別是什么?
1、node.js是平台,JavaScript是編程語言;
2、javascript是客戶端編程語言,需要瀏覽器的javascript解釋器進行解釋執行;
3、node.js是一個基於Chrome JavaScript運行時建立的平台,它是對Google V8引擎進行了封裝的運行環境;
4、node.js就是把瀏覽器的解釋器封裝起來作為服務器運行平台,用類似javascript的結構語法進行編程,在node.js上運行。
二、優缺點
優點:
1、采用事件驅動、異步編程,為網絡服務而設計。其實Javascript的匿名函數和閉包特性非常適合事件驅動、異步編程。而且JavaScript也簡單易學,很多前端設計人員可以很快上手做后端設計。
2、Node.js非阻塞模式的IO處理給Node.js帶來在相對低系統資源耗用下的高性能與出眾的負載能力,非常適合用作依賴其它IO資源的中間層服務。
3、Node.js輕量高效,可以認為是數據密集型分布式部署環境下的實時應用系統的完美解決方案。Node非常適合如下情況:在響應客戶端之前,您預計可能有很高的流量,但所需的服務器端邏輯和處理不一定很多。
4、前后端語言互通。
缺點:
因為Nodejs是單線程,帶來的缺點有:
-
不適合CPU密集型應用
-
只支持單核CPU,不能充分利用CPU
-
可靠性低,一旦代碼某個環節崩潰,整個系統都崩潰
三、應用場景
借助Nodejs的特點和弊端,其應用場景分類如下:
-
善於I/O,不善於計算。因為Nodejs是一個單線程,如果計算(同步)太多,則會阻塞這個線程
-
大量並發的I/O,應用程序內部並不需要進行非常復雜的處理
-
與 websocket 配合,開發長連接的實時交互應用程序
具體場景可以表現為如下:
-
第一大類:用戶表單收集系統、后台管理系統、實時交互系統、考試系統、聯網軟件、高並發量的web應用程序
-
第二大類:基於web、canvas等多人聯網游戲
-
第三大類:基於web的多人實時聊天客戶端、聊天室、圖文直播
-
第五大類:操作數據庫、為前端和移動端提供基於json的API
其實,Nodejs能實現幾乎一切的應用,只考慮適不適合使用它。
以上。
四、延伸
以上內容,僅僅是node.js的一點皮毛,下面推薦部分內容,有興趣的同學可以進行深入了解和探究實踐。
KOA介紹
Koa 是由 Express 原班人馬打造的,致力於成為一個更小、更富有表現力、更健壯的 Web 框架。使用 koa 編寫 web 應用,通過組合不同的 generator,可以免除重復繁瑣的回調函數嵌套,並極大地提升錯誤處理的效率。koa 不在內核方法中綁定任何中間件,它僅僅提供了一個輕量優雅的函數庫,使得編寫 Web 應用變得得心應手。
Koa 是一種簡單好用的 Web 框架,node可以在ssr服務端渲染,bff層,接口聚合,削減api,或處理api數據等方面應用,減小前端代碼復雜度,為企業節省成本,讓吞吐率更高。
二、KOA代碼結構
koa 非常小巧,總共就 4 個文件,每個文件的功能也十分單一,文件名也清楚的反應了文件功能。
├── application.js
├── context.js
├── request.js
└── response.js
復制代碼
-
request.js
主要針對 http 的 request 對象提供了改對象的大量的 get 方法,文件主要是用來獲取 request 對象屬性。
-
response.js
主要針對 http 的 response 對象提供了該對象的大量 set 方法;該文件主要是用來設置 response 對象屬性。
-
context.js
koa 引入了上下文對象的概念,即 ctx,這里所謂的上下文對象實際上是 request 和 response 兩個對象的並集,request 和 response 分別通過代理的形式,將自己的方法委托給 ctx。那樣我們就可以用 ctx 同時操作兩個對象,來簡化操作。
-
application.js
該文件是整個 koa 的核心,簡單來說主要有兩大功能: 掛載真實請求到 ctx 下,封裝中間件的執行順序
三、KOA和express的區別
於是二者的使用區別通過表格展示如下:
|
|
koa(Router = require('koa-router'))
|
express(假設不使用app.get之類的方法)
|
|---|---|---|
|
|
koa(Router = require('koa-router'))
|
express(假設不使用app.get之類的方法)
|
| 初始化 | const app = new koa() | const app = express() |
| 實例化路由 | const router = Router() | const router = express.Router() |
| app級別的中間件 | app.use | app.use |
| 路由級別的中間件 | router.get | router.get |
| 路由中間件掛載 | app.use(router.routes()) | app.use('/', router) |
| 監聽端口 | app.listen(3000) | app.listen(3000) |
上表展示了二者的使用區別,從初始化就看出koa語法都是用的新標准。在掛載路由中間件上也有一定的差異性,這是因為二者內部實現機制的不同。其他都是大同小異的了。
與 express,hapi,eggjs 比起來,koa 真的十分小巧,以至於不能稱作一種框架,可以看做一種庫,但這並不妨礙 koa 生態的發展。
express 當初也是大而全的框架,慢慢的把各種功能已中間件的形式抽離出來,koa 可以看做這種思想的一種實現。大而全的框架主要存在起初的學習成本高,功能冗余等問題,使用 koa 對於初次使用 nodejs 開發 web 的人員非常友好,對於初學者來說,建議從 koa 入手,使用不同的中間件來實現不同的功能,對於了解 web 開發有很大幫助。
四、中間件
1、什么是中間件?
1.執行任何代碼
2.修改請求和和響應對象
3.終結請求-響應循環
4.調用堆棧中的下一個中間件
通過next來實現
在express 中間件(Middleware) 是一個函數,它可以訪問請求對象(request object(req)),響應對象(response object()res)和web應用中處理請求-相應循環流程中的中間件,一般被命名為next的變量。在Koa中中間件和express有點類似。
經典的洋蔥圖概念能很好的解釋next的執行,請求從最外層進去,又從最里層出來。

中間件的功能包括:
-
執行任何代碼
-
修改請求和響應請求對象
-
終結請求-響應循環
-
調用堆棧中的下一個中間件
如果get、post回調函數中,沒有next參數,那么就匹配上第一個路由,就不會往下匹配了。如果想往下匹配的話,那么就需要寫next()。
app.use('/',function(){});
Koa應用可以使用如下幾種中間件:
-
應用級中間件
-
路由級中間件
-
錯誤處理中間件
-
第三方中間件
可以寫兩個參數,第一個是匹配的路徑,第二個是回調函數,第一個參數可以省略
五、實踐
1、安裝
檢查node版本
$ node -v v14.15.1
Koa 必須使用 7.6 以上的版本。如果你的版本低於這個要求,就要先升級 Node。
你可以使用自己喜歡的版本管理器快速安裝支持的 node 版本:
$ nvm install 7
$ npm i koa
$ node my-koa-app.js
2、架設HTTP服務
const Koa = require('koa');
const app = new Koa();
// 本地服務
app.listen(3000, ()=>{
console.log('http://localhost:3000')
})
顯示Not Found,因為我們沒有給內容,所以顯示這個。
3、Context 對象
Koa 提供一個 Context 對象,表示一次對話的上下文(包括 HTTP 請求和 HTTP 回復)。
demo如下:
const Koa = require('koa');
const app = new Koa();
const main = ctx => {
ctx.body = 'Hello 星火組';
};
app.use(main);
// 本地服務
app.listen(3000, ()=>{
console.log('http://localhost:3000')
})
HTTP Response類型:
const Koa = require('koa');
const app = new Koa();
const fs = require('fs');
// const main = ctx => {
// ctx.body = 'Hello 星火組';
// };
//HTTP Response 的類型
// const main = ctx => {
// if (ctx.request.accepts('xml')) {
// ctx.response.type = 'xml';
// ctx.response.body = '<data>Hello World</data>';
// } else if (ctx.request.accepts('json')) {
// ctx.response.type = 'json';
// ctx.response.body = { data: 'Hello World' };
// } else if (ctx.request.accepts('html')) {
// ctx.response.type = 'html';
// ctx.response.body = '<p>Hello World</p>';
// } else {
// ctx.response.type = 'text';
// ctx.response.body = 'Hello World';
// }
// };
const main = ctx => {
ctx.response.type = 'html';
ctx.response.body = fs.createReadStream('template.html');
};
app.use(main);
// 本地服務
app.listen(3000, ()=>{
console.log('http://localhost:3000')
})
node 項目實操查看ctx返回
https://koajs.com/#context。官方文檔介紹。
4、router 路由
上面代碼中,根路徑/的處理函數是main,/about路徑的處理函數是about。
const Koa = require('koa');
const route = require('koa-route');
const app = new Koa();
const about = ctx => {
ctx.response.type = 'html';
ctx.response.body = '<a href="/">Index Page</a>';
};
const main = ctx => {
ctx.response.body = 'Hello World';
};
app.use(route.get('/', main));
app.use(route.get('/about', about));
app.listen(3000, ()=>{
console.log('http://localhost:3000')
})
5、中間件
// 引入Koa模塊 const Koa = require('koa'); // 引入Koa-router const Router = require('koa-router'); // 實例化Koa模塊 const app = new Koa(); // 實例化路由模塊 const router = new Router(); // Koa 中間件 // app.use('/',function(){}); //可以寫兩個參數,第一個是匹配的路徑,第二個是回調函數,第一個參數可以省略 // 匹配任何路由之前打印日期 app.use(async (ctx,next)=>{ console.log(new Date()); await next(); //當前路由匹配完成以后繼續向下匹配 }); // 配置路由 // ctx 上下文 context, 包含了request和response等信息 router.get('/',async (ctx)=>{ ctx.body = '網站首頁'; //返回數據 相當於:原生里面的res.writeHead() res.end() }); // 路由級中間件 // 匹配帶news路由以后繼續向下匹配路由 router.get('/news',async (ctx,next)=>{ console.log('這是一個新聞路由'); await next(); }); router.get('/news',async (ctx)=>{ ctx.body = '新聞列表頁面'; }); router.get('/login',async (ctx)=>{ ctx.body = '網站登錄頁面'; }); // 啟動路由 app .use(router.routes()) /*啟動路由*/ .use(router.allowedMethods()); //作用:當請求出錯時處理邏輯 /* * router.allowedMethods()作用: 這是官方文檔的推薦用法,我們可以 * 看到 router.allowedMethods()用在了路由匹配 router.routes()之后,所以在當所有 * 路由中間件最后調用.此時根據 ctx.status 設置 response 響應頭 * */ // 監聽3000端口 app.listen(3000,()=>{ console.log('starting at port 3000'); });
6、獲取請求數據
const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')
const router = new Router()
router.get('/data', async (ctx , next)=> {
let url = ctx.url
// 從ctx的request中拿到我們想要的數據
let data = ctx.request.query
let dataQueryString = ctx.request.querystring
ctx.body = {
url,
data,
dataQueryString
}
})
app.use(router.routes())
app.listen(3333, ()=>{
console.log('server is running at http://localhost:3333')
})
在瀏覽器里輸入http://localhost:3333/data?user=wuyanzu&id=123456 ,可以看到運行結果:
可以看到區別,.query返回的結果是對象,而.querystring返回的是字符串,這個很好理解。(chrome插件顯示成json格式)
如果遵從 RESTful 規范,比如請求要以 '/user/:id'的方式發出的話,我們可以用下面的例子來獲取到想要的數據。
router.get('/data/:id', async (ctx, next) => {
// 也從ctx中拿到我們想要的數據,不過使用的是params對象
let data = ctx.params
ctx.body = data
})
以上,我們通過上面的代碼和描述,已經對koa及node有一個初步的印象和概念。
課后作業:
用koa框架實現一個web頁面。
進階:本地json存儲,實現增刪改查。
參考:
【完】

