)
经常出现在页面首尾的HTML元素3、相对简单的style元素3.1、废弃的type属性3.2、局部样式特性4、script元素的水很深4.1、async属性和defer属性的区别4.2、不太一般的type属性4.2.1、可缺省的type属性值4.2.2、自定义type与文本化4.2.3、熟知typemodule4.2.4、认识typeimportmap4.3、灵活运用Import Maps4.4、nonce、integrity属性与脚本安全4.4.1、nonce属性4.4.2、integrity属性4.5、有必要使用noscript元素吗3、相对简单的style元素style元素是专门用来呈现内联CSS样式的这个元素很简单没有什么难以理解的地方。值得一提的应该是下面这几点。3.1、废弃的type属性style元素的type属性已经被废弃了所以下面代码中的typetext/css没必要再使用了。直接使用裸露的style元素即可。3.2、局部样式特性在HTML中style元素的样式表默认是全局的即style元素无论是被写在页面的头部还是尾部甚至是SVG元素的内部都可以对整个页面元素产生影响。例如下面这段代码中的circle{}语句会影响页面所有的circle元素而不仅仅是当前SVG元素中的circle元素。但是如果我们希望组件中的有些样式能够私有有没有什么办法呢答案是没有办法Chrome浏览器曾经短暂支持过一个名为scope的HTML属性可以实现局部样式特性不过很快被废弃了。当下想要实现CSS局部渲染只有一个方法就是在Shadow DOM中创建用在Web Components组件开发中例如自定义一个名为ui-button的按钮元素此时上面代码中创建的style元素样式就只会在ui-button元素的内部生效不会对外面的文档元素产生任何影响。Shadow DOM中的按钮样式如图所示。不过最近浏览器支持了一个新的名为scope的CSS规则多少有点局部渲染的味道不过这里的“布局渲染”更像一种语法糖而非局部上下文特性例如等同于另外media属性也可以被看成一种变相的“局部”特性不过这个“局部”不是指当前的文档页面而是指不同的设备环境。例如下面这段CSS样式代码只会在屏幕宽度小于480像素的设备下生效。4、script元素的水很深script元素可比style元素的水深多了它支持众多HTML属性涉及Web领域的多个方面。比方说在介绍a元素时介绍过的referrerpolicy属性script元素是支持的可以设置当前JavaScript文件请求的referrer策略又比方说在介绍link元素时介绍的fetchpriority属性script元素也是支持的可以改变当前JavaScript文件加载的优先级。4.1、async属性和defer属性的区别在默认情况下外链的多个JavaScript文件一定是按照前后顺序一个一个加载的前面的JavaScript文件没加载完是不会加载后面的JavaScript文件的。假设有一个a.js文件里面的代码是console.log(‘a’)还有一个b.js文件里面的代码是console.log(‘b’)则下面这段代码在控制台输出的结果一定是a b c。如果a.js和b.js文件都不重要好比是用户行为统计代码那么这种阻塞后续文件加载与执行的渲染表现就会带来体验问题延迟页面的可交互时间。此时我们就可以将对JavaScript文件的引用改成异步的方式使用async属性或者defer属性都可以。都可以难道没有区别吗当然有区别说“都可以”的意思是如果仅仅希望JavaScript文件进行异步加载那么两个属性都能满足但是如果希望保证加载顺序就不能随便设置了只能使用defer属性。换言之defer属性可以让JavaScript文件在异步加载的同时保证顺序所以下面这段代码的输出就会是cab。因为是异步加载且加载一定需要时间所以一定是console.log(‘c’)先执行。由于defer属性保证了顺序因此接下来的输出一定是a和b。如果将上述代码的defer改成async输出顺序就不一定是c a b了也可能是c b a。另外defer属性会推迟对DOMContentLoaded事件的执行直到所有设置了defer属性的JavaScript文件都加载完毕。4.2、不太一般的type属性script元素的type属性要比一般的属性包含更多的特性。4.2.1、可缺省的type属性值如果script元素执行的是JavaScript代码那么typetext/javascript的设置可以省去直接使用script元素即可例如但是如果服务器返回的JavaScript文件的MIME Type类型不对比如命名是.js后缀的文件而运维人员又让此静态文件的MIME Type头信息是text/html此时就有必要设置type“text/javascript”否则JavaScript代码的解析就会出问题。4.2.2、自定义type与文本化内联的script元素有一个特性就是如果定义的type类型不标准那么此时script元素里面的文本内容就会按照普通的文本字符串进行渲染而不再是JavaScript代码。例如又例如此特性特别适合在页面环境中临时存储大段信息。4.2.3、熟知type“module”接下来介绍script元素原生支持的两个type属性值module和importmap它们都是值得关注的新特性。假设有一个a.js文件代码如下所示此时我们就可以使用import语法引入a.js中的add()方法但是需要注意的是无论是内联引用还是以单独的JavaScript文件引用我们都必须设置type“module”就像下面这样。不然就会报如下所示的错误Uncaught SyntaxError:Cannot use import statementoutside a module如果是非export语法的传统JavaScript文件也可以设置typemodule吗可以也不可以我们不妨通过一个简单的例子快速了解一下。假设有一个a.js文件有如下所示的代码片段其中函数是非常传统的一个JavaScript函数则下面的HTML引用是可以正常解析的浏览器不会报错但是不要高兴得太早对于这类传统写法的JavaScript文件设置typemodule是很容易出问题的。先看下面这段HTML代码有没有问题答案是有会报错提示Uncaught ReferenceError:add is not defined而这里的“add is not defined”是由两方面的原因造成的。先看第一方面的原因私有的上下文环境也就是一旦JavaScript文件设置了type“module”其JavaScript代码的上下文环境就不再是全局对外的而是私有的要想外部可访问要么使用export语法暴露要么使用全局对象暴露例如然而上述改造还是没用还是会报同样的错误这是因为第二个方面的原因异步加载。也就是设置了epub:typemodule的script元素是异步加载与执行的异步但保证顺序和defer属性的表现类似因此正确的书写应该是像下面这样的所以typemodule是不能随便设置的。如果你有向下兼容的需求则可以使用nomodule属性例如对于支持ES6模块导入的浏览器自然也支持原生的nomodule属性此时fallback.js是被忽略的但是对于不支持nomodule属性的老浏览器会无视nomodule此时fallback.js就会被执行于是新旧浏览器都能正常运行。这里有一个小瑕疵那就是类似IE11这样的老浏览器会额外请求module.mjs文件。这里多说一句规范是建议模块JavaScript文件使用.mjs作为后缀以便和传统的JavaScript文件做区分但实际使用下来这样做的必要性并不是很大因为export和import语法已经成为主流没必要刻意区分。4.2.4、认识type“importmap”typeimportmap可以被看成JavaScript模块化加载的进阶用法。由于epub:typeimportmap所包含的知识细节比较多因此专门用一个章节详细介绍。4.3、灵活运用Import Maps在浏览器没有支持import语法之前对JavaScript模块的引入多是使用RequireJSAMD规范或者Sea.jsCMD规范这样的模块加载器为了方便开发这些模块加载器都带有URL根地址、别名、map映射等配置功能。后来前端工程化流行之后这些配置就交给了webpack或者vite这类打包工具。而这里要介绍的Import Maps与之类似其作用也是解决JavaScript模块导入的配置问题区别在于Import Maps是浏览器原生支持的更简洁更清爽。先看基本的使用案例此时如果我们要导入VUE文件就不再需要长长的URL地址了直接像下面这样做即可以上案例很好地展示了Import Maps的语法。首先在script元素上设置type“importmap”然后在script内部按照特定的格式写下JSON对象。此JSON对象支持imports和scopes两个关键字对应的释义见下表。scopes关键字平常不会用到只有当同一个JavaScript模块需要分场景加载不同资源的时候需要用到。常见的使用场景有下面两类。加载不同版本的JavaScript文件。加载本地版本和线上版本的JavaScript文件。先是对不同版本JavaScript文件的加载场景如下某项目采用Vue3架构但是其中某个页面由于历史原因只能使用Vue2该页面的入口JavaScript文件名是main-old.js于是我们就可以通过如下所示的设置进行区分。此时无论是main-old.js还是其他JavaScript文件我们都可以直接使用Vue作为标志量进行引入就像下面这样浏览器会自动根据文件或者目录确定加载哪个版本的JavaScript文件再看本地版本和线上版本的例子。在进行本地开发的时候为了实现更快的加载速度往往使用本地地址此时可以这样设置其中/src/assets/是本地开发场景下的静态资源路径。路径规范映射规则的地址也就是说冒号右侧的文件路径是有格式要求的如果是相对地址则必须以“/”“…/”或“./”开头如果是绝对定位则协议头是可以省略的也就是说直接以“//”开头也是合法的。不支持外链语法Import Maps并不支持外链语法也就是说下面的用法是不合法的这其实很好理解因为import加载依赖Import Maps设置的映射规则如果支持外链那么势必要等JSON文件加载完毕才会执行这显然会带来严重的性能问题。也正是由于这个原因对Import Maps的设置必须在所有模块JavaScript的前面进行。4.4、nonce、integrity属性与脚本安全在script元素中nonce属性和integrity属性都是用来保证脚本安全的。其中nonce属性用来设置脚本的安全随机数而integrity属性用来设置脚本的安全哈希值。4.4.1、nonce属性nonce的中文意思是“临时造的偶造的只使用一次的”配合“内容安全策略(Content Security Policy,CSP)”可以决定是否执行给定元素的请求。nonce属性诞生的背景如下。CSP可以在后端设置通过设置Response Headers也可以在前端设置如果在前端设置就使用前面介绍过的meta元素实现就像下面这样上面这段HTML代码可以让只有和页面同域名的静态资源才被加载同时会阻止页面上所有内联JavaScript代码的执行这样可以有效避免XSS攻击。例如有如下页面代码此时内联的script元素内的JavaScript代码是不被执行的也就是说页面不会出现alert提示。自然那些注入的XSS攻击代码也就不会被执行。这看起来很棒不是吗然而实际情况却不是这样的在业务开发的过程中难免会使用一些内联script元素例如全局的JavaScript配置代码而上述CSP策略会让这部分JavaScript代码也无法执行造成误伤。当然处理方法是有的使用unsafe-inline策略即可例如此时既避免了跨域资源执行又允许了内联JavaScript代码的执行。然而问题又回去了这没法阻止XSS攻击了假设某技术人员的代码实现不到位就有可能会遇到如下所示的XSS攻击。归根结底就是meta元素的策略影响面太广是全局的覆盖整个页面很容易造成误伤。如果HTML元素自身有某些属性可以重置meta元素的策略那么不就两全其美了nonce属性就是在这样的背景下诞生的通过特定的规则设置可以允许特定的内联JavaScript代码的执行。其实现通常分为下面几步。(1)生成一段随机的Base64字符串作为密钥至少包含128位数据页面每次载入时这个值都必须是新的且唯一的。下面代码示意的是字符串在Node.js环境下的生成Web侧可以试试下面的方法(2)将此随机字符串作为nonce的属性值例如(3)在Content-Security-Policy策略中指定此nonce值以nonce-开头。此时仅对nonce值进行了设置且值匹配的script代码会被执行。4.4.2、integrity属性integrity的中文意思是“完整、健全”。该属性是与SRISubresource Integrity子资源完整性密切相关的一个HTML属性。通俗来讲SRI要保证静态资源就是网站提供给用户的最原始的资源。咦难道用户看到的东西还有可能不是网站开发商提供的没错这种现象还挺普遍具有代表性的就是CDN劫持尤其在过去HTTPS还没普及的时候Web网页会遭遇各种各样的劫持。例如我们提供给用户的JavaScript文件代码是这样的结果因为某些原因该JavaScript文件的CDN被劫持被他人注入了其他代码就像下面这样此时用户看到的网页就会跳转到https://somebadsite.com这就是CDN劫持。使用integrity属性可以避免这种劫持因为浏览器会根据integrity设置的属性值做完整性验证以决定这段代码是否执行。在JavaScript中无论是何种字符串都可以通过加密算法转换成一段唯一的加密哈希例如“const str‘helloworld’”这段内容经过SHA-384加密后就是如下字符串于是我们就可以将此Base64编码格式的值应用在integrity属性上就像下面这样注意添加sha384前缀此时如果example.js被劫持由于JavaScript文件里面的内容通过SHA-384加密算法生成的Base64编码内容和设置的integrity值不匹配浏览器就会认为这个JavaScript文件是不安全的不会执行用户也就不会被带到不良网 站去了。这就是integrity属性作用的原理即加密后的值一定要和JavaScript文件里面的内容匹配以保证JavaScript文件的内容是完整的没有被篡改过。目前integrity属性支持的加密算法有sha256、sha384和sha512这些算法对应的JavaScript代码都可以在https://github.com/Caligatio/jsSHA中找到。在日常开发中我们可以求助工具自动化生成integrity属性。我认为此特性可以作为项目开发的标准配置。另外integrity属性还支持多个值其中只要有一个值与编码内容匹配就不会阻止JavaScript代码的执行例如下面随便书写的sha256-also-ok并不会影响example.js的执行4.5、有必要使用noscript元素吗noscript元素的作用是如果用户的浏览器禁止JavaScript代码运行就可以显示元素里面的内容于是开发人员会利用此元素作为禁用JavaScript代码的提示例如这看起来有点作用实际上在我看来没有任何必要。用户禁用JavaScript自己肯定是知道的而且针对如今的Web网页禁用JavaScript肯定寸步难行不存在主动禁用JavaScript的场景所以你可以放心大胆地放弃使用noscript元素。