使用 React 开发邮件模板

刚来土澳的时候很不习惯每天检查邮件,大小事都是邮件,在国内很多时候都是手机短信通知或者应用内通知。在澳洲待了几年,也习惯了每天查看下邮箱。现在这家公司绝大多数于用户沟通都是通过邮件,包括关键的用户通知,还有营销推送。我们每天发送大量的营销邮件,公司使用 Orrto 这个平台,这个工具可能很少人听说,举个场景:用户在我们网站上进行了一些操作,填了邮箱信息,就成了一个潜在用户。我们会在 Ortto 中创建一个 Journey,这个潜在用户进入这个 Journey 之后,我们会开始会发送一些邮件,过几天会再次发送一封类似的邮件提醒,当然在此过程中他已经完成了相应订单,我们就要将其从这个 Journey 中移除。总的来说,发送邮件的场景很多,我们的邮件模板梳理也非常多。

阅读更多

对当前前端技术栈的理解

最近被问到对一些对技术的理解,虽说很多时候自己感觉对技术选择有自己的理解,知道背后的原因,但还是不大能说的上来。这儿就总结一下对所经历的一些技术的理解吧。谈到理解,总免不了介绍技术出现的原因,解决的问题,可能的方案以及未来的可能的形态。比如,不少人认为前端玩的一些概念是在后端存在很久的东西,这个现象也确实存在。虽然 web 技术诞生有一定的年头了,不过就前端而言,可能要在 2005 年之后 Ajax 出现之后以及 SPA 成为趋势之后,才算是真正的开始发展。JS 和 CSS 存在的问题也才逐渐的突出起来。

阅读更多

Distribute tracing - newrelic

newrelic 提供的 Distribute tracing 功能非常实用。阿里内部使用的是鹰眼系统,因为一直做前端开发所以我并不是很清楚到底怎么工作的。虽说之前也多少翻过一些文章介绍大概的系统的架构,但是一直以为在调用下游服务时是显式传入当前 TraceId 的。直到最近使用 newrelicDistribute tracing 追踪才了解到还有一种无侵入的方案。

比较流行的全链路监控方案 Zipkin Pinpoint Skywalking 也有完全无侵入的方案,但都是 JAVA 编写的。而且在搜寻对应的 Node.js 实现时,找到的仓库代码往往并不齐全。无奈只能在 newrelic 源码中寻找线索,果然发现大量的 instrumentation,其中就有对原生 http模块上方法的拦截。这样就可以解释为什么我们不需要显式传给下游 TraceId

或许不难猜到,在发起新请求时,只需要把当前的 traceId 附带到请求头上即可。在接收请求时解析请求头上信息,并将其传给当前新建的 transaction 作为 parentId。
但是还有一个问题:如果有多个请求同时处理,如何保证传给下游正确的 TraceId。假设服务端的伪代码是这样的:

1
2
3
4
5
6
app.get('/frontend', (req, res) => {
asyncTask((is) => {
const result = await http.get('/downstream');
res.json(result);
});
})

如果同时有两个请求 /frontend(1) /frontend(2) 怎么保证调用链不串呢?因为我们并不能保证异步任务的时间,完全有可能第二个请求先调用了下游服务。 Skywalking Node.js 就有这样的问题

继续查看代码又有新的发现,newrelic 同时也对很多基础模块进行了拦截,包括我能想到的 Timer, Promise Async, FS 等等, 包装回调主要目的在执行回调之前找到当时的 segment。

这么多的拦截代码想当然的会需要一定的开销。暂时未见到 newrelic 上关于这方面的文档。

更多链接
https://juejin.im/post/5a7a9e0af265da4e914b46f1

一个内存泄漏问题分析

一般来说,借助于强大的 GC 和 lint 工具,前端还是很少会碰到内存泄漏问题的。这篇文章说下我最近遇到的例子以及排查的过程。

内存泄漏的检测还是非常容易的:打开 Chrome DevTools 选择 Memory 选项,点击 Take heap snapshot 等待查看内存大小。重复这个步骤,如果你发现内存大小定期增长,或者增长的很有规律,那么八成出现内存泄漏了。这个是 Google 的 文档

问题检测

