什么是前端埋点

前端埋点是指在网页的前端(即客户端)放置的记录用户行为的代码。通常,这些代码会将用户的操作记录下来,然后通过某种方式传回后端服务器,以便进行分析和统计。前端埋点常常被用来跟踪用户在网站上的行为,以便对网站进行优化并提供更好的用户体验。

要实现一个前端埋点系统,通常需要以下几个步骤:

  1. 确定要跟踪哪些用户行为。这可能包括点击按钮、访问特定页面、填写表单等等。
  2. 在网页中插入跟踪代码。这些代码通常使用 JavaScript 实现,并且需要在每个需要跟踪的行为发生时触发。
  3. 设计后端服务器来接收并处理来自前端的埋点数据。这可能包括将数据存储到数据库中、使用分析工具对数据进行汇总和可视化等等。
  4. 使用工具来可视化和分析埋点数据。这可能包括使用网络分析工具、数据可视化工具等来帮助您理解用户的行为模式。

技术选型

ts + rollup

  • ts 可以提供更好的代码提示
  • rollup 配置比较干净和简洁,相比较 webpack 更适合开发工具类的库或框架,比方说 vue3 就是使用 rollup 打包的。

下面是 Rollup 和 Webpack 的一些主要区别:

  • Rollup 是一个模块打包器,而 Webpack 是一个模块打包工具。这意味着 Webpack 支持打包更多类型的文件,如图像和字体,以及更多的加载器(loaders)。
  • Rollup 具有更快的打包速度,因为它是专注于打包 JavaScript 模块的。Webpack 打包速度可能较慢,因为它可能需要处理更多类型的文件。
  • Rollup 使用树摇晃(tree-shaking)机制来删除未使用的代码。Webpack 也支持树摇晃,但需要使用 UglifyJS 插件。
  • Rollup 具有较少的配置选项,因此它可能更容易使用。Webpack 具有更多的配置选项,但也提供了更多的灵活性。

「rollup 适合对工具类的库进行打包, webpack 适合对项目类的库进行打包」

项目结构

1
2
3
4
5
6
src/
--core/
--types/
--utils/
rollup.config.js
tsconfig.json

初始化项目

1
npm init -y

因为 rollup 使用 esm, 所以在 package.json 中加上
"type": module

安装相关依赖

1
2
3
4
npm install rollup -D
npm install rollup-plugin-dts -D
npm install rollup-plugin-typescript2 -D
npm install typescript -D

其中 rollup-plugin-dts 是一个 Rollup 插件,用于将 TypeScript 声明文件转换为 JavaScript 代码,并生成相应的 *.d.ts 文件。这个插件可以帮助你在使用 TypeScript 编写的 JavaScript 库的时候更方便地生成声明文件。

配置 rollup

📄 rollup.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import ts from "rollup-plugin-typescript2"
import dts from "rollup-plugin-dts"
export default [
{
//入口文件 相当于 webpack 里的 entry 选项
input: "./src/core/index.ts",
output: [
//打包esModule
{
file: "./dist/index.esm.js",
format: "es",
},
//打包common js
{
file: "./dist/index.cjs.js"
format: "cjs",
},
//打包 AMD CMD UMD
{
file: "./dist/index.js",
format: "umd",
},
],
//配置 ts 插件
plugins: [ts()],
},
{
//打包声明文件
input: "./src/core/index.ts",
output: {
file: path.resolve(__dirname, "./dist/index.d.ts"),
format: "es",
},
plugins: [dts()],
},
]

配置一下 npm script

加上打包命令 build: rollup -c

初始化 Tracker 类

定义选项类型

我们给用户提供一个默认选项,并且用户可以传入个性化地配置。

默认选项的类型如下,
📃 /types/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export interface DefaultOpitons {
// 唯一标识符
uuid?: string,

// 上报地址
requestUrl?: string,

// 是否监听 hash 路由
hashTracker: boolean,

// 是否监听 history 路由
historyTracker: boolean,

// 是否监听 dom
domTracker: boolean,

// 其他配置信息
extra?: Record<string, any>,

// 版本号
sdkVersion: string | number,
}

