1. 程式人生 > 實用技巧 >伺服器端基礎概念

伺服器端基礎概念

1 伺服器端基礎概念

1.1 網站的組成

網站應用程式主要分為兩大部分:客戶端和伺服器端。

客戶端:在瀏覽器中執行的部分,就是使用者看到並與之互動的介面程式,使用HTML、CSS、JavaScript構建。

伺服器端:在伺服器中執行的部分,負責儲存資料和處理應用邏輯。

1.2 Node 網站伺服器

能夠提供網站訪問服務的機器就是網站伺服器,它能夠接受客戶端的請求,能夠對請求作出響應

1.3 IP地址

網際網路中裝置的唯一標識。

IP是Internet Protocol Address 的縮寫,程式碼網際網路協議地址。

1.4 域名

由於 IP 地址難於記憶,所以就產生了域名的概念,所謂域名就是平時上網所使用的網址。

雖然在位址列中輸入的是網址,但是最終還是會將域名轉換為 ip 才能訪問到指定的網站伺服器。

1.5 埠

埠是計算機與外界通訊交流的出口,用來區分伺服器電腦中提供的不同服務。

1.6 URL

統一資源定位符,又叫URL(Uniform Resource Location),是專為標識 Internet 網上資源位置而設的一種編址方式。
我們平時所說的網頁地址指的即是URL。

URL 的組成:

傳輸協議://伺服器IP或域名:埠/資源所在位置標識
https://www.cnblogs.com/joe235/p/12745332.html

http: 超文字傳輸協議,提供了一種釋出和接收 HTML 頁面的方法。
https: 是以安全為目標的 HTTP 通道,在HTTP的基礎上通過傳輸加密和身份認證保證了傳輸過程的安全性。

1.7 開發過程中客戶端和伺服器端說明

本機域名:localhost
本地IP:127.0.0.1

2 建立 web 伺服器

示例程式碼:

// 引用系統模組
const http = require('http');
// 建立 web 伺服器
const app = http.createServer();
// 當客戶端傳送請求的時候
app.on('request', (req, res) => {
  // 響應
  res.end('<h1>hi,user</h1>');
});
// 監聽3000埠
app.listen(3000);
console.log('伺服器已啟動,監聽3000埠,請訪問locathost:3000')

例子:

1.新建專案 server,並建立 app.js 檔案:

// 用於建立網站伺服器的模組
const http = require('http');
// 建立 web 伺服器,app 物件就是網站伺服器物件
const app = http.createServer();
// 當客戶端有請求來的時候
app.on('request', (req, res) => {
  // 響應
  res.end('<h2>hello user</h2>');
});
// 監聽3000埠
app.listen(3000);
console.log('網站伺服器已啟動,監聽3000埠,請訪問localhost:3000')

2.回到命令列工具,切換到 server 目錄下,輸入:

nodemon app.js

3.開啟瀏覽器,輸入localhost:3000

可以看到瀏覽器上顯示:

3 HTTP 協議

3.1 HTTP 協議的概念

超文字傳輸協議HTTP(HyPer Text Transfer Protocol)規定了如何從網站伺服器傳輸超文字到本地瀏覽器。它基於客戶端伺服器架構工作,是客戶端(使用者)和伺服器端(網站)請求和應答的標準。

3.2 報文

在 HTTP 請求和響應的過程中傳遞的資料塊就叫報文,包括要傳送的資料和一些附加資訊,並且要遵循規定好的格式。

3.3 請求報文

1、請求方式(Resquest Method):

  GET 請求資料

  POST 傳送資料

例子:

在server 專案下的 app.js 中新增 req 程式碼:

// 用於建立網站伺服器的模組
const http = require('http');
// 建立 web 伺服器,app 物件就是網站伺服器物件
const app = http.createServer();
// 當客戶端有請求來的時候
app.on('request', (req, res) => {
  // 獲取請求方式
  // req.method
  console.log(req.method);
  // 響應
  res.end('<h2>hello user</h2>');
});
// 監聽3000埠
app.listen(3000);
console.log('網站伺服器已啟動,監聽3000埠,請訪問localhost:3000')

在 server 專案根目錄下,新建 form.html 檔案:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- 
   method:指定當前表單提交的方式 action:指定當前表單提交的地址
--> <form method="POST" action="http://localhost:3000"> <input type="submit" name=""> </form> </body> </html>

右鍵點選在瀏覽器執行

點選提交按鈕,會跳轉到http://localhost:3000

回到命令列工具中,可以看到:先是POST輸出,後是GET(表單的跳轉行為預設是get方式)

開啟 app.js 檔案,新增修改程式碼:

// 用於建立網站伺服器的模組
const http = require('http');
// 建立 web 伺服器,app 物件就是網站伺服器物件
const app = http.createServer();
// 當客戶端有請求來的時候
app.on('request', (req, res) => {
  // 獲取請求方式
  // req.method
  console.log(req.method);
  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 響應
  // res.end('<h2>hello user</h2>');
});
// 監聽3000埠
app.listen(3000);
console.log('網站伺服器已啟動,監聽3000埠,請訪問localhost:3000')

重新開啟瀏覽器,輸入:localhost:3000

可以看到頁面上顯示“get”

在瀏覽器重新開啟 form.html ,點選提交按鈕

可以看到頁面上顯示的是“post”

2、請求地址(Requset URL)

app.on('request', (req, res) => {
  req.headers // 獲取請求報文 
  req.url // 獲取請求地址
  req.method // 獲取請求方法
});

例子:

在 app.js 檔案中新增獲取請求地址:

// 用於建立網站伺服器的模組
const http = require('http');
// 建立 web 伺服器,app 物件就是網站伺服器物件
const app = http.createServer();
// 當客戶端有請求來的時候
app.on('request', (req, res) => {
  // 獲取請求方式
  // req.method
  // console.log(req.method);

  // 獲取請求地址 req.url
  console.log(req.url);

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 響應
  // res.end('<h2>hello user</h2>');
});
// 監聽3000埠
app.listen(3000);
console.log('網站伺服器已啟動,監聽3000埠,請訪問localhost:3000')

開啟瀏覽器,在位址列中輸入:localhost:3000/index

回到命令列工具,可以看到:

再在瀏覽器的位址列中輸入:localhost:3000/list

那麼在命令列工具就可以看到,打印出:/list

繼續修改 app.js 檔案:

