一文摸清前端监控自研实践(二)行为监控
前言
上篇文章我们分享了关于 页面性能监控
的内容,本文我们接着来看 用户行为监控
的方面
系列文章传送门
用户的行为特征
为什么要做用户的行为情况监控?其实也就是问:采集了用户的行为信息后我们能做什么,答案其实很简单:
- PV、UV量,日同比、周同比等。能清晰的明白流量变化。
- 用户热点页面、高访问量TOP10
- 设备、浏览器语言、浏览器、活跃时间段等的用户特征
- 用户的行为追踪:某个用户,进入了网站后的一系列操作或者跳转行为;
- 用户自定义埋点上报用户行为:想做一些自定义事件的监听,比如播放某个视频的行为动作。
- 多语种站点,每个语种的用户量
整体封装
跟上文一样,这边先附上整体的一个初始化封装;
数据暂存
暂存在 store 里的数据跟 文一:性能监控
基本类似,这里就附一下用户行为所多的参数
export enum metricsName {
PI = 'page-information',
OI = 'origin-information',
RCR = 'router-change-record',
CBR = 'click-behavior-record',
CDR = 'custom-define-record',
HT = 'http-record',
}
整体初始化
export default class UserVitals {
private engineInstance: EngineInstance;
// 本地暂存数据在 Map 里 (也可以自己用对象来存储)
public metrics: UserMetricsStore;
public breadcrumbs: BehaviorStore;
public customHandler: Function;
// 最大行为追踪记录数
public maxBehaviorRecords: number;
// 允许捕获click事件的DOM标签 eg:button div img canvas
clickMountList: Array<string>;
constructor(engineInstance: EngineInstance) {
this.engineInstance = engineInstance;
this.metrics = new UserMetricsStore();
// 限制最大行为追踪记录数为 100,真实场景下需要外部传入自定义;
this.maxBehaviorRecords = 100;
// 初始化行为追踪记录
this.breadcrumbs = new BehaviorStore({ maxBehaviorRecords: this.maxBehaviorRecords });
// 初始化 用户自定义 事件捕获
this.customHandler = this.initCustomerHandler();
// 作为 真实sdk 的时候,需要在初始化时传入与默认值合并;
this.clickMountList = ['button'].map((x) => x.toLowerCase());
// 重写事件
wrHistory();
// 初始化页面基本信息
this.initPageInfo();
// 初始化路由跳转获取
this.initRouteChange();
// 初始化用户来路信息获取
this.initOriginInfo();
// 初始化 PV 的获取;
this.initPV();
// 初始化 click 事件捕获
this.initClickHandler(this.clickMountList);
// 初始化 Http 请求事件捕获
this.initHttpHandler();
// 上报策略在后几篇细说
}
// 封装用户行为的上报入口
userSendHandler = (data: IMetrics) => {
// 进行通知内核实例进行上报;
};
// 补齐 pathname 和 timestamp 参数
getExtends = (): { page: string; timestamp: number | string } => {
return {
page: this.getPageInfo().pathname,
timestamp: new Date().getTime(),
};
};
// 初始化用户自定义埋点数据的获取上报
initCustomerHandler = (): Function => {
//... 详情代码在下文
};
// 初始化 PI 页面基本信息的获取以及返回
initPageInfo = (): void => {
//... 详情代码在下文
};
// 初始化 RCR 路由跳转的获取以及返回
initRouteChange = (): void => {
//... 详情代码在下文
};
// 初始化 PV 的获取以及返回
initPV = (): void => {
//... 详情代码在下文
};
// 初始化 OI 用户来路的获取以及返回
initOriginInfo = (): void => {
//... 详情代码在下文
};
// 初始化 CBR 点击事件的获取和返回
initClickHandler = (mountList: Array<string>): void => {
//... 详情代码在下文
};
// 初始化 http 请求的数据获取和上报
initHttpHandler = (): void => {
//... 详情代码在下文
};
}
用户的基本信息
获取用户一些基本的信息;包括:当前访问的网页路径
、浏览器语种
、屏幕大小
、等等
export interface PageInformation {
host: string;
hostname: string;
href: string;
protocol: string;
origin: string;
port: string;
pathname: string;
search: string;
hash: string;
// 网页标题
title: string;
// 浏览器的语种 (eg:zh) ; 这里截取前两位,有需要也可以不截取
language: string;
// 用户 userAgent 信息
userAgent?: string;
// 屏幕宽高 (eg:1920x1080) 屏幕宽高意为整个显示屏的宽高
winScreen: string;
// 文档宽高 (eg:1388x937) 文档宽高意为当前页面显示的实际宽高(有的同学喜欢半屏显示)
docScreen: string;
}
// 获取 PI 页面基本信息
getPageInfo = (): PageInformation => {
const { host, hostname, href, protocol, origin, port, pathname, search, hash } = window.location;
const { width, height } = window.screen;
const { language, userAgent } = navigator;
return {
host,
hostname,
href,
protocol,
origin,
port,
pathname,
search,
hash,
title: document.title,
language: language.substr(0, 2),
userAgent,
winScreen: `${width}x${height}`,
docScreen: `${document.documentElement.clientWidth || document.body.clientWidth}x${
document.documentElement.clientHeight || document.body.clientHeight
}`,
};
};
// 初始化 PI 页面基本信息的获取以及返回
initPageInfo = (): void => {
const info: PageInformation = this.getPageInfo();
const metrics = info as IMetrics;
this.metrics.set(metricsName.PI, metrics);
};
用户行为记录栈
有时候,我们需要去获取用户的一个行为追踪记录
(比如说:出现了一个线上异常,我们要追溯异常如何发生
),这虽然说可能算是错误监控里面的内容,不过数据捕获部分我们就放在这章 行为监控
里讲,也就是说,用户自从打开我们的网站后,看了什么,点击了什么
一般来说,我们所谈到的用户行为记录栈,需要追踪的事件包括以下
:
- 路由跳转行为
- 点击行为
- ajax 请求行为
- 用户自定义事件
捕获上面的四个行为,只需要在上述四个事件的代码中做数据捕获就可以了,我们放下下面的叙述中细细说明,这里我就贴一下封装 用户行为记录栈
的代码:
export interface behaviorRecordsOptions {
maxBehaviorRecords: number;
}
export interface behaviorStack {
name: metricsName;
page: string;
timestamp: number | string;
value: Object;
}
// 暂存用户的行为记录追踪
export default class behaviorStore {
// 数组形式的 stack
private state: Array<behaviorStack>;
// 记录的最大数量
private maxBehaviorRecords: number;
// 外部传入 options 初始化,
constructor(options: behaviorRecordsOptions) {
const { maxBehaviorRecords } = options;
this.maxBehaviorRecords = maxBehaviorRecords;
this.state = [];
}
// 从底部插入一个元素,且不超过 maxBehaviorRecords 限制数量
push(value: behaviorStack) {
if (this.length() === this.maxBehaviorRecords) {
this.shift();
}
this.state.push(value);
}
// 从顶部删除一个元素,返回删除的元素
shift() {
return this.state.shift();
}
length() {
return this.state.length;
}
get() {
return this.state;
}
clear() {
this.state = [];
}
}
路由跳转
一般的路由跳转行为,都是针对于 SPA单页应用
的,因为对于非单页应用来说,url
跳转都以页面刷新的形式;
Hash 路由
hash
路由的监听比较简单,大家都知道可以用hashchange
来监听,
但是 hash
变化除了触发 hashchange
,也会触发 popstate
事件,而且会先触发 popstate
事件,我们可以统一监听 popstate
History 路由
接着往下阅读之前,我们先来了解一下,html5
的 History API
,它所支持的 API
有以下五个
history.back()
history.go()
history.forward()
history.pushState()
history.replaceState()
同时在 History API
中还有一个 事件
,该事件为 popstate
;它有着以下特点;
History.back()
、History.forward()
、History.go()
在被调用时,会触发popstate事件
- 但是
History.pushState()
和History.replaceState()
不会触发popstate事件
。
所以我们需要对 replaceState
和 pushState
,去创建新的全局Event事件。然后 window.addEventListener
监听我们加的 Event
即可
封装
- 简单封装一下 以适合上文的
整体封装
// 派发出新的 Event
const wr = (type: keyof History) => {
const orig = history[type];
return function (this: unknown) {
const rv = orig.apply(this, arguments);
const e = new Event(type);
window.dispatchEvent(e);
return rv;
};
};
// 添加 pushState replaceState 事件
export const wrHistory = (): void => {
history.pushState = wr('pushState');
history.replaceState = wr('replaceState');
};
// 为 pushState 以及 replaceState 方法添加 Event 事件
export const proxyHistory = (handler: Function): void => {
// 添加对 replaceState 的监听
window.addEventListener('replaceState', (e) => handler(e), true);
// 添加对 pushState 的监听
window.addEventListener('pushState', (e) => handler(e), true);
};
export const proxyHash = (handler: Function): void => {
// 添加对 hashchange 的监听
// hash 变化除了触发 hashchange ,也会触发 popstate 事件,而且会先触发 popstate 事件,我们可以统一监听 popstate
// 这里可以考虑是否需要监听 hashchange,或者只监听 hashchange
window.addEventListener('hashchange', (e) => handler(e), true);
// 添加对 popstate 的监听
// 浏览器回退、前进行为触发的 可以自己判断是否要添加监听
window.addEventListener('popstate', (e) => handler(e), true);
};
// 初始化 RCR 路由跳转的获取以及返回
initRouteChange = (): void => {
const handler = (e: Event) => {
// 正常记录
const metrics = {
// 跳转的方法 eg:replaceState
jumpType: e.type,
// 创建时间
timestamp: new Date().getTime(),
// 页面信息
pageInfo: this.getPageInfo(),
} as IMetrics;
// 一般路由跳转的信息不会进行上报,根据业务形态决定;
this.metrics.add(metricsName.RCR, metrics);
// 行为记录 不需要携带 pageInfo
delete metrics.pageInfo;
// 记录到行为记录追踪
const behavior = {
category: metricsName.RCR,
data: metrics,
...this.getExtends(),
} as behaviorStack;
this.breadcrumbs.push(behavior);
};
proxyHash(handler);
// 为 pushState 以及 replaceState 方法添加 Evetn 事件
proxyHistory(handler);
};
PV、UV
首先我们来了解一下 PV 和 UV 的定义:
PV
是页面访问量UV
是24小时内(00:00-24:00
)访问的独立用户数。
那么了解了 PV 和 UV 各是什么之后,我们应该如何做才能采集这两个数据指标呢?其实很简单
PV
只需要在用户每次进入页面的时候
,进行上报即可,这里需要注意SPA单页面
应用的PV上报需要结合上面的路由跳转进行;UV
我们就会转为使用服务端进行采集
,当服务端判断到上报的PV所属的IP
结合登录信息或者用户标志
,是当天的第一次上报时,就给它记录一次UV
那么按照这个逻辑,我们的 PV上报代码
可以像这样写,简单封装一下;
// 初始化 PV 的获取以及返回
initPV = (): void => {
const handler = () => {
const metrics = {
// 还有一些标识用户身份的信息,由项目使用方传入,任意拓展 eg:userId
// 创建时间
timestamp: new Date().getTime(),
// 页面信息
pageInfo: this.getPageInfo(),
// 用户来路
originInformation: getOriginInfo(),
} as IMetrics;
this.userSendHandler(metrics);
// 一般来说, PV 可以立即上报
};
afterLoad(() => {
handler();
});
proxyHash(handler);
// 为 pushState 以及 replaceState 方法添加 Evetn 事件
proxyHistory(handler);
};
点击事件
有时,我们获取用户的点击情况是非常有价值的,比如说有以下场景
:
- 网站的首页有三个推广广告,那么哪一个广告更能够吸引用户的点击?
- 放在网页上的视频是否有人进行播放?播放量为多少?
- ......等等等等
简而言之,我们如果能够捕获到用户的点击行为,是能够得到一些非常具有价值的指标数据的,当然我们也不是要获取用户的所有点击
,里面会包含很多的无意义点击行为,我们需要获取的是具有一些指标意义的点击行为,这就需要一定的过滤
,过滤可以根据标签、id、class等等进行过滤:
- 简单封装一下 以适合上文的
整体封装
// 初始化 CBR 点击事件的获取和返回
initClickHandler = (mountList: Array<string>): void => {
const handler = (e: MouseEvent | any) => {
// 这里是根据 tagName 进行是否需要捕获事件的依据,可以根据自己的需要,额外判断id\class等
// 先判断浏览器支持 e.path ,从 path 里先取
let target = e.path?.find((x: Element) => mountList.includes(x.tagName?.toLowerCase()));
// 不支持 path 就再判断 target
target = target || (mountList.includes(e.target.tagName?.toLowerCase()) ? e.target : undefined);
if (!target) return;
const metrics = {
tagInfo: {
id: target.id,
classList: Array.from(target.classList),
tagName: target.tagName,
text: target.textContent,
},
// 创建时间
timestamp: new Date().getTime(),
// 页面信息
pageInfo: this.getPageInfo(),
} as IMetrics;
// 除开商城业务外,一般不会特意上报点击行为的数据,都是作为辅助检查错误的数据存在;
this.metrics.add(metricsName.CBR, metrics);
// 行为记录 不需要携带 完整的pageInfo
delete metrics.pageInfo;
// 记录到行为记录追踪
const behavior = {
category: metricsName.CBR,
data: metrics,
...this.getExtends(),
} as behaviorStack;
this.breadcrumbs.push(behavior);
};
window.addEventListener(
'click',
(e) => {
handler(e);
},
true,
);
};
同理,如果我们想捕获
input
、keydown
、doubleClick
也是类似的写法,有兴趣的可以自行拓展
用户自定义埋点
其实用户自定义埋点这个东西,并没有那么神秘和复杂,原理也就是 SDK 内部暴露出接口供 项目使用方
调用,这样用户就可以在任意的时间段
(页面加载
、用户点击
、观看视频达到一半进度
....等等)去调用接口 上报任意的自定义内容
;
而我们 SDK 暴露出的接口,可以由SDK挂载在 window
上,也可以通过暴露接口的方式给外部调用;
然后在服务端再进行数据的归类分析
,就完成了用户自定义埋点的一系列流程;实现起来很简单,但是重要的是什么呢?是数据结构的定义,定义的数据结构需要能让服务端方便进行归类分析;
// 这里参考了 谷歌GA 的自定义埋点上报数据维度结构
export interface customAnalyticsData {
// 事件类别 互动的对象 eg:Video
eventCategory: string;
// 事件动作 互动动作方式 eg:play
eventAction: string;
// 事件标签 对事件进行分类 eg:
eventLabel: string;
// 事件值 与事件相关的数值 eg:180min
eventValue?: string;
}
// 初始化用户自定义埋点数据的获取上报
initCustomerHandler = (): Function => {
const handler = (options: customAnalyticsData) => {
// 记录到 UserMetricsStore
this.metrics.add(metricsName.CDR, options);
// 自定义埋点的信息一般立即上报
this.userSendHandler(options);
// 记录到用户行为记录栈
this.breadcrumbs.push({
category: metricsName.CDR,
data: options,
...this.getExtends(),
});
};
return handler;
};
HTTP 请求捕获
HTTP行为
也是用户行为追踪
的重要一环,有的时候,页面出现问题往往是 HTTP 请求了某些数据,渲染造成的;而除了是用户行为追踪
的重要一环外;采集 HTTP请求
的各种信息:包括 请求地址
、方法
、耗时
、请求时间
、响应时间
、响应结果
等等等等...
而为了实现上述的监控需求,我们需要了解到:现在异步请求的底层原理都是调用的 XMLHttpRequest
或者 Fetch
,我们只需要对这两个方法都进行 劫持
,就可以往接口请求的过程中加入我们所需要的一些参数捕获;
XMLHttpRequest 的劫持
预期就是,我们只需要传入一个 loadHandler
方法,它就自动会在 请求
结束时给我返回该有的数据
export interface httpMetrics {
method: string;
url: string | URL;
body: Document | XMLHttpRequestBodyInit | null | undefined | ReadableStream;
requestTime: number;
responseTime: number;
status: number;
statusText: string;
response?: any;
}
// 调用 proxyXmlHttp 即可完成全局监听 XMLHttpRequest
export const proxyXmlHttp = (sendHandler: Function | null | undefined, loadHandler: Function) => {
if ('XMLHttpRequest' in window && typeof window.XMLHttpRequest === 'function') {
const oXMLHttpRequest = window.XMLHttpRequest;
if (!(window as any).oXMLHttpRequest) {
// oXMLHttpRequest 为原生的 XMLHttpRequest,可以用以 SDK 进行数据上报,区分业务
(window as any).oXMLHttpRequest = oXMLHttpRequest;
}
(window as any).XMLHttpRequest = function () {
// 覆写 window.XMLHttpRequest
const xhr = new oXMLHttpRequest();
const { open, send } = xhr;
let metrics = {} as httpMetrics;
xhr.open = (method, url) => {
metrics.method = method;
metrics.url = url;
open.call(xhr, method, url, true);
};
xhr.send = (body) => {
metrics.body = body || '';
metrics.requestTime = new Date().getTime();
// sendHandler 可以在发送 Ajax 请求之前,挂载一些信息,比如 header 请求头
// setRequestHeader 设置请求header,用来传输关键参数等
// xhr.setRequestHeader('xxx-id', 'VQVE-QEBQ');
if (typeof sendHandler === 'function') sendHandler(xhr);
send.call(xhr, body);
};
xhr.addEventListener('loadend', () => {
const { status, statusText, response } = xhr;
metrics = {
...metrics,
status,
statusText,
response,
responseTime: new Date().getTime(),
};
if (typeof loadHandler === 'function') loadHandler(metrics);
// xhr.status 状态码
});
return xhr;
};
}
};
Fetch 的劫持
// 调用 proxyFetch 即可完成全局监听 fetch
export const proxyFetch = (sendHandler: Function | null | undefined, loadHandler: Function) => {
if ('fetch' in window && typeof window.fetch === 'function') {
const oFetch = window.fetch;
if (!(window as any).oFetch) {
(window as any).oFetch = oFetch;
}
(window as any).fetch = async (input: any, init: RequestInit) => {
// init 是用户手动传入的 fetch 请求互数据,包括了 method、body、headers,要做统一拦截数据修改,直接改init即可
if (typeof sendHandler === 'function') sendHandler(init);
let metrics = {} as httpMetrics;
metrics.method = init?.method || '';
metrics.url = (input && typeof input !== 'string' ? input?.url : input) || ''; // 请求的url
metrics.body = init?.body || '';
metrics.requestTime = new Date().getTime();
return oFetch.call(window, input, init).then(async (response) => {
// clone 出一个新的 response,再用其做.text(),避免 body stream already read 问题
const res = response.clone();
metrics = {
...metrics,
status: res.status,
statusText: res.statusText,
response: await res.text(),
responseTime: new Date().getTime(),
};
if (typeof loadHandler === 'function') loadHandler(metrics);
return response;
});
};
}
};
简单初始化封装
上面都是 proxy 劫持接口的封装,具体的调用看如下:
// 初始化 http 请求的数据获取和上报
initHttpHandler = (): void => {
const loadHandler = (metrics: httpMetrics) => {
if (metrics.status < 400) {
// 对于正常请求的 HTTP 请求来说,不需要记录 请求体 和 响应体
delete metrics.response;
delete metrics.body;
}
// 记录到 UserMetricsStore
this.metrics.add(metricsName.HT, metrics);
// 记录到用户行为记录栈
this.breadcrumbs.push({
category: metricsName.HT,
data: metrics,
...this.getExtends(),
});
};
proxyXmlHttp(null, loadHandler);
proxyFetch(null, loadHandler);
};
页面停留时间
还有一项比较通用的指标,叫做页面停留时间,是通过统计用户在每个页面的停留时间而成的
这里给一个简单的采集实例代码思路
(未封装):
const routeList = [];
const routeTemplate = {
userId: '', // 用户信息等
// 除了userId以外,还可以附带一些其余的用户特征到这里面
url: '',
startTime: 0,
dulation: 0,
endTime: 0,
};
function recordNextPage() {
// 记录前一个页面的页面停留时间
const time = new Date().getTime();
routeList[routeList.length - 1].endTime = time;
routeList[routeList.length - 1].dulation = time - routeList[routeList.length - 1].startTime;
// 推一个新的页面停留记录
routeList.push({
...routeTemplate,
...{ url: window.location.pathname, startTime: time, dulation: 0, endTime: 0 },
});
}
// 第一次进入页面时,记录
window.addEventListener('load', () => {
const time = new Date().getTime();
routeList.push({
...routeTemplate,
...{ url: window.location.pathname, startTime: time, dulation: 0, endTime: 0 },
});
});
// 单页面应用触发 replaceState 时的上报
window.addEventListener('replaceState', () => {
recordNextPage();
});
// 单页面应用触发 pushState 时的上报
window.addEventListener('pushState', () => {
recordNextPage();
});
// 浏览器回退、前进行为触发的 可以自己判断是否要上报
window.addEventListener('popstate', () => {
recordNextPage();
});
// 关闭浏览器前记录最后的时间并上报
window.addEventListener('beforeunload', () => {
const time = new Date().getTime();
routeList[routeList.length - 1].endTime = time;
routeList[routeList.length - 1].dulation = time - routeList[routeList.length - 1].startTime;
// 记录完了离开的时间,就可以上报了
// eg: report()
});
访客来路
有的时候,产品可能会问我们几个直击灵魂的问题
:
- 我们现在的新用户流量,大部分都是从哪里引流过来的啊?
- 线上环境404页面访问激增,我想知道用户是访问了哪个不存在页面才跳到 404 的
很简单,采集一下用户来路
的数据就可以了,原理就是获取 document.referrer
以及window.performance.navigation.type
用户来路地址
我们可以直接用 document.referrer
来获取用户在我们的网页上的前一个网页地址;但是需要注意的是,有几个场景我们获取到的值会是空
- 直接在地址栏中输入地址跳转
- 直接通过浏览器收藏夹打开
- 从https的网站直接进入一个http协议的网站
用户来路方式
我们可以直接使用 window.performance.navigation.type
来获取用户在我们网页上的来路方式
该属性返回一个整数值,可能有以下4种情况
0
: 点击链接、地址栏输入、表单提交、脚本操作等。1
: 点击重新加载按钮、location.reload。2
: 点击前进或后退按钮。255
: 任何其他来源。即非刷新/非前进后退、非点击链接/地址栏输入/表单提交/脚本操作等。
代码封装
export interface OriginInformation {
referrer: string;
type: number | string;
}
// 返回 OI 用户来路信息
export const getOriginInfo = (): OriginInformation => {
return {
referrer: document.referrer,
type: window.performance?.navigation.type || '',
};
};
// 初始化 OI 用户来路的获取以及返回
initOriginInfo = (): void => {
const info: OriginInformation = getOriginInfo();
const metrics = info as IMetrics;
this.metrics.set(metricsName.OI, metrics);
};
User Agent 解析
我们的 User Agent
信息里面有带有很多的信息,比如浏览器内核
、设备类型等等
,但是解析它并不是个简单的事情,如果我们自己写的话,会用到很多的正则表达式去解析它,所以这边推荐两个现成的插件
来使用:bowser 和 ua-parser-js
// nodejs 环境下 require
const parser = require('ua-parser-js');
const Bowser = require('bowser');
// 获取user-agent解析
function getFeature(userAgent) {
const browserData = Bowser.parse(userAgent);
const parserData = parser(userAgent);
const browserName = browserData.browser.name || parserData.browser.name; // 浏览器名
const browserVersion = browserData.browser.version || parserData.browser.version; // 浏览器版本号
const osName = browserData.os.name || parserData.os.name; // 操作系统名
const osVersion = parserData.os.version || browserData.os.version; // 操作系统版本号
const deviceType = browserData.platform.type || parserData.device.type; // 设备类型
const deviceVendor = browserData.platform.vendor || parserData.device.vendor || ''; // 设备所属公司
const deviceModel = browserData.platform.model || parserData.device.model || ''; // 设备型号
const engineName = browserData.engine.name || parserData.engine.name; // engine名
const engineVersion = browserData.engine.version || parserData.engine.version; // engine版本号
return {
browserName,
browserVersion,
osName,
osVersion,
deviceType,
deviceVendor,
deviceModel,
engineName,
engineVersion,
};
}
IP 采集解析
为什么要采集 IP 呢?其实很简单,我们可以通过解析 IP 地址,来解析出用户的地域
、网络运营商等信息
;而解析IP
我们可以使用诸如腾讯云、阿里云等各种的 三方API
来实现;但是采集 IP 信息
就需要我们自己来进行实现了;
具体来说,IP
并不是一个通过 JS SDK
来采集的指标数据,我们需要在服务端去获取访问过来的IP地址,获取 IP 地址的方法
我建议先阅读一下这篇文章: HTTP X-Forwarded-For 介绍 | 菜鸟教程
直接贴结论:
- 对于直接面向用户部署的 Web 应用,
必须使用
从 TCP 连接中得到的Remote Address
; - 对于部署了 Nginx 这样反向代理的 Web 应用,可以使用 Nginx 传过来的
X-Real-IP
或X-Forwarded-For 最后一节
(实际上它们一定等价)。
我这边举例一下 Nginx 下的获取,这里取 x-real-ip
或 x-forwarded-for
的最后一节即可
// nodejs 代码
const http = require('http');
function getIp(req) {
const ip = (req.headers['x-forwarded-for'] || req.connection.remoteAddress).replace('::ffff:', '');
// 取 x-forwarded-for 的话再做一个取最后一节的处理
return ip === '::1' ? '127.0.0.1' : ip;
}
http
.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
// 先取x-real-ip
// 再取service里的自定义方法,取x-forwarded-for最后一节
const ip = req.headers['x-real-ip'] || getIp(req);
res.write(`ip: ${ip}\n`);
res.end();
})
.listen(9009, '0.0.0.0');
参考链接
链接:https://juejin.cn/post/7098656658649251877
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。