<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>CocaColf</title>
        <link>https://kkkf.vercel.app/</link>
        <description>CocaColf Blog</description>
        <lastBuildDate>Tue, 24 Jun 2025 15:10:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>CocaColf</title>
            <url>https://kkkf.vercel.app/favicon.ico</url>
            <link>https://kkkf.vercel.app/</link>
        </image>
        <copyright>MIT License</copyright>
        <atom:link href="https://kkkf.vercel.app/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[时间的圆周运动]]></title>
            <link>https://kkkf.vercel.app/posts/时间不是直线.html</link>
            <guid>https://kkkf.vercel.app/posts/时间不是直线.html</guid>
            <pubDate>Sun, 22 Jun 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>十四五岁的那几年暑假，我在乡下的座机这头，去重庆过暑假的澜姐在电话那头，在商场听我描述给我买运动鞋。鞋子带回来放在老式的木柜子里，一天看好几遍，闻新鞋的味道好几遍，穿在脚上走几步生怕有褶子印，这是暑假最快乐的事情。</p>
<p>有些东西过了那个年纪和阶段，再也弥补不了那种感觉。那个年纪的我喜欢在纸上“设计”球鞋和画各种牌子的标志，当标志改版后的李宁实物从我的纸上出现在我手上时，我又少了一样年少时不可得之物。<br>
​<br>
​下午打开门拿到了快递，是澜姐送的球鞋，意外的惊喜。在阳台试了又试，半晌没有脱下来，给同款球鞋的球友们发去了照片。家里的木柜子已是历史，买双球鞋也是一件简单的事情，但久违的十五岁的那种心情又回来了。</p>
<p>又一个十四五年过去了，时间从来不是一条直线，当新鞋的橡胶鞋底又一次亲吻地面，从小孩到大人的所有夏天围在一起完成了它们的圆周运动。</p>
<img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E6%94%B6%E5%88%B0%E7%90%83%E9%9E%8B.jpg" width = "25%" alt="收到球鞋" align=center />]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[2024]]></title>
            <link>https://kkkf.vercel.app/posts/2024.html</link>
            <guid>https://kkkf.vercel.app/posts/2024.html</guid>
            <pubDate>Tue, 31 Dec 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>上一次写年终总结还是在 <a href="https://cocacolf.vercel.app/blog/2020/">2020</a> 年。12 月有一天我感觉日子以重复居多，回忆过往的时候竟然有很多事情想不起来。也许写年终就像是一个时间刻度的进度条，能够快速拉回一些时间记忆。穿梭在时间的来回，感受以前自己的所思所做。中立客观的去记录就好，至于它是否能够深刻反思自己和计划未来倒不重要。</p>
<h2>健康/锻炼</h2>
<p>上半年做了<a href="https://cocacolf.vercel.app/blog/%E5%85%B3%E7%88%B1%E8%8F%8A%E8%8A%B1/">手术</a>，后面恢复得挺好的，生活少了一番痛苦和烦心的问题。年中体检的结果来看身体也没什么大问题，基本都是这个职业大家都常有的一些毛病。</p>
<p>在工作日晚饭时间爬坡有氧 40 分钟的习惯已经坚持了两年了，它已经成为了一种惯性。也许是这个习惯和适当的饮食控制，让我的体重在一年前从 75 kg 到 65 kg 后，基本在 65 到 68 之间浮动。每年的体型巅峰都在夏天的尾巴，从国庆后到过年之间逐渐变胖。减脂最重要的当然是饮食，但仅从运动层面来谈，单纯靠有氧运动还是差点意思，需要力量训练适当增肌，而我一直以撸铁会影响我投篮的手感为借口基本没有进行。</p>
<p>说说篮球。今年花了很多时间和精力在投篮辅助手的纠正上，对抗十多年的肌肉记忆让我在几个月的时间里投篮都非常不稳定，而带来的收益是投篮姿势更加美观，投篮的稳定性更高。由于手术原因，两个半月没有打过球。到可以开始运动时，在中午午睡时间去公司对面的篮球馆一个人恢复训练，我很享受夏天安静的球馆里一个人练球的感觉。刚恢复训练没多久就遇上小区举办三对三篮球赛，我在这个比赛里打得很好，有一种“一夜成名”的感觉。而在正式一点的各种全场比赛里，我今年遇到了瓶颈，基本都没怎么打好过，比赛的节奏和心理都不对，这让我非常苦恼。我写了打球日记去反思总结每场比赛，但比赛就是无法打出自己满意的表现。有时候连续好几场比赛一个球都投不进，我每次都戏称今天我去打“保零球”。这几年因为晚上下班时间熄灭了太多的打球热情，也没法参加比赛导致和球友不温不火，成为了一个“角落里的球员”。热爱和现实的冲突，无奈。</p>
<h2>工作/职业</h2>
<p>今年还是继续在做 <code>D2C</code> 相关的工作，从博客没有输出内容可以看出来，工作上今年并没有什么亮眼一点的产出和突破，都在做一些业务上的开发和重构工作。</p>
<p>上半年继续做<a href="https://cocacolf.vercel.app/blog/%E8%AE%BE%E8%AE%A1%E7%A8%BF%E8%A7%84%E8%8C%83%E6%A3%80%E8%A7%86%E5%B7%A5%E5%85%B7/">设计稿检视工具</a>，主要是易用性改善、性能提升和更多类型的设计稿检视支持。从工程和既定的能力层面来说，工具做得不错，但是交付给设计部进行内部推广使用后，覆盖面和使用率并不高。一方面是因为能力还是不够，支持的页面类型有限；另一方面是改变一个工种的工作习惯和流程很难；以及这个工具的使用和设计部门今年的 OKR 没有形成足够强的目标互锁。</p>
<p>下半年写了不少 Python 代码，主要是将 AI 部分的一个 Python 模块进行设计和重构，以及给新的 AI 布局预测模型做一些特征和样本处理的工作。为了将这个新的模型接入到 <code>D2C</code> 流程中，一个人完成了从浏览器插件、UI、Server、节点算法模块的流程重构工作。做的事情很多，但都不够系统。<code>D2C</code> 的每个模块环节都能看到我的身影，但这些都是工程层面的东西，在核心的竞争力的模型、图片 CV 能力、样式还原等等环节我并没有成为核心贡献者或者负责人。所以当 <code>D2C</code> 已经成熟，新功能很难挖掘，只能尽可能提高各个维度的准确性的现在，我成了一个有点尴尬的角色，我感觉在来年我很难有长期投入和有价值的内容去做。这是一件很危险的事情。</p>
<p>今年团队不稳定，半数的人主动或被动的离职了，他们中有些人是我认为能力很不错很值得学习的人，在他们 last day 的时候我向他们表达了我的肯定和祝福，也进行了一些交流。有趣的是他们的去向都比较单一，不是在去字节就是在去字节的路上。在公司“降本增效”的现在，每个人都对自己的职业会有担忧和思考，我能感受到留下来的人心里的“鼓点”，而我在这一年里也感觉到自己正处于“界线”上。这三年我晋升了两个大的职级，但前两年我对当前这份工作常常有 <a href="https://zh.wikipedia.org/wiki/%E8%81%B7%E6%A5%AD%E9%81%8E%E5%8B%9E">burnout</a> 的感受，后面逐渐缓解和消失；而现在我常常想去寻求一些“变化”，也许是感兴趣的业务领域方向，也许是 work 和 life 的平衡，也许是人生新的可能性。接下来这一年我会去做一些变化，只是这个变化是“内求”还是“外求”我还没有想清楚，也没有看到机会。我从书中以及<a href="https://www.bilibili.com/video/BV1xq63YUEqY/#reply250574335985">熟悉的人的经历</a> 里，也在收获勇气的力量。</p>
<h2>友情/社交</h2>
<p>工作后基本没交过朋友，我平日的社交圈基本都是老婆的圈子，都是一些相处没什么压力的人。由于婚礼（自己或其他人）和年会的关系，和各种同学朋友见了几次。</p>
<p>年初在深圳和大学室友见面，晚上在海边和城市街道走了几个小时，现在回想起来很多内容都不记得了，但记得那种聊不完的感觉，以及听完一些事情后我对“时间”的感慨——只要花时间去投入、去浇灌，时间会带来奇迹。</p>
<p>婚礼见到了非常多的同学朋友。昨天他们都还在各种城市上班，下班后连夜坐高铁来到这里，赴一场以我们为中心的约，能表达的只有真心感动和万分感谢。只是由于精力的有限，我事后安静下来才想到很多地方没有到位，非常遗憾和对不住。</p>
<h2>个人生活/家庭</h2>
<p>随便写写，想到什么写什么</p>
<ul>
<li>和<a href="https://cocacolf.vercel.app/blog/%E6%9C%89%E8%B6%A3%E7%9A%84%E5%A7%91%E5%A8%98%E7%9C%9F%E7%BE%8E/">她</a>结婚了，备婚完全是我们两个人进行的，很累，也许后面会单独写一篇</li>
<li>父母的体检做了，还算健康；保险买了，但还不是最好的方案</li>
<li>由于以前的电动车她每天骑走了，我坐了几个月的公交。国庆前夕买了辆续航更强动力更足的电动车，我的通勤快乐又回来了</li>
<li>厨艺进步很大，可以在有不少客人的时候做出一桌饭菜的水平了。不过拿手菜的数量还不够多，种类还不够丰富，速度也比较慢</li>
<li>我俩挺喜欢爬山的，爬了一些山</li>
<li>去了顺德、江门、东莞，广东很好吃；昆明、大理的天气很好，饮食不适合我，洱海没什么很大的感觉，但很喜欢苍山</li>
<li>在家搞卫生、整理收纳这些事情很放空，想学习水电维修</li>
<li>看了 19 部电视剧或电影；这几年都不看 NBA 了，只看 CBA，主队辽宁队</li>
<li>老家装了几个监控，监控里看独居奶奶的日常生活，满屏的孤独感很让人难过</li>
</ul>
<h2>学习/知识管理</h2>
<ul>
<li>Kindle 常在手，断断续续看了几本书，阅读的乐趣和习惯在慢慢回来</li>
<li>花了一些时间在深度学习上，知道了一些深度学习的概念和原理，也大概清楚了用深度学习去解决实际问题应该怎么去做。对一年前的我来说，这件事让我对 AI “祛魅”了，以前会觉得这是一件很困难的事情，拖延或者逃避入门，现在跨出了第一步</li>
<li>我使用 <a href="https://obsidian.md/">Obsidian</a> 进行知识管理很多年了，但是一直没有找到适合自己的最佳实践，像 <a href="https://sspai.com/post/78349">PARA</a> 等实践方法最后都失败了</li>
</ul>
<h2>明年的目标和愿望</h2>
<ul>
<li>迎接变化，做出变化，这点还需要花点时间去思考和计划</li>
<li>输入和输出
<ul>
<li>输出，（形式/主题）不设限、不端着，以输出促进输入，同时提高我正在变弱的表达能力</li>
<li>探索适合自己的信息流管理方式</li>
<li>阅读，6 本书</li>
<li>提高英语水平，侧重于听说</li>
<li>学会基础的弹琴，录制一个弹唱的视频</li>
<li>提高拍照水平，记录美和回忆</li>
</ul>
</li>
<li>锻炼
<ul>
<li>减脂，腹部能够看到腹肌的轮廓，我也不知道这是什么样的体脂率</li>
<li>有 3 场以上得分 20+ 的比赛表现</li>
</ul>
</li>
</ul>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[非空穴来风的关爱菊花]]></title>
            <link>https://kkkf.vercel.app/posts/关爱菊花.html</link>
            <guid>https://kkkf.vercel.app/posts/关爱菊花.html</guid>
            <pubDate>Mon, 27 May 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>阶段</h2>
<p>前两年偶尔会感觉菊部难受，倒也没有对生活有多大影响，只是关爱菊花的种子开始种下，装上了智能马桶，囤上了湿厕纸。</p>
<p>在最近一年，菊部形式越加严峻，我的感受分为几个不同的阶段：</p>
<ul>
<li>偶尔会坐着感觉膈应，需要时不时挪动位置</li>
<li>便秘情况增多，上完厕所感觉有一点疼痛，总感觉不能完全擦拭干净，纸上偶尔有血</li>
<li>便秘情况加重，上厕所的过程中会感觉有很强烈的撕裂痛感，上完厕所痛感消失</li>
<li>上厕所的痛感加剧，痛感无法即刻消失。上完厕所后当晚会一直感觉疼痛，肛门处感觉有东西在抽动跳动，涂塞马应龙等药物第二天好转</li>
<li>涂塞药物后第二天甚至几天疼痛也无法消失，影响行走，持续感觉疼痛</li>
</ul>
<p>第二个阶段的时候我有去医院进行检查，医生表示问题还好，开了一些吃的和泡洗药物；最后的这个感受是今年三月份，这一次我又去了医院，检查结果为：纤维瘤、混合痔、肛裂、直肠炎。医生说建议手术治疗。由于看过很多这个手术的痛苦经历，我很犹豫，所以我作了一番思想斗争才决定接受手术治疗，关键决策点在于我的问题是无法自愈的，随着长期摩擦，可能演变为肛瘘，同时纤维瘤存在病变的可能。回到家里洗了个澡，给自己收拾好住院需要的衣物和物品，背着包提着东西，带着一种“视死如归”的心情去了住院部。</p>
<h2>术前</h2>
<p>术前一天晚上，护士拿了两包药，在晚上七点和八点各喝一包，每包药兑1L水，一次性喝完。这个药本质上就是泻药，对我来说并不陌生，在之前肠镜的经历已经体验过，只是这次的味道比之前好喝。也许是我有便秘的问题，这个泻药我都觉得没啥很大的作用，吃完第一包的时候我甚至没有任何上厕所的感觉；第二包后有所感觉但是排不出来。后面蹦跳了一会儿后开始发作。</p>
<p>主治医生在晚上找我谈话，介绍手术的方案供选择，以及签署一些必要的文件。我选择的是传统的外剥内扎方案。简单来说就是肛门外面的东西是切除，同时切开部分括约肌；肛门里面的东西是用“绳子”绑起来，让其血流不通最后坏死脱落，然后生长新的组织。</p>
<p>手术的当晚七点后不再进食进水。这是我第一次住院，当晚一直睡不着，第二天早上六点被护士叫醒，到换药室给我刮了肛毛（方便手术），同时进行灌肠（将里面彻底弄干净）。</p>
<h2>术中</h2>
<p>我的手术原定于上午十一点，结果推到了下午两点多，一直没有喝水，内心也开始有点焦躁，如同盼着春雨一样盼望早点手术。手术车过来接我，推进了手术室，当我躺在手术室的床上的时候我明白了为什么书里都写冰冷的手术室，真的很冷。</p>
<p>我躺在手术床上，护士给我盖上了一床大被子，医生开始给我打麻醉。麻醉医生让我把身体弓成一个虾状，然后用很长的针从后背向椎管打入，这个针刺入骨头的时候感觉很痛很胀。我的裤子被脱掉，像块肉一样的被医生们搬动，侧躺着听着医生们在聊山姆的椰子水和麦德龙的椰子水哪个更好喝，完全不知道此时已经在动手术了。手术医生还在给我开玩笑，问我有没有“男朋友”。进出手术室的时间一个多小时，真正动刀子的时间可能就20分钟。切完后医生将切下来的肉块放在托盘里拿给我看，也拿出去给手术室外的妈妈看了，我妈还拍照留念。</p>
<p>护士们把我推回病房，搬到床上的过程不小心把文件夹摔在了我的眉骨位置，流了血，护士非常抱歉，我表示问题不大。麻药要六个小时才会消失，我的下半身完全没有知觉，我就一点一点的感受着下半身的感觉恢复，晚上十点多终于开始可以抬腿，24小时没有进水进食的我也终于可以吃东西了，但是只能喝点白粥。麻药退去后一定要排尿，但由于麻药的作用，排尿是比较困难的，如果排不出来会上尿管，那又是另一个痛苦。我在厕所站了十来分钟才作业成功。</p>
<p>手术的外表面的切口是不缝的，也就是说切开的伤口完全是开放式的，可以直接看到肉，触目惊心。因为这个部位比较特殊，如果缝了反而更容易感染。我的切口有三个，围绕着肛口呈现一个风车状。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E6%89%8B%E6%9C%AF%E5%88%87%E5%8F%A3.jpg" alt="手术切口"></p>
<h2>术后</h2>
<p>手术的第一天就很难熬，不是因为切口疼痛，而是手术后屁股里塞了一些纱布，导致肚子里胀气非常严重，总感觉要大便。肛门非常高频的坠胀感，让我后半夜开始便无法入睡。我按铃叫护士，护士忍俊不禁，说你都排空了也没吃什么东西，肯定是没厕所要上的，是纱布导致胀气。这种难受的感觉让人完全说不出话来，一直到上午十点多纱布取出才结束痛苦。</p>
<p>术后前两天都是吃流食或者粉面等易消化食物，医生会开一些促便和软便的药物，术后差不多一个月都要喝，一方面是缓解便疼；另一方面是培养规律排便的习惯。第三天需要第一次排便，而我这一天没有什么感觉，最后医生给我弄了一管开塞露才显灵。术后第一次排便痛到怀疑人生，我感觉像是在拉仙人掌，一边有东西扎着一边有东西割着，我的腿都在颤抖，额头一直冒汗。擦屁股的时候完全不敢用力，同时由于手术完肛门外一直是水肿状态，所以那种触感很奇怪，我完全不清楚擦拭的是外部还是靠里一点的位置。每次上完厕所需要坐浴泡药，以防伤口感染。</p>
<p>住院的一周里日子每天大同小异，早上六点多被护士叫醒，脱下裤子用一个激光仪器照半小时屁股，主要用途是消水肿；每天都要吊很多水；医生每天都很关心你是否排便，每次的排便都要拍照发给医生看，医生每天上班后来换药。换药是很痛的，很多人受不了，但我有点“变态”，每次换药都闭着眼睛去感受那种医生用镊子夹掉腐肉的痛，管子插入的痛，医生掰开伤口的痛，药膏涂抹的痛。我认为”去感受“本质上就是一种直面的对抗。他们有些人一盒止痛药都不够，而我只吃了一片。住院的一周除了晚上老婆会来陪我一会儿，我没有让人陪护。白天我什么都不去想，只关注当下的感受，没有任何烦恼和忧愁，无所事事的躺在病床上一晃一下午就过去了。只是到后面两天感觉自己除了疼痛和行动缓慢外其他感受都还行，就想回家了。一周出院，那天朋友开车来接我，我提着坐浴盆走出住院部的时候，阳光洒在身上，让我想起了肖申克的救赎——阳光洒在身上，犹如自由人。</p>
<p>在家需要尽量少动，当然伤口的疼痛也会阻挡你想行走的心。依然要坚持每天吃药、坐浴、排便、疼痛，每两天需要去医院换药一次。我唯一的出门机会就是去换药，打个车开门后往后座一躺，留下的士师傅诧异的眼光。拉仙人掌的日子需要过半个多月，每天的多次排便都要做一番心理准备。两周后开始可以慢慢走动较远距离，我去上班了，无法久坐，高频的站起来活动一下，还要尽可能避免出汗。依然是坚持两天去医院大换药和检查一次，医生会检查恢复情况，同时看是否存在术后肛门狭窄的问题（有的话需要扩肛），平日里自己每天涂药两次。这个期间由于排便和流汗，伤口会反复的长好和裂开，不止是身体上的疼痛，也是心理上的折磨。在术后半个月左右，医生会用肛门镜去查看里面扎着的东西是否脱落以及长好，这是一个里程碑。</p>
<p>往后的日子一天比一天好起来，伤口长得很慢，脆弱的伤口在裂开和长好之间反复，在倒数第二次的时候，医生给我将长好的肉剪开了，说这样弄一次会长得更好，之前长的方向不太对。给我剪肉的时候没有麻药，可以想象一下那种感觉。恢复的时间因人而异，我一直到五一结束后才在医生的允诺下开始可以慢跑运动，以后不用去换药了，我对医生开玩笑说我毕业了。不过伤口处还不算完全长好，总感觉有点异物感，大概是疤痕增生。现在我又重新拍打起了篮球。每天规律和顺畅的排便，让我感受到幸福感，是的，顺畅而又规律的排便是人生的一大幸事。</p>
<h2>原因</h2>
<p>关爱菊花这句话不知道从何时开始的，绝不是空穴来风，难言之隐，苦痛自知。手术也并不是一劳永逸，这玩意儿还可能再生，所以需要仔细想想原因，不过这个原因是我主观原因，不构成科学解释。</p>
<p>排除人体本身体质问题，一般是因为吃辣重口饮食，熬夜酗酒，长期久坐，大便努挣等等。至于我个人，病情快速发展是近一年的事情，这一年我不怎么吃辣，很少喝酒，我思考了一番最后得出一个结论，让我迅速发展的出发原因在于减肥，一个很诧异的结论。我这一年期间减重20斤，饮食中摄入的碳水和油脂是不够的，蛋白质偏多，这种饮食会出现便秘问题，我有问过健身的同事，他们减脂期间是会吃一些润便的药物。当然我的便秘一方面是饮食，另一方面也是纤维瘤或痔疮有阻挡。而我在出现便秘时，不时会努挣，直到某一次或者某几次叠加使得出现了肛裂，使得疼痛感加剧不得不进行手术治疗。</p>
<p>所以我认为预防的关键点在于：维持良好的、规律的排便和饮食习惯。偶尔便秘时可以寻求药物辅助，一定不要努挣。</p>
<h2>感谢</h2>
<p>感谢我的主治医生，非常年轻的一个医生。他手法娴熟，说话耐心，每个东西都会和我讲为什么和怎么做，和我解释原理。出院后比我自己还关心我，每天问是否排便，要便便照片，约换药时间。下次还......算了，祝我从此舒舒坦坦，健康快乐！</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E6%84%9F%E8%B0%A2%E5%8C%BB%E7%94%9F.jpg" alt="锦旗"></p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[有人说奶奶发胖了]]></title>
            <link>https://kkkf.vercel.app/posts/有人说奶奶发胖了.html</link>
            <guid>https://kkkf.vercel.app/posts/有人说奶奶发胖了.html</guid>
            <pubDate>Thu, 29 Feb 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>中午在理发时奶奶打来了电话没有接，后面回了过去。其实也没什么事，她就是告诉我周日是小姑爷生日，看有没有时间过去，毕竟娘家人这边去一个代表也是一件高兴的事。说着自然就说开去，说到今天提了一箱牛奶去一位老人家，原因是去年奶奶八十大寿时她来了，送了 400 块钱礼钱（普通关系，情义很重）。那位老人家说奶奶发胖了，说者无意听者有心，奶奶在电话里说到这件事开始哭了。她说爷爷去世那年也是有人说他发胖了，结果四十多天后他去世了（这里的发胖我猜应该是生病的浮肿）。奶奶哭着说她要是死了更好，我们后人更好过，她总觉得老了不中用，对后人是一种负担。我以前听到她这么说会生气会难过，但现在已经免疫了。奶奶对于死亡这件事是害怕的，本质上是舍不得离开，她时常感慨现在日子好过，吃喝不愁，后人争气，她八十多岁了也在争气的活着，一人住在乡下，把家里收拾得干净整洁，喂了很多鸡。</p>
<p>感谢妈妈的行动力，一鼓作气给乡下装好了网络和好几个摄像头，我们得以可以在摄像头里每天看到奶奶的部分活动和状态。她也似乎是希望和我们每天打照面的，开始天天在外面地坪里端着碗吃饭。</p>
<p>尽管平日里会嫌弃她的反复唠叨，过分关心所有后人的事情，但是不得不承认需要珍惜她还能够明白世事、还有精力想这想那的时光。她常说我孝顺，但与小姑他们相比，我自认为是完全说不上孝顺的，孝顺不是给钱和打电话关心。</p>
<p>希望她老人家健康！</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[开发了设计稿规范检视工具]]></title>
            <link>https://kkkf.vercel.app/posts/设计稿规范检视工具.html</link>
            <guid>https://kkkf.vercel.app/posts/设计稿规范检视工具.html</guid>
            <pubDate>Tue, 26 Dec 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1>背景</h1>
<p>规范的设计稿对 D2C（设计稿生成代码）是非常重要的，如果存在许多层级错乱的节点、冗余的节点、交叉的节点，那么势必在 D2C 开发的过程中需要处理许多的异常节点的处理和计算，有很多异常场景很难通过代码的手段去解决，从而很难生成正确的高可用性的前端代码。因此 D2C 的实践过程中需要提炼出许多 D2C 的设计规范让设计师配合。</p>
<p>同时，设计团队也有自己的设计规范，在设计师的日常设计中需要进行设计检视，类似于我们开发人员的 code review。间距、颜色等一一核对的检视是枯燥的。</p>
<p>如果有个工具可以让设计师在设计自检或设计评审时一键检视存在的设计问题，那么无论对于设计稿的质量或者是设计效率都有很大的帮助。设计稿检视工具的需求应运而生。它就像是设计稿的 ESLint。</p>
<p>很显然，由于两种不同角色对规范的不同要求，因此设计稿检视工具的规则会分为 D2C 规则和设计规则两种类型。</p>
<h1>整体效果</h1>
<p><strong>选中设计稿，使用设计稿检视插件一键检测</strong></p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E6%A3%80%E8%A7%86%E6%8F%92%E4%BB%B6.png" alt="插件"></p>
<p><strong>检测完成自动打开检测报告</strong></p>
<p>检测报告以画板的形式展示（和设计软件一致的操作方式），在这里可以进行问题查看、问题忽略、问题过滤等操作，同一个设计稿被忽略的问题在下一次检视时将不会提示：</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E6%A3%80%E8%A7%86%E7%BB%93%E6%9E%9C.png" alt="检测结果"></p>
<p><strong>历史记录和数据汇总</strong></p>
<p>所有的检视记录会按照「同一个版本」，「同一个设计稿」的维度进行聚合，同时会对这些记录进行数据分析和汇总：</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95.png" alt="历史记录"></p>
<h1>实现：整体流程</h1>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E8%A7%84%E8%8C%83%E6%A3%80%E6%B5%8B%E5%B7%A5%E5%85%B7%E6%95%B4%E4%BD%93.png" alt="整体流程"></p>
<p><strong>插件端</strong></p>
<p>用户在 Mastergo 设计稿页面上选中需要检视的设计稿后点击插件开始检测，插件会对设计稿进行节点提取（节点树）、图片导出等设计稿数据处理，将数据发送给服务端</p>
<p><strong>服务端</strong></p>
<p>服务端收到检视任务后，主要做几件事：</p>
<ul>
<li>对上报的设计稿数据做进一步必要的处理，比如将插件上报的图片 (base 64)转为静态资源存储，用于 AI 识别</li>
<li>将检视任务加入任务队列中，在任务队列中会对设计稿进行 AI 组件识别，得到组件识别结果。将这个和原始设计稿信息一起提供给「规范检测库」进行检视，获得检视结果</li>
<li>搜索相同的设计稿的历史记录，将这个设计稿以前处置（如某个问题被忽略）延续到本次检视操作；同时将相同的设计稿的检视结果进行合并</li>
</ul>
<p>组件识别结果：</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E8%A7%84%E8%8C%83%E6%A3%80%E6%B5%8B%E7%BB%84%E4%BB%B6%E8%AF%86%E5%88%AB%E7%BB%93%E6%9E%9C.png" alt="组件识别结果"></p>
<p><strong>UI 平台</strong></p>
<p>提供报告展示、问题处置、历史记录、管理员操作等功能。</p>
<p>整体流程就是普通的 web 开发，毫无疑问最核心的功能和能力在于「规范检测库」。</p>
<h1>实现：规范检测库</h1>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E8%A7%84%E8%8C%83%E6%A3%80%E6%B5%8B%E5%BA%93%E6%B5%81%E7%A8%8B.png" alt="规范检测库"></p>
<h3>输入数据</h3>
<p>我们需要检测的对象按照场景划分为两种：设计稿原始节点类型和具体的场景组件。</p>
<p><strong>设计稿原始节点类型</strong></p>
<p>比如校验「TEXT 节点」的最小字号、「TEXT 节点」之间不能存在交叉、「子节点」大小不能超过「父容器」的大小。</p>
<p><strong>具体的场景组件</strong></p>
<p>表单、表格等具体场景我们是无法从设计稿原始节点中判断出来的，这也是服务端为什么对设计稿进行 AI 组件识别的原因。通过 AI 识别我们知道具体的（场景）组件的位置、大小。</p>
<p>因此「规范检测库」的输入就是设计稿的原始节点（树）和设计稿组件识别结果。</p>
<h3>上下文环境</h3>
<p>有些规则需要计算节点之间的关联关系，因此单个规则的输入不仅仅是当前节点。我们可以抽象一个 context 注入到每个规则中去，校验过程中所有规则共享。这种 context 的思想在各种库中非常常见。</p>
<p>校验过程中需要不断地收集每个规则的校验结果，我们可以在 context 中增加一个 event emitter，规则将问题”广播出去“，然后统一由一个收集者负责接收广播，收集问题。</p>
<h3>策略模式</h3>
<p>如果我们制定了文本规范，那么则会对文本节点进行对应的规则检视。方式则是对输入的数据进行遍历，遇到类型为 TEXT 的节点则进行规则检视；对于表单规范也是同理。用伪代码表示这个过程：</p>
<pre><code class="language-js">traveAndValidate (tree, node =&gt; {
	if (node.type === 'TEXT') {
		checkRule1();
		checkRule2();
	}
	if (node.type === 'xxxx') {  // ...}

	// ...
});
</code></pre>
<p>这样的方式显然是不行的，<code>if-else</code> 多、规则可维护性差、复用性差。这样的场景其实在表单的校验中也很常见，不同的表单有不同的校验规则。对于这种问题策略模式就要派上用场了，下面是使用策略模式制定一个校验器。</p>
<pre><code class="language-js">// 一条规则的定义
interface Rule {
	name: string;  // 规则名
    description: string;  // 规则描述
    level: 'warn' | 'error' | 'info';  // 报错级别
    category: ' D2C' | 'design'; // 类别
    checker: Function;  // 规则实现
}
class ComponentValidator {
    checkerMap: {[nodeType: string]: string[]}  = {};  // 每个组件绑定的规则
    strategies: Record&lt;string, Rule&gt;;  // 策略

    constructor (strategies: Record&lt;string, SubRule&gt;) {
        this.strategies = strategies;
    }

    // 注册不同类型组件的校验规则
    add (component: string, rules: string[]) {
        this.checkerMap[component] = rules;
    }

	// 遍历节点树，对节点进行规则检查
    traveAndValidate (tree: Tree&lt;Node&gt;, context: LintContext) {
        const component = tree.component;

        if (this.checkerMap[component]) {
            for (let componentRule of (this.checkerMap[component] || [])) {
                this.strategies[componentRule].checker(tree, context);
            }
        }

        for (const childComponent of (tree.children || [])) {
            this.traveAndValidate(childComponent, context);
        }
    }

    start (context: LintContext) {
        this.traveAndValidate(context.componentTree, context);
    }
}
</code></pre>
<p>当有了这个校验器后，我们就可以很方便的维护规则了。只需要关注规则的逻辑实现，如果某个组件需要使用某条规则，只需要它需要绑定的规则数组里增加规则名称即可。</p>
<pre><code class="language-js">// 先制定规则，以及将组件和对应的规则进行映射
const rules = {
	ruleA: {
		name: 'ruleA',
	    description: 'ruleA description',
	    level: 'warn',
	    category: 'design',
	    checker (node, context) {  // do something }
	},
};
const registerMap = {
	form: ['ruleA', 'ruleB'],
	table: ['ruleC'],
	select: ['ruleD']
};

// 检查所有组件是否符合规范
function componentChecker (context: LintContext) {
	const validator = new ComponentValidator(rules);
	Object.entries(registerMap).forEach(([type, rule]) =&gt; {
		validator.add(type, rule);
	});
	validator.start(context);
}
</code></pre>
<h3>规范实现</h3>
<p>规范实现本质上就是按照规则要求进行各种节点之间的关联计算或者是节点的属性检查，在上面 <code>Rule</code> 的定义中可以看到它本质上就是 <code>checker</code> 函数的实现。因为最终问题都是在规则内部使用事件广播的方式由问题收集器统一收集处理，所以对于每个规则内部的实现可自由发挥，只需要最终报告的问题结果的数据结构保持统一。</p>
<p>规范检测工具的核心在规范检测库，而规范检测库的核心在规范实现。有些规范的实现一点也不简单。比如对于表单，要检查 label 之间是否左对齐、要检查表单的单个一行的各个元素之间的间距，需要提取所有的 label 成组，单行成组等等，要把表单不断地横着分割，竖着切割。横看成岭侧成峰，远近高低各不同。</p>
<p>此外，这些规则形成的规则库是以插件的形式在「规范检测库」加载的，所以后续不同的设计规范只需关注规则的具体实现即可。</p>
<h1>其他</h1>
<p>本节为工具的开发或者落地过程中的一些零碎的问题。</p>
<h2>和设计师沟通</h2>
<p>由于使用者是设计师，所以实现工具的过程中需要和设计师进行大量的沟通工作。包括：</p>
<ul>
<li>工具的交互实现</li>
<li>设计规范的提取</li>
<li>工具的宣贯和推广</li>
</ul>
<p>因此需要在设计团队找到一个设计负责人来负责工具的工作。工具的交互实现倒是和平时开发业务差不多，但规范的提取就不那么容易了。因为设计规范虽然看起来是有明文的条条框框的，但是很零碎，要转为工具实现的代码逻辑还有很多的精简、规则定义、报错文案等工作。</p>
<h2>同一个设计稿如何判断</h2>
<p>无论是下面涉及的「问题忽略」功能，还是「同个设计稿的记录合并」等，都需要能够判断两张设计稿是不是同一个设计稿。</p>
<p>一个设计稿有两种可能：</p>
<ul>
<li>整个设计稿中各个节点编组为一个组合</li>
<li>设计稿未被编组，是零碎的节点</li>
</ul>
<p>第一种场景下，插件上报的设计稿信息中有唯一的 id，所以在 server 我们可以通过这个 id 进行查询即可找到相同设计稿的历史检视记录。</p>
<p>第二种场景下，没有唯一的 id 存在，那么如何判断设计稿是同一张呢？经过思考，这种情况下一张设计稿本质上是由大量的节点（每个节点有唯一 id）组成的，所以判断是否是同一个设计稿只能通过这个数组来判断，所以我们通过节点 id 数组的相似度（交并比）来判断是否是同一张设计稿。</p>
<pre><code class="language-js">/**
 * 使用数组的交并比来判断相似度
 * @param ids 当前被对比的设计稿节点的id
 * @param standardIds 对比的基准
 */
export const isSameDraftByNodeSimilarity= (ids: string[], standardIds: string[]) =&gt; {
    const iouThread = 0.9;

    const intersection = ids.filter(id =&gt; standardIds.includes(id));
    const union = new Set([...ids, ...standardIds]);
    if (union.size === 0) return false;

    const iou = (intersection.length) / union.size;

    return iou &gt; iouThread;
};
</code></pre>
<p>如果这两种情况下都没有找到相同的设计稿，那么就认定为新设计稿。很显然这种策略从逻辑上来看存在一些的问题。举个例子，一个设计稿还未完成时（少量节点）检视和最终完成时检视由于节点的增删可能就会被判断为不同设计稿。不过从现实使用工具的时间来看（最终设计完成准备评审），这个策略是满足要求的。</p>
<h2>忽略问题的实现</h2>
<p>规则的实现存在 bug 或者 AI 识别存在不准等都可能导致检查出现误报，因此在报告展示界面提供了「问题忽略」操作，本次设计稿检视忽略的问题在下一次检视时将不再提示。</p>
<p>每一个被报告的问题都会生成一个「问题id」，所以问题忽略就是将被忽略的「问题id」从结果中过滤即可。由于同一个设计稿的判断的问题解决了，那么只要保证同一个节点的同一个问题的「问题id」每次检视都是唯一的即可。那么这个「问题id」的生成是否可以使用<code>[节点 id]-[问题类型]-[问题报错文本]</code>呢？</p>
<p>理论上同一个节点在每次检查时这些值确实应该是不变的，但是在我的场景中存在一点问题：同一个节点的 id 不一定保持不变。在设计稿中，每个节点都有一个唯一 id，所以设计稿节点中的节点 id 是不变的。但是 AI 识别组件时，每次生成的组件节点都会生成一个不同的 id，所以这导致同一个设计稿在每次 AI 识别时，同一个组件的 id 是不一致的。所以对于组件，必须想一个不变量来替换「节点id」，我选择使用 <code>x-y-width-height</code> 来替代「节点id」，因为理论上只要 AI 识别稳定，同一个设计稿每次同一个区域的识别结果应当是一致的。</p>
<p>最终使用<code>md5</code>对这些信息进行编码，从而达到同一个节点的同一个问题每次检视都是相同的 id 的目的。</p>
<pre><code class="language-js">function genIssueId (
    node: NodeInfo,
    category: keyof typeof IssueCategory,
    msg: string,
) {
    const {x, y, width, height} = node;
    let nodeMark = node.id;
    let issueInfo = `${category}-${msg}`;

    // 因为这两种节点每次的id并不是固定的，所以用区域来标识节点
    // 会存在误判，不过可以接受，因为区域变化说明设计稿有改动，那这一块的校验内容也要变化，可以理解为产生了新的问题
    if (/@AI/.test(node.id)) {
        nodeMark = `${x}-${y}-${width}-${height}`;
    }

    return md5(`${nodeMark}-${issueInfo}`);
}
</code></pre>
<h2>规则下架</h2>
<p>如果某个规则上线后有严重的问题，可能导致每次检视都有很多误报，所以需要有个规则下架的机制。这个实现也很简单：</p>
<ul>
<li>「规范检测库」对外提供接口获取所有规则名，管理员可以对这些规则进行下架操作</li>
<li>任务队列中执行规范检测时，获取被下架的规则传入「规范检测库」，在注册规则之前将这些下架规则忽略即可。</li>
</ul>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E8%A7%84%E5%88%99%E4%B8%8B%E6%9E%B6.png" alt="规则下架"></p>
<h1>碎语</h1>
<p>这个工具包含了插件开发、服务端开发、规范检测库的实现、UI 平台开发，一个人独立开发了两个月时间，工作量大时间紧。我将它拆分为三个迭代，每个拆分的小迭代都顺利的按时完成。经过演示、内测、宣贯，设计团队在这个月开始使用了。和高频使用的设计师进行沟通时，得到的都是提质提效的肯定反馈，还是很欣慰的。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[视觉稿中的静态图片识别]]></title>
            <link>https://kkkf.vercel.app/posts/视觉稿中的静态图片识别.html</link>
            <guid>https://kkkf.vercel.app/posts/视觉稿中的静态图片识别.html</guid>
            <pubDate>Wed, 24 May 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>背景</h2>
<p>网页上的静态图片从代码上来说，是如 <code>&lt;img src=&quot;xxx&quot; /&gt;</code> 这样的代码。因此在设计稿转代码时，需要知道设计稿里哪个地方是静态图片，同时将这个静态图片导出后生成对应的链接地址，这样生成的代码才能完整的运行起来看到页面效果。</p>
<p>我司设计师使用的设计工具是 <a href="https://mastergo.com/">MasterGo</a>。和其他设计工具无异，通过各种类型的节点裁切组合来创造图形。组合方式五花八门，设计师的绘图方式也可以千奇百怪，所以从直观感受来说一个静态图片内部节点的组成情况是非常复杂的。所以这个问题本质上是给你一个设计稿页面节点数据，判断哪些节点组合起来是静态图片。</p>
<p>下面是 MasterGo 节点的类型和一个静态图片的节点组成的部分片段。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/mastergo_node_types.png" alt="节点类型"></p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/%E8%8A%82%E7%82%B9%E7%BB%84%E6%88%90%E7%A4%BA%E4%BE%8B.png" alt="节点组成示例"></p>
<h2>思考&amp;过程</h2>
<p>众多的组合方式和绘图方法，意味着输入不固定、可能性无穷多，这种不确定性很大的问题仅通过编码手段来解决是否不太可行性呢？答案是否定的。第一，这种不确定性只是自己的感觉而已，有待证明；第二，如果有个解法可以覆盖其中的绝大多数场景，那么这个解决方案也是非常够用的；第三，可以添加限制条件，使得输入减少，往确定性高的方向靠拢，解决频率最高的场景。所以解决这个问题应当先立下要求（限定），再进行归纳，再提出解法。</p>
<p>根据我的场景，我提出了下面两个条件或要求：</p>
<ol>
<li>静态图片里不含有文本节点。因为文本一般要做国际化，一般由开发人员编码</li>
<li>由于方案不一定完全准确，为了避免误判造成出码不准确和导出太多的静态文件造成困扰，导出静态图片以「宁可少导出不要误导出」为优先级原则</li>
</ol>
<p>接下来我翻看大量的设计稿，主要看其中静态图片的节点组合方式，我逐渐发现了规律。</p>
<p><strong>节点组成</strong></p>
<ol>
<li>图片由众多节点组成，节点之间必然会分组聚合，这些同组节点的父节点一般是：GROUP(组节点)、BOOLEAN_OPERATION（一种逻辑节点）、FRAME（容器节点）。</li>
<li>在设计（绘制）图片时，只会使用 PEN_NODE（路径节点，即各种自由绘制的路径，如曲线）、基础图形（方形、圆形等）、BOOLEAN_OPERATION、其他绘图辅助节点</li>
</ol>
<p><strong>图形类型</strong></p>
<ul>
<li>
<p>简单图形</p>
<ul>
<li>仅使用 PEN_NODE 或基础图形绘制的</li>
<li>FRAME 或 RECTANGLE 等基础图形作为一个外框，实际的内容是使用一张图片填充进去的</li>
</ul>
</li>
<li>
<p>复合图形</p>
<ul>
<li>各种节点以 GROUP 等分组聚合，组和组之间嵌套</li>
</ul>
</li>
</ul>
<p>下面是一些实际的示例。</p>
<h3>单个 GROUP 节点</h3>
<p>根节点下只存在 PEN_NODE、基础图形、BOOLEAN_OPERATION 节点。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/%E5%8D%95%E4%B8%AAgroup%E6%A0%91.png" alt="单个group树"></p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/%E5%8D%95%E4%B8%AAgroup%E7%A4%BA%E4%BE%8B.png" alt="单个group树示例"></p>
<h3>GROUP 嵌套 GROUP</h3>
<p><strong>GROUP 下含有文本节点和 GROUP 节点</strong></p>
<p>因为已经限定视觉切图是不含有文字的（国际化需要），所以这种情况，最外层的 GROUP 应该被取消认定为静态图片，内层 GROUP 取代认定。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/group%E4%B8%8B%E5%AD%98%E5%9C%A8%E6%96%87%E6%9C%AC.png" alt="group下存在文本"></p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/group%E4%B8%8B%E5%AD%98%E5%9C%A8%E6%96%87%E6%9C%AC%E7%A4%BA%E4%BE%8B.png" alt="group下存在文本示例"></p>
<p><strong>GROUP 节点嵌套，共同组合为一个图片</strong></p>
<p>多个 GROUP 组合为一张图，每个 GROUP 下只含有 PEN_NODE、基础图形、BOOLEAN_OPERATION 节点。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/group%E5%B5%8C%E5%A5%97.png" alt="group下节点嵌套"></p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/group%E5%B5%8C%E5%A5%97%E7%A4%BA%E4%BE%8B.png" alt="group下节点嵌套示例"></p>
<p><strong>GROUP 节点嵌套，含有多张图形</strong></p>
<p>GROUP 下有多个 GROUP 嵌套，每个 GROUP 各为一张图。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/group%E5%B5%8C%E5%A5%97%E5%A4%9A%E5%BC%A0%E5%9B%BE.png" alt="group下多张图"></p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/mastergo_static_image/group%E5%B5%8C%E5%A5%97%E5%A4%9A%E5%BC%A0%E5%9B%BE%E7%A4%BA%E4%BE%8B.png" alt="group下多张图示例"></p>
<h2>解决方案</h2>
<p>通过归纳，可以判定一个静态图片从节点的组成上是有共性的：</p>
<ul>
<li>以 GROUP、BOOLEAN_OPERATION、FRAME 作为父（根）节点</li>
<li>子节点为 PEN_NODE、基础图形、BOOLEAN_OPERATION、GROUP 等节点</li>
</ul>
<p>从数据结构来说，整个设计稿可以抽象为一颗多叉树，所以判断设计稿中的静态图片，<strong>本质上可以抽象为从一颗树中寻找满足条件的所有子树</strong>。</p>
<pre><code class="language-js">const findStaticImgNode = node =&gt; {
    if (node.isVisible === false) return [];
    if (groupPossibleStaticImg(node)) {
        return [{
            id: node.id,
            name: node.name,
        }];
    }

    let collections = [];
    for (let childNode of (node.children || [])) {
        if (!['GROUP', 'BOOLEAN_OPERATION', 'FRAME'].includes(childNode.type)) continue;

        const res = findStaticImgNode(childNode);
        res.length &amp;&amp; collections.push(...res);
    }

    return collections;
};

const groupPossibleStaticImg = node =&gt; {
    if (!imgNodeAllowType(node.type)) return false;
    if (['GROUP', 'BOOLEAN_OPERATION', 'PEN', '各种基础图形'].includes(node.type)) return false;

    return (node.children || []).reduce((valid, curNode) =&gt; {
        return valid &amp;&amp; groupPossibleStaticImg(curNode);
    }, true);
};
</code></pre>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[基于 Fabric.js 开发图形编辑器]]></title>
            <link>https://kkkf.vercel.app/posts/基于Fabric.js开发图形编辑器.html</link>
            <guid>https://kkkf.vercel.app/posts/基于Fabric.js开发图形编辑器.html</guid>
            <pubDate>Tue, 04 Apr 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>前段时间实现了一个简单的图形编辑器，它是 AI 识别视觉稿生成代码的中间一环，用于手动调整视觉稿中 AI 识别不到位的组件，同时也可以给识别到的组件进行相关配置让生成的代码更加完整。此前我对 Canvas 只是略知皮毛，对于编辑器总有一种这个需求不好做的印象。通过技术选型，我选择用 Fabric.js 来实现我的需求，不得不说 Fabric.js 非常适合简单编辑器的开发。本文忽略 Fabric.js 前置知识，主要关注我在使用 Fabric.js 开发出一个麻雀虽小五脏俱全的图形编辑器中遇到的一些问题。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/fabric/%E7%BC%96%E8%BE%91%E5%99%A8%E6%95%B4%E4%BD%93.png" alt="编辑器整体"></p>
