最近一年的工作

时隔一年没有更新了,想把最近一年的事情记录吐槽一下。
我是在去年八月加入 G 公司的一个小团队,担任打杂万金油的角色,复杂构建、架构、攻坚预研等。我对这样的工作内容挺满意的,因为不需要对接业务开发,每天上班时很少人来找我;而且因为我在远程在深圳 office 支援 厦门团队的工作,所以日常交流形式主要是钉钉和视频会议。这样的「自闭」式开发体验,可能别人看起来会有点苦闷,但是我会乐在其中,我的听力问题就不太会影响工作。

去年底

去年10月到年底时,我主要在研究可视化网页编辑器的原型。这个编辑器会提供的能力是:将解析后的 PSD 作品通过拖拽几下就能得到一个响应式网页。因为 Leader 希望延续使用旧的技术路线,即复用平面编辑器的代码,所以我只能很别扭地在一个平面编辑器上基于 Yoga-Layout 去实现一个响应式布局引擎;原本写在 CSS 里面的内容,需要用 JSON 去描述。这个技术上也不太难,所以 DEMO 很快就能弄出。

除了这个,我还被分派去预研营销小游戏,比如抽奖页面,的一些技术;Leader 点名了几个技术,比如 WebRTC、ServerLess 等,然后就埋头研究去了。

今年初

在年初时,公司原有规划因为疫情的出现而有些变动。导致团队方向很久还没定下来。那时候我也比较闲,人闲下来就会想得比较多;所以那时候的我是比较焦虑的,为未来工作而担忧。

我那时抓住的救命稻草是 Draw.io 开源的流程图编辑器。按照我往常的做法,每次遇到用 ES5 写成的代码,我都喜欢改写成 T。这次也不例外,所以就把大部分代码改写了,也能正常运行。不过研究下去后,这个流程图编辑器太多领域特定的业务代码了。我也没有意向往这个小众领域发展,所以就浅尝即止。

年中以来

后来团队的发展方向定了,需要做一个网页编辑器,我就去写了这个网页编辑器的大部分代码——包括核心部分、UI 组件、整体页面等。后来有一位同事加入支援,所以就由他负责前端部分,我去写后端。

这是我第一次去正经去写后端代码,虽然它只是个大型 Hello World,不过我也接触了很多后端代码开发的知识。

最近

最近 Leader 找来了一个新活,我们去年谈到的互动项目可以启动了,所以一年前对 LeadCloud 游戏 SDK 的研究结果能用上了。

LeanCloud 的后端服务和部分游戏服务规格不能满足我们业务要求,所以我需要根据 LeanCloud SDK 接口去实现一个 API 兼容的后端服务。由于之前有所研究,而且也有些后端经验,我两周就完成了这个服务;简单的测试是倒是挺容易通过,不过对于这个用于管理 WebSocket 链接的 Broker 类型的服务来说,并发下会有很多考验。我还需要了解更多后端知识来保证服务的安全运行。

最近一年的个人研究

这一年来,由于工作原因,我接触的技术有

  • Yoga-Layout: RN 的布局引擎。
  • WebRTC: Leader 想推,但是在现实网络情况下不太实用的方案。
  • 阿里云函数计算:一种 Serverless 方案,开发体验说不上便利。
  • EggJS: 阿里的后端框架、用起来确实挺方便的,可以抛下成见去试一下。

上面的都是因为工作原因而去研究的。在业余,我主要是研究了这几项:

流程图编辑器

Draw.io 提供了在线流程图编辑器,而且开源了他们的代码。出于技术崇拜和好奇,我研究了他们的代码。

他们的是用 ES5 写的,对于这样的代码,我都喜欢改成 TS 进行研究。所以就埋头弄了几个月,将他们改写完,而且也能正常地跑起来。

不过这并没有给我带来太多喜悦,他们的编辑器代码是在有太多和流程图编辑相关的业务代码。如果把这样的代码都清理掉,剩下的架子并没有太多价值。所以我尝试去写一个自己的编辑器框架。

公司现在是有一个核心编辑器,其他业务可以用它来进行扩展。不过这个编辑器的代码扩展性是在太差了,在代码体积上来说,多其他业务是一种负担。所以我根据在这时研究了一个基于 TS 的框架,参考了 VSCode 代码做了一下全部 class 都能注入覆盖的设计。

这套 class 注入系统是我的研究成果;虽然他没有机会发挥,也没能在公司实际业务中去落地,不过以后应该有机会的吧。

CSS 广告编辑器

这个广告编辑器是我在网页编辑器弄完之后的空余时间里研究的。国外有些「古典」的 GIF 小广告,所以这样的广告编辑器可能会有些需求。出于给团队找活的目的,我就自己研究了一下。

这样的编辑器实现起来应该不难,我们团队的 H5 编辑器改一下应该就能做成。但是对于隔壁组提供的核心编辑器的厌恶。我想写一个 100% 可控的编辑器。

因为之前有写 Canvas 和动画经验,我就用以前的代码改写了一下,最后挺简单地就能兼容别人家的数据结构、实现动画播放。而且比起原来的编辑器,还实现了时间轴的 Seek 功能。

这个研究很快就结束,因为可能前景不佳。

视频编码和 FFMPEG 和 WASM

有个新闻是某个在线编辑网站拿了挺多风投,公司也开始在视频编辑方向发力。虽然我不是视频组的,但是多媒体相关的技术都没有研究过。

我从零了解了容器格式、编码格式这些基础内容,也编译了个 FFMPEG 的 WASM 版本用于播放 H.265 编码的视频。我甚至想去了解 MP4 的 SPE。不过出于实用角度考虑,需要为一个有商业价值的产品去研究技术。

我想到的是 LogRocket 那样的操作录屏,通过前端截图去完成记录用户操作。通过 HTML2Canvas 去做前端截图,然后用视频编码技术去压缩传输。不过考虑到 HTML2Canvas 和前端视频编码都效率不佳,就放弃了。

JS 沙箱

我入职 G 公司之后就开始做插件机制;但是插件并没有运行在沙箱,而且直接在主线程运行,所以会有安全问题。

最近公司的前端基建团队讨论了微前端方案,还有就是阅读了 Figma 的技术 Blog,我现在开始研究 QuickJS VM 打包成 WASM,然后配合类似 WorkerDOM 的设施完成一个安全的 JS 沙箱。

从上周遇到的 CORS 问题说起

数了一下,已经有两个月没写了,这份工作入职后就在忙着工作的事情和自己的 Side Project, 所以之前那种为了写而写的文章,就没有太多愿望去写。但是在上周工作中,遇到一些事情,还是想记录一下。

我上周在做一些 URL 迁移的工作,就是把前端页面的地址从a.b.com改成b.com/a, 代码改动也算顺利;但是这个改动会涉及公司的其他系统,所以有一些跨团队协调配合的工作。其中,和某团队的同事干脆面(化名)的合作,是令我比较尴尬的。

在公司,有三套环境可以用来发布上线,测试、预发、生产。我在一开始,就把这次需要的改动写下来,告诉其他同事如何配合。这位干脆面在测试环境的改动也算顺利,不过发现了一个不是由于这次改动产生的 400 错误,本着负责任的态度,我也积极配合解决问题。这个问题呢,我那边也能复现,然后一顿操作完,又没了,我们双方都没能再次重现,然后把它当作后端问题没管。

到了预发环境,这个问题还是存在,而且因为干脆面访问我们页面没有使用 https,我硬编码了请求的 BaseURL 出现了 CORS 问题,然后他就语气急了。

我刚开始时没想到是 Https , 被追问后没有想到原因,所以感到压力。我现在相当于 Remote 工作,算是一个可有可无的边缘角色,内心是有点慌的。然后就不加辨别地找了其他同事,问了些低级问题。

冷静下来后,我重新看了一下 W3C 的文档, https://www.w3.org/TR/cors/#resource-preflight-requests, 发现我对 CORS 还停留在水过鸭背的阶段,遇到问题就忘了背过的知识点,对自己有点失望。

不过工作还得继续,总结经验让以后的路更好走吧:

  • 别被同事的情绪影响
  • 遇到问题先分析,别急着回复,宁愿慢也不要出错
  • 继续深入研究,做个可靠的专家

键盘入坑记

公司给前端配的是 13’ MBP,每天都得低头使用,所以我打算买个笔记本支架把它架起来,然后外接键盘和鼠标使用。这次需要把笔记本抬高,选了一款完全脱离桌面的支架。有的同事是使用斜面很高的支架,然后手掌要在倾斜 45 度的斜面上输入;我试了一下,这样的手掌姿势不太舒服,所以没选这种。

我之前用的都是旧款 15’ MBP, 旧款的蝶式键盘用起来挺适合我,家里用了几年的 XPS 的键盘也挺好用,所以并没有使用外接键盘的习惯。我上一次使用外置键盘,是高中时用家里的台式机,从大学以后就在使用蝶式键盘,有点习惯了这种键程超短、比较省力的键盘。