// 用於建立網站伺服器的模組
const http = require('http');
// 建立 web 伺服器,app 物件就是網站伺服器物件
const app = http.createServer();
// 當客戶端有請求來的時候
app.on('request', (req, res) => {
  // 獲取請求方式 req.method
  // console.log(req.method);

  // 獲取請求地址 req.url
  // console.log(req.url);

  if (req.url == '/index' || req.url == '/') {
    res.end('Welcome to homepage');
  } else if (req.url == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 響應
  // res.end('<h2>hello user</h2>');
});
// 監聽3000埠
app.listen(3000);
console.log('網站伺服器已啟動,監聽3000埠,請訪問localhost:3000')

開啟瀏覽器在位址列中輸入:localhost:3000/index,或者輸入:localhost:3000

頁面上會顯示:“Welcometohomepage”。

同理,輸入:localhost:3000/list,頁面會顯示'Welcometolistpage'。

如果輸入其他地址,頁面會顯示“not found”。

3、請求報文資訊(Requset headers)

例子:

在 app.js 中新增請求報文資訊程式碼:

// 用於建立網站伺服器的模組
const http = require('http');
// 建立 web 伺服器,app 物件就是網站伺服器物件
const app = http.createServer();
// 當客戶端有請求來的時候
app.on('request', (req, res) => {
  // 獲取請求方式 req.method
  // console.log(req.method);

  // 獲取請求地址 req.url
  // console.log(req.url);

  // 獲取請求報文資訊 req.headers
  console.log(req.headers);

  if (req.url == '/index' || req.url == '/') {
    res.end('Welcome to homepage');
  } else if (req.url == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 響應
  // res.end('<h2>hello user</h2>');
});
// 監聽3000埠
app.listen(3000);
console.log('網站伺服器已啟動,監聽3000埠,請訪問localhost:3000')

重新整理頁面,開啟命令列工具可以看到:

如果只想獲取 accept:項的資訊,那麼可以寫為:

console.log(req.headers['accept:']);

重新整理頁面,回到命令列工具可以看到:

3.4 響應報文

1、HTTP 狀態碼

  200 請求成功
  404 請求的資源沒有被找到
  500 伺服器錯誤
  400 客戶端請求有語法錯誤

比如例子:

res.writeHead(200)

2、內容型別

  text/html
  text/css
  application/javascript
  image/jpeg
  application/json

例子:app.js檔案新增 content-type:

// 用於建立網站伺服器的模組
const http = require('http');
// 建立 web 伺服器,app 物件就是網站伺服器物件
const app = http.createServer();
// 當客戶端有請求來的時候
app.on('request', (req, res) => {
  // 獲取請求方式 req.method
  // console.log(req.method);

  // 獲取請求地址 req.url
  // console.log(req.url);

  // 獲取請求報文資訊 req.headers
  // console.log(req.headers['accept']);

  res.writeHead(200, {
    'content-type': 'text/plain' // 純文字型別
  });

  if (req.url == '/index' || req.url == '/') {
    res.end('<h2>Welcome to homepage</h2>');
  } else if (req.url == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 響應
  // res.end('<h2>hello user</h2>');
});
// 監聽3000埠
app.listen(3000);
console.log('網站伺服器已啟動,監聽3000埠,請訪問localhost:3000')

這時重新整理頁面,顯示的是:

因為是純文字型別,把<h2>也當做文字輸出了。

如果想識別 h2 標籤,那要把content-type型別修改為 html:

res.writeHead(200, {
    'content-type': 'text/html' 
});

重新整理頁面後顯示為:

另外,如果輸出內容為中文,比如:

if (req.url == '/index' || req.url == '/') {
    res.end('<h2>歡迎來到首頁</h2>');
}

有時候頁面會出現亂碼,那麼我們需要指定編碼:

res.writeHead(200, {
    'content-type': 'text/html;charset=utf8'
});

重新整理後就沒有亂碼的問題了。

4 HTTP 請求與相應處理

4.1 請求引數

客戶端向伺服器端傳送請求時,有時候需要攜帶一些客戶資訊,客戶資訊需要通過請求引數的形式傳遞到伺服器,比如登入操作。

4.2 GET 請求引數

引數被放置在瀏覽器地址中,例如:http://localhost:3000/?name=zhangsan&age=20

node 內建的 url 模組,用於處理 url 地址

// 用於處理 url 地址
const url = require('url');

url.parse(req.url); // 返回物件

例子:在瀏覽器輸入“localhost:3000/index?name=zhangsan&age=20”

修改 app.js 檔案:

// 用於建立網站伺服器的模組
const http = require('http');
// 用於處理 url 地址
const url = require('url');
// 建立 web 伺服器,app 物件就是網站伺服器物件
const app = http.createServer();
// 當客戶端有請求來的時候
app.on('request', (req, res) => {
  // 獲取請求方式 req.method
  // console.log(req.method);

  // 獲取請求地址 req.url
  // console.log(req.url);

  // 獲取請求報文資訊 req.headers
  // console.log(req.headers['accept']);

  res.writeHead(200, {
    'content-type': 'text/html;charset=utf8' // plain 純文字型別
  });

  console.log(req.url);
  console.log(url.parse(req.url));

  if (req.url == '/index' || req.url == '/') {
    res.end('<h2>歡迎來到首頁</h2>');
  } else if (req.url == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

  if (req.method == 'POST') {
    res.end('post');
  } else if (req.method == 'GET') {
    res.end('get');
  }
  // 響應
  // res.end('<h2>hello user</h2>');
});
// 監聽3000埠
app.listen(3000);
console.log('網站伺服器已啟動,監聽3000埠,請訪問localhost:3000')

重新整理頁面,頁面顯示“not found”,回到命令列工具,發現顯示:

我們想把以 & 符號分隔的字串,轉換成物件的形式,新增第二個引數:

// 第1個引數:要解析的url地址 
// 第2個引數:將查詢引數解析成物件的形式
console.log(url.parse(req.url, true));

意思是:把查詢引數轉換成物件的形式。

這時再重新整理頁面,然後回到命令列工具:

拿到物件,修改程式碼為:

  // 第1個引數:要解析的url地址 
  // 第2個引數:將查詢引數解析成物件的形式
  let params = url.parse(req.url, true).query;
  console.log(params.name); 
  console.log(params.age);

重新整理頁面後,回到命令列工具顯示:zhangsan 20

繼續修改 app.js 程式碼:

  // 第1個引數:要解析的url地址 
  // 第2個引數:將查詢引數解析成物件的形式
  let {query, pathname} = url.parse(req.url, true);
  console.log(query.name); // zhangsan
  console.log(query.age); // 20

  if (pathname == '/index' || pathname == '/') {
    res.end('<h2>歡迎來到首頁</h2>');
  } else if (pathname == '/list') {
    res.end('Welcome to listpage');
  } else {
    res.end('not found');
  }

重新整理頁面,發現可以顯示“歡迎來到首頁”了。

4.3 POST 請求引數

引數被放置在請求體中進行傳輸
獲取 POST 引數需要使用 data 事件和 end 事件
使用 querystring 系統模組將引數轉換為物件格式

示例程式碼:

// 匯入系統模組 querystring 用於將 HTTP 引數轉換為物件格式
const querystring = require('querystring');

app.on('request', (req, res) => {
  let postData = '';
  // 監聽引數傳輸事件
  req.on('data', (chunk) => postData += chunk;);
  // 監聽引數傳輸完畢事件
  req.on('end', () => {
    console.log(querystring.parse(postData));
  }); 
});

例子:

開啟 form.html 檔案,新增表單項程式碼:

<form method="POST" action="http://localhost:3000">
    <input type="text" name="infoname" />
    <input type="password" name="password" />
    <input type="submit" name="">
</form>

右鍵點選在瀏覽器執行 form.html

隨便輸入一些內容點選提交,然後開啟 network 選項可以看到:

此時知道如何從客戶端傳送 POST 請求引數了。

下面要在伺服器端接收這些引數:

在根目錄下新建一個檔案 post.js:

// 用於建立網站伺服器的模組
const http = require('http');
// 建立 web 伺服器,app 物件就是網站伺服器物件
const app = http.createServer();
// 當客戶端有請求來的時候
app.on('request', (req, res) => {
  // post 引數是通過事件的方式接收的
  // data 當請求引數傳遞的時候觸發 data 事件
  // end 當引數傳遞完成的時候觸發 end 事件

  let postParams= '';

  req.on('data', params => {
    postParams += params;
  });

  req.on('end', () => {
    console.log(postParams);
  });

  res.end('ok');
});
// 監聽3000埠
app.listen(3000);
console.log('網站伺服器已啟動,監聽3000埠,請訪問localhost:3000')

開啟命令列工具,輸入:

nodemon post.js

然後重新整理瀏覽器,輸入資訊後,點選提交按鈕,發現頁面地址跳轉到:http://localhost:3000,並且顯示“ok”。

而命令列工具則顯示:infoname=213&password=123345

是剛剛輸入的資訊內容,表示引數接收成功了。

這個引數依然是字串型別,我們想要的是物件的形式。

node 提供的內建模組querystring。

在 post.js 中匯入querystring 模組:

// 處理請求引數模組
const querystring = require('querystring');

使用parse() 方法:

  req.on('end', () => {
    console.log(querystring.parse(postParams));
  });

再重新整理頁面重新輸入資訊提交,然後可以在命令列工具中看到:{ infoname: '213', password: '123345' }

4.4 路由

路由是指客戶端請求地址與伺服器端程式程式碼的對應關係。簡單的說,就是請求什麼響應什麼。

核心程式碼:

// 當客戶端發來請求的時候
app.on('request',  (req, res) => {
  // 獲取客戶端的請求路徑
  let {pathname} = url.parse(req.url);
  if (pathname == '/' || pathname == '/index') {
    res.end('歡迎來到首頁');
  } else if (pathname == '/list') {
    res.end('歡迎來到列表頁面');
  } else {
    res.end('抱歉,您訪問的頁面出遊了');
  }
});

例子:

新建 route 專案資料夾,並新建 app.js 檔案:

// 1.引入系統模組 http
const http = require('http');
// 用於處理 url 地址
const url = require('url');
// 2.建立網站伺服器
const app = http.createServer();
// 3.為網站伺服器物件新增請求事件
app.on('request', (req, res) => {
  // 獲取客戶端的請求方式 req.method
  const method = req.method.toLowerCase(); // toLowerCase 轉為小寫
  // 獲取客戶端的請求地址 req.url
  const pathname = url.parse(req.url).pathname;

  res.writeHead(200, {
    'content-type': 'text/html;charset=utf8'
  });
  // 4.實現路由功能
  if (method == 'get') {
    if (pathname == '/' || pathname == '/index') {
      res.end('歡迎來到首頁');
    } else if (pathname == '/list') {
      res.end('歡迎來到列表頁');
    } else {
      res.end('您訪問的也不存在');
    }
  } else if (method == 'post') {

  }
});

// 監聽3000埠
app.listen(3000);
console.log('伺服器啟動成功')

在命令列工具輸入:

nodemon app.js

在瀏覽器輸入:localhost:3000

可以看到頁面顯示:歡迎來到列表頁

4.5 靜態資源

伺服器端不需要處理,可以直接響應給客戶端的資源就是靜態資源,例如CSS、JavaSCript、image檔案。

例子:

新建專案 static 資料夾,並建立 public 資料夾,把上次 gulp-demo 專案 dist 目錄下的檔案都拷貝過來。

再新建 app.js 檔案:

// 1.引入系統模組 http
const http = require('http');
// 2.建立網站伺服器
const app = http.createServer();
// 3.為網站伺服器物件新增請求事件
app.on('request', (req, res) => {
  res.end('ok');
});

// 監聽3000埠
app.listen(3000);
console.log('伺服器啟動成功')

在命令列工具輸入:

nodemon app.js

此時頁面顯示 ok,代表伺服器啟動成功了。

這時我們想在瀏覽器直接輸入:localhost:3000/default.html 就可以訪問到 public 目錄下的 default.html 檔案,需要:

1)先通過req.url 獲取到使用者的請求路徑,也就是說獲取到 /default.html

2)把這個請求路徑轉換為檔案所在伺服器上的真實物理路徑,然後讀取這個檔案的內容

3)最終把讀取的內容返回給客戶端

繼續編輯 app.js 檔案:

// 引入系統模組 http
const http = require('http');
// 用於處理 url 地址
const url = require('url');
// 匯入系統模組 path 模組
const path = require('path');
// 匯入系統模組 fs
const fs = require('fs');
// 建立網站伺服器
const app = http.createServer();
// 為網站伺服器物件新增請求事件
app.on('request', (req, res) => {
  // 1.獲取使用者的請求路徑
  let pathname = url.parse(req.url).pathname;
  // 2.將使用者的請求路徑轉換為實際的伺服器硬碟路徑
  let realPath = path.join(__dirname, 'public' + pathname);
  // 3.通過模組內部的 readFile 方法,讀取檔案內容
  fs.readFile(realPath, 'utf8', (err, result) => {
    // 如果檔案讀取失敗
    if (err != null) {
      res.writeHead(404, {
        'content-type': 'text/html;charset=utf8'
      });
      res.end('檔案讀取失敗');
      return;
    }
    res.end(result);
  });
  //res.end('ok');
});

// 監聽3000埠
app.listen(3000);
console.log('伺服器啟動成功')

此時在瀏覽器輸入:localhost:3000/default.html ,可以訪問到頁面了。

這裡圖片和樣式有點問題,需要修改下程式碼:

// 這裡的 utf8 去掉
fs.readFile(realPath,  (err, result) => {

重新整理頁面,已經好了:

還有個問題,如果輸入:localhost:3000 ,頁面會顯示“檔案讀取失敗”,這裡我們也想它訪問到 default 頁面。

我們需要在路徑做個判斷,當是‘/’的時候讓它也訪問‘/default.html’

繼續編輯 app.js 檔案,新增判斷:

// 1.獲取使用者的請求路徑
let pathname = url.parse(req.url).pathname;
pathname = pathname == '/' ? '/default.html' : pathname

這時重新整理頁面,localhost:3000 也可以訪問到頁面內容了。

當伺服器端向客戶端做出響應的時候,要告訴客戶端,當前所給的型別是什麼。我們只是在錯誤的時候給了型別,正常的時候沒給。

這裡需要用到一個第三方模組:mime

功能是:可以根據當前的請求路徑分析出這個資源的型別,然後把資源的型別通過返回值返給你。

開啟命令列工具先打斷服務的執行,然後下載這個 mime:

npm install mime

然後重新執行服務:

nodemon app.js

回到 app.js 檔案,先引用模組:

// 匯入第三方模組 mime
const mime = require('mime');

app.on('request', (req, res) => {
  。。。

  // 2.將使用者的請求路徑轉換為實際的伺服器硬碟路徑
  let realPath = path.join(__dirname, 'public' + pathname);

  console.log(mime.getType(realPath));

  。。。
});

重新整理頁面,然後回到命令列工具,可以看到:

這些都是當前請求檔案的型別。

新建一個變數,把檔案的型別儲存下:

  // 請求檔案的型別
  let type = mime.getType(realPath);

  // 3.通過模組內部的 readFile 方法,讀取檔案內容
  fs.readFile(realPath, (err, result) => {
    、、、
    // 成功的報文資訊
    res.writeHead(200, {
        'content-type': type
    })
    res.end(result);
  });

此時開啟 network 並重新整理頁面,可以看到樣式和圖片資源加載出來了。

4.6 動態資源

相同的請求地址不同的響應資源,這種資源就是動態資源。

5 Node.js 非同步程式設計

5.1 同步API,非同步API

// 路徑拼接
const publib = path.join(__dirname, 'public');

// 請求地址解析
const urlObj = url.parse(req.url);

// 獲取檔案內容
fs.readFile('./demo.txt', 'utf8', (err, result) => {
  console.log(result);
});

同步API:只有當前 API 執行完成後,才能繼續執行下一個 API

例如:

console.log('before');
console.log('after');

結果是:先輸出 before,然後再輸出 after。

非同步API:當前 API 的執行不會阻礙後續程式碼的執行

例如:

console.log('before');
setTimeout(() => {
  console.log('last');
}, 2000);
console.log('after');

結果是:先輸出 before,再輸出 after,然後過2秒後輸出 last。

5.2 同步API,非同步API 的區別(獲取返回值)

同步 API 可以從返回值中拿到 API 執行的結果,但是非同步 API 是不可以的。

// 同步
function sum (n1, n2) {
  return n1 + n2;
}
const result = sum(10, 20);

可以拿到返回值:30。

例子:

建立專案 async, 新建 getRetrunValue.js 檔案:

// 非同步
function getMsg () {
  setTimeout(() => {
    return { msg: 'Hello Node.js'}
  }, 2000);
}
const msg = getMsg();
console.log(msg);

在命令列工具中執行:

node getReturnValue.js

結果是:undefined

結論:在非同步 API 裡,我們是無法通過返回值的方式,去拿到非同步 API的執行結果。需要通過回撥函式。

5.3 回撥函式

自己定義函式讓別人去呼叫。

// getData 函式定義
function getData (callback) {}
// getData 函式呼叫
getData(() => {});

例子:

新建 callback.js 檔案:

function getData (callback) {
  callback();
}

getData(function () {
  console.log('callback 函式被呼叫了')
});

回到命令列工具中輸入:

node callback.js

結果是:callback函式被呼叫了

修改下程式碼:

function getData (callback) {
  callback('123');
}

getData(function (n) {
  console.log('callback 函式被呼叫了');
  console.log(n);
});

重新在命令列工具中執行

結果是:

callback 函式被呼叫了
123

重新修改 getRetrunValue.js 的程式碼:

// 非同步
function getMsg (callback) {
  setTimeout(() => {
    callback({ 
      msg: 'Hello Node.js'
    });
  }, 2000);
}

getMsg(function(data){
  console.log(data);
});

到命令列工具中輸入:

node getReturnValue.js

結果是:2秒後顯示:{ msg: 'Hello Node.js' }

5.5 同步API,非同步API 的區別(程式碼執行順序)

同步 API 從上到下依次執行,前面程式碼會阻塞後面程式碼的執行。

for(var i = 0; i < 100000; i++) {
  console.log(i)
}
console.log('for 迴圈後面的程式碼');

上面的程式碼: for 迴圈不執行完,後面的程式碼是不會執行的。

非同步 API 不會等待 API執行完成後再向下執行程式碼

console.log('程式碼開始執行');
setTimeout(() => { console.log('2秒後執行的程式碼') }, 2000);
setTimeout(() => { console.log('0秒後執行的程式碼') }, 0);
console.log('程式碼結束執行');

結果是:

程式碼開始執行
程式碼結束執行
0秒後執行的程式碼
2秒後執行的程式碼

5.6 程式碼執行順序分析

1.console.log 是同步 API,會在同步程式碼執行區執行

2.setTimeout 是非同步 API,放到非同步程式碼執行區,緊接著會把非同步 API 對應的回撥函式,放到回撥函式佇列中。這時的程式碼還沒有被執行。

3.setTimeout 同上

4.console.log 是同步 API,會在同步程式碼執行去執行

這時2個同步程式碼已經執行完成了,然後轉到非同步程式碼執行區中去依次執行程式碼

5.由於第2個定時器是在0秒後執行,系統會在回撥函式佇列中找到第2個定時器所對應的回撥函式,拿到同步程式碼執行區中去執行

6.等待2秒後,第1個定時器也執行了,系統會在回撥函式佇列中找到第1個定時器所對應的回撥函式,也拿到同步程式碼執行區中去執行

5.7 Node.js 中的非同步 API

fs.readFile('./demo.txt', (err, result) => {]);

fs.readFile 就是非同步 API。

var server = http.createServer();
server.on('request', (req, res) => {});

事件監聽也是非同步 API。

如果非同步 API 後面程式碼的執行依賴當前非同步 API 的執行結果,但實際上後續程式碼在執行的時候非同步 API 還沒有返回結果,這個問題要怎麼解決?

例子:需求:依次讀取A檔案、B檔案、C檔案

新建1.txt、2.txt、3.txt:裡面分別寫入1、2、3

建立 callbakehell.js 檔案:

const fs = require('fs');

fs.readFile('./1.txt', 'utf8', (err, result1) => {
  console.log(result1)
  fs.readFile('./2.txt', 'utf8', (err, result2) => {
    console.log(result2)
    fs.readFile('./3.txt', 'utf8', (err, result3) => {
      console.log(result3)
    });  
  });
});

回到命令列工具,輸入:

node callbackhell.js

可以看到結果是:

1
2
3

證明確實是依次讀取檔案內容。

但是這種巢狀形式的話,如果巢狀的層數過多,維護起來會很麻煩。這種回撥巢狀回撥再巢狀回撥,我們比作為:回撥地獄。

5.8 Promise

Promise 出現的目的是解決 Node.js 非同步程式設計中回撥地獄的問題。

基礎語法:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (true) {
      resolve({name: '張三'})
    } else {
      reject('失敗了')
    }
  }, 2000);
});