<h3>编辑器功能</h3>
<p>从图片可以看出来，编辑器渲染的主要内容是一张设计稿图片，图片中每一个识别出的组件由一个含有左上角文本名称的矩形绘制于上。这个编辑器主要功能如下：</p>
<ul>
<li>
<p><strong>画布</strong></p>
<ul>
<li>图形渲染</li>
<li>画布内容自适应画布大小</li>
<li>缩放：Ctrl + 滚轮</li>
<li>画布移动：SPACE + 鼠标</li>
<li>右键菜单</li>
<li>顶部工具栏：选择模式、鼠标绘制组件、撤销、重做、拖拽、帮助中心</li>
<li>快捷键操作</li>
<li>历史记录</li>
</ul>
</li>
<li>
<p><strong>图形</strong></p>
<ul>
<li>拖拽移动</li>
<li>拉伸缩放</li>
<li>节点删除</li>
<li>节点变化信息和右侧面板联动</li>
<li>区域限制：节点的缩放、拖拽、绘制操作限制在底部图片区域内</li>
<li>节点穿透选中</li>
</ul>
</li>
<li>
<p><strong>右侧面板</strong></p>
<ul>
<li>节点的操作和右侧面板可以双向联动</li>
</ul>
</li>
</ul>
<p>下面主要记录开发过程中遇到的主要的问题和解决方式。</p>
<h3>自定义图形</h3>
<p>Fabric.js 提供了 <a href="http://fabricjs.com/fabric-intro-part-3#subclassing">subclassing</a> 扩展基本图形，比如矩形加文本就可以通过扩展矩形实现：</p>
<pre><code class="language-js">fabric.ComponentRect = fabric.util.createClass(fabric.Rect, {
	type: 'component-rect',
	initialize: function (initializeOptions: fabric.IObjectOptions) {
        const options = { ...initializeOptions };

        this.callSuper('initialize', options);

        // 取消旋转功能
        this.setControlsVisibility({ mtr: false });

        // 设置需要的整体样式
        this.set({
            id: options.id || '',
            // ...属性设置
        });
    },

    toObject: function () {
        return fabric.util.object.extend(this.callSuper('toObject'), {
            id: this.get('id'),
        });
    },

    _render: function (ctx: CanvasRenderingContext2D) {
        this.callSuper('_render', ctx);

        ctx.font = `${LABEL_FONT_SIZE}px Helvetica`;
        ctx.fillStyle = this.labelColor;
        ctx.fillText(this.label, -this.width / 2, -this.height / 2);
    }
});
</code></pre>
<p>我们扩展 Rect 类创建了 ComponentRect 这个图形，所以我们只需要通过 <code>new fabric.ComponentRect({})</code> 即可创建一个组件矩形。但是真实情况并非那么顺利，我们会遇到几个问题。</p>
<p><strong>问题 1：无法获得自定义属性</strong></p>
<p>可以通过 <code>fabric.Canvas</code>的 <code>toObject</code>方法获得所有节点的数据，但是发现缺少通过节点的 <code>set</code> 方法设置的自定义属性。对于自定义的属性，我们需要全部在节点的 <code>toObject</code> 方法内进行返回，如代码中 id 这个值所示。</p>
<p><strong>问题 2：loadFromJson 时，自定义图形无法绘制</strong></p>
<p><code>loadFromJson</code>是 <code>fabric.Canvas</code>的方法，给定一个 JSON 数据即可绘制画布内容，在历史记录功能中很有用。在实操中，可能会遇到下面几个与自定义图形相关的问题：</p>
<ul>
<li>Cannot read properties of undefined (reading 'fromObject')</li>
<li>自定义图形无法渲染</li>
</ul>
<p>这些问题主要原因是在渲染时，fabric 不认识 ComponentRect 这个图形。解决方式为：</p>
<ul>
<li>subClassing 使用 fabric 成员变量的方式定义。也就是说不能使用 <code>const ComponentRect = fabric.util.createClass(fabric.Rect, {})</code>来定义自定义图形，必须使用 <code>fabric.ComponentRect = ...</code>的方式</li>
<li>扩展定义 fromObject 方法</li>
</ul>
<pre><code class="language-js">fabric.ComponentRect.fromObject = function (object, callback) {
	return fabric.Object._fromObject('ComponentRect', object, callback);
};
</code></pre>
<h3>光标绘制图形</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/fabric/%E5%85%89%E6%A0%87%E7%BB%98%E5%88%B6%E5%9B%BE%E5%BD%A2.gif" alt="光标绘制图形"></p>
<p>分析实现需求的几个关键点：</p>
<ul>
<li>光标从默认形状变为十字架形</li>
<li>鼠标按下的坐标</li>
<li>鼠标拖动时跟随变化的矩形</li>
<li>鼠标抬起的坐标</li>
</ul>
<p>鼠标的行为可以通过监听画布的 <code>mouse:down</code>、<code>mouse:move</code>、<code>mouse:up</code>来捕获，而变化的矩形则是在按下鼠标时创建一个矩形，在 <code>mouse:move</code>事件中实时改变此矩形的形状。不过对于矩形来说，我们可以利用 fabric 的多选模式走一个捷径，这个多选的效果刚好是一个变化的矩形。如果把它默认的样式改为中部透明只保留边框就是期望的效果。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/fabric/fabric%E5%A4%9A%E9%80%89.gif" alt="fabric多选"></p>
<p>大概实现如下：</p>
<pre><code class="language-js">function setDrawMode (canvas: fabric.Canvas) {
    canvas.selection = true;
    canvas.selectionColor = 'transparent';
    canvas.selectionBorderColor = 'rgba(0, 0, 0, 0.2)';
    canvas.setCursor('crosshair');

    canvas.discardActiveObject().renderAll();

    // 此函数是遍历节点，禁用节点的交互功能，否则导致：
    // 1. 无法在某个元素内部绘制，看不到绘制的区域效果
    // 2.绘制时会拖拽外层大元素
    disableNodeInteractive();
}

let downPointer;
canvas.on('mouse:down', e =&gt; {
    let evt = e.e;
    if (e.absolutePointer) {
        downPointer = e.absolutePointer;
    }
});

// 因为可以利用 fabric 多选效果走捷径，所以 mouse:move 事件就不需要监听了
// canvas.on('mouse:move', ...

canvas.on('mouse:up', e =&gt; {
    if (downPointer &amp;&amp; e.absolutePointer) {
        // 此函数功能为绘制自定义图形
        drawComponentRect(canvas, downPointer, e.absolutePointer);
        downPointer = undefined;
    }
});
</code></pre>
<h3>图形区域限制</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/fabric/%E5%8C%BA%E5%9F%9F%E9%99%90%E5%88%B6.gif" alt="区域限制"></p>
<p>区域限制的逻辑则很简单，无非是判断各个端点是否在可移动区域内。不过这里有几个需要注意的点：</p>
<ul>
<li>节点自身属性中有 left 和 top ，这个点代表的是左上角的坐标。那么右上角的 x 坐标则是 <code>left + width</code> 。这里需要注意，这个 width 只是原始的宽度，缩放的情况下实际的宽度应当是 <code>width * scaleX</code> ；其他点也是同理</li>
<li>判断是否超出范围应该是将各端点都判断一遍，而不是一个端点不正确就立即返回。应当以一个数组收集超出的边界。比如超出左边和上边 <code>['left', 'top']</code></li>
<li>在图形拖动或者拖拽超出边界时，如果有修正位置的需求，则应该是遍历上面收集的超界数组，对每一条超出的边进行修正</li>
</ul>
<h3>节点穿透选中</h3>
<p>Fabric.js 的节点可以通过 <code>canvas.getObjects()</code> 获取，得到的结果是一个节点数组，而这个数组同时也代表了节点之间的层级关系。数组的第零项代表最下层，最后一项为最上层。所以如果节点 A 的大小完全覆盖节点 B 且 B 的层级比 A 低，那么是无法直接选中 B 节点的。如下图所示，只能调整 form 节点的层级实现选中 input 节点。当同一个方向上节点众多的复杂的情况下，也许层级的调整还需要仔细的操作一番才能打到目的。因此有必要实现节点的穿透选中。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/fabric/%E6%97%A0%E6%B3%95%E7%A9%BF%E9%80%8F%E8%8A%82%E7%82%B9.gif" alt="无法穿透选中"></p>
<p>这里有两种思考的视角。</p>
<p>一种是从节点出发。当鼠标悬浮或者点击在节点 A 上时，如果目的是穿透选中底部的 B 节点，那么需要对页面上的所有节点同 A 进行位置和大小的计算，算出来哪些节点被 A 完全覆盖且位于 A 的下方，然后对其进行选中操作。</p>
<p>一种是从鼠标事件的位置出发。仔细感受节点穿透选中操作，不难发现其实最终应该被操作的节点应该是<strong>包含了鼠标位置的最小面积节点</strong>。那么只需要对所有节点做一次遍历判断找到满足要求的节点即可。</p>
<p>显然从鼠标位置来入手更简单。</p>
<pre><code class="language-js">function minAreaNodeContainPointer (canvas: fabric.Canvas, pointer: fabric.Point) {
    const objects = canvas.getObjects();
    let minArea = Infinity;
    let targetNode: fabric.Object | null = null;

    for (let object of objects) {
        const objectArea = object.width! * object.height!;
        if (object.containsPoint(pointer, null, true) &amp;&amp; objectArea &lt; minArea) {
            targetNode = object;
            minArea = objectArea;
        }
    }

    // 将该节点置顶，否则会导致拖拽时聚焦的是该节点但移动的是上层节点
    if (targetNode) {
        canvas.moveTo(targetNode, objects.length);
    }

    return targetNode;
}

canvas.on('mouse:down', e =&gt; {
    let evt = e.e;

    // 穿透选中内层节点
    if (/* e.e.ctrlKey &amp;&amp; */ e.absolutePointer) {
        const innerNode = minAreaNodeContainPointer(canvas, e.absolutePointer);
        innerNode &amp;&amp; canvas.setActiveObject(innerNode);
    }
});
</code></pre>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/fabric/%E7%A9%BF%E9%80%8F%E8%8A%82%E7%82%B9%E9%80%89%E4%B8%AD.gif" alt="穿透选中"></p>
<h3>自适应布局</h3>
<p>默认情况下，画布以左上角为原点进行节点渲染，当节点的坐标超出当前视口范围内画布大小时，这些节点是不可见的，因此需要提供画布的自适应布局的功能，让所有的节点都可以在当前视口范围内可见。下面是一个 demo 示例，黑色线框区域为 800*800 大小的画布，画布内实际上存在 6 个节点，有两个是在视口范围之外。当我们点击 fit view 按钮后，所有节点都可以在视口区域内可视。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/fabric/fitview.gif" alt="fitview"></p>
<p>这个功能的实现思路为：</p>
<ul>
<li>如果没有这个按钮提供的功能，我们使用习惯是通过画布的缩放和拖拽来让所有节点位于视口范围。因此这个功能的实现则是通过改变视口中心位置和画布缩放来实现</li>
<li>我们将所有的节点包裹在一个虚拟的盒子里，实际上就是将视口移到这个盒子中心以及缩放这个盒子大小</li>
<li>这个盒子的大小取决于最远和最近的节点坐标</li>
</ul>
<p>我将这部分代码封装为 <a href="https://github.com/CocaColf/fabric-fitView">fabric-fitView</a></p>
<h3>画布缩放和拖拽</h3>
<p>这个在 <a href="http://fabricjs.com/fabric-intro-part-5#pan_zoom">文档</a> 上已经非常详细</p>
<h3>历史记录</h3>
<p>历史记录的实现原理非常简单，将所有的状态存放在数组内，通过改变指向当前状态的指针来加载不同时候的画布内容即可。</p>
<h3>右键菜单</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/fabric/%E5%8F%B3%E9%94%AE%E8%8F%9C%E5%8D%95.gif" alt="右键菜单"></p>
<p>分析这个需求，有下面几个点：</p>
<ul>
<li>按下右键时，浏览器的菜单不要弹出</li>
<li>自己实现的右键菜单需要在鼠标按下的位置附近弹出</li>
</ul>
<p>第一个问题 Fabric 已经给予了帮助，通过设置 <code>stopContextMenu</code>和 <code>fireRightClick</code>即可；第二个问题则需要监听鼠标的事件和捕获坐标，将我们的菜单在 DOM 中的位置移动至鼠标附近。</p>
<pre><code class="language-js">canvas.on('mouse:down', e =&gt; {
  	// button: 1-左键；2-中键；3-右键
    // target 为 null 则是发生在 canvas 上
    if (e.button === 3) {
        if (e.target) {
            canvas.setActiveObject(e.target);
        }
        showMenu(canvas, e);
    } else {
        hideMenu();
    }
});

function showMenu (canvas, opt) {
  const menu = document.getElementById('menu');  // 获取菜单容器

  // 设置右键菜单位置
  // 1. 获取菜单组件的宽高
  const menuWidth = menu.offsetWidth;
  const menuHeight = menu.offsetHeight;

  // 当前鼠标位置
  let pointX = opt.pointer.x;
  let pointY = opt.pointer.y;

  if (canvas.width &amp;&amp; canvas.width - pointX &lt;= menuWidth) {
      pointX -= menuWidth;
  }
  if (canvas.height &amp;&amp; canvas.height - pointY &lt;= menuHeight) {
      pointY -= menuHeight;
  }

  menu.style.left = `${pointX}px`;
  menu.style.top = `${pointY}px`;
}
</code></pre>
<h3>右侧面板联动</h3>
<p>改变右侧面板的值调整画布上内容呈现或者是将画布上内容的调整同步至右侧面板，本质上是数据通信，对于数据通信有很多可以选择的方式，在 Vue 中个人觉得最简单的还是 Vue EventBus。</p>
<h3><code>_render</code> 中使用 ctx 绘制图形和缩放有关</h3>
<p>如果通过覆写 <code>_render</code> 函数，在其中通过 ctx 绘制图形，这个绘制的图形会和节点自身的缩放有关系。比如图形缩放了 3 倍，此时绘制出来的文本也会显示成 3 倍大，而我期望这个文本大小适中是保持同等大小的字号。解决这个问题可以监听元素的 modified 事件，在 modified 结束后设置元素宽高，且把缩放重置为 1</p>
<pre><code class="language-js">canvas.on('object:modified', e =&gt; {
	if (
		e.action === 'scale' &amp;&amp;
		e.target &amp;&amp;
		e.target.width &amp;&amp;
		e.target.height
	) {
		e.target.set({
			width: Math.round(e.target.width * (e.target.scaleX || 1)),
			height: Math.round(e.target.height * (e.target.scaleY || 1)),
			scaleX: 1,
			scaleY: 1,
			dirty: true,
		});
	}
})
</code></pre>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[面试官视角的校招]]></title>
            <link>https://kkkf.vercel.app/posts/面试官视角的校招.html</link>
            <guid>https://kkkf.vercel.app/posts/面试官视角的校招.html</guid>
            <pubDate>Fri, 23 Sep 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>这个月是第一次以面试官的身份参加校招，至此已经参加三批校招（一次笔试为一批），我负责的是一面，共和 37 位候选人交流，其中 10 人通过本轮面试。下一批面试要到下个月了，因此很适合在这个节点记录一下这段体验。</p>
<h3>如何进行技术面试</h3>
<p>“八股文”这个词在招聘中广为流传，我一直认为这种一问一答没有深度的八股交流无法筛选出合适的人才，从我最初面试实习生开始，便打定我不会做这样的面试官的想法。从实际面试经历中，确实可以看到许多候选人对于知识点侃侃而谈，一旦深入或进行编码便寸步难行。由于前端的入门门槛比较低，转行的或者学习时间短的较多，因此这种现象非常普遍。</p>
<p>一面主要关注「基础知识」和「编码能力」，我进行技术面试的策略是：<strong>问题都从一个需求或者实际场景出发编写代码，在编写代码的过程中进行细节询问或者内容扩展让知识点尽量串联</strong>。比如：</p>
<p>可以从一个函数出发，逐步扩展：</p>
<ul>
<li>为了确保后面某些操作的正常执行，我们需要对函数的入参进行类型判断</li>
<li>现在确定入参是一个对象，那么我们来遍历这个对象，这里就可以考察原型相关内容；对象属性不允许修改谈到 defineProperty 、Vue 双向绑定等，这里又可以衍生出一些场景问题</li>
<li>实现某个逻辑执行前停顿 3s，聊到 Promise、聊到异步</li>
</ul>
<p>可以从前端每两秒轮询调用后端接口拿到某个数据内容这个常见的业务场景出发：</p>
<ul>
<li>完成基本编码</li>
<li>如果候选人使用了 setInterval，便可以和他说如果后端的接口耗时很长，这里会有什么问题？如何解决这个问题；如果候选人使用了这个场景下应该使用的 setTimeout 解决方案，便询问是基于什么考虑没有使用 setInterval。总之这里可以考察到 setTimeout 和 setInterval 的共性和区别，也可以考察候选人分析问题理解问题的能力</li>
<li>当然这里也可以从定时器聊到事件循环</li>
</ul>
<p>这种方式考察基础知识的同时，也编写了较多的代码，但这些代码比较零碎，篇幅也不会很长，虽然能一定程度反映出编码风格，但编码这块还是需要所谓的“做题环节”进一步考察。相对于 leetcode 上的题，我更喜欢一些在实际工作中常遇到过的数据转换、查找的问题，这些问题有些是需要对 JavaScript 有较好的了解才可以写出可读性好的代码；有些是需要考虑到足够的用例场景；有些是半开放性的，需要沟通这里是否有 xx 方法已经在业务中存在了。</p>
<p>这样的面试方式对于候选人来说是高质量的能力考察，在实际面试中有三类表现：</p>
<ul>
<li>如果是仅仅停留在“八股文”这个层面而没有写过足量代码练习的候选人，会步步难行，一般在面试开始十分钟后逐渐破防。最初自我介绍时声音洪亮，逐渐声音弱小，对于自己的回答开始不自信，其中有一位女生我甚至感觉快要落泪了</li>
<li>进行过一定的编码练习且在前端领域投入过一定时间的候选人，虽然会有中途卡顿、想不出来的情况，但是经过引导最终顺利想清楚问题，面试官和候选人都会有一种协作的快乐</li>
<li>能力较强的候选人会和面试官聊得非常开心，你知道我想问的是什么，我给的就是你想要的。</li>
</ul>
<p>在面试结束后许多候选人对我表示了感谢，说这种引导式的面试比之前参加过的一问一答的体验好多了，从面试过程中学到了很多东西，也知道了自己哪里不够好。我对于他们的感谢非常开心，这也是我期望中的理想面试。面试者可以从面试中展现自己、学到东西、认识到不足；面试官可以从面试过程中考察到自己想要的东西、修正自己不够清楚的问题阐述方式、学会如何对候选人思维进行引导、如何感受和转移候选人的情绪变化。</p>
<p>不过这种面试也存在问题，一方面是内容过于充实，导致面试时间经常不够；另一方面是对于面试官来说有点累。在某天劳累的面试结束后，我坐在地铁上不禁对自己面试反省。</p>
<p>首先，我认为这样的方式对面试官的要求是比较高的，也许经验丰富、能力强的面试官可以做到手到擒来，但是对于我来说，不得不在面试前思考我会从哪些知识点考察候选人，这些知识点如何通过一些场景在编码中进行串联考察。这需要花一定的精力和时间去准备一场面试。</p>
<p>另外，在候选人非常一般的情况下，我需要想尽各种方式，甚至需要打起问题答案的“擦边球”引导，当面试场次频繁的情况下对于脑力和耐心的消耗是比较大的。有时候甚至仿佛是在手把手教学生明白如何解决这个问题，这一块的原理是什么。这一点尤其让我自我反省这是否是必要的；有时候甚至觉得这种考查方式或者我的行为方式是有问题的；也会觉得一问一答，不会就打断跳过的面试方式确实会轻松很多。</p>
<h3>候选人卡住时怎么办</h3>
<p>候选人在面试这种高压环境下，因为紧张等原因卡住非常正常，这个时候我们的适当引导是非常有必要的。我一般会这样做：</p>
<p>如果候选人的思路正确，首先对候选人思路进行肯定，继而提醒他可能是什么没有想清楚。比如有个候选人在一个问题解决中卡在将 <code>Map&lt;string, number&gt;</code> 这个数据结构根据 number 进行排序并转为二维数组时，便可以提醒他这个问题通过 Map 进行统计的想法是正确的，但这里使用循环遍历 Map 进行重新排序以及二维数组转换有点麻烦，我们是不是可以想一下在 JavaScript 中 Map 和二维数组是不是有什么联系？</p>
<p>我们可以让候选人重新阐述思路或者我们进行引导，并在白板上写出思路中的关键字。比如说用 JavaScript 实现一个 sleep 函数，有些候选人想不到解决方式。这时候便可以这样引导：</p>
<ul>
<li>我们在 JavaScript 中，有一个同步阻塞的方式，是什么呢？候选人提到 <code>await</code></li>
<li>那 await 它等待的是什么呢？候选人思考后回答是等待 <code>Promise</code> 完成</li>
<li>那 Promise 它有哪几个状态呢？什么时候才算完成呢</li>
<li>那这里如何实现一个  Promise 3s 后完成？候选人答使用 setTimeout</li>
</ul>
<p>这样下来，候选人能力正常的情况下，根据这些关键词，应该就可以想到解决方法了。</p>
<p>如果经过引导候选人还是没能想出答案，而面试时间并不允许长时间停留于此，那么就只能对候选人进行情绪安慰，比如可以表示这里没有想（做）出来没关系哈，你的想法是正确的/已经很接近了，主要是这个思考过程答案并不重要哈。我们继续探讨后面的内容。</p>
<h3>候选人反问</h3>
<p>候选人反问一般喜欢问部门做什么的、学习建议、面试评价、内部技术栈这几个问题。我觉得对于反问，面试官应该真实、真诚，不要不好意思说。尤其是“面试评价”这个问题，我觉得相比于明明表现不够好但却夸赞，结束后将候选人挂掉，不如真实的和候选人说他的优点和不足，适当给予建议。前者虽然在面试时心里似乎比较好受，大家相互也是 peace&amp;love，但是收到拒信后会很失望甚至难过吧，心里也免不了去想很久的为什么。</p>
<h3>如何做决定</h3>
<p>不通过的候选人基本在面试过程中就有了决定，但是总会有一些让你纠结一二的候选人。可以通过下面几点做决定：</p>
<ul>
<li>你愿不愿意和他做同事？</li>
<li>如果他入职后是你做导师，你愿不愿意培养他？</li>
<li>沟通能力、表达能力、逻辑能力</li>
<li>有没有感受到技术热情</li>
</ul>
<h3>其他</h3>
<p><strong>表达和礼貌</strong></p>
<p>面试官代表公司形象，在面试过程中不管是态度还是用词，对候选人要礼貌、尊重、平等。在提问时要注意问题的边界。同时阐述问题时不要故弄玄虚，清晰准确地表达想要问的问题即可，如果候选人回答的方向是错误的理解了问题意思，这个时候可以适当回头想想是不是自己阐述方式有些问题。</p>
<p><strong>面试和上班</strong></p>
<p>面试比上班累多了！一天 7-10 个，45 分钟一个，每个之间 15 分钟，经常出现上一个面试评价刚写完就要开始下一个的情况，因此休息时间很短。</p>
<p><strong>关于运气</strong></p>
<p>以前听人说面试是一门“玄学”，这次我多少也感受到这一点意味，其中我最想提一下的是招聘策略调整导致后批次面试难度下降。</p>
<p>在秋招开始前，我们面试官群里发了一个文档，里面有一部分是“总体原则”，其中两点有所争议：基础好但看不出什么潜力的，淘汰；前端学习时间短，但觉得学习能力强的，淘汰。</p>
<p>基于这个文档里的一些内容，前两批面试的通过率比较低，我手上挂了非常多的大厂实习+本硕 985 的候选人。后面到第三批时 HR 说我们前端这边的一面通过率只有 20% 多，其他部门有 40%，因此到第三批策略进行了调整，具体内容不言，简而言之就是要求有所降低。当时我想起了几个我纠结过但最后淘汰的候选人，心里有不平、内疚、可惜等混合心情。</p>
<p>当然“运气”这部分更多的还是在面试官的问题是否刚好命中到候选人准备的点上等情况，像上面这种问题应该还是比较少吧？</p>
<p><strong>秋招存在不老实的行为现象</strong></p>
<p>我遇到过两三个候选人面试过程中让我感觉存在“可疑行为”。一种是眼神飘忽不定，上下看或者左右看，仿佛旁边有参考资料；一种是写代码或者回答问题时断断续续，突然变得流利或下笔如有神。候选人还是太自作聪明了，这种视频面试一举一动都是清楚的，面试官也不是傻子；而且面试时也会拿到一份笔试的作弊判断报告。</p>
<p><strong>寒气</strong></p>
<p>手上简历的“硬度”还算是不错，60% 的候选人是硕士，60% 的候选人是 211 以上，我感觉这个节点应该不算秋招的早期吧，基本大家都还没有拿到 offer。40% 的候选人有大厂的实习经历，无一转正，原因清一色的今年没有转正名额。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[代码改动关联分析和GitLab打通]]></title>
            <link>https://kkkf.vercel.app/posts/关联分析工具和GitLab打通.html</link>
            <guid>https://kkkf.vercel.app/posts/关联分析工具和GitLab打通.html</guid>
            <pubDate>Tue, 21 Jun 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>之前的 <a href="https://cocacolf.vercel.app/blog/%E4%BB%A3%E7%A0%81%E6%94%B9%E5%8A%A8%E5%85%B3%E8%81%94%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/">博文</a> 介绍了 <a href="https://github.com/CocaColf/coderfly">Coderfly</a> ，近两周在它基础上做了点比较有意思的工作，将它和 GitLab 打通，打造一个易配置、无感知的工作流，我叫它为 <code>coderfly-flow</code>。</p>