我们再定义一下可以选择性的配置类型 Options, 它是默认选项的一部分,所以我们通过 Partial 来实现:
📃 /types/index.ts

1
2
3
export interface Opitons extends Partial<DefaultOpitons>{
requestUrl: string,
}

定义 Tracker 类

📃 /core/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {Options, DefaultOPtions} from './types'
class Tracker {
public data: Options;
constructor(opitons: Options){
this.data = Object.assign(this.initDef(), options);
}
private initDef(): DefaultOptions{
return <defaultOpitons> {
hashTracker: false,
historyTracker: false,
domTracker: false,
}
}
}

实现 page view

  • 使 pushState 和 replaceState 可以被监听,
    因为 addEventListener 监听不到 History API 中的 pushState 和 replaceState, 可以采用装饰器模式对其重写在 📄utils/pv.ts 里面实现一个

    1
    2
    3
    4
    5
    6
    7
    8
    9
    export function createHistoryEvent<T extends keyof History>(type: T){
    origin = window.history[type];
    return function(this: any){
    const e = new Event(type);
    const window.dispatchEvent(e)
    const res = origin.apply(this, arguments);
    return res
    }
    }
  • 实现监听
    在 core/index.ts 中实现监听逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Tracker {
...
constructor(){
...
this.installTracker();
}
...
private installTracker(){
if(this.data.historyTracker){
this.captureEvent(["pushState", "replaceState", "popState"], "history-page-view")
}
if(this.date.hashTracker){
this.captureEvent(['hashChange'], "hash-page-view")
}
}

private captureEvent(mouseEventList: string [], targetKey: string, date?: any){
mouseEventList.forEach(e=>{
window.addEventListener(e, (e)=>{
console.log("页面跳转已被监听", e)
})
})
}
}

实现 unique visitor

未登陆的访客(一台计算机作为一个访客)如何进行记录呢
解决方案一:
将一个 uuid 存在 localStorage 里

解决方案二:
使用 canvas 指纹追踪技术

这里使用第一种方法,在 Tracker 类中暴露两个公共类来设置 uuid 和 extra 字段

1
2
3
4
5
6
7
8
9
10
11
class Tracker{
...
public setUserId<T extends DefaultOpitons["uuid"]>(uuid: T){
this.data["uuid"] = uuid;
}

public setExtra<T extends DefaultOpitons["extra"]>(extra: T){
this.data["extra"] = extra;
}
...
}

上报

对监听到的信息进行上报

XMLHttpRequest vs Navigator.sendBeacon

  • Navigator.sendBeacon 方法在页面卸载时发送数据,XMLHttpRequest 可以在任何时间发送数据。
  • XMLHttpRequest 可以发送任何类型的 HTTP 请求,而 Navigator.sendBeacon 只能发送 HTTP POST 请求。
  • XMLHttpRequest 还可以处理请求和响应的头部信息,而 Navigator.sendBeacon 不能。
  • Navigator.sendBeacon 方法可以将数据发送到服务器,即使页面已经卸载了,而XMLHttpRequest 无法保证数据在页面卸载后发送到服务器。
  • 浏览器对于XMLHttpRequest的支持度高于Navigator.sendBeacon,所以XMLHttpRequest在兼容性上会更好。
  • Navigator.sendBeacon 和 XMLHttpRequest 在发送数据的类型上有所不同。
    Navigator.sendBeacon 只能发送 ArrayBufferView、Blob、USVString 类型的数据。
    XMLHttpRequest 可以发送多种数据类型,包括 ArrayBufferView、Blob、Document、DOMString、FormData、URLSearchParams 等。
    因为 Navigator.sendBeacon 可以在页面关闭后继续发送,所以我们采用 Navigator.sendBeacon

实现细节

我们分两层,首先我们实现以下一个私有的方法用来发送数据。

1
2
3
4
5
6
7
8
9
private reportTracker(data){
// sendBeacon 可以发送 blob
const params = Object.assign(this.data, data, { time: new Date().getTime() })
let headers = {
type: 'application/x-www-form-urlencoded'
};
let blob = new Blob([JSON.stringify(params)], headers);
navigator.sendBeacon(this.data.requestUrl, blob)
}

然后我们在上层调用它