promise.then(
  result => console.log(resule); // {name: '張三'}
)
.catch(
  error => console.log(error); // 失敗了
)

例子:

新建 promise.js 檔案:

const fs = require('fs');

let promise = new Promise((resolve, reject) => {
  fs.readFile('./1.txt', 'utf8', (err, result) => {
      if (err != null) {
        reject(err);
      } else {
        resolve(result);
      }
  });
});

promise.then((result) => {
  console.log(result);
})
.catch((err) => {
  console(err);
});

回到命令列工具,輸入:

node promise.js

結果是:1

下面就要解決依次讀取檔案例子的回撥地獄的問題:

新建promise2.js檔案:

const fs = require('fs');

// fs.readFile('./1.txt', 'utf8', (err, result1) => {
//   console.log(result1)
//   fs.readFile('./2.txt', 'utf8', (err, result2) => {
//     console.log(result2)
//     fs.readFile('./3.txt', 'utf8', (err, result3) => {
//       console.log(result3)
//     });  
//   });
// });

function p1 () {
  return new Promise ((resolve, reject) => {
    fs.readFile('./1.txt', 'utf8', (err, result) => {
      resolve(result)
    }); 
  });
}

function p2 () {
  return new Promise ((resolve, reject) => {
    fs.readFile('./2.txt', 'utf8', (err, result) => {
      resolve(result)
    }); 
  });
}