我们的应用是这样组织的,采用微前端架构,涉及到几个项目,一个 Shell 负责管理具体渲染哪个页面,应用默认是 Documents 页面,还有一个 Teams 页面分别属于不同的项目。 在 Teams 页面采集内存信息,回到 Documents 页面等待页面加载完成再回到 Teams 页面再次采集内存信息。重复这个过程几次,这是结果截图。为了确保数据准确,在隐私窗口测试以免受插件影响,在每次收集之前都点击 Collect Garbage。每次都多次采集直到得到 4 个相同的值表示内存大小稳定。这个截图上,5,6,7与8相同就移除了,不过足以说明应用确实存在内存泄漏了。

memory snapshot

问题诊断

现在我们知道有内存泄漏,先比较下 Snapshot 19 和 Snapshot 15 的内存信息. 😱 好吧, 太多对象了,几乎是组件树上的所有示例都有在列,毫无头绪。因为涉及到三个项目,完全不知道如何下手。不过不管怎样,先从可以做的做起,先把开发环境准备好。当然开发环境本身就有更多干扰因素,不过好歹还是有了第一条线索。当我尝试复现问题时,根本不等 Documents 页面完成渲染就切换页面,控制台有一个警告信息:

Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

因为有错误堆栈信息,所以很快发现,这个错误时因为没有清理定时器导致的。虽说本书是有逻辑来清理定时器的,但是没有考虑到这些逻辑因为用户页面跳转中断。所以说最好还是在 componentWillUnmount 完成所有的清理工作。

修复这个问题并且排查了所有的定时器之后,发现内存泄漏还在,看起来没那么容易解决。

为了方便问题排查,先修改 Teams 为仅渲染普通文本,依然稳定复现内存泄漏。不过其次最为可疑的就是全局的事件监听,排查一遍发现有些监听未被移除。幸运的是,发现一个低级错误,本来 componentWillUnmount 应该移除监听结果又添加了一遍:.

1
2
3
4
5
6
componentDidMount() {
window.matchMedia('print').addListener(this.printHandler);
}
componentWillUnmount() {
window.matchMedia('print').addListener(this.printHandler);
}

把 addListener 改为 removeListener 之后重新检查一遍,发现还是存在内存泄漏。

检查了所有的事件监听之后,确信没有遗漏,在查看内存信息的时候看到 onLoad 事件回调,对了,on 事件给漏了。在 shell 里一些可疑代码:

1
2
3
4
5
6
7
const tag = document.createElement('script');
// ... some code
tag.onload = () => {
resolve();
};
// ...
document.body.appendChild(tag);

不管怎样,我们应该清理到这些事件:

1
2
3
4
5
6
tag.onerror = () {
tag.onload = tag.onerror = null;
};
tag.onload = () {
tag.onload = tag.onerror = null;
};

重新检查内存泄漏,发现还是存在。上面的代码因为只允许了一次所以不会导致内存大小变化。这个时候都怀疑是不是第三方库的原因了。不过还是当把 Documents 页面换成普通文本时发现,没有问题了。所以问题肯定在 Documents 组件上。 为了进一步缩小范围,试着把 render 方法移除,发现问题这样都有问题。 所以说,问题还是在 componentDidMountcomponentDidMount 上的事件监听上. 但是看起来一切正常。 因为对 matchMedia 这个实验特性不熟,又再次查看了下文档. 这次注意到:它说每次都会返回一个新的对象 …

意思就是 matchMedia('print') !== matchMedia('print') 这也就是为什么 matchMedia('print').removeListener(this.printHandler); 压根没有的原因。修复这个问题之后,再次检查就没有内存泄漏的问题了。

总结

内存泄漏很少碰到,当然也很难调试犹如大海捞针。 除了排查定位:定时器,全局的事件监听,以及全局对象是优先排查的对象。

烦人的 Flow.js

不管怎么说,Javascript 构建的项目越来越大且越来越复杂,弱类型这个短板显得越来越为致命。而 Flow.js 提供了一个向强类型过渡的方案。

  1. 挫挫的枚举类型
1
2
3
4
5
6
7
8
type Color = 'red' | 'blue' | 'green';

const Colors = Object.freeze({
Red: 'Red',
Blue: 'Blue',
Green: 'Green',
})
type Color = $Values<typeof Colors>;
  1. 莫名其妙的严格
1
2
3
4
5
6
7
8
type Vehicle = { name?: string }
const car: Vehicle = {}; // It works

type Vehicle = {| name?: string, color?: string |}
const car: Vehicle = { color: '' }; // It works

type Vehicle = {| name?: string |}
const car: Vehicle = {}; // Error
  1. 不严格的类型和里式替换

我们知道,在 flow.js 中,当我们定义一个类型时,type Vehicle = { name: string } 只是意味着这个对象至少有 name: string 这个属性,你可以这样 const bike: Vehicle = { name: 'bike', brand: 'phoenix' }。就是因为太不严格,所以我们会选择严格类型 type Vehicle = {| name: string |} 不过接下来就遇到麻烦了。

1
2
3
4
5
6
7
8
9
type Vehicle = {|
name: string,
|}
type Car = {
...Vehicle,
wheel: number
};
const car: Car = { name: 'Toyota', wheel: 4 };
const vehicle: Vehicle = (car: Vehicle); // Cannot cast `car` to `Vehicle`
  1. 三方库依赖问题

有次突然发现我们漏掉了 import * as React from "react"; 但是可以直接使用 React.Node,flow.js 竟然没有报错。当然,它只可能是默认的 Any 类型。很快就定位到在 flow-typed/npm/enzyme_v3.x.x.js 包含有:

1
2
3
4
import * as React from "react";
declare module "react-redux" {
// Here will use React type
}

看样子是,只要在声明文件中导入之后,类型就被污染了。被污染的还有:Dispatch, Store, React, ComponentType and ElementConfig 不难猜到越是流行的库越可能被污染。而这一切发生的时候,flow.js 没有任何警告信息或错误提示。
Known issue: https://github.com/flow-typed/flow-typed/issues/1857

  1. 第三方库

