溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

基于Node.js的前端面試題有哪些

發(fā)布時間:2021-11-11 10:10:19 來源:億速云 閱讀:107 作者:小新 欄目:web開發(fā)

這篇文章主要介紹基于Node.js的前端面試題有哪些,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!

一、Node基礎概念

1.1 Node是什么

Node.js 是一個開源與跨平臺的 JavaScript 運行時環(huán)境。在瀏覽器外運行 V8 JavaScript 引擎(Google Chrome 的內核),利用事件驅動、非阻塞和異步輸入輸出模型等技術提高性能。我們可以理解為:Node.js 就是一個服務器端的、非阻塞式I/O的、事件驅動的JavaScript運行環(huán)境。

理解Node,有幾個基礎的概念:非阻塞異步和事件驅動。

  • 非阻塞異步: Nodejs采用了非阻塞型I/O機制,在做I/O操作的時候不會造成任何的阻塞,當完成之后,以時間的形式通知執(zhí)行操作。例如,在執(zhí)行了訪問數(shù)據(jù)庫的代碼之后,將立即轉而執(zhí)行其后面的代碼,把數(shù)據(jù)庫返回結果的處理代碼放在回調函數(shù)中,從而提高了程序的執(zhí)行效率。

  • 事件驅動: 事件驅動就是當進來一個新的請求的時,請求將會被壓入一個事件隊列中,然后通過一個循環(huán)來檢測隊列中的事件狀態(tài)變化,如果檢測到有狀態(tài)變化的事件,那么就執(zhí)行該事件對應的處理代碼,一般都是回調函數(shù)。比如,讀取一個文件,文件讀取完畢后,就會觸發(fā)對應的狀態(tài),然后通過對應的回調函數(shù)來進行處理。

基于Node.js的前端面試題有哪些

1.2 Node的應用場景及存在的缺點

1.2.1 優(yōu)缺點

Node.js適合用于I/O密集型應用,值的是應用在運行極限時,CPU占用率仍然比較低,大部分時間是在做 I/O硬盤內存讀寫操作。缺點如下:

  • 不適合CPU密集型應用

  • 只支持單核CPU,不能充分利用CPU

  • 可靠性低,一旦代碼某個環(huán)節(jié)崩潰,整個系統(tǒng)都崩潰

對于第三點,常用的解決方案是,使用Nnigx反向代理,開多個進程綁定多個端口,或者開多個進程監(jiān)聽同一個端口。

1.2.1 應用場景

在熟悉了Nodejs的優(yōu)點和弊端后,我們可以看到它適合以下的應用場景:

  • 善于I/O,不善于計算。因為Nodejs是一個單線程,如果計算(同步)太多,則會阻塞這個線程。

  • 大量并發(fā)的I/O,應用程序內部并不需要進行非常復雜的處理。

  • 與 WeSocket 配合,開發(fā)長連接的實時交互應用程序。

具體的使用場景如下:

  1. 用戶表單收集系統(tǒng)、后臺管理系統(tǒng)、實時交互系統(tǒng)、考試系統(tǒng)、聯(lián)網(wǎng)軟件、高并發(fā)量的web應用程序。

  2. 基于web、canvas等多人聯(lián)網(wǎng)游戲。

  3. 基于web的多人實時聊天客戶端、聊天室、圖文直播。

  4. 單頁面瀏覽器應用程序。

  5. 操作數(shù)據(jù)庫、為前端和移動端提供基于json的API。

二、Node全部對象

在瀏覽器 JavaScript 中,window 是全局對象, 而 Nodejs 中的全局對象則是 global。

在NodeJS里,是不可能在最外層定義一個變量,因為所有的用戶代碼都是當前模塊的,只在當前模塊里可用,但可以通過exports對象的使用將其傳遞給模塊外部。所以,在NodeJS中,用var聲明的變量并不屬于全局的變量,只在當前模塊生效。像上述的global全局對象則在全局作用域中,任何全局變量、函數(shù)、對象都是該對象的一個屬性值。

2.1 常見全局對象

Node常見的全局對象有如下一些:

  • Class:Buffer

  • process

  • console

  • clearInterval、setInterval

  • clearTimeout、setTimeout

  • global

Class:BufferClass:Buffer可以用來處理二進制以及非Unicode編碼的數(shù)據(jù),在Buffer類實例化中存儲了原始數(shù)據(jù)。Buffer類似于一個整數(shù)數(shù)組,在V8堆原始存儲空間給它分配了內存,一旦創(chuàng)建了Buffer實例,則無法改變大小。

processprocess表示進程對象,提供有關當前過程的信息和控制。包括在執(zhí)行node程序的過程中,如果需要傳遞參數(shù),我們想要獲取這個參數(shù)需要在process內置對象中。比如,我們有如下一個文件:

process.argv.forEach((val, index) => {
   console.log(`${index}: ${val}`);
});

當我們需要啟動一個進程時,可以使用下面的命令:

 node index.js 參數(shù)...

consoleconsole主要用來打印stdout和stderr,最常用的比如日志輸出:console.log。清空控制臺的命令為:console.clear。如果需要打印函數(shù)的調用棧,可以使用命令console.trace。

clearInterval、setIntervalsetInterval用于設置定時器,語法格式如下:

setInterval(callback, delay[, ...args])

clearInterval則用于清除定時器,callback每delay毫秒重復執(zhí)行一次。

clearTimeout、setTimeout

和setInterval一樣,setTimeout主要用于設置延時器,而clearTimeout則用于清除設置的延時器。

globalglobal是一個全局命名空間對象,前面講到的process、console、setTimeout等可以放到global中,例如:

console.log(process === global.process)     //輸出true

2.2 模塊中的全局對象

