1. 服務器端基礎概念
1.1網站組成
- 網站應用程序主要分爲兩大部分:客戶端和服務器端。
- 客戶端:在瀏覽器中運行的部分,就是用戶看到並與之交互的界面程序。使用HTML、CSS、JavaScript構建。
- 服務器端:在服務器中運行的部分,負責存儲數據和處理應用邏輯。
1.2Node網站服務器
能夠提供網站訪問服務的機器就是網站服務器,它能夠接收客戶端的請求(request),能夠對請求做出響應(respond)。
1.3 IP地址、域名和端口
IP地址:是指互聯網中設備的唯一標識。這裏不多敘述,大家應該都瞭解一些~
域名:是指平時上網所使用的網址。如:www.baidu.com,雖然在地址欄中輸入的是網址, 但是最終還是會將域名轉換爲ip才能訪問到指定的網站服務器。
例如:http://www.baidu.com => http://220.181.38.148/
端口:是指計算機與外界通訊交流的出口,用來區分服務器電腦中提供的不同的服務。
因爲大多數網站應用使用的是80端口號,如果輸入域名或IP訪問網站時沒有輸入端口號,瀏覽器在請求的時候默認在輸入的域名或IP地址後面接的是80端口號,如果是非80端口號,在IP地址或域名後面加上端口號即可。
1.4 URL
統一資源定位符,又叫URL(Uniform Resource Locator),是專爲標識Internet網上資源位置而設的一種編址方式,我們平時所說的網頁地址指的即是URL。在URL中標註了要請求的服務器地址,提供服務的端口及要請求的資源位置。
URL的組成
傳輸協議://服務器IP或域名:端口/資源所在位置標識
https://editor.csdn.net/md?articleId=106287460
http:超文本傳輸協議,提供了一種發佈和接收HTML頁面的方法。
1.5開發過程中客戶端和服務器端說明
在開發階段,客戶端和服務器端使用同一臺電腦,即開發人員電腦。
我們自己電腦的服務器(本地)可以通過一組特殊的IP或域名來訪問
本機域名:localhost
本機IP地址:127.0.0.1
2. 創建web服務器
我們使用nodejs當中創建網站服務器。做服務器端開發如果沒有網站服務器,一切都無從談起。
在電腦當中,我們要先安裝好node軟件,參考node學習筆記,再用nodejs創建軟件層面上的服務器,得到請求對象和響應對象。
創建網站服務器實例代碼如下圖:
//引入http創建網站服務器系統模塊
const http = require('http');
//創建網站服務器對象
const app = http.createServer();
//當客戶端有請求來的時候
app.on('request',(req,res) => {
//響應
res.end('hello');
});
//監聽的端口號
app.listen(3000);
console.log('網站服務器啓動成功');
保存代碼,打開PowerShell窗口,切換到當前目錄,使用nodemon xxx.js(當前文件名),這裏也可以用node命令,但是建議用nodemon,這樣每次修改保存文件後可以自動運行,無需手動再去運行一遍
nodemon test.js
打開瀏覽器,在網址欄輸入localhost:3000,測試成功,響應內容成功顯示出來。(如果你響應的內容是中文字符,此時代碼還不夠完善所以顯示出來的不是想要的效果,這個問題後面會提到)
3. HTTP協議
3.1 HTTP協議的概念
超文本傳輸協議(英文:HyperText Transfer Protocol,縮寫:HTTP)規定了如何從網站服務器傳輸超文本到本地瀏覽器,它基於客戶端服務器架構工作,是客戶端(用戶)和服務器端(網站)請求和應答的標準。
3.2 報文
在HTTP請求和響應的過程中傳遞的數據塊就叫報文,包括要傳送的數據和一些附加信息,並且要遵守規定好的格式。
拿博客首頁來看看
信息量挺大的,可以瞭解一些
3.3 請求報文
請求方式 (Request Method)
- GET 請求數據
- POST 發送數據
最常見的get請求就是瀏覽器通過地址欄中輸入的網址的方式。如果既不是獲取數據也不是添加數據的請求,比如登陸操作,一般是post方式,因爲使用post相對於get 更安全一些。
實例代碼如下圖,保存文件,
//引入http,創建網站服務器模塊
const http = require('http');
//app對象就是網站服務器對象
const app = http.createServer();
//當客戶端有請求來的時候
app.on('request',(req,res) => {
//獲取請求方式
// console.log(req.method);
if(req.method=='POST'){
res.end('post');
}else if(req.method=='GET'){
res.end('get');
}else {}
});
//監聽的端口號
app.listen(3000);
console.log('網站服務器啓動成功');
編寫一個表單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>
保存後運行,點擊提交按鈕,可以看到此時的請求方式就是post。
請求地址 (Request URL)
app.on('request', (req, res) => {
req.headers // 獲取請求報文
req.url // 獲取請求地址
req.method // 獲取請求方法
});
3.4 響應報文
HTTP狀態碼
- 200 請求成功
- 404 請求的資源沒有被找到
- 500 服務器端錯誤
- 400 客戶端請求有語法錯誤
內容類型
- text/html
- text/css
- application/javascript
- image/jpeg
- application/json
app.on('request', (req, res) => {
// 設置響應報文
res.writeHead(200, {
'Content-Type': 'text/html;charset=utf8‘
});
});
綜合請求地址及響應報文應用實例代碼如下圖:
//引入http創建網站服務器系統模塊
const http = require('http');
//創建網站服務器對象
const app = http.createServer();
//當客戶端有請求來的時候
app.on('request',(req,res) => {
//獲取請求地址
// res.end(req.url);
//響應報文
res.writeHead(200,{
//設置了響應報文的文本類型和字符編碼格式後,中文以及html標籤的格式就可以在瀏覽器渲染出來了
'content-type':'text/html;charset=utf8'
});
if(req.url=='/index' || req.url=='/'){
res.end('<h2>這是index界面</h2>');
}else if(req.url=='/list'){
res.end('<h2>這是list界面</h2>');
}else {
res.end('<h3>404</h3>');
}
});
//監聽的端口號
app.listen(3000);
console.log('網站服務器啓動成功');
4. HTTP請求與響應處理
4.1 請求參數
客戶端向服務器端發送請求時,有時需要攜帶一些客戶信息,客戶信息需要通過請求參數的形式傳遞到服務器端,比如登錄操作。
4.2 GET請求參數
參數被放置在瀏覽器地址欄中,例如:http://localhost:3000/?name=zhangsan&age=20
參數獲取需要藉助系統模塊url,url模塊用來處理url地址
const http = require('http');
// 導入url系統模塊 用於處理url地址
const url = require('url');
const app = http.createServer();
app.on('request', (req, res) => {
//響應報文
res.writeHead(200,{
'content-type':'text/html;charset=utf8'
});
//獲取請求地址
// console.log(req.url);
// 將url路徑的各個部分解析出來並返回對象
//獲取url相應對象方式,true 代表將參數解析爲對象格式
// let query = url.parse(req.url).query;
// let paras = url.parse(req.url).query;
// console.log(paras);
// console.log(paras.name);
// console.log(paras.age);
// let {query} = url.parse(req.url,true);
//let {pathname,query} = url.parse(req.url,true);
let {pathname,query} = url.parse(req.url,true);
if(pathname=='/index' || pathname=='/'){
res.end('<h2>這是index界面</h2>');
console.log(query);
}else if(pathname=='/list'){
res.end('<h2>這是list界面</h2>');
console.log(query);
}else {
res.end('<h3>404</h3>');
}
});
app.listen(3000);
在瀏覽器地址欄輸入:http://localhost:3000/index?name=zahngsan&age=18
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));
});
res.end('hello');
});
通過表單文件點擊按鈕形式請求參數
4.4 路由
http://localhost:3000/index
http://localhost:3000/login
路由是指客戶端請求地址與服務器端程序代碼的對應關係。簡單的說,就是請求什麼響應什麼。
實例代碼:
// 當客戶端發來請求的時候
app.on('request', (req, res) => {
//響應報文,設置文本類型和字符編碼
res.writeHead(200,{
'content-type':'text/html;charset=utf8'
});
//獲取請求方式
let method = req.method.toLowerCase();
//獲取請求地址的pathname對象和query參數對象
let {pathname,query} = url.parse(req.url,true);
if(method == 'get'){
if(pathname=='/index' || pathname=='/'){
res.end('<h2>這是index界面</h2>');
console.log(query);
}else if(pathname=='/list'){
res.end('<h2>這是list界面</h2>');
console.log(query);
}else {
res.end('<h3>404</h3>');
}
}else if(method == 'post'){
}else {}
});
4.5 靜態資源和動態資源
靜態資源:服務器端不需要處理,可以直接響應給客戶端的資源就是靜態資源,例如CSS、JavaScript、image文件。如:http://www.itcast.cn/images/logo.png
動態資源:相同的請求地址不同的響應資源,這種資源就是動態資源。
如:http://www.itcast.cn/article?id=1
http://www.itcast.cn/article?id=2
//創建網站服務器模塊
const http = require('http');
//請求地址模塊
const url = require('url');
//路徑模塊
const path = require('path');
//文件模塊
const fs = require('fs');
//mime模塊,引用前需要下載mime模塊
const mime = require('mime');
//創建服務器對象
const app = http.createServer();
//請求響應服務器對象
app.on('request',(req,res) => {
//獲取請求路徑
let pathname = url.parse(req.url).pathname;
pathname = pathname == '/'? '/default.html' : pathname;
//轉行成物理地址
let realPath = path.join(__dirname,'public'+pathname);
//獲取文件類型
let type = mime.getType(realPath);
//讀取文件
fs.readFile(realPath,(err,result) => {
if(err != null){
res.writeHead(404,{'content-type':'text/html;charset=utf8'});
res.end('文件讀取失敗!');
return ;
}
//響應報文
res.writeHead(200,{'content-type':type});
res.end(result);
});
});
//監聽端口號
app.listen(3000);
console.log("網站服務器啓動成功");
下載mime模塊命令
npm install mime
4.6 客戶端請求途徑
GET方式
- 瀏覽器地址欄
- link標籤的 href屬性
- script標籤的src屬性
- img標籤的src屬性
- Form表單提交
POST方式
- Form表單提交
5.Node.js異步編程
5.1 同步API, 異步API
同步API:只有當前API執行完成後,才能繼續執行下一個API
console.log('before');
console.log('after');
異步API:當前API的執行不會阻塞後續代碼的執行
//輸出順序:before after last
console.log('before');
setTimeout(
() => { console.log('last');
}, 2000);
console.log('after');
5.2 同步API, 異步API的區別( 獲取返回值 )
同步API可以從返回值中拿到API執行的結果, 但是異步API是不可以的
// 同步
function sum (n1, n2) {
return n1 + n2;
}
const result = sum (10, 20);
// 異步,下面代碼
function getMsg () {
setTimeout(function () {
return { msg: 'Hello Node.js' }
}, 2000);
}
//結果undefined
console.log(getMsg());
5.3 回調函數
自己定義函數讓別人去調用。
function getData (callback) {
callback('123')
}
getData(function (n) {
console.log('callback函數被調用了')
console.log(n)
});
5.4 使用回調函數獲取異步API執行結果
function getMsg (callback) {
setTimeout(function () {
callback ({ msg: 'Hello Node.js' })
}, 2000);
}
getMsg (function (msg) {
console.log(msg);
});
5.5 同步API, 異步API的區別(代碼執行順序)
同步API從上到下依次執行,前面代碼會阻塞後面代碼的執行
for (var i = 0; i < 100000; i++) {
console.log(i);
}
console.log('for循環後面的代碼');
異步API不會等待API執行完成後再向下執行代碼
console.log('代碼開始執行');
setTimeout(() => { console.log('2秒後執行的代碼')}, 2000);
setTimeout(() => { console.log('0秒後執行的代碼')}, 0);
console.log('代碼結束執行');
5.6 代碼執行順序分析
//下列代碼執行結果
//代碼開始執行
//代碼結束執行
//0秒後執行的代碼
//2秒後執行的代碼
console.log('代碼開始執行');
setTimeout(() => {
console.log('2秒後執行的代碼');
}, 2000);
setTimeout(() => {
console.log('0秒後執行的代碼');
}, 0);
console.log('代碼結束執行');
5.7 Node.js中的異步API
前面學過的讀取文件和事件監聽屬於常見的異步API
//讀取文件
fs.readFile('./demo.txt', (err, result) => {});
//事件監聽
var server = http.createServer();
server.on('request', (req, res) => {});
如果異步API後面代碼的執行依賴當前異步API的執行結果,但實際上後續代碼在執行的時候異步API還沒有返回結果,這個問題要怎麼解決呢?
fs.readFile('./demo.txt', (err, result) => {});
console.log('文件讀取結果');
需求:依次讀取A文件、B文件、C文件
方法一:套娃方式,倘若讀取的文件n個,顯然用這種方法需要“套娃”n-1遍,造成回調地獄。這種方法顯然不適合用來讀取過多的文件。
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)
})
})
});
5.8 Promise
方法二:使用Promise,Promise出現的目的是解決Node.js異步編程中回調地獄的問題。
//resolve執行成功的返回結果,結果以對象形式存儲
//reject執行失敗的返回結果
//resolve和reject本質是函數
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({name: '張三'})
}else {
reject('失敗了')
}
}, 2000);
});
promise.then(result => console.log(result); // {name: '張三'})
.catch(error => console.log(error); // 失敗了);
利用Promise方式讀取單個文件
const fs = require('fs');
let promise = new Promise((resolve, reject) => {
fs.readFile('./100.txt', 'utf8', (err, result) => {
if (err != null) {//讀取文件失敗
reject(err);
}else {//成功讀取文件
//用Promise執行成功的返回函數resolve返回結果
resolve(result);
}
});
});
//用then接收成功的返回結果
promise.then((result) => {
console.log(result);
})
//catch捕捉執行異常
.catch((err)=> {
console.log(err);
})
利用Promise方式讀取多個文件
const fs = require('fs');
function p1() {
return new Promise((resolve,reject) => {
fs.readFile('1.txt','utf8',(err,result) => {
if(err != null){
reject(err);
}else {
resolve(result);
}
});
});
}
function p2() {
return new Promise((resolve,reject) => {
fs.readFile('2.txt','utf8',(err,result) => {
if(err != null){
reject(err);
}else {
resolve(result);
}
});
});
}
function p3() {
return new Promise((resolve,reject) => {
fs.readFile('3.txt','utf8',(err,result) => {
if(err != null){
reject(err);
}else {
resolve(result);
}
});
});
}
p1().then((r1) => {
console.log(r1);
return p2();
})
.then((r2) => {
console.log(r2);
return p3();
})
.then((r3) => {
console.log(r3);
})
雖然這個方式解決了回調地獄問題,但是代碼過於繁瑣,怪嚇人的~
5.9 異步函數
方法三:使用async關鍵字
異步函數是異步編程語法的終極解決方案,它可以讓我們將異步代碼寫成同步的形式,讓代碼不再有回調函數嵌套,使代碼變得清晰明瞭。
const fn = async () => {};
async function fn () {};
//可以測試一下,使用async關鍵字的函數,其返回結果爲promise對象
async function fn () {
throw '發生了一些錯誤';
return 123;
}
// console.log(fn ())
fn ().then(function (data) {
console.log(data);
}).catch(function (err){
console.log(err);
})
async關鍵字
- 普通函數定義前加async關鍵字 普通函數變成異步函數
- 異步函數默認返回promise對象
- 在異步函數內部使用return關鍵字進行結果返回 結果會被包裹的promise對象中 return關鍵字代替了resolve方法
- 在異步函數內部使用throw關鍵字拋出程序異常
- 調用異步函數再鏈式調用then方法獲取異步函數執行結果
- 調用異步函數再鏈式調用catch方法獲取異步函數執行的錯誤信息
await關鍵字
- await關鍵字只能出現在異步函數中
- await promise await後面只能寫promise對象 寫其他類型的API是不不可以的
- await關鍵字可是暫停異步函數向下執行 直到promise返回結果
使用async關鍵字函數,讀取多個文件,解決了上述的回調地獄問題以及代繁瑣問題,從而使代碼簡潔易懂。
const fs = require('fs');
//改造現有的異步API,讓其返回promise對象,從而支持異步函數語法
const readFile = require('util').promisify(fs.readFile);
async function run() {
let f1 = await readFile('1.txt','utf8');
let f2 = await readFile('2.txt','utf8');
let f3 = await readFile('3.txt','utf8');
console.log(f1);
console.log(f2);
console.log(f3);
}
run();
util模塊
const fs = require('fs');
//獲取util模塊下的promisify方法,
const promisify = require('util').promisify
//promisify方法可以改造異步API,讓其返回promise對象
const readFile = promisify(fs.readFile);