天(第 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小時內與您取得聯系。