Created
April 30, 2017 08:34
-
-
Save kinka/ea3ad293bf7e850ddc984fe9d8db233e to your computer and use it in GitHub Desktop.
workec.com
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
##开场 | |
自我介绍, 来自tencent imweb团队的前端开发kinka, 列举工作经历(qq群, ptlogin/wtlogin, 视频云,腾讯课堂,企鹅辅导),贴出几张ptlogin登录框的图,大家都应该用过QQ登录,指出每天pv达亿级别,指出腾讯登录特有的快速登录功能;wtlogin也是大家都用过的,基本上每个腾讯的APP都会接入,虽然没有界面通常感受没那么明显,但是每次打开手Q或者微信使用Q号登录的时候,都会运行wtlogin sdk的代码。腾讯云上也开放了对应的登录能力,tls sdk,帮助大家解决基本又必须的功能。腾讯课堂是一个在线学习的平台,提供了大量的课程供大家学习,提升各方面技能,而说到企鹅辅导,可以说就是为下一代准备了。名校名师,为孩子们提供更加公平的教育环境。 | |
广告做完了,开始进入分享主题:前端页面监控 | |
###先抛出问题,做产品需求的时候,程序员们最关心什么? | |
我想多半是关心如何实现吧。好不容易理解完需求,跟产品经理进行各种PK之后,排期,开工了,开始了一个漫长的coding/debug过程,然而测试,发布,然后就收工大吉了~ | |
突然某一天,产品过来问,上次做的某某功能,有多少人在用,用得怎么样,功能是不是达到预期了,是不是要改进? | |
那个。。。我不知道呀。(黑人问号图) | |
又某一天,用户反馈,某个功能怎么不能用了? | |
在我这里是好的呀,刷新页面试试?(贴出程序员遇到bug经典回复图) | |
又某一天, 老大说,你这个页面加载太慢了,给我优化下。。。 | |
我这里打开挺快的,是不是你机器配置太差了。。。 | |
上面列举的场景,肯定都不是理想的效果。为什么会这样子,哪个环节出了问题?可以看出,这些列举的场景都是产品发布之后的事情,程序员甩手之后的事情,程序员不应该直接这么甩,要把握产品的线上质量,我们还要给页面加上相应的监控。 | |
通常来说,从两个角度看待监控问题。 | |
对于产品来说,关注功能的曝光,关注功能的使用效果,这里需要有pv/uv/用户行为的上报; | |
对于程序员来说,一个是质量,一个是性能。就前端而言,就分别是js有没有出现异常,页面加载快不快,特别是如果页面出现异常的时候,程序员能不能得到及时的反馈。 | |
## 添加监控代码演示 | |
接下来,我们以一个简化的页面为例子,演示如何一步步加上监控。 | |
展示页面效果,然后是页面代码结构 | |
**贴出登录框的图片**,有以下功能: | |
1. 快速登录(出现多个头像,示例pagelocation) | |
2. 密码输入登录 (从开始输入密码到点击登录按钮,时间打点监控) | |
3. 两种场景可以切换(示例用户行为) | |
``` | |
<html> | |
<head></head> | |
<body> | |
<div class="portal"> | |
<section class="qlogin" id="qlogin"> | |
<ul class="avatar-list" id="avatar_list"><li class="avatar"></li></ul> | |
<button id="switch_to_plogin">切换普通登录</button> | |
</section> | |
<section class="plogin" id="plogin"> | |
<form> | |
<input name="username" id="username" /><label for="username">用户名</label> | |
<input name="password" id="password" type="password"/><label for="password">密码</label> | |
<button id="do_login">登录</button> | |
</form> | |
<button id="switch_to_qlogin">切换快速登录</button> | |
</section> | |
</div> | |
<script> | |
function init() { | |
if (isSupportQlogin()) | |
switchTo(QLOGIN); | |
} | |
$('#do_login').onclick = function() {...异步代码}; | |
function initQlogin() { | |
const qloginList = getQloginList(); | |
for (let i=0; i<qloginList.length; i++) { | |
const avatar = qloginList[i]; | |
$('#avatar_list').appendChild(<li onClick={doQlogin(avatar)}>{avatar.name}</li>); | |
} | |
} | |
function isSupportQlogin() {...} | |
</script> | |
</body> | |
</html> | |
``` | |
##pv/uv/用户行为的监控 | |
先来解决产品同学的问题,比如说快速登录功能是一个新上线的需求,那么肯定想知道其曝光PV。 | |
假设功能都已经完成,现在是后期加上报阶段。 | |
代码还是很清晰的,那就在`switchTo(QLOGIN)`那里加个上报调用好了。 | |
``` | |
if (isSupportQlogin()) { | |
report('expose', 'qlogin'); | |
switchTo(QLOGIN); | |
} | |
``` | |
很简单嘛,普通登录也要。那就加加加。 | |
然而,这个功能是不是真的用户想要的,产品想验证下,用户有没有点头像去登录,又或者出现了快速登录页面用户会不会又切回普通登录去?这就是用户行为了,加click上报。 | |
也不难,绑定用户click事件,调用上报。 | |
``` | |
$('#switch_to_plogin').addEventListener('click', () => report('click', 'switch_to_plogin')); | |
$('#avatar_list li').forEach( | |
(node, which) => node.addEventListener('click', () => report('click', 'avatar', which)); | |
); | |
``` | |
于是乎,感觉我们可以这样子一直加下去。 | |
这样子的问题是什么呢? | |
1. 影响已有业务逻辑,增加代码错误概率 | |
2. 大量事件绑定,影响性能 | |
3. 动态生成的节点,要注意时机(头像为例) | |
3. 重复劳动 | |
这些问题能不能缓解或者解决?当然可以(这里可以提问) | |
1. 事件冒泡机制 | |
2. 自定义标签属性声明 | |
利用事件冒泡,对于单击事件,我们可以在 `document.body`上添加,根据`src.target`进行分别的上报,一方面抽离了上报代码,一方面对于动态添加的节点也省事了,以及只绑定一次。但是对于target进行进行区分又成了另一个问题, 于是引入自定义属性声明。页面上这样子写: | |
``` | |
<button id="switch_to_plogin" data-modid="switch_to_plogin">切换普通登录</button> | |
``` | |
但是对于页面曝光,好像就失效了: | |
1. 没有直接对应的事件 | |
2. 动态添加的节点 | |
3. 滚动之后才可见 | |
可以如此依次解决: | |
1. 页面onload事件作为曝光时机 | |
2. 延迟收集曝光元素 | |
3. 监听scroll事件,判断元素是否在可视区域内(getBoundingClientRect) | |
最后,还是会有些漏网之鱼,那确实只能根据实际情况手动调用上报。不过,如果是使用React/Vue这样一些生命周期更加具体的框架,那么我们可以通过编写中间件在`componentDidMount/mounted`等事件回调中上报 | |
到目前为止,我们只是实现了上报的埋点操作,往往还会需要一些额外的属性,比如说某个上报元素在页面上的相对位置。再拿快速登录的头像列表作为例子,万一有人好奇点击的头像是第几个。。。 | |
``` | |
for (let i=0; i<qloginList.length; i++) { | |
const avatar = qloginList[i]; | |
$('#avatar_list').appendChild( | |
<li data-modid="avatar" data-location={i}>{avatar.name}</li> | |
); | |
} | |
``` | |
当然,就这个例子我们可以这么做,然而我们仍然能通过计算得到这个位置信息。 | |
比如说快速登录头像列表最终结构是这样子: | |
``` | |
<section class="qlogin" id="qlogin" data-modid="qlogin"> | |
<ul class="avatar-list" id="avatar_list" data-modid="avatar-list"> | |
<li class="avatar" data-modid="avatar"></li> | |
<li class="avatar" data-modid="avatar"></li> | |
<li class="avatar" data-modid="avatar"></li> | |
</ul> | |
</section> | |
``` | |
分两步做: | |
1. 当前元素在兄弟节点中的位置 | |
2. 当前元素在树形结构中的位置 | |
这样子的计算对于js来说,都是很容易实现的(给个遍历示意图),最后可以得到这样一个位置信息:`qlogin_1>avatar-list_1>avatar_2` | |
至此,就实现了页面数据的采集。然后再上报出去,就可以了。 | |
而关于最后的上报操作,也可以有两点小技巧: | |
1. 合并上报,减少请求 | |
2. 延迟上报,减少对正常逻辑的影响 | |
### 真实案例 | |
逆袭时刻到了, ptlogin确实有这方面的数据。蓝色线是快速登录页面PV, 红色线则是点击切回普通登录的点击量. 至此, 我们可以确定地说,快速登录这个功能是非常受欢迎的. | |
![快速登录](https://se.logger.im/couchdb/images/workec.com/1493450845545.png) | |
### 可用选择 | |
解决数据采集上报之后,可以有多种选择去进行数据的存储和展示,比如ta.qq.com, 百度统计等。 | |
## 质量监控 | |
那页面出现错误怎么监控?首先要看是什么错误: | |
1. 资源加载失败(css/js/img) | |
2. cgi返回错误 | |
3. 脚本出现错误 | |
一个个来看,资源是如何引进的?html标签(link/script/img)。想当然地在标签上添加onerror,监听失败事件,当然存在兼容性问题(ie6~8),主要是css/js的加载判断,不过这方面其实问题没那么突出;对于cgi的访问,同域情况下,通过ajax请求,可以判断status和cgi协议提供的返回码,只要把这些请求统一调用,就可以统一上报错误情况了。 | |
**脚本错误**,才是前端同学最关注的问题。 | |
**错误信息的规范化处理** | |
我们可以使用`window.onerror`进行全局的监听,那我们能拿到什么样子的错误信息? | |
``` | |
/** | |
* @param {String} errorMessage 错误信息 | |
* @param {String} scriptURI 出错的文件 | |
* @param {Long} lineNumber 出错代码的行号 | |
* @param {Long} columnNumber 出错代码的列号 | |
* @param {Object} errorObj 错误的详细信息 | |
*/ | |
window.onerror = function(errorMessage, scriptURI, lineNumber,columnNumber,errorObj) { | |
// TODO | |
} | |
``` | |
前4个参数含义都很明确,最后一个参数,其实是个Error对象,它的属性message, fileName, lineNumber都跟前几个参数重复了,但是最重要的是stack参数,这个参数的内容是出错代码的调用堆栈。 | |
在我们的登录框例子中,我们构造一个错误: | |
``` | |
function tryErrorStack() { | |
var x = y.name; | |
} | |
$('#switch_to_plogin').onclick = function() { | |
tryErrorStack() | |
} | |
window.onerror = function(msg, url, line, col, error) { | |
console.log(error.stack); | |
} | |
``` | |
``` | |
ReferenceError: y is not defined | |
at tryErrorStack (login.html:44) | |
at HTMLButtonElement.$.onclick (login.html:48) | |
``` | |
可以看到浏览器chrome58打印出详情的调用信息,可以更好地帮助我们调试错误了。 | |
然而,这个stack属性并没有得到标准支持,所以它输出的内容因浏览器而异,比如说,相同代码firefox53输出: | |
``` | |
tryErrorStack@http://localhost:8000/login.html:44:7 | |
@http://localhost:8000/login.html:48:3 | |
``` | |
不过这个问题还好,因为还有更糟糕的,有的浏览器根本不支持的: | |
![enter image description here](https://se.logger.im/couchdb/images/workec.com/image.png) | |
还好,我们还有try-catch手动捕获可以达到polyfill效果, 但是IE9/8确实是信息有限了。 | |
try-catch的问题先放下,看看 | |
window.onerror的另一个问题:**Script Error.** | |
出现Script Error是页面加载的外联JS抛出异常的时候,抛出来的错误信息可能包含敏感信息,比如用户名,登录状态等,会造成信息泄露伤害用户,所以出于安全考虑,浏览器对这些信息进行了屏蔽,所以window.onerror触发的时候,只给出笼统的Script Error。不过这是跨域场景才会触发,也有相应的解决方案: | |
1. script标签增加crossorigin属性 | |
2. js文件响应头增加access-control-allow-orgin | |
crossorigin 属性有两个值 | |
1. anonymouse(默认) | |
不能带cookie | |
副作用:当Access-Control-Allow-Origin的值不是*或者不等于origin时,js拒绝加载 | |
2. use-credentials | |
可以带cookie(响应头需要 Access-Control-Allow-Credentials:true) | |
副作用:不支持*,Access-Control-Allow-Origin的值不等于origin时,js拒绝加载 | |
这个问题在现在的环境下还是容易出现的,毕竟我们都把静态资源部署在CDN上,而CDN通常也不需要cookie,所以目前课堂和辅导都是选择设置 `crossorigin=anonymouse` | |
然而,safari并不支持这个特性。(有待考证) | |
但是从另一方面讲 window.onerror采集的信息又太全面了,比如说现在浏览器各种插件,所以还需要进行适当的过滤,比如上报之前对出错域名进行判断。 | |
前面说到浏览器对于error.stack的兼容问题,我们可以用**try-catch**来进行polyfill,以及跨域脚本无法获得详情也需要用try-catch,那怎么做?如果每个函数都需要程序员重新进行try-catch的包装,未免过于麻烦。考虑到现在前端开发都使用模块化管理,我们可以对其模块在define/require的入口函数进行统一包裹,也有两种方法: | |
1. 运行时wrap | |
2. 构建时wrap | |
不过殊途同归,都是为了生成这样子的代码: | |
``` | |
var oldFn = fn; | |
fn = function() { | |
try { | |
oldFn.apply(this, arguments); | |
} catch(e) { | |
e.stack = e.stack || (e.message + ' ' + e.description); | |
report(e); | |
} | |
} | |
``` | |
这里可以看到需要对errostack进行兼容。 | |
依次类推,我们可以在需要的函数上进行try-catch包裹。 | |
百姓网有一个关于通过babel插件在函数级别上添加try-catch的分享: | |
[try-catch-wrapper](http://www.infoq.com/cn/presentations/javascript-exception-monitoring-for-dummies-in-browser-side) | |
[babel-try-catch](http://foio.github.io/babel-try-catch/) | |
但是, 不管是window.onerror,还是try-catch, 对于Promise里抛出的异常,都无法在top-level进行捕获,怎么办? | |
1. 添加promise.catch | |
2. unhandledrejection 和 rejectionhandled | |
3. async/await | |
chrome49 开始支持`unhandledrejection`事件,可以对promise未处理的reject或者异常进行全局的监听。 | |
所以,保底方案:**主动try-catch** | |
### 代码定位 | |
压缩混淆后的代码,就算拿到了相关调用堆栈,排查也不容易。别担心,可以用sourcemap进行还原,前提是生成了对应的sourcemap文件: | |
``` | |
var rawSourceMap = { | |
version: 3, | |
file: 'min.js', | |
names: ['bar', 'baz', 'n'], | |
sources: ['one.js', 'two.js'], | |
sourceRoot: 'http://example.com/www/js/', | |
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA' | |
}; | |
var smc = new SourceMapConsumer(rawSourceMap); | |
console.log(smc.sources); | |
// [ 'http://example.com/www/js/one.js', | |
// 'http://example.com/www/js/two.js' ] | |
console.log(smc.originalPositionFor({ | |
line: 2, | |
column: 28 | |
})); | |
// { source: 'http://example.com/www/js/two.js', | |
// line: 2, | |
// column: 10, | |
// name: 'n' } | |
``` | |
badjs 提供了一个巧妙的方案,通过把上报的错误堆栈打印到chrome控制台,输出来的带行号和列号的链接可以直接跳转到sources面板。 | |
![l输出演示](http://yorts52.github.io/sources/sourcemap/4.png) | |
![跳转演示 ](http://yorts52.github.io/sources/sourcemap/1.gif) | |
[原文链接](http://imweb.io/topic/565c49f23ad940357eb9986e) | |
## 可用选择 | |
1. badjs-report | |
2. stackstrace.js | |
3. raven.js | |
4. babel-try-catch-loader | |
## 错误率持续处理案例 | |
## 性能监控 | |
当我们说到页面性能的时候,是在讨论什么? | |
想想看,一个用户打开一个网页最直观的感受是什么? | |
**看到有内容展示。** | |
在这个过程中,我们可以把主页面加载分成三部分: | |
1. 网络时间 | |
2. 后端时间 | |
3. 前端时间 | |
结合美团前端的一张图片 | |
![enter image description here](http://tech.meituan.com/img/performance-framework-and-platform/navigation-timing.png) | |
网络时间包含了重定向,DNS解释和建立连接的时间 | |
后端时间包含了服务器处理时间和数据下载时间 | |
前端时间则是DOM的解析和渲染时间 | |
这些值都可以通过浏览器提供的Timing API得到(IE9+),这样子就得到了监控数据。 | |
而用户开始看到页面有内容并且页面可操作的时间点,就是在domComplete,称之为首屏加载。 | |
为了不影响首屏加载速度,我们往往会在首屏渲染之后再对DOM进行操作,这部分的监控浏览器API帮不上忙,仍然需要我们另加监控,称之为**主逻辑监控**: | |
``` | |
const base = +new Date(); | |
init(); // 主逻辑 | |
const initEnd = +new Date(); | |
report(initEnd - base); | |
``` | |
**功能监控** | |
仍然以前面的登录框为例,快速登录是个方便的功能,虽然首屏展示并不是必要的 , 但是仍然需要监控: | |
``` | |
const base = +new Date(); | |
... | |
function init() { | |
if (isSupportQlogin()) | |
switchTo(QLOGIN); | |
const switchEnd = +new Date(); | |
report(switchEnd - base); | |
} | |
``` | |
##可用选择 | |
1. apm | |
## 性能提升案例 | |
##结束 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment