Node.js 系列:構建原生 Node.js 應用

ylone666發表於2019-02-27

原生 Node.js 應用

Node.js 是一個基於 Chrome V8 引擎的 JavaScript 執行環境

Node.js 使用了一個事件驅動、非阻塞式 I/O 的模型,使其輕量又高效

Node.js 的包管理器 npm,是全球最大的開源庫生態系統

? 本文主要介紹構建一個 Node.js 應用的基本步驟和模組,並假定你已經對 Node.js Api 有一定的瞭解

? 本文引用部分程式碼作為例子,如果希望參看全部原始碼,歡迎去 github 查閱(如果覺得有一定幫助,歡迎star)

模組架構設計

整個 Node.js 應用的架構設計

original

node.js 應用構成

  • 引入模組:通過 require 指令來引入 Node.js 模組

  • 建立伺服器:伺服器用來監聽客戶端請求

  • 接收請求和響應請求:接收客戶端的HTTP請求,返回響應資料

// 通過 require 引入 http 模組,並將例項化的 HTTP 賦值給 http 變數
const http = require(`http`);
// 引入 url 模組,用來解析資料
const url = require(`url`);

function ylone(router, handleObj) {
    const hostname = `127.0.0.1`;
    const port = 7777;
    // http.createServer(function(){}) 方法建立伺服器,並返回一個物件
    const server = http.createServer((req, res) => {
        const path = url.parse(req.url);
        const pathName = path.pathname;
        // 處理node.js每次自動請求favicon.ico
        if (pathName !== `/favicon.ico`) {
          const content = router(handleObj, pathName, res, req);
        }
    });

    // server.listen() 方法繫結主機和埠
    server.listen(port, hostname, () => {
        console.log(`服務執行在${hostname}:${port}`);
    });
}

exports.ylone = ylone;
複製程式碼

基於事件驅動的回撥

  • http.createServer((req, res) => {...}) 是一個典型的回撥,事實上,這就是 Node.js 原生的工作方式

  • 這裡直接將一個匿名函式作為變數進行傳遞,繞開了“先定義,再傳遞”的圈子

  • 像寫 PHP 應用時:任何時候當有請求進入時,伺服器(如Apache)就會為這個請求建立一個新的程式,並且從頭至尾執行相應的 PHP 指令碼

  • 在 Node.js 中,無論何時當一個新的請求到達指定埠時,我們在伺服器建立時傳遞的函式就會被呼叫


Node.js 模組

  • 模組意味著將 Node.js 應用(如 http.js)抽象成一個模組,通過入口檔案 index.js 去呼叫相應的模組來引導和啟動應用

  • 將程式碼模組化意味著我們將提供其功能的部分(如 一個函式)匯出到請求這個模組的指令碼內

const server = require(`./http`);
const router = require(`./route`);
const handle = require(`./requestHandle`);

var handleObj = {};
// 入口 Case
handleObj[`/`] = handle.hello;
// 非阻塞 Case
handleObj[`/vlone`] = handle.vlone;
// post Case
handleObj[`/supreme`] = handle.supreme;
// get Case
handleObj[`/adidas`] = handle.adidas;

server.ylone(router.router, handleObj);
複製程式碼

路由

  • 為了處理不同的 http 請求,我們需要通過建立一個路由模組來進行“路由選擇”

  • 為路由提供請求的 url 和其他需要的 get 和 post 引數,隨後路由根據這些資料來執行相應的程式碼

  • 我們所需要的資料都在 http.createServer((req, res) => {...}) 的 req 引數中,為了解析 req,需要額外引入 urlquerystring Node.js 模組

  • 在 index.js 內引入路由物件,將路由方法傳遞給 http 應用,在 http.createServer((req, res) => {...}) 內解析 req 引數,然後呼叫 router 方法

  • 理解以下函數語言程式設計:將 router 物件傳遞給 index,在 index 內將 router 方法傳遞給 http,因為 http 並不關心 router 方法從哪來,只需要執行方法,然後完成業務,但是首先需要保證有這個物件

  • 函數語言程式設計最基本,最核心的即思想轉換,由名詞到動詞,由物件到方法,行為驅動執行

function router(handleObj, pathName, res, req) {
  if (typeof handleObj[pathName] === `function`) {
    return handleObj[pathName](res, req);
  } else {
    res.writeHead(200, {
          `Content-type`: `text/plain`
    });
    const content = `404 Not Found`;
    res.write(content);
    res.end();
  }
}

exports.router = router;
複製程式碼

將路由分發到請求處理函式

  • 需要建立一個新的 resquestHandlers 模組,封裝各個處理函式來對應不同的請求

  • 在 JavaScript 中通過物件鍵值對來封裝 路徑->方法 的對映關係

  • 在 C++ 或者 C# 中,物件指的是類或者結構體的例項,物件根據其例項化的模板會擁有不同的屬性和方法

  • 在 JavaScript 中,物件是一個鍵值對集合

  • 在入口檔案(index.js)內引入 requestHandle,同時宣告一個操作物件(handleObj),用來儲存 路徑->方法 的對映關係,最後將路由方法和操作物件傳遞給伺服器應用(http.js)

  • 在伺服器應用內,獲得瀏覽器請求的路徑,呼叫路由方法(router),將操作物件(handleObj)和請求路徑作為引數傳遞

  • 在路由內(route.js)獲取路徑對應的函式,自執行函式

因為文章篇幅原因,這裡只展示關鍵程式碼,原始碼參看 github

const { exec } = require(`child_process`);
const querystring = require(`querystring`);
const url = require(`url`);

function createHttp(type, res, val) {
    const content = val;
    const conType = {
        plain: `text/plain;charset=utf-8`,
        html: `text/html`,
    };
    // 為隱式的響應頭設定值
    res.writeHead(200, {
        `Content-type`: conType[type]
    });
    // 傳送響應主體
    res.write(content);
    // http 完成響應
    res.end();
}

... something else

function vlone(res) {
    exec(`node --version`, (error, stdout, stderr) => {
        if (error) {
            console.log(error, stdout, stderr);
            return;
        }
        const content = stdout;
        const type = `plain`;
        createHttp(type, res, content);
    });
}

... something else
複製程式碼

阻塞與非阻塞

A() 方法讀取檔案,因此需要一定的響應時間,B() 方法代表其他需要執行的程式碼

阻塞:在A() 執行的過程中,B() 處於等待狀態,當A() 訪問檔案資料準備就緒後,B() 才開始執行

阻塞IO模型

由上圖可以看出,應用程式從進行系統呼叫到複製資料包到應用程式緩衝區的整段過程是阻塞的,直到資料包被複制到使用者空間完成後,使用者程式才解除阻塞狀態,繼續執行下一個應用程式

  • 優點:能夠及時返回資料,無延遲,方便除錯

  • 缺點:需要等待

非阻塞:在A() 執行的過程中,B() 同時執行,且當A() 訪問檔案資料準備就緒後,A() 會被執行完成

非阻塞IO模型

由上圖可以看出,應用程式在呼叫過程中,如果資料包還沒有準備就緒,會先返回一個錯誤資訊(EWOULDBLOCK),此時當前程式可以執行其他方法,而不會阻塞。而 A() 會輪詢核心,返回緩衝區資料是否準備就緒

  • 優點:不需要等待,當前執行緒可以處理多個任務

  • 缺點:增大了任務完成的響應延遲,因為任務可能在兩次輪詢間隔內完成,從而導致整體資料的吞吐量降低


以非阻塞方式進行請求響應

  • 當前的應用互動方式:(請求處理程式 -> 請求路由 -> 伺服器)將請求處理程式返回的內容(請求處理程式最終要顯示給使用者的內容)傳遞給HTTP伺服器

  • 當前這種互動方式的問題在於,如果請求處理程式中有 Node.js 封裝的非阻塞方法A(),那麼A() 在阻塞過程中,伺服器就已經將資料返回了,並不會等到A() 執行完畢

  • 為了解決上述問題,以非阻塞方式進行請求響應,相對於之前將資料傳遞給伺服器的方式,現在我們需要將伺服器(response物件)傳遞給生成資料的應用內,待資料準備完畢,再返回響應資料

  • 這樣可以同時請求兩個路徑(實際上就是觸發兩個函式方法),B() 並不會因為A()執行時間長而處於等待狀態


處理 post 請求

  • 建立一個表單元素,設定表單提交方法為 post, 每當使用者提交表單時,則觸發 supreme() 方法

  • 處理 post 請求一般採用非同步非阻塞方式,因為 post 請求一般會比較重,你無法控制使用者輸入的資料量,如果用阻塞的方式處理則必然會導致使用者操作阻塞

  • 為了實現非阻塞,Node.js 會將 post 資料拆分成資料塊,然後通過出發特定事件,將這些資料塊傳遞給回撥函式

  • 常用的 post 兩個事件:data事件(新的資料塊到達時觸發),end事件(所有資料都已經接收完畢時觸發)

  • 通過在 request 物件上註冊監聽器(listener)來告訴應用當 post 事件觸發時,應該觸發哪些回撥函式


處理 get 請求

  • 通過 Node.js 封裝的 url物件來解析 url 引數,獲取關鍵資料

  • url.parse() 的第二引數 parseQueryString 如果為 true,則 query 屬性總是會通過 querystring 模組的 parse() 方法生成一個物件

some pieces

  • 當寫好 Node.js 指令碼(如 ylone.js)後,通過 node ylone.js 命令執行指令碼

  • 在瀏覽器訪問指定地址(如 http://localhost:7777/)意味著向伺服器發出請求,從而觸發伺服器建立時的回撥函式

  • 當訪問網頁(如 http://localhost:7777/)時,控制檯可能會輸出兩次 req 的資料,那是因為大部分瀏覽器會在訪問網頁時嘗試讀取 favicon.ico 檔案

  • 針對瀏覽器每次傳送請求,都會預設請求一次 /favicon.ico 的問題,可以在 http 中對其進行過濾,不執行操作

  • 如果希望在 Node.js 內的傳遞一個 html 片段,並渲染在瀏覽器上,需要將 res.writeHead(200, {`Content-type`: `text/plain`}) 的 Content-type 設定為 text/html

  • Node.js 返回資料(response)在瀏覽器展示亂碼,通過在 res.writeHead(200, {`Content-type`: `text/plain;charset=utf-8`}) 加上 charset=utf-8 配置解決


–Respect Node.js⚡–

相關文章