背景
最近我老板让我每周四统计一下小程序的性能数据,汇总到表格中,通过飞书表格来维护各个团队的小程序性能指标数据,比如:启动时间、首次渲染耗时、页面白屏占比等等。
这个工作在一定程度上是有意义的,因为通过这些指标可以复盘一个月或者两个月来,前端对小程序方面的优化是否对性能有提升,结合飞书表格,可以呈现一个趋势图,非常直观。
但是,有一个低效的统计问题,我无法接受,就是每周四我需要登录微信公众号平台,打开性能菜单,找到每一个指标,手动复制到表格中,这个过程大概每次耗时5分钟左右,时间虽然也不长,但是周复一周,每年如此,还是会觉得挺乏味。
于是,我想了很多骚主意,比如:能不能调用微信开放接口?能不能写个chrome插件?能不能用自动化工具?我尝试了每一个方案,最终决定使用 puppeteer自动化工具来实现。
目标
统计小程序性能指标数据。
| 性能评估 | 总启动耗时 | 首次渲染耗时 | JS注入耗时 | 代码包下载耗时 | 5秒启动占比 | 页面切换耗时 | 切换较慢占比 | 页面进入白屏占比 |
|---|---|---|---|---|---|---|---|---|
| 良好 | 2000 | 100 | 20 | 300 | 95% | 270 | 16 | 1.51 |
注:以上数字均为随机填写,并不代表标准性能数据。
最终通过自动化方案,实现数据抓取,并返回:
抓取到数据后,绘制小程序性能趋势图:
方案
微信开放接口
在小程序开发文档找到了一个接口:获取小程序性能数据,这个接口的调用也非常简单,只有一个api。
调用方式:
POST https://api.weixin.qq.com/wxa/business/performance/boot?access_token=ACCESS_TOKEN
使用问题
使用以后发现几个问题:access_token 和 返回数据。
access_token为线上的token,前端一般不太容易拿到,前端不能自己去实现,因为整个小程序只有一个access_token,后端已经实现了,前端就不能再去生成,否则会引发线上问题。如果让后端写一个读取token接口给前端,可能会涉及到安全风险。- 微信性能接口返回的数据并不全,缺少一些指标,比如:页面白屏占比。
考虑到上面两个因素,决定暂缓使用API的方式。
chrome 插件
思路是,打开浏览器,登录公众号平台,借助chrome插件来实现对指标数据的获取。
实现思路:
- 创建一个文件夹,定义:
manifest.json文件。
- 关键属性:
manifest_version、background、content_scripts等。
{
"name": "性能抓取插件",
"version": "2.0.0",
"description": "抓取微信数据",
"manifest_version": 3,
"icons": {
"16": "images/icon.png",
"48": "images/icon.png",
"128": "images/icon.png"
},
"action": {
"default_icon": "images/icon.png",
"default_title": "汽销",
"default_popup": "index.html"
},
"background": {
"service_worker": "service-worker.js"
},
"content_scripts":[
{
"matches":["https://wedata.weixin.qq.com/*"],
"js":["content-script.js"],
"run_at":"document_end"
}
],
"permissions": [
"cookies",
"tabs",
"notifications",
"storage"
],
"host_permissions":[]
}
background是设置插件在后台运行,对于微信数据抓取,此处用不上。content_scripts用来匹配对应的网站,然后再run_at时机下,执行脚本。这个是高频功能。permissions用来设置插件用到的权限,比如:cookies、tabs、notifications等,此处也用不上。
- 定义
content-script.js文件
此文件会在微信的性能页面打开后,内容加载完成时执行,我们可以通过传统的DOM API来获取数据,比如:
- 按照这个方式,依次获取剩下的指标数据。
使用问题:
在使用的过程中,同样发现了一些问题,因为微信性能页面,都是后端渲染的,比如性能报告、启动性能、网络性能、运行性能和体验分析他们都是点击以后,才会渲染页面,那就意味着,chrome插件有获取难度。
- 人为点击后,才会渲染页面,所以点击之前,
content-script.js无法获取内容。 - 微信数据接口都是加密返回,即使在插件里面调用接口,注入
cookie,也无法拿到明文数据。
总结:考虑上面问题后,此方案也暂缓使用。
使用 puppeteer 自动化插件
为什么会想到这个方案呢?人工获取数据,需要先登录,依次点击各个Tab页面,然后复制数据,这个过程刚好是puppeteer最擅长的,所以技术吻合,省时省力。
实现方案:
创建一个nestjs项目,安装puppeteer插件,除了扫码以外,其它都是自动化实现:自动点击菜单、自动抓取数据、自动截图、自动点击按钮、自动切换Tab、自动打开新窗口等。
- 安装
nestjs脚手架,并创建项目。
npm i -g @nestjs/cli
$ nest new puppeteer
如果对于nestjs 不了解,可以去官网看一下文档,写几个
Demo就能入门,实现这个插件实际上并不需要很了解nestjs,只是用它当做node服务而已,你也可以用Express或者koa去做。
- 安装
puppeteer插件。
pnpm add puppeteer
- 打开
app.controller.ts文件,创建有一个有头浏览器,并启动。
@Get()
async getPerformanceData() {
// 启动浏览器
const browser = await puppeteer.launch({
headless: false,
});
}
@Get() 是一个注解函数,表示这个方法是get请求。
headless设置为false表示启动的浏览器是能打开,看见的。如果是无头,则不会打开浏览器。
- 打开
chrome后,新建一个页面,跳转到微信公众号平台。
// 新建一个页面
const page = await browser.newPage();
// 打开公众号平台
await page.goto('https://mp.weixin.qq.com/', {
waitUntil: 'domcontentloaded',
});
注:每一步的代码,都是接着上一步的代码编写的。
- 设置窗口大小
// 设置窗口大小
await page.setViewport({ width: 1400, height: 1200 });
- 等待用户扫码
// 等待二维码加载
await page.waitForFunction(() => {
return document.querySelectorAll('.data-overview__title').length > 0;
});
这里要注意,扫码只能用户来做,插件没办法实现扫码功能,而且微信公众平台,也无法用账号密码的方式来登录,大家可以去测试。
通过waitForFunction函数,可以实现异步等待,当用户扫码成功后,会进入到首页,我们只需要判断首页某一个节点存在就相当于扫码成功。
- 进入首页后,点击左侧性能统计菜单
// 查找性能统计按钮,并点击,打开性能统计页面
await page.waitForSelector('dt[class="menu_title clickable menu_data"]>a');
await page.click('dt[class="menu_title clickable menu_data"]>a');
- 异步打开页面,并设置窗口。
const newPagePromise: Promise<Page> = new Promise((resolve) =>
browser.on('targetcreated', (target) => resolve(target.page())),
);
// 获取新窗口page对象
const newPage = await newPagePromise;
// 设置窗口大小
await newPage.setViewport({ width: 1400, height: 2100 });
当模拟点击【统计】菜单时,微信会另开一个页面,上面代码中的page对象指的是第一个窗口,如果微信另开一个窗口,我们是无法使用的,所以必须一步接收一个新的窗口页面,当做上下文对象。
- 等待页面加载
// 在这里等待5秒
await newPage.waitForSelector('div[id="outer_view"]', {
timeout: 5000,
});
平台数据这个页面数据比较多,加载时间比较久,我们多等待一下。
- 依次点击性能质量、点击性能数据 获取首页性能报告。
// 点击性能质量菜单
await newPage.locator('div ::-p-text(性能质量)').click();
// 展示子菜单
await newPage.locator('div ::-p-text(性能数据)').click();
await newPage.waitForSelector('.performance_evaluate');
await new Promise((resolve) => setTimeout(resolve, 3000));
// 屏幕截图
await newPage.screenshot({
path: resolve(__dirname, `./首页性能报告.png`),
type: 'png',
fullPage: true,
});
点击性能质量菜单,会展开性能数据菜单,再次点击,才会加载首页性能数据。通过screenshot可以对首页进行截图,中间有一个延迟3秒,也是为了等待页面加载完成,否则截图不全。
- 获取首页的相关指标数据
// 使用page.evaluate来在浏览器上下文中执行代码,并获取元素的文本值
const data = await newPage.evaluate(() => {
const obj = {};
// 注意:这里使用的是浏览器的DOM API,而不是Puppeteer的API
const nodes = document.querySelector(
'.weui-desktop-popover__desc .row',
).childNodes;
obj['性能评估'] = nodes[1].textContent.trim();
obj['启动耗时'] = nodes[3].textContent.trim();
obj['首次渲染耗时'] = nodes[4].textContent.trim();
obj['JS注入耗时'] = nodes[5].textContent.trim();
obj['代码包下载耗时'] = nodes[6].textContent.trim();
return obj;
});
首页是包含各种指标数据的,我们通过变量保存并返回。
- 点击运行性能按钮,获取数据并截图
// 点击运行性能
await newPage
.locator('.board-nav-container__nav ::-p-text(运行性能)')
.click();
// 在这里等待2秒
await new Promise((resolve) => setTimeout(resolve, 3000));
await newPage.waitForSelector('span[data-short=切换耗时分析]');
// 设置窗口大小
await newPage.setViewport({ width: 1400, height: 5100 });
// 屏幕截图
await newPage.screenshot({
path: resolve(__dirname, `./运行性能.png`),
type: 'png',
fullPage: true,
});
// 使用page.evaluate来在浏览器上下文中执行代码,并获取元素的文本值
const switchData = await newPage.evaluate(() => {
// 注意:这里使用的是浏览器的DOM API,而不是Puppeteer的API
const nodes = document.querySelectorAll('.num_1t7ooPGwJu');
return [nodes[0].textContent.trim(), nodes[2].textContent.trim()];
});
data['页面切换耗时'] = switchData[0];
data['切换较慢占比'] = switchData[1];
- 点击体验分析按钮,获取数据并截图
// 点击体验分析
await newPage
.locator('.board-nav-container__nav ::-p-text(体验分析)')
.click();
// 在这里等待2秒
await new Promise((resolve) => setTimeout(resolve, 3000));
// 使用page.evaluate来在浏览器上下文中执行代码,并获取元素的文本值
const whiteData = await newPage.evaluate(() => {
// 注意:这里使用的是浏览器的DOM API,而不是Puppeteer的API
const nodes = document.querySelectorAll('.num_1t7ooPGwJu');
return nodes[1].textContent.trim();
});
data['页面进入白屏占比'] = whiteData;
// 设置窗口大小
await newPage.setViewport({ width: 1400, height: 2400 });
// 屏幕截图
await newPage.screenshot({
path: resolve(__dirname, `./体验分析.png`),
type: 'png',
fullPage: true,
});
- 点击启动性能并截图
// 点击启动性能
await newPage
.locator('.board-nav-container__nav ::-p-text(启动性能)')
.click();
// 在这里等待2秒
await new Promise((resolve) => setTimeout(resolve, 5000));
// 定义图片路径
const path = resolve(__dirname, `./启动性能.png`);
await newPage.setViewport({
width: 1400,
height: 8300,
});
// 屏幕截图
await newPage.screenshot({
path,
type: 'png',
fullPage: true,
});
- 关闭浏览器,接口返回数据。
// 关闭浏览器
await browser.close();
return data;
通过上面一系列的操作,插件就自动完成了所有数据的抓取,并通过接口返回,我们只需要拷贝数据到表格即可,当然也可以进一步对接飞书表格,通过API完成读写,此处不再扩展了。
总结
其实这个工作量并不大,每周也只是耽误5分钟,但是作为程序员,如果有更快的方式,岂不美哉。我们通过puppeteer插件结合nestjs,除了启动后的扫码以外,剩下的全程自动化实现。
既完成了工作,又学习了新技能。
感谢大家观看,我是河畔一角,一名普通的前端工程师。