|
```html 如何在head标签内嵌入JavaScript代码 把JavaScript塞进<head>里,签内嵌入最直接的代码办法就是写一对<script>标签。 浏览器解析HTML是签内嵌入从上往下走的。碰到<head>里的代码<script>,它会立刻停下来,签内嵌入先把这段JS执行完,代码再继续往下解析<body>。签内嵌入这个行为本身没什么问题,代码但有个坑:如果JS里试图去拿<body>里的签内嵌入元素,比如document.getElementById('app'),代码那必然拿到null。签内嵌入因为那会儿DOM还没生出来。代码我见过不少新手栽在这上面,签内嵌入控制台报一堆Cannot read property of null,代码回头一看,签内嵌入代码就写在head里,没加任何等待机制。 所以关键不在于“能不能写”,而在于“什么时候执行”。 三种常见的嵌入方式 第一种,裸写。就是直接把逻辑放在<script>里,不加任何修饰。适合那种不依赖DOM、纯粹做初始化配置的代码。比如设置一个全局变量,或者检测浏览器特性: <head> <script> window.APP_CONFIG = { debug: false, api: '/v1' }; if (!window.Promise) { document.write('<script src="polyfill.js"><\/script>'); } </script> </head> 注意那个document.write——它在head里同步执行时是有效的,会往文档流里插入新脚本。但如果你在异步环境里用它,浏览器可能直接把整个文档清空。这招现在很少见了,属于上古技巧。 第二种,用defer。加上这个属性,脚本会异步下载,但延迟到文档解析完成后再按顺序执行。也就是说,你依然写在head里,但执行时机被推到了<body>之后。DOM已经就绪,可以放心操作元素。 <head> <script defer src="main.js"></script> </head> 内联脚本不能用defer,只有外链脚本才行。这点容易搞混。如果你非要把代码写在head的内联<script>里,又想要DOM就绪,那就得手动包一层DOMContentLoaded事件监听: <head> <script> document.addEventListener('DOMContentLoaded', function() { var btn = document.getElementById('submit'); btn.addEventListener('click', handleSubmit); }); </script> </head> 这种写法很常见,但有个细节:如果脚本在DOMContentLoaded触发之后才加载(比如通过动态插入),那回调永远不会执行。所以更稳妥的是先判断document.readyState。 第三种,async。同样只对外链脚本有效。它下载完就立刻执行,不管DOM有没有解析完,也不保证多个脚本之间的顺序。适合那种完全独立的第三方统计代码,比如埋点。但如果你在head里用async加载一个操作DOM的脚本,翻车概率极高。 一个实际踩过的坑 之前给一个项目做性能优化,把原本放在<body>底部的脚本挪到了head里,加上了defer。页面加载速度确实快了,因为脚本下载和HTML解析并行。但有个依赖jQuery的插件,它内部用了document.write来加载字体文件。结果defer环境下,document.write被浏览器忽略,字体没加载出来,页面布局全乱了。最后只能把这个插件单独拎出来,放回body底部,不加任何异步属性。 所以head里嵌JS,不是单纯的技术问题,得看脚本本身干了什么。操作DOM的、依赖其他库的、用document.write的,都得小心。纯配置、纯工具函数、不碰DOM的,随便放。 另外,内联脚本在head里会阻塞渲染。浏览器必须下载并执行完这段JS,才会继续构建DOM树。如果这段JS很重,比如一个大的JSON解析,用户会看到白屏时间变长。外链加defer或者async能缓解,但内联的没法异步。所以内联代码尽量精简,别塞大段逻辑。 最后提一句CSP。如果你的站点启用了Content-Security-Policy,并且设置了script-src 'self',那内联的<script>会被直接拦截。控制台会报Refused to execute inline script。解决办法是加nonce或者hash,或者干脆全部外链。这个坑在部署到生产环境时才容易暴露,本地开发往往没事。 写代码这事,位置决定行为。head里的JS,要么别碰DOM,要么乖乖等DOM准备好。没有第三种选择。 ``` |