Node.js必知必會(安裝配置、應用實例及同步控制)

本文來自 悟塵紀,獲取更新內容可查看原文:https://www.lixl.cn/2020/011231581.html

一、Node.js簡介

Node.js 是一個基於 Chrome V8 引擎的 JavaScript 運行環境。使用了一個事件驅動、非阻塞式 I/O 的模型,使其輕量又高效。於2009年由Google Brain團隊的軟件工程師Ryan Dahl發起創建,2015年後正式被NodeJS基金會接管。

NodeJS 架構

Node.js架構

Node使用事件驅動模型,當web server接收到請求,就把它關閉然後進行處理,然後去服務下一個web請求。

當這個請求完成,它被放回處理隊列,當到達隊列開頭,這個結果被返回給用戶。

這個模型非常高效可擴展性非常強,因爲 webserver 一直接受請求而不等待任何讀寫操作。(這也稱之爲非阻塞式IO或者事件驅動IO)。在事件驅動模型中,會生成一個主循環來監聽事件,當檢測到事件時觸發回調函數。

相關資源

二、安裝配置

建議使用nvm來進行node版本管理,它會安裝相應版本的npm。

安裝nvm及Node.js

nvm全名node.js version management,顧名思義是一個nodejs的版本管理工具。通過它可以安裝和切換不同版本的nodejs。

# 安裝nvm( 升級nvm重新執行此命令):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash
# 列出所有可以安裝的node版本號
nvm ls-remote
# 安裝指定版本號的node
nvm install v12.4.1
# 設置 nodejs 默認版本
nvm alias default 12.4.1
# 切換node的版本
nvm use v10.15.3
# 當前node版本
nvm current
node -v
# 列出所有已經安裝的node版本
nvm ls
# 卸載已安裝的node版本
nvm uninstall v6.9.5

npm

npm 是世界上最大的軟件註冊中心,隨同NodeJS一起安裝,來自全球各地的開源開發人員使用 npm 來共享和複用軟件包。npm 由三個獨立的部分組成:

  • 網站: https://npmjs.com 是開發者查找包(package)、設置參數以及管理 npm 使用體驗的主要途徑。
  • 註冊表(registry):是一個巨大的數據庫,保存了每個包(package)的信息。
  • 命令行工具 (CLI):通過命令行或終端運行。開發者通過 CLI 與 npm 打交道。
# 查看 npm 版本
npm -v
# 更新npm版本
npm install npm@latest -g
# 搜索模塊
npm search hexo
# 安裝依賴包
npm install <Module Name>
npm install hexo      # 本地安裝 hexo
npm install hexo -g   # 全局安裝 hexo
# 查看所有全局安裝的模塊
npm list -g
# 查看某個模塊
npm list hexo
# 卸載模塊
npm uninstall hexo
# 卸載後,查看包是否還存在
npm ls
# 更新某個模塊
npm update hexo
# 創建模塊
npm init

每個版本的 Node 都自帶一個不同版本的 npm,可以用 npm -v 來查看 npm 的版本。全局安裝的 npm 包並不會在不同的 Node 環境中共享,因爲這會引起兼容問題。它們被放在了不同版本的目錄下,例如 ~/.nvm/versions/node/${version}/lib/node_modules 這樣的目錄。

運行下面這個命令,可以從特定版本導入之前安裝過的 npm 包到我們將要安裝的新版本 Node 中:

nvm install v12.16.1 --reinstall-packages-from=v10.15.3

三、Node特點

異步I/O

在Node中,絕大多數的操作都以異步的方式進行調用。在底層構建了很多異步I/O的API,從文件讀取到網絡請求等,均是如此。這樣的意義在於,在Node中,我們可以從語言層面很自然地進行並行I/O操作。每個調用之間無須等待之前的I/O調用結束。在編程模型上可以極大提升效率。
下面的兩個文件讀取任務的耗時取決於最慢的那個文件讀取的耗時:

fs.readFile('/path1', function (err, file) {
  console.log('讀取文件1完成');
});
fs.readFile('/path2', function (err, file) {
  console.log('讀取文件2完成');
});

而對於同步I/O而言,它們的耗時是兩個任務的耗時之和。這裏異步帶來的優勢是顯而易見的。

事件與回調函數

在JavaScript中,函數被作爲第一等公民來對待,可以將函數作爲對象傳遞給方法作爲實參進行調用。Node將前端瀏覽器中應用廣泛且成熟的事件引入後端,配合異步I/O,將事件點暴露給業務邏輯。

