B站前端评论区源码逆向

简介

之前看到一个UP主因为一些原因在视频底下大规模控评,我注意到在控评的评论区下,原本网页端应该每次加载返回20条评论,但实际上要少很多。我就好奇是评论本身后端没有返回还是怎么回事,就打开F12抓了下请求看看,结果发现被标记为隐藏的评论后端也在正常返回数据,只是前端隐藏了。不仅如此,我还发现评论的API还有返回评论者评论时的IP属地,只是前端没有进行渲染。于是我就打算写个脚本来把这些数据显示出来

脚本目前已经发布,详见这里

以下功能已集成至Bilibili Evolved,请搜索IP属地显示关注时间显示相簿发布时间显示等组件

此文就来讲解该脚本的原理

实现原理

以下是该脚本的实现原理讲解

前期准备

API分析

B站网页端每次会请求20条评论,后端的评论API会返回评论的相关信息。评论的详细JSON格式可以参考这个

以下是一条评论数据的示例

1
2
3
4
5
6
7
8
9
10
{
...
"invisible": false,
"reply_control": {
"max_line": 6,
"time_desc": "10秒前发布",
"location": "IP属地:江苏"
}
...
}

根据我和网页上实际显示的评论和后端实际返回的评论比较,我发现前端会检测评论条目对象里的invisible属性,若属性为空时则不会渲染评论本身。除此之外,可以发现reply_control.location属性正是App端显示的IP属地,而网页端并没有对这个属性进行解析。因此,只要能实现对这两个属性进行单独处理,就能实现显示隐藏评论以及在网页端显示IP属地

技术选择

为了实现这些功能多半得直接修改B站的前端JS脚本源码,因为众所周知这年头的打包工具全都会把JS脚本压缩成IIFE运行,变量全都给整成闭包了,根本没法用JS脚本在外部进行操作,所以用TamperMonkey这种在网页上执行用户JS脚本的插件肯定没辙,得想别的办法

幸好我以前凑巧就用过一个能实现这种功能的插件,Header Editor

Header Editor除了修改Header和请求重定向等功能之外,在Firefox里也能使用自定义脚本直接修改响应体,这样就能实现修改源码的功能

不过我经过研究发现Chrome等浏览器貌似不支持这么做,找了几个同类型的插件都说Chrome里无法修改响应体,估计是浏览器本身的限制
幸好这类插件的重定向请求等功能在Chrome里也可以用,可以在修改完B站前端的源码后把修改后的文件放到别的CDN上,然后把B站原先的脚本重定向到CDN那里存储的修改后的脚本,这样也能实现一样的效果

最终我选择在GitHub新建一个仓库,并且使用jsDelivr作为CDN直接获取GitHub仓库里的文件,这样就能在非Firefox浏览器里也实现同样的效果

旧版评论区逆向

我是先从网页端旧版界面开始逆向的,先看看网页端的评论元素长什么样

1
2
3
4
5
6
7
8
9
10
11
<div class="con ">
...
<div class="info">
<span class="time-location">
<span class="reply-time"> xx分钟/小时前 / 评论具体时间 </span>
<span class="like "> 点赞信息... </span>
...
</span>
</div>
...
</div>

要添加自定义元素,那肯定得先找到是哪段代码创建的这个评论元素。这里的class非常明显,我们可以直接在浏览器的开发者工具里全局搜索time-locationreply-time等样式。以下是我在Firefox开发者工具里搜索的结果

