我的技术和生活

  • 聊聊瑞幸咖啡

    聊聊瑞幸咖啡

    根据财报,瑞兴咖啡 2022Q1 开始盈利。这不是简单的盈利,而是扛住了美国证监局的罚款、扛住了核心管理团队大换血、扛住了疫情,没有大规模关店、裁员、拖欠供应商货款,然后还能盈利。很多人觉得不可思议,我倒觉得蛮正常:瑞幸咖啡很用心在运营,而且也做对了很多事情。

    比拼竞争对手

    vs 星巴克

    星巴克卖的不是咖啡,而是第三空间,这点疯投圈的黄海有很精彩且完整的讲解,我就不复述了。所以它可以把一杯咖啡卖到 40 块,还敢于逆势涨价。从定位上看,星巴克虽然江湖地位稳固,但是与瑞幸并不存在直接竞争关系,所以也不会影响瑞幸的顾客群体。

    vs 奶茶

    我国的传统还是喝茶,对新鲜人来说,奶茶则是结合了传统与好喝的首选。但是问题在于:

    1. 奶茶没有咖啡因,其成瘾性存在很大差距;
    2. 奶茶的热量太高,职场人士不敢敞开喝;
    3. 奶茶受众群体偏年轻,覆盖人群有限

    所以,奶茶行业自己打得死去活来,对瑞幸的影响也很有限。

    vs 低端咖啡(全家、便利蜂等)

    如果只为摄入咖啡因,低端咖啡可能性价比更优。但是有个问题:咖啡很难喝,低端咖啡尤其难喝。无论美式、拿铁、馥芮白,一般店用普通全自动咖啡机,配合普通无品牌咖啡豆,打出来的咖啡都很难喝。但凡舌头没问题,都很难长期坚持喝低端咖啡。

    瑞幸的运营

    瑞幸跟其它咖啡比起来,胜在哪里呢?我觉得有三点:

    1. 好喝的新品

    瑞幸的运营团队很用心,即使在创始人带着 CFO 疯狂造假时,他们也非常努力地摸索新品。在我看来,他们并没有向命运和老板低头,而是努力地把手里的牌打出最好的结果。

    与一些普通咖啡店万年不变的美式、意式、拿铁、馥芮白不同,瑞幸每隔一段时间就会推出一些新品,而且都很好喝。比如早期的气泡咖啡、去年的生椰拿铁、今年用代糖升级气泡咖啡,等。节令性质的什么樱花蜜桃西瓜更不用提。

    所以,当你希望喝一杯咖啡满足自己的咖啡因需求,又希望这杯咖啡不要那么难喝的时候,瑞幸几乎是最佳选择。

    2. 稳定的食品

    瑞幸食品的性价比很高,源于足够好的供应商。相对来说,做咖啡比较容易,买来好的咖啡机、配上好的咖啡豆,剩下的工作很少;但做食物刚好相反,无论经济成本多少,都要搭配很多人工成本,才能把东西做的好吃。

    星巴克比较不担心这个,毕竟一块蛋糕小小块就能卖 30+;但很多咖啡厅做不到,所以食品很差,难吃、贵、品类少。瑞幸这方面做的最好,食品好吃、价格不贵、出品稳定。

    3. 店铺多,出品稳定

    我刚毕业那会儿,还没有大众点评和各种探店,去外地面试时,基本都会选择吃麦当劳肯德基,不是因为喜欢吃,而是到了完全陌生的地方,与其尝试完全陌生的食物和饭店,不如保证口味和健康的同时,集中精力干重要的事情。

    瑞幸也是如此。不管走到哪里,想喝咖啡,打开手机查地图,周围要么是没有品牌的小咖啡店,要么是溢价严重的网红咖啡,怎么办?瑞幸咖啡解君愁。

    总结

    瑞幸的运营有很多值得学习的地方。能够逆势翻盘,能够扛住各种不利因素稳步前进,瑞幸的产品护城河起到非常重要的作用。希望我们能从瑞幸身上学到足够的教训和经验。

  • Vue 使用 Provide/Inject 向子组件内注入数据

    Vue 使用 Provide/Inject 向子组件内注入数据

    前阵子做厂里的需求,允许用户编辑算法生成的 CSS,以便将我厂的产品应用到生产环境。我们允许用户使用可视化编辑器编辑某条特定规则,大约如下图所示:

    我们知道,CSS 规则有优先级,优先级高的规则会覆盖优先级低的规则中的同名属性;也会继承低优先级规则里没有被覆盖的属性。反应到我们的可视化编辑器里,就是:

    1. 有些属性用户不能删除,因为那是从低优先级的样式里继承来的
    2. 有些属性用户修改了也不会生效,因为会有更高优先级的规则把它覆盖掉

    所以我们希望给用户一些提示,避免他们实际操作的时候感到疑惑。那么问题来了:怎么实现呢?

    从图上可以看出,CSS 可视化编辑器挺复杂的,有些属性可以直接编辑,比如 visibility,独立生效,那随便写个 <input type="checkbox"> 就行;有些则与其它属性一起发生作用,比如 align-items,那我们就需要比较复杂的组件,里面会嵌套别的组件。

    所以一般的 v-bind 方式就不适用:我计算出优先级之后,需要传递多次才能穿透组件间的嵌套关系,太复杂,很难用。我们需要更简单的传递方案。好在 Vue 提供了 provide/inject 方式。

    Vue 官方称这个功能为“依赖注入”,我们在父组件上使用 provide 暴露一些属性,然后在子组件里用 inject 把这些属性拿进来使用。不管子组件和父组件之间嵌套了几层,都可以获取到:

    provide() {
      return {
        foo: this.foo
      }
    }
    inject: ['foo']

    用法比较简单,就这么两步,但是有一些注意事项:

    1. 注入的变量默认没有响应式,Vue 就是这么设计的,这样可以避免一些问题。如果需要响应式,那就需要传入包含响应式的变量,比如 data 或者 computed
    2. 如果你用 vue-property-decorator,那么需要用 ProvideReactiveInjectReactive
    3. 这个依赖注入和设计模式里的 DI 不是一回事,面试时不要乱讲。
    4. 不要滥用这个设计,只有不特定层级子组件需要用到属性,才这么做。

    扩展阅读:

  • 远程工作误区:边旅行边工作

    远程工作误区:边旅行边工作

    今天聊一个大家对远程工作的误会:边旅行边工作。

    前阵子有个电鸭上的同学加我,想了解远程工作的一些情况。交谈之中他提到,期待能边旅行边工作,作为一个东北人,想到南方看看海。这也是很多同学包括我对远程工作的幻想,事实上,这只是一种误会。

    第 0 关:相对固定的工作时间

    首先,远程工作也是工作。工作,就有工作内容的要求,对质量、数量、产出、时间都会有要求。有些公司能做到双休、弹性时间、结果考核,已经实属不易,但也只是在每天 8 小时,每周 5 天的基础上,给予弹性。虽然大家可能不愿意接受,但事实上,绝大部分工作,包括软件开发,工作产出就是跟工作时长绑定的,要做出那么多功能、要修补好那么多 bug,就得先保证工时。

    而且,大部分工作也不是单人独立就能完成,协作体系要求大家必须保证工作日的核心时间里,能够找得到彼此,能够约会、讨论。

    所以,即使是远程工作,工作日的核心工作时间也是基本固定的。我们能享受到的弹性,基本局限于下楼测个核酸,中午去趟超市,或者提前半天时间去大热的饭店抢号。并非我今天不爽就不干了,或者我大干特干一周,然后休息一周去旅行。

    第 1 关:交通时间

    远程工作不需要通勤,但是旅行需要交通时间,而且不是一般的消耗时间。基本上,单程一天算是很正常的情况。

    比如,我在广州,想开车带全家去阳江海陵岛(强烈推荐这个地方)玩几天。省内游,成本已经算是极低。路上大约需要 3、4 小时,到了之后,办理入住、收拾东西、吃饭,大约 2~3 小时。这样就需要 5~7 小时,工作日很难成行。

    那就周末去呗?当然可以,但是一来是 6 个小时,一回又是 6 个小时,周末很可能就这么蹉跎过去了。更别说去更远的地方,比如那位同学说的,他在北京,想找份远程工作,然后到南方海边,往返各耗一整天基本是必然结果。

    那就只能这样,我们这个周末去,住在那里;玩一整个周末,然后下下个周末回。

    第 2 关:生活费用

    这就面临一个经济成本的问题。一般酒店每天几百块到上千块不等,住个周末压力不大,住久了还是有些贵。租房子住,半个月一个月未必能很快租到。比较可行的是月租酒店公寓,具体多少钱我没试过,大家可以看看。但基本上,一定比你固定租住在一个地方要贵。

    接下来是吃饭。国内的话,考虑到外卖,城市里问题不大;但是如果在海边、风景区,旅游淡季吃饭相当困难。快递也送不到,大概率还是需要自己到本地市场买菜自己做,时间成本不低。又会遇到交通问题。

    很多风景区或者海边楼盘(便宜的租住地)没有公共交通,离开了长期居住的地方到了新环境,可能也没有办法继续开车或者骑电动车。真的应了广智那句话,谁把共享单车骑过来,谁就是英雄。

    第 3 关:怎么实现旅行自由?

    那么,真的无法实现旅行自由了么?即使远程工作也不行么?

    是的,至少目前来看,远程工作对实现旅行自由没什么帮助。真的想实现旅行自由,只需要有正向财富流,其它无所谓。正向财富流,即你获得财富的速度大于支出财富的速度(这里的支出包括为将来储蓄的部分);不要求赚多少,只要比花销多就行。

    然后,就可以选择合适自己的旅行方式。挣得多就多花,挣得少就少花。只不过大部分情况下,全职工作和四处旅行之间是互斥的。

  • 一些近况

    最近都没写博客,一方面是暂时没有特别想分享的内容, 一方面也的确遇到点事情,简单记一下。

    首先,老婆的奶奶在家中仙逝。老人家今年 93 高龄,四世同堂,应该也算志得意满,寿终正寝了。

    正好孩子在家上网课,于是我们一家准备回重庆奔丧。我们计划,我和孩子周一去周三回。按规定,广州属于带星地区,回重庆要三天两检,然后我们落地就抽时间去做了核酸,没想到回来老婆孩子的健康码就没了,但我的码还坚挺,不知何故。

    周一晚上守灵,熬了个大夜,连轴转了将近 40 个小时,老人家入土为安,我们也准备返程。但是孩子一直没有健康码,我跟老婆讨论半天,得出结论:

    1. 进机场要看重庆健康码,我有,孩子没有,可以闯闯看
    2. 后面只看广州健康码,我们都有,应该问题不大
    3. 我们都有 48 小时核酸

    于是周三我们按照既定计划到了机场,准备不行就改签。结果还算顺利,我跟机场安保解释了一下,就被放行了,然后顺利回到家。

    我老婆上周二才回来,回来没两天就赶上机场地勤检出阳性。一种不祥的预感。

    果然,4 月 29 日凌晨 3 点多钟,电话打过来,要求居家隔离七天。得,小长假哪儿也甭去了。

    第二天上午大概 10 点多钟,负责隔离的人上门,发通知单、贴封条、贴门禁,做核酸。其实并不严格限制“开门”:外卖、快递、做核酸,都要开门才能处理,只要不出门就行。

    然后疾控又打电话,让提供机票证明,说如果只出入了 T1,可以只隔离三天。于是今天,隔离就结束了。

    整体来说,隔离对我们来说不算难熬,本来我们也都宅在家里打游戏上网,外卖送菜不间断跟平时没什么差别。只是不能遛狗,姆伊比较难熬。这几天我都是11点之后才偷偷出门遛狗,但是经常能遇到疾控负责消毒收垃圾的人,也是这么晚还在工作,真是辛苦他们了。

    我不是公共政策专家,工作这么多年,我也对评价别人变得越来越谨慎。我觉得,这种程度的清零政策可以接受,代价也可以承受。

    回家后,Pathfinder: Wraith of righteous 突然打折,虽然折扣不多,不过我也下单了。这几天的闲暇时光几乎都拿来打游戏了(更不想写文章了……

    接下来,如果暂时没有技术好分享的话,我打算系统的总结一下远程工作的相关知识、经验,写几篇博客。

    最后,隔离这几天,我老婆的广州健康码也变成红码,连带孩子也红了。只有我还是坚挺的绿码。真是人品好什么都好。

  • WebRTC 笔记

    WebRTC 笔记

    最近研究了一下 WebRTC,写篇笔记记录下。

    0. WebRTC 简介

    WebRTC 是一种 p2p 技术,它可以在两个不同的浏览器之间建立直连,让它们互相传输数据、视频和音频流。

    这个技术可以充分利用用户自己的上行带宽和网络环境,降低中心服务器的负载,既提升用户体验,又能降低服务提供者的成本。用在一些低成本的场景非常合适,比如视频会议、轻型联网游戏、内容共享网络等。

    由于其低成本的优势,我想用它给 mywordle.org 升级,支持用户 1v1 对战。将来的话,还可以搞一些 FC 模拟器玩玩。

    1. WebRTC 基本概念

    WebRTC 的概念不少,初次连接也很复杂,有些我现在还没完全搞清楚。所以这个部分以后再慢慢更新。另外这里的内容并不是按着规范走的,而是来自我的实践。

    1.1 基本概念

    1.1.1 信令服务器

    这可能是 WebRTC 里最重要的一个概念,每个浏览器 tab 页都是网络中的一个孤岛,必须通过信令服务器才能找到对方,建立连接。

    信令服务器是必须的,不能通过人工方式完成两个节点的互联(或者说很难)。但是信令服务器并不一定要自己建,有不少公共的可以蹭。

    1.1.2 Ice 服务器 RTCIceServer

    同一个局域网内连接很简单,但实际上并不常见。我们日常使用的网络,无论移动网络还是家里的宽带,其实都是位于 NAT 后面,相当于总机分机的概念。两个 NAT 后面的应用想连接就比较困难了,此时就需要 RTCIceServer 帮忙,协商在两个 NAT 上打洞。

    具体的实现逻辑我们不用关心,只要会用就行。目前有两类 RTCIceServer:STUN 和 TURN。前者只负责给双方牵线搭桥,本身不介入连接,有很多公共服务可以蹭;TURN 不光能把两个端连接起来,还能在两端中间网络不通的时候作为 fallback 方案。功能更强,真正生产级别的产品都需要;但是相应的,TURN 需要更高的资源支撑,免费资源也很少。

    1.2 发起连接

    当用户 A 想要跟用户 B 建立 WebRTC 连接时:

    1. A 创建一个 RTCPeerConnectioin 对象
    2. 创建一个 offer,其中包含着 A 的网络信息,其它人通过这个信息可能找到 A
    3. A 把 offer 发给信令服务器
    4. 信令服务器把 offer 发给用户 B
    5. 用户 B 记录下 offer,然后生成 answeransweroffer 其实是一样的,只是用来作为响应。这也是必须有信令服务器的原因。
    6. B 把 answer 发给信令服务器
    7. 信令服务器把 answer 发给 A
    8. A 尝试用 answer 建立连接,如果是局域网,双方可能已经连上了
    9. 如果连不上,则 A 尝试通过 ice server 连接。
    10. A 创建自己的 icecandidate 信息,然后发给信令服务器
    11. 信令服务器将 A 的 icecandidate 发给 B
    12. B 添加后,也创建自己的 icecandidate,发给信令服务器
    13. 信令服务器将 B 的 icecandidate 发给 A
    14. A 添加之,并尝试创建连接
    15. 如果一切正常,这个时候就连上了。

    2. 实操

    我建立了一个项目:meathill/webrtc-playground: learn webrtc (github.com),目前还在升级开发中。

    1. 实现一个 websocket 服务器(基于 socket.io),作为信令服务器,交换信令
    2. 用各种姿势尝试建立连接
    3. 同浏览器多 tab 连接成功
    4. 内网连接成功
    5. 手机开热点,尝试公网连接,也成功
    6. 尝试跟朋友连接,失败。遇到两个问题:
      1. 他家的网络是广州联通,我家是广州电信
      2. 他的手机网络是北京联通
      3. 目前的信令服务器会无条件广播各种信息,当我跟他都多开 tab 的时候,很难保证连接的两端是匹配的
    7. 于是接下来要重构。

    3. 总结

    这次学习过程一波三折。首先,WebRTC 的用户大部分关注音视频传输,毕竟这方面效果最明显;DataChannel 其实只算个添头,偏偏我最关注这个,所以找内容花了不少时间。

    接下来,大部分范例代码都只是抄来抄去,一个 tab 内部来回连,经常会看错。另外,一些概念也不清不楚,比如 ice server,无法主动触发请求。

    终于调通了内网和公网,又遇到联通电信问题……看来将来 TURN 服务器也必不可少。

  • 聊聊当年我设计的“新-用户轨迹产品”

    聊聊当年我设计的“新-用户轨迹产品”

    突然想聊聊当年在 201 做的用户轨迹追踪产品,也算是自己当年产品工程能力的体现吧。

    0. 需求

    大约在 2010 年,作为最大的 IT 资讯垂直门户,201 需要进一步理解用户行为,要增加数据统计的维度和数据挖掘的深度。

    当时的 Google 统计刚刚引入热力图(这里我记不准,可能不是“刚刚”),可以统计用户在页面的交互动作,显示用户最关注哪个区域,跟哪个区域交互最多。见下图,越热的地方,就是用户越关心的地方。

    201 产品部门也想用这款产品,不过面临几个问题:

    1. 需要使用第三方工具,数据安全性存在疑虑,也担心将来很难整合其它数据
    2. 当时普通用户还是 IE 为主,浏览器性能很差,201 的页面本身消耗资源就不少,加入更多统计可能会影响到用户
    3. 热力图的结果其实是可以预期的,如上图;如果差太远,那就是出了问题。所以,要不要用一个大概率没什么用的产品呢?
    4. 自己开发的话,统计量很大,预估实际点击和有效点击可能有 10 倍左右的差距,对我们的统计服务器也是很大的负担。

    1. 现有用户路径统计

    当时我们已经基于服务器 access log 打造了一款用户路径统计产品。我们都会给 每个用户分配一个 sesssion id,当他们访问网站的时候,记录下网页 URL 和 session id,后面就可以分析访问日志,得到每个用户的访问路径。比如:首页 > 手机 > 苹果专区 > iPhone 13,等。

    负责这套系统的朋友找到我,让我帮忙做一套前端工具,给产品部门使用。最初的前端界面很简单,输入一个页面地址,搜索,得到一连串 URL,然后产品经理一个一个点过去看。我接手后,很自然地,将其改造成发散图:

    1. 从输入的节点取出所有以此 URL 为开始的用户路径
    2. 整理节点,按照访问数量排序
    3. 用线路粗细表示用户的数量
    4. 点击 URL,可以跳转到 URL 或者以 URL 进行筛选

    上线后,效果良好,产品经理可以很轻松的看出用户的流向,辅助他们做调整页面的决策。他们给予我们高度好评,并且深入讲述了他们的其它需求,包括前面提到的,更详细记录用户轨迹的需求。

    接下来我开始思考这个问题的解决方案。

    2. 解决方案

    功夫不负有心人,我想到一个方案:

    1. document.body 侦听用户的点击事件,判断点击的目标,如果是链接,则记录下链接的坐标
    2. 然后将坐标记录在 cookie 里面,cookie 会随着 http 请求发给服务器
    3. 生成访问日志时,记录对应的被点击链接的坐标
    4. 根据坐标生成热力图

    这套方案有几个好处:

    1. 完全不需要修改目标页面,也不需要其他部门同事配合,只需要在统计代码中加入几行代码
    2. 不增加页面负担,用户不会感知到任何变化
    3. 不影响其它统计,多出来的 cookie 经过压缩,只需要几个字节

    后来,我们又做了页面快照、合并排重等,用很小的成本,实现了不亚于第三方的热力图方案。还可以跟我们其它统计数据做整合,得到更丰富的数据视图。

    比如,201 的页面有很多广告,这些广告会使内容的位置有变动,普通热力图无法区分这些变动,导致热区不准。而我们可以根据广告尺寸、快照记录等,把不同的点击区域合并到一起,提供更加准确的用户流向。

    3.评价

    当时的产品总监给予这套产品很高评价,说它至少可以提升全站访问量的 5%。

    (更多…)
  • 中年男人找工作有感

    中年男人找工作有感

    熟悉我的人应该知道,前段时间我在找工作。不得不说,中年人找工作,尤其在春节前那个时间点,的确有点困难。

    困难并不在获得面试机会。实际上,我的大部分简历投递都获得了面试机会,似乎并没有人一看我年龄超过 35 就把我简历丢掉。面试过程也还算顺利,好几个能走到最后一面,甚至拿到 offer。

    真正的困难来自 offer 本身——这些 offer 跟我的预期几乎都有差距。大部分公司都是金字塔结构,区别只在于金字塔的层数。所以越往上走,位置越少;如果这些位置上刚好有人,那我就不太有机会。

    另一方面,公司的薪酬体系,以及某些 HRBP 的刻板与不思进取也让我很失望。我不介意领导比我年纪小,或者技术比我差。但是 HRBP 们无法接受我比领导工资高。我的目标是改进团队战斗力,让大家能写出更好的代码,让公司的技术能满足更多需求;按理说,只要我能提供足够多的价值,这些并不应该成为问题。但实际上,HR 给我解释 offer 的时候就说,你不能比你的领导工资高。

    不得不说,在互联网行业工作十几年后,我觉得互联网越来越不互联网了……早先的互联网强调自由,不仅是用户的自由,也包括从业者的自由。你能干、愿意干,就能多劳多得;你不想干、不能干,就躺平拿基本工资。工作所得和贡献挂钩,而不是岗位和职级。我不想归过于 HRBP,但是我的确没看到他们有做什么正面贡献,只看到他们把传统行业里的陈腐观念拿来污染互联网。

    最后感谢杨老板收留,让我的技术有用武之地,让我继续对将来抱有期待。

  • node.js 里 ESM 与 CommonJS 的区别

    node.js 里 ESM 与 CommonJS 的区别

    可能大部分同学并不会直接用 node.js 开发 Web 后端程序,但是作为现代化前端,我们日常的各种开发都严重依赖开发脚手架,也即 node.js 环境下的各种工具链。目前已经有一些仓库逐步迁移到 ESM,所以了解一下 node.js 里 ESM 和 CommonJS 的区别也很有必要。

    我还是老习惯,后文列举出的差异并非文档记录,而是我在实操中遇到的大坑小坑,希望记下来能节省将来的时间和大家的时间。

    0. 设计原则

    我们可以把 CommonJS 理解成按需加载,需要什么就加载什么,加载进来就执行。所以可以动态加载、条件加载、循环加载。

    ESM 倾向于静态加载,方便解析依赖,优化运行效率。所以起初不能条件加载或者循环加载。不过后面考虑到实际需求,还是开放了 import() 做动态加载。

    这个设计原则可以导出后面的诸多不同。

    1. package.json

    package.json 里添加 type: 'module' 可以开启本项目的 ESM。不写或者 type: 'commonjs' 则继续使用 CommonJS。

    如果没有此配置,虽然我们代码中写的 好像是 ESM,但其实都会被 Babel 或其它什么工具转译成 CommonJS,最终运行的并不是 ESM。这点一定要注意。

    2. __dirname

    ESM 不再支持用 __dirname__filename 获取正在执行的 JS 文件在系统中的路径。作为替代方案,可以使用 import.meta.url 获取当前文件的 URL,不过返回结果是 file:// 协议,如果要继续使用 __dirname 可以这样:

    import {dirname} from 'path';
    import { fileURLToPath } from 'url';
    
    function getDirname(url) {
      const __filename = fileURLToPath(url);
      return dirname(__filename);
    }

    3. 解构

    使用 CommonJS 时,我们可以直接对导出的对象进行解构,比如:

    // lib.js
    module.exports = {
      foo: 'bar',
    };
    
    // index.js
    const {foo} = require('./lib');

    这样的用法在 ESM 中不可行。ESM 解构只能针对使用 export foo = 'bar' 这样主动暴露出的属性。对于一般对象的解构,我们只能写成:

    // lib.js
    export default {
      foo: 'bar',
    }
    
    // index.js
    import lib from './lib';
    
    const {foo} = lib;

    4. .mjs 文件与 .cjs 文件

    我们知道,node.js 加载模块时可以省去文件扩展名,比如 require('./foo'),不需要写最后的 .js.json,node.js 会自动去目录里查找对应的文件。

    node.js 会根据 type 的不同使用不同默认策略加载 js,我们也可以使用特定扩展名要求 node.js 在加载时使用特定模块类型。比如,我们使用 ESM 时,node.js 会把加载进来的 JS 都当 ESM 处理,如果这些 JS 还在使用 CommonJS 加载其它 JS,就会报错(前面说了,ESM 里不支持 require)。此时,我们可以把目标文件的扩展名写为 .cjs,node.js 就会当它是 CommonJS 来处理了。

    此功能在使用第三方库的时候很有用。比如 Postcss,它会加载项目里的配置文件,但它只支持 CommonJS,这时,如果执行时因没有 require 报错,就可以把配置文件的扩展名改成 .cjs

    5. 顶层 await

    开启 ESM 之后,可以使用顶层 await,省去一个异步函数。

    (这是促使我使用 ESM 的主要原因)

    6. importrequire

    ESM 中,我们可以使用 import 导入 CommonJS 模块和 ESM 模块;但是 CommonJS 的 require 只能用来导入 CommonJS 模块。如果要在 CommonJS 中导入 ESM 模块,需要使用 import() 然后异步处理。

    自然,ESM 里不能用 require

    7. 其它区别

    这些区别我在实际开发中没有遇到,大家自己阅读吧:Modules: ECMAScript modules | Node.js v17.8.0 Documentation (nodejs.org)

    (更多…)
  • 在 GitHub Actions 里使用 Lighthouse 和 Cypress

    在 GitHub Actions 里使用 Lighthouse 和 Cypress

    GitHub 给我们每人每月 2000 分钟的 Actions 免费额度,可以用来跑 CI,不好好利用就太浪费了。正好最近想学学 Cypress,于是就拿前面说的 mywordle.org 项目练手,加上了自动化 Lighthouse+Cypress。下面主要分享过程,希望对大家有帮助。

    0. 开启 GitHub Action 并完成 hello world

    点击项目里的 Actions 选项卡,如果该项目之前没有 workflow,就会自动进入创建 workflow 的界面。

    项目首页
    新建 workflow

    建议直接选择 node.js 模版,可以节省很多配置成本。因为免费额度只有 2000分钟/月,所以我建议只保留一个 node.js 版本;又因为我要用 pnpm 管理依赖,所以改造后的配置是这样的:

    name: Node.js CI
    
    on:
      push:
        branches: [ master ]
      pull_request:
        branches: [ master ]
    
    jobs:
      build:
    
        runs-on: ubuntu-latest
    
        strategy:
          matrix:
            node-version: [16.x]
    
        steps:
        - uses: actions/checkout@v2
        - uses: pnpm/action-setup@v2.2.1
          with:
            version: 6.32.2
        - name: Use Node.js ${{ matrix.node-version }}
          uses: actions/setup-node@v2
          with:
            node-version: ${{ matrix.node-version }}
            cache: 'pnpm'
        - run: pnpm i
        - run: npm run build --if-present

    这个阶段我只需要确保代码可以完成构建就行了,没有运行更多脚本。

    1. 使用 Lighthouse 校验 CLS

    Lighthouse 是 Google 提供的网页评价工具,可以对网页的众多指标进行打分,帮助我们提升网页的运行效率和用户体验。CLS=Cumulative Layout Shift,即页面初始化阶段,布局变化的统计值,变化越多值就越大,页面抖动越厉害,评分就越低。

    据说这个值很影响 Google 对页面的评价,所以我们必须时时关注。

    首先,使用 pnpm 安装 Lighthouse:pnpm i lighthouse -D

    我们还需要静态服务器提供网页服务。如果你仔细看前面的配置文件,会发现这个环境基于 Ubuntu 搭建。在配置里增加一行 - run: nginx -v,确认镜像里集成了 nginx 1.18,那就好办了,添加 mime.types 和 nginx 配置文件:

    pid logs/travis.nginx.pid;
    
    events {
        accept_mutex off;
    }
    
    http {
    
        server {
            access_log logs/travis.access.log;
            error_log logs/travis.error.log warn;
    
            listen 9000;
    
            include mime.types;
    
            location / {
                alias dist/;
                # 下面这行是 SPA 的关键
                try_files $uri $uri/ /index.php$args;
            }
        }
    }

    放到 workflow 里跑一下,报错了:

    Run nginx -c conf/travis.conf -p `pwd`
    nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (13: Permission denied)
    2022/03/18 09:38:08 [emerg] 1870#1870: open() "/var/log/nginx/access.log" failed (13: Permission denied)
    Error: Process completed with exit code 1.

    因为 Nginx 一定要把全局错误日志放在 /var/log/nginx/error.log,而又没有权限,所以报错。解决方案有两个:

    1. 升级到 nginx 1.19,便可以使用 -e 参数自定义全局错误日志的路径。不过这意味着我们每次都必须更新镜像,毫无疑问会浪费宝贵的免费额度。基本放弃。
    2. 确保 nginx 可以操作这个文件,这可能需要 root 权限。

    在配置文件里随便加一行 sudo touch /var/log/nginx/error.log,顺利完成,说明我们拥有 root 权限。于是加入以下配置,完成 nginx 配置:

        - run: sudo chmod -R 755 /var/log/nginx
        - run: sudo touch /var/log/nginx/error.log
        - run: sudo chmod 777 /var/log/nginx/error.log
        - run: nginx -c conf/travis.conf -p `pwd`

    接下来就简单了,使用下面的命令可以调用 Lighthouse 给网页打分,并且将数据输出到 lighthouse.json,然后我们写个脚本分析即可:

    lighthouse --chrome-flags="--headless --disable-gpu --no-sandbox" --output json --output-path=lighthouse.json http://localhost:9000

    2. Cypress

    接下来搞 Cypress,时间因素我就不详细介绍 Cypress 的用法了,他们官网有很详细的视频,虽然是英文的,不过我觉得大概也看得懂。建议大家先看一下,了解个大概:Installing Cypress | Cypress Documentation

    这次我打算添加两个测试:过关,和失败。

    尝试 Cypress 的过程并不顺利,主要原因是 Cypress 的语法设计有点难懂。比如下面两行代码:

    cy.visit('http://localhost:9000/');
    cy.get('#header').should('contain', 'Wordle Unlimited');

    看起来它要完成两步操作,其实不然。Cypress 只是暂时把这两步操作存入操作队列,未来满足某个条件才会执行。所以,我们不能将其它操作插入这些步骤中间。比如我的游戏数据存在 localStroage 里,我希望在网页打开后校验这些数据,于是我就后面读取 localStorage,但怎么也读不到。正确的做法是在第二句的后面用 .then(() => {}) ,然后把操作放在里面。

    哦,对了,基于同样的原因,Cypress 的测试函数也不能是 Async Function。

    不知道如果将来需要做条件判断或者循环的话,应该怎么写。

    接下来,只要配置对应的 action 即可。我选择使用官方的 cypress-io/github-action@v2,虽然我也不知道它比自己写多了些什么,不过我觉得能省事总是好的。需要注意的是,因为我使用了 pnpm,所以我不需要官方配置里的 install 步骤。

    3. 使用环境变量

    有时候,我们的项目依赖一些第三方工具,这就需要我们能够在构建或测试的时候配置第三方工具的鉴权方式,比如 access_key,secret_key 之类的东西。很显然,我们不能把这些鉴权信息入库。(实际上,随便搜一搜,能找到大量这么做的代码。)所以我们需要用别的方式来配置这些信息。

    最常见的做法是环境变量。比如我们配置一个 WX_PAY_PRIVATE_KEY,然后我们就可以在代码中使用 process.env.WX_PAY_PRIVATE_KEY。GitHub Actions 可以使用 env: 配置环境变量,但是这些变量一样不适合入库,所以我们需要配置安全信息。

    配置位于上图所示的地方,添加完之后,就可以在配置文件中使用:

    - name: 步骤
      env:
        AIRTABLE_API_KEY: ${{ secrets.AIRTABLE_API_KEY }}
      run: 命令

    4. 完整配置文件

    name: Node.js CI
    
    on:
      push:
        branches: [ master ]
      pull_request:
        branches: [ master ]
    
    jobs:
      build:
    
        runs-on: ubuntu-latest
    
        strategy:
          matrix:
            node-version: [16.x]
    
        steps:
        - uses: actions/checkout@v2
        - uses: pnpm/action-setup@v2.2.1
          with:
            version: 6.32.2
        - name: Use Node.js ${{ matrix.node-version }}
          uses: actions/setup-node@v2
          with:
            node-version: ${{ matrix.node-version }}
            cache: 'pnpm'
        # setup local server
        - run: pnpm i
        - name: build repo
          env: 
            AIRTABLE_API_KEY: ${{ secrets.AIRTABLE_API_KEY }}
          run: npm run build --if-present
        - run: mkdir logs
        - run: sudo chmod -R 755 /var/log/nginx
        - run: sudo touch /var/log/nginx/error.log
        - run: sudo chmod 777 /var/log/nginx/error.log
        - run: nginx -c conf/travis.conf -p `pwd`
        # run lighthouse
        - run: pnpm add lighthouse -g
        - run: lighthouse --chrome-flags="--headless --disable-gpu --no-sandbox" --output json --output-path=lighthouse.json http://localhost:9000
        - run: node tools/lighthouse.js
    
        - name: Cypress run
          uses: cypress-io/github-action@v2
          with:
              install: false

    贴上完整配置,供各位参考。

    5. 总结

    实际体验下来,大约花了 60 分钟,每次运行大约两分半钟,看起来一个月 2000 分钟额度还是蛮充足的。

    建议大家都学习一下,CI/CD 是现代化开发的基础设施,可以大大提升我们的开发效率。Lighthouse 是非常重要的网站评价工具,Cypress 可能是现在最好的 UI e2e 测试工具,结合这几个工具可以保障我们的网站始终可用,始终好用。

    如果你看过我的上一篇文章《在 Code.fun 做 Code Review》,其实偿还技术债最好的办法就是写自动化测试,因为只有自动化测试才能告诉你重构有没有引入新问题、能不能上线,优化有没有成功;以及,能否推进下一步重构与优化。

  • 在 Code.fun 做 Code Review

    在 Code.fun 做 Code Review

    0. Code Review 简介

    Code Review,翻译成中文应该是“代码评审”,是软件开发中非常重要的一个环节。简单来说,就是 A 做完一个功能后,发给 B 审查;B 确认代码符合规范、没有明显的质量问题后,才允许合并入主干,以及上线。

    Code Review 有很多好处:

    1. 提升代码质量,维护代码规范。
    2. 传承知识。Code Reviewe 是非常好的查缺补漏机会,可以针对性补强开发者的知识盲区,纠正不良习惯。
    3. 找出潜在 bug。一个人可能会考虑不周,但多个人同时犯错的几率就会小很多。
    4. 降低安全风险。如果开发者知道自己的代码会被多人审查,那 ta 多半就不会主动引入漏洞和后门。

    那么,Code Review 怎么进行呢?实际上,一次提交少则几十行,多则几百上千行代码,想短时间内完全看明白是很困难的。负责审查代码的人自己也有工作,不可能投入太多时间在看别人代码上。所以,真正的 Code Review 并不要求完全弄明白对方的代码,一般重点在下面几个方面:

    1. 确保开发者遵守了规范
    2. 确保开发者提供了测试用例,且测试用例能覆盖需求场景
    3. 确保代码中没有明显的问题

    1. Code Review 实操

    接下来,我就结合最近两周的 Code Review 工作,分享一下相关经验,希望对大家有所帮助。

    1. 不该使用入口文件

    我司之前很喜欢把组件全 import 到一个入口文件里,然后全部 export。在需要的地方 import { someComponent } from '@/component'。这样做的坏处是,如果想使用路由 lazy-loading,Webpack 打包的时候,很难判断哪些组件是当前路由需要的,哪些是不需要的,导致大部分组件都被打包进入口文件,影响启动速度。

    正确的做法是哪里用组件哪里引用,不要多此一举。

    2. 乱用自定义事件

    截图中的组件是个 <el-input>,写代码的同事在面前给 <el-form> 加上了 submit.native.prevent,侦听并阻止原生 submit 事件;然后在 <el-input> 里侦听用户按下回车键事件,以提交表格。

    这也是很不好的方式。原生的、规范的事件和操作,一般都已得到众多浏览设备的支持,包括浏览器、屏幕阅读器、各种 IoT 设备,等。如果我们禁用原生事件,自己定义新的事件处理函数,可能在我们自己的设备+浏览器里运行正常,在更丰富的环境里则随时有失败的风险。

    3. Boolean 属性应使用单一属性值

    这是个规范问题,Boolean 属性都应该使用单一属性值,遵守 HTML 规范。

    4. 尽量不要写死 style

    我们要适配的设备很多,分辨率千奇百怪,写死的属性值可能在部分环境下运行正常,但是在其它环境下就会失败。再来,CSS 和 JS 的环境是分离的,很难共享数据,很可能 CSS 如果要配置 dark mode、响应式、打印策略时会遭遇问题。

    所以,当我们需要条件渲染时,建议:

    1. 写类名。于是 CSS 可以修改类的属性值。
    2. 写 CSS Variable。CSS 里用表达式来调整。

    5. 谨慎处理循环

    Code Review 时一定要特别注意循环的处理,这里是最容易出问题的地方。比如图上这段代码,应该先筛选再映射(.filter().map()),而不是现在这样。那样的话,可以大大节省 map() 的执行时间。

    6. 仔细查文档,不要对付

    window.postMessage 需要两个参数,但是我的同事只想传一个,但是会过不了 TypeScript 校验,于是他就把 window as any 了。这就是不该偷懒的地方偷懒 。

    2. Code Review 的深度,以及我们应当遵守什么规范

    因为大量其它代码有类似……的做法,可以等之后统一修改验证

    看到我的 changes request 之后,同事这么回复我。其实这也是一个常见问题:已知我们的代码没有严格遵守规范,存在不少历史积累的技术债,我们应当怎么处理?

    有些同事说,我们有一些代码是按照老规范写的,如果我们现在按照新规范,就会在代码仓库里留下两个风格的代码,这样是不是更有问题?

    我的观点是,我们要把新规范分为几个层次:

    1. 有很大优势。那我们应该尽快完成代码升级、重构,把这些优势带到我们的产品当中。
    2. 有明显优势,但可能没大到值得延后交付去处理。我们应该在新代码中使用这些规范、在触及老代码时顺手修改,不需要担心两种规范的冲突。截图中大部分都在这个级别,我们应该开始改进,但不需要刻意追求覆盖率。
    3. 优势不明显。那就可以只在新代码中使用,旧代码可以留着不改。
    4. 几乎只是风格问题。那应该在采纳成新规范时做决策。

    3. 总结

    以上是我在我司最近两周作 Code Review 的经验分享,希望对大家有所帮助。质量和效率、如何还技术债,是软件开发领域永恒的话题,我以后也会继续在这方面展开分享。如果诸位读者老爷有什么想问的、想说的,敬请在评论区与我互动。

    我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,也可以联系我。