我的技术和生活

  • 2022 Code for Better _ Hackthon 获奖感言

    2022 Code for Better _ Hackthon 获奖感言

    奖项揭晓,没想到我竟然拿了二等奖。其实我是瞄准安慰奖去的,得知入围获奖名单就很开心了。写这段感言的时候,我以为自己是“优秀奖”,没想到竟然高攀二等奖,真是既惊又喜呀。

    看来我真的好好把这款产品做好了,做成一个真正意义上的 side project。


    简单介绍获奖作品,可从使用的技术、作品意义/价值等

    参加本次 Hackathon 的意义和获奖感言

    大家好,我是参赛团队 roudan.io 的代表翟路佳。其实我们团队有两个人,但我那位设计师朋友觉得工作量太少,就没有报名,于是今天大家就只看得到我了。

    我是一名全栈偏前端开发者,目前就职于 code.fun。我喜欢编程,希望把编程当作终身职业。我 2006 年毕业,从页面仔开始入行,如今已经工作 16 年了,作为一名中年开发者,我仍然能感受到编程的乐趣,也很享受学习各种新知识、尝试各种新技术的过程,只不过享受过恣意妄为的青春岁月之后,我不得不付出中年罹患各种慢性病的代价。

    感谢现代医学昌明,大部分慢性病都只影响生活质量,不太致命。在长时间学习与慢性病共存之后,我发现,吃药这件事,比我之前以为的复杂;而如果能好好吃药,就能提高药效,用更低的剂量换来更好的效果。

    同时,作为一名程序员,我也很喜欢尝试新设备与新应用,比如各种智能药盒和用药助手 App。但我也遗憾地发现,这些产品的产品经理自己可能不太吃药,他们设计的产品只能帮忙患者记着用药,但并不能让患者科学用药。

    于是我就想做这样一个应用或者产品,能帮患者科学用药,改进健康情况。所以,我看到这期 hackthon 的主题:Code for Better Life/Health 之后,觉得天助我也,时不我待,于是早早就报名参赛。我的作品是姆伊用药助手,希望它能像我家的狗狗一样忠诚的陪伴在用户身边。

    作为 web 全栈开发者,我首选基于浏览器的各种技术。Google 一直是浏览器技术的重要推动者,我们常用的 Chrome 基本代表着 web 技术的先进方向;PWA 则将 Web 应用的体验门槛拉低到绝大部分用户都能享受的地步。选择浏览器+PWA 开发,既能覆盖足够多的用户、又能提供很高的开发效率、以及广阔的发展空间,于是我就选择了这套技术方案。

    当然,仅在 hackthon 的规定时间内,我无力把产品做得足够强大可靠有效,所以目前只实现了一款糖尿病药物的添加。实话实说我没想到能获奖,感谢评委老师们的错爱,给了我更强的动力去完善这款产品。作为这款产品的目标用户,我希望能够把这款产品继续开发下去,支持更多的药物类型,支持更多的用药方式提醒。我还想趁机学习一下 TersorFlow 深度学习技术,让 AI 能够助力这款产品,帮助更多的慢性病患者科学用药,减轻病痛。

    思否一直是 hackthon 的泰山,Google 则是技术型公司的北斗,感谢两家公司给我们开发者带来这样的机会,让我们一起为更好的未来编码。希望明年还能有机会参加这样的 hackthon,预祝我们大家明天都会更好。


    我们本次参赛作品是姆伊用药助手,我们希望这款应用能帮助慢性病患者科学服药,提高药效、减轻病情、改善生活质量。它的灵感来源于我们自己的日常难题,很多类似的产品缺少对用药方式的关注,于是我们希望动手改进。

    这次比赛的主题让我们非常激动,感觉机不可失时不我待。通过此次比赛我们终于把想法变成现实,其中借助谷歌浏览器开发技术,我们得以快速实现产品原型;假借现代化浏览器和 PWA,我们让这款产品有机会惠及更多用户。目前梦想才刚刚开始,我们希望将来产品能帮到更多慢性病患者。

  • 远程工作面面谈(1):远程工作的类型

    远程工作面面谈(1):远程工作的类型

    我经常在 V2ex.com 上潜水,看到很多同学想找远程工作,但是对远程工作的基础概念都还搞不清楚,于是想把自己这些年远程工作的经验系统的总结一下,供大家参考。今天先简单列举一下远程工作的类型吧。

    这里的类型,是按照对劳动者个人的保障程度来划分的。因为我接受了一家媒体的方案,期间记者提到,她不敢找远程工作,因为怕公司不靠谱。我觉得这个问题很常见,不仅对员工,对客户也是如此。于是,就按个人保障程度,列出下面种种工作形式。

    0. 远程工作的定义

    这里,我准备用一个比较宽泛的定义:

    1. 绝大部分工作需要在书案前完成
    2. 大部分工作时间不需要去往特定的工作地点,可以自由选择
    3. 雇工双方不一定存在严格的劳动关系

    按照这个标准,低底薪的销售岗位就不算远程工作,接私单的程序员就是远程工作。大概这样吧,比较方便后面的讨论。

    1. 全职远程工作

    我现在的工作就是全职远程工作,我也推荐这样的工作形式。主要特点有:

    1. 我会跟公司签订劳动合同
    2. 公司会提供稳定的薪资、福利,上社保公积金,代缴税
    3. 我的工作地点不受限制
    4. 我要保证大部分工作时间可以快速响应公司需求

    可以这么说,除了第 3 点外,这种工作形式和传统的集中办公并没什么区别。具体到我厂,我们有自己的产品、自己的愿景,也有健全的岗位架构,通过人事代理公司在员工所在地上社保公积金,与集中办公的公司并无差异。

    当然,并不是所有支持远程工作的公司都是这样,每家公司的工作流程不同、管理模式不同,最终表现出来的工作状态也不同。大家应该在面试时了解清楚。

    2. 独立开发者

    独立开发者久已有之,不过随着移动互联网的到来,各种应用市场如雨后春笋般涌现,独立开发者越来越多。独立开发者,一般自主捕捉市场需求,开发应用或游戏,上传到应用市场销售。用户可以购买、订阅,开发者们则通过市场分成获得收益。

    独立开发者一般没有老板,他们都是自己的老板,自负盈亏。独立开发者需要自己解决缴税、社保等问题;不过独立开发者也更自由,不想工作甚至可以不工作。

    因为我本身是程序员,所以我用“开发者”指代这个群体,实际上,独立音乐人、自媒体等也可以算在这个大类里。只要自己有作品,全权(或大部分)拥有作品,靠自己的作品挣钱过活,都可以算在这里。

    3. 接单

    这是很多同学所理解的远程工作。通过某些途径接单,然后完成,然后收钱,然后寻找下一单。这种形式下,劳动者和雇主之间是劳务关系。雇主不承担社保公积金,也不关注具体的工作完成过程,只在验收之后支付商量好的报酬。

    它的问题显而易见:

    1. 缺少劳动保障
    2. 单量不确定,多的时候可能忙不过来,少的时候青黄不接
    3. 缺少沉淀,纯粹的劳动换收入
    4. 遇到不靠谱的老板就鸡飞蛋打

    有些同学会成立一些松散的工作室或小团队,通过多人分摊工作量的方式提高接单量,分散风险。我觉得国内的码市比较靠谱,他们有专门的项目经理帮忙分解项目,按期付费。当然,这是 5 年前了,现在的情况不太确定。

    我觉得这种形式偶尔为之,兼职搞搞未尝不可,当作真正的远程工作风险太大。

    4. 兼职

    有些公司会招兼职人员,比如以前的 OpenResty,有一些非关键岗位,需求不是很固定,老板会愿意招兼职员工。他们不需要像我们一样保证每天的工作时间,也不需要固定参加各种会议,只要保证每周完成一定工作量的工作即可。

    类似的招聘需求在各个网站都比较常见,有些是固定岗位,有些可能只是抓壮丁。一般来说,我也不太推荐这种工作形式,尤其不建议有全职工作的同学尝试。

    5. 区块链相关

    我觉得这一类工作应该单列出来。目前大部分区块链公司的主营业务(Web3、NFT、空气币等)在国内都是非法的,所以这些公司实体基本都在国外。但是国内工程师能干耐操放眼世界范围都实属难得,又受语言等因素影响,很多公司很乐意从国内招人。

    于是这方面的机会也很多。不过显然,这些公司无法在国内提供公司实体,签合同都是跟国外某个不知真假的公司签,出了问题很难追责。工资发放也多半是各种币,如何评估其价值见仁见智,但是转换过程一定会有所损失,需要在起初就考虑清楚。

    6. 总结

    上述几个不同的远程工作类型,除了(1)之外,其它几个界限往往并不明显。有些同学可能到处接单,在接单之余自己开发应用,或者以兼职身份受雇于某家公司。

    即使第一种工作,不同公司的工作流程、对工作过程的考核也不太一样。比如我厂,使用敏捷开发,每天有晨会,迭代结束有总结会,其它时间则基本各干各的,有问题就抓人开会。有的公司则需要装监控软件,打卡计时;甚至随时保持摄像头开启以便管理者检查。我认为这两种工作形式是完全不同的。

    总之,我推荐第一种,也就是全职远程工作,然后选择类似 OpenResty 或者 Code.fun 这样彼此信任,有共同目标和愿景的公司。因为只有这种工作形式,才能培养供需双方的远程工作能力,把我们导向更好的就业环境、工作环境。


    昨天给我厂升级 Sentry,官方文档漏提插件处理的步骤,导致我把 Sentry 搞挂了,折腾了一天才恢复,没时间写博客。今天随便水一篇吧,一直想好好聊聊远程工作的话题,今天开个头。

  • 在 Code.fun 做 Code Review(四)

    在 Code.fun 做 Code Review(四)

    时光如梭,一晃 2022 年已经过去 2/3,我们一起迎来 9 月。秋风送爽,丹桂漂亮,下面,我们一起回顾 8 月份我在 code.fun 完成的 Code Review 吧。

    关于 Code Review 的介绍和经验,欢迎大家回顾前三篇,本文暂不重复:


    (更多…)
  • 免费线上讲习班:使用 vue3 开发扫雷游戏

    免费线上讲习班:使用 vue3 开发扫雷游戏

    【2022-09-03 更新】把游戏先做出来了,以后可以扫自己的雷了:meathill/minesweeper: yet another minesweeper game (github.com)

    不过也发现,一次 workshop 肯定讲不完,考虑到现场的情况、各位同学的状态,估计初版开发要两次课。于是我干脆把后面的 Workshop 也规划出来了。


    好了伤疤忘了痛,我又想搞 Workshop 了。这次想做一次 Vue3 相关的,考虑了一下,觉得扫雷游戏比较合适:

    1. 界面简单,操作简单
    2. 覆盖全面,布局、JS、交互都有需求
    3. 不需要前后端数据交互,控制学习范围和难度
    4. 本身有一定可玩性

    可以学到的知识技能

    1. Vue3 开发应用
    2. Vue3 组件式开发
    3. CSS 高级用法
      1. CSS 布局
      2. CSS grid
      3. CSS 变量
    4. vite 脚手架

    难度

    • 中低难度
    • 逻辑较简单
    • 纯前端,无后端,无数据交互
    • 不需要开发环境,在浏览器里完成编码

    预备知识

    • HTML、CSS、JS 的基本语法
    • Vue 的基本用法

    开始时间

    (暂定)2022-09-18(周日)下午 3~5点

    报名方式

    微信直接找我口头报名即可。未加我微信的同学,请扫码

    参与方式

    我会把所有报名的同学拉到一个群里。workshop 开始时,我会创建一个腾讯会议,然后把会议链接发到群里,大家通过链接进入参加。

    事先准备

    Workshop 强调参与,不建议大家只是看,所以最好提前做一些准备:

    1. 一台支持浏览器的设备,最好是电脑
      • 可以的话,最好多准备一台设备,可以边看边敲
    2. 现代化浏览器,推荐 Edge
    3. 提前打开 sfc.vuejs.org
    4. 自行熟悉扫雷游戏
    5. 最好实现了解网页开发,HTML+CSS+JS

    欢迎报名。达到两位或两位以上同学报名,就按时开始。已达到启动标准,我开始准备了。

    我会同步开启直播,有兴趣但无法参加 workshop 的同学可以到我的直播间观看:https://live.bilibili.com/5126601


    什么是 Workshop(讲习班)

    Workshop,中文叫讲习班,工作坊。大概方式是:

    1. 由讲师讲解一些技术知识点
    2. 其他参与者针对讲过的技术知识点做现场演练
    3. 讲师及时答疑、讲解
    4. 考虑到时间因素,一般针对一些小专题,非系统性的培训

    与直播不同,直播是讲师一个人说+做,workshop 则强调给大家一个现场练习与答疑的机会。

  • 复盘 mywordle.org

    复盘 mywordle.org

    去年,有位开发者设计了一个填字游戏 wordle,取得了巨大的成功,最后被纽约时报斥资百万收购。就像众多成功产品一样,wordle 也有很多追随者和模仿者,其中就包括我们做的 mywordle.org

    刚上线时,因为优化得当,排名不错;如今,随着 wordle 游戏的关注度消退,这个产品已经趋于平静,访问量跌入谷底。于是写篇文章总结下技术、产品、运营方面的经验得失。

    技术向

    技术栈

    之前发过笔记:使用 Vite+Vue3+TypeScript+Tailwind CSS 开发 Wordle

    • Vue3 + Vue-router + Vuex
    • Vite
    • TypeScript
    • TailwindCSS
    • 骨架屏
    • nginx
    • i18n (编辑本地 json)
    • PWA

    纯静态页面,通过构建脚本一次性发布,后面就不需要服务器运算,只需要 CDN,运维成本很低,容易扩展。

    前端通过适当的分包实现按需加载,加快打开速度,提升用户体验。实际效果不错,搜索排名和留存都相当好。Lighthouse 一度基本满分。

    挑战0:全新技术栈

    项目启动时,Vite、Vue3、TypeScript 的内容不算很多,技术生态也没有完全适配,花了不少时间去学习。不过好在当时有时间,慢慢也捋顺了,虽然有一些问题到今天也没能妥善解决,但并没有影响整体进度。

    挑战1:多语言多模式共存

    当时存在两个模式:

    • hourly,每小时一个词
    • unlimited,随便玩

    以及十几种语言。因为我们是静态网站,想实现 /:lang/:type 和 /:type/:lang 共存,就要同时打包这么多组目录组合。如果将来又增加其它类型,就还要成倍增加。但是几个目录里的内容又是完全一样的,很浪费。

    现在想想,应该通过 nginx 来解决这个问题,不要放在前端构建脚本端。

    挑战2:WebRTC

    我们准备尝试用 WebRTC 实现多人对战,如果能成的话,将来还有很多应用场景。可惜 WebRTC 比我想象中复杂很多,不是抽点时间看看文档就能搞定的。当时我已经开始在 code.fun 的全职工作,时间不如启动时充足。

    于是此功能最终停留在 demo,未能整合进产品,更别提上线。

    未解决问题

    • import 类型的时候必须 import type { xxx } from '@/some/types',经常出错
    • TailwindCSS 添加新样式时无法即时生效,需要等下一次更新,或者手动刷新页面
    • ESLint 有很多误报,主要是 <script setup> 导致的未使用变量问题

    产品/运营向

    原始版 wordle 一天只能猜一次,一个词。很多玩家感觉不过瘾,所以搜索 wordle unlimited 就很多,我们也是那会儿做好,然后优化得当,排名很靠前,Google 前4(最好前3),吃了不少流量。

    但是很快,wordle unlimited 的搜索量就下降了,到现在跌了90%,只有一成。

    这是流量统计,外国人也是上班摸鱼,周末不玩页游 [Facepalm]

    这部分流量流去了 quodle 关键词,还是这个猜词游戏,但是一次猜 4 个词,更考验技巧和统筹能力。

    有趣的是,quodle 主流分两个模式:daily 和 practice。daily 还是一天一次,用的是高频词,比较好猜;practice 相当于我们做的 unlimited,不限次数,但是,用的是全部词库,几乎猜不到。

    本来有两个方向,quodle 和 pvp,我说服朋友搞 pvp:我说 quodle 这种玩法太硬核了,没人爱玩;pvp 用 WebRTC 搞联机,成本低效果好,用户粘性大。结果还是我的锅,以前没搞过 WebRTC,搞了两周没搞出来,工作一忙就扔掉了……

    总结,wordle 是个比较轻量的游戏,玩法简单,可玩性有限。daily 模式可能更合适;unlimited 有些饮鸩止渴,快速消耗掉了玩家的热情。quodle 的开发者注意到这一点,一方面提供新的玩法刺激用户,另一方面利用全量词库难以游戏的特点尽量将玩家留在每天一次的游戏里。

    总结

    从这个项目中,我学到很多技术之外的知识,产品和运营方面都刷新了我的知识边界、扩展了我的视野,很有意思。

    比如,mywordle.org 是纯静态网站,运维只需要 CDN,加上优化得当,费用很低。凭借早发优势和一些 SEO,流量和广告收入都不错。即使以我的工资标准和开发习惯(动不动重构、选择新技术栈,等)来支付开发费用,也能达到不错的收益结果(目前折算 1/3 吧)。后期收入虽低,但很稳定,也不需要继续投入研发成本,而且还能作为高质量搜索导入来源。以后我应该多做几个类似的网站。

    项目启动时,我刚被金山开除,于是可以投入大量的时间去学习使用没接触过的技术栈;后面开始继续全职工作了,就只有一点时间可以支配,以至于 WebRTC 都没能用上。不知道将来还有没有类似的机会——我的心情很矛盾,既希望有,又希望没有,哈哈。

  • 在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    嗯,不知不知觉这个系列写到第三篇,这一篇会改变一下写法,从一次 Code Review 出发,讲解几个技术点,然后分享一下技术学习的经验,以及 Code Review 的用途。希望对大家有帮助。

    顺便推广下前两篇:

    0. 起因

    某天,我给一位同事做 code review,看到下面这段代码:

    const err = new Error('错误信息');
    
    return Promise.reject(err);

    于是我就回复:error 最好直接 throw

    然后他回复我:如果改成 throw 的话,这个 catch 好像是捕获不到 throw 的。

    这就很诡异了,这不符合 Promise 的设计;而 Promise 不是新生事物,它有很大的测试集可以保证行为符合预期,我觉得我们想遇到问题都很困难。于是我就跟这位同事连线,帮他分析问题。

    1. 真正的问题

    连线之后,我发现他的真正问题并不是按照我的要求修改代码之后遇到的。由于基础不太牢靠,他先写了一段实验代码,想要验证我的要求,结果这段代码行为出现异常:

    他希望能够通过 .catch(err => console.log(err)) 捕获并记录错误,但是从控制台的输出来看,错误却是 Uncaught(未捕获)。那么问题出在哪儿呢?

    作为曾经讲解过 Promise 的我当然是一眼就看出来问题所在,但是这位同学却琢磨不透。于是我就把问题发到研发群里,果然又问倒好几位同学。

    现在我请问各位读者老爷,你们知道么?或者换个方向,如果想正常使用 .catch() 捕获到错误,应该怎么修改呢?

    2. 问题解答

    Promise

    这里我们必须回到 Promise 的规范,才能了解上面截图的问题所在(关于详细规范,请参阅 MDN Promise,这里只摘我们需要的部分):

    1. Promise 主要用来改进编写异步函数的体验。
    2. 通过 new Promise(function (resolve, reject) {}) 会创建一个 Promise 实例,其中的参数应该是一个函数(通常是异步函数),接受两个参数:resolvereject。当异步操作成功之后,应调用 resolve(result) 并传递结果;当异步操作失败,应调用 reject(reason) 并传递错误信息。
    3. Promise 有三个状态:pendingfulfilledrejected,起始状态是 pending,变更后,就固定下来,不会再次变更。
    4. 如果异步函数本身抛出错误,Promise 也会进入 rejected 状态。
    5. fulfilled 的 Promise 实例会转入 .then() 处理;rejected 会转入 .catch() 处理。

    我们再回头看上面的截图,这里的问题在于,错误是在 setTimeout(异步函数)的回调函数里抛出的,抛出时当前 Promise 所在的执行栈已经结束了,回调函数是全新开启的执行栈,所以 Promise 无法捕获到它里面的异常;而它也没有主动调用 reject(err) 传出错误,所以就变成了 Uncaught(未捕获)。

    函数执行栈

    既然说到执行栈,我们就顺便补充一下执行栈的知识吧。形如以下代码:

    function a() {
      b();
    }
    function b() {
      c();
    }
    function c() {
      d();
    }
    function d() {
      // do something
    }
    a();

    当函数 a 执行的时候,运行时会开启一个新的执行栈,并且把 a 入栈接下来发现 a 调用了 b,就又会把 b 入栈;然后发现 c……运行时会重复这个过程。当一个函数执行完毕,比如 d,运行时就会把它移出栈,然后继续执行 c 的剩下部分。

    对于 JavaScript 而言,错误是可以冒泡的。即在 d 抛出的异常,如果没有被捕获,它就会一直上浮,直到回到全局。所以我们可以在栈内的任何一个环节捕获它;但如果跨栈,那就无法捕获。

    Event Loop

    异步函数的回调函数会由 Event Loop 开启新栈执行,与所以异步函数自身处于不同的执行栈,所以错误不会被异步函数捕获,自然也不会改变 Promise 的状态。

    解决问题

    所以,要正确捕获错误的话,就需要想办法捕获异步函数的回调函数的错误,即:

    function yy() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // 方案1
          reject(new Error('错误信息'));
          // 方案2,实际中很少这么写,这里只是用来演示。但要注意,捕获必须放在回调栈,才能捕获到发生的错误。
          try {
            throw new Error('错误信息');
          } catch (e) {
            reject(e);
          }
        }, 1000);
    
        // 实际场景中,更多是这样的,err 作为参数传给回调函数
        fs.writeFile(path, content, 'utf8', (err) => {
          if (err) {
            return reject(err);
          }
          resolve();
        });
      });
    }

    3. 如何学习技术

    对于上述问题,我们只要掌握 Promise 规范、函数执行栈、Event Loop,就很容易判断。但是如果这三个概念有一个不太清楚,就容易犯迷糊。所以,很多时候,要避免出问题、要保证软件正常工作,我们必须清楚了解每个技术的规范、定义。

    比如 Promise、原型链、闭包等等,它们都不是自然界的产物;而是在长期软件开发实践中,由开发者总结设计出来,用于解决特定问题的发明。他们都有严格的定义、功用、优缺点,等等。我们日常开发,应该把这些东西当成知识储备起来,针对需求做排列组合,给出解决方案。

    有些同学相反,他们会记下来一些技术的用法,然后反推这些技术的特性。如果对一个技术不熟悉,他们会尝试做类似实际场景的实验,然后再想办法搬到实际场景中。这样做,覆盖常见场景可能没问题,遇到陌生的领域就容易踩坑。

    所以,我要强调,对于新技术,大家不熟悉又要尽快用在实际开发中,临时做点实验记住些经验用法当然可以。但不应满足于此,要找时间把技术规范补起来,把完整的设计至少读个几遍。一方面纠正自己的错误实践,另一方面,也可以扩大你对这个技术的应用面。

    4. Code Review 的作用

    这里也不得不提 Code Review 的一个重要作用:

    传承知识。Code Reviewe 是非常好的查缺补漏机会,可以针对性补强开发者的知识盲区,纠正不良习惯。

    通过 Code Review,我发现了一位同事在 Promise 和函数执行栈方面存在知识盲区,然后我借机帮他补齐了这方面的知识。接下来,我把这个问题分享到技术研讨群,还有几位同学也不是很清楚,也趁机补齐了。于是,我厂再出现这个问题的概率,就降低了。换言之,我厂技术群体的下限,就拔高了。

    思否上有同学问:什么样的技术 leader 是称职的? 我的答案第一条就是:

    给团队的技术兜底。通过工具、规范、流程,保证无论开发水平如何,都能尽快提交符合要求、满足规范、质量过硬的代码。

    具体一点:要重视每一次 Code Review,找到问题,解决问题,补全大家的知识点,提升团队下限。

    5. 总结

    上次的 Code Review 分享获得了意料之外的欢迎,希望这次同样能帮助到大家——我已经把上面的问题加入我的面试题库了,哈哈。

    我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。我们公司提供远程工作岗位,有兴趣的同学可以联系我;朋友的公司 API7 也在招聘前端,有兴趣可以找我内推。

    如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

    6. 扩展阅读

  • 应用创意:用药助手

    应用创意:用药助手

    作为一名慢性病患者,定时吃药是必不可少的日常。我也试用过一些辅助软件和智能药盒,都比较失望,我感觉这些产品的产品经理本身都不怎么吃药,也不知道该怎么吃药。

    药物并不是顺水服下就能生效,其实有很多注意事项:

    1. 抗菌消炎止痛类药物,应该间隔特定时间服下,以保证体内药物浓度。比如一日两次,那就应该隔 12 小时;一次三次,就应该隔 8 小时。
    2. 降糖药二甲双胍,应该餐后半小时服下,这里的半小时以第一口食物入口为标准,要跟着餐时走。
    3. 降尿酸药苯溴马隆服下 1~2 小时后浓度最大,此时肾脏会加速排酸,要多喝水多排尿,所以要在能保证喝水的时候吃,睡前就不合适。

    我试过的软件和药盒都只能做到每日定时,无法做到开始计时、打开、提醒等功能,基本没啥用。

    另外,吃药久了,我还经常会忘记自己有没有吃药,也很烦。

    所以我想根据自己的需要和经验,开发一款用药助手小应用:

    1. 可以在开吃时计时,提醒用药
    2. 登记药品时,提醒服药须知
    3. 超过特定时间还没有打卡,也发出提醒
    4. 记录打卡,方便回顾

    技术选型方面,我希望探索 PWA 在手机平台的 background service worker 能否正常发出提醒。所以大体上还是 Web 技术,Vite+Vue3+Pinia+PWA,将来如果可以,再想办法转为 Flutter 或者小程序。

    刚好在思否上看到 2022 Code for Better _ Hackthon,因为疫情,所有活动都线下,那就利用这个机会做出来吧。

    有类似需求的同学,或者有相关知识的同学,欢迎指正和探讨。

  • 在 Code.fun 做 Code Review(二)

    在 Code.fun 做 Code Review(二)

    这周周会上,有同事说:

    以前他的 PR 被 approved 就直接合;现在他会等上一两天,期待我来 review。晚上耍手机时如果突然一阵爆响就说明我 reviewing,他就爬起来好好看我的评论然后作出修改,学到很多东西。

    被承认当然很爽、很骄傲。于是我想起这个系列,该再更一篇了。本文选材自七月的 Code Review 经验,趁热先分享一波。更早的 Code Review 过两天再往前翻翻看能否整理出来。

    关于 Code Review 的介绍和经验,欢迎大家回顾:在 Code.fun 做 Code Review,本文我就不重复了。

    0. 不应该使用独立 <script src="...">

    在这个需求中,我们要使用火山引擎统计用户行为,某同事就直接在模版里添加了 <script src="..."> 导入初始化 JS。这段 JS 非常之短,只是帮火山引擎初始化执行环境,但是为了保证执行状态,它既不能 async 也不能 defer

    这样做有几个问题:

    1. <script> 会阻塞后面的 HTML 渲染和 JS 执行;
    2. 这段 JS 虽然短,但是它的下载时间不会短:
      1. 浏览器要解析 DNS,
      2. 然后文件服务器要找到文件
      3. 文件服务器甚至没有 http2

    正确的做法是把它直接塞到现在 JS 里,只增加几十个字节而已。

    1. mounted 钩子添加的事件处理函数没有清理;直接使用 window.onblur

    这位同学犯了两个错误:

    1. 他在 mounted 钩子里添加了事件处理函数,但是并没有在 destroyed 钩子里清理。这可能导致内存泄漏,也可能导致奇怪的表现。
    2. 他直接将处理函数绑在 window 上,如果其它库、其它代码使用了同样的事件,就会导致冲突。

    2. 糟糕的命名

    这里这位同学犯了两个错误:

    1. event 是名词,不应该用做函数名。函数名应该是动词、动宾短语、动副短语,表达一个动作。
    2. event 是个非常非常通用的单词,用作全局函数太容易和其它全局对象起冲突或起混淆,应该加长。

    结合上下文,合适的写法是:triggerHuoshanEvent()

    3. 依赖 JSON.parse()

    我们需要记录一个小数据:sourceType,这个小数据被封装在一堆很大的 JSON 里面。这位同学直接拿 JSON.parse() 解码,然后打点。这样做当然可行,但是效率上非常之浪费。

    大家千万不要小看 JSON.parse(),它其实有很多细节:

    1. JSON 数据格式非常之严格,换言之,绝大对数情况下,必须把所有字符串都分析完才能结束
    2. 这个过程中,会构建大量对象
    3. 之后不再使用,这些对象又要被逐个回收

    所以正确的写法,应该使用字符串匹配或者正则。字符串操作都是 O(1),而且会在匹配到结果后立即返回,所以性能优秀非常多。

    (其实吧,这位同学的正则也有些一言难尽,不过今天先不展开。)

    4. TypeScript 的数据转换

    使用 TypeScript 之后,我们需要声明函数的返回值类型。对于一些比较通用的返回值,比如截图中的 mongoose lean(),就要进行类型声明转换,后面的代码才知道取到的值是什么类型。这里大家要记住,无论是浏览器还是 node,TS 都要经历编译之后,变成 JS 再被执行。

    所以 TS 阶段的类型转换不会出现在最终执行的代码里,也不会影响效率。但是截图中属于不同的情况,它会留下一个 .then() ,并增加一个微任务,不是我们想要的结果。

    5. TS 的枚举值可以适当调整

    TypeScript 提供 enum 关键字,用来声明枚举值,可以大大提升开发效率,降低出错概率。枚举的值可以手动指定,也可以省略,省略后 TS 会自动帮我们从 0 开始递增。上面就是典型的使用场景,列举若干设计软件,最后是 Other(其它),可以做成单选多选放在表单里。

    这段代码的问题在于,设计软件很多,我们列出来的肯定有疏漏;即使没有,将来也会发布更多设计软件。比如有天,我们自己做了设计软件,叫 design.fun,那么该放到哪里呢?放到 Other 前面,它的值就会跟 Other 冲突;放到后面,又会使得数据看起来很奇怪。

    此时只要给 Other 一个大点的值,拉开差距,就可以解决这个问题:

    export enum DesignSoftware {
      Photoshop,
      XD,
      JsDesign,
      Other = 100, // 再大点也行,不过 100 应该够我们用了
    }

    6. 组件名称和根节点样式应包含不少于两个单词

    这个其实是 Vue 官方的风格建议。原因很简单,一个单词,无论作为组件名称还是根节点样式名,都太容易和其它组件重复,也会影响 IDE 里快速跳转的效率,所以尽量多写几个单词,反正编译过程可以自动优化。


    总结

    时间过的真快,距离上次分享 Code Review 经验已经过去四个月了,我在 Code.fun 也是半年多工龄的老员工了。经过半年多的努力,我厂的产品完整了很多,但也很难称得上完善。希望这次分享的内容对大家有用,也希望大家多来试用我们的产品。

    如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

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

  • 聊聊糖尿病

    聊聊糖尿病

    免责声明

    本文为病友之间探讨病情,不构成医疗建议,强烈建议有类似症状的读者前往本地正规医院找有资质的医生问诊。

    糖尿病是很常见的基础疾病,在我国发病率很高。我父母两边的亲属均受此病困扰,我姥姥直接死于糖尿病导致的各种并发症。我个人也因为不良生活习惯,早在 2015 年年底就确诊 II 型糖尿病。写此文是想给大家分享一些基础知识、发病前、发病后的注意事项。

    0. 糖尿病及其诱因

    首先,纠正一个误区(可能只是我个人的误区):糖尿病并不特指尿里有糖,实际上,尿里有糖,说明发现得已经太迟了。确诊糖尿病的主要指标是血糖,血糖高过正常标准,就说明身患糖尿病了。

    人体内,由胰岛素负责调整血糖。当血糖升高时,身体就会分泌更多的胰岛素,身体各器官接到胰岛素的“通知”,就会尝试把血糖留下来、存起来,于是血糖就会降低。如果胰岛素无法正常产生、或者脏器无法正确理解胰岛素的信号(“胰岛素对抗”),血糖就无法正常下降,久而久之,就患上糖尿病了。

    从发病原因可以把糖尿病分 I 型和 II 型。前者一般是遗传问题,暴露的非常早,需要长期注射胰岛素,目前也没有办法避免。像我这样,因为生活习惯患上的糖尿病,基本是 II 型。II 型糖尿病主要是不良饮食习惯导致的,比如暴饮暴食、大量喝含糖饮料等,用医生告诉我的话:

    饮料里的糖会让你的血糖快速上升,然后你的胰岛细胞就要赶紧分泌胰岛素降血糖。你经常喝含糖饮料,胰岛就要经常出来工作,久而久之,就像人老是加班一样,就累病了。等你的血糖再次升高时,胰岛细胞心有余而力不足,没法分泌胰岛素,你就得上糖尿病了。

    1. 糖尿病的症状

    人体非常复杂多变。几乎所有科普文章都会告诉你:得了糖尿病会三多一少,喝水多、吃饭多、上厕所多,体重减少。这个说法本身没什么问题,但是具体到每个人身上,又会有不同的具体表现。

    比如说我。熟悉我的人知道,我非常爱出汗,于是我也非常爱喝水。糖尿病发作期间,我的饮水量并没有明显变化,上厕所的频率也基本没变。作为一个 120+kg 的胖子,我的食量其实并不算很大,但是也不算小,食量变化也不明显。体重变化较大,但是前面说过,我当时 120+kg,少个一斤半斤,效果肉眼几乎不可见。

    而且体重下降给我带来很多正向增益。之前因为太胖,我有比较严重的睡眠呼吸问题,体重下降之后,打鼾症状减轻,睡眠质量一下好了很多,精神状态也随之改善,中午甚至不需要午睡也能保证全天的工作效率。

    相信很多人都一样,状态好的时候,不会想说主动去医院看看。我也拖了很久,才决定去医院,当时糖化血红蛋白高达 12.5,算很严重的情况了,于是医生立刻安排住院。

    2. 糖尿病与并发症

    除了三多一少外,糖尿病并不会直接导致其它症状,很多人也是因此而忽视糖尿病,直到患上严重的并发症,才开始想办法治疗,但为时已晚。

    糖尿病主要有三个并发症:

    1. 眼部病变
    2. 足部病变
    3. 肾脏病变

    具体就不展开讲了,大家只要记得非常严重,死状甚惨即可。

    3. 能否治愈糖尿病

    首先我们要定义什么是“治愈”。

    通过改变生活习惯,配合按时用药,我们可以把血糖控制在合适的范围内。如果能长期坚持下去,不产生并发症的话,我们可以平稳度过一生。这是目前糖尿病患者的最佳归宿。

    如果想要尽快断药,过上正常人的生活,继续想吃啥吃啥,想喝啥喝啥,那是不可能的。

    我们每个人的胰岛细胞有强有弱:有些人能做大胃王吃播;有些人生活习惯不好,也不会得病;有些人还算重视,但中老年之后还是会患上。不过除了 I 型患者,大部分坚持到寿终正寝问题不大。

    按照目前的科技水平,胰岛细胞无法修复或再生,损坏不可逆。如果天生胰岛水平较好,只是太作导致糖尿病发作,经过调养,症状消失。也许短期内不吃药也还好,但将来胰岛一定会提前出问题。

    4. 糖尿病的日常保养

    前面说过,我得糖尿病是因为大量喝含糖饮料,所以,戒含糖饮料是第一步。

    接下里,要改善生活习惯,首先是饮食。对糖尿病患者来说,食物有三个重要属性:

    1. 含糖量。主要看单糖,果糖的升糖速度慢于白砂糖,但是吃多了会胖。
    2. 升糖速度。升糖速度快,对胰岛压力就大。
    3. 饱腹感。顶不顶饱,是否容易饿,少吃胰岛自然压力小。

    有些食物吃起来是甜的,但其实它的含糖量并不高,比如西瓜,因为含水量极高,所以吃下去的糖并不多,升糖速度也不快,算是比较好的消夏水果。

    有些食物吃起来并不甜,但是升糖速度高得吓人,比如白粥,比糖水升糖都快,又缺少膳食纤维,饱腹感很差,吃了很容易饿,就是糖尿病患者要尽力避免的食物。

    接下来推荐几个糖尿病友好食物:

    1. 黄瓜。黄瓜是很好的食物,清爽无怪味,可以提供水分和纤维素,能量是负的,很健康。建议大家吃饭前,先啃一根黄瓜(不要拌),然后再吃,既能控制饭量,又能减缓升糖速度。
    2. 玉米,特指玉米棒子。主粮方面,大米白面这样的精细制品只有不健康和很不健康的差异,但玉米因为其丰富的纤维素,是很健康的食物。
    3. 其它绿叶蔬菜。

    除了改变饮食习惯之外,还要增加体育锻炼。尤其是餐后一小时,血糖开始上升,胰岛细胞开始工作的时候,通过体育锻炼,让身体消耗掉多余的能量,可以很好的控制血糖上升。同时,体育锻炼可以让身体释放更多的能量出来,而不是存储成脂肪。

    5. 总结

    糖尿病是个慢性病,得了糖尿病不会立刻出问题,但是如果不重视,将来会遭遇非常严重且痛苦的并发症。糖尿病的核心是控糖,控糖控得好,糖尿病就不会对我们生活产生影响。所以日常生活要注意,要改变不好的生活习惯。

    愿病痛远离大家。

    有什么问题,欢迎留言交流。

  • MongoDB 实现多表联查并更新数据

    MongoDB 实现多表联查并更新数据

    最近有个需求,用 SQL 描述起来大约是这样的:

    # pages 表按照 projectId 统计每个 project 的页面数
    # 然后更新到 projects 表的 `pageCount` 字段里
    UPDATE `projects` a LEFT JOIN
      (
        SELECT COUNT('x') as `num`, `projectId`
        FROM `pages`
        WHERE `isDeleted`=false
        GROUP BY `projectId`
      ) b
      ON a.`_id`=b.`projectId`
    SET `pageCount`=`num`
    WHERE a.`isDeleted`=false
    
    # 上面 SQL 凑合看,我好久不写了,有点忘记怎么写……

    MongoDB Shell 其实就是封装好的 JavaScript + Node.js 16 REPL 环境,我们熟悉的箭头函数、异步函数等都可以放心使用,所以写起来大约是这样:

    // 进入聚合状态
    db.projects.aggregate([
      // 聚合状态会从上至下执行,所以我们先筛选出来合适的 projects
      {
        $match: {
          creatorId: '我的用户id',
          isDeleted: false,
          kind: 'Normal',
        },
      },
      // 因为我厂 pages 表里 projectId 是字符类型,所以这里要先转换一次
      {
        $addFields: {
          projectId: { '$toString': '$_id'},
        }
      },
      // 接下来就可以聚合了,用本地的 projectId 对上连表的 projectId,连起来之后的字段名为 pages,它应该是个数组
      {
        $lookup: {
          from: 'pages',
          localField: 'projectId',
          foreignField: 'projectId',
          as: 'pages',
        }
      },
      // 再添加一个字段用来存储 pages 的长度,即有多少个页面,这里要加 `$` 前缀
      {
        $addFields: {
          pageCount: {
            $size: '$pages'
          }
        }
      }
    ])
    // 聚合完毕之后,再把内容写入数据库
    .forEach(doc => {
      db.projects.updateOne({ _id: doc._id}, {
        $set: { pageCount: doc.pageCount }
      })
    });

    对于我来说,操作 mongosh 的难点主要在于:

    1. 很多时候要加 $ 前缀,一会儿一个一会儿两个,不熟悉经常搞错
    2. 不知道是否该用异步函数或者 .then()
    3. 不知道 $addFields 函数
    4. 没法打断点调试,需要一长串执行完才知道结果
    5. MongoDB 作为也有不短的历史,搜索得到的内容覆盖很多版本,很难直接应用

    希望将来能慢慢克服这些难点。欢迎诸位读者指教,如有问题,也欢迎提出讨论。