高效能Web 應用程式實踐
介紹
fibjs 是一個主要為Web 後端開發而設計的高效能應用程式伺服器框架。它建立在Google v8 JavaScript 引擎基礎上,並且選擇了和傳統的callback 不同的並發解決方案。fibjs 利用fibers 在框架層隔離了非同步呼叫所帶來的業務複雜性,極大降低了開發難度,並減少因為使用者空間頻繁非同步處理所帶來的效能問題。
fibjs 的設計非常注重效能,內建的網路IO 和HTTP 模組採用了基於事件驅動的非阻塞I/O 模型,開發者可以輕鬆實現高可靠性的伺服器應用程式。而且由於底層是由C++ 實現的,fibjs 的性能表現非常優越,可以輕鬆地應對高並發的訪問,提供極為穩定和可靠的服務。
同時,fibjs 也支援WebSocket,這是一種基於TCP 協定的全雙工通訊協議,在瀏覽器和伺服器之間建立起一個不斷開的連接,可以實現即時雙向資料傳輸,並且可以支援任意格式的數據傳輸。利用WebSocket 可以輕鬆實現溝通效果更加優秀的即時通訊應用。
總之,fibjs 既強調了高效能和高可靠性,又提供了像WebSocket 這樣的即時通訊特性,是一款非常適合開發高速Web 應用程式的框架。
開發環境搭建
在開始進行fibjs 的開發之前,我們需要先準備好開發環境。本章節將介紹如何安裝fibjs,如何使用fibjs 工具初始化專案以及如何使用IDE 整合開發環境。
安裝fibjs
針對不同的作業系統,安裝fibjs 的方式也略有不同。
對於Linux 和macOS 用戶,可以使用以下命令來安裝fibjs:
1curl -s https://fibjs.org/download/installer.sh | sh
如果使用macOS 並且使用了Homebrew 套件管理器,也可以使用以下指令安裝:
1brew install fibjs
對於Windows 用戶,需要從fibjs 官網下載安裝程序,然後依照指示進行安裝。
使用fibjs –init 建立新項目
安裝好fibjs 後,可以使用fibjs 工具來快速建立新專案。使用以下命令可以建立一個基礎的專案範本:
1fibjs --init
該命令將在當前目錄中創建一個新的項目結構,其中包括package.json,用來存儲項目的基本信息,以及依賴信息。
編寫Web 應用程式
Web 應用程式開發是目前fibjs 最常用的一種應用場景,fibjs 提供了一系列工具和模組幫助我們更快速地建立Web 應用程式。
編寫HTTP 伺服器
- 先導入http 模組;
- 實例化http.Server,監聽請求。
- 伺服器透過start 函數啟動。
1
2
3
4
5
6const http = require('http');
const server = new http.Server(8080, (req) => {
req.response.write('Hello World!');
});
server.start();
解析URL 參數和請求體
解析url 參數和請求體非常重要,應用於各種服務端應用程式。在fibjs 中可以直接透過req.query 解析傳入的url 參數,而請求體是透過req.body 讀取。
1
2
3
4
5
6
7
8const http = require('http');
const server = new http.Server(8080, (req) => {
var name = req.query.get('name');
var msg = name ? `Hello ${name}!` : 'Hello world!';
req.response.write(msg);
});
server.start();
實現介面存取控制
透過介面限制使用者存取是非常常見的一種場景。下面是一個簡單的例子。
1
2
3
4
5
6
7
8
9
10const http = require('http');
const server = new http.Server(8080, (req) => {
if (req.headers.get('auth') === 'ok') {
req.response.write('Hello World!');
} else {
req.response.write('Access Denied!');
}
});
server.start();
增加路由處理
路由是一個網路應用程式中最重要的概念之一,路由就是指將接收到的請求依照一定規則分發到處理器中去。在fibjs,你可以自己編寫路由模組,並綁定到http server 中,然後透過自訂的路由解析進行URL匹配以及對應的處理。
1
2
3
4
5
6
7
8
9const http = require('http');
const { Router } = require('mq');
var router = new Router();
router.get('/hello/:name', function (req, name) {
req.response.write('hello, ' + name);
});
var svr = new http.Server(8080, router);
svr.start();
以上範例也可以用更簡單的語法來實現:
1
2
3
4
5
6
7
8const http = require('http');
var svr = new http.Server(8080, {
'/hello/:name': function (req, name) {
req.response.write('hello, ' + name);
}
});
svr.start();
錯誤處理與日誌記錄
在fibjs 中你可以透過try-catch 區塊捕捉邏輯異常,透過輸出到日誌檔案進行調式記錄;若為致命性異常,可以直接拋出給上層框架處理。
1
2
3
4
5
6
7
8
9
10const console = require('console');
const http = require('http');
const server = new http.Server(8080, (req) => {
try {
// ...
} catch (e) {
console.log(e.message, e.stack);
}
});
跨域請求
在fibjs 中,我們可以使用enableCrossOrigin 這個方法來允許跨域請求。以下是如何透過建立http 伺服器並允許跨網域請求的範例程式碼:
1
2
3
4
5
6
7
8const http = require('http');
const server = new http.Server(8080, (req) => {
req.response.write('Hello World!');
});
server.enableCrossOrigin(); // enable cross domain request
server.start();
在上面的範例中,我們以8080 連接埠建立了一個http 伺服器。enableCrossOrigin() 方法允許跨域請求。
在使用enableCrossOrigin 允許跨域請求時,可以透過傳遞一個參數allowHeaders 來指定允許接收的跨域Header。預設情況下,allowHeaders 值為Content-Type。
範例程式碼如下:
1
2// enable "Content-Type" and "Authorization" headers in cross domain request
server.enableCrossOrigin("Content-Type, Authorization");
在上述程式碼中,allowHeaders 的值為"Content-Type, Authorization",表示伺服器允許接收"Content-Type" 和"Authorization" 這兩個跨域Header。如果請求中包含其他header,則會被伺服器拒絕。
需要注意的是,當我們使用enableCrossOrigin 設定允許接收跨域Header 時,也需要在發送跨域請求時設定對應的request header,否則同樣會被伺服器拒絕。
WebSocket
WebSocket 協定是一種基於TCP 協定的全雙工通訊協議,在瀏覽器和伺服器之間建立起一個不斷開的連接,可以實現即時雙向資料傳輸,並且可以支援任意格式的資料傳輸。在fibjs 中,WebSocket 支援模組提供了對應的API 接口,可以實現WebSocket 伺服器端和客戶端的開發。
使用fibjs 原生WebSocket 模組實作WebSocket 伺服器端
在伺服器端,可以透過upgrade 函數將HTTP 請求轉換為WebSocket 連線。在建立http server 物件的時候,可以使用ws.upgrade(callback) 並將其傳入http.start() 方法,實作將http 請求轉換為WebSocket 的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27var ws = require('ws');
var http = require('http');
var server = new http.Server(8080, {
'/ws': ws.upgrade(function(conn, req) {
console.log('a client connected.');
// listening for message events
conn.onmessage = function(evt) {
console.log('received message: ', evt.data);
// echo the message back to client
conn.send('Server: ' + evt.data);
};
// listening for close events
conn.onclose = function(code, reason) {
console.log('closed.');
};
// listening for error events
conn.onerror = function(err) {
console.log(err);
};
})
});
server.start();
在上述範例中,我們可以監聽到客戶端發送的訊息事件及伺服器與客戶端連線關閉事件。當伺服器接收到客戶端訊息後,會將相同的訊息傳回客戶端。此時實作了簡單的WebSocket 點對點通訊。
實現與資料儲存的交互
使用WebSocket 進行通訊的時候,除了單純訊息的收發之外,還需要考慮資料的持久化儲存和查詢等操作。這時候就需要使用到資料庫,可以使用fibjs 內建的db 模組與資料庫互動。
範例程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33var ws = require("ws");
var http = require("http");
var db = require("db");
// open a mysql connection
var mysql = db.openMySQL("mysql://root:password@localhost/dbname");
var server = new http.Server(8080, {
"/ws": ws.upgrade(function(conn, req) {
console.log("a client connected.");
// listening for message events
conn.onmessage = function(evt) {
console.log("received message: ", evt.data);
// use execute to query the data
var rs = mysql.execute("SELECT * FROM user WHERE name=?", evt.data.toString());
conn.send(JSON.stringify(rs));
};
// listening for close events
conn.onclose = function(code, reason) {
console.log("closed.");
};
// listening for error events
conn.onerror = function(err) {
console.log(err);
};
})
});
server.start();
在上面的範例中,我們首先使用了db 模組的openMySQL 方法建立了一個MySQL 資料庫連接物件mysql,然後在監聽到來自客戶端的訊息後,使用execute 方法直接執行SQL 查詢,取得滿足條件的記錄。最後將查詢結果透過WebSocket 協定傳送回客戶端。
需要注意,在實際的開發中,需要做好異常處理和資料的安全性。
綜上所述,透過db 模組,我們可以方便輕鬆地進行與資料庫的交互,結合WebSocket 協議,實現面向即時、高效能的Web 應用程式。
實現WebSocket 客戶端與伺服器端通信
在客戶端,可以透過建立WebSocket 實例並指定URL 來連接WebSocket 伺服器,然後將訊息傳送給伺服器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24var ws = require('ws');
// create a WebSocket object and connect to ws://localhost:8080/ws
var conn = new ws.Socket('ws://localhost:8080/ws');
// listening for open events
conn.onopen = function() {
conn.send('hello');
}
// listening for message events
conn.onmessage = function(evt) {
console.log('received message:', evt.data);
}
// listening for close events
conn.onclose = function(code, reason) {
console.log('closed.');
}
// listening for error events
conn.onerror = function(err) {
console.log(err);
}
在上述客戶端程式碼中,我們建立了一個WebSocket 實例並指定其URL,在連線建立成功後就可以向服務端發送訊息了。當伺服器接收到客戶端訊息後,會將相同的訊息傳回客戶端。此時實作了簡單的WebSocket 點對點通訊。
WebSocket 的優勢和使用場景
WebSocket 協定具有典型的雙向通訊模型,允許伺服器主動向客戶端推送數據,常常用於實現聊天、線上遊戲等需要即時且即時性高的場合。相較於其他傳輸協議,WebSocket 協定具有以下優勢:
• 即時性高,支援雙向通訊• 協定規範簡單,使用方便• 能夠處理大量並發連接• 支援長連接,減少網路傳輸的時間消耗
WebSocket 最常見的使用場景包括網頁聊天、遊戲對戰、線上播放和即時通訊等。
綜上所述,透過WebSocket 支援模組,實現起來十分簡單,開發者可以快速建立自己的Web 應用程式。
單元測試
測試框架和測試方法
在軟體開發過程中,測試是一個非常重要的環節,單元測試是其中的重要一環,透過單元測試可以有效地驗證程式碼是否滿足設計和需求,以及避免程式碼修改時引入的錯誤。一般地,單元測試的原則是對每個函數和方法進行測試,確保每個函數和方法的輸入和輸出都是正確的。
測試框架是用來編寫、執行和驗證測試案例的程式碼庫,它提供了測試案例的管理、運行和報告等功能。在JavaScript 和Node.js 中,流行的單元測試框架有Mocha、Jest 和Jasmine 等。在fibjs 中,我們也有自己的測試框架,也就是test 模組。
在單元測試過程中,一般採用的測試方法包括黑盒測試和白盒測試兩種方法。
黑盒測試是一種測試方法,它只考慮函數的輸入和輸出,而不考慮函數內部的實作細節。黑盒測試基於需求分析和設計規格書,透過測試案例分析和執行,判斷程式是否出現邏輯錯誤、邊界錯誤以及安全性問題等。它的優點是測試過程簡單,測試結果可靠,缺點是測試無法覆蓋所有的程式路徑。
白盒測試是一種測試方法,它考慮函數的內部實作細節,包括條件語句、循環語句、遞歸以及程式碼的覆蓋率等。透過這些測試,可以發現在共享資料和程式碼之間的互動中可能存在的問題。白盒測試的優點是可以涵蓋所有的程式路徑,缺點是測試過程較為複雜,測試結果受到環境和實作方法的影響。
使用test 模組編寫測試案例
在fibjs 中,我們可以使用test 模組來撰寫web server 的測試案例。下面是一個簡單的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14var test = require('test');
test.setup();
var http = require('http');
describe('Web server test', () => {
it('should return hello world', () => {
var r = http.get('http://localhost:8080/hello');
assert.equal(r.statusCode, 200);
assert.equal(r.data.toString(), 'Hello World');
});
});
test.run();
在這個例子中,我們使用describe 和it 函數分別定義測試模組和測試案例,並使用assert 函數進行斷言驗證。
在describe 函數中,我們可以定義多個it 函數來分別測試不同的場景。在每個it 函數中,我們可以使用http.get 函數來模擬HTTP GET 請求,取得請求回應並進行assertTrue 和assertEqual 等斷言驗證。
透過編寫測試案例,可以有效地測試函數和模組的正確性,確保產品質量,同時也能加強程式碼的可維護性。
熱更新
熱更新指的是在不停止服務的情況下,實作服務端程式碼的更新。在程式開發過程中,為了快速迭代,經常需要進行程式碼調整,以及新增功能。使用熱更新可以在不停止服務的前提下,使用新的程式碼,更有效率的完成迭代工作。
在fibjs 中,我們可以使用SandBox 模組來實現平滑的熱更新。SandBox 模組可以提供一個安全的執行環境,模擬全域變數等功能。具體實作可以參考以下步驟:
- 載入需要更新的程式碼檔案(例如web.js)。
- 透過SandBox,建立一個新的安全模組,在該模組中載入web.js,產生安全模組。透過產生的安全模組重新掛載正在運行的服務的handler。
- 伺服器繼續處理先前的請求,新的請求將被掛載在新的handler 上。
下面是一個使用SandBox 模組實現平滑熱更新的範例程式碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const fs = require('fs');
const http = require('http');
const { SandBox } = require('vm');
let FILE_PATH = './web.js';
let handler = new SandBox().require(FILE_PATH).handler;
const server = new http.Server(8080, handler);
server.start();
fs.watch(FILE_PATH, (event, filename) => {
handler = new SandBox().require(FILE_PATH).handler;
server.handler = handler;
console.log(`[${new Date().toLocaleString()}] server reloaded.`);
});
這份程式碼中,我們在程式啟動時首先載入一次web.js 中的程式碼,隨後建立SandBox 實例,並在實例中載入程式碼。之後我們新建了一個HTTP Server,使用handler 中的方法來處理請求。
在程式碼中,我們使用fs.watch 監聽web.js 檔案的變化,一旦檔案發生變化,我們重新載入程式碼,更新handler 中的實作。
效能最佳化
在開發的過程中,我們經常需要面對效能問題。優化程式碼,提高效能,是開發者必備技能之一。在fibjs 中,我們可以利用CPU Profiler 來幫助我們分析程式的運作狀態,最佳化程式碼。
在fibjs 中,只需要使用命令列參數--prof,啟動fibjs,也就是可以啟動CPU Profiler(預設間隔是1000ms)。如果需要更高精確度的分析日誌,可以使用--prof-interval參數,設定日誌間隔。例如:
1
2$ fibjs --prof test.js # 启动 CPU Profiler,默认以 1000ms 为间隔
$ fibjs --prof --prof-interval=10ms test.js # 启动 CPU Profiler,以 10000us(即 10ms)为间隔
當fibjs 運行完成之後,會在目前目錄下產生一個來源檔案名稱目錄,這個目錄包含了一個日誌檔案和一些輔助檔案。日誌檔案的預設名字為fibjs-xxxx.log,其中xxxx 是一個時間戳記。可以使用--log 選項來指定日誌檔案名稱。此時,你可以使用--prof-process
處理產生的日誌:
1fibjs --prof-process fibjs-xxxx.log prof.svg
運行結束,使用瀏覽器打開prof.svg,即可查看此次日誌的火焰圖: 你可以點擊查看全尺寸的圖片,在全尺寸圖片中,你可以透過滑鼠操作,查閱更詳細的資訊:prof. svg。
產生的火焰圖中,每一個色塊,代表一個記錄點,色塊越長,表示被記錄的次數越多;每一行代表一層調用堆疊,層數越多表示調用的層數越多;調用堆疊的擺放,是倒置的,越靠下的色塊,越是最初的函數。
色塊的顏色有兩類,一類是紅色,一類是藍色。在fibjs 的profiler 裡,紅色代表JavaScript 運算,藍色代表io 運算或Native 運算。根據你需要解決的問題不同,所需要關注的區域也會不同。例如你需要解決cpu 佔用過高的問題,此時需要注意紅色的色塊;而如果你的應用,cpu 佔用不高,但是響應卻比較慢,就需要關注藍色的色塊。靠近頂部越大的色塊,越是需要關注和優化的重點。
我們可以嘗試針對佔用CPU 資源較多的函數做調整,透過非同步方式實作IO等,或是在編寫時優化程式碼。
部署上線
為了使我們的專案能夠運行在生產環境中,我們需要對其進行編譯和部署。這裡我們介紹如何使用package.json 檔案配置編譯和部署。
在專案中,我們可以使用package.json 來管理專案依賴、配置編譯和部署。以一個簡單的範例package.json 為例:
1
2
3
4
5
6
7{
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"fib-pool": "^1.0.0"
}
}
當我們需要編譯並部署專案時,只需要在終端機進入專案目錄,然後執行以下命令:
1fibjs --install
此指令會自動安裝專案所依賴的模組。之後,我們可以使用以下命令啟動專案:
1fibjs app.js
👉 【域名路由】