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
頁面上會顯示:“Welcome to homepage”。
同理,輸入:localhost:3000/list,頁面會顯示'Welcome to listpage'。
如果輸入其他地址,頁面會顯示“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 關鍵字,普通函數就變成了異步函數。
例子:
新建 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.await promise 它可以暫停異步函數的執行,等待 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 關鍵字,改造依次讀取三個文件的例子
新建 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
執行結果是: