Skip to content

Instantly share code, notes, and snippets.

@kinka
Created April 30, 2017 08:34
Show Gist options
  • Save kinka/ea3ad293bf7e850ddc984fe9d8db233e to your computer and use it in GitHub Desktop.
Save kinka/ea3ad293bf7e850ddc984fe9d8db233e to your computer and use it in GitHub Desktop.
workec.com
##开场
自我介绍, 来自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