除了系統(tǒng)提供的全局對象外,還有一些只是在模塊中出現(xiàn),看起來像全局變量,如下所示:

  • __dirname

  • __filename

  • exports

  • module

  • require

__dirname__dirname主要用于獲取當前文件所在的路徑,不包括后面的文件名。比如,在/Users/mjr 中運行 node example.js,打印結果如下:

console.log(__dirname);         // 打印: /Users/mjr

__filename__filename用于獲取當前文件所在的路徑和文件名稱,包括后面的文件名稱。比如,在/Users/mjr 中運行 node example.js,打印的結果如下:

console.log(__filename);// 打印: /Users/mjr/example.js

exportsmodule.exports 用于導出一個指定模塊所的內容,然后也可以使用require() 訪問里面的內容。

exports.name = name;exports.age = age;
exports.sayHello = sayHello;

requirerequire主要用于引入模塊、 JSON、或本地文件, 可以從 node_modules 引入模塊??梢允褂孟鄬β窂揭氡镜啬K或JSON文件,路徑會根據(jù)__dirname定義的目錄名或當前工作目錄進行處理。

三、談談對process的理解

3.1 基本概念

我們知道,進程計算機系統(tǒng)進行資源分配和調度的基本單位,是操作系統(tǒng)結構的基礎,是線程的容器。當我們啟動一個js文件,實際就是開啟了一個服務進程,每個進程都擁有自己的獨立空間地址、數(shù)據(jù)棧,像另一個進程無法訪問當前進程的變量、數(shù)據(jù)結構,只有數(shù)據(jù)通信后,進程之間才可以數(shù)據(jù)共享。

process 對象是Node的一個全局變量,提供了有關當前 Node.js 進程的信息并對其進行控制。 由于JavaScript是一個單線程語言,所以通過node xxx啟動一個文件后,只有一條主線程。

3.2 常用屬性和方法

