java8部分理解
请添加个描述吧
或许对于现在很多人来说,浏览器是一个很少使用的 App,因为所有功能都可以在 App 做,不需要使用浏览器。一方面,是由于国内的大环境就是把用户当成傻瓜(事实上,即便是吾辈目前所在的公司,在 UI/UX 设计方面也倾向于将用户当成傻瓜),导致用户真的是越来越傻瓜,甚至遇到了除了微信其他的都不怎么会用的地步,更何况浏览器这种“高端”的 App 呢?
然而对于某些特定人群来说,尤其是不喜欢安装太多 App 的人来说,浏览器几乎是 App 上数一数二的应用了,甚至称之为互联网的入口都不为过(现在一般的傻瓜式用户大概认为微信才是吧,缅怀曾经互联网都是通过浏览器使用的时代)。
在使用过 UC => Chrome => FireFox => Kiwi browser 之后,吾辈可以确定的说,Kiwi browser 是目前 Android 上最好的浏览器,其中尤以支持 Chrome PC 版的 Plugin 最为有名。
在早期吾辈使用的浏览器中,UC 绝对是当年(2016 年之前)使用体验非常好的浏览器,直到后来被阿里系收购,导致它逐渐充斥着各种广告,甚至首先开辟出了浏览器收费这一跨世纪倒车壮举。


附:这里的跨世纪倒车壮举指的是自从浏览器始祖 Netscape 被 MS 的 IE 使用系统捆绑 + 免费的策略干掉以后,后来流行起来的浏览器还从未出现过收费的(或许是吾辈孤陋寡闻了?)
附:所有的国内浏览器都有内置的黑名单,例如 GitHub 上的 996 icu 就被国内浏览器屏蔽了,不仅仅是墙,就连浏览器都是墙的帮凶。参考:国外网站无法访问?
UC 国际版,这是一个比较有趣的版本,没有国内版那么多花俏无用的功能,但同时不支持简体中文(当然支持繁体中文),同时保留了 UC 的核心功能——怪不得手机上 UC 的使用占有率很高呢!
它默认支持的主要功能如下


看起来没什么太大的问题,然而,它却在隐私方面做的一如既往的烂,参考:美媒:中国人上网须防泄露隐私 浏览器存安全隐患
后来,吾辈在 PC 上先后遇到了 FireFox 和 Chrome,简直比国内的各种浏览器干净整洁了一百倍。在实际使用过相当长的一段时间后,吾辈最终在 PC 上选择了 Chrome,手机上也安装了 Chrome 进行网页浏览,它的速度非常理想,使用体验也还算不错,尤其是翻译功能更是被完整地保留了下来。唯有一点,插件功能被整个砍掉了。



为什么 Chrome For Android 不支持插件?虽然官方说是为了使之能够在较旧的浏览器上也能正常运行,但实际上应该有两个原因
- Android 是 Google 自己的,而 Google 对于 App 的控制力要超过网页,所以为了让用户使用 App,Google 不原因让用户更多的浏览网页。
- 大约 70%的 chrome 用户在 android 和 ios 端,只有 30%的 chrome 用户在台式机和 mac 上。谷歌知道,如果他们在智能手机上引入扩展功能,那么由于广告拦截器的存在,他们将损失很多钱,而且大多数互联网用户由于舒适性,大部分时间都倾向于将智能手机用于互联网。Google 在这里玩一个聪明的游戏。参考:https://www.quora.com/Why-arent-there-any-extensions-for-Google-Chrome-on-Android
所以,在知道 FireFox 在 Android 上亦支持插件后,吾辈感觉它就是吾辈理想中的手机浏览器,吾辈在手机上也开始尝试使用 FireFox 了。但事与愿违,它真的太慢了。。。在使用体验上和 Chrome 无法相提并论,而且翻译插件的支持不完善导致有时候想在移动端阅读大量英文网页时还需要切到 Chrome.
注:FireFox 的桌面版插件支持存在一些问题,同时插件官方一直未曾修复,这也是吾辈放弃 FireFox 的重要原因之一。


最终,吾辈遇到了 Kiwi browser,诚然,它也不是十全十美的,也有很多缺点。
但有一个及其突出的优点,足以掩盖以上的所有缺点 – 支持 Chrome 桌面版插件。