function p3 () {
  return new Promise ((resolve, reject) => {
    fs.readFile('./3.txt', 'utf8', (err, result) => {
      resolve(result)
    }); 
  });
}

p1().then((r1) =>{
  console.log(r1);
  return p2();
})
.then((r2) =>{
  console.log(r2);
  return p3();
})
.then((r3) => {
  console.log(r3);
})

p1.then 裡return了一個 p2的呼叫,p2的呼叫返回一個promise物件,也就是說實際上 return了一個promise物件。在下一個 then裡就能拿到上一個 then裡面return 的 promise 物件的結果。

回到命令列工具,輸入:

node promise2.js

結果為:

1
2
3

5.9非同步函式

非同步函式是非同步變成語法的終極解決方案,它可以讓我們將非同步程式碼寫成同步的形式,讓程式碼不再有回撥函式巢狀,使程式碼變得清晰明瞭。

基礎語法:

const fn = async () => ();
async function fn () {}

在普通函式定義的前面加上 async 關鍵字,普通函式就變成了非同步函式。

非同步函式預設的返回值是promise物件,不是undefined。

例子:

新建asyncFunction.js檔案:

// 1.在普通函式定義的前面加上 async 關鍵字,普通函式就變成了非同步函式
// 2.非同步函式預設的返回值是 promise 物件,不是 undefined
async function fn () {
  return 123;
}