process的常見屬性如下:

  • process.env:環(huán)境變量,例如通過 `process.env.NODE_ENV 獲取不同環(huán)境項目配置信息

  • process.nextTick:這個在談及 EventLoop 時經(jīng)常為會提到

  • process.pid:獲取當前進程id

  • process.ppid:當前進程對應的父進程

  • process.cwd():獲取當前進程工作目錄

  • process.platform:獲取當前進程運行的操作系統(tǒng)平臺

  • process.uptime():當前進程已運行時間,例如:pm2 守護進程的 uptime 值

進程事件: process.on(‘uncaughtException’,cb) 捕獲異常信息、 process.on(‘exit’,cb)進程推出監(jiān)聽

  • 三個標準流: process.stdout 標準輸出、 process.stdin 標準輸入、 process.stderr 標準錯誤輸出

  • process.title:用于指定進程名稱,有的時候需要給進程指定一個名稱

四、談談你對fs模塊的理解

4.1 fs是什么

fs(filesystem)是文件系統(tǒng)模塊,該模塊提供本地文件的讀寫能力,基本上是POSIX文件操作命令的簡單包裝??梢哉f,所有與文件的操作都是通過fs核心模塊來實現(xiàn)的。

使用之前,需要先導入fs模塊,如下:

const fs = require('fs');

4.2 文件基礎知識

在計算機中,有關于文件的基礎知識有如下一些:

  • 權限位 mode

  • 標識位 flag

  • 文件描述為 fd

4.2.1 權限位 mode

基于Node.js的前端面試題有哪些

針對文件所有者、文件所屬組、其他用戶進行權限分配,其中類型又分成讀、寫和執(zhí)行,具備權限位4、2、1,不具備權限為0。如在linux查看文件權限位的命令如下:

drwxr-xr-x?1?PandaShen?197121?0?Jun 28 14:41?core
-rw-r--r--?1?PandaShen?197121?293?Jun 23 17:44?index.md

在開頭前十位中,d為文件夾,-為文件,后九位就代表當前用戶、用戶所屬組和其他用戶的權限位,按每三位劃分,分別代表讀(r)、寫(w)和執(zhí)行(x),- 代表沒有當前位對應的權限。

4.2.2 標識位

標識位代表著對文件的操作方式,如可讀、可寫、即可讀又可寫等等,如下表所示:

基于Node.js的前端面試題有哪些

4.2.3 文件描述 fd

操作系統(tǒng)會為每個打開的文件分配一個名為文件描述符的數(shù)值標識,文件操作使用這些文件描述符來識別與追蹤每個特定的文件。

Window 系統(tǒng)使用了一個不同但概念類似的機制來追蹤資源,為方便用戶,NodeJS 抽象了不同操作系統(tǒng)間的差異,為所有打開的文件分配了數(shù)值的文件描述符。

在 NodeJS 中,每操作一個文件,文件描述符是遞增的,文件描述符一般從 3 開始,因為前面有 0、1、2三個比較特殊的描述符,分別代表 process.stdin(標準輸入)、process.stdout(標準輸出)和 process.stderr(錯誤輸出)。

4.3 常用方法

由于fs模塊主要是操作文件的,所以常見的文件操作方法有如下一些:

  • 文件讀取

  • 文件寫入

  • 文件追加寫入

  • 文件拷貝

  • 創(chuàng)建目錄

4.3.1 文件讀取

常用的文件讀取有readFileSync和readFile兩個方法。其中,readFileSync表示同步讀取,如下:

const fs = require("fs");

let buf = fs.readFileSync("1.txt");
let data = fs.readFileSync("1.txt", "utf8");

console.log(buf); // <Buffer 48 65 6c 6c 6f>
console.log(data); // Hello
  • 第一個參數(shù)為讀取文件的路徑或文件描述符。

  • 第二個參數(shù)為 options,默認值為 null,其中有 encoding(編碼,默認為 null)和 flag(標識位,默認為 r),也可直接傳入 encoding。

readFile為異步讀取方法, readFile 與 readFileSync 的前兩個參數(shù)相同,最后一個參數(shù)為回調函數(shù),函數(shù)內有兩個參數(shù) err(錯誤)和 data(數(shù)據(jù)),該方法沒有返回值,回調函數(shù)在讀取文件成功后執(zhí)行。

const fs = require("fs");

fs.readFile("1.txt", "utf8", (err, data) => {
   if(!err){
       console.log(data);         // Hello
   }
});

4.3.2 文件寫入

文件寫入需要用到writeFileSync和writeFile兩個方法。writeFileSync表示同步寫入,如下所示。

const fs = require("fs");

fs.writeFileSync("2.txt", "Hello world");
let data = fs.readFileSync("2.txt", "utf8");

console.log(data); // Hello world
  • 第一個參數(shù)為寫入文件的路徑或文件描述符。

  • 第二個參數(shù)為寫入的數(shù)據(jù),類型為 String 或 Buffer。

  • 第三個參數(shù)為 options,默認值為 null,其中有 encoding(編碼,默認為 utf8)、 flag(標識位,默認為 w)和 mode(權限位,默認為 0o666),也可直接傳入 encoding。

writeFile表示異步寫入,writeFile 與 writeFileSync 的前三個參數(shù)相同,最后一個參數(shù)為回調函數(shù),函數(shù)內有一個參數(shù) err(錯誤),回調函數(shù)在文件寫入數(shù)據(jù)成功后執(zhí)行。

const fs = require("fs");

fs.writeFile("2.txt", "Hello world", err => {
    if (!err) {
        fs.readFile("2.txt", "utf8", (err, data) => {
            console.log(data);       // Hello world
        });
    }
});

4.3.3 文件追加寫入

文件追加寫入需要用到appendFileSync和appendFile兩個方法。appendFileSync表示同步寫入,如下。

const fs = require("fs");

fs.appendFileSync("3.txt", " world");
let data = fs.readFileSync("3.txt", "utf8");
  • 第一個參數(shù)為寫入文件的路徑或文件描述符。

  • 第二個參數(shù)為寫入的數(shù)據(jù),類型為 String 或 Buffer。

  • 第三個參數(shù)為 options,默認值為 null,其中有 encoding(編碼,默認為 utf8)、 flag(標識位,默認為 a)和 mode(權限位,默認為 0o666),也可直接傳入 encoding。

appendFile表示異步追加寫入,方法 appendFile 與 appendFileSync 的前三個參數(shù)相同,最后一個參數(shù)為回調函數(shù),函數(shù)內有一個參數(shù) err(錯誤),回調函數(shù)在文件追加寫入數(shù)據(jù)成功后執(zhí)行,如下所示。

const fs = require("fs");

fs.appendFile("3.txt", " world", err => {
    if (!err) {
        fs.readFile("3.txt", "utf8", (err, data) => {
            console.log(data); // Hello world
        });
    }
});

4.3.4 創(chuàng)建目錄

創(chuàng)建目錄主要有mkdirSync和mkdir兩個方法。其中,mkdirSync為同步創(chuàng)建,參數(shù)為一個目錄的路徑,沒有返回值,在創(chuàng)建目錄的過程中,必須保證傳入的路徑前面的文件目錄都存在,否則會拋出異常。

// 假設已經(jīng)有了 a 文件夾和 a 下的 b 文件夾
fs.mkdirSync("a/b/c")

mkdir為異步創(chuàng)建,第二個參數(shù)為回調函數(shù),如下所示。

fs.mkdir("a/b/c", err => {
    if (!err) console.log("創(chuàng)建成功");
});

五、談談你對Stream 的理解

5.1 基本概念

流(Stream)是一種數(shù)據(jù)傳輸?shù)氖侄?,是一種端到端信息交換的方式,而且是有順序的,是逐塊讀取數(shù)據(jù)、處理內容,用于順序讀取輸入或寫入輸出。在Node中,Stream分成三部分:source、dest、pipe。

其中,在source和dest之間有一個連接的管道pipe,它的基本語法是source.pipe(dest),source和dest就是通過pipe連接,讓數(shù)據(jù)從source流向dest,如下圖所示:

基于Node.js的前端面試題有哪些

5.2 流的分類

在Node,流可以分成四個種類:

  • 可寫流:可寫入數(shù)據(jù)的流,例如 fs.createWriteStream() 可以使用流將數(shù)據(jù)寫入文件。

  • 可讀流: 可讀取數(shù)據(jù)的流,例如fs.createReadStream() 可以從文件讀取內容。

  • 雙工流: 既可讀又可寫的流,例如 net.Socket。

  • 轉換流: 可以在數(shù)據(jù)寫入和讀取時修改或轉換數(shù)據(jù)的流。例如,在文件壓縮操作中,可以向文件寫入壓縮數(shù)據(jù),并從文件中讀取解壓數(shù)據(jù)。

在Node的HTTP服務器模塊中,request 是可讀流,response 是可寫流。對于fs 模塊來說,能同時處理可讀和可寫文件流可讀流和可寫流都是單向的,比較容易理解。而Socket是雙向的,可讀可寫。

5.2.1 雙工流

在Node中,比較的常見的全雙工通信就是websocket,因為發(fā)送方和接受方都是各自獨立的方法,發(fā)送和接收都沒有任何關系。

基于Node.js的前端面試題有哪些

基本的使用方法如下:

const { Duplex } = require('stream');

const myDuplex = new Duplex({
  read(size) {
    // ...
  },
  write(chunk, encoding, callback) {
    // ...
  }
});

5.3 使用場景

流的常見使用場景有:

  • get請求返回文件給客戶端

  • 文件操作

  • 一些打包工具的底層操作

5.3.1 網(wǎng)絡請求

流一個常見的使用場景就是網(wǎng)絡請求,比如使用stream流返回文件,res也是一個stream對象,通過pipe管道將文件數(shù)據(jù)返回。

const server = http.createServer(function (req, res) {
    const method = req.method;  
    // get 請求
    if (method === 'GET') { 
        const fileName = path.resolve(__dirname, 'data.txt');
        let stream = fs.createReadStream(fileName);
        stream.pipe(res);   
    }
});
server.listen(8080);

5.3.2 文件操作

文件的讀取也是流操作,創(chuàng)建一個可讀數(shù)據(jù)流readStream,一個可寫數(shù)據(jù)流writeStream,通過pipe管道把數(shù)據(jù)流轉過去。

const fs = require('fs')
const path = require('path')

// 兩個文件名
const fileName1 = path.resolve(__dirname, 'data.txt')
const fileName2 = path.resolve(__dirname, 'data-bak.txt')
// 讀取文件的 stream 對象
const readStream = fs.createReadStream(fileName1)
// 寫入文件的 stream 對象
const writeStream = fs.createWriteStream(fileName2)
// 通過 pipe執(zhí)行拷貝,數(shù)據(jù)流轉
readStream.pipe(writeStream)
// 數(shù)據(jù)讀取完成監(jiān)聽,即拷貝完成
readStream.on('end', function () {
    console.log('拷貝完成')
})

另外,一些打包工具,Webpack和Vite等都涉及很多流的操作。

六、事件循環(huán)機制

6.1 什么是瀏覽器事件循環(huán)

Node.js 在主線程里維護了一個事件隊列,當接到請求后,就將該請求作為一個事件放入這個隊列中,然后繼續(xù)接收其他請求。當主線程空閑時(沒有請求接入時),就開始循環(huán)事件隊列,檢查隊列中是否有要處理的事件,這時要分兩種情況:如果是非 I/O 任務,就親自處理,并通過回調函數(shù)返回到上層調用;如果是 I/O 任務,就從 線程池 中拿出一個線程來處理這個事件,并指定回調函數(shù),然后繼續(xù)循環(huán)隊列中的其他事件。

當線程中的 I/O 任務完成以后,就執(zhí)行指定的回調函數(shù),并把這個完成的事件放到事件隊列的尾部,等待事件循環(huán),當主線程再次循環(huán)到該事件時,就直接處理并返回給上層調用。 這個過程就叫 事件循環(huán) (Event Loop),其運行原理如下圖所示。

基于Node.js的前端面試題有哪些

從左到右,從上到下,Node.js 被分為了四層,分別是 應用層、V8引擎層、Node API層 和 LIBUV層。

  • 應用層:   即 JavaScript 交互層,常見的就是 Node.js 的模塊,比如 http,fs

  • V8引擎層:  即利用 V8 引擎來解析JavaScript 語法,進而和下層 API 交互

  • Node API層:  為上層模塊提供系統(tǒng)調用,一般是由 C 語言來實現(xiàn),和操作系統(tǒng)進行交互 。

  • LIBUV層: 是跨平臺的底層封裝,實現(xiàn)了 事件循環(huán)、文件操作等,是 Node.js 實現(xiàn)異步的核心 。

在Node中,我們所說的事件循環(huán)是基于libuv實現(xiàn)的,libuv是一個多平臺的專注于異步IO的庫。上圖的EVENT_QUEUE 給人看起來只有一個隊列,但事實上EventLoop存在6個階段,每個階段都有對應的一個先進先出的回調隊列。

6.2 事件循環(huán)的六個階段

事件循環(huán)一共可以分成了六個階段,如下圖所示。

基于Node.js的前端面試題有哪些

  • timers階段:此階段主要執(zhí)行timer(setTimeout、setInterval)的回調。

  • I/O事件回調階段(I/O callbacks):執(zhí)行延遲到下一個循環(huán)迭代的 I/O 回調,即上一輪循環(huán)中未被執(zhí)行的一些I/O回調。

  • 閑置階段(idle、prepare):僅系統(tǒng)內部使用。

  • 輪詢階段(poll):檢索新的 I/O 事件;執(zhí)行與 I/O 相關的回調(幾乎所有情況下,除了關閉的回調函數(shù),那些由計時器和 setImmediate() 調度的之外),其余情況 node 將在適當?shù)臅r候在此阻塞。

  • 檢查階段(check):setImmediate() 回調函數(shù)在這里執(zhí)行

  • 關閉事件回調階段(close callback):一些關閉的回調函數(shù),如:socket.on('close', ...)

每個階段對應一個隊列,當事件循環(huán)進入某個階段時, 將會在該階段內執(zhí)行回調,直到隊列耗盡或者回調的最大數(shù)量已執(zhí)行, 那么將進入下一個處理階段,如下圖所示。

基于Node.js的前端面試題有哪些

七、EventEmitter

7.1 基本概念

前文說過,Node采用了事件驅動機制,而EventEmitter 就是Node實現(xiàn)事件驅動的基礎。在EventEmitter的基礎上,Node 幾乎所有的模塊都繼承了這個類,這些模塊擁有了自己的事件,可以綁定、觸發(fā)監(jiān)聽器,實現(xiàn)了異步操作。

Node.js 里面的許多對象都會分發(fā)事件,比如 fs.readStream 對象會在文件被打開的時候觸發(fā)一個事件,這些產(chǎn)生事件的對象都是 events.EventEmitter 的實例,用于將一個或多個函數(shù)綁定到命名事件上。

7.2 基本使用

Node的events模塊只提供了一個EventEmitter類,這個類實現(xiàn)了Node異步事件驅動架構的基本模式:觀察者模式。

在這種模式中,被觀察者(主體)維護著一組其他對象派來(注冊)的觀察者,有新的對象對主體感興趣就注冊觀察者,不感興趣就取消訂閱,主體有更新會依次通知觀察者,使用方式如下。

const EventEmitter = require('events')

class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter()

function callback() {
    console.log('觸發(fā)了event事件!')
}
myEmitter.on('event', callback)
myEmitter.emit('event')
myEmitter.removeListener('event', callback);

在上面的代碼中,我們通過實例對象的on方法注冊一個名為event的事件,通過emit方法觸發(fā)該事件,而removeListener用于取消事件的監(jiān)聽。

除了上面介紹的一些方法外,其他常用的方法還有如下一些:

  • emitter.addListener/on(eventName, listener) :添加類型為 eventName 的監(jiān)聽事件到事件數(shù)組尾部。

  • emitter.prependListener(eventName, listener):添加類型為 eventName 的監(jiān)聽事件到事件數(shù)組頭部。

  • emitter.emit(eventName[, ...args]):觸發(fā)類型為 eventName 的監(jiān)聽事件。

  • emitter.removeListener/off(eventName, listener):移除類型為 eventName 的監(jiān)聽事件。

  • emitter.once(eventName, listener):添加類型為 eventName 的監(jiān)聽事件,以后只能執(zhí)行一次并刪除。

  • emitter.removeAllListeners([eventName]): 移除全部類型為 eventName 的監(jiān)聽事件。

7.3 實現(xiàn)原理

EventEmitter其實是一個構造函數(shù),內部存在一個包含所有事件的對象。

class EventEmitter {
    constructor() {
        this.events = {};
    }
}

其中,events存放的監(jiān)聽事件的函數(shù)的結構如下:

{
  "event1": [f1,f2,f3],
  "event2": [f4,f5],
  ...
}

然后,開始一步步實現(xiàn)實例方法,首先是emit,第一個參數(shù)為事件的類型,第二個參數(shù)開始為觸發(fā)事件函數(shù)的參數(shù),實現(xiàn)如下:

emit(type, ...args) {
    this.events[type].forEach((item) => {
        Reflect.apply(item, this, args);
    });
}

實現(xiàn)了emit方法之后,然后依次實現(xiàn)on、addListener、prependListener這三個實例方法,它們都是添加事件監(jiān)聽觸發(fā)函數(shù)的。

on(type, handler) {
    if (!this.events[type]) {
        this.events[type] = [];
    }
    this.events[type].push(handler);
}

addListener(type,handler){
    this.on(type,handler)
}

prependListener(type, handler) {
    if (!this.events[type]) {
        this.events[type] = [];
    }
    this.events[type].unshift(handler);
}

移除事件監(jiān)聽,可以使用方法removeListener/on。

removeListener(type, handler) {
    if (!this.events[type]) {
        return;
    }
    this.events[type] = this.events[type].filter(item => item !== handler);
}

off(type,handler){
    this.removeListener(type,handler)
}

實現(xiàn)once方法, 再傳入事件監(jiān)聽處理函數(shù)的時候進行封裝,利用閉包的特性維護當前狀態(tài),通過fired屬性值判斷事件函數(shù)是否執(zhí)行過。

once(type, handler) {
    this.on(type, this._onceWrap(type, handler, this));
  }

  _onceWrap(type, handler, target) {
    const state = { fired: false, handler, type , target};
    const wrapFn = this._onceWrapper.bind(state);
    state.wrapFn = wrapFn;
    return wrapFn;
  }

  _onceWrapper(...args) {
    if (!this.fired) {
      this.fired = true;
      Reflect.apply(this.handler, this.target, args);
      this.target.off(this.type, this.wrapFn);
    }
 }

下面是完成的測試代碼:

class EventEmitter {
    constructor() {
        this.events = {};
    }

    on(type, handler) {
        if (!this.events[type]) {
            this.events[type] = [];
        }
        this.events[type].push(handler);
    }

    addListener(type,handler){
        this.on(type,handler)
    }

    prependListener(type, handler) {
        if (!this.events[type]) {
            this.events[type] = [];
        }
        this.events[type].unshift(handler);
    }

    removeListener(type, handler) {
        if (!this.events[type]) {
            return;
        }
        this.events[type] = this.events[type].filter(item => item !== handler);
    }

    off(type,handler){
        this.removeListener(type,handler)
    }

    emit(type, ...args) {
        this.events[type].forEach((item) => {
            Reflect.apply(item, this, args);
        });
    }

    once(type, handler) {
        this.on(type, this._onceWrap(type, handler, this));
    }

    _onceWrap(type, handler, target) {
        const state = { fired: false, handler, type , target};
        const wrapFn = this._onceWrapper.bind(state);
        state.wrapFn = wrapFn;
        return wrapFn;
    }

    _onceWrapper(...args) {
        if (!this.fired) {
            this.fired = true;
            Reflect.apply(this.handler, this.target, args);
            this.target.off(this.type, this.wrapFn);
        }
    }
}

八、中間件

8.1 基本概念

中間件(Middleware)是介于應用系統(tǒng)和系統(tǒng)軟件之間的一類軟件,它使用系統(tǒng)軟件所提供的基礎服務(功能),銜接網(wǎng)絡上應用系統(tǒng)的各個部分或不同的應用,能夠達到資源共享、功能共享的目的。 在Node中,中間件主要是指封裝http請求細節(jié)處理的方法。例如,在express、koa等web框架中,中間件的本質為一個回調函數(shù),參數(shù)包含請求對象、響應對象和執(zhí)行下一個中間件的函數(shù),架構示意圖如下。

基于Node.js的前端面試題有哪些

通常,在這些中間件函數(shù)中,我們可以執(zhí)行業(yè)務邏輯代碼,修改請求和響應對象、返回響應數(shù)據(jù)等操作。

8.2 koa

Koa是基于Node當前比較流行的web框架,本身支持的功能并不多,功能都可以通過中間件拓展實現(xiàn)。 Koa 并沒有捆綁任何中間件, 而是提供了一套優(yōu)雅的方法,幫助開發(fā)者快速而愉快地編寫服務端應用程序。

基于Node.js的前端面試題有哪些

Koa 中間件采用的是洋蔥圈模型,每次執(zhí)行下一個中間件都傳入兩個參數(shù):

  • ctx :封裝了request 和 response 的變量

  • next :進入下一個要執(zhí)行的中間件的函數(shù)

通過前面的介紹,我們知道了Koa 中間件本質上就是一個函數(shù),可以是 async 函數(shù),也可以是普通函數(shù)。下面就針對koa進行中間件的封裝:

// async 函數(shù)
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

// 普通函數(shù)
app.use((ctx, next) => {
  const start = Date.now();
  return next().then(() => {
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  });
});

當然,我們還可以通過中間件封裝http請求過程中幾個常用的功能:

token校驗

module.exports = (options) => async (ctx, next) {
  try {
    // 獲取 token
    const token = ctx.header.authorization
    if (token) {
      try {
          // verify 函數(shù)驗證 token,并獲取用戶相關信息
          await verify(token)
      } catch (err) {
        console.log(err)
      }
    }
    // 進入下一個中間件
    await next()
  } catch (err) {
    console.log(err)
  }
}

日志模塊

const fs = require('fs')
module.exports = (options) => async (ctx, next) => {
  const startTime = Date.now()
  const requestTime = new Date()
  await next()
  const ms = Date.now() - startTime;
  let logout = `${ctx.request.ip} -- ${requestTime} -- ${ctx.method} -- ${ctx.url} -- ${ms}ms`;
  // 輸出日志文件
  fs.appendFileSync('./log.txt', logout + '\n')
}

Koa存在很多第三方的中間件,如koa-bodyparser、koa-static等。

8.3 Koa中間件

koa-bodyparserkoa-bodyparser 中間件是將我們的 post 請求和表單提交的查詢字符串轉換成對象,并掛在 ctx.request.body 上,方便我們在其他中間件或接口處取值。

// 文件:my-koa-bodyparser.js
const querystring = require("querystring");

module.exports = function bodyParser() {
    return async (ctx, next) => {
        await new Promise((resolve, reject) => {
            // 存儲數(shù)據(jù)的數(shù)組
            let dataArr = [];

            // 接收數(shù)據(jù)
            ctx.req.on("data", data => dataArr.push(data));

            // 整合數(shù)據(jù)并使用 Promise 成功
            ctx.req.on("end", () => {
                // 獲取請求數(shù)據(jù)的類型 json 或表單
                let contentType = ctx.get("Content-Type");

                // 獲取數(shù)據(jù) Buffer 格式
                let data = Buffer.concat(dataArr).toString();

                if (contentType === "application/x-www-form-urlencoded") {
                    // 如果是表單提交,則將查詢字符串轉換成對象賦值給 ctx.request.body
                    ctx.request.body = querystring.parse(data);
                } else if (contentType === "applaction/json") {
                    // 如果是 json,則將字符串格式的對象轉換成對象賦值給 ctx.request.body
                    ctx.request.body = JSON.parse(data);
                }

                // 執(zhí)行成功的回調
                resolve();
            });
        });

        // 繼續(xù)向下執(zhí)行
        await next();
    };
};

koa-statickoa-static 中間件的作用是在服務器接到請求時,幫我們處理靜態(tài)文件,比如。

const fs = require("fs");
const path = require("path");
const mime = require("mime");
const { promisify } = require("util");

// 將 stat 和 access 轉換成 Promise
const stat = promisify(fs.stat);
const access = promisify(fs.access)

module.exports = function (dir) {
    return async (ctx, next) => {
        // 將訪問的路由處理成絕對路徑,這里要使用 join 因為有可能是 /
        let realPath = path.join(dir, ctx.path);

        try {
            // 獲取 stat 對象
            let statObj = await stat(realPath);

            // 如果是文件,則設置文件類型并直接響應內容,否則當作文件夾尋找 index.html
            if (statObj.isFile()) {
                ctx.set("Content-Type", `${mime.getType()};charset=utf8`);
                ctx.body = fs.createReadStream(realPath);
            } else {
                let filename = path.join(realPath, "index.html");

                // 如果不存在該文件則執(zhí)行 catch 中的 next 交給其他中間件處理
                await access(filename);

                // 存在設置文件類型并響應內容
                ctx.set("Content-Type", "text/html;charset=utf8");
                ctx.body = fs.createReadStream(filename);
            }
        } catch (e) {
            await next();
        }
    }
}

總的來說,在實現(xiàn)中間件時候,單個中間件應該足夠簡單,職責單一,中間件的代碼編寫應該高效,必要的時候通過緩存重復獲取數(shù)據(jù)。

九、如何設計并實現(xiàn)JWT鑒權

9.1 JWT是什么

JWT(JSON Web Token),本質就是一個字符串書寫規(guī)范,作用是用來在用戶和服務器之間傳遞安全可靠的,如下圖。

基于Node.js的前端面試題有哪些

在目前前后端分離的開發(fā)過程中,使用token鑒權機制用于身份驗證是最常見的方案,流程如下:

  • 服務器當驗證用戶賬號和密碼正確的時候,給用戶頒發(fā)一個令牌,這個令牌作為后續(xù)用戶訪問一些接口的憑證。

  • 后續(xù)訪問會根據(jù)這個令牌判斷用戶時候有權限進行訪問。

Token,分成了三部分,頭部(Header)、載荷(Payload)、簽名(Signature),并以.進行拼接。其中頭部和載荷都是以JSON格式存放數(shù)據(jù),只是進行了編碼,示意圖如下。

基于Node.js的前端面試題有哪些

9.1.1 header

每個JWT都會帶有頭部信息,這里主要聲明使用的算法。聲明算法的字段名為alg,同時還有一個typ的字段,默認JWT即可。以下示例中算法為HS256:

{  "alg": "HS256",  "typ": "JWT" }

因為JWT是字符串,所以我們還需要對以上內容進行Base64編碼,編碼后字符串如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

9.1.2 payload

載荷即消息體,這里會存放實際的內容,也就是Token的數(shù)據(jù)聲明,例如用戶的id和name,默認情況下也會攜帶令牌的簽發(fā)時間iat,通過還可以設置過期時間,如下:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

同樣進行Base64編碼后,字符串如下:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

9.1.3 Signature

簽名是對頭部和載荷內容進行簽名,一般情況,設置一個secretKey,對前兩個的結果進行HMACSHA25算法,公式如下:

Signature = HMACSHA256(base64Url(header)+.+base64Url(payload),secretKey)

因此,就算前面兩部分數(shù)據(jù)被篡改,只要服務器加密用的密鑰沒有泄露,得到的簽名肯定和之前的簽名也是不一致的。

9.2 設計實現(xiàn)

通常,Token的使用分成了兩部分:生成token和校驗token。

  • 生成token:登錄成功的時候,頒發(fā)token。

  • 驗證token:訪問某些資源或者接口時,驗證token。

9.2.1 生成 token

借助第三方庫jsonwebtoken,通過jsonwebtoken 的 sign 方法生成一個 token。sign有三個參數(shù):

  • 第一個參數(shù)指的是 Payload。

  • 第二個是秘鑰,服務端特有。

  • 第三個參數(shù)是 option,可以定義 token 過期時間。

下面是一個前端生成token的例子:

const crypto = require("crypto"),
  jwt = require("jsonwebtoken");
// TODO:使用數(shù)據(jù)庫
// 這里應該是用數(shù)據(jù)庫存儲,這里只是演示用
let userList = [];

class UserController {
  // 用戶登錄
  static async login(ctx) {
    const data = ctx.request.body;
    if (!data.name || !data.password) {
      return ctx.body = {
        code: "000002", 
        message: "參數(shù)不合法"
      }
    }
    const result = userList.find(item => item.name === data.name && item.password === crypto.createHash('md5').update(data.password).digest('hex'))
    if (result) {
      // 生成token
      const token = jwt.sign(  
        {
          name: result.name
        },
        "test_token", // secret
        { expiresIn: 60 * 60 } // 過期時間:60 * 60 s
      );
      return ctx.body = {
        code: "0",
        message: "登錄成功",
        data: {
          token
        }
      };
    } else {
      return ctx.body = {
        code: "000002",
        message: "用戶名或密碼錯誤"
      };
    }
  }
}

module.exports = UserController;

在前端接收到token后,一般情況會通過localStorage進行緩存,然后將token放到HTTP 請求頭Authorization 中,關于Authorization 的設置,前面需要加上 Bearer ,注意后面帶有空格,如下。

axios.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  config.headers.common['Authorization'] = 'Bearer ' + token; // 留意這里的 Authorization
  return config;
})

9.2.2 校驗token

首先,我們需要使用 koa-jwt 中間件進行驗證,方式比較簡單,在路由跳轉前校驗即可,如下。

app.use(koajwt({
  secret: 'test_token'
}).unless({ 
   // 配置白名單
  path: [/\/api\/register/, /\/api\/login/]
}))

使用koa-jwt中間件進行校驗時,需要注意以下幾點:

  • secret 必須和 sign 時候保持一致。

  • 可以通過 unless 配置接口白名單,也就是哪些 URL 可以不用經(jīng)過校驗,像登陸/注冊都可以不用校驗。

  • 校驗的中間件需要放在需要校驗的路由前面,無法對前面的 URL 進行校驗。

獲取用戶token信息的方法如下:

router.get('/api/userInfo',async (ctx,next) =>{   
 const authorization =  ctx.header.authorization // 獲取jwt  
 const token = authorization.replace('Beraer ','')    
 const result = jwt.verify(token,'test_token')  
 ctx.body = result
}

注意:上述的HMA256加密算法為單秘鑰的形式,一旦泄露后果非常的危險。

在分布式系統(tǒng)中,每個子系統(tǒng)都要獲取到秘鑰,那么這個子系統(tǒng)根據(jù)該秘鑰可以發(fā)布和驗證令牌,但有些服務器只需要驗證令牌。這時候可以采用非對稱加密,利用私鑰發(fā)布令牌,公鑰驗證令牌,加密算法可以選擇RS256等非對稱算法。

除此之外,JWT鑒權還需要注意以下幾點:

  • payload部分僅僅是進行簡單編碼,所以只能用于存儲邏輯必需的非敏感信息。

  • 需要保護好加密密鑰,一旦泄露后果不堪設想。

  • 為避免token被劫持,最好使用https協(xié)議。

十、Node性能監(jiān)控與優(yōu)化

10.1 Node優(yōu)化點

Node作為一門服務端語言,性能方面尤為重要,其衡量指標一般有如下幾點:

  • CPU

  • 內存

  • I/O

  • 網(wǎng)絡

10.1.1 CPU

對于CPU的指標,主要關注如下兩點:

  • CPU負載:在某個時間段內,占用以及等待CPU的進程總數(shù)。

  • CPU使用率:CPU時間占用狀況,等于 1 - 空閑CPU時間(idle time) / CPU總時間。

這兩個指標都是用來評估系統(tǒng)當前CPU的繁忙程度的量化指標。Node應用一般不會消耗很多的CPU,如果CPU占用率高,則表明應用存在很多同步操作,導致異步任務回調被阻塞。

10.1.2 內存指標

內存是一個非常容易量化的指標。 內存占用率是評判一個系統(tǒng)的內存瓶頸的常見指標。 對于Node來說,內部內存堆棧的使用狀態(tài)也是一個可以量化的指標,可以使用下面的代碼來獲取內存的相關數(shù)據(jù):

// /app/lib/memory.js
const os = require('os');
// 獲取當前Node內存堆棧情況
const { rss, heapUsed, heapTotal } = process.memoryUsage();
// 獲取系統(tǒng)空閑內存
const sysFree = os.freemem();
// 獲取系統(tǒng)總內存
const sysTotal = os.totalmem();

module.exports = {
  memory: () => {
    return {
      sys: 1 - sysFree / sysTotal,  // 系統(tǒng)內存占用率
      heap: heapUsed / headTotal,   // Node堆內存占用率
      node: rss / sysTotal,         // Node占用系統(tǒng)內存的比例
    }
  }
}
  • rss:表示node進程占用的內存總量。

  • heapTotal:表示堆內存的總量。

  • heapUsed:實際堆內存的使用量。

  • external :外部程序的內存使用量,包含Node核心的C++程序的內存使用量。

在Node中,一個進程的最大內存容量為1.5GB,因此在實際使用時請合理控制內存的使用。

10.13 磁盤 I/O

硬盤的 IO 開銷是非常昂貴的,硬盤 IO 花費的 CPU 時鐘周期是內存的 164000 倍。內存 IO 比磁盤 IO 快非常多,所以使用內存緩存數(shù)據(jù)是有效的優(yōu)化方法。常用的工具如 redis、memcached 等。

并且,并不是所有數(shù)據(jù)都需要緩存,訪問頻率高,生成代價比較高的才考慮是否緩存,也就是說影響你性能瓶頸的考慮去緩存,并且而且緩存還有緩存雪崩、緩存穿透等問題要解決。

10.2 如何監(jiān)控

關于性能方面的監(jiān)控,一般情況都需要借助工具來實現(xiàn),比如Easy-Monitor、阿里Node性能平臺等。

這里采用Easy-Monitor 2.0,其是輕量級的 Node.js 項目內核性能監(jiān)控 + 分析工具,在默認模式下,只需要在項目入口文件 require 一次,無需改動任何業(yè)務代碼即可開啟內核級別的性能監(jiān)控分析。

Easy-Monitor 的使用也比較簡單,在項目入口文件中按照如下方式引入。

const easyMonitor = require('easy-monitor');
easyMonitor('項目名稱');

打開你的瀏覽器,訪問 http://localhost:12333 ,即可看到進程界面,更詳細的內容請參考官網(wǎng)

10.3 Node性能優(yōu)化

關于Node的性能優(yōu)化的方式有如下幾個:

  • 使用最新版本Node.js

  • 正確使用流 Stream

  • 代碼層面優(yōu)化

  • 內存管理優(yōu)化

10.3.1 使用最新版本Node.js

每個版本的性能提升主要來自于兩個方面:

  • V8 的版本更新

  • Node.js 內部代碼的更新優(yōu)化

10.3.2 正確使用流

在Node中,很多對象都實現(xiàn)了流,對于一個大文件可以通過流的形式發(fā)送,不需要將其完全讀入內存。

const http = require('http');
const fs = require('fs');

// 錯誤方式
http.createServer(function (req, res) {
    fs.readFile(__dirname + '/data.txt', function (err, data) {
        res.end(data);
    });
});

// 正確方式
http.createServer(function (req, res) {
    const stream = fs.createReadStream(__dirname + '/data.txt');
    stream.pipe(res);
});

10.3.3 代碼層面優(yōu)化

合并查詢,將多次查詢合并一次,減少數(shù)據(jù)庫的查詢次數(shù)。

// 錯誤方式
for user_id in userIds 
     let account = user_account.findOne(user_id)

// 正確方式
const user_account_map = {}  
 // 注意這個對象將會消耗大量內存。
user_account.find(user_id in user_ids).forEach(account){
    user_account_map[account.user_id] =  account
}
for user_id in userIds 
    var account = user_account_map[user_id]

10.3.4 內存管理優(yōu)化

在 V8 中,主要將內存分為新生代和老生代兩代:

  • 新生代:對象的存活時間較短。新生對象或只經(jīng)過一次垃圾回收的對象。

  • 老生代:對象存活時間較長。經(jīng)歷過一次或多次垃圾回收的對象。

若新生代內存空間不夠,直接分配到老生代。通過減少內存占用,可以提高服務器的性能。如果有內存泄露,也會導致大量的對象存儲到老生代中,服務器性能會大大降低,比如下面的例子。

const buffer = fs.readFileSync(__dirname + '/source/index.htm');

app.use(
    mount('/', async (ctx) => {
        ctx.status = 200;
        ctx.type = 'html';
        ctx.body = buffer;
        leak.push(fs.readFileSync(__dirname + '/source/index.htm'));
    })
);

const leak = [];

當leak的內存非常大的時候,就有可能造成內存泄露,應當避免這樣的操作。

減少內存使用,可以明顯的提高服務性能。而節(jié)省內存最好的方式是使用池,其將頻用、可復用對象存儲起來,減少創(chuàng)建和銷毀操作。例如有個圖片請求接口,每次請求,都需要用到類。若每次都需要重新new這些類,并不是很合適,在大量請求時,頻繁創(chuàng)建和銷毀這些類,造成內存抖動。而使用對象池的機制,對這種頻繁需要創(chuàng)建和銷毀的對象保存在一個對象池中,從而避免重讀的初始化操作,從而提高框架的性能。

以上是“基于Node.js的前端面試題有哪些”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。

AI