<p>它的价值在于，每一次提交合并请求后，触发服务，服务会将改动关联影响结果以 Review 的形式在合并请求下评论，同时也提供更详细的报告查询。如果开发者认为自测没问题了，就关掉该问题。假设后面本次改动造成了改动引发，而回过头看到关联分析服务已提醒了该影响，那么就说明自测并不仔细。说白了是两个意义：一方面是给开发者自测建议；另一方面是提供了质量回溯的方式。效果如下：</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/GitLab_review.png" alt="gitlab review"></p>
<center style="font-size:14px;text-decoration:underline">GitLab review 评论</center> 
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/coderfly_report.png" alt="report detail"></p>
<center style="font-size:14px;text-decoration:underline">关联影响报告</center> 
<p>它的整个流程如下，如果看过之前的 <a href="https://cocacolf.vercel.app/blog/%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%B5%8B%E5%B7%A5%E5%85%B7%E6%9C%8D%E5%8A%A1%E7%AB%AF/">代码质量检测工具服务端设计</a> 便能发现二者是相似的，都是对 GitLab上的某一个提交或分支的代码进行扫描。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/coderfly_gitlab.png" alt="流程图"></p>
<p>我并不认为做相似的工作是一件轻松的事情。首先，解决相似的问题往往会依赖过去经验，而过去的经验并不一定是最佳实践，最常见的例子便是过一段时间再回过头去看自己之前写过的代码，会觉得很多地方写得不好；另外，既往的经验增长的不仅是解决问题的信心，同时还有分析问题的掉以轻心，很多潜在的问题会在开始时被忽略。由于二者的技术方案大方向一致，所以本文关注的是这两件相似工作「存异」的部分。</p>
<h3>Webhook 还是 CICD</h3>
<p>在「代码质量检测」（bcode）中，当提交合并请求后触发流水线，流水线中预先构建好的脚本开始将此分支代码压缩上传到服务端进行扫描。我在 <code>coderfly-flow</code> 中故技重施，毕竟有老代码可以直接复用。一顿操作后，顺利的跑了起来，但是后面准备尝试第一个项目时，我将这一块代码全部删除了，重构为 webhook 方式。原因在二：</p>
<ul>
<li>准备尝试落地的第一个项目是前后端同仓的项目，我对自己的 shell 脚本编写能力不自信，不敢保证不会影响后端大几百行的脚本</li>
<li>大家对之前 bcode 的接入方式并不喜欢，因为流水线会一定程度影响它们的合并请求</li>
</ul>
<h3>任务资源竞争</h3>
<p>由于现在是 webhook 方式触发，那么自然服务器收不到代码了，所以必须由服务端进行代码 clone。webhook 中携带的参数是足够做这件事的，但必然不能每个合并请求都 clone 代码，有些代码好几十 G，等到代码 clone 完毕，合并请求都被合并了，分析还有啥价值。因此必须在项目接入该服务前，提供接口在服务器上提前 clone 代码。后续通过 fetch 和 checkout 命令切换到不同的分支获得对应分支的代码。那么这里就出现了一个问题，同个项目的所有合并请求分析都是在同一份代码中进行的，这些分析任务并不是有序的，因此任务之间会存在资源竞争。</p>
<p>对于这种问题，我有两种思路：</p>
<ul>
<li>方案一：当提前 clone 源码后，对该源码打个压缩包。假设现在有 A 和 B 两个合并请求，那么就在当前文件夹下解压这个压缩包两次到 A 和 B 文件夹内，这样两个合并请求的分析工作就互不干扰了</li>
<li>方案二：这种问题一般的关键字都是「锁」。这个场景让我想到秒杀系统，于是得到了关键字「Redis 分布式锁」。</li>
</ul>
<p>方案一个人觉得还挺巧妙的，但并不是个好方案，因为 clone 源码后的压缩、任务执行前的解压缩都是 CPU 密集型任务，资源消耗和时间消耗都非常大。我编写了代码进行了尝试，任务常常会因为挂起时间过长而失败。</p>
<p>因此选择使用方案二。以项目的 url 为 key 加锁，任务在加锁失败时不断去尝试获取锁，加锁成功方才执行任务，这样就可以保证一份代码下只有一个任务在操作。这个应用只会部署在一台服务器上，因此它只是简单的单实例分布式锁。</p>
<p><strong>上锁</strong></p>
<p>Redis 2.8 版本之后支持 set 命令传入 setnx、expire 扩展参数，避免了 setnx 非原子性操作的问题，所以直接利用 set 命令即可。</p>
<ul>
<li>value：设置的值</li>
<li>EX seconds：设置的过期时间</li>
<li>PX milliseconds：也是设置过期时间，单位不一样</li>
<li>NX|XX：NX 同 setnx 效果是一样的</li>
</ul>
<pre><code class="language-shell">set key value [EX seconds] [PX milliseconds] [NX|XX]
</code></pre>
<p><strong>释放锁</strong></p>
<p>释放锁将 key 删除掉即可，但忘记之前在哪个博文里看到过释放锁并不能仅仅使用 del key 删除掉，这样很容易删除掉别人的锁。假如某个任务的处理时间超过了锁自动释放的时间，此时锁被其他任务占用了，那么此时释放的是别人的锁。所以这里要保证释放的只是自己的锁。因此在 del key 之前先判断这个 key 的 value 是否是指定值。方式是借助 Lua 脚本实现。</p>
<pre><code class="language-lua">if redis.call(&quot;get&quot;,KEYS[1]) == ARGV[1] then
    return redis.call(&quot;del&quot;,KEYS[1])
else
    return 0
end
</code></pre>
<p><strong>Node.js 单实例分布式锁完整代码</strong></p>
<pre><code class="language-javascript">export async function lock (key: string, val: string) {
    
    return (async function _loopLock() {
        try {
            const result = await redis.set(key, val, 'EX', REDIS_LOCK_SECONDS, 'NX');
    
            // 上锁成功
            if (result === 'OK') {
                return true;
            }

            await sleep(3000);
            
            return _loopLock();
        } catch(err) {
            throw new Error(err);
        }
    })();
}

export async function unLock(key: string, val: string) {
    const script = &quot;if redis.call('get',KEYS[1]) == ARGV[1] then&quot; +
    &quot;   return redis.call('del',KEYS[1]) &quot; +
    &quot;else&quot; +
    &quot;   return 0 &quot; +
    &quot;end&quot;;

    try {
        const result = await redis.eval(script, 1, key, val);

        if (result === 1) {
            return true;
        }

        return false;
    } catch(err) {
        throw new Error(err);
    }
}

</code></pre>
<h3>自动生成任务脚本</h3>
<p>假设此时我们的待扫描的项目 A 的代码存放在 <code>/mnt/code</code> 下，在 bcode 中，每一个任务都是新开子进程执行，我的做法是在每一份待扫描代码的根目录下自动生成脚本执行扫描任务，假设这个脚本为 <code>scan.js</code>，其内容如下：</p>
<pre><code class="language-js">// scan.js

const run = require('bcode');

process.on('message', async ({params}) =&gt; {
    const scanMark = await run(JSON.parse(params));
    
    process.send(scanMark);
});

</code></pre>
<p>因此这个脚本会在 A 扫描前生成在 <code>/mnt/code/scan.js</code>。</p>
<p>接下来对这个任务进行扫描：</p>
<pre><code class="language-js">const { fork } = require('child_process');

const p = fork('/mnt/code/scan.js', {
  cwd: '/mnt/code',
});

p.send({
  // ...
});

</code></pre>
<p>这种做法有两个问题，然而 bcode 在线上跑了半年多了我至今都没有意识到。</p>
<p>第一个问题是，如果我们的服务端代码部署的目录和扫描代码存放的在同一个目录，这样的方式没有问题，因为我们的服务依赖于 bcode，所以我们肯定会安装 bcode 这个包，因此 A 项目中自动生成的 <code>scan.js</code> 向上查找最终是可以从我们服务端代码的 <code>node_modules</code> 中找到这个包的。如下所示：</p>
<pre><code class="language-js">- server_code
  - node_modules
  - clone_projects
    - A
      - scan.js
</code></pre>
<p>但是有一天我们调整了目录，将这些待扫描的项目代码放到另一个磁盘呢？因为待分析的代码只是静态代码本身，而不需要运行，自然不会安装依赖，因此这种场景下扫描，那自然 <code>scan.js</code> 执行时就会由于找不到包而报错。</p>
<p>第二个问题是有必要对每个待扫描项目都生成 <code>scan.js</code> 吗？再次回头看我们开启子进程执行扫描的这段代码：</p>
<pre><code class="language-js">const p = fork('/mnt/code/scan.js', {
  cwd: '/mnt/code',
});
</code></pre>
<p>真正需要关注的其实并不是脚本在哪里，而是我们的脚本工作在哪里。因为对于每一个任务，我们生成脚本的内容是一样的，只是脚本的参数不一样，而参数是在业务代码里处理的。也就是说我们只需要将 <code>scan.js</code> 放在业务代码里即可，而 <code>cwd</code> 参数就是每个被扫描项目的目录，这个才是动态变化的。</p>
<h3>Session 保活机制</h3>
<p>在每次分析完成后，需要在当前合并请求下方以 Review 的形式提一个问题，问题内容则是分析结果。GitLab 并没有对这个功能提供接口，因此需要自己抓包。研究了许久后，终于兜兜转转调了几个接口拿到参数实现了该功能，但有个很关键的问题，接口需要传递 Session。由于我们的 GitLab 登录实际上并不是走的它平台本身的登录，而是走的公司内部统一的登录，因此通过 <code>private token</code> 获取到的 Session 并不能用。最终没办法，只能自己手动去维护这个 Session。</p>
<p>策略很简单：</p>
<ul>
<li>定时任务，每五分钟一次去访问 GitLab 首页</li>
<li>访问首页获得的响应内容为 html 文本，解析这段 html 判断是登录状态还是非登录状态</li>
<li>通知告警机制，一旦 Session 失效，及时通过内部通讯工具告知我</li>
<li>页面表单填写新的 Session 上报，服务端写入文件</li>
</ul>
<p>第二点比较有意思，以前写爬虫的时候就处理过这种问题，在 Node 中可以使用 <a href="https://www.npmjs.com/package/cheerio">cheerio</a> 这个包来解析 html。这里我是通过某个只有登录状态下才有的 class 来判断是否是登录状态，并不太靠谱，万一哪天改名了呢，但好像也没有更好的方式了。</p>
<hr>
<p>突然想到我好像没有介绍过为什么叫 <code>coderfly</code>。我取名的时候在想，我这个工具是检测代码改动影响的，代码是程序员写的，我们的每一行改动有可能对其他功能造成影响，让我想起蝴蝶效应。因此我就将 coder 和 butterfly 组合到一起，叫它 coderfly。</p>
<p>这个项目给的时间并不多，节奏比较紧张，实际还有一些文章没有描述的细碎且棘手的问题和场景。我写得很开心，去年写 bcode 时也有这样的感觉，骑着电动车吹着风下班回家感觉今天很有意义和收获的满足感。我感觉我很适合做这种类型的工作，我不知道这种类型的工作有没有一种岗位，比如说开发者工具研发？测试（质量）工具开发？</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[Node工具库性能提升]]></title>
            <link>https://kkkf.vercel.app/posts/线程池优化Node应用.html</link>
            <guid>https://kkkf.vercel.app/posts/线程池优化Node应用.html</guid>
            <pubDate>Wed, 18 May 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>  本文记录了最近将 Node 工具 <a href="https://github.com/CocaColf/coderfly">coderfly</a> 性能提升 55.8% 的过程。主要优化手段为：</p>
<ul>
<li>Node.js 多线程：<code>work_threads</code></li>
<li>增加缓存</li>
<li>合理的编写异步代码</li>
</ul>
<h2>优化前性能情况</h2>
<blockquote>
<p>可以在<a href="https://cocacolf.vercel.app/blog/%E4%BB%A3%E7%A0%81%E6%94%B9%E5%8A%A8%E5%85%B3%E8%81%94%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/">这篇博文</a>了解 coderfly</p>
</blockquote>
<p>  项目含有 3279 个有效文件，所有变更文件实际变更的函数为 74 个，性能情况如下：</p>
<pre><code class="language-bash">time node coderfly_performance.js

# 搜索配置文件：0.002
# 获取函数变更耗时：0.641
# 文件过滤耗时：0.395
# 一共需要处理 3279 个文件
# 构建文件树耗时：37.692
# 需要搜索的目标函数个数：74 个
# 搜索耗时：0.581

# 总耗时：39.314
</code></pre>
<p>可以看到耗时集中于「函数树构建」。</p>
<h2>为什么慢</h2>
<p>  函数树的构建主要对每一个文件做如下几个工作：</p>
<ul>
<li>将文件解析为 AST，遍历 AST 提取所有函数和函数内部的被调用函数信息</li>
<li>解析 Vue 文件的 Template  AST，将节点中疑似使用的函数提取出来</li>
<li>在遍历文件 AST 时同时会获取模块（依赖）导入信息、Mixin 信息</li>
<li>处理完上述内容后，会对每个文件再进行一次处理，将所有函数的来源路径都确定</li>
</ul>
<p>  将这些工作的总耗时统计出来，平均下来每个耗时大概是 0.007 秒，但积少成多。以 3000 个文件计算达到了 <code>0.007 * 3000 = 21</code> 秒。</p>
<p>这部分的代码很简单，就是 for 循环所有文件。</p>
<pre><code class="language-js">let tree: FileInfoTree = {};

for (const file of files) {
    const fileInfo = getFileInfo(file, options);
    tree[file] = fileInfo;
}
</code></pre>
<p>也就是说这个时间复杂度是 <code>O(files count)</code>。</p>
<p>问题原因知道了，想法随之而来：由一个一个处理，改成同时处理多个文件。</p>
<h2>多线程方式生成文件树</h2>
<p>  Node.js 中，由 Libuv 提供了事件轮询机制，因此在「io 密集型」处理上，不用太担心性能，但是在「CPU 密集型」问题上，由于 JavaScript 是单线程，因此这种耗时操作会阻塞主线程，导致整体耗时增加，同时也无法有效的利用执行环境的多核优势。对于这种「CPU 密集型」问题，在 Node.js 中有两种方案，一种是使用 <code>children_process</code> 或者 <code>cluster</code> 开启多进程进行计算；另一种是使用 <code>worker_thread</code> 开启多线程进行计算。这里选择更轻量级的 <code>worker_threads</code>。</p>
<p><strong>重构</strong></p>
<p>使用 <code>worker_threads</code> 重构这段循环代码：</p>
<pre><code class="language-js">// parent worker
// run_worker.ts
import { cpus } from 'os';
import path from 'path';

function getFileInfoWorker (files: string[], options?: GetTreeOptions): Promise&lt;FileInfoTree&gt; {
    return new Promise((resolve, reject) =&gt; {
        // 根据 cpu 数目将文件进行均分
        const threadCount = cpus().length;
        const everyWorkerFileCount = Math.ceil(files.length / threadCount);

        // 线程池
        const threads: Set&lt;Worker&gt; = new Set();

        for (let i = 0; i &lt; threadCount; i++) {
            threads.add(new Worker(path.resolve(__dirname, './get_file_info_thread.js'), {
                workerData: {
                    files: files.splice(0, everyWorkerFileCount),
                    options,
                }
            }));
        }

        for (const worker of threads) {
            worker.on('error', (err) =&gt; {
                reject(new Error(`worker stopped with ${err}`));
            });

            worker.on('exit', (code) =&gt; {
                threads.delete(worker);

                if (code !== 0) {
                    reject(new Error(`stopped with  ${code} exit code`))
                }

                if (threads.size === 0) {
                    resolve(tree);
                }
            })
    
            worker.on('message', (msg) =&gt; {
                Object.assign(tree, msg);
            });
        }
    });
}
</code></pre>
<p>每一个 <code>child worker</code> 执行任务并通过 <a href="https://nodejs.org/api/worker_threads.html#worker_threads_class_messagechannel">message channel</a> 将结果发给 <code>parent worker</code>：</p>
<pre><code class="language-js">// get_file_info_thread.ts
import fs from &quot;fs&quot;;
import { parentPort, workerData } from &quot;worker_threads&quot;;
import { FileInfoTree, GetFileInfoWorkerData } from &quot;../type&quot;;
import { getFileInfo } from &quot;../utils/handle_file_utils.js&quot;;

parentPort?.postMessage(_getFilesInfo(workerData));

function _getFilesInfo (workerData: GetFileInfoWorkerData) {
    const currentTree: FileInfoTree = {};

    for (const file of workerData.files) {
        const fileInfo = getFileInfo(file, workerData.options);
        currentTree[file] = fileInfo;
    }

    return currentTree;
}
</code></pre>
<p><strong>效果</strong></p>
<p>  使用 <code>worker_threads</code> 重构后，「构建文件树」的耗时由 37.692 秒降低到了 12.253 秒，提升 67.5%。可以预见的是，当文件数目更大时，这个效果会更突出。</p>
<h2>增加缓存</h2>
<p>  在第一次执行扫描后，第二次扫描可能 99% 的文件都是没有变化的，不应该再重新去构建它们的文件树，因此需要增加「缓存」来记录状态。</p>
<p>  做法很简单，只需要在第一次扫描时，本地生成一份以 <code>key-value</code> 形式记录 <code>文件路径-最后一次编辑时间</code> 的 json 文件， 第二次扫描时用「最后一次编辑时间」作为判断依据即可。如果没有更改，则使用旧数据；已更改方才需要重新构建。这个工作放在 <code>child worker</code> 中即可。</p>
<p>  增加缓存后时间从 12.253 秒降低到了 3.24 秒。</p>
<h2>合理的编写异步代码</h2>
<p>  对于 JavaScript 开发者来说，async await 的出现让异步代码变得更加的清晰，可以写出很多好看的代码。</p>
<pre><code class="language-js">async function test (items) {
    for (const item of items) {
        const userInfo = await getUserInfo(item);
        const someOtherInfo = await getOtherInfo(item);
     
        // do something
    }
}
</code></pre>
<p>但是在实际开发中，我们常常忽略了这种写法同步阻塞的性能问题。这时候应该想起 Promise 本身提供的「并发」能力。</p>
<pre><code class="language-js">async function test (items) {
     
    const allPromiseRes =  await Promise.all(items.map(item =&gt; {
        return mergeLogic(item);
    }));
 
}
 
async function mergeLogic (item) {
    const userInfo = await getUserInfo(item);
    const someOtherInfo = await getOtherInfo(item);
 
    // do something
}
</code></pre>
<p>在一个 <code>for await</code> 写法的计算密集型操作中，15 个输入计算耗时 7.791 秒，改写为 <code>Promise.all</code> 后时间降低到 2.03 秒，效果还是很好的。</p>
<h2>后话</h2>
<p>  在大多数普通业务开发中，大多数逻辑执行耗时并不长，优雅的写法和简单的逻辑结构先行，并不需要非常关注性能问题。我们应该在日常开发和学习中有意识的去了解和对比不同写法、不同 API 的性能差异和不良实践；去关注常用的性能优化手段，这可以更有效的帮助我们写出高性能的代码和排查性能问题。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[函数级别的代码改动关联影响分析工具]]></title>
            <link>https://kkkf.vercel.app/posts/代码改动关联分析工具.html</link>
            <guid>https://kkkf.vercel.app/posts/代码改动关联分析工具.html</guid>
            <pubDate>Thu, 14 Apr 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>  本文记录了最近实现的前端代码改动关联分析工具的实现思路。该工具可以识别你的代码改动，从函数级别的颗粒度找到代码改动对其他函数或页面的影响，并输出影响报告，从而提高开发人员的测试效率和测试范围。可以在 GitHub 上查看其<a href="https://github.com/CocaColf/coderfly">代码</a>。</p>
<h2>背景</h2>
<p>  由于长期的业务迭代，业务代码规模越来越大，改动引发现象频发。我们修改一块老代码时，不是很清楚它是否会对其他模块产生影响。这时候许多人的做法是单纯测一下这个页面（功能），没问题就行了，提测下班；或者使用编辑器的搜索功能全局搜索一下某些关键信息，看这个修改的东西在哪里用到了，附带看一下有没有影响。</p>
<p>  如果当前修改的内容是独立的，或者用的地方不多，那么简单的测试往往也不会造成质量问题，但这种检查方式本身也不算高效，我们往往需要在代码文件里跳来跳去。因此如果有个工具，它可以识别你的修改，自动的给你找出你这个修改影响了哪些地方，是不是可以节省很多时间同时也提高自测质量呢？</p>
<h2>思路</h2>
<p>  我们的目的是根据代码的修改找到最终的影响面，这里有个很重要的词——「找」。拿什么找？找什么？</p>
<p><strong>拿什么找？</strong></p>
<p>  拿什么找？拿代码的修改找。不过这个修改是零零碎碎的，我们是要追踪一个「数据变量」的变化导致的影响？我们是要追踪一个「函数变更」的影响？因此在做这个分析之前，我们需要先确定「颗粒度」。</p>
<p>  如果是以「数据变量」为颗粒度，这个颗粒度非常细，而且对于 JavaScript 这种静态语言来说，在纯代码文本层面要做数据变量的追踪是非常困难的。因此选择「函数」作为颗粒度是比较合适的，毕竟业务功能也是由一个一个的函数组合完成的。</p>
<p>  所以拿「函数」去找，也就是说颗粒度为函数级别。</p>
<p><strong>找什么？</strong></p>
<p>我们最终的目的是为了更好的测试，那么我作为一个前端开发人员，我关注的是：</p>
<ul>
<li>我这个函数被哪些函数调用</li>
<li>我这个函数或者函数调用链上的函数被用在页面上的哪个地方，即对页面功能有什么影响</li>
</ul>
<p>  所以要找「调用链」和「页面影响」。</p>
<p><strong>如何实现</strong></p>
<p>从目的出发，可以拆解出如下几件要做的事情：</p>
<ul>
<li>因为要知道当前哪些函数发生了变更，即影响因子，所以需要根据当前代码的修改（文件的变化）得出函数变化</li>
<li>因为要得到函数的调用链，所以要构建整个项目的函数调用关系</li>
<li>因为要知道函数变更对页面的影响，所以要提取页面上 html 中的变量使用</li>
<li>将影响因子作为输入，在函数调用关系和页面数据中找到影响面并输出</li>
</ul>
<p>整体流程如下图所示：</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E5%85%B3%E8%81%94%E5%88%86%E6%9E%90%E6%B5%81%E7%A8%8B.png" alt="整体流程"></p>
<blockquote>
<p>该工具当前只关注 JavaScript 和 Vue 技术栈的项目，所以后文都是针对这俩技术栈的情况来做的。</p>
</blockquote>
<h2>文件变化得到函数变化</h2>
<h3>获得函数变化情况</h3>
<p>  只需要对修改前后的文件内容进行对比，就可以从文件变化中获得函数变化情况。比如之前文件中有某个函数，现在没了，那就说明这个函数被删除了；如果现在和之前都有，但是函数体内容变了，那就说明函数修改了。</p>
<p>  我们将 JavaScript 代码转为抽象语法树，遍历 JavaScript AST 并收集函数节点。集合中每个函数元素的定义如下：</p>
<pre><code class="language-js">{
    name: 'function_name',
    text: 'function block content...'
}
</code></pre>
<p>  其中 name 是函数名称，这个可以在 AST 中获取到；text 是函数体的文本内容，这个似乎是无法在 AST 中拿到的，不过可以绕一下，先通过 AST 中 loc 属性拿到函数所在的起始行，然后再从文件中读取该范围内的文本内容来获得。</p>
<p>  所以只要对比这两个集合中的节点，就可以知道函数变化情况了。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E8%8E%B7%E5%8F%96%E5%87%BD%E6%95%B0%E5%8F%98%E5%8C%96%E6%83%85%E5%86%B5%E6%95%88%E6%9E%9C.png" alt="函数变化情况"></p>
<h3>获得文件变更前的内容</h3>
<p>  但是，如何获取文件变更前的文本内容呢？当然这里要限定该项目必须已使用 git 管理。我立马想到，git 在文件恢复操作时，是怎么找到旧文件内容的？可以阅读<a href="https://www.cnblogs.com/eret9616/p/11694426.html">这里</a>了解一下。我简单概述一下。</p>
<p>  在使用 git 管理项目时，根目录会生成 <code>.git</code> 文件夹，其中有个 objects 目录，我们的数据存储在该目录内以 40 位哈希值命名的文件中。</p>
<p>git 有三种类型的数据对象：</p>
<ul>
<li>提交对象： commit object</li>
<li>树对象：tree object</li>
<li>文件对象： blob object</li>
</ul>
<p>它们之间的关系如下</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/git%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1.png" alt="git数据对象"></p>
<p>  所以通过 <code>commit id</code> 可以获取到 <code>tree id</code>，通过 <code>tree id</code> 可以获取到 <code>blob id</code>，而拿到了 <code>blob id</code> 就可以通过 <code>git cat-file blob &lt;blob id&gt;</code> 读取到文件内容了，大家可以在自己本地实验一下。当然也不必那么麻烦，git 提供了递归命令一步到位，使用 <code>git ls-tree -r &lt;commit id&gt;</code> 就可以获得所有的 blob 信息。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/git_tree_blob.png" alt="git_tree_blob"></p>
<h3>获得函数调用关系</h3>
<p>函数调用关系必然需要分析整个项目的所有文件，并将每个文件中的函数信息提取出来。</p>
<p><strong>第一步：构建由所有的文件组成的「文件树」</strong></p>
<p>解析所有的文件构成文件树：</p>
<pre><code class="language-js">{
    'src/1.vue': {...},
    'src/2.js': {...}
}
</code></pre>
<p>每个文件节点中有如下属性，具体属性的内容在后文提到：</p>
<pre><code class="language-js">{
    file: 'src/1.vue',
    allFuncsInfo: {},  // 该文件中的函数信息
    importPkgs: {},  // 该文件导入的依赖
    mixin: [],  // 该（vue）文件中所使用的 mixin 文件
    templateKeyInfo: []  // （vue）文件中模板上的关键信息
}
</code></pre>
<p><strong>第二步：解析单个文件中的函数数据</strong></p>
<p>即文件节点的 <code>allFuncsInfo</code> 属性。它的数据结构示例如下：</p>
<pre><code class="language-js">{
    &quot;submit&quot;: {
        &quot;name&quot;: &quot;submit&quot;,
        &quot;filePath&quot;: &quot;src/1.vue&quot;,
        &quot;callFnList&quot;: [&quot;isExistRef&quot;, &quot;$ok&quot;]  // 该函数内调用了哪些函数
    },
     
    ...
}
</code></pre>
<p>它是 <code>key-value</code> 形式的数据：</p>
<ul>
<li>每一个 key 是该文件中定义的函数。这个可以遍历 Javascript AST 中的函数节点拿到；</li>
<li>每一个 key 对应的 value 为一个对象，其中的主要信息为 callFnList 这个数组，该数组保存函数内部调用的其他函数。这些被调用的函数可以在 AST 的函数节点中搜索 CallExpression 节点获取，不过这里需要注意处理回调函数的场景，对于回调函数需要递归遍历。</li>
</ul>
<p>这里需要对文件进行抽象语法树分析，因此可以在访问 AST 时访问 <code>importDeclaration</code> 节点，将导入的（函数等）内容确定，即文本节点中的 <code>importPkgs</code> 属性。</p>
<p><strong>第三步：解析单文件中的 template 数据</strong></p>
<p>即文件节点的 templateKeyInfo 属性。它存储的是 Vue template 中所有可能用到的变量信息。</p>
<p>以一个简单的 template 为例：</p>
<pre><code class="language-html">&lt;template&gt;
    &lt;div @click=&quot;handleClick&quot;&gt;
        &lt;child-component v-if=&quot;getValue() &gt; 1&quot; :title=&quot;getTitle&quot;&gt;&lt;/child-component&gt;
    &lt;/div&gt;
