前端不止:Web性能优化 – 关键渲染路径以及优化策略

我问你:“当你从搜索引擎的结果页面选择打开一条搜索结果时,你觉得多长时间之后,如果页面还处于白屏或者没有加载到关键信息,你会选择关掉这个窗口?”

《Designing for Performance》的作者 Lara Swanson 在2014年写过一篇文章《Web性能即用户体验》,她在文中提到“网站页面的快速加载,能够建立用户对网站的信任,增加回访率,大部分的用户其实都期待页面能够在2秒内加载完成,而当超过3秒以后,就会有接近40%的用户离开你的网站”。

Google和亚马逊的研究表明,Google页面加载的时间从0.4秒提升到0.9秒导致丢失了20%流量和广告收入,对于亚马逊,页面加载时间每增加100毫秒就意味着1%的销售额损失。可见,页面的加载速度对于用户可能的下一步操作是多么的举足轻重。

想一想,如果你希望你的网站在一秒钟之内呈现用户想看的关键信息,有哪些可行的手段?Minify,压缩,雪碧图等等。

Google的Web性能工程师 Ilya Grigorik 会告诉你,你只需要理解浏览器的关键渲染路径。

页面性能可能是一个感性的东西

页面的性能,看似是一个理性和量化的概念,实则也来自于用户的感知,主观的评价,是一个偏感性的东西。

(参考自Google关键渲染路径)

如果页面可以做到优先显示与用户操作有关的内容,就可以让用户更快速的感知到操作得到响应,这个过程叫做“优化关键渲染路径”。

什么是关键渲染路径

我记得,有一个非常经典的面试题叫做:《当浏览器地址栏输入URL并回车后,发生了什么?》。

关键渲染路径就是描述浏览器从收到 HTML、CSS 和 JavaScript 字节开始,到如何使用HTML、CSS 和 JavaScript 在屏幕上渲染像素的中间过程。

如果我们能够优化这条路径,就能让页面更快速的展示内容,给用户更好的体验。

全景图

我们先尝试站在高处,看一眼关键渲染路径的全景图,这样能够快速的领略一个大致轮廓和一些关键概念。

文档对象模型 (DOM)

DOM概念之于Web开发人员再熟悉不过了,当浏览器发出请求并接收到HTML文档后,它会有这样一个流程来构建DOM:字节 → 字符 → 令牌 → 节点 → 对象模型。

以下面这段代码为例:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

浏览器接收到HTML请求的返回结果,根据预定的流程解析HTML,文档中的“开标签”,比如<html><head>等会转换成一个令牌(Token),然后令牌转换成节点对象(Node)。

这个令牌解析并转换为节点对象的过程,也是每个节点建立关系(树形结构)的过程。例如:head的令牌出现在html令牌之后,但其闭标签出现在html闭标签之前,这就意味着headhtml的子节点,以此类推,建立节点的父子关系。

这个过程在浏览器中,叫做“Parse HTML”。

CSS 对象模型 (CSSOM)

当DOM捕获了页面的内容,我们还需要知道页面如何展示这些内容,所以需要构建CSS 对象模型(CSSOM)。

浏览器解析DOM,遇到了link标签,发现它引用了一个外部样式资源:style.css,于是浏览器会向外部请求样式资源,然后进行后续的DOM构建工作。

CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。

CSSOM有着一个和DOM构建相似的流程:字节 → 字符 → 令牌 → 节点 → CSS对象模型。

以下面的CSS样式为例,它会根据具体解析规则,将CSS文档转换成下面的树形结构:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

这种树形结构让CSS有层级继承关系,子节点会继承父节点的样式。

前面谈到CSS会阻塞浏览器的渲染过程,因为渲染树的构建同时需要DOM和CSSOM,所以当浏览器请求的style.css返回之后,浏览器会开始解析样式表,并重新计算样式(Recalculate Style),将CSS转换成CSSOM,然后进行后续的操作。

值得注意的是,CSSOM运算是一个非常复杂的过程,性能消耗会比较大,所以你会常常听到“老人们”说写样式“尽量使用classid,保证层级扁平,减少过度层叠”,而且越是通用的CSS样式,执行速度越快,越是具体(选择器)的CSS样式,则执行速度越慢。

DOM + CSSOM = 渲染树

渲染树和DOM树不同,它只会捕获一些页面上可见的元素,比如,Headerdisplay:none的元素不会放在渲染树中。

渲染树的构建会从DOM的根节点开始遍历,对于不可见节点会忽略,然后在CSSOM中找到每个对应节点的样式规则并应用,最后输出的渲染树会包含所有的可见内容和样式信息,如下图:

布局和绘制

有了渲染树,浏览器会进入布局和绘制阶段。

布局就是弄清每个对象在页面视窗(Viewport)上的确切大小和位置,它的输出是一个“盒模型”,里面准确的捕获每一个元素在页面视窗中的位置和尺寸。

在布局工作完成之后,浏览器会开始绘制,将渲染树转换成屏幕上的像素,这样,我们就能在浏览器中看到页面的内容。

短暂回顾一下“关键渲染路径”的步骤

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局。
  5. 将各个节点绘制到屏幕上。

当DOM或者CSSOM发生变化的时候,浏览器就需要再次执行一次上面的步骤。

JavaScript

到目前为止,我们还没涉及到JavaScript,但它在整个关键渲染路径中扮演着非常重要的角色,就如全景图中画的那样,我们从一段简单的代码开始:

<body>
   <p>Hello <span>web performance</span> students!</p>
   <script>
     var span = document.getElementsByTagName('span')[0];
     span.textContent = 'javascript';
   </script>
</body>

一个大家都知道的重要事实是:脚本在文档的何处插入,就在何处执行。

当HTML解析过程中遇到一个script标记时,它会暂停DOM构建,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。也就是说,执行内联的JavaScript会阻塞页面的首次渲染。

现在我们假设,这段JavaScript是外部资源。

<body>
   <p>Hello <span>web performance</span> students!</p>
   <script src="write.js"></script>
</body>

则浏览器的渲染会阻塞直到write.js的请求返回后,并执行JavaScript后,继续。

需要注意的是,在网页中引入JavaScript脚本有一个微妙事实,就是JavaScript不仅可以读取和修改DOM属性,还可以读取和修改CSSOM属性。

