6.2.2 其他逻辑实现
1. 实现ajax接口和渲染数据
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实现聊天
6.5 增加、查询分类、关键字查询、分页查询、删除、修改
加入小数浮点数问题,引入BigNumber
普通查询
类别查询
分页使用limit(pageSize).skip((page-1)*pageSize)
编写点mock数据。
表单
6.6 实现网络聊天
步骤
搭建socket服务器
前端链接
前端主动发送数据
后端主动发送数据
断开连接
应用场景
实时刷新
使用
socket.io 服务器部署
客户端部署
原理
net实现socket
WebSocket
广播机制
七. 安全和性能
五. 客户端请求
到目前为止,我们已经学习了如何通过Express搭建一个后台服务,并且能够利用路由输出HTML页面和json数据。
HTML页面我们可以直接在浏览器中输入网址自动加载,但是json数据并非直接用来渲染,往往需要在JavaScript文件中通过ajax接口请求来获取并动态拼凑成数据并插入到DOM结构中。
在讲解本节课之前,我们一般都是使用最知名的jQuery的$.ajax来进行网络请求,然后基于jQuery的css选择器来操作DOM并最终把数据渲染出来。
本节,我们学习两个纯粹的ajax库,以便为后面学习Vue和React打下基础。
5.1 axios
axios是目前最广泛使用的Ajax库,使用axios,axios库具有以下特点:
支持Promise。
同时支持node端和浏览器端。
支持拦截器等高级配置项。
支持请求的取消。
HTML中引入axios
创建axios实例
const axiosInstance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
调用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请求之后的结果是一个Promise对象
axios 支持的配置
url:如果是绝对路径则直接请求,如果是相对路径则基于baseURL的请求地址。
method:请求方法,默认GET。对于axios调用特定请求,该字段不需要。
baseURL:如果url是相对路径,则放置在url前。
transformRequest:[function(data,headers)],对于PUT、POST、PATCh请求前的body数据处理。
transformResponse:和transformRequest类似,处理响应的response数据。
headers:{},自定义header,不要在这里写浏览器一些内置Header,浏览器并不会用它来覆盖。
params:{},URL后面的参数,
data:对于POST、PUT等方法要传入body中的内容。
timeout:超时毫秒数,
withCredentials:对于跨域请求设置是否需要认证。
auth:HTTP认证。
responseType:设置响应类型,json、blob、text、stream、arraybuffer。
proxy:设置代理
cancelToken:指定一个可取消请求的token,
validateStatus:function(status),可以通过返回true来划定不合法范围的状态码。
响应数据格式
{
data:response数据主体,
status:响应状态码,
statusText:响应状态码的状态文本,
headers:Response的header,
config:配置给axios的config,
request:发送的请求
}
数据截获
const requestInterceptor = axiosInstance.interceptors.request.use(function (config) {
// request请求之前截获
return config;
}, function (error) {
// request报错的处理
return Promise.reject(error);
});
const responseInterceptor = axiosInstance.interceptors.response.use(function (response) {
// 获得到response后的截获
return response;
}, function (error) {
// response报错的处理
return Promise.reject(error);
});
// 截获可以通过该方法移除
axios.interceptors.request.eject(requestInterceptor);
取消请求
先通过const source = axios.CancelToken.source()创建一个生产cancelToken的工厂函数。
把source.token 赋值给配置中的cancelToken属性。
在适当场合,通过source.cancel(message)来取消。
axios.isCancel在结果的catch方法中调用,可以判断错误来源是否是被取消的请求。
source.token可以传递给多个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('请求已被取消', thrown.message);
} else {
// handle error
}
});
5.2 fetch
Fetch API是新版浏览器提供的原生HTTP接口,提供了和axios类似的功能,并原生支持Promise。
检测浏览器是否支持Fetch
if(window.fetch){//支持fetch}
在ES5中使用Fetch
可以下载Fetch的polyfill,放到项目中,通过script或import引入。
Fetch需要注意的地方:
服务器上传的数据如果是json,需要在headers里面指定content-type为application/json。
HTTP响应了错误的状态码,如500、400,其结果并不会reject,而是resolve,但会把Response对象的ok属性设置成false。而reject仅仅是网络故障或请求失败时才会出现。
Fetch默认是不带Cookie的,要发送 cookies,必须设置 credentials 选项。
使用方法,参照Fetch API MDN
fetch(url[,config]).then((response)=>{})
fetch(request).then((response)=>{})
fetch既可以传入一个url和config,也可以传入一个request对象
config支持的主要的可选项配置。
method:请求使用的方法,GET/PUT/POST/DELETE/HEAD,
headers:请求的头部信息,是一个Headers对象,
body:请求的body信息,可以传入 Blob、BufferSource、FormData、URLSearchParams 或者 USVString对象,此字段不能用于GET和HEAD。
mode:请求模式,cors(跨域,跨域需要服务器有相应设置)/no-cors(不跨域)/same-origin(仅同一域下),
credentials:发送认证信息,**如果需要发送cookie,必须提供这一选项**,可选项omit(不包含,默认)/same-orgin(同一域名可包含)/include(包含),这个不同浏览器默认值不一样,最好手动指定。
redirect:可用的 redirect模式。follow(自动重定向)/error(如果重定向产生就直接抛错)/manual(手动)
涉及到的对象
Response对象,用于fetch接收响应数据,其中字段:ok-是否成功。更多详见Response对象
Blob/FormData/string/Json/ArrayBuffer。可用于body参数配置或response的结果转换。
5.3 跨域处理
5.3.1 跨域问题的由来
浏览器的同源策略
如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。
IE8的同源例外(非标准同源):
两个高度互信的域名,不会被同源策略限制;
IE8未将端口号加入到同源策略中
为什么要有同源限制
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制,举个例子:
有一个银行网站,用户登录成功后,服务器会在响应头中加入一个Set-Cookie字段,设置当前网站的cookie。
该cookie用于下次操作(如:转账)的时候就可以直接拿着它(存储了用户信息)来发送请求,如:https://bank.com/transfer?money=1000&name="李老师"(携带了cookie);
如果这是在当前银行网站下没有问题,假如有一天你的小伙伴想跟你做一个恶作剧,发给你一个链接,这个第三方链接你点开后发现没啥毛病,但就在后台偷偷调用https://bank.com/transfer?money=1000&name="李老师"(跨域访问,而且携带cookie)。这就相当于利用你的cookie来给别人转账了(相当于人家直接登录了你的账号);
这就是典型的CSRF攻击(跨站资源伪造);
避免CSRF攻击的方式
1. 检查HTTP请求Header中的Referer字段,它标明请求的来源,只有在同一网站请求Referer字段才会和请求网站域名一样;
2. 添加Token校验,不把用户关键数据存在cookie中,而是服务端生成一个伪随机数附加给客户端,当发送请求时随同伪随机数一并发送给服务器进行校验;
同源可以修改吗?
通过document.domain可以将当前源改为所在源的父源,使用 document.domain 来允许子域安全访问其父域,而且只用于iframe中修改,如:
// 当前iframe源是:https://app.baidu.com/main.js
document.domain = 'baidu.com'
// 源就被改为了:https://baidu.com/main.js
// 这样可以绕过同源检查,但是不能把app.baidu.com 改成 google.com 这种跨父域同源
跨源脚本API访问
当两个文档不同源时,例如iframe嵌套会导致文档的window、location访问限制,如:
window.location跨源只能访问不能修改,但可以通过window.postMessage来跨页面传递消息;
但是对于XMLHttpRequest这种网络请求会受到同源限制;
允许嵌入非同源的资源
script:但是语法错误只能在同源脚本中捕捉到;
link嵌入css,请求时需要设置正确的Content-type;
img/video/audio等媒体资源;
object/embed/applet等插件;
frame/iframe等嵌入其他网站页面,但是如果站点使用X-Frame-Options头部可以阻止跨源;
@font-face引入字体;
5.3.2 跨域问题解决方案
1. JSONP
JSONP可以解决简单的GET请求跨域。
JSONP跨域原理
JSONP就是利用script标签可以引入外部资源的特性,通过在请求地址中加入回调方法参数作为JSONP回传数据执行的方法,然后服务器会根据该回调方法自动返回执行该方法的资源。
前端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)
}
服务器端js
const query = qs.parse(req.query)
if (query.callback) {
// jsonpCallback({})
res.send(`${query.callback}(${JSON.stringify({ jsonpData: "这就是回调给你的JSONP数据" })})`)
} else {
res.send(`console.log('没有发现callback参数')`)
}
因为script脚本的限制,JSONP只能进行GET请求,而且不能传递复杂数据。
2. iframe跨域
iframe可以解决上传表单(也就是POST请求)的跨域问题。
iframe跨域原理
因为一个网页可以通过iframe嵌入其他源的页面,这样就可以通过通过在网页中创建一个iframe标签,然后在iframe中模拟一个表单上传操作来规避当前域名下不能上传信息的问题。
利用iframe来实现post请求
// 首先创建一个用来发送数据的iframe.
const iframe = document.createElement('iframe')
iframe.name = 'iframePost'
// 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
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中执行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()
// 表单提交后,就可以删除这个表单,不影响下次的数据发送.
document.body.removeChild(form)
iframe跨域仅能解决表单的上传或模拟POST操作,而且上传的数据是表单格式而不是json格式,但是PUT、PATCH、DELETE方法不支持。
3. 跨域资源共享-CORS
3.1 XMLHTTPRequest处理跨域
注意:这是处理跨域的标准做法
大多数现代浏览器都提供了XMLHttpRequest类用来提供网络请求。
一. XMLHttpPRequest在某些情况下的请求可以处理成简单的跨域:
以下情况符合简单跨域的情况:
只进行GET、POST、HEAD请求。
请求的Header中的Content-Type设置成了如下三种类型:(不包括application/json哦)
text/plain
multipart/form-data
application/x-www-form-urlencoded
在上面简单跨域的情况下,客户端发送请求只需要在请求的Header中加入如下一条信息即可:
Origin:跨域的请求源
Orgin:http://127.0.0.1:5500
服务器返回的Access-Control-Allow-Origin允许域
Access-Control-Allow-Origin:http://127.0.0.1:5500
XMLHttpRequest进行简单跨域请求
console.log('请求一个简单的跨域')
// 注意只能通过GET方法并且需要保证Content-Type属于那三种简单跨域类型才能简单跨域哦
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('请求失败,错误信息:' + xhr.statusText)
}
}
}
xhr.send(null)
XHR简单跨域有以下限制:
默认情况下不能通过xhr.setRequestHeader()设置自定义头部;
默认只支持GET、POST和HEAD。
不能发送和接收cookie。
调用xhr.getAllResponseHeaders()返回空字符串;
二. 简单跨域有很多限制,因此后来提出了另一种解决上述问题的机制:Preflighted Requests(请求预检)机制。
注意:该机制IE10及其以下版本浏览器不支持。
预检机制的原理
预检机制的原理就是利用OPTIONS方法,在发送真实请求前,先自动先发送一个OPTIONS请求询问服务器能否继续接下来的请求,OPTIONS请求会发送以下头部:
Origin:请求源地址;Access-Control-Request-Method:即将请求的方法;Access-Control-Request-Headers:即将请求的自定义的Header,多个Header以逗号分隔;
Orgin:http://127.0.0.1:5500
Access-Control-Request-Method:GET
Access-Control-Request-Headers:content-type
接下来服务器会对OPTIONS请求返回一个200状态码,如果允许跨域的话,响应的Header中携带以下信息:
Access-Control-Allow-Orgin:允许客户端请求的域
Access-Control-Allow-Methods:允许请求的方法
Access-Control-Allow-Headers:允许放置的自定义头部
Access-Control-Allow-Max-Age:这个Preflight请求缓存的时长(秒)
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请求之后,结果会被缓存(缓存时间不超过Access-Control-Allow-Max-Age指定的时间)。
接下来,浏览器就会根据OPTIONS响应返回的Header来判断是否可以继续自动发送跨域请求了。
即使上面的跨域预检请求之后成功请求了跨域,但是跨域默认是不带cookie的。
默认情况下,跨域请求不能携带cookie、HTTP认证等凭据,但是通过设置XHR对象的withCredentials=true可以让Request请求支持携带凭证,如果服务器接受带凭据请求,会在响应的Header中加入:
Access-Control-Allow-Credentials:true
但如果服务器不接受凭证,此时XHR就会执行onerror(请求失败)。
var xhr = new XMLHttpRequest()
if("withCredentials" in xhr){
// IE10如果使用XMLHttpRequest,是没有withCredentials属性的,因此IE10的XMLHTTPRequest不支持跨域。
xhr.open(method,url,ture)
}
3.2. IE8中的跨域问题解决方式
IE8中引入XDomainRequest(XDR)类型的请求类,它于XMLHttpRequest类似,但能实现稳定的跨域操作,XDmoainRequest(XDR)有以下限制:
cookie不会再客户端和服务器之间传输,也就是没有cookie传输;
Request请求的Header只能设置Content-Type;
Response的Header无法访问;
只支持GET和POST。
以下代码只能在IE8环境下运行
var xdr = new XDomainRequest();
xdr.onload = function(){
console.log(xdr.responseText)
}
xdr.onerror = function(){
console.log("发生了错误")
}
xdr.open("GET","服务器地址")
xdr.send(null)
4. 后端做代理转发
原理
后端代理转发相当于在服务器做跨域,前端资源和代理服务器在同一域下,此时前端请求的源是在当前服务器,然后服务器接收到请求之后会自动转发请求到目标服务器,然后拿到返回结果返回给前端请求。
Express使用http-proxy-middleware做后端代理转发。