3.1.2 插件核心实现代码
-
登录态,token同步; -
popup面板监测到未登录,会显示一个未登录按钮;点击该按钮打开阿里云控制台官网,用户登录完后会通过注入的脚本拿到token;
-
版本升级检测; -
配置中心,轮询版本信息通过版本算法进行对比;
setInterval(() => {
try {
const manifest = chrome.extension.getURL('manifest.json');
Promise.all([
fetch(manifest),
fetch('https://xx.com/pts-record-chrome-plugin/package.json'),
])
.then(res => {
return [ res[0].json(), res[1].json() ];
})
.then(result => {
return Promise.all([ result[0], result[1] ]);
})
.then(res => {
return [ res[0].version, res[1].version ];
})
.then(versions => {
const [ currentVersion, latestVersion ] = versions;
if (compareVersion(latestVersion, currentVersion) > 0) {
storage.set('hasNew', latestVersion);
} else {
storage.set('hasNew', false);
}
})
.catch(() => {
storage.set('hasNew', false);
});
} catch (error) {
storage.set('hasNew', false);
}
}, 1000 * 60);
-
流量录制
-
不同请求类型的可选录制,如GET/POST,filter一下 -
请求头等信息的录制,chrome.webRequest
// 不同的hook获取不同的信息,通过requestId关联,从而拼接出完整的请求&响应
function openListener() {
chrome.webRequest.onBeforeRequest.addListener(
handleBeforeRequest,
// filters
{
urls: [ '<all_urls>' ],
},
// extraInfoSpec
[ 'requestBody', 'blocking', 'extraHeaders' ],
);
chrome.webRequest.onBeforeSendHeaders.addListener(
handleBeforeSendHeaders,
// filters
{
urls: [ '<all_urls>' ],
},
// extraInfoSpec
[ 'requestHeaders', 'blocking', 'extraHeaders' ],
);
chrome.webRequest.onBeforeRedirect.addListener(
handleOnBeforeRedirect,
{
urls: [ '<all_urls>' ],
},
);
chrome.webRequest.onCompleted.addListener(
handleOnCompleted,
{
urls: [ '<all_urls>' ],
},
[ 'responseHeaders', 'extraHeaders' ],
);
chrome.runtime.onMessage.addListener(handleRuntimeMessage);
return true;
}
-
响应体的录制,劫持fetch和xhr
// 劫持xhr
const send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
this.addEventListener(
'readystatechange',
function() {
if (this.readyState === 4) {
const self = this;
window.top.postMessage(
{
direction: 'from-page-monkey-patch-script',
response: 'response',
value: {
body: handleResponse(self),
responseUrl: self.responseURL,
},
},
'*',
);
}
},
false,
);
send.apply(this, arguments as any);
};
const open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
this.addEventListener(
'readystatechange',
function() {
if (this.readyState === 4) {
const self = this;
window.top.postMessage(
{
direction: 'from-page-monkey-patch-script',
response: 'request',
value: {
body: handleResponse(self),
responseUrl: self.responseURL,
},
},
'*',
);
}
},
false,
);
open.apply(this, arguments as any);
};
// 劫持fetch
const constantMock = window.fetch;
window.fetch = function() {
return new Promise((resolve, reject) => {
constantMock
.apply(this, arguments as any)
.then(response => {
const responseForText = response.clone();
responseForText.text().then(text => {
window.top.postMessage(
{
direction: 'from-page-monkey-patch-script',
response: 'fetch',
value: {
body: text,
responseUrl: response.url,
},
},
'*',
);
resolve(response);
});
})
.catch(response => {
reject(response);
});
});
};
3.2 行为分析插件
3.2.1 插件业务背景
-
采集用户操作过程中的页面访问/点击等行为数据,使用数据分析手段对数据进行分析帮助产品演进;
3.2.1 插件核心实现代码
-
用户身份信息同步; -
通过接收站点的消息实现身份信息同步; -
因为只需要发送埋点时明确当前用户身份,不需要cookie,所以简单点;
import { MessageTarget, MessageType } from '../../contstants/message';
export const ticketEventhandler = (e) => {
if (
e.target === MessageTarget.IntelligentAssistantBackgroundScript &&
e.origin === MessageTarget.IntelligentAssistantContentScript
) {
if (e.type === MessageType.UidEvent) {
// 如果uid存在,设置aesConfig
if (e.data?.uid !== (window as any)?.aes?.getConfig('uid')) {
(window as any)?.aes?.setConfig?.({
uid: e.data?.uid,
});
}
}
}
};
-
版本升级检测
-
由于保证用户使用最新即可,直接判定和远程是否同一个版本,不是就要升级
const useVersions = () => {
const [versions, setVersions] = useState<{
currentVersion?: string;
newVersion?: string;
hasNew?: boolean;
}>({});
useEffect(() => {
chrome.storage.local.get(['currentVersion'], ({ currentVersion }) => {
updateConfigInfo().then((res) => {
setVersions({
currentVersion,
hasNew: res?.version !== currentVersion,
});
});
});
}, []);
return versions;
};
-
用户行为埋点上报; -
由于chrome插件后台window对象存在,所以aplus的umd可以直接用;aes-tracker需要改造适配; -
先初始化aplus,将aplus umd直接下载到本地,然后入口文件处import即可;
-
然后初始化aes,aes初始化要放在第二步,因为后续行为上报都需要依赖aes;
// index.ts
import { initAES } from './aes/index';
initAes();
// aes/index.ts
// 改造了一下,适配chrome插件下origin_url和title的取值逻辑
import AES from '@alife/aes-tracker-chrome-plugin';
import AESPluginEvent from '@ali/aes-tracker-plugin-event';
// 改造了一下,适配chrome插件下origin_url和title的取值逻辑
import AESPluginPV from '@ali/aes-tracker-plugin-pv-chrome-plugin';
import AESPluginAutolog from '@ali/aes-tracker-plugin-autolog';
export const initAES = () => {
// 初始化SDK
const aes = new AES({
pid: 'xx',
user_type: 'xx',
app_name: 'xx',
// 版本号默认值,实际代码中会通过manifest.json设置
app_version: 'xx',
requiredFields: ["uid"],
maxUrlLength: 20000
});
(window as any).aes = aes;
(window as any).AESPluginEvent = aes.use(AESPluginEvent);
(window as any).AESPluginPV = aes.use(AESPluginPV, {
autoPV: false,
autoLeave: false
});
(window as any).AESPluginAutolog = aes.use(AESPluginAutolog);
};
-
然后初始化版本信息,注册定时器和监听器,同时注册过程要函数化,因为每次更新配置信息都会经历一次清除再注册的过程,达到可以动态配置定时器执行时间间隔的效果; -
其中比较关键的是用户页面访问行为采集方案,需要推敲一下,考虑好各种边界; -
点击/曝光事件的采集直接用aes自带的就好;
-
远程配置中心; -
存在如下几个需要远程配置的信息; -
页面访问上报过程中,我们要动态控制上报哪些站点的访问信息(最小化插件权限);
-
-
定时上报过程中,我们要动态控制定时上报的时间间隔; -
针对插件popup实时性能分析能力,我们需要根据实际情况灵活调整采集频率 / 最大存储数据量; -
以及还需要根据实际情况调整插件配置信息更新时间;
-
-
结合远程配置诉求和用户可以手动关闭采集过程的诉求,针对入口文件代码进行了改造; -
核心是 启动逻辑收敛到start函数中,监听器清除逻辑收敛到close函数中(避免内存泄露);
-
-
针对开启/关闭行为分析操作,重复一下 close => start 的流程; -
针对远程配置信息采用监听方案,特定配置信息变化后才会更新特定监听器,减少不必要性能开销;
-
// 必须最先初始化aplus umd,aes依赖于aplus
import './aplus/aplus_mini';
import { initAES } from './aes/index';
import { initVersionByManifest } from './timer/config';
import { autologEventhandler } from './listener/autolog';
import { ticketEventhandler } from './listener/ticket';
import {
tabsUpdatedEventhandler,
tabsActivatedEventHandler,
tabsRemovedEventHandler,
windowsFocusedEventHandler,
windowsRemovedEventHandler,
} from './listener/behavior';
import { collectNetworkInfo } from './timer/network';
import { collectDeviceInfo } from './timer/device';
import { updateConfigInfo } from './timer/config';
import { collectPagesCount } from './timer/pagesCount';
import type { IConfigInfo } from './timer/config';
// 必须第二初始化 aes,接下来的数据采集都依赖于 aes
initAES();
// 利用manifest初始化版本信息
initVersionByManifest();
// 初始化默认打开开关
chrome.storage.local.set({
isOn: true,
});
const closeListener = () => {
// 移除ticket相关事件监听器
chrome.runtime.onMessage.removeListener(ticketEventhandler);
// 移除曝光事件监听器
chrome.runtime.onMessage.removeListener(autologEventhandler);
// 移除tab激活事件监听器
chrome.tabs.onActivated.removeListener(tabsActivatedEventHandler);
// 移除tab更新事件监听器
chrome.tabs.onUpdated.removeListener(tabsUpdatedEventhandler);
// 移除tab关闭事件监听器
chrome.tabs.onRemoved.removeListener(tabsRemovedEventHandler);
// 移除window获得焦点事件监听器
chrome.windows.onFocusChanged.removeListener(windowsFocusedEventHandler);
// 移除window关闭事件监听器
chrome.windows.onRemoved.removeListener(windowsRemovedEventHandler);
};
const startListener = () => {
closeListener();
// 优先监听ticket页面事件
if (!chrome.runtime.onMessage.hasListener(ticketEventhandler)) {
chrome.runtime.onMessage.addListener(ticketEventhandler);
}
// 初始化点击曝光事件监听器
if (!chrome.runtime.onMessage.hasListener(autologEventhandler)) {
chrome.runtime.onMessage.addListener(autologEventhandler);
}
// 初始化行为监听器
// 监听tab激活事件
if (!chrome.tabs.onActivated.hasListener(tabsActivatedEventHandler)) {
chrome.tabs.onActivated.addListener(tabsActivatedEventHandler);
}
// 监听tab更新事件
if (!chrome.tabs.onUpdated.hasListener(tabsUpdatedEventhandler)) {
chrome.tabs.onUpdated.addListener(tabsUpdatedEventhandler);
}
// 监听tab关闭事件
if (!chrome.tabs.onRemoved.hasListener(tabsRemovedEventHandler)) {
chrome.tabs.onRemoved.addListener(tabsRemovedEventHandler);
}
// 监听window获得焦点事件
if (!chrome.windows.onFocusChanged.hasListener(windowsFocusedEventHandler)) {
chrome.windows.onFocusChanged.addListener(windowsFocusedEventHandler);
}
// 监听window关闭
if (!chrome.windows.onRemoved.hasListener(windowsRemovedEventHandler)) {
chrome.windows.onRemoved.addListener(windowsRemovedEventHandler);
}
};
let collectDeviceInfoIntervalId: NodeJS.Timeout | undefined;
let collectNetworkInfoIntervalId: NodeJS.Timeout | undefined;
let updateConfigInfoIntervalId: NodeJS.Timeout | undefined;
let collectPagesCountIntervalId: NodeJS.Timeout | undefined;
const closeTimer = () => {
clearInterval(collectDeviceInfoIntervalId);
collectDeviceInfoIntervalId = undefined;
clearInterval(collectNetworkInfoIntervalId);
collectNetworkInfoIntervalId = undefined;
clearInterval(updateConfigInfoIntervalId);
updateConfigInfoIntervalId = undefined;
clearInterval(collectPagesCountIntervalId);
collectPagesCountIntervalId = undefined;
};
const startTimer = () => {
closeTimer();
updateConfigInfo()
.then((configInfo: IConfigInfo | void) => {
const {
collectDeviceInfoInterval = 10000,
collectNetworkInfoInterval = 10000,
collectPagesCountInterval = 30000,
updateConfigInfoInterval = 600000,
} = configInfo || {};
// 初始化设备性能信息定时器
if (!collectDeviceInfoIntervalId) {
collectDeviceInfoIntervalId = setInterval(
() => {
collectDeviceInfo();
},
collectDeviceInfoInterval < 1000 ? 1000 : collectDeviceInfoInterval,
);
}
// 初始化网络延迟信息定时器
if (!collectNetworkInfoIntervalId) {
collectNetworkInfoIntervalId = setInterval(
() => {
collectNetworkInfo();
},
collectNetworkInfoInterval < 1000 ? 1000 : collectNetworkInfoInterval,
);
}
// 初始化页面数量信息定时器
if (!collectPagesCountIntervalId) {
collectPagesCountIntervalId = setInterval(
() => {
collectPagesCount();
},
collectPagesCountInterval < 5000 ? 5000 : collectPagesCountInterval,
);
}
// 初始化配置信息定时更新器,同时重新设置timer
if (!updateConfigInfoIntervalId) {
updateConfigInfoIntervalId = setInterval(
() => {
startTimer();
},
updateConfigInfoInterval < 60000 ? 60000 : updateConfigInfoInterval,
);
}
})
.catch((error) => {
// 处理错误
console.error('Error:', error);
});
};
const start = () => {
// 插件刚安装等时机,先读取一次远程配置,再初始化定时监听器
startTimer();
startListener();
};
const close = () => {
closeTimer();
closeListener();
};
// start之前会先close,来保证重复启动下不会有问题
start();
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'local' && changes?.isOn) {
if (changes?.isOn?.newValue) {
// popup页面访问行为分析开关
start();
} else {
// popup页面访问行为分析开关
close();
}
}
});
3.3 XSwitch代理插件
3.3.1 插件业务背景
-
前端开发代理资源到本地使用
3.3.2 插件核心实现代码
chrome.webRequest.onBeforeRequest.addListener(function (details) {
if (forward[constants["s" /* DISABLED */]] !== enums["b" /* Enabled */].NO) {
if (clearCacheEnabled) {
clearCache();
}
return forward.onBeforeRequestCallback(details);
}
return {};
}, {
urls: [constants["c" /* ALL_URLS */]]
}, [constants["e" /* BLOCKING */]]); // Breaking the CORS Limitation
chrome.webRequest.onHeadersReceived.addListener(headersReceivedListener, {
urls: [constants["c" /* ALL_URLS */]]
}, [constants["e" /* BLOCKING */], constants["O" /* RESPONSE_HEADERS */]]);
chrome.webRequest.onBeforeSendHeaders.addListener(function (details) {
return forward.onBeforeSendHeadersCallback(details);
}, {
urls: [constants["c" /* ALL_URLS */]]
}, [constants["e" /* BLOCKING */], constants["N" /* REQUEST_HEADERS */]]);
最后,再从业务视角、效能视角以及个人开发者视角几个角度的可落地方向进行一下总结。回答一下我们可以从哪些角度入手自己开发一款可以落地实用的浏览器插件这个问题。
业务视角
-
作为产品能力的一环,流量录制器 / 操作录制器; -
作为产品能力的增强,aem热力分析图插件 /email; -
作为产品的一种载体,sider ai/monica/im;
效能视角
-
降低研发/问题排查成本,react developer tools/formily devtools/xswitch等; -
网络孤岛环境错误信息收集;
个人开发者视角
-
换肤 -
网站聚合 / top访问 -
网页翻译 -
等其他工具
原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/81905.html