很多人会选 HHKB 来配 MBP, 看起来逼格满满。但是我不喜欢背 Vim 的快捷键,也不习惯没有方向键的键盘,还是不要装比较好,不然连怎样退出 Vim 都得上 SO 去问,太尴尬了。HHKB 的颜值倒是不错,可以参考一下,选键盘要按键数往低里选。

然后我找到 FILCO minila air 67, 这款 67 键的键盘有方向键,颜值也比较高,虽然自带的绿色空格键比较丑,不过可以自己换键。这是一款机械键盘,提供青轴、茶轴、红轴等版本。第一次接触这些轴,有点懵,上知乎学习一下。了解它们的特性后,考虑到现在办公室静得诡异的气氛、连新款 MBP 的键盘觉得响,有点不敢买。后来在厦门体验旁边的同事使用红轴 IKBC, 确实声音挺响的。

最后我选的是一款国产静电容键盘。静电容键盘比较有名的是 HHKB 和 Realforce,但是 HHKB 没有方向键;而 Realforce 实在是贵得下不了手。作为第一把键盘,选错和弃坑的可能性很高,没必要一开始就投入重金。有的人还会先使用试轴器确定适合自己的键轴后,才选适合自己的键盘。这种选购方式挺好的,可是我要买的键盘没有试轴器,只能凭想象去选。我按现在的使用习惯,首先考虑敲击省力。NiZ 提供了 40g 压力和 35g 压力两种版本,感觉差不多,选了一个低的数值;键盘布局有66、82、87、104几种,需求点是要有方向键、键数少,排除66和104键。82和87对比,87的更便宜,但是听说由于键的尺寸问题,换键会比较麻烦,最后选了价高的82键盘。

拿到手后感觉压力数选少了,一个按键动作,抬起手指要比按下按键要费劲。还有是,使用了蝶式键盘这么多年,感觉键盘的键程有点高,需要重新适应一下。键盘的底盘比较高,我买了块木质的手托,这样手掌就不容易蹭到最下面一排的按键。

图表的 Scale

Scale 是图表的核心部分,它用于数据和显示的相互映射,数据点绘图位置需要用 Scale 确定,从画布的位置反过来获取对应位置的的数据点,也需要 Scale。Scale 通常是实现了如下的接口:

1
2
3
4
5
6
interface IScale {
domain: [number, number]
range: [number, number]
toDomain(rangeValue: number): number
toRange(domainValue: number): number
}

domain 是原始数据的范围,如同函数f(x) = y,domain 相当于 x 的取值范围,即定义域;range 相当于函数的输出范围,即值域,在显示时就是 canvas 画图的坐标范围。比如将一组[1, 2, 3, 4, 5, 6]的数据映射到画布的[0, 50]范围,我们可以说,domain 是[1, 6], range 是[0, 50]

将上面这组数据进行绘图时,要用到toRange方法,如果 Scale 是线性的,f(domain)= range = A * domain + B 中,A = 10, B = -10, 对于每一个 domain 的输入值,其输出是:

1
2
3
4
5
6
7
f(domain)= 10 * domain - 10 = range
f(1) = 0
f(2) = 10
f(3) = 20
f(4) = 50
f(5) = 40
f(6) = 50

这是toRange的计算。相应地,toDomain的计算是:

1
2
3
4
5
6
7
8
f(domain)= 10 * domain - 10 = range
g(range) = 0.1 * range + 1
g(0) = 1
g(10) = 2
g(20) = 3
g(30) = 4
g(40) = 5
g(50) = 6

这样就完成了线性映射。现在图表里只用到了线性映射,假如要做 Log Scale 功能,就要参考一下 d3 的实现。

平移

在平移时,range 的范围并没有发生变化,需要变化的 domain,假如画布需要向右移动 10px,原有的画布范围是 50px 宽,显示的是 domain = [1, 6], f(domain) = 10 * domain - 10, 在平移后,f(domain) = 10 * domain。这时,如果要显示 range = 0 的点,需要 domain = 0;如果需要显示 range = 50 的点,需要domain = 5。所以新的domain = [0, 5]

另一种计算方法是

1
2
f(domain) = 10 * domain = range
g(range) = 0.1 * range = domain

得到

1
2
g(0) = 0.1 * 0 = 0
g(50) = 0.1 * 5 = 5

和上面的分析方法相同。

缩放

在平移时,range 的范围并没有发生变化,需要变化的 domain。假如需要画图当大一倍,中心点是 25px,则:

1
2
3
4
5
6
7
8
9
对于原来的domian, 在放大后
f'(range) = (range - 25) * 0.5 + 25
f‘(0)= 12.5
f'(50) = 37.5
只显示原来在[12.5, 37.5]的range区间的domain值
因为f(domain) = 10 * domain - 10
即 g(range) = 0.1 * range + 1 = domain
g(12.5) = 2.25
g(37.5) = 4.75

所以在放大后,domain 变为[2.25, 4.75],只需要 2.25 到 4.75 的值,就能填满画布。

放大一倍要乘以 0.5,可以理解为固定 domain ,将显示的窗口作倒数倍数变化;比如放大一倍,相当于显示原来的一半区域就满足要求。

改变画布大小

改变画布大小时,range 的范围作等量的变化。比如画布增加 50px ,那么新的 range 为[0, 100]

对于 domain 有两种处理方式,一种是 domain 不变,必然地各个点之间的距离变大,出现拉伸效果。
另一种处理是,domain 也跟随变化,保持显示比例。

1
2
3
4
因为f(domain) = 10 * domain - 10
即 g(range) = 0.1 * range + 1 = domain
g(0) = 1
g(100) = 11

所以 domain 范围变为[1, 11]

上述计算结果中,domain 的取值可能为负数,这里的正负并没有太大意义。在实际应用中,比如@antv/g2,会将 domain 和 range 归一化处理。

记一次在V2EX的回帖经历

这是发生在一个多月前的事情,某天中午我在V2上看到有人问JS里toFixed的返回值问题,这里。我在之前稍微看过ECMAScript的Spec,所以对这个问题有点了解,所以就随便回复了一下需要查Spec。但是回复的走向并没有想象中的学术展开,反而是有人引用了道听途说的资料,做了误导性的回答。我只好把自己吞下的知识,“反刍”出来喂给他们吃。

看到这样的情景,我觉得有点无奈,那是,当碰到知识盲区时,得到的回答不一定是正确的答案。

我也算有自知之明,知道自己的技术知识体系并不严密,所以会想着去补充完善。但受限于各种因素,平时都只能功利性地去学习零碎的知识、看一些所谓的文章。刚开始时还有点新鲜感,但是看多了就会觉得千篇一律。它们的套路都差不过,对他人的文章排列组合一下,又能水一篇。这样的松鼠病患者撰出来的文,是不是只为了满足收集癖?我自己也在Blog里写些不太上得了门面的博文,出于对“松鼠文”的厌恶,所以写的都是自己经历过的项目里的事情、自己求证过的知识。但是呢,市面上还有很多收集站,专门做一些二手文章转载的事情,作者不详、写作时间不详。若是鉴别能力稍差,很容易以深信了里面写的内容;正确的还好,要是错了就贻笑大方了。

可能是S1上得多,我的“冷无缺”纯度越来越高了。其中的“无信仰”特质,使我不会在短时间内去信任网上的内容。缺德网友可以因为各种原因发表各种大胆言论,但是这边的我应该是谨慎判断、或者无视。一个例子是B站的弹幕,多少无常识言论在那里,一条错的弹幕,舆论风向还得反转两次才扭过来。我的网络生活,很大一部分时间是在看各种文章,也许是精彩的独见,也许是隔夜的呕吐物,也可能是我没发现的毒药。在逻辑求证上,我也只是个不太高明的初心者。刚接触一本书,叫做《批判性思维工具》,好像可以读一下。

PS:写这篇是为了督促自己看书🙃。

我不知道的前端知识-kepler.gl的近大远小轮播

今天访问了kepler.gl这个网站,看到它的轮播挺有意思,就研究了一下。轮播已经是个old school的话题,做移动端页面或者做官网的同学,应该是熟得不能再熟的技能,但是我这两个都没做过,所以有点好奇。
首先,页面上这个轮播,有个初始化过程。轮播的图都是叠合状态,当页面滚动到它的区域时,聚焦到中间顺序的图,左右的图稍微露出一部分,且会有一个从视觉上向后方运动的过程。
这个动画里,视觉上的远近效果,主要是用了CSS Transform 3D 的perspective(透视)函数,具体用法参考MDN;除了perspective,Transform里还用了translate3d,比如聚焦的图,Z会比较高,而作为背景的图,Z依次递减。
组件的初始化,其实和图片懒加载使用了相同的技术,就是监测Element是否进入Viewport,当Element进入后,做相应的处理,改变样式或者加载图片。这个交互的监测,有挺多LazyLoading/ScrollLoading的插件,原理是页面目前的位置和元素的位置进行比较;或者使用ntersection Observer API。kepler.gl是基于React的,可能用了React-Reveal,后者是两种方法都兼容。
前面的方法,可以有两种实现方式。一是获取页面绝对的滚动值和元素相对于page的绝对位置,通过遍历元素parent,累加offetTop;另外一种是获取element.getBoundingClientRect()里的top值,如果大于0且小于window.innerHeight,则是在viewport里全部或者部分显示。

Mindmap 挑战

最近在V2看到XMind的招聘信息,然后就突发奇想,想试一下自己写一个Mindmap。

为了防止闭门造车,我先在Github上找找可以借鉴的项目,比较有名的是百度脑图Kityminder。Kityminder是由几个部分组成:底层处理SVG的Kity、核心逻辑部分Kityminder-core、外面的Kityminder-editor等。

我花了点时间研究Kity,发现Kityminder里面只用到Kity很小的部分,当初我写Canvas图表前,倒是花了很多功夫写Canvas绘图库;对比之下用SVG的话,的确省事很多。然后Kityminder-core这个项目里,核心的layout逻辑也只是很少的一部分。Kityminder太多不想看的代码。

然后找到Antv的hierarchy,这个repo是只做Mindmap layout的,给出了经典的layout几种变形。我读了一下,其中的基本思路是树遍历。它适合输入数据然后输出图像的模式,不是我想要那种,在交互时动态更新显示的模式。

所以我开始自己写。刚开始时,凭直觉地写了巨大一个Node类,这个类既有model数据,也有view数据,还有很多很多方法。这样写倒是能工作,写layout算法时也很方便。不过代码的确太暴力了,需要分离model和view的数据。

在写Canvas图表时,我是有把源数据和变换后显示用到的数据分离,只是Canvas绘图区域可以一次清理后重新绘制;而SVG还有一个view数据和DOM节点绑定的问题,像@antv/hierarchy,它的demo就是直接用canvas绘制,没有给出和DOM节点结合的方案。我暂时是采用MVC而不是MVVM的思路(不知道该怎样划分View和View model)把数据分离为model的数据和view的数据。Model是树状结构,view也是树状结构;但是DOM节点不是树状结构,每一个节点都是容器元素的子元素。

Model节点和view的节点该如何关联?我开始想了用ID的方案,通过在树中遍历出节点来达到双向映射。但是这方案看是来太慢了(虽然没测过benchmark)。后面想的是,Map可以用Object作为key,可以用Map来存节点的关联关系;如果需要考虑浏览器兼容性时,可以降级为用ID为key。想象一下,两个’A’字母叠在一起,上面的’A’各个顶点都对应着下面的顶点,这就是我需要的数据结构。这个方案的好处是,不需要在暴露给用户的类里带上太多和内部实现相关的数据和方法。比如用户在使用时,有这样的代码:

1
2
3
4
5
6
let map = new Mindmap(document.getElementById('container'));
let topic = new Topic();
map.addTopic(topic);

let subTopic1 = new Topic();
map.addTopic(subTopic1, topic);

里面的Topic类里,只要求实现ITopic接口:

1
2
3
4
5
interface ITopic {
parent: ITopic | null
children: ITopic[]
content: any
}

这样,设计处理的API会简洁很多,用法也很清晰。

就如同上面的例子,我的Mindmap API设计上适合动态添加/删除节点。在内部实现里是,节点有改动时,依次影响的是自身尺寸、自身连同后代节点的总体大小、兄弟节点的位置、父节点的总体大小等。这个过程连续向上,直至根节点。这样的更新逻辑还能再优化,响应更快。

在实现节点渲染时,我使用了SVG的<foreignObject>,然后里面渲染普通的DOM节点,如<textarea>。我试过把这些节点用普通地挂在一个div下渲染,用CSS Transform, 然后连线用SVG,效果挺好速度也很快;但是看了有些文章,里面提到这种混合不太适合SVG导出,才改为全用SVG,缺点就是Mindmap节点渲染起来慢很多,所以我需要使用上面的按需更新策略。

最后这个Demo还需要加上快捷键,我抽取了VSCode快捷键绑定相关的代码来使用,现实了和XMind一样的快捷键。最后完成的代码看这个。这个还是简单的,接下来我想把它做成Coggle那种可以在节点进行富文本编辑的形式,而不是单行输入 + Image + Marker + URL 这种,不过面对富文本大魔王,还需要更多准备。