&lt;/template&gt;
</code></pre>
<p>  我们需要将 handleClick、getValue 等数据提取出来。可以使用 <code>vue-template-compiler</code> 库将 template 解析为 AST，对 AST 进行深度优先遍历将 template 中的关键信息提取出来，结果如下：</p>
<pre><code class="language-js">{
    type: 1,
    tag: &quot;div&quot;,
    vBinds: [],
    events: [
        {
            click: 'handleClick'
        }
    ],
    children: [
        {
            type: 1,
            tag: &quot;child-component&quot;,
            vIf: 'getValue',
            vBinds: [
                {
                    title: &quot;getTitle&quot;
                }
            ],
            children: [...]
        },
        ...
    ]
}
</code></pre>
<p>  一般变量的使用主要在 v-if、v-bind、事件、样式绑定、“Mustache”语法上，然而不同的人有不同的代码写法，所以要从模板上非常准确的解析出变量还是有一点难度的。</p>
<p><strong>第四步：处理 mixin 和确定函数来源</strong></p>
<p>在对整个项目中所有的文件进行上述几步处理后，此时文件树形态如下：</p>
<pre><code class="language-js">// 文件树
{
    'a.vue': {
        file: 'a.vue',
        allFuncsInfo: {
            submit: {
                name: 'submit',
                filePath: 'a.vue',
                callFnList: ['isExistRef', '$ok', 'nameValidator']
            }
        },
        importPkgs: {
            isExistRef: 'src/util.js'
            globalValidators: 'src/mixin/validators.js'
        },
        mixin: ['globalValidators'],
        templateKeyInfo: [// ....]
    },

    'b.vue': { ... },

    'c.js': { ... }
}
</code></pre>
<p>  以 a.vue 为例，它混入了 globalValidators ，从 importPkgs 中我们可以拿到 globalValidators 这个 mixin 的路径，因此可以在文件树中找到名为 <code>src/mixin/validators.js</code> 的值，将它的 allFuncsInfo 合并到 a.vue 的 allFuncsInfo 中即完成了混入。</p>
<p>a.vue 中定义了 submit 函数，该函数调用了其他函数。它所调用的其他函数可能来自于以下几种情况：</p>
<ul>
<li>从其他文件中导入的函数：从 importPkgs 中可以拿到路径</li>
<li>在该文件中定义的其他函数：allFuncsInfo 的 key 就是该文件内的所有函数，因此可以判断出一个函数是不是定义在同文件内</li>
<li>Javascript 内置函数或全局函数：来源非上面两个，这个无法找到来源，因此将来源置为 'un_know'</li>
</ul>
<p>  综上所述，此时我们只需要对文件树进行遍历，就可以确定文件中的所有函数来源。注意下面的 <code>callFnFrom</code> 字段：</p>
<pre><code class="language-js">// 文件树
{
    'a.vue': {
        file: 'a.vue',
        allFuncsInfo: {
            submit: {
                name: 'submit',
                filePath: 'a.vue',
                callFnList: ['isExistRef', '$ok', 'nameValidator'],
                callFnFrom: {  // 新增字段
                    isExistRef: 'src/util.js',
                    $ok: 'un_know',
                    nameValidator: 'src/mixin/validators.js'
                }
            }
        },
        importPkgs: {
            isExistRef: 'src/util.js'
            globalValidators: 'src/mixin/validators.js'
        },
        mixin: ['globalValidators'],
        templateKeyInfo: [// ....]
    },

    'b.vue': { ... },

    'c.js': { ... }
}
</code></pre>
<p>那么至此，完整的文件树就构建好了。</p>
<h2>搜索影响面</h2>
<p>  当文件树完成构建后，确定影响面就非常简单了。</p>
<p>  假设我修改了 <code>src/util.js</code> 中的 <code>isExistRef</code> 函数，我在文件树对每个文件的 allFuncsInfo 进行遍历，如果 callFnList 中含有 isExistRef 函数，且 callFnFrom 中该函数的来源也是 <code>src/util.js</code> 函数，就说明找到了一处函数调用路径 <code>submit -&gt; isExistRef</code>；此时继续遍历该文件的 templateKeyInfo 中的数据，如果存在 isExistRef 或 submit 函数的使用，则说明找到了一处页面影响。</p>
<p>  如果要找到多处的、完整的调用路径，那么就要不断地对找到的函数进行上述搜索，比如继续使用 a.vue 中的 submit 函数继续搜索。因此很显然这里要进行递归操作。使用队列实现的伪代码如下：</p>
<pre><code class="language-js">/**
 * treeData 为文件树数据
 * funcInfo 是给定的搜索条件，它是一个 key-value 形式的数据，包含：name 函数名；file 函数所在文件
 */
function getImpact (treeData, funcInfo) {
    let callList = [funcInfo];

    while (callList.length) {
        const curFuncInfo = callList.shift();

        // 找到哪些函数调用了该函数
        // 找到该函数影响哪些模板
        const { theyCallYou, /* 其他数据 */ } = findWhoCallMe(treeData, curFuncInfo);
        
        if (!theyCallYou.length) { // 说明这个函数没有被任何函数调用，即是调用链的尾部
            // 该调用路径到头了，这里做一些收集工作
        } else {
            callList.push(...theyCallYou);  // 存入队列中，继续遍历
        }
    }
}
</code></pre>
<p>  我们在搜索过程中不断地保存函数调用路径和页面影响，最终结果就是想要的影响面，结果的一部分截取如下图所示：</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E5%BD%B1%E5%93%8D%E9%9D%A2%E6%8A%A5%E5%91%8A.png" alt="影响面"></p>
<ul>
<li>main 字段表示影响因子，即发生变更的函数</li>
<li>callPaths 字段表示调用路径，数组中每一个元素表示一个函数（包括函数所在文件、行数）。数组中的顺序实际上是被调用关系，如图中结果所示一条调用路径是 <code>hasReplaceHostTask -&gt; jumpToAppStore</code></li>
<li>templateImpact 字段表示模板影响。这里可以看到 dom 节点的路径和影响的具体内容。例如图中所示，该调用路径会影响 <code>header.vue</code> 中的 click 事件</li>
</ul>
<p>这个报告清晰的展示了本次改动的影响面，我们只需要看一下报告就知道应该测试哪些地方。</p>
<h2>其他</h2>
<p>  上面描述了该工具实现的主要思路，在实际编码实现时有许多的细节问题，比如函数调用路径成环等。抛开细节和不同的技术栈的支持度不言，这个实现还存在一些问题。</p>
<p>  这个实现思路的时间复杂度是比较高的，在构建文件树时要对所有文件进行多次遍历；在判断影响面时，每个函数的被调用关系判断都需要遍历整个文件树。不过从现实使用来说，判断影响面这个操作一般是在功能开发完成后进行，从频率上来说不高，因此对时间并没有特别高的要求。这些时间就起来接杯水活动一下吧！</p>
<p>  同时当前对于函数的提取有一定的局限性。这里可以把函数调用分为「显式调用」和「隐式调用」。前者则是很清晰的 <code>A()</code> 或 <code>this.A()</code> 等调用；后者则是像继承、<code>$refs</code> 等。我们当前的处理很显然主要是「显示调用」。</p>
<p>  影响面报告本身是以 json 的形式展示，在拿到这个结果后，可能依然需要去代码中看一下所在的函数或者模板内容。而有些开发人员在提测或提交代码时，可能需要编写改动的测试建议，那么针对这个场景，这份报告就有一定的局限性了，因为这个被提供测试建议的对象可能是测试人员，他并不懂代码，因此如何将影响面报告做得更语义化也是值得思考的问题。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[木凳倒放当作车]]></title>
            <link>https://kkkf.vercel.app/posts/木凳倒放当作车.html</link>
            <guid>https://kkkf.vercel.app/posts/木凳倒放当作车.html</guid>
            <pubDate>Fri, 18 Mar 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>六岁由于读书，从外省的城市回到了乡下老家。虽然我总认为开始读书就是和童年告别了，但我还是把这叫作开始童年的第二阶段。</p>
<p>我家在山脚下，从院子里顺着一路延伸的田野可以看到直线距离五六百米外的大车路，但真要走过去只能走出村的路，弯弯绕绕距离要比这远很多，而我最近的同龄人便集中在那一块。村子里是没有同龄人一起玩耍的，所以大部分时间我都是一个人玩。没有人那就创造人，一人分饰多角和想象便从那时候开始成为了我从小到大的乐趣，一直到今天我都时常自言自语，自我想象。</p>
<p>家的旁边有个约莫十五米的黄土山，脚下是一块大菜园，这里属于我伯爷爷，他精心的用竹子编织了篱笆将这里围得严实也防不住我的“轻功”。我踩着软绵的黄沙从山底冲向山顶，我每一脚都要被黄沙拉扯入内，时日久了我便练就飞檐走壁和快速爆发之术。我在山里“浪迹江湖”，捡到了许多棍子，它们有些短小凌厉，握感刚好，我有了一把绝世好剑；有些又长又直，坚硬无比，我有了一根如意神棍。我在土墙上凿了很多洞，那是我的武器库，那里有我的神剑神棍这些“明器”，也有用又薄又扁的飞镖制作的“暗器”，偶尔我也会制造一些“毒药”，有一次我差点让自己的毒药毒死了。那是春笋泛滥的时候，我拔了许多小笋子作为我的药引，存于可乐瓶中以井水浸泡七七四十九日。日子到了，我揭开了瓶盖缓缓凑近，我闻到了我至今都记得的味道，让我忍不住现在打下这些字的时候依然干呕了两下。我在这里练就了我的许多武功，同时也进行了许多“战役”。每次坐在那里，起风了，风吹动叶声响我便感觉有动静，遂恍惊起而制杖，冲上黄山俯瞰底下围攻我的千军万马，定睛怒视，大声疾呼，以壮士断腕的决心杀向敌方；每次当我节节败退，我便握紧双拳，闭目大喊，我感觉身上一股气浪涌出，此时风吹起漫天黄沙，地动山摇，我仿佛下一秒就要变成超级赛亚人。后来我踩死了许多菜，纵然我浑身武功也挡不住伯爷爷的“疯魔乱剑”，被他抓在手上下了“再被抓到就格杀勿论”的最后通知。正所谓人在江湖飘谁能不挨刀，现在黄土山被推了盖了房子，连江湖都没了。</p>
<p>黄土山是我的武侠乐园，我在这里练就了我的灵敏，我的爆发，让我在日后学业生涯的每一次跑步运动中从未落于下风；让我总能惊奇的在长人林立的篮板下摘下篮板球。我也在这里混迹武林，没有找到侠肝义胆，只收获了奇奇怪怪的自信心——我从小到大都觉得自己与众不同能成大事，尤其是我其实是超级赛亚人这件事我更是深信不疑。</p>
<p>农村都是这样的凳子，我小时候很喜欢把它倒在地上，人坐在靠背上，双手握在“屁股板”上，这便是我的第一辆车。它载着我和奶奶聊天，它载着我跟路人打招呼，它载着我听着一年又一年的蝉鸣长大，却载不了十八岁那年的我去大学报道。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E6%9C%A8%E5%87%B3.jpeg" alt="木凳"></p>
<p>很多人说大学是新的开始，“从此故乡只有冬夏再无春秋”，我常觉得这都是“微词”，自此故乡可能都没有了春夏秋冬。我家对面有个小瀑布，水顺山而下，坠下瀑布流入小河，流出村子汇进三江五湖。现在瀑布早已不响，小河早已干枯，田地也荒芜，村子里的人也在地图上四零八落。留守的老人坐在院子里晒太阳，半里的太阳下都没有人影，偶尔的车轮声让她仿佛年轻了几十岁般兴奋的疾步张望。</p>
<p>那是一个下午，我的视线从电脑前移出窗外看到了很好的阳光，我不由自主的笑了，这么好的天气我却在打工，请问这合理吗？「木凳倒放当作车」就在这时钻入了我的脑中。我时常会想到这句乍一看奇奇怪怪的话，心中有很多的情绪——有开心，有安心，有丝丝苦涩，有淡淡怀念。</p>
<p>它是一个又一个那时无聊但现在开心的夏天回忆；</p>
<p>它像心理学哲学一样，安我工作和生活烦恼、躁动的心；</p>
<p>它让我产生许多关于故乡、农村、留守老人的苦涩思考和难过；</p>
<p>它是我回不去的童年，是年轻不回去的奶奶和父辈，是停不下来的长大。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[你也可以变成光]]></title>
            <link>https://kkkf.vercel.app/posts/你也可以变成光.html</link>
            <guid>https://kkkf.vercel.app/posts/你也可以变成光.html</guid>
            <pubDate>Thu, 06 Jan 2022 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    我们常说的工作，从狭义上来说本质上是雇佣，一方付出劳动力从另一方获取报酬，双方的关系受劳动法保护。双方的关系应当是平等的，但是我感觉到市场的劳动者越来越奴性，面对资本家的笑面狼牙不敢发出自己心里真实的声音。</p>
<p>    这个市场是资源有限而劳动者过剩的资本市场，劳动者的可替代性很强，作为一个普通人，我们没有足够的勇气和能力去承担对抗的最坏后果。我理解不敢发声的原因，发出声音是危险的，所以保持沉默；天空黑暗了，那就摸黑生活。</p>
<p>    总要有人发光，所以尽管知道结果如何的你还是站出来了，但没人和你一起你发声，孤立无援。我敢保证 96 个人在心里佩服你的勇敢热情，内心波动但不敢附和，2 个人蜷伏于墙角为黑暗辩护，1 个人嘲讽你，1 个人一脸麻木漠不关心。</p>
<p>    不知道你会怎么想那些为黑暗辩护、嘲讽你、对是非茫然的人，但我会笑他们的奴性，他们的卑微，他们的扭曲。你很棒，你的行为是十月革命的一声炮响，让他们革一革自己心里的奴性。要是有一个人附和了你，那请你和 ta 坐上 1921 年的江上红船，泛舟点亮燎原星星之火。</p>
<p>    我说要有光，于是你成为了光。我为我的女朋友骄傲，你很棒。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E4%BD%A0%E4%B9%9F%E5%8F%AF%E4%BB%A5%E5%8F%98%E6%88%90%E5%85%89.jpeg" alt="你也可以变成光"></p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[代码质量检测工具服务端设计]]></title>
            <link>https://kkkf.vercel.app/posts/代码质量检测工具服务端.html</link>
            <guid>https://kkkf.vercel.app/posts/代码质量检测工具服务端.html</guid>
            <pubDate>Tue, 28 Dec 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>    今年很多时间都在开发和维护<a href="https://cocacolf.vercel.app/blog/%E5%BC%80%E5%8F%91%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%B5%8B%E5%B7%A5%E5%85%B7/">代码质量检测工具</a>，到现在已经稳定运行了两个多月。之前写过它的 cli 端，本文是关于支撑该 cli 端以及质量报告平台的服务端的设计。它由设计文档简化精炼而来，作为个人记录和回顾所用。代码质量检测服务端开发的那段时间是我非常高效、聪明、有激情的一段时间，我很感谢它带给我的快乐。每个软件开发工程师在自己的编码生涯里都会有自己的代表作，它不一定有多厉害。你报之它以你的时间、智慧、苦恼，它报之你以喜悦和成就感，它让你感知到自己的能力和成长，让你在某些场合下嘴里滔滔不绝，眼里四射光芒。我喜欢这样的作品，我喜欢这样的过程，我喜欢这样的回忆。</p>
<hr>
<h1>需求分析</h1>
<p>    如果有阅读过之前写的<a href="https://cocacolf.vercel.app/blog/%E5%BC%80%E5%8F%91%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%B5%8B%E5%B7%A5%E5%85%B7/">代码质量检测工具</a>可能会对 bcode 有印象，但这里我还是简单描述一下：代码质量检测工具的起点是一个命令行工具，安装且在代码目录下执行 <code>bcode check [文件(夹)]</code> 后会扫描代码并生成一系列报告文件提供给服务端，服务端承载后续的工作，同个项目的扫描记录和数据分析会以报告汇总的形式在 Web 平台上查看。</p>
<p>如下导图所示，需求按照三种场景来进行分析。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E5%9C%BA%E6%99%AF.png" alt="需求分析"></p>
<p>由于无法从内网捕获截图演示，这里对标注的几点进行扩展解释，序号和导图中一致。</p>
<ol>
<li>
<p>为了让工具更好的自动化，此工具将和 CICD 结合使用。然而许多产品线的流水线环境中 Node 版本过低，因此提供了远程扫描功能。</p>
</li>
<li>
<p>bcode 的使用很简单，一行命令即可。但会存在较复杂的场景，比如某些文件要忽略检查；甚至每个维度都有单独的忽略文件规则，因此为了方便使用，可以将这些规则定义在约定的配置文件(bcode.config.js)中，执行命令时工具会去寻找和读取该配置。我们在 web 平台上项目设置里也提供了这些参数的设置，因此这个配置文件省略，每次工具扫描时会从服务端获取该项目的配置。（还是很贴心的吧）</p>
</li>
<li>
<p>bcode 有个维度是拼写检查，那么有很多单词是词典非法但大众默认合法的（比如 nums），所以有项目词库这种需求出现。每次扫描时服务端会下发单词词库，cli 端扫描时会将这些单词当作合法单词。</p>
</li>
<li>
<p>扫描结束后，会根据配置的通知方式将报告结果进行通知。</p>
</li>
</ol>
<p>将需求进行收敛后，得到如下的功能点：</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E5%8A%9F%E8%83%BD%E7%82%B9.png" alt="功能点"></p>
<blockquote>
<p>大部分功能都是比较简单的，下面内容聚焦在几个主要的功能模块上。</p>
</blockquote>
<h1>整体静态结构</h1>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E9%9D%99%E6%80%81%E7%BB%93%E6%9E%84.png" alt="整体静态结构"></p>
<table>
   <tr>
      <td>模块</td>
      <td>说明</td>
   </tr>
   <tr>
      <td>扫描模块</td>
      <td>
        主要处理扫描相关的工作：
        <ol>1. Cli 扫描上报的文件处理</ol>
        <ol>2. 扫描数据处理</ol>
        <ol>3.远程扫描</ol>
      </td>
   </tr>
   <tr>
      <td>词库管理</td>
      <td>
      主要处理词库相关是事情：
        <ol>1.词库上报、删除</ol>
        <ol>2.项目词库同步到平台词库</ol>
      </td>
   </tr>
   <tr>
      <td>项目管理</td>
      <td>
        主要工作是：
        <ol>1.将相同扫描项目的记录汇总</ol>
        <ol>2.处理项目的扫描配置</ol>
      </td>
   </tr>
   <tr>
      <td>用户管理</td>
      <td>处理用户登录、记录用户信息等</td>
   </tr>
   <tr>
      <td>定时任务</td>
      <td>
        所有的定时任务在这里注册和处理：
        <ol>1.文件的定期清理</ol>
      </td>
   </tr>
</table>
<h1>流程设计</h1>
<h2>扫描整体流程</h2>
<p>扫描分为本地扫描和远程扫描两种场景：</p>
<ul>
<li>本地扫描：用户使用 cli 扫描，将扫描结果文件上报服务端处理。</li>
<li>远程扫描：用户上传源码到服务端，服务端在服务器上模拟本地扫描。</li>
</ul>
<h3>静态结构</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E6%89%AB%E6%8F%8F%E9%9D%99%E6%80%81%E7%BB%93%E6%9E%84.png" alt="扫描静态结构"></p>
<h3>处理流程</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E6%89%AB%E6%8F%8F%E5%A4%84%E7%90%86%E6%B5%81%E7%A8%8B.png" alt="整体扫描流程"></p>
<table>
   <tr>
      <td>序号</td>
      <td>说明</td>
   </tr>
   <tr>
      <td>1</td>
      <td>远程扫描和本地扫描的判断，已在 4.2.1远程扫描方案选型中讲述，通过 body 参数来判断</td>
   </tr>
   <tr>
      <td>2</td>
      <td>进行远程扫描，详细流程在 4.4.2 中讲述</td>
   </tr>
   <tr>
      <td>3</td>
      <td>远程扫描本质是在服务端模拟本地扫描，因此此处相当于本地扫描完成，又将上传扫描结果文件到服务端</td>
   </tr>
   <tr>
      <td>4</td>
      <td>该结束流程是指扫描任务结束，数据分析等都已完成，用户可以在平台上通过编号查询报告。消息通知就无需用户等待了。</td>
   </tr>
   <tr>
      <td>5</td>
      <td>触发消息发送事件，如果需要发送消息通知，则发送消息</td>
   </tr>
</table>
<h2>远程扫描流程</h2>
<h3>静态结构</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E8%BF%9C%E7%A8%8B%E6%89%AB%E6%8F%8F%E9%9D%99%E6%80%81%E7%BB%93%E6%9E%84.png" alt="远程扫描静态结构"></p>
<h3>处理流程</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E8%BF%9C%E7%A8%8B%E6%89%AB%E6%8F%8F%E6%B5%81%E7%A8%8B.png" alt="远程扫描流程"></p>
<table>
   <tr>
      <td>序号</td>
      <td>说明</td>
   </tr>
   <tr>
      <td>1</td>
      <td>远程扫描使用 bcode 库(cli)来扫描，这里采用的方案是在源码目录下创建一个扫描脚本，通过 fork 子进程的方式执行该脚本扫描</td>
   </tr>
   <tr>
      <td>2</td>
      <td>远程扫描的任务会加到任务队列中，每一个任务有唯一的任务 id</td>
   </tr>
   <tr>
      <td>3</td>
      <td>为了避免客户端持续等待扫描完成，我们可以先将任务 id 返回给客户端，结束本次请求。客户端后续可以通过任务 id 查询任务完成状态</td>
   </tr>
   <tr>
      <td>4</td>
      <td>开启子进程，执行扫描脚本</td>
   </tr>
   <tr>
      <td>5</td>
      <td>给子进程发送扫描配置数据，扫描配置数据是 cli 端在执行命令行时的参数信息</td>
   </tr>
   <tr>
      <td>6</td>
      <td>扫描任务完成后，更新记录在 Redis 中的任务 id 的信息，此时客户端通过任务 id 查询时就可获悉任务结束</td>
   </tr>
</table>
<h3>关键算法描述</h3>
<p>    之前在<a href="https://cocacolf.vercel.app/blog/nest.js%E4%BB%BB%E5%8A%A1%E9%98%9F%E5%88%97%E5%AE%9E%E7%8E%B0%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%AB%E6%8F%8F/">这篇文章</a>有记录。</p>
<h2>结果处理评分流程</h2>
<h3>静态结构</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E8%AF%84%E5%88%86%E9%9D%99%E6%80%81%E7%BB%93%E6%9E%84.png" alt="评分静态结构"></p>
<h3>处理流程</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E8%AF%84%E5%88%86%E6%B5%81%E7%A8%8B.png" alt="评分处理流程"></p>
<h2>平台词库自动维护流程</h2>
<p>平台词库出现的目的是为了解决：</p>
<ol>
<li>提高项目冷启动（第一次使用）时拼写检查扫描的准确度</li>
<li>减少项目需要上报的合法词汇的数目</li>
</ol>
<p>服务会在两个地方进行平台词库的数据维护：</p>
<ol>
<li>平台词库模块启动时</li>
<li>有项目词汇上报请求时</li>
<li>有项目词汇删除请求时</li>
</ol>
<h3>处理流程</h3>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/bcode-server%E5%B9%B3%E5%8F%B0%E8%AF%8D%E5%BA%93%E7%BB%B4%E6%8A%A4%E6%B5%81%E7%A8%8B.png" alt="平台词库维护处理流程"></p>
<table>
   <tr>
      <td>序号</td>
      <td>说明</td>
   </tr>
   <tr>
      <td>1</td>
      <td>词汇管理模块启动时开始工作，它只会工作一次</td>
   </tr>
   <tr>
      <td>2</td>
      <td>当有词汇上报请求时开始工作</td>
   </tr>
   <tr>
      <td>3</td>
      <td>在服务投入生产之前，先通过扫描成熟的大项目，已经提取出了许多合法词汇以配置文件的形式放置在项目中</td>
   </tr>
   <tr>
      <td>4</td>
      <td>
        平台词库的整理操作：
        <ol>1.平台词库的整理是异步的，通过事件触发。所以在该步骤之前，先响应用户的请求，再整理词库</ol>
        <ol>2. 如果上面流程传来的词汇是数组形式，则该步骤的后续操作需要循环数组中每个词汇进行处理</ol>
      </td>
   </tr>
   <tr>
      <td>5</td>
      <td>当有项目词汇删除时，我们需要对删除词汇进行判断，如果该词汇被删除后不再满足平台词汇中存在的条件（至少3个项目存在），则需要把它过滤出来</td>
   </tr>
   <tr>
      <td>6</td>
      <td>将 5 中的词汇从平台词库中删除</td>
   </tr>
</table>
<h1>各种机制设计</h1>
<h2>可调试性设计</h2>
<p>主要依托以下几种方式提高应用的可调式性。</p>
<p><strong>机制一：日志齐全、分级、输出内容可以动态调节。</strong></p>
<ol>
<li>
<p>在应用的各个关键阶段，均有日志输出。</p>
</li>
<li>
<p>日志分级和携带标识</p>
</li>
</ol>
<p>日志分为以下几个级别：</p>
<ul>
<li>普通日志</li>
<li>告警日志</li>
<li>错误日志</li>
<li>调试日志</li>
</ul>
<p>此外，日志输出应该携带有标识，这样可以方便我们进行日志过滤，以及特定的追踪某一次扫描的问题。标识分为两种：</p>
<ul>
<li>携带有模块名</li>
<li>如果是属于扫描模块，携带有扫描任务编号</li>
</ul>
<ol start="3">
<li>日志输出可以调节</li>
</ol>
<p>日志的输出可以通过环境变量来控制，这样可以在排查问题时排除其他日志的干扰。</p>
<p><strong>机制二：上传的文件保留7天，可复现问题。</strong></p>
<p>上传的扫描结果或源码文件均会在服务器上保留 7 天，因此如果某个扫描出问题的话，可以拿到当时的文件信息，通过 postman 等请求工具模拟当时的过程进行问题复现。</p>
<p><strong>机制三：模块之间界限要清晰，数据流明确</strong></p>
<p>1.架构上：由于 Web 服务框架使用的 Nest.js ，本身分层非常清晰，从数据到异常都会有对应层进行处理；<br>
2.业务上：服务中的处理函数尽可能抽成纯函数，让执行过程和数据流清晰。</p>
<h2>可测试性机制分析及设计</h2>
<ol>
<li>核心函数抽成单个的纯函数，方便单独拿出来测试或进行单元测试</li>
<li>服务都以 API 的形式对外，可以用 postman 等工具发送请求进行测试</li>
<li>不同维度的检查和数据处理都拆成了单个函数，同时可以通过标识符进行检查与否的控制，因此很方便对单个维度进行测试</li>
<li>不同的业务拆分成不同的服务进行模块化，该服务只做自己的事情，要进行其他服务操作需要调用 API 。</li>
<li>有效的日志：分级、动态开关</li>
</ol>
<hr>
<p>    以上就是一个代码质量检测平台的主要模块设计，由于篇幅和其他原因这里没有涉及到数据库的设计，在实际开发过程中细节还是比较多的。</p>
<p>    代码质量检测服务端开发的那段时间是我非常高效、聪明、有激情的一段时间，我很感谢它带给我的快乐。每个软件开发工程师在自己的编码生涯里都会有自己的代表作，它不一定有多厉害，你报之它以你的时间、智慧、苦恼，它报之你以喜悦和成就感，它让你感知到自己的能力和成长，让你在某些场合下嘴里滔滔不绝，眼里四射光芒。我喜欢这样的作品，我喜欢这样的过程，我喜欢这样的回忆。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[颈椎病缓解和预防]]></title>
            <link>https://kkkf.vercel.app/posts/颈椎病缓解和预防.html</link>
            <guid>https://kkkf.vercel.app/posts/颈椎病缓解和预防.html</guid>
            <pubDate>Thu, 23 Dec 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    最近感觉不太好，几个问题：</p>
<ul>
<li>左肩背部有点酸痛</li>
<li>嗓子有异物感，尤其是睡觉躺着略微的窒息感影响呼吸</li>
<li>早上起来后右手酸软无力，一整天都有一种麻的感觉</li>
<li>脖子右边有痛感，且摸起来明显比左边要偏僵硬</li>
<li>有点晕，整天都有犯困的感觉，但实际上睡眠时间充足</li>
</ul>
<p>    我怀疑是颈椎病，于是前天上午查了很久相关资料，果然印证了猜测，这是中等程度的颈椎病现象。手臂酸麻是因为神经压迫；大脑昏沉是压迫血管影响脑部供血。看了一下别人的分享，发现去医院可能有以下一些治疗方案：</p>
<ul>
<li>先拍片，核磁共振/CT，确定严重程度和劲椎病<a href="https://zh.wikipedia.org/wiki/%E8%84%8A%E6%A4%8E%E9%97%9C%E7%AF%80%E9%80%80%E5%8C%96#%E5%88%86%E7%B1%BB%E5%8F%8A%E7%97%87%E7%8A%B6">类型</a></li>
<li>开膏药（普遍反映效果不怎么样）</li>
<li>经常去医院进行牵引加推拿，时间周期比较长</li>
<li>叫你回去多锻炼；跳颈椎康复操</li>
<li>动手术（非常严重的情况下）</li>
</ul>
<p>    同时也了解到颈椎病基本只能缓解，完全康复很难。</p>
<p>    我的成因主要还是因为长期以来的部分不良生活姿势：</p>
<ul>
<li>最大的罪魁祸首是使用手机的时候头低得太厉害</li>
<li>之前偶尔头靠在床沿呈 L 型姿势追剧</li>
<li>办公时颈（头）前倾（还真就是程序“猿”呗）</li>
</ul>
<p>    颈椎生理曲度发生变化是渐进累积的过程，但是最近一周突然暴发问题，我想了想有几个可能的影响：</p>
<ul>
<li>前不久打篮球别人冲撞时猛烈撞击了我的头部，当时脖子快速大幅度扭转造成损伤</li>
<li>搬新家后床垫偏软不合适，在之前我一直睡的硬床</li>
<li>显示器留给女友用，我在家用笔记本时喜欢在餐桌上，头需要始终低着，而最近两周使用的频率比较高</li>
<li>最近午休没有躺着睡觉，而是趴着侧着头睡</li>
</ul>
<p>    至此我认为无需前往医院拍片，基本可确定我是颈椎病，既然基本都是通过运动康复，那我打算不妨先进行自我运动康复。我先根据个人症状搜索了一番，找到了一个和我相似症状的<a href="https://zhuanlan.zhihu.com/p/378084403">分享</a>；同时在 B 站上找到了一个评价还不错的<a href="https://www.bilibili.com/video/BV1Yb411b7nd">颈椎康复操</a>和<a href="https://b23.tv/mWGc8V9">肩颈舒缓瑜伽</a>，每天跟着做康复训练，希望一周后我的症状可以有所缓解，长期坚持后症状可以彻底缓解不影响生活。</p>
<p>    想起一句话：程序员从入门到颈椎病康复。因此我在程序员较多的 v2ex 上搜索了一些相关帖子，其中有一些有用的信息：</p>
<ul>
<li>每天都要运动！颈椎不单单和这一个地方有关，还和肩部、背部等相互关联，因此多运动，其他地方增强力量相互配合</li>
<li>暂时可以找找健身或瑜伽类视频，活动颈部，肩部，背部，主要是压迫神经导致供血不足，一定要注意脖子保温</li>
<li>羽毛球和游泳是比较好的康复颈椎的运动，尤其是游泳推荐人数比较多</li>
<li>换荞麦枕</li>
<li>对付手臂发麻：拉公交拉环、吊环、拉力带</li>
<li>佩戴颈托矫正低头等不良姿势，同时也起到一定的牵引作用</li>
</ul>
<p>    至今天我已经锻炼了两天，这两天打了两场篮球赛；在办公室就像个“练功人士”般较高频率的进行了颈椎康复操的锻炼；睡前进行肩颈放松瑜伽拉伸。锻炼的第一天手臂发麻现象好转；喉咙异物感消失。今天（第二天）脖子右边疼痛减弱，而且摸起来明显软化到和左边一致，可以摸出我胖胖的肉肉。</p>
<p>    这件事让我意识到不良生活姿势的危害性和正确姿势的重要性，颈椎病和略微驼背的问题在现代年轻人身上或多或少存在，这些长期生活姿势造成的影响也需要长期的矫正才能还原，而一旦新姿势形成，改变的过程又十分的痛苦和难以坚持。</p>
<p>    回到过去，一起做个听话的学生：坐得端正，挺得笔直，亲近自然，打闹奔跑。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[制定你的GitLab流水线任务]]></title>
            <link>https://kkkf.vercel.app/posts/合入npm包到CICD.html</link>
            <guid>https://kkkf.vercel.app/posts/合入npm包到CICD.html</guid>
            <pubDate>Wed, 13 Oct 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    最近要将开发的 npm 包合入 CICD 流水线里，摸索着完成了这个搭建。</p>
<blockquote>
<p>本文默认你已了解 CICD 是什么，并且 GitLab 具备必要条件：如可用的 Runner。</p>
</blockquote>
<p>    一个流水线服务需要以下几个基本要素：</p>
<ul>
<li><code>.gitlab-ci.yml</code> 文件</li>
<li>可用的 <code>Runner</code></li>
<li>执行某个流水线任务的脚本文件</li>
</ul>
<h2>编写配置文件和脚本</h2>
<p>    <code>.gitlab-ci.yml</code> 文件是必须的，它定义了流水线任务如何执行。而我们的任务肯定要在某个环境中执行，Runner 就是一个具备运行条件的环境，可能是某个服务器，也可能是某个 Docker 容器，这里不涉及 Runner 的搭建，因此默认已经有可用的 Runner。</p>
<p>下面是一个示例文件：</p>
<pre><code class="language-yml">stages:
  - code_scan
  - eslint_check

代码扫描:
  stage: code_scan
  tags:
    - code-check
  script:
    - cd ${CI_PROJECT_DIR}
    - chmod +x ./.gitlab/ci/codeCheck.sh
    - ./.gitlab/ci/codeCheck.sh encore
  allow_failure: true
  when: always
  only:
    variables:
      - $UEDC_MERGE_REQUEST_APPROVED == &quot;true&quot;

eslint检查:
  stage: eslint_check
  tags:
  	- eslint-check
  script:
  	- ./.gitlab/ci/eslintCheck.sh encore
  only:
  	- pushes
</code></pre>
<p><strong>stages</strong></p>
<p>    它定义了整个流水线的阶段，你的流水线的任务会按照这个阶段的编写顺序一步步来执行。</p>
<p>    接下来就需要指定 Job，即流水线要做的事情。比如我这里就制定了 <code>代码扫描</code> 和 <code>eslint检查</code> 两个任务。 以 <code>代码扫描</code> 为例来讲述 Job 的内容。</p>
<p><strong>stage</strong></p>
<p>    它表示 <code>代码扫描</code> 这个任务属于哪一个阶段，前面说了 stages 会按照定义的顺序来执行，也就是说如果多个 Job 都属于同一个 stage，那么这些 Job 是同时执行的。</p>
<p><strong>tags</strong></p>
<p>    它用来指定这个 Job 在哪个特定的 Runner 上执行，因为不同的任务可能需要不同的运行环境，一个公用的 Runner 无法满足。</p>
<p><strong>script</strong></p>
<p>    它定义了这个 Job 具体要做的事情。如果比较简单，那么就可以直接在这里写上脚本命令；如果比较复杂，则建议将其单独写在某个文件里。这里是写在了 <a href="http://codeCheck.sh">codeCheck.sh</a> 这个 shell 文件里。</p>
<p><strong>allow_failure</strong></p>
<p>    它表示这个 Job 是否允许失败，即它是否会阻塞剩余的 CI 执行</p>
<p><strong>when</strong></p>
<p>    它指定这个任务的执行时机。有以下字段可配置：</p>
<ul>
<li>on_success: 前面任务成功</li>
<li>on_failure: 前面任务失败</li>
<li>always: 不管怎么样都执行</li>
<li>manual: 自己手动在 GitLab页面上去点击触发任务</li>
</ul>
<p><strong>only</strong></p>
<p>    这个字段定义 Job 在什么时候被创建，注意这个【创建】，说明从过程上来说它是在 when 之前。</p>
<p>    它的内容还蛮多的，详细可以见<a href="https://docs.gitlab.com/ee/ci/yaml/#only--except">这里</a>。简单来说你可以指定：</p>
<ul>
<li><code>分支名</code>：比如配置为 <code>/^test-.*$/</code>，则该 Job 只会在 test- 开头的分支下运行</li>
<li><code>关键字</code>：比如 eslint 检查就是使用了 pushes 关键字，表示在执行 git push 时创建</li>
<li><code>变量条件</code>：如代码扫描就是通过 UEDC_MERGE_REQUEST_APPROVED 这个环境变量来决定是否创建 Job</li>
</ul>
<p>    顺带一提，示例文件中代码扫描任务 script 中 <code>CI_PROJECT_DIR</code> 是 GitLab的<a href="https://docs.gitlab.com/ee/ci/variables/">环境变量</a>，这里指的是仓库存放代码的路径，比如 <code>CI_PROJECT_NAME</code> 是当前仓库名。</p>
<p>    接下来便是编写各任务的脚本代码，这个当然就是具体问题具体编写了。</p>
<pre><code class="language-shell">#!/bin/sh

echo &quot;这里是代码扫描任务&quot;

npm i codeScan -D

codeScan check ./src

if [ $? != 0 ]; then 
    echo &quot;代码扫描失败&quot;;
    exit 1;
fi

echo &quot;代码检查结束&quot;
</code></pre>
<p>    至此一个可用的流水线服务就写完了，还是很简单的。</p>
<h2>结合 GitLab webhook</h2>
<p>    在实际的搭建中，可能由于 GitLab版本原因导致某些配置/字段不支持，因此需要结合 webhook 来解决。这里以我遇到的问题为例。</p>
<p>    我期望代码检查是在<strong>提交合并请求的时候执行</strong>，要等扫描没有问题或者扫描出来的问题被解决的情况下，该分支才允许被合入。Gitlab 可以通过配置 only 字段来解决这个实现这个操作：</p>
<pre><code class="language-yml">only:
	- merge_request
</code></pre>
<p>    但还是由于版本原因，该字段不支持，因此只能另辟蹊径。</p>
<p>    在 GitLab中，我们可以对项目配置 webhook，配置路径在项目左侧菜单中 <code>设置-&gt;集成</code>。绑定项目触发的事件和一个自己的 web 服务后，GitLab 会在这个事件发生时向 web 服务发送 post 请求。比如我勾选触发器事件为<code>合并请求事件</code>，配置服务链接地址为 <code>https://test.com/merge_reqest</code>，那么就可以实现在提交合并请求时告知我的 web 服务，同时 GitLab 会携带很多有用的数据过来供我操作，那么通过配置 webhook 以及结合 GitLab API，便可以在自己的 web 服务中实现代码扫描流水线任务的触发。</p>
<p>    由于 webhook 需要安全令牌来验证接收信息的有效性，它将通过 HTTP 头的 X-Gitlab-Token 发送。这个 token 是在我们配置触发器时要配置的，它的生成方法是：</p>
<ul>
<li>先在 GitLab 该项目到左树菜单中 <code>设置-&gt;CI/CD-&gt;流水线触发器-&gt;增加触发器</code>，此操作会生成一个令牌</li>
<li>在 <code>设置-&gt;集成</code> 页面中，将刚刚生成的令牌配置为 webhook 的安全令牌</li>
</ul>
<p>    接下来实现这个 web 服务。 web 服务的路由为 <code>/merge_request</code>，该路由处理逻辑的伪代码如下：</p>
<pre><code class="language-javascript">// headers 和 body 数据是由 GitLab 的请求带过来的数据，根据具体实现该服务的框架来获取

const token = headers['x-gitlab-token'];
const { 
	projectId,    // 该项目的id
	source_branch,    // 当前提交合并请求的分支
	target_branch,    // 要合入的目标分支
} = body;

// 要添加到 CI 中的变量，比如代码扫描中要用到的 UEDC_MERGE_REQUEST_APPROVED 字段
const variables = 'variables[UEDC_MERGE_REQUEST_APPROVED]=true&amp;variables[UEDC_MERGE_REQUEST_IID]=123';

// 触发流水线的 api
const triggerPipeline = `[你的gitlab地址]/api/v4/projects/${projectId}/trigger/pipeline?ref=${source_branch}&amp;token=${token}&amp;${variables}`;

// 发送请求
http.post(triggerPipeline);
</code></pre>
<p>这样便可以在提交合并请求时通过 web 服务来触发流水线执行。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[Nest.js任务队列实现远程代码扫描]]></title>
            <link>https://kkkf.vercel.app/posts/Nest.js任务队列实现远程代码扫描.html</link>
            <guid>https://kkkf.vercel.app/posts/Nest.js任务队列实现远程代码扫描.html</guid>
            <pubDate>Mon, 11 Oct 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    之前开发的<a href="https://cocacolf.vercel.app/blog/%E5%BC%80%E5%8F%91%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%B5%8B%E5%B7%A5%E5%85%B7/">代码质量检测工具</a>存在一个问题：在大部分时候开发业务代码已经够费神了，开发者并不会主动使用这个工具给代码打分，而且万一检查出来很多问题还得改，这不是没事找事吗？但是<del>你们不使用我怎么完成我的OKR呢？</del>为了能够让工具的使用更加无感知，结合 CICD 是一种比较好的做法，每次提交合并请求就自动检查，有问题这个请求就不能被合入，再添以邮件通知告警等功能，则可对项目实现一次配置，持续监察。</p>