事实上,吾辈也曾断断续续地使用过一些微型浏览器,在功能上做的比较简洁(但并不意味着不够用),但内核却薄弱不堪(其实就是用系统默认浏览器的内核)。
包括但不限于以下列表
其他吾辈没有使用过的浏览器可以参考:手机浏览器有哪些?安卓平台良心浏览器推荐
虽然这些微型浏览器理念很好:少即是多,简单就是美!,但实际上,这些没有内核的浏览器很多地方,尤其是性能方面,仍然受限于系统默认浏览器。大多数自带的插件系统,往往只是实现了一套加载 *.user.js 的机制罢了,而且生态之小与 Chrome/FireFox 这些主流其相比往往是天壤之别。
桌面的 Chrome 使用建议可以参考:优化 Google Chrome 的使用体验
有时候需要使用二维码扫描使用手机去打开某个网站,而 Kiwi 并没有自带这个功能,所以需要配合 App 二维码扫描 食用。
虽然名字听起来就像是一本正经的胡说八道,但吾辈确实遇到了一个奇怪的问题,于此分享给大家。
事情的起始如下
下班回家 => 想要看动画 => 去动漫花园下载 BT 种子 => 动漫花园一片空白 => Why?

于是吾辈偷偷的的打开了控制台看了一下,发现是页面中的内容元素不见了。经过深思熟虑(好吧其实也就是稍微想了一下)首先把 uBlock 禁用,毕竟这个最容易被网站检测出来并且对抗嘛!果不其然,页面恢复了正常,但。。。同时广告也出现在了页面上。

这可不行,重新启用了 uBlock 看了一下分析,很显然,内容不存在大概率是被 uBlock 的元素过滤功能隐藏掉了,查看被隐藏的内容元素,发现 id 为 1280_adv,但同时又包含了广告与主体内容,所以只要关掉 uBlock 的元素过滤就可以避免正常内容被误杀了。
之所以不在该网站整个禁用掉 uBlock 的原因在于 uBlock 并不只有元素过滤,它还阻止了一些广告资源的加载,仅在动漫花园就包括但不限于 _baidu.com, bebi.com, histats.com_。显而易见,禁用它们还能提高加载速度。

既然无法使用 uBlock 的元素屏蔽了,那么吾辈便需要使用一个新的方式去阻止广告了,幸运的是吾辈安装了 Stylus 和 Tampermonkey 插件。
Stylus 能够使用被称为
user.css的技术,能够在本地修改任意网站的样式 – 即自定义 UI 显示。
而 Tampermonkey 则更强大,支持user.js– 可以在本地打开任意网站时载入自定义的 JavaScript 脚本,不再局限于修改 UI,几乎与插件无异(事实上它也确实被认为是更轻量的插件)。
原以为就几句 css 的事情,找到了广告的 id,于是吾辈写下了下面这些 css
1 | /*屏蔽动漫花园的广告*/ |
但结果却是。。。只生效了一半!

可以看到,上面两个广告确实被隐藏了,但下面一个却并没有,而且吾辈在控制台直接使用 document.querySelector('#1280_adv') 也获取 dom 会抛出错误 SyntaxError: Document.querySelector: '#1280_adv' is not a valid selector。吾辈是直接复制的 id,理论上来说不会有错才是。
仔细想想,吾辈或许是漏掉了什么。。。于是,吾辈使用 Copy => Copy Selector 功能,有趣的东西出现了,复制出来的内容竟然是 '\31 280_adv',wtf?
嗯,或许吾辈需要冷静一下,尝试使用 document.querySelector("#\\31 280_adv") 获取一下
注意:这里 JS 里面去查询 DOM Selector 的字符串又进行了转义。