前面我们提到CSS是阻塞渲染的资源,当它和JavaScript一起出现在页面上时,会发生这样的事情:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path: Script</title>
  </head>
  <body>
    <p>Hello <script>document.write('web performance')</script> students!</p>
  </body>
</html>

在浏览器解析HTML构建DOM过程中,发现了link标签,于是发出请求获取style.css,然后继续构建DOM,此时,它发现script标签,由于JavaScript可能会访问样式属性,所以它会阻止JavaScript的执行直到styles.css返回并完成CSSOM构建,然后执行这一段JavaScript代码,再继续后面DOM的构建和相关渲染操作。

于是styles.css的请求不仅阻塞后面的渲染,还阻塞了DOM的构建。

如果将这段JavaScript作为外部资源,就是一个比较典型的页面结构:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path Render</title>
  </head>
  <body>
    <p>Hello <span>web performance</span></p>
    <script src="app.js"></script>
  </body>
</html>

JavaScript和CSS资源请求是并行的,但仍然需要等到CSSOM构建完成之后,JavaScript才可以执行,然后在进行后面的渲染工作。于是,当 DOM、CSSOM 和 JavaScript 执行之间有大量的依赖关系时,就很可能导致浏览器在处理及渲染网页时出现延迟。

优化策略

我们花了大量的篇幅来理解浏览器的渲染过程,理解DOM,CSSOM,渲染树,浏览器绘制,分析HTML,CSS和JS在渲染过程中的关系,我相信你已然受益匪浅,现在,我们来运用这些知识加速你的网站。

第一步,分析你的网站渲染状况

我们以Google为例,通过Chrome的Performance工具查看页面渲染情况,如下图,你应该可以清晰的看到图中有四条竖线,他们分别是什么含义呢?

(Google主页的性能分析情况)

  1. 绿色竖线,代表First Paint,即浏览器开始进行像素的绘制
  2. 黄色竖线,代表First Meaningful Paint(首次有效绘制)用户可以开始看到部分内容,但绘制仍在继续
  3. 蓝色竖线,代表大家比较熟悉的DOMContentLoaded
  4. 红色竖线,代表load,页面加载完成

优秀的网站都能够把“首次有效渲染”做到1秒之内完成,这样能够让用户更快的看到所请求的页面得到响应。如果你的网站“首次有效渲染”超过1秒,那么就非常有必要重新分析一下网站的关键渲染路径是否合理。

第二步,分析关键渲染路径

在关键渲染路径中,我们通常要关注三个点:

  1. 页面首次渲染需要的关键资源数量
  2. 关键资源的大小
  3. 关键渲染路径的往返次数(Roundtrip)

我们的策略也非常简单,就是减少关键资源数量,降低资源大小,减少关键路径的往返次数。

关键渲染的资源一般是阻止屏幕首次渲染HTML,CSS和JavaScript,所以最重要也是最难的部分的是你需要根据自己网站的实际情况分析,哪些是页面绘制的所必须的,哪些是无关的。

第三步,根据分析采取优化手段

1、减少关键资源的大小

我们首先从最简单也是最直接的减少关键资源的大小开始:

对于所有的资源(HTML,JavaScript,CSS,Image等),你都应该用上三大绝招:Minify,Compression和Cache,这里不过多的赘述里面的细节。

这一点对于HTML来说,非常关键,HTML作为渲染的关键资源,消除或者延迟加载肯定不太可能(这里指的是非局部渲染的关键HTML),能够做到是消除无用代码(比如:注释)和最小化代码(Minify)以及动态局部渲染等。

(Google对页面的HTML进行了压缩)

2、延迟JavaScript非阻塞资源加载

JavaScript和CSS都是阻塞渲染的资源,对于已经鉴别出的对于首次渲染没有起到关键作用的代码,我们首先想到的是要延迟它的加载,让它脱离关键渲染路径。

首先,对于阻塞渲染的JavaScript,应该将它放置在页面body的底部,为什么呢?

JavaScript可以查询和操作DOM和CSSOM,正如前面介绍的,HTML解析过程中构建DOM,当遇到JavaScript就停止DOM构建执行JavaScript,如果被执行的JavaScript是放置在head附近,那么很可能要被操作或者查询的DOM还没有构建到DOM当中。

而对于,非阻塞渲染的JavaScript,我们应该采用异步的方式加载,如下:

  <script src="script.js"></script>
  <script async src="script.js"></script>
  <script defer src="myscript.js"></script>
</body>
</html>

方式一:即阻塞的JavaScript,HTML解析过程中遇到script标签,发出网络请求获取script.js,在网络请求返回后,解析并执行script.js,然后浏览器继续HTML解析。

方式二:async,完全的异步操作,HTML解析遇到该标签后,发出网络请求,但不阻止HTML解析和其后面的渲染操作,当JavaScript请求返回后立刻执行,且不等待HTML解析或其他操作的完成。所以,如果脚本中有DOM操作,就并不适合。比较适合的场景是Google Analytics。

方式三:defer,HTML的解析和对JavaScript资源的网络请求是并行的,但它会等待HTML解析完成之后,才执行脚本。

(图片参考自:Asynchronous and deferred JavaScript execution explained « Peter Beverloo)

不过,async和defer,他们对浏览器的兼容性有一定的要求,但仍然应该使用它们,同时可以采用退而求其次的延迟代码执行的方法(比如:on DOMContentLoad后),特别是与首次渲染无关的计算逻辑和功能。

3、尽早和按需的加载CSS

你可能在思考,有没有异步加载CSS的需求?我认为不应该有,页面应该只引用与该页面相关的样式文件。(只不过很多时候,我们将所有的CSS都打包在了一个压缩的CSS文件中了。)目前,已经有许多帮助你分析关键渲染路径上的所需要的CSS的工具:grunt-criticalcsscriticalcriticalCSS,在线CRPCSS工具。

前面已经提到,CSS是阻塞渲染的资源,在CSSOM完全解析完成之前,浏览器不可能开始屏幕的绘画。

所以,我们应该尽早的开始对样式资源的请求,将它尽早、尽快地下载到客户端,这样解释了为什么我们看到样式资源的link标签一般都放在head表中:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Sample Site</title>
  <link href="style.css" rel="stylesheet">
</head>