<p>    问题在于该工具的使用条件需要满足 Node 版本大于 12，调研后发现许多产品线的 Runner 中打包项目的 Node 版本并不满足此要求，因此<strong>远程扫描</strong>这个功能便应运而生。</p>
<p>远程扫描功能整个过程是：</p>
<ul>
<li>cli 将本地的代码发送至服务端进行扫描</li>
<li>服务端扫描代码（等价于在本地扫描代码，只是执行环境在服务端），返回扫描结果</li>
</ul>
<p>    很显然如果代码量较大，则压缩传输和服务端扫描都需要花费一定的时间，但使用者关注的只是报告结果，这个结果过会儿去报告平台上去查阅即可，因此扫描的这部分时间并不值得等待。所以服务端的接口在获取到上传文件后便可向客户端返回响应告知其上传成功即可，扫描的工作在后台执行。像这种场景，任务队列便是解决这种类型问题的好办法。</p>
<p>    在 Nest.js <a href="https://docs.nestjs.com/techniques/queues">Queues</a> 中了解到 <a href="https://github.com/OptimalBits/bull">bull</a>，它基于 Redis 实现任务队列。下面是 Nest.js 中使用 bull 实现任务队列的一个例子。</p>
<p>先定义任务队列管理器：</p>
<pre><code class="language-js">import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';

@Processor('bcodeScan')
export class ScanCodeProcessor {

 // 某个任务队列
 @Process('startRemoteScan')
 async startScan (job: Job) {
 	// 获取该任务处理相关的数据
 	const { dataExample } = job.data;
	
	// do something to handle task...
	
	// 这里返回值会保存在 Redis 该记录的 returnValue 中，因此可以通过某个标识去获取任务结果
	return xxxx;
 }
}
</code></pre>
<p>在业务中使用该任务处理器：</p>
<pre><code class="language-js">import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';

export class RemoteScanService {
  // 注入任务管理器
  constructor(@InjectQueue('bcodeScan') private readonly scanQueue: Queue) {}
  
  async handleUploadSource(fileData: Buffer, bodyParams: UploadSourceParams) {
	this.scanQueue.add(
        'startRemoteScan',    // 将任务添加到哪个队列
        {
          dataExample: 'test'    // 数据
        },
        {
          jobId: +new Date(),    // 自定义任务id
        },
      );
  }
}
</code></pre>
<p>在 Modules 中注册队列：</p>
<pre><code class="language-js">import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';

@Module({
	// ...
	
	imports: [
		BullModule.registerQueue({
		  name: 'bcodeScan',
		  redis: {
			host: 'localhost',
			port: 6379,
		  },
		}),
	]
})
</code></pre>
<p>比较简单的就实现了一个任务队列。回到问题本身，这里如何实现远程扫描？</p>
<p>我的解决方案：</p>
<ul>
<li>当待扫描源码上传且将其解压到指定目录后，在待扫描源码根目录下生成 <code>bcodeExcute.js</code> 文件，它的内容则是将开发者在本地执行的命令行转为 Javascript 代码</li>
<li>通过子进程的方式执行 <code>bcodeExcute.js</code> 来扫描</li>
</ul>
<pre><code class="language-js">// 服务端处理上传源码的逻辑
async handleUploadSource(fileData: Buffer, bodyParams: UploadSourceParams) {
	
	// 解压源码文件，获取目标目录
	const targetDir = depressFile(fileData);
	
	// 在待扫描源码根目录下生成 bcodeExcute.js 文件
	genScanScode(targetDir);
	
	// 任务队列新增任务
	this.scanQueue.add(
        'startRemoteScan',    // 将任务添加到哪个队列
        {
          targetDir    // 待扫描源码目录
        },
        {
          jobId: +new Date(),    // 自定义任务id
        },
      );
  }
</code></pre>
<p>我们知道开发的命令行工具的原理是解析外部输入命令，调用功能函数来执行操作，因此此处只需要调用 bcode 暴露出来的 run 方法，即可将命令行转为同等功能的 Javascript 代码来执行。生成的 <code>bcodeExcute.js</code> 内容如下：</p>
<pre><code class="language-js">// 生成的bcodeExcute.js
const genScanScode = (targetDir: string, scanDir: string, config: string) =&gt; {
  const code = `
  const run = require('bcode');

  process.on('message', async ({params}) =&gt; {
    const scanMark = await run(
      JSON.parse(params),
      true,
    );

    process.send(scanMark);
  });
  `;

  fs.writeFileSync(path.resolve(targetDir, 'bcodeExcute.js'), code);
};
</code></pre>
<p>接下来在该任务队列的处理逻辑里，实现父子进程通信执行脚本文件。</p>
<pre><code class="language-js">import * as treeKill from 'tree-kill';
import * as Redis from 'ioredis';
const redisClient = new Redis({
  db: 0,
});

@Process('startRemoteScan')
 async startScan (job: Job) {
 	// 获取待扫描源码目录
 	const { targetDir, params } = job.data;
	
	// 执行远程扫描
	
	// fork 的方式创建子进程
    const p = fork(forkPath, {
      cwd: targetDir,    // 指定该脚本 process.cwd 到源码目录
    });
	
	// 发送子进程所需要的数据
    p.send({
      scanDir,
      params,
    });

    p.on('exit', (code, singal) =&gt; {
      //  子进程退出
    });
	
	// 子进程出错
    p.on('error', (error) =&gt; {
	  
	  // 将错误信息记录在该任务redis数据内
      redisClient.hmset(`bull:bcodeScan:${job.id}`, {
        errorInfo: error.message,
      });
	  
	  // 退出子进程
      treeKill(p.pid);
    });
	
	// 子进程消息
	p.on('message', async (scanMark: string) =&gt; {
	  // 成功，将结果记录在该记录内
      redisClient.hmset(`bull:bcodeScan:${job.id}`, {
        returnvalue: scanMark,
      });
	  
	  // ... 做一些扫描成功后的事情，如邮件通知
	  
	  
	  treeKill(p.pid);
	});
 }
</code></pre>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[童年的纸飞机]]></title>
            <link>https://kkkf.vercel.app/posts/童年的纸飞机.html</link>
            <guid>https://kkkf.vercel.app/posts/童年的纸飞机.html</guid>
            <pubDate>Fri, 09 Jul 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div style="text-align: center;">
飞机落地双流，公交到站中和，我立在巷子口
<p>小时候每天走过的路，骑车冲下的坡，翻过的平房屋顶是不是已成了高楼</p>
<p>巷口的小卖部还在不在，排骨面二块五还卖不卖</p>
<p>过路的儴儴已不认识，开裆裤的玩伴戴上了戒指</p>
<p>半个月后我去成都</p>
<p>我希望那天起风</p>
<p>阳光铺满巷子，亮堂让夏乏的人恍惚</p>
<p>童年的纸飞机飞回我手中</p>
</div>]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[走马观Nest.js]]></title>
            <link>https://kkkf.vercel.app/posts/走马观Nest.js.html</link>
            <guid>https://kkkf.vercel.app/posts/走马观Nest.js.html</guid>
            <pubDate>Fri, 02 Jul 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>从19年夏天偶然了解 <a href="https://docs.nestjs.com/">Nest.js</a>  至今，我已经使用它构建了三个应用，不过应用的规模都不大。每个应用之间都相隔半年以上，但每一次它给我的开发体验都是十分舒服的，它已成为目前我构建后端应用的首选框架。最近在开发<a href="https://cocacolf.vercel.app/blog/%E5%BC%80%E5%8F%91%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%B5%8B%E5%B7%A5%E5%85%B7/">代码质量检测工具</a>的服务端时我依然选择它开发。这篇文章对 Nest.js 进行了简单的介绍，通过此文能够基本了解 Nest.js 的使用和开发。</p>
<hr>
<p>初次接触 Nest.js，可能会觉得它的概念特别多，引入了 <code>Providers</code> 、<code>Pipes</code> 等概念。我个人觉得这些概念的出现是很自然的，而且这恰好是它最大的特点，一个后端应用从请求到达至响应的每一个环节都有具体的执行者，这种“专人专事”的方式使得项目非常清晰。</p>
<h2>控制器 (Controllers)</h2>
<p>在 Nest.js 中，每一个控制器是一个类，我们使用 <code>@Controller</code> 来标识它，配合 @Get/Post 等 method 标识，即可完成一个控制器。</p>
<pre><code class="language-js">import { Controller, Get } from '@nestjs/common'

@Controller('test')
export class TestController {
	@Get('list')
	getList (@Query('name') name: string) {}
}
</code></pre>
<p>而且框架内置了非常多的 HttpStatus 和 HttpExceptions，极方便的辅助开发。</p>
<h2>提供者 (Providers)</h2>
<p>Providers 是一个很抽象的概念，其几乎可以是任何一个类，通过依赖注入到不同的类中，让被注入对象的创建工作委托给 Nest.js 运行时，使得不同模块/类之间的使用变得非常简单。使用 @Injectable 修饰一个类，即可将其变成 Providers。</p>
<p>比如我们上面有了控制器，根据 MVC 思想，一般我们的业务处理会放在 M 层去处理。</p>
<pre><code class="language-js">// testService.ts
import {Injectable} from '@nestjs/common'

@Injectable()
export class TestService {
	hanleList () {
	
	}
}
</code></pre>
<p>那么在我们的其他模块，便可以通过依赖注入使用这个 Providers，你不需要去做诸如 <code>new TestService()</code> 的事情。</p>
<pre><code class="language-js">import { Controller, Get } from '@nestjs/common'
import { TestService } from 'testService.ts'

@Controller('test')
export class TestController {
	constructor (
		private readonly testService: TestService
	) {}
	
	@Get('list')
	getList () {
		testService.hanleList();
	}
}
</code></pre>
<h2>模块</h2>
<p>从外部看，模块则是一个具体功能的所有系统的集合。Nest.js 应用是由不同的模块组成，就像是前端开发中的组件，由根组件出发，由各个子组件组成。模块由 @Module 进行修饰，其内部定义模块的各个元素：</p>
<pre><code class="language-js">// testModule.ts

import { Module } from '@nestjs/common';
import { TestController } from 'testController.ts'
import { TestService } from 'testService.ts'

@Module({
	controllers: [TestController],
	providers: [TestService]
})
export class TestModule {}
</code></pre>
<p>那么在其他模块，如果想使用某一个模块，就像是前端开发中使用某一个组件一样，只需要在父模块中引入即可：</p>
<pre><code class="language-js">// rootModule.ts

import { Module } from '@nestjs/common'
import { TestModule } from 'testModule.ts'

@Module({
	import: [
		TestModule
	]
})
export class AppModule {}
</code></pre>
<p>当了解了这几个概念后，不考虑工程化的情况下，已经足够我们去编写应用。所以其他概念的引入，则是将开发中各个相似的处理抽离出来。</p>
<h2>中间件</h2>
<p>中间件是在路由处理之前调用的函数，可以使用它在路由请求处理前做一些事情，譬如对请求进行修改。Nest.js 中使用中间件很简单，大致分为两类，应用中间件和全局中间件。</p>
<p><strong>应用中间件</strong></p>
<p>应用中间件即只用在某个模块上，它的使用一般是在模块文件中，比如下面这个中间件 LoggerMiddleware 则提供给 <code>/test/xxx</code> 路由使用</p>
<pre><code class="language-js">
// testModule.ts
import { Module, MiddlewareConsumer } from '@nestjs/common';
import LoggerMiddleware from './LoggerMiddleware.ts';

@Module({
	// ...
})
export class TestModule {
    configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('test');
  }
}

</code></pre>
<p><strong>全局中间件</strong></p>
<p>全局中间件则是对整个应用使用，我们需要在 Nest.js 构建的应用的启动文件里 <code>use</code> 即可：</p>
<pre><code class="language-js">// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './appModule.ts';
import { logger } form './loggerMiddleware.ts';

async function bootstrap () {
    const app = await NestFactory.create(AppModule);
    app.use(logger);
    await app.listen(80);
}
</code></pre>
<h2>异常过滤器</h2>
<p>程序出现异常很正常，但是对于用户侧，我们并不希望用户看到具体的异常报错，而是提供给其清晰地友好的信息。</p>
<p>一般来说，我们常在逻辑中这么抛出异常：</p>
<pre><code class="language-js">// testService.ts

// do something...
if (xxx) {
    throw new HttpException({
        status: HttpStatus.FORBIDDEN,
        error: '这里是给用户看的错误信息',
      }, HttpStatus.FORBIDDEN);
}
</code></pre>
<p>于是客户端便收到了如下的响应：</p>
<pre><code class="language-json">{
    &quot;status&quot;: 403,
  	&quot;error&quot;: &quot;这里是给用户看的错误信息&quot;
}
</code></pre>
<blockquote>
<p>上面的 <code>HttpException</code> 和 <code>HttpStatus</code> 分别是 Nest.js 内置的类和枚举器。</p>
</blockquote>
<p>但是并不是所有的异常我们都希望在每个地方这么处理，有时候我们想统一处理异常，并做一些诸如日志记录的事情，这时候就要自己实现异常过滤器。下面是一个示例：</p>
<pre><code class="language-js">// allExceptionFilter.ts

import { 
    ArgumentsHost, 
    Catch, 
    ExceptionFilter, 
    HttpException, 
    HttpStatus, 
    Injectable, 
    Logger 
} from &quot;@nestjs/common&quot;;

@Injectable()
@Catch()
export class AllExceptionFilter implements ExceptionFilter {
    catch(exception: unknown, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse();

        const status =
        exception instanceof HttpException
            ? exception.getStatus()
            : HttpStatus.INTERNAL_SERVER_ERROR;

        const msg = exception['response'] &amp;&amp; exception['response']['msg'] || exception['message'] || '服务端错误';
        Logger.error(`捕获异常，${msg}`, 'exception');
        
        response.status(200).json({
            statusCode: 200,
            success: 0,
            msg
        });
    }
}
</code></pre>
<p>可以看到，异常过滤器需要继承 <code>ExceptionFilter</code>，并重载 <code>catch</code> 方法。异常过滤器也是一个 Provider。</p>
<p>要使用这个异常过滤器，我们只需要在目标 controller 上用  <code>UseFilters</code> 装饰器声明即可：</p>
<pre><code class="language-js">// testController.ts
import { Controller, UseFilters } from '@nest/common';
import { AllExceptionFilter } from './allExceptionFilter';

@Controller('test')
@UseFilters(new AllExceptionFilter())
export class TestController {}
</code></pre>
<h2>管道</h2>
<p>从客户端传到路由控制器的数据，会经过管道，所以管道是数据传输的管子，在这里我们可以对数据进行：</p>
<ul>
<li>转换</li>
<li>验证</li>
</ul>
<p>如果在管道层抛出异常，则请求并不会到达路由层。</p>
<p>假设我们已经存在一个叫做 <code>testPipe</code> 的管道，我们要使用它对 <code>/test/report</code> 接口的数据进行验证或转换，只需要：</p>
<ul>
<li>使用 <code>UsePipes</code> 声明要使用的管道</li>
<li>将参数需满足的形式传给管道 [非必须]</li>
</ul>
<pre><code class="language-js">// testController.ts

@Controller('test')
export class TestController {
    
    @Post('report')
    @UsePipes(new TestPipe(testPipeSchema))
    handleReport (@Body() reportData: reportData) {
        // ...
    }
}
</code></pre>
<p>很显然，管道内部则是对数据进行处理。为了再加深印象理解管道，我们简单实现 testPipe 这个管道。</p>
<p>一个管道需要实现 <code>PipeTransform</code>， 而 transform 方法中 value 变量为当前处理的参数，即我们路由中收到的参数； metaData 中包含有请求上下文的信息。</p>
<pre><code class="language-js">// testPipe.ts
import { 
    Injectable, 
    PipeTransform, 
    ArgumentMetadata, 
    BadRequestException, 
    HttpStatus 
} from &quot;@nestjs/common&quot;;

@Injectable()
export class TestPipe implements PipeTransform {
    transform (value: reportData, metaData: ArgumentMetadata) {
        if (!Array.isarray(value)) {
            throw new BadRequestException({
               status:  HttpStatus.BAD_REQUEST,
               message: '参数错误，xxx必须是数组'
            });
        }
    }
    
    return value;
}
</code></pre>
<p>Nest.js 中也内置了许多有用的管道，对于许多场景都可以开箱即用。而且我们当然也可以绑定全局的管道，在 <code>main.ts</code> 中 <code>use</code> 即可：</p>
<pre><code class="language-js">// ...
app.useGlobalPipes(new ValidationPipe());
// ...
</code></pre>
<h2>守卫</h2>
<p>守卫从名字也看得出来，它的工作就是询问“来者何人”——鉴权。当一个操作请求到达，一般来说我们的应用针对它需要做两件事情：</p>
<ul>
<li>我们的对某些操作（代码里的函数）需要定义什么角色或者说具备什么凭证才能执行，即定义条件</li>
<li>守卫里要校验当前请求者是否是这个角色或具备凭证，即验证条件</li>
</ul>
<p>Nest.js 的设计也正是如此，下面我们从这两个角度来看看 Nest.js 里 守卫是怎么样的。先假设我们构建了一个叫做 RolesGuard 的守卫。</p>
<p><strong>定义条件</strong></p>
<pre><code class="language-js">// testController.ts

import { Controller, UseGuards, Post, SetMetadata } from '@nestjs/common';
import { RolesGuard } from './rolesGuard.ts';

@Controller('test')
@UseGuards(RolesGuard)
export class TestController {
    @Post('deleteValue')
    @SetMetadata('roles', ['admin', 'admin2'])
    async deleteValue (@Body() deleteItem: string) {
        // ....
    }
}
</code></pre>
<p>解释一下这段代码：</p>
<ul>
<li>首先在控制器上通过 <code>UseGuard</code> 使用了这个守卫</li>
<li>在 deleteValue 这个操作上，通过 <code>setMetadata</code> 这个装饰器指定了权限，即 roles 为 <code>['admin', 'admin2']</code> 中的任意一个才可以</li>
</ul>
<p><strong>验证条件</strong></p>
<p>下面我们来实现这个守卫，来看看守卫的原理。</p>
<pre><code class="language-js">// rolesGuard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get&lt;string[]&gt;('roles', context.getHandler());
    if (!roles) {
      return false;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return roles.includes(user.roles);
  }
}

</code></pre>
<ul>
<li>守卫也是一个 Provider，因此我们在 Controller 中直接使用的 RolesGuard，而没有 <code>new RolesGuard()</code></li>
<li>通过 <code>canActivate</code> 返回的布尔值决定是否通过验证</li>
<li>在守卫中，可以通过 <code>context</code> 参数拿到请求的上下文。除了示例中可以拿到 request 外，还可以知道这个请求将会被哪个方法处理（getHandler）等信息</li>
<li>我们可以通过 <code>Reflector</code> 这个反射器，拿到 Controller 中通过 <code>setMetadata</code> 设置的条件</li>
</ul>
<p>怎么样，这种依赖注入解耦后，整个结构是不是赏心悦目。</p>
<h2>拦截器</h2>
<p>当我们想要在函数或整个应用执行前/后做一些事情，但是又不想对其有侵入性时，便可以使用拦截器。比如说：</p>
<ul>
<li>想统计一下函数执行时间，侵入性的做法是不是就需要在函数开始前后获取时间然后运行完后相减</li>
<li>想对所有请求的响应结果做一个统一的数据格式转换</li>
</ul>
<p>这种不入侵原程序来影响原程序的思想便是面向切面编程（AOP）。下面看看 Nest.js 中的拦截器，先假设我们定义了叫 <code>ResponeInterceptor</code> 的拦截器，它将请求的响应转成统一格式。</p>
<p><strong>使用拦截器</strong></p>
<p>只需要在控制器或者方法上使用 <code>UseInterceptors</code> 装饰器即可。</p>
<pre><code class="language-js">// testController.ts
import { Controller, UseInterceptors, Get } from '@nest/common';

@Controller('test')
@UseInterceptors(ResponeInterceptor)
export class TestController {
    @Get('getValue')
    async getValue (@Query() id: string) {
        return '这是获取的数据';
    }
}
</code></pre>
<p><strong>拦截器实现</strong></p>
<pre><code class="language-js">import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from &quot;@nestjs/common&quot;;
import { Observable } from &quot;rxjs&quot;;
import { map } from 'rxjs/operators';

@Injectable()
export class RequestInterceptor implements NestInterceptor {
    intercept (context: ExecutionContext, next: CallHandler): Observable&lt;any&gt; {
        return next
            .handle()
            .pipe(map(data =&gt; {
                const baseResponse = {
                    success: 1,
                    data: null,
                    msg: ''
                };
                
                return Object.assign(baseResponse, data);
            }));
    }
}
</code></pre>
<p>这段拦截器的功能则是将每个路由处理中返回的数据以 data 字段返回给客户端。拦截器的几个要素：</p>
<ul>
<li>实现 <code>NestInterceptor</code> 接口的 intercept 方法</li>
<li>intercept 方法的参数中，context 参数和守卫中一致，即在拦截器中也可以获得请求、方法执行者等信息</li>
<li><code>next.handle()</code> 实际上是在执行对应路由的方法，返回值为 <code>Observable</code> 流，比如控制器中的 getValue 方法。然后 <code>pipe</code> 中订阅了 handle 返回的 <code>Observable</code>，于是可以在 map 做想要对值做的事情。这部分实际上是 <a href="%5BRxJS%5D(https://rxjs.dev/guide/overview)">Rxjs</a> 的知识。</li>
</ul>
<p>顺带一提，我们甚至还可以在拦截器里处理异常。</p>
<pre><code class="language-js">// ...
intercept(context: ExecutionContext, next: CallHandler): Observable&lt;any&gt; {
    return next
      .handle()
      .pipe(
        catchError(err =&gt; throwError(new BadGatewayException())),
      );
  }
</code></pre>
<p>一切准备就绪后，还需要在 Module 里声明拦截器：</p>
<pre><code class="language-js">import { APP_INTERCEPTOR } from '@nestjs/core';
import { RequestInterceptor } from './requestInterceptor.ts';

@Module({
    // ...
    providers: [
        {
            provide: APP_INTERCEPTOR,
            useClass: RequestInterceptor
        }
    ],
    // ...
})
</code></pre>
<hr>
<h2>其他</h2>
<h3>使用 Express 生态</h3>
<p>Nest.js 底层基于 Express.js，因此可以使用 Express 的周边生态。</p>
<p><strong>静态资源、跨域、body大小限制</strong></p>
<pre><code class="language-js">// main.ts

import * as serverStatic from 'serve-static';
import { AppModule } from './appModule.ts';
import { json, urlencoded } from 'body-parser';

const bodyLimit = '50mb';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableCors();
  app.use(json({limit: bodyLimit}));
  app.use(urlencoded({limit: bodyLimit, extended: true}));
  app.use('/', serverStatic(path.join(__dirname, '../public'), {
    maxAge: '1d'
  }));

  await app.listen(80);
}

bootstrap();
</code></pre>
<h3>数据库</h3>
<p>以 MongoDB 为例，我们数据存储在 test 这个数据库里。</p>
<p><strong>安装依赖</strong></p>
<p><code>npm i @nestjs/mongoose mongoose</code></p>
<p><strong>建立数据库连接</strong></p>
<p>在根模块里（app.module.ts）:</p>
<pre><code class="language-js">// app.module.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
    imports: [
        // ...
        MongooseModule.forRoot('mongodb://xxx:27017/test')
    ]
})
export class AppModule {}
</code></pre>
<p><strong>创建数据表 schema 和文档接口</strong></p>
<p>下面指定了 People 这个数据表的字段和类型：</p>
<pre><code class="language-js">// peopleSchema.ts

import * as mongoose from 'mongoose';

export const peopleSchema = new mongoose.Schema({
    name: String,
    address: String,
    telephone: String
    // ...
});

// peopleDocument.ts

import { Document } from 'mongoose';

export interface PeopleDocument extent Document {
    name: string,
    address: string,
    telephone: string
    // ...
};
</code></pre>
<p><strong>在某个业务模块里使用表</strong></p>
<pre><code class="language-js">// testModule.ts

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { people } from './peopleSchema.ts';

@Module({
    imports: [
        MongooseModule.forFeature([
            {
                name: 'People',    // 表名
                schema: peopleSchema
            }
        ])
    ]
})
export class BcodeModule {}
</code></pre>
<p><strong>业务中注入模型</strong></p>
<pre><code class="language-js">// testService.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { peopleDocument } from './peopleDocument.ts';

@Injectable()
export class testService {
    constructor (
    	@InjectModel('People') private readonly peopleModel: Model&lt;peopleDocument&gt;
    ) {}
    
    // 就可以在业务中操作数据表了
    async findAll () {
        return await this.peopleModel.find({});
    }
}
</code></pre>
<p>官方文档有很多非常详细的[技术介绍](<a href="https://docs.nestjs.com/techniques/database">Database | NestJS - A progressive Node.js framework</a>)</p>
<hr>
<p>上述基本上简单但是又较全面的涵盖了 Nest.js 的内容，了解这些已经可以开始使用它构建一般性应用了。经过上面的走马观花，对 Nest.js 高度解耦的特点有了很直观的感受，除此之外，良好的开发体验还来自于非常完善的类型提示所带来的 TypeScript 编码舒适度；cli 快速创建控制器、模型等文件。不过也不得不承认，相比于其他 Node.js 框架，Nest.js 比较重，于是有一种论调是：既然都选择了 Nest.js，为何不干脆上 Java？很显然，作为前端开发工程师，选择上自然会更倾向于 JavaScript。</p>
<p>如果你感觉 Nest.js 有让你使用的欲望，那不妨开始构建你的第一个  Nest.js  应用。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[开发代码质量检测工具，来给代码打打分]]></title>
            <link>https://kkkf.vercel.app/posts/开发代码质量检测工具.html</link>
            <guid>https://kkkf.vercel.app/posts/开发代码质量检测工具.html</guid>
            <pubDate>Fri, 25 Jun 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1>TL;DR</h1>
<p>最近这一个多月，我和组长一起做了一个代码质量检测工具，我们把它叫做质量模型，取名叫 <code>bcode</code>。</p>
<p>工具对外是一个 npm 包，安装后在项目目录下执行 <code>bcode check [文件夹]</code>，便会扫描代码并上传报告，可打开命令行中输出的链接指向的页面获得代码分数和报告。</p>
<p>当前这个工具经过足够的测试，即将对内推出1.0版本提供使用。未来计划当我们所有规划的 feature 完成后，对外开源。</p>
<p>本文主要内容只涉及命令行部分。</p>
<h2>代码扫描的维度</h2>
<p>bcode 会从以下几个维度来检测代码：</p>
<ul>
<li>代码重复度。即复制粘贴的代码比例</li>
<li>代码拼写检查。即将代码中拼写有误的单词都找出来</li>
<li>代码可维护性。即一个代码好不好维护，复杂度高不高</li>
<li>代码安全性。这个安全性非狭义的安全漏洞，且包括譬如&quot;输入密码栏必须使用type=password，保证密码用掩码方式显示&quot;等</li>
<li>最佳实践。即内置一些最佳实践，扫描后提示开发人员哪些地方有更好的做法或库有更好的替代品推荐等</li>
</ul>
<h2>整体架构</h2>
<p>从整体来看，整个质量模型当前由三部分组成：</p>
<ul>
<li>
<p><code>bcode-cli</code> 负责扫描代码，生成报告数据上报给 <code>bcode-server</code></p>
</li>
<li>
<p><code>bcode-server</code> 负责处理上报数据，根据模型计算出得分并存储到数据库。同时对 <code>bcode-client</code> 提供 API 接口</p>
</li>
<li>
<p><code>bcode-client</code> 是一个对所有报告展示的网页，点击每个报告后可查看报告详情；同时还可以执行将认为误报的单词进行上报等操作</p>
</li>
</ul>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84.png" alt="整体架构"></p>
<p>我主要负责 <code>bcode-cli</code> 和 <code>bcode-server</code> 的开发。cli 使用 TypeScript 和 Node.js 开发；server 使用 <a href="https://docs.nestjs.com/">Nest.js</a> 和 <a href="https://www.mongodb.com/">MongoDB </a> 开发。</p>
<hr>
<h2>cli 部分</h2>
<h3>整体架构</h3>
<p>cli部分整体架构如下：</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/cli%E6%9E%B6%E6%9E%84.png" alt="cli架构"></p>
<h3>基本信息获取</h3>
<p>基本信息主要是获取如下信息：</p>
<ul>
<li>项目被扫描目录路径</li>
<li>项目的 git 仓库地址。这个通过分析 .git 文件即可</li>
<li>扫描的路径下，以数组形式保存过滤掉需要忽略的文件后需要扫描的文件，同时在此处可以计算得到所有文件的代码量总和</li>
</ul>
<h3>条件问询</h3>
<p>在分析完成基本信息后，可以根据具体的需求来进行一些校验性质的工作。比如缺少了一些基本信息则不继续进行检查；比如在当前检测的总代码行数下，某些维度耗时较长，因此可以交互式命令行（Y/n）询问是否进行这个维度的检查。</p>
<h3>扫描</h3>
<p>每一个扫描任务，都是新开一个子进程来执行。</p>
<pre><code class="language-js">import * as path from 'path';
import { fork }  from 'child_process';

mutiProcessScan();

async function mutiProcessScan (files) {

    // 定义任务，每一个进程
    const task = {
        spellCheck: path.resolve(__dirname, './spell/fork.ts'),
        copyCheck: path.resolve(__dirname, './copy/fork.ts'),
        // ...
    };

    // 执行扫描任务
    await processScan(task.spell, files);
    await processScan(task.copyCheck, files);
    // ...
}

async function processScan (taskFile, files) {
    
    return new Promise((resolve, reject) =&gt; {
        const p = fork(taskFile);

        p.send({
            files,
            // ...其他一些需要传给每个任务的参数
        });

        p.on('close', () =&gt; {
            // ...
        });

        p.on('exit', () =&gt; {
            resolve();
        });

        p.on('error', () =&gt; {
            reject();
        });
    })
}
</code></pre>
<p>有了上面这段代码封装，对于某一个维度我们只需要对外提供 <code>fork.ts</code> 即可。以拼写检查扫描为例：我们创建两个文件, <code>/spell/fork.ts</code> 和 <code>/spell/index.ts</code>。</p>
<pre><code class="language-js">// fork.ts
import { run } from './index.ts';

process.on('message', async ({ files }) =&gt; {
    await run(files);
    process.exit(1);
})

// index.ts
export const run = (files) =&gt; {
    // 每一个维度检查要做的事情

    // 做完后输出报告到指定目录
}
</code></pre>
<p>因此可以看出，如果要新增一个维度是很简单的。</p>
<p><strong>拼写检查</strong></p>
<p>拼写检查最开始实现时，想到的是 vscode 有个 <code>code spell checker</code> 插件，同时它是<a href="https://github.com/streetsidesoftware/vscode-spell-checker">开源</a>的，于是我计划参照它的源码来做。当我 clone 下代码时，我想如果是我，我会把核心的词库和检查部分等拆分出来单独作为一个包，于是我发现作者果然也是这么做的。核心的检查部分，作者抽成了<a href="https://github.com/streetsidesoftware/cspell#readme">cspell</a>库。但是 cspell 本身就是一个命令行工具，而我要做的是一个脚本，我该如何将其为我所用？哈哈，我本身就是命令行工具的开发者，这个对我来说很简单，因为命令行工具的本质只是对外提供交互式接口，根据收到的对外指令，执行对应的函数而已。因此我只要使用 cspell 命令行处理所调用的核心函数即可。整个思路如下：</p>
<pre><code class="language-js">// index.ts
import {startSpellCheck} from './index.ts';

const run  = async (files) =&gt; {

    // 从 bcode-server 获取单词词库白名单，这些单词默认是正确的
    // 单词结果生成文件 cspell.json
    const cSpellPath = await handleSpellWords();

    await startSpellCheck(files, {
        config: cSpellPath
    });
};

// check.ts
import App = require('cspell/dist/application');
import * as path from 'path';

const defaultOptions = {
    issues: true,
    config: path.resolve(__dirname, 'cspell.json')    // 单词词库
    // ...其他 cspell 配置
};

const genIssueEmitter = (issue) =&gt; {
    // 将cspell检查到的错误单词写入到拼写检查报告文件中
};

const startSpellCheck = async (files, options={}) =&gt; {
    const mergeOptions = Object.assign(defaultOptions, options);

    // cspell库抛出的事件响应
    // key 为事件名称，value 为回调函数
    const emitter = {
        issue: genIssueEmitter,
        error: nullEmitter,
        info: ...
    };

    return await App.lint(files, mergeOptions, emitter);
};
</code></pre>
<p>注意到在每次扫描拼写检查之前，会先从 <code>bcode-server</code> 获取当前项目的项目词库以及整个平台的平台此库，以在后面扫描时提供白名单，进一步提高扫描的准确率。</p>
<p><strong>可维护性检查</strong></p>
<p>代码的可维护性如何衡量？我最开始还很纳闷，这个应该是一个很感性的东西，还能被计算出来？其实有许多的论文都在研究这个。可以阅读这个<a href="https://zhuanlan.zhihu.com/p/66241474">分享</a>进行了解，看看下面的引用文章。</p>
<p>从工程上来做，这方面的库很少，我们使用的是 <a href="%60https://github.com/typhonjs-node-escomplex/typhonjs-escomplex%60">typhonjs-escomplex</a>。很简单的使用它就可以获得分析报告：</p>
<pre><code class="language-js">import escomplex from 'typhonjs-escomplex';

let source = 'let a = 1; ...';

escomplex.analyzeModule(source, {
    // 配置项
});
</code></pre>
<p>不过这个库已经三年没有维护了，我最开始觉得这个库所在领域场景实在是太狭窄了，用户肯定不多，这个从 star 数也看得出来。star 数可能也会让作者感受到的外在正反馈比较少。后面和作者交流后，还有一个原因是作者转去别的兴趣领域了。</p>
<p>同时这个库不支持 Vue 文件，因此我给这个项目提了一个 <a href="https://github.com/typhonjs-node-escomplex/typhonjs-escomplex/pull/29">PR</a> ，没想到很快就收到了作者的答复。最后和作者交谈后，完全理解他的想法，因此我最终通过修改其代码作为一个单独的包来解决 Vue 项目扫描问题，同时兼容使用 Typescript 所编写的 Vue 项目。</p>
<pre><code class="language-js">import escomplex from 'typhonjs-escomplex';

const sources = '&lt;template&gt;&lt;div&gt;test for vue&lt;/div&gt;&lt;/template&gt; &lt;script lang=&quot;javascript&quot;&gt;@Component export default class Test {readonly test = 1;}&lt;/script&gt;';
escomplex.analyzeModule(source, {
    extName: 'vue',
    commonjs: true,
    logicalor: true,
    newmi: true,
}, undefined, {
    decoratorsBeforeExport: true,    // 支持装饰器，且装饰器书写在 export 之前
    decoratorsLegacy: true
});
</code></pre>
<p><strong>重复度检查</strong></p>
<p>重复度检查已有库做了这个事情，具体可以看看 <a href="https://github.com/kucherenko/jscpd">jscpd</a>。</p>
<h3>数据上报</h3>
<p>每一个维度的扫描都会生成一个 <code>xxx.json</code> 文件，最终将其打包成压缩包发送给 <code>bcode-server</code> 即可。</p>
<pre><code class="language-js">import * as fs from 'fs';
import * as tar from 'tar';
import * as path from 'path';
import FormData from 'form-data';
import axios from 'axios';