时不时的第三方库导致 flow 错误,又不能 ignore 所有的 node_modules 文件,每次遇到新错误,都只能加载后面。甚至 node_modules/**/test/*.json 都有可能导致 flow 错误。 目前没有什么优雅的办法:https://github.com/facebook/flow/issues/869

1
2
[ignore]
.*\/node_modules\/draft-js\/lib\/.*.js.flow.*
  1. 关于 $FlowFixMe

有时 flow 不够聪明,即使我们知道没有那个逻辑,当然有时我们可以绕过这些报错。个人认为用额外的逻辑来弥补 flow 的错误更不合适。$FlowFixMe 是很烦,但是至少不会引起困惑。

谈谈测试与代码质量

平常开发中,你花多少时间写测试?覆盖率有多少?除了单元测试,其它的整合测试以及 UI 测试有实践么?

以我个人经历来说,在国内工作时几乎不写任何测试,仅限于倒腾过测试。即使阿里这样的大公司,业务部门也几乎不写任何测试,基础研发部门或许很有节操,具体情况不得而知不好瞎猜。为什么不写测试,这跟整体的氛围有关,真有人在意你的代码质量吗?没有!大家在意的是业务结果。原来的负责人升职加薪之后项目交给后面的人维护,如果新需求不多,代码还过得去维护着就行。需求实在太多,维护不下去了,那只能重写了(自动化测试都没有,谈不上重构)。

国内开发节奏太快,着急于抢占市场,不断试错,计划赶不上变化,这周加的功能下周都可能就要废掉。这种情况下,写测试完全得不偿失,自然也就不会花时间在写测试上了。那如何保证代码质量呢?一般在上线截止日期前,集中时间手动测试,或许有些公司将这些任务外包给其它公司来做,毕竟大多数的开发可不屑于干这么枯燥无聊的活。

现在就职的公司在测试上做的可谓非常专业。我们的 QA 团队会支持各个业务团队,QA 是质量辅助而非质量保证。这儿有个文章介绍质量保证与质量辅助。QA 的职责更多的是监督,指导我们来完成测试任务,而非自己测试。他们会收集各种数据,建立指标来评估代码质量。另外,也会帮助新团队或新项目建立自动化测试,指导新人完成测试工作。

再说说我的发布模式,我们的发布周期是每日发布,也就是说当你的代码合并到 master 时必须保证它是正确的。因为第二天固定时间就会部署到线上,部署之后自动化测试通过就认为部署成功。除了单元测试,我们还需要用 UI 测试覆盖到所有的关键路径,也即冒烟测试。冒烟测试只会测试整体流程,允许存在 bug 但不是严重的,不影响正常的流程即可。

一个潜在的 bug 发现的越早所需的修复成本越低。单元测试并不能保证万无一失,模块与模块之间功能是否匹配还需要整合测试来保证。现在的问题是我们为什么而测试,于是,在开发前我们列出所有的需求点来,以保证我们会用适当的测试来覆盖这些需求点。

最后,在后端和前端之间,我们加入了合约测试以保证后端的 API 变动不会影响到前端代码。

现在的趋势是,开发将承担运维和测试的工作,而运维和测试只做些支撑性的工作。某些大厂强制测试转开发,就我之前见到后端写前端的经验来看,除非是内驱的并且公司给予足够的时间来转,否则只是添乱。口号喊的很好,全栈工程师,简单易上手,但是只要不让我接手维护他们的代码随便他们怎么折腾都行。

说说发布周期,每日发布就必须要求开发对代码有信心任何时候合并进去就要能够部署,避免开发将测试责任推卸。缺点是用户不能很好的利用缓存,用户重新加载新资源却并没有任何新的可用功能。将任务切分很细只会带来更多的工作量,而开发分支上本来就不是直接可交付给用户的。个人来看,除非必要的 bug 修复,完全可以等到 Sprint 结束一起发布。

关于测试,我并不是认为测试写多了,代码质量就会变好。测试只应该是辅助,不应该为测试而改动代码的可读性或使得代码变得复杂。真有一个极端的同事,几乎所有的代码都是依赖注入,一个功能函数的参数是一个接口。比如,fetchUser() 依赖的 request 只是一个接口,你需要一层一层往上查看,才知道这个是如何实现的。
我知道这会让我的代码非常容易测试,但毫无疑问它让代码变得更复杂了。如果说好的架构师懂得如何权衡各种技术做出取舍,好的程序员一样也需要懂得取舍。测试的增加必然会拖累产品迭代的速度,有时我常会想,我现在增加的一个 UI 测试,真的以后这儿会做改动吗?或许那时交互的需求早就变了。我同样好奇这些测试到底帮助我们揪出了多少个潜在的重大 bug,是否值得我们的投入。

关于是否应该面向需求点检查覆盖率,我们也有不同的意见。个人不太认同,因为我们做单元测试很多时候是知道这个模块会被复用,其它人在使用这个模块时,如果需要改动,单元测试可以很好的保证其不被破坏。这个可复用的模块即使已经被整合测试覆盖到,也应该有对应的单元测试。整合测试并不能够帮助我们迅速定位问题,只是确保某一个功能是否完好。

对于测试,总的来说,它带来好处,但是也不是免费的。对可复用的模块,基本的单元测试是必需的,写单元测试也可以帮助你发现代码异味。关键路径的冒烟测试也是有必要的,至少确实会减少人工成本。业务逻辑代码因情况而定,非关键功能且你认为是一次开发即可的,你老板也不会让你投入这么多时间写测试的。如果你没有写过测试,开始尝试用单元测试覆盖你的可复用模块,尝试写些 E2E 测试减少重复劳动吧(E2E 测试远并没有你所想的复杂)。

前端开发畅想

整篇文章的思路是这样子的:重新思考前端开发过程,有不少让人觉得不痛快的地方。如果没有这些束缚的话,理想中的开发部署又是怎样的?理想有点远,眼下我们又能做些什么呢?

阅读更多

HMR 踩坑记

昨天遇到一个问题:使用 next/tree 时报错,即使最简单的 Demo 也会报错,而官网提供的则不会,最终问题定位到 react-hot-loader 上。我使用的是 react-hot-loader@next版本,需要在 babel 配置中引入 react-hot-loader/babel plugin。禁用这个 plugin 正常渲染,启用就报错,这个问题太诡异了,所以有必要搞清楚 HMR。

阅读更多

探秘 MobX

MobX 是最近在 React 社区比较火的状态管理工具。与 Redux 相比,Mobx 简单又神秘。不止是因为 MobX 比较火,MobX 的双向绑定与 Vue 的实现也是非常相似,十分有必要去了解一下双向绑定的实现。这篇文章通过源码解释 MobX 这些奇怪的“特性”。
就像 Redux,MobX 跟 React 也没有关系,我们从最最简单的例子开始。MobX 版本 3.1.7

阅读更多

Promise 被玩坏了

收到产品同学反馈的一个 bug:在 iOS 上,进入首页之后很快滑动,再点击切换到第二个页面会一直处于loading状态,可以稳定复现。拿自己的手机试了几次果然可以复现。在模拟器上准备调试,打开控制台并未看到错误。于是猜想有异常没有处理,检查代码是否遗漏:

1
2
3
4
showLoading();
fetch(url).then(() => {}, () => ([])).then(() => {
hideLoading();
});
阅读更多