CSSOM的运算是一个非常复杂和相对耗时的过程,但它也有一个特点,就是可能只有在特定的情况下才会起作用,比如:响应式设计的页面。

对于响应式页面,我们可以考虑将不同媒体上的样式分离,在<link>中使用媒体查询,浏览器仍然会下载对应的资源,但是可以避免不必要的CSSOM解析导致对渲染的阻塞。

<link href="style.css" rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<link href="print.css" rel="stylesheet" media="print">

同时,我们还应该避免在首次渲染的CSS样式中使用@import指令,因为它只有在收到并解析完带有@import规则的CSS资源之后,才会发现导入的 CSS 资源,这个时候就会重新请求,从而增加了关键渲染路径的往返次数。

4、内联CSS来提高渲染性能

到目前为止,我们已经做到了识别关键渲染资源,将非关键资源延迟加载或者不加载。那么,减少关键路径的往返次数是什么意思?其实就是减少关键渲染资源从服务器端到客户端的往返次数。比如,外链的JS和CSS文件以前CSS的@import,在页面渲染的过程中,都会重新去服务器端请求。这其实,和我们常说的减少http请求量(合并http请求)类似,但是我么从渲染路径的角度来理解这样一种性能的消耗。

根据这样的逻辑,我们很容易就想到可以将渲染必备CSS内联到HTML中,来减少渲染路径的往返次数。

实际上不少的优秀网站都采用了在head内联样式的做法:Google,百度,淘宝,京东。

(百度和Google将样式inline在head中)

关于内联样式还有更进一步的做法,在文章的一开始就提到,优化关键渲染路径就是要优先显示和用户先关内容。

所以,我们可以考虑仅仅将当前屏幕展示的内容(above-the-fold,一屏)所需的CSS内联到HTML的head中,然后采用异步的方式加载整个页面所需要的完整CSS,以便用户能够更快的看到首屏出现的内容。(inlining-critical-css-for-better-web-performance

5、一个神奇的数字14kb

在最开始我们提到,要减小关键资源的大小,那么多小比较合适呢?(废话,当然是越小越好)。

其实,有一个神奇的数字14kb,它是怎么来?

HTTP的传输层协议是TCP,TCP协议有一个慢启动的过程,即它在第一次传递数据时,只能同时传递14kb的数据块,所以当数据超多14kb时,TCP协议传递数据实际是多次的往返(roundtrip)。如果能够将渲染所需要的资源控制在14kb之内,那么就能TCP协议启动时,一次完成数据的传递。

其他Web资源和关键渲染路径的关系

你一定会思考,除了HTML,JavaScript和CSS,Web页面还包含许多其他的资源,比如:图片,网络字体(Icon Font),他们和关键渲染路径的关系是什么?

大家对图片加载感受都应该大致一样,它会在页面加载过程中或完成后,逐步显示,也就是说它不是阻塞渲染的资源,它的痛点主要在于质量和资源大小的权衡,以及请求数量带来的性能消耗(雪碧图)。

网络字体,在网络加载比较慢的情况下,用户可能会感受到字体或者图形的变化(Icon Font)。其实,浏览器在渲染树构建完成之后,会指示需要哪些字体在网页上渲染指定文本,然后分派字体请求,浏览器执行布局并将内容绘制到屏幕上,如果字体尚不可用,浏览器可能不会渲染任何文本像素,待字体可用之后,再绘制文本像素,当然,不同浏览器之间实际行为有所差异,这里不在赘述,请参考文章尾部的资料链接。

总结

优化关键渲染路径的最终目的是优先显示和用户操作相关的内容,减少低优先级资源对浏览器渲染的阻塞,从而尽早显示用户真正关心的关键内容。页面性能就是用户体验的一个重要维度,尝试用感性的思维去思考理性的代码,也许真的能受益不少。


参考资料:

  1. Google关键渲染路径
  2. 《Web性能即用户体验》
  3. https://peter.sh/experiments/asynchronous-and-deferred-javascript-execution-explained/

更多精彩洞见,请关注微信公众号:思特沃克

Share

前端不止:请告诉我,你要什么样的图标

有一个英语成语叫做一画胜千言(A picture is worth a thousand words),不知道大家有没有听过?它是指的是一张静态的图片就可表达一个复杂的概念或者与一个主题相关的图片有时比起详细的解释,能够更有效的描述有关主题。- “一画胜千言”维基百科

如果我们要用一句话来说明图标的作用,没有比这个成语更适合的词了。本篇文章,我们就来聊聊关于图标的一些事情。

一个图标的生命周期(工作流程)

关于图标的生命周期,在我个人所经历的开发项目中,有以下两种:

第一种方式:图标库(选择阶段) -> 图标使用(开发阶段)

第二种方式:图标设计(设计阶段) -> 图标导出(沟通阶段) -> 图标使用(开发阶段)

一般来说,小公司或者独立开发者会采用第一种工作流程。而大型组织或公司因为拥有更完善的团队和资源,一般会采取第二种方式,能够获得更多自主权和建立企业VI(Visual Identity,企业视觉识别)的能力。

但无论哪种方式,都包括两个角色:设计师和Web开发。只是在第一种工作方式中,设计师是不可见的。

图标的设计和使用

设计阶段通常是由不了解Web开发的设计师们来完成的,他们会根据产品的需要,绘出满足需求的图标,然后交给Web开发人员使用。

(ThoughtWorks官网“Contact with us”图标)

为什么要先介绍图标的使用,而一笔跳过导出过程呢?原因很简单,因为我们需要先知道服务的对象是谁,才知道如何正确的为它服务。

常见的三种图标的使用方式

1.使用图片

直接将设计师画好的图标,以PNG格式的图片一个个分离导出,这是最直观的图标打包方式。

(FlatIcon图标)

它的优点是:

  • 能够使用彩色的图标
  • 能够支持大部分浏览器

缺点是:

  • 图标大小是固定的(不能根据场景自由缩放)
  • Retina屏幕需要两倍图

开发人员拿到这样的图标,通常需要先将其合成为一张图片,以方便制作雪碧图,这个过程可以由开发人员自己完成,也可以由设计师来做(设计师可以根据源文件中心导出一张包含所有图标的PNG文件制作)。

制作雪碧图的工具有很多,我比较常用的在线雪碧图工具是:Sprite Cow,或者是NodeJS平台下的构建工具插件,如:webpack-spritesmith

2.直接使用svg

使用SVG(可缩放矢量图形),W3C标准是最被看好的Web端图形解决方案。它能提供如裁剪路径、Alpha通道、滤镜效果等复杂渲染能力,具备传统图片没有的矢量功能,还可以被记事本等阅读器、搜索引擎访问。

设计师可以轻松的在设计绘图软件(AI,PS)的帮助下导出SVG格式的图标/图片。

但目前,国内svg还没有被非常广泛的使用,原因在于兼容性不足,不能够很好的兼容旧的IE版本和一些Android原生浏览器。

(Can I use svg?)

上图为百度对2017年前三个月的浏览器使用进行的统计,目前国内还有超过20%的用户仍在使用IE8,9甚至是IE7。

3.IconFont

IconFont是目前最为流行的图标解决方案,顾名思义,它就是字体文件,你可以用任何一个字体编辑工具打开它,如果你打开某一个查看,就会发现它就是一些路径,这些路径可以用AI,PS,Sketch等软件来绘制。

IconFont的优点在于能够用CSS控制样式,无限缩放而不失真,支持IE7+,兼顾屏幕阅读器,不过缺点是不能支持彩色图标(拥有多种颜色的图标)。获得IconFont的方式也很简单,设计师将图标通过AI/PS转成SVG文件,然后由开发人员通过工具(在线或者本地)转换为IconFont,比如:国外的icomoon.io,国内的iconfont.cn,开源构建工具插件有gulp-iconfont等等。

产生适合Web开发的图标

“产生适合Web开发的图标”是我们本篇文章要关注的重点。

1.使用图片的方式

如果开发人员直接使用图片,则相对简单,设计师只需要针对普通屏幕和Retina屏幕准备两套图(单倍图和两倍图)。

以国内某著名的中文小说阅读网站为例,会针对不同的设备使用不同倍数的logo图片,以保证在如Retina屏幕下的清晰度。

.logo-wrap .logo a {
    display: block;
    width: 219px;
    height: 52px;
    background: url(/qd/images/logo.dbed5.png) no-repeat;
}

@media not all, not all, (-webkit-min-device-pixel-ratio: 1.3), not all, (min-resolution: 1.3dppx) {  
    .logo-wrap .logo a {
        background: url(/qd/images/logo3x.fd980.png) no-repeat;
        background-repeat: no-repeat;background-size: 217px;
    }
}
)

2.使用SVG

关于转换成SVG,这里就要引荐一下Sara Soueidan在Generate London 2015 Conference上的演讲《Sara Soueidan: SVG for Web Designers (and Developers)》(YouTube视频需要翻墙),如果不方便,Sara Soueidan有一篇博客《Tips for Creating and Exporting Better SVGs for the Web》更详细的讲解了关于SVG导出的内容,当然,还有一篇国内的翻译文章《创建和导出SVG的技巧》,最后再推荐一篇Adobe工程师michael chaize写的关于AI导出SVG的文章《Export SVG for the web with Illustrator CC》

在上述资料中,我觉得看视频更直观,顺便领略一下这位优秀的阿拉伯女性前端开发工程师(兼自由作家和演讲人)的风采。

博客和视频中谈到了多个点导出SVG需要注意的地方,由于篇幅限制,这里简单描述三个tips:

1. 选择适合绘画的画板

你有在网页上嵌入过SVG吗,给它指定一个高度和宽度,然后发现它其实比你指定的尺寸要小?开发人员常常会遇到这样的问题。

一般来说,这是因为SVG视窗中有一定大小的白色空白空间。视窗是按照样式表的指定尺寸显示的,但是它里面有额外的空白——在图形周围——使得你的图片看起来好像“缩水”了,因为这块空白在视窗里面是占空间的。为了避免这种情况,你需要确保你的画板是刚刚好能容纳里面的图像的,不要大太多。

画板的尺寸就是导出的SVG视窗的尺寸,所有画板上的空白最终都会变成视窗中的白色空白。

对于没有AI工具的开发,可以在下面的SVGO优化选项中选择“Prefer viewBox to width/height”。

2. 选择合适的导出选项

上图展示的选项是推荐的生成适合Web使用的SVG。如果你不想使用Web字体,可以选择把文本转换成轮廓。

如果SVG中包含大量的文字,这个选项output fewer tspan elements可以在很大程度上降低svg的大小。

3. 优化SVG

通常是建议在把SVG从图形编辑器中导出后,再用单独的优化工具来进行优化。比如:删除无用Comments和Metadata,简化代码,简化单个路径等。推荐的第三方工具:NodeJS工具svgomg,AI插件SVG-NOW,Sketch插件Svgo-compressor等,请参考Sara Soueidan的文章《Useful SVGO[ptimization] Tools》

3. IconFont

前面提到IconFont一般是由SVG通过工具转换而来,而如果开发最终需要使用IconFont来展示图标,那么对于导出的SVG有一些特殊要求。我在本文的前面一小节,已经介绍了几款IconFont的转换工具,每一款工具都有详细的文档来说明SVG绘制的规则,尽管不尽相同,但有一些基本原则是一致的:

  1. 将文字转换为路径
  2. 不可以使用图片(字体只是路径)
  3. 修剪画板(trimming to art boundaries)(前面已经介绍过)
  4. 将描边转化为闭合图形
  5. 简化无用的节点
  6. ……

更多关于IconFont的绘画规则,请参考:Iconfont.cn文档Icomoon文档gulp-iconfont文档fontello文档。

及时和频繁的沟通

Sara Soueidan说过一句话:“设计师和开发者应该成为好朋友”。

我们今天的话题正好涉及到这两个角色,也许你会觉得它们俩似乎有点“八竿子打不着”,但其实不是。请看下面这张图,敏捷的开发过程中不同角色共享职责,那么设计师和开发也不例外。

(敏捷开发中不同角色共享职责)

在ThoughtWorks工作,你会发现不少设计师懂HTML,CSS,甚至如何用Chrome查看元素,同时有不少开发对设计也颇有研究和兴趣。而我们的设计师和开发人员会坐在同一张桌子上一起完成工作,以保证及时和频繁的需求沟通和合作。