1
2
3
private senndTracker(data){
this.reportTracker(data)
}

===== 写成 =

使用 Fira Code 字体解决, 然后打开 font-ligatures

  • 安装 Fira Code, -> _GitHub 链接
  • 打开 vscode 设置 JSON 文件,添加
    1
    2
    "editor.codeLensFontFamily": "Fira Code",
    "editor.fontLigatures": true,

数组越界问题

数组越界问题也是高频问题
解决方案:

  • 牢记初始下标是 0 , 最大下标是 arr.length - 1
  • 使用长度为 5 的小数组验证逻辑

死循环

循环也是问题一般是由于以下原因导致的

  • 循环内更新错误,例如没有更新循环变量或者 -- 写成了 ++
  • 错误的重置了变量
  • 跳出循环的条件错误
    解决方案:
    使用小的数据进行验证

边界情况

  • 在 javascript 中,如果对象(object)中的属性不存在则会返回 undefined
    例如:
    假设你服务器返回一个包含对象的数组, 如果里面没有一个对象,那么去则会返回 undefined
    1
    2
    3
    4
    5
    const nestObj = res[0];
    // 如果 res 为空数组
    // 此时如果在 nestObj 上使用对象方法则会发生错误
    nextObj.toString();
    // 会报错 undefined 上面没有 toString 方法
    解决方案:
    我们可以设置一个默认值解决这个问题
    1
    const nextObj = res[0] || {}
    如果使用了 typescript 则可以在配置中添加 "strictNullChecks": true

在遍历数组的时候操作数组

从 arr 数组中删去上去下标在 ast 数组中的元素
示例:
输入:
arr = [1, 2, 3, 4]
ast = [2,3]
输出:
arr 变为 [1, 2]

错误示例:hello

1
2
3
4
5
6
7
8
// arr表示待操作的数组
// ast中存储待删除元素的下标
arr=[1,2,3,4]
ast=[2,3]
ast.forEach((item)=>{
arr.splice(item,1)
})
// arr 变成了 [1, 2, 4]

正确示例

1
2
3
4
arr = [1, 2, 3, 4]
ast = [2, 3]
arr = arr.fliter((e,index)=>!ast.includes(index))
// arr 变成了 [1, 2]

API是应用程序编程接口的缩写,它是一组规范,定义了应用程序如何与其他软件或服务通信。它提供了一组标准的、稳定的接口,可以让不同的系统或服务之间进行交互。

SDK是软件开发工具包的缩写,它是一组工具,可以帮助开发人员使用某种软件或服务来创建应用程序。SDK通常包括一组API、文档、样例代码和工具。

因此,可以将API看作是一组规范,定义了如何与某种软件或服务进行通信,而SDK则是用来实现这些规范的工具包。

简而言之,API是提供了一组接口,而SDK是提供了使用这些接口的工具。

四数之和 - 力扣(LeetCode)

题目描述

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]  

提示:

1 <= nums.length <= 200
-109 <= nums[i] <= 109
-109 <= target <= 109

解1 哈希表

和两数之和的思路相似我们可以用哈希表解决这个问题
循环两层然后就转化成了两数之和的问题, 需要注意的是循环的时候需要跳过相同的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @param {number []} nums
* @param {number} target
* @return {number [][]}
* 时间复杂度 O(n^3)
* 空间复杂度 O(n)
**/
function fourSum(nums: number [][], target: number){
for(let i = 0; i < nums.length; i++){
// 跳过重复元素
if(i !== 0 && nums[i] === nums[i - 1]) continue
}
}

当然去重操作也可以放在后面进行 :)

描述

放苹果_牛客题霸_牛客网
把m个同样的苹果放在n个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?
注意:如果有7个苹果和3个盘子,(5,1,1)和(1,5,1)被视为是同一种分法。
数据范围:0 <= m <= 10 ,1 <= n <= 10

输入描述:

输入两个int整数

输出描述:

输出结果,int型

示例1

输入:
7 3

输出:
8

