如果你是一名对未来开发趋势有所了解的开发者,那么你一定知道,前后端分离的趋势。
现在的web体系中包含这么一项技术,称之为异步请求。
与之相对应的就是,模板页面。它是我我眼中比较旧的技术体系,因为其使用的还是通过模板引擎来生产页面,简单来说类似于asp,jsp那一类,无不例外使用了模板引擎,吹牛归吹牛,现在还是很多东西用的模板引擎开发出来的,是当下的主流技术。
在这种开发模式中,前端的网页要是涉及到后端数据的话,那么百分之百会将页面丢到后端开发者,后端开发者将页面修改为模板,以适应对模板开发的要求。这就有几率触发发生前端与后端开发者之间矛盾,造成对整个团队的开发效率下降。当然,如果团队管理者分发任务得当,工作分工合理,标准制作合理,那么问题也不大。但是如果团队管理者不懂技术,对团队内部的分工也不合理,那么前后端开发者发生了矛盾,能不能解决就不好说了。
在未来的技术开发体系中,前端与后端之间的联系渐渐断开,美曰其名:前后端分离。也就是前端的页面不再会交给后端开发人员做二次修改,而是使用js解析引擎中的网络请求api实现对数据的请求。具体表现为:前端开发者通过JavaScript语言,去调用浏览器内置的JavaScript api,实现从浏览器到服务器的数据请求。
前端开发人员编写的网络程序,应该用怎么样的数据格式请求到后端,或者后端开发者应该怎么配合前端发送数据,这需要前后端开发人员进行预先协商,这个协商和前面通过模板的方式协商不同。前后端分离后协商的内容将减小,主要讨论前端与后端怎么交换数据的问题,也就是关键在于后端网路接口该如何定义,前端数据请求有什么特点以及要求,会发送什么样格式的数据。
如果没有前后端分离,那么可能就不仅仅是协商api如何定义的问题了,可能还会涉及到其他问题,造成团队总体开发效率下降。
基于以上趋势,异步请求是解决前后端分离的一项炙手可热的技术。
现在所能直接使用的异步请求有两种一种称之为ajax,另外一种就是本文要介绍的fetch。当然,你可能会说那jQuery还能异步请求呢!其实jQuery只是包装了下ajax。还有vue开发团队开发的另外一个异步请求,那就是axios,这个组件相对于jQuery要更加高级一点,还封装了一层premise,可以实现对异步请求的链式调用,编程效率更高。
而本文所介绍的fetch api是用来替代ajax的下一代异步网络请求,axios的用法跟fetch相似。fetch关键在于他是原生的,可以在js代码中直接调用,而不同于jQuery、axios需要导入组件。
以上技术列表中,带有*号结尾的是非必须掌握的,因为文中会涉及到前端到后端的完整过程,如果读者朋友不懂java后端,可以忽略这个要求,忽略后,不影响对fetch api的使用
fetch( UrlStr|RequestObj [,initPropertiesObj] )
fetch的第一个参数,大部分博客中使用的url字符串,作为异步请求的请求地址,其第二参数是可选的。如果使用fetch的时候,只传入了url字符串作为第一个参数,在这种请情况下,fetch只是一个简单的get请求。如果你需要将 fetch
改为post请求,那么就必须要将第二个参数对象中添加 method
属性,且 method
属性值等于字符串POST
字面值。
但你如果使用了Request对象作为fetch的第一个参数,那么就不需要传入fetch的第二个参数了。本质上,如果使用Request对象作为参数,fetch充其量就算个异步请求启动器了。
因本文同大多数博客一样,使用的 url
字符串作为 fetch
的第一个参数,所以下文就只贴上Request对象相关文档链接了。因为在这里介绍 Request
对象的话,那么下文将会出现相当多的内容与这里重复。也就是说在下文介绍的 InitPropertiesObj
参数的时候,其属性与 Request
对象基本一致。
相关文档:mdn:Request构造器文档
fetch方法的第二个参数:initPropertiesObj是一个对象,这个对象中定义了请求中的请求头,请求体,方法等内容。如果不传入fetch的第二个参数,那么默认就是一个简单的get请求、
以下开始介绍这个对象会被使用属性,被*号标注的表示是我所认为的、不常用的、需要了解的
(GET,POST,PUT,OPTIONS,HEAD,DELETE,CONNECT,TRACE)
(cors,no-cors,same-origin,navigate)
,是否是跨域请求{omit,same-origin,include}
{default, no-store,reload,no-cache,force-cache,only-if-cached}
{manual,follow,error}
fetch函数调用后,返回值是一个Premise对象实例,其内部包含response对象。可以通过调用Premise的then函数来获取其内部的response对象。then函数的参数是必须是一个带参函数,这样then在内部调用这个传入的函数时,就会将premise内部的response传给这个函数,以达到使用response的目的。传入then函数的函数,可以有返回值,也可以没有返回值。如果有返回值,那么then将会把这个返回值包装成premise返回出来。这样,你可以在then后面继续调用新的premise对象。这就是链式编程,可以无限调用新的premise对象的then函数。
response对象的相关文档 : mdn:response对象文档链接 , 通过premise对象的then函数获得其内部的response之后,这个response对于我来说,所用到的属性不多,我下面就简单介绍下相关属性:
除了以外,我还常用其自带的函数来解析来自后端发来的数据,通常会用两个response对象的函数:
json()
: 这个函数通过对后端返回的数据按照json字符串解析为JavaScript对象,在第二大节的时候我会使用到。text()
:这个函数式直接将后端响应的原始数据体返回。在下文中我不会使用到这个函数。 <dependency
> com.alibabagroupId
> fastjsonartifactId>
<version>1.2.68version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>4.0.1version>
dependency>
@WebFilter(urlPatterns = "/json_api")
public class CharsetFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("字符过滤器初始化成功");
}
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain)
throws IOException, ServletException
{
servletRequest.setCharacterEncoding("utf-8");
servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType("application/json");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("字符过滤器销毁成功");
}
}
@MultipartConfig //这里必须标注,如果不标注,会出现fetch的post请求数据无法接收的情况
@WebServlet(urlPatterns = "/json_api", name = "test_api")
public class FetchJsonTestServlet extends HttpServlet {
Map respMap = new HashMap();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
try (PrintWriter writer = resp.getWriter()) {
System.out.println(
String.format("get request,query string 'p' was %s",
req.getParameter("p"))
);
respMap.put("msg", "这是get返回消息");
writer.write(JSON.toJSONString(respMap));
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println(
String.format("post request,Parameter 'p' was %s, parameter 's' was %s",
req.getParameter("p"),
req.getParameter("s")));
System.out.println(
String.format("upload file name was %s",
req.getPart("f") == null ?
"null" : req.getPart("f").getSubmittedFileName()));
}
}
<html>
<head>
<meta charset="UTF-8">
<title>fetch测试title>
head>
<body>
body>
<script>
fetch('http://localhost/2020722/json_api').then(res => {
return res.json()
}).then(json => {
console.log(json)
})
script>
html>
代码解释:
以上代码表明我的使用fetch请求后,通过调用其返回premise对象的then函数,然后用箭头函数传入给then,然后这个箭头函数内部,通过调用response对象的json()函数,这个json()将后端返回的数据体按照json格式,解析为对象并返回了出来。我编写的这个箭头函数,又将这个解析好的对象返回给了then内部,这样then函数将这个解析好的对象再次包装成一个新的premise对象返回出来。我们继续调用这个新Premise对象then函数,将这个新premise内部被解析好的对象传递给我们的箭头函数,最后我在箭头函数内部将这个对象,输出到了浏览器控制台。
通过Firefox访问index.html,控制台输出结果如下
通过GoogleChrome访问inex.html结果如下
以上代码,整个流程看起来非常紧凑,这是因为在实例中使用了箭头函数,是因为这样的代码看起来整洁、简约、高效,但是对于没有搞懂premise对象的和不熟悉es6语法的读者可能不太友好。
以上index.html中的JavaScript代码等同于:
function resphandler(response) {
return response.json()
}
function jsonoutput(jsonobj) {
console.log(jsonobj)
}
let pre1 = fetch('http://localhost/2020722/json_api')
let pre2 = pre1.then(resphandler)
pre2.then(jsonoutput)
那么,通过以上的直观比较,很容易看出来箭头函数以及premise对象简单高效的优势。
fetch('http://localhost/2020722/json_api?p=param')
,这种写法等于在浏览器地址栏中输入http://localhost/2020722/json_api?p=param
,这两种方式,从http请求本质上来看是一样的,但是结果形式不一样。fetch使用的异步,而地址栏请求使用的是同步
<html>
<head>
<meta charset="UTF-8">
<title>fetch测试title>
head>
<body>
body>
<script>
fetch('http://localhost/2020722/json_api?p=param').then(res =>{
return res.json()
}).then(json >={
console.log(json)
})
script>
html>
不论是通过地址栏还是通过fetch异步请求,在后端控制台都会以下输出:
get request,query string 'p' was param
以上能说明通过地址栏和通过fetch,且fetch不额外添加参数的情况下,本质上http请求都是一个类别。
从这里开始就要涉及到较多的对象了.现index.html主体代码如下:
<head>
<meta charset="UTF-8">
<title>fetch测试title>
head>
<body>
<div id="app">
<h6>通过fetch提交的表单h6>
参数p<input type="text" name="p" value="">
参数s<input type="text" name="s" value="">
文件f<input type="file" name="f">
<button onclick="submit()">提交button>
div>
body>
<script>
function submit() {
let data = new FormData()
data.append('p', document.querySelector('input[name=p]').value)
data.append('s', document.querySelector('input[name=s]').value)
data.append('f',
document.querySelectorAll('input[type=file]').item(0).files.item(0))
fetch('http://localhost/2020722/json_api', {
method: 'POST',
body: data,
}).then(response =>{
console.log(response.ok)
})
}
script>
代码解释:
代码中主要模拟了一个普通表单,并携带文件的的post请求。以上代码中我传入了fetch的第二个参数,initPropertiesObj,我给这个对象设置了method方法,也就是说这次异步请求,是一个http的post请求,接着我配置了post请求的请求体,body属性。这里使用了FormData对象,这个对象是用于自定义表单的,对于前端开发者来说,FormData对象就是为了异步请求而存在的。FormData对象这里就不再做过多的介绍
请参考以下文档
通过GoogleChrome提交的表单内容截图
后端接收到的表单内容以及文件如下:
post request,Parameter ‘p’ was p, parameter ‘s’ was s
upload file name was titl2.png
index.html 内容修改为:
<head>
<meta charset="UTF-8">
<title>fetch测试title>
head>
<body>
<div id="app">
<h6>通过fetch提交的表单h6>
参数p<input type="text" name="p" value="">
参数s<input type="text" name="s" value="">
<button onclick="submit()">提交button>
div>
body>
<script>
function submit() {
fetch('http://localhost/2020722/json_api', {
method: 'POST',
body: JSON.stringify({
p: document.querySelector('input[name=p]').value,
s: document.querySelector('input[name=s]').value,
}),
headers: {
'content-type': 'application/json'
}
}).then(response => {
console.log(response.ok)
})
}
script>
后端servlet doPost方法代码修改为:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
try (BufferedReader reader = req.getReader()) {
String jsonStr = reader.readLine();
System.out.println(jsonStr);
Map map = JSON.parseObject(jsonStr, Map.class);
System.out.println(map);
}
}
后端输出如下
{“p”:“param”,“s”:“str”}
{p=param, s=str}
我所踩的坑,都是为了模拟form标签的默认enctype请求,也就是form标签内部的enctype为application/x-www-form-urlencoded值时,所提交的post表单请求。但是没有模拟成功过,我所做过的实验都失败了。通过总结我发现,fetch post提交的表单数据永远是按照content-type类型为multipart/form-data格式提交的,即使在fetch第二参数initPropertiesObj对象中声明content-type类型为普通表单(application/x-www-form-urlencoded)也没有用。
基于以上总结,能解释后端(java)代码中,必须在servlet上标注@MultipartConfig注解,才能接收到fetch的POST请求,否则servlet是无论如何都没法接收到来自前端的请求数据的。
所以基于目前最主流的提交数据格式来看,fetch的使用场景局限于jsonStr提交和使用FormData对象将数据提交到后端。