<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>繁易の前端乐园</title>
  <icon>https://www.gravatar.com/avatar/9180eef1e0707a144cc8748fc78e8c37</icon>
  <subtitle>不为繁华易匠心</subtitle>
  <link href="/feed.xml" rel="self"/>
  
  <link href="https://blog.lxxyx.cn/"/>
  <updated>2023-01-02T12:32:51.309Z</updated>
  <id>https://blog.lxxyx.cn/</id>
  
  <author>
    <name>Lxxyx</name>
    <email>Lxxyxzj@gmail.com</email>
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>1169.7km，我的第一次自驾游</title>
    <link href="https://blog.lxxyx.cn/2022/09/14/2022/travel/"/>
    <id>https://blog.lxxyx.cn/2022/09/14/2022/travel/</id>
    <published>2022-09-14T12:04:29.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663158763455-1ce9f422-1a55-482a-bee5-c995fad68d66.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1329&amp;id=Q5n1h&amp;margin=%5Bobject%20Object%5D&amp;name=2a18bff4a6f0ae143619e2eb41af992.jpg&amp;originHeight=1661&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=1131115&amp;status=done&amp;style=none&amp;taskId=u1f761a7c-baef-4148-b1a1-ed3d50a1406&amp;title=&amp;width=1027.2" alt="2a18bff4a6f0ae143619e2eb41af992.jpg"></p><p>买车已 3 月有余，适逢中秋，天气也算凉爽，于是临时开始做起了自驾游的攻略。<br>与平时出去玩不同，自驾游更像是一种随性的选择。明天要哪儿、中间时间怎么安排、在哪里吃、住哪里都是浑然没有安排的。<br>走到哪儿算哪儿，快到地点时找饭店，吃饭时候看攻略，下午五六点才预定酒店也算是这几天的常态了。<br><a name="c9M8B"></a></p><h2 id="出发前"><a href="#出发前" class="headerlink" title="出发前"></a>出发前</h2><p><a name="W76o9"></a></p><h3 id="物资"><a href="#物资" class="headerlink" title="物资"></a>物资</h3><p>出发的前一天，临时在盒马囤了 300 多的物资和小零食，基本上是感觉啥有用就买啥。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663131499943-17d20eb2-3010-4982-b647-f647f9b3e3f1.jpeg#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=631&amp;id=ud5707b31&amp;margin=%5Bobject%20Object%5D&amp;name=IMG_EE0E3C5AD0AD-1.jpeg&amp;originHeight=631&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=230608&amp;status=done&amp;style=none&amp;taskId=u59db84de-3a3b-46b7-91e8-8777570529e&amp;title=&amp;width=1284" alt="IMG_EE0E3C5AD0AD-1.jpeg"></p><p>现在来看其实还是买多了，大约有一半都没有用上或者吃掉（特别是自热火锅/米饭，在当地有美食的情况下真的完全不会打开）</p><p>如果再来一次，我可能会着重囤一些水果、没有刺激味道且不是汤汤水水的小零食，一次性的内裤、袜子方便换洗等。</p><p><a name="oIkQI"></a></p><h3 id="补能"><a href="#补能" class="headerlink" title="补能"></a>补能</h3><p>我的车子是增程式的混动车，支持 180km 的纯电续航与 800 km 左右的纯油续航。</p><p>出发前去了一趟家门口的中石化加油中，加满了油后又去充电站把电池充到了 95%。满油满电折合 1000+km 的续航，安全感满满的。</p><blockquote><p>回来之后重新补满了</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663132063231-ccf848c1-4ee7-4561-bd63-0425b2aa123c.jpeg#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=648&amp;id=uc7a4278f&amp;margin=%5Bobject%20Object%5D&amp;name=IMG_141B9A4F640E-1.jpeg&amp;originHeight=648&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=95858&amp;status=done&amp;style=none&amp;taskId=ub0a487c7-b183-4797-8709-d82947b5e23&amp;title=&amp;width=1284" alt="IMG_141B9A4F640E-1.jpeg"></p><p><a name="AdsGi"></a></p><h3 id="路线图"><a href="#路线图" class="headerlink" title="路线图"></a>路线图</h3><p>因为之前有买过江浙沪的自驾路书，所以这次也派上了用场，最终选择了环清凉峰的路线。考虑到整体行程 160km 左右（到达龙岗收费站后）。时间也适宜，就出发了。</p><p>只是没有想到，最终这种路书地图，我们只用到了半张图。</p><blockquote><p>收费攻略，隐藏了本次无关的路线，有需要请自行购买。</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663132391679-6f17adda-5841-4c5c-b26e-de9b9c087cb8.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1054&amp;id=ucb44a77c&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1054&amp;originWidth=1541&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=1954205&amp;status=done&amp;style=none&amp;taskId=ude18fca8-6dd3-4d61-abbb-dcd403c60a9&amp;title=&amp;width=1541" alt="image.png"></p><blockquote><p>视频攻略</p></blockquote><p><a href="https://player.bilibili.com/player.html?bvid=BV1w14y1x7BY&amp;p=1&amp;page=1">点击查看【bilibili】</a></p><blockquote><p>Bilibili 图片</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663132626315-9940f9ad-93ce-49a7-bced-3028a28804b7.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1219&amp;id=uef4106ba&amp;margin=%5Bobject%20Object%5D&amp;name=IMG_3226.PNG&amp;originHeight=1219&amp;originWidth=960&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=912725&amp;status=done&amp;style=none&amp;taskId=u6640f5ee-450d-49ac-b646-2477afbeace&amp;title=&amp;width=960" alt="IMG_3226.PNG"></p><p><a name="JCnnI"></a></p><h2 id="Day-1-杭州-临安大明山清凉峰"><a href="#Day-1-杭州-临安大明山清凉峰" class="headerlink" title="Day 1 杭州 - 临安大明山清凉峰"></a>Day 1 杭州 - 临安大明山清凉峰</h2><p><a name="NWTb3"></a></p><h3 id="出发"><a href="#出发" class="headerlink" title="出发"></a>出发</h3><p>早起，在收拾完行李后，打开导航看了一眼，果不其然路上开始堵车了。中秋的第一天，看起来大家都想出去玩儿。</p><p>尽管心里早有准备，但一路上的堵车情况还是超出了我们的预期。原计划 1 小时就能抵达的收费站，最终花费了接近两小时才到达。中间高速有一段路全程都是 20 码的在开。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663133000420-7868d9ea-39c2-4552-b237-381e0af26164.jpeg#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=563&amp;id=u38990a3d&amp;margin=%5Bobject%20Object%5D&amp;name=IMG_44331D4D5E26-1.jpeg&amp;originHeight=563&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=241776&amp;status=done&amp;style=none&amp;taskId=ue8cb7eea-b321-452f-891e-b2b04dc50ad&amp;title=&amp;width=1284" alt="IMG_44331D4D5E26-1.jpeg"><br><a name="woO3A"></a></p><h3 id="景观"><a href="#景观" class="headerlink" title="景观"></a>景观</h3><p>抵达龙岗收费站后，我们马不停蹄的前往今天的第一个景观：石浪。</p><p>石浪景观本身是不收费的，入口也比较隐蔽，如果没有路书提示我们应该会直接错过。<br><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663133235860-da4fb45f-69b8-43ae-97ce-60c40b0cd48f.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1278&amp;id=ub460f2f5&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1278&amp;originWidth=1706&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=560900&amp;status=done&amp;style=none&amp;taskId=u45928617-fdda-4f87-a27b-fac6dcb79d1&amp;title=&amp;width=1706" alt="image.png"></p><blockquote><p>石浪景观</p></blockquote><p>此处风景我觉得一般，可能是对这么多大石头无感吧 XD，也有可能是因为当天下午天气还是比较晒的状态，在山上走着还是挺热的，也缺了观赏风景的心。</p><p>站在山上的观景亭，能看到远处的华光潭大桥，桥底的水是碧绿的，反而挺好看。<br><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663133462693-34984328-e5ba-4a6b-b102-6a0e6ee6b564.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=3024&amp;id=u47a2419f&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=3024&amp;originWidth=4032&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=15348115&amp;status=done&amp;style=none&amp;taskId=ub212f484-4e88-46e4-9cda-39b57aabba6&amp;title=&amp;width=4032" alt="image.png"></p><blockquote><p>华光潭大桥</p></blockquote><p>稍作休息后，跨过华光潭大桥后去了兔川悬索桥。在华光潭大桥建起来之前，当地人民都是走兔川悬索桥过河的。<br><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663133598338-daab4dff-5779-4e9a-8718-ad1ec3befc79.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1706&amp;id=ubb151f20&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1706&amp;originWidth=1278&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=229405&amp;status=done&amp;style=none&amp;taskId=uc6042ef4-f487-4805-94bd-3a33f5bf37c&amp;title=&amp;width=1278" alt="image.png"></p><p>这也是我当天非常满意的景点（未开发，看路书攻略找的），风很大，桥两边的水也很清澈，吹着很舒服但也带着一丝惶恐，生怕掉下去，所以走了一半就回头了。</p><p>继续往前开，路边有一条溪流，因为过往天气热的原因，河里水流不多，河床裸露。<br>女朋友提议下去玩水，考虑了一下不影响今天的行程（虽然那时候我还不知道住哪儿），就下去游玩了。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663133929207-d5310164-3556-4f6f-822c-06acff58f3ee.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1177&amp;id=u9680521c&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1177&amp;originWidth=1583&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=4425588&amp;status=done&amp;style=none&amp;taskId=u8a96d113-d515-4bfe-99c5-ba1ca850f1c&amp;title=&amp;width=1583" alt="image.png"></p><p>可惜的是出门匆忙，只准备了一把椅子，我只能在边上的石头上站着。<br><a name="TaRFl"></a></p><h3 id="盘山公路"><a href="#盘山公路" class="headerlink" title="盘山公路"></a>盘山公路</h3><p>再往上走，就是一段二十多公里的盘山公路，华浪线。山路比较陡，弯也多，但公路是新修缮的，整体体验非常不错。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663134185872-fb57bc2b-dbad-440e-afcc-f1d5f9a1ae95.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1280&amp;id=I9CDC&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1280&amp;originWidth=1706&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=187628&amp;status=done&amp;style=none&amp;taskId=u5ad41793-b773-4f5b-a567-299b14101cd&amp;title=&amp;width=1706" alt="image.png"></p><blockquote><p>自驾时导航的路线图</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663134108387-3fd87d52-758b-4e24-b7c5-46993f7214c0.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1161&amp;id=u9b7ec7e7&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1161&amp;originWidth=1586&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=2955871&amp;status=done&amp;style=none&amp;taskId=uf724cf9f-befe-4001-a83b-91d439fd001&amp;title=&amp;width=1586" alt="image.png"></p><blockquote><p>山顶的风景</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663134365925-a1040a88-4d7d-4c0c-810f-da5cca84d79a.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1170&amp;id=u1855909e&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1170&amp;originWidth=1579&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=2819632&amp;status=done&amp;style=none&amp;taskId=ua7e16763-66e1-4bec-a47d-df4aef57645&amp;title=&amp;width=1579" alt="image.png"></p><blockquote><p>观景台</p></blockquote><p>随后顺着盘山公路下山，抵达石长城景区，当时天色已经快黑了所以没有进去游玩，短暂的歇息了一会儿。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1663134707469-fe7ae4fa-10f9-4746-8e4b-07c836750e2d.png#clientId=u195e2b95-11bc-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1280&amp;id=u537da1d3&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1280&amp;originWidth=1706&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=412764&amp;status=done&amp;style=none&amp;taskId=ubd43f3c6-5c5b-464f-981c-6249a07e2e3&amp;title=&amp;width=1706" alt="image.png"></p><p>歇息的时候，吃了根香蕉，看着对面的人家在处理新鲜摘下来的山核桃，才知道原来核桃剥壳都是机械化的。</p><blockquote><p>不得不说的是，在临安自驾的路上，真的是全员都在搞山核桃，太夸张了。事后也了解到，山核桃的树不是随便采摘的，而是村民自己种的。</p></blockquote><p><a name="CX0qu"></a></p><h3 id="前往大明山-amp-入住"><a href="#前往大明山-amp-入住" class="headerlink" title="前往大明山 &amp; 入住"></a>前往大明山 &amp; 入住</h3><p>在歇息的片刻，我们定下了当晚的酒店，大明春天。</p><p>这是一个路书视频上推荐的酒店，在携程上预定要 360，电话问老板有没有别的渠道预定时则反馈还有去哪儿，只需要 260，感觉自己省下了一百多。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160215683-ef5b6670-4971-4632-8cc9-d0cfb0486f8b.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=658&amp;id=u5f0f86e5&amp;margin=%5Bobject%20Object%5D&amp;name=2ac8c4dfcad1c6c702ad938c35559f1.jpg&amp;originHeight=823&amp;originWidth=1098&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=154659&amp;status=done&amp;style=none&amp;taskId=ubb831c76-494a-40dd-901b-dc1d2c02e6b&amp;title=&amp;width=878.4" alt="2ac8c4dfcad1c6c702ad938c35559f1.jpg"></p><blockquote><p>大明山入口</p></blockquote><p>开车到民宿附近，整个一片都是做民宿相关的，当然家家户户也在搞着山核桃。简单办理了入住，酒店条件一般，但也还行（毕竟景区）。</p><p>想着都到山里了，那么去吃点农家乐，结果一连找了几家餐馆，都反应接待不过来。最后找的一家，也是等了一个多小时才上的菜。<br>问老板，说参观一般都没有厨师，都是夫妻店。加上近期受疫情影响，又是旅游淡季，餐馆一般没有怎么备菜，所以招待不过来。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160191249-8175d127-c674-4b2e-a21e-fdb72c1340d6.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1024&amp;id=u4c38daa5&amp;margin=%5Bobject%20Object%5D&amp;name=778fb5880e60d88b76a1b80f5ac566f.jpg&amp;originHeight=1280&amp;originWidth=960&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=285508&amp;status=done&amp;style=none&amp;taskId=u1ab28f84-685d-4101-8b4a-e3321e76738&amp;title=&amp;width=768" alt="778fb5880e60d88b76a1b80f5ac566f.jpg"></p><blockquote><p>山里的小河</p></blockquote><p>而后就是简单的休息了，因为是山里，虫子很多，这点体验很糟糕。晚上实在受不了下去找前台要了点蚊香，点起来后又熏得睡不着，属实是进退两难。<br>最后实在是没有办法了，把酒店的纱窗打开，蚊香移到阳台，静静呆了十几分钟后呼吸到山里的新鲜空气后，才安然睡去。</p><p>如果下次出游，还是尽可能选标准的酒店吧，至少能睡个好觉。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663156573734-93749e11-d0ec-4c17-be0e-ff4bf923fc49.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1232&amp;id=u8c2ea149&amp;margin=%5Bobject%20Object%5D&amp;name=6e68257bf32263bc0b5c3bd7e8d6bd2.jpg&amp;originHeight=1540&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=497033&amp;status=done&amp;style=none&amp;taskId=u5d738a92-904e-4c4d-9953-92c1871a6c8&amp;title=&amp;width=1027.2" alt="6e68257bf32263bc0b5c3bd7e8d6bd2.jpg"></p><blockquote><p>旅程轨迹</p></blockquote><p><a name="KSKfb"></a></p><h2 id="Day-2-临安-婺源晒秋-amp-上饶"><a href="#Day-2-临安-婺源晒秋-amp-上饶" class="headerlink" title="Day 2 临安 - 婺源晒秋 &amp; 上饶"></a>Day 2 临安 - 婺源晒秋 &amp; 上饶</h2><p>上午起来，吃了酒店的提供的早餐。虽然就是简单的稀饭 + 玉米、鸡蛋、饼之类的食物，但粥刚好温凉，喝起来格外的惬意。</p><p>在昨晚睡前，我们考虑是要去千岛湖还是继续原路线走山路。但女朋友很明确的表达不想再跑山路了，莫得意思。于是我在观察地图后，提出了新的地点：婺源。</p><p>洗好水果，备好车车，我们就准备出发了。<br><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160107478-2df1aefe-8c87-46ce-9f56-430a51253c04.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=u49c92c44&amp;margin=%5Bobject%20Object%5D&amp;name=a876ef44108f84b4921bbabe1d37430.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=225825&amp;status=done&amp;style=none&amp;taskId=u0a2869f2-10e0-4aed-a0c5-0f1bbe89f43&amp;title=&amp;width=871.2" alt="a876ef44108f84b4921bbabe1d37430.jpg"><br><a name="zgCIZ"></a></p><h3 id="婺源"><a href="#婺源" class="headerlink" title="婺源"></a>婺源</h3><p>我们选择的是婺源的篁岭景区。从临安大明山出发，单程 2 小时左右。<br>不知道是线路本身就比较空，还是因为当天是中秋假期第二天的缘故，道路上格外的空，一路上都看不见几辆车。和女朋友两个人换着开，一人一个小时，很快就到了。</p><p>中途还下错了服务区，和高速的工作人员说明了情况希望掉头返回，他还给我们指明了路线，让我们原地掉头重新上了高数，没有浪费一分钟的时间。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160231786-854751cd-ed0b-4821-b925-38b8bc49d659.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=ua9ddc3c2&amp;margin=%5Bobject%20Object%5D&amp;name=aa2ec57e50822308f406febfc163dca.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=109571&amp;status=done&amp;style=none&amp;taskId=u701726a0-28bf-420d-a7fe-a9991befb49&amp;title=&amp;width=871.2" alt="aa2ec57e50822308f406febfc163dca.jpg"></p><p>到达婺源后选了大众点评排名第一的餐馆吃了午餐，价格还是略贵的， ￥198，但是味道也算对得起这个价格，很下饭。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663150460145-14e491aa-731b-4969-a737-13fff680ae21.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1048&amp;id=uaf89e520&amp;margin=%5Bobject%20Object%5D&amp;name=94c4ede7916b3c8f260dc3ddabd7d09.jpg&amp;originHeight=1310&amp;originWidth=1279&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=139642&amp;status=done&amp;style=none&amp;taskId=u5cbbb068-d4e6-4935-b0ed-a62eb986147&amp;title=&amp;width=1023.2" alt="94c4ede7916b3c8f260dc3ddabd7d09.jpg"></p><p>在进景区之前，买了两顶草帽，确认上饶户口优惠政策已经结束了，就买票坐索道上去了。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160243731-9e6d974a-26e4-452c-87c2-042ea6424ff7.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=u7d554938&amp;margin=%5Bobject%20Object%5D&amp;name=56d11ce45f444bd06fa0586a6d75317.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=142542&amp;status=done&amp;style=none&amp;taskId=ua6ecaad0-9486-4466-9a9a-6934c71a872&amp;title=&amp;width=871.2" alt="56d11ce45f444bd06fa0586a6d75317.jpg"></p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160255825-f77b0e64-5e3c-4552-87d3-6b53d9b65e56.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=u7439fd88&amp;margin=%5Bobject%20Object%5D&amp;name=cdf01d4e4ef43ef28225caf4bfe4767.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=294848&amp;status=done&amp;style=none&amp;taskId=u04f9f701-cdf6-44a2-a78f-fd8ce290f90&amp;title=&amp;width=871.2" alt="cdf01d4e4ef43ef28225caf4bfe4767.jpg"></p><p>篁岭是一个很大的景区，这种大并不是来自于平面面积，而是立体的体积。<br><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160265788-16a7f21a-1aca-485b-a27e-adeb0dc6c762.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=u39958d8a&amp;margin=%5Bobject%20Object%5D&amp;name=9ff84e0786986e93a958141f97cf5d9.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=269633&amp;status=done&amp;style=none&amp;taskId=uae457ed4-0ba3-46ef-bc9c-cdc96d98250&amp;title=&amp;width=871.2" alt="9ff84e0786986e93a958141f97cf5d9.jpg"></p><blockquote><p>晒秋</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663131183545-432b1a1a-79d8-4fe3-9507-bc30b77bc9e2.jpeg#clientId=u1738fc3e-f1f2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1440&amp;id=sSomD&amp;margin=%5Bobject%20Object%5D&amp;name=IMG_3222.JPG&amp;originHeight=1440&amp;originWidth=1080&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=383471&amp;status=done&amp;style=none&amp;taskId=ua0d4366f-1c4e-469f-aa0a-94d03033ae6&amp;title=&amp;width=1080" alt="IMG_3222.JPG"></p><blockquote><p>晒秋</p></blockquote><p><a name="pLbW6"></a></p><h3 id="回家"><a href="#回家" class="headerlink" title="回家"></a>回家</h3><p>在看完篁岭经典的晒秋场景后，我们选择前往上饶的家。</p><p>虽说信州区和婺源都同属上饶，但两个地方实际上相隔挺远，开车下来也有一百多公里，接近两小时的车程了。<br><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160289582-67ca829a-4a9f-447d-9b1c-6495f1804b44.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=uebcbc920&amp;margin=%5Bobject%20Object%5D&amp;name=5ab16b69df749848e648b1a43015ec6.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=127283&amp;status=done&amp;style=none&amp;taskId=u43d53d91-cc01-4b27-b050-862ffad2e54&amp;title=&amp;width=871.2" alt="5ab16b69df749848e648b1a43015ec6.jpg"></p><p>到上饶市区后，选择去商场买点补给。原本看到楼下停车场有充电桩还挺惊喜的，结果全部都不能用。</p><p>结束补给后，找到了一家还在营业的洗车店，惊喜的是洗车店的工作人员里里外外洗的非常干净（应该是我洗这么多次车以来最干净的一次了），可惜没有拍照片。</p><p>又是一路狂奔，回老家的路因为修路的原因，导航给我导到了一条从未开过的村道上，一路上开的怀疑人生。而后半段好不容易回到了大路，路边的路灯暗的简直没法看，对向一直有来车也不能一直开远光，胆战心惊的。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160312071-ecb20530-f24e-4c0c-b334-d2f461807665.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1024&amp;id=u450f9ba2&amp;margin=%5Bobject%20Object%5D&amp;name=cf4f575d268b4548d72424aad792fcd.jpg&amp;originHeight=1280&amp;originWidth=960&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=150069&amp;status=done&amp;style=none&amp;taskId=ucd61b294-888b-467f-9c5a-d187b74a962&amp;title=&amp;width=768" alt="cf4f575d268b4548d72424aad792fcd.jpg"><br>到家之后，见过爸妈，苗苗小朋友很是开心，一个劲的在玩滑滑梯，停不下来。</p><p>再晚一点，稍作收拾后，我们就继续休息了。天气还是很热，住的三楼没有空调，当晚也是辗转反侧过好久才睡着。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663156542119-1d5227d0-8314-4540-a5c8-1736b8a77deb.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1152&amp;id=u740fa211&amp;margin=%5Bobject%20Object%5D&amp;name=bae3790868a07131f0a5ab0edbbaa5e.jpg&amp;originHeight=1440&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=492273&amp;status=done&amp;style=none&amp;taskId=u2c4d67c6-2eeb-4f48-84cb-7bb0ee69b2b&amp;title=&amp;width=1027.2" alt="bae3790868a07131f0a5ab0edbbaa5e.jpg"></p><blockquote><p>旅程轨迹</p></blockquote><p><a name="eNsjF"></a></p><h2 id="Day-3-家-千岛湖"><a href="#Day-3-家-千岛湖" class="headerlink" title="Day 3 家 - 千岛湖"></a>Day 3 家 - 千岛湖</h2><p>第二天一早，回家看望了爷爷奶奶，吃过了午饭后，因为昨晚的闷热，选择午休过后再出发。</p><p>出发上高速前，感觉油量可能有不足的风险，所以选择在当地的加油站加满了油，花费 ￥315，我继续出发前往下一个目的地了。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663156895029-b21b63c9-9496-4743-9702-72981cc3dde7.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=954&amp;id=u86a4b7c0&amp;margin=%5Bobject%20Object%5D&amp;name=6b2eb2e34ebf6ed422e4db16f1d38aa.jpg&amp;originHeight=1193&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=88153&amp;status=done&amp;style=none&amp;taskId=u7065f6a8-a1c0-43a7-88a1-d33ae76aa65&amp;title=&amp;width=1027.2" alt="6b2eb2e34ebf6ed422e4db16f1d38aa.jpg"><br><a name="hvZKz"></a></p><h3 id="千岛湖"><a href="#千岛湖" class="headerlink" title="千岛湖"></a>千岛湖</h3><p>起初我们是准备去横店的。因为恰好顺路，在横店游玩一天后直接返回杭州也不会很累。但在中途一直在搜索横店的攻略和门票，没有找到合适的。</p><p>恰好在高速路上，我们路过了一个路口，右转可以去千岛湖。于是我就想着要不去千岛湖吧。虽然女朋友不太理解，但我们依然在下一个路口下了高速，途径龙游出发去了千岛湖。</p><p>在服务区时，我想起之前好像囤过一张千岛湖的酒店券，正好这次就用上了。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160320364-9c07ca42-8192-4b4d-ae7b-6087be4d4dae.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=562&amp;id=u3843b08d&amp;margin=%5Bobject%20Object%5D&amp;name=fa666f95dada84b0bdb9d3a574d878e.jpg&amp;originHeight=703&amp;originWidth=937&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=65340&amp;status=done&amp;style=none&amp;taskId=u4b7f27c1-8394-4f60-bcbc-b5a677c12a5&amp;title=&amp;width=749.6" alt="fa666f95dada84b0bdb9d3a574d878e.jpg"></p><blockquote><p>入住的房间，很大</p></blockquote><p>晚上人有点不舒服，或许是因为昨晚没睡好的缘故有些偏头痛，买了散列痛后吃了一片，感觉好些就出去觅食了。</p><p>千岛湖这边是没有什么夜生活的，基本上一到酒店饭店就都关门了，我因为人不舒服，拖到九点半才去找到一家馆子吃的晚饭。</p><p>馆子比较小，但胜在味道不错，名字叫“胖子饭馆”，有兴趣的同学下次可以去试试，价格也还算经济实惠。<br><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160334168-62e17faa-7853-40eb-8120-6f2c697df3da.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=999&amp;id=ubdc8342f&amp;margin=%5Bobject%20Object%5D&amp;name=0206e3a72f87cf2d8cbb990b60e1498.jpg&amp;originHeight=1249&amp;originWidth=937&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=166800&amp;status=done&amp;style=none&amp;taskId=u6813dedb-7e66-4557-81aa-e18a86ebbaf&amp;title=&amp;width=749.6" alt="0206e3a72f87cf2d8cbb990b60e1498.jpg"></p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160341398-a33b247a-ea8c-4b7b-86ed-f27c167e8c53.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=u6a3100ca&amp;margin=%5Bobject%20Object%5D&amp;name=038b06367b11641b7ca87beb531d947.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=96294&amp;status=done&amp;style=none&amp;taskId=u8e5f4f26-3526-48eb-8901-182e191bbae&amp;title=&amp;width=871.2" alt="038b06367b11641b7ca87beb531d947.jpg"><br>回酒店后，玩了一会儿水，就稍作休息了。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663157370480-3ca28a07-e027-4691-9374-18ffccd9a4af.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=927&amp;id=u03a1121f&amp;margin=%5Bobject%20Object%5D&amp;name=c50430d7b1d8a590bcf0dd85e1b4a89.jpg&amp;originHeight=1159&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=410909&amp;status=done&amp;style=none&amp;taskId=u4978f722-aae8-43ba-8738-95ff868fca0&amp;title=&amp;width=1027.2" alt="c50430d7b1d8a590bcf0dd85e1b4a89.jpg"></p><blockquote><p>当日路程</p></blockquote><p><a name="E4kOB"></a></p><h2 id="Day-4-千岛湖环湖-杭州"><a href="#Day-4-千岛湖环湖-杭州" class="headerlink" title="Day 4 千岛湖环湖 - 杭州"></a>Day 4 千岛湖环湖 - 杭州</h2><p>一大早起来，房间内设施出了点故障，和相关经理反馈不愉快后，一通沟通之后决定退费一半，想了一下也还行，就决定出发继续自驾游之旅。</p><p>当天的安排还是比较明确的，下午出发去环千岛湖自驾游，结束后上高速回杭州，只是当天的天气预报通知我杭州即将有台风，所以我们想加快速度。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663157732550-dd0a7be7-0b96-4d8c-9ab9-71c1ec23cab3.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=927&amp;id=u150262be&amp;margin=%5Bobject%20Object%5D&amp;name=6e4361733c77fcabeb21c3f18f845cd.jpg&amp;originHeight=1159&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=174363&amp;status=done&amp;style=none&amp;taskId=u33f1dbd0-dbd2-4dc0-997f-851095d2837&amp;title=&amp;width=1027.2" alt="6e4361733c77fcabeb21c3f18f845cd.jpg"></p><p>千岛湖整个自驾游路线 135km，我们使用的是趣兜风的 App，导航的过程中还附带讲解（收费），旅途过程中也不觉得那么单调。<br><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160350368-53d66500-433d-4093-862a-b76bb3a6d2bc.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=ubaab45af&amp;margin=%5Bobject%20Object%5D&amp;name=aa7a934aca2d5ffe3abd37441fe569a.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=35223&amp;status=done&amp;style=none&amp;taskId=u70aa4c3b-c832-4b58-973e-f7e10f59b76&amp;title=&amp;width=871.2" alt="aa7a934aca2d5ffe3abd37441fe569a.jpg"></p><p>出发过程中，路过三谭驿站，走过上江埠大桥，我也就抵达到了今天的第一个目的地，下姜村。起初只是想吃顿饭，但最终没有想到是此次环湖自驾游中，呆的最久的地方。</p><p><a name="lAi8T"></a></p><h3 id="下姜村"><a href="#下姜村" class="headerlink" title="下姜村"></a>下姜村</h3><blockquote><p>餐厅</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160365652-cbc50ce5-d040-46fa-a7db-bf2c61bd1259.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1024&amp;id=u65ebf1f4&amp;margin=%5Bobject%20Object%5D&amp;name=7b072335660b85c81cdf85282e7cb07.jpg&amp;originHeight=1280&amp;originWidth=960&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=310848&amp;status=done&amp;style=none&amp;taskId=ud24df15e-83ac-4d79-bcf0-6900ece9252&amp;title=&amp;width=768" alt="7b072335660b85c81cdf85282e7cb07.jpg"><br><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160370062-6e6471e6-65ae-448e-83f4-a29773c19535.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1024&amp;id=u4325f441&amp;margin=%5Bobject%20Object%5D&amp;name=25ea2233cce09c31c040240f74b0f2e.jpg&amp;originHeight=1280&amp;originWidth=960&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=175392&amp;status=done&amp;style=none&amp;taskId=ua73564a4-3a83-4280-af93-e2a3cdfecf8&amp;title=&amp;width=768" alt="25ea2233cce09c31c040240f74b0f2e.jpg"></p><p>下姜村不大，但是很漂亮，整个村子的给我的感觉就是非常平和，舒缓，大家都有自己的节奏和自己的生活。而我在过桥后，与茶室里的阿姨一起聊了半个多小时，喝了几杯茶，也感觉到自己确实在放松。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160378047-11848920-8dae-43ca-837b-6bde9e92589b.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=u99282f41&amp;margin=%5Bobject%20Object%5D&amp;name=ed7afe86e3fbf7e9a1b03f2d4c0655f.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=160153&amp;status=done&amp;style=none&amp;taskId=ud21cee32-ba7b-4d00-b607-db1fe5b152c&amp;title=&amp;width=871.2" alt="ed7afe86e3fbf7e9a1b03f2d4c0655f.jpg"></p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663158194145-fc76a5da-87e3-4b2b-9575-17c98ce40f36.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1365&amp;id=u16b31f82&amp;margin=%5Bobject%20Object%5D&amp;name=ba44f9ba58f0591dd904a61db0ea9c5.jpg&amp;originHeight=1706&amp;originWidth=1280&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=449251&amp;status=done&amp;style=none&amp;taskId=u040b6d44-a2ee-49ff-9075-858c800f25b&amp;title=&amp;width=1024" alt="ba44f9ba58f0591dd904a61db0ea9c5.jpg"></p><p><a name="NVK4E"></a></p><h3 id="环湖游的其它景点"><a href="#环湖游的其它景点" class="headerlink" title="环湖游的其它景点"></a>环湖游的其它景点</h3><p>后面有陆续去了芹川古村与文渊狮城，听着车里 App 的介绍，才知道原来为了建立新安江水库，是淹了上千个村庄的。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663158298518-13bbea2d-ad3b-4774-849c-ca96f267fcc6.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1024&amp;id=u54383727&amp;margin=%5Bobject%20Object%5D&amp;name=162692a3d9ebcb176ee10bead40dcf5.jpg&amp;originHeight=1280&amp;originWidth=1706&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=166934&amp;status=done&amp;style=none&amp;taskId=ue067adc3-66b9-42eb-995b-c0fa2bfb603&amp;title=&amp;width=1364.8" alt="162692a3d9ebcb176ee10bead40dcf5.jpg"></p><p>在游玩的路上还能看到亚运会的福娃和提示牌。<br><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160391895-677ab43d-1f33-430c-8fde-132c10942466.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=u9df8eb53&amp;margin=%5Bobject%20Object%5D&amp;name=1aea92443efe76929fd9304c92006eb.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=250819&amp;status=done&amp;style=none&amp;taskId=ufb932560-7367-4d54-9e94-6f5bb4979a9&amp;title=&amp;width=871.2" alt="1aea92443efe76929fd9304c92006eb.jpg"></p><p>因为在一些景点上耽搁的有些久，因此我们也加快了脚步，开始往回赶。<br><a name="b0xUM"></a></p><h3 id="返程"><a href="#返程" class="headerlink" title="返程"></a>返程</h3><p>返程的前半段还算顺利，路上车不多，天也没有完全黑下来。而到了后半段，天黑 + 台风 + 高速路前方偶发的事故使得开车变得异常艰难。</p><p>在和女朋友商量过后，我们决定从最近的出口下高速，走国道回去。国道反而开起来比高速要顺畅许多，车不多，照明条件也好。</p><p>折腾了将近两小时后，我也终于开车回到了家，在家门口的加油站补满了油，想看看本次行程的总开销。</p><blockquote><p>一共加了 307 元。</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160399739-e20ffa98-8c3a-408d-a7d3-a43e32ef5a8e.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=ue1ccd827&amp;margin=%5Bobject%20Object%5D&amp;name=b99be020ca39c3c0800da688ce33752.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=72868&amp;status=done&amp;style=none&amp;taskId=u428edaa0-d492-43a2-bd7d-47c417182ea&amp;title=&amp;width=871.2" alt="b99be020ca39c3c0800da688ce33752.jpg"></p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663158575937-f374e536-a10b-4a5d-bc26-3f86d45b9164.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1069&amp;id=u2b987490&amp;margin=%5Bobject%20Object%5D&amp;name=94092eabee1244da85c683e85995375.jpg&amp;originHeight=1336&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=486492&amp;status=done&amp;style=none&amp;taskId=ub4bd8619-cc8f-4044-96af-c81b9098e10&amp;title=&amp;width=1027.2" alt="94092eabee1244da85c683e85995375.jpg"><br><a name="tKqkZ"></a></p><h2 id="行程总结"><a href="#行程总结" class="headerlink" title="行程总结"></a>行程总结</h2><p>这是我第一次出行自驾，出门前没想到会在外面开这么久，这么远。<br><a name="uHOlR"></a></p><h3 id="里程"><a href="#里程" class="headerlink" title="里程"></a>里程</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663158763455-1ce9f422-1a55-482a-bee5-c995fad68d66.jpeg#clientId=u312aa1ba-bbb2-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1329&amp;id=u6aaf5e47&amp;margin=%5Bobject%20Object%5D&amp;name=2a18bff4a6f0ae143619e2eb41af992.jpg&amp;originHeight=1661&amp;originWidth=1284&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=1131115&amp;status=done&amp;style=none&amp;taskId=u1f761a7c-baef-4148-b1a1-ed3d50a1406&amp;title=&amp;width=1027.2" alt="2a18bff4a6f0ae143619e2eb41af992.jpg"></p><p><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663160409587-927581f1-21e0-405b-b3f5-372bd25a9d4e.jpeg#clientId=u29e9ee14-41a4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=654&amp;id=uabad9a96&amp;margin=%5Bobject%20Object%5D&amp;name=0e327c4930cd2593bb5d229db45cc24.jpg&amp;originHeight=817&amp;originWidth=1089&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=87264&amp;status=done&amp;style=none&amp;taskId=u60951a99-d245-4534-a001-2fc86a35877&amp;title=&amp;width=871.2" alt="0e327c4930cd2593bb5d229db45cc24.jpg"></p><p>车辆统计一共行驶了 1169.7km。<br><a name="FNe2I"></a></p><h3 id="能耗"><a href="#能耗" class="headerlink" title="能耗"></a>能耗</h3><p>按上图来看，平均能耗 3.2kwh + 5.4L，约等于 6.3L 每百公里。</p><p>按实际加油充电情况，整个过程中共加了 622 元的油，中间有补充过一次电，大约 30 元，整体算下来就是 652 元的用车成本。</p><p>按 95# 汽油均价 8.92 来算，折合百公里用油 6.2L 左右，一公里 0.55 左右。整体与车载油耗表接近，我个人对这个能耗也表示满意。毕竟中间有山路、高速、国道等复杂的驾驶场景。</p><p><a name="ZOtNJ"></a></p><h3 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h3><p>穿行于山林之间，感受山谷湿润的空气，享受与家人团聚的时刻，沉浸在下姜“世外桃源”般的场景中。我想，在那一刻，我是放松且自由的。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2022/jpeg/98602/1663158763455-1ce9f422-1a55-482a-bee5-c995fad68d66.jpeg#clientId=u312aa1ba-bbb2-4
      
    
    </summary>
    
    
      <category term="随笔" scheme="https://blog.lxxyx.cn/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>Web 框架不是答案</title>
    <link href="https://blog.lxxyx.cn/2022/04/22/2022/web-framework-is-not-answer/"/>
    <id>https://blog.lxxyx.cn/2022/04/22/2022/web-framework-is-not-answer/</id>
    <published>2022-04-22T10:51:00.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1650595518919-10412d36-8186-430a-a95c-9a90db424e85.png#clientId=u4242e7ad-2ba4-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=713&amp;id=u4543a564&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=713&amp;originWidth=1392&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=167415&amp;status=done&amp;style=none&amp;taskId=ud37f7e1c-c241-43cc-9d2b-4c24429ad47&amp;title=&amp;width=1392" alt="image.png"></p><p>晚上在看 <a href="https://deno.com/deploy">Deno Deploy</a> 官网介绍时，脑海里突然蹦出了一句话：“Web 框架不是答案”。</p><p>我个人目前是 Node.js Web 框架 Midway 的开发者，日常也会造一些小轮子自娱自乐，我很清楚的知道 Web 框架在现代 Web 开发下的不可替代性与企业级应用下所产生的效用。但今天我却想说，Web 框架不是答案。</p><p>在恢弘的巨型软件工程中，我可能会惊讶于在框架的约束与规则下，这个项目仍然得以继续开发，而不是凌乱不堪难以维护。但巨石应用终究还是离大部分普通开发者太远了，这时候 Web 框架所设立的一系列有利于后续维护的规则，此时都会变成约束。</p><p>“世间安得两全法，不负如来不负卿”。这句话也适用于 Web 框架开发者的我。</p><h2 id="从那段代码开始"><a href="#从那段代码开始" class="headerlink" title="从那段代码开始"></a>从那段代码开始</h2><p>这是 Deno Deploy 官网上，那段给我带来灵感的代码：<br><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; serve &#125; <span class="keyword">from</span> <span class="string">&quot;https://deno.land/std@0.114.0/http/server.ts&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; h, ssr, tw &#125; <span class="keyword">from</span> <span class="string">&quot;https://crux.land/nanossr@0.0.1&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Hello</span> = (<span class="params">props</span>) =&gt; (</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&#123;tw</span>`<span class="attr">bg-white</span> <span class="attr">flex</span> <span class="attr">h-screen</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">h1</span> <span class="attr">class</span>=<span class="string">&#123;tw</span>`<span class="attr">text-5xl</span> <span class="attr">text-gray-600</span> <span class="attr">m-auto</span> <span class="attr">mt-20</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      Hello &#123;props.name&#125;!</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;Listening on http://localhost:8000&quot;</span>);</span><br><span class="line"><span class="keyword">await</span> <span class="title function_">serve</span>(<span class="function">(<span class="params">req</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> url = <span class="keyword">new</span> <span class="title function_">URL</span>(req.<span class="property">url</span>);</span><br><span class="line">  <span class="keyword">const</span> name = url.<span class="property">searchParams</span>.<span class="title function_">get</span>(<span class="string">&quot;name&quot;</span>) ?? <span class="string">&quot;world&quot;</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">ssr</span>(<span class="function">() =&gt;</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Hello</span> <span class="attr">name</span>=<span class="string">&#123;name&#125;</span> /&gt;</span></span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure></p><p>代码很简单，在模块中，我们通过监听 Http 事件，调用 nanossr 渲染出 Html 并返回给客户端。<br>整个渲染流程看起来非常的清晰且简洁，代码是无状态的，输入输出也很确定，只要有足够多的容器，它在云上也可以以非常快的速度扩容并支撑服务。</p><p>而如果使用框架，以 Midway Hooks 为例（伪代码），用户代码是这样的。<br><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Api</span>, <span class="title class_">Get</span>, useContext, run &#125; <span class="keyword">from</span> <span class="string">&#x27;@midwayjs/hooks&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; h, ssr, tw &#125; <span class="keyword">from</span> <span class="string">&quot;https://crux.land/nanossr@0.0.1&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">Hello</span> = (<span class="params">props</span>) =&gt; (</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&#123;tw</span>`<span class="attr">bg-white</span> <span class="attr">flex</span> <span class="attr">h-screen</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">h1</span> <span class="attr">class</span>=<span class="string">&#123;tw</span>`<span class="attr">text-5xl</span> <span class="attr">text-gray-600</span> <span class="attr">m-auto</span> <span class="attr">mt-20</span>`&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">      Hello &#123;props.name&#125;!</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> api = <span class="title class_">Api</span>(</span><br><span class="line">  <span class="title class_">Get</span>(<span class="string">&#x27;/&#x27;</span>),</span><br><span class="line">  <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; query &#125; = <span class="title function_">useContext</span>()</span><br><span class="line">    <span class="keyword">const</span> name = query.<span class="property">name</span> ?? <span class="string">&quot;world&quot;</span>; </span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">ssr</span>(<span class="function">() =&gt;</span> <span class="language-xml"><span class="tag">&lt;<span class="name">Hello</span> <span class="attr">name</span>=<span class="string">&#123;name&#125;</span> /&gt;</span></span>);</span><br><span class="line">  &#125;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">await</span> <span class="title function_">run</span>(api)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;Listening on http://localhost:8000&quot;</span>);</span><br></pre></td></tr></table></figure><br>相比较之下，因为使用了框架，你需要处理初始化、路由等工作，不可避免的加入了许多开销与理解成本。</p><p>在这之前，我会认为上面这种纯函数的代码是简单的，不足以覆盖后续迭代场景的，用哲学一点的说法就是，这种开发方式是不具备发展的眼光的与可持续性的。</p><p>但现在我觉得，或许一些简单的 Api 开发，就是应该这么简单的，只是写两行胶水代码，聚合后端 Api，又或者是渲染一段模版，此时框架设计是显得过重的。</p><p>此时再来重新思考，之前的我会觉得让每个可能场景都用上我的框架会是正确的答案。但现在我觉得，在简单的场景下，简单的框架或无框架的服务才是正确的。使用复杂的框架容易使用户代码出现过度设计与提早优化，从而适得其反。</p><h2 id="自问自答"><a href="#自问自答" class="headerlink" title="自问自答"></a>自问自答</h2><p>Q：你是做框架的，为什么突然出来唱反调，说起 “Web 框架不是答案”了？<br>A：正因为我是做框架的，所以我对框架的适用范围，用户遇到的问题理解的更为深刻一些，但也因为屁股思维，所以做事情都是朝着框架去的。而简单框架和无框架意味着更透明的使用方式，更低的心智负担。且“Web 框架不是答案”是我今晚在读代码时产生的灵感，给我打开了一个新的方向，所以我给它配了一篇文章。这不是唱反调，这是对思路的补充。</p><p>Q：简单框架或无框架开发模式下，代码架构、开发范式、可维护性都是问题，你怎么解决？<br>A：不解决。我认为开发人员在使用 Web 框架时，容易存在两大误区。a. 什么都想用框架实现，过度优化与提前设计 b. 不用框架，后续维护不过来了只能重构。建议开发者在第一时间就想好自己的功能要不要用框架，实在不行就重构吧（毕竟架构师都是一遍一遍的重构出来的）。</p><p>Q：后续有什么落地的 Action 吗？<br>A：会有。后续我将尝试简单框架或无框架的设计，尝试将 Web Api 开发的心智负担下降到尽可能的低。目标不是消灭框架，而是让开发者在使用时觉得简单且可信赖。</p><p>Q：这么做的价值在哪里？<br>A：简单框架和无框架的价值需要通过配套的平台 &amp; 基建体现出来，单独拎出来说事情是不公平的。强大的基建 + 易用的平台 + 低心智负担的开发方式，配合极速启动 + 保存代码即上线等特性，开发者可以放心的开发和部署功能。BTW，我也觉得对一个初生的想法或者灵感询问价值，是会带来非常大压力的。</p><p>Q：现阶段有实际的产物吗？<br>A：就是这篇文章，梳理思路 &amp; 阐述个人观点。</p><p>Q：这个过程中你觉得可能会遇到什么问题？<br>A：克制与做减法。框架设计随着时间迭代与功能完善，总是会做的越来越复杂，最终达到用户难以理解全貌，框架由开发者的工具转变为了开发者心中的黑匣子。所以我觉得在后续过程中，最难的问题不是我要加什么功能，而是知道我不要什么功能，我给用户提供的究竟是什么？能不能用一句话描述并概括出来。</p><p>Q：没看懂你想说什么和想做什么<br>A：正常，深夜灵感 + 随笔，只是描述个人的看法而已。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2022/png/98602/1650595518919-10412d36-8186-430a-a95c-9a90db424e85.png#clientId=u4242e7ad-2ba4-4&amp;a
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>Midway &amp; 一体化 3.0 ：新语法 &amp; 新路由 &amp; 新全栈套件</title>
    <link href="https://blog.lxxyx.cn/2022/02/10/2022/hooks-v3/"/>
    <id>https://blog.lxxyx.cn/2022/02/10/2022/hooks-v3/</id>
    <published>2022-02-10T16:45:05.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644474071183-60908470-8b3c-44a9-8a18-973d47de743a.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ude3606ba&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=176725&amp;status=done&amp;style=none&amp;taskId=u1bebb02d-c2b9-468f-b15b-3cced1953d5&amp;title=&amp;width=1920" alt="image.png"><br>在刚过去的 2022 年 1 月，我们如约带来了 Midway 冬季直面会的直播，并正式发布了 Midway &amp; 一体化 3.0 版本，下面是冬季直面会现场发布内容的文字稿。<br>​</p><h1 id="Midway-3-0"><a href="#Midway-3-0" class="headerlink" title="Midway 3.0"></a>Midway 3.0</h1><p>首先将给大家介绍的是 Midway 3.0 的相关内容。<br>​</p><h2 id="3-0-版本发布"><a href="#3-0-版本发布" class="headerlink" title="3.0 版本发布"></a>3.0 版本发布</h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644474140850-f23c6d28-d8fb-41a4-8169-a9909c7e9b9d.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=uut0R&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=137960&amp;status=done&amp;style=none&amp;taskId=u628f488a-8bd6-4efa-a9f7-885b3b1d906&amp;title=&amp;width=1920" alt="image.png"><br>去年的秋季发布会时，我们介绍了 Midway 3.X 预览版本中的新功能。<br>​</p><p>而在这几个月中，我们一直在对 Midway 3.X 的功能进行迭代与完善，并且不断有同学通过微信、钉钉、Issue 等渠道来关注我们 3.X 的进度。<br>​</p><p>而在本次冬季直面会中，我们非常高兴的告诉大家，在 2022.01.20 这天，Midway 3.0 正式发布了，相关新功能可以参考：<a href="https://www.bilibili.com/video/BV1aL4y1p7oA?from=search&amp;seid=8235946720906913847&amp;spm_id_from=333.337.0.0">Midway v3 新功能预览</a>。<br>​</p><h2 id="官网支持多版本文档"><a href="#官网支持多版本文档" class="headerlink" title="官网支持多版本文档"></a>官网支持多版本文档</h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644474447060-b4170c94-1a34-4849-bd96-b96533dd7e78.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u64cd0c97&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=306302&amp;status=done&amp;style=none&amp;taskId=ub2c58235-a44c-4ce7-85c5-84fa375e6b1&amp;title=&amp;width=1920" alt="image.png"><br>我们也对 Midway 的官网进行了更新。在新版本中，我们支持了多版本的文档，大家可以在网页右上角切换版本来查阅相关文档。<br>​</p><p>Midway 3.0 发布了，官网文档默认以 3.0 为主。<br>​</p><h2 id="技术栈明确"><a href="#技术栈明确" class="headerlink" title="技术栈明确"></a>技术栈明确</h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644474515285-b8a96015-64fe-44ec-a187-e94782b5b809.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u06e5a1a0&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=129273&amp;status=done&amp;style=none&amp;taskId=u43550f84-393b-48f8-bce0-6eca0a25138&amp;title=&amp;width=1920" alt="image.png"><br>在 3.0 发布后，Midway 在不同场景的技术栈也愈发明确。<br>​</p><p>第一部分是标准项目，使用 <code>Class + IoC</code> 的技术栈，来承载传统 Node.js Web 项目的开发。<br>第二部分是 Serverless 项目，我们支持了阿里云 / 腾讯云等云厂商，同时我们也在积极接入 KNative 原生的基建。<br>第三部分是一体化部分，是从传统的 Node.js Web 工程孵化出的创新模式。它主要支持前后端一体化融合的开发方式，并且在前后端都使用了函数式去进行开发，去给大家带去一个不同的体验。今天也将在后面发布 Midway 一体化 3.0 的相关内容。<br>​</p><h2 id="感谢参与开源"><a href="#感谢参与开源" class="headerlink" title="感谢参与开源"></a>感谢参与开源</h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644474791627-2293c387-07fb-4e77-aa56-a497ac534bc3.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=uf5cf5cea&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=107892&amp;status=done&amp;style=none&amp;taskId=u2f896a6a-9351-47ae-943d-e862bc3a332&amp;title=&amp;width=1920" alt="image.png"><br>同时在过去的一年中，我们发现有很多的小伙伴给我们提交过 PR，非常感谢这些同学。我们深知，所有的开源项目，不论大小都是从一点一滴开始做起的。<br>​</p><p>为了感谢这些给我们提交 PR 的同学，我们也准备了一些精美礼品，如果大家在 2021.01.01 - 2021.12.31 期间有给 Midway 提交过 PR，欢迎扫码或访问 <a href="https://survey.taobao.com/apps/zhiliao/vDoGO5p27">礼品问卷</a> 提交信息，我们将及时寄出礼品。<br>​</p><h2 id="信息同步"><a href="#信息同步" class="headerlink" title="信息同步"></a>信息同步</h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644474966144-9b667152-e261-408c-b013-be223db8d8c8.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u2c20adcb&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=96860&amp;status=done&amp;style=none&amp;taskId=u3e4592b1-5ecb-4d0f-947f-811382c7536&amp;title=&amp;width=1920" alt="image.png"><br>在接下来的一段时间，我们会主要完成以下三件事情：</p><ul><li>提供 Egg 模版</li><li>【年后】提供 Open telemetry 的集成方案</li><li>【年后】阿里内部版本同步</li></ul><p>​</p><h1 id="一体化-3-0"><a href="#一体化-3-0" class="headerlink" title="一体化 3.0"></a>一体化 3.0</h1><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644475221260-998da023-6e57-4fdf-836b-a6f7b7e7d928.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u60af5d9c&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=99017&amp;status=done&amp;style=none&amp;taskId=u1cf0c03c-bba0-4d71-827b-c53c2b8b4b4&amp;title=&amp;width=1920" alt="image.png"><br>在本次直面会中，我们也将为大家带来全新的一体化 3.0 内容。<br>​</p><p>Midway 一体化 2.0 于 2021 年的 3 月份发布，截止至发布会当日已经有差不多 9 个月，这段时间我们一方面是在内部做了大规模的落地实践，同时我们也在不断的探索，去研究社区的新方案，思考一体化未来的演讲方向。<br>​</p><p>在经过 9 个月的探索后，我们决定推出全新的一体化 3.0 版本，包含新语法、新路由设计、新全栈套件三部分功能。<br>​</p><h2 id="现状"><a href="#现状" class="headerlink" title="现状"></a>现状</h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644475408601-679b8327-c7f8-49be-91e1-d2677d512267.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1120&amp;id=u58cd2737&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1120&amp;originWidth=2437&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=190317&amp;status=done&amp;style=none&amp;taskId=uf9ba5848-518d-48c3-9fde-9505305ce54&amp;title=&amp;width=2437" alt="image.png"><br>Midway 一体化方案的想法诞生于 2020 年的 2 月份，并于同年的 4 月份在内部正式发布。<br>​</p><p>截止至发布会当日（2021.01），目前已经有超过 2800 个应用是基于 Midway 一体化方案开发的，目前一体化也已成为了阿里前端的主流研发模式之一。<br>​</p><h2 id="新语法"><a href="#新语法" class="headerlink" title="新语法"></a>新语法</h2><p>一体化 3.0 的首要革新在于 API 语法。<br><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644476536209-0641724c-7b2b-4388-ae4e-cfb3a78ee48b.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1196&amp;id=u539fc3b3&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1196&amp;originWidth=2532&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=475570&amp;status=done&amp;style=none&amp;taskId=u789093f6-dd6f-4c4d-abf1-7288c2c1b2f&amp;title=&amp;width=2532" alt="image.png"><br>上图是 2.X 版本的语法，在 2.X 版本中，我们通过 <code>函数即接口</code>、<code>“零”API 调用</code>、<code>文件系统路由</code>三个理念设计了整套语法。<br>​</p><p>在 2.0 的语法中，我们牺牲一定的功能性，换取接口开发的便利性。以 <code>Http Method</code> 为例，上述的接口实际上只能描述出 <code>Get / Post</code> 两种 Method，对于 <code>Put / Delete</code> 等支持则无能为力。<br>​</p><h3 id="3-0-语法"><a href="#3-0-语法" class="headerlink" title="3.0 语法"></a>3.0 语法</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644476526086-a04b4cd9-b327-4bb6-9371-cdb8070903ec.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1196&amp;id=uef9cce48&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1196&amp;originWidth=2550&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=367126&amp;status=done&amp;style=none&amp;taskId=u9fd2fb35-3afe-40b1-8424-a56b541e13c&amp;title=&amp;width=2550" alt="image.png"></p><p>在 3.0 中，我们则<strong>重新设计了整套语法</strong>，在保留<code>函数即接口</code>、<code>“零”API 调用</code>的特性之上，全面增强了功能性。</p><p>一个 API 接口由以下部分组成：</p><ul><li><code>Api()</code>：定义接口函数<ul><li><code>Get(path?: string)</code>：指定 Http 触发器，此处指定请求方法为 GET</li><li><code>Query&lt;&#123; page: string &#125;&gt;()</code>：操作符，声明前端传参或执行自定义逻辑</li><li><code>Handler: async (...args: any[]) =&gt; &#123; ... &#125;</code>：用户逻辑，处理请求并返回结果</li></ul></li></ul><p>​</p><p>同时我们也支持了<code>“零”API 调用</code>的特性，你依然可以在前端直接导入函数并调用，我们的 SDK 会自动将你的请求转换为正确的格式。<br>​</p><h3 id="Http-触发器"><a href="#Http-触发器" class="headerlink" title="Http 触发器"></a>Http 触发器</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644476775964-c29a0a78-5aad-4e50-814d-60252c0cff3a.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1204&amp;id=ufdcf5abe&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1204&amp;originWidth=1678&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=419495&amp;status=done&amp;style=none&amp;taskId=u2dee286a-24fb-4f8d-a144-19203e4101c&amp;title=&amp;width=1678" alt="image.png"></p><p>得益于新的语法设计，我们支持了全系列的 Http 触发器，同时也支持用户指定路径。</p><p>新功能下，对于类似 OAuth2 的场景是非常有用的，你可以非常方便的指定接口地址，并完成认证逻辑的开发。<br>​</p><h3 id="更多触发器的支持"><a href="#更多触发器的支持" class="headerlink" title="更多触发器的支持"></a>更多触发器的支持</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644476812309-fe283073-4b32-49de-91cc-eee79b12f351.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1146&amp;id=u1b7a07ef&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1146&amp;originWidth=2473&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=285850&amp;status=done&amp;style=none&amp;taskId=u3997a0bd-2bc6-4741-9f7d-8c319756a4b&amp;title=&amp;width=2473" alt="image.png"><br>在新语法的加持下，我们也实现了更多触发器的支持，如上图展示的定时器、OSS 回调等，在 3.0 中都可以非常容易的实现。</p><h3 id="操作符"><a href="#操作符" class="headerlink" title="操作符"></a>操作符</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644476948755-213f56b3-c775-44ac-9187-ff48750f679b.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1152&amp;id=ue6ad7415&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1152&amp;originWidth=2517&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=260596&amp;status=done&amp;style=none&amp;taskId=u186c3737-9c57-491c-91c9-7b5e35e21a6&amp;title=&amp;width=2517" alt="image.png"><br>操作符（Operator）的设计是一体化 3.0 的核心，我们通过操作符的使用、组合来拓展 Api 的功能。<br>​</p><p>操作符一共支持以下 3 种功能：</p><ul><li>声明：声明前端传参的类型，如 <code>Query/Params/Header</code></li><li>定义：定义函数元信息，如 <code>Middleware</code></li><li>执行：控制函数执行流程，如 <code>Validate/HttpCode/Redirect</code></li></ul><p>​</p><p>我们在上图中提供了一个复杂的例子，如果在传统的一体化中，要实现上述的功能会将逻辑写的非常复杂，容易出现面条式的代码。而在 3.0 中，你通过操作符的组合就可以实现了。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644477418686-f4d86bb4-c01d-4575-82d7-0dd1fc9cc281.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1165&amp;id=uf921217b&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1165&amp;originWidth=2479&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=326568&amp;status=done&amp;style=none&amp;taskId=u79241baa-1cea-4d31-8fab-4fc5fbdc124&amp;title=&amp;width=2479" alt="image.png"></p><p>同时，我们依旧保留了简洁的<code>“零”API 调用</code>。<br>​</p><h4 id="Request-amp-Response"><a href="#Request-amp-Response" class="headerlink" title="Request &amp; Response"></a>Request &amp; Response</h4><p>我们提供了一系列的内置操作符，来帮助开发者快速完成常见功能的开发。<br>​</p><p>以 Request 操作符为例，你可以通过 <code>Query / Params / Headers</code> 操作符来声明前端需要传入的类型，在使用<code>“零”API 调用</code>时，我们会将其转换为正确的结构体。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644477564078-645b27d8-a8f2-4cf0-b33a-fe10e9e74c97.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1240&amp;id=u0337b112&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1240&amp;originWidth=2485&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=376616&amp;status=done&amp;style=none&amp;taskId=u3991431d-5a04-4838-afa2-35b0be104f8&amp;title=&amp;width=2485" alt="image.png"></p><p>我们也提供了 Response 相关的操作符。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644477729016-a22e5e26-c8de-4306-9008-6589e0ce6a02.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1060&amp;id=u6d48c9c0&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1060&amp;originWidth=2429&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=259330&amp;status=done&amp;style=none&amp;taskId=ua7aa7a9b-69c9-4aa7-a28e-a1407ad5809&amp;title=&amp;width=2429" alt="image.png"></p><h4 id="Middleware"><a href="#Middleware" class="headerlink" title="Middleware"></a>Middleware</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644477825252-069a9e34-ffe4-4a62-9173-426c8f3d0005.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1201&amp;id=uffc106a2&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1201&amp;originWidth=1503&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=250489&amp;status=done&amp;style=none&amp;taskId=u65dc1997-8106-493b-a620-78d8e48dd0f&amp;title=&amp;width=1503" alt="image.png"><br>我们也支持使用 <code>Middleware</code> 操作符来定义中间件。</p><h4 id="Validate-amp-ValidateHttp"><a href="#Validate-amp-ValidateHttp" class="headerlink" title="Validate &amp; ValidateHttp"></a>Validate &amp; ValidateHttp</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644478373212-7c87826d-0ee9-4792-8cc6-7bd31e48e5fa.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=977&amp;id=u2dbc8c49&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=977&amp;originWidth=2385&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=219104&amp;status=done&amp;style=none&amp;taskId=u67214f78-ab40-4441-8199-1418051ce7a&amp;title=&amp;width=2385" alt="image.png"><br>参数校验是我们在 3.0 中着重实现的功能，其中一体化默认使用 <a href="https://www.npmjs.com/package/zod">Zod</a> 作为校验库。你可以通过 <code>Validate(...schemas)</code> 来校验用户入参，如上图所示。</p><p>这是一种非常自由的用法，当你不使用 Validate 校验器时，你实现了静态类型安全，前后端基于 TypeScript 的类型系统，确保安全性。<br>​</p><p>而当你使用 Validate 校验器时，你实现了运行时安全，在不损失前后端调用体验的同时，校验器会校验用户入参是否正确。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644479873310-acca5ea3-7e49-4560-9b5b-a536065e17bb.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u88b09356&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=350507&amp;status=done&amp;style=none&amp;taskId=u8d556dd0-0a53-4ebe-9d63-fcd31b0f90c&amp;title=&amp;width=1920" alt="image.png"></p><p>如上图所示，同时我们也支持了 Http 结构的校验，<br>​</p><p>基于 <a href="https://www.npmjs.com/package/zod">Zod</a> 对 TypeScript 的良好支持，你可以在编写 Schema 时直接获得推导的类型。<br>​</p><h3 id="Prisma-ORM"><a href="#Prisma-ORM" class="headerlink" title="Prisma ORM"></a>Prisma ORM</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644479977737-296f4f08-d68b-4a00-aa42-94b17f6cc844.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1197&amp;id=u6a678fde&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1197&amp;originWidth=2554&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=351336&amp;status=done&amp;style=none&amp;taskId=uf2e8c4a2-f4a6-4d45-835e-1725f2446e4&amp;title=&amp;width=2554" alt="image.png"><br><a href="https://www.prisma.io/">Prisma</a> 是为 Node.js &amp; TypeScript 而生的 ORM。简单来说就是，你可以通过编写数据库的 Schema（也可以基于当前数据库结构自动生成），来自动生成 ORM Client。<br>​</p><p>因为 ORM 是生成的，因此无需用户手动定义 Model，并可以获得完善的 TypeScript 支持。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644480171119-fc4fa7e5-6785-4d0b-b34e-240c1e66fbbb.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=uf25edeb9&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=365328&amp;status=done&amp;style=none&amp;taskId=u88fced1f-6e94-425c-aae4-5ffe5bac871&amp;title=&amp;width=1920" alt="image.png"><br>基于 Prisma 的特性与 Validate 校验器，我们可以实现类型安全 + 运行时安全的方案。<br>​</p><p>这个方案成本足够的低，不仅在编程时通过静态的类型信息确保安全性，也通过校验器在运行时保障安全性，并且带来了极其流畅的开发体验，前后端全链路紧密的联系在一起。</p><h2 id="新路由"><a href="#新路由" class="headerlink" title="新路由"></a>新路由</h2><p>在 3.0 中，我们也设计了全新的路由机制。<br>​</p><h3 id="2-X-文件路由机制"><a href="#2-X-文件路由机制" class="headerlink" title="2.X - 文件路由机制"></a>2.X - 文件路由机制</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644480496386-89d8ddcf-4cff-4d74-b330-08129ca48e6f.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1240&amp;id=ue639828d&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1240&amp;originWidth=1654&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=273169&amp;status=done&amp;style=none&amp;taskId=u30ea32ba-70f3-4a2c-8d6a-2af226dc39c&amp;title=&amp;width=1654" alt="image.png"><br>在 2.X 版本中，我们使用文件路由来作为默认的路由机制，文件系统路由是基于约定的，易于理解，但也存在强依赖于文件系统、规则较多的问题。</p><h3 id="API-路由"><a href="#API-路由" class="headerlink" title="API 路由"></a>API 路由</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644480621525-15d679c9-ac71-40ba-84da-e12cae371bde.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1221&amp;id=u9eb8db0d&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1221&amp;originWidth=2375&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=295665&amp;status=done&amp;style=none&amp;taskId=u74d636fc-f71d-4fc2-b280-61aa7402cb3&amp;title=&amp;width=2375" alt="image.png"><br>在 3.0 中，我们极大的简化了整体的路由机制，分为以下两种策略：</p><ul><li>不指定路径：使用函数名 / 文件名（默认导出的情况下）来生成路由</li><li>指定路径：使用指定的路径，支持动态传入 Params 的情况<h2 id="新全栈套件"><a href="#新全栈套件" class="headerlink" title="新全栈套件"></a>新全栈套件</h2>在 3.0 版本中，我们也提供了全新的全栈套件。<br>​</li></ul><h3 id="2-X-全栈工程"><a href="#2-X-全栈工程" class="headerlink" title="2.X - 全栈工程"></a>2.X - 全栈工程</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644480777877-0396ff80-16a3-44a8-b0cb-fd74ef328107.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1159&amp;id=u620644d9&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1159&amp;originWidth=2414&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=327355&amp;status=done&amp;style=none&amp;taskId=uec66adb0-ef27-4bd9-9a9b-f41e884cb9d&amp;title=&amp;width=2414" alt="image.png"><br>在 2.0 版本中，我们更多的是将 Vite 和 Midway 拼接了起来，如上图的项目目录 &amp; <code>npm scripts</code> 所示。<br>​</p><h3 id="3-0-全栈套件"><a href="#3-0-全栈套件" class="headerlink" title="3.0 - 全栈套件"></a>3.0 - 全栈套件</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644481164265-64c29fc9-0cbd-4700-9325-bac02bd03c51.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1170&amp;id=ua9fea37d&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1170&amp;originWidth=2205&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=297134&amp;status=done&amp;style=none&amp;taskId=uc849b89e-f247-4959-8f00-99339532aa6&amp;title=&amp;width=2205" alt="image.png"><br>在 3.0 中，我们开发了新的全栈套件，提供统一的 dev/start/build &amp; 工程配置功能，来简化用户的学习成本和认知成本。<br>​</p><h3 id="新请求-SDK"><a href="#新请求-SDK" class="headerlink" title="新请求 SDK"></a>新请求 SDK</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644481223913-b0fa0059-7ef5-4e97-b7b5-03f06c58216b.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1195&amp;id=u0bc422b8&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1195&amp;originWidth=2462&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=198928&amp;status=done&amp;style=none&amp;taskId=u40325216-d7fa-4485-ac9c-befbf13d430&amp;title=&amp;width=2462" alt="image.png"><br>我们还开发了新的请求客户端 <a href="https://npmjs.com/package/@midwayjs/rpc">@midwayjs/rpc</a>，作为<code>“零”API 调用</code>默认的请求客户端。<br>​</p><p>新的请求客户端对于原版本（基于 axios），体积减少了 64%（5.6kb -&gt; 2kb），且支持 Browser &amp; Node.js 环境，并且支持了配置中间件、替换请求客户端等新功能。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644481449435-115bf4e9-d478-4e30-9072-a85a31dccd26.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=607&amp;id=u8c7b3149&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=607&amp;originWidth=947&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=103139&amp;status=done&amp;style=none&amp;taskId=u08569926-78be-4b13-9b0a-2c3d3090b4f&amp;title=&amp;width=947" alt="image.png"><br>上图是支持的配置项。<br>​</p><h4 id="自定义的请求客户端"><a href="#自定义的请求客户端" class="headerlink" title="自定义的请求客户端"></a>自定义的请求客户端</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644481504072-cb6ddd91-c632-440e-889a-739af676e262.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u8bcc0e39&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=251152&amp;status=done&amp;style=none&amp;taskId=u64b670a1-00f6-4306-b6bc-5955a335cef&amp;title=&amp;width=1920" alt="image.png"></p><p>如上图所示，我们可以非常方便的替换为自己的请求客户端。<br>​</p><h4 id="请求中间件"><a href="#请求中间件" class="headerlink" title="请求中间件"></a>请求中间件</h4><p>我们参考 Koa 的洋葱机制实现了请求客户端的中间件，可以实现打印日志、统一错误处理等非常多实用的功能。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644482030271-a17160e5-31e6-4c72-8169-063645802c2e.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=Fowk6&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=241710&amp;status=done&amp;style=none&amp;taskId=ub7e299a8-b4bd-48fa-8ec3-19366cc15bd&amp;title=&amp;width=1920" alt="image.png"></p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644482184770-f6197a8f-fc7d-4d1e-b58d-1d9815d8bb5e.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u40c20685&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=224116&amp;status=done&amp;style=none&amp;taskId=u027bdb60-eaa6-4f60-b6b0-5ea7e48843c&amp;title=&amp;width=1920" alt="image.png"></p><h3 id="前端构建器"><a href="#前端构建器" class="headerlink" title="前端构建器"></a>前端构建器</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644482504734-36cbe416-38dd-4e49-922d-7271224bc1c5.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1145&amp;id=u05e8d23e&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1145&amp;originWidth=2513&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=274704&amp;status=done&amp;style=none&amp;taskId=ub27b1e01-15ab-4b62-bc91-d2adccb5172&amp;title=&amp;width=2513" alt="image.png"><br>在 3.0 中，我们基于 <a href="https://github.com/unjs/unplugin">unplugin</a> ，提供了 <code>Vite / Webpack</code> 的插件，用于快速接入前端工程。相比于 2.X 的接入方式，3.0 可以实现一行代码接入的效果。</p><h2 id="预览"><a href="#预览" class="headerlink" title="预览"></a>预览</h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644482628470-0928a65a-5e2c-48b8-bc15-2d9fbbe9865c.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1206&amp;id=u4cdb8474&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1206&amp;originWidth=1910&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=377891&amp;status=done&amp;style=none&amp;taskId=u48110060-a4ba-4c72-8e19-df9f737f780&amp;title=&amp;width=1910" alt="image.png"><br>目前一体化 3.0 已对外发布预览版，文档、试用可以访问我们的官网：<a href="https://midwayjs.org/">https://midwayjs.org/</a>，也欢迎大家提交反馈。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1644482681343-03db752b-7bb3-42c2-8fc8-d5909cdf6809.png#clientId=u967cffa8-04ab-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=uc6e87ea2&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=435706&amp;status=done&amp;style=none&amp;taskId=u16183f3d-3888-48c5-a1b0-67471d9aa2b&amp;title=&amp;width=1920" alt="image.png"></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2022/png/98602/1644474071183-60908470-8b3c-44a9-8a18-973d47de743a.png#clientId=u967cffa8-04ab-4&amp;a
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>繁易 の 2021 总结 - ✈ 人生加速</title>
    <link href="https://blog.lxxyx.cn/2022/01/31/annual/summary/"/>
    <id>https://blog.lxxyx.cn/2022/01/31/annual/summary/</id>
    <published>2022-01-31T16:12:45.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p>前些年因为种种原因（应该就是懒），总是没有写年度总结。而今年值得记录的事情实在是太多，标题 <code>✈ 人生加速</code> 是我在 2 月底时冒出来的念头，因为总觉得是时候花时间去推进生活的进度了~</p><h3 id="开源"><a href="#开源" class="headerlink" title="开源"></a>开源</h3><p>年初的时候，Trott 提名我成为了 Node.js Core Collaborator（就是下图）。<br /><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643557236293-86f566f2-9ebb-4930-a05d-08afa2285dff.png#clientId=u30c365a6-f48e-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=300&amp;id=u922170cc&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=600&amp;originWidth=1200&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=245731&amp;status=done&amp;style=none&amp;taskId=u19f3f97a-795f-4d4f-ae87-fe037d106bf&amp;title=&amp;width=600" alt="image.png"><br /><br><br />这个其实主要与 2020 年年末阶段的工作有关，那段时间内，因为想深入学习 Node.js &amp; 融入开源社区，于是选择了给 Node.js 提交 PR 的方式。那段时间基本上每天晚上都在忙活，每晚也都有 1-2 个 PR 产出（修复 BUG &amp; 补充单测为主）。<br />​</p><p>简单，充实，快乐。于是也顺利的在 2021 年的年初获得了提名，也算是为全年开了个好头。</p><p><a name="HlsTm"></a></p><h3 id="英语"><a href="#英语" class="headerlink" title="英语"></a>英语</h3><p>2021.01 - 04，因为参与开源社区的原因，便主动的自学起了英语，主要以练口语为主（之前都是哑巴英语，简单的对话都不敢进行），辅以一点点的英文材料学习。<br />​</p><p>个人主要是通过 Cambly 这个网站去学习英语的，之前也有朋友在朋友圈推荐过，我使用过后觉得确实很不错，于是也推荐给了我的朋友们。<br />​</p><p>这个网站的特点在于，可以寻找不同的外教，我个人的话是基本上每次都找不同的外教上课，聊天。很开心的是聊过天的外教都觉得我讲的还行，大部分情况下还能听懂，就是发音有些不太标准，需要多加练习。<br /><br><br />最后也尝试在真实的语境下与老外进行了交流，感觉挺好，物超所值。<br /><br><br /><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643558606416-20d0bd25-42b9-4765-b25f-ab85fb64604a.png#clientId=u30c365a6-f48e-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1061&amp;id=u354c6145&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=2122&amp;originWidth=1042&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=1442172&amp;status=done&amp;style=none&amp;taskId=u74a98a36-8b8c-4db1-a81d-a90e5775597&amp;title=&amp;width=521" alt="image.png"><br><a name="Lyi7m"></a></p><h3 id="晋升"><a href="#晋升" class="headerlink" title="晋升"></a>晋升</h3><p>在 4 月底时，我被通知获得了晋升 P7 的提名。<br />​</p><p>我是 18 年校招加入阿里的，19 年晋升了 P6，在晋升的当时给自己定的目标就是 3 年升 7 。还记得晋升 P6 的当天，老板给我通知结果，我还很认真的问了 P7 的要求是什么，该怎么达到。<br />​</p><p>接下来在 5-6 月份，就是在忙活准备晋升材料的事情，今年晋升的形式和以往做 PPT 的方式不太一样，改成了写文字稿，于是又来回倒腾了一段时间。<br />​</p><p>7 月底，主管公布了晋升的结果，我晋升 P7 通过。之后的那几天，我走在路上时常会有一种不真实感，3 年升 7 曾一度而言，对我是个遥不可及的目标，而我如今确确实实的实现了。<br />​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643557815582-18b2cdf3-2ee3-49be-aa60-a19802c2baf7.png#clientId=u30c365a6-f48e-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=210&amp;id=uda8f5998&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=420&amp;originWidth=1054&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=226242&amp;status=done&amp;style=none&amp;taskId=ubd978cc6-626d-47c8-bec7-d978a67d1f7&amp;title=&amp;width=527" alt="image.png"><br /><br><br />嘛，还是要感谢这一路上帮助过我的所有人，ღ( ´･ᴗ･` )比心！</p><p><a name="bWxNK"></a></p><h3 id="健身"><a href="#健身" class="headerlink" title="健身"></a>健身</h3><p>我是一个胖子，一直都是。<br />​</p><p>我也想减肥，一直都想。只是之前要么停留在想，要么总是中途失败。<br /><br><br />之后有一天，朋友通知我说要结婚了，送上恭喜的同时，也在心里震惊同龄人已经要结婚完成人生大事了，而我还是一个死肥宅（降不下来的那种）。<br />​</p><p>于是这次下定决心，要调整体重。从饮食开始，到寻找专业的健身教练（真的很重要！之前找过一次不专业的导致我直接不敢去健身房），再到和同事互相约健身，成为健身房的常客，只花了 3 个月。<br />​</p><p>下面是我的体重变化曲线。s<br /><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643558340217-ef23b72f-351d-424a-8e39-bd0611e9ed61.png#clientId=u30c365a6-f48e-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=436&amp;id=u4701180e&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=872&amp;originWidth=888&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=204639&amp;status=done&amp;style=none&amp;taskId=u8343dec4-2543-4741-8ddb-a76e2230c0a&amp;title=&amp;width=444" alt="image.png"><br /><br><br />当然，现在又胖回来了~<br />​</p><p>不过不再焦虑了，这次感觉在饮食方面还存在非常非常非常大的改进空间，之前健身期的饮食感觉要么不规律，要么营养配比不均衡，争取在新的一年继续试试看吧~<br /></p><p><a name="dcRrW"></a></p><h3 id="恋爱"><a href="#恋爱" class="headerlink" title="恋爱"></a>恋爱</h3><p>在瘦下来后，真的会遇见春天。<br />​</p><p>而和女朋友认识的经历也颇为神奇，神奇到让我觉得这就是天意。<br />​</p><p>在遇见她的前一周，我去了一趟法喜寺，为自己的姻缘祈福 🙏🏻。在离开寺庙时，看见门口的牌匾上写着四个大字：“莫向外求”。<br />​</p><p>而在一周后，我在微信搜索关键字时，发现能搜到之前的“摇号相亲群”，而我早在八月份就退出了。好奇的我点了进去，发现不能说话，但是能加好友，也能看群里的文档。于是试着加了女朋友的微信，发现也能加上，聊了几句后对方也觉得不可思议。<br />​</p><p>再然后，就很快很快的在一起了 ❤️，超甜的那种！<br />​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643559590623-6138e714-2831-4861-9ab3-33ad7a896acc.png#clientId=u30c365a6-f48e-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=ud9af0eb0&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=6640098&amp;status=done&amp;style=none&amp;taskId=uef33f9b2-7857-4b7b-aeea-ac4c1ce35b6&amp;title=&amp;width=888" alt="image.png"></p><p><a name="vk94Y"></a></p><h3 id="买房"><a href="#买房" class="headerlink" title="买房"></a>买房</h3><p>2020 年 10 月份，我动了买房安居的念头。<br />​</p><p>主要是有以下几点原因</p><ul><li>不想租房，不想被涨房租，想要稳稳的幸福</li><li>想用上自己喜欢的家电，提升生活质量</li><li>投资 &amp; 保值</li></ul><p>​</p><p>但杭州的二手房一直很贵，并且 2020.10 -&gt; 2021.05 这段时间，房子每天都在涨价，我整个人也是非常恐慌的状态。因为定不下来，就得考虑换城市的事情，甚至一度还在长沙缴纳了几个月的社保。后续因为购房政策的原因（杭州 30 岁单身非人才以下不得参与红盘摇号），也放弃了很多。<br />​</p><p>2021 年的 11 月，我使用了人才证参与了新一轮红盘摇号，这也是我的第五次摇号，这次很幸运的中签了。回溯起当时的心情，应该就俩字，开心。<br />​</p><p>随后的两天请了年假，忙忙碌碌的走完了买房，选车位，签合同，付首付等一系列事情。当付完首付后看着银行卡里所有积蓄一扫而空的状态，就感觉：“嗯，当有今日”。<br /><br><br />也是从买房后，真切的感受到人生在加速，并且是以极快的加速度在前进。</p><blockquote><p>摇到号当天女朋友给送的花</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643560217686-36f1e9d4-a27f-4dd7-928d-d012f53fdbce.png#clientId=u30c365a6-f48e-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=ubf11e561&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=4058012&amp;status=done&amp;style=none&amp;taskId=u23303c17-c001-4f3e-a79e-dd4a4ec2ae4&amp;title=&amp;width=888" alt="image.png"><br /></p><p><a name="hw7Dx"></a></p><h3 id="年度好书"><a href="#年度好书" class="headerlink" title="年度好书"></a>年度好书</h3><p>今年读到的年度好书是：<a href="https://book.douban.com/subject/26369699/">《被讨厌的勇气》</a>。<br />​</p><p>读完之后感觉整个人的心灵得到了解放，我又可以重新做回我自己，果敢的做决定，不再内疚、后悔、患得患失。<br />​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643562286233-c490e865-f7de-49d3-b445-13d61c6c2b6c.png#clientId=u933cbe7e-58e1-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=500&amp;id=u43c9e487&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1000&amp;originWidth=694&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=1050657&amp;status=done&amp;style=none&amp;taskId=u0a70886c-ecc7-4283-ae46-8e59bba6b91&amp;title=&amp;width=347" alt="image.png"><br /></p><p><a name="lxhu5"></a></p><h3 id="年度设备"><a href="#年度设备" class="headerlink" title="年度设备"></a>年度设备</h3><p>今年不出意外的也购入了很多电子设备和小家电。从中选出了我很喜欢的几件。</p><ol><li>文石 Boox Poke 3 电纸书<ol><li>尺寸完美，可以塞进口袋里，基于安卓，可以安装微信读书，性能强续航长。我从买来后阅读了很多之前在手机上不会去读的书籍</li></ol></li><li>小米 4 件套洗碗机<ol><li>再也不洗碗，吃得开心</li></ol></li><li>小浪即热 TDS 上水器<ol><li>可以用 20L 的水桶，不用频繁换水，随时随地喝热水幸福感 Max</li></ol></li><li>Macbook Pro 13寸 M1 版<ol><li>性能强，轻，续航长，各方面都接近完美的一台笔记本，也是我第一台全新的电脑（终于不捡二手了）</li></ol></li></ol><p><br /></p><p><a name="hnr4u"></a></p><h3 id="演讲-amp-文章-amp-视频"><a href="#演讲-amp-文章-amp-视频" class="headerlink" title="演讲 &amp; 文章 &amp; 视频"></a>演讲 &amp; 文章 &amp; 视频</h3><p>2021 年我做了很多场分享。</p><ul><li>【03.06】<a href="https://subway.midwayjs.org/season-10.html">Node.js 地下铁#10 ——《Midway Hooks — 更具生产力的全栈框架：全栈、“零” API、Hooks 开发后端》</a></li><li>【03.25】Midway.js 春季直面会 —— Midway Hooks 2.0</li><li>【05.23】<a href="https://subway.midwayjs.org/season-11.html">Node.js 地下铁#11 ——《 Node.js &amp; Midway.js 源码贡献指北》</a></li><li>【07.24】<a href="https://rtcdeveloper.agora.io/t/topic/22435">Dev Meetup 前端技术巡回沙龙上海站——《渐进式的 Web 框架: Midway.js 的设计与思考》</a></li><li>【08.11】Midway.js 夏季直面会 —— Midway Hooks 2.0 + 小程序</li><li>【10.19 - 10.22】云栖大会——云端一体研发模式工作坊</li><li>【11.15】前端练习生计划——师兄师姐分享</li><li>【12.05】深圳 GMTC —— <a href="https://gmtc.infoq.cn/2021/shenzhen/presentation/3979">跨云 &amp; 跨端——小程序云开发一体化方案</a></li><li>【12.19】<a href="https://d2.alibabatech.com/">杭州 D2 前端论坛——《Node.js Web 框架再进化，面向前端与未来标准》</a></li><li>【12.26】<a href="https://zaozao.run">前端早早聊——《探索面向未来标准的 Node.js Web 框架》</a></li></ul><p>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643563433805-1c721db4-791c-40b4-9966-cdcb3bf1cf36.png#clientId=ud6205a7b-2c77-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=534&amp;id=ue222225f&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1067&amp;originWidth=1600&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=3435160&amp;status=done&amp;style=none&amp;taskId=ud8f46792-69fb-4d16-a042-06132d67696&amp;title=&amp;width=800" alt="image.png"><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643563494082-29f3e29e-c8c6-4ae9-932c-d06eacb006b7.png#clientId=ud6205a7b-2c77-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=540&amp;id=u2451fe06&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1440&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=1986917&amp;status=done&amp;style=none&amp;taskId=u3508294e-1007-4073-b9cf-34aada51f59&amp;title=&amp;width=720" alt="image.png"><br /><br><br />也写过一些文章。</p><ul><li><a href="https://blog.lxxyx.cn/2021/01/17/2021/net/">net 模块，参数传错就进程崩溃</a></li><li><a href="">Jamstack ，你真的了解它吗？</a></li><li><a href="https://juejin.cn/post/7007246909001711623#comment">程序员最重要的能力是什么？</a></li><li><a href="https://juejin.cn/post/7028135881043083300">从文科生到前端专家 - 在转行时我想过的问题</a></li><li>还有一些文章在内部，就不提了……</li></ul><p>​</p><p>也参与录制了一段视频。<br><a name="iiODF"></a></p><h3 id="其它"><a href="#其它" class="headerlink" title="其它"></a>其它</h3><p>生活里还有其他许多值得纪念的事情。</p><blockquote><p>百年校庆</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643562935840-29658da3-1015-4333-90c9-be385a29a4d7.png#clientId=u933cbe7e-58e1-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=u74b5cfff&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=2094199&amp;status=done&amp;style=none&amp;taskId=uea8ad200-b091-4c81-a39c-9501c38815c&amp;title=&amp;width=888" alt="image.png"></p><blockquote><p>2021.06 川西自驾游（真的好好看，下次还来）</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643561914448-37a38b4f-6c2e-428c-9f1f-f47e89187877.png#clientId=u933cbe7e-58e1-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=uc9c00e32&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=3440091&amp;status=done&amp;style=none&amp;taskId=u455463d7-559c-4a9b-8d53-a5a675e56ee&amp;title=&amp;width=888" alt="image.png"><br /></p><blockquote><p>25 岁生日，我心目中的成年日（年轻人的第一个生日蛋糕）</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643562003049-198c97a7-be65-4048-b662-6814ebc0380a.png#clientId=u933cbe7e-58e1-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=u483833fd&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=3343482&amp;status=done&amp;style=none&amp;taskId=ua5f2370c-3a00-4cc2-bd65-a7f779abfcb&amp;title=&amp;width=888" alt="image.png"><br /></p><blockquote><p>新电脑</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643562082889-7bce5c66-606f-4477-a1be-8c0957d3579f.png#clientId=u933cbe7e-58e1-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=u3aae0391&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=3753265&amp;status=done&amp;style=none&amp;taskId=u4a8b33dc-5a91-48bd-89d2-99945db8c9a&amp;title=&amp;width=888" alt="image.png"><br /></p><blockquote><p>第一场音乐交响会</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643562148405-51502066-7443-4645-b76f-f15e2fc6af45.png#clientId=u933cbe7e-58e1-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=437&amp;id=u18f983b8&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=874&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=2444921&amp;status=done&amp;style=none&amp;taskId=u3f4d390c-bf1b-4060-9090-f66082b7c3c&amp;title=&amp;width=960" alt="image.png"><br /></p><blockquote><p>阿里三周年</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643562204726-781dfcd8-5d15-45de-a04f-280bd4ac5406.png#clientId=u933cbe7e-58e1-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=uece01b95&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=2368&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=8781754&amp;status=done&amp;style=none&amp;taskId=u51c74bc1-5bd2-4902-81bb-43477e8737b&amp;title=&amp;width=1184" alt="image.png"><br /></p><blockquote><p>陈老板的婚礼 &amp; 家园红包</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643562879959-23af0170-b5ff-4062-8a3a-27a1985ef6c8.png#clientId=u933cbe7e-58e1-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=uce589a93&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=5557519&amp;status=done&amp;style=none&amp;taskId=ub31f3c1c-6987-49b0-a94b-0fce7429e3c&amp;title=&amp;width=888" alt="image.png"></p><blockquote><p>全程负责的《前端修炼小册》</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643563122274-dbfc1b47-6b81-449b-ada2-0c8b009aa681.png#clientId=ud6205a7b-2c77-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=ud8f1eb5c&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=3209538&amp;status=done&amp;style=none&amp;taskId=ue8c9e3e3-be8c-4d13-9793-bb80ffc77b0&amp;title=&amp;width=888" alt="image.png"><br /></p><blockquote><p>喜提家里的第一辆车</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643563170845-4b94416a-6145-43e3-9124-cef4e7739efe.png#clientId=ud6205a7b-2c77-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=u453adb0e&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=3328198&amp;status=done&amp;style=none&amp;taskId=ub588346e-30a4-4129-a442-b6b0bd1fc94&amp;title=&amp;width=888" alt="image.png"><br /></p><blockquote><p>表白的花花</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643563273844-0e54f2a3-eb41-46d9-9463-cc4fd7f40a73.png#clientId=ud6205a7b-2c77-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=ufc60f41a&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=3656453&amp;status=done&amp;style=none&amp;taskId=ubd644242-3d86-4e89-a334-f6cb1d94ae5&amp;title=&amp;width=888" alt="image.png"><br /></p><blockquote><p>音乐节</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1643563314467-16345319-f27e-4003-a6c3-2710869cf60a.png#clientId=ud6205a7b-2c77-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=666&amp;id=u4e61e010&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1332&amp;originWidth=1776&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=3513682&amp;status=done&amp;style=none&amp;taskId=u6d425953-dc27-4aa7-bb8a-afc023dab3f&amp;title=&amp;width=888" alt="image.png"><br /></p><p><a name="nJyAN"></a></p><h3 id="2022"><a href="#2022" class="headerlink" title="2022"></a>2022</h3><p>2022 年才刚开始，新的主题我也已经想好了（先保密）。<br />​</p><p>希望在新的一年：</p><ul><li>培养正确的饮食习惯，继续健身</li><li>拿到驾照（21 年报名了，但是因为懒一直没有去成，但是又很想开车自驾游 OTZ）</li><li>工作顺利</li><li>多参与开源，希望能做出自己满意 &amp; 社区喜欢的作品</li></ul><p>​</p><p>新的一年，继续大步向前~</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;前些年因为种种原因（应该就是懒），总是没有写年度总结。而今年值得记录的事情实在是太多，标题 &lt;code&gt;✈ 人生加速&lt;/code&gt; 是我在 2 月底时冒出来的念头，因为总觉得是时候花时间去推进生活的进度了~&lt;/p&gt;
&lt;h3 id=&quot;开源&quot;&gt;&lt;a href=&quot;#开源&quot; cla
      
    
    </summary>
    
    
      <category term="年度总结" scheme="https://blog.lxxyx.cn/tags/%E5%B9%B4%E5%BA%A6%E6%80%BB%E7%BB%93/"/>
    
  </entry>
  
  <entry>
    <title>跨云 &amp; 跨端 — 小程序云开发一体化方案</title>
    <link href="https://blog.lxxyx.cn/2022/01/07/2022/hooks-miniapp/"/>
    <id>https://blog.lxxyx.cn/2022/01/07/2022/hooks-miniapp/</id>
    <published>2022-01-07T10:46:00.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>演讲录音转文字 + 校对，偏口语化。</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641372993619-3078e36a-b069-4398-8b90-d5d6c021a270.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u8b011520&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=177086&amp;status=done&amp;style=none&amp;taskId=uec7e7ac6-3e2e-40bf-a8ef-fa46d916249&amp;title=&amp;width=1920" alt="image.png"><br>​</p><p>大家下午好，非常感谢大家的到来。<br>​</p><p>我是刘子健，花名繁易。目前在阿里巴巴淘系前端的 Node.js 架构组就职。今天给大家带来的分享话题标题是：《跨云 &amp; 跨端 — 小程序云开发一体化方案》。<br>​</p><h2 id="个人简介"><a href="#个人简介" class="headerlink" title="个人简介"></a>个人简介</h2><p>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641373045523-931ef785-0d02-4a4f-b014-3741810beb14.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u9b69905c&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=612780&amp;status=done&amp;style=none&amp;taskId=u743e5030-d3e2-4120-b793-966b3789a9c&amp;title=&amp;width=1920" alt="image.png"><br>​</p><p>首先是个人自我简介，我叫刘子健，花名繁易，Github 的账号是 <a href="https://github.com/Lxxyx">Lxxyx</a>。 目前在阿里巴巴的 Node.js 架构组担任前端技术专家一职。<br>​</p><p>个人工作主要专注于以下三方面：</p><ul><li>Node.js</li><li>Serverless</li><li>Web 框架</li></ul><p>​</p><p>然后我个人也有积极在参与开源社区，简介如下：</p><ul><li>阿里 Node.js 框架 Midway.js 团队核心成员，负责框架研发工作</li><li>Node.js Core Collaborator</li><li>TC39 阿里巴巴代表，对部分提案提供反馈 &amp; 意见</li></ul><p>​</p><h2 id="大纲"><a href="#大纲" class="headerlink" title="大纲"></a>大纲</h2><p>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641373287769-1026a956-0f95-4e13-8720-251c48c0d672.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u637fa5a2&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=114074&amp;status=done&amp;style=none&amp;taskId=u108b37de-b72b-41de-a90f-c36531b6490&amp;title=&amp;width=1920" alt="image.png"><br>今天的分享会从以下四部分给大家去讲述。<br>​</p><ul><li>介绍一下小程序和云开发</li><li>介绍推出的云开发一体化方案</li><li>内部落地实践案例</li><li>展望和总结</li></ul><p>​</p><h2 id="小程序与云开发"><a href="#小程序与云开发" class="headerlink" title="小程序与云开发"></a>小程序与云开发</h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641373662656-ea31cec4-d88d-4330-baa2-f907a6954e76.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u08f49ce7&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=89033&amp;status=done&amp;style=none&amp;taskId=u8156b2d9-a4a9-4650-97a4-b490512b478&amp;title=&amp;width=1920" alt="image.png"></p><h3 id="小程序"><a href="#小程序" class="headerlink" title="小程序"></a>小程序</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641373366852-e012a286-8906-47c8-8547-c2f42099214f.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u0ec9aa2f&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=202735&amp;status=done&amp;style=none&amp;taskId=ue664d80d-746e-42ba-bc5d-7346d707c7b&amp;title=&amp;width=1920" alt="image.png"><br>​</p><p>从上图大家可以看到，自小微信推出小程序开始，各大厂商都针对自己的平台推出了相应的小程序的方案，可以说层出不穷，基本上每一个场都有一个对应的小程序的一个方案。<br>​</p><p>但是很多第三方开发者或者厂商，他开发的应用不只是投放至单一平台的。他很多时候有一码多投或代码复用的需求，在这种情况下，开发者必须去使用跨端框架。<br>​</p><p>然后大家也知道微信传统的小程序开发模式，我认为是非常低效且痛苦的。于是很多开发者想着使用前端框架来解决开发体验的问题，所以也会去使用跨端框架，比如基于 Vue 或基于 React 的各种第三方跨端框架。<br>​</p><p>因此对于目前小程序跨端框架的现状，我的判断是：<strong>“跨端和效率的需求催生了大量的跨端框架，每次都有新方案，每年都有新东西。”</strong><br><strong>​</strong></p><h3 id="云开发"><a href="#云开发" class="headerlink" title="云开发"></a>云开发</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641373714851-72f18d0e-f66d-4474-b52b-d352469fe5b1.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u7b3098cf&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=203704&amp;status=done&amp;style=none&amp;taskId=uce505564-76c5-4ff2-bb43-23b5f9d471f&amp;title=&amp;width=1920" alt="image.png"><br>第二部分我会讲述云开发。要提到云开发，就不得不先提我们传统的服务开发。<br>​</p><p>在我们传统服务开发中，如果你要去开发一个应用的后端，其实是非常繁琐的。<br>大家可以看上图，在实际开发过程中，开发者其实是需要关心应用扩容、安全、文件存储、容灾等各方各面的。开发者在服务端需要做非常多的运维工作，然后也需要花费很多时间在接口联调上，然后才能去实现业务逻辑，整体工作其实非常繁琐。<br>​</p><p>对一些第三方一些小的开发者，甚至说个人开发者，这个部分的成本其实高到难以接受的。<br>​</p><p>面对这些问题，云厂商推出了云开发功能。关于云开发，我把它拆解成以下两个部分：</p><ul><li>平台特定的 API</li><li>云开发基础能力</li></ul><p>​</p><p>那么云开发到底节省了哪部分的时间呢？我认为有以下两方面：</p><ol><li>在一些常用的功能的开发上，你不再需要重新去做很多事情了。例如登录支付功能，你如果有做过对接都知道这个过程非常痛苦。但是有云开发提供的 API 情况下，你可以直接调用它内置的 API 去实现功能，从而节约你的开发成本。</li><li>云开发的基础能力（函数、文件、数据库等）。也就是说你不用再关心运维，不用再关心安全，不用再关心容灾，直接去调用这些就可以。然后它整个的目标是为了让你能专注到业务逻辑上。</li></ol><p>​</p><p>关于云开发，我们的判断是：<strong>“Serverless + BaaS，可以降低小程序开发成本。”</strong><br><strong>​</strong></p><p>但云开发不是一个新东西，它在 18~19 年左右，其实就已经有云厂商提出并落地了。大家在各种会议上听分享，其实可以看到社区重点提的都是跨端框架，例如我的框架可以跨几个端，但几乎没有人关注到云开发可以怎么做。<br>对于这一点，其实我们也有自己的一个判断：云开发和跨端之间存在“不可调和”的矛盾。我们可以做跨端，但很难说跨端框架上可以很好的去利用云开发的能力。<br>​</p><p>我们进行的归纳总结，认为跨端与云开发间存在以下三个矛盾。<br>​</p><h3 id="矛盾-云开发语法分裂"><a href="#矛盾-云开发语法分裂" class="headerlink" title="矛盾 - 云开发语法分裂"></a>矛盾 - 云开发语法分裂</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641374737546-4b261f7a-f745-4528-b005-c0fa00f02458.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u6af6fbeb&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=156326&amp;status=done&amp;style=none&amp;taskId=uf4159765-6910-42b6-9c02-a599669ead5&amp;title=&amp;width=1920" alt="image.png"><br>第一个矛盾是：云开发的语法分裂。<br>​</p><p>在上图中我列举了几个云：阿里云、腾讯云、轻服务。这一点其实非常有趣，因为我在长期跟踪各个云的云函数功能，发现每个云为了让自己长得不太一样，它必须设计一个不太一样，但其实一样的语法。例如：</p><ul><li>阿里云：<code>context</code></li><li>腾讯云：<code>event</code></li><li>轻服务：<code>params</code></li></ul><p>​</p><p>虽然功能大致是一样的，但不同厂商有着不同的一个语法，导致云开发的语法其实相当分裂。之前做跨端小程序，开发者可以通过同一套 DSL 去做，但在云开发上其实很难去做，每个云服务商你都要学习和接受全新的语法。<br>​</p><h3 id="矛盾-开发繁琐"><a href="#矛盾-开发繁琐" class="headerlink" title="矛盾 - 开发繁琐"></a>矛盾 - 开发繁琐</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641374999205-ed4f8f02-6df8-43a2-9b5c-48799b44fbc7.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u3b7af4f9&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=330437&amp;status=done&amp;style=none&amp;taskId=u965d59b7-31f7-46ef-b2d6-a4f6f77922c&amp;title=&amp;width=1920" alt="image.png"><br>第二个矛盾是：开发繁琐。<br>​</p><p>目前所有云开发的目录结构或者是云开发所谓的架构都是上图的模式。</p><ul><li>架构视角：端侧使用了云开发 BaaS 服务进行开发</li><li>代码视角：前端后端同一个仓库，通过 monorepo 的方式去开发，其中小程序的每一个函数都需要创建新文件夹</li></ul><p>​</p><p>这种情况下，其实我们认为它只是机械的组合资源，你的代码很多部分是雷同的，并没有发挥 JavaScript 通吃前后端的优势。<br>​</p><h3 id="矛盾-云平台锁定"><a href="#矛盾-云平台锁定" class="headerlink" title="矛盾 - 云平台锁定"></a>矛盾 - 云平台锁定</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641375322483-03ae465e-1d89-4d70-9d04-acb635ac30e2.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u8e937a16&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=197252&amp;status=done&amp;style=none&amp;taskId=u23ffa8f4-8c71-4793-8603-39ff5215426&amp;title=&amp;width=1920" alt="image.png"><br>第三个矛盾是：云平台锁定。</p><p>云平台锁定其实是提的比较多的一个概念。简单来说，如果你选择了一家云服务厂商，你的数据，你的代码、你的服务器全都上云之后，如果这个云服务厂商的质量没有达到你的要求，或者说它的功能达不到你要求，你想去切换云的时候，这种时候成本是非常高并且难以接受的。这种时候我们称为云平台锁定。<br>​</p><p>然后很多时候很多厂商会选择多云策略，同理做小程序也是一样，做小程序时，开发者可能说不止投一个平台，但很多时候你也希望去使用别的平台的功能，就容易遇见云平台锁定问题。<br>​</p><h2 id="云开发一体化方案"><a href="#云开发一体化方案" class="headerlink" title="云开发一体化方案"></a><strong>云开发一体化方案</strong></h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641375608709-941486fc-b935-4a01-bbe6-e0da6c05aeed.png#clientId=ua50d3a86-1e21-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=uf154861f&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=92495&amp;status=done&amp;style=none&amp;taskId=u2215af81-46e0-4f45-a4ab-7177d60d1ed&amp;title=&amp;width=1920" alt="image.png"><br>说完以上三个矛盾，接下来我给大家一起来看我们是如何去解决这些问题的。这边就要提到我们推出的云开发一体化方案。<br>​</p><h3 id="一体化应用开发方案"><a href="#一体化应用开发方案" class="headerlink" title="一体化应用开发方案"></a>一体化应用开发方案</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641375761256-48cb352f-6087-4206-83a1-ce37dd438d21.png#clientId=ua50d3a86-1e21-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ub24a1e25&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=170824&amp;status=done&amp;style=none&amp;taskId=uef9c4b94-d0a5-469d-8665-caf6a775667&amp;title=&amp;width=1920" alt="image.png"><br>​</p><p>一体化应用开发方案它并不是一个非常新的一个概念。这个方案其实是我们团队 2020 年 4 月份在阿里内部发布的。因为涉及到非常多配套的云平台，因此也没有做大规模的宣传。</p><p>目前在阿里内部有 2500+ 一体化应用，它也是目前阿里前端主流开发模式之一。举个例子：目前我们在开发中后台应用的时候，已经很少在开发那种纯资源型的应用，而是选择一体化应用的研发模式，它可以更好地利用函数与 Serverless 的能力，让开发者可以去做一些后端的一些事情，我们认为这对应用是一个增强的能力。<br>​</p><p>然后一体化应用本来只是我们在开发 Web 应用时去使用的方案，比如说 C 端或者中后台应用。但是我们发现如果把它的方案和我们内部的小程序方案去做一个结合（例如淘宝小程序），其实也可以解决小程序所带来的非常多的一些痛点。<br>​</p><p>因此这次我们也将云开发与小程序的方案去进行了一个融合，这边会讲到我们是如何解决上面的三个矛盾的。</p><h3 id="语法分裂-—-函数式"><a href="#语法分裂-—-函数式" class="headerlink" title="语法分裂 — 函数式"></a>语法分裂 — 函数式</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641376331639-e401c9e8-ab4f-4f9b-a7d0-28749a93c38b.png#clientId=ua50d3a86-1e21-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ua55088d8&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=156326&amp;status=done&amp;style=none&amp;taskId=ua7b2b038-86b5-4549-a34d-32dd2dd4161&amp;title=&amp;width=1920" alt="image.png"><br>第一个矛盾是语法分裂。<br>​</p><p>上图可以看到，不同厂商有不同语法。往往这种时候，我们需要设计一种 DSL，通过跨端的DSL 来解决这个问题。而在后端情况下我们发现其实有更简单的方式，因此我们最终选择的 DSL 可能简单到，我相信在座的每个同学不用学习就会了。</p><h4 id="函数式-函数即接口"><a href="#函数式-函数即接口" class="headerlink" title="函数式 - 函数即接口"></a>函数式 - 函数即接口</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641376594094-28da4397-b57d-4c87-b3c6-f12866998755.png#clientId=ua50d3a86-1e21-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u6955ad18&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=238313&amp;status=done&amp;style=none&amp;taskId=u2cead45a-539f-4e80-8471-18f561bd600&amp;title=&amp;width=1920" alt="image.png"><br>在这里，我们使用的 DSL 是 JavaScript 函数，也称作为： JavaScript 函数即接口 - 统一 &amp; 无协议。<br>​</p><p>我们发现，在你实际开发的过程中，你去开发一个 JavaScript 函数，这个函数所携带的信息其实就已经足够描述API 接口或者云函数需要的信息。这边我以 HTTP 接口为例。</p><ul><li>HTTP 路径：函数名 + 文件名</li><li>HTTP Method：无参数为 <code>GET</code>，有参数为 <code>POST</code></li><li>HTTP Body：参考 RPC 设计，使用数组来映射函数实际入参</li><li>返回值：TypeScript 自带类型分析</li></ul><p>这一点就是我们所提出的一个理念，叫做基于函数元信息是生成接口。同理如果你生成的是一个云函数，你有一个函数名，你就能只要保证这个函数名不重名，你就能使用函数名作为函数 ID 去生成一个云函数。<br>​</p><p>然后大家可以看图中的代码，可以看到用了一个 <code>useContext</code>  的 API，这也是我们方案的创新之处。<br>​</p><h4 id="函数式-使用-Hooks-开发后端接口"><a href="#函数式-使用-Hooks-开发后端接口" class="headerlink" title="函数式 - 使用 Hooks 开发后端接口"></a>函数式 - 使用 Hooks 开发后端接口</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641381544146-d4b7284e-6a8a-4c71-84af-097ed7725c37.png#clientId=ua50d3a86-1e21-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ub0152926&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=464991&amp;status=done&amp;style=none&amp;taskId=u4d3252a4-8155-4c27-b36c-013ce2cea4c&amp;title=&amp;width=1920" alt="image.png"><br>这一点我们称之为: 使用 Hooks 开发后端接口。<br>​</p><p>为了给开发者提供更好的开发体验，解决语法问题，解决 DSL 问题。我们提出了一个全新的理念：Hooks 开发后端接口。在传统的 Web 开发中，大家其实需要传递 <code>Context</code> 的，此处 <code>Context</code> 你可以理解为 <code>Express</code> 的 <code>req/res</code> 或者是 <code>Koa</code> 的 <code>ctx</code>。也可以理解为云函数里面 <code>Context</code> 所带的一些函数信息。<br>​</p><p>上图有简单的示例，这个如果你云函数支持 HTTP 请求的话，可以使用 <code>useContext</code> Api 来获取到 <code>Context</code>，从而拿到请求的 IP。这种情况下你可以获取上下文，而且不需要手动传参数，使用方法也与前端保持完全一致，然后这一点也会引出我们后面的一个核心 feature ，后文会说到。<br>​</p><p>然后在当时做的时候其实也遇到了一些难题，比如在我们做的时候，其实 JavaScript 是不支持线程级 <code>Context</code> 传递的，想要实现需要依赖 Node.js <code>[async_hooks](https://nodejs.org/api/async_hooks.html)</code> 模块。<br>​</p><p>然后这背后其实也有一个小故事，就是 JavaScript 不支持线程上下文，我们想让它支持，要怎么办？我们团队在 2020 年 的 6 月份向 ECMA 去提交了一份关于线程上下文的提案，但是因为各种语言语义等潜在的问题，并没有通过，所以后面我们才切换到 Node.js <code>async_hooks</code> 的实现。所以我们也在积极参与标准，只不过这个过程比较坎坷，不像 <a href="https://github.com/tc39/proposal-error-cause">Error Cause</a> 提案推进的比较顺利。<br>​</p><p>然后当我们在使用 <code>async_hooks</code> 时，其实也存在两个问题：</p><ul><li>性能问题非常严重</li><li>稳定性不足，Node.js Module Stability 1</li></ul><p>​</p><p>然后我们也尝试做了两版方案，来解决提到的这两个问题。<br>​</p><h5 id="编译时方案"><a href="#编译时方案" class="headerlink" title="编译时方案"></a>编译时方案</h5><p>首先是性能问题，我们通过编译器解决。我们自研了 TypeScript 的编译器 <a href="https://github.com/midwayjs/mwcc">mwcc</a>，这里面用到了非常多的 TypeScript Internal Api。我们通过将所有函数的调用编译为带 bind 的调用方式，同时去生成正确的 <code>source map</code> ，从而得到一个可以正确被加载和调用的函数，也解决了性能问题。当然也存在一定的语法限制。<br>​</p><p>但最后我们还是放弃了这个方案。因为在 TypeScript 3.9 迭代到 4.0 的时候，TypeScript 4.0 出现了非常多的 breaking changes，然后内部的 API 改动也也非常大，我们已经改不动了，所以我们决定投入到第二版方案，运行时方案。<br>​</p><h5 id="运行时方案"><a href="#运行时方案" class="headerlink" title="运行时方案"></a>运行时方案</h5><p>这个使用了 Node.js 中的 <code>[AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage)</code> API。<code>[AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage)</code> 是 Node.js JS 为了专门解决请求上下文追踪难题或者用来做监控的一个模块，它性能比较好，然后也没有语法限制，API 也比较稳定。<br>​</p><p>然后在这之后我们也在积极参与社区。图里面其实是我们参与社区的例子，包含我们对 <code>[AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage)</code> 的反馈等，推进这个 API 达到稳定可用的状态。</p><p>在 Node.js 16.x 时，这个 API 正式落地了，目前是一个 Stability: 2 的稳定模块。整个过程略显繁琐，但其实也是我们在不断探索的一个过程，也是我个人加入 Node.js 社区的初衷。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641382722443-a9a9299d-db90-48c0-a7fa-5caf25672962.png#clientId=ua50d3a86-1e21-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=240&amp;id=u22ad74d9&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=240&amp;originWidth=860&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=22287&amp;status=done&amp;style=none&amp;taskId=u74efdd24-7b62-4a6d-9b86-1f595a7b2f6&amp;title=&amp;width=860" alt="image.png"><br>​</p><h3 id="开发繁琐-全栈架构-amp-简化调用"><a href="#开发繁琐-全栈架构-amp-简化调用" class="headerlink" title="开发繁琐 - 全栈架构 &amp; 简化调用"></a>开发繁琐 - 全栈架构 &amp; 简化调用</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641382775882-42ff3f7d-bff8-49ac-8920-30675db36558.png#clientId=ua50d3a86-1e21-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u5130fe18&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=330437&amp;status=done&amp;style=none&amp;taskId=ue8bb05cf-da96-494f-a11f-15a570d7e90&amp;title=&amp;width=1920" alt="image.png"><br>​</p><p>关于开发繁琐。其实大家可以看到你用传统的开发模式，去开发一个全栈应用或者云函数应用其实是非常繁琐的。你每一个云函数你都需要新建一个文件夹，需要拷代码，很难复用。你抽象成一个 npm 模块，你需要来回发布，也非常繁琐，并且前后端其实也是不连通的。<br>​</p><h4 id="全栈架构"><a href="#全栈架构" class="headerlink" title="全栈架构"></a>全栈架构</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641382836854-d5e70214-d71a-4006-a4c6-b61267e81390.png#clientId=ua50d3a86-1e21-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=uff02d7e2&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=226518&amp;status=done&amp;style=none&amp;taskId=ud19d2296-ae9e-4692-9733-b6203549ade&amp;title=&amp;width=1920" alt="image.png"><br>在这里，我们也推出了一个全新的架构模式，叫全栈架构。<br>​</p><p>这个就是目前我们内部使用一体化去开发的一个目录结构。可以看到与传统的方式完全不一样的是，<code>src</code> 目录内包含了云函数、小程序页面等前后端的资源。<br>​</p><p>这么做的好处其实有很多：</p><ul><li>统一依赖管理：不再需要重复安装依赖了，你也不需要再复制粘贴文件了</li><li>统一工程配置：避免来回配置</li><li>共享 src：这个其实带来了非常大的一个想象力空间，你可以共享代码，你也可以共享类型。之前前后端那种分割的模式下，你每次需要手动生成类型的时代已经过去了，你们都在一起，为什么还要互相引用呢？为什么还要互相生成呢？直接引用就行，而且是最新的。</li></ul><p>​</p><h4 id="云函数依赖管理"><a href="#云函数依赖管理" class="headerlink" title="云函数依赖管理"></a>云函数依赖管理</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641437162328-9089a894-d1cb-4c43-a712-8f1f7d506104.png#clientId=uf3597841-7d38-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=uf4e611f0&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=222456&amp;status=done&amp;style=none&amp;taskId=u96bb403e-6df6-4846-bcb2-65b21b395e1&amp;title=&amp;width=1920" alt="image.png"><br>针对这个目录结构，我们也做了非常多的优化，其中就包括依赖管理。<br>​</p><p>上图左侧是我们云开发目前的函数结构，我相信所有做过云开发用过函数的都知道，每个云开发函数都是一个单独的一个实例，它包含业务逻辑 / <code>node_modules</code> / <code>package.json</code>，每个函数都互相分隔开来。<br>​</p><p>这样做会带来以下的问题：</p><ul><li>跨函数逻辑复用：比如说你要去检查用户是否登录，utils 的抽象复用，都会带来逻辑复用的问题</li><li>重复依赖：每个函数如果要用到 loadsh，就得重复安装</li><li>重复文件：package.json 等</li></ul><p>针对这个问题，我们提出了新方案：一体化函数。<br>​</p><p>我们将函数以入口文件的方式做了隔离。简单来说，我们不再是像左边一样一个函数包含所有文件，而是只包含一个入口文件。<br>大家可以看到这边我每个函数都有一个入口文件， A 函数 / B 函数 / C 函数其实是可以引用公用的一些逻辑的，比如说有 <code>utils / model / service</code>， 然后共享同一份 <code>node_modules</code> 和 <code>package.json</code>。然后在打包的时候，我们在工程上做优化，而不是在你让你手动去做优化。</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641437548770-a9ace070-37e9-400a-825b-94fbe4eaf553.png#clientId=uf3597841-7d38-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ucac89e3e&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=318530&amp;status=done&amp;style=none&amp;taskId=uf5511ab2-de4c-458e-858a-0234be92605&amp;title=&amp;width=1920" alt="image.png"><br>​</p><p>在打包的时候，我们会根据每个函数入口文件去打包成一个函数，然后单独去做部署，相当于是说你在保留了应用开发的习惯之下，你同时拥有了函数的一个优势，而且不需要去做切换，也不会遇到刚才的那些问题。<br>​</p><p>在这一点，其实我们是使用了 vercel 的 <a href="https://github.com/vercel/ncc">ncc</a> 。 <a href="https://github.com/vercel/ncc">ncc</a> 有个最简单的解释： “Webpack for Node.js”。它是可以把一个函数的入口文件去打包成单独的一个 bundle 的，会把这个函数所引用到的各种的后端的一些模块，不管你是 <code>lodash</code> 还是相关的模块，它都会帮你打包在一起。这样做的话其实是可以保证说你每个函数入口文件都是一个单独的可以部署的一个 JS。<br>​</p><p>然后在这里其实我们也对新的方案做行了一个优化。在我们传统的一个打包方案过程中，我们打一次包其实可能会需要 20 秒到 30 秒，我们需要做非常多构建和优化的一些事情。压缩包也很大，然后解压后的体积也很大，启动时间也很慢。<br>而在单文件的情况下，我们不仅打包时间快，我们压缩包大小也小，我们解压体积也小。因为 Webpack 大家都知道它有 <code>Tree Shaking</code> 的功能，可以把一些用不到的模块给去掉，因为体积小所以启动时间也快。<br>​</p><p>通过这种方案，我们解决了云开发工程中资源机械组合的问题，但是我们觉得还可以再往前进一步。<br>​</p><h4 id="简化调用-“零”-API-实践"><a href="#简化调用-“零”-API-实践" class="headerlink" title="简化调用 - “零” API 实践"></a>简化调用 - “零” API 实践</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641437750414-28882e68-6a81-4e60-8130-42dffe44e966.png#clientId=uf3597841-7d38-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ubfbf6d29&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=291301&amp;status=done&amp;style=none&amp;taskId=uc3728742-aa13-4319-aceb-6b646c1cc04&amp;title=&amp;width=1920" alt="image.png"><br>我们把后面进一步的实践叫：“零” API 实践。<br>​</p><p>刚才我说过共享 src 的代码其实有非常广阔的想象空间，想象空间就在于此。因为你共享代码之后，那么你调用函数或者接口，其实你不需要再去手动发请求，或者不需要再手动使用云函数的客户端。你只需要两步就可以完成整个调用。<br>​</p><ol><li>把你的函数从 API 目录里面导入进来，比如上图的 <code>getUserProfile</code> 函数</li><li>调用函数，拿到返回值</li></ol><p>​</p><p>在这个过程中，我们把原来传统的云函数的发布、调用、获取返回值的过程给精简化了。<br>为什么我们上一章会提到用 JavaScript 函数去写接口，因为这一章我们就要用 JavaScript 去调用。把云函数开发或前后端调用的过程中的胶水层给抹到 0。<br>​</p><p>然后这个就是我们一个核心的设计理念，“零” API。这样做的好处很多：</p><ul><li>前后端调用的胶水层都被抹去了</li><li>通过  <code>useContext</code> 获取到请求上下文，调用接口函数和普通函数是一样的</li><li>前后端都使用 <code>Hooks</code> 的语法，前后端非常一致</li></ul><p>​</p><p>简单且容易理解。<br>​</p><h4 id="简化调用-“零”-API-原理"><a href="#简化调用-“零”-API-原理" class="headerlink" title="简化调用 - “零” API 原理"></a>简化调用 - “零” API 原理</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641439149042-739f5588-a2c2-4509-a5fd-91ddca951a6c.png#clientId=uf3597841-7d38-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ube3ccd29&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=378244&amp;status=done&amp;style=none&amp;taskId=u6e561f43-3188-4da9-bd85-3ecf8803ad6&amp;title=&amp;width=1920" alt="image.png"></p><p>整个方案的原理其实也很简单。<br>​</p><p>大家都知道前端工程里，我们会依赖 Webpack / <code>Vite</code> 此类的构建工具。我们可以通过 Webpack Loader 或者 Vite 的 Plugin 来对导入的文件做处理。<br>​</p><p>如上图所示，一个导入的函数，我们可以在 Loader 中，通过 <a href="https://github.com/guybedford/es-module-lexer">es-module-lexer</a> 来解析导出的函数，最终构建出前端可用的 API Client。也就是说，前端在编辑器里面引入的是后端接口，但实际运行的时候其实已经被替换成了 API Client，从而可以正确的发起调用。</p><p>提一句题外话，<a href="https://github.com/guybedford/es-module-lexer">es-module-lexer</a> 实际上也是个非常有趣的库，这也是 Vite 使用的底层库，用来分析导入导出的，大家有兴趣可以上 Github 看看相关文档和实现。<br>​</p><h3 id="云平台锁定-跨云-amp-多云适配"><a href="#云平台锁定-跨云-amp-多云适配" class="headerlink" title="云平台锁定 - 跨云 &amp; 多云适配"></a>云平台锁定 - 跨云 &amp; 多云适配</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641375322483-03ae465e-1d89-4d70-9d04-acb635ac30e2.png#clientId=u92a3224d-97c6-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=btR89&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=197252&amp;status=done&amp;style=none&amp;taskId=u23ffa8f4-8c71-4793-8603-39ff5215426&amp;title=&amp;width=1920" alt="image.png"></p><p>刚才我们有提到过云平台锁定，那么我们能不能去做一些多云的适配工作呢？其实是可以的。<br>​</p><h4 id="函数式-多云适配"><a href="#函数式-多云适配" class="headerlink" title="函数式 - 多云适配"></a>函数式 - 多云适配</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641440007270-a5cf856b-4310-45d4-b985-31137766166e.png#clientId=u41bb3191-67d3-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u5473bbff&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=144106&amp;status=done&amp;style=none&amp;taskId=u39192368-6982-4225-a343-a20d10b1627&amp;title=&amp;width=1920" alt="image.png"><br>​</p><p>多云适配的原理比较简单。<br>​</p><p>我们针对不同的云厂商，开发了不同的 <code>adapter</code>，每个 <code>adapter</code> 负责如下的工作：</p><ul><li>解析参数</li><li>调用函数</li><li>错误处理</li></ul><p>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641440271475-fae19f57-2198-4728-8da3-b4e7c3d17c5b.png#clientId=u41bb3191-67d3-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u7e1edc1d&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=256957&amp;status=done&amp;style=none&amp;taskId=ud199ff9f-6170-453d-b85c-7ab9755d2e0&amp;title=&amp;width=1920" alt="image.png"><br>​</p><p>上图中有 <code>adapter</code> 简单的示例。一个 <code>adapter</code> 接收 <code>fn</code> 和 <code>ctx</code> 两个参数，其中 <code>fn</code> 就是我要调用的云函数，<code>ctx</code> 则是调用的上下文。<br>​</p><p>关于 <code>ctx</code>，因为不同云对应的入参不一样，因此获取参数的方式也不一样，可以看上图。通过这种方式我拿到它的入参。当我们拿到入参的时候，因为之前提到过入参的格式是数组的话，其实是可以直接和后端参数映射上的。这种情况下，你前端传入的参数就是后端实际收到的参数，就可以实现跨云的一体化调用了。</p><h4 id="跨云原理-多云部署"><a href="#跨云原理-多云部署" class="headerlink" title="跨云原理 - 多云部署"></a>跨云原理 - 多云部署</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641440980703-21026d3c-21d5-49a3-a0f3-9f484e448e3b.png#clientId=u41bb3191-67d3-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ud5d8d0c6&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=142814&amp;status=done&amp;style=none&amp;taskId=u498f4654-1c09-4bbc-a5ee-9bea27a097f&amp;title=&amp;width=1920" alt="image.png"></p><p>同时多云其实还涉及到部署的问题。<br>​</p><p>上图是我们的部署产物的结构，包含：</p><ul><li>User Code：用户代码</li><li>entry.js：云平台入口文件，不同云平台要求入口文件格式和导出方法不一致</li><li>Args Adapter：云平台适配</li></ul><p>这些构建产物可以去部署到不同的云平台，比如说我生成的是阿里云的部署结构，我就可以部署到阿里云生成到腾讯云的结构，我就可以部署到腾讯云，生成到自有服务的结构，就可以部署到自有的一个云平台，非常自由。<br>​</p><p>然后关于该方案其实后面还有很多可以做的事情，但基本原理就是根据不同云平台去生成适配代码，提交部署。而且适配代码大家可以发现是做的非常的轻量的，你不需要去关心特别多的事情，你只需要说去拿到参数，调用函数并返回即可。</p><h4 id="跨云-使用-Case"><a href="#跨云-使用-Case" class="headerlink" title="跨云 - 使用 Case"></a>跨云 - 使用 Case</h4><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641441573814-9aff9fde-1173-495b-b252-69a3703b1fac.png#clientId=u41bb3191-67d3-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ud267f4a3&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=190900&amp;status=done&amp;style=none&amp;taskId=u8be8337c-3581-4892-b2ce-9214b5e7c3a&amp;title=&amp;width=1920" alt="image.png"><br>这里我会列举一下跨云的使用 Case。<br>​</p><p>例如小程序 + 云函数的组合，去落地跨云方案时，其实是可以根据不同云平台去使用不同用的云函数的一些服务的。例如：</p><ul><li>支付宝：支付宝小程序 + 阿里云 FC</li><li>微信：微信小程序 + 微信云函数</li><li>头条：头条小程序 + 轻服务</li></ul><p>​</p><p>因为你的代码你不涉及到具体的后端服务，其实你在云平台上就可以部署和调用。<br>​</p><p>然后还有一点，是关于我们的内部场景，我们称为自建云服务，这个其实能做的事情就非常多了。我们开发的一体化应用，前端可以部署为 Web / 小程序 / Native 页面，后端也可以部署在自建的云服务 / FaaS / 裸机上的。在这种架构模式下，我可以在不同的前端源站及云服务间切换，不仅防止锁定，而且可以统一语法，降低学习成本。<br>​</p><p>这里其实也提到了一个很重要的概念，就是降低统一语法，降低学习成本。像之前社区有两个概念：</p><ul><li>Learn once write everywhere. 这个是 React 的理念</li><li>Write once run everywhere. 这个是国内部分开源产品推荐的理念</li></ul><p>​</p><p>而我们实际上更认可前者，学习一次在哪儿都能写。</p><h2 id="iFashion-落地实践"><a href="#iFashion-落地实践" class="headerlink" title="iFashion 落地实践"></a><strong>iFashion 落地实践</strong></h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641450163664-726f1dd8-8fad-4037-9fc5-a1a354d99471.png#clientId=u41bb3191-67d3-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u0deabe21&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=94163&amp;status=done&amp;style=none&amp;taskId=ua685b71a-438a-4953-9e30-eacfc2e53e1&amp;title=&amp;width=1920" alt="image.png"><br>在解决了上述的问题后，我们尝试在内部找了一个小程序 IFashion 做落地尝试。<br>​</p><h3 id="项目概况"><a href="#项目概况" class="headerlink" title="项目概况"></a>项目概况</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641450206802-6b6ca26e-2cba-44c0-bee7-637ac917b875.png#clientId=u41bb3191-67d3-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ua2a00316&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=977245&amp;status=done&amp;style=none&amp;taskId=ufebff0d5-6d43-4661-b2d0-f07bf11eab1&amp;title=&amp;width=1920" alt="image.png"><br>​</p><p>iFashion 是淘宝服饰的应用，也是我们内部流量非常大的一个小程序应用。<br>​</p><p>技术栈如下：</p><ul><li>Rax</li><li>阿里云定制版</li><li>构建工具 Web</li><li>投放平台：手淘小程序 &amp; H5 页面（我们开发的应用会投放至全网。投放在手淘是小程序，投放在外部我们是 H5 页面）</li></ul><p>前两年淘系前端在推动一个前端研发模式升级战役，在一体化方案推出后，iFashion 也希望使用一体化方案来去接入一个内部的云服务去提升效率。然后这一点就是我刚才提到的那个使用的 case ，如何去把这个服务去部署到自建云平台上。因为有些公司不会用公有云，它自己就有云服务（比如阿里、腾讯等云服务商）。<br>​</p><h3 id="Webpack-工程接入"><a href="#Webpack-工程接入" class="headerlink" title="Webpack 工程接入"></a>Webpack 工程接入</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641450761016-b7cf1739-1417-45d4-a710-ab2ee60c4574.png#clientId=u41bb3191-67d3-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ubf1162a2&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=277942&amp;status=done&amp;style=none&amp;taskId=uccd9acda-28e2-4250-b437-7eb2c5eb32d&amp;title=&amp;width=1920" alt="image.png"><br>实际工程接入一体化应用需要解决几个问题，第一个就是如何接入 Webpack 的工程体系。<br>​</p><p>这一点其实非常简单。这一点我们开发了 Webpack Loader 和 Express（for webpack dev server） 的中间件，然后使用 Webpack chain 去快速接入原有的工程。<br>​</p><p>然后上图就是我们实际接入的代码，你通过这简单的几行代码，你就可以去接入一体化工程，成本很低。然后图中右侧就是我们实际的调用流程图。<br>​</p><h3 id="一体化调试"><a href="#一体化调试" class="headerlink" title="一体化调试"></a>一体化调试</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641451130067-8734040c-0498-4c7f-b49b-beb3b655a10a.png#clientId=u41bb3191-67d3-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u225f63dd&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=538411&amp;status=done&amp;style=none&amp;taskId=u11d8de79-2940-4655-ae2f-1bf40d21829&amp;title=&amp;width=1920" alt="image.png"><br>然后还有这部分是第二部分是关于一体化调试的，这部分也是目前我们做的觉得体验非常好的一个功能。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641451141085-ef279df7-7358-419d-acb8-2c9a8a3b38f0.png#clientId=u41bb3191-67d3-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u6d3da8f0&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=376085&amp;status=done&amp;style=none&amp;taskId=ub58bc81a-3143-4e31-bc3f-c1fd891350b&amp;title=&amp;width=1920" alt="image.png"><br>这里我写了一个简单的工程，前后端代码分别如上图，功能也很简单，就是展示 <a href="https://github.com/alibaba/rax">Rax</a> 和 <a href="https://midwayjs.org/">Midway</a> 两个仓库的 Stars。</p><p>题外话 +1。大家看上图的后端代码，我第四行其实有个注释，这个给 Github 的 <a href="https://copilot.github.com/">Copilot</a> 用的。整段 Demo 基本上是 Copilot 写的，在小 Demo 的场景非常好用。<br>​</p><p>然后下面是我们调试录制的视频。在一体化调试功能下，你可以在编辑器中，一次完成从前端到后端的全链路 Debug。<br>​</p><p><a href=""><img src="https://gw.alipayobjects.com/mdn/prod_resou/afts/img/A*NNs6TKOR3isAAAAAAAAAAABkARQnAQ" alt="iShot2021-11-26-15.52.50-8476.mp4 (1.71MB)"></a></p><blockquote><p>一体化调试放大版</p></blockquote><p><a href=""><img src="https://gw.alipayobjects.com/mdn/prod_resou/afts/img/A*NNs6TKOR3isAAAAAAAAAAABkARQnAQ" alt="我的影片-1-9576.mp4 (687.29KB)"></a>​</p><h3 id="一体化发布"><a href="#一体化发布" class="headerlink" title="一体化发布"></a>一体化发布</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641453450859-b85cf931-9fd7-459e-b0ec-9c7cfae86c4a.png#clientId=u05debb1c-79cd-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u878b0cb1&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=192870&amp;status=done&amp;style=none&amp;taskId=u584f612f-c9c5-4003-a31a-8e4984d49b2&amp;title=&amp;width=1920" alt="image.png"><br>关于应用的发布，我们也去定制了我们内部的发布工作流。<br>​</p><p>如上图所示。当我们在点击发布之后，我们 Workflow 会运行设定好的 steps ，去做不同的事情。<br>​</p><p>从源码检查开始，然后同时会进入一个并行的构建的过程。首先是前端进行构建，这种时候前端会进将它的产物进行一个上传和部署，你是网页就到 CDN，你是小程序就到小程序平台。然后也会对函数做一个构建和一个部署，去部署到相应的 FaaS 平台上或者多个云平台上。<br>​</p><p>构建完成，我们有一体化灰度和切流的节点。简单的说，相较于之前的前后端分开切流，中间需要配合不一样。我们在一体化切流的 workflow 中也做了非常多的一些定制，我们能保证整个应用现在按一体化的维度去做切流，新应用新接口，老应用老接口，新老接口不会串。我们认为这种是一种更简单的一种开发方式。</p><p>灰度完成后即正式上线。</p><h3 id="提效-30"><a href="#提效-30" class="headerlink" title="提效 30%"></a>提效 30%</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641454024070-cf8cee93-7cb1-49f5-b776-6533da51335b.png#clientId=u05debb1c-79cd-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u38e2bfcd&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=160280&amp;status=done&amp;style=none&amp;taskId=uf8d70596-1c74-4138-b2a2-35b2d016c57&amp;title=&amp;width=1920" alt="image.png"><br>这个是我们上线数据实测的结果。<br>​</p><p>图中上半部分，是我们之前在开发一个需求时，需要投入前端和后端两名同学，大概一起开发 13 个人日。<br>而图中下半部分则是使用一体化这种开发，只要投入一个人 + 10 个人日即可。<br>​</p><p>这个听起来有点不可思议。但是我们在内部大量的应用的实践的情况下，我们发现当你使用一体化这种开发的方式，你去开发一些简单或者一般的需求的时候，你完全可以实现节约人力投入且节约时间的效果。<br>​</p><p>因为在开发过程中，其实大家有做过前后端联调，都知道联调是非常繁琐的一个过程。联调 / debug /上线等待花费了很多时间，但可能你的改动就两行代码。因此在使用一体化的时候，你一个人开发，就可以大大加速整个流程。<br>​</p><h2 id="总结与展望"><a href="#总结与展望" class="headerlink" title="总结与展望"></a>总结与展望</h2><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641454833942-39e57956-aca3-4331-8850-507a8ae26d93.png#clientId=u05debb1c-79cd-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ude558a44&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=88465&amp;status=done&amp;style=none&amp;taskId=u2f7ca88f-c686-47ec-80d0-ee3f35bd953&amp;title=&amp;width=1920" alt="image.png"><br>最后一部分是总结与展望。<br>​</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641454874186-2a127bdb-c647-4a51-81c3-33e6407fd9b6.png#clientId=u05debb1c-79cd-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ucbedbe8e&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=144322&amp;status=done&amp;style=none&amp;taskId=ufcd25cf2-b7ac-4efe-9e82-68806ede601&amp;title=&amp;width=1920" alt="image.png"></p><p>在跨端小程序方案中，我们去尝试引入云和一体化的开发方案来进一步为开发同学提效。<br>​</p><p>这一点其实也是我们架构团队一直在探索的，我们认为端侧红利及工程红利已经见顶的时候，有什么办法能进一步提高它的开发效率呢？<br>​</p><p>于是我们尝试了云和端的一个结合，这个也是我们未来非常重要的目标和方案，我们会尝试去更多的和云上的能力去做一个结合，而不仅仅是在端上去寻找更多的一些红利。因为你端上再怎么做，你还是需要那么多人。而你在云上做，你一个人做掉了反而可能效率更高，这是一个非常神奇的一个反应。<br>​</p><h3 id="展望：云端融合"><a href="#展望：云端融合" class="headerlink" title="展望：云端融合"></a>展望：云端融合</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641454996662-71ba633e-312f-4091-98e4-fd34f50f0d50.png#clientId=u05debb1c-79cd-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u2c97806e&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=391796&amp;status=done&amp;style=none&amp;taskId=u73c9fa0f-bdd7-46f7-9988-9ed0faf695e&amp;title=&amp;width=1920" alt="image.png"><br>在目前的场景中，我们也去加入了 TC 39，参与提案的讨论。<br>​</p><p>上图中有两个提案，一个是 <a href="https://github.com/tc39/proposal-js-module-blocks">JS Module Blocks</a>，另外一个是衍生的提案 <a href="https://github.com/tc39/proposal-module-fragments">JS Module Fragments</a>。我们更关注的是后者，因为这能为一体化应用带来更好的研发体验。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641455795449-1c68b3d8-e58f-4054-9196-ddebcbce9a80.png#clientId=u05debb1c-79cd-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=ub4a10d4e&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=308542&amp;status=done&amp;style=none&amp;taskId=uc87e7f7f-8292-4002-aa4b-1688cace31a&amp;title=&amp;width=1920" alt="image.png"><br>上图就是我们基于 <a href="https://github.com/tc39/proposal-module-fragments">JS Module Fragments</a> 写的 Demo。这里面我可以有一个叫 cloud 的模块，写一些后端的代码，我可以去引入微信的服务端的 SDK，可以去拿到微信的 <code>context</code>，我可以去获取它的 <code>OpenId</code>。同时我可以有一个 client 的模块，来写前端的逻辑。其中后端接口的调用可以直接从云端这个 cloud 这个模块导入。<br>​</p><p>可以发现在这个提案下，一个 JS 文件就包含了前端、后端两部分的代码，并且非常和谐。然后这个也是目前我们未来可能会去倡导的一个理念叫云端更加融合，更加一体化。<br>​</p><p>题外话 +2，云端一体在前端框架的趋势已经非常明显了，无论是 Next.js/Nuxt/Svelte Kit 或者是新出的 Remix 框架，大家都在更多地去结合云函数的能力，去提供服务 OR 实现 SSR。</p><p>然后目前我们也在和前端委员会的标准化小组去共同推进提案，这个是我们的反馈链接：<a href="https://github.com/tc39/proposal-module-fragments/issues/14">https://github.com/tc39/proposal-module-fragments/issues/14</a>。<br>​</p><h3 id="欢迎关注"><a href="#欢迎关注" class="headerlink" title="欢迎关注"></a>欢迎关注</h3><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641456188061-2aade372-fc12-4057-8617-85af337605fb.png#clientId=u05debb1c-79cd-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=u7767f352&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=446001&amp;status=done&amp;style=none&amp;taskId=u5967dfa1-6469-42ee-85af-83a487b9250&amp;title=&amp;width=1920" alt="image.png"><br>整个一体化框架是基于 Midway.js 开发的，也欢迎大家多多 Star 或者 Watch 我们的 Repo，看看我们最近在搞什么新东西。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2022/png/98602/1641456238140-b8687011-6e65-4a0c-a707-06304794115f.png#clientId=u05debb1c-79cd-4&amp;crop=0&amp;crop=0&amp;crop=1&amp;crop=1&amp;from=paste&amp;height=1080&amp;id=uf4353a2c&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;originalType=binary&amp;ratio=1&amp;rotation=0&amp;showTitle=false&amp;size=245092&amp;status=done&amp;style=none&amp;taskId=ua0120752-f63e-4dbb-8a7a-ae11515b564&amp;title=&amp;width=1920" alt="image.png"><br>图中“<strong>让 Node.js Web 开发更简单 &amp; 有趣</strong>”是个人的 Slogan，也是一直在努力的方向。<br>​</p><p>本次分享到此结束，谢谢大家！</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;演讲录音转文字 + 校对，偏口语化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2022/png/98602/1641372993619-3078e36a-b069-4
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>从文科生到前端专家 - 在转行时我想过的问题</title>
    <link href="https://blog.lxxyx.cn/2021/10/11/2021/seven/"/>
    <id>https://blog.lxxyx.cn/2021/10/11/2021/seven/</id>
    <published>2021-10-11T17:00:34.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<h2 id="从一篇约稿说起"><a href="#从一篇约稿说起" class="headerlink" title="从一篇约稿说起"></a>从一篇约稿说起</h2><p>在 2021 年的 8 月份，工作中的我接到了淘系技术运营同学的约稿，邀请我谈一下“程序员最重要的能力是什么”。随后我根据自身经历，写下了回答，全文发表在淘系技术公众号的<a href="https://mp.weixin.qq.com/s/h_95kkAKy1Nr-Gs3iokxTg">《程序员最重要的能力是什么？》</a>中。<br>​</p><p>到了九月底，淘系技术运营的同学和我说文章发表出去的反馈不错，希望我能继续写一下具体的心路历程，于是就有了这篇文章。<br>​</p><p>希望能为和我一样，对编程感兴趣/犹豫转行/正在转行的同学们提供一点经验分享。<br>​</p><h2 id="我的编程学习经历"><a href="#我的编程学习经历" class="headerlink" title="我的编程学习经历"></a>我的编程学习经历</h2><p>我是转行成为程序员的，可以分享一下我的编程学习经历。<br>​</p><h3 id="在学习编程之前"><a href="#在学习编程之前" class="headerlink" title="在学习编程之前"></a>在学习编程之前</h3><p>在高中时，因为学不会物理和化学，我选择了文科，这也意味着计算机专业类对我关上了大门，不过在当时我也没有想到自己在未来会喜欢上写代码并以此为生。<br>​</p><p>现在回想起来，与编程有关的只有两件事情。<br>​</p><p>一是在繁忙的高中生活中，我一直订阅着《电脑报》，并通过它了解互联网的时事。二是当时的文科数学需要解一种流程图题，也是我最喜欢的题型，现在看来与代码流程其实是无异的。<br>​</p><blockquote><p>流程图题</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1633525665670-6b52d3d3-0394-4b0b-a506-f93c262487d8.png#clientId=ua694a533-e7a2-4&amp;from=paste&amp;id=u9a0a0643&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=367&amp;originWidth=548&amp;originalType=binary&amp;ratio=1&amp;size=29640&amp;status=done&amp;style=stroke&amp;taskId=u16fcd43a-e8f3-46ba-a5ee-c78f20222f8" alt="image.png"></p><h3 id="从-C-语言开始"><a href="#从-C-语言开始" class="headerlink" title="从 C 语言开始"></a>从 C 语言开始</h3><p>升学后，我就读的是人力资源管理专业。2015 年初，大一上学期的期末考试刚结束，距离回家尚有一段时间，因为闲着无聊便上网络学起了翁恺老师的《C 语言基础》课程，随后就被带入到了计算机编程的坑中，至今尚未脱坑并越陷越深。<br><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1633525049531-6af147b7-848e-4ba9-93b2-f98827d62172.png#clientId=ua694a533-e7a2-4&amp;from=paste&amp;height=366&amp;id=u49b08494&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=732&amp;originWidth=1894&amp;originalType=binary&amp;ratio=1&amp;size=721903&amp;status=done&amp;style=none&amp;taskId=uec90fb7e-d607-4287-a9c6-170c5156c3b&amp;width=947" alt="image.png"><br>记得当时的我，在 2015 年寒假一个月的时间内。开始了每天听课，思考。然后把代码输进手机慢慢运行的过程（那时候还没有笔记本电脑）。当时最开心的就是图片的这一段代码，一次性编译运行通过。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1633941931466-ebf7be53-2cfe-45b5-b49a-0016fae4e62a.png#clientId=u1acf5c5b-d152-4&amp;from=paste&amp;height=659&amp;id=u83035b24&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=659&amp;originWidth=576&amp;originalType=binary&amp;ratio=1&amp;size=199631&amp;status=done&amp;style=none&amp;taskId=u21c6b1a2-2378-4f8a-a0d9-70a8fb6b174&amp;width=576" alt="image.png"></p><h3 id="从“年轻工程师”到阿里前端技术专家"><a href="#从“年轻工程师”到阿里前端技术专家" class="headerlink" title="从“年轻工程师”到阿里前端技术专家"></a>从“年轻工程师”到阿里前端技术专家</h3><p>往后的半年中，编程的学习也并没有像想象中的那样顺利。学习进度停滞不前。我还清楚的记得当时的自己：“会用 20 门语言输出 Hello World!”，“别人一周就能上手的 BootStrap 我学了四个月” 等诸如此类的事情。</p><p>偶然的一个契机，我找到了愿意带我写代码的同学，并磕磕绊绊的完成了第一次 <code>Ajax</code> 操作。那之后的我仿佛开窍了一般，编程世界的大门，终于向我敞开，任我探索和遨游。<br>​</p><p>直到有一天，我遇到了瓶颈。<br>​</p><p>2016 年的前端，技术日新月异，当时的我颇有一些手足无措，总觉得时间不够用。于是在参加当年的 JSConf 时，遇见了当时前端圈的名人贺师俊（Hax），于是便壮着胆上去交流了一番。<br>​</p><p>对话的内容我只记得个大概，在交流结束时我向贺老致谢，他回复我说：“学东西不要有那么强的功利心，而且对于我来说，能帮你们这些年轻的工程师就非常好了。”<br>​</p><p>在那时候我发现，对<strong>编程的兴趣已经托着我从学生过渡到大家眼里“年轻的工程师”了</strong>。时间一晃眼已经过去了 5 年，当年的学生，也从“年轻的工程师”蜕变成了阿里的前端技术专家，<strong>身份在变、环境在变、技术在变，只有对代码的兴趣没有改变</strong>。<br>​</p><h3 id="从-Node-js-使用者到-Node-js-Collaborator"><a href="#从-Node-js-使用者到-Node-js-Collaborator" class="headerlink" title="从 Node.js 使用者到 Node.js Collaborator"></a>从 Node.js 使用者到 Node.js Collaborator</h3><p>由兴趣驱动的源动力，往往是纯粹且不掺杂利益的，而在软件世界中，<strong>兴趣驱动与开源精神是高度契合的</strong>。</p><p>在 2020 年冬季到来的时候，我遇到了新的瓶颈，想学习更多 Node.js 的知识却不知从何下手。<br>​</p><p>这一次我的选择是参与 Node.js 开源社区的协作。我开始阅读 Node.js 的源码，解决 Github 上的 Issue 等。在事后提名 Node.js Core Collaborator 时，我统计了一下，两个月的时间内我一共提出了 50+ 的 Pull Request。<br>​</p><p>现在回头来看，<strong>这些与实际工作“无关的兴趣”，最终支撑着我走的更远</strong>，现在的我在 Node.js 架构团队，全职负责着 Node.js 的工作，个人的眼界也开拓了许多。<br>​</p><h2 id="转行的那些问题"><a href="#转行的那些问题" class="headerlink" title="转行的那些问题"></a>转行的那些问题</h2><p>转行并非是一帆风顺的。在这个过程中，最常见的问题莫过于如下：</p><ul><li>非科班的同学，能学会计算机吗？</li><li>计算机专业学了 4 年，你自学的能跟得上吗？</li><li>出来后能找到工作吗？</li><li>转行的过程中需要做什么呢</li><li>……</li></ul><p>​</p><p>这些问题从我学习编程时，就一直萦绕在耳旁，时至今日依然会有同学找我聊天，然后再问出上述的问题。也是借着此机会，我写一下我的想法。<br>​</p><h3 id="非科班能学的会计算机-自学能跟得上吗？"><a href="#非科班能学的会计算机-自学能跟得上吗？" class="headerlink" title="非科班能学的会计算机 / 自学能跟得上吗？"></a>非科班能学的会计算机 / 自学能跟得上吗？</h3><p>答案是肯定的：“可以的，没问题的”。<br>​</p><p>说起来，我从未纠结过自己非科班的出身。在学习编程的伊始，我就想清楚了，如果想要成为一名优秀的程序员，科班 / 非科班的身份根本就无关紧要，真正的关键在于个人的学习效率与投入的时间。<br>​</p><p>而在大学中，我投入了大量的时间，平均每天编程 6h，这样的生活一直持续到毕业。而在毕业时，我已经完成了大约十几个项目，写过十几万行代码，也有一个十几万字的博客。<br>​</p><blockquote><p>博客统计</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1633942029296-2dc476e6-07f5-4a33-b851-1d03d7dc85e1.png#clientId=u1acf5c5b-d152-4&amp;from=paste&amp;height=119&amp;id=u777d0885&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=119&amp;originWidth=360&amp;originalType=binary&amp;ratio=1&amp;size=13570&amp;status=done&amp;style=none&amp;taskId=u7c51bc31-1200-478c-ac21-960819235cb&amp;width=360" alt="image.png"></p><blockquote><p>大学时写的一个项目</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1633942063448-7b7c351b-1715-4452-93a3-4f6078f503e8.png#clientId=u1acf5c5b-d152-4&amp;from=paste&amp;height=304&amp;id=uc2215393&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=304&amp;originWidth=730&amp;originalType=binary&amp;ratio=1&amp;size=50959&amp;status=done&amp;style=none&amp;taskId=ufbcbb1f3-00dd-4d3c-bd1f-e3f320dd44a&amp;width=730" alt="image.png"></p><blockquote><p>Commit 记录</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1633942119773-64abe441-9209-4edb-a600-903007de4ec3.png#clientId=u1acf5c5b-d152-4&amp;from=paste&amp;height=208&amp;id=u6c494868&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=208&amp;originWidth=783&amp;originalType=binary&amp;ratio=1&amp;size=19161&amp;status=done&amp;style=none&amp;taskId=u6fc04d0b-d167-4345-9e3a-83112f91d08&amp;width=783" alt="image.png"><br>​</p><p>换言之，我也认为，如果一名非科班的同学，学过计算机的课程，写过很多行代码，也可以顺利的完成项目，那么他就是程序员。<br>​</p><h3 id="出来后能找到工作吗"><a href="#出来后能找到工作吗" class="headerlink" title="出来后能找到工作吗"></a>出来后能找到工作吗</h3><p>答案也是肯定的：“可以的，没问题的”。</p><p>计算机编程就业环境还是很好的，并不会卡死专业，而是通过过往的经历 + 笔试/面试来决定是否录用你。所以不用太担心这方面的问题。<br>​</p><p>这个问题我之前在知乎也回答过，所以就不再重写啦，可以看下面的截图，也可以通过<a href="">链接</a>查看。<br><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1633942500244-b482417a-14b7-4534-a8a6-67a792fd0a3a.png#clientId=u1acf5c5b-d152-4&amp;from=paste&amp;height=969&amp;id=ue81758a4&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=969&amp;originWidth=751&amp;originalType=binary&amp;ratio=1&amp;size=198528&amp;status=done&amp;style=none&amp;taskId=u35c00568-388a-4518-b79a-55767674451&amp;width=751" alt="image.png"><br>​</p><h3 id="转行的同学要如何学习呢"><a href="#转行的同学要如何学习呢" class="headerlink" title="转行的同学要如何学习呢"></a>转行的同学要如何学习呢</h3><p>这可以说是社区每天都能遇见的问题了。但授人以鱼不如授人以渔，我说一下我自学新领域所使用的方法。</p><p>方法很简单：上知乎，搜索 XX 入门（如计算机入门，前端入门），就会出现一大堆提问，你需要的只是耐心读完其中高赞的一些回答，并归纳总结分析，大概心里就有数了。随后选择其中你觉得靠谱的高赞回答去学习即可。</p><p>许多提问的高赞回答其实都回答的很详细也很好，认准教程后跟着学习即可，学习的过程中你又会遇到新的问题，这个时候继续再搜索即可。<br>​</p><p>这个方法很管用，时至今日，我在学习产品、设计、演讲、游戏等新领域时都是这样做的，在尝试和摸索的过程中，你的自学能力也会得到提升。<br>​</p><h4 id="我的自学方式"><a href="#我的自学方式" class="headerlink" title="我的自学方式"></a>我的自学方式</h4><p>我在刚学习前端时，幸运的在知乎上看到了大漠关于前端自学的回答。<br>​</p><blockquote><p>后面我还经历了这样的一过程,我思考过三类问题,并且将他们列在一起： 1.哪些知识点懂了？ 2.哪些知识似懂非懂？ 3.哪些知识不懂？<br>接下来有了这样的三份清单之后，就能非常清楚自己知道自己，然后先解决第二个清单中的 list，再解决第三个清单中的 list。<br>最后建议，学习这个过程是不断渐进的，整个过程把握：多看、多想、多问和多做。这也是我自己的四多原则。如果你时间允许，多写写东西，总结自己的知识。现多看看规范。<br>​</p></blockquote><blockquote><p><a href="https://www.zhihu.com/question/29236821/answer/43823443">我想做 web 前端，怎么学习–大漠的回答</a></p></blockquote><p>​</p><p>后面也针对自己的情况，画了一张思维导图，把似懂非懂的知识点列了出来。<br><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1633941705330-ff1bf3d8-3035-4496-ad4c-cfc6b56b80d1.png#clientId=u1acf5c5b-d152-4&amp;from=paste&amp;height=1240&amp;id=u49cebb02&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1240&amp;originWidth=924&amp;originalType=binary&amp;ratio=1&amp;size=258328&amp;status=done&amp;style=none&amp;taskId=u45bce3ef-8a7c-4ae4-a280-5193e2252f7&amp;width=924" alt="image.png"><br>后面在寒假里针对这些问题重点攻关，并写成总结的技术文章。<br>​</p><blockquote><p>2016 年的文章</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1633941771739-b19e339b-539c-44b2-9f77-6686678473dd.png#clientId=u1acf5c5b-d152-4&amp;from=paste&amp;height=384&amp;id=u194768b2&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=384&amp;originWidth=600&amp;originalType=binary&amp;ratio=1&amp;size=87326&amp;status=done&amp;style=none&amp;taskId=u24394ddb-5e65-4a7a-a10d-96b970404c0&amp;width=600" alt="image.png"></p><p>目前来看，找到自己“似懂非懂”的知识，学习，总结，分享是一个非常有效的学习方式，有兴趣的同学也可以参考试试。<br>​</p><p>我也一直坚持，学习方案的重点在于学习效果，而不是方案有多复杂和多详尽，适合自己就好。<br>​</p><h3 id="转行的过程中需要做什么呢"><a href="#转行的过程中需要做什么呢" class="headerlink" title="转行的过程中需要做什么呢"></a>转行的过程中需要做什么呢</h3><p>这里我的建议是：</p><ul><li>多做项目</li><li>多写总结</li><li>多打基础</li></ul><p>首先我建议多做项目，比起打下“扎实的基础”，对于转行的同学，培养起对编程的兴趣是更重要的。不同于科班良好的学习氛围与节奏，自学往往对自制力的要求更高，这时候培养起对计算机的兴趣，能让自己继续学习写代码才是更重要的。<br>​</p><p>其次我建议多写总结，总结是一个非常高效的学习过程，如果你要把一件事情给讲清楚，那么首先一定要弄懂这个知识。自学计算机的过程中，多总结不仅可以查漏补缺，也可以让自己在回顾的时候多一份积累的感觉。<br>​</p><p>最终则是建议多打基础，转行计算机的同学，计算机底层原理，算法等知识接触较少，但这些却实打实的是基础，可以帮助你未来少踩很多坑的知识，因此在学习之余也可以多关注计算机基础知识（至于怎么学习，可以参考上面的回答）。<br>​</p><h2 id="回头看转行"><a href="#回头看转行" class="headerlink" title="回头看转行"></a>回头看转行</h2><p>时光荏苒，回头看已经写了快 7 年代码了。<br>​</p><p>回头再看自己的转行经历，与其说是文科生转行学了计算机，倒不如说是自己发现了对计算机的兴趣，并持续投入至今。<br>​</p><p>最后，送上我很喜欢的一段话作为结尾。<br>​</p><blockquote><p>「_Your work is going to fill a large part of your life, and the only way to be truly satisfied is to do what you believe is great work. And the only way to do great work is to love what you do. If you haven’t found it yet, keep looking. Don’t settle. As with all matters of the heart, you’ll know when you find it. _」 — Steve Jobs</p></blockquote>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;从一篇约稿说起&quot;&gt;&lt;a href=&quot;#从一篇约稿说起&quot; class=&quot;headerlink&quot; title=&quot;从一篇约稿说起&quot;&gt;&lt;/a&gt;从一篇约稿说起&lt;/h2&gt;&lt;p&gt;在 2021 年的 8 月份，工作中的我接到了淘系技术运营同学的约稿，邀请我谈一下“程序员最重要的能
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>esbuild 二进制下载提速 3 倍的秘密</title>
    <link href="https://blog.lxxyx.cn/2021/09/29/2021/esbuild-install/"/>
    <id>https://blog.lxxyx.cn/2021/09/29/2021/esbuild-install/</id>
    <published>2021-09-29T12:01:55.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1632885482713-4be205ee-f80b-4564-b8a9-5372d1de0f3e.png#clientId=ucf0fa086-a4ed-4&amp;from=paste&amp;height=340&amp;id=oo85y&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=340&amp;originWidth=680&amp;originalType=binary&amp;ratio=1&amp;size=130834&amp;status=done&amp;style=stroke&amp;taskId=u0cd531c2-c18c-4716-b393-dd344786340&amp;width=680" alt="image.png"></p><p><a name="RHki8"></a></p><h2 id="esbuild-原有下载机制的问题"><a href="#esbuild-原有下载机制的问题" class="headerlink" title="esbuild 原有下载机制的问题"></a>esbuild 原有下载机制的问题</h2><p>最近有用户在 esbuild 的 Github 仓库提了一个 Issue：<a href="https://github.com/evanw/esbuild/issues/789">Different strategy for installing platform-specific binaries</a>，报告了部分用户在安装 esbuild 依赖时出现的权限错误。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">&gt; esbuild@0.8.43 postinstall /usr/local/lib/node_modules/netlify-cli/node_modules/esbuild</span><br><span class="line">&gt; node install.js</span><br><span class="line">Trying to install <span class="string">&quot;esbuild-linux-64&quot;</span> using npm</span><br><span class="line">Failed to install <span class="string">&quot;esbuild-linux-64&quot;</span> using npm: EACCES: permission denied, <span class="built_in">mkdir</span> <span class="string">&#x27;/usr/local/lib/node_modules/netlify-cli/node_modules/esbuild/esbuild-r63cli2pgpi&#x27;</span></span><br><span class="line">Trying to download <span class="string">&quot;https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.8.43.tgz&quot;</span></span><br><span class="line">/usr/local/lib/node_modules/netlify-cli/node_modules/esbuild/install.js:217</span><br><span class="line">      throw e;</span><br><span class="line">      ^</span><br><span class="line">Error: EACCES: permission denied, open <span class="string">&#x27;/usr/local/lib/node_modules/netlify-cli/node_modules/esbuild/bin/esbuild&#x27;</span></span><br><span class="line">  errno: -13,</span><br><span class="line">  syscall: <span class="string">&#x27;open&#x27;</span>,</span><br><span class="line">  code: <span class="string">&#x27;EACCES&#x27;</span>,</span><br><span class="line">  path: <span class="string">&#x27;/usr/local/lib/node_modules/netlify-cli/node_modules/esbuild/bin/esbuild&#x27;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>背后的原因也很简单，因为 esbuild 是使用 Go 语言开发的，因此如果要使用 esbuild ，那么在安装时就必须下载各自平台对应的二进制包。</p><p>而在之前的 esbuild 版本中，这段逻辑是利用 npm  <code>postinstall</code> 的 hooks 去实现的，在安装时会通过 node 判断系统平台，并下载对应的二进制包。</p><p>这样做也带来一些问题：</p><ul><li>权限问题，动态下载可能会报错</li><li>二进制包是从 npm 下载的，不支持内网、自定义 npm 源、代理等情况，速度也慢</li></ul><p>在 Issue 中，也提到了可以使用 npm 的 <code>optionalDependencies + cpu</code> 来解决多平台二进制包分发的难题。</p><p>随后 esbuild 实现了该逻辑，并提交了 PR。</p><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1632886317011-0bd6b9d9-57c0-4edd-a496-3670f19f6b8f.png#clientId=ucf0fa086-a4ed-4&amp;from=paste&amp;height=340&amp;id=u19f1168d&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=340&amp;originWidth=680&amp;originalType=binary&amp;ratio=1&amp;size=123399&amp;status=done&amp;style=stroke&amp;taskId=uc1c0d1ba-bb28-4375-9468-bb4097b7b63&amp;width=680" alt="image.png"></p><p>测试数据的结果也非常喜人，安装时间从 5.4s 减少到了 1.5s，提速了大约 3 倍。<br><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1632886245827-644e160d-0624-4923-9e5a-fbd3c6a242fc.png#clientId=ucf0fa086-a4ed-4&amp;from=paste&amp;height=136&amp;id=u339f3012&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=136&amp;originWidth=834&amp;originalType=binary&amp;ratio=1&amp;size=21943&amp;status=done&amp;style=none&amp;taskId=u5ce01928-2d69-47ca-8f47-48d099bdd08&amp;width=834" alt="image.png"></p><p>在仔细查看相关 PR 后，我觉得这是一个非常巧妙的解决方案。<br><a name="PS908"></a></p><h2 id="npm-的-optionalDependencies-与-cpu"><a href="#npm-的-optionalDependencies-与-cpu" class="headerlink" title="npm 的 optionalDependencies 与 cpu"></a>npm 的 optionalDependencies 与 cpu</h2><p>长话短说。</p><p>在 npm 中，optionalDependencies 意味着是可选依赖，此类依赖在安装时，即使是安装失败了也不会中断安装的过程，而是交由 npm 包自行去判断。</p><p>而 cpu 字段则代表这个 npm 包只能在指定的 cpu 架构上运行。比如你指定了<code>&quot;cpu&quot;: [ &quot;arm&quot; ]</code>，那么 npm 在安装时会通过 <code>process.arch</code> 来判断，这个包是否可以安装。</p><p>而 esbuild，正是通过 <code>optionalDependencies</code> 与 <code>cpu</code> 字段的配合，实现了下载提速 3 倍的效果。<br><a name="KczoJ"></a></p><h2 id="下载提速的原理"><a href="#下载提速的原理" class="headerlink" title="下载提速的原理"></a>下载提速的原理</h2><p>在 PR 中，esbuild 将所有的二进制包都加入了 <code>optionalDependencies</code> 中。</p><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1632892927240-c22a28b3-8a45-4c40-a6e2-f7d252ff9a50.png#clientId=ucf0fa086-a4ed-4&amp;from=paste&amp;height=1108&amp;id=ub7a98115&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1108&amp;originWidth=2144&amp;originalType=binary&amp;ratio=1&amp;size=209124&amp;status=done&amp;style=none&amp;taskId=u5a53ddfd-4880-4b9c-b424-8995622c3f5&amp;width=2144" alt="image.png"><br>而同时在每个二进制包的 package.json 中，指定了 <code>cpu</code> 字段，也就是将原来的匹配平台，下载对应二进制包的工作交由了 npm 去实现。</p><p>尝试通过 <code>npm i esbuild --verbose</code> 来输出日志，可以清晰的看到：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-windows-arm64</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-windows-64</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-windows-32</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-sunos-64</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-openbsd-64</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-ppc64le</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-mips64le</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-arm64</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-arm</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-64</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-linux-32</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-freebsd-arm64</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-freebsd-64</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-darwin-64</span><br><span class="line">npm verb reify failed optional dependency esbuild/node_modules/esbuild-android-arm64</span><br><span class="line"></span><br><span class="line">npm http fetch GET 200 https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.3.tgz 3635ms (cache miss)</span><br><span class="line">npm timing reifyNode:node_modules/esbuild-darwin-arm64 Completed <span class="keyword">in</span> 3641ms</span><br></pre></td></tr></table></figure><p>因为我的系统是 m1 的 mac，因此只有 <code>esbuild-darwin-arm64</code> 这个包被安装了下来，其他的包则失败了。</p><p>这么做的好处在于：</p><ul><li>不用动态去 npm 拉取包，对于使用自定义 npm 源的用户是友好的（特别是使用 npm 镜像源做反代的情况）</li><li>安装全程由 npm 完成，最大程度的避免了权限的问题</li></ul><p>原理很简单，但是思路很巧妙，值得学习。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2021/png/98602/1632885482713-4be205ee-f80b-4564-b8a9-5372d1de0f3e.png#clientId=ucf0fa086-a4ed-4&amp;a
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>程序员最重要的能力是什么？</title>
    <link href="https://blog.lxxyx.cn/2021/08/17/2021/coder/"/>
    <id>https://blog.lxxyx.cn/2021/08/17/2021/coder/</id>
    <published>2021-08-17T15:22:54.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>淘系技术 - 技术运营约稿</p></blockquote><p>TLDR：对写代码始终充满兴趣，这是我一切的源动力。</p><h5 id="过往-从-HR-专业学生到程序员繁易"><a href="#过往-从-HR-专业学生到程序员繁易" class="headerlink" title="过往 - 从 HR 专业学生到程序员繁易"></a>过往 - 从 HR 专业学生到程序员繁易</h5><p>我是一名半路出家的程序员。高中读的文科，大学选的人力资源管理专业，生活如果按部就班的下去，这个世界或许会多一位叫繁易的 HR 或者猎头，波澜不惊。</p><p>引导我走上程序员之路的故事也很简单，2014 年刚入学，大一课程少，期末时便在网络上自学起了 C 语言，当时没有电脑就在手机上的 C 语言软件敲着简单的 <code>if/else</code>，就这样我入坑了。<br>​</p><p>往后的半年中，编程的学习也并没有像想象中的那样顺利。学习进度停滞不前。我还清楚的记得当时的自己：“会用 20 门语言输出 Hello World!”，“别人一周就能上手的 BootStrap 我学了四个月” 等诸如此类的事情。<br>​</p><p>虽然整个过程中极其令人丧气，<strong>但我始终知道，我对写代码这件事情是抱有极大的兴趣的</strong>。在当时同龄的同学已经纷纷在准备考证、找实习之类的事情，而我依旧乐此不疲。<br>​</p><p>我想，<strong>如果不是因为真的有兴趣，我大概会放弃编程，选择做一些“更适合”自己的工作</strong>。<br>​</p><h5 id="成长-从“年轻工程师”到阿里前端技术专家"><a href="#成长-从“年轻工程师”到阿里前端技术专家" class="headerlink" title="成长 - 从“年轻工程师”到阿里前端技术专家"></a>成长 - 从“年轻工程师”到阿里前端技术专家</h5><p>在学写代码半年后，偶然的一个契机，我找到了愿意带我写代码的同学，并磕磕绊绊的完成了第一次 <code>Ajax</code> 操作。那之后的我仿佛开窍了一般，编程世界的大门，终于向我敞开，任我探索和遨游。<br>​</p><p>直到有一天，我遇到了瓶颈。</p><p>2016 年的前端，技术日新月异，当时的我颇有一些手足无措，总觉得时间不够用。于是在参加当年的 JSConf 时，遇见了当时前端圈的名人贺师俊（Hax），于是便壮着胆上去交流了一番。<br>​</p><p>对话的内容我只记得个大概，在交流结束时我向贺老致谢，他回复我说：“学东西不要有那么强的功利心，而且对于我来说，能帮你们这些年轻的工程师就非常好了。”<br>​</p><p>在那时候我发现，对<strong>编程的兴趣已经托着我从学生过渡到大家眼里“年轻的工程师”了</strong>，我想<strong>我会继续带着兴趣走下去，尽全力去 Coding 就行。</strong><br><strong>​</strong></p><p>时间一晃眼已经过去了 5 年，当年的学生，也从“年轻的工程师”蜕变成了阿里的前端技术专家，<strong>身份在变、环境在变、技术在变，只有对代码的兴趣没有改变</strong>。</p><h5 id="开源-从-Node-js-使用者到-Node-js-Collaborator"><a href="#开源-从-Node-js-使用者到-Node-js-Collaborator" class="headerlink" title="开源 - 从 Node.js 使用者到 Node.js Collaborator"></a>开源 - 从 Node.js 使用者到 Node.js Collaborator</h5><p>由兴趣驱动的源动力，往往是纯粹且不掺杂利益的，而在软件世界中，<strong>兴趣驱动与开源精神是高度契合的</strong>。</p><p>在 2020 年冬季到来的时候，我遇到了新的瓶颈，想学习更多 Node.js 的知识却不知从何下手。<br>​</p><p>这一次我的选择是参与 Node.js 开源社区的协作。我开始阅读 Node.js 的源码，解决 Github 上的 Issue 等。在事后提名 Node.js Core Collaborator 时，我统计了一下，两个月的时间内我一共提出了 50+ 的 Pull Request。<br>​</p><p>现在回头来看，<strong>这些与实际工作“无关的兴趣”，最终支撑着我走的更远</strong>，现在的我在 Node.js 架构团队，全职负责着 Node.js 的工作，个人的眼界也开拓了许多。<br>​</p><h5 id="总结-兴趣与代码"><a href="#总结-兴趣与代码" class="headerlink" title="总结 - 兴趣与代码"></a>总结 - 兴趣与代码</h5><p>在别人眼里，我是枯坐一整天的“怪人”，在我的脑海中，我是“键新世界（阿里 2018 年校招 Slogan）”的程序员。<br>​</p><p>因为是兴趣，所以获得成长时会快乐，而停滞不前时也不会焦虑与抱怨。对代码的兴趣支撑我走到了现在，我想后面也会是一样。<br>​</p><p>这就是我眼中程序员最重要的能力与我的经历。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;淘系技术 - 技术运营约稿&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TLDR：对写代码始终充满兴趣，这是我一切的源动力。&lt;/p&gt;
&lt;h5 id=&quot;过往-从-HR-专业学生到程序员繁易&quot;&gt;&lt;a href=&quot;#过往-从-HR-专业学生到程序员繁易&quot; 
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>Jamstack - 理念、误区、思考</title>
    <link href="https://blog.lxxyx.cn/2021/07/27/2021/jamstack/"/>
    <id>https://blog.lxxyx.cn/2021/07/27/2021/jamstack/</id>
    <published>2021-07-27T18:25:39.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<h2 id=""><a href="#" class="headerlink" title=""></a><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1627198825385-b5f978df-3945-423b-b914-7c9e1c220c72.png#clientId=u369d6ad1-291c-4&amp;from=paste&amp;id=ud0453b99&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=394&amp;originWidth=850&amp;originalType=binary&amp;ratio=1&amp;size=52330&amp;status=done&amp;style=none&amp;taskId=uf7388108-1efc-4f82-a69b-aa155aa7312" alt="image.png"></h2><blockquote><p>如果你之前从未了解过 Jamstack，我推荐先阅读文章：<a href="https://zhuanlan.zhihu.com/p/281085404">《Jamstack，下一代 Web 建站技术栈？》</a>。</p></blockquote><h2 id="Jamstack-是什么"><a href="#Jamstack-是什么" class="headerlink" title="Jamstack 是什么"></a>Jamstack 是什么</h2><p>Jamstack 是一套用于构建现代 Web 站点的技术栈，拥有高性能、安全性、易扩展的特性。<br>​</p><blockquote><p>Jamstack 技术栈 &amp; 生态</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1627199006775-18392a5a-c7fb-43fe-a30c-4a00159a6d3d.png#clientId=u369d6ad1-291c-4&amp;from=paste&amp;id=u9576f13a&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=720&amp;originWidth=1280&amp;originalType=binary&amp;ratio=1&amp;size=673800&amp;status=done&amp;style=none&amp;taskId=ubb2a3f21-f7e8-4bc9-8e86-d6511d70658" alt="image.png"><br>​</p><p>Jamstack 聚合了现代前端开发所需要的脚手架，框架，工作流等，从而最大化的提高工程师的生产力。<br>​</p><blockquote><p>工作流</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1627199078527-85ca7b79-35d5-423f-8f4b-caf05dc052f5.png#clientId=u369d6ad1-291c-4&amp;from=paste&amp;id=u18842002&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1001&amp;originWidth=1351&amp;originalType=binary&amp;ratio=1&amp;size=114465&amp;status=done&amp;style=stroke&amp;taskId=uac593987-5fb1-4afd-8da3-d263d8a3019" alt="image.png"><br>​</p><p>在这里，Jamstack 的核心理念是预渲染、使用 JavaScript 实现动态功能、使用 HTTP Api 连接第三方服务。<br>​</p><h3 id="举个例子"><a href="#举个例子" class="headerlink" title="举个例子"></a>举个例子</h3><p>当你要开发一个博客，在这之前你可能会使用 Wordpress 去搭建你的博客站点，但与此同时，你也需要负责维护 Wordpress 的服务与数据库等。<br>​</p><p>而如果你使用 Jamstack，你可以使用诸如 strapi 的 headless cms 服务(意为只提供 API 而不提供页面渲染)，用来存放你的文章数据，你在前端可以使用类似 Next.js 的框架去构造站点，通过请求 headless cms 的 Api 来渲染页面。<br>而在发布时，你将在构建时生成静态页面，并发布至 CDN。因为是静态页面，所以性能好，而托管至 CDN 意味着该页面是只读的，安全性高，且 CDN 是全球部署的话，那么页面也能实现全球部署，拓展性非常好。</p><h2 id="误区"><a href="#误区" class="headerlink" title="误区"></a>误区</h2><p>Jamstack 在国内落地时，总是会有同学认为这是新瓶装旧酒，或者是前端炒出来的新概念，但实际上忽略了 Jamstack 自身架构的特性与优势。</p><h3 id="Jamstack-是不是新瓶装旧酒"><a href="#Jamstack-是不是新瓶装旧酒" class="headerlink" title="Jamstack 是不是新瓶装旧酒"></a>Jamstack 是不是新瓶装旧酒</h3><p>Q：Jamstack 和之前的手动发布页面到 CDN 有什么区别，是不是新瓶装旧酒？<br>A：这里我可以很明确的给一个答案：不是。</p><p>首先在我看来，Jamstack 虽然表现的都是页面和资源托管至 CDN，是实际上背后的工作流与过往是截然不同的。相较于以往手动开发和发布的模式，Jamstack 是聚合了现代前端框架、工作流、发布平台的，这一点非常的重要。<br>​</p><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1627199129311-16b1d27b-0bdc-4956-89a8-6965301efd60.png#clientId=u369d6ad1-291c-4&amp;from=paste&amp;id=uad6b83b4&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=577&amp;originWidth=836&amp;originalType=binary&amp;ratio=1&amp;size=264100&amp;status=done&amp;style=none&amp;taskId=uf9723511-de07-4d5e-9b35-96cbc1c3d1b" alt="image.png"><br>​</p><p>对于工程师而言，效率就是生产力，之前的页面不一定是基于现代工程的，每次生成静态页面也依赖于自己写的脚本，每次页面更新还需要手动发布，效率偏低。而使用 Jamstack，你可以利用你最熟悉的框架，通过框架去批量生成静态页面，并且聚合 Git 等服务，实现推送即发布的全自动工作流。效率会高很多。<br>​</p><p>举个相似的例子：使用 React/Vue/Angular 和使用 jQuery 写页面，最终都是操作 DOM，所以 React/Vue/Angular 和 jQuery 是一样的，新瓶装旧酒？<br>​</p><p>先进的工具带来先进的生产力，即使最终的表现是前端页面和十年前一样，部署至 CDN。但实际上生产效率与质量还有可生产的页面已经发生了很大的进步。<br>​</p><p>不论是什么前端页面，能打开的更快终究是更好的，因此如果只关注到部署的结果，而没有注意到生产流程与生产效率的变革，便会落入“Jamstack 是新瓶装旧酒”这个论点的坑中。顺带一提，近几年的前端工具基本上也是朝着“性能更好”、“更易用”等方向去前进的。<br>​</p><h3 id="Jamstack-能适应所有的场景"><a href="#Jamstack-能适应所有的场景" class="headerlink" title="Jamstack 能适应所有的场景"></a>Jamstack 能适应所有的场景</h3><p>Q：那是不是什么网站都上 Jamstack 就对了？<br>A：明确的回复，不对，Jamstack 的优势非常明显，所以劣势也很明显。<br>​</p><p>如果只关注到 Jamstack 的性能优势，希望将 Jamstack 用于所有场景，其实是不正确的。Jamstack 的站点为了获得性能、安全、可拓展性的优势，需要将页面托管至类似 CDN 的服务中，这个过程中，一个页面需要经过以下两步才会真正的发布到线上。<br>​</p><ol><li>预渲染<ol><li>需要提前渲染出最终的页面</li></ol></li><li>发布<ol><li>托管服务刷新缓存后展示新页面</li></ol></li></ol><p>​</p><p>针对预渲染，由于往往需要在构建时或者运行时实现功能，那么会存在一定的限制。构建时生成无法实现千人千面，运行时生成则需要考虑生成的数量与成本的考虑。假定将每一条微博都生成一个静态页面，诚然性能是好了，但所带来的成本也是不可估量了，且许多微博往往访问的人极少，那么运行时生成的性能可能还不如之前，<br>​</p><p>在 Jamstack 架构下，CDN 是最常见的托管服务，但 CDN 为了保证性能也存在着缓存的机制，这意味着页面的实时性无法保障。现有的 Jamstack 框架也会添加定时生成的功能，比如每 10S 就刷新一个页面并推送至 CDN，但不论怎么做，在实时性上还是不如实时服务的。</p><blockquote><p>Jamstack 最适合一些<strong>内容更新不太频繁的网站</strong>（比如新闻、电商、文档）。它不适合 Feeds 流、聊天室、论坛、个性化推荐这样高度动态化的网站，以及邮箱、编辑器这样偏重型的 Web 应用。</p></blockquote><h2 id="Jamstack-会是企业级框架的核心特性而非唯一，混合渲染是未来方向"><a href="#Jamstack-会是企业级框架的核心特性而非唯一，混合渲染是未来方向" class="headerlink" title="Jamstack 会是企业级框架的核心特性而非唯一，混合渲染是未来方向"></a>Jamstack 会是企业级框架的核心特性而非唯一，混合渲染是未来方向</h2><p>这个观点是我个人的想法。<br>​</p><p>在我看来，之所以 Jamstack 在国内难以落地，除去老生常谈的工作流、部署平台、底层依赖的限制外，其实还存在着适用范围单一的问题。<br>​</p><p>这里我抛出我的观点：在国内的市场下，Jamstack 将会成为企业级框架的核心特性，但并非唯一的特性，混合渲染才是未来的方向。<br>​</p><p>诚然，Jamstack 的优势非常明显，用过的同学都说省事都说好，但我在前文也提到了 Jamstack 的劣势，这决定了在企业内部错综复杂的场景中，Jamstack 不是那么万能的。<br>​</p><p>此外，Jamstack 作为一种现代 Web 站点的开发理念，其是易于被框架实现的。这也意味着，在企业级的场景中，往往会是框架去实现 Jamstack 特性，这个过程是新增而不是替换。因此 Jamstack 会是企业级框架的核心特性而非唯一特性。<br>​</p><p>至于后一句提到的混合渲染，Idea 实际上是源于 Next.js 10。在 Next.js 中，框架配合 Vercel 云服务平台，实现了纯静态页面托管、增量生成、服务端渲染等多种渲染策略的聚合。从这个角度来看，Next.js 相较其他的 Jamstack 框架是更有优势的。<br>​</p><blockquote><p>静态生成</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1627199329426-c07335b1-92bb-4fa8-bcd5-0f13a05be9ce.png#clientId=udc9f227a-db2b-4&amp;from=paste&amp;id=ucbee6077&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=472&amp;originWidth=880&amp;originalType=binary&amp;ratio=1&amp;size=40914&amp;status=done&amp;style=stroke&amp;taskId=u85897e04-3265-4dd8-8d65-970e596f5ce" alt="image.png"></p><blockquote><p>增量生成策略</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1627199295157-370f3146-df20-4c85-ba03-a9c62725b7b8.png#clientId=udc9f227a-db2b-4&amp;from=paste&amp;height=450&amp;id=u7d8a9c56&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=900&amp;originWidth=1600&amp;originalType=binary&amp;ratio=1&amp;size=84712&amp;status=done&amp;style=none&amp;taskId=udc3cfca0-4bd3-4709-a6be-7a27718c2a9&amp;width=800" alt="image.png"><br>​</p><p>先进的框架 + 先进的工作流 + 更多渲染场景的适配，我认为这才会是企业级框架进步的方向。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Jamstack 是一套优秀的现代 Web 站点开发技术栈，在现代前端工程的加持下解决了开发效率与性能的难题。但由于其劣势也非常明显，所以在企业级框架中，Jamstack 会是一种核心的特性，但不是唯一的特性，企业级是需要支持类似混合渲染的开发模式的。<br>​</p><p>BTW，本文没有提及到使用 JavaScript 与连接第三方服务这两个特性，理由是：构建现代站点，完全脱离 JavaScript 不现实，故略过；预渲染页面时，往往就包含使用第三方服务（当然我觉得这一点的商业价值实际更大），故也略过。<br>​</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://zhuanlan.zhihu.com/p/281085404">Jamstack，下一代 Web 建站技术栈？</a></li><li><a href="https://jamstack.org/">https://jamstack.org/</a></li><li><a href="https://nextjs.org/docs/basic-features/data-fetching">Next.js - Data Fetching</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;&quot;&gt;&lt;a href=&quot;#&quot; class=&quot;headerlink&quot; title=&quot;&quot;&gt;&lt;/a&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2021/png/98602/1627198825385-b5f978df-3945-423
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>net 模块，参数传错就进程崩溃</title>
    <link href="https://blog.lxxyx.cn/2021/01/17/2021/net/"/>
    <id>https://blog.lxxyx.cn/2021/01/17/2021/net/</id>
    <published>2021-01-17T13:19:57.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p>近期给 Node.js 提了许多 PR。这当中有一些 BUG 我觉得还挺有意思的，所以开个专栏专门讲述 BUG 的定位、修复、提 PR 的过程。</p><p>话不多说，先来看第一个 BUG：使用 <code>net</code> 模块的 <code>BlockList</code> 类时，如果调用 <a href="https://nodejs.org/dist/latest-v15.x/docs/api/net.html#net_blocklist_addsubnet_net_prefix_type"><code>addSubnet</code></a> 的第 2 个参数为 NaN，进程就会 Crash 退出。</p><p>Issue 由我发现和修复，目前已合并至 Node.js 主分支中，并在 15.6.0 版本中分发。</p><p>Github 上提交的 Issue 和 PR：</p><ul><li>Issue: <a href="https://github.com/nodejs/node/issues/36731">net: blockList.addSubnet throw Assertion `args[2]-&gt;IsInt32()’ failed when prefix is NaN</a></li><li>Pull Request: <a href="https://github.com/nodejs/node/pull/36732">net: throw ERR_OUT_OF_RANGE if blockList.addSubnet prefix is NaN </a></li></ul><h2 id="复现"><a href="#复现" class="headerlink" title="复现"></a>复现</h2><p><strong>Node 版本</strong>: 15.4.0</p><p>问题代码如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> net = <span class="built_in">require</span>(<span class="string">&#x27;net&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> blockList = <span class="keyword">new</span> net.<span class="title class_">BlockList</span>();</span><br><span class="line">blockList.<span class="title function_">addSubnet</span>(<span class="string">&#x27;&#x27;</span>, <span class="title class_">NaN</span>);</span><br></pre></td></tr></table></figure><p>报错信息：</p><figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&gt; blockList.addSubnet(&#x27;&#x27;, NaN)</span><br><span class="line"><span class="keyword">node</span><span class="title">[2155</span>]: ../src/node_sockaddr.cc:<span class="number">614</span>:static void <span class="keyword">node</span><span class="title">::SocketAddressBlockListWrap</span>::AddSubnet(const FunctionCallback<span class="literal">Inf</span>o<span class="tag">&lt;v8::Value&gt;</span> &amp;): Assertion `args[<span class="number">2</span>]-&gt;IsInt32()&#x27; failed.</span><br><span class="line"> <span class="number">1</span>: <span class="number">0</span>x101379d05 <span class="keyword">node</span><span class="title">::Abort</span>() (.cold.<span class="number">1</span>) [/usr/local/bin/<span class="keyword">node</span><span class="title">]</span></span><br><span class="line"><span class="title"> 2</span>: <span class="number">0</span>x1000bde89 <span class="keyword">node</span><span class="title">::Abort</span>() [/usr/local/bin/<span class="keyword">node</span><span class="title">]</span></span><br><span class="line"><span class="title"> ....</span></span><br><span class="line"><span class="title">[1</span>]    <span class="number">2155</span> abort      <span class="keyword">node</span><span class="title"></span></span><br></pre></td></tr></table></figure><p>而这种在 C++ 抛出的异常，是你用 Try/Catch 也捕获不了的，如果你传入了错误的参数，那么进程只能 Crash。</p><h2 id="定位"><a href="#定位" class="headerlink" title="定位"></a>定位</h2><p>在 <code>net</code> 模块中，<code>BlockList</code> 负责网络黑名单的工作，用于屏蔽某些 IP 地址。本次出问题就是其中的 <code>addSubnet</code> 方法。</p><p>在 Node.js 的源码中，所有 JS 提供的功能都在 <code>lib</code> 目录下，简单的搜索 BlockList 关键词，我定位到了本次发生问题的代码文件及方法：<a href="https://github.com/nodejs/node/blob/37a8179673590af10b9e8e413388adffc21ba713/lib/internal/blocklist.js#L81-L105">https://github.com/nodejs/node/blob/37a8179673590af10b9e8e413388adffc21ba713/lib/internal/blocklist.js#L81-L105</a>。<br><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1610856720170-f6734e94-26c2-463c-8ef1-ec914e05b8ef.png#align=left&amp;display=inline&amp;height=317&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=634&amp;originWidth=712&amp;size=56652&amp;status=done&amp;style=none&amp;width=356" alt="image.png"></p><p>在 <code>addSubnet</code> 方法中，会对传入 <code>prefix</code> 参数做校验，而这个校验实际上是存在漏洞的。</p><p>在使用 <code>typeof</code> 来校验 number 时，往往会遗漏 NaN 的校验，因为 <code>typeof NaN === &#39;number&#39;</code> 。此次的问题也是如此。</p><p>在最终调用 <code>this[kHandle].addSubnet(network, type, prefix);</code> 时我们传入了 NaN，而 <code>this[kHandle]</code> 是 C++ 的 binding。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; <span class="title class_">BlockList</span>: <span class="title class_">BlockListHandle</span> &#125; = <span class="title function_">internalBinding</span>(<span class="string">&#x27;block_list&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">BlockList</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">handle = <span class="keyword">new</span> BlockListHandle()</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>[kHandle] = handle;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 C++ 代码中，则对传入的参数又做了一次校验。</p><p><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1610858744812-b584dc03-1ed4-4e5d-a21a-fe0b2675fd8e.png#align=left&amp;display=inline&amp;height=158&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=316&amp;originWidth=746&amp;size=48167&amp;status=done&amp;style=none&amp;width=373" alt="image.png"></p><p>而 <code>NaN</code> 不是 <code>Int32</code>，因此将导致进程异常退出（exit code: 134，代表中断）。</p><h2 id="修复"><a href="#修复" class="headerlink" title="修复"></a>修复</h2><p>了解问题后，修复并不困难，只要在 JavaScript 侧加入对 NaN 的校验即可。</p><p>而在 Node.js 的内部提供了 <code>validateInt32</code> 方法，用于校验数字。我们只需要将原来的 <code>if (typeof prefix !== &#39;number&#39;)</code> 替换为 <code>validateInt32</code> 即可。</p><p>下面是完整的 Diff。<br><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1610859134001-5ed46955-3f85-45cf-bd9d-1ef544f57a59.png#align=left&amp;display=inline&amp;height=590&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=1179&amp;originWidth=2264&amp;size=226887&amp;status=done&amp;style=none&amp;width=1132" alt="image.png"></p><h2 id="单测"><a href="#单测" class="headerlink" title="单测"></a>单测</h2><p>在 Node.js 源码中，任何变更都需要配上相应的单元测试，此次也不例外。</p><p>我在 <a href="https://github.com/nodejs/node/blob/f658bd1e8f7cd0a328c10c8d2a4eea6cf113122b/test/parallel/test-blocklist.js#L153">test/parallel/test-blocklist.js</a> 加入了单元测试，本地测试通过后便提交了 Pull Request。</p><p>单元测试的代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">assert.<span class="title function_">throws</span>(<span class="function">() =&gt;</span> blockList.<span class="title function_">addSubnet</span>(<span class="string">&#x27;&#x27;</span>, <span class="title class_">NaN</span>), <span class="regexp">/ERR_OUT_OF_RANGE/</span>);</span><br></pre></td></tr></table></figure><h2 id="提交-amp-合入主分支"><a href="#提交-amp-合入主分支" class="headerlink" title="提交 &amp; 合入主分支"></a>提交 &amp; 合入主分支</h2><p>相关的 Pull Request: <a href="https://github.com/nodejs/node/pull/36732">net: throw ERR_OUT_OF_RANGE if blockList.addSubnet prefix is NaN </a></p><p>因为改动不大，且 BUG 确实存在，因此这个 PR 没有争议的迅速通过了。<br><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1610859603215-e13e9a0e-12f5-4b62-b157-02f10b85abe7.png#align=left&amp;display=inline&amp;height=274&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=548&amp;originWidth=1631&amp;size=89721&amp;status=done&amp;style=none&amp;width=815.5" alt="image.png"><br>提出 PR 大概一周的时间，代码就已经被合入 Node.js 主分支，并在 Node.js 15.6.0 中分发到全世界了（顺带一提，在 15.6.0 版本中，我有 12 个 commit 被合入当前版本）。<br><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1610859737838-4ef12a0c-f55e-4b70-a765-e80e1a6a3448.png#align=left&amp;display=inline&amp;height=244&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=487&amp;originWidth=774&amp;size=48168&amp;status=done&amp;style=none&amp;width=387" alt="image.png"><br><img src="https://cdn.nlark.com/yuque/0/2021/png/98602/1610859806964-428735a6-df73-43fa-8327-32249f4765e3.png#align=left&amp;display=inline&amp;height=55&amp;margin=%5Bobject%20Object%5D&amp;name=image.png&amp;originHeight=109&amp;originWidth=983&amp;size=18946&amp;status=done&amp;style=none&amp;width=491.5" alt="image.png"></p><h2 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h2><p>全文看下来，你会发现往 Node.js 提交 Pull Request 并不困难，且修复 BUG 的过程也是熟悉源码的过程。感兴趣的话不妨阅读 Node.js 的文档：<a href="https://github.com/nodejs/node/blob/master/CONTRIBUTING.md">《Contributing to Node.js》</a> 与 Starkwang 的文章：<a href="https://zhuanlan.zhihu.com/p/27932211">《为 Node.js 贡献你的力量》</a>。</p><p>后续我也会持续更新这个专栏，将我解决过的 Issue 写成文字，供大家学习和参考，有兴趣的同学可以点个关注~</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;近期给 Node.js 提了许多 PR。这当中有一些 BUG 我觉得还挺有意思的，所以开个专栏专门讲述 BUG 的定位、修复、提 PR 的过程。&lt;/p&gt;
&lt;p&gt;话不多说，先来看第一个 BUG：使用 &lt;code&gt;net&lt;/code&gt; 模块的 &lt;code&gt;BlockList&lt;/
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>Midway Serverless - 新一代云端一体研发框架</title>
    <link href="https://blog.lxxyx.cn/2020/09/07/2020/hooks/"/>
    <id>https://blog.lxxyx.cn/2020/09/07/2020/hooks/</id>
    <published>2020-09-07T17:52:03.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p>这是 Midway Serverless 体系在 8 月底位于 <strong>阿里云云原生微服务大会 </strong>对外分享的内容文字版，介绍了 Midway Serverless 全新的类 React Hooks 风格的 Serverless 开发方案，欢迎阅读和转发。<br /><br><br />大家好，我是来自淘系技术部 - 前端架构团队的繁易，今天我要给大家来分享是《Midway Serverless，新一代云端一体研发框架》。向大家分享我们是在如何在开发过程中，打破云和端的那堵墙，从而去享受真正的云端一体应用研发。<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1gSdRjLzO3e4jSZFxXXaP_FXa-1920-1080.png" alt="image.png"><br /></p><p><a name="opePr"></a></p><h2 id="自我介绍"><a href="#自我介绍" class="headerlink" title="自我介绍"></a>自我介绍</h2><p>首先是个人介绍，我的花名叫繁易,目前就职于阿里巴巴-淘系技术部-前端架构团队，主要负责淘系前台业务的 Serverless 落地和前端提效战役，目前也在负责 Midway Serverless 框架云端一体场景的架构设计和落地工作。<br /><img src="https://img.alicdn.com/tfs/TB1do91T1L2gK0jSZPhXXahvXXa-1920-1080.png" alt="image.png"><br /></p><p><a name="c3Sjg"></a></p><h2 id="大纲"><a href="#大纲" class="headerlink" title="大纲"></a>大纲</h2><p>本次分享我将会从以下 4 部分出发：<br /></p><ul><li>阿里巴巴 Node.js Serverless 建设现状</li><li>Midway Serverless 介绍</li><li>新一代云端一体研发方案</li><li>未来规划</li></ul><p><br /><img src="https://img.alicdn.com/tfs/TB1hmr8knM11u4jSZPxXXahcXXa-1920-1080.png" alt="image.png"><br /></p><p><a name="eb87fd55"></a></p><h2 id="阿里巴巴-Node-js-Serverless-建设现状"><a href="#阿里巴巴-Node-js-Serverless-建设现状" class="headerlink" title="阿里巴巴 Node.js Serverless 建设现状"></a>阿里巴巴 Node.js Serverless 建设现状</h2><p><img src="https://img.alicdn.com/tfs/TB1PfCYT7L0gK0jSZFtXXXQCXXa-1920-1080.png" alt="image.png"><br /><br><br />在过去的 2019 年中，阿里经济体前端委员会提出了四大技术方向。分别是：</p><ul><li>搭建服务</li><li>Serverless</li><li>智能化</li><li>Web IDE</li></ul><p><br /><img src="https://img.alicdn.com/tfs/TB1SnvzkGNj0u4jSZFyXXXgMVXa-1920-1080.png" alt="image.png"><br /></p><p><a name="vsHGk"></a></p><h3 id="Serverless-与前端"><a href="#Serverless-与前端" class="headerlink" title="Serverless 与前端"></a>Serverless 与前端</h3><p>Serverless 首次作为一个技术方向，被列为前端委员会的主要攻关方向。这背后也是有一定原因的。<br /><br><br />首先是在阿里巴巴集团，我们拥有 1600+ Node.js 应用，但应用常年的 CPU 利用率非常低，大部分 CPU 利用率 &lt; 10%，甚至有 &lt; 5% 的应用。<br /><br><br />从技术视角来看，Node.js 的维护实际上是有一定难度的，在开发的过程中，我们会遇到非常多前端之前所没有处理过的问题。例如 Docker、限流、日志、跨语言调用等。而这一系列的难题则会带来非常高的成本。<br />恰巧的是，这些传统应用开发所会面对的一些问题，正好是我们函数开发中的优势。因为函数可以提高服务器的利用率，按量付费，也可以减少非常多不必要的运维成本。<br /><br><br />而从业务视角来看，前端也提出了一些业务上的诉求。阿里巴巴是几年前开始了中台战略，并希望做到大中台小前台的目标，因此我们前端同学，也希望借中台服务来快速组合为各类业务接口，从而达到快速交付的目标。更快的完成业务需求和降低业务试错成本。<br />因此我们需要 Serverless 来赋能前端，让云原生给前端降本增效。<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1s4uRg7cx_u4jSZFlXXXnUFXa-1920-1080.png" alt="image.png"><br><a name="PXGBu"></a></p><h3 id="Serverless-落地成果"><a href="#Serverless-落地成果" class="headerlink" title="Serverless 落地成果"></a>Serverless 落地成果</h3><p>经过大约一年的实践之后，我们也取得了一定的一些成果。<br /><br><br />首先，我们在多个 BU 实现了 Serverless 的落地 ，平稳度过了双促。这些 BU 大家可能也都很熟悉，比如：淘系、新零售、飞猪、ICBU、天猫精灵等。<br /><br><br />而在业务增效降本上，Serverless 也有亮眼的表现。<br /><br><br />在机效方面，我们传统业务机器成本降低 ~<strong>30%</strong>，而中后台业务机器成本更是降低 ~<strong>87%</strong>，Serverless 帮我们节约了成本的同时，也一定程度上解决了服务器利用率低下的问题。<br />在人效方面，使用函数整体人效提升 ~<strong>48%。</strong>这部分是我们根据用户使用工具埋点 + 调用时长 + 代码量测算得出。<br /><br><br />最后则是 Serverless 整体架构经过了双促的验证，零故障。有力的证明了 Serverless 架构的稳定性。<img src="https://img.alicdn.com/tfs/TB1R_oqhMgP7K4jSZFqXXamhVXa-1920-1080.png" alt="image.png"><br><a name="dvmbQ"></a></p><h2 id="Midway-Serverless-体系介绍"><a href="#Midway-Serverless-体系介绍" class="headerlink" title="Midway Serverless 体系介绍"></a><strong>Midway Serverless 体系介绍</strong></h2><p><img src="https://img.alicdn.com/tfs/TB1Ku2qlz39YK4jSZPcXXXrUFXa-1920-1080.png" alt="image.png"><br />我们在整个 Node FaaS 落地的过程中，都是基于我们 Midway Serverless 框架去开发的。所以在讲述云端一体前，我还会向大家介绍一下整个 Midway Serverless 的体系。<br /></p><p><a name="rRqhR"></a></p><h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>下面我们以请求调用为例，大家可以看到请求处理的过程中，云开发平台的 Runtime 会加载 index.js，而 index.js 又会执行我们的 Midway Serverless Framework，最后再由框架去执行用户代码，返回结果。<br /><img src="https://img.alicdn.com/tfs/TB1TQ2aT7T2gK0jSZFkXXcIQFXa-1920-1080.png" alt="image.png"><br><a name="kAYM2"></a></p><h3 id="体系"><a href="#体系" class="headerlink" title="体系"></a>体系</h3><p>而在整个 Midway Serverless 的体系里面。我们一共有以下三个部分：</p><ul><li>工具链</li><li>框架</li><li>标准化</li></ul><p><br />首先是工具链，我们在本地工具链这边做了非常多的事情。包括本地触发器的模拟、调试、一体化、多平台的发布等。</p><p>第二部分则是框架，包括数据模拟、环境配置、请求适配、组件拓展、IoC 等。这部分都是希望开发者可以更方便的去开发应用<br /><br><br />最后一个则是标准化。目前云厂商在 Serverless 标准层面是割裂的，而我们则希望一定程度上的去解决这个问题，比如我们推出的 yml 标准、前端调用标准，运行时标准、一体化标准。这一切都是希望大家在 Serverless 时代能更好的去利用 Serverless 的红利来加速开发。</p><p><img src="https://img.alicdn.com/tfs/TB1M66Yg5pE_u4jSZKbXXbCUVXa-1920-1080.png" alt="image.png"><br /></p><p><a name="DSWV0"></a></p><h2 id="新一代云端一体研发方案"><a href="#新一代云端一体研发方案" class="headerlink" title="新一代云端一体研发方案"></a><strong>新一代云端一体研发方案</strong></h2><p><strong><img src="https://img.alicdn.com/tfs/TB1.E2zkGNj0u4jSZFyXXXgMVXa-1920-1080.png" alt="image.png"></strong><br />这部分我们将会向大家介绍新一代一体化云端一体研发方案，至于为什么是新一代，这儿先卖个悬念，后面会提及。</p><p><a name="PzYMl"></a></p><h3 id="我们遇到的问题"><a href="#我们遇到的问题" class="headerlink" title="我们遇到的问题"></a>我们遇到的问题</h3><p>在阿里巴巴实践 Serverless 并不是一帆风顺的，我们也遇到了许多难题。<br /><br><br />首先是上手难度高的难题，因为 Serverless 虽说把运维的成本降低了，但是你在开发一个应用时，需要去接触后端，相比于传统的前端开发来说，难度是升高的。</p><p>其次是前后端割裂的问题，我们现阶段主要在负责纯函数的开发。从代码层面来看，就是前端是一个仓库，后端是另外一个仓库，前后端分开开发与发布。虽然都是一个人在做，但依然会有比较强的一个割裂感。<br /><br><br />最后则是研发成本高问题，上面提到前后端割裂的问题，当你前端和后端在不同的仓库时，意味着你需要分开发布，并且多次联调，这部分带来的研发成本是偏高的。</p><p>带着这些问题，我们经过了大约一年的思考与探索，推出了我们自己的解决方案：云端一体应用开发模式。</p><p><img src="https://img.alicdn.com/tfs/TB1xD6jTVP7gK0jSZFjXXc5aXXa-1920-1080.png" alt="image.png"><br><a name="jAh0G"></a></p><h3 id="云端一体在社区的定义"><a href="#云端一体在社区的定义" class="headerlink" title="云端一体在社区的定义"></a>云端一体在社区的定义</h3><p>云端一体这个概念并不是一个新词，近期在社区也很常见。而在社区的视角上，关于云端一体主要有以下两种定义。</p><p><a name="JjGDz"></a></p><h4 id="架构视角"><a href="#架构视角" class="headerlink" title="架构视角"></a>架构视角</h4><p>第一种是架构视角出发的。他们所认定的云端一体，是云服务和端侧技术能够结合在一起使用。比如说前端使用 BaaS 服务进行开发，他们把这种开发模式就叫云端一体。</p><p><a name="HK9Ru"></a></p><h4 id="代码视角"><a href="#代码视角" class="headerlink" title="代码视角"></a>代码视角</h4><p>另外一种是从代码视角出发，当前后端同仓库时，我们通过 monorepo 等工具进行开发，也被称之为云端一体。<br />像下图中展示的就是一种比较经典的研发模式。Server 端与 Web 端分别是一个文件夹，双方管理自己的依赖，一同开发与一同发布，这种也称之为云端一体。</p><p>但是我们在去年调研方案时，我们觉得这两种方向，只是机械的将资源组合在一起，并没有发挥出 JavaScript 统一前后端的优势。</p><p><img src="https://img.alicdn.com/tfs/TB1GUP8knM11u4jSZPxXXahcXXa-1920-1080.png" alt="image.png"></p><p><a name="wcL0z"></a></p><h3 id="Midway-Serverless-的云端一体方案"><a href="#Midway-Serverless-的云端一体方案" class="headerlink" title="Midway Serverless 的云端一体方案"></a><strong>Midway Serverless 的云端一体方案</strong></h3><p>对此，我们也提出了自己的云端一体解决方案。新方案的定义为：云端协同开发、无缝融合。</p><p>然后在下图有我们方案的一个目录结构。在新方案中，前后端是在同一个仓库里的，而且前后端只需要管理一份依赖。这么做有以下几个好处：<br /></p><ul><li>易于开发：前后端在一个仓库里，只需要管理一份依赖。可以降低开发时的心智负担</li><li>易于维护：前后端都在一起的时候，功能是一同开发一同管理的，可以提升项目的可维护性</li><li>易于部署：前后端都是一个仓库，在一次发布中，我们可以同时发布前后端，降低部署的成本</li></ul><p><img src="https://img.alicdn.com/tfs/TB1emjcT4v1gK0jSZFFXXb0sXXa-1920-1080.png" alt="image.png"></p><p><a name="TcyTz"></a></p><h2 id="Midway-Serverless-云端一体特性"><a href="#Midway-Serverless-云端一体特性" class="headerlink" title="Midway Serverless 云端一体特性"></a>Midway Serverless 云端一体特性</h2><p>我们这次推出的云端一体方案，主要有以下 4 个特性：</p><ul><li>函数式研发（<em>Functional R&amp;D Solutions</em>）：函数即接口。我们通过函数来统一前后端的体验，减少不必要的样板代码，加速应用研发</li><li>一体化调用（<em>Api not required</em>）：从此不再手动调用 Api。在新框架中，你可以直接从服务端导入函数并调用，就像调用普通函数一样简单</li><li>Hooks（<em>Using Node.js like React Hooks</em>）：通过 Hooks 开发 Node.js 应用。是全新的体验，也是你熟悉的语法。更为函数式开发带去了完善的能力支持</li><li>渐进式开发（<em>Progressive development</em>）：简单和复杂场景通吃。通过对于 IoC 的支持，我们可以复用阿里 Node.js 复杂应用最佳实践，支撑企业级应用开发</li></ul><p><strong><br /></strong><img src="https://img.alicdn.com/tfs/TB1oq5Zh6MZ7e4jSZFOXXX7epXa-1920-1080.png" alt="image.png">**<br /></p><p><a name="iRrxS"></a></p><h3 id="函数式研发：Functional-R-amp-D-Solutions"><a href="#函数式研发：Functional-R-amp-D-Solutions" class="headerlink" title="函数式研发：Functional R&amp;D Solutions"></a>函数式研发：Functional R&amp;D Solutions</h3><p><a name="54leS"></a></p><h4 id="当前云厂商接口开发模式"><a href="#当前云厂商接口开发模式" class="headerlink" title="当前云厂商接口开发模式"></a>当前云厂商接口开发模式</h4><p>在当前的云厂商提供的 Serverless 服务中，不同云厂商的接口开发方式是不一样的。</p><p>比如说阿里云，腾讯云、AWS 这三个云平台，他们都有着不同的入参。而不同的入参意味着需要开发者去自行了解各个平台间的差异，学习成本高。</p><p>我们在经过思考之后，觉得这种开发模式所带来的成本太高了，那么有没有一种更简单的方式呢？<br /><img src="https://img.alicdn.com/tfs/TB1CxkvgmslXu8jSZFuXXXg7FXa-1920-1080.png" alt="image.png"><br /></p><p><a name="ap0mW"></a></p><h4 id="用最简洁的方式开发接口"><a href="#用最简洁的方式开发接口" class="headerlink" title="用最简洁的方式开发接口"></a>用最简洁的方式开发接口</h4><p>答案是有。<br /><br><br />我们发现，其实使用原生的 JavaScript 函数，就可以实现我们的目标。以下图为例，左边的是 Get 接口的开发方式，右边的是 Post 接口的开发方式。</p><p>当函数没有参数时，则接口的 HTTP Method 为 Get，而当接口需要传参时，它就是一个 Post 接口，整个开发方式是自然且完全对齐 JavaScript 函数开发体验的。</p><p>像写 JavaScript 函数一样写接口。</p><p><img src="https://img.alicdn.com/tfs/TB1pODpkBBh1e4jSZFhXXcC9VXa-1920-1080.png" alt="image.png"><br><a name="VbkoK"></a></p><h4 id="函数元信息"><a href="#函数元信息" class="headerlink" title="函数元信息"></a>函数元信息</h4><p>使用 JavaScript 函数，是可以描述函数的信息的。<br /><br><br />JavaScript 函数与接口元信息的映射关系如下：</p><ul><li>路径：文件名 + 函数名</li><li>HTTP Method：函数是否有参数</li><li>HTTP Request Body：函数参数</li></ul><p><br />通过这种方式，我们使得函数元信息能转换为接口的信息，来进行 HTTP 服务的开发。<br /><img src="https://img.alicdn.com/tfs/TB1HGXSjLzO3e4jSZFxXXaP_FXa-1920-1080.png" alt="image.png"></p><p><a name="F7koC"></a></p><h3 id="一体化调用：Api-not-required"><a href="#一体化调用：Api-not-required" class="headerlink" title="一体化调用：Api not required"></a>一体化调用：Api not required</h3><p>我们通过函数自身的信息，来生成接口的信息，这中间实际上包含了一个转换的过程。如果要让开发者手动拼接路径，调用函数，那无疑是一种倒退。<br /><br><br />接下来展示的一体化调用则会向大家展示我们是如何解决这个问题，充分发挥函数的优势。<br /></p><p><a name="7WWxC"></a></p><h4 id="重塑接口调用体验"><a href="#重塑接口调用体验" class="headerlink" title="重塑接口调用体验"></a>重塑接口调用体验</h4><p>在过往的开发中，我们要完成接口的请求，总是需要手动去拼接参数并且调用，整个过程显得繁琐不堪。<br />而在云端一体的开发场景下，我们希望重塑整个接口的调用体验，真正做到云端融合，忘记 Api 和 Ajax 调用。<br /></p><p><a name="S6bX6"></a></p><h4 id="一体化调用：最简单的接口请求方式"><a href="#一体化调用：最简单的接口请求方式" class="headerlink" title="一体化调用：最简单的接口请求方式"></a>一体化调用：最简单的接口请求方式</h4><p>我们选择使用函数开发接口，是为了实现一体化调用。</p><p>所谓一体化调用，就是在接口的调用过程中，我们不再手动调用 Ajax 与 Api，而是直接通过 JavaScript import 的方式，导入函数并调用。<br /><br><br />调用接口，就和调用普通函数一样简单。<br /></p><blockquote><p>Get 接口调用</p></blockquote><p><img src="https://img.alicdn.com/tfs/TB1DXnjT1L2gK0jSZFmXXc7iXXa-1920-1080.png" alt="image.png"></p><blockquote><p>Post 接口调用</p></blockquote><p><img src="https://img.alicdn.com/tfs/TB1HelVicVl614jSZKPXXaGjpXa-1920-1080.png" alt="image.png"><br />和普通函数一样写接口，也和普通函数一样调接口，从此忘记 Ajax 和 Api 调用。<br /></p><blockquote><p>前后端调用示例</p></blockquote><p><img src="https://img.alicdn.com/tfs/TB1FNy9TWL7gK0jSZFBXXXZZpXa-1920-1080.png" alt="image.png"><br><a name="4qbQ2"></a></p><h4 id="实现原理"><a href="#实现原理" class="headerlink" title="实现原理"></a>实现原理</h4><p>一体化调用的实现原理并不复杂，我们基于 Webpack 和 Babel 开发了编译插件，将前端对于函数的引用转换成了 HTTP 的请求。<br /><br><br />具体的可以看下图：<br /><img src="https://img.alicdn.com/tfs/TB1Foq_TYr1gK0jSZFDXXb9yVXa-1920-1080.png" alt="image.png"><br /></p><p><a name="Eyscw"></a></p><h3 id="Hooks：Using-Node-js-like-React-Hooks"><a href="#Hooks：Using-Node-js-like-React-Hooks" class="headerlink" title="Hooks：Using Node.js like React Hooks"></a>Hooks：Using Node.js like React Hooks</h3><p><a name="smHj8"></a></p><h4 id="为什么会有-Hooks"><a href="#为什么会有-Hooks" class="headerlink" title="为什么会有 Hooks"></a>为什么会有 Hooks</h4><p>在传统的 Web 应用开发中，我们需要的不仅仅是函数参数，还包括非常多的请求上下文信息，例如请求的 Header、Method 等。</p><p>但在一体化调用中，由于接口也是通过 JavaScript 函数开发，因此无法获取到请求上下文的信息，也不能通过参数手动传入（因为会损害调用体验，前端也无法传入上下文参数）</p><p><img src="https://img.alicdn.com/tfs/TB1yP9YT7L0gK0jSZFtXXXQCXXa-1920-1080.png" alt="image.png"></p><p><a name="i2SAm"></a></p><h4 id="通过-Hooks-获取请求上下文"><a href="#通过-Hooks-获取请求上下文" class="headerlink" title="通过 Hooks 获取请求上下文"></a>通过 Hooks 获取请求上下文</h4><p>我们在借鉴和吸取了 React Hooks 的经验之后，决定通过 Hooks 去解决获取请求上下文的难题。</p><p>整个 Api 非常简单：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> ctx = <span class="title function_">useContext</span>();</span><br></pre></td></tr></table></figure><p><img src="https://img.alicdn.com/tfs/TB1d3Ypf0Tfau8jSZFwXXX1mVXa-1920-1080.png" alt="image.png"><br />通过 useContext 这个 Api 与 Hook 的开发方式，带来了以下三个好处：</p><ul><li>解决了在函数中获取请求上下文的难题</li><li>无需手动传入参数，不损害调用体验</li><li>遵循 React Hooks Style 的开发方式，云端一体融合不仅仅是项目目录结构与接口调用的融合，更是开发心智的融合</li></ul><p><br />下面是一些简单的示例：<br /><img src="https://img.alicdn.com/tfs/TB1sqHjT1L2gK0jSZFmXXc7iXXa-1920-1080.png" alt="image.png"><br />通过 Hooks 的方式，我们可以像写 React Hooks 一样去开发 Web 服务。<br /></p><p><a name="b1uaG"></a></p><h4 id="可复用的-Hooks"><a href="#可复用的-Hooks" class="headerlink" title="可复用的 Hooks"></a>可复用的 Hooks</h4><p>在 Hooks 的开发中，我们也支持将复杂或重复的逻辑，提取成单独的 Hooks 并进行复用，从而减少重复性的劳动。<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1PWTjT1L2gK0jSZFmXXc7iXXa-1920-1080.png" alt="image.png"><br><a name="u2GSh"></a></p><h4 id="实现原理与性能问题的解决"><a href="#实现原理与性能问题的解决" class="headerlink" title="实现原理与性能问题的解决"></a>实现原理与性能问题的解决</h4><p>在实现 Hooks 语法的支持时，实际上是存在一波三折的。<br /><br><br />在社区上，Node.js 官方提供了 Async Hooks 这个模块，可以用于模拟实现请求上下文的传递功能。但该模块也存在两个问题：</p><ul><li>模块 Api 不稳定</li><li>性能问题严重</li></ul><p><br />其中，第二个问题是我们弃用该方案的直接原因，具体的 Benchmark 可以看下图：<br /></p><blockquote><p>Async Hooks 所带来的性能损耗十分惊人</p></blockquote><p><img src="https://img.alicdn.com/tfs/TB1OCiRg7cx_u4jSZFlXXXnUFXa-1920-1080.png" alt="image.png"><br />因此，我们尝试另辟蹊径，通过在工程上预编译的方式，来实现 Hooks 的开发与请求上下文的传递。<br /><br><br />编译原理并不复杂，主要是以下两点：</p><ul><li>获取请求上下文：转换为对 <code>this</code> 的引用</li><li>调用 Hooks：转换为 <code>bind</code> 调用，将 <code>this</code> 传递下去</li></ul><p><br />通过这种方式，我们在函数间对于上下文与 Hooks 的引用，串成了一条完整的调用链。而 bind 操作所带来的调用开销完全是可接受的。<br /><br><br />由于我们是使用的 TypeScript，在编译过程中对于源码的更改会影响到 Source Map 的生成，因此我们也自研了 Midway 的编译器 mwcc，不仅解决了 Source Map 生成等问题，更是提供了类似于 Babel + Babel Traverse + Plugin 的开发体验，有兴趣的同学可以自行了解一下~<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1TT2Yg5pE_u4jSZKbXXbCUVXa-1920-1080.png" alt="image.png"><br /></p><p><a name="krI6U"></a></p><h3 id="渐进式开发：Progressive-development"><a href="#渐进式开发：Progressive-development" class="headerlink" title="渐进式开发：Progressive development"></a>渐进式开发：Progressive development</h3><p>阿里在落地 Node.js 时，会遇到非常多复杂的业务场景，这部分将向大家介绍我们在 Node.js 企业级应用开发的实践与解决方案，及在函数式开发的场景下，如何复用这部分最佳实践。<br /></p><p><a name="Qxh9A"></a></p><h4 id="阿里巴巴企业级-Node-js-应用开发实践"><a href="#阿里巴巴企业级-Node-js-应用开发实践" class="headerlink" title="阿里巴巴企业级 Node.js 应用开发实践"></a>阿里巴巴企业级 Node.js 应用开发实践</h4><p>在设计整个 Midway 框架时，我们一直在思考一个问题：“用什么方式解决复杂业务问题”？而我们给出的答案是参考软件设计的经典原则：<a href="https://zh.wikipedia.org/zh-hans/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1">SOLID 软件设计原则</a>&gt;)与其中的<a href="https://zh.wikipedia.org/wiki/%E4%BE%9D%E8%B5%96%E5%8F%8D%E8%BD%AC%E5%8E%9F%E5%88%99">依赖倒置原则</a>。</p><p>同时我们也参考了诸多业界的实践，发现成熟的 IoC 设计已经能够解决复杂业务的问题，包括 Java 的 Spring、JS 社区的 Nest.js/TypeOrm 等，都采用了基于 IoC 的实现方式。<br /><br><br />因此，我们决定通过自研的 IoC 框架，作为 Midway 体系的核心去解决复杂应用的维护问题。<br /><br><br />而函数式研发作为 Midway 体系的一种解决方案，因此在设计之初我们就考虑了函数与 Midway IoC 体系的融合，使得函数式研发可以复用 IoC 的最佳实践。<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1erDjT1L2gK0jSZFmXXc7iXXa-1920-1080.png" alt="image.png"><br /></p><p><a name="K3AJK"></a></p><h4 id="函数式与-IoC-的结合"><a href="#函数式与-IoC-的结合" class="headerlink" title="函数式与 IoC 的结合"></a>函数式与 IoC 的结合</h4><p>这儿我们依然通过 Hooks 来解决这个问题。<br /><br><br />我们提供了 <code>useInject</code> Api，通过这个 Hooks 在函数中来使用 IoC。<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1X.1VjlFR4u4jSZFPXXanzFXa-1920-1080.png" alt="image.png"><br /><br><br />通过这种方式，我们实现了函数式与 IoC 的无缝结合，从简单到复杂场景都能处理。<br /></p><p><a name="XNx2f"></a></p><h3 id="内部落地情况"><a href="#内部落地情况" class="headerlink" title="内部落地情况"></a>内部落地情况</h3><p>Midway Serverless 的云端一体方案在对外发布前，在阿里内部实际上已经探索了有大半年的时间。<br /><br><br />项目时间轴如下</p><ul><li>2020.02：Idea 提出 &amp; POC 演示</li><li>2020.03：核心功能确认 &amp; Api 确认</li><li>2020.04：首个业务落地</li><li>2020.05 - Now：多个业务落地并使用，持续迭代中</li></ul><blockquote><p>内部落地的 BU</p></blockquote><p><img src="https://img.alicdn.com/tfs/TB1g35YT4z1gK0jSZSgXXavwpXa-1920-1080.png" alt="image.png"><br><a name="ygwot"></a></p><h2 id="未来展望"><a href="#未来展望" class="headerlink" title="未来展望"></a>未来展望</h2><p><a name="FMkFw"></a></p><h3 id="开源"><a href="#开源" class="headerlink" title="开源"></a>开源</h3><p>目前 Midway Serverless 的云端一体方案已正式对社区发布并可用，大家可以进入 Midway Serverless 的 <a href="http://gitlab.alibaba-inc.com/midway/midway">Github 仓库</a>，<a href="https://www.yuque.com/midwayjs/faas/quickstart_integration">查看云端一体方案的文档与使用方式</a>，觉得好用的话记得点个 Star 哦~<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1ctBSjLzO3e4jSZFxXXaP_FXa-1920-1080.png" alt="image.png"><br><a name="tOn3b"></a></p><h3 id="未来：多场景落地"><a href="#未来：多场景落地" class="headerlink" title="未来：多场景落地"></a>未来：多场景落地</h3><p>在阿里巴巴内部，我们主打的是中后台与移动端的场景，但实际上这套方案是可以用于多场景的。因此在未来，我们希望能投入到开源，结合前端框架、SSR、小程序等各种场景并落地。<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1XcO3T.z1gK0jSZLeXXb9kVXa-1920-1080.png" alt="image.png"><br />这部分是我们未来的一个规划，也欢迎大家来参与<a href="http://gitlab.alibaba-inc.com/midway/midway">开源</a>，提交 Idea 与代码。一同去打造更优秀的研发体验。<br /><img src="https://img.alicdn.com/tfs/TB1ctjjT1L2gK0jSZFmXXc7iXXa-1920-1080.png" alt="image.png"></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这是 Midway Serverless 体系在 8 月底位于 &lt;strong&gt;阿里云云原生微服务大会 &lt;/strong&gt;对外分享的内容文字版，介绍了 Midway Serverless 全新的类 React Hooks 风格的 Serverless 开发方案，欢迎阅读和转
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>千万级流量业务的 Serverless 实践，看 FaaS 给前端带来的变化</title>
    <link href="https://blog.lxxyx.cn/2020/02/19/2020/wow/"/>
    <id>https://blog.lxxyx.cn/2020/02/19/2020/wow/</id>
    <published>2020-02-19T11:19:46.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p>2019 年初，淘系技术部启动了 Serverless 研发模式升级计划。而哇哦视频作为首个落地的业务，迄今已有半年。</p><p>本次则会为大家分享哇哦视频在这半年中发生的故事，与大家一起看看在一线业务同学的眼中，Serverless 会给前端同学带来什么，而我们又能收获什么？</p><h2 id="分享内容"><a href="#分享内容" class="headerlink" title="分享内容"></a>分享内容</h2><p>本次分享我会从以下三个部分出发：</p><ul><li>业务落地</li><li>从零到一</li><li>未来抉择</li></ul><p><img src="https://img.alicdn.com/tfs/TB13P29vEz1gK0jSZLeXXb9kVXa-1920-1080.png" alt="image.png"></p><p><a name="NHXgc"></a></p><h2 id="业务落地"><a href="#业务落地" class="headerlink" title="业务落地"></a>业务落地</h2><p>哇哦视频是在手淘首页的短视频导购业务。</p><p><img src="https://img.alicdn.com/tfs/TB1UKUcvAT2gK0jSZFkXXcIQFXa-1920-1080.png" alt="image.png"></p><p><a name="fUibs"></a></p><h3 id="“三高”业务"><a href="#“三高”业务" class="headerlink" title="“三高”业务"></a>“三高”业务</h3><p>而其作为手淘的导购业务，其具有“三高”的特点：</p><ul><li>流量高</li><li>稳定性要求高</li><li>迭代频率高</li></ul><p>流量高指的是哇哦视频业务体量大，日常流量高，随着高流量所带来的，就是稳定性要求高的特点，且作为导购业务，其迭代频率也是非常高的。</p><p><img src="https://img.alicdn.com/tfs/TB17XsfvBr0gK0jSZFnXXbRRXXa-1920-1080.png" alt="image.png"></p><p><a name="1eqrC"></a></p><h3 id="开发痛点"><a href="#开发痛点" class="headerlink" title="开发痛点"></a>开发痛点</h3><p>在长达一年的开发实践中，我逐渐发现了业务在开发中遇到的一些痛点。</p><p>在这里我以最近三次首页改版的时间为例。通过下图这个时间轴，我发现整个业务迭代的过程中开发联调时间较长，从而导致需求完成时间往往会超过 10 天。</p><p><img src="https://img.alicdn.com/tfs/TB171EcvAT2gK0jSZFkXXcIQFXa-1920-1080.png" alt="image.png"></p><p>基于此我也开始在思考业务研发模式中存在的一些痛点。</p><p>首先站在整个业务的大背景来看，有两个特点：</p><ul><li>淘系大力推行中台化战略，业务能力实现了中台化，日常需求基本可以通过中台能力组装实现</li><li>导购业务一直处于高速迭代期，从未停歇。</li></ul><p>基于以上的背景，我们会发现在目前业务的研发模式中有着诸多的痛点。</p><p><a name="PniMe"></a></p><h4 id="联调"><a href="#联调" class="headerlink" title="联调"></a>联调</h4><p>首当其冲的痛点则是联调。在联调期中前后端需要不断对数据字段、业务逻辑进行确认，从而确保需求实现的正确性，而这种密集的沟通所带来的成本是非常高的。在哇哦视频这儿，我们发现联调成本一般要占到开发成本的 30% 左右。</p><p>居高不下的联调成本，一方面使得工程师们精疲力尽，另一方面也不利于业务的快速迭代。</p><p><a name="AjOyx"></a></p><h4 id="业务开发的枯燥"><a href="#业务开发的枯燥" class="headerlink" title="业务开发的枯燥"></a>业务开发的枯燥</h4><p>对于业务开发来说，日常工作就是取数据调接口，枯燥且重复。</p><p><a name="SyxCj"></a></p><h4 id="前端资源化"><a href="#前端资源化" class="headerlink" title="前端资源化"></a>前端资源化</h4><p>值得一提还有前端资源化的痛点。</p><p>在目前前后端的分工模式中，前端只负责交互逻辑与相对应的 UI 实现，对于业务核心逻辑无需过多了解。虽然这使得前端团队可以快速完成某些业务，但同样也带来了前端资源化的隐患。而在强调前端要深入业务，具有商业化思考能力的今天，前端资源化实际上是不利于前端的自身发展的。</p><p>因为很多时候前端想去深入业务，想进一步升级自己的能力，但往往会苦于没有相关场景。至于说介入后端的工作领域，毕竟术业有专攻，很多事情也掺和不进去。</p><p><img src="https://img.alicdn.com/tfs/TB1rfz_vpP7gK0jSZFjXXc5aXXa-1920-1080.png" alt="image.png"></p><p><a name="fV5WP"></a></p><h3 id="研发模式升级战役"><a href="#研发模式升级战役" class="headerlink" title="研发模式升级战役"></a>研发模式升级战役</h3><p>恰好 2019 年的 4 月份，淘系前端开启了研发模式升级的战役。而我也参与进去负责导购体系 Node FaaS 相关建设的工作。基于 Node FaaS 提供的能力，我开始重新思考上面碰到的业务痛点，并认为研发模式升级是可以解决以上的痛点问题的。</p><p>对于为什么研发模式升级可以解决上述提到的痛点，我个人认为主要有以下三点：</p><ul><li>单人负责前后端，且均使用 JS，能极大的<strong>减少沟通与联调成本，满足业务高速迭代需求</strong></li><li>离用户最近，熟悉业务规则，<strong>业务方沟通成本低。</strong></li><li><strong>前端对于自身能力升级的渴望。</strong>对于身为前端的我来说，我不想也只是作为一个资源，每次被调配，我也希望自己可以去对业务负责，也希望可以有机会去赋能业务，去做更多的事情。</li></ul><p>而对于后端同学来说，当前端负责前后端，后端同学则可以从业务中释放出来，从而选择去开辟更多的新战场，去做更多更有挑战性的事情。</p><p><img src="https://img.alicdn.com/tfs/TB1Hz69vEz1gK0jSZLeXXb9kVXa-1920-1080.png" alt="image.png"></p><p><a name="KSWAU"></a></p><h3 id="业务迁移"><a href="#业务迁移" class="headerlink" title="业务迁移"></a>业务迁移</h3><p>带着以上这些思考，我仔细阅读了业务的后端代码并梳理整体的业务逻辑，发现其具有以下的特点：</p><ul><li>后端无状态</li><li>复杂度适中</li></ul><p>这两个特点使得业务非常适合使用 Serverless 技术来承接。<br />而在正式进行迁移前，我和业务方沟通了这个事情对于业务可能产生的影响以及后续规划。业务方对于技术侧的改造是没有意见的，只有一个诉求，那就是业务不受影响。</p><p>整个诉求看似简单，拆解下来包括以下三部分：</p><ul><li>不会为技术侧改造预留时间，原定需求要按时完成</li><li>迁移后线上不能出任何问题，线上对迁移无感知</li><li>后端工作交接至前端后，对后续需求推进无影响</li></ul><p>说起来就是既要快，又要稳，还要能扛住后续需求。而在充分梳理和分析后，我确认我可以在“不坑业务”的基础下，完成整个迁移。</p><p><img src="https://img.alicdn.com/tfs/TB1par_vxD1gK0jSZFsXXbldVXa-1920-1080.png" alt="image.png"></p><p>在开始迁移后，事情进展比我们想象中的要顺利很多。下面是我们研发过程中的一些数据。</p><p><img src="https://img.alicdn.com/tfs/TB1IIr_vEH1gK0jSZSyXXXtlpXa-1920-1080.png" alt="image.png"></p><p>在迁移完成后，我又尝试接了几个需求，并对前后所用的时间做了一个对比。</p><p>可以看到在之前完成一个业务迭代，前端总共需要投入 10 天，整体投入人数是 2 人。<br />而在研发模式升级后，前端总投入时间增加到 12 天，虽然看起来投入时间增加了，但是却减少了一个人的投入。总的来看还是节约成本的。</p><p><img src="https://img.alicdn.com/tfs/TB13W_.vxn1gK0jSZKPXXXvUXXa-1920-1080.png" alt="image.png"></p><p><a name="8Wz6X"></a></p><h2 id="从零到一"><a href="#从零到一" class="headerlink" title="从零到一"></a>从零到一</h2><p>这部分我主要我想讲清楚一个问题，那就是业务从开始迁移，到研发完成上线，这个过程中都发生了什么？</p><p>在这里我把它主要分为以下 4 个部分来讲述：</p><ul><li>需求承接</li><li>本地研发</li><li>部署与调用</li><li>运维</li></ul><p><img src="https://img.alicdn.com/tfs/TB1rhz.vrr1gK0jSZR0XXbP8XXa-1920-1080.png" alt="image.png"><br><a name="97ymY"></a></p><p>###</p><p><a name="JPTI8"></a></p><h3 id="需求承接"><a href="#需求承接" class="headerlink" title="需求承接"></a>需求承接</h3><p>迁移的基础是原有需求的承接，而需求承接的核心就在于需求评估。在业务方提出需求时，我们会按照如下的节奏开始需求工作流：</p><ul><li>召开需求评审会，确认需求与具体内容</li><li>召开技术评审会，确认实现方式与涉及到的二方平台</li><li>需求开发、发布等</li><li>经验积累总结</li></ul><p>而在需求承接的过程中其实会遇到一个难题，那就是如何确认需求是可实现的？<br />这是一个前期困扰我许久的问题，然而答案却也简单，那就是：如果需求的实现不涉及三方平台的，那么依据自身能力评估即可。如果是涉及到三方的需求，则需要让业务方组织技术评审会，会上确认是否该需求是可实现。</p><p><img src="https://img.alicdn.com/tfs/TB1NGQdvrj1gK0jSZFuXXcrHpXa-1920-1080.png" alt="image.png"></p><p><a name="tYuXW"></a></p><h3 id="研发流程"><a href="#研发流程" class="headerlink" title="研发流程"></a>研发流程</h3><p>这部分是关于函数研发流程相关，会按以下四部分去阐述：</p><ul><li>函数开发</li><li>服务调用</li><li>单元测试</li><li>函数发布</li></ul><p><img src="https://img.alicdn.com/tfs/TB10aMdvrj1gK0jSZFuXXcrHpXa-1920-1080.png" alt="image.png"></p><p><a name="npMFT"></a></p><h3 id="本地研发"><a href="#本地研发" class="headerlink" title="本地研发"></a>本地研发</h3><p>整个本地研发，按照时间顺序主要分为以下四个步骤：</p><ul><li>函数开发</li><li>服务调用</li><li>单元测试</li><li>函数发布</li></ul><p><img src="https://img.alicdn.com/tfs/TB1XpP_vAL0gK0jSZFtXXXQCXXa-1920-1080.png" alt="image.png"></p><p>值得一提的是，在之前我提到过：“哇哦视频作为淘系首个落地 Node FaaS 技术的项目，其开发的整个过程，也与 Serverless 体系的成长密不可分。”</p><p>所以这部分即会讲述到本地开发，也会讲述到哇哦视频从迁移伊始到上线这段时间内，给 Node FaaS 带来的一些改变。</p><p><a name="CsBoY"></a></p><h4 id="多函数的目录结构"><a href="#多函数的目录结构" class="headerlink" title="多函数的目录结构"></a>多函数的目录结构</h4><p>在这之前，主流 FaaS 项目结构如我图中所画的那样。每个函数间是相互隔离的，这样虽然比较干净，但其实也带来了非常多的一些痛点。比如跨函数逻辑复用困难，重复的依赖安装和重复的文件。</p><p>因此我们在思考，既然 FaaS 的优势在于零运维，那我们不应该为了运维的优势而强行降低开发者的效率。</p><p>因此我们提出了新的目录结构方案。在新的目录结构中，我们使得多函数可以复用同一依赖，避免重复的文件与依赖。且我们也提出了新的打包方案，每个函数入口文件都会被打包为一个函数单独部署。</p><p>至此，我们获得了应用级的开发体验，也获得了 FaaS 零运维的优势，从而达到开发者提效，专注业务逻辑开发的目标。</p><p><img src="https://img.alicdn.com/tfs/TB1wq2.vAP2gK0jSZPxXXacQpXa-1920-1080.png" alt="image.png"></p><p><a name="UtBT2"></a></p><h4 id="整洁架构的探索"><a href="#整洁架构的探索" class="headerlink" title="整洁架构的探索"></a>整洁架构的探索</h4><p>在开发过程中，哇哦视频也对业务的架构做了一些新的探索。在此我参考了整洁架构的一个实践，事后证明配合 IoC 与依赖倒置，有效的减少了业务的迭代难度。</p><p>同时关于这个架构与具体的落地实践，外部有非常多的案例，有兴趣的同学可以自行查阅。</p><p><img src="https://img.alicdn.com/tfs/TB1iLH_vpP7gK0jSZFjXXc5aXXa-1920-1080.png" alt="image.png"></p><p><a name="kykPM"></a></p><h4 id="服务市场-HSF-调用一站式解决方案"><a href="#服务市场-HSF-调用一站式解决方案" class="headerlink" title="服务市场 - HSF 调用一站式解决方案"></a>服务市场 - HSF 调用一站式解决方案</h4><p>在业务开发的过程中，一定会遇到的就是 HSF 的服务调用。为了简化开发工作量，尽可能的提升研发效率，对此我们也提供了对应的解决方案：服务市场。</p><p>服务市场囊括了 HSF 服务，从调用到测试的全流程支持。是我们在开发业务时的一把利器。</p><p><img src="https://img.alicdn.com/tfs/TB1fq2.vAP2gK0jSZPxXXacQpXa-1920-1080.png" alt="image.png"></p><p><a name="18zxS"></a></p><h4 id="单元测试"><a href="#单元测试" class="headerlink" title="单元测试"></a>单元测试</h4><p>在之前的 Midway 项目中，是默认支持使用 Mocha 测试的。</p><p>而正好在今年的 <a href="https://2019.stateofjs.com/">JavaScript 开发者报告</a>中，关于测试框架一项，Mocha 的人气在持续的走低，而 Jest 作为近两年的新起之秀，人气在不断的升高。因此我们在原有的 Mocha 支持上，又新增了 Jest 的支持。</p><p>同时哇哦视频有着通过单测保障代码质量的需求，因此我们也打通了  FaaS 函数在内部 Ci 系统单元测试的链路。</p><p><img src="https://img.alicdn.com/tfs/TB1DuIcvAT2gK0jSZFkXXcIQFXa-1920-1080.png" alt="image.png"></p><p><a name="6ariA"></a></p><h4 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h4><p>其实这部分与实际的代码书写风格相关，虽然说是战时盯着屏幕看监控，但是如何第一时间发现错误源于何处，是否需要处理其实是值得琢磨的问题。毕竟无效的监控一多，人就会陷入到报警疲劳中。</p><p>在这儿，哇哦视频采取对于错误采取了以下的几个小策略，确保线上可以快速定位问题：</p><ul><li>错误分级：将错误分为 忽略/警告/重视 3 种等级，方便一眼确认重要错误</li><li>自定义错误名：将错误自定义名称，而非是笼统的 Error，方便迅速确认具体错误</li><li>携带关键信息：针对不同错误，携带不同关键信息，方便快速定位与复现问题</li></ul><p>简单示例如下：</p><p><img src="https://img.alicdn.com/tfs/TB1bI_yvp67gK0jSZPfXXahhFXa-798-528.png" alt="image.png"><br /><br><br />而在实际业务中的监控中，实际上会较容易的区分与定位错误：这样最终是对于线上问题的排查与修复是有帮助的。</p><p><a name="KZ8TD"></a></p><h4 id="Java-兜底"><a href="#Java-兜底" class="headerlink" title="Java 兜底"></a>Java 兜底</h4><p>对于 Java 兜底，我现在就一个感觉，真香！<br />虽然这是一个临时性的方案，但是在你出问题时还有兜底的服务可以帮忙扛着，无比安心呀。<br /><br><br />具体实现上非常简单，实际上就是在请求自己接口失败时，再去请求一次 Java 接口，确保可以提供服务。毕竟两个应用同时都宕机的几率是非常低的。<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1Y2L_vpP7gK0jSZFjXXc5aXXa-1920-1080.png" alt="image.png"></p><p><a name="AIxah"></a></p><h3 id="发布流程"><a href="#发布流程" class="headerlink" title="发布流程"></a>发布流程</h3><p>这部分主要与代码的发布，我们提供了一站式的研发平台，来帮助你管理函数，实现版本管理与回滚等功能。</p><p><img src="https://img.alicdn.com/tfs/TB1ONP9vvb2gK0jSZK9XXaEgFXa-1920-1080.png" alt="image.png"></p><p><a name="MUit9"></a></p><h3 id="调试闭环"><a href="#调试闭环" class="headerlink" title="调试闭环"></a>调试闭环</h3><p>对于现代化的开发而言，调试是必不可少的一项能力。在此 Sandbox 平台 提供了一站式的调试闭环服务。我们可以在函数中启用远程调试，然后在预发去调试代码，从而帮助我们更快的去定位问题。</p><p><img src="https://img.alicdn.com/tfs/TB1P.EbvxD1gK0jSZFyXXciOVXa-1920-1080.png" alt="image.png"></p><p><a name="xCqdw"></a></p><h3 id="函数监控"><a href="#函数监控" class="headerlink" title="函数监控"></a>函数监控</h3><p>关于函数监控，在此我们也提供了一站式全功能的 Node.js 治理平台，Sandbox。包括运维数据、链路分析、监控告警、白屏化日志四大功能。</p><p><img src="https://img.alicdn.com/tfs/TB1gDAcvuH2gK0jSZJnXXaT1FXa-1920-1080.png" alt="image.png"></p><p><a name="kDABc"></a></p><h4 id="运维数据"><a href="#运维数据" class="headerlink" title="运维数据"></a>运维数据</h4><p>在此，我们可以通过 Sandbox 提供的功能，清楚的看到当前函数的运行数据。功能也非常简洁明了。</p><p><a name="uRWVG"></a></p><h4 id="链路分析"><a href="#链路分析" class="headerlink" title="链路分析"></a>链路分析</h4><p>在阿里做后端，怎么能缺少全链路分析的辅助。</p><p>在此，Sandbox 也对全链路分析做了支持。<br />对于函数的每个请求，我们可以查看到其链路的详情与对应的链路，帮助我们快速定位到链路问题。而针对于错误链路，在这里也能看到其对应的链路分析与错误的日志。</p><p><a name="RbKih"></a></p><h4 id="白屏化日志"><a href="#白屏化日志" class="headerlink" title="白屏化日志"></a>白屏化日志</h4><p>Sandbox 也提供了白屏化日志的功能，通过查看实时日志，可以有效的了解函数情况。</p><p><a name="LfcMT"></a></p><h4 id="监控报警"><a href="#监控报警" class="headerlink" title="监控报警"></a>监控报警</h4><p>Sandbox 提供了监控报警的功能，在这里我们可以设定自己的报警项。在报警触发时，会通过短信电话等方式实时通知到同学。</p><p>配合之前提到的那些功能，可以使得快速发现并定位修复故障。</p><p><a name="VCblw"></a></p><h3 id="FaaS-的业务能力"><a href="#FaaS-的业务能力" class="headerlink" title="FaaS 的业务能力"></a>FaaS 的业务能力</h3><p>基于以上的这些功能，与哇哦视频业务长达半年的实践与踩坑，我们可以说：</p><blockquote><p>“<strong>FaaS 函数工具完备，完成业务就像是搭积木一样！</strong>”</p></blockquote><p>如果有相关业务需求的同学，可以大胆的放心的使用 FaaS 去完成。</p><p><img src="https://img.alicdn.com/tfs/TB1_Twbvrj1gK0jSZFOXXc7GpXa-1920-1080.png" alt="image.png"></p><p><a name="uyZMw"></a></p><h2 id="未来抉择"><a href="#未来抉择" class="headerlink" title="未来抉择"></a>未来抉择</h2><p>这部分是业务迁移 FaaS 半年后，个人的一些思考与总结。</p><p><a name="zJzt5"></a></p><h3 id="研发模式升级推论"><a href="#研发模式升级推论" class="headerlink" title="研发模式升级推论"></a>研发模式升级推论</h3><p>首先先看一个研发模式升级的推论。</p><ol><li>因为：我们在技术上使用 Serverless</li><li>所以：实现前端能力升级</li><li>结果：助力业务先赢</li></ol><p>这个推论听起来好像是很正确的一件事情，但却让我想起我之前听到的一个故事。</p><p><img src="https://img.alicdn.com/tfs/TB12robvxv1gK0jSZFFXXb0sXXa-1920-1080.png" alt="image.png"></p><p><a name="Q9vAl"></a></p><h4 id="故事：科技改变生活"><a href="#故事：科技改变生活" class="headerlink" title="故事：科技改变生活"></a>故事：科技改变生活</h4><p>都说科技改变生活，以前我们遇到不懂的问题，要去图书馆查资料。<br />而现在，遇到不懂的问题，我们只需要掏出手机，然后，你就会忘记这个问题。</p><p><img src="https://img.alicdn.com/tfs/TB1Dqv_vxD1gK0jSZFsXXbldVXa-1920-1080.png" alt="image.png"></p><p>而这个故事和之前研发模式升级的推论，在我看来是差不多的。虽然我们有了新的方式去解决问题，但最后的结果可能天差地别。</p><p>在这里我就发现前端能力升级，并不一定能助力业务先赢，这也是困扰我长达半年之久的问题。</p><p><img src="https://img.alicdn.com/tfs/TB12ij_vxz1gK0jSZSgXXavwpXa-1920-1080.png" alt="image.png"></p><p><a name="s0qTJ"></a></p><h3 id="我在哇哦视频的选择"><a href="#我在哇哦视频的选择" class="headerlink" title="我在哇哦视频的选择"></a>我在哇哦视频的选择</h3><p>“研发模式升级究竟能否助力业务先赢？”</p><p>这个问题就像心魔一样在我心头萦绕，但遇到问题就要解决问题。总得试试看，因此在半年内，分为 3 个阶段，我做了如下的一些尝试。</p><p><a name="rTCTP"></a></p><h4 id="支撑（19-06-19-09）"><a href="#支撑（19-06-19-09）" class="headerlink" title="支撑（19.06 - 19.09）"></a>支撑（19.06 - 19.09）</h4><p>第 1 个阶段我称为支撑，时间是 19 年的 6 月份到 19 年的 9 月份。</p><p>在这期间我对自己的要求就只有一个，那就是“活下来”。因为首先要能做业务，才有后续与业务谈价值的可能性。否则都只是空中楼阁罢了。</p><p><a name="BYmXx"></a></p><h4 id="对话（19-10-19-11）"><a href="#对话（19-10-19-11）" class="headerlink" title="对话（19.10 - 19.11）"></a>对话（19.10 - 19.11）</h4><p>第 2 个阶段我称为对话。</p><p>在能做业务的基础上，我开始尝试去理解业务数据，主动去沟通、学习、了解相关业务领域知识。并且在遇到问题时会习惯性与业务方进行沟通，看看这个问题，从业务视角出发是如何理解的。</p><p><a name="4yKCG"></a></p><h4 id="助力（19-12-至今）"><a href="#助力（19-12-至今）" class="headerlink" title="助力（19.12 - 至今）"></a>助力（19.12 - 至今）</h4><p>第 3 个阶段我称为助力。</p><p>在逐渐理解业务规划、与业务方也有了一定的信任后，我开始尝试参与部分业务规划，与业务方一同去挖掘痛点。同时同时站在技术的角度上做一些技术预研，尝试为业务带去更多的发挥空间。</p><p><img src="https://img.alicdn.com/tfs/TB1mYMbvrH1gK0jSZFwXXc7aXXa-1920-1080.png" alt="image.png"></p><p><a name="EyRWR"></a></p><h4 id="抉择"><a href="#抉择" class="headerlink" title="抉择"></a>抉择</h4><p>在我刚提到的第一阶段“支撑期”时，我经历了非常长一段时间的思想抉择。</p><p>在我可以支撑业务时，当时的我面临着两个选择：</p><ol><li>做一辈子的业务工具人：业务说什么我做什么，承接需求就好</li><li>尝试做一次业务合伙人：试着给自己提出的问题（研发模式升级究竟能否助力业务先赢？）找一个答案</li></ol><p>当时的纠结在现在看来，主要是以下几点：</p><ul><li>作为一名技术同学，尝试着做非常多看起来与技术不相关的事情，风险高难落地拿不到结果</li><li>受非技术因素影响多，业务的走向与起起落落，并非是技术可解的问题</li><li>做成了可能与技术无关，做不成却一定有自己的问题</li></ul><p>但最终我还是选择尝试做一次“业务合伙人”，因为我觉得如果研发模式升级如果不能证明它能助力业务先赢，那么整个战役实际上是失败的。对于自己而言，也只是简单的完成了一次业务迁移的过程，而没有真正的去解决问题。</p><p><img src="https://img.alicdn.com/tfs/TB1wdbyvp67gK0jSZPfXXahhFXa-1920-1080.png" alt="image.png"></p><p>比起拿到迁移的结果，我更想知道研发模式升级的答案。</p><p><a name="zxpJq"></a></p><h3 id="业务对话"><a href="#业务对话" class="headerlink" title="业务对话"></a>业务对话</h3><p>在下决定后，我开始按自己的思路去做这一切的事情。</p><p>首先是对既有经验的一个学习。正好最近阿里经济体前端委员会在阿里技术论坛出品了《技术与业务同行》这个专题，里面有非常多业务思考的文章，细细拜读后收获颇多。</p><p>在拜读相关文章后，我开始主动去了解业务的数据，目标与未来的规划，且在此基础上，我开始主动要求与产品经理，去做一个沟通，希望能去与业务方，共同了解整个产品业务的规划与交流。</p><p>在这之后，我被邀请去参与哇哦视频用户调研活动。这也是我第一次面对面的去与用户对话，了解用户的真实需求。<br />而在与用户面对面沟通后，我发现自己业务思考上的诸多盲区。这件事情也是一个警醒，虽然我自诩为前端是离用户最近的岗位，但实际上与用户面对面之后，才知道实际上相差甚远，比起自嗨，我们仍然需要更多的去倾听用户的声音。</p><p><img src="https://img.alicdn.com/tfs/TB1pOH_vBv0gK0jSZKbXXbK2FXa-1920-1080.png" alt="image.png"></p><p><a name="f8WZN"></a></p><h3 id="例子：承接手淘-Push"><a href="#例子：承接手淘-Push" class="headerlink" title="例子：承接手淘 Push"></a>例子：承接手淘 Push</h3><p>哇哦视频的业务方之前一直存在一个痛点，那就是业务入口流量不足。在与业务频繁对话的那段时间，这个问题时常会被提起。</p><p>针对这个问题，我开始尝试与业务方讨论有没有相应的解法，最终将目标锁定在使用手淘 Push 消息的能力来为业务导流。<br />与业务方沟通并达成一致后，技术侧迅速开始了改造，为哇哦视频新增了承接手淘 Push 的能力，且随着消息的推送，不断进行相应的调整与优化。<br />在多日的持续优化下，哇哦视频通过手淘 Push 实现 UV 单日最高提升 20%+的结果。</p><p>而在经过这件事情后，我想我找到了“研发模式升级究竟能否助力业务先赢”这个问题的答案：Serverless 是前端撬动业务场景的一把利剑。</p><p><img src="https://img.alicdn.com/tfs/TB14Wv_vuL2gK0jSZPhXXahvXXa-1920-1080.png" alt="image.png"></p><p><a name="43Bd9"></a></p><h2 id="总结：FaaS-给前端带来的变化"><a href="#总结：FaaS-给前端带来的变化" class="headerlink" title="总结：FaaS 给前端带来的变化"></a>总结：FaaS 给前端带来的变化</h2><p>在我之前写的文章《哇哦视频 ❤️ FaaS - 迁移前后的那些事儿》中，公司的同事留了一句评论：</p><p><img src="https://img.alicdn.com/tfs/TB1Jfv_vpP7gK0jSZFjXXc5aXXa-1920-1080.png" alt="image.png"></p><p>而对于我来说，后续的目标则是希望在助力业务成长上去探寻到更多的可能性。基于 FaaS 的研发模式升级，实际上是为了助力业务先赢。</p><p>我们不要因为走得太远，而忘记为什么出发。</p><p><img src="https://img.alicdn.com/tfs/TB1Y4Y.vrr1gK0jSZR0XXbP8XXa-1920-1080.png" alt="image.png"></p><p><img src="https://img.alicdn.com/tfs/TB148T.vET1gK0jSZFhXXaAtVXa-1920-1080.png" alt="image.png"></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;2019 年初，淘系技术部启动了 Serverless 研发模式升级计划。而哇哦视频作为首个落地的业务，迄今已有半年。&lt;/p&gt;
&lt;p&gt;本次则会为大家分享哇哦视频在这半年中发生的故事，与大家一起看看在一线业务同学的眼中，Serverless 会给前端同学带来什么，而我们又能收
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>哇哦视频 ❤️ FaaS - 迁移前后的那些事儿</title>
    <link href="https://blog.lxxyx.cn/2019/11/16/2019/wowvideo/"/>
    <id>https://blog.lxxyx.cn/2019/11/16/2019/wowvideo/</id>
    <published>2019-11-16T17:30:52.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p><a name="ijr6D"></a></p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>作为一位技术同学，我相信你对于今年集团内部 Serverless 与 FaaS 的热度一定有所感知。</p><p>在看了那么多“技术原理/顶层设计/平台建设”相关的文章之后，我相信你对 FaaS 肯定产生过跃跃欲试的感觉，但也肯定存在诸多疑惑，例如：</p><ul><li>FaaS 在业务中能落地吗？</li><li>FaaS 能帮助前端同学实现能力升级吗？</li><li>……</li></ul><p>而这些疑惑对于刚开始接触 FaaS 的我而言，只会多不会少。恰好，我所负责的“哇哦视频”业务是淘系第一个基于 Node FaaS 开发的线上业务，在线上已经稳稳当当的跑了 4 个月，这期间不仅接手了 Java 同学的工作，同时也顺利的承接了日常与双十一需求。</p><p>关于上面的这些疑惑，经过了这四个月的考验，我想我已经有了自己的答案。接下来我将会向大家分享我这四个月的历程，带大家一起看看，在一名一线业务同学的眼中，FaaS 究竟会给前端同学带来什么？</p><p><a name="I5TpJ"></a></p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>哇哦视频是在手淘首页的短视频导购业务，业务核心目标如下：</p><blockquote><p>打造围绕“人用物”为核心有 “温度”的短视频；引导更多的商家视频，商家模板化生产;增加首页分发效率，让用户快速的且容易定位到自己想看的视频内容。</p></blockquote><p>而其作为手淘的导购业务，其具有“三高”的特点：</p><p><img src="https://gw.alicdn.com/tfs/TB13hDdlHr1gK0jSZFDXXb9yVXa-1920-1080.png#align=left&amp;display=inline&amp;height=420&amp;name=image.png&amp;originHeight=1080&amp;originWidth=1920&amp;search=&amp;size=104295&amp;status=done&amp;width=746" alt="image.png"><br />由于是身处手淘首页的业务，其流量相对于普通业务而言是比较高的，属于大流量业务。而流量高的特点也带来了稳定性高的要求，由于用户众多，因此线上的任何不稳定都有可能产生舆情。</p><p>而作为导购业务，业务本身还有一个迭代频率高的特性。为了能实现更好的导购效果，业务需要不断的推陈出新，快速建场，从而获得更好的导购效果。</p><p><a name="C6hdA"></a></p><h2 id="淘系导购研发模式"><a href="#淘系导购研发模式" class="headerlink" title="淘系导购研发模式"></a>淘系导购研发模式</h2><p><a name="nCS3J"></a></p><h3 id="中台化"><a href="#中台化" class="headerlink" title="中台化"></a>中台化</h3><p>在淘系，随着中台化战略的成熟与导购侧近几年的发展，导购业务的开发工作由独立开发各种能力向中台化支持转变。业务所需要的绝大部分能力均可以由中台提供。</p><p>这么做带来的好处是显而易见的。大部分常见的导购业务，都可以通过中台能力的组装从而实现快速上线，避免重复开发带来的人力物力的浪费。如下图所示，此时在哇哦视频，后端绝大多数的工作在于调用中台的在线服务与离线服务，编写相关的业务逻辑来完成相关需求。</p><p><img src="https://gw.alicdn.com/tfs/TB1mALglND1gK0jSZFKXXcJrVXa-1354-349.png#align=left&amp;display=inline&amp;height=175&amp;name=image.png&amp;originHeight=349&amp;originWidth=1354&amp;search=&amp;size=38172&amp;status=done&amp;width=677" alt="image.png"><br /><br><br /></p><p><a name="MzTRZ"></a></p><h3 id="工作流"><a href="#工作流" class="headerlink" title="工作流"></a>工作流<br /></h3><p>在淘系导购业务现今的开发中，一般都由一位前端搭配一位后端一起完成，每个需求的开发，都需要遵循开发 + 联调 + 测试的完整流程去进行。</p><p>而我也针对于哇哦视频最近的几次需求开发做了时间的分析，具体结果如下图所示：</p><p><img src="https://gw.alicdn.com/tfs/TB1B0TclUz1gK0jSZLeXXb9kVXa-1754-473.png#align=left&amp;display=inline&amp;height=237&amp;name=image.png&amp;originHeight=473&amp;originWidth=1754&amp;search=&amp;size=73228&amp;status=done&amp;width=877" alt="image.png"></p><p>后端同学得益于中台能力的完善支持，许多代码可以复用，因此开发工作量会小一些。而前端则由于 UI 开发的特性，许多东西需要推倒重来，难以复用（首页改版，整体样式都换了），所以工作量会稍微大一些。</p><p>这一套流程实际上已经相当成熟，但成熟并不代表完美。实际上开发过程中，痛点还是有很多的。</p><p><a name="Cl2Cv"></a></p><h2 id="研发模式痛点"><a href="#研发模式痛点" class="headerlink" title="研发模式痛点"></a>研发模式痛点</h2><p><a name="uKQ2e"></a></p><h3 id="联调成本过高"><a href="#联调成本过高" class="headerlink" title="联调成本过高"></a>联调成本过高</h3><p>首当其冲的痛点则是联调。在联调期中前后端需要不断对数据字段、业务逻辑进行确认，从而确保需求实现的正确性，而这种密集的沟通所带来的成本是非常高的。</p><p>如下图所示，在业务开发中我们发现联调成本一般要占到开发成本的 30% 左右。</p><p><img src="https://img.alicdn.com/tfs/TB1l.zhlKH2gK0jSZJnXXaT1FXa-1352-381.png#align=left&amp;display=inline&amp;height=191&amp;name=image.png&amp;originHeight=381&amp;originWidth=1352&amp;search=&amp;size=62979&amp;status=done&amp;width=676" alt="image.png"></p><p>居高不下的联调成本，一方面使得工程师们精疲力尽，另一方面也不利于业务的快速迭代。</p><p><a name="Ksjqh"></a></p><h3 id="前端资源化"><a href="#前端资源化" class="headerlink" title="前端资源化"></a>前端资源化</h3><p>值得一提还有前端资源化的痛点。</p><p>在目前前后端的分工模式中，前端只负责交互逻辑与相对应的 UI 实现，对于业务核心逻辑无需过多了解。虽然这使得前端团队可以快速完成某些业务，但同样也带来了前端资源化的隐患。而在强调前端要深入业务，具有商业化思考能力的今天，前端资源化实际上是不利于前端的自身发展的。</p><p>因为很多时候前端想去深入业务，想进一步升级自己的能力，但往往会苦于没有相关场景。至于说介入后端的工作领域，毕竟术业有专攻，很多事情也掺和不进去。</p><p><a name="I9W90"></a></p><h2 id="遇见-FaaS"><a href="#遇见-FaaS" class="headerlink" title="遇见 FaaS"></a>遇见 FaaS</h2><p>吐槽归吐槽，但是工作还是要继续的。既然每天的工作有这么多痛点，那么是否有办法去尝试解决它呢？</p><p>恰好今年的四月份我开始参与淘宝导购体系 Node FaaS 相关建设的工作，开始接触到一些 Serverless &amp; FaaS 相关的工作。在经过一段时间的基础建设后，我们需要一个业务作为试点业务来检验工作成果。</p><p>出于对<strong>自身能力升级的渴望</strong>，我主动梳理与分析了当前业务的特性，并且主动要求将哇哦视频作为 Node FaaS 的首个试点业务。</p><blockquote><p>哇哦视频后端分析：</p><p>哇哦视频是一个主打纯视频的导购业务，流量高。基于对后端代码与日常需求的剖析，总结其特点为：<strong>运营位多、强依赖算法推荐、数据源多、无状态服务</strong>  四点。<br>其中运营位多 + 强依赖算法推荐的特性，使得业务具有一定的复杂度，改造工作量主要在于理解业务规则，填充数据。</p></blockquote><blockquote><p>而数据源多则代表其引用的外部服务较多，如视频服务、话题、特斯拉资源位定投等。该部分工作量主要在于熟悉上下游系统。</p></blockquote><blockquote><p>最后无状态服务是改造 FaaS 的大利好消息，无状态则意味着横向拓展极其便利，完美契合 FaaS 的工作场景。（其他导购业务应该也类似）</p></blockquote><blockquote><p>总结：哇哦视频复杂度适中，无状态的业务模型十分契合 FaaS 的业务场景，且个人在通读完代码后，有把握能 Hold 住整个后端业务迁移 FaaS 的需求。因此我认为哇哦视频迁移 FaaS 平台是具有高可行度的。</p></blockquote><p><br />非常顺利，也没有任何波折的，哇哦视频成为了淘系首个 Node FaaS 试点业务。怀揣着对于能力升级的渴望，我开始尝试将现有的业务逻辑迁移至 Node FaaS 实现。<br /><br><br />期望达到的效果如下图所示：<br /><br><br /><img src="https://img.alicdn.com/tfs/TB1LVTelSf2gK0jSZFPXXXsopXa-1610-554.png#align=left&amp;display=inline&amp;height=277&amp;name=image.png&amp;originHeight=554&amp;originWidth=1610&amp;search=&amp;size=90325&amp;status=done&amp;width=805" alt="image.png"><br /></p><p><a name="hWaI8"></a></p><h2 id="迁移进行中·"><a href="#迁移进行中·" class="headerlink" title="迁移进行中·"></a>迁移进行中·</h2><p>在正式进行迁移前，我和业务方沟通了这个事情对于业务可能产生的影响以及后续规划。业务方对于技术侧的改造是没有意见的，只有一个诉求，那就是业务不受影响。</p><p>整个诉求看似简单，拆解下来包括以下三部分：</p><ul><li>不会为技术侧改造预留时间，原定需求要按时完成</li><li>迁移后线上不能出任何问题，线上对迁移无感知</li><li>后端工作交接至前端后，对后续需求推进无影响</li></ul><p>说起来就是既要快，又要稳，还要能扛住后续需求。</p><p>针对这个诉求与当时的实际情况，我采取了以下三个措施，来保障整个迁移对业务侧没有影响：</p><ul><li>快速 Copy Java 代码上线</li><li>使用 Java 兜底，保障线上稳定性</li><li>谨慎评估后续需求，确保需求可实现</li></ul><p><a name="c5whq"></a></p><h3 id="Copy-Java-代码上线"><a href="#Copy-Java-代码上线" class="headerlink" title="Copy Java 代码上线"></a>Copy Java 代码上线</h3><p>Copy &amp; Paste 听起来像是不光彩的事情，但这并不是一件需要遮遮掩掩的事情。相反我现在还很庆幸自己在迁移刚开始时选择了这样的方式，而没有愣头青一样选择另起炉灶，从零开始。毕竟学会跑之前得先学会走路。</p><p>前面也提过，哇哦视频是一个大流量导购业务。即使诸多能力已经中台化，但必要的胶水代码 + 相关的业务逻辑代码，总行数也高达 5000 行左右。<br />选择从零开始固然炫酷，但是这样难以保障代码的稳定性，毕竟原有的业务代码不仅包含必要的业务逻辑，也包含了诸多的错误处理与边界处理，而技术侧改造是必须要考虑到稳定性问题的。</p><p>且对于原有 Java 代码的 Copy 也算是一种另类的学习方式了，在这个过程中对于 Java 开发也有了足够的了解，毕竟在整个集团都是 Java 技术栈的情况下，对于 Java 的学习与了解非常有利于后续工作的开展。</p><p>非常幸运的是，后端同学的代码质量很高，该有的注释一个不缺（如下图所示），因此整个读代码 &amp; Copy 的过程非常流畅。</p><p><img src="https://gw.alicdn.com/tfs/TB1cu2glKL2gK0jSZFmXXc7iXXa-1516-928.png#align=left&amp;display=inline&amp;height=464&amp;name=image.png&amp;originHeight=928&amp;originWidth=1516&amp;search=&amp;size=181390&amp;status=done&amp;width=758" alt="image.png"></p><p>也因此在后续 FaaS 版本的哇哦视频提测时，是 0 BUG 直接上线的，节约了大量的返工时间，从而避免对业务需求造成影响。</p><p><a name="1pTbn"></a></p><h3 id="使用-Java-兜底"><a href="#使用-Java-兜底" class="headerlink" title="使用 Java 兜底"></a>使用 Java 兜底</h3><p>这其实算是一个开发中的小 Tricks 了，但却足够好用。</p><p>在之前的导购研发中，为了避免后端宕机对线上带来的影响，因此网关层做了一个 CDN 容灾方案，如下图所示：</p><blockquote><p>注释：</p><ul><li>XCtrl - 前端调用 mtop SDK</li><li>TCE - 淘宝导购网关</li></ul></blockquote><p><img src="https://img.alicdn.com/tfs/TB1qt2dlNz1gK0jSZSgXXavwpXa-720-450.png#align=left&amp;display=inline&amp;height=466&amp;name=image.png&amp;originHeight=450&amp;originWidth=720&amp;search=&amp;size=103009&amp;status=done&amp;width=746" alt="image.png"><br /><br><br />对于前端同学而言，当请求线上接口失败时，前端的请求 SDK 就会根据当前请求数据，去 CDN 上获取最近成功的数据，从而确保对于用户端产品是可用的。<br />但目前导购侧的业务基本都接入了千人千面的算法，而 CDN 容灾的一个缺点便在于只是随机取一份成功数据存入 CDN，并不支持千人千面。<br /><br><br />非常不妙的是，在我迁移 FaaS 时，底层能力还相对羸弱，时不时会有宕机等问题，这时候即使有 CDN 容灾能保障产品可用，但用户侧的体验依然是有一定损失的，属于有损降级。<br />而此时其实产品需求并未发生较大的变更，原有的 Java 接口也能继续使用，因此灵机一动准备将 Java 作为兜底的数据源，确保在降级的请求用户体验是完整的。<br /><br><br />整体思路其实非常简单，请求路径整理如下：</p><ul><li>之前的：Java 接口 - CDN 容灾</li><li>现在：FaaS 接口 - Java 接口 - CDN 容灾</li></ul><p><br />得益于这种设计，哇哦视频在上线后，顽强的活了下来。即使那段时间底层时常不稳定，但对于用户体验来说并没有多少损失（两个接口 RT 都很短，请求两次基本无感）。</p><p><a name="9QOwi"></a></p><h2 id="迁移之后"><a href="#迁移之后" class="headerlink" title="迁移之后"></a>迁移之后</h2><p>在完成代码迁移之后，便开始筹备上线的事情。上线的过程中倒是没有什么故事可以说，波澜不惊的按照既定节奏进行灰度、放量，慢慢的也就上线了。</p><p>在整个业务真正交接到自己手中的时候，我开始遇到了真正的麻烦。</p><p><a name="hoosn"></a></p><h3 id="这个需求我该怎么做？"><a href="#这个需求我该怎么做？" class="headerlink" title="这个需求我该怎么做？"></a>这个需求我该怎么做？</h3><p>随着技术侧改造的完成，业务交给我的新需求也得继续推进，于是迷迷蒙蒙的去参加了很多场业务需求会，接触了很多自己之前作为前端根本不会接触的方面。</p><p>但事情的进展并不顺利，自己刚转型成后端，很多事情都是迷迷糊糊的，似懂非懂。于是那段时间每天最大的疑惑就是：“这个需求我该怎么做？”虽说导购业务都是调用中台，但是一个需求到我手上，哪些应该调中台，哪些应该自己完成我都是不清晰的。</p><p>于是那段时间整个人的工作都变的非常拘谨，开始主动 Push 自己去学习和了解更多业务知识，了解更多后端的业务中台。整个人面对需求，进入了一种“谨慎评估”的状态。</p><p>遇到需求，首先做的不是一口答应承接下来，而是和业务确定具体要做的事情，然后拆分需求。根据业务方的指引与自己的认识，开始不断找各个对应的后端同学去学习和了解完成需求的方式。我记得有好几个下午，我都坐在之前哇哦视频后端同学的身边，不断询问和学习着后端完成问题的思路。</p><p>逐渐的，自己的状态从 “这个需求我该怎么做” 开始向 “这个需求我觉得应该这么做” 转变，整个人面对后端的工作状态从手忙脚乱像游刃有余转变。（其实这也算能力升级吧~毕竟可以 Hold 住更多的事情了）</p><p><a name="JOa1S"></a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在整个迁移的过程中，个人最深刻的感受便是“撕裂”。因为 Serverless &amp; FaaS 并不仅仅只是一种编程方式，它更多的是给了你去 Owner 业务的机会。</p><p>而为了把握住这个机会，你需要或主动或被动的去 Push 自己学非常多的东西，也需要思考比之前多的场景，比如：</p><ul><li>业务的完整链路</li><li>业务需求与最终目标的关系</li><li>后端的工作方式</li><li>中间件、数据库、运维……</li><li>……</li></ul><p>不断的学习新东西，不断的思考更多，不断的对原有自己造成更大的冲击。如果要给我迁移 FaaS 期间的感受下一个总结，那么一定是：“在撕裂中成长”。</p><p>回到我们最初的疑惑，我想我可以对第一个问题进行解答了：</p><ul><li>Q：FaaS 在业务中能落地吗？</li><li>A：能，虽然过程很辛苦，但现在你们落地应该会好很多，因为坑都被我们填的七七八八了</li></ul><p>而关于第二个问题：“FaaS 能帮助前端同学实现能力升级吗？”，我想看完全文的你，心中已经有了答案。这儿也给出我的答案：</p><ul><li>Q：FaaS 能帮助前端同学实现能力升级吗？</li><li>A：能，且能力升级并不止于技术，更多的是业务思维的成长。FaaS 使得前端有机会可以更深入业务，从而更好的去支持业务。技术能力与业务思维共成长，非凡不止一面。</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;a name=&quot;ijr6D&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;作为一位技术同学，我相信你对于今年集团内部 Serverless 与 FaaS 的热
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>TS 3.7 新增的超甜“语法糖”：Optional Chaining &amp; Nullish coalescing operator</title>
    <link href="https://blog.lxxyx.cn/2019/09/30/2019/ts3-7/"/>
    <id>https://blog.lxxyx.cn/2019/09/30/2019/ts3-7/</id>
    <published>2019-09-30T17:43:55.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p><a name="c7UxF"></a></p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>如果说近期我最关注的的事情，那么无疑是 TS 3.7 的更新。因为在官方的迭代计划中，其包含了众多令人期待的新功能。</p><p>其中在 TS 语言层面的更新令我尤为激动（具体的改动可以看下面的链接）</p><ul><li><a href="https://github.com/microsoft/TypeScript/issues/16">Optional Chaining</a></li><li><a href="https://github.com/microsoft/TypeScript/issues/26578">Nullish Coalescing</a></li><li><a href="https://github.com/microsoft/TypeScript/pull/32695">Assertion Signatures</a></li><li><a href="https://github.com/microsoft/TypeScript/pull/33050">Recursive Type References</a></li><li><a href="https://github.com/Microsoft/TypeScript/pull/30829">ECMAScript Private Fields</a></li><li><a href="https://github.com/microsoft/TypeScript/issues/25988">Top-Level <code>await</code></a></li></ul><p>而今天我想介绍的，就是 TS 3.7 中个人认为最甜的语法糖：“Optional Chaining &amp; Nullish coalescing operator”</p><p><img src="https://gw.alicdn.com/tfs/TB1ZXjol.D1gK0jSZFGXXbd3FXa-820-456.png#align=left&amp;display=inline&amp;height=415&amp;name=image.png&amp;originHeight=456&amp;originWidth=820&amp;search=&amp;size=120741&amp;status=done&amp;width=746" alt="image.png"></p><p><a name="0PmlD"></a></p><h2 id="Optional-Chaining"><a href="#Optional-Chaining" class="headerlink" title="Optional Chaining"></a>Optional Chaining</h2><p>这个 Feature 于我而言，是相当有用的特性。</p><p>因为我相信，大部分的开发者都和我一样，为了保证能正确的取到值与代码不报错。写过这样的代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> res = <span class="title function_">getData</span>();</span><br><span class="line"><span class="keyword">const</span> videoCover = res &amp;&amp; res.<span class="property">data</span>[<span class="number">0</span>] &amp;&amp; res.<span class="property">data</span>[<span class="number">0</span>].<span class="property">video</span> &amp;&amp; res.<span class="property">data</span>[<span class="number">0</span>].<span class="property">video</span>.<span class="property">cover</span>;</span><br></pre></td></tr></table></figure><p>虽然可以通过一些类似于 lodash.get/ts-optchain 的工具去解决这个问题，但总归是需要考虑包体积与兼容性的问题。</p><p>而且这个 Feature，其实已经在 C#/Swift/Kotllin/… 等主流语言存在多时，属于常见功能。<br />于是就有人在 TC 39 发起了  <a href="https://github.com/tc39/proposal-optional-chaining">proposal-optional-chaining</a>  的提案，时至今日已经是处于 Stage 3 状态，即将合并进入 ECMAScript。</p><p><a name="iEBEq"></a></p><h3 id="Syntax"><a href="#Syntax" class="headerlink" title="Syntax"></a>Syntax</h3><p>这个语法糖可以说是相当简洁易懂了，如下面的代码所示。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">obj?.<span class="property">prop</span>; <span class="comment">// optional static property access</span></span><br><span class="line">obj?.[expr]; <span class="comment">// optional dynamic property access</span></span><br><span class="line">func?.(...args); <span class="comment">// optional function or method call</span></span><br></pre></td></tr></table></figure><p>你可以通过 ?. 的操作符，实现可选链式调用，其中包括以下功能：</p><ul><li>读取属性</li><li>读取动态属性</li><li>执行函数</li></ul><p>基本上 Cover 到了我们日常使用的情况。</p><p><a name="HZvev"></a></p><h3 id="Demo"><a href="#Demo" class="headerlink" title="Demo"></a>Demo</h3><p>举个 🌰，可以看下面的获取属性 Demo，不难看出获取属性从此可以变得非常简洁：<br /></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> nestedProp = obj.<span class="property">first</span> &amp;&amp; obj.<span class="property">first</span>.<span class="property">second</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ===&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> nestedProp = obj.<span class="property">first</span>?.<span class="property">second</span>;</span><br></pre></td></tr></table></figure><blockquote><p>通过使用<code>?.</code>操作符取代<code>.</code>操作符。JavaScript 知道在尝试访问<code>obj.first.second</code>之前先明确的校验并确定<code>obj.first</code>t 是非<code>null</code>且非<code>undefined</code>。如果<code>obj.first</code>是<code>null</code>或<code>undefined</code>，表达式将会短路计算直接返回<code>undefined</code>。</p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/可选链">MDN - Optional Chaining</a></p></blockquote><p><br /><br><a name="fjjXt"></a></p><h2 id="Nullish-coalescing-operator"><a href="#Nullish-coalescing-operator" class="headerlink" title="Nullish coalescing operator"></a>Nullish coalescing operator</h2><p>另一个值得关注的 Feature 是 <a href="proposal-nullish-coalescing">Nullish Coalescing</a>，同样也是进入了 stage 3。</p><p>这是与  <a href="https://github.com/tc39/proposal-optional-chaining">proposal-optional-chaining</a>  十分搭配的 feature，通过 ?? 来表示默认值的功能。（感觉后续会看见 JS 中遍地都是 ?????? 的场景。。）</p><p>Nullish coalescing 的用法和 JS 的默认值语法：“||”非常像，但还是有一些本质的不同。</p><p>“||” 语法在进行判断时，实际上是基于 JS 的 <a href="https://developer.mozilla.org/zh-CN/docs/Glossary/Truthy">Truthy</a> 特性去判断的。隐式类型转换带来的问题，代表着在 “0 || 1 ”之类的场景下，实际上可能是不符合用户期望的。</p><p>而 “??” 的判断条件则更为严谨，我们称之为 <a href="https://v8.dev/features/nullish-coalescing">nullish</a>。nullish 实际上指的是这个值与 null/undefined 严格相等，因此与  Optional Chaining 可以说是相当搭配了。</p><p><a name="VShe9"></a></p><h3 id="Demo-1"><a href="#Demo-1" class="headerlink" title="Demo"></a>Demo</h3><p>举个 🌰，可以通过下面的示例了解这个 feature。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="literal">false</span> ?? <span class="literal">true</span>; <span class="comment">// =&gt; false</span></span><br><span class="line"><span class="number">0</span> ?? <span class="number">1</span>; <span class="comment">// =&gt; 0</span></span><br><span class="line"><span class="string">&#x27;&#x27;</span> ?? <span class="string">&#x27;default&#x27;</span>; <span class="comment">// =&gt; &#x27;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="literal">null</span> ?? []; <span class="comment">// =&gt; []</span></span><br><span class="line"><span class="literal">undefined</span> ?? []; <span class="comment">// =&gt; []</span></span><br></pre></td></tr></table></figure><p><a name="Ekzb0"></a></p><h3 id="真甜"><a href="#真甜" class="headerlink" title="真甜"></a>真甜</h3><p>上面两个 feature 总结下来，非常符合语法糖的特性：“使用简单，用户友好”。</p><p>而同时在 TC39 的 <a href="https://github.com/tc39/proposals">Proposals</a> 中，我们可以看到许多类似的提案正在不断的被提出，不断的在简化着我们的编码：</p><ul><li><a href="https://www.npmjs.com/package/globalthis">globalThis</a></li><li><a href="https://github.com/tc39/proposal-class-fields">Private fields</a></li><li>……</li></ul><p>不得不说，虽然语法糖有一定的学习成本，但是：</p><p><img src="https://gw.alicdn.com/tfs/TB1nP5OlV67gK0jSZPfXXahhFXa-500-313.png#align=left&amp;display=inline&amp;height=157&amp;name=image.png&amp;originHeight=313&amp;originWidth=500&amp;search=&amp;size=263939&amp;status=done&amp;width=250" alt="image.png"></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;a name=&quot;c7UxF&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;如果说近期我最关注的的事情，那么无疑是 TS 3.7 的更新。因为在官方的迭代计划中
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>ECMAScript Modules 在 Node.js 中的支持与使用</title>
    <link href="https://blog.lxxyx.cn/2019/08/11/2019/es-modules/"/>
    <id>https://blog.lxxyx.cn/2019/08/11/2019/es-modules/</id>
    <published>2019-08-11T16:37:04.000Z</published>
    <updated>2023-01-02T12:32:51.305Z</updated>
    
    <content type="html"><![CDATA[<p>2019 年的 4 月份，Node.js 官方团队在发布 Node.js 12 时，也给我们带来了最新的  <a href="https://medium.com/@nodejs/announcing-a-new-experimental-modules-1be8d2d6c2ff">ECMAScript Modules 支持</a>。</p><p>首先我们需要明确的是，<strong>ECMAScript Modules 在现在已经不是什么新鲜事了</strong>。<br />早在 ES6 规范推出时，我们通过 Babel/TypeScript 等工具便已能在项目中使用该 Feature，那为什么我们还需要关注该 Feature 在 Node.js 上的实现与具体使用呢？</p><p>答案是明确的，因为  ECMAScript Modules 在 Node.js 规范中的实现与使用，实际上与现今 Babel/TypeScript 的使用是有较大的区别的。</p><p>关于这一点，我想从  Babel/TypeScript 当时的设计思路上去分析。</p><p><a name="D39y2"></a></p><h2 id="Babel-TypeScript-的设计思路"><a href="#Babel-TypeScript-的设计思路" class="headerlink" title="Babel/TypeScript 的设计思路"></a>Babel/TypeScript 的设计思路</h2><p>首先我们看一下 Babel/TypeScript 的 Slogan：</p><ul><li>Babel：Babel is a JavaScript compiler：Use next generation JavaScript, today.</li><li>TypeScript：TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.</li></ul><p>从两个产品的 Slogan 上不难看出，Babel 专注于通过编译，在现在的 JS 引擎中使用最新的 JS Feature。而 TS 则是通过编译，实现静态类型的校验等。</p><p>而这两者的最终产物都受限于当前 JS 引擎的能力，也就是说 Babel 和 TypeScript 并不能凭空模拟出之前 JS 引擎尚未支持的 Feature。</p><p>这一点非常重要，因为在 Babel 与 TypeScript 对  ECMAScript Modules 时，实际上是编译成 Node.js 所支持的 <a href="https://nodejs.org/docs/latest/api/modules.html">CommonJS </a>规范，从而使得最终产物可以在 Node.js 上运行。</p><p>也就是说，当时我们的使用方式，其实是遵循了 CommonJS 的规范的，只是写法上是  ECMAScript Modules 而已。且由于底层缺失对于  ECMAScript Modules 的强约束（因为还不存在），所以大家的写法上也都是五花八门的，只能最终编译成 CommonJS 时能运行即可。</p><p>而 Node.js 12 的这个 Feature，则对  ECMAScript Modules 的开发与使用做了强约束，所以在正式开始使用该规范前，我们还是需要对其有一定的了解的。</p><p><a name="i0OA3"></a></p><h2 id="启用-Feature"><a href="#启用-Feature" class="headerlink" title="启用 Feature"></a>启用 Feature</h2><p>通过  –experimental-modules ，我们可以在 Node.js 中启用该 Feature。</p><p>当设置该选项时，Node.js 便会以 ECMAScript Modules 的方式去解析 JS 并运行，在这儿值得注意的是，在新模式下，文件的后缀与解析规则也发生了变更。</p><p>在该 Feature 下，文件分为了以下几种后缀：</p><ul><li>.mjs：ECMAScript Modules 模式，使用 import/export</li><li>.cjs：CommonJS 模式，也就是原有的 Node.js 模块解析方式</li><li>.js：在  ECMAScript Modules 模式下，如果 package.json 中 的 type 字段为 module 时（后文会提及），则该文件会被认为是符合 ES Module 规范的文件。</li></ul><p><a name="5pUeI"></a></p><h2 id="通过-package-json-区分模块类型"><a href="#通过-package-json-区分模块类型" class="headerlink" title="通过 package.json 区分模块类型"></a>通过 package.json 区分模块类型</h2><p>ECMAScript Modules 由于具体实现上与之前的 CommonJS 有较大区别，因此在使用时是需要对两种情况进行区分的。而官方则提供了一种简单有效的方式，那就是通过 package.json 中的 type 字段。</p><p>该字段主要影响该 package 下 .js 后缀的解析，而新增的 .cjs/.mjs 后缀则从文件名上已经做了类型区分，Node 会根据后缀切换不同的解析方式。</p><p>在规范中，package.json 的 type 字段一共有两种值，”module” 与 “commonjs”，而当 type 字段不存在时则默认使用 “commonjs” 来适应原有规范。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// package.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;module&quot;</span> | <span class="string">&quot;commonjs&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>而在具体使用时，当导入项目中的 js 时，根据 type 的值，会有以下两种情况：</p><ul><li>module：以  ECMAScript Modules 模式解析</li><li>commonjs：以 commonjs 的方式解析</li></ul><p>通过这种设计，我们可以非常方便的实现对原有代码的兼容，且 CommonJS 与 ES Modules 之间也能互相引用，Node.js 会处理好运行时的一切。</p><p>至于解析的例子，大家可以看下面的代码：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// package.json 中 &quot;type&quot; 为 &quot;module&quot;.</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 以 ECMAScript Modules 解析与加载</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;./startup/init.js&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 以 CommonJS 加载，因为 ./node_modules/commonjs-package/package.json</span></span><br><span class="line"><span class="comment">// 缺乏 &quot;type&quot; 字段或者 &quot;type&quot; 为 &quot;commonjs&quot;.</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;commonjs-package&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 以 CommonJS 加载，因为 ./node_modules/commonjs-package/package.json</span></span><br><span class="line"><span class="comment">// 缺乏 &quot;type&quot; 字段或者 &quot;type&quot; 为 &quot;commonjs&quot;.</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;./node_modules/commonjs-package/index.js&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 以 CommonJS 加载，因为 .cjs 后缀即代表该模块遵循 CommonJS 规范.</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;./legacy-file.cjs&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 以 ECMAScript Modules 加载，因为 .mjs 后缀即代表该模块遵循 ECMAScript Modules 规范.</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;commonjs-package/src/index.mjs&#x27;</span>;</span><br></pre></td></tr></table></figure><p><a name="azkwO"></a></p><h2 id="ES-Modules-与-CommonJS-的区别"><a href="#ES-Modules-与-CommonJS-的区别" class="headerlink" title="ES Modules 与 CommonJS 的区别"></a>ES Modules 与 CommonJS 的区别</h2><p>在 Node.js 的实现中，ES Modules 实际上与 CommonJS 的规范在部分细节上已有了较大的区别。这部分差异直接影响到我们书写代码的方式，因此我会具体贴出部分重要改动。</p><p><a name="29Ar8"></a></p><h3 id="导入模块时需要提供文件拓展名"><a href="#导入模块时需要提供文件拓展名" class="headerlink" title="导入模块时需要提供文件拓展名"></a>导入模块时需要提供文件拓展名</h3><p>在 CommonJS 时代，我们在导入模块时无需书写文件后缀，而是由 Node.js 自行通过 extensions 来加载指定文件。如  import ‘index’  在 Node.js 中实际上会加载 index.js，Node 会帮忙自动尝试加载该文江。<br />而在 ES Modules 规范下，导入一个模块时，我们需要提供确切的文件拓展名。</p><p>这一点虽然对比现在的方案缺失了灵活性，但却使得整体模块的依赖关系可以在编译时就确定，而不需要等到运行时。这是符合  ES Modules 的设计初衷的。</p><p><a name="I2w7X"></a></p><h3 id="require-exports-module-exports-filename-dirname"><a href="#require-exports-module-exports-filename-dirname" class="headerlink" title="require, exports, module.exports, filename, dirname"></a>require, exports, module.exports, <strong>filename, </strong>dirname</h3><p>Node 在实现  CommonJS 规范时，实际上给每一个文件都做了包裹，传入了以上的这些变量，从而使得在代码中可以使用 require/exports 等方式实现模块化。</p><p>而<strong>在 ES Modules 规范下，这些都将不复存在</strong>。这一点对于原有的代码而言，是一个非常大的变更。<br />这也就是为什么在 Babel/TypeScript 等工具体系下，明明可以使用 ES Modules 进行开发了，还需要关注 Node 具体实现的原因，因为之前的代码强依赖于这些变量，在新规范下必须进行修改才能继续使用。</p><p>然而这些都是 Node 运行的基础，总不能一下子就没有了吧？答案是确定的，这些变量在 ES Modules 规范下的使用方式，Node 官方也给出了具体的方案：</p><p>比如 require，可以通过 <a href="https://nodejs.org/dist/latest-v12.x/docs/api/modules.html#modules_module_createrequire_filename">module.createRequire()</a> 方法使用。</p><p>又比如  <strong>filename 与 </strong>dirname：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; fileURLToPath &#125; <span class="keyword">from</span> <span class="string">&#x27;url&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; dirname &#125; <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> __filename = <span class="title function_">fileURLToPath</span>(<span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>);</span><br><span class="line"><span class="keyword">const</span> __dirname = <span class="title function_">dirname</span>(__filename);</span><br></pre></td></tr></table></figure><p><a name="p3HpY"></a></p><h4 id="import-meta"><a href="#import-meta" class="headerlink" title="import.meta"></a>import.meta</h4><p>这儿其实有一个小知识，那就是关于 import.meta 的。<br />在 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import.meta">MDN 的文档</a>中，解释如下：</p><blockquote><p><code>import.meta</code>是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息，比如说这个模块的 URL。</p></blockquote><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="keyword">import</span>.<span class="property">meta</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 输出</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="attr">url</span>: <span class="string">&#x27;file:///home/user/my-module.mjs&#x27;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a name="1uQEF"></a></p><h3 id="没有-require-extensions-与-require-cache"><a href="#没有-require-extensions-与-require-cache" class="headerlink" title="没有 require.extensions 与 require.cache"></a>没有 require.extensions 与 require.cache</h3><p>在 ES Modules 规范中，require.extensions 与 require.cache 将不再被使用。</p><p><a name="EmNei"></a></p><h3 id="基于-URL-的文件路径"><a href="#基于-URL-的文件路径" class="headerlink" title="基于 URL 的文件路径"></a>基于 URL 的文件路径</h3><p>在文件路径上，ES Modules 的解析与缓存是基于 <a href="https://url.spec.whatwg.org/">URL </a>规范的。<br />这也就意味着，模块实际上是可以携带查询参数的，且当查询参数不同时，Node 会重新加载该模块。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&#x27;./foo.mjs?query=1&#x27;</span>; <span class="comment">// loads ./foo.mjs with query of &quot;?query=1&quot;</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;./foo.mjs?query=2&#x27;</span>; <span class="comment">// loads ./foo.mjs with query of &quot;?query=2&quot;</span></span><br></pre></td></tr></table></figure><p><a name="OiFhK"></a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在此需要特别提及的是，目前 Node.js 所提供的  ECMAScript Modules 规范并非是最终解，其具体实现与诸多技术细节未来也可能进行一定的调整。<br />比如关于 CommonJS 与 ES Modules 的互相调用，实际上是还没有完全确定下来的（因此我这儿也没有特别去阐述如何使用）。也因此在 Node.js 的文档中，ECMAScript Modules 规范的稳定性等级还是 1，属于  Experimental 。</p><p>而个人对于  ECMAScript Modules 规范态度，总体是看好的。强有力的约束有利于 Node.js 去做更多的优化，统一的模块规范则避免了浏览器与 Node.js 生态的进一步割裂。虽然过程是曲折，但前途却充满了光明。</p><p><a name="hf4hP"></a></p><h3 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h3><ul><li><a href="https://nodejs.org/dist/latest-v12.x/docs/api/esm.html">ECMAScript Modules</a> - Node.js 官方文档</li><li><a href="https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md">Plan for New Modules Implementation</a> - Node.js 规划</li><li><a href="https://2ality.com/2019/04/nodejs-esm-impl.html">The new ECMAScript module support in Node.js 12</a> - 2ality</li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import.meta">import.meta</a> - MDN</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;2019 年的 4 月份，Node.js 官方团队在发布 Node.js 12 时，也给我们带来了最新的  &lt;a href=&quot;https://medium.com/@nodejs/announcing-a-new-experimental-modules-1be8d2d6c
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>从改变加载路径开始，一探 Node 的模块加载机制</title>
    <link href="https://blog.lxxyx.cn/2019/07/31/2019/modules-load/"/>
    <id>https://blog.lxxyx.cn/2019/07/31/2019/modules-load/</id>
    <published>2019-07-31T00:54:25.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<p><a name="968aa9a1"></a></p><h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>最近因为工作等原因，开始接触到 ncc 这样的 Node 打包方案，而在阅读其源代码的时候，看到了如下的一段代码，用来引入 Typescript 模块的，代码不长，但是很有意思。</p><p>代码如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; <span class="title class_">Module</span> &#125; = <span class="built_in">require</span>(<span class="string">&#x27;module&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> m = <span class="keyword">new</span> <span class="title class_">Module</span>(<span class="string">&#x27;&#x27;</span>, <span class="literal">null</span>);</span><br><span class="line">m.<span class="property">paths</span> = <span class="title class_">Module</span>.<span class="title function_">_nodeModulePaths</span>(process.<span class="title function_">cwd</span>() + <span class="string">&#x27;/&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> typescript;</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">  typescript = m.<span class="built_in">require</span>(<span class="string">&#x27;typescript&#x27;</span>);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;ncc: Using typescript@&#x27;</span> + typescript.<span class="property">version</span> + <span class="string">&#x27; (local user-provided)&#x27;</span>);</span><br><span class="line">&#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">  typescript = <span class="built_in">require</span>(<span class="string">&#x27;typescript&#x27;</span>);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;ncc: Using typescript@&#x27;</span> + typescript.<span class="property">version</span> + <span class="string">&#x27; (ncc built-in)&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = typescript;</span><br></pre></td></tr></table></figure><p>整个代码的运行规则比较简单，当本地存在 TypeScript 模块时，则引入本地的，否则引用当前项目中内置的  TypeScript。<br />仔细想想，这其实在开发工具时是一个非常常见的需求，部分涉及到运行时的依赖，框架在调用时希望优先引用用户本地的版本，本地不存在则引用内置或者全局安装的版本。</p><p>而我则对其原理产生了好奇，自己之前也没有见过 module 模块，更没有见过这种可以自由改变模块加载时搜索路径的用法。</p><p>本文则是本人的探秘之旅，也仅仅专注于这一个问题上，并不扩散。</p><p><a name="e2f8bf5c"></a></p><h2 id="module-模块的实现"><a href="#module-模块的实现" class="headerlink" title="module 模块的实现"></a>module 模块的实现</h2><p>Node 中，关于 Module 部分的源代码在 <a href="https://github.com/nodejs/node/blob/b04de23afa6da18d7b81b70c1a4bb53476f125c7/lib/internal/modules/cjs/loader.js">lib/internal/modules/cjs/loader.js</a>  中。整个文件的“分量”还是有些重的，代码量高达 1000+ 行。</p><p>直奔主题，从代码中我们不难发现，我们首先初始化了一个 Module 的实例，并通过调用  <code>_nodeModulePaths</code> 的静态方法，设置实例的 paths 为当前目录。</p><p>那么，通过对以下几个要点的探索，我们应该就可以得出想要的答案：</p><ul><li><code>_nodeModulePaths</code> 方法内部的实现</li><li>Module 实例中，paths 变量的作用</li><li>调用  Module 实例的 require 方法，是怎么样引入模块的</li></ul><p><a name="41fc29c7"></a></p><h2 id="nodeModulePaths，引用模块时，逐级查找-node-modules-的秘密"><a href="#nodeModulePaths，引用模块时，逐级查找-node-modules-的秘密" class="headerlink" title="_nodeModulePaths，引用模块时，逐级查找 node_modules 的秘密"></a>_nodeModulePaths，引用模块时，逐级查找 node_modules 的秘密</h2><p>在代码中，我们通过 <code>Module._nodeModulePaths(process.cwd() + &#39;/&#39;)</code> 的方式，来设置 Module 的 <code>paths</code> 变量。</p><p>而这也是一段非常有趣的代码，大家都知道 Node 在定位模块时，是会逐级的向上查找的。而向上查找路径的生成代码，就是这个 <code>_nodeModulePaths</code> 函数。</p><p>代码如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="variable constant_">CHAR_FORWARD_SLASH</span> = <span class="number">47</span>;</span><br><span class="line"><span class="keyword">const</span> nmChars = [<span class="number">115</span>, <span class="number">101</span>, <span class="number">108</span>, <span class="number">117</span>, <span class="number">100</span>, <span class="number">111</span>, <span class="number">109</span>, <span class="number">95</span>, <span class="number">101</span>, <span class="number">100</span>, <span class="number">111</span>, <span class="number">110</span>];</span><br><span class="line"><span class="keyword">const</span> nmLen = nmChars.<span class="property">length</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">_nodeModulePaths</span>(<span class="params"><span class="keyword">from</span></span>) &#123;</span><br><span class="line">  <span class="comment">// Guarantee that &#x27;from&#x27; is absolute.</span></span><br><span class="line">  <span class="comment">// Return early not only to avoid unnecessary work, but to *avoid* returning</span></span><br><span class="line">  <span class="comment">// an array of two items for a root: [ &#x27;//node_modules&#x27;, &#x27;/node_modules&#x27; ]</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">from</span> === <span class="string">&#x27;/&#x27;</span>) <span class="keyword">return</span> [<span class="string">&#x27;/node_modules&#x27;</span>];</span><br><span class="line"></span><br><span class="line">  <span class="comment">// note: this approach *only* works when the path is guaranteed</span></span><br><span class="line">  <span class="comment">// to be absolute.  Doing a fully-edge-case-correct path.split</span></span><br><span class="line">  <span class="comment">// that works on both Windows and Posix is non-trivial.</span></span><br><span class="line">  <span class="keyword">const</span> paths = [];</span><br><span class="line">  <span class="keyword">var</span> p = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">var</span> last = <span class="keyword">from</span>.<span class="property">length</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="keyword">from</span>.<span class="property">length</span> - <span class="number">1</span>; i &gt;= <span class="number">0</span>; --i) &#123;</span><br><span class="line">    <span class="keyword">const</span> code = <span class="keyword">from</span>.<span class="title function_">charCodeAt</span>(i);</span><br><span class="line">    <span class="keyword">if</span> (code === <span class="variable constant_">CHAR_FORWARD_SLASH</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (p !== nmLen) paths.<span class="title function_">push</span>(<span class="keyword">from</span>.<span class="title function_">slice</span>(<span class="number">0</span>, last) + <span class="string">&#x27;/node_modules&#x27;</span>);</span><br><span class="line">      last = i;</span><br><span class="line">      p = <span class="number">0</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (p !== -<span class="number">1</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (nmChars[p] === code) &#123;</span><br><span class="line">        ++p;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        p = -<span class="number">1</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Append /node_modules to handle root paths.</span></span><br><span class="line">  paths.<span class="title function_">push</span>(<span class="string">&#x27;/node_modules&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> paths;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><a href="https://codesandbox.io/s/ecstatic-ganguly-m1n9f">Online Demo</a></p></blockquote><p>调用如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">_nodeModulePaths</span>(<span class="string">&#x27;/Users/lxxyx/playground/node&#x27;</span>));</span><br><span class="line"></span><br><span class="line"><span class="comment">// output</span></span><br><span class="line">[</span><br><span class="line">  <span class="string">&#x27;/Users/lxxyx/playground/node/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/Users/lxxyx/playground/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/Users/lxxyx/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/Users/node_modules&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;/node_modules&#x27;</span>,</span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>可以看到，这个路径是由传入的目录决定，并逐级生成的。</p><p>因此，在此我们可以明确我们的两个疑惑：</p><ul><li><code>_nodeModulePaths</code> 方法内部的实现：通过对传入路径的处理，生成 Node 引用模块时逐级引用的目录，<strong>由于是根据用户传入的变量生成的，因此也会改变引用模块的初始路径</strong></li><li>Module 实例中，paths 变量的作用： Node 引用模块时逐级引用的目录路径</li></ul><p><a name="a223feb0"></a></p><h3 id="require-方法，引入模块的具体实现"><a href="#require-方法，引入模块的具体实现" class="headerlink" title="require 方法，引入模块的具体实现"></a>require 方法，引入模块的具体实现</h3><p>require 方法应该是每个接触 Node 同学最为熟悉的函数之一，其核心作用便是引用模块。</p><blockquote><p>而之前的我却也只是知其然而不知其所以然，只是看些文章，未曾深入的学习与理解，似懂非懂的。</p></blockquote><p>首先我们看像 require 的源代码：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Loads a module at the given file path. Returns that module&#x27;s</span></span><br><span class="line"><span class="comment">// `exports` property.</span></span><br><span class="line"><span class="title class_">Module</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">require</span> = <span class="keyword">function</span> (<span class="params">id</span>) &#123;</span><br><span class="line">  <span class="title function_">validateString</span>(id, <span class="string">&#x27;id&#x27;</span>);</span><br><span class="line">  <span class="keyword">if</span> (id === <span class="string">&#x27;&#x27;</span>) &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title function_">ERR_INVALID_ARG_VALUE</span>(<span class="string">&#x27;id&#x27;</span>, id, <span class="string">&#x27;must be a non-empty string&#x27;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  requireDepth++;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Module</span>.<span class="title function_">_load</span>(id, <span class="variable language_">this</span>, <span class="comment">/* isMain */</span> <span class="literal">false</span>);</span><br><span class="line">  &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    requireDepth--;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>代码相对简单，核心逻辑都在 <code>Module._load</code> 方法中实现了。</p><p><a name="848f15da"></a></p><h2 id="Module-load，引入模块的内部实现"><a href="#Module-load，引入模块的内部实现" class="headerlink" title="Module._load，引入模块的内部实现"></a>Module._load，引入模块的内部实现</h2><p><code>Module._load</code> 方法是一段较长的函数，有五六十行，里面包括了模块的加载、缓存、解析等机制，可以说是引入模块的核心逻辑。</p><p>而 Node 的开发者们，也非常贴心的留下了注释供我们参考：</p><blockquote><p>// Check the cache for the requested file.<br>// 1. If a module already exists in the cache: return its exports object.<br>// 2. If the module is native: call<br>//    <code>NativeModule.prototype.compileForPublicLoader()</code> and return the exports.<br>// 3. Otherwise, create a new module for the file and save it to the cache.<br>//    Then have it load  the file contents before returning its exports<br>//    object.</p></blockquote><p>由于内部逻辑较多，且许多与本次探讨的内容无关，因此关于该方法，我会着重于探索以下几个问题（也是个人比较感兴趣的）：</p><ul><li>缓存逻辑是怎么实现的</li><li>Node 如何加载一个模块，模块中调用的 <code>exports/require</code> 等对象又是从哪儿来的</li><li>加载模块时，如何通过 <code>paths</code> 变量改变引入模块时查找的 node_modules 目录</li></ul><p><a name="b89974c5"></a></p><h3 id="缓存逻辑的实现"><a href="#缓存逻辑的实现" class="headerlink" title="缓存逻辑的实现"></a>缓存逻辑的实现</h3><p>Node 中关于 require 的缓存逻辑比我想的要简单许多。<br />缓存仅仅是一个对象，通过：<code>Module._cache = Object.create(null);</code> 来创建。</p><p>每次加载文件时，当发现这是一个新文件时，则通过唯一的文件名作为 Key，存入该模块的实例。<br />而当重复加载某文件时，则会先检查缓存中是否已存在，有的话则返回已存在的实例。</p><p>代码：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 设置缓存</span></span><br><span class="line"><span class="title class_">Module</span>.<span class="property">_cache</span>[filename] = <span class="variable language_">module</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 从缓冲中取出</span></span><br><span class="line"><span class="keyword">const</span> cachedModule = <span class="title class_">Module</span>.<span class="property">_cache</span>[filename];</span><br><span class="line"><span class="keyword">if</span> (cachedModule !== <span class="literal">undefined</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> cachedModule.<span class="property">exports</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过这也不难发现，如果一个文件被缓存起来了，那么在整个生命周期内，它都是在缓存中的。</p><p>想要更新也很简单，清空一下 cache 对象即可，而我们常用的清除 require.cache 方式，实际上是清空的 Module 的 _cache，这一点在代码中也有所表示：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">require</span>.<span class="property">cache</span> = <span class="title class_">Module</span>.<span class="property">_cache</span>;</span><br></pre></td></tr></table></figure><p><a name="5318d5d0"></a></p><h3 id="如何加载一个-JS-模块"><a href="#如何加载一个-JS-模块" class="headerlink" title="如何加载一个 JS 模块"></a>如何加载一个 JS 模块</h3><p>在 Node 中，加载某个具体的文件是通过 <code>module.load(filename);</code> 方法实现的。</p><p><code>load</code> 方法源码如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Given a file name, pass it to the proper extension handler.</span></span><br><span class="line"><span class="title class_">Module</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">load</span> = <span class="keyword">function</span> (<span class="params">filename</span>) &#123;</span><br><span class="line">  <span class="title function_">debug</span>(<span class="string">&#x27;load %j for module %j&#x27;</span>, filename, <span class="variable language_">this</span>.<span class="property">id</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">assert</span>(!<span class="variable language_">this</span>.<span class="property">loaded</span>);</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">filename</span> = filename;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">paths</span> = <span class="title class_">Module</span>.<span class="title function_">_nodeModulePaths</span>(path.<span class="title function_">dirname</span>(filename));</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> extension = <span class="title function_">findLongestRegisteredExtension</span>(filename);</span><br><span class="line">  <span class="title class_">Module</span>.<span class="property">_extensions</span>[extension](<span class="variable language_">this</span>, filename);</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">loaded</span> = <span class="literal">true</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>在源码中有一行关键的代码：<code>Module._extensions[extension](this, filename);</code>。而这行代码负责引入文件的编译与执行，这也是我们本次想了解地方。</p><p><code>Module._extensions</code> 实际上是一种模块加载的机制，通过定义后缀名与处理方法，来去支持某个文件在 Node 代码中的直接引入。</p><p>JS 代码的处理如下：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Native extension for .js</span></span><br><span class="line"><span class="title class_">Module</span>.<span class="property">_extensions</span>[<span class="string">&#x27;.js&#x27;</span>] = <span class="keyword">function</span> (<span class="params"><span class="variable language_">module</span>, filename</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> content = fs.<span class="title function_">readFileSync</span>(filename, <span class="string">&#x27;utf8&#x27;</span>);</span><br><span class="line">  <span class="variable language_">module</span>.<span class="title function_">_compile</span>(content, filename);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><a name="29269923"></a></p><h3 id="模块的编译与执行"><a href="#模块的编译与执行" class="headerlink" title="模块的编译与执行"></a>模块的编译与执行</h3><p>从代码中不难看出，此处先读取了代码的内容，并且 JS 代码进行了编译操作。</p><p>而在 <code>module._compile</code> 的<a href="https://github.com/nodejs/node/blob/b04de23afa6da18d7b81b70c1a4bb53476f125c7/src/node_contextify.cc#L986">实现</a>中，调用了 C++ ，将其包裹为一个函数：</p><p>比如你书写的代码：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line"></span><br><span class="line">fs.<span class="title function_">readFile</span>(<span class="string">&#x27;./file.txt&#x27;</span>, <span class="string">&#x27;utf-8&#x27;</span>, <span class="function">(<span class="params">err, data</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;data: &#x27;</span>, data);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>将在 compile 后变为：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">(<span class="keyword">function</span> (<span class="params"><span class="built_in">exports</span>, <span class="built_in">require</span>, <span class="variable language_">module</span>, __filename, __dirname</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line">  fs.<span class="title function_">readFile</span>(<span class="string">&#x27;./file.txt&#x27;</span>, <span class="string">&#x27;utf-8&#x27;</span>, <span class="function">(<span class="params">err, data</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;data: &#x27;</span>, data);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>这其中就包括了运行时的一些关键变量，exports/require/__filename 等。<br />执行时则是通过传入这些预先定义或者生成好的变量，<a href="https://github.com/nodejs/node/blob/b04de23afa6da18d7b81b70c1a4bb53476f125c7/lib/internal/modules/cjs/loader.js#L867-L868">来执行并拿到结果</a>：</p><p>而执行结果，就包括 module.exports 变量，可以通过这个变量拿到这个模块导出的所有内容。<br />至此，我们就可以将整个 require 的链路完整的串起来了。</p><p><a name="c09bb1dd"></a></p><h3 id="paths-变量对于引用模块的影响"><a href="#paths-变量对于引用模块的影响" class="headerlink" title="paths 变量对于引用模块的影响"></a>paths 变量对于引用模块的影响</h3><p>回到我们一开始的问题，我们通过定义 paths ，使得 require 的查找路径可以从我们指定的目录开始。<br />那么自然我们也想知道这部分在代码中是如何实现的。</p><p>在代码中，关于这部分的代码有这么几段：</p><ul><li>基于传入的 paths 生成最终需要查找的目录：<a href="https://github.com/nodejs/node/blob/b04de23afa6da18d7b81b70c1a4bb53476f125c7/lib/internal/modules/cjs/loader.js#L692">链接</a></li><li>依据 paths ，逐级查找 npm 包：<a href="https://github.com/nodejs/node/blob/b04de23afa6da18d7b81b70c1a4bb53476f125c7/lib/internal/modules/cjs/loader.js#L413">链接</a></li><li>根据 npm 包中的 main 字段，定位到最终 require 的文件：<a href="https://github.com/nodejs/node/blob/b04de23afa6da18d7b81b70c1a4bb53476f125c7/lib/internal/modules/cjs/loader.js#L212">链接</a></li></ul><p>那么关于这个问题，我想也有明确的答案了。</p><p><a name="7a0a21d5"></a></p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>而此次对于 Node 模块加载机制的深入学习与了解，于我而言是更了解底层实现的一个过程，除此之外感受最深的则是：代码虽然很多，但读起来完全不会觉得累或者绕弯子。看似冗长的变量名，恰到好处的注释，一点也不“优雅”的代码组织方式，恰恰让这段代码比许多代码都要好读。</p><p>现在想想，ES201X / TypeScript / ESLint 等诸多工具，其实与能不能写出好的代码关系不大，从道术器的理论来探讨，工具也只是“器”而已，具体还是要看使用者的道行。</p><p>希望自己将来也可以写出这种“容易看懂”的代码吧~</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;a name=&quot;968aa9a1&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;起因&quot;&gt;&lt;a href=&quot;#起因&quot; class=&quot;headerlink&quot; title=&quot;起因&quot;&gt;&lt;/a&gt;起因&lt;/h2&gt;&lt;p&gt;最近因为工作等原因，开始接触到 ncc 这样的 Node 打包方案，而在阅读其
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>研发体验的思考</title>
    <link href="https://blog.lxxyx.cn/2019/07/09/2019/dev-exp/"/>
    <id>https://blog.lxxyx.cn/2019/07/09/2019/dev-exp/</id>
    <published>2019-07-09T12:23:22.000Z</published>
    <updated>2023-01-02T12:32:51.305Z</updated>
    
    <content type="html"><![CDATA[<p>最开始我以为，好的 UI 就是好的体验，后面发现很多设计精美的网站功能却残缺的厉害。<br />后来我以为，好用的工具就是好的体验，却忽略了很多事情从不只是一个工具就能解决的。<br />前段时间我以为，一个完善的体系就是好的体验，却忽略了工具庞杂所带来的理解与交互成本。</p><p>现在的我认为，好的体验定义从不应该局限于 UI、工具、技术体系。<br />它指的是一种以人为本的产品设计理念，目标是辅助使用者尽可能高效的去理解庞杂的系统并自然完成某些工作任务。</p><p>好的体验应当辅助用户理顺整个流程的生产关系，尽可能的减少工作中不必要、反人性的事务，使整体使用变的自然且高效。</p><p><a name="XiVPX"></a></p><h3 id="前端的价值"><a href="#前端的价值" class="headerlink" title="前端的价值"></a>前端的价值</h3><p>前端的价值在于创造尽可能好的体验，在不同阶段对于体验的要求也不一样。</p><ul><li>写出一段好的代码，后面的同学维护起来很方便，那么你在代码层面创造了好的体验。</li><li>开发了一个简洁易用的工具，特定场景下的使用者感觉好用，那么你在工具层面创造了好的体验。</li><li>设计出一个完整的体系，解决了某个领域的问题，你开始创造价值，但并不一定创造了好的体验。</li><li>归纳总结某个体系，理顺生产关系，减少不必要的事物，提升生产力。那么你在某个领域下创造了更好的体验。</li></ul><p>前端的价值不只是 Code Less，而应该是 Input Less，降低人力投入。<br />技术要有投入产出比，而技术产品也要考虑。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;最开始我以为，好的 UI 就是好的体验，后面发现很多设计精美的网站功能却残缺的厉害。&lt;br /&gt;后来我以为，好用的工具就是好的体验，却忽略了很多事情从不只是一个工具就能解决的。&lt;br /&gt;前段时间我以为，一个完善的体系就是好的体验，却忽略了工具庞杂所带来的理解与交互成本。&lt;
      
    
    </summary>
    
    
      <category term="随笔" scheme="https://blog.lxxyx.cn/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>Rax Hooks 原理</title>
    <link href="https://blog.lxxyx.cn/2019/01/17/2019/hooks/"/>
    <id>https://blog.lxxyx.cn/2019/01/17/2019/hooks/</id>
    <published>2019-01-17T14:00:44.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<h1 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h1><p>随着 React 版本的更迭，越来越多的新 Feature 加入到了 React 当中，其中需要 Api，在社区都引起了极大的反响与相当的争议。</p><p>而 React Hooks ，则是社区最近热议的话题，个人也对其十分的感兴趣。<br />比如下面的例子，便能实现一个简单的计数器。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Example</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// Declare a new state variable, which we&#x27;ll call &quot;count&quot;</span></span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>You clicked &#123;count&#125; times<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCount(count + 1)&#125;&gt;Click me<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="简洁而不简单"><a href="#简洁而不简单" class="headerlink" title="简洁而不简单"></a>简洁而不简单</h1><p>正如上面的例子说是，Hooks 的代码看上去非常的简洁易懂，然而背后原理却不简单。看似是纯函数，却自身拥有状态。这种“黑魔法”让我着了迷，不自觉的想去了解其中的原理。</p><p>在查看源代码与相关文章之前，凭借着自己的编程知识，我做了如下几个假设：</p><ul><li>使用 Hooks 的函数式组件，看似函数内部并没有保存状态，但其状态一定存在于某个地方，只是我们看不见。</li><li>由于内部状态依赖于外部调用，因此这种关系是非常脆弱，且容易被更改的，所以使用 Hooks 的组件，需要准备某种强规范才能确保运行时不出问题。</li></ul><h1 id="Rax-与-Hooks"><a href="#Rax-与-Hooks" class="headerlink" title="Rax 与 Hooks"></a>Rax 与 Hooks</h1><p>Rax 是阿里巴巴的淘宝前端团队开源的类 React 框架，拥有速度快，体积小，适配多种容器等诸多优点。</p><p>官方介绍如下：</p><blockquote><p>「Rax 基于 React 的标准，支持在不同容器中渲染，当前最重要的容器即 Weex 和 Web」</p></blockquote><p><br /><br />而自己在校招时，有幸加入了淘宝前端团队，并且与 Rax 的核心开发者们有过一段时间的讨论与交流，收益颇多。</p><p>在 React 甩出 Hooks 这个重磅 Feature 时，我有幸与 Rax 之父元彦讨论过 React Hooks 的实现机制与原理。我当时的想法是 Hooks 是基于 React Fiber 的，没有 Fiber 就没有 Hooks。而元彦则认为目前 Rax 基于递归的方式，也可以做出 React Hooks 的效果。</p><p>过了几天，在我日常浏览 Github 获取第一手知识与信息的时候，我看到 Rax 的仓库里多了个 PR：</p><p><img src="https://cdn.nlark.com/yuque/0/2019/png/98602/1547053973452-a6789e51-7d69-4533-839e-a2e23e817dec.png#align=left&amp;display=inline&amp;height=424&amp;linkTarget=_blank&amp;name=image.png&amp;originHeight=933&amp;originWidth=2287&amp;size=245658&amp;width=1040" alt="image.png"></p><p>仔细看了一下，不太相信自己的眼睛，随后又认真的看了几遍，确认是 Hooks 相关的 PR。<br />元彦实现了自己的想法，而我在知道自身想法不足之余，也想看看不基于 Fiber 的 Rax，是如何实现 Hooks 的这个 feature。</p><h2 id="关于-Hooks-的疑惑"><a href="#关于-Hooks-的疑惑" class="headerlink" title="关于 Hooks 的疑惑"></a>关于 Hooks 的疑惑</h2><p>首先，看一个上文提到过的 Demo：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useState, useEffect &#125; <span class="keyword">from</span> <span class="string">&#x27;rax&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Example</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// Declare a new state variable, which we&#x27;ll call &quot;count&quot;</span></span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">document</span>.<span class="property">title</span> = <span class="string">&#x27;Count: &#x27;</span> + count;</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>You clicked &#123;count&#125; times<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> setCount(count + 1)&#125;&gt;Click me<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是一个大家都能轻易看懂的 Demo，而从上述的代码中，可以得到三点信息：</p><ul><li>这个函数式组件会被 Rax 调用</li><li>函数式组件内部调用了 Rax  内置的  <code>useState</code>  方法，来获得最新状态与修改的方法。</li><li>在调用 <code>setCount</code>  改变 <code>count</code>  时，也会触发 <code>useEffect</code>  所定义的回调</li></ul><p>我个人的疑惑如下：</p><ul><li>函数内部不保存状态，那么这个状态究竟是保存在哪儿，怎么样确保每次重新渲染组件，都能拿到正确的值？</li><li>在调用修改状态的方法后，为何能触发组件更新？</li><li>在状态改变后，为何  <code>useEffect</code>  也能够被调用？</li></ul><p>而在这儿，首先要介绍一下 Rax 是如何渲染函数式组件的。</p><h1 id="Rax-Hooks-原理解析"><a href="#Rax-Hooks-原理解析" class="headerlink" title="Rax Hooks 原理解析"></a>Rax Hooks 原理解析</h1><h2 id="函数式组件的渲染"><a href="#函数式组件的渲染" class="headerlink" title="函数式组件的渲染"></a>函数式组件的渲染</h2><p>在 Rax 这儿，并没有直接去调用函数式组件，来获取生成的 Elements（created by createElement），而是将函数式组件做了一层包裹，简化后的代码如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 函数式组件</span></span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">typeof</span> <span class="title class_">Component</span> === <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">  <span class="comment">// Functional reactive component with hooks</span></span><br><span class="line">  instance = <span class="keyword">new</span> <span class="title class_">ReactiveComponent</span>(<span class="title class_">Component</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在代码中，可以看出如果是函数式组件，函数式组件会被   ReactiveComponent  包裹一层。而注释中也明确的写着：”Functional reactive component with hooks”。此处不难看出，这个  <code>ReactiveComponent</code>  就是 Hooks 的运行时的核心了，并且与  <code>useState</code> 、<code>useEffect</code>  等 Api 共同组成了 Hooks 这个功能。</p><p>首先，让我们来看一看，<code>useState</code>  作为核心的 Api，在运行期都做了什么？</p><h2 id="useState-的实现"><a href="#useState-的实现" class="headerlink" title="useState 的实现"></a>useState 的实现</h2><p>useState 的 API 如下，传入  initialState，便可以获得两个返回值，state 与 setState，第一个代表当前值，第二个则是函数，可以更新当前值，属于非常简洁易懂的 Api。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 初始化 state</span></span><br><span class="line"><span class="keyword">const</span> [state, setState] = <span class="title function_">useState</span>(initialState);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 更新状态</span></span><br><span class="line"><span class="title function_">setState</span>(newState);</span><br></pre></td></tr></table></figure><blockquote><p>在这儿其实还有一段小插曲，Hooks 用的是数组解构的方式来传递当前值与更新的函数，有人则在 React RFC 中提出为何不用对象的结构方式，对此官方给出的解释是：对象结构需要指定名字，而基于数组的结构不用，你可以随意命名。</p></blockquote><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 初始化 state</span></span><br><span class="line"><span class="keyword">const</span> [state, setState] = <span class="title function_">useState</span>(initialState);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 更新状态</span></span><br><span class="line"><span class="title function_">setState</span>(newState);</span><br></pre></td></tr></table></figure><p>在这儿，我们看向 Rax 中，<code>useState</code>   的具体实现方式（为方便理解，精简部分代码）：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取当前正在渲染的组件，设置时机如下：</span></span><br><span class="line"><span class="comment">// 此处 Component 为当前正在处理中的组件</span></span><br><span class="line"><span class="comment">// 1. currentInstance = Component</span></span><br><span class="line"><span class="comment">// 2. Component.render() 【也就是说在组件 render 期间，currentInstance 是组件自身】</span></span><br><span class="line"><span class="comment">// 3. currentInstance = null</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">getCurrentRenderingInstance</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> currentInstance = <span class="title class_">Host</span>.<span class="property">component</span>.<span class="property">_instance</span>;</span><br><span class="line">  <span class="keyword">return</span> currentInstance;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">useState</span>(<span class="params">initialState</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> currentInstance = <span class="title function_">getCurrentRenderingInstance</span>();</span><br><span class="line">  <span class="keyword">const</span> hookId = currentInstance.<span class="title function_">getCurrentHookId</span>();</span><br><span class="line">  <span class="keyword">const</span> hooks = currentInstance.<span class="property">hooks</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!hooks[hookId]) &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="title function_">setState</span> = (<span class="params">newState</span>) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> current = hooks[hookId][<span class="number">0</span>];</span><br><span class="line">      <span class="keyword">if</span> (newState !== current) &#123;</span><br><span class="line">        hooks[hookId][<span class="number">0</span>] = newState;</span><br><span class="line">        currentInstance.<span class="title function_">update</span>();</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    hooks[hookId] = [initialState, setState];</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> hooks[hookId];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>useState</code>  函数中，第一步便是取得当前正在渲染中的组件。而 <code>currentInstance</code>  这个值，是在组件调用 render 前被设置的，render 结束后便会被设置为 null。</p><p>而这也就意味着：组件在 <code>render</code>  期间调用 <code>useState</code>  ，<code>useState</code>  函数是可以知道当前渲染的组件的，同一个函数式组件的调用，拿到的  <code>currentInstance</code>  永远是其自身。</p><p>我们的疑惑点在于：函数内部不保存状态，那么这个状态究竟是保存在哪儿，怎么样确保每次重新渲染组件，都能拿到正确的值？</p><p>现在看来，一切仿佛都有了头绪，既然我能在调用  <code>useState</code>  时，拿到当前的实例且同一个函数式组件，每次调用  <code>useState</code>  都能获取到当前实例。那么<strong>当前实例将会是一个保存与隐藏组件状态的最佳地点。</strong></p><p>原因如下：</p><ul><li>同一组件，获取到  <code>currentInstance</code>    始终是其自身，储存其中的数据，并不受 Rax 的更新影响</li><li>不同组件，获取到的实例也不一样，通过  <code>useState</code>  获取到的状态，也互不影响</li></ul><p>这样一来，所有的函数式组件，调用的同一个  <code>useState</code>  方法，却互不影响，既保证了代码的整洁性，又保证了功能的完善性，该处理方法不可谓不巧妙。</p><p>而在前面我们提到过，Rax 的函数式组件，是被 <code>ReactiveComponent</code>  所包裹着的，而在  <code>useState</code>  的具体实现中，也出现了 <code>getCurrentHookId</code>  等属于  <code>ReactiveComponent</code>  的方法调用，因此想要理解透彻 Rax 的 Hooks 机制，我们需要看  ReactiveComponent 的实现。</p><h2 id="ReactiveComponent-的实现"><a href="#ReactiveComponent-的实现" class="headerlink" title="ReactiveComponent 的实现"></a>ReactiveComponent 的实现</h2><p>首先，看源代码（为方便理解，精简部分代码）：</p><p>核心参数解释如下：</p><ul><li>pureRender：传入的函数式组件，调用方式如下：</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 函数式组件</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Component</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello Hooks<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 函数式组件的初始化</span></span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">typeof</span> <span class="title class_">Component</span> === <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">  <span class="comment">// Functional reactive component with hooks</span></span><br><span class="line">  instance = <span class="keyword">new</span> <span class="title class_">ReactiveComponent</span>(<span class="title class_">Component</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>ReactiveComponent 源码：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">scheduleImmediateCallback</span>(<span class="params">callback</span>) &#123;</span><br><span class="line">  <span class="built_in">setTimeout</span>(callback, <span class="number">99</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ReactiveComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">pureRender</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>();</span><br><span class="line">    <span class="comment">// A pure function</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">pureRender</span> = pureRender;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">hooksIndex</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">hooks</span> = &#123;&#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">getCurrentHookId</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> ++<span class="variable language_">this</span>.<span class="property">hooksIndex</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Async update</span></span><br><span class="line">  <span class="title function_">update</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="title function_">scheduleImmediateCallback</span>(<span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">forceUpdate</span>());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">hooksIndex</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">let</span> children = <span class="variable language_">this</span>.<span class="title function_">pureRender</span>(<span class="variable language_">this</span>.<span class="property">props</span>);</span><br><span class="line">    <span class="keyword">return</span> children;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">ReactiveComponent</span>;</span><br></pre></td></tr></table></figure><p>而此时再看  useState 的源码，一切都清晰了起来。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// useState 源码</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取当前实例，此处指的是 ReactiveComponent</span></span><br><span class="line"><span class="keyword">const</span> currentInstance = <span class="title function_">getCurrentRenderingInstance</span>();</span><br><span class="line"><span class="comment">// 获取当前调用的 Hooks 下标，用于解决函数式组件内调用多个 Hooks 的问题</span></span><br><span class="line"><span class="keyword">const</span> hookId = currentInstance.<span class="title function_">getCurrentHookId</span>();</span><br><span class="line"><span class="comment">// 之前存放的 Hooks，</span></span><br><span class="line"><span class="keyword">const</span> hooks = currentInstance.<span class="property">hooks</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果是首次渲染，则创建这个 Hooks 的值与对应的更新函数</span></span><br><span class="line"><span class="comment">// hooks[hookId] 指代当前调用的 Hooks 所对应的 [state, setState]</span></span><br><span class="line"><span class="keyword">if</span> (!hooks[hookId]) &#123;</span><br><span class="line">  <span class="comment">// 更新函数</span></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">setState</span> = (<span class="params">newState</span>) =&gt; &#123;</span><br><span class="line">    <span class="comment">// 上一次的 state 值</span></span><br><span class="line">    <span class="keyword">const</span> current = hooks[hookId][<span class="number">0</span>];</span><br><span class="line">    <span class="keyword">if</span> (newState !== current) &#123;</span><br><span class="line">      <span class="comment">// 如果值更新了则更新 State</span></span><br><span class="line">      hooks[hookId][<span class="number">0</span>] = newState;</span><br><span class="line">      <span class="comment">// 调用 ReactiveComponent 的 update 方法</span></span><br><span class="line">      <span class="comment">// 而 update 方法会在 99ms 后调用 forceUpdate 来更新组件</span></span><br><span class="line">      currentInstance.<span class="title function_">update</span>();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  hooks[hookId] = [initialState, setState];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回当前调用的 Hooks 所对应的 [state, setState]</span></span><br><span class="line"><span class="keyword">return</span> hooks[hookId];</span><br></pre></td></tr></table></figure><blockquote><p>引申出一个小话题<br>Q：Why ReactiveComponent 在调用 update 方法时，需要延迟 99ms 执行？<br>A：Rax 中 forceUpdate 是同步执行的，如果  update 方法同步执行，则会造成 Hooks 下标的错乱问题。</p></blockquote><p>大家有注意到，在这儿，我们提起了一个之前未曾提起的概念：当前执行 Hooks 的下标。由于函数内部可能会不止一次调用 Hooks ，因此需要实现一个多次调用钩子的解决方案。</p><h2 id="多次调用-Hooks-的实现方式"><a href="#多次调用-Hooks-的实现方式" class="headerlink" title="多次调用 Hooks 的实现方式"></a>多次调用 Hooks 的实现方式</h2><p>首先，看一段代码：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// useState.js</span></span><br><span class="line"><span class="keyword">const</span> hookId = currentInstance.<span class="title function_">getCurrentHookId</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// ReactiveComponent</span></span><br><span class="line"><span class="title function_">getCurrentHookId</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> ++<span class="variable language_">this</span>.<span class="property">hooksIndex</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">hooksIndex</span> = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">let</span> children = <span class="variable language_">this</span>.<span class="title function_">pureRender</span>(<span class="variable language_">this</span>.<span class="property">props</span>);</span><br><span class="line">  <span class="keyword">return</span> children;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在组件 Render 前，ReactiveComponent 会把  hooksIndex 设置为 0，而在后续的过程中，每次调用  useState 等 Hook 时，都会将  hooksIndex 执行 +1 操作。而这样则保证了，同一个组件内部，不同的 Hooks 调用，相互之间是不影响的。</p><p>这样一来，便允许函数内部存在多个 Hooks，且相互之间不影响。因为每次调用时，都会获取到一个独一无二的下标。</p><p>但是这样做，也带来了另外一个问题，那就是函数内部的 Hooks 调用是强依赖于执行顺序的。</p><h2 id="Hooks-所带来的问题"><a href="#Hooks-所带来的问题" class="headerlink" title="Hooks 所带来的问题"></a>Hooks 所带来的问题</h2><p>具体原因可以看下面一段伪代码：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Example</span>(<span class="params">props</span>) &#123;</span><br><span class="line">  <span class="keyword">let</span> count, setCount;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> ((props.<span class="property">useCounter</span> = <span class="literal">true</span>)) &#123;</span><br><span class="line">    [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> [name, setName] = <span class="title function_">useState</span>(<span class="string">&#x27;Hello&#x27;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">      <span class="attr">onClick</span>=<span class="string">&#123;()</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">        setCount &amp;&amp; setCount((count += 1));</span></span><br><span class="line"><span class="language-xml">      &#125;&#125;</span></span><br><span class="line"><span class="language-xml">    &gt;</span></span><br><span class="line"><span class="language-xml">      &#123;name&#125;： &#123;count&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title class_">Example</span>(&#123; <span class="attr">useCounter</span>: <span class="literal">true</span> &#125;);</span><br><span class="line"><span class="title class_">Example</span>(&#123; <span class="attr">useCounter</span>: <span class="literal">false</span> &#125;);</span><br></pre></td></tr></table></figure><p>我们知道，hooks[hookId] 代表着对应 Hooks 存放数据的地方，而在此处，第一次渲染时，Props 中的 useCounter 为 True，此时：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hooks[<span class="number">0</span>] = [count, setCount];</span><br><span class="line">hooks[<span class="number">1</span>] = [name, setName];</span><br></pre></td></tr></table></figure><p>而第二次渲染时，Props 中的 useCounter 为 False，而 Hooks 是不会重复创建的，因此：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// example.js</span></span><br><span class="line"><span class="keyword">const</span> [name, setName] = <span class="title function_">useState</span>(<span class="string">&#x27;Hello&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// useState.js</span></span><br><span class="line"><span class="comment">// 调用 useState(&#x27;Hello&#x27;) 时</span></span><br><span class="line">hookId = <span class="number">0</span>;</span><br><span class="line">hooks[<span class="number">0</span>] = [count, setCount];</span><br><span class="line"><span class="keyword">return</span> hooks[<span class="number">0</span>];</span><br></pre></td></tr></table></figure><p>你调用的明明是 <code>useState(&#39;Hello&#39;)</code> ，但返回的却是 <code>hooks[0] = [count, setCount]</code> ，这就会引发未知的 BUG。</p><p>而这恰恰也是 Hooks 所带来的问题与争议，React 开始变的不那么明确，或多或少的有了一些隐式规则，对于这个问题，Facebook 官方提出的解决方案是引入一个 ESLint 插件，来确保 Hooks 的正确书写。</p><p>插件地址如下：<a href="https://github.com/facebook/react/blob/ab03e3d6518ae3c68feca8d5debf2ef69a91a38c/packages/eslint-plugin-react-hooks/README.md">https://github.com/facebook/react/blob/ab03e3d6518ae3c68feca8d5debf2ef69a91a38c/packages/eslint-plugin-react-hooks/README.md</a></p><p>解决了一个问题，引入了新的问题，打上了新的补丁。</p><h2 id="useEffect-的实现"><a href="#useEffect-的实现" class="headerlink" title="useEffect 的实现"></a>useEffect 的实现</h2><p>在上文中，我们解答了最开始的两个疑惑：</p><ul><li>函数内部不保存状态，那么这个状态究竟是保存在哪儿，怎么样确保每次重新渲染组件，都能拿到正确的值？</li><li>在调用修改状态的方法后，为何能触发组件更新？</li></ul><p>我们还剩下最后一个疑惑：“在状态改变后，为何  <code>useEffect</code>  也能够被调用？”。</p><p>如下面的例子：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Example</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [width, setWidth] = <span class="title function_">useState</span>(<span class="variable language_">window</span>.<span class="property">innerWidth</span>);</span><br><span class="line"></span><br><span class="line">  <span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="title function_">handleResize</span> = (<span class="params"></span>) =&gt; <span class="title function_">setWidth</span>(<span class="variable language_">window</span>.<span class="property">innerWidth</span>);</span><br><span class="line">    <span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;resize&#x27;</span>, handleResize);</span><br><span class="line">    <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">window</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;resize&#x27;</span>, handleResize);</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">p</span>&gt;</span>Window width is &#123;width&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>就实现了以下的效果：</p><ul><li>状态改变后， <code>useEffect</code>  也能够被正确调用</li><li>useEffect 可以返回一个函数，在组件取消订阅时使用</li></ul><p>这儿需要再次看向  ReactiveComponent 的源代码（精简部分逻辑）：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ReactiveComponent</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Component</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params">pureRender</span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>();</span><br><span class="line">    <span class="comment">// A pure function</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">pureRender</span> = pureRender;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">hooksIndex</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">hooks</span> = &#123;&#125;;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">didMountHandlers</span> = [];</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">didUpdateHandlers</span> = [];</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">willUnmountHandlers</span> = [];</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">componentDidMount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">didMountHandlers</span>.<span class="title function_">forEach</span>(<span class="function">(<span class="params">handler</span>) =&gt;</span> <span class="title function_">handler</span>());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">componentDidUpdate</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">didUpdateHandlers</span>.<span class="title function_">forEach</span>(<span class="function">(<span class="params">handler</span>) =&gt;</span> <span class="title function_">handler</span>());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">componentWillUnmount</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">willUnmountHandlers</span>.<span class="title function_">forEach</span>(<span class="function">(<span class="params">handler</span>) =&gt;</span> <span class="title function_">handler</span>());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">getCurrentHookId</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> ++<span class="variable language_">this</span>.<span class="property">hooksIndex</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Async update</span></span><br><span class="line">  <span class="title function_">update</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="title function_">scheduleImmediateCallback</span>(<span class="function">() =&gt;</span> <span class="variable language_">this</span>.<span class="title function_">forceUpdate</span>());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">render</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">hooksIndex</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">let</span> children = <span class="variable language_">this</span>.<span class="title function_">pureRender</span>(<span class="variable language_">this</span>.<span class="property">props</span>, <span class="variable language_">this</span>.<span class="property">forwardRef</span> ? <span class="variable language_">this</span>.<span class="property">forwardRef</span> : <span class="variable language_">this</span>.<span class="property">context</span>);</span><br><span class="line">    <span class="keyword">return</span> children;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">ReactiveComponent</span>;</span><br></pre></td></tr></table></figure><p>在  ReactiveComponent 中，与之前的代码对比，主要是增加了 Handlers 这个概念。Handlers 是数组，里面会存放函数，并且在对应的生命周期执行时，执行该函数。</p><p>主要涉及以下几个生命周期：</p><ul><li>componentDidMount</li><li>componentDidUpdate</li><li>componentWillUnmount</li></ul><p>包括了组件的初次挂载，升级，卸载这三个步骤。</p><p>让我们继续看一下 useEffect 的源码（为方便理解，精简部分代码）：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">useEffect</span>(<span class="params">effect</span>) &#123;</span><br><span class="line">  <span class="title function_">useEffectImpl</span>(effect);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">useEffectImpl</span>(<span class="params">effect</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> currentInstance = <span class="title function_">getCurrentRenderingInstance</span>();</span><br><span class="line">  <span class="keyword">const</span> hookId = currentInstance.<span class="title function_">getCurrentHookId</span>();</span><br><span class="line">  <span class="keyword">const</span> hooks = currentInstance.<span class="property">hooks</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!hooks[hookId]) &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="title function_">create</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> &#123; current &#125; = create;</span><br><span class="line">      <span class="keyword">if</span> (current) &#123;</span><br><span class="line">        destory.<span class="property">current</span> = <span class="title function_">current</span>();</span><br><span class="line">        create.<span class="property">current</span> = <span class="literal">null</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> <span class="title function_">destory</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> &#123; current &#125; = destory;</span><br><span class="line">      <span class="keyword">if</span> (current) &#123;</span><br><span class="line">        <span class="title function_">current</span>();</span><br><span class="line">        destory.<span class="property">current</span> = <span class="literal">null</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    create.<span class="property">current</span> = effect;</span><br><span class="line"></span><br><span class="line">    currentInstance.<span class="property">hooks</span>[hookId] = &#123;</span><br><span class="line">      create,</span><br><span class="line">      destory,</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    currentInstance.<span class="property">didMountHandlers</span>.<span class="title function_">push</span>(create);</span><br><span class="line">    currentInstance.<span class="property">willUnmountHandlers</span>.<span class="title function_">push</span>(destory);</span><br><span class="line">    currentInstance.<span class="property">didUpdateHandlers</span>.<span class="title function_">push</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> &#123; destory, create &#125; = hooks[hookId];</span><br><span class="line">      <span class="title function_">destory</span>();</span><br><span class="line">      <span class="title function_">create</span>();</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> hook = hooks[hookId];</span><br><span class="line">    <span class="keyword">const</span> &#123; create &#125; = hook;</span><br><span class="line">    create.<span class="property">current</span> = effect;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过分析源代码，我们可以得知，useEffect 会在初次挂载时，创建两个函数，create 与 destory。并且订阅了  ReactiveComponent 组件的生命周期。</p><p>也就是这一段：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">currentInstance.<span class="property">didMountHandlers</span>.<span class="title function_">push</span>(create);</span><br><span class="line">currentInstance.<span class="property">willUnmountHandlers</span>.<span class="title function_">push</span>(destory);</span><br><span class="line">currentInstance.<span class="property">didUpdateHandlers</span>.<span class="title function_">push</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; destory, create &#125; = hooks[hookId];</span><br><span class="line">  <span class="title function_">destory</span>();</span><br><span class="line">  <span class="title function_">create</span>();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="create-函数"><a href="#create-函数" class="headerlink" title="create 函数"></a>create 函数</h3><p>在组件触发  componentDidMount 时，会触发  create 函数，用于初始化钩子，而 create 函数会调用 useEffect 中传入的回调。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">useEffectImpl</span>(<span class="params">effect</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">create</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; current &#125; = create;</span><br><span class="line">    <span class="keyword">if</span> (current) &#123;</span><br><span class="line">      <span class="comment">// 如果有返回值，则将 destory.current 设置为 返回值</span></span><br><span class="line">      destory.<span class="property">current</span> = <span class="title function_">current</span>();</span><br><span class="line">      create.<span class="property">current</span> = <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  create.<span class="property">current</span> = effect;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这儿需要注意的是，由于 useEffect 通常会用来处理一些副作用，比如订阅某些事件，所以 useEffect 在设计 Api 时，是支持传入的 effect ，在调用后有一个函数的返回值，用于在组件更新或者销毁时，取消订阅的。</p><p>如下面的代码所示：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleResize</span> = (<span class="params"></span>) =&gt; <span class="title function_">setWidth</span>(<span class="variable language_">window</span>.<span class="property">innerWidth</span>);</span><br><span class="line">  <span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;resize&#x27;</span>, handleResize);</span><br><span class="line">  <span class="keyword">return</span> <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">window</span>.<span class="title function_">removeEventListener</span>(<span class="string">&#x27;resize&#x27;</span>, handleResize);</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="destory-函数"><a href="#destory-函数" class="headerlink" title="destory 函数"></a>destory 函数</h3><p>destory 函数在此处的作用比较单纯，就是在触发该函数时，会触发 effect 的回调值。用于取消事件订阅。</p><h3 id="组件更新触发-useEffect"><a href="#组件更新触发-useEffect" class="headerlink" title="组件更新触发 useEffect"></a>组件更新触发 useEffect</h3><p>在这儿，我们之前提出的第三个问题：“在状态改变后，为何  <code>useEffect</code>  也能够被调用？” 也能够得到解答。</p><p>在 useEffect 钩子第一次在组件内部使用时，它会订阅组件的  componentDidUpdate 事件，在组件升级后，它将依次调用  destory 与  create 函数。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">currentInstance.<span class="property">didUpdateHandlers</span>.<span class="title function_">push</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; destory, create &#125; = hooks[hookId];</span><br><span class="line">  <span class="title function_">destory</span>();</span><br><span class="line">  <span class="title function_">create</span>();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>而之所以先调用  destory 的原因，是需要先取消之前的订阅，然后再执行订阅的流程。<br />这样也就避免了，多次订阅所带来的问题，确保每次只会订阅一次。</p><h2 id="那些没有提到的"><a href="#那些没有提到的" class="headerlink" title="那些没有提到的"></a>那些没有提到的</h2><p>在写这篇文章时，贴出源代码的部分，我都会加上“为方便理解，精简部分代码”这句话。因为随着 Rax 的能力增强，其项目整体的复杂度也在逐渐的提高。</p><p>在文章中，为了能让读者清晰的了解到 Hooks 是什么，与 Rax 的实现方式，我精简了许多代码。包括异步更新/Ref 的处理/重复循环的检测。<br />而这些东西，也同样的有价值，具有其闪光之处，建议有兴趣进一步了解 Rax Hooks 实现的同学，自行前往 Rax 的仓库中阅读源代码，你会收获的更多。</p><p>Rax 仓库地址：<a href="https://github.com/alibaba/rax/tree/master/packages/rax">https://github.com/alibaba/rax/tree/master/packages/rax</a></p><h1 id="Hooks-将给我们带来什么"><a href="#Hooks-将给我们带来什么" class="headerlink" title="Hooks 将给我们带来什么"></a>Hooks 将给我们带来什么</h1><p>在 Hooks 出现之前，React 在实现部分功能时，因为涉及到生命周期等操作，是略微繁琐的。<br />而这时候的解决方法是 HOC，将组件包裹一层，但是组件包裹多了也不好呀，看起来整个层级就会特别的深。<br />而 Hooks 的出现，以其简洁的 API，解决了这个问题。<br />Hooks 将给我们带来的，首先是代码量上的减少。</p><p>当函数具有生命周期，可以通过 Hooks 做很多 Class Component 才可以做到的事情，也预示了 Class Component 在将来很长的一段时间内，不再是唯一推荐的范式。又随着 Memo 等 Api 的出现，函数式组件第一次可以尝试与  Class Component 做一番较量。<br />Hooks 将给我们带来的，是 React 编程范式的变更。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>纵观 React 发布这几年，官方推荐的编程范式随着时代不断的变更着。万变不离其宗的，是 React 希望做好 View 层框架的基础理念。Mixins/HOC/Class Component/Hooks，概念总是在增加，在变化。</p><p>在学习 Hooks 过程中，比起 Api 与具体的实现，我更为关注的是 Why Hooks？<br />把握 React 这一类框架在技术选型， Api 设计上的思考，可以很好的让自己了解到整个前端框架界，遇到的实际问题，大家的想法与最后的解决方案。</p><p>很多时候，选择做不做一件事情，比起把所有功能都提供给用户，是一件更难的事情。这意味着你需要在万千种范式中选择出最合适的几种，融入自己的思考，形成一套新的体系。在功能完善的同时还需要足够好用。</p><p>如果说自己想要什么的话，大概就是上面提到的，希望自己可以有一双智慧的双眼，可以指引自己做出正确的选择。</p><h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><h2 id="Rax-1-0-思考"><a href="#Rax-1-0-思考" class="headerlink" title="Rax 1.0 思考"></a>Rax 1.0 思考</h2><p>随着前几天 <a href="https://github.com/alibaba/rax/pull/856">Rax 1.0 Api PR</a>  的提出，Rax 正式确定了自己在新版本中的方向。<br />有一个比较大的改动是，Rax 将原有属于 JSX 的 Component/PropTypes/isValidElement 等 Api 抽离出来，打成了一个单独的包。<br />单独看可能还不是很理解意义何在，但是在这之前，Rax 还做了一波改动，使其能够支持 3 种 DSL：<br /><br /></p><ul><li>JSX（React Style）</li><li>SFC（Vue Style）</li><li>小程序</li></ul><p>这样看来，将原有属于 JSX 的一些 Api 抽离出来，单独打包的意义也就凸显了出来：</p><ul><li>精简 Rax，减少 Bundle Size（12kb -&gt; 8kb）</li><li>Core 的 Api 更为精简，可以支持多种 DSL</li><li>业务的选择与定制性更强，只需要引入自己需要的 Api 对应的 package，就可以实现部分功能。</li></ul><p>当然这种拆成多个 Package 的方式也并非没有缺陷，比如业务使用起来需要引入多个  Package，同时也需要管理多个 Package 的版本。<br />就像那天和元彦聊 React 一样，他说过一个技术有优势肯定也有其劣势，并没有什么技术是完美无缺可以 Cover 到所有 Case 的。而 Rax 1.0 的升级也一样，有优势也有缺点。</p><p>期待后续 Rax 的升级与改进，想知道 Rax  如何将 1.0 拆分多个 Package 所带来的影响降低到最小，也想了解在引入 Hooks 后会产生如何的化学反应。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;起因&quot;&gt;&lt;a href=&quot;#起因&quot; class=&quot;headerlink&quot; title=&quot;起因&quot;&gt;&lt;/a&gt;起因&lt;/h1&gt;&lt;p&gt;随着 React 版本的更迭，越来越多的新 Feature 加入到了 React 当中，其中需要 Api，在社区都引起了极大的反响与相当的争
      
    
    </summary>
    
    
      <category term="前端" scheme="https://blog.lxxyx.cn/tags/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>《坚守底线》</title>
    <link href="https://blog.lxxyx.cn/2019/01/17/2019/bottom-line/"/>
    <id>https://blog.lxxyx.cn/2019/01/17/2019/bottom-line/</id>
    <published>2019-01-17T13:59:18.000Z</published>
    <updated>2023-01-02T12:32:51.305Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.nlark.com/yuque/0/2019/jpeg/98602/1547372556792-5087813e-7e4e-4337-b671-d1917fbb7a14.jpeg#align=left&amp;display=inline&amp;height=501&amp;linkTarget=_blank&amp;originHeight=800&amp;originWidth=800&amp;size=0&amp;width=501" alt=""></p><h1 id="这本书"><a href="#这本书" class="headerlink" title="这本书"></a>这本书</h1><p>遇见《坚守底线》这本书时，我还是大二，也喜欢去图书馆。<br />时光荏苒一过 3 年，回头来看，这本书是大学期间改变我最多的一本书籍。</p><p>在那之后，如果有朋友让我推荐几本书给他，我一定会推荐的一本书，就是这本《坚守底线》。<br />书的内容很简单，讲的东西也不复杂，需要的只是认认真真的去读了，按照书里面指引的信念认认真真去做，仅此而已。</p><h2 id="聪明人"><a href="#聪明人" class="headerlink" title="聪明人"></a>聪明人</h2><p>在我的大学期间，遇到过很多聪明的人，他们读书读的比我快，脑子比我转得快，去过更远的地方，接触过更高层次的人。<br />我是小城镇里出来的，没有见过什么世面，很多时候不自觉的就会对自己的能力产生怀疑，在大学的前一年，我看着那些生活与过往履历光鲜亮丽的朋友们，想着如果能和他们合作，一定可以学到很多东西吧。虽然我见识少，但是站在巨人的肩膀上能看的更远这个概念我还是清楚的。</p><p>但是与部分聪明朋友在实际的合作中，便会出现一种怪象，明明这件事情只需要坚持下去便能守得云开见月明，但是朋友们却觉得收益太低，转而投向其它的方向。</p><p>我在继续坚持着，同时也在观察着这些聪明的朋友们，希望可以汲取到一些经验，以便为自己将来的决策提供参考。</p><p>经过很长一段时间的观察（大概 1 年），我发现身边的聪明朋友们，除了极个别的同学，拿到了自己想要的成就外，其它的聪明朋友，在这一年之中都是东一榔头西一棒子，看起来所获颇微。</p><p>在这之后，我接触到了《坚守底线》这本书，忽然也明白了一些什么，于是开始坚定且认真的相信自己，并走自己的路。</p><h1 id="再读"><a href="#再读" class="headerlink" title="再读"></a>再读</h1><p>读完《坚守底线》，把书还给了图书馆。<br />还回去的是书，而留下来的则是指导自己整个大学生活的信念。</p><p>工作后，我从网上又买了这本书，并带在身边，随时翻阅。<br />这一次也是正好借着读书笔记的活动，把自己觉得的精华与感悟记录下来。</p><p>其中有一些，我会带上具体的事例。</p><h1 id="笔记"><a href="#笔记" class="headerlink" title="笔记"></a>笔记</h1><h2 id="能力与人品"><a href="#能力与人品" class="headerlink" title="能力与人品"></a>能力与人品</h2><blockquote><p>如果是说“选能力还是选人品，二者必须选择其一”。那么，我会毫不犹豫的回答：“人品最重要”。</p></blockquote><p>其实这句话，是与我在家园工作的过程中，一直在做的事情。<br />而类似的话，其实在《家园信条》中也有提出：“所有事情的基础，一颗为学校出力的心，一个正直的人品。”</p><p>在家园招人的过程中，总会有个把能力突出，但是品行不佳的同学。“目中无人，狂妄自大，甚至随意攻击家园同学”。<br />这种情况下，即使能力突出，也选择劝退。</p><h2 id="经营家园"><a href="#经营家园" class="headerlink" title="经营家园"></a>经营家园</h2><blockquote><p>作为经营者，如果你真想把公司搞好，让员工自豪，让员工感觉到工作的喜悦，那么你就要把全部心血倾注在事业上，并做出自我牺牲，为员工的幸福和公司的发展殚精竭虑。没有这样的奉献精神，就不要当经营者。</p></blockquote><p>在大三的时候，我加入了家园中心组，那时候时时刻刻都想着如何让家园变得更好，如何从泥淖里脱身。<br />但当时的我，除了参考一些成熟公司或者团队的做法，剩下的时候便是按着这句话，努力认真的去做事情，即使有些时候会“自我牺牲”。</p><p>经营家园是一件需要极其认真的事情。</p><p>而两年下来，家园也给我带来了出乎意料的回报，这儿就暂且不提了。</p><h2 id="优秀人才与愚钝之人"><a href="#优秀人才与愚钝之人" class="headerlink" title="优秀人才与愚钝之人"></a>优秀人才与愚钝之人</h2><blockquote><p>从长远的眼光来看，比起那些半吊子的所谓优秀人才，倒是头脑略微迟钝但拼命努力的人可以从事伟大的工作。</p></blockquote><p>这句话对我影响深广。<br />在那之前，我一直觉得自己还可以，挺聪明的，但不知道为何就是一无所获。</p><p>读到这句话后，我放弃了编程外所有的机会，即使有些机会非常的诱人。<br />自那之后，我给自己的定位就是“头脑略微迟钝”的人。<br />知道编程的未来不是一塌糊涂，知道自己坚持下去就可以超越很多人，就足够了，老老实实认认真真的努力，会是最稳妥的方法，也是生活最简单的方法。</p><h2 id="社会的不公"><a href="#社会的不公" class="headerlink" title="社会的不公"></a>社会的不公</h2><blockquote><p>一味的怨恨社会的不公平，老是抱着这种心态，自己的人生就会不顺畅</p></blockquote><p>在学校偶尔能看到跑车，那种看起来就很贵要大几百万的跑车。<br />我知道现在的自己买不起，将来的自己也可能买不起，更有可能这辈子都只能看看而已。</p><p>在那时候，我觉得这样的生活有些糟糕，毕竟自己再努力也只是活成别人的一个零头。</p><p>读完书后，我开始乐观的看待这一切，开始正视自己心中的欲望，认真的思考并给自己一个答复。<br />之后再看到豪车，我的想法变成了：“哇哦，好棒，羡慕 ing，希望自己多多努力，将来虽然开不起保时捷法拉利，但至少可以搞辆自己喜欢的汽车”。</p><h2 id="喜欢自己的工作"><a href="#喜欢自己的工作" class="headerlink" title="喜欢自己的工作"></a>喜欢自己的工作</h2><blockquote><p>人生的道路是漫长的。人要在现实社会里生活工作好几十年。因此，对踏上社会后从事的工作要有种迷恋的心态，这点很重要。如果喜爱自己的工作，就会涌出一股力量，促使人持续地钻研努力。同时，持续不懈地努力，不知疲倦地反复钻研、琢磨创新，就有可能到达被称为名人高手的境界。</p></blockquote><p>想起读书时，对比起编程这件事情来说，其他的一切事情都仿佛是那么的苍白无力。<br />在选定编程作为方向后，我曾经往某个崇拜的前辈的知乎专栏中，投过一篇稿子，并得到了如下的回复。</p><p><img src="https://cdn.nlark.com/yuque/0/2019/png/98602/1547375122474-4f7c82e4-d551-4111-9001-ae6f6eacd1aa.png#align=left&amp;display=inline&amp;height=369&amp;linkTarget=_blank&amp;name=image.png&amp;originHeight=922&amp;originWidth=1347&amp;size=276458&amp;width=539" alt="image.png"></p><p>而三年过去了，自己在面对代码时依旧充满了思考与力量。</p><h2 id="一味祷告纵然心虔"><a href="#一味祷告纵然心虔" class="headerlink" title="一味祷告纵然心虔"></a>一味祷告纵然心虔</h2><blockquote><p>踏上社会，在最初就职的公司里使我懂得，不管所处的环境多么恶劣，不管被逼入何等绝望的境地，如果只是一味地发牢骚，逃避现实，犹豫彷徨，那么，人生绝不会出现转机。</p></blockquote><p>在高三时，我每周不会落下的，是“进击的巨人”这部番剧。<br />在片首曲中，有这么一句话：</p><blockquote><p>一味祷告纵然心虔，世间又能有何改变。</p></blockquote><p>而坚守底线中的这句话，是一样的意思。<br />在家园工作与学习的过程中，时常有同学夸奖我执行力很强，而执行力强的缘由，便是这两句话。</p><p>同样的话，在家园信条中也有提及：</p><blockquote><p>我们需要能够提出问题的人，但是更需要能够解决问题的人，提出问题 80 分，解决问题 100,但是我们坚信：100 分大于 80 分,80 分大于 0 分</p></blockquote><p>有想法，大胆去做就好了~</p><h2 id="无欲则刚"><a href="#无欲则刚" class="headerlink" title="无欲则刚"></a>无欲则刚</h2><blockquote><p>包括自己的生命在内，只有能够彻底抛弃私心的人才能成就大事”。这句话凝缩了最重要的思想不仅对经济界，而且对政界、官界、各行各界都适用。我坚信，领袖的必要条件就在于克己奉公。</p></blockquote><p>看到这一段时，只想起了一句话：</p><blockquote><p>海纳百川，有容乃大。壁立千仞，无欲则刚。</p></blockquote><p>我自己逐渐可以体会到其中的意思，只是距离这样的高度还非常非常的远，在此不做评价。</p><h1 id="四个关键字"><a href="#四个关键字" class="headerlink" title="四个关键字"></a>四个关键字</h1><p>再次阅读《坚守底线》这本书，我提炼出了四个关键词。</p><p>在大学四年中，我受其影响最深，但却一直没有总结，直到写下文章的今天，脑海中突然冒出来的四个关键词，即代表了我为人处世的态度，也代表了我未来会一直努力的方向。</p><p>它们是：认真、正直、努力、坚持</p><p>我会结合自身经历，对其一一进行解释。</p><h2 id="认真"><a href="#认真" class="headerlink" title="认真"></a>认真</h2><blockquote><p>千万不要自己感动自己。大部分人看似的努力，不过是愚蠢导致的。什么熬夜看书到天亮，连续几天只睡几小时，多久没放假了，如果这些东西也值得夸耀，那么富士康流水线上任何一个人都比你努力多了。人难免天生有自怜的情绪，唯有时刻保持清醒，才能看清真正的价值在哪里。  ——于宙  《我们这一代人的困惑》</p></blockquote><p>首当其冲的，是认真。</p><p>认真有很多具体的解释，比如认真的对待自己的生活，认真对待工作，等等等等。<br />而我自己对于认真的理解，则是：“运用科学的方法与自身的智慧，为当前与未来寻找最优解。”</p><p>认真的对待一件事，就意味着不能以“战术上的勤奋掩盖战略上的懒惰”。<br />只有认认真真的去了解，去探知，去努力，才知道一件事情，一份工作是否真正的合适自己。切忌自己跪在地上把自己感动哭了这种情况。</p><p>剩下的三个关键词，在我心目中都同等重要，并无先后之分。</p><h2 id="正直"><a href="#正直" class="headerlink" title="正直"></a>正直</h2><p>堂堂正正的做人，堂堂正正的做事，不搞歪门邪道。<br />于我而言，这会是一种让我内心感觉安稳的生活信念。</p><p>这中间涉及到非常久远的个人历史背景故事，在此我就不做赘述了。<br />还好个人经历的一些黑历史，并没有让我成长为一个病态的人。</p><h2 id="努力"><a href="#努力" class="headerlink" title="努力"></a>努力</h2><p>下面是我很喜欢的一句话：</p><blockquote><p>杀人须就咽喉上着刀，吾人为学，当从心髓入微处用力，自然笃实光辉 — 王阳明</p></blockquote><p>在认真的去观察一件事情，做出决定后，剩下的就是需要自己持之以恒的投入。<br />努力对于我意味着什么呢？<br />想了一会儿，脑海里给出的解释是：“认认真真，拼尽全力，做到最好”。</p><p>就像自己之前在<a href="http://www.bilibili.com/video/av8343038/">【中国诗词大会】武亦姝 cut 合集</a>的第一个视频中，看到了她的自我介绍与准备。最后在总结时她说了一句：</p><blockquote><p><strong>我是武亦姝，既然来了就做到最好吧。</strong></p></blockquote><p>**<br />而我的想法也是一样，既然认认真真的做出了决策，那么来了就做到最好吧。</p><h2 id="坚持"><a href="#坚持" class="headerlink" title="坚持"></a>坚持</h2><blockquote><p>若有恒，何必三更眠五更起；最无益，莫过一日曝十日寒。</p></blockquote><p>很多事情，一味想着快速出成绩，人就容易变的浮躁，而浮躁过后便是放弃。<br />一曝十寒说的就是这个道理。</p><p>但凡是我人生中重要的技能，无一不是由常年累月的积累而熟能生巧的。<br />就算是我最喜欢的编程，也是坚持了半年才摸到了门槛。</p><p>除此之外，生活之中还碰到过许多事情，明明我不是最优秀的那个，但最后却是收货最多的。无他，只是因为我坚持的最久而已。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>把这四个关键字，整理一下，便得到了自己做人做事的理念。</p><p><strong>做人做事堂堂正正，做事之前要认认真真的分析，决定后便要持之以恒的努力。</strong></p><p>这本书教会我的，总结下来，也就是这么一句话。<br />但知易行难，就像韩寒的那句：“听过许多大道理，却依然过不好这一生”一样。真正对自己有用的，并不是大道理，而是自己对于大道理的理解，实践，并将这些“大道理”改造，内化到自身的这一个过程。</p><p>读书也是一样，只是阅读精要，阅读那些给出的道理，自己除了有一种“醍醐灌顶”的感觉外，所获颇微（仅针对个人而言）。<br />真正扩展自己知识边界的，恰恰是自己在读书中大量的思考，交流与查阅的过程。在这个过程中，这些“大道理”从别人的哲理，变成了我自己的想法，我会去实践，会去根据实际情况做调整，会去讲解这些“大道理”。</p><p>正所谓，知行合一。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2019/jpeg/98602/1547372556792-5087813e-7e4e-4337-b671-d1917fbb7a14.jpeg#align=left&amp;amp;display=in
      
    
    </summary>
    
    
      <category term="随笔" scheme="https://blog.lxxyx.cn/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>Transfrom-2018</title>
    <link href="https://blog.lxxyx.cn/2019/01/17/annual/2018/"/>
    <id>https://blog.lxxyx.cn/2019/01/17/annual/2018/</id>
    <published>2019-01-17T13:56:41.000Z</published>
    <updated>2023-01-02T12:32:51.309Z</updated>
    
    <content type="html"><![CDATA[<h1 id="于我而言"><a href="#于我而言" class="headerlink" title="于我而言"></a>于我而言</h1><p>2018 年，于我而言，是剧变的一年。<br /><br>生活产生了剧变，上半年还在学校里聆听着老师的教诲，与同学自由开心的玩耍，而下半年却是投身于阿里，埋头于无穷无尽的工作中。<br /><br>身份产生了剧变，从学生到工作人士，压力也随之而来。而到了年底，则越来越明白，学校已成母校，再回去也难。家园已成记忆中的家园，她闪闪发光，只是我不再是其中的参与者。只记得学校的一切都是那么的清晰，但是却也在慢慢的离我而去。<br /><br>朋友圈产生了剧变，学校里身边的朋友们各奔东西。毕业离别的那段时间，我清楚的知道，我们依然还是很好的朋友，只是日后再难相见。</p><p>学校的生活熟悉且简单，工作后的生活繁复但新鲜，每天都能接触到新事物新挑战。<br /><br>但于我而言，真正走出大学校园的那一刻，发现自己还是想呆在学校，做一个无忧无虑的学生，过着简单的生活，哪怕只有一个月。</p><p>但是时间的残酷之处，在于自己不能回头，也不会等待自己踌躇不前。在校门口望了南昌大学一眼，我拉着我的拉杆箱，上了去南昌西站的出租车。</p><p>想起毕业前给家园的学弟们分享的那两句话：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545007691584-901f4033-0e04-432c-b9df-866eb1722976.png#align=left&amp;alt=image.png&amp;width=800" alt=""></p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545007700166-95a67fd0-3c7e-4d92-bbfb-eb9bba8a8eae.png#align=left&amp;alt=image.png&amp;width=800" alt=""></p><p>我在我的大学，得到了我认为最好的培养，进入了南昌大学最好的社团，遇见了我认为最好的朋友们，做了一些我认为最正确的事情，这样的生活，虽然离开后万分想念，但是却没有遗憾。</p><h1 id="GoodBye-NCUHOME"><a href="#GoodBye-NCUHOME" class="headerlink" title="GoodBye NCUHOME"></a>GoodBye NCUHOME</h1><p>从机缘巧合加入家园，到成为家园的中坚力量，再到南大家园的诞生，最后再到自己的身份变成了老家园，从此不再出现在机房。在家园的三年半时间，如同白驹过隙，转瞬即逝。</p><p>在这儿，我想阐述，家园与我而言，意味着什么？</p><p>这个问题，我思来想去的想了两年多。两年前，佛爷和我说，他希望能给家园找一个关键词来定义她。从那时起，我便也开始了寻找。<br /><br>两年后的今天，我坐在电脑前，脑海不断浮现家园的回忆，就像一幅画卷一样，在自己的眼前展开。在画卷的最后，我看见了家园的关键词：“<strong>家</strong>”。</p><p>家园于我而言，就是我在南昌大学的家，我在这儿成长，我在这儿完成一些了不起的事情，在这儿认识大学里最好的朋友们，也在这儿做出了属于自己的一份努力与贡献。<br /><br>在家园这儿，我生根发芽，结果开花，最后的最后也尽自己努力，培养下一代的家园人。</p><p>在很久之前，我觉得自己是有点自私的。而遇到家园之后，有什么事情需要我去做的，都会全力以赴的去做。那些时候的思路，是“家园需要我，我必须挺身而出”。<br /><br>在家园这儿，我学会了无私，一开始我也疑惑与纳闷，只是一个大学社团而已，为什么自己会有这般的转变？<br /><br>直到今天，我才意识到，其实自己早已被家园同化，成为家园这个大家庭中的一份子，且对于家园，有了真正的家的认同与责任感。<br /><br>而一个人，又怎么会对家里的事情，总是计较得失呢？</p><p><strong>“My NCUHOME.”</strong></p><p>这是我的家园，我的大学生活，我毕业时最难以割舍的部分。</p><h3 id="一些照片与留念"><a href="#一些照片与留念" class="headerlink" title="一些照片与留念"></a>一些照片与留念</h3><p>家园：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545009115056-472290f9-3117-4054-9416-0bf151192e34.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>夜里的家园：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545009156277-6bb30b5c-b4a2-4fca-a8a3-1015776211b8.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>我的工位：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545009076987-1ba4fffe-2930-4209-8560-01c31529b1cd.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>我的工牌：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545009337109-04597166-8e56-4eef-a1a5-df6d796feace.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1546147423543-639e769e-796b-4f46-9ba1-614ff668cb03.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>我的朋友们：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545009265696-97831ebd-1bdf-4bc6-ac70-4a08bafebbfd.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1546147464100-67b18bcc-bfa2-4178-9f11-2da49df990cc.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>七月初的我，带着对学校的无限留恋，踏上了东去的列车。<br /><br>背后是再难回去的校园，与十几年的学生生活，身前是复杂的社会，与余生不可知的生活。</p><h1 id="Hello-Alibaba"><a href="#Hello-Alibaba" class="headerlink" title="Hello Alibaba"></a>Hello Alibaba</h1><p>到公司入职的那一天，天很蓝，拍出的照片也很好看。<br /><br>园区和之前一样好看，多了几座雕像，除此之外没有什么变化。</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545311127954-2082767c-255b-4d0a-a214-d64eedffc04e.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>当时的心情，现在想起来很是复杂。兴奋与不安掺杂其中，不知道自己在期待什么，又在害怕什么。<br /><br>到今天，我理解了我当时的心情。不安源于没有退路，之后的几年间，在这家公司无论遇到何种困难，我再也没有一个回学校躲着恢复元气的机会了，只能硬着头皮去扛，硬着头皮去闯。</p><p>在入职结束，去往工位的路上，自己小声的嘟囔了一句：</p><p>“Hello Alibaba.”</p><h1 id="天猫未来店"><a href="#天猫未来店" class="headerlink" title="天猫未来店"></a>天猫未来店</h1><p>这是一场盛大瑰丽且不应该忘记的梦。梦里有我的朋友，我的心血，与最好的我自己。</p><blockquote><p>下图是云栖大会开幕前夕，天猫未来店场馆</p></blockquote><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545557560819-5717e130-0af0-4ba7-9ec2-75c9851a505e.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>这是我入职后的第一个任务，负责云栖大会天猫未来店的前端开发，并承担前端 PM 的任务。<br /><br>当时的情况就是，开发任务重，时间紧凑，现场情况各种不稳定，团队成员对于这种线下开发的业务，之前基本没有接触过。<br /><br>所以整个下来，压力是非常之大的。</p><p>中间的日子，有住过工地，有扛过物资，也有当过超市店员理货与接待客人。中间的情况多变与复杂，难以言表。<br /><br>在九月份，连续上班 22 天，整个的生活就像是一场马拉松赛，明知终点就在那儿，但是过程却是艰苦漫长。</p><h2 id="遇见最好的自己"><a href="#遇见最好的自己" class="headerlink" title="遇见最好的自己"></a>遇见最好的自己</h2><p>超高强度的工作在给我带来极大压力的同时，也给我带来了极大的收获与成长。</p><p>首先是个人能力的极限压榨。</p><h3 id="硬件"><a href="#硬件" class="headerlink" title="硬件"></a>硬件</h3><p>我从初中开始玩电脑，接触到大量的电脑应维知识，如硬件、系统、维护、Etc.。因此对于电脑硬件等杂七杂八的知识，都有相关的了解。大学时也曾参加朋友的创业项目，维修了 3 位数的电脑（笑，因为修电脑时感觉到了重复与无趣性，所以后面选择转向了开发）。<br /><br>在天猫未来店项目中，前端页面是部署在各种设备上的，如下图中挂着的电视机，闸机上放着的小屏幕等，且这些设备的硬件，是需要前端去做选择的。同时现场需要考虑容灾的问题，比如突然停电等问题。<br /><br>我的应维知识，在选择硬件，部署页面，应对突发情况，都起到了非常关键的作用。它使我在面对复杂的线下场景时，不会两眼一抹黑，而是知道如何一步步的去解决问题，得到最优解。</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545558509353-701ae824-5987-4ecf-86be-dc2f386c81ce.png#align=left&amp;alt=image.png&amp;width=821" alt=""></p><p>Eg：悬挂屏硬件选择与现场停电问题<br /><br>Answer（悬挂屏硬件选择）：上图中后方挂着的四块大屏幕，其实是由一台主机驱动的。屏幕需要实现最好的显示效果，因此需要实现 4k<a href="/60FPS">60FPS </a> ，而如何用一台主机带动 4 块 4k 的屏幕，也是一个难题。最终，我们选择了 4 台具有 HDMI 2.0（1.X 系列智能到 30FPS，带宽限制） 的电视机 + 一台拥有 GTX1080 的服务器（正好实验室里可以临时借用），实现了这个功能。假如对帧率的知识不太了解，那么买来的电视机只能实现 4k@30FPS。假如对 GPU 知识不太熟悉，有可能不支持 4K 输出，也有可能爆显存。 <br /><br>Answer（悬挂屏硬件选择）：主机是放在悬挂的黑盒子中，平时如果断电了，只能爬梯子上去重启，并且连接蓝牙鼠标，进行服务恢复，整套弄下来大概 20 分钟，且需要关闭店门。而我由于对硬件知识的了解，知道主板可以在 Bios 设置来电后自动开机，所以只要启用这个功能，就可以避免需要爬梯子上去重启的问题。且我在电脑中安装了一个远程管理的软件，并设置为开机自启。这样就能确保，我无论在哪儿，出问题可以第一时候在远程进行控制。几次实践后，断电后该悬挂屏恢复服务只需要 3 分钟不到。</p><p>这些事情，看起来与前端无关，但是却与整个项目息息相关。所以我会去重视，会去不断的尝试各种解决方案解决问题，而不是轻飘飘的来一句：“我是前端”，就再也不管不顾了。而这也是我今年一直以来的理念，我是工程师，然后才是前端工程师，身为工程师的话，注定是要解决问题的。在现场人力紧张的情况下，把问题甩给其他同事，并非是明智之选，选择留下，选择解决问题，才是自己应该做的。</p><blockquote><p>Last man standing. —— 《阿里土话》</p></blockquote><h3 id="项目管理"><a href="#项目管理" class="headerlink" title="项目管理"></a>项目管理</h3><p>在项目中，我也负责这个项目的前端 PM 工作，开始着手于项目管理。<br /><br>实践过程中，往往有着跨部门的沟通协作，本部门的工作分配，进度安排等工作，和项目经理一样，细碎，繁杂。<br /><br>同时我也要负责开发，负责 Code Review，负责现场调试时紧急问题的修复，而这一切得益于我在家园大三时的工作经历，整个过程虽然细碎繁杂但是也处理的井井有条，不至于手忙脚乱误了事情。<br /><br>最后的结果也非常喜人，前端页面在整个四天的活动过程中，实现了 0 BUG 的目标。</p><h3 id="技术"><a href="#技术" class="headerlink" title="技术"></a>技术</h3><p>项目里虽然忙碌，但也是实践自己过往想法的一片大好天地。<br /><br>一路做下来，主要集中与前端工程化、HTML5 开发理念、RxJS、TypeScript 等方面。看起来像是名词的堆砌，其实是个人对于前端开发最佳实践的一种实验。</p><p>得益于过往的经验，前端工程化、RxJS、TS 等事物我都有大概一两年的实践与学习经历，项目当中，虽然有很多坎坷，但最后的结果却是好的。</p><p>和我在天猫未来店的总结会里讲的一样，技术方面的事情，我并不准备长篇累牍的去赘述，点到为止即可。<br /><br>毕竟我还年轻，很多事情还没有接触到，也正如我学前端开始一直对自己说的那句话一样：</p><blockquote><p>前端路漫漫，且行且歌。</p></blockquote><h1 id="百阿"><a href="#百阿" class="headerlink" title="百阿"></a>百阿</h1><p>百阿又称百年阿里，是阿里每个新人都要参加的培训，为期两周。<br /><br>在百阿过程中，认识一群不同年龄，不同地域，不同岗位的好朋友们。</p><p>虽然时间不长，但我们的情谊地久天长~</p><p>一起去酒吧夜谈（年轻人的第一次酒吧之旅）：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545740425336-ebdcf6d6-28aa-49e2-bbfd-859f0543d19f.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>一起去做公益：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545740485191-8c131cbd-b4b0-41eb-8994-1db1e2b1803d.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>一起玩钉钉办公楼里的滑滑梯：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545740532155-94b747e1-41fb-48ff-8a1a-d41a05f0c3c4.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>一起跳了《卡路里》这支舞蹈（年轻人的第一次跳舞）：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1546144905629-06d344a3-30db-4676-a2ae-66d218a36502.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>一起在西湖山边的小酒馆吃好吃的，有一种空山新雨后，天气晚来秋的感觉：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1546144665800-2c569e63-0ea7-4760-aa98-4c5c6eb623f6.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1546144596710-06700583-26dc-49f5-a770-ea7baf666ff5.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>在培训的最后，也写下了自己的小目标：</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545740565347-94edb89a-6abe-4bbe-81a8-897f962eb170.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><h1 id="生活"><a href="#生活" class="headerlink" title="生活"></a>生活</h1><p>带表哥和爸妈来杭州玩了一圈，第一次完整的游了西湖，吃了盒马。<br /><br>来之前，我妈在电话里一直和我说，她从小就听说上有天堂下有苏杭，只是从未去过，这次终于可以见识到了。</p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545740269965-881de3db-8c4b-41b5-96d2-bca95bc73d29.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p><img src="https://cdn.nlark.com/yuque/0/2018/png/98602/1545740686787-348b5673-5b6c-4777-b4d6-f4eb46697991.png#align=left&amp;alt=image.png&amp;width=827" alt=""></p><p>带着爸妈，去了雷峰塔，游了西湖，吃了好吃的，也坐船上摇了一个小时。<br /><br>当时的天气很好，西湖很美，我们都很开心。</p><h1 id="2019-新生"><a href="#2019-新生" class="headerlink" title="2019 - 新生"></a>2019 - 新生</h1><p>过完了充满转变的 2018 年，自己也逐渐的稳定了下来，对于生活也有了一定的掌控力。<br /><br>在 18 年的年底，我想清楚了自己在 2019 年，需要做的三件事情。而这三件事，会让我迸发新生。</p><ul><li>Running</li><li>Coding</li><li>Reading</li></ul><h2 id="Running"><a href="#Running" class="headerlink" title="Running"></a>Running</h2><p>随着工作压力的增加，自己在锻炼上的投入愈发的少，身体也在不断的给自己报警。<br /><br>对于运动，我的想法并不是“生命在于运动”，或者“运动是一种爱好”等。</p><p>我想要的，是我的身体可以承受住我的工作压力，可以保障我安全的活下去，安全的工作。<br /><br>“多活几年。”<br /><br>这就是我最真实的想法。</p><p>2019 年，定一个小目标，跑 365 KM。</p><h2 id="Coding"><a href="#Coding" class="headerlink" title="Coding"></a>Coding</h2><p>代码是兴趣与工作所在，也是占据我生活最大的一块。<br /><br>2019 年的目标，定在以下 3 个方向：</p><ul><li>数学</li><li>英语</li><li>架构</li></ul><p>于我而言，一直以来就有的问题是，写业务代码是全然没有问题的，也能按时完成工作，但自己却难以写出好的代码。所以在新的一年，加大在架构层面的投入，属于一件磨刀不误砍柴工的事情。</p><p>数学和英语则是自己在工作中，实实在在遇到的问题。<br /><br>在接下的任务中，有一个是实现一个前端框架，在工作过程中，翻阅了大量的资料。深感英语与数学上的不足，给自己带来的掣肘。同时在这两门学科上的头颅，也是一件磨刀不误砍柴工的事情。</p><h2 id="Reading"><a href="#Reading" class="headerlink" title="Reading"></a>Reading</h2><p>读书是兴趣，也是一种极有效缓解我压力的方式。<br /><br>当静下心来，捧上一本书，边上放一杯温开水，打开台灯，仿佛整个世界就剩我与书。<br /><br>万籁俱静，但我心不平静。</p><p>2019 年，目标是读 27 本书，平均 2 周一本。</p><p>在知识的海洋遨游，与先贤大拿交流，学习接触着那些我知道或者不知道的事情。<br /><br>至于能给自己带来的作用，我向来没有考虑太多，正如胡适先生说的那句话一样：</p><blockquote><p>怕什么真理无穷，进一寸有一寸的欢喜。</p></blockquote><p>学习这件事情本身给自己带来的愉悦与充实感，就已经是莫大的奖赏了。</p><h2 id="新生"><a href="#新生" class="headerlink" title="新生"></a>新生</h2><blockquote><p>“谢谢曾经努力的自己。”</p></blockquote><p>这句话我在实习入职阿里时，说过一句。<br /><br>在我转正的那天，也对自己说过一句。</p><p>如果能实现这些 Flag，我相信明年这个时候的自己，一定会如获新生。<br /><br>期待这明年的自己，可以对着自己说出那句话。</p><p>2019，我准备好了~</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;于我而言&quot;&gt;&lt;a href=&quot;#于我而言&quot; class=&quot;headerlink&quot; title=&quot;于我而言&quot;&gt;&lt;/a&gt;于我而言&lt;/h1&gt;&lt;p&gt;2018 年，于我而言，是剧变的一年。&lt;br /&gt;&lt;br&gt;生活产生了剧变，上半年还在学校里聆听着老师的教诲，与同学自由开心的
      
    
    </summary>
    
    
      <category term="年度总结" scheme="https://blog.lxxyx.cn/tags/%E5%B9%B4%E5%BA%A6%E6%80%BB%E7%BB%93/"/>
    
  </entry>
  
</feed>