至于“设计师和开发者应该成为好朋友”,作为一名Dev,我就跟好多设计师都是朋友(至少我是这么认为的)。

而为了更好的做到沟通顺畅和职责共享,还出现了一种新(相对较新)的角色UI Dev,如下图。不过,关于这个角色的定义众说纷纭,我们就不在这里细聊了。

(UI Developer(参考自Stack Overflow答案))

结尾

在本篇文章中,我们谈了图标的三种使用方式:图片、SVG、IconFont,而它们也只是图标这个话题的冰山一角。虽然篇幅很短,但尤其重要的是,保证团队中设计师和开发人员便捷的协作工作,一起找到满足团队需求的解决方案,才是保证图标质量的关键。


更多精彩洞见,请关注微信公众号:思特沃克

Share

前端不止:Retina屏幕下两倍图

所见不一定即所得

眼睛是心灵的窗户,也是蒙蔽你的一种途径。

假设,我给你一张图片,你觉得肉眼可以观察到全部的细节吗?

屏幕上一张清晰的图片

肉眼在屏幕上看到图片的清晰度由三个因素决定,一是图片像素本身是否精细,二是屏幕分辨率,三是屏幕大小。

我们来逐步分析它们之间的关系:

屏幕分辨率

屏幕分辨率也就是设备分辨率,设备像素,它是物理的像素,比如,新的iPhone7,屏幕分辨率是1334 x 750像素分辨率,326 ppi。

图像大小

如果你学过《数字图像处理》这门课,那你对下面的解释就是非常熟悉了。

位图是由像素(Pixel)组成的,像素是位图最小的信息单元,存储在图像栅格中。每个像素都具有特定的位置和颜色值。按从左到右、从上到下的顺序来记录图像中每一个像素的信息,如:像素在屏幕上的位置、像素的颜色等。位图图像质量是由单位长度内像素的多少来决定的。单位长度内像素越多,分辨率越高,图像的效果越好。

假设,以上这个logo的图像大小是1334 x 750像素和iPhone7屏幕分辨率一样,那么,一位图像素对应的就是一个设备像素,这就是会是一个完全保真的显示。因为一个位置像素不能进一步分裂,我想这一点应该大家非常容易理解,也就是一个萝卜一个坑。

屏幕分辨率和屏幕尺寸

相信大部分人对上面这个设置肯定特别熟悉,有些人可能对XP,甚至98系统的样式更熟悉(一不小心暴露了年龄),在Windows系统下,提高屏幕分辨率一般都需要提高屏幕尺寸。

因为在固定屏幕的情况下,提高屏幕分辨率(如上图),图像和文字显示目标会相应缩小,原因是系统并不会自动根据屏幕尺寸和分辨率关系相应的调整文字和图标的大小,这是Windows系统自身的行为。

我相信,如果家里有年长的人使用电脑,肯定屏幕分辨率调的很低,因为这样文字和图标才会比较大,我家06年买的台式机就是这样。

也因此,我们很容易有一个错觉,那就是屏幕越大,分辨率就能越大(在单位面积内像素数量固定的情况下,尺寸越大,单个屏幕拥有的像素就越多,分辨率自然就越大)。

直到,苹果Retina屏幕的出现,原来小屏幕也可以拥有大分辨率。

PPI的概念

PPI,像素密度,即每英寸所拥有的像素数目(比如:上面iPhone 7的PPI是326),PPI数值越高,代表显示屏能够以越高的密度显示图像,画面的细节就会越丰富。

以Retina屏幕为例,它并不是像普通显示器那样通过增大尺寸来增加分辨率,而是靠提升屏幕单位面积内的像素数量,即像素密度来提升分辨率,这样就有了高像素密度屏幕。

根据上面的分析,分辨率提升了,那么图标和文字尺寸就会变小,但是Mac的操作系统不同,它自动采取相应的模式(如Mac下的HiDPI)进行适配,将缩小后的字体(苹果一直采用矢量字体)和图标重新放大,这样苹果用了更多的像素数来显示同样的内容,所以显示尺寸仍然不变。

苹果将“高像素密度屏幕”的概念营销出一个专业的术语“Retina”,将其称为双密度显示,声称人类的肉眼将无法区分单个像素。

当一个显示屏像素密度超过300ppi时,人眼就无法区分出单独的像素。这也是讲:显示设备清晰度已达到人视网膜可分辨像素的极限。因此,行动电话显示器的像素密度达到或高于300ppi就不会再出现颗粒感,而手持平板类电器显示器的像素密度达到或高于260ppi就不会再出现颗粒感,苹果电脑Mac的Retina显示器像素密度只要超过200ppi就无法区分出单独的像素。

好,说了这么多,都是谈屏幕的问题,貌似和前端开发没有什么关系,我又不是要买新手机(呵呵),那么现在,我们现在来谈谈前端的问题。

Web中的像素(CSS像素)

CSS像素是一个抽象概念,设备无关像素,简称-“DIPS”,device-independent像素,主要使用在浏览器上,用来精确的度量(确定)Web页面上的内容。

在标准情况下一个CSS像素对应一个设备像素。

.box {
  width: 200px;
  height: 300px;
  font-size: 12px;
}

上面的代码,将会在显示屏设备上绘制一个200×300像素的盒子,在标准屏幕下,它占据的就是200×300设备像素。但是在Retina屏幕下,相同的div却使用了400×600设备像素,保持相同的物理尺寸显示,导致每个像素点实际上有4倍的普通像素点。

对于图片来说也是如此:

这个时候,屏幕会怎么处理呢?其实,有点类似图像软件的放大图片功能,采用自有的算法(图像处理算法)计算放大方式。只不过,这里是苹果Retina屏幕的计算方法,一个CSS像素点实际分成了四个,造成颜色肯定会存在偏差(非全保真的显示),于是,我们看上去就变得模糊了(特别是图片,非常的明显)。

开发当中遇到这样的事情,我们应该怎么处理呢?这时,我们需要引出devicePixelRatio的概念。

devicePixelRatio设备像素比

window.devicePixelRatio是设备上物理像素和设备独立像素(device-independent pixels (dips))的比例。

公式表示就是:window.devicePixelRatio = 物理像素 / dips

  • 普通密度桌面显示屏的devicePixelRatio=1
  • 高密度桌面显示屏(Mac Retina)的devicePixelRatio=2
  • 主流手机显示屏的devicePixelRatio=2或3

