banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

青訓營 |「Node.js 與前端開發實戰」

本節課重點內容#

Node.js 的應用場景(why)#

  • 前端工程化

    • 早期的 jQuery 等庫都是直接在頁面中引入,後來模組化逐漸成熟,Node.js 賦予了開發者在瀏覽器外運行代碼的能力,前端逐漸模組化、

    • Bundle:webpackViteesbuildParcel

    • Uglify:UglifyJS

    • Transplie:babeljsTypeScript

      個人理解:Transplie 就是將 ES6 這樣最新的語法轉譯成低版本的寫法,實現瀏覽器兼容

    • 其他語言加入前段工程化的競爭:esbuildParcel 、prisma 等

    • 現狀:Node.js 難以替代

  • Web 服務端應用

    • 學習曲線平緩,開發效率較高
    • 運行效率接近常見的編譯語言
    • 社區生態豐富及工具鏈成熟(npm,V8 inspector)
    • 與前端結合的場景會有優勢(SSR,同構前端應用。 編寫頁面 和 後端數據的獲取和填充都由 JavaScript 來完成)
    • 現狀:競爭激烈,Node.js 有自己獨特的優勢
  • Electron跨端桌面應用

    • 商業應用: vscode、slack、discord、zoom
    • 大型公司內的效率工具
    • 現狀:大部分場景在選型時,都值得考慮

Node.js 運行時結構(what)#

image.png

  • 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 操作時,會在響應返回後恢復操作,而不是阻塞線程並佔用額外內存等待。(內存佔用更少)

      image.png

  • 單線程

    • 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#

編寫 Http Server + Client, 收發 GET, POST 請求#

  • 前提:安裝好 Node.js,以管理員權限打開 cmd 下轉至當前文件目錄下

Http Server#

  • 首先編寫一個 server.js,如下

    • createServer 說明

      req 請求,res 響應

      server.listen 說明

      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

image.png

image.png

  • 改為 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));
      });
  });
  • image.png
  • image.png

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);

image.png

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));
    });
    

image.png

編寫靜態文件服務器#

編寫一個簡單的靜態服務,接受用戶發過來的 http 請求,拿到圖片的 url 約定為靜態文件服務器磁碟上對應的路徑,再把具體內容返回給用戶。這次除了 http 模塊,還需要 fs 模塊和 path 模塊

先編寫一個簡單的 index.html,放於 static 目錄下

image.png

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}`);
})

image.png

image.png

  • 與高性能、可靠的服務相比,還差什麼?
  1. CDN緩存 + 加速
  2. 分佈式儲存,容災(服務器掛了也能正常服務)

編寫 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);
})

image.png

  • SSR 難點
  1. 需要處理打包代碼
  2. 需要思考前端代碼在服務端運行時的邏輯
  3. 移除對服務端無意義的副作用,或重置環境

適用 inspector 進行調試、診斷#

  • V8 Inspector: 開箱即用、特性豐富強大、與前端開發一致、跨平

    • node -- inspect
    • open http://localhost:9229/json

    image.png

  • 場景:

    • 查看 console.log 內容
    • breakpoint
    • 高 CPU、死循環: cpuprofile
    • 高內存佔用:heapsnapshot(堆快照)
    • 性能分析

部署簡介#

寫完了,如何部署到生產環境捏?

  • 部署要解決的問題

    • 守護進程:當進程退出時,重新拉起

    • 多進程:cluster 便捷地利用多進程

    • 記錄進程狀態,用於診斷

  • 容器環境

    • 通常有健康檢查的手段,只需考慮多核 cpu 利用率即可

延伸話題#

快速了解 Node.js 代碼#

Node. js Core 貢獻入門

  • 好處
    • 從使用者的角色逐步理解底層細節,可以解決更複雜的問題,
    • 自我證明,有助於職業發展;
    • 解決社區問題,促進社區發展;
  • 難點:
    • 花時間(真實)

編譯 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。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。