6.2.2 其他邏輯實(shí)現(xiàn)
1. 實(shí)現(xiàn)ajax接口和渲染數(shù)據(jù)
2. 使用multer處理文件上傳
npm install --save multer
var express = require('express')
var multer? = require('multer')
var upload = multer({ dest: 'uploads/' })
app.post('/logo', upload.single('logo'), function (req, res, next) {
? // req.file is the `logo` file
? console.log(req.file)
? // req.body will hold the text fields, if there were any
})
3. socket.io實(shí)現(xiàn)聊天
6.5 增加、查詢分類侮东、關(guān)鍵字查詢抒钱、分頁查詢类浪、刪除簿废、修改
加入小數(shù)浮點(diǎn)數(shù)問題心肪,引入BigNumber
普通查詢
類別查詢
分頁使用limit(pageSize).skip((page-1)*pageSize)
編寫點(diǎn)mock數(shù)據(jù)。
表單
6.6 實(shí)現(xiàn)網(wǎng)絡(luò)聊天
步驟
搭建socket服務(wù)器
前端鏈接
前端主動(dòng)發(fā)送數(shù)據(jù)
后端主動(dòng)發(fā)送數(shù)據(jù)
斷開連接
應(yīng)用場(chǎng)景
實(shí)時(shí)刷新
使用
socket.io 服務(wù)器部署
客戶端部署
原理
net實(shí)現(xiàn)socket
WebSocket
廣播機(jī)制
七. 安全和性能
五. 客戶端請(qǐng)求
到目前為止过蹂,我們已經(jīng)學(xué)習(xí)了如何通過Express搭建一個(gè)后臺(tái)服務(wù)十绑,并且能夠利用路由輸出HTML頁面和json數(shù)據(jù)。
HTML頁面我們可以直接在瀏覽器中輸入網(wǎng)址自動(dòng)加載榴啸,但是json數(shù)據(jù)并非直接用來渲染孽惰,往往需要在JavaScript文件中通過ajax接口請(qǐng)求來獲取并動(dòng)態(tài)拼湊成數(shù)據(jù)并插入到DOM結(jié)構(gòu)中。
在講解本節(jié)課之前鸥印,我們一般都是使用最知名的jQuery的$.ajax來進(jìn)行網(wǎng)絡(luò)請(qǐng)求勋功,然后基于jQuery的css選擇器來操作DOM并最終把數(shù)據(jù)渲染出來。
本節(jié)库说,我們學(xué)習(xí)兩個(gè)純粹的ajax庫(kù)狂鞋,以便為后面學(xué)習(xí)Vue和React打下基礎(chǔ)。
5.1 axios
axios是目前最廣泛使用的Ajax庫(kù)潜的,使用axios骚揍,axios庫(kù)具有以下特點(diǎn):
支持Promise。
同時(shí)支持node端和瀏覽器端啰挪。
支持?jǐn)r截器等高級(jí)配置項(xiàng)信不。
支持請(qǐng)求的取消。
HTML中引入axios
<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.js"></script>
創(chuàng)建axios實(shí)例
const axiosInstance = axios.create({
? baseURL: 'https://some-domain.com/api/',
? timeout: 1000,
? headers: {'X-Custom-Header': 'foobar'}
});
調(diào)用axios方法
axios#request(config)
axios#get(url[, config])
axios#delete(url[, config])
axios#head(url[, config])
axios#options(url[, config])
axios#post(url[, data[, config]])
axios#put(url[, data[, config]])
axios#patch(url[, data[, config]])
axios#getUri([config])
axios請(qǐng)求之后的結(jié)果是一個(gè)Promise對(duì)象
axios 支持的配置
url:如果是絕對(duì)路徑則直接請(qǐng)求亡呵,如果是相對(duì)路徑則基于baseURL的請(qǐng)求地址抽活。
method:請(qǐng)求方法,默認(rèn)GET锰什。對(duì)于axios調(diào)用特定請(qǐng)求下硕,該字段不需要丁逝。
baseURL:如果url是相對(duì)路徑,則放置在url前梭姓。
transformRequest:[function(data,headers)],對(duì)于PUT霜幼、POST、PATCh請(qǐng)求前的body數(shù)據(jù)處理誉尖。
transformResponse:和transformRequest類似罪既,處理響應(yīng)的response數(shù)據(jù)。
headers:{},自定義header释牺,不要在這里寫瀏覽器一些內(nèi)置Header萝衩,瀏覽器并不會(huì)用它來覆蓋。
params:{},URL后面的參數(shù)没咙,
data:對(duì)于POST猩谊、PUT等方法要傳入body中的內(nèi)容。
timeout:超時(shí)毫秒數(shù)祭刚,
withCredentials:對(duì)于跨域請(qǐng)求設(shè)置是否需要認(rèn)證牌捷。
auth:HTTP認(rèn)證。
responseType:設(shè)置響應(yīng)類型涡驮,json暗甥、blob、text捉捅、stream撤防、arraybuffer。
proxy:設(shè)置代理
cancelToken:指定一個(gè)可取消請(qǐng)求的token棒口,
validateStatus:function(status)寄月,可以通過返回true來劃定不合法范圍的狀態(tài)碼。
響應(yīng)數(shù)據(jù)格式
{
? data:response數(shù)據(jù)主體无牵,
? status:響應(yīng)狀態(tài)碼漾肮,
? statusText:響應(yīng)狀態(tài)碼的狀態(tài)文本,
? headers:Response的header茎毁,
? config:配置給axios的config克懊,
? request:發(fā)送的請(qǐng)求
}
數(shù)據(jù)截獲
const requestInterceptor = axiosInstance.interceptors.request.use(function (config) {
? ? // request請(qǐng)求之前截獲
? ? return config;
? }, function (error) {
? ? // request報(bào)錯(cuò)的處理
? ? return Promise.reject(error);
? });
const responseInterceptor = axiosInstance.interceptors.response.use(function (response) {
? ? // 獲得到response后的截獲
? ? return response;
? }, function (error) {
? ? // response報(bào)錯(cuò)的處理
? ? return Promise.reject(error);
? });
// 截獲可以通過該方法移除
axios.interceptors.request.eject(requestInterceptor);
取消請(qǐng)求
先通過const source = axios.CancelToken.source()創(chuàng)建一個(gè)生產(chǎn)cancelToken的工廠函數(shù)。
把source.token 賦值給配置中的cancelToken屬性七蜘。
在適當(dāng)場(chǎng)合谭溉,通過source.cancel(message)來取消。
axios.isCancel在結(jié)果的catch方法中調(diào)用橡卤,可以判斷錯(cuò)誤來源是否是被取消的請(qǐng)求夜只。
source.token可以傳遞給多個(gè)request,來一起取消
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
? cancelToken: source.token
}).catch(function (thrown) {
? if (axios.isCancel(thrown)) {
? ? console.log('請(qǐng)求已被取消', thrown.message);
? } else {
? ? // handle error
? }
});
5.2 fetch
Fetch API是新版瀏覽器提供的原生HTTP接口蒜魄,提供了和axios類似的功能扔亥,并原生支持Promise。
檢測(cè)瀏覽器是否支持Fetch
if(window.fetch){//支持fetch}
在ES5中使用Fetch
可以下載Fetch的polyfill谈为,放到項(xiàng)目中旅挤,通過script或import引入。
Fetch需要注意的地方:
服務(wù)器上傳的數(shù)據(jù)如果是json伞鲫,需要在headers里面指定content-type為application/json粘茄。
HTTP響應(yīng)了錯(cuò)誤的狀態(tài)碼,如500秕脓、400柒瓣,其結(jié)果并不會(huì)reject,而是resolve吠架,但會(huì)把Response對(duì)象的ok屬性設(shè)置成false芙贫。而reject僅僅是網(wǎng)絡(luò)故障或請(qǐng)求失敗時(shí)才會(huì)出現(xiàn)。
Fetch默認(rèn)是不帶Cookie的傍药,要發(fā)送 cookies磺平,必須設(shè)置 credentials 選項(xiàng)。
使用方法拐辽,參照Fetch API MDN
fetch(url[,config]).then((response)=>{})
fetch(request).then((response)=>{})
fetch既可以傳入一個(gè)url和config拣挪,也可以傳入一個(gè)request對(duì)象
config支持的主要的可選項(xiàng)配置。
method:請(qǐng)求使用的方法俱诸,GET/PUT/POST/DELETE/HEAD菠劝,
headers:請(qǐng)求的頭部信息,是一個(gè)Headers對(duì)象睁搭,
body:請(qǐng)求的body信息赶诊,可以傳入 Blob、BufferSource介袜、FormData甫何、URLSearchParams 或者 USVString對(duì)象,此字段不能用于GET和HEAD。
mode:請(qǐng)求模式遇伞,cors(跨域辙喂,跨域需要服務(wù)器有相應(yīng)設(shè)置)/no-cors(不跨域)/same-origin(僅同一域下),
credentials:發(fā)送認(rèn)證信息,**如果需要發(fā)送cookie鸠珠,必須提供這一選項(xiàng)**巍耗,可選項(xiàng)omit(不包含,默認(rèn))/same-orgin(同一域名可包含)/include(包含)渐排,這個(gè)不同瀏覽器默認(rèn)值不一樣炬太,最好手動(dòng)指定。
redirect:可用的 redirect模式驯耻。follow(自動(dòng)重定向)/error(如果重定向產(chǎn)生就直接拋錯(cuò))/manual(手動(dòng))
涉及到的對(duì)象
Response對(duì)象亲族,用于fetch接收響應(yīng)數(shù)據(jù)炒考,其中字段:ok-是否成功。更多詳見Response對(duì)象
Blob/FormData/string/Json/ArrayBuffer霎迫《Р浚可用于body參數(shù)配置或response的結(jié)果轉(zhuǎn)換妒貌。
5.3 跨域處理
5.3.1 跨域問題的由來
瀏覽器的同源策略
如果兩個(gè)頁面的協(xié)議,端口(如果有指定)和域名都相同,則兩個(gè)頁面具有相同的源觉痛。
IE8的同源例外(非標(biāo)準(zhǔn)同源):
兩個(gè)高度互信的域名躺酒,不會(huì)被同源策略限制啸箫;
IE8未將端口號(hào)加入到同源策略中
為什么要有同源限制
同源策略限制了從同一個(gè)源加載的文檔或腳本如何與來自另一個(gè)源的資源進(jìn)行交互馆里。這是一個(gè)用于隔離潛在惡意文件的重要安全機(jī)制,舉個(gè)例子:
有一個(gè)銀行網(wǎng)站筒扒,用戶登錄成功后怯邪,服務(wù)器會(huì)在響應(yīng)頭中加入一個(gè)Set-Cookie字段,設(shè)置當(dāng)前網(wǎng)站的cookie霎肯。
該cookie用于下次操作(如:轉(zhuǎn)賬)的時(shí)候就可以直接拿著它(存儲(chǔ)了用戶信息)來發(fā)送請(qǐng)求,如:https://bank.com/transfer?money=1000&name="李老師"(攜帶了cookie)擎颖;
如果這是在當(dāng)前銀行網(wǎng)站下沒有問題,假如有一天你的小伙伴想跟你做一個(gè)惡作劇观游,發(fā)給你一個(gè)鏈接搂捧,這個(gè)第三方鏈接你點(diǎn)開后發(fā)現(xiàn)沒啥毛病,但就在后臺(tái)偷偷調(diào)用https://bank.com/transfer?money=1000&name="李老師"(跨域訪問懂缕,而且攜帶cookie)允跑。這就相當(dāng)于利用你的cookie來給別人轉(zhuǎn)賬了(相當(dāng)于人家直接登錄了你的賬號(hào));
這就是典型的CSRF攻擊(跨站資源偽造)搪柑;
避免CSRF攻擊的方式
1. 檢查HTTP請(qǐng)求Header中的Referer字段聋丝,它標(biāo)明請(qǐng)求的來源,只有在同一網(wǎng)站請(qǐng)求Referer字段才會(huì)和請(qǐng)求網(wǎng)站域名一樣工碾;
2. 添加Token校驗(yàn)弱睦,不把用戶關(guān)鍵數(shù)據(jù)存在cookie中,而是服務(wù)端生成一個(gè)偽隨機(jī)數(shù)附加給客戶端渊额,當(dāng)發(fā)送請(qǐng)求時(shí)隨同偽隨機(jī)數(shù)一并發(fā)送給服務(wù)器進(jìn)行校驗(yàn)况木;
同源可以修改嗎?
通過document.domain可以將當(dāng)前源改為所在源的父源旬迹,使用 document.domain 來允許子域安全訪問其父域火惊,而且只用于iframe中修改,如:
// 當(dāng)前iframe源是:https://app.baidu.com/main.js
document.domain = 'baidu.com'
// 源就被改為了:https://baidu.com/main.js
// 這樣可以繞過同源檢查奔垦,但是不能把a(bǔ)pp.baidu.com 改成 google.com 這種跨父域同源
跨源腳本API訪問
當(dāng)兩個(gè)文檔不同源時(shí)屹耐,例如iframe嵌套會(huì)導(dǎo)致文檔的window、location訪問限制椿猎,如:
window.location跨源只能訪問不能修改惶岭,但可以通過window.postMessage來跨頁面?zhèn)鬟f消息寿弱;
但是對(duì)于XMLHttpRequest這種網(wǎng)絡(luò)請(qǐng)求會(huì)受到同源限制;
允許嵌入非同源的資源
script:但是語法錯(cuò)誤只能在同源腳本中捕捉到俗他;
link嵌入css脖捻,請(qǐng)求時(shí)需要設(shè)置正確的Content-type;
img/video/audio等媒體資源兆衅;
object/embed/applet等插件;
frame/iframe等嵌入其他網(wǎng)站頁面嗜浮,但是如果站點(diǎn)使用X-Frame-Options頭部可以阻止跨源羡亩;
@font-face引入字體;
5.3.2 跨域問題解決方案
1. JSONP
JSONP可以解決簡(jiǎn)單的GET請(qǐng)求跨域危融。
JSONP跨域原理
JSONP就是利用script標(biāo)簽可以引入外部資源的特性畏铆,通過在請(qǐng)求地址中加入回調(diào)方法參數(shù)作為JSONP回傳數(shù)據(jù)執(zhí)行的方法,然后服務(wù)器會(huì)根據(jù)該回調(diào)方法自動(dòng)返回執(zhí)行該方法的資源吉殃。
前端js
function jsonpCallback(data) {
? ? console.log(data)
}
function jsonpCors() {
? ? var script = document.createElement('script')
? ? script.src = "http://127.0.0.1:8080/jsonp-cors?callback=jsonpCallback"
? ? document.body.insertBefore(script, document.body.firstChild)
}
服務(wù)器端js
const query = qs.parse(req.query)
? ? if (query.callback) {
? ? ? // jsonpCallback({})
? ? ? ? res.send(`${query.callback}(${JSON.stringify({ jsonpData: "這就是回調(diào)給你的JSONP數(shù)據(jù)" })})`)
? ? } else {
? ? ? ? res.send(`console.log('沒有發(fā)現(xiàn)callback參數(shù)')`)
? ? }
因?yàn)閟cript腳本的限制辞居,JSONP只能進(jìn)行GET請(qǐng)求,而且不能傳遞復(fù)雜數(shù)據(jù)蛋勺。
2. iframe跨域
iframe可以解決上傳表單(也就是POST請(qǐng)求)的跨域問題瓦灶。
iframe跨域原理
因?yàn)橐粋€(gè)網(wǎng)頁可以通過iframe嵌入其他源的頁面,這樣就可以通過通過在網(wǎng)頁中創(chuàng)建一個(gè)iframe標(biāo)簽抱完,然后在iframe中模擬一個(gè)表單上傳操作來規(guī)避當(dāng)前域名下不能上傳信息的問題贼陶。
利用iframe來實(shí)現(xiàn)post請(qǐng)求
// 首先創(chuàng)建一個(gè)用來發(fā)送數(shù)據(jù)的iframe.
? ? const iframe = document.createElement('iframe')
? ? iframe.name = 'iframePost'
? ? // 注冊(cè)iframe的load事件處理程序,如果你需要在響應(yīng)返回時(shí)執(zhí)行一些操作的話.
? ? iframe.addEventListener('load', function () {
? ? ? ? console.log("上傳完成")
? ? })
? ? document.body.appendChild(iframe)
? ? const form = document.createElement('form')
? ? form.action = 'http://127.0.0.1:8080/forbidden-cors'
? ? form.enctype = "multipart/form-data"
? ? // 在指定的iframe中執(zhí)行form
? ? form.target = iframe.name
? ? form.method = 'post'
? ? const node = document.createElement('input')
? ? node.name = 'info'
? ? node.value = '我要拿到跨域信息'
? ? form.appendChild(node)
? ? // 表單元素需要添加到主文檔中.
? ? document.body.appendChild(form)
? ? form.submit()
? ? // 表單提交后,就可以刪除這個(gè)表單,不影響下次的數(shù)據(jù)發(fā)送.
? ? document.body.removeChild(form)
iframe跨域僅能解決表單的上傳或模擬POST操作,而且上傳的數(shù)據(jù)是表單格式而不是json格式巧娱,但是PUT碉怔、PATCH、DELETE方法不支持禁添。
3. 跨域資源共享-CORS
3.1 XMLHTTPRequest處理跨域
注意:這是處理跨域的標(biāo)準(zhǔn)做法
大多數(shù)現(xiàn)代瀏覽器都提供了XMLHttpRequest類用來提供網(wǎng)絡(luò)請(qǐng)求撮胧。
一. XMLHttpPRequest在某些情況下的請(qǐng)求可以處理成簡(jiǎn)單的跨域:
以下情況符合簡(jiǎn)單跨域的情況:
只進(jìn)行GET、POST老翘、HEAD請(qǐng)求芹啥。
請(qǐng)求的Header中的Content-Type設(shè)置成了如下三種類型:(不包括application/json哦)
text/plain
multipart/form-data
application/x-www-form-urlencoded
在上面簡(jiǎn)單跨域的情況下,客戶端發(fā)送請(qǐng)求只需要在請(qǐng)求的Header中加入如下一條信息即可:
Origin:跨域的請(qǐng)求源
Orgin:http://127.0.0.1:5500
服務(wù)器返回的Access-Control-Allow-Origin允許域
Access-Control-Allow-Origin:http://127.0.0.1:5500
XMLHttpRequest進(jìn)行簡(jiǎn)單跨域請(qǐng)求
console.log('請(qǐng)求一個(gè)簡(jiǎn)單的跨域')
// 注意只能通過GET方法并且需要保證Content-Type屬于那三種簡(jiǎn)單跨域類型才能簡(jiǎn)單跨域哦
var xhr = new XMLHttpRequest()
xhr.open("GET", "http://127.0.0.1:8080/simple-cors", true)//注意這里
xhr.setRequestHeader("Content-Type", "text/plain")//注意這里
xhr.onreadystatechange = function () {
? if (xhr.readyState === 4) {
? ? if (xhr.status >= 200 || xhr.status < 300 | xhr.status === 304) {
? ? ? console.log(xhr.responseText)
? ? } else {
? ? ? console.error('請(qǐng)求失敗,錯(cuò)誤信息:' + xhr.statusText)
? ? }
? }
}
xhr.send(null)
XHR簡(jiǎn)單跨域有以下限制:
默認(rèn)情況下不能通過xhr.setRequestHeader()設(shè)置自定義頭部酪捡;
默認(rèn)只支持GET叁征、POST和HEAD。
不能發(fā)送和接收cookie逛薇。
調(diào)用xhr.getAllResponseHeaders()返回空字符串捺疼;
二. 簡(jiǎn)單跨域有很多限制,因此后來提出了另一種解決上述問題的機(jī)制:Preflighted Requests(請(qǐng)求預(yù)檢)機(jī)制永罚。
注意:該機(jī)制IE10及其以下版本瀏覽器不支持啤呼。
預(yù)檢機(jī)制的原理
預(yù)檢機(jī)制的原理就是利用OPTIONS方法卧秘,在發(fā)送真實(shí)請(qǐng)求前,先自動(dòng)先發(fā)送一個(gè)OPTIONS請(qǐng)求詢問服務(wù)器能否繼續(xù)接下來的請(qǐng)求官扣,OPTIONS請(qǐng)求會(huì)發(fā)送以下頭部:
Origin:請(qǐng)求源地址翅敌;Access-Control-Request-Method:即將請(qǐng)求的方法;Access-Control-Request-Headers:即將請(qǐng)求的自定義的Header惕蹄,多個(gè)Header以逗號(hào)分隔蚯涮;
Orgin:http://127.0.0.1:5500
Access-Control-Request-Method:GET
Access-Control-Request-Headers:content-type
接下來服務(wù)器會(huì)對(duì)OPTIONS請(qǐng)求返回一個(gè)200狀態(tài)碼,如果允許跨域的話卖陵,響應(yīng)的Header中攜帶以下信息:
Access-Control-Allow-Orgin:允許客戶端請(qǐng)求的域
Access-Control-Allow-Methods:允許請(qǐng)求的方法
Access-Control-Allow-Headers:允許放置的自定義頭部
Access-Control-Allow-Max-Age:這個(gè)Preflight請(qǐng)求緩存的時(shí)長(zhǎng)(秒)
Access-Control-Allow-Orgin:http://127.0.0.1:5500
Access-Control-Allow-Methods:GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers:content-type
Access-Control-Allow-Max-Age:172800
Preflight請(qǐng)求之后遭顶,結(jié)果會(huì)被緩存(緩存時(shí)間不超過Access-Control-Allow-Max-Age指定的時(shí)間)。
接下來泪蔫,瀏覽器就會(huì)根據(jù)OPTIONS響應(yīng)返回的Header來判斷是否可以繼續(xù)自動(dòng)發(fā)送跨域請(qǐng)求了棒旗。
即使上面的跨域預(yù)檢請(qǐng)求之后成功請(qǐng)求了跨域,但是跨域默認(rèn)是不帶cookie的撩荣。
默認(rèn)情況下铣揉,跨域請(qǐng)求不能攜帶cookie餐曹、HTTP認(rèn)證等憑據(jù)逛拱,但是通過設(shè)置XHR對(duì)象的withCredentials=true可以讓Request請(qǐng)求支持?jǐn)y帶憑證,如果服務(wù)器接受帶憑據(jù)請(qǐng)求凸主,會(huì)在響應(yīng)的Header中加入:
Access-Control-Allow-Credentials:true
但如果服務(wù)器不接受憑證橘券,此時(shí)XHR就會(huì)執(zhí)行onerror(請(qǐng)求失敗)卿吐。
var xhr = new XMLHttpRequest()
if("withCredentials" in xhr){
? // IE10如果使用XMLHttpRequest旁舰,是沒有withCredentials屬性的,因此IE10的XMLHTTPRequest不支持跨域嗡官。
? xhr.open(method,url,ture)
}
3.2. IE8中的跨域問題解決方式
IE8中引入XDomainRequest(XDR)類型的請(qǐng)求類箭窜,它于XMLHttpRequest類似,但能實(shí)現(xiàn)穩(wěn)定的跨域操作衍腥,XDmoainRequest(XDR)有以下限制:
cookie不會(huì)再客戶端和服務(wù)器之間傳輸磺樱,也就是沒有cookie傳輸;
Request請(qǐng)求的Header只能設(shè)置Content-Type;
Response的Header無法訪問婆咸;
只支持GET和POST竹捉。
以下代碼只能在IE8環(huán)境下運(yùn)行
var xdr = new XDomainRequest();
xdr.onload = function(){
? console.log(xdr.responseText)
}
xdr.onerror = function(){
? console.log("發(fā)生了錯(cuò)誤")
}
xdr.open("GET","服務(wù)器地址")
xdr.send(null)
4. 后端做代理轉(zhuǎn)發(fā)
原理
后端代理轉(zhuǎn)發(fā)相當(dāng)于在服務(wù)器做跨域,前端資源和代理服務(wù)器在同一域下尚骄,此時(shí)前端請(qǐng)求的源是在當(dāng)前服務(wù)器块差,然后服務(wù)器接收到請(qǐng)求之后會(huì)自動(dòng)轉(zhuǎn)發(fā)請(qǐng)求到目標(biāo)服務(wù)器,然后拿到返回結(jié)果返回給前端請(qǐng)求。
Express使用http-proxy-middleware做后端代理轉(zhuǎn)發(fā)憨闰。