举例说明,一张100×100的图片,通过CSS设置它{ width:100px; height:100px }。在普通密度桌面显示屏的电脑上打开,没有什么问题,但假设在手机/或者Retina屏幕的Mac,按照逻辑分辨率来渲染,他们的devicePixelRatio=2,那么就相当于拿4个物理像素来描绘1个电子像素。这等于拿一个2倍的放大镜去看图片,图片可能因此变得模糊。

代码如何解决呢?

原理我们明白了,那么从代码层面,我们应该如何实现呢?

一个常见的做法是把图片换成200×200的,CSS宽高不变,仍然是{ width:100px; height:100px },这样,CSS宽高换算成物理像素是200×200,图片也是200×200,就不会变糊了。可以采用媒体查询和JS操作的方式

CSS Media Queries

#element { background-image: url('hires.png'); }

@media only screen and (min-device-pixel-ratio: 2) {
    #element { background-image: url('hires@2x.png'); }
}

@media only screen and (min-device-pixel-ratio: 3) {
    #element { background-image: url('hires@3x.png'); }
}

JS查询

retinajs库

是不是适配Retina屏幕所有的图片都需要切换呢?

不是,一般情况下,不需要针对网站上的所有图片都提供两个版本(非Retina屏幕和Retina屏幕),大部分图片缩放并不会太多的影响用户的体验。

常常需要被处理的图片有:网站的logo、彩色图片图标,因为他们的图像大小都偏小,在Retina上物理像素放两倍显示就会出现模糊情况,这个时候,你就需要通过媒体查询或者JS操作来替换图片。

最后

眼睛是心灵的窗户,也是蒙蔽你的一种途径,带上知识的眼镜,将世界看个清楚。


参考资料:

  1. http://www.w3cplus.com/css/towards-retina-web.html
  2. http://www.jianshu.com/p/bb76c606f0b4
  3. https://developer.mozilla.org/zh-CN/docs/Mobile/Viewport_meta_tag
  4. http://caniuse.com/#search=devicePixelRatio
  5. https://www.web-tinker.com/article/20590.html

更多精彩洞见,请关注微信公众号:思特沃克

Share

8大前端安全问题(下)

《8大前端安全问题(上)》这篇文章里我们谈到了什么是前端安全问题,并且介绍了其中的4大典型安全问题,本篇文章将介绍剩下的4大前端安全问题,它们分别是:

  • 防火防盗防猪队友:不安全的第三方依赖包
  • 用了HTTPS也可能掉坑里
  • 本地存储数据泄露
  • 缺乏静态资源完整性校验

防火防盗防猪队友:不安全的第三方依赖包

现如今进行应用开发,就好比站在巨人的肩膀上写代码。据统计,一个应用有将近80%的代码其实是来自于第三方组件、依赖的类库等,而应用自身的代码其实只占了20%左右。无论是后端服务器应用还是前端应用开发,绝大多数时候我们都是在借助开发框架和各种类库进行快速开发。

这样做的好处显而易见,但是与此同时安全风险也在不断累积——应用使用了如此多的第三方代码,不论应用自己的代码的安全性有多高,一旦这些来自第三方的代码有安全漏洞,那么对应用整体的安全性依然会造成严峻的挑战。