这不是一下就搜出来了(

可知脚本来自comment.min.js 这个.min.js后面根本不min啊(恼) 。注意这里搜到了两个片段,分别是处理评论主楼和楼中楼回复的,处理的时候两个地方都得改一遍。跳转过去查看代码,我们就会发现这是在创建评论区对应的DOM元素

很明显这是在用字符串拼接的方式直接创建了HTML元素并直接插入到<body>里。但是等等,有没有注意到什么

1
// item?.reply_control?.location ? `<span class="reply-location">IP属地:${item?.reply_control?.location || ''}</span>` : '',

注释?

是的,你没看错,他们已经把IP属地显示都写好了,仅仅是去掉这行注释就可以直接启用网页端IP属地显示。而且注释里写的样式reply-location也是个确实存在的可用的CSS样式
这说明B站早就做好了对应的功能,只是由于某种未知的原因没有把这个功能在网页端上线
那这样的话只需要去掉这行注释就能实现IP属地显示了

但我要做的可不止是这个,我要做的还有显示被隐藏评论。另外有人觉得只显示IP属地不够明显,所以我还顺便改了下IP属地的样式(

简单检查下代码可以发现这一段行大致是这样的

1
2
3
4
5
6
7
8
9
var con = [
...,
'<div class="info">',
this._createPlatformDom(item.content.plat),
"<span class=\"time-location\">",
"<span class=\"reply-time\">".concat(this._formateTime(item.ctime), "</span>"),
// item?.reply_control?.location ? `<span class="reply-location">IP属地:${item?.reply_control?.location || ''}</span>` : '',
...
]

因此我们可以猜测con数组就是原始HTML片段,只要我们在后面继续加就可以实现添加自己的元素

同时可以注意到这其中用的参数都是item.xxx,比如评论时间是取的item.ctime,评论属地用的是item?.reply_control?.location
因此我们可以猜测item就是评论元素本身,那要检测评论是否被隐藏的话,只要取item.invisible即可判断

所以最终我的处理方法是把

1
// item?.reply_control?.location ? `<span class="reply-location">IP属地:${item?.reply_control?.location || ''}</span>` : '',

替换成

1
2
item?.reply_control?.location ? `<span style="margin-left:5px; color: #0CC;">${item?.reply_control?.location || ''}</span>` : '',
item?.invisible ? '<span style="margin-left: 5px;color: red;">评论被隐藏</span>' : '',

注意IP属地:这几个字在返回的JSON数据里已经加上了,所以这里应该把HTML里的IP属地:这几个字删掉,不然就会重复显示了
除了恢复IP属地显示之外,我加了一条判断,如果item.invisible为true就添加一个<span>用来说明评论被隐藏,否则就什么也不添加
这样就能在显示评论属地的时候也能标记出隐藏评论了

但是别忘了,前文刚说了

我发现前端会检测评论条目对象里的invisible属性,若属性为空时则不会渲染评论本身

如果元素直接不被创建,在这光拼接HTML也没有用,所以还得把这个判断给整掉
但有了之前的经验,要整掉这个判断就很简单了

在该脚本里直接搜索invisible就能发现5段相同的判断

1
2
3
if (item.invisible) {
return '';
}

明显是在评论被隐藏时不渲染评论
那想干掉这个判断就很简单了,直接把判断条件替换成if (false)或者干脆删掉这段代码都行

所以最后在Header Editor里实现个替换脚本就很简单了

1
2
3
4
5
6
7
8
return val
.replaceAll("// item?.reply_control?.location ? `<span class=\"reply-location\">IP属地:${item?.reply_control?.location || ''}</span>` : '',",
"item?.reply_control?.location ? `<span style=\"margin-left:5px; color: #0CC;\">${item?.reply_control?.location || ''}</span>` : '',item?.invisible ? '<span style=\"margin-left: 5px;color: red;\">评论被隐藏</span>' : '',"
)
.replaceAll(
"item.invisible",
"false"
);

这就是网页端旧版评论区逆向的原理了

新版评论区逆向

就在我整完旧版评论区逆向发布脚本后,有群友跟我说不能用,F12抓网络也只能找到个comment-pc-vue.next.js的东西。经我测试发现B站网页端的旧版和新版界面竟然用的不是同一套处理方案,新版的是以vue为基础整的,并且这个comment-pc-vue.next.js虽然名字里没有min但确实是给压缩完了。幸好有前面逆向旧版评论区的经验,我猜测新版评论区也保留了reply-location等一系列和IP属地相关的样式等,所以可以按类似的方法分析

获取js源码

首先要做的自然是获取相关脚本的源码了
B站新版界面前端的源码似乎还处于开发中,大概每隔几天就会重新打包更新一次,这里以2023/3/5的版本为例

源码
Firefox开发者工具格式化后的源码

https://s1.hdslb.com/bfs/seed/jinkela/commentpc/comment-pc-vue.next.js把脚本下载下来看看

2023/3/5

彳亍,这可是真的是.min.js了。不过问题不大,先用Firefox对代码进行格式化并保存后再来找找reply-location看看

1
2
3
4
5
// 第44279行
os = {
key: 0,
class : 'reply-location'
}

搜索出来能搜出好几个,这里明显能看出这段代码是把reply-location样式给整进变量os里了,那我们来看看os在哪里被引用了

1
2
3
4
5
6
7
8
9
10
// 44714行
(0, P._) ('div', ts, [
(0, P._) ('span', rs, (0, kt.toDisplayString) ((null === (a = (0, z.SU) (m)) || void 0 === a ? void 0 : a.last_mtime_text) || (0, z.SU) (lt) ((0, z.SU) (v))), 1),
(0, z.SU) (f) ? ((0, P.wg) (), (0, P.iD) ('span', os, (0, kt.toDisplayString) ((0, z.SU) (f)), 1)) : (0, P.kq) ('v-if', !0),
(0, P._) ('span', {
class : 'reply-like',
onClick: ke
}, ...
]),
...

找是找出来了,可是这坨代码是nm啥啊(

先别急,一步步来分析

源码分析

首先我们可以注意到,这段代码里有很多字符串常量,如divspanreply-like等。再加上我们刚刚找到的os变量,可以合理猜测这里就是在创建评论区对应的DOM元素

再注意一下每行代码的开头

1
2
3
(0, P._)
(0, P._)
(0, z.SU)

不熟悉JavaScript特性的话可能就不知道这段是在整啥,这个其实是在使用JavaScript的逗号表达式,会返回最后一个表达式的值,详见这里

所以这里其实就是等于直接取后面的值,也就是P._z.SU。在这几个后面直接就跟了括号,所以这里其实就是取P._z.SU,并把它们当作函数进行调用
为什么是P._z.SU这种鬼畜的变量名?这是打包工具对源码进行压缩的结果,就当作是混淆后的变量名得了。因为是经过打包压缩的代码,所以我们这里就不指望能通过删注释就复原IP属地显示了,我们得自己写一个出来

至于为什么不直接写P._()进行调用,而要写(0, P._) ()这种鬼畜的形式?这是为了改变函数里的this指向,可以看看隔壁的讲解,这里不再赘述

所以这里摆明了就是函数调用,包括后面的(0, P.wg)(0, P.iD)(0, kt.toDisplayString)(0, z.SU)(0, P.kq)全都是一个意思,都是在进行函数调用而已

接下来尝试分析其中的一行

1
(0, P._) ('span', rs, (0, kt.toDisplayString) ((null === (a = (0, z.SU) (m)) || void 0 === a ? void 0 : a.last_mtime_text) || (0, z.SU) (lt) ((0, z.SU) (v))), 1),

先别两眼一黑,一步步来
我们知道P._就是一个函数,先不管这个函数具体是啥,一个个来看后面的参数

第一个参数'span'很明显就是指的<span>标签

第二个参数rs我们可以查找一下引用,不难发现

1
2
3
4
// 44276行
rs = {
class : 'reply-time'
},

好,这个明显就是评论时间标签的样式。注意rs赋值的方法是上述代码,而不是rs = 'reply-time',,所以我们可以猜测rs和上文提到的os都不止是class,而是这个元素的所有属性,包括class

继续看下一个参数

1
(0, kt.toDisplayString) ((null === (a = (0, z.SU) (m)) || void 0 === a ? void 0 : a.last_mtime_text) || (0, z.SU) (lt) ((0, z.SU) (v)))

???
别急,还是一步步来

首先,kt.toDisplayString很明显就是个把后面的东西转换成字符串的函数,那我们直接看后面的参数…

1
(null === (a = (0, z.SU) (m)) || void 0 === a ? void 0 : a.last_mtime_text) || (0, z.SU) (lt) ((0, z.SU) (v))

这nm是啥?

说实话,我逆向到这的时候我也不知道这是个锤子,但后面能看到几个有意义的参数,如(0, z.SU) (m)a.last_mtime_text(0, z.SU) (lt) ((0, z.SU) (v))

前面两个提供不了什么特别有用的信息,来看看第三个(0, z.SU) (lt) ((0, z.SU) (v))

查看一下lt的引用,可以找到它的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 37575行
lt = function (e) {
var n = Math.floor((new Date).getTime() / 1000) - e,
...
if (n < 60) {
var v = 10 * Math.floor(n % 60 / 10);
return v >= 10 ? ''.concat(v, '秒前') : '刚刚'
}
if (60 <= n && n < 3600) {
var f = Math.floor(n / 60);
return ''.concat(f, '分钟前')
}
if (n >= 3600) {
var A = Math.floor(n / 3600);
return ''.concat(A, '小时前')
}
},

一眼顶针,鉴定为根据评论时间计算该显示的字符串
所以这个函数的参数e就是后面的(0, z.SU) (v)

我们来看看v的定义

1
2
// 44345行
v = p.replyTime,

这名字怎么看都像是评论的具体时间

因此,这里我们可以简单猜测一下这行代码的作用

  1. P._是创建HTML元素用的函数,它接收三个参数,第一个是字符串类型的元素名称,第二个是元素的各种属性,第三个估计不是innerText就是innerHTML
  2. z.SU是某种取值函数,像是这里的ltv就是和评论时间相关的函数/变量

(这里其实可以猜测P._就是vue的渲染函数h(),详见这里,可以看到参数类型跟这里的一模一样)

既然得知了这行代码的大致作用,我们可以修改源码输出一下v试试,将(0, z.SU) (lt) ((0, z.SU) (v))直接整成console.log(v),会发现v就是vue的响应式对象

(0, z.SU)的作用就很明显了,就是对响应式对象进行.value取值

接下来我们可以看看下一行代码

1
(0, z.SU) (f) ? ((0, P.wg) (), (0, P.iD) ('span', os, (0, kt.toDisplayString) ((0, z.SU) (f)), 1)) : (0, P.kq) ('v-if', !0),

好,这nm又是什么(

别急,开头的f我们可以先查找下引用,不难发现

1
2
// 44346行
f = p.replyLocation,

这很有可能就是没有被显示出来的IP属地
后面跟的是一个三元表达式,既然当ffalse时显示(0, P.kq) ('v-if', !0),那就当这段代码是v-if判断得了,结果应该就是不创建元素

所以我们把重点放到前面来,可以看到关键代码

1
(0, P.iD) ('span', os, (0, kt.toDisplayString) ((0, z.SU) (f)), 1)

(0, P.iD)和前文类似,一眼顶针鉴定为创建元素
后面的三个参数一样,os就是刚刚的reply-location,即IP属地的显示样式。后面的(0, z.SU)就是前面说的对响应式对象取值

我当时逆向时是在这里把前面的(f)改成(true)强制让它输出f的内容,结果发现f恒为false

上文提到f = p.replyLocation,那我们看看这个p是怎么回事

1
2
// 44343行
p = xt(t),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 37905行
xt = function (e) {
var n = (0, P.Fl) ((function () {
var n;
return (null === (n = e.value) || void 0 === n ? void 0 : n.rpid) || 0
})),
t = (0, P.Fl) ((function () {
var n;
return (null === (n = e.value) || void 0 === n ? void 0 : n.root) || 0
})),

...

return {
replyId: n,
rootReplyId: t,
replyForReplyId: r,
...
}
}

很明显xt就是把评论数据解析成对象的函数,来看其中一个

1
2
3
4
var n = (0, P.Fl) ((function () {
var n;
return (null === (n = e.value) || void 0 === n ? void 0 : n.rpid) || 0
})),

这里就是在解析一个属性了。注意看n.rpid,这个是后端返回的评论原始JSON数据里的一个属性
P.Fl应该就是vue的ref了,因为返回的值是vue的响应式对象,需要我们使用.value才能获取具体的值
前面那一段可以猜测是打包工具针对不支持ES6的浏览器写的polyfill,猜测源码应该是使用链判断运算符写的空值判断

1
let n = ref(e.value?.rpid ?? 0);

因此可以猜测后面每一个x = (0, P.Fl) ...都是在读取一个属性,最后的return就是把这些读取的单个属性拼接成一个对象
那我们就来看看最后返回的replyLocation到底是怎么回事

1
2
// 38172行
replyLocation: l,
1
2
3
4
// 37926行
l = (0, P.Fl) ((function () {
return !1
})),

这里要提一下js的自动类型转换,!1是打包工具对于false的简写,在比较前会自动转换为false,所以!1 === false
所以我们可以合理猜测源码是

1
let l = ref(false);

合着location是tm固定返回false啊(恼)

我们还有一个功能是标记隐藏评论,这个就在刚刚p = xt(t)的下面,不难发现C = p.invisible,创建新元素的时候判断一下这个C,并查找下C的引用把原先的判断条件全改成false就行了(

现在我们把需要了解的源码都基本上浏览了一遍,可以开始魔改了

源码修改

我们从44716行开始魔改

1
(0, z.SU) (f) ? ((0, P.wg) (), (0, P.iD) ('span', os, (0, kt.toDisplayString) ((0, z.SU) (f)), 1)) : (0, P.kq) ('v-if', !0),

这一行的作用是创建IP属地显示元素,若存在就显示,不存在就不创建

按照惯例我们先改一下它的样式,把os改成我们自己的{class:"reply-location",style:{color:"#0CC"}},这样就能在保持原有样式的同时加上我们自己的字体颜色显示了

源码里已经把创建元素都写好了,改的是收到的IP属地的数据,所以我们这里不需要动创建元素这里,直接取改原数据处理那一段
接下来我们就需要改f,即replyLocation

按照上文的分析,replyLocation本身就被设计成固定返回false,我们得直接修改它的值
不过按照上文的分析也不难实现,只要把37927行的return !1改成return e.value?.reply_control?.location即可,这样就可以正确处理IP属地了

下一行我们得创建自己的隐藏评论标记元素,已知C是ref(评论是否被隐藏),P.iD是创建元素,直接照抄上面创建元素的方法即可

1
(C.value)?((0,P.iD)("span",{style:{marginLeft:"-10px",marginRight:"10px",color:"red"}},(0,kt.toDisplayString)("评论被隐藏"),1)):(0,P.iD)("span",{style:{display:"none"}}),

这里的逻辑是,先取C.value判断评论是否被隐藏,如果是就在IP属地元素后创建个自己的span,设置好样式,内容就是"评论被隐藏",否则就创建个不显示的空元素(我不知道怎么在不创建元素的情况下还不报错,只能这么弄了)

最后别忘了查找对于C的引用并把判断条件改成false

1
2
// 44556行
return (0, z.SU) (w) || (0, z.SU) (C) ? (0, P.kq) ('v-if', !0) : ((0, P.wg) (), (0, P.iD) ('div', { ...

直接把(C)改成(false)即可

这样就完成主楼回复内容替换了

但别忘了上文说的,除了主楼之外还有楼中楼也得进行相同的处理,楼中楼对应的样式是sub-reply-location,按和上文完全相同的流程再走一遍即可

通用替换脚本

按以上方法确实可以实现所需的功能,但是没法整出通用替换脚本,因为B站前端新版界面似乎一直在开发,每几天就会重新打包一次,很多的变量名都会改
所以得用一些方法在变量名被修改时也能匹配对应的代码

我用的方法是正则表达式配合捕获组
虽然匹配的方法有亿点复杂吧(

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
37
38
39
40
//主楼invisible变量
let replyInvisible = val.match(/(return\(0,\w+\.\w+\)\(\w\)\|\|\(0,\w+\.\w+\)\()(\w)(\)\?\(0,\w+\.\w+\)\("v-if",!0\):\(\(0,\w\.\w+\)\(\),\(0,\w+\.\w+\)\("div",{key:0,class:"reply)/)?.[2];
//回复楼invisible变量
let subReplyInvisible = val.match(/(return\(0,\w+\.\w+\)\(\w\)\|\|\(0,\w+\.\w+\)\()(\w)(\)\?\(0,\w+\.\w+\)\("v-if",!0\):\(\(0,\w\.\w+\)\(\),\(0,\w+\.\w+\)\("div",{key:0,class:\()/)?.[2];

//隐藏评论恢复显示
val = val
.replace(`(${replyInvisible})?(0`, "(false)?(0")
.replace(`(${subReplyInvisible})?(0`, "(false)?(0");

//主楼IP属地样式reply-location
let classReplyLocation = val.match(/(\w\w)={key:0,class:"reply-location"}/)?.[1];
//回复楼IP属地样式sub-reply-location
let classSubReplyLocation = val.match(/(\w\w)={key:0,class:"sub-reply-location"}/)?.[1];

//主楼元素创建匹配正则
let repRegExp = new RegExp(`(\\(0,\\w+\\.\\w+\\)\\(\\w\\)\\?\\(\\(0,\\w+\\.\\w+\\)\\(\\),\\(0,)(\\w+\\.\\w+)(\\)\\("span",)(${classReplyLocation})(,\\(0,)(\\w+)(\\.toDisplayString\\)\\(\\(0,\\w+\\.\\w+\\)\\(\\w\\)\\),1\\)\\):\\(0,\\w+\\.\\w+\\)\\("v-if",!0\\),)`);
//回复楼元素创建匹配正则
let subRepRegExp = new RegExp(`(\\(0,\\w+\\.\\w+\\)\\(\\w\\)\\?\\(\\(0,\\w+\\.\\w+\\)\\(\\),\\(0,)(\\w+\\.\\w+)(\\)\\("span",)(${classSubReplyLocation})(,\\(0,)(\\w+)(\\.toDisplayString\\)\\(\\(0,\\w+\\.\\w+\\)\\(\\w\\)\\),1\\)\\):\\(0,\\w+\\.\\w+\\)\\("v-if",!0\\),)`);

let createElement = val.match(repRegExp)?.[2];
let toDisplayStringVar = val.match(repRegExp)?.[6];

val = val
.replace(repRegExp,
//主楼IP属地显示
'$1$2$3{class:"reply-location",style:{color:"#0CC"}}$5$6$7' +
//主楼隐藏评论标记
`(${replyInvisible}.value)?((0,${createElement})("span",{style:{marginLeft:"-10px",marginRight:"10px",color:"red"}},(0,${toDisplayStringVar}.toDisplayString)("该评论被隐藏"),1)):(0,${createElement})("span",{style:{display:"none"}}),`)
.replace(subRepRegExp,
//回复楼IP属地显示
'$1$2$3{class:"sub-reply-location",style:{color:"#0CC"}}$5$6$7' +
//回复楼隐藏评论标记
`(${subReplyInvisible}.value)?((0,${createElement})("span",{style:{marginLeft:"-10px",marginRight:"10px",color:"red"}},(0,${toDisplayStringVar}.toDisplayString)("评论被隐藏"),1)):(0,${createElement})("span",{style:{display:"none"}}),`);

val = val
// location唯一替换,IP属地还原
.replace(/(\w=\(0,\w+\.\w+\)\(\(function\(\){return)(!1)(}\)\))/, "$1 e.value?.reply_control?.location||false$3");

return val;

正则表达式的相关内容这里不再叙述,这边匹配的原理就是匹配各段代码之间的不同处理逻辑,而不是变量名,变量名会因为打包工具随便更改,所以我直接用的\w+\.\w+进行匹配,但基础的处理逻辑是基本不会改的

提取指定变量我是直接取的捕获组,用括号区分各个捕获组之后再用match进行匹配,可以直接以数组下标的形式把匹配到的内容提取出来
用正则表达式替换的时候也可以使用捕获组,在替换的内容里写$n就可以使用第n个捕获组里的文本内容

所以这里为了提取各种变量最多甚至用到了多达7个捕获组

我是在这里测试这些超级复杂的正则表达式的,这网站甚至会帮你把捕获组标出来,还能测试替换后的结果

个人空间关注/粉丝数查看上限突破+关注时间显示

讲真这里的逆向比刚刚评论区逆向简单得多,我就不(lan)用(de)放图演示了,直接口述吧

首先F12抓包粉丝/关注显示可以发现,get参数里有个ps=20,这个参数就是控制每页显示多少个人的。经测试可以发现最大可以设置为50。由于访问页数还是只有5页,因此针对其他用户,可查看的粉丝/关注数就由原先的5 * 20 = 100个提升为5 * 50 = 250

既然参数里有ps,那可以合理猜测发起请求时使用了一个包含ps属性的js对象作为参数,简单搜索ps:20即可发现相应的源码,直接查找替换成ps:50即可

至于页数计算,查看源码可知是由Math.ceil(this.curTag.count/50)写死的……

那就直接把/20替换成/50即可

关注时间显示这块,照例直接找关注界面的css class属性,可以很快就找到这部分源码。查看源码可知变量i很可能是粉丝对象本身,配合抓包内容可以猜测i.mtime就是关注时间。模仿前面的创建元素的代码,使用t._v(t._s())即可创建元素。注意对于部分较早期关注的粉丝,关注时间可能是不存在的,因此记得做空值判断

相簿界面发布时间显示

一样开局根据css的class属性直接找相关代码,比如可以直接搜查看次数的图标的classalbum-card__count-icon--view即可直接找到对应代码。同空间相关代码,可以猜测t.item就是每个相簿元素本身,根据抓包结果可以猜测t.item.ctime就是发布时间。模仿上文代码,使用e()即可创建元素。这里要注意,根据前文代码的staticClass,搜索一下可知这是vue的创建元素的参数。如果我们想添加内嵌样式,得使用staticStyle,而这个参数需要传递一个对象。最后,第三个数组就是元素的内部内容,同样仿造前文使用t._v(t._s())传入一个字符串即可创建对应元素,我这直接使用了new Date(t.item.ctime * 1000).toLocaleString()

总结

以上就是本插件的主要原理讲解。由于该插件的原理是直接修改b站的前端源码,因此我能想到的通用方案就只有使用能重定向请求的插件配合CDN实现了。希望以上原理讲解能给你的开发带来或多或少的帮助

文章作者: Light_Quanta
文章链接: https://lq0.tech/2023/02/27/bilicommentreverse/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 许可协议。转载请注明来自 Light_Quanta's Site