下面的例子展示的是Ajax異步提交的服務器端處理過程。Node創建一個Web服務器,並偵聽8080端口。對於服務器,我們爲其綁定了request事件,對於請求對象,我們爲其綁定了data事件和end事件:

var http = require('http');
var quertstring = require('querystring');

http.createServer(function(req,res){
  var postData = '';
  req.setEncoding('utf8');
  
  // 監聽請求的data事件
  req.on('data',function(chunk){
    postData += chunk;
  });

  // 監聽請求的end事件
  req.on('end', function(){
    res.end(postData);
  });
}).listen(8080);
console.log('服務器啓動完成,監聽端口:8080')

相應地,我們在前端爲Ajax請求綁定了success事件,在發出請求後,只需關心請求成功時執行相應的業務邏輯即可,相關代碼如下:

$.ajax({
  'url': '/url',
  'method': 'POST',
  'data': {},
  'success': function (data) {
    // success事件
  }
});

與其他的Web後端編程語言相比,Node除了異步和事件外,回調函數是一大特色。縱觀下來,回調函數也是最好的接受異步調用返回數據的方式。但是這種編程方式對於很多習慣同步思路編程的人來說,也許是十分不習慣的。代碼的編寫順序與執行順序並無關係,這對他們可能造成閱讀上的障礙。

單線程

Node保持了JavaScript在瀏覽器中單線程的特點。而且在Node中,JavaScript與其餘線程是無法共享任何狀態的。單線程的最大好處是不用像多線程編程那樣處處在意狀態的同步問題,這裏沒有死鎖的存在,也沒有線程上下文交換所帶來的性能上的開銷。
同樣,單線程也有它自身的弱點。Node採用了與Web Workers相同的思路來解決單線程中大計算量的問題:child_process。子進程的出現,意味着Node可以從容地應對單線程在健壯性和無法利用多核CPU方面的問題。

擅長I/O密集型的應用

通常,說Node擅長I/O密集型的應用場景基本上是沒人反對的。Node面向網絡且擅長並行I/O,能夠有效地組織起更多的硬件資源,從而提供更多好的服務。I/O密集的優勢主要在於Node利用事件循環的處理能力,而不是啓動每一個線程爲每一個請求服務,資源佔用極少。

性能不俗

CPU密集型應用給Node帶來的挑戰主要是:由於JavaScript單線程的原因,如果有長時間運行的計算(比如大循環),將會導致CPU時間片不能釋放,使得後續I/O無法發起。但是適當調整和分解大型運算任務爲多個小任務,使得運算能夠適時釋放,不阻塞I/O調用的發起,這樣既可同時享受到並行異步I/O的好處,又能充分利用CPU,I/O阻塞造成的性能浪費遠比CPU的影響小。

計算斐波那契數列的耗時排行

四、Node.js常用模塊

更多模塊詳細介紹,可查閱官方文檔: https://nodejs.org/api/

Global模塊

瀏覽器JavaScript當中window是全局對象,NodeJS中全局對象是global,global最根本的作用是作爲全局變量的宿主(即所有的全局變量都是global對象的屬性),因此在所有模塊中都可以直接使用而無需包含。

Process模塊

process是全局變量(即global對象的屬性),用於描述當前NodeJS進程狀態。

Console模塊

console用於提供控制檯標準輸出。

console.log():向標準輸出流打印字符並以換行符結束(如果只有1個參數,則輸出該參數的字符串形式;如果有2個參數,則以類似於C語言printf()的格式化輸出)。

console.error():與console.log()的用法相同,只是向標準錯誤流進行輸出。

console.trace():向標準錯誤流輸出當前的調用棧:

