天(第 4 天),我们实现了第一个 API —— echo,并通过 httpie 成功调用。今天我们来尝试一下用浏览器来调用是否还能成功调通?答案是否定的。原因就是我们今天要学习的 浏览器同源策略 导致的,同时引出了 CORS 实现跨域访问。本文主要内容包括:
先来回顾一下,在 day 4 文章的实例中,我们已经通过 httpie 成功的调用了 echo 接口,如下图:
非浏览器成功调用
下面我们来写个 JavaScript 脚本,通过浏览器来调用 echo 接口。
新建一个 html 文件,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="callEcho()">call /api/util/echo</button>
<script>
function callEcho() {
fetch('http://localhost:8991/api/util/echo', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'zhangsan', num: 100 })
})
.then(res=> res.json())
.then(res=> {
console.log(res)
})
.catch(err=> {
console.log(err)
})
}
</script>
</body>
</html>
保存后,双击用浏览器打开该 HTML,点击按钮即可触发调用 echo 接口,调用结果如下:
执行失败,报被 CORS 策略阻塞
下面是 VUE 代码,添加到 VUE 项目中,执行 yarn -dev 命令运行,点击按钮,触发调用 echo 接口。
<script setup>
function callApiEcho() {
fetch('http://localhost:8991/api/util/echo', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'zhangsan', num: 100 })
})
.then(res=> res.json())
.then(res=> {
console.log(res)
})
.catch(err=> {
console.log(err)
})
}
</script>
<template>
<el-button type="primary" @click="callApiEcho">调用 /api/util/echo</el-button>
</template>
这种方式运行是有 HTTP 服务器的,调用的结果如下图,它更清晰的指出了源域 IP:
执行失败,报被 CORS 策略拒绝
CORS:Cross Origin Resource Sharing, 俗称“跨域”,全称“跨域资源共享”,是每个 WEB 项目开发人员,不管是前端还是后端,都会遇到的问题。
跨域问题是浏览器为了安全才有的,使用其它客户端工具,比如 httpie、curl 等都没有该问题。
Web 浏览器实现了一种被称为“同源策略”的安全机制,防止网页在不同域中访问资源,包括 API;而 CORS 提供了一种安全的方式,允许一个域(源域,用 origin 表示)调用另一个域中的资源,即允许在一个域下运行的 web 应用程序访问另一个域。
SOP:Same Origin Policy,同源策略。同源是指协议、域名和端口都相同,任何一个不相同都不算同源。
CORS Request 有两类:"simple" requests 和 "preflight" requests,浏览器自己会决定使用哪种请求,无需我们人为的干预。我们需要了解该机制即可。
当请求满足下面条件时,浏览器将该请求视为“simple”请求:
(1)使用 GET、POST 或 HEAD 请求
(2)使用 CORS safe-listed header
(3)使用 Content-Type header 值为 application/x-wwww-form-urlencoded、multipart/form-data 或 text/plain
(4)没有在任何 XMLHttpRequestUpload 对象上注册事件侦听器
(5)请求中未使用 ReadableStream 对象
满足这些条件的请求,则被允许继续正常执行,不会被阻止,并且在返回响应时检查 Access-Control-Allow-Origin header。
如果不是“simple” request,浏览器将使用 HTTP OPTIONS 方法自动发出预检请求。 预检请求用于确定服务端确切的 CORS 能力,判断服务端是否理解预期的 CORS 协议。 如果 OPTIONS 调用的结果指示无法请求,则不会再发起对服务端的实际请求。
预检请求将请求模式设置为 OPTIONS,并设置一组 header 来描述接下来的请求:
(1)Access-Control-Request-Method:请求的预期方法(如 GET、POST)
(2)Access-Control-Request-Headers:将随请求一起发送的自定义 header 的名称
(3)Origin:current origin
预检请求举例:
curl -i -X OPTIONS localhost:3031/api/echo \
-H 'Access-Control-Request-Method: GET' \
-H 'Access-Control-Request-Headers: Content-Type, Accept' \
-H 'Origin: http://localhost:3030'
这个例子表示,客户端向服务端询问:我想向从 http://localhost:3030 向 http://localhost:3031/api/echo 发起一个 Get 请求,该请求包含 “Content-Type, Accept” header,是否可以?
服务器判断后,在响应中包含一些类似 Access-Control-* 的 header,以指示是否允许随后的请求。 这些 header 有如下几种:
(1)Access-Control-Allow-Origin: 表示允许发请求的源, “*” 表示允许所有源访问
(2)Access-Control-Allow-Methods: 允许的 HTTP methods,以逗号分隔
(3)Access-Control-Allow-Headers: 允许发送的 custom headers,以逗号分隔
(4)Access-Control-Max-Age: preflight request 预请求响应结果的缓存时长,在这段时长内,再次调用该接口不用进行预请求调用。
一个 preflight 请求的 Response 可能是像下面这样的:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Vary: Access-Control-Request-Headers
Access-Control-Allow-Headers: Content-Type, Accept
Content-Length: 0
Date: Fri, 05 Apr 2023 11:41:08 GMT
Connection: keep-alive
看到这里,是不是已经明白为什么在浏览器调试工具“网络”窗口中经常看到发出一次请求,有两条 log 的原因了吧?
一次调用显示两条 log
对的,没错就是因为其中一条是预检请求,另外一条才是真实请求。
后端跨域配置
添加 CorsConfig.java 文件,代码如下。
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header.
// To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
.allowedOrigins("*")
//.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "DELETE")
.allowedHeaders("*")
// restful api 是无状态的,无需缓存 cookie 等信息
//.allowCredentials(true)
// Access-Control-Max-Age header 表明预检请求响应的有效时间。在有效时间内,浏览器无须为同一请求再次发起预检请求。
// 请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
// 在预检中,浏览器发送的头中包含有 HTTP 方法和真实请求中会用到的头。
// 也就是说对于同样的请求,在 max-age 规定的时间内就不用再次通过预检了,就可以直接请求了,单位s
.maxAge(1800);
}
}
支持跨域之后,我们再分别用 httpie 和 浏览器来调用 echo 接口看看有什变化。
跨域前后 httpie 调用结果对比
跨域前后 chrome 调用结果对比
从上面实践可以看到,支持跨域后,浏览器能成功调用 echo 接口了。
最后我们再来增加一个 delete 接口,来亲自见识一下“预检请求”。
从上面的解释,我们知道预检请求不能是 GET,POST 这种请求,而我们已有的 echo 接口是一个 POST 请求,所以需要新增一个符合条件的接口,这里我们增加一个 HTTP DELETE 接口来演示。
增加 delete 接口
增加一个新的 Controller,并添加 delete 接口,采用 @DeleteMapping 表示使用 HTTP DELETE 方法来请求。
import com.example.springdemo.dto.ProductQueryDto;
import com.example.springdemo.model.Result;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("api/product")
public class ProductController {
@DeleteMapping("delete")
public Result<String> delete(@RequestBody ProductQueryDto param) {
System.out.printf("[product][del] %s\n", param.getId());
Result<String> res=new Result<>();
return res.setData(param.getId());
}
}
增加 ProductQueryDto,用于接口传参。这里大家先不用去管什么是 DTO,什么是 Model,后面的分享会逐一说明的。
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ProductQueryDto {
private String id;
}
<button onclick="callDeleteProduct()">call /api/product/delete</button>
<script>
function callDeleteProduct() {
fetch('http://localhost:8991/api/product/delete', {
method: 'delete',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: 'p001' })
})
.then(res=> res.json())
.then(res=> {
console.log(res)
})
.catch(err=> {
console.log(err)
})
}
</script>
一次调用,有两条log
点击这两条数据,查看两次调用的请求头和请求响应,对比如下图:
预检请求和真正请求的对比
小结,今天掌握了浏览器的同源策略 SOP,实现跨域访问 CORS 的方法,学习了预检请求 Preflight Request 和 Simple Request,自己定义了一个符合需要 Preflight Request 的接口,通过代码亲自做了实践。
这里是云端源想IT,帮你轻松学IT”
嗨~ 今天的你过得还好吗?
我们总是先扬起尘土
然后抱怨自己看不见
- 2024.04.17 -
JavaScript是一种轻量级的编程语言,通常用于网页开发,以增强用户界面的交互性和动态性。然而在HTML中,有多种方法可以嵌入和使用JavaScript代码。
本文就带大家深入了解如何在HTML中使用JavaScript。
要在HTML中使用JavaScript,我们需要使用<script>标签。这个标签可以放在<head>或<body>部分,但通常我们会将其放在<body>部分的底部,以确保在执行JavaScript代码时,HTML文档已经完全加载。
使用 <script> 标签有两种方式:直接在页面中嵌入 JavaScript 代码和包含外部 JavaScript 文件。
包含在 <script> 标签内的 JavaScript 代码在浏览器总按照从上至下的顺序依次解释。
所有 <script> 标签都会按照他们在 HTML 中出现的先后顺序依次被解析。
HTML 为 <script> 定义了几个属性:
1)async:可选。表示应该立即下载脚本,但不妨碍页面中其他操作。该功能只对外部 JavaScript 文件有效。
如果给一个外部引入的js文件设置了这个属性,那页面在解析代码的时候遇到这个<script>的时候,一边下载该脚本文件,一边异步加载页面其他内容。
2)defer:可选。表示脚本可以延迟到整个页面完全被解析和显示之后再执行。该属性只对外部 JavaScript 文件有效。
3)src:可选。表示包含要执行代码的外部文件。
4)type:可选。表示编写代码使用的脚本语言的内容类型,目前在客户端,type 属性值一般使用 text/javascript。不过这个属性并不是必需的,如果没有指定这个属性,则其默认值仍为text/javascript。
1.1 直接在页面中嵌入JavaScript代码
内部JavaScript是将JavaScript代码放在HTML文档的<script>标签中。这样可以将JavaScript代码与HTML代码分离,使结构更清晰,易于维护。
在使用<script>元素嵌入JavaScript代码时,只须为<script>指定type属性。然后,像下面这样把JavaScript代码直接放在元素内部即可:
<script type="text/javascript">
function sayHi(){
alert("Hi!");
}
</script>
如果没有指定script属性,则其默认值为text/javascript。
包含在<script>元素内部的JavaScript代码将被从上至下依次解释。在解释器对<script>元素内部的所有代码求值完毕以前,页面中的其余内容都不会被浏览器加载或显示。
在使用<script>嵌入JavaScript代码的过程中,当代码中出现"</script>"字符串时,由于解析嵌入式代码的规则,浏览器会认为这是结束的</script>标签。可以通过转义字符“\”写成<\/script>来解决这个问题。
1.2 包含外部 JavaScript 文件
外部JavaScript是将JavaScript代码放在单独的.js文件中,然后在HTML文档中通过<script>标签的src属性引用这个文件。这种方法可以使代码更加模块化,便于重用和共享。
如果要通过<script>元素来包含外部JavaScript文件,那么src属性就是必需的。这个属性的值是一个指向外部JavaScript文件的链接。
<script type="text/javascript" src="example.js"></script>
与解析嵌入式JavaScript代码一样,在解析外部JavaScript文件(包括下载该文件)时,页面的处理也会暂时停止。
注意:带有src属性的<script>元素不应该在其<script>和</script>标签之间再包含额外的JavaScript代码。如果包含了嵌入的代码,则只会下载并执行外部脚本文件,嵌入的代码会被忽略。
通过<script>元素的src属性还可以包含来自外部域的JavaScript文件。它的src属性可以是指向当前HTML页面所在域之外的某个域中的完整URL。
<script type="text/javascript" src="http://www.somewhere.com/afile.js"></script>
于是,位于外部域中的代码也会被加载和解析。
1.3 标签的位置
在HTML中,所有的<script>标签会按照它们出现的先后顺序被解析。在不使用defer和async属性的情况下,只有当前面的<script>标签中的代码解析完成后,才会开始解析后面的<script>标签中的代码。
通常,所有的<script>标签应该放在页面的<head>标签中,这样可以将外部文件(包括CSS和JavaScript文件)的引用集中放置。
然而,如果将所有的JavaScript文件都放在<head>标签中,会导致浏览器在呈现页面内容之前必须下载、解析并执行所有JavaScript代码,这可能会造成明显的延迟,导致浏览器窗口在加载过程中出现空白。
为了避免这种延迟问题,现代Web应用程序通常会将所有的JavaScript引用放置在<body>标签中的页面内容的后面。这样做可以确保在解析JavaScript代码之前,页面的内容已经完全呈现在浏览器中,从而加快了打开网页的速度。
JavaScript 解析过程包括两个阶段:预处理(也称预编译)和执行。
1、执行过程
HTML 文档在浏览器中的解析过程是:按照文档流从上到下逐步解析页面结构和信息。
JavaScript 代码作为嵌入的脚本应该也算做 HTML 文档的组成部分,所以 JavaScript 代码在装载时的执行顺序也是根据 <script> 标签出现的顺序来确定。
你是不是厌倦了一成不变的编程模式?想要突破自我,挑战新技术想要突破自我,挑战新技术?却迟迟找不到可以练手的项目实战?是不是梦想打造一个属于自己的支付系统?那么,恭喜你,云端源想免费实战直播——《微实战-使用支付宝/微信支付服务,网站在线支付功能大揭秘》正在进行,点击前往获取源码!云端源想
2、预编译
当 JavaScript 引擎解析脚本时候,他会在与编译期对所有声明的变量和函数预先进行处理。当 JavaScript 解析器执行下面脚本时不会报错。
alert(a); //返回值 undefined
var a=1;
alert(a); //返回值 1
由于变量声明是在预编译期被处理的,在执行期间对于所有的代码来说,都是可见的,但是执行上面代码,提示的值是 undefined 而不是 1。
因为变量初始化过程发生在执行期,而不是预编译期。在执行期,JavaScript 解析器是按照代码先后顺序进行解析的,如果在前面代码行中没有为变量赋值,则 JavaScript 解析器会使用默认值 undefined 。
由于第二行中为变量 a 赋值了,所以在第三行代码中会提示变量 a 的值为 1,而不是 undefined。
fun(); //调用函数,返回值1
function fun(){
alert(1);
}
函数声明前调用函数也是合法的,并能够正确解析,所以返回值是 1。但如果是下面这种方式则 JavaScript 解释器会报错。
fun(); //调用函数,返回语法错误
var fun=function(){
alert(1);
}
上面的这个例子中定义的函数仅作为值赋值给变量 fun 。在预编译期,JavaScript 解释器只能够为声明变量 fun 进行处理,而对于变量 fun 的值,只能等到执行期时按照顺序进行赋值,自然就会出现语法错误,提示找不到对象 fun。
总结:声明变量和函数可以在文档的任意位置,但是良好的习惯应该是在所有 JavaScript 代码之前声明全局变量和函数,并对变量进行初始化赋值。在函数内部也是先声明变量,后引用。
通过今天的分享,相信大家已经对JavaScript在HTML中的应用有了一定的了解。这只是冰山一角,JavaScript的潜力远不止于此。希望这篇文章能激发大家对编程的热情,让我们一起在编程的世界里探索更多的可能性!
我们下期再见!
END
文案编辑|云端学长
文案配图|云端学长
内容由:云端源想分享
TML: HyperText Markup Language 超文本标记语言
HTML代码不区分大小写, 包括HTML标记、属性、属性值都不区分大小写;
任何空格或回车键在代码中都无效,插入空格或回车有专用的标记,分别是 、<br>
HTML标记中不要有空格,否则浏览器可能无法识别。
如何添加注释(comment:评论;注释)
<!-- -->
<comment></comment>
<!-- --> 不能留有空格
字符集
<meta http-equiv="Content-Type" content="text/html;charset=#"/>
<base target="_blank">
可以将a链接的默认属性设置为_blank属性
单个标签要有最好有结束符(可以没有结束符)
<br/> <img src="" width="" />
便于兼容XHTML(XHTML必须要有结束符)
HTML标签的属性值可以有引号,可以没有引号,为了提高代码的可读性,推荐使用引号(单引号和双引号),尽管属性值是整数,也推荐加上引号。
<marquee behavior="slide"></marquee>
便于兼容XHTML(XHTML必须要有引号)
<marquee behavior=slide></marquee>
经过测试,以上程序都可以正确运行
HTML标签涉及到的颜色值格式:
color_name 规定颜色值为颜色名称的文本颜色(比如 "red")。
hex_number 规定颜色值为十六进制值的文本颜色(比如 "#ff0000")。
rgb_number 规定颜色值为 rgb 代码的文本颜色(比如 "rgb(255,0,0)")。
transparent 透明色 color:transparent
rgba(红0-255,绿0-255,蓝0-255,透明度0-1)
opacity属性: 就是葫芦娃兄弟老六(技能包隐身)
css:
div{opacity:0.1} /*取值为0-1*/
英文(颜色值)不区分大小写
HTML中颜色值:采用十六进制兼容性最好(十六进制显示颜色效果最佳)
CSS中颜色值:不存在兼容性
红色 #FF0000
绿色 #00FF00
蓝色 #0000FF
黑色: #000000
灰色 #CCCCCC
白色 #FFFFFF
青色 #00FFFF
洋红 #FF00FF
黄色 #FFFF00
请问后缀 html 和 htm 有什么区别?
答: 1. 如果一个网站有 index.html和index.htm,默认情况下,优先访问.html
2. htm后缀是为了兼容以前的DOS系统8.3的命名规范
XHTML与HTML之间的关系?
XHTML是EXtensible HyperText Markup Language的英文缩写,即可扩展的超文本标记语言.
XHTML语言是一种标记语言,它不需要编译,可以直接由浏览器执行.
XHTML是用来代替HTML的, 是2000年w3c公布发行的.
XHTML是一种增强了的HTML,它的可扩展性和灵活性将适应未来网络应用更多的需求.
XHTML是基于XML的应用.
XHTML更简洁更严谨.
XHTML也可以说就是HTML一个升级版本.(w3c描述它为'HTML 4.01')
XHTML是大小写敏感的,XHTML与HTML是不一样的;HTML不区分大小写,标准的XHTML标签应该使用小写.
XHTML属性值必须使用引号,而HTML属性值可用引号,可不要引号
XHTML属性不能简写:如checked必须写成checked="checked"
单标记<br>, XHTML必须有结束符<br/>,而HTML可以使用<br>,也可以使用<br/>
除此之外XHTML和HTML基本相同.
网页宽度设置多少为最佳?
960px
target属性值理解
_self 在当前窗口中打开链接文件,是默认值
_blank 开启一个新的窗口打开链接文件
_parent 在父级窗口中打开文件,常用于框架页面
_top 在顶层窗口中打开文件,常用语框架页面
字符集:
charset=utf-8
Gb2312 简单中文字符集, 最常用的中文字符
Gbk 简繁体字符集, 中文字符集
Big5 繁体字符集, 台湾等等
Utf-8 世界性语言的字符集
ANSI编码格式编码格式的扩展字符集有gb2312和gbk
单位问题:
HTML属性值数值型的一般不带单位, CSS必须带单位;
强制刷新
ctrl+F5
*请认真填写需求信息,我们会在24小时内与您取得联系。