思路:减少盘子, 或者减少苹果,让情况退化
可以分为一下两种情况:
盘子 > 苹果
当盘子的数量比苹果多的时候,把多余的盘子拿走不会对结果产生影响
盘子 <= 苹果
当盘子的数量比苹果多的时候,又可以分为两种情况
一种情况是盘子上全放了苹果
如果都放了苹果则所有的盘子上都拿去一个苹果对结果不会产生影响
另一种情况是有的盘子上没放苹果
既然有点盘子没有放苹果, 那么拿走一个空盘也不会对结果产生影响

解1 递归

1
2
3
4
5
6
7
8
// const [m, n] = readline().split(' ').map(Number);
function sol(m, n){
// console.log('mn',m,n)
if(m <= 1 || n <= 1) return 1;
if(n > m) return sol(m ,m);
return sol(m, n - 1) + sol(m - n, n);
}
console.log(sol(3, 3))

解2 动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function putApple(m, n){
const arr = new Array(m + 1);
for(let i = 0; i < m + 1; i++){
arr[i] = new Array(n + 1).fill(1)
}
// console.log(arr)
for(let i = 1; i < m + 1; i++){
for(let j = 1; j < n + 1; j++){
if(i <= 1 || j <= 1){
arr[i][j] = 1;
continue;
}
if(i < j){
arr[i][j] = arr[i][i];
continue;
}
arr[i][j] = arr[i - j][j] + arr[i][j - 1]

}
}

console.log(arr.join('\n'))
return arr[m][n];
}

putApple(3,3)

深度优先遍历(Depth-First Search, DFS)是按照树的深度遍历树的节点,优先遍历节点的子树。

广度优先遍历(Breadth-First Search, BFS)是按照树的宽度遍历树的节点,优先遍历节点的同层节点。

下面是用 JavaScript 实现的 DFS 和 BFS 算法的例子。这里我们假设我们有一棵树,树上的每个节点都是一个 JavaScript 对象,有一个 value 属性表示节点的值,一个 children 属性表示节点的子节点数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const tree = {
value: 1,
children: [
{
value: 2,
children: [
{ value: 4, children: [] },
{ value: 5, children: [] }
]
},
{
value: 3,
children: [
{ value: 6, children: [] },
{ value: 7, children: [] }
]
}
]
};

下面是 DFS 的实现:

1
2
3
4
5
6
7
8
function DFS(tree) {
console.log(tree.value);
for (const child of tree.children) {
DFS(child);
}
}

DFS(tree);

输出结果为:

1
2
3
4
5
6
7
1
2
4
5
3
6
7

下面是 BFS 的实现:

1
2
3
4
5
6
7
8
9
10
function BFS(tree) {
const queue = [tree];
while (queue.length > 0) {
const node = queue.shift();
console.log(node.value);
queue.push(...node.children);
}
}

BFS(tree);

输出结果为:

1
2
3
4
5
6
7
1
2
3
4
5
6
7

q: 将未知长度的字符串分成长度为 K 的 n 段,如果字符串不够则补 ‘0’
a: 直接先补 K 个 ‘0’ 然后开始截取

q: 防止表达式值溢出
a: value = Math.max([expression], limit_value)

q: 区间开闭对应的元素个数
a:

1
2
3
4
5
[a, b) (a, b]
// 这个区间共有b - a个数

[a, b]
// 共有 b - a + 1个数

q: 求两个数的平均数且不能溢出
%%防溢出%%
a: a + (b - a) >> 1

q: 求十位,个位

1
2
3
4
5
// 十位
n / 10 % 10

// 个位
n % 10

q: 不用额外的变量交换两个数

1
2
3
4
5
6
7
// 以下代码限于二进制
a = a ^ b;
b = a ^ b;
a = a ^ b;

// 最右边的1
const rightMostOne = a & (1 + ~a)

q: 如何在字符串中套变量, 该变量的值是字符串
a:

1
2
3
4
5
// 1
`'${str}'`

// 2
"'" + str + "'"

q: 当用二进制补码(two’s compelement)表示负数是怎样的
a:

Flip the bits and add one
Example :  +7 =  0b 0000 0111, -7 =  0b 1111 1001

时间复杂度和空间复杂度的概念

时间复杂度是指算法执行所需要的时间。通常情况下,我们用大 O 表示法来描述时间复杂度,其中 O(f(n)) 表示算法的最坏时间复杂度为 f(n)。