function packageFile () {

    // 获取扫描报告文件的文件夹
    const tmpDir = getTempDir();

    const tgzFilename = path.resolve(path.dirname(tmpDir), 'report.tgz');
    tar.create({
        gzip: true,
        cwd: tmpDir,
        file: tgzFilename,
        sync: true,
    }, [...fs.readdirSync(tmpDir)]);
}

function report (tgzFilename) {
    const formData = new FormData();
    formData.append('file', fs.readFileSync(tgzFilename), tgzFilename);

    return axios({
        url: 'url',
        method: 'POST',
        headers: {
            ...formData.getHeaders()
        },
        data: formData
    });
}
</code></pre>
<p>cli 部分目前的基本功能已完成，后期还有一些事情要做。当项目基本功能开发完成，我觉得其实从实现上来说，核心扫描部分每一个维度的扫描基本已经有现成的库，<code>bcode-cli</code> 要做的只是在工程上将整个扫描任务连起来达成自己的需求。这里面涉及到的每一个库都是值得花时间学习的，不过这些库都有很多可以优化的地方，比如说效率，希望日后我能给这几个库提提 PR。同时在开发过程中我还是收获到了一些经验，其中最重要的是如何增强工具的可调式性，毕竟我们推出的工具最后要是在使用者那里出 Bug 了要可以快速定位到 Bug 所在，以后我在这方面积累了更多经验，看能否写点什么分享出来。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[我们生命的历史何存]]></title>
            <link>https://kkkf.vercel.app/posts/我们生命的历史何存.html</link>
            <guid>https://kkkf.vercel.app/posts/我们生命的历史何存.html</guid>
            <pubDate>Fri, 25 Jun 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I want to write something.</p>
<p>When I view GitHub activity yesterday, I aware something suddenly that It's been a long time since I've seen a friend star a project.<br>
And he always star some interesting project before. So I clicked into his homepage, and I saw he had no activity during February to June.</p>
<p>Ok, talk something else about how we know each other by the way. I received an email when I was studying in the library, I hadn't graduated at that time. The email said he got an offer from the same company as me and he found me by searching some keyword in google, so he send an email to find out something.This is the beginning of our acquaintance.Interestingly, even though we work for the same company, we have never met because of same company but different city(office). He worked here for a short time before he left for personal reasons.</p>
<p>Ok, back to the topic. I remember we talked on Wechat when he left, he talked about the topic of 'healthy'. So, I suddenly had some bad ideas when I saw he had no activity during February to June. There are too much people no longer active on GitHub, but I don't tnink he will because in my impression that he is the one who likes to write code.</p>
<p>I persuade myself that I was thinking too much and go back focus on work.</p>
<p>I kept thinking about it and couldn't control myself yesterday, so I send a message to him on Wechat. Finnaly, I received his reply.</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E5%AD%98%E5%9C%A8_reply.jpg" alt="Reply"></p>
<p>Yes,I think too much,indeed. But when I rethinking it,I have some ideas.</p>
<p>The Internet becomes a part of human life, the information we leave on internet was proof that we existed(At least in one aspect).But when the server of an App or Website closed, the 'proof' dismissed, a bit of our personal history disappeared too.</p>
<p>Suddenly I felt a little scared. Where should I storge my 'personal history'? Is Twitter/Weibo/Wechat? The answer of me is GitHub now. Why I trust GitHub now? Because:</p>
<blockquote>
<p>On 02/02/2020 GitHub captured a snapshot of every active public repository. Those millions of repos were then archived to hardened film designed to last for 1,000 years, and stored in the GitHub Arctic Code Vault in a decommissioned coal mine deep beneath an Arctic mountain in Svalbard, Norway.</p>
</blockquote>
<p><a href="https://laike9m.com/blog/people-die-but-long-live-github,122/">People die, but long live in GitHub.</a></p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[编写命令行工具升级eslint配置]]></title>
            <link>https://kkkf.vercel.app/posts/编写命令行工具升级eslint配置.html</link>
            <guid>https://kkkf.vercel.app/posts/编写命令行工具升级eslint配置.html</guid>
            <pubDate>Tue, 08 Jun 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    前段时间，我们组对旧的 eslint 规则做了一次改造，推出了新的 eslint 规则。为了让从旧往新的过度更顺畅，同时让使用者的改造意愿更强，我们提供了命令行无缝升级，只需要执行 <code>idux-cli init</code> 即可完成配置升级或初始化。组长把这个叫做 <code>开发者体验</code>。</p>
<hr>
<p>    配置升级的本质是：将无用的旧配置去掉，有用配置保留，再将旧配置和新规则进行合并生成新配置文件。</p>
<p>    首先有一个标准的 eslint 配置作为升级后的目标配置，大概如下(用xxx替换了一些缩写信息)：</p>
<pre><code class="language-jsx">module.exports = {
    root: true,
    parserOptions: { // ts的项目 或者 ts+vue的项目启用
        parser: '@typescript-eslint/parser'
    },
    extends: [
      '@xxx/base', // 基础规则，必须启用
      '@xxx/vue', // 使用 vue 需要启用
      '@xxx/vue2', // 使用 vue2 需要启用
      '@xxx/vue3', // 使用 vue3 需要启用
      '@xxx/typescript', // 使用 typescript 需要启用
      '@xxx/i18n', // 老版本国际化需要启用
      '@xxx/jsformat', // 格式化相关
      '@xxx/vueformat', // 格式化相关
      '@xxx/tsformat', // 格式化相关
    ],
    env: {
      
    },
    globals: {
      
    },
    rules: {
      // Customize your rules
    },
  };
</code></pre>
<p>    从配置里可以看出来，新配置的生成和以下几个因素有关：</p>
<ul>
<li>是否通过 eslint 来控制格式化</li>
<li>使用的技术栈</li>
</ul>
<p>    同时 eslint 配置文件有 <code>.eslintrc.[js,json,yml,yaml]</code> 等多种后缀文件，甚至还可以定义在项目的 <code>package.json</code> 文件中，我们对所有的文件格式都提供支持。</p>
<p>整个过程伪代码如下：</p>
<pre><code class="language-jsx">init () {
		
	// 命令行询问格式化控制是否由 eslint 控制
	let formatrControlByEslint = await formatControlByEslint();

    // step1: 生成eslintrc
    genEslintRc(formatrControlByEslint);

    // step2: 格式化如果由prettier控制，则生成 prettierrc
    if (!formatrControlByEslint) {
        prettierrc();
    }

    // step4: 删除package.json中旧的eslint依赖
    deleteOldPkgsInPackageJson();
}
</code></pre>
<p><code>genEslintRc</code> 要做以下几件事：</p>
<ul>
<li>判断技术栈</li>
<li>根据技术栈创建新的 eslint 文件的内容</li>
<li>根据现在项目的后缀来分析现在的配置，且和新的配置合并生成新的 eslint 配置</li>
<li>写入/创建对应的 eslint 配置文件</li>
</ul>
<p><strong>如何判断技术栈？</strong></p>
<ul>
<li>通过 <code>package.json</code> 中的依赖来判断</li>
</ul>
<p><strong>如何分析旧 eslint 配置？</strong></p>
<ul>
<li><code>.eslintrc.js</code> 文件</li>
</ul>
<p>    最开始，我是通过 require  <code>.eslintrc.js</code> 文件来对旧配置进行处理，但是后面发现这种方式是不对的。因为在配置中，可能存在三元表达式，如果 require 此文件，则得到的是表达式执行后的结果，而我的目标是保留原始代码。因此只能通过 <strong>操作抽象语法树来处理</strong>。</p>
<p>    起手就是三板斧： <code>esprima</code> 来解析 AST，<code>estraverse</code> 来操作 AST，<code>escodegen</code> 来生成代码。伪代码如下：</p>
<pre><code class="language-jsx">import * as estraverse from 'estraverse';
import * as esprima from 'esprima';
import * as escodegen from 'escodegen';

// 解析ast, fileContent为读取出来的 .eslintrc.js 文件内容
let parseAst = esprima.parseScript(fileContent);

/**
 * 操作抽象语法树
 * @Params {Object} ast 解析出来的ast
 * @Params {Object} 根据技术栈构造的eslint一些配置项
 */
const eslintAst = (ast, newEslintConfig) =&gt; {
	estraverse.replace(ast, {
		enter (astNode) {
			// 对节点做增删改查处理，构造出想要的eslint配置
		}
	});
}

// 生成代码，得到配置
let newAst = eslintAst(parseAst, newEslintConfig);
escodegen.generate(newAst);
</code></pre>
<ul>
<li>json、yml、yaml文件</li>
</ul>
<p>    这里将这三者放在一起讲，很显然他们的处理是相似的。将 yml 或 yaml 处理为 json 数据来进行操作，最后再把处理好的 JSON 转为相应后缀的文件格式即可。这个工作可以使用 <code>js-yaml</code> 来完成。</p>
<p>    后续生成新的配置则是对 JSON 进行操作，问题就变得很简单了。</p>
<p><strong>其他</strong></p>
<ul>
<li>注意新生成的配置，其缩进要和旧的配置缩进一致。可以使用 <code>detect-indent</code> 这个包来获得缩进位数。</li>
<li>容易忽略没有配置 eslint 的新项目的场景</li>
</ul>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[分析文件夹生成文档页面]]></title>
            <link>https://kkkf.vercel.app/posts/分析文件夹生成文档页面.html</link>
            <guid>https://kkkf.vercel.app/posts/分析文件夹生成文档页面.html</guid>
            <pubDate>Tue, 13 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>背景</h2>
<p>    最近小组在做 <code>eslint</code> 相关的升级改造，同时我们需要搭建一个平台，其内容为文档和一些好坏写法的代码展示等。</p>
<p>    为了让日后平台的维护者更好的维护文档，或者说只需要编写或删除文档，而不用修改代码，因此需要将文档自动化处理，和代码分离。</p>
<ul>
<li>新增或删除文档只需要在对应的文件夹下新增或删除文档文件即可</li>
<li>平台代码 <code>build</code> 的时候会自动处理 <code>/docs</code> 文件夹下的文档，生成配置文件，我们只需要在某个文档组件里引用该配置文件即可生成文档页面</li>
</ul>
<p>    由于在内网开发的我也不好截图，但是其效果和 Vue 官网这个效果基本一致：点击顶部某个导航栏，页面左侧为目录层级，右侧为文档信息。</p>
<p><img src="https://i.loli.net/2021/04/13/uQtyT9rZi8zcgEX.png" alt="效果"></p>
<h2>build时处理文档</h2>
<p>    首先有两个约定：</p>
<ul>
<li>所有的文档都放在 <code>/docs</code> 下，按照目录放好。比如 <code>/docs/Vue/基础/安装.md</code></li>
<li>生成的目录结构为：一级为分类，二级为标题，三级为文档下的标题</li>
</ul>
<p>    这个平台技术栈为 <code>Vite</code> 和 <code>Vue</code>。</p>
<p><strong>markdown to vue</strong></p>
<p>    我们需要把 md 文档当成 Vue 组件渲染在页面上，因此配置一下 <code>vite.config.js</code>：</p>
<pre><code class="language-js">import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Markdown from 'vite-plugin-md';

export default defineConfig({
    plugins: [
        vue({
            include: [/\.vue$/, /\.md$/],
        }),
        Markdown({
            markdownItSetup(md) {
                md.use(require('markdown-it-anchor'));
                md.use(require('markdown-it-prism'));
            },
        }),
    ],
});

</code></pre>
<p><strong>编写脚本，build时处理文档</strong></p>
<p>    我的思路如下：</p>
<ul>
<li>build 时，分析文档生成目录层级信息和路由信息</li>
<li>目录层级信息提供给左侧目录组件渲染左侧目录</li>
<li>文档路由和平台现有路由合并</li>
</ul>
<p>    生成目录信息没有什么的难度：</p>
<ul>
<li>读取 <code>/docs</code> 目录生成目录层级，数据结构为树</li>
<li>markdown 中的标题，可以通过将 markdown 转成 html，通过正则匹配 html 中的 h标签得到</li>
<li>最终结果会生成两个配置文件：<code>文档名.json</code> 和 <code>docsRouter.json</code></li>
</ul>
<p>    其中 <code>文档名.json</code> 内容示例如下，它表示的是左侧的目录层级关系，用于提供给左侧目录组件渲染目录：</p>
<pre><code class="language-json">// title为显示在左侧的目录标题
// child 为markdown中的标题
// path为该文档的路由
[{
    &quot;title&quot;: &quot;基础&quot;,
    &quot;path&quot;: &quot;&quot;,
    &quot;child&quot;: [
        {
            &quot;title&quot;: &quot;介绍&quot;,
            &quot;path&quot;: &quot;/docs/test/jichu/jieshao.md&quot;,
            &quot;child&quot;: [
                &quot;介绍&quot;,
                &quot;标题2&quot;,
                &quot;标题3&quot;
            ]
        },
        {
            &quot;title&quot;: &quot;安装&quot;,
            &quot;path&quot;: &quot;/docs/test/jichu/anzhuang.md&quot;,
            &quot;child&quot;: [
                &quot;安装&quot;,
                &quot;标题2&quot;,
                &quot;标题3&quot;
            ]
        }
    ]
},
{
    &quot;title&quot;: &quot;深入&quot;,
    &quot;path&quot;: &quot;&quot;,
    &quot;child&quot;: [
        {
            &quot;title&quot;: &quot;原理&quot;,
            &quot;path&quot;: &quot;/docs/test/shenru/yuanli.md&quot;,
            &quot;child&quot;: [
                &quot;原理&quot;,
                &quot;标题2&quot;,
                &quot;标题3&quot;
            ]
        }
    ]
}]
</code></pre>
<p>    <code>docsRouter.json</code>示例如下：</p>
<pre><code class="language-json">// pathForRouter为路由
// path为某markdown文档的路径
[{
    &quot;pathForRouter&quot;: &quot;/docs/test/shenru/yuanli.md&quot;,
    &quot;path&quot;: &quot;/docs/test/深入/原理.md&quot;,
    &quot;content&quot;: &quot;这里是文档的内容&quot;
}]
</code></pre>
<p>    然后在路由配置文件里，写一个小函数将原有的路由配置和现在生成的路由进行合并。文档路由的 <code>component</code> 配置为 <code>import(/docs/test/深入/原理.md)</code> 即可，其余的就交给 Vite 了。</p>
<p>    这里有两个细节：</p>
<ul>
<li>生成的路由都是中文文件名的拼音</li>
<li>文件中将文档的内容也放了进去，即 content 字段</li>
</ul>
<p>    如果生成的路由是中文的话，比如 <code>/docs/test/深入/原理.md</code>，那么当你刷新当前路由时，会发现当前页面白屏了。这是因为浏览器会对中文进行编码处理 <code>/docs/test/%E6%B7%B1%E5%85%A5/%E5%8E%9F%E7%90%86.md</code>，因此刷新时，Vite 找不到编码后的这个路由导致找不到组件。</p>
<p>    生成路由信息文件时，顺便将文件内容也放入 json 中，是为了做前端搜索功能。你可以在 Vue 官网上 <code>ctrl+k</code> 体验一下搜索功能，实现效果基本是照它来的。不过Vue 官网的搜索是后端搜索的，我这里做的搜索功能是前端搜索。想必你猜到了，就是将搜索框内的搜索字符串在 <code>docsRouter.json</code> 中进行字符串搜索，因此我可以非常方便的一并将搜索结果对应的文档路由也获取到！因此做结果跳转就非常方便了。</p>
<h2>业务代码中使用文档</h2>
<p>    build 时，已经将所有信息都生成且处理好了，因此在某个页面中使用就很简单了方便了。比如现在 <code>QA.vue</code> 是一个文档展示页面：</p>
<pre><code class="language-vue">// QA.vue

&lt;template&gt;
    &lt;side-toc :tocData=&quot;tocValue&quot; /&gt;
    &lt;router-view /&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot;&gt;
import { defineComponent } from &quot;vue&quot;;

// 这里在导入生成的文档目录数据
import testDoc from '@/toc/test.json';

// 这是左侧目录组件
import SideToc from '@/components/SideToc.vue';

export default defineComponent({
    components: {
      SideToc
    },

    setup() {
        return {
            tocValue: []
        }
    }
});
&lt;/script&gt;
</code></pre>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[弄丢的生活气]]></title>
            <link>https://kkkf.vercel.app/posts/我也有过生活气.html</link>
            <guid>https://kkkf.vercel.app/posts/我也有过生活气.html</guid>
            <pubDate>Thu, 01 Apr 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    午睡起来后看他人的博客，当我看到博文写自2015年时，突然想了一下那时候我在干嘛。我脑海里出现的画面是晴朗的下午，我穿着短袖走过金翰林宿舍区的一片树荫。啊，2015年好像是很久之前了，久到忘记。这不是我第一次有这种感觉。我时常觉得我是个容易忘记的人，过去的事情我常忘记，直到某些外部因素的触发我才会想起。</p>
<p>    当我在某清晨路过一家“复古”的早餐店——人们坐在路边上抑或是端着碗蹲在台阶上吃早餐，这生活气让我不禁微笑；当我下午四点多就离开工作回家，那是一个晴朗的春天下午，路过幼儿园看到孩子跑向外面的家长，我心里传来一股幸福的暖流和羡慕之情。</p>
<p>    是生活的接触面太小了，时间让无意义的事情消磨了，让自己的真实世界和精神世界都变得狭窄和枯燥。不怀念过去让过去被忘记；不接触广袤而坐井观天，最后留下的是平庸、厌倦、抱怨、重复。直到某一天被“生活气”所提醒，原来我也曾有过，原来我可以这样。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[2020]]></title>
            <link>https://kkkf.vercel.app/posts/2020.html</link>
            <guid>https://kkkf.vercel.app/posts/2020.html</guid>
            <pubDate>Fri, 05 Feb 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>工作</h2>
<p>    在年初我提出了转岗，于是疫情暂缓复工后我便到了新的产品线。在新的产品线这一年，主要支撑了三个版本的开发，以及独自负责一个老平台的维护和需求开发。</p>
<p>    刚到新产品线便负责一个大版本的开发，抛去技术栈不同来说，主要挑战是业务不熟悉和异地协作，整个开发团队只有我在另一个城市。最后我执行了两个策略极大的帮助了我：</p>
<ul>
<li>
<p>时常，有段时间甚至每天都主动给项目经理打电话，对齐进度和她的预期。保证工作在正确的方向和预期的时间节点里</p>
</li>
<li>
<p>和对接的开发集中在某个时间连着语音联调，相比于文字沟通，效率高太多了</p>
</li>
</ul>
<p>    最后这个版本，我拿到了质量评级 B+，体验评级 A 的成绩。忙完版本工作后的空闲期，我做了一个“体验需求池在线统计平台”，主要目的是将设计师每个月发体验运营报告的工作进行自动化。</p>
<p>    下半年前期，花了一些时间修复一个老平台 xss 攻击问题。在这个工作中，主要有两个亮点：</p>
<ul>
<li>花了半天时间修改项目使用的模板引擎源码来修复其中一种 xss 漏洞，将原本至少一周的工作量缩减到半天</li>
<li>将 xss 漏洞攻击修复的一些经验输出为文档给其他团队，极大的帮助到了其他团队的工作</li>
</ul>
<p>    之后的一个功能开发比较有意思，其中一个模块需要以桑基图形式将数据可视化，表达不同数据之间的联系。我本以为 <code>Echarts</code> 可以完成这个需求，但是通过调研后发现不满足我们的需求，最后还是自行通过 <code>SVG</code> 实现了桑基图的开发。早期开发完成后，我颇有成就感，不过这个模块在后半年里不断地进行需求变更，以致于我一度对这个模块产生了恶心感。这里也不得不说，我十分质疑这个产品线的产品经理的专业性。</p>
<p>    下半年还负责了团队业务组件库的落地，我做了三件事有效的保障了此工作在预期的时间节点完成：</p>
<ul>
<li>
<p>提前梳理出产品的所有业务组件，整理成文档交付给设计师，作为业务组件库建立基础，大大减少了设计师整理的工作量</p>
</li>
<li>
<p>在落地过程中，和设计师一起识别并讨论业务组件，产出了协作文档和开发流程</p>
</li>
<li>
<p>承担了13个业务组件的开发</p>
</li>
</ul>
<p>    最后组件库的工作获得 “超出预期” 的评价。</p>
<p>    整体来看，今年工作做得不错，在二季度和三季度都拿到了公司级别的奖项；绩效考核我也拿到了工作两年来的最佳绩效。最不满意的还是沟通能力，一个是自己的对问题的表达还是太啰嗦，不精炼；另一个是和项目相关干系人的沟通上，做得不够，很多信息有偏差。</p>
<p>    2021年我的工作内容又将发生变化，离开此产品线去往公共组。不做业务开发，有了技术自由似乎是一件很棒的事情，此前我也是这么认为的，不过现在我有了新的看法。如果单纯从技术提高的角度来说，公共业务或者说基础建设确实有更快的技术成长。不过从绩效产出层面来说在业务线更容易保障下限，自己能够更主动一点那一般都能拿到不错的绩效。而纯技术岗位，很容易没有有价值的产出。不过既然我决定去，那也意味着我愿意接受这个挑战和可能的结果。</p>
<h2>生活</h2>
<p>    年初买了车，有了车之后似乎没有特别大的感觉，但是在某些特别的时候你会不由感慨有车真好。</p>
<p>    篮球在下半年相对打得多一点，技术一度退步比较大。一直到十二月我才慢慢的找到了大学时候的感觉。女朋友的进步也非常大，我在某天带她打完球后，甚至觉得她现在有CUBA二级联赛的水平！</p>
<p>    六月到九月，坚持了三个月中午跑步和带餐上班，比较慢的瘦了八斤。那段时间现在回想起来还是比较快乐的，我从未想过我居然可以在跑步机上跑一个多小时。很遗憾，国庆后这个习惯就没了。</p>
<p>    九月中下旬去了三亚。因为毕业那年去过，所以在那待了四天，除了最后一天早上，其他时间我和她甚至没有去海边。庆幸最后一天早上五点多起来去看了日出，海边的日出真是太美了，每过几分钟景象还不一样。清晨海边天空的颜色和动漫里绘画出来的一模一样，红紫交错。此外，这次旅行我们俩一起自学游泳，取得了一些进步，两个人都可以游一段距离和站立了。来年游泳也会放在我的计划里。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E6%97%A5%E5%87%BA.jpg" alt="日出.jpg"></p>
<p>    在三亚住的是香格里拉和亚特兰蒂斯。这两个之间我还是更喜欢香格里拉，香格里拉更像个度假的地方，整个酒店楼栋矮，安静悠闲，草和树很多，我们的房间对着海，早上拉开窗帘，阳光晃入后世界模糊在一片光亮之中，世界清晰后，近处的绿植和远处的海映入眼帘，扑面而来的是春暖花开。至于亚特兰蒂斯，我倒并没有所谓七星级酒店的逼格，它的大堂乱得像个菜市场。不过酒店房间是真的大，里面的用品也比较有品质。它自带的水世界还是值得玩耍的，就是人太多了。</p>
<p>    我去过两次三亚，总会在某些时候想写一下感受，但从未真正写下来。18年毕业时，我说三亚适合花半个月的时间住在这里，我以后还会来的。我现在依然这么认为，不过比起住在星级酒店，我更推荐住在靠海的民宿。我时常会想起那年凌晨一点多，我坐在阳台上看着楼下的马路在路灯下沿着海延伸，一侧是昏黄的路灯，一侧是看不清的海。安静的夜里虚虚实实，时间便过得很慢，此前从未想起的许多往事在脑袋里清晰起来，我真真切切的感受到了生活的味道和我在这世上的真实存在。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E5%87%8C%E6%99%A8%E6%B5%B7%E8%BE%B9.jpg" alt="凌晨海边.jpg"></p>
<h2>2021</h2>
<p>2021心里有个愿望，希望一个月后能够实现。🙏</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[vue-router重复点击报错和新链接跳转失败]]></title>
            <link>https://kkkf.vercel.app/posts/vue-router-issues.html</link>
            <guid>https://kkkf.vercel.app/posts/vue-router-issues.html</guid>
            <pubDate>Thu, 14 Jan 2021 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>最近解决了项目的前端路由问题，具体而言有两个：</p>
<ul>
<li>路由重复点击，会在控制台输出报错</li>
<li>一个 vue-router 渲染出来的 a 标签，右键新链接打开，在新页面无法打开网页</li>
</ul>
<p>由于我们公司内外网隔离，所以无法具体截图现象。</p>
<h2>路由重复点击，会在控制台输出报错</h2>
<p>报错的大概内容为： <code>vueAll.js?v=3.0:2 Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location: xxxx</code>。</p>
<p>阅读 vue-router 的文档，可以<a href="https://router.vuejs.org/zh/guide/essentials/navigation.html#router-push-location-oncomplete-onabort">看到</a>：</p>
<blockquote>
<p>router.push 或 router.replace 将返回一个 Promise</p>
</blockquote>
<p>查看 vue-router 源码，从源码此处可以看出，我们对此方法进行异常处理，便可一劳永逸解决此问题。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/vue_router_src.png" alt="vue-router-push.png"></p>
<p>因此在初始化路由时，对此方法进行处理：</p>
<pre><code class="language-js">import VueRouter from 'vue-router';

function handleRouterErr (methodsList) {
    methodsList.forEach(item =&gt; {
        const ORIGIN_METHOD = VueRouter.prototype[item];

        VueRouter.prototype[item] = function (location) {
            let vm = this;
            return ORIGIN_METHOD.call(vm, location).catch(err =&gt; err);
        }
    });
}

// 调用
handleRouterErr(['push', 'replace'])
</code></pre>
<h2>无法在新标签页打开链接</h2>
<p>右键一个 <code>vue-router</code> 渲染的 <code>a</code> 标签，新标签页打开后页面显示 <code>Not Found</code>。</p>
<p><strong>base配置不正确</strong></p>
<p>首先，我发现我们所有的页面都是 <code>ip/index#xxxx</code> 的形式，但是此时打开的链接确是 <code>ui/#/#/xxxx</code> 的形式。也就是说渲染出来的a标签的href是不正确的。因此很容易想到我们路由配置有问题。</p>
<p>路由是这样写的：</p>
<pre><code class="language-js">router = new VueRouter({
    routes: ROUTER_DATA,
    base: '/ui/#'
});
</code></pre>
<p>那显然这个 base 是不对的，期望最后渲染的href应当是 <code>index#xxx</code> 的形式，所以这里应该将base改成 <code>/index/</code>。</p>
<p>为什么这里不需要 # 符号了？因为查看 <code>vue-router</code> 代码可以看到，构造 href 逻辑如下：</p>
<pre><code class="language-js">function createHref (base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}
</code></pre>
<p>mode 默认为 hash，所以会自动添加 # 号，这也解释了为何新标签页的那个链接存在两个 #。</p>
<p>我们的服务器上并没有 ui 这个目录，所以自然返回 404。</p>
<p><strong>新标签页打开报错</strong></p>
<p>当修改完base后，这次新标签页打开的链接似乎正常了：<code>ip/index/#/xxxx</code>，但是打开页面白屏，控制台报错了：<code>uncaught syntaxerror unexpected token '&lt;'</code>。</p>
<p>仔细一看，发现此时链接其实还是不正确的，相比于正确的链接，在index后面多了一个 /。为何多了这个会导致报错？</p>
<p>我选择一个报错的js文件请求，从请求中可以看到两个问题：</p>
<ul>
<li>请求的js文件链接不正确。正常情况下，这个js的请求应该是： <code>/static/xxx.js</code>，但是此时却是 <code>/index/static/xxx.js</code></li>
<li><code>Content-Type</code> 不正确，此时是 <code>text/html</code>，返回的也是一个html文件</li>
</ul>
<p>寻其根本，还是第一个请求不正确的问题导致的。</p>
<p>这些报错文件我看了一下，是在 webpack 打包后的 index.html 中引入的，在这个 html 文件中，都是以相对路径 <code>./static/xxx</code> 引入的。于是我想这里应该改成绝对路径，于是我将webpack配置中的 <code>relativePublicPath</code> 设置为 <code>true</code>，重新生成打包文件后，问题就解决了。</p>
<h2>更简单的解决方式</h2>
<p>我上面的解决方式改了三个地方：</p>
<ul>
<li>修改 base</li>
<li>修改入口文件资源路径</li>
<li>修改 webpack relativePublicPath 配置</li>
</ul>
<p>组长觉得应该不需要这样改，他搜索了 base 相关信息，发现大家都不配置 base，于是和我说删掉 base 这个配置试试。我一试，还真就解决新标签页跳转问题了。</p>
<p>那么为什么这种修改方式是可以的？</p>
<p>首先还是回到链接身上观察一下，在不修改 base 的情况下：</p>
<ul>
<li>正确的链接是 <code>ip/index#xxxx</code></li>
<li>有问题的链接是 <code>ip/ui/#/#/xxxx</code></li>
</ul>
<p>然后再来看 vue-router 构造 href 的规则：</p>
<pre><code class="language-js">function createHref (base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}
</code></pre>
<p>我们项目的 base 配置是 <code>/ui/#</code>，mode 默认为 hash ，fullPath 是 router-link 中设置的 to 参数，那么此时 path 就是 <code>#/{fullPath}</code>，我们配置了  base，那么最终结果就是 <code>/ui/#/#/{fullPath}</code>。路由链接发生了改变，浏览器会重新请求资源，但是服务器上并没有 /ui/ 这个文件导致无法返回正确的页面。</p>
<p>此时我们把base删去，那么整个 createHref 的结果就是 <code>#/{fullPath}</code>。我们改变的仅仅只是 hash 部分，hash 部分并不会被包括在 http 请求中，它是用来指导浏览器动作的，对服务器端没影响，因此，改变 hash 不会重新加载页面，也就是此时此链接点击时，浏览器跳转的实际地址为 <code>ip/index#{fullPath}</code>，和我们希望的链接形式是一致的。</p>
<p>删除一行代码就解决了。</p>
<p>那么这种改法是偶然还是通用？</p>
<p>之所以说是不是偶然，因为整个过程看起来似乎是刚好拼成了我们期望的链接形式。对于这个问题，我们需要看看 base 到底是什么。</p>
<p>文档上写的是：</p>
<blockquote>
<p>应用的基路径。例如，如果整个单页应用服务在 /app/ 下，然后 base 就应该设为 &quot;/app/&quot;</p>
</blockquote>
<p>举个例子：</p>
<p>假设单页应用的入口文件地址是：<code>www.xxx.com/test/index.html</code>，那么 base 应当是 <code>/test/index.html</code>。<br>
如果后端服务在 /test/ 下，那么后端配置 Nginx 配置，将 /test/ 重定向到 /test/index.html，那么我们的 base 此时就是写 /test/。</p>
<p>实际项目：</p>
<p>在我们的项目中，我们访问项目时，整个过程是 <code>ip -&gt; ip/index</code>，从请求中可以看到发送了一个 <code>ip/index</code> 的网络请求，这个请求指向一个 cgi 文件，最终返回了 index.html 文件，那么此时前端代码或者说得更小一点就是 vue-router 开始工作了，渲染出正确的组件。</p>
<p>那么回到最开始的问题：这种改法是偶然还是通用？</p>
<p>答案是通用的。因为我们配置了项目的根路径重定向到 /index，所以这里我们的 base 应该是配置  / ，但是vue-router默认 base 为 /，所以我们可以删去。这么一品，反而也有一些偶然的味道了。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[实现类桑基图展示数据关系]]></title>
            <link>https://kkkf.vercel.app/posts/实现类桑基图效果.html</link>
            <guid>https://kkkf.vercel.app/posts/实现类桑基图效果.html</guid>
            <pubDate>Tue, 01 Sep 2020 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>最近开发的页面中，有如下所示的板块，它展示了左右两侧信息的数据关系。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/%E5%88%86%E5%B1%82.jpg" alt="效果图"></p>
<ul>
<li>左边是类似表格的列表，每一行右侧有一根比例图柱子，可以看到柱子有黄色和蓝色两种颜色分层</li>
<li>右侧是存储设备全局的信息，这个设备分为性能层和容量层，水球图一黄一蓝来区分</li>
<li>左侧和右侧中间有一条顺滑的曲线从左侧的每一根柱子连接到右侧，柱子的起点的颜色连接同色的右侧水球。连线的宽度和柱子颜色的比例成正比，同时这里有个看不出来的动态效果——每根连线里有一颗“能量球”沿着连线流动</li>
</ul>
<hr>
<p>最初，交互设计师和我说这种设计图叫<a href="https://www.zhihu.com/question/45502919">桑基图</a>。我检索后，发现 <a href="https://echarts.apache.org/zh/index.html">ECharts</a>  就有桑基图<a href="https://echarts.apache.org/examples/zh/index.html#chart-type-sankey">效果</a>，于是我想可以基于其进行二次开发。在阅读文档后，我发现 ECharts 的桑基图可以自定义的部分不多，非常不灵活，我们的左侧和右侧板块的效果完全无法实现。好了，那就自己来了。</p>
<h2>分析</h2>
<p>将整个设计抽象为三个组件：</p>
<ul>
<li>左侧类似于表格的组件</li>
<li>中间绘图（连线）区域</li>
<li>右侧分层容量展示区域</li>
</ul>
<p>左侧和右侧很好实现，因此问题在于连线部分。连线部分主要有两点：</p>
<ul>
<li>左侧”表格“中每一行的右边框那根柱子，黄色的容量要连上右侧容量展示区域的黄色球，蓝色连上蓝色球</li>
<li>连线需要和尺寸无关，即浏览器缩放，连线依然准确。换言之，连线坐标不能写死，要动态计算</li>
</ul>
<h2>实现</h2>
<p>实现连线我有两种方案可选： Canvas 或 Svg 实现。我发现这个场景特别适合 Svg，因为它和 HTML 可以结合得很好，而且连线的绘制非常简单，使用它提供的 path 标签即可连线。两点确定一条线，那么问题的关键就成了如何确定起始坐标。</p>
<p><strong>结构设计</strong></p>
<p>从 HTML 结构自上而下来说，整个视图分为两层：</p>
<ol>
<li>第一层是组件层，即容纳左侧“表格”和右侧球信息的层</li>
<li>第二层是 Svg 连线层</li>
</ol>
<p>这两层都容纳在同一个大容器内，每一层的宽度和高度都和最外层的容器一样大小。这里必须要将连线层放在组件层之下，因为如果不这样，那么组件层中像鼠标悬浮到某个元素显示信息气泡的效果就无法实现，因为鼠标悬浮到的实际上是连线层。</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/%E6%A1%91%E5%9F%BA%E5%9B%BE_HTML%E7%BB%93%E6%9E%84.png" alt="结构图"></p>
<pre><code class="language-html">&lt;div class=&quot;container&quot;&gt;
	&lt;line /&gt;
	&lt;component-layer /&gt;
&lt;/div&gt;
</code></pre>
<p><strong>连线</strong></p>
<p>开始绘制连线，以左侧某一行和右侧连接为例：</p>
<p>如何获取起点？起点是左侧柱子的位置坐标，我们可以通过获取柱子的 dom 元素，使用 <code>getBoundingClientRect</code> api 获取这个元素相对于容器的位置信息 leftSide，那么这个柱子的位置则是 <code>(leftSide.right, leftSide.top)</code>。</p>
<p>同理获取终点坐标 <code>(rightSide.right, rightSide.top)</code>。</p>
<p>这里有个要注意的事情，我们连线是使用 Svg path 连线，那么这个坐标信息应当是相对于 Svg 层的位置。即我们要从 Svg 的起点到我们算出来的某个点的位置需要平移多少，因此我们也需要用同样的方式得到 Svg 层的坐标信息 SvgPosition。</p>
<p>到这里，某一根线的点信息就很简单了，以柱子里的黄色层为例：</p>
<pre><code class="language-js">let startPoint = {
    X: leftSide.right - svgPosition.left,
    Y: leftSide.top + 左边表格的某一行的右边缘柱子的高度*黄色容量所占比例 - svgPosition.top

}

let endPosition = {
    X: rightSide.left - svgPosition.left
    Y: rightSide.top + 球的半径 - svgPosition.top
}
</code></pre>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/%E8%BF%9E%E7%BA%BF%E7%A4%BA%E6%84%8F%E5%9B%BE.png" alt="连线示意图"></p>
<p>不过这样的线是直线，因此可以使用一些数学计算来美化这条连线，我采用的是一次贝塞尔曲线。</p>
<pre><code class="language-js">interface PositionInfo {
	x: number,
	y: number
}

/**
* 一次贝塞尔曲线
*/
private oneBezire (start: PositionInfo, end: PositionInfo) {
	const TWO = 2;

    let ret = `M ${start.x} ${start.y}`;
    let cpx1 = start.x + (end.x - start.x) * CURVATURE / TWO;
    let cpy1 = start.y;
    let cpx2 = start.x / TWO + end.x / TWO;
    let cpy2 = start.y / TWO + end.y / TWO;
    ret += ` Q ${cpx1} ${cpy1} ${cpx2} ${cpy2}`;
    ret += ` T ${end.x} ${end.y}`;
    
    return ret;
}
</code></pre>
<p>将计算出来的 Svg path 规则赋给 Svg path 属性即可绘制连线。</p>
<p>其余连线的绘制都是一模一样的，因此我们只需要获取到左侧的容器 dom，遍历其所有的每一行子元素去计算位置信息即可获得所有的连线信息。</p>
<p>由于位置信息都是动态计算的，因此在不同的分辨率下或缩放浏览器，连线的位置都不会错乱。</p>
<hr>
<h2>其他</h2>
<p><strong>流珠如何实现？</strong></p>
<p>使用 Svg circle 和 animateMotion 属性配置即可完成动态流珠。</p>
<pre><code class="language-html">&lt;circle :cx=&quot;0&quot;
        :cy=&quot;0&quot;
        :r=&quot;2&quot;
        :fill=&quot;#ccc&quot;&gt;

    &lt;animateMotion :path=&quot;流珠的滑动路径，和线的路径一致&quot;
                   begin=&quot;0s&quot;
                   dur=&quot;5s&quot;
                   repeatCount=&quot;indefinite&quot;
                   rotate=&quot;auto&quot;/&gt; 
&lt;/circle&gt;
</code></pre>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[简单版html模板引擎]]></title>
            <link>https://kkkf.vercel.app/posts/简单版html模板引擎.html</link>
            <guid>https://kkkf.vercel.app/posts/简单版html模板引擎.html</guid>
            <pubDate>Thu, 17 Oct 2019 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h3>背景</h3>
<p>功能中包含一个历史申请记录的显示页面，简笔画简单画了一下，如下所示：</p>
<p><img src="https://blog-1305900062.cos.ap-guangzhou.myqcloud.com/blog_pic/html%E6%A8%A1%E6%9D%BF%E5%BC%95%E6%93%8E.jpg" alt="需求"></p>
<p>不知道多久没写过字了，画下来的东西着实丑，既然不能绘色，那就绘声一下。<br>
每一条记录对应一个面板，每一个面板上呈现着对应记录的信息。那么不同的审批状态，就有很多东西不一样：</p>
<ol>
<li>数据，这个没得说</li>
<li>内容的样式。比如审批成功用绿色标识，失败是红色</li>
<li>操作。比如审批中，那么就有一个撤销申请的按钮；其他的状态，就没有这个按钮，但是有一个面板折叠的按钮</li>
<li>面板左侧有状态条，反映当前的状态，同时也是时间线的作用</li>
<li>。。。</li>
</ol>
<p>这个历史申请的数据是用XML格式传递的，如下所示：</p>
<pre><code class="language-xml">&lt;?xml version=\&quot;1.0\&quot; encoding=\&quot;utf-8\&quot;?&gt;
	&lt;Expansion&gt;
		&lt;Result&gt;0&lt;/Result&gt;
		&lt;ApplicationRecord&gt;
		&lt;record 
			id=&quot;1&quot; 
			status=&quot;0&quot; 
			apply_cpu=&quot;4&quot; 
			apply_mem=&quot;8&quot; 
			apply_disk=&quot;256&quot; 
			apply_time=&quot;2019-10-14 20:00&quot; 
			approve_time=&quot;2019-10-15 15:00&quot; 
			apply_reason=&quot;卡得不能用了&quot;
			approve_reason=&quot;满足你&quot;
			admin_name=&quot;XXX&quot; 
			admin_phone=&quot;2333333&quot; 
			admin_email=&quot;test@gmail.com&quot;/&gt;

			&lt;record 
			id=&quot;2&quot; 
			status=&quot;2&quot; 
			apply_cpu=&quot;4&quot; 
			apply_mem=&quot;8&quot; 
			apply_disk=&quot;256&quot; 
			apply_time=&quot;2019-10-14 20:00&quot; 
			approve_time=&quot;2019-10-15 15:00&quot; 
			apply_reason=&quot;卡得不能用了&quot;
			approve_reason=&quot;那就继续卡着吧&quot;
			admin_name=&quot;XXX&quot; 
			admin_phone=&quot;2333333&quot; 
			admin_email=&quot;test@gmail.com&quot;/&gt;
		&lt;/ApplicationRecord&gt;
	&lt;/Expansion&gt;
</code></pre>
<p>所以很明显，要去遍历解析这个XML，去循环创建面板，再根据每条记录里对应的字段，写上正确的内容以及设置不同的样式。</p>
<p>由于某些原因，没有使用任何样式库或者框架，使用原生js编写。</p>
<p>以上是背景。</p>
<h3>思路</h3>
<p>当然这个需求没有难点，最一般的方法，当然就是html字符串拼接，在大量的 js if else逻辑里，将不同的数据和html片段拼接起来。其中一个片段可能是这样：</p>
<pre><code class="language-js">var data = {};  // 数据
var str = '';   // html拼接

if(data.status === '0') {
    str += '&lt;p class=&quot;fail&quot;&gt;审核失败&lt;/p&gt;'
} else if(data.status === '1') {
    str += '&lt;p class=&quot;ing&quot;&gt;审核中&lt;/p&gt;'
} else if
</code></pre>
<p>这种写法会让整个代码逻辑显得很乱，整个html片段也是分散的，不便于后期维护，别人看这份代码也不能够一眼看明白到底做了什么。这个需求如果是使用Vue这种现代化框架，可以良好的使用诸如 v-if 等指令来完成，然而这里并不能使用现代化框架来开发。</p>
<p>我的期望解决方式是：</p>
<ul>
<li>
<p>html是完整写在一起的，而不是分散拼接的</p>
</li>
<li>
<p>我只需要传入对应的数据，一个符合状态的面板就创建好了</p>
</li>
</ul>
<p>于是就编写了一个简单的模板引擎来解决这个问题，它对外是一个函数，使用的方式如下:</p>
<pre><code class="language-js">// tpmplate是html片段，第二个参数是数据
tplEngine(template, {
    status: 1,
    applyTime: '2019-10-17',
    applyCpu: 'XXX',
    ......
});
</code></pre>
<p>那么html片段是这个样子的:</p>
<pre><code class="language-js">var template = `
    &lt;div class=&quot;&lt;% statusClass %&gt;&quot; &gt; &lt;%statusWords %&gt; &lt;/div&gt;
    &lt;span&gt;申请时间: &lt;% applyTime %&gt; &lt;/span&gt;

    &lt;% if(this.status !== 1) {%&gt;
        &lt;div&gt;
            撤销申请
        &lt;/div&gt;
    &lt;% } %&gt;

    &lt;% else {%&gt;
        &lt;div&gt;
            折叠面板的按钮
        &lt;/div&gt;
    &lt;% } %&gt;
`
</code></pre>
<p>虽然嵌入了一些奇怪的东西，但是很明显：</p>
<ol>
<li>html是完整的写在一起的，一眼就能看懂</li>
<li>嵌入的部分是数据和js语法，不影响阅读习惯，逻辑很清晰</li>
<li>内容很灵活的根据传入的数据改变而改变，不需要自己去做其他任何操作</li>
</ol>
<h3>结果</h3>
<p>通过这样封装，那么最后完成这个需求的代码就很简单和清晰了,伪代码如下:</p>
<pre><code class="language-js">// 定义好模板
var tpl = '&lt;div&gt;.......';

// 解析数据，得到所有的申请记录 data
var data = parseXML();

// 遍历data创建面板
data.each(function(index, item) {
    // 这一行代码就得到了创建好的面板
    var panel = tplEngine(tpl, item);

    $('pannel-container').append(panel);
});
</code></pre>
<h3>实现原理</h3>
<p>使用正则表达式，讲模板数据、js语法关键字、普通字符串匹配出来，然后把他们组成成Javascript语句。这些语句组成起来就是一个函数的具体实现，然后把这个函数的作用域设置在我们传入的数据对象上。</p>
<pre><code class="language-js">/**
 * html模板转换
 * @param {String} tpl html模板
 * @param {Object} data 数据
 */
var tplEngine = function(tpl, data) {

    var templateBoundary = /&lt;%([^%]+)?%&gt;/g,     // 模板边界 &lt;% %&gt;
        jsKeyWord = /(^( )?(for|if|else|switch|case|break|{|}))(.*)?/g,     // js关键字匹配出来
        tplToCode = 'var r=[];\n',  // html转化为js代码
        matchedLen = 0;     // 已经处理的长度

    var _addToCode = function(elememt, isJs) {

        // 去除空字符串的影响，否则会导致一些语法，比如 else 没有和if连接起来，出现报错
        if(elememt === '') return;

        // 组装成对应的js语句
        if(isJs) {
            if(elememt.match(jsKeyWord)) {
                tplToCode +=  elememt + '\n';
            } else {
                tplToCode += 'r.push(' + elememt + ');\n';
            }
        } else {
            tplToCode += 'r.push(&quot;' + elememt.replace(/&quot;/g, '\\&quot;') + '&quot;);\n';
        }
    };

    while (match = templateBoundary.exec(tpl)) {
        // 不存在模板(不需要处理)的部分
        _addToCode(tpl.slice(matchedLen, match.index));

        // 不存在关键字则认为是变量替换
        jsKeyWord.test(match[1]) ? _addToCode(match[1], true) : _addToCode(&quot;this.&quot; + match[1], true);

        matchedLen = match.index + match[0].length;
    }

    _addToCode(tpl.substr(matchedLen, tpl.length - matchedLen));

    tplToCode += 'return r.join(&quot;\n&quot;);'; 
    // console.info(code);

    return new Function(tplToCode.replace(/[\r\t\n]/g, '')).apply(data);
};
</code></pre>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[组内Node.js分享]]></title>
            <link>https://kkkf.vercel.app/posts/组内Node.js分享.html</link>
            <guid>https://kkkf.vercel.app/posts/组内Node.js分享.html</guid>
            <pubDate>Thu, 22 Aug 2019 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    昨天得知今天要进行小组分享，但是我内心并不慌张。关于今天的分享“当我在学习Node时，我在学什么”，是自己一路学习的心得体会，即便临场讲述也能言之有物。</p>
<h3>聊什么</h3>
<p>    其实在两周前，我报名分享时，我打算自下而上来分享Node，从底层架构到上层应用，但后来我可以很明显的预知到这样分享的效果会不好。所以我改变了分享的内容，同时由于时间原因，这次分享侧重点并不是在讲述Node是什么，以及Node Api相关的东西，我也相信以大家的自学能力，入门并不是问题。我今天分享的是个人在初期学习Node的过程中的经历、感受。</p>
<h3>说说历史</h3>
<p>    我喜欢听故事也喜欢讲故事，所以我还是想从最开始说起。那是2009年，Ryan Dahl想写一个基于事件驱动和非阻塞IO的高性能服务器，他尝试了许多语言来构建，比如C、Lua、Haskell等，但由于自己hold不住或语言本身已存在这些东西，他最后瞄向了没有历史包袱的Javascript，从而创建了Node。但要注意的是，Node并不是一门语言，只是一个Javascript的运行时，可以理解为PythonVM之于Python，JVM之于Java。</p>
<h3>特点</h3>
<p>    <strong>异步IO和基于事件和回调函数</strong>。比如在Node中使用文件模块读取文件:</p>
<pre><code class="language-js">fs.readFile('./1.txt', (err, data) =&gt; {
    console.log(data);
});

fs.readFile('./2.txt', (err, data) =&gt; {
    console.log(data);
});

</code></pre>
<p>对于这种风格显然我们不陌生，比如：</p>
<pre><code class="language-js">oneDom.addEventListener('click', function() {
    // do something
});

$.get('/url', function(data) =&gt; {
    // do something
})

</code></pre>
<p>这种异步IO，带来的好处就是性能更快，它的耗时取决于最慢的操作，但是也带来了成本，一是异步风格带来的理解成本，因为编码顺序和执行顺序并不一致；二是错误的处理。</p>
<p>另外，我们可以看到代码风格是基于事件和回调函数风格，再举一个例子，使用原生http模块来接收post数据时：</p>
<pre><code class="language-js">let str = '';

req.on('data', (trunk) =&gt; {
    str += trunk;
});  

req.on('end', () =&gt; {
    res.end(str);
});

</code></pre>
<p>这种基于事件的回调风格可以有一些优点，也是我个人最喜欢Node的地方，<code>Don't Call Me, I Wll Call You</code>。这种风格可以带来轻解耦，我们只需要关注事务即可。但是也有一些不足，比如每个事件事务相对独立，如果事件之间要彼此协作就不太方便。</p>
<p>    <strong>单线程</strong>。Node上层是Javascript，所以它是单线程的，单线程好处就是不用考虑锁、线程之间通信的问题。但是也有问题，比如：错误引起整个程序退出、无法利用多核CPU、当有大量计算占据CPU时，异步IO回调无法执行。不过这些问题都有解决方案。不知道大家在此处是否会有疑问，既然Node是单线程的，那它是如何实现异步IO的呢？这个问题在后面会说到。</p>
<p><strong>我们可以用Node做什么</strong></p>
<ul>
<li>工具</li>
<li>Web应用，尤其是实时应用</li>
<li>并行IO，抛弃同步等待式请求加速数据获取，加快渲染</li>
<li>游戏开发（因为对实时和并发要求高）</li>
<li>客户端开发</li>
<li>但<code>不太适合</code>CPU密集型任务</li>
</ul>
<h3>如何学习</h3>
<p>    终于到了主题了。当我们学习Node时，很容易陷入不知道该学什么的圈子，因为仿佛大家都是在用npm包。</p>
<p>    <strong>原生模块</strong>。我们刚入门时，应当从原生模块看起，原生模块量比较多，并不需要全看，只需要从自己需求最常用的看起。比如fs、http、url等。我个人在学习Node的初期，能写一些东西，但是总感觉有层膜隔着，很不自在，后来的转变是理解异步和熟悉基于事件的异步回调风格，而通过原生模块的练习，刚好可以达到这个目的，同时我们在这个阶段可能理解异步处理方案的一步步改变的原因，也能理解为什么早期包括现在还在被人黑的callback hell。比如做一些类似于<a href="https://github.com/CocaColf/demo-or-pratice/tree/master/node-sync-compare">这个例子的事情</a>。</p>
<p>    之后就可以使用Node来做一些事情了，比如<a href="https://github.com/CocaColf/demo-or-pratice/tree/master/mini-node-static-server">写一个静态服务器</a>，通过这个例子，可以了解使用原生模块写一个简单地静态服务器，以及自己从零撸起的繁杂，所以我们要使用框架。</p>
<h3>框架</h3>
<p>    Node的web框架太多了，我个人接触的第一个框架是Koa2。</p>
<p>    <strong>首先说说Koa2</strong>。Koa2很简单，它只是封装了http层，以及提供了一个中间件模型。但Koa提供的基础功能还是太简单了，要完成一个web应用的话，自己要做的事情还有很多，所以我们需要引入中间件来快速开发。关于Koa入门的代码有很多，随着框架的使用，会发现写法很自由，没有架构可言的的设计是不适合做大一点项目的，代码如面条一般，所以我们要进行抽象分层，分层的方式就太多了，每个人都有自己的方式，这里<a href="https://yesixuan.github.io/2018/01/04/node-koa%E5%88%86%E5%B1%82/">有个例子</a>提供了一种方式。这样分层后，显然可维护性提高了，项目规模可以大一点了，我们可以使用Koa去构建自己的web应用了，比如部门的门户后端就是使用Koa构建。但是写多了，就会发现不是很舒服，我个人的感受是，个人开发时会感觉每次写应用就需要自己引入中间件进行初始化、分层等；比如团队协作的话，风格和目录结构的约定五花八门。</p>
<p>    <strong>于是我接触了Nest.js</strong>。不知道大家有没有听过Java的spring的大名，Nest.js和spring比较像，我跟我写Java的朋友看Nest的代码时，他表示很亲切。多提一句，当你学会Nest.js后，Angular你也入门了。这是我学习时候的一个<a href="https://github.com/CocaColf/demo-or-pratice/tree/master/Nest.js/base-curd">Nest.js的例子</a>。可以看到，Nest.js默认使用typescript编码，大量的装饰器使用，相比于Koa也多了很多概念，比如提供者、守卫、拦截器等。通过这个例子可以看出来，Nest.js构建的应用，代码清晰，职责分明，适合团队和大型项目，但是上手成本比较高。</p>
<p>    当然还有很多框架，比如阿里的egg，midway，沃尔玛的hapi等，感兴趣可以去看看他们的思想和实现。</p>
<h3>底层</h3>
<p>    学Node很容易陷入Node庞大的的生态之中，仅是使用其作为工具完成开发，自然是没有问题的，但是从技术学习层面论，我认为你学会使用了一个npm包，你学会了一个新的web框架，并不代表你进步了。于是我觉得我的眼光还是要放在Node本身上，现在也在这方面学习沉淀。要深入学习Node，首先应该了解Node的架构，才知道学什么。<br>
<img src="https://user-images.githubusercontent.com/25732253/63482233-bcb61c00-c4ca-11e9-8868-99eb03fbecce.png" alt="Node架构"></p>
<p>所以可以去了解：</p>
<ol>
<li>
<p>libuv。通过学习libuv，可以对异步的实现更加了然；也可以回答出来为什么Node单线程可以实现非阻塞IO等。</p>
</li>
<li>
<p>Js和C++交互。Js是不能进行IO操作的，Node可以使用Js进行IO操作，本质上是通过C++扩展实现的。</p>
</li>
<li>
<p>V8引擎。这个不用多说。</p>
</li>
<li>
<p>模块化实现<br>
<img src="https://user-images.githubusercontent.com/25732253/63482241-c3449380-c4ca-11e9-836f-9399eab51376.png" alt="V8引擎"></p>
</li>
<li>
<p>。。。。</p>
</li>
</ol>
<h3>其他</h3>
<p>    不得不说，表象之学是很简单的，也是最容易过时的。也不得不说一句扎心的话，大多前端用的Node和后端用的Node不是同一个东西。要构建稳定的应用，还有很多问题需要学习和解决，数据库、缓存、安全、网络编程、服务化、设计模式等。我喜欢Node，一是可以使用熟悉的Javascript做更多的事，二是以Node为入口为工具去学习其他更多的东西。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[进行了一次肠镜检查]]></title>
            <link>https://kkkf.vercel.app/posts/进行了一次肠镜检查.html</link>
            <guid>https://kkkf.vercel.app/posts/进行了一次肠镜检查.html</guid>
            <pubDate>Mon, 27 May 2019 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    之前腹部隐痛，加上大便习惯改变，所以去看了医生。医生开了半个月的治疗肠炎的药，大便有所改善，但是腹部隐疼问题依然还在，于是再次去了医院，医生建议肠镜，于是进行了预约。时隔十来天，今天下午进行了检查。</p>
<p>    首先要表示的是，一定要健康饮食，包括食物健康（不要重油等）和吃饭习惯良好（细嚼小口），因为<code>肠镜体验实在是不好</code>。不过尽管过程不好过，但是这个检查是很有必要的，美国医学界建议人到了50岁就要筛查有没有结肠癌，一般是5到10年做一次结肠镜检查，这个费用是免费的（国内，比如我是花了八百块）。</p>
<p>    肠镜进行的前一天和当天，要进行清肠，具体是三点：吃流食、吃泻药、禁食禁饮。前一天要尽量是流食，我选择的是粥。我是不爱喝粥的，但是却足足喝了三顿粥，饿得特别快，而且总感觉没味道，好想吃好吃的，要不是女朋友监督着我，我感觉分分钟忍不住。晚上九点的样子，要开始服用泻药，这种泻药的效果和自己平时感冒等腹泻不一样，它会补充电解质，让你不会脱水严重，让人虚弱。一盒药分为6包A剂和6包B剂，用1000毫升水泡在一个大杯子里，三十分钟喝完！虽然这个药泡水后的味道感觉就是盐水，不难喝，但是在半个小时里往自己体内灌一升水，简直是太难受了，我大口大口的喝，中间好几次都想吐。这里有一点，杯子上特别提示说喝的过程中要来回走动，否则会严重影响清肠效果，我可能是没有严格遵守这一点，导致第一晚我居然没一点事，厕所都没有上。第二天到了，也就是检查当天，当天早午餐是禁食的，大概在上午九点开始喝泻药，这次更猛，两盒也就是24包一共两升水在40分钟喝完！我做个键盘侠...为了效果建议还是忍着喝完，因为我就没有喝完，只喝了一升。这次我就注意了走动，喝完后去小区里走了几圈，然后感觉要来了，速度回家...省略八百字。之后在十一点，喝一种XX油的药，作用是消除消化道里的气泡。这个药量少，也不难喝。然后十二点后禁饮，开始去医院。</p>
<p>    肠镜是一个大房间里很多人一起做，但是隐私保护还是有的。进去后侧躺在床上，褪下裤子献上自己的菊花。我选择的是无痛肠镜，会使用麻醉，这里有个有意思的事。给我打药的护士给我鼻子插上氧气，把针筒插到我手上，轻轻推了一下，我以为我会马上麻醉，然而好像只有扎针的这只手好像有点乏力，没有别的感觉，意识无比清醒，甚至觉得有趣。于是我便和护士说，这麻药对我没啥用啊，护士看了我一眼，大拇指动了动，我看着她一点一点的挪动着大拇指，眼睛开始模糊，意识却还是想说点什么，刚准备开口，就睡着了。不知道过了多久，醒了，躺在床上，裤子被穿起来了，很粗暴的只穿好了外裤，里面的内裤还是褪下的，于是我想提一下内裤，然而使用不上力气。就像是早上睁眼还想睡一样，又睡着了。这次应该没有多久，便醒了，医生扶我到了外面沙发上坐着等结果，我想去找女朋友告诉她我做完了，就像个醉汉东摇西撞。不知道普通肠镜不适感是怎么样，无痛肠镜是全程无感的，就是醒来后感觉屁股有点不舒服，毕竟作为一个直男，屁股被捅了怎么都会感觉不舒服；另外就是肚子里感觉很胀，很想排气，这是因为在检查过程中医生为了更仔细检查，会打气把肠道鼓起来。</p>
<p>    结果很快出来了，没有什么问题，长舒一口气。之前还在网上查了好多肠癌啥的乱七八糟，真是吓自己。</p>
<p>    检查之前，我在女友那立了个flag，我说这次要是健康的话，以后重新做人：</p>
<ul>
<li>
<p>少吃重油的东西，饮食要比以前更清淡一点（我以前特别喜欢菜的油拌饭）</p>
</li>
<li>
<p>吃饭不要吃太快，细嚼慢咽</p>
</li>
<li>
<p>八成饱就好（我遇到好吃的 / 舍不得浪费时好多时候都可能十二分饱）</p>
</li>
<li>
<p>蔬菜水果</p>
</li>
</ul>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[工作快一年了]]></title>
            <link>https://kkkf.vercel.app/posts/工作快一年了.html</link>
            <guid>https://kkkf.vercel.app/posts/工作快一年了.html</guid>
            <pubDate>Sun, 26 May 2019 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>应该是2018年7月2日开始上班的，差不多快要一年了。</p>
<h2>工作</h2>
<p>挺杂。</p>
<p>试用期的三个月，是进步最大的阶段。最大的收获是在导师的指导下，掌握了一些调试技巧，对于问题的排查和定位更有一条线的感觉，而不是连蒙带猜。</p>
<p>最初由于不熟悉，做事比较慢，同时承担了一些比较有难度的模块，所以不管是解决问题还是抗压能力都有很大进步。但半年后随着对业务和各个平台熟悉，有些时候会感觉比较无聊，没有挑战。</p>
<p>由于历史原因，产品线技术栈很老，比如<code>Ext.js</code>。这点在很多时候让我很分心，主要是两个原因：<code>1.与主流脱节 2.现在外界根本不再使用这些来构建应用，即使精通，对自己的竞争力并没有太对加成 3.新技术、主流技术会关注，会写项目，但是自己写的东西和真实的商业应用是两码事</code>。这个产品线在很多时候，完成需求并没有太多难点，我很多时候觉得周围的同事和该产品线耦合很严重，温水煮青蛙的感觉。我偶然看到这篇<a href="http://blog.xgheaven.com/2019/03/12/257-days-in-netease/">博文</a>，博主和我是同一年毕业的，也遇到了这个问题，他选择了从网易离开。</p>
<p>发现沟通很重要，学生时代我一直觉得我的表达能力还不错，但从工作中的表现来看，还需要不断提高。</p>
<p>写了好多PHP...不过今年二月份开始就不写了。</p>
<p>运气不错，遇上全员涨薪。</p>
<p>写代码之前没有设计的习惯，有时候回过头看自己的代码，觉得问题不少。</p>
<p>前主管好像很器重我，私下找我沟通，和我说成为前端架构师。然而我感觉我现在应该让他蛮失望吧。</p>
<p>这一年增加编制，招了好多厉害的同事，各种大厂过来的，看有些人的简历都把我看自闭了。</p>
<p>团队氛围挺不错的，同事都挺好相处，团建活动里我比较喜欢：滑雪、烤全羊。</p>
<p>觉得办公环境有点吵，有些同事讨论声音太大了，还有人喜欢隔空喊话。</p>
<p>双显真是生产力。</p>
<p>当了两次面试官。</p>
<p>公司的面挺好吃的，而菜有时候好吃有时候不好吃，种类倒挺多。</p>
<p>拿了两个奖，开放共享奖和优秀新人奖。<br>
<img src="https://user-images.githubusercontent.com/25732253/58377945-0b801280-7fbd-11e9-8346-f0eaee3ccb04.jpg" alt="优秀新人奖"></p>
<h2>学习</h2>
<p>工作了相比读书时候，想看看书好好学习太难了。</p>
<p>PHP就算了，只是写写，没有深入研究过。</p>
<p>Vue和React都进行了一些实践，React不错，Vue没有以前那么讨厌了。</p>
<p>看了一些Node相关的东西，喜欢。</p>
<p>最近在看操作系统。</p>
<p>算法第四版一直没看完，现在还在第三章不动。</p>
<p>github用得挺多的。</p>
<p>linux下一些基本操作。</p>
<p>文学没怎么看，经济学原理看了一点没坚持，我真是没救了。</p>
<p>什么乱七八糟的。</p>
<h2>生活</h2>
<p>白了，白了，白了。不那么黑不溜秋了。</p>
<p>胖了，胖了，胖了。我一百三十多了。</p>
<p>租的单间，自己独立的空间真好。</p>
<p>社交啥的基本都在墙外了，主要还是twitter用得多。博客上以前的博文基本上都删掉了，以后尽量多写点更有价值的东西，而不是充满着太多浅显的内容。</p>
<p>回家看奶奶的次数还是少了点，回去给她钱再多也比不上去陪陪她，奶奶真可怜。</p>
<p>球运不行，公司的比赛输了好几场一分的比赛，从夺冠热门到最终第四。今年的新赛季又要开始了，加油。</p>
<p>吃了不少没吃过的：三百多的自助餐，烤全羊，酒店的早餐，羊蛋，羊腰子（这辈子再也不吃了）。<br>
<img src="https://user-images.githubusercontent.com/25732253/58377940-09b64f00-7fbd-11e9-9da0-4520c91ebbcc.jpg" alt="自助"><br>
在家偶尔下厨：煮面，煮饺子，炖菜熬汤，超级失败的炸酱面（把自己吃吐了）。</p>
<p>女朋友厨艺好像超过我了：看起来很“电影”的早餐，和她自己的一些减脂餐。下面这是早餐和庆祝元旦:<br>
<img src="https://user-images.githubusercontent.com/25732253/58378090-c9a49b80-7fbf-11e9-9cee-6b7f23209288.jpg" alt="电影一样的早餐"><br>
<img src="https://user-images.githubusercontent.com/25732253/58377946-0c18a900-7fbd-11e9-94dd-c97ef11a1f7c.jpg" alt="庆祝元旦"></p>
<p>奶奶生日给她煮了面，很开心：<br>
<img src="https://user-images.githubusercontent.com/25732253/58377936-091db880-7fbd-11e9-9bb0-c1d53533db33.jpg" alt="奶奶生日"><br>
跟着她吃了好多鸡胸肉，不过我觉得挺好吃的啊。</p>
<p>去女朋友那里骑了好多哈啰bike，都够买一辆自行车了吧？</p>
<p>经济独立的感觉还不错，消费升级了太多，买东西和下馆子随意了不少。</p>
<p>把用了三年的iPhoneSE换成了小米8UD，要不是坏了我感觉还能再战三年。</p>
<p>房间还是小了点，床和学习的桌子一定要离远一点。</p>
<p>用机械键盘了，不过我觉得就那样吧。</p>
<p>喝了不少星巴克，最喜欢星冰乐系列。<br>
<img src="https://user-images.githubusercontent.com/25732253/58377944-0b801280-7fbd-11e9-99c1-ddfbc151e08c.jpg" alt="星冰乐"><br>
身体感觉有不少小毛病，医院去了几次了。</p>
<p>明天要做肠镜，好可怕。</p>
<p>喜欢看Vlog和做菜。</p>
<p>上班喜欢听podcast。</p>
<p>去了两次深圳，年会的时候和大学同学见面吃饭了。</p>
<h2>买了</h2>
<p>很多衣服，比较喜欢优衣库和不知道是不是牌子的bershika。</p>
<p>新球鞋，李宁闪击3白天鹅，然而穿它的第一场比赛，我就受了比较严重的伤。</p>
<p><img src="https://user-images.githubusercontent.com/25732253/58377937-091db880-7fbd-11e9-87c2-b68c43013e63.jpg" alt="闪击3"></p>
<p>开放共享奖的奖励买的我第一双Nike：</p>
<p><img src="https://user-images.githubusercontent.com/25732253/58377930-0753f500-7fbd-11e9-8429-5f874085fb7f.jpg" alt="Nike SB"></p>
<p>女朋友送的欧文5奥利奥，感动得我眼泪都流出来了：<br>
<img src="https://user-images.githubusercontent.com/25732253/58378083-77637a80-7fbf-11e9-9fc7-00d07572fc21.jpg" alt="欧文5"><br>
开放共享奖的奖励买的机械键盘：杜伽K320红轴：</p>
<p><img src="https://user-images.githubusercontent.com/25732253/58377933-07ec8b80-7fbd-11e9-9ab2-85b3762eb9bc.jpg" alt="机械键盘"><br>
小米8UD。</p>
<p>用来跑步的蓝牙耳机。</p>
<p>送女友的Kindle。</p>
<p>送女友的口红：Mac小辣椒和Dior999。</p>
<h2>接下来</h2>
<p>工作上试着去发现和解决一些问题，产出一些创新。</p>
<p>技术上要有所深度积累，提高竞争力，目前来看是Node。</p>
<p>生活上，健康饮食合理作息、运动、常回家看看。</p>
<p>还想学点新技能，培养更多的兴趣，比如Vlog或者视频剪辑。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[Node.js Event Loop]]></title>
            <link>https://kkkf.vercel.app/posts/Node.js event Loop.html</link>
            <guid>https://kkkf.vercel.app/posts/Node.js event Loop.html</guid>
            <pubDate>Sun, 28 Apr 2019 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    第一次接触Node的时候就喜欢上它了，但是那时候学习它的时候，一直感觉入不了门，会写一点但总感觉隔了层膜，挺难受的，后来的渐渐有所感觉是在简单的了解了事件驱动后。再后来就觉得JS层面的Node其实没有太多的东西，于是开始想要去了解下层一点的东西。最近看了一篇非常好的<a href="https://github.com/zhangxiang958/zhangxiang958.github.io/issues/43">博文</a>，在这里做几点记录。</p>
<h2>什么是事件循环</h2>
<p><img src="https://camo.githubusercontent.com/dd28ae6f0212f319720e8f712d8db15ff8feffc6/687474703a2f2f696d672e696a61727669732e636e2f76322d36363934636163633138343964386336313438333831616161643165616637375f68642e6a7067" alt="事件轮询"></p>
<ul>
<li>
<p><code>event Demultiplexer</code>来处理事件分发，同时把IO操作委托给硬件。它是一种抽象，各操作系统有自己的实现(Linux -&gt; epoll,MacOS -&gt; kqueue,Windows -&gt; IOCP)。为了支持不同的操作系统中不同的IO操作，于是诞生了libuv。</p>
</li>
<li>
<p>当IO操作被处理时，相对应的回调函数就被加入事件队列</p>
</li>
<li>
<p>事件队列执行并清空事件队列</p>
</li>
<li>
<p>循环上述过程</p>
</li>
</ul>
<h2>事件队列</h2>
<p><strong>简化的事件循环的执行阶段顺序</strong></p>
<p><img src="https://camo.githubusercontent.com/d94dd80a874eca894c6b9fb7e7ef643f84977835/687474703a2f2f696d672e696a61727669732e636e2f76322d34386333386431663231663438306462363131643962303138323864303762615f722e6a7067" alt="事件队列的执行顺序"></p>
<p>    这里有几个要点:</p>
<ul>
<li>
<p>有两个中间过渡检查: <code>process.nextTick</code> 和 <code>process.resolve</code>，且前者优先级大于后者</p>
</li>
<li>
<p>next tick 队列始终不为空导致IO饿死的问题</p>
</li>
<li>
<p><code>Timers</code>并不一定会准确执行，与CPU性能和当前所处的事件阶段有关</p>
</li>
<li>
<p>两个很经典的例子</p>
</li>
</ul>
<pre><code class="language-js">// 执行顺序不能保证
setTimeout(function() {
    console.log('setTimeout');
}, 0);
setImmediate(function() {
    console.log('setImmediate');
});
</code></pre>
<pre><code class="language-js">// setImmediate 一定在 setTimeout 之前执行
const fs = require('fs');

fs.readFile(__filename, () =&gt; {
    setTimeout(() =&gt; {
        console.log('timeout');
    }, 0);
    setImmediate(() =&gt; {
        console.log('immediate');
    });
});
</code></pre>
<p><strong>事件循环的阶段</strong></p>
<p>当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序循环阶段：</p>
<p><img src="https://user-gold-cdn.xitu.io/2018/8/2/164f6645917f6cce?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="事件循环阶段"></p>
<ul>
<li>
<p>Timer ——到期的定时器回调和 interval 回调。</p>
</li>
<li>
<p>Pending IO Callback——处理被挂起的 I/O 事件，包括完成的和失败的。</p>
</li>
<li>
<p>Idle —— 执行一些 libuv 内部操作。</p>
</li>
<li>
<p>Prepare —— 执行一些 I/O 操作的预准备工作。</p>
</li>
<li>
<p>Poll —— <code>可选择性地</code>等待 I/O 操作完成，<code>这里可能会发生Node阻塞</code>。在node.js里，任何异步方法（除timer,close,setImmediate之外）完成时，都会将其callback加到poll queue里,并立即执行。所以Poll阶段：<code>1.处理poll队列（poll quenue）的事件(callback); 2.当到达timers指定的时间时,执行timers的callback;</code><br>
这里引用博客上的一段解释:</p>
</li>
</ul>
<blockquote>
<p>如果event loop进入了 poll阶段，且代码未设定timer，将会发生下面情况：</p>
<ul>
<li>如果poll queue不为空，event loop将同步的执行queue里的callback,直至queue为空，或执行的callback到达系统上限;</li>
</ul>
</blockquote>
<blockquote>
<p>如果poll queue为空，将会发生下面情况：</p>
<ul>
<li>如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段，并执行check阶段的queue (check阶段的queue是 setImmediate设定的)</li>
</ul>
</blockquote>
<blockquote>
<p>如果代码没有设定setImmediate(callback)，event loop将阻塞在该阶段等待callbacks加入poll queue;</p>
</blockquote>
<blockquote>
<p>如果event loop进入了 poll阶段，且代码设定了timer：</p>
<ul>
<li>如果poll queue进入空状态时（即poll 阶段为空闲状态），event loop将检查timers,如果有1个或多个timers时间时间已经到达，event loop将按循环顺序进入 timers 阶段，并执行timer queue.</li>
</ul>
</blockquote>
<pre><code>关于这里，我觉得 cNode上这个 [帖子](https://cnodejs.org/topic/57d68794cb6f605d360105bf) 的讨论非常精彩。
</code></pre>
<ul>
<li>
<p>Check handlers —— 执行一些 I/O 操作的后续处理工作，通常来说，setImmediate 添加的回调也会在这个阶段执行。</p>
</li>
<li>
<p>Close handlers —— 执行一些 close 事件相关的操作比如 socket 连接等等。</p>
</li>
<li>
<p>此外，process.nextTick()不在event loop的任何阶段执行，而是在各个阶段切换的中间执行<br>
<strong>事件轮询的核心代码</strong></p>
</li>
</ul>
<pre><code class="language-c">//deps/uv/src/unix/core.c
int uv_run(uv_loop_t *loop, uv_run_mode mode) {
	int timeout;
	int r;
	int ran_pending;
	//uv__loop_alive返回的是event loop中是否还有待处理的handle或者request
	//以及closing_handles是否为NULL,如果均没有,则返回0
	r = uv__loop_alive(loop);
	//更新当前event loop的时间戳,单位是ms
	if (!r)
    	uv__update_time(loop);
	while (r != 0 &amp;&amp; loop-&gt;stop_flag == 0) {
    	//使用Linux下的高精度Timer hrtime更新loop-&gt;time,即event loop的时间戳
    	uv__update_time(loop);
    	//执行判断当前loop-&gt;time下有无到期的Timer,显然在同一个loop里面timer拥有最高的优先级
    	uv__run_timers(loop);
    	//判断当前的pending_queue是否有事件待处理,并且一次将&amp;loop-&gt;pending_queue中的uv__io_t对应的cb全部拿出来执行
    	ran_pending = uv__run_pending(loop);
    	//实现在loop-watcher.c文件中,一次将&amp;loop-&gt;idle_handles中的idle_cd全部执行完毕(如果存在的话)
    	uv__run_idle(loop);
    	//实现在loop-watcher.c文件中,一次将&amp;loop-&gt;prepare_handles中的prepare_cb全部执行完毕(如果存在的话)
    	uv__run_prepare(loop);

    	timeout = 0;
    	//如果是UV_RUN_ONCE的模式,并且pending_queue队列为空,或者采用UV_RUN_DEFAULT(在一个loop中处理所有事件),则将timeout参数置为
    	//最近的一个定时器的超时时间,防止在uv_io_poll中阻塞住无法进入超时的timer中
    	if ((mode == UV_RUN_ONCE &amp;&amp; !ran_pending) || mode == UV_RUN_DEFAULT)
        	timeout = uv_backend_timeout(loop);
    	//进入I/O处理的函数(重点分析的部分),此处挂载timeout是为了防止在uv_io_poll中陷入阻塞无法执行timers;并且对于mode为
    	//UV_RUN_NOWAIT类型的uv_run执行,timeout为0可以保证其立即跳出uv__io_poll,达到了非阻塞调用的效果
    	uv__io_poll(loop, timeout);
    	//实现在loop-watcher.c文件中,一次将&amp;loop-&gt;check_handles中的check_cb全部执行完毕(如果存在的话)
    	uv__run_check(loop);
    	//执行结束时的资源释放,loop-&gt;closing_handles指针指向NULL
    	uv__run_closing_handles(loop);

    	if (mode == UV_RUN_ONCE) {
        	//如果是UV_RUN_ONCE模式,继续更新当前event loop的时间戳
        	uv__update_time(loop);
        	//执行timers,判断是否有已经到期的timer
        	uv__run_timers(loop);
    	}
    	r = uv__loop_alive(loop);
    	//在UV_RUN_ONCE和UV_RUN_NOWAIT模式中,跳出当前的循环
    	if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
        	break;
		}
		
	//标记当前的stop_flag为0,表示当前的loop执行完毕
	if (loop-&gt;stop_flag != 0)
    	loop-&gt;stop_flag = 0;
	//返回r的值
	return r;
}
</code></pre>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[小时不识月]]></title>
            <link>https://kkkf.vercel.app/posts/小时不识月.html</link>
            <guid>https://kkkf.vercel.app/posts/小时不识月.html</guid>
            <pubDate>Mon, 24 Sep 2018 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    已经记不得上一次在家过中秋已是什么时候，今年难得在家过中秋。</p>
<p>    参加工作无非也就三个月，却已感觉离家许久，回家的大巴上，乡间小镇的景色陌生得很，一路上稻田已开始泛金色，自大学开始，这份收获的金色我再也没见过了。近乡情更怯，下车进村的路上，我拿出手机低头前行，不想与人照面。路上遇到几位伯伯，大家还是习惯性的对我说：“放学了啊”。我笑了笑，“没读书了，毕业了开始工作了”。耳后传来伯伯们对时间的感慨声；遇到了成团聊天的村里伯母、奶奶，我已辨认不出其中几位，有位奶奶认出来我，我们互相寒暄了几句，耳后传来她们对我的议论，大概是想把我做媒给谁吧。</p>
<p>    自家的屋子开始出现在视野里，我加快了脚步。奶奶很开心，“哎呀我在栏杆那里来回都看了十几次了，想着我爱孙怎么还没有回来”。给奶奶买了一些吃的，和去年实习回来一样，给了奶奶五百块。“孙子刚参加工作开始赚钱，一点点奶奶一定要拿着”这才驳回了奶奶想要退还给我的手。</p>
<p>    我踱步在院子里，一边打量着小山村，一边听奶奶讲这几个月的事。一场秋雨一场寒，傍晚的冷风把我们吹进来屋里，也到了吃饭的时候。奶奶说“没有人啊，平时候村里真的都没有人，下村p伯母g伯母白天打牌去了，隔壁t叔叔做工夫去了，白天干脆没有人，我一个人在家这里走到这那里，真的没什么意思。有时候来个什么人坐一下，我就很欢迎很欢迎；p伯母总是对我说你这个老太婆怎么不出去走走吧，一个人窝在家里干什么。有一次我就同意去她家坐坐，但我走到前面柱子那里，就感到人不好，我赶紧回来吃了几粒救心丸…”。</p>
<p>    我默默的咽了口饭，如鲠在喉，接不上话，默默想想，这样孤独的老年生活这是奶奶第十一年了，以前我觉得奶奶好像还好，因为她从来不曾抱怨，但这两年“没意思，没味”这样的话语我时常听到。我想到在哪见过的一句话，大意是老人越老就越像个孩子，奶奶可能就越来越像个孩子了，开始渴望陪伴，渴望玩耍，以前不爱吃零食的她能吃很多零食。此刻我的心里开始难过，想起来奶奶枕边放着的柴刀；想起来奶奶一个人傍晚五六点就上床独自坐到半夜无人说话；想起来她担心所有的后人的事情却不会用手机打电话只能干等。我曾经央求爸爸不工作了回来陪奶奶，但却被奶奶唤作不懂事，此刻这想法又在我心里翻涌。但生活啊就是这样，我也何尝不懂爸爸的无奈，孝顺的父亲又何尝不在千里之外挂念着母亲。可为了我，父亲暂时不会横心回来，哪怕我再怎么和他说结婚买房的事我自己努力。</p>
<p>    雨后乡下的清晨是舒服的，空气清新，凉风过处，一丝丝清香。让刚从床上爬起来的我瞬间清醒起来。我站在院子里，觉得村里和我记忆里的不一样了。门前不远的小瀑布早就干涸了，那一年四季不休的水流声其实消失了几年了，而后知后觉的我到现在才感到可惜。我们村这两年不允许再种植水稻，似乎是被检测出来土里某种元素超标了，对人不好，取而代之种上了高粱。一眼望去满满的绿色和棕色，微风里整齐的摇摆，可我并不喜欢。乡下真的没有太多的人影，年轻人外出打工，而我以前见过的老人们也大多归土，哪怕是过节，也没有见到来往的人。记得小时候，大人们聚在院子里乘凉，小孩子们追赶打闹，隔着老远都会喊话拉家常。半夜都会有刚打完牌结对回家的人，手电筒从外面扫进来我的房间。那时候现已去世的老人刚享受天伦之乐；有家庭重担的中年人偷偷的呼喊一起去看毛片；刚为人父母的哥哥姐姐还在写作业；也开始走进社会的我还在守着少儿频道的大风车。那时候还有人住着土砖瓦房，还很少有摩托车，村里谁家吃点好菜便乘一点点送别户分享，生活好像很舒服，邻里之间和睦友好，好像没有那么多暗自较劲，没有那么多眼红，没有那么多买房买车的攀比，没有那么多有去就必须有回的算计。那时候村人好像都是好人，而现在，好人里有两个进了牢狱，有两个依然在潜逃。是民风慢慢不如以前淳朴，还是我的眼睛也慢慢的开始深邃，亦或是我也开始沾上这些模样。月是故乡明，而我明晚是看不到村里的满月了，我多想留下来，在这圆月的镜像里找找小时候，我不要玉盘珍羞值万钱，我想回到小时不识月。</p>
<p>    明天两个姑姑两大家子都来我家过节，我和奶奶说“难得这么热闹一次，当得（相当于）过年了”。可这年过完，我们都将驶离乡村去向城市。我现在已经有了画面，挥手送别我们的奶奶，转身走回家，迈进一个人的家里，在傍晚六点坐上床。</p>
<p>    习惯性失眠，半夜起来透透气，不能秉烛夜游，只能喃喃自语。透过窗外，隐隐约约的是对面的山，已到中秋的清晨云里也没有漏着一两点光，倒是我这渴睡人的眼在夜里放光。静谧的村里什么声音都没有，生活和时间再怎么赶着人走又再怎么让人回想过去，此刻世界仿佛都像是停止了所有的活动。最热闹的是我，但热闹是我一个人的，其他人什么也没有。</p>
<p>    中秋快乐。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[什么才算是真正的编程能力]]></title>
            <link>https://kkkf.vercel.app/posts/什么是真正的编程能力.html</link>
            <guid>https://kkkf.vercel.app/posts/什么是真正的编程能力.html</guid>
            <pubDate>Sun, 05 Feb 2017 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>转自<a href="https://www.zhihu.com/question/31034164/answer/50423838">这里</a></p>
<p>    非常好的一个问题。这可能是我在知乎见到过的问编程有关的问题中问得最好的一个了。我非常喜欢这个问题。</p>
<p>    计算机科学有两类根本问题。一类是理论：算法，数据结构，复杂度，机器学习，模式识别，等等等。一类是系统：操作系统，网络系统，分布式系统，存储系统，游戏引擎，等等等等。</p>
<p>    理论走的是深度，是在追问在给定的计算能力约束下如何把一个问题解决得更快更好。而系统走的是广度，是在追问对于一个现实的需求如何在众多的技术中设计出最多快好省的技术组合。</p>
<p>    搞ACM的人，只练第一类。像你这样的更偏向于第二类。其实挺难得的，但很可惜的是第二类能力没有简单高效的测量考察方法，不像算法和数据结构有ACM竞赛，所以很多系统的苗子都因为缺少激励和正确引导慢慢就消隐了。</p>
<p>    所以比尔盖茨才会说，看到现在学编程的人经常都把编程看作解各种脑筋急转弯的问题，他觉得很遗憾。</p>
<p>    做系统，确实不提倡“重复发明轮子”。但注意，是不提倡“重复发明”，不是不提倡“重新制造”。恰恰相反的，我以为，系统的编程能力正体现在“重新制造”的能力。</p>
<p>    能把已有的部件接起来，这很好。但当你恰好缺一种关键的胶水的时候，你能写出来吗？当一个已有的部件不完全符合你的需求的时候，你能改进它吗？如果你用的部件中有bug，你能把它修好吗？在网上繁多的类似功能的部件中，谁好谁坏？为什么？差别本质吗？一个开源代码库，你能把它从一个语言翻译到另一个语言吗？从一个平台移植到另一个平台吗？能准确估计自己翻译和移植的过程需要多少时间吗？能准确估计翻译和移植之后性能是会有提升还是会有所下降吗？</p>
<p>    系统编程能力体现在把已有的代码拿来并变成更好的代码，体现在把没用的代码拿来并变成有用的代码，体现在把一个做好的轮子拿来能画出来轮子的设计蓝图，并用道理解释出设计蓝图中哪些地方是关键的，哪些地方是次要的，哪些地方是不容触碰的，哪些地方是还可以改进的。</p>
<p>    如果你一点不懂理论，还是应该学点的。对于系统性能的设计上，算法和数据结构就像在自己手头的钱一样，它们不是万能的，但不懂是万万不行的。</p>
<p>    怎么提高系统编程能力呢？土办法：多造轮子。就像学画画要画鸡蛋一样，不是这世界上没有人会画鸡蛋，但画鸡蛋能驯服手指，感受阴影线条和笔触。所以，自己多写点东西吧。写个编译器？渲染器？操作系统？web服务器？web浏览器？部件都一个个换成自己手写的，然后和已有的现成部件比一比，看看谁的性能好，谁的易用性好？好在哪儿？差在哪儿？为什么？</p>
<p>    更聪明一点的办法：多拆轮子。多研究别人的代码是怎么写的。然而这个实践起来经常很难。原因：大部分工业上用的轮子可能设计上的思想和技术是好的，都设计和制造过程都很烂，里面乱成一团，让人乍一看毫无头绪，导致其对新手来说非常难拆。这种状况其实非常糟糕。所以，此办法一般只对比较简单的轮子好使，对于复杂的轮子，请量力而行。</p>
<p>    轮子不好拆，其实是一个非常严重的问题。重复发明轮子固然是时间的浪费，但当轮子复杂而又不好拆的时候，尤其是原来造轮子的人已经不在场的时候，重新发明和建造轮子往往会成为无奈之下最好的选择。这是为什么工业界在明知道重复发明/制造轮子非常不好的情况下还在不断重复发明/制造轮子的根本原因。<br>
程序本质是逻辑演绎的形式化表达，记载的是人类对这个世界的数字化理解。不能拆的轮子就像那一篇篇丢了曲谱的宋词一样，能读，却不能唱。</p>
<p>    鄙人不才，正在自己研究怎么设计建造一种既好用又好拆的轮子。您没那么幸运，恐怕是等不到鄙人的技术做出来并发扬光大了。在那之前，多造轮子，多拆好拆的小轮子，应该是提高编程能力最好的办法了。</p>
<p>    以上。嗯。<br>
（文章属个人观点，与本人工作雇主无关。）</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[过期的凤梨罐头]]></title>
            <link>https://kkkf.vercel.app/posts/过期的凤梨罐头.html</link>
            <guid>https://kkkf.vercel.app/posts/过期的凤梨罐头.html</guid>
            <pubDate>Fri, 02 Sep 2016 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    有年双十一我在微博上看到一个好看的女生，顺便就保存了她的照片。回去的路上我遇到和女朋友约会的兄弟，我向他挥了挥手。</p>
<p>    我把QQ打开，把照片上传到了照片墙上，她的笑在我照片墙上像朵花，我觉得效果就像开了QQ会员。</p>
<p>    我在房间里百无聊赖的逛着虎扑，但我发了条朋友圈，配图是那个女生，但其实照片是我偷的，我不认识这个人。</p>
<p>    我的兄弟给我发来了消息，我吃惊他在这个夜晚居然有空理我。他问我怎么有女朋友了也不和他说，女朋友还那么好看。</p>
<p>    我应该告诉他的，但今天是双十一，我说不是是不是有点凄惨。所以我说是，她是我女朋友。</p>
<p>    他发了个滑稽的表情，我没有再回复他。我想这个表情有好多意思，我想他接下来的今晚很忙。</p>
<p>    但其实照片是我偷的，我不认识这个人。我又百无聊赖的逛了下虎扑，我心里慌得很，于是我把那条朋友圈删了。我刷新了一遍消息列表，没有收到晚安的消息。我换上了自己喜欢的睡衣，倒在熬不住的夜里。</p>
<p>    后来在路上遇上了另一个朋友，他就问我，你交女朋友了啊？我想应该是我兄弟告诉了别人，我交了一个女朋友，也许他还补充了一句，我女朋友还很漂亮。</p>
<p>    我该如何回复呢？我只能继续说是，我不能告诉他们照片是我偷的，我不认识这么一个人。</p>
<p>    因此我想我接下来要计划一下日子了，我不能再经常宅在家里了，因为很多人都知道了，我有个漂亮的女朋友。</p>
<p>    我每晚在外面晃到很晚，他们遇到晚归的我，“呦，x兄送完女朋友回来了啊”。</p>
<p>    以前我很少去电影院看电影，但我现在经常去看电影，我买两张票，我吃两份爆米花，因为他们都说，我有个女朋友。可我的旁边明明就空着一张座位，可是我的爆米花明明就吃不完。所以我总是浪费了一张票，我总是挺着饱胀的肚子出影院。</p>
<p>    也有些时候他们在很早的时候就在街上遇到我，就很奇怪的问我你女朋友怎么没和你一起？他们低头看了看我空空的两手，满脸关心的教导我应该要给女朋友买点东西的，别舍不得那些钱，女孩子是拿来疼的。我怔怔的点点头，笑着说待会儿买，边说边指着后面的商场，她上厕所去了。</p>
<p>    所以我后来上街就会买一些女生的东西，有些款式还真挺好看，我甚至想选个中性的颜色买给自己。我想，这样的话他们会以为这是我买给自己的吧，于是我总是买粉色。</p>
<p>    一个晚上我在想，他们如果要聚会，大家都会带上女朋友的时候，我该怎么办？我告诉他们照片是我偷的，我不认识这么一个人？我觉得我会被嘲笑到脸比猴子屁股还要红。于是我出去旅游了，当然是带上我的女朋友一起。</p>
<p>    我把美景发到了朋友圈，很多人都给我点赞，也有很多人评论我别光晒景。</p>
<p>    我说我拍照技术不好，那些图拍出来把她拍丑了，不让发。</p>
<p>    我想这时候女性朋友看到我的回复估计笑开了花，挽着男朋友的胳膊，摸着男朋友的手，感叹有个会拍照的男友好幸福。我想男性朋友看到是一副我不行而他好能干的神色。</p>
<p>    好久好久都没人知道，我不认识这么一个人，照片是我偷的。</p>
<p>    有一天我兄弟叫我去打球，打完后又请我去吃烧烤。这孙子平日很抠门，我在想他是不是今天买了国足赢球而中了大奖。</p>
<p>    他莫名其妙的和我说着分手没什么大不了之类的话，还夸我这种有才华的男生能找到更好的人。</p>
<p>    我才知道那天他借我手机打电话的时候看我相册了，发现我手机里把女朋友的照片全删光了。</p>
<p>    我的天，原来我就分手了啊。</p>
<p>    可我竟然一点也不伤心甚至想笑，如果我知道会是这样的话，我早就应该把手机给他们看。还好电影票在淘宝买的不太贵，还好出去旅游我也获得了精神自由。而此时此刻，我面前摆着的烧烤更香了，我都开始吧唧吧唧嘴来。兄弟问我怎么都不难受，我摆了摆手洒脱的说人要看得开，要学会放手。</p>
<p>    我又没女友了，一切又回到最初的起点。后来我真的遇到了一个女孩子，我一直都觉得我们会好很久，但她说我这人实在是够无趣，委婉的和我说了拜拜。</p>
<p>    我的心里空落落的，也在这个时候我喜欢上了听周杰伦，我喜欢那首明明就，“远方传来风笛，我却在意有你的消息”。我也开始成了诗人，我写了很多情诗，但我没有发出来，南风知我意，吹梦到西洲。那段日子好沉闷好孤独。</p>
<p>    于是我想起了我和前女友的那段日子，我看电影，我吃爆米花，我逛街，我旅游，多么丰富啊！可是我才记起来，我明明就没有前女友啊。</p>
<p>    照片是我偷的，我不认识这么一个人。</p>
<p>    又到了一年双十一，我没有出门，但我知道我的兄弟出去约会了，对象已经不是去年那个姑娘了，而我还是一个人百无聊赖的逛虎扑。</p>
<p>    我卸载了微博，鬼知道去年我干嘛把一个我不认识的女孩发到我的朋友圈。</p>
<p>    今年入冬感觉比较早啊，我穿着牛仔外套感觉很冷。我看完了虎扑的帖子，我便去刷新了消息列表，没有收到晚安，但时间告诉我我该睡了。</p>
<p>    我不知道为什么，我突然想发一条朋友圈。图片还是配上了去年那个女孩。</p>
<p>    正准备睡觉的时候我的手机突然亮了，是一个最近走得比较近的女孩。</p>
<p>    “那是你女朋友吗？”</p>
<p>    “不，照片是我偷的，我不认识这么一个人。晚安。”</p>
<p>    “那你干嘛把一个不认识的女生发到朋友圈啊？”</p>
<p>    我没有再回复了，我也没了睡意。我穿好衣服出了门，路上几乎没了行人，我沿着一条枝叶繁茂的街行走，树叶在月下隐隐绰绰，叶下的我在风里摇曳。我开始想念一个毫不存在的人。</p>
<p>    我想此时路上头是隐隐绰绰的光，路下头是相互依偎的影。路这头是她嫌我冰冷的手，路那头是我唱起不动听的歌。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[20]]></title>
            <link>https://kkkf.vercel.app/posts/20.html</link>
            <guid>https://kkkf.vercel.app/posts/20.html</guid>
            <pubDate>Thu, 07 Jul 2016 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>今天我 20 岁。</p>
<p>我本来想写一封所谓的给 20 岁的自己一封信，但发现我又要大面积回忆往事和进入圣人模式进行规划，而我早已看透了这无聊的把戏。</p>
<p>回忆是日复一日的晚餐民谣与做爱，晚餐吃多了会撑，民谣听多了会矫情，爱做多了会肾虚；而圣人模式就是用鸡汤的文字把自己感动得屁滚尿流结果第二天睡醒依然如旧。</p>
<p>我更愿意简单的，我愿——我以后的日子通透阳光；有一件件会被实现的奢望无论大小；有一些我努力去做到极致的疯狂无论是非；有几个要好的真心朋友无论男女；有一次想走就走的旅行无论远近；有一个和谐健康的家庭无论老少；有一个我想她，也想睡她，更想睡醒有她的女人独一无二 ………</p>
<p>我在这个黄金时代的岔路口，我感觉自己很酷很生猛，我期待未来的一切，仿佛什么也不能把我锤倒在地。</p>
<p>生日快乐。</p>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[老大哥]]></title>
            <link>https://kkkf.vercel.app/posts/老大哥.html</link>
            <guid>https://kkkf.vercel.app/posts/老大哥.html</guid>
            <pubDate>Sun, 22 May 2016 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    我坐在南苑球场的台阶上，眯着眼睛看着面前这个场的人打球。夕阳的余晖洒在我身上，我觉得自己有点帅。我拧开手里的可乐，喝了一口，可乐慢慢咽下时，我想起老大哥来。</p>
<p>    老大哥不老，和我一样大，之所以叫他老大哥是因为他看起来有种这个年龄不应该有的老气。第一次见到老大哥是初二的一个下午，我们分在了同一个队打三打三。那天阳光很温柔，时不时也会吹起舒服的风，我和他有着像老公老婆的默契，连连打出绝妙的配合来，对方被我俩打得落花流水，打到酣畅淋漓的时候我俩忍不住互相击了个掌，在空气中激起一声清脆的啪声，同队的队友看着咱俩，我现在想起来觉得他是个电灯泡。打完球，老大哥要请我喝可乐，我们坐在场边聊了起来，我们就这么认识了。</p>
<p>    老大哥是理科实验班的，而我是普通班。只是我在普通班常年坐第一把交椅，他在实验班末尾拖火车。理科实验班是每年都会调整的，普通班优秀的调进去，实验班垃圾的丢出来，而我挤破头也没挤进去，老大哥靠着他不错的物理也没被丢出来。</p>
<p>     后来我才知道老大哥并不是个省油的灯，他抽烟，他翻墙，他泡妹，对于初二时纯洁的我来说那实在是太可怕了。但我还是愿意继续和他玩，因为快乐。学校是禁止吃方便面的，可老大哥每周都能翻墙出去时给我带上几包康师傅，那时候的我便会蹦蹦跳跳的去打开水，由于禁止吃方便面所以也没有买过碗，所以我聪明的想到了用刷牙的口杯泡面，后来有次差点在宿舍被抓了，于是我又聪明的躲到了厕所吃。所以那时候啊，每周末躲在厕所用口杯吃方便面就是我例行的快乐。老大哥不爱写作文，但我觉得他是个很有文学内涵的人，他的包里总是有一本王小波的书，他总是对我说，“在自己的作品里总是喜欢和生殖器扯在一起的作家里，王小波敢称第二，恐怕无人敢称第一”，于是我想老大哥肯定是和王小波学坏了，他球场上喷垃圾话的时候总是喜欢带上生殖器来组词，那对于纯洁的我来说实在是太色情了，每当那时候，我球都投不进了。但老大哥也会时不时说一些我要做阅读理解的话，他在看月亮的时候曾说过“这是个喧嚣的世界，可我从未觉得它安静过”。我望着他，若有所思。</p>
<p>    只是尽管后来我和他很熟了，可我依然会觉得他很神秘。他似乎有用不完的钱。有时候走在街上要买东西的时候，前几分钟他说自己没带钱，可刚走几步后他又说有钱了，每次我都在心里嘟囔着，“不就是想我买单嘛，还不是见我没表态就改口了”。我有次在老大哥的寝室里等他的时候，无意中听到他室友说半夜上厕所的时候总不见老大哥人在床上，可第二天早上醒来他明明就在床上。我当时听了觉得后背有点凉，我也想过委婉的问老大哥是不是梦游，可后来想，梦游的人怎么知道自己梦游呢，也就不了了之了。老大哥特别喜欢穿兜很多的裤子，而且哪怕是夏天我也没见他穿过短袖，这我就按耐不住了，我问他不热吗，他每次都是嘿嘿的说怕晒得像我这么黑。</p>
<p>    我从没见到老大哥面容凝重过，直到那次他接到一个电话。</p>
<p>    “什么事？地点？力度多大？”</p>
<p>    对方接下来说了些什么。</p>
<p>    “五牛顿？这样会见血的！”老大哥咆哮了。</p>
<p>    “如果我不做呢？”</p>
<p>    对方接下来说了些什么，老大哥听完表情痛苦的闭了几秒眼睛，然后回复了一个“好，我做。”</p>
<p>    我问老大哥发生了什么，老大哥顿了顿，说明天告诉我，他现在要走了。但他并没有告诉我他要去哪里。</p>
<p>    翌日，在我忐忑不安中老大哥来找我了，我方才知道许多我一直不知道的事。</p>
<p>    老大哥是扒手，我并不愿意这么说，我更愿意给他一个类似于小李飞刀的名号。这么小的年纪是扒手，这的确难以置信，但一切是因为他爸。他爸是个江湖闻名的小偷，外号鹰爪，在老大哥小的时候他爸教会了他很多技能，但老大哥手速天生很快，在不断的练习中，他越练越快，要命的是不仅是速度快，位置也把握得准，手持刀片经过之处，手起刀落，想要的东西必定落入手中。他这本领早已青出于蓝而胜于蓝，超过了他爸。他爸曾经是个团队的老大，可他爸在一次行动中没有脱开身被警察抓了个正着，锒铛入狱。没有他爸之后的团，内斗之后权力交接，新把手慢慢把它扩大成了黑帮。团里的元老从小看着老大哥长大，对他的本领早已觊觎，但老大哥说什么也不愿意入帮。老大哥的爸爸曾经偷过一份保密程度很高的某党文件，一旦被发现必然是死罪，那几位元老每次都是以此事相威胁，老大哥不得不就范为他们做了很多事。</p>
<p>    我听完，惊愕不已，不停的咽着口水，更是说不上话来。老大哥半晌没有说话，然后他拍了拍我的肩膀。</p>
<p>    “我要走了”</p>
<p>    “走了？去哪？”</p>
<p>     “去一个他们抓不到我的地方”，老大哥说完给我敬了个军礼，我莫名其妙的看着他。</p>
<p>    “那我们是要离别了吗，什么时候再见？”</p>
<p>     “是离别，但我不会说什么时候可以再见，我不会如期归来，而这正是离别的意义不是吗。”</p>
<p>    我恍惚中说不出话来，这时开始起风了，天气特别闷热，后来那天下了场很大的雨。老大哥递了瓶可乐给我，我打开喝了一口。</p>
<p>    那天我们的分别是以击掌结束，一如那天打球的我们。</p>
<p>    依然记得那天的可乐一点也不可乐。</p>
<p>     老大哥走了，我第二天去他们班的时候他的桌子已经空了。到现在我也再也没见过他了。</p>
<p>    大学军训，在太阳下敬军礼的时候太阳晃得我睁不开眼，我眯着眼睛蓦然想起来老大哥，想起来那天他给我敬的莫名其妙的军礼，想起来他说要去一个抓不到他的地方。我不自觉的笑了，我想我知道了他去了哪儿。</p>
<p>    到现在几年过去了，我依然爱着篮球，但我再也没遇到过如老大哥一样默契的队友。</p>
<p>    这是个喧嚣的世界，可我从未觉得安静过。他的繁荣，他的昌盛，带给人们的只是更多的疲惫，更多的抱怨。于是我捂住双耳，不如听他的繁荣，不如听他的疲惫，不如听他的昌盛，也不去听他的抱怨。只是这世上总有那么一些人，哪怕他从未告诉我他去了哪里，可走过的那些快乐一直留在我心里。</p>
<p>    有些故事还没讲完那就算了吧，那些心情在岁月中迟早会蒸发。他们在哪里呀，他们都老了吧。</p>
<blockquote>
<p>写于固体物理课上</p>
</blockquote>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
        <item>
            <title><![CDATA[有趣的姑娘真美]]></title>
            <link>https://kkkf.vercel.app/posts/有趣的姑娘真美.html</link>
            <guid>https://kkkf.vercel.app/posts/有趣的姑娘真美.html</guid>
            <pubDate>Fri, 20 May 2016 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>    从周二下午开始就没去上过课了，周二下午打球把膝盖摔伤了，第二天膝盖肿得难以行走，去了市医院检查，医生要我先回来观察，倘若两天后情况不好的话再去去做进一步检查甚至需要手术，他提到了半月板这个词，我当时着实被吓到了，这可是我只会在NBA球员身上听到的伤病啊。回来的路上，我心里是很忐忑的，更多的是害怕，怕再也打不了球了，望着车窗外转瞬即逝的风景，我很沮丧。吃了一天的云南白药，热敷了一天，第二天醒来的很早，下意识的抬了抬膝盖，发现能抬腿了，我心里的石头落地了。到现在已经可以行走了，只是会疼，安心休息，两周后伤好后再去球场干一场。</p>
<p>    昨晚一个人在宿舍，把灯都关了，只留下橘色的台灯的光，我在这静默中听着歌。室友去俱乐部听千瓦演唱会了，忽然手机震动了，他给我录了现场的开场曲，是灌篮高手的主题曲，那旋律听了就想战斗。刚刚听完，却收到了小芳姑娘的消息，同样是一段语音，我很讶异。小芳姑娘是机械院的，工设专业，我和她从没见过面，是从一个群里加的好友，最初就是因为灌篮高手而聊起来的，她是个很喜欢灌篮高手的女生。和她就只有过那次的聊天，之后记忆里没有了。所以在收到语音的时候我讶异。点开语音，原来也是开场曲，我不自觉的笑了，这姑娘真是用心，还记得她列表里躺着一个和她一样喜欢着灌篮高手的男孩儿啊。她说她听到这首歌的时候克制着自己，不然就蹦起来了，我想象了一下那种感觉，我觉得像只兔子。和她聊了几句，我想还是不聊了吧，她还是好好听晚会吧，唱歌的晚会这种活动我觉得是要连贯的去投入才嗨。一个半小时后，我听到了雨拍打窗户的声音，随即声音越来越大，我起身走到窗前看了看外面，我还是给小芳姑娘发了消息：外面下着很大的雨，晚会结束回去注意点...不多时她便回复我了，我这人其实话不多，但只要我想说了，话就会多起来而且往往还会有趣，而恰好小芳姑娘是个很有趣的姑娘，趣味相投。而进一步认识后，她不仅有趣，还热爱着跑步，已经是一种习惯，这让我觉得很厉害，能坚持跑步这是一件不容易的事，而且能改变一个人的气质。</p>
<p>    我慢慢的觉得这姑娘很善良很有趣，我开始觉得有趣的姑娘好美，哪怕我没见过她，我想象中便会有一个很美但模糊的轮廓。我想气自华就是这种感觉吧，姑娘就是要这般明媚。对于我以后的女朋友，我也希望她是这般明媚，在她的明媚被乌云遮盖时我给她拨开乌云重见日，而她一直会是我的小太阳。</p>
<p>    对了，小芳姑娘叫李芳，给这个有趣的姑娘写句诗吧：庭前桃李满，院外小径芳。</p>
<blockquote>
<p>2016.12.9这位姑娘成为了我女朋友。</p>
</blockquote>
]]></content:encoded>
            <author>CocaColf@gmail.com (CocaColf)</author>
        </item>
    </channel>
</rss>