这本书出版于2017年,虽然里面有些知识点是过时的,但涵盖前端技术体系比较全面,作为自己查漏补缺的资料。增加知识广度还是不错的,内容的深度不够,还是得靠实践项目慢慢积累。
第1章 Web前端技术基础
现代Web前端技术发展概述
借助符合特定场景的前端框架来提高开发效率,页面内容多且复杂,项目的管理和维护必须考虑用模块化和组件化的思路来管理,所谓的模块化和组件化是指采用代码管理中分治的思想,将复杂的代码结构拆分成多个独立、简单、解耦合的结构或文件分开管理,使项目结构更加清晰。
在页面内容较多、较复杂的情况下,为了让用户尽可能快速看到内容,我们可以通过异步的方式来实现,即将一部分内容先展示给用户,然后根据用户的操作,异步加载用户需要的其他内容,避免用户长时间等待。
通常,网页上的图片都是比较大的,下载时也比较消耗流量。需要考虑图片的优化处理,如使用更高压缩比webp格式的图片,在图片质量不降低的情况下,可以大幅度减小图片的网络流量消耗,提高图片加载速度。
浏览器默认可以支持文件缓存,对于一段时间内浏览器的重复请求,服务器可能会返回HTTP的304状态码或者不发送请求,让浏览器直接从本地读取内容。
从前后端分离到出现各种封装的前端框架,都在解决一个前端编程开发效率的问题。前端性能作为前端质量的一个重要部分一直倍受关注,而现在前端Virtual DOM和MNV*交互模式等的实现思路,就是为解决前端交互性能问题而出现的。
浏览器应用基础
通常我们认为浏览器主要由七部分组成:用户界面、网络、JavaScript引擎、渲染引擎、UI后端、JavaScript解释器和持久化数据存储。
浏览器引擎可以在用户界面和渲染引擎之间传送指令或在客户端本地缓存中读写数据等,是浏览器中各个部分之间相互通信的核心。浏览器渲染引擎的功能是解析DOM文档和CSS规则并将内容排版到浏览器中显示有样式的界面,也有人称之为排版引擎,我们常说的浏览器内核主要指的就是渲染引擎。
网络功能模块则是浏览器开启网络线程发送请求或下载资源文件的模块,UI后端则用于绘制基本的浏览器窗口内控件,比如组合选择框、按钮、输入框等。JavaScript解释器则是浏览器解释和执行JavaScript脚本的部分,例如V8引擎。
浏览器数据持久化存储则涉及cookie、localStorage等一些客户端存储技术,可以通过浏览器引擎提供的API进行调用。
作为前端开发者,我们需要重点理解渲染引擎的工作原理,灵活运用数据持久化存储技术,因为实际的开发工作中这部分的操作比较多,而其他几个部分都是由浏览器决定的,开发者能控制的部分相对较少。
浏览器通过网络模块下载HTML文件后进行页面解析渲染,流程主要包括以下几个步骤:解析HTML构建DOM树、构建渲染树、渲染树布局、绘制渲染树。
这里我们要关注的是渲染树的布局阶段和绘制阶段。页面生成后,如果页面元素位置发生变化,就要从布局阶段开始重新渲染,也就是页面重排,所以页面重排一定会进行后续重绘;如果页面元素只是显示样式改变而布局不变,那么页面内容改变将从绘制阶段开始,也称为页面重绘。重排通常会导致页面元素几何大小位置发生变化且伴随着重新渲染的巨大代价,因此我们要尽可能避免页面的重排,并减少页面元素的重绘。
渲染引擎对DOM渲染树的解析和输出是逐行进行的,所以渲染树前面的内容可以先渲染展示,这样就保证了较好的用户体验。
另外也尽量不要在HTML显示内容中插入script脚本等标签,script标签内容的解释执行常常会阻塞页面结构的渲染。
在渲染树逐行生成的阶段,DOM树中的节点会在CSS分析树中根据元素、类、id选择器来提取与之对应元素的一条或多条CSSRule,进行CSS规则的层叠和权重计算,得到最终生效的样式CSSRule并添加到DOM渲染树上,当每个DOM节点提取CSS样式完成时,用于页面布局和绘制的DOM渲染树便形成了。
关于CSS规则的权重计算,一般认为是!important>内联样式规则(权重1000)> id选择器(权重100)>类选择器(权重10)>元素选择器(权重1)。
浏览器缓存(Browser Caching)是浏览器端用于在本地保存数据并进行快速读取以避免重复资源请求的传输机制的统称。有效的缓存可以避免重复的网络资源请求并让浏览器快速地响应用户操作,提高页面内容的加载速度。
HTTP文件缓存是基于HTTP协议的浏览器端文件级缓存机制。
在HTML中,我们可以添加<meta>
中的Expires或Cache-Control来设置,需要注意的是,一般这里Cache-Control设置max-age的时间单位是秒,如果同时设置了Expires和Cache-Control,则只有Cache-Control的设置生效。当然服务端也需要进行对应设置,Node.js服务器可以使用中间件来这样设置静态资源文件的缓存时间。
localStorage是HTML5的一种本地缓存方案,目前主要用于浏览器端保存体积较大的数据(如AJAX返回结果等),需要了解的是,localStorage在不同浏览器中有长度限制且各不相同。大小限制指的是单个域名下localStorage的大小,所以localStorage中不适合存放过多的数据,如果数据存放超过最大限制可能会读取报错,因此在使用之后最好移除不再使用的数据。尽管单个域名下localStorage的大小是有限制的,但是可以用iframe的方式使用多个域名来突破单个页面下localStorage存储数据的最大限制。另外使用浏览器多个标签页打开同个域名页面时,localStorage内容一般是共享的。
sessionStorage和localStorage的功能类似,但是sessionStorage在浏览器关闭时会自动清空。
Cookie(或Cookies),指网站为了辨别用户身份或Session跟踪而储存在用户浏览器端的数据。Cookie信息一般会通过HTTP请求发送到服务器端。一条Cookie记录主要由键、值、域、过期时间和大小组成,一般用于保存用户的网站认证信息。
浏览器中Cookie的最大长度和单个域名支持的Cookie个数由浏览器的不同来决定。
被设置为HttpOnly的Cookie记录只能通过HTTP请求头发送到服务器端进行读写操作,这样就避免了服务器端的Cookie记录被前端JavaScript修改,保证了服务端验证Cookie的安全性。
IndexDB也是一个可在客户端存储大量结构化数据并且能在这些数据上使用索引进行高性能检索的一套API。
Application Cache是一种允许浏览器通过manifest配置文件在本地有选择性地存储JavaScript、CSS、图片等静态资源的文件级缓存机制。
使用Application Cache来实现浏览器应用具有以下三个优势。
1.离线浏览。
2.快速加载。
3.服务器负载小。
Application Cache已经开始被标准弃用,渐渐将会由ServiceWorkers来代替,所以现在不建议使用Application Cache来实现离线应用,仅作为一种技术了解即可。Application Cache仍是一个不成熟的本地缓存解决方案,实际项目中也并不推荐使用,但其设计思路为我们后面实现离线访问机制提供了方向。
cacheStorage是在ServiceWorker规范中定义的,可用于保存每个ServiceWorker声明的Cache对象,cacheStorage有open()、match()、has ()、delete()、keys ()五个核心API方法,我们在使用ServiceWorker时通常需要先注册ServiceWorker的脚本文件,然后在其脚本中运行caches的缓存控制方法,caches是浏览器提供的用于存储文件缓存管理的对象,也是浏览器提供的cacheStrage全局对象。
前端高效开发技术
前端快速调试工具Chrome浏览器,另外还有一些高效的开发者工具插件,如Postman、hostAdmin等。
辅助开发工具就是Fiddler,其基本原理是作为本地的一个代理服务,将特定的应用层网络请求拦截,来模拟需要的不同场景。服务端开发调试的工具也比较多。例如node-supervisor、node-inspector、Vorlon.js、Weinre等用于移动端浏览器的远程调试工具。
第2章 前端与协议
HTTP协议简介
HTTP(HyperText Transport Protocol,超文本传输协议)协议是WWW服务器和用户请求代理(例如浏览器等)之间通过应答请求模式传输超文本(例如HTML文件、JavaScript文件、CSS文件、图片甚至服务器接口数据等)内容的一种协议,协议的详细规范序号为RFC2616。
通常一个完整的HTTP报文由头部、空行、正文三部分组成。空行用于区分报文头部和报文正文,由一个回车符和一个换行符组成。请求头部通常由请求类型、请求URI、协议版本和扩展内容组成;请求头中还包含其他请求头部域信息,如Accept、Cookie、Cache-Control、Host等;请求正文可以携带浏览器端请求的内容,
响应报文头部由状态码、状态描述、协议版本、扩展内容组成;响应头包含响应头部域信息,如Date、Content-Type、Cachel-Control、Expires等;HTTP协议(Hyper Text Transfer Protocol,超文本传输协议)通常运行在TCP之上,指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。HTTP协议经历了多个版本的迭代,每个版本都在前一版本的基础上进行了改进和优化。以下是对HTTP协议几个主要版本的介绍,以及使用最为广泛的版本说明:
HTTP 0.9
发布时间:1991年
特点:HTTP 0.9是HTTP协议的第一个版本,它极其简单,仅支持GET方法,且请求和响应都是单行的,不支持请求头和状态码,只能传输HTML格式的文本,不支持其他类型的资源。
使用广泛性:由于功能过于简单,HTTP 0.9在实际应用中已经很少使用。
HTTP 1.0
发布时间:1996年
特点:HTTP 1.0引入了请求头和响应头的概念,支持多种格式的资源(如图片、音频等),并引入了持久连接(keep-alive)机制,允许在一个TCP连接上发送多个请求和响应,以减少连接建立和关闭的开销。然而,对于每个请求,仍需单独的TCP连接,这在处理大量资源时效率不高。
使用广泛性:尽管HTTP 1.0在性能上有所提升,但由于其仍需要为每个请求建立单独的TCP连接,因此在现代Web应用中已逐渐被更高效的版本所取代。不过,由于HTTP 1.0的广泛兼容性和历史遗留问题,它仍然在某些场合被使用。
HTTP 1.1
发布时间:1997年
特点:HTTP 1.1是目前使用最为广泛的HTTP版本。它在HTTP 1.0的基础上进行了大量改进,包括默认启用持久连接(keep-alive),允许在同一个TCP连接上顺序传输多个请求和响应;引入了管道化(pipelining)机制,允许客户端发送多个请求而无需等待每个请求的响应;支持虚拟主机,允许在同一个IP地址上托管多个域名;增强了缓存控制机制等。
使用广泛性:由于HTTP 1.1在性能、效率和功能上的显著提升,它已成为现代Web应用中最常用的HTTP版本。无论是Web浏览器、Web服务器还是各种Web应用,都广泛支持HTTP 1.1。
HTTP 2.0
发布时间:2015年
特点:HTTP 2.0基于Google的SPDY协议开发,采用了二进制传输而非纯文本传输,引入了多路复用(multiplexing)机制,允许多个请求和响应并行交错地在同一个连接上传输;使用HPACK算法对请求和响应头部进行压缩;支持服务器推送(server push)等。这些特性共同提高了性能和效率,减少了延迟。
使用广泛性:随着Web应用的不断发展和对性能要求的提高,HTTP 2.0逐渐得到广泛应用。许多现代Web浏览器和服务器都支持HTTP 2.0,以提供更好的用户体验和更高的传输效率。
HTTP 3.0
发布时间:2018年
特点:HTTP 3.0是基于QUIC(Quick UDP Internet Connections)协议的,QUIC使用UDP传输数据,提供了更快的连接建立和更可靠的数据传输。HTTP 3.0在QUIC之上实现了HTTP的功能,通过减少延迟和改进传输机制,提供了更好的性能和效率。
使用广泛性:尽管HTTP 3.0在技术上具有诸多优势,但由于其相对较新,且需要服务器和客户端都支持才能发挥其优势,因此目前的使用广泛性还不及HTTP 1.1和HTTP 2.0。不过,随着技术的不断发展和普及,HTTP 3.0有望在未来得到更广泛的应用。
综上所述,HTTP 1.1是目前使用最为广泛的HTTP协议版本。它在性能、效率和功能上都达到了一个相对平衡的状态,能够满足大多数Web应用的需求。同时,随着技术的不断进步和Web应用的不断发展,HTTP 2.0和HTTP 3.0等更高效的协议版本也在逐渐得到应用和推广。HTTP 1.1标准发布于1999年,相对于HTTP 1.0增加了协议扩展切换、缓存、部分文件传输优化、长连接、消息传递、host头域、错误提示等一些重要的增强特性。
HTTP 2即超文本传输协议2.0版本,是HTTP协议的下一个版本。
web安全机制
XSS(Cross Site Script,跨站脚本攻击)、SQL(Structured Query Language,结构化查询语言)注入和CSRF(Cross-site Request Forgery,跨站请求伪造)均属于基础的前端安全知识。
XSS通常是由带有页面可解析内容的数据未经处理直接插入到页面上解析导致的。需要注意的是,XSS分为存储型XSS、反射型XSS、MXSS(也叫DOM XSS)三种。存储型XSS的攻击脚本常常是由前端提交的数据未经处理直接存储到数据库然后从数据库中读取出来后又直接插入到页面中所导致的;反射型XSS可能是在网页URL参数中注入了可解析内容的数据而导致的,
MXSS则是在渲染DOM属性时将攻击脚本插入DOM属性中被解析而导致的。XSS主要的防范方法是验证输入到页面上所有内容来源数据是否安全,如果可能含有脚本标签等内容则需要进行必要的转义。
SQL注入攻击主要是因为页面提交数据到服务器端后,在服务器端未进行数据验证就将数据直接拼接到SQL语句中执行,因此产生执行与预期不同的现象。主要防范措施是对前端网页提交的数据内容进行严格的检查校验。
CSRF是指非源站点按照源站点的数据请求格式提交非法数据给源站点服务器的一种攻击方法。目前解决CSRF的最佳方式就是通过加密计算的Token验证,而Token除了通过session也可以使用HTTP请求头中Authorization的特定认证字段来传递。
任何所谓的安全都是相对的,只是说理论的破解时间变长了,而不容易被攻击。很多时候要使用多种方法结合的方式来一起增加网站的安全性,可以结合验证码等手段大大减少盗刷网站用户信息的频率等,进一步增强网站内容的安全性。
网络劫持一般指网站资源请求在请求过程中因为人为的攻击导致没有加载到预期的资源内容。网络请求劫持目前主要分为两种:DNS劫持与HTTP劫持。
DNS劫持一般通过篡改DNS服务器上的域名解析记录,来返回给用户一个错误的DNS查询结果实现。
HTTP劫持是指,在用户浏览器与访问的目的服务器之间所建立的网络数据传输通道中从网关或防火墙层上监视特定数据信息,当满足一定的条件时,就会在正常的数据包中插入或修改成为攻击者设计的网络数据包,目的是让用户浏览器解释“错误”的数据,或者以弹出新窗口的形式在使用者浏览器界面上展示宣传性广告或者直接显示某块其他的内容。
请求劫持唯一可行的预防方法就是尽量使用HTTPS协议来访问目标网站。
HTTPS协议是通过加入SSL(Secure Sockets Layer)层来加密HTTP数据进行安全传输的HTTP协议,同时启用默认的443端口进行数据传输。
公钥(Public Key)与私钥(Private Key)是通过一种加密算法得到的密钥对。公钥通常用于会话加密、验证数字签名或者加密可以用相应私钥解密的数据。使用这个密钥对的时候,如果用其中一个密钥加密一段数据,则必须用另一个密钥解密。客户端在需要使用HTTPS请求数据时,首先会发起连接请求,告诉服务器将建立HTTPS连接;服务器收到通知后自己生成一个公钥并将它返回给客户端,如果是第一次请求,同时还要告诉客户端需要进行连接验证;如果需要验证,客户端接收到服务器公钥后开始发送验证请求,将一个特定的验证串使用服务器返回的公钥加密后形成密文发送给服务器,同时客户端也将自己生成的公钥发送给服务器;服务器获取到加密的报文和客户端公钥,先使用服务器私钥解密报文获得验证串,然后将验证串通过接收到的客户端公钥加密后返回给客户端;客户端再通过私钥解密验证串,判断是否为自己开始发送的验证串;如果正确,说明双方的连接是安全的,连接验证成功,客户端开始将后面的数据通过服务器初始返回的公钥不断加密发送给服务器,服务器也不断解密获取报文,并通过客户端公钥加密响应的报文内容返回给客户端验证。这样就建立了HTTPS双向的加密传输连接。在这种情况下,传输层传输的内容不会以明文的方式显示,而且HTTPS的请求只能被添加了对应数字证书的应用层代理拦截,因此第三方攻击者就无计可施了。
HTTPS请求报文和HTTP的请求报文区别不大,但是在请求的头部域字段多了upgrade-insecure-requests
,该头部字段指令很关键,它可以用于让页面打开的后续请求自动从HTTP请求升级到HTTPS请求。否则如果使用HTTPS来加载HTML文件,而HTML中加载的是HTTP链接的资源文件,则会产生Mixed Content类型的错误,并且无法加载资源。
同时我们在服务器端响应头域中也要加入下面的头域来返回给浏览器,否则浏览器默认安全显示策略会阻塞内容并提示block-all-mixed-content
类型的错误。header("Content-Security-Policy: upgrade-insecure-requests") ;
通过某些特定的head头配置,就可以完成浏览器端的安全性设置。下面我们再来看几个典型的安全消息头域设置。X-XSS-Protection
这个head消息头设置主要是用来防止浏览器中的反射性XSS问题的发生,通过这种方式可以在浏览器层面增加前端网页的安全性。Strict Transport Security(STS)
是一种用来配置浏览器和服务器之间安全通信的机制,主要用来防止中间者攻击,因为它强制所有的通信都使用HTTPS,在普通的HTTP报文请求中配置STS是没有作用的,而且攻击者也能更改这些值。
为了防止这样的现象发生,很多浏览器内置了一个配置STS的站点列表,在Chrome浏览器下可以通过访问chrome://net-internals/#hsts
查看浏览器中站点的STS列表Content-Security-Policy
我们简称它为CSP,这是一种由开发者定义的安全策略性声明,通过CSP所约束的的规则设定,浏览器只可以加载指定可信的域名来源的内容。Access-Control-Allow-Origin
是从Cross Origin Resource Sharing(CORS)
中分离出来的。这个头部设置是决定哪些网站可以访问当前服务器资源的设置,通过定义一个通配符或域名来决定是单一的网站还是所有网站可以访问服务器的资源。
前端实时协议
包括AJAX的方式在内,目前可用来在前端浏览器上进行实时通信的功能实现方式主要有WebSocket、Poll、Long-poll和DDP协议。
相对于HTTP 1.1协议,WebSocket协议的优势是方便服务器和浏览器之间的双向数据实时通信。WebSocket在网络中传输的最小单位也为帧,数据的传输也可以理解为流式的传输。Poll方案很容易理解,即浏览器采用定时向服务器发送请求轮询的方法不断发送或拉取消息。这种方案相对来说实时性较差,而且没有新消息时依然需要不断轮询,比较消耗系统资源。
HTTP请求可以设置一个较长的Timeout等待时间,这样网络轮询请求就可以维持一段较长的时间后返回结果,这也就是Long-poll(长轮询)的基本思路。相比于Poll,Long-poll的实现更加节省系统资源,实时性更好,不用持续地定时发送网络请求。
DDP(Distributed Data Protocol,分布式数据协议)是一种新型的客户端与服务器端的实时通信协议。有名的Meteor Web框架的双向实时数据更新机制底层使用的就是DDP,这种协议模式下客户端可向服务器端发起远程过程调用,客户端也可以订阅服务端数据,在服务端数据变化时,服务器会向客户端发起通知,触发浏览器响应的操作。
RESTful数据协议规范
RESTful是一种软件架构之间交互调用数据的协议风格规范,它建议以一种通用的方式来定义和管理数据交互调用接口。RESTful API的主要设计原则就是结合HTTP的固有方式来表征资源的状态变化描述,而不是通过动词加名词的方式来设计。推荐使用某一级路径来清晰地标识接口的版本号信息。
与Native交互协议
一般将移动端原生应用的开发称为移动端Native开发。Hybrid App是在Native App应用的基础上结合了Web App应用所形成的模式,我们称之为混合App。
Web到Native协议调用:
在HTML5中调用Native程序一般有两种较通用的方法,1、通过URI请求:其主要原理是,Native应用可在移动端系统中注册一个Scheme协议的URI,这个URI可在系统的任意地方授权访问来调起一段原生方法或一个原生的界面。同样,Native的WebView控件中的JavaScript脚本的请求也可以匹配调用这一通用的Scheme协议。
2、通过addJavascriptInterface注入方法到页面中调用:通过addJavascriptInterface将Java的实例对象注入到WebView中,让WebView中的页面JavaScript可以直接使用,采用这种方法也可以实现更加复杂的调用功能。
Native到Web协议调用:
如果Native需要主动调用HTML5页面中JavaScript方法或指令,需要先使用JavaScript在HTML5页面全局中声明相对应的方法,Native向HTML5发起的调用是通过loadUrl方法。
JSBridge设计规范:
以Android为例,目前使用较多的解决方案是通过一个协议串定义Native和JavaScript间的数据通信规则,这个协议串必须包括以下内容:调用Native App的特定标识头、类名称、方法名、参数、回调JavaScript的方法。
第3章 前端三层结构与应用
前端的三个基本构成:结构层HTML、表现层CSS和行为层JavaScript。
HTML结构层基础
通过更高效的工具来快速开发和管理复杂的应用项目,最后编译为基础结构运行是因为仍然有旧的浏览器需要兼容。
以HTML5的方式声明文档类型,即<!DOCTYPE html>
,HTML 4.01是W3C在1999年制定发布的HTML语言规范。HTML4.01是基于SGML(Standard Generalized Markup language,标准通用标记语言)规范来制定的。HTML5正式发布于2014年,HTML5不是基于SGML演化而来的,可以理解为是W3C的另一套实现规范。SGML的规范很多,所以我们以前使用HTML开发页面时需要对文档类型定义(Document Type Difinition,DTD)进行声明,即在DOCTYPE后面声明DTD的定义,不同的文档DTD会指示浏览器以不同的文档模式来解析HTML文本。如果DOCTYPE不存在或格式不正确,则会导致文档以兼容模式呈现,这时浏览器会使用较低的浏览器标准模式来解析整个HTML文本。
Web语义化是指在HTML结构的恰当位置上使用语义恰当的标签,使页面具有良好的结构,使页面标签元素具有含义,能够让人或搜索引擎更容易理解。
CSS规范规定:每个标签元素都是有display属性的。所以根据标签元素的display属性特点,可以将HTML标签分为以下几类。
○ 行内元素:包括<a>、<b>、<span>、<img>、<input>、<button>、<select>、<strong>
等标签元素,其默认宽度是由内容宽度决定的。
○ 块级元素:包括<div>、<ul>、<ol>、<li>、<dl>、<dt>、<dd>、<h1>、<h2>、<h3>、<h4>、<h5>、<h6>、<p>、<table>
等标签元素,其默认宽度为父元素的100%。
○ 常见空元素:例如<br>、<hr>、<link>、<meta>、<area>、<base>、<col>、<command>、<embed>、<keygen>、<param>、<source>、<track>
等不能显示内容甚至不会在页面中出现,但是对页面的解析有着其他重要作用的元素。
在设计HTML结构时要尽量注意语义化的使用。
Web语义化规范并不是在任何时候都需要严格遵守的,有时直接使用甚至会产生一些副作用。
关于前端标签元素或特性的兼容性,我们一般可以通过访问 http://caniuse.com/ 来查询。
如页面中使用<table>
这个语义化标签是会导致内容渲染较慢的,因为<table>
里面的内容渲染是等表格内容全部解析完生成渲染树后一次性渲染到页面上的,如果表格内容较多,就可能产生渲染过程较慢的问题,HTML中还有一些糟糕的设计需要我们注意,甚至HTML标签随意写或者CSS属性使用错误都不会报错。这样就比较糟糕了,增加了很多犯错的可能性。
流动网页提速(Accelerated Mobile Pages,AMP)是google推行的一个提升页面资源载入效率的HTML提议规范。基本思路有两点:使用严格受限的高效HTML标签以及使用静态网页缓存技术来提高网络访问静态资源的性能和用户体验。
使用AMP提升页面性能的基本的原则如下。
○ 只允许异步的script脚本
○ 只加载静态的资源
○ 不能让内容阻塞渲染
○ 不在关键路径中加载第三方JavaScript
○ 所有的CSS必须内联
○ 字体使用声明必须高效
○ 最小化样式声明
○ 只运行GPU加速的动画
○ 处理好资源加载顺序问题
○ 页面必须立即加载
○ 提升AMP元素性能
这里所说的“快”其实并不是单纯指页面渲染速度快,还包括将可以异步或延后的渲染操作延后,在保证用户体验不降低的情况下尽早展示关键性内容。
AMP HTML标签的实现原理可以理解为采用自定义的快加载标签元素(标签资源内容延后加载的元素)来代替慢加载标签元素(标签资源内容立即加载的元素)。
浏览器同一个域名的最大并行下载线程个数是有限的,所以我们常常要先加载页面的关键性展示资源,延后加载页面脚本类资源或页面的非关键性图片资源。所以为了增大资源下载并行数,我们常常将HTML、JavaScript、CSS、图片资源分域存放。分域也可以将静态资源请求进行服务器端的负载均衡,并对请求中的cookie信息进行隔离,因为跨域请求默认是不带Cookie的,这样便减小了JavaScript、CSS、图片等资源的请求头部信息大小,从而提升了请求的解析速度。
前端结构层演进
可扩展标记语言(Extentsible Markup Language,XML)是用来描述网络上存储数据的一种特殊文本标记格式。它是在SGML的基础上演化而来的,XML推荐使用一对闭合标签的形式来描述数据的名称,这对闭合标签里面的内容则表示对应的值,同时XML规定任何一个开始的标签必须与一个结束标签配对,否则将不能按照正确的XML结构解析,而且标签之间可以嵌套。
相比而言,XML更偏向于存储数据,而HTML则偏重于将数据读取出来装载到浏览器中展示给用户。HTML则是从SGML的基础上演化而来的另一种文本标记语言,一般用于网络上数据的展示。HTML5是HTML的第5个版本,但它不是基于SGML语言演化而来,而是W3C完全自定义的一套标准规范。它向后兼容了低版本的HTML 4.01的绝大多数标签,并添加了更多语义化标签
目前遵循W3C标准的浏览器均默认支持使用HTML5的DOCTYPE定义解析,如果不支持则默认使用低版本兼容模式来解析。
HTML5除了新增一部分元素标签外,还在一部分原有标签中增加了一些新属性。例如<input>
这个元素标签的属性值变化就比较大,<input>
新增的属性有autocomplete、placeholder、autofocus、required,另外type属性也增加了email、url、number、range、color、search、date等这些类型来满足更多的应用场景。以Chrome浏览器为例,选择浏览器调试工具设置中的show userAgent Shadow DOM,就可以看到Shadow DOM里的内容。
Shadow DOM是HTML的一个规范,它允许浏览器开发者封装自己的HTML标签、CSS样式和特定的JavaScript代码,同时也可以让开发人员创建类似<video>
这样的自定义一级标签,创建这些新标签内容和相关的API被称为Web Component。
Shadow root是Shadow DOM的根节点;Shadow tree为这个Shadow DOM包含的节点子树结构;Shadow host则称为Shadow DOM的容器元素,即上面的标签<video>
。指定一个元素,然后可以使用document.createShadowRoot()方法创建一个Shadow root,在Shadow root上可以任意通过DOM的基本操作API添加任意的Shadow tree,同时指定样式和处理的逻辑,并将自己的API暴露出来。完成创建后需要通过document.registerElement()在文档中注册元素,这利用Web Component API创建实现一个Shadow DOM时需要注意
○ 注册的Shadow host名称不能与浏览器自带的原生Shadow host名称相同
○ 注意样式模块的隔离,一定要保证当前的Shadow DOM样式只在当前Shadow DOM内生效。
○ 暴露合理的有效属性配置和DOM API接口。
○ 在目前浏览器不完全支持Web Component的情况下,如果需要在实际项目中使用,我们还需要解决好如何构建打包的问题。
浏览器脚本演进历史
CoffeeScript代表着前端脚本语言历史上的一段辉煌时期。CoffeeScript是一套可转译为JavaScript语法的语言。
CoffeeScript的创建者Jeremy Ashkenas借鉴了部分其他语言简洁、开发高效的特性,重新定义了一套语法规则,然后按照统一的规则转译成规范、可读、默认在严格模式下运行的JavaScript代码,这样就有效地保证了最后运行的JavaScript代码是具有一定规范且风格统一的,而CoffeeScript本身定义的语法规则也是十分高效且带有较好设计规范的。随着ECMAScript 6标准的发布,更加标准的规范和同样类似的高效特性出现了,此外,ECMAScript 6标准还补充完善并增强了JavaScript本身的运行特性。CoffeeScript因此开始走向没落。
ECMAScript,ECMAScript是TC39(Technical Committee 39,技术专家委员会39)负责制定的JavaScript标准。2015年6月17日,ECMAScript 6正式发布。其中增加了块级作用域变量声明规范、String模板、解构、arrow函数、Symbol类型、类、迭代器、生成器、集合、Promise、Proxy等特性。这也是CoffeeScript开始走向没落的一个转折点。2016年,ECMAScript 7发布,主要增加了幂函数和Array.prototype.includes特性。2009年12月,ECMAScript 5标准发布。其中主要新增了严格模式、JSON对象、新增Object接口、新增Array接口、Function.prototype.bind等特性。
TypeScript是微软在2012年推出的一种自由开源编程语言,是JavaScript的一个超集。
JavaScript的衍生脚本:目前可以理解为基于现有JavaScript的实现扩展自己特有语法规则来适应特殊应用场景的一类脚本规范。也可以理解为JavaScript的超集,并且通常需要额外支持自身特性的解析器来转译成JavaScript解析执行,例如JSX或HyperScript。
前端脚本语言的演进过程主要包括以下几个阶段:ECMAScript 5、CoffeeScript、ECMAScript 6+、TypeScript和衍生脚本。
JavaScript标准实践
可以简单理解为JavaScript是语言,ECMAScript则是语言习惯。
- ECMAScript 5
ECMAScript 5严格模式的提出为开发者提供了更加安全规范的编程范围,限制了原有一些不规范的写法,让一些不合理的语法直接报错,从而提高了代码的安全性和规范性。需要注意的是,严格模式下错误的提示类型和描述在不同浏览器可能不相同,具体与浏览器的实现有关。
ECMAScript 5于2009年12月发布,内容主要包括严格模式、JSON对象、新增Object接口、新增Array接口和Function.prototype.bind。
JSON对象中有几个容易混淆的方法:JSON.stringify()、JSON.toString ()、JSON.valueOf()、JSON.toLocaleString()。JSON.stringify()和JSON.parse()两个方法,用于将JavaScript对象转换为JSON字符串,以及将JSON字符串解析为JavaScript对象;JSON.valueOf()用于获取某个对象中的值;JSON.toString ()被调用时会调用Object原型上的toString方法,会取得JSON对象的值并转为字符串,如果没有具体的值,则返回原型数组;JSON.toLocaleString ()也是Object原型上的方法,经常会返回与toString ()相同内容,但对于Date对象,toLocaleString()会返回格式化后的时间字符串。
ECMAScript中新增的函数的bind ()方法比较常用,bind ()方法会创建一个新函数,称为绑定函数。
Object.keys()、Object.getOwnPropertyNames()和Object.getOwnPropertySymbols():用于获取对象自身的所有属性名、所有自有属性的名称(包括不可枚举的)以及所有自有符号属性的名称等新增Object方法属性。
新增了多个数组操作方法,如indexOf()、lastIndexOf()、forEach()、map()、filter()、reduce()和reduceRight()等,这些方法提供了更强大的数组处理能力。
Array.isArray():用于判断一个对象是否为数组。
String.prototype.trim():去除字符串两端的空白字符。
Date.now():返回自1970年1月1日00:00:00 UTC至调用该方法的那一刻之间的毫秒数。
Date().toJSON :返回Date 对象的字符串形式。 - ECMAScript 6
块级作用域变量声明关键字let、const。
有几个需要注意的地方:一是let和const都只能作为块级作用域变量的声明,且只能在块作用域内生效,块内声明的变量无法在块级外层引用;二是使用const声明的变量必须进行初始化赋值,而且一旦赋值就不能再进行二次修改赋值;三是使用let、const在全局作用域下声明的变量不会作为属性添加到全局作用域对象里面,这点和var是不同的;四是通过测试,使用let、const赋值语句的执行速度比使用var快约65%左右。
字符串模板:
当有字符串内容和变量混合连接时,可以使用字符串模板进行更高效的代码书写并保持代码的格式和整洁性。字符串模板不会压缩内部的换行与空格,而是按照原有的格式输出,只将变量内容填充替换掉。实际的项目开发中,如果使用ECMAScript 6的转译工具将ECMAScript 6的代码处理生成ECMAScript 5的代码后运行,格式可能丢失,因为ECMAScript 5及之前的版本中字符串是没有字符串模板格式的。
解构赋值:
解构赋值主要分为数组解构和对象解构。数组解构是严格按照数组下标依次对应顺序赋值的,如果赋值的常量个数不够,则对应下标的变量默认为undefined;如果常量个数超出,则多余的会被舍弃,所以顺序很重要;而对象解构赋值则是根据对象引用的键名来进行赋值的,可以无视顺序。
数组新特性:
主要包括…
复制数组和新增的数组API。…
进行的数组复制是浅拷贝。新增的数组方法如: Array.from(),从一个类数组对象或可迭代对象(如Set和Map)中创建一个新的、浅拷贝的数组实例;Array.of(),创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型;fill(),用一个固定值填充一个数组从起始索引到终止索引内的全部元素(不包括终止索引)。ECMAScript 6中还添加了更多关于数组迭代的方法:entries ()、keys()和values (),均可用来遍历数组。区别是keys ()是对数组键名进行遍历、values ()是对数组键值进行遍历,entries ()是对数组中键值对进行遍历。
函数参数:
ECMAScript 6对函数参数进行了新的设计,对原有函数参数设计糟糕的部分进行改善,主要添加了默认参数、不定参数和拓展参数。
不定参数是使用数组来表示多个参数,扩展参数则是将多个参数映射到一个数组。不定参数的…
和数组复制的…
是有区别的,不定参数可以使用函数的形参来表示所有的参数组成的列表。
箭头函数:
箭头函数的设计来自于CoffeeScript等语言的特性,ECMAScript 6标准中也添加了这个特性,让短函数的声明更加方便。
需要注意的是,箭头函数没有完整的执行上下文,因为其this和外层的this相同,可以理解为它的执行上下文只有变量对象和作用域链,没有this值。如果需要创建具有独立上下文的函数,就不要使用箭头函数。
增强对象:
在定义对象时通过属性简写、变量作为属性名或省略对象函数属性的书写等方式来提高编码的效率。
类:
ECMAScript 6添加了class关键字。
模块module:
ECMAScript 6引入了模块引用规范,这样现有的JavaScript模块化规范又多了一种选择:import/export。
循环与迭代器Iterator:
除了do…while、for循环,还可以使用for…in来遍历对象(注意不要用for…in来遍历数组,因为遍历出来的键不是数字,而且在部分浏览器中会产生乱序)。数组循环遍历最佳方式是for…of,此外for…of也可以用来遍历Map、Set、WeakMap、WeakSet等集合。Interator可以控制每次单步循环触发的时机,不用一次遍历所有的循环。
每次Iterator调用next()都会返回一个对象{done: false, value: item},done属性是boolean值,表示循环遍历是否完成,value则是每一步next()调用获取到的值。
生成器Generator:
Generator可以认为是一个可中断执行的特殊函数,声明方法是在函数名后面加上*来与普通函数区分。Generator遇到yield关键字会暂停往后执行,但并不表示后面的程序就不执行了。
集合类型Map+Set+WeakMap+WeakSet:
集合类型是对对象的增强类型,是一类使数据管理操作更加高效的对象类型。Set可以认为是增强的数组类型,Map则可以认为是增强的对象类型,WeakSet和WeakMap则对应着Set和Map的优化类型,所以某种程度上,为了让程序开发更加方便,我们有必要引入集合这类更为高效的类型。WeakSet和WeakMap在生成时有更加严格的限制:WeakSet只存储对象类型的元素,不能遍历,没有size属性;WeakMap只接受基本类型的值作为键名,没有keys、values、entries等遍历方法,也没有size属性。
再总结一下JavaScript可能出现内存泄露的常见场景:闭包函数、全局变量、对象属性循环引用、DOM节点删除时未解绑事件、Map和Set的属性直接删除。需要注意的是,Map和Set都为内部的每个键或值保持了强引用,也就是说,如果一个存储的属性元素被移除了,回收机制可能无法回收它占用的内存,容易造成内存泄露,所以我们使用时要尽可能先删除引用的相关内容。相比之下,使用WeakSet和WeakMap则不会出现上述情况,因为它们并不使用强引用。
Promise、Symbol、Proxy增强类型:
Promise可以用来避免异步操作函数里的多层嵌套回调(callback hell)问题。一般将这些Promise实现的规范分为Promise/A规范和Promise/A+规范,通常我们认为Promise/A+规范是在Promise/A规范的基础上进行修正和增强形成的,更具有规范性。
ECMAScript 6中Promise的实现严格遵循了Promise/A+规范。
Symbol:
Symbol是ECMAScript 6新增加的基本数据类型,一般用作属性键值,并且能避免对象属性键的命名冲突。
Proxy:
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。Proxy构造函数接受两个参数:目标对象(target)和一个处理器对象(handler)。处理器对象定义了要拦截并自定义的行为。Proxy
Proxy可以用来拦截某个对象的属性访问方法,然后重载对象的“.”运算符。
统一码:
ECMAScript 6字符串支持新的Unicode文本形式,同时也增加了新的正则表达式修饰符u来处理统一码。
进制数支持:
ECMAScript 6增加了对二进制(b)和八进制(o)数字面量的支持。
Reflect对象和tail calls尾调用:
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。Reflect和Proxy的关系非常紧密。在编写Proxy处理器时,通常会使用Reflect来调用对象的默认行为。Reflect可以理解为原有对象上的一个引用代理,它用于对原有对象进行赋值或取值操作,但不会触发对象属性的getter或setter调用,而直接使用=对对象进行赋值或取值操作会自动触发getter或setter方法,尾调用指某个函数的最后一步是调用另一个函数。”尾调用优化”(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
参考文章:尾调用优化 - 阮一峰的网络日志 https://www.ruanyifeng.com/blog/2015/04/tail-call.htmltail calls尾调用保证了函数尾部调用时调用栈有一定的长度限制,这使得递归函数即使在没有限制输入时也能保证安全性而避免发生错误。 - ECMAScript 7+
幂指数操作符:
幂(**)运算符返回第一个操作数取第二个操作数的幂的结果。它等价于 Math.pow(),不同之处在于,它还接受 BigInt 作为操作数。
Array.prototype.includes:
这个数组方法主要用来判断数组中是否包含某个元素。
异步函数async/await:
可以认为async/await是对Generator的一种封装简化,专门用于处理Generator中异步的场景。 - TypeScript
强类型支持:
TypeScript的数据类型是强类型的,声明时需要对类型进行定义。
Decorator装饰器特性:
Decorator可以用来注解class、property、method和parameter,也是一种面向对象编程语言设计模式的借鉴前端表现层基础
CSS(Cascading Style Sheets)的设计则提出使用样式描述语言来表达页面内容,而不是用HTML的标签来表达。继CSS1后,W3C在1998年发布了CSS2规范,CSS2的出现主要是为了解决早期网页开发过程中排版时表现分离的问题,CSS3可以认为是在CSS2规范的基础上进行补充和增强形成的,让CSS体系更能适应现代浏览器的需要,拥有更强的表现能力,尤其对于移动端浏览器
一般认为CSS中选择器属性优先级顺序为!important>内联样式(权重1000)> id选择器(权重100)>类选择器(权重10)>元素选择器(权重1),多种组合情况按照权重相加的原则来计算,!important优先级最高。
伪元素会在HTML中添加before或after这类内容,而像:visited、:hover、:first-child、:nth-child、:enable、:checked这些伪类则不会,一般用于表示元素在用户不同操作下的状态或选择指定某些元素的描述。
CSS的属性和值直接决定着元素在页面上的渲染表现形式。前端界面技术
由于浏览器间内核实现的差异性,不同浏览器可能对同一元素标签样式的默认设置是不同的,如果不对CSS样式进行统一化处理,可能会出现同一个网页在不同浏览器下打开时显示不同或样式不一致的问题。前端统一化CSS样式主要有reset,normalize,neat三种实现思路:reset是清除浏览器的默认样式并保持在所有浏览器中一致;normalize是使用同一种默认样式并在所有浏览器中保持样式一致;neat则可以认为是前两种的结合,具体需要根据网站的设计特点来确定,但仍需要保证默认样式在所有浏览器中是一致的。
CSS预处理技术具有几个优势:提高开发效率,例如SCSS的嵌套、父级选择符、Mixin、Extend,或者postCSS的autoprefixer等都是为了减少书写重复性代码、提高编写效率而设计的;便于管理,预处理器使用的类CSS脚本可以按模块编写开发进行管理,而且幸运的是大部分预处理工具都有相应的模块引用机制并能借助构建工具自动完成编译打包。
一个高效的预处理语法工具一般具有以下特性。
○ 变量声明和计算。
○ 语法表达式。
○ 函数处理。
○ 属性的继承。
○ 兼容性补全。
实现动画的方案主要有6种:JavaScript直接实现动画、可伸缩矢量图形(Scalable Vector Graphics,SVG)动画、CSS3 transition、CSS3 animation、Canvas动画、requestAnimationFrame。
通过JavaScript实现动画通常会导致页面频繁性重排重绘,很消耗性能,如果是稍微复杂的动画,在性能较差的浏览器上就会明显感觉到卡顿,所以我们尽量避免使用它。在很多移动端动画性能优化时,一般使用16ms来进行节流处理连续触发的浏览器事件
,例如对touchmove、scroll事件进行节流等。我们通过这种方式来减少持续性事件的触发频率,可以大大提升动画的流畅性。
SVG又称可伸缩矢量图形,原生支持一些动画效果,通过组合可以生成较复杂的动画,而且不需要使用JavaScript参与控制。
元素较多且复杂的动画使用SVG渲染会比较慢,而且SVG格式的动画绘制方式必须让内容嵌入到HTML中使用,但随着CSS3的出现,这种动画实现方式相对使用得越来越少了。
CSS3过渡动画transition并不能实现独立的动画,只能在某个标签元素样式或状态改变时进行平滑的动画效果过渡,而不是马上改变。在移动端开发中,直接使用transition动画会让页面变慢甚至变卡顿,所以我们通常通过添加transform: translate3D(0, 0, 0)或transform: translateZ(0)来开启移动端动画的GPU加速,让动画过程更加流畅。
CSS3 animation 通过对关键帧和循环次数的控制,页面标签元素会根据设定好的样式改变进行平滑过渡,而且关键帧状态的控制一般是通过百分比来控制的,这样我们就可以在这个过程中实现很多动画的动作了。CSS3实现动画的最大优势是脱离JavaScript的控制,而且能用到硬件加速,可以用来实现较复杂的动画效果。定义动画的keyframes中from值和0%的意义是相同的,表示动画的开始关键帧。to和100%的意义相同,表示动画的结束关键帧。<canvas>
作为HTML5的新增元素,也可以借助Web API实现页面动画。Canvas动画的进行只能在<canvas>
元素内部,超出<canvas>
元素边界将不被显示。使用Canvas的主要优势是可以应对页面中多个动画元素渲染较慢的情况,完全通过JavaScript来渲染控制动画的执行,这就避免了DOM性能较慢的问题,可用于实现较复杂的动画。
requestAnimationFrame是前端表现层实现动画的另一种API实现,它的原理和setTimeout及setInterval类似,都是通过JavaScript持续循环的方法调用来触发动画动作的,但是requestAnimationFrame是浏览器针对动画专门优化而形成的API,在实现动画方面性能比setTimeout及setInterval要好,可以将动画每一步的操作方法传入到requestAnimationFrame中,在每一次执行完后进行异步回调来连续触发动画效果。响应式网站开发技术
通常认为,响应式设计是指根据不同设备浏览器尺寸或分辨率来展示不同页面结构层、行为层、表现层内容的设计方式。目前比较主流的实现方法有两种:一是通过前端或后端判断userAgent来跳转不同的页面完成不同设备浏览器的适配,也就是维护两个不同的站点来根据用户设备进行对应的跳转;二是使用media query媒体查询等手段,让页面根据不同设备浏览器自动改变页面的布局和显示,但不做跳转。该实现方案适用于功能复杂并对性能要求较高的站点应用
第二种方案。桌面浏览器和移动端浏览器使用同一个站点域名来加载内容,只需要开发维护一个站点就可以了,然后根据media query来实现不同屏幕下的布局显示,适用于访问量较小、性能要求不高的应用场景存在一些明显的问题。
○ 移动端浏览器加载了与桌面端浏览器相同的资源,例如图片、脚本资源等,导致移动端加载到冗余或体积较大的资源。
○ 桌面端浏览器和移动端浏览器访问站点需要展示的内容可能不完全相同,这种响应式的方式只实现了内容布局显示的适应,但是要做更多差异性的功能比较难。
○ 桌面端浏览器和移动端浏览器页面功能本身具有差异性,使用同一套处理方式,会有更多的兼容性问题。
结构层响应式设计可以理解成HTML内容的自适应渲染实现方式,即根据不同的设备浏览器渲染不同的页面内容结构,而不是直接进行页面跳转。
两种不同的设计思路了:一是页面内容是在前端渲染,二是页面内容在后端渲染(也就是直出层)首先要保证移动端加载的内容资源最小,因此会以移动端优化资源为主,保证移动端页面的首屏内容优先加载,然后通过异步的方式来实现桌面端或移动端剩余内容的加载。
一般下载的HTML文件内容在桌面端和移动端会有差异化内容存在,这是不可避免的,所以我们只能尽可能减少差异化内容来保证冗余资源减到最小。直出层以Node为例,在生成页面内容时可以判断用户的userAgent来渲染不同的HTML输出模板。这里不同的模板内容可以完全不一样,并可以独立维护,这样就能根据同一个地址直出不同的内容了。
根据浏览器设备屏幕宽度和屏幕的分辨率来加载不同大小尺寸的图片,避免在移动端上加载体积过大的资源。前端图片响应式的几种常见解决方案:
1.使用Media Query背景图片代替
使用背景图片引入来引入页面上的图片,并在CSS中通过Media Query来判断加载所需要的不同背景图片,这样浏览器就会根据浏览器设备的屏幕宽度或屏幕分辨率来加载不同的图片了。
由于是背景图片,无法定义页面图片属性和描述内容,不利于搜索引擎优化(Search Engine Optimization,SEO),也不能在图片加载失败时给予文字提示,而且如果图片内容是动态生成的,那么就需要修改标签元素的style属性来设置背景图,显然这是不合理的。
由于高清屏的特性,以2倍Retina高清屏的移动设备为例,CSS中的1px是由2×2个屏幕物理像素点来渲染的,那么样式上的border:1px在Retina高清屏下会渲染成2个物理像素宽度或高度的边框。实现1px边框的方式比较多,通常可以设置元素after或before伪元素为1px内容,并使用transform:scaleY(1/devicePixelRatio)来进行单方向的缩放实现1个物理像素的边框或内容。对于字体,我们也可以设置transform:scale(.5)在浏览器中支持显示小于12px的文字。
同时如果页面的内容因为使用高清屏而导致模糊,则需要使用-webkit-font-smoothing: antialiased
来尝试修复。
2.Picture标签元素
HTML5标签<picture>
是一个类似<img>
展示图片的元素,但图片内容是由多个源图组成,并能根据屏幕的特性选择使用不同的图片。用于图片响应式处理<picture>
元素的Polyfill(Polyfill是指使用第三方手段让浏览器支持浏览器原本并不支持的新特性)思路——Picturefill。仍需要考虑Picturefill的性能解析问题,尤其是在移动端图片较多的页面,对应每个图片都需要去解析,可能会因为解析速度慢而阻塞其他页面脚本逻辑的执行。
3.模板判断响应式图片
使用前端模板进行判断渲染输出不同的图片是最简单、最直接的响应式图片实现方式。我们可以判断浏览器userAgent或检测是否高清屏let isRetina= window.devicePixelRatio > 1;
来填充不同尺寸的图片地址到页面模板中。
4.图片服务器判断输出内容
通过浏览器访问服务器图片时带上浏览器的userAgent或URL参数等信息来实现的,服务器读取到这些信息后结合userAgent的不同浏览器特点输出不同大小的图片。
响应式布局是根据浏览器宽度、分辨率、横屏、竖屏等情况来自动改变页面元素展示的一种布局方式,一般可以使用栅格方式来实现,实现思路有两种:一种是桌面端浏览器优先,扩展到移动端浏览器适配;另一种则是以移动端浏览器优先,扩展到桌面端浏览器适配。
一般比较推荐从移动端扩展到桌面端的方式进行适配,这样就避免了在移动端加载冗余的桌面端CSS样式内容。屏幕适配布局则是主要针对移动端的,由于目前移动端设备屏幕大小各不相同,屏幕适配布局是为了实现网页内容根据移动端设备屏幕大小等比例缩放所提出的一种布局计算方式。
一般是通过栅格系统来解决百分比方式布局。通常在移动端页面上,首先为了固定浏览器对HTML文件的渲染,会在HTML的<head>
里面加上下面一段<meta>
声明来控制页面使用移动端浏览器展示并保持内容不缩放。
<meta>
中viewport控制的缩放是在屏幕宽度确定后浏览器的视窗内容不随用户的操作而缩放,而屏幕适配布局的自动缩放是在屏幕宽度不确定的情况下页面元素展示内容与宽屏大小保持比例不变。
1.zoom属性控制方案
如果希望在320px宽度屏幕上显示的内容在414px的宽度屏幕上进行等比例自动放大,
可以设置<html>或<body>
标签的CSS属性为“zoom: 1.29375”,其中1.29375 = 414/320
,即将zoom的值按照屏幕宽度的320分之一计算。使用JavaScript就可以按如下方式来设置body的zoom属性。document.body.style.zoom = screen.width/320;
这个zoom值通常需要通过JavaScript计算得出,如果JavaScript在页面渲染之后完成,则会出现页面内容全部重排的情况,所以我们需要尽量在页面文档开始的地方给<html>或<body>
设置zoom属性。但是这样处理之后,其他屏幕下面的缩放参考尺寸必须全部按照这个固定的比例来缩放,显得不太灵活。
2.REM屏幕适配方案
REM方案目前应用广泛。通常以某个屏幕宽度的设计稿为基准(例如320像素宽度)进行缩放。1rem=屏幕宽度/320*10
这样1rem在宽度为320px的屏幕上表示的是10px,按照这个基准,1rem在其他屏幕上页面的显示比例和在320px宽度的屏幕上显示比例将是一致的。
给HTML根元素一个根据屏幕自动调整的font-size,页面上所有元素的尺寸全部以rem为单位,rem通常也是通过JavaScript的计算得出的。除此之外,通过Media Query直接对常见的几种屏幕宽度的<html>
元素标签来进行设置,避免使用JavaScript来计算。
和结构层类似,行为层的响应式同样分为JavaScript内容在前端引入和在后端引入这两种情况。对于前一种情况,我们主要可以通过设备浏览器环境判断来异步加载不同的JavaScript脚本。
第4章 现代前端交互框架
数据的处理和操作的核心其实就是DOM的处理和操作,即便是今天,所有前端JavaScript框架最终要解决的仍然是如何实现高效、高性能DOM交互操作的问题。
DOM API提供了浏览器上进行DOM对象树交互操作的一系列原生JavaScript方法。使用这些原生的API开发就显得比较低效而且不易管理。因为想对这套API进行必要的封装来提高调用效率,所以jQuery这个典型的DOM交互框架就应运而生了,移动端开发类似jQuery的框架,以zepto为主,可以认为它是一个简化版的jQuery,其常用的API和jQuery完全相同。
AJAX跨域请求时默认不会带有浏览器端Cookie信息,需要在请求头部加上xhrFields: {withCredentials: true}
才能将Cookie信息正常带到请求中发送给服务器。
想要高效地使用jQuery,可以参考以下的优化建议和原则:(1)尽可能使用id选择器进行DOM查询操作,不要使用组合选择器;(2)缓存一切需要复用的jQuery DOM对象,使用find()子查询;(3)不要滥用jQuery,尽量使用原生的代码代替;(4)尽可能使用jQuery的静态方法;(5)使用事件代理,不要直接使用元素的事件绑定;(6)尽量使用较新的jQuery版本;(7)尽可能使用链式写法来提高编程效率和代码运行效率。
MV*交互模式
MVC可以认为是一种开发设计模式,其基本思路是将DOM交互的内容分为数据模型、视图和事件控制函数三个部分,并对它们进行统一管理。Model用来存放请求的数据结果和数据对象,View用于页面DOM的更新与修改,Controller则用于根据前端路由条件(例如不同的HASH路由)来调用不同Model给View渲染不同的数据内容。
MVP(Model-View-Presenter): Presenter和View的操作绑定通常是双向的,View的改变一般会触发Presenter的动作,Presenter的动作也会改变View。Model和View是不直接发生关系的,所有的逻辑调用数据Model和渲染视图View模板都在Presenter里面完成,同时用户在View层操作的改变会反馈到Presenter然后改变Model并渲染新的View。
MVVM则可以认为是一个自动化的MVP框架,并且使用ViewModel代替了Presenter,即数据Model的调用和模板内容的渲染不需要我们主动操作,而是ViewModel自动来触发完成,任何用户的操作也都是通过ViewModel的改变来驱动的。
根据数据的变化来自动触发其他操作的机制就是我们说的数据变更检测,实现数据变更检测的方法主要有手动触发绑定、脏数据检测、对象劫持、Proxy等。
手动触发指令绑定:
是比较直接的实现方式,主要思路是通过在数据对象上定义get()方法和set()方法(当然也可以使用其他命名方法),调用时手动触发get()或set()函数来获取、修改数据,改变数据后会主动触发get()和set()函数中View层的重新渲染功能。
脏检测机制:
以典型的MVVM框架Angularjs为例,Angularjs是通过检查脏数据来进行View层操作更新的,脏检测的基本原理是在ViewModel对象的某个属性值发生变化时找到与这个属性值相关的所有元素,然后再比较数据变化,如果变化则进行Directive指令调用,对这个元素进行重新扫描渲染。其实这里的和手动绑定扫描节点的方式类似,不同的是,脏检测只针对可能修改的元素进行扫描,这样就提高了ViewModel内容变化后扫描视图渲染的效率。
前端数据对象劫持(Hijacking):
其基本思路是使用Object.defineProperty和Object.defineProperies对ViewModel数据对象进行属性get()和set()的监听,当有数据读取和赋值操作时则扫描元素节点,运行指定对应节点的Directive指令,这样ViewModel使用通用的等号赋值就可以了。
ECMAScript 6 Proxy:
可以用于在已有的对象基础上重新定义一个对象,并重新定义对象原型上的方法,包括get()方法和set()方法。
Virtual DOM交互模式
通常认为,Virtual DOM是一个能够直接描述一段HTML DOM结构的JavaScript对象,浏览器可以根据它的结构按照一定规则创建出确定唯一的HTML DOM结构。整体来看,Virtual DOM的交互模式减少了MVVM或其他框架中对DOM的扫描或操作次数,并且在数据发生改变后只在合适的地方根据JavaScript对象来进行最小化的页面DOM操作,避免大量重新渲染。
创建Virtual DOM即把一段HTML字符串文本解析成一个能够描述它的JavaScript对象。
DOM的过程相当于实现了一个HTML文本解析器,但是没有生成DOM对象树,只是生成了一个操作效率更高的JavaScript对象,因此通常不会直接将HTML交给浏览器去解析,因为浏览器的DOM解析很慢,这也是Virtual DOM交互模式和普通DOM编程最本质的区别。
对于Virtual DOM的对比算法实际上是对于多叉树结构的遍历算法。
Virtual DOM最本质的区别在于减少了对DOM对象的操作,通过JavaScript对象来代替DOM对象树,并且在页面结构改变时进行最小代价的DOM渲染操作,提高了交互的性能和效率。这就是Virtual DOM交互模式的优势,也是提高前端交互性能的根本原因。尽管Virtual DOM的交互模式能在页面数据渲染和变更时尽可能地减少DOM操作,但仍无法完全脱离DOM交互的模式。
DOM的操作效率不高,在移动设备的Hybrid WebView上表现会更慢,所以为了进一步改进Hybrid应用中的DOM性能,希望完全脱离DOM编程的模式来进行结构层的操作。
把这种使用JavaScript调用原生控件或事件绑定来生成应用程序的交互模式称为前端MNV*开发模式。可以简单理解为Model-NativeView-*,而后面的*可以表示Virtual DOM或MVVM中的ViewModel,我们也可以自己使用Controller来实现调用的方式。
MNV*框架端的主要任务是解析Model、ViewModel或Virtual DOM组成JSBridge协议串并发送,而Native端的实现将会比较复杂,需要处理不同的标签元素解析,
,还可能需要处理事件的绑定等,即将JavaScript的事件通过Native事件来实现。整体上像是使用移动端原生的方式来解析HTML上需要实现的应用功能。
第5章 前端项目与技术实践
前端开发规范
统一的开发规范常常可以降低代码的出错概率和团队开发的协作成本。
平时所说的开发规范更多时候指的是狭义上的编码规范,广义上的开发规范包括实际项目开发中可能涉及的所有规范,如项目技术选型规范、组件规范、接口规范、模块化规范等。
- 1 前端通用规范
三层结构分离:
前端页面开发应做到结构层(HTML)、表现层(CSS)、行为层(JavaScript)分离,保证它们之间的最小耦合,这对前期开发和后期维护都是至关重要的。
移动端开发可以适当地进行CSS样式、图片资源、JavaScript内联,内联的资源大小标准一般为2KB以内,否则可能会导致HTML文件过大,页面首次加载时间过长。
缩进:
统一使用tab(或4个空格宽度)来进行缩进,可以在开发编辑器或IDE里进行设置。
内容编码:
在HTML文档中用<meta charset="utf-8 ">
来指定编码,以避免出现页面乱码问题。不需要为CSS显式定义编码,其默认为utf-8。
小写:
所有的HTML标签、HTML标签属性、样式名及规则建议使用小写,HTML属性的id属性可以使用驼峰大小写组合的命名方式。
代码单行长度限制:
代码单行长度不要超过120字符(或80字符,具体可根据团队习惯来决定),长字符串拼接通常使用加号来连接换行的内容。
注释:
尽可能地为代码写上注释。自文档化开发是目前比较提倡的一种书写带有具体含义项目代码的编码方式,它提出要尽可能让代码本身来表示代码执行的功能描述,而减少文档注释的书写,因为文档注释需要更多的时间去维护。
行尾空格与符号:
删除行尾空格与多余的符号。 - 2 前端HTML规范
文档类型定义:
统一使用HTML5的标准文档类型<!DOCTYPE html>来定义,这样更简洁,而且向后兼容。不使用HTML 4.01的DTD定义。
head内容:
head中必须定义title、keyword、description,保证基本的SEO页面关键字和内容描述。移动端页面head要添加viewport控制页面不缩放,有利于提高页面渲染性能。
省略type属性:
在引用CSS或JavaScript时,可以省略type属性不写,因为HTML5在引入CSS时默认type值为text/css,在引入JavaScript时默认type值为text/javascript。
使用双引号包裹属性值:
所有的标签属性值必须要用双引号包裹,不允许有的用双引号有的用单引号,这样有利于区分标签的属性名和属性值。
属性值省略:
非必需的属性值可以省略。
嵌套:
所有元素必须正确嵌套,尽量使用语义化标签,不允许交叉,也不允许在inline元素中包含block元素。
标签闭合:
非自闭合标签必须添加关闭标识,自闭合标签无须关闭。
使用img的alt属性:
alt属性的内容可以简要描述图片的内容,有利于页面搜索引擎优化,而且对于盲人用户和图像加载失败时的提示也很实用,即支持无障碍阅读和提示,所以要尽量避免alt的属性值为空。
使用label的for属性:
为表单内部元素<label>
加上for属性或者将对应控件放在<label>
标签内部,这样在点击<label>
标签的时候,同时会关联到对应的input或textarea上选中,可以增加输入的响应区域。
按模块添加注释:
在每个大的模块的开始和结束的地方添加起始注释标记,便于开发者识别、维护。
标签元素格式:
块级元素一般另起一行写。行内元素可以根据情况换行,尽量保证行内元素代码长度不超过一行,否则要考虑另起一行写。HTML的子元素要尽量相对其父级进行缩进,这样更有层次。
语义化标签:
在合适的地方选择语义合适的标签。不要使用被HTML5废弃用于样式表现的无语义化标签 - 3 前端CSS规范
CSS引用规范:
使用link的方式调用外部样式文件,外部样式文件可以复用并能利用浏览器缓存提高加载速度。禁止在标签元素中使用内联样式,否则后期很不容易管理,强烈不建议使用。
样式的命名约定:
CSS类名命名一般由单词、中画线组成,当然也有BEM(块block、元素element、修饰符modifier,是由Yandex团队提出的一种前端命名方法)方案,这里推荐一种规范——所有命名都使用小写,加上ui-等前缀,表示这个类名只用来控制元素的样式展现。不推荐使用拼音作为样式名,尤其是使用缩写的拼音与英文混合的方式,很让人费解。
不以模块表现样式来命名,要根据内容来命名。
书写CSS样式时不能用id选择器,因为针对id的元素样式很难复用。
简写方式:
如果属性值为0,则不需要为0加单位。如果是以0为个数位的小数,前面的0可以省略不写。
颜色值写法,所有的颜色值要使用小写并尽量缩写至3位。
属性书写顺序:
先写元素的布局属性,再写元素的内容属性。
Hack写法:
尽可能减少对CSS Hack的使用和依赖,可以使用其他的解决方案代替Hack的思路。
CSS规则若要实现在多种浏览器内核上兼容,就要遵循先写私有属性后写标准属性的原则,这样有利于浏览器版本向前兼容。
CSS高效实现规范:
标签名与id或class组合的选择器会造成冗余,而且降低CSS的解析速度,应避免。
尽量使用简短的CSS实现方式,对于无继承关系的元素使用合并的写法更简洁。
不同元素之间属性存在继承关系时,使用分拆方式,避免继承属性的重复定义。
使用预处理脚本编码开发:
尽可能使用预处理器的高效语法来提高开发效率,如嵌套、变量、嵌套属性、注释、继承等,避免直接使用CSS开发。 - 4 ECMAScript 5常用规范
分号:
JavaScript语句后面统一加上分号。
空格:
在所有运算符、符号与英文单词之间添加必要的空格,利于开发者阅读。
空行:
一般推荐在代码块后保留一行空行,显得块内容层次更加分明
引号:
推荐JavaScript字符串最外层统一使用单引号。
变量命名:
标准变量采用驼峰式命名。常量使用全大写形式命名,并用下画线连接。构造函数首字母大写
对象:
对象属性名不需要加引号。对象属性键值以缩进的形式书写,不要写在同一行。数组、对象属性后不能有逗号,否则部分浏览器可能会解析出错。
大括号:
程序中的块代码推荐使用大括号包裹,要注意换行,这样更加清晰,而且方便后面扩展增加内容。
条件判断:
尽量不要直接使用undefined进行变量判断,使用typeof和字符串’undefined’对变量类型进行判断。分别用===、!==代替==、!=更加严谨。
不要在条件语句或循环语句中声明函数。
一些其他的可选规范参考:
for-in循环里面要尽量含有hasOwnProperty的判断,防止访问不存在的对象属性时出错。不要在内置对象的原型上添加方法,如Array、Date,否则会污染JavaScript内置对象。不要在同一个作用域下声明同名变量,这是不安全的JavaScript书写方法,严格模式下是禁止使用的。移除声明但未使用过的变量。不要在应该比较的地方赋值。不要像new function () {…}、new Object()等这样使用构造函数。 - 5 ECMAScript 6+参考规范
正确使用ECMAScript 6的变量声明关键字;
字符串拼接使用字符串模板完成;
解构赋值尽量使用一层解构,否则声明变量嵌套太深难以理解;
数组拷贝推荐使用…实现,更加简洁高效;
数组循环遍历使用for…of,非必须情况下不推荐使用forEach、map、简单循环;
使用ECMAScript 6的类来代替之前的类实现方式,尽量使用constructor进行属性成员变量赋值;
模块化多变量导出时尽量使用对象解构,不使用全局导出。尽量不要把import和export写在一行;
导出类名时,保持模块名称和文件名相同,类名首字符需要大写;
生成器中yield进行异步操作时需要使用try…catch包裹,方便对异常进行处理;
推荐使用Promise,避免使用第三方库或直接回调,原生的异步处理性能更好而且符合语言规范;
如果不是必须,避免使用迭代器:
迭代器Iterators性能比较差,对于数组来说大致与Array.prototype.forEach相当,比不过原生的for循环,而且使用起来比较麻烦。目前数组遍历提供了for…of方法,对象遍历提供了for…in方法,所以非必须情况下还是不建议使用迭代器。
不要使用统一码,中文的正则匹配和计算较消耗时间,而且容易出问题;
合理使用Generator,推荐使用async/await,更加简洁;
具体关于ECMAScript 6+特性的介绍和使用可以参考本书第3章的内容。 - 6 前端防御性编程规范
对外部数据的安全检测判断;
规范化的错误处理:
对于常用的AJAX请求或长时间文件读写等可能失败的异步操作,需要进行错误情况的处理或异常捕获处理,而不应该被静默,否则一旦出错,用户将得不到正常的提示,对用户体验影响极大。
防御性编程是指通过检测任何可能存在的逻辑异常问题的代码实现,提高脚本执行过程健壮性的一种编程手段。防御性编程要求我们对程序的实现进行更加全面、严谨的考虑。
前端组件规范
- UI组件规范
目前前端主流的一些组件相关规范:UI(User Interface,用户界面)组件规范、模块化规范、项目组件化设计规范。UI组件规范强调了一个网站中所有网页结构层和表现层实现的一致性。UI层的规范能带来一些明显的好处:UI层风格统一化;增加UI层复用性;更符合用户的体验习惯;增加了开发规范的统一性。 - 模块化规范
模块化规范是JavaScript文件之间相互依赖引用的一种通用语法约定,就是按照一定的规范来写JavaScript文件,让它可以方便地被其他JavaScript文件引用。
就规范种类来说,主要包括AMD(Asynchronous Module Definition,异步模块定义)、CMD(Common Module Definition,通用模块定义)、CommonJS、import/export等。
AMD是运行在浏览器端的模块化异步加载规范,主要以requireJS为代表,基本原理是定义define和require方法异步请求对应的javascript模块文件到浏览器端运行。模块执行导出时可以使用函数中的return返回结果。
CMD是Seajs提出的一种模块化规范,在浏览器端调用类似CommonJS的书写方式来进行模块引用,但却不是完全的CommonJS规范。CMD遵循按需执行依赖的原则,只有在用到某个模块的时候才会执行模块内部的require语句,同时加载完某个依赖模块文件后并不立即执行,在所有依赖模块加载完成后进入主模块逻辑,遇到模块运行语句的时候才执行对应的模块,这和AMD是有区别的。
CommonJS是Node端使用的JavaScript模块化规范,使用require进行模块引入,并使用modules.exports来定义模块导出。与前面两种方式相比,CommonJS的写法更加清晰简洁。
import/export是ECMAScript 6定义的JavaScript模块引用方式,是唯一一个遵循JavaScript语言标准的模块化规范,在讲解ECMAScript 6的时候也重点进行了分析。import/export使用import引入其他模块,使用export来进行模块导出。
关于模块化规范有一个容易误解的地方:很多人认为AMD规范只能在浏览器端使用,CommonJS只能在Node端使用。这里要理解的是,模块化规范只是规范,AMD最早被使用在浏览器端不代表其只能在浏览器端运行,主要还是取决于模块化规范的支持库运行在哪里。 - 项目组件化设计规范
Web Component组件化:
Polymer是Google在2013年的Google I/O大会上提出的一个新的UI框架,使用了Web Component标准,并且针对各种平台的浏览器,Polymer UI库和组件都具备较好的兼容性。Web Component 是一套丰富的技术方案,它通过Custom element、Shadow DOM、<template> 和 <slot>
的结合使用来在原生层面实现组件的封装与复用。实际上,Polymer自提出后到现在并没有得到很广泛的实践,但是其遵循Web Component规范的这一实践思路被越来越多的组件化框架借鉴。
MVVM框架组件化:
基本思路是将页面中的模块按照元素来划分,并将与这个模块相关的MVVM描述语法、CSS样式、执行脚本放在同一个文件里进行引用。
Virtual DOM的组件化方案:
Virtual DOM的出现更多情况下是为了改善MVVM的DOM性能。以reactjs为例,
在组件化设计实现方面,它使用的仍是和Polymer类似的组织管理方式,所不同的是将HTML的结构描述换了另一种语法形式且JavaScript的调用API不同。
基于目录管理的通用组件化实践:
对组件的三层结构进行文件划分,各个文件负责自己的功能部分,然后在构建生成的时候进行组件中不同类文件内容的打包处理。
设计一个高效的组件化规范应该解决哪些问题。
○ 组件之间独立、松耦合。
○ 组件间嵌套使用。
○ 组件间通信。
○ 组件公用部分设计。
○ 组件的构建打包。
○ 异步组件的加载模式。
○ 组件继承与复用性。
○ 私有组件的统一管理。
○ 根据特定场景进行扩展或自定义。
对于组件设计规范,我们要认识其本质——前端三层结构的设计和组织形式。自动化构建
前端构建工具的作用可以认为是对源项目文件或资源进行文件级处理,将文件或资源处理成需要的最佳输出结构和形式。
在处理过程中,我们可以对文件进行模块化引入、依赖分析、资源合并、压缩优化、文件嵌入、路径替换、生成资源包等多种操作,这样就能完成很多原本需要手动完成的事情,极大地提高开发效率。
构建的流程主要分成7个基本步骤(不同的构建工具各有差异,但基本原理是类似的):读取入口文件→分析模块引用→按照引用加载模块→模块文件编译处理→模块文件合并→文件优化处理→写入生成目录。
模块分析引入:
JavaScript组件模块文件的依赖分析过程,以require的引用方式为例。
1.从入口模块开始分析require函数调用依赖。
2.根据依赖生成JavaScript AST(Abstract Syntax Tree,抽象语法树,是将JavaScript代码映射成一个树形结构的JSON对象树)。
3.根据AST找到每个模块的模块名。
4.得到每个模块的依赖关系,生成一个依赖字典。
5.根据模块化引用机制包装每个模块,传入依赖字典以及import或require函数,生成执行的JavaScript代码。
模块化规范支持:
好的构建工具应该尽可能支持较多种类模块化规范进行打包,这样我们就不用考虑模块化规范不统一的问题了。
CSS编译、自动合并图片:
CSS的预处理、图片引用的分析、内联图片资源、自动合并雪碧图等功能在一个理想的构建工具里都是必不可少的。如果构建工具能自动移除解析后重复冗余的CSS规则就更完美了。
HTML、JavaScript、CSS资源压缩优化:
例如HTML内注释和多余空格的压缩、JavaScript的uglify操作、CSS的压缩等,这些都是我们希望构建工具自动完成的。
HTML路径分析替换:
提供将文件相对路径自动替换成绝对路径或线上CDN路径的能力。
区分开发和上线目录环境:
能够在配置构建任务时指明开发和发布资源目录来满足不同场景下的需要。
异步文件打包方案:
异步文件加载的场景很常见,尤其是在移动端,为了保证首屏加载的资源最小,非首屏的内容都希望通过JavaScript来异步渲染,这就需要构建工具能将非首屏的组件打包成异步资源,以按需或异步的方式加载。通常将异步组件放在异步的目录里进行单独打包或加入特殊的标识,不过也需要构建工具支持才行。
文件目录白名单设置:
为了加快项目代码构建的处理速度,建议提供一些配置绕过不需要处理的文件目录,否则文件目录较多的情况下构建处理速度就会比较慢。
除了这些,通常构建工具也应该是可以扩展的,通过配置更多的插件来共同完成项目自动化处理中的任务。前端性能优化
前端性能测试
通常前端性能可以认为是用户获取所需要页面数据或执行某个页面动作的一个实时性指标,一般以用户希望获取数据的操作到用户实际获得数据的时间间隔来衡量。
用户的等待延时可以分成两部分:可控等待延时和不可控等待延时。可控等待延时可以理解为能通过技术手段和优化来改进缩短的部分,例如减小图片大小让请求加载更快、减少HTTP请求数等。不可控等待延时则是不能或很难通过前后端技术手段来改进优化的,
获取和衡量一个页面的性能,主要可以通过以下几个方面: - Performance Timing API(已被最新 W3C 标准废弃)PerformanceNavigationTiming:最新标准随 Navigation Timing Level 2 于 2019 年推出,Navigation Timing Level 2 目的是代替涵盖 PerformaceTiming 的 Navigation Timing Level 1。Performance Timing API。
PerformanceNavigationTiming 是浏览器提供的 Navigation Timing API 的一部分,用于获取与网页导航相关的详细时间信息。这个接口允许开发者精确地测量网页加载过程中各个关键阶段的时间,从而帮助识别性能瓶颈和优化用户体验。
PerformanceNavigationTiming 包含了一系列的时间戳,这些时间戳代表了网页加载过程中的不同阶段。以下是一些关键的时间戳及其含义:
navigationStart:表示浏览器开始导航到当前页面的时间。这是所有时间戳的基准点。
unloadEventStart:表示前一个网页(如果有的话)开始执行 unload 事件的时间。如果当前页面没有前一个页面,或者前一个页面与当前页面不同源,则此值为0。
unloadEventEnd:表示前一个网页(如果有的话)unload 事件执行完毕的时间。同样,如果当前页面没有前一个页面,或者前一个页面与当前页面不同源,则此值为0。
redirectStart:表示第一个HTTP重定向开始的时间。如果页面没有经过重定向,则此值为0。
redirectEnd:表示最后一个HTTP重定向完成的时间。如果页面没有经过重定向,则此值也为0。
fetchStart:表示浏览器开始获取页面的时间,即浏览器准备好使用HTTP请求抓取网络资源的时间。这通常发生在DNS查找和检测本地缓存之前。
domainLookupStart 和 domainLookupEnd:分别表示DNS域名查询开始和结束的时间。如果使用了本地缓存或持久连接,则这两个值可能与fetchStart相等。
connectStart 和 connectEnd:分别表示开始/重新与服务器建立HTTP(TCP)连接的时间和连接建立完成的时间。如果是持久连接,connectStart可能与fetchStart相等,而connectEnd则可能表示TCP/TLS握手结束的时间。
secureConnectionStart:表示开始/重新与服务器建立HTTPS(TLS)连接的时间。如果不是安全连接,则此值为0。
requestStart:表示浏览器开始发送请求的时间,即HTTP请求开始读取真实文档的时间。
responseStart:表示浏览器收到响应的第一个字节的时间。
responseEnd:表示浏览器响应结束,即HTTP响应全部接收完成的时间。
domLoading、domInteractive、domContentLoadedEventStart、domContentLoadedEventEnd、domComplete:这些时间点与DOM的加载和解析过程相关,分别表示DOM树开始解析的时间、DOM树完成解析的时间、DOMContentLoaded事件开始和结束的时间,以及整个DOM加载完成的时间。
loadEventStart 和 loadEventEnd:分别表示load事件开始和结束的时间。load事件会在页面上所有的资源(包括图片、音频、视频等)加载完成后触发。
常见的性能指标:
网页重定向的耗时:redirectEnd - redirectStart
检查本地缓存的耗时: domainLookupStart - fetchStart
DNS查询的耗时:domainLookupEnd - domainLookupStart
TCP连接的耗时:connectEnd - connectStart
从客户端发起请求到接收到响应的时间 / TTFB:responseStart - fetchStart
首次渲染时间/白屏时间:responseStart - pnt.startTime
下载服务端返回数据的时间:responseEnd - responseStart
request请求耗时:responseEnd - requestStart
解析dom树耗时:domComplete - domInteractive
dom加载完成的时间:domContentLoadedEventEnd
页面load的总耗时:duration对前端有意义的几个过程主要是解析DOM树耗时、load事件耗时和整个加载过程耗时等,不过在页面性能获取时我们可以尽量获取更详细的数据信息,以供后面分析。 - Profile工具
Profile是Chrome和Firefox等标准浏览器提供的一种用于测试页面脚本运行时系统内存和CPU资源占用情况的API。结合Profile,可以实现以下几个功能。
1.分析页面脚本执行过程中最耗资源的操作
2.记录页面脚本执行过程中JavaScript对象消耗的内存与堆栈的使用情况
3.检测页面脚本执行过程中CPU占用情况 - 页面埋点计时
实际项目中,我们不会过多关注页面内存或CPU资源的消耗情况,因为JavaScript有自动内存回收机制。我们关注更多的是页面脚本逻辑执行的时间。
可以记录JavaScript代码开始执行的时间戳,后面在需要记录的地方埋点记录结束时的时间戳,最后通过差值来计算一段HTML解析或JavaScript解析执行的时间。为了方便操作,可以将某个操作开始和结束的时间戳记录到一个数组中,然后分析数组之间的间隔就得到每个步骤的执行时间
这种方式常常在移动端页面中使用,因为移动端浏览器HTML解析和JavaScript执行相对较慢,通常为了进行性能优化,我们需要找到页面中执行JavaScript耗时的操作,如果将关键JavaScript的执行过程进行埋点计时并上报,就可以轻松找出JavaScript执行慢的地方,并有针对性地进行优化。 - 资源加载时序图
这种方法可以粗粒度地宏观分析浏览器的所有资源文件请求耗时和文件加载顺序情况,如保证CSS和数据请求等关键性资源优先加载,JavaScript文件和页面中非关键性图片等内容延后加载。需要通过资源加载时序图来辅助分析页面上资源加载顺序的问题。桌面浏览器前端优化策略
- 网络加载类
1.减少HTTP资源请求次数
通过构建工具合并雪碧图、CSS、JavaScript文件等都是为了减少HTTP资源请求次数。另外也要尽量避免重复的资源,防止增加多余请求。
2.减小HTTP请求大小
如减少没必要的图片、JavaScript、CSS及HTML代码,对文件进行压缩优化,或者使用gzip压缩传输内容等都可以用来减小文件大小,缩短网络传输等待时延。
3.将CSS或JavaScript放到外部文件中,避免使用<style>或<script>
标签直接引入
如果CSS或JavaScript文件内容较多,业务逻辑较复杂,建议放到外部文件引入。
4.避免页面中空的href和src
当<link>
标签的href属性为空,或<script>、<img>、<iframe>
标签的src属性为空时,浏览器在渲染的过程中仍会将href属性或src属性中的空内容进行加载,直至加载失败,这样就阻塞了页面中其他资源的下载进程,而且最终加载到的内容是无效的,因此要尽量避免。
5.为HTML指定Cache-Control或Expires
为HTML内容设置Cache-Control或Expires可以将HTML内容缓存起来,避免频繁向服务器端发送请求。
6.合理设置Etag和Last-Modified
合理设置Etag和Last-Modified使用浏览器缓存,对于未修改的文件,静态资源服务器会向浏览器端返回304,让浏览器从缓存中读取文件,减少Web资源下载的带宽消耗并降低服务器负载。
7.减少页面重定向
页面每次重定向都会延长页面内容返回的等待延时,一次重定向大约需要600毫秒的时间开销,为了保证用户尽快看到页面内容,要尽量避免页面重定向。
8.使用静态资源分域存放来增加下载并行数
浏览器在同一时刻向同一个域名请求文件的并行下载数是有限的,因此可以利用多个域名的主机来存放不同的静态资源,增大页面加载时资源的并行下载数,缩短页面资源加载的时间。通常根据多个域名来分别存储JavaScript、CSS和图片文件。
9.使用静态资源CDN来存储文件
利用CDN网络加快同一个地理区域内重复静态资源文件的响应下载速度,缩短资源请求时间。
10.使用CDN Combo下载传输内容
CDN Combo是在CDN服务器端将多个文件请求打包成一个文件的形式来返回的技术,这样可以实现HTTP连接传输的一次性复用,减少浏览器的HTTP请求数,加快资源下载速度。
11.使用可缓存的AJAX
cache作用:是否在缓存中读取数据的读取。
cache属性是true(默认值)时:在第一次请求完成之后,如果地址和参数不变化,第二次去请求,会默认获取缓存中的数据,不去读取服务器端的最新数据。
cache属性是flase(默认值)时:每次读取的是最新的数据。
ajax缓存只对GET方式的请求有效,因为浏览器认为POST请求提交的内容必定有变化,所以不走缓存。对于返回内容相同的请求,没必要每次都直接从服务端拉取,合理使用AJAX缓存能加快AJAX响应速度并减轻服务器压力。
12.使用GET来完成AJAX请求
使用XMLHttpRequest时,浏览器中的POST方法发送请求首先发送文件头,然后发送HTTP正文数据。而使用GET时只发送头部,所以在拉取服务端数据时使用GET请求效率更高。
13.减少Cookie的大小并进行Cookie隔离
HTTP请求通常默认带上浏览器端的Cookie一起发送给服务器,所以在非必要的情况下,要尽量减少Cookie来减小HTTP请求的大小。对于静态资源,尽量使用不同的域名来存放,因为Cookie默认是不能跨域的,这样就做到了不同域名下静态资源请求的Cookie隔离。
14.缩小favicon.ico并缓存
有利于favicon.ico的重复加载,因为一般一个Web应用的favicon.ico是很少改变的。
15.推荐使用异步JavaScript资源
异步的JavaScript资源不会阻塞文档解析,所以允许在浏览器中优先渲染页面,延后加载脚本执行。
16.消除阻塞渲染的CSS及JavaScript
对于页面中加载时间过长的CSS或JavaScript文件,需要进行合理拆分或延后加载,保证关键路径的资源能快速加载完成。
17.避免使用CSS import引用加载CSS
带有@import的CSS样式需要在CSS文件串行解析到@import时才会加载另外的CSS文件,大大延后CSS渲染完成的时间。 - 页面渲染类
1.把CSS资源引用放到HTML文件顶部
2.JavaScript资源引用放到HTML文件底部
由于JavaScript资源默认是解析阻塞的,除非被标记为异步或者通过其他的异步方式加载,否则会阻塞HTML DOM解析和CSS渲染的过程。
3.不要在HTML中直接缩放图片
在HTML中直接缩放图片会导致页面内容的重排重绘,此时可能会使页面中的其他操作产生卡顿,因此要尽量减少在页面中直接进行图片缩放。
4.减少DOM元素数量和深度
HTML中标签元素越多,标签的层级越深,浏览器解析DOM并绘制到浏览器中所花的时间就越长,所以应尽可能保持DOM元素简洁和层级较少。
5.尽量避免使用<table>、<iframe>
等慢元素<table>
内容的渲染是将table的DOM渲染树全部生成完并一次性绘制到页面上的,所以在长表格渲染时很耗性能,应该尽量避免使用它,可以考虑使用列表元素<ul>
代替。尽量使用异步的方式动态添加iframe,因为iframe内资源的下载进程会阻塞父页面静态资源的下载与CSS及HTML DOM的解析。
6.避免运行耗时的JavaScript
任何与页面初次渲染无关的逻辑功能都应该延迟加载执行,这和JavaScript资源的异步加载思路是一致的。
7.避免使用CSS表达式或CSS滤镜
CSS表达式或CSS滤镜的解析渲染速度是比较慢的,在有其他解决方案的情况下应该尽量避免使用。移动端浏览器前端优化策略
- 网络加载类
1.首屏数据请求提前,避免JavaScript文件加载后才请求数据
2.首屏加载和按需加载,非首屏内容滚屏加载,保证首屏内容最小化
需要保证首屏加载资源最小化,非首屏内容使用滚动的方式异步加载。一般推荐移动端页面首屏数据展示延时最长不超过3秒。
3.模块化资源并行下载
4.inline首屏必备的CSS和JavaScript
将页面渲染时必备的CSS和JavaScript通过<script>或<style>
内联到页面中,避免页面HTML载入完成到页面内容展示这段过程中页面出现空白。
5.meta dns prefetch设置DNS预解析
设置文件资源的DNS预解析,让浏览器提前解析获取静态资源的主机IP,避免等到请求时才发起DNS解析请求。123<!-- cdn域名预解析--><meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//cdn.domain.com">
6.资源预加载
7.合理利用MTU策略
通常情况下,我们认为TCP网络传输的最大传输单元(Maximum Transmission Unit,MTU)为1500B,即一个RTT(Round-Trip Time,网络请求往返时间)内可以传输的数据量最大为1500字节。在前后端分离的开发模式中,尽量保证页面的HTML内容在1KB以内,这样整个HTML的内容请求就可以在一个RTT内请求完成,最大限度地提高HTML载入速度。
- 缓存类
1.合理利用浏览器缓存
除了上面说到的使用Cache-Control、Expires、Etag和Last-Modified来设置HTTP缓存外,在移动端还可以使用localStorage等来保存AJAX返回的数据,或者使用localStorage保存CSS或JavaScript静态资源内容,实现移动端的离线应用,尽可能减少网络请求,保证静态资源内容的快速加载。
2.静态资源离线方案
对于移动端或Hybrid应用,可以设置离线文件或离线包机制让静态资源请求从本地读取,加快资源载入速度,并实现离线更新。
3.尝试使用AMP HTML
AMP(Accelerated Mobile Pages):意思是移动页面加速,是由谷歌公司启动的一个加快移动页面加载速度的项目。2021年5月19日消息,谷歌宣布将会在 2021 年 6 月对页面体验排名算法进行重大调整,砍掉加速移动页面 (Accelerated Mobile Pages,简称AMP)的权重。 - 图片类
1.图片压缩处理
2.使用较小的图片,合理使用base64内嵌图片
需要注意的是,要保证图片较小,一般图片大小超过2KB就不推荐使用base64嵌入显示了。
3.使用更高压缩比格式的图片
使用具有较高压缩比格式的图片,如webp等。在同等图片画质的情况下,高压缩比格式的图片体积更小,能够更快完成文件传输,节省网络流量。
4.图片懒加载
5.使用Media Query或srcset根据不同屏幕加载不同大小图片
6.使用iconfont代替图片图标
使用iconfont体积较小,而且是矢量图,因此缩放时不会失真;可以方便地修改图片大小尺寸和呈现颜色。
7.定义图片大小限制
加载的单张图片一般建议不超过30KB,避免大图片加载时间长而阻塞页面其他资源的下载,因此推荐在10KB以内。 - 脚本类
1.尽量使用id选择器
2.合理缓存DOM对象
3.页面元素尽量使用事件代理,避免直接事件绑定
4.使用touchstart代替click
由于移动端屏幕的设计,touchstart事件和click事件触发时间之间存在300毫秒的延时,所以在页面中没有实现touchmove滚动处理的情况下,可以使用touchstart事件来代替元素的click事件,加快页面点击的响应速度,提高用户体验。但同时我们也要注意页面重叠元素touch动作的点击穿透问题。
5.避免touchmove、scroll连续事件处理
需要对touchmove、scroll这类可能连续触发回调的事件设置事件节流,例如设置每隔16ms(60帧的帧间隔为16.7ms,因此可以合理地设置为16ms)才进行一次事件处理,避免频繁的事件调用导致移动端页面卡顿。
6.避免使用eval、with,使用join代替连接符+,推荐使用ECMAScript 6的字符串模板
7.尽量使用ECMAScript 6+的特性来编程 - 渲染类
1.使用Viewport固定屏幕渲染,可以加速页面渲染内容
在移动端设置Viewport可以加速页面的渲染,同时可以避免缩放导致页面重排重绘。
2.避免各种形式重排重绘
页面的重排重绘很耗性能,所以一定要尽可能减少页面的重排重绘,例如页面图片大小变化、元素位置变化等这些情况都会导致重排重绘。
3.使用CSS3动画,开启GPU加速
可以设置transform: translateZ (0)来开启移动设备浏览器的GPU图形处理加速,让动画过程更加流畅。
4.合理使用Canvas和requestAnimationFrame
5.SVG代替图片
6.不滥用float
在DOM渲染树生成后的布局渲染阶段,使用float的元素布局计算比较耗性能,所以尽量减少float的使用,推荐使用固定布局或flex-box弹性布局的方式来实现页面元素布局。
7.不滥用web字体或过多font-size声明 - 架构协议类
1.尝试使用SPDY和HTTP 2
考虑使用SPDY协议来进行文件资源传输,利用连接复用加快传输过程,缩短资源加载时间。HTTP 2在未来也是可以考虑尝试的。
2.使用后端数据渲染
3.使用Native View代替DOM的性能劣势
任何一部分优化都可以做得很深入,但不一定都值得,在优化的同时也要尽量考虑性价比,这才是我们作为一名前端工程师处理前端优化时应该具有的正确思维。
通常页面上用户访问统计主要包括PV(Page View)、UV(Unique Visitor)、VV(Visit View)、IP(访问站点的不同IP数)等。前端用户数据分析
- 用户访问统计
PV:
PV一般指在一天时间之内页面被所有用户访问的总次数,即每一次页面刷新都会增加一次PV;
PV作为单个页面的统计量参数,通常用来统计获取关键入口页面或临时推广性页面的访问量或推广效果,由于PV的统计一般是不做任何条件限制的,可以人为地刷新来提升统计量,所以单纯靠PV是无法反应页面被用户访问的具体情况的。
UV:
UV是指在一天时间之访问内页的不同用户个数,和PV不同的是,如果一个页面在同一天内被某个相同用户多次访问,只计算一次UV;UV则可以认为是前端页面统计中一个最有价值的统计指标,因为其直接反应页面的访问用户数。
目前有较多站点的UV是按照一天之内访问目标页面的IP数来计算的,
因为在办公区或校园局域网的情况下,多个用户访问互联网网站的IP可能是同一个,但实际上的访问用户却有很多。所以为了得到更加准确的结果,除了根据IP,还需要结合其他的辅助信息来识别统计不同用户的UV,这里介绍两种常用的方式。
○ 根据浏览器Cookie和IP统计。
存在的问题是,如果用户手动清除了Cookie再进入访问,页面被重新访问时就只能算第二次。
○ 结合用户浏览器标识userAgent和IP统计。
在一定程度上区分同IP下的不同用户,但也不完全准确,IP和浏览器标识userAgent相同的情况也很常见,但仍却只能计算一次。
虽然UV是网站统计的一个很重要的统计量,但一般情况下是无法用于精确统计的,所以通常需要结合PV、UV来一起分析网站被用户访问的情况。此外,我们还可以对站点一天的新访客数、新访客比率等进行统计,计算第一次访问网站的新用户数和比例,这对判断网站用户增长也是很有意义的。
VV:
VV是统计网站被用户访问次数的参考数据,通常用户从进入网站到最终离开该网站的整个过程只算一次VV;PV和UV更多是针对单页面进行的统计,而VV则是用户访问整个网站的统计指标。例如用户打开站点,并在内部做了多次跳转操作,最后关闭该网站所有的页面,即为一次VV。
IP:
IP则表示一天时间之内访问网站的不重复IP数,一天时间内相同IP地址多次访问同一个网站只计算一次。
一般服务器端可以直接获取用户访问网站时的独立IP,统计也比较容易处理。 - 用户行为分析
页面点击量:
页面点击量用来统计用户对于页面某个可点击或可操作区域的点击或操作次数。
用户点击流分析:
点击流用来统计用户在页面中发生点击或操作动作的顺序,可以反映用户在页面上的操作行为。
用户访问路径分析:
用户访问路径不针对用户的可点击或操作区域埋点,而是针对每个页面埋点记录用户访问不同页面的路径。
用户点击热力图:
用户点击热力图是为了统计用户的点击或操作发生在整个页面哪些区域位置的一种分析方法,一般是统计用户操作习惯和页面某些区域内容是否受用户关注的一种方式。
这种统计方法获取上报点的方式主要是捕获鼠标事件在屏幕中的坐标位置进行上报,然后在服务端进行计算归类分析并绘图。
用户转化率与导流转化率:
对用户转化率的分析一般在一些临时推广页面或拉取新用户宣传页面上比较常用,导流的页面统计分析和该页面的功能类似,不过其作用是将某个页面的用户访问流量引导到另一个页面中,导流转化率可以用通过源页面导入的页面访问PV相对于源页面的总PV比例来表示。
用户访问时长、内容分析:
用户访问时长和内容分析则是统计分析用户在某些关键内容页面的停留时间,来判断用户对该页面的内容是否感兴趣,从而分析出用户对网站可能感兴趣的内容,方便以后精确地向该用户推荐他们感兴趣的内容。 - 前端日志上报
怎样获取错误日志:
浏览器提供了try…catch和window.onerror的两种机制来帮助我们获取用户页面的脚本错误信息。一般来说,使用try…catch可以捕捉前端JavaScript的运行时错误,同时拿到出错的信息,例如错误信息描述、堆栈、行号、列号、具体的出错文件信息等。我们也可以在这个阶段将用户浏览器信息等静态内容一起记录下来,快速地定位问题发生的原因。需要注意的是,try…catch无法捕捉到语法错误,只能在单一的作用域内有效捕获错误信息,如果是异步函数里面的内容,就需要把function函数块内容全部加入到try…catch中执行。
window.onerror一般用于捕捉脚本语法错误和运行时错误,可以获得出错的文件信息,如出错信息、出错文件、行号等,当前页面执行的所有JavaScript脚本出错都会被捕捉到。
然而,使用onerror要注意,在不同浏览器中实现函数处理返回的异常对象是不相同的,而且如果报错的JavaScript和HTML不在同一个域名下,错误时window.onerror中的errorMsg全部为script error而不是具体的错误描述信息,此时需要添加JavaScript脚本的跨域设置。
如果服务器因为一些原因不能设置跨域或设置起来比较麻烦,那就只能在每个引用的文件里添加try…catch进行处理。我们可以对前端脚本中常用的异步方法入口函数或模块引用的入口方法统一使用try…catch进行一层封装,这样就可以使用try…catch捕获每个引用模块作用域下的主要错误信息了
怎样将错误信息上传到服务器:
错误信息上报设计时需要注意一点:页面的访问量可能很大,如果到达百万级、千万级,那么就需要按照一定的条件上报,例如根据一定的概率进行上报,否则大量的错误信息上报请求会占用日志收集服务器的很多资源和流量。
怎样通过高效的方式来找到问题:
通常可以建立一个简单的内容管理系统(Content Management System,CMS)来管理查看错误日志,对同一类型的错误做归并统计,也可以建立错误量实时统计来查看错误量的即时变化情况。当某个版本发布后,如果收到的错误量明显增加,就需要格外注意。另外一点要注意的是,上报错误信息机制是用来辅助产品质量改进的,不能因为在页面中添加了错误信息收集和上报而影响了原有的业务模块功能。
文件加载失败监控:
可以对<img>
或<script>
标签元素的readyChange进行是否加载成功的判断。不幸的是,只有部分IE浏览器支持<img>或<script>
的readyState,因此一般还需要结合其他方式,如onload,针对不同浏览器分开处理。
通过这种方法仅仅是判断了文件或脚本加载成功的情况,我们还需要指定一个文件加载的列表,在一段时间后将页面文件加载的结果对象上报给服务器端来统计不同文件的具体加载情况。 - 前端性能分析上报
移动端首屏建议加载的最长时间为3秒,推荐为1.5秒以内。PC端推荐为1秒以内,最长不超过1.5秒。需要注意的是,不要过度设计,例如对于访问量很少的网站进行大量的用户行为分析可能就得不偿失了。将页面性能数据进行上报统计,在服务器端统计计算性能数据的平均值来评判前端具体页面的性能情况。
### 前端搜索引擎优化基础 - 1 title、keywords、description的优化
title、keywords、description是可以在HTML的<meta>
标签内定义的,有助于搜索引擎抓取到网页的内容。要注意的是,一般title的权重是最高的,也是最重要的,因此我们应该好好利用title来提高页面的权重。
title的优化
一般title的设置要尽量能够概括页面的内容,可以使用多个title关键字组合的形式,并用分隔符连接起来。
“_”分隔符比较容易被百度搜索引擎检索到,“-”分隔符则容易被谷歌搜索引擎检索到,“,”则在英文站点中使用比较多,可以使用空格。title的长度在桌面浏览器端一般建议控制在30个字以内,在移动端控制在20个字以内,若长度超出时浏览器会默认截断并显示省略号。
keywords
keywords是目前用于页面内容检索的辅助关键字信息,容易被搜索引擎检索到,所以恰当的设置页面keywords内容对于页面的SEO也是很重要的,而且keywords本身的使用也比较简单。
description优化
description更重要的作用是作为搜索结果的描述,而不是作为权值计算的重要参考因素。description的长度在桌面浏览器页面中一般为78个中文字符,移动端为50个,超过则会自动截断并显示省略号。 - 2 语义化标签的优化
使用具有语义化的HTML5标签结构
唯一的H1标题
建议每个页面都有一个唯一的<h1>
标题,但一般<h1>
内容并不是网站的标题。<h1>
作为页面最高层级的标题能够更容易被搜索引擎收录,并赋予页面相对较高权重的内容描述。<img>
添加alt属性。 - 3 URL规范化
最好统一搜索引擎访问页面的地址,否则可能影响网站入口搜索结果的权重。
301跳转:
如果URL发生改变,一定要使旧的地址301指向新的页面,否则搜索引擎会把原有的这个URL当作死链处理,之前完成的页面内容收录权重的工作就都失效了。
canonical:
对于防止内容被重复索引和防止内容被误认为重复内容(duplicate content)是非常有用的。这对于网站优化(SEO)特别重要,尤其是当你有多个URL指向相同内容的时候。canonical标签主要用于规范化网址,比如当网站一个页面有多个URL可以访问时,为避免权重分散,我们通过canonical标签告诉搜索引擎哪个页面地址才是主要的,同时这个URL也是我们想被收录的地址。<head>
上加上canonical声明,告诉搜索引擎在收录页面时可以按照这个href提供的页面地址去处理,而不是将每个地址都独立处理。 - 4 robots
robots.txt是网站站点用来配置搜索引擎抓取站点内容路径的一种控制方式,放置于站点根目录下。搜索引擎爬虫访问网站时会访问robots.txt文件,robots.txt可以指导搜索引擎爬虫禁止抓取网站某些内容或只允许抓取哪些内容,这就保证了搜索引擎不抓取站点中临时或不重要的内容,保证网站的主要内容被搜索引擎收录。 - 5 sitemap
sitemap格式一般分为HTML和XML两种,命名可以为sitemap.html或sitemap.xml,作用是列出网站所有的URL地址,方便搜索引擎去逐个抓取网站的页面,增加网站页面在搜索引擎中的的曝光量。
前端工程师需要合作或沟通的角色多种多样,所以我们平时要注意提高沟通效率来节省沟通时间,从而提升工作效率。前端协作
沟通能力
通俗地说就是你向别人传达信息或从别人那里获取信息的能力。沟通技巧就是为了高效传达信息或获取信息使用的方法和手段。
对于产品经理的需求,我们要完成,但同时务必要考虑以下几点。
○ 产品经理提出的需求是否明确。
○ 技术方案是否可行,即需求开发的难度评估。
○ 需求性价比是否够高。
○ 需求是否合理。
○ 风险管理。
如果因为需求变更导致延期一定要告知产品经理,询问其是否接受,不要等到需求交付时再告诉大家并未完成。尽量想的更多一些,理解和尊重合作伙伴,因为能一起合作都是彼此的荣幸。
数据协议文档
推荐使用RESTful的通用协议规范来定义数据接口。当然基本的文档一定是需要的,它可以帮助我们解决大部分的问题。
开发沟通方式
开发流程在进行时,必要的会议不能少,信息沟通的方式当然最好是前、后台开发人员能在相同的办公区域内协作。
如果你要找运维工程师协作,一定要有耐心,同时也不能被他们拖慢节奏,也许你唯一能做的就是Push,推动他们配合你的工作。
除了保质完成业务外,也要利用一部分时间来不断学习,支持团队的技术建设,例如团队开源项目开发维护等,自己如果想尝试新的想法也可以提出。
成为别人眼中的优秀工程师,同时在团队的技术建设中体现自身的价值。第6章 前端跨栈技术
JavaScript跨后端实现技术
- 1 Node后端开发基础概述
Node后端基础知识和技术
○ 服务器知识基础。
○ 简单的数据库设计能力。
○ 后端MVC设计理念。
○ 后端异步。
○ 模块化思想。
○ 中间件技术。
○ 接口设计规范。
○ 后端部署技术和基本运维能力。 - 2 早期MEAN简介
使用Express作为Web框架进行小型的Web站点建设,与之结合的主流技术则以M(Mysql)、E(Express)、A(Angular)、N(Node)最为典型,甚至到了今天MEAN技术组合的方式仍在沿用。各类其他前后端框架都可以用来灵活组合作为MEAN的替代选型方案。 - 3 Node后端数据渲染
SPA场景下SEO的问题:通常情况下,SPA应用或前后端分离的开发模式下页面加载的基本流程是,浏览器端先加载一个空页面和JavaScript脚本,然后异步请求接口获取数据,渲染页面数据内容后展示给用户。
搜索引擎抓取页面解析该页面HTML中关键字、描述或其他内容时,JavaScript尚未调用执行,搜索引擎获取到的仅仅是一个空页面,所以无法获取页面上<body>
中的具体内容,这就比较影响搜索引擎收录页面的内容排行了。尽管我们会在空页面的<meta>
里面添加keyword和description的内容,但这肯定是不够的,因为页面关键性的正文内容描述并没有被搜索引擎获取到。如果使用Node后端数据渲染(有人称之为直出,后文中也称之为直出层),在页面请求时将内容渲染到页面上输出,那么搜索引擎获取到的HTML就已经包含页面完整的内容,页面也就更容易被检索到了。前端页面渲染展示缓慢的问题:在前后端分离的开发模式下页面在JavaScript执行渲染之前是空白的(或提示用户加载中)。用户在看到数据时已经花费的网络等待时间:DOM下载时间+ DOM解析时间+ JavaScript文件请求时间+ JavaScript部分执行时间+接口请求时间+ DOM渲染时间。
目前一般后台页面数据直出的通用架构设计,直出层接受前端的路由请求,并在Node端的Controller层异步请求服务接入层接口,获得Model数据并进行组装拼接,然后提取相对应的Node端View模板渲染出HTML输出给用户浏览器,而不用通过前端JavaScript请求动态数据后渲染。不仅如此,直出层根据不同的浏览器userAgent,也可以提取不同的模板渲染页面返回给不同的用户浏览器,所以这种实现方式不仅非常适合大型应用服务的实现场景,而且可以方便地实现网站的响应式内容直出。 - 4 前后端同构概述
思考两个新的问题。不得不维护两套不同的前后台模板或技术实现——前端渲染实现逻辑和后端直出实现逻辑,尽管可能都是用JavaScript写的。如果是在移动端Hybrid应用上,离线包机制实现可能就会出现问题。因为每次都是从后端直出HTML结构给前端,这样就难做到将HTML文件进行离线缓存,而只能进行其他静态文件的缓存。
前后端同构的一些内容。
4.1.实现同构的核心
前后端同构的宗旨是,只开发一套项目代码,既可以用来实现前端的JavaScript加载渲染也可以用于后台的直出渲染。
前后端同构的核心问题是实现前后台数据渲染的统一性。
4.2.同构的优势
(1)可以根据用户的需求方便地选择使用前端渲染数据还是后台直出页面数据;(2)开发者只需维护一套前端代码,而且可以沿用前端原有的项目组件化管理、打包构建方式,根据不同的构建指令生成类似的前后端数据模板或组件在前后端执行解析,所以这对于DOM结构层上的开发方式应该是一致的。 - 5 前后端同构实现原理
5.1 基于数据模板的前后端同构方案
基本原理是将模板描述语法与数据进行拼接生成HTML代码字符串插入到页面特定的元素中来完成数据的渲染。如果前后端使用同一个模板解析引擎,那么我们只需要编写同一段模板描述语法结构就可以在前端和后端分开进行渲染了。如果选择在前端渲染,则可以将模板进行打包编译,在数据请求成功后进行DOM渲染;如果选择后端渲染,就可以将模板数据直接发送到直出层的View视图进行渲染,实现同一个模板语法结构在前后端渲染出相同的内容。要保证前后端使用的模板渲染引擎或者模板解析的语法是一致的。
5.2 基于MVVM的前后端同构
前端编写的同一段MVVM的语法结构通过前端MVVM框架解析或后端Directive运行解析最终都可以生成相同的HTML结构,不同的是前端执行解析后生成的是ViewModel对象并通过浏览器体现,后端渲染则生成HTML标签的文本字符串输出给浏览器。这里同样需要做一件事,即在后台实现一个与前端解析Directive相同的模块,甚至还包括一些filter、语法表达式等的实现。
5.3 基于Virtual DOM的前后端同构
在前端开发的组件中声明某段Virtual DOM描述语法,然后通过Virtual DOM框架解析生成Virtual DOM,这里的Virtual DOM既可以用于在浏览器端生成前端的DOM结构,也可以在直出层直接转换成HTML标记的文本字符串输出,后面这种情况就可以在服务端上实现Virtual DOM到HTML文本字符串的转换。通过对Virtual DOM的不同操作处理,就可以统一前后端渲染机制,实现组件的前后端对同一段描述语法进行渲染。
无论选择哪一种前后端同构的实现方案,所需要处理的几个问题是类似的。
○ 前后端框架选择。这里主要包括前端使用的主要框架和后端结构渲染解析模块的选择,通常选择不同的实现方式所对应的框架实现也不一样。
○ 模板渲染机制。
需要保证前后端都能对同一套模板或描述语法进行识别处理,生成前后端各自能处理的结构。
○ 构建打包。
同一套代码基于前后端场景打包完成后是不一样的,并且对于开发者来说需要有完整的模块化机制、打包体系和不同的输出调试目录,方便业务层上的开发。
○ 渲染和直出区分。跨终端设计与实现
1 Hybrid技术趋势
Native应用的优点
○ 原生系统级Native API的支持,如访问本地资源、相机API等
○ 资源在打包安装时生成,节省用户使用时的流量
○ 可针对不同平台特性进行用户体验优化
○ 运行速度快、性能好,可使用原生Native动画库
Native应用的缺点
○ 开发成本高,兼容性差,尤其是对于Android机型
○ 维护成本高,用户必须手动下载更新,历史版本也需要维护
○ 上线时间不确定,一般需要通过应用商店的审核
○ 版本更新慢,更新时需要用户重新下载安装包
○ 应用界面的内容不可被搜索引擎检索
Web应用的优点
○ 开发成本低,使用前端开发技术即可
○ 跨平台和终端,基于浏览器或WebView运行
○ 部署方式简单、快捷,无需用户安装
○ 用户总能访问到最新版本,迭代速度快
○ 内容可被搜索引擎检索
Web应用的缺点
○ 浏览体验无法超越Native应用
○ 消息推送、动画等实现方式相对Native实现方式较差
○ 不能调用设备的原生特性,如无法访问本地资源、相机API等
Hybrid应用的优点
○ 开发成本较低,可以使用前端开发技术,甚至可以自动添加Native外壳来实现独立移动端应用
○ 跨平台和终端,内容网页可基于浏览器或WebView运行
○ 拥有与Web应用相同的快速迭代特性
○ 部署方式简单、快捷,只更新Web资源即可
○ 可支持实现离线应用
○ 可通过JSBridge调用设备的系统级API,如访问本地资源、相机API等
○ 原生应用版本迭代和Web功能迭代相互独立也可以相互结合
○ 不同性能需求的功能可以选择性使用Native或Web实现
○ 内容可被搜索引擎检索
○ 借助于MNV*的开发模式可以更接近Native应用的用户体验
Hybrid应用的缺点
○ 部分机型兼容相对Native较差,但比Web应用体验好很多2 Hybrid实现方式
以前端为主的Hybrid实现方式
在以Apache Cordova为基础的开发工具实现下,前端开发者不需要了解Native相关的内容,只需要专注于前端页面功能的开发即可,功能开发完成后自动将前端内容统一打包进发布安装包,安装时解压到移动端本地目录加载运行。
其优势是前端开发者可以独立快速构建Hybrid应用,不需要Native开发人员的支持,前端调用Native的解决方案也可以使用开源的JSBridge库(如cordova.js等)来实现。
这种实现方式适合于中小型需要快速完成开发的应用场景,如果是在用户量较多、实时性要求较高、应用需要快速持续迭代或者需要与扩展Native功能结合的应用场景中,就不适用了。
Native和Web结合的Hybrid方式
通常在一个完整的移动端Hybrid应用中,功能是由核心Native功能和Web站点页面组成的,其中Web站点页面中可以调用Native功能。此外,Native和Web的功能通常也不一样,实现自己最擅长的功能。
例如Native就可以用来实现移动端应用的通用导航菜单、系统UI层、核心界面动效、默认访问界面、高效的消息推送或APP大版本的应用更新等,因为这些功能一般比较稳定,变化不大,而且不涉及需要快速迭代的业务逻辑。而Web端则可用来实现开发迭代速度更快的相关业务层界面逻辑,它很可能是某个Native应用内关联的某个Web轻应用。
使用这种开发模式首先要非常关注开发过程中Web和Native的调用接口规范问题,因为这种情况下我们一般不会借助开源的交互实现方案,所以自己如何去设计Web和Native的交互协议就显得很重要了。3 基于localStorage的资源离线和更新技术
在Hybrid应用开发时,常常需要在离线的情况下打开页面或者为了让Hybrid页面应用加载启动更快,避免长时间等待资源加载过程中造成页面空白的出现。因此我们必须要考虑使用资源的离线缓存技术来加快页面启动时的载入速度了。
ServiceWorker的资源离线与更新
localStorage资源离线缓存与更新
其基本思路是将JavaScript、CSS资源文件甚至是接口返回的数据资源缓存到浏览器的localStorage中,下次打开页面时不进行JavaScript和CSS资源的请求,而是直接通过localStorage读取内容,然后插入到页面中解析执行。这里需要注意的是,为了判断是加载线上静态资源还是从localStorage中读取资源,页面中JavaScript和CSS资源的加载方式通常都是动态创建标签加载或通过eval执行的,而且通常只有页面在第二次打开或之后加载静态资源的情况才可能从localStorage中读取。
这种实现方式的好处是比较简单,不需要服务端和移动客户端平台的支持,可以实现纯移动端应用的离线访问。当然缺点也很明显:首先localStorage的大小有限制(同域一般认为是5M以内),同域名的localStorage存储的离线资源较多时很可能会内容超出,容易出错,需要通过资源替换策略来处理,这样就比较麻烦;其次是用户如果手动清空localStorage会使离线资源失效,这个问题基本上不能解决;还有就是读取localStorage的速度其实是比较慢的,尤其在移动端,如果localStorage的内容较多,返回的速度可能会比较慢。
基于增量文件的更新方式
通过比较不同版本就可以只加载不同版本的增量文件,但同时需要在服务器端每次新版本发布时维护多个增量文件来适应不同的旧版本更新的需要。
如何根据两个新旧版本的文件生成字符级的增量文件,
增量文件算法:指只处理文件发生变化的部分,而不是整个文件,从而节省时间、带宽和存储空间。
分块:将文件分割成较小的、易于管理的数据块。这些块可以是固定大小的,也可以是可变大小的,具体取决于算法的设计。目前主要有两种基本的算法来实现这一过程。
基于文件代码分块的增量更新机制
来看一个具体的例子,新旧两个版本的JavaScript文件压缩后上线的代码字符串分别为leta=1;alert(a);和let a=1;alert(a+1);,我们根据块大小为4个字符来分割,
所以计算获取的增量内容描述为[1,2,3,’t(a+’ , ‘1);’]。这种方式在代码较多的情况下就可以用块序号来描述不变的代码块,减小增量文件大小,达到节省流量的目的。
基于编辑距离的增量更新机制
前一种方法是基于文件内容分块chunk算法来进行增量更新的,节省资源的量取决于块的大小和内容变化的块序号分布,比如每个块中都只更新了一个字符,那么仍然需要下载这个文件的所有块内容。
1965年俄罗斯科学家Vladimir Levenshtein提出了Levenshtein Distance(编辑距离,相关算法的实现内容比较多,有兴趣可以参考其他资料)的概念,它指的是从一个字符串变换到另一个字符串所需要的最少变化操作步骤。
这种情况对于少量的字符更新很有用,如果一次更新的内容很多,生成的增量文件很有可能比源文件还大,所以实际使用过程中需要结合具体情况在这两种增量算法实现中进行选择。4 基于Native与Web的资源离线和更新技术
Native和Web结合的Hybrid资源离线与更新
Web端的代码资源是通过离线包的方式发布到服务端静态目录上的,同时主站点会保存一个线上的前端页面实现供浏览器直接加载使用。
通常Native应用启动时会主动拉取线上Web离线包版本,然后与本地保存的版本进行对比,如果没有更新则不做操作;如果本地的离线包需要更新或者本地没有离线包,则会去离线包服务器拉取最新的离线包或者拉取增量离线包到本地,然后解压合并到本地的指定离线包目录下。当有用户访问目标页面时,Native应用会先检查该文件地址映射到的离线包本地目录中的文件,如果离线包目录有内容,则直接读取离线包目录的文件加载;否则线上拉取静态资源到页面上解析执行,同时通知Native应用去拉取最新的离线包资源,这样当下一次继续请求目标页面时WebView就可以读取到本地离线目录中的内容了。
前端离线包的生成比较简单,一般是通过构建工具将站点资源目录直接压缩,在发布前端页面与静态资源的同时上传离线包到服务器或CDN上。这里需要注意的是,为了保证移动端应用每次尽可能拉到较小的离线包内容,上传服务器端离线包时也需要生成与之前版本对比的增量更新包发布到服务器或CDN上的。
如何根据两个离线包计算增量包的算法也和计算差量文件的算法类似,既可以根据增量包内每个文件资源的增量文件来计算,计算方法和localStorage实现增量文件的方法类似,也可以通过直接针对压缩包文件二进制数据内容分块进行对比的增量方法来实现。5 资源覆盖率统计
在新的资源发到服务器上后,用户客户端里面可能仍会存在旧版本的运行代码逻辑,这就需要知道有多少用户已经更新到了新的版本。在增量包的生成过程中,如果某个旧版本的用户使用率已经很小,或者基本接近0,那我们就可以考虑后面不再对这个版本生成增量包,并让这部分用户直接拉取最新的全量包,避免在版本发布较多时线上有过多的历史增量包版本存在。因此,统计离线包资源覆盖率是很有意义的。
统计的方法很多,一个简单可行的方式就是后台统计上报版本号。
在发布了新版本后,每次PV统计时带上版本号,最后根据PV中的版本号来统计访问不同版本上用户的分布情况。
我们通常将某版本以上的覆盖率总和超过95%(当然这个值根据不同业务的场景可以自己定义,而且尽量不要去统计单个版本的覆盖率情况)时认为是该版本覆盖完成,而将该版本发布时起到该版本以上版本覆盖率总和超过95%的这段时间称为离线包的更新周期。
通过离线包覆盖率的统计,我们可以很清晰地了解之前版本的用户覆盖情况,也有助于进行产品的完善和改进。6 仍需要注意的问题
Hybrid性能问题
Hybrid应用的WebView存在另一个性能问题:HTML的DOM渲染和操作较慢。需要注意的是,这里所说的慢是相对的,是Native应用WebView内容操作相对于移动端浏览器的内容操作来说的,即便我们把前端优化做到极致,HTML DOM的运行机制较慢仍是不可改变的。
前端技术栈的其他应用实现
如Native编译技术或者桌面应用开发。
第7章 未来前端时代
未来前端趋势
- 1 新标准的进化与稳定
经过大版本的更新稳定,目前前端三层结构实现已经处于HTML5、CSS3、ECMAScript 6+标准规范结合的阶段,后面标准的新变化也会越来越小。 - 2 应用开发技术趋于稳定并将等待下一次革新
前端技术栈的Native开发实现技术必将成为前端技术的下一个实践核心。 - 3 持续不断的技术工具探索
- 4 浏览器平台新特性的应用
经过多版本的迭代,浏览器上已经可以实现较多的增强和实用特性,例如Web Component、Service Worker、IndexDB、WebAssembly、WebRTC、ECMAScript 6+的支持等,但由于浏览器的种类和版本多样,我们还不能在业务中直接推广使用这些新的特性,但这些特性仍然给了我们很多实现未来技术的可能,并且未来较多技术会在这些新特性的基础上进行优化或改进产生。 - 5 更优化的前端技术开发生态
前端正朝着多端、多技术实现的方向发展。作为整套技术开发生态的一部分,每一项技术的出现都必须要考虑开发效率、维护成本、性能、扩展性这几个方面的问题,所以寻找并发展更优的开发生态体系仍是未来前端技术的大方向
其实扩展性并不只是框架的方便定制和扩展特性,还要考虑是否能与原来的技术框架相兼容并解耦合。 - 6 前端新领域的出现
现有前端开发技术趋渐成熟,新的前端技术领域跃跃欲试,可以肯定的是物联网Web、Web VR、人工智能必定会成为前端的下一批革命性技术。我们需要做的,仍是把握技术发展趋势,紧跟领域前进的步伐,在漫漫前端道路上继续前进。做一名优秀的前端工程师
- 1 学会高效沟通
- 2 使用高效的开发工具
- 3 处理问题方法论
3.1.代码类问题
第一步要先确认问题,就是弄清楚是不是真正的问题。绝大部分情况下是有问题的,但有时是因为产品经理需求的策略、设置了网络代理或测试方法环境等导致的,所以这里要稍微注意一点。
如果确实有问题,那么第二步要确定是什么问题,可能是代码类问题,也可能是产品或设计考虑不全面的问题。作为开发者,我们通常只能处理可控的代码类问题,如果问题不在自己的可控范围内就要尽快沟通反馈,让相关人员做出修改,如果确认是因为自己开发引起的,第三步就要想想解决的方法了。如果问题修改很快就能解决,建议马上进行处理;如果需要较大的修改工作量,就要考虑下解决方案性价比的问题了;如果处理比较麻烦,建议通过新版本或将问题独立出来处理,当然这类情况通常会比较少。
3.2.需求类问题
(1)作为开发者,我们遇到最常见的需求类问题可能就是变更了,首先不要直接爽快地接受需求变更,而是评估需求的等级:如果是一些小的问题不会花太多时间,那么建议先接受确认修改;当然也不排除会带来较多额外工作量的情况,这种情况就最好重新进行需求排期,毕竟是产品同学设计时欠考虑所导致的问题,这是开发者不可控的;还有一种情况你可能也会遇到,变更的需求涉及比较细节的非核心内容,但要实现的代价较大,这种情况更多的处理方式是不接受,或降低优先级在核心功能完成后再进行处理。
(2)除了需求变更,可能遇到的第二类问题就是应对多个需求并行的情况,甚至可能同时应对多个不同的产品经理的需求,而且每个需求都是不能马上解决的。需求之间是有优先级关系的:如果一个需求不马上完成会明显导致线上大的业务流程缺陷,那就建议先先从这个下手;如果几个需求都显得很紧迫,这就只能和产品经理一起讨论开发计划。一心不能两用,工作时间也不能double,在给不出好建议的前提下,那就让产品经理自己决策好优先级。
(3)无法避免需求风险管理问题。简单理解就是,需求不能按期交付,你虽然已经做好了排期管理和需求评估,但是由于各种原因(需求变更或技术方案变更)已经确定不能按期完成。需求不能按时交付,要将风险尽早地暴露出来,千万不要等到最后告诉大家。
3.3.其他类问题
原先定好的开发周期会因为各种开会让开发时间显得不足,评审会、总结会、分享会都会占用时间。要相信,这是工作常态。这种情况需要灵活处理,需求开发排期评估时预留些缓冲时间就可以解决了。 - 4 学会前端项目开发流程设计
具体来说就是能否快速地设计建立一个项目开发的Codebase,这里面包括前端框架选型、模块化方案、代码规范化、构建自动化、组件化目录设计、代码优化处理、数据统计、同构项目结构设计等,基于这个组建好的Codebase,个人或团队其他人就能在这个基础上直接便捷、高效地开发业务模块了。
在进行开发项目流程设计时,也不一定需要使用那些最新的技术和设计思路,更建议根据自己或团队的具体情况来决定。一方面,如果不这样做有可能会增加其他人对项目的上手难度,或者后期维护的成本;另一方面,可以避免过度设计,如果你的网站访问量本来就比较小,就没必要用很复杂的前、后台架构了。 - 5 持续的知识和经验积累管理
推荐读者们关注一些更新较多的技术论坛或一些优秀前端团队的技术博客,常去看看总会有些收获。很多时候,我们都不知道要学什么,前端的知识很零散,涉及的方面很广,看了别人的很多博客,但都感觉只是了解点点面面,没能完全体系化地学会。书中的前几章涉及的内容通过几条主线涵盖了目前前端方面几乎所有的技术知识和设计原理,按照这些方向去研究挖掘就可以基本了解前端需要掌握的所有知识了。 - 6 切忌过分追求技术
一切技术的最终目的都是为产品实现服务的。技术研究应该是在完成并希望将产品打造更好的目的上进行的,切忌过分追求技术,让自己沉迷在探索技术的道路上。 - 7 必要的产品设计思维
有经验的工程师开发需求时除了实现需求,做的另一件事情是思考这个需求为什么是这样的呢?是不是合理呢?如果不合理该怎么修改呢?很多时候产品人员不懂开发知识,设计细节时都是站在用户的角度上思考,功能设计都比较主观,从而可能会出现很多问题。
开发者也可以从一个用户的角度上去思考产品。作为前端工程师,强烈建议大家去看一两本关于产品经理方面的书籍。一是作为整个产品项目流程的下游实现者,有必要去了解一个互联网产品的生命周期是怎样的;二是学习一些常用的产品设计常识,在一定程度上学会分析需求的漏洞和完整性。