众所周知,每当我们在B站PC Web的大多数页面打开控制台,都会看到一个由字符画组成的小电视和"BILIBILI"的组合图案,右下角还有当前前端程序的构建信息。今天我们就来探究一下这个小电视是怎么画出来的。 提取对应逻辑(函数)首先我们点击控制台输出内容的右上角,找出输出这个小电视的initiator(发起者)。格式化之后可以很容易地看出输出小电视的是一个既无参数也无返回值的函数,它被绑定到了一个名为`__getClientLogo`的变量,并且在绑定之后立即被调用。至于为什么绑定到变量,而不是像下面的主逻辑一样写成立即执行函数,可能是因为在之后的逻辑中会再次调用,不过这个我们不关心。函数定义被写到了一个if里面,判断有无名为`ActiveXObject`的全局变量,用来排除使用ActiveX技术的早期IE浏览器。接下来看这个函数是否有依赖,可以看出只依赖了一个函数,叫做`_toConsumableArray`,从函数名以及上面的一系列相互依赖的实现可以判断出这是一个用来对各种可迭代对象和不标准的数组进行“格式化”的工具函数,于是大胆猜测可以直接去掉这个函数调用。 分析(化)逻辑(简)接下来将输出小电视的函数复制到编辑器,把这一长串base64字符串先剪切出去,保存在另一个文档里面,用一个变量text代替,以防编辑器卡顿。右键格式化一下,开始调教。 首先做一些准备工作:将`text`提取成函数参数;去掉对`_toConsumableArray`的调用;代码中并没有`this`,于是将全部函数改为箭头函数。 接下来将变量声明和赋值拆开成多个语句,并且先补全所有分号。这是一个需要胆大心细的操作,一定要全神贯注。 把作用于函数对象`console.log`的`apply`方法改为(es6+的)数组展开语法,并且移除变量`t`。 通过代入值,移除所有只被引用过一次的变量。 通过多次代入值,移除变量`a`。 通过直接展开数组,移除`concat`方法和数组展开语法。这两步同样需要胆大心细。 将解析后的`text`的`forEach`-`push`循环改写为`map`循环,注意这样改写后需要用展开语法代入到绑定到变量`i`的数组。 通过代入值移除变量`i`,并且改写箭头函数为直接返回形式(反正都是void)。 ![]() 到这一步为止,`text`还是一个黑箱,如果不解析`text`,最多只能化简到这一步。打开一个新的标签页,再打开控制台,解析一下`text`。 ![]() 可以看出,解析后的`text`是一个数组,每一项都是两项是字符串的元组(第一项是小电视,第二项是"BILIBILI"和构建信息),记作类型`[string, string][]`。区别于数组是相同类型的对象的有序集合,元组是长度和每一个位置的类型都确定的数组。 ![]() 将解析`text`的过程提取到函数体外,并将参数名改为input。接下来完成最后一步化简:将`t[0] + t[1]`改为数组的`join`方法。我还是那句话,不建议用加法运算符连接字符串,否则容易出现隐式类型转换从而导致意外的结果。最后为了验证一下,可以把文件扩展名改为ts,在函数参数处加上类型标记,无报错。还可以再用IntelliSense手动检查一下各个函数/方法调用的类型,确认无误。(将`text`变为字符串是为了防止ts模式下报错) 分析一下最终的这段代码。首先对输入的数组进行`map`循环,将每一项的元组中的两个字符串连接起来,并展开到一个新的数组中;这个新的数组除了那些展开的项之外,还有一个字符串首项`%c`,用来提示`console.log`对之后的输出内容应用第二个参数中的样式;这个新的数组中的每一个字符串又被连接起来,并且每一个字符串之间加上了换行符,组合而成的长字符串作为`console.log`的第一个参数;`console.log`的第二个参数则包含了输出所用的样式,而#00a1d6的色值正是B站蓝色;最终调用`console.log`,按照指定的样式输出内容。 至于原本的逻辑为什么要设计成那样绕来绕去的?编译时进行的代码混淆和es6+语法降级可能是原因之一,但是我个人觉得更可能是程序员为了装B故意写成这样的,毕竟控制台的小电视本来就是用来装B的(无误)。 验证见证奇迹的时刻就要到了。打开一个新的标签页,再打开控制台,先写下`const text = `,然后将base64字符串的内容复制过来。然后将最终代码的函数参数处的类型`: [string, string][]`手动擦除掉(也就是直接删掉),将`text`由字符串还原为变量引用,复制到控制台,回车运行。 ![]() 和原版完全一致,除了多输出一个`undefined`之外(多输出一个`undefined`是因为repl模式下会自动输出函数的返回值)。 简单的修改如果只想输出小电视,可以把函数`(t) => t.join('')`替换为`(t) => t[0]`,只取第一项的值;如果只是不想输出构建信息,可以替换为`(t, i) => i === 12 ? t[0] : t.join('')`,因为之前解析`text`时可以看到构建信息在`[12][1]`处,那么只需要在遍历到12时只取第一项的值即可。 ![]() 可以如此方便地改变输出,这也许就是数据结构是`[string, string][]`,而不是`string[]`甚至直接是要输出的`string`的原因之一。 |
|