本節課重點內容#
Node.js 的應用場景(why)#
-
前端工程化
-
Web 服務端應用
- 學習曲線平緩,開發效率較高
- 運行效率接近常見的編譯語言
- 社區生態豐富及工具鏈成熟(npm,V8 inspector)
- 與前端結合的場景會有優勢(SSR,同構前端應用。 編寫頁面 和 後端數據的獲取和填充都由 JavaScript 來完成)
- 現狀:競爭激烈,Node.js 有自己獨特的優勢
-
Electron跨端桌面應用
- 商業應用: vscode、slack、discord、zoom
- 大型公司內的效率工具
- 現狀:大部分場景在選型時,都值得考慮
Node.js 運行時結構(what)#
- N-API:用戶代碼中利用 npm 安裝的一些包
- V8:JavaScript Runtime,診斷調試工具 (inspector)
- libuv:eventloop (事件循環),syscall (系統調用)
- 舉例:用 node-fetch 發起請求時
- 整個過程中底層會調用非常多的 c++ 代碼
特點
-
異步 I/O:
setTimeout(() => { console.log('B'); }) console.log('A');
-
一個常見場景:讀取文件時。當 Node.js 執行 I/O 操作時,會在響應返回後恢復操作,而不是阻塞線程並佔用額外內存等待。(內存佔用更少)
-
-
單線程
-
worker_thread 可以起一個獨立線程,但每個線程的模型沒有太大變化
function fibonacci(num:number):number { if(num === 1 || num === 2) { return 1; } return fibonacci(num-1) + fibonacci(num-2); } fibonacci(42) fibonacci(43)
-
JS 單線程
- 實際線程 + uv 線程池(4 個線程) + V8 任務線程池 + V8 Inspector 線程
-
優點:不用考慮多線程狀態同步問題,也就不需要鎖。同時還能比較高效地利用系統資源;
-
缺點:阻塞會產生更多負面影響、異步問題、延時有要求的場景需要考慮。
- 解決辦法:多進程或多線程
-
-
跨平台(大部分功能、api)
-
想用 linux 上的 Socket,而不同平台上調用的又不一樣,只需:
const net = require('net') const socket = new net.Socket('/tmp/socket.sock')
-
Node.js 跨平台 + JS 無需編譯環境(+ Web 跨平台 + 診斷工具跨平台)
- = 開發成本低(大部分場景無需擔心跨平台問題),整體學習成本低
-
編寫 Http Server (how)#
安裝 Node.js#
- Mac, Linux 推薦使用 nvm。多版本管理。
- Windows 推薦 nvm4w 或是官方安裝包。
- 安裝慢,安裝失敗的情況,設置安裝源
- NVM_ NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node nvm install 16
編寫 Http Server + Client, 收發 GET, POST 請求#
- 前提:安裝好 Node.js,以管理員權限打開 cmd 下轉至當前文件目錄下
Http Server#
-
首先編寫一個 server.js,如下
-
createServer
說明req 請求,res 響應
port 要監聽的端口號,成功後的回調函數
-
const http = require('http'); const server = http.createServer((req, res) => { res.end('hello'); // 響應直接就是hello }); const port = 3000; server.listen(port, () => { console.log(`server listens on:${port}`); // 監聽3000端口 })
-
-
使用 node 啟動,此時輸入 localhost:3000 就可以看到 hello
- 改為 JSON 版
const server = http.createServer((req, res) => {
// receive body from client
const bufs = []; // 取傳的數據
req.on('data', data => {
bufs.push(data);
});
req.on('end', () => {
const buf = Buffer.concat(bufs).toString('utf-8');
let msg = 'Hello';
try {
reqData = JSON.parse(buf);
msg = reqData.msg;
} catch (err) {
res.end('invalid json');
}
// response
const responseJson = {
msg: `receive:${msg}`
}
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(responseJson));
});
});
Http Client#
const http = require('http');
const body = JSON.stringify({ msg: 'hello from my own client' });
// [url] [option] [callback]
const req = http.request('http://127.0.0.1:3000', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': body.length,
},
}, (res) => { // 響應體
const bufs = [];
res.on('data', data => {
bufs.push(data);
});
res.on('end', () => {
const buf = Buffer.concat(bufs);
const receive = JSON.parse(buf);
console.log('receive json.msg is:', receive);
});
})
req.end(body);
Promisify#
可以用 Promise + async 和 await 重寫這兩個例子(why?)
當 await 關鍵字與異步函數一起使用時,它的真正優勢就變得明顯了 —— 實際上, await 只在異步函數裡面才起作用。它可以放在任何異步的,基於 promise 的函數之前。它會暫停代碼在該行上,直到 promise 完成,然後返回結果值。在暫停的同時,其他正在等待執行的代碼就有機會執行了
async/await
讓你的代碼看起來是同步的,在某種程度上,也使得它的行為更加地同步。await
關鍵字會阻塞其後的代碼,直到 promise 完成,就像執行同步操作一樣。它確實可以允許其他任務在此期間繼續運行,但您自己的代碼被阻塞。
-
回調寫太多容易找不到,不宜維護
function wait(t) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, t); }); } wait(1000).then(() => { console.log('get called'); });
-
並不是所有回調都適合改寫成 Promise
- 適合只被調用一次的回調函數
const server = http.createServer(async (req, res) => { // 注意這裡的async // receive body from client 改成了Promise形式 const msg = await new Promise((resolve, reject) => { //執行完再交給msg const bufs = []; req.on('data', data => { bufs.push(data); }); req.on('error', (err) => { reject(err); }) req.on('end', () => { const buf = Buffer.concat(bufs).toString('utf-8'); let msg = 'Hello'; try { reqData = JSON.parse(buf); msg = reqData.msg; } catch (err) { // } resolve(msg); }); }); // response const responseJson = { msg: `receive:${msg}` } res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(responseJson)); });
編寫靜態文件服務器#
編寫一個簡單的靜態服務,接受用戶發過來的 http 請求,拿到圖片的 url 約定為靜態文件服務器磁碟上對應的路徑,再把具體內容返回給用戶。這次除了 http 模塊,還需要 fs 模塊和 path 模塊
先編寫一個簡單的 index.html,放於 static 目錄下
static_server.js#
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
// __dirname是當前這個文件所在位置, ./為當前文件所在文件夾 folderPath即為static文件夾相對於當前文件路徑
const folderPath = path.resolve(__dirname, './static');
const server = http.createServer((req, res) => { // 注意這裡的async
// expected http://127.0.0.1:3000/index.html
const info = url.parse(req.url);
// static/index.html
const filepath = path.resolve(folderPath, './'+info.path);
console.log('filepath', filepath);
// stream風格的api,其內部內存使用率更好
const filestream = fs.createReadStream(filepath);
filestream.pipe(res);
});
const port = 3000;
server.listen(port, () => {
console.log(`server listens on:${port}`);
})
- 與高性能、可靠的服務相比,還差什麼?
- CDN:緩存 + 加速
- 分佈式儲存,容災(服務器掛了也能正常服務)
編寫 React SSR 服務#
- SSR (server side rendering) 有什麼特點?
- 相比傳統 HTML 模版引擎:避免重複編寫代碼
- 相比 SPA (single page application):首屏渲染更快,SEO(搜索引擎優化)友好
- 缺點:
- 通常 qps(每秒查詢率)較低,前端代碼編寫時需要考慮服務端渲染情況
- 編寫比較難,編寫 js 還要考慮前端中的表現
安裝 React#
npm init
npm i react react-dom
編寫示例#
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const http = require('http');
function App(props) {
return React.createElement('div', {}, props.children || 'Hello');
}
const server = http.createServer((req, res) => {
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>My Application</title>
</head>
<body>
${ReactDOMServer.renderToString(
React.createElement(App, {}, 'my_content'))}
<script>
alert('yes');
</script>
</body>
</html>
`);
})
const port = 3000;
server.listen(port, () => {
console.log('listening on: ', port);
})
- SSR 難點
- 需要處理打包代碼
- 需要思考前端代碼在服務端運行時的邏輯
- 移除對服務端無意義的副作用,或重置環境
適用 inspector 進行調試、診斷#
-
V8 Inspector: 開箱即用、特性豐富強大、與前端開發一致、跨平
台node -- inspect
open http://localhost:9229/json
-
場景:
- 查看 console.log 內容
- breakpoint
- 高 CPU、死循環: cpuprofile
- 高內存佔用:heapsnapshot(堆快照)
- 性能分析
部署簡介#
寫完了,如何部署到生產環境捏?
-
部署要解決的問題
-
守護進程:當進程退出時,重新拉起
-
多進程:cluster 便捷地利用多進程
-
記錄進程狀態,用於診斷
-
-
容器環境
- 通常有健康檢查的手段,只需考慮多核 cpu 利用率即可
延伸話題#
快速了解 Node.js 代碼#
- 好處
- 從使用者的角色逐步理解底層細節,可以解決更複雜的問題,
- 自我證明,有助於職業發展;
- 解決社區問題,促進社區發展;
- 難點:
- 花時間(真實)
編譯 Node.js#
- 為什麼要學習編譯 Node.js
- 認知:黑盒到白盒,發生問題時能有跡可循
- 貢獻代碼的第一步
- 如何編譯
- ./configure &&make install
- 演示:給 net 模塊添加自定義屬性
診斷 / 追蹤#
- 診斷是一個低頻、重要同時也相當有挑戰的方向。是企業衡量自己能否依賴一個門語言的重要參考。
- 技術諮詢行業中的熱門角色。
- 難點:
- 需要了解 Node.js 底層,需要了解操作系統以及各種工具
- 需要經驗
WASM, NAPI#
- Node.js (因為 V8) 是執行 WASM 代碼的天然容器,和瀏覽器 WASM 是同一運行時,同時 Node.js 支持 WASI。
- NAPI 執行 C 接口的代碼 (C/C++/Rust...),同時能保留原生代碼的性能。
- 不同編程語言間通信的一種方案。
總結感想#
本節課從 Node.js 介紹起,實現了其編寫 Http Server 的一個實戰(並用 Promise 優化回調,還對 SSR 有了一定的了解),並在延伸話題裡老師也給出了一些建議與拓展閱讀,好欸~
本文引用的內容大部分來自歐陽亞東老師的課以及 MDN。