空间复杂度是指算法执行所需要的存储空间。通常情况下,我们也用大 O 表示法来描述空间复杂度,其中 O(f(n)) 表示算法的最坏空间复杂度为 f(n)。

在分析算法的时间和空间复杂度时,我们通常关注的是随着输入规模的增大,算法的执行时间和所需空间的变化情况。例如,如果算法的时间复杂度是 O(n^2),则当输入规模增大时,算法的执行时间会呈平方级别增长。如果算法的空间复杂度是 O(n),则当输入规模增大时,算法所需的存储空间也会呈线性增长。

计算时间复杂度

通常来说,计算算法的时间复杂度可以采用以下步骤:

  1. 分析算法的基本操作:首先要分析算法中执行的基本操作,并确定其时间复杂度。这通常是分析算法复杂度的第一步。

  2. 分析算法的执行次数:其次,要分析算法中基本操作的执行次数,即算法的时间复杂度的主要因素。

  3. 确定算法的时间复杂度:根据算法中基本操作的时间复杂度和执行次数,可以确定算法的时间复杂度。

例1

对于算法 A,如果分析出其中基本操作的时间复杂度为 O(1),并且该基本操作会被执行 n 次,那么算法 A 的时间复杂度就是 O(n)。

例2:

假设我们有一个算法,该算法的流程如下:

从 1 到 n 遍历一个数组 A,执行基本操作 1
对于每个数字 x,从 1 到 x 遍历一个数组 B,执行基本操作 2
对于这个算法,我们可以分析其时间复杂度如下:

基本操作 1 的时间复杂度为 O(1)
基本操作 2 的时间复杂度为 O(1)
基本操作 1 在整个算法中会被执行 n 次,基本操作 2 在整个算法中会被执行 n(n+1)/2 次,因此算法的时间复杂度为 O(n+n(n+1)/2) = O(n^2)。

Master Theorem(又称为 Master 公式)

对于分治算法, 常常需要考虑三个方面:

  1. 划分子问题的代价:这可能包括复制数据的开销,或者对数据进行排序的开销。
  2. 解决子问题的代价:这可能包括递归调用的开销,以及在解决子问题时执行的其他操作的开销。
  3. 合并子问题的解决方案的代价:这可能包括对子问题的解决方案进行排序或合并的开销。
    如果某递归算法运行时间具有形式为递推关系的形式:
    T(n) = aT(n/b) + O(N^d)
    则其时间复杂度为下:
    condition 时间复杂度
    logba > d O(nlogba)
    logba < d nd
    logba = d O(nd * logn)

计算空间复杂度

我们可以通过以下方法来计算空间复杂度:

  1. 计算所使用的静态存储空间,即在程序运行之前就已经分配好的存储空间,包括常量、变量和数组等。
  2. 计算动态存储空间,即在程序运行过程中临时分配的存储空间,例如递归调用时使用的栈空间。

在计算空间复杂度时,应该将上述两种存储空间都考虑在内,最后使用大O表示法来表示结果。
分析所需空间大小时需要考虑以下几点:

  • 变量的数量:算法使用的变量数量与数据规模之间的关系。
  • 辅助数据结构的大小:辅助数据结构所使用的存储空间大小与数据规模之间的关系。
    一般来说,如果算法使用的存储空间大小与数据规模呈线性关系,空间复杂度为 O(n);如果使用的存储空间大小是常数级别的,空间复杂度为 O(1)。
    举个例子,如果有一个算法使用了一个大小为 n 的数组来存储数据,那么它的空间复杂度就是 O(n),因为数组的大小是与数据规模呈线性关系的。
    如果有一个算法只使用了几个常数个变量来存储数据,那么它的空间复杂度就是 O(1),因为变量的数量是常数级别的。

用来求解一个数组中的最大子序列和。这个算法的时间复杂度是 O(n)(其中 n 是数组中元素的个数),空间复杂度是 O(1),因为它只使用了常数个变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
function maxSubsequenceSum(arr) {
let maxSum = 0;
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
if (sum > maxSum) {
maxSum = sum;
} else if (sum < 0) {
sum = 0;
}
}
return maxSum;
}
0%