console.log(fn ())

回到命令列工具,輸入:

node asycnFunction.js

結果是:

修改下程式碼:

// 1.在普通函式定義的前面加上 async 關鍵字,普通函式就變成了非同步函式
// 2.非同步函式預設的返回值是 promise 物件,不是 undefined
async function fn () {
  return 123;
}

// console.log(fn ())
fn ().then(function (data) {
  console.log(data)
})

重新執行,結果是:

而錯誤資訊用throw來丟擲返回,throw一旦執行以後,後面的程式碼就不會再執行了。用 .catch來捕獲throw丟擲的錯誤資訊。

// 1.在普通函式定義的前面加上 async 關鍵字,普通函式就變成了非同步函式
// 2.非同步函式預設的返回值是 promise 物件,不是 undefined
// 3.在非同步函式內部使用 throw 關鍵字進行錯誤的丟擲
async function fn () {
  throw '發生了一些錯誤';
  return 123;
}

// console.log(fn ())
fn ().then(function (data) {
  console.log(data)
}).catch(function (err) {
  console.log(err)
})

執行結果是:

await關鍵字

1.它只能出現在非同步函式中

2.awaitpromise它可以暫停非同步函式的執行,等待promise物件返回結果後,再向下執行函式

例子:還是依次讀取檔案

