伺服器端基礎概念
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
執行結果是: