[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"list-\u002Farticles\u002F4":3},{"items":4,"total":61},[5,21,31,40,50,60,75,83,92,103,113,121],{"id":6,"type":7,"slug":8,"title":9,"date":10,"category":11,"tags":12,"body_markdown":15,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":20},130,"article","scripts-to-improve-md-writing","使用脚本改进VSCode Markdown博客编写体验","2022-08-25 08:46","tech",[13,14],"Markdown","图片","\n## 背景\n\n本博客用的是Hexo，最近迁移了很多以前写在别处的文章过来，在迁移过程中发现最不方便的一件事情是图片的处理。\n\n大概的目录结构：\n\n```\n- source\n    - _posts  文章目录\n        - hello1.md  文章\n        - hello2.md  文章\n    - assets  图片目录\n        - hello1  与文章对应的子目录\n            - 01.jpg\n            - 02.jpg\n        - hello2  与文章对应的子目录\n            - 01.jpg\n            - 02.jpg\n```\n\n因为使用的是通用的工具（如VSCode）来编写Markdown，没有专门针对图片资源做额外的处理，因此在写文章的时候如果碰到需要插图，需要经历以下几步：\n\n1. 打开图片目录\n2. 新建与文章对应的子目录\n3. 放入图片\n4. 重命名图片\n5. 在文章中插入图片标记，类似这样`![image 01](\u002Fassets\u002Fhello1\u002F01.jpg)`\n\n其中第1步和第5步都涉及到路径的问题，而且都需要手工进入或者手工输入，当路径比较深或者文章名称比较长的时候，既不方便也容易出错。\n\n\u003C!-- more -->\n\n## 目标\n\n在忍受了这种不方便很久之后，本着“重复的事情一定可以用代码解决”的想法，我决定找到一种更自动化的方式来做。首先，定义一下目标：\n\n1. 图片存入与文章相关的路径\n\n    因为我的图片路径是与文章相关的，因此固定将图片统一放到某个目录的方式不纳入考虑，这就排除了很多现成的解决方案。\n\n    希望这个方案可以自动建立与文章相关联的目录并打开，以便放入图片。\n2. 自动完成重命名\n3. 自动将图片地址填入文章中预留好的位置\n\n## 实现\n\n首先，新建一个脚本放入博客目录中，以便调用：`utils\u002Fassets.js`。\n\n### VSCode调用\n\n当需要插图的时候，对应的文章刚好是VSCode中当前编辑文件，因此希望能将当前文件路径直接传递给脚本。这里使用了一个插件：[Command Runner](https:\u002F\u002Fmarketplace.visualstudio.com\u002Fitems?itemName=edonet.vscode-command-runner)。\n\n这个插件可以通过VSCode配置或者`package.json`来定义一些自定义的命令。于是我在`package.json`中加了这么一段：\n\n```json\n{\n    \"commands\": {\n        \"assets\": \".\u002Futils\u002Fassets.js ${file}\"\n    }\n}\n```\n\n这样就可以通过VSCode直接调用脚本并且传入当前文章的信息。\n\n![command runner 1](\u002Fassets\u002Ftech\u002F2022\u002Fscripts-to-improve-md-writing\u002F01.png)\n\n![command runner 2](\u002Fassets\u002Ftech\u002F2022\u002Fscripts-to-improve-md-writing\u002F02.png)\n\n接下来就是脚本的实现了。\n\n### 新建图片目录\n\n原理比较简单，首先计算一下文章对应的图片目录，然后使用`fs.mkdirSync`新建对应的目录。最后为了方便使用，调用一下系统的`open`命令，把刚新建好的目录打开。\n\n```javascript\nconst fs = require('fs');\nconst path = require('path');\nconst { spawn } = require('child_process');\n\nconst mdPath = process.argv[2];\n\nif(!mdPath) {\n    console.log('md file path required.');\n    process.exit(1);\n}\n\nconst assetsPath = mdPath.replace(\u002F\\\u002F_posts\\\u002F\u002F, '\u002Fassets\u002F').replace(\u002F.md$\u002F, '');\nfs.mkdirSync(assetsPath, { recursive: true });\n\nconsole.log('opening ' + assetsPath);\nspawn('open', [assetsPath]);\n```\n\n> 注意：这个代码实现比较粗糙：\n>\n> 1. 在计算图片目录时粗暴地使用了`replace`来替换，更稳妥的办法是使用相对路径来计算\n> 2. 目录路径未考虑windows系统，这里应该使用`path.sep`更好\n> 3. 未考虑新建目录失败的情况\n> 4. 未考虑没有`open`命令的系统，应该使用封装好的库更好\n\n### 自动重命名\n\n要想让代码为我们放进去的文件自动重命名，首先需要让代码知道“有一个文件被放进去了”，这里就需要使用到`fs.watch`。这个方法可以监听一个目录，当目录发生变化时触发回调，这样我们就可以在回调中完成重命名。\n\n> 注意：`fs.watch`并不是非常好用，在macOS下放入一个文件，会触发`rename`事件两次，不管是事件名称还是触发次数都不太正常。更好的方法是使用`chokidar`这样的库来监听变化。\n\n我这里选择使用数字编号来命名，如果目录中已经有文件存在的话，就需要先知道最新的编号是多少。\n\n```javascript\nlet index = 0;\nconst files = fs.readdirSync(assetsPath);\nfiles.forEach((filename) => {\n    if (\u002F^\\.\u002F.test(filename)) return;\n    if (!\u002F^[\\d]+\\..*$\u002F.test(filename)) return;\n    index = Number(filename.replace(\u002F[^\\d]\u002Fg, ''));\n});\nconsole.log('lastIndex: ' + index);\n```\n\n这段代码首先将序号`index`设为`0`，然后对以纯数字命名的文件进行扫描并解析其数字作为最新的序号。\n\n> 注：这个逻辑也不严谨，如果数字位数不同的话，顺序不是按序号排列的，可能得到错误的结果。但因为我的命令规则全部是2位数字，所以默认这个问题不存在，忽略了。\n\n接下来是监视新的文件并重命名：\n\n```javascript\nconst getNewName = (filename, isPrev = false) => {\n    if (!filename) return filename;\n    const targetIndex = isPrev ? index : index + 1;\n    return filename.replace(\u002F.*(\\..*)$\u002F, (targetIndex + '').padStart(2, '0') + '$1');\n}\n\nfs.watch(assetsPath, {\n    persistent: true,\n}, (e, filename) => {\n    if (\u002F^\\.\u002F.test(filename)) return;\n    if (filename === getNewName(filename, true)) return;\n    setTimeout(() => {\n        try{\n            const newName = getNewName(filename);\n            const oldPath = path.join(assetsPath, filename);\n            const newPath = path.join(assetsPath, newName);\n            fs.renameSync(oldPath, newPath);\n            console.log(filename + ' renamed to ' + newName);\n            index++;\n            \u002F\u002F fillMd(newPath);\n        } catch (e) {\n            \u002F\u002F nothing\n        }\n    }, 500);\n});\n```\n\n这段代码首先定义了一个`getNewName()`方法，用于获取即将被重命名的文件的新文件名。这个方法接受一个`isPrev`参数，它的作用是判断要获取“前一个（已生成）”的文件名，还是“下一个（即将生成）”的文件名。\n\n因为`fs.watch`对新文件和重命名的文件都触发`rename`事件，因此无法区分是被放入了一个新文件，还是之前放入的文件被代码重命名了。这里要做一个判断，将文件名与“前一个（已生成）”的文件名对比，如果相同，说明是刚刚重命名过的文件。\n\n接下来的逻辑比较好理解，就是重命名的过程了，如果成功，就将序号`+1`。\n\n> 值得注意的2个点：\n>\n> 1. `setTimeout`的作用，主要是因为事件会连续触发两次，希望在事件都触发完之后再处理（可能并不是必要的）\n> 2. `try...catch`的使用，也是因为事件会连续触发两次，第2次一定会失败，因此加一个`try...catch`，并不是为了考虑严谨\n\n## 填入Markdown指定位置\n\n在上面的代码中有一个注释的`fillMd()`的调用，它的作用就是将图片地址填入Markdown指定位置。\n\n具体而言，在编写文章时，我会先留一个“洞”，也就是图片标记中的地址是空的：\n\n```\n![图片描述]()\n```\n\n在调用`fillMd()`时会将图片地址填入上述标记中的“洞”：\n\n```javascript\nconst fillMd = (imagePath) => {\n    const mdContent = fs.readFileSync(mdPath, 'utf-8');\n    const imageRelativePath = imagePath.replace(\u002F^.*\\\u002Fsource\\\u002Fassets\\\u002F\u002F, '\u002Fassets\u002F');\n    const newContent = mdContent.replace(\u002F\\!\\[(.*?)\\]\\(\\)\u002F, '![$1](' + imageRelativePath + ')');\n    fs.writeFileSync(mdPath, newContent);\n};\n```\n\n原理也很简单，计算图片的地址并读取Markdown文件，然后通过正则表达式替换的方式将地址填进去，再将新内容写入原文件即可。注意这里的正则表达式没有加`\u002Fg`标记，也就是只替换找到的第一处“洞”，说人话就是一次只填一个，这正是预期的工作方式。\n\n## 效果\n\n有了脚本之后改进的流程：\n\n1. 写文章&留好“洞”（可以写一个就往下处理一个图片，也可以全部写好，最后一起处理）\n2. 运行“assets”脚本\n3. 在自动打开的文件夹中放入图片\n4. 没有然后了\n\n演示视频\u003Chttps:\u002F\u002Ftwitter.com\u002FTooooooBug\u002Fstatus\u002F1562273683246555142>（需要翻墙）。\n\n## 总结和展望\n\n我始终认为，写代码是为了解决问题，至于写得是否完备是否漂亮都是其次的。这个例子其实也是一次“为自己写代码来解决问题”的过程，整个代码只有60行，尽管它不是很严谨，也不是很完美，但仍然算是很满意的一次实践。\n\n如果这个工具继续做下去的话，可能会有几个方向：\n\n1. 做成npm包，提升易用性\n2. 适配更多的文章\u002F图片目录规则，以适应更多的情况\n3. 替换一些工具\u002F写法，提升代码严谨性\n4. 加入更多的功能，比如\n    1. 反过来从md文本中获取图片信息，然后移动\u002F下载保存等\n    2. 加入图床自动上传和替换功能\n    3. ...\n\n不过我只是想把它当成解决自己问题的小工具，因此可能不会再继续花时间深入了，有兴趣的朋友可以参照一下做出更适合自己的工具。\n\n## 附：对Twitter上一些观念的回复\n\n1. 可以用XXX软件：是可以的，但之所以选择Markdown来写东西就是看中了纯文本的通用性，不希望被绑在某个工具上。事实上我也从来不用任何Markdown工具的高级功能，我觉得对纯文本每个字节的掌控是非常必要的。这个脚本也是通用的，你可以手工运行它。\n2. 可以用笔记软件：我认为笔记和文章\u002F博客写作是两种不同的用途。尽管有人喜欢all in one，把文章\u002F资料采集\u002F知识管理\u002F分享\u002Ftodo\u002FOKR等都放入笔记软件，但我不喜欢，我认为笔记就是用来记录的，我自己写的笔记软件也没有任何采集\u002F分享之类的功能，仅仅是输入、记录和搜索而已。\n3. 可以用图床类工具：个人不喜欢将图片和文章分开保存，这些松散的结构间很难建立起联系，在整理的时候会非常困难。此外图床类也会涉及到付费\u002FCDN\u002F防盗链等额外工作。\n\n当然，尽管现阶段不适用于我的文章\u002F博客的场景，但推友们的回复仍然有不少值得学习的地方，也有不少好工具，值得收藏。\n",null,0,"2026-08-28 04:37:17","published","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2022\u002Fscripts-to-improve-md-writing\u002F01.png",{"id":22,"type":7,"slug":23,"title":24,"date":25,"category":11,"tags":26,"body_markdown":29,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":30},131,"talking-about-code-style","关于代码风格","2022-08-19 18:00",[27,28],"代码风格","代码规范","\n> 2016年的旧文，之前未发表，时隔6年后整理一下发出来。\n\n如果从我写下第一行代码开始算的话，我写代码的历史大概已经超过20个年头了。这20多年里接触过各种各样的语言，也接触过各种各样的代码风格。当然也见证了很多代码风格引起的撕逼大战。\n\n对于代码风格这种事情上的架，我一般不参与吵的，觉得是一件很无聊的事情。直到今天（2016年）打开微博看到几十条评论，有点蒙圈。\n\n事情的起因是上周我在微博上发了一个条吐槽：\n\n> 最近面试好多人连12345这五行代码的执行顺序都讲不清楚。就算不知道5我也忍了，好多人连1234这四行代码是什么顺序跑的都搞不清楚。现在前端门槛低到这样的程度了么？（为了把它们排到第12345行上，刻意调整了代码格式，请轻拍。）\n\n附上了一段代码：\n\n```javascript\nfor(var i=0;\n\t\ti\u003C3;\n\t\ti++){\n\tsetTimeout(function(){\n\t\tconsole.log(i)\n\t},0)\n}\n```\n\n然后今天收到了一堆吐槽：\n\n- “花括号不换行，烧死”\n- “等号两边不换行，烧死”\n- “逗号右边没加空格，烧死”\n\n在微博上也零碎地做了一些回复，不过还是觉得没有把想说的话说完，于是有了这篇。\n\n\u003C!-- more -->\n\n## 什么是代码风格\n\n具体一点说，代码风格即是代码长成什么样子。哪些因素会决定代码长成什么样子呢？\n\n- 换行符\n- 空行\n- 空格\n- 缩进\n- 分号\n- 括号\n- ...\n\n自然，决定代码长成什么样子的还有很多因素，比如：\n\n- 变量声明在一行还是每行一个\n- 变量声明在函数最前面还是使用的地方\n- 函数声明还是函数表达式\n- 用`if`还是用`switch`\n- 写成三个函数还是一个函数\n- 分模块还是一个文件到底\n- 面向过程还是面向对象\n- ...\n\n虽然这一些因素也会决定代码长成什么样子，但是同时，它们也决定了代码的其它部分，比如结构是否容易理解和维护，性能是否足够好……简单说，这些会决定风格，但决定的并不只是风格。\n\n所以本文的想说的仅限于前面说的那一堆。\n\n## 代码风格的重要性\n\n好的代码风格到底有什么作用呢？为什么如此多人强调代码风格呢？一般可以找到如下答案：\n\n- 避免bug\n- 方便阅读\n- 便于维护\n- 提高代码编写效率\n- 方便团队协作\n\n然而仔细想一想，仅仅靠我们上面所说的“风格”，这些目标似乎都无法达成。所有这些目标都需要同时依赖其它的工程手段或者流程，才有可能达到一部分效果。甚至可以说代码风格对于这些目标来说是不太重要的。\n\n## 我和代码风格的战斗史\n\n一方面，基于上面的怀疑而产生的结论，一方面，也开始接触各种各样的代码风格，有加空格的，有不加空格的，有喜欢加很多空行把代码写松散的，也有一行空行都没有让结构很紧凑的，有tab缩进的，有2空格缩进的，有4空格缩进的，有分号党，有不加分号党的……\n\n这些代码中有很好理解和维护的，也有不太好理解和维护的，但无论如何，都很难得出代码风格与可维护之间有直接关系的结论。如果一定要说的话，只能说：能把代码写得易于理解和维护的，大概率代码风格也是统一的。\n\n基于这样的认知，我开始不拘泥于代码风格的限制，在不同的项目中会使用不同的风格（在同一个项目中还是会使用同一种风格，并使用工具严格约束），心情好时加加空格，心情不好时不加空格。发现这些事情中，除了在没有注释的情况下用于逻辑分段的空行以外，其它的其实对代码编写和阅读几乎没有影响。\n\n但……上面所说都限于个人项目。在团队作战中风格仍然要保持统一，而原因，可能更出乎意料，只是为了让大家的IDE不要乱改代码造成diff困难。\n\n## 小结\n\n代码风格是个严肃的事情，但将它上升到非常高的高度则大可不必。不关注代码风格的不能叫程序员，只关注代码风格的不能叫好的程序员。\n\n> 本来有一节中“除了代码风格，程序员更应该关注什么”，但是2016年没有写，就不再补了。\n","",{"id":32,"type":7,"slug":33,"title":34,"date":35,"category":36,"tags":37,"body_markdown":39,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":30},89,"2020-summary","2020总结","2021-03-13 15:00","life",[38],"年度总结","\n2020年的第一天是如何开始的已经完全没有任何印象了，现在来猜测一下，估计已经是快要开始进入回家过年的兴奋中了。然而这份兴奋还没来得及留下什么印象，就被突如其来的疫情冲散得无影无踪。\n\n总而言之，一个复杂的新年开始来了。\n\n1月23日，年二十九，武汉封城，随后湖北封省。就此开始没有走亲戚的过年，以及随后的居家办公。\n\n于我而言，每天看着疫情数字和武汉的各种神奇操作，焦虑和揪心都少不了。但就个人而言，却并不觉得有很不幸。目睹了身边的亲戚，因为隔离不能重返工作岗位，一边心里无比焦急，一边还要顾及过年的氛围多少需要一些自我安慰。相比之下，我能不受影响地工作，公司继续保障每个月的收入，算幸运了。\n\n这段日子不能说刻骨铭心，却也算得上印象深刻了。有吃有喝，还有点无所事事，能和家人在一起待这么久，也很好。\n\n\u003C!-- more -->\n\n2月20日启程，带着一车的南瓜白菜萝卜和几罐子红糖回到深圳，过程坎坷但还好结果顺利。居家隔离的日子也规律，起床、上班、做饭，还有每天给医生报告体温，除此之外也没什么其他的事情。\n\n惠惠常说怀念这段日子，因为我经常做饭。但我们都清楚，这个怀念里还有一些当时很重要的期待。\n\n3月的深圳一片萧条，气氛压抑。医院人不少，但医院是一个经常带来负面情绪的地方。这种压抑的情绪几乎覆盖了整个上半年，无论去商场吃饭还是去海边散步都很难消解。\n\n下半年的深圳逐步恢复了以往的活力，节奏也就逐步明快起来了。然而于我而言，似乎只有两件事，就是工作和赶稿。\n\n工作压力的增大可能也不是一天两天完成的，但组织的反应总会是要慢一些，压力上来后基本上每周都在招人、写项目中度过。工作日的时间留给公司，周末则几乎在赶稿和需要赶稿的焦虑中度过。我的感受只有一个——时间过得飞快。\n\n除了工作以外，值得一说的是，今年开始折腾键盘了，这实在是一个极好的减压方式，也让我找回了一些最初做代码的欣喜，就是要创造世界改变世界呀。\n\n十一前很草率地决定去青岛，在青岛和济南度过了十一假期。最大的意义大概是真的能散心，暂时放下所有的工作，去感受一下不一样的城市。总体来说，还是不错的。\n\n然而这趟旅程也留下了痛风的后遗症，第一次发作的时候真的会从心底喊出来，健康多么重要。以后应该会多注意一些了。\n\n最后必须得记录一下理财，因为开始认真操盘了。2019年底清掉了被套牢的股票，也去了杠杆，开始重新操作。很幸运的是，在经历了数次熔断之后，美股行情爆发了。所以看上的股票都涨了不少，算是赶上了。\n\n公司的激励终于也到账，并跟随行情涨了一波，有一些不真实。\n\n回忆起来，2020年发生了太多事，曲折而漫长，有很多非常重要的节点值得做一些记录。\n\n但，时间却又突然到了2021，滚滚红尘，永远向前。明天一切都更好。\n",{"id":41,"type":7,"slug":42,"title":43,"date":44,"category":45,"tags":46,"body_markdown":49,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":30},188,"coding-is-a-sure-thing","写代码是一件与确定性为伍的事情","2020-09-19 13:39","web",[47,48],"stream","node.js","\n我们所处的世界充满了各种各样的不确定性。但有一件事是不存在不确定性的，即写代码。\n\n多年以前，在我刚入行不久的时候，有一位前辈和我说过“出现问题的时候，先怀疑是自己的原因，因为机器是不会出错的，错的永远是人。”这句话我记了很久，也时不时就会翻出来回想一番，也会冒出很多更细的想法：那机器不也是人造的？机器的程序不也是人写的？就一定是自己的原因，不能是别人的原因吗？但反复想了很多年，还是觉得这句话相当有道理，即使是别人的原因，那错的也是人而不是机器。\n\n这其实就是写代码时的确定性，我们写的代码会被怎么运行，是非常确定的。即便它要依赖更多的底层软硬件机制，但仍然是确定的，只是找出这个确定性的过程更加复杂而已。\n\n## 一个例子\n\n> 如果你看不懂例子，跳过就好。\n\n### 背景\n\n项目中需要上传下载文件，使用的是某云服务的存储服务。在下载的部分，为了方便，使用Node.js封装了一个下载方法，返回一个`Stream`，而这个`Stream`本质上是由http请求库request.js请求后返回的。最后由koa框架返回这个`Stream`给浏览器。\n\n请求下载 -> 下载方法 -> request.js请求云服务 -> 返回`Stream`\n\n代码大致如下：\n\n```javascript\nrouter.get('\u002Fapi\u002Fdownload-file', async (ctx) => {\n    ctx.body = Download.getPrivateStream(ctx.query.fileId);\n});\n```\n\n然而，同样的代码，在不同的项目下，表现却大不一样，A项目访问图片时是直接在浏览器中显示图片，B项目访问同样的图片却变成了下载。调试工具一查看，发现它们有不一样的HTTP Header返回：\n\n- A项目`Content-Type: image\u002Fpng`\n- B项目`Content-Type: application\u002Foctet-stream`\n\n\u003C!-- more -->\n\n### 解决\n\n经过初步排查，A B两个项目中都没有手工设置过这个Header值，可以基本确认这个差异并不是由于下载部分的写法造成的。\n\n虽然原因不是很明朗，但这个问题却很好解决：手工加一个设置`Content-Type`值的代码，一行代码就能解决。\n\n```javascript\nrouter.get('\u002Fapi\u002Fdownload-file', async (ctx) => {\n    ctx.type = mime.getType(fileExt);\n    ctx.body = Download.getPrivateStream(ctx.query.fileId);\n});\n```\n\n### 寻找确定性\n\n虽然上面的代码解决了这个应用场景下的问题，但却并没有找到真正的原因。也就是说，这里遗留了一段具有不确定性的代码。\n\n为了找到真正确定的原因，我在接下来的两天内花了一个晚上+一个上午的时间，从下载的封装到request.js的源码都一一做了排查，最终找到了原因。\n\nrequest.js在发现`response`（`Stream`）被`pipe`到一个新的`Stream`的时候，会尝试使用新`Stream`的`setHeader`方法，将源响应中的HTTP header都设置到新的`Stream`上。\n\n```javascript\nif (dest.headers && !dest.headersSent) {\n    if (response.caseless.has('content-type')) {\n        var ctname = response.caseless.has('content-type')\n        if (dest.setHeader) {\n            dest.setHeader(ctname, response.headers[ctname])\n        } else {\n            dest.headers[ctname] = response.headers[ctname]\n        }\n    }\n\n    if (response.caseless.has( 'content-length')) {\n        var clname = response.caseless.has( 'content-length')\n        if (dest.setHeader) {\n            dest.setHeader(clname, response.headers[clname])\n        } else {\n            dest.headers[clname] = response.headers[clname]\n        }\n    }\n}\n```\n\n然而调试到这里的时候会发现A B两个项目走到了不同的逻辑。B项目的新`Stream`（代码中的`dest`）并不存在`setHeader`方法。\n\n通过查看koa的源码，会发现这个`dest`其实就是`ctx.body`。按理说，`ctx.body`是一个http response stream，肯定是有`setHeader`方法的。那么，唯一的解释就是：有别的代码动过`ctx.body`了。\n\n最后经过一番排查，找到了一个万万想不到的事实：`koa-logger`会替换`ctx.body`\n\n```javascript\n\u002F\u002F calculate the length of a streaming response\n\u002F\u002F by intercepting the stream with a counter.\n\u002F\u002F only necessary if a content-length header is currently not set\nconst length = ctx.response.length\nconst body = ctx.body\nlet counter\nif (length == null && body && body.readable) {\n    ctx.body = body\n        .pipe(counter = Counter())\n        .on('error', ctx. onerror)\n}\n```\n\n原来，`koa-logger`为了记录响应体的大小，粗暴地将`ctx.body` `pipe`到了一个`Counter`实例上，并就此替换了`ctx.body`。刚好，A项目没有使用`koa-logger`，而B项目使用了。\n\n\n## 结\n\n因为出于对不确定性的不放心，所以尽管有现成的解决办法，但仍然没有放弃对它的追查。谁能想得到，最终的问题出在一个看起来人畜无害的代码库中呢？好在，花费了一番工夫，总算把一个不确定的事情变成了确定的事情。\n在这个例子中，可能我们会再慎重地评估这个代码库，即使不能第一时间进行替换和修复，也可以给出足够的文档，而这将为后续代码的稳定运行打下坚实基础。\n\n写代码是一件与确定性为伍的事情，如果你觉得你的代码有诸多不确定性，那只能说明一件事情，就是你又欠工夫了。\n\n> 2022-08更新：2年过去了，koa-logger仍然没有修复这个问题。\n>\n> [issue](https:\u002F\u002Fgithub.com\u002Fkoajs\u002Flogger\u002Fpull\u002F81) [Pull Request](https:\u002F\u002Fgithub.com\u002Fkoajs\u002Flogger\u002Fpull\u002F85)\n",{"id":51,"type":7,"slug":52,"title":53,"date":54,"category":36,"tags":55,"body_markdown":59,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":30},88,"wind-in-july-ran-in-auguest-and-memos-in-september","七月的风 八月的雨 还有九月的碎碎念","2020-09-16 09:29",[56,57,58],"随笔","内卷","读书","\n七八月，又是一年新朋友从校园走入职场的季节，也是又一年职场困惑季的开始。\n\n## 加班与内卷\n\n在996被全民声讨之后，“奋斗逼”又成为了被声讨的对象。然而很多人声音很大，却未看明白背后的关系。\n\n去过台湾的话，会发现台湾的朋友很敬业，在自己的岗位上怡然自得，初看非常羡慕。回来细想，这种怡然自得背后却是“看不到希望”的另一种表达。人活世上，是需要希望的。于有的人而言，努力工作升职加薪便是希望，于另一些人而言，不再相信努力工作可以升职加薪，于是觉得职场不是一个有希望的地方。\n\n《肖申克的救赎》是IMDB评分排名第一的电影，也是我最喜爱的电影之一。这部影片在我看来就是在讲希望。有希望就有可能，而一旦失去希望，即便你逃离了高墙大院，仍然会被自己困住。\n\n生活有意思的地方就在于，你可以选择相信，也可以选择不相信。有选择便是这个时代最大的幸运。我看到很多年轻朋友心中仍然有梦想、眼里仍然有星光。\n\n\u003C!-- more -->\n\n## 声音\n\n在今天，互联网上目光所及全是“内卷”。我相信很多朋友也一样逃不开铺天盖地的词语轰炸。\n\n但，我一直不喜欢“内卷”这个词，这倒并非因为这个词是好或不好，而只是对热度高的词保留本能的谨慎。\n\n二十年前，我们说 “斑竹” “886” “GG\u002FMM” “520” ，这叫网络用语。十年前，我们说 “十动然拒” “喜大普奔”，这叫网络热词。再然后，出现了 “生态化反” “轻奢” 等词，我把它们理解为“诞生于运营需求的生造词”。\n\n在今天的互联网上，你很难碰到一篇不夹杂运营流量目的的文章。所有的平台都在造网红带流量，至于读者和内容，没有人在乎。这一行玩得过火之后，也会有玩不下去的，比如咪蒙。然而还有更多的内容，就那样一篇一篇地拼凑到你我的眼前，让人反胃。\n\n我时常在说，中文互联网已经没有东西可看了。我也时常在想，互联网上愿意分享真知灼见的那批人，到底去哪了？\n\n然而想想发一个句号就会被炸号的敏感时期，想想个人博客都不得备案的大环境。就不难理解了。更何况还有发长文不如编故事的平台，做内容不如抄段子的流量分配策略推波助澜。曾经听某平台负责内容运营的人聊过他们是如何做垂直流量的，然而直到我私下去问他，他都没有意识到他们的运营策略驱赶了一大批高质量小众作者。\n\n所以，在这个时代，声音大并不是有道理，更可能是因为好赚钱。保持对热点的高度警惕，便是我能做的最好的应对措施了。\n\n## 读书\n\n好在，我们有书。\n\n时常听图书行业的人说做书难。我自己也深有体会，一本书动则一两年，最后收入还不如一天炒股赚得多。\n\n这固然有一些不幸，然而转念一想，这有可能反而是这个时代最大的幸运。正因为这个行业没有太多投机的空间，反而避免了做流量的生意人来把它做坏。\n\n在这个时代读书是一件十分难能可贵的事情，有些生意人已经把人性摸得清清楚楚，就是要你花时间看它。然而书不会，它永远在那里，朴朴素素地躺在那，不需要钻研人性，不需要层层包装，不需要吸引流量。\n\n## 结\n\n一首喜欢的歌，歌词里有七月的风、八月的雨，最后还有遥远的你。\u003Chttps:\u002F\u002Fy.qq.com\u002Fn\u002Fryqq\u002FsongDetail\u002F223238766>\n",{"id":61,"type":7,"slug":62,"title":63,"date":64,"category":45,"tags":65,"body_markdown":72,"permalink":73,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":74},190,"work-with-urlencode","Urlencode踩坑日记","2020-03-31 13:37",[66,67,68,69,70,71],"JavaScript","PHP","Python","Urlencode","编码","签名","\nUrlencode又称百分号编码，是一种很常用的编码方式，作为前端工程师，少不了与它要打交道。不管是GET请求发送参数，还是POST请求发送body，都少不了要使用Urlencode来编码。\n\n而Urlencode的编码规则又特别简单：取出字符的ASCII码，转成16进制，然后前面加上百分号即可。如果是多字节的字符，则取出每一字节，按照同样的规则进行转换即可。例如问号`?`的ASCII码为`63`，转换为16进制为`3F`，所以`%3F`即为`?`进行Urlencode编码的结果。\n\n![urlencode](\u002Fassets\u002Fwork-with-urlencode\u002F1.png)\n\n## 背景\n\n项目需要对外提供HTTP API接口，因此接口鉴权成为一个很重要的内容。为了确保安全，防止中间人篡改数据或进行重放攻击，双方约定的私钥不可以直接出现在请求中，因此采用请求签名的方式来鉴权。\n\n双方约定appKey和appSecret，其中appKey用于识别请求对象，appSecret用于请求签名。具体的方案如下：\n\n1. 客户端按照当前时间生成时间戳`timestamp`和随机数`nonce`\n2. 客户端按照指定的规则将HTTP请求的queryString和POST的body进行编码，得到一个字符串`data`\n3. 将`timestamp`、`nonce`和`data`按规则拼接，然后使用`appSecret`计算签名\n4. 将`appKey`、`timestamp`、`nonce`和签名一起随请求发出\n\n服务端在接到请求后将使用获得的数据和`appSecret`重新计算签名，然后判断与客户端给出的签名值是否一致，如果不一致则鉴权不通过。\n\n这一套鉴权机制可以有效防御一些攻击手段：\n\n- 使用了时间戳，可以避免过期请求被重发\n- 使用了随机数，可以避免请求被短时间重复发送\n- 签名数据包含了完整的时间戳、随机数和请求数据，保证服务端收到的确实是客户端发送的数据，避免被拦截修改\n- 签名的密钥是双方协商好的，避免请求伪造\n\n## 踩坑\n\n在上面的鉴权过程中，一个非常重要的点就是第2点，即将请求的queryString和POST的body进行编码，得到一个字符串。\n\n因为GET和POST请求中，数据都会被Urlencode编码，因此很容易想到，我们也使用Urlencode来进行这个鉴权前的编码过程。\n\n于是坑就这么不期而遇了。\n\n\u003C!-- more -->\n\n由于服务端和客户端都使用JavaScript编写，因此都使用了`encodeURIComponent`来进行Urlencode编码，并且过程相当愉快。\n\n但既然是开放接口，就早晚会面临各种各样的客户端。于是在我自己编写的PHP客户端上，踩坑了：有时候请求一切正常，有时候却鉴权无法通过。经过反复的调试分析，最终发现，PHP获取的待签名的字符串和JS获取的不一样，而问题就出在对`*`的转义上。\n\n在JS中，`encodeURIComponent`并不会对`*`进行转义，而PHP中`rawurlencode`却会将`*`转义为`%2A`。因此，同样的数据在不同的客户端中就产生了不同的字符串，最终导致计算出的签名值不同，鉴权失败。\n\n## 爬坑失败\n\n如果一个接口一直使用都没有问题，突然来了一个新的客户端就鉴权失败，那么必然是这个客户端有问题了。在这种想法的驱使下，对PHP这个世界上最好的语言好感度再次-1，然后硬着头皮去查资料。发现确实有很多人碰到了PHP在使用Urlencode编码的时候星号被编码的问题。还有人给出了解决问题的代码，即在`rawurlencode`之后再将`%2A`替换成`*`。\n\n于是就这么更新上线了，一切又恢复了正常。\n\n然而好景不长，才刚正常几天，又出现了诡异的鉴权失败的问题。而这一次，请求的内容是`[链接](https:\u002F\u002Fwww.qq.com)`。再次对比后，发现括号`(`、`)`在Urlencode后又不一致了：JS没有对括号进行转义，而PHP对它们进行了转义，于是再次出现签名不一致的问题。\n\n直觉告诉我，当一个问题第一次出现时，也许可以绕得过去，但是当它再一次出现的时候，就必须得挖到底了，否则未来一定会有更严重的问题出现。\n\n## 认真审视Urlencode\n\n回到问题本质：兼容性问题。这可是前端工程师最擅长的领域，于是很自然地想到——规范。urlencode的规范是[RFC3986](https:\u002F\u002Ftools.ietf.org\u002Fhtml\u002Frfc3986)，但是看完规范之后，并没能解决这个兼容性的问题，反而解释了兼容性的来源：很多字符是否进行编辑取决于具体的场景和实现……\n\n> 下面这一段有点烧脑，如果不是特别有兴趣，建议跳过。\n\n规范将保留字符分为`gen-delims`和`sub-delims`两部分：\n\n- `gen-delims = \":\" \u002F \"\u002F\" \u002F \"?\" \u002F \"#\" \u002F \"[\" \u002F \"]\" \u002F \"@\"`\n- `sub-delims = \"!\" \u002F \"$\" \u002F \"&\" \u002F \"'\" \u002F \"(\" \u002F \")\" \u002F \"*\" \u002F \"+\" \u002F \",\" \u002F \";\" \u002F \"=\"`\n\n然后定义了`pchar`（`unreserved`指除了保留字符之外的字符）\n\n- `pchar = unreserved \u002F pct-encoded \u002F sub-delims \u002F \":\" \u002F \"@\"`\n\n以URL中出现的`path`和`query`为例，它们的规则分别是\n\n```\npath          = path-abempty    ; begins with \"\u002F\" or is empty\n                    \u002F path-absolute   ; begins with \"\u002F\" but not \"\u002F\u002F\"\n                    \u002F path-noscheme   ; begins with a non-colon segment\n                    \u002F path-rootless   ; begins with a segment\n                    \u002F path-empty      ; zero characters\n\n      path-abempty  = *( \"\u002F\" segment )\n      path-absolute = \"\u002F\" [ segment-nz *( \"\u002F\" segment ) ]\n      path-noscheme = segment-nz-nc *( \"\u002F\" segment )\n      path-rootless = segment-nz *( \"\u002F\" segment )\n      path-empty    = 0\u003Cpchar>\n\n      segment       = *pchar\n      segment-nz    = 1*pchar\n      segment-nz-nc = 1*( unreserved \u002F pct-encoded \u002F sub-delims \u002F \"@\" )\n                    ; non-zero-length segment without any colon \":\"\n```\n\n```\nquery       = *( pchar \u002F \"\u002F\" \u002F \"?\" )\n```\n\n可以看到，它们都有引用`pchar`作为规则（或规则的一部分），除此之外，还有各自允许的字符。这中间的细节要弄明白需要花非常多的时间，我们也可以先不纠结，虽然规范中写了每个部分可以包含哪些字符，却并没有明确写出这些字符是否需要进行`urlencode`（例如`sub-delims`）。\n\n规范中唯一能给我们一些比较明确指引的只有对`unreserved`非保留字符的描述，明确定义了它们是`字母 \u002F 数字 \u002F \"-\" \u002F \".\" \u002F \"_\" \u002F \"~\"`这几个字符。\n\n## 回到现实\n\n既然规范无法给出足够明确的指引，就只能看看现实世界是怎么运作的了。在搜索urlencode规范的时候，发现有很多文档都是这么写：\n\n> 按照rfc3986，除字母、数字、`-`、`.`、`_`、`~`字符外，其它字符均需要进行百分号编码。\n\n也即，大家在实际应用时，会把除非保留字符之外的其他字符全部进行编码。\n\n那编程语言又是如何处理的呢？于是拿JavaScript、PHP、Python分别跑了一下。由于JS中有`encodeURI`\u002F`encodeURIComponent`两个方法，PHP有`urlencode`\u002F`rawurlencode`两个方法，因此一共有5组结果。\n\n|ASCII|char              |python|js encodeURI|js encodeURIComponent|php urlencode|php rawurlencode|\n|-----|------------------|------|------------|---------------------|-------------|----------------|\n|0    |NUT 空字符（Null）|%00   |%00          |%00                   |%00           |%00              |\n|1    |SOH 标题开始      |%01   |%01          |%01                   |%01           |%01              |\n|2    |STX 本文开始      |%02   |%02          |%02                   |%02           |%02              |\n|3    |ETX 本文结束      |%03   |%03          |%03                   |%03           |%03              |\n|4    |EOT 传输结束      |%04   |%04          |%04                   |%04           |%04              |\n|5    |ENQ 请求          |%05   |%05          |%05                   |%05           |%05              |\n|6    |ACK 确认回应      |%06   |%06          |%06                   |%06           |%06              |\n|7    |BEL 响铃          |%07   |%07          |%07                   |%07           |%07              |\n|8    |BS 退格           |%08   |%08          |%08                   |%08           |%08              |\n|9    |HT 水平定位符号TAB|%09   |%09          |%09                   |%09           |%09              |\n|10   |LF 换行键         |%0A   |%0A          |%0A                   |%0A           |%0A              |\n|11   |VT 垂直定位符号   |%0B   |%0B          |%0B                   |%0B           |%0B              |\n|12   |FF 换页键         |%0C   |%0C          |%0C                   |%0C           |%0C              |\n|13   |CR Enter回车键    |%0D   |%0D          |%0D                   |%0D           |%0D              |\n|14   |SO 取消变换       |%0E   |%0E          |%0E                   |%0E           |%0E              |\n|15   |SI 启用变换       |%0F   |%0F          |%0F                   |%0F           |%0F              |\n|16   |DLE 跳出数据通讯  |%10   |%10          |%10                   |%10           |%10              |\n|17   |DC1 设备控制一    |%11   |%11          |%11                   |%11           |%11              |\n|18   |DC2 设备控制二    |%12   |%12          |%12                   |%12           |%12              |\n|19   |DC3 设备控制三    |%13   |%13          |%13                   |%13           |%13              |\n|20   |DC4 设备控制四    |%14   |%14          |%14                   |%14           |%14              |\n|21   |NAK 确认失败回应  |%15   |%15          |%15                   |%15           |%15              |\n|22   |SYN 同步用暂停    |%16   |%16          |%16                   |%16           |%16              |\n|23   |TB 区块传输结束   |%17   |%17          |%17                   |%17           |%17              |\n|24   |CAN 取消          |%18   |%18          |%18                   |%18           |%18              |\n|25   |EM 连接介质中断   |%19   |%19          |%19                   |%19           |%19              |\n|26   |SUB 替换          |%1A   |%1A          |%1A                   |%1A           |%1A              |\n|27   |ESC 退出键        |%1B   |%1B          |%1B                   |%1B           |%1B              |\n|28   |FS 文件分区符     |%1C   |%1C          |%1C                   |%1C           |%1C              |\n|29   |GS 组群分隔符     |%1D   |%1D          |%1D                   |%1D           |%1D              |\n|30   |RS 记录分隔符     |%1E   |%1E          |%1E                   |%1E           |%1E              |\n|31   |US 单元分隔符     |%1F   |%1F          |%1F                   |%1F           |%1F              |\n|32   |空格              |%20   |%20          |%20                   |+             |%20              |\n|33   |!                 |%21   |!            |!                     |%21           |%21              |\n|34   |\"                 |%22   |%22          |%22                   |%22           |%22              |\n|35   |#                 |%23   |#            |%23                   |%23           |%23              |\n|36   |$                 |%24   |$            |%24                   |%24           |%24              |\n|37   |%                 |%25   |%25          |%25                   |%25           |%25              |\n|38   |&                 |%26   |&            |%26                   |%26           |%26              |\n|39   |'                 |%27   |'            |'                     |%27           |%27              |\n|40   |(                 |%28   |(            |(                     |%28           |%28              |\n|41   |)                 |%29   |)            |)                     |%29           |%29              |\n|42   |\\*                 |%2A   |\\*            |*                     |%2A           |%2A              |\n|43   |+                 |%2B   |+            |%2B                   |%2B           |%2B              |\n|44   |,                 |%2C   |,            |%2C                   |%2C           |%2C              |\n|45   |-                 |-     |-            |-                     |-             |-                |\n|46   |.                 |.     |.            |.                     |.             |.                |\n|47   |\u002F                 |\u002F     |\u002F            |%2F                   |%2F           |%2F              |\n|48   |0                 |0     |0            |0                     |0             |0                |\n|49   |1                 |1     |1            |1                     |1             |1                |\n|50   |2                 |2     |2            |2                     |2             |2                |\n|51   |3                 |3     |3            |3                     |3             |3                |\n|52   |4                 |4     |4            |4                     |4             |4                |\n|53   |5                 |5     |5            |5                     |5             |5                |\n|54   |6                 |6     |6            |6                     |6             |6                |\n|55   |7                 |7     |7            |7                     |7             |7                |\n|56   |8                 |8     |8            |8                     |8             |8                |\n|57   |9                 |9     |9            |9                     |9             |9                |\n|58   |:                 |%3A   |:            |%3A                   |%3A           |%3A              |\n|59   |;                 |%3B   |;            |%3B                   |%3B           |%3B              |\n|60   |\u003C                 |%3C   |%3C          |%3C                   |%3C           |%3C              |\n|61   |=                 |%3D   |=            |%3D                   |%3D           |%3D              |\n|62   |>                 |%3E   |%3E          |%3E                   |%3E           |%3E              |\n|63   |?                 |%3F   |?            |%3F                   |%3F           |%3F              |\n|64   |@                 |%40   |@            |%40                   |%40           |%40              |\n|65   |A                 |A     |A            |A                     |A             |A                |\n|66   |B                 |B     |B            |B                     |B             |B                |\n|67   |C                 |C     |C            |C                     |C             |C                |\n|68   |D                 |D     |D            |D                     |D             |D                |\n|69   |E                 |E     |E            |E                     |E             |E                |\n|70   |F                 |F     |F            |F                     |F             |F                |\n|71   |G                 |G     |G            |G                     |G             |G                |\n|72   |H                 |H     |H            |H                     |H             |H                |\n|73   |I                 |I     |I            |I                     |I             |I                |\n|74   |J                 |J     |J            |J                     |J             |J                |\n|75   |K                 |K     |K            |K                     |K             |K                |\n|76   |L                 |L     |L            |L                     |L             |L                |\n|77   |M                 |M     |M            |M                     |M             |M                |\n|78   |N                 |N     |N            |N                     |N             |N                |\n|79   |O                 |O     |O            |O                     |O             |O                |\n|80   |P                 |P     |P            |P                     |P             |P                |\n|81   |Q                 |Q     |Q            |Q                     |Q             |Q                |\n|82   |R                 |R     |R            |R                     |R             |R                |\n|83   |S                 |S     |S            |S                     |S             |S                |\n|84   |T                 |T     |T            |T                     |T             |T                |\n|85   |U                 |U     |U            |U                     |U             |U                |\n|86   |V                 |V     |V            |V                     |V             |V                |\n|87   |W                 |W     |W            |W                     |W             |W                |\n|88   |X                 |X     |X            |X                     |X             |X                |\n|89   |Y                 |Y     |Y            |Y                     |Y             |Y                |\n|90   |Z                 |Z     |Z            |Z                     |Z             |Z                |\n|91   |[                 |%5B   |%5B          |%5B                   |%5B           |%5B              |\n|92   |\\                 |%5C   |%5C          |%5C                   |%5C           |%5C              |\n|93   |]                 |%5D   |%5D          |%5D                   |%5D           |%5D              |\n|94   |^                 |%5E   |%5E          |%5E                   |%5E           |%5E              |\n|95   |_                 |_     |_            |_                     |_             |_                |\n|96   |`                 |%60   |%60          |%60                   |%60           |%60              |\n|97   |a                 |a     |a            |a                     |a             |a                |\n|98   |b                 |b     |b            |b                     |b             |b                |\n|99   |c                 |c     |c            |c                     |c             |c                |\n|100  |d                 |d     |d            |d                     |d             |d                |\n|101  |e                 |e     |e            |e                     |e             |e                |\n|102  |f                 |f     |f            |f                     |f             |f                |\n|103  |g                 |g     |g            |g                     |g             |g                |\n|104  |h                 |h     |h            |h                     |h             |h                |\n|105  |i                 |i     |i            |i                     |i             |i                |\n|106  |j                 |j     |j            |j                     |j             |j                |\n|107  |k                 |k     |k            |k                     |k             |k                |\n|108  |l                 |l     |l            |l                     |l             |l                |\n|109  |m                 |m     |m            |m                     |m             |m                |\n|110  |n                 |n     |n            |n                     |n             |n                |\n|111  |o                 |o     |o            |o                     |o             |o                |\n|112  |p                 |p     |p            |p                     |p             |p                |\n|113  |q                 |q     |q            |q                     |q             |q                |\n|114  |r                 |r     |r            |r                     |r             |r                |\n|115  |s                 |s     |s            |s                     |s             |s                |\n|116  |t                 |t     |t            |t                     |t             |t                |\n|117  |u                 |u     |u            |u                     |u             |u                |\n|118  |v                 |v     |v            |v                     |v             |v                |\n|119  |w                 |w     |w            |w                     |w             |w                |\n|120  |x                 |x     |x            |x                     |x             |x                |\n|121  |y                 |y     |y            |y                     |y             |y                |\n|122  |z                 |z     |z            |z                     |z             |z                |\n|123  |{                 |%7B   |%7B          |%7B                   |%7B           |%7B              |\n|124  |&#124;            |%7C   |%7C          |%7C                   |%7C           |%7C              |\n|125  |}                 |%7D   |%7D          |%7D                   |%7D           |%7D              |\n|126  |~                 |~     |~            |~                     |%7E           |~                |\n|127  |删除              |%7F   |%7F          |%7F                   |%7F           |%7F              |\n\n总结一下：\n\n- 非保留字符的处理上，非常一致（除了`~`）\n- JS的`encodeURI`方法保留了很多符号，这些符号没有进行编码\n- JS的`encodeURIComponent`方法相比PHP和Python，少了`!`、`'`、`（`、`）`、`*`这5个字符的编码\n- PHP的`urlencode`方法将空格编码成了加号（`+`），且对`~`进行不必要的编码\n- Python默认没有对`\u002F`进行编码，需要显式指定`safe=''`才会进行编码（`urllib.parse.quote(str, safe='')`）\n\n如果按照业界“非保留字符一律进行编码”的实践规则来看，那么Python（指定`safe=''`）和PHP（`rawurlencode`）是符合要求的，而JS的`encodeURIComponent`则需要针对额外的5个字符打补丁。\n\n```javascript\nfunction fixedEncodeURIComponent (str) {\n  return encodeURIComponent(str).replace(\u002F[!'()*]\u002Fg, function(c) {\n    return '%' + c.charCodeAt(0).toString(16);\n  });\n}\n```\n\n## 善后\n\n因为结论比较明显，业界和主流语言都采用了比较一致的规则，因此最终这个项目的鉴权部分也进行了修改，除了非保留字符外，其他的字符都需要进行百分号编码。\n\nurlencode由于规范中没有规定得非常细致，将很多细节交给了实现，因此导致各语言的处理并不一致。当然如果能提前预知有这样的问题，有可能在选择方案的时候就不会选择urlencode这么一种“不太确定”的编码规则。\n\n> 如果你认为JSON大法好，恭喜你即将进行另一个坑：不同语言在JSON编码的处理上也不一致，例如中文要不要变成unicode格式、斜杠要不要编码等等。\n\n大概一位前端工程师也没想到有一天需要在后端处理“兼容性问题”。好在处理兼容性问题的原则是一致的：找到差异点、抹平它。\n","\u002Farticle\u002Fwork-with-urlencode.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fwork-with-urlencode\u002F1.png",{"id":76,"type":7,"slug":77,"title":78,"date":79,"category":45,"tags":80,"body_markdown":82,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":30},189,"nodejs-in-frontend-dev","【问答】为什么前端越来越复杂？Node.js有什么作用？","2020-03-18 23:47",[81],"Node.js","\n本文来自知乎问题：[为什么要把前端搞的这么复杂，UI 组件不是很好用吗， 难道就是为了推广 nodejs 和 npm 吗?](https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F378533373\u002Fanswer\u002F1088512439)\n\n这个题目中有无数槽点：\n\n画个界面只要快就好了吗？JS类库能帮你解决渲染慢的问题？类库和组件能帮你解决所有的兼容问题？不需要高并发所以就不需要架构？\n\n这里的每一条都值得展开来反驳，但鉴于题主的主要疑问不在这里，就不跑题。\n\n这个问题的核心，抽取一下就是两个：\n\n1. 为什么前端越来越复杂了\n2. Node.js在前端开发中是干什么的\n\n\u003C!-- more -->\n\n先回答第2个问题。在前端开发中，现在的确基本上都会使用Node.js，但事实上，绝大部分前端工程师只是需要安装一下Node.js而已，一行Node.js代码都不用写。因此Node.js在这里只是一个工具平台，它为前端各种工程化的工具提供一个可执行的环境，仅此而已。打个不是特别恰当的比喻，很多软件在安装的时候都需要编译，编译过程中需要python，这个时候你要做的仅仅是安装一下python，并不需要写python代码。\n\n第1个问题，前端为什么越来越复杂了。\n\n比较欣慰的是，题主好歹说了“代码分布合理，清晰”，要不然实在是聊不下去。JS作为一个20多年的语言，在语言特性上是缺失很多现代软件工程的特性的，典型的就是模块化。你写Java，一个文件就是一个文件，一个包就是一个包，跨文件跨包的代码不会有互相影响吧？要引用只需要import一下就好了吧？那JS呢？\n\nJS的模块化从AMD诞生开始，一直到Node.js的CommonJS规范，中间有十年左右的时间是没有官方方案的，全靠社区想各种办法，从一开始的命名空间，到AMD\u002FCommonJS，全都不是语言层面的官方支持。而这些规范中，只有AMD可以在不编译的情况下运行，CommonJS直到今天都是不可以直接在浏览器中使用的，所以“编译”的第一个用处，在这里——解决JS模块化的问题。\n\n即使AMD可以不编译直接在浏览器中运行，也会面临另一个问题：文件碎片化。大家都是喜欢组织代码的程序员，一旦模块可以不互相影响了，那一定会导致原来在同一个文件中的代码被拆分成更多的子模块。于是有可能你一个页面就要加载几十上百个JS文件（一个文件一个模块），于是你仍然要想办法把它们进行合并打包，于是“编译”的第二个用处出现——合并JS文件，减少HTTP请求。\n\n好了，再回到题主题到的UI组件库和JS类库。这些东西基本上都不是你自己开发的对吗？意味着你要使用别人的代码。那么问题来了，你要如何引入别人的代码？在java中，你可以用marven，在前端代码中呢？你自然可以直接复制代码到项目中，但你要如何管理呢？难道包管理是只有java程序员可以用，并不是呀，写windows的写mac的写android的写ios的都可以用包管理来引入第三方的代码，凭什么写前端的不能这么干呢？于是有了bower，但是bower死了，只剩下了npm。所以npm就成了前端御用的包管理软件。那你下载这么多包了，在代码中要怎么找到这些包？require('jquery')怎么就能找到node_modules\u002Fjquery\u002Fdist\u002Fjquery.min.js呢，又要怎么变成浏览器可访问的呢？于是“编译”的第三个用处出现——可复用软件包的寻址。\n\n“编译”当然还有第四个用处，那就是处理兼容性。这里的兼容性和题主理解的UI组件\u002FJS类库处理的兼容器不是一回事，主要是指语言的兼容性。ES5之后ES6 7 8 9一直在不断出现，JSX、TypeScript等新玩意也在不断出现，但问题是浏览器并不支持或并不完全支持这些玩意，那就不能玩了吗？要等所有浏览器都支持再玩吗？能通过编译一下让大家都愉快地玩起来，何乐而不为？\n\n还有第五个用处吗？有的呢，有了编译，意味着代码的组织有更多可能性，你会看到Vue单文件组件，你会看到Web Components的代码，你会看到在JS中引入CSS的用法，你会看到styled components \u002F scoped css \u002F css modules等等各种神奇的玩法。然而归根结底，他们在做什么，无非是在追求题主所说的“代码分布合理、清晰”。\n\n代码这个世界之所以好玩，就是因为你可以玩出花，而不是被别人限制，对不起，你只能这么玩。\n\n题主觉得用UI组件库\u002FJS类库对前端来说就足够了，但是对前端工程师来说，对不起，这不是我们想玩的玩法，我们就想让前端也可以更好玩一些，不要天天去羡慕做别的语言\u002F平台的工程师。作为一个局外人，你可以不理解，你也可以选择你认为舒服的方式，但是请不要随意质疑别人觉得舒服的方式。\n\n写个HTML，放个jQuery，放个bootstrap，$.ajax()写起来，没有人会鄙视你的。但是我vue-cli \u002F npm install \u002F npm run dev，也不要鄙视我好吗？大家各自开心。\n",{"id":84,"type":7,"slug":85,"title":86,"date":87,"category":11,"tags":88,"body_markdown":91,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":30},128,"why-weixin-can-only-login-via-cellphone","为什么微信的登录一定需要手机","2020-02-24 14:17",[89,90],"微信","手机","\n来自知乎的问题，原地址\u003Chttps:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F270040312\u002Fanswer\u002F1034679430>。\n\n先说结论：微信是一个移动端通讯工具，再通俗一点，它就是一个手机软件。至于电脑版什么的，就是随手一做，如果威胁到手机版的地位，关掉都不会有人心疼的。\n\n知乎确实迎来新一辈的朋友了，似乎已经没有多少人知道微信诞生的背景，也没有人在乎背后的逻辑了。微信在知乎上的口碑也从一开始的神作，变成了最被唾弃的软件。\n\n先问几个问题：\n\n1. 微信10亿左右的月活用户（最近没翻财报，但大约是这个数字，量级不会错），你们猜有没有1亿人使用PC微信？\n2. 为什么你一边骂着微信，一边却离不开微信？\n\n\u003C!-- more -->\n\n## 移动互联网大潮和“船票”论\n\n大约在2013年，所有的互联网大厂都意识到，移动互联网要来了。伴随移动互联网的而来的，则是各个大厂无尽的恐慌感，因为害怕一招不慎就被拍死了。彼时大家都在寻找各自在移动互联网的杀手级APP，则腾讯的马化腾则将之称之为移动互联网的船票。\n\n回头来看，这个论断是非常有前瞻性的。2013年之前的互联网产品，到现在还存在的已经不多了，即使存在，也已经是一个全新的移动APP的形态了。360电脑管家、QQ影音、暴风影音、千千静听、光影魔术手、PPS……当年的装机必备，希望现在还有记得它们的名字……\n\nQQ诞生于1998年，就地位而言，是当之无愧的PC互联网王者。但是在移动互联网大潮来临之前，手机QQ和PC QQ竟然都不是在同一个事业群（彼时还叫业务线），可想而知手机QQ的地位和状态是如何。光是从版本这一个侧面就能看出来：QQ手机版、手机QQ、手机精简版、轻聊版……（这些版本名称不一定完全准确，但当时确实是有三到四个不同的版本。）所以当时尽管手机QQ的下载量非常高，但是在产品上它仍然是一个非常彻底的PC first的产品。\n\n于是，大家都在寻找针对移动互联网形态的王牌产品。对腾讯而言，很幸运的就是，微信帮腾讯拿到了移动互联网的第一张船票。\n\n## Mobile Only\n\n(用英文作标题不是装逼，而是业内习惯这样称呼，mobile\u002Fpc only\u002Ffirst。)\n\n当时的QQ有一个现实的情况：你要QQ找人，需要先看他在不在线。当他不在线，你的选择是：1. 在吗？（你隐身了吗？）2. 发短信\u002F打电话。\n\n再对比一下今天的微信，你要发消息，是否会先问一句“在吗”？（如果是的话，大概是知乎上最受人讨厌的人之一吧。）也就是说，你已经默认了，我发微信，他一定会收得到，最起码不会像QQ一样，可能大半个月之后才看到。所以你为什么一边骂着微信一边离不开微信，因为大家都知道微信消息的触达率高，因为大家都用微信，你不得不用微信。\n\n这个差异并不是天然形成的，而是微信长期以来的产品策略造成的结果。\n\n微信从一开始就希望自己是纯粹的移动端软件，因为张小龙坚信，手机是人的延伸，而电脑不是。现在回头看，这个判断是非常正确的。你想想有什么东西能让你在醒着的时候一直不离手，唯有手机。这样来看，一款通讯软件，能让人只能通过手机找到我，和能让人通过电脑或手机找到我，消息触达率是完全不同的。如果微信是一个PC和mobile并重的软件，你可以手机也可以电脑登录，那么你会面临和QQ完全一样的情况：我需要思考我发的消息能不能被看到。如果是这种情况，微信完全不可能有今天全面替代短信甚至电话的局面。\n\n所以web版微信一开始叫作“连接键盘”，它只是帮你打字的一个工具，不是微信。所以电脑版微信一直姗姗来迟而且功能不全，正如开头说的，如果它被关掉，都不会有人心疼的。\n\n所以微信永远不会提供电脑端独立登录。因为电脑端是依托移动端而存在的，当你的手机不在身边时，对不起，你不能使用微信。这和你没有手机时不能发短信、打电话是一样的道理。（iPhone用户别扛，背后的思路是一样的。）微信希望，如果你不能通过微信找到对方，理由只能是手机没带\u002F手机没电，而不是“我手机没登，我只在上班的时候电脑上用微信”。\n\n## 微信的好用与不好用\n\n微信在知乎的口碑全面下滑，最被诟病的就是不好用：传文件不行，群管理不行，聊天列表管理不行，消息同步不行……\n\n所有的这些问题真实存在，并且当你碰到的时候，真的很痛。但是张小龙说，每天有1亿人教我做微信。言下之意，你们懂个屁。这到底是怎么一回事，我试着做一些猜测。\n\n有一些功能的设计，是故意为之。举两个稍微证据充足一些的例子吧：\n\n### 群\n\n在微信群之前，群聊的主战场是QQ群，但是QQ群在以前是非常严格的限制的，每个人只能建3个群（随等级提升而提升），群成员有限制，从100到1000人不等。每个群都有群号，建一个就要占用一个建群名额。QQ群有群主，有管理员，有正式的公告，有群文件等等……简单来说，QQ群的建立是需要非常正式的考虑的，三思而后行。\n\n但是微信群一推出就打破了这一限制，随便建群，可以从通讯录建，也可以当面建群（这个功能是后加的）。群也没有什么管理员，大家都可以改群名。\n\n这么一对比就发现，QQ群适合组织，而微信群，适合平等而非正式的交流。\n\n### 消息同步\n\n微信的确有了电脑端，也有了iPad端，理想情况下，你的确可以在三个左右的终端同步登录同一个微信，但是只要某个端不在登录状态，消息是不会同步的。（事实上一开始PC端是完全不同步的，现在可以勾选同步最近消息，勉强同步一部分。）\n\n微信在非正式场合有给出过一些解释：同步会带来认知上的麻烦，因此选择不同步。而这个认知上的麻烦是什么呢？就是你得时刻记着我的哪个设备上同步有登录，这些消息都会同步过去。而如果你忘记这一点，就有可能带来隐私泄漏，造成更多的麻烦。（参考男\u002F女朋友互相登录QQ查看聊天记录。）\n\n还有更多的功能，微信在设计的时候都有过一些很慎重的考虑，但是因为证据没有那么明确，说多了有些洗地嫌疑，就不再列举。\n\n但是核心的意思是，微信的功能设计有其场景，我个人觉得非常明显的两个点：私人会话、简化认知。\n\n为什么老人觉得微信好用，其实就是老人的使用正好符合这两个点：微信里基本都是家人和朋友，都是私人会话；对手机不是很精通，甚至对界面元素的识别都有困难，因此认知越简单越好。\n\n而知乎的朋友觉得微信不好用：因为都是工作群，都是工作场景，需要更精细的消息同步\u002F消息提醒\u002F文件传输；因为都是高知人群，界面上差一个像素都能看出区别，因此觉得微信有些过于简陋。\n\n这些不是微信的错，也不是你的错，而是你的需求和微信的定位产生了冲突。\n\n## 10亿人的微信\n\n微信是一个面向十多亿用户的软件，而关注微信使用体验的知乎用户有多少呢？这十多亿用户和知乎用户的画像（人在美国、刚下飞机、年薪百万）又差多少呢？有多少人会想到“大字体”是微信的一个刚需？有多少人会想到微信支付输完密码后询问是否开启指纹支付的弹窗对大量的人群是一个困扰（因为没有人告诉他应该选开通还是不开通）？\n\n回到问题，十多亿人里，又有多少人需要电脑端微信呢？\n\n所以，我的看法：\n\n1. 电脑端微信登录时需要手机扫码，没毛病，因为它是一个纯手机软件。（但是除了微信之外，其他的这么操作我不能理解，典型的比如企业微信。）\n2. 微信\u002F电脑端微信的种种难用之处，同样感同身受。但是给10亿人做软件和给知乎高知群体做软件，做法本来就不一样了，同为互联网从业者，表示理解。\n3. 如果有可能，尽量选择场景合适的工具做合适的事情，放过自己，也放过微信。\n",{"id":93,"type":7,"slug":94,"title":95,"date":96,"category":11,"tags":97,"body_markdown":100,"permalink":101,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":102},127,"remove-sensitive-data-from-git-with-rebase","使用Rebase操作抹去Git仓库中的敏感信息","2019-11-29 20:38",[98,99],"Git","Rebase","\n使用Git的时候，有时候会碰到需要从Git仓库中永久“抹除”某些敏感信息的情况。例如不小心提交了密码之类的信息到仓库，此时只抹掉这些信息重新提交是没有用的，因为其他人仍然可以通过Git历史看到这些敏感信息。因此需要一种方法将这些信息彻底从仓库中抹去。\n\n如果去网上搜索的话，能很容易找到使用`branch-filter`来处理的方法，例如\n\n```sh\ngit filter-branch --tree-filter \"find . -name '*.*' -exec sed -i '' -e 's\u002FOLDSTRING\u002FNEWSTRING\u002Fg' {} \\;\" -f\n```\n\n写法有很多种，但是思路都差不多，就是遍历一遍所有的提交，对这些提交执行指定的命令（例如用`sed`替换指定的内容，或者移除相关文件），然后重新生成新的提交和分支。\n\n不过这种思路对于我来说却不太受用，原因有几个：\n\n1. 命令行掌握不太好，看到这种命令都不太认识，完全不敢直接放在项目中去跑\n2. 直接进行字符串级别的替换，在某些情况下不够用，例如想通过更复杂的编辑手段（新增文本、修改文本、删除文本同时操作）抹除敏感信息\n3. 直接对整个仓库\u002F整个文件进行字符串级别的替换还是有些不放心，毕竟要修改的部分是明确的，却无法明确地指定这个命令只修改这一部分信息\n\n那怎么办呢？其实在这种场景下，也可以尝试使用`git rebase`来解决问题。\n\n## rebase是干什么的\n\n`rebase`顾名思义，就是重新确定一个提交（一个分支）的“基”，这个“基”就是指它的祖先元素。具体的做法是，首先将提交退回到“基”所在的点，然后将之前做过的提交在这个“基”的基础上重复做一遍。相当于修改了当前分支衍生出来的基础，因此中文也被译为“变基”。\n\n\u003C!-- more -->\n\n还是举个例子：\n\n新建一个仓库，然后做两次提交`A1`、`A2`：\n\n```sh\n# 初始化\nmkdir test\ncd test\ngit init\n\n# 两次提交\necho \"A1\">>1.txt\ngit add .\ngit commit -m A1\n\necho \"A2\">>2.txt\ngit add .\ngit commit -m A2\n```\n\n接下来分成两个分支，分别进行提交`B1`、`B2`和`C1`、`C2`：\n\n```sh\n# 创建新分支\ngit checkout -b new\n\n# 新分支提交B1 B2\necho \"B1\">>1.txt\ngit add .\ngit commit -m B1\n\necho \"B2\">>2.txt\ngit add .\ngit commit -m B2\n\n# 切换主分支\ngit checkout master\n\n# 主分支提交C1 C2\necho \"C1\">>1.txt\ngit add .\ngit commit -m C1\n\necho \"C2\">>2.txt\ngit add .\ngit commit -m C2\n```\n\n于是得到一张这样的分支图：\n\n![分支图](\u002Fassets\u002Fremove-sensitive-data-from-git-with-rebase\u002F01.png)\n\n此时`new`和`master`两个分支分别指向`B2`和`C2`两次提交，而他们的共同祖先（即前文中说的“基”，这个说法不严谨，仅为理解），则是`A2`这次提交。此时我们就可以用`rebase`来改变其中某一个分支的走向。例如，让`C1`在`B2`的基础上修改，即让`master`分支的提交顺序变成`A1-A2-B1-B2-C1-C2`：\n\n```sh\n# 对哪个分支rebase就切换到哪个分支\ngit checkout master\n\ngit rebase new\n```\n\n而此时就出现了冲突，因为`C1`和`B1`两次提交都对`1.txt`做了修改。\n\n```sh\nA1\n\u003C\u003C\u003C\u003C\u003C\u003C\u003C HEAD\nB1\n=======\nC1\n>>>>>>> C1\n```\n\n我们选择手工解决冲突，将`B1`放前面，`C1`放后面，解决完之后继续`rebase`\n\n```sh\n# 用add标记解决完冲突\ngit add 1.txt\ngit rebase --continue\n```\n\n此时分支图会变成这样，表明C1这次提交已经变基成功。但同时也能看到C2在变基的时候也产生了冲突。\n\n![分支图](\u002Fassets\u002Fremove-sensitive-data-from-git-with-rebase\u002F02.png)\n\n这里和上面一样处理即可。完成后就能看到变基后的分支图。\n\n![分支图](\u002Fassets\u002Fremove-sensitive-data-from-git-with-rebase\u002F03.png)\n\n需要说明的是，尽管提交的描述信息（commit message）没变，但是`C1`、`C2`这两次提交实际上是新产生的，因此它们的commit id和之前的提交是完全不同的。\n\n通过这个例子，能清楚地看到`rebase`命令的作用，即改变提交（分支）的基础。一般来说，在多人协作过程中，适合将同一分支互相拉取变更的操作使用`rebase`来完成，这样可以保持同一分支的提交历史是线性的，方便回溯。\n\n## 使用rebase改变Git历史\n\n在上面`rebase`的例子中，还有一个点值得注意，以`C1`这次提交为例，`rebase`前后两种情况下，虽然都是在`1.txt`结尾添加`C1`这行文字，但是基础和结果都是不同的。在`rebase`之前，`1.txt`的内容是由`A1`变成`A1\\nC1`，而在`rebase`之后则是由`A1\\nB1`变为`A1\\nB1\\nC1`。\n\n可见在`rebase`的时候，不止是提交的父节点（“基”）会变，文件内容也有所变化。而我们之前在`rebase`时面临的冲突，也正是因为这个变化所带来的。但同时，正因为有这样一个变化，使得我们有机会通过`rebase`的方式来永久改写Git仓库中某一个文件的历史。\n\n仍然看上面的例子，现在只看`rebase`之后的情况。在`C1`这次提交中，我们为文件`1.txt`在结尾处添加了内容`C1`。假设这个`C1`是一个很敏感的信息（例如密码），我们要如何将它从仓库历史中抹去呢？\n\n![分支图](\u002Fassets\u002Fremove-sensitive-data-from-git-with-rebase\u002F04.png)\n\n首先我们在C1提交之前找到一个点，例如B2，然后基于它新建一个分支。（例如`new`这个分支。）接下来在这个分支上，对文件`1.txt`进行修改，例如我们增加一行`C2`。即`1.txt`内容变为\n\n```\nA1\nB1\nC2\n```\n\n并进行一次提交。\n\n![分支图](\u002Fassets\u002Fremove-sensitive-data-from-git-with-rebase\u002F05.png)\n\n接下来，我们对C2这次提交（即`master`分支）进行`rebase`操作：\n\n```sh\n> git checkout master\n> git rebase new\n```\n\n此时git会告诉我们，产生了冲突。\n\n```\nFirst, rewinding head to replay your work on top of it...\nApplying: C1\nUsing index info to reconstruct a base tree...\nM\t1.txt\nFalling back to patching base and 3-way merge...\nAuto-merging 1.txt\nCONFLICT (content): Merge conflict in 1.txt\nerror: Failed to merge in the changes.\nPatch failed at 0001 C1\nhint: Use 'git am --show-current-patch' to see the failed patch\n\nResolve all conflicts manually, mark them as resolved with\n\"git add\u002Frm \u003Cconflicted_files>\", then run \"git rebase --continue\".\nYou can instead skip this commit: run \"git rebase --skip\".\nTo abort and get back to the state before \"git rebase\", run \"git rebase --abort\".\n```\n\n冲突内容正是原来的`C1`和我们刚在新分支上添加的`C2`：\n\n```\nA1\nB1\n\u003C\u003C\u003C\u003C\u003C\u003C\u003C HEAD\nC2\n=======\nC1\n>>>>>>> C1\n```\n\n前文假设，`C1`是我们要删除的敏感信息，因此此时手工解决冲突，将`C1`删除，留下`C2`。然后`git rebase --continue`即可。\n\n可以看到我们的提交记录变成了这样：\n\n![分支图](\u002Fassets\u002Fremove-sensitive-data-from-git-with-rebase\u002F06.png)\n\n敏感信息被彻底删除了。\n\n## 小结\n\n上面只是展示了一种简单的情况，但已经足够说明使用`rebase`来删除代码库中敏感信息的核心思路和关键步骤了。\n\n有一些值得注意的细节：\n\n1. 由于C1提交和重写C1的提交都只修改了一行代码，因此在`rebase`过程中，把这一行的冲突解决完，并且`git rebase --continue`时，会提示没有变更（因为唯一的变更在冲突解决过程中被编辑好了），此时需要使用`git rebase --skip`跳过这次提交。\n2. 上面我们是使用`rebase`操作时，编辑冲突的时机来编辑代码文件，从而将`C1`这个敏感信息删除的。如果无法保证一定产生冲突，则可以使用`git rebase -i`（交互式变基）来手工指定需要对哪些提交进行编辑，从而在不一定有冲突时，也有机会编辑代码文件，来将敏感信息删除。关于交互式变基，可参考网络上相关文档。\n3. 如果敏感信息在第一次提交就被带入版本库了，则上面说的“在C1提交之前找到一个点”无法完成。此时可以用`git checkout --orphan branch-name`来创建一个完全空白且没有父节点的分支，并且将当前分支的提交基于这个新的空分支来进行`rebase`，从而获得编辑代码删除敏感信息的机会。\n\n最后，一个提醒：不论用什么方法来修改版本库历史，都是在重写历史，虽然看起来提交的commit message是一样的，但是却是完全全新的提交和分支发展路径。当推送到代码库时，需要使用`git push --force`来强制推送，其他人则需要使用`git pull --rebase`来重写本地分支。\n","\u002Farticle\u002Fremove-sensitive-data-from-git-with-rebase.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fremove-sensitive-data-from-git-with-rebase\u002F01.png",{"id":104,"type":7,"slug":105,"title":106,"date":107,"category":45,"tags":108,"body_markdown":111,"permalink":112,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":30},186,"sequelize-tricks","Sequelize的一些小技巧","2019-11-10 21:36",[81,109,110],"Sequelize","ORM","\n[Sequelize.js](https:\u002F\u002Fsequelize.org\u002F)是一个用于Node.js的数据库ORM库，支持Postgres、MySQL\u002FMariaDB、SQLite、SQL Server等引擎。\n\n本文记录一些团队在使用Sequelize过程中积累的经验教训。\n\n## 介绍\n\nORM即Object Relational Mapping，中文叫“对象关系映射”。简单地说就是可以将数据库的各种对象（表、字段）及关系映射为程序语言的对象和关系，从而使开发者不需要直接操作数据库，转而操作对象即可。\n\n例如，将表`user`映射为模型`User`后，从数据库中查询`id`为`1`的用户就可以直接调用`findOne()`方法：\n\n```javascript\nconst user = await User.findOne({\n    where: {\n        id: 1\n    }\n});\n```\n\n这样做会带来几个明显的好处：\n\n1. 降低开发难度：ORM都有完善的文档，几乎所有的操作只需要按文档调用指定方法即可，不需要自己拼接SQL\n2. 提升安全性：ORM会处理好SQL注入问题，不需要开发者关注\n3. 降低封装复杂度：公共逻辑可以基于ORM封装，非常方便\n\n> 下文不区分“模型”和“Model”，均指Sequelize中与数据表对应的数据模型。\n\n\u003C!-- more -->\n\n## 命名\n\n团队合作中统一大家的命名规则是很重要的事情，因此一般稍微规范一些的团队都会有比较详尽的命名规范。但是不同地方的命名规则却不一定完全一致，例如：\n\n- 数据库规范：表名及字段名使用小写字母，单词间以下划线分隔\n- JS编码规范：变量命名使用驼峰式命名（即首字母小写，后续单词的首字母大写）\n\n这种情况可以通过Sequelize模型定义来解决，直接指定表名和字段名即可：\n\n```javascript\nsequelize.define('targetInfo', {\n    targetId: {\n        type: DataTypes.INTEGER(11),\n        allowNull: true,\n        field: 'target_id',\n    },\n}, {\n    tableName: 'target_info',\n});\n```\n\n上例中的`target_id`字段，在使用Sequelize的Model时就可以使用`targetId`属性来访问`target_id`字段，完全遵守JS编码规则。\n\n## 软删除 & 自动管理时间戳\n\n很多时候，因为保留痕迹、灾难恢复等各种原因，在设计技术方案时，我们都会使用一个字段来标记数据是否被删除。当业务需要删除数据时，只需要改变这个标记即可，而不是真的删除数据库记录。\n\n但是选择这种方案的同时，却会为业务带来一些复杂性，即每一个查询都需要考虑删除标记的状态。作为一个合格的ORM库，Sequelize也很贴心地提供了软删除的支持。在开启这个特性后，开发者不需要关注数据记录是否已被删除，只需要正常地使用查询、删除等操作即可，Sequelize会在执行对应的SQL查询前自动加上软删除的条件。\n\n具体的操作非常简单：\n\n1. 数据库和模型文件添加`deleted_at`字段\n2. 在模型定义的选项中加上`paranoid: true`选项\n\n此后，被删除的数据记录的`deleted_at`会记录被删除的时间，而没被删除的记录`deleted_at`为`NULL`。\n\n除了软删除外，记录的建立和更新时间也可以交给Sequelize来管理，操作同样简单：\n\n1. 数据库和模型文件添加`created_at`和`updated_at`字段\n2. 在模型定义的选项中加上`timestamps: true`选项\n\n这样定义之后，数据建立时会自动记录创建时间到`created_at`字段中，而当数据发生修改时，`updated_at`会自动记录更新时间。\n\n## 关联\n\n多表的查询在数据操作中也是一个比较常见的操作。Sequelize也可以让我们指定模型之间的关联（且有完善的1:1、1:n、m:n关联）。一旦指定完成，则可以在查询数据时直接带出关联数据。\n\n例如每一个会议（`Meeting`）有多个参会者（`Participator`），在查询会议时可以直接拿出参会者信息：\n\n```javascript\n\u002F\u002F 指定1:n关联\nMeeting.hasMany(Participator);\n\n\u002F\u002F 查询会议\nconst meeting = Meeting.findOne({\n    where: {\n        id: 1,\n    },\n    include: [Participator],\n});\n```\n\n接下来访问`meeting.participators`即可获得会议参会者列表。\n\n如果希望关联数据进行排序，则可以直接在查询中指定`order`排序规则。但是这个排序和直观想法不太一样，从SQL的角度来讲，关联数据的查询无论是用多表查询还是`join`，都没有办法单独对关联表单独排序，因此不管怎么排序会影响主表的排序。所以如果要对关联数据排序，最好将主表的排序依据写在前面：\n\n```javascript\n\u002F\u002F 查询会议\nconst meeting = Meeting.findAll({\n    where: {\n        id: 1,\n        \u002F\u002F 排序\n        order: [\n            \u002F\u002F 先对主表排序\n            ['id', 'asc'],\n            \u002F\u002F 再对关联表排序\n            [Participator, 'id', 'asc'],\n        ],\n    },\n    include: [Participator],\n});\n```\n\n## Model Diff\n\n之前团队碰到了一个需求：在同步数据的同时记录数据发生的变化情况。一开始的想法是在同步前先读取一次数据，等数据同步完之后，再读取一次数据，然后对两次数据进行对比和记录。但是在深入了解Sequelize之后，发现这个事情有更简单的解法。\n\nSequelize的Model在结构上是有记录两个值的，内部分别用`_previousDataValues`和`dataValues`记录。其中`_previousDataValues`表示从数据库中读出来的原始记录，而`dataValues`则记录Model经过一些操作之后的新值。例如`.set()`方法会改变`dataValues`的值，但不会改变`_previousDataValues`的值。但是如果调用`.save()`方法，则新值会写入数据库，`_previousDataValues`也会改变。\n\n因此，我们可以将数据保存的过程分为设置新值和保存到数据库两步，并且从中获取数据的变更：\n\n1. 通过`findOne()\u002FfindAll()`读取原值获取Model\n2. 通过`.set()`方法设置Model的新值\n3. 读取原值和新值的变化\n4. 通过`.save()`方法保存数据\n\n而具体的第3步，获取变化，Sequelize也有提供一些帮助：`.changed()`方法可以返回所有发生变更的字段名，`.previous()`方法在不传参数的情况下，会返回仅包含变化字段的原数据，可以直接作为记录变化的原值。而新值则只要拿到发生变更的字段名列表，然后新值即可。\n\n```javascript\n\u002F\u002F 获取一个模型发生变化的值\nconst getChanges = function(model){\n    const changedFields = model.changed();\n    if(!changedFields){\n        return false;\n    }\n    const oldValue = model.previous();\n    const newValue = {};\n    changedFields.forEach((field) => {\n        newValue[field] = model[field];\n    });\n    return {\n        oldValue,\n        newValue,\n    };\n};\n```\n\n## 事务\n\n事务是数据库的一个很重要的特性，它的最重要的一个应用场景即是将一系列的数据库操作原子化——要么全部成功，要么全部失败。上一节提到的场景，在提交数据变更本身的同时记录数据变更情况即是一种典型的适合使用事务的场景。\n\nSequelize也提供了事务的支持，在使用时先初始化一个事务对象，然后在进行数据操作时传入事务对象即可，没有很特别的地方，仅仅是作为一个记录，在适当的场景下记得使用它即可。\n\n下面的例子删除了一堆数据，并且新增了一堆与之对应的日志：\n\n```javascript\nawait sequelize.transaction((t) => {\n    return Promise.all([\n        \u002F\u002F 创建删除日志\n        Event.bulkCreate(models.map((item)=>{\n            return {\n                type: 'delete',\n                targetId: item.id,\n                oldValue: JSON.stringify(item),\n                newValue: null,\n            };\n        }), {transaction: t}),\n        \u002F\u002F 删除数据\n        Model.destroy({\n            where: {\n                id: {\n                    [Op.in]: deleteIdList\n                }\n            }\n        }, {transaction: t})\n    ]);\n});\n```\n\n## Model扩展\n\nSequelize的Model有很丰富的内部结构，但在进行JSON输出（`JSON.stringify`）的时候，却只会输出模型的数据，不需要进行其他的额外处理。在大部分情况下这种处理是合适的，但是在某些情况下，我们仍然需要对数据进行一些处理，例如字段扩展或者字段裁剪。\n\n在这种情况下，我们可以对Model进行扩展，添加一些最终输出前进行整理的代码：\n\n```javascript\nModel.protoype.output = function(){\n    \u002F\u002F 只输出指定的字段，且根据需要格式化\n    return {\n        foo: this.foo,\n        bar: this.bar + '@toobug.net'\n    }\n}\n```\n\n> 在Sequelize 4中，我们使用的Model的原型并不是Model本身，而是Model.Instance，所以需要在`Model.Instance.prototype`上定义方法才有效。\n\n在实际使用的过程中，我们还可以将这个过程整理得更加工程化：\n\n1. 定义一个`extend`目录，专门定义对每个Model的扩展，且命名与Model定义一一对应\n2. 在初始化的时候读取所有的Model（`sequelize.models`），然后一一读取对应的`extend`，并扩展到原型上\n\n这样在以后需要扩展模型的时候，只要在`extend`目录下定义对模型的扩展方法即可，不用再手工操作Model。\n\n## Model生成\n\nSequelize Model的使用非常方便，但是手写模型的过程并不太愉快。一个字段就要定义类型、默认值、是否为NULL、字段名等，如果一个表有30个字段，则光写模型定义就需要写100多行。\n\n事实上，如果已经建好了数据库表，则这些模型定义的内容基本上都可以从数据库中读取出来。`sequelize-auto`正是做这件事情的库，它会连接数据库，并从数据库中读出所有表，生成对应的模型文件。\n\n它的使用很简单，首先作为工具安装`sequelize-auto`模块（`npm i sequelize-auto -g`或者不加`-g`安装到项目中），然后在命令行中调用它即可，例如：\n\n```sh\nsequelize-auto -h 127.0.0.1 -d database -u username -x password -p 3306 -C -a .modelConfig.json -o server\u002Fmodels\u002Fmodels\n```\n\n参数\n\n- `-h`\u002F`-p`\u002F`-d`\u002F`-u`\u002F`-x`，数据库的IP、端口、数据库、用户名、密码\n- `-C`，使用驼峰式命名规则\n- `-a`，模型的配置项\n\n其中模型的配置项是一个JSON文件，这些配置项会出现在生成的Model文件中，作为模型的配置，例如：\n\n```json\n{\n    \"paranoid\": true,\n    \"timestamps\": true\n}\n```\n\n生成模型后，使用`sequelize.import(modelFilePath)`即可引入模型，然后愉快地使用。\n\n> sequelize-auto略有些年久失修，比如生成的模型中还有jshint的注释，且缩进是2空格。如果与你的项目规范不符，可以在生成后再加一个`eslint --fix`或者`prettier`之类的工具进行格式化即可。\n","\u002Farticle\u002Fsequelize-tricks.html",{"id":114,"type":7,"slug":115,"title":116,"date":117,"category":45,"tags":118,"body_markdown":120,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":30},187,"why-babel-exists","【问答】为什么会允许babel这种解析工具的存在？","2019-03-28 18:21",[66,119],"babel","\n本文来自知乎问题[为什么会允许babel这种解析工具的存在？](https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F315934143\u002Fanswer\u002F634975427)\n\n希望提问者真的没有在调侃……因为在我看来，这有点像“何不食肉糜”的提问了。\n\nES6又名ES2015，也就是在2015年定稿的，在定稿之前其实大家已经讨论了很久了。但是光讨论有什么用呢？没有任何一个环境是支持ES6运行的。所以就讨论讨论再讨论，然后大家一拍桌子，好，定稿？\n\n事实上在ES2015之前，ES5可能就是这么定下来的，ES4可能也是这么废弃的。\n\n这时候，就有个神奇的东西，叫6to5出现了，它的第一次提交出现在2012年9月。Initial import · babel\u002Fbabel@aedcd4e 它的作用就是把ES6的代码编译成ES5的代码，它的神奇之处就在于，虽然一个能支持ES6的环境都没有，但是我们仍然可以使用ES6来编写代码。这是一种前所未有的模式，甚至在其它语言中都没有出现过这种模式。（希望不是孤陋寡闻，至少py3 -> py2是没有见到类似工具的。）\n\n于是，我们可以在规范还没有定稿的时候就先用用看，用着觉得不爽了再回去修改规范。这样是不是比拍桌子要科学得多？事实上现在的ES规范制定过程就是这么干的，定了stage 0到stage 4等几个级别，而且规定了需要在多少个环境中先验证，验证完之后才可以定稿发布。基本上可以毫不客气地说，这个东东就是由6to5开创的新局面。\n\n\u003C!-- more -->\n\n当然，在规范发展过程中，浏览器、Node.js等环境也在不断实现ES6规范的特性，6to5的作者自然也清楚，等环境都支持ES6了，可能就没我什么事了。（像题主这样，认为大家都支持就好，不需要编译了。）所以这个作者又开了个脑洞，首先将6to5做了个改名，以便让它的适应范围更广，要不然以后ES 7\u002F8\u002F9出来的时候玩得名不正言不顺的嘛。然后，将所有的转译过程全部插件化，你可以自由选择某个ES6\u002F7\u002F8特性是否要做转换。这样作为一个转译插件，它就可以更好、更长久地生存下去，也能持续为JS社区带来价值。不得不说，作者真是个天才。（据说写6to5的时候还是个高中生……）\n\n这个神奇的6to5后来改的名字，就叫babel。可以说，没有babel就没有JS社区今天在语言规范上的高度繁荣。也就没有提主提问的前提存在了。\n\n（当然，也要承认，在6to5同期也有其它的转译插件，例如来自Google的traceur，但babel一开始在转译后的代码可读性上就有优势，后期改名和插件化又是神来之笔，因此很快胜出。）\n\n再来说今天仍然使用babel的重要性。\n\n首先一个很重要的点，用户用什么鬼玩意你是无法决定的。虽然现在主流浏览器对ES6的支持都能到99%以上，但是如果用户用的safari 8呢？用的IE9呢？用的Android 4.0呢？这一点很多答主都已经解释得很清楚了，不再多说。\n\n第二个点其实在看完上面一大段后也容易理解了，babel != ES6，并不是只有ES6才可以用babel呀。事实上你用的ESM模块化规范import\u002Fexport，并不属于ES6规范，你用的async\u002Fawait也不属于ES6规范，你用的666的解构，也有一部分不属于ES6规范……但是非常幸运的是，babel并不苛求你知道它属不属于ES6，而是默默都帮你处理了。因此，后ES6时代，babel仍然可能长期存在。（这也再次印证作者眼光很毒！）\n\n第三个点，babel除了做ES规范的转译以外，也可以做做别的转译，比如JSX\u002FTS。但这个在我看来不属于babel的主业，而是人们发现babel的转译是万能的，既然在工具链中已经无法缺少了，顺手让它也帮帮忙也无可厚非。\n\n以上。均是个人理解，如有错漏，欢迎评论指正。\n",{"id":122,"type":7,"slug":123,"title":124,"date":125,"category":36,"tags":126,"body_markdown":127,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"cover":30},87,"2018-summary","2018小结","2019-02-03 12:00",[38],"\n时间像一位长者，慢慢把生活的真相一层一层剥开给你看。\n\n2018年像一溜烟，还没来昨及看清，就已然消失不见，留下我一个人，站在这里不断回想，它究竟是个什么样子。\n\n“人在沮丧的时候特别喜欢思考人生。”这是我在4月份说的，大概也是在沮丧的情绪中不断反思而得出的结论。这种沮丧大部分要来自泥沙俱下的股市。“牛市的时候人人都觉得自己是股神”，这些话，也只有到山穷水尽的时候才能真正明白。好在，除此之外，倒也并没有什么真的大风大浪，一边跟着时间走，一边安慰自己，竟也就这样过来了。\n\n\u003C!-- more -->\n\n时常想起刚进公司时，总是听说一天巨亏的故事，直到最近仍然在听闻各种财务大起大落的版本。以前听到，是悟不出道理的，现在却能更踏实地感受这些故事。在熊市中学会与自己和解，这可能是一堂必修课。\n\n股市教的第二课，是如何看待钱。\n\n想起以前租户的时候，有一次给家里打电话，听到我妈和婆婆在山上挖草药卖。从天黑到天黑，满满一天，能卖8块钱。而彼时我正花完几百块吃了一顿大餐。挂完电话眼泪就忍不住了。\n\n我一直是一个精打细算的人，对钱的花法和去向还是有规划的，但不会过分纠结于一分一厘。从毕业开始一直有记账的习惯，每个月钱花在哪了还是心里有数。但可能一方面受到我妈的影响，一方面记账也会有很多心理暗示，我对于花钱一直比较慎重。\n\n从2015年买房，一直到2018年，两三年的时间几乎没有在家里添置任何东西。直到2018年，买了NAS家庭存储，换了电视，买了switch游戏机，也尝试换了洗衣机。这里面的转变也主要来自股市。看多了每天5位数上下的波动后，慢慢也开始觉得那只是个数字了。与其每天看着数字跳动，还不如实打实买点东西。\n\n有了这个转变后，好像忽然看开了，花钱也更随心了，开心就好。\n\n最近常常在想，三十而立，立的是什么？临近三十的时候，会恐惧，觉得去日无多，再折腾的资本会越来越少，害怕一步走错满盘皆输。尤其是伴随着长大的人们一个接一个离开这个世界的时候，会深刻地感受到这个世界的无情：时间的车轮滚滚向前，谁都不会放过。\n\n想着让自己尽量不要被干扰，保持好心态，然而各方面的改变又真实而强烈。\n\n我一直觉得自己是一个耐得住寂寞的人，能够自我约束自我激励。然而2018年却觉得状态垮了。沉不下心看书学习，也沉不下心专心做管理。\n\n对待技术，我一直相信厚积薄发的力量，因此也保持着相当高的热情，也给我带来了不小的回报。然而过去的一年确实积累得少了，回想起来，大概是因为之前专注做技术的目标变得虚无了。没有了目标就陷入了深深的迷茫。不再那么关注业界的动态，不再为新的技术感到兴奋，甚至连自己的业余项目也开始停滞，很难说到底是因为自己对行业失望还是因为没有时间，还是未来不可期了。\n\n而对待管理，接触了很多条条框框，甚至包括做事的清单，但骨子里却并不是太认可。我一直觉得，炉火纯青的时候是可以举重若轻的，如果做什么还需要背下来，那只能说明还处在一知半解的阶段。\n\n一边，熟悉的事情没有了目标，另一边，不熟的事情并不真正认可，于是就陷入自我怀疑的状态。\n\n浮生若梦。混日子的时候总会冒出这个词。在小事上我乐于接受随遇而安。但是大方向也如此时，人就展现出了可怕的一面。\n\n下半年能感觉到自己有了很多不纯粹的想法，并且当我认真分析的时候，发现这些想法并非来自近期的外界干扰，而是由内心生发出来的。这实在是一件很可怕的事。\n\n人性本善？人性本恶？于我来说，更能感受到人性之恶。而所谓的善不过是来自社会的约束。然而，本质虽如此，人却不得不接受自己是社会动物的事实，并接受这些约束，只不过它们有时候叫道德，有时候叫法律，有时候叫良知。\n\n再看“读千卷书，行万里路”会有新的感悟。读书、走路之所以重要，是因为人需要有外界的滋润，来压制内心的邪恶，从而一路向善，岁月静好。\n\n枕边摆着《万历十五年》，讲的是1587这无关紧要的一年，然而这一年却成为后续诸多事情的起点。2018于我可能也是无关紧要的一年，然而我却仍然觉得它很关键。\n\n三十而立，不光是有立锥之地，还需要能立好心态。\n\n因为有过内心的剖析，所以更能坦然面对生活，更能认清生活的真相，知道什么应该敬畏，什么应该打碎，更能轻装上阵，义无反顾。\n\n心怀正念，保持年轻，事在人为，听天由命。\n"]