OK,确实能够正常拿到。由于这些奇怪的字符在 css 中存在语法错误,那么接下来便用 user.js 去屏蔽掉它们吧!
基本实现如下
1 | ;[ |
甚至吾辈都发到了 GitHub 与 GreasyFork 上了。
然后,有个(万能的)网友就提出,可以转换思路,既然 #\\31 280_adv 在 css 中存在语法错误,那么使用属性选择器过滤 id 将值包裹在 '' 之中不就好了么?此话真是九言劝醒迷途仕,一语惊醒梦中人,吾辈瞬间 GET 到了这个点。
于是吾辈编写出了下面这段 user.css 样式
1 | /*屏蔽动漫花园的广告*/ |
使用后效果如下

现在,初始目的达成了,吾辈开始有点好奇它是怎么实现这个功能,于是下载了它的源码,id 那里并未发现什么奇怪的东西,但吾辈却也无法复现一个 demo!
1 | <!DOCTYPE html> |
demo 效果

如果有人知道原因的话,请务必不吝赐教!
参考:ASCII Wiki
后续,万能的网友 NiaMori 又来说明啦,实际上是 id 以数字开头的原因,具体问题参考:
是 id 以数字开头的原因,简单的<div id="1">test<div>就能复现这个效果。document.getElementById('1')能够选中,但document.querySelector('#1')不能,因为 HTML5 允许 id 以数字开头而 CSS 不允许
0x31 是 ‘1’ 的 Unicode 编码值,Copy selector 的时候 Chrome 做了一个智能的 escape
参考:
在业务需求中不希望用户保存图片,因为是一些供内部使用的图片。
注:以下内容使用 react+ts 实现
简而言之,这是一种简单有效的方式,能够在用户不打开控制台的情况下阻止用户保存图片。
1 | export function preventDefaultListener(e: any) { |
另一种思路是将图片转换为 canvas 避免用户使用 img 相关的操作。
将图片转成 canvas
1 | export async function imageToCanvas(url: string, canvas: HTMLCanvasElement) { |
禁用 canvas 事件
1 | const throwFn = () => { |
如果能禁止用户操作控制台,那么自然能够避免用户查看源码了,下面是一个简单的实现。
1 | /** |
1 | useEffect(() => { |
该功能需要服务端配合,故而此处赞不实现,可以参考 微信读书,就是将文本转为 canvas,数据传输也进行了加密,可以在很大程度上防止普通用户想要复制/下载的行为了。
如同所有的前端限制用户的技术一样,这是一个没有终点的斗争。。。
参考广告屏蔽和屏蔽复制粘贴的发展。。。
由于需要在 Browser 进行大量的(音频转解码)计算,所以吾辈开始尝试使用 webworker 分离 CPU 密集型的计算操作,最终找到了 comlink 这个库,但之前在 vue 中使用时发生了错误,目前看起来已经得到了解决,所以在此记录一下。
最后决定使用 comlink 结合 worker-plugin 实现简单的 worker 使用。
在 GitHub 上有 可运行示例 demo
相关问题:comlink-loader 工作不正常
1 | yarn add comlink |
1 | { |
这里一般不需要特殊参数配置,如果需要,可以参考:worker-plugin
添加一个简单的 hello.worker.ts
1 | import { expose } from 'comlink' |
在 main.ts 中使用
1 | const obj = wrap(new Worker('./hello.worker.ts', { type: 'module' })) as any |
但这里并不是类型安全的,所以我们可以实现正确的类型。
添加一个 hello.worker.ts 暴露出来的类型 HelloWorkerType
1 | export interface HelloWorkerType { |
同时为了支持在 main.ts 中使用正确的类型,需要使用泛型
main.ts 修改如下
1 | const obj = wrap<HelloWorkerType>( |
声明函数的类型 HelloCallback.worker.type.d.ts
1 | type ListItem<T extends any[]> = T extends (infer U)[] ? U : never |
声明一个纯函数 HelloCallback.worker.ts
1 | import { MapWorkerType } from './HelloCallback.worker.type' |
注:此处最好使用变量的形式,主要是为了方便将函数类型剥离出去。
在 main.ts 中使用
1 | const map = wrap<MapWorkerType>( |
声明接口 HelloClass.worker.type.d.ts
1 | export class HelloClassWorker { |
worker 文件 HelloClass.worker.ts
1 | import { HelloClassWorker } from './HelloClass.worker.type' |
关于此处
implements class的问题,吾辈偶然一试之下没报错也很奇怪,所以找到了相关问题 Typescript: How to extend two classes?,官方文档也同样说明了这个特性 Mixins。
在 main.ts 中使用
1 | const HelloClassWorkerClazz = wrap<typeof HelloClassWorker>( |
总的来说,使用 worker 的基本分三步
注:当然,如果是复杂的东西,可以直接在单独的文件中实现,然后声明一个 .worker.ts 暴露出去,不在 .worker.ts 中包含任何
在编写一个重载函数时,吾辈发现了 ts 的方法签名问题。
1 | enum TypeEnum { |
上面是一个简单的重载函数,吾辈希望在输入第一个参数 type 之后,ts 就能匹配到正确的参数,然而事实上,ts 并没能完全做到。

当然,如果真的这样写 ts 的类型检查仍然能正确地抛出错误消息,然而未能推导终究是有点问题的。
1 | // TS2769: No overload matches this call. Overload 1 of 2, '(type: TypeEnum.A, obj: A): void', gave the following error. Argument of type '{ a: string; b: number; }' is not assignable to parameter of type 'A'. Object literal may only specify known properties, and 'b' does not exist in type 'A'. Overload 2 of 2, '(type: TypeEnum.B, obj: B): void', gave the following error. Argument of type 'TypeEnum.A' is not assignable to parameter of type 'TypeEnum.B' |
然后,吾辈想到了几种方式可以尝试解决。
尝试使用继承限制字段的类型。
1 | //region 对象参数 |
很遗憾的是,这是行不通的,即便是下面的这种变体,仍然是不可行的。

1 | interface Base<T extends TypeEnum> { |
事实上,使用泛型确实可以做到让 ts 的类型更加 正确。

缺点:
1 | //region 泛型 |
最后,高阶函数可以简单的解决这个问题,它将一次调用更改为两次调用,第一次调用返回的函数便已经确认了类型。

缺点:
1 | //region 高阶函数 |
总的而言,泛型和高阶函数都能解决这个问题,吾辈个人倾向于泛型,因为它并未改变调用者的使用方式,而是让作者去改变,避免改变函数的接口本身。