<script>
标签不就是写 JavaScript 代码的地方吗?
没错!可是你知道它的async
和defer
属性是用来干嘛的吗?crossorigin
属性有什么用?为什么 Vite 项目的入口文件中要使用type="module"
?
来嘛,我们一个一个的来看!
defer
和async
首先我们要知道浏览器在解析html
文档的时候,是自上而下,一边解析一边渲染的。遇到CSS
样式和普通的script
标签,会阻塞解析过程,直到样式脚本下载并执行完成后,才会继续往下解析。
因此,我们通常都是将js
脚本放到body
的最后面,以避免阻塞html
文档的解析与渲染(实际上也阻塞了,因为脚本本身也属于解析html
文档的一部分)。
<!-- 省略 -->
<!-- 脚本文件放在body底部 -->
<script src="js/script1.js"></script>
<script src="js/script2.js"></script>
</body>
</html>
而defer
和async
属性就可以避免阻塞html
文档的解析与渲染,无论<script>
标签处于什么位置。
在解析的过程中,如果遇到有defer
或async
属性的<script>
标签,浏览器会异步下载脚本文件,页面继续解析。
- 如果是
async
属性,则会在下载完成后立即执行,且执行会阻塞页面解析。 - 如果是
defer
属性,会等到页面全部解析完成后,在DOMContentLoaded
事件之前按顺序执行。也就是说,它会阻塞DOMContentLoaded
事件,直到脚本下载并执行完成。
<!-- 可以放在任意位置 -->
<!-- 1.异步下载并立即执行 2. 不能确定执行顺序 -->
<script async src="js/script1.js"></script>
<script async src="js/script2.js"></script>
<!-- 异步下载DOMContentLoaded事件之前顺序执行 -->
<script defer src="js/script1.js"></script>
<script defer src="js/script2.js"></script>
根据上面的介绍,async
中的脚本不能操作DOM
元素,也不能依赖其他的脚本文件。其使用场景一般用于独立、不依赖DOM的脚本,如:广告、统计分析脚本,也可用于动态加载的脚本。
defer
则更加常用,它可以完全代替将脚本放在body底部的传统写法。并且性能更优,因为它是异步下载,节省了下载的时间,对前端首页性能优化有一定的作用。
思考:
defer
的方式与将脚本放在body
底部,两者有什么区别?
crossorigin
从名称上就可以看出,只有在访问跨域资源时才会用到。
我们知道,<script>
标签是可以访问跨域资源的,早期,解决跨域问题的JSONP
就是利用了此特性。
但是,跨域资源的访问也存在许多的安全问题,现在一些浏览器对跨域的脚本资源也做了一些限制。
crossorigin
会启用浏览器的CORS检查,如果不通过,浏览器就会拒绝执行此代码。当然,这需要服务端的支持,现在很多CDN平台都已支持CORS响应头。
思考:既然不配置
crossorigin
就能访问跨域资源,那为什么还要手动配置crossorigin
属性呢?这不是多此一举吗?
在实际开发中,我们可能会遇到这样的代码:
<script src="xxxxx" integrity="oqVuA..." crossorigin="anonymous"></script>
此时,crossorigin
可以配合integrity
属性进行资源完整性校验,如果资源被篡改过,浏览器就会拒绝执行。
另外,使用crossorigin
属性后,如果脚本代码报错,能够获取到详细的错误信息。
还有,如果你的网站配置了CSP内容安全策略,那么就必须使用crossorigin
属性以启用CORS来加载资源。
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://trusted-cdn.com">
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
总之,使用crossorigin
是安全性上的一个提升,如果访问第三方的资源,建议使用。
crossorigin
属性有两个值:
anonymous
:启用CORS,如果省略或使用非法值,默认使用anonymous
。use-credentials
:启用CORS,并且携带cookie信息
type属性
很久以前,我们常用的方式是type="text/javascript"
,但现在这种写法通常都被省略了,因为目前浏览器已经明确规定了js
脚本只能使用text/javascript
类型。
现在我们主要介绍另外两个属性值:module
和importmap
。
很多人接触这两个属性值可能都是源于 Vite 和 Vue,在webpack
时代,我们通常没有使用过浏览器的ES模块语法,而 Vite 正是利用了这一特性,才让我们在开发环境的编译速度变得非常快。
通俗来说,只要设置了type="module"
,浏览器就能识别我们代码中的 import
语法。
<div id="app">{{ message }}</div>
<script type="module">
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({
setup() {
const message = ref('Hello Vue!')
return {
message
}
}
}).mount('#app')
</script>
对于type="importmap"
我们可能比较陌生一点,如果我们想直接写import { createApp, ref } from 'vue'
,则需要先导入映射表,来指定导入vue
的目标地址。
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
通过这些代码我们可以一目了然的明白它们的用法。
但是使用浏览器的ES模块能力,需要考虑浏览器的兼容性,尽量避免在生产环境使用。
需要注意的是,启用浏览器的ES模块后,脚本会使用CORS方式加载,并且是异步加载,与defer
效果类似,并且自动使用严格模式。
最后
如果你认真看完了,你会发现文章对crossorigin
安全方面的介绍并不深入。比如:<script>
标签上的nonce
属性又是什么?
确实,这涉及到安全领域相关的一个话题,要深入讲解确实很难,一方面是我自己的能力有限,另一方面因为与文章主题不符。
后期我会专门写一篇关于CSP安全策略的文章,有兴趣的朋友点赞支持一下吧!