$ node app.js
Trace
    at Object.<anonymous> (/workspace/app.js:1:71)
    at Module._compile (module.js:643:30)
    at Object.Module._extensions..js (module.js:654:10)
    at Module.load (module.js:556:32)
    at tryModuleLoad (module.js:499:12)
    at Function.Module._load (module.js:491:3)
    at Function.Module.runMain (module.js:684:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

Util模塊

util提供常用函數集合,用於彌補核心JavaScript功能方面的不足。

Events模塊

events是NodeJS最重要的模塊,因爲NodeJS本身就是基於事件式的架構,該模塊提供了唯一接口,所以堪稱NodeJS事件編程的基石。events模塊不僅用於與下層的事件循環交互,還幾乎被所有的模塊所依賴。

events模塊只提供1個events.EventEmitter對象,EventEmitter對象封裝了事件發射和事件監聽器。每個EventEmitter事件由1個事件名和若干參數組成,事件名是1個字符串。EventEmitter對每個事件支持若干監聽器,事件發射時,註冊至該事件的監聽器依次被調用,事件參數將作爲回調函數參數傳遞。

下面例子中,emitter爲事件targetEvent註冊2個事件監聽器,然後發射targetEvent事件,結果2個事件監聽器的回調函數被依次先後調用。

var events = require("events");

var emitter = new events.EventEmitter();

emitter.on("targetEvent", function(arg1, arg2) {
  console.log("listener1", arg1, arg2);
});

emitter.on("targetEvent", function(arg1, arg2) {
  console.log("listener2", arg1, arg2);
});

emitter.emit("targetEvent", "Hank", 2018);
$ node app.js
listener1 Hank 2018
listener2 Hank 2018

EventEmitter常用API

  • EventEmitter.on(event, listener):爲指定事件註冊監聽器,接受1個字符串事件名event和1個回調函數listener。
  • EventEmitter.emit(event,[arg1],[arg2],[...]):發射event事件,傳遞若干可選參數到事件監聽器的參數列表。
  • EventEmitter.once(event, listener):爲指定事件註冊1個單次監聽器,即該監聽器最多隻會觸發一次,觸發後立刻解除。
  • EventEmitter.removeListener(event, listener):移除指定事件的某個監聽器,listener必須是該事件已經註冊過的監聽器。
  • EventEmitter.removeAllListeners([event]):移除所有事件的所有監聽器,如果指定event,則移除指定事件的所有監聽器。

File System模塊

fs模塊封裝了文件操作,提供了文件讀取、寫入、更名、刪除、遍歷、鏈接等POSIX文件系統操作,該模塊中所有操作都提供了異步和同步2個版本。

fs.readFile(filename,[encoding],[callback(err,data)])用於讀取文件,第1個參數filename表示要讀取的文件名。第2個參數encoding表示文件的字符編碼,第3個參數callback是回調函數,用於接收文件內容。

回調函數提供errdata兩個參數,err表示有無錯誤發生,data是文件內容。如果指定encodingdata將是1個解析後的字符串,否則data將會是以Buffer`形式表示的二進制數據。

fs.readFileSync()

NodeJS提供的fs.readFileSync()函數是readFile()的同步版本,兩者接受的參數相同,讀取到的文件內容會以函數返回值形式返回。如果有錯誤發生fs將會拋出異常,需要使用try...catch捕捉並處理異常。

與同步I/O函數不同,NodeJS中異步函數大多沒有返回值。

fs.open()

fs.open(path,flags,[mode],[callback(err,fd)])封裝了POSIX的open()函數,與C語言標準庫中fopen()函數類似。該函數接受2個必選參數,第1個參數path爲文件路徑,第2個參數flags代表文件打開模式,第3個參數mode用於創建文件時給文件指定權限(默認0666),第4個參數是回調函數,函數中需要傳遞文件描述符fd

fs.read()

fs.read(fd,buffer,offset,length,position,[callback(err,bytesRead,buffer)])封裝了POSIX的read函數,相比fs.readFile()提供了更底層的接口。

fs.read()的功能是從指定的文件描述符fd中讀取數據並寫入buffer指向的緩衝區對象。offsetbuffer的寫入偏移量。length是要從文件中讀取的字節數。position是文件讀取的起始位置,如果position的值爲null,則會從當前文件指針的位置讀取。回調函數傳遞bytesReadbuffer,分別表示讀取的字節數緩衝區對象

Http模塊

NodeJS標準庫提供的http模塊封裝了一個高效的HTTP服務器http.Server和一個簡易的HTTP客戶端http.request

http模塊中的HTTP服務器對象,核心由NodeJS底層依靠C++實現,接口使用JavaScript封裝,兼顧了高性能與簡易性。

五、創建Node.js應用

使用Node創建http服務器

使用 require 指令來載入 http 模塊,並將實例化的 HTTP 賦值給變量 http,實例如下:

var http = require("http");

使用 http.createServer() 方法創建服務器,並使用 listen 方法綁定 8888 端口。 函數通過 request, response 參數來接收和響應數據。在你項目的根目錄下創建一個叫 server.js 的文件,並寫入以下代碼:

const http = require('http');
const hostname = '127.0.0.1';
const port = 8080;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

// 終端打印如下信息
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

以上代碼我們完成了一個可以工作的 HTTP 服務器。使用 node 命令執行以上的代碼:

node server.js
Server running at http://127.0.0.1:8080/

打開瀏覽器訪問 http://127.0.0.1:8080/,會看到一個寫着 "Hello World"的網頁。

web框架express簡單使用

express 是 Node應用最廣泛的快速、開放、極簡主義 web 框架,現在是 4.x 版本。官方提供了應用程序生成器工具 express-generator 可以快速創建應用程序骨架。安裝:

npm install express --save
npm install express-generator -g

創建名稱爲 ExpressDemo 的 Express 應用。此應用將在當前目錄下的 ExpressDemo 目錄中創建,並且設置爲使用 Pug 模板引擎:

express --view=pug ExpressDemo

   create : ExpressDemo/
   create : ExpressDemo/public/
   create : ExpressDemo/public/javascripts/
   create : ExpressDemo/public/images/
   create : ExpressDemo/public/stylesheets/
   create : ExpressDemo/public/stylesheets/style.css
   create : ExpressDemo/routes/
   create : ExpressDemo/routes/index.js
   create : ExpressDemo/routes/users.js
   create : ExpressDemo/views/
   create : ExpressDemo/views/error.pug
   create : ExpressDemo/views/index.pug
   create : ExpressDemo/views/layout.pug
   create : ExpressDemo/app.js
   create : ExpressDemo/package.json
   create : ExpressDemo/bin/
   create : ExpressDemo/bin/www

   change directory:
     $ cd ExpressDemo

   install dependencies:
     $ npm install

   run the app:
     $ DEBUG=expressdemo:* npm start

按提示安裝依賴並啓動。

cd ExpressDemo
npm install
DEBUG=expressdemo:* npm start  # MacOS

在瀏覽器中打開 http://localhost:3000/ 就可以看到這個應用了。

通過生成器創建的應用一般都有如下目錄結構:

tree -I "node_modules"
.
├── app.js
├── bin
│   └── www
├── package-lock.json
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.pug
    ├── index.pug
    └── layout.pug

7 directories, 10 files

六、異步編程方案(Promise & Async)

異步是Node得天獨厚的特點和優勢,但我們經常還是會需要解決同步執行的場景。如方法A執行完纔可以執行方法B。如下面這個例子:

setTimeout(() => {
    console.log('A')
}, 3000)
console.log('B');

執行結果爲:

[Running] node "test.js"
B
A
[Done] exited with code=0 in 3.12 seconds

如果想要輸出結果爲 A B,可以採取 PromiseAsync 來實現。

基於Promise實現同步控制

Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。ES6 原生提供了Promise對象,提供統一的 API,各種異步操作都可以用同樣的方法進行處理。示例如下:

var f = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('A');
        resolve();
    }, 3000);
});

f.then(() => {
    console.log('B');
}, (err) => {
    console.log('Err');
});

執行結果:

[Running] node "test.js"
A
B
[Done] exited with code=0 in 3.123 seconds

可以把 Promise 對象比喻爲一個容器,裏面有一個異步操作,Promise 容器只有在收到信號(resolve或者reject)時纔會調用then方法。通過 Promise.All方法將多個Promise對象實例包裝,生成並返回一個新的Promise實例,等執行完所有異步操作之後執行then方法:

const a = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('A');
    resolve();
  }, 3000);
});

const b = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('B');
    resolve();
  }, 3000);
});


Promise.all([a, b]).then(() => {
  console.log('end');
});

執行結果如下:

[Running] node test.js
A
B
end

[Done] exited with code=0 in 3.122 seconds

Promise 擴展信息

Promise 構造函數接受一個函數作爲參數,該函數兩個參數分別是resolvereject。它們是兩個函數,由 avaScript 引擎提供。resolve 函數在異步操作成功時用,其作用是將Promise對象的狀態從“pending”變爲resolved”,並將異步操作的結果作爲參數傳遞出去;reject函數在異步操作失敗時調用,其作用是將Promise`對象的狀態從pending”變爲“rejected”,並將異步操作報出的錯誤作爲參傳遞出去。

Promise實例生成以後,可以用then方法分別指定resolved狀態和rejected狀態的回調函數。then方法以接受兩個回調函數作爲參數,第一個回調函數是Promise對的狀態變爲resolved時調用,第二個回調函數(可選提供)是Promise對象的狀態變爲rejected時調用。這兩個函數都受Promise對象傳出的值作爲參數。

Promise狀態轉換圖

Promise對象有以下兩個特點。

  • 對象的狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。
  • 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected。只要這兩種情況發生,狀態就會一直保持不再改變,稱爲 resolved。

下面是一個Promise對象的簡單例子。

function f() {
  return new Promise(function(resolve, reject) {
    setTimeout(resolve, 3000, 'done.');
    console.log('A');  //會立即執行
  });
}
f().then(function (result) {
  console.log(`resolve result: ${result}`);
}),function(error){
  console.log(`reject error: ${error}`);
};

上面代碼中,f1方法返回一個Promise實例,表示一段時以後纔會發生的結果。過了指定的時間(3000毫秒)以後,Promise實例的狀態爲resolved,就會觸發then`方法定的回調函數。執行結果如下:

[Running] node "test.js"
A
resolve result: done.
[Done] exited with code=0 in 3.175 seconds

另外,resolve函數的參數除了正常的值以外,還可能是另一個 Promise 實例。then方法是定義在原型對象Promise.prototype上的。返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。因此可以採用鏈式寫法,即then方法後面再調用另一個then方法。第一個回調函數完成以後,會將返回結果作爲參數,傳入第二個回調函數。

Promise也有一些缺點。首先是無法取消,一旦新建它就會立即執行,無法中途取消;其次,如果不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。第三,當處於pending狀態時,無法得知目前進展到哪一個階段。

基於Async實現同步控制

ES2017 標準引入了 async 函數,使得異步操作變得更加方便。隨着Node.js 8的發佈,期待已久的async函數也在其中默認實現了。async 函數的實現原理,是將 Generator 函數和自動執行器,包裝在一個函數裏。

async函數返回一個 Promise 對象,可以使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,必須等到內部所有await命令後面的 Promise 對象執行完,纔會發生狀態改變(除非遇到return語句或者拋出錯誤),再接着執行函數體內後面的語句。看下面這個例子:

function f() {
  return new Promise((resolve) => {
    setTimeout(resolve, 3000, 'Hello');
    console.log('A');
  });
}

async function asyncF(value) {  //前面的 `async` 關鍵字,表明該函數內部有異步操作。
  console.log('B');
  await f().then(value => console.log(value));
  console.log('C');
  return value; // return語句的返回值,會成爲`then`方法回調函數的參數。
}

asyncF('world').then(result => console.log(result));

執行結果如下:

[Running] node "test.js"
B      #立即輸出
A      #立即輸出
Hello  #3秒後輸出
C
world
[Done] exited with code=0 in 3.165 seconds

async 函數的await命令後面,可以是 Promise 對象或原始類型的值(數值、字符串和布爾值,但會自動轉成立即 resolved 的 Promise 對象)。

sync函數內部拋出錯誤,會導致返回的 Promise 對象變爲reject狀態。拋出的錯誤對象會被catch方法回調函數接收到。如下面這個例子:

async function f() {
throw new Error('發生異常');
}

f().then(
result => console.log(`resolve result: ${result}`),
error => console.log(error)
);

執行結果:

[Running] node "test.js"
Error: 發生異常
 at f (/Users/lixl.cn/nodework/blog/test.js:4:9)
 at Object.<anonymous> (/Users/lixl.cn/nodework/blog/test.js:7:1)
 at Module._compile (internal/modules/cjs/loader.js:701:30)
 at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
 at Module.load (internal/modules/cjs/loader.js:600:32)
 at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
 at Function.Module._load (internal/modules/cjs/loader.js:531:3)
 at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
 at startup (internal/bootstrap/node.js:283:19)
 at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)

[Done] exited with code=0 in 0.153 seconds

七、基於ESLint保障質量

JavaScript 是一個動態的弱類型語言,在開發中比較容易出錯,一般會藉助 Lint 工具來保障質量。

ESLint 是新一代開源 JavaScript 代碼檢查工具,使用 Node.js 編寫,常用於尋找有問題的模式或者代碼,並且不依賴於具體的編碼風格。

# 全局安裝 ESLint
npm install -g eslint

# 進入項目
cd ~/NodeWork/NodeDemo

# 初始化 package.json
npm init -f

# 初始化 ESLint 配置
eslint --init

通過 Lint 工具可以讓我們:

  • 避免低級bug,找出可能發生的語法錯誤
  • 提示刪除多餘的代碼
  • 確保代碼遵循最佳實踐 (可參考 airbnb stylejavascript standard)
  • 統一團隊的代碼風格

項目初始化完畢,可以開始在 ESLint 的提示下,高質量編寫代碼了。

八、參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章