(图片来自:http://t.cn/RlAQsZ0

举个例子,jQuery就存在多个已知安全漏洞,例如jQuery issue 2432,使得应用存在被XSS攻击的可能。而Node.js也有一些已知的安全漏洞,比如CVE-2017-11499,可能导致前端应用受到DoS攻击。另外,对于前端应用而言,除使用到的前端开发框架之外,通常还会依赖不少Node组件包,它们可能也有安全漏洞。

手动检查这些第三方代码有没有安全问题是个苦差事,主要是因为应用依赖的这些组件数量众多,手工检查太耗时,好在有自动化的工具可以使用,比如NSP(Node Security Platform),Snyk等等。

用了HTTPS也可能掉坑里

为了保护信息在传输过程中不被泄露,保证传输安全,使用TLS或者通俗的讲,使用HTTPS已经是当今的标准配置了。然而事情并没有这么简单,即使是服务器端开启了HTTPS,也还是存在安全隐患,黑客可以利用SSL Stripping这种攻击手段,强制让HTTPS降级回HTTP,从而继续进行中间人攻击。

问题的本质在于浏览器发出去第一次请求就被攻击者拦截了下来并做了修改,根本不给浏览器和服务器进行HTTPS通信的机会。大致过程如下,用户在浏览器里输入URL的时候往往不是从https://开始的,而是直接从域名开始输入,随后浏览器向服务器发起HTTP通信,然而由于攻击者的存在,它把服务器端返回的跳转到HTTPS页面的响应拦截了,并且代替客户端和服务器端进行后续的通信。由于这一切都是暗中进行的,所以使用前端应用的用户对此毫无察觉。

解决这个安全问题的办法是使用HSTS(HTTP Strict Transport Security),它通过下面这个HTTP Header以及一个预加载的清单,来告知浏览器在和网站进行通信的时候强制性的使用HTTPS,而不是通过明文的HTTP进行通信:

Strict-Transport-Security: max-age=<seconds>; includeSubDomains; preload

这里的“强制性”表现为浏览器无论在何种情况下都直接向服务器端发起HTTPS请求,而不再像以往那样从HTTP跳转到HTTPS。另外,当遇到证书或者链接不安全的时候,则首先警告用户,并且不再让用户选择是否继续进行不安全的通信。

(图片来自:http://t.cn/Rfj3Tku

本地存储数据泄露

以前,对于一个Web应用而言,在前端通过Cookie存储少量用户信息就足够支撑应用的正常运行了。然而随着前后端分离,尤其是后端服务无状态化架构风格的兴起,伴随着SPA应用的大量出现,存储在前端也就是用户浏览器中的数据量也在逐渐增多。

前端应用是完全暴露在用户以及攻击者面前的,在前端存储任何敏感、机密的数据,都会面临泄露的风险,就算是在前端通过JS脚本对数据进行加密基本也无济于事。

举个例子来说明,假设你的前端应用想要支持离线模式,使得用户在离线情况下依然可以使用你的应用,这就意味着你需要在本地存储用户相关的一些数据,比如说电子邮箱地址、手机号、家庭住址等PII(Personal Identifiable Information)信息,或许还有历史账单、消费记录等数据。

尽管有浏览器的同源策略限制,但是如果前端应用有XSS漏洞,那么本地存储的所有数据就都可能被攻击者的JS脚本读取到。如果用户在公用电脑上使用了这个前端应用,那么当用户离开后,这些数据是否也被彻底清除了呢?前端对数据加密后再存储看上去是个防御办法,但其实仅仅提高了一点攻击门槛而已,因为加密所用到的密钥同样存储在前端,有耐心的攻击者依然可以攻破加密这道关卡。

所以,在前端存储敏感、机密信息始终都是一件危险的事情,推荐的做法是尽可能不在前端存这些数据。

缺乏静态资源完整性校验

出于性能考虑,前端应用通常会把一些静态资源存放到CDN(Content Delivery Networks)上面,例如Javascript脚本和Stylesheet文件。这么做可以显著提高前端应用的访问速度,但与此同时却也隐含了一个新的安全风险。

如果攻击者劫持了CDN,或者对CDN中的资源进行了污染,那么我们的前端应用拿到的就是有问题的JS脚本或者Stylesheet文件,使得攻击者可以肆意篡改我们的前端页面,对用户实施攻击。这种攻击方式造成的效果和XSS跨站脚本攻击有些相似,不过不同点在于攻击者是从CDN开始实施的攻击,而传统的XSS攻击则是从有用户输入的地方开始下手的。

防御这种攻击的办法是使用浏览器提供的SRI(Subresource Integrity)功能。顾名思义,这里的Subresource指的就是HTML页面中通过<script><link>元素所指定的资源文件。

每个资源文件都可以有一个SRI值,就像下面这样。它由两部分组成,减号(-)左侧是生成SRI值用到的哈希算法名,右侧是经过Base64编码后的该资源文件的Hash值。

<script src=“https://example.js” integrity=“sha384-eivAQsRgJIi2KsTdSnfoEGIRTo25NCAqjNJNZalV63WKX3Y51adIzLT4So1pk5tX”></script>

浏览器在处理这个script元素的时候,就会检查对应的JS脚本文件的完整性,看其是否和script元素中integrity属性指定的SRI值一致,如果不匹配,浏览器则会中止对这个JS脚本的处理。

小结

在上一篇和本篇文章中,我们为大家介绍了在开发前端应用的时候容易遇到的8大安全问题,它们是:

  • 老生常谈的XSS
  • 警惕iframe带来的风险
  • 别被点击劫持了
  • 错误的内容推断
  • 防火防盗防猪队友:不安全的第三方依赖包
  • 用了HTTPS也可能掉坑里
  • 本地存储数据泄露
  • 缺乏静态资源完整性校验

我们希望能通过对这些问题的介绍,引起前端开发小伙伴的注意,尽可能提前绕过这些安全问题的坑。


更多精彩洞见,请关注微信公众号:思特沃克

Share

8大前端安全问题(上)

当我们说“前端安全问题”的时候,我们在说什么

“安全”是个很大的话题,各种安全问题的类型也是种类繁多。如果我们把安全问题按照所发生的区域来进行分类的话,那么所有发生在后端服务器、应用、服务当中的安全问题就是“后端安全问题”,所有发生在浏览器、单页面应用、Web页面当中的安全问题则算是“前端安全问题”。比如说,SQL注入漏洞发生在后端应用中,是后端安全问题,跨站脚本攻击(XSS)则是前端安全问题,因为它发生在用户的浏览器里。

除了从安全问题发生的区域来分类之外,也可以从另一个维度来判断:针对某个安全问题,团队中的哪个角色最适合来修复它?是后端开发还是前端开发?

总的来说,当我们下面在谈论“前端安全问题”的时候,我们说的是发生在浏览器、前端应用当中,或者通常由前端开发工程师来对其进行修复的安全问题。

8大前端安全问题

按照上面的分类办法,我们总结出了8大典型的前端安全问题,它们分别是:

  • 老生常谈的XSS
  • 警惕iframe带来的风险
  • 别被点击劫持了
  • 错误的内容推断
  • 防火防盗防猪队友:不安全的第三方依赖包
  • 用了HTTPS也可能掉坑里
  • 本地存储数据泄露
  • 缺失静态资源完整性校验

由于篇幅所限,本篇文章先给各位介绍前4个前端安全问题。

老生常谈的XSS

XSS是跨站脚本攻击(Cross-Site Scripting)的简称,它是个老油条了,在OWASP Web Application Top 10排行榜中长期霸榜,从未掉出过前三名。XSS这类安全问题发生的本质原因在于,浏览器错误的将攻击者提供的用户输入数据当做JavaScript脚本给执行了。

XSS有几种不同的分类办法,例如按照恶意输入的脚本是否在应用中存储,XSS被划分为“存储型XSS”和“反射型XSS”,如果按照是否和服务器有交互,又可以划分为“Server Side XSS”和“DOM based XSS”。

无论怎么分类,XSS漏洞始终是威胁用户的一个安全隐患。攻击者可以利用XSS漏洞来窃取包括用户身份信息在内的各种敏感信息、修改Web页面以欺骗用户,甚至控制受害者浏览器,或者和其他漏洞结合起来形成蠕虫攻击,等等。总之,关于XSS漏洞的利用,只有想不到没有做不到。

如何防御

防御XSS最佳的做法就是对数据进行严格的输出编码,使得攻击者提供的数据不再被浏览器认为是脚本而被误执行。例如<script>在进行HTML编码后变成了&lt;script&gt;,而这段数据就会被浏览器认为只是一段普通的字符串,而不会被当做脚本执行了。

编码也不是件容易的事情,需要根据输出数据所在的上下文来进行相应的编码。例如刚才的例子,由于数据将被放置于HTML元素中,因此进行的是HTML编码,而如果数据将被放置于URL中,则需要进行URL编码,将其变为%3Cscript%3E。此外,还有JavaScript编码、CSS编码、HTML属性编码、JSON编码等等。好在现如今的前端开发框架基本上都默认提供了前端输出编码,这大大减轻了前端开发小伙伴们的工作负担。

其他的防御措施,例如设置CSP HTTP Header、输入验证、开启浏览器XSS防御等等都是可选项,原因在于这些措施都存在被绕过的可能,并不能完全保证能防御XSS攻击。不过它们和输出编码却可以共同协作实施纵深防御策略。

你可以查阅OWASP XSS Prevention Cheat Sheet_Prevention_Cheat_Sheet),里面有关于XSS及其防御措施的详细说明。

警惕iframe带来的风险

有些时候我们的前端页面需要用到第三方提供的页面组件,通常会以iframe的方式引入。典型的例子是使用iframe在页面上添加第三方提供的广告、天气预报、社交分享插件等等。

iframe在给我们的页面带来更多丰富的内容和能力的同时,也带来了不少的安全隐患。因为iframe中的内容是由第三方来提供的,默认情况下他们不受我们的控制,他们可以在iframe中运行JavaScirpt脚本、Flash插件、弹出对话框等等,这可能会破坏前端用户体验。

如果说iframe只是有可能会给用户体验带来影响,看似风险不大,那么如果iframe中的域名因为过期而被恶意攻击者抢注,或者第三方被黑客攻破,iframe中的内容被替换掉了,从而利用用户浏览器中的安全漏洞下载安装木马、恶意勒索软件等等,这问题可就大了。

如何防御

还好在HTML5中,iframe有了一个叫做sandbox的安全属性,通过它可以对iframe的行为进行各种限制,充分实现“最小权限“原则。使用sandbox的最简单的方式就是只在iframe元素中添加上这个关键词就好,就像下面这样:

<iframe sandbox src="..."> ... </iframe>

sandbox还忠实的实现了“Secure By Default”原则,也就是说,如果你只是添加上这个属性而保持属性值为空,那么浏览器将会对iframe实施史上最严厉的调控限制,基本上来讲就是除了允许显示静态资源以外,其他什么都做不了。比如不准提交表单、不准弹窗、不准执行脚本等等,连Origin都会被强制重新分配一个唯一的值,换句话讲就是iframe中的页面访问它自己的服务器都会被算作跨域请求。

另外,sandbox也提供了丰富的配置参数,我们可以进行较为细粒度的控制。一些典型的参数如下:

  • allow-forms:允许iframe中提交form表单
  • allow-popups:允许iframe中弹出新的窗口或者标签页(例如,window.open(),showModalDialog(),target=”_blank”等等)
  • allow-scripts:允许iframe中执行JavaScript
  • allow-same-origin:允许iframe中的网页开启同源策略

更多详细的资料,可以参考iframe中关于sandbox的介绍

别被点击劫持了

有个词叫做防不胜防,我们在通过iframe使用别人提供的内容时,我们自己的页面也可能正在被不法分子放到他们精心构造的iframe或者frame当中,进行点击劫持攻击。

这是一种欺骗性比较强,同时也需要用户高度参与才能完成的一种攻击。通常的攻击步骤是这样的:

  1. 攻击者精心构造一个诱导用户点击的内容,比如Web页面小游戏
  2. 将我们的页面放入到iframe当中
  3. 利用z-index等CSS样式将这个iframe叠加到小游戏的垂直方向的正上方
  4. 把iframe设置为100%透明度
  5. 受害者访问到这个页面后,肉眼看到的是一个小游戏,如果受到诱导进行了点击的话,实际上点击到的却是iframe中的我们的页面

点击劫持的危害在于,攻击利用了受害者的用户身份,在其不知情的情况下进行一些操作。如果只是迫使用户关注某个微博账号的话,看上去仿佛还可以承受,但是如果是删除某个重要文件记录,或者窃取敏感信息,那么造成的危害可就难以承受了。

如何防御

有多种防御措施都可以防止页面遭到点击劫持攻击,例如Frame Breaking方案。一个推荐的防御方案是,使用X-Frame-Options:DENY这个HTTP Header来明确的告知浏览器,不要把当前HTTP响应中的内容在HTML Frame中显示出来。

关于点击劫持更多的细节,可以查阅OWASP Clickjacking Defense Cheat Sheet

错误的内容推断

想象这样一个攻击场景:某网站允许用户在评论里上传图片,攻击者在上传图片的时候,看似提交的是个图片文件,实则是个含有JavaScript的脚本文件。该文件逃过了文件类型校验(这涉及到了恶意文件上传这个常见安全问题,但是由于和前端相关度不高因此暂不详细介绍),在服务器里存储了下来。接下来,受害者在访问这段评论的时候,浏览器会去请求这个伪装成图片的JavaScript脚本,而此时如果浏览器错误的推断了这个响应的内容类型(MIME types),那么就会把这个图片文件当做JavaScript脚本执行,于是攻击也就成功了。

问题的关键就在于,后端服务器在返回的响应中设置的Content-Type Header仅仅只是给浏览器提供当前响应内容类型的建议,而浏览器有可能会自作主张的根据响应中的实际内容去推断内容的类型。

在上面的例子中,后端通过Content-Type Header建议浏览器按照图片来渲染这次的HTTP响应,但是浏览器发现响应中其实是JavaScript,于是就擅自做主把这段响应当做JS脚本来解释执行,安全问题也就产生了。

如何防御

浏览器根据响应内容来推断其类型,本来这是个很“智能”的功能,是浏览器强大的容错能力的体现,但是却会带来安全风险。要避免出现这样的安全问题,办法就是通过设置X-Content-Type-Options这个HTTP Header明确禁止浏览器去推断响应类型。

同样是上面的攻击场景,后端服务器返回的Content-Type建议浏览器按照图片进行内容渲染,浏览器发现有X-Content-Type-OptionsHTTP Header的存在,并且其参数值是nosniff,因此不会再去推断内容类型,而是强制按照图片进行渲染,那么因为实际上这是一段JS脚本而非真实的图片,因此这段脚本就会被浏览器当作是一个已经损坏或者格式不正确的图片来处理,而不是当作JS脚本来处理,从而最终防止了安全问题的发生。

更多关于X-Content-Type-Options的细节请参考这里

小结

本文对前端安全问题进行了一次梳理,介绍了其中4个典型的前端安全问题,包括它们发生的原因以及防御办法。在下篇文章中,我们将介绍其他的几个前端安全问题,敬请期待。


更多精彩洞见,请关注微信公众号:ThoughtWorks商业洞见

Share