// await 關鍵字
// 1.它只能出現在非同步函式中
// 2.await promise 它可以暫停非同步函式的執行,等待 promise 物件返回結果後,再向下執行函式

async function p1 () {
  return 'p1';
}
async function p2 () {
  return 'p2';
}
async function p3 () {
  return 'p3';
}

async function run () {
  let r1 = await p1()
  let r2 = await p2()
  let r3 = await p3()
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
run();

執行後結果為:

可以看出是順序輸出的:p1、p2、p3

總結:

async 關鍵字:

1、普通函式定義前加 async 關鍵字,普通函式變成非同步函式
2、非同步函式預設的返回 promise 物件
3、在非同步函式內部使用 return 關鍵字進行結果返回,結果會被包裹的 promise 物件中 return 關鍵字代替了 resolve 方法
4、在非同步函式內部使用 throw 關鍵字丟擲錯誤異常
5、使用非同步函式再鏈式呼叫 then 方法獲取非同步函式執行的結果
6、呼叫非同步函式再鏈式呼叫 catch 方法獲取非同步函式執行的錯誤資訊

await 關鍵字:

1、await 關鍵字只能出現在非同步函式中
2、await promise :await 後面只能寫 promise 物件,寫其他型別的 API 是不可以的
3、await 關鍵字可以暫停非同步函式向下執行,直到 promise 物件返回結果

例子:通過 await關鍵字,改造依次讀取三個檔案的例子

fs.readFile()方法是通過返回值的方式,來獲取檔案的讀取結果,也就是說它不返回 promise 物件。node提供了一個promisify方法,這個方法儲存在util 模組中,然後用這個promisify方法,對readFile進行包裝,讓它返回promise 物件。

新建asyncFunctionReadFile.js檔案:

const fs = require('fs');
// promisify 方法是用來改造現有非同步函式 API,讓其返回 promise 物件,從而支援非同步函式語法
const promisify = require('util').promisify;
// 呼叫 promisify 方法改造現有非同步 API,讓其返回 promise 物件
const readFile = promisify(fs.readFile);

async function run () {
  let r1 = await readFile('./1.txt', 'utf8')
  let r2 = await readFile('./2.txt', 'utf8')
  let r3 = await readFile('./3.txt', 'utf8')
  console.log(r1);
  console.log(r2);
  console.log(r3);
}
run();

回到命令工具中,輸入:

node asyncFunctionReadFile.js

執行結果是: