这本来是一个已经解决的问题,至少在一年之前,这个流程是完全能够跑通的。但是不知道从什么时候,在 repo URL 中通过 username@
的方式携带 Personal Access Token (PAT)的方式已经行不通了,会报错。
1 | fatal: could not read Password for 'https://***@github.com': No such device or address |
这是可能是因为 Github 不认携带在 URL 上的 PAT。转而向用户请求密码,但是在 CI 环境里没有键盘这样的输入设备,然后就报错了。
hexo 官方的 hexo-deploy-git 在支持 token 方面也采用了上述方法。即使正确配置 token,也会部署失败。hexo-deploy-git 在 token 配置上还有一些坑,例如当 repo
字段是字符串时,token
字段是不生效的。只有在 repo
字段里面配置 token
字段才有效。
1 | deploy: |
由于 PAT 现在只能够人工输入,在 CI 环境下做不到。所以,我们似乎只能转向使用 SSH 的方式来操作 git。
在本机创建一个 SSH Key,用 -C
参数注明要操作的 repo 的 git 链接。
1 | ssh-keygen -C git@github.com:xxxx/yyy.git |
执行生成一个新的 key,将公钥上传到 GitHub 账号的 SSH and GPG keys 中。务必注意,SSH key 的权限比 PAT 要高,能够读写账户里所有的 repo。所以请务必像密码一样的保管私钥。
把私钥通过 repo 的 Secrets and variables 设置,在 action 下配置一个 Repository secrets,假设 secrets 的名字是 SSH_PRIVATE_KEY
。我们可以在 action
的 yaml 配置中用${{ secrets.SSH_PRIVATE_KEY }}
获得这个私钥。
在部署前增加一个可以配置 ssh 的 action。例如 webfactory/ssh-agent
,yaml 配置如下:
1 | jobs: |
_config.yaml
中的 deploy
配置将 deploy
中的 url 配置为 repo 的 SSH 链接,例如 git@github.com:a/a.github.io.git
。
做完之后,hexo deploy 会切换为通过 SSH 的方式去发布文章。绕过了 PAT 输入困难的问题。但是,前面提到过 SSH Key 的权限很高,会丧失 PAT 可以限制权限的好处。谨慎使用。
]]>这一年里,我经历了公司搬迁、经历了半夜还在配合修改需求的日子、也经历了被裁员。在人生的第一份工作中以被裁员而终结,如果在游戏中应该拿一个成就吧,哈哈哈。
今年在生活上算是完成了一个目标——把驾照考出来了。在同学的“刺激”之下,6月底我终于狠下心来报了驾校。这期间我也体会到了为什么大家都说驾照最好在大学考出来。学车完全占据了我7月的所有的周末,几乎没办法休息。由于考试只能在工作日进行,我还需要请若干天年假来考试。如果考试不通过的话还要再请假,一天的工资也是不小的损失。幸好这次驾考是一次过。我还挺怀念那段时间的,非常充实。拿到驾照之后,我还去与驾校散养的羊驼与孔雀告别。哈哈。
由于被裁员,今年我多出了很多时间用于生活。但是不巧,新冠疫情仍然在世界范围内流行,出去旅行可能并不是一个好的选择。在毕业之前,我就有想出去走一走的想法,一直未能成行。我非常希望世界能够回到新冠前时代的开放,祝愿疫情早日平息。
今年的工作总结起来就是——寄。今年年初,我与小伙伴以支援的身份被调出。两个人接手一个新的小程序项目,项目从零开始搭建,在过程中踩了一些坑,总体而言算是痛并快乐着。
然后到了6月份,我回到了原来的开发组。但是,7月份我又被以支援的身份调出到其他开发组。我感觉在今年的工作中,我一直处于哪里需要往哪搬的备份状态。没有一个固定的项目来维护,对于程序员的成长是很不利的。也由此萌生了离职的念头。但是没想到没等我提出来,公司抢先一步把我裁了,当时不知道是一件该难过还是该伤心的事情。现在想起来,这可能并不是坏事。因为我有了很多宝贵的时间来思考之前中很难思考到的重要问题。在之前,「技术改变世界」这个想法占据了我的头脑。技术仿佛是能够解决一切问题的手段。但是,在仔细思考之后发现,这个世界不是这么运转的。因此,在这后半年里,我开始(重新)接触技术之外的东西,发现确实十分受用。
除了工作之外,我抽出了一些时间来参与开源项目的开发。成为了 Node.js 的 Collaborator。出乎意料的是,成为开源项目甚至是知名开源项目的维护者对技术的要求并没有想象中的那么高。只需要热情与恒心加上一点点的编码能力,就足够了。
从 去年的年终总结 中复制过来的。
❌ 基于 Jest 与 Cypress 的前端单元测试与 E2E 测试的系列教程;
非常遗憾,第一项没有完成。没能够规划好时间来写这一系列文章。但是,我仍然认为自动化测试应该是严肃的软件项目中重要的组成部分。希望今年能够完成这个目标。
✅ 参与开源项目,向知名开源项目提交至少一个功能性或者修复性 PR;
在开源项目方面,我今年向 Node.js 提交了 27 个 PR,除去 2 个程序性的 PR,应该有 25 个文档或者修复性的 PR,其中 15 个被接收。我应该算是完成了第二个目标。
❌ 至少发布 12 篇有质量的技术文章
很遗憾没有完成,今年在技术方面只发布了 3 篇文章。尚且不论有没有质量,在数量上就没有达标。这自己还是堕怠了啊。
掌握系统化的开发方法,并熟练运用于日常开发中;
这个目标从今年看来,没有可以量化的指标来衡量这个目标,也就无法判断是否达到这个目标。
在 2022 年里,我会从之前的技术中心向生活中心转变,重新出发去寻找适合自己的生活方式与节奏。
初步的 2022 年目标如下:
Object 1:在技术领域继续提高,丰富自己的业务经验与能力
Object2: 平衡生活与工作,使自己感到生活的快乐
Object3: 保持身体健康
在最后,祝大家新年快乐,在 2022 年里身体健康,万事如意。希望新冠疫情能够早日平息,能够重回开放且自由的生活。
]]>最近在看来自哈佛大学的 Tal 博士主讲的《积极心理学》课程。在课程中,Tal 博士分享了很多心理学研究成果,将它们串联起来变成一条通往「幸福」的通路,让人变得更加积极,更加有生产力与创造力。我强烈推荐各位去看看这一系列视频。真的能够受益终身。
在课程中,Tal 博士专门花了一节课的时间来讨论完美主义以及其造成的影响。比如完美主义是拖延的主要因素、完美主义降低了人们的幸福度,使人自卑等。我特别想在此分享他的一些思考。
在 Tal 博士的定义中,完美主义是一种对失败的失能性的恐惧,特别是在自己在意的领域上失败。所谓失能性的恐惧是指,完美主义者害怕失败,害怕到以至于无法继续前进或者失去尝试的勇气。
与此相对的,有一类人我们定义为是追求卓越的人,他们在追求一个目标的时候,能够允许一定的失败。在完美主义者中眼中,如果要从 A 点到 B 点,选择的必然是从 A 点到 B 点的直线。但是在追求卓越者眼中,从 A 点到 B 点可以是一个反复曲折的过程,只要在最后达成目的即可。
在生活中,我们似乎不会将完美主义视为一种缺点。拥有完美主义特质的人会竭尽自己的全力去达成自己的目标,通常会选择最直接的那条路,永远不回头的走下去。完美主义与完美和成功通常联系在一起。这样的特质很好啊,为什么会说有缺点呢?
在开头提到了博客主都会遇到的一个难题。自己写出来的文章比不上大家,如Dan Abramov、 Martin Flower、张鑫旭等等。然后久而久之就失去了写文章的兴趣。这就是完美主义作祟的一个例子。
人对失败是敏感的,没有人会喜欢失败。人总会竭尽自己的全力来避开失败。但是,人生中失败是不可避免的。就如一场比赛中不会所有人都是第一名一样,一场 8 个人的比赛中,就会产生 7 个失败者。更不如说,失败才是生活的常态。
如果人惧怕失败,以至于用「不行动」来实现「不失败」这一目标。这就落入了完美主义的圈套中了。这是拖延乃至扼杀一种兴趣的普遍原因之一。如果自己写的文章比不上大家,那我不写不就好了吗?博客就会这样无限期的断更下去。
另外,有一句古话曰「失败是成功之母」。每一次失败都是一次获得新知识的机会。如果不写文章,那么写作技巧就不会提高。如果不实际编写程序,那么技术也不会提高。如果想要一发入魂,一次就编写出「最完美」的作品,这是不可能的,身体就会自然的抗拒。
人类的大脑能够感知到现实与理想的差异。大脑不喜欢这种差异,如果有差异存在,大脑会产生焦虑的情绪,迫使人类要么改变现实,要么改变理想,使这两个世界变得一致。这种机制不是坏事,它驱动着人们向着理想世界的方向改变现实。但是在完美主义者中,这个机制却出现了一些问题。在完美主义者的理想世界中,失败是不应该存在的东西,而在现实世界中,失败是普遍的。因此理想世界与现实世界出现了差异,大脑会产生焦虑的情绪。此时,完美主义为了缓解焦虑需要使理想世界与现实世界变得一致。要改变理想吗?开始接纳失败。如果这样的话,完美主义者就不是完美主义者了。为了符合完美主义者特质,完美主义者只能够改变现实世界,即在现实生活中要一直赢。一步失败就会让完美主义者陷入差异产生的深深的焦虑与抑郁中。然而,在现实生活中,失败才是常态。这意味着这种焦虑与抑郁也会是完美主义者人生中的常态。这不禁让人自问——「这值得吗?」
那么,什么因素造成了完美主义如植入了 DNA 中一样如此普遍呢?也许有两个因素,其一是人类社会与教育重视达成目标,却轻视达成目标的过程;其二是在个人层面,完美主义与「成功」等正面素质关联在一起,当想放弃完美主义的时候,潜意识中会以为同时在放弃「成功」与「完美」,进而会阻止放弃完美主义。
在社会与教育中,一直以来达成目标是最重要的。达成目标会收到奖励,如升学,晋升等。但是,为了升学与晋升,期间的努力与坎坷是没有被重视的。至少我没有听说过为了庆祝失败举办的活动。也许大家会问,升学与晋升这一结果不就是这段过程的奖励吗?但是如果没能够成功达成目标又会怎么样呢?这就不变成了纯粹的痛苦经历了吗?为了逃避这样悲惨的事情,人们只能采取「只能成功不能失败」这种观点。
另外在个人层面,「完美主义」常常与「成功」等正面素质关联在一起。如果想要放弃「完美主义」,接纳失败的话,人的潜意识中会认为「成功」会被同时放弃。当然,潜意识会抗拒这样的改变。但是,放弃「完美主义」并不意味着放弃「成功」。只不过是在通往「成功」的道路上可能绕一些路,看到更多的风景而已。
Dr. Tal 对于完美主义的研究也启发了我的一些思考。首先,在做事的方法论上,无论是什么事情,只要想去做的话尽管去做。如果成功的话自然很好,如果失败的话也能从中获取一些新的经验。做事最怕的不是失败,而是不做事。其次,要重视旅途上风景,不要总盯着目标。奖励不一定是要在达成目标的时候,开始做某件事之类的也是值得奖励的。
自从看了《积极心理学》课程之后,我特别想分享 Dr. Tal 的一些关于如何变得快乐与积极的看法。这是其中之一。在现代社会中,我们的物质变得更加富足,但是我们似乎并没有变得更加快乐。快乐并不是少数人的特权,它是每个人应得的。所以,希望世界上每一个人都能够获得自己的幸福与快乐。
]]>那么栏目的第一篇,我打算聊一聊「自动化测试」。
很多研发会觉得写测试用例是非常累赘的事情。有些会认为,业务变化太快,维护测试用例费时不讨好;有些会认为,公司配备了完整的 QA 团队,有问题让 QA 人工测试就好,没必要让研发写自动化测试等等;还有可能,业务需求开发时间被压缩,觉得“额外”再写一份测试用例降低工作效率等等。
在日常工作中,至今为止接触的数十个项目中,只有少数一两个项目在 CI 的要求下保证了一定的单元测试覆盖率(如果我没记错的话 50% )。其它项目基本上没有自动化测试用例的存在。毫不令人意外的,项目经常出现的现象有:
console.log
去寻找问题的根源在哪,耗费大量时间;如果自动化测试是低效的,那么与之相反,没有自动化测试应该会体现出高效。但是在实际中,没有自动化测试的项目中,反而也体现了低效与风险。仔细思考的话,是否对于自动化测试的看法是有误的吗?是否自动化测试真的会对项目的开发效率、调试效率、可维护性产生负面影响?🤔
拒绝自动化测试的一大原因是开发害怕实施自动化测试会影响开发效率。毕竟,研发除了业务代码之外,还要多维护一份测试用例代码。很多人下意识的会同意这种说法。
但是,实际上真的是这样吗?回想实际开发的过程,在完成一个需求的时候,最频繁的操作会是什么?对于我而言会是验证操作。比如修改一处代码,然后满怀希望的等待编译(可能耗时 10s 左右),然后在浏览器上模拟用户操作,看程序的反应。这一套操作下来,个人估计至少需要 30s 左右。所以人工验证需要30s 来一个功能点是否正常工作。人工验证是繁琐与无趣的,中间可能走神,这样需要时间会更长。
如果换做用自动化测试的话,一个单元测试耗费的时间是毫秒级的,一个 E2E 测试用例的时间大概在 1s 左右。在一次人工验证的时间中,通过自动化测试我们能验证多少功能点呢?只跑 E2E 测试,我们都至少能够验证 30 几个功能点。况且 E2E 测试是自动化测试中最耗费时间的一类测试用例了,自动化测试的效率对于人工验证的效率是数量级上的提升的。另外,很多框架为了最小化繁琐操作,都自带了 watch
功能。当检测到代码改变,自动运行相关的测试用例。基本上,在修改代码后的几秒之内就能够知道代码修改对整个项目的影响。这是人工验证无法做到的。
另外,自动化测试会使开发在整个研发的工作流中,不用在工具之间切来切去。只用在代码编辑器与命令行中就可以知道修改代码的结果。这会使开发更加专注于编写逻辑,不受“上下文”切换的影响。
自动化测试将开发中最频繁操作的效率提升了一个数量级。付出的是仅仅几分钟时间设计一个测试用例。
在调试中最难的问题莫过于找到问题的原因。如果没有测试用例的话,我们可能会通过在有疑问的地方打日志的方式来找出问题。对于一个较大型的程序而言,涉及到的代码可能横跨几个文件,好几个方法,打日志的方法非常盲目,属于最后的手段。
但是如果有测试用例覆盖的话,DEBUG 会容易很多。如果程序出现了问题,而同时某个测试用例没有通过。可以直接怀疑相关的代码出现了问题。修正代码使测试用例通过,bug 就修完了。不再有到处打 console.log
来寻找问题在哪的操作。
即使,当所有测试用例都通过,程序仍然出现了 bug。这说明当前测试用例没有完全覆盖需·求。此时,我们用 bug 的复现方法编写一个一定会失败的测试用例。然后调整代码使这个测试用例通过。这样,不仅在调试过程中借助了自动化工具提升了体验,而且更重要的是,测试用例可以保证这个 bug 永远不会再次出现。
代码的可维护性与很多因素有关。如架构设计的合理性,模块之间是否存在耦合等等。由于业务多变难以预测,几乎很少能够一次性的写出既符合需求又易于扩展的代码。因此,在开发过程中,不断对代码进行重构,优化代码结构是提高项目可维护性的必经之路。
从定义而言,重构要求代码的改动不能改变软件的功能。然而,现实并不是乌托邦。有时,代码几经接手,原需求已经不可考;有时,代码充斥着坏味道,我们已经无法理解代码的职责是什么。在这种情况下,我们无法保证代码能够被安全重构。
重构对于项目健康而言是必要的,那么如何保证在安全的情况下重构呢?——完整的测试用例。测试用例就像合同一样,保证了一个单元的输入输出的正确性。单元的用户不关心单元内部的实现逻辑,只需要单元的输入输出符合预期即可。如果一个单元有完整的测试用例覆盖的话,我们在单元内部任意调整代码结构,只要能够保证测试通过,我们都能够认为重构是安全的。因此,Martin Fowler在《重构——改善既有代码的设计》中提到了单元测试是重构的基础。
自动化测试是提高软件设计质量与可靠性的良好手段。在现代的软件开发中,软件代码与测试用例代码是同样重要的。在大型开源项目中,如 Node.js 中,实现任何新特性或者修复 bug 都会被要求附上相应的测试用例。但是在实际工作中,推行自动化测试也许遇到一些阻力。希望借由本文能够消除一些对单元测试的误解。
]]>tl;dr; 本文介绍的是调试 Node.js 核心中 JS 侧的代码方法。请注意,并不是 Node.js 应用的调试哦。
Node.js 由两部分代码组成,一部分代码是 C++ 代码,位于项目的 src
目录中,另外一部分是 JavaScript 代码,位于项目的 lib
目录中。
C++ 部分负责把用 C 或者 C++ 编写的一些提供底层能力的库粘在一起,如异步 I/O 库 libuv(C)、JS引擎 V8(C++)、http解析器 llhttp (TS 编译到 C)等。C++ 部分也提供了程序的入口点,如 node xxx.js
实际上最先到达的就是 C++ 部分的main 函数另外,C++ 部分也提供了一些 Node.js 内部才能够使用到的模块,这些模块可以被 JavaScript 部分调用来实现逻辑。
JavaScript 部分提供了用户侧可使用的API。这些 API 在文档中分为了若干模块,基本上每一个模块对应 lib
中的一个 JS 文件。根据模块的名字能容易找到对应的文件,如 Events
模块,对应的就是 lib/events.js
。
Node.js 严格上算是一个 C++ 程序。对程序的修改要走编译流程生效。对于习惯于热重载等技术带来的即时反应的工程师来说,这套流程是非常慢。好在,如果只修改 JavaScript 侧的代码的话,我们可以运行下列命令:
1 | ./configure --node-builtin-modules-path $(pwd) |
此时,如果运行 make -j4
会构建在 out/Release
下出来一个特殊的 node
二进制。在这个二进制中,JS 层的文件不会被编译进去,而是会使用 --node-builtin-modules-path
指定的外置 JSpath 下的 lib
中的 JS 文件运行时解释。在这个例子中,就是 Node.js 的项目文件夹。对 JS 文件的修改会即时地反应到这个特殊二进制当中。
例如,为了测试,我们在 EventEmitter
的 init
方法中抛出一个异常:
1 | // lib/events.js |
保存后,执行 ./out/Release/node
,就可以看到 node 确实抛出了异常
1 | node:events:186 |
使用外置 JS 的可以很大的提升开发效率。但是,调试时如果我想看到一些属性或者变量又怎么办呢?console.log
大法吗?
很不幸的是,console.log
在一部分模块里是行不通的。因为 console
本身也是 Node.js 公共模块的一部分。使用 Stream
模块实现。如果在调试Stream
模块相关的模块时,就容易出现爆栈无法打印的情况。emmmm🤔,难道我们就束手无策了吗?其实,是有解决方案的——调试器。
调试器就是那个可以单步调试,在每一步都能够打出来当前环境的中的变量的情况的程序。也许 console
过于方便已经让我们忘记了有这把瑞士军刀了。对于 Node.js 应用,VS Code 的调试器原生支持。但是对于 Node.js 项目本身呢?我们有办法在模块源码中打断点吗?这也是有的。
mmomtchev 向 VS Code JS 调试器提交了 debug Node.js 内部代码的功能[2]。结合上一节的外置 JS,我们可以方便地在代码中打断点,然后更改调试了。在 .vscode/launch.json
中添加下面的调试配置:
1 | { |
然后把需要调试的外部 JS 文件,复制到 Node.js 项目中。打开 VS Code 调试器,选择 Launch current file
启动,就可以愉快调试了。
[1] Node.js BUILDING[2] Add support for debugging Node internals when they are externally loaded
]]>user.name
是 xyz
,user.email
是 xyz@abc.com
公司项目用另外一套 git 配置,user.name
是 Real Name
、user.email
是 realname@corp.com
。git config
支持系统层级 --system
、用户层级 --global
与仓库层级(无选项)的配置。但是,对于大量项目,手动地通过 git config
指定未免过于繁琐。本文介绍了一种通过修改 git 的配置文件 .gitconfig
,使用[includeIf]
对某个文件夹下的所有 git 项目指定 git 配置的方法。
在 git 中,有三个层级的配置文件:
/etc/gitconfig
,作用于系统中所有用户的 git 配置;$HOME/.gitconfig
,作用于用户的 git 配置;.git/config
,作用于项目中。如果有相同的配置,按照 项目 > 用户 > 系统 的优先级获取配置。
[includeIf]
从 git 2.13.0 开始,git 配置文件开始支持Conditional Includes的配置。通过设置 includeIf.<condition>.path
,可以向命中 condition
的git 仓库引入 path
指向的一个 git 配置文件中配置。
[includeIf]
的语法如下,<keyword>
为关键词,<data>
是与关键词关联的数据,具体意义由关键词决定。
1 | [includeIf "<keyword>:<data>"] |
其中支持的 keyword 有:
gitdir
: 其中 <data>
是一个 glob pattern如果代码仓库的.git
目录匹配 <data>
指定的 glob pattern,那么条件命中;gitdir/i
:gitdir
的大小写不敏感版本。onbranch
:其中 <data>
是匹配分支名的一个glob pattern。假如代码仓库中分支名匹配 <data>
,那么条件命中。就我们的需求,使用 gitdir
完全可以。
假设在家用工作电脑上,我们默认开发的是个人项目。有时为了应对紧急需求,会将公司项目 clone 到电脑中,统一放置放到 ~/corp-projects/
目录下面。个人项目与公司项目的差异点在:第一、使用的邮箱名不同,个人项目会使用个人邮箱,公司项目使用公司邮箱;第二,公司项目可能需要 VPN 接入才能够存取代码库。我们首选使用,用户层级的 git 配置文件。
1 | vim ~/.gitconfig |
在最后添加一个 conditional include:
1 | # ~/corp-projects/ 下面的所有仓库引入 `~/crop-projects/.gitconfig` 中的配置 |
最后创建公司项目统一的配置文件:
1 | vim ~/corp-projects/.gitconfig |
1 | [user] |
——「预估 40.5 元」
「哎」,李华轻叹了一声,放下手机,拖着疲惫的身体,缓缓向附近的地铁站走去。
经济关乎着我们每一个的生活。前几天,我在 v2ex上看到一些关于房产税的讨论,「如果国家决定征收房产税,对于房屋租金会如何改变?」,不论立场而言,是一个很有趣的经济学问题。答案也很简单,
tl;dr; 租金会稍微上涨,但是房产税的负担不会完全落到租客头上。
在研究租金问题之前,我们首先需要了解「价格是如何形成的?」 。
为了解释这个问题,我们假设有这样一个小区,其中有五个房东 A, B, C, D, E。每一个房东都有一间房屋等待出租。每一个房东对于待出租的房屋有一个心理价位,只要租客出价不小于这个心理价位,房东就愿意将待出租的房屋租给租客。
假设,每个房东的心理价位如下:
房东 | 心理价位 |
---|---|
A | 1000 |
B | 1100 |
C | 1300 |
D | 1400 |
E | 1500 |
为了把 5 个房东的心理价位聚合起来,我们以 $y$ 轴为房租价格,$x$ 轴为可出租房屋数量,绘制一张曲线图。
从曲线中,我们可以看到,如果我们愿意付出 1200 元租一间房屋,此时小区中可以有 2 间房屋可以选择。这条曲线,刻画了市场上单一商品(这个例子中是出租屋)上的供给情况,因此这条曲线,也被称为供给曲线(Supply Curve)。
由于人性,供给曲线的斜率一定是大于等于 0 (为什么?)。
在同时,有五位打工人 I、J、K、L、M 准备在这个小区租房,他们每一个人也有一个心理价位。当房屋租金不高于这个心理价位,他们就会花钱租下这个房屋。
租客 | 心理价位 |
---|---|
I | 1400 |
J | 1300 |
K | 1200 |
L | 1000 |
M | 900 |
借鉴供给曲线,我们可以通过曲线的方式,把所有打工人的心理价位聚合起来。
曲线中每一点$(x, y)$,表示当房租为 $y$ 时,会有 $x$ 间房屋出租出去。这条曲线被称为需求曲线(Demand Curve)。
同样由于人性,需求曲线的斜率一定会小于等于 0 (为什么?)。
如果我们将两条曲线放到同一张图上,我们会发现两条曲线相交于一段线段。如果当租金落在这个线段上,市场中待出租房屋与出租房屋数相等,达到了平衡。此时,价格被称为均衡价格(Equilibrium Price)。很显然,此时市场中商品实际成交的价格就是均衡价格。
用上面的例子,假设租金为 1250 元。此时房东侧,房东 A、B 愿意出租房屋,C、D、E 由于低于心理价位,没有出租房屋。租客侧,租客 I,J 愿意租赁房屋,租客 K,L,M 由于高于心理价位,没有在小区中租赁房屋。此时市场上,A、B 与 I、J 会达成交易,实际出租房屋数为 2。
在实际中,参与市场的个体数量众多,一般而言供应曲线和需求曲线都是平滑的。不失一般性的,我们用平滑的曲线来代替供应曲线与需求曲线。
上面这个小区的例子,就是所谓的「供需决定价格」。确切的来说,是供给曲线与需求曲线共同决定价格——两条曲线的交点。
对于不同的商品,人们对价格变动的敏感度是不一样的。如大米、面粉等生活必需品,人们对于价格变动是不敏感的。然而,对于另外一些商品,如奢侈品,价格的变动常常会影响人们的购买决策。在经济学中,我们可以通过*价格弹性(Price Elasticity)*来描述市场这一行为。
上图是一条需求曲线(为什么?),假设价格从 A 点变化到 B 点,我们可以计算出来需求量变化的百分比与价格变化的百分比的比值,这被称为需求的价格弹性。
$$E_d =\frac{\Delta Q / Q_A}{\Delta P / P_A}$$
实际上,价格弹性的精确定义需要借助微分,此处为了简单不展开。
类似定义,也可以定义出供给的价格弹性。由于需求的价格弹性恒大于等于 0,供给的价格弹性小于等于 0(为什么?)。因此在讨论价格弹性的时候,常常把符号舍去。例如,说某个商品的供给的价格弹性为 1,其实在数学上,它的价格弹性应该为 -1.
弹性也可以是一个相对的概念,假如我们说某个商品,供给比需求更有弹性,指的是供给的价格弹性(注意舍去符号)要大于需求的价格弹性。
建立了供给、需求、均衡价格与价格弹性的概念之后。我们已经准备好了研究税赋的理论工具了。
Our new Constitution is now established, and has an appearance thatpromises permanency; but in this world nothing can be said to be certain,except death and taxes.
– Benjamin Franklin
税,相信大家都很熟悉。购买商品时,发票上有时会打印着增值税;发工资的时候,员工会被征收所得税;交易股票的时候,卖出者会被征收印花税等等。如果,政府对某件商品征收消费税,我们的一般直觉是「啊,这笔税收一定最终是消费者承担吧」。然而,事实真的是这样吗?
tl;dr; 政府无法将税收精确地分摊到买卖双方。
假设,政府有一天决定对「手抓饼」进行征税,政府有两种选择,第一种是在需求侧征税,例如手抓饼的价格是 5 元/个,消费税每个 1 元。此时消费者需要付出 6 元,5 元付给卖家,1 元作为税;另外一种是在供应端征税,例如卖家供应手抓饼的价格为 5 元,税率每售出 1 个 1 元。此时卖出一个手抓饼收到 5 元,其中需要向政府缴纳 1 块钱作为税收,卖家只能留存 4 元。
在上面价格的决定因素中提到,市场价格由供需决定。要研究在增加税收时,市场的价格如何变化,我们需要研究供需的变化。假设在征税之前,「手抓饼」市场的供求曲线如下,均衡价格为 5 元。
假设政府在需求侧征税,每一个手抓饼向消费者征收 1 元税收。此时,我们来分析供需的变化。
首先对于供给侧而言,对需求侧征税对供给侧完全无影响,因此,供给曲线是不变的。作为消费者而言,只会关心最后的到手价格,也就是含税价格,消费者不会含税价格里面有多少是税收,多少归属于商家。因此,以含税价格计,商品的需求曲线是不变的。但是,对于供给侧只能收到非含税价格,如果以非含税价格计,扣除税收后,商品的需求曲线会向下平移税收个单位,在手抓饼的例子中,也就是向下平移 1 个单位,到 $D^{\prime}$ 处。可以看到,以非含税价格计,市场建立到一个新的平衡态 $E^{\prime}$。此时,消费者需要付出 $P_d$ 元,卖家收到 $P_s$ 元,$P_d - P_s$正好为 1 元,交给政府作为税收。
从上面的结果中可以发现,在征税后,手抓饼的价格上涨到 $P_d$ 元,但是 $P_d$ 会小于 6 元($P_s < E = 5, P_d = P_s + 1 < 6$),也就是这 1 元税收不会完全转嫁到消费者身上。那么,这 1 块钱税收是如何分配的呢?观察图中,因为税收的影响,手抓饼的价格上涨到 $P_d$ 元,为了获得一个手抓饼,消费者要多付出 $P_d - E$ 的钱,也就是图中蓝色阴影部分,很显然,这一部分钱是税收的一部分。然而,由于市场新平衡,卖家只能够收到 $P_s$ 的钱,比未征税时的 $E$,少挣了 $E - P_s$,也就是图中绿色阴影部分,相当于卖家将这部分少挣的钱作为了税收的一部分上缴给了国家。蓝色阴影的面积与绿色阴影的面积比就是这 1 块钱税收在两者之间的分摊。
假设政府在供给侧征税,也就是当卖家每卖出一个手抓饼的时候,向卖家征收 1 元税收。仿照上面的分析来看市场价格如何变动。
首先对于需求侧来说,不会感知到对供给侧的税收,因此,对需求曲线无任何影响。对于供给侧来说,也只会关心自己的到手价格,即不含税价格。以不含税价格计,供给曲线不变。但是对于消费者而言,是以含税价格成交的。以含税价格计,加上税收,供给曲线向上平移税收个单位,在手抓饼的例子中是1 个单位,到 $S^{\prime}$ 处。可以看到,市场建立了新的平衡态$E^{\prime}$,此时,消费者需要付出 $P_d$ 元,卖家收到 $P_s$ 元,$P_d - P_s$正好为 1 元,交给政府作为税收。
分析征税后的变化,手抓饼的价格上涨到 $P_d$,但是同样 $P_d < 6$ 元。也就是税收不会完全转嫁到消费者身上。看税收的分配,消费者相较征税前多付出了 $P_d - E$,也就是图中蓝色阴影部分,这一部分作为了隐性税收上交给了政府。卖家在征税后只能收到 $P_s$ 的钱,比未征税时的 $E$,少挣了 $E - P_s$,也就是图中绿色阴影部分,相当于这一部分作为了税收上交给了国家。蓝色阴影的面积与绿色阴影的面积比就是这 1 块钱税收在两者之间的分摊。
从上面的例子中可以看到,无论政府对那一方征税,另外一方都会一同承担税收。可以得出结论:「如果政府对一项商品征税,对于需求侧而言,商品价格会上涨,对于供给侧而言,收入会下跌」。
接下来,我们来考虑最后一个问题,什么因素决定了赋税的分担比例,即税负归宿(Tax Incidence)呢?从供求曲线和需求曲线来看,似乎曲线越「平」,形成的阴影面积会越小。从价格弹性一节中,我们可以知道,曲线的「平度」可以用价格弹性度量。也就是分担的税收越少。如下图所示,展示了在供给侧征税时,税赋分担比例随需求曲线的价格弹性的变化。
A 图中,需求曲线平缓,需求非常富有弹性。此时,可以看到蓝色部分消费者负担的税收,是要少于绿色部分卖家负担的税收的。B 图中,两者的弹性相比不大,消费者与卖家负担的比例差不多。C 图中,需求曲线较陡,需求非弹性,蓝色部分消费者负担的税收要多于卖家负担的税收。总结起来,税赋会由弹性更加弱的一方承受更多,无论政府选择向哪一方征税(向需求侧征税的场合,读者可以仿照本节方法自行证明)。
本节不构成任何投资建议。
在了解了税赋归宿之后,我们可以来脑内演习一下,当政府征收房产税之后,会如何影响租房市场。
首先,由于征税的本质,市场租金(短期内)会上涨。房产税是财产税,也就是对房东持有房地产的这一行为征税。显然,房东有将房产税转嫁给租客的动机,是完全合理的。从什么决定了税负归宿这一节中,我们知道税负的分担比例是由双方的价格弹性决定的。那么,有什么因素影响双方的价格弹性呢?
在需求(租客)侧,有以下因素影响价格弹性:
必要性:如果一件商品对于需求侧更加必要,那么需求更加趋于非弹性。对于租客而言,如果无法在生活的城市购置房屋的时候,租房是唯一的选择,有较高的必要性。
替代商品:如果一件商品有替代商品的话,需求趋于弹性。
对市场的定义:需求曲线的弹性也取决于我们对市场的定义。市场范围越小,需求曲线更加趋于弹性。以租房市场为例,北京的租房的需求会比全国的租房市场的需求更加弹性,海淀区的租房需求会比北京的租房需求更加弹性。原因是,市场范围越小,越容易找到替代商品,比如以海淀区的租房市场,昌平区、朝阳区、丰台区的出租屋都构成海淀区的出租屋替代商品。
时间跨度:考虑的时间跨度更长,需求趋于弹性。
虽然,租房对于租客而言有一定的必要性。但是就替代商品而言,租客可以较为方便的流动,有丰富的替代商品可以选择。另外,租客也可以选择以买房或者移往别的城市的方式退出该地的租房市场。因此,考虑各种因素,需求侧具有一定的价格弹性。
在供应(房东)侧,有以下因素影响价格弹性:
生产的灵活性:如果供应者可以很灵活地调整供应量,那么供应趋于弹性;
时间跨度:与需求相同,考虑的时间跨度更长,供应趋于弹性;
假设,房东会出租所有非自住房屋1。房东调整供应量的方式只有买卖房屋的方式。对于房地产价值高企,未来变化不明的现在,买卖房屋会是一种风险很高的决策。如果,房东无法灵活地调整供应量,那么供应侧的价格弹性会比较低。
综合上面的两种考虑,如果征收房产税,房东想要将税转嫁到租客侧,由于双方价格弹性,房东无法将所有的房产税转嫁到租客侧,甚至有较大可能,供给侧价格弹性较低,房东必须自行负担房产税的大部分。
在文章中,我们主要讨论了「征税对价格的影响」。了解了「价格由供需共同决定」、「价格弹性」以及「税收归宿」三个概念。当政府对一件商品征税的时候,供需双方的税负分担比例由各自的价格弹性决定。
以租房市场为例,如果政府推出房产税,在短期内,房租会上涨,但是不会完全负担房产税。根据价格弹性的分析,有较大的可能,房东自行负担房产税中的大部分。
[1]: Mankiw N G. Principles of Economics[M]. Nelson Education, 2014.
1:考虑到房屋空置时除了资产价值的变化,不会产生任何其他的收入。基于理性,假设房东会出租所有非自住房屋。
]]>2020 年,对于全世界而言会是非常不平凡的一年。肆虐的 COVID-19 疫情使世界经济受到了重大的打击。由于病毒的威胁,人们或主动或被迫接触 Work from Home(WFH)这一全新的工作形式。尽管,由于仓促上阵,人们没有掌握正确的 WFH 的工作方法,很难在 WFH 环境下发挥 100% 的工作效率。但是,这次突然的检验会暴露很多现有实践与工具的问题。WFH 是一种非常具性价比的工作方式,我看好 WFH 的未来。
各国对 COVID-19 疫情应对的方式不同导致了疫情发展的结果也不同。率先控制疫情的国家将会吃到后疫情时代起步早的红利。世界上巨头国家力量的相对变化到底会对人们的生活造成什么影响?这又是一个非常值得关注的发展方向。
2020 年是一切皆有可能之年——美国总统特朗普未能在大选中连任、英法美首脑确诊感染 COVID-19、桐生可可涉台问题导致 Hololive 被驱逐出中国市场等等。不禁令人感叹 2020 年真是魔幻的一年啊。
第一次完全独自一个人生活,有了足以养活自己的收入,两件快乐事情重合在一起…咳咳,就不咏唱圣经了。今年在生活中算是圆儿时梦的一年。
在大概初中的时候,我就拥有了一台自己的 PC。那是我爸从附近网吧淘汰的机器中淘回来的电脑。CPU 是一颗 Intel 赛扬处理器。在那台电脑上打一些大型游戏非常的卡。那时我非常沉迷《微软模拟飞行 2004》(也被称为 FS9),基本上只能跑出 20FPS 左右的帧率。这个习惯也因此培养了我现在 「10 帧能玩,20 帧流畅」的低要求。后来《微软模拟飞行 10》(FSX)流行起来,当时的主流配置都无法满足 FSX 的要求,更不用说我那赛扬老机器了。因此,我儿时最大的愿望可能就是拥有一台能够流畅运行 FSX 的 PC 了。
在今年里,乘着 AMD YES!的风潮,我终于拥有了一台高性能的 PC。3900 + 2080Super + 32G 足以流畅运行市面上所有 3A 大作。初次之外也购置了 Honeycomb 飞行摇杆、节流阀与脚舵三件套。终于(在硬件上)成为了键盘飞行员(笑)。完成了儿时的愿望。另外值得一提的是,2080Super 却是被老黄暗算了啊。
2020 年是我作为全职前端工程师的第一年。由于所在的组是业务的第一线,这一年里主要的精力都投入到了这种业务之上。这一年在工作中的体会有两点:一是业务逻辑抽象的困难性;二是开发方式上的随意性。
业务不像理论有缜密的逻辑。业务是谈判、妥协与实验的产物。代码中充斥着特例,给抽象带来了很大困难,因此很难有业务逻辑上的沉淀。很多时候,我们都在重复造轮子,工作在编程语言与框架提供的最低的抽象层级,开发效率上难以提升。
在公司业务中开发方式是随意的。你可以使用任何开发方法论去开发一个功能。一个功能可以没有具体的结构设计,在开发过程中可以随意发散。最后的产物可能是一个功能完好,却很难重用与扩展的软件。另外,在开发过程中很少有单元测试与集成测试,使得后来人几乎无法重构,渐渐项目代码就腐化为了臭不可闻的遗留代码。
前几天和朋友开玩笑说:「2021 年的新年愿望是写完 2020 年的年终总结,2020 年未完成的愿望是写完 2019 年的年终总结」。的确,去年我没有发布 2019 年年终总结。但是很遗憾没能够完成它。它还躺静静地在我的草稿箱里。2020 年中,做的不好的地方可以总结为:
由于年初没有建立目标,可以说今年我是没有任何既定目标,在盲目地摸索。没有目标很爽,因为我无法得到一个「控制信号」 ,结果怎么样都好。但是长期而言,没有目标是很危险的,就像前面所说的随意发散的软件,没有目标的人非常容易沉沦与腐化。因此,明年一定要改变这一现状。
一个良性的渐进改良过程离不开正确的反馈。比如,目标是提高编码效率,首先我们需要定义「编码效率」这一概念的具体度量指标,是完成一个需求的平均天数?还是每日编码的平均行数?然后,通过实验备择的改良方式,观察备择方式对指标的影响,从而做出决策。但是,在今年,无论是生活还是工作中,都没有能够建立起正确的反馈渠道。因此无从对生活方式或者工作方法进行改良。久而久之就会一直陷在一个效率较低的方法论里。
谈到将要到来的 2021 年,为了终结「没有计划性」这一现状,为明年立一些 flag 就成为了 2020 年最后的重要工作。我为明年挑选的 flag 大概可以列为:
今年,我在前端工作中实践了 TDD,使用了 Jest 与 Cypress 对前端项目编写了完整的单元测试与端到 E2E 测试。在实践中,收获了很多经验,也踩了很多坑。我认为任何严肃的软件项目都应该有一套完整的测试用例。我希望能够将这些经验系统化起来,形成一个系列文章。帮助软件项目逃离祖传代码的诅咒。
作为软件工程师,参与高质量的项目无疑是成长最快的方法。而在软件领域,能够参与的最高质量的项目无疑又是一些知名的开源项目,如 Node.js、React、Vue 等。这些项目支撑了全球数以亿计的前端业务。从中可以一窥究竟大型项目的组织、设计思路以及工程实践。对于工程师而言,这些经验是不可估量的宝贵财富。
第 3 个 flag 是「至少发布 12 篇有质量的技术文章」。在这里,「有质量的技术文章」指的是由自我思考得出来的具有严谨逻辑的技术文章。在工作中,我发现「输出观点」对于工程师是非常重要的。然而,没有人会认同毫无根据的观点。要「输出观点」,严谨的逻辑是必要的。锻炼逻辑,最有效的方式可能就是写文章了。
最后一个 flag 是「掌握系统化的开发方法,并熟练运用于日常开发中」。在前面,我也提到过,在公司中开发方式一般是随意的、不成系统的。随意的软件设计导致了成品代码耦合程度高,难以维护。在 2021 年,掌握系统化的开发设计方法,从像 Code Complete 这样的软件设计经典中取经,将这些标准流程用到日常的开发实践中。提高软件架构设计的能力。
在文章的最后,祝愿大家接下来的 2021 年里身体健康,万事如意。希望席卷全球的新冠疫情能够平息下来,世界各地的人们的生活能够恢复正常。 Everything will be better.
]]>在讨论逻辑与数据的对立统一时,首先我们应该对逻辑与数据做出一个定义。
逻辑是指解决一类问题的具体步骤。比如想要知道一杯未知液体是否是酸性的,我们可以用下面的步骤来判断:
严格而言,在本文中,逻辑与算法的定义是等价的。
数据指的是一种概念的具体化的产物。比如,文章这个概念可以具体化为一个含有标题,内容等字段的记录。在数据库中,文章表中的一列可以看作一个数据。
如果把程序看作一个工厂,数据就是原料,而逻辑就是机器。我们把原料放到机器里,最终的产物就是我们期望的结果。任何有意义的程序都是逻辑与数据的有机结合。就连最简单的 Hello World
也不例外。
然而,在和谐共存的表面下,逻辑和数据在灵活性却有着截然相反的特性。逻辑就像房屋的骨架,在整个生命周期里都是固定的;而数据就像是房屋的外墙,可以红色油漆来粉刷,也可以用黄色油漆来粉刷,在生命周期里可以多次改变。
逻辑在程序的整个生命周期是不可变的。如果想要改变程序的逻辑,必须要更改程序的源代码,有必要时需要重新编译,最后重新运行。比如,有这样一个程序,当用户的积分大于 800 时,赋给用户一些高级权限。用伪代码描述如下:
1 | function processPrivilege() { |
有一天,由于营销策略的变更,需要将这个积分阈值改成 700。此时,我们无法不停机地(on the fly)满足这一需求。要满足需求,我们需要将这一段代码更改为:
1 | function processPrivilege() { |
之所以我们无法不停机地满足这一需求,是因为,我们把积分阈值 800 当作了逻辑的一部分。在之前这段代码中,我们解决何时赋给用户权限问题的方法是—— 当用户的积分大于 800 时,赋给用户一些高级权限。很显然,在解决上一需求的现场,我们并没有考虑到积分阈值可能是会变化的。我们轻率地把积分阈值硬编码到了逻辑中。当需求变更时,我们必须通过调整逻辑来满足最新的需求。在实际项目的开发中,类似这种需求是非常常见的。
逻辑的不可变性进一步会带来开发效率的下降。调整逻辑需要研发全链路的参与,产品需要提出正式的需求文档、开发人员需要根据需求文档调整代码、QA 需要对需求进行测试验收、最后可能还需要发版或者上线才能使改动的逻辑生效。有时,涉及代码改动也许仅仅几行,然而整个流程会被拉得很长。
与逻辑的不可变性相对的,数据具有高度的可变性。
以一个寻路程序为例,假设我们有一个方法 findBestRouteBtw(origin, dest)
可以找到从出发地origin
到目的地 dest
的最佳路径。如果,我们想找从北京西站到北京站的最佳路径,我们只需要调用findBestRouteBtw('北京西站', '北京站')
就可以得出答案。想要找从天安门到首都机场的最佳路径也没有问题,调用 findBestRouteBtw('天安门', '首都机场')
即可。
对于方法 findBestRouteBtw(origin, dest)
而言,origin
与 dest
表示的是一种概念,origin
表示的是一个地点,是出发地。dest
表示的也是一个地点,是目的地。它不与某个概念的具现所绑定,因此具有极为强大的灵活性。我们可以传入各种各样的地点(数据),它都能够给出从目的地到出发地的最佳路径。计算机的强大也来源于此。
对于一些需求变更,也可以通过变更数据来满足。例如,需求需要有一个程序,能够实时展示当时上证综指的指数。很明显,上证综指是一个实时变化的量。这个量我们需要通过某个接口去获取,用伪代码表示如下:
1 | const sseIndex = await getSSEIndex(); |
可以看到,获取上证综指已经委托给了一个方法 getSSEIndex
。这个方法可以通过调用上海证券交易所的接口,也可以调用其他第三方接口获取指数。显然,指数每次变化都不需要重新编码或者重新部署程序。
逻辑和数据是可以互相转化。从数据转化为逻辑很容易,比如硬编码某个值就是把数据转化为逻辑的一种。这里不再赘述。
逻辑也可以转化为数据,以用户提权例子为例。当用户积分大于 800 时,赋予用户一些高级权限。如果我们意识到,积分阈值是可变的,我们就可以把积分阈值当作一个参数。
1 | function processPrivilege(privilegeThreshold) { |
在上述代码中,800 仍然是硬编码的。但是,将积分阈值作为一个形参抽象出来,给积分阈值的变化带来了可能。我们稍作一些变化:
1 | function getPrivilegeThreshold() { |
这样的话,积分阈值完全成为了一个数据。我们可以从任何地方获取积分阈值这个数据,比如从数据库获取、从某个微服务接口中获取等等。此时,如果有一天积分阈值需要从 800 变到 700,我们只需要把数据源中的对应值改成 700 即可。不需要修改源代码,不需要提测,不需要上线,可以节省大量时间。
在理想世界中,我们希望任何需求都可以通过修改数据的方式来实现。显然,这种方式是最有效率的。但是,很多时候我们很难看清楚逻辑与数据的边界,因此错误地把数据当作逻辑,或者把数据当作逻辑来实现。
误把数据当作逻辑的例子有很多,比如各种硬编码的值。误将逻辑当作数据实现不常见,但是也存在。这种错误最典型就是抽象过度,例如一个计算圆的周长的函数
1 | function getPI() { |
这里圆周率并不是一个可变量,并不需要可变性。因此把获取圆周率抽象成一个函数是不恰当的。更好的方法是把圆周率作为一个常量看待:
1 | function calculatePerimeter(radius) { |
抽象过度会极大地损害程序的可读性与可维护性。因此也是需要极力避免的。
因为我们很难一次性弄清楚逻辑与数据的边界,所以,随着业务需求变化对程序进行调整是必要的。我们需要把一些逻辑调整为数据,或者把数据调整为逻辑。这种调整就是重构。重构本身是非常大的话题,在本文中就不展开了。
setState
与生命周期的延伸。其实不然,Hooks 创造出了一种更加声明式的编程范式。带建议的输入框是非常常见的需求,例如 Google 的搜索建议。
在这种需求中,一般而言后台会提供一个 API,我们以用户输入作为关键字调用这个 API 来获取备选词的列表。我们一起来使用 Class-based Component 来实现这个组件。为了简单起见,我们实现输入框和建议列表,点击建议项回填到输入框暂时不实现。
首先,我们需要一个输入框和一个显示建议项的列表。
1 | class AutoComplete extends React.Component { |
然后,由于需要以近乎实时的方式给用户提供建议,用受控组件的方式来管理用户输入是更好的选择。这里我们新增加了一个状态keyword
来保存用户的输入,增加了一个事件处理函数 handleKeywordInput
。将 handleKeywordInput
绑定在 onChange
事件之后,我们的状态 keyword
就与用户输入同步了。
1 | class AutoComplete extends React.Component { |
接下来,我们需要获取建议项的列表。显然,最好的时机是componentDidUpdate
生命周期。同样为了简单期间,我们不调用真正的 API,用下面一个根据关键词来生成一个列表的函数来替代。
1 | function fetchSuggestions(keyword) { |
有了这个伪API 之后,我们需要一个新的状态suggestions
来保存获取到的建议项,并且使用map
将这些建议项渲染出来。最终的代码就像这样。
1 | class AutoComplete extends React.Component { |
Demo 如下:
See the Pen Suggestion CBC by Qingyu Deng (@ayase-252) on CodePen.
让我们回想一下设计组件的整个思考流程:
keyword
与事件处理函数handleKeywordInput
来实现受控组件;componentDidUpdate
这个生命周期中调用 API。可以看到,我们的思路仍然是命令式的。因为我们需要获取建议列表,所以我们要在组件的某个生命周期里去做这件事情。这样的思路对于熟悉命令式的开发者而言是很自然的。但是,如果我们从另外一个角度来看,是不是会有一些新的想法呢?
从需求来看,建议列表suggestions
是我们通过向一个 API 输入关键词keyword
获取的。从数据的关系来看,建议列表suggestions
就像是关键词keyword
的卫星数据。换句话说,suggestions
是依赖的keyword
。想起数据依赖,熟悉 Hooks API 的同学可能会想起useEffect
等 API 中的依赖数组。没有错,这些 API 就是我们刻画数据依赖关系的核心。
在上一节中,我们发现了suggestions
依赖于keyword
。我们可以很方便地用useEffect
来刻画这样的数据依赖关系。现在,让我们用 Hooks 重写这个组件。
首先,我们仍然会有一个keyword
状态与一个suggestions
状态。然后,为了表现两者的依赖关系,我们使用以[keyword]
为依赖数组的 useEffect
来自动地在keyword
改变的时候重新获取相对应的suggestions
。
1 | function AutoComplete() { |
Demo 如下:
See the Pen Sugguestion with Hooks by Qingyu Deng (@ayase-252) on CodePen.
我们回顾使用 Hooks 实现的思路。我们不再是为了获取某个数据,然后在某个生命周期里执行某些操作去考虑。而是变成了,因为某个数据 A 是依赖某个数据 B 的,所以在被依赖数据 B 改变的时候,我们也应该通过某些手段让数据 A 与数据 B 同步。我们不再刻意地去挑选同步的时机,刚渲染完也好(componentDidMount
)、刚更新完也好(compnentDidUpdate
)甚至是任意时候,只要框架能够保证这两个数据是同步的就行。正因为如此,在 Hooks 的作用下,React 的数据管理能够变得更加声明化。
由于我们不必要去挑选数据同步时机,组件的生命周期这一概念消失了。数据之间的交互与组件彻底解耦,因此,Hooks 带来了第二个好处——逻辑重用。我们可以很轻松地将通过关键词来获取建议的逻辑封装起来变成一个自定义 Hook——useSuggestions
。
1 | function useSuggestions { |
现在我们可以在其他组件上也可以用到根据关键词提供的建议列表了。甚至,通过参数化请求方法,我们可以创造出更加通用的 Hook。具体的组件不需要知道里面的数据之间的交互逻辑。因此,Hooks 开创了可以将一些业务中典型的数据交互逻辑抽象出来的方法。这一方面,无论是之前的 Class-based Component 或者 Vue 的响应式系统都是没能做到的。
Hooks 强调数据之间的依赖关系,在 Hooks 的框架下:
前者免除了我们需要手动同步相关联数据的需要,抹除了组件生命周期的存在意义;后者可以将业务中典型的数据交互逻辑抽象出来应用于其他组件中。通过 Hooks,我们实现组件的思路得以更加向声明式与数据驱动靠近。
]]>本篇日志将有大量剧透。
诚哥电影的一个标签就是爱情故事,诚哥擅长制作各种各样的爱情故事。然而诚哥的爱情故事不总是只有一种中心主题。比如《秒速五厘米》诉说了一种带有缺憾的爱情;《你的名字。》讲述了「所有的相遇都是久别重逢」的爱情。《天气之子》也不例外,而且这种爱情更为奔放,我称之为一种「只要有爱,世界与我何干」的爱情。
如果你看过《你的名字》的话,也许你会产生一种既视感。是的,《天气之子》的剧情模式与《你的名字》是大致相同的。《你的名字》的剧情模式是:
《天气之子》有着相似的剧情模式:
可以看到《天气之子》的剧情模式复制了大为成功的《你的名字》的剧情模式。所以,观影过程中也许出现某种既视感是正常的。我个人对于使用同样的剧情模式是有一点不满意的。毕竟 3 年过去了,个人还是希望看到更多不一样的东西。
每看一部动画或者电影,都是一种独特体验。尽管剧情模式相同,然而观看《天气之子》的体验也是独特的。《天气之子》的主题更加直接——「只要有我喜欢的人在,世界与我何干」,非常唯我的立场。
有轨电车问题是一个经典的道德两难问题。《天气之子》中最主要的剧情冲突抽象起来就是一个有轨电车问题。对这个问题的解答将会体现作者在个人利益与群体利益之间的偏好。
有轨电车问题是一个这样的问题。有一辆刹车坏掉的电车,即将撞上前方轨道上的五个人,所幸的是在电车前方有一个道岔,如果你扳动这一道岔就可以让电车驶入备用轨道,但是备用轨道上有一个人。
在剧中,只要牺牲阳菜,东京就可以放晴。对应到有轨电车问题上就是,电车前方是东京,而备用轨道上是阳菜。我们可以很容易看到诚哥在这个问题上的抉择。扳什么道岔?让东京自生自灭吧。对这个问题的解答,也就奠定了《天气之子》的主题——「只要有我喜欢的人在,世界与我何干」。即便是与整个社会为敌,我都要和你在一起。
《天气之子》描述的是一个处于低潮的主人公们的故事,我们可以看到故事的角色大多处于低潮,像失业、未成年、离家出走、缺少收入、丧母、丧偶等等甚至还被警察调查、逮捕等等。尽管在《你的名字》中三叶直接遇难了,但瀧依然是衣食无忧、不用担心生活的状态的。所以我认为,《天气之子》的低潮更加的“低”。给人一种喘不过气来的感觉。
《天气之子》的故事推进,我认为更加“无谋”一点。主人公们似乎没有思考,没有选择地直接奔着结局前进。这给观众一种主人公是“铁头”的感觉。当然,我在思考之后觉得也许是因为在当时的情景下,他们没有选择。毕竟他们的人生是真的难啊。
离《你的名字》3 年之后,诚哥交出新作《天气之子》。从观影体验上来说是值回票价的。这里没有提电影的画面以及音乐,我认为没有必要提。诚哥的作品这些元素的质量是业界顶尖的。《天气之子》用与《你的名字》同样的模式,写出了一个不一样的,更加直接与唯我的故事。我个人而言还挺吃这一套的。希望诚哥下个作品能够带来一种更“新”的感觉。下一部作品我还会再去看的。
]]>本文将会从正则表达式的数学原理出发,看正则表达式如何仅仅从用 3 种基本运算就能够表示各种各样的句法规则。
通过本文,你将会了解:
从数学角度重新定义正则表达式;
正则表达式的基本运算只有 3 种;
正则表达式如何通过这 3 种正则运算扩展;
如何构造一个复杂的正则表达式,如验证电子邮件地址的正则表达式。
本文假定读者有一些集合论的基础,特别需要了解集合的并的概念。
经常,我们需要去提取一些符合一定规则的信息。例如,我们的手机号由 11 位数字组成,11 位数字就是一个规则,或者说叫做模式(Pattern)。我们可以很容易地理解 11 位数字是什么。但是,计算机却不会简单地理解这一概念。因此,我们需要构造一种方法,最好是有明确规则的方法。使用这种方法,让计算机知道我们所需要查找的字符串的模式,然后从原始字符串中把所需的信息提取出来。
美国数学家Stephen Cole Kleene教授就发明了这样一种方法——正则表达式。
为了研究正则表达式,我们需要先定义一些前置概念:
字母表(Alphabet):一个符号的有限集合。典型的字母表有像英文字母表 a-z,数字字母表 0-9。当然不限定是上述两类。任意符号的有限集合都是字母表。
字符串(String):一个由从字母表中抽出的符号组成的有限序列,如 abc 就是定义在英文字母表上的字符串。我们用$|s|$表示一个字符串的长度,并且定义空字符串$|\epsilon|$是长度为 0 的字符串,即$|\epsilon| = 0$。
语言(Language):某个字母表上字符串组成的可数集。如 $L=\{0,1,2,3,4,5,6,7,8,9\}$组成了一种语言,这种语言只包含位数为 1 的数字。定义只包含空字符串的语言为$\emptyset$。
可以看到,语言是一个字符串组成的集合,我们的目的就是去找到一种方式描述这种语言中字符串的模式。
在开始正则表达式的探索之前,我们先定义一个字符串之间拼接的操作。这个操作很简单,如果字符串$s$与字符串$t$进行拼接,会得到字符串$st$。假如$s=\text{cat}$、$t=\text{house}$,那么$st=\text{cathouse}$。为了更加简便地表示同一个字符串之间的拼接操作,这里定义一个类似数学上指数的操作,令$s$为字符串,那么定义$s^n=\underbrace{s \ldots s}_{n个}$。可以简单理解为$s$的$n$次重复。
有了前置概念之后,我们将会定义 3 个语言之间的基本运算,有了这 3 个运算,我们可以使用一些基本的语言来表达出更加高级的语言。比如使用数字作为基本语言,表达出 11 位手机号。下面$L$,$M$均表示一种语言。这三种运算分别是:
$L$与$M$的并:$L\cup M=\{s|s \in L 或 s \in M\}$
$L$与$M$的拼接:$LM = \{st | s \in L 且 t \in M\}$
$L$与$M$的 Kleene 闭包:$L^*=\cup_{i=0}^{\infty}L^i$
其中$\cup{i=0}^{\infty}L^i=L^0 \cup L^1 \cup \ldots$,$L^n = \underbrace{L \ldots L}_{n个}$。
通过语言之间的并操作,我们可以通过两个基本语言扩展成一个范围更大的语言。如使用一个包含大写字母与小写字母的语言$L=\{A,\ldots,Z,a, \ldots, z\}$与包含数字的语言$D=\{0,\ldots,9\}$,通过并操作我们可以获得一个既包含大小写字母也包含数字的语言$L\cup D = \{A, \ldots, Z, a, \ldots, z,0,\ldots,9\}$。
接下来是语言之间的拼接操作。从定义可以看到,拼接操作产生了类似笛卡尔积的效果。拼接操作可以极大地扩展语言。例如$LM$就产生了一个长度为 2 的字符串,其中第 1 位是字母,第 2 位是数字。显然,字符串$\text{a1} \in LM$。
最后一种操作是 Kleene 闭包,这个操作可以将语言自身从 0 次重复(空语言$\emptyset$)到无限次重复产生的所有语言并起来。如$D^*=D^0 \cup D^1 \cup D^2, \ldots$,$D^0$是一个只包含的空字符串的语言,$D^1$是一个只包含 1 位数字的语言,$D^2$是一个只包含 2 位数字的语言,一直到$D^{\infty}$,因此$D^*$表示所有正整数加上空字符串。
有了上一节提到的 3 种运算,再定义一些基本语言,我们就可以用这些运算来表示另外一种符合某种模式的语言。为了简单起见,我们这一节还是使用前面定义的两种基本语言,字母语言$L=\{A,\ldots,Z,a, \ldots, z\}$,数字语言$D=\{0,\ldots,9\}$。
现在我们想表示一个即包含字母也包含数字的语言,我们该怎么表示呢?显然,我们可以使用$L\cup D$。为了简化表述,我们接下来用语言中字符串的模式来表示语言本身。
接下来,我们想要表示一个包含长度为 2 的仅包含字母的字符串,该怎么表示?显然,使用一次拼接就可以$L^2=LL$。长度增加到 3 ?再加一次拼接,使用$L^3$就可以表示。那么增加到$n$位?那么我们就拼接$n-1$次,使用$L^n$。最后,我们想表示一个不固定长度仅包含字母的字符串(可以包含空字符串),使用 Kleene 闭包$D^*$即可。使用这些技巧,我们可以表示$n$次重复的字符串。
最后,我们把这些运算结合起来,可以表示模式更加复杂的语言:
$L(L \cup D)^3$:以字母开头的一个长度为 4 的字符串,后 3 位可以由数字与字母组成;
$DD^*$:所有正整数(由于与一个$D$进行了拼接,不含空字符串);
$L^3 \cup L^4 \cup L^5$:由 3 到 5 个字母组成;
$\emptyset \cup L^1$:由 0 到 1 个字母组成。
下面有一个小问题,如果要表达一个以两个字母开头,两个数字结尾的字符串?
如果对正则表达式的语法比较熟悉的同学可能已经发现了,在正则表达式中对应上述几种常见模式的简写。
到这里,我们已经掌握了正则表达式背后最基本的数学原理了。没错,就那么简单。接下来,为了进一步形式化我们上面用集合语言表达的想法,就得出了正则表达式,它包含一套运算,以及一套优先级的定义,使得我们可以简化在大部分情况下需要的括号。
基本运算还是上面提到的 3 种,但是变换了符号:
并:$(r)|(s)=L(r) \cup L(s)$
拼接:$(r)(s)=L(r)L(s)$
Kleene 闭包:$(r)^*=(L(r))^*$
$(r) = L(r)$
第 4 种运算的出现是为了让某种语言的表示与语言本身分开,如我们可以使用\d
代表一个数字语言$D$。这么的话就有(\d) = L(\d) = D
通过设定合适的优先级,可以免去大部分表示优先级的括号的需要,我们设定的优先级如下:
一元运算符 Kleene 闭包$*$具有最高的优先级,具有左结合性;
拼接运算符有第二高的优先级,具有左结合性;
并操作符$|$具有最低的优先级,具有左结合性;
遵循上述运算优先级,我们可以把$(a)|((b)^*(c))$简化为$a|bc$。
到这里,我们已经完全掌握了如何使用正则表达式去表示某种模式。
为了便于使用,编程语言中的正则表达式对两个方面进行了扩展。第一,为常用的模式定义了新的运算;第二,为一些常用的基本语言提供了简便表示方法。下面以 JavaScript 为例:
新的运算包括:
含空集的不定长度字符串$LL^*$,我们将它表示为$L^+$,也被称为正闭包(Positive Closure)。
长度在一定范围内的字符串$L\{m,n\}=\cup_{i=m}^{n}L^i$
可选字符串,空字符串或者长度为 1 的字符串$L?=\emptyset | L$
新的常见基本语言表示方法:
通配.
,任意除了\n
、\r
、\u2028
(LINE SEPARATOR)、\u2029
(PARAGRAPH SEPARATOR)之外的字符。
字符集[abc]
:[abc]
是$a|b|c$的简便写法。在字符集中通过-
字符可以指定一个字符范围。
数字\d
:[0-9]
数字、大小写以及下划线\w
:[a-zA-Z0-9_]
取反[^abc]
作为练习,我们用正则表达式来符合 RFC-5321 规定的电子邮件地址,进而验证一个地址是否符合标准。
根据RFC-5321(Simple Mail Transfer Protocol):合法的电子邮件地址的格式为<local-part>@<domain>
,这里使用<name-regexp>
表示一个子正则表达式,定义如下:
1 | # local-part可以是两种字符串<Dot-String>或者<Quoted-String>之一 |
按照标准,我们可以逐步从子正则表达式入手,逐步地拼接成更加高级的子表达式,直至形成最终的表达式。由于这个正则表达式非常复杂,我们使用模板字符串来一步一步的拼接形成最后的表达式,代码如下:
1 | const atext = `[a-zA-Z0-9!#$%&'*+-/=?^_\`{}\|~]` |
现在我们已经写好了一个可能是正则表达式中最难的一个——验证邮箱地址的正则。
在本文中,我们从几个基本概念——字母表、字符串以及语言出发,介绍了构成正则表达式的 3 种基本操作——并、拼接与 Kleene 闭包。我们使用了这 3 种基本操作,从字母语言与数字语言出发,表达了一些具有复杂模式的语言。
通过重新定义基本运算以及运算优先级正式化了正则表达式,并且探讨了正则表达式的一些扩展。最后我们使用了 JS 的正则表达式,实现了一个符合 RFC-5321 的电子邮件地址验证器。
]]>复制字符串问题,就是实现下面这个函数repeatStr
。这个函数接收source
, times
两个参数,返回重复times
的source
字符串,如repeatStr('abc', 2)
就应该返回abcabc
。
1 | function repeatStr(source, times) {} |
这个问题初看起来非常简单,我看到这个问题的第一眼就想到了迭代解法。问题要求我重复多少次,我就循环多少次嘛。Easy Question。实现如下:
1 | function repeatStrByIteration(source, times) { |
一切完美。但是…它还可以优化吗?如果想让它跑得更快该怎么办?(我:嗯?还有跑得更快的方法?)
其实真的还有跑得更快的复制实现。但是我们需要转换一下解决问题的思路。
在复制中,费时间的操作是字符串之间的加法,因为string
是不可变类型,每一次改变它都需要创建一个新的string
对象出来。所以我们优化的方向是如何尽可能地减少两个字符串相加操作。
在迭代实现中,对于$times = n$,我们需要相加n
次。联想到二分查找,一个很自然的想法就是——我们可以将目标字符串*“对折”*起来相加,比如,我们要重复 10 次a
,可以通过'aaaaa' + 'aaaaa'
得到最后的字符串,这里需要 1 次加法,然后为了构造aaaaa
,我们可以通过aa+aaa
,需要 1 次加法,aa
可以分解为a+a
,需要 1 次加法,最后构造aaa
,可以分解为a+aa
,需要 1 次加法。可以看到,构造整个目标字符串aaaaaaaaaa
所需要的加法次数,从 10 次下降到了 4 次。
可以通过二叉树的性质证明我们的算法的时间复杂度是$O(\log n)$的(二叉树的高度)。实现如下:
1 | function repeatStrByBinaryJoin(source, times) { |
我们在 node 环境下进行测试。我们使用两个函数执行复制一千万次abc
,测试函数脚本如下:
1 | const { performance } = require('perf_hooks') |
输出如下:
1 | repeat by iteration: 2006.9261000156403ms |
可以看到我们的二分复制版本相较于迭代版本从时间上来看性能有了巨大的提升。
通过本文,我们使用二分技巧成功地将复制字符串这个工作的复杂度从$O(n)$降低到了$O(\log n)$。
]]>master
分支rebase
一下,然后整理一下 commit 记录。临近下班了,又要面对一天中最困难的问题——晚上吃什么?楼下那家 KFC 的菜单已经吃完一轮了。突然之间,我隐隐感觉有一点不太对劲。一看git log
。Oh shit!!我惊出了一身冷汗,瞬间没心思考虑晚饭问题了。看了一眼本地master
分支,最后一条 commit 记录的 SHA 和远程master
最后一条 commit 的 SHA 不一样。诶?远程仓库的 merge commit 全不见了。然后一看我刚刚敲的命令:
1 | git checkout master |
Oh, shit!我在干什么?我以前听说过搞乱 Git 仓库是很麻烦的问题。此时无数想法从脑中闪过:难道 feature 要重写吗?好几百行啊,而且 feature 已经快到 deadline 了。我慌了,真的慌了。还是先喝口水冷静下来吧。
冷静下来之后,我想我为了面试看过一点点 Git 的基本原理。从原理入手,一点点小心地修正记录的话应该是可以修好的。此时最重要的是理解现在仓库的历史。Don’t Panic。
在这个项目中,生产代码单独是一个 repo。开发的时候,开发者fork
生产代码的 repo,然后clone
自己fork
的 repo。在本地,按照惯例,我自己fork
的 repo 是origin
,远程生产代码的 repo 是upstream
。开发新功能的时候,我从本地的master
分支checkout
一个特性分支feature
。在开发了几天之后,远程生产代码已经合并了数个 PR。在我的愚蠢操作之前,整个项目的分支情况是下面这样的:
1 | C5---C6---C7---C8 feature |
其中Mx
是 merge commit。在愚蠢的操作之后,由于rebase
默认丢弃掉将要rebase
分支的merge commit,项目的分支情况变成了:
1 | C5'--C6'--C7'--C8' feature |
知道病因之后,似乎还有救的样子。首先,我们可以在feature
分支中以C1
为起点、剔除掉C2'
、C3'
。这样,feature
就恢复了之前的状态。然后,我们将master
分支回退到origin/master
。这样,master
分支也恢复了。最后做一遍正常的rebase
操作应该就行了。未来实现这个方案,我们可以使用两把非常好用的“手术刀”——reset
、还有rebase
本身,在这个情况下可以使用。
首先我们做第一步,以某个 commit 为起点,剔除掉一些 commit,我们可以使用git rebase -i start_commit_sha
命令。-i
参数表示interactive
。执行之后,git 会调用一个文本编辑器打开一个文件,里面的内容是从起点之后的第一个 commit 到最后一个 commit 的所有 commit。commit 可以被更改、压缩(squash/fix up)、删除。要删除某个 commit,只需要在编辑器中把该行删掉即可。保存退出之后,git 会根据这个文件的内容进行rebase
操作。
删掉C2'
、C3'
之后,项目结构变成:
1 | C2'--C3' master |
然后,在master
分支中使用git reset --hard commit_sha
。这个命令的语义是将HEAD(分支的指针)、工作目录、暂存区重置到commit_sha
指向的 commit。这里官方文档解释得很清楚。
此时项目结构变为:
1 | master |
和没经过愚蠢操作的项目结构是一样的。此时只要来一遍正常操作就行了。
解决 git 仓库被意外破坏的问题,最重要的是要冷静。只有冷静下来,才能够准确地分析出现在 git 仓库的状况。然后看情况使用对应的工具操作。git 其实内置了很强大的对 commit 历史进行操作的工具,所以大多数的 git 操作失误是不用重写的。
当然,最重要的是操作 git 仓库的时候要专心。敲命令真的很容易错的 orz。
]]>争取在 24 点之前填完这个坑吧(笑)。
在 7 天前,我在一个年终总结的贴子上发了不是“还有 7 天吗?我还能拖”。(笑)事实上,我在很久之前就在想写一篇年终总结了。可是今年发生了太多,回看起来我有点百感交集。
首先,今年调了很久的参,发了一篇会议论文。然后,今年又是毕业前的最后一年,对于我来说重中之重当然是找工作了。但是没想到的是满怀期待开始,却以零收获结束。对于这一滑铁卢,我总结起来原因有二。第一是,我没能够理解社会运行的机理。社会是非常讲求效率的。因此,如果看到有想要抓住的机会一定要马上抓住。第二是,我没能够提前了解业界对工程师的要求,导致我对工程师应该掌握的认识上有点偏差。现实让我认识到,这些偏差是很难在短时间内弥补的。难道,我要放弃了吗?
但是我还是喜欢搞技术的,我喜欢创造一件东西的感觉。大概是 2016 年,我看了一些前端技术的东西。我的本科毕设还在导师的要求下增加了一个用来展示数据的 Web 页面。很自然的,我就有“要不要试一试前端”的想法。于是,我在很短时间内重拾了 CSS/JavaScript。了解一些之后发现前端技术变化很大啊。在 2016 年,ES6 只是刚刚提上台,而在 2018 年已经成为了必备。应工程化的发展,构建工具和框架也成为了必须学习的一部分。Node.js 让前端工程师不仅能在“前端”领域大展身手,也能够在传统上属于“后端”的领域也能够做一些事情。这是非常酷的工作啊。就决定是它了。我在年前找到一份实习工作,氛围非常棒。但是这份工作的总结还是留给下一年吧(笑)。
2018 年,对于我而言是“改变”的一年。从学生到社会人,为了完成这一转变,我还有很多需要去学。
mv goals-2018.md goal-2019.md
在新的 2019 年里,第一重要的还是毕业。第二,多学东西,多写东西,在面试中,我感受到了写博客的作用。写过的东西理解上要深很多。第三,保持身体健康,多锻炼锻炼。最后,如果有机会的话,带父母在年底的时候出去玩一玩?
希望大家和自己都能够在新的 2019 年里健康、好运。
2018-12-31
北京
]]>Jest框架标榜"Delightful JavaScript Testing",提供零配置、快速反馈与快照测试功能。
零配置方面,Jest本身就是测试框架、断言库、Mock框架与测试覆盖率检查工具的结合体。而Mocha仅仅是测试框架。要在Mocha里配置出与Jest相同的功能的话需要自行安装像Chai断言库、Sinon.js框架与istanbul测试覆盖率检查。所以说Jest可以说是开箱即用的。Jest在零配置的情况下就可以自动检测以*.spec.js
,*.test.js
模式命名的文件以及__tests__
文件夹下的文件,把它们作为测试用例。
快速反馈方面,Jest的监视模式可以只运行与被改变的文件相关的测试用例,不需要把所有的测试用例重跑一遍。开发者可以快速知道改变代码的结果。
快照测试方面,Jest可以将某次函数的输出保存为一个快照,之后测试就把实际输出与快照中的输出进行对比,保证函数行为的一致性。
Jest可以分为测试框架、断言以及Mock三个部分。
关注Jest的测试框架部分,主要是关注在Jest中测试用例如何写、如何组织的问题。Jest中,测试用例的写法与Mocha非常相似。
1 | const binaryStringToNumber = binString => { |
在Jest中,不必将test
嵌入describe
中。如果能使测试更简单的话,test
也可以直接写在顶层。
describe
与test
可以连接skip
,only
,each
修饰符。如describe.skip('something', testFunction)
,会在测试时跳过这一个describe
。only
会使测试只运行指定的测试用例,这在某个测试用例出错Debug时非常好用。each
修饰符可以执行多次参数不同的测试,它接受一个数组table
和一个测试函数,table
里的元素会作为参数传入测试函数。具体语法可以参见文档。
Jest也支持在执行测试用例之前以及之后执行一些代码来做一些工作,像在测试前设置好测试数据、在测试后清理测试数据。这些工作可以作为beforeAll
、afterAll
、beforeEach
、afterAll
的回调函数。
Jest支持expect
式的断言,像expect(1).toBe(1)
,其中toBe
就是断言部分。Jest支持很丰富的断言。
断言两个基本类型的值相等使用expect(val1).toBe(val2)
。注意toBe
断言使用Object.is()
判断相等。它与==
以及===
都有不同。相对===
,Object.is()
在-0
, +0
与NaN
的判断上有所不同。
如果要断言数组或者Object相等,使用toEqual
断言。它会递归地判断每个属性/元素是否是相等的。
大小关系断言有toBeGreaterThan
、toBeGreaterThanOrEqual
、toBeLessThan
、toBeLessThanOrEqual
。名字很直白,不解释。
对于浮点数,不能使用toBe
或者toEqual
进行相等断言。Jest提供了toBeCloseTo
断言,可以在忽略一定误差的情况下,断言浮点数相等。
1 | test('float equality', function () { |
Jest提供toBeTruthy
与toBeFalsy
断言被测试函数的返回结果在if
中是真还是假。像:
1 | test('nonempty string should be true', function () { |
Jest也提供toBeNull
、toBeUndefined
、toBeDefined
针对性的断言null
与undefined
的情况。
Jest提供toMatch
断言被测试的字符串是否匹配给定正则表达式。
1 | test('but there is a "stop" in Christoph', () => { |
要断言数组中包含某个子项可以使用toContain
断言。
1 | const shoppingList = [ |
要断言对函数的某些操作会抛出异常可以使用toThrow
断言。
1 | test('throws on octopus', () => { |
not
修饰符可以把所有的断言反向,像expect(1).not.toBe(2)
。
Jest提供的断言不止上面提到那么多。常用到的还有像断言长度的toHaveLength
,断言对象有某个属性以及属性的值的toHaveProperty
。更多断言的可以参见Expect文档。
如果目前正在开发的模块存在依赖,比如某个函数需要一个随机产生的结果。假如依赖模块没有开发完成或者结果不可预知,我们是无法测试我们的模块的。为了解开开发模块与依赖模块在测试上的耦合,我们可以使用Mock功能去模拟被依赖模块的行为,比如规定某个函数调用时返回某些值。此时测试我们开发的模块就与被依赖模块的实际逻辑无关,实现了测试上的解耦。
这一部分比较复杂,我会在另外一篇文章来介绍这一部分。
这里是我将一个用Vue编写的项目从Mocha转到Jest时踩的一些坑,算是笔记吧。
@
是Vue项目中src
目录的alias
(通过jest.config.js
的moduleNameMapper
指定)。如果尝试使用alias
指定Automatic mock的模块:
1 | import Module from '@/src/libs/module' |
上述会报错。目前的解决方案是jest issue #1290提到的用Manual mock。这个方式有用,但是过了那么多年可能有更好的做法。
vscode-jest
是一款VS Code的插件,它可以在文件保存时使用Jest的监视模式进行单元测试。但是对于Vue CLI 3生成的项目,vscode-js
原生的配置无法使用。为了使vscode-jest
有用我们需要做一些额外的工作。下面假设已经通过vue add @vue/unit-jest
安装了@vue/cli-plugin-unit-jest
。
jest.pathToJest
设置为npm run test:unit
(使用npm)或者yarn test:unit
(使用yarn)。jest.config.js
里添加两行1 | process.env.VUE_CLI_BABEL_TARGET_NODE = true |
npx jest --clearCache
在vue-cli issue #1879上有关于这个问题的讨论。如果没用可以尝试其他人提出的方法。
]]>在讨论怎么写CSS好之前,首先要解决的问题是——好的CSS是什么?好看?好用?或者是…
在谷歌工程师Philip Walton的这篇CSS Architecture中提到了好的CSS应该是可预见的、可复用的、可维护的与可扩展的。
设计良好的CSS规则可以让开发者可以预见应用规则之后元素的样式。比如一个.big-red-btn
规定了一个大红按钮。那么我们在某个元素中应用class="big-red-btn"
时,这个元素就应该是一个大红按钮。
CSS规则应该足够抽象与解耦。这样,同一条CSS规则就可以在整个工程的不同地方使用。可复用性最直观的好处是CSS代码的体积的减少。而且在进一步开发中或项目进行重大重构时,如果项目使用了可重用的规则,我们更改代码的工作量就可以大大减少。
我们希望开发新功能时不需要去重构原来的代码。增加一个组件不会影响另外一个相关的组件。
当新的开发者接手项目时,不需要太难的学习曲线就可以上手开发。如果在为新的组件编写CSS规则时需要了解过去全部的CSS规则,这样的CSS代码是不可扩展的。
文章列出了一些常见的Bad practices,例如:根据父元素改变组件、过于复杂的选择器、过于一般的类名以及在单条规则里做太多事情。这些Bad practice我是全中了,ORZ。
在网站中可能有些在不同区域中外观有细微差别的组件,比如一个小物件在侧边栏中以及主页中表现不一样。我们很容易写出像下面的CSS规则:
1 | .widget { |
这个看起来无害的.widget
可以数出它的三宗罪。第一、这个.widget
不符合可预见性的要求。在同样的HTML标签<div class="widget"></div>
放在不同的地方它的表现就不一样。第二、.widget
规则不好复用,当我就想在侧边栏上用普通外观的.widget
时该怎么办呢?只能搞一个新的copy-and-paste的规则。第三、.widget
规则不好维护,当.widget
被重新设计的时候,我可能需要到几个不同的地方把样式找出来修改。
在给列表,特别是嵌套列表写样式的时候,很容易写出下面一串CSS规则:
1 | #main-nav ul li ul li div { } |
上面的规则问题在哪里呢?假如导航栏里面的HTML结构一辈子不变,这样的写法还可以商榷。一旦将来改变,上面的规则将会失效。这个问题总结起来就是过于复杂的选择器会造成过深的耦合。太深的耦合在程序世界里不是什么好事情。另外,这样的规则也无法复用,一个页面只能有一个#main-nav
,后面的元素如果不在#main-nav
里面也无法匹配到规则。
在写可复用组件的时候,很容易给组件取一些很一般的名字像title
,content
。在大型项目中,可能其他人也会给他们的组件取相同的名字。由于CSS没有作用域的概念,这样就容易出现命名冲突。当写出一个规则但是完全不按写的规则来的时候,我们就会很疑惑,然后debug发现自己写的规则被其他人写的同名规则覆盖…WTF!上面的例子也就是说,太一般的类名是没有可预见性的。
有的时候,我们会在一条规则中把所有该指定的东西都指定完——如元素的位置、背景、字体设置等等,像:
1 | .widget { |
.widget
一条规则设置了组件的位置、背景、字体。这一条看起来也是人畜无害的规则。但是仔细一想的话,这条规则似乎无法复用。如果我想把.widget
用在绝对定位元素的右下角怎么办?当我们把规则写的越细,那么这条规则适用的范围就越小。用在CSS世界中就是,规则的具体程度与可复用性是矛盾的。
从上面这些bad practice反面去理解,我们可以摸到一些如何写CSS的门路:
当然,上面的想法还是过于粗略。社区在如何写CSS方面有了一些探索,其中最有名有OOCSS(Object Oriented CSS)、SMACSS(Scalable and Modular Architecture for CSS)与BEM(Block, Element, Modifier),如果有兴趣的话可以浏览一下这些规范。
]]>初音未来,原本是语音合成软件Vocaloid 2的一个音源库。现在已经成为了日本文化的一个标志。在这则CM中。Google真正抓到了现象背后的本质——Miku背后无数充满想法的创作者。Miku为所有喜爱她的人提供了创作的可能性,如果你喜欢音乐,你可以为Miku创作歌曲;如果你喜欢绘画,你可以画出你心目中独一无二的Miku;如果你会视频剪辑,你可以为Miku制作出酷炫的PV。你可以用任何你擅长的技能去创造出自己心目中的Miku。无数的想法在网络空间中得到共鸣,它们对于社区形成了正反馈,激励更多的人参与创作。Everyone, Creator,初音社区做到这一点。
用爱创造,这也许是最幸福的事情之一了吧。
]]>npm
包恰好能够满足,然后npm install package
一把梭,require
解决问题。完美的工作流,体现了JavaScript社区的开放与强大。但是,最近一个月下载量达百万的包event-stream
被注入了恶意代码。恶意代码会尝试劫持另外一个包中的bitcore-wallet-client.getKeyFunc
方法。如果一个项目同时依赖了event-stream
与copay-dash
,当bitcore-wallent-client
中的getKeyFunc
运行时,恶意代码会检查钱包的id,持有比特币BTC的数量以及比特币现金BCH的数量。如果BTC数量大于100或者BCH数量大于1000,就将钱包的公钥发给一个地址。当我看到这个新闻的时候,我心里想,我没写过写区块链相关的对象,应该不会被影响到吧。但是当我没事跑一下检查方法时,结果却傻眼了:
我中招了!
我中招是因为vue-cli
间接依赖了受影响event-stream@3.3.6
。目前vue-cli
已经更新了版本将event-stream
锁定在了未受影响的3.3.4
。如果有向我一样中招的朋友,请尽快升级vue-cli
。
1 | # 全局安装时 |
如果没有安装过vue-cli
,也要检查一下是否有项目简介依赖了event-stream@3.3.6
。方法如下:
1 | # 全局层面 |
太简单了,假如我们有一个正常的包good.js
是这么写的:
1 | module.exports.goodFunc = () => 'good' |
然后我们有一个包含恶意的包malicious.js
:
1 | const good = require('./good') |
当我们的项目同时依赖两个包的时候:
1 | // index.js |
简单到不可思议吧,比起别的程序深入汇编挖漏洞。JavaScript只需要,从一个不知道干什么的包里require
被攻击的包,然后修改一下就行。这里event-stream
干了很容易被发现的通过网络发送数据,如果其他攻击不是那么容易被发现呢?我们项目里动辄上千的依赖,我们能够保证它们都是善良的天使吗?I don’t know…
Web开发者整天与不安全的网络、不可信任的输入等打交道。Web安全对于开发者而言,似乎就是保证用户的信息安全。我们会使用很多技术来保证用户的安全,像HTTPS,CSRF防御等等。
但是…对于开发者本身呢?Node.js的模块机制提供的保护是0。它只能够保证第一次require
的时候代码是包作者提供的。接下来轮到我们的代码require
的时候,说实话,我们不能保证我们调用的函数是李逵还是“李鬼”。JavaScript语言层面提供的保护…近乎为0。能够阻止修改我们exports
出去的对象的方法只有一个,Object.defineProperty(module.exports, 'myMethod', { writable: false })
。那么长,还只能锁住一个方法,一般库的作者是不会写的。
现在我们使用这些包,就像使用C++的未定义行为一样。有时它确实有用,但是可能不是像你想象中的一样作用的。也许哪一天,一个包给Object.create
注入新行为,当执行这个函数时删除项目文件夹,也说不定。。
目前JavaScript社区是靠人来解决这一事情的,默认信任大牛、高Star的包。但是这些“高信任度”的包引入的依赖呢?巨量的基础包依赖,像is-odd
(判断一个数是不是奇数。很简单是吧?这个包每周下载量达1百万次哦)这些,使得我们根本不可能去检查项目中的每一个依赖。当其中一个依赖出现问题的时候,我们每个人都是潜在的受害者。
综上所述,Node.js
不严格的模块机制、过于灵活的JavaScript以及项目中巨量的包依赖已经对开发安全形成了威胁。我们需要一个解决方案保证我们调用的函数就是作者写的函数。
既然现有机制无法保证包在export
出去之后其公共接口不受改变。那么包的开发者应该承担起这一责任,无论是通过Object.defineProperty
去设定公共接口不可写或什么其他的方法。这里我写了一个exports-lock,能够通过递归地设置module.exports
的属性为不可写(writable: false
)、不可配置(configurable: false
)。保证公共接口在export
之后不受篡改。
(可能暴力了点,可以改良。。。)
async/await
语法的异步函数,这里我们称为异步中间件。一种是使用普通函数语法的普通中间件。尽管说官方支持两种写法,但是在实际应用中,我们可能不大常见普通中间件的写法。为什么呢?tl;dr
- 普通中间件可以有,但没必要;
- 错误的普通中间件写法可能破坏洋葱模型;
- 正确的普通中间件的写法可能和想象中的有点不太一样。
大家都知道Koa2的中间件的执行流类似洋葱,即请求会从第一个中间件开始然后暂停在next
,接着执行第二个中间件,一直到最后一个中间件暂停在next
。接着最后一个中间件从next
恢复执行,执行完之后倒数第二个中间件从next
的地方恢复执行,一直到第一个中间件从next
恢复执行到完毕。
1 | const Application = require('koa') |
这一部分我们简单介绍一下Koa2中间件洋葱模型的实现。当我们使用app.use(middleware)
时,其实内部会将middleware
推入一个数组保存:
1 | // koa/lib/application.js |
忽略掉大量检查代码,use
就做了一件事情,this.middleware.push(fn)
。this.middleware
是一个数组。
接下来,在app.listen(3000)
的时候,app.listen
会调用原生的http
模块,然后使用this.callback()
产生一个回调函数在请求到来的时候调用。接下来就是重头戏了,
1 | callback() { |
注意到compose(this.middleware)
,它将中间件数组组合起来,然后返回一个执行函数fn
,当调用执行函数时,如fn(ctx)
时,ctx
就会以洋葱模型被各个中间件所处理。this.handleRequest
干的就是这么一件事,可以看源码:
1 | handleRequest(ctx, fnMiddleware) { |
这里我们重点来看一下compose
函数,它是实现洋葱模型的核心函数。
compose
函数并不在koa
包里,而是在koa-compose
包中,是一个非常短小精悍的函数。源码地址在这里。我们只保留函数的核心部分如下:
1 | module.exports = compose |
从上面可以看到我们的中间件是如何调用的,比如上面第一个中间件其实是以middleware_1(context, dispatch(1))
的形式调用的。函数中使用到了Promise
来保证各个中间件按照洋葱模型执行。具体原理可以自行推导一下。
下面是一种很流行的错误的普通中间件写法:
1 | const one = (ctx, next) => { |
代码出自阮一峰老师的Koa 框架教程。上面的代码是说明中间件栈(和洋葱模型差不多)这个概念。但是,代码成立的条件是所有中间件都是同步的。当其中有任意一个中间件是async
的时候,代码就可能不是按照洋葱模型执行了。阮一峰老师提到**如果有异步操作(比如读取数据库),中间件就必须写成 async 函数。**可能就是因为这一原因。
下面是一个例子,在响应请求的过程中,我们需要记录响应请求所需要的时间,然后对用户传过来的密码进行哈希之后保存。对密码加盐哈希是一个非常耗时的操作,一般使用异步,这里为了模拟耗时,使用setTimeout
延时1s。
1 | const Koa = require('koa') |
当在浏览器里请求localhost:3000
时,console输出如下:
1 | logger starts |
可以看到logger
的next
之下的部分先于hashPassword
的next
之下的部分执行,这破坏了洋葱模型。
注意到,next()
返回的是一个Promise
,在上面的写法中没有对这个Promise
做任何处理,直觉告诉我们,这里极有可能会出现异步调用的顺序问题。事实上也是如此,我们来一步一步的分析问题产生的原因。
首先,logger
函数被调用,传入的next
参数是dispatch(1)
。当logger
执行next()
,实际上dispatch(1)
被执行。注意到dispatch(i)
的返回值:
1 | return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); |
这个Promise.resolve
是什么东西呢?
Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。但如果这个值是个thenable(即带有then方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态(指resolved/rejected/pending/settled);如果传入的value本身就是promise对象,则该对象作为Promise.resolve方法的返回值返回;否则以该值为成功状态返回promise对象。
很不巧,接下来的fn
,即hashPassword
是一个async
函数,当运行async
函数时,遇见await
,函数会暂停执行并立即返回一个状态为pending
的Promise
。也就是说,hashPassword
在第一个await
的时候就返回了一个pending
的Promise
。
1 | const hashPassword = async (ctx, next) => { |
然后问题就大了,Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
一看到fn
返回了一个Promise
,接着就把这个Promise
再返回给上层。此时dispatch(1)
结束。logger
看到next()
返回,然后兴高采烈地执行接下来的语句。此时hashPassword
仍然在紧张地哈希用户的密码,甚至还没有调用next()
函数。洋葱模型就此打乱。
既然官方支持普通中间件,那么正确的写法是什么呢?根据文档一个正确的普通中间件的写法应该是:
1 | // Middleware normally takes two parameters (ctx, next), ctx is the context for one request, |
可以看到正确处理next()
返回的Promise
才能够保证洋葱模型的正确性。。。
既然要处理Promise
的话…为什么不直接使用async/await
呢?
1 | app.use(async (ctx, next) => { |
上面的代码明显要比普通中间件的写法要好看一点。
文章可能标题党了一点,Koa里面的中间件非得是async
函数吗?当然不是。但是普通中间件的写法,第一可能和你想象中的不一样,第二还要略懂中间件实现的原理才能够正确实现普通中间件。如果坚信自己会用普通中间件,just do it!