SegmentFault 脑洞前端最新的文章
2022-05-15T21:37:51+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
**伯克利大学** 的计算机入门教程
https://segmentfault.com/a/1190000041847735
2022-05-15T21:37:51+08:00
2022-05-15T21:37:51+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
0
<p><img src="/img/remote/1460000038855084" alt="" title=""></p><p>每天给你推荐一个新奇,好玩,高品质的开源库,好文,观点或言论等。</p><blockquote>项目主页维护当前月份的内容,想看往期内容,可以翻到下方历史汇总部分,然后选择自己感兴趣的月份点进去即可。</blockquote><h2>2022-04</h2><h3>2022-04-28[工具]</h3><p>import-local 是一个 NodeJS 端的工具,用于检测本地是否安装了某个包。如果你在开发一个 node 的 cli 应用,并且想要提高性能使用用户本地安装好的包,它就很适合你。</p><p>via: <a href="https://link.segmentfault.com/?enc=TZux3X%2BET1i7thgJDseIAw%3D%3D.BReqfs2Exn70TuH9RReJHpvHI4CWjJPlDsEV5ZSZ8MBzOjfeOEcCJJ%2FSK%2BRgBYfZ" rel="nofollow">https://github.com/sindresorh...</a></p><h3>2022-04-28[好文]</h3><p>rest api 中的 POST 和 PUT 有什么区别?一个用于新建资源,一个用于更新资源?不是的!</p><p>via: <a href="https://link.segmentfault.com/?enc=3slsa0YWr9Dy6MIoYTrWBw%3D%3D.4AUAVpVi8k1CRWY4c3UBweUZAKSwH2bJjODwzTKfZl4ChoFSJXJHFTamEEaE0U5jPC189LEtic8hBz7PXnKplfjNrx5NdlM3n%2BfTMk66A6WHeWEj%2BJq2QFwJNZDm7nqq" rel="nofollow">https://stackoverflow.com/que...</a></p><h3>2022-04-25[网站]</h3><p>一个低代码平台,通过它可以拖拽生成自己的网站。</p><p>值得一提的是,一些框架已经开始集成它了。你可以通过 builder.io 导出代码,然后通过一些工具生成各个框架的中间代码(比如 react,vue),也可以直接生成原生 JS 代码。</p><p>via: <a href="https://link.segmentfault.com/?enc=SypQ56m6nPS%2BFTaTl%2FQdsg%3D%3D.BGSRo0eg6uASh8pthQFC4I5b%2BjSp%2FHPSz1o5PR0vVVU%3D" rel="nofollow">https://www.builder.io/</a></p><h3>2022-04-24[工具]</h3><blockquote>今天是中国五一假期的调休。调休真的是一个反人类的设计。</blockquote><p>cypress 是一个 e2e 测试工具,可以很容易地集成到各个测试框架中去,比如 jest。</p><p>via: <a href="https://link.segmentfault.com/?enc=b%2BfQ32dJvRcXkgxItpuK8g%3D%3D.Gn6%2FfZ4lyJPq%2FeKGYJh2tmEjDnINjVqCaP1qOifSz8PkqIpOCwWtq9uMBcDbacMA" rel="nofollow">https://github.com/cypress-io...</a></p><h3>2022-04-23[好文]</h3><p>Navidrome 是一个音乐管理系统,你可以将其部署到本地,然后通过网页播放器播放<strong>本地音乐</strong>。Navidrome 体验 via: <a href="https://link.segmentfault.com/?enc=%2Fv%2FGvfhl2l9KN2wk4%2BK51w%3D%3D.IAkjKYM%2FG4caJXQzeONjNCkh6BRoT0a00A6alFLJR9c%3D" rel="nofollow">https://demo.navidrome.org/app/</a></p><p>很多其他的工具使用的都是网上现成的资源,比如 1listen 就是使用的虾米,QQ 和网易云的音乐源。</p><p><img src="/img/remote/1460000041847737" alt="" title=""></p><p>via: <a href="https://link.segmentfault.com/?enc=yoIOWoKVJ%2BjV7%2F0%2FqxA8Pw%3D%3D.coKFhUn1AFxsHNCG0L0QpdOKPUzo9xm37LuMTXAIxEk%3D" rel="nofollow">https://www.navidrome.org/</a></p><h3>2022-04-22[好文]</h3><p>之前我写过 TypeScript 系列教程,其中有一篇是 <a href="https://segmentfault.com/a/1190000023489694">上帝视角看 TypeScript</a></p><p>这个文章和我的那篇很像,都是从宏观上带大家理解 TypeScript 究竟做了什么。这篇文章比我的更细致一点,推荐大家结合起来阅读。</p><p>via: <a href="https://link.segmentfault.com/?enc=Yp8U1M2%2B4vdtlcxmD9XfVQ%3D%3D.HjSjPV%2F%2FdUuicI2I%2BQBGLbUzaRdjk6YBjjLZRMHntOa78xp0FMCoVEwX7JkFjC0GCeIMdPlHXyCUU%2B1RCgj6iEKwtyIoaTIG8csIU2g0kDQ%3D" rel="nofollow">https://www.huy.rocks/everyda...</a></p><h3>2022-04-21[网站]</h3><p>yandex 提供了简洁的在线翻译功能。 你可以直接输入文字进行翻译,也可以上传文件整体翻译。</p><p>它还提供了 api 供开发者使用,我的 leetcode 项目的部分内容就是用它进行翻译的。不得不承认,专有名词的翻译还是不行,比如动态规划会翻译为 dynamic planning。</p><p>via: <a href="https://link.segmentfault.com/?enc=IDMseyvm%2FM8wXzcurAiwTA%3D%3D.7eH5XxnND%2F6jJzxflMObSxZx%2FTOgFYc%2BoXSz0IqEEX4%3D" rel="nofollow">https://translate.yandex.com/</a></p><h3>2022-04-20[好文]</h3><p>只要 5 美元就可以破解指纹解锁?Your Fingerprint Can Be Hacked For $5. Here’s How.</p><p>via: <a href="https://link.segmentfault.com/?enc=%2BnI3NDu9V1EeMpzjvmePJw%3D%3D.aDqyQXCfk%2BhLyDT5QPjzN5qYAAkRTS3d8ogbUoyYXCzpzBqSXDjVAdg4bySiq0IdSPPGAOQmhiMz4eUfWygAkKb0W1p2DtXw1C%2B%2BFvVWcWecP0eUeDTV2SwaPSxB3kgp" rel="nofollow">https://blog.kraken.com/post/...</a></p><h3>2022-04-19[好文]</h3><p>如果检测有没有全局变量引起的内存泄漏?这篇文章告诉你,作者提供了 js 代码,大家可以直接拿来主义。</p><p>文中有一点没有提到,其实很多全局变量是需要一定条件才会触发的。因此要想真正将其集成到项目的 CI 中,还需要一些额外的条件,那就是在程序中手动多次调用检测方法,而不是调用一次就完事了。</p><p>via: <a href="https://link.segmentfault.com/?enc=ooRXltK%2B9HlnZ76O95C1eQ%3D%3D.2E3yGjcgWwGgK1%2BMn85%2FYdhcabkHYSUcnBwtLerqZtQsjxNxJNsXHA3MXd6JNAxkTTEfyj4hZO1TPmsXVJNr2VLFR3uG22E3IPY%2BrmsPMm5%2FD60eykjn6JXNnh9zUrzbX5xet7HXITbBr87v0Hmavw%3D%3D" rel="nofollow">https://mmazzarolo.com/blog/2...</a></p><h3>2022-04-18[工具]</h3><p>上海疫情使得很多人买不到菜。热爱折腾的网友开源了抢菜软件。</p><p>注:如非必要,不要使用这种极端方法,这会给其他没有菜吃的人带来很多麻烦。</p><p>via:</p><ul><li><a href="https://link.segmentfault.com/?enc=YGkx5HVe130pqEi3QvtfbQ%3D%3D.aT6fdQcMDa4Bo%2FvJ7cDFZAeFL3L9Ta9lDrsB5qpTga9yjBIYbK91%2B%2BBq%2FKtmwi0Y" rel="nofollow">ddshop</a></li><li><a href="https://link.segmentfault.com/?enc=tUBTgjQ64ZSZ7FyruEgXhA%3D%3D.FdLSsBdPTL6ztHHmkEm%2BWnztZRIkxF2zC9fQ0aAfOPVCyTUDq2JdPUhxny%2FR%2BybZ" rel="nofollow">dingdong-helper</a></li></ul><h3>2022-04-15[技巧]</h3><p>Github 的 issue 有很多不好用的地方,比如不支持 comment 自定义排序,以至于有一些插件“钻了空子”,提供了按照 reactions 进行排序的功能。</p><p>Disscussion 弥补了这块空白。 Disscussion 内置两种排序规则,分别是时间顺序和投票数。</p><p><img src="/img/remote/1460000041847738" alt="" title=""></p><p>你可以结合使用 issue 和 Disscussion 获得更好的体验。</p><p>近期 Github 还给 Disscussion 提供了问答社区才有的功能 - <strong>选为答案</strong>。</p><p><img src="/img/remote/1460000041847739" alt="" title=""></p><p>只需要在新建 Disscussion 的时候类别选择 Q&A 就可以体验这个功能了。</p><p>via: <a href="https://link.segmentfault.com/?enc=TWCTqQ1zWGPRyMJa42PtNQ%3D%3D.xFnNLy5vPO9cCvgUnNpQQYifxm0FyfU%2F%2FfYn60TIXoYe58Zs9XJs1uKNTf0PwGXzincqJJLon9PE5k17vLq3rg%3D%3D" rel="nofollow">https://github.com/azl3979858...</a></p><h3>2022-04-14[好文]</h3><p>JS 的继承和传统的 class 继承(比如 Java 的)有什么区别?(How does JavaScript's prototypal inheritance differ from classical inheritance?)</p><p>via: <a href="https://link.segmentfault.com/?enc=LvfoXnDKqpVj5sdDTPrxnA%3D%3D.pq2yeYObUTK3Xe0fRQ2NvI6yBL8SfdKGxOb8ysBSx7YSyWoZ9ag3nqVfvBG5NDlDX8F2zXQ0j97NBTjCHblD9QHEYHgcbF9%2BPeghfXyQ973Rhc24vVFYCyA3X0zqD8EF2zErkJWGK4uhbuzd9ngvOw%3D%3D" rel="nofollow">https://dev.to/chalarangelo/h...</a></p><h3>2022-04-13[网站]</h3><p>和昨天的推荐类似,这个网站也是移除不想要的部分的神奇网站。</p><p>只不过它不是移除图片中不想要的部分,而是分离音频中的人声和非人声。这样就可以达到<strong>移除人声或者移除噪音的效果</strong>。</p><p>via: <a href="https://link.segmentfault.com/?enc=bqmvpxCL%2BVPZMtKjZcVsOw%3D%3D.me5aoM12lDYelBvM0uXvIbzY18g%2BycbmBciG%2BNHqu9k%3D" rel="nofollow">https://vocalremover.org/</a></p><h3>2022-04-12[网站]</h3><p>一个无需注册的在线网站,你可以用它来移除图片中的部分内容。</p><p><img src="/img/remote/1460000041847740" alt="" title=""></p><p>via: <a href="https://link.segmentfault.com/?enc=NejyoSHG6h1110bDA1qCog%3D%3D.Vtai%2B6yQUlfCCCqIhgfmD0S2OFV1hNHlr2Dkjnu6fug%3D" rel="nofollow">https://www.magiceraser.io/</a></p><h3>2022-04-11[网站]</h3><p>一个俄罗斯的网站,据说是全世界最大的<strong>名画博物馆</strong>。</p><p><img src="/img/remote/1460000041847741" alt="" title=""></p><p>并且提供免费的高清下载,比如蒙娜丽莎这里可以直接下载,分辨率是 3931 * 5178,4 M 左右的大小。</p><p>via: <a href="https://link.segmentfault.com/?enc=nfLcKL497CVC0oKuKlWUng%3D%3D.gMXi%2FA%2FOp21JNGjJhEexDrmnP9zDVe1hCpDmZxQXqco%3D" rel="nofollow">https://gallerix.asia/</a></p><h3>2022-04-08[网站]</h3><p>Games104 网站提供了从零学习游戏引擎的教程,有成型的完整代码托管在开源的 Github 仓库。</p><p><img src="/img/remote/1460000041847742" alt="" title=""></p><p>有做游戏的,或者想了解游戏引擎的可以看一下。</p><p>via: <a href="https://link.segmentfault.com/?enc=ypMsNgGZkUJlU0zhh385pQ%3D%3D.rtNHHRzjMep%2BjNmA45TJyO110WZ%2BqF3autoJN0NAiPPeSnoFvfqnIuSu%2Bw4zEiglLwa9v3Pd4JDC3XQSi4X4eQ%3D%3D" rel="nofollow">https://games104.boomingtech....</a></p><h3>2022-04-07[好文]</h3><p>chrome 103 目前支持了 fs api。</p><p>用户可以通过 fs api 来读取文件,写入文件,删除文件,创建文件等。</p><p>比如读取文件的代码:</p><pre><code class="js">let fileHandle;
document.querySelector(".pick-file").onclick = async () => {
[fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const content = await file.text();
return content;
};</code></pre><p>除了 chrome 103 ,其他部分浏览器的新版本也提供了支持,具体支持情况如下图。</p><p><img src="/img/remote/1460000041847743" alt="" title=""></p><p>via: <a href="https://link.segmentfault.com/?enc=fcje4vJqNP3INoX9nYmuuw%3D%3D.tNLXyMhJCG9tr2J%2B52jKxw9CWZ9pTh76LgeGi5k4KUBeRaK6%2F0moeLcEAw22hQvwQ%2BCvqHvaf3Mxwgfc91cv6e4Tn2iLXUsk00eVSF0T0kY%3D" rel="nofollow">https://css-tricks.com/gettin...</a></p><h3>2022-04-06[杂谈]</h3><p>想去贵州看樱花~</p><p>via: <a href="https://link.segmentfault.com/?enc=9AvctXCyqQ%2Fm2dj5syYnBg%3D%3D.9v27ECO%2Fa%2F%2FmfN3%2Bnn%2BHkhse8T3ni1MjlROStmrwD5uDyB069iidEzfh8mYzIqG0c7nPvcOkgRAYzOLU1VbDKZUhfBOjdWHsYYh%2FL9jtB2Q%3D" rel="nofollow">https://fashion.sina.cn/l/ds/...</a></p><h3>2022-04-05[工具]</h3><p>bitbucket 是一个开源的代码仓库,可以用来存放开源项目的代码。</p><p>和 Github,Gitlab 不同,bitbucket 内置了 jira 用于管理需求 ,snyk 用于管理 包安全。个人感觉 Github 和 Gitlab 在这几方面体验还没那么好。</p><p><img src="/img/remote/1460000041847744" alt="" title=""></p><p>via: <a href="https://link.segmentfault.com/?enc=vKzXmyKfIuz52W4Arm2xGw%3D%3D.NZCWHAwrg%2BeJQy%2BG7CGSkIiEqDry7gSIRj6ek7%2BV0x8%3D" rel="nofollow">https://bitbucket.org</a></p><h3>2022-04-03[技巧]</h3><p>vscode 中会自动为 typescript 项目选择 workspace 的 node_modules 的 typescript,但是我们可以手动选择 workspace。</p><p>方法很简单, 你只需要打开一个 workespace 下的 TypeScript 文件,然后点击右下角的 TypeScript 旁边的版本号。</p><p><img src="/img/remote/1460000041847745" alt="" title=""></p><p>然后会让你选择版本。</p><p><img src="/img/remote/1460000041847746" alt="" title=""></p><p>如果有多个 TypeScript ,错误使用其他版本的 TypeScript 会导致编译失败。项目中可以通过配置 vscode 的方式解决这问题。</p><p>具体地,大家可以在项目根目录的 .vscode 文件夹下新建一个 setting.json 然后进行如下配置。</p><pre><code class="json">{
"typescript.tsdk": "node_modules/typescript/lib/typescript.js"
}</code></pre><p>更多用法参考官方文档:<a href="https://link.segmentfault.com/?enc=aTXb097D9ZIWcqleaph5%2FA%3D%3D.yvbIH7WhvGnc%2FvVZMKRDnUg8%2BZV2U7eEPZ5enknoNZjsj6LApdxVMxGCxZWnEOu2YWgQZ5vA8DzoYbWC4wYCJzEeWeMYezr13OPbd%2BOWLfw%3D" rel="nofollow">https://code.visualstudio.com...</a></p><h3>2022-04-02[好文]</h3><p>Github 面试还会布置家庭作业?</p><p>家庭作业也通过 Github 进行。大概是给你一个仓库,然后你 fork 过去后进行编辑,完成后 pr 到原仓库进行 review。</p><p>via: <a href="https://link.segmentfault.com/?enc=%2B0KfdW2NVdezsgiNJtzpgg%3D%3D.9VhKvj3la%2FU7e84pcKUwr8gqkNxAfrNYNioCzGiRq7m%2BN7pBAhxeZJRVdSmUaDy%2F%2BdxLV46oz3%2ByvKasIUX0%2BKGEhSzgq1NpuzjUWvY6Gbs%3D" rel="nofollow">https://github.blog/2022-03-3...</a></p><h3>2022-04-01[网站]</h3><p>CS61A(Structure and Interpretation of Computer Programs)是伯克利所有计算机系学生必须要上的第一门编程课,前半部分以 Python 为主,后半部分以 Schema 为主。网站资源很丰富,作为一个普通游客最主要的就是课件,其提供了 html 和 pdf 两种格式。课件图文丰富,这和其他同级别课程差异很大,对新手比较友好。</p><p><img src="/img/remote/1460000041847747" alt="" title=""></p><p>via: <a href="https://link.segmentfault.com/?enc=W92l6OfehpV8Gh4NOUpyFQ%3D%3D.6EpH0Cx%2FFccHhqqpAXPXErtrcKdW3mFhZE1eZ2JLqXk%3D" rel="nofollow">https://cs61a.org/</a></p><h2>关注我</h2><p>我重新整理了下自己的公众号,并且我还给它换了一个名字<code>脑洞前端</code>,它是一个帮助你打开大前端新世界大门的钥匙 🔑,在这里你可以听到新奇的观点,看到一些技术尝新,还会收到系统性总结和思考。</p><p>在这里我会尽量通过图的形式来阐述一些概念和逻辑,帮助大家快速理解,图解是我的目标。</p><p>之后我的文章会同步到微信公众号 <code>脑洞前端</code> ,你可以关注获取最新的文章,并和我进行交流。</p><p>另外你可以回复大前端进大前端微信交流群, 回复 leetcode 拉你进 leetcode 微信群,如果想加入 qq 群,请回复 qq。</p><p><img width="300" src="https://tva1.sinaimg.cn/large/006y8mN6ly1g7he9xdtmyj30by0byaac.jpg"></p>
跨域了? 装个插件就够了!
https://segmentfault.com/a/1190000041367088
2022-02-06T17:22:46+08:00
2022-02-06T17:22:46+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
12
<p>浏览器为了安全引入了同源策略,这直接导致默认情况下前后端域名如果不同,那么则功能会受限。 随着前后端分离的发展,前端和后端职责的分离,前端会有专门的<code>本地开发服务器(local dev server)</code>用于本地开发。这个时候和后端接口联调时就很可能会遇到跨域安全问题。</p><p>这本身是浏览器的一种安全策略,但是却对前端开发造成了影响。如何解决呢?</p><p>之前我的解决方式是通过本地代理(node 或 nginx 等服务)解决。基本思路就是给请求响应头增加大概如下内容:</p><pre><code>Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400</code></pre><p>后来我使用了更方便的工具: 浏览器扩展。之后跨域问题便可以一去不复返。</p><p>刚开始的时候用的是一个专门给请求加跨域头的插件 <code>Allow CORS: Access-Control-Allow-Origin</code> ,地址:<a href="https://link.segmentfault.com/?enc=x5JKwJ5v603ohshD4dkbrA%3D%3D.%2BcHYc6ND69InuFmE0KtMG7c3PSjLE2h%2FNWDcvRdDqslRu4jewf%2F%2F9U5TjBe0QV0rx2s5KaZGndmsYFqxy4l57qLIql0djIcyGowhVVZxkXCU7DWiYAorC8HuN0li%2FtqAwFSG1bozxPj7edofkHI%2Bt144PPA%2BBaiU%2BjMF6gYcLPQ%3D" rel="nofollow">https://chrome.google.com/web...</a>。</p><p><img src="/img/remote/1460000041367090" alt="" title=""></p><p>这个插件使用起来非常简单,只需要点击切换 on 和 off 的状态即可。 on 的时候会自动给请求头增加跨域功能,off 则相当于禁用了插件。</p><p>后来我发现这个插件有些头不会加上,比如 access-control-expose-headers 。</p><p>因此一个<strong>通用的给请求增加头信息</strong>的插件就有必要了。于是我选择了 <code>requestly</code></p><p><img src="/img/remote/1460000041367091" alt="" title=""></p><p>美中不足是每个规则只能免费改<strong>一个</strong>头。不过好消息是你可以新建多个规则,每个规则改一个头就可以白嫖了。</p><p>地址:<a href="https://link.segmentfault.com/?enc=qvX3U7pD3XaRRfPp6g72gg%3D%3D.nJQ32xK9G2jjXQEnguZff3ZLGTz4MtNeOuVxzBK3EF0%3D" rel="nofollow">https://app.requestly.io</a></p><p>requestly 不仅仅可以增加跨域请求头,理论上可以对请求和响应做任意的修改。因此用来做 mock,统一加参数等等都是可以的。</p><p>基于此,使用浏览器扩展就可以彻底解决前端在本地开发时候遇到的跨域问题了。</p>
Chrome 新功能 - 录制小视频
https://segmentfault.com/a/1190000040944051
2021-11-11T14:33:31+08:00
2021-11-11T14:33:31+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
1
<p>Chrome 97 推出了一个预览功能 - Recorder。它允许你录制 Web 页面的操作并支持<strong>回放,编辑,测量性能</strong> 等诸多功能。</p><h2>它长什么样</h2><p>你可以直接在 chrome devtool 中看到一个 Recorder 面板,点击它就可以体验。</p><blockquote>如果没有找到,可以尝试 cmd + shift + p 调出命令面板搜索 Recorder。当然如果该功能未发布是搜不到的</blockquote><p><img src="/img/remote/1460000040944053" alt="" title=""></p><h2>它有什么用?</h2><p>通过它,你可以实现一些有趣的功能。</p><p>比如:</p><ul><li>测试同学录制一段“视频”, 然后发送给开发,开发根据这段视频定位问题。</li><li>测试某一个业务流程在各种不同的网络和硬件环境下的表现,甚至你可以看其在不同平台的表现(比如 PC,手机,平板等)。</li></ul><p><img src="/img/remote/1460000040944054" alt="" title=""></p><ul><li>自动化测试。你可以录制一段视频,然后通过修改其中部分参数的形式来自动化生成很多测试用例。</li></ul><p><img src="/img/remote/1460000040944055" alt="" title=""></p><ul><li>。。。</li></ul><p>由于是预览版,因此最终是什么样可能还不确定。</p><h2>大招</h2><p>对于我来说,我想要到一个比较有意思的功能。</p><p>我们知道 Chrome 的 devtool frontend(就是你看到的开发者工具) 是开源的,代码托管在 Github:<a href="https://link.segmentfault.com/?enc=USVBbAU8ydNQyDiwZog%2BRg%3D%3D.vwC5BZycIVqKhgjVEfJhH0KT0NaJBYts98TQwYE35YPuRfqyyZm7WcdhjsVqxjRCP45gzX%2BIW6ehBAc1DtQZjQ%3D%3D" rel="nofollow">https://github.com/ChromeDevT...</a></p><p>因此你可以直接集成它到你的项目中。比如你可以开发一个调试工具,这个工具 fork 一下 devtool frontend,然后修改 Recoreder 部分的源码,使得用户可以手动上报自己的录像,然后你将用户的录像数据,网络数据等其他数据发送到你的后端进行分析。</p><p>这个功能我在之前的公司做过,不过做的并不好。而如果依托于 Chrome 团队,那些棘手的问题都不需要你解决了,比如性能问题就很棘手。</p><p>如果你的公司有做用户错误上报或者信息收集的需求,不妨考虑一下是否可以为你所用。</p><p>更多介绍:<a href="https://link.segmentfault.com/?enc=BzJMfhj1zydHrZ54eDK5jw%3D%3D.csPIP3APfLfTyLFEAnxUKZEebmA0UIyyYpaataCp%2BbbjyH2DwsPls4AbpYI5%2FbsbD4wzm%2Bklf%2BiLcy%2Frl%2FIxrw%3D%3D" rel="nofollow">https://developer.chrome.com/...</a></p>
【10月精彩回顾】Github 支持脚注,Chrome插件开发全攻略
https://segmentfault.com/a/1190000040894754
2021-11-01T16:37:58+08:00
2021-11-01T16:37:58+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
1
<h3>2021-10-29[技巧]</h3><p>chrome 的 <code>IdleDetector</code> 可以允许你检测用户是否<strong>AFK</strong>(Away from keyboard),即双手离开键盘一段时间。</p><p>和 requestIdleCallback 不同, 它是检测用户是否空闲,即是否有交互动作,比如鼠标,键盘等。而 requestIdleCallback 是检测浏览器本身是否空闲。</p><p>你可以用它做一些事情,比如官方提到的:</p><ul><li>聊天应用看对方是否在线</li><li>用户不在的话减少一些大型计算</li></ul><p>我之前在看斗鱼直播或者 youtube 视频也会有类型的检测,当你长时间离开的话,它会暂停并提示你是否继续。我觉得这个可以显著地减少用户忘记关闭窗口带来的带宽消耗,不要小看它。这或许可以为你的公司节省很大一笔钱。</p><p>更多关于这个 api 的使用请参考:<a href="https://link.segmentfault.com/?enc=0ipTF%2FPMASd1QGvfbNf4jg%3D%3D.6BrGORjsonflLwKN6pqgJIyTIYxZcg3ld80dfRVe2Qk%3D" rel="nofollow">https://web.dev/idle-detection/</a></p><h3>2021-10-28[技巧]</h3><p>chrome 95 出了一个新的 api <code>EyeDropper</code> 蛮有意思的。 据说这个功能是微软大佬贡献的。</p><p>使用后会出来一个采集颜色的光标,当你确定后会在用户选择的颜色的 rgb 值返回你。</p><p>代码:</p><pre><code class="js">const eyeDropper = new EyeDropper();
const result = await eyeDropper.open();
// result = {sRGBHex: '#160731'}</code></pre><p>大家可以直接在 chrome 95 以上的浏览器上在 devtool 的 console 中输入上面的代码查看效果。</p><p>更多 chrome 95 新特性:<a href="https://link.segmentfault.com/?enc=s2PSOZO3ARfy9km8C3lhUA%3D%3D.wC886bFHJoIVi5AqZ5tlTEzF5TWjbh3sWkwpCbrrNxR%2BoiC8vctDDI3QCA%2FugybmM%2FXgxr0VIJia3k5Fdq2N3w%3D%3D" rel="nofollow">https://developer.chrome.com/...</a></p><h3>2021-10-27[好文]</h3><p>chrome 扩展想必大家都用过么? 那你有想过开发一款 chrome 扩展么?</p><p>西法就开发了一款 chrome 扩展:《leetcode-cheatsheet》<a href="https://link.segmentfault.com/?enc=6OvLSWKhwntgSivV3tdtzg%3D%3D.p0UsbcZEp5ITs8RMgIR70OMMhGaw3RyEFCIaPU3gbFSo2h5Tot7s61sx%2FNFCOTDn" rel="nofollow">https://leetcode-solution.cn/...</a></p><p>chrome 扩展开发最最头疼的就是各种通信,比如 content-script 和 backgound 通信, background 如何和 popup 通信等等。这篇文章对这些常见的扩展开发问题进行了讲述,并有大量的代码和图片,使得内容通俗易懂。</p><p>比如如果在 devtool 新建一个 panel?怎么在 devtool 的 elements 面板新建一个侧边栏。</p><p>代码演示:</p><pre><code class="js">// 创建自定义面板,同一个插件可以创建多个自定义面板
// 几个参数依次为:panel标题、图标(其实设置了也没地方显示)、要加载的页面、加载成功后的回调
chrome.devtools.panels.create('MyPanel', 'img/icon.png', 'mypanel.html', function(panel)
{
console.log('自定义面板创建成功!'); // 注意这个log一般看不到
});
// 创建自定义侧边栏
chrome.devtools.panels.elements.createSidebarPane("Images", function(sidebar)
{
// sidebar.setPage('../sidebar.html'); // 指定加载某个页面
sidebar.setExpression('document.querySelectorAll("img")', 'All Images'); // 通过表达式来指定
//sidebar.setObject({aaa: 111, bbb: 'Hello World!'}); // 直接设置显示某个对象
});</code></pre><p>图片演示:</p><p><img src="/img/remote/1460000040894756" alt="" title=""></p><p><img src="/img/remote/1460000040894757" alt="" title=""></p><p>文章很长,有几万字,大家可以根据自己的需要挑重点看。</p><p>地址:<a href="https://link.segmentfault.com/?enc=3S%2BHG0dzr7LYSFZYMmLAZw%3D%3D.X35H83dk6Qj2OO3fSRdXaQwhd7V8LgGSaTX%2F2meC8%2B%2Bcg90NMsyaAB0Fd6%2BaZZFaGrCGVK51aJMvPKjaNS1Ceg%3D%3D" rel="nofollow">https://www.cnblogs.com/liuxi...</a></p><h3>2021-10-26[好文]</h3><p>OAuth 是什么?你如果还不知道就太 out 了。我的《91天学算法》官方网站就用了 OAuth 来链接 Github 登录,地址:<a href="https://link.segmentfault.com/?enc=37d91ubK6dUc6Rp3Ulv7PQ%3D%3D.wKnejUcEeVQFiVIK4NnN7DuktW0rabtOJcbkIFBqT4A%3D" rel="nofollow">https://leetcode-solution.cn/91</a></p><p>本文以 Github 为例,讲解如果从零接入 OAuth2,适合新手。</p><p>地址:<a href="https://link.segmentfault.com/?enc=WR2gujvf8bZDQ5YH7Ml1Ww%3D%3D.DGPmaUVkVevZRoaLhwUuQ9ymuo0R0%2FuRbSiqlpglFwTaiuu7abr3TNDnwuGj1uE3NZxP9hrIeUd6FuR8zUI91A%3D%3D" rel="nofollow">https://www.honeybadger.io/bl...</a></p><h3>2021-10-25[工具]</h3><p>一个语法检查的工具,注意这里的语法是自然语言的语法,不是编程语言的语法,这同时也是该工具独特的地方。</p><p>地址:<a href="https://link.segmentfault.com/?enc=7YP1rYBmE%2FXiOAJo2hTB0g%3D%3D.wNmJ8uf6fBZYIBMSU7CFpK9%2BIVJLuU2f7bWmk%2FkigECzgjOSAe6ssXpDS7DsrauT" rel="nofollow">https://caderek.github.io/gra...</a></p><h3>2021-10-19[工具]</h3><p>上家公司一直想做一个前端工具链,其中一环就是在编辑器(我们是 vscode)中集成工作流。比如在编辑器中新建工程,提交代码,发布代码, code review , 代码检查等等。</p><p>只不过这个还是需要很多时间投入的,截止到我离职也没有做的很完善。而这个开源产品做的已经相当完善了。如果你的公司有类似的需求,不妨直接尝试使用,或者 fork 一份修改,能够省去不少时间。</p><p>这个工具是直接集成到 vscode 中的,无需切换到其他窗口,对于程序来说方便许多。</p><p><img src="/img/remote/1460000040894758" alt="" title=""></p><p>地址:<a href="https://link.segmentfault.com/?enc=So%2BzmvQVE95Igm9aZvTjNw%3D%3D.4uPTCP4oX%2BdAGOHcdTWYP1BRrORDjjdIMHhm2kZ9bDfowwI9ZxxnadWE2ScelHfQ" rel="nofollow">https://github.com/apptools-l...</a></p><h3>2021-10-18[好文]</h3><p>最近在开发小程序的调试工具,类似微信的调试工具。参考了几篇文章,如果你也在做类似的事情,不妨参考下这几篇文章。</p><ul><li><a href="https://link.segmentfault.com/?enc=5HxbtyQMDkkW8eGU3ZnRTA%3D%3D.RMiR1919zplqRwyfJmjMDjldX98ZkCPeXB3eAHxswYtiw0XYh8bWsBefW9ZbnLxLx5ttw27jAEW06XUuVbq%2FAZVg8pk4fodGvYds75ZQdRc%3D" rel="nofollow">深入理解 Chrome DevTools</a></li><li><a href="https://link.segmentfault.com/?enc=KxH1GSOVt6h3SfkBgrhgkQ%3D%3D.8Wo0LOuzEDRfwC%2BKmrOnEUAP06Zl6k%2B0FXjQS5mWO2iKYGGI835dhEdDvnnwB1MF" rel="nofollow">devtools</a></li></ul><p>不过说实话这几篇文章的思路很值得借鉴,但是缺乏细节,并且部分细节由于版本原因已经缺乏参考性了(新版本 devtool api 发生了些变化),大家在阅读的时候注意一下。</p><h3>2021-10-15[资讯]</h3><p>Github 中的 markdown 语法支持脚注(Footnotes)了。</p><p>你可以使用如下语法</p><pre><code>Here is a simple footnote[^1].
[^1]: My reference.</code></pre><p>这样就可以渲染出如下带有脚注的内容:</p><p><img src="/img/remote/1460000040894759" alt="" title=""></p><p>类似地,之前其他平台有类似如下的渲染脚注的语法,它通过扩展 markdown 链接语法的形式实现了脚注。</p><pre><code>Here is a simple [footnote](http://xxx.com "My reference")</code></pre><p>不过限制也很明显,那就是必须是链接才能生成脚注,Github 的这种脚注语法就很好地解决了这个问题。</p><p>更多关于 Github markdown 的语法可以参考这篇文章:<a href="https://link.segmentfault.com/?enc=CRTW27JLinUOMMxBvQJUAA%3D%3D.xDZPeyzwiWERSUD82xwULuBVpgl3WPt4lWdYixl9op6mfCBTxJ85htEjW5Reash%2F9wtW3DIdO%2FOJa5TaCl4MQMqfBu0RiolmAgVZr3%2BkMGBP4DyREmVbcjR6ric6ydR2KlIC55mMo%2B3AqanzUaiy1UxN4KBJE2lgU93e7Mk5e3YbPgsqnLlG2Zpj7uqB6%2B%2F0" rel="nofollow">https://docs.github.com/en/gi...</a></p><h3>2021-10-13[工具]</h3><p>Graphql 是 facebook 开源的一门查询语言(query language)。</p><p>如今在国内的普及程度还远远不够,主要原因就是上手难度高以及国内社区和大公司输出不够(很多大公司其实都在用)。它不仅仅是前端的一个 client,还需要后端 server 的配合。</p><p>而如果运用得到,甚至可以用来“替代” service worker,redux 等工具。另一方面和 ts 配合,可以大大完善后端接口类型,这个我在 9 月份的每日一荐推荐过相关的工具。今天推荐的是 Graphql 在社区非常有名的一个框架,它的估值也在随着它的流行越来越高。</p><p>地址:<a href="https://link.segmentfault.com/?enc=hh9UsZlr3UVsHJs8lmpIGQ%3D%3D.PUX6HLCKWlwZJQ7HEe94PlA1XQELUpDdPC0aengMHncQRKwpefvJ%2F9zfwXzmXMk%2B" rel="nofollow">https://www.apollographql.com...</a></p><h3>2021-10-12[工具]</h3><p>ES Module Lexer 是一个针对 ESM 的词法分析器,使用它可以对 ESM 文本进行分析,vite 中就使用了它分析文件依赖。由于使用了 wasm(默认情况下), 它的速度很快。</p><p>地址:<a href="https://link.segmentfault.com/?enc=AZjPflzKOJSxwZKP8En%2BHA%3D%3D.9Db0H1%2B%2B8BgJf%2F9R9Ptk17htxenGtRdDdqUlLzZ28h5Gqxm8FyG5Xpyz%2BjnYweLa" rel="nofollow">https://github.com/guybedford...</a></p><h3>2021-10-11[好文]</h3><p>vue 中可以用 v-html 直接动态注入 html,类似地,React 则可以通过 dangerouslySetInnerHTML 设置 html。</p><p>但是如果不加以处理,很可能会遭遇 xss 攻击。 一种简单的方法就是 html entity 转义。社区也有类似的解决方案,比如 DomPurify。而这次官方标准出来了,它就是 Sanitizer API。这篇文章就详细讲述了 sanitizer 是什么,有什么用,兼容性如何,demo 程序,如何开启等等一系列问题。</p><p>地址:<a href="https://link.segmentfault.com/?enc=70t0oVNWJVcKAr1IdkLyeg%3D%3D.ciIc5N0wEjNOIKvMMryxzTUo5wHKgtAqOVYx7UdywxU%3D" rel="nofollow">https://web.dev/sanitizer/</a></p><h3>2021-10-10[组件库]</h3><p>Webview UI Toolkit for Visual Studio Code 是一个针对 vscode 开发的组件库,由微软官方开发。</p><p>相比于其他组件库,它有如下特点:</p><ol><li>针对 vscode 定制,不仅 UI 更加一致,并且还能根据 vscode 主题变换样式</li><li>使用 web components,因此理论上可以应用于任何前端框架</li><li>注重可访问性。这点是国内的很多组件库都不注重的,但是这点却很重要,不仅仅是针对障碍人士,对待一些正常人这也是必要的。比如我习惯使用 ESC 来关闭弹窗,但是很多网站却关不掉,这让我想起了垃圾广告横行的年代。</li><li>官方出品,必数精品。</li></ol><p>地址:<a href="https://link.segmentfault.com/?enc=wxcqzlpCDntGMk95Lmk8vg%3D%3D.ZPnqCxlx%2FW4d9iUx0ldfQBQMxXzW%2BBkukSwXKxKohtiO9KXdQVtSO%2B%2BSk3hXJ3NLlk3JgfTjZ2RBVILt641E%2FQ%3D%3D" rel="nofollow">https://microsoft.github.io/v...</a></p><h3>2021-10-09[网站]</h3><p>很多人会问这样的问题:<strong>xx 语言的 yy 特性在 zz 语言中怎么写啊?</strong> 比如 Python 语言的 reverse 在 C++ 中怎么实现?</p><p>我其实刚刚在用新语言的时候脑子也潜意识的有这种疑问,今天介绍的网站就整理了<strong>很多常见操作的不同语言对比实现</strong></p><p>仍然以 C++ 的 reverse 为例:</p><p><img src="/img/remote/1460000040894760" alt="" title=""></p><p>你可以点击上面的编程语言查看其他语言的 reverse 是如何实现的,目前该网站已经提供了 277 个语言特性,这个工具网站对那些刚开始学习新语言的人非常有用。</p><p>我们甚至可以直接开启对比模式,以 Python 和 C++ 对比为例:</p><p><img src="/img/remote/1460000040894761" alt="" title=""></p><p>地址:<a href="https://link.segmentfault.com/?enc=omYuADoDBjg2C2kc1kNm4A%3D%3D.5fm1fkluhUaQmWBoCCzjE%2FyjjlRkyzGYe0T91KqPKkvZGblm5JLo009xLPZPxUBYFwWZ5Szm6TfhusAtZWYe4Q%3D%3D" rel="nofollow">https://programming-idioms.or...</a></p>
【每日一荐月刊】每天分享一点好玩的东西~
https://segmentfault.com/a/1190000039763904
2021-04-02T13:58:01+08:00
2021-04-02T13:58:01+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
1
<h2>每日一荐</h2><p><img src="/img/remote/1460000038855084" alt="" title=""></p><p>每天给你推荐一个新奇,好玩,高品质的开源库,好文,观点或言论等。</p><blockquote>项目主页维护当前月份的内容,想看往期内容,可以翻到下方历史汇总部分,然后选择自己感兴趣的月份点进去即可。</blockquote><h3>2021-03</h3><h4>2021-03-31[工具]</h4><p>一个可以实时翻译不同语言,以支持不同语言的人一起开会。这或许就是跨国远程办公党的胜利?</p><blockquote>更容易赚美刀 ^\_^ 啦?</blockquote><p><a href="https://link.segmentfault.com/?enc=dOQzg8tHBcLXm9ERRD4AKA%3D%3D.HniDlVLUhYiuDzbhiKQVTBQsRJqqY6nYLZbdZ%2BPGM5G64AMr7M03TtI5PPxE3gT4m5FO44GL9PNNqI0%2F5hMe%2FhCfQT%2B2pRSl7zyfUxSCv4ALTZdVHTQkuANJeGoia2gtnsliMyVEghj%2FH9%2F9l7VN%2Bn5rUdhRYqri1U1AISXbH6U%3D" rel="nofollow">https://business-review.eu/te...</a></p><h4>2021-03-30[类库]</h4><p>prisma 是一个 Nodejs 端的 ORM 框架。和 <a href="https://link.segmentfault.com/?enc=kNDRd%2Bqvr3%2BF%2FC8RhMJlXw%3D%3D.63JXlKcBsBs9w3y578UFK3jCNY8TNDz6Uj08sQqvFPNqvrL7pmN8RGIEN1%2BmCdED" rel="nofollow">waterline</a> 类似,也提供了多种主流数据库的接入,以及统一的便捷的封装函数。</p><p>从使用体验上来看,prisma 更加舒服。 prisma 通过自己定义一套 DSL(prisma 文件中使用的语法),并自己解析,使得开发体验更加友好。另外从 Github 的 star 数以及 npm 上的下载量来看,prisma 都表现地不错。</p><p>使用示例(Create a new User and a new Post record in the same query):</p><pre><code class="js">// Run inside `async` function
const user = await prisma.user.create({
data: {
name: "Alice",
email: "alice@prisma.io",
posts: {
create: { title: "Join us for Prisma Day 2021" },
},
},
});</code></pre><p>Update an existing Post record</p><pre><code class="js">// Run inside `async` function
const post = await prisma.post.update({
where: { id: 42 },
data: { published: true },
});</code></pre><p>地址:<a href="https://link.segmentfault.com/?enc=%2BrYp1oyULWA9R0wNWr130A%3D%3D.u1Whu%2F3XpgmYeXtok3fKs1qpSulFuUyhtL%2FTuiznZVd%2FeONbF%2FIqv5o2IfqN2ry7" rel="nofollow">https://github.com/prisma/prisma</a></p><h4>2021-03-29[工具]</h4><p>一个可以运行在多端(window, linux 和 mac)的 switch 模拟器。从此畅玩 switch?</p><p>地址:<a href="https://link.segmentfault.com/?enc=W6UQQ2t%2Br6p9y5eaFuKKnQ%3D%3D.knQW8ChMDFVUqPlskdwj6w6effa3EAhl24dvBaMmYxk%3D" rel="nofollow">https://ryujinx.org/download</a></p><h4>2021-03-26[工具]</h4><p><a href="https://link.segmentfault.com/?enc=D69FA2doXEbqi2I4UrZvNA%3D%3D.bZWb5qhNZme7LdhwX3mnoobv%2FeEDkprTUJ7%2BhJgk88F8TIDm5ZN1PkflC8l5mTKD" rel="nofollow">https://github.com/azl3979858...</a></p><h4>2021-03-25[好文]</h4><p>OpenSSH 是使用 SSH 透过计算机网络加密通信的开源免费实现。大多数程序员都或多或少和它打过交道。</p><p>OpenSSH 实践手册详细讲解了如何使用 ssh,比如 sshpass 实现无密码登录,做 Socks 代理,端口转发等。我想<strong>大多数人都没有彻底利用好 ta</strong>。</p><p>地址:<a href="https://link.segmentfault.com/?enc=uWCwwZf%2FCeohWyfZerLRZA%3D%3D.6HtNE%2FKEErtYJF%2B%2FiyYFs7SnCY0bb%2FyLFSSw2NVlHGE%3D" rel="nofollow">https://engr-z.com/326.html</a></p><h4>2021-03-24[类库]</h4><p>vue-use-gesture 是一个 vue 的 hooks 库,用于手势控制。可以使得任意的元素可拖动。你还可以配合 <code>vue-use-spring</code> 实现更多动画效果。</p><p><img src="/img/remote/1460000039763906" alt="" title=""></p><p>地址:<a href="https://link.segmentfault.com/?enc=CcAF9eWEqNNDb2m0qB%2BVzg%3D%3D.jgSqzmQ9XY%2BSdqgNnv1n0lY7P9bKNmCnes40ltB1CLpAgdlP9SssDRgYLKDL%2B7Mh" rel="nofollow">https://vue-use-gesture.netli...</a></p><h4>2021-03-23[工具]</h4><p>今天介绍的是一个打包工具 snowpack。它借助了现代浏览器支持 ESM 的特性,使得开发的时候不进行打包,而是每次修改文件只修改改变部分的 ESM 模块,使得热更新的时间复杂度从 $O(n)$,降低到 $O(1)# 其中 n 为模块数目。</p><p>推荐的做法是开发中用 snowpack 的 ESM 构建使得构建速度不会随着项目变大而增长,发布的时候根据自己项目的需要支持的浏览器进行打包。</p><p>snowpack 也提供了类似 React 的 create-react-app。使用方式很简单,敲以下命令就可以了:</p><pre><code class="bash">npx create-snowpack-app react-snowpack --template @snowpack/app-template-react</code></pre><p>地址:<a href="https://link.segmentfault.com/?enc=%2F2g1UNv0L4c8nkqXBDt6xQ%3D%3D.m4HojHSLZZEIWBGnTCzxIv%2Bh1BFWzhdnaEuBXPFY9Gs%3D" rel="nofollow">https://www.snowpack.dev/</a></p><h4>2021-03-22[类库]</h4><p>console-ban 是一个脑洞很大的库。其可以让你的前端代码免于被人打开控制台查看,减少了前端代码被分析,盗取的风险。</p><p><img src="/img/remote/1460000039763911" alt="" title=""></p><p>地址:<a href="https://link.segmentfault.com/?enc=kDYg3i3qGzvqznZLY%2F8asQ%3D%3D.i4slQlecEI%2FenjnBxnJfxd%2BbLo602sg4fU9i2xqARLzQ3qMFKcRmEXFvYSpig50Y" rel="nofollow">https://github.com/fz6m/conso...</a></p><h4>2021-03-19[类库]</h4><p>nolimits4web(一个俄国程序员) 写的 swiper 或许是最好的 swiper 插件了。唯一的缺点就是太大了,不过你可以懒加载它。如果你的需求比较轻量,不想引入那么大的文件可考虑使用 tiny-swiper,地址:<a href="https://link.segmentfault.com/?enc=4Z31hRX7Bbpih6wNs%2BER8g%3D%3D.hJTGpOFY0UHbp5EI2DnbAXO2IBKeRJwIeBaSO0JrQ%2FIRN95xqkl%2FeSYImoIz7IAp" rel="nofollow">https://github.com/joe223/tin...</a></p><p>swiper 地址:<a href="https://link.segmentfault.com/?enc=%2FiuaTYf5ybz%2FYbCl9MLuwg%3D%3D.s2PPFR5P70FG9QTmBYbElYixbxy5%2FyvZQXbKkMpgSMJFscEyWEDh0d3pJnuKTt2y" rel="nofollow">https://github.com/nolimits4w...</a></p><h4>2021-03-18[仓库]</h4><p>波兰的的一个牛人用 JS 实现了一个非常火爆的像素风游戏 《我的世界》。服务端和客户端代码全部开源。</p><p>地址:<a href="https://link.segmentfault.com/?enc=24DGpx%2FRm8iA%2F6UXoQGcvg%3D%3D.m5LJIRjTPXu3dOtZltmArxqGx%2F0lmpCG%2FKhGFmGu%2BUj49SVglIkmMTCf3f9uzF4H" rel="nofollow">https://github.com/michaljaz/...</a></p><h4>2021-03-17[网站]</h4><p>OrbitDB 一个去中心化的 p2p 数据库,非常有意思。相对于传统的集中式, OrbitDB 将数据分布在我们每个人的机器上。你可以通过他们的网站来感受一下。</p><p>地址:<a href="https://link.segmentfault.com/?enc=tIoGO9DVgP%2Bwt13I%2B0WWpg%3D%3D.69SpUUk1xBj75I8h%2Bk2FWeJTmi6r8mNw0zpVXEjBcJI%3D" rel="nofollow">https://orbitdb.org/</a></p><h4>2021-03-16[类库]</h4><p>一些网站会有一些新手导航,就是下一步上一步的那个蒙层,用来帮助用户快速了解网站的使用方式。</p><p>之前刚毕业的时候用过一个这种类库,是基于 React 的 reactour。 类似的还有 react-wizard-tour 等。</p><p>今天给大家介绍的是一个原生 JS 写的类库 - shepherd,其不仅支持原生,还提供了所有主流框架的集成版本,比如 react,vue,angular 等等。此外其 api 设计比较好,star 也比较多。</p><p>地址:<a href="https://link.segmentfault.com/?enc=Z7yOZK17ABZ8SEs7jd9sOg%3D%3D.TJejBCTY7eP9zl8B%2Bb7PeAZ43lEA5MazDT26oiq5JbO4iFWMU9jgW29moN%2FXYyBA" rel="nofollow">https://github.com/shipshapec...</a></p><h4>2021-03-15[好文]</h4><p>广告无处不在,它是支撑互联网高速发展的经济基石。互联网广告对互联网公司意味着什么?互联网的产业组成有哪些?CPC/CPM/CPS/CPI,这些术语意味着什么?各种 Vlog 博主能赚多少钱?普通人如何利用互联网广告赚钱?这篇文章告诉你答案。</p><p>地址:<a href="https://link.segmentfault.com/?enc=dOcOLQaIjs%2FwTN4872wVtg%3D%3D.SnLm9ZgVG32wLrt2MKIM2I4wh%2BM3h1Xl6u3PJxFBY24Hd8n8YyWGEJjB32CuIlWU7rzRnBblERLPUjTDp9xC1A%3D%3D" rel="nofollow">https://www.bmpi.dev/dev/what...</a></p><h4>2021-03-12[好文]</h4><p>很多技术人员到了一定的阶段(年纪)都会转到管理岗。而技术转管理很多人一开始是不适应的,毕竟工作内容以及责任发生了很大的变化。从对自己负责变成了对整个团队负责。</p><p>人的习惯是难以改变的,这需要大家不断地有意识地提醒自己并纠正才可以。除此之外一个指导准则也很重要,这几篇文章是我整理的适合技术转管理看的文章列表。</p><blockquote>列表后续也会持续更新</blockquote><ul><li><a href="https://link.segmentfault.com/?enc=3w9UHeqoE1cQIvEHuIZLlg%3D%3D.uf7ckqd1Ynp%2B7nwfDuyuYujMVkwqUWz3bPIWzAhcYEaUHOgu19iHajRQ%2BYWwpLFReMLjcvQHjRj3N0L%2FdyAs1A%3D%3D" rel="nofollow">如何做好技术 Team Leader?</a></li><li><a href="https://link.segmentfault.com/?enc=7E7R%2FH9UiUAdTGkDJbsoZA%3D%3D.rF0rCDKqKJaBZryo7fpCFyBv6vQc0zeqpDoAc%2Ft0DS0NFNJOBzYmhW37I1E6eePbz1AqrjEcCsGW7akCNvU15w%3D%3D" rel="nofollow">想要做大事,就要有高效的协作机制</a></li></ul><h4>2021-03-11[好文]</h4><p>很多公司都在讲 OKR。但说实话落地起来都效果不好,各种走偏。这其实也和当前的发展阶段有关。相关的 OKR 文章我也看过一些, 而这篇文章是字节飞书团队写的,是我见过<strong>实操性最强的文章</strong>。不仅有大量的例子还有工具(飞书 OKR 模块)配合。如果你正在用 OKR 或者即将使用 OKR,一定不要错过~</p><p>文章列表:</p><ul><li><a href="https://link.segmentfault.com/?enc=TTZmmIotLncUuvqrI9MD%2Fg%3D%3D.VIjLIjgfIRoNU2gAa7a8uoMqZD7SkV8tkFiDitfZdwCSe44x9pt89CmaGp8kcvyzK1nXs7OrszznYEBtIyflfQ%3D%3D" rel="nofollow">5 分钟快速掌握 OKR 管理法 - OKR 理论篇</a></li><li><a href="https://link.segmentfault.com/?enc=9kY6ZLTkLGVhVNB%2FO1pu3w%3D%3D.j32kDO5Wo6QEQRiIqtXyWrDzz3mo42EWW8cweoxDJOmePyf6RGigKHwBuUTCmaDJrfNjre%2B8BTcSvBwR64Vdvg%3D%3D" rel="nofollow">5 分钟快速掌握 OKR 管理法 - OKR 实施篇</a></li><li><a href="https://link.segmentfault.com/?enc=u%2BaioflB%2FUuxoTrX01HjEw%3D%3D.oLbuwx%2BML7tvm3zTsfiU2KdORCKWQ185aZe%2BYb9g1EeDe7N9PVlxiSRFBEPH6Jkyc1NgoDiMKcM9GW1I1zsqYg%3D%3D" rel="nofollow">制定与撰写 OKR | 两种思路与七类方法</a></li></ul><h4>2021-03-10[好文]</h4><p>文章名《如何构建一款超级玛丽》。虽然说是教你构建超级玛丽,其实就是一个简单的小人运动,并没有什么吃蘑菇的场景。不过基于它的代码再去实现也不是难事,毕竟入门是最难的。作者写了好几篇博客来讲构建超级玛丽的细节。</p><p><img src="/img/remote/1460000039763909" alt="" title=""></p><p>文章地址:<a href="https://link.segmentfault.com/?enc=K2A%2BYt%2Fx4NGBAE%2BTnuxgkg%3D%3D.zszVIzRcgbGEM0NUGgRjjWEmZPR1rlkCS9ohvWbznCdGwu2ENXsD2Ro1bWqw17J9bti3CmVBiUA0bWb9jlruHA%3D%3D" rel="nofollow">http://www.wopaige.cn/views/J...</a></p><h4>2021-03-09[仓库]</h4><p>随着 vue3.0 的发布 elementui 也开始跟进,推出了 elementui-plus。如果你对 element-ui 比较熟悉,那么使用它的话过渡也会比较自然。</p><p><img src="/img/remote/1460000039763907" alt="" title=""></p><p>地址:<a href="https://link.segmentfault.com/?enc=v3JhgkXmWWdk%2F%2FG1I40NJA%3D%3D.hqHvdDkdYFRTgm2DAn5EYdhQFECs5IjKQWzmVPJxYtfV8FZMNTuQfptYenid9MZ%2F" rel="nofollow">https://github.com/element-pl...</a></p><h4>2021-03-08[仓库]</h4><p>一个关于 JS 统计的仓库,收集一些比较流行的框架,工具等,并按照多个维度进行整合,帮助大家<strong>快速了解最近有哪些 JS 技术值得关注</strong>。</p><p>地址:<a href="https://link.segmentfault.com/?enc=SPXJ26Gh%2FAlA8GVi2ghJmw%3D%3D.mSSOUF87uNxmXvK8K99AfUD2zYIUefZkW%2BDjwBdID%2BKRLI4QBppyCvDnl1HDqjfSepgw20Bz%2BNfNNBd1pgS7zw%3D%3D" rel="nofollow">https://github.com/bestofjs/j...</a></p><h4>2021-03-05[好文]</h4><p>有道前端团队出品的《有道云笔记新版编辑器架构设计 》,分为上下两篇。详细讲述了<strong>富文本编辑器</strong>的采坑之路。如果你对<strong>富文本编辑器</strong> 感兴趣,千万不要错过。</p><p>文章地址:</p><ul><li>上篇:<a href="https://link.segmentfault.com/?enc=ej92RTuTn7urhXfVuwx8xQ%3D%3D.4DnNAvjSE1E7dDIJgegzvVa6Qm5D8o0zX7i%2B1TAsUdux3z6V9iCvsh8EoX7V3hYN" rel="nofollow">https://zhuanlan.zhihu.com/p/...</a></li><li>下篇:<a href="https://link.segmentfault.com/?enc=7tleq18YhITZnyr%2BIhJogA%3D%3D.kGlz2iboguBgJIlUjGKIOF69vmt%2BNgMa8dJSfsuO0G%2FubUL017QXtM4dfs5gnpFN" rel="nofollow">https://zhuanlan.zhihu.com/p/...</a></li></ul><h4>2021-03-04[仓库]</h4><p><img src="/img/remote/1460000039763908" alt="" title=""></p><p>类似于 Rust,deno 等,rome 是对 JS,TS, JSON, HTML, CSS 等资源进行管理的一体化工具。之所以说是一体化,指的是其提供 lint,编译,打包,测试等全生命周期。</p><p>它的出现就是位了取代 babel,eslint,webpack,prettier,jest 等等,可以看出其野心之大。这或许也是没有被大家广泛使用的原因之一吧。现在项目已经 14.8 star,值得大家关注一波。</p><p>地址:<a href="https://link.segmentfault.com/?enc=9DHosJKpMb2ayMk4dVvtcg%3D%3D.i7J1jcIFZhiCahu9x%2BOYQPgx0p2ycJ73eHPWc92WUJc%3D" rel="nofollow">https://github.com/rome/tools</a></p><h4>2021-03-03[好文]</h4><p>一个关于 Ryan Dahl (nodejs 和 deno 的作者)的采访。来看看,大佬是如何看待当前大环境的,以及大佬有哪些良好的习惯可以借鉴吧!</p><p>地址:<a href="https://link.segmentfault.com/?enc=%2FUPvJK2wbJCkBRiA2lyu1w%3D%3D.%2FIMp0EwjGjsAzJyDgh%2FxFwojpQLjJ2FbuoU%2BNh2iNL6wXF7G%2FWJFEFR43ypc6phM" rel="nofollow">https://evrone.com/ryan-dahl-...</a></p><h4>2021-03-02[网站]</h4><p>有时候你想快速运行一个代码片段,而不想繁琐地准备一大堆环境。JS 有类似的工具浏览器控制台或者 runjs,而其他语言呢?</p><p>其实很多语言都有在线运行网站,这里推荐一个在线运行各种语言的网站(现在支持语言有十余种)。免去了你准备环境的烦恼,直接打开浏览器输入代码就可运行看效果。</p><p><img src="/img/remote/1460000039763912" alt="网站效果" title="网站效果"></p><p>地址: <a href="https://link.segmentfault.com/?enc=4k7QLYvSmNuwetFk8o1orA%3D%3D.0rVCAJfO4L0YitajB1%2FO5ufQD9jsoEeDpOlBjcJMBfSRhkr5siTWqRrqUY9ZrDZPoSxCuG5fCrJ%2BppLt1PGzWg%3D%3D" rel="nofollow">https://www.onlinegdb.com/onl...</a></p><h4>2021-03-01[好文]</h4><p>91 天学算法是我和几个算法大佬一起组建的一个算法提高班。通过 91 天的集中化的学习,让你<strong>遇见更好的自己</strong>。</p><p>这是 91 天学算法第三期视频会议的一个文字版总结。</p><p>地址:<a href="https://link.segmentfault.com/?enc=9LO1r6Lqhgz0CzaV7P1Iog%3D%3D.wioOnhUqwXjw4kbYM85tiiAjYVmbdrHUPUJNCyhYkWMFIaMGnixRl9jiaWQI7jHfaBjXKcWQV10F93XJp8uuCg%3D%3D" rel="nofollow">https://lucifer.ren/blog/2021...</a></p><h3>关注我</h3><p>我重新整理了下自己的公众号,并且我还给它换了一个名字<code>脑洞前端</code>,它是一个帮助你打开大前端新世界大门的钥匙 🔑,在这里你可以听到新奇的观点,看到一些技术尝新,还会收到系统性总结和思考。</p><p>在这里我会尽量通过图的形式来阐述一些概念和逻辑,帮助大家快速理解,图解是我的目标。</p><p>之后我的文章会同步到微信公众号 <code>脑洞前端</code> ,你可以关注获取最新的文章,并和我进行交流。</p><p>另外你可以回复大前端进大前端微信交流群, 回复 leetcode 拉你进 leetcode 微信群,如果想加入 qq 群,请回复 qq。</p><p><img src="/img/remote/1460000039763910" alt="" title=""></p>
如何自动同步博客到 Github 主页?
https://segmentfault.com/a/1190000039298060
2021-02-27T21:09:14+08:00
2021-02-27T21:09:14+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
0
<h2>前言</h2><p>Github 支持通过创建同名仓库的形式自定义主页。比如我的 Github 用户名是 azl397985856,那么新建一个名为 azl397985856 的仓库即可。接下来你可以通过修改此仓库的 README 文件来自定义 Github 主页。也就是说,你想要自定义主页就新建一个同名仓库并修改 README 就行了。</p><p>修改 README 能玩出什么花样呢?请接着往下看。</p><h2>装修效果</h2><p>先上一下我的装修效果:</p><p><img src="/img/remote/1460000039298062" alt="" title=""></p><h2>开始动手</h2><h3>添加数据统计</h3><p>上图的那几个 Github 数据统计以及奖杯使用的是一个外部服务。想要显示哪个就添加相应代码即可:</p><p>数据统计:</p><pre><code class="md">![](https://github-readme-stats.vercel.app/api?username=azl397985856&show_icons=true)</code></pre><blockquote>注意将 username 改成自己的用户名哦(下面也是一样,不再赘述),不然就显示的 lucifer 我的信息啦。</blockquote><p>奖杯:</p><pre><code class="md">![](https://github-profile-trophy.vercel.app/?username=azl397985856&theme=flat&column=7)</code></pre><h3>自动更新博客</h3><p>如上图我的装修主页,其中博客的文章列表不是写死的,而是每隔一个小时定时读取我的<a href="https://link.segmentfault.com/?enc=%2Bw5ubl7AmIsC1Yfbx8wryQ%3D%3D.jUHdhhcvhtl1z1%2FTcCzHf1e7HjpTfkLUc6mn7d7LluU%3D" rel="nofollow">博客</a> 内容,并提取前 5 篇文章。</p><p>如果你也想要这个功能,就在 README 中添加如下代码即可:</p><pre><code class="md">## 📕 Latest Blog Posts
<!-- BLOG-POST-LIST:START -->
<!-- BLOG-POST-LIST:END --></code></pre><p>之后读取的博客列表会填充在两个注释之间,也就是说你可以<strong>通过改变注释的位置,将其放到页面任意位置</strong>。</p><p>为了实现<strong>每个小时定时更新的功能</strong>,我们可以使用 Github Action 的定时任务来实现。</p><p>具体操作步骤如下:</p><p><img src="/img/remote/1460000039298065" alt="" title=""></p><p><img src="/img/remote/1460000039298063" alt="" title=""></p><p>接下来将如下内容复制粘贴进去:</p><pre><code class="yml">name: Blog Posts
on:
# Run workflow automatically
schedule:
# Runs every hour, on the hour
- cron: "0 * * * *"
jobs:
update-readme-with-blog:
name: Update this repo's README with latest blog posts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gautamkrishnar/blog-post-workflow@master
with:
# comma-separated list of RSS feed urls
feed_list: "https://lucifer.ren/blog/atom.xml"</code></pre><blockquote>注意:这里的 <code>cron: "0 * * * *"</code> 的意思是每个小时进行一次,并且是每个小时的 0 分。 因为你需要等到下一个整点才能看到效果,有时候 Github 会有延时,晚几分钟也正常,大家不要着急,耐心等待即可。</blockquote><p>请将 feed_list 替换为你自己的 RSS 订阅地址。如果有多个订阅地址,则用英文半角逗号分割。</p><p>如果你的博客没有 RSS 或者你不知道自己的 RSS 地址就无法使用了哦。我的博客是用 hexo 生成的,因此添加 RSS 就很容易了,如果你的博客是挂到第三方的,也会提供 RSS 地址。比如 CSDN 就提供了 RSS 地址:</p><p><img src="/img/remote/1460000039298064" alt="" title=""></p><p>由于大家的博客可能都不相同,因此具体大家可以自行搜索。</p><h2>完整源代码</h2><p>本文所有的代码都可以在如下的代码仓库中找到。</p><p>仓库地址:<a href="https://link.segmentfault.com/?enc=hbaOWysyBHKYDo998fjYiQ%3D%3D.9uds5vDYQDOZCtP9SvIvUPTdc%2FRqYTQutH3GdBPCKpTtQX1e42MHX2Uyq1US5M9O" rel="nofollow">https://github.com/azl3979858...</a></p><p>如果在使用过程中碰到其他问题,也欢迎私信我哦~ 最后祝大家都有一个高大上的 Github 主页。</p>
2021届秋招哈啰出行前端面经(一面)
https://segmentfault.com/a/1190000039261344
2021-02-23T14:42:28+08:00
2021-02-23T14:42:28+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
6
<h2>面试过程</h2><ol><li>简单做个自我介绍吧。</li></ol><ul><li>面试官您好。。。</li></ul><ol><li>看到了你的项目经验,简单介绍一下你的项目吧。</li></ol><ul><li>。。。</li></ul><ol><li>你的项目中用到了 React,用的是那个版本呢?</li></ol><ul><li>用的 React 16.5(记错了,应该是 16.8)</li></ul><ol><li>React 16.5 已经有 Hooks 了吗?</li></ol><ul><li>没有(场面一度十分尴尬,React 16.8.0 是第一个支持Hook 的版本)。</li></ul><ol><li>Hooks 和 class 有哪些优势?</li></ol><ul><li>有时 class 里不用的生命周期函数中会出现相同的逻辑,使用 Hooks 使得代码更易维护。</li></ul><blockquote>lucifer 注: hooks 带来的优势一方面是赋予了函数式组件更多功能,使得其从功能上可完全替代 class 组件。 另一方面 hooks 创造了一种新的代码组织方式,和以前的 mixin 以及 render props 的出发点是类似的,但其注重相同功能的代码放到一起,使得代码逻辑复用多了一种选择。当然这其中有利有弊,由于篇幅限制,不在这里展开。</blockquote><ol><li>你用过 Hooks 的哪些方法?哪些函数?</li></ol><ul><li>用过 useEffect。</li></ul><blockquote>lucifer 注: 建议大家多讲几种,比如 useState, useCallback 等。</blockquote><ol><li>useEffect 有什么特点?</li></ol><ul><li>使用 useEffect 可以模拟一些生命周期函数,比如 componentDidMount,componentDidUpdate,componentWillUnmount。</li></ul><ol><li>react 的生命周期有哪些?</li></ol><ul><li>挂载时有 componentDidMount,更新的时候有 componentDidUpdate,shouldcomponentUpdate,卸载的时候有 componentWillUnmount 等。</li></ul><ol><li>componentWillReceiveProps() 你了解过吗?</li></ol><ul><li>没有了解过。后来查文档说 componentWillReceiveProps() 不太常用。</li></ul><ol><li>当 Redux 中的数据发生变化时,哪几个生命周期会发生变化?或者解释一下 Redux 的运行机制?</li></ol><ul><li>乱答了一通……答得不好。其中谈到了 redux 主要用于复杂的数据流。</li></ul><blockquote>lucifer 注:推荐大家阅读 <a href="https://link.segmentfault.com/?enc=p28fDGiq3hMpUmTB6L9ioQ%3D%3D.VFLZzWkLwnytuahHojZtJJ3V%2BWOm%2Fx1H9cwNvdHYoSfDYI7nUjwh7nGaUAWLqPwb" rel="nofollow">深入理解redux</a></blockquote><ol><li>复杂的数据流是指什么样的?</li></ol><ul><li>给自己挖了个坑,没太说明白,原因在于不了解 Redux 的应用场景,即为什么要使用 Redux。这篇文章 讲得挺好的,意思就是当我们的状态分布在多个组件中,跟踪这些组件的状态变得繁琐时,就可以使用 Redux 来统一管理了。</li></ul><ol><li>webpack 平时做过哪些配置?</li></ol><ul><li>平时会设置 loader 和 plugin,用过插件 commonChunks 等。</li></ul><ol><li>plugin 和 loader 有什么区别?</li></ol><ul><li>loader 相当于一个模块转化器,比如将 less 文件转化为 css 文件,plugin 支持一些拓展的插件,比如 HTMLWebpackplugin,commonChunks 等。</li></ul><ol><li>loader 如何将 less 文件转化为 css 文件?</li></ol><ul><li>这里没搞清楚是问具体配置还是问原理,结果面试官就换下一个话题了。</li></ul><blockquote>lucifer 注:其实就是 ast 的解析和转化,具体内容大家可以搜索下相关文章。</blockquote><ol><li>loader 的执行顺序是什么样的?</li></ol><ul><li>从右往左、从下往上。</li></ul><blockquote>lucifer 注:没看到从下到上什么意思。 不过 loader 的执行顺序是借鉴了 compose,函数式编程中的 compose 的结合顺序就是从右向左,而功能类似的 pipe 则是从左到右,大家常见的 linux 管道(pipe)就是从左到右。</blockquote><ol><li>我想配置一个单页应用,如何配置 entry 部分?</li></ol><pre><code class="js">entry: {
// 这里写什么
}</code></pre><ol><li>ES6 了解吗?let, const, Map, Set, 箭头函数等等。</li><li>下面的代码 b 等于什么?</li></ol><pre><code class="js">let a = [1, 2, 4]
let b = a.map(v => { v= v*2 })</code></pre><p>我先回答了 b = [1, 4, 8](脑子抽了,小学数学都算错),面试官问为什么不是 [2, 4, 8],我说说错了😂</p><p>然后又说这样不对,因为箭头右边有个大括号。</p><p>其实上面代码中 b 是 [undefined, undefined, undefined],面试官引导该怎么处理才能使 b 为 [2, 4, 8]。</p><p>答案就是如果箭头右边是打括号,需要在大括号内加一个 return 返回就行了。即下面的代码:</p><pre><code class="js">
let a = [1, 2, 4]
let b = a.map(v => { v= v*2; return v; })</code></pre><p>平时写 map 一直都没用过大括号,面试的时候才明白,我炸了!</p><ol><li>了解事件循环吗?</li></ol><blockquote>lucifer 注:推荐阅读我之前写的 <a href="https://link.segmentfault.com/?enc=ZxhO2LV%2FlZ%2BU0WrRzyGGHQ%3D%3D.5nacwXhs1vZn2jNVS8d5cGTnVrshjAT8ipIjmnb8PT1%2BBMY7rJK%2BM6dJACJ43YLf" rel="nofollow">《一文看懂浏览器事件循环》</a></blockquote><ol><li>有什么问题?</li></ol><p>具体工作地点看自己安排。杭州和上海。杭州主要负责算法平台的搭建和地图相关。技术栈是 React。</p><h2>感受</h2><p>面试官很和蔼,由于堵车来的较晚,还表示不好意思,面试时的感觉自己较长时间没有面试水平下滑,菜得一批,map 中如果是大括号应该加上 return 都不知道,纠结了半天。总体还是根据简历来提问的,所以一个好的项目至关重要,项目不在于多么宏大,而是自己真正独立做了哪些部分,有收获才是最重要的。</p><h2>lucifer 总结</h2><p>由于是秋招,总体面试难度偏低。可能是一面的原因,就连编程和算法题也没有。面试中的大多数问题都是常见的问题,中规中矩。大家查缺补漏即可。之后会给大家带来更多面经。</p><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p>
如何将 github 上的代码一键部署到服务器?
https://segmentfault.com/a/1190000039220623
2021-02-18T10:38:36+08:00
2021-02-18T10:38:36+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
0
<ul><li>在 Github 上看到一些不错的仓库,想要贡献代码怎么办?</li><li>在 Github 上看到一些有用的网站,想部署到自己的服务器怎么办?</li><li>。。。</li></ul><p>我想很多人都碰到过这个问题。</p><ul><li>如果要贡献代码,之前我的做法通常是将代码克隆到本地,然后在本地的编辑器中修改并提交 pr。</li><li>如果想部署到自己的服务器,之前我的做法通常是克隆到本地,然后本地修改一下部署的配置,最后部署到自己的服务器或者第三方的云服务器(比如 Github Pages)。</li></ul><p>而现在随着云技术的普及,我们<strong>没有必要将代码克隆到本地进行操作,而是直接在云端编辑器中完成修改,开发,并直接部署到云服务器</strong>。今天就给大家推荐一个工具,一键将代码部署到云服务器。</p><h2>什么是一键部署?</h2><p>今天给大家介绍的就是一键部署。那什么是一键部署呢?顾名思义,就是有一个按钮,点击一下就能完成部署工作。</p><p>如下是一个拥有一键部署按钮的项目:</p><p><img src="/img/remote/1460000039220625" alt="" title=""></p><p>点击之后进入如下页面,你可以对一些默认配置进行修改(也可以直接使用默认配置):</p><p><img src="/img/remote/1460000039220626" alt="" title=""></p><p>修改后点击<strong>Deploy app</strong> 即可。部署成功之后就可以通过类似如下的地址访问啦~</p><p><img src="/img/remote/1460000039220628" alt="" title=""></p><blockquote>图中演示地址是:<a href="https://link.segmentfault.com/?enc=OLV%2BUnrS9nap21Lm%2FdxCFA%3D%3D.DAcve618A4vXqlwFHwFnFIDXAq%2Fy7FpjX%2BJlnZmCKMB0vqvcODwbm3I9pEAqL4K6" rel="nofollow">https://leetcode-cheat.heroku...</a></blockquote><p>大家可以直接进我的仓库 <a href="https://link.segmentfault.com/?enc=G1h%2FAXwDvqozOV%2F5GhNHMg%3D%3D.dhE%2FaZkesu1PekrpF%2BAm3IuUoYDvElO6fzDCB%2Fg2B5SmRM98HDKlJMu%2BNxe7ADD1" rel="nofollow">https://github.com/leetcode-p...</a>,点击部署按钮试试哦。</p><h2>它是如何实现的呢?</h2><p>我是一个喜欢探究事物原理的人,当然对它们的原理了如指掌才行。其实它的原理很容易,我们从头开始说。</p><h3>1. 如何在 Github 中显示发布按钮。</h3><p>上面的部署按钮就是如下的一个 Markdown 内容渲染的:</p><pre><code class="md">[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)</code></pre><p>上面内容会被渲染成如下的 DOM:</p><pre><code class="html"><a href="https://heroku.com/deploy" rel="nofollow"
><img
src="https://camo.githubusercontent.com/6979881d5a96b7b18a057083bb8aeb87ba35fc279452e29034c1e1c49ade0636/68747470733a2f2f7777772e6865726f6b7563646e2e636f6d2f6465706c6f792f627574746f6e2e737667"
alt="Deploy"
data-canonical-src="https://www.herokucdn.com/deploy/button.svg"
style="max-width:100%;"
/></a></code></pre><p>也就是说其实就是一个被 a 标签包裹的 svg 图片,点击之后会完成 url 跳转。</p><h3>2. 云服务厂商如何获取默认配置?</h3><p>这里以 heroku 为例,其他厂商(比如腾讯)原理都差不多。</p><p>由于上面的原因,实际上我们传递给第三方云厂商的方式只可能是 url。因此我们可以直接将配置通过 ur 的方式传输。比如 <a href="https://link.segmentfault.com/?enc=I%2F3S%2B%2BNixpHMq1mCqMSRrQ%3D%3D.JRV3ESKNBNrlhpmKv2HMDXv3nNryvWrSf9XlnhfGNJUgRZv117QB4JhKWO2beKdM" rel="nofollow">https://heroku.com/deploy?a=1...</a> 。 这种方式对于少量数据是足够的,那如何数据量很大呢?我们知道浏览器 url 的长度是有限的,而且不同的浏览器限制也不尽相同。</p><p>那怎么解决呢?现在比较流行的思路是<strong>约定</strong>。以 heroku 来说,就约定根目录的 <code>app.json</code> 文件中存配置,这种约定的方式我个人强烈推荐。</p><p>比如我的仓库的 app.json 就是:</p><pre><code class="json">{
"name": "LeetCode Cheatsheet",
"description": "力扣加加,或许是西湖区最好的算法题解",
"repository": "https://github.com/leetcode-pp/leetcode-cheat",
"logo": "https://tva1.sinaimg.cn/large/008eGmZEly1gnm68epc0kj30u00tsaav.jpg",
"keywords": ["github", "leetcode", "cheatsheet", "91algo", "algorithm"],
"env": {
"REACT_APP_BUILD_TARGET": {
"description": "枚举值:extension 和 web",
"value": null
},
"PUBLIC_URL": {
"description": "静态资源存放位置(可使用 cdn 加速)",
"value": "https://cdn.jsdelivr.net/gh/leetcode-pp/leetcode-cheat@gh-pages/"
}
},
"buildpacks": [
{
"url": "https://buildpack-registry.s3.amazonaws.com/buildpacks/mars/create-react-app.tgz"
}
]
}</code></pre><p>可以看出,除了配置仓库,logo,描述这些常规信息,我还配置了环境变量和 buidpacks。buildpacks 简单来说就是构建应用的方式, 关于 buildpacks 的更多信息可以参考 <a href="https://link.segmentfault.com/?enc=33g%2F6GKMCUonclGR8qRhTg%3D%3D.Pyjrc5%2BdsXRCBjapZTD7N4I8Go5b5rFsso1O4qZohgfRslPoavl6LMwMtQiRjef3yx%2FTGX1djenneaXgbFwz1w%3D%3D" rel="nofollow">heroku 官方文档</a></p><p>大家可能还有疑问,为啥上面的链接是 <a href="https://link.segmentfault.com/?enc=e%2B5FTAd0R3LkBeknx6UKlw%3D%3D.KGI8Ox8ixfuIJMwAvbW7fjrK7HqW7fcnpUjc5ykbi8A%3D" rel="nofollow">https://heroku.com/deploy</a>。可以看出 url 中也没有任何参数信息,那为什么它就知道从哪来的呢?我觉得 ta 应该利用的是浏览器的 referer,用它可以判断从哪里过来的,进而搜索对应项目根目录的 app.json 文件。你可以通过右键在新的<strong>无痕模式</strong>中打开来验证。你会发现右键在新的无痕模式中打开是无法正常部署的。</p><h2>这有什么用呢?</h2><p>一键部署意味着部署的门槛更低,不仅是技巧上的,而且是成本上的。比如 heroku 就允许你直接免费一键部署若干个应用,直接生成网站,域名可以直接访问。如果你觉得域名不喜欢也可以自定义。如果你想修改源码重新构建也是可以的。</p><p>比如我看到别人的博客很漂亮。如果 ta 提供了一键部署,那么就可以直接部署到自己的云服务器,生成自己的 url。关联自己的 git 之后,推送还能自动部署(CD)。而且这一切都可以是免费的,至少我现在用的是免费的。 而如果 ta 没有提供一键部署,就需要你自己手动完成了。如果你对这些熟悉还好,无非就是多花点时间。而如果你是技术小白,我可能仅仅是想部署一下,用自己的域名访问之类,没有一键部署就很不友好啦。</p><h2>相关技术</h2><p>gitpod 是我一直在用的一个工具,它可以帮助我直接在云端编辑一些内容。或者有一些环境问题,需要虚拟主机的,也可以用它来解决。 它不仅仅提供了在线 IDE 的所有功能,还集成了 CI 和 CD,用起来也是非常方便。</p><p>同样地,你也可以在你的仓库中增加<strong>在 Gitpod</strong> 一键打开的功能。</p><p><img src="/img/remote/1460000039220627" alt="" title=""></p><h2>小技巧</h2><p>一些开源项目你不知道怎么贡献。其实可以另辟蹊径,比如给他们贡献一个 logo,再比如贡献<strong>一键部署</strong>功能。这或许是你迈向开源事业的第一步。</p><h2>更多资料</h2><ul><li><a href="https://link.segmentfault.com/?enc=gIPJJKUQTknS6ElCXJd3lA%3D%3D.raBTbbxG2Gmr9nt7lE5p1VJtKxVzldFGXK67AYX9OlOv48ZdLF%2B%2F5qAGll6KX2spdvGJa9hB7h14Fi%2FDCH00yA%3D%3D" rel="nofollow">heroku-button</a></li><li><a href="https://link.segmentfault.com/?enc=GMzYZ624%2BzjWy4EsU2oLOA%3D%3D.W%2BqMlx81tOY9LvZ280AZJ6vfyoKGKFBG5sBACK34%2Bu%2F%2FHSVjtVwG%2B9s8Eyy%2FLsPp4iEHX%2BtpB4Q9SwL%2F06XJiZqvk3lct8W93buWnFvi2%2BWXTC7suAPjCVqP1RhHkbQI" rel="nofollow">cloudbase 一键部署</a></li></ul><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p>
一个让你的 YouTube 丝滑般柔顺的插件
https://segmentfault.com/a/1190000038991879
2021-01-15T14:28:40+08:00
2021-01-15T14:28:40+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
4
<p>Youtube 和 B 站是很多人浏览视频的两个地方。</p><p>一开始我还挺喜欢 Youtube 的,不仅是因为可以看到一些其他人看不到的东西,也因为体验比较棒。而现在,我已经彻底失望了。每次看一个十分钟的视频被跳出来四次广告的痛苦你了解么?如果你在看一个需要集中精力思考的视频,那么这种打断真的是非常让人恼怒。如果不是实在无法忍受这种行为,我也不会使用一些广告屏蔽的插件,毕竟看广告其实也是对 up 主的另外一种支持。</p><p>另外 YouTube 竟然没有小窗播放。比如我下滑到评论区,视频并不会小窗悬浮播放,差评!</p><p>废话不多说了。今天给大家推荐的这个插件名字叫 Enhancer for YouTube。安装地址:<a href="https://link.segmentfault.com/?enc=7zWTzqFbPJTglyB6pXJeNw%3D%3D.75eBRrKmuOgV4Ers%2F3LGaaEpefHRaCSjEm%2Fei78Rnc33vL05AJNuj2NGSlG8eewPxHXqNICTJa6wcyrRPWrfDdB8hjlV%2FXqBg0%2F9n8RfEtHFWfKIS3L8OW3fY4LY%2FvVi" rel="nofollow">https://chrome.google.com/web...</a></p><p>这个插件有 400,000+用户,超过 9000 人做出了评价,评分是五分(满分五分)。</p><p>它提供了很多功能以及众多的配置项,从外观的配置到功能的配置应有尽有。</p><p><img src="/img/remote/1460000038991881" alt="插件提供的众多配置项" title="插件提供的众多配置项"></p><p>还可以配置主题,我把主题换成了暗黑模式。</p><p><img src="/img/remote/1460000038991882" alt="暗黑模式" title="暗黑模式"></p><p>主要功能有:</p><ul><li>使用鼠标滚轮控制音量和播放速度</li><li>视频去广告(自动或手动)</li><li>频道白名单(不自动去广告)</li><li>屏蔽注解(自动或手动)</li><li>自动切换视频清晰度(可以设定为 4K、HD 或任意清晰度)</li><li>循环播放视频(完整循环或部分循环)</li><li>使用自定义主题样式</li><li>自动拉伸播放器</li><li>查看评论时将播放器固定在右下角</li></ul><p><img src="/img/remote/1460000038991883" alt="" title=""></p><ul><li>执行自定义脚本</li><li>。。。</li></ul><p>更多功能等待大家的探索。</p>
【每日一荐月刊】2020-12
https://segmentfault.com/a/1190000038855080
2021-01-06T18:32:49+08:00
2021-01-06T18:32:49+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
2
<p><img src="/img/remote/1460000038855084" alt="" title=""></p><p>每天给你推荐一个新奇,好玩,高品质的开源库,好文,观点或言论等。</p><blockquote>项目主页维护当前月份的内容,想看往期内容,可以翻到下方历史汇总部分,然后选择自己感兴趣的月份点进去即可。</blockquote><p>在线阅读:<a href="https://link.segmentfault.com/?enc=t8Vhc4NXe2aVDTJOGcXjaQ%3D%3D.L9aK6apGY7zL0RBSmIOl0tGA1Tm7wIPFIAst8WY8WF4gQtw69cMtQ40Q6aKNHhNsIBS8fBsnpZhgIaujr0Fy7Q%3D%3D" rel="nofollow">https://leetcode-solution-lee...</a></p><p>电子书文件太大, 大家可到我的公众号《脑洞前端》回复“每日一荐”获取。</p><h2>新鲜出炉 (2020-12)</h2><h3>2020-12-31[好文]</h3><p>啊哈!今天是我的生日 🎂</p><p><strong>管道和重定向</strong>是 shell 中两个非常重要的概念,搞不懂它那么就无法精通 shell,很多高阶操作都依赖于 ta。</p><p>这篇文章讲解地非常细致,且例子很丰富,排版也很棒,让你有学习的欲望。</p><p><a href="https://link.segmentfault.com/?enc=jAohSu%2FtZD91iEIaIGqFOQ%3D%3D.OTRNo9JDJ1vNAu79SEisN9XX2B5mAzYS01xT6ArCtNg%2BgUMVp6d2ZtWLxyUGA7AsSdNo3nfo2QJqr2LCk9NpVA%3D%3D" rel="nofollow">https://ryanstutorials.net/li...</a></p><h3>2020-12-30[教程]</h3><p>《The Missing Semester of Your CS Education》,中文是《计算机教育中缺失的一课》。其实之前就已经听说过了,也看了看。内容比较少,但是却都很实用。刚看到了中文翻译,就立马推荐给大家了。</p><blockquote>翻译的质量不高,我读了几章,发现了很多错别字,并提交了 pr。</blockquote><p>其中部分章节讲的非常精简,但是讲解角度很奇妙,比市面上大多数的文章要好,比如 <a href="https://link.segmentfault.com/?enc=k0NaosbPd5Y0SlaDAqCAzA%3D%3D.mOPn8jB7s2vVEleKdlJsCCIbnh%2Fv8rTiQE6rjH09E%2BbkcxjGEnghV7IOXvOiQLGn6w7qL4ziw1rtEMX5voSQpA%3D%3D" rel="nofollow">版本控制(Git)</a>") 这一节。</p><ul><li>英文版:<a href="https://link.segmentfault.com/?enc=9Lq3L38GJRb14LLhbi21Zg%3D%3D.baTvidzMq49kTkWFaHvPqNcKo%2FpRKpWj0r43M4U1PS4%3D" rel="nofollow">https://missing.csail.mit.edu/</a></li><li>中文版: <a href="https://link.segmentfault.com/?enc=INytxxwgP50xLzlh0zl2YA%3D%3D.wGf6K%2BuPtLcqyXfB%2FffTOvXiwBRT2EILRefewGdnkFQEFS0mxXjkUJ96ldX5C895" rel="nofollow">https://missing-semester-cn.g...</a></li></ul><h3>2020-12-28[好文]</h3><p>到年底了,很多平台都开始了一年一度的年度总结。Github 在这一年都做了什么大事呢?这篇文章告诉你~</p><p><a href="https://link.segmentfault.com/?enc=z3UnSnRr2AtVt7hLEcRTEA%3D%3D.glcAywQ7ulFJrP6rh47ez5%2FbIIJ5pnra5Y3csCeTY7Ct6B2CAguT2OPQyTSt8QrP" rel="nofollow">https://www.toutiao.com/i6909...</a></p><h3>2020-12-25[好文]</h3><p>圣诞节快乐 🎄</p><p>bash 大家可能在工作中都用过。不过很多人写的 bash 都比较粗糙,没有错误处理,打印不友好,没有帮助文档等等。</p><p>这其中的主要原因在于 bash 比较难,还有一个原因是 bash 不是我们工作的主要内容。但其实用好 bash 能很大提高工作的质量和效率,并且 bash 中的很多知识(比如管道)也可以迁移到其他地方。</p><p>而这篇稳重不是教你 bash 的,而是告诉你一个合格的 bash 脚本应该有什么,并直接给出了模板,大家之后直接“套”即可。</p><p>文章地址:<a href="https://link.segmentfault.com/?enc=4D73cql7CCz%2BA0D%2BEuv1UQ%3D%3D.C1%2B35xkD97AYm4mgTQqC%2B5MGmoPBikZdpmf5ccPaY3R%2BkfuqfoRcLQVAFa4MV9p3xKancqCo9ZinMD3ZgevnAw%3D%3D" rel="nofollow">https://betterdev.blog/minima...</a></p><p>另外我将其中的模板提取到了 gist,大家可以直接访问 <a href="https://gist.github.com/azl397985856/40cd0d80f5cb3e011ebc62bf98246f77">https://gist.github.com/azl39...</a>。</p><h3>2020-12-23[仓库]</h3><p>fuse.js 是一个前端模糊搜索的库,帮助你在不依赖后端的情况下实现模糊搜索。</p><p><a href="https://link.segmentfault.com/?enc=8UugMkn1BzlkM3x6rhflwg%3D%3D.uSkjReqtmfEfw4YeJWOeZPjJgaEJPNkYQLjF8RONIQw%3D" rel="nofollow">https://github.com/krisk/fuse</a></p><h3>2020-12-22[工具]</h3><p>可视化递归过程的网站。</p><p><img src="/img/remote/1460000038855085" alt="" title=""></p><p><a href="https://link.segmentfault.com/?enc=HtWqobkWjhTFxd6n1qOONA%3D%3D.i%2Bn53%2BxmnFGh2Jt%2FZyxlEJSsBlGYEhHWzS7Fzla4m5Q%3D" rel="nofollow">https://recursion.now.sh/</a></p><h3>2020-12-18[工具]</h3><p>pycallgraph 是一个可以可视化代码执行过程的 Python 工具,底层基于大名鼎鼎的图形库 graphviz 实现。</p><p>使用效果:</p><p><img src="/img/remote/1460000038855089" alt="" title=""></p><p>一个更复杂的例子:</p><p><img src="/img/remote/1460000038855090" alt="" title=""></p><p><a href="https://link.segmentfault.com/?enc=PDINjNPPPRMwR3EBv%2FBUvw%3D%3D.MVhsj0Fppbh13HdTgXL0TeUFdDRlrZGspKTnPegCVu0whNsQQUGRuQv8hjVAJegJ6PMWj6tVBruGPCtg2lktgQ%3D%3D" rel="nofollow">http://pycallgraph.slowchop.c...</a></p><h3>2020-12-17[工具]</h3><p>大家可能用过 top 和 iotop 查看机器的负载情况。而 htop 是一个功能更强大的替代工具,UI 更美观,且支持功能更多,用户自定义的选项有很多。</p><p>mac 用户可直接使用 brew 安装:</p><pre><code class="bash">brew install htop</code></pre><p>使用效果:</p><p><img src="/img/remote/1460000038855083" alt="" title=""></p><p>地址:<a href="https://link.segmentfault.com/?enc=NR4kq5EJkKywNIsEY44v7w%3D%3D.xHGrOoY11IJ5NjW3B7%2FmH35fTs%2B1RHQC9sQoWVJmM6s%3D" rel="nofollow">https://htop.dev/</a></p><h3>2020-12-16[工具]</h3><p>你可能对自己写的代码进行过基准测试,那你对命令行程序进行过基准测试么?hyperfine 就是一个对命令行进行基准测试的工具。</p><p>使用方式非常简单, 最简单的用法是直接 hyperfine + command 即可 比如:</p><pre><code class="bash">hyperfine 'sleep 0.3'</code></pre><p>ta 还支持很多选项,可以说是一个合格的基准测试工具了。</p><p>地址:<a href="https://link.segmentfault.com/?enc=PkILKxx5NdXQDdjJpgWsYw%3D%3D.SZ1ZYrrnhrYAOqIMQOw0l6Qy86AMW59SS%2FLwRzIQ3U8FpeRQmN7zXp2xH%2FOKyzxb" rel="nofollow">https://github.com/sharkdp/hy...</a></p><h3>2020-12-15[杂谈]</h3><p>Python 是一门动态语言,但是它却同 Java,C++等一样是强类型的,这体现在其不会发生隐式类型转换。而弱类型语言,比如 JS 和 PHP 则会发生隐式类型转换。</p><p>Python 可以选择性地给变量加类型签名。在 Python 3.x 之前,只能使用注释的方式增加类型注解:</p><pre><code class="py">def is_palindrome(s):
# type: (str) -> bool
return s == s[::-1]</code></pre><p>在 Python 3.x 中,我们可以使用如下的方式添加类型定义:</p><pre><code class="py">from typing import Iterator
def fib(n: int) -> Iterator[int]:
a, b = 0, 1
while a < n:
yield a
a, b = b, a + b</code></pre><p>这样你就可以对代码进行静态检查,并且其也不会对代码运行时产生影响,你如果不需要检查了,也可以选择性去除,这在重构遗留系统是一样的。如果你懂 JS 和 TS,那么一定能够感同身受。更详细的介绍大家可以参考官方文档: <a href="https://link.segmentfault.com/?enc=XhvMajfkY6SlqM8a0o7Emg%3D%3D.5XwZrZvhancQEny3hdvNLKIokWIyFNnV%2BrPUKIoUiGM%3D" rel="nofollow">http://www.mypy-lang.org/</a></p><p>除了静态类型分析,有时候我们也需要对代码风格和语法逻辑等进行检查,这在大型项目中尤为重要,尤其是一堆不太熟悉 Python 的人,比如从 Java 转来的开发。</p><p>如果大家需要对<strong>代码风格</strong>进行校验,那么推荐使用 Pylint,默认是 PEP-8 风格。</p><p>如果大家需要对<strong>语法或者逻辑</strong>进行校验,那么推荐使用 PyFlakes,其并不会检查上面的代码风格问题。</p><blockquote>PEP-8 是 Python 的官方编码风格指导</blockquote><h3>2020-12-14[视频教程]</h3><p><strong>网络,操作系统,数据结构与算法</strong>都是程序员的内功,学好这些内功可使你的学习效率大增,之后学习其他知识就会得心应手。相信我,各位程序员一定先打好基础,练好内功,不可着急去学语言框架这种东西。</p><p>这是一个北京大学的操作系统原理课,一共 82 集,每集大概十几分钟的样子。如果正想要学习操作系统,并且偏好视频的方式学习,那么不妨试试这个。</p><p><a href="https://www.bilibili.com/video/BV1Gx411Q7ro?p=1">https://www.bilibili.com/vide...</a></p><h3>2020-12-11[工具]</h3><p>一个可以帮助你可视化递归过程的工具,我在<a href="https://link.segmentfault.com/?enc=BtjGwivPXmyYamH0%2BL7e6w%3D%3D.L0uZ%2BY1v6KbHb27CCM%2BDNIf3iV7Oli4Kph%2FsW8F5l8iRnOqgE9M2azZQE3ochiO7" rel="nofollow">几乎刷完了力扣所有的树题,我发现了这些东西。。。</a>中提到过 ta。</p><p><img src="/img/remote/1460000038855088" alt="递归计算 fib 的过程图" title="递归计算 fib 的过程图"></p><p><a href="https://link.segmentfault.com/?enc=qMyx%2Bd9ISwv%2FygCQTv8eFQ%3D%3D.lYFoCBdmxAPMHe5pui9xqnmfTTSFPTHC5e5aC3u1Xq8%3D" rel="nofollow">https://recursion.now.sh/</a></p><h3>2020-12-10[工具]</h3><p>很多人问我使用的画图工具是什么。这里就介绍一个我经常使用的一个画图工具 - <strong>excalidraw</strong>。它可以快速画出一些基本图形,并且支持手写风(目前仅支持英文手写风,中文是不行的)。</p><p><img src="/img/remote/1460000038855087" alt="" title=""></p><blockquote>不过这对于写题解的我来说还是不够方便,于是我有了自己做一个画图工具的打算,主要功能就是根据数据结构直接生成图片,省去了手绘的过程。</blockquote><p>地址:<a href="https://link.segmentfault.com/?enc=y9gmFEeeHt3RwRNTcjECWA%3D%3D.ihAmqQLxsEIDGuXW3ZOtKjS%2F0zN3TndRNxIWKC8s0WM%3D" rel="nofollow">https://excalidraw.com/</a></p><h3>2020-12-09[工具]</h3><p>我写的一个刷题插件,帮助你高效刷题,不仅内置各种常见算法模板,时间复杂度估算表,学习路线,题解等学习资源。更重要的是,提供了<strong>一键复制所有用例</strong>,<strong>禅定模式</strong>等实用功能以增强刷 leetcode 的体验。欢迎多题意见哦~</p><p><a href="https://link.segmentfault.com/?enc=aNJWFCOyNePj%2F9Pagkvtrw%3D%3D.BNwuRvEd5agUaFDX4%2B3ldaXu6wUG1zgY0XqYE%2F2Pw1QSmgcYiewLOuWWGjH2CoNWxr3Jt7XWgd3lwtoFL2ooRqxpLTKwKcVheFvxUR3%2FMyXtHsrlOEmshGT5OS2njloh5uMcoM06RO2VnHhgFG8maWqITPgdCDWd0756bw3UCCA%3D" rel="nofollow">https://chrome.google.com/web...</a></p><blockquote>打不开的朋友可使用离线版,离线版可关注我的公众号《力扣加加》回复插件获取。 不过还是建议有条件的使用在线版,毕竟咱的更新还是蛮快的。</blockquote><h3>2020-12-08[cheatsheet]</h3><p>如果你使用 markdown 写东西,需要用到数学公式等特殊符号,那么掌握 latex 就很有必要了。</p><p>这里有一份 latex 语法手册,需要用的时候看一下,多用几次就熟练了。</p><p><a href="https://link.segmentfault.com/?enc=ukKaeipqhhMvu1cmomnocQ%3D%3D.9bEnUwuclGhSnUq2RBGsDzHKB5v4FZgaAA1m52p5uAXosezBInveu5pUHYa78iyuUckcLOJNP%2BNAryN6r0nnhESCh6s7nHdUC3fjDCWjHZryQ2mg%2FC8abccDouSkfN5iD%2B8ZiVNdpbJl%2FF1zdkY7Ey0BtPumkI8NFf5kbsoZC2QpW9qStrvguYyBkf0jXFY6" rel="nofollow">https://assets.ctfassets.net/...</a></p><h3>2020-12-07[好文]</h3><p>ShellCheck 是一个帮助你检查 shell 错误的工具,本质就是一个 lint 工具,和 eslint 等工具的功能是类似的。</p><p><img src="/img/remote/1460000038855086" alt="" title=""></p><p><a href="https://link.segmentfault.com/?enc=gFqVWRskVQtvOI6hbRGdDA%3D%3D.KRZzLeADhOIQXOqOY%2BlYzmPovQvfkNjjlR022uuuqOqY3uzE7Bufw92%2BlHbBxHev" rel="nofollow">https://github.com/koalaman/s...</a></p><h3>2020-12-04[好文]</h3><p>NPM 7 发布了,新增重要功能 <strong>workspace</strong>,这不是一个无感升级,需要用户手动配合。这项功能可以在你的多个项目有很多公共依赖的时候发挥重大作用,使得你可以不必安装重复依赖。</p><p>详情:<a href="https://link.segmentfault.com/?enc=8hI%2F7vj1QiFNmcNFLYApOQ%3D%3D.aCoboajDe3U%2FbVNF8BZ4VqomtucsGb%2Flf1LvA8oqeWtu%2Bsq%2BYb%2F3AuAiTTZNOmkeUdAAjc7cZFyxlF4idB7WBKzHoS50thvRJZcAeM49DCk%3D" rel="nofollow">https://blog.bitsrc.io/npm-7-...</a></p><h2>历史汇总</h2><ul><li><a href="https://link.segmentfault.com/?enc=TIGr4fXpgKaJCSd1m0A5mA%3D%3D.tG1HbkHSKXjyUJFFR%2Fo1hDYEGqfM32XDT6Jg6DDJBMyGN915O%2BExafwBtA1id8U9ImMvAiBx8j1qhWi4cw9YPyuVmRuy4fRd2Nrw6BHegAx19wdpZ6NsDHKOmKfhewRE" rel="nofollow">2020-11</a></li><li><a href="https://link.segmentfault.com/?enc=Z91WjcVKS%2Bk433g85A1CQA%3D%3D.yIkq2qDKYYFgBzPdOYB8NbbtJ%2B%2B7x8oKokvMlJtiA0LiV5ysvrFpn1fVqanew9uPRSu%2Ft%2BBNcetXrWE2WDALCQZrADbt8Chwy1wlM%2FU53311CAGl8WwnRt%2BhcVtt3vet" rel="nofollow">2020-10</a></li><li><a href="https://link.segmentfault.com/?enc=hJ3MxJn0Io7OqsM86emRVw%3D%3D.T%2Frym0e2S4s5JIVhOnhsZR6uQQp3uKKRdmDOAmSHwox1y14lSIl%2BRBJFJsJeA3kUQweHPVAkEW5Qz1WTIAoVkdkXcCp%2BPPDmsEIjEgWIZCLA86m4uCsWFKfRi0GKLIDP" rel="nofollow">2020-09</a></li><li><a href="https://link.segmentfault.com/?enc=dz1vuZe2Pp5B9ACwHKoOgQ%3D%3D.CFB2RjeRSqlXd6cD4ohE7w4QR4Hp%2B1%2FzVLsjpkYxW0k5%2FgMkgfyUXB5%2B8JSpwbvMjGZId9YubLleq1ggx3yJddJecUiCsdLrCWxOGb18C%2BlZI0BYfjl5TnC0mylKycGa" rel="nofollow">2020-08</a></li><li><a href="https://link.segmentfault.com/?enc=5yt2f4PQivV1jD0TZ4nBOQ%3D%3D.DoImBNMt86Xvj4nFwkGXUzZoHYXJ3quyx8uUr1waMPpq7Xjune5yLTlhg1wsS5T9PYFE5JwGK%2FgHIsM6dBAZkZNUEw4M%2Bx4Dylb7Ms5eI1LbdDefW6um3tSY0fceJCHf" rel="nofollow">2020-05</a></li><li><a href="https://link.segmentfault.com/?enc=9v6nxoyqRMjDPwNNTTxRfQ%3D%3D.W7h5Ukv%2B0wwbgqQWqJadH8j2iML%2BhS1hiSJRQB0Ak2PZt5ZaH6bq3iRNyJkKts15xMO1Vuua8lIg5%2BRow%2FiXbeer0WxzZPZSYFA67g8ywkyzOBlj9miDeQseB6Esxuou" rel="nofollow">2020-04</a></li><li><a href="https://link.segmentfault.com/?enc=6bzAhwB7PtI39FwLbqNdmA%3D%3D.lNmY81ppZp95Et9OrCqwy4vKBmmTi1mGXqQEAI59Bazn9qOZkYuKM24f7uQZ%2FTmTvSrL5q8kDjZ0wFIdLTk40d%2FZD0T4t8ahL9SCLfM3F1%2Byk9Y5gyEKmg3vD2cPGU0L" rel="nofollow">2020-03</a></li><li><a href="https://link.segmentfault.com/?enc=kMxdd63er%2BknBohV2fsaZQ%3D%3D.T63MuMYkeG1LFZo11p56UsoEYon8S0p3vH1XnvK7K4hqowajV2QcFKGOxzd7PyiDPD%2BXYsjcp9hgnfEY1NmgD2TuKJtzWTogn9BjKcsH5SuqygvZQACpIKWSSVD3DgbZ" rel="nofollow">2020-02</a></li><li><a href="https://link.segmentfault.com/?enc=2upLf3j%2Bjoi9H7UiGdqwHA%3D%3D.8ciOri0oVxzgRVC1qPvrBOdI2HJw3HRgP9bom40dHLuUgwq%2F9jIexUEgh1jZOsS5mcrM6R7CYz9%2Fh71TKZZwT2UDon4wfDSUl538vMUHdwra3u3TJQiOj3Ml2a4%2FdH7i" rel="nofollow">2020-01</a></li><li><a href="https://link.segmentfault.com/?enc=PUi2f5tdzVZ99cnqIHY6VA%3D%3D.odtabGiYmoHdgKzqwNjDI5EbnER37cc07JzcdrBvJ8hbGEP8OqDMSQwQlTo4%2BJ0mDttWcj1avby0kwZwFb7W5N4CP79780sAwbO4alhouXA8V6lZSwryPhQk5REM%2FpPV" rel="nofollow">2019-12</a></li><li><a href="https://link.segmentfault.com/?enc=L%2BNTdzronMwCflgEZmYGIg%3D%3D.XWNq7ke%2Bz9GF01pj%2BFKixEdBz2yxGbztncqMIB%2F7cooD49tg52zHrwx%2BnG8IGxRSf0C5VSNRA4lpzcJZztqMqgGSvtxX3CTe8pkRO1sP75WPBycq8MPa9zAP5tZIhXxN" rel="nofollow">2019-11</a></li><li><a href="https://link.segmentfault.com/?enc=5u0v2oVxOVDPym9bK70WXA%3D%3D.Jtg5dJXPjaDLjTHLAdnW2F9%2BndCMjcG%2B%2BX2NE7O1FKlXUg0PfBsTDCaX1h0LdxfQ6DQd8P%2Fjvjpj%2FG0j%2FI17FnS8JDYMDTA2nvKGIgpUQjr7oXWbK0alQzgN5fnaKmIo" rel="nofollow">2019-10</a></li><li><a href="https://link.segmentfault.com/?enc=ISfUFlj%2FTgG7C8BTzqS7Wg%3D%3D.Hsc49scVKElJSIWOnIkXdY8LlszQKmx0Euj5jQw0B7OOWeHXm%2BNC1wLOiMmm3cuCjyr9IKiZmATMngc4DrFdzd9DVRLNgcli%2FJq6H4ekbKmyNa0xoSBTOzIPqiKrfy1P" rel="nofollow">2019-09</a></li></ul><h2>关注我</h2><p>我重新整理了下自己的公众号,并且我还给它换了一个名字<code>脑洞前端</code>,它是一个帮助你打开大前端新世界大门的钥匙 🔑,在这里你可以听到新奇的观点,看到一些技术尝新,还会收到系统性总结和思考。</p><p>在这里我会尽量通过图的形式来阐述一些概念和逻辑,帮助大家快速理解,图解是我的目标。</p><p>之后我的文章会同步到微信公众号 <code>脑洞前端</code> ,你可以关注获取最新的文章,并和我进行交流。</p><p>另外你可以回复大前端进大前端微信交流群, 回复 leetcode 拉你进 leetcode 微信群,如果想加入 qq 群,请回复 qq。</p><p><img width="300" src="https://tva1.sinaimg.cn/large/006y8mN6ly1g7he9xdtmyj30by0byaac.jpg"></p>
TypeScript 练习题(第二弹)
https://segmentfault.com/a/1190000037521679
2020-10-19T11:34:57+08:00
2020-10-19T11:34:57+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
11
<p>TypeScript 的学习资料非常多,其中也不乏很多优秀的文章和教程。但是目前为止没有一个我特别满意的。原因有:</p><ul><li>它们大多数没有一个清晰的主线,而是按照 API 组织章节的,内容在<strong>逻辑上</strong>比较零散。</li><li>大多是“讲是什么,怎么用“,而不是”讲为什么,讲原理“。</li><li>大多数内容比较枯燥,趣味性比较低。都是干巴巴的文字,没有图片,缺乏能够引起强烈共鸣的例子。</li></ul><p>因此我的想法是做一套不同市面上大多数的 TypeScript 学习教程。以人类认知的角度思考问题,学习 TypeScript,通过通俗易懂的例子和图片来帮助大家建立 TypeScript 世界观。</p><p>系列安排:</p><ul><li><a href="https://link.segmentfault.com/?enc=1kFrqYZqJhNF8wjHGSrpZA%3D%3D.r7dfAk7ibMZGXkqMjl6ZH8Nq5vWCiSLqgtT%2FCb2Ba0r6NUvD44FVGUnnoUgWH%2BshwYLPlX2l6lBrUnTyzheoNA%3D%3D" rel="nofollow">上帝视角看 TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=y4QUUX10TDEYc%2Bj5DUK30g%3D%3D.Y9AAFqIAcL0AMVMkBD06GFJosYQUQKoo6mcsO%2Fy223AB1DU3cEh7pIqkfZ4oKaaYYn7iDho235TaDPFgvuPB4g%3D%3D" rel="nofollow">TypeScript 类型系统</a></li><li><a href="https://link.segmentfault.com/?enc=o0iNPqCUX3x8z4ql9UMYsA%3D%3D.%2FhgJdd9jbVVTIpC%2BaaaSQpsz%2FDgsW%2BCPquHz3bC2Djnxy620YqRQxaa3GoxyzszH" rel="nofollow">types 和 @types 是什么?</a></li><li><a href="https://link.segmentfault.com/?enc=HeH2nkrzDAVCmt4PT6hvCA%3D%3D.qJYoRsefdNa%2FOW4FT5BWCM8jiCaFazH5FlXhPp1IK5U8frUJHDOpfMsQbUMJiQFwYTGlWzT26i%2BEz49oUNsPnA%3D%3D" rel="nofollow">你不知道的 TypeScript 泛型(万字长文,建议收藏)</a></li><li><a href="https://link.segmentfault.com/?enc=wo4ppD459Q9QGzLJPbGYBA%3D%3D.PyuDMRweBD1tVpjnrNvVHds7G2VszimQJHqa4uVlhfqd6EbcqcWa%2Bt4jVzqg3GsD" rel="nofollow">TypeScript 配置文件该怎么写?</a></li><li>TypeScript 是如何与 React,Vue,Webpack 集成的?</li><li><a href="https://link.segmentfault.com/?enc=6N%2FR7DvqmswtoBZRYrEfAA%3D%3D.wVXEFawGqaq12i2biAunTKason6RPhcK2k9ZynUGS%2FPb2b0ZAV456NKoWWkePTEkCf787MHUusW0gDCWSYu8AA%3D%3D" rel="nofollow">TypeScript 练习题(第一弹)</a></li></ul><blockquote>目录将来可能会有所调整。</blockquote><p>注意,我的系列文章基本不会讲 API,因此需要你有一定的 TypeScript 使用基础,推荐两个学习资料。</p><ul><li><a href="https://link.segmentfault.com/?enc=H%2BSWvcIw3TJchGgmxpa2Vw%3D%3D.tTIUVYVZppxfIo7LcYwqRsc4KZa%2Bb4gtu7zmUe0e2nrsMETKblMLl7hVbKYcoWf%2BF2b%2B87jtm5%2BvqH1vkBQ7og%3D%3D" rel="nofollow">深入理解 TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=sSCLe7x6iJvklW4m%2BvKNDQ%3D%3D.IAB0CxyPcDdhCv8olMSvI80Xj9%2F%2FPHqKsF3%2Bs3khaQpqJaHpSUyFy6Z9Viva5%2FTI" rel="nofollow">TypeScript 官方文档</a></li></ul><p>结合这两个资料和我的系列教程,掌握 TypeScript 指日可待。</p><p>接下来,我们通过几个方面来从宏观的角度来看一下 TypeScript。</p><p><!-- more --></p><h2>前言</h2><p>本文涉及的题目一共十六道,全部都可以在 <a href="https://link.segmentfault.com/?enc=Eu25XnrAmVL873gP1nYplA%3D%3D.RP4fy7NoBROF7qrGOk9a4dAwBlWcC6OsP4RIPVLeT1ux9S2MPe%2BP6yTEG4m3kX%2Fg" rel="nofollow">typescript-exercises</a> 上在线提交。</p><p><img src="/img/remote/1460000025157676" alt="" title=""></p><p>可以和标准答案进行对比。<br><img src="/img/remote/1460000025157675" alt="" title=""></p><p>并且由于使用了浏览器缓存, 因此无需登录的情况下也可以保证关掉页面,你的答题进度也会保留。</p><blockquote>想重置进度,清空缓存,无痕模式或者换浏览器都可以。</blockquote><p>题目中涉及到的知识点我基本也都在之前的文章中提到了,如果你没有看过,强烈建议先完成前面的教程,然后将上面的题目自己做一遍之后再看本文。另外一定要按照顺序读, 因此前面的题目都是后面的铺垫。</p><p>为了不让文章太过于冗长, 本篇文章分两次发布, 一次 8 道题,一共十五道。每道题都有思路,前置知识以及代码。 <strong>这次给大家带来的是后 6 道</strong></p><blockquote>其中有一道题需要大家有函数式编程的知识, 如果大家不知道会比较难以解释。 为了避免内容太过分散,将这道题从我的题解中移除,故只有 6 道。</blockquote><h2>题目九</h2><h3>题目描述</h3><pre><code>Intro:
PowerUsers idea was bad. Once those users got
extended permissions, they started bullying others
and we lost a lot of great users.
As a response we spent all the remaining money
on the marketing and got even more users.
We need to start preparing to move everything to a
real database. For now we just do some mocks.
The server API format was decided to be the following:
In case of success: { status: 'success', data: RESPONSE_DATA }
In case of error: { status: 'error', error: ERROR_MESSAGE }
The API engineer started creating types for this API and
quickly figured out that the amount of types needed to be
created is too big.
Exercise:
Remove UsersApiResponse and AdminsApiResponse types
and use generic type ApiResponse in order to specify API
response formats for each of the functions.
</code></pre><p>题目的大概意思是:之前都是写死的数据, 现在数据需要从接口拿,请你定义这个接口的类型。</p><h3>题目内置代码</h3><pre><code class="ts">interface User {
type: "user";
name: string;
age: number;
occupation: string;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
}
type Person = User | Admin;
const admins: Admin[] = [
{ type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
{ type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
];
const users: User[] = [
{
type: "user",
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{ type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
];
export type ApiResponse<T> = unknown;
type AdminsApiResponse =
| {
status: "success";
data: Admin[];
}
| {
status: "error";
error: string;
};
export function requestAdmins(callback: (response: AdminsApiResponse) => void) {
callback({
status: "success",
data: admins,
});
}
type UsersApiResponse =
| {
status: "success";
data: User[];
}
| {
status: "error";
error: string;
};
export function requestUsers(callback: (response: UsersApiResponse) => void) {
callback({
status: "success",
data: users,
});
}
export function requestCurrentServerTime(
callback: (response: unknown) => void
) {
callback({
status: "success",
data: Date.now(),
});
}
export function requestCoffeeMachineQueueLength(
callback: (response: unknown) => void
) {
callback({
status: "error",
error: "Numeric value has exceeded Number.MAX_SAFE_INTEGER.",
});
}
function logPerson(person: Person) {
console.log(
` - ${person.name}, ${person.age}, ${
person.type === "admin" ? person.role : person.occupation
}`
);
}
function startTheApp(callback: (error: Error | null) => void) {
requestAdmins((adminsResponse) => {
console.log("Admins:");
if (adminsResponse.status === "success") {
adminsResponse.data.forEach(logPerson);
} else {
return callback(new Error(adminsResponse.error));
}
console.log();
requestUsers((usersResponse) => {
console.log("Users:");
if (usersResponse.status === "success") {
usersResponse.data.forEach(logPerson);
} else {
return callback(new Error(usersResponse.error));
}
console.log();
requestCurrentServerTime((serverTimeResponse) => {
console.log("Server time:");
if (serverTimeResponse.status === "success") {
console.log(
` ${new Date(serverTimeResponse.data).toLocaleString()}`
);
} else {
return callback(new Error(serverTimeResponse.error));
}
console.log();
requestCoffeeMachineQueueLength((coffeeMachineQueueLengthResponse) => {
console.log("Coffee machine queue length:");
if (coffeeMachineQueueLengthResponse.status === "success") {
console.log(` ${coffeeMachineQueueLengthResponse.data}`);
} else {
return callback(new Error(coffeeMachineQueueLengthResponse.error));
}
callback(null);
});
});
});
});
}
startTheApp((e: Error | null) => {
console.log();
if (e) {
console.log(
`Error: "${e.message}", but it's fine, sometimes errors are inevitable.`
);
} else {
console.log("Success!");
}
});</code></pre><h3>前置知识</h3><ul><li>泛型</li><li>回调函数</li></ul><h3>思路</h3><p>我们还是直接看报错。</p><p><img src="/img/remote/1460000037521683" alt="" title=""></p><p>很明显这个报错的原因是类型是 unknown, 因此我们只有将 unknown 改成正确的类型即可。</p><p>换句话说, 就是把这种地方改成正确类型即可。</p><p><img src="/img/remote/1460000037521682" alt="" title=""></p><p>题目描述说了, 这个 response 其实是从后端返回的。 而后端返回的数据有固定的格式。比如获取用户列表接口:</p><pre><code class="ts">type UsersApiResponse =
| {
status: "success";
data: User[];
}
| {
status: "error";
error: string;
};</code></pre><p>其他接口也是类似, 不同的是 data 的类型。因此我们考虑使用泛型封装,将 data 的类型作为参数即可。</p><p>从本质上来说, 就是从后端取的数据有两种大的可能, 一种是错误, 一种是成功。两者在同一接口同一时刻只会出现一个,且必须出现一个。</p><p>而成功的情况又会随着接口不同从而可能产生不同的类型。</p><p><img src="/img/remote/1460000037521684" alt="" title=""></p><p>这是明显的使用 <strong>或逻辑关系</strong> 和<strong>泛型进行类型定义</strong>的强烈信号。<br>我们可以使用泛型做如下改造:</p><pre><code class="ts">export type ApiResponse<T> =
| {
status: "success";
data: T;
}
| {
status: "error";
error: string;
};</code></pre><p>那么上面的 UsersApiResponse 就可以变成:</p><pre><code class="ts">type UsersApiResponse = ApiResponse<User[]>;</code></pre><p>不懂的同学建议看下我之前的文章:- <a href="https://link.segmentfault.com/?enc=xMAV5y4ID9oeJVM%2BTwroqA%3D%3D.3%2FjJb36eBAPVv7Hqd6n50vzBCI0fZV9L0FXaUIEVlrJVtnBj%2FRQzNtdFZz3fpzRrs0MgXxdy%2BAiJtxQhGja%2FxA%3D%3D" rel="nofollow">你不知道的 TypeScript 泛型(万字长文,建议收藏)</a></p><p>用同样的套路把其他后端返回加上类型即可。</p><h3>代码</h3><p>核心代码:</p><pre><code class="ts">export type ApiResponse<T> =
| {
status: "success";
data: T;
}
| {
status: "error";
error: string;
};
export function requestAdmins(
callback: (response: ApiResponse<Admin[]>) => void
) {
callback({
status: "success",
data: admins,
});
}
export function requestUsers(
callback: (response: ApiResponse<User[]>) => void
) {
callback({
status: "success",
data: users,
});
}
export function requestCurrentServerTime(
callback: (response: ApiResponse<number>) => void
) {
callback({
status: "success",
data: Date.now(),
});
}
export function requestCoffeeMachineQueueLength(
callback: (response: ApiResponse<number>) => void
) {
callback({
status: "error",
error: "Numeric value has exceeded Number.MAX_SAFE_INTEGER.",
});
}</code></pre><h2>题目十</h2><h3>题目描述</h3><pre><code>Intro:
We have asynchronous functions now, advanced technology.
This makes us a tech startup officially now.
But one of the consultants spoiled our dreams about
inevitable future IT leadership.
He said that callback-based asynchronicity is not
popular anymore and everyone should use Promises.
He promised that if we switch to Promises, this would
bring promising results.
Exercise:
We don't want to reimplement all the data-requesting
functions. Let's decorate the old callback-based
functions with the new Promise-compatible result.
The final function should return a Promise which
would resolve with the final data directly
(i.e. users or admins) or would reject with an error
(or type Error).
The function should be named promisify.
Higher difficulty bonus exercise:
Create a function promisifyAll which accepts an object
with functions and returns a new object where each of
the function is promisified.
Rewrite api creation accordingly:
const api = promisifyAll(oldApi);</code></pre><p>题目大意是:前面用的是基于 callback 形式的代码, 他们对代码进行了重构,改造成了 Promise,让你对基于 Promise 的接口进行类型定义。</p><h3>题目内置代码</h3><pre><code class="ts">interface User {
type: "user";
name: string;
age: number;
occupation: string;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
}
type Person = User | Admin;
const admins: Admin[] = [
{ type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
{ type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
];
const users: User[] = [
{
type: "user",
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{ type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
];
export type ApiResponse<T> =
| {
status: "success";
data: T;
}
| {
status: "error";
error: string;
};
export function promisify(arg: unknown): unknown {
return null;
}
const oldApi = {
requestAdmins(callback: (response: ApiResponse<Admin[]>) => void) {
callback({
status: "success",
data: admins,
});
},
requestUsers(callback: (response: ApiResponse<User[]>) => void) {
callback({
status: "success",
data: users,
});
},
requestCurrentServerTime(callback: (response: ApiResponse<number>) => void) {
callback({
status: "success",
data: Date.now(),
});
},
requestCoffeeMachineQueueLength(
callback: (response: ApiResponse<number>) => void
) {
callback({
status: "error",
error: "Numeric value has exceeded Number.MAX_SAFE_INTEGER.",
});
},
};
export const api = {
requestAdmins: promisify(oldApi.requestAdmins),
requestUsers: promisify(oldApi.requestUsers),
requestCurrentServerTime: promisify(oldApi.requestCurrentServerTime),
requestCoffeeMachineQueueLength: promisify(
oldApi.requestCoffeeMachineQueueLength
),
};
function logPerson(person: Person) {
console.log(
` - ${person.name}, ${person.age}, ${
person.type === "admin" ? person.role : person.occupation
}`
);
}
async function startTheApp() {
console.log("Admins:");
(await api.requestAdmins()).forEach(logPerson);
console.log();
console.log("Users:");
(await api.requestUsers()).forEach(logPerson);
console.log();
console.log("Server time:");
console.log(
` ${new Date(await api.requestCurrentServerTime()).toLocaleString()}`
);
console.log();
console.log("Coffee machine queue length:");
console.log(` ${await api.requestCoffeeMachineQueueLength()}`);
}
startTheApp().then(
() => {
console.log("Success!");
},
(e: Error) => {
console.log(
`Error: "${e.message}", but it's fine, sometimes errors are inevitable.`
);
}
);</code></pre><h3>前置知识</h3><ul><li>Promise</li><li>promisify</li><li>泛型</li><li>高阶函数</li></ul><h3>思路</h3><p>题目给了一个 promisefy, 并且类型都是 unknown,不难看出, 它就是想让我们改造 promisefy 使其不报错, 并能正确推导类型。</p><pre><code class="ts">export function promisify(arg: unknown): unknown {
return null;
}</code></pre><p>我们先不考虑这个类型怎么写,先把 promiify 实现一下再说。这需要你有一点高阶函数和 promise 的知识。由于这不是本文的重点,因此不赘述。</p><pre><code class="ts">export function promisify(fn) {
return () =>
new Promise((resolve, reject) => {
fn((response) => {
if (response.status === "success") resolve(response.data);
else reject(response.error);
});
});
}</code></pre><p>接下来,我们需要给其增加类型签名。</p><p>这个 fn 实际上是一个函数,并且又接受一个 callback 作为参数。 因此大概是这个样子:</p><pre><code class="ts">((something) = void) => void</code></pre><p>这里的 something 实际上我们在上一节已经解决了,直接套用即可。代码:</p><pre><code class="ts">(callback: (response: ApiResponse<T>) => void) => void</code></pre><p>整体代码大概是:</p><pre><code class="ts">export function promisify<T>(
fn: (callback: (response: ApiResponse<T>) => void) => void
): () => Promise<T> {
// 上面的实现
}</code></pre><h3>代码</h3><p>核心代码:</p><pre><code class="ts">export function promisify<T>(
fn: (callback: (response: ApiResponse<T>) => void) => void
): () => Promise<T> {
return () =>
new Promise((resolve, reject) => {
fn((response) => {
if (response.status === "success") resolve(response.data);
else reject(response.error);
});
});
}</code></pre><h2>第十一题</h2><h3>题目描述</h3><pre><code>Intro:
In order to engage users in the communication with
each other we have decided to decorate usernames
in various ways. A brief search led us to a library
called "str-utils". Bad thing is that it lacks
TypeScript declarations.
Exercise:
Check str-utils module implementation at:
node_modules/str-utils/index.js
node_modules/str-utils/README.md
Provide type declaration for that module in:
declarations/str-utils/index.d.ts
Try to avoid duplicates of type declarations,
use type aliases.</code></pre><p>题目的意思是他们用到了一个库 <code>str-utils</code>,这个库的人又没给我们写类型定义,于是我们不得不去自己写(好真实的例子啊)。</p><p>其实就是让我们实现以下函数的类型签名:</p><pre><code class="ts">import {
strReverse,
strToLower,
strToUpper,
strRandomize,
strInvertCase,
} from "str-utils";</code></pre><h3>题目内置代码</h3><pre><code class="ts">// declarations/str-utils/index.d.js
declare module "str-utils" {
// export const ...
// export function ...
}
// index.ts
import {
strReverse,
strToLower,
strToUpper,
strRandomize,
strInvertCase,
} from "str-utils";
interface User {
type: "user";
name: string;
age: number;
occupation: string;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
}
type Person = User | Admin;
const admins: Admin[] = [
{ type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
{ type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
{ type: "admin", name: "Steve", age: 40, role: "Steve" },
{ type: "admin", name: "Will Bruces", age: 30, role: "Overseer" },
{ type: "admin", name: "Superwoman", age: 28, role: "Customer support" },
];
const users: User[] = [
{
type: "user",
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{ type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
{ type: "user", name: "Moses", age: 70, occupation: "Desert guide" },
{ type: "user", name: "Superman", age: 28, occupation: "Ordinary person" },
{ type: "user", name: "Inspector Gadget", age: 31, occupation: "Undercover" },
];
const isAdmin = (person: Person): person is Admin => person.type === "admin";
const isUser = (person: Person): person is User => person.type === "user";
export const nameDecorators = [
strReverse,
strToLower,
strToUpper,
strRandomize,
strInvertCase,
];
function logPerson(person: Person) {
let additionalInformation: string = "";
if (isAdmin(person)) {
additionalInformation = person.role;
}
if (isUser(person)) {
additionalInformation = person.occupation;
}
const randomNameDecorator =
nameDecorators[Math.round(Math.random() * (nameDecorators.length - 1))];
const name = randomNameDecorator(person.name);
console.log(` - ${name}, ${person.age}, ${additionalInformation}`);
}
([] as Person[]).concat(users, admins).forEach(logPerson);
// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/modules.html#ambient-modules</code></pre><h3>前置知识</h3><ul><li>如何给缺乏类型定义的第三方库定义类型</li></ul><h3>思路</h3><p>这个题目的考点就是<strong>如何给缺乏类型定义的第三方库定义类型</strong>。</p><p>这个时候我们只要新建一个文件然后加入以下代码即可。</p><pre><code class="ts">declare module "str-utils" {
// 在这里定义类型
// export const ...
// export function ...
}</code></pre><p>其中 str-utils 是那个可恶的没有类型定义的库的名字。</p><p>有了这个知识,我们的代码就简单了。</p><h3>代码</h3><pre><code class="ts">declare module "str-utils" {
// export const ...
// export function ...
export function strReverse(s: string): string;
export function strToLower(s: string): string;
export function strToUpper(s: string): string;
export function strRandomize(s: string): string;
export function strInvertCase(s: string): string;
}</code></pre><h2>第十二题</h2><h3>题目描述</h3><pre><code>Intro:
We have so many users and admins in the database!
CEO's father Jeff says that we are a BigData
startup now. We have no idea what it means, but
Jeff says that we need to do some statistics and
analytics.
We've ran a questionnaire within the team to
figure out what do we know about statistics.
The only person who filled it was our coffee
machine maintainer. The answers were:
* Maximums
* Minumums
* Medians
* Averages
We found a piece of code on stackoverflow and
compiled it into a module `stats`. The bad
thing is that it lacks type declarations.
Exercise:
Check stats module implementation at:
node_modules/stats/index.js
node_modules/stats/README.md
Provide type declaration for that module in:
declarations/stats/index.d.ts
Higher difficulty bonus exercise:
Avoid duplicates of type declarations.</code></pre><p>题目大概意思是又来了一个库,这个库又没有写定义,我们又要自己写。 (真实++)</p><h3>题目内置代码</h3><pre><code class="ts">// declartions/stats/index.d.ts
declare module "stats" {
export function getMaxIndex(input: unknown, comparator: unknown): unknown;
}
// index.ts
import {
getMaxIndex,
getMaxElement,
getMinIndex,
getMinElement,
getMedianIndex,
getMedianElement,
getAverageValue,
} from "stats";
interface User {
type: "user";
name: string;
age: number;
occupation: string;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
}
const admins: Admin[] = [
{ type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
{ type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
{ type: "admin", name: "Steve", age: 40, role: "Steve" },
{ type: "admin", name: "Will Bruces", age: 30, role: "Overseer" },
{ type: "admin", name: "Superwoman", age: 28, role: "Customer support" },
];
const users: User[] = [
{
type: "user",
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{ type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
{ type: "user", name: "Moses", age: 70, occupation: "Desert guide" },
{ type: "user", name: "Superman", age: 28, occupation: "Ordinary person" },
{ type: "user", name: "Inspector Gadget", age: 31, occupation: "Undercover" },
];
function logUser(user: User | null) {
if (!user) {
console.log(" - none");
return;
}
const pos = users.indexOf(user) + 1;
console.log(` - #${pos} User: ${user.name}, ${user.age}, ${user.occupation}`);
}
function logAdmin(admin: Admin | null) {
if (!admin) {
console.log(" - none");
return;
}
const pos = admins.indexOf(admin) + 1;
console.log(` - #${pos} Admin: ${admin.name}, ${admin.age}, ${admin.role}`);
}
const compareUsers = (a: User, b: User) => a.age - b.age;
const compareAdmins = (a: Admin, b: Admin) => a.age - b.age;
const colorizeIndex = (value: number) => String(value + 1);
export {
getMaxIndex,
getMaxElement,
getMinIndex,
getMinElement,
getMedianIndex,
getMedianElement,
getAverageValue,
};
console.log("Youngest user:");
logUser(getMinElement(users, compareUsers));
console.log(
` - was ${colorizeIndex(getMinIndex(users, compareUsers))}th to register`
);
console.log();
console.log("Median user:");
logUser(getMedianElement(users, compareUsers));
console.log(
` - was ${colorizeIndex(getMedianIndex(users, compareUsers))}th to register`
);
console.log();
console.log("Oldest user:");
logUser(getMaxElement(users, compareUsers));
console.log(
` - was ${colorizeIndex(getMaxIndex(users, compareUsers))}th to register`
);
console.log();
console.log("Average user age:");
console.log(
` - ${String(getAverageValue(users, ({ age }: User) => age))} years`
);
console.log();
console.log("Youngest admin:");
logAdmin(getMinElement(admins, compareAdmins));
console.log(
` - was ${colorizeIndex(getMinIndex(users, compareUsers))}th to register`
);
console.log();
console.log("Median admin:");
logAdmin(getMedianElement(admins, compareAdmins));
console.log(
` - was ${colorizeIndex(getMedianIndex(users, compareUsers))}th to register`
);
console.log();
console.log("Oldest admin:");
logAdmin(getMaxElement(admins, compareAdmins));
console.log(
` - was ${colorizeIndex(getMaxIndex(users, compareUsers))}th to register`
);
console.log();
console.log("Average admin age:");
console.log(
` - ${String(getAverageValue(admins, ({ age }: Admin) => age))} years`
);</code></pre><h3>前置知识</h3><ul><li>泛型</li><li>高阶函数</li><li>如何给缺乏类型定义的第三方库定义类型</li></ul><h3>思路</h3><p>和上面的思路类似。 唯一的不同的是这道题的需要实现的几个方法支持不同的入参类型。</p><pre><code class="ts">import {
getMaxIndex,
getMaxElement,
getMinIndex,
getMinElement,
getMedianIndex,
getMedianElement,
getAverageValue,
} from "stats";</code></pre><p>因此,我们考虑使用泛型来定义。 知道了这个, 代码就不难写。 这是最最基本的泛型, 比我们前面写的还简单。</p><h3>代码</h3><pre><code class="ts">declare module "stats" {
export function getMaxIndex<T>(
input: T[],
comparator: (a: T, b: T) => number
): number;
export function getMaxElement<T>(
input: T[],
comparator: (a: T, b: T) => number
): T;
export function getMinElement<T>(
input: T[],
comparator: (a: T, b: T) => number
): T;
export function getMedianIndex<T>(
input: T[],
comparator: (a: T, b: T) => number
): number;
export function getMedianElement<T>(
input: T[],
comparator: (a: T, b: T) => number
): T;
export function getAverageValue<T>(
input: T[],
getValue: (a: T) => number
): number;
export function getMinIndex<T>(
input: T[],
comparator: (a: T, b: T) => number
): number;
}</code></pre><h2>第十三题</h2><h3>题目描述</h3><pre><code>Intro:
The next logical step for us is to provide more
precise registration date for our users and admins.
We've approximately made up dates for each user and
admin and used a library called "date-wizard" in
order to pretty-format the dates.
Unfortunately, type declarations which came with
"date-wizard" library were incomplete.
1. DateDetails interface is missing
time related fields such as hours, minutes and
seconds.
2. Function "pad" is exported but not declared.
Exercise:
Check date-wizard module implementation at:
node_modules/date-wizard/index.js
node_modules/date-wizard/index.d.ts
Extend type declaration of that module in:
module-augmentations/date-wizard/index.ts</code></pre><p>题目大概意思是又来了一个库,这个库又没有写定义,我们又要自己写。 (真实+++++++++++++)</p><h3>题目内置代码</h3><pre><code class="ts">// module-augmentations/data-wizard/index.d.ts
// This enables module augmentation mode.
import "date-wizard";
declare module "date-wizard" {
// Add your module extensions here.
}
// index.ts
import * as dateWizard from "date-wizard";
import "./module-augmentations/date-wizard";
interface User {
type: "user";
name: string;
age: number;
occupation: string;
registered: Date;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
registered: Date;
}
type Person = User | Admin;
const admins: Admin[] = [
{
type: "admin",
name: "Jane Doe",
age: 32,
role: "Administrator",
registered: new Date("2016-06-01T16:23:13"),
},
{
type: "admin",
name: "Bruce Willis",
age: 64,
role: "World saver",
registered: new Date("2017-02-11T12:12:11"),
},
{
type: "admin",
name: "Steve",
age: 40,
role: "Steve",
registered: new Date("2018-01-05T11:02:30"),
},
{
type: "admin",
name: "Will Bruces",
age: 30,
role: "Overseer",
registered: new Date("2018-08-12T10:01:24"),
},
{
type: "admin",
name: "Superwoman",
age: 28,
role: "Customer support",
registered: new Date("2019-03-25T07:51:05"),
},
];
const users: User[] = [
{
type: "user",
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
registered: new Date("2016-02-15T09:25:13"),
},
{
type: "user",
name: "Kate Müller",
age: 23,
occupation: "Astronaut",
registered: new Date("2016-03-23T12:47:03"),
},
{
type: "user",
name: "Moses",
age: 70,
occupation: "Desert guide",
registered: new Date("2017-02-19T17:22:56"),
},
{
type: "user",
name: "Superman",
age: 28,
occupation: "Ordinary person",
registered: new Date("2018-02-25T19:44:28"),
},
{
type: "user",
name: "Inspector Gadget",
age: 31,
occupation: "Undercover",
registered: new Date("2019-03-25T09:29:12"),
},
];
const isAdmin = (person: Person): person is Admin => person.type === "admin";
const isUser = (person: Person): person is User => person.type === "user";
function logPerson(person: Person, index: number) {
let additionalInformation: string = "";
if (isAdmin(person)) {
additionalInformation = person.role;
}
if (isUser(person)) {
additionalInformation = person.occupation;
}
let registeredAt = dateWizard(
person.registered,
"{date}.{month}.{year} {hours}:{minutes}"
);
let num = `#${dateWizard.pad(index + 1)}`;
console.log(
` - ${num}: ${person.name}, ${person.age}, ${additionalInformation}, ${registeredAt}`
);
}
export { dateWizard };
console.log("All users:");
([] as Person[]).concat(users, admins).forEach(logPerson);
console.log();
console.log("Early birds:");
([] as Person[])
.concat(users, admins)
.filter((person) => dateWizard.dateDetails(person.registered).hours < 10)
.forEach(logPerson);
// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/modules.html#ambient-modules
// https://www.typescriptlang.org/docs/handbook/declaration-merging.html</code></pre><h3>前置知识</h3><ul><li>interface 或 type 声明自定义类型</li><li>如何给缺乏类型定义的第三方库定义类型</li></ul><h3>思路</h3><p>和上面两道题思路一样, 不用多说了吧?</p><h3>代码</h3><pre><code class="ts">// This enables module augmentation mode.
import "date-wizard";
declare module "date-wizard" {
// Add your module extensions here.
function dateWizard(date: string, format: string): string;
function pad(s: number): string;
interface DateDetails {
year: number;
month: number;
date: number;
hours: number;
minutes: number;
seconds: number;
}
function dateDetails(date: Date): DateDetails;
}</code></pre><h2>第十四题</h2><p>需要大家有函数式编程的知识, 如果大家不知道会比较难以解释。 为了避免内容太过分散,将这道题从我的题解中移除。</p><p>对函数式编程感兴趣的,也可是看下我之前写的文章 <a href="https://link.segmentfault.com/?enc=bpz11rsZT1Mvq%2F1gJ%2B5qBw%3D%3D.J2iuwDS8jk1xxC8c%2BLNqmI7%2FrbGoeXT9mfFoF0FjxdgLd8zSuuzVLPFrrer3PyAjS7Ohf9f8xj0eyohvN3HxAg%3D%3D" rel="nofollow">函数式编程系列教程</a>。</p><h2>第十五题</h2><h3>题目描述</h3><pre><code>Intro:
Our attempt to Open Source didn't work quite as
expected. It turned out there were already many
existing functional JS libraries.
All the remaining developers left the company as
well. It seems that they are joining a very
ambitious startup which re-invented a juicer and
raised millions of dollars.
Too bad we cannot compete with this kind of
financing even though we believe our idea is
great.
It's time to shine for the last time and publish
our new invention: object-constructor as our CTO
named it. A small library which helps
manipulating an object.
Exercise:
Here is a library which helps manipulating objects.
We tried to write type annotations and we failed.
Please help!</code></pre><p>题目大概意思是函数式编程他们 hold 不住,于是又准备切换到面向对象编程。 于是你需要补充类型定义使得代码不报错。</p><h3>题目内置代码</h3><pre><code class="ts">export class ObjectManipulator {
constructor(protected obj) {}
public set(key, value) {
return new ObjectManipulator({ ...this.obj, [key]: value });
}
public get(key) {
return this.obj[key];
}
public delete(key) {
const newObj = { ...this.obj };
delete newObj[key];
return new ObjectManipulator(newObj);
}
public getObject() {
return this.obj;
}
}</code></pre><h3>前置知识</h3><ul><li>泛型</li><li>Omit 泛型</li><li>ES6 class</li><li>keyof</li><li>使用 extends 进行泛型约束</li><li>联合类型</li></ul><h3>思路</h3><p>这道题难度颇高,比前面的泛型题目都要难。 也是本系列的压轴题,我们重点讲一下。</p><p>首先题目有五个报错位置, 报错信息都是隐式使用了 any , 因此我们的思路就是将五个地方显式声明类型即可。</p><p><img src="/img/remote/1460000037521685" alt="" title=""></p><p>从它的名字 ObjectManipulator 以及 api 可以看出, 它应该可以存储任何对象,因此使用泛型定义就不难想到。</p><p>你也可是把这个 ObjectManipulator 想象成抽象包包。 你的期望是限量款包包拍照的时候用,普通包包和闺蜜逛街的时候用,优衣库送的包包逛超市的时候用等等。</p><p>ObjectManipulator 是一个抽象的包包概念,不是具体的包, 比如当你买一个 LV 的包包的时候就是 <code>ObjectManipulator<LVBag></code>。这样当你往 LV 里放超市买的水果的时候就可以报错:<code>你怎么可以用 LV 包包装这样东西呢?你应该用 ta 装*</code>。</p><p><img src="/img/remote/1460000037521686" alt="" title=""></p><blockquote>当然这个例子很不严谨, 这个只是帮助大家快速理解而已,切莫较真。</blockquote><p>理解了题意,我们就可以开始写了。</p><p>我们先改第一个错 - 构造函数 constructor, 这个错比较简单。</p><pre><code class="ts">export class ObjectManipulator<T> {
constructor(protected obj: T) {
this.obj = obj;
}
...
}</code></pre><p>这个时候经过 ObjectManipulator 实例化产生的对象的 this.obj 都是 T 类型,其中 T 是泛型。因此 getObject 的错也不难改,返回值写 T 就行。</p><pre><code class="ts">export class ObjectManipulator<T> {
...
public getObject(): T {
return this.obj;
}
}</code></pre><p>剩下的 get,set 和 delete 思路有点类似。 先拿 get 来说:</p><pre><code class="ts">export class ObjectManipulator<T> {
...
public get(key) {
return this.obj[key];
}
...
}</code></pre><p>这个怎么写类型呢? key 理论上可是是任何值,返回值理论上也可以是任何值。但是一旦类型 T 确定了, 那么实际上 key 和返回值就不是任意值了。 比如:</p><pre><code class="ts">type A = ObjectManipulator<{ name: string; age: number }>;
const a: A = new ObjectManipulator({ name: "", age: 17 });</code></pre><p>如上代码中的 A 是 ObjectManipulator 传入具体类型 <code>{ name: string; age: number }</code> 产生的新的类型。</p><blockquote>我这里用的是行内类型, 实际项目建议使用 interface 或者 type 定义类型。</blockquote><p>之后我们模拟一些操作:</p><pre><code class="ts">a.set("name", "脑洞前端");
a.get("name");
a.get("name123"); // 期望报错
a.set("name123", "脑洞");
a.delete("name123"); // 期望报错
a.delete("name");</code></pre><p>实际上,我<strong>可能</strong>期望的是其中一些行为可以借助 TypeScript 的类型分析直接报错。</p><p>简单来说,我的期望是 <strong>get 和 delete 不在 T 中的 key 都报错。</strong></p><blockquote>当然你的真实项目也可以不认同我的观点, 比如 get 一个不在 T 中定义的 key 也可以,但是我还是推荐你这么做。</blockquote><p>知道了这个, 再结合我之前有关泛型的文章就不难写出来。</p><p>其中 get 和 delete 的代码:</p><pre><code class="ts">export class ObjectManipulator<T> {
public get<K extends keyof T>(key: K): T[K] {
return this.obj[key];
}
public delete<K extends keyof T>(key: K): ObjectManipulator<Omit<T, K>> {
const newObj = { ...this.obj };
delete newObj[key];
return new ObjectManipulator(newObj);
}
}</code></pre><p>最后是 set,其实一开始我的 set 是这么写的。</p><pre><code class="ts">export class ObjectManipulator<T> {
public set<K extends keyof T, V>(key: K, value: V): ObjectManipulator<T> {
return new ObjectManipulator({
...this.obj,
[key]: value,
}) as ObjectManipulator<T & { [k in K]: V }>;
}
}</code></pre><p>但是无奈没有通过官方的测试用例。 实际项目我其实更推荐我上面的这种写法。下面是我为了通过所有的测试用例写的方法。</p><p>经过分析, 我发现它期望的是 set 中的 key 可以不是 T 中的。这一点从官方给的测试用例就可以看出来。</p><p><img src="/img/remote/1460000037521687" alt="" title=""></p><p>因此我将代码改成 K 放宽到任意 string,返回值做了一个联合类型。代码:</p><pre><code class="ts">export class ObjectManipulator<T> {
...
public set<K extends string, V>(
key: K,
value: V
): ObjectManipulator<T & { [k in K]: V }> {
return new ObjectManipulator({
...this.obj,
[key]: value,
}) as ObjectManipulator<T & { [k in K]: V }>;
}
...
}</code></pre><p>终于通过了所有的测试用例。</p><h3>代码</h3><pre><code class="ts">export class ObjectManipulator<T> {
constructor(protected obj: T) {
this.obj = obj;
}
public set<K extends string, V>(
key: K,
value: V
): ObjectManipulator<T & { [k in K]: V }> {
return new ObjectManipulator({
...this.obj,
[key]: value,
}) as ObjectManipulator<T & { [k in K]: V }>;
}
public get<K extends keyof T>(key: K): T[K] {
return this.obj[key];
}
public delete<K extends keyof T>(key: K): ObjectManipulator<Omit<T, K>> {
const newObj = { ...this.obj };
delete newObj[key];
return new ObjectManipulator(newObj);
}
public getObject(): T {
return this.obj;
}
}</code></pre><h2>总结</h2><p>以上就是给大家带来的题目解析。 这六道题的考点有,按照我个人理解的重要程度划分为:</p><ul><li>type 和 interface 的基本操作(必须掌握)</li><li>如何给缺乏类型定义的第三方库定义类型(必须掌握)</li><li>联合类型 和 交叉类型(强烈建议掌握)</li><li>类型断言和类型收缩(强烈建议掌握)</li><li>泛型和常见内置泛型(强烈建议掌握)</li><li>高阶函数的类型定义(强烈建议掌握)</li></ul><p>最后祝愿大家告别 anyscript,成为 TypeScript 魔法师。</p><h2>关注我</h2><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p>
TypeScript 练习题
https://segmentfault.com/a/1190000025157672
2020-09-28T17:44:53+08:00
2020-09-28T17:44:53+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
39
<p>TypeScript 的学习资料非常多,其中也不乏很多优秀的文章和教程。但是目前为止没有一个我特别满意的。原因有:</p><ul><li>它们大多数没有一个清晰的主线,而是按照 API 组织章节的,内容在<strong>逻辑上</strong>比较零散。</li><li>大多是“讲是什么,怎么用“,而不是”讲为什么,讲原理“。</li><li>大多数内容比较枯燥,趣味性比较低。都是干巴巴的文字,没有图片,缺乏能够引起强烈共鸣的例子。</li></ul><p>因此我的想法是做一套不同市面上大多数的 TypeScript 学习教程。以人类认知的角度思考问题,学习 TypeScript,通过通俗易懂的例子和图片来帮助大家建立 TypeScript 世界观。</p><p>系列安排:</p><ul><li><a href="https://link.segmentfault.com/?enc=c2Dma10Gom78V7vDl41J8g%3D%3D.e2ZYcOjvNtgLpQ2GGyjk53eFRaFJxNSUSbKJiBk4UNAAlQJSp7TWTdy1DVVoZN%2FXOY5RDvKQ3wV6Phndb%2F2k1A%3D%3D" rel="nofollow">上帝视角看 TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=72Qr0orgBRvOLLvDPX%2FXcQ%3D%3D.I3YLYztlt72NR32C3irtyxRlFeyt7O9axWo6lUED9W%2BP%2Btz7wy%2BWn5LQtbcO4eoVNP7VT51wEbmL0w0cdE0yOw%3D%3D" rel="nofollow">TypeScript 类型系统</a></li><li><a href="https://link.segmentfault.com/?enc=L0PegLoa0QRUnGLwRy7pVw%3D%3D.td%2FVnRPhGa7gkQp5hDjGNR1%2B%2B6Ch2cmrVnvYjI%2BHpwr0uMPCk9rBGw9BQy9BoZWk" rel="nofollow">types 和 @types 是什么?</a></li><li><a href="https://link.segmentfault.com/?enc=vqKMZkaTXkClaAMTQs53Hg%3D%3D.HsjMY%2FMCYFYCgpyyA9swuKMi%2F2dkLQQ%2B6wPSi9kfo8m5tWK4%2FsaBAJIOPEDmzqqUj5f5SCP%2Ft33BOrJZGcWS9A%3D%3D" rel="nofollow">你不知道的 TypeScript 泛型(万字长文,建议收藏)</a></li><li><a href="https://link.segmentfault.com/?enc=kTybskaFHOA5uw4QayfN5A%3D%3D.bJWSfZ9asyd0%2B7PjwzbvWa19szVhl5Wh1%2F8NO1zizAjDXry4P5O6T5vRVmet7gTP" rel="nofollow">TypeScript 配置文件该怎么写?</a></li><li>TypeScript 是如何与 React,Vue,Webpack 集成的?</li><li>TypeScript 练习题(就是本文)</li></ul><blockquote>目录将来可能会有所调整。</blockquote><p>注意,我的系列文章基本不会讲 API,因此需要你有一定的 TypeScript 使用基础,推荐两个学习资料。</p><ul><li><a href="https://link.segmentfault.com/?enc=oIE1qr0UeQ4%2B8LkQW1uT4g%3D%3D.3A8oVHpqaPn9BXHcfoHmHU5t5ayT%2BnsCNyq%2BBnOyyD5sZfhpbEpwNrPZZY%2BuVoClXrdARp8kLb8f4xrY9rgZ0w%3D%3D" rel="nofollow">深入理解 TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=j9hR%2B2HCiB6U2DLMInD1dQ%3D%3D.qth2TSxZsCp0SGdxAQfyeB%2FRkCwk%2FxvT4InEcj2yB8GeReLc4oAf8nx4tv4bOndn" rel="nofollow">TypeScript 官方文档</a></li></ul><p>结合这两个资料和我的系列教程,掌握 TypeScript 指日可待。</p><p>接下来,我们通过几个方面来从宏观的角度来看一下 TypeScript。</p><p><!-- more --></p><h2>前言</h2><p>本文涉及的题目一共十六道,全部都可以在 <a href="https://link.segmentfault.com/?enc=LeYDgykZYRIhVmlqOyMl1g%3D%3D.Pv6DKDuD1J29jbtcw%2FwP26cP2c3Rc91ZJCtg%2FFO7TEVD439BH4rR%2FoxJKW%2BxRBr4" rel="nofollow">typescript-exercises</a> 上在线提交。</p><p><img src="/img/remote/1460000025157676" alt="" title=""></p><p>可以和标准答案进行对比。<br><img src="/img/remote/1460000025157675" alt="" title=""></p><p>并且由于使用了浏览器缓存, 因此无需登录的情况下也可以保证关掉页面,你的答题进度也会保留。</p><blockquote>想重置进度,清空缓存,无痕模式或者换浏览器都可以。</blockquote><p>题目中涉及到的知识点我基本也都在之前的文章中提到了,如果你没有看过,强烈建议先完成前面的教程,然后将上面的题目自己做一遍之后再看本文。</p><p>为了不让文章太过于冗长, 本篇文章分两次发布, 一次 8 道题,一共十六道。每道题都有思路,前置知识以及代码。</p><h2>题目一</h2><h3>题目描述</h3><pre><code>Intro:
We are starting a small community of users. For performance
reasons we have decided to store all users right in the code.
This way we can provide our developers with more
user-interaction opportunities. With user-related data, at least.
All the GDPR-related issues we will solved some other day.
This would be the base for our future experiments during
these exercises.
Exercise:
Given the data, define the interface "User" and use it accordingly.
</code></pre><p>题目的大概意思是让你定义一个类型 <code>User</code>, 使得代码可以正常运行。</p><h3>题目内置代码</h3><pre><code class="ts">export type User = unknown;
export const users: unknown[] = [
{
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{
name: "Kate Müller",
age: 23,
occupation: "Astronaut",
},
];
export function logPerson(user: unknown) {
console.log(` - ${user.name}, ${user.age}`);
}
console.log("Users:");
users.forEach(logPerson);</code></pre><h3>前置知识</h3><ul><li>interface 或 type 声明自定义类型</li></ul><h3>思路</h3><p>这道题比较简单, 我们只有定义一个 User 类即可。从 users 数组中不难看出, User 中有三个属性 name ,age 和 occupation,类型分别为 string, number 和 string。因此直接使用 type 或者 interface 定义自定义类型即可。</p><h3>代码</h3><p>核心代码:</p><pre><code class="ts">export type User = {
name: string;
age: number;
occupation: string;
};</code></pre><h2>题目二</h2><h3>题目描述</h3><pre><code>Intro:
All 2 users liked the idea of the community. We should go
forward and introduce some order. We are in Germany after all.
Let's add a couple of admins.
Initially we only had users in the in-memory database. After
introducing Admins, we need to fix the types so that
everything works well together.
Exercise:
Type "Person" is missing, please define it and use
it in persons array and logPerson function in order to fix
all the TS errors.</code></pre><p>题目大意是补充 Person 类, 使得代码不报错。</p><h3>题目内置代码</h3><pre><code class="ts">interface User {
name: string;
age: number;
occupation: string;
}
interface Admin {
name: string;
age: number;
role: string;
}
export type Person = unknown;
export const persons: User[] /* <- Person[] */ = [
{
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{
name: "Jane Doe",
age: 32,
role: "Administrator",
},
{
name: "Kate Müller",
age: 23,
occupation: "Astronaut",
},
{
name: "Bruce Willis",
age: 64,
role: "World saver",
},
];
export function logPerson(user: User) {
console.log(` - ${user.name}, ${user.age}`);
}
persons.forEach(logPerson);</code></pre><h3>前置知识</h3><ul><li>联合类型</li></ul><h3>思路</h3><p>我们直接从报错入手。</p><p><img src="/img/remote/1460000025157678" alt="" title=""></p><p>不难发现 persons 数组既有 User 又有 Admin。 因此 person 的函数签名应该是两者的联合类型。而题目又让我们补充 Person,于是代码将 Person 定义为 Admin 和 User 的联合类型就不难想到。</p><h3>代码</h3><p>核心代码:</p><pre><code class="ts">export type Person = User | Admin;</code></pre><p>这个时候, persons 数组使用的过程只能用 User 和 Admin 的共有属性, 也就是 name 和 age,这点后面的题目也会提到。 因此如果你使用了 role 或者 occupation 就会报错。怎么解决呢? 我们继续看下一题。</p><h2>第三题</h2><h3>题目描述</h3><pre><code>Intro:
Since we already have some of the additional
information about our users, it's a good idea
to output it in a nice way.
Exercise:
Fix type errors in logPerson function.
logPerson function should accept both User and Admin
and should output relevant information according to
the input: occupation for User and role for Admin.</code></pre><h3>题目内置代码</h3><pre><code class="ts">interface User {
name: string;
age: number;
occupation: string;
}
interface Admin {
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{
name: "Jane Doe",
age: 32,
role: "Administrator",
},
{
name: "Kate Müller",
age: 23,
occupation: "Astronaut",
},
{
name: "Bruce Willis",
age: 64,
role: "World saver",
},
];
export function logPerson(person: Person) {
let additionalInformation: string;
if (person.role) {
additionalInformation = person.role;
} else {
additionalInformation = person.occupation;
}
console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}
persons.forEach(logPerson);</code></pre><h3>前置知识</h3><ul><li>类型断言</li><li>类型收敛</li><li>in 操作符</li></ul><h3>思路</h3><p>关于类型收敛, 我在 <a href="https://link.segmentfault.com/?enc=7KyXT%2BeNEmaXqX31lkVfmQ%3D%3D.SbSyX1Z8PWJgov%2BZaZYsSKUcfNGa3PmyKi%2FGeZUD4SrZORsOFnrYkEpuJzN0TjtWFoE0hsthEs1zct2sDsAeiQ%3D%3D" rel="nofollow">TypeScript 类型系统</a> 做了很详情的讨论。</p><p>上面代码报错的原因前面已经讲过了, 那么如何解决呢?由于 person 可能是 User ,也可能是 Admin 类型,而 TypeScript 没有足够的信息确定具体是哪一种。因此你使用 User 或者 Admin <code>特有</code>的属性就会报错了。</p><p>因此解决方案的基本思想就是告诉 TypeScript <strong>person 当前是 Admin 还是 User 类型</strong>。有多种方式可以解决这个问题。</p><ol><li>将 person 断言为准确的类型。 就是告诉 TypeScript ”交给我吧, person 就是 xxx 类型,有错就我的锅“。</li></ol><p>代码:</p><pre><code class="ts">if ((<Admin>person).role) {
additionalInformation = (<Admin>person).role;
} else {
additionalInformation = (<User>person).occupation;
}</code></pre><ol><li>另外一种方式是使用类型收缩,比如 is , in, typeof , instanceof 等。使得 Typescript 能够 Get 到当前的类型。”哦, person 上有 role 属性啊,那它就是 Admin 类型,有问题我 Typescript 的锅“</li></ol><p>这里我们使用 in 操作符,写起来也很简单。</p><blockquote>推荐哪种不用我多说了吧 ?</blockquote><h3>代码</h3><pre><code class="ts">if ("role" in person) {
// person 会被自动推导为 Admin
additionalInformation = person.role;
} else {
// Person 会被自动推导为 User
additionalInformation = person.occupation;
}</code></pre><h2>第四题</h2><h3>题目描述</h3><pre><code>Intro:
As we introduced "type" to both User and Admin
it's now easier to distinguish between them.
Once object type checking logic was extracted
into separate functions isUser and isAdmin -
logPerson function got new type errors.
Exercise:
Figure out how to help TypeScript understand types in
this situation and apply necessary fixes.</code></pre><p>大概意思还是让你改代码, 使得 Typescript 能理解(不报错)。</p><h3>题目内置代码</h3><pre><code class="ts">interface User {
type: "user";
name: string;
age: number;
occupation: string;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{
type: "user",
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{ type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
{ type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
{ type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
];
export function isAdmin(person: Person) {
return person.type === "admin";
}
export function isUser(person: Person) {
return person.type === "user";
}
export function logPerson(person: Person) {
let additionalInformation: string = "";
if (isAdmin(person)) {
additionalInformation = person.role;
}
if (isUser(person)) {
additionalInformation = person.occupation;
}
console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}
console.log("Admins:");
persons.filter(isAdmin).forEach(logPerson);
console.log();
console.log("Users:");
persons.filter(isUser).forEach(logPerson);</code></pre><h3>前置知识</h3><ul><li>类型收敛</li><li>is 操作符</li></ul><h3>思路</h3><p>我们仍然从报错入手。</p><p><img src="/img/remote/1460000025157677" alt="" title=""></p><p>实际上还是 person 的类型问题, 没有被收缩到正确的类型。看题目的代码,期望效果应该是<code>如果进入 isAdmin 内部,那么 person 就是 Admin 类型,同理进入 isUser 内部,那么 person 就是 User 类型。</code></p><p>继续看下 isAdmin 和 isUser 的实现:</p><pre><code class="ts">export function isAdmin(person: Person) {
return person.type === "admin";
}
export function isUser(person: Person) {
return person.type === "user";
}</code></pre><p>这里我们期望的效果是如果 isAdmin 函数返回 true ,那么 person 就应该被收敛为 Admin,isUser 同理。</p><p>这里就需要用到 is 操作符。</p><blockquote>上文提到了类型收敛常见的操作符是 is , in, typeof , instanceof</blockquote><h3>代码</h3><pre><code class="ts">export function isAdmin(person: Person): person is Admin {
return person.type === "admin";
}
export function isUser(person: Person): person is User {
return person.type === "user";
}</code></pre><p>这样当 isAdmin 返回 true, 那么 person 变量就会被推导成 Admin 类型,而不是联合类型, 也就是类型发生了收缩。</p><p>不难看出,这样的类型断言会直接影响到调用 isAdmin 或 isUser 的<strong>函数的入参的类型</strong>。</p><h2>第五题</h2><h3>题目描述</h3><pre><code>Intro:
Time to filter the data! In order to be flexible
we filter users using a number of criteria and
return only those matching all of the criteria.
We don't need Admins yet, we only filter Users.
Exercise:
Without duplicating type structures, modify
filterUsers function definition so that we can
pass only those criteria which are needed,
and not the whole User information as it is
required now according to typing.
Higher difficulty bonus exercise:
Exclude "type" from filter criterias.</code></pre><p>大概意思是让你改 filterUsers, 但要注意 <code>DRY</code>(Don't Repeat Yourself)。</p><h3>题目内置代码</h3><pre><code class="ts">interface User {
type: "user";
name: string;
age: number;
occupation: string;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{
type: "user",
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{
type: "admin",
name: "Jane Doe",
age: 32,
role: "Administrator",
},
{
type: "user",
name: "Kate Müller",
age: 23,
occupation: "Astronaut",
},
{
type: "admin",
name: "Bruce Willis",
age: 64,
role: "World saver",
},
{
type: "user",
name: "Wilson",
age: 23,
occupation: "Ball",
},
{
type: "admin",
name: "Agent Smith",
age: 23,
role: "Administrator",
},
];
export const isAdmin = (person: Person): person is Admin =>
person.type === "admin";
export const isUser = (person: Person): person is User =>
person.type === "user";
export function logPerson(person: Person) {
let additionalInformation = "";
if (isAdmin(person)) {
additionalInformation = person.role;
}
if (isUser(person)) {
additionalInformation = person.occupation;
}
console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}
export function filterUsers(persons: Person[], criteria: User): User[] {
return persons.filter(isUser).filter((user) => {
const criteriaKeys = Object.keys(criteria) as (keyof User)[];
return criteriaKeys.every((fieldName) => {
return user[fieldName] === criteria[fieldName];
});
});
}
console.log("Users of age 23:");
filterUsers(persons, {
age: 23,
}).forEach(logPerson);</code></pre><h3>前置知识</h3><ul><li>泛型</li><li>Partial 泛型</li></ul><h3>思路</h3><p>老规矩, 从报错入手。</p><p><img src="/img/remote/1460000025157679" alt="" title=""></p><p>大概意思是 { age: 23 } 不完整,缺失了部分 key。而题目实际上的想法应该是想根据部分内容对人员进行检错。比如可以根据 age 查, 也可以根据 name 查,也可以同时根据 age 和 name 查等,这和我们平时的搜索逻辑是一致的。</p><p>直接用 Partial 泛型即可解决, 不懂的可以看下我的文章<a href="https://link.segmentfault.com/?enc=tDleQVnE4%2FjFJRx%2BXCtiwA%3D%3D.j8WP3w4Ucpy%2FIgKgHLI0o8hYNTqzxIoL06M9rYEP5CqRzy5W4MTHdeMl0ora4aRdqC9fRFoOQqeDVMubsto%2FYw%3D%3D" rel="nofollow">你不知道的 TypeScript 泛型(万字长文,建议收藏)</a>。</p><h3>代码</h3><pre><code class="ts">export function filterUsers(persons: Person[], criteria: Partial<User>): User[] {
...
}</code></pre><h2>第六题</h2><h3>题目描述</h3><pre><code>Intro:
Filtering requirements have grown. We need to be
able to filter any kind of Persons.
Exercise:
Fix typing for the filterPersons so that it can filter users
and return User[] when personType='user' and return Admin[]
when personType='admin'. Also filterPersons should accept
partial User/Admin type according to the personType.
`criteria` argument should behave according to the
`personType` argument value. `type` field is not allowed in
the `criteria` field.
Higher difficulty bonus exercise:
Implement a function `getObjectKeys()` which returns more
convenient result for any argument given, so that you don't
need to cast it.
let criteriaKeys = Object.keys(criteria) as (keyof User)[];
-->
let criteriaKeys = getObjectKeys(criteria);</code></pre><p>大概意思是让你改 filterUsers, 但要注意 <code>DRY</code>(Don't Repeat Yourself)。并且可以根据 personType 的不同,返回不同的类型。</p><h3>题目内置代码</h3><pre><code class="ts">interface User {
type: "user";
name: string;
age: number;
occupation: string;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{
type: "user",
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{ type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
{ type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
{ type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
{ type: "user", name: "Wilson", age: 23, occupation: "Ball" },
{ type: "admin", name: "Agent Smith", age: 23, role: "Anti-virus engineer" },
];
export function logPerson(person: Person) {
console.log(
` - ${person.name}, ${person.age}, ${
person.type === "admin" ? person.role : person.occupation
}`
);
}
export function filterPersons(
persons: Person[],
personType: "admin",
criteria: Partial<Person>
): Admin[];
export function filterPersons(
persons: Person[],
personType: "user",
criteria: Partial<Person>
): User[];
export function filterPersons(
persons: Person[],
personType: string,
criteria: Partial<Person>
): Person[] {
return persons
.filter((person) => person.type === personType)
.filter((person) => {
let criteriaKeys = Object.keys(criteria) as (keyof Person)[];
return criteriaKeys.every((fieldName) => {
return person[fieldName] === criteria[fieldName];
});
});
}
export const usersOfAge23 = filterPersons(persons, "user", { age: 23 });
export const adminsOfAge23 = filterPersons(persons, "admin", { age: 23 });
console.log("Users of age 23:");
usersOfAge23.forEach(logPerson);
console.log();
console.log("Admins of age 23:");
adminsOfAge23.forEach(logPerson);</code></pre><h3>前置知识</h3><ul><li>泛型</li><li>Partial 泛型</li><li>函数重载</li></ul><h3>思路</h3><p>题目描述也懒得看了, 直接看报错。</p><p><img src="/img/remote/1460000025157680" alt="" title=""></p><p>报错信息提示我们没有找到合适的函数重载。 因此我的思路就是补上合适的重载即可。关于函数重载,我的系列教程不涉及,大家可以看下官网资料。</p><p>重载之后,不同的情况调用返回值就可以对应不同的类型。本题中就是:</p><ul><li>如果 personType 是 admin,就会返回 Admin 数组。</li><li>如果 personType 是 user,就会返回 User 数组。</li><li>如果 personType 是其他 string,就会返回 Person 数组。</li></ul><h3>代码</h3><pre><code class="ts">export function filterPersons(persons: Person[], personType: 'admin', criteria: Partial<Person>): Admin[]
export function filterPersons(persons: Person[], personType: 'user', criteria: Partial<Person>): User[]
export function filterPersons(persons: Person[], personType: string, criteria: Partial<Person>): Person[] {
...
}
</code></pre><h2>第七题</h2><h3>题目描述</h3><pre><code>Intro:
Filtering was completely removed from the project.
It turned out that this feature was just not needed
for the end-user and we spent a lot of time just because
our office manager told us to do so. Next time we should
instead listen to the product management.
Anyway we have a new plan. CEO's friend Nick told us
that if we randomly swap user names from time to time
in the community, it would be very funny and the project
would definitely succeed!
Exercise:
Implement swap which receives 2 persons and returns them in
the reverse order. The function itself is already
there, actually. We just need to provide it with proper types.
Also this function shouldn't necessarily be limited to just
Person types, lets type it so that it works with any two types
specified.</code></pre><p>题目大概意思是让你修改 swap 函数,使得不报错。 并且,我希望这个函数可以适用于任意两个变量,不管其类型一样不一样, 也不管二者类型是什么。</p><h3>题目内置代码</h3><pre><code class="ts">interface User {
type: "user";
name: string;
age: number;
occupation: string;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
}
function logUser(user: User) {
const pos = users.indexOf(user) + 1;
console.log(` - #${pos} User: ${user.name}, ${user.age}, ${user.occupation}`);
}
function logAdmin(admin: Admin) {
const pos = admins.indexOf(admin) + 1;
console.log(` - #${pos} Admin: ${admin.name}, ${admin.age}, ${admin.role}`);
}
const admins: Admin[] = [
{
type: "admin",
name: "Will Bruces",
age: 30,
role: "Overseer",
},
{
type: "admin",
name: "Steve",
age: 40,
role: "Steve",
},
];
const users: User[] = [
{
type: "user",
name: "Moses",
age: 70,
occupation: "Desert guide",
},
{
type: "user",
name: "Superman",
age: 28,
occupation: "Ordinary person",
},
];
export function swap(v1, v2) {
return [v2, v1];
}
function test1() {
console.log("test1:");
const [secondUser, firstAdmin] = swap(admins[0], users[1]);
logUser(secondUser);
logAdmin(firstAdmin);
}
function test2() {
console.log("test2:");
const [secondAdmin, firstUser] = swap(users[0], admins[1]);
logAdmin(secondAdmin);
logUser(firstUser);
}
function test3() {
console.log("test3:");
const [secondUser, firstUser] = swap(users[0], users[1]);
logUser(secondUser);
logUser(firstUser);
}
function test4() {
console.log("test4:");
const [firstAdmin, secondAdmin] = swap(admins[1], admins[0]);
logAdmin(firstAdmin);
logAdmin(secondAdmin);
}
function test5() {
console.log("test5:");
const [stringValue, numericValue] = swap(123, "Hello World");
console.log(` - String: ${stringValue}`);
console.log(` - Numeric: ${numericValue}`);
}
[test1, test2, test3, test4, test5].forEach((test) => test());</code></pre><h3>前置知识</h3><ul><li>泛型</li></ul><h3>思路</h3><p>题目废话很多, 直接忽略看报错。</p><p><img src="/img/remote/1460000025157681" alt="" title=""></p><p>这个其实我在 <a href="https://link.segmentfault.com/?enc=mD7wnEP2rDhCFJS2B%2Fk6jA%3D%3D.P%2B7eca%2Bc2K8BvqpzvrBSudSNqXmTcy9WucdPe2iZaePSGyb257KNTVBaTHfP9dIDbWEKW9SNM2%2BSWFPjlk1dbQ%3D%3D" rel="nofollow">你不知道的 TypeScript 泛型(万字长文,建议收藏)</a> 里也讲过了,直接看代码。</p><h3>代码</h3><pre><code class="ts">export function swap<U, T>(v1: T, v2: U): [U, T] {
return [v2, v1];
}</code></pre><h2>第八题</h2><h3>题目描述</h3><pre><code>Intro:
Project grew and we ended up in a situation with
some users starting to have more influence.
Therefore, we decided to create a new person type
called PowerUser which is supposed to combine
everything User and Admin have.
Exercise:
Define type PowerUser which should have all fields
from both User and Admin (except for type),
and also have type 'powerUser' without duplicating
all the fields in the code.</code></pre><p>题目大概意思是定义一个类型 PowerUser, 里面包含 User 和 Admin 的所有属性, 并且有一个字段是固定的 type: 'powerUser'。</p><h3>题目内置代码</h3><pre><code class="ts">interface User {
type: "user";
name: string;
age: number;
occupation: string;
}
interface Admin {
type: "admin";
name: string;
age: number;
role: string;
}
type PowerUser = Omit<User & Admin, "type"> & { type: "powerUser" };
export type Person = User | Admin | PowerUser;
export const persons: Person[] = [
{
type: "user",
name: "Max Mustermann",
age: 25,
occupation: "Chimney sweep",
},
{ type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
{ type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
{ type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
{
type: "powerUser",
name: "Nikki Stone",
age: 45,
role: "Moderator",
occupation: "Cat groomer",
},
];
function isAdmin(person: Person): person is Admin {
return person.type === "admin";
}
function isUser(person: Person): person is User {
return person.type === "user";
}
function isPowerUser(person: Person): person is PowerUser {
return person.type === "powerUser";
}
export function logPerson(person: Person) {
let additionalInformation: string = "";
if (isAdmin(person)) {
additionalInformation = person.role;
}
if (isUser(person)) {
additionalInformation = person.occupation;
}
if (isPowerUser(person)) {
additionalInformation = `${person.role}, ${person.occupation}`;
}
console.log(`${person.name}, ${person.age}, ${additionalInformation}`);
}
console.log("Admins:");
persons.filter(isAdmin).forEach(logPerson);
console.log();
console.log("Users:");
persons.filter(isUser).forEach(logPerson);
console.log();
console.log("Power users:");
persons.filter(isPowerUser).forEach(logPerson);</code></pre><h3>前置知识</h3><ul><li>集合操作(交叉类型)</li><li>& 操作符</li><li>泛型</li><li>Omit 泛型</li></ul><h3>思路</h3><p>从题目信息不难看出,就是让我们实现 PowerUser。</p><p>有前面的分析不难得出我们只需要:</p><ul><li>合并 User 和 Admin 的属性即可。 借助 & 操作符可以实现。即 <code>User & Admin</code>。</li><li>增加特有的属性 type: powerUser。 首先去掉上一步合并的 type 属性, 然后继续和 { type: "powerUser" } 交叉即可。</li><li>增加 { type: "powerUser" } 之前使用内置泛型 Omit 将原本的 type 删掉即可。</li></ul><h3>代码</h3><pre><code class="ts">type PowerUser = Omit<User & Admin, "type"> & { type: "powerUser" };</code></pre><h2>总结</h2><p>以上就是给大家带来的题目解析。 这八道题的考点有,按照我个人理解的重要程度划分为:</p><ul><li>type 和 interface 的基本操作(必须掌握)</li><li>联合类型 和 交叉类型(强烈建议掌握)</li><li>类型断言和类型收缩(强烈建议掌握)</li><li>泛型和常见内置泛型(强烈建议掌握)</li><li>函数重载(推荐掌握)</li></ul><p>最后祝愿大家告别 anyscript,成为 TypeScript 魔法师。</p><h2>关注我</h2><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p><p><img src="/img/remote/1460000022977789" alt="" title=""></p><p>公众号【 <a href="https://link.segmentfault.com/?enc=wuNOHfiGUSK2y%2BzUdjMGxA%3D%3D.cl7y8RBrCoJLvGU4DXnJe9BU0nybaWHPYOAJsNLk75gKWLp04340iZrvKT62zFRDHy%2FoYBSwG%2FeWyq4QijN51lKxqArddUmPZGhPMDKRZ78%3D" rel="nofollow">力扣加加</a>】<br>知乎专栏【 <a href="https://link.segmentfault.com/?enc=xF4pDsKlijWkqIvqyZW2NA%3D%3D.4motS%2BbonZM8vswJhvw2G1gcBl5u54tcD4iPVCRyZIeIvl%2BGnIQmx2D5gzyFFJ58" rel="nofollow">Lucifer - 知乎</a>】</p><p>点关注,不迷路!</p>
TypeScript 配置文件该怎么写?
https://segmentfault.com/a/1190000023750243
2020-08-25T10:51:38+08:00
2020-08-25T10:51:38+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
58
<p>TypeScript 的学习资料非常多,其中也不乏很多优秀的文章和教程。但是目前为止没有一个我特别满意的。原因有:</p><ul><li>它们大多数没有一个清晰的主线,而是按照 API 组织章节的,内容在<strong>逻辑上</strong>比较零散。</li><li>大多是“讲是什么,怎么用“,而不是”讲为什么,讲原理“。</li><li>大多数内容比较枯燥,趣味性比较低。都是干巴巴的文字,没有图片,缺乏能够引起强烈共鸣的例子。</li></ul><p>因此我的想法是做一套不同市面上大多数的 TypeScript 学习教程。以人类认知的角度思考问题,学习 TypeScript,通过通俗易懂的例子和图片来帮助大家建立 TypeScript 世界观。</p><p>系列安排:</p><ul><li><a href="https://link.segmentfault.com/?enc=H0xk4whrYWV9ck9ljFrOEg%3D%3D.FUfDgxummU4u2nWmtxd%2FvKL0dHA7X4aTAgAi8Qwhwjq839i5lxrnoZld5%2BLnivxpYbolAtdyBDBAtrG59LOuXA%3D%3D" rel="nofollow">上帝视角看 TypeScript(已发布)</a></li><li><a href="https://link.segmentfault.com/?enc=YOYm80AKrZAeuxqvwESW8A%3D%3D.DKTXBnalQFnS%2FJair0D59SroK1LVvCXeZBUk76MwgyMwvVCxdiK8cINi4dnS75ewC2Khj4NGJutr6gFbSOR7gA%3D%3D" rel="nofollow">TypeScript 类型系统(已发布)</a></li><li><a href="https://link.segmentfault.com/?enc=OLMawyhWeI68Y4LMY4rNpA%3D%3D.Qskfb10JLSzMCYa1cQaaRd38pABIQcE5jQtsa6R%2BBPCegZBFpr0WNPmMQ0XLtn%2BK" rel="nofollow">types 和 @types 是什么?(已发布)</a></li><li><a href="https://link.segmentfault.com/?enc=DlNkU3daxYMCQOwBwCfACA%3D%3D.MuRHWWhRQ2Pda8LPtVk3TaRdiHX3koFnFQbDjsy8CpgIpENR26nzyhCL1kc6VlVXbfcCPAQXUIl7TKKng0V%2B%2FQ%3D%3D" rel="nofollow">你不知道的 TypeScript 泛型(万字长文,建议收藏)(已发布)</a></li><li>TypeScript 配置文件该怎么写?(就是本文)</li><li>TypeScript 是如何与 React,Vue,Webpack 集成的?</li><li>TypeScript 练习题</li></ul><blockquote>目录将来可能会有所调整。</blockquote><p>注意,我的系列文章基本不会讲 API,因此需要你有一定的 TypeScript 使用基础,推荐两个学习资料。</p><ul><li><a href="https://link.segmentfault.com/?enc=1rBDygsf8CzzYlz7LBXmwg%3D%3D.Q2NJLJIHQx0A7KJdO7XR4eEy%2FOIEUzh7psI1LFe34jxPlFHiTiHZNgZFGazTuU4wRGSLKU5jwj5V1MKKTRFLdQ%3D%3D" rel="nofollow">深入理解 TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=%2BEFLgZf2CHPny04WSVuMcw%3D%3D.QurYj%2Fd%2BX%2BKLcHM1XDVr4TNjYk8GKEvwpe5DvReKMCWh2kqLeWDDMlTbcnvB6mv8" rel="nofollow">官方文档</a></li></ul><p>结合这两个资料和我的系列教程,掌握 TypeScript 指日可待。</p><p>接下来,我们通过几个方面来从宏观的角度来看一下 TypeScript。</p><p><!-- more --></p><h2>前言</h2><p>这篇文章是我的 TypeScript 系列的<strong>第 5 篇</strong>。今天我们就来看下, TypeScript 的配置文件 tsconfig.json 该如何写。</p><p>和 package.json 一样, 它也是一个 JSON 文件。package.json 是包描述文件,对应的 Commonjs 规范,而 <strong>tsconfig.json 是最终被 TypeScript Compiler 解析和使用的一个 JSON 文件</strong>。 TypeScript Compiler 用这个配置文件来决定如何对项目进行编译。</p><p>说到编译,不得不提一个知名选手 - <a href="https://link.segmentfault.com/?enc=3nELlX4tzPMfHxvxRmqPxA%3D%3D.NSHlGmq3i2y5osxYFjgPAhaKpSYxaARD%2FC8OXeMpI3FgSeiBqzOeAmNcppmzgnaF" rel="nofollow">babel</a>。 和 TypeScript 类似, 他们都可以将一种语法静态编译成另外一种语法。如果说我想编译一个文件,我只需要告诉 babel 我的文件路径即可。</p><pre><code class="bash">npx babel script.js</code></pre><p>有时候我想编译整个文件夹:</p><pre><code class="bash">npx babel src --out-dir lib</code></pre><p>babel 也可以指定输出目录,指定需要忽略的文件或目录等等, TypeScript 也是一样!你当然可以像 babel 一样在命令行中全部指定好,也可以将这些配置放到 tsconfig.json 中,以配置文件的形式传递给 TypeScript Compiler 。 这就是 tsconfig.json 文件的初衷,即接受用户输入作为配置项。</p><h2>初探 tsconfig</h2><p>我们先来看一个简单的 tsconfig 文件。</p><pre><code class="json">{
"compilerOptions": {
"outDir": "./built",
"allowJs": true,
"target": "es5"
},
"include": ["./src/**/*"]
}</code></pre><p>如上配置做了:</p><ul><li>读取所有可识别的 src 目录下的文件(通过 include)。</li><li>接受 JavaScript 做为输入(通过 allowJs)。</li><li>生成的所有文件放在 built 目录下(通过 outDir)。</li><li>将 JavaScript 代码降级到低版本比如 ECMAScript 5(通过 target)。</li></ul><p>实际项目有比这个更复杂。 接下来, 我们来进一步解读。 不过在讲配置项之前,我们先来看下 tsconfig.json 是如何被解析的。</p><h2>tsconfig 是如何被解析的?</h2><p><strong>如果一个目录下存在一个 tsconfig.json 文件,那么意味着这个目录是 TypeScript 项目的根目录。</strong> 如果你使用 tsc 编译你的项目,并且没有显式地指定配置文件的路径,那么 tsc 则会逐级向上搜索父目录寻找 tsconfig.json ,这个过程类似 node 的模块查找机制。</p><p><img src="/img/remote/1460000023750246" alt="" title=""></p><p>如图:</p><ul><li>在 _uglify-js@3.7.2@uglify-js 下执行 tsc 则会找到 配置文件 1,在 _uglify-js@3.7.2@uglify-js/bin 下执行 tsc 也会找到 配置文件 1</li><li>同理在 lib,node_modules 也会找到 配置文件 1</li><li>在 _uglify-js@3.7.2@uglify-js/bin/lucifer 下执行 tsc 则会找到 配置文件 2</li><li>在 _uglify-js@3.7.2@uglify-js/lib/lucifer 下执行 tsc 则会找到 配置文件 3</li></ul><p>我在 <a href="https://link.segmentfault.com/?enc=xjtV%2BtdFiToxDBngzWp0RA%3D%3D.aHuY2NnXIQ0cN2iNrAkS%2FtRoPcMO04S4zw6CRraoKGe83MJy1SsM1LuExNSd7J5UEjeloLvZGwJFbNZvAtvRwQ%3D%3D" rel="nofollow">上帝视角看 TypeScript</a> 一种讲述了 TypeScript 究竟做了什么,带你从宏观的角度看了一下 TypeScript。 其中提到了 TypeScript 编译器会接受文件或者文件集合作为输入,最终转换为 JavaScript(noEmit 为 false) 和 .d.ts(declarations 为 true)。</p><p><img src="/img/remote/1460000023489697" alt="" title=""></p><p>这里其实还少了一个点,那就是除了接受文件或者文件集合作为输入,还会接受 tsconfig.json。tsconfig.json 的内容决定了编译的范围和行为,不同的 配置可能会得到不同的输出,或者得到不同的检查结果。</p><p>当 tsc 找到了一个 tsconfig.json 文件,那么其规定的编译目录则全部会被 typescript 处理,当然也包括其依赖的文件。 如果 tsc 没有找到一个 tsconfig.json 或 tsconfig 没有有效信息,那么 tsc 会使用默认配置。 比如 tsconfig 是一个空的就没有有效信息:</p><pre><code class="json">{}</code></pre><blockquote>tsconfig 的全部属性,以及属性的默认值可以在这里找到: <a href="https://link.segmentfault.com/?enc=DUzLSwTkdVnCpfO3b9i4gw%3D%3D.I%2FDb4MGL9cyA%2BwSOZ7TyqBsWtIp3twtyPRCQe2%2FZbY6a9nYUDowwgo6ElFImP7el" rel="nofollow">http://json.schemastore.org/t...</a></blockquote><p>总结一下 tsc 解析 tsconfig.json 的逻辑。</p><ul><li><p>如果命令行指定了配置选项或者指定了配置文件的路径,那么直接会读取。</p><ul><li><p>根据 <a href="https://link.segmentfault.com/?enc=rAdubafUsTqX%2BUfQj%2BSDfw%3D%3D.6CEVcNcHtQI0oVA6az614jSXJ4rgwURaj3BYVP6MY7tccCU7Jw7qNDkeAwLGVCyL" rel="nofollow">tsconfig json schema</a> 校验是否格式正确。</p><ul><li>如果正确,则将其和默认配置合并(如果有 extends 字段,也会一起合并),将合并后的配置传递给 TypeScript 编译器并开始编译。</li><li>否则抛出错误</li></ul></li></ul></li><li><p>否则,会从当前目录查找 tsconfig.json 文件, 如果找不到则逐层向上搜索父目录。</p><ul><li><p>如果找到了则会去根据 <a href="https://link.segmentfault.com/?enc=inyY16WGIkGSx3ip1dEuEw%3D%3D.tihjOIgh9Vqq3McG6k%2Fb86mkRDMkc2j72cLAHcLiJIyzALE5lREaTud06xIkyenW" rel="nofollow">tsconfig json schema</a> 校验是否格式正确。</p><ul><li>如果正确,则将其和默认配置合并(如果有 extends 字段,也会一起合并),将合并后的配置传递给 TypeScript 编译器并开始编译。</li><li>否则抛出错误</li></ul></li><li>否则,始终找不到则直接使用默认配置</li></ul></li></ul><h2>tsconfig 的顶层属性</h2><p>tsconfig 的顶层属性(Top Level)不多,主要有:<strong>compilerOptions, files, include, exclude,extends,compileOnSave</strong>等。</p><ul><li>compilerOptions 是重头戏,其属性也是最多的,我们的项目也是对这个定制比较多,这个我后面会重点讲。</li><li>files 则是你需要编译的文件</li><li>exclude 则是你不需要编译的文件目录(支持 glob)</li><li>include 是你需要编译的文件目录(支持 glob)</li><li>extends 就是继承另外一个配置文件,TypeScript 会对其进行合并,多项目公共配置有用。你也可以直接继承社区的“最佳实践”,比如:</li></ul><pre><code class="json">{
"extends": "@tsconfig/node12/tsconfig.json",
"compilerOptions": {},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}</code></pre><ul><li>compileOnSave 则是和编辑器(确切地说是文件系统)联动的配置,即是否在文件保存后进行编译,实际项目不建议使用。</li></ul><p>除了 compilerOptions,其他也相对比较好理解。 因此接下来我只针对 compilerOptions 详细讲解一番。</p><h2>tsconfig 的编译项</h2><p>详细全面的内容,大家只需要参考<a href="https://link.segmentfault.com/?enc=QMZNn5lvmYQKoBI24y0WNQ%3D%3D.pHP%2FP4IuWusSsmR%2B9tb5nkUQ1K5%2BnFXqaIDSBSi9NZ3T%2FPm4vKgMZviaJx0hYT73" rel="nofollow">官网</a>的就好了。官网写的不仅全面,而且做了分类,非常清晰。</p><p>接下来,我会根据功能分开讲几个<strong>常用</strong> 的配置。</p><h3>文件相关</h3><p>常用的是以下四个,由于前面已经做了介绍,因此就不赘述了。</p><ul><li>exclude</li><li>extends</li><li>files</li><li>include</li></ul><h3>严格检查</h3><ul><li>alwaysStrict</li></ul><p>默认:false</p><p>首次发布版本:2.1</p><p>这个是和 ECMAScript 规范相关的,工作机制和 ES 5 的严格模式一样, 并且输出的 JS 顶部也会也会带上 'use strict'。</p><ul><li>noImplicitAny(推荐打开)</li></ul><p>默认:true</p><p>首次发布版本:-</p><p>我在 - <a href="https://link.segmentfault.com/?enc=3upkCpPGmF9a05fRwfoXoA%3D%3D.siIgYDxLSSi0JBB9h3KBn8uV%2BlZzTsVODf4czoRHdY1WxvfN3uRdhcp2atP8STt5xlKn7g%2FDsqKautzip4UOyQ%3D%3D" rel="nofollow">TypeScript 类型系统</a> 中提到了如果不对变量显式声明类型,那么 TypeScript 会对变量进行类型推导,这当然也有推导不出的情况,这个时候该变量的类型就是 any,这个叫做隐式 any。区别于显式 any:</p><pre><code class="ts">const a: any = {};</code></pre><p>隐式 any 是 TypeScript 编译器推断的。</p><ul><li>noImplicitThis(推荐打开)</li></ul><p>默认:true</p><p>首次发布版本:2.0</p><p>和隐式 any 类型, 只不过这次是针对的特殊的一个关键字 this,也就是你需要显式地指定 this 的类型。</p><ul><li>strict(推荐打开)</li></ul><p>默认:true</p><p>首次发布版本:2.3</p><p>实际上 strict 只是一个简写,是多个规则的合集。 类似于 babel 中插件(plugins)和 预设(presets)的差别。换句话说如果你指定了 strict 为 true ,那么所有严格相关的规则的都会开启,我所讲的<strong>严格检查</strong>都是,还有一部分我没有提到的。另外将来如果增加更多严格规则,你只要开启了 strict 则会自动加进来。</p><h3>模块解析</h3><h4>模块相关</h4><p>目的:<strong>allowSyntheticDefaultImports,allowUmdGlobalAccess,esModuleInterop,moduleResolution 都是为了和其他模块化规范兼容做的。</strong></p><ul><li>allowSyntheticDefaultImports</li><li>allowUmdGlobalAccess</li><li>esModuleInterop</li><li>moduleResolution</li></ul><p>还有一个配置 <strong>module</strong>,规定了项目的模块化方式,选项有 AMD,UMD,commonjs 等。</p><h4>路径相关</h4><p>目的: <strong>baseUrl,paths,rootDirs, typeRoots,types 都是为了简化路径的拼写做的。</strong></p><ul><li>baseUrl</li></ul><p>这个配置是告诉 TypeScript 如何解析模块路径的。比如:</p><pre><code class="ts">import { helloWorld } from "hello/world";
console.log(helloWorld);</code></pre><p>这个就会从 baseUrl 下找 hello 目录下的 world 文件。</p><ul><li>paths</li></ul><p>定义类似别名的存在,从而简化路径的书写。</p><ul><li>rootDirs</li></ul><p>注意是 rootDirs ,而不是 rootDir,也就是说根目录可以有多个。 当你指定了多个根目录的时候, 不同根目录的文件可以像在一个目录下一样互相访问。</p><blockquote>实际上也有一个叫 rootDir 的, 和 rootDirs 的区别就是其只能指定一个。</blockquote><ul><li>typeRoots</li><li>types</li></ul><p>types 和 typeRoots 我在 - <a href="https://link.segmentfault.com/?enc=mGKHiBIp4LJzJ4q45h%2BJ%2FA%3D%3D.aCbi3QBda%2BJnIdK3a0X6F1Ur49Gf%2FUa%2Fk6%2B4C6Dh%2FLqWSF4rsKJpk2q%2B%2BxtAzqcj" rel="nofollow">types 和 @types 是什么?</a> 已经讲得很清楚了,这里就不多说了。</p><h3>项目配置</h3><h4>JavaScript 相关</h4><ul><li>allowJs</li></ul><p>默认:false</p><p>首次发布版本:1.8</p><p>顾名思义,允许在 TypeScript 项目中使用 JavaScript,这在从 JavaScript 迁移到 TypeScript 中是非常重要的。</p><ul><li>checkJs</li></ul><p>默认:false</p><p>首次发布版本:-</p><p>和 allowJs 类似, 只不过 checkJs 会额外对 JS 文件进行校验。</p><h4>声明文件相关</h4><p>如果 TypeScript 是将 TS 文件编译为 JS,那么声明文件 + JS 文件就可以反推出 TS 文件。</p><p>这两个用来生成 .d.ts 和 .d.ts 的 sourcemap 文件。</p><ul><li>declaration</li></ul><p>默认:false</p><p>首次发布版本:1.0</p><ul><li>declarationMap</li></ul><p>默认:false</p><p>首次发布版本:2.9</p><h4>外部库相关</h4><ul><li>jsx</li></ul><p>默认:react</p><p>首次发布版本:2.2</p><p>这个是告诉 TypeScript 如何编译 jsx 语法的。</p><ul><li>lib</li></ul><p>默认:-</p><p>首次发布版本:2.0</p><p>lib 我在 <a href="https://link.segmentfault.com/?enc=ruqpaWQ2dB3Wlbkk%2BU0%2Bdw%3D%3D.6CSa7yLhdFMm39JZhrZK5ksiQfBHEoNyXn%2Bq5YGg8qO7utNghGaITuylQrz9CYS1o%2Bx7LiNBF8kmhT8bGc6zCg%3D%3D" rel="nofollow">TypeScript 类型系统</a> 中讲过。 Typescript 提供了诸如 lib.d.ts 等类型库文件。随着 ES 的不断更新, JavaScript 类型和全局变量会逐渐变多。Typescript 也是采用这种 lib 的方式来解决的。</p><p><img src="/img/remote/1460000023647616" alt="" title=""></p><p>(TypeScript 提供的部分 lib)</p><h4>输出相关</h4><p>outDir 和 outFile 这两个配置则是告诉 TypeScript 将文件生成到哪里。</p><ul><li>outDir</li></ul><p>默认:和 ts 文件同目录(且同名,只是后缀不同)</p><p>首次发布版本:-</p><ul><li>outFile</li></ul><p>默认:-</p><p>首次发布版本:1.0</p><p>module 是 CommonJS 和 ES6 module 不能知道 outFile,只有是 None, System 或 AMD 才行,其会将这些模块的文件内容打包到全局文件内容之后。</p><p>而 noEmit 则是控制是否输出 JS 文件的。</p><ul><li>noEmit</li></ul><p>默认:false</p><p>首次发布版本:-</p><p>如果你只希望用 TypeScript 进行类型检查,不希望要它生成文件,则可以将 noEmit 设置成 true。</p><ul><li>target</li></ul><p>即输出的 JavaScript 对标的 ECMA 规范。 比如 “target”: “es6” 就是将 es6 + 的语法转换为 ES6 的 代码。其选项有 ES3,ES5,ES6 等。</p><blockquote>为什么没有 ES4 ? ^_^</blockquote><h2>总结</h2><ul><li>tsconfig 就是一个 JSON 文件,TypeScript 会使用该文件来决定如何编译和检查 TypeScript 项目。和 babel 类似,甚至很多配置项都是相通的。</li><li>如果一个目录下存在一个 tsconfig.json 文件,那么意味着这个目录是 TypeScript 项目的根目录。 如果你使用 tsc 编译你的项目,并且没有显式地指定配置文件的路径,那么 tsc 则会逐级向上搜索父目录寻找 tsconfig.json ,这个过程类似 node 的模块查找机制。</li><li>tsconfig 中最重要的恐怕就是编译器选项(compilerOptions)了。如果你按照功能去记忆则会比较简单, 比如文件相关的有哪些, 严格检查的有哪些,声明文件的有哪些等等。</li></ul><h2>参考</h2><ul><li><a href="https://link.segmentfault.com/?enc=US3m4bOBSCOgcbgdvSP8Yw%3D%3D.5o3%2FOap1dx%2Be%2BIh6tuKRA179cgqUhjfuCDop9C153JjwH92VYqWIfYLP0iXxcamf" rel="nofollow">typescriptlang's tsconfig</a></li></ul><h2>关注我</h2><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p><p>知乎专栏【 <a href="https://link.segmentfault.com/?enc=VZVpOa4FklP6ZldEITmBiw%3D%3D.aXPhS0e1PG7JY5PgTTKJy0KUzloGWb8w1IS0JEn9TFzo16Rfpm7IQQ81ybqBXKFN" rel="nofollow">Lucifer - 知乎</a>】</p><p>点关注,不迷路!</p>
types 和 @types 是什么?
https://segmentfault.com/a/1190000023722037
2020-08-22T12:18:58+08:00
2020-08-22T12:18:58+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
10
<p>TypeScript 的学习资料非常多,其中也不乏很多优秀的文章和教程。但是目前为止没有一个我特别满意的。原因有:</p><ul><li>它们大多数没有一个清晰的主线,而是按照 API 组织章节的,内容在<strong><em>逻辑上</em></strong>比较零散。</li><li>大多是“讲是什么,怎么用“,而不是”讲为什么,讲原理“。</li><li>大多数内容比较枯燥,趣味性比较低。都是干巴巴的文字,没有图片,缺乏能够引起强烈共鸣的例子。</li></ul><p>因此我的想法是做一套不同市面上大多数的 TypeScript 学习教程。以人类认知的角度思考问题,学习 TypeScript,通过通俗易懂的例子和图片来帮助大家建立 TypeScript 世界观。</p><p>系列安排:</p><ul><li><a href="https://link.segmentfault.com/?enc=Z23LNMOXepm3OoF7kmR0Ow%3D%3D.qfiWPHawoSt5i9MhD8B2v8iPStsxrEsxwW3ep29hci1mn4Cwa%2FXzw5ZTbOJsVQXaIxyAfJ%2FnsoU9Io4RSzg8CQ%3D%3D" rel="nofollow">上帝视角看 TypeScript(已发布)</a></li><li><a href="https://link.segmentfault.com/?enc=2y71N43jr9iamqj40235gA%3D%3D.GS7HDCMFGaXjVJdCwuk3XN9wGaCnnodbki7ObMrEvDF2SoaXasx21c19F1oVoHJ%2B5rl8R%2FJfA15mwaeX9v6AWg%3D%3D" rel="nofollow">TypeScript 类型系统(已发布)</a></li><li>types 和 @types 是什么?(就是本文)</li><li><a href="https://link.segmentfault.com/?enc=opUvxHRsJflfKHuiedqd4A%3D%3D.5tfIHcFOzJU%2F5GA2wJ44SkK9M%2F0KNy62AlEr9dotwZ9EjEcb6jHqFhmGP9N5uBZbMJcUOx%2B9zhK561p780OixQ%3D%3D" rel="nofollow">你不知道的 TypeScript 泛型(万字长文,建议收藏)</a>(已发布)</li><li>TypeScript 配置文件该怎么写?</li><li>TypeScript 是如何与 React,Vue,Webpack 集成的?</li><li>TypeScript 练习题</li></ul><blockquote>目录将来可能会有所调整。</blockquote><p>注意,我的系列文章基本不会讲 API,因此需要你有一定的 TypeScript 使用基础,推荐两个学习资料。</p><ul><li><a href="https://link.segmentfault.com/?enc=fAqyem9pqm6HykbL79uN8w%3D%3D.cGeNqut%2B8KM20g6s5okYKRdN5PG8qIEIDM8twXHP1hJXDiTX2lQZ1JaFS%2Bpv4wFxteL1qYdFBWbK9kjiFtH0tA%3D%3D" rel="nofollow">深入理解 TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=r1Gpy21gQ8cxFd2UKZrPHw%3D%3D.TU0mhUmcqhezQTW0a6dTQ8lzVrAcyZsh3jOK6dCpLzL%2F2a4HnNsIKcVqfFWOFFBR" rel="nofollow">官方文档</a></li></ul><p>结合这两个资料和我的系列教程,掌握 TypeScript 指日可待。</p><p>接下来,我们通过几个方面来从宏观的角度来看一下 TypeScript。</p><p><!-- more --></p><h2>前言</h2><ul><li>作者:feiker & Lucifer</li></ul><p>TypeScript 中有几个概念和名字很像,会让初学者傻傻分不清楚。比如配置文件中的 <strong><em>types 和 typeRoots</em></strong>,并且还有一个 @types。接触过 TypeScript 的人一定接触过它们, 这几个有什么区别和联系呢?今天就带你来重新认识下它们。</p><h2>一个例子</h2><p>这里我通过一个例子来说明一下什么是 @types,这样大家理解起来更深刻一点。</p><p>当我们用 npm 等包管理工具安装第三方包的时候,有些包并不是 TypeScript 编写的,自然也不会导出 TypeScript 声明文件。这种情况下,如果我们在 TypeScript 项目中引入了这种包,则会编译报错(没有设置 allowJS)。举个例子,当我们通过<code>npm install jquery --save</code> 安装 jquery 包并引用的时候,TypeScript 会报错。</p><blockquote>allowJS 是 TypeScript 1.8 引进的一个编译项。</blockquote><p><img alt="" title=""></p><p>报错内容如下:</p><blockquote>Could not find a declaration file for module ‘jquery’. Try <code>npm install @types/jquery</code> if it exists or add a new declaration (.d.ts) file containing <code>declare module 'jquery';</code></blockquote><p>这里的意思是 TypeScript 没有找到 jquery 这个包的定义,你可以通过<code>npm install @types/jquery</code>安装相关声明,或者自己定义一份.d.ts 文件,并将 jquery 声明为 module。</p><p>全世界不是 TypeScript 编写的包多了去了。即使你的包是 TypeScript 编写的,如果你没有导出声明文件,也是没用的。(TypeScript 默认不会导出声明文件,只会编译输出 JavaScript 文件)。因此 TypeScript 必须对这种情况提供解决方案,而上面的两种方案(安装 @types 和 自己 declare module)就是 TypeScript 官方提出的, 你可以选择适合你的方案。我的推荐是尽量使用 @types 下的声明,实在没有,再使用第二种方法。</p><p>值得一提的是,并不是所有的包都可以通过这种方式解决的, 能解决的是 DefinitelyTyped 组织已经写好定义的包, 好消息是比较流行的包基本都有。 如果你想查一个包是否在 @type 下,可以访问 <a href="https://link.segmentfault.com/?enc=3zyeFVCUp%2BVnS73MoJ2O7w%3D%3D.7lvgv5iZTuEyBjFTjW42yPTKgqEnaTSzeiVOnaFbLWhpAsjQvEZb72Nkdl3SBG5G" rel="nofollow">https://microsoft.github.io/T...</a></p><p>那么 TypeScript 是怎么找定义的,什么情况会找不到定义而报类似上面举的例子的错误,这里简单介绍下原理。</p><h2>包类型定义的查找</h2><p>就好像 node 的包查找是先在当前文件夹找 node_modules,在它下找递归找,如果找不到则往上层目录继续找,直到顶部一样, TypeScript 类型查找也是类似的方式。</p><p>具体来说就是:</p><ul><li>TypeScript 编译器先在当前编译上下文找 jquery 的定义。</li><li>如果找不到,则会去 node_modules 中的@types (默认情况,目录可以修改,后面会提到)目录下去寻找对应包名的模块声明文件。</li></ul><blockquote><code>@types/*</code>模块声明文件由社区维护,通过发布到@types 空间下。 <a href="https://link.segmentfault.com/?enc=ZWDn%2F1uq5vcwQxHkk6q7og%3D%3D.4zTh26Wg5qViTD8Kt1MZ%2Fd2GVLCvR27QLUm83JbaqFsmYKK%2FCYVmEU%2FThKPG%2Frm6hksNHfwIZjkEjJHTCkzHaw%3D%3D" rel="nofollow">GitHub - DefinitelyTyped/DefinitelyTyped: The repository for high quality TypeScript type definitions.</a></blockquote><h2>变量类型定义的查找</h2><p>和包查找类似,默认情况下变量类型定义的查找也会去 @types 下去寻找。只不过并不是直接去 @types 找,而是有一定的优先级, 这个过程类似原型链或者作用域链。</p><p>比如如下代码:</p><pre><code class="ts">const user: User = { name: "lucifer" };</code></pre><ul><li>Typescript 则会先在本模块查找 User 的定义。</li><li>如果找到,则直接返回。 如果找不到, 则会到全局作用域找,而这个全局默认就是指的就是 @types 下的所有类型定义。(注意目录页是可以配的)</li></ul><blockquote>也就是说 @types 下的定义都是全局的。当然你可以导入 @types 下导出的定义,使得它们的作用域变成你的模块内部。</blockquote><h2>typeRoots 与 types</h2><p>前面说了 TypeScript 会默认引入<code>node_modules</code>下的所有<code>@types</code>声明,但是开发者也可以通过修改<code>tsconfig.json</code>的配置来修改默认的行为.</p><p>tsconfig.json 中有两个配置和类型引入有关。</p><ol><li><code>typeRoots</code>: 用来指定默认的类型声明文件查找路径,默认为<code>node_modules/@types</code>, 指定<code>typeRoots</code>后,TypeScript 编译器会从指定的路径去引入声明文件,而不是<code>node_modules/@types</code>, 比如以下配置会从<code>typings</code>路径下去搜索声明</li></ol><pre><code class="json">{
"compilerOptions": {
"typeRoots": ["./typings"]
}
}</code></pre><ol><li><code>types</code>: TypeScript 编译器会默认引入<code>typeRoot</code>下所有的声明文件,但是有时候我们并<strong><em>不希望全局引入所有定义</em></strong>,而是仅引入部分模块。这种情景下可以通过<code>types</code>指定模块名只引入我们想要的模块,比如以下只会引入 jquery 的声明文件</li></ol><pre><code class="json">{
"compilerOptions": {
"types": ["jquery"]
}
}</code></pre><h2>总结</h2><ol><li>typeRoots 是 tsconfig 中 compilerOptions 的一个配置项,typeRoots 下面的包会被 ts 编译器自动包含进来,typeRoots 默认指向 node_modules/@types。</li><li>@types 是 npm 的 scope 命名空间,和@babel 类似,@types 下的所有包会默认被引入,你可以通过修改 compilerOptions 来修改默认策略。</li><li>types 和 typeRoots 一样也是 compilerOptions 的配置,指定 types 后,typeRoots 下只有被指定的包才会被引入。</li></ol><h2>参考</h2><ul><li><a href="https://link.segmentfault.com/?enc=ovhjeNlBvDwjmiM9OZbV2g%3D%3D.vnHR5Px6riUrpOwtn9JQ25DO5UdXiEqGnenFzhAnYVQi3o3tOTf73r1Q1FMFRtYpEuFxP%2FDtD24Gyx%2BAeKUuig%3D%3D" rel="nofollow">GitHub - DefinitelyTyped/DefinitelyTyped: The repository for high quality TypeScript type definitions.</a></li><li><a href="https://link.segmentfault.com/?enc=tpLiY0j%2BmiJUhsIXCmMWyQ%3D%3D.KKn%2BpqkWUp6Mox84vYrkkHlRENyyKIPfu8KFduHuPkYkMgc%2BAuotQl7aCMaDvYWSsWpvcwNWbTdLqvIYbawh834Ev4mYjrbjCra1RDh4qjg%3D" rel="nofollow">@types | 深入理解 TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=zzWjbFMu%2FVQrmvPhr3HOdQ%3D%3D.4r1KIq4YYl3tAbehGngsw6i%2FghkcjljpEBTdyPiWEOwuSSCcR4F0pfRFa2UKYpp78ylVYyOEymXxtFfg%2FtSslQ%3D%3D" rel="nofollow">tsconfig.json · TypeScript 中文网 · TypeScript——JavaScript 的超集</a></li><li><a href="https://segmentfault.com/a/1190000013514680">理解 Typescript 配置文件 - 个人文章 - SegmentFault 思否</a></li></ul><h2>关注我</h2><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p><p><img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gfxro1x125j30oz0dw43s.jpg" alt="" title=""></p><p>公众号【 <a href="https://link.segmentfault.com/?enc=cegnY4TnVAaEBpLpXXkbpg%3D%3D.ziQGpGULnBsksSHWCU%2B8hPp75252CRgjq5yZzje%2Ff43s%2Bbhhvtr8CGXs0of0ops26BIAn2SyljWWp0NC3HyZ3NmQbJCCi7QqULuXk%2BtEn9E%3D" rel="nofollow">力扣加加</a>】<br>知乎专栏【 <a href="https://link.segmentfault.com/?enc=TkYUHXKRhYZTzd%2BQLqWQvg%3D%3D.vHrYdzLVijsc0rYhugZryoJQrqNRWWF42S83%2FawIhe8q28pd99lfW06IdGTIuo0X" rel="nofollow">Lucifer - 知乎</a>】</p><p>点关注,不迷路!</p>
TypeScript 类型系统
https://segmentfault.com/a/1190000023647609
2020-08-16T16:47:31+08:00
2020-08-16T16:47:31+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
18
<p>TypeScript 的学习资料非常多,其中也不乏很多优秀的文章和教程。但是目前为止没有一个我特别满意的。原因有:</p><ul><li>它们大多数没有一个清晰的主线,而是按照 API 组织章节的,内容在<strong>逻辑上</strong>比较零散。</li><li>大多是“讲是什么,怎么用“,而不是”讲为什么,讲原理“。</li><li>大多数内容比较枯燥,趣味性比较低。都是干巴巴的文字,没有图片,缺乏能够引起强烈共鸣的例子。</li></ul><p>因此我的想法是做一套不同市面上大多数的 TypeScript 学习教程。以人类认知的角度思考问题,学习 TypeScript,通过通俗易懂的例子和图片来帮助大家建立 TypeScript 世界观。 而本篇文章则是这个系列的开篇。</p><p>系列安排:</p><ul><li><a href="https://link.segmentfault.com/?enc=9MsPXJWQBipCXeFpItBMhQ%3D%3D.tse5wRVo06wN3DPPT8WUTIx%2BThtSeLwOb3AwSbK3XYdMR3Y5IiJ1vYsBRw6W6ukIlpJDGh7fVHp%2BJB2HbuRWDw%3D%3D" rel="nofollow">上帝视角看 TypeScript</a>(已发布)</li><li>TypeScript 类型系统(就是本文)</li><li>types 和 @types 是什么?</li><li><a href="https://link.segmentfault.com/?enc=8WC4avshvQbwQGaE5AA05A%3D%3D.kjkiROyWJbutrukdv1mHik9mCEUDI2VqUIAodKm5yiH6oghBZUsE2aItllMYF4M%2Fk3P%2FgXQJzAmLTsmfhGSerw%3D%3D" rel="nofollow">你不知道的 TypeScript 泛型(万字长文,建议收藏)</a>(已发布)</li><li>TypeScript 配置文件该怎么写?</li><li>TypeScript 是如何与 React,Vue,Webpack 集成的?</li><li>TypeScript 练习题</li></ul><blockquote>目录将来可能会有所调整。</blockquote><p>注意,我的系列文章基本不会讲 API,因此需要你有一定的 TypeScript 使用基础,推荐两个学习资料。</p><ul><li><a href="https://link.segmentfault.com/?enc=LmxP%2Fn97NfZ10EoBhDF2HQ%3D%3D.2oeeLs6hicG5qemj6gSU9sEEol%2FzNCKTrwRsbVaFr%2FO2hGd5PI3vpCE0DYs7MZIgKwB9yNs0Q6RbZCrR3IOTDA%3D%3D" rel="nofollow">深入理解 TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=LPhA2mtZFT6unL8t4C8Shg%3D%3D.eGc5GCD6N5V1QG1XJc%2Bki59VnFPWRnTBQBrUJUcQRDZoju4OmWv9JuG1Dna75FLO" rel="nofollow">官方文档</a></li></ul><p>结合这两个资料和我的系列教程,掌握 TypeScript 指日可待。</p><p>接下来,我们通过几个方面来从宏观的角度来看一下 TypeScript。</p><p><!-- more --></p><h2>前言</h2><p>上一节的<a href="https://link.segmentfault.com/?enc=0l2K1MzjZAuWO91E%2FlQARQ%3D%3D.s0BN%2ByT7s%2FWddYfgFe2jPpTI45A5kJr4ZcCZddu0lCI5CKWWHmb8bO2FYWVY5tNulKxfolB%2FCt47H9qMbPCBvw%3D%3D" rel="nofollow">上帝视角看 TypeScript</a>,我们从宏观的角度来对 Typescript 进行了一个展望。之所以把那个放到开头讲是让大家有一个大体的认识,不想让大家一叶障目。当你对整个宏观层面有了一定的了解,那么对 Typescript 的理解就不会错太多。相反,一开始就是具体的概念和 API,则很可能会让你丧失都整体的基本判断。</p><p>实际上, Typescript 一直在不断更新迭代。一方面是因为当初许下的诺言”Typescript 是 JavaScript 的超集“(JavaScript 的特性你要同步支持,同时也要处理各种新语法带来的不兼容情况)。不单是 ECMA,社区的其他发展可能也会让 Typescript 很难受。 比如 JSX 的广泛使用就给 Typescript 泛型的使用带来了影响。</p><p>TypeScript 一直处于高速的迭代。除了修复日常的 bug 之外,TypeScript 也在不断发布新的功能,比如最新 <a href="https://link.segmentfault.com/?enc=IIJJChQFJDZmcm6f49ZUFA%3D%3D.r%2BvRe1eXCsjdiyJ5duORGAieEinCYO7%2FIg4o6RDcfwHhFeH4hNdIiOokI8Fech7OGsPRhHHZTczEMpHqGuPPVcLtjDLH4cxIIxjaidE5bVIPDyzkGKRjHYKLmF05mR8i" rel="nofollow">4.0.0 beta 版本的<strong>标签元祖</strong></a> 的功能就对智能提示这块很有用。Typescript 在社区发展方面也做的格外好,以至于它的竞争对手 Flow 被 Typescript 完美击败,这在很大程度上就是因为 Typescript 没有烂尾。如今微软在开源方向的发力是越来越显著了,我很期待微软接下来的表现,让我们拭目以待。</p><p><img src="/img/remote/1460000023647613" alt="" title=""></p><h2>变量类型和值类型</h2><p>有的同学可能有疑问, JavaScript 不是也有类型么? 它和 Typescript 的类型是一回事么?JavaScript 不是动态语言么,那么经过 Typescript 的限定会不会丧失动态语言的动态性呢?我们继续往下看。</p><p><img src="/img/remote/1460000023647612" alt="" title=""></p><ul><li>JavaScript 中的类型其实是值的类型。实际上不仅仅是 JavaScript,任何动态类型语言都是如此,这也是动态类型语言的本质。</li><li>Typescript 中的类型其实是变量的类型。实际上不仅仅是 Typescript,任何静态类型语言都是如此,这也是静态类型语言的本质。</li></ul><p>记住这两句话,我们接下来解释一下这两句话。</p><p>对于 JavaScript 来说,一个变量可以是任意类型。</p><pre><code class="js">var a = 1;
a = "lucifer";
a = {};
a = [];</code></pre><p>上面的值是有类型的。比如 1 是 number 类型,"lucifer" 是字符串类型, {} 是对象类型, [] 是数组类型。而变量 a 是没有固定类型的。</p><p><img src="/img/remote/1460000023647615" alt="" title=""></p><p>对于 Typescript 来说, 一个变量只能接受和它类型兼容的类型的值。说起来比较拗口, 看个例子就明白了。</p><pre><code class="ts">var a: number = 1;
a = "lucifer"; // error
var b: any = 1;
a = "lucifer"; // ok
a = {}; // ok
a = []; // ok</code></pre><p>我们不能将 string 类型的值赋值给变量 a, 因为 string 和 number 类型不兼容。而我们可以将 string,Object,Array 类型的值赋值给 b,因此 它们和 any 类型兼容。简单来说就是,一旦一个变量被标注了某种类型,那么其就只能接受这个类型以及它的子类型。</p><p><img src="/img/remote/1460000023647614" alt="" title=""></p><h2>类型空间和值空间</h2><p>类型和值居住在不同的空间,一个在阳间一个在阴间。他们之间互相不能访问,甚至不知道彼此的存在。类型不能当做值来用,反之亦然。</p><p><img src="/img/remote/1460000023647617" alt="" title=""></p><h3>类型空间</h3><p>如下代码会报类型找不到的错:</p><pre><code class="ts">const aa: User = { name: "lucifer", age: 17 };</code></pre><p>这个比较好理解,我们只需要使用 interface 声明一下 User 就行。</p><pre><code class="ts">interface User {
name: string;
age: number;
}
const aa: User = { name: "lucifer", age: 17 };</code></pre><p>也就是说使用 interface 可以在类型空间声明一个类型,这个是 Typescript 的类型检查的基础之一。</p><p>实际上类型空间内部也会有子空间。我们可以用 namespace(老)和 module(新) 来创建新的子空间。子空间之间不能直接接触,需要依赖导入导出来交互。</p><h3>值空间</h3><p>比如,我用 Typescript 写出如下的代码:</p><pre><code class="ts">const a = window.lucifer();</code></pre><p>Typescript 会报告一个类似<code>Property 'lucifer' does not exist on type 'Window & typeof globalThis'.</code> 的错误。</p><p>实际上,这种错误并不是类型错误,而是找不到成员变量的错误。我们可以这样解决:</p><pre><code class="ts">declare var lucifer: () => any;</code></pre><p>也就是说使用 declare 可以在值空间声明一个变量。这个是 Typescript 的变量检查的基础,不是本文要讲的主要内容,大家知道就行。</p><p>明白了 JavaScript 和 TypeScript 类型的区别和联系之后,我们就可以来进入我们本文的主题了:<strong>类型系统</strong>。</p><h2>类型系统是 TypeScript 最主要的功能</h2><p>TypeScript 官方描述中有一句:<strong>TypeScript adds optional types to JavaScript that support tools for large-scale JavaScript applications</strong>。实际上这也正是 Typescript 的主要功能,即给 JavaScript 添加静态类型检查。要想实现静态类型检查,首先就要有类型系统。总之,我们使用 Typescript 的主要目的仍然是要它的静态类型检查,帮助我们提供代码的扩展性和可维护性。因此 Typescript 需要维护一套完整的类型系统。</p><p><strong>类型系统包括 1. 类型 和 2.对类型的使用和操作</strong>,我们先来看类型。</p><h3>类型</h3><p>TypeScript 支持 JavaScript 中所有的类型,并且还支持一些 JavaScript 中没有的类型(毕竟是超集嘛)。没有的类型可以直接提供,也可以提供自定义能力让用户来自己创造。 那为什么要增加 JavaScript 中没有的类型呢?我举个例子,比如如下给一个变量声明类型为 Object,Array 的代码。</p><pre><code class="ts">const a: Object = {};
const b: Array = [];</code></pre><p>其中:</p><ul><li>第一行代码 Typescript 允许,但是太宽泛了,我们很难得到有用的信息,推荐的做法是使用 interface 来描述,这个后面会讲到。</li><li>第二行 Typescript 则会直接报错,原因的本质也是太宽泛,我们需要使用泛型来进一步约束。</li></ul><h3>对类型的使用和操作</h3><p>上面说了<strong>类型和值居住在不同的空间,一个在阳间一个在阴间。他们之间互相不能访问,甚至不知道彼此的存在。</strong></p><p>使用 declare 和 interface or type 就是分别在两个空间编程。比如 Typescript 的泛型就是在类型空间编程,叫做类型编程。除了泛型,还有集合运算,一些操作符比如 keyof 等。值的编程在 Typescript 中更多的体现是在类似 lib.d.ts 这样的库。当然 lib.d.ts 也会在类型空间定义各种内置类型。我们没有必要去死扣这个,只需要了解即可。</p><p>lib.d.ts 的内容主要是一些变量声明(如:window、document、math)和一些类似的接口声明(如:Window、Document、Math)。寻找代码类型(如:Math.floor)的最简单方式是使用 IDE 的 F12(跳转到定义)。</p><h2>类型是如何做到静态类型检查的?</h2><p>TypeScript 要想解决 JavaScript 动态语言类型太宽松的问题,就需要:</p><ol><li>提供给<strong>变量</strong>设定类型的能力</li></ol><blockquote>注意是变量,不是值。</blockquote><ol><li>提供常用类型(不必须,但是没有用户体验会极差)并可以扩展出自定义类型(必须)。</li><li>根据第一步给变量设定的类型进行类型检查,即不允许类型不兼容的赋值, 不允许使用值空间和类型空间不存在的变量和类型等。</li></ol><p>第一个点是通过类型注解的语法来完成。即类似这样:</p><pre><code class="ts">const a: number = 1;</code></pre><blockquote>Typescript 的类型注解是这样, Java 的类型注解是另一个样子,Java 类似 int a = 1。 这个只是语法差异而已,作用是一样的。</blockquote><p>第二个问题, Typescript 提供了诸如 lib.d.ts 等类型库文件。随着 ES 的不断更新, JavaScript 类型和全局变量会逐渐变多。Typescript 也是采用这种 lib 的方式来解决的。</p><p><img src="/img/remote/1460000023647616" alt="" title=""></p><p>(TypeScript 提供的部分 lib)</p><p>第三个问题,Typescript 主要是通过 interface,type,函数类型等打通<strong>类型空间</strong>,通过 declare 等打通<strong>值空间</strong>,并结合 binder 来进行类型诊断。关于 checker ,binder 是如何运作的,可以参考我第一篇的介绍。</p><p>接下来,我们介绍类型系统的功能,即它能为我们带来什么。如果上面的内容你已经懂了,那么接下来的内容会让你感到”你也不过如此嘛“。</p><h2>类型系统的主要功能</h2><ol><li>定义类型以及其上的属性和方法。</li></ol><p>比如定义 String 类型, 以及其原型上的方法和属性。</p><p><img src="/img/remote/1460000023647618" alt="" title=""></p><p>length, includes 以及 toString 是 String 的<strong>成员变量</strong>, 生活在值空间, 值空间虽然不能直接和类型空间接触,但是类型空间可以作用在值空间,从而给其添加类型(如上图黄色部分)。</p><ol><li>提供自定义类型的能力</li></ol><pre><code class="ts">interface User {
name: string;
age: number;
say(name: string): string;
}</code></pre><p>这个是我自定义的类型 User,这是 Typescript 必须提供的能力。</p><ol><li>类型兼容体系。</li></ol><p>这个主要是用来判断类型是否正确的,上面我已经提过了,这里就不赘述了。</p><ol><li>类型推导</li></ol><p>有时候你不需要显式说明类型(类型注解),Typescript 也能知道他的类型,这就是类型推导结果。</p><pre><code class="ts">const a = 1;</code></pre><p>如上代码,编译器会自动推导出 a 的类型 为 number。还可以有连锁推导,泛型的入参(泛型的入参是类型)推导等。类型推导还有一个特别有用的地方,就是用到类型收敛。</p><p>接下来我们详细了解下类型推导和类型收敛。</p><h2>类型推导和类型收敛</h2><pre><code class="ts">let a = 1;</code></pre><p>如上代码。 Typescript 会推导出 a 的类型为 number。</p><p>如果只会你这么写就会报错:</p><pre><code class="ts">a = "1";</code></pre><p>因此 string 类型的值不能赋值给 number 类型的变量。我们可以使用 Typescript 内置的 typeof 关键字来证明一下。</p><pre><code class="ts">let a = 1;
type A = typeof a;</code></pre><p>此时 A 的类型就是 number,证明了变量 a 的类型确实被隐式推导成了 number 类型。</p><p>有意思的是如果 a 使用 const 声明,那么 a 不会被推导为 number,而是推导为类型 1。即<strong>值只能为 1 的类型</strong>,这就是类型收敛。</p><pre><code class="ts">const a = 1;
type A = typeof a;</code></pre><blockquote>通过 const ,我们将 number 类型收缩到了 <strong>值只能为 1 的类型</strong>。</blockquote><p>实际情况的类型推导和类型收敛要远比这个复杂, 但是做的事情都是一致的。</p><p>比如这个:</p><pre><code class="ts">function test(a: number, b: number) {
return a + b;
}
type A = ReturnType<typeof test>;</code></pre><p>A 就是 number 类型。 也就是 Typescript 知道两个 number 相加结果也是一个 number。因此即使你不显示地注明返回值是 number, Typescript 也能猜到。<strong>这也是为什么 JavaScript 项目不接入 Typescript 也可以获得类型提示的原因之一</strong>。</p><p>除了 const 可以收缩类型, typeof, instanceof 都也可以。 原因很简单,就是<strong>Typescript 在这个时候可以 100% 确定你的类型了</strong>。 我来解释一下:</p><p>比如上面的 const ,由于你是用 const 声明的,因此 100% 不会变,一定永远是 1,因此类型可以收缩为 1。 再比如:</p><pre><code class="ts">let a: number | string = 1;
a = "1";
if (typeof a === "string") {
a.includes;
}</code></pre><p>if 语句内 a 100% 是 string ,不能是 number。因此 if 语句内类型会被收缩为 string。instanceof 也是类似,原理一模一样。大家只要记住<strong>Typescript 如果可以 100% 确定你的类型,并且这个类型要比你定义的或者 Typescript 自动推导的范围更小,那么就会发生类型收缩</strong>就行了。</p><h2>总结</h2><p>本文主要讲了 Typescript 的类型系统。 Typescript 和 JavaScript 的类型是很不一样的。从表面上来看, TypeScript 的类型是 JavaScript 类型的超集。但是从更深层次上来说,两者的本质是不一样的,一个是值的类型,一个是变量的类型。</p><p>Typescript 空间分为值空间和类型空间。两个空间不互通,因此值不能当成类型,类型不能当成值,并且值和类型不能做运算等。不过 TypeScript 可以将两者结合起来用,这个能力只有 TypeScript 有, 作为 TypeScript 的开发者的你没有这个能力,这个我在第一节也简单介绍了。</p><p>TypeScript 既会对变量存在与否进行检查,也会对变量类型进行兼容检查。因此 TypeScript 就需要定义一系列的类型,以及类型之间的兼容关系。默认情况,TypeScript 是没有任何类型和变量的,因此你使用 String 等都会报错。TypeScript 使用库文件来解决这个问题,最经典的就是 lib.d.ts。</p><p>TypeScript 已经做到了足够智能了,以至于你不需要写类型,它也能猜出来,这就是类型推导和类型收缩。当然 TypeScript 也有一些功能,我们觉得应该有,并且也是可以做到的功能空缺。但是我相信随着 TypeScript 的逐步迭代(截止本文发布,TypeScript 刚刚发布了 4.0.0 的 beta 版本),一定会越来越完善,用着越来越舒服的。</p><p>我们每个项目的需要是不一样的, 简单的基本类型肯定无法满足多样的项目需求,因此我们必须支持自定义类型,比如 interface, type 以及复杂一点的泛型。当然泛型很大程度上是为了减少样板代码而生的,和 interface , type 这种刚需不太一样。</p><p>有了各种各样的类型以及类型上的成员变量,以及成员变量的类型,再就加上类型的兼容关系,我们就可以做类型检查了,这就是 TypeScript 类型检查的基础。TypeScript 内部需要维护这样的一个关系,并对变量进行类型绑定,从而给开发者提供<strong>类型分析</strong>服务。</p><h2>关注我</h2><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p><p>公众号【 <a href="https://link.segmentfault.com/?enc=zuhte18zVGCWubQfQMn46g%3D%3D.alM9RJoQPhZyexgGXv9wmmYYeB966AUCqgTs77led3hAiT5r9VL6DpAiVRrEtgWeJj8ZIHBVOpXcqsx1c5lC%2FAIZ3uNdZy56wU83l0rKimc%3D" rel="nofollow">力扣加加</a>】<br>知乎专栏【 <a href="https://link.segmentfault.com/?enc=1IerhOc4VvvB2AbvbeNNmA%3D%3D.w%2BivmgmB0QQKafjfofGMWgG74WlvTauvJXYh%2BARDaXpDmPT7HP0gzn0YvTfSqu6O" rel="nofollow">Lucifer - 知乎</a>】</p><p>点关注,不迷路!</p>
Webkit 内核初探
https://segmentfault.com/a/1190000023588803
2020-08-12T10:00:45+08:00
2020-08-12T10:00:45+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
18
<ul><li>作者: 阿吉</li><li>校对&整理: lucifer</li></ul><p>当下浏览器内核主要有 Webkit、Blink 等。本文分析注意是自 2001 年 Webkit 从 KHTML 分离出去并开源后,各大浏览器厂商魔改 Webkit 的时期,这些魔改的内核最终以 Chromium 受众最多而脱颖而出。本文就以 Chromium 浏览器架构为基础,逐层探入进行剖析。</p><h2>引子</h2><p>这里以一个面试中最常见的题目<code>从 URL 输入到浏览器渲染页面发生了什么?</code>开始。</p><p>这个很常见的题目,涉及的知识非常广泛。大家可先从浏览器监听用户输入开始,浏览器解析 url 的部分,分析出应用层协议 是 HTTPS 还是 HTTP 来决定是否经过会话层 TLS 套接字,然后到 DNS 解析获取 IP,建立 TCP 套接字池 以及 TCP 三次握手,数据封装切片的过程,浏览器发送请求获取对应数据,如何解析 HTML,四次挥手等等等等。 这个回答理论上可以非常详细,远比我提到的多得多。</p><p>本文试图从浏览器获取资源开始探究 Webkit。如浏览器如何获取资源,获取资源时 Webkit 调用了哪些资源加载器(不同的资源使用不同的加载器),Webkit 如何解析 HTML 等入手。想要从前端工程师的角度弄明白这些问题,可以先暂时抛开 C++源码,从浏览器架构出发,做到大致了解。之后学有余力的同学再去深入研究各个底层细节。</p><p>本文的路线循序渐进,从 Chromium 浏览器架构出发,到 Webkit 资源下载时对应的浏览器获取对应资源如 HTML、CSS 等,再到 HTML 的解析,再到 JS 阻塞 DOM 解析而产生的 Webkit 优化 引出浏览器多线程架构,继而出于安全性和稳定性的考虑引出浏览器多进程架构。</p><h2>一. Chromium 浏览器架构</h2><p><img src="http://m.qpic.cn/psc?/V50vddMl0YvJ9c0IGUY913J7mG1rVs8G/bqQfVz5yrrGYSXMvKr.cqTMrUK10g4vRaHHV49*HvW0FoJTS4c2bz3tSzUqnlZb.vVmuaQiNqi3bk24Ey1ONmRFcv3mSLTbCQZ07nedhD9M!/b&bo=qAMaBAAAAAADB5c!&rf=viewer_4" alt="Chromium浏览器架构" title="Chromium浏览器架构"></p><p>(Chromium 浏览器架构)</p><p>我们通常说的浏览器内核,指的是渲染引擎。</p><p>WebCore 基本是共享的,只是在不同浏览器中使用 Webkit 的实现方式不同。它包含解析 HTML 生成 DOM、解析 CSS、渲染布局、资源加载器等等,用于加载和渲染网页。</p><p>JS 解析可以使用 JSCore 或 V8 等 JS 引擎。我们熟悉的谷歌浏览器就是使用 V8。比如比较常见的有内置属性 <code>[[scope]]</code> 就仅在 V8 内部使用,用于对象根据其向上索引自身不存在的属性。而对外暴露的 API,如 <code>__proto__</code> 也可用于更改原型链。实际上 <code>__proto__</code> 并不是 ES 标准提供的,它是浏览器提供的(浏览器可以不提供,因此如果有浏览器不提供的话这也并不是 b ug)。</p><p>Webkit Ports 是不共享的部分。它包含视频、音频、图片解码、硬件加速、网络栈等等,常用于移植。</p><p>同时,浏览器是多进程多线程架构,稍后也会细入。</p><p>在解析 HTML 文档之前,需要先获取资源,那么资源的获取在 Webkit 中应该如何进行呢?</p><h2>二.Webkit 资源加载</h2><p>HTTP 是超文本传输协议,超文本的含义即包含了文本、图片、视频、音频等等。其对应的不同文件格式,在 Webkit 中 需要调用不同的资源加载器,即 特定资源加载器。</p><p>而浏览器有四级缓存,Disk Cache 是我们最常说的通过 HTTP Header 去控制的,比如强缓存、协商缓存。同时也有浏览器自带的启发式缓存。而 Webkit 对应使用的加载器是资源缓存机制的资源加载器 <code>CachedResoureLoader</code> 类。</p><p>如果每个资源加载器都实现自己的加载方法,则浪费内存空间,同时违背了单一职责的原则,因此可以抽象出一个共享类,即通用资源加载器 <code>ResoureLoader</code> 类。 Webkit 资源加载是使用了三类加载器:<strong>特定资源加载器,资源缓存机制的资源加载器 CachedResoureLoader 和 通用资源加载器 ResoureLoader</strong>。</p><p>既然说到了缓存,那不妨多谈一点。</p><p>资源既然缓存了,那是如何命中的呢?答案是根据资源唯一性的特征 URL。资源存储是有一定有效期的,而这个有效期在 Webkit 中采用的就是 LRU 算法。那什么时候更新缓存呢?答案是不同的缓存类型对应不同的缓存策略。我们知道缓存多数是利用 HTTP 协议减少网络负载的,即强缓存、协商缓存。但是如果关闭缓存了呢? 比如 HTTP/1.0 Pragma:no-cache 和 HTTP/1.1 Cache-Control: no-cache。此时,对于 Webkit 来说,它会清空全局唯一的对象 MemoryCache 中的所有资源。</p><p>资源加载器内容先到这里。浏览器架构是多进程多线程的,其实多线程可以直接体现在资源加载的过程中,在 JS 阻塞 DOM 解析中发挥作用,下面我们详细讲解一下。</p><h2>三.浏览器架构</h2><p>浏览器是多进程多线程架构。</p><p>对于浏览器来讲,从网络获取资源是非常耗时的。从资源是否阻塞渲染的角度,对浏览器而言资源仅分为两类:<strong>阻塞渲染</strong>如 JS 和 <strong>不阻塞渲染</strong>如图片。</p><p>我们都知道 JS 阻塞 DOM 解析,反之亦然。然而对于阻塞,Webkit 不会傻傻等着浪费时间,它在内部做了优化:启动另一个线程,去遍历后续的 HTML 文档,收集需要的资源 URL,并发下载资源。最常见的比如<code><script async></code>和<code><script defer></code>,其 JS 资源下载和 DOM 解析是并行的,JS 下载并不会阻塞 DOM 解析。这就是浏览器的多线程架构。</p><p><img src="http://a1.qpic.cn/psc?/V50vddMl0YvJ9c0IGUY913J7mG1rVs8G/bqQfVz5yrrGYSXMvKr.cqQYq655fTdqtV.7hiPLQGNK3lEmq2f9GXjc.RrNkpc2KrnPa34sYOqInvolRFQDCNcqbuGHTk4D5eqi6VWgzlNs!/c&ek=1&kp=1&pt=0&bo=EwMgAwAAAAADFwE!&tl=1&vuin=741183972&tm=1597122000&sce=60-2-2&rf=0-0" alt="JS async defer" title="JS async defer"></p><p>总结一下,多线程的好处就是,高响应度,UI 线程不会被耗时操作阻塞而完全阻塞浏览器进程。</p><p>关于多线程,有 GUI 渲染线程,负责解析 HTML、CSS、渲染和布局等等,调用 WebCore 的功能。JS 引擎线程,负责解析 JS 脚本,调用 JSCore 或 V8。我们都知道 JS 阻塞 DOM 解析,这是因为 Webkit 设计上 GUI 渲染线程和 JS 引擎线程的执行是互斥的。如果二者不互斥,假设 JS 引擎线程清空了 DOM 树,在 JS 引擎线程清空的过程中 GUI 渲染线程仍继续渲染页面,这就造成了资源的浪费。更严重的,还可能发生各种多线程问题,比如脏数据等。</p><p>另外我们常说的 JS 操作 DOM 消耗性能,其实有一部分指的就是 JS 引擎线程和 GUI 渲染线程之间的通信,线程之间比较消耗性能。</p><p>除此之外还有别的线程,比如事件触发线程,负责当一个事件被触发时将其添加到待处理队列的队尾。</p><p>值得注意的是,多启动的线程,仅仅是收集后续资源的 URL,线程并不会去下载资源。该线程会把下载的资源 URL 送给 Browser 进程,Browser 进程调用网络栈去下载对应的资源,返回资源交由 Renderer 进程进行渲染,Renderer 进程将最终的渲染结果返回 Browser 进程,由 Browser 进程进行最终呈现。这就是浏览器的多进程架构。</p><p>多进程加载资源的过程是如何的呢?我们上面说到的 HTML 文档在浏览器的渲染,是交由 Renderer 进程的。Renderer 进程在解析 HTML 的过程中,已搜集到所有的资源 URL,如 link CSS、Img src 等等。但出于安全性和效率的角度考虑,Renderer 进程并不能直接下载资源,它需要通过进程间通信将 URL 交由 Browser 进程,Browser 进程有权限调用 URLRequest 类从网络或本地获取资源。</p><blockquote>近年来,对于有的浏览器,网络栈由 Browser 进程中的一个模块,变成一个单独的进程。</blockquote><p>同时,多进程的好处远远不止安全这一项,即沙箱模型。还有单个网页或者第三方插件的崩溃,并不会影响到浏览器的稳定性。资源加载完成,对于 Webkit 而言,它需要调用 WebCore 对资源进行解析。那么我们先看下 HTML 的解析。之后我们再谈一下,对于浏览器来说,它拥有哪些进程呢?</p><h2>四.HTML 解析</h2><p>对于 Webkit 而言,将解析半结构化的 HTML 生成 DOM,但是对于 CSS 样式表的解析,严格意义 CSSOM 并不是树,而是一个映射表集合。我们可以通过 document.styleSheets 来获取样式表的有序集合来操作 CSSOM。对于 CSS,Webkit 也有对应的优化策略---ComputedStyle。ComputedStyle 就是如果多个元素的样式可以不经过计算就确认相等,那么就仅会进行一次样式计算,其余元素仅共享该 ComputedStyle。</p><p>共享 ComputedStyle 原则:</p><p>(1) TagName 和 Class 属性必须一样。</p><p>(2)不能有 Style。</p><p>(3)不能有 sibling selector。</p><p>(4)mappedAttribute 必须相等。</p><p>对于 DOM 和 CSSOM,大家说的合成的 render 树在 Webkit 而言是不存在的,在 Webkit 内部生成的是 RenderObject,在它的节点在创建的同时,会根据层次结构创建 RenderLayer 树,同时构建一个虚拟的绘图上下文,生成可视化图像。这四个内部表示结构会一直存在,直到网页被销毁。</p><p>RenderLayer 在浏览器控制台中 Layers 功能卡中可以看到当前网页的图层分层。图层涉及到显式和隐式,如 scale()、z-index 等。层的优点之一是只重绘当前层而不影响其他层,这也是 Webkit 做的优化之一。同时 V8 引擎也做了一些优化,比如说隐藏类、优化回退、内联缓存等等。</p><h2>五.浏览器进程</h2><p>浏览器进程包括 <strong>Browser 进程、Renderer 进程、GPU 进程、NPAPI 插件进程、Pepper 进程</strong>等等。下面让我们详细看看各大进程。</p><ul><li>Browser 进程:浏览器的主进程,有且仅有一个,它是进程祖先。负责页面的显示和管理、其他进程的管理。</li><li>Renderer 进程:网页的渲染进程,可有多个,和网页数量不一定是一一对应关系。它负责网页的渲染,Webkit 的渲染工作就是在这里完成的。</li><li>GPU 进程:最多一个。仅当 GPU 硬件加速被打开时创建。它负责 3D 绘制。</li><li>NPAPI 进程:为 NPAPI 类型的插件而创建。其创建的基本原则是每种类型的插件都只会被创建一次,仅当使用时被创建,可被共享。</li><li>Pepper 进程:同 NPAPI 进程,不同的是 它为 Pepper 插件而创建的进程。</li></ul><blockquote>注意:如果页面有 iframe,它会形成影子节点,会运行在单独的进程中。</blockquote><p>我们仅仅在围绕 Chromium 浏览器来说上述进程,因为在移动端,毕竟手机厂商很多,各大厂商对浏览器进程的支持也不一样。这其实也是我们最常见的 H5 兼容性问题,比如 IOS <code>margin-bottom</code> 失效等等。再比如 H5 使用 video 标签做直播,也在不同手机之间会存在问题。有的手机直播页面跳出主进程再回来,就会黑屏。</p><p>以 Chromium 的 Android 版为例子,不存在 GPU 进程,GPU 进程变成了 Browser 进程的线程。同时,Renderer 进程演变为服务进程,同时被限制了最大数量。</p><p>为了方便起见,我们以 PC 端谷歌浏览器为例子,打开任务管理器,查看当前浏览器中打开的网页及其进程。</p><p><img src="http://a1.qpic.cn/psc?/V50vddMl0YvJ9c0IGUY913J7mG1rVs8G/bqQfVz5yrrGYSXMvKr.cqbiyEougc0AL0QEl3PZdg8VoMc3IvtAbJcZAPdlr61RytWnxdKgGug9CJ1x5zgjBdRtUb1h3Ve5FOlicgiswBqg!/c&ek=1&kp=1&pt=0&bo=rQU4BAAAAAADN4Y!&tl=1&vuin=741183972&tm=1597118400&sce=60-2-2&rf=0-0" alt="打开浏览器任务管理器" title="打开浏览器任务管理器"></p><p>当前我打开了 14 个网页,不太好容易观察,但可以从下图中看到,只有一个 Browser 进程,即第 1 行。但是打开的网页对应的 Renderer 进程,并不一定是一个网页对应一个 Renderer 进程,这跟 Renderer 进程配置有关系。比如你看第 6、7 行是每个标签页创建独立 Renderer 进程,但是蓝色光标所在的第 8、9、10 行是共用一个 Renderer 进程,这属于为每个页面创建一个 Renderer 进程。因为第 9、10 行打开的页面是从第 8 行点击链接打开的。第 2 行的 GPU 进程也清晰可见,以及第 3、4、5 行的插件进程。</p><p><img src="http://a1.qpic.cn/psc?/V50vddMl0YvJ9c0IGUY913J7mG1rVs8G/bqQfVz5yrrGYSXMvKr.cqUONLiXR1HrQh.acB9iAV0PCQsnilSowX4VUd2fTJ1890e2KO4dg5OnmEryV3jxZP7N3ZkaTys7CU0rlYm3mHq8!/c&ek=1&kp=1&pt=0&bo=zAMYAwAAAAADJ9Y!&tl=1&vuin=741183972&tm=1597118400&sce=60-2-2&rf=0-0" alt="浏览器进程" title="浏览器进程"></p><p>关于,Renderer 进程和打开的网页并不一定是一一对应的关系,下面我们详细说一下 Renderer 进程。当前只有四种多进程策略:</p><ol><li>Process-per-site-instance: 为每个页面单独创建一个进程,从某个网站打开的一系列网站都属于同一个进程。这是浏览器的默认项。上图中的蓝色光标就是这种情况。</li><li>Process-per-site:同一个域的页面共享一个进程。</li><li>Process-per-tab:为每个标签页创建一个独立的进程。比如上图第 6、7 行。</li><li>Single process:所有的渲染工作作为多个线程都在 Browser 进程中进行。这个基本不会用到的。</li></ol><p>Single process 突然让我联想到零几年的时候,那会 IE 应该还是单进程浏览器。单进程就是指所有的功能模块全部运行在一个进程,就类似于 Single process。那会玩 4399 如果一个网页卡死了,没响应,点关闭等一会,整个浏览器就崩溃了,得重新打开。所以多进程架构是有利于浏览器的稳定性的。虽然当下浏览器架构为多进程架构,但如果 Renderer 进程配置为 Process-per-site-instance,也可能会出现由于单个页面卡死而导致所有页面崩溃的情况。</p><p>故浏览器多进程架构综上所述,好处有三:</p><p>(1)单个网页的崩溃不会影响这个浏览器的稳定性。</p><p>(2)第三方插件的崩溃不会影响浏览器的稳定性。</p><p>(3)沙箱模型提供了安全保障。</p><h2>总结</h2><p>Webkit 使用三类资源加载器去下载对应的资源,并存入缓存池中,对于 HTML 文档的解析,在阻塞时调用另一个线程去收集后续资源的 URL,将其发送给 Browser 进程,Browser 进程调用网络栈去下载对应的本地或网络资源,返回给 Renderer 进程进行渲染,Renderer 进程将最终渲染结果(一系列的合成帧)发送给 Browser 进程,Browser 进程将这些合成帧发送给 GPU 从而显示在屏幕上。<br>(文中有部分不严谨的地方,已由 lucifer 指出修改)</p><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p>
上帝视角看 TypeScript
https://segmentfault.com/a/1190000023489694
2020-08-04T12:06:13+08:00
2020-08-04T12:06:13+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
28
<p>TypeScript 的学习资料非常多,其中也不乏很多优秀的文章和教程。但是目前为止没有一个我特别满意的。原因有:</p><ul><li>它们大多数没有一个清晰的主线,而是按照 API 组织章节的,内容在<strong>逻辑上</strong>比较零散。</li><li>大多是“讲是什么,怎么用“,而不是”讲为什么,讲原理“。</li><li>大多数内容比较枯燥,趣味性比较低。都是干巴巴的文字,没有图片,缺乏能够引起强烈共鸣的例子。</li></ul><p>因此我的想法是做一套不同市面上大多数的 TypeScript 学习教程。以人类认知的角度思考问题,学习 TypeScript,通过通俗易懂的例子和图片来帮助大家建立 TypeScript 世界观。 而本篇文章则是这个系列的开篇。</p><p>系列安排:</p><ul><li>上帝视角看 TypeScript(就是本文)</li><li>TypeScript 类型系统</li><li>什么是 types?什么是 @types?</li><li>类型推导, 类型断言与类型保护</li><li><a href="https://link.segmentfault.com/?enc=bDfBOtgTXEmcKetOe6VvAg%3D%3D.UMTD1I5oucMYbV6fd7cV95emYke4FSSJfNRbhs36uXAFtNNY5C6Nosh7Lt8f4IParhV%2F0LS3lSHH2wYt50MDRA%3D%3D" rel="nofollow">你不知道的 TypeScript 泛型(万字长文,建议收藏)</a>(已发布)</li><li>TypeScript 练习题</li><li>TypeScript 配置文件该怎么写?</li><li>TypeScript 是如何与 React,Vue,Webpack 集成的?</li></ul><blockquote>目录将来可能会有所调整。</blockquote><p>注意,我的系列文章基本不会讲 API,因此需要你有一定的 TypeScript 使用基础,推荐两个学习资料。</p><ul><li><a href="https://link.segmentfault.com/?enc=9Oa3vqLCY%2F8bhjK3nh63rA%3D%3D.RUz6zB6jB3s5kbA7XLs0PW2MSgX838c6VIv6PTaIlmq6X4wPT%2BKHXlt1XDThpYqGQz%2Bi8sAQf34gPwTznCe%2FZw%3D%3D" rel="nofollow">深入理解 TypeScript</a></li><li><a href="https://link.segmentfault.com/?enc=RnfQUVOA7DuKKa5tRl19Rg%3D%3D.LgkmsO%2B%2BoZCbKgU5y%2FduAywGtY7oysHB4mW0ccYmqPTtXSL7Z6IFiERLq%2FGa1ekh" rel="nofollow">官方文档</a></li></ul><p>结合这两个资料和我的系列教程,掌握 TypeScript 指日可待。</p><p>接下来,我们通过几个方面来从宏观的角度来看一下 TypeScript。</p><h2>从输入输出上来看</h2><p>如果我们把 Typescript 编译器看成一个黑盒的话。其<strong>输入则是使用 TypeScript 语法书写的文本或者文本集合</strong>。</p><p><img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gh5l5pqyw1j304s04wwea.jpg" alt="" title=""></p><p>(文本)</p><p>如果几个文本有引用关系,比如 a.ts 依赖 foo.ts 和 bar.ts,其就是一个文本集合。</p><p><img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gh5l7apwnnj30ho09f74h.jpg" alt="" title=""></p><p>(文本集合)</p><p><strong>输出是编译之后的 JS 文件 和 .d.ts 的声明文件</strong>。</p><p><img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gh5ld0kfitj30ow0csdjs.jpg" alt="" title=""></p><p>其中 JS 是将来需要运行的文件,而 .d.ts 声明文件则是 ts 文件中的类型声明,<strong>这个类型声明就是你在 ts 文件中声明的类型和 TypeScript 类型推导系统推导的类型</strong>。当然你也可以自己写 .d.ts 声明文件。</p><h2>从功能上来看</h2><p>从宏观的视角来看,TypeScript 的功能就是:</p><ul><li>提供了丰富的类型系统。</li></ul><p>最简单的就是 变量名:类型 = 值</p><pre><code class="ts">const a: Number = 1;</code></pre><p>除了这些基本类型,还提供了函数类型,复合类型等。</p><ul><li>提供了类型操作 API。TypeScript 不但提供内置类型,用户也可以利用集合操作和泛型对类型操作从而生成新的类型。</li></ul><p><img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gh5lzqpqirj30d104ogml.jpg" alt="" title=""></p><ul><li>对每一种类型的属性和方法都进行了定义。</li></ul><p>比如 String 类型有 toString 方法,但是没有 toFixed 方法,这就是 lib.d.ts 定义的。这样我在 String 类型的变量上使用 toFixed 方法就会报错,达到了“类型检查”的作用。</p><blockquote>小提示:lib.d.ts 的内容主要是一些变量声明(如:window、document、math)和一些类似的接口声明(如:Window、Document、Math)。 你可以通过 --noLib 来关闭这一功能</blockquote><ul><li>提供了模块系统(module,namespace)。</li><li>提供了更加方面的 API,比如 class(这在 ES6 class 出来之前尤其好用),装饰器等。</li><li>。。。</li></ul><h2>TypeScript 编译器是如何工作的?</h2><p>上面已经讨论了 TypeScript 编译器的输入和输出。那黑盒内部是怎么工作呢?这里我简单介绍一下:</p><p><img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gh5nm8tmokj30dl02zq2s.jpg" alt="" title=""></p><ul><li>TypeScript 文本首先会被解析为 <strong>token 流</strong>。这个过程比较简单,就是单纯地按照分隔符去分割文本即可。</li></ul><p><img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gh5npflqbbj30eh0490sw.jpg" alt="" title=""></p><ul><li>接着 token 流会被转换为 AST,也就是<strong>抽象语法树</strong>。</li></ul><p><img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gh5nqa41bpj309106lt8z.jpg" alt="" title=""></p><ul><li>binder 则根据 AST 信息生成 <strong>Symbol</strong>(TypeScript 中的一个数据结构)。拿上面的图来说,就是 number 节点。</li><li>当我们需要类型检查的时候, checker 会根据<strong>前面生成的 AST 和 symbols 生成类型检查结果</strong>。</li><li>当我们需要生成 JS 文件的时候,emitter 同样会根据<strong>前面生成的 AST 和 symbols 生成 JS 文件</strong>。</li></ul><p>完整图:</p><p><img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gh5nfcui6sj30xz0gendl.jpg" alt="" title=""></p><h2>总结</h2><p>总的来说,TypeScript 就是一门语言,和 Java,Python,C++ 等类似。只不过这门语言主要目标就是为了弥补 JavaScript 弱类型带来的问题的。因此设计语言的出发点就是:</p><ul><li>静态类型系统</li><li>可以编译成 JavaScript</li></ul><p>因此 TypeScript 是一门最终编译为 JavaScript 的语言(当然还有类型文件)。既然是一门语言,就涉及词法分析,语法分析等流程。由于相对 JavaScript 增加了很多功能, 其中最主要的就是类型系统。因此 TypeScript 的分析工作要比 JavaScript 更加复杂, 集中体现在 binder 和 checker 部分。</p><p>由于提供了静态类型, 因此就需要提供一些内置类型给我们用,比如 number,string,Array 等。但是这并不能满足我们的所有需求,我们需要自定义类型,因此有了 type,有了 interface 等。后来我们又发现自定义的类型重复代码太多, 要是类型也可以通过编程生成新的类型就好了,于是有了集合运算和泛型。</p><p>代码都放到一起不方便维护,要是可以放到不同文件,需要用的时候组装起来就好了,于是有了模块化。我用了别人的用 TypeScript 开发的库,如果也能有类型校验就好了,于是有了 types。</p><p>。。。</p><p>其实这些都是有因果关系的,如果你可以牢牢地掌握这些因果关系,那么学起来还不是易如反掌?</p><h2>相关阅读</h2><ul><li><a href="https://link.segmentfault.com/?enc=sdqr762oMul4m3I1yj6V4g%3D%3D.hcFb9fZ8yfcih9G5a9%2FeLMwLXYeFNiedhumielLl8Cuq7fr3aCguDUqlyMB4MNBK9GfK3zkcMbdwqciSsRzqc4xQXc2do5rVYhxHvhX7ItI%3D" rel="nofollow">TypeScript 编译原理</a></li><li><a href="https://link.segmentfault.com/?enc=0yI4PkyBCYi8un4DPSju%2BA%3D%3D.%2ByRRRQfnwUU42MAWYGOCWkEtM3XX6Mcf0Bw7fklVcszXN8bzmrNjGeMWC28W%2B5%2FU" rel="nofollow">Bring your own TypeScript with more internal definitions</a></li><li><a href="https://link.segmentfault.com/?enc=D4gv83E7JQZ4LgyMxFmQGg%3D%3D.6u9uTXHqg9PdWvQf2%2F14xmJzw1nG1SX1p7fjo2eDQmpz2pU%2FMWVV13EPt1Ea0qFiQ%2B1NYKpB%2FJ8t4sqAtJ6I4w%3D%3D" rel="nofollow">Compiler Internals</a></li><li><a href="https://link.segmentfault.com/?enc=Oa4xIGsbQ%2B7X82mO8KH1FQ%3D%3D.nHAAYZKGxn%2FcqnBqBZtFqkPbqme2Z212SFaIo1k%2BC%2FQgzoZgeAAJ3bNK3Pt7WP2pQwmC7kAVd%2BeBuWmaWXJyVg%3D%3D" rel="nofollow">TypeScript 编译器是用 TypeScript 写的,那是先有编译器还是 TS?</a></li></ul><h2>点关注,不迷路</h2><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p><p>知乎专栏【 <a href="https://link.segmentfault.com/?enc=b5mo895xswLMgKaEUbOYkA%3D%3D.fG56T71n6aMOQETH5WkWcQOkyFfdhfdz3mlXYQVCE0uQ%2FhbITuly39awBlvUsqPE" rel="nofollow">Lucifer - 知乎</a>】</p>
你不知道的 TypeScript 泛型(万字长文,建议收藏)
https://segmentfault.com/a/1190000022993503
2020-06-22T11:53:12+08:00
2020-06-22T11:53:12+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
101
<p>泛型是 TypeScript(以下简称 TS) 比较高级的功能之一,理解起来也比较困难。泛型应用场景非常广泛,很多地方都能看到它的影子。平时我们阅读开源 TS 项目源码,或者在自己的 TS 项目中使用一些第三方库(比如 React)的时候,经常会看到各种泛型定义。如果你不是特别了解泛型,那么你很可能不仅不会用,不会实现,甚至看不懂这是在干什么。</p><p>相信大家都经历过,看到过,或者正在写<strong>一些应用,这些应用充斥着各种重复类型定义, any 类型层出不穷,鼠标移到变量上面的提示只有 any,不要说类型操作了,类型能写对都是个问题</strong>。我也经历过这样的阶段,那个时候我对 TS 还比较陌生。</p><p>随着在 TS 方面学习的深入,越来越认识到 <strong>真正的 TS 高手都是在玩类型</strong>,对类型进行各种运算生成新的类型。这也好理解,毕竟 <strong>TS 提供的其实就是类型系统</strong>。你去看那些 TS 高手的代码,会各种<strong>花式使用泛型</strong>。 可以说泛型是一道坎,只有真正掌握它,你才知道<strong>原来 TS 还可以这么玩</strong>。怪不得面试的时候大家都愿意问泛型,尽管面试官很可能也不怎么懂。</p><p><strong>只有理解事物的内在逻辑,才算真正掌握了,不然永远只是皮毛,不得其法</strong>。 本文就带你走进泛型,带你从另一个角度看看究竟什么是泛型,为什么要有它,它给 TS 带来了什么样的不同。</p><blockquote>注意:不同语言泛型略有不同,知识迁移虽然可以,但是不能生搬硬套,本文所讲的泛型都指的是 TS 下的泛型。</blockquote><h2>引言</h2><p>我总结了一下,学习 TS 有两个难点。第一个是<code>TS 和 JS 中容易混淆的写法</code>,第二个是<code>TS中特有的一些东西</code>。</p><ul><li>TS 中容易引起大家的混淆的写法</li></ul><p>比如:</p><p><img src="/img/remote/1460000022993508" alt="容易混淆的箭头函数" title="容易混淆的箭头函数"></p><p>(容易混淆的箭头函数)</p><p>再比如:</p><p><img src="/img/remote/1460000022993506" alt="" title=""></p><p>(容易混淆的 interface 内的小括号)</p><ul><li>TS 中特有的一些东西</li></ul><p>比如 typeof,keyof, infer 以及本文要讲的泛型。</p><p><strong>把这些和 JS 中容易混淆的东西分清楚,然后搞懂 TS 特有的东西,尤其是泛型</strong>(其他基本上相对简单),TS 就入门了。</p><h3>泛型初体验</h3><p>在强类型语言中,一般而言需要给变量指定类型才能使用该变量。如下代码:</p><pre><code class="ts">const name: string = "lucifer";
console.log(name);</code></pre><p>我们需要给 name 声明 string 类型,然后才能在后面使用 name 变量,当我们执行以下操作的时候会报错。</p><ul><li>给 name 赋其他类型的值</li><li>使用其他类型值特有的方法(比如 Number 类型特有的 toFixed)</li><li>将 name 以参数传给不支持 string 的函数。 比如 <code>divide(1, name)</code>,其中 divide 就是功能就是<code>将第一个数(number 类型)除以第二个数(number 类型),并将结果返回</code>。</li></ul><p>TS 除了提供一些基本类型(比如上面的 string)供我们直接使用。还:</p><ul><li>提供了 <code>inteface</code> 和 <code>type</code> 关键字供我们定义自己的类型,之后就能像使用基本类型一样使用自己定义的类型了。</li><li>提供了各种逻辑运算符,比如 &, | 等 ,供我们对类型进行操作,从而生成新的类型。</li><li>提供泛型,允许我们在定义的时候不具体指定类型,而是泛泛地说一种类型,并在函数调用的时候再指定具体的参数类型。</li><li>。。。</li></ul><p>也就是说泛型也是一种类型,只不过不同于 string, number 等具体的类型,它是一种抽象的类型,我们不能直接定义一个变量类型为泛型。</p><p>简单来说,区别于平时我们对<strong>值</strong>进行编程,泛型是对<strong>类型</strong>进行编程。这个听起来比较抽象。之后我们会通过若干实例带你理解这句话,你先留一个印象就好。</p><p>为了明白上面这句话,·首先要区分“值”和“类型”。</p><h3>值和类型</h3><p>我们平时写代码基本都是<strong>对值编程</strong>。比如:</p><pre><code class="js">if (person.isVIP) {
console.log('VIP')
}
if (cnt > 5) {
// do something
}
const personNames = persons.map(p => p.name)
...</code></pre><p>可以看出这都是对具体的值进行编程,<strong>这符合我们对现实世界的抽象</strong>。从集合论的角度上来说, 值的集合就是类型,在 TS 中最简单的用法是对值限定类型,从根本上来说是限定值的集合。这个集合可以是一个具体的集合,也可以是多个集合通过集合运算(交叉并)生成的新集合。</p><p><img src="/img/remote/1460000022993507" alt="" title=""></p><p>(值和类型)</p><p>再来看一个更具体的例子:</p><pre><code class="ts">function t(name: string) {
return `hello, ${name}`;
}
t("lucifer");</code></pre><p>字符串 "lucifer" 是 string <strong>类型</strong>的一个具体<strong>值</strong>。 在这里 "lucifer" 就是值,而 string 就是类型。</p><p>TS 明白 "lucifer" 是 string 集合中的一个元素,因此上面代码不会有问题,但是如果是这样就会报错:</p><pre><code class="ts">t(123);</code></pre><p>因为 123 并不是 string 集合中的一个元素。</p><p>对于 t("lucifer")而言,TS 判断逻辑的伪代码:</p><pre><code class="js">v = getValue(); // will return 'lucifer' by ast
if (typeof v === "string") {
// ok
} else {
throw "type error";
}</code></pre><blockquote>由于是静态类型分析工具,因此 TS 并不会执行 JS 代码,但并不是说 TS 内部没有执行逻辑。</blockquote><p>简单来总结一下就是: 值的集合就是类型,平时写代码基本都是对值编程,TS 提供了很多<strong>类型</strong>(也可以自定义)以及很多<strong>类型操作</strong>帮助我们<strong>限定值以及对值的操作</strong>。</p><h2>什么是泛型</h2><p>上面已经铺垫了一番,大家已经知道了值和类型的区别,以及 TS 究竟帮我们做了什么事情。但是直接理解泛型仍然会比较吃力,接下来我会通过若干实例,慢慢带大家走进泛型。</p><p>首先来思考一个问题:<code>为什么要有泛型呢</code>?这个原因实际上有很多,在这里我选择大家普遍认同的一个切入点来解释。如果你明白了这个点,其他点相对而言理解起来会比较轻松。还是通过一个例子来进行说明。</p><h3>不容小觑的 id 函数</h3><p>假如让你实现一个函数 <code>id</code>,函数的参数可以是任何值,返回值就是将参数原样返回,并且其只能接受一个参数,你会怎么做?</p><p>你会觉得这很简单,顺手就写出这样的代码:</p><pre><code class="js">const id = (arg) => arg;</code></pre><blockquote>有的人可能觉得 id 函数没有什么实际作用。其实不然, id 函数在函数式编程中应用非常广泛。</blockquote><p>由于其可以接受任意值,也就是说你的函数的入参和返回值都应该可以是任意类型。 现在让我们给代码增加类型声明:</p><pre><code class="js">type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number;
type idString = (arg: string) => string;
...</code></pre><p>一个笨的方法就像上面那样,也就是说 JS 提供多少种类型,就需要复制多少份代码,然后改下类型签名。这对程序员来说是致命的。这种复制粘贴增加了出错的概率,使得代码难以维护,牵一发而动全身。并且将来 JS 新增新的类型,你仍然需要修改代码,也就是说你的代码<strong>对修改开放</strong>,这样不好。还有一种方式是使用 any 这种“万能语法”。缺点是什么呢?我举个例子:</p><pre><code class="js">id("string").length; // ok
id("string").toFixed(2); // ok
id(null).toString(); // ok
...</code></pre><p>如果你使用 any 的话,怎么写都是 ok 的, 这就丧失了类型检查的效果。实际上我知道我传给你的是 string,返回来的也一定是 string,而 string 上没有 toFixed 方法,因此需要报错才是我想要的。也就是说我真正想要的效果是:<code>当我用到id的时候,你根据我传给你的类型进行推导</code>。比如我传入的是 string,但是使用了 number 上的方法,你就应该报错。</p><p>为了解决上面的这些问题,我们<strong>使用泛型对上面的代码进行重构</strong>。和我们的定义不同,这里用了一个 类型 T,这个 <strong>T 是一个抽象类型,只有在调用的时候才确定它的值</strong>,这就不用我们复制粘贴无数份代码了。</p><pre><code class="js">function id<T>(arg: T): T {
return arg;
}</code></pre><p>为什么这样就可以了? 为什么要用这种写法?这个尖括号什么鬼?万物必有因果,之所以这么设计泛型也是有原因的。那么就让我来给大家解释一下,相信很多人都没有从这个角度思考过这个问题。</p><h3>泛型就是对类型编程</h3><p>上面提到了一个重要的点 <code>平时我们都是对值进行编程,泛型是对类型进行编程</code>。上面我没有给大家解释这句话。现在铺垫足够了,那就让我们开始吧!</p><p>继续举一个例子:假如我们定义了一个 Person 类,这个 Person 类有三个属性,并且都是必填的。这个 Person 类会被用于用户提交表单的时候限定表单数据。</p><pre><code class="ts">enum Sex {
Man,
Woman,
UnKnow,
}
interface Person {
name: string;
sex: Sex;
age: number;
}</code></pre><p>突然有一天,公司运营想搞一个促销活动,也需要用到 Person 这个 <code>shape</code>,但是这三个属性都可以选填,同时要求用户必须填写手机号以便标记用户和接受短信。一个很笨的方法是重新写一个新的类:</p><pre><code class="ts">interface MarketPerson {
name?: string;
sex?: Sex;
age?: number;
phone: string;
}</code></pre><blockquote>还记得我开头讲的重复类型定义么? 这就是!</blockquote><p>这明显不够优雅。如果 Person 字段很多呢?这种重复代码会异常多,不利于维护。 TS 的设计者当然不允许这么丑陋的设计存在。那么是否可以根据已有类型,生成新的类型呢?当然可以!答案就是前面我提到了两种对类型的操作:<strong>一种是集合操作,另一种是今天要讲的泛型。</strong></p><p>先来看下集合操作:</p><pre><code class="ts">type MarketPerson = Person & { phone: string };</code></pre><p>这个时候我们虽然添加了一个必填字段 phone,但是没有做到<code>name, sex, age</code> 选填,似乎集合操作做不到这一点呀。我们脑洞一下,假如我们可以<strong>像操作函数那样操作类型</strong>,是不是有可能呢?比如我定义了一个函数 <code>Partial</code>,这个函数的功能入参是一个类型,返回值是新的类型,这个类型里的属性全部变成可选的。</p><p>伪代码:</p><pre><code class="js">
function Partial(Type) {
type ans = 空类型
for(k in Type) {
空类型[k] = makeOptional(Type, k)
}
return ans
}
type PartialedPerson = Partial(Person)
</code></pre><p>可惜的是上面代码不能运行,也不可能运行。不可能运行的原因有:</p><ul><li>这里使用函数 Partial 操作类型,可以看出上面的函数我是没有添加签名的,我是故意的。如果让你给这个函数添加签名你怎么加?没办法加!</li><li>这里使用 JS 的语法对类型进行操作,这是不恰当的。首先这种操作依赖了 JS 运行时,而 TS 是静态分析工具,不应该依赖 JS 运行时。其次如果要支持这种操作是否意味者 TS 对 JS 妥协,JS 出了新的语法(比如早几年出的 async await),TS 都要支持其对 TS 进行操作。</li></ul><p>因此迫切需要一种不依赖 JS 行为,特别是运行时行为的方式,并且逻辑其实和上面类似的,且不会和现有语法体系冲突的语法。 我们看下 TS 团队是怎么做的:</p><pre><code class="js">// 可以看成是上面的函数定义,可以接受任意类型。由于是这里的 “Type” 形参,因此理论上你叫什么名字都是无所谓的,就好像函数定义的形参一样。
type Partial<Type> = { do something }
// 可以看成是上面的函数调用,调用的时候传入了具体的类型 Person
type PartialedPerson = Partial<Person></code></pre><p>先不管功能,我们来看下这两种写法有多像:</p><p><img src="/img/remote/1460000022993509" alt="" title=""></p><p>(定义)</p><p><img src="/img/remote/1460000022993510" alt="" title=""></p><p>(运行)</p><p>再来看下上面泛型的功能。上面代码的意思是对 T 进行处理,是返回一个 T 的子集,具体来说就是将 T 的所有属性变成可选。这时 <code>PartialedPerson</code> 就等于 :</p><pre><code class="ts">interface Person {
name?: string;
sex?: Sex;
age?: number;
}</code></pre><blockquote>功能和上面新建一个新的 interface 一样,但是更优雅。</blockquote><p>最后来看下泛型 Partial<Type> 的具体实现,可以看出其没有直接使用 JS 的语法,而是自己定义了一套语法,比如这里的 <code>keyof</code>,至此完全应证了我上面的观点。</p><pre><code class="ts">type Partial<T> = { [P in keyof T]?: T[P] };</code></pre><blockquote>刚才说了“由于是形参,因此起什么名字无所谓” 。因此这里就起了 T 而不是 Type,更短了。这也算是一种约定俗称的规范,大家一般习惯叫 T, U 等表示泛型的形参。</blockquote><p>我们来看下完整的泛型和函数有多像!</p><p><img src="/img/remote/1460000022993511" alt="" title=""></p><p>(定义)</p><p><img src="/img/remote/1460000022993512" alt="" title=""></p><p>(使用)</p><ul><li>从外表看只不过是 <code>function</code> 变成了 <code>type</code>,<code>()</code> 变成了 <code><></code>而已。</li><li>从语法规则上来看, 函数内部对标的是 ES 标准。而泛型对应的是 TS 实现的一套标准。</li></ul><p><img src="/img/remote/1460000022993513" alt="" title=""></p><p><img src="/img/remote/1460000022993514" alt="" title=""></p><p>简单来说,将类型看成值,然后对类型进行编程,这就是泛型的基本思想。泛型类似我们平时使用的函数,只不过其是作用在类型上,思想上和我们平时使用的函数并没有什么太多不同,泛型产生的具体类型也支持类型的操作。比如:</p><pre><code class="js">type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;</code></pre><p>有了上面的知识,我们通过几个例子来巩固一下。</p><pre><code class="js">function id<T, U>(arg1: T, arg2: U): T {
return arg1;
}</code></pre><p>上面定义了泛型 id,其入参分别是 T 和 U,和函数参数一样,使用逗号分隔。定义了形参就可以在函数体内使用形参了。如上我们在函数的参数列表和返回值中使用了形参 T 和 U。</p><p>返回值也可以是复杂类型:</p><pre><code class="js">function ids<T, U>(arg1: T, arg2: U): [T, U] {
return [arg1, arg2];
}</code></pre><p><img src="/img/remote/1460000022993516" alt="" title=""></p><p>(泛型的形参)</p><p>和上面类似, 只不过返回值变成了数组而已。</p><p>需要注意的是,思想上我们可以这样去理解。但是具体的实现过程会有一些细微差别,比如:</p><pre><code class="ts">type P = [number, string, boolean];
type Q = Date;
type R = [Q, ...P]; // A rest element type must be an array type.</code></pre><p>再比如:</p><pre><code class="ts">type Lucifer = LeetCode;
type LeetCode<T = {}> = {
name: T;
};
const a: LeetCode<string>; //ok
const a: Lucifer<string>; // Type 'Lucifer' is not generic.</code></pre><p>改成这样是 ok 的:</p><pre><code class="ts">type Lucifer<T> = LeetCode<T>;</code></pre><h2>泛型为什么使用尖括号</h2><p>为什么泛型要用尖括号(<>),而不是别的? 我猜是因为它和 () 长得最像,且在现在的 JS 中不会有语法歧义。但是,它和 JSX 不兼容!比如:</p><pre><code class="tsx">function Form() {
// ...
return (
<Select<string> options={targets} value={target} onChange={setTarget} />
);
}</code></pre><p>这是因为 TS 发明这个语法的时候,还没想过有 JSX 这种东西。后来 TS 团队在 TypeScript 2.9 版本修复了这个问题。也就是说现在你可以直接在 TS 中使用带有泛型参数的 JSX 啦(比如上面的代码)。</p><h2>泛型的种类</h2><p>实际上除了上面讲到的函数泛型,还有接口泛型和类泛型。不过语法和含义基本同函数泛型一样:</p><pre><code class="js">interface id<T, U> {
id1: T;
id2: U;
}</code></pre><p>(接口泛型)</p><pre><code class="js">class MyComponent extends React.Component<Props, State> {
...
}</code></pre><p>(类泛型)</p><p>总结下就是: 泛型的写法就是在标志符后面添加尖括号(<>),然后在尖括号里写形参,并在 body(函数体, 接口体或类体) 里用这些形参做一些逻辑处理。</p><h2>泛型的参数类型 - “泛型约束”</h2><p>正如文章开头那样,我们可以对函数的参数进行限定。</p><pre><code class="js">function t(name: string) {
return `hello, ${name}`;
}
t("lucifer");</code></pre><p>如上代码对函数的形参进行了类型限定,使得函数仅可以接受 string 类型的值。那么泛型如何达到类似的效果呢?</p><pre><code class="js">type MyType = (T: constrain) => { do something };</code></pre><p>还是以 id 函数为例,我们给 id 函数增加功能,使其不仅可以返回参数,还会打印出参数。熟悉函数式编程的人可能知道了,这就是 trace 函数,用于调试程序。</p><pre><code class="js">function trace<T>(arg: T): T {
console.log(arg);
return arg;
}</code></pre><p>假如我想打印出参数的 size 属性呢?如果完全不进行约束 TS 是会报错的:</p><blockquote>注意:不同 TS 版本可能提示信息不完全一致,我的版本是 3.9.5。下文的所有测试结果均是使用该版本,不再赘述。</blockquote><pre><code class="js">function trace<T>(arg: T): T {
console.log(arg.size); // Error: Property 'size doesn't exist on type 'T'
return arg;
}</code></pre><p>报错的原因在于 T 理论上是可以是任何类型的,不同于 any,你不管使用它的什么属性或者方法都会报错(除非这个属性和方法是所有集合共有的)。那么直观的想法是限定传给 trace 函数的<strong>参数类型</strong>应该有 size 类型,这样就不会报错了。如何去表达这个<strong>类型约束</strong>的点呢?实现这个需求的关键在于使用类型约束。 使用 extends 关键字可以做到这一点。简单来说就是你定义一个类型,然后让 T 实现这个接口即可。</p><pre><code class="js">interface Sizeable {
size: number;
}
function trace<T extends Sizeable>(arg: T): T {
console.log(arg.size);
return arg;
}
</code></pre><p><img src="/img/remote/1460000022993519" alt="" title=""></p><p>这个时候 T 就不再是任意类型,而是被实现接口的 shape,当然你也可以继承多个接口。<strong>类型约束是非常常见的操作,大家一定要掌握。</strong></p><blockquote>有的人可能说我直接将 Trace 的参数限定为 Sizeable 类型可以么?如果你这么做,会有类型丢失的风险,详情可以参考这篇文章<a href="https://link.segmentfault.com/?enc=fH0O8UpR0V8%2ByjpGNctxwQ%3D%3D.bzYno4EmgYb9Y3DrrT3agSG0UbvsBLrZ%2F3KRKwpP3xM1AkuJNsejpE1AIyt7m9ub" rel="nofollow">A use case for TypeScript Generics</a>。</blockquote><h2>常见的泛型</h2><h3>集合类</h3><p>大家平时写 TS 一定见过类似 <code>Array<String></code> 这种写法吧? 这其实是集合类,也是一种泛型。</p><p>本质上数组就是一系列值的集合,这些值可以可以是任意类型,数组只是一个容器而已。然而平时开发的时候通常数组的项目类型都是相同的,如果不加约束的话会有很多问题。 比如我应该是一个字符串数组,然是却不小心用到了 number 的方法,这个时候类型系统应该帮我识别出这种<strong>类型问题</strong>。</p><p>由于数组理论可以存放任意类型,因此需要使用者动态决定你想存储的数据类型,并且这些类型只有在被调用的时候才能去确定。 <code>Array<String></code> 就是调用,经过这个调用会产生一个具体集合,这个集合只能存放 string 类型的值。</p><p><img src="/img/remote/1460000022993515" alt="" title=""></p><p>不调用直接把 Array 是不被允许的:</p><pre><code class="js">const a: Array = ["1"];</code></pre><p>如上代码会被错:<code>Generic type 'Array<T>' requires 1 type argument(s).ts</code> 。 有没有觉得和函数调用没传递参数报错很像?像就对了。</p><p>这个时候你再去看 Set<number>, Promise<string>,是不是很快就知道啥意思了?它们本质上都是包装类型,并且支持多种参数类型,因此可以用泛型来约束。</p><h3>React.FC</h3><p>大家如果开发过 React 的 TS 应用,一定知道 <code>React.FC</code> 这个类型。我们来看下它是如何<a href="https://link.segmentfault.com/?enc=D4Pt2GplZBVKhFGsHZ%2FrWQ%3D%3D.%2BmjR5zKo2oiRE39kLerr3gxVTn5lZ07ZCLDbdP%2Bm0zg86HUKyAPIcY%2BWIzRclW8dNUui7eQV%2FBGgfKxT0D0NjgNHnxdjJd%2FnFxxHVX9JJboVwQm3hztKf1%2B9y12XFRvy" rel="nofollow">定义</a>的:</p><pre><code class="js">type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}</code></pre><p>可以看出其大量使用了泛型。你如果不懂泛型怎么看得懂呢?不管它多复杂,我们从头一点点分析就行,记住我刚才讲的类比方法,将泛型类比到函数进行理解。·</p><ul><li>首先定义了一个泛型类型 FC,这个 FC 就是我们平时用的 React.FC。它是通过另外一个泛型 FunctionComponent 产生的。</li></ul><blockquote>因此,实际上第一行代码的作用就是起了一个别名</blockquote><ul><li>FunctionComponent 实际上是就是一个接口泛型,它定义了五个属性,其中四个是可选的,并且是静态类属性。</li><li>displayName 比较简单,而 propTypes,contextTypes,defaultProps 又是通过其他泛型生成的类型。我们仍然可以采用我的这个分析方法继续分析。由于篇幅原因,这里就不一一分析,读者可以看完我的分析过程之后,自己尝试分析一波。</li><li><code>(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;</code> 的含义是 FunctionComponent 是一个函数,接受两个参数(props 和 context )返回 ReactElement 或者 null。ReactElement 大家应该比较熟悉了。<code>PropsWithChildren</code> 实际上就是往 props 中插入 children,源码也很简单,代码如下:</li></ul><pre><code class="js">type PropsWithChildren<P> = P & { children?: ReactNode };</code></pre><p>这不就是我们上面讲的<strong>集合操作</strong>和 <strong>可选属性</strong>么?至此,React.FC 的全貌我们已经清楚了。读者可以试着分析别的源码检测下自己的学习效果,比如 <code>React.useState</code> 类型的签名。</p><h2>类型推导与默认参数</h2><p>类型推导和默认参数是 TS 两个重要功能,其依然可以作用到泛型上,我们来看下。</p><h3>类型推导</h3><p>我们一般常见的类型推导是这样的:</p><pre><code class="js">const a = "lucifer"; // 我们没有给 a 声明类型, a 被推导为 string
a.toFixed(); // Property 'toFixed' does not exist on type 'string'.
a.includes("1"); // ok</code></pre><p>需要注意的是,类型推导是仅仅在初始化的时候进行推导,如下是无法正确推导的:</p><pre><code class="js">let a = "lucifer"; // 我们没有给 a 声明类型, a 被推导为string
a.toFixed(); // Property 'toFixed' does not exist on type 'string'.
a.includes("1"); // ok
a = 1;
a.toFixed(); // 依然报错, a 不会被推导 为 number</code></pre><p>而泛型也支持类型推导,以上面的 id 函数为例:</p><pre><code class="ts">function id<T>(arg: T): T {
return arg;
}
id<string>("lucifer"); // 这是ok的,也是最完整的写法
id("lucifer"); // 基于类型推导,我们可以这样简写</code></pre><p>这也就是为什么 useState 有如下两种写法的原因。</p><pre><code class="ts">const [name, setName] = useState("lucifer");
const [name, setName] = useState<string>("lucifer");</code></pre><p>实际的类型推导要更加复杂和智能。相信随着时间的推进,TS 的类型推导会更加智能。</p><h3>默认参数</h3><p>和<code>类型推导</code>相同的点是,默认参数也可以减少代码量,让你少些代码。前提是你要懂,不然伴随你的永远是大大的问号。其实你完全可以将其类比到函数的默认参数来理解。</p><p>举个例子:</p><pre><code class="ts">type A<T = string> = Array<T>;
const aa: A = [1]; // type 'number' is not assignable to type 'string'.
const bb: A = ["1"]; // ok
const cc: A<number> = [1]; // ok</code></pre><p>上面的 A 类型默认是 string 类型的数组。你可以不指定,等价于 Array<string>,当然你也可以显式指定数组类型。有一点需要注意:在 JS 中,函数也是值的一种,因此:</p><pre><code class="js">const fn = () => null; // ok</code></pre><p>但是泛型这样是不行的,这是和函数不一样的地方(设计缺陷?Maybe):</p><pre><code class="js">type A = Array; // error: Generic type 'Array<T>' requires 1 type argument(s).</code></pre><p>其原因在与 Array 的定义是:</p><pre><code class="ts">interface Array<T> {
...
}</code></pre><p>而如果 Array 的类型也支持默认参数的话,比如:</p><pre><code class="ts">interface Array<T = string> {
...
}</code></pre><p>那么 <code>type A = Array;</code> 就是成立的,如果不指定的话,会默认为 string 类型。</p><h2>什么时候用泛型</h2><p>如果你认真看完本文,相信应该知道什么时候使用泛型了,我这里简单总结一下。</p><p>当你的函数,接口或者类:</p><ul><li>需要作用到很多类型的时候,比如我们介绍的 id 函数的泛型声明。</li><li>需要被用到很多地方的时候,比如我们介绍的 Partial 泛型。</li></ul><h2>进阶</h2><p>上面说了泛型和普通的函数有着很多相似的地方。普通的函数可以嵌套其他函数,甚至嵌套自己从而形成递归。泛型也是一样!</p><h3>泛型支持函数嵌套</h3><p>比如:</p><pre><code class="ts">type CutTail<Tuple extends any[]> = Reverse<CutHead<Reverse<Tuple>>>;</code></pre><p>如上代码中, Reverse 是将参数列表反转,CutHead 是将数组第一项切掉。因此 CutTail 的意思就是将传递进来的参数列表反转,切掉第一个参数,然后反转回来。换句话说就是切掉参数列表的最后一项。 比如,一个函数是 function fn (a: string, b: number, c: boolean):boolean {},那么经过操作<code>type cutTailFn = CutTail<typeof fn></code>,可以返回<code>(a: string, b:number) => boolean</code>。 具体实现可以参考<a href="https://link.segmentfault.com/?enc=%2FmgOZbVyn6FhbqgxjKXSBw%3D%3D.8RfkWgyLqrvfYwnisD%2BcWSUsvrlqJtZ1wNt35gQikPavEW25xbKz9W9H0H5cl3%2Fg" rel="nofollow">Typescript 复杂泛型实践:如何切掉函数参数表的最后一个参数?</a>。 在这里,你知道泛型支持嵌套就够了。</p><h3>泛型支持递归</h3><p>泛型甚至可以嵌套自己从而形成递归,比如我们最熟悉的单链表的定义就是递归的。</p><pre><code class="ts">type ListNode<T> = {
data: T;
next: ListNode<T> | null;
};</code></pre><p>(单链表)</p><p>再比如 <strong>HTMLElement</strong> 的定义。</p><pre><code class="ts">declare var HTMLElement: {
prototype: HTMLElement;
new(): HTMLElement;
};。</code></pre><p>(<a href="https://link.segmentfault.com/?enc=%2BIGaL41GA9kit%2BXQELlqyw%3D%3D.qntg%2BZz3lx7veIOL3SEY%2FVW18M5%2BgOe44N92yvm%2B8kzE6%2FddumFvGD0izUr%2B4wiBbJ2BWw%2F7TpIdoo6lwn%2B1YThkRd2bg4qmE5O5Zm5mqho%3D" rel="nofollow">HTMLElement</a>)</p><p>上面是<strong>递归声明</strong>,我们再来看一个更复杂一点的递归形式 - <strong>递归调用</strong>,这个递归调用的功能是:<strong>递归地将类型中所有的属性都变成可选</strong>。类似于深拷贝那样,只不过这不是拷贝操作,而是变成可选,并且是作用在类型,而不是值。</p><pre><code class="ts">type DeepPartial<T> = T extends Function
? T
: T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
type PartialedWindow = DeepPartial<Window>; // 现在window 上所有属性都变成了可选啦</code></pre><h2>TS 泛型工具及实现</h2><p>虽然泛型支持函数的嵌套,甚至递归,但是其语法能力肯定和 JS 没法比, 想要实现一个泛型功能真的不是一件容易的事情。这里提供几个例子,看完这几个例子,相信你至少可以达到比葫芦画瓢的水平。这样多看多练,慢慢水平就上来了。</p><p>截止目前(2020-06-21),TS 提供了 <a href="https://link.segmentfault.com/?enc=1W9tfRdoAFu3hfVYmiGBjw%3D%3D.w%2FJ28o1EUIAxnhBv5i06raQONYAn6WNi2LD1SyOEIKzgJ%2Bw8%2BivTy%2BVcnlxiMx%2B2jFjLGGturLrEtE35DhD1t55gCpnjr%2B6IjYCGxGgCvqU%3D" rel="nofollow">16 种工具类型</a>。</p><p><img src="/img/remote/1460000022993518" alt="" title=""></p><p>(官方提供的工具类型)</p><p>除了官方的工具类型,还有一些社区的工具类型,比如<a href="https://link.segmentfault.com/?enc=lEjpUbOhGBxtfn0FLJLW7A%3D%3D.wHHw65uVaIbzqBC5kQrGZNLN8f5Wt4XtBVq%2F9G2eSQUI6gOvxV%2BP%2F5jcdw13VIpr" rel="nofollow">type-fest</a>,你可以直接用或者去看看源码看看高手是怎么玩类型的。</p><p>我挑选几个工具类,给大家讲一下<strong>实现原理</strong>。</p><h3>Partial</h3><p>功能是将类型的属性<strong>变成可选</strong>。注意这是浅 Partial,DeepPartial 上面我讲过了,只要配合递归调用使用即可。</p><pre><code class="ts">type Partial<T> = { [P in keyof T]?: T[P] };</code></pre><h3>Required</h3><p>功能和<code>Partial</code> 相反,是将类型的属性<strong>变成必填</strong>, 这里的 <code>-</code>指的是去除。 <code>-?</code> 意思就是去除可选,也就是必填啦。</p><pre><code class="ts">type Required<T> = { [P in keyof T]-?: T[P] };</code></pre><h3>Mutable</h3><p>功能是将类型的属性<strong>变成可修改</strong>,这里的 <code>-</code>指的是去除。 <code>-readonly</code> 意思就是去除只读,也就是可修改啦。</p><pre><code class="ts">type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};</code></pre><h3>Readonly</h3><p>功能和<code>Mutable</code> 相反,功能是将类型的属性<strong>变成只读</strong>, 在属性前面增加 <code>readonly</code> 意思会将其变成只读。</p><pre><code class="ts">type Readonly<T> = { readonly [P in keyof T]: T[P] };</code></pre><h3>ReturnType</h3><p>功能是用来得到一个函数的返回值类型。</p><pre><code class="ts">type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;</code></pre><p>下面的示例用 ReturnType 获取到 Func 的返回值类型为 string,所以,foo 也就只能被赋值为字符串了。</p><pre><code class="ts">type Func = (value: number) => string;
const foo: ReturnType<Func> = "1";</code></pre><p>更多参考<a href="https://link.segmentfault.com/?enc=v67skFeUFF8RdlNy4lJWBw%3D%3D.X%2BIN1lO5SWcARSXuOBHHqz1xWeUYGANAQeWcuNTZWeJK3ZOwQaq%2FS2aJBpeONhsOS3yGxzHvQT990oFjdKtdehiAqMH5xRrT9b9zy9EaRWo%3D" rel="nofollow">TS - es5.d.ts</a> 这些泛型可以极大减少大家的冗余代码,大家可以在自己的项目中自定义一些工具类泛型。</p><h2>Bonus - 接口智能提示</h2><p>最后介绍一个实用的小技巧。如下是一个接口的类型定义:</p><pre><code class="ts">interface Seal {
name: string;
url: string;
}
interface API {
"/user": { name: string; age: number; phone: string };
"/seals": { seal: Seal[] };
}
const api = <URL extends keyof API>(url: URL): Promise<API[URL]> => {
return fetch(url).then((res) => res.json());
};</code></pre><p>我们通过泛型以及泛型约束,实现了智能提示的功能。使用效果:</p><p><img src="/img/remote/1460000022993517" alt="" title=""></p><p>(接口名智能提示)</p><p><img src="/img/remote/1460000022993520" alt="" title=""></p><p><img src="/img/remote/1460000022993521" alt="" title=""></p><p>(接口返回智能提示)</p><p>原理很简单,当你仅输入 api 的时候,其会将 API interface 下的所有 key 提示给你,当你输入某一个 key 的时候,其会根据 key 命中 interface 定义的类型,然后给予类型提示。</p><h2>总结</h2><p>学习 Typescript 并不是一件简单的事情,尤其是没有其他语言背景的情况。而 TS 中最为困难的内容之一恐怕就是泛型了。</p><p>泛型和我们平时使用的函数是很像的,如果将两者进行横向对比,会很容易理解,很多函数的都关系可以迁移到泛型,比如函数嵌套,递归,默认参数等等。泛型是对类型进行编程,参数是类型,返回值是一个新的类型。我们甚至可以对泛型的参数进行约束,就类似于函数的类型约束。</p><p>最后通过几个高级的泛型用法以及若干使用的泛型工具类帮助大家理解和消化上面的知识。要知道真正的 TS 高手都是玩类型的,高手才不会满足于类型的交叉并操作。 泛型用的好确实可以极大减少代码量,提高代码维护性。如果用的太深入,也可能会团队成员面面相觑,一脸茫然。因此抽象层次一定要合理,不仅仅是泛型,整个软件工程都是如此。</p><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p>
你不知道的前端异常处理(万字长文,建议收藏)
https://segmentfault.com/a/1190000022977773
2020-06-19T18:19:59+08:00
2020-06-19T18:19:59+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
50
<p>除了调试,处理异常或许是程序员编程时间占比最高的了。我们天天和各种异常打交道,就好像我们天天和 Bug 打交道一样。因此正确认识异常,并作出合适的异常处理就显得很重要了。</p><p>我们先尝试抛开前端这个限定条件,来看下更广泛意义上程序的报错以及异常处理。不管是什么语言,都会有异常的发生。而我们程序员要做的就是正确识别程序中的各种异常,并针对其做相应的<strong>异常处理</strong>。</p><p>然而,很多人对异常的处理方式是<strong>事后修补</strong>,即某个异常发生的时候,增加对应的条件判断,这真的是一种非常低效的开发方式,非常不推荐大家这么做。那么究竟如何正确处理异常呢?由于不同语言有不同的特性,因此异常处理方式也不尽相同。但是异常处理的思维框架一定是一致的。本文就<strong>前端</strong>异常进行详细阐述,但是读者也可以稍加修改延伸到其他各个领域。</p><blockquote>本文讨论的异常指的是软件异常,而非硬件异常。</blockquote><h2>什么是异常</h2><p>用直白的话来解释异常的话,就是<strong>程序发生了意想不到的情况,这种情况影响到了程序的正确运行</strong>。</p><p>从根本上来说,异常就是一个<strong>数据结构</strong>,其保存了异常发生的相关信息,比如错误码,错误信息等。以 JS 中的标准内置对象 Error 为例,其标准属性有 name 和 message。然而不同的浏览器厂商有自己的自定义属性,这些属性并不通用。比如 Mozilla 浏览器就增加了 filename 和 stack 等属性。</p><p>值得注意的是错误只有被抛出,才会产生异常,不被抛出的错误不会产生异常。比如:</p><pre><code class="js">function t() {
console.log("start");
new Error();
console.log("end");
}
t();</code></pre><p><img src="/img/remote/1460000022977777" alt="" title=""></p><p>(动画演示)</p><p>这段代码不会产生任何的异常,控制台也不会有任何错误输出。</p><h2>异常的分类</h2><p>按照产生异常时程序是否正在运行,我们可以将错误分为<strong>编译时异常</strong>和<strong>运行时异常</strong>。</p><p>编译时异常指的是源代码在编译成可执行代码之前产生的异常。而运行时异常指的是可执行代码被装载到内存中执行之后产生的异常。</p><h3>编译时异常</h3><p>我们知道 TS 最终会被编译成 JS,从而在 <code>JS Runtime</code>中执行。既然存在编译,就有可能编译失败,就会有编译时异常。</p><p>比如我使用 TS 写出了如下代码:</p><pre><code class="ts">const s: string = 123;</code></pre><p>这很明显是错误的代码, 我给 s 声明了 string 类型,但是却给它赋值 number。</p><p>当我使用 tsc(typescript 编译工具,全称是 typescript compiler)尝试编译这个文件的时候会有异常抛出:</p><pre><code class="bash">tsc a.ts
a.ts:1:7 - error TS2322: Type '123' is not assignable to type 'string'.
1 const s: string = 123;
~
Found 1 error.</code></pre><p>这个异常就是编译时异常,因为我的代码还没有执行。</p><p>然而并不是你用了 TS 才存在编译时异常,JS 同样有编译时异常。有的人可能会问 JS 不是解释性语言么?是边解释边执行,没有编译环节,怎么会有编译时异常?</p><p>别急,我举个例子你就明白了。如下代码:</p><pre><code class="js">function t() {
console.log('start')
await sa
console.log('end')
}
t()</code></pre><p>上面的代码由于存在语法错误,不会编译通过,因此并不会打印<code>start</code>,侧面证明了这是一个编译时异常。尽管 JS 是解释语言,也依然存在编译阶段,这是必然的,因此自然也会有编译异常。</p><p>总的来说,编译异常可以在代码被编译成最终代码前被发现,因此对我们的伤害更小。接下来,看一下令人心生畏惧的<strong>运行时异常</strong>。</p><h3>运行时异常</h3><p>相信大家对运行时异常非常熟悉。这恐怕是广大前端碰到最多的异常类型了。众所周知的 <a href="https://link.segmentfault.com/?enc=BXR%2BZxeDHNCYufNN%2Fvpapg%3D%3D.Yg0sA97DPzAOuPJHLNzu6tJLmwzb3r7%2F3eBE%2BkW%2FCkCX3NmK1efnM8o257uHuX624hP%2FTfqV20D0D%2BnbfzJfxA5%2B9Vl0xLRPof27JjhYjic%3D" rel="nofollow">NPE(Null Pointer Exception)</a> 就是运行时异常。</p><p>将上面的例子稍加改造,得到下面代码:</p><pre><code class="js">function t() {
console.log("start");
throw 1;
console.log("end");
}
t();</code></pre><p><img src="/img/remote/1460000022977776" alt="" title=""></p><p>(动画演示)</p><blockquote>注意 end 没有打印,并且 t 没有弹出栈。实际上 t 最终还是会被弹出的,只不过和普通的返回不一样。</blockquote><p>如上,则会打印出<code>start</code>。由于异常是在代码运行过程中抛出的,因此这个异常属于运行时异常。相对于编译时异常,这种异常更加难以发现。上面的例子可能比较简单,但是如果我的异常是隐藏在某一个流程控制语句(比如 if else)里面呢?程序就可能在客户的电脑走入那个抛出异常的 if 语句,而在你的电脑走入另一条。这就是著名的 <strong>《在我电脑上好好的》</strong> 事件。</p><h2>异常的传播</h2><p>异常的传播和我之前写的<a href="https://link.segmentfault.com/?enc=jc0zoQTCHRYbj6ggWC1lnw%3D%3D.68pqkSvQL0VY9a8VrFp5Cpcg4St4Uu9OyGXvG1oCv1G91QqA%2FzLisLkanz4Yifjbvl1x60qoar%2Bcxm7g3M5eFQ%3D%3D" rel="nofollow">浏览器事件模型</a>有很大的相似性。只不过那个是作用在 <strong>DOM 这样的数据结构</strong>,这个则是作用在<strong>函数调用栈这种数据结构</strong>,并且事件传播存在捕获阶段,异常传播是没有的。不同 C 语言,JS 中异常传播是自动的,不需要程序员手动地一层层传递。如果一个异常没有被 catch,它会沿着函数调用栈一层层传播直到栈空。</p><p>异常处理中有两个关键词,它们是<strong>throw(抛出异常)</strong> 和 <strong>catch(处理异常)</strong>。 当一个异常被抛出的时候,异常的传播就开始了。异常会不断传播直到遇到第一个 catch。 如果程序员没有手动 catch,那么一般而言程序会抛出类似<strong>unCaughtError</strong>,表示发生了一个异常,并且这个异常没有被程序中的任何 catch 语言处理。未被捕获的异常通常会被打印在控制台上,里面有详细的堆栈信息,从而帮助程序员快速排查问题。实际上我们的程序的目标是<strong>避免 unCaughtError</strong>这种异常,而不是一般性的异常。</p><h3>一点小前提</h3><p>由于 JS 的 Error 对象没有 code 属性,只能根据 message 来呈现,不是很方便。我这里进行了简单的扩展,后面很多地方我用的都是自己扩展的 Error ,而不是原生 JS Error ,不再赘述。</p><pre><code class="js">oldError = Error;
Error = function ({ code, message, fileName, lineNumber }) {
error = new oldError(message, fileName, lineNumber);
error.code = code;
return error;
};</code></pre><h3>手动抛出 or 自动抛出</h3><p>异常既可以由程序员自己手动抛出,也可以由程序自动抛出。</p><pre><code class="js">throw new Error(`I'm Exception`);</code></pre><p>(手动抛出的例子)</p><pre><code class="js">a = null;
a.toString(); // Thrown: TypeError: Cannot read property 'toString' of null</code></pre><p>(程序自动抛出的例子)</p><p>自动抛出异常很好理解,毕竟我们哪个程序员没有看到过程序自动抛出的异常呢?</p><blockquote>“这个异常突然就跳出来!吓我一跳!”,某不知名程序员如是说。</blockquote><p>那什么时候应该手动抛出异常呢?</p><p>一个指导原则就是<strong>你已经预知到程序不能正确进行下去了</strong>。比如我们要实现除法,首先我们要考虑的是被除数为 0 的情况。当被除数为 0 的时候,我们应该怎么办呢?是抛出异常,还是 return 一个特殊值?答案是都可以,你自己能区分就行,这没有一个严格的参考标准。 我们先来看下抛出异常,告诉调用者<strong>你的输入,我处理不了</strong>这种情况。</p><pre><code class="js">function divide(a, b) {
a = +a;
b = +b; // 转化成数字
if (!b) {
// 匹配 +0, -0, NaN
throw new Error({
code: 1,
message: "Invalid dividend " + b,
});
}
if (Number.isNaN(a)) {
// 匹配 NaN
throw new Error({
code: 2,
message: "Invalid divisor " + a,
});
}
return a / b;
}</code></pre><p>上面代码会在两种情况下抛出异常,告诉调用者你的输入我处理不了。由于这两个异常都是程序员自动手动抛出的,因此是<strong>可预知的异常</strong>。</p><p>刚才说了,我们也可以通过返回值来区分<code>异常输入</code>。我们来看下返回值输入是什么,以及和异常有什么关系。</p><h3>异常 or 返回</h3><p>如果是基于异常形式(遇到不能处理的输入就抛出异常)。当别的代码调用<code>divide</code>的时候,需要自己 catch。</p><pre><code class="js">function t() {
try {
divide("foo", "bar");
} catch (err) {
if (err.code === 1) {
return console.log("被除数必须是除0之外的数");
}
if (err.code === 2) {
return console.log("除数必须是数字");
}
throw new Error("不可预知的错误");
}
}</code></pre><p>然而就像上面我说的那样,divide 函数设计的时候,也完全可以不用异常,而是使用返回值来区分。</p><pre><code class="js">function divide(a, b) {
a = +a;
b = +b; // 转化成数字
if (!b) {
// 匹配 +0, -0, NaN
return new Error({
code: 1,
message: "Invalid dividend " + b,
});
}
if (Number.isNaN(a)) {
// 匹配 NaN
return new Error({
code: 2,
message: "Invalid divisor " + a,
});
}
return a / b;
}</code></pre><p>当然,我们使用方式也要作出相应改变。</p><pre><code class="js">function t() {
const res = divide("foo", "bar");
if (res.code === 1) {
return console.log("被除数必须是除0之外的数");
}
if (res.code === 2) {
return console.log("除数必须是数字");
}
return new Error("不可预知的错误");
}</code></pre><p>这种函数设计方式和抛出异常的设计方式从功能上说都是一样的,只是告诉调用方的方式不同。如果你选择第二种方式,而不是抛出异常,那么实际上需要调用方书写额外的代码,用来区分正常情况和异常情况,这并不是一种良好的编程习惯。</p><p>然而在 Go 等返回值可以为复数的语言中,我们无需使用上面蹩脚的方式,而是可以:</p><pre><code class="go">res, err := divide("foo", "bar");
if err != nil {
log.Fatal(err)
}
</code></pre><p>这是和 Java 和 JS 等语言使用的 try catch 不一样的的地方,Go 是通过 panic recover defer 机制来进行异常处理的。感兴趣的可以去看看 <a href="https://link.segmentfault.com/?enc=B2c0wQwOpfOFgBo9pN3DZA%3D%3D.7Vdnj4WP3MaO%2BQ8eRhl9hzEZTeGtCpllNYlLrPAOZU8%2Fr0coBQ6lAB1logfngzRvydYZPxB%2FPyG8rK1%2FLYVR%2Bg%3D%3D" rel="nofollow">Go 源码关于错误测试部分</a></p><p>可能大家对 Go 不太熟悉。没关系,我们来继续看下 shell。实际上 shell 也是通过返回值来处理异常的,我们可以通过 &dollar;? 拿到上一个命令的返回值,这本质上也是一种调用栈的传播行为,而且是通过返回值而不是捕获来处理异常的。</p><blockquote>作为函数返回值处理和 try catch 一样,这是语言的设计者和开发者共同决定的一件事情。</blockquote><p>上面提到了异常传播是作用在<strong>函数调用栈</strong>上的。当一个异常发生的时候,其会沿着函数调用栈逐层返回,直到第一个 catch 语句。当然 catch 语句内部仍然可以触发异常(自动或者手动)。如果 catch 语句内部发生了异常,也一样会沿着其函数调用栈继续执行上述逻辑,专业术语是 <strong>stack unwinding</strong>。</p><blockquote>实际上并不是所有的语言都会进行 stack unwinding,这个我们会在接下来的《运行时异常可以恢复么?》部分讲解。</blockquote><p><img src="/img/remote/1460000022977783" alt="" title=""></p><p>伪代码来描述一下:</p><pre><code class="js">function bubble(error, fn) {
if (fn.hasCatchBlock()) {
runCatchCode(error);
}
if (callstack.isNotEmpty()) {
bubble(error, callstack.pop());
}
}</code></pre><blockquote>从我的伪代码可以看出所谓的 stack unwinding 其实就是 callstack.pop()</blockquote><p>这就是异常传播的一切!仅此而已。</p><h2>异常的处理</h2><p>我们已经了解来异常的传播方式了。那么接下来的问题是,我们应该如何在这个传播过程中处理异常呢?</p><p>我们来看一个简单的例子:</p><pre><code class="js">function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error("an error occured");
}
a();</code></pre><p>我们将上面的代码放到 chrome 中执行, 会在控制台显示如下输出:</p><p><img src="/img/remote/1460000022977779" alt="" title=""></p><p>我们可以清楚地看出函数的调用关系。即错误是在 c 中发生的,而 c 是 b 调用的,b 是 a 调用的。这个函数调用栈是为了方便开发者定位问题而存在的。</p><p>上面的代码,我们并没有 catch 错误,因此上面才会有<strong>uncaught Error</strong>。</p><p>那么如果我们 catch ,会发生什么样的变化呢?catch 的位置会对结果产生什么样的影响?在 a ,b,c 中 catch 的效果是一样的么?</p><p>我们来分别看下:</p><pre><code class="js">function a() {
b();
}
function b() {
c();
}
function c() {
try {
throw new Error("an error occured");
} catch (err) {
console.log(err);
}
}
a();</code></pre><p>(在 c 中 catch)</p><p>我们将上面的代码放到 chrome 中执行, 会在控制台显示如下输出:</p><p><img src="/img/remote/1460000022977787" alt="" title=""></p><p>可以看出,此时已经没有<strong>uncaught Error</strong>啦,仅仅在控制台显示了<strong>标准输出</strong>,而<strong>非错误输出</strong>(因为我用的是 console.log,而不是 console.error)。然而更重要是的是,如果我们没有 catch,那么后面的同步代码将不会执行。</p><p>比如在 c 的 throw 下面增加一行代码,这行代码是无法被执行的,<strong>无论这个错误有没有被捕获</strong>。</p><pre><code class="js">function c() {
try {
throw new Error("an error occured");
console.log("will never run");
} catch (err) {
console.log(err);
}
}</code></pre><p>我们将 catch 移动到 b 中试试看。</p><pre><code class="js">function a() {
b();
}
function b() {
try {
c();
} catch (err) {
console.log(err);
}
}
function c() {
throw new Error("an error occured");
}
a();</code></pre><p>(在 b 中 catch)</p><p>在这个例子中,和上面在 c 中捕获没有什么本质不同。其实放到 a 中捕获也是一样,这里不再贴代码了,感兴趣的自己试下。</p><p>既然处于函数调用栈顶部的函数报错, 其函数调用栈下方的任意函数都可以进行捕获,并且效果没有本质不同。那么问题来了,我到底应该在哪里进行错误处理呢?</p><p>答案是责任链模式。我们先来简单介绍一下责任链模式,不过细节不会在这里展开。</p><p>假如 lucifer 要请假。</p><ul><li>如果请假天数小于等于 1 天,则主管同意即可</li><li>如果请假大于 1 天,但是小于等于三天,则需要 CTO 同意。</li><li>如果请假天数大于三天,则需要老板同意。</li></ul><p><img src="/img/remote/1460000022977782" alt="" title=""></p><p>这就是一个典型的责任链模式。谁有责任干什么事情是确定的,不要做自己能力范围之外的事情。比如主管不要去同意大于 1 天的审批。</p><p>举个例子,假设我们的应用有三个异常处理类,它们分别是:<code>用户输入错误</code>,<code>网络错误</code> 和 <code>类型错误</code>。如下代码,当代码执行的时候会报错一个<code>用户输入异常</code>。这个异常没有被 C 捕获,会 unwind stack 到 b,而 b 中 catch 到这个错误之后,通过查看 code 值判断其可以被处理,于是打印<code>I can handle this</code>。</p><pre><code class="js">function a() {
try {
b();
} catch (err) {
if (err.code === "NETWORK_ERROR") {
return console.log("I can handle this");
}
// can't handle, pass it down
throw err;
}
}
function b() {
try {
c();
} catch (err) {
if (err.code === "INPUT_ERROR") {
return console.log("I can handle this");
}
// can't handle, pass it down
throw err;
}
}
function c() {
throw new Error({
code: "INPUT_ERROR",
message: "an error occured",
});
}
a();</code></pre><p>而如果 c 中抛出的是别的异常,比如<code>网络异常</code>,那么 b 是无法处理的,虽然 b catch 住了,但是由于你无法处理,因此一个好的做法是<code>继续抛出异常</code>,而不是<strong>吞没</strong>异常。不要畏惧错误,抛出它。<strong>只有没有被捕获的异常才是可怕的</strong>,如果一个错误可以被捕获并得到正确处理,它就不可怕。</p><p>举个例子:</p><pre><code class="js">function a() {
try {
b();
} catch (err) {
if (err.code === "NETWORK_ERROR") {
return console.log("I can handle this");
}
// can't handle, pass it down
throw err;
}
}
function b() {
try {
c();
} catch (err) {
if (err.code === "INPUT_ERROR") {
return console.log("I can handle this");
}
}
}
function c() {
throw new Error({
code: "NETWORK_ERROR",
message: "an error occured",
});
}
a();</code></pre><p>如上代码不会有任何异常被抛出,它被完全吞没了,这对我们调试问题简直是灾难。因此切记<strong>不要吞没你不能处理的异常</strong>。正确的做法应该是上面讲的那种<strong>只 catch 你可以处理的异常,而将你不能处理的异常 throw 出来</strong>,这就是责任链模式的典型应用。</p><p>这只是一个简单的例子,就足以绕半天。实际业务肯定比这个复杂多得多。因此异常处理绝对不是一件容易的事情。</p><p>如果说谁来处理是一件困难的事情,那么在异步中决定谁来处理异常就是难上加难,我们来看下。</p><h2>同步与异步</h2><p>同步异步一直是前端难以跨越的坎,对于异常处理也是一样。以 NodeJS 中用的比较多的<strong>读取文件</strong> API 为例。它有两个版本,一个是异步,一个是同步。同步读取仅仅应该被用在没了这个文件无法进行下去的时候。比如读取一个配置文件。而不应该在比如浏览器中读取用户磁盘上的一个图片等,这样会造成主线程阻塞,导致浏览器卡死。</p><pre><code class="js">// 同步读取文件
fs.readFileSync();
// 异步读取文件
fs.readFile();</code></pre><p>当我们试图<strong>同步</strong>读取一个不存在的文件的时候,会抛出以下异常:</p><pre><code class="js">fs.readFileSync('something-not-exist.lucifer');
console.log('脑洞前端');
Thrown:
Error: ENOENT: no such file or directory, open 'something-not-exist.lucifer'
at Object.openSync (fs.js:446:3)
at Object.readFileSync (fs.js:348:35) {
errno: -2,
syscall: 'open',
code: 'ENOENT',
path: 'something-not-exist.lucifer'
}</code></pre><p>并且<code>脑洞前端</code>是不会被打印出来的。这个比较好理解,我们上面已经解释过了。</p><p>而如果以异步方式的话:</p><pre><code class="js">fs.readFile('something-not-exist.lucifer', (err, data) => {if(err) {throw err}});
console.log('lucifer')
lucifer
undefined
Thrown:
[Error: ENOENT: no such file or directory, open 'something-not-exist.lucifer'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: 'something-not-exist.lucifer'
}
></code></pre><p><code>脑洞前端</code>是会被打印出来的。</p><p>其本质在于 fs.readFile 的函数调用已经成功,并从调用栈返回并执行到下一行的<code>console.log('lucifer')</code>。因此错误发生的时候,调用栈是空的,这一点可以从上面的错误堆栈信息中看出来。</p><blockquote>不明白为什么调用栈是空的同学可以看下我之前写的<a href="https://link.segmentfault.com/?enc=ioWTOBnEqyrPMicthl2jbw%3D%3D.Xo4EpH5Iq9umYb%2BgjlwztU%2FfTVUC5lVQxxelnS6o0iljch7aRg7FA4piaGUXzweQ" rel="nofollow">《一文看懂浏览器事件循环》</a></blockquote><p>而 try catch 的作用仅仅是捕获当前调用栈的错误(上面异常传播部分已经讲过了)。因此异步的错误是无法捕获的,比如;</p><pre><code class="js">try {
fs.readFile("something-not-exist.lucifer", (err, data) => {
if (err) {
throw err;
}
});
} catch (err) {
console.log("catching an error");
}</code></pre><p>上面的 <code>catching an error</code> 不会被打印。因为错误抛出的时候, 调用栈中不包含这个 catch 语句,而仅仅在执行<code>fs.readFile</code>的时候才会。</p><p>如果我们换成同步读取文件的例子看看:</p><pre><code class="js">try {
fs.readFileSync("something-not-exist.lucifer");
} catch (err) {
console.log("catching an error");
}</code></pre><p>上面的代码会打印 <code>catching an error</code>。因为读取文件被同步发起,文件返回之前线程会被挂起,当线程恢复执行的时候, fs.readFileSync 仍然在函数调用栈中,因此 fs.readFileSync 产生的异常会冒泡到 catch 语句。</p><p>简单来说就是<strong>异步产生的错误不能用 try catch 捕获,而要使用回调捕获。</strong></p><p>可能有人会问了,我见过用 try catch 捕获异步异常啊。 比如:</p><pre><code class="js">rejectIn = (ms) =>
new Promise((_, r) => {
setTimeout(() => {
r(1);
}, ms);
});
async function t() {
try {
await rejectIn(0);
} catch (err) {
console.log("catching an error", err);
}
}
t();</code></pre><p>本质上这只是一个语法糖,是 Promise.prototype.catch 的一个语法糖而已。而这一语法糖能够成立的原因在于其用了 Promise 这种包装类型。如果你不用包装类型,比如上面的 fs.readFile 不用 Promise 等包装类型包装,打死都不能用 try catch 捕获。</p><p>而如果我们使用 babel 转义下,会发现 try catch 不见了,变成了 switch case 语句。这就是 try catch “可以捕获异步异常”的原因,仅此而已,没有更多。</p><p><img src="/img/remote/1460000022977781" alt="" title=""><br>(babel 转义结果)</p><p>我使用的 babel 转义环境都记录在<a href="https://link.segmentfault.com/?enc=g3iHHQ8TcWkIdiJpMdjUcw%3D%3D.yIIN0qFXtUFHb1zTjYKyN3jLxuzuHdw6Q5KGobYT2JgD6x7E3zZ1APjyt0UvMGqwsTGfLYwmP%2FiUJaXJEHOBnCsHhLTuq1tFH2A%2Fn7n9cn6j8X9l6UDKKTP3xFmQHIcUd5il9nOXWkfvd49HdAtK7Nau7V4a8qfgbEKZfWtaWNODQ%2BP272mRCsCczXa6gAyT6pSId2eSU7DiHhtYDfPbdkqqV4eQyo48tIDMmYcVIAYX%2FYSqBNgBrcPawk4lfb5AmhDk0AX2C%2Bo%2BApiRkSIRzmtbMDzfqjgo77MiSQsQDmR50%2FNKdS1XicOmE0%2BZFMSFW39PR9peX%2BrouxdHnSFTYznFF9j2qQ6N5SH%2Fd8cT%2FkMLBekY%2BX1G6qWFSvugZsvQ%2FRKGrwBtqNUVCqV4AuNLSrpqx6nvsA4b2ByH3xL%2B4TKPYBYOZH6DywP3fLYN9r%2FuC2ANxpNQqZgXUneswwIBAWh2LyS1f3YcYUiaIbln%2FrAREMEtd2RfbjerEPNyqtLppCVLuuRKx9%2F0dzKaA9%2F6gX8HcpNbjFLcJ8Uf1bIbQ%2FuVYHoM%2B2xP4C91lLgx%2FcnVUr3vzBrDXYKQEfyL1k0Pw7xCet0RtAie0qZcoWm2fKBGEutqjOsCnTl8R%2FNQ47LAKYhLGe0PhoNSx3zRbuCfXx7sbaJRjXqhgjc%2BCnoth5A7%2FYEb3FhQcr2gmvp4LwX5xXd5ovfeNNQWzC3cgaZ5PF%2BDcYPLAjM6taasC2VMDERwNuTKtpqMgd1DQ5SstkKD2puULwYpQrcNgCm4rEnfrnsPSWLqrT6dbUD5FNb6orOTImMYNGbtOsdsEmAz9YXIkAoaCDV3vy4bLXEsYsNou0zFHOBirR8os7OAHlHTOmP1XOI2NxS7cG34W5J66Gd0iPqfHZA5N%2FhD3uloLGbX7tp0RiOqcCAD4sEreFFtedwE4vLa8wyuke0502YsN%2FZam2PfK6QP%2FPRH3VNj9wVU1CWWjiGUGkBuy9t2pOWA3GcfPypQwxK2uebKMad4%2FnyIwlcVDFNe8HFBygiiRL2ORL2A3H0Rt1jhOe4IaL2S5sD3hCBpGpa3gpyfnp9v1Ykn20OpSwLMw4HfuQLBYG3Y60sNAirI8MrG4ErH0kKrwfdUs6KBhLvcf4yqLz1OMfqt" rel="nofollow">这里</a>,大家可以直接点开链接查看.</p><blockquote>虽然浏览器并不像 babel 转义这般实现,但是至少我们明白了一点。目前的 try catch 的作用机制是无法捕获异步异常的。</blockquote><p>异步的错误处理推荐使用容器包装,比如 Promise。然后使用 catch 进行处理。实际上 Promise 的 catch 和 try catch 的 catch 有很多相似的地方,大家可以类比过去。</p><p>和同步处理一样,很多原则都是通用的。比如异步也不要去吞没异常。下面的代码是不好的,因为它吞没了<strong>它不能处理的</strong>异常。</p><pre><code class="js">p = Promise.reject(1);
p.catch(() => {});</code></pre><p>更合适的做法的应该是类似这种:</p><pre><code class="js">p = Promise.reject(1);
p.catch((err) => {
if (err == 1) {
return console.log("I can handle this");
}
throw err;
});</code></pre><h2>彻底消除运行时异常可能么?</h2><p>我个人对目前前端现状最为头疼的一点是:<strong>大家过分依赖运行时,而严重忽略编译时</strong>。我见过很多程序,你如果不运行,根本不知道程序是怎么走的,每个变量的 shape 是什么。怪不得处处都可以看到 console.log。我相信你一定对此感同身受。也许你就是那个写出这种代码的人,也许你是给别人擦屁股的人。为什么会这样? 就是因为大家太依赖运行时。TS 的出现很大程度上改善了这一点,前提是你用的是 typescript,而不是 anyscript。其实 eslint 以及 stylint 对此也有贡献,毕竟它们都是静态分析工具。</p><p>我强烈建议将异常保留在编译时,而不是运行时。不妨极端一点来看:假如所有的异常都在编译时发生,而一定不会在运行时发生。那么我们是不是就可以<strong>信心满满</strong>地对应用进行重构啦?</p><p>幸运的是,我们能够做到。只不过如果当前语言做不到的话,则需要对现有的语言体系进行改造。这种改造成本真的很大。不仅仅是 API,编程模型也发生了翻天覆地的变化,不然函数式也不会这么多年没有得到普及了。</p><blockquote>不熟悉函数编程的可以看看我之前写的<a href="https://link.segmentfault.com/?enc=ld%2BUsE4gf7PqaP4rMFT5%2BA%3D%3D.CRNgqwbHYWpEBzUPE0AhietcNNSbgPvjYovunsq4qt6YWbLFRwK3M4TJsI7q5IgejNGSi8rahrSJoWv02sYVYg%3D%3D" rel="nofollow">函数式编程入门篇</a>。</blockquote><p>如果才能彻底消除异常呢?在回答这个问题之前,我们先来看下一门号称<strong>没有运行时异常</strong>的语言 <a href="https://link.segmentfault.com/?enc=cG3%2BQAVNODwODqixXMv7bw%3D%3D.okoldzKT%2F5%2BJr9icUP%2BrE1Ch9HT12TscssMzzGa1r2bNcYSUa4s%2BrpLscj6bPWHw" rel="nofollow">elm</a>。elm 是一门可以编译为 JS 的函数式编程语言,其封装了诸如网络 IO 等副作用,是一种声明式可推导的语言。 有趣的是,elm 也有异常处理。 elm 中关于异常处理(Error Handling)部分有两个小节的内容,分别是:<code>Maybe</code> 和 <code>Result</code>。elm 之所以没有运行时异常的一个原因就是它们。 一句话概括“为什么 elm 没有异常”的话,那就是<strong>elm 把异常看作数据(data)</strong>。</p><p>举个简单的例子:</p><pre><code class="js">maybeResolveOrNot = (ms) =>
setTimeout(() => {
if (Math.random() > 0.5) {
console.log("ok");
} else {
throw new Error("error");
}
});</code></pre><p>上面的代码有一半的可能报错。那么在 elm 中就不允许这样的情况发生。所有的可能发生异常的代码都会被强制包装一层容器,这个容器在这里是 Maybe。</p><p><img src="/img/remote/1460000022977778" alt="" title=""></p><p>在其他函数式编程语言名字可能有所不同,但是意义相同。实际上,不仅仅是异常,正常的数据也会被包装到容器中,你需要通过容器的接口来获取数据。如果难以理解的话,你可以将其简单理解为 Promsie(但并不完全等价)。</p><p>Maybe 可能返回正常的数据 data,也可能会生成一个错误 error。某一个时刻只能是其中一个,并且只有运行的时候,我们才真正知道它是什么。从这一点来看,有点像薛定谔的猫。</p><p><img src="/img/remote/1460000022977780" alt="" title=""></p><p>不过 Maybe 已经完全考虑到异常的存在,一切都在它的掌握之中。所有的异常都能够在编译时推导出来。当然要想推导出这些东西,你需要对整个编程模型做一定的封装会抽象,比如 DOM 就不能直接用了,而是需要一个中间层。</p><p>再来看下一个更普遍的例子 NPE:</p><pre><code class="js">null.toString();</code></pre><p>elm 也不会发生。原因也很简单,因为 null 也会被包装起来,当你通过这个包装类型就行访问的时候,容器有能力避免这种情况,因此就可以不会发生异常。当然这里有一个很重要的前提就是<strong>可推导</strong>,而这正是函数式编程语言的特性。这部分内容超出了本文的讨论范围,不再这里说了。</p><h2>运行时异常可以恢复么?</h2><p>最后要讨论的一个主题是运行时异常是否可以恢复。先来解释一下,什么是运行时异常的恢复。 还是用上面的例子:</p><pre><code class="js">function t() {
console.log("start");
throw 1;
console.log("end");
}
t();</code></pre><p>这个我们已经知道了, <code>end</code> 是不会打印的。 尽管你这么写也是无济于事:</p><pre><code class="js">function t() {
try {
console.log("start");
throw 1;
console.log("end");
} catch (err) {
console.log("relax, I can handle this");
}
}
t();</code></pre><p>如果我想让它打印呢?我想让程序面对异常可以自己 recover 怎么办?我已经捕获这个错误, 并且我确信我可以处理,让流程继续走下去吧!如果有能力做到这个,这个就是<strong>运行时异常恢复</strong>。</p><p>遗憾地告诉你,据我所知,目前没有任何一个引擎能够做到这一点。</p><p>这个例子过于简单, 只能帮助我们理解什么是运行时异常恢复,但是不足以让我们看出这有什么用?</p><p><img src="/img/remote/1460000022977786" alt="" title=""></p><p>我们来看一个更加复杂的例子,我们这里直接使用上面实现过的函数<code>divide</code>。</p><pre><code class="js">function t() {
try {
const res = divide("foo", "bar");
alert(`you got ${res}`);
} catch (err) {
if (err.code === 1) {
return console.log("被除数必须是除0之外的数");
}
if (err.code === 2) {
return console.log("除数必须是数字");
}
throw new Error("不可预知的错误");
}
}</code></pre><p>如上代码,会进入 catch ,而不会 alert。因此对于用户来说, 应用程序是没有任何响应的。这是不可接受的。</p><blockquote>要吐槽一点的是这种事情真的是挺常见的,只不过大家用的不是 alert 罢了。</blockquote><p>如果我们的代码在进入 catch 之后还能够继续返回出错位置继续执行就好了。</p><p><img src="/img/remote/1460000022977784" alt="" title=""></p><p>如何实现异常中断的恢复呢?我刚刚说了:据我所知,目前没有任何一个引擎能够做到<strong>异常恢复</strong>。那么我就来<strong>发明一个新的语法</strong>解决这个问题。</p><pre><code class="js">function t() {
try {
const res = divide("foo", "bar");
alert(`you got ${res}`);
} catch (err) {
console.log("releax, I can handle this");
resume - 1;
}
}
t();</code></pre><p>上面的 resume 是我定义的一个关键字,功能是如果遇到异常,则返回到异常发生的地方,然后给当前发生异常的函数一个返回值 <strong>-1</strong>,并使得后续代码能够正常运行,不受影响。这其实是一种 fallback。</p><p>这绝对是一个超前的理念。当然挑战也非常大,对现有的体系冲击很大,很多东西都要改。我希望社区可以考虑把这个东西加到标准。</p><h2>最佳实践</h2><p>通过前面的学习,你已经知道了异常是什么,异常是怎么产生的,以及如何正确处理异常(同步和异步)。接下来,我们谈一下异常处理的最佳实践。</p><p>我们平时开发一个应用。 如果站在生产者和消费者的角度来看的话。当我们使用别人封装的框架,库,模块,甚至是函数的时候,我们就是消费者。而当我们写的东西被他人使用的时候,我们就是生产者。</p><p>实际上,就算是生产者内部也会有多个模块构成,多个模块之间也会有生产者和消费者的再次身份转化。不过为了简单起见,本文不考虑这种关系。这里的生产者指的就是给他人使用的功能,是纯粹的生产者。</p><p>从这个角度出发,来看下异常处理的最佳实践。</p><h3>作为消费者</h3><p>当作为消费者的时候,我们关心的是使用的功能是否会抛出异常,如果是,他们有哪些异常。比如:</p><pre><code class="js">import foo from "lucifer";
try {
foo.bar();
} catch (err) {
// 有哪些异常?
}</code></pre><p>当然,理论上 foo.bar 可能产生任何异常,而不管它的 API 是这么写的。但是我们关心的是<strong>可预期的异常</strong>。因此你一定希望这个时候有一个 API 文档,详细列举了这个 API 可能产生的异常有哪些。</p><p>比如这个 foo.bar 4 种可能的异常 分别是 A,B,C 和 D。其中 A 和 B 是我可以处理的,而 C 和 D 是我不能处理的。那么我应该:</p><pre><code class="js">import foo from "lucifer";
try {
foo.bar();
} catch (err) {
if (err.code === "A") {
return console.log("A happened");
}
if (err.code === "B") {
return console.log("B happened");
}
throw err;
}</code></pre><p>可以看出,不管是 C 和 D,还是 API 中没有列举的各种可能异常,我们的做法都是直接抛出。</p><p><img src="/img/remote/1460000022977785" alt="" title=""></p><h3>作为生产者</h3><p>如果你作为生产者,你要做的就是提供上面提到的详细的 API,告诉消费者你的可能错误有哪些。这样消费者就可以在 catch 中进行相应判断,处理异常情况。</p><p><img src="/img/remote/1460000022977788" alt="" title=""></p><p>你可以提供类似上图的错误表,让大家可以很快知道可能存在的<strong>可预知</strong>异常有哪些。不得不吐槽一句,在这一方面很多框架,库做的都很差。希望大家可以重视起来,努力维护良好的前端开发大环境。</p><h2>总结</h2><p>本文很长,如果你能耐心看完,你真得给可以给自己鼓个掌 👏👏👏。</p><p>我从什么是异常,以及异常的分类,让大家正确认识异常,简单来说异常就是一种数据结构而已。</p><p>接着,我又讲到了异常的传播和处理。这两个部分是紧密联系的。异常的传播和事件传播没有本质不同,主要不同是数据结构不同,思想是类似的。具体来说异常会从发生错误的调用处,沿着调用栈回退,直到第一个 catch 语句或者栈为空。如果栈为空都没有碰到一个 catch,则会抛出<strong>uncaught Error</strong>。 需要特别注意的是异步的异常处理,不过你如果对我讲的原理了解了,这都不是事。</p><p>然后,我提出了两个脑洞问题:</p><ul><li>彻底消除运行时异常可能么?</li><li>运行时异常可以恢复么?</li></ul><p>这两个问题非常值得研究,但由于篇幅原因,我这里只是给你讲个轮廓而已。如果你对这两个话题感兴趣,可以和我交流。</p><p>最后,我提到了前端异常处理的最佳实践。大家通过两种角色(生产者和消费者)的转换,认识一下不同决定关注点以及承担责任的不同。具体来说提到了 <strong>明确声明可能的异常</strong>以及 <strong>处理你应该处理的,不要吞没你不能处理的异常</strong>。当然这个最佳实践仍然是轮廓性的。如果大家想要一份 前端最佳实践 checklist,可以给我留言。留言人数较多的话,我考虑专门写一个前端最佳实践 checklist 类型的文章。</p><p>大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。</p>
一文看懂浏览器事件循环
https://segmentfault.com/a/1190000021295911
2019-12-16T10:13:26+08:00
2019-12-16T10:13:26+08:00
lucifer
https://segmentfault.com/u/shuohaodeyixuene
61
<p>实际上浏览器的事件循环标准是由 HTML 标准规定的,具体来说就是由whatwg规定的,具体内容可以参考<a href="https://link.segmentfault.com/?enc=duKkhwZ42MU8GoL2uAlBzg%3D%3D.uBt9rVVjcDpUkw0CLTahpTl0qGCrrf0Afs1tFvCJEHS3K6qwhJZ4R5B97XeChVmj76WotB5xGDx2WQwGZIGb8xDpqPP5WHjqLxawTPlJ9ro%3D" rel="nofollow">event-loops in browser</a>。而NodeJS中事件循环其实也略有不同,具体可以参考<a href="https://link.segmentfault.com/?enc=vuoX9FrXU5shEg93%2FKL%2Feg%3D%3D.6u%2FMxdB7hLIi%2B4GMa9p4cCbEhbs3SelCNHpT6C6u6ouYdPhYvYh5CAAUGNMb9BGPlS34xtJJRF7OHmED2cPfhZA1KaxJ6USy9xhxZcl%2FUCZWeZOtqgDAOjkm%2Bu4MUDyO" rel="nofollow">event-loops in nodejs</a></p><p>我们在讲解<code>事件模型</code>的时候,多次提到了事件循环。 <code>事件</code>指的是其所处理的对象就是事件本身,每一个浏览器都至少有一个事件循环,一个事件循环至少有一个任务队列。<code>循环</code>指的是其永远处于一个“无限循环”中。不断将注册的回调函数推入到执行栈。</p><p>那么事件循环究竟是用来做什么的?浏览器的事件循环和NodeJS的事件循环有什么不同?让我们从零开始,一步一步探究背后的原因。</p><h2>为什么要有事件循环</h2><h3>JS引擎</h3><p>要回答这个问题,我们先来看一个简单的例子:</p><pre><code class="js">function c() {}
function b() {
c();
}
function a() {
b();
}
a();</code></pre><p>以上一段简单的JS代码,究竟是怎么被浏览器执行的?</p><p>首先,浏览器想要执行JS脚本,需要一个“东西”,将JS脚本(本质上是一个纯文本),变成一段机器可以理解并执行的计算机指令。这个“东西”就是JS引擎,它实际上会将JS脚本进行编译和执行,整个过程非常复杂,这里不再过多介绍,感兴趣可以期待下我的V8章节,如无特殊说明,以下都拿V8来举例子。</p><p>有两个非常核心的构成,<code>执行栈</code>和<code>堆</code>。执行栈中存放正在执行的代码,堆中存放变量的值,通常是不规则的。</p><p>当V8执行到<code>a()</code>这一行代码的时候,a会被压入栈顶。</p><p><img src="/img/remote/1460000021295914" alt="" title=""></p><p>在a的内部,我们碰到了<code>b()</code>,这个时候b被压入栈顶。</p><p><img src="/img/remote/1460000021295915" alt="" title=""></p><p>在b的内部,我们又碰到了<code>c()</code>,这个时候c被压入栈顶。</p><p><img src="/img/remote/1460000021295917" alt="" title=""></p><p>c执行完毕之后,会从栈顶移除。</p><p><img src="/img/remote/1460000021295915" alt="" title=""></p><p>函数返回到b,b也执行完了,b也从栈顶移除。</p><p><img src="/img/remote/1460000021295914" alt="" title=""></p><p>同样a也会被移除。</p><p><img src="/img/remote/1460000021295919" alt="" title=""></p><p>整个过程用动画来表示就是这样的:</p><p><img src="/img/remote/1460000021295920" alt="" title=""><br>(<a href="https://link.segmentfault.com/?enc=Qoea153rZVjLhV6bvqPS1g%3D%3D.p9i2NXgYjIFQ59%2Bisj%2F9al6UI9zs49ymi3V%2FDbschpFuVE9IiLZZ4PcTkrxY7fM3HrTLL6y9jZM4wZjB%2BGTlnlXS%2B48Lea3iy5Jew3QSVQZdKRdHf97%2Ffh27Ea3Y%2FHafQ%2Fm9ZbXi1U3TnFcVeiTPv61%2FaBGLM5A8IY1wsv3Df6c%3D" rel="nofollow">在线观看</a>)</p><p>这个时候我们还没有涉及到<code>堆内存</code>和<code>执行上下文栈</code>,一切还比较简单,这些内容我们放到后面来讲。</p><h3>DOM 和 WEB API</h3><p>现在我们有了可以执行JS的引擎,但是我们的目标是<code>构建用户界面</code>,而传统的前端用户界面是基于DOM构建的,因此我们需要引入DOM。DOM是<code>文档对象模型</code>,其提供了一系列JS可以直接调用的接口,理论上其可以提供其他语言的接口,而不仅仅是JS。 而且除了DOM接口可以给JS调用,浏览器还提供了一些WEB API。 DOM也好,WEB API也好,本质上和JS没有什么关系,完全不一回事。JS对应的ECMA规范,V8用来实现ECMA规范,其他的它不管。 这也是JS引擎和JS执行环境的区别,V8是JS引擎,用来执行JS代码,浏览器和Node是JS执行环境,其提供一些JS可以调用的API即<code>JS bindings</code>。</p><p>由于浏览器的存在,现在JS可以操作DOM和WEB API了,看起来是可以构建用户界面啦。 有一点需要提前讲清楚,V8只有栈和堆,其他诸如事件循环,DOM,WEB API它一概不知。原因前面其实已经讲过了,因为V8只负责JS代码的编译执行,你给V8一段JS代码,它就从头到尾一口气执行下去,中间不会停止。</p><p>另外这里我还要继续提一下,JS执行栈和渲染线程是相互阻塞的。为什么呢? 本质上因为JS太灵活了,它可以去获取DOM中的诸如坐标等信息。 如果两者同时执行,就有可能发生冲突,比如我先获取了某一个DOM节点的x坐标,下一时刻坐标变了。 JS又用这个“旧的”坐标进行计算然后赋值给DOM,冲突便发生了。 解决冲突的方式有两种:</p><ol><li>限制JS的能力,你只能在某些时候使用某些API。 这种做法极其复杂,还会带来很多使用不便。</li><li>JS和渲染线程不同时执行就好了,一种方法就是现在广泛采用的<code>相互阻塞</code>。 实际上这也是目前浏览器广泛采用的方式。</li></ol><h3>单线程 or 多线程 or 异步</h3><p>前面提到了<code>你给V8一段JS代码,它就从头到尾一口气执行下去,中间不会停止</code>。 为什么不停止,可以设计成可停止么,就好像C语言一样?</p><p>假设我们需要获取用户信息,获取用户的文章,获取用的朋友。</p><h4>单线程无异步</h4><p>由于是单线程无异步,因此我们三个接口需要采用同步方式。</p><pre><code class="js">fetchUserInfoSync().then(doSomethingA); // 1s
fetchMyArcticlesSync().then(doSomethingB);// 3s
fetchMyFriendsSync().then(doSomethingC);// 2s</code></pre><p>由于上面三个请求都是同步执行的,因此上面的代码会先执行<code>fetchUserInfoSync</code>,一秒之后执行<code>fetchMyArcticlesSync</code>,再过三秒执行<code>fetchMyFriendsSync</code>。 最可怕的是我们刚才说了<code>JS执行栈和渲染线程是相互阻塞的</code>。 因此用户就在这期间根本无法操作,界面无法响应,这显然是无法接受的。</p><h4>多线程无异步</h4><p>由于是多线程无异步,虽然我们三个接口仍然需要采用同步方式,但是我们可以将代码分别在多个线程执行,比如我们将这段代码放在三个线程中执行。</p><p>线程一:</p><pre><code class="js">fetchUserInfoSync().then(doSomethingA); // 1s</code></pre><p>线程二:</p><pre><code class="js">fetchMyArcticlesSync().then(doSomethingB); // 3s</code></pre><p>线程三:</p><pre><code class="js">fetchMyFriendsSync().then(doSomethingC); // 2s</code></pre><p><img src="/img/remote/1460000021295922" alt="1575538849801.jpg" title="1575538849801.jpg"></p><p>由于三块代码同时执行,因此总的时间最理想的情况下取决与最慢的时间,也就是3s,这一点和使用异步的方式是一样的(当然前提是请求之间无依赖)。为什么要说最理想呢?由于三个线程都可以对DOM和堆内存进行访问,因此很有可能会冲突,冲突的原因和我上面提到的JS线程和渲染线程的冲突的原因没有什么本质不同。因此最理想情况没有任何冲突的话是3s,但是如果有冲突,我们就需要借助于诸如<code>锁</code>来解决,这样时间就有可能高于3s了。 相应地编程模型也会更复杂,处理过锁的程序员应该会感同身受。</p><h4>单线程 + 异步</h4><p>如果还是使用单线程,改成异步是不是会好点?问题的是关键是如何实现异步呢?这就是我们要讲的主题 - <code>事件循环</code>。</p><h2>事件循环究竟是怎么实现异步的?</h2><p>我们知道浏览器中JS线程只有一个,如果没有事件循环,就会造成一个问题。 即如果JS发起了一个异步IO请求,在等待结果返回的这个时间段,后面的代码都会被阻塞。 我们知道JS主线程和渲染进程是相互阻塞的,因此这就会造成浏览器假死。 如何解决这个问题? 一个有效的办法就是我们这节要讲的<code>事件循环</code>。</p><p>其实<code>事件循环就是用来做调度的,浏览器和NodeJS中的事件循坏就好像操作系统的调度器一样。</code>操作系统的调度器决定何时将什么资源分配给谁。对于有线程模型的计算机,那么操作系统执行代码的最小单位就是线程,资源分配的最小单位就是进程,代码执行的过程由操作系统进行调度,整个调度过程非常复杂。 我们知道现在很多电脑都是多核的,为了让多个core同时发挥作用,即没有一个core是特别闲置的,也没有一个core是特别累的。操作系统的调度器会进行某一种神秘算法,从而保证每一个core都可以分配到任务。 这也就是我们使用NodeJS做集群的时候,Worker节点数量通常设置为core的数量的原因,调度器会尽量将每一个Worker平均分配到每一个core,当然这个过程并不是确定的,即不一定调度器是这么分配的,但是很多时候都会这样。</p><p>了解了操作系统调度器的原理,我们不妨继续回头看一下事件循环。 事件循环本质上也是做调度的,只不过调度的对象变成了JS的执行。事件循环决定了V8什么时候执行什么代码。<code>V8只是负责JS代码的解析和执行,其他它一概不知。</code>浏览器或者NodeJS中触发事件之后,到事件的监听函数被V8执行这个时间段的所有工作都是事件循环在起作用。</p><p>我们来小结一下:</p><ol><li>对于V8来说,它有:</li></ol><ul><li>调用栈(call stack)</li></ul><blockquote>这里的单线程指的是只有一个call stack。只有一个call stack 意味着同一时间只能执行一段代码。</blockquote><ul><li>堆(heap)</li></ul><ol><li>对于浏览器运行环境来说:</li></ol><ul><li>WEB API</li><li>DOM API</li><li>任务队列</li></ul><blockquote>事件来触发事件循环进行流动</blockquote><p>以如下代码为例:</p><pre><code class="js">function c() {}
function b() {
c();
}
function a() {
setTimeout(b, 2000)
}
a();
</code></pre><p>执行过程是这样的:</p><p><img src="/img/remote/1460000021295921" alt="" title=""><br>(<a href="https://link.segmentfault.com/?enc=TVlzQbJ6DIAJCzRKOajKTg%3D%3D.z6wI8egIuONgqCP0MDYGZE0RNc7EV7%2FQ60c7TwKnE8l1xMsoyt2mtsurnt1U2xXl%2BeR0L%2Fm1ci4neRvg7J4fCfMc7TdcAx%2FFzMEWu6QZ9evmG6nAUHXnQpsIbGDdQAtCDK4wtXIZNJV5Wxw6prFi2cJB8wsWKxC%2BELZKZW7BwShvz4u6gsfrHJq1Kyl%2Bzd54EI1uk3DJ9F166fuhcVWjRg%3D%3D" rel="nofollow">在线观看</a>)</p><p>因此事件循环之所以可以实现异步,是因为碰到异步执行的代码“比如fetch,setTimeout”,浏览器会将用户注册的回调函数存起来,然后继续执行后面的代码。等到未来某一个时刻,“异步任务”完成了,会触发一个事件,浏览器会将“任务的详细信息”作为参数传递给之前用户绑定的回调函数。具体来说,就是将用户绑定的回调函数推入浏览器的执行栈。</p><p>但并不是说随便推入的,只有浏览器将当然要执行的JS脚本“一口气”执行完,要”换气“的时候才会去检查有没有要被处理的“消息”。<br>如果于则将对应消息绑定的回调函数推入栈。当然如果没有绑定事件,这个事件消息实际上会被丢弃,不被处理。比如用户触发了一个click事件,但是用户没有绑定click事件的监听函数,那么实际上这个事件会被丢弃掉。</p><p>我们来看一下加入用户交互之后是什么样的,拿点击事件来说:</p><pre><code class="js">$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('You clicked the button!');
}, 2000);
});
console.log("Hi!");
setTimeout(function timeout() {
console.log("Click the button!");
}, 5000);
console.log("Welcome to loupe.");</code></pre><p>上述代码每次点击按钮,都会发送一个事件,由于我们绑定了一个监听函数。因此每次点击,都会有一个点击事件的消息产生,浏览器会在“空闲的时候”对应将用户绑定的事件处理函数推入栈中执行。</p><p>伪代码:</p><pre><code class="js">while (true) {
if (queue.length > 0) {
queue.processNextMessage()
}
}
</code></pre><p>动画演示:</p><p><img src="/img/remote/1460000021295923" alt="" title=""><br>(<a href="https://link.segmentfault.com/?enc=TNxO7S6USFI7DZmOwbw3wA%3D%3D.xr3%2BvaeD%2BdPtImocPoE%2FqT7sLHobU4V6YfKMJJGDEJSH4%2FyUu1mO6fw04usz8rWKmzsm1LIWnBC9Rzw2gYX02lHPBb8noRu6CMcwYkcT4p%2BeAIa1IlatxBj4CEiU3Z2%2FcNbvPsaZJSYLwby4iKXWWPpUT6iHezlMSO75g%2BoD6X8C2dXf1mEUrpE%2FhaZ%2Bqt8E4uAhtBBX5cBuNDAlMgxw6%2BMTsjMKIIqK0EA%2FjeZ5gZLsO17jSlKmhtmfeXbbohAO1YeKxlgpooVk9x68X1InXcG1AMpIN3Hk3bfJTAFc3UJ4whWD%2FYzWvXMlFlS2V5MVTyxxzZtEkxJJrGsUbgWECC2WVwtbl9cgNFx0zxUuP6w9D8%2FZFrKqawbd371GEpWhUrF67Ztd3knEWO%2BA2KRVUSVG01CwMDZX4d%2Bzv0F3REzPhBmDBULqdGgvzCUMmcl%2FSdg69WWC%2Bx%2Bw8iDNjwxp%2BN%2FYGM0hZWrhjrVBKR38o5aegNxIOlCa3Ly50ysujwI2izmHKL%2FLng%2F3BOHporLTyBePQqAgvjt9VzJJlzbTr3q9ak1b2owGUf%2BRnoNEq23FQxukgdOWeYtxD%2FpgjxwnnGdyF8W1lPa65GFN0o2kLDQ%3D" rel="nofollow">在线观看</a>)</p><h3>加入宏任务&微任务</h3><p>我们来看一个更复制的例子感受一下。</p><pre><code class="js">console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
return console.log(3)
}).then(() => {
console.log(4)
})
console.log(5)</code></pre><p>上面的代码会输出:1、5、3、4、2。 如果你想要非常严谨的解释可以参考 whatwg 对其进行的描述 -<a href="https://link.segmentfault.com/?enc=QXldGXs%2BmHvD0GO76fbMlg%3D%3D.%2BY2wbUcVdp46XmFdPapDAkYRoZPLRKjZEO1R%2BezJDQPae3R1%2B%2FZs17MvEqbzx6Le0k1ztOHPT5ilpR2wlWPPkm8B%2BPTy8AKvsFraTHG1iy3PpVIQd9nr6yNuN6CYe4VI" rel="nofollow">event-loop-processing-model</a>。</p><p>下面我会对其进行一个简单的解释。</p><ul><li>浏览器首先执行宏任务,也就是我们script(仅仅执行一次)</li><li>完成之后检查是否存在微任务,然后不停执行,直到清空队列</li><li>执行宏任务</li></ul><p>其中:</p><p>宏任务主要包含:setTimeout、setInterval、setImmediate、I/O、UI交互事件</p><p>微任务主要包含:Promise、process.nextTick、MutaionObserver 等</p><p><img src="/img/remote/1460000021295924" alt="" title=""></p><p>有了这个知识,我们不难得出上面代码的输出结果。</p><p>由此我们可以看出,<code>宏任务&微任务</code>只是实现异步过程中,我们对于信号的处理顺序不同而已。如果我们不加区分,全部放到一个队列,就不会有<code>宏任务&微任务</code>。这种人为划分优先级的过程,在某些时候非常有用。</p><h3>加入执行上下文栈</h3><p>说到执行上下文,就不得不提到<code>浏览器执行JS函数其实是分两个过程的</code>。一个是创建阶段<code>Creation Phase</code>,一个是执行阶段<code>Execution Phase</code>。</p><p>同执行栈一样,浏览器每遇到一个函数,也会将当前函数的执行上下文栈推入栈顶。</p><p>举个例子:</p><pre><code class="js">function a(num) {
function b(num) {
function c(num) {
const n = 3
console.log(num + n)
}
c(num);
}
b(num);
}
a(1);</code></pre><p>遇到上面的代码。 首先会将a的压入执行栈,我们开始进行创建阶段<code>Creation Phase</code>, 将a的执行上下文压入栈。然后初始化a的执行上下文,分别是VO,ScopeChain(VO chain)和 This。 从这里我们也可以看出,this其实是动态决定的。VO指的是<code>variables, functions 和 arguments</code>。 并且执行上下文栈也会同步随着执行栈的销毁而销毁。</p><p>伪代码表示:</p><pre><code class="js">const EC = {
'scopeChain': { },
'variableObject': { },
'this': { }
}
</code></pre><p><img src="/img/remote/1460000021295925" alt="" title=""></p><p>我们来重点看一下ScopeChain(VO chain)。如上图的执行上下文大概长这个样子,伪代码:</p><pre><code class="js">
global.VO = {
a: pointer to a(),
scopeChain: [global.VO]
}
a.VO = {
b: pointer to b(),
arguments: {
0: 1
},
scopeChain: [a.VO, global.VO]
}
b.VO = {
c: pointer to c(),
arguments: {
0: 1
},
scopeChain: [b.VO, a.VO, global.VO]
}
c.VO = {
arguments: {
0: 1
},
n: 3
scopeChain: [c.VO, b.VO, a.VO, global.VO]
}</code></pre><p>引擎查找变量的时候,会先从VOC开始找,找不到会继续去VOB...,直到GlobalVO,如果GlobalVO也找不到会返回<code>Referrence Error</code>,整个过程类似原型链的查找。</p><p>值得一提的是,JS是词法作用域,也就是静态作用域。换句话说就是作用域取决于代码定义的位置,而不是执行的位置,<code>这也就是闭包产生的本质原因</code>。 如果上面的代码改造成下面的:</p><pre><code class="js">function c() {}
function b() {}
function a() {}
a()
b()
c()</code></pre><p>或者这种:</p><pre><code class="js">function c() {}
function b() {
c();
}
function a() {
b();
}
a();
</code></pre><p>其执行上下文栈虽然都是一样的,但是其对应的scopeChain则完全不同,因为函数定义的位置发生了变化。拿上面的代码片段来说,c.VO会变成这样:</p><pre><code class="js">c.VO = {
scopeChain: [c.VO, global.VO]
}
</code></pre><p>也就是说其再也无法获取到a和b中的VO了。</p><h2>总结</h2><p>通过这篇文章,希望你对单线程,多线程,异步,事件循环,事件驱动等知识点有了更深的理解和感悟。除了这些大的层面,我们还从执行栈,执行上下文栈角度讲解了我们代码是如何被浏览器运行的,我们顺便还解释了作用域和闭包产生的本质原因。</p><p>最后我总结了一个浏览器运行代码的整体原理图,希望对你有帮助:</p><p><img src="/img/remote/1460000021295926" alt="" title=""></p><p>下一节<code>浏览器的事件循环和NodeJS的事件循环有什么不同</code>, 敬请期待~</p><h2>参考</h2><ul><li><a href="https://link.segmentfault.com/?enc=1IKTiBftYpCnM%2BG%2FxkImEg%3D%3D.mf3Jmkv6dgOaiQRxt8bUgPF%2FqrSqrlKG2aFzC3cIGao4MLxncYSJb7GaFBY7PT8CnQWfC49jiOyLh0zy%2FNdGOFlHbdvtnwX%2FUE%2BrslpYUyw%3D" rel="nofollow">Node.js event loop - logrocket</a></li><li><a href="https://link.segmentfault.com/?enc=T%2BJS91WSNJMGJaG%2Be6cT%2BQ%3D%3D.j8qoJhK1Z0UhXpVvvdjSDbsmlppHLvCnlkgc%2FCWL1QJuKYjNULy6QkqX8F%2BPIZKWfduZD7WYfnVayIaMoCmUyogDiN8Z9nImPKOUugxQqyg%3D" rel="nofollow">event-loop - nodejs.org</a></li><li><a href="https://link.segmentfault.com/?enc=lQsUjUteHCWE%2FIuqQYUfew%3D%3D.w1b5%2B3jRsip2Gltv70S6s7HrNrJEKaHqult8TTMca958xiaotB3H2BzBB4qa9AAXcEqUItM9ogN2K6trC2AlXKNGYSn0z6uzc5aPeO%2BrrMQ%3D" rel="nofollow">what-is-the-execution-context-in-javascript</a></li><li><a href="https://link.segmentfault.com/?enc=apk%2B3q9BAQpK96qGp8%2FYoA%3D%3D.yjb2Zo4oCeU%2BBxbF5WY8pev7MHtmXciZpJ0NYj4dBQmyZA4cWAT4z%2BxNWPr83fKU" rel="nofollow">Event Loop in JS - youtube </a></li></ul><h2>关注我</h2><p>觉得不错点个赞👍,欢迎 加群 互相学习。我的个人博客:<a href="https://link.segmentfault.com/?enc=SYm5tBk%2BpC9dg%2FSHhRmmNA%3D%3D.K%2FJ4E1Q0yBbwStuKpjBK2c9aP59lk%2FWse808QNzK5TE%3D" rel="nofollow">https://lucifer.ren/blog/</a></p><p>也欢迎关注我的个人公众号,原创好货持续更新!<br><img src="/img/remote/1460000021296018" alt="" title=""></p>