<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>抹桥的博客</title><description>记录前端开发以及生活</description><link>https://ab.kisnows.com/</link><language>zh-CN</language><item><title>2015年终总结</title><link>https://ab.kisnows.com/zh-CN/2015/12/31/2015-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/12/31/2015-year-end-summary/</guid><pubDate>Thu, 31 Dec 2015 07:49:23 GMT</pubDate><content:encoded>&lt;h2&gt;来到杭州&lt;/h2&gt;
&lt;p&gt;回想一下二月二十八号怀着忐忑的心来到杭州。&lt;/p&gt;
&lt;p&gt;那天天还下着大雨，我从机场出来一路大巴加地铁到滨江区。出了地铁站，站口一个三轮师傅一看我拉一个行李箱，明显外地人，隔老远就吆喝，走不走！想想我怎么也是有导航的人，怎么能让你们坑。然后就跟着导航找酒店，结果怎么找也找不到，就在目的地旁边转来转去，问路人也没人听过酒店名，我一度认为是不是订的酒店有问题。最后无奈，又跑回地铁口，掏了二十块上了三轮，结果五分钟就开到了，其实那酒店距离地铁口也就不到一公里，因为下雨视线不好所以我一直找不到。到了酒店，收拾了一下发现行李箱都被雨水打湿了，赶紧打开看了一下，还好，只有基本放在外面的书有点潮，衣服什么的还是干的。洗了个澡，就出去感受这个被誉为人间天堂的陌生城市了。不得不说，杭州城市真的很干净，而且绿化也很好，但是雨真的真的太多了&lt;/p&gt;
&lt;h2&gt;找房子&lt;/h2&gt;
&lt;p&gt;到一个新的城市工作，首要的事情就是找房子。说到找房子，不得不说这是一个非常痛苦的过程。&lt;/p&gt;
&lt;p&gt;从好友三月初来到杭州开始找房子，找了差不多一星期的时间，最后还是实在没办法，两个人第二天都要上班的情况随便找了一个两居室租了下来。这房间说好也还行，说不好那就是太吵了，尤其是天气很冷或者很热人们需要开空调的时候，那噪音，晚上不戴耳塞是没法睡觉的。睡眠质量不好大大影响了我的日常工作和生活，可是由于种种原因，暂时又不能搬家，所以也就在各种噪音中度过了小一年时间。&lt;/p&gt;
&lt;h2&gt;工作&lt;/h2&gt;
&lt;p&gt;本来是拿到余杭区一家公司 offer 的，三月一号要去报到的。但是被我们现在的老大提前一天拉去面试，发的 offer 虽然薪水没给到位，甚至还不如另一家公司给的多情况，依然选择了现在的公司。为什么呢？也许我傻吧，至少现在看来当时我确实是傻。一个公司给你薪水的多少，也就是对你能力的认可有多少。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;整个工作内容，我还是可以轻松 hold 住的，从 PC 到 Mobile，大大小小也有成十个项目，虽然有时会有些棘手的问题，但是在老大的指点下还是轻松解决了。总体上来看，工作内容难度不大，非要说有难度，那也只有工作量上的难度，对这一点我已经无力吐槽了。公司也不知道是因为要上市还是什么原因，整体上都比较浮躁，以至于会出现说让三天搞完一个手机商城这种奇葩的事情。
不过工作归工作，不能因为工作难度不大，就忘了提升自己的技能。这一年时间里，大半的业余时间都用在给自己充电上了 。其实是因为周末只有自己一个人，完全不知道该干嘛，除了偶尔一个人去西湖边走走，也就只能写写代码，聊以慰藉了。&lt;/p&gt;
&lt;h2&gt;其它&lt;/h2&gt;
&lt;p&gt;年初凑了套厨具，像模像样的炒了几次菜，然后基本就再也没有进过厨房；&lt;/p&gt;
&lt;p&gt;六月份我投奔的好友因为网易加班过于严重离开了杭州，启程去了武汉，好友的离去伴随的是换了一个新的室友，爱养花养鱼，有时还会做顿饭给我吃；&lt;/p&gt;
&lt;p&gt;一个人去游泳，泳池玩了一会出去上了个厕所，回来忘了带泳镜直接跳到深水区，然后睁眼一片浑浊，水性不好的我当时吓得连呛几口水，强行稳定情绪后努力游到了对岸。事后想想真是后怕，深水区那么多人，如果当时没能稳定紧张的情绪恰好安全员又没看到我，那我基本上 GG 了，以后一个人还是不要去深水区了；&lt;/p&gt;
&lt;p&gt;陪伴了自己四年的电脑突然散热风扇不转了，淘宝买了风扇装上，风扇是好了，结果屏幕不亮了，换了跟屏线依然没有什么用，只能剁手买了个 23 寸的显示器外接，不过屏大真心爽；&lt;/p&gt;
&lt;p&gt;下雨天早上上班时为了帮一个的哥推抛了锚的车而错过了公交，不过还好的哥免费把我送到了上班的地方；&lt;/p&gt;
&lt;p&gt;一个人跑去看电影，结果看的自己眼泪唰唰的，果然是随着年龄的增长，人也变得越来越感性了；&lt;/p&gt;
&lt;h2&gt;展望 2016&lt;/h2&gt;
&lt;p&gt;2015 年作为自己转行后的第一年，整体上对自己还是比较满意的，但是不能懈怠，要继续保持技术能力的增长，但同时也要提高自己的生活质量，毕竟工作是为了更好的生活嘛。&lt;/p&gt;
&lt;p&gt;在此给明年订个简单的规定：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个月至少写一篇博客&lt;/li&gt;
&lt;li&gt;至少每两周看一次电影&lt;/li&gt;
&lt;li&gt;每两个月看一本书&lt;/li&gt;
&lt;li&gt;加强身体锻炼&lt;/li&gt;
&lt;li&gt;该追的美剧不能落下&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然这些计划并不会执行，o(∩_∩)o 哈哈&lt;/p&gt;
</content:encoded></item><item><title>2016年终总结</title><link>https://ab.kisnows.com/zh-CN/2016/12/10/2016-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/12/10/2016-year-end-summary/</guid><pubDate>Sat, 10 Dec 2016 06:03:10 GMT</pubDate><content:encoded>&lt;h1&gt;工作&lt;/h1&gt;
&lt;p&gt;时间也真是快，从 15 年到现在算来我做前端开发也将近两年了，没想到这一路走过来这么顺利，运气真是挺好的。&lt;/p&gt;
&lt;p&gt;当初自学三个月，然后投简历面试，就拿到了好几个 offer，一切都太顺利了。然后去了一家做互联网金融的公司，老大对我很好，然而待了一年多还是在今年五月份选择了离职。原因很简单，公司的开发模式过于保守，一直待下去真的不会有任何长进了， 整天干的活都像是美工干的活，公司很不重视前端，而且前后端不分离，同一份文件前后端两个人一起改，想想真是痛苦。&lt;/p&gt;
&lt;p&gt;那离职了就要找工作，不得不说，找工作的经历真有意思。&lt;/p&gt;
&lt;h2&gt;找工作&lt;/h2&gt;
&lt;p&gt;有一家创业公司，人家对我还比较满意，但就是薪资没谈妥，就算了，谁知道后来我都拿到另一家公司的 offer 了又说那个薪资可以接受了。&lt;/p&gt;
&lt;p&gt;有一家外企，面试的时候对他们的工作环境非常满意，然而面试发挥太差，连 sass 的循环都写不出来，结果可想而知。面试官还很和蔼，说回去等消息吧，然后我出门不到五分钟就收到了短信，‘您不适合本公司的要求’，嗯，效率真高，挺喜欢这点。&lt;/p&gt;
&lt;p&gt;有一家在滨江的大公司，面试通过了，薪资都谈妥了， Hr 说回去等 offer 吧，一般三天内就会发出来。我很高兴，第二天回原公司就提了离职。结果等了一星期还没收到 offer ，就打电话问，对方很不好意思的回答说，部门公司架构重组，我面试的那个事业部已经没了,,Ծ ㅂ Ծ,,，我也不好说什么了。&lt;/p&gt;
&lt;p&gt;中途还有一堆不靠谱的公司，当然也有我能力不足没通过面试的。&lt;/p&gt;
&lt;p&gt;有一个最直接就是蘑菇街的人问你能接受加班吗，我说偶尔可以，经常不行，然后人家来了一句，那不行，我们这边加班还是很严重的，几乎每天 11 点。然后，就没有然后了。后来小伙伴给内推了阿里，面天猫挂在了三面，又面了滨江的 B2B 部门。&lt;/p&gt;
&lt;p&gt;到二面的时候，接到了网易的面试，面网易的时候谈的很高兴，双方都很满意，最后 hr 问说有没有在面其他公司，我说当然， 比如阿里也在面，现在已经到了二面。然后就有了下面的对话：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hr ：“那如果阿里和网易同时给你发 offer 你去哪个？“
我当时随口一说，“哪个先发 offer 就去哪个”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后当天晚上就电话通知我面试通过了，第二天就发了 offer。现在想想，我真是机智啊，O(∩_∩)O&lt;/p&gt;
&lt;h2&gt;网易&lt;/h2&gt;
&lt;p&gt;找工作的事情就告一段落，来到网易。首先网易给我的第一印象就是，食堂真好（虽然已经吃腻了），仿佛回到了大学时候一样，然后妹子真多，嗯，真的。&lt;/p&gt;
&lt;p&gt;不得不说大公司跟小公司还是有很多差别的。首先，流程上就很规范，不会出现说之前公司那种产品经理过来一会要这样一会要那样的事情，但同时也有弊端，就是会议太多。想想那些会议有没有必要开，还是有必要的，但是会议的整体效率，真是低啊。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;一开始去网易除了业务上的需求，还自告奋勇接了几个前端技术部的非业务工作，可能这一点让老大觉得我积极性比较强，就又比较看好我。中途本来还要做个前端监控系统的，可惜刚搞好前期工作，负责人就离职了，项目也就搁置了。想想也是挺可惜的。&lt;/p&gt;
&lt;p&gt;都说进了网易会长胖，然而短短半年时间，我竟瘦了七八斤。不得不说，工作还是挺忙的，每天都得八九点多才能下班。想想以前六点下班，下班后看两小时书，打俩小时游戏，然后躺床睡觉，劳逸结合，简直完美。现在一下班就跟死猪一样了，什么都不想干，游戏都懒得打，就刷刷微博、A、B 站然后就睡了。看书很少了，偶尔也看看一些前端相关的博客和文章。所以感觉这半年，除了工作经验上的积累，个人知识拓展上几乎是停滞的，长期这样下去肯定是不行的。&lt;/p&gt;
&lt;p&gt;而且我们那条业务线负责人离职后，老大让我顶大梁。虽然这对我来说是个锻炼，但确实亚历山大，每天除了业务需求，还有一堆杂七杂八的事情要处理，搞得焦头烂额。&lt;/p&gt;
&lt;p&gt;不过，还好。公司的同事都挺有意思的，上班整体也挺开心，这到挺好的。&lt;/p&gt;
&lt;h1&gt;生活&lt;/h1&gt;
&lt;p&gt;翻了一下去年的日记，哦，不对，应该叫月记。看来日常生活上还是那个样子，跟去年没什么大的区别。&lt;/p&gt;
&lt;h2&gt;找房子&lt;/h2&gt;
&lt;p&gt;一开年就又是找房子，原来的房东直接涨价 500 块，真心不能接受。就只能开始又一次找房子的旅程了，跑了很多小区，最后在原来房子附近的小区找到一个还行的，不过价格也真是高。这种每年都要找房子的感觉真是不爽。&lt;/p&gt;
&lt;p&gt;找到的房子是个三室一厅的，就拉了个前同事入伙，开始三人合租的日子。&lt;/p&gt;
&lt;p&gt;新室友养了只猫，挺可爱的， 就是那猫实在掉毛掉的厉害，猫主人又很少打扫，这里让人不太愉快。不过还好，大家关系都不错，周末一起看个电影吃个饭，偶尔也去西湖、九溪等地方逛逛，挺好的。&lt;/p&gt;
&lt;h2&gt;方向&lt;/h2&gt;
&lt;p&gt;到现在都还是有点迷茫，还是不知道自己到底是个怎样的人，想要成为什么样的人。有点找不到方向的感觉 。&lt;/p&gt;
&lt;p&gt;年初和同学聊天，聊到想在哪里发展。他说不太想待杭州，又不想回去，比较想去上海。然而年中就结婚并定居杭州了。我呢，是想回去又不想回去，跟所有出来工作的人一样，纠结。&lt;/p&gt;
&lt;p&gt;总想活的潇洒一点，可又给自己画了很多条条框框，画地为牢，把自己困在原地。&lt;/p&gt;
&lt;p&gt;果然，一个人想要改变自己还是挺难的。&lt;/p&gt;
&lt;h2&gt;目标&lt;/h2&gt;
&lt;p&gt;先看看去年给自己定的目标吧：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在此给明年订个简单的规定：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;每个月至少写一篇博客&lt;/li&gt;
&lt;li&gt;至少每两周看一次电影&lt;/li&gt;
&lt;li&gt;每两个月看一本书&lt;/li&gt;
&lt;li&gt;加强身体锻炼&lt;/li&gt;
&lt;li&gt;该追的美剧不能落下&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除了 2，5 其他的一个都没完成，果然这执行力还是不行啊。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;博客自从进了网易就没有更新过；&lt;/li&gt;
&lt;li&gt;书，一年下来就看了四本，好多书买回来，连封皮都没撕；&lt;/li&gt;
&lt;li&gt;锻炼身体，买了瑜伽毯，买了新的泳具，但锻炼总是断断续续的，一直没能坚持下来；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以今年的计划就简单一点吧：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个月至少一篇博客&lt;/li&gt;
&lt;li&gt;每个月都要出去走走，哪怕一个人&lt;/li&gt;
&lt;li&gt;书还是要看的，那就三个月一本书吧&lt;/li&gt;
&lt;li&gt;身体还是要锻炼的，心里有这个事情就行&lt;/li&gt;
&lt;li&gt;写一个个人项目，并发布出来，无论项目多水&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;感情&lt;/h2&gt;
&lt;p&gt;&amp;lt;div style=&quot;opacity:0&quot;&amp;gt;→←&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h1&gt;最后&lt;/h1&gt;
&lt;p&gt;整体的感觉，就是这一年过的真快呀！转眼毕业都两年多了，有时都觉得挺可怕的，必须得再次告诫自己：
&lt;strong&gt;&lt;em&gt;Seize the day.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>2017年终总结</title><link>https://ab.kisnows.com/zh-CN/2018/03/28/2017-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2018/03/28/2017-year-end-summary/</guid><pubDate>Wed, 28 Mar 2018 22:03:41 GMT</pubDate><content:encoded>&lt;h2&gt;工作&lt;/h2&gt;
&lt;p&gt;年初，公司经历大的变动，整个理财业务线都干掉了，一大批人转岗的转岗、离职的离职。前端技术部这里，人也走了几个，六楼本来 5 个前端，最后只剩下了 3 个。原来的项目也都不做了，开始做的新的项目网易白条。风风火火做了将近一年，现在产品的数字还比较好看，年后就会上公开申请制对外正式开放，前景还是不错的。&lt;/p&gt;
&lt;p&gt;只是，现在楼上两个前端年后又要走一个人，流动性那么大，没有一点归属感。加上阿里很多部门给我打电话让我过去面试，想想网易不怎么高的工资，有时候都有点动心，但还是一一拒绝了。起码在网易待满三年，好好沉淀一下。也磨练一下自己的性子，否则天天换工作，也不利于长久的职业发展。只是最近新上的区块链项目，虽然我没参与，但能让我感觉整个部门非常的浮躁，又在犹豫呆下去是否是正确的。&lt;/p&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;p&gt;今年整体上还是过的像一碗白开水，中间偶有放点糖，但很快也就没有味道。&lt;/p&gt;
&lt;p&gt;身体上已经明显能感觉到开始横向发展了，体重上了五六公斤，但是身体素质却下降了，引体向上只能做一个，我的天呐，大学的时候好歹也能轻松做十个的，这衰减的也太快了。说要锻炼身体，然而基本上只停留在嘴上，锻炼一次中间能间隔一个月，哈哈。&lt;/p&gt;
&lt;p&gt;有时候觉得无聊，就想给自己培养个爱好，想来想去最后决定学个口琴，投入小，见效快。然后就买琴网上找教程，刚开始的几个月，还比较上心，基本天天练习，到现在不复杂的曲子都可以吹，而且自我感觉还不错。就是因为学的是布鲁斯口琴，低音的 6 需要压音，到现在也不能很好的压出好听的声音，更别提融入到曲子中去了，还需要继续练习啊。现在最喜欢的吹的曲子有两个，一个是 《alone》 一个是 《you are my sunshine》，心情不好的时候就自己吹给自己听。&lt;/p&gt;
&lt;h3&gt;日本旅游&lt;/h3&gt;
&lt;p&gt;11 月左右，公司组织我们出国旅游，有三个地方可选：日本、塞班岛、澳洲。最后我选了日本，也是第一次出国，还是很开心。&lt;/p&gt;
&lt;p&gt;去了北海道，日本的大农村，第一天入住的小镇下午五点大街上就一个人都没有了，跟同事出逛了一圈最后发现酒店附近的居酒屋七点以后开门。一群人进去想去感受日本当地的居酒屋文化，只是老板就一个人，而且跟我们语言不通，完全照顾不过来，看起来有点不是很开心的样子，不过我们喝的倒是很开心。&lt;/p&gt;
&lt;p&gt;第二天去的第一个景点就是个动物园，这也是我第一次去动物园，看到了企鹅、狼、海豹、河马、长颈鹿，就是出来的时候差点掉队找不到组织。&lt;/p&gt;
&lt;p&gt;后来又去了北海道厅、洞爷湖、熊厂、地狱谷、当天晚上入住的洞爷湖酒店环境真好。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2017-year-end-summary/landscape.jpg&quot; alt=&quot;洞爷湖酒店&quot; /&gt;&lt;/p&gt;
&lt;p&gt;中间还去了哪都忘了，最后落脚在札幌市区，然后第二天自由活动。白天逛街买东西，几个大商场同事们都买了好多东西，都是土豪啊，哈哈。&lt;/p&gt;
&lt;p&gt;期间有件很有意思的事情，我想买个 SONY 1000x ，结果和售货员就这个耳机的输入电压是多少沟通了好久，用各种翻译软件都不行，说英语不行、说汉语也不行。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;最后还是找了个湾湾过来，才成功的解决了输入电压的问题，人家是 usb 充电的，不牵扯 110v 还是 220v，然后才成功买到了耳机。&lt;/p&gt;
&lt;p&gt;晚上和同事一起点了个 2w 日元的帝王蟹，看起来很不错，只是实在不怎么好吃。后来还上了电视塔，看了看札幌全貌。&lt;/p&gt;
&lt;p&gt;整个日本一周游，也就到这结束了，玩的很开心。&lt;/p&gt;
&lt;h3&gt;换房子&lt;/h3&gt;
&lt;p&gt;年末，房子到期。房东要把房租从 5000 涨到 6500 ，我的天呐，30% 的涨幅，去年我年度绩效是 A 也就 10% 的工资涨幅。这一看，住不下去了。就和几个合租的室友商量了一下，不租了。&lt;/p&gt;
&lt;p&gt;好吧，又是 TMD 的换房子，真 TM 烦。&lt;/p&gt;
&lt;p&gt;由于时间比较紧急，只有两个周末时间。所以看的房子也不多，一开始想的说找个好一点的小区，不住这些吹拉弹唱的回迁房了。然后看了几个高端小区的房子，结果大多都是隔断，而且价格还很坑爹。没办法，就继续在园小区附近找。房源不是很多，我们周末两天就去各个小区门口贴的小广告上找房子，最后的周末看了三个房子。两个都在路边，太吵，但其他环境还行。一个不靠路，但是在一楼，看房的时候挺安静的。当时就选了这个一楼的看起来挺安静的房子，然而这一切都是噩梦的开始啊。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2017-year-end-summary/me.jpg&quot; alt=&quot;搬家&quot; /&gt;&lt;/p&gt;
&lt;p&gt;搬进去睡的第一天，早上我突然听到一个陌生的声音在我耳边说话，我还以为房间来了什么人，吓了一跳。睁眼一看，一个人都没有，但说话的声音还在持续。拉开帘子一看窗外，我靠，小区起的早的人在楼下聊天，由于住在一楼，而且房子的单层玻璃隔音效果非常差，那声音就跟在我跟前说话差不多。当时都有点崩溃了，看来懒觉以后都睡不了了。而且，最近三月份左右，房间很潮，每天晚上躺床上的时候都能感觉到褥子潮潮的，睡着非常不舒服。这几天就找了个除湿器放房间里面除潮。新租的房子搬进来也差不多两个月了，得出的结论就是，一楼的房子真心不适合居住啊。&lt;/p&gt;
&lt;h3&gt;感情&lt;/h3&gt;
&lt;p&gt;反正很曲折就是了，懒得说。&lt;/p&gt;
&lt;h2&gt;去年的目标&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;每个月至少一篇博客&lt;/li&gt;
&lt;li&gt;每个月都要出去走走，哪怕一个人&lt;/li&gt;
&lt;li&gt;书还是要看的，那就三个月一本书吧&lt;/li&gt;
&lt;li&gt;身体还是要锻炼的，心里有这个事情就行&lt;/li&gt;
&lt;li&gt;写一个个人项目，并发布出来，无论项目多水&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;去年的目标也就完成了 50% 的样子，1 没完成、2 完成了、3 完成了、4 只能算偶尔会想起来、 5 写了个自动交易程序，确实够水，不过也算完成了。总体执行力实在是有点差，制定的计划基本很难执行。&lt;/p&gt;
&lt;p&gt;这工作也快四年了，已经没有太大的心劲了，总觉得缺乏动力。所以今年就一个目标，给自己找点动力，具体下来，那就是找个女朋友，结束单身狗的生活。&lt;/p&gt;
&lt;p&gt;PS： 一个年终总结拖了几个月， 2018 都过去 1/3 才勉强凑出来，实在是烦心的事情太多了，总之就这样吧。&lt;/p&gt;
</content:encoded></item><item><title>过去25年</title><link>https://ab.kisnows.com/zh-CN/2018/12/07/2018-year-end-summary-the-past-25-years/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2018/12/07/2018-year-end-summary-the-past-25-years/</guid><pubDate>Fri, 07 Dec 2018 06:08:24 GMT</pubDate><content:encoded>&lt;p&gt;过了今天，就在这个世界上满满的混了 25 年了，如果要活 100 岁的话，现在过了 1/4 ，算是一个里程碑吧。&lt;/p&gt;
&lt;h2&gt;回首过去&lt;/h2&gt;
&lt;p&gt;过去 25 年，整体上还算比较顺利。&lt;/p&gt;
&lt;h3&gt;侥幸&lt;/h3&gt;
&lt;p&gt;高一班级组织春游，大家去爬山，爬到山顶休息聊天，我看到山下有两个同学。不知道是脑抽了还是怎么了，就非常想下去找他们聊天，于是就往山下走。由于走的不是常规下山路线，所以不是台阶式的，都是野路。就那么走，谁知重力作用导致我越走越快，最后跑了起来。是的，要在外人看来应该就是一个傻逼从山上往山下跑，我就记得我眼前的景象越来越模糊，速度妥妥地超过博尔特了。结果，腿实在换不过来，绊了一下，结果就以一个前空翻的姿势朝山下滚去，那几条人体抛物线应该是画的挺完美的。当时我的大脑一片空白，想着可能我这伟大的一生竟然就要交代到这里了，叹息，不过想想这毕竟也是埋皇帝的地方，值了。还没来得及想其他的，突然被一块超大的石头挡住了，就那么把我拦在了半山腰了。停下来后，感觉头上好像在流汗，就下意识去擦，擦完一看，我靠，竟然红色的，完了，完了，这回得躺医院了。
结果呢，最后去医院检查，只是皮外伤加轻微胃出血，掉了两天消炎针就活蹦乱跳的去学校了。&lt;/p&gt;
&lt;p&gt;真是幸运，如果没有那块大石头可能就真的交代到我大 x 陵的半山腰了。&lt;/p&gt;
&lt;h3&gt;求学&lt;/h3&gt;
&lt;p&gt;后来高中就愉快的打了三年游戏，导致高中三年实验班、重点班、普通班，通通呆了一遍。印象深刻的是，高三的一节化学课上，老师说这个高二已经学过了，我们就不讲了，我：一脸懵逼.jpg。回家赶紧去翻化学课本，发现还真有，就花了一个月的时间把高二课程好好补了下，然后就又愉快的去玩毒奶粉了。以至于高中三年玩网游的经历，直接让我看透了网游的本质，毕业到现在也再没碰过。还有一件有意思的事情，有天晚上我通宵上网，第二天上早读直接趴桌子睡了。我们班主任，一个数学老师，人称“老汉”。过来看我在睡觉，拧着耳朵就把我从课桌上拎了起来，然后让大家安静，“大家不要学他，老是晚上开夜车学习，不知道这样睡太晚会影响第二天上课的吗？” 全班哄堂大笑，&lt;s&gt;都知道我去通宵打游戏了&lt;/s&gt;。&lt;/p&gt;
&lt;p&gt;后来高考，虽然语文没考好，但英语超长发挥，最终以高一本线一分的成绩，勉强上了大学。&lt;/p&gt;
&lt;p&gt;大学吧，选了石油地质专业，想着以后就要像李四光一样为祖国找油了，怀揣着这么一种悲壮的英雄主义情怀进了大学，然后又打了四年游戏。到大三下学期找工作的时候，四级考了四次没过，还连着考了两次 424 。以至于中石油来校开双选会，面试人家前面都聊的好好的，一看四级没过，直接刷了。我当时非常沮丧，然后回宿舍继续愉快的打我的 Dota 。还有件有趣的事情，斯伦贝谢来我校招聘，我去面试。人家一面纯英文面，我这渣渣英语赶紧好好准备了一下，到我的时候我就开始背我准备的自我介绍加专业知识，结果没背两句就忘了后面的，于是就在那胡诌，诌了半天我都不知道我说的是啥。完事，人面试官给我来一句，“you are so lovely” , 然后就这么过了一面，神奇。因为四级没过，当时找工作非常不顺利，然而我还一点都不慌，后来没复习又考了一次四级，竟然过了。&lt;/p&gt;
&lt;p&gt;成绩单还没下来的时候，中海油来校招，拿着简历 + 网上打印的成绩单去面试，终于面过收到了 offer ，不容易。&lt;/p&gt;
&lt;h3&gt;工作&lt;/h3&gt;
&lt;h4&gt;哪里有石油，那里就是我的家&lt;/h4&gt;
&lt;p&gt;大学毕业，先去中海油总部天津培训了一个月，然后被分到了深圳分公司，去开发南海。哇，南海啊，祖国的蓝色国土啊，就要靠我了。在深圳搞了好久的安全培训，然后再一次迎新酒会上，被告知我分到了 HYSY981 平台。我的天，我真是太幸运了，981 可是当时国内最先进的半潜式平台，当时别提有多高兴了。天天盼着出海，在盼星星盼月亮等出海的过程中，本着怕掉海里淹死的恐惧强行自学了游泳。
大概到 9 月底的时候告诉我们可以出海了，然后一帮同事从深圳搞到了湛江分公司，在那里领导请我们大吃了一顿，从下午五点吃到凌晨两点，说是为我们送行。第二天又坐船从湛江到海南，等直升机又等了将近两周。直升机来的前一天，一帮大佬又请我们大吃了一顿。说是出海前吃点好的，从海上回来要再大吃一顿，这是一种习俗，毕竟有出去回不来的危险。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2018-year-end-summary-the-past-25-years/hainan.JPG&quot; alt=&quot;被坑的海南&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;后来到了平台上，前几天都是参观培训，一个外国大佬给我们上课加带领我们参观平台的各个设施。当时发现，我英语好像还不错，基本都能听懂，哈哈。整个培训过程还是挺开心的，平台很大，走走看看吃吃，一周就过去了。参观过程中发现，海水真篮，真美啊，由于平台上出于安全考虑不让拍照，真是遗憾。还有额外的发现，那就是其实会不会游泳并不重要，平台甲板离海面 30 多米，掉下去不死也残，就别指望游泳能救命了。&lt;br /&gt;
培训完下去了一批同学，留下了几个人直接开始干活，而我就是要干活的人中的一个。而且一上来直接整夜班，夜班啊，真不好受。从晚上 12 点开始上，一个中山大学的研究生带着我干活，到凌晨六点的时候下去吃饭，吃完我问师傅，“吃完饭什么时候上班啊”，“吃完就上”，我靠。又上了甲板，当时刚过六点，太阳还没出来，海面还是黑色的，天上一片片的云连在一起，非常的压抑。而我一想到还要上六个小时的班，顿时跳海的心都有了。&lt;/p&gt;
&lt;p&gt;前前后后在平台上呆了小一个月，下来我就提了辞职，这活我干不了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2018-year-end-summary-the-past-25-years/platform.png&quot; alt=&quot;HYSY981&quot; /&gt;&lt;/p&gt;
&lt;p&gt;辞职去办手续，大领导不在，签不了字，我没管直接回了家。父母给我一顿批，说这么好的工作别人想着都找不到，你倒好，没几天就不干了。我脸皮也厚，没管，那会刚好赶上秋招，就又去参加面试，想找一份陆地上的工作，就又去面了中海油，面试过了要签三方的时候我没有。人家直接不要，说只招应届生，毕竟作为国企有社会责任的。我这一心为祖国报效，哪里有石油那里就是我的家的心就此放下。&lt;br /&gt;
打算跨专业考个计算机专业的研究生，毕竟一直感兴趣，而且大学刷了两年知乎让我觉得计算机行业发展不错。就买了几本课本我在家里学习，打算自学去考研。父母知道我的计划后，相当不支持，说我一大小伙子天天窝在家里算什么回事。&lt;br /&gt;
考研的事情就作罢了，那就直接找工作吧，于是刚好有几个同学在做前端，聊了下觉得还可以。就在家自学了两个月，然后就在网上投了几份简历。&lt;br /&gt;
几轮电话加视频面试完，拿到 offer ，新年后就此出发去了杭州。&lt;/p&gt;
&lt;h4&gt;杭州啊&lt;/h4&gt;
&lt;p&gt;去公司报到的前一天，被后来我的第一个主管截了胡，最后毁了约，去了他那。主管对我挺好，我干的也很开心，大概干了一年半，公司新三板上市了，而后整个公司都浮躁的不行。&lt;/p&gt;
&lt;p&gt;我觉得待不下去就出去找工作，去了网易。&lt;/p&gt;
&lt;p&gt;在网易又开开心心干了两年，从刚入职被评价的 “满脸的胶原蛋白” 到“你应该是 88 年的吧”，嗯，成长了很多。原本打算至少干三年的，起码也要干到新建的园区改好搬进去体验一下的。奈何组里的同事被蚂蚁挖的太厉害，跟我一起来的基本被快挖没了，等同全锅端了。加上年初我经济非常紧张，急需人民币，而阿里员工的借呗非常满足我的需求。&lt;br /&gt;
于是就着了老同事的道，来了阿里。&lt;/p&gt;
&lt;p&gt;来阿里前几个月听的最多的一句话就是：“你胖了”，我也确实胖了。&lt;br /&gt;
自从来到阿里，两个月的时间从 60kg 蹭蹭的涨到了 68kg ，我对此也没什么直观感觉。直到有一天早上起床，拿起一条两个月前买的裤子，穿上的时候发现，我靠，扣子系不上。那一瞬间我觉得好不真实，好像做梦没醒，难以置信这样的事情竟然会发生在我的身上。因为我从大学时候的目标就是 65kg ，结果一直到毕业工作三年半了，体重还一直在 60kg 上下挣扎，真的属于怎么吃都吃不胖的那种。而现在体重的暴增让我切实的感到我的身体发生了极大的变化，对此我的解释是：过劳肥。&lt;/p&gt;
&lt;p&gt;可以很直观的感受到工作压力真 TM 大，粗略估算至少是网易的两倍。个人时间被大大压缩，口琴、游戏什么的基本都没时间搞了，日记都快写成季记了，生活也少了很多可能性。&lt;/p&gt;
&lt;p&gt;一度心生悔意，但来都来了，还能跑是咋地，那不成懦夫了，开玩笑。况且我这么一个职业的人，哪能这样。所以就好好在这干呗，平台又大，带我的师兄又好，主管也不错，还要啥自行车。起码干出点事情，也算对得起自己。&lt;/p&gt;
&lt;h3&gt;一夜暴富&lt;/h3&gt;
&lt;p&gt;15 年开始炒股，刚入市就经历了 2015 的大跌，几乎腰斩，起起伏伏一直到 18 年初退出股市和基金。算了下帐，总投资最多六万的情况下竟然还赚了三万多，平均年化收益差不多 14.5%，不错。&lt;/p&gt;
&lt;p&gt;17 年去挖矿，后来觉得不得劲，就去炒币，为此还专门写了个自动交易系统，结果一顿操作猛如虎，两月亏了六千五，没成想，当了一个勤奋的韭菜。&lt;/p&gt;
&lt;h3&gt;四处溜达&lt;/h3&gt;
&lt;p&gt;大学的时候，几乎每年都出去参加地质实习。去的地方吧，都是些荒郊野外，除了我们一同实习的同学，八百米都看不到其他人的那种。爬山走沟测产状，画了很多地质图。出门一把地质锤，天下除我还有谁。当时觉得以后这样的工作好像也不错啊，能游尽祖国大好河山的样子。
&lt;img src=&quot;/imgs/2018-year-end-summary-the-past-25-years/equ.jpg&quot; alt=&quot;HYSY981&quot; /&gt;&lt;/p&gt;
&lt;p&gt;17 年跟着公司去了趟&lt;a href=&quot;https://blog.kisnows.com/2018/03/28/2017-year-end-summary/#%E6%97%A5%E6%9C%AC%E6%97%85%E6%B8%B8&quot;&gt;日本&lt;/a&gt;，感受了下资本主义的寒冷。这也是人生第一次出国，感觉挺新奇，日本城市真的很干净，即使我去的是大农村北海道。只是那边吃饭基本都是吃冷的，我还不太适应。人生第一次去动物园竟然也是在这次出国之旅中，哈哈。&lt;/p&gt;
&lt;p&gt;18 年借着换工作的时间，给自己放了半个月的架，一个人去&lt;a href=&quot;https://blog.kisnows.com/2018/06/09/thailand-travel-notes/&quot;&gt;泰国&lt;/a&gt;玩了一趟。
机票买了后给家人说，家人觉得你人生地不熟，一个人出国不太安全，担心我。我听了后细细想想也打了退堂鼓，但是一看机票退票要折一半价，所以还是去吧。
去了才发现我这蹩脚的英语竟然能正常进行交流，而且出租车司机师傅还夸我英语好，惭愧惭愧。
泰国的风景还是风不错的，消费也低，我一个人也很自由，白天睡到十一点才出门，想逛哪里光哪里，惬意。&lt;/p&gt;
&lt;h3&gt;吹拉弹唱&lt;/h3&gt;
&lt;p&gt;从 15 年来杭州到现在，三年多，一直在公司附近的几个回迁房小区换来换去。先是忍受大早上传教的敲锣打鼓，接着是红白喜事的吹拉弹唱，这吹拉弹唱啊，吹拉弹唱啊，没整改前夸张点，平均一个月一次，一次持续一个星期，到了冬天更不得了，两个月三次的样子。而且这小区的人也特喜欢，就天天凑在一起吹拉弹唱，跳大神。一开始不明白为什么，后来理解了，这回迁房大部分住的都是老年人，基本没什么事情，还不如大家聚在一起弹唱弹唱。&lt;/p&gt;
&lt;p&gt;我就经常在想，要是我老了也是这样那该怎么办，想到这我就很害怕。&lt;/p&gt;
&lt;p&gt;于是就打算给自己培养个兴趣爱好，挑来挑去选择了布鲁斯，简单方便易上手。其实以前也尝试自学过一些乱七八糟的乐器，不过热情都没超过半年，这次是目前为止坚持的最久的一次，到现在断断续续有一年多了，大部分没有低音 6 的曲子都能吹了，偶尔自娱自乐一下也很开心。&lt;/p&gt;
&lt;h3&gt;娱乐&lt;/h3&gt;
&lt;h4&gt;电影&lt;/h4&gt;
&lt;p&gt;基本上上映的电影我都看，&lt;/p&gt;
&lt;p&gt;一个人看，
两个人看，
三个人看，
跟男生看，
跟女生看，
跟男生女生看。&lt;/p&gt;
&lt;p&gt;真的很喜欢看电影，在那两个小时中，你可以什么都不想，全身心投入到电影中，跟着剧情走，这种感觉真的很享受。&lt;/p&gt;
&lt;h4&gt;喝酒&lt;/h4&gt;
&lt;p&gt;喝一点挺好&lt;/p&gt;
&lt;h4&gt;唱歌&lt;/h4&gt;
&lt;p&gt;以前听的是周杰伦林俊杰 SHE，现在听的是张学友、陈升。唱的歌也变成了这样，以至于进了 KTV 一开口，人家说你丫八零后吧，哈哈，真是暴露年龄。&lt;/p&gt;
&lt;h4&gt;口琴&lt;/h4&gt;
&lt;p&gt;自娱自乐，挺好。&lt;/p&gt;
&lt;h3&gt;租房&lt;/h3&gt;
&lt;p&gt;从来杭州以后，基本上一年换一个地方，四年换了三次，也没能逃离公司附近的一个回迁房小区，不过这个情况应该要暂时停止一段时间了。九月份左右的时候我又打算找房子，因为现在这个地方条件实在太差而且太吵太潮，没法住。同事知道后，跟我说可以申请人才公寓，我以为阿里也有这个渠道，结果说是个人申请。我想着随便吧，贵是贵了点，但是条件好。于是就提交了申请，我问了下管理员，要等多久，人家说‘择优则先’，我一想完犊子了，我这一渣本，不知道得拍到猴年马月去，所以也就没当回事。&lt;/p&gt;
&lt;p&gt;每周都出去找房子，但是一直找的不太理想，要么太贵，要么太烂。在我都已经不自己找，全靠中介的情况下，都鲜有合适的，只能说滨江的房租真给力，要是我买的股票像这样涨，睡觉我都能笑醒。&lt;br /&gt;
结果昨天，11 号，人家说我资料过审了，让我去看房子。我提早去食堂吃了点东西就打车过去了，一看，挺好，当即就定了。两个多月的找房之旅就此结束，可以预见两三年内也不换换地方了，开心。&lt;br /&gt;
不过得跟住了两三年的室友分开了，挺伤感的。然而天下没有不散的宴席，C&apos;est la vie .&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2018-year-end-summary-the-past-25-years/rencai.jpg&quot; alt=&quot;人才公寓&quot; /&gt;
&lt;img src=&quot;/imgs/2018-year-end-summary-the-past-25-years/home.jpg&quot; alt=&quot;人才公寓&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;回首结束&lt;/h3&gt;
&lt;p&gt;总体上来说，过去这 25 年还是挺顺利的，而我又是一个很乐观的人，所以对过去还是挺满意的。
这两年也经常听到亲人朋友都对我说“好着呢”，以前从没有听过类似的话，也许是到一个年纪了，周围人对你的期望已经不再是读书怎么样了，而变成生活怎么样了。这几年虽然累确实是累了点，但整体上对现状也还算满意，挺知足的，“好着呢”。&lt;/p&gt;
&lt;p&gt;想起列夫托尔斯泰在《战争与和平》中的一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;生活上只有两种真正的不幸：良心的谴责和疾病，只要没有这两大祸患，就是幸福。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这句话我很认同，所以我觉得我现在就挺幸福的。&lt;/p&gt;
&lt;h2&gt;强行展望&lt;/h2&gt;
&lt;p&gt;最近呢，经济情况不太乐观，又要还房贷又要还借朋友的钱还要交房租，加上日常开销，每月发的工资真的是杯水车薪。幸好我没有太高的物质要求，否则就会很痛苦了。&lt;/p&gt;
&lt;p&gt;除了这些，还非要说有其他不满意的地方的话，那就是以前活的太朴素，太接地气，太单纯，认为人与人之间交流最重要的是真诚。当然这样没什么不好，但生活难免少了很多乐趣，人跟你一打交道，我这交浅言深的，啥都跟人说，也不大合适，是吧。&lt;/p&gt;
&lt;p&gt;所以打算接下来几年，就不要太真实太朴素太单纯了，适当装一点逼，为生活添加一点乐趣，就当作个实验，看看有什么变化。我相信生活方式的改变应该也能对这个世界有不一样的认识和体验吧。就和打游戏一样，同一款游戏，创建角色时选择不一样人物性格，就会有完全不同的游戏体验，跟笛卡尔说的“我思故我在”有那么一点点类似的感觉，就是你的内在思想最终决定了你对外在世界的感受。&lt;/p&gt;
&lt;p&gt;那既然决定了就要做，适当装逼这一点就先从本文开始。&lt;/p&gt;
&lt;p&gt;最近一年来也经常在想以后到底想要过什么样的生活，做一个什么样的人。但一直没想太明白。&lt;/p&gt;
&lt;p&gt;长者说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个人的发展，固然要靠自我奋斗，但是也要考虑到历史的行程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;我也不知道一个南海挖石油怎么就来到了杭州。也许一直待在中海油估计日子会比现在更舒坦，更符合我想当咸鱼的诉求。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;而我对未来还没有一个比较清晰的规划，也不知道能不能跟上历史的行程。&lt;/p&gt;
&lt;h2&gt;年终总结&lt;/h2&gt;
&lt;p&gt;反正已经到年底了，索性连今年的年终总结一起写了吧。&lt;/p&gt;
&lt;p&gt;本来年初的打算是给自己放两年假的，毕竟从毕业开始已经努力了三四年，应该稍微歇息一下，整理整理思路。谁知工作换到了阿里，放假计划失败。&lt;/p&gt;
&lt;p&gt;至于事情吧，要说今年做了什么，总结一下其实也就三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;背上了贷款&lt;/li&gt;
&lt;li&gt;体验了真枪射击&lt;/li&gt;
&lt;li&gt;换了工作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而年初给 2018 定的&lt;a href=&quot;https://blog.kisnows.com/2018/03/28/2017-year-end-summary/&quot;&gt;目标&lt;/a&gt;就一个，结果完全没进展，进度 0% ，完成度 0% ，这执行力不行啊。&lt;/p&gt;
&lt;p&gt;虽然执行力不行，但是目标也是要定的，2019 就以下几个小目标吧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 完成 2017 的目标(可选)&lt;/li&gt;
&lt;li&gt;[ ] 被动收入 500/m&lt;/li&gt;
&lt;li&gt;[ ] 不能把 kindle 变成压泡面的工具&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后，一句话作为生日礼物送给自己：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;奔跑吧，骄傲的少年。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>2019 年终总结</title><link>https://ab.kisnows.com/zh-CN/2020/03/09/2019-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2020/03/09/2019-year-end-summary/</guid><pubDate>Mon, 09 Mar 2020 00:03:20 GMT</pubDate><content:encoded>&lt;p&gt;2019 年我的生活有了很多变化。&lt;/p&gt;
&lt;h1&gt;生活&lt;/h1&gt;
&lt;h2&gt;东北&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2019-year-end-summary/tianchi.jpg&quot; alt=&quot;天池&quot; /&gt;&lt;/p&gt;
&lt;p&gt;元旦，也就是 2019 年的第一天，就跟着同事去东北玩了，想想也是神奇。&lt;/p&gt;
&lt;p&gt;那天早上同事问我元旦假期要不要去东北玩，我想都没想就答应了，连行程和时间都没问。回想起来可能是那会工作的太压抑了，需要放松一下，所以一听到出去玩，就直接答应了，至于去哪怎么玩都不重要。&lt;/p&gt;
&lt;p&gt;现在回想起来，去了哪些地方都已经不太记得了。&lt;/p&gt;
&lt;p&gt;印象深刻的就几件事情：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 和同事以及滴滴司机说服了另一位同事不去冰雪大世界
- 凭着不怕摔的精神，学会了滑雪，说到学滑雪就让我想起来第一次去滑冰的时候，也是凭着这样的精神学会的，虽然摔的很惨，还是挺欣赏自己的勇气的
- 与旅游团的伙伴一起玩黑魔法 &amp;amp; 狼人杀，其乐无穷
- 和团里所有的妹子合了一张影
- 最重要的，认识了现在的女朋友
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以这次东北之行还是收获颇多，这也是第一次主动和同事出去玩。
&lt;img src=&quot;/imgs/2019-year-end-summary/jianying.jpg&quot; alt=&quot;剪影&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;哼哼&lt;/h2&gt;
&lt;p&gt;在东北的几天里，很多时间都在大巴上度过，我座位前面坐的是两个从上海来的妹子，其中一个开始自我介绍的时候竟然说是来找对象的，惊了。&lt;/p&gt;
&lt;p&gt;在雪乡的时候，有妹子戴了个墨镜，挺了个白色的大羽绒服，挺帅的，吸引了我的眼球。吃饭的时候，大家在玩黑魔法，而坐我旁边的从上海来的墨镜妹子，说自己好像知道什么是黑魔法，但是想不起来。于是我就通过微信告诉了她，在桌上说怕泄露黑魔法的秘密（才不是为了加妹子微信。中间几天也都略有互动，但是基本都通过另一个从上海来的妹子做中转，哈哈，挺有意思。最后东北行程结束的时候，我就约了两位妹子吃个散伙早餐，我竟然约妹子吃了个散伙早餐，还挺新颖的。吃完我从包里拿出了俄罗斯套娃，对，俄罗斯套娃，我也不知道自己怎么想的，就送给了墨镜妹子，说是作为离别礼物。在过程中竟然有人说空气中弥漫着恋爱的酸臭味，哼。&lt;/p&gt;
&lt;p&gt;东北之旅很快就结束了，但是和墨镜妹子的故事才刚刚开始。回来后，每天下班就聊天，是的，之前在东北微信上基本没聊过，回来后才聊的。一开始是聊自己喜欢的音乐，聊着聊着就越聊越多，最后把我聊到了上海，就跟网友见面一样。当然一起来上海的还有积极的 ds ，见面的除了墨镜妹子还有一起的另一个妹子。妹子很上心，大老远从昆山跑过来，先吃海底捞再射箭，然后又唱 ktv ，第一天行程很满。第二天就比较佛性，散散步、逛逛公园，聊聊天，总之很愉快的一次见面。&lt;/p&gt;
&lt;p&gt;后来又约了几次，带妹子走遍了杭州的各大景点，然后墨镜妹子就成了我女朋友。有时自己都觉神奇，出去玩了一趟就认识了个心仪的姑娘，然后还成功脱了单，真神奇。&lt;/p&gt;
&lt;p&gt;中途也得知，另两位同学也略有互动，原来积极的 ds 别有所图啊，哈哈。&lt;/p&gt;
&lt;p&gt;后来的生活就从一个人变成两个人了，有人挂念，也有了牵挂。两个人做做饭、躺躺沙发、看看电影、玩玩游戏，有了小小的幸福。&lt;/p&gt;
&lt;h2&gt;日本&lt;/h2&gt;
&lt;p&gt;年中跟着团队去了日本团建，是的又去了日本。&lt;/p&gt;
&lt;p&gt;除了第一天去环球大世界，后面都是自由行，凭借着丰富的肢体表达能力，也能与当地人沟通，玩的还挺开心。
&lt;img src=&quot;/imgs/2019-year-end-summary/nihong.jpg&quot; alt=&quot;日本村落&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;新型肺炎&lt;/h2&gt;
&lt;p&gt;农历春节前，突然爆发了新型肺炎，这种类似 SARS 的传染性基本突然就在武汉弥漫开了。以至于每个人都度过了完全不一样的春节，不走亲戚、不串门、不聚餐，所有人都宅在家里，尽可能避免和人接触，以免患上这一疾病。各村各县也都采取了严格的出入管制，从不曾经历过这样的事情。&lt;/p&gt;
&lt;p&gt;截至今天全国已经有八万多人感染，三千多人死亡。而造成这一悲剧的，既有天灾又有人祸，疫情中各种牛鬼蛇神都出来了。从一开始官员瞒报，央视新闻把传出疫情状况的人定义为谣言并大肆播报谣言可耻，这其中就包括了被人们称之为吹哨人的李文亮医生，到专家说可防可控到后面终南山院士出来说明实情的全面爆发，再到武汉全面封城，火神山雷神山医院的极速搭建并厉害了我的国以及后面治疗过程中对各种宣传中药牛逼的报道，世态炎凉。&lt;/p&gt;
&lt;p&gt;好在疫情中有一个个平凡而又伟大的人，外卖小哥、基层工作人员以及医生和护士的坚守岗位，都让我们觉得人间还是值得的。多的也不能说，感谢疫情前线中每一个奋斗的人，也希望悲剧不要再次上演。&lt;/p&gt;
&lt;h2&gt;小事&lt;/h2&gt;
&lt;p&gt;2.29 号收到一个论坛的提醒：
&lt;img src=&quot;/imgs/2019-year-end-summary/2.29.png&quot; alt=&quot;2.29&quot; /&gt;
四年前的 2.29 号希望自己的工作能有好的发展，同时不要再为了租房而疲于奔命。四年后的今天看，都基本实现了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  1.  入职了两家知名的互联网企业，发展也不错。
  2. 租房上，暂时住人才公寓，也不用为此疲于奔命了。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这位帖子的帖主还是很用心的，对这种用心的人很有好感，也很佩服，毕竟我自己都忘了四年前我还在这个贴子上为四年后的自己做过期望。&lt;/p&gt;
&lt;h1&gt;投资&lt;/h1&gt;
&lt;p&gt;a 股从年初就开始大涨，到年末的时候，连很多基金都涨了 30% ，个股也基本都是大涨。而我年初穷啊，各种贷款加上税，导致再次入市时已经是六月份了，错过了前面的大涨，在小幅震荡上涨的行情中，依靠股票和基金搭配，勉强把收益率维持在了 6~7% 左右，远低于往年水平。&lt;/p&gt;
&lt;h1&gt;工作&lt;/h1&gt;
&lt;p&gt;今年是在阿里的第二年。&lt;/p&gt;
&lt;p&gt;不得不说阿里的工作强度是真 TM 大，每天上班都跟打仗一样，从早到晚，精神得一直紧绷着，丝毫不得松懈，一天班上下来真的是累。工作节奏基本上 995，虽然没有 996 ，但我明显感觉这小身板不太能扛的住。不知道其他人都是怎么待那么久的，有时怀疑是不是自己水平有限导致能力 cover 不住，所以感到累。但从网上各种渠道看到，都在说阿里累，看来不是我的个人问题。想了下，还有很多部门 9115 或者 9116 ，就真的是佩服那些部门的同事，牛逼，只能说牛逼，身体素质真好。&lt;/p&gt;
&lt;p&gt;技术上感觉没什么成长，主要都是扑在业务上添砖加瓦，一年没有学任何新的技术，我自己归因为没有时间。但是在做事情的方法和能力上有了很大的进步。今年一整年在推一件事情，就是融合我手头的一个业务的三端技术方案。原本无线端是 weex，pc 是 react，别提 weex 有多恶心了，所以就想着把两套技术融合为一套。由于我大部分时间都在支持业务，所以为了推进这个融合项目，也是做好了长期的规划和方案。一步步的推进，先验证 weex 是不是可以干掉，在借助业务项目打通新版技术方案，同时慢慢下掉一些老的页面，到今天基本上已经融合完毕。&lt;/p&gt;
&lt;p&gt;对于这件事情还是很满意的，虽然技术含量不大，但是过程中和人合作、推动依赖方以及用数据和事实去说服一些异见者，对自己的职场成长应当是有很大的帮助，这一点还是很开心的。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://github.com/996icu/996.ICU/blob/master/README_CN.md&quot;&gt;996.icu&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;年中爆出了 996.icu 事件，本以为这种运动能给现在畸形的资本压榨带来一点改变，让各大企业收敛一点，按照劳动法来，让大家能回归朝九晚五的良性竞争环境。不料，虽然事情一度闹得很大，但很快就被平息了。&lt;/p&gt;
&lt;p&gt;究其原因，是政府默认现阶段就当如此吧，这一点可以从微博扯热搜，各大国内浏览器厂商，包括 QQ 浏览器、360 以及 UC 浏览器等，以违反相关法律规定为由屏蔽 996.icu 的项目地址可以看出来。&lt;/p&gt;
&lt;p&gt;同时社交场所一些人的评论真是令人心寒，有人替你发声应该感到庆幸，而不是抹黑说什么人家是炒作、是境外势力、是自己懒。这些自己每天被压榨不敢说话怂着就好了，不能要求人人都是英雄，但竟然攻击替你争取利益的人，真难以理解他们的脑回路。&lt;/p&gt;
&lt;h1&gt;目标 Review&lt;/h1&gt;
&lt;h2&gt;去年的目标&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] 找到女朋友&lt;/li&gt;
&lt;li&gt;[ ] 被动收入 500/m&lt;/li&gt;
&lt;li&gt;[x] 不能把 kindle 变成压泡面的工具&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;完成情况&lt;/h2&gt;
&lt;p&gt;3 个目标，完成两个，第二个目标努力了，但是没能成功。
对去年目标整体上的完成情况还是非常满意的，毕竟第一个目标完成就已经超出了自己的预期，哈哈。&lt;/p&gt;
&lt;h2&gt;今年的目标&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 整理一套适合自己的财务规划方案&lt;/li&gt;
&lt;li&gt;[ ] 重新学习英语&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;总结 &amp;amp; 反思&lt;/h1&gt;
&lt;p&gt;今年总体上还是对自己比较满意的，生活上有了女朋友，工作上发展还不错。&lt;/p&gt;
&lt;p&gt;但也有一些不好的变化，就是个人时间被大大压缩了。以前还会写日记、时不时也更新一篇博客，还有时间打游戏发呆练口琴鼓捣一些乱七八糟的东西，而现在基本很少有这样的时间，连一个人静下来思考的时间都很少了，这是一个很不好的信号。长此以往下去，必然是恶性循环，对生活和工作都不好，甚至不利于身心健康，需要找到解决的办法。&lt;/p&gt;
&lt;p&gt;有时自己反思为什么会这样，但没找到答案，不知道是个人责任的增长带来的，还是自己的时间规划有了问题，仰或就单单是因为工作压力太大导致的。&lt;/p&gt;
&lt;p&gt;总之希望新的一年里，能解决这个问题吧。&lt;/p&gt;
</content:encoded></item><item><title>2020 年终总结</title><link>https://ab.kisnows.com/zh-CN/2021/01/01/2020-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2021/01/01/2020-year-end-summary/</guid><pubDate>Fri, 01 Jan 2021 00:03:20 GMT</pubDate><content:encoded>&lt;p&gt;今年总体上是心情比较消沉的一年。&lt;/p&gt;
&lt;h2&gt;工作&lt;/h2&gt;
&lt;p&gt;年初谈绩效，老板还说需要努力，哪哪做的还不够好，随后就给了提名了晋升，感觉很神奇。随后认真准备了一周多的晋升材料，还在家里给女朋友试讲了几遍，最终如愿通过了晋升。只是晋升后的待遇提升配股跟外界说的数量差距甚大，据说仅有往年的一半。晋升带来的喜悦并没有很大，随之而来的工作压力确是肉眼可见的增大。&lt;/p&gt;
&lt;p&gt;有些事情也有了新的认识，比如一件事情要不要做，不在于技术的难度，而在于普惠程度，不要怕别人说这个太简单，就这也好意思拿出来分享。只要做的事情真的能帮助到其他人，这就够了。&lt;/p&gt;
&lt;h2&gt;投资&lt;/h2&gt;
&lt;p&gt;今年投资成果还是不错的，当然也是大行情比较好，跟着趋势赚了点小钱。
结果上来看：投资收入占年总收入的 &lt;strong&gt;23.2%&lt;/strong&gt; ，由于今年工作太忙太累导致很多投资操作没有记录，以至于算不出来具体的投资收益率，但保守估计今年收益率应该在&lt;strong&gt;50%&lt;/strong&gt; 以上，&amp;lt;!-- more --&amp;gt; 还是非常满意的。
但不好的地方是，由于行情太好导致一些赌博性质的操作也挣到钱了，影响了投资心态。变得有点浮躁了，比如开始经常操作期权，导致已经接受不了正股那微小的波动幅度。用一个不恰当的词来形容就是“由俭入奢易，由奢入简难”。&lt;/p&gt;
&lt;h3&gt;知行合一&lt;/h3&gt;
&lt;p&gt;拼多多猝死了一个刚毕业的 98 年的姑娘，实在是太可惜了，多好的年华就这样没了。拼多多这样的公司就该退市，不要出来继续拉低整个行业的下限。
于是去美股做空拼多多，但谁知拼多多大涨 12 个点，亏惨了。但没关系，表达了我的态度：这种不把人当人的公司就改退市倒闭。
跟拼多多相对的，地球另一边的企业谷歌成立员工工会，保护劳工权益，然后拼多多员工猝死却是股价暴涨。有时真的不能理解，或者说不愿意去理解。&lt;/p&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;h3&gt;压力的增长&lt;/h3&gt;
&lt;p&gt;我本是一个自认乐观的人，但今年听到好几个人说我老叹气，才感觉不太对。自己注意了一下，发现确实如此。也不知道是从那一天开始，心情变得低落了，后来思考了很久原因，归咎于一下几个原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;年龄增长带来责任的增长&lt;/li&gt;
&lt;li&gt;工作压力的增长&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随着自己年龄的增长，能感受到身上的责任在不断增长。父母年纪大了，身体不如以前好了，妈妈腰不好，爸爸腿不好，每年催着他们体检希望他们身体健康。同时父母也希望我早点成家，毕竟没几年也三十了。和女朋友的家不在一个地方，导致对未来的发展也发生了一些分歧。我期望的是只要能在一个省会城市，过的轻松一点就可以了。而女朋友家里的要求是江浙沪，这三个地方没一个是生活压力小的地方。拿我工作的杭州来说，房子均价快四万了，而且还都是些相对偏远的地方。如果买的话，且不说首付从何而来，就三十年每个月两万多的房贷，想想都头疼。虽然我不是很把这些事情放在心上，但依然是不小的压力。&lt;/p&gt;
&lt;p&gt;工作上，阿里的工作压力还是挺大的，这一点待过的人应该都能切身体会到。而且今年晋升到了 P7 ，压力又进一步增加。&lt;/p&gt;
&lt;p&gt;可能这些压力潜移默化的影响着我，让我不如以前那么笑容常在，但即使如此，相对其他人我还是一个笑容常在的状态。&lt;/p&gt;
&lt;p&gt;日常生活中也多次发现自己手脑不同步，比如打字的时候脑子想的和手打的不同，以至于发现错了都不能理解（就在大这句话的时候，把不能打成了能不）。不知道这是什么原因，以前很少出现，或者至少我自己没能发觉。&lt;/p&gt;
&lt;h3&gt;租房搬家&lt;/h3&gt;
&lt;p&gt;住了两年多的人才公寓，虽然千万般好，但依然还是要搬走了。一个是不到 40 平的地方两个人住还是太小了，一个是对面施工也太吵了，而且人才公寓最多住三年，今年不搬明年也得搬。恰好去一个同时拿里吃饭的时候发现他们住的小区不错，于是下定决心搬家。&lt;/p&gt;
&lt;p&gt;很快找到了一个还不错的房子，跟中介签了合同准备搬家。过程中还出现了一点不愉快，找房子的时候跟中介说了多少次我们不看公寓，只看住宅性质的房子，因为要办理租房补贴。结果在中介一口一个没问题的说辞下，我还是签了个公寓性质的房子。怪只能怪我太容易信任陌生人，才让我在不断忽视同事的不要信任中介的话的劝告中，看着有点奇怪的房产证下，签了租房合同。毕竟我想着，我已经说得很清楚了，我不看公寓只看住宅。&lt;/p&gt;
&lt;p&gt;搬家找了货拉拉，在平台约定价钱之上再加了一包中华和 50 块钱，同时找了同事帮忙，终于搬完了家。搬家过程中发现，我的家当越来越多了，哈哈。挺高兴的，因为我没有因为租房子就舍弃买一些东西，还是勉强维持了比较好的生活质量的：&lt;/p&gt;
&lt;p&gt;这也是来杭州以后的第五次搬家了，算了下六年搬了五次，频率还是高了点，稍稍有点折腾。&lt;/p&gt;
&lt;h3&gt;碎碎念&lt;/h3&gt;
&lt;p&gt;一直以来，我认为：“人活着是为了体验不同的事务”，毕竟只有自己感受到的才是真的。也经常跟别人开玩笑说，如果给我两个选择，一个是脑后插管进入一个自己想要的世界，一个是现实世界继续拼搏。我会选择脑后插管，当然要保证能一直在插管状态下。毕竟所谓的现实世界又怎能证明不是一个虚拟的插管世界呢。&lt;/p&gt;
&lt;p&gt;同时依然坚信，工作是为了更好的生活，为此不会因为工作对生活做太多的妥协。&lt;/p&gt;
&lt;p&gt;整一年娱乐活动上，除了以前一些常规操作。今年多了几项，一个是听播客，比如&quot;故事 FM&quot;，是一个由不同的人来讲述自己不同人生经历的播客，挺有意思。每周也会定期看一下 “STN 快报”和 “Steam 销量周榜”。&lt;/p&gt;
&lt;p&gt;以前坚持写的&lt;code&gt;日记&lt;/code&gt;，近几年也慢慢从 &lt;code&gt;日记&lt;/code&gt; -&amp;gt; &lt;code&gt;周记&lt;/code&gt; -&amp;gt; &lt;code&gt;月季&lt;/code&gt; -&amp;gt; &lt;code&gt;季记&lt;/code&gt; 到现在的&lt;code&gt;年终总结&lt;/code&gt;都要拖一两个月才能写出来。&lt;/p&gt;
&lt;p&gt;有时，也会感慨自己没有为社会做出太大的贡献。仅仅是做了一个普通人能做的事情，看到去乡村支教的青年教师会打心底佩服，自叹不如。&lt;/p&gt;
&lt;p&gt;在有些事情上和女朋友也慢慢有了分歧，感慨要把自己的想法表达清楚真的太难了。人跟人之间几乎是不能互相理解的，感同身受这个词可能真的就是一种描述吧。因为言语或者问题的表达都是有信息损失的，当你从嘴里说出来的时候信息已经丢失一大半了。所以两个人沟通中想要表达清楚一件事情，需要讲述着和倾听者有着同样的知识基础和差不多的人生经历，这样讲述着虽然有信息损失，但倾听者也可以自行脑补一部分。沟通的方式和技巧的提升也能提高沟通的质量和效率，新的一年要努力。&lt;/p&gt;
&lt;p&gt;可能上面这些说出来好像不那么阳光积极，稍微有点丧的感觉。但没有这么严重，哈哈。&lt;/p&gt;
&lt;h2&gt;2020 Review&lt;/h2&gt;
&lt;p&gt;去年的目标：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 整理一套适合自己的财务规划方案&lt;/li&gt;
&lt;li&gt;[ ] 重新学习英语&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;说来惭愧，一个都没有完成，或者说完全都没有开始。&lt;/p&gt;
&lt;p&gt;反思一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作压力确实比较大，现在有点业余时间都拿来放松了，发呆也好玩游戏看视频也好，反正下班后几乎不会去做需要动脑子的事情&lt;/li&gt;
&lt;li&gt;一个是心里确实不是很重视这两个目标，都属于重要不紧急的那种，做的好不好短期都是看不到效果的，以至于潜意识里没有当回事&lt;/li&gt;
&lt;li&gt;另一个原因是，实施起来成本有点高，第一个目标还好，第二个需要长年累月的坚持，而我根本没有好这样的准备&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2021 目标&lt;/h2&gt;
&lt;p&gt;今年的核心思想是需要调整心态，心态调整好了才能更好的工作和生活。同时需要吸取去年目标设定上的教训，要有可操作性，难度不能太高。目标上也不要完全纠结于人生大事，给自己放一两年假也没什么大的关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;射箭，投资至少 3000 元&lt;/li&gt;
&lt;li&gt;至少带妹子出去玩一次，投资不低于 5000 元&lt;/li&gt;
&lt;li&gt;延续去年的目标：整理一套适合自己的财务规划方案&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;今年工作上升了职，生活上和女朋友感情进展也很顺利，也换(租)了一个还不错的房子，还给自己升级了一台高性能的 PC ，应该说是发展不错的一年。但主观感受上，压力变大，略有点迷茫。&lt;/p&gt;
&lt;p&gt;新的一年努力调整心态吧！&lt;/p&gt;
</content:encoded></item><item><title>2021 年终总结</title><link>https://ab.kisnows.com/zh-CN/2021/12/31/2021-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2021/12/31/2021-year-end-summary/</guid><pubDate>Fri, 31 Dec 2021 22:01:21 GMT</pubDate><content:encoded>&lt;p&gt;疫情后的第二次春节，我和女友本打算各回各家（省略各种对于回谁家的争执），各找各妈。我票都订好了，为了自己的老腰还订了一等座，女友也约了当天的顺风车，就等 1.27 出发了。结果出发前一天杭州突然又出现新的病例，这突如其来的变化打破了我们的计划。回家的旅程充满了不确定性，毕竟如果路上或者到了目的地被劝返或者被要求更可怕的强制隔离，那就很吃不消了。&lt;/p&gt;
&lt;p&gt;于是我问了老爸家里的政策，听起来问题不大，应该是可以回家的。只是女朋友很担心，她听到的消息不够确定，怕回家被隔离，想等形式明朗，政策清晰后再决定还要不要回家。那我想就等你一两天再杭州陪你吧，不行的话年 30 再回家也行。还考虑到如果她家回不去，那就跟我一起回家，为此我花大价钱订了一张特等座（没有票了，只候补到这个席位）。&lt;/p&gt;
&lt;p&gt;第二天一早，女友起床一看直接黄码了，她哪也去不了了，我也只能留在杭州陪她了。于是就这样稀里糊涂的要渡过在杭州的第一个春节了，也好，哪也不跑风险小。&lt;/p&gt;
&lt;p&gt;刚好趁着这个时间，对去年做个总结。&lt;/p&gt;
&lt;h2&gt;工作&lt;/h2&gt;
&lt;p&gt;年初由于组织架构调整，大前端团队拆掉了都划分到各个业务小组里去，变化非常大，同时夹杂着一些其他原因，整个大组里面资深的人走了一大半。我入职虽不久，但三年来还没有这么大的动荡。老板也对我的想法比较关注，我都是直说一直以来都有看机会，当然这个不针对最近发生的事情，只是作为一个职业的职场人士，这是应该的，需要一直关心市场的行情。&lt;/p&gt;
&lt;p&gt;当然最后我还是选择继续留了下来，原因也很多，简单说就只能是懒了，加上外面也没有非常合适的机会。留下来也不是一个非常错误的决定，一来因为组织架构调整对我们小组影响不是很大，二来优秀的人变少了不能说完全是一件坏事，毕竟作为留下来的优秀人士，我的发展机会变大了那么一点点，只是我比较佛，倒不是很在意这个，所以整体上在团队的职责和定位没有发生太大的变化。甚至由于前后端团队的合并，在我们店铺小组来说合作更加方便了。提了三年的智能店铺也最终一起达成了方向上的一致，打算今年进行落地。&lt;/p&gt;
&lt;p&gt;整个项目的推进还是很顺利的，到了年中一切都按照计划进行。老板中期谈话，跟我说你这个级别只搞智能店铺是不行的，要搞一点更有挑战性、前瞻性的东西，并给我指了个方向说搞&lt;strong&gt;端智能&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;我当时想法是，我既不是搞端内开发的，又不是搞算法的，这端智能我两头不沾。我说我也不是谦虚，老板你还是另请高明吧。虽然当时老板同意了，但后续还是很坚持。所以我一个前端开发（当然这么局限自己是不好的）又领了一个端智能的探索任务。既然是探索，那就是可以不落地的，我当时是这样想的。后来我在内外网上看了很多端智能相关的文章去做调研，发现如果对端智能的理解局限在客户端端内基于用户数据进行算法预测这一条路上的话，那我真的是什么都做不了。后来我打开了思路，事情貌似有了一点转机。那既然是端智能的探索，端可以是端内也可以端外，智能可以是基于算法的动态决策也可以是基于固定决策树的决策选择嘛。&lt;/p&gt;
&lt;p&gt;于是我就把端智能这个课题缩小到了前端浏览器场景下的固定策略选择，也算是端智能吧。而且可行性很高，先规范化记录用户的行为数据并进行上报，或者直接本地存储计算。然后定义一套规范，可以在后台进行策略的编排和下发。简单举例：产品可以定义一个规则，如果一个人点了两次某个按钮，同时滚动了几屏后就触发一个 Action ，如更换推荐的商品等任何操作。可以同时进行 n 个策略的下发，具体命中哪一个就触发哪一个，然后效果数据回流到服务端。这样的话，所有的判断逻辑都是在浏览器端进行，就算不是端智能，也是端计算吧。而且后期有了标准化的用户行为，以及更多的产品策略效果反馈，下一步就可以进行基于算法预测的策略下发，或者直接把算法模型放到浏览器端运行，那就是真正的端智能了。&lt;/p&gt;
&lt;p&gt;当我刚刚为自己想到了实时策略而开心的和老板沟通时，发现老板想要的根本不是探索啊，他都已经按照常规的端智能的理解想好了实际的落地场景。这样的话，那我这一项年底考核要完蛋啊。&lt;/p&gt;
&lt;p&gt;正当我忧愁不知该如何解的时候，事情有了转机。更大的变化发生了，我们又要进行组织架构调整，而且中间调整了很多次，有一次都已经和新调整的团队大老板谈完话了，后面又调整了。最终就是我不再做我已经做了三年的店铺，而要改去做一个我从来没有接触过的流量业务，好处就是端智能我可以 say goodbye 了，哈哈。&lt;/p&gt;
&lt;p&gt;后续就是我需要负责整个流量域的业务，给了我一个正式员工和两个外包同学一起搞，等于工作职责比原来大了一点吧。&lt;/p&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;h3&gt;流水账&lt;/h3&gt;
&lt;p&gt;今年可谓败家不少，租的房子比原来大了一点，差不多 65㎡ 左右。终于有地方放一张大桌子，真是开心，于是下单了一个 1800*60 的升降桌，放上新买的两个显示器，真是开心啊。又能更加愉快的学习了（当然是当游戏了）。
&lt;img src=&quot;/imgs/2021-year-end-summary/desk.jpg&quot; alt=&quot;个人小空间&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还换掉了客厅房东的老电视，整了个 4k120hz 的 OLED 大电视，说是给女朋友看剧用的。可是女朋友给买了个 Xbox Series X ，就经常被我拿来打游戏了。有一说一，大电视打游戏时真的爽。&lt;/p&gt;
&lt;p&gt;新添置的硬件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LG 32UN880&lt;/li&gt;
&lt;li&gt;LG GN950&lt;/li&gt;
&lt;li&gt;LG OLED 65C1PCB&lt;/li&gt;
&lt;li&gt;Xbox Series X&lt;/li&gt;
&lt;li&gt;Xbox Wireless Headset&lt;/li&gt;
&lt;li&gt;Xbox Elite 无线控制器 2 代 - 《Halo Infinite》限量版&lt;/li&gt;
&lt;li&gt;Haworth HAT 电动升降桌&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可能也因为这个原因，今年真的通关了很多游戏，而且《光环 5》的多人模式以及《彩虹六号：撤离》也玩了非常多的时间，开心啊。这么一看的话，我天天抱怨时间变少了好像没有什么信服度啊，哈哈。&lt;/p&gt;
&lt;p&gt;通关的游戏：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《地狱之刃：塞娜的献祭》（Hellblade: Senua&apos;s Sacrifice)&lt;/li&gt;
&lt;li&gt;《Hades》&lt;/li&gt;
&lt;li&gt;《Warhammer 40,000: Battlesector》&lt;/li&gt;
&lt;li&gt;《仁王 2》&lt;/li&gt;
&lt;li&gt;《战争机器 5》&lt;/li&gt;
&lt;li&gt;《Metro Exodus （地铁离乡）》&lt;/li&gt;
&lt;li&gt;《光环 5》&lt;/li&gt;
&lt;li&gt;《赛博朋克 2077》&lt;/li&gt;
&lt;li&gt;《Warhammer 40,000: Mechanicus》&lt;/li&gt;
&lt;li&gt;《光环 无限》&lt;/li&gt;
&lt;li&gt;《生化危机：村庄》&lt;/li&gt;
&lt;li&gt;《Age of Empires IV》（除了莫斯科，其他阵营战役都通关了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;今年还破天荒看了两部电视剧：《乔家的儿女》、《红楼梦》，自从工作以后我都没怎么看过国产电视剧了，感觉还挺好的。&lt;/p&gt;
&lt;p&gt;看完的剧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《乔家的儿女》&lt;/li&gt;
&lt;li&gt;《红楼梦》&lt;/li&gt;
&lt;li&gt;《dark》&lt;/li&gt;
&lt;li&gt;《僵尸校园》&lt;/li&gt;
&lt;li&gt;《恶魔城》&lt;/li&gt;
&lt;li&gt;《猎魔人》&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;到了书，相较于前面的游戏就少的多了。&lt;/p&gt;
&lt;p&gt;看完的书：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《1984》&lt;/li&gt;
&lt;li&gt;《罗生门》&lt;/li&gt;
&lt;li&gt;《三体》&lt;/li&gt;
&lt;li&gt;《基地（1-2）》&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;投资&lt;/h3&gt;
&lt;p&gt;今年的投资成绩可谓一塌糊涂，去年赚的钱基本都赔掉了，哈哈，只能说来年再接再厉吧。&lt;/p&gt;
&lt;h2&gt;2021 Review&lt;/h2&gt;
&lt;p&gt;去年的目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;射箭，投资至少 3000 元&lt;/li&gt;
&lt;li&gt;至少带妹子出去玩一次，投资不低于 5000 元&lt;/li&gt;
&lt;li&gt;延续去年的目标：整理一套适合自己的财务规划方案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只能说完成了 20% 吧，第二个目标因为疫情搁置了，第三个目标实在是太懒了，坚持了六年的月度资金 review 今年都没在做了。&lt;/p&gt;
&lt;p&gt;鉴于最近几年的目标完成率都在 30% 以下，太难听了，所以今年就不设定年度目标了。这样明年 review 的时候，目标完成率就是 100% 了，哈哈，已经提前感受到开心了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;其实感觉没写完，不过简单 review 一下，感觉去年还是挺开心的。虽然各种压力一直在增多增大，但我对自己还是比较好的。物质生活上从不亏待自己，工作上稳中有进，又是成功的一年啊。&lt;/p&gt;
</content:encoded></item><item><title>2022 年终总结</title><link>https://ab.kisnows.com/zh-CN/2023/01/01/2022-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2023/01/01/2022-year-end-summary/</guid><pubDate>Sun, 01 Jan 2023 19:25:32 GMT</pubDate><content:encoded>&lt;h2&gt;变化&lt;/h2&gt;
&lt;p&gt;今年生活发生的几大变化：买车，领证，租大房子。&lt;/p&gt;
&lt;h3&gt;买车&lt;/h3&gt;
&lt;p&gt;本来 21 年就看了车，奥迪、宝马、凯迪拉克还有本田的店里都看过也试驾过了，但是最后没有买。&lt;/p&gt;
&lt;p&gt;22 年又看车，这次本来预算 10w 左右的车，目标车辆是个 Polo ，打算买来给女朋友通勤。结果看着看着，就从 10w 的车看到了 30w ，尤其是参加了个车展以后。
后来各种对比后采纳了女朋友的意见，定了 Volvo S60 ，虽然我更喜欢 CT5。&lt;/p&gt;
&lt;p&gt;车到现在已经开了七千公里，最大的感受就是：&lt;strong&gt;车买的越早越好，车的好坏不重要，有没有车很重要&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;有了车就方便了很多，可以去周边城试玩，去商场什么的也更加方便了，虽然打车也能解决问题，但是自己的车上是一个更加私人的空间跟打车还是有很大的区别。但是也有不好的地方，比如说，有车以后，就不想骑自行车了，骑自行车的机会就少了，体重就上去了。而且养车开销还是挺大的，不过总体还是&lt;strong&gt;利远远大于弊&lt;/strong&gt;，非常值的一次消费。&lt;/p&gt;
&lt;h3&gt;领证&lt;/h3&gt;
&lt;p&gt;认识三四年终于领证了，也算是一个里程碑吧。不过感觉领证真是没什么意义，领不领证都是两个人一起生活，毫无影响，除非要生孩子的话，不然结婚证就是一张纸而已。&lt;/p&gt;
&lt;p&gt;而且领证后在婚礼的准备上有了很多分歧，什么彩礼啊流程啊之类，麻烦事啊，还是没领证的时候好。&lt;/p&gt;
&lt;h3&gt;租房&lt;/h3&gt;
&lt;p&gt;又换租一次房子，这次换了一个大房子， 150 平米+ 。刚住进去还有点小害怕，现在感觉还是不够大，因为我们东西实在是太多了。&lt;/p&gt;
&lt;p&gt;我也不知道一直租房住为什么有这么多东西，搬家的时候叫了一个大货车，感觉人家一家三口搬家也就那么点东西了，我们两个人东西多的一个货车都没放下，自己还单独搬了点东西。搬家叫了蓝犀牛的师傅，非常靠谱，非常好，我们只需要负责打包，其他的都是两个师傅在搞。最后总共 600 多块钱，这个价格只能说非常的合理。&lt;/p&gt;
&lt;p&gt;房子本身什么都好，旁边就是地铁，小区里面一堆别墅，整体比较静谧，居住环境也挺好的。要说不好的地方就是房东租给我的车位，实在是太难停车了。导致在一个平平无奇的要开车出去的傍晚，从车位出来的时候车门直接碰到墙上，整个车门都凹进去了，最后走保险换门修了 1.4w。后面还出了一次小事故，倒车的时候车头碰到了别的车尾，赔了人家四五百块钱。&lt;/p&gt;
&lt;h3&gt;其他&lt;/h3&gt;
&lt;h3&gt;疫情防控&lt;/h3&gt;
&lt;p&gt;疫情防控总算是停下来了，本来可悲观了，感觉要防控到天荒地老了，中间滋味真是没有经历过的人难以想象，个人的自由各种被限制，出行无法计划。动不动就核酸、集中转运或者封城。十一回家本来问好了政策，说是在家隔离三天就好了，结果三天后我做了核酸打算去出门，事情发生了变化。幸好被我敏锐的捕捉到了不对的风气，第三天当天中午决定马上回杭州，改了票一路高速到机场，到机场后看了下手机一堆信息说是我们那已经封城了，我要晚走一两个小时就完蛋了。那次封城封了挺久，我要是一直待在家里，可能今年的工作就完蛋了。只能说是幸运，或者说是悲哀吧，本来回家玩，结果精神要时刻紧张，小心翼翼的。万幸，我们伟大的 gov 已经宣布：疫情防控获得了伟大的胜利。&lt;/p&gt;
&lt;h3&gt;停车&lt;/h3&gt;
&lt;p&gt;换房子前，那个小区没有停车位，虽然租车位的价格已经 600/m 依然没有车位可以租。所以车每天没有地方停，导致下班后就在路边转悠，看有没有路边的停车位。实在没有停车位的时候，违停了几次还被贴了两次条，实惨。&lt;/p&gt;
&lt;h3&gt;工作业务更换&lt;/h3&gt;
&lt;p&gt;工作吧，做了一年流量业务，感觉真是奇怪，十几个业务方，没有人对具体的承接页负责。后端也很佛，合作起来也挺麻烦的。我们前端要做性能，后端 rt 1s+ ，哈哈，真是服了。&lt;/p&gt;
&lt;h3&gt;箭术&lt;/h3&gt;
&lt;p&gt;箭术有了进步，由于自己使用左手弓而箭馆只有一个左手弓且不好用的原因。自己买了一把弓，虽然没有瞄准器还被箭馆的工作人员开玩笑竟然有人买迪卡侬的弓，不过不影响我每次去玩的很 high 的事实。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2022-year-end-summary/shejian.png&quot; alt=&quot;射箭&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;游玩&lt;/h3&gt;
&lt;p&gt;公司团建去海南玩了一圈，尝试了冲浪，然而由于早上没吃早点的原因，三个浪打翻就没体力了，最终还是没能在冲浪板上站起来，遗憾。&lt;/p&gt;
&lt;p&gt;买车后第一次开车青山湖溜达了一圈，后来还去了一次安吉，挺好的。&lt;/p&gt;
&lt;h3&gt;NAS&lt;/h3&gt;
&lt;p&gt;自己搞了台威联通的 NAS，先是年初买了 453dmin ，用了没几天换了 464c ，几乎是 4 盘位的旗舰了。现在上面搭了一些服务，主要是家庭媒体中心的角色，当然更主要的角色是作为我的玩具而存在：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2022-year-end-summary/nas.png&quot; alt=&quot;NAS&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在它上面花了真是不少时间，主要还是自己对 linux 的一些概念和操作不够熟悉的缘故。&lt;/p&gt;
&lt;h2&gt;一些杂乱的感受&lt;/h2&gt;
&lt;p&gt;对生活的掌控力大幅下降，制定的目标难以完成，执行力越来越差。尤其在业余时间，把一件事情做完变得越来越难，经常被各种各样的事情打断 or 打乱。比如给自己定了五个小任务，结果一天下来，要么是没开始，要么是各种分神导致一件事也没干完。可能也和晚上睡不着，睡的晚有关系吧。&lt;/p&gt;
&lt;p&gt;总觉得没劲，尤其是工作上，感觉从公司组织架构调整拆分掉大前端团队后，整个人的状态从那个时候就没有走出来。&lt;/p&gt;
&lt;p&gt;做重大决定的时候一定要慎重，以前觉得自己是浪子，浪漫主义，走哪停哪，想怎样就怎样，一切随心而动。然而实际上在一些事情的决定上，还是需要深思熟虑，决策前充分沟通思考，而不是无所谓，做了就做了。&lt;/p&gt;
&lt;p&gt;有很多很容易走的路，机缘巧合下走了一条不好走的路，悬崖勒马是一件很难的事情，惯性太大了。&lt;/p&gt;
&lt;p&gt;调整心态，理性乐观、理性悲观、不能盲目乐观，多和乐观的人呆在一起，人还是要阳光明媚才好。同样的话不同的人，不同的语气说出来，差别太大了，会直接影响听者的感受和对此做出的反应，最终影响一件事情的走向。&lt;/p&gt;
&lt;h2&gt;流水账&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;电影 OR 剧集：
&lt;ul&gt;
&lt;li&gt;鬼灭之刃 👍🏻&lt;/li&gt;
&lt;li&gt;The last of us 👍🏻&lt;/li&gt;
&lt;li&gt;铃芽之旅 👍🏻&lt;/li&gt;
&lt;li&gt;狂飙&lt;/li&gt;
&lt;li&gt;电锯人&lt;/li&gt;
&lt;li&gt;哈利波特七部曲&lt;/li&gt;
&lt;li&gt;天气之子&lt;/li&gt;
&lt;li&gt;天注定&lt;/li&gt;
&lt;li&gt;新*奥特曼&lt;/li&gt;
&lt;li&gt;沉默的羔羊&lt;/li&gt;
&lt;li&gt;隐入尘烟&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;书：
&lt;ul&gt;
&lt;li&gt;三体&lt;/li&gt;
&lt;li&gt;基地
&lt;ul&gt;
&lt;li&gt;第二基地&lt;/li&gt;
&lt;li&gt;基地与帝国&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;骆驼祥子&lt;/li&gt;
&lt;li&gt;家庭、私有制以及国家的起源 👍🏻&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通关的游戏：
&lt;ul&gt;
&lt;li&gt;女神异闻录 5 👍🏻&lt;/li&gt;
&lt;li&gt;卧龙:苍天陨落 👍🏻&lt;/li&gt;
&lt;li&gt;战锤40K: Chaos Gate&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;目标&lt;/h2&gt;
&lt;h3&gt;Review&lt;/h3&gt;
&lt;p&gt;由于去年没有定目标，所有今年的目标完成率达到了惊人的 100%，史无前例，非常开心。&lt;/p&gt;
&lt;h3&gt;2023 目标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 能够在射箭馆射 30m 的靶子（意味着 10m 靶子，12 支箭，9支在黄心&lt;/li&gt;
&lt;li&gt;[ ] 体重控制到 70kg 以内，现在实在是有点胖了&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>三十而立 - 2023年终总结</title><link>https://ab.kisnows.com/zh-CN/2023/12/14/2023-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2023/12/14/2023-year-end-summary/</guid><pubDate>Thu, 14 Dec 2023 13:25:32 GMT</pubDate><content:encoded>&lt;p&gt;所谓三十而立：“三十岁的人，应当依靠自己的本领，独立承担应当承担的责任，并已经确定人生的追求与发展方向。”&lt;/p&gt;
&lt;p&gt;今年我已经满三十了，也不知道立住了没有，可能差一些吧，毕竟还居无定所呢。&lt;/p&gt;
&lt;p&gt;整体上来说活的比较糊涂，很多事情都没有经过严谨的思考、或者说深思熟虑的思考就稀里糊涂的做了决定。&lt;/p&gt;
&lt;p&gt;想起昨天公司的 openTalk 会议，主题是要讲当你遇到巨大压力的时候，你会怎么办，如何来释放压力。刚好我那天发烧请假了，不然也会比较尴尬，因为我不觉得我有承受过巨大的压力，自然也就不存在如何释放压力这一回事了。可能说起来有点假，都三十了都没有遇到过压力很大的时候，你富二代吗？当然不是，我自己思考了一下，可能有这么几个原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;起点低，很多人觉得起点低是坏事，但只要心态好，那么起点低全是好处，虽然有点唯心主义，但是人的感受最终还是来自自己的思想而非外界物质环境：
&lt;ul&gt;
&lt;li&gt;对很多东西不奢求，有机会自然会把握，但是没有也不会强求，那自然少了很多烦恼。小学老师给我的评语，初生牛犊不怕虎，但安于现状，不思进取。现在想起来我觉得评价的很好。我真就一直是随着生活的惯性在往前走；&lt;/li&gt;
&lt;li&gt;因为起点低，所以上升空间巨大，可能奋斗到二十五岁才勉强拥有大多数人一出生就拥有的东西，而我已经在这个过程中获得了巨大的成就感，就像白赚的一样；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对自己一直都很好，不主动给自己上压力，从上学到上班都是这样
&lt;ul&gt;
&lt;li&gt;上学的时候从没有逼自己学习，整个学生生涯都很快乐，当然最终结果差了点，这是我应得的；&lt;/li&gt;
&lt;li&gt;上班以后，虽然一开始收入很低，但从不亏待自己，住好的吃好的，每天回到家都是很开心的。工作上前几年对工作充满兴趣，每天下班都会主动学习，所以进步很快，市场大行情也好，所以职业发展也相对顺利；&lt;/li&gt;
&lt;li&gt;这里不得不说，参加工作以后是否还在保持学习真的是很重要&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;父母开明，不给我主动上压力，当然也可能是因为一直相对优秀能够满足他们的期望，但确实开明。念书的时候一直学习比较好，高中另算；工作以后不催婚，结婚后不催生小孩，当然可能是我的老哥哥在前面给我顶着压力。但不管过程如何，结果就是我几乎没有来自父母的压力；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以从来没有感觉到有什么巨大的压力，当然一些小的压力是有的，不值一提。&lt;/p&gt;
&lt;p&gt;因为五年前写过一篇文章，《过去 25 年》，那会也反思了很多，再后来的生活上做过很多改变。现在五年过去了，可以 review 一下改变的成果。当时说的核心的改变就是：“打算接下来几年，就不要太真实太朴素太单纯了，适当装一点逼，为生活添加一点乐趣，就当作个实验”。&lt;/p&gt;
&lt;p&gt;现在来看改变的很好，核心体现就是去东北的时候参加的旅游团在玩国王天使的游戏。我上去就提了一个简单的，操作性很强的要求：“和团里所有的妹子单独拍一张合影。” 放以前，我大概率是不会这么干的。结果也很好，最终收获女友加老婆一枚。&lt;br /&gt;
其他的在工作中，日常生活中也略有体验，尤其是前几年改变明显，现在又有点回归朴素了。&lt;/p&gt;
&lt;h2&gt;年度 review&lt;/h2&gt;
&lt;h3&gt;变化&lt;/h3&gt;
&lt;p&gt;今年生活上没有任何大的变化，哦，办了婚礼，差点都忘了。&lt;/p&gt;
&lt;p&gt;婚后生活怎么说呢，两个人有很多需要磨合的地方，有的时候还是挺生气的。比如我觉得家里的安全是第一，所以安全通道不要放垃圾堆箱子，剪刀放在桌上要注意会不会掉下来等等类似的我觉得十分应该的且非常重要的事情，沟通过很多次还是没有结果。做事情要有做事情的样子，不做就不做，做就好好做，然而队友经常很难做到，比如说要跳舞买了课去几次，说要弹琴我琴买了三年了到现在完整的曲子弹不出来。说要给我拼模型，结果拉了个小桌子放到书房进出口要道上，把一大堆零件放在小小的桌子上面，一会这个找不到了一会那个找不到了，我正在看 cloudflare 的文档呢，让我帮他找这个找那个，感觉就像故意的。当然我身上也有很多问题，也换位思考过但还是觉得自己是问题少的一方，可能来自人的潜意识的自我保护不会把自己放在错误的位置吧。&lt;/p&gt;
&lt;h2&gt;成就&lt;/h2&gt;
&lt;p&gt;今年还是达成了去年的目标，射箭能够满足射 18 米靶子的要求。然而我已经超额完成目标，可以射 30 米的靶子了。而且第一次射 18 米一支箭就上靶，30 米的靶子两支箭就上靶。在这一项上可以打 3.75 了，完完全全超出预期。&lt;br /&gt;
真是喜欢射击啊，要是可以再玩真枪就好了。&lt;/p&gt;
&lt;h2&gt;经历&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;第一次去国内的动物园：杭州野生动物园&lt;/li&gt;
&lt;li&gt;跟随团队团建去了南澳岛&lt;/li&gt;
&lt;li&gt;办了婚礼&lt;/li&gt;
&lt;li&gt;第一次听演唱会：Westlife&lt;/li&gt;
&lt;li&gt;看了亚运会&lt;/li&gt;
&lt;li&gt;去了香港：
&lt;ul&gt;
&lt;li&gt;去了迪士尼&lt;/li&gt;
&lt;li&gt;去了海洋公园&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;感受&lt;/h2&gt;
&lt;p&gt;家里发生的事情，对我的心理还是造成了一些潜在的影响，虽然我自己总觉得应该没有，但从自己对自己的观察还是能发现一些变化。
比如现在对于一些需要耐心的工作意外的事情，真的不如以前好了。比如这篇博客，从 23 年写到了 24 年，还是一篇水文。&lt;br /&gt;
总之还是要调整好心态，接受已经发生的事情，继续前行。&lt;/p&gt;
&lt;h2&gt;目标&lt;/h2&gt;
&lt;p&gt;新的一年就不设目标了，调整好心态就行。&lt;/p&gt;
</content:encoded></item><item><title>2024 年终总结</title><link>https://ab.kisnows.com/zh-CN/2025/01/02/2024-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2025/01/02/2024-year-end-summary/</guid><pubDate>Thu, 02 Jan 2025 16:40:32 GMT</pubDate><content:encoded>&lt;h2&gt;变故&lt;/h2&gt;
&lt;p&gt;2023 年末，队友还在开心的给我准备 30 岁的生日礼物。很用心，吹了气球，做了丰盛的菜。到了元旦还去了北京，看故宫、环球影城，作为一个北方人，这么多年才第一次去北京，很开心的。&lt;/p&gt;
&lt;p&gt;原本 2023 就这么的过去，要展望幸福的 2024 了，意外总是突然出现。今年没有因为会谁家过年而争吵，原本打算先去队友家再回家。然后有一天还在上班，接到了队友的电话，电话中哭着说爸爸可能不好了，得回去一趟。我一听，不太对，赶紧站起来跟对面的老板请假。&lt;br /&gt;
于是去公司接队友，马上出发回家。到了医院，医生的说法很不好，基本上确诊癌症，而且是癌中之王。听完心都凉了，前段时间看着还好好的，突然就得这么个病。而且说是晚期了，可能就三个月左右的时间。&lt;br /&gt;
我们没有放弃，第二天就把人转运到了杭州的医院。而后就是大半年的抗癌历程。杭州、上海各大医院、各路名医基本上都看了一遍，然而一切似是徒劳。&lt;/p&gt;
&lt;p&gt;在这个过程中，我的妻子展现了强大的勇气和坚强的品质，我想如果我面临同样的境遇时，大概率做的不如她。最终人还是拉回了老家，在见了大部分亲人后就走了。&lt;/p&gt;
&lt;p&gt;这个求医过程，只能说患者面对医院和医生时真的太不对等了。浙二的一个医生，号称副主任医师，结果在几乎没有告知我们任何信息的情况下说可以做手术了，问手术如果成功的话，五年存活率大概是什么样子，手术整体的成功概率是什么样子。医生直接一句“我又不是算命的，我也不知道”，当时就无语了。&lt;/p&gt;
&lt;p&gt;后续回家参加葬礼，整个过程如同做梦，亲眼见证了岳父因为癌症去世，对我有很大的冲击。&lt;/p&gt;
&lt;h2&gt;工作&lt;/h2&gt;
&lt;p&gt;参加了公司的五年陈受戒仪式，其实那会已经加入阿里六年了。当时满五年时刚好赶上 1+6+n 的拆分，以为这个受戒仪式都要没了。毕竟过去一年了都没有反应。&lt;br /&gt;
实际上参与的过程也没什么感觉，一个是组职责从阿里集团缩小为了阿里国际。想要感受其他人说的“味很冲”的合伙人演讲也没有，只有内部的高管的一些讲话。不过感觉还是比较接地气，朴实的，没有那么多花里胡哨的或者全是远景之类的空话的。&lt;/p&gt;
&lt;p&gt;老板们相继离职，作为一个前端这个岗位来说，机会基本上没有了。&lt;br /&gt;
工作真的缺乏动力，有时候不知道是因为自己的心境变了，还是外在环境真的变了，导致工作很没有激情。不过还是在工作中自己给自己找到了一些可以做的事情，偶尔还会觉得挺有意思。&lt;br /&gt;
当然工作本身我还是认真负责，大家对我评价也还行。&lt;/p&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;p&gt;老爸腰椎不舒服，带他来杭州检查，原本计划在这边做手术，最后还是回西安做了。&lt;/p&gt;
&lt;p&gt;每年聚那么一两次的高中同学们，今年有一个应该是要润了。能在杭州真的是缘分，对于家乡都在很远的地方的人来说。&lt;/p&gt;
&lt;p&gt;去了一趟日本，冲击不大，但是还是有颇多感受。京都的生活节奏并没有那么快。之前也去了几次日本，但都不如这次来的感受强烈。&lt;/p&gt;
&lt;p&gt;今年对自己的一些消费观、金钱观有了一点点怀疑。比如买房这件事情，可以选择现在背负压力贷款买房，也可以选择暂时租房以观望，也许将来不在这个城市，为此背上枷锁还是很痛苦的事情。毕竟西安的房子卖掉后的感受真的是很好。
但租房的体验确实是差一点，即使我已经付出相当高昂的租金，并且在日常电器上也投入很多，尽量不因为租房而带来生活质感上的下降，但体验终究是和自己的房子差一点，毕竟心态不一样。而且由于没有买房，现金压力没那么大，队友在消费上也非常的大手大脚吧，也许买了房子有了一定的压力是一个更好的结果？以前我是坚定的租房派，但现在有一点点觉得自己可能错了的感觉。&lt;/p&gt;
&lt;p&gt;老家盖了房子，耗资不少，意义不大，毕竟住不上。&lt;/p&gt;
&lt;h2&gt;目标&lt;/h2&gt;
&lt;p&gt;今年还是要把心思放在家庭上，毕竟家庭是最重要的，对这一点的认识更加深刻了。&lt;/p&gt;
</content:encoded></item><item><title>2025 年终总结</title><link>https://ab.kisnows.com/zh-CN/2026/05/10/2025-year-end-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2026/05/10/2025-year-end-summary/</guid><pubDate>Sun, 10 May 2026 04:54:34 GMT</pubDate><content:encoded>&lt;p&gt;本来拖着拖着都不太想写了，毕竟已经 2026 年中了。但看到女儿又觉得还是得写一下，毕竟现在身份和去年不一样了，就当作按照公司的财年时间来算吧，刚好现在四月底五月初，算是2026财年结束，对去年做个总结。&lt;/p&gt;
&lt;h2&gt;升级了&lt;/h2&gt;
&lt;p&gt;今年最大的变化毫无疑问是女儿出生，我升级当爸爸了，虽然很多人问我当爸爸了什么感觉，我都说没什么特别的感觉。但每天抱在怀里的，看着这么小的生命，还是觉得很神奇。&lt;/p&gt;
&lt;h3&gt;怀孕生产&lt;/h3&gt;
&lt;p&gt;队友怀孕的过程并不是一路轻松，中间有几次还是挺吓人的，好几次晚上说肚子不舒服，我们凌晨开车去医院急诊，虽然最后医生检查后都说问题不大。最后一次去急诊的时候，医生都以为我们可能要生了，结果急诊医生检查后让我们走出去的时候被急诊的护士拦住说你们怎么走了，她都以为我们要住院了，最后医生还是让回去再等等。&lt;/p&gt;
&lt;p&gt;孩子的胎位一直不正，前期一直是臀位，而且绕颈一圈，我们都很担心。后来到了 37 周左右她自己转了过来，都以为可以顺产了，结果最后一次产检又变成了臀位，本来是可以顺产的，结果最后一次产检不知道为什么胎位又变了。一般到了后期胎位不会再怎么变，这种情况属于胎位可变，医生不建议顺产，只能剖腹产了。&lt;/p&gt;
&lt;p&gt;在手术室门口等的时候，我好像没有想象中那么激动。可能整个怀孕过程我都参与得比较多，没有一个突然的变化，可能潜意识里已经习惯了。大概等了一个小时，队友出来了，看起来状态还可以，没有很虚弱。护士喊爸爸过来看了一眼，说是个女儿，然后就把宝宝推到病房去了。&lt;/p&gt;
&lt;h3&gt;抱到手里&lt;/h3&gt;
&lt;p&gt;宝宝出生后很健康，可以直接回病房，好像说评级是10分。隔壁床的孩子生下来后直接去了新生儿重症监护室，我们还是比较幸运的。刚出生的时候孩子很小，很可爱，小手小脚（就是稍微有点黑），是一种很神奇的感觉。&lt;/p&gt;
&lt;p&gt;住院一开始因为病房紧张，只能在楼道，后来搬到了三人间。在这个三人间里，我简直就是模范丈夫。晚上宝宝一哭我就起来照看，哄不过来就去找护工。其他两床的丈夫一个白天在，一个晚上在，但基本不管，只负责打呼噜，真是吵死我了。不过我们孩子打了卡介苗以后晚上哭得很厉害，把中间一床的人都哭走了。当然也可能和剖腹产后妈妈暂时没有奶有关，最后找护士要了水奶，喂完就好了很多。&lt;/p&gt;
&lt;h3&gt;回家&lt;/h3&gt;
&lt;p&gt;医院住了大概三四天，就回家了，中途还装上队友闲鱼买的二手婴儿座椅。但是出院的时候我看没法坐，应该用提篮放车上的，没办法最后让丈母娘抱着坐车回去。我觉得很不安全，毕竟一个急刹车可能孩子就扔了，所以我开车开得很慢，城市道路开慢点还是相对安全的。&lt;/p&gt;
&lt;p&gt;回家我妈不知道从哪里看的说是要让孩子回家先摸一下苹果和米缸，代表衣食无忧和平平安安，图个吉利。&lt;/p&gt;
&lt;h3&gt;名字&lt;/h3&gt;
&lt;p&gt;起名字也很难。我们从怀孕到出生想了很久都没有想到很满意的名字。直到有一天晚上睡觉，我脑子里乱想，突然想到两个名字，一个代表太阳，给男生用；一个代表月亮，给女生用。我们俩都觉得不错，先做了备选。后来孩子出生是女儿，就用了月亮这个名字。&lt;/p&gt;
&lt;p&gt;原本还想再想想有没有更好的，但一直也没找到，再加上要办出生登记和户口必须要名字，最后就这么定了。名字都是很简单的字，没有专门去凹。&lt;/p&gt;
&lt;p&gt;现在感觉还是挺好的，非常的女生，而且很有画面感，我想她一定会喜欢的。也希望她像名字里那样温柔、有灵气。由于名字的意象是月亮，所以小名取了“昕”这个字，代表太阳将要升起的时候，黎明、晨光，内敛的同时有朝气，和夜晚互补一下。&lt;/p&gt;
&lt;h3&gt;情感&lt;/h3&gt;
&lt;p&gt;最近一年整体上都还可以，队友好像成长了一样，脾气没有那么差了。&lt;/p&gt;
&lt;p&gt;但最近可能因为生了孩子的原因，激素波动导致的脾气又变差了，经常一句话不对付就要甩脸，而且不知道是不是小红书这个粪坑刷多了，经常说话就是你们怎么怎么（指我和我妈），我听着这种话真的很不爽，不应该是一家人吗，有的时候真的挺烦的，不知道怎么办。经常上一天班回到家里本来很开心，但几句话不对付情绪就变得很差，整个家里氛围都不轻松，难不成要变成那种每天下班回来还在车库里呆一会的男人了吗。&lt;/p&gt;
&lt;p&gt;妈的，我不想变成这样。&lt;/p&gt;
&lt;h2&gt;工作&lt;/h2&gt;
&lt;p&gt;2025 年工作的关键词毋庸置疑应该是 AI。&lt;/p&gt;
&lt;h3&gt;从人机协作到 AI 主导&lt;/h3&gt;
&lt;p&gt;AI 的发展实在是太快了，现在看我去年写的文章 &lt;a href=&quot;https://blog.kisnows.com/2025/06/20/aicode-product-detail-practice/&quot;&gt;AICode 在 Alibaba.com 商详前端的实践&lt;/a&gt; 已经几乎完全过时了，只有一些基本的理念比如 Single Source of Truth 、通过强类型约束等还算是适用的，但具体的实践已经完全不一样了。&lt;/p&gt;
&lt;p&gt;去年年中我对公司的 AI 战略不是很满意，感觉管理层很浮躁可能也是因为 AI 带来的焦虑，让我们大部分人全栈，并且要求开发效率提升 30%，而且要证明如何提升了这个 30%。&lt;/p&gt;
&lt;p&gt;全栈这个方向我觉得是没有问题的，提效 30% 可能多了，体感 10~20% 左右，那会主要还是靠 cursor 的 Tab 补全，独立写代码还不行。而且关键不是提效就行了，还要证明如何提升了这个 30%，就很困难，很多人写一堆文章、各种工具来论证怎么实现一行代码不写就实现了某个需求，非常的虚，大公司的形式主义还是无处不在。&lt;/p&gt;
&lt;p&gt;我因为在会上说了几次实际开发需求的感受，就被扣上了抵抗 AI 顽固分子的帽子，真的是无语。我可是 AI 的深度使用者，从 ChatGPT3.5 开始就一直在用并且学习各方面知识，我只是觉得老板们太乐观了。&lt;/p&gt;
&lt;p&gt;然后到了下半年，尤其是 Claude 4.6 和 GPT 5.4 之后，AI 的编程能力突飞猛进。很多简单的编码工作，已经可以变成 AI 为主、人为辅来做。业务团队上，所有的产品、技术都在想怎么把自己的业务 AI 化或者说 skills 化，来接入各种 AIAgent 入口，可以看到所有人都因为 AI 很焦虑。&lt;/p&gt;
&lt;p&gt;很直观的感受，作为程序员以后如果只有写代码这一个技能，那么被淘汰就是迟早的事情。&lt;/p&gt;
&lt;h3&gt;AI 店铺&lt;/h3&gt;
&lt;p&gt;工作内容上，下半年开始做 AI 店铺。大概就是类似 Loveable、Shopify 这种方向的平台能力，用商家自己的素材和信息，快速搭建一个自己的店铺。以前传统装修需要在复杂编辑器里拖拖拽拽，还要填各种表单，现在 AI 店铺建站确实快了很多。&lt;/p&gt;
&lt;p&gt;但问题也很明显。对商家来说，精准描述自己的诉求并不容易。生成之后要做进一步修改，也并没有想象中那么简单。AI 可以提升初始效率，但想让结果真正符合人的预期，中间还有很多产品和交互问题要解决。&lt;/p&gt;
&lt;p&gt;后来我被调走做别的项目，也没再继续关注了，现在好像又换了方向，直接走 skills 和 plugin 往各种 AI Agent 入口比如我们自己的 ACCIO 里面嵌入的方式来做了。&lt;/p&gt;
&lt;h3&gt;性能&lt;/h3&gt;
&lt;p&gt;同时我还负责团队的性能指标，这个事情让我很疲惫。&lt;/p&gt;
&lt;p&gt;我一直觉得当前指标本身定得不太合理，和 CTO 在 one-on-one 里提过，在公开的性能圆桌会里也提过。但指标一旦定下来，再想改就很难。之前的目标是一个我觉得要么是不懂性能要么是太想给他的老板表现的人定下来的，在 Google CWV 的 good 基础上又往前提了两步：从看 75 分位变成看 90 分位，指标项也进一步收紧，比如 LCP 从 2.5s 提升到 2s。&lt;/p&gt;
&lt;p&gt;对我们这种全球化业务来说，这个指标非常难，完全不合理。机房实际只有中、美、新加坡三地，从弱网国家过来的请求到机房，网络延迟就可能有 500ms 左右。想在这种情况下达成 LCP 2s，本身就很困难。更难的是 INP，像印度这种网络环境差、设备性能也差的地区，90 分位要控制在 200ms 以内，那真的你的产品最好不要有什么稍微复杂一点的交互，否则 INP 指标直接超标。还不说由于我们这是个老业务，各种逻辑、不同系统之间嵌入的各种脚本，以及集团的安全脚本、打点脚本等你完全无法控制运行时机和时长的东西，会导致 INP 非常难优化，很多东西控制权都不在你手里，我当时分析我们页面的 INP，统计是 230ms，其中二方业务+三方业务的脚本耗时占了 150ms，留给我的只有 50ms，我的系统上所有的操作响应都得在 50ms 以内完成，而且还是 90 分位，真的是离谱。&lt;/p&gt;
&lt;p&gt;所以到现在也没有完全达成目标。即使已经投入了大量人力去优化，还是会有一些场景是差或者良。而且 90 分位的数据在全球化场景下很敏感，一个小小的流量波动就可能导致指标大幅波动，今天你的场景是优秀明天可能就是良或者差了，为这个事情真是疲于奔命。&lt;/p&gt;
&lt;p&gt;希望 2026 年不要再过度看重这个指标了，不是性能不重要，而是优化到这个阶段后，边际收益已经很小，投入和产出越来越不成正比。&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;p&gt;今年的工作整体上虽然我觉得自己在很多方面做的都不错，但可能老板的预期比较高，绩效不是很好，有点点打击到了积极性。很多我平时工作中非常负责任在做的事情，老板都不是很关心，而且说我对组里的同学影响力不够，我应该带我们前端小组的同学一起做一些事情，但是觉得我只管自己的事情。真是冤枉啊，不能因为我没说，就说我没做吧，组里基本都是我带的师弟师妹，平心而论，在阿里至少在我们部门，我这个师兄都是非常负责任和尽心的，只能说今年确实没有一起搞个什么东西让老板看到吧。&lt;/p&gt;
&lt;p&gt;我觉得最大的原因还是因为今年我在 AI 上没有做出什么特别亮眼的东西来，其他的那些都是话术罢了。我今年只是在 AI 上优化私域的工作流提升效率，以及 AI 店铺我在整体的项目设计和技术方案上有点贡献，但确实没有直接做了什么一个全新的使用 AI 的范式来大幅提升业务效果的东西。&lt;/p&gt;
&lt;p&gt;所以这个结果我也能接受。&lt;/p&gt;
&lt;h2&gt;生活&lt;/h2&gt;
&lt;h3&gt;春节返程&lt;/h3&gt;
&lt;p&gt;25年春节回老家以后，收假要回杭州，当时没买到回来的票，高铁和飞机都没有，要么就得买 4000 一张的商务舱，两个人 8000。最后没办法，是我哥开车和我妈一起把我和队友一路送到杭州。从陕西到杭州 1400 多公里，路上真的非常辛苦。出陕西还比较顺利，但在河南和安徽路上堵了一天多，最后到杭州的时候已经是第二天晚上了。&lt;/p&gt;
&lt;p&gt;在杭州稍微休息了一天出去逛了一天，去了法喜寺，全称上天竺法喜讲寺，这个名字还有挺有意思的，说是求姻缘很灵，当时希望我哥能早日找到对象。没怎么再敢休息，他们就马不停蹄就又得回去了，因为我哥还要上班，请了几天假不能再请了。回程非常快，他们中途稍微逛了一下，但好像一天多也就到了。&lt;/p&gt;
&lt;p&gt;真的非常感谢他们，尤其是我哥。
&lt;img src=&quot;/imgs/2025-year-end-summary/25chunjie.png&quot; alt=&quot;春节返程&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;出去转了转&lt;/h3&gt;
&lt;p&gt;2025 年在杭州周边去了很多地方，当时想着队友怀孕了就不好出去玩了。&lt;/p&gt;
&lt;p&gt;4 月去了扬州。江泽民故居、到处卖老鹅、人多、土也多，跟我印象中的南方不太一样。扬州炒饭鼎鼎大名，但是不好吃，也可能是吃得不正宗，反正没有在外地吃到的那种感觉。
&lt;img src=&quot;/imgs/2025-year-end-summary/yangzhou.png&quot; alt=&quot;扬州&quot; /&gt;
当时比较有意思的是，我们邻桌坐了一堆男生，聊得眉飞色舞，看起来好像很熟。结果最后一个人问另一个人从哪里过来的，才发现两人根本不认识，真有意思。&lt;/p&gt;
&lt;p&gt;回去以后在南通一个镇上体验了一下赶集。我好像以前都没有赶过集，和队友骑了个小电动车感受了一下，其实就是很多村里或者镇里的人集中在一起摆个摊，最后买了个不是皮卡丘的皮卡丘气球。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2025-year-end-summary/ganji.png&quot; alt=&quot;赶集&quot; /&gt;&lt;/p&gt;
&lt;p&gt;5 月去上海看了车展，真的是跑断了腿。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2025-year-end-summary/chezhan.png&quot; alt=&quot;上海车展&quot; /&gt;&lt;/p&gt;
&lt;p&gt;6 月先去了绍兴鲁迅故居。当天即使有点下雨，人还是很多。从百草园到三味书屋，我一个都没进去，懒得排队。
&lt;img src=&quot;/imgs/2025-year-end-summary/shaoxing.png&quot; alt=&quot;绍兴&quot; /&gt;&lt;/p&gt;
&lt;p&gt;6 月中旬参加团建去了武夷山。整体感觉还不错，玩了《无间风云》桌游很有意思，还挺好玩。还爬了天游峰真是累死了，不过后面坐竹筏游溪，景色也很美，真的蓝天白云。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2025-year-end-summary/wuyishan.png&quot; alt=&quot;武夷山&quot; /&gt;&lt;/p&gt;
&lt;p&gt;7 月看了太阳马戏团，挺有意思，讲的故事是分两条线，你一次去只能看一条，所以要看完整的故事需要看两次，挺有新意的。从开头在一起，然后分男女主角两条路，最后再交汇到一起，迎来结局，很精彩。&lt;/p&gt;
&lt;p&gt;这几年陆续看了一些话剧、音乐剧、马戏团，感觉和电视上看真的不一样。现场看到真人演出，有一种很真实的感染力，更容易沉浸，也更容易被打动。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2025-year-end-summary/taiyangmaxituan.png&quot; alt=&quot;太阳马戏团&quot; /&gt;&lt;/p&gt;
&lt;p&gt;10 月去了舟山。当时队友其实已经怀孕了，按理说不太适合坐那么久的车，但当时不懂。我都是自己开车去，有一天开了四个小时，我自己都开到腰疼，还好宝宝没什么问题。不过舟山玩得还不错，海很棒，当时订的酒店很高级，在酒店可以直接看海，很惬意。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2025-year-end-summary/zhoushan.png&quot; alt=&quot;舟山&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;家人&lt;/h3&gt;
&lt;p&gt;5 月队友还有个小手术，去医院陪床陪了几天。&lt;/p&gt;
&lt;p&gt;10 月让我爸妈过来，帮忙照看队友。&lt;/p&gt;
&lt;p&gt;11 月带爸妈去了杭州野生动物园。因为我爸腿不好，租了个电瓶车，我当车夫，感觉还挺好的。虽然爸爸妈妈从来没有带我去过动物园，但我觉得带他们去动物园也挺好，看得出来他们也挺喜欢。就是他们觉得有点贵，哈哈。&lt;/p&gt;
&lt;p&gt;后来我爸的腿不好，在我的支持下做了膝盖置换手术。手术过程中可能麻药没控制好，我爸一个从来不说疼的人，出了手术室就喊疼。不过手术整体还是成功的，恢复得也不错，现在说比以前好多了。我爸这两年先做了腰椎手术，又做了膝盖手术，现在应该差不多能正常走远路了，还是挺好的。我姑姑看我爸做了这个手术，她也去做了。很多比较大的手术，可能就是要看到身边的人做完觉得安全，自己才敢做。&lt;/p&gt;
&lt;h3&gt;一些小事&lt;/h3&gt;
&lt;p&gt;年初看了《哪吒》，电影还不错，画面很棒，剧情也燃。&lt;/p&gt;
&lt;p&gt;8 月卖掉了之前的 4090 显卡，竟然卖了 1.7w。两三年前买的时候也不过 1.4w。然后加了五千买了一个 5090FE，真的好看，像工艺品一样，性能也非常强。现在看，这个决定真是正确，否则现在买得 3w+ 了，都是拜 AI 发展所赐。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2025-year-end-summary/5090FE.png&quot; alt=&quot;5090FE&quot; /&gt;&lt;/p&gt;
&lt;p&gt;10 月底有一天晚上实在无聊，去星光大道二期转悠。走到顶楼发现上面竟然还有一个马场，真是震惊了，谁能想到商场顶楼还有个马场。后来在那里吃了一个米其林还是黑珍珠来着的餐厅，貌似很高级，人均三四百，但吃着真的很一般，感觉不值这个价钱。&lt;/p&gt;
&lt;p&gt;11 月看了《鬼灭之刃》的剧场版。怎么说呢，看着是很精彩，毕竟动漫本身就很好看。只是里面猗窝座的设定，以及电影想表达的东西，我不是很认可。一个杀了很多人的角色，最后自我感悟、自我了结，就好像把很多人感动了一样，感觉有点奇怪。&lt;/p&gt;
&lt;p&gt;26 年春节因为怀孕的关系，所以哪也没去，就待在杭州。春节还和队友去了运河公园。人很多，各种小吃街，好像还去了一个什么面包节，就一堆卖面包的。还去了莫干山的一个牧场，近距离看到了牛、羊、兔子，还有各种奇怪的鸡。印象最深的有两个：一个好像是火鸡还是什么鸡，长得特别丑，甚至有点恶心；另一个是羊，羊的眼睛看着很邪恶，瞳孔竟然是横条的，一直盯着看的话还有点恐怖。&lt;/p&gt;
&lt;p&gt;可能是因为春节期间吃饭实在不规律，弄的胃不舒服了好久，最后还去做了肠胃镜，还好结果都没什么问题，但现在胃还是不舒服，大概三四个月了，医生都看了两个了药也吃了两种方案了，偶尔都觉得好了，但是一旦吃了凉的就又不舒服了，今年得好好养胃了，不过今年的情况看家里我的优先级貌似不是很高。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2025-year-end-summary/chunjie.png&quot; alt=&quot;26春节&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;爱好&lt;/h2&gt;
&lt;h3&gt;射箭&lt;/h3&gt;
&lt;p&gt;同样由于怀孕的关系，下半年就基本没怎么去射箭了。现在水平停滞在 10米平均 9环左右。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/2025-year-end-summary/shejian.png&quot; alt=&quot;射箭&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;B 站&lt;/h3&gt;
&lt;p&gt;2024 年玩《丝之歌》的时候上传了几个视频，算是开始了 UP 主之路。后面玩《仁王 3》《生化危机 9》《生化危机 2》等游戏，也陆陆续续上传了一些游戏实况视频。&lt;/p&gt;
&lt;p&gt;其中有一个视频竟然有 3w 播放。每次打开 B 站看到有消息，点进去发现有人给我点赞或者评论，都会非常高兴，那种多巴胺分泌的快感，怪不得那么多人想当网红。&lt;/p&gt;
&lt;p&gt;还是得感谢 Steam 的游戏录制功能，实在太方便了。我本来就是打自己的游戏，打完它自动录好了，我只需要稍微剪一剪就可以上传。早知道就应该更早当 UP 主，反正图个乐子，也不靠这个赚钱，感觉还挺有意思。&lt;/p&gt;
&lt;h3&gt;口琴&lt;/h3&gt;
&lt;p&gt;口琴好像是 2018 年左右开始自学的，大概学了不到两年，能吹一些简单的曲子。但因为不会压音，导致低音的 6 吹不了，而大部分流行歌曲都有这个音，于是非常受挫，搁置了很久。&lt;/p&gt;
&lt;p&gt;现在有了女儿，想着以后要是能给她吹个口琴，既能娱乐她，又能让她觉得爸爸很棒，于是又打算重新拾起来，不过最终目的还是自娱自乐。&lt;/p&gt;
&lt;p&gt;这次在布鲁斯和半音阶之间犹豫了很久。布鲁斯的好处是方便、小巧、可以单手演奏，缺点是很多音需要高阶技巧，对我来说太难。半音阶的好处是音域更广，吹的时候只需要考虑音在哪、怎么演奏，而不是考虑怎么把这个音吹出来；缺点是体积大，大概率需要双手，因为还有个按键。&lt;/p&gt;
&lt;p&gt;最终还是因为音色和便携以及之前练过布鲁斯的原因，这次重新买了一个 paddy 调音的布鲁斯口琴，好处是低音的 6 不需要压音，大部分流行歌曲应该都可以吹。坏处是低音的 4 还是得压音，但先试试吧，实在不行我就转半音阶。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;记了这么多流水账，还是有用的，在记流水账的过程中把去年的照片、写的文章、备忘录都翻了翻，很多事情如果不看都忘掉了，甚至觉得不是 25 年发生的，人的记忆是真不可靠。&lt;/p&gt;
&lt;p&gt;工作上的事情越来无聊了，不如以前工作那么兴奋了，做的事情记忆也没有那么深刻了，就记得 AI 发展的速度真是突飞猛进。&lt;/p&gt;
&lt;p&gt;生活上则进入了一个新阶段，身份不一样了。从怀孕开始就很期待，而且在这期间去了很多周边的城市玩，毕竟后面可能就不好出去了。&lt;/p&gt;
&lt;p&gt;情绪：有了孩子以后，更希望家里有一个轻松的氛围，以前两个人有点小摩擦冷战一下无所谓，但是有了孩子以后这种不轻松和谐的氛围对孩子的成长影响太大了，会让她觉得家里没有安全感。而且我一个非常阳光开朗在公司都会被人问怎么每天脸上都笑哈哈的人，现在竟然回到家里有时也会因为几句话说的不投机就搞得不高兴，我自己都觉得匪夷所思。队友本身是很好的人，我觉得只是她被自己的情绪困扰了，可能也有激素的原因，她有时也意识到这个问题，会尽量克制一下，但每次同样的事情发生的时候还是难免会重蹈覆辙，可能人就是这样的吧，无论她还是我。她一秒进情绪，我看到有情绪我就不想说话了，结果就形成了一个恶性循环，真的很烦。不过还是要往好的方向想，也许有了孩子以后会有改变，共勉吧。&lt;/p&gt;
&lt;p&gt;很多时候感悟，越来越理解向内求的重要性，只有内求才能得到最终的自由。&lt;/p&gt;
&lt;p&gt;不管怎么说，新的一年，还是希望家人身体健康，工作少一点无意义的折腾，自己也能继续保持对生活的兴趣，女儿也要健康快乐地成长。没想到现在期望的东西都越来越俗套了，哈哈。&lt;/p&gt;
</content:encoded></item><item><title>前端面试题(1)</title><link>https://ab.kisnows.com/zh-CN/2015/02/06/f2e-interview-question1/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/02/06/f2e-interview-question1/</guid><pubDate>Fri, 06 Feb 2015 20:44:05 GMT</pubDate><content:encoded>&lt;p&gt;在群里有人分享了一套试题，拿来练练手。&lt;/p&gt;
&lt;h3&gt;1、请用 HTML5 标准完成以下布局&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/interval/QQ%E6%88%AA%E5%9B%BE20150206124835.png&quot; alt=&quot;图1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTML:
	&amp;lt;header&amp;gt;HEADER&amp;lt;/header&amp;gt;
	&amp;lt;nav&amp;gt;NAV&amp;lt;/nav&amp;gt;
	&amp;lt;aside&amp;gt;ASIDE&amp;lt;/aside&amp;gt;
	&amp;lt;section&amp;gt;SECTION&amp;lt;/section&amp;gt;
	&amp;lt;footer&amp;gt;FOOTER&amp;lt;/footer&amp;gt;
CSS:
	body{
		color: white;
		font-size: 14px;
		text-align: center;
	}
	header{
		background-color: black;
	}
	nav{
		background-color: gray;
	}
	aside{
		width: 30%;
		height: 200px;
		float: left;
		background-color: red;
	}
	section{
		width: 70%;
		height: 200px;
		margin-left: 30%;
		background-color: blue;
	}
	footer{
		background-color: orange;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、相应框架布局 css 编写，实现以下两种情况即可。&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/interval/QQ%E6%88%AA%E5%9B%BE20150206124951.png&quot; alt=&quot;图2&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTML:
	&amp;lt;div class=&quot;box1&quot;&amp;gt;1&amp;lt;/div&amp;gt;
	&amp;lt;div class=&quot;box2&quot;&amp;gt;2&amp;lt;/div&amp;gt;
CSS:
	div{width: 600px;height: 50px}
	.box1{background-color: red;}
	.box2{background-color: blue;}
	@media screen and (min-width:1024px ) {
		div{
			float: left;
		}
	}
	@media screen and (max-width: 1024px) {
		div{
			float: none;
		}
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、实现一行数据中同时含有图片和文字垂直居中（不能使用 table，注意兼容性）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HTML:
	&amp;lt;div class=&quot;box1&quot;&amp;gt;
		&amp;lt;div class=&quot;wrap1&quot;&amp;gt;
			&amp;lt;p&amp;gt;我要居中&amp;lt;/p&amp;gt;
			&amp;lt;img src=&quot;http://images.cnitblog.com/blog/607355/201408/100022249123500.png&quot; alt=&quot;liveReload&quot;&amp;gt;
		&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;
	&amp;lt;div class=&quot;box2&quot;&amp;gt;
		&amp;lt;div class=&quot;wrap2&quot;&amp;gt;
			&amp;lt;p&amp;gt;我也要居中&amp;lt;/p&amp;gt;
			&amp;lt;img src=&quot;http://images.cnitblog.com/blog/607355/201408/100022249123500.png&quot; alt=&quot;liveReload&quot;&amp;gt;
		&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;
	&amp;lt;div class=&quot;box3&quot;&amp;gt;
		&amp;lt;div class=&quot;wrap3&quot;&amp;gt;
			&amp;lt;p&amp;gt;我也要居中&amp;lt;/p&amp;gt;
			&amp;lt;img src=&quot;http://images.cnitblog.com/blog/607355/201408/100022249123500.png&quot; alt=&quot;liveReload&quot;&amp;gt;
		&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;
CSS:
	.box1,.box2,.box3,.box4{
		width: 33%;
		height: 300px;
		float: left;
		border: 1px solid #00FFFF
	}
	/*方法1，用display:table*/
	.box1{
		display: table;
	}
	.wrap1{
		display: table-cell;
		vertical-align: middle;
		text-align: center;
	}
	/*方法2，用绝对定位法*/
	.box2{
		position: relative;
	}
	.wrap2{
		width: 150px;
		height: 150px;
		border: 1px solid black;
		position: absolute;
		left: 0;
		right: 0;
		top: 0;
		bottom: 0;
		margin: auto;
	}
	/*方法3，用translate方法*/
	.box3{
		position: relative;
	}
	.wrap3{
		position: absolute;
		top: 50%;
		left: 50%;
		-webkit-transform: translate(-50%,-50%);
		   -moz-transform: translate(-50%,-50%);
		    -ms-transform: translate(-50%,-50%);
		     -o-transform: translate(-50%,-50%);
		        transform: translate(-50%,-50%);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、请描述一下 cookie，sessionStorage 和 localStorage 的异同点。&lt;/h3&gt;
&lt;p&gt;共同点：都是保存在浏览器端的数据。
区别：
cookie 数据始终在同源的 http 请求中携带（即使不需要），即 cookie 在浏览器和服务器间来回传递。而 sessionStorage 和 localStorage 不会自动把数据发给服务器，仅在本地保存。cookie 数据还有路径（path）的概念，可以限制 cookie 只属于某个路径下。
&lt;strong&gt;存储大小&lt;/strong&gt;限制也不同，cookie 数据不能超过 4k，同时因为每次 http 请求都会携带 cookie，所以 cookie 只适合保存很小的数据，如会话标识。sessionStorage 和 localStorage 虽然也有存储大小的限制，但比 cookie 大得多，可以达到 5M 或更大。
&lt;strong&gt;数据有效期&lt;/strong&gt;不同， sessionStorage：仅在当前浏览器窗口关闭前有效，自然也就不可能持久保持； localStorage：始终有效，窗口或浏览器关闭也一直保存，因此用作持久数据； cookie 在设置的 cookie 过期时间之前一直有效，即使窗口或浏览器关闭。
&lt;strong&gt;作用域不同&lt;/strong&gt;， sessionStorage 不在不同的浏览器窗口中共享，即使是同一个页面； localStorage 在所有同源窗口中都是共享的；cookie 也是在 所有同源窗口中都是共享的。&lt;/p&gt;
&lt;h3&gt;5、请指出以下代码的性能问题，并进行优化。&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var str=&quot; 123&quot;;
str +=&quot; 456 ,&quot;;
str +=&quot; 789 ,&quot;;
str +=&quot; 012&quot; ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不知如何解答，-_-&lt;/p&gt;
&lt;h3&gt;6、编写一个用户类，需求如下：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;属性：id、name、password、age&lt;/li&gt;
&lt;li&gt;方法：获取用户脚本信息，返回 json 字符串 GetUserInfo&lt;/li&gt;
&lt;li&gt;方法：验证用户登录信息，ajax 实现，返回结果提示登录成功或失败即可 CheckLogin&lt;/li&gt;
&lt;li&gt;创建一个用户类的一个对象，并重写 GetUserInfo 方法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var Person = {
  id: &quot;ID&quot;,
  name: &quot;Name&quot;,
  password: &quot;PassWord&quot;,
  $age: null,
  get age() {
    if (this.$age == undefined) {
      return new Date().getFullYear() - 1993;
    } else {
      return this.$age;
    }
  },
  set age(val) {
    if (!isNaN(+val) &amp;amp;&amp;amp; +val &amp;gt; 0 &amp;amp;&amp;amp; +val &amp;lt; 120) {
      this.$age = +val;
    } else {
      throw new Error(val + &quot;不是正确的年龄&quot;);
    }
  },
  GetUserInfo: function () {
    return {
      id: this.id,
      name: this.name,
      password: this.password,
      age: this.age,
    };
  },
  CheckLogin: function (data, success, error) {
    //data为用户数据，success和error分别是成功和失败的回调函数
    var xmlhttp;

    if (window.XMLHttpRequest) {
      xmlhttp = new XMLHttpRequest();
    } else {
      //for IE6
      xmlhttp = new ActiveXObject(&quot;Microsoft.XMLHTTP&quot;);
    }
    xmlhttp.onreadystatechange = function () {
      if (xmlhttp.readyState == 4 &amp;amp;&amp;amp; xmlhttp.status == 200) {
        if (xmlHttp.responseText === &quot;OK&quot;) {
          if (typeof success === &quot;function&quot;) {
            success();
            console.log(&quot;登录成功&quot;);
          }
        } else {
          error();
          console.log(&quot;登录错误&quot;);
        }
      }
    };
    xmlhttp.open(&quot;POST&quot;, &quot;Login&quot;, true);
    xmlhttp.setRequestHeader(
      &quot;Content-type&quot;,
      &quot;application/x-www-form-urlencoded&quot;,
    );
    xmlhttp.send(data);
  },
};

var me = Object.create(Person);
me.id = &quot;123&quot;;
me.name = &quot;kisnows&quot;;
me.password = &quot;kPassword&quot;;
var myData = me.GetUserInfo();
console.log(myData);
me.GetUserInfo = function () {
  console.log(&quot;Rewrite GetUserInfo&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7、jQuery 插件开发，有一数据列表页面，需求如下：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;奇数行无背景色&lt;/li&gt;
&lt;li&gt;偶数行背景色#EEE&lt;/li&gt;
&lt;li&gt;鼠标移入某行时背景色#0066CC，文字颜色变为白色，移出还原&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;假设是给 ul 下的 li 设置背景，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(function ($) {
  $.fn.extend({
    ChageBg: function () {
      var lis = $(this).find(&quot;li&quot;);
      var bfBg, bfCl;
      for (var i = lis.length - 1; i &amp;gt;= 0; i--) {
        lis[i].index = i;
        if (i % 2 === 0) {
          $(lis[i]).css(&quot;backgroundColor&quot;, &quot;#EEE&quot;);
          // lis[i].style.backgroundColor = &apos;#EEE&apos;;
        }
        $(lis[i]).mouseover(function () {
          bfBg = $(this).css(&quot;backgroundColor&quot;);
          // console.log($(this),bfBg)
          bfCl = $(this).css(&quot;color&quot;);
          $(this).css({
            backgroundColor: &quot;#0066CC&quot;,
            color: &quot;#FFF&quot;,
          });
        });
        $(lis[i]).mouseout(function (pro) {
          $(this).css({
            backgroundColor: bfBg,
            color: bfCl,
          });
        });
      }
    },
  });
})(jQuery);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8、用 HTML5 播放一段视频或音频，并支持打点功能（从指定开始时间播放之制定结束时间，如一段视频长度为 10 秒，从第 2s 开始播放至第 8s 结束）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HTML:
&amp;lt;video id=&quot;video1&quot; controls=&quot;controls&quot;&amp;gt;
  &amp;lt;source src=&quot;/example/html5/mov_bbb.mp4&quot; type=&quot;video/mp4&quot; /&amp;gt;
  &amp;lt;source src=&quot;/example/html5/mov_bbb.ogg&quot; type=&quot;video/ogg&quot; /&amp;gt;
  Your browser does not support HTML5 video.
&amp;lt;/video&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;只知道如何引入带控制条的音视频，打点功能不了解。网上也没有找到。
希望知道的同学能够告诉我一下，谢谢了。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9、CSS3 的 transition 与 Animation 动画结束后的结束回调函数是什么。&lt;/h3&gt;
&lt;p&gt;此题答案来自网络&lt;a href=&quot;http://blog.csdn.net/renfufei/article/details/19617745&quot;&gt;http://blog.csdn.net/renfufei/article/details/19617745&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ransitionend&lt;/code&gt;事件和&lt;code&gt;animationend&lt;/code&gt;事件是标准浏览器的动画结束回调函数。基于 webkit 的浏览器仍然依赖于前缀,我们必须先确定事件的前缀,然后才能调用:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/* From Modernizr */
function whichTransitionEvent(){
    var t;
    var el = document.createElement(&apos;fakeelement&apos;);
    var transitions = {
      &apos;transition&apos;:&apos;transitionend&apos;,
      &apos;OTransition&apos;:&apos;oTransitionEnd&apos;,
      &apos;MozTransition&apos;:&apos;transitionend&apos;,
      &apos;WebkitTransition&apos;:&apos;webkitTransitionEnd&apos;,
      &apos;MsTransition&apos;:&apos;msTransitionEnd&apos;
    }

    for(t in transitions){
        if( el.style[t] !== undefined ){
            return transitions[t];
        }
    }
}

/* 监听 transition! */
var transitionEvent = whichTransitionEvent();
transitionEvent &amp;amp;&amp;amp; e.addEventListener(transitionEvent, function() {
    console.log(&apos;Transition 完成!  原生JavaScript回调执行!&apos;);
});

/*
    在 &quot;whichTransitionEvent&quot; 中,可以将 &quot;transition&quot;文本替换为  &quot;animation&quot;,则处理的就是动画,此处代码省略...)
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;第四题做之前理解没有那么深,只知道它们都是存在浏览器端的本地数据，且 sessionStorage 和 localStorage 为 HTML5 的新特性，sessionStorage 为短期存储，localStorage 为长期存储。&lt;/li&gt;
&lt;li&gt;第六题中的 CheckLogin 方法不知具体如何实现。&lt;/li&gt;
&lt;li&gt;第八题打点功能不了解。&lt;/li&gt;
&lt;li&gt;第九题之前没听说过。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Hexo部署到VPS并启用HTTPS</title><link>https://ab.kisnows.com/zh-CN/2016/03/11/hexo%E9%83%A8%E7%BD%B2%E5%88%B0vps%E5%B9%B6%E5%90%AF%E7%94%A8https/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/03/11/hexo%E9%83%A8%E7%BD%B2%E5%88%B0vps%E5%B9%B6%E5%90%AF%E7%94%A8https/</guid><pubDate>Fri, 11 Mar 2016 05:12:49 GMT</pubDate><content:encoded>&lt;p&gt;上周买了个 VPS 玩 ，从板瓦工买的，一年的 19 刀。性价比还算合适，这是配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;10 GB SSD RAID-10 Disk Space
256 MB RAM
500 GB Transfer
Gigabit port
Multiple locations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是&lt;a href=&quot;https://bandwagonhost.com/aff.php?aff=7249&quot;&gt;购买链接&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;买过来，先是搭了个 SS 解决科学上网的问题。然后想想要不要把博客也迁移过去算了,一个 ss 又用不了多少流量。于是说干就干，一番搜索查询后，达成成就。&lt;/p&gt;
&lt;p&gt;简单的记录下操作的步骤：&lt;/p&gt;
&lt;h1&gt;客户端：&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;配置 ssh&lt;/li&gt;
&lt;li&gt;本地部署 hexo&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;服务器：&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;安装所需软件
a. Git
b. Nginx&lt;/li&gt;
&lt;li&gt;为了安全与方便，新建专用与部署 hexo 的用户 git，并给予相应权限。&lt;/li&gt;
&lt;li&gt;配置 ssh，与客户端达成通道&lt;/li&gt;
&lt;li&gt;配置 git hooks，这样本地在 hexo 部署到服务器 git 用户的对应仓库后就不用在把它复制到用以访问的网站的文件夹了。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
GIT_REPO=/home/git/hexo.git #git仓库
TMP_GIT_CLONE=/tmp/hexo
PUBLIC_WWW=/var/www/hexo #网站目录
rm -rf ${TMP_GIT_CLONE}
git clone ${GIT_REPO} ${TMP_GIT_CLONE}
rm -rf ${PUBLIC_WWW}/*
cp -rf ${TMP_GIT_CLONE}/* ${PUBLIC_WWW}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置 nginx，并启动 nginx 服务&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;遇到的问题&lt;/h2&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;访问 vps 地址，直接显示 403 forbidden，以为是权限问题，网上查了半天也没解决。最后发现用于网站的目录下是空的。&lt;/li&gt;
&lt;li&gt;由问题 1 推断出，Git hooks 压根没起作用。因为用于放网站文件夹下一直都是空的，于是手动试 git hooks 下面的命令，执行到 rm -rf ${PUBLIC_WWW}的时候，系统提示操作被拒绝，发现是权限问题，赋予 git 用户用于放网站的 &lt;code&gt;www/hexo&lt;/code&gt; 的权限。&lt;/li&gt;
&lt;li&gt;ssh 在服务器重启后出现问题，多方排查后发现也是权限问题，赋予 git 用户.ssh 文件 700 权限&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;输入 www.kisnows.com ，访问成功，哈哈，好开心。而且国内访问速度比原来快了一点点，虽然就那么一点点。&lt;/p&gt;
&lt;p&gt;想想也上个 SSL 吧，不然太落后了，毕竟最近电信劫持很严重。因为不想投入过大成本，就打算用 cloudFlare 提供的免费 SSL。虽然只是浏览器到 cloudFlare 之间是加密的，但是有总比没有强嘛。
于是把 DNS 服务从 DNSPod 切换到了 cloudFlare，并借助 cloudFlare 全站启用了 HTTPS，同时将顶级域名重定向到了 www 下的二级域名。方法如下：&lt;/p&gt;
&lt;h2&gt;启用 HTTPS&lt;/h2&gt;
&lt;p&gt;进入 cloudFlare 管理中心，Crypto 下
&lt;img src=&quot;/imgs/Hexo%E9%83%A8%E7%BD%B2%E5%88%B0VPS%E5%B9%B6%E5%90%AF%E7%94%A8HTTPS/ssl.png&quot; alt=&quot;ssl&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;强制非 HTTPS 访问跳转到 HTTPS&lt;/h2&gt;
&lt;p&gt;位于 Page Rules 模块下
&lt;img src=&quot;/imgs/Hexo%E9%83%A8%E7%BD%B2%E5%88%B0VPS%E5%B9%B6%E5%90%AF%E7%94%A8HTTPS/pagerules.png&quot; alt=&quot;ssl&quot; /&gt;
上图 1，选 always use https，然后如图第一条规则，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://*.kisnows.com/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样所有匹配的所有访问，都会走 https 了。&lt;/p&gt;
&lt;h2&gt;顶级域名重定向到带 www 的二级域名下&lt;/h2&gt;
&lt;p&gt;也是用 Page Rules，如上图 2。选 Forwarding，然后匹配如下填写就 OK 了：
&lt;img src=&quot;/imgs/Hexo%E9%83%A8%E7%BD%B2%E5%88%B0VPS%E5%B9%B6%E5%90%AF%E7%94%A8HTTPS/redirect.png&quot; alt=&quot;redirect&quot; /&gt;
Forwarding 中的 &lt;code&gt;$1&lt;/code&gt; 就是 &lt;code&gt;kisnows.com/*&lt;/code&gt; 中 &lt;code&gt;*&lt;/code&gt; 匹配到元素。&lt;/p&gt;
&lt;p&gt;至此，为期两天的折腾完毕，全站迁移到了 VPS，并全程启用 HTTPS。
真是生命不息，折腾不止啊！&lt;/p&gt;
&lt;p&gt;参考文章：
http://tiktoking.github.io/2016/01/26/hexo/
http://www.hansoncoder.com/2016/03/02/VPS%20building%20Hexo/
http://hejun.me/2015/01/05/deploy-hexo-on-cloud/&lt;/p&gt;
</content:encoded></item><item><title>借助gulp自动化前端开发</title><link>https://ab.kisnows.com/zh-CN/2015/01/18/f2e-tools-gulp/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/18/f2e-tools-gulp/</guid><pubDate>Sun, 18 Jan 2015 23:18:27 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;gulp&lt;/code&gt;是一款小巧的自动化构建工具，可以帮助我们自动运行大量乏味重复性的任务，比如代码压缩、合并等等，可以为我们节约大量的时间。在制作简历过程中，因为需要用到&lt;code&gt;less&lt;/code&gt;，而&lt;code&gt;less&lt;/code&gt;需要编译，所以就简单学习了这款工具。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;安装 Gulp.js&lt;/h2&gt;
&lt;p&gt;因为&lt;code&gt;gulp&lt;/code&gt;基于&lt;code&gt;node&lt;/code&gt;构建，所以必须先装上&lt;code&gt;node&lt;/code&gt;。安装好&lt;code&gt;node&lt;/code&gt;后，通过&lt;code&gt;npm&lt;/code&gt;来安装&lt;code&gt;gulp&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    npm install -g gulp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;在项目中配置&lt;/h2&gt;
&lt;p&gt;因为我需要编译&lt;code&gt;less&lt;/code&gt;，所以先要安装&lt;code&gt;gulp&lt;/code&gt;的&lt;code&gt;less&lt;/code&gt;插件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev gulp gulp-less
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令中的&lt;code&gt;--save-dev&lt;/code&gt;可以将安装的包添加到包配置文件 package.json 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;dependencies&quot;: {
    &quot;gulp&quot;: &quot;^3.8.10&quot;,
    &quot;gulp-livereload&quot;: &quot;^3.4.0&quot;,
    &quot;gulp-less&quot;: &quot;^2.0.1&quot;,
    &quot;less-plugin-autoprefix&quot;: &quot;^1.3.0&quot;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果项目中没有这个文件，可以通过&lt;code&gt;npm init&lt;/code&gt;初始化命令来创建。
在项目下的根目录下创建 Gulpfile.js 文件，这个文件用来定义用到什么插件，执行哪些任务等。
在我制作简历过程中，需要将通过 bower 管理的两个依赖库复制到特定文件夹下，同时监控并编译&lt;code&gt;less&lt;/code&gt;文件。所以配置文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var gulp = require(&quot;gulp&quot;),
  less = require(&quot;gulp-less&quot;); //引入less插件

// 定义 less 任务
gulp.task(&quot;less&quot;, function () {
  gulp.src(&quot;./less/main.less&quot;).pipe(less()).pipe(gulp.dest(&quot;./style&quot;));
});
//复制文件到特定目录
gulp.task(&quot;depends&quot;, function () {
  gulp
    .src([
      &quot;./bower_components/pagepiling.js/jquery.pagepiling.min.js&quot;,
      &quot;./bower_components/pagepiling.js/jquery.pagepiling.css&quot;,
    ])
    .pipe(gulp.dest(&quot;./depends&quot;));
});
//监控less文件，一旦有更改，就执行less编译任务
gulp.task(&quot;watch&quot;, function () {
  gulp.watch(&quot;less/*.less&quot;, [&quot;less&quot;]);
});
//默认任务
gulp.task(&quot;default&quot;, [&quot;less&quot;, &quot;depends&quot;, &quot;watch&quot;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gulp.task()&lt;/code&gt;就是定义任务的函数，需要两个参数，一个是任务名，一个是回调函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gulp.src()&lt;/code&gt;是引入文件函数，用来将需要执行的文件路劲引入函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.pipe()&lt;/code&gt;将引入的文件传递给执行函数，比如&lt;code&gt;.pipe(less())&lt;/code&gt;将&lt;code&gt;gulp.src(&apos;./less/main.less&apos;)&lt;/code&gt;中得文件传递给&lt;code&gt;less()&lt;/code&gt;函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gulp.desk()&lt;/code&gt;输出函数，将文件输出到指定目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gulp.watch()&lt;/code&gt;监控函数，一旦发现指定文件有了变化，就会执行相应的函数。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ECMAScript6学习简记（一）-变量声明·字符串扩展·数值扩展</title><link>https://ab.kisnows.com/zh-CN/2015/08/21/learning-ecmascript6-one/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/08/21/learning-ecmascript6-one/</guid><pubDate>Fri, 21 Aug 2015 18:19:59 GMT</pubDate><content:encoded>&lt;h1&gt;let 和 const 命令&lt;/h1&gt;
&lt;h2&gt;let 命令&lt;/h2&gt;
&lt;p&gt;let 声明的变量，只存在与所声明的代码块内。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  let a = 10;
  var b = 11;
}
console.log(a); //ReferenceError: a is not defined
console.log(b); //11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，在代码块外面，是不能访问到用 let 声明的变量，也就是说 let 声明的变量是基于块级作用域的。这样就可以避免一些变量声明的问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var arr = [1, 2, 3];
for (let i = 0; i &amp;lt; arr.length; i++) {}
console.log(i); //ReferenceError: a is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见上面的的代码 for 循环外面是不能访问到变量 i 的。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;而下面的代码用 var 声明，最后会输出 10。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = [];
for (var i = 0; i &amp;lt; 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而如果使用 let，则会输出 6。而以前要做到这样，可能会需要使用立即执行函数才行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = [];
for (let i = 0; i &amp;lt; 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且 let 不会像 var 一样发生‘变量提升’现象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function a(){
    console.log(b)  //ReferenceError
    let b = 2;
}
function c(){
    console.log(d); //undefined
    var d = 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在块级作用域内，使用 let 声明的变量，会当定在这个区域，不会受到外部的影响。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var temp = 123;
let temp1 = 222;
var temp2 = 555;
if (true) {
  let temp = 333;
  let temp1 = 444;
  console.log(temp); //333
  console.log(temp1); //444
  console.log(temp2); //ReferenceError
  let temp2 = 555;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;let 在一个作用域内不允许重复声明同一个变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function(){
    let a = 10;
    var a = 1;
}
function(){
    let a = 11;
    let a = 212;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;const 命令&lt;/h2&gt;
&lt;p&gt;const 同 let 类似，只是用 const 声明的变量是常量，一旦声明就不可更改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const a = 1;
a; //1
a = 3;
a; //1
const a = 12;
a; //1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，const 命令只是指向变量所在的地址，所以将对象声明为常量时需要注意。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const foo = {};
foo.prop = 1;
foo.prop; //1
const a = [];
a.push(&quot;Hello&quot;); // 可执行
a.length = 0; // 可执行
a = [&quot;Dave&quot;]; // 报错
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;上面代码中，常量 foo 储存的是一个地址，这个地址指向一个对象。不可变的只是这个地址，即不能把 foo 指向另一个地址，但对象本身是可变的，所以依然可以为其添加新属性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说将一个对象或数组声明为常量时，它本身是不可赋值的，但是可以给它添加方法。因为不变的只是它本身的地址。&lt;/p&gt;
&lt;h2&gt;全局对象的属性&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;ES6 规定，var 命令和 function 命令声明的全局变量，属于全局对象的属性；let 命令、const 命令、class 命令声明的全局变量，不属于全局对象的属性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;var a = 1;
// 如果在node环境，可以写成global.a
// 或者采用通用方法，写成this.a
window.a; // 1
let b = 1;
window.b; // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;变量的解构赋值&lt;/h1&gt;
&lt;p&gt;ES6 允许按照一定模式，从数组和对象中提取值，对变量进行赋值，这被称为解构。感觉这里跟 python 挺像的。&lt;/p&gt;
&lt;h2&gt;数组的解构赋值&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;var [a, b] = [1, 2];
console.log(a, b);
//1 2
let [c, d] = [3, 4];
console.log(c, d);
//3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上，它属于‘模式匹配’，也就是说，只要等号两边的模式相同，那么左边的变量就会被赋予相应的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var [a, b, [c, d]] = [1, 2, [3, 4]];
//a = 1,b = 2,c = 3,d = 4
var [aa, bb, cc] = [, , 3];
//aa = undefined,bb = undefined,cc = 3;
let [one, ...more] = [1, 2, 3, 4, 5, 6];
//one = 1,more = [2,3,4,5,6]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到上面的代码，只要他们的结构相同，那么左边相应位置的变量就会被赋值为右边的对应的值。如果没有对应的值，那么就是 &lt;code&gt;undefined&lt;/code&gt;.
以下情况属于解构不成功，都会报 TypeError 的错，然后变量的值会等于 undefined.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以上几种情况都属于解构不成功，foo 的值都会等于 undefined。这是因为原始类型的值，会自动转为对象，比如数值 1 转为 new Number(1)，从而导致 foo 取到 undefined。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;var [foo] = [];
var [foo] = 1;
var [foo] = false;
var [foo] = NaN;
var [bar, foo] = [1];
let [foo] = undefined;
let [foo] = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解构赋值允许指定默认值。当数组成员严格等于 undefined 时，默认值就会生效。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var [foo = true] = [];
foo[(x, (y = &quot;b&quot;))] = // true
  [&quot;a&quot;][(x, (y = &quot;b&quot;))] = // x=&apos;a&apos;, y=&apos;b&apos;
  [&quot;a&quot;, undefined][(x, (y = &quot;b&quot;))] =
    // x=&apos;a&apos;, y=&apos;b&apos;
    [1, null]; // x= 1,y=null
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;对象的解构赋值&lt;/h2&gt;
&lt;p&gt;结构同样作用于对象,于数组不同的地方在于：数组的元素的取值由它的位置决定；对象属性因为没有顺序，所以变量名需要与属性同名，才能取到正确的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var { foo, bar } = { foo: &quot;aaa&quot;, bar: &quot;bbb&quot; };
foo; // &quot;aaa&quot;
bar; // &quot;bbb&quot;
//注意两者区别
var { foo, bar } = { bar: &quot;aaa&quot;, foo: &quot;bbb&quot; };
foo; // &quot;bbb&quot;
bar; // &quot;aaa&quot;
var { fos } = { foo: &quot;aaa&quot; };
fos; // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到上面的代码，赋值只与属性名有关，与顺序无关。如果属性名对不上，就会等于 &lt;code&gt;undefined&lt;/code&gt;.
而如果需要在变量名与属性名不一致情况下赋值，需要用下面的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var { foo: abs } = { foo: &quot;sss&quot; };
abs; // &apos;sss&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样也可以对有嵌套结构的对象解构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let obj = {
  p: [&quot;Hello&quot;, { y: &quot;World&quot; }],
};
let {
  p: [x, { y }],
} = obj;
console.log(x, y);
//Hello World
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和数组一样，对象的解构也可以指定默认值，只要对要赋值对象相对应的对象属性严格等于 &lt;code&gt;undefined&lt;/code&gt;，默认值就会成立。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var { a = 3 } = {};
a; //3
var { b = 4, y } = { y: 4 };
(b, y); //4,4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对象的解构赋值给我们提供了较大的方便。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var obj = {
  method1: function () {},
  method2: function () {},
};
var { method1, method2 } = obj;
//以前的写法
var method1 = obj.method1;
var method2 = obj.method2;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字符串的解构赋值&lt;/h2&gt;
&lt;p&gt;字符串同样可以进行解构，在进行解构的时候，字符串会被转换成类数组对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let [f, g, h] = &quot;length&quot;;
console.log(f, g, h);
//l,e,n
let { length: len } = &quot;length&quot;;
console.log(len);
//6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;函数参数的解构赋值&lt;/h2&gt;
&lt;p&gt;函数参数是一个类数组对象，同样可以解构赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function a([x, y]) {
  return x + y;
}
add([1, 2]); //3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也同样可以设置默认值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function move({x=0,y=0}={}){
    &quot;use strict&quot;;
    console.log([x,y]);
}
move({x:3,y:4});        //[3,4]
move({x:3});            //[3,0]
move({});               //[0,0]
move();                 //[0,0]
function remove({x,y}={x:0,y:0}){
    &quot;use strict&quot;;
    console.log(x,y);
}
remove({x:1,y:2});      //1,2
remove({x,y});          //Hello World
remove({});             //undefined undefined
remove();               // 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数参数的解构方法会根据参数类型来自动选择，数组就按照数组解构，对象就按照对象解构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function fc([x,y,z]){
    &quot;use strict&quot;;
    console.log(x,y,z);
}
fc([1,2,3]);     //1,2,3
function nf({x,y,z}){
    &quot;use strict&quot;;
    console.log(x,y,z);
}
nf({y:1,z:2,x:3});    //3,1,2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;用途&lt;/h2&gt;
&lt;h3&gt;提取 JSON 数据&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var jsonDate = {
  id: 42,
  status: &quot;notOK&quot;,
  data: [12, 32],
};
let { id, status, data } = jsonDate;
console.log(id, status, data); //42 &apos;notOK&apos; [12,32]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;###遍历 Map 结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var map = new Map();
map.set(&quot;first&quot;, &quot;HELLO&quot;);
map.set(&quot;second&quot;, &quot;WORLD&quot;);
for (let [key, value] of map) {
  console.log(key + &quot; is &quot; + value);
}
// first is HELLO
// second is WORLD
for (let key of map) {
  console.log(key);
}
//[&apos;first&apos;,&apos;HELLO&apos;]
//[ &apos;second&apos;, &apos;WORLD&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;版权&lt;/h1&gt;
&lt;p&gt;文章中所有代码皆来自或演变自：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://es6.ruanyifeng.com/#docs/&quot;&gt;阮一峰-ECMAScript 6 入门&lt;/a&gt;,
本文同样遵循&lt;a href=&quot;http://creativecommons.org/licenses/by-nc/4.0/&quot;&gt;署名-非商用许可证&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>ECMAScript6学习简记（三）- 函数扩展</title><link>https://ab.kisnows.com/zh-CN/2015/09/09/learning-ecmascript6-three/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/09/09/learning-ecmascript6-three/</guid><pubDate>Wed, 09 Sep 2015 05:24:03 GMT</pubDate><content:encoded>&lt;h1&gt;1.函数参数默认值&lt;/h1&gt;
&lt;p&gt;以前我们经常会些下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function doS(e) {
  e = e | window.e;
  //doSomeThing with e
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这样的方法来给函数设置默认值，这样有一个缺点就是，如果传进来的参数对应的布尔值为&lt;code&gt;false&lt;/code&gt;，比如空字符串或者 0，那就尴尬了。&lt;/p&gt;
&lt;p&gt;所以如果我们还想上面的代码得到正确的结果，就需要对 e 在做一个判断，判断它是否被赋值了。&lt;/p&gt;
&lt;p&gt;这样会很麻烦，所以 ES6 有了新的方法，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function doS(e = window.e) {
  //doSomeThing with e
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样不尽代码简短了很多，而且可读性大大提高，看代码的人可以一样看到那些参数是有默认值的。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;同时，默认值的设置也非常人性化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function get(url,{body=&apos;&apos;,method=&apos;GET&apos;){
    console.log(method);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，传入第二个参数是一个对象，我们可以给对象的属性设置默认值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;甚至还可以设置双重默认值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;fetch(url, { method = &apos;GET&apos; } = {}){
  console.log(method);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，调用函数 fetch 时，如果不含第二个参数，则默认值为一个空对象；如果包含第二个参数，则它的 method 属性默认值为 GET。&lt;/p&gt;
&lt;p&gt;参数的默认值设置同样支持解构赋值，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function ha({ x, y = 5 }) {
  console.log(x, y);
}
ha(); //TypeError: Cannot read property &apos;x&apos; of undefined
ha({}); //undefined 5
ha({ x: 1 }); //1 5
ha({ x: 1, y: 2 }); //1 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义默认值的参数，必须在参数的尾部，因为有了默认之后参数可以被省略，而只有位于尾部，函数才能判断到底省略了那个参数。&lt;/p&gt;
&lt;p&gt;如果给设置默认值的参数传入&lt;code&gt;undefined&lt;/code&gt;和&lt;code&gt;null&lt;/code&gt;,前者该函数会触发默认值，而后者不会。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function a(b = 1, c = 2) {
  console.log(b, c);
}
a(undefined, null);
//1 null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，如果设置了默认值，那么函数的&lt;code&gt;length&lt;/code&gt;属性会失真&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; (function a(a,b){}).length
2
&amp;gt; (function b(a,b,c){}).length
3
&amp;gt; (function c(a,b,c=1){}).length
2
&amp;gt; (function e(a,b=1,c=1){}).length
1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，没设置默认值的时候，函数的&lt;code&gt;length&lt;/code&gt;等于它的参数个数，而设置了默认值之后，它的&lt;code&gt;length&lt;/code&gt;值等于没有设置默认值的参数的个数。&lt;/p&gt;
&lt;h1&gt;2.rest 参数&lt;/h1&gt;
&lt;p&gt;rest 参数用于获取函数的多余变量，这样可以避免使用 arguments 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function add(...values) {
  let sum = 0;
  values.forEach(function(value){
    sum += value;
    });
  return sum;
}
&amp;gt; add(1,2,3,4,5)
15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，该函数可以返回传入参数的和。同时，注意到 rest 参数其实是一个数组，所以数组的方法它都可以使用，所以上面代码中的&lt;code&gt;forEach&lt;/code&gt;是可以使用的。&lt;/p&gt;
&lt;p&gt;需要注意的是，rest 参数同参数的默认值一样，必须在参数的最后。而函数的&lt;code&gt;length&lt;/code&gt;属性是不包括 rest 参数的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(function (a) {})
  .length(
    // 1
    function (...a) {},
  )
  .length(
    // 0
    function (a, ...b) {},
  ).length; // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3.扩展运算符&lt;/h1&gt;
&lt;p&gt;扩展运算符，符号三个点（...），它可以看作是 rest 参数的你运算，可以讲一个数组转为用逗号分割的参数序列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; let array = [1,2,34];
&amp;gt; console.log(...array)
1 2 34
&amp;gt; console.log(11,22,...array,4)
11 22 1 2 34 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该运算符主要用于函数的调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function push(array, ...items) {
  array.push(...items);
}
function add(x, y) {
  return x + y;
}
var numbers = [4, 38];
add(...numbers); // 42
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;上面代码中，array.push(...items)和 add(...numbers)这两行，都是函数的调用，它们的都使用了扩展运算符。该运算符将一个数组，变为参数序列。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;由于扩展运算符可以展开数组，所以不再需要 apply 方法，将数组转为函数的参数了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// ES5的写法
function f(x, y, z) {}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的写法
function f(x, y, z) {}
var args = [0, 1, 2];
f(...args);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个实际的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; let a=[1,23,12,31,23,12,31,23,123,12,31,24,]
&apos;use strict&apos;
&amp;gt; Math.max(...a)
123
//ES5要实现同样的功能则需要
Math.max.apply(null,a);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;扩展运算符可以简化很多种 ES5 的写法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```javascript
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

// ES5
list.push.apply(list, [3, 4])
// ES6
list.push(...[3, 4])

// ES5
list = [1,2,3,4]
var a = list[0], rest = list.slice(1)
a = 1,rest = [2,3,4]
// ES6  这里是用到了解构赋值
var [a, ...rest] = list
a = 1,rest = [2,3,4]
// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES6
new Date(...[2015, 1, 1]);
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，如果将扩展运算符用于数组赋值，只能放在参数的最后，否则会报错。（这是肯定的嘛，否则程序怎么知道扩展运算符的长度是多少）。&lt;/p&gt;
&lt;p&gt;扩展运算符可以将字符串转为数组(Array.from()也可以)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [...&apos;hello&apos;]
[ &apos;h&apos;, &apos;e&apos;, &apos;l&apos;, &apos;l&apos;, &apos;o&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4.箭头函数&lt;/h1&gt;
&lt;h2&gt;基本用法&lt;/h2&gt;
&lt;p&gt;使用‘箭头’来快速定义函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let fn = (f) =&amp;gt; f;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let fn = function (f) {
  return f;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当不需要参数的时候，或参数大于一个的时候，用括号代表参数部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```javascript
var f = () =&amp;gt; 5;
//等于
var f = function(){return 5};

var sum =(sum1,sum2) =&amp;gt; num1 + num2;
//等于
var sum = function(sum1,sum2){
    return sum1+sum2;
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总之箭头后面就是函数要&lt;code&gt;return&lt;/code&gt;的内容。
当箭头函数的代码块部分多余一条语句的时候，需要用大括号括起来，并且使用&lt;code&gt;return&lt;/code&gt;返回。而由于大括号被解释为代码块，所以如果箭头函数需要返回对象的时候，必须在对象外面加上括号，否则会被解释为代码块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var sum = (num1, num2) =&amp;gt; {
  return num1 + num2;
};
var sum1 = (sumI) =&amp;gt; ({ su1: &quot;1&quot;, su2: &quot;2&quot; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样，箭头函数可以配合变量的解构赋值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let person = ({you,i}) =&amp;gt; you + &apos;love&apos; + i;
&amp;gt; person({you:&apos;y&apos;,i:&apos;i&apos;})
&apos;ylovei&apos;
//等于
var person = function(obj){
    return obj.you +&apos;love&apos;+obj.i;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;使用注意点
箭头函数有几个使用注意点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（1）函数体内的 this 对象，绑定定义时所在的对象，而不是使用时所在的对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（2）不可以当作构造函数，也就是说，不可以使用 new 命令，否则会抛出一个错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（3）不可以使用 arguments 对象，该对象在函数体内不存在。如果要用，可以用 Rest 参数代替。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;（4）不可以使用 yield 命令，因此箭头函数不能用作 Generator 函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;上面四点中，第一点尤其值得注意。this 对象的指向是可变的，但是在箭头函数中，它是固定的。下面的代码是一个例子，将 this 对象绑定定义时所在的对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;版权&lt;/h1&gt;
&lt;p&gt;文章中所有代码皆来自或演变自：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://es6.ruanyifeng.com/#docs/&quot;&gt;阮一峰-ECMAScript 6 入门&lt;/a&gt;,
本文同样遵循&lt;a href=&quot;http://creativecommons.org/licenses/by-nc/4.0/&quot;&gt;署名-非商用许可证&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>为自己配置的第二台 PC</title><link>https://ab.kisnows.com/zh-CN/2020/10/12/a-new-pc-on-2020/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2020/10/12/a-new-pc-on-2020/</guid><pubDate>Mon, 12 Oct 2020 22:08:47 GMT</pubDate><content:encoded>&lt;h2&gt;忆往昔&lt;/h2&gt;
&lt;p&gt;想来自己的第一台电脑已经是 2016 年配的了，当时拿了工作以来的第一笔年终奖，差不多两万块大洋，心里很是激动，就给自己配了第一台属于完全自己的台式电脑。当时选用的配置已经很高了，英特尔 i7-6700k 的 CPU 加上华硕 1070 猛禽的显卡，硬盘用的浦科特的 SSD ，这一套配置下来无论是工作还是日常娱乐，都能完全满足我的诉求。&lt;/p&gt;
&lt;p&gt;参加工作的前四年，几乎保持着每周都拿出至少三天业余时间来学习工作相关知识的习惯。工作之余，放松的活动除了看电影之外，最喜欢的就是玩各种单机游戏，Steam 库中的游戏也越来越多了。这台电脑都很好的胜任了自己的工作，CPU 性能强劲让我在各种代码编译中不必有多余的等待，次旗舰的显卡也能带我在 1080p 的高特效下体验各种虚拟游戏世界而不会卡顿，就这样一声不吭的伴随我不知多少个日夜。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- { TODO: 插入图片} --&amp;gt;&lt;/p&gt;
&lt;p&gt;总之，这台电脑在毕业后前几年无论是生活还是工作中都扮演了很重要的角色，帮助我保持好的心情不断努力并从不知名的小公司进入了网易而后再入职阿里。&lt;/p&gt;
&lt;p&gt;然而随着时间的推移，它的性能已不如当年那么优异。虽然在日常代码编译上还没有太大的问题，但在娱乐上，面对体量越来越大，计算越来越复杂的游戏，以及 4K 高清屏带来的压力下，无论是 i7-6700k 的 CPU 还是 1070 的显卡，都逐渐感到有点吃力，一些游戏都只能勉强跑在中低特下，而且还时不时的卡顿，大大影响了游戏体验。影响游戏体验可是一件很大的事情，毕竟影响了游戏体验，就会影响我的心情，影响我的心情就会影响我的工作，影响了我的工作，就会影响我的经济收入。没了经济收入可不行，所以必须对这台老电脑进行升级换代了。&lt;/p&gt;
&lt;h2&gt;探未来&lt;/h2&gt;
&lt;p&gt;由于很久没有关注硬件市场了，所以就在 NGA 上逛了一段时间，由于我预算比较充足，所以直接上了一套旗舰配置：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;品类&lt;/th&gt;
&lt;th&gt;型号&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;机箱&lt;/td&gt;
&lt;td&gt;追风者(PHANTEKS) P600s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主板&lt;/td&gt;
&lt;td&gt;MSI GodLike Z490&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;i9-10900k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;电源&lt;/td&gt;
&lt;td&gt;海韵(SEASONIC)旗舰钛金 PRIME TX1000 1000W 电源 80PLUS 钛金全模&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;显卡&lt;/td&gt;
&lt;td&gt;七彩虹 adoc 3090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;散热器&lt;/td&gt;
&lt;td&gt;NH-U12A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;td&gt;芝奇 32GB(16G×2)套装 3600 频率-焰光戟 (C16)(2020.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储&lt;/td&gt;
&lt;td&gt;三星 980 PRO 1T SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储&lt;/td&gt;
&lt;td&gt;三星 970 1T SSD * 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储&lt;/td&gt;
&lt;td&gt;三星 860 EVO 1T SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储&lt;/td&gt;
&lt;td&gt;浦科特 M9PeG 1TB SSD 固态硬盘 M.2 接口(NVMe 协议)(2019.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储&lt;/td&gt;
&lt;td&gt;浦科特 M6S+ 256G SATA3 固态硬盘(2016.03&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;简单开箱&lt;/h3&gt;
&lt;p&gt;半全家福：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/a-new-pc-on-2020/quanjiafu.jpg&quot; alt=&quot;半全家福&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;主板 &amp;amp; CPU&lt;/h4&gt;
&lt;p&gt;本来是想要上 X570 + 3950x 的，但是考虑到没有雷电三接口(虽然手里没有这个设备，但就是想要)，而且还是打游戏的用途更多，所以还是上了 Intel 平台下微星 Z490 Godlike 的主板：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/a-new-pc-on-2020/motherboard-1.jpg&quot; alt=&quot;主板-正面&quot; /&gt;
&lt;img src=&quot;/imgs/a-new-pc-on-2020/motherboard-2.jpg&quot; alt=&quot;主板-背面&quot; /&gt;
看着质感是比我老的 Z170M 要好很多，毕竟价格差距很大。&lt;/p&gt;
&lt;p&gt;CPU 的话就上了 i9 的，一部到位：
&lt;img src=&quot;/imgs/a-new-pc-on-2020/cpu.png&quot; alt=&quot;CPU&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;散热&lt;/h4&gt;
&lt;p&gt;散热的话，因为我电脑装好以后基本上不会再去折腾的，而且一般是 7*24 小时不关机的。本来想上水冷，但是考虑到可能漏液以及过保的问题，所以选择了风冷。风冷的话，D15 看着太大不太喜欢，看了些评测 U12A 勉强能压住，所以就选了 U12A 。装在一起就这样：
&lt;img src=&quot;/imgs/a-new-pc-on-2020/cool.jpg&quot; alt=&quot;散热&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;机箱&lt;/h4&gt;
&lt;p&gt;机箱的话，其实是犹豫最久的。在追风者 P600s、518 以及分型工艺的 Define7 之间犹豫了很久，一直没下单，最后还是女朋友说 P600s 好看点，索性就选了这个。
这玩意到的时候我抬了一下，是跟我以前的 UMX3 比实在是太重了。开包装和外表觉得做工好像还挺好的样子，实际打开侧板一看，做工是真差，薄铁皮就不说了，边也是毛边，像没打磨过一样。
不知道分型工艺的做工是不是会好一点。
&lt;img src=&quot;/imgs/a-new-pc-on-2020/box.jpg&quot; alt=&quot;机箱&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;电源&lt;/h4&gt;
&lt;p&gt;电源，犹豫怕吵，所以选了海韵的这款 1000w 的钛金电源，70% 负载以下风扇不会转。送来的时候惊呆了，竟然是个礼盒，一开始还不知道是啥，拆开发现竟然是个礼盒，哈哈：
&lt;img src=&quot;/imgs/a-new-pc-on-2020/power.png&quot; alt=&quot;电源&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;装机&lt;/h3&gt;
&lt;p&gt;装机的话花了差不多四个小时，真是累死了:
&lt;img src=&quot;/imgs/a-new-pc-on-2020/result-1.jpg&quot; alt=&quot;装机&quot; /&gt;
显卡用了老机器拆下来的 1070 ，等 30 系的新卡。
&lt;img src=&quot;/imgs/a-new-pc-on-2020/result-2.jpg&quot; alt=&quot;装机&quot; /&gt;
理线什么的不存在的，盖上背板就好了。&lt;/p&gt;
&lt;h2&gt;新老对比&lt;/h2&gt;
&lt;p&gt;新主机明显比老的主机大多了，毕竟一个主板是 M-ATX 一个是 E-ATX ，对应到机箱上区别就更大了：
&lt;img src=&quot;/imgs/a-new-pc-on-2020/new-old.jpg&quot; alt=&quot;新老主机对比&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;显卡&lt;/h2&gt;
&lt;p&gt;Nvidia 新的 30 系显卡上市以后，本想着买一个 3080 就够了，但缺货严重。只能选择更高一级的 3090 了，毕竟价格感人，买的人也少，买到的几率也就大一点。一开始京东上抢购了几次没抢到就直接放弃抢了，毕竟我浪费不起时间，所以直接选择了无货订购。十一在外面逛游的时候收到了京东的短信，提醒我已经订购到了七彩虹的 3090 adoc ，运气还是挺好的，前前后后也就一周左右不到的时间就买到了首发。&lt;/p&gt;
&lt;p&gt;到货的时候箱子竟然是用一个小米的箱子装着，打开前还疑惑了一下：
&lt;img src=&quot;/imgs/a-new-pc-on-2020/graphics-card-1.jpg&quot; alt=&quot;显卡&quot; /&gt;
&lt;img src=&quot;/imgs/a-new-pc-on-2020/graphics-card-2.jpg&quot; alt=&quot;显卡&quot; /&gt;
&lt;img src=&quot;/imgs/a-new-pc-on-2020/graphics-card-3.jpg&quot; alt=&quot;显卡&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;装机&lt;/h3&gt;
&lt;p&gt;原本的 1070 是个单 8pin 的电源，当时装新机的时候多预留了一根 8pin 的线想着 2*8 够了吧，没想到新卡竟然是 3*8 的，又费力多引了一根线：
&lt;img src=&quot;/imgs/a-new-pc-on-2020/graphics-card-4.png&quot; alt=&quot;显卡装机&quot; /&gt;
其实竖装也可以，延长线什么的都买了，但是嫌麻烦而且会影响我插其他 pcie 设备，就没有竖装。&lt;/p&gt;
&lt;h3&gt;跑分&lt;/h3&gt;
&lt;p&gt;没有做任何手工超频，就是按了下显卡后面的一键超频按钮：
tse 10072
&lt;img src=&quot;/imgs/a-new-pc-on-2020/benchmark-1.jpg&quot; alt=&quot;显卡跑分&quot; /&gt;
ts 19753
&lt;img src=&quot;/imgs/a-new-pc-on-2020/benchmark-2.jpg&quot; alt=&quot;显卡跑分&quot; /&gt;
奥德赛全高 4k 下平均 88 帧。
&lt;img src=&quot;/imgs/a-new-pc-on-2020/benchmark-3.jpg&quot; alt=&quot;显卡跑分&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;这台新的电脑，虽然价值不菲，是目前为止给自己买的最贵的东西了。但我对它的评价颇高，对自己各种配件型号的选择也非常的满意。
现在它又跟我的第一台电脑一样能胜任各种工作了，代码编译什么的自然不在话下，各种新出的次世代游戏也能在 4K 高特效下畅玩，精美的游戏画面，丝滑般的场景推进，配上我精准的操作，哦，真爽~ 我又能多工作一段时间了。&lt;/p&gt;
</content:encoded></item><item><title>Windows10 中 thunderbolt3 接口的故障排查经历</title><link>https://ab.kisnows.com/zh-CN/2021/03/04/a-thunderbolt3-question-on-windows/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2021/03/04/a-thunderbolt3-question-on-windows/</guid><pubDate>Thu, 04 Mar 2021 22:22:37 GMT</pubDate><content:encoded>&lt;p&gt;主板型号是 &lt;code&gt;z490 GodLike&lt;/code&gt;, 微星旗下 &lt;code&gt;Z490&lt;/code&gt; 系列最高端的品牌，拓展坞是贝尔金的 Intel 认证的 13 合一的那一款。
拓展坞插到电脑上的时候毫无反应，想了一下可能是缺少驱动。于是就上微星官网找了一下，果然有一个 thunderbolt 的驱动，下载下来却发现根本不知道如何安装：
&lt;img src=&quot;/imgs/a-thunderbolt3-question-on-windows/thunderbolt-1.png&quot; alt=&quot;&quot; /&gt;
看起来执行 setup 这个批处理文件就好了，但是每次执行都是失败，毫无进展。&lt;/p&gt;
&lt;p&gt;于是直接上网搜 thunderbolt 的驱动，看到 intel 官方有给自己 NCU 产品提供的&lt;a href=&quot;https://www.intel.cn/content/www/cn/zh/search.html?ws=text#q=thunderbolt&amp;amp;t=Downloads&amp;amp;layout=**table**&quot;&gt;驱动&lt;/a&gt;，于是下载下来直接安装。安装好后会有一个 Thunderbolt 控制中心的应用，同时在设备管理器里面也能看到名为 &lt;code&gt;Thunderbolt(TM) Controller - xxxx&lt;/code&gt;的设备：
&lt;img src=&quot;/imgs/a-thunderbolt3-question-on-windows/thunderbolt-device.png&quot; alt=&quot;&quot; /&gt;
同时打开前面提到安装好的 Thunderbolt 控制中心，能看到如下展示就说明驱动都没有问题了：
&lt;img src=&quot;/imgs/a-thunderbolt3-question-on-windows/thunderbolt-details.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;重新插拔拓展坞，会看到控制中心有提示新的设备接入，需要授权（如果 Bios 中设置的不需要授权就不会有这个提示）：
&lt;img src=&quot;/imgs/a-thunderbolt3-question-on-windows/thunderbolt-app.png&quot; alt=&quot;&quot; /&gt;
点击授权就好了。
至此，第一次 ThunderBolt3 拓展坞的连接就成功了，安装在上面的 U 盘之类的扩展设备也都能正常的识别和读取。&lt;/p&gt;
&lt;h2&gt;奇怪的事情&lt;/h2&gt;
&lt;p&gt;按理说我的拓展坞已经正常运行了，不应该再有其他的事情了。上面的操作也都是常规操作不值得专门写一篇文章记录，然而奇怪的事情就在几天后发生了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;我照常插上 U 盘，发现不识别，仔细看了一下拓展坞的指示灯也变成不正常的黄色了。但是我这几天明明什么都没有做，也没有更新过系统，也没有安装过奇怪的东西，怎么突然就不行了呢？我做了以下尝试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重新插拔拓展坞：无效&lt;/li&gt;
&lt;li&gt;卸载并重装驱动：无效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我就怀疑这个拓展坞坏了，然后上京东申请了售后,心里还在想这两千块大洋的东西怎么质量这么差呢。京东的小哥倒是很快就上门把东西拿走了。&lt;/p&gt;
&lt;p&gt;刚好我有个 type-c 接口的移动硬盘要用，想着雷电 3 接口是向下兼容 type-c 的，就插到了那个雷电 3 接口里面。如你所想，毫无反应，我又换上了我的手机插上，毫无反应，电都不冲。这回我就有点懵了，电都不冲，难道是硬件坏了？为了避免是微星的接口做了阉割，我还专门上官网查了我这块主板的雷电 3 接口介绍：
&lt;img src=&quot;/imgs/a-thunderbolt3-question-on-windows/thunderbolt-msi-3.png&quot; alt=&quot;&quot; /&gt;
写的很清楚，向下兼容 usb 3.2 而且支持 5V/3A 的充电，所以难道真的是我的主板里面接口坏掉了吗。怀着难受的心，我先是咨询了微星客服，向客服描述了我的主机现状：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不充电&lt;/li&gt;
&lt;li&gt;插 USB 协议的硬盘也不识别&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;客服觉得按照我的描述应该是主板硬件坏了，建议我走售后流程，并告知我如何报修后我们就挂断了电话。我一想，走售后的话太麻烦了，要先把主机拆掉，然后把主板寄到售后地址，等检查修完再寄回来，太麻烦了，太麻烦了。我结合之前的情况判断了一下，硬件应该是没有问题的，于是又开始了排查问题之旅。
周六那一天早上起来我就开始排查问题，做各种尝试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重刷主板 BIOS&lt;/li&gt;
&lt;li&gt;再 BIOS 中关闭 Thunderbolt 开关并重新开启&lt;/li&gt;
&lt;li&gt;卸载并重装驱动&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;期间重启了 N 此电脑，最终都以失败告终。就剩下一个没做了，那就是重装系统。但重装系统对我来说代价太大了，我又要花很多时间来配置各种开发环境，安装各种软件，想想都麻烦。于是再几个论坛发了帖子，一段时间后也没有有意义的回复，只能靠自己了。&lt;/p&gt;
&lt;p&gt;既然我觉得重装系统有可能解决问题，那么就需要进一步排除 Windows 操作系统带来的干扰。于是开机直接进 BIOS ，然后把我的 Switch 插上去，第一次没有反应，我就拔了再插了一次，竟然充电了！直接开机插的时候都是不充的，现在竟然充了。那么就说明我的硬件是没有问题的，别提我有多高兴了。&lt;/p&gt;
&lt;p&gt;于是插着 Switch 开机，进入系统后依然保持充电状态了。我试着拔掉重插了一下，又不冲了，什么情况？！我就继续插拔，发现大约插拔七八次，会有一次成功充电。而且从设备管理器中可以看到，thunderbolt3 的驱动一直在掉了又自动安装上，就这样在有驱动和没驱动之间反复横跳。&lt;/p&gt;
&lt;p&gt;至此，我已经大概率怀疑是系统的问题了。不说了，重装吧！直接用 Windows10 提供的恢复系统功能，选择保留个人文件重刷系统的选项进行重装。重装好后自带雷电 3 驱动，兴奋的插上我的移动硬盘， 这次应该好了吧。然后残酷的现实再一次给我破了冷水，毫无反应。一怒之下，我再次选择恢复系统，并选择其中的不保留个人文件。这相当于真正的重装系统，我也已经做好了花大量时间来重新配置系统的准备。这次重装之后，再插上我的移动硬盘。噢，自动弹出了资源管理器并打开了这个移动硬盘。我知道，好了，都好了。我花差不多四千五百块大洋买的主板还是没有问题的，问题都是这个奇怪的 Windows 操作系统。&lt;/p&gt;
&lt;p&gt;后来京东换货的拓展坞也到了，插上也是一切正常，问题已经解决了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;对于第二次为什么接口出问题了，到现在我也不清楚，按理说重装过驱动没有解决问题就应该排除掉是系统的问题，但最后还是通过重装解决了，神奇。
总结一下雷电 3 接口能正常运转的必要条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;BIOS 中打开 Thunderbolt 相关开关&lt;/li&gt;
&lt;li&gt;安装 Intel 提供的 Thunderbolt 驱动和控制中心程序&lt;/li&gt;
&lt;li&gt;上一步完成后，设备管理器中应该可以看到系统设备中有了 &lt;code&gt;Thunderbolt(TM) Controller - xxxx&lt;/code&gt; 的设备&lt;/li&gt;
&lt;li&gt;插上雷电 3 设备后，需要在第 2 步提到的控制中心程序中允许连接&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以上四部都做完如果接口还是不能正常运转的话，可以做以下尝试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重刷主板 BIOS&lt;/li&gt;
&lt;li&gt;重装系统&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;理论上这样就能解决所有可能是软件造成的问题了，如果到这一步还是不行，那就只能找硬件问题了。
只能说，祝好运。&lt;/p&gt;
</content:encoded></item><item><title>AICode 在 Alibaba.com 商详前端的实践</title><link>https://ab.kisnows.com/zh-CN/2025/06/20/aicode-product-detail-practice/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2025/06/20/aicode-product-detail-practice/</guid><pubDate>Fri, 20 Jun 2025 08:48:09 GMT</pubDate><content:encoded>&lt;p&gt;源自公司内部的分享文档，做了部分脱敏处理。&lt;/p&gt;
&lt;h2&gt;项目概览&lt;/h2&gt;
&lt;p&gt;这个是用 cursor 基于对商详前端应用的理解，画的一个 svg （因为 cursor 是不支持画图）格式的图片。&lt;/p&gt;
&lt;p&gt;整体上还是大概的描述出了项目的情况， 包括项目结构，技术选型，以及一些子 package 的作用。但是整体的图片在细节上有非常多的问题，文字超出、重叠、对齐问题等，多次要求 ai 修改也不行，这就是在一个合理的能效比下 ai 能做出的最佳效果了，还凑合吧&amp;lt;font style=&quot;color:#E7E9E8;&quot;&amp;gt;&amp;lt;/font&amp;gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;cursor 编辑器本身的一些实践&lt;/h2&gt;
&lt;h3&gt;指定具体的模型&lt;/h3&gt;
&lt;p&gt;不要使用 auto 模式，关掉然后指定 claude-4 ，目前测试下效果是不错的。如果使用默认的 auto 模型，那么回答质量是非常不稳定的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;cursor rule 实践&lt;/h3&gt;
&lt;p&gt;&amp;lt;details class=&quot;lake-collapse&quot;&amp;gt;&amp;lt;summary id=&quot;u1cf600f9&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;十句话让 cursor 提升 10 倍的编程水平（玩笑）&amp;lt;/span&amp;gt;&amp;lt;/summary&amp;gt;&amp;lt;p id=&quot;ubbedd309&quot; class=&quot;ne-p&quot;&amp;gt;&amp;lt;img src=&quot;/imgs/aicode-product-detail-practice/3.png&quot; width=&quot;553.5&quot; title=&quot;&quot; crop=&quot;0,0,1,1&quot; id=&quot;w5flS&quot; class=&quot;ne-image&quot;&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h4&gt;提升基本回答质量&lt;/h4&gt;
&lt;p&gt;这个是之前忘了在哪里看到的一篇文章中提到的 cursor rule &lt;a href=&quot;https://forum.cursor.com/t/i-created-an-amazing-mode-called-riper-5-mode-fixes-claude-3-7-drastically/65516&quot;&gt;riper-5&lt;/a&gt;，实践以后觉得不错，就一直在用。&lt;/p&gt;
&lt;p&gt;整体的指导流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实际使用的时候发现， Ai 对于这个指令的正向流程的跟随做的是很好的，但是在对于所有的逆向流程比如发现偏差或问题回到上一步这种流程，我实际使用下来的就没有遇到过它走到过这里。他每次都是非常的自信，给出的最终 review 都是 &lt;strong&gt;完美，很棒&lt;/strong&gt;之类的对于自己的正向反馈；没有一次能够自己发现自己的问题，即使是很智障的问题，它自己也发现不了，所以还是少不了人为干预；
&lt;ul&gt;
&lt;li&gt;比如这个例子，他的回答看着质量非常高，图文并茂，看着好像很专业的样子，但是实际上它自己画的图片里面包含的功能比如 totolPrice 的计算、quantity 的计算等相关逻辑全部都没有，所以不能被表象骗到了；&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/5.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但整体的实践以后，在日常的功能开发中能还是大幅提升回答质量的，而且即使有的时候不能帮你把代码写了，但是思考过程以及给出的多个参考方案，还是可以借鉴的；&lt;/p&gt;
&lt;p&gt;但是对于过于简单的需求，可能回答过程太冗长了，等于绕了大半天路来解决一个可能一开始就能给出最佳答案的情况；&lt;/p&gt;
&lt;h4&gt;代码重构 review&lt;/h4&gt;
&lt;p&gt;&amp;lt;details class=&quot;lake-collapse&quot;&amp;gt;&amp;lt;summary id=&quot;u06031bba&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;代码重构审查 rule&amp;lt;/span&amp;gt;&amp;lt;/summary&amp;gt;&amp;lt;h1 id=&quot;wWmsF&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;技术重构代码审查指南（Git Diff）&amp;lt;/span&amp;gt;&amp;lt;/h1&amp;gt;&amp;lt;p id=&quot;u2c5fc18c&quot; class=&quot;ne-p&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;我是前端开发工程师，我有一份 Git&amp;lt;br /&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;Diff 想让你审查。请你遵循以下技术检查清单，仔细检查并提供详尽的分析报告：&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;h2 id=&quot;LOOi8&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;检查清单&amp;lt;/span&amp;gt;&amp;lt;/h2&amp;gt;&amp;lt;h3 id=&quot;aXRIx&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;一、代码组织与结构调整&amp;lt;/span&amp;gt;&amp;lt;/h3&amp;gt;&amp;lt;p id=&quot;u055a7b62&quot; class=&quot;ne-p&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;判断是否仅是代码组织、架构、命名层面的调整，具体表现包括但不限于：&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;ul class=&quot;ne-ul&quot;&amp;gt;&amp;lt;li id=&quot;u776a755c&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;函数、方法或变量的&amp;lt;/span&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;名称调整&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;u85e6f426&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;文件或目录结构的变更（如&amp;lt;/span&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;拆分或合并文件&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;）&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;ucb3469ed&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;逻辑拆分或组合到不同函数或文件，但&amp;lt;/span&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;输入、输出、流程不变&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;u7978cd58&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;增加或改进注释&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;，不涉及逻辑变动&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;ud0300be4&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;删除废弃代码&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;，不影响现有流程&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;h3 id=&quot;Cns74&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;二、业务逻辑潜在变更&amp;lt;/span&amp;gt;&amp;lt;/h3&amp;gt;&amp;lt;p id=&quot;ud50242f7&quot; class=&quot;ne-p&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;请细致检查改动是否影响业务逻辑或功能，包括但不限于以下情况：&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;ul class=&quot;ne-ul&quot;&amp;gt;&amp;lt;li id=&quot;udc575b40&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;输入参数的类型、数量、默认值是否发生变化&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;ua096083d&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;函数或方法返回值类型或结构是否改变&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;u2eafdd6c&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;控制流（如条件判断、循环结构）是否发生逻辑上的变化&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;uf125170b&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;数据处理、计算方法或关键算法是否有调整&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;u899fcee7&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;异步调用、API 请求或依赖外部服务逻辑是否发生变动&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;uacdc6213&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;状态管理、缓存或数据存储逻辑是否有变化&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;u15240fa9&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;新增或删除了业务关键函数、方法、接口或外部依赖&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;h2 id=&quot;xab22&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;输出格式要求&amp;lt;/span&amp;gt;&amp;lt;/h2&amp;gt;&amp;lt;p id=&quot;ufb66b537&quot; class=&quot;ne-p&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;请以以下格式输出你的审查报告：&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;ol class=&quot;ne-ol&quot;&amp;gt;&amp;lt;li id=&quot;u11ddeee7&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;概要结论&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ol&amp;gt;&amp;lt;ul class=&quot;ne-list-wrap&quot;&amp;gt;&amp;lt;ul ne-level=&quot;1&quot; class=&quot;ne-ul&quot;&amp;gt;&amp;lt;li id=&quot;u580bc7d1&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;明确说明本次改动整体是否可能对现有业务逻辑造成影响。&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;ol start=&quot;2&quot; class=&quot;ne-ol&quot;&amp;gt;&amp;lt;li id=&quot;ube7b4c42&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;仅组织、架构类改动&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ol&amp;gt;&amp;lt;ul class=&quot;ne-list-wrap&quot;&amp;gt;&amp;lt;ul ne-level=&quot;1&quot; class=&quot;ne-ul&quot;&amp;gt;&amp;lt;li id=&quot;u02b90324&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;列出所有确认仅为结构、组织上的调整。&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;ol start=&quot;3&quot; class=&quot;ne-ol&quot;&amp;gt;&amp;lt;li id=&quot;uc12487a1&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;疑似影响业务逻辑的改动&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ol&amp;gt;&amp;lt;ul class=&quot;ne-list-wrap&quot;&amp;gt;&amp;lt;ul ne-level=&quot;1&quot; class=&quot;ne-ul&quot;&amp;gt;&amp;lt;li id=&quot;ucfab3daa&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;明确指出代码改动位置（文件名、行数、函数或变量名）。&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;u5ebef90d&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;详细描述改动的性质（如参数、返回值、逻辑变化）。&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li id=&quot;u9648f4a9&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;评估可能带来的影响以及风险。&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;ol start=&quot;4&quot; class=&quot;ne-ol&quot;&amp;gt;&amp;lt;li id=&quot;u6ded0c68&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;建议与风险控制措施&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ol&amp;gt;&amp;lt;ul class=&quot;ne-list-wrap&quot;&amp;gt;&amp;lt;ul ne-level=&quot;1&quot; class=&quot;ne-ul&quot;&amp;gt;&amp;lt;li id=&quot;u71ab7817&quot; data-lake-index-type=&quot;0&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;若存在疑似业务影响，请提供建议的应对措施（如增加单元测试、集成测试、代码审查等方法）来保证业务稳定性。&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;p id=&quot;ucb7330d4&quot; class=&quot;ne-p&quot;&amp;gt;&amp;lt;span class=&quot;ne-text&quot;&amp;gt;请务必细致、逐条检查，并提供充分的理由或代码片段作为支撑你的判断依据，以确保技术重构的安全性。&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;/details&amp;gt;
在每次有相对较大的改动以后，可以让 ai 帮我总结到底改了什么东西，哪些改动会影响原有的业务逻辑，哪些不会，同时给出一个整体的评价，告诉我是否需要进行回归测试之类的；&lt;/p&gt;
&lt;p&gt;实际体验上每次有大的重构，我都会用不同的模型针对我的代码修改让 ai 基于这个 prompt 来回答；然后我来 review 最终是否是存代码重构还是可能会有业务逻辑修改；实际体验下来，还是比较可靠的；用不同的 ai 回答，是为了交叉验证以避免 ai 胡说八道骗我；&lt;/p&gt;
&lt;h3&gt;cursor mcp 实践&lt;/h3&gt;
&lt;p&gt;目前在用的 mcp 如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个是同事写的语雀 mcp，用来给 cursor 提供读取语雀文档的能力&lt;/li&gt;
&lt;li&gt;一个是开源的 npm 包版本管理的 mcp，用来管理页面整体的依赖的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;商详前端应用为 Ai 做的优化&lt;/h2&gt;
&lt;p&gt;在 cursor 本身的能力加强以外，我们针对商详的前端应用，也做了一些为 cursor 定制或者说更加 ai 友好的改造。&lt;/p&gt;
&lt;h3&gt;Monorepo：每个模块之间完全独立，仅暴露固定 Api ，大幅降低代码之间的耦合度，为 Ai 理解整个仓库相对一般的前端应用更加容易&lt;/h3&gt;
&lt;p&gt;首先通过 Monorepo 的能力，把整个商详的所有依赖工程都放到了一个应用下面，避免一些孤岛应用的存在，为 ai 理解整个项目提供了基础，不存在依赖以外 ai 不知道的代码。&lt;/p&gt;
&lt;p&gt;同时商详前端已经做了多次改造，从技术栈上几乎已经完全和社区标准保持 7 一致，少有阿里内部的私有解决方案。也就是说从技术上来说，所有的技术框架、组件库以及解决方案都是最新的 ai 模型的固有知识，不存在 ai 不了解的阿里私域知识。&lt;/p&gt;
&lt;p&gt;把一个应用的所有依赖都放到一个仓库可能会出现依赖混乱、过度耦合的问题，所以我们最终通过 Monorepo 隔离各个子 package 、子应用，让他们按照统一的规范来进行交互，为 ai 理解整个仓库的运行机制打下基础。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;使用原子化 css 避免 css 复杂的作用域问题&lt;/h3&gt;
&lt;p&gt;通过使用 tailwind css 方案，在技术上确保所有的样式都是独立的、不会互相干扰的样式。为 ai 在生成样式代码时，不需要理解整个项目的复杂 css 全局，只需要关心当下部分的内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;项目级别的 cursor rule&lt;/h3&gt;
&lt;h4&gt;针对商详的特定项目情况，编写整个项目的 devGuid&lt;/h4&gt;
&lt;p&gt;参考文档：&lt;code&gt;https://code.alibaba-inc.com/sc/detail-fantasy/blob/master/project-rule.mdc&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心介绍项目的结构、开发理念，以及要注意的内容&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/9.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;针对 pc&amp;amp;wap 不同的特性编写不同的 cursor rule&lt;/h4&gt;
&lt;p&gt;由于大模型上下文有限，所以在 project 级别只写了关键的规则，对于开发 pc 和 wap 时的不同规则则通过 globs: apps/wap/**/. alwaysApply: false 条件引用确保只对特定的文件修改生效 prompt。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://code.alibaba-inc.com/sc/detail-fantasy/blob/master/.cursor/rules/pc-dev-guide.mdc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://code.alibaba-inc.com/sc/detail-fantasy/blob/master/.cursor/rules/wap-dev-guide.mdc&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;针对每个独立的子 package 写了 README，供 ai 理解&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;针对特定工作编写特定的 rule&lt;/h4&gt;
&lt;p&gt;我最近在做 m 站的体验拉齐项目，涉及到老的 wap detail 代码的迁移和重构。&lt;/p&gt;
&lt;p&gt;其中很大的一部分工作就是商详的模块的迁移，需要根据 App 的样式，pc 的数据，来迁移 wap 的模块，同时需要严格遵守我们的项目规范；所以专门为此写了特定的 rule，&lt;code&gt;https://code.alibaba-inc.com/sc/detail-fantasy/blob/master/apps/wap/src/modules/wap_module_devGuide.mdc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;内容的核心就是强调了我们的模块开发规范，以及如何使用项目中现有的工具和方法来完成工作，不要自己一通乱写。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;通过 eslint 解决 ai 破坏同构的问题&lt;/h3&gt;
&lt;p&gt;同构问题：结合 cursor rule 和 eslint ，实现 ai 生成的代码尽可能不破坏同构&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过 cursor rule 限制，但是无法完全保障，因为 ai 会一本道
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/12.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过 eslint 限制，可以约束 ai 的一些行为，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;通过 swagger 生成前后端字段协议类型，约束 ai 的乱写乱画&lt;/h3&gt;
&lt;p&gt;关键内容： detail.backend.d.ts ，基于后端接口生成前端需要的 ts 类型定义，和我们后端在商详落地了 swagger 作为前后端数据交互的文档。同时基于 swagger 生成的 openApi.json ，生成前端工程需要的 ts 类型定义文件。&lt;/p&gt;
&lt;p&gt;解决 ai 不理解前后端数据交互模型的问题，给了 ai 能从前端代码上了解整个数据模型的可能。&lt;strong&gt;&amp;lt;font style=&quot;color:#DF2A3F;&quot;&amp;gt;实践下来对 ai 的帮助非常大&amp;lt;/font&amp;gt;&lt;/strong&gt;，让其能够通过字段命名理解字段含义并用在正确的地方，如果后端同学能够依照 swagger 的规范给字段编写详细的文档，那么效果可能会更好。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://aliyuque.antfin.com/hzpmxg/ss6a8g/qrsg56ick9mx4hgv&quot;&gt;基于 swagger 的前后端接口约定规范&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;通过单元测试确保 ai 修改的代码有一定的保证&lt;/h3&gt;
&lt;p&gt;由于商详的业务本身特性， 会在前端有非常多的比较复杂的数据处理和计算。原本在非 ai 情况下都是有经验的前端同学参与，所以所有写业务逻辑、ui 逻辑都糅合在项目的 store 中，虽然说把逻辑单独提取出来是一个不错的优化点，但是在繁杂的业务需求下这个优先级是提不上来的。&lt;/p&gt;
&lt;p&gt;但是由于我们需要 Ai 辅助全栈，也就是说有非前端经验的后端同学也要来开发前端的应用。那么原本复杂各种逻辑糅杂的 store 对于后端同学理解来说有点困难，而且对于 ai 修改这一块的代码也都心里没有底。所以我们在 wap 改造时把纯后端的数据处理逻辑，一些特定的业务逻辑都单独抽成独立的方法，在 store 中只是调用而已。这样的话，在绝大多数情况下这些代码可能不需要改动，store 的复杂度是可以急剧降低的，对于后端同学参与也相对友好一点。&lt;/p&gt;
&lt;p&gt;所以我们改造的方向就是尽可能抽离这些相对固定的业务逻辑处理函数，收口在独立的 package 中。由于抽离出来的都是纯函数，所以也比较方便补足单元测试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样改造以后，对于核心业务逻辑基本上都抽离到独立的 package biz-core 中，上层 wap &amp;amp;pc（改造中）的 store 也只是对 biz-core 的调用，一般情况下不感知他们的具体逻辑。&lt;/p&gt;
&lt;p&gt;这样即使 ai 修改代码存在不可控，但在修改任何代码以后只要能通过我的单元测试，那么业务逻辑风险基本可控。&lt;/p&gt;
&lt;h3&gt;大幅降低 store 的复杂度&lt;/h3&gt;
&lt;p&gt;下面的图是我们页面的 store 的改造前后的对比，可以一眼看到他们之间的复杂度还是有非常大的简化的，当然一部分原因是 wap 端没有样品、物流模块本身业务复杂度上较低，但对于普通品本身的复杂度也是做了大幅简化。对于 ai 理解整个页面的状态也有帮助。&lt;/p&gt;
&lt;p&gt;之前：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之后：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/aicode-product-detail-practice/17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实际体验&lt;/h2&gt;
&lt;h3&gt;前端的 Ai 体验&lt;/h3&gt;
&lt;p&gt;这个是我大约三周的开发过程中的一些我觉得有意义的记录，仅供参考。其中让我觉得&lt;strong&gt;最有用的两次&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;[使用 mock 数据的模块进行打标]：我只需要功能，完全不关心实现，也完全不关心后续的维护，我也不想动脑子，所以这种问题交个 ai 非常合适，而且最终他也非常好的帮我实现了我想要的内容；&lt;/li&gt;
&lt;li&gt;[为了店铺性能的录屏优化而做的一个需要后端支持的需求]：由于我对店铺本身渲染上有不错的理解，加上后端的指引，借助 ai 快速的完成了我要的东西，&lt;strong&gt;帮助巨大，节省 1.5d&lt;/strong&gt;；&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;全栈的 Ai 体验&lt;/h3&gt;
&lt;p&gt;在做完 Ai 改造以后，我们有全栈的同学进来参与需求开发，实际运行下来还是有帮助的。我们的后端同学能够在 ai 的帮助下，快速的完成一些简单的需求，复杂一点的需求多花一点时间也可以最终完成，解决了从无到有的问题，在一些情况下前后端同学可以快速补位支持业务。&lt;/p&gt;
&lt;p&gt;但依然存在一些问题，有些样式的改造、组件的使用由于对前端工程的理解程度相对较浅，cursor 即使在各种 rules 的限制下也依然是会随意发挥，改使用组件的不使用组件，有现成方法的不用非要自己写，但是全栈同学可能一下看不出来这些问题，只能从浏览器看这一块的改造展示是否 ok ，最终会导致一部分返工。&lt;/p&gt;
&lt;p&gt;但总体上看，在一定程度上是可以提升人员的补位程度的。&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;p&gt;我预估我的 cursor 使用的统计数据中，代码采用率可能在 40%～ 60%，但是实际的效率提升应该是在 15%～ 25% 之间，这之间的 gap 在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统计的代码采用率会远高于实际最终 merge 到主干的代码：因为每次他生成的代码我没有时间去详细的看到底对不对，所以我大部分时间下都是瞄一眼代码，没有问题就直接 accept 然后看效果是否满足预期，因为每次采纳代码也只是为了测试一下这次抽卡抽的成功与否，只有在满足预期以后才会最终 merge 这份代码或者修改以后再 merge 代码；实际的大多数情况下，都是同一个问题可能 accept 了三到五次甚至更多次以后，才勉强给了一个能用的版本，然后在上面进行修改后 merge ，所以实际的代码采用率应该在 20% 左右；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;对于全栈（如果没有相应的技术背景）：可以快速的切入其他技术栈进行需求开发，但&lt;strong&gt;仅限于非常简单的需求&lt;/strong&gt;，稍微有复杂度的需求就需要更长的时间沉淀和相关技术栈基础知识进行学习才可能支持的了；但是在特定情况下可以大幅提升效率。&lt;/li&gt;
&lt;li&gt;对于前端自己：如果不抱着 cursor 能够提效的目的来用，那么提效比例大概是在 10%～ 20% 之间；如果抱着 cursor 一定要给我提效多少多少来用，那么整体上提效有限；
&lt;ol&gt;
&lt;li&gt;cursor 本身的 AiTab 功能就非常好用，日常开发中在不需要任何环境支持的情况下就可以&lt;strong&gt;提效 5%～ 10% 左右&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;对于一般的 ui 需求，&lt;strong&gt;如果是一次性的日抛 or 月抛代码&lt;/strong&gt;，那么直接让 Ai 来搞，风险可控，&lt;strong&gt;提效不错&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;对于非最终上线功能，比如开发中需要一个临时的打标样式功能之类的最终不会上线的功能，那么可以全权交给 Ai ，只要最终不会改到我们的生产代码，那么多次对话以后就可以得到你要的效果，&lt;strong&gt;提效明显&lt;/strong&gt;；&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;lt;font style=&quot;color:#DF2A3F;&quot;&amp;gt;使用 Ai 的一些陷阱&amp;lt;/font&amp;gt;&lt;/strong&gt;：对于一些复杂问题，直接让 ai 去写，可能在 n 轮对话以后， ai 确实给出了一个能够 work 的代码，但这个本身是一个陷阱，体现在两个方面：
&lt;ol&gt;
&lt;li&gt;你自己直接上手写，可能花费的时间不会比 ai 写来的多，但是使用 ai 一直对话一直让它改，在这个过程中，你的脑子出力了，但又没完全出力，最终能够 work 的代码也只是你黑盒测试加肉眼粗略的白盒测试以后的结果，你觉得可以 work，就打算发上线了，毕竟开发时间已经耗费掉了；这个是时间上的陷阱，看似 ai 生成了代码，但短期看也没有提效；&lt;/li&gt;
&lt;li&gt;这份通过 ai 生成的解决一个复杂问题的代码，只要不是一个非常独立的模块，那么这份代码就&lt;strong&gt;变成了纯纯的负债&lt;/strong&gt;，&lt;strong&gt;因为你作为开发这个代码的人，你也不完全理解这一块的逻辑&lt;/strong&gt;，这个是很可怕的情况；虽然生成这份代码的过程中你是一直参与的，但是你的脑子可能并没有全程参与，最终虽然代码能够 work，但是为什么能 work 你仅仅只有一个模糊的概念而已；当真的出现问题，或者需要加功能的时候，你对代码本身没有完整的掌控了，此时你面临两个选择：
&lt;ol&gt;
&lt;li&gt;把之前 ai 生成的代码完整的吃透，然后基于它的代码来加功能；之前所谓的节省的时间有需要重来一遍，在效率上，整体就是降效而非提效了&lt;/li&gt;
&lt;li&gt;继续把功能交给 ai ，让它在它的基础上去改，但是随着功能的迭代，ai 大概率会掌控不了这种情况，出现了拆东墙补西墙或者按下葫芦起了瓢的情况就会开始出现；&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终，在一开始你看似 ai 为你的大脑降低了负担，但实际上整体无论是从效率还是对项目的掌控上，都是下降的，负向的。所以不推荐对于复杂的多依赖的功能，使用 ai 来实现。需要加强的个人能力：&lt;strong&gt;要能感知模型的边界&lt;/strong&gt;，在该用的地方用，不该用的地方不用，那么 ai 提升的就是很大的，否则可能很多情况下都是负向的；&lt;/p&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;p&gt;一篇 ATA 文章中提到的内容，我觉得非常有帮助：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;当写代码不再那么重要，程序员的能力要求必然发生变化。

**我们需要快速适应变化的能力** ：AI 技术发展日新月异，从开始写提示词调用大模型，再到模型微调和搭建工作流和智能体，到现在开始搞 MCP 等，从 DeepSeek-R1 、V3 到 DeepSeek-R1 0528 ，从 Claude 3.5 到 3.7 和 4 模型也在飞速发展。从 AI 只能写纯前端的“玩具”代码，到现在使用 Cursor 可以实现企业级的 AI Coding，也不过几个月甚至一两年的光景。

**我们需要更强的提出问题的能力** ：只有懂 AI ，又懂业务，才能发现问题，提出问题，进而解决问题。

**我们需要更强的任务拆解能力** ：能够感知模型边界，将任务拆解为大模型可以 Hold 住的粒度，才能更好地发挥出模型的效果。

**我们需要更强的架构设计的能力** ：代码不是资产，真正的资产是代码所实现的业务能力。每一行代码都需要维护、安全保障、调试和淘汰，本质上是负债。AI Coding 带来了提效，同时也带来了很多风险，技术债的积累，程序员编码能力退化等。新的 AI Coding 工具让程序员从基础的编码中解放出来，可以更专注于系统架构的设计。代码易得的时代，设计出复杂且连贯系统架构的人，比单纯会写代码的人更有价值。详情参见：[https://www.linkedin.com/posts/vbadhwar_sysadmins-devops-ai-activity-7333228313433227268-WNW4](https://www.linkedin.com/posts/vbadhwar_sysadmins-devops-ai-activity-7333228313433227268-WNW4?spm=ata.21736010.0.0.50de4238i2BGMr)

**我们需要更强的需求理解能力** ： 快速理解需求，使用 AI 工具快速实现。

**我们需要更强的沟通表达能力** ：现在很多 AI Coding 团队反馈用户不懂如何提问。现在这个时代很多人说“提示词工程不重要了”，然而，很多特别好用的提示词依然需要精心设计。很多人并不能很好地表达需求，缺少输入，缺少明确的输出要求，任务粒度太粗，上下文不足等等问题非常突出。沟通表达能力不是人与人的沟通，还应该包括人与 AI 的沟通。随着 AI 能力的不断增强，我们的工作方式开始更多地和 AI 沟通，因此提示词工程，沟通表达能力非常重要。

**我们需要更强的批判性思维能力** ：AI 很容易出现幻觉，我们需要有能力评判出 AI 产出的内容是否正确。

**我们需要终身学习的能力** ：技术发展远比想象得更快，上面有一张图提到 60 岁程序“终身学习” 快速掌握新一代 AI Coding 工具，可以超越自己的能力限制，轻松编写代码。终身学习让人能够跟上时代发展的步伐，享受到时代发展的红利。值得庆幸的是， AI 的时代，学习成本进一步降低，可以创建各种有意思的智能体快速帮助我们学习和理解知识，已经是现在进行时。
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Alibaba.com 前端性能优化经验总结</title><link>https://ab.kisnows.com/zh-CN/2023/01/11/alibaba-buyer-performance-optimization-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2023/01/11/alibaba-buyer-performance-optimization-summary/</guid><pubDate>Wed, 11 Jan 2023 15:15:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本文原发于公司内部 ATA 平台，后来经过数据披露审核后，重新整理成为了这篇文章。所以文章内会有很多内部系统的介绍和链接，这些链接以及平台无法在非公司内网打开，但不影响文章整体的阅读和理解。本文同步发布于阿里开发者平台，地址：&lt;a href=&quot;https://mp.weixin.qq.com/s/Mi9LSBo2BbdnpttOxKK0Ig&quot;&gt;经验总结｜我们如何进行 Alibaba.com 买家性能优化？&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;h3&gt;为什么要做性能优化&lt;/h3&gt;
&lt;p&gt;对绝大多数网站来说，从宏观视角上看主要有两个因素影响最终的网站业务效益：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户的整体规模&lt;/li&gt;
&lt;li&gt;用户在网站内的转化率&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;业务效益 = 用户规模 * 转化率
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以为了提升业务效益，需要提升用户规模以及转化率。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用户规模&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用户主要由新用户和老用户组成，新老用户也有各自的流量组成，但无论新老用户进入网站，都要经过从用户的客户端也就是站外落地到我们网站也就是站内这个过程。&lt;/p&gt;
&lt;p&gt;尤其我们 Alibaba.com 是一个全球化的网站，从网络设施发达的欧美到欠发达的第三世界国家，都有用户通过电脑或者手机等设备访问我们的网站。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/country.png&quot; alt=&quot;image.png&quot; /&gt;
Alibaba.com 某页面的用户国家分布图&lt;/p&gt;
&lt;p&gt;而复杂网络状况以及技术现状，会导致网页加载的缓慢，用户很可能无法忍受漫长的白屏等待而选择关闭网站，造成用户流失。
很多案例也说明了加载性能对用户规模的影响：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.creativebloq.com/features/how-the-bbc-builds-websites-that-scale&quot;&gt;At the BBC we’ve noticed that, for every additional second a page takes to load, 10 per cent of users leave.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/pinterest-engineering/driving-user-growth-with-performance-improvements-cfc50dafadd7&quot;&gt;The result of rebuilding our pages for performance led to a 40 percent decrease in Pinner wait time, a 15 percent increase in SEO traffic and a 15 percent increase in conversion rate to signup.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以网站的加载性能会直接影响到最终落地站内的用户规模。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;转化率&lt;/strong&gt;
影响转化率的因素非常多：网站提供的价值、用户的意愿度以及页面的用户体验都是重要的影响因素。而网站的交互性能，则是用户体验的一大影响因素。&lt;/p&gt;
&lt;p&gt;如果一个网站打开过程中，页面元素各种抖动、滚动页面时卡顿掉帧，或者鼠标点击一个按钮时需要一秒钟才能响应用户的交互，都会极大地损害用户体验，直接影响用户的点击率和蹦失率，最终对转化率造成影响。&lt;/p&gt;
&lt;p&gt;所以我们要做性能优化。&lt;/p&gt;
&lt;h3&gt;为什么总是做性能优化&lt;/h3&gt;
&lt;p&gt;每隔一段时间就要重新提性能优化，为什么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;性能腐化：随着业务的发展和功能迭代，越来越多的功能会逐步添加到现有的业务中，功能复杂度的增加也会导致性能现状逐步恶化&lt;/li&gt;
&lt;li&gt;指标的迭代：随着技术的发展以及对用户极致体验的追求，会有更加准确和贴合用户体感的性能评价指标被提出和使用，老的性能评价指标指导下的优化结果在新的指标下并不一定适用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于这些原因，我们现有的页面性能并不是很理想。&lt;/p&gt;
&lt;h2&gt;目标&lt;/h2&gt;
&lt;p&gt;我们将性能优化的目标定为买家前台核心浏览链路页面满足 Core Web Vitials 衡量指标下的 Good Url 标准。&lt;/p&gt;
&lt;h3&gt;什么是 Core Web Vitials&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://web.dev/vitals/&quot;&gt;Core Web Vitials&lt;/a&gt;(后面简称为 cwv) 是 Google 为了衡量页面性能而提出的有多个指标组成的一套页面体验衡量标准，目前主要包含三个指标：LCP, FID, CLS。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;LCP: 最大内容绘制，测量加载性能。&lt;/li&gt;
&lt;li&gt;FID: 首次输入延迟，测量交互性能。&lt;/li&gt;
&lt;li&gt;CLS: 累计布局偏移，测量视觉稳定性。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;为什么采用 Core Web Vitials 衡量指标&lt;/h3&gt;
&lt;p&gt;从上面对 cwv 的解释可以看到，核心指标从页面加载速度、用户输入性能以及页面的视觉稳定性三个方向衡量了一个好的页面性能应该具备的特点，这三个指标也是用户体验的核心要素。 相较于我们以前的衡量方式 hero element time ，即由每个业务线的开发自己定义一个关键页面元素,以它的渲染完成时间来衡量页面的性能，cwv 的指标明显要更加全面，而且更加通用，不易作弊且经受了整个行业的考验。&lt;/p&gt;
&lt;p&gt;下面是 Google 官方对于真实网站在 cwv 指标优化下的性能提升情况的统计：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Why page performance matters Studies show that better Core Web Vitals improves user engagement and business metrics. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When a site meets the Core Web Vitals thresholds, research showed that users were 24% less likely to abandon page load.&lt;/li&gt;
&lt;li&gt;With each 100ms reduction in Largest Contentful Paint (LCP), web conversion rate for Farfetch increased by 1.3%.&lt;/li&gt;
&lt;li&gt;Reducing Cumulative Layout Shift (CLS) by 0.2 led Yahoo! JAPAN to a 15% increase in page views per session, 13% longer session durations, and a 1.72 percentage point decrease in bounce rate.&lt;/li&gt;
&lt;li&gt;Netzwelt improved Core Web Vitals and saw advertising revenues increase by 18% and page views by 27%.&lt;/li&gt;
&lt;li&gt;Reducing CLS from 1.65 to 0 significantly uplifted domain rankings globally for redBus.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以看到无论是 LCP，FID 还是 CLS ，他们的提升都能实实在在的带来业务的提升，这也是我们要做性能优化的原因以及选择 cwv 作为衡量指标的原因。&lt;/p&gt;
&lt;p&gt;更多选择 cwv 作为衡量指标的原因，可以参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://webmasters.googleblog.com/2020/05/evaluating-page-experience.html&quot;&gt;Why Google chose Core Web Vitals as the new page experience ranking factor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ata.alibaba-inc.com/articles/250883?flag_data_from=mail_daily_group&amp;amp;uid=13035&quot;&gt;全球化的前端性能度量&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们将性能的衡量指标定为 Core Web Vitials，并将目标定为满足 Good Url 标准，即：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LCP &amp;lt; 2500ms&lt;/li&gt;
&lt;li&gt;FID &amp;lt; 100ms&lt;/li&gt;
&lt;li&gt;CLS &amp;lt; 0.1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优化方法&lt;/h2&gt;
&lt;p&gt;性能优化是一个老生常谈的课题，网上有很多现成的方案和方法。当我们直接照搬这些方案或方法时，可能并不一定能快速达到我们的目标。
比如著名的雅虎军规里面说：静态资源尽量分散在多个域名下，以提高浏览器并发请求的数量。但随着技术的发展 HTTP2 的普及，这条方案已经并不适用了。又比如有文章说 js 里面的数组遍历方法 for 比 forEach 要快很多，所以为了更好的性能我们要用 for 来代替 forEach，你花了几天时间把所有 for 改成了 forEach 结果 cwv 指标根本没有任何变化。&lt;/p&gt;
&lt;p&gt;所以我们要做性能优化，不能单纯的照搬别人的方案，而是要掌握性能优化的方法，正所谓理论指导实践。对于性能优化来说，方法就是：度量、分析和验证。&lt;/p&gt;
&lt;h3&gt;度量&lt;/h3&gt;
&lt;p&gt;如果我们不能衡量页面现状，那么优化也就无从谈起。&lt;/p&gt;
&lt;p&gt;所以性能优化的首要事情就是要有度量页面的性能现状的方案，为后面的分析和优化提供数据支持。&lt;/p&gt;
&lt;p&gt;前面提到我们选择 cwv 作为我们的性能衡量指标，Google 提供了很多 cwv 的检测方式，包括 PageSpeed Insights、Chrome 开发者工具、搜索控制台、web.dev 测量工具等测试工具和平台。&lt;/p&gt;
&lt;p&gt;但它们都有一个问题，不能够&lt;strong&gt;实时检测&lt;/strong&gt;线上实际用户的总体统计值，要么是只能本地单次通过类似 PSI（PageSpeed Insights）的测试来看单次评估结果，要么是类似 Google Search Console 这样只能在滞后一段时间以后才能看到大量用户的实际统计值。&lt;/p&gt;
&lt;p&gt;而实时的性能数据对我们非常重要，如果没有实时数据，那么会有一下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法快速验证实验方案，每上一个方案等两周再看效果，这效率难以接受&lt;/li&gt;
&lt;li&gt;对于一些大的改造，如果造成了性能的严重下跌，却没有实时数据的反馈，两周后再看数据时可能已经对业务造成了很大的损失&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们必须有一套实时观测性能数据的方案，来指导我们优化的策略或者优化方向是否正确。&lt;/p&gt;
&lt;p&gt;最终我们选择 &lt;a href=&quot;https://github.com/GoogleChrome/web-vitals&quot;&gt;web-vitals&lt;/a&gt;来获取 cwv 的值，并在 ICBU 统一性能监控脚本 big-brother 中添加了相应的打点来上报每个用户的性能数据并建立了相应的性能监控和报表来实时观测页面的性能数据，详细可以参考：&lt;a href=&quot;https://ata.alibaba-inc.com/articles/229830?spm=ata.25287382.0.0.570337b2VWzgjX&quot;&gt;Google Core Web-Vitals  统计&amp;amp;监控&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;_PS：现在可以直接接入 _&lt;a href=&quot;https://agado.alibaba-inc.com/overview&quot;&gt;agado&lt;/a&gt;&lt;em&gt;来快速获取更加详细的性能数据。&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;分析&lt;/h3&gt;
&lt;p&gt;结合上一步获取的性能数据，我们可以系统性的分析当前每一个页面的性能现状。知道它现在到底慢不慢，为什么慢，具体哪里慢，这样的话我们也可以针对性的进行优化。&lt;/p&gt;
&lt;p&gt;拿一个典型的前后端分离的页面来看，LCP 的组成部分如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/anyalzy.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到，从用户打开浏览器到页面的 LCP 出现需要经过：建联、后端响应，HTML 传输下载，前端资源下载再到最后的解析渲染，链路非常长，导致最终的 LCP 往往离 2500ms 相去甚远。&lt;/p&gt;
&lt;p&gt;如何减少 LCP 的时间，不考虑技术细节的情况下我们不难想到，就是缩短每个耗时组成或者尽可能让他们并行，这也就是我们优化 LCP 的核心宗旨：&lt;strong&gt;能减少的减少，不能减少的并行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;从 LCP 的组成上来看，其中重定向、DNS 解析以及建联和网络传输的时间主要受三方或者用户自身的网络状况限制，我们能做的并不多。&lt;/p&gt;
&lt;p&gt;所以核心的优化方向应该聚焦在我们能控制的网络耗时以外的地方，即：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;LCP 的时间线组成排布优化：即在现有的 LCP 时间线中，哪些可以去掉，哪些可以并行；&lt;/li&gt;
&lt;li&gt;LCP 的时间线组成中每一个步骤本身的耗时优化：即除了网络耗时以外，后端 RT 以及前端资源下载解析执行时间的优化；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对这两个方向的优化我们分别通过：渲染架构优化和关键渲染路径优化来进行分析优化，对于后端 RT 的优化本文暂不涉及。&lt;/p&gt;
&lt;h3&gt;渲染架构上的优化&lt;/h3&gt;
&lt;p&gt;从宏观视角上来看，渲染架构的选择是非常重要的，合理的架构选择可以提高系统的性能下限，为后续进一步优化打下基础。&lt;/p&gt;
&lt;p&gt;按照 LCP 优化的核心宗旨：能减少的减少，不能减少的并行。从前面 LCP 构成图上不难看出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传统的页面渲染是不依赖 js 加载的，也就是说这部分时间是有可能省掉的&lt;/li&gt;
&lt;li&gt;前端资源的加载和后端响应两个大的耗时组成之间是串行的关系，有没有可能让它们并行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个优化方向对应了我们渲染架构上的优化方案：同构改造以及流式渲染。&lt;/p&gt;
&lt;h4&gt;同构改造&lt;/h4&gt;
&lt;p&gt;渲染架构下性价比最高的优化就是同构改造，在有基建支持的情况下可以以较少的人力投入获取有确定性的比较大的性能提升，一般来说，同构改造可以带来 500~800ms 的性能提升。&lt;/p&gt;
&lt;p&gt;前面提到，我们现在绝大部分业务都是前后端分离的架构。这种架构的优势是降低了前后端沟通以及发布维护的成本，两端可以并行开发独立维护自己的代码库，稳定性也更高。但这种架构的问题是，由于前后端分离了，后端输出的 HTML 中只有骨架而没有内容，到了前端需要再进行一次渲染，这次前端渲染又依赖 js 的下载以及首屏数据的获取，导致渲染链路变长。&lt;/p&gt;
&lt;p&gt;能不能把这次额外的渲染的时间省掉呢？答案是可以的，同构直出就是为了解决这个问题。同样是前后端分离的架构，可以在不损失开发效率以及合作效率的情况下，通过一套 js 代码同时跑在前端和后端来实现页面的直出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/tonggougaizao.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就可以省去前端额外的 js 下载以及解析执行渲染的时间，节省下来的时间一般是 500~800ms ，但它的代价就是需要额外维护一个同构服务。&lt;/p&gt;
&lt;p&gt;同构服务的话，现在主要有通用同构服务 silkworm-engine（内部自建服务），店铺自建的同构服务，以及营销导购的接入集团天马平台所提供的同构服务。对于一般业务来说，对接 silkworm-engine 就可以了，对接本身成本也不高。&lt;/p&gt;
&lt;h4&gt;流式渲染&lt;/h4&gt;
&lt;p&gt;早在 2010 年，Facebook 的工程师就提出了 &lt;a href=&quot;https://engineering.fb.com/2010/06/04/web/bigpipe-pipelining-web-pages-for-high-performance/&quot;&gt;bigpipe&lt;/a&gt; 的页面渲染方案。通过将一个大的页面划分为多个 pagelet 来将渐进式的完成页面的渲染，核心是为了解决一次性渲染一个大的页面后端耗时过长的问题。&lt;/p&gt;
&lt;p&gt;这个方案思路非常超前，但有一个缺点就是侵入性较大，以我们现有的技术架构下来说不太可能做这样的改造。 但它背后的原理即：http1.1 （在当时，现在 http2 以后默认支持）的&lt;strong&gt;分块传输编码&lt;/strong&gt;的特性，可以指导我们来进行成本更低的优化，也就是流式渲染，将后端一次性的业务处理 vm 模板渲染并返回分为两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先返回是不依赖任何业务逻辑的 HTML &amp;lt;head&amp;gt; 部分，浏览器拿到后可以先开始进行静态资源的下载&lt;/li&gt;
&lt;li&gt;在返回第一段后，同步的去做业务逻辑处理，比如商品数据请求计算等，然后渲染 vm 并返回 HTML 剩余的 &amp;lt;body&amp;gt; 部分&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/liushixuanran.png&quot; alt=&quot;image.png&quot; /&gt;
可以看到在流式渲染的方案下，蓝框中 &amp;lt;head&amp;gt; 的下载解析以及首屏 css 等资源的下载和后端的取数计算并行了。&lt;/p&gt;
&lt;p&gt;那么我们就可以节省 Min(前端首屏资源下载时间，后端 RT) 的时间，一般情况下可以有 300~500ms 左右的优化，优化后的时间线如下：
&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/line-liushiruanran.png&quot; alt=&quot;image.png&quot; /&gt;
流式渲染的对接也有了工程化的方案:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 应用参考 &lt;a href=&quot;https://yuque.antfin.com/b2b-shop/omy4bn/fm3a1m&quot;&gt;流式渲染- Java  端接入文档&lt;/a&gt;来进行对接&lt;/li&gt;
&lt;li&gt;NodeJs 下的 Egg 应用可以参考：&lt;a href=&quot;https://ata.alibaba-inc.com/articles/193056&quot;&gt;Spark：五分钟接入流式渲染&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;渲染架构总结&lt;/h4&gt;
&lt;p&gt;通过同构改造和流式渲染两个渲染架构的上优化，一般情况下都能有 1s 以上的提升。而且不挑业务场景，只要按部就班的做，几乎所有页面都可以进行这两项改造并获得肉眼可见的增益。&lt;/p&gt;
&lt;p&gt;同时除非做了其他渲染架构上的优化，一般情况下这 1s 以上的提升不会随着页面所在项目的功能迭代而腐化，确保了性能下限。&lt;/p&gt;
&lt;h3&gt;关键渲染路径优化&lt;/h3&gt;
&lt;p&gt;引用 MDN 上对关键渲染路径（Critical rendering path, 后文简称 CRP）的定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Critical Rendering Path is the sequence of steps the browser goes through to convert the HTML, CSS, and JavaScript into pixels on the screen. Optimizing the critical render path improves render performance. The critical rendering path includes the Document Object Model (DOM), CSS Object Model (CSSOM), render tree and layout.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;简单来说就是浏览器将 HTML，CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列，优化包含文档对象模型(DOM)，CSS 对象模型 (CSSOM)，渲染树和布局在内的这些步骤可以提高首屏渲染性能。&lt;/p&gt;
&lt;p&gt;CRP 的优化涉及到浏览器的渲染原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器首先需要下载并解析 DOM 生成 DOM 树&lt;/li&gt;
&lt;li&gt;在解析的 DOM 的过程中遇到 CSS 文件进行下载并解析，生成 CSSOM&lt;/li&gt;
&lt;li&gt;根据 DOM 树和 CSSOM 生成渲染树&lt;/li&gt;
&lt;li&gt;根据渲染树进行布局&lt;/li&gt;
&lt;li&gt;布局完成后，将具体的内容绘制到屏幕上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当 DOM 或者 CSSOM 发生变化时，比如通过 js 操作了 DOM 节点或者动态插入了 style 标签等，浏览器会重新执行上述步骤。理解了 CRP 的原理，可以看到影响 CRP 的几个因素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML 文件的下载和解析时间&lt;/li&gt;
&lt;li&gt;CSS 文件下载和解析时间&lt;/li&gt;
&lt;li&gt;同步 js 文件的下载和解析&lt;/li&gt;
&lt;li&gt;DOM 和 CSSOM 的大小&lt;/li&gt;
&lt;li&gt;以及布局和绘制的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中 HTML、CSS 以及同步 js 资源，我们称其为关键资源，因为它们的下载和解析时间会影响到 CRP 的各个阶段，这样我们只要优化关键资源的下载解析以及 DOM 和 CSSOM 的大小就好了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于关键资源的优化，可以通过缩减关键资源的数量，减少资源的大小以及调整关键资源的加载顺序和缓存等来优化；&lt;/li&gt;
&lt;li&gt;对于 DOM 和 CSSOM 的优化，我们可以通过减少 DOM 节点的数量，减少 CSSOM 的大小，以及减少 DOM 和 CSSOM 的层级来优化；&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;缓存&lt;/h4&gt;
&lt;p&gt;可以通过静态资源缓存的方式来减少资源的下载时间，缩短关键路径长度，提高页面的渲染速度。
具体的方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置资源缓存时间&lt;/li&gt;
&lt;li&gt;静态资源走 CDN&lt;/li&gt;
&lt;li&gt;动静分离&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;静态资源缓存配置&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#private_caches&quot;&gt;HTTP  缓存&lt;/a&gt;的具体规范定义本文不过多赘述，简单讲可以将缓存分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;强缓存：缓存有效期内不发起请求直接使用本地缓存的内容&lt;/li&gt;
&lt;li&gt;弱缓存 or 协商缓存：本地缓存过期后发起请求和服务器协商后再使用本地的缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于一般的 Assets 应用来说，可以通过在项目根目录下添加 .assetsmetafile 文件，来自己指定资源的缓存时间，下面的示例是让浏览器以及 CDN (特定厂商)都缓存 1 年的配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cache-control:max-age=31536000,s-maxage=31536000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，只要资源所在的应用没有发布（仅针对非覆盖式发布资源，链接没有发生变化），那么一年的请求都直接走缓存的内容。&lt;/p&gt;
&lt;h4&gt;上 CDN&lt;/h4&gt;
&lt;p&gt;众所周知 CDN 可以通过自己强大的网络和服务器，将静态资源缓存到离用户最近的节点上，从而减少资源的下载时间。&lt;/p&gt;
&lt;h5&gt;CDN 静态加速&lt;/h5&gt;
&lt;p&gt;虽然静态资源上 CDN 是一个常规操作，但本文依然要把静态资源上 CDN 提出来是因为我们网站仍然有一些类似 &amp;lt;img src=&quot;//icbu-cpv-image.oss-us-west-1.aliyuncs.com/Had9f38eda3d942aa9e65ee9198b0660fU.jpg_300x300.jpg&quot;&amp;gt;的资源并没有走 CDN 分发，而是需要用户每次从源站请求下载资源。&lt;/p&gt;
&lt;h5&gt;CDN 动态加速&lt;/h5&gt;
&lt;p&gt;CDN 动态加速这个概念可能不是很常见，它的“动态”是相较于静态加速中“静态”来说的，也就是作用的对象的不同。&lt;/p&gt;
&lt;p&gt;原理上简单来说，就是利用 CDN 天然的优势：有距离用户近的边缘节点，并且分布广泛。通过 CDN 厂商专门的优化，从 CDN 的节点机器中来寻找一条从用户到源站的更优线路，避免走外网的层层网关。从而缩短 CDN 节点于源站之间的网络耗时，用户的动态请求走 CDN 节点来回源到服务器，会比用户自己访问源服务器要快很多。&lt;/p&gt;
&lt;p&gt;根据我们的统计，动态加速的收益在 200ms+ 。&lt;/p&gt;
&lt;h4&gt;提高缓存命中率&lt;/h4&gt;
&lt;p&gt;如果发布频率过高，由于版本号的变化导致资源对应的 url 频繁发生变化，那么本地的缓存除了占用本地的存储资源外已经失去了它原本的作用。&lt;/p&gt;
&lt;p&gt;所以我们需要提高页面静态资源的缓存命中率，一般提高缓存命中率的方法就是动静分离。&lt;/p&gt;
&lt;p&gt;可以通过对页面依赖的 js，css 进行分层，将公共的依赖比如 React、ReactDOM、Fusion 等打包成一个公共的 bundle 我们称其为静态资源，将业务相关的依赖打包成一个业务相关的 bundle 我们称其为动态资源。&lt;/p&gt;
&lt;p&gt;由于静态资源的发布频率是远远低于动态资源的发布频率的，在绝大多数功能迭代的时候，只会发布上层业务应用。对于用户来说就只需要下载动态资源即可，而静态资源由于没有变动，所以可以直接从缓存中读取，加快资源加载速度。&lt;/p&gt;
&lt;p&gt;这样就可以提高整体的资源缓存命中率，避免整个页面打一个 js 或者 css bundle 导致的哪怕是一行代码的发布，也需要用户重新下载整个 js, css bundle 包的问题。&lt;/p&gt;
&lt;h4&gt;建联&lt;/h4&gt;
&lt;p&gt;前面提到著名的雅虎军规里面说：静态资源尽量分散在多个域名下，以提高浏览器并发请求的数量。这是由于浏览器限制同一个域名下的请求数量，受限于当时 HTTP1.x 下一个 TCP 连接只能处理一个 http 请求的原因，为了能让浏览器能在同一时间对并行请求多个资源而提出的优化方案。&lt;/p&gt;
&lt;p&gt;到了 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP#http2_%E2%80%93_a_protocol_for_greater_performance&quot;&gt;HTTP2&lt;/a&gt; 时代，一个 TCP 连接就可以并行进行多个请求了，配合 HTTP1.1 的 Connection: Keep-Alive 来进行连接的复用，之前的优化方案已经没有太大的意义了。&lt;/p&gt;
&lt;h4&gt;域名收拢&lt;/h4&gt;
&lt;p&gt;甚至由于将资源分布在了不同的域名下，每个域名的都需要重新进行 DNS 解析、建联等过程，原本的优化手段已经变成了负向优化了。&lt;/p&gt;
&lt;p&gt;为此需要将分布在多个不同域名下的地址收敛到一个域名下，减少 DNS 解析的次数并通过连接复用和并发请求获得更好的效果。&lt;/p&gt;
&lt;p&gt;关于更多 HTTP2 的特性，可以参考：&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP#http2_%E2%80%93_a_protocol_for_greater_performance&quot;&gt;HTTP/2 – A protocol for greater performance&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;提前建连&lt;/h4&gt;
&lt;p&gt;前面提到 DNS 的解析以及建联都是非常耗时的操作，为此浏览器提供了提前进行 DNS 解析以及建联的方法，分别是：&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Performance/dns-prefetch&quot;&gt;dns-prefetch&lt;/a&gt; 以及 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preconnect&quot;&gt;preconnect&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;dns-prefetch 是 HTML5 新增的一个属性，用于告诉浏览器提前解析某个域名，这样在浏览器需要解析这个域名的时候就不需要再去解析了，可以直接使用缓存中的结果。DNS-Prefetch 可以通过两种方式使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;dns-prefetch&quot; href=&quot;https://s.alicdn.com/&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;preconnect：告诉浏览器提前建立 TCP 连接，DNS 解析以及 TLS 握手等操作，这样在浏览器需要使用这个域名的时候就不需要再去做这些操作了，可以直接使用缓存中的结果。在 head 标签添加如下的 meta 标签就可以了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;preconnect&quot; href=&quot;https://s.alicdn.com&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;PS: 需要注意，preconnet 真实建立了连接，属于比较重的操作，所以仅在关键资源对应的域名上使用，否则提前建立过多的连接可能会对页面造成负向作用。&lt;/em&gt;
由于 dns-prefetch 有着比 preconnet 更好的浏览器兼容性，建议组合 dns-prefetch 以及 preconnet 使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;preconnect&quot; href=&quot;https://s.alicdn.com/&quot; crossorigin /&amp;gt;
&amp;lt;link rel=&quot;dns-prefetch&quot; href=&quot;https://s.alicdn.com/&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;预加载&lt;/h4&gt;
&lt;p&gt;对于页面依赖的核心资源，我们可以通过资源预加载的方式来提前下载关键资源，从而缩短 CRP 所需时间。效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/preload.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过在 html head 中添加 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload&quot;&gt;preload&lt;/a&gt;的 meta 标签实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;css: &amp;lt;link rel=&quot;preload&quot; href=&quot;styles/main.css&quot; as=&quot;style&quot;/&amp;gt;&lt;/li&gt;
&lt;li&gt;js: &amp;lt;link rel=&quot;preload&quot; href=&quot;main.js&quot; as=&quot;script&quot; /&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续需要加载对应的已经预载过的资源的时候就不需要从网络上重新获取了。&lt;/p&gt;
&lt;h4&gt;非核心内容异步&lt;/h4&gt;
&lt;p&gt;我们网站的内容非常丰富，这意味着功能复杂代码体量大。如果一定要在整个首屏加载完毕之后才能使用，那么用户体验就会很差。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们可以将非核心内容异步加载，这样用户可以在首屏加载完毕之后就可以使用网站的核心功能，而非核心内容则可以在后续加载完毕之后再使用。&lt;/p&gt;
&lt;p&gt;方案上，对于使用 webpack 构建代码的用户，可以直接通过 &lt;a href=&quot;https://webpack.js.org/guides/code-splitting/#dynamic-imports&quot;&gt;dynamic import&lt;/a&gt; 来实现。&lt;/p&gt;
&lt;p&gt;比如 PPC 的 PC 页面，我们把左侧的筛选进行了异步加载，以让用户能够更早的看到更为核心的商品 List ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { lazy } from &quot;react&quot;;
import SSRCompatibleSuspense from &quot;../../component/ssr-suspense&quot;;
import { Icon } from &quot;@alifd/next&quot;;

function LeftFilterAsync({ data }: { data: PPCSearchResult.PageData }) {
  const TrafficLeftFilter = lazy(() =&amp;gt;
    // NOTE：页面的左侧筛选，功能复杂，但不属于用户首屏需要的内容，所以异步加载
    import(/* webpackChunkName: &quot;left-filter&quot; */ &quot;@alife/traffic-left-filter&quot;)
  );
  const handleChange = (link: string) =&amp;gt; {
    xxx;
  };
  return (
    &amp;lt;SSRCompatibleSuspense fallback={&amp;lt;Icon type=&quot;loading&quot; /&amp;gt;}&amp;gt;
      &amp;lt;TrafficLeftFilter
        data={data?.snData}
        i18n={data?.i18nText}
        handleChange={handleChange}
      /&amp;gt;
    &amp;lt;/SSRCompatibleSuspense&amp;gt;
  );
}

export default LeftFilterAsync;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;选择性渲染&lt;/h4&gt;
&lt;p&gt;当代码不能进一步分割，但是渲染的内容有非常多，DOM 节点数量巨大，意味 CRP 的时间。那么在首屏可以选择性的渲染一部分更为关键的内容，这样可以减少首屏渲染的时间。&lt;/p&gt;
&lt;p&gt;比如 showroom 的无线页面，首屏后端返回 48 个品的数量，而无线端的手机大部分首屏只能展示 4 个品，所以我们在首屏只渲染前 8 个品，后续的品则在 js ready 后进行二次渲染。&lt;/p&gt;
&lt;p&gt;这样，对于同步直出的页面，即减少了 html 体积，可以加快下载 html 的速度，同时又减少了首屏 DOM 节点的数量，降低了浏览器生成 DOM Tree 的时间。&lt;/p&gt;
&lt;p&gt;最终减少 CRP 所需要的时间。&lt;/p&gt;
&lt;h3&gt;FID 以及 CLS 的优化&lt;/h3&gt;
&lt;p&gt;前面大端的篇幅提到的都是 LCP 相关的优化，因为对于面向买家的页面来说，主要是纯展示型页面，一般没有非常复杂的交互，所以 FID 和 CLS 的问题不是很凸显，开发同学在意识上注意一下就好了。&lt;/p&gt;
&lt;h4&gt;FID&lt;/h4&gt;
&lt;p&gt;FID: First Input Delay , 从用户第一次与您的网站交互直到浏览器实际能够对交互作出响应的时间。&lt;/p&gt;
&lt;p&gt;这个时间主要是由 js 的执行时间决定的，当浏览器的主线程执行 js 时，就无法相应用户的操作，最终影响 FID。那么优化的方向就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;减少 js 的执行时长&lt;/li&gt;
&lt;li&gt;分割 js 中繁重计算任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;减少 js 的执行时长&lt;/h5&gt;
&lt;p&gt;限制页面依赖的 js 总体积，可以非常直观的降低执行时长。&lt;/p&gt;
&lt;p&gt;对于页面的主 js ，通过代码分割的方式延迟加载非首屏核心功能的代码，只有在代码真正要被使用，或者空闲的时候再去加载，将主线程空闲出来，从而减少 js 的执行时长。&lt;/p&gt;
&lt;p&gt;对于页面依赖的二方包，一定要严格审查，避免同样的包、依赖比如 React、Fusion 等被重复引入，这样会导致 js 的体积增大，从而增加执行时长。比如我们某个场景中引入的一个二方 SDK，体积足足 1.6MB，比我们宿主页面代码的 300kb 还大，这里就有很大的优化空间。&lt;/p&gt;
&lt;p&gt;控制第三方脚本的引入，或者尽可能延迟他们的加载，有些投放页面里面依赖了很多三方的统计脚本，这些脚本的加载会阻塞页面的渲染，从而影响 FID。&lt;/p&gt;
&lt;h5&gt;分割 js 中繁重计算任务&lt;/h5&gt;
&lt;p&gt;多余繁重的计算任务，可以通过 requestAnimationFrame 的方式，将计算任务分割到多个帧中执行，从而减少 js 的执行时长，给浏览器留出更多的空闲时间可以响应用户的操作。&lt;/p&gt;
&lt;p&gt;如果是无法分割的计算任务，可以考虑使用 web worker 的方式，将计算任务放到 worker 中执行，从而不会阻塞主线程。&lt;/p&gt;
&lt;h4&gt;CLS&lt;/h4&gt;
&lt;p&gt;CLS: Cumulative Layout Shift , 通过计算未在用户输入 500 毫秒内发生的布局偏移的偏移分数总和来测量内容的不稳定性。&lt;/p&gt;
&lt;p&gt;导致 CLS 较差的原因：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;无尺寸的图像&lt;/li&gt;
&lt;li&gt;无尺寸的广告、嵌入和 iframe&lt;/li&gt;
&lt;li&gt;动态注入的内容&lt;/li&gt;
&lt;li&gt;导致不可见文本闪烁 (FOIT)/无样式文本闪烁 (FOUT) 的网络字体&lt;/li&gt;
&lt;li&gt;在更新 DOM 之前等待网络响应的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;在国际站场景，核心需要注意的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图片需要设置具体的宽高而不是 auto ，这样浏览器在图片下载完成之前就知道该预留多少位置出来&lt;/li&gt;
&lt;li&gt;页面主体部分尽量同步直出，对于异步渲染的区块，做好占位，不要因为异步渲染导致页面跳来跳去&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本上做到这些，那么 CLS 就可以达到标准。&lt;/p&gt;
&lt;h2&gt;策略沉淀&lt;/h2&gt;
&lt;p&gt;在我们这一年的优化过程中，沉淀了一些工具和经验来帮助我们快速的进行性能优化。&lt;/p&gt;
&lt;h3&gt;工具或系统沉淀&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://agado.alibaba-inc.com/overview&quot;&gt;agado&lt;/a&gt;*： *端架构团队打造的全球化性能度量平台，几行代码即可高效接入全面的性能度量系统；&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yuque.antfin.com/ks3gwb/cswbx6&quot;&gt;silkworm engine&lt;/a&gt;：买家基础技术在五年前左右开始搭建的一套通用同构渲染服务，满足除特定解决方案下（店铺、天马等）的页面快速接入同构能力的诉求，现在由 @咸鱼 维护；&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yuque.antfin.com/b2b-shop/omy4bn&quot;&gt;流式渲染二方库&lt;/a&gt;：快速接入流式渲染的解决方案，并在前期打通了各层代理，后端接入成本可以从 3 人日减少到小时级别；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;取得的成果&lt;/h2&gt;
&lt;h3&gt;SEO 性能优化&lt;/h3&gt;
&lt;p&gt;Google Search Console 后台的 GoodUrl 比例增长：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PC：&lt;strong&gt;0 -&amp;gt; 85.9%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;无线：&lt;strong&gt;0-&amp;gt; 95.1%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体性能指标（箭头指向优化后的结果）：多个场景下页面的性能都有非常大的提升，CLS 和 FID 全部达到 cwv Good Url 标准，我们着重优化的两个场景中 LCP 也达到 Good Url 标准。
对于 SEO 场景，由于性能提升带来的搜索引擎加权，以及更好的到达率，在多个场景下都有** 10%~20% **的 uv 增长，大幅提升了用户规模。&lt;/p&gt;
&lt;h3&gt;付费页面优化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Wap DPA 页面：LCP 降低了 500ms 左右，业务指标：&lt;strong&gt;uv 提升 9% 左右&lt;/strong&gt;，其他业务指标如点击率、跨端等相关指标也有非常大的提升；&lt;/li&gt;
&lt;li&gt;PC PLA 页面：LCP 降低了 900ms 左右，业务指标：&lt;strong&gt;核心点击上升 16.1%，uv 增长 4.6%；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;以及其他很多还在实验中的项目，都可以看到业务数据有不同程度的增长；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;实际案例&lt;/h2&gt;
&lt;p&gt;前面说了非常多的理论知识，接下来结合一个实际的例子 —— 无线 showroom （一个 SEO 承接页）的优化，来验证我们如何根据上面的方法来将一个 LCP、FID 以及 CLS 都不达标的页面优化为 Good Url 的。&lt;/p&gt;
&lt;h3&gt;现状分析&lt;/h3&gt;
&lt;p&gt;当我们接手 showroom 的时候，只知道性能很差，因为业务反馈说 Google Search Console 后台看到我们网站的 Good Url 的达标率为 0%，也就是说 LCP、CLS 以及 FID 都不及格。但具体差到什么样子，哪里差，一无所知。&lt;/p&gt;
&lt;p&gt;优化的诉求非常迫切，但连现状是个什么样子都不知道，那自然是没有办法优化的。所以首先要有度量现状的方案，经过分析和调研，采用了 Google web-vitals 的库来获取用户数据，使用 ICBU 之前就对接 big-brother 来上报采集到的数据，同时通过配置 xflush 来实时展示数据的变动趋势。&lt;/p&gt;
&lt;p&gt;这是最终的性能汇总大盘监控：
&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/xflush.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有了度量方案以后，我们得到了性能的现状：数据比较差，LCP、CLS 以及 FID 都需要进行优化。&lt;/p&gt;
&lt;p&gt;有了性能数据以后，再来看一下 showroom 的技术架构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ui 框架采用的 rax + dx
&lt;ul&gt;
&lt;li&gt;rax 是当时集团的类 react ui 库&lt;/li&gt;
&lt;li&gt;dx 是一个能够解决跨端开发问题的解决方案，主要用于端内 Android 和 iOS 的动态配置，Web 端也有兼容展示的方案&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;渲染链路：页面骨架上了 CDN，采用 js 调用异步接口获取首屏数据，然后异步完成页面的渲染
&lt;ul&gt;
&lt;li&gt;由于使用 dx， 需要从后端数据中获取 dx 模板的字符串，然后前端把字符串 evel 后作为通过 dx-h5 库转化为 rax 组件，再由 rax 渲染到页面上&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;样式文件：由于采用 rax 和 dx ，最终都是通过内联样式实现样式布局，没有独立的 css 文件供浏览器缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第一次同构改造&lt;/h3&gt;
&lt;p&gt;首先看 LCP 的组成：
&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/tonggou.png&quot; alt=&quot;image.png&quot; /&gt;
由于页面异步渲染的原因，即使 html 上了 CDN 也没有太大的帮助，整个渲染链路拉得非常长，我们接手后先做了同构改造。&lt;/p&gt;
&lt;p&gt;由于原本页面是 rax + dx ，rax 只是空壳，主要内容都是通过 dx 来渲染的， 所以需要对接 dx 的同构。刚好隔壁团队有一个 dx 的同构服务，我们尝试对了一下。&lt;/p&gt;
&lt;p&gt;首先我们和 Akamai 的同学沟通，将 showroom 的 html 静态化方案从 CDN 上下了下来，然后前端层面做了一些改造对接了 DX 的同构。&lt;/p&gt;
&lt;p&gt;对接完成后，虽然能够 dx 的同构也能实现直出，但是存在一个很严重的问题：服务端数据的 html 结构和前端渲染出来的结构不一样。&lt;/p&gt;
&lt;p&gt;这导致前端 js 的二次渲染页面会闪动，毕竟 html 结构不一样，做不到像 react 那样的 hydrate ，LCP 的时间也变成了二次渲染完成后的时间，同时页面 FID、CLS 也没有太大的优化。&lt;/p&gt;
&lt;h3&gt;第二次同构改造&lt;/h3&gt;
&lt;p&gt;dx 的优势是一套配置，三端使用。但是在 Web 端实际的操作使用情况下，端内的配置用到 Web 端各种适配问题，最终发展成了一个组件，端内一套 dx 模板配置，端外另一套。也就是说端内外依然是各自独立维护模板，那么它的优势已经没有了。&lt;/p&gt;
&lt;p&gt;同时由于使用了 dx，带了很多额外的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;渲染效率低下：“那么古尔丹，代价是什么？” dx 想要解决的是跨端配置的问题，但跨端就要有中间层，每加一层都是成本，导致最终的效率低下， LCP 和 FID 居高不下，之前维护的同学甚至还尝试过 wasm 的 dx 方案，但依然没有解决太多的问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/tonggou-2.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dx 同构端的输出结构和浏览器端不一致，无法进一步的优化&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这些问题下， 我们最终选择了对 showroom 进行重构，将原本 dx、rax 的方案推到，用 react 进行了重写。&lt;/p&gt;
&lt;p&gt;react 下各种技术方案都相对成熟，重构完成后对接了跑了很多年的 silkworm 的同构服务。react 的同构解决方案非常成熟了，不会出现 dx 下二次渲染页面需要闪动的问题，由于也没有胶水层，渲染效率相较于 dx 有了很大的提升。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/tonggou-3.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到渲染链路简单了非常多，从监控上看 LCP 降低了 900ms 左右，FID 直接降到了 Good 的标准 100ms 以下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/alibaba-buyer-performance-optimization-summary/tonggou-4.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;关键渲染路径优化&lt;/h3&gt;
&lt;h4&gt;应用拆分&lt;/h4&gt;
&lt;p&gt;对原本的 showroom 应用进行了拆分，从一个大应用拆分为两个应用：基础公共包 traffic-base 以及上层业务应用 traffic-free-wap。&lt;/p&gt;
&lt;p&gt;平时的发布基本都是 traffic-free-wap 的发布，traffic-base 的发布频率很低，提高缓存命中率，同时也降低了发布风险。&lt;/p&gt;
&lt;p&gt;但是由于缓存的效果是一个长期的影响，短期内看不到 LCP 的变化。&lt;/p&gt;
&lt;h4&gt;aplus 异步改造&lt;/h4&gt;
&lt;p&gt;aplus 是集团打点方案，需要对接一个 aplus.js 的脚本来实现各种打点尤其是 pv 的上报。原本页面的 aplus.js 是同步加载和执行的，会阻塞页面的渲染，所以我们将其改造为异步加载。&lt;/p&gt;
&lt;p&gt;需要修改的是后端服务中的 beacon 模块，在 beacon 模块中的给其中的 script 标签加上 async 就可以了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[aplus]
aplusKeyUrl=.com
aplusKeyUrl=.net
aplusKeyUrl=.org
aplusKeyUrl=.cn
aplusKeyUrl=.hk
aplusKeyUrl=.vipserver
aplusLocation=header
aplusCmpType=find
aplusFilter=find&quot;iframe_delete=true
aplusFilter=find&quot;at_iframe=1
aplusFilter=find&quot;/wangwang/update
aplusUrl=
&amp;lt;script
id=&quot;beacon-aplus&quot;
async
src=&quot;//assets.alicdn.com/g/alilog/??aplus_plugin_icbufront/index.js,mlog/aplus_v2.js&quot;
exparams=&quot;userid=\#getuid()\#&amp;amp;aplus&amp;amp;ali_beacon_id=\#getcookievalue(ali_beacon_id)\#&amp;amp;ali_apache_id=\#getcookievalue(ali_apache_id)\#&amp;amp;ali_apache_track=\#getcookievalue(ali_apache_track)\#&amp;amp;ali_apache_tracktmp=\#getcookievalue(ali_apache_tracktmp)\#&amp;amp;dmtrack_c={\#getHeaderValue(resin-trace)\#}&amp;amp;pageid=\#getpageid()\#&amp;amp;hn=\#gethostname()\#&amp;amp;asid=\#get_token()\#&amp;amp;treq=\#getHeaderValue(tsreq)\#&amp;amp;tres=\#getHeaderValue(tsres)\#&quot;&amp;gt;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;aplus 异步改造上线后，从监控看获得了 50ms 的 LCP 提升。&lt;/p&gt;
&lt;h4&gt;预建联、预加载以及域名收拢&lt;/h4&gt;
&lt;p&gt;按照我们的优化策略，对 showroom 整个页面的静态资源域名都收敛到了 s.alicdn.com 下面。&lt;/p&gt;
&lt;p&gt;同时对域名进行了 DNS 预解析和建联：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;preconnect&quot; href=&quot;https://s.alicdn.com&quot; crossorigin /&amp;gt;
&amp;lt;link rel=&quot;dns-prefetch&quot; href=&quot;https://s.alicdn.com&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于首屏的几个商品图，进行了预加载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link
  rel=&quot;preload&quot;
  href=&quot;https://s.alicdn.com/@sc04/kf/H2df0c8cbb22d49a1b1a2ebdd29cedf05y.jpg_200x200.jpg&quot;
  as=&quot;image&quot;
/&amp;gt;
&amp;lt;link
  rel=&quot;preload&quot;
  href=&quot;https://s.alicdn.com/@sc04/kf/H423ae0f4cf494848bb5c874632270299J.jpg_200x200.jpg&quot;
  as=&quot;image&quot;
/&amp;gt;
&amp;lt;link
  rel=&quot;preload&quot;
  href=&quot;https://s.alicdn.com/@sc04/kf/Hdf15b5c8a7c544c2aee0b2616b2715e3K.jpg_200x200.jpg&quot;
  as=&quot;image&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上线后， 从监控上看 LCP 有了 200ms 左右的提升，稳定在了 Good Url LCP 的边缘，即将达标。&lt;/p&gt;
&lt;h3&gt;流式渲染改造&lt;/h3&gt;
&lt;p&gt;做完以上优化后，我们开始进行流式渲染的改造，前后端一起打通了流式渲染的方案，预发测试效果非常好。但是上线后一直不生效，一直排查也排查不出来为什么。&lt;/p&gt;
&lt;p&gt;最后想起来我们走了 Akamai 的动态加速，这意味着用户的请求还是先打到了 Akamai 的 CDN 上，然后再打到我们的后端服务上。而我们的流式渲染是在后端服务上进行的，所以有可能是 CDN 这里做了什么导致我们不生效。&lt;/p&gt;
&lt;p&gt;和 CDN 的同学沟通后，发现需要加一个 &lt;code&gt;chunked response streaming&lt;/code&gt; 的配置，加上后重新上线测试，流式渲染终于生效了，上线后 LCP 直接降低了 500ms，达到了 CWV Good Url 标准。&lt;/p&gt;
&lt;h3&gt;CLS 优化&lt;/h3&gt;
&lt;p&gt;LCP 和 CLS 达标以后，只剩下 CLS 还差一点。对于 showroom 来说，导致 cls 的问题主要是 header 的高度不固定，以及商品卡片中图标的位置不固定。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 header 的问题，我们和业务沟通后确定了 header 的高度，并加上了占位&lt;/li&gt;
&lt;li&gt;对于 icon 图标的问题，针对性的设置固定宽度就可以了&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;优化总结&lt;/h3&gt;
&lt;p&gt;showroom 的优化过程就是根据我们前端提到的优化方法，度量-&amp;gt;分析-&amp;gt;实验，一步步推进，最终达到了 cwv Good Url 标准，search console 后台显示的 good rate 稳定在 90% 以上。&lt;/p&gt;
&lt;p&gt;同时由于达到了 Google Good Url 的要求，获得了 Google 的搜索权重的加权。按照和 Google 对接的同学提供的衡量方法，wap showroom 的 Clicks +10.6%，Impression +8.8%，相当于额外带来 10% 左右的 uv。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;如果没有系统性的理论指导，性能优化很容易变成一件非常琐碎的事情，发现 A 解决 A ，发现 B 解决 B，随着业务迭代也会逐渐腐化，而且如果方向错误的话，很容易浪费大量的时间却拿不到想要的结果。&lt;/p&gt;
&lt;p&gt;本文从性能优化的理论出发，从度量、分析、验证三个方面，介绍了我们在性能优化中的一些实践，希望能够帮助到大家。&lt;/p&gt;
&lt;p&gt;虽然目前取得了一些性能和业务上的成果，但是很多页面比如付费承接依然有很大的优化空间，需要持续的优化和推进。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/&quot;&gt;MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/vitals/&quot;&gt;Web Vitals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yuque.antfin.com/icbu-performance/ddprbu/emxdg9laor2vg1gm&quot;&gt;Alibaba.com PC  性能情况&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ata.alibaba-inc.com/articles/231222?spm=ata.25287382.0.0.570337b2VWzgjX&quot;&gt;流量页面性能分析与策略&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ata.alibaba-inc.com/articles/214609?spm=ata.23639746.0.0.447174b7dXMil2#JVb8jeYS&quot;&gt;关键渲染路径（CRP）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yuque.antfin.com/icbu-performance/ddprbu/pvrnk512t41glwgu#HcGVb&quot;&gt;Alibaba.com web 性能优化演进之路  (antfin.com)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>JavaScript 异步编程</title><link>https://ab.kisnows.com/zh-CN/2015/07/05/asynchronous-javascript-ajax/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/07/05/asynchronous-javascript-ajax/</guid><pubDate>Sun, 05 Jul 2015 04:16:56 GMT</pubDate><content:encoded>&lt;p&gt;工作中需要实现一个功能，简化一下大概是这么一个逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bills.getData = function (monthNow) {

    //DONE Get data of monthNow.
    //url = 通过 monthNow 来决定 url 地址
    var url = &quot;api/data.json&quot;;

    //noinspection JSUnusedGlobalSymbols
    $.ajax({
        url: url,
        dataType: &quot;json&quot;,
        type: &quot;GET&quot;,
        success: function (d) {
            Data = d;
        },
        error: function (d) {
            window.console.log(&quot;error&quot;);
        }
    });
	return Data
};

data = bills.getData(monthNow);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果测试的时候发现完全不对。
调试了一下发现，当 &lt;code&gt;bills.getData&lt;/code&gt; 执行完毕将 Data &lt;code&gt;return&lt;/code&gt; 出去并且跟着的赋值语句也执行完毕后，才触发了 &lt;code&gt;ajax&lt;/code&gt; 中 &lt;code&gt;success&lt;/code&gt; 事件。
这样明显是错误的，分析一下原因：由于 &lt;code&gt;ajax&lt;/code&gt; 请求是异步的，所以实际的情况是， &lt;code&gt;bills.getDate()&lt;/code&gt; 里的 &lt;code&gt;success&lt;/code&gt; 还没有执行完毕， Data 就已经已经 &lt;code&gt;return&lt;/code&gt; 了。
导致最后的赋值语句其实是错误的，它并没有被赋值为 &lt;code&gt;ajax&lt;/code&gt; 请求成功结果，这没有达到我们的预期。
所以这里需要对代码结构进行改变，我们需要在确认 &lt;code&gt;ajax&lt;/code&gt; 请求完毕成功后，再执行赋值语句。
这里有两种方法了达到效果，分别是 &lt;code&gt;callback&lt;/code&gt; 和 &lt;code&gt;promise&lt;/code&gt; 。
为了方便，我这里选择了 &lt;code&gt;callback&lt;/code&gt; 的形式。那么代码就是下面这个样子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Data 是全局变量
bills.getData = function (monthNow, callback) {

    //DONE Get data of monthNow.
    //url = 通过 monthNow 来决定 url 地址
    var url = &quot;api/data.json&quot;;

    //noinspection JSUnusedGlobalSymbols
    $.ajax({
        url: url,
        dataType: &quot;json&quot;,
        type: &quot;GET&quot;,
        success: function (d) {
            Data = d;
            if (typeof callback === &apos;function&apos;) {
                callback();
            }
        },
        error: function (d) {
            window.console.log(&quot;error&quot;);
        }
    });
};
bills.getData(monthNow,doSomeThing);
function doSomeThing(){
    //doSomeThing with Data
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以保证 &lt;code&gt;callback&lt;/code&gt; 中的 &lt;code&gt;data&lt;/code&gt; 是 &lt;code&gt;ajax&lt;/code&gt; 请求成功后的结果。&lt;/p&gt;
</content:encoded></item><item><title>初试chrome应用</title><link>https://ab.kisnows.com/zh-CN/2015/01/12/chrome-extension-firsttime/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/12/chrome-extension-firsttime/</guid><pubDate>Mon, 12 Jan 2015 05:01:28 GMT</pubDate><content:encoded>&lt;p&gt;众所周知，onedrive 是微软提供的一款个人网络硬盘，对老用户提供 25GB 的免费空间，新注册用户提供 7GB 的空间，要是这个空间可以用来做博客的图床就太好不过了。毕竟微软这么一个财大气粗的公司，而且在国内也不会随随便便就被墙掉，所以稳定性有保证。但是 onedrive 提供的公开链接并不能直接插入博客，需要做一定的格式转换才可以。在网上找到了转换连接的办法，但是要是手动转换，太麻烦了，于是想着做一个 chrome 扩展，毕竟方便嘛。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;粗略的看了一下 chrome 提供的 api，发现如果只是做一个简单的应用其实很简单。那么说干就干，下面是做好的效果图。
&lt;img src=&quot;/imgs/2015-01-11_210833.png&quot; alt=&quot;onedrive&quot; /&gt;
我也直接将内容嵌入进来，可以直接在这转换：&lt;/p&gt;
&lt;p&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/css-my/popup.css&quot;&amp;gt;
&amp;lt;script src=&quot;/js-my/popup.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;div id=&quot;driver&quot;&amp;gt;
&amp;lt;span&amp;gt;公开链接：&amp;lt;/span&amp;gt;&amp;lt;input id=&quot;one_before&quot; type=&quot;text&quot; value=&quot;请在此输入文件公开连接&quot;&amp;gt;
&amp;lt;span&amp;gt;文件类型：&amp;lt;/span&amp;gt;&amp;lt;input id=&quot;one_type&quot; type=&quot;text&quot; value=&quot;请在此输入文件类型&quot;&amp;gt;
&amp;lt;span&amp;gt;转换后地址：&amp;lt;/span&amp;gt;&amp;lt;input id=&quot;one_after&quot; type=&quot;text&quot;&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;input id=&quot;chage&quot; class=&quot;btn&quot; type=&quot;button&quot; value=&quot;获取&quot; /&amp;gt; &amp;lt;input id=&quot;clean&quot; class=&quot;btn&quot; type=&quot;button&quot; value=&quot;清除&quot; /&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;它的功能：输入从 onedrive 获取的公开链接和文件的类型，就可以得到这个文件的可直接访问的地址。
它由四个基本文件组成，分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;manifest.json&lt;/li&gt;
&lt;li&gt;popup.html&lt;/li&gt;
&lt;li&gt;popup.css&lt;/li&gt;
&lt;li&gt;popup.js&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中第一个文件是应用的描述信息，后面的三个文件分别是应用的内容、样式以及逻辑，其实就和一个网页是一样的。&lt;/p&gt;
&lt;p&gt;&amp;lt;hr&amp;gt;
因为暂时没有开通开发者账号，所以不能提交到应用商店。不过可以在这里下载：
&lt;a href=&quot;http://pan.baidu.com/s/1kTtE7EB&quot;&gt;点这里&lt;/a&gt;,下载后不能直接安装，需要手动拖动到插件管理处，然后就可以尽情使用了。&lt;/p&gt;
&lt;p&gt;如果你对源码感兴趣，那么：
&lt;a href=&quot;https://github.com/kisnows/chrome_onedriveChanger&quot;&gt;点这里&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>JavaScript 中的类和继承</title><link>https://ab.kisnows.com/zh-CN/2016/03/22/class-and-inherit-in-javascript/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/03/22/class-and-inherit-in-javascript/</guid><pubDate>Tue, 22 Mar 2016 03:38:13 GMT</pubDate><content:encoded>&lt;p&gt;我们都知道 JavaScript 是一门基于原型的语言。当我们调用一个对象本身没有的属性时，JavaScript 就会从对象的原型对象上去找该属性，如果原型上也没有该属性，那就去找原型的原型，一直找原型链的末端也就是 &lt;code&gt;Object.prototype&lt;/code&gt; 的原型 &lt;code&gt;null&lt;/code&gt;。这种属性查找的方式我们称之为原型链。&lt;/p&gt;
&lt;h1&gt;类的实现&lt;/h1&gt;
&lt;p&gt;由于 JavaScript 本身是没有的类的感念的。所以我们如果要实现一个类，一般是通过构造函数来模拟类的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person(name, age) {
  //实现一个类
  this.name = name;
  this.age = age;
}
var you = new Person(&quot;you&quot;, 23); //通过 new 来新建实例
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先新建一个 &lt;code&gt;Person&lt;/code&gt; 的构造函数，为了和一般的函数区别，我们会使用 CamelCase 方式来命名构造函数。
然后通过 &lt;code&gt;new&lt;/code&gt; 操作符来创建实例，&lt;code&gt;new&lt;/code&gt; 操作符其实干了这么几件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个继承自 &lt;code&gt;Person.prototype&lt;/code&gt; 的新对象&lt;/li&gt;
&lt;li&gt;构造函数 &lt;code&gt;Person&lt;/code&gt; 执行时，相应的参数传入，同时上下文被指定为这个新建的对象。&lt;/li&gt;
&lt;li&gt;如果构造函数返回了一个对象，那么这个对象会取代 &lt;code&gt;new&lt;/code&gt; 的结果。如果构造函数返回的不是对象，则会忽略这个返回值。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;返回值不是对象;
function Person(name) {
  this.name = name;
  return &quot;person&quot;;
}
var you = new Person(&quot;you&quot;);
//  you 的值: Person {name: &quot;you&quot;}
返回值是对象;
function Person(name) {
  this.name = name;
  return [1, 2, 3];
}
var you = new Person(&quot;you&quot;);
//  you的值: [1,2,3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果类的实例需要共享类的方法，那么就需要给构造函数的 &lt;code&gt;prototype&lt;/code&gt; 属性添加方法了。因为 &lt;code&gt;new&lt;/code&gt; 操作符创建的对象都继承自构造函数的 &lt;code&gt;prototype&lt;/code&gt; 属性。他们可以共享定义在类 &lt;code&gt;prototype&lt;/code&gt; 上的方法和属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype = {
  sayName: function () {
    console.log(&quot;My name is&quot;, this.name);
  },
};
var you = new Person(&quot;you&quot;, 23);
var me = new Person(&quot;me&quot;, 23);
you.sayName(); // My name is you.
me.sayName(); // My name is me.
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;继承的实现&lt;/h1&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;JavaScript 中常用的继承方式是组合继承，也就是通过构造函数和原型链继承同时来模拟继承的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//Person 构造函数如上
function Student(name, age, clas) {
  Person.call(this, name, age);
  this.clas = clas;
}
Student.prototype = Object.create(Person.prototype); // Mark 1
Student.prototype.constructor = Student; //如果不指明，则 Student 的 constructor 是 Person
Student.prototype.study = function () {
  console.log(&quot;I study in class&quot;, this.clas);
};
var liming = new Student(&quot;liming&quot;, 23, 7);
liming instanceof Person; //true
liming instanceof Student; //true
liming.sayName(); // My name is liming
liming.study(); // I study in class 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中 Mark 1 用到了 &lt;code&gt;Object.create&lt;/code&gt; 方法。这个是 ES5 中新增的方法，用来创建一个拥有指定原型的对象。如果环境不兼容，可以用下面这个 Polyfill 来实现（仅实现第一个参数）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!Object.create) {
  Object.create = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就是把 &lt;code&gt;obj&lt;/code&gt; 赋值给临时函数 &lt;code&gt;F&lt;/code&gt; ，然后返回一个 &lt;code&gt;F&lt;/code&gt; 的实例。这样通过代码 Mark 1 &lt;code&gt;Student&lt;/code&gt; 就得到了 &lt;code&gt;Person.prototype&lt;/code&gt; 上的所有属性。有人会问了，那么为什么不干脆把 &lt;code&gt;Person.prototype&lt;/code&gt; 直接赋值给 &lt;code&gt;Student.prototype&lt;/code&gt; 呢？&lt;/p&gt;
&lt;p&gt;是的，直接赋值是可以达到子类共享父类 &lt;code&gt;prototype&lt;/code&gt; 的目的，但是它破坏了原型链。即：子类和父类共用了同一个 &lt;code&gt;prototype&lt;/code&gt;,这样当某一个子类修改 &lt;code&gt;prototype&lt;/code&gt; 的时候，其实同时也修改了父类的 &lt;code&gt;prototype&lt;/code&gt;,那么就会影响到所有基于这个父类创建的子类，这并不是我们想要的结果。看例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//Person 同上
//Student 同上
Student.prototype = Person.prototype;
Student.prototype.sayName = function () {
  console.log(&quot;My name is&quot;, this.name, &quot;my class is&quot;, this.clas);
};
var liming = new Student(&quot;liming&quot;, 23, 7);
liming.sayName(); //My name is liming,my class is 7;
//另一个子类
function Employee(name, age, salary) {
  Person.call(name, age);
  this.salary = salary;
}
Employee.prototype = Person.prototype;
var emp = new Employee(&quot;emp&quot;, 23, 10000);
emp.sayName(); //Mark 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你们猜 Mark 2 会输出什么？&lt;/p&gt;
&lt;p&gt;我们期望的 Mark 2 应该会输出 &quot;My name is emp&quot;. 但实际上报错，为什么呢？因为我们改写 &lt;code&gt;Student.prototype&lt;/code&gt; 的时候，也同时修改了 &lt;code&gt;Person.prototype&lt;/code&gt;，最终导致 &lt;code&gt;emp&lt;/code&gt; 继承的 &lt;code&gt;prototype&lt;/code&gt; 是我们所不期望的，它的 sayName 方法是 &lt;code&gt;My name is&apos;,this.name,&apos;my class is&apos;,this.clas&lt;/code&gt;，这样自然是会报错的。&lt;/p&gt;
&lt;h2&gt;ES6 的继承&lt;/h2&gt;
&lt;p&gt;随着 ECMAScript 6 的发布，我们有了新的方法来实现继承。也就是通过 &lt;code&gt;class&lt;/code&gt; 关键字。&lt;/p&gt;
&lt;h3&gt;类的实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHello() {
    console.log(`My name is ${this.name},i&apos;m ${this.age} years old`);
  }
}
var you = new Person(&quot;you&quot;, 23);
you.sayHello(); //My name is you,i&apos;m 23 years old.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;继承&lt;/h3&gt;
&lt;p&gt;ES6 里面的继承也很方便，通过 &lt;code&gt;extends&lt;/code&gt; 关键字来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Student extends Person {
  constructor(name, age, cla) {
    super(name, age);
    this.class = cla;
  }
  study() {
    console.log(`I&apos;m study in class ${this.class}`);
  }
}
var liming = new Student(&quot;liming&quot;, 23, 7);
liming.study(); // I&apos;m study in class 7.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个继承相比上面的 ES5 里面实现的继承要方便了很多，但其实原理是一样的，提供的这些关键字方法只是语法糖而已，并没有改变 Js 是基于原型这么一个事实。不过 &lt;code&gt;extends&lt;/code&gt; 这样实现的继承有一个限制，就是不能定义属性，只能定义方法。要新添属性，还是得通过修改 &lt;code&gt;prototype&lt;/code&gt; 来达到目的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student.prototype.teacher = &quot;Mr.Li&quot;;
var liming = new Student(&quot;liming&quot;, 23, 7);
var hanmeimei = new Student(&quot;hanmeimei&quot;, 23, 7);
liming.teacher; //Mr.Li
hanmeimei.teacher; //Mr.Li
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;静态方法&lt;/h3&gt;
&lt;p&gt;ES6 还提供了 &lt;code&gt;static&lt;/code&gt; 关键字，来实现静态方法。静态方法可以继承，但只能由类本身调用，不能被实例调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static say() {
    console.log(&quot;Static&quot;);
  }
}
class Student extends Person {}
Person.say(); // Static
Student.say(); // Static
var you = new Person(&quot;you&quot;, 23);
you.say(); // TypeError: liming.say is not a function
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，在实例上调用的时候会直接报错。&lt;/p&gt;
&lt;h3&gt;Super 关键字&lt;/h3&gt;
&lt;p&gt;在子类中可以通过 &lt;code&gt;super&lt;/code&gt; 来调用父类，根据调用位置的不同，行为也不同。在 &lt;code&gt;constructor&lt;/code&gt; 中调用，相当于调用父类的 &lt;code&gt;constructor&lt;/code&gt; 方法，而在普通方法里面调用则相当于调用父类本身。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHello() {
    console.log(`My name is ${this.name},i&apos;m ${this.age} years old`);
  }
}
class Student extends Person {
  constructor(name, age, cla) {
    super(name, age); // 必须在子类调用 this 前执行，调用了父类的 constructor
    this.class = cla;
  }
  sayHello() {
    super.sayHello; // 调用父类方法
    console.log(&quot;Student say&quot;);
  }
}
var liming = new Student(&quot;liming&quot;, 23, 7);
liming.say(); // My name is liming,i&apos;m 23 years old.\n Student say.
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;至此，我们可以看到：在 ES6 发布以后，JavaScript 中实现继承有了一个标准的方法。虽然它们只是语法糖，背后的本质还是通过原型链以及构造函数实现的，不过在写法上更易于我们理解而且也更加清晰。&lt;/p&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000002440502#articleHeader7&quot;&gt;JavaScript 继承方式详解&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ruby-china.org/topics/27499&quot;&gt;JavaScript 原型系统的变迁，以及 ES6 class&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>css3选择器整理</title><link>https://ab.kisnows.com/zh-CN/2015/01/22/css3-seletor/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/22/css3-seletor/</guid><pubDate>Thu, 22 Jan 2015 04:16:33 GMT</pubDate><content:encoded>&lt;h2&gt;属性选择器&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;E[att^=&quot;val&quot;]&lt;/code&gt;：选择&lt;code&gt;att&lt;/code&gt;属性值以&lt;code&gt;val&lt;/code&gt;&lt;strong&gt;开头&lt;/strong&gt;的 E 元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;E[att$=&quot;val&quot;]&lt;/code&gt;：选择&lt;code&gt;att&lt;/code&gt;属性值以&lt;code&gt;val&lt;/code&gt;&lt;strong&gt;结尾&lt;/strong&gt;的 E 元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;E[att*=&quot;val&quot;]&lt;/code&gt;：选择&lt;code&gt;att&lt;/code&gt;属性值&lt;strong&gt;包含&lt;/strong&gt;&lt;code&gt;val&lt;/code&gt;的 E 元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;结构性伪类选择器&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;:root:&lt;/strong&gt; 根选择器，匹配元素所在文档的根元素。在&lt;code&gt;HTML&lt;/code&gt;中，根元素始终为&lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;。
&lt;strong&gt;:not :&lt;/strong&gt; 否定选择器，选择某元素以外的所有元素。下面的代码，最终会为&lt;code&gt;type=button&lt;/code&gt;的元素设置红色的背景。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
	input:not([type=&quot;text&quot;]){background-color: red};
&amp;lt;/style&amp;gt;
&amp;lt;body&amp;gt;
	&amp;lt;input type=&apos;text&apos;&amp;gt;
	&amp;lt;input type=&apos;text&apos;&amp;gt;
	&amp;lt;input type=&apos;button&apos;&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;:empty&lt;/strong&gt;: 空选择器，选择内容为空的元素。空元素为里面什么都没有的元素，包括空格。
&lt;strong&gt;:target&lt;/strong&gt;: 目标选择器，匹配文档的 url 的某个标志符的目标元素。下面的代码可以给每个 a 添加点击事件，当点击 a1 时，id=&apos;a1&apos;元素的背景会变成红色，其他同理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;style&amp;gt;
	#a1:target{background-color: red}
	#a2:target{background-color: blue}
	#a3:target{background-color: yellow}
&amp;lt;/style&amp;gt;
&amp;lt;body&amp;gt;
	&amp;lt;a href=&quot;#a1&quot;&amp;gt;a1&amp;lt;/a&amp;gt;
	&amp;lt;a href=&quot;#a2&quot;&amp;gt;a2&amp;lt;/a&amp;gt;
	&amp;lt;a href=&quot;#a3&quot;&amp;gt;a3&amp;lt;/a&amp;gt;
	&amp;lt;div id=&quot;a1&quot;&amp;gt;a1&amp;lt;/div&amp;gt;
	&amp;lt;div id=&quot;a2&quot;&amp;gt;a2&amp;lt;/div&amp;gt;
	&amp;lt;div id=&quot;a3&quot;&amp;gt;a3&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;:first-child&lt;/strong&gt;: 选择父元素的第一个子元素,&lt;code&gt;ul li:first-child&lt;/code&gt;，选择 ul 下的第一个 li 元素。
&lt;strong&gt;:last-child&lt;/strong&gt;: 与上一个选择相反，选择最后一个子元素。
&lt;strong&gt;nth-child(n)&lt;/strong&gt;: 选择父元素下的一个或多个指定的子元素。其实 n 是一个参数，可以是一个数字，也可以是一个表达式。
选择 ul 下序号为偶数（即第二个和第四个，n 从零算起）的 li，并将其字体设置为黄色。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;li:nth-child(2n){color:yellow;}
&amp;lt;ul&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;nth-last-child(n)&lt;/strong&gt;: 和上一个选择器相反，从父元素的最后一个子元素开始算起，来选择指定的元素。
&lt;strong&gt;first-of-type&lt;/strong&gt;: 选择父元素下第一个指定 tpye 的元素，&lt;code&gt;div p:first-of-type&lt;/code&gt;,选择一个 div 里的第一个 p 元素。
&lt;strong&gt;nth-of-type(n)&lt;/strong&gt;: 与&lt;code&gt;nth-child(n)&lt;/code&gt;类似，选择父元素中指定 type 的指定位置的元素。&lt;code&gt;div p:nth-of-type(3)&lt;/code&gt;,选择 div 下第 3 个 p 元素。
&lt;strong&gt;last-of-type&lt;/strong&gt;: 与&lt;code&gt;first-of-type&lt;/code&gt;类似，不过选择是父元素下最后指定 type 的元素。
&lt;strong&gt;nth-last-of-type(n)&lt;/strong&gt;: 与&lt;code&gt;nth-of-type(n)&lt;/code&gt;类似，不同的是从父元素的后面开始选择。
&lt;strong&gt;only-child&lt;/strong&gt;: &lt;code&gt;div p:only-child&lt;/code&gt;当有多个 div 时，选择仅有一个元素的 div 下的 p 元素。
仅第一个 ul 下的 li 的颜色会变成橘色，其他 li 的不变。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;li:only-child{color: orange}
&amp;lt;ul&amp;gt;
	&amp;lt;li&amp;gt;你好&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&amp;lt;ul&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;Hello World&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;only-of-type&lt;/strong&gt;: 与上一个类似，不同之处为选择父级下只有一个指定 type 的元素的子元素。&lt;/p&gt;
&lt;h2&gt;其他选择器&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;:enable&lt;/strong&gt;: 表单中，有可用（“:enabled”）和不可用（“:disabled”）两种状态。此选择其选择可用状态的元素。
&lt;strong&gt;:disbale&lt;/strong&gt;: 与上一个选择器类似，选择不可用状态的元素。
&lt;strong&gt;checked&lt;/strong&gt;: 单选按钮和复选按钮都具有选中和未选中状态。此选择器选择选中状态下的按钮。
&lt;strong&gt;selection&lt;/strong&gt;: 伪元素是用来匹配突出显示的文本(即用鼠标选则文本时的文本)。浏览器默认情况下，用鼠标选择网页文本是以“蓝色背景，白色字体”显示。&lt;code&gt;::selection{color:black;}&lt;/code&gt;,将选中的文本夜色变为黑色。
&lt;strong&gt;read-only&lt;/strong&gt;: 选择只读状态的元素。即设置了&quot;readonly=&apos;readonly&apos;&quot;,(在 html5 中为直接设置 readonly 的元素)。
&lt;strong&gt;read-write&lt;/strong&gt;: 与 read-only 相反，选择处于可读写状态的元素。
&lt;strong&gt;before&lt;/strong&gt;: 用来给元素前面插入内容。
&lt;strong&gt;after&lt;/strong&gt;: 给元素后面插入内容。&lt;/p&gt;
</content:encoded></item><item><title>基于 Express+Gulp+BrowserSync 搭建高性能的前端开发环境</title><link>https://ab.kisnows.com/zh-CN/2015/11/03/dev-environment-express-gulp-browsersync/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/11/03/dev-environment-express-gulp-browsersync/</guid><pubDate>Tue, 03 Nov 2015 04:45:23 GMT</pubDate><content:encoded>&lt;h1&gt;为什么要搭这么一套框架&lt;/h1&gt;
&lt;p&gt;公司 Pc 端以前遗留的项目，都是基于 jekyll+ruby-sass 这一套比较老的技术搭建的。不过 jekyll 的模版继承加上 sass 强大预处理能力，同时配合 Grunt 做任务管理，一切还是很得心应手的。&lt;/p&gt;
&lt;p&gt;然而随着项目规模的急剧增大，这一套东西的速度是在是太慢了，一至于后来一旦这个项目有需要求要改我就头疼，倒不是说头疼需求怎么改，而是你随便改一个文件，从 jekyll 检测到改动到编译完 sass 到浏览器自动刷新，基本需要 40+ s，这完全不能接受。&lt;/p&gt;
&lt;p&gt;于是一直就打算新搭一套开发环境，刚好前段时间有个新项目，我就拿来操刀动手了，目前已经用它做了两个项目下来，在原有基础上做了一些修改之后，已经完全可以替代原来那一套了。&lt;/p&gt;
&lt;h2&gt;性能&lt;/h2&gt;
&lt;p&gt;得益于 node-sass 对 ruby-sass 在编译速度上的碾压以及被 jade 完爆的 jekyll。现在项目中从文件变动=&amp;gt;编译完成=&amp;gt;浏览器自动刷新，整个过程在 1s 左右，可以说速度提升了几十倍。
而且整个过程都是全自动的，无论你修改了 js,css 还是 html 文件，浏览器都会在重新编译完成后自动刷新。
&lt;img src=&quot;/imgs/dev-environment/compile-time.png&quot; alt=&quot;编译时间&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;文件结构&lt;/h2&gt;
&lt;p&gt;这是项目开发时的文件结构：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/dev-environment/structure.png&quot; alt=&quot;文件结构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中 public,router,views 都是 express 默认的文件结构，这里就不做介绍了。
submodule 是我们不同项目中公用的代码库，这样可以少写很多公共代码。&lt;/p&gt;
&lt;h1&gt;技术细节&lt;/h1&gt;
&lt;p&gt;整个项目的技术栈是 jade+node-sass+js，实现全自动开发。
首先在 views 下面，定义好 jade 模版文件，然后开始新加页面&lt;/p&gt;
&lt;h2&gt;关系依赖&lt;/h2&gt;
&lt;p&gt;这个环境，主要基于 Express,Gulp 以及一系列的 Gulp 插件搭建，这个是开发环境的依赖关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;dependencies&quot;: {
    &quot;body-parser&quot;: &quot;~1.13.2&quot;,
    &quot;cookie-parser&quot;: &quot;~1.3.5&quot;,
    &quot;debug&quot;: &quot;~2.2.0&quot;,
    &quot;express&quot;: &quot;~4.13.1&quot;,
    &quot;jade&quot;: &quot;~1.11.0&quot;,
    &quot;morgan&quot;: &quot;~1.6.1&quot;,
    &quot;serve-favicon&quot;: &quot;~2.3.0&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;browser-sync&quot;: &quot;^2.9.1&quot;,
    &quot;del&quot;: &quot;^2.0.2&quot;,
    &quot;gulp&quot;: &quot;^3.9.0&quot;,
    &quot;gulp-autoprefixer&quot;: &quot;^3.0.1&quot;,
    &quot;gulp-jade&quot;: &quot;^1.1.0&quot;,
    &quot;gulp-nodemon&quot;: &quot;^2.0.4&quot;,
    &quot;gulp-sass&quot;: &quot;^2.0.4&quot;,
    &quot;gulp-sourcemaps&quot;: &quot;^1.5.2&quot;,
    &quot;jade&quot;: &quot;^1.11.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，express 主要用来渲染 jade 模版引擎以及提供路由功能，同时起一个本地的服务器。有人说，渲染 jade 模版，我直接用 gulp 也可以啊。是的，gulp 是可以直接渲染 jade 模版，但是有一个问题，就是如果你本地只修改了一个 jade 文件时候，gulp 也需要把所有的 jade 文件全部渲染一遍，这明显会浪费很多时间。&lt;/p&gt;
&lt;p&gt;所以我引入了 express，只会按需渲染 jade，节省了很多时间。同时一个高效的开发环境必须有浏览器自动刷新，但是 express 没有这个功能。所以我引入了 browser-sync，用它来代理 express 启动的本地服务，然后来监测本地文件的变动，来做到浏览器自动刷新。&lt;/p&gt;
&lt;p&gt;有同学可能会问了，如果我改了 express 相应的 js 文件怎么办，或者 jade 文件编译出错了怎么办。因为这些时候 express 服务器都会 crash 掉，需要重启。所以我引入了 nodemon，来做 express 服务器的自动重启功能。&lt;/p&gt;
&lt;p&gt;在这个环境下面，我在开发中所能碰到的所有 jade，scss，js 文件的改动都会被检测到并作出相应的动作，而且速度还非常快。让我可以花更多时间在业务代码的开发上，而不是不断重启服务和刷新浏览器。&lt;/p&gt;
&lt;h1&gt;最后&lt;/h1&gt;
&lt;p&gt;这个东西被我放到 Github 上了，&lt;a href=&quot;https://github.com/kisnows/Express-Gulp-BrowserSync&quot;&gt;地址在这。&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;同时附上 gulpfile 全文，以供参考：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;use strict&quot;;

var gulp = require(&quot;gulp&quot;);
var browserSync = require(&quot;browser-sync&quot;);
var reload = browserSync.reload;
var sass = require(&quot;gulp-sass&quot;);
var prefix = require(&quot;gulp-autoprefixer&quot;);
var nodemon = require(&quot;gulp-nodemon&quot;);
var sourcemaps = require(&quot;gulp-sourcemaps&quot;);
var jade = require(&quot;gulp-jade&quot;);
var stylus = require(&quot;gulp-stylus&quot;);
var rename = require(&quot;gulp-rename&quot;);
var del = require(&quot;del&quot;);

//dev task start
//DONE can not compile the sass or less file
gulp.task(&quot;sass&quot;, function () {
  return gulp
    .src([&quot;./sass/personal.scss&quot;])
    .pipe(sourcemaps.init())
    .pipe(sass({ errLogToConsole: true }).on(&quot;error&quot;, sass.logError))
    .pipe(prefix(&quot;last 2 versions&quot;, &quot;&amp;gt; 1%&quot;, &quot;ie 8&quot;, &quot;Android 2&quot;))
    .pipe(sourcemaps.write())
    .pipe(gulp.dest(&quot;./public/css&quot;))
    .pipe(reload({ stream: true }));
});

gulp.task(&quot;browser-sync&quot;, [&quot;nodemon&quot;], function () {
  browserSync.init(null, {
    proxy: &quot;http://localhost:3000&quot;,
    files: [&quot;public/**/*.*&quot;, &quot;views/**/*.*&quot;, &quot;submodule/**/*.*&quot;],
    browser: &quot;google chrome&quot;,
    notify: false,
    port: 5000,
  });
});

gulp.task(&quot;movesub&quot;, function () {
  return gulp
    .src([&quot;./submodule/images/**/*.*&quot;], { base: &quot;./submodule&quot; })
    .pipe(gulp.dest(&quot;./public&quot;));
});

gulp.task(&quot;stylus&quot;, function () {
  return gulp
    .src(&quot;submodule/stylus/public.styl&quot;)
    .pipe(stylus())
    .pipe(
      rename({
        extname: &quot;.scss&quot;,
      }),
    )
    .pipe(gulp.dest(&quot;submodule/stylus/&quot;));
});

gulp.task(&quot;nodemon&quot;, function (cb) {
  del([&quot;./public/*.html&quot;]);

  var called = false;

  return nodemon({
    script: &quot;bin/www&quot;,
  }).on(&quot;start&quot;, function () {
    if (!called) {
      cb();
      called = true;
    }
  });
});
//dev task end

gulp.task(&quot;clean&quot;, function (cb) {
  del([&quot;./dist/*&quot;], cb);
});

gulp.task(&quot;copy&quot;, function () {
  return gulp
    .src(
      [
        &quot;public/css/**/*&quot;,
        &quot;public/images/**/*&quot;,
        &quot;public/js/**/*&quot;,
        &quot;public/pageScripts/**/*&quot;,
      ],
      { base: &quot;./public&quot; },
    )
    .pipe(gulp.dest(&quot;./dist&quot;));
});

//build task start
//DONE add build task
gulp.task(&quot;jade&quot;, function () {
  return gulp
    .src([
      &quot;views/**/*.jade&quot;,
      &quot;!views/layout/**/*.jade&quot;,
      &quot;!views/includes/**/*.jade&quot;,
    ])
    .pipe(jade({ pretty: true }))
    .pipe(gulp.dest(&quot;./dist&quot;));
});
//build task end

gulp.task(&quot;dist&quot;, [&quot;clean&quot;, &quot;copy&quot;, &quot;jade&quot;]);

gulp.task(&quot;default&quot;, [&quot;browser-sync&quot;, &quot;sass&quot;, &quot;movesub&quot;], function () {
  gulp.watch([&quot;sass/**/*.*&quot;, &quot;.submodule/stylus/**/*.*&quot;], [&quot;sass&quot;]);
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>如何高效的调试 vm 模版</title><link>https://ab.kisnows.com/zh-CN/2018/11/09/efficient-debug-the-vm-template/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2018/11/09/efficient-debug-the-vm-template/</guid><pubDate>Fri, 09 Nov 2018 02:00:03 GMT</pubDate><content:encoded>&lt;p&gt;由于合规要求，所以文章一些名词或者数据进行了脱敏处理。&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;加入阿里做的第一个项目，由于是很老的业务，前后端不分离，大部分逻辑都在后端 velocity 模版里面。&lt;/p&gt;
&lt;p&gt;作为一个前端，我们开发时在没有后端环境的支持情况下，想要调试 vm 模版，这真的是一件极其痛苦的事情。&lt;/p&gt;
&lt;p&gt;因为对于任何一个需求的，都需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在后端工程里面修改 vm 代码&lt;/li&gt;
&lt;li&gt;在 aone 上部署后端工程，这一步需要大概半个小时的时间&lt;/li&gt;
&lt;li&gt;部署成功后，刷新浏览器进行验证&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;也就是说，任何一行代码的修改成本都至少要半个小时。而且开发时严重依赖后端同学的支持，比如我要前端显示分支流程中 A 流程，那么需要后端同学配合吐出 A 流程的数据，然后等半个小时，我要前端显示分支流程中的 B 流程，又得半个小时。如果后端同学有事情支持不了，我前端开发得暂时搁置一下了。&lt;/p&gt;
&lt;p&gt;做了一个需求后，我发现这整个开发流程真的太痛苦了，而且效率也相当低下。一个我觉得两天就能搞定的需求，搞了整整一周，还通宵了一天才勉强赶上进度。&lt;/p&gt;
&lt;p&gt;而且觉得阿里的技术不如想象中的那么先进，但这也是个机会，于是就想有没有什么好的解决方案。&lt;/p&gt;
&lt;h2&gt;方案&lt;/h2&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;经过一通查找和沟通后，决定通过使用 velocityjs 在前端渲染 vm 模版，以此来解决本地开发时的 vm 调试功能。而 velocityjs 就是通过 nodejs 去解析 vm 模版， 从而得到 html 字符串。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/efficient-debug-the-vm-template/debugvm-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有了 velocityjs 以后就好办了，对于一个模块的调试，我们要做的就是找到 mock 数据和 vm 模版，然后渲染并返回给浏览器。&lt;/p&gt;
&lt;p&gt;最终方案如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/efficient-debug-the-vm-template/debugvm-2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;有了上面的方案，那我们实现起来也就很简单了，对于一个请求过来，处理的流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/efficient-debug-the-vm-template/debugvm-3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;本地 Node 服务会跑在 3000 端口上&lt;/li&gt;
&lt;li&gt;绑定 host：127.0.0.1 xxx.alibaba.com&lt;/li&gt;
&lt;li&gt;对于一个线上商品 url:&lt;code&gt;https://wholesaler.alibaba.com/xxx/xxx.html&lt;/code&gt;替换 wholesaler 为 xxx 得到:&lt;code&gt;http://xxx.alibaba.com:3000/xxx/xxx.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;本地 Node 服务拿到请求后，先去线上服务器找到这个商品的原有 html 以及用来异步渲染的模块数据 &lt;code&gt;PAGE_SCHEMA&lt;/code&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;PAGE_SCHEMA&lt;/code&gt; 是具体到国际站某网站下，后端在渲染 vm 的时候会把渲染这个 vm 模块的数据同时返回给前端，挂载在全局变量 window 下面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;找到本地要调试模块的 vm 模版&lt;/li&gt;
&lt;li&gt;用 Mock 数据或线上 &lt;code&gt;PAGE_SCHEMA&lt;/code&gt; 的模块数据渲染 vm 模版&lt;/li&gt;
&lt;li&gt;用渲染好的 vm 模版字符串替换原有 html 对应 DOM 节点，得到重建后的 html&lt;/li&gt;
&lt;li&gt;返回重建后的 html 给浏览器，达到本地调试的目的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终的结果：前面的环境配好以后，比如我们要调试 &lt;code&gt;module-xxx&lt;/code&gt; 这个模块 。 那么我们本地直接去修改后端工程里面的 vm 模版，然后刷新浏览器，就可以得到修改的反馈了。而且也只有这一个模块是处于调试状态，其他的模块都是线上原有的样子。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/efficient-debug-the-vm-template/debugvm-4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就把一次代码的改动成本从半小时缩减到了一分钟以内，而且不需要依赖后端环境，前端就可以直接把项目跑起来，也不用老是麻烦后端同学来协助我们改数据了。&lt;/p&gt;
&lt;p&gt;对开发体验的提升那是大大的，再也不会那么痛苦了。&lt;/p&gt;
</content:encoded></item><item><title>五分钟接入流式渲染</title><link>https://ab.kisnows.com/zh-CN/2021/03/23/egg-spart-a-way-to-stream-render-you-page/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2021/03/23/egg-spart-a-way-to-stream-render-you-page/</guid><pubDate>Tue, 23 Mar 2021 17:27:36 GMT</pubDate><content:encoded>&lt;h2&gt;流式渲染&lt;/h2&gt;
&lt;p&gt;首先, 简单介绍一下什么是流式渲染.
流式渲染是利用 &lt;code&gt;http1.1&lt;/code&gt; 的&lt;strong&gt;分块传输编码&lt;/strong&gt;的特性, 让服务端分块返回 &lt;code&gt;html&lt;/code&gt; , 浏览器可以在接收时逐步渲染页面, 这样有助于页面的首屏展现, 提升用户体验.&lt;/p&gt;
&lt;h3&gt;分块传输编码&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;分块传输编码（Chunked transfer encoding）是超文本传输协议（HTTP）中的一种数据传输机制，允许 HTTP 由网页服务器发送给客户端应用（ 通常是网页浏览器）的数据可以分成多个部分。分块传输编码只在 HTTP 协议 1.1 版本（HTTP/1.1）中提供。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;分块传输格式&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;如果一个 HTTP 消息（包括客户端发送的请求消息或服务器返回的应答消息）的 Transfer-Encoding 消息头的值为 chunked，那么，消息体由数量未定的块组成，并以最后一个大小为 0 的块为结束。
每一个非空的块都以该块包含数据的字节数（字节数以十六进制表示）开始，跟随一个 CRLF （回车及换行），然后是数据本身，最后块 CRLF 结束。在一些实现中，块大小和 CRLF 之间填充有白空格（0x20）。
最后一块是单行，由块大小（0），一些可选的填充白空格，以及 CRLF。最后一块不再包含任何数据，但是可以发送可选的尾部，包括消息头字段。
消息最后以 CRLF 结尾。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;一个栗子&lt;/h3&gt;
&lt;p&gt;店铺无线和 PC 端都做了流式渲染的改造, 首屏提升都在 200ms 左右, 而且业务数据也都有相应的提升 。&lt;/p&gt;
&lt;p&gt;店铺普通渲染和流式渲染对比时序图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/egg-spart-a-way-to-stream-render-you-page/2021-03-30-14-38-17.png&quot; alt=&quot;2021-03-30-14-38-17.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;工程化解决方法 Spark&lt;/h2&gt;
&lt;p&gt;流式渲染的好处很明显，但每个应用接起来还是有一定的成本，我就打算从店铺的改造思路中抽离一个可以快速接入流式渲染的方案。&lt;/p&gt;
&lt;h3&gt;方案预研&lt;/h3&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;流式渲染本身很是很简单的，就是后端分端吐 html 就好了。问题是如何高效的让业务开发无痛的接入，一开始想了几个方案，在周会上和组里过了一下：&lt;/p&gt;
&lt;h4&gt;方案 a&lt;/h4&gt;
&lt;p&gt;提供一个中间件或者插件, 在其中实现流式渲染的相关逻辑.&lt;/p&gt;
&lt;p&gt;调用方接入中间件, 并采用人工定义配置的方式, 来实现模板和数据的关联, 然后服务端(Node or Java) 根据配置来流式渲染页面.&lt;/p&gt;
&lt;p&gt;以 egg.js 举例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xxx Controller {
  test() {
    //  spark 为流式渲染中间件挂载在 ctx 上的方法
    const pipe = this.ctx.spark();
    this.ctx.body = pipe;
    //  设置分块渲染的配置
    pipe.setPageConfig([{
      tpl: &apos;admin/index-pipe/pipe-1.nj&apos;,
      getData: (ctx) =&amp;gt; Promise.resolve(mergerCommonContext({}, ctx)),
    }, {
      tpl: &apos;admin/index-pipe/pipe-2.nj&apos;,
      getData: ctx =&amp;gt; bindGetPageData(ctx),
    }]);
    // 调用流式渲染方法
    pipe.render()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现成本低&lt;/li&gt;
&lt;li&gt;业务侵入较小: 不需要动业务逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要对模板进行拆分&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方案 b&lt;/h4&gt;
&lt;p&gt;通过对模板进行约定方式来实现页面的分块, 然后在服务端渲染模板时, 根据模板分块的内容去插入数据, 实现分块渲染.&lt;/p&gt;
&lt;p&gt;举例:
模板&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- spark:start block0 --&amp;gt;
&amp;lt;!doctype html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Test&amp;lt;/title&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot; /&amp;gt;
    &amp;lt;meta name=&quot;renderer&quot; content=&quot;webkit&quot; /&amp;gt;
    &amp;lt;meta
      name=&quot;viewport&quot;
      content=&quot;width=device-width, initial-scale=1.0, user-scalable=no&quot;
    /&amp;gt;
    &amp;lt;link
      rel=&quot;shortcut icon&quot;
      href=&quot;//is.alicdn.com/favicon.ico&quot;
      type=&quot;image/x-icon&quot;
    /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;!-- spark:end block0 --&amp;gt;

    &amp;lt;!-- spark:start block1 --&amp;gt;
    &amp;lt;div style=&quot;width: 100%&quot;&amp;gt;
      {% for item in pageData %}
      &amp;lt;img
        src=&quot;http://design3d-zhangbei.oss-cn-zhangjiakou.aliyuncs.com/{{ item.thumbnailOssKey }}&quot;
        style=&quot;
          width: 200px;
          height: 200px;
          display: inline-block;
          vertical-align: top;
        &quot;
      /&amp;gt;
      {% endfor %}
    &amp;lt;/div&amp;gt;
    &amp;lt;!-- spark:end block1 --&amp;gt;

    &amp;lt;!-- spark:start block2 --&amp;gt;
    &amp;lt;div style=&quot;width: 100%&quot;&amp;gt;
      {% for item in pageData %}
      &amp;lt;img
        src=&quot;http://design3d-zhangbei.oss-cn-zhangjiakou.aliyuncs.com/{{ item.thumbnailOssKey }}&quot;
        style=&quot;
          width: 200px;
          height: 200px;
          display: inline-block;
          vertical-align: top;
        &quot;
      /&amp;gt;
      {% endfor %}
    &amp;lt;/div&amp;gt;
    &amp;lt;!-- spark:end block2 --&amp;gt;

    &amp;lt;!-- spark:start block3 --&amp;gt;

    &amp;lt;script&amp;gt;
      window._PAGE_DATA_ = {{ pageData | default({}) | dump | replace(r/&amp;lt;\/script/ig, &apos;&amp;lt;\\\/script&apos;) | safe }};
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&amp;lt;!-- spark:start block3 --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务端调用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xxx Controller {
  test() {
    //  spark 为流式渲染中间件挂载在 ctx 上的方法
    const pipe = this.ctx.spark();
    this.ctx.body = pipe;
    //  设置分块渲染的配置
    pipe.renderBlock( &apos;block0&apos;, {xxx})
    pipe.renderBlock( &apos;block1&apos;, {xxx})
    pipe.renderBlock( &apos;block2&apos;, {xxx})
    pipe.renderBlock( &apos;block3&apos;, {xxx})
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;bigPipe&lt;/h4&gt;
&lt;p&gt;流式渲染还是要依赖服务端的返回顺序, 当有多个页面块要取数渲染时需要串行调用返回.
如果在流式渲染的基础上再往前进一步就是 bigPipe 了, 不需要依赖后端的返回顺序. 哪一块取数完毕渲染好了, 就把哪一块 html 返回给浏览器渲染.&lt;/p&gt;
&lt;p&gt;原理非常简单, 服务端首先返回页面的 layout . 然后每个页面 block 渲染好后, 以 script 标签的形式返回给浏览器, 比如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  spark.render(&quot;#id&quot;, &quot;&amp;lt;div&amp;gt;page1&amp;lt;/div&amp;gt;&quot;);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过和前端约定好的方法, 来通过 js 往页面的特定部位插入 html 实现页面的渐进式渲染.
优点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以不按照 html 来更快返回内容供浏览器渲染
缺点:&lt;/li&gt;
&lt;li&gt;js 插入的 html 中的内联 script 标签中的 js 不会执行. 比如下面这个 onclick 事件就不会触发:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  spark.render(&apos;#id&apos;, &apos;&amp;lt;div&amp;gt;page1&amp;lt;script&amp;gt;console.log(123)&amp;lt;/script&amp;gt;&amp;lt;/div&amp;gt;&apos;);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;需要把 script 标签的内容单独拎出来 &lt;code&gt;eval&lt;/code&gt; 一下才能执行.&lt;/li&gt;
&lt;li&gt;需要侵入前端代码: 由于页面乱序返回, 所以原本依赖 script 插入顺序来保证运行的先后机制就可能失效了, 需要通过事件或其他方式来通知页面的渲染情况来保证 js 运行顺序, 增加复杂度&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;结论&lt;/h4&gt;
&lt;p&gt;和同事过下来，得到的建议主要为一下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虽然在 AB 两个方案中，大家更加倾向于 B ，因为不用分割多个模板，但依然对需要改动到模板本身有抵触&lt;/li&gt;
&lt;li&gt;对于有些业务是存在多个业务场景用一个模板作为 layout 的情况，如果需要不同的页面分割情况，那么方案 B 存在问题&lt;/li&gt;
&lt;li&gt;对 bigPiep 的诉求不强，因为提升有限但成本相对提升很多&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这些意见的基础上，对原有的方案作了一下改进：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模板不需要关心是否被流式渲染页面用了&lt;/li&gt;
&lt;li&gt;去除后续对 bigPipe 的支持&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终方案如下：
&lt;img src=&quot;/imgs/egg-spart-a-way-to-stream-render-you-page/2021-03-31-16-46-04.png&quot; alt=&quot;2021-03-31-16-46-04.png&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先调用插件提供的 SparkRender 方法，传入 regList 和 dataList，立即返回一个 Readable Stream 给浏览器&lt;/li&gt;
&lt;li&gt;根据 regList 对 html 进行切分，然后和 dataList 组成一个待渲染的 blockQueue&lt;/li&gt;
&lt;li&gt;根据 blockQueue 的顺序，串行调用 RenderBlock 方法，并将渲染的结果 push 到 Readable Stream 中&lt;/li&gt;
&lt;li&gt;当 blockQueue 渲染完毕时，关闭流，整个流式渲染进程结束&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;支持情况&lt;/h3&gt;
&lt;p&gt;目前仅支持基于 egg.js 的 node 应用。&lt;/p&gt;
&lt;h3&gt;适用场景&lt;/h3&gt;
&lt;p&gt;所有对性能有追求的需要后端进行模板渲染的场景。&lt;/p&gt;
&lt;h3&gt;如何使用&lt;/h3&gt;
&lt;p&gt;对于一个基于 egg.js 的 node 应用，只需要两步即可完成接入：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在应用中引入并打开 &lt;a href=&quot;https://github.com/eggjs/egg-spark&quot;&gt;&lt;code&gt;egg-spark&lt;/code&gt;&lt;/a&gt; 插件：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;tnpm install egg-spark
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;在 `config/plugin.js` 中开启插件
exports.spark = {
  enable: true,
  package: &apos;egg-spark&apos;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;对要开启流式渲染页面的 controller 做一些微小的修改：即将原有的 render 方法替换为 spark 插件提供的 render ：&lt;code&gt;spark.render(tpl, regList, dataList):void&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;入参：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tplPath&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;要渲染的模板地址，和框架提供的参数含义相同，不用修改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;regList&lt;/td&gt;
&lt;td&gt;reg[]&lt;/td&gt;
&lt;td&gt;用来切分页面的正则列表，根据 regList 的长度将页面切分为 regList.length+1 份。每次切分会以当前正则匹配到第一个字符作为基准&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dataList&lt;/td&gt;
&lt;td&gt;object[] / function[]&lt;/td&gt;
&lt;td&gt;根据 regList 切分的页面块，来传入对应的渲染模板需要的数据。可以直接传入数据的 object ，也可以传入同步或者异步的数据方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//原代码：
this.ctx.render(&quot;admin/index.nj&quot;, mergerCommonContext({ pageData }, this.ctx));
//修改后代码：
this.ctx.spark.render(&quot;admin/index.nj&quot;, {
  // regList 是一个用来切分页面的正则列表，根据 regList 的长度将页面切分为 regList.length+1 份
  regList: [/id=&quot;root&quot;/],
  // dataList：根据 regList 切分的页面块，来传入对应的渲染模板需要的数据。可以传入数据 object ，也可以传入同步或者异步的数据方法
  dataList: [
    mergerCommonContext({}, this.ctx),
    async () =&amp;gt; getPageData.call(this),
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样你的页面就已经支持流式渲染了，是的，就这么简单。&lt;/p&gt;
&lt;h3&gt;最佳实践&lt;/h3&gt;
&lt;p&gt;一般情况下，一个页面只需要拆分为两块就好：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先返回包含页面 &lt;code&gt;css&lt;/code&gt;  标签的 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;  部分，这样浏览器就可以开始加载并解析 &lt;code&gt;css&lt;/code&gt;  资源&lt;/li&gt;
&lt;li&gt;与此同时，后端开始进行取数等其他耗时较长的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;拿刚上线的定制页面举例，实际的看一下如何通过 Spark 接入流式渲染。这是定制管理后台的模板 &lt;code&gt;admin/index.nj&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;script&amp;gt;
      window._customizePerfTimeTTfb = Date.now();
    &amp;lt;/script&amp;gt;
    ...
    &amp;lt;link
      rel=&quot;shortcut icon&quot;
      href=&quot;//is.alicdn.com/favicon.ico&quot;
      type=&quot;image/x-icon&quot;
    /&amp;gt;
    {% block cssContents %}
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;{{cssFile}}&quot; /&amp;gt;
    {% endblock %}
    &amp;lt;script&amp;gt;
      window._customizePerfTimeCss = Date.now();
    &amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body data-spm=&quot;customize&quot; class=&quot;{{ ctx.session.locale }}&quot;&amp;gt;
    {% block header %}
    &amp;lt;div class=&quot;page-header&quot;&amp;gt;{% using &quot;mm-sc-new-header-v5&quot; %}&amp;lt;/div&amp;gt;
    {% endblock %}
    &amp;lt;div class=&quot;page-main&quot;&amp;gt;
      &amp;lt;div class=&quot;content&quot;&amp;gt;
        {% block content %}
        &amp;lt;div class=&quot;page-root&quot; id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;script&amp;gt;
          window._PAGE_DATA_ = {{ pageData | default({}) | dump | replace(r/&amp;lt;\/script/ig, &apos;&amp;lt;\\\/script&apos;) | safe }};
          {% if pageConfig %}
          window._PAGE_CONFIG_ = {{ pageConfig | default({}) | dump | safe }};
          {% endif %}
        &amp;lt;/script&amp;gt;
        {% endblock %}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先返回 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;  部分的内容，然后返回 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;  ，其中 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;  只依赖静态资源版本，几乎不耗时，而 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;  中依赖一些比较耗时的取数逻辑。要做的就是修改 &lt;code&gt;admin&lt;/code&gt;  页面的 &lt;code&gt;controller&lt;/code&gt;  ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = app =&amp;gt; class AdminController extends app.Controller {
  async index() {
    // 首先对用户进行鉴权
    const previliges = await this.service.userInfo.checkPrevilige();
		// 调用插件提供的 rende 方法
    this.ctx.spark.render(&apos;admin/index.nj&apos;, {
      // 要先返回 head 部分，所以正则需要匹配 head 紧跟着的第一个元素，也就是 &amp;lt;body
      regList: [/&amp;lt;body data-spm=&quot;customize&quot;/],
      dataList: [
        // 由于第一部分需要的数据是现成的，直接挂载在 ctx 上，所以直接传入就好了
        this.ctx,
        // 第二部分是复杂的取数逻辑，将取数的逻辑封装成一个异步方法传进来就好了
        async () =&amp;gt; getPageData.call(this),
      ],
    });

    async function getPageData() {
      ...耗时逻辑
      const pageData = {
        ...
      };
      return mergerCommonContext({ pageData }, this.ctx);
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样浏览器请求 &lt;code&gt;admin&lt;/code&gt;  页面时，就会按照给定的正则对页面进行切分并流式返回了。&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一定要先对请求进行鉴权，然后再调用 render 方法，因为一旦页面开始返回，再进行鉴权动作就已经晚了；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;一开始对做这件事情没有很大的热情，但做出来之后只花了半天就在两个业务中成功接入落地，而且获得了页面性能的上升，结果还是很令我满意的。&lt;/p&gt;
</content:encoded></item><item><title>硬盘文件整理</title><link>https://ab.kisnows.com/zh-CN/2015/01/30/file-management/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/30/file-management/</guid><pubDate>Fri, 30 Jan 2015 00:25:21 GMT</pubDate><content:encoded>&lt;p&gt;看着自己电脑里的文件越来越多，文件的存放也变越来越混乱，如何整理文件成了一个大问题，是该花点时间好好整理整理了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h1&gt;磁盘分区&lt;/h1&gt;
&lt;p&gt;我的电脑是两个硬盘，一个 128G 的固态和 650G 的机械硬盘，分别设置为 C 盘和 D 盘两个盘。其中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C 盘：安装系统和所有的应用软件。&lt;/li&gt;
&lt;li&gt;D 盘：用来存放数据，包括文档、音乐、电影等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么不设置 E、F 等多个分区呢，因为多分区会造成空间的浪费。举个例子：假设有一个 600G 的硬盘，分为 C、D、E 三个分区，其中 C 盘 100G，D 盘 200G，E 盘 300G。然后 C 盘放了 95G，D 盘放了 198G，E 盘放了 287G。现在有一个大小为 20G 的文件，你说你放哪？C、D、还是 E？哪里都不放下，可是明明 600G 的空间，我现在只用了 580G，明明还有 20G 的空余空间。可是就是没地方放。这就是多分区造成的空间浪费，所以为了空间最大化利用，就是分最少的区。理想情况下，只存在一个 C 盘就够了。可是有的时候系统出问题需要格式化 C 盘，假如没有备份会造成数据丢失。所以退而求其次，分为两个盘是一个折中的办法。&lt;/p&gt;
&lt;h1&gt;文件管理&lt;/h1&gt;
&lt;p&gt;分区搞定了，文件该怎么存放呢。是通过文件类型来存放，还是通过文件功能来存放呢？&lt;/p&gt;
&lt;h3&gt;通过文件类型来存放&lt;/h3&gt;
&lt;p&gt;将所有的音乐文件放在一个文件夹，所有的电影放在一个文件夹，所有的文档放在一个文件夹……&lt;/p&gt;
&lt;p&gt;乍看起来挺好的，但是有时会有冲突。比如所有的文档放在一个文件夹，工作、学习、娱乐文档都放在一块，要找一个文件的时候还是会麻烦。有人会说，我用搜索啊。用搜索是个好办法，但是需要你知道你要找的文件的名字，除非你有非常良好的文件命名习惯，比如&apos;a 项目-b 分支-c 文件&apos;这样，那用搜索是一个非常好的办法（比如 everything 的搜索速度就神快）。
所以如果没有良好的命名习惯，那么通过文件类型存放是行不通的。&lt;/p&gt;
&lt;h3&gt;通过文件功能来存放&lt;/h3&gt;
&lt;p&gt;首先将文件类型分为四大类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作&lt;/li&gt;
&lt;li&gt;生活&lt;/li&gt;
&lt;li&gt;学习&lt;/li&gt;
&lt;li&gt;备份&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后在这个分类下面，在通过类型进行划分。所有和工作有关的划分到工作类中，和工作无关的其他学习划分到学习中，其他的音乐、电影、图片等都属于生活。
然后在各个大类下面，再通过各自的功能进行进一步的划分，类似一种递归的思想。
然后将重要的东西定期更新到备份中，方便文件的复制和拷贝。&lt;/p&gt;
&lt;h1&gt;我的分类&lt;/h1&gt;
&lt;p&gt;我的最终分类如下图：
&lt;img src=&quot;/imgs/%E6%96%87%E4%BB%B6%E6%95%B4%E7%90%86.png&quot; alt=&quot;文件分类&quot; /&gt;
&lt;img src=&quot;/imgs/2015-01-29_173642.png&quot; alt=&quot;磁盘使用情况&quot; /&gt;
我这 C 盘是不是用的有点太狠了。*^_^*&lt;/p&gt;
</content:encoded></item><item><title>Canvas 基础</title><link>https://ab.kisnows.com/zh-CN/2015/10/20/first-contact-of-canvas/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/10/20/first-contact-of-canvas/</guid><pubDate>Tue, 20 Oct 2015 06:41:38 GMT</pubDate><content:encoded>&lt;p&gt;首先引用 &lt;code&gt;MDN&lt;/code&gt; 上对于 &lt;code&gt;canvas&lt;/code&gt; 的定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; 是 HTML5 新增的元素，可使用 JavaScript 脚本来绘制图形。例如：画图，合成照片，创建动画甚至实时视频处理与渲染。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说这是浏览器提供给我们的一个画布和丰富的接口，可以用来制作各种复杂的效果。
&lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; 元素创造了一个固定大小的画布，它公开了一个或多个渲染上下文。其可以用来绘制和处理被现实的页面。
&lt;img src=&quot;https://mdn.mozillademos.org/files/224/Canvas_default_grid.png&quot; alt=&quot;Canvas坐标&quot; /&gt;
这是 canvas 2d 的坐标，可以看到左上角为原点，横轴为 x 轴，纵轴为 y 轴。我们所有的 canvas2d 的功能实现都是在这个坐标系中。&lt;/p&gt;
&lt;h1&gt;基本用法&lt;/h1&gt;
&lt;p&gt;本文只简单介绍一下矩形、线段和圆弧的画法。&lt;/p&gt;
&lt;h2&gt;矩形&lt;/h2&gt;
&lt;p&gt;通过一个简单的例子来了解一下基本的 API：&lt;/p&gt;
&lt;h3&gt;HTML:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;canvas id=&quot;canvas&quot; style=&quot;height:100%&quot;&amp;gt;
  当前浏览器不支持Canvas，请更换浏览器后再试
&amp;lt;/canvas&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JavaScript:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var canvas = document.getElementById(&quot;canvas&quot;);
var ctx = canvas.getContext(&quot;2d&quot;);
var WINDOW_WIDTH = window.screen.availWidth;
var WINDOW_HEIGHT = window.screen.availHeight;
ctx.clearRect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
ctx.beginPath();
ctx.fillStyle = &quot;#000&quot;;
ctx.fillRect(10, 10, 55, 50);
ctx.fillStyle = &quot;rgba(0,0,200,0.5)&quot;;
ctx.fillRect(30, 30, 55, 50);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;页面上会如下显示：&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;300&quot; src=&quot;//jsfiddle.net/kisnows/v5Lhnf8k/embedded/result/&quot; allowfullscreen=&quot;allowfullscreen&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;首先获取到 canvas 元素，并通过 getContext(&apos;2d&apos;) 获取到它的渲染上下文。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;clearRect(x, y, width, height)&lt;/h3&gt;
&lt;p&gt;清空指定区域，如果需要做动画的话，每次重新渲染前都需要调用这个函数来清除前一帧的内容。&lt;/p&gt;
&lt;h3&gt;beginPath()&lt;/h3&gt;
&lt;p&gt;新建一条路径&lt;/p&gt;
&lt;h3&gt;fillStyle&lt;/h3&gt;
&lt;p&gt;用来设置要填充的颜色&lt;/p&gt;
&lt;h3&gt;ctx.fillRect(10, 10, 55, 50);&lt;/h3&gt;
&lt;p&gt;以 x 轴等于 10，y 轴等于 10 为起点，绘制一个宽 55 高 50 的矩形。&lt;/p&gt;
&lt;h3&gt;strokeRect(x,y,width,height)&lt;/h3&gt;
&lt;p&gt;跟 fillRect 不同的地方在于，strokeRect 会绘制一个矩形的边框，如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;300&quot; src=&quot;//jsfiddle.net/kisnows/87jtz89q/embedded/result/&quot; allowfullscreen=&quot;allowfullscreen&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;线段&lt;/h2&gt;
&lt;p&gt;绘制一个三角形&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ctx.beginPath();
ctx.moveTo(75, 50);
ctx.lineTo(100, 75);
ctx.lineTo(100, 25);
ctx.closePath();
ctx.stroke();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;300&quot; src=&quot;//jsfiddle.net/kisnows/6n0emmf3/embedded/result/&quot; allowfullscreen=&quot;allowfullscreen&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h3&gt;moveTo(x,y)&lt;/h3&gt;
&lt;p&gt;定义一个起点，从这个点开始绘制&lt;/p&gt;
&lt;h3&gt;lineTo(x,y)&lt;/h3&gt;
&lt;p&gt;绘制一条从当前位置到指定 x 以及 y 位置的直线&lt;/p&gt;
&lt;h3&gt;closePath()&lt;/h3&gt;
&lt;p&gt;闭合当前路径，也可以用 lineTo 到起点来代替&lt;/p&gt;
&lt;h3&gt;storke()&lt;/h3&gt;
&lt;p&gt;根据已有路径的来绘制线段，也就是说如果不执行 stroke(),那么画布上什么都没有。&lt;/p&gt;
&lt;h2&gt;圆弧&lt;/h2&gt;
&lt;h3&gt;arc(x, y, radius, startAngle, endAngle, anticlockwise)&lt;/h3&gt;
&lt;p&gt;该方法有五个参数： x,y 为绘制圆弧所在圆上的圆心坐标。radius 为半径。startAngle 以及 endAngle 参数用弧度定义了开始以及结束的弧度。这些都是以 x 轴为基准。参数 anticlockwise 为一个布尔值。为 true 时，是逆时针方向，否则顺时针方向。
看个直观的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ctx.beginPath();
ctx.arc(75, 75, 50, 0, Math.PI * 2, true);
ctx.moveTo(110, 75);
ctx.arc(75, 75, 35, 0, Math.PI, false);
ctx.moveTo(65, 65);
ctx.arc(60, 65, 5, 0, Math.PI * 2, true);
ctx.moveTo(95, 65);
ctx.arc(90, 65, 5, 0, Math.PI * 2, true);
ctx.stroke();
ctx.clearRect(0, 0, 500, 500);
for (var i = 0; i &amp;lt; 4; i++) {
  for (var j = 0; j &amp;lt; 3; j++) {
    ctx.beginPath();
    var x = 20 + j * 40;
    var y = 20 + i * 40;
    var radisu = 20;
    var startAngle = 0;
    var endAngle = Math.PI + (Math.PI * j) / 2;
    var anticlockwise = i % 2 == 0 ? true : false;
    ctx.arc(x, y, radisu, startAngle, endAngle, anticlockwise);
    if (i &amp;gt; 1) {
      ctx.fill();
    } else {
      ctx.stroke();
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;300&quot; src=&quot;//jsfiddle.net/kisnows/z6ywoj8g/embedded/result/&quot; allowfullscreen=&quot;allowfullscreen&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
通过这个例子可以看清楚的看到 fill 和 stroke 这个两个函数的区别，前者渲染边框，后者填充整个指定区域。同时可以区分，anticlockwise 分别 true 和 false 时渲染的机制，前者为逆时针，后者为顺时针。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;仅仅通过这三个基础的用法，就可以制作出很多漂亮的动画了。我这里做了一个简单运动的小球，同时做了边界判定。&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;400&quot; src=&quot;//jsfiddle.net/kisnows/jkLgatsm/embedded/result/&quot; allowfullscreen=&quot;allowfullscreen&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;参考文章： &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API&quot;&gt;MDN-Canvas&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>阿里一年工作总结</title><link>https://ab.kisnows.com/zh-CN/2019/06/05/first-year-summary-for-alibaba-work-experience/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2019/06/05/first-year-summary-for-alibaba-work-experience/</guid><pubDate>Wed, 05 Jun 2019 06:25:49 GMT</pubDate><content:encoded>&lt;p&gt;三年前写了一篇&lt;a href=&quot;https://blog.kisnows.com/2017/08/11/first-year-summary-for-netease-work-experience/&quot;&gt;网易一年工作工作总结&lt;/a&gt;，如今换工作到了阿里，到今天也算满一年了，总结一下这一年的工作。&lt;/p&gt;
&lt;p&gt;本想如法炮制，再写一篇总结，谁知从 6.4 号刚好满一周年到今天，已经拖了一个月了。&lt;/p&gt;
&lt;p&gt;虽然已经拖了一个多月了，但真的没空写。&lt;/p&gt;
&lt;p&gt;估计这篇文章后面也不会有时间写了，就这样吧。&lt;/p&gt;
</content:encoded></item><item><title>网易一年工作总结</title><link>https://ab.kisnows.com/zh-CN/2017/08/11/first-year-summary-for-netease-work-experience/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/08/11/first-year-summary-for-netease-work-experience/</guid><pubDate>Fri, 11 Aug 2017 23:46:49 GMT</pubDate><content:encoded>&lt;h1&gt;做了哪些事情&lt;/h1&gt;
&lt;p&gt;说是一年，其实也一年多了，不过这倒不重要。主要看看做了哪些事吧：&lt;/p&gt;
&lt;h2&gt;1. 改造发布流程&lt;/h2&gt;
&lt;p&gt;刚来公司的时候，白领贷的前端是基于 Riot 框架开发的系统。语言层面，使用 ES6 和 Less 开发，发布到线上是需要一个构建打包和编译的过程，不能直接部署到线上服务器。&lt;/p&gt;
&lt;p&gt;所以项目的开发流程就是，功能开发，提交代码，本地打包，打包后的代码跟源代码一起提交测试，而后合并代码到 master 然后再在本地打包，然后上线。&lt;/p&gt;
&lt;p&gt;这个流程非常的不合理，因为源代码和编译打包后的代码放到一块管理，本身就不合理。经常会出现，你一打包测试，我这边代码推上去就一堆冲突。所以我就想怎么来改进这个流程。&lt;/p&gt;
&lt;h3&gt;改进方案一&lt;/h3&gt;
&lt;p&gt;网易内部是有一个 omad 系统（自动部署平台）的，想着说看能不能借助这个平台干点事情。因为初来乍到，对这个部署平台不是很熟悉，就问了和这个平台接触最多的测试同学，测试同学告诉我说 omad 没有提供任何对外的接口，所以我想的可能没法实现。&lt;/p&gt;
&lt;p&gt;这样就尴尬了，没法做线上打包发布，那么打包这个过程就必然得放在本地来做。所以我就搞了一套现在看来很蠢的方案。那就是，用两个仓库来分别管理源代码和打包后的代码。&lt;/p&gt;
&lt;p&gt;那么，如果需要发布的话，就需要在源代码仓库先进行打包编译，然后把打包编译的结果复制到管理要发布的代码的仓库。&lt;/p&gt;
&lt;p&gt;一张示意图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/first-year-summary-for-netease-work-experience/release1.png&quot; alt=&quot;改进方案一&quot; /&gt;&lt;/p&gt;
&lt;p&gt;很明显，上面这种方式也只是临时解决方案，需要两个仓库来管理一份代码。每次提交代码都需要额外执行一个 run build 命令。&lt;/p&gt;
&lt;h3&gt;改进方案二：&lt;/h3&gt;
&lt;p&gt;由于上面的方案太蠢了，所以有了改进方案二，也是我们现在一直在用的方案：那就是直接线上打包。具体如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;本地代码开发 -&amp;gt; 提交 -&amp;gt; 推送到远端 -&amp;gt; omad 部署 -&amp;gt; omad 专用打包机执行一段脚本来进行打包编译操作 -&amp;gt; 把打包编译后的结果拷贝到目标机器。&lt;/p&gt;
&lt;p&gt;一张示意图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/first-year-summary-for-netease-work-experience/release2.png&quot; alt=&quot;改进方案二&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其实，本来第一次改造就应该改成这样的。但是因为一开始对内部的部署平台 omad 并不熟悉，所以不知道 omad 部署到目标机器前还可以执行一段脚本的。以至于有了第一种非常蠢的解决方案。&lt;/p&gt;
&lt;p&gt;现在这套方案就比较完善了， 我们只关注源代码就可以了，发布的时候又 omad 去做构建和打包部署的事情。非常舒心，这套方案也一直用到了现在。&lt;/p&gt;
&lt;h2&gt;2. 搭建了一套完善的开发环境&lt;/h2&gt;
&lt;p&gt;原有的白领贷开发环境是基于 express 和 webpack 配合搭建起来的，但是功能比较简陋，只支持开发和打包两个命令，而且打包的配置文件是完全服务于多页面的工程。&lt;/p&gt;
&lt;p&gt;后来我们开发来钱系统的时候，我就在原有的基础上重新搭建了一套完善的开发环境。从开发、联调、测试、打包到上线，该有的功能都有了。放一张图，具体可以 &lt;a href=&quot;https://github.com/kisnows/spart&quot;&gt;看这里&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/first-year-summary-for-netease-work-experience/environment.png&quot; alt=&quot;开发环境&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3. 主导并推动了移动端组件 ne-rc 的开发&lt;/h2&gt;
&lt;p&gt;再开发来钱的时候，积累了一套 react 的基础组件，但是跟业务耦合度比较高。考虑到以后可能还要做其他项目，就和一伙伴一起商量着把基础组件从业务中抽离出来，作为一个第三方组件库在接入到项目中。&lt;/p&gt;
&lt;p&gt;于是就有了: &lt;strong&gt;&lt;a href=&quot;https://github.com/NE-LOAN-FED/NE-Component&quot;&gt;ne-rc&lt;/a&gt;&lt;/strong&gt;,我们的移动端组件库，索然还在为 1.0 版本的发布继续努力，但已经成功接入了三个内部的项目。&lt;/p&gt;
&lt;p&gt;效果显著，大大降低了重复开发成本，同时也降低了未来维护的成本。由此可见，一个稳定发展的团队，还是需要一套自己的内部组件库的。&lt;/p&gt;
&lt;h2&gt;4. 团队工作改进&lt;/h2&gt;
&lt;p&gt;由于原有业务线前端负责人在我入职后不久就离职了， 前端技术部派我临时挑大梁。我深知自己经验不足，但也知道这是一个锻炼的机会，这里非常感谢我的老大信任我。&lt;/p&gt;
&lt;p&gt;所以我非常努力，和业务线团队的小伙伴就如何提高我们的团队技术实力和业务支持能力讨论了很多，最后做了一下事情:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发框架优化&lt;/li&gt;
&lt;li&gt;遗留项目改造&lt;/li&gt;
&lt;li&gt;项目规范&lt;/li&gt;
&lt;li&gt;技术规划&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;开发框架优化&lt;/h3&gt;
&lt;p&gt;最终的成果就是上面说的搭建了一套完整的开发环境。并不断进行优化。从一开始白领贷项目开发时每次 rebuild 需要 30s , 打包上线需要 4~5 min 到后来去花项目 rebuild 600ms, 打包只需要 1min .这中间既有针对 webpack 的优化，也有项目开放交付方式的改变。&lt;/p&gt;
&lt;p&gt;在这些优化的过程中，积累了很多经验。&lt;/p&gt;
&lt;h3&gt;遗留项目改造&lt;/h3&gt;
&lt;p&gt;有几个遗留项目，虽然整体处于维护状态，但偶尔还是有很多运营需求。而原有的开发方式和发布方式都比较麻烦，我们也尽可能在不需要多的重构的基础上，进行了逐步升级。主要是构建和发布上。
改写原有的 webpack 配置文件方式，并从本地打包发布升级到了线上打包发布。减轻了这些项目的维护成本。&lt;/p&gt;
&lt;h3&gt;项目规范&lt;/h3&gt;
&lt;p&gt;业务线上人员来越多，在需要合作的情况下，规范是必不可少。我们从项目文档、代码风格检查、Git 分支管理、代码 review 等各个方面制订了完善又尽可能不繁琐的规范。&lt;/p&gt;
&lt;h3&gt;技术规划&lt;/h3&gt;
&lt;p&gt;我们组的项目大多应用 react ，所以需要持续关注 react 生态群的技术动向。但同时也关注其他技术和解决方案，包括 Nodejs , Angular 等。前端技术部也让我们多看看 Vue . 然而到现在我都没看过 Vue ，哈哈，可能是提不起兴趣吧。&lt;/p&gt;
&lt;h2&gt;5. 业务支持&lt;/h2&gt;
&lt;p&gt;虽然我们前端技术部属于职能部门，但支持业务仍然是我们工作的最主要构成部分，毕竟公司要赚钱。除了一些小的以及一些后台项目，我主要参与维护和开发一下项目：&lt;/p&gt;
&lt;p&gt;a. 白领贷项目维护
b. 来钱项目开发并上线
c. 去花项目开发并上线&lt;/p&gt;
&lt;p&gt;去花最近就要上线了，我们已经开始内测了。这是一个提供分期的项目，目前只接入了考拉，马上也要对外了，不久大家就在考拉上购物时就可以有分期的支付选项了。&lt;/p&gt;
&lt;p&gt;看到项目上线，还是很高兴的 😄。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;时间还是蛮快的， 都来网易一年多了。&lt;/p&gt;
&lt;p&gt;这段时间成长了许多，无论时工作技能、沟通技能还是工作流程和规范，都让我学会了很多东西。&lt;/p&gt;
&lt;p&gt;也认识了很多优秀的同事，能跟这些优秀的同事一起做事情，也是一件很开心的事情。也许以后不会一直在网易，但在网易的这段经历还是很值得的。&lt;/p&gt;
</content:encoded></item><item><title>前端开发标准化 - 最佳实践 - 从开发到上线</title><link>https://ab.kisnows.com/zh-CN/2023/09/15/frontend-development-standardization-best-practices/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2023/09/15/frontend-development-standardization-best-practices/</guid><pubDate>Fri, 15 Sep 2023 07:49:23 GMT</pubDate><content:encoded>&lt;p&gt;本文原发于公司内部 ATA 平台，经过数据脱敏后，重新整理成为了这篇文章。所以文章内会有很多内部系统的介绍和链接，这些链接以及平台无法在非公司内网打开，但不影响文章整体的阅读和理解。&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在过去的一年中，我们团队的流量业务经历了一系列的重构和大幅度性能优化。最近，在&lt;strong&gt;前端开发标准化平台&lt;/strong&gt;的支持下，我们也进行了一轮标准化治理。现在，从各个角度来看，整个应用相比之前都有了不小的提升。&lt;/p&gt;
&lt;p&gt;抛砖引玉，我把其中的一些思考和选择总结成了这篇文章，给大家做参考。&lt;/p&gt;
&lt;h2&gt;技术选型&lt;/h2&gt;
&lt;h3&gt;开发框架选择&lt;/h3&gt;
&lt;h4&gt;为什么选择 React&lt;/h4&gt;
&lt;p&gt;这个没有什么需要讨论的，内部的所有基建以及大环境决定了必然是 React。&lt;/p&gt;
&lt;h4&gt;为什么是 React18&lt;/h4&gt;
&lt;p&gt;原本项目是 React16，React18 相较于 16 主要的变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并发模式 - React18 默认开启了并发模式, 简单讲就是组件渲染从同步不可中断改为了异步可终端，从而提升页面的响应效率；
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;With this capability, React can prepare new screens in the background without blocking the main thread. This means the UI can respond immediately to user input even if it’s in the middle of a large rendering task, creating a fluid user experience.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自动批处理 - 允许将多个状态更新批处理到一次重新渲染中，通过减少重新渲染的次数来提高性能；&lt;/li&gt;
&lt;li&gt;详细文档参考 React 官网：&lt;a href=&quot;https://react.dev/blog/2022/03/29/react-v18&quot;&gt;React v18.0&lt;/a&gt; 或者内网 ATA 文章&lt;a href=&quot;https://ata.atatech.org/articles/11000262147?spm=ata.25287382.0.0.4a9c75365NLJCR&quot;&gt;React18 新在哪 - ATA (atatech.org)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;如何升级&lt;/h4&gt;
&lt;p&gt;默认情况下从 React16 升级到 React18 是无损升级的，但是如果需要用 React18 的新特性 &lt;code&gt;并发模式&lt;/code&gt; 以及 &lt;code&gt;自动批处理&lt;/code&gt; 则需要稍微一点改动：&lt;/p&gt;
&lt;p&gt;从&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { useEffect, lazy } from &apos;react&apos;;
import ReactDOM from &apos;react-dom&apos;;

ReactDOM.render(
	&amp;lt;ShowRoom {...(window.globalUtils.getPageData() as SearchResult.PageData)} /&amp;gt;,
	document.getElementById(&apos;root&apos;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { useEffect } from &apos;react&apos;;
import { createRoot } from &apos;react-dom/client&apos;;

const container = createRoot(document.querySelector(&apos;#root&apos;)!);
container.render(
	&amp;lt;ShowRoom {...(window.globalUtils.getPageData() as SearchResult.PageData)} /&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样会开启 React18 的 &lt;code&gt;并发模式&lt;/code&gt; 以及 &lt;code&gt;自动批处理&lt;/code&gt; ，这里可能会导致一点问题：React18 以前只有在事件处理中的 setState 会批量处理，但现在基本上都是批量处理了。
需要测试一下，不过一般情况下都会有问题，建议在开发环境使用 &lt;code&gt;StrictMode&lt;/code&gt; 模式，更容易暴露问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  const RootComponent = process.env.NODE_ENV === &apos;development&apos; ? React.StrictMode : React.Fragment;
  if ($root) {
    const container = createRoot($root);
    container.render(
      &amp;lt;RootComponent&amp;gt;
        &amp;lt;DRMSearch {...(window.globalUtils.getPageData() as DRMSearchResult.PageData)} /&amp;gt;
      &amp;lt;/RootComponent&amp;gt;
    );
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;开发语言&lt;/h3&gt;
&lt;h4&gt;typescript&lt;/h4&gt;
&lt;p&gt;typescript 作为 JavaScript 的超集，采用 typescript 的好处太多了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类型系统：在 JavaScript 至上添加了类型系统&lt;/li&gt;
&lt;li&gt;工具支持：代码自动补全、跳转到定义、接口提示等 IDE 功能,都需要类型系统的支持&lt;/li&gt;
&lt;li&gt;更低的维护成本以及低成本的重构&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些好处在开发过程中是能实时感受到的，不言而喻。&lt;/p&gt;
&lt;p&gt;选用 typescript 的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于添加类型导致的开发成本&lt;/li&gt;
&lt;li&gt;可能会有人烦与类型系统而变为 anyScript&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个我们只能通过代码 review 来避免，但一般情况下开发者在体验到了 typescript 的好处后不会这样去做的。&lt;/p&gt;
&lt;p&gt;对于 tsconfig 的配置，react 项目我们一般采用 create-react-app 中的默认 &lt;code&gt;tsconfig&lt;/code&gt; 作为基地扩展，默认情况下能满足大部分项目的诉求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
  &quot;extends&quot;: &quot;@tsconfig/create-react-app/tsconfig.json&quot;,
  &quot;include&quot;: [&quot;src&quot;, &quot;types&quot;],
  &quot;compilerOptions&quot;: {
    &quot;outDir&quot;: &quot;./build/&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然有自定义诉求可以参考 &lt;a href=&quot;https://www.typescriptlang.org/tsconfig&quot;&gt;TypeScript: TSConfig Reference - Docs on every TSConfig option (typescriptlang.org)&lt;/a&gt; 进行修改。&lt;/p&gt;
&lt;h4&gt;less&lt;/h4&gt;
&lt;p&gt;我们在样式预处理器：&lt;code&gt;scss&lt;/code&gt;, &lt;code&gt;less&lt;/code&gt; ,&lt;code&gt;stylus&lt;/code&gt; 中挑选了 &lt;code&gt;less&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stylus：内部没有用这个的，直接排除&lt;/li&gt;
&lt;li&gt;scss：包括 &lt;code&gt;fusion&lt;/code&gt; 在内的很多内部组件都是使用的 scss，但是 scss 太重了，无论是依赖的 node-scss 的安装编译还是每次项目的构建，都太慢了&lt;/li&gt;
&lt;li&gt;less：其实有了后处理器 postcss 后这些预处理器都可以不用，但是长久依赖的嵌套以及一些简单的变量、函数功能还是能够提高开发效率的，所以选用了 less，虽然在功能上相较于 scss 弱一点，但是完完全全满足日常开发了，而且足够轻量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前面提到了 &lt;code&gt;postcss&lt;/code&gt;, 我们用的多主要是两个功能，解决浏览器兼容性以及在无线上用来自适应的 &lt;code&gt;postcss-px-to-viewport&lt;/code&gt; ，配置文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const pxtoviewport = require(&quot;postcss-px-to-viewport&quot;);
const postcssPresetEnv = require(&quot;postcss-preset-env&quot;);

module.exports = {
  plugins: [
    postcssPresetEnv(),
    pxtoviewport({
      viewportWidth: 375, // (Number) The width of the viewport.
      viewportHeight: 667, // (Number) The height of the viewport.
      unitPrecision: 3, // (Number) The decimal numbers to allow the REM units to grow to.
      viewportUnit: &quot;vw&quot;, // (String) Expected units.
      selectorBlackList: [&quot;.ignore&quot;, &quot;.hairlines&quot;, /^\.bc-/, /^\.wlkeeppx/], // (Array) The selectors to ignore and leave as px.
      minPixelValue: 1, // (Number) Set the minimum pixel value to replace.
      mediaQuery: true, // (Boolean) Allow px to be converted in media queries.
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;postcss-preset-env&lt;/code&gt; 依赖 &lt;code&gt;browserslist&lt;/code&gt; 的配置，这个根据自己业务的浏览器分布情况具体来看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;browserslist&quot;: [
    &quot;android &amp;gt;= 7&quot;,
    &quot;ios &amp;gt;= 13&quot;
  ],
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;组件库&lt;/h3&gt;
&lt;h4&gt;Fusion Next 组件&lt;/h4&gt;
&lt;p&gt;面向买家的组件库目前只能选 Fusion ，需要注意的是需要用 &lt;code&gt;@alifd/next&lt;/code&gt; 这个包而不是 &lt;code&gt;@alife/next&lt;/code&gt; ,后者应该已经不更新了。&lt;/p&gt;
&lt;p&gt;用 Fusion 的组件会有一个问题，就是直接使用它的体积太大了, 即使是 gzip 压缩后，js 和 css 加起来都在 300kb 以上。同时如果在项目中参与构建的话会大幅拖慢的开发服务构建速度。&lt;/p&gt;
&lt;p&gt;为了解决体积以及开发体验的问题，所以我们有了 &lt;code&gt;TrafficModlib&lt;/code&gt; .&lt;/p&gt;
&lt;h4&gt;TrafficModLib&lt;/h4&gt;
&lt;p&gt;这是我们流量业务的共用组件库，包含了 Fusion 的部分组件以及我们的业务组件。通过这样的拆分，来避免直接引用 Fusion 的体积问题以及应用开发体验的问题，后面会详细讲。&lt;/p&gt;
&lt;h3&gt;构建工具&lt;/h3&gt;
&lt;p&gt;本来流量的应用分布在几个独立的应用中独立构建，之前我们改造了一次升级到了统一的构建器 fie ，降低多个项目中构建工具的维护成本和隐性风险。&lt;/p&gt;
&lt;p&gt;但是由于集团的拆分背景以及考虑到 fie 的支持问题，我们最终选择抛弃了 fie 的基地改造为无夸集团依赖的 icbuBuyer 构建器。&lt;/p&gt;
&lt;p&gt;过程中还是有些曲折，本身想要基于 def 的底座来开发脚手架以及构建器，结果 def 的命令行工具不更新了，升级到了 o2 ,而 o2 对命令行支持从现状看也让人担忧。在一开始基于 def 又从 def 升级到 o2 的过程中，走了很多弯路，结果最终 def-dev-kit 对应的升级后的 o2 命令根本找不到任何文档。所以最终还是抛弃了这些什么 def、o2 而选择自己开发简单普通的命令行工具。&lt;/p&gt;
&lt;h4&gt;icbuBuyer&lt;/h4&gt;
&lt;p&gt;我们最终将这个构建器命名为 icbu-buyer 定位买家前台构建器，目前的使用文档：&lt;a href=&quot;https://aliyuque.antfin.com/we-are-no-one/ggofk9/mv7nzi16uh7u9zyl&quot;&gt;脚手架 icbu-buyer 使用文档 (antfin.com)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;整个构建工具只做了很简单的事情，即包装了一个支持单页和多页开发的 webpack 配置，同时在项目初始化的时候做了一些符合开发标准的环境配置：比如 eslint 配置、prettier、commilint 等，没有任何魔法在里面。在最终 build 时针对 ssr 场景特殊优化单独构建了一份资源，核心的 webpack 配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function (userCofig: Partial&amp;lt;userConfig&amp;gt;): Configuration {
  const { port, production, debug, entryName, type } = userCofig;
  return {
    context: cwd,
    entry: getEntry({ dev: !production, entryName }),
    output: {
      publicPath: &apos;&apos;,
      path: getBuildPath(),
      filename: &apos;[name].js&apos;,
      chunkFilename: &apos;[name].chunks.js&apos;,
      crossOriginLoading: &apos;anonymous&apos;,
    },

    mode: !production ? &apos;development&apos; : &apos;production&apos;,
    devtool: !production &amp;amp;&amp;amp; &apos;source-map&apos;,
    devServer: !production
      ? comboProxy({
          port,
          host: &apos;0.0.0.0&apos;,
          hot: true,
        })
      : {},

    resolve: getResolve(),

    optimization: getOptimization(),

    module: {
      rules: getLoaderRules({ production, debug, entryName }),
    },

    externals: getExternals(false, userCofig),

    plugins: [
      debug &amp;amp;&amp;amp; new BundleAnalyzerPlugin(),
      new MiniCssExtractPlugin({
        filename: &apos;[name].css&apos;,
        chunkFilename: &apos;[name].chunk.css&apos;,
      }),
      !production &amp;amp;&amp;amp;
        new ForkTsCheckerWebpackPlugin({
          typescript: {
            memoryLimit: 1024,
          },
        }),
    ].filter(item =&amp;gt; !!item) as Configuration[&apos;plugins&apos;],
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码地址 &lt;a href=&quot;https://anpm.alibaba-inc.com/package/@ali/builder-icbu-buyer&quot;&gt;@ali/builder-icbu-buyer - Alibaba NPM Registry (alibaba-inc.com)&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;统一构建器的好处是什么&lt;/h4&gt;
&lt;p&gt;一个大业务下的多个应用，采用统一的构建器，可以减少很多维护以及选择上的问题。即可以享受应用独立拆分后的好处，又可以降低应用拆分后维护成本的问题。&lt;/p&gt;
&lt;p&gt;主要的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;babel 的统一配置和管理：不会出现一份代码在这个项目可以用在另一个项目可能因为 babel 配置的问题遭遇了浏览器兼容性问题&lt;/li&gt;
&lt;li&gt;代码风格的统一控制：可以在项目启动时检测当前项目的各种配置是否符合规范，不规范的话可以强制统一，不再需要在每个项目下去手动配置 eslint、stylelint、prettier、husky 等各种各样麻烦的事情&lt;/li&gt;
&lt;li&gt;减少依赖的心智负担：每个应用都不再需要在 &lt;code&gt;devDependents&lt;/code&gt; 里面写一堆一堆的 babel、webpack 之类的依赖了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于本地的开发服务以及构建相关的配置都被收拢在了统一的 icbuBuyer 中，相较于直接明了的 webpack 配置文件来说，理解成本略有上升&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而且越来越多的业务采用 ice、umi 等之类的解决方案也是懒得去做类似的构建相关的配置维护，不一样的地方在于，ice 之类的工具为了更加通用覆盖更多的业务场景会相对臃肿，同时技术自主度会更低一点。&lt;/p&gt;
&lt;p&gt;权衡下来，我们选择更加贴合业务场景、成本更低更可控的，对自己构建工具进行最低限度的简单封装。&lt;/p&gt;
&lt;h4&gt;应用如何接入&lt;/h4&gt;
&lt;p&gt;在项目下执行 &lt;code&gt;tnpm install @ali/builder-icbu-buyer &amp;amp;&amp;amp; npx ibuyer setup&lt;/code&gt; 就可以了，会在项目下写入 &lt;code&gt;icbubuyer.config.js&lt;/code&gt; 以及修改云构建 &lt;code&gt;abc.json&lt;/code&gt; 的配置。&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;icbubuyer.config.js&lt;/code&gt; 为应用的自定义配置，一般的应用默认配置就可以了，但是如果需要自定义，可以修改下的 &lt;code&gt;icbubuyer.config.js&lt;/code&gt; 的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * ICBU 买家前台通用构建器配置
 * 只要 abc.json 中的 type 以及 builder 为以下字段
 *    &quot;type&quot;: &quot;icbu-buyer&quot;
 *    &quot;builder&quot;: &quot;@ali/builder-icbu-buyer&quot;
 * 那么就不要删除该文件，否则构建器将无法正常工作。
 */
const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);
const cwd = process.cwd();
const { DEV, DEBUG } = process.env;

module.exports = {
  /**
   * 项目类型, 取值为以下值之一，
   * const PROJECT_TYPE = {
      pc: &apos;web-app-pc&apos;,
      m: &apos;web-app-m&apos;,
      npm: &apos;npm-package&apos;,
    };
   */
  type: &quot;web-app-pc&quot;,
  port: 3000,
  // 目前仅支持 webpack 作为构建器，后续可能支持别的比如 vite 等
  builder: &quot;webpack&quot;,
  // 如果有提取业务功能组件作为独立的包，挂载在全局的 window 上，需要在这里配置
  modLib: {
    // 公共包 package.json 中的 name，用来作为 import 时候的 id
    // 比如 import {Button} from &quot;@alife/icbu-mod-lib&quot;，packageName 为 @alife/icbu-mod-lib
    packageName: &quot;@alife/icbu-traffic-mod-lib&quot;,
    // 公共包挂载到全局变量上的命名，比如 IcbuModLib
    modName: &quot;TrafficModLib&quot;,
  },
  extendWebpackConfig: (webpackConfig) =&amp;gt; {
    // NOTE: 如果一定要自定义 webpack 的配置，可以在这里做一些修改。
    // 但是请注意，如果你的配置有问题，可能会导致构建失败，而且可能会和后续的脚手架升级不兼容【所以强烈不建议修改】
    // webpackConfig.externals = {
    //   react: &apos;window.React&apos;,
    //   &apos;react-dom&apos;: &apos;window.ReactDOM&apos;,
    // };
    // TODO: 待七巧板的引用改为 index.js 后，删除 showroom 的配置
    return webpackConfig;
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;技术规约&lt;/h2&gt;
&lt;h3&gt;代码规范&lt;/h3&gt;
&lt;p&gt;代码规范这里没什么说的，除了 js 以外其他采用之前集团前端技术部的整体规范，js 由于之前的 eslint-config-ali 规范过老以及无人维护的原因采用目前最新 eslint-config-att 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;js：eslint-config-att，配置文档：&lt;a href=&quot;https://aliyuque.antfin.com/livetech/ko5unm/gfw2swu7nbd5xwgn?singleDoc&quot;&gt;Web 终端代码规范指南 (antfin.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;css、less：styleling-config-ali，参考配置：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
    extends: [&apos;stylelint-config-ali&apos;],
    customSyntax: &apos;postcss-less&apos;,
    plugins: [&apos;stylelint-order&apos;],
    rules: {
      &apos;no-descending-specificity&apos;: null,
    },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;代码风格统一：prettier 在常规的规范上加了 import 的顺序规范，可以确保所有人代码的 import 顺序一致，参考:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  semi: true,
  singleQuote: true,
  trailingComma: &quot;es5&quot;,
  bracketSpacing: true,
  arrowParens: &quot;avoid&quot;,
  proseWrap: &quot;always&quot;,
  importOrder: [
    &quot;public-path&quot;,
    &quot;&amp;lt;THIRD_PARTY_MODULES&amp;gt;&quot;,
    &quot;^@ali/(.*)$&quot;,
    &quot;^@alife/(.*)$&quot;,
    &quot;^@alifd/(.*)$&quot;,
    &quot;^[./]&quot;,
    &quot;^[./](.*).(json)$&quot;,
    &quot;^[./](.*).(css|less|scss|sass)$&quot;,
  ],
  importOrderSeparation: true,
  importOrderSortSpecifiers: true,
  plugins: [&quot;@trivago/prettier-plugin-sort-imports&quot;],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通过 lint-staged 、husky 确保提交的代码都是合格的代码&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;提交拦截&lt;/h3&gt;
&lt;p&gt;上面那些规范配了如果没人执行也是没有用的，所以在 commit 的时候加了统一的 hook 确保所有提交上来的代码都是符合规范的。这里我们用的是 husky 以及 lint-staged ，具体的配置方法可以参考官方文档，我们的配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;lint-staged&quot;: {
    &quot;*.{json,md,scss,css,less,html}&quot;: &quot;npx prettier --write --ignore-unknown&quot;,
    &quot;src/**/*.{js,jsx,ts,tsx}&quot;: [
      &quot;npx prettier  --write&quot;,
      &quot;npx eslint --fix&quot;
    ]
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;快速接入&lt;/h3&gt;
&lt;p&gt;这些代码规范也好、提交的拦截也好，配置起来还是挺麻烦的，所以我们有工具可以快速接入，只需要在项目下执行：&lt;code&gt;tnpm install @ali/builder-icbu-buyer &amp;amp;&amp;amp; npx ibuyer setup --configOnly&lt;/code&gt; 就可以快速接入这套规范了。&lt;/p&gt;
&lt;h2&gt;架构设计&lt;/h2&gt;
&lt;p&gt;在之前我们的应用架构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/Snipaste_2024-01-02_15-25-23.png&quot; alt=&quot;应用架构-before&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这些业务都是糅杂在搜索的应用 searchBoost 以及 sclist 里面的，那真是牵一发而动全身，seo 的一个发布都有可能导致搜索的前台挂掉。当然这里面有历史的原因，也有组织架构的原因，我们不去过多的讨论过去的问题，来看一下在当下如何调整来更好的适合我们现有的业务。&lt;/p&gt;
&lt;h3&gt;背景分析&lt;/h3&gt;
&lt;p&gt;流量业务域下具体需要支撑的业务场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;免费承接页：主要是 SEO 场景的 showroom 、countrySearch, 同时包含 PC 和 无线&lt;/li&gt;
&lt;li&gt;付费承接页：PPC、PLA 以及 DRM 等，同时包含 PC 和 无线&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当下我们最终选择用四个应用来分别来承接这些业务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;traffic-free-pc：免费承接的 PC 应用&lt;/li&gt;
&lt;li&gt;traffic-free-wap：免费承接的 无线 应用&lt;/li&gt;
&lt;li&gt;traffic-pay-pc： 付费承接的 PC 应用&lt;/li&gt;
&lt;li&gt;traffic-pay-wap：付费承接的 无线应用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个应用划分粒度相对比较合适，遵循的理念为业务域隔离、端型隔离&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务域隔离：首先业务域之间必须独立划分，即免费和付费的业务不能在同一个应用，不能互相影响&lt;/li&gt;
&lt;li&gt;端型隔离：一个业务域下，PC 和 无线需要应用隔离，两者之间在前台没有共性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时我们在此之外额外建立了两个应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;traffic-base：提供全局的基础方法&lt;/li&gt;
&lt;li&gt;traffic-mod-lib：提供业务域的共用组件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么需要额外再抽两个应用出来，核心是为了复杂度的拆分：即将复杂度高的部分尽可能抽离到 &lt;code&gt;traffic-base&lt;/code&gt; 或者 &lt;code&gt;traffic-mod-lib&lt;/code&gt; 中，上层的业务应用只关心最终的页面展示。复杂度拆分之外，它的附带好处是：性能、开发体验，后面会详细讲。&lt;/p&gt;
&lt;h3&gt;最终方案&lt;/h3&gt;
&lt;p&gt;最终我们应用架构如下：
&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/Snipaste_2024-01-02_15-27-22.png&quot; alt=&quot;应用架构-before&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;新架构的优点&lt;/h3&gt;
&lt;h5&gt;复杂度管理（Complexity Management）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;上层业务应用主要负责业务相关逻辑和前端渲染，使其逻辑更加独立。&lt;/li&gt;
&lt;li&gt;高复杂度的逻辑被下沉到 &lt;code&gt;traffic-mod-lib&lt;/code&gt; 和 &lt;code&gt;traffic-base&lt;/code&gt; 中，从而实现复杂度的有效拆分。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;代码复用（Code Reusability）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;通过抽离基础组件和方法至 &lt;code&gt;traffic-base&lt;/code&gt; 和 &lt;code&gt;traffic-mod-lib&lt;/code&gt;，不仅提高了代码复用性，还降低了发布风险。&lt;/li&gt;
&lt;li&gt;基础方法在 &lt;code&gt;traffic-base&lt;/code&gt; 中被抽离出来，鼓励开发者将全局性、业务无关的方法独立出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;稳定性（Stability）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;上层业务发布频率最高，而共用组件和基础方法的发布频率相对较低，这样的层级设计降低了整体的发布风险。&lt;/li&gt;
&lt;li&gt;业务域（如免费和付费应用）之间的发布不会互相影响，进一步提高系统稳定性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;开发体验（Development Experience）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;因为依赖库（如 React, ReactDOM, Fusion）不需要在每次构建中都被重新编译，应用拆分也让每个应用下的无关代码大幅减少，从而大幅减少了本地开发服务冷启动和热编译的耗时，从而极大地提升了开发体验。&lt;/li&gt;
&lt;li&gt;每个代码仓库中无关的代码减少，也降低了开发者的心智负担&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;性能（Performance）&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;低发布频率的基础应用（&lt;code&gt;traffic-mod-lib&lt;/code&gt;和 &lt;code&gt;traffic-base&lt;/code&gt;）更容易在 CDN 和用户浏览器端被缓存，从而被动地提高了页面性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终在构建的代码体积上有 70% 的缩减，构建时间有 50% 以上的缩减，具体参考文档：&lt;a href=&quot;https://aliyuque.antfin.com/carlyuan.yq/fck4kg/slv2b6olb4u57lz7&quot;&gt;流量业务重构升级 (antfin.com)&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;基础应用详解&lt;/h3&gt;
&lt;h4&gt;traffic-base&lt;/h4&gt;
&lt;p&gt;提供全局方案，具体来讲：统一的打点、统一的事件监听、统一的事件发布订阅机制以及统一的三方资源加载控制等。&lt;/p&gt;
&lt;h5&gt;日志采集&lt;/h5&gt;
&lt;p&gt;比如我们为了规范化打点，制定了&lt;a href=&quot;https://aliyuque.antfin.com/we-are-no-one/eatinq/vomopx&quot;&gt;打点规范 (antfin.com)&lt;/a&gt;，但是如何确保所有人的打点都符合规范呢？codereview 当然可以，但是成本太高，所以我们的方案是所有打点方法收口到 &lt;code&gt;traffic-base&lt;/code&gt; 中：自动曝光打点直接由 &lt;code&gt;traffic-base&lt;/code&gt; 控制，点击打点统一用 &lt;code&gt;traffic-base&lt;/code&gt; 提供的 logClick 方法，这样就能确保所有的打点都是符合规范的了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统一曝光打点代码&lt;/li&gt;
&lt;li&gt;统一点击打点方法&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;事件总线管理&lt;/h5&gt;
&lt;p&gt;在很多业务场景下，组件或模块需要通过事件机制进行通信。这样的事件发布-订阅模式具有显著的解耦优点，然而，过度的解耦也是它的缺点。随着时间的推移项目的复杂度增加，没有明确的约束和文档可能会导致项目中充斥着各种不可控的事件。
为此我们采用了以下统一的控制方案：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;统一的监听与订阅方法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过使用统一的 &lt;code&gt;TrafficEvent&lt;/code&gt; 类，我们可以在一个中心点管理所有的事件。该类继承自 &lt;code&gt;EventEmitter&lt;/code&gt;，并包含一个预定义的 &lt;code&gt;EventType&lt;/code&gt; 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { EventEmitter } from &apos;events&apos;;
import { EventType } from &apos;./constants&apos;;

class TrafficEvent extends EventEmitter {
  EventType: typeof EventType;

  constructor() {
    super();
    this.EventType = EventType;
  }

  on&amp;lt;T extends string | symbol&amp;gt;(event: T, fn: (...args: any[]) =&amp;gt; void, context?: any): this {
    if (typeof event === &apos;string&apos;) {
      if (!Object.values(EventType).includes(event)) {
        throw new Error(`无效的事件类型：${event}`);
      }
      console.log(`订阅事件：${event}`);
    }
    // 如果类型是 symbol，则不进行 EventType 校验
    return super.on(event, fn, context);
  }

  emit&amp;lt;T extends string | symbol&amp;gt;(event: T, ...args: any[]): boolean {
    if (typeof event === &apos;string&apos;) {
      if (!Object.values(EventType).includes(event)) {
        throw new Error(`无效的事件类型：${event}`);
      }
      console.log(`发布事件：${event}`);
    }
    // 如果类型是 symbol，则不进行 EventType 校验
    return super.emit(event, ...args);
  }
}

export default new TrafficEvent();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;统一的事件 ID 管理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所有的事件类型都预先定义在 &lt;code&gt;EventType&lt;/code&gt; 对象中。这样做不仅增加了代码的可读性，还确保了事件的唯一性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const EventType = {
  scrollToEnd: &quot;scrollToEnd&quot;,
  scrolling: &quot;scrolling&quot;,
  videoChange: &quot;videoChange&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用这样的设计，所有需要事件通信的组件必须在 &lt;code&gt;base&lt;/code&gt; 中注册新的 &lt;code&gt;eventType&lt;/code&gt;。同时，该方案限制了只能发布和订阅预先定义的事件 ID。通过这种方式，整个事件通信系统都处于一个可控和可管理的状态。如果需要，你还可以在 &lt;code&gt;TrafficEvent&lt;/code&gt; 总线中添加更多的管理和监控功能。&lt;/p&gt;
&lt;p&gt;这样，我们不仅保留了解耦的优点，还通过统一的管理机制，增加了项目的可维护性和可控性。&lt;/p&gt;
&lt;p&gt;一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function useScrollToEnd(fn: () =&amp;gt; void): void {
  const { globalUtils } = useGlobalUtils();
  React.useEffect(() =&amp;gt; {
    if (!globalUtils) return;
    const { eventEmitter } = globalUtils;
    eventEmitter.on(eventEmitter.EventType.scrollToEnd, fn);
    return () =&amp;gt; {
      eventEmitter.off(eventEmitter.EventType.scrollToEnd, fn);
    };
  }, [fn, globalUtils]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;页面数据获取&lt;/h5&gt;
&lt;p&gt;提供了统一的页面同步数据获取方法，所以的上层业务应用在获取页面数据时都通过调用这个方法，而不是直接去读取 window 下的全局变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  getPageData(): Record&amp;lt;string, any&amp;gt; {
    return window._PAGE_DATA_;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来很简单，就三行代码，但是这体现了一种开发理念：即对全局变量的使用需要管控，同时屏蔽了细节。对于上层业务应用来讲，我只需要知道我要通过 getPageData 方法来获取页面数据就好了， 至于它到底怎么来的不重要。&lt;/p&gt;
&lt;p&gt;这样上层业务应用不需要直接去操作这个全局变量了，如果某一天这个全局变量和其他变量冲突了，我们可以放心的对齐进行修改而不是因为全局变量的读取写入散布在多个系统的各个代码文件中导致无法修改陷入死局。&lt;/p&gt;
&lt;p&gt;如果再进一步，对于这个只读的数据，可以在获取的时候直接通过 &lt;code&gt;Object.freeze()&lt;/code&gt; 把它冻结掉，变成只读，以免被不知道什么地方修改导致的隐形 bug 。&lt;/p&gt;
&lt;h5&gt;其他&lt;/h5&gt;
&lt;p&gt;还有一些类似全局的页面滚动监听、统一的三方资源加载控制确保页面核心功能优先级等方法。&lt;/p&gt;
&lt;h4&gt;traffic-mod-lib&lt;/h4&gt;
&lt;p&gt;作为公共组件应用，主要承载 FusionNext 组件以及我们自己的业务通用组件。&lt;/p&gt;
&lt;p&gt;FusionNext 组件由于保罗万象，所以体积也比较惊人，我们场景只能用到其中的一部分组件，所以进行了单独的处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ---------------Next 组件 start---------------
import Balloon from &quot;@alifd/next/lib/balloon&quot;;
import Button from &quot;@alifd/next/lib/button&quot;;
import Checkbox from &quot;@alifd/next/lib/checkbox&quot;;
import Collapse from &quot;@alifd/next/lib/collapse&quot;;
import ConfigProvider from &quot;@alifd/next/lib/config-provider&quot;;
import Dialog from &quot;@alifd/next/lib/dialog&quot;;
import Drawer from &quot;@alifd/next/lib/drawer&quot;;
import Icon from &quot;@alifd/next/lib/icon&quot;;
import Input from &quot;@alifd/next/lib/input&quot;;
import Loading from &quot;@alifd/next/lib/loading&quot;;
import Pagination from &quot;@alifd/next/lib/pagination&quot;;
import Select from &quot;@alifd/next/lib/select&quot;;
import Slider from &quot;@alifd/next/lib/slider&quot;;
import Tag from &quot;@alifd/next/lib/tag&quot;;
// ---------------Next 组件 end---------------

export {
  Balloon,
  Button,
  Checkbox,
  Collapse,
  Dialog,
  Loading,
  Pagination,
  Icon,
  ConfigProvider,
  Tag,
  Select,
  Slider,
  Drawer,
  Input,
  NextLocalEnUS,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他就是业务通用组件了，没什么好说的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;./public-path&quot;;

import classnames from &quot;classnames&quot;;
import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom&quot;;

// import IcbuIcon from &apos;./components/icon&apos;;
import IcbuIcon, { Certificate, FlagIcon, GsYear } from &quot;@alife/bc-icbu-icon&quot;;
import safeLog from &quot;@alife/bc-safe-log&quot;;

import Alitalk from &quot;./components/alitalk&quot;;
import &quot;./index.less&quot;;
import &quot;./index.scss&quot;;
// ---------------PC流量Footer三巨头-------------
import {
  FloatingWindow,
  RecommendLayer,
  RfqLayer,
  Swiper,
  SwiperRefType,
} from &quot;./modules&quot;;
import {
  Balloon,
  Button,
  Checkbox,
  Collapse,
  ConfigProvider,
  Dialog,
  Drawer,
  Icon,
  Input,
  Loading,
  NextLocalEnUS,
  Pagination,
  Select,
  Slider,
  Tag,
} from &quot;./next&quot;;

IcbuIcon.GsYear = GsYear;
IcbuIcon.FlagIcon = FlagIcon;
IcbuIcon.Certificate = Certificate;

export {
  // 基础框架&amp;amp;方法
  React,
  ReactDOM,
  classnames,
  safeLog,

  // next 组件
  Balloon,
  Button,
  Checkbox,
  Collapse,
  Dialog,
  Loading,
  Pagination,
  Icon,
  ConfigProvider,
  Select,
  Tag,
  Slider,
  Drawer,
  Input,
  NextLocalEnUS,

  // 流量业务组件
  FloatingWindow,
  RfqLayer,
  RecommendLayer,
  Swiper,
  // 三方业务组件
  IcbuIcon,
  Alitalk,
};

export type { SwiperRefType };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，上层业务应用如何在项目中使用我们 traffic-mod-lib 中组件。这里就需要提到 traffic-mod-lib 的构建上的处理了，我们需要把它作为一个独立的包来构建而不是普通的应用，修改项目下的 &lt;code&gt;icbubuyer.config.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  extendWebpackConfig: (webpackConfig, env) =&amp;gt; {
    const isSSR = env === &apos;ssr&apos;;
    // 本应用会作为一个基础包加载到其他应用中，所以需要将打包后的代码挂载到全局变量上
    webpackConfig.output.library = &apos;TrafficModLib&apos;;
    webpackConfig.output.libraryTarget = isSSR ? &apos;global&apos; : &apos;window&apos;;
    webpackConfig.output.globalObject = isSSR ? &apos;global&apos; : &apos;window&apos;;
    return webpackConfig;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样只要页面加载了 traffic-mod-lib 的 js，那么就会在全局变量上挂载一个 &lt;code&gt;TrafficModLib&lt;/code&gt; 的变量。然后需要在上层业务应用的 &lt;code&gt;icbubuyer.config.js&lt;/code&gt; 中进行配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  modLib: {
    // 公共包 package.json 中的 name，用来作为 import 时候的 id
    // 比如 import {Button} from &quot;@alife/icbu-mod-lib&quot;，packageName 为 @alife/icbu-mod-lib
    packageName: &apos;@alife/icbu-traffic-mod-lib&apos;,
    // 公共包挂载到全局变量上的命名，比如 IcbuModLib
    modName: &apos;TrafficModLib&apos;,
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样所有从 &lt;code&gt;@alife/icbu-traffic-mod-lib&lt;/code&gt; 的引用最终都是从全局变量 &lt;code&gt;TrafficModLib&lt;/code&gt; 里面获取了，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  Alitalk,
  Balloon,
  Button,
  IcbuIcon,
  Icon,
  Swiper,
  SwiperRefType,
} from &quot;@alife/icbu-traffic-mod-lib&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;基础框架&lt;/h4&gt;
&lt;p&gt;基础框架 React、ReactDOM 这种，我们通过独立的 js 来引入，集团提供了对应的 CDN，流量业务目前引用的是最新的 react 版本。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生产环境：&lt;code&gt;https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.production.min.js,react-dom/18.2.0/umd/react-dom.production.min.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;开发环境：&lt;code&gt;https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.development.js,react-dom/18.2.0/umd/react-dom.development.js&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;如果你使用 lightProxy 来做本地的代理，可以在开发时添加这条命令 &lt;code&gt;https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.production.min.js,react-dom/18.2.0/umd/react-dom.production.min.js https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.development.js,react-dom/18.2.0/umd/react-dom.development.js&lt;/code&gt; 来提供开发体验&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用独立 cdn 来引入基础框架代码的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更加持久的缓存以及相对更高的缓存命中率&lt;/li&gt;
&lt;li&gt;进一步降低开发服务编译构建时间，提高开发体验，为手持号称末代 Intel 顶配实际性能羸弱的 16GB 内存经常被打满风扇狂转系统经常性差一点卡死同时被云壳蹂躏的 MacBook 的开发者送去关爱，做一个仁慈的人&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;开发&amp;amp;调试&lt;/h2&gt;
&lt;h3&gt;工程目录结构&lt;/h3&gt;
&lt;p&gt;这是我们业务应用的目录规范：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── README.md                   # 项目文档
├── abc.json                    # 云构建的配置
├── demo                        # 示例代码
│   ├── data.json               # 示例数据
│   ├── index.html              # 示例HTML
│   ├── index.tsx               # 示例TypeScript React文件
│   └── pla.json                # 示例配置
├── icbubuyer.config.js         # icbubuyer 构建器配置
├── package.json                # 依赖管理
├── src                         # 源代码
│   ├── component               # 共用组件
│   ├── hook                    # 自定义 Hooks
│   ├── module                  # 业务模块
│   ├── pages                   # 页面级组件
│   ├── public-path.js          # webpack 的chunk 引入路径配置
│   ├── style                   # 样式文件
│   ├── types                   # 类型定义
│   └── utils                   # 工具函数
├── tsconfig.json               # TypeScript配置
└── yarn.lock                   # Yarn锁文件

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有项目都保持同样的规范好处不言而喻，如果言一下的话就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;代码组织与可维护性&lt;/strong&gt;: 清晰的目录降低维护难度和出错率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;降低入门门槛&lt;/strong&gt;: 了解一个项目的目录结构，就了解了其他所有项目。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码可读性&lt;/strong&gt;: 明确清晰的目录降低工程的理解成本，提高代码可读性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提升协作效率&lt;/strong&gt;: 约定大于配置，规范可以减少不必要的沟通，提高团队效率。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;本地开发&lt;/h3&gt;
&lt;p&gt;所有的项目拥有统一的 npm script 规范：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过 npm run start 开启本地调试服务&lt;/li&gt;
&lt;li&gt;通过 npm run build 执行本地构建&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不会说启动一个开发服务，这个项目是 &lt;code&gt;npm run start&lt;/code&gt; 那个是 &lt;code&gt;npm run dev&lt;/code&gt; ,虽说是看一眼 package.json 就能了解的东西，但是不看就是比看更令人省心，身心愉悦。&lt;/p&gt;
&lt;p&gt;执行 npm run start 后，控制台会输出一些内容：构建器是否需要更新、最终使用的配置是什么样的开发服务地址端口等；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/bendikaifa.png&quot; alt=&quot;本地开发&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后需要绑定以下 hosts 或者代理，核心目的是把所有前端资源代理到本地开发服务，至于是要测试后端的预发环境还是线上环境那看自己的需求进行响应的代理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 前端资源代理
127.0.0.1 assets.alicdn.com
127.0.0.1 s.alicdn.com
127.0.0.1 dev.g.alicdn.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时打开线上的一个需要调试的页面，可以在控制台看到类似这样的输出：
&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/bedikaifa-1.png&quot; alt=&quot;本地开发&quot; /&gt;
说明我们当前打开的页面已经在使用本地开发服务的代码了，可以直接进行代码修改调试了。&lt;/p&gt;
&lt;h3&gt;多应用开发&amp;amp;联调&lt;/h3&gt;
&lt;p&gt;前面架构部分我们提到，任何一个页面其实都至少依赖三个应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上层业务应用&lt;/li&gt;
&lt;li&gt;traffic-mod-lib&lt;/li&gt;
&lt;li&gt;traffic-base&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这会面临几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;上层业务应用中调用 &lt;code&gt;traffic-mod-lib&lt;/code&gt; 的组件或者 &lt;code&gt;traffic-base&lt;/code&gt; 的方法时，怎么知道它有哪些组件或者方法，难道每次用都要去看它们的代码吗？&lt;/li&gt;
&lt;li&gt;如果一个需求需要同时修改上层业务应用、&lt;code&gt;traffic-mod-lib&lt;/code&gt; 以及 &lt;code&gt;traffic-base&lt;/code&gt; 时，怎么调试，难道每次都需要把其他的应用发到预发环境，或者需要写一堆的代理规则来进行本地调试吗？
我们来依次解答这些问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;跨应用 api 调用提示&lt;/h4&gt;
&lt;p&gt;在业务应用中使用 traffic-mob-lib 的组件或者 &lt;code&gt;traffic-base&lt;/code&gt; 的方法时，不可能没用一下就要去查代码或者文档，这样效率太低了，也容易出错，同时影响开发体验，会让人觉得应用拆分是徒增烦恼。&lt;/p&gt;
&lt;p&gt;这个问题的答案实际上也是对前面为什么采用 typescript 的一个回答，即两个底层应用都构建出一份 d.ts 的定义文件，每个上层业务应用引用就好了。&lt;/p&gt;
&lt;p&gt;比如 &lt;code&gt;traffic-base&lt;/code&gt; 中，有这么几个命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;build-types&quot;: &quot;./node_modules/.bin/tsc --emitDeclarationOnly&quot;,
&quot;publish-patch&quot;: &quot;rm -rf ./typings &amp;amp;&amp;amp; tnpm run build-types &amp;amp;&amp;amp; tnpm run build  &amp;amp;&amp;amp; tnpm version patch &amp;amp;&amp;amp; tnpm publish &amp;amp;&amp;amp; git push&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行 build-types 会在工程下生成整个应用的类型定义文件，然后以 npm 包的形式发布。在业务应用的 d.ts 文件中引用一下就好了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;reference types=&quot;@alife/cdn-traffic-base/typings/pc&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样实际使用时就会有类型安全的代码提示了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不建议业务应用直接从全局变量上读取调用方法，所以了封装 &lt;code&gt;traffic-base&lt;/code&gt; 方法的调用
&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/traffic-base-1.png&quot; alt=&quot;traffic-base&quot; /&gt;&lt;/li&gt;
&lt;li&gt;在使用时，就能获取完整的 api 提示了
&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/traffic-base-2.png&quot; alt=&quot;traffic-base&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;traffic-mod-lib 就更简单了，只需要 build 出一份类型定义文件然后发布 npm 包，上层业务应用直接使用就好了：
&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/traffic-base-3.png&quot; alt=&quot;traffic-base&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要注意的是，无论是 @alife/icbu-traffic-mod-lib 还是 @alife/cdn-traffic-base 虽然在业务应用中被引用了，但是它们并没有真正的参与项目的构建编译打包。在 webpack 中都 external 掉了，参考 icbubuyer.config.js 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  modLib: {
    // 公共包 package.json 中的 name，用来作为 import 时候的 id
    // 比如 import {Button} from &quot;@alife/icbu-mod-lib&quot;，packageName 为 @alife/icbu-mod-lib
    packageName: &apos;@alife/icbu-traffic-mod-lib&apos;,
    // 公共包挂载到全局变量上的命名，比如 IcbuModLib
    modName: &apos;TrafficModLib&apos;,
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;多应用本地联调&lt;/h4&gt;
&lt;p&gt;如果一个需求需要同时修改上层业务应用、&lt;code&gt;traffic-mod-lib&lt;/code&gt; 以及 &lt;code&gt;traffic-base&lt;/code&gt; 时，怎么调试？我们的构建器 &lt;code&gt;icbu-buyer&lt;/code&gt; 中使用 comboProxy 来做 webpack 的本地开发服务，具体在构建器中的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    devServer: !production
      ? comboProxy({
          port,
          host: &apos;0.0.0.0&apos;,
          hot: true,
        })
      : {},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 comboProxy 是我们自己维护的包，内网地址 &lt;a href=&quot;https://anpm.alibaba-inc.com/package/@ali/webpack-server-combo-proxy&quot;&gt;@ali/webpack-server-combo-proxy - Alibaba NPM Registry (alibaba-inc.com)&lt;/a&gt; 目前店铺、流量以及几个后台应用都是用这个包来做本地开发服务的。&lt;/p&gt;
&lt;h5&gt;webpack-server-combo-proxy&lt;/h5&gt;
&lt;p&gt;这个插件主要做了这几件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动一个后台服务，跑在 80 以及 443 端口上&lt;/li&gt;
&lt;li&gt;将当前的应用注册在这个后台服务中&lt;/li&gt;
&lt;li&gt;当浏览器端的请求被代理到本地服务时，根据被注册的地址转发到具体的本地应用中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/proxy-1.png&quot; alt=&quot;webpack-server-combo-proxy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;举个例子，比如我在开发 &lt;code&gt;cdn-traffic-pay-pc&lt;/code&gt; 应用，当我本地执行 npm run start 启动开发服务时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检测后台服务是否启动，如果没有的启动一个新的服务，占据 80 以及 443 端口&lt;/li&gt;
&lt;li&gt;在这个后台服务上用 &lt;code&gt;cdn-traffic-pay-pc&lt;/code&gt; 这个应用的 git 地址作为 id 注册到后台服务中，打开 127.0.0.1 可以看到：
&lt;ol&gt;
&lt;li&gt;&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/proxy-1-1.png&quot; alt=&quot;webpack-server-combo-proxy&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;当浏览器访问付费的承接页比如 &lt;a href=&quot;https://www.alibaba.com/premium/Seals.html?src=sem_ggl&amp;amp;mark=drm0611&amp;amp;tagId=62431584548&amp;amp;product_id=62431584548&amp;amp;pcate=1407&amp;amp;cid=1407&amp;amp;ali_creative_id=7396748df8d7b7171c8c69322fc97f56&amp;amp;ali_image_material_id=0f7f01f815bf5e66f0fe539b6a5802ff&quot;&gt;alibaba.com/premium/Seals.html?src=sem_ggl&amp;amp;mark=drm0611&amp;amp;tagId=62431584548&amp;amp;product_id=62431584548&amp;amp;pcate=1407&amp;amp;cid=1407&amp;amp;ali_creative_id=7396748df8d7b7171c8c69322fc97f56&amp;amp;ali_image_material_id=0f7f01f815bf5e66f0fe539b6a5802ff&lt;/a&gt; 时，其中依赖的比如 drm-search.css 之类的资源就会先通过本地的后台服务然后经过判断最终转发到本地的 webpack-dev-server 或者直接请求到预发前端或者线上的地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当此时需要同时调试其他项目时，只需要在被调试的项目下执行 npm run start，就会根据同样的逻辑注册新的端口以及服务地址，比如此时我需要开发 traffic-mod-lib ，那么启动本地开发服务后再访问 127.0.0.1 查看就会发现多了一个 &lt;code&gt;cdn-traffic-mod-lib&lt;/code&gt; 的注册地址：
&lt;img src=&quot;/imgs/frontend-development-standardization-best-practices/proxy-2.png&quot; alt=&quot;webpack-server-combo-proxy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样再次访问前台页面时，请求路径符合 &lt;code&gt;cdn-traffic-pay-pc&lt;/code&gt; 的链接会被转发到 3000 端口下的开发服务，路径符合 &lt;code&gt;cdn-traffic-mod-lib&lt;/code&gt; 的链接请求会被转发到 3001 下，实现多个项目的同时联调。&lt;/p&gt;
&lt;p&gt;这套机制的几个关键点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前台应用发布到 cdn 后的地址和 git 仓库地址之间有一一对应的关系，比如这个 cdn 资源：&lt;code&gt;https://s.alicdn.com/@g/ife/cdn-traffic-mod-lib/1.0.4/index.js&lt;/code&gt; 他对应的代码仓库的 git 地址路径就是 &lt;code&gt;git@gitlab.alibaba-inc.com:ife/cdn-traffic-mod-lib.git&lt;/code&gt; 所以我们的 comboProxy 可以通过资源链接分析出对应的代码仓库，从而可以注册并转发到正确的本地服务地址&lt;/li&gt;
&lt;li&gt;服务必须跑在 80 以及 443 端口上，然后通过 hosts 绑定也好代理也好，将所有的前端资源请求代理到本地的这个服务，这样所有的资源请求都得经过本地的代理服务&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;性能&lt;/h2&gt;
&lt;p&gt;承接页非常注重性能的体验，因为会直接影响到 Google 的算法排名，付费的 landingRate ，所以在性能上我们也做了很多事情。本文只关心代码工程方面的，完整的性能优化指引可以参考：&lt;a href=&quot;https://blog.kisnows.com/2023/01/11/alibaba-buyer-performance-optimization-summary&quot;&gt;Alibaba.com 性能优化经验总结&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在应用层面，我们做了这些事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非核心功能异步&lt;/li&gt;
&lt;li&gt;CDN 配置&lt;/li&gt;
&lt;li&gt;公共库独立接入&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;非核心功能异步&lt;/h3&gt;
&lt;p&gt;对于页面上非核心的功能，可以使用 webpack 的异步 import 来加载，比如我们页面上的左侧筛选模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function LeftFilterAsync({ data }: { data: PPCSearchResult.PageData }) {
  const TrafficLeftFilter = lazy(() =&amp;gt;
    import(/* webpackChunkName: &quot;left-filter&quot; */ &quot;@alife/traffic-left-filter&quot;)
  );
  const { snData, i18nText } = data;
  const handleChange = (link: string) =&amp;gt; {
    window.location.href = link;
    window.globalUtils.logUtils.logModClick(
      {
        moduleName: &quot;left-filter&quot;,
      },
      {
        action: &quot;filter&quot;,
        type: link,
      }
    );
  };
  return (
    &amp;lt;SSRCompatibleSuspense
      fallback={
        &amp;lt;Icon type=&quot;loading&quot; style={{ marginLeft: 40, marginTop: 50 }} /&amp;gt;
      }
    &amp;gt;
      &amp;lt;TrafficLeftFilter
        data={snData}
        i18n={i18nText}
        handleChange={handleChange}
      /&amp;gt;
    &amp;lt;/SSRCompatibleSuspense&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;import(/* webpackChunkName: &quot;left-filter&quot; */ &apos;@alife/traffic-left-filter&apos;)&lt;/code&gt; 来异步加载 &lt;code&gt;@alife/traffic-left-filter&lt;/code&gt; 模块，需要注意的是我们使用了一个组件 &lt;code&gt;SSRCompatibleSuspense&lt;/code&gt; 包裹了异步加载的模块，它的好处是在加载完成之前可以展示一个 loading 来占位，以免用户等待焦虑或者页面抖动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function SSRCompatibleSuspense(
  props: Parameters&amp;lt;typeof Suspense&amp;gt;[&quot;0&quot;]
) {
  const isMounted = useMounted();

  if (isMounted) {
    return &amp;lt;Suspense {...props} /&amp;gt;;
  }
  return &amp;lt;&amp;gt;{props.fallback}&amp;lt;/&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时需要注意的一点，由于我们的 js 都是在 cdn 上，域名一般都和页面的主域名不一致，所以需要进行配置才能时运行时能够加载到正确的 chunk 地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;文件名：public-path
/**
 * 处理 webpack 代码切分的 path 路径
 */
const REG_SRC = /^(https?:)?\/\/(.+)\/msite\/(.*)cdn-traffic-pay-pc\/([\d.]+)\//;

function getSrcMatch() {
  if (document.currentScript) {
    return document.currentScript.src.match(REG_SRC);
  } else {
    const elScripts = document.querySelectorAll(&apos;script[src]&apos;);
    // render脚本一般在最后，这里从后往前找，以便提升查找效率
    for (let i = elScripts.length - 1; i &amp;gt;= 0; i--) {
      const matches = elScripts[i].src.match(REG_SRC);
      if (matches) {
        return matches;
      }
    }
  }
}

export function getPublicPath() {
  const matches = getSrcMatch();
  return `//${matches[2]}/msite/cdn-traffic-pay-pc/${matches[4]}/`;
}

try {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = getPublicPath();
} catch (err) {
  console.error(err);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要手动设置 webpack 的 public_path ，然后在应用的主入口 js 处引入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;../../public-path&quot;;

import React from &quot;react&quot;;
import classnames from &quot;classnames&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样异步的 chunks 下就会从你配置的 &lt;strong&gt;webpack_public_path&lt;/strong&gt; 加载而不是当前页面的域名下加载了。&lt;/p&gt;
&lt;h3&gt;CDN 配置&lt;/h3&gt;
&lt;p&gt;项目更目录下需要有 &lt;code&gt;.assetsmetafile&lt;/code&gt; 文件来告诉 CDN 服务器当前资源的缓存时间，参考缓存一年的配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cache-control:max-age=31536000,s-maxage=31536000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;备注：和架构组的同学沟通说这是个私有的配置，很有可能某一天就没有用了。&lt;/p&gt;
&lt;h3&gt;公共库独立接入&lt;/h3&gt;
&lt;p&gt;前面提到的 React、ReactDOM 之类的一般不会发生变化的且整个部门机会都是用同样的选择的公共库，建议走独立的 CDN 地址引入，集团提供了常用库 CDN 地址平台 https://work.def.alibaba-inc.com/lib ，流量业务用的是目前 React 的最新版本专转换成 s.alicdn.com 后的资源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;combo 后的资源：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.production.min.js,react-dom/18.2.0/umd/react-dom.production.min.js
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;解 combo 后：
&lt;ul&gt;
&lt;li&gt;React:https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.production.min.js&lt;/li&gt;
&lt;li&gt;ReactDOM: https://s.alicdn.com/@g/code/lib/??react-dom/18.2.0/umd/react-dom.production.min.js&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;独立引用的好处：发布频率低，缓存命中率高。
采用现成的集团提供的 CDN 地址的好处：使用的业务方会更多，在 CDN 的各级边缘节点上的缓存概率也会更大&lt;/p&gt;
&lt;h2&gt;稳定性&lt;/h2&gt;
&lt;p&gt;在开发过程中，我们通过使用 typescript 提供更强的约束，eslint 以及 stylelint 之类的 lint 工具降低开发阶段的代码问题。同时因为经过应用拆分后，上层业务的复杂度较低，已经大幅降低了日常发布的风险。&lt;/p&gt;
&lt;p&gt;同时在发布过程中遵循集团的安全发布的规范，所有的发布需要经过以下几个流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预发环境测试通过&lt;/li&gt;
&lt;li&gt;codereview：核心项目需要 2 人通过&lt;/li&gt;
&lt;li&gt;菲尔兹自动化测试通过&lt;/li&gt;
&lt;li&gt;安全生产环境验证&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;都没有问题后发布上线，上线后观察一段时间内的监控指标 [流量业务的监控地址] 是否正常，不正常的话及时回滚。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文以从开发到上线的脉络，详细梳理了我们业务的技术选型、规约、架构设计、开发调试方法以及在稳定性和性能上的工作，一些地方都放上了我们实际的代码，希望更加容易理解。并没有什么高深的技术或者很巧妙的实现，都是从工作中沉淀下来的一些自认为的最佳实践。&lt;/p&gt;
&lt;p&gt;关于文中的方法或者选择以及方案，有任何疑问或者意见，欢迎讨论。&lt;/p&gt;
</content:encoded></item><item><title>用ES6重构fullPage</title><link>https://ab.kisnows.com/zh-CN/2016/02/22/fullpage-js-es6/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/02/22/fullpage-js-es6/</guid><pubDate>Mon, 22 Feb 2016 06:18:09 GMT</pubDate><content:encoded>&lt;p&gt;用 ES6 重写了 &lt;a href=&quot;https://github.com/kisnows/fullpage&quot;&gt;fullpage-light.js&lt;/a&gt;。
主要做了一下修改：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模块化：将整个文件问个成多个独立的模块，每个模块负责一个功能&lt;/li&gt;
&lt;li&gt;新语法：替换一些新的语法，比如变量声明的 let,const,Object.assign 等&lt;/li&gt;
&lt;li&gt;转码：因为目前浏览器还并不支持 ES2015,所以还需要用 Babel 做一下转码&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;模块化&lt;/h2&gt;
&lt;p&gt;根据功能，将整个文件分割成为了五个模块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-bootstrap.js   //方法和功能
-constant.js    //一些常量
-event.js       //绑定的事件
-index.js       //入口
-utils.js       //工具函数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样原来一个很大文件就被分割成为了五个独立的模块，每个模块只负责自己的功能就好，维护起来会方便很多。&lt;/p&gt;
&lt;h2&gt;新语法&lt;/h2&gt;
&lt;p&gt;除了用了 let,const 这个声明变量的关键字外，最主要还用 Object.assign 这个方法替换了下面这个函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 扩展 Option 对象
 * @param {Object} Default 默认设置
 * @param {Object} Customize 自定义设置
 * @returns {Object} Default 扩展后的设置
 */
function extendOption(Default, Customize) {
  if (typeof Customize !== &quot;object&quot;) {
    Customize = {};
  }
  for (var i in Customize) {
    if (Default.hasOwnProperty(i)) {
      Default[i] = Customize[i];
    }
  }
  return Default;
}

options = extendOption(defaults, Customize);

//现在
options = Object.assign({}, defaults, Customize);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既然用到了 Object.assign，就来说说它吧。参考 MDN 上的定义：&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Object.assign() 方法可以把任意多个的源对象所拥有的自身可枚举属性拷贝给目标对象，然后返回目标对象。
Object.assign(target, ...sources)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，它可以把...sources 包含对象上所有可枚举的属性，复制到目标对象。这样浅拷贝一个对象就很容易了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var obj = { a: 1, b: 2 };
var copy = Object.assign({}, obj);
//
console.log(copy); //Object {a: 1, b: 2}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时也可以用来合并若干的对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = {a:1}
var b = {b:1}
var c = {c:1}
var copy = Object.assign({},a,b,c)
copy
Object {a: 1, b: 1, c: 1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是需要注意的是继承属性和不可枚举属性是不能拷贝的，如果你尝试这么做，会发现虽然语句不会报错，但是继承属性和不可枚举的属性并没有拷贝成功。
兼容性上，目前 chrome 已经支持这个属性了，上面的例子都是在 chrome 控制台上实验过的。&lt;/p&gt;
&lt;h2&gt;转码&lt;/h2&gt;
&lt;p&gt;因为现在浏览器大多还不支持 ES2015 的语法，所以 webpack+Babel 来将代码转换为 ES5 的语法。这样，就可以直接在现代浏览器里面使用了。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;ES6 带来了很大的改进。比如 let 和 const 的块级作用域可以避免一些坑，合理使用箭头函数也可提高代码的可读性，以及 class,promise,module 等新特性，可以大大提高编程的快感。
虽然目前兼容性上还是有很多问题，但是有 babel 转码器的存在，也算是可以逐步从 ES3 和 ES5 过渡到 ES2015 了，尤其是最近在尝试用 React，发现用 ES2015 简直爽的飞起。&lt;/p&gt;
</content:encoded></item><item><title>轻量级fullPage整屏滚动框架V1.3发布</title><link>https://ab.kisnows.com/zh-CN/2015/10/14/fullpage-js/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/10/14/fullpage-js/</guid><pubDate>Wed, 14 Oct 2015 04:23:40 GMT</pubDate><content:encoded>&lt;h1&gt;项目初衷&lt;/h1&gt;
&lt;p&gt;前段时间接到一个新的项目，是一个整屏滚动的单页应用。&lt;/p&gt;
&lt;p&gt;本来打算完全自己写的，但是由于项目时间吃紧，就用了&lt;a href=&quot;https://github.com/alvarotrigo/fullPage.js&quot;&gt;fullpage&lt;/a&gt;这个框架。这个框架确实很强大，几乎你想要的 API 它都有，所以用起来也很方便。&lt;/p&gt;
&lt;p&gt;但是它也有几个缺点，第一它必须依赖 &lt;code&gt;jQuery&lt;/code&gt;,而且文件体积也比较庞大，对于移动端来说不是很友好。于是自己就抽出业余时间，写了这个项目。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kisnows/fullpage-light.js&quot;&gt;项目地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://kisnows.com/fullpage-light.js/&quot;&gt;一个简单的 DEMO&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;changeList&lt;/h1&gt;
&lt;p&gt;V1.3.0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修复对 firefox 的支持&lt;/li&gt;
&lt;li&gt;添加页面导航控制&lt;/li&gt;
&lt;li&gt;解决一些 bug&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;V1.1.0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加自动播放&lt;/li&gt;
&lt;li&gt;添加循环播放&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;V1.0.0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持移动端触摸控制&lt;/li&gt;
&lt;li&gt;添加 slide 水平滑动&lt;/li&gt;
&lt;li&gt;添加 PC 端键盘控制和鼠标滚轮控制&lt;/li&gt;
&lt;li&gt;提供实用的 API 方便开发&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;简介&lt;/h1&gt;
&lt;p&gt;一个轻巧的&lt;code&gt;fullpage&lt;/code&gt;框架，不依赖其他任何库，主要针对移动端设备（同时也支持桌面端），压缩后不到 4kb。
轻松创建炫酷的单页滑动网站。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h1&gt;兼容性&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Android 4.1+&lt;/th&gt;
&lt;th&gt;Safari 7.1+&lt;/th&gt;
&lt;th&gt;IE Edge&lt;/th&gt;
&lt;th&gt;Opera&lt;/th&gt;
&lt;th&gt;Chrome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;h1&gt;使用方法&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;引入 JavaScript 文件 &lt;code&gt;fullpage.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;引入 css 文件 &lt;code&gt;fullpage.css&lt;/code&gt;（如果你使用&lt;code&gt;less&lt;/code&gt;，则可以在 less 主文件中引入&lt;code&gt;fullpage.less&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;按照下面格式书写&lt;code&gt;html&lt;/code&gt;代码（其中 id 为 &lt;code&gt;sectionContent&lt;/code&gt; 的为包裹层，你可以自定义修改其 id）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;sectionContent&quot; class=&quot;fp-section-content&quot;&amp;gt;
        &amp;lt;div class=&quot;fp-section&quot;&amp;gt;
            &amp;lt;div class=&quot;fp-slide-wrap&quot;&amp;gt;
              &amp;lt;div class=&quot;fp-slide&quot;&amp;gt;1&amp;lt;/div&amp;gt;
              &amp;lt;div class=&quot;fp-slide&quot;&amp;gt;2&amp;lt;/div&amp;gt;
              &amp;lt;div class=&quot;fp-slide&quot;&amp;gt;3&amp;lt;/div&amp;gt;
              &amp;lt;div class=&quot;fp-slide&quot;&amp;gt;4&amp;lt;/div&amp;gt;
              &amp;lt;div class=&quot;fp-slide&quot;&amp;gt;5&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;fp-section&quot;&amp;gt;2&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;fp-section&quot;&amp;gt;3&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;初始化&lt;/h1&gt;
&lt;p&gt;简单使用，只要在页面加载完成后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fullpage.init(&apos;#sectionContent&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要定制化，则需要如下方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; fullpage.init(&apos;#sectionContent&apos;,{
        threshold: 10,              //触发滚动事件的阈值，越小越灵敏
        pageSpeed: 600,             //滚屏速度，单位为毫秒 ms
        afterLoad: null,            //页面载入事件，具体查看下面的 afterLoad 函数
        beforeLeave: null           //页面离开事件，具体查看下面的 beforeLeave 函数
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;##beforeLeave(leaveIndex,nowIndex)
离开当前页面时触发的事件，函数中 &lt;code&gt;this&lt;/code&gt; 指向当前页面的 &lt;code&gt;section&lt;/code&gt;,&lt;code&gt;leaveIndex&lt;/code&gt;为要离开页面的 &lt;code&gt;index&lt;/code&gt; ，&lt;code&gt;nowIndex&lt;/code&gt; 为要载入页面的 &lt;code&gt;Index&lt;/code&gt;
##afterLoad(afterIndex)
载入下一张页面后触发的事件，函数中 &lt;code&gt;this&lt;/code&gt; 指向将要载入页面的 &lt;code&gt;section&lt;/code&gt;, &lt;code&gt;afterIndex&lt;/code&gt; 为要载入页面的 &lt;code&gt;index&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fullpage.init(&quot;#sectionContent&quot;, {
  beforeLeave: function (leaveIndex, nowIndex) {
    //如果现在在第1个页面，向下滚动后
    if (nowIndex === 2) {
      //leaveIndex = 1,nowIndex = 2
      console.log(&quot;You will leave page 2&quot;); //这条语句会执行
    }
    console.log(this, leaveIndex, nowIndex); //这里的 this 指向将要离开的页面元素，即第一个页面
  },
  afterLoad: function (afterIndex) {
    //afterIndex = 2
    if (afterIndex === 2) {
      console.log(&quot;You will go to page 2&quot;); //这条语句会执行
    }
    console.log(this, afterIndex); //此处 this 指向当前载入的页面，即第二个页面
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;方法&lt;/h1&gt;
&lt;p&gt;##init(el,options)
页面初始化，&lt;code&gt;el&lt;/code&gt;为最外包裹层选择器，&lt;code&gt;options&lt;/code&gt;是要定制的参数。具体同&lt;a href=&quot;#%E5%88%9D%E5%A7%8B%E5%8C%96&quot;&gt;初始化&lt;/a&gt;
##moveTo(index,slideIndex)
滚动到指定页面,&lt;code&gt;index&lt;/code&gt; 为必选参数，&lt;code&gt;slideIndex&lt;/code&gt;为可选参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fullpage.moveTo(1); //滚动到第一个页面
fullpage.moveTo(3, 2); //滚动到第三个页面的第二个slider
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;##moveToNext(callback)
垂直滚动到下一个页面,&lt;code&gt;callback&lt;/code&gt;为回调函数，可选。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    fullpage.moveToNext();            //滚动到下一个页面
    fullpage.moveToNext(callback)     //滚动到下一个页面后，执行 callback
    fullpage.moveToNext(callback,params...)   //滚动到下一个页面后，执行 callback,params为callback的参数，根据情况传入
    function foo(a,b){
        console.log(a,b)
    }
    fullpage.moveToNext(foo,1,2)              //滚动到下一个页面，并输出 1，2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;moveToPre(callback)&lt;/h2&gt;
&lt;p&gt;垂直滚动到上一个页面，用法同 &lt;code&gt;moveToNext(callback)&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;slideToNext()&lt;/h2&gt;
&lt;p&gt;水平滚动到下一个页面（页面向左滚动）&lt;/p&gt;
&lt;h2&gt;slideToPre()&lt;/h2&gt;
&lt;p&gt;水平滚动到上一个页面（页面向右滚动）&lt;/p&gt;
&lt;h1&gt;TODO&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;s&gt;添加滚动到指定页面方法&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;s&gt;页面滚动时，页面载入或离开时添加自定义事件&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;s&gt;横屏滚动支持&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;s&gt;添加鼠标滚轮控制&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;s&gt;添加键盘控制&lt;/s&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>借助Hexo和Github搭建一个博客</title><link>https://ab.kisnows.com/zh-CN/2015/01/09/hexo-blog/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/09/hexo-blog/</guid><pubDate>Fri, 09 Jan 2015 00:34:46 GMT</pubDate><content:encoded>&lt;p&gt;花了两天时间，总算把博客搭起来了。期间碰到了很多问题，还好有伟大的 Google，也都一一化解。现将其间过程写下来，也好帮助像我一样需要帮助的人。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;框架选择&lt;/h2&gt;
&lt;p&gt;一开始打算搭建博客，先要思考你要搭建那种类型的网站：&lt;code&gt;动态网站&lt;/code&gt;还是&lt;code&gt;静态网站&lt;/code&gt;。
考虑到我只是想有一个可以记录自己想法的空间，所以就选择了静态网站。而静态网站也有很多框架可供选择，&lt;code&gt;github&lt;/code&gt;推荐的&lt;code&gt;jekyll&lt;/code&gt;，基于 jekyll 开发的&lt;code&gt;octopress&lt;/code&gt;，以及使用·node·编写的&lt;code&gt;hexo&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;jekyll&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Jekyll 是一个简单的博客形态的静态站点生产机器。它有一个模版目录，其中包含原始文本格式的文档，通过 Markdown（或者 Textile 以及 Liquid 转化成一个完整的可发布的静态网站，你可以发布在任何你喜爱的服务器上。Jekyll 也可以运行在 GitHub Page 上，也就是说，你可以使用 GitHub 的服务来搭建你的项目页面、博客或者网站，而且是完全免费的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本来打算用&lt;code&gt;jekyll&lt;/code&gt;，但是在配置过程中出现了一些问题，windows 下安装&lt;code&gt;jekyll&lt;/code&gt;各种失败，百般不得求解，最终放弃。&lt;/p&gt;
&lt;h3&gt;octopress&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Octopress 是利用 Jekyll 博客引擎开发的一个博客系统，生成的静态页面能够很好的在 github page 上展现。号称是 hacker 专属的一个博客系统(Ablogging framework for hackers.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;hexo&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;hexo 是由 Node.js 驱动的一款快速、简单且功能强大的博客框架。它和 jekyll 相比，更快，更轻量。其出自台湾大学生 tommy351 之手，编译上百篇文字只需要几秒。hexo 生成的静态网页可以直接放到 GitHub Pages，BAE，SAE 等平台上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为它的小巧轻便，易于配置，而且功能同样强大，最终选择了&lt;code&gt;hexo&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;环境准备&lt;/h2&gt;
&lt;p&gt;需要安装&lt;code&gt;nods.js&lt;/code&gt;，&lt;code&gt;git&lt;/code&gt;，和一款编辑器。这些直接找相应的官网，一路安装就行了。&lt;/p&gt;
&lt;h3&gt;安装 HEXO&lt;/h3&gt;
&lt;p&gt;以上软件安装好后，直接执行以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g hexo
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;初始化&lt;/h3&gt;
&lt;p&gt;执行&lt;code&gt;init&lt;/code&gt;命令初始化&lt;code&gt;hexo&lt;/code&gt;到你指定的目录,比如我是在&lt;code&gt;D&lt;/code&gt;盘的&lt;code&gt;blog&lt;/code&gt;文件夹下(以下默认都在这个路径下)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\blog&amp;gt;hexo init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装过程就完了，是不是很简单呢？&lt;/p&gt;
&lt;h3&gt;生成页面&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;D:\blog&amp;gt;hexo generate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;本地启动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;D:\blog&amp;gt;hexo server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在浏览器中输入&lt;a href=&quot;http://localhost:4000&quot;&gt;http://localhost:4000&lt;/a&gt;就可以看到你自己的博客了。&lt;/p&gt;
&lt;h2&gt;写博客&lt;/h2&gt;
&lt;p&gt;执行&lt;code&gt;new&lt;/code&gt;命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\blog&amp;gt;hexo new [layout] &quot;postName&quot;#其中layout为模版类型，默认为post，“postName”为你的文章名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来在&lt;code&gt;hexo\source\_posts\&lt;/code&gt;中就可以找到相应的文件，接下来就可书写文章了。&lt;/p&gt;
&lt;h3&gt;部署&lt;/h3&gt;
&lt;p&gt;可以选择 github page，免费 300m 的空间，而且不限流量。方法也很简单，在你的 github 下，新建一个&lt;code&gt;username.github.io&lt;/code&gt;的仓库，然后在&lt;code&gt;hexo&lt;/code&gt;目录下名为的&lt;code&gt;_config.yml&lt;/code&gt;的配置文件里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deploy:
deploy:
  type: github
  repository: https://github.com/kisnows/kisnows.github.io.git
  branch: master
  #此处将kisnows替换为你的github账号名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一切就 ok 了。&lt;/p&gt;
&lt;h3&gt;常用命令&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;hexo new &quot;postName&quot; #新建文章
hexo new page &quot;pageName&quot; #新建页面
hexo generate #生成静态页面至public目录 简写：hexo g
hexo server #开启预览访问端口（默认端口4000，&apos;ctrl + c&apos;关闭server） 简写：hexo s
hexo deploy #将.deploy目录部署到GitHub 简写：hexo d
hexo d -g #组合命令，先生成页面，后部署到Github
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>ReactDOM 是如何把组件渲染到 DOM 中的？</title><link>https://ab.kisnows.com/zh-CN/2017/09/21/how-react-render-component-to-dom/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/09/21/how-react-render-component-to-dom/</guid><pubDate>Thu, 21 Sep 2017 03:28:39 GMT</pubDate><content:encoded>&lt;p&gt;当我们在开发 React 项目中，第一次调用 &lt;code&gt;ReactDOM.render&lt;/code&gt; 的时候都发生了什么呢？
今天就从源码角度来追踪一下这个问题（主要看流程， 而不纠结与细节）。&lt;/p&gt;
&lt;h2&gt;ReactDOMStackEntry&lt;/h2&gt;
&lt;p&gt;首先我们可以从 ReactDOM 的入口文件 ReactDOMStackEntry.js 中找到 render 方法。可以看到，&lt;code&gt;render&lt;/code&gt; 方法是 ReactMount 组件提供的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ReactDOMStack = {
  findDOMNode: findDOMNode,
  render: ReactMount.render,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  version: ReactVersion,

  /* eslint-disable camelcase */
  unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
  unstable_renderSubtreeIntoContainer: ReactMount.renderSubtreeIntoContainer,
  /* eslint-enable camelcase */

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
    // For TapEventPlugin which is popular in open source
    EventPluginHub: require(&quot;EventPluginHub&quot;),
    // Used by test-utils
    EventPluginRegistry: require(&quot;EventPluginRegistry&quot;),
    EventPropagators: require(&quot;EventPropagators&quot;),
    ReactControlledComponent: require(&quot;ReactControlledComponent&quot;),
    ReactDOMComponentTree,
    ReactDOMEventListener: require(&quot;ReactDOMEventListener&quot;),
    ReactUpdates: ReactUpdates,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ReactMount.render&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;  render: function(nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(
      null,
      nextElement,
      container,
      callback,
    );
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又调到了 &lt;code&gt;_renderSubtreeIntoContainer&lt;/code&gt; 方法, 这个方法核心内容如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;ReactMount._renderSubtreeIntoContainer&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;_renderSubtreeIntoContainer: function(
    parentComponent,
    nextElement,
    container,
    callback,
  ) {
    callback = callback === undefined ? null : callback;
    if (!React.isValidElement(nextElement)) {
      ...
    }
    // 创建下一个 wrapped 元素
    var nextWrappedElement = React.createElement(TopLevelWrapper, {
      child: nextElement,
    });

    var nextContext = getContextForSubtree(parentComponent);
    // 拿到当前的顶层容器组件
    var prevComponent = getTopLevelWrapperInContainer(container);
    // 对于第一次 render 来说，prevComponent 为 null
    if (prevComponent) {
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props.child;
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback =
          callback &amp;amp;&amp;amp;
          function() {
            validateCallback(callback);
            callback.call(publicInst);
          };
        ReactMount._updateRootComponent(
          prevComponent,
          nextWrappedElement,
          nextContext,
          container,
          updatedCallback,
        );
        return publicInst;
      } else {
        ReactMount.unmountComponentAtNode(container);
      }
    }

    var reactRootElement = getReactRootElementInContainer(container);
    var containerHasReactMarkup =
      reactRootElement &amp;amp;&amp;amp; !!internalGetID(reactRootElement);
    var containerHasNonRootReactChild = hasNonRootReactChild(container);

    var shouldReuseMarkup =
      containerHasReactMarkup &amp;amp;&amp;amp;
      !prevComponent &amp;amp;&amp;amp;
      !containerHasNonRootReactChild;
    var component = ReactMount._renderNewRootComponent(
      nextWrappedElement,
      container,
      shouldReuseMarkup,
      nextContext,
      callback,
    )._renderedComponent.getPublicInstance();
    return component;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有几个方法，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getTopLevelWrapperInContainer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shouldUpdateReactComponent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_renderNewRootComponent&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;getTopLevelWrapperInContainer&lt;/h3&gt;
&lt;p&gt;这个方法用来拿到现有的顶层容器组件，相关代码如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getTopLevelWrapperInContainer(container) {
  var root = getHostRootInstanceInContainer(container);
  return root ? root._hostContainerInfo._topLevelWrapper : null;
}
function getHostRootInstanceInContainer(container) {
  var rootEl = getReactRootElementInContainer(container);
  var prevHostInstance =
    rootEl &amp;amp;&amp;amp; ReactDOMComponentTree.getInstanceFromNode(rootEl);
  return prevHostInstance &amp;amp;&amp;amp; !prevHostInstance._hostParent
    ? prevHostInstance
    : null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用了两个方法，&lt;code&gt;getReactRootElementInContainer&lt;/code&gt; 和 &lt;code&gt;ReactDOMComponentTree.getInstanceFromNode&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;getReactRootElementInContainer&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getReactRootElementInContainer(container) {
  if (!container) {
    return null;
  }

  if (container.nodeType === DOCUMENT_NODE) {
    return container.documentElement;
  } else {
    return container.firstChild;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;ReactDOMComponentTree&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个模块有三个方法，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;precacheChildNodes&lt;/code&gt; 在 DOM 节点上存储相应的 React 实例&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getNodeFromInstance&lt;/code&gt; 从一个实例上获取到对应的 DOM 节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getInstanceFromNode&lt;/code&gt; 从一个 DOM 节点上获取到对应的实例&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;shouldUpdateReactComponent&lt;/h3&gt;
&lt;p&gt;判断是否需要更新组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }

  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  if (prevType === &quot;string&quot; || prevType === &quot;number&quot;) {
    return nextType === &quot;string&quot; || nextType === &quot;number&quot;;
  } else {
    return (
      nextType === &quot;object&quot; &amp;amp;&amp;amp;
      prevElement.type === nextElement.type &amp;amp;&amp;amp;
      prevElement.key === nextElement.key
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到逻辑是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前后两次元素都为 &lt;code&gt;null&lt;/code&gt; 返回 &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果是 &lt;code&gt;textComponent&lt;/code&gt;，那么直接更新&lt;/li&gt;
&lt;li&gt;否则当为 DOM 元素或者 React 元素时，且 type 和 key 都相同时返回 &lt;code&gt;true&lt;/code&gt;, 执行 update&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ReactMount._renderNewRootComponent&lt;/h2&gt;
&lt;p&gt;这个方法是 &lt;code&gt;_renderSubtreeIntoContainer&lt;/code&gt; 的核心，用来把一个新的组件挂载到 DOM 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  _renderNewRootComponent: function(
    nextElement,
    container,
    shouldReuseMarkup,
    context,
    callback,
  ) {
    // 通过 instantiateReactComponent 拿到 React Component 组件实例
    var componentInstance = instantiateReactComponent(nextElement, false);

    if (callback) {
      componentInstance._pendingCallbacks = [
        function() {
          validateCallback(callback);
          callback.call(
            componentInstance._renderedComponent.getPublicInstance(),
          );
        },
      ];
    }

    // The initial render is synchronous but any updates that happen during
    // rendering, in componentWillMount or componentDidMount, will be batched
    // according to the current batching strategy.
    ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,
      componentInstance,
      container,
      shouldReuseMarkup,
      context,
    );

    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;

    return componentInstance;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;instantiateReactComponent&lt;/h3&gt;
&lt;p&gt;根据传入的参数来生成不同的 React Component, 核心代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  if (node === null || node === false) {
    instance = ReactEmptyComponent.create(instantiateReactComponent);
  } else if (typeof node === &apos;object&apos;) {
    var element = node;
    var type = element.type;
    if (typeof type !== &apos;function&apos; &amp;amp;&amp;amp; typeof type !== &apos;string&apos;) {
      ...
    }

    // Special case string values
    if (typeof element.type === &apos;string&apos;) {
      instance = ReactHostComponent.createInternalComponent(element);
    } else if (isInternalComponentType(element.type)) {
      // This is temporarily available for custom components that are not string
      // representations. I.e. ART. Once those are updated to use the string
      // representation, we can drop this code path.
      instance = new element.type(element);

      // We renamed this. Allow the old name for compat. :(
      if (!instance.getHostNode) {
        instance.getHostNode = instance.getNativeNode;
      }
    } else {
      instance = new ReactCompositeComponentWrapper(element);
    }
  } else if (typeof node === &apos;string&apos; || typeof node === &apos;number&apos;) {
    instance = ReactHostComponent.createInstanceForText(node);
  } else {
    invariant(false, &apos;Encountered invalid React node of type %s&apos;, typeof node);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中可以看到，根据 &lt;code&gt;element.type&lt;/code&gt; 的不同，有三个方法来生成三种不同 React 组件实例，分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ReactHostComponent.createInternalComponent(element)&lt;/li&gt;
&lt;li&gt;new ReactCompositeComponentWrapper(element)&lt;/li&gt;
&lt;li&gt;ReactHostComponent.createInstanceForText(node);&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可能读者会注意到，当 &lt;code&gt;isInternalComponentType(element.type)&lt;/code&gt; 成立时， &lt;code&gt;instance = new element.type(element);&lt;/code&gt; 的这段代码被我忽略了，那是因为这个是 React 封装的内部组件不是由字符串表达时的解决方法，我们是不用关心的。
我们来看上面的三个方法，其中两个方法都调用了 ReactHostComponent 模块。&lt;/p&gt;
&lt;h3&gt;ReactHostComponent&lt;/h3&gt;
&lt;p&gt;核心代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ReactHostComponentInjection = {
  // This accepts a class that receives the tag string. This is a catch all
  // that can render any kind of tag.
  injectGenericComponentClass: function (componentClass) {
    genericComponentClass = componentClass;
  },
  // This accepts a text component class that takes the text string to be
  // rendered as props.
  injectTextComponentClass: function (componentClass) {
    textComponentClass = componentClass;
  },
};

function createInternalComponent(element) {
  invariant(
    genericComponentClass,
    &quot;There is no registered component for the tag %s&quot;,
    element.type,
  );
  return new genericComponentClass(element);
}

/**
 * @param {ReactText} text
 * @return {ReactComponent}
 */
function createInstanceForText(text) {
  return new textComponentClass(text);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是提供了两个方法来创建组件，而其中两个组件 class 的实现是通过其他模块注入进来的，那到底是从哪里注入进来的呢。
经过一番查找，发现是在 &lt;code&gt;ReactDOMStackInjection.js&lt;/code&gt; 中注入的，我们看一下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ReactComponentEnvironment = require(&quot;ReactComponentEnvironment&quot;);
var ReactComponentBrowserEnvironment = require(&quot;ReactComponentBrowserEnvironment&quot;);
var ReactDOMComponent = require(&quot;ReactDOMComponent&quot;);
var ReactDOMComponentTree = require(&quot;ReactDOMComponentTree&quot;);
var ReactDOMEmptyComponent = require(&quot;ReactDOMEmptyComponent&quot;);
var ReactDOMTextComponent = require(&quot;ReactDOMTextComponent&quot;);
var ReactDefaultBatchingStrategy = require(&quot;ReactDefaultBatchingStrategy&quot;);
var ReactEmptyComponent = require(&quot;ReactEmptyComponent&quot;);
var ReactGenericBatching = require(&quot;ReactGenericBatching&quot;);
var ReactHostComponent = require(&quot;ReactHostComponent&quot;);
var ReactReconcileTransaction = require(&quot;ReactReconcileTransaction&quot;);
var ReactUpdates = require(&quot;ReactUpdates&quot;);

var findDOMNode = require(&quot;findDOMNode&quot;);
var getHostComponentFromComposite = require(&quot;getHostComponentFromComposite&quot;);

ReactGenericBatching.injection.injectStackBatchedUpdates(
  ReactUpdates.batchedUpdates,
);

ReactHostComponent.injection.injectGenericComponentClass(ReactDOMComponent);

ReactHostComponent.injection.injectTextComponentClass(ReactDOMTextComponent);

ReactEmptyComponent.injection.injectEmptyComponentFactory(
  function (instantiate) {
    return new ReactDOMEmptyComponent(instantiate);
  },
);

ReactUpdates.injection.injectReconcileTransaction(ReactReconcileTransaction);
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);

ReactComponentEnvironment.injection.injectEnvironment(
  ReactComponentBrowserEnvironment,
);

findDOMNode._injectStack(function (inst) {
  inst = getHostComponentFromComposite(inst);
  return inst ? ReactDOMComponentTree.getNodeFromInstance(inst) : null;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，这个模块通过 &lt;code&gt;ReactHostComponent.injection&lt;/code&gt; 注入了 ReactDOMComponent 和 ReactDOMTextComponent. 同时也注入了一些其他模块，这个我们后面还会用到。&lt;/p&gt;
&lt;p&gt;这里 ReactDOMComponent 和 ReactDOMTextComponent 才是真是的生成 DOM 标记的模块，它们的内容过多，但是代码比较简单了，这里就不细说了。总之调用这两个模块的 &lt;code&gt;mountComponent&lt;/code&gt; 方法都会生成 DOM Markup. 不同的地方在于， ReactDOMComponent 会如下结构的 Markup&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  node: node,
  children: [],
  html: null,
  text: null,
  toString
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 ReactDOMTextComponent 会直接生成要渲染在 DOM 里面的 &lt;code&gt;String&lt;/code&gt; 类型的文本。&lt;/p&gt;
&lt;h3&gt;ReactCompositeComponent&lt;/h3&gt;
&lt;p&gt;最后一类组件应该是 &lt;code&gt;ReactCompositeComponentWrapper&lt;/code&gt;, 但查看 instantiateReactComponent 里面的这两段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ReactCompositeComponentWrapper = function (element) {
  this.construct(element);
};

Object.assign(
  ReactCompositeComponentWrapper.prototype,
  ReactCompositeComponent,
  {
    _instantiateReactComponent: instantiateReactComponent,
  },
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当调用 &lt;code&gt;this.construct&lt;/code&gt; 的时候，还是调用到了 ReactCompositeComponent ,这个就是用户自定义的组件。&lt;/p&gt;
&lt;h3&gt;ReactUpdates.batchedUpdates&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function batchedUpdates(callback, a, b, c, d, e) {
  ensureInjected();
  return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 ReactUpdates 里面是 通过 batchingStrategy 调用 batchedUpdate. 而 batchingStrategy 也是前面说到的 ReactDOMStackInjection 来注入进去的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReactUpdates.injection.injectReconcileTransaction(ReactReconcileTransaction);
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ReactUpdates 共注入了两个模块，分别是 ReactReconcileTransaction 和 ReactDefaultBatchingStrategy.&lt;/p&gt;
&lt;p&gt;先来看一下 ReactDefaultBatchingStrategy, ReactReconcileTransaction 后面碰到再说，来看一下它的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ReactUpdates = require(&quot;ReactUpdates&quot;);
var Transaction = require(&quot;Transaction&quot;);

var emptyFunction = require(&quot;fbjs/lib/emptyFunction&quot;);

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  /**
   * Call the provided function in a context within which calls to `setState`
   * and friends are batched such that components aren&apos;t updated unnecessarily.
   */
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      // 如果当前 updates 已经完成，那么直接调用 callback
      return callback(a, b, c, d, e);
    } else {
      // 通过事务的方式去调用 callback
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到我们前面调用的 &lt;code&gt;ReactUpdates.batchUpdates&lt;/code&gt; 实际上调用到了这里的 batchedUpdates, 里面的逻辑也很简单。&lt;/p&gt;
&lt;p&gt;这里面用到了一个 transaction 方法，这里我理解为“事务”。也就是说当如果当前正在进行一次更新，那么就通过事务的方式去调用这个 callback.&lt;/p&gt;
&lt;h4&gt;transaction&lt;/h4&gt;
&lt;p&gt;transaction 在 React 源码里面使用非常广泛，作用是通过事务的方式去调用一个方法。&lt;/p&gt;
&lt;p&gt;用一个或多个 wrapper 把方法包裹起来，在方法调用前和调用之后依次执行。事务会确保 wrapper 的 initialize 和 close 方法都会执行，无论要执行的方法执行成功或失败报错，看看它的源码解释就很明白了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; * &amp;lt;pre&amp;gt;
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +-----------------&amp;gt;|-|---|-|---|--&amp;gt;|anyMethod|---|---|-|---|-|--------&amp;gt;
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * &amp;lt;/pre&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们再回到刚才的 ReactDefaultBatchingStrategy, 代码里面有两个 wrapper.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它们的 initialize 方法都是一个空函数，close 方法分别是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把当前的更新状态置为 false&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flushBatchedUpdates&lt;/code&gt; 这个方法比较复杂，这里不展开讲，主要是确保所有组件能够正确更新（&lt;code&gt;flushBatchedUpdates-&amp;gt;ReactUpdates.runBatchedUpdates-&amp;gt;ReactCompositeComponent.performUpdateIfNecessary&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在回到上面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReactUpdates.batchedUpdates(
  batchedMountComponentIntoNode,
  componentInstance,
  container,
  shouldReuseMarkup,
  context,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以知道，这里是调用了 batchedMountComponentIntoNode 进行后续的工作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function batchedMountComponentIntoNode(
  componentInstance,
  container,
  shouldReuseMarkup,
  context,
) {
  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
    /* useCreateElement */
    !shouldReuseMarkup,
  );
  transaction.perform(
    mountComponentIntoNode,
    null,
    componentInstance,
    container,
    transaction,
    shouldReuseMarkup,
    context,
  );
  ReactUpdates.ReactReconcileTransaction.release(transaction);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里第一条赋值语句就用到了上面注入到 ReactUpdates 里面的另一个模块 ReactReconcileTransaction 即 React 的调度事务模块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var SELECTION_RESTORATION = {
  initialize: ReactInputSelection.getSelectionInformation,
  close: ReactInputSelection.restoreSelection,
};

/**
 * Suppresses events (blur/focus) that could be inadvertently dispatched due to
 * high level DOM manipulations (like temporarily removing a text input from the
 * DOM).
 */
var EVENT_SUPPRESSION = {
  initialize: function() {
    var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
    ReactBrowserEventEmitter.setEnabled(false);
    return currentlyEnabled;
  },
  close: function(previouslyEnabled) {
    ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
  },
};

/**
 * Provides a queue for collecting `componentDidMount` and
 * `componentDidUpdate` callbacks during the transaction.
 */
var ON_DOM_READY_QUEUEING = {
  initialize: function() {
    this.reactMountReady.reset();
  },
  close: function() {
    this.reactMountReady.notifyAll();
  },
};
...
var TRANSACTION_WRAPPERS = [
  SELECTION_RESTORATION,
  EVENT_SUPPRESSION,
  ON_DOM_READY_QUEUEING,
];
function ReactReconcileTransaction(useCreateElement) {
  this.reinitializeTransaction();
  this.renderToStaticMarkup = false;
  this.reactMountReady = CallbackQueue.getPooled();
  this.useCreateElement = useCreateElement;
}
...
var Mixin = {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
  ...
}
Object.assign(ReactReconcileTransaction.prototype, Transaction, Mixin);

PooledClass.addPoolingTo(ReactReconcileTransaction);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里也是通过事务的方式去调用，它有三个 wrapper:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Selection Restoration 在更新过程中尽可能不打扰用户的选中范围（selection range）&lt;/li&gt;
&lt;li&gt;Event Suppression 抑制一些不需要的事件分发，比如暂时性删除一个 input 元素而导致的 blur 事件&lt;/li&gt;
&lt;li&gt;On DOM Ready Queueing 在事务执行过程中，提供一个 &lt;code&gt;componentDidMount&lt;/code&gt; 和 &lt;code&gt;componentDidUpdate&lt;/code&gt; 回调函数的队列&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而后面的 getPooled 方法，是一个利用实例池来避免不必要的 GC 的方法，不过多解释。&lt;/p&gt;
&lt;p&gt;接着用这个事务的方式去调用 mountComponentIntoNode, 详细的看一下这个方法。&lt;/p&gt;
&lt;h3&gt;mountComponentIntoNode&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function mountComponentIntoNode(
  wrapperInstance,
  container,
  transaction,
  shouldReuseMarkup,
  context,
) {
  var markup = ReactReconciler.mountComponent(
    wrapperInstance,
    transaction,
    null,
    ReactDOMContainerInfo(wrapperInstance, container),
    context,
    0 /* parentDebugID */,
  );

  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
  ReactMount._mountImageIntoNode(
    markup,
    container,
    wrapperInstance,
    shouldReuseMarkup,
    transaction,
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里看到了重点变量 &lt;strong&gt;markup&lt;/strong&gt; 的操作, 这个就是我们最终要往 DOM 里面渲染的对象。通过 &lt;code&gt;ReactReconciler.mountComponent&lt;/code&gt; 方法来得到 markup. &lt;code&gt;ReactReconciler.mountComponent&lt;/code&gt; 的源码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  mountComponent: function(
    internalInstance,
    transaction,
    hostParent,
    hostContainerInfo,
    context,
    parentDebugID, // 0 in production and for roots
  ) {
    var markup = internalInstance.mountComponent(
      transaction,
      hostParent,
      hostContainerInfo,
      context,
      parentDebugID,
    );
    if (
      internalInstance._currentElement &amp;amp;&amp;amp;
      internalInstance._currentElement.ref != null
    ) {
      transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
    }
    return markup;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它又调用了 &lt;code&gt;internalInstance.mountComponent&lt;/code&gt;, 这里的 internalInstance 其实就是前面说的通过 instantiateReactComponent 得到的 React Component 实例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// _renderNewRootComponent
var componentInstance = instantiateReactComponent(nextElement, false);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;nextElement&lt;/code&gt; 是要渲染的 React root 元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// _renderSubtreeIntoContainer
var nextWrappedElement = React.createElement(TopLevelWrapper, {
  child: nextElement,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TopLevelWrapper 的实现，需要注意它的 &lt;code&gt;render&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var TopLevelWrapper = function () {
  this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.isReactComponent = {};
TopLevelWrapper.prototype.render = function () {
  return this.props.child;
};
TopLevelWrapper.isReactTopLevelWrapper = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它的 &lt;code&gt;render&lt;/code&gt; 方法里面返回的 &lt;code&gt;this.props.child&lt;/code&gt; 就是 nextElement 也就是我们项目代码里面入口 &lt;code&gt;ReactDOM.render(&amp;lt;App/&amp;gt;,document.getElementById(&apos;root&apos;))&lt;/code&gt; 里面的 &lt;code&gt;&amp;lt;App/&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;回到 &lt;code&gt;ReactReconciler.mountComponent&lt;/code&gt; 里面的 &lt;code&gt;internalInstance.mountComponent&lt;/code&gt;. 通过前面的讲到的 instantiateReactComponent 我们知道返回的组件有三类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ReactDOMComponent&lt;/li&gt;
&lt;li&gt;ReactDOMTextComponent&lt;/li&gt;
&lt;li&gt;ReactCompositeComponent&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;前两类很简单，都是 DOM 本身的元素，最终会渲染出来它们对应的 Markup. 而 ReactCompositeComponent 比较复杂，我们只看关键代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//ReactCompositeComponent.mountComponent
    var Component = this._currentElement.type;

    var updateQueue = transaction.getUpdateQueue();

    // Initialize the public class
    var doConstruct = shouldConstruct(Component);
    var inst = this._constructComponent(
      doConstruct,
      publicProps,
      publicContext,
      updateQueue,
    );
    var renderedElement;
    if (!doConstruct &amp;amp;&amp;amp; (inst == null || inst.render == null)) {
      renderedElement = inst;
      inst = new StatelessComponent(Component);
      this._compositeType = ReactCompositeComponentTypes.StatelessFunctional;
    } else {
     。...
    }
      markup = this.performInitialMount(
        renderedElement,
        hostParent,
        hostContainerInfo,
        transaction,
        context,
      );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先得到 inst , 得到 inst 的调用栈是这样的： &lt;code&gt;this._constructComponent -&amp;gt; this._constructComponentWithoutOwner&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// this._constructComponentWithoutOwner
...
   var Component = this._currentElement.type;

    if (doConstruct) {
      if (__DEV__) {
        return measureLifeCyclePerf(
          () =&amp;gt; new Component(publicProps, publicContext, updateQueue),
          this._debugID,
          &apos;ctor&apos;,
        );
      } else {
        return new Component(publicProps, publicContext, updateQueue);
      }
    }
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据 &lt;code&gt;_currentElement.type&lt;/code&gt; 生成 Component 实例, &lt;code&gt;currentElement.type&lt;/code&gt; 即就是继承自 &lt;code&gt;React.Component&lt;/code&gt; 的 class 或者纯渲染组件 function.
然后声明 renderElement, 对于 stateless (函数声明的纯渲染组件)组件，&lt;code&gt;renderElement = inst&lt;/code&gt;, 否则为 undefined.&lt;/p&gt;
&lt;p&gt;接着来看 performInitialMount,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (renderedElement === undefined) {
  renderedElement = this._renderValidatedComponent();
}

var nodeType = ReactNodeTypes.getType(renderedElement);
this._renderedNodeType = nodeType;
var child = this._instantiateReactComponent(
  renderedElement,
  nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
);
this._renderedComponent = child;

var markup = ReactReconciler.mountComponent(
  child,
  transaction,
  hostParent,
  hostContainerInfo,
  this._processChildContext(context),
  debugID,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于非 stateless 组件时，需要对 renderedElement 赋值。调用栈为： &lt;code&gt;this._renderValidatedComponent -&amp;gt; this._renderValidatedComponentWithoutOwnerOrContext&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  _renderValidatedComponentWithoutOwnerOrContext: function() {
    var inst = this._instance;
    var renderedElement;

    if (__DEV__) {
      renderedElement = measureLifeCyclePerf(
        () =&amp;gt; inst.render(),
        this._debugID,
        &apos;render&apos;,
      );
    } else {
      renderedElement = inst.render();
    }

    if (__DEV__) {
      // We allow auto-mocks to proceed as if they&apos;re returning null.
      if (renderedElement === undefined &amp;amp;&amp;amp; inst.render._isMockFunction) {
        // This is probably bad practice. Consider warning here and
        // deprecating this convenience.
        renderedElement = null;
      }
    }

    return renderedElement;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终得到 renderedElement 也就是 &lt;code&gt;inst.render()&lt;/code&gt; 后的结果。&lt;/p&gt;
&lt;p&gt;再往下走 &lt;code&gt;child = this._instantiateReactComponent(renderedElement)&lt;/code&gt; 和 &lt;code&gt;markup = ReactReconciler.mountComponent(child,...)&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;可以联想到，这里会不断的循环递归调用 &lt;code&gt;ReactReconciler.mountComponent&lt;/code&gt;, 知道 child 不是 ReactCompositeComponent 为止，得到最终的 MarkUp. MarkUp 的数据结构可以在 DOMLazyTree 中找到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// DOMLazyTree
{
  node: node,
  children: [],
  html: null,
  text: null,
  toString,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拿到了 MarkUp，就只剩下最后一步了，通过 &lt;code&gt;ReactMount._mountImageIntoNode&lt;/code&gt; 来吧 MarkUp 挂载到实际 DOM 中。&lt;/p&gt;
&lt;h3&gt;ReactMount._mountImageIntoNode&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;  _mountImageIntoNode: function(
    markup,
    container,
    instance,
    shouldReuseMarkup,
    transaction,
  ) {
    invariant(
      isValidContainer(container),
      &apos;mountComponentIntoNode(...): Target container is not valid.&apos;,
    );
    // 首次渲染，这里为 shouldReuseMarkup = false ，里面都是更新的逻辑
    if (shouldReuseMarkup) {
      ...
    }
    // 首次渲染，transaction.useCreateElement = true
    if (transaction.useCreateElement) {
      while (container.lastChild) {
        container.removeChild(container.lastChild);
      }
      DOMLazyTree.insertTreeBefore(container, markup, null);
    } else {
      setInnerHTML(container, markup);
      ReactDOMComponentTree.precacheNode(instance, container.firstChild);
    }
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑很简单，有两种方式把 markup 渲染到 DOM 中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;清空给定的容器组件，然后把 markup 插入到给定的容器中&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;setInnerHTML&lt;/code&gt; 来把 markup 插入到给定容器中，并缓存虚拟 DOM 到实际的 DOM 节点上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于初次渲染， 会执行第一种方法，清空容器组件，把 markup 挂载到实际的 DOM 中。调用栈：&lt;code&gt;DOMLazyTree.insertTreeBefore -&amp;gt; insertTreeChildren&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function insertTreeChildren(tree) {
  if (!enableLazy) {
    return;
  }
  var node = tree.node;
  var children = tree.children;
  if (children.length) {
    for (var i = 0; i &amp;lt; children.length; i++) {
      insertTreeBefore(node, children[i], null);
    }
  } else if (tree.html != null) {
    setInnerHTML(node, tree.html);
  } else if (tree.text != null) {
    setTextContent(node, tree.text);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递归的挂载所有子组件到 DOM 中。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;那么到这里整个初次渲染的逻辑就算走完了，整体上可以看到 React 和 ReactDOM 的代码抽象程度非常高，以至于代码阅读起来非常的绕。本文也只限于整体的流程，没有深究细节，因为细节太多了。&lt;/p&gt;
&lt;p&gt;最后整理了一个 &lt;code&gt;ReactDOM.render&lt;/code&gt; 执行后流程关系图，能一定程度上帮助理解它的整过过程：
图很大，点击放大后也看不清，建议保存到本地后浏览，会看的清晰点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/how-react-render-component-to-dom/ReactDOM.png&quot; alt=&quot;ReactDOM.render&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;相关文章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;{% post_link react-source-code-analyze-1 React 源码浅析之 - 入口文件 %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-2 React 源码浅析之 - ReactBaseClasses %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-3 React 源码浅析之 - ReactChildren %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-4 React 源码浅析之 - ReactElement %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-5 React 源码浅析之 - onlyChildren %}&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>HTTP 缓存</title><link>https://ab.kisnows.com/zh-CN/2016/03/08/http-cache/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/03/08/http-cache/</guid><pubDate>Tue, 08 Mar 2016 04:57:46 GMT</pubDate><content:encoded>&lt;p&gt;下午意外的接到了阿里钉钉前端的面试，很是欣喜。
面试内容从做的项目，到使用的技术 Angular、React、Express 等，让我感觉到自己的理解还是不够深刻。
比如我说我喜欢 React，大项目中需要配合 Redux 来用处理数据，然后就问到 Redux 到底解决了哪些问题，我没能清楚的答上来。但是整体上，面试的前面部分，自我感觉还是很不错的。
但是到最后，面试官说问一个比较基础的问题：如何在 HTTP 协议中控制缓存？
我懵了，平时工作中根本接触不到。只能凭自己感觉说，从文件名的更改以及在 HTTP 的 header 中设置相应参数来设置，但具体怎样设置，设置的内容是什么，我完全答不上来。感觉肯定是要挂在这里。不过俗话说的好，人不能在同一个地方跌倒两次，谨以此篇记录 HTTP 缓存的相应内容。&lt;/p&gt;
&lt;h1&gt;概览&lt;/h1&gt;
&lt;p&gt;HTTP 控制缓存主要有一下几种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Expires&lt;/li&gt;
&lt;li&gt;Cache-Control&lt;/li&gt;
&lt;li&gt;Last-Modified/If-Modified-Since&lt;/li&gt;
&lt;li&gt;Etag/If-None-Match&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来，就一个一个来了解。&lt;/p&gt;
&lt;h2&gt;Expires&lt;/h2&gt;
&lt;p&gt;过期时间，有点类似于 cookies 里面的 Expires。可以在 header 中设置一个具体的过期时间，在这个过期时间内，浏览器都不会向服务器请求这个文件，会直接存本地的缓存中读取。
&lt;img src=&quot;/imgs/http-cache/headers.png&quot; alt=&quot;1&quot; /&gt;
上图是我博客中一个 js 文件的 HTTP header，可以看到其中红框 1 Expires 设置为当前 Date 的 4 个小时后，也就是说正常情况这个文件在 Expires 这个时间点以前的请求都会直接使用本地缓存，而不用从服务端重新获取。
需要注意的是，Expires 是 HTTP 1.0 的东西，现在绝大多数浏览器都默认使用 HTTP 1.1，所以用 Expires 控制缓存不是首选的方法。&lt;/p&gt;
&lt;h2&gt;Cache-control&lt;/h2&gt;
&lt;p&gt;Cache-control 与 Expires 作用基本相同，都是标志出当前资源的有效期，以此来控制浏览器是使用本地缓存还是从服务器重新获取资源。不过，不同的地方在于，Cache-control 能够控制的更加细致，当 header 中同时存在 Expires 时，Cache-control 的优先级要更高一些。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HTTP 协议头 Cache-Control 的值可以是 public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age
各个消息中的指令含义如下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Public      指示响应可被任何缓存区缓存。
Private     指示对于单个用户的整个或部分响应消息，不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息，此响应消息对于其他用户的请求无效。
no-cache    指示请求或响应消息不能缓存
no-store    用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
**max-age**   **指示客户机可以接收生存期不大于指定时间（以秒为单位）的响应。**
min-fresh   指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
max-stale   指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值，那么客户机可以接收超出超时期指定值之内的响应消息。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/http-cache/headers.png&quot; alt=&quot;2&quot; /&gt;
如图，红框 2 上面的 Cache-Control 这一栏，值为 public，max-age=14400. 就是说它的有效期是 14400s，也就是 4h，和上面的 Expires 的过期时间是一样的，但是它不像 Expires 那样必须给一个类似 Mon，07 Mar 2016 17:14:33 GMT 这样具体的时间，只需要给定一个最大寿命的时间就可以了。这个应该是比较常用的方法。&lt;/p&gt;
&lt;h2&gt;Last-Modified/If-Modified-Since&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Last-Modified&lt;/strong&gt; 表示这个资源的最后修改时间，服务器在相应请求时，会告诉浏览器此资源的最后修改时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If-Modified-Since&lt;/strong&gt; 发送 HTTP 请求时，浏览器会把缓存资源的最后修改时间一起发送到服务器去，服务器会用这个时间与服务器上实际文件的最后修改事件进行对比。如果时间一致，则返回 HTTP 状态码 304，浏览器接收到后会直接显示缓存的文件。如果时间不一致，则返回 HTTP 状态码 200 和新的文件内容，浏览器拿到文件后会丢掉本地旧文件，缓存新文件并展示出来。要注意的是 Last-Modified/If-Modified-Since 需要配合 Cache-control 使用，只有当本地的资源过期时（即超过了 max-age 定义的时间），才会向服务器发送相应的带有 If-Modified-Since 的请求。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Etag/If-None-Match&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Etag/If-None-Match&lt;/strong&gt; 同样需要配合 Cache-control 使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Etag&lt;/strong&gt; 服务器相应浏览器请求时，会告诉浏览器当前资源在服务的唯一标识，标识规则有服务器决定。If-None-Match 当资源过期时（即超过了 max-age 定义的时间），发现资源具有 Etag 声明，则在再次向服务器发送求情时带上 If-None-Match（即本地缓存资源的 Etag 值）。服务器收到请求后如果发现有 If-None-Match 则与服务端被请求资源的 Etag 进行比对，如果相同则说明资源无更改并返回 304，否则返回 200 和新的资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这四种方式的优先级可以通过一张图来说明：
&lt;img src=&quot;/imgs/http-cache/priority.png&quot; alt=&quot;3&quot; /&gt;
&lt;a href=&quot;HTTP://www.cnblogs.com/skynet/archive/2012/11/28/2792503.html&quot;&gt;图片来自&lt;/a&gt;
可以看到 Etag 的优先级是大于 Last-Modified 的。&lt;/p&gt;
&lt;p&gt;参考文章： 1.&lt;a href=&quot;HTTP://www.cnblogs.com/skynet/&quot;&gt;浏览器缓存机制&lt;/a&gt; 2.&lt;a href=&quot;HTTP://www.path8.net/tn/archives/2745&quot;&gt;浏览器缓存相关的 HTTP 头介绍:Expires,Cache-Control,Last-Modified,ETag&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;3.&lt;a href=&quot;HTTP://www.cnblogs.com/zh2000g/archive/2010/03/22/1692002.html&quot;&gt;HTTP 的请求头标签 If-Modified-Since&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>CSS3 实现人物赛跑动画</title><link>https://ab.kisnows.com/zh-CN/2015/05/21/income-plan-animation/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/05/21/income-plan-animation/</guid><pubDate>Thu, 21 May 2015 05:32:53 GMT</pubDate><content:encoded>&lt;p&gt;最近公司的一个手机项目里面有一个动画，就是一个人在跑道上跑步。为了能在手机端发挥更好的性能，选择了全部用 CSS3 来做一个动画。
因为是第一次做，碰到了很多问题，记录下来。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- moer --&amp;gt;&lt;/p&gt;
&lt;p&gt;效果图是这样的：
&lt;img src=&quot;/imgs/income-animation-all.png&quot; alt=&quot;All&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;拿到效果图先分析一下，可以看到动画分为一下四个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;跑动的小人&lt;/li&gt;
&lt;li&gt;运动的路&lt;/li&gt;
&lt;li&gt;左边的路牌&lt;/li&gt;
&lt;li&gt;收益的牌子&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;小人&lt;/h3&gt;
&lt;p&gt;小人很好解决，通过使用一张跑步的分解图片，
&lt;img src=&quot;/imgs/income-person.png&quot; alt=&quot;person&quot; /&gt;
然后通过逐帧的变动它的`background-position&apos;就可以了，真是简单啊。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.person {
    width: 62px;
    height: 110px;
    background: url(../imgs/plan/person1.png) -7px 0 no-repeat;
    background-size: auto 110px;
    animation: run .5s infinite;			//要注意加上各种浏览器的前戳，尤其是-webkit-，在这我就不加了
    position: absolute;
    top: 40%;
    left: 110*.84px;
}
@keyframes run {
    0% {
        background-position: -7px 0
    }
    25% {
        background-position: -79px 0
    }
    50% {
        background-position: -150px 0
    }
    75% {
        background-position: -216px 0
    }
    100% {
        background-position: -283px 0
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而小人就成这样了了，
&lt;img src=&quot;/imgs/income-person-wrong.gif&quot; alt=&quot;person-wrong&quot; /&gt;
简直惨不忍睹，分析原因，原来是因为 keyframes 默认是有过渡的。也就是说，我的&lt;code&gt;background-position&lt;/code&gt;是从-7 线性的渐变到-283，这当然不是我想要的结果。想到这样该是动画时间函数的问题，就去查&lt;code&gt;animation-timing-funciton&lt;/code&gt;的取值：&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;linear：
线性过渡。等同于贝塞尔曲线(0.0, 0.0, 1.0, 1.0)
ease：
平滑过渡。等同于贝塞尔曲线(0.25, 0.1, 0.25, 1.0)
ease-in：
由慢到快。等同于贝塞尔曲线(0.42, 0, 1.0, 1.0)
ease-out：
由快到慢。等同于贝塞尔曲线(0, 0, 0.58, 1.0)
ease-in-out：
由慢到快再到慢。等同于贝塞尔曲线(0.42, 0, 0.58, 1.0)
&lt;strong&gt;&lt;em&gt;step-start：&lt;/em&gt;&lt;/strong&gt;
等同于 steps(1, start)
&lt;strong&gt;&lt;em&gt;step-end：&lt;/em&gt;&lt;/strong&gt;
等同于 steps(1, end)
&lt;strong&gt;&lt;em&gt;steps(&amp;lt;integer&amp;gt;[, [ start | end ] ]?)：&lt;/em&gt;&lt;/strong&gt;
接受两个参数的步进函数。第一个参数必须为正整数，指定函数的步数。第二个参数取值可以是 start 或 end，指定每一步的值发生变化的时间点。第二个参数是可选的，默认值为 end。
cubic-bezier(&amp;lt;number&amp;gt;, &amp;lt;number&amp;gt;, &amp;lt;number&amp;gt;, &amp;lt;number&amp;gt;)：
特定的贝塞尔曲线类型，4 个数值需在[0, 1]区间内&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;step&lt;/code&gt;这个属性就是我们想要的，让他从一个状态直接跳到另个状态，而不是过渡过去。给上面的.person 再加上这条属性就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;animation-timing-function: step-start;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/income-person-run.gif&quot; alt=&quot;person-run&quot; /&gt;
这样小人跑步就解决了。&lt;/p&gt;
&lt;h3&gt;路&lt;/h3&gt;
&lt;p&gt;路一开始还想不到好的方法，本来想的是直接截一大段，弄两张图，然后让两张图交替的出现。但是这样图片会很大，即耗费网络资源又感觉方法很笨。看着跑道分析了一下，想到了一个好方法。
&lt;img src=&quot;/imgs/income-runway.png&quot; alt=&quot;runway&quot; /&gt;
通过截取跑道的最小组成部分，纵向平铺，然后让他的&lt;code&gt;background-position&lt;/code&gt;每次挪此图高度的一半就可以了。
&lt;img src=&quot;/imgs/income-runway-run.gif&quot; alt=&quot;runway-run&quot; /&gt;
这样跑道也解决了。&lt;/p&gt;
&lt;h3&gt;左边的路牌&lt;/h3&gt;
&lt;p&gt;这个很简单，给她设置绝对定位，然后更改&lt;code&gt;top&lt;/code&gt;值就可以了。
要注意的是，最后一块牌子要在到中间后停在那，加上这个属性就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;animation-fill-mode: forwards;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;animation-fill-mode 属性规定动画在播放之前或之后，其动画效果是否可见。
&lt;strong&gt;取值&lt;/strong&gt;
none： 不改变默认行为。
forwards： 当动画完成后，保持最后一个属性值（在最后一个关键帧中定义）。
backwards： 在 animation-delay 所指定的一段时间内，在动画显示之前，应用开始属性值（在第一个关键帧中定义）。
both： 向前和向后填充模式都被应用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;收益的牌子&lt;/h3&gt;
&lt;p&gt;和左边的路标一样，很简单。就是出现和消失而已。&lt;/p&gt;
&lt;h2&gt;组合&lt;/h2&gt;
&lt;p&gt;将动画的四个部分解决后，剩下的就是将他们组合起来了。
&lt;img src=&quot;/imgs/income-animation-process.png&quot; alt=&quot;process&quot; /&gt;
上图是第一块路标从屏幕出现到消失过程的流程图，其他两块路标的过程只需要将动画延迟相应的时间段就可以了。
这中间还有一个问题，就是让小人和路定点停下来。
这个用 CSS3 貌似实现不了，于是退而求其次，选择了用 js 来控制。通过给它的设置属性&lt;code&gt;webkit-animation-play-state: paused&lt;/code&gt;和&lt;code&gt;webkit-animation-play-state: running&lt;/code&gt;就可让它停下来后继续动了。
最后要在动画完成后，弹出一个弹窗。这个通过检测&lt;code&gt;animationEnd&lt;/code&gt;事件来实现,下面是实现方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* From Modernizr */
function whichAnimationEvent(){
    var t;
    var el = document.createElement(&apos;fakeelement&apos;);
    var animations = {
      &apos;animation&apos;:&apos;animationend&apos;,
      &apos;OAnimation&apos;:&apos;oAnimationEnd&apos;,
      &apos;MozAnimation&apos;:&apos;animationend&apos;,
      &apos;WebkitAnimation&apos;:&apos;webkitAnimationEnd&apos;
    }

    for(t in animations){
        if( el.style[t] !== undefined ){
            return animations[t];
        }
    }
}

/* Listen for a transition! */
var transitionEvent = whichAnimationEvent();
whichAnimationEvent &amp;amp;&amp;amp; e.addEventListener(whichAnimationEvent, function() {
	//doSomething
});

/*
	The &quot;whichTransitionEvent&quot; can be swapped for &quot;animation&quot; instead of &quot;transition&quot; texts, as can the usage :)
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;效果&lt;/h2&gt;
&lt;p&gt;这是最后的完成效果。
&lt;a href=&quot;http://kisnows.com/F2E-practice/running/index.html&quot;&gt;点这儿看动画&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>jQuery插件-changeBg</title><link>https://ab.kisnows.com/zh-CN/2015/02/08/jquery-plugin-changebg/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/02/08/jquery-plugin-changebg/</guid><pubDate>Sun, 08 Feb 2015 04:36:04 GMT</pubDate><content:encoded>&lt;p&gt;把上篇博客中写的 jQuery 题改进了一下，做成了一个新插件&lt;code&gt;ChangeBg&lt;/code&gt;。
可以给某标签下的所有奇数行的子元素(注意：是&lt;em&gt;子元素&lt;/em&gt;，而非&lt;em&gt;后代元素&lt;/em&gt;)设置背景颜色，同时给所有子元素添加鼠标移入移出时更改背景和字体颜色的事件。&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;300&quot; src=&quot;http://jsfiddle.net/cherryoung/5d9ckxLk/embedded/result/&quot; allowfullscreen=&quot;allowfullscreen&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
调用方法：&lt;code&gt;$(&apos;select&apos;).ChangeBg(bgCol, chBgCl, chCol);&lt;/code&gt;或直接&lt;code&gt;$(&apos;select&apos;).ChangeBg();&lt;/code&gt;
函数有三个参数，分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bgCol&lt;/code&gt; 奇数行要设置的背景颜色,默认为&lt;code&gt;&apos;#EEE&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chBgCl&lt;/code&gt; 鼠标移入时背景颜色，默认为&lt;code&gt;&apos;#0066CC&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chCol&lt;/code&gt; 鼠标移入时的字体颜色，默认为&lt;code&gt;&apos;#FFF&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;http://pan.baidu.com/s/1sjqeEBF&quot;&gt;下载请点我&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>elefixed</title><link>https://ab.kisnows.com/zh-CN/2015/01/22/jquery-plugin-elefixed/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/22/jquery-plugin-elefixed/</guid><pubDate>Thu, 22 Jan 2015 04:02:45 GMT</pubDate><content:encoded>&lt;p&gt;写了一款&lt;code&gt;js&lt;/code&gt;插件，可以让一个元素在随着页面向下滚动到达浏览器顶部时，固定在那，向上滚动时又回到原来的状态。
具体介绍可以点击这里查看：&lt;a href=&quot;http://kisnows.com/eleFixed/#&quot;&gt;eleFixed&lt;/a&gt;
本插件提供原生&lt;code&gt;js&lt;/code&gt;和&lt;code&gt;jQuery&lt;/code&gt;两个版本：
使用方法:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原生 js 版：首先在 html 中引入 elefixed.js 文件，然后这样调用它:&lt;code&gt;elefixed(&quot;ele&quot;);&lt;/code&gt;，其中的&lt;code&gt;&quot;ele&quot;&lt;/code&gt;可以换为你要设置元素的 id。&lt;/li&gt;
&lt;li&gt;jQuery 版：直接在元素后面调用该方法即可，像这样&lt;code&gt;$(&quot;ele&quot;).elefixed()&lt;/code&gt;,这里的&lt;code&gt;&quot;ele&quot;&lt;/code&gt;不再像原生版一样需要 id 值了，只要能选中该元素即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kisnows/eleFixed&quot;&gt;下载地址：&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>js函数表达式和函数声明的区别</title><link>https://ab.kisnows.com/zh-CN/2015/02/09/jsfunction-expression-declaration/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/02/09/jsfunction-expression-declaration/</guid><pubDate>Mon, 09 Feb 2015 07:01:09 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;JavaScript&lt;/code&gt;中的两种函数定义方式：&lt;/p&gt;
&lt;h4&gt;函数表达式：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var foo = function(){
	conlose.log(&apos;Message&apos;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;函数声明：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function foo(){
	console.log(&apos;Message&apos;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两种方法都可以定义一个相同的函数，但是却有一些区别：&lt;/p&gt;
&lt;h3&gt;函数表达式中：&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;var foo&lt;/code&gt;和一般的&lt;code&gt;var&lt;/code&gt;定义的变量一样被提到了函数或脚本顶部，但是函数体却还是在原来的地方。所以必须在函数体后调用函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var foo = function(){
	console.log(&apos;Message&apos;);
};
foo() 	// 输出结果为&apos;Message&apos;

foo();
var foo = function(){
	console.log(&apos;Message&apos;);
};		// 会报错TypeError: Property &apos;test&apos; of object #&amp;lt;Object&amp;gt; is not a function

上面的第二段代码其实和下面的是一样，所以会报错

var foo;
foo();
var foo = function(){
	console.log(&apos;Message&apos;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;函数声明中：&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;foo()&lt;/code&gt;函数的整个函数块都被提到了整个函数或脚本的顶部，在整个函数或脚本中都是可见的。也就是说可以在函数定义之前调用函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foo()
function foo(){
	console.log(&apos;Message&apos;)
}			//输出结果为&apos;Message&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以后在实际使用中，要特别注意两者的区别。&lt;/p&gt;
</content:encoded></item><item><title>ECMAScript6学习简记（二）- 数组扩展</title><link>https://ab.kisnows.com/zh-CN/2015/09/02/learning-ecmascript6-two/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/09/02/learning-ecmascript6-two/</guid><pubDate>Wed, 02 Sep 2015 21:39:20 GMT</pubDate><content:encoded>&lt;h1&gt;Array.from()&lt;/h1&gt;
&lt;p&gt;将类数组或可迭代对象转换为真正的数组。&lt;/p&gt;
&lt;h2&gt;可转换的类别&lt;/h2&gt;
&lt;h3&gt;Dom 集合&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;let ps = document.querySelectAll(&quot;p&quot;); //通过Array.from 转换后可以使用forEach方法
Array.from(ps).forEach(function (p, index) {
  &quot;use strict&quot;;
  console.log(p, index);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;arguments 对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function foo() {
  &quot;use strict&quot;;
  var args = Array.from(arguments); //可以替代以前下面这种方式来转换
  var argvs = Array.prototype.slice.call(arguments);
  console.log(argvs, args);
}
foo(1, 2, 3, 4); //[ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h3&gt;拥有&lt;code&gt;length&lt;/code&gt;属性的对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var obj = {
  0: &quot;a&quot;,
  1: &quot;b&quot;,
  2: &quot;c&quot;,
  length: 3,
};
var obj1 = {
  0: &quot;a&quot;,
  1: &quot;b&quot;,
  2: &quot;c&quot;,
};
console.log(Array.from(obj)); //[&apos;a&apos;,&apos;b&apos;,&apos;c&apos;]
console.log(Array.from(obj1)); //[] 没有length属性，无法转换
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，对象的属性值必须是数字，否则转换的数组内容都是&lt;code&gt;undefined&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let obj2 = {
  a: 1,
  b: 2,
  c: 3,
  length: 3,
};
console.log(Array.from(obj2)); //[ undefined, undefined, undefined ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;同时 Array.from()接受第二个参数，作用类似于 map 方法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;console.log(Array.from(obj, (x) =&amp;gt; x + &quot;1&quot;)); //[ &apos;a1&apos;, &apos;b1&apos;, &apos;c1&apos; ]  类似于map方法
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;#Array.of()
可将一组值转换为数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Array.of(3, 12, 2, 3, 24, 2); //[ 3, 12, 2, 3, 24, 2 ]    将一组值转换为数组
Array.of(3, &quot;12&quot;, [2], 3, 24, 2); //[ 3, &apos;12&apos;, [ 2 ], 3, 24, 2 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法的目的是为了弥补数组构造函数的 Array()的不足，因为当参数个数不同时，它会产生不同的效果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Array(8); // [ , , , , , , ,  ]
Array(3, 11); // [3, 11]
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;find()和 findIndex()&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;数组实例的 find 方法，用于找出第一个符合条件的数组成员。它的参数是一个回调函数，所有数组成员依次执行该回调函数，直到找出第一个返回值为 true 的成员，然后返回该成员。如果没有符合条件的成员，则返回 undefined。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;var found = [1, 2, 3, 4, -1].find((n) =&amp;gt; n &amp;lt; 0); //find 找出第一个返回值为true的成员
var founded = [1, 2, 3, 4, -1].find(function (b) {
  return b &amp;gt; 3;
});
console.log(&quot;result:&quot;, found, founded); //result: -1 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;findIndex 返回第一个符合条件的值的&lt;code&gt;index&lt;/code&gt;,如果没找到则返回 -1.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var foundIndex = [1, 2, 324, 5, 34].findIndex(function (value, index, arr) {
  &quot;use strict&quot;;
  return value &amp;gt; 100;
});
console.log(foundIndex); //2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两个方法都可以发现&lt;code&gt;NaN&lt;/code&gt;，而&lt;code&gt;IndexOf&lt;/code&gt;是不能发现的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log([2, 3, 4, NaN].indexOf(NaN)); //-1
console.log([1, 2, NaN].find((value) =&amp;gt; Object.is(NaN, value))); //NaN
console.log([1, 2, 3, 4, NaN].findIndex((value) =&amp;gt; Object.is(NaN, value))); //4
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Array.fill()&lt;/h1&gt;
&lt;p&gt;使用给定值，填充一个数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[1, 2, 3, 4].fill(&quot;a&quot;); //[ &apos;a&apos;, &apos;a&apos;, &apos;a&apos;, &apos;a&apos; ]
new Array(6).fill(3); //[ 3, 3, 3, 3, 3, 3 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fill()还可以接受第二个和第三个参数，用于指定填充的起始位置和结束位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[1, 2, 3, 4]
  .fill(8, 1, 2) //[1,8,3,4]
  [(1, 2, 3, 4)].fill(8, 1, 3); //[ 1, 8, 8, 4 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;entries()，keys()和 values()&lt;/h1&gt;
&lt;p&gt;这三个方法都是用来遍历数组，可以使用&lt;code&gt;for...of&lt;/code&gt;来进行遍历，&lt;code&gt;keys()&lt;/code&gt;用来遍历键名，&lt;code&gt;values()&lt;/code&gt;用来遍历键值，&lt;code&gt;entries()&lt;/code&gt;对键值对遍历。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (let index of [&quot;a&quot;, &quot;b&quot;].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of [&quot;a&quot;, &quot;b&quot;].values()) {
  console.log(elem);
}
// &apos;a&apos;
// &apos;b&apos;

for (let [index, elem] of [&quot;a&quot;, &quot;b&quot;].entries()) {
  console.log(index, elem);
}
// 0 &quot;a&quot;
// 1 &quot;b&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;个人感觉意义不大，和以前的&lt;code&gt;forEach&lt;/code&gt;是一样的功能，有个不同的地方是如果不适用&lt;code&gt;for...of&lt;/code&gt;遍历的话，可以使用&lt;code&gt;next&lt;/code&gt;来手动遍历。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let word = [&quot;h&quot;, &quot;e&quot;, &quot;l&quot;, &quot;l&quot;, &quot;o&quot;];
let entries = word.entries();
console.log(entries.next().value);
[0, &quot;h&quot;];
console.log(entries.next().value);
[1, &quot;e&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;其他方法&lt;/h1&gt;
&lt;p&gt;这些都属于 ES7 要实现的内容&lt;/p&gt;
&lt;h2&gt;include&lt;/h2&gt;
&lt;p&gt;判断一个数组是否包含给定的值，返回的是一个布尔值。&lt;/p&gt;
&lt;h2&gt;数组推导&lt;/h2&gt;
&lt;p&gt;感觉这里跟&lt;code&gt;python&lt;/code&gt;越来越像了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a1 = [1, 2, 3, 4];
var a2 = [for (i of a1) i * 2];
a2 // [2, 4, 6, 8]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;for...of 后面还可以附加 if 语句，用来设定循环的限制条件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];

[for (year of years) if (year &amp;gt; 2000) year];
// [ 2006, 2010, 2014 ]

[for (year of years) if (year &amp;gt; 2000) if(year &amp;lt; 2010) year];
// [ 2006]

[for (year of years) if (year &amp;gt; 2000 &amp;amp;&amp;amp; year &amp;lt; 2010) year];
// [ 2006]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Array.observe()，Array.unobserve()&lt;/h2&gt;
&lt;p&gt;用于监听和数组的变化，制定回调函数。
比较期待这个方法，作用会很大。&lt;/p&gt;
&lt;p&gt;#版权
文章中所有代码皆来自或演变自：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://es6.ruanyifeng.com/#docs/&quot;&gt;阮一峰-ECMAScript 6 入门&lt;/a&gt;,
本文同样遵循&lt;a href=&quot;http://creativecommons.org/licenses/by-nc/4.0/&quot;&gt;署名-非商用许可证&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>前端开发学习笔记一</title><link>https://ab.kisnows.com/zh-CN/2014/12/11/learning-note-1/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2014/12/11/learning-note-1/</guid><pubDate>Thu, 11 Dec 2014 21:29:41 GMT</pubDate><content:encoded>&lt;p&gt;刚刚花了点时间学完 html 和 css，同时粗略的看了一点 javascript。于是看了一遍教程后，就动手做了下面这个页面。（这是原效果图）&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/F2ELearnNote/1-1.png&quot; alt=&quot;1-1&quot; /&gt;
其中有些问题，比如教程中的一些圆角按钮，是用一个三层嵌套然类似&lt;code&gt;&amp;lt;ahref=&apos;&apos;&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;span&amp;gt;HOME&amp;lt;/span&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/a&amp;gt;&lt;/code&gt;后分别给每个标签设置背景图片来实现。
感觉有点得不偿失，其实完全可以用&lt;code&gt;border-radius&lt;/code&gt;加上一个平铺背景来实现，即简单代码看起来又清爽，而且符合 html 的语义化:
&lt;code&gt;&amp;lt;a&amp;gt;HOME&amp;lt;/a&amp;gt;&lt;/code&gt;，然后只需要在 css 中设置&lt;code&gt;a{border-radius:5px}&lt;/code&gt;，就可以了。
实现图如下：
&lt;img src=&quot;/imgs/F2ELearnNote/1-2.png&quot; alt=&quot;1-2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还有一个问题，就是整个页面的主题部分的背景，是一个圆角的有阴影效果的白色图片。教程中同样是用切三张图加上 html 的三层嵌套来实现。其实这里也可以不那么麻烦，用一个圆角边框加上背景白色，再设置一个阴影效果就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#Content{
background:#fff;
box-shadow:0 3px 5px#e4e4e4;
border-radius:25px25px 0 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果图如下：
&lt;img src=&quot;/imgs/F2ELearnNote/1-3.png&quot; alt=&quot;1-3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不过上面的这些解决方法有一个问题，就是都用到了 css3 中的新特性，在较老的浏览器中效果会大打折扣，要考虑到需求在决定用那种方法。
最后放上整体做好后的页面图，和最上面那张比一下，感觉还是不错的。
&lt;img src=&quot;/imgs/F2ELearnNote/1-4.png&quot; alt=&quot;1-4&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>前端开发学习笔记二</title><link>https://ab.kisnows.com/zh-CN/2014/12/25/learning-note-2/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2014/12/25/learning-note-2/</guid><pubDate>Thu, 25 Dec 2014 04:53:46 GMT</pubDate><content:encoded>&lt;p&gt;前几天把之前做的购物网页的 js 效果写了出来。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;如图所示，分别为以下几个效果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;搜索按钮的文字效果&lt;/li&gt;
&lt;li&gt;轮播图&lt;/li&gt;
&lt;li&gt;模拟下拉菜单&lt;/li&gt;
&lt;li&gt;鼠标点击按钮，图片进行左右滚动
&lt;img src=&quot;/imgs/F2ELearnNote/2-1.png&quot; alt=&quot;2-1&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在学习过程中，思想很重要。写一个效果之前，现对其进行设计，然后分析，最后再实现。&lt;/p&gt;
&lt;p&gt;比如很简单的一个轮播图，让图片淡入淡出的切换。它的原理就是，先让所有的图片淡出，然后让当前要显示的图片淡入就可以了。理解了它的工作原理，写起来就很简单了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; shop.app.Banner=function(){            //Banner轮播图效果
     var ad=document.getElementById(&quot;ad&quot;);
     var ul=ad.getElementsByTagName(&quot;ul&quot;)[0];
     var li=ul.getElementsByTagName(&quot;li&quot;);

     var iNow=0;
     var timer=setInterval(auto,3000);

     function auto()       //自动切换效果
     {
         if(iNow==li.length-1)
         {
             iNow=0;
         }
         else{
             iNow++;
         }
         for(var i=0;i&amp;lt;li.length;i++)
         {
             shop.ui.fadeOut(li[i]);
         }
         shop.ui.fadeIn(li[iNow]);
     }
 };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一个就是，在写整个页面的代码的时候，要对齐进行分层。比如分为作为底层的工具层，多处都会用到的 UI 层，以及最终的应用层。这样写可以减少代码的重复，使文档看起来结构清晰，也易于别人的理解和后期的维护。在这里，我跟随教程采用了命名空间的方法来编写这个页面。整个代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.onload=function(){        //调用函数
    shop.app.Banner();
    shop.app.search();
    shop.app.sort();
    shop.app.Run();
};
var shop={};        //命名空间
shop.tools={};
shop.tools.getByClass=function(Par,Cla)        //获取对应class的对象
{
    var allEle=Par.getElementsByTagName(&quot;*&quot;);
    var arr=[];
    for(i=0;i&amp;lt;allEle.length;i++)
    {
        if(allEle[i].className==Cla)
        {
            arr.push(allEle[i]);
        }
    }
    return arr;
};
shop.tools.getStyle=function(obj,attr)        //获取对象属性
{
    if(obj.currentStyle)
    {
        return obj.currentStyle[attr];
    }
    else
    {
        return getComputedStyle(obj,false)[attr];
    }
};

shop.ui={};
shop.ui.fadeIn=function(obj)        //淡入效果
{
    var attr=shop.tools.getStyle(obj,&apos;opacity&apos;);
    if(attr==1)
    {
        return false;
    };
    var value=0;
    clearInterval(obj.timer);
    obj.timer=setInterval(function(){
        var speed=5;
        if(value==100){
            clearInterval(obj.timer);
        }
        else
        {
            value+=speed;
            obj.style.opacity=value/100;
        }
    },30);
};
shop.ui.fadeOut=function(obj)        //淡出效果
{
    var attr=shop.tools.getStyle(obj,&apos;opacity&apos;);
    if(attr==0)
    {
        return false;
    };
    var value=100;
    obj.timer=setInterval(function(){
        var speed=-5;
        if(value==0)
        {
            clearInterval(obj.timer);
        }
        else
        {
            value+=speed;
            obj.style.opacity=value/100;
        };
    },30);
};
shop.ui.textChange=function(obj,str){        //清空默认文字
    obj.onfocus=function(){
        if(this.value==str){
            this.value=&quot;&quot;;
        }
    };
    obj.onblur=function(){
        if(this.value==&quot;&quot;){
            this.value=str;
        }
    };
};
shop.ui.moveLeft=function(obj,old,now){        //对象位置左移
    clearInterval(obj.timer);
    obj.timer=setInterval(function(){
        var speed=(now-old)/15;
        speed=Math.round(speed);
        if(now==old){
            clearInterval(obj.timer);
        }
        else{
            old+=speed;
            obj.style.left=old+&quot;px&quot;;
        }
    },30);
};
shop.app={};
shop.app.Banner=function(){            //Banner轮播图效果
    var ad=document.getElementById(&quot;ad&quot;);
    var ul=ad.getElementsByTagName(&quot;ul&quot;)[0];
    var li=ul.getElementsByTagName(&quot;li&quot;);

    var iNow=0;
    var timer=setInterval(auto,3000);

    function auto()
    {
        if(iNow==li.length-1)
        {
            iNow=0;
        }
        else{
            iNow++;
        }
        for(var i=0;i&amp;lt;li.length;i++)
        {
            shop.ui.fadeOut(li[i]);
        }
        shop.ui.fadeIn(li[iNow]);
    }
};
shop.app.search=function(){
    var s1=document.getElementById(&quot;search1&quot;);
    var s2=document.getElementById(&quot;search2&quot;);
    shop.ui.textChange(s1,&quot;Search website&quot;);
    shop.ui.textChange(s2,&quot;Search website&quot;);
};
shop.app.sort=function(){            //模拟下拉菜单
    var sor=document.getElementById(&quot;sort&quot;);
    var sordd=sor.getElementsByTagName(&quot;dd&quot;);
    var sorul=sor.getElementsByTagName(&quot;ul&quot;);
    var sorh4=sor.getElementsByTagName(&quot;h4&quot;);
    for(var i=0;i&amp;lt;sordd.length;i++){
        sordd[i].index=i;
        sordd[i].onclick=function(ev){
            var ev=ev||window.event;
            var This=this;
            for(var n=0;n&amp;lt;sorul.length;n++){
                sorul[n].style.display=&quot;none&quot;;
            }
            sorul[this.index].style.display=&quot;block&quot;;

            document.onclick=function(){
                sorul[This.index].style.display=&apos;none&apos;;
            };
            ev.cancelBubble=true;
        }
    };

    for(var i=0;i&amp;lt;sorul.length;i++){
        sorul[i].index=i;
        (function(ul){
            var sorli=ul.getElementsByTagName(&quot;li&quot;);
            for(var i=0;i&amp;lt;sorli.length;i++){
                sorli[i].onmouseover=function(){
                    this.className=&quot;active&quot;;
                };
                sorli[i].onmouseout=function(){
                    this.className=&quot;&quot;;
                };
                sorli[i].onclick=function(ev){
                    var ev = ev || window.event;
                    sorh4[this.parentNode.index].innerHTML=this.innerHTML;
                    ev.cancelBubble = true;
                    this.parentNode.style.display=&quot;none&quot;;
                };
            }
        })(sorul[i]);
    };
};
shop.app.Run=function(){            //点击左移效果
    var slide=document.getElementById(&apos;slide&apos;);
    var ul=slide.getElementsByTagName(&apos;ul&apos;)[0];
    var li=ul.getElementsByTagName(&apos;li&apos;);

    var pre=shop.tools.getByClass(slide,&quot;pre&quot;)[0];
    var next=shop.tools.getByClass(slide,&quot;next&quot;)[0];

    var iNow=0;

    ul.innerHTML+=ul.innerHTML;
    ul.style.width=li.length*li[0].offsetWidth+&quot;px&quot;;

    pre.onclick = function(){
        if(iNow == 0){
            iNow = li.length/2;
            ul.style.left = -ul.offsetWidth/2 + &apos;px&apos;;
        }
        shop.ui.moveLeft(ul,-iNow*li[0].offsetWidth,-(iNow-1)*li[0].offsetWidth);
        iNow--;
    };

    next.onclick = function(){
        if(iNow == li.length/2){
            iNow = 0;
            ul.style.left = 0;
        }
        shop.ui.moveLeft(ul,-iNow*li[0].offsetWidth,-(iNow+1)*li[0].offsetWidth);
        iNow++;
    };
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>前端开发学习笔记三</title><link>https://ab.kisnows.com/zh-CN/2014/12/26/learning-note-3/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2014/12/26/learning-note-3/</guid><pubDate>Fri, 26 Dec 2014 22:55:24 GMT</pubDate><content:encoded>&lt;p&gt;把慕课的首页临摹了一下，感觉还不错。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;顺便推荐一个前端编辑器，Adobe 出的 Brackets，可以实时预览，再也不用老是切换加 f5 了，可以极大的提高编写速度。
&lt;img src=&quot;/imgs/F2ELearnNote/3-1.png&quot; alt=&quot;3-1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下载地址：&lt;/p&gt;
&lt;p&gt;http://brackets.io/&lt;/p&gt;
</content:encoded></item><item><title>前端开发学习笔记四</title><link>https://ab.kisnows.com/zh-CN/2014/12/31/learning-note-4/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2014/12/31/learning-note-4/</guid><pubDate>Wed, 31 Dec 2014 00:09:34 GMT</pubDate><content:encoded>&lt;p&gt;固定导航栏根据页面滚动条距离来自动定位的效果，其中心思想就是通过判定当前滚动条的值与页面中相应内容值大小的关系来选择。js 代码如下（用到了 jQuery 库）：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    $(document).ready(function () {
        $(window).scroll(function () {
            var top = $(window).scrollTop();
            var menu = $(&quot;#menu&quot;);
            var items = $(&quot;#content&quot;).find(&quot;.item&quot;);
            //滚动条发生滚动时，要获取相应的值。
            var currentId = &quot;&quot;;
            //让导航菜单实现在滚动条滚动的时候自动设置焦点
            items.each(function () {
                var This = $(this);
                var itemTop = This.offset().top;
                if (top &amp;gt; itemTop - 200) {
                    currentId = This.attr(&quot;id&quot;);
                } else {
                    return false;
                }
            })
            //给相应楼层的a 设置 current，取消其他链接的current
            var currentLink = menu.find(&quot;.current&quot;);
            if (currentId &amp;amp;&amp;amp; currentId != &quot;#&quot;+currentLink.attr(&quot;href&quot;)) {
                currentLink.removeClass(&quot;current&quot;);
                menu.find(&quot;[href=#&quot; + currentId + &quot;]&quot;).addClass(&quot;current&quot;);
            }
        })
    })
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>前端开发学习笔记五</title><link>https://ab.kisnows.com/zh-CN/2015/01/01/learning-note-5/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/01/learning-note-5/</guid><pubDate>Thu, 01 Jan 2015 23:32:23 GMT</pubDate><content:encoded>&lt;p&gt;幽灵按钮最近很流行，就寻思着做了一个，它有如下一些效果。
&lt;img src=&quot;/imgs/F2ELearnNote/5-1.gif&quot; alt=&quot;5-1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;其中按钮上的线条和图标的旋转完全用&lt;code&gt;CSS3&lt;/code&gt;中的&lt;code&gt;transtion&lt;/code&gt;和&lt;code&gt;transform&lt;/code&gt;来实现，其中图标的旋转效果实现过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.link .img{
    display: block;
    width:180px;
    height:180px;
    margin-bottom: 20px;
    -webkit-transition:all 0.5s cubic-bezier(0, .18, 0, .97);　　　　//为了兼容chrome和Safari
    transition:all 0.5s cubic-bezier(0, .18, 0, .97);　　　　　　　　//定义动画属性，包括要变化的属性，时间，以及变化曲线（linear、ease、ease-out或者自定义等）
}
.mis .img{
    background: url(../images/mission.png) no-repeat  center 0;
}
.link .img:hover{
    -webkit-transform: rotate(360deg) scale(1.2);
    -ms-transform: rotate(360deg) scale(1.2);
    transform: rotate(360deg) scale(1.2);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按钮的线条效果与上面的方法类似，而按钮上的提示文字使用 js 来实现的。&lt;/p&gt;
&lt;p&gt;主要原理就是通过 js 获取按钮上相应的 title（即要显示的文字），然后将其增加在提示框内，在通过判定当前按钮的位置来设置提示框要出现的相应位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$(function(){　　　　　　　　　　
    $(&quot;.link .btn&quot;).hover(function(){
        var title = $(this).attr(&quot;data-title&quot;);
        $(&quot;.tip em&quot;).text(title);
        var left = $(this).position().left;
        var dis = Math.abs(($(&quot;.tip&quot;).outerWidth()-$(this).outerWidth())/2);
        var now = left -dis;
        console.log(&quot;title=&quot;+title+&quot;  &quot;,&quot;left=&quot;+left,&quot;dis=&quot;,dis,&quot;now=&quot;+now,&quot;this&quot;+$(&quot;.tip&quot;).outerWidth());
        $(&quot;.tip&quot;).css({&quot;left&quot;:now+&quot;px&quot;}).animate({&quot;top&quot;:130,&quot;opacity&quot;:1},300);
    },
    function(){
        $(&quot;.tip&quot;).animate({&quot;top&quot;:100,&quot;opacity&quot;:0},300);
    }
    )
})
//用了jQuery框架中的hover(function(),function())函数，前一个函数为鼠标放上去的动作，后一个为鼠标离开时的动作。
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>前端开发学习笔记六</title><link>https://ab.kisnows.com/zh-CN/2015/01/13/learning-note-6/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/13/learning-note-6/</guid><pubDate>Tue, 13 Jan 2015 20:05:27 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;终于搞定了一个电商整站临摹，共四张页面，实现了基本的交互功能，同时也应用了&lt;code&gt;less&lt;/code&gt;这个新学的&lt;code&gt;css&lt;/code&gt;预处理语言。之前也做过一个电商首页&lt;a href=&quot;http://kisnows.com/F2E-practice/shopping/index.html&quot;&gt;点这儿&lt;/a&gt;。但是因为只是单页面，处理起来比较简单。但是在制作这个整站的过程中，更加深刻的理解一些之前学过的内容：比如样式结构的分离，&lt;code&gt;js&lt;/code&gt;设计时的分层思想等。&lt;a href=&quot;http://kisnows.com/F2E-practice/shopping-page/home.html&quot;&gt;点击这里&lt;/a&gt;查看网页效果，或者下面有效果图(图好丑)：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/F2ELearnNote/whole.gif&quot; alt=&quot;效果图&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;结构&lt;/h3&gt;
&lt;p&gt;首先说结构，拿到设计图时，先要进行分析。比如这四张页面，是典型的上中下结构，其除了中间正文部分，头部和脚部都是完全一样的，而且除了商品详细页面外，其他的正文部分都分为左右结构，可先将这部分相同的结构写好后作为模版，在写其他页面时只需要在模版的基础上填充内容就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;div id=&quot;head&quot;&amp;gt;&amp;lt;!-- 完全一样的 --&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div id=&quot;content&quot;&amp;gt;&amp;lt;!-- 部分一样 --&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;div id=&quot;footer&quot;&amp;gt;&amp;lt;!-- 完全一样 --&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;样式&lt;/h3&gt;
&lt;p&gt;因为在写样式过程中用了&lt;code&gt;less&lt;/code&gt;这个 css 高级语言，写起来方便了许多，不用再去将一些重复的东西写上一遍又一遍，省去诸多麻烦。我对 Css 也惊醒了分层，先针对模版写一份样式名为&lt;code&gt;mian.js&lt;/code&gt;，接下在针对不同的页面写不同的样式，最后在页面中同时引入进来就可以了。其实还可以再进一步细分，比如针对头部和页脚的，banner 部分的，页码部分的等等。&lt;/p&gt;
&lt;h3&gt;逻辑&lt;/h3&gt;
&lt;p&gt;最后是&lt;code&gt;js&lt;/code&gt;，在分析中发现，不同的页面中用的许多的逻辑都是一样的、比如几个 banner 部分的自动切换、模拟下拉菜单等。这时，将整个&lt;code&gt;js&lt;/code&gt;代码分为以下三个层来写：&lt;/p&gt;
&lt;h4&gt;工具层&lt;/h4&gt;
&lt;p&gt;这里是一些最基础的东西，比如获取元素的 class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shop.tools = {};
shop.tools.getByClass = function(par, cla) {};//获取相应class元素
shop.tools.onmouseover = function(ele,eles,select){};//参数为子元素，和父元素，最终为当前元素添加active状态
shop.tools.onclick = function(ele,eles){};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;用户界面层&lt;/h4&gt;
&lt;p&gt;这里写一下会多处用到的 UI 层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shop.ui = {};
shop.ui.fadeIn = function(ele, time, opacity) {};
shop.ui.fadeOut = function(ele, time, opacity) {};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;应用层&lt;/h4&gt;
&lt;p&gt;这里就是页面会直接用的交互层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shop.app = {};
shop.app.tip = function() {};
shop.app.banner = function() {};
shop.app.chose = function(){
	var specification = document.getElementById(&apos;specification&apos;);
	var color = document.getElementById(&apos;color&apos;);
	var speSpans = specification.getElementsByTagName(&apos;span&apos;);
	var colorSpans = color.getElementsByTagName(&apos;span&apos;)
	for (var i = speSpans.length - 1; i &amp;gt;= 0; i--) {
		shop.tools.onmouseover(speSpans[i],speSpans);
		shop.tools.onclick(speSpans[i],speSpans);
	}
	for (var j = colorSpans.length - 1; j &amp;gt;= 0; j--) {
		shop.tools.onmouseover(colorSpans[j],colorSpans);
		shop.tools.onclick(colorSpans[j],colorSpans);
	};
}
shop.app.chose_screen = function(){
	var top = document.getElementById(&apos;top&apos;);
	var a = top.getElementsByTagName(&apos;a&apos;);
	for (var i = a.length - 1; i &amp;gt;= 0; i--) {
		if (a[i].innerHTML == &quot;更多&quot;) {continue}
		else{
			shop.tools.onmouseover(a[i],a,&quot;active&quot;);
		}
	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码太多就不全贴了，可以看到在这里会多次用到之前 tools 和 ui 里面的函数，这样就提高了代码的利用率，使结构看起来清晰，易于他人理解和维护。最后在页面中，哪里用到，哪里直接引用就可以了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在这个过程中也认识到自己很多方面还是不够熟练，需要继续加油啊。&lt;/p&gt;
</content:encoded></item><item><title>learnyounode-functional-javascript</title><link>https://ab.kisnows.com/zh-CN/2015/09/24/learnyounode-functional-javascript/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/09/24/learnyounode-functional-javascript/</guid><pubDate>Thu, 24 Sep 2015 03:53:33 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/imgs/learnyounode-functional-javascript.png&quot; alt=&quot;fun&quot; /&gt;
坑爹的 CMD 不知怎么回事，在做到倒数第三道题后突然罢工了，不能选择题目，公司和自己的电脑都是这样。
于是最后三道题还是放弃了，也没精力去找那三道题到底是什么了，就这样吧。&lt;/p&gt;
&lt;p&gt;做题的过程中有个需要注意的地方，也是在 js 中一个很常用的方法。
那就是被 setTimeout 延迟的函数会等前面函数彻底执行完以后，才会执行,也就是说可以阻塞 js 的进程。如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function repeat(operation, num) {
  // modify this so it can be interrupted
  if (num &amp;lt;= 0) return;
  operation();
  setTimeout(function () {
    return repeat(operation, --num);
  }, 0);
}
module.exports = repeat;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Operation 是一个操作很费时间的函数，如果不用 setTimeout 延迟后面的递归调用的话，就会造成 repeat 函数已经执行完了，但是含有很多个 operation 没有执行完的情况。&lt;/p&gt;
&lt;p&gt;其它的到没有什么了，所有代码都放在&lt;a href=&quot;https://github.com/kisnows/nodeschool/tree/master/functional-javascript&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>learnyounode</title><link>https://ab.kisnows.com/zh-CN/2015/06/11/learnyounode/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/06/11/learnyounode/</guid><pubDate>Thu, 11 Jun 2015 22:41:30 GMT</pubDate><content:encoded>&lt;p&gt;Learn You The Node.js For Much Win!
&lt;img src=&quot;/imgs/learnyounode.png&quot; alt=&quot;learnyounode&quot; /&gt;
花了几天业余时间做完这个 &lt;code&gt;Node.js&lt;/code&gt; 入门的题目，梳理一下知识点。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;HELLO WORLD&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个程序，在终端（标准输出 stdout）打印出 &quot;HELLO WORLD&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;学习一门语言的最好方法就是先写一个 Hello Wolrd 程序。这道题很简单，每个人都会。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&quot;HELLO WORLD&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;BABY STEPS&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个简单的程序，使其能接收一个或者多个命令行参数，并且在终端（标准输出 stdout）中打印出这些参数的总和。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接受的参数可以通过 &lt;code&gt;process.argv&lt;/code&gt; 来访问，它是一个数组。前两位是固定的，第一位是 &lt;code&gt;node&lt;/code&gt; ，第二位是文件的路径，后面就是程序输入的内容的了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;首先，请先编写一个包含如下带简易代码的程序来熟悉一下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;console.log(process.argv)
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;通过执行命令 node program.js 并在后面多加几个参数，来运行我们的程序，比如：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;$ node program.js 1 2 3
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;这样，你就会得到这样一个数组：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;[ &apos;node&apos;, &apos;/path/to/your/program.js&apos;, &apos;1&apos;, &apos;2&apos;, &apos;3&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么有了这些知识后，程序写起来也就很简单了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var result = 0;
process.argv.forEach(function(el,index){
	if (index&amp;gt;1) {
		result += +el;
	}
});
console.log(result);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;MY FIRST I/O!&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个程序，执行一个同步的文件系统操作，读取一个文件，并且在终端（标准输出 std
out）打印出这个文件中的内容的行数。类似于执行 cat file | wc -l 这个命令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;所要读取的文件的完整路径会在命令行第一个参数提供。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先要对一个文件进行操作，需要用到 &lt;code&gt;fs&lt;/code&gt; 这个模块，我们可以用 &lt;code&gt;var fs = require(&apos;fs&apos;);&lt;/code&gt; 来引入。然后可以通过 &lt;code&gt;fs&lt;/code&gt; 这个变量来访问整个 &lt;code&gt;fs&lt;/code&gt; 模块了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 fs 中，所有的同步（或者阻塞）的操作文件系统的方法名都会以 &apos;Sync&apos; 结尾。要读取
一个文件，你将需要使用 fs.readFileSync(&apos;/path/to/file&apos;) 方法。这个方法会返回一
个包含文件完整内容的 Buffer 对象。
Buffer 对象是 Node 用来高效处理数据的方式，无论该数据是 ascii 还是二进制文件，或
者其他的格式。Buffer 可以很容易地通过调用 toString() 方法转换为字符串。如：var s
tr = buf.toString()。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样我们要做的就是先引入 &lt;code&gt;fs&lt;/code&gt; 模块，然后读取输入的数据。我们的目的是计算行数，可以知道，每行末尾其实都是有一个 &lt;code&gt;\n&lt;/code&gt; 的，我们只要计算 &lt;code&gt;\n&lt;/code&gt; 的个数就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var fs = require(&apos;fs&apos;);
var data = fs.readFileSync(process.argv[2]);
var str = data.toString();
var array = str.split(&apos;\n&apos;);
console.log(array.length-1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;MY FIRST ASYNC I/O!&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个程序，执行一个异步的对文件系统的操作：读取一个文件，并且在终端（标准输出
stdout）打印出这个文件中的内容的行数。类似于执行 cat file | wc -l 这个命令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;所要读取的文件的完整路径会在命令行第一个参数提供。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这道题上一道几乎一样，只是变成了异步的读取。这是就需要用到 &lt;code&gt;fs.readFile()&lt;/code&gt; ，而不是 &lt;code&gt;fs.readFileSync()&lt;/code&gt; 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;记住，我们习惯中的 Node.js 回调函数都有像如下所示的特征：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;function callback (err, data) { /_ ... _/ }&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;所以，你可以通过检查第一个参数的真假值来判断是否有错误发生。如果没有错误发生，你
会在第二个参数中获取到一个 Buffer 对象。和 readFileSync() 一样，你可以传入 &apos;utf8
&apos; 作为它的第二个参数，然后把回调函数作为第三个参数，这样，你得到的将会是一个字符
串，而不是 Buffer。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;var fs = require(&apos;fs&apos;);
var data = fs.readFile(process.argv[2],function(err,data){
    if(err){
        throw console.error(err);
    }
    var list = data.toString().split(&apos;\n&apos;);
    console.log(list.length-1);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;FILTERED LS&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个程序来打印出指定目录下的文件列表，并且以特定的文件名扩展名来过滤这个列表
。这次会有两个参数提供给你，第一个是所给的文件目录路径（如：path/to/dir），第二
个参数则是需要过滤出来的文件的扩展名。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;举个例子：如果第二个参数是 txt，那么你需要过滤出那些扩展名为 .txt 的文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，第二个参数将不会带有开头的 .。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你需要在终端中打印出这个被过滤出来的列表，每一行一个文件。另外，你必须使用异步的
I/O 操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个也很简单，把文件名通过 &lt;code&gt;.&lt;/code&gt; 来分成两个部分，后面的就是后戳名了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var fs = require(&apos;fs&apos;);
var path = process.argv[2];
var extname = process.argv[3];

fs.readdir(path,function(err,list){
    if(err){
        throw err;
    }else{
        list.forEach(function(file){
            if(file.split(&apos;.&apos;)[1] === extname){
                console.log(file);
            }
        });
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;MAKE IT MODULAR&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这个问题和前面一个一样，但是这次会介绍模块的概念。你将需要创建两个文件来解决这个
问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个程序来打印出所给文件目录的所含文件的列表，并且以特定的文件名后缀来过滤这
个列表。这次将会提供两个参数，第一个参数是要列举的目录，第二个参数是要过滤的文件
扩展名。你需要在终端中打印出过滤出来的文件列表（一个文件一行）。此外，你必须使用
异步 I/O。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你得编写一个模块 文件去做大部分的事情。这个模块必须导出（export）一个函数，这个
函数将接收三个参数：目录名、文件扩展名、回调函数，并按此顺序传递。文件扩展名必须
和传递给你的程序的扩展名字符串一模一样。也就是说，请不要把它转成正则表达式或者加
上 &quot;.&quot; 前缀或者做其他的处理，而是直接传到你的模块中去，在模块中，你可以做一些处
理来使你的过滤器能正常工作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;这个回调函数必须以 Node 编程中惯用的约定形式（err, data）去调用。这个约定指明了
，除非发生了错误，否则所传进去给回调函数的第一个参数将会是 null，第二个参数才会
是你的数据。在本题中，这个数据将会是你过滤出来的文件列表，并且是以数组的形式。如
果你接收到了一个错误，如：来自 fs.readdir() 的错误，则必须将这个错误作为第一个，
也是唯一的参数传递给回调函数，并执行回调函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你绝对不能直接在你的模块文件中把结果打印到终端中，你只能在你的原始程序文件中编写
打印结果的代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;当你的程序接收到一些错误的时候，请简单的捕获它们，并且在终端中打印出相关的信息&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;这里有四则规定，你的模块必须遵守：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;导出一个函数，这个函数能准确接收上述的参数。&lt;/li&gt;
&lt;li&gt;当有错误发生，或者有数据的时候，准确调用回调函数。&lt;/li&gt;
&lt;li&gt;不要改变其他的任何东西，比如全局变量或者 stdout。&lt;/li&gt;
&lt;li&gt;处理所有可能发生的错误，并把它们传递给回调函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;遵循一些约定的好处是，你的模块可以被任何其他也遵守这些约定的人所使用。因此，这里
你新建的模块可以被其他 learnyounode 的学习者使用，或者拿去验证，都会工作得很好。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先是我们的模块文件，可以从上一题改造过来,并命名为 &apos;program-6-require.js&apos; 。它要做的事情就是返回一个过滤后的文件名的数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var fs = require(&apos;fs&apos;);
module.exports = function(path, extname, callback) {
    fs.readdir(path, function(err, list) {
        if (err) {
            return callback(err, null);
        } else {
            list = list.filter(function(file) {
                return file.split(&apos;.&apos;)[1] === extname;
            });
        }
        callback(null, list);
    });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在程序文件中调用这个模块，将模块中返回数组的内容打印出来就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var mymodule = require(&apos;./program-6-require&apos;);
var path = process.argv[2];
var extname = process.argv[3];

mymodule(path,extname,function(err,files){
    if(err){
        return console.log(err);
    }
    files.forEach(function(file){
        console.log(file);
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP CLIENT&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个程序来发起一个 HTTP GET 请求，所请求的 URL 为命令行参数的第一个。然后将 每一个 &quot;data&quot; 事件所得的数据，以字符串形式在终端（标准输出 stdout）的新的一行打印出来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;完成这个练习，你需要使用 Node.js 核心模块之一：http。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;http.get() 方法是用来发起简单的 GET 请求的快捷方式，使用这个方法可以一定程度简化 你的程序。http.get() 的第一个参数是你想要 GET 的 URL，第二个参数则是回调函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;与其他的回调函数不同，这个回调函数有如下这些特征：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;function callback (response) { /_ ... _/ }&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;response 对象是一个 Node 的 Stream 类型的对象，你可以将 Node Stream 当做一个会触 发一些事件的对象，其中我们通常所需要关心的事件有三个： &quot;data&quot;，&quot;error&quot; 以及 &quot;end &quot;。你可以像这样来监听一个事件：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;response.on(&quot;data&quot;, function (data) { /_ ... _/ })&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&apos;data&apos; 事件会在每个数据块到达并已经可以对其进行一些处理的时候被触发。数据块的大 小将取决于数据源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你从 http.get() 所获得的 response 对象/Stream 还有一个 setEncoding() 的方法。如 果你调用这个方法，并为其指定参数为 utf8，那么 data 事件中会传递字符串，而不是标 准的 Node Buffer 对象，这样，你也不用再手动将 Buffer 对象转换成字符串了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有了上面那些提示，实现起来还是很容易的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var http = require(&apos;http&apos;);

http.get(process.argv[2],function(response){
    response.setEncoding(&apos;utf8&apos;);
    response.on(&quot;data&quot;,function(data){
        console.log(data);
    });
    response.on(&quot;error&quot;,function(err){
        throw err;
    });
    response.on(&quot;end&quot;,function(){
        // console.log(date);
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP COLLECT&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个程序，发起一个 HTTP GET 请求，请求的 URL 为所提供给你的命令行参数的第一
个。收集所有服务器所返回的数据（不仅仅包括 &quot;data&quot; 事件）然后在终端（标准输出 std
out）用两行打印出来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你所打印的内容，第一行应该是一个整数，用来表示你所收到的字符串内容长度，第二行则
是服务器返回给你的完整的字符串结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们要做的就是先把所有的数据都收集起来，然后在请求结束后把他们凭借起来就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var http = require(&apos;http&apos;);
var url = process.argv[2];

http.get(url,function(response){
    var list = [];
    // response.setEncoding(&apos;utf8&apos;);
    response.on(&quot;data&quot;,function(data){
        list.push(data.toString());
    });
    response.on(&quot;error&quot;,function(err){
        throw err;
    });
    response.on(&quot;end&quot;,function(){
        var str = &apos;&apos;;
        list.forEach(function(s){
            str += s;
        });
        console.log(str.length);
        console.log(str);
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JUGGLING ASYNC&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这次的问题和之前的问题（HTTP 收集器）很像，也是需要使用到 http.get() 方法。然而
，这一次，将有三个 URL 作为前三个命令行参数提供给你。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你需要收集每一个 URL 所返回的完整内容，然后将它们在终端（标准输出 stdout）打印出
来。这次你不需要打印出这些内容的长度，仅仅是内容本身即可（字符串形式）；每个 URL
对应的内容为一行。重点是你必须按照这些 URL 在参数列表中的顺序将相应的内容排列打
印出来才算完成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里有几个问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 要收集每个 url 返回的完整内容
2 判断是否所有请求完成
3 按 url 顺序打印出所获得内容
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第一个问题&lt;/h3&gt;
&lt;p&gt;这个很简单，和前面的题一样。&lt;/p&gt;
&lt;h3&gt;第二个问题&lt;/h3&gt;
&lt;p&gt;要判断所有请求是否都完成了，我们需要一个列表来存放发的状态，如果它的请求完成了，就给他标注出来。&lt;/p&gt;
&lt;h3&gt;第三个问题&lt;/h3&gt;
&lt;p&gt;要按照 url 的顺序打印出内容，那么我们在保存数据的时候就要记录下它的 index ，最后按照这个 index 打印就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var http = require(&apos;http&apos;);
var urls = process.argv.slice(2,process.argv.length);
var datas = [];
var end = [];
var is = false;
urls.forEach(function(url,index){
    http.get(url,function(response){
        var list = [];
        response.setEncoding(&apos;utf8&apos;);
        response.on(&quot;data&quot;,function(data){
            list.push(data);
        });
        response.on(&quot;error&quot;,function(err){
            throw err;
        });
        response.on(&quot;end&quot;,function(){
            var str = &apos;&apos;;
            list.forEach(function(s){
                str += s;
            });
            datas[index] = str;
            end.push(true);
            if (isEnd()) {
                for (var i = 0; i &amp;lt; datas.length; i++) {
                    console.log(datas[i]);
                }
            }
        });
    });

});
function isEnd(){
    if (end.length === urls.length) {
        end.forEach(function(blo){
            if(blo){
                is = true;
            }else{
                is = false;
            }
        });
    }
    return is;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这道题，我的方法略显繁琐。下面附上官方答案，以并参考。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var http = require(&apos;http&apos;)
var bl = require(&apos;bl&apos;)
var results = []
var count = 0

function printResults () {
  for (var i = 0; i &amp;lt; 3; i++)
    console.log(results[i])
}

function httpGet (index) {
  http.get(process.argv[2 + index], function (response) {
    response.pipe(bl(function (err, data) {
      if (err)
        return console.error(err)

      results[index] = data.toString()
      count++

      if (count == 3)
        printResults()
    }))
  })
}

for (var i = 0; i &amp;lt; 3; i++)
  httpGet(i)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TIME SERVER&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个 TCP 时间服务器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你的服务器应当监听一个端口，以获取一些 TCP 连接，这个端口会经由第一个命令行参数
传递给你的程序。针对每一个 TCP 连接，你都必须写入当前的日期和 24 小时制的时间，如
下格式：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;YYYY-MM-DD hh:mm&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;然后紧接着是一个换行符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;月份、日、小时和分钟必须用零填充成为固定的两位数：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;2013-07-06 17:42&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;官方的提示：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这次练习中，我们将会创建一个 TCP 服务器。这里将不会涉及到任何 HTTP 的事情，因此
我们只需使用 net 这个 Node 核心模块就可以了。它包含了所有的基础网络功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;net 模块拥有一个名叫 net.createServer() 的方法，它会接收一个回调函数。和 Node 中
其他的回调函数不同，createServer() 所用的回调函数将会被调用多次。你的服务器每收
到一个 TCP 连接，都会调用一次这个回调函数。这个回调函数有如下特征：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;function callback (socket) { /_ ... _/ }&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;net.createServer() 也会返回一个 TCP 服务器的实例，你必须调用 server.listen(portN
umber) 来让你的服务器开始监听一个特定的端口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;一个典型的 Node TCP 服务器将会如下所示：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;var net = require(&apos;net&apos;)
var server = net.createServer(function (socket) {
// socket 处理逻辑
})
server.listen(8000)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;记住，请一定监听由第一个命令行参数指定的端口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;socket 对象包含了很多关于各个连接的信息（meta-data），但是它也同时是一个 Node 双
工流（duplex Stream），所以，它即可以读，也可以写。对这个练习来说，我们只需要对
socket 写数据和关闭它就可以了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 socket.write(data) 可以写数据到 socket 中，用 socket.end() 可以关闭一个 s
ocket。另外， .end() 方法也可以接收一个数据对象作为参数，因此，你可简单地使用 so
cket.end(data) 来完成写数据和关闭两个操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有了官方提示，一切都很简单了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var net = require(&apos;net&apos;);

var port = process.argv[2];
var getTime = function(){
    var date = new Date(),
        year = date.getFullYear(),
        month = formate(date.getMonth()+1),
        day = formate(date.getDate()),
        hour = formate(date.getHours()),
        minute = formate(date.getMinutes());
    var time = year + &quot;-&quot;+month+&quot;-&quot;+day+&quot; &quot;+hour+&quot;:&quot;+minute;
    function formate(time){
        return (time.toString().length&amp;gt;1?&apos;&apos;:&apos;0&apos;)+time;
    }
    return time;
};

var server = net.createServer(function(socket){
    socket.write(getTime()+&apos;\n&apos;);
    socket.end();
});
server.listen(port);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP FILE SERVER&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个 HTTP 文件 服务器，它用于将每次所请求的文件返回给客户端。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你的服务器需要监听所提供给你的第一个命令行参数所制定的端口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;同时，第二个会提供给你的程序的参数则是所需要响应的文本文件的位置。在这一题中，你
必须使用 fs.createReadStream() 方法以 stream 的形式作出请求相应。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里需要用到 &lt;code&gt;http&lt;/code&gt; 的 &lt;code&gt;http.createServer()&lt;/code&gt; 方法，和前面的 &lt;code&gt;net&lt;/code&gt; 类似，不同的是它使用 HTTP 协议进行通讯。还有下面这个&lt;code&gt;fs&lt;/code&gt;的方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;fs 这个核心模块也含有一些用来处理文件的流式（stream） API。你可以使用 fs.createR
eadStream() 方法来为命令行参数指定的文件创建一个 stream。这个方法会返回一个 stre
am 对象，该对象可以使用类似 src.pipe(dst) 的语法把数据从 src 流传输(pipe) 到 dst
流中。通过这种形式，你可以轻松地把一个文件系统的 stream 和一个 HTTP 响应的 strea
m 连接起来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;var fs = require(&apos;fs&apos;),
    http = require(&apos;http&apos;);

var port = process.argv[2],
    path = process.argv[3];

var fileStream = fs.createReadStream(path);

var server = http.createServer(function(req,res){
    res.writeHead(200,{&apos;content-type&apos;:&apos;text/plain&apos;});
    fileStream.pipe(res);
});

server.listen(port);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP UPPERCASERER&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个 HTTP 服务器，它只接受 POST 形式的请求，并且将 POST 请求主体（body）所带
的字符转换成大写形式，然后返回给客户端。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你的服务器需要监听由第一个命令行参数所指定的端口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里只要知道 &lt;code&gt;toUpperCase()&lt;/code&gt; 这个方法就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var http = require(&apos;http&apos;);
var port = process.argv[2];

var server = http.createServer(function(req,res){
    var post = &apos;&apos;;

    // console.log(req.method);
    if (req.method !== &quot;POST&quot;) {
        return res.end(&quot;The method must be POST&quot;);
    }
    res.writeHead(200,{&apos;content-type&apos;:&apos;text/plain&apos;});

    req.on(&apos;data&apos;,function(data){
        post += data;
    });

    req.on(&apos;end&apos;,function(){
        res.end(post.toString().toUpperCase());
    });

});

server.listen(port);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP JSON API SERVER&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编写一个 HTTP 服务器，每当接收到一个路径为 &apos;/api/parsetime&apos; 的 GET 请求的时候，
响应一些 JSON 数据。我们期望请求会包含一个查询参数（query string），key 是 &quot;iso&quot;
，值是 ISO 格式的时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;如:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;/api/parsetime?iso=2013-08-10T12:10:15.474Z&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;所响应的 JSON 应该只包含三个属性：&apos;hour&apos;，&apos;minute&apos; 和 &apos;second&apos;。例如：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;{&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;  	&quot;hour&quot;: 14,
  	&quot;minute&quot;: 23,
  	&quot;second&quot;: 15
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;然后增再加一个接口，路径为 &apos;/api/unixtime&apos;，它可以接收相同的查询参数（query stri
ng），但是它的返回会包含一个属性：&apos;unixtime&apos;，相应值是一个 UNIX 时间戳。例如:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;{ &quot;unixtime&quot;: 1376136615474 }&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;你的服务器需要监听第一个命令行参数所指定的端口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先我们要有两个方法来分别将时间转换为相应格式的 &lt;code&gt;JSON&lt;/code&gt; 文件。然后我们需要用到 &lt;code&gt;url&lt;/code&gt; 这个模块，可以用它来解析请求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var http = require(&apos;http&apos;),
    url = require(&apos;url&apos;);

var port = process.argv[2];

var parseTime = function(time){
    return{
        &quot;hour&quot;: time.getHours(),
        &quot;minute&quot;: time.getMinutes(),
        &quot;second&quot;: time.getSeconds()
    };
};
var getUnixTime = function(time){
    return{
        &quot;unixtime&quot;: time.getTime()
    };
};

var server = http.createServer(function(req,res){
    // console.log(url.parse(req.url,true));
    var queryString = url.parse(req.url,true).query,
        time = new Date(queryString.iso),
        pathname = url.parse(req.url,true).pathname,
        result;
    if (req.method != &quot;GET&quot;) {
        res.end(&apos;Method must be GET&apos;);
    }
    if (pathname === &apos;/api/parsetime&apos;) {
        result  = parseTime(time);
    }else if(pathname ===  &apos;/api/unixtime&apos;){
        result = getUnixTime(time);
    }

    if (result) {
        res.writeHead(200,{&apos;Content-Type&apos;:&apos;application/json&apos;});
        res.end(JSON.stringify(result));
    }else{
        res.writeHead(404);
        res.end(&apos;404,not found&apos;);
    }
});

server.listen(Number(port));
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;最后&lt;/h1&gt;
&lt;p&gt;因为我也是第一次接触，所以对代码有问题的。欢迎与我讨论，互相学习。&lt;/p&gt;
&lt;p&gt;以上所有的代码都整合放在&lt;a href=&quot;https://github.com/kisnows/learnyounode-solutions&quot;&gt;这里了&lt;/a&gt;;&lt;/p&gt;
</content:encoded></item><item><title>被终止的项目</title><link>https://ab.kisnows.com/zh-CN/2015/08/16/project-termination/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/08/16/project-termination/</guid><pubDate>Sun, 16 Aug 2015 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在五月份公司接到一个客户的手机钱包项目，这个项目得到了公司很大的重视，因为这是公司手机端继其它两个主要项目后开辟的一个新的产品线。
老大给了我机会，让我全权负责这个项目的前端部分。&lt;/p&gt;
&lt;p&gt;当时我的内心是很兴奋的，不敢怠慢，认认真真的把项目做了出来。开发过程中遇到了很多以前没有遇到过的问题，但也都被我一一克
服了，很有成就感。项目定格在 1.2 版本后，就交给了后台开发，然后自己就一直再期待这个项目的上线。&lt;/p&gt;
&lt;p&gt;大概在上个月下旬后台开发完毕，然后就上了测试环境进行测试。
上周二测试妹妹过来找我，说页面在低版本安卓上出现了一些兼容性问题，给我提了几个 issue 让我改。兼容性问题，在所难免嘛。然后我就哼哧
哼哧去改 issue 了，费了好大劲改好了。提交代码后，我去通知后台。&lt;/p&gt;
&lt;p&gt;然后就接到一个令人蛋疼的消息：&lt;strong&gt;项目被砍掉了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这真是太突然了，前一秒我还在埋头改 bug ，下一秒就得知项目就被砍掉了，挂了。这意味我的工作也都付诸东流了，这感觉真是很不好。就好
像大家商量好周末要在你家聚餐，为此你准备了很丰盛的饭菜，结果突然聚餐取消了……&lt;/p&gt;
&lt;p&gt;而项目被砍的原因竟然是因为前段时间，证监会发布的一条新规定。&lt;/p&gt;
&lt;p&gt;我该说点什么好呢&lt;/p&gt;
</content:encoded></item><item><title>网易云课堂考试题-python</title><link>https://ab.kisnows.com/zh-CN/2015/01/29/python-neteasy-exam/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/29/python-neteasy-exam/</guid><pubDate>Thu, 29 Jan 2015 00:23:14 GMT</pubDate><content:encoded>&lt;h1&gt;题目内容：&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;两位整数相乘形成的最大回文数是 9009 = 99 × 91。编写程序，求得任意输入的 n 位整数相乘形成的最大回文数。
输入格式:
正整数 n
输出格式：
n 位整数相乘形成的最大回文数
输入样例：
2
输出样例：
9009&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    def is_palindrome(num): 	#判断是否是回文
    	n = str(num)
    	if n == n[::-1]:
    		return True
    	else:
    		return False

    n = int(input())

    a = 1
    arr1 = []
    arr2 = []
    while len(str(a)) &amp;lt; n + 1: 		#将n位数的数字加到数组中，同时抛弃n-1位数
    	if len(str(a)) &amp;gt; n - 1:
    		arr1.append(a)
    		arr2.append(a)
    	a = a + 1

    maxPalindrome = 1
    for i in arr1[::-1]: 		#从后面往前开始遍历，取得最大的回文数
    	for j in arr2[::-1]:
    		if i*j &amp;gt; maxPalindrome and is_palindrome(i*j):
    			maxPalindrome = i*j
    print(maxPalindrome)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;感觉算法太粗暴，运行效率较低，但一时还没想到更好的算法。&lt;/p&gt;
</content:encoded></item><item><title>快速排序的实现</title><link>https://ab.kisnows.com/zh-CN/2015/01/30/quick-sort/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/30/quick-sort/</guid><pubDate>Fri, 30 Jan 2015 07:05:32 GMT</pubDate><content:encoded>&lt;p&gt;快速排序是一种经常用到的排序方法，它的时间复杂度为 O(nlogn)。因为大多数情况下速度都比一般的排序方法快，用的比较多，自己就琢磨着实现了一下。&lt;/p&gt;
&lt;h1&gt;原理&lt;/h1&gt;
&lt;p&gt;快速排序用到了分治法的原理：
  从数组中随机取出一个数字，将这个数组分为两个部分。将取出的数字作为基准数，小于基准数的放到它的左边，大于基准数的放到它的右边。这样基准数左边的都是比它小的，右边的都是比它大的。然后再对基准数的左边和右边重复前面的过程，直到每个部分的数组长度为 1。最后再将这些分开的数组组合起来就成功了。步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从数组中随机抽取一个数组作为基准数，将数组中的每个数字与基准数比较，小于基准数的放到基准数左边，大于基准数的放到基准数右边。&lt;/li&gt;
&lt;li&gt;对上面结束后的左右两个部分重复步骤 1。&lt;/li&gt;
&lt;li&gt;当所有部分的长度为 1 时停止。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h1&gt;实现&lt;/h1&gt;
&lt;p&gt;下面是自己熟悉的语言的实现(用第一个数作为基准数,并且采用递归的方法)：&lt;/p&gt;
&lt;h2&gt;Python 版本&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    def quick_sort(lst,left,right):
    	i = left;
    	j = right
    	if i &amp;gt;= j:
    		return lst
    	k = lst[i]
    	while i &amp;lt; j:
    		while i &amp;lt; j and lst[j] &amp;gt;= k:		#lst[j]&amp;lt;=k时，循环结束，并将其值赋给lst[i]
    			j -= 1
    		lst[i] = lst[j]
    		while i &amp;lt; j and lst[i] &amp;lt;= k:		#lst[i]&amp;gt;=k时，循环结束，并将其值赋给lst[j]
    			i += 1
    		lst[j] = lst[i]
    	lst[i] = k								#当i&amp;gt;=j时，将k的值赋给lst[i]
    	quick_sort(lst,left,i-1)
    	quick_sort(lst,j+1,right)
    	return lst
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JavaScript 版&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function quick_sort(lst, l, r) {
  var i = l,
    j = r,
    k = lst[l];
  if (i &amp;gt;= j) {
    return lst;
  }
  while (i &amp;lt; j) {
    for (; i &amp;lt; j; j--) {
      if (lst[j] &amp;lt;= k) {
        lst[i] = lst[j];
        i++;
        break;
      }
    }
    for (; i &amp;lt; j; i++) {
      if (lst[i] &amp;gt;= k) {
        lst[j] = lst[i];
        j--;
        break;
      }
    }
  }
  lst[i] = k;
  quick_sort(lst, l, i - 1);
  quick_sort(lst, j + 1, r);
  return lst;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;C 语言版&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    void qucik_sort(int lst[],int l,int r)
    {
    	if(l &amp;lt; r)
    	{
    		int i = l, j = r, k = lst[l];
    		while (i &amp;lt; j){
    			for (; i &amp;lt; j; --j)
    			{
    				if (lst[j] &amp;lt;= k)
    				{
    					lst[i] = lst [j];
    					i++;
    					break;
    				}
    			}
    			for (; i &amp;lt; j; ++i)
    			{
    				if (lst[i] &amp;gt;= k)
    				{
    					lst[j] = lst[i];
    					j--;
    					break;
    				}
    			}
    		}
    		lst[i] = k;
    		qucik_sort(lst, l, i-1);
    		qucik_sort(lst, j+1, r);
    	}
    }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>翘边阴影制作方法</title><link>https://ab.kisnows.com/zh-CN/2015/01/26/rase-shadow/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/26/rase-shadow/</guid><pubDate>Mon, 26 Jan 2015 05:36:08 GMT</pubDate><content:encoded>&lt;p&gt;一些特殊阴影的制作方法，其实就是将多个图形叠加在一起，然后通过&lt;code&gt;z-index&lt;/code&gt;调整前后顺序，最后&lt;a href=&quot;http://kisnows.com/Front-end-practice/boxshadow/&quot;&gt;实现效果&lt;/a&gt;。
&lt;img src=&quot;/imgs/2015-01-25_214045.png&quot; alt=&quot;翘边阴影&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;HTML：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;li&amp;gt;&amp;lt;img src=&quot;img/1.gif&quot; alt=&quot;img&quot; /&amp;gt;&amp;lt;/li&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;li::after {
  content: &quot;&quot;;
  width: 90%;
  height: 80%;
  left: 1.6%;
  bottom: 4.6%;
  position: absolute;
  background-color: transparent;
  box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.6);
  -webkit-transform: rotate(-4deg) skew(-8deg);
  -moz-transform: rotate(-4deg) skew(-8deg);
  -ms-transform: rotate(-4deg) skew(-8deg);
  -o-transform: rotate(-4deg) skew(-8deg);
  transform: rotate(-4deg) skew(-8deg);
  z-index: -1;
}
li::before {
  content: &quot;&quot;;
  width: 90%;
  height: 80%;
  right: 1.6%;
  bottom: 4.6%;
  position: absolute;
  background-color: transparent;
  box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.6);
  -webkit-transform: rotate(4deg) skew(8deg);
  -moz-transform: rotate(4deg) skew(8deg);
  -ms-transform: rotate(4deg) skew(8deg);
  -o-transform: rotate(4deg) skew(8deg);
  transform: rotate(4deg) skew(8deg);
  z-index: -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就是将两个平行四边形分别左右旋转后放在原图的下面（如下示意图），并给它设置阴影，最后就会实现图片中阴影效果。
&lt;img src=&quot;/imgs/boxshadow.png&quot; alt=&quot;示意图&quot; /&gt;
&lt;a href=&quot;http://kisnows.com/F2E-practice/boxshadow/index.html&quot;&gt;可以点击这里查看 DEMO&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>基于 React 的前端项目开发总结</title><link>https://ab.kisnows.com/zh-CN/2017/05/12/react-base-project-summary/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/05/12/react-base-project-summary/</guid><pubDate>Fri, 12 May 2017 23:00:52 GMT</pubDate><content:encoded>&lt;h1&gt;技术选型&lt;/h1&gt;
&lt;p&gt;我们的项目主要选用以下技术开发，再配合一些其它辅助工具。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;react&lt;/li&gt;
&lt;li&gt;react-router&lt;/li&gt;
&lt;li&gt;redux&lt;/li&gt;
&lt;li&gt;react-redux&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;开发及线上环境&lt;/h1&gt;
&lt;h2&gt;开发环境&lt;/h2&gt;
&lt;p&gt;由于项目是前后端分离的，所以我们需要一套完整的开发环境，需要包括以下功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据 Mock&lt;/li&gt;
&lt;li&gt;Webpack 实施编译刷新&lt;/li&gt;
&lt;li&gt;方便前后端联调&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基于这些需求，我们基于 Express, Webpack,Webpack-dev-middleware 搭建了这套完整的开发环境。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/react-base-project-summary/dev-env.png&quot; alt=&quot;开发环境&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到，浏览器所有的请求都被本地的 Node.js 服务拦截。对于静态资源请求，都委托给 &lt;code&gt;webpack-dev-middleware&lt;/code&gt; 来处理，对于接口请求根据不同的环境来决定要做的操作。&lt;/p&gt;
&lt;h3&gt;本地开发&lt;/h3&gt;
&lt;p&gt;当 &lt;code&gt;ENV = &apos;development&apos;&lt;/code&gt; 时，也就是开发环境，那么就直接读取本地的 mock 数据来渲染页面。&lt;/p&gt;
&lt;h3&gt;前后端联调&lt;/h3&gt;
&lt;p&gt;当 &lt;code&gt;ENV = &apos;api&apos;&lt;/code&gt; 时，也就是我们认为的联调环境，这个时候对于接口请求由 node.js 转发到需要联调的真实后端服务地址上，从而避免直接调用所产生的跨域问题。&lt;/p&gt;
&lt;p&gt;这样就可以直接用本地开发代码和后端联调，能大大提高效率，省去了每次需要往服务器上构建部署的步骤。&lt;/p&gt;
&lt;h2&gt;线上环境&lt;/h2&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;前后端是分开部署的，所有的静态资源都放在 CDN (example.cdn.com)上面。&lt;/p&gt;
&lt;p&gt;也就是说我们的页面在 example.cdn/index.html 这里，但是请求的接口在 example.163.com/api/xxx,我们肯定不能让用户去直接去访问 example.cdn.com/index.html,这样不合理，而且由跨域问题存在&lt;/p&gt;
&lt;p&gt;那么访问 example.dai.163.com 的时候，怎么拿到我们的 html 页面呢？
看下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/react-base-project-summary/online.png&quot; alt=&quot;线上环境&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在客户端和后台服务之间架设一台 nginx, 我们访问的 example.dai.163.com 有两种请求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;html 页面资源&lt;/li&gt;
&lt;li&gt;接口请求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两种请求都先经过 nginx，在这里做判断，如果是页面请求那么由 nginx 转发到 CDN， 否则交给后端服务来响应接口请求。&lt;/p&gt;
&lt;p&gt;拿到页面以后，其它所有的 css, js 等静态资源都是直接请求到 CDN ，这里没什么说的。&lt;/p&gt;
&lt;h1&gt;数据流转&lt;/h1&gt;
&lt;p&gt;借助 redux 来管理数据流，我们来看这张图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/react-base-project-summary/data-flow.png&quot; alt=&quot;数据流转&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先，通过 &lt;code&gt;middleware&lt;/code&gt; 和 &lt;code&gt;reducer&lt;/code&gt; 生成 &lt;code&gt;store&lt;/code&gt;, 然后获得项目的初始 &lt;code&gt;state&lt;/code&gt;,通过初始 &lt;code&gt;state&lt;/code&gt; 去渲染页面的初始状态。&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;Home&lt;/code&gt; 页面为例，首先 &lt;code&gt;Home&lt;/code&gt; 通过 &lt;code&gt;react-redux&lt;/code&gt; 提供的 &lt;code&gt;connect&lt;/code&gt; 方法拿到初始 &lt;code&gt;state&lt;/code&gt; 作为 &lt;code&gt;Home&lt;/code&gt; 的 &lt;code&gt;prop&lt;/code&gt; 传递给 &lt;code&gt;Home&lt;/code&gt;. 而 &lt;code&gt;Home&lt;/code&gt; 由多个不同的子组件组成，这些组件的需要数据再由 Home 通过 props 传递给自己的子组件。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;Home&lt;/code&gt; 的初始状态加载完毕后，我们需要向后端请求去拿去一些用户数据。这时，我们分发一个下面这种格式的 &lt;code&gt;action&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  types: [&apos;home/start&apos;,&apos;home/success&apos;,&apos;home/failure&apos;],
  payload: {
    api:
    ...
  },
  meta: {
    isApi: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有的 &lt;code&gt;action&lt;/code&gt; 都会按照我们制定的循序通过一个个 &lt;code&gt;middleware&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;在这里，我们的 &lt;code&gt;action&lt;/code&gt; 会被 &lt;code&gt;callApiMiddleware&lt;/code&gt; 通过 &lt;code&gt;meta&lt;/code&gt; 里面的 &lt;code&gt;isApi&lt;/code&gt; 标识命中，并去做相应的事情。&lt;/p&gt;
&lt;p&gt;比如在这个中间件里面，我们去做了真实的接口请求，在请求成功或失败的时候分发对应的 &lt;code&gt;action&lt;/code&gt;，以及做一些统一的业务逻辑。比如我们对后端返回的接口中 &lt;code&gt;code&lt;/code&gt; 值有统一的约定，假设 1 为成功， 2 为失败， 3 为未登录。那么我们就可以在中间件中去处理这些业务逻辑。&lt;/p&gt;
&lt;p&gt;当请求成功，并渲染页面后，假设用户点击了一个按钮，这个按钮需要唤起 &lt;code&gt;native&lt;/code&gt; 的一些功能，比如说拍照。那么我们分发一个唤起拍照功能的 &lt;code&gt;camera/start&lt;/code&gt; 的&lt;code&gt;action&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  types: [&apos;sdk/start&apos;,&apos;sdk/success&apos;,&apos;sdk/failure&apos;],
  payload: {
    command:
    ...
  },
  meta: {
    isSDK: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的道理，这个 &lt;code&gt;action&lt;/code&gt; 会被 &lt;code&gt;EpaySDKMiddleware&lt;/code&gt; 所识别并处理，在调起 native 的时候， 为了保证安全性，我们需要向后发起一个请求去拿签名，这个时候就可以在 &lt;code&gt;EpaySDKMiddleware&lt;/code&gt; 里面分发一个接口请求的 &lt;code&gt;action&lt;/code&gt;，那么这个 &lt;code&gt;action&lt;/code&gt; 同样需要走一遍所有的 &lt;code&gt;middleware&lt;/code&gt;. 那么这个接口请求的 &lt;code&gt;action&lt;/code&gt; 就会像上面的流程一样，通过 &lt;code&gt;callApiMiddleware&lt;/code&gt; 去处理。&lt;/p&gt;
&lt;p&gt;中间件的存在，使整个流程变得非常清晰，接口的请求的中间件就只做接口请求，调用 native 接口的中间件就只做对 native 的调用，当对 native 接口的调用需要做后端接口请求的时候，去分发一个 &lt;code&gt;action&lt;/code&gt; 走接口请求的中间件。&lt;/p&gt;
&lt;p&gt;每个中间件只专注于自己的事情，既方便后续的维护，同时也提供了一个很好的拓展方式。而 &lt;code&gt;View&lt;/code&gt; 层需要做的就只是做出分发 action，然后拿数据渲染页面就可以了，其他的逻辑都不用关心。&lt;/p&gt;
&lt;h1&gt;一个例子&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/react-base-project-summary/overall-process.png&quot; alt=&quot;整体流程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;假设我们由如下的一个路由配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    component: App,
    path: &apos;/&apos;,
    onEnter: initLogin(store),
    indexRoute: {
      getComponent(nextState, cb) {
        require.ensure([], require =&amp;gt; {
          cb(null, require(&apos;../views/Home&apos;).default)
        }, &apos;Home&apos;)
      },
      onEnter: initHome(store)
    },
    childRoutes: [
      createActivateRoute(store),
      {
        path: &apos;test&apos;,
        indexRoute: {
          getComponent(nextState, cb) {
            require.ensure([], require =&amp;gt; {
              cb(null, require(&apos;../views/Test&apos;).default)
            }, &apos;Test&apos;)
          }
        }
      },
      ...
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那结合 &lt;code&gt;react-route&lt;/code&gt; 我们来看一个完整的流程。当我们浏览器里面输入 example.dai.163.com/index.html/#/ 的时候。&lt;/p&gt;
&lt;p&gt;首先，通过最上面 线上环境 一节提到的内容，拿到页面需要 html,css,js.&lt;/p&gt;
&lt;p&gt;然后渲染 &lt;code&gt;Provide&lt;/code&gt; 和 &lt;code&gt;Router&lt;/code&gt; 组件，分别提供 &lt;code&gt;store&lt;/code&gt; 的注入和路由的控制。&lt;/p&gt;
&lt;p&gt;此时触发根路径的路由匹配，然后加载根组件 &lt;code&gt;APP&lt;/code&gt;, 然后根据路由匹配规则匹配到 &lt;code&gt;IndexRouter&lt;/code&gt;, 加载 &lt;code&gt;Home&lt;/code&gt; 组件。&lt;/p&gt;
&lt;p&gt;后面的事情就和前面数据流转一节讲的是一样的了。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;在前后端完全分离的基础上，借助一套完善的开发环境，可以大大提高的我们的开发效率，降低前后端联调的成本。&lt;/p&gt;
&lt;p&gt;同时借助于 Redux 思想，实现单向数据流，让我们可以实现一个非常清晰的数据流向。并且，借助于中间件，我们可以更加有效的控制数据流转的过程。为后面项目的扩展提供无限的想象空间。&lt;/p&gt;
</content:encoded></item><item><title>一个 React Form 组件的重构思路</title><link>https://ab.kisnows.com/zh-CN/2017/03/18/react-form-rework-thinking/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/03/18/react-form-rework-thinking/</guid><pubDate>Sat, 18 Mar 2017 05:31:31 GMT</pubDate><content:encoded>&lt;p&gt;最近对团队内部 React 组件库（&lt;a href=&quot;https://github.com/NE-LOAN-FED/NE-Component&quot;&gt;&lt;strong&gt;ne-rc&lt;/strong&gt;&lt;/a&gt;）中的 Form 组件进行了重构，记录一下思考的过程。&lt;/p&gt;
&lt;p&gt;一些前置定义：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名词&lt;/th&gt;
&lt;th&gt;定义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;表单&lt;/td&gt;
&lt;td&gt;Form 组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;子表单&lt;/td&gt;
&lt;td&gt;嵌套在 Form 下面的类似 Input, Select 这样的子组件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;首先我们看一下，我们的对 Form 组件的需求是什么。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取当前变动表单的状态
&lt;ul&gt;
&lt;li&gt;校验所有必填表单是否填写完成&lt;/li&gt;
&lt;li&gt;对外触发具体表单变化的方法 &lt;code&gt;formFieldChange&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;暴露对外提供整个表单状态的方法
&lt;ul&gt;
&lt;li&gt;提供整个表单最新状态的方法 $Form.data&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;提交方法
&lt;ul&gt;
&lt;li&gt;校验表单是否通过校验&lt;/li&gt;
&lt;li&gt;对外触发 &lt;code&gt;formSubmit&lt;/code&gt; 方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接着我们从重构前和重构后，看如何来解决这个问题。&lt;/p&gt;
&lt;h1&gt;&lt;strong&gt;Before&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;获取当前变动表单的状态&lt;/h2&gt;
&lt;h3&gt;如何获取变动的子表单&lt;/h3&gt;
&lt;p&gt;React 父子通信需要通过 prop 传递方法，对于 Form 下面的类似与 Input 之类的子表单的变化想要通知到父级，如果不借助第三方的事件传递方法，那么就只能通过由父级通过
props 向 Input 传递 &lt;code&gt;formFieldChange&lt;/code&gt;（假设就叫这个名字）方法，然后当子组件变化时去调用 &lt;code&gt;formFieldChange&lt;/code&gt; 来实现。&lt;/p&gt;
&lt;p&gt;那么问题来了，什么时候去传递这个方法呢？&lt;/p&gt;
&lt;p&gt;不能在具体页面里面使用的时候再去每条表单里面注册这个方法，那每个用到表单组件的时候就都需要给子表单进行这样的事件绑定，这样太累了。&lt;/p&gt;
&lt;p&gt;所以一开始，我选择通过直接递归的遍历 Form 下面的 children，只要发现这个 children
是我想要的表单类型，那么就重新克隆一个带有 &lt;code&gt;formFieldChange&lt;/code&gt; 的组件来替换掉原来的组件。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 获取 form 下面每一个表单对象，注入属性，并收集起来
 * @param children
 * @returns {*}
 */
function getForms(children) {
  return React.Children.map(children, (el, i) =&amp;gt; {
    if (!el) {
      return null;
    }
    switch (el.type) {
      case Input:
        Forms.push(el);
        return React.cloneElement(el, {
          key: i,
          formFieldChange,
          emptyInput,
        });
      case Select:
        Forms.push(el);
        return React.cloneElement(el, {
          key: i,
          formFieldChange,
        });
      case CheckBox:
        Forms.push(el);
        return React.cloneElement(el, {
          key: i,
          formFieldChange,
        });
      default:
        if (el.props &amp;amp;&amp;amp; el.props.children instanceof Array) {
          const children = getForms(el.props.children);
          return React.cloneElement(el, {
            key: i,
            children,
          });
        } else {
          return el;
        }
    }
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，所有的特定子组件就都可以拿到被注册的方法。以 Input 为例，在 Input 的 &lt;code&gt;onChange&lt;/code&gt; 方法里面去调用从父级 props
传入的 &lt;code&gt;formFieldChange&lt;/code&gt; 就可以通知到 Form 组件了。&lt;/p&gt;
&lt;h3&gt;收集变动表单的数据。&lt;/h3&gt;
&lt;p&gt;前一步完成后，这一步就比较简单了，Input 在调用 &lt;code&gt;formFieldChange&lt;/code&gt; 的时候把想要传递的数据作为参数传进去，在 Form
里面去对这个参数做处理，就可以拿到当前变动的表单状态数据了。&lt;/p&gt;
&lt;h3&gt;校验表单是否填写完成&lt;/h3&gt;
&lt;p&gt;前面我们收集了每一条变动表单的数据。但是要判断当前 Form
下面的表单是否填写完成，那么首先需要知道我们有多少个需要填写的表单，然后在 &lt;code&gt;formFieldChange&lt;/code&gt; 的时候进行判断就可以了。如何来提前知道我们有多少需要填写的
Field 呢，之前我选择的是通过在使用 Form 的时候先初始化一个包含所有表单初始化状态的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default class Form extends React.Component {
  constructor (props) {
    super(props)
    this.Forms = []
    this.formState = Object.assign({}, {
      isComplete: false,
      isValidate: false,
      errorMsg: &apos;&apos;,
      data: {}
    }, this.props.formState)
  }

  static propTypes = {
    onChange: PropTypes.func,
    onSubmit: PropTypes.func,
    formState: PropTypes.object
  }
  // 初始化一个类似这样的对象传递给 Form
  formState: {
    data: {
      realName: {},
      cityId: {},
      email: {},
      relativeName: {},
      relativePhone: {},
      companyName: {}
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就很粗暴的解决了这个问题，但是这中间存在很多问题。&lt;/p&gt;
&lt;p&gt;因为限定了特定的组件类型（Input，Select，CheckBox），导致不利于扩展，如果在开发过程遇到其他类型的比如自定义的子表单，那么 Form
就没法对这个自定义子表单进行数据收集，解决起来比较麻烦。&lt;/p&gt;
&lt;p&gt;所以就在考虑另一个种实现方式， Form 只去收集一个特定条件下的组件，只要这个组件满足了这个条件，并实现了对应的接口，那么 Form
就都可以去收集处理。这样也就大大挺高了适用性。&lt;/p&gt;
&lt;h2&gt;暴露对外提供整个表单状态的方法&lt;/h2&gt;
&lt;p&gt;通过在外监听每次 Form 触发的 &lt;code&gt;onChange&lt;/code&gt; 事件来获取整个 Form 的状态。&lt;/p&gt;
&lt;h2&gt;提交方法&lt;/h2&gt;
&lt;h3&gt;检验表单是否通过校验&lt;/h3&gt;
&lt;p&gt;已经有了整个 Form 的数据对象，做校验并不是什么困难。通过校验的时候调用 &lt;code&gt;formSubmit&lt;/code&gt; 方法，没有通过校验的时候对外把错误信息添加到
Form 的 state 上去。&lt;/p&gt;
&lt;h3&gt;对外触发 formSubmit 方法&lt;/h3&gt;
&lt;p&gt;当表单通过校验的时候，对外触发 &lt;code&gt;formSubmit&lt;/code&gt; 方法，把要提交的数据作为 &lt;code&gt;formSubmit&lt;/code&gt; 的参数传递给外面。&lt;/p&gt;
&lt;h1&gt;&lt;strong&gt;After&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;前面是之前写的 Form 组件的一些思路，在实际使用中也基本能满足业务需求。&lt;/p&gt;
&lt;p&gt;但是整个 Form 的可拓展性比较差，无法很好的接入其他自定义的组件。所以萌生了重写的想法。&lt;/p&gt;
&lt;p&gt;对于重写的这个 Form，我的想法是：首先一定要方便使用，不需要一大堆的起始工作；其次就是可拓展性要强，除了自己已经提供的内在
Input，Select 等能够接入 Form 外，对于其他的业务中的特殊需求需要接入 Form 的时候，只要这个组件实现了特定的接口就可以了很方便的接入，而不需要大量的去修改组件内部的代码。&lt;/p&gt;
&lt;p&gt;重构主要集中在上面需求 1 里面的内容，也就是：&lt;strong&gt;获取当前变动表单的状态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;获取当前表单的状态分解下来有一下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取所有需要收集的子表单 &lt;code&gt;formFields&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;初始化 Form &lt;code&gt;state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;表单下面子表单数量或类型发生变化时更新 1 里面创建的 &lt;code&gt;formFields&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;子表单内部状态发生变化时通知到父表单&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;获取当前变动表单的状态&lt;/h2&gt;
&lt;h3&gt;获取所有需要的子表单&lt;/h3&gt;
&lt;p&gt;同样通过递归遍历 children 来获取需要收集的子表单，通过子表单的 type.name 命名规则是否符合我们的定义来决定是否要进行收集。
直接来看代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;collectFormField = (children) =&amp;gt; {
  const handleFieldChange = this.handleFieldChange;

  // 简单粗暴，在 Form 更新的时候直接清空上一次保存的 formFields，全量更新，
  // 避免 formFields 内容或者数量发生变化时 this.formFields 数据不正确的问题
  const FormFields = (this.formFields = []);

  function getChildList(children) {
    return React.Children.map(children, (el, i) =&amp;gt; {
      // 只要 Name 以 _Field 开头，就认为是需要 From 管理的组件
      if (!el || el === null) return null;

      const reg = /^_Field/;
      const childName = el.type &amp;amp;&amp;amp; el.type.name;
      if (reg.test(childName)) {
        FormFields.push(el);
        return React.cloneElement(el, {
          key: i,
          handleFieldChange,
        });
      } else {
        if (el.props &amp;amp;&amp;amp; el.props.children) {
          const children = getChildList(el.props.children);
          return React.cloneElement(el, {
            key: i,
            children,
          });
        } else {
          return el;
        }
      }
    });
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要组件的 class name 以 _Field 开头，就把它收集起来，并传入 &lt;code&gt;handleFieldChange&lt;/code&gt; 方法，这样当一个自定义组件接入的时候，只需要在外面包一层，并把
class 的命名为以 _Field 开头的格式就可以被 Form 收集管理了。&lt;/p&gt;
&lt;p&gt;接入组件里面需要做的就是，在合适的时机调用 &lt;code&gt;handleFieldChange&lt;/code&gt; 方法，并把要传递的数据作为参数传递出来就可以了。&lt;/p&gt;
&lt;p&gt;为什么一定要执迷不悟的使用遍历这种低效的方式去收集呢，其实都是为了组件上使用的方便。这样就不需要每次在引用的时候在对子表单做什么操作了。&lt;/p&gt;
&lt;h3&gt;初始化 Form state&lt;/h3&gt;
&lt;p&gt;上一步拿到了所有的子表单，然后通过调用 &lt;code&gt;initialFormDataStructure&lt;/code&gt; 拿来初始化 Form 的 &lt;code&gt;state.data&lt;/code&gt; 的结构，同时通知到外面
Form 发生了变化。&lt;/p&gt;
&lt;h3&gt;子表单数量或类型发生变化时&lt;/h3&gt;
&lt;p&gt;当 Form 下面子组件被添加或删除时，需要及时更新 Form Data 的结构。通过调用 &lt;code&gt;updateFormDataStructure&lt;/code&gt;
把新增的或者修改的子表单更新到最新，并通知到外面 Form 发生了变化。&lt;/p&gt;
&lt;h3&gt;子表单内部状态发生变化时&lt;/h3&gt;
&lt;p&gt;在第一步收集子表单的时候就已经把 &lt;code&gt;handleFieldChange&lt;/code&gt;
注入到了子表单组件里面，所以子表单来决定调用的时机。当 &lt;code&gt;handleFieldChange&lt;/code&gt; 被调用的时候，首先对 Form &lt;code&gt;state&lt;/code&gt;
进行更新，然后外通知子表单发生了变化，同时通知外面 Form 发生了变化。&lt;/p&gt;
&lt;p&gt;这样看起来整个流程就走通了，但实际上存在很多问题。&lt;/p&gt;
&lt;p&gt;首先由于 &lt;code&gt;setState&lt;/code&gt; 是一个异步的过程，只有在 &lt;code&gt;render&lt;/code&gt; 后才能获取到最新的 &lt;code&gt;state&lt;/code&gt;.
这就导致，在一个生命周期循环内如果我多次调用了 &lt;code&gt;setState&lt;/code&gt; ，那么两次调用之间对 &lt;code&gt;state&lt;/code&gt;
的读取很可能是不准确的。（有关生命周期的详细内容可以看这篇文章：https://www.w3ctech.com/topic/1596）&lt;/p&gt;
&lt;p&gt;所以我创建了一个临时变量 &lt;code&gt;currentState&lt;/code&gt; 来存放当前状态下最新的 &lt;code&gt;state&lt;/code&gt;，每次 &lt;code&gt;setState&lt;/code&gt; 的时候都对其进行更新。&lt;/p&gt;
&lt;p&gt;另一个问题是当 Form 发生变化的时候，&lt;code&gt;updateFormDataStructure&lt;/code&gt; 调用的过于频繁。其实只有在子表单的数量或者类型发生变化时才需要更新
Form state 的结构。而直接去对比子表单的类型是否发生变化也是意见开销很大操作，所以选择另一种折中方式。通过给 Form 当前的状态打标，将
Form 可能处于的状态都标识出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const STATUS = {
  Init: &quot;Init&quot;,
  Normal: &quot;Normal&quot;,
  FieldChange: &quot;FieldChange&quot;,
  UpdateFormDataStructure: &quot;UpdateFormDataStructure&quot;,
  Submit: &quot;Submit&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，只有在 Form 的 &lt;code&gt;STATUS&lt;/code&gt; 处于 &lt;code&gt;Normal&lt;/code&gt; 的时候才对其进行 &lt;code&gt;updateFormDataStructure&lt;/code&gt;
操作。这样就可以省去很多次渲染以及无效的对外触发的 &lt;code&gt;FormChange&lt;/code&gt; 事件。&lt;/p&gt;
&lt;p&gt;提交和对外暴露 Form 状态的方法和之前基本一致，这样整个对 Form 的重构就算完成了，具体项目中使用体验还不错 O(∩_∩)O&lt;/p&gt;
&lt;p&gt;Form
组件地址： &lt;a href=&quot;https://github.com/NE-LOAN-FED/NE-Component/tree/master/src/Form&quot;&gt;https://github.com/NE-LOAN-FED/NE-Component/tree/master/src/Form&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最后，如果看文章的你有什么更好的想法，请告诉我 😛。&lt;/p&gt;
</content:encoded></item><item><title>React 单页面初始化渲染优化</title><link>https://ab.kisnows.com/zh-CN/2017/04/20/react-page-render-improve/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/04/20/react-page-render-improve/</guid><pubDate>Thu, 20 Apr 2017 23:16:05 GMT</pubDate><content:encoded>&lt;p&gt;由于项目使用 React, React-Router, Redux 来开发项目，采取了前端渲染的方式（如果采用 ssr 就不会有这个问题）。&lt;/p&gt;
&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;所以每到一个页面，需要发一个初始化的请求来获取页面数据，然后重新渲染页面，所以之前一个页面从请求到加载完毕的流程是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Route match -&amp;gt; ComponentWillMount -&amp;gt;  render -&amp;gt; ComponentDidMount -&amp;gt; dispatch(init())-&amp;gt;  render -&amp;gt; componentDidUpdate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先是路由匹配，然后准备加载组件，使用通过 &lt;code&gt;Reducer&lt;/code&gt; 里面的初始化 &lt;code&gt;state&lt;/code&gt; 进行 &lt;code&gt;render&lt;/code&gt;, 然后触发 &lt;code&gt;ComponentDidMount&lt;/code&gt; 事件，在这个事件里面去 &lt;code&gt;dispatch&lt;/code&gt; 一个执行页面初始化的请求的 &lt;code&gt;Action&lt;/code&gt;，请求成功过以后触发组件的重新渲染。&lt;/p&gt;
&lt;p&gt;可以看到，展示最终的页面需要组件重新渲染两次。一次是使用写死在前端的&lt;code&gt;reducer&lt;/code&gt; 里面的 &lt;code&gt;initialState&lt;/code&gt; 数据来渲染，一次是在拿到后端数据进行的渲染。&lt;/p&gt;
&lt;p&gt;所以有的时候会出现闪屏的情况，用户体验很不好。&lt;/p&gt;
&lt;p&gt;我们的要求是进页面以后就是用从后台获取的最新数据渲染的页面，整个页面只会 &lt;code&gt;render&lt;/code&gt; 一次。&lt;/p&gt;
&lt;p&gt;那么怎么解决这个问题呢？&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;要解决这个问题，那么必然是加载好数据后，再去挂载页面组件，那么加载数据的时机就显得尤为重要。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;借鉴传统服务端渲染页面的方式，这个时机肯定是放在路由里面去做比较合适。&lt;/p&gt;
&lt;p&gt;具体到项目里面，就是在 &lt;code&gt;react-route&lt;/code&gt; 里面的 &lt;code&gt;onEnter&lt;/code&gt; 事件里面去做页面的初始化请求，当所有数据请求成功以后，在去加载这个页面。&lt;/p&gt;
&lt;p&gt;整个页面加载的流程就变成了这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Route match -&amp;gt; dispatch(init()) -&amp;gt; ComponentWillMount -&amp;gt;  render -&amp;gt; ComponentDidMount
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;具体代码如下：&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;HomeAction.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createAction } from &quot;redux-actions&quot;;

import { HOME_INDEX } from &quot;../../config/apis.js&quot;;
import createAsyncAction from &quot;../../utils/createAsyncAction.js&quot;;
import initAPI from &quot;../../utils/initAPI.js&quot;;

export const InitActionList = createAsyncAction(&quot;home/init&quot;);
export const FormChange = &quot;home/formChange&quot;;
export const FormFieldChange = &quot;home/formFieldChange&quot;;

export function init() {
  return initAPI(InitActionList, HOME_INDEX, &quot;get&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;HomeReducer.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  BillStatus,
  CreditStatus,
  InstallmentStatus,
} from &quot;../../config/constant&quot;;
import { InitActionList } from &quot;./HomeAction.js&quot;;

const initState = {
  foo: 1,
  bar: 10,
};

export default function (state = initState, action) {
  const type = action.type;
  const payload = action.payload;
  const meta = action.meta;
  switch (type) {
    case InitActionList.start:
      return state;
    case InitActionList.success:
      const currentStatus = getCurrentStatus(payload);
      return {
        ...state,
        foo: currentStatus,
      };
    case InitActionList.failure:
      return state;
    default:
      return state;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Router.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { init as initHome } from &quot;./homeAction&quot;;

export default function createRoutes(store) {
  function initHome(store) {
    return (nextState, replace, next) =&amp;gt; {
      // dispatch 页面加载的 Action，在数据加载完成后在执行 next() 以挂载组件
      store.dispatch(homeInit()).then(() =&amp;gt; next());
    };
  }

  return {
    component: App,
    path: &quot;/&quot;,
    childRoutes: [
      require(&quot;./activate&quot;),
      {
        path: &quot;test&quot;,
        getComponent(nextState, cb) {
          require.ensure(
            [],
            (require) =&amp;gt; {
              cb(null, require(&quot;../views/Test&quot;).default);
            },
            &quot;Test&quot;,
          );
        },
      },
    ],
    indexRoute: {
      getComponent(nextState, cb) {
        require.ensure(
          [],
          (require) =&amp;gt; {
            cb(null, require(&quot;../views/Home&quot;).default);
          },
          &quot;Home&quot;,
        );
      },
      onEnter: initHome(store),
    },
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Index.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import { render } from &quot;react-dom&quot;;
import { AppContainer } from &quot;react-hot-loader&quot;;
import { Provider } from &quot;react-redux&quot;;
import { hashHistory, Router } from &quot;react-router&quot;;
import { syncHistoryWithStore } from &quot;react-router-redux&quot;;

import &quot;react-hot-loader/patch&quot;;

import createRoutes from &quot;./routes&quot;;
import configureStore from &quot;./store&quot;;

import &quot;./style/app.scss&quot;;

export const store = configureStore(hashHistory, {});
const history = syncHistoryWithStore(hashHistory, store);
const root = document.getElementById(&quot;root&quot;);
const routers = createRoutes(store);

const renderRoot = () =&amp;gt; {
  render(
    &amp;lt;AppContainer&amp;gt;
      &amp;lt;Provider store={store} key=&quot;provider&quot;&amp;gt;
        &amp;lt;Router routes={routers} history={history} key={Math.random()} /&amp;gt;
      &amp;lt;/Provider&amp;gt;
    &amp;lt;/AppContainer&amp;gt;,
    root,
  );
};

if (module.hot) {
  module.hot.accept(&quot;./routes&quot;, () =&amp;gt; {
    renderRoot();
  });
}

renderRoot();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>React-Redux 源码粗读</title><link>https://ab.kisnows.com/zh-CN/2017/08/04/react-redux-source-code-read/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/08/04/react-redux-source-code-read/</guid><pubDate>Fri, 04 Aug 2017 03:54:30 GMT</pubDate><content:encoded>&lt;h1&gt;Overview&lt;/h1&gt;
&lt;p&gt;首先看一张图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/react-redux-source-code-read/react-redux.png&quot; alt=&quot;React-Redux&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到入口文件里面引入了这几个模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provider&lt;/li&gt;
&lt;li&gt;connectAdvanced&lt;/li&gt;
&lt;li&gt;connect&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们就来依次看一下这几个模块&lt;/p&gt;
&lt;h2&gt;Provider&lt;/h2&gt;
&lt;p&gt;将 &lt;code&gt;store&lt;/code&gt; 挂在在了 context 上面，给予了子组件访问 &lt;code&gt;store&lt;/code&gt; 的能力。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;connectAdvanced&lt;/h2&gt;
&lt;p&gt;一个用来生成 &lt;code&gt;connect&lt;/code&gt; 的高阶函数,引用了以下模块:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Component, createElement } from &quot;react&quot;;
import hoistStatics from &quot;hoist-non-react-statics&quot;;
import invariant from &quot;invariant&quot;;

import { storeShape, subscriptionShape } from &quot;../utils/PropTypes&quot;;
import Subscription from &quot;../utils/Subscription&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Subscription&lt;/h3&gt;
&lt;p&gt;首先看一下 &lt;code&gt;Subscription&lt;/code&gt; 模块。&lt;/p&gt;
&lt;h4&gt;createListenerCollection&lt;/h4&gt;
&lt;p&gt;实现一个订阅发布模式，提供 &lt;code&gt;clear&lt;/code&gt;，&lt;code&gt;notify&lt;/code&gt;，&lt;code&gt;subscribe&lt;/code&gt; 方法。分别用来清除当前队列，触发事件，订阅方法。&lt;/p&gt;
&lt;h4&gt;Subscription&lt;/h4&gt;
&lt;p&gt;订阅类，订阅 &lt;code&gt;onStateChange&lt;/code&gt; 事件，同时确保当父级也有事件订阅时，事件触发时，先触发父级事件，然后再触发当前组件事件。&lt;/p&gt;
&lt;h3&gt;makeSelectorStateful(sourceSelector,store):selector&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function makeSelectorStateful(sourceSelector, store) {
  // wrap the selector in an object that tracks its results between runs.
  const selector = {
    run: function runComponentSelector(props) {
      try {
        const nextProps = sourceSelector(store.getState(), props);
        if (nextProps !== selector.props || selector.error) {
          selector.shouldComponentUpdate = true;
          selector.props = nextProps;
          selector.error = null;
        }
      } catch (error) {
        selector.shouldComponentUpdate = true;
        selector.error = error;
      }
    },
  };

  return selector;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;selector&lt;/code&gt; 用来通过 &lt;code&gt;state&lt;/code&gt; 和 &lt;code&gt;prop&lt;/code&gt; 来计算出下一个状态的 &lt;code&gt;props&lt;/code&gt;，然后传递给连接的组件。
此函数是对 &lt;code&gt;selector&lt;/code&gt; 的封装，在计算 &lt;code&gt;props&lt;/code&gt; 的基础上，并记录上一次计算的结果。&lt;/p&gt;
&lt;h3&gt;connectAdvanced(selectorFactory,{options}):wrapWithConnect(WrappedComponent)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;connectAdvanced&lt;/code&gt; 的功能主要是根据传入的 &lt;code&gt;selectorFactory&lt;/code&gt;, &lt;code&gt;options&lt;/code&gt; 返回用于生成 &lt;code&gt;Connect&lt;/code&gt; 组件一个高阶组件。&lt;/p&gt;
&lt;h3&gt;connect&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;    class Connect extends Component {
      constructor(props, context) {
        super(props, context)
        ...
        this.initSelector()
        this.initSubscription()
      }
      ...
      initSelector() {
        const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
        this.selector = makeSelectorStateful(sourceSelector, this.store)
        this.selector.run(this.props)
      }

      initSubscription() {
        if (!shouldHandleStateChanges) return

        // parentSub&apos;s source should match where store came from: props vs. context. A component
        // connected to the store via props shouldn&apos;t use subscription from context, or vice versa.
        const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
        this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this))

        // `notifyNestedSubs` is duplicated to handle the case where the component is  unmounted in
        // the middle of the notification loop, where `this.subscription` will then be null. An
        // extra null check every change can be avoided by copying the method onto `this` and then
        // replacing it with a no-op on unmount. This can probably be avoided if Subscription&apos;s
        // listeners logic is changed to not call listeners that have been unsubscribed in the
        // middle of the notification loop.
        this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription)
      }
      ...
      render() {
        const selector = this.selector
        selector.shouldComponentUpdate = false

        if (selector.error) {
          throw selector.error
        } else {
          return createElement(WrappedComponent, this.addExtraProps(selector.props))
        }
      }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义在 &lt;code&gt;connectAdvanced&lt;/code&gt; 内部的组件，通过 &lt;code&gt;context&lt;/code&gt; 获取 &lt;code&gt;store&lt;/code&gt; 中数据，通过 &lt;code&gt;initSelector&lt;/code&gt; 方法初始化一个包含初始 &lt;code&gt;store&lt;/code&gt; 的 &lt;code&gt;selector&lt;/code&gt;, 并通过 &lt;code&gt;initSubscription&lt;/code&gt; 方法订阅 &lt;code&gt;store&lt;/code&gt; 的变化。
此组件的 &lt;code&gt;render&lt;/code&gt; 方法只是一个代理，用来把通过 &lt;code&gt;selector&lt;/code&gt; 计算出来的 &lt;code&gt;props&lt;/code&gt; 传递给需要包裹的组件。&lt;/p&gt;
&lt;h2&gt;connect&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;connect&lt;/code&gt; 引入了以下模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import connectAdvanced from &quot;../components/connectAdvanced&quot;;
import shallowEqual from &quot;../utils/shallowEqual&quot;;
import defaultMapDispatchToPropsFactories from &quot;./mapDispatchToProps&quot;;
import defaultMapStateToPropsFactories from &quot;./mapStateToProps&quot;;
import defaultMergePropsFactories from &quot;./mergeProps&quot;;
import defaultSelectorFactory from &quot;./selectorFactory&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只是在 &lt;code&gt;connectAdvanced&lt;/code&gt; 的基础上，传入了 &lt;code&gt;selectorFactory&lt;/code&gt; 和 &lt;code&gt;mapStateToPropsFactories&lt;/code&gt;, &lt;code&gt;mapDispatchToProps&lt;/code&gt;, &lt;code&gt;mapDispatchToPropsFactories&lt;/code&gt; 作为 &lt;code&gt;connectAdvanced&lt;/code&gt; 的 &lt;code&gt;options&lt;/code&gt; 参数。&lt;/p&gt;
&lt;h3&gt;defaultSelectorFactory&lt;/h3&gt;
&lt;p&gt;定义了默认 &lt;code&gt;selector&lt;/code&gt; 的行为，用来从传入的 &lt;code&gt;state&lt;/code&gt; 中计算下一个状态的 &lt;code&gt;props&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;defaultMap xxx To xxx Factories&lt;/h3&gt;
&lt;p&gt;这类模块用来生成映射到 props 方法的工厂函数，它们的代码如下形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { bindActionCreators } from &quot;redux&quot;;

import { wrapMapToPropsConstant, wrapMapToPropsFunc } from &quot;./wrapMapToProps&quot;;

export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
  return typeof mapDispatchToProps === &quot;function&quot;
    ? wrapMapToPropsFunc(mapDispatchToProps, &quot;mapDispatchToProps&quot;)
    : undefined;
}

export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
  return !mapDispatchToProps
    ? wrapMapToPropsConstant((dispatch) =&amp;gt; ({ dispatch }))
    : undefined;
}

export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
  return mapDispatchToProps &amp;amp;&amp;amp; typeof mapDispatchToProps === &quot;object&quot;
    ? wrapMapToPropsConstant((dispatch) =&amp;gt;
        bindActionCreators(mapDispatchToProps, dispatch),
      )
    : undefined;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，主要引用了 &lt;code&gt;wrapMapToProps&lt;/code&gt; 这个模块。&lt;/p&gt;
&lt;h4&gt;wrapMapToProps&lt;/h4&gt;
&lt;h5&gt;wrapMapToPropsFunc&lt;/h5&gt;
&lt;p&gt;这个方法是一个通过把 &lt;code&gt;mapToProps&lt;/code&gt; 函数包裹起来作为一个代理函数,用来做一下几件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检测 &lt;code&gt;mapToProps&lt;/code&gt; 函数的调用是否依赖于 &lt;code&gt;props&lt;/code&gt;, 用来供 &lt;code&gt;selectorFactory&lt;/code&gt; 来决定在 &lt;code&gt;props&lt;/code&gt; 变化的时候是否需要重新调用。&lt;/li&gt;
&lt;li&gt;在第一次调用的时候，如果 &lt;code&gt;mapToProp&lt;/code&gt; 执行后返回另一个函数，那么处理它，并且把这个返回的函数作为新的 &lt;code&gt;mapToProps&lt;/code&gt; 用来处理后续的调用。&lt;/li&gt;
&lt;li&gt;当第一次调用的时候，验证调用的结果是否是一个字面量对象。为了能够警告开发者，他们的 &lt;code&gt;mapToProps&lt;/code&gt; 函数没有返回一个合法的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;react-redux 就是一个用来绑定 redux 到 react 的一个库。如果不用 react-redux 的话，直接在 react 里面去用 redux 也是可以的，只是过于麻烦。需要手动监听 &lt;code&gt;store&lt;/code&gt; 的变化，并手动执行组件的 &lt;code&gt;render&lt;/code&gt; 方法。
react-redux 提供了一个高阶组件 &lt;code&gt;connect&lt;/code&gt; 来帮我们做这件事情，而且帮我们做了一些优化。我们需要的只是，提供 &lt;code&gt;mapDispatchToProps&lt;/code&gt; 和 &lt;code&gt;mapStateToProps&lt;/code&gt; 给需要监听 &lt;code&gt;store&lt;/code&gt; 的组件就可以了。&lt;/p&gt;
</content:encoded></item><item><title>初试 react-router</title><link>https://ab.kisnows.com/zh-CN/2015/10/09/react-router/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/10/09/react-router/</guid><pubDate>Fri, 09 Oct 2015 05:58:24 GMT</pubDate><content:encoded>&lt;p&gt;react-router 提供简单有强大 API 来方便我们在&lt;code&gt;React&lt;/code&gt;搭建的单页面中实现路由功能。
首先看一个简单的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import { findDOMNode, render } from &quot;react-dom&quot;;
import { Link, Route, Router } from &quot;react-router&quot;;

let Page1 = React.createClass({
  render() {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;h1&amp;gt;Page1&amp;lt;/h1&amp;gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit.
        Ab accusantium animi dicta dignissimos earum eos esse impedit ipsum iste
        laboriosam numquam odio perspiciatis porro, quas sequi tempore vero
        vitae voluptates?
      &amp;lt;/div&amp;gt;
    );
  },
});

let Page2 = React.createClass({
  render() {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;h1&amp;gt;Page2&amp;lt;/h1&amp;gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit.
        Fuga porro voluptas voluptatum. Corporis debitis deleniti, doloremque et
        eum ex id iste magni nobis nostrum quae, reiciendis rem repellendus
        similique tempora.
      &amp;lt;/div&amp;gt;
    );
  },
});

let App = React.createClass({
  render() {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;Link to=&quot;/page1&quot;&amp;gt;Page1&amp;lt;/Link&amp;gt;
        &amp;lt;Link to=&quot;/page2&quot;&amp;gt;Page2&amp;lt;/Link&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  },
});

render(
  &amp;lt;Router&amp;gt;
    &amp;lt;Route path=&quot;/&quot; component={App}&amp;gt;
      &amp;lt;Route path=&quot;page1&quot; component={Page1} /&amp;gt;
      &amp;lt;Route path=&quot;page2&quot; component={Page2} /&amp;gt;
    &amp;lt;/Route&amp;gt;
  &amp;lt;/Router&amp;gt;,
  document.body,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只需要在 &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; 标签中的 to 属性中定义路径，那么在渲染出的页面中点击 Page1 或 Page2 标签，就会显示相应的 Components，而且 url 也会跟着路径进行变化。
我们来简单分析一个这段代码，这里我们定义了三个路由，一个指向根目录即 &apos;/&apos;,一个指向 &apos;page/&apos;,一个指向 &apos;page/2&apos;.
路由定义必须包裹在 &lt;code&gt;&amp;lt;Router&amp;gt;&amp;lt;/Router&amp;gt;&lt;/code&gt; 中。
&lt;code&gt;&amp;lt;Route&amp;gt;&amp;lt;/Route&amp;gt;&lt;/code&gt;为每一个路由要定义的路径，并处理它的行为。其中 path 为路径， component 为要渲染的组件。并且还可以提供相应的 handle，用来处理控制路由的行为。
接下来我们来模拟一个拦截登录的行为。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import TransitionGroup from &quot;react-addons-transition-group&quot;;
import { findDOMNode, render } from &quot;react-dom&quot;;
import { History, Link, Route, Router } from &quot;react-router&quot;;

require(&quot;./index.css&quot;);

let goLogin = {
  isLogin: true,
  login() {
    this.isLogin ? this.onChange(false) : this.onChange(true);
    this.isLogin = !this.isLogin;
  },
  onChange() {},
};

let App = React.createClass({
  getInitialState() {
    return {
      login: goLogin.isLogin,
    };
  },
  updateAuth(logged) {
    this.setState({
      login: logged,
    });
  },
  componentWillMount() {
    goLogin.onChange = this.updateAuth;
  },
  render() {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;nav className=&quot;nav&quot;&amp;gt;
          {this.state.login ? (
            &amp;lt;Link to=&quot;/logout&quot;&amp;gt;Log out&amp;lt;/Link&amp;gt;
          ) : (
            &amp;lt;Link to=&quot;/login&quot;&amp;gt;Log in&amp;lt;/Link&amp;gt;
          )}
        &amp;lt;/nav&amp;gt;
        &amp;lt;ul&amp;gt;
          &amp;lt;li&amp;gt;
            &amp;lt;Link to=&quot;/page1&quot; activeClassName=&quot;active&quot;&amp;gt;
              Page1
            &amp;lt;/Link&amp;gt;
          &amp;lt;/li&amp;gt;
          &amp;lt;li&amp;gt;
            &amp;lt;Link to=&quot;/page2&quot; activeClassName=&quot;active&quot;&amp;gt;
              Page2
            &amp;lt;/Link&amp;gt;
          &amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
        {this.props.children}
      &amp;lt;/div&amp;gt;
    );
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;let Login = React.createClass({
  mixins: [History],
  handleClick(event) {
    event.preventDefault();
    goLogin.login();
    var { location } = this.props;
    if (location.state &amp;amp;&amp;amp; location.state.nextPathname) {
      this.history.replaceState(null, location.state.nextPathname);
    } else {
      this.history.replaceState(null, &quot;/about&quot;);
    }
    console.log(this.history);
  },
  render() {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;button onClick={this.handleClick}&amp;gt;Login&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  },
});

let Logout = React.createClass({
  componentDidMount() {
    goLogin.login();
  },
  render() {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;p&amp;gt;You are now logged out&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  },
});

let About = React.createClass({
  render() {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;h1&amp;gt;登录成功&amp;lt;/h1&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  },
});

let Page1 = React.createClass({
  render() {
    return (
      &amp;lt;div className=&quot;page&quot;&amp;gt;
        &amp;lt;h1&amp;gt;Page1&amp;lt;/h1&amp;gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit.
        Ab accusantium animi dicta dignissimos earum eos esse impedit ipsum iste
        laboriosam numquam odio perspiciatis porro, quas sequi tempore vero
        vitae voluptates?
      &amp;lt;/div&amp;gt;
    );
  },
});
let Page2 = React.createClass({
  getInitialState() {
    return {
      className: &quot;pageactive&quot;,
    };
  },
  handler() {
    if (this.state.className === &quot;pageactive&quot;) {
      this.setState({
        className: &quot;&quot;,
      });
    } else {
      this.setState({
        className: &quot;pageactive&quot;,
      });
    }
  },
  render() {
    return (
      &amp;lt;div className={`${this.state.className} page`} onClick={this.handler}&amp;gt;
        &amp;lt;h1&amp;gt;Page2&amp;lt;/h1&amp;gt;Lorem ipsum dolor sit amet, consectetur adipisicing elit.
        Fuga porro voluptas voluptatum. Corporis debitis deleniti, doloremque et
        eum ex id iste magni nobis nostrum quae, reiciendis rem repellendus
        similique tempora.
      &amp;lt;/div&amp;gt;
    );
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义 &lt;code&gt;needLogin&lt;/code&gt; 函数，用来判断用户是否登录并根据情况跳转到相应的页面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function needLogin(nextState, replaceState) {
  if (!goLogin.isLogin) {
    replaceState({ nextPathname: nextState.location.pathname }, &quot;/login&quot;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，执行 &lt;code&gt;render&lt;/code&gt; 命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;render(
  &amp;lt;Router&amp;gt;
    &amp;lt;Route path=&quot;/&quot; component={App}&amp;gt;
      &amp;lt;Route path=&quot;login&quot; component={Login} /&amp;gt;
      &amp;lt;Route path=&quot;logout&quot; component={Logout} /&amp;gt;
      &amp;lt;Route path=&quot;about&quot; component={About} /&amp;gt;
      &amp;lt;Route path=&quot;page1&quot; component={Page1} /&amp;gt;
      &amp;lt;Route path=&quot;page2&quot; component={Page2} onEnter={needLogin} /&amp;gt;
    &amp;lt;/Route&amp;gt;
  &amp;lt;/Router&amp;gt;,
  document.body,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;http://kisnows.com/F2E-practice/react-router/src/index.html&quot;&gt;点击这里查看 DEMO&lt;/a&gt;
这段代码实现了这么一个功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先页面默认为未登录状态。&lt;/li&gt;
&lt;li&gt;此时点击 Page1,会显示 Page1 的内容。&lt;/li&gt;
&lt;li&gt;点击 Page2 的时候，Page 内容的显示被拦截，出现登录页面。&lt;/li&gt;
&lt;li&gt;当你点击 login 后，就会显示 Page2 的内容，并且状态切换为 Logout.&lt;/li&gt;
&lt;li&gt;当我们点击 Logout 的时候，就会退出登录。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在这段代码中，当我们点击 page2 的时候，首先会触发 &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; 标签中的 to 属性来找到对应的 path 为 page2 的路由，然后会触发 needLogin 事件。
在 needLogin 事件中，我们判断 goLogin.isLogin 事件，如果未登录，则将路径替换为 &apos;/login&apos;.
并在 &lt;code&gt;&amp;lt;Route&amp;gt;&lt;/code&gt; 中找到 path 为 &apos;/login&apos; 的一项，然后渲染 Login 组件。
也就是说我们可以在 &lt;code&gt;&amp;lt;Route&amp;gt;&lt;/code&gt; 中监听事件，以此来根据需要动态的改变路由地址。&lt;/p&gt;
</content:encoded></item><item><title>React 源码浅析之 - 入口文件</title><link>https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-1/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-1/</guid><pubDate>Thu, 21 Sep 2017 00:10:41 GMT</pubDate><content:encoded>&lt;p&gt;本系列文章以 react 最新的版本： 16.0.0-beta.5 为准。
首先我们入口文件 ReactEntry.js 来看，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ReactBaseClasses = require(&quot;ReactBaseClasses&quot;);
var ReactChildren = require(&quot;ReactChildren&quot;);
var ReactElement = require(&quot;ReactElement&quot;);
var ReactVersion = require(&quot;ReactVersion&quot;);

var onlyChild = require(&quot;onlyChild&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依赖模块：
• ReactBaseClasses
• ReactChildren
• ReactElement
• onlyChild
• ReactElementValidator
接着:&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var createElement = ReactElement.createElement;
var createFactory = ReactElement.createFactory;
var cloneElement = ReactElement.cloneElement;

if (__DEV__) {
  var ReactElementValidator = require(&quot;ReactElementValidator&quot;);
  createElement = ReactElementValidator.createElement;
  createFactory = ReactElementValidator.createFactory;
  cloneElement = ReactElementValidator.cloneElement;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到在开发环境下提供了对组件的验证。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var React = {
  Children: {
    map: ReactChildren.map,
    forEach: ReactChildren.forEach,
    count: ReactChildren.count,
    toArray: ReactChildren.toArray,
    only: onlyChild,
  },

  Component: ReactBaseClasses.Component,
  PureComponent: ReactBaseClasses.PureComponent,
  unstable_AsyncComponent: ReactBaseClasses.AsyncComponent,

  createElement: createElement,
  cloneElement: cloneElement,
  isValidElement: ReactElement.isValidElement,

  createFactory: createFactory,

  version: ReactVersion,

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
    ReactCurrentOwner: require(&quot;ReactCurrentOwner&quot;),
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这些就是 React 提供的所有 API 了。
我会在后续的文章依次对这些 API 的实现进行解读。&lt;/p&gt;
&lt;h1&gt;相关文章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;{% post_link react-source-code-analyze-1 React 源码浅析之 - 入口文件 %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-2 React 源码浅析之 - ReactBaseClasses %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-3 React 源码浅析之 - ReactChildren %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-4 React 源码浅析之 - ReactElement %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-5 React 源码浅析之 - onlyChildren %}&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>React 源码浅析之 - ReactBaseClasses</title><link>https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-2/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-2/</guid><pubDate>Thu, 21 Sep 2017 00:19:00 GMT</pubDate><content:encoded>&lt;h1&gt;引入的模块&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;var ReactNoopUpdateQueue = require(&quot;ReactNoopUpdateQueue&quot;);

var emptyObject = require(&quot;fbjs/lib/emptyObject&quot;);
var invariant = require(&quot;fbjs/lib/invariant&quot;);
var lowPriorityWarning = require(&quot;lowPriorityWarning&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中， &lt;code&gt;ReactNoopUpdateQueue&lt;/code&gt; 是默认的 updater ，用来提供 &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;replaceState&lt;/code&gt;, &lt;code&gt;setState&lt;/code&gt; 的入队操作，但可能是由于是默认 &lt;code&gt;updater&lt;/code&gt; 的原因，只提供了 API 和对入参的校验，但没有提供实际的功能。比如：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enqueueSetState: function(
  // 需要 render 的实例
  publicInstance,
  // 接下来要 merge 的 state
  partialState,
  // 可选参数，setState 组件 update 后的回调
  callback,
  // 可选参数，调用函数的的名字
  callerName,
) {
  warnNoop(publicInstance, &apos;setState&apos;);
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其余几个模块是一些通用辅助模块，就不细说了。&lt;/p&gt;
&lt;h1&gt;Export 的对象&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  Component: ReactComponent,
  PureComponent: ReactPureComponent,
  AsyncComponent: ReactAsyncComponent,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们依次来看这几个 Component .&lt;/p&gt;
&lt;h2&gt;ReactComponent&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Base class helpers for the updating state of a component.
 */
function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}
ReactComponent.prototype.isReactComponent = {};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用来创建基础组件的构造函数，其中 &lt;code&gt;refs&lt;/code&gt; 默认是一个空对象，而 &lt;code&gt;updater&lt;/code&gt; 默认就是我们上面说的 &lt;code&gt;ReactNoopUpdateQueue&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;setState&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ReactComponent.prototype.setState = function (partialState, callback) {
  invariant(
    typeof partialState === &quot;object&quot; ||
      typeof partialState === &quot;function&quot; ||
      partialState == null,
    &quot;setState(...): takes an object of state variables to update or a &quot; +
      &quot;function which returns an object of state variables.&quot;,
  );
  this.updater.enqueueSetState(this, partialState, callback, &quot;setState&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给它添加 &lt;code&gt;setState&lt;/code&gt; 方法，实际操作就是把下一个要设置的 state 放入到更新队列里面。注释中提到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;永远使用 setState 这个方法去更改 state ，你应该认为 this.state 是不可变的&lt;/li&gt;
&lt;li&gt;不保证 this.state 会及时更新，也就是说调用 this.state 可能拿到的还是旧的 state&lt;/li&gt;
&lt;li&gt;不保证 setState 会同步调用，有可能会把几个 setState 的调用一次性批量更新掉，一定要在某个 setState 的调用完成后执行-操作，可以提供可选的 callback 函数。&lt;/li&gt;
&lt;li&gt;当一个 callback 函数传入给 setState 的时候，它会在未来的某个时机调用。会用最新的组件参数 (state, props, context). 而这些参数和此时组件本身的 this.* 上的参数值可能会不一样，因为 callback 函数有可能在 receiveProps 之后 shouldComponentUpdate 之前调用，而此时这个新的 state, props 和 context 还没有赋值给 this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其核心思想就是说， setState 的调用是异步的，从代码 &lt;code&gt;this.updater.enqueueSetState(this, partialState, callback, &apos;setState&apos;);&lt;/code&gt; 中可以看到，只是把要更新的 state 放入到了更新队列里面，而不是直接进行 state 的更新。&lt;/p&gt;
&lt;h3&gt;forceUpdate&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ReactComponent.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this, callback, &quot;forceUpdate&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个强制更新的方法，这个方法不会触发 &lt;code&gt;shoudleComponentUpdate&lt;/code&gt; ，但是会正常调用 &lt;code&gt;componentWillUpdate&lt;/code&gt; 和 &lt;code&gt;componentDidUpdate&lt;/code&gt; .
要确保调用这个方法的时候，所有的 DOM 事务操作已经完成。
只有在当你知道一个组件的深层次的 state 变化了，但是并没有调用 setState 的时候才需要调用这个方法。&lt;/p&gt;
&lt;h3&gt;不推荐的方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Deprecated APIs. These APIs used to exist on classic React classes but since
 * we would like to deprecate them, we&apos;re not going to move them over to this
 * modern base class. Instead, we define a getter that warns if it&apos;s accessed.
 */
if (__DEV__) {
  var deprecatedAPIs = {
    isMounted: [
      &quot;isMounted&quot;,
      &quot;Instead, make sure to clean up subscriptions and pending requests in &quot; +
        &quot;componentWillUnmount to prevent memory leaks.&quot;,
    ],
    replaceState: [
      &quot;replaceState&quot;,
      &quot;Refactor your code to use setState instead (see &quot; +
        &quot;https://github.com/facebook/react/issues/3236).&quot;,
    ],
  };
  var defineDeprecationWarning = function (methodName, info) {
    Object.defineProperty(ReactComponent.prototype, methodName, {
      get: function () {
        lowPriorityWarning(
          false,
          &quot;%s(...) is deprecated in plain JavaScript React classes. %s&quot;,
          info[0],
          info[1],
        );
        return undefined;
      },
    });
  };
  for (var fnName in deprecatedAPIs) {
    if (deprecatedAPIs.hasOwnProperty(fnName)) {
      defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里可以看到 &lt;code&gt;isMounted&lt;/code&gt; 和 &lt;code&gt;replaceState&lt;/code&gt; 这两个方法，已经被官方认为将要废弃的 API ，在开发过程中应该尽量避免使用它们。&lt;/p&gt;
&lt;h2&gt;PureComponent&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function ReactPureComponent(props, context, updater) {
  // Duplicated from ReactComponent.
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

function ComponentDummy() {}
ComponentDummy.prototype = ReactComponent.prototype;
var pureComponentPrototype = (ReactPureComponent.prototype =
  new ComponentDummy());
pureComponentPrototype.constructor = ReactPureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, ReactComponent.prototype);
pureComponentPrototype.isPureReactComponent = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个典型 js 实现继承的方式，可以看到 PureComponent 继承自 ReactComponent ，PureComponent 拥有 ReactComponent 的所有属性和方法，并在此基础上多了一个属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pureComponentPrototype.isPureReactComponent = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面在组件更新部分会用到这个属性。&lt;/p&gt;
&lt;h2&gt;AsyncComponent&lt;/h2&gt;
&lt;p&gt;这个组件在我实际开发过程中还没有用到过，我们来看一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function ReactAsyncComponent(props, context, updater) {
  // Duplicated from ReactComponent.
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

var asyncComponentPrototype = (ReactAsyncComponent.prototype =
  new ComponentDummy());
asyncComponentPrototype.constructor = ReactAsyncComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(asyncComponentPrototype, ReactComponent.prototype);
asyncComponentPrototype.unstable_isAsyncReactComponent = true;
asyncComponentPrototype.render = function () {
  return this.props.children;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和 PureComponent 一样，同样继承自 ReactComponent. 再次基础上添加了
unstable_isAsyncReactComponent 属性，并设置了 render 方法为直接返回它的 children. 从新添加的属性名可以看到，这是一个还没有稳定下来的特性组件。从名字和内容初步判断是用来创建异步的组件，先用一个 React 组件占位，当 props 传入 children 的时候，在进行渲染，从 render 方法直接 return 可以猜到这样可以实现无多余嵌套 html 标签的组件。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;这个模块定义了 React 中会用到的三个基础组件类，需要注意的是： 1. State 应该被看作是 immutable 的，所有的对 state 的修改只能通过 setState 来完成，不能使用 &lt;code&gt;this.state = **&lt;/code&gt; 来进行 state 的更新 2. setState 是一个异步的操作，所以在调用 setState 后从 this.state 上直接取值可能不是你期望的结果，读取的结果有可能依旧是老的 state
而组件内部的 updater 逻辑，这个模块里面并没有定义具体的执行逻辑。比如调用 setState 后会对要更新的 state 进行入队操作，那么这个操作过程中发生了什么，究竟是如何进行批量的更新的，以及什么时候 state 才会更新完毕，那就需要从其他的模块里来寻找答案了。&lt;/p&gt;
&lt;h1&gt;相关文章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;{% post_link react-source-code-analyze-1 React 源码浅析之 - 入口文件 %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-2 React 源码浅析之 - ReactBaseClasses %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-3 React 源码浅析之 - ReactChildren %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-4 React 源码浅析之 - ReactElement %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-5 React 源码浅析之 - onlyChildren %}&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>React 源码浅析之 - ReactChildren</title><link>https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-3/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-3/</guid><pubDate>Thu, 21 Sep 2017 00:19:09 GMT</pubDate><content:encoded>&lt;h1&gt;引入的模块&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;var ReactElement = require(&quot;ReactElement&quot;);

var emptyFunction = require(&quot;fbjs/lib/emptyFunction&quot;);
var invariant = require(&quot;fbjs/lib/invariant&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来看一下 ReactElement 模块，其他两个都是工具函数，不用关心。&lt;/p&gt;
&lt;h1&gt;Export 的对象&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;var ReactChildren = {
  forEach: forEachChildren,
  map: mapChildren,
  count: countChildren,
  toArray: toArray,
};

module.exports = ReactChildren;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依次来看一下这个四个 API&lt;/p&gt;
&lt;h2&gt;forEach&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  var traverseContext = getPooledTraverseContext(
    null,
    null,
    forEachFunc,
    forEachContext,
  );
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;入参： children, forEachFunc, forEachContext.
首先通过 &lt;code&gt;getPooledTraverseContext&lt;/code&gt; 拿到一个遍历的上下文对象 &lt;code&gt;traverseContext&lt;/code&gt;，然后调用 traverseAllChildren 方法来遍历所有传入 children 的后代节点。
最后释放当前的 &lt;code&gt;traverseContext&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h3&gt;getPooledTraverseContext&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var POOL_SIZE = 10;
var traverseContextPool = [];
function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  if (traverseContextPool.length) {
    var traverseContext = traverseContextPool.pop();
    traverseContext.result = mapResult;
    traverseContext.keyPrefix = keyPrefix;
    traverseContext.func = mapFunction;
    traverseContext.context = mapContext;
    traverseContext.count = 0;
    return traverseContext;
  } else {
    return {
      result: mapResult,
      keyPrefix: keyPrefix,
      func: mapFunction,
      context: mapContext,
      count: 0,
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义了一个 &lt;code&gt;traverseContextPool&lt;/code&gt; 以避免每次重新创建新对象的成本，大小为 10. &lt;code&gt;getPooledTraverseContext&lt;/code&gt; 方法接收四个参数，mapResult, keyPrefix, mapFunction, mapContext.
然后赋值到 traverseContext 上，除此之外还添加了一个计数器属性 count.&lt;/p&gt;
&lt;h3&gt;traverseAllChildren&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

  return traverseAllChildrenImpl(children, &apos;&apos;, callback, traverseContext);
}
 
`traverseAllChildren` 只是个空壳，里面的 `traverseAllChildrenImpl` 才是真正的实现。
** traverseAllChildrenImpl **

function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  var type = typeof children;

  if (type === &apos;undefined&apos; || type === &apos;boolean&apos;) {
    // All of the above are perceived as null.
    children = null;
  }

  if (
    children === null ||
    type === &apos;string&apos; ||
    type === &apos;number&apos; ||
    // The following is inlined from ReactElement. This means we can optimize
    // some checks. React Fiber also inlines this logic for similar purposes.
    (type === &apos;object&apos; &amp;amp;&amp;amp; children.$$typeof === REACT_ELEMENT_TYPE)
  ) {
    callback(
      traverseContext,
      children,
      // If it&apos;s the only child, treat the name as if it was wrapped in an array
      // so that it&apos;s consistent if the number of children grows.
      nameSoFar === &apos;&apos; ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收四个参数，首先判断 children 参数的 type ，不合法都认为 children 是 null.
紧接着的一堆判断，就是说当 children 是单个合法 React 元素的时候，执行 callback 函数，并返回 1，因为后面会递归的调用当前这个函数，所以这里也是递归调用的出口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var child;
var nextName;
var subtreeCount = 0; // Count of children found in the current subtree.
var nextNamePrefix = nameSoFar === &apos;&apos; ? SEPARATOR : nameSoFar + SUBSEPARATOR;

if (Array.isArray(children)) {
  for (var i = 0; i &amp;lt; children.length; i++) {
    child = children[i];
    nextName = nextNamePrefix + getComponentKey(child, i);
    subtreeCount += traverseAllChildrenImpl(
      child,
      nextName,
      callback,
      traverseContext,
    );
  }
} else {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当传入的 children 是一个 &lt;code&gt;Array&lt;/code&gt; 的时候，遍历这个 Children Array ， 并对里面的每个元素调用当前的函数 &lt;code&gt;traverseAllChildrenImpl&lt;/code&gt;.
传入的参数中需要注意， nextName 用来给要遍历的 React 元素添加 key 值，callback 和 &lt;code&gt;traverseContext&lt;/code&gt; 和当前函数的值都是一样的，保证了每个子元素也能应用当前的 callback 且能访问到原始的 &lt;code&gt;traverseContext&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;} else {
  var iteratorFn =
    (ITERATOR_SYMBOL &amp;amp;&amp;amp; children[ITERATOR_SYMBOL]) ||
    children[FAUX_ITERATOR_SYMBOL];
  if (typeof iteratorFn === &apos;function&apos;) {
    if (__DEV__) {
      // Warn about using Maps as children
      if (iteratorFn === children.entries) {
        warning(
          didWarnAboutMaps,
          &apos;Using Maps as children is unsupported and will likely yield &apos; +
            &apos;unexpected results. Convert it to a sequence/iterable of keyed &apos; +
            &apos;ReactElements instead.%s&apos;,
          getStackAddendum(),
        );
        didWarnAboutMaps = true;
      }
    }

    var iterator = iteratorFn.call(children);
    var step;
    var ii = 0;
    while (!(step = iterator.next()).done) {
      child = step.value;
      nextName = nextNamePrefix + getComponentKey(child, ii++);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else if (type === &apos;object&apos;) {
    var addendum = &apos;&apos;;
    if (__DEV__) {
      addendum =
        &apos; If you meant to render a collection of children, use an array &apos; +
        &apos;instead.&apos; +
        getStackAddendum();
    }
    var childrenString = &apos;&apos; + children;
    invariant(
      false,
      &apos;Objects are not valid as a React child (found: %s).%s&apos;,
      childrenString === &apos;[object Object]&apos;
        ? &apos;object with keys {&apos; + Object.keys(children).join(&apos;, &apos;) + &apos;}&apos;
        : childrenString,
      addendum,
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当不是 Array 但是一个可迭代的对象的时候，和上面一样，递归调用 traverseAllChildrenImpl 方法。
对于其他情况，认为 child 不合法，进行报错。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  return subtreeCount;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后返回所有后代元素的数量。
整体上来看 &lt;code&gt;traverseAllChildrenImpl&lt;/code&gt; 方法的作用就是，遍历给定 children 的所有后代元素，在每个后代元素上调用 callback 方法，并给每个元素分配一个当前上下文下唯一的 key 值作为参数要传入的参数。&lt;/p&gt;
&lt;p&gt;回到 forEach 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;traverseAllChildren(children, forEachSingleChild, traverseContext);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一句就是说遍历给定 children 的所有后代元素，并给它们调用 forEachSingleChild 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function forEachSingleChild(bookKeeping, child, name) {
  var { func, context } = bookKeeping;
  func.call(context, child, bookKeeping.count++);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个传入的 callback 方法 &lt;code&gt;forEachSingleChild&lt;/code&gt; 就是从入参 bookKeeping 也就是 &lt;code&gt;traverseContext&lt;/code&gt; 中拿到 &lt;code&gt;func&lt;/code&gt; 和 context，把 context 作为 &lt;code&gt;func&lt;/code&gt; 的上下文， child 和计数器 count 作为参数进行调用。这里的 &lt;code&gt;func&lt;/code&gt; 就是 &lt;code&gt;forEachChildren&lt;/code&gt; 的入参 &lt;code&gt;forEachFunc&lt;/code&gt;，也就是需用最终用户提供的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;releaseTraverseContext(traverseContext);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;释放当前的 &lt;code&gt;traverseContext&lt;/code&gt; 也就是把 &lt;code&gt;traverseContext&lt;/code&gt; 的属性都置为 &lt;code&gt;null&lt;/code&gt; 并放入 &lt;code&gt;traverseContextPool&lt;/code&gt; 中供后续使用，提高使用效率。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function releaseTraverseContext(traverseContext) {
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  if (traverseContextPool.length &amp;lt; POOL_SIZE) {
    traverseContextPool.push(traverseContext);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;map&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  var result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给传入 children 的后代元素调用 func 并返回调用 func 的结果集合。&lt;/p&gt;
&lt;h3&gt;mapIntoWithKeyPrefixInternal&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  var escapedPrefix = &quot;&quot;;
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + &quot;/&quot;;
  }
  var traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，如果传入了 prefix ，那么转义 prefix 作为 &lt;code&gt;traverseContext&lt;/code&gt; 的 prefix key.
然后拿到一个 &lt;code&gt;traverseContext&lt;/code&gt; 对象，接着和 &lt;code&gt;forEachChildren&lt;/code&gt; 一样，遍历所有 children 的后代元素并执行给定的 callback 函数，最后对 &lt;code&gt;traverseContext&lt;/code&gt; 进行释放。&lt;/p&gt;
&lt;p&gt;唯一的不同就是这个 callback 方法：&lt;code&gt;mapSingleChildIntoContext&lt;/code&gt; .&lt;/p&gt;
&lt;h3&gt;mapSingleChildIntoContext&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  var { result, keyPrefix, func, context } = bookKeeping;

  var mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(
      mappedChild,
      result,
      childKey,
      emptyFunction.thatReturnsArgument,
    );
  } else if (mappedChild != null) {
    if (ReactElement.isValidElement(mappedChild)) {
      mappedChild = ReactElement.cloneAndReplaceKey(
        mappedChild,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        keyPrefix +
          (mappedChild.key &amp;amp;&amp;amp; (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + &quot;/&quot;
            : &quot;&quot;) +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法和上面的 &lt;code&gt;forEachSingleChildren&lt;/code&gt; 很像。从 bookKeeping 上拿到 result , keyPrefix, func, context.
Result 其实就是 &lt;code&gt;mapChildren&lt;/code&gt; 里面一开始定义的空数组， keyPrefix 就是 &lt;code&gt;mapIntoWithKeyPrefixInternal&lt;/code&gt; 里面 escapedPrefix ， func 和 context 都是 &lt;code&gt;mapChildren&lt;/code&gt; 对应的入参。&lt;/p&gt;
&lt;p&gt;首先定义 &lt;code&gt;mappedChild&lt;/code&gt; 为用户传入的 &lt;code&gt;mapFunc&lt;/code&gt; 函数调用的返回值，然后判断这个返回值 &lt;code&gt;mappedChild&lt;/code&gt; 是不是一个 &lt;code&gt;Array&lt;/code&gt;.
如果是，那么循环调用 &lt;code&gt;mapIntoWithKeyPrefixInternal&lt;/code&gt; 方法；否则在不为 &lt;code&gt;null&lt;/code&gt; 的情况且是一个合法 React 元素的时候，用一个通过 keyPrefix , 用户分配的 key 即 &lt;code&gt;mappedChild.key&lt;/code&gt; 和原有的 childkey 组成新 key 值的 &lt;code&gt;mappedChild&lt;/code&gt; 的克隆元素作为 map 的结果，push 到 result 中。&lt;/p&gt;
&lt;p&gt;整个 &lt;code&gt;mapChildren&lt;/code&gt; 方法，就是对提供的 children 的每个后代元素调用 &lt;code&gt;mapFunc&lt;/code&gt; 方法，给返回的结果设置新的 key ，最后把每一个执行的结果 &lt;code&gt;mappedChild&lt;/code&gt; 放入到一个列表中返回给用户。&lt;/p&gt;
&lt;h3&gt;countChildren&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function countChildren(children, context) {
  return traverseAllChildren(children, emptyFunction.thatReturnsNull, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个就很简单了，只是通过遍历返回所有后代节点的个数。
&lt;code&gt;emptyFunction.thatReturnsNull&lt;/code&gt;
就是一个返回为 null 的函数。&lt;/p&gt;
&lt;h3&gt;toArray&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function toArray(children) {
  var result = [];
  mapIntoWithKeyPrefixInternal(
    children,
    result,
    null,
    emptyFunction.thatReturnsArgument,
  );
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理解了上面的 mapIntoWithKeyPrefixInternal ，那么这里也很简单了。
emptyFunction.thatReturnsArgument, 是一个函数，会返回它的第一个参数。&lt;/p&gt;
&lt;p&gt;** mapSingleChildIntoContext **&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var mappedChild = func.call(context, child, bookKeeping.count++);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么这句也就是返回 child 本身了。并将结果放入到 result 里面, 最后把所有的 result 返回给调用方。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;ReactChildren 有四个 API ，而这四个 API 主要依赖于两个方法，&lt;code&gt;traverseAllChildrenImpl&lt;/code&gt; 和 &lt;code&gt;mapSingleChildIntoContext&lt;/code&gt; 其他方法都是在此之上的组合调用。
还有一个值得注意的地方，就是用到对象池 &lt;code&gt;traverseContextPool&lt;/code&gt; 。个人认为是因为在这里经常会递归调用而频繁的需要新建 &lt;code&gt;traverseContext&lt;/code&gt; 对象，而每次都重新新建对象需要在堆里面重新分配内存，成本比较高，所以引入了对象池，以提高性能。&lt;/p&gt;
&lt;h1&gt;相关文章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;{% post_link react-source-code-analyze-1 React 源码浅析之 - 入口文件 %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-2 React 源码浅析之 - ReactBaseClasses %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-3 React 源码浅析之 - ReactChildren %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-4 React 源码浅析之 - ReactElement %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-5 React 源码浅析之 - onlyChildren %}&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>React 源码浅析之 - ReactElement</title><link>https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-4/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-4/</guid><pubDate>Thu, 21 Sep 2017 00:27:33 GMT</pubDate><content:encoded>&lt;h2&gt;ReactElement&lt;/h2&gt;
&lt;p&gt;这个模块定义了 React 元素的行为和方法，首先看 ReactElement 函数：&lt;/p&gt;
&lt;h3&gt;ReactElement&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    // This tag allow us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  if (__DEV__) {
    // The validation flag is currently mutative. We put it on
    // an external backing store so that we can freeze the whole object.
    // This can be replaced with a WeakMap once they are implemented in
    // commonly used development environments.
    element._store = {};

    // To make comparing ReactElements easier for testing purposes, we make
    // the validation flag non-enumerable (where possible, which should
    // include every environment we run tests in), so the test framework
    // ignores it.
    Object.defineProperty(element._store, &quot;validated&quot;, {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, &quot;_self&quot;, {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    // Two elements created in two different places should be considered
    // equal for testing purposes and therefore we hide it from enumeration.
    Object.defineProperty(element, &quot;_source&quot;, {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抛去其中 DEV 里面的内容，其他部分很简单，定义了一个 React 元素应有属性。包括： type, key, ref, self, source, owner, props.&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;还有一个 &lt;code&gt;$$typeof&lt;/code&gt; 属性，是一个常量，用来判断这个对象是不是一个 React 元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var REACT_ELEMENT_TYPE =
  (typeof Symbol === &quot;function&quot; &amp;amp;&amp;amp; Symbol.for &amp;amp;&amp;amp; Symbol.for(&quot;react.element&quot;)) ||
  0xeac7;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ReactElement.createElement&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ReactElement.createElement = function (type, config, children) {
  var propName;

  // Reserved names are extracted
  var props = {};

  var key = null;
  var ref = null;
  var self = null;
  var source = null;

  if (config != null) {
    // ref 和 key 都属于保留 props key 值，所以这里需要做判断
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = &quot;&quot; + config.key;
    }
    // __self 和 __source 这两个属性目前没有看到他们的作用，先放着
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    // 其他的属性添加到新的 props 对象上，同时需要排除掉保留字段 RESERVED_PROPS
    // var RESERVED_PROPS = {key: true, ref: true, __self: true, __source: true,};
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &amp;amp;&amp;amp;
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // Children 可以传递一个以上的参数，这些 children 参数都会作为新分配的 props 的属性
  var childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength &amp;gt; 1) {
    var childArray = Array(childrenLength);
    for (var i = 0; i &amp;lt; childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

  // 设置 defaultProps 属性
  if (type &amp;amp;&amp;amp; type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  // 开发环境下，如果使用了保留字段 key 和 ref ，那么进行控制台报错提醒
  if (__DEV__) {
    if (key || ref) {
      if (
        typeof props.$$typeof === &quot;undefined&quot; ||
        props.$$typeof !== REACT_ELEMENT_TYPE
      ) {
        var displayName =
          typeof type === &quot;function&quot;
            ? type.displayName || type.name || &quot;Unknown&quot;
            : type;
        if (key) {
          defineKeyPropWarningGetter(props, displayName);
        }
        if (ref) {
          defineRefPropWarningGetter(props, displayName);
        }
      }
    }
  }
  // 返回一个 React 元素， ReactCurrentOwner.current 是指当前正处于构建过程中的组件，这里默认是 null
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用给定的参数来创建一个 React 元素，需要注意的是 &lt;code&gt;ref&lt;/code&gt; 和 &lt;code&gt;key&lt;/code&gt; 属于保留字段，不能作为 props 的属性传递。&lt;/p&gt;
&lt;h3&gt;ReactElement.createFactory&lt;/h3&gt;
&lt;p&gt;一个简单的工厂函数，用来方便的创建同类型的组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReactElement.createFactory = function (type) {
  var factory = ReactElement.createElement.bind(null, type);
  // Expose the type on the factory and the prototype so that it can be
  // easily accessed on elements. E.g. `&amp;lt;Foo /&amp;gt;.type === Foo`.
  // This should not be named `constructor` since this may not be the function
  // that created the element, and it may not even be a constructor.
  // Legacy hook TODO: Warn if this is accessed
  factory.type = type;
  return factory;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ReactElement.cloneAndReplaceKey&lt;/h3&gt;
&lt;p&gt;这个 API 没用过，可以用来替换一个 React 元素的保留属性： key 值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReactElement.cloneAndReplaceKey = function (oldElement, newKey) {
  var newElement = ReactElement(
    oldElement.type,
    newKey,
    oldElement.ref,
    oldElement._self,
    oldElement._source,
    oldElement._owner,
    oldElement.props,
  );

  return newElement;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ReactElement.cloneElement&lt;/h3&gt;
&lt;p&gt;cloneElement 方法和 createElement 基本一样，只是前者是通过现有的 React 元素来复制一个新的元素出来。&lt;/p&gt;
&lt;h3&gt;ReactElement.isValidElement&lt;/h3&gt;
&lt;p&gt;用来判断一个对象是不是一个合法的 React 元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReactElement.isValidElement = function (object) {
  return (
    typeof object === &quot;object&quot; &amp;amp;&amp;amp;
    object !== null &amp;amp;&amp;amp;
    object.$$typeof === REACT_ELEMENT_TYPE
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用到了上面一开始定义的 $$typeof 属性。&lt;/p&gt;
&lt;h3&gt;export&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;module.exports = ReactElement;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ReactElement 模块就是这么些东西了，主要定义了一个 React 元素应有的属性，以及操作元素的一些方法。&lt;/p&gt;
&lt;h1&gt;相关文章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;{% post_link react-source-code-analyze-1 React 源码浅析之 - 入口文件 %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-2 React 源码浅析之 - ReactBaseClasses %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-3 React 源码浅析之 - ReactChildren %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-4 React 源码浅析之 - ReactElement %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-5 React 源码浅析之 - onlyChildren %}&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>React 源码浅析之 - onlyChildren</title><link>https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-5/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/09/21/react-source-code-analyze-5/</guid><pubDate>Thu, 21 Sep 2017 00:27:39 GMT</pubDate><content:encoded>&lt;p&gt;这个模块的代码非常简单，短短十几行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ReactElement = require(&quot;ReactElement&quot;);

var invariant = require(&quot;fbjs/lib/invariant&quot;);
function onlyChild(children) {
  invariant(
    ReactElement.isValidElement(children),
    &quot;React.Children.only expected to receive a single React element child.&quot;,
  );
  return children;
}

module.exports = onlyChild;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是判断传入的 children 是不是一个合法的 React 元素，否则就抛错出来。判断的逻辑就在 ReactElement 模块里面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReactElement.isValidElement = function (object) {
  return (
    typeof object === &quot;object&quot; &amp;amp;&amp;amp;
    object !== null &amp;amp;&amp;amp;
    object.$$typeof === REACT_ELEMENT_TYPE
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;那么到这里为止，所有 React 本身提供的 API 和模块就算是看完了。但是光看 React 本身的这些代码，其实并不能解决什么问题，比如 React Component 的生命周期是怎么实现的， setState 是怎么运作的，以及是如何把 React Component 渲染到浏览器中的，还有很多问题。
有机会我会继续寻找这些答案。&lt;/p&gt;
&lt;h1&gt;相关文章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;{% post_link react-source-code-analyze-1 React 源码浅析之 - 入口文件 %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-2 React 源码浅析之 - ReactBaseClasses %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-3 React 源码浅析之 - ReactChildren %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-4 React 源码浅析之 - ReactElement %}&lt;/li&gt;
&lt;li&gt;{% post_link react-source-code-analyze-5 React 源码浅析之 - onlyChildren %}&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>react-v2ex</title><link>https://ab.kisnows.com/zh-CN/2015/12/26/react-v2ex/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/12/26/react-v2ex/</guid><pubDate>Sat, 26 Dec 2015 08:11:25 GMT</pubDate><content:encoded>&lt;p&gt;为了练手，用 react 写了个 v2ex 的首页：
&lt;a href=&quot;http://kisnows.com/react-v2ex/&quot;&gt;页面地址&lt;/a&gt;
&lt;a href=&quot;https://github.com/kisnows/react-v2ex&quot;&gt;项目地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;整个项目基础 &lt;code&gt;react&lt;/code&gt; 搭建，样式本来打算用 inlineStyle 来写的，但是发现写起来太累，于是还是用回到 &lt;code&gt;sass&lt;/code&gt;，最后用 &lt;code&gt;webpack &lt;/code&gt;来编译。本来想着看能不能直接调用 V 站的 API 来做一个纯前端的首页，所有的数据都用 ajax 来调，但是发现因为跨域的问题，前端无法拿到数据，也就放弃了，最终也只能是一个静态页面。&lt;/p&gt;
&lt;p&gt;说一下写代码中遇到的一些问题吧，首先整个项目的文件结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;│  .babelrc
│  .editorconfig
│  .eslintrc
│  .gitignore
│  index.html
│  index.js
│  package.json
│  readme.md
│  server.js
│  tree.txt
│  webpack.config.js
│
├─api
│      hot.json
│      latest.json
│
├─build
│      bundle.js
│      index.html
│
├─components
│      CommunityStatus.js
│      Footer.js
│      Header.js
│      Main.js
│      SearchInput.js
│      TopicsHot.js
│      UserLink.js
│      UserPanel.js
│
├─containers
│      App.js
│
├─sass
│  │  main.scss
│  │  _config.scss
│  │  _global.scss
│  │  _normalize.scss
│  │  _page.scss
│  │
│  └─components
│          _CommunityStatus.scss
│          _Footer.scss
│          _Header.scss
│          _Main.scss
│          _TopicHot.scss
│          _UserPanel.scss
│
├─static
│  └─images
│          qbar_light@2x.png
│
└─utils
        getData.js
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;p&gt;先说说用 react 写的优点，它火起来不是没有道理的，确实是有很多优点的，我这里只说实际码代码中的优点，不涉及 virtualDom 带来的性能提升之类的。&lt;/p&gt;
&lt;h3&gt;结构清晰&lt;/h3&gt;
&lt;p&gt;因为 react 本身就是组件化的，所以整个页面按结构被分为几个组件，每个组件自己管理自己的展示和行为，最后通过容器组合起来，结构非常清晰。
组件的状态都是通过 state 或者 props 来控制，而我认为大多数组件只需要 props 就行了，只在顶层组件上控制 state，这样可以更加清晰的管理 state。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;易于维护&lt;/h3&gt;
&lt;p&gt;因为结构清晰，所以可以预想到，这样是易于维护的。比如头部要改结构和样式，那就只改 Hearker.js 和对应 _Header.scss 就行了，或者要改逻辑，那只要修改 Hearker.js 中和 props 或者 state 相关的代码就行了，不用像以前那样在整个页面的逻辑里面去找这块的代码。
这跟我目前维护的一些老项目来比，维护性上简直是天壤之别，再也不用愁找不到代码在哪改了，也不用吐槽那一串串的不知道干什么的 jquery 代码了。&lt;/p&gt;
&lt;h3&gt;省去了模版引擎&lt;/h3&gt;
&lt;p&gt;因为 react 可以说是自带了模版引擎，类似的 jade 或者 ejs 之类的模版引擎也就不需要了，类似这样拿到数据直接渲染就行了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  {Hot.map((topic, index) =&amp;gt; (
    &amp;lt;TopicsHotItem {...topic} key={index} /&amp;gt;
  ))}
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;h3&gt;组件划分大小的疑惑&lt;/h3&gt;
&lt;p&gt;一个页面到底要划分成几个部分？根据逻辑分还是根据页面布局分？组件分到那个层级？像 V 站的这个头部，是划分成一个组件还是三个？
&lt;img src=&quot;/imgs/react-v2ex-header.png&quot; alt=&quot;头部&quot; /&gt;
当然这个可能不能算是缺点，可能是因为我经验不足以至于无法确定该如何化分。&lt;/p&gt;
&lt;h3&gt;报错不友好&lt;/h3&gt;
&lt;p&gt;比如我 className 习惯性的写成了 class，然而 console 里面只是报了个 Did you mean className 的错，但是报不出来错在哪个文件，更不用指望报错在哪行了？只能自己去找，当项目大起来的时候，这个肯定很坑爹。
&lt;img src=&quot;/imgs/react-v2ex-error.png&quot; alt=&quot;报错&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;整体来说， react 写起来还是很爽的，而且有点也很明确，组件化·单向数据流·函数式编程，虽然有一些不成熟的问题，但是优点还是突出，如果可以的话，在一些小型项目里实际试水一下应该还是不错的。&lt;/p&gt;
</content:encoded></item><item><title>Redux 源码解读（长文慎入）</title><link>https://ab.kisnows.com/zh-CN/2017/08/18/redux-source-code-read/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/08/18/redux-source-code-read/</guid><pubDate>Fri, 18 Aug 2017 00:07:34 GMT</pubDate><content:encoded>&lt;p&gt;Redux 的源码非常的精炼，短短几百行代码，确提供了强大的功能。今天，我们就来一探究竟。&lt;/p&gt;
&lt;p&gt;看源码最简单的方式，就是从入口文件来看，看它依赖哪些模块，然后在依次看这些模块的内容，最后也就对整个代码有个清晰的认识了。&lt;/p&gt;
&lt;p&gt;所以我们就从入口文件开始来看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import applyMiddleware from &quot;./applyMiddleware&quot;;
import bindActionCreators from &quot;./bindActionCreators&quot;;
import combineReducers from &quot;./combineReducers&quot;;
import compose from &quot;./compose&quot;;
import createStore from &quot;./createStore&quot;;
import warning from &quot;./utils/warning&quot;;

/*
 * This is a dummy function to check if the function name has been altered by minification.
 * If the function has been minified and NODE_ENV !== &apos;production&apos;, warn the user.
 */
function isCrushed() {}
// 就是根据 isCrushed 是否被压缩了，来警告开发者正在非生产环境使用一个压缩过的代码。
if (
  process.env.NODE_ENV !== &quot;production&quot; &amp;amp;&amp;amp;
  typeof isCrushed.name === &quot;string&quot; &amp;amp;&amp;amp;
  isCrushed.name !== &quot;isCrushed&quot;
) {
  warning(
    &quot;You are currently using minified code outside of NODE_ENV === &apos;production&apos;. &quot; +
      &quot;This means that you are running a slower development build of Redux. &quot; +
      &quot;You can use looseenvify (https://github.com/zertosh/looseenvify) for browserify &quot; +
      &quot;or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) &quot; +
      &quot;to ensure you have the correct code for your production build.&quot;,
  );
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到它依赖了下面这几个模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;createStore&lt;/li&gt;
&lt;li&gt;combineReducers&lt;/li&gt;
&lt;li&gt;bindActionCreators&lt;/li&gt;
&lt;li&gt;applyMiddleware&lt;/li&gt;
&lt;li&gt;compose&lt;/li&gt;
&lt;li&gt;warning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他没什么说的，就是把一些 API 暴露出去。那我们就先按照这个模块依赖顺序，依次进行解读。&lt;/p&gt;
&lt;h2&gt;createStore&lt;/h2&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;首先是&lt;code&gt;createStore&lt;/code&gt;， 用来创建整个应用的 store .
它的依赖模块，都是些工具函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;isPlainObject&lt;/li&gt;
&lt;li&gt;$$observable&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === &apos;function&apos; &amp;amp;&amp;amp; typeof enhancer === &apos;undefined&apos;) {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== &apos;undefined&apos;) {
    if (typeof enhancer !== &apos;function&apos;) {
      throw new Error(&apos;Expected the enhancer to be a function.&apos;)
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== &apos;function&apos;) {
    throw new Error(&apos;Expected the reducer to be a function.&apos;)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里逻辑很简单：&lt;/p&gt;
&lt;p&gt;第一个 if 语句的意思就是说，如果只传入了两个参数，且第二个参数 preloadedState 为函数，那么就认为第二个参数为 enhancer .&lt;/p&gt;
&lt;p&gt;第二个 if 语句确保 enhancer 是一个函数，并且当 enhancer 作为参数传入的时候，返回 enhancer(createStore)(reucer, preloadedState) 作为 createStore 的返回，也就是我们要的 store.&lt;/p&gt;
&lt;p&gt;第三个 if 语句确保 reducer 是一个函数。&lt;/p&gt;
&lt;p&gt;接着往下看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let currentReducer = reducer;
let currentState = preloadedState;
let currentListeners = [];
let nextListeners = currentListeners;
let isDispatching = false;

function ensureCanMutateNextListeners() {
  if (nextListeners === currentListeners) {
    nextListeners = currentListeners.slice();
  }
}

/**
 * Reads the state tree managed by the store.
 *
 * @returns {any} The current state tree of your application.
 */
function getState() {
  return currentState;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，把 &lt;code&gt;preloadState&lt;/code&gt; 赋值给 &lt;code&gt;currentState&lt;/code&gt; ，可以使应用直接重现某一个状态，也可以用来做服务端渲染时直接由后台计算出来作为应用的初始状态。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ensureCanMutateNextListeners&lt;/code&gt; 这个函数在 &lt;code&gt;nextListeners === currentListeners&lt;/code&gt; 成立时把 &lt;code&gt;currentListeners&lt;/code&gt; 复制了一份赋值给了 &lt;code&gt;nextListeners&lt;/code&gt; . 用来做什么还不太清楚，先放着。&lt;/p&gt;
&lt;p&gt;然后定义了一个获取当前 state 的方法。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;subscribe&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;接下来是一个&lt;code&gt;subscribe&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Adds a change listener. It will be called any time an action is dispatched,
 * and some part of the state tree may potentially have changed. You may then
 * call `getState()` to read the current state tree inside the callback.
 *
 * You may call `dispatch()` from a change listener, with the following
 * caveats:
 *
 * 1. The subscriptions are snapshotted just before every `dispatch()` call.
 * If you subscribe or unsubscribe while the listeners are being invoked, this
 * will not have any effect on the `dispatch()` that is currently in progress.
 * However, the next `dispatch()` call, whether nested or not, will use a more
 * recent snapshot of the subscription list.
 *
 * 2. The listener should not expect to see all state changes, as the state
 * might have been updated multiple times during a nested `dispatch()` before
 * the listener is called. It is, however, guaranteed that all subscribers
 * registered before the `dispatch()` started will be called with the latest
 * state by the time it exits.
 *
 * @param {Function} listener A callback to be invoked on every dispatch.
 * @returns {Function} A function to remove this change listener.
 */
function subscribe(listener) {
  if (typeof listener !== &quot;function&quot;) {
    throw new Error(&quot;Expected listener to be a function.&quot;);
  }

  let isSubscribed = true;

  ensureCanMutateNextListeners();
  nextListeners.push(listener);

  return function unsubscribe() {
    if (!isSubscribed) {
      return;
    }

    isSubscribed = false;

    ensureCanMutateNextListeners();
    const index = nextListeners.indexOf(listener);
    nextListeners.splice(index, 1);
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释已经说的非常明白了，注册一个 &lt;code&gt;listener&lt;/code&gt; 监听函数，把他 push 到当前的监听里列表 &lt;code&gt;nextListener&lt;/code&gt; 里面，并返回一个 &lt;code&gt;unsubscribe&lt;/code&gt; 方法用来注销当前这个监听函数。&lt;/p&gt;
&lt;h3&gt;dispatch&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function dispatch(action) {
  if (!isPlainObject(action)) {
    throw new Error(
      &quot;Actions must be plain objects. &quot; +
        &quot;Use custom middleware for async actions.&quot;,
    );
  }

  if (typeof action.type === &quot;undefined&quot;) {
    throw new Error(
      &apos;Actions may not have an undefined &quot;type&quot; property. &apos; +
        &quot;Have you misspelled a constant?&quot;,
    );
  }

  if (isDispatching) {
    throw new Error(&quot;Reducers may not dispatch actions.&quot;);
  }

  try {
    isDispatching = true;
    currentState = currentReducer(currentState, action);
  } finally {
    isDispatching = false;
  }

  const listeners = (currentListeners = nextListeners);
  for (let i = 0; i &amp;lt; listeners.length; i++) {
    const listener = listeners[i];
    listener();
  }

  return action;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!-- TODO: --&amp;gt;&lt;/p&gt;
&lt;p&gt;用来分发一个 &lt;code&gt;action&lt;/code&gt; 来改变当前的 &lt;strong&gt;state&lt;/strong&gt; . 也是唯一的改变 &lt;strong&gt;state&lt;/strong&gt; 的方法。接受一个用来描述动作的 &lt;code&gt;action&lt;/code&gt; 为参数，并且把这个 &lt;code&gt;action&lt;/code&gt; 作为函数的返回值。&lt;/p&gt;
&lt;p&gt;从代码前面的判断可以看到，action 必须是一个字面量对象，并且必须包含一个 &lt;code&gt;type&lt;/code&gt; 的属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (isDispatching) {
  throw new Error(&quot;Reducers may not dispatch actions.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从这里可以看到，如果当前正处于上一个 &lt;code&gt;action&lt;/code&gt; 的分发阶段，那么当前这个 &lt;code&gt;action&lt;/code&gt; 有可能会分发失败。&lt;/p&gt;
&lt;p&gt;后面进行当前 &lt;strong&gt;state&lt;/strong&gt; 的计算，并且按顺序去触发 &lt;code&gt;nextListeners&lt;/code&gt; 里面的监听函数。&lt;/p&gt;
&lt;h3&gt;replaceReducer&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Replaces the reducer currently used by the store to calculate the state.
 *
 * You might need this if your app implements code splitting and you want to
 * load some of the reducers dynamically. You might also need this if you
 * implement a hot reloading mechanism for Redux.
 *
 * @param {Function} nextReducer The reducer for the store to use instead.
 * @returns {void}
 */
function replaceReducer(nextReducer) {
  if (typeof nextReducer !== &quot;function&quot;) {
    throw new Error(&quot;Expected the nextReducer to be a function.&quot;);
  }

  currentReducer = nextReducer;
  dispatch({ type: ActionTypes.INIT });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;替换掉当前的 reducer 并且分发一个用来初始化的内部 action.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const ActionTypes = {
  INIT: &quot;@@redux/INIT&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;observable&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Interoperability point for observable/reactive libraries.
 * @returns {observable} A minimal observable of state changes.
 * For more information, see the observable proposal:
 * https://github.com/tc39/proposalobservable
 */
function observable() {
  const outerSubscribe = subscribe;
  return {
    /**
     * The minimal observable subscription method.
     * @param {Object} observer Any object that can be used as an observer.
     * The observer object should have a `next` method.
     * @returns {subscription} An object with an `unsubscribe` method that can
     * be used to unsubscribe the observable from the store, and prevent further
     * emission of values from the observable.
     */
    subscribe(observer) {
      if (typeof observer !== &quot;object&quot;) {
        throw new TypeError(&quot;Expected the observer to be an object.&quot;);
      }

      function observeState() {
        if (observer.next) {
          observer.next(getState());
        }
      }

      observeState();
      const unsubscribe = outerSubscribe(observeState);
      return { unsubscribe };
    },

    [$$observable]() {
      return this;
    },
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用来把一个对象变成可 observe 的方法，一般情况下用不到。&lt;/p&gt;
&lt;h3&gt;最后&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// When a store is created, an &quot;INIT&quot; action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT });

return {
  dispatch,
  subscribe,
  getState,
  replaceReducer,
  [$$observable]: observable,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分发一个 &lt;code&gt;INIT&lt;/code&gt; 的初始化 action ，用来让所有的 reducer 来返回默认的初始化 &lt;strong&gt;state&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;然后把上面的函数返回出来，作为通过 &lt;code&gt;createStore&lt;/code&gt; 创建出来的 &lt;strong&gt;store&lt;/strong&gt; 的 api.&lt;/p&gt;
&lt;h2&gt;combineReducers&lt;/h2&gt;
&lt;p&gt;这个模块用来合并多个 reducers 到一个 reducer，它的依赖模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ActionTypes&lt;/li&gt;
&lt;li&gt;isPlainObject&lt;/li&gt;
&lt;li&gt;warning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们依次来看看 combineReducers 里面的内容。&lt;/p&gt;
&lt;h3&gt;getUndefinedStateErrorMessage&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function getUndefinedStateErrorMessage(key, action) {
  const actionType = action &amp;amp;&amp;amp; action.type;
  const actionName =
    (actionType &amp;amp;&amp;amp; `&quot;${actionType.toString()}&quot;`) || &quot;an action&quot;;

  return (
    `Given action ${actionName}, reducer &quot;${key}&quot; returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义一个用来生成当 reducer 返回 &lt;code&gt;undefined&lt;/code&gt; 时错误内容的函数，没什么好说的。&lt;/p&gt;
&lt;h3&gt;getUnexpectedStateShapeWarningMessage&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache,
) {
  const reducerKeys = Object.keys(reducers);
  const argumentName =
    action &amp;amp;&amp;amp; action.type === ActionTypes.INIT
      ? &quot;preloadedState argument passed to createStore&quot;
      : &quot;previous state received by the reducer&quot;;

  if (reducerKeys.length === 0) {
    return (
      &quot;Store does not have a valid reducer. Make sure the argument passed &quot; +
      &quot;to combineReducers is an object whose values are reducers.&quot;
    );
  }

  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of &quot;` +
      {}.toString.call(inputState).match(/\s([az|AZ]+)/)[1] +
      `&quot;. Expected argument to be an object with the following ` +
      `keys: &quot;${reducerKeys.join(&apos;&quot;, &quot;&apos;)}&quot;`
    );
  }

  const unexpectedKeys = Object.keys(inputState).filter(
    (key) =&amp;gt; !reducers.hasOwnProperty(key) &amp;amp;&amp;amp; !unexpectedKeyCache[key],
  );

  unexpectedKeys.forEach((key) =&amp;gt; {
    unexpectedKeyCache[key] = true;
  });

  if (unexpectedKeys.length &amp;gt; 0) {
    return (
      `Unexpected ${unexpectedKeys.length &amp;gt; 1 ? &quot;keys&quot; : &quot;key&quot;} ` +
      `&quot;${unexpectedKeys.join(&apos;&quot;, &quot;&apos;)}&quot; found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `&quot;${reducerKeys.join(&apos;&quot;, &quot;&apos;)}&quot;. Unexpected keys will be ignored.`
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从函数名 “获取未期望 State 结构错误信息” 可以看出这个函数用来生成当传入的 &lt;code&gt;inputState&lt;/code&gt; 组成结构错误时的错误信息。&lt;/p&gt;
&lt;p&gt;Reducer 必须有 key 值（这不废话），&lt;code&gt;inputState&lt;/code&gt; 必须是一个字面量对象。且&lt;code&gt; inputState&lt;/code&gt; 的 key 都应该在 reducer 的自身属性（OwnProperty, 非原型链上的）中，并且不能在传入的 &lt;code&gt;unexpectedKeyCache&lt;/code&gt; 中。&lt;/p&gt;
&lt;h3&gt;assertReducerShape&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function assertReducerShape(reducers) {
  Object.keys(reducers).forEach((key) =&amp;gt; {
    const reducer = reducers[key];
    const initialState = reducer(undefined, { type: ActionTypes.INIT });

    if (typeof initialState === &quot;undefined&quot;) {
      throw new Error(
        `Reducer &quot;${key}&quot; returned undefined during initialization. ` +
          `If the state passed to the reducer is undefined, you must ` +
          `explicitly return the initial state. The initial state may ` +
          `not be undefined. If you don&apos;t want to set a value for this reducer, ` +
          `you can use null instead of undefined.`,
      );
    }

    const type =
      &quot;@@redux/PROBE_UNKNOWN_ACTION_&quot; +
      Math.random().toString(36).substring(7).split(&quot;&quot;).join(&quot;.&quot;);
    if (typeof reducer(undefined, { type }) === &quot;undefined&quot;) {
      throw new Error(
        `Reducer &quot;${key}&quot; returned undefined when probed with a random type. ` +
          `Don&apos;t try to handle ${ActionTypes.INIT} or other actions in &quot;redux/*&quot; ` +
          `namespace. They are considered private. Instead, you must return the ` +
          `current state for any unknown actions, unless it is undefined, ` +
          `in which case you must return the initial state, regardless of the ` +
          `action type. The initial state may not be undefined, but can be null.`,
      );
    }
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用来保证传入的 &lt;code&gt;reducers&lt;/code&gt; 的结构正确，也就说说每个 &lt;code&gt;reducer&lt;/code&gt; 都必须在收到 INIT action 后返回一个不为 &lt;code&gt;undefined&lt;/code&gt; 的 &lt;code&gt;initState&lt;/code&gt; ，并且这个 &lt;code&gt;action&lt;/code&gt; 不能在 &lt;code&gt;reducer&lt;/code&gt; 中专门去处理。这也是为什么我们在 reducer 里面一定要指定默认返回的 state 的原因.&lt;/p&gt;
&lt;h3&gt;combineReducers&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 *
 * @param {Object} reducers An object whose values correspond to different
 * reducer functions that need to be combined into one. One handy way to obtain
 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
 * undefined for any action. Instead, they should return their initial state
 * if the state passed to them was undefined, and the current state for any
 * unrecognized action.
 *
 * @returns {Function} A reducer function that invokes every reducer inside the
 * passed object, and builds a state object with the same shape.
 */
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);
  const finalReducers = {};
  for (let i = 0; i &amp;lt; reducerKeys.length; i++) {
    const key = reducerKeys[i];

    if (process.env.NODE_ENV !== &quot;production&quot;) {
      if (typeof reducers[key] === &quot;undefined&quot;) {
        warning(`No reducer provided for key &quot;${key}&quot;`);
      }
    }

    if (typeof reducers[key] === &quot;function&quot;) {
      finalReducers[key] = reducers[key];
    }
  }
  const finalReducerKeys = Object.keys(finalReducers);

  let unexpectedKeyCache;
  if (process.env.NODE_ENV !== &quot;production&quot;) {
    unexpectedKeyCache = {};
  }

  let shapeAssertionError;
  try {
    assertReducerShape(finalReducers);
  } catch (e) {
    shapeAssertionError = e;
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError;
    }

    if (process.env.NODE_ENV !== &quot;production&quot;) {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache,
      );
      if (warningMessage) {
        warning(warningMessage);
      }
    }

    let hasChanged = false;
    const nextState = {};
    for (let i = 0; i &amp;lt; finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i];
      const reducer = finalReducers[key];
      const previousStateForKey = state[key];
      const nextStateForKey = reducer(previousStateForKey, action);
      if (typeof nextStateForKey === &quot;undefined&quot;) {
        const errorMessage = getUndefinedStateErrorMessage(key, action);
        throw new Error(errorMessage);
      }
      nextState[key] = nextStateForKey;
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    return hasChanged ? nextState : state;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;combineReducer&lt;/code&gt; 接收一个用来合并成一个 &lt;code&gt;reducer&lt;/code&gt; 的对象，执行后返回一个函数，也即是我们的 rootReducer .&lt;/p&gt;
&lt;p&gt;首先把传入的 &lt;code&gt;reducers&lt;/code&gt; 按 &lt;code&gt;key&lt;/code&gt; 遍历后赋值给 &lt;code&gt;finalReducers&lt;/code&gt; . 然后进行一堆错误判断，最后返回一个函数 &lt;code&gt;combination&lt;/code&gt;. 也就是合并后的 reducer :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let hasChanged = false
const nextState = {}
// 遍历 finalReducerKeys
for (let i = 0; i &amp;lt; finalReducerKeys.length; i++) {
// 拿到当前的 reducer key
    const key = finalReducerKeys[i]
// 根据 reducer key 拿到具体的 reducer 函数
      const reducer = finalReducers[key]
// 获取之前的 key 对应的 state
      const previousStateForKey = state[key]
// 计算下一个当前 key 对应的 state
      const nextStateForKey = reducer(previousStateForKey, action)
// 如果计算出来的 state 为 undefined 那么报错
      if (typeof nextStateForKey === &apos;undefined&apos;) {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
// 把当前 key 对应的 state 赋值到下一个全局 state
      nextState[key] = nextStateForKey
// 只要有一个 key 对应的 state 发生了变化，那么就认为整个 state 发生了变化
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 根据 state 是否发生变化，返回下一个 state 或者上一个 state
    return hasChanged ? nextState : state
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;bindActionCreators&lt;/h2&gt;
&lt;p&gt;这个函数非常简单，是一个辅助函数。用来把 dispatch 绑定到一个 actionCreator 上，这样当就可以通过直接调用绑定后的函数来分发一个 action ，而不需要 &lt;code&gt;dispatch(actionCreator(…))&lt;/code&gt; 了。&lt;/p&gt;
&lt;h2&gt;applyMiddleware&lt;/h2&gt;
&lt;p&gt;这里是重点，也是一般初学者难以理解的地方，我们仔细看看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import compose from &quot;./compose&quot;;

/**
 * Creates a store enhancer that applies middleware to the dispatch method
 * of the Redux store. This is handy for a variety of tasks, such as expressing
 * asynchronous actions in a concise manner, or logging every action payload.
 *
 * See `reduxthunk` package as an example of the Redux middleware.
 *
 * Because middleware is potentially asynchronous, this should be the first
 * store enhancer in the composition chain.
 *
 * Note that each middleware will be given the `dispatch` and `getState` functions
 * as named arguments.
 *
 * @param {...Function} middlewares The middleware chain to be applied.
 * @returns {Function} A store enhancer applying the middleware.
 */
export default function applyMiddleware(...middlewares) {
  return (createStore) =&amp;gt; (reducer, preloadedState, enhancer) =&amp;gt; {
    const store = createStore(reducer, preloadedState, enhancer);
    let dispatch = store.dispatch;
    let chain = [];

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) =&amp;gt; dispatch(action),
    };
    chain = middlewares.map((middleware) =&amp;gt; middleware(middlewareAPI));
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch,
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码量非常短，依赖了模块 &lt;code&gt;compose&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;&lt;code&gt;applyMiddleware&lt;/code&gt; 函数接受一系列中间件函数作为参数，返回了一个拥有 &lt;code&gt;createStore&lt;/code&gt; 方法的闭包函数。这个函数，接收 &lt;code&gt;reducer&lt;/code&gt; ，&lt;code&gt;preloadedState&lt;/code&gt; 和 &lt;code&gt;enhancer&lt;/code&gt; 为参数。
配合 createStore 函数来看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === &apos;function&apos; &amp;amp;&amp;amp; typeof enhancer === &apos;undefined&apos;) {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== &apos;undefined&apos;) {
    if (typeof enhancer !== &apos;function&apos;) {
      throw new Error(&apos;Expected the enhancer to be a function.&apos;)
    }

    return enhancer(createStore)(reducer, preloadedState)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们这样创建 store 的时候：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const store = createStore(reducer, applyMiddleware(...middleware));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;createStore 的第二个参数是个函数，所以就会走到&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return enhancer(createStore)(reducer, preloadedState);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是由 applyMiddleware(...middleware) 的结果接管了 createStore ,实际的 store 是在 applyMiddleware 里面再次调用 createStore 创建的，此时传入的 preloadedState, enhancer 都是 undefined.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// applyMiddleware
const store = createStore(reducer, preloadedState, enhancer);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回过头来继续往下看，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//applyMiddleware
dispatch = compose(...chain)(store.dispatch);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要先看一下 &lt;code&gt;compose&lt;/code&gt; 这个模块，它的作用就是达到 &lt;code&gt;compose(f, g, h) &amp;gt; (...args) =&amp;gt; f(g(h(...args)))&lt;/code&gt; 这么一个目的。&lt;/p&gt;
&lt;p&gt;那么这里的 &lt;code&gt;dispatch&lt;/code&gt; 就是在 &lt;code&gt;store.dispatch&lt;/code&gt; 基础上经过 &lt;code&gt;middleware&lt;/code&gt; 加强封装后的 &lt;code&gt;dispatch&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const middlewareAPI = {
  getState: store.getState,
  dispatch: (action) =&amp;gt; dispatch(action),
};
// 把 middlewareAPI 传入到每个中间件中
chain = middlewares.map((middleware) =&amp;gt; middleware(middlewareAPI));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;dispatch: (action) =&amp;gt; dispatch(action)&lt;/code&gt; ，说明每个中间件中的 &lt;code&gt;dispatch&lt;/code&gt; 都是独立互不影响的，以免某个中间件中修改了 &lt;code&gt;dispatch&lt;/code&gt; 的行为。然后给每个中间件都传入 getState 和 dispatch 作为他们的参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
  ...store,
  dispatch,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后用加强后的 &lt;code&gt;dispatch&lt;/code&gt; 覆盖掉原有 store 中的 &lt;code&gt;dispatch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;整个中间件的代码看下来，可能比较抽象，我们结合一个例子来看一下：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;errorMiddleware&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default ({ dispatch, getState }) =&amp;gt;
  (next) =&amp;gt;
  (action) =&amp;gt; {
    const { error, payload } = action;
    if (error) {
      dispatch(showToast(payload.message || payload.toString()));
    }
    return next(action);
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是我们的一个错误处理中间件。它也是一个高阶函数，首先接受 &lt;code&gt;dispatch&lt;/code&gt;, &lt;code&gt;getState&lt;/code&gt; 为参数，返回一个接受 &lt;code&gt;next&lt;/code&gt; 为参数的函数。&lt;code&gt;dispatch&lt;/code&gt;, &lt;code&gt;getState&lt;/code&gt; 就是在上面代码里通过 middlewareAPI 传入了中间件中。&lt;/p&gt;
&lt;p&gt;然后我们继续看 errorMiddleware 执行后返回的接受 &lt;code&gt;next&lt;/code&gt; 为参数的函数，而 &lt;code&gt;next&lt;/code&gt; 其实就是下一个要执行的 &lt;code&gt;middleware&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;然后我们需要了解一下中间件的执行顺序，那么为了更清楚的描述一个 action 在中间件中的传播过程，我们假设有以下三个中间件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mid1 = () =&amp;gt; (next) =&amp;gt; (action) =&amp;gt; {
  console.log(&quot;mid1 before&quot;);
  next(action);
  console.log(&quot;mid1 after&quot;);
};
const mid2 = () =&amp;gt; (next) =&amp;gt; (action) =&amp;gt; {
  console.log(&quot;mid2 before&quot;);
  next(action);
  console.log(&quot;mid2 after&quot;);
};
const mid3 = () =&amp;gt; (next) =&amp;gt; (action) =&amp;gt; {
  console.log(&quot;mid3 before&quot;);
  next(action);
  console.log(&quot;mid3 after&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行 &lt;code&gt;applyMiddleware( mid1, mid2, mid3 )&lt;/code&gt;, 那么经过下面代码后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dispatch = compose(...chain)(store.dispatch);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就可以得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dispatch = (store.dispatch) =&amp;gt; mid1(mid2(mid3(store.dispatch)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的 &lt;strong&gt;midx&lt;/strong&gt; 都是已经执行了 &lt;code&gt;middleware(middlewareAPI)&lt;/code&gt; 后返回的结果。所以 &lt;code&gt;mid3&lt;/code&gt; 的 &lt;code&gt;next&lt;/code&gt; 的值就是 &lt;code&gt;store.dispatch&lt;/code&gt; 。而 &lt;code&gt;mid2&lt;/code&gt; 的 &lt;code&gt;next&lt;/code&gt; 则是 &lt;code&gt;mid3(store.dispatch)&lt;/code&gt; ，以此类推，&lt;code&gt;mid1&lt;/code&gt; 的 next 就是 &lt;code&gt; mid2(mid3(store.dispatch))&lt;/code&gt; , 这也就是在 &lt;strong&gt;middleware&lt;/strong&gt; 调用 &lt;code&gt;next&lt;/code&gt; 能够让 &lt;code&gt;action&lt;/code&gt; 转到下一个 &lt;strong&gt;middleware&lt;/strong&gt; 的原因。&lt;/p&gt;
&lt;p&gt;当我们分发一个 action 时，控制台打印出来的顺序是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mid1 before
mid2 before
mid3 before
mid3 after
mid2 after
mid1 after
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到它的流程是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行 mid1 中 next 方法调用之前的代码&lt;/li&gt;
&lt;li&gt;执行 mid2 中 next 方法调用之前的代码&lt;/li&gt;
&lt;li&gt;执行 mid3 中 next 方法调用之前的代码&lt;/li&gt;
&lt;li&gt;执行 dispatch 来分发 action&lt;/li&gt;
&lt;li&gt;执行 mid3 中 next 方法调用之后的代码&lt;/li&gt;
&lt;li&gt;执行 mid2 中 next 方法调用之后的代码&lt;/li&gt;
&lt;li&gt;执行 mid1 中 next 方法调用之后的代码&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;看一张图，会更明白一点：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/redux-source-code-read/redux-middleware.png&quot; alt=&quot;reduxmiddleware&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中红色的路径就是我们刚才描述的流程。可以看到其中还有一条黑色路径，也就是如果我们直接在 mid2 中调用 dispatch 会怎么样？我们来改一下 mid2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mid2 =
  ({ dispatch, getStore }) =&amp;gt;
  (next) =&amp;gt;
  (action) =&amp;gt; {
    console.log(&quot;mid2 before&quot;);
    dispatch(action);
    console.log(&quot;mid2 after&quot;);
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改成这样，猜猜会怎样？&lt;/p&gt;
&lt;p&gt;答案是，会一直在 mid1 before 和 mid2 before 中死循环，因为调用的 &lt;code&gt;dispatch&lt;/code&gt; 会让这个 &lt;code&gt;action&lt;/code&gt; 重新走一遍所有的中间件，也就是图中的黑色路径。那么当我们需要在一个中间件中调用 &lt;code&gt;dispatch&lt;/code&gt; 的时候，是要对 action 做判断的，只有满足某个条件的时候才调用 &lt;code&gt;dispatch&lt;/code&gt; 以免出现死循环。改造一下 mid2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mid2 = ({ dispatch, getStore }) =&amp;gt; next =&amp;gt; action =&amp;gt; {
  console.log(&apos;mid2 before&apos;)
  if(action.isApi) {
    dispatch({
      isApi: false,
      ...
    })
  }
  dispatch(action)
  console.log(&apos;mid2 after&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，就只有在 action 满足 isApi 条件的时候才会取分发一个不满足 isApi 条件的 action ，这样就不会死循环。一般在异步分发 action 的时候会经常用这个方法。比如我们生产环境用来请求数据的 callAPIMiddleware :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default ({dispatch, getState}) =&amp;gt; {
return next =&amp;gt; action =&amp;gt; {
  const {
    types,
    api,
    callType,
    meta,
    body,
    shouldCallAPI
  } = action
  const state = getState()
  const callTypeList = [&apos;get&apos;, &apos;post&apos;]
  if (!api) {
    return next(action)
  }
  if (!(types.start &amp;amp;&amp;amp; types.success &amp;amp;&amp;amp; types.failure)) {
    throw new Error(&apos;Expected types has start &amp;amp;&amp;amp; success &amp;amp;&amp;amp; failure keys.&apos;)
  }
  if (callTypeList.indexOf(callType) === 1) {
    throw new Error(`API callType Must be one of ${callTypeList}`)
  }

  const {start, success, failure} = types
  if (!shouldCallAPI(state)) {
    return false
  }

  dispatch({
    type: start,
    payload: {
      ...body
    },
    meta
  })
  const mapCallTypeToFetch = {
      post: () =&amp;gt; fetch(api, {
        method: &apos;post&apos;,
        // credentials 设置为每次请求都带上 cookie
        credentials: &apos;include&apos;,
        headers: {
          &apos;Accept&apos;: &apos;application/json&apos;,
          &apos;Content-Type&apos;: &apos;application/json&apos;
        },
        body: JSON.stringify(bodyWithSource)
      }),
      get: () =&amp;gt; {
        const toString = Object.keys(bodyWithSource).map(function (key, index) {
          return encodeURIComponent(key) + &apos;=&apos; + encodeURIComponent(bodyWithSource[key])
        }).join(&apos;&amp;amp;&apos;)
        return fetch(`${api}?${toString}`, {
          method: &apos;get&apos;,
          credentials: &apos;include&apos;,
          headers: {
            &apos;Accept&apos;: &apos;application/json&apos;
          }
        })
      }
    }
    const fetching = mapCallTypeToFetch[callType]()
... 省略一堆业务逻辑
  return fetching.then(res =&amp;gt; {
    clearTimeout(loadingTimer)
    dispatch(hideLoading())
    if (res.ok) {
      try {
        return res.json()
      } catch (err) {
        throw new Error(err)
      }
    } else {
      dispatch(showToast(&apos;请求出错&apos;))
      return Promise.reject(res.text())
    }
  })
    .then(res =&amp;gt; resBehaviour(res))
    .then(res =&amp;gt; {
      dispatch({
        type: success,
        meta,
        payload: {
          ...res.data
        }
      })
      return Promise.resolve(res)
    })
    .catch(err =&amp;gt; {
      console.error(`接口请求出错,${err}`)
      return Promise.reject(err)
    })
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于中间件就说这么多，大家应该也能理解了。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;总体上看， Redux 的源码非常短，但是各种实现都非常的精巧。&lt;/p&gt;
&lt;p&gt;而且作者非常重视对开发者的体验，注释非常的详细，整体上读起来比较轻松。错误处理也非常详细，可以帮助开发者更容易的定位错误。&lt;/p&gt;
&lt;p&gt;最后，由于本人能力有限，文中如果有错误的地方，还请指出一起讨论。&lt;/p&gt;
</content:encoded></item><item><title>店铺体验优化总结</title><link>https://ab.kisnows.com/zh-CN/2021/04/29/shop-performance-improve/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2021/04/29/shop-performance-improve/</guid><pubDate>Thu, 29 Apr 2021 00:41:08 GMT</pubDate><content:encoded>&lt;p&gt;本文整理了近两年来国际站店铺在体验优化上做的努力和探索，也取得了不错的成果，分享出来希望对其他类似场景能有帮助。&lt;/p&gt;
&lt;p&gt;性能优化大体上可以分为网络性能优化和交互体验优化：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;交互体验优化，优化页面的交互体验，滚动是否顺滑，点击反馈是否及时，操作是否有卡顿等
&lt;ul&gt;
&lt;li&gt;避免 DOM 的重绘和重排
&lt;ul&gt;
&lt;li&gt;DOM 操作尽量减少或者进行操作合并&lt;/li&gt;
&lt;li&gt;动画元素尽量采用 absolute 等脱离文档流的定位&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;确保对用户的操作反馈及时，刷新率保持 60fps
&lt;ul&gt;
&lt;li&gt;保证每一个 js 任务在 1000/60=16.67ms 执行完毕&lt;/li&gt;
&lt;li&gt;复杂计算交给 woker 进程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;网络性能优化,大多数情况是要进行首屏性能优化
&lt;ul&gt;
&lt;li&gt;减少 ttfb 的时间
&lt;ul&gt;
&lt;li&gt;减少 dns 查询、TCP 握手建链、ssl 建连&lt;/li&gt;
&lt;li&gt;避免 302 redirect&lt;/li&gt;
&lt;li&gt;减少后端 rt 时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;减少资源下载时间：比如减少 html 内容、开启 gzip 压缩等&lt;/li&gt;
&lt;li&gt;减少前端阻塞资源加载时间
&lt;ul&gt;
&lt;li&gt;减少同步 css 资源&lt;/li&gt;
&lt;li&gt;避免同步 js 标签&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;增加合适的缓存&lt;/li&gt;
&lt;li&gt;通过 ServiceWorker 等手段进行提前加载和预渲染&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体到国际站店铺： 一个由用户后台搭建并生成前台场景的页面上，在优化上依然遵循上述方向。下面是我结合店铺实际情况后在体验优化上做的一些尝试（由于合规问题，一些业务数据进行了脱敏）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BD%91%E7%BB%9C%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%8E%AA%E6%96%BD&quot;&gt;网络性能优化措施&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#pc-%E6%97%BA%E9%93%BA---%E8%A3%85%E4%BF%AE%E5%90%8E%E5%8F%B0%E5%9B%BE%E7%89%87%E4%BC%98%E5%8C%96&quot;&gt;PC 旺铺 - 装修后台图片优化&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%83%8C%E6%99%AF&quot;&gt;背景&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%96%B9%E6%A1%88&quot;&gt;方案&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#pc-%E6%97%BA%E9%93%BA---%E4%B8%89%E6%96%B9%E6%A8%A1%E5%9D%97%E7%94%A8%E6%88%B7%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96&quot;&gt;PC 旺铺 - 三方模块用户性能优化&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%83%8C%E6%99%AF-1&quot;&gt;背景&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%96%B9%E6%A1%88-1&quot;&gt;方案&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%95%88%E6%9E%9C&quot;&gt;效果&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#pc-%E6%97%BA%E9%93%BA---%E6%A8%A1%E5%9D%97%E5%BC%82%E6%AD%A5%E5%8C%96&quot;&gt;PC 旺铺 - 模块异步化&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%83%8C%E6%99%AF-2&quot;&gt;背景&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%88%86%E6%9E%90&quot;&gt;分析&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BB%93%E6%9E%9C&quot;&gt;结果&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%97%A0%E7%BA%BF%E6%97%BA%E9%93%BA---%E4%BB%8E-weex-%E8%BF%81%E7%A7%BB%E5%88%B0-react-%E5%90%8C%E6%9E%84&quot;&gt;无线旺铺 - 从 weex 迁移到 react 同构&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%83%8C%E6%99%AF-3&quot;&gt;背景&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%96%B9%E6%A1%88-2&quot;&gt;方案&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BB%93%E6%9E%9C-1&quot;&gt;结果&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%B5%81%E5%BC%8F%E6%B8%B2%E6%9F%93&quot;&gt;流式渲染&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%88%90%E6%9E%9C&quot;&gt;成果&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%BA%A4%E4%BA%92%E4%BD%93%E9%AA%8C%E4%BC%98%E5%8C%96&quot;&gt;交互体验优化&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#pc-%E6%97%BA%E9%93%BA-spa-%E6%94%B9%E9%80%A0%E6%95%88%E6%9E%9C&quot;&gt;PC 旺铺 SPA 改造效果&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%83%8C%E6%99%AF-4&quot;&gt;背景&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%96%B9%E6%A1%88-3&quot;&gt;方案&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%88%90%E6%9E%9C-1&quot;&gt;成果&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%97%A0%E7%BA%BF%E6%97%BA%E9%93%BA---spa-%E6%94%B9%E9%80%A0---tab-%E6%95%B0%E6%8D%AE%E9%A2%84%E8%BD%BD&quot;&gt;无线旺铺 - SPA 改造 - Tab 数据预载&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AD%96%E7%95%A5&quot;&gt;策略&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%88%90%E6%9E%9C-2&quot;&gt;成果&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%97%A0%E7%BA%BF%E6%97%BA%E9%93%BA---producttab-%E6%97%A0%E9%99%90%E5%8A%A0%E8%BD%BD%E6%94%B9%E9%80%A0&quot;&gt;无线旺铺 - ProductTab 无限加载改造&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%83%8C%E6%99%AF-5&quot;&gt;背景&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%96%B9%E6%A1%88-4&quot;&gt;方案&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%88%90%E6%9E%9C-3&quot;&gt;成果&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%80%BB%E7%BB%93&quot;&gt;总结&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;网络性能优化措施&lt;/h2&gt;
&lt;h3&gt;PC 旺铺 - 装修后台图片优化&lt;/h3&gt;
&lt;h4&gt;背景&lt;/h4&gt;
&lt;p&gt;店铺是一个可以卖家自己搭建页面的场景，很多卖家喜欢通过上传一个背景图来提升店铺的美观度。但这些图片的体积都非常大，一个 1920 * xxx 尺寸的图片体积都在 1M 以上，非 wifi 网络环境下加载比较慢。&lt;/p&gt;
&lt;h4&gt;方案&lt;/h4&gt;
&lt;p&gt;要对其优化并不能通过简单的图片压缩来解决，因为很多情况下压缩后都会导致图片失真严重，卖家就会提工单过来投诉。为此我们需要另一种方法来对图片体积进行缩减。通过观察可以发现，绝大多数情况下背景图中间部分大都被正常模块所覆盖，前台能看到都是中间模块展示宽度以外的内容。所以可以尝试用单色覆盖图片中间的部分，这样可以大大减少用户上传的背景图片大小。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/shop-performance-improve/2021-04-14-20-24-02-canvas-bg.png&quot; alt=&quot;bg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实现的原理很简单：把背景图画到 canvas ，然后对其中间 1200px 宽度部分进行纯色填充，核心实现代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  /**
   * 用纯色填充指定区域
   */
  coverArea (canvas) {
    const { coverColor, coverWidth } = this.props
    if (!this.state.needCover) {
      return canvas
    }
    const w = canvas.width
    const h = canvas.height
    const ctx = canvas.getContext(&apos;2d&apos;)
    const gap = (w - coverWidth) / 2
    // 清除中间 1200px 部分的内容
    ctx.clearRect(gap, 0, coverWidth, h)
    ctx.fillStyle = coverColor
    // 用给定的颜色填充中间 1200px 的内容
    ctx.fillRect(gap, 0, coverWidth, h)

    return canvas
  }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过测试：对于一张 1920 * 1080 的细节较多的风景图，在 jpeg 格式下，原图 1.6MB ，裁剪后为 464KB，缩减后为原来的 464/(1.6*1024)=0.2832 ,差不多为原图 30% 的体积，提升巨大，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/shop-performance-improve/2021-04-28-16-45-02.png&quot; alt=&quot;bgimg&quot; /&gt;
优化后既可以提升页面打开速度，又可以减轻 CDN 流量消耗，一举两得。&lt;/p&gt;
&lt;h3&gt;PC 旺铺 - 三方模块用户性能优化&lt;/h3&gt;
&lt;h4&gt;背景&lt;/h4&gt;
&lt;p&gt;由于目前三方模板&lt;a href=&quot;%E7%94%B1%E4%B8%89%E6%96%B9%E7%94%9F%E6%80%81%E8%AE%BE%E8%AE%A1%E5%B8%88%E5%BC%80%E5%8F%91%E7%9A%84%E6%A8%A1%E6%9D%BF&quot;&gt;^isv&lt;/a&gt;的静态资源走的是阿里云的 cdn, 域名为 &lt;code&gt;shopmod-icbu.alicdn.com&lt;/code&gt;，海外节点较少，资源加载慢，影响用户体验。&lt;/p&gt;
&lt;p&gt;所以尝试通过采用拥有更多海外加点的 akama 服务商的 cdn 进行转发，从而加快海外用户访问使用三方模板商铺的访问速度，以提升三方模板商家的用户访问体验。&lt;/p&gt;
&lt;h4&gt;方案&lt;/h4&gt;
&lt;p&gt;将 &lt;code&gt;shopmod-icbu.alicdn.com/1dbb3279357007484e7778d9a1060811/web-index.css&lt;/code&gt;
替换为 &lt;code&gt;http://i.alicdn.com/@shopmod/1dbb3279357007484e7778d9a1060811/web-index.css&lt;/code&gt; 然后由 i.alicdn.com 进行转发。&lt;/p&gt;
&lt;p&gt;旺铺前台的 js 文件都是由 &lt;code&gt;shop-render&lt;/code&gt; 引用的 &lt;code&gt;intl-mod-loader&lt;/code&gt; 控制异步加载的。
对于二方模块， &lt;code&gt;ownType&lt;/code&gt; 为 *，资源地址由 &lt;code&gt;shop-render&lt;/code&gt; 的 config 决定。
对于三方模块， &lt;code&gt;ownType&lt;/code&gt; 为 ** ，资源地址由三方模块的 &lt;code&gt;module-data&lt;/code&gt; 中 mds 的 assetsCss、assetsJs 决定，模块数据 assetsCss、assetsJs 地址在后端应用中进行修改。&lt;/p&gt;
&lt;h4&gt;效果&lt;/h4&gt;
&lt;p&gt;首屏在以前优化的基础上又减少了 &lt;strong&gt;12.4%&lt;/strong&gt; ，停留时长也增加了 &lt;strong&gt;4.6%&lt;/strong&gt;，蹦失率降低了 &lt;strong&gt;5.2%&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;PC 旺铺 - 模块异步化&lt;/h3&gt;
&lt;h4&gt;背景&lt;/h4&gt;
&lt;p&gt;首页后端 RT 相较于整个店铺或者说国际站来说都有点长，最终影响了首屏速度。&lt;/p&gt;
&lt;h4&gt;分析&lt;/h4&gt;
&lt;p&gt;可以看到旺铺首页后端 RT 在 **0ms ，而整体 RT 在 **0ms 左右，所以减少首页的 RT 是一个入手点。为什么首页的 RT 远大于平均值，是因为首页支持用户自定义装修，模块数量不固定，而每个模块的渲染取树都需要花费时间。&lt;/p&gt;
&lt;p&gt;之前跟后端同学一起追踪过整个渲染链路，发现推品相关模块后端取数时间较长，平均一个品类模块需要 **0ms 左右。那么如果一个用户首页装修了 10 个推品模块，那么单后端 RT 就需要至少 **0ms * 10 （是的，我们接入的中台渲染逻辑是所有模块串行渲染），加上 **0ms 的网络传输时间，那么 ttfb 就需要 **0ms ，所以我们采取的优化策略是对品类模块进行异步化。&lt;/p&gt;
&lt;h4&gt;结果&lt;/h4&gt;
&lt;p&gt;我们先对用户使用频率较高的模块进行了异步化改造，上线后有了不错的数据：首屏提升了 100ms 左右。&lt;/p&gt;
&lt;p&gt;整体 rt 时间缩短，ttfb 降低了 &lt;strong&gt;12%&lt;/strong&gt; 左右。&lt;/p&gt;
&lt;h3&gt;无线旺铺 - 从 weex 迁移到 react 同构&lt;/h3&gt;
&lt;h4&gt;背景&lt;/h4&gt;
&lt;p&gt;老的无线旺铺是基于 weex 的异步渲染的页面，首屏 **s+，一顿复杂的优化操作下勉强优化到了 **s 左右，而且开发上存在很严重的效率问题。当时在做了深入的分析后，绝对定无线旺铺进行前端架构升级，从 weex 迁移到和 PC 店铺一样的 react 同构的 h5 店铺。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/shop-performance-improve/2021-04-16-10-43-48-per.png&quot; alt=&quot;weextoreactper&quot; /&gt;&lt;/p&gt;
&lt;p&gt;详细背景在公司内网写了一篇 《ICBU 旺铺前端架构现状以及演化方向》，里面涉及到合规问题就不放出来了。&lt;/p&gt;
&lt;h4&gt;方案&lt;/h4&gt;
&lt;p&gt;由于无线店铺的情况很复杂，有官方模块还有三方模块，官方模块有 50+ 个，三方模块更是数不胜数。所以无法一次性重构完毕再上线，需要一个能边跑边换轮子的方案。考虑到店铺的模块大多以纯展示为主，重交互的模块为辅，在这个背景下我提出了两个方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rax 转换代码转换方案
&lt;ul&gt;
&lt;li&gt;顾名思义，就是把基于 rax 的 weex 代码，通过 babel 转码工具转换为 react， 加速模块迁移过程&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/imgs/shop-performance-improve/2021-04-16-10-23-29-raxtoreact.png&quot; alt=&quot;raxtoreact&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;rax react 混合渲染方案
&lt;ul&gt;
&lt;li&gt;多余三方模块以及一些其他优先级不高的模块，通过混合渲染的方案直接在新架构下运行新老两种模块，解决模块太多无法迁移的问题&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/imgs/shop-performance-improve/2021-04-16-10-23-56-hybrid.png&quot; alt=&quot;hybrid&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;结果&lt;/h4&gt;
&lt;p&gt;整个项目分了两期来做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一期项目对单个 List 页面进行了人工迁移，主要是为了验证 react 同构的 h5 页面是否能够满足业务需求&lt;/li&gt;
&lt;li&gt;二期开始通过前面提到的两个方案，直接整个店铺进行迁移&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成果上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一期
&lt;ul&gt;
&lt;li&gt;性能：List 首屏性能从 ***s(优化后的 weex) 提升到了 ***s ，降低 &lt;strong&gt;39.7%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;业务上：List 的滚动深度提升了 &lt;strong&gt;3.82%&lt;/strong&gt;，List-&amp;gt;Detial 提升了 &lt;strong&gt;4.3%&lt;/strong&gt;，广义 ab 提升了 &lt;strong&gt;3.2%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二期
&lt;ul&gt;
&lt;li&gt;性能：整体首屏从 ***s 提升到了 ***s ，降低了 &lt;strong&gt;37.5%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;业务：SEO 流量环比日均增加 &lt;strong&gt;29.075%&lt;/strong&gt;，停留时长上升了 &lt;strong&gt;7.681%&lt;/strong&gt;，广义 ab 上升了 &lt;strong&gt;18.673%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;流式渲染&lt;/h3&gt;
&lt;p&gt;在做了一些常规优化方案后，店铺的 PC 和无线端的性能都进入了 ***s 以内，此时店铺的 ttfb 为 ***ms 左右，还有优化的空间。于是我们做了[流式渲染改造]:&lt;/p&gt;
&lt;p&gt;流式渲染是利用 http1.1 的分块传输编码的特性, 让服务端分块返回 html , 浏览器可以在接收时逐步渲染页面, 这样有助于页面的首屏展现, 提升用户体验.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/shop-performance-improve/2021-04-16-11-36-20-stream-render.png&quot; alt=&quot;stream-render&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;成果&lt;/h4&gt;
&lt;p&gt;PC 店铺和无线店铺先后都上了流式渲染， 由于后端 rt 的大幅下降，ttfb 的降低都在 ***ms 左右，具体成果如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PC：
&lt;ul&gt;
&lt;li&gt;性能上：首屏降低了 &lt;strong&gt;8%&lt;/strong&gt; 左右&lt;/li&gt;
&lt;li&gt;业务上：由于当时上线没有做 abtest ，所以没有观察具体的业务数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;M 站：
&lt;ul&gt;
&lt;li&gt;在性能上首屏减少了 &lt;strong&gt;8.69%&lt;/strong&gt; 的时间&lt;/li&gt;
&lt;li&gt;业务上：
&lt;ul&gt;
&lt;li&gt;平均访问深度相对提升了 &lt;strong&gt;1.024%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;平均访问时长相对降低了 &lt;strong&gt;0.020%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;一步 ab 转化相对提升 &lt;strong&gt;0.318%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;App：
&lt;ul&gt;
&lt;li&gt;首屏在 50% 灰度的情况下，首屏减少了 &lt;strong&gt;7.2%&lt;/strong&gt; 左右&lt;/li&gt;
&lt;li&gt;业务上：
&lt;ul&gt;
&lt;li&gt;访问深度相对降低 &lt;strong&gt;2.090%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;访问时长降低 &lt;strong&gt;2.093%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;一步 ab 转化相对提升 &lt;strong&gt;0.547%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时产出了工程化接入流式渲染的方案 [[流式渲染工程化]]&lt;/p&gt;
&lt;h2&gt;交互体验优化&lt;/h2&gt;
&lt;h3&gt;PC 旺铺 SPA 改造效果&lt;/h3&gt;
&lt;h4&gt;背景&lt;/h4&gt;
&lt;p&gt;PC 店铺原本是一个传统的后端渲染的多页面体系架构, 点击每个 Tab 的时候需要做一次普通的页面渲染来完成页面的刷新.
虽然我们已经做了很多诸如流式渲染, 耗时模块异步等优化方案, 但每次页面切换都需要浏览器进行整个页面的刷新,这样的体验依然是不够好.
所以我们对 PC 店铺进行了又一次升级, 通过前端手段将其升级成为了一个伪单页应用.&lt;/p&gt;
&lt;h4&gt;方案&lt;/h4&gt;
&lt;p&gt;现有的渲染方案如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;浏览器发起请求
-&amp;gt; 后端取数
-&amp;gt; 调用 React 同构服务进行模块渲染
-&amp;gt; 拼接同构结果为一个完整的字符串返回给浏览器
-&amp;gt; 浏览器解析加载静态资源并渲染页面
-&amp;gt; js 加载完成后用 React 重新刷新整个页面进行事件绑定等二次渲染
-&amp;gt; 渲染完成.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改造的方案如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/shop-performance-improve/2021-04-16-12-06-59-spa.png&quot; alt=&quot;spa&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;成果&lt;/h4&gt;
&lt;p&gt;店铺的单页改造上线后 AB Test 对比，其中实验桶相对对照桶首屏降低了 &lt;strong&gt;5%&lt;/strong&gt; 左右，ab_rate 相对提升 &lt;strong&gt;1.5%&lt;/strong&gt;，蹦失率相对降低了 &lt;strong&gt;1.7%&lt;/strong&gt; 。
后续预载功能上线后（没有做 AB Test），更是进一步提升了性能，首屏降低了 &lt;strong&gt;16%&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;无线旺铺 - SPA 改造 - Tab 数据预载&lt;/h3&gt;
&lt;h4&gt;策略&lt;/h4&gt;
&lt;p&gt;无线旺铺 Tab 预载： 在首页停留 1.5s 后开始预载其他后续 tab 数据加快用户 tab 切换时的体验。&lt;/p&gt;
&lt;h4&gt;成果&lt;/h4&gt;
&lt;p&gt;发布后，无线 M 站首屏 &lt;strong&gt;20%&lt;/strong&gt; 左右。&lt;/p&gt;
&lt;h3&gt;无线旺铺 - ProductTab 无限加载改造&lt;/h3&gt;
&lt;h4&gt;背景&lt;/h4&gt;
&lt;p&gt;无线旺铺的 ProductTab 页面原本只展示部分产品的概览，展示更多需要跳转到新的页面，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/shop-performance-improve/2021-04-16-12-11-58-view-all.png&quot; alt=&quot;view-all&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跳转新页面会有用户流失，如果能在当前页面直接无限加载所有品的话理论上会有更好的用户体验。很好奇为什么以前没有做，咨询了之前无线旺铺的负责人，得到的原因是原本 weex 旺铺的首页因为是一个单页应用，本身内存占用量就很大了，如果 Prouduct 页面再去做无限加载可能会对端上内存造成更大的压力，有可能导致奔溃或者闪退，所以当时采用的是新开页面去承载。&lt;/p&gt;
&lt;p&gt;后来旺铺做了 h5 改造，不再采用 weex 的技术体系，同时业务上也有在当前页面无线加载产品的诉求。所以做了 ProductTab 的无线加载改造。&lt;/p&gt;
&lt;h4&gt;方案&lt;/h4&gt;
&lt;p&gt;由于依然担心无限加载带来的内存压力问题，所以在项目上线过程中做了跨度为一个月的实验，同时技术上做了一些相关保障。&lt;/p&gt;
&lt;h4&gt;成果&lt;/h4&gt;
&lt;p&gt;最终 abtest 对比 ，ab_rate 相对提升了 1.288%。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;店铺从开始做体验优化时的 PC 和无线相对来说都不够极致的情况优化到现在,PC 首屏是优化前的 60%，无线是优化前的 37.5%， 提升巨大。&lt;/p&gt;
&lt;p&gt;过程中也产出了快速接入流式渲染的方案。而且在业务数据上包括访问深度、停留时长以及转化也取得了不错的成果。&lt;/p&gt;
&lt;p&gt;感谢过程中一起合作的同事以及老板的支持。&lt;/p&gt;
</content:encoded></item><item><title>简易计算器</title><link>https://ab.kisnows.com/zh-CN/2015/02/04/simple-calculate/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/02/04/simple-calculate/</guid><pubDate>Wed, 04 Feb 2015 18:24:33 GMT</pubDate><content:encoded>&lt;p&gt;做简单的计算器，只提供最基本的运算，不过满足日常生活需求是够了。
&lt;img src=&quot;/imgs/2015-02-04_103255.png&quot; alt=&quot;calculate&quot; /&gt;
&lt;a href=&quot;http://kisnows.com/F2E-practice/calculate/popup.html&quot;&gt;运行效果点这里&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Postman 的简单使用</title><link>https://ab.kisnows.com/zh-CN/2016/02/27/simple-use-of-postman/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/02/27/simple-use-of-postman/</guid><pubDate>Sat, 27 Feb 2016 05:51:47 GMT</pubDate><content:encoded>&lt;p&gt;Postman 是一款用来测试 WEB 接口的工具，可以简单的发送 GET 、POST、PUT、DELETE 等请求，可以在 chrome 商店里面搜索安装。
为了介绍它的功能，首先本地起一个服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var http = require(&quot;http&quot;);
var url = require(&quot;url&quot;);
var querystring = require(&quot;querystring&quot;);

http
  .createServer(function (req, res) {
    res.writeHead(200, { &quot;content-type&quot;: &quot;text/json&quot; });
    if (req.method === &quot;GET&quot;) {
      var params = url.parse(req.url, true).query;
      res.write(params.name + &quot; is &quot; + params.age + &quot; years old&quot;);
      res.on(&quot;error&quot;, function (err) {
        throw new Error(err);
      });
      res.end();
    } else {
      var resDate = [];
      req
        .on(&quot;data&quot;, function (chunk) {
          resDate.push(chunk);
        })
        .on(&quot;end&quot;, function () {
          var params = querystring.parse(resDate.join(&quot;&quot;));
          res.write(JSON.stringify(params));
          res.end();
        })
        .on(&quot;error&quot;, function (e) {
          throw new Error(e);
        });
    }
  })
  .listen(8088);
console.log(&quot;Server listen at localhost:8088&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用来监听 POST 和 GET 请求，并返回相应的数据。
接下来打开 Postman ，界面很简洁，所有的功能都在图上标注了出来
&lt;img src=&quot;/imgs/simple-use-of-postman.png&quot; alt=&quot;postman&quot; /&gt;
上图是模拟 post 请求，填好要发送的参数，点击 send ，就可以看到返回了正确的结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;moqiao&quot;,
  &quot;age&quot;: &quot;23&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GET 请求同理。&lt;/p&gt;
</content:encoded></item><item><title>indexedDB 不完全指南</title><link>https://ab.kisnows.com/zh-CN/2017/12/06/step-into-indexdb/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/12/06/step-into-indexdb/</guid><pubDate>Wed, 06 Dec 2017 22:49:52 GMT</pubDate><content:encoded>&lt;h1&gt;介绍&lt;/h1&gt;
&lt;p&gt;引用 MDN 的介绍：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;IndexedDB 是一个事务型数据库系统，类似于基于 SQL 的 RDBMS。 然而不同的是它使用固定列表，IndexedDB 是一个基于 JavaScript 的面向对象的数据库。 IndexedDB 允许您存储和检索用键索引的对象; 可以存储 structured clone algorithm 支持的任何对象。 您只需要指定数据库模式，打开与数据库的连接，然后检索和更新一系列事务中的数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;IndexedDB  是一种低级 API，用于客户端存储大量结构化数据(包括, 文件/ blobs)。该 API 使用索引来实现对该数据的高性能搜索。虽然  Web Storage  对于存储较少量的数据很有用，但对于存储更大量的结构化数据来说，这种方法不太有用。IndexedDB 提供了一个解决方案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;区别&lt;/h1&gt;
&lt;p&gt;这是我整理的 WebStorage 和 indexedDB 的之间区别，有问题的地方还请指出。
&lt;img src=&quot;/imgs/step-into-indexdb/diff.png&quot; alt=&quot;indexedDB和WebStorage的区别&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;用法&lt;/h1&gt;
&lt;h2&gt;1. 打开数据库&lt;/h2&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const DB_NAME = &quot;Netease&quot;;
const DB_VERSION = 1;
const OB_NAMES = {
  UseKeyPath: &quot;UseKeyPath&quot;,
  UseKeyGenerator: &quot;UseKeyGenerator&quot;,
};
/**
 * NOTE:
 * 1. 第一次打开可能会提示用户获取 indexedDB 的权限
 * 2. 浏览器隐身模式不会存在本地，只会存储在内存中
 */
const request = window.indexedDB.open(DB_NAME, DB_VERSION);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;indexedDB.open&lt;/code&gt; 接收两个参数，分别为数据库名称和版本，返回的是一个 &lt;code&gt;IDBOpenDBRequest&lt;/code&gt; 对象。可以以 DOM 事件的方式监听它的 success 和 error 来获取到它的结果。几乎所有对 indexedDB 的异步操作都是这种以事件的方式进行，返回一个拥有结果或错误的 &lt;code&gt;IDBRequest&lt;/code&gt; 对象。在这里，&lt;code&gt;open&lt;/code&gt; 方法得到的结果是一个 &lt;code&gt;IDBDatabase&lt;/code&gt; 的实例。&lt;/p&gt;
&lt;p&gt;第二个参数是数据库的版本。版本决定了数据库的模式：存储在里面的 object store 和它们的结构。当第一次通过 &lt;code&gt;open&lt;/code&gt; 方法打开数据库时，会触发一个 &lt;code&gt;onupgradeneeded&lt;/code&gt; 事件，我们可以也只能在这里设置数据库模式。当数据库已经存在，而我们打开一个更高版本时，同样会触发 &lt;code&gt;onupgradeneeded&lt;/code&gt; 事件，用来更新数据库模式。&lt;/p&gt;
&lt;h3&gt;添加处理方法&lt;/h3&gt;
&lt;p&gt;我们可以通过监听它的 &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt; 以及 &lt;code&gt;upgradeneeded&lt;/code&gt; 事件来做相应的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;request.onerror = function (event) {
  // Do something with request.errorCode!
  console.error(&quot;open request failed&quot;, event.target.error);
};
request.onsuccess = function (event) {
  // Do something with request.result!
  // console.log(&apos;open request success&apos;, event)
  var db = event.target.result;
  db.onerror = function (e) {
    console.error(&quot;Database error: &quot;, e.target.error);
  };
  db.onclose = (e) =&amp;gt; {
    console.error(&quot;Database close:&quot;, e.target.error);
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以在 &lt;code&gt;success&lt;/code&gt; 事件里面拿到 db 对象，这个是后续操作的主体。&lt;/p&gt;
&lt;h3&gt;错误处理&lt;/h3&gt;
&lt;p&gt;由于是基于 DOM 事件模式的，所以所有的错误是会冒泡的。也就是说在一个特定 request 的上引发的错误会依次冒泡到事务，然后到 db 对象。
如果为了简化错误处理，可以直接在 db 对象上添加错误处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;db.onerror = function (e) {
  // 可以处理这个数据库上所有的错误
  console.error(&quot;Database error: &quot;, e.target.error);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 创建或更新数据库版本&lt;/h2&gt;
&lt;p&gt;前面已经说过，当创建或者增大数据库版本的时候，会触发 &lt;code&gt;onupgradeneeded&lt;/code&gt; 事件。在事件内部，可以拿到 db 对象来创建或更新 object store , 具体如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;request.onupgradeneeded = function (event) {
  /**
   * NOTE:
   * 1. 创建新的 objectStore
   * 2. 删除旧的不需要的 objectStore
   * 3. 如果需要更新已有 objectStore 的结构，需要先删除原有的 objectStore ，然后重新创建
   */
  // The IDBDatabase interface
  console.log(&quot;onupgradeneeded&quot;, event);
  var db = event.target.result; // Create an objectStore for this database
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
    keyPath: &quot;time&quot;,
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 构建数据库&lt;/h2&gt;
&lt;p&gt;indexedDB 是以对象存储（object store）而不是以表结构存储的，一个数据库可以存储任意多个存储对象。每当有一个值存储在 object store 里面，就必须和一个 key 关联起来。有几种提供 key 的方法，取决于 object store 使用 &lt;strong&gt;key path&lt;/strong&gt; 还是 &lt;strong&gt;key generator&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;它们之间区别，借用 MDN 的一个表格来看一下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key Path (keyPath)&lt;/th&gt;
&lt;th&gt;Key Generator (autoIncrement)&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;This object store can hold any kind of value, even primitive values like numbers and strings. You must supply a separate key argument whenever you want to add a new value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;This object store can only hold JavaScript objects. The objects must have a property with the same name as the key path.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;This object store can hold any kind of value. The key is generated for you automatically, or you can supply a separate key argument if you want to use a specific key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;This object store can only hold JavaScript objects. Usually a key is generated and the value of the generated key is stored in the object in a property with the same name as the key path. However, if such a property already exists, the value of that property is used as key rather than generating a new key.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;来自 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;当存储的是不是基础类型而是 js 对象的时候，我们还可以给 object store 创建索引，这样就可以通过索引属性来查找某一个具体的对象。而且， 索引还能再一定程度上约束要存储的对象。当创建索引的时候通过设置唯一标识，可以确保不会存储拥有两个相同索引值的对象。
看一个例子，假设我们有如下日志数据需要存储：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const TestData = [
  {
    event: &quot;NE-TEST1&quot;,
    level: &quot;warning&quot;,
    errorCode: 200,
    url: &quot;http://www.example.com&quot;,
    time: &quot;2017/11/8 下午4:53:039&quot;,
    isUploaded: false,
  },
  {
    event: &quot;NE-TEST2&quot;,
    msg: &quot;测试2&quot;,
    level: &quot;error&quot;,
    errorCode: 1000,
    url: &quot;http://www.example.com&quot;,
    time: &quot;2017/11/8 下午4:53:042&quot;,
    isUploaded: false,
  },
  {
    event: &quot;NE-TEST3&quot;,
    msg: &quot;测试3&quot;,
    level: &quot;info&quot;,
    errorCode: 3000,
    url: &quot;http://www.example.com&quot;,
    time: &quot;2017/11/8 下午4:53:043&quot;,
    isUploaded: false,
  },
  {
    event: &quot;NE-TEST4&quot;,
    mgs: &quot;测试4&quot;,
    level: &quot;info&quot;,
    url: &quot;http://www.example.com&quot;,
    time: &quot;2017/11/8 下午4:53:0423&quot;,
    isUploaded: false,
  },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两种存储方式，分别是通过 key path 和 key generator.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function obUseKeypath(db) {
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
    keyPath: &quot;time&quot;,
  });
  objectStore.createIndex(&quot;errorCode&quot;, &quot;errorCode&quot;, { unique: false });
  objectStore.createIndex(&quot;level&quot;, &quot;level&quot;, { unique: false });
}

function obUseKeyGenerator(db) {
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyGenerator, {
    autoIncrement: true,
  });
  objectStore.createIndex(&quot;errorCode&quot;, &quot;errorCode&quot;, { unique: false });
  objectStore.createIndex(&quot;time&quot;, &quot;time&quot;, { unique: true });
  objectStore.createIndex(&quot;level&quot;, &quot;level&quot;, { unique: false });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 增删改查&lt;/h2&gt;
&lt;p&gt;为了方便介绍这部分的内容，我们先把上一节的代码包装一下，为了方便后续例子的讲解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function openindexedDB() {
  // The call to the open() function returns an IDBOpenDBRequest object with a result (success) or error value that you handle as an event.
  return new Promise((resolve, reject) =&amp;gt; {
    /**
     * NOTE:
     * 1. 第一次打开可能会提示用户获取 indexedDB 的权限
     * 2. 浏览器隐身模式不会存在本地，只会存储在内存中
     */
    const request = window.indexedDB.open(DB_NAME, DB_VERSION);
    request.onerror = function (event) {
      // Do something with request.errorCode!
      console.log(&quot;open request failed&quot;, event);
      console.error(event.target.error);
    };
    request.onsuccess = function (event) {
      // Do something with request.result!
      // console.log(&apos;open request success&apos;, event)
      var db = event.target.result;
      db.onerror = function (e) {
        console.error(&quot;Database error: &quot;, e.target.error);
        reject(e.target.error);
      };
      db.onclose = (e) =&amp;gt; {
        console.error(&quot;Database close:&quot;, e.target.error);
        reject(e.target.error);
      };
      resolve(db);
    };
    request.onupgradeneeded = function (event) {
      /**
       * NOTE:
       * 1. 创建新的 objectStore
       * 2. 删除旧的不需要的 objectStore
       * 3. 如果需要更新已有 objectStore 的结构，需要先删除原有的 objectStore ，然后重新创建
       */
      // The IDBDatabase interface
      console.log(&quot;onupgradeneeded&quot;, event);
      var db = event.target.result; // Create an objectStore for this database
      obUseKeypath(db);
      obUseKeyGenerator(db);
      /**
       * NOTE:
       * transaction
       * 三个事件：
       * 1. error
       * 2. abort
       * 3. complete
       * 两个方法：
       * 1. abort
       * Rolls back all the changes to objects in the database associated with this transaction. If this transaction has been aborted or completed, then this method throws an error event.
       * 2. objectStore
       * Returns an IDBObjectStore object representing an object store that is part of the scope of this transaction.
       */
      db.transaction.oncomplete = function (e) {
        console.log(&quot;obj create success&quot;, e);
      };
    };
  });
}
function obUseKeypath(db) {
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
    keyPath: &quot;time&quot;,
  });
  objectStore.createIndex(&quot;errorCode&quot;, &quot;errorCode&quot;, { unique: false });
  objectStore.createIndex(&quot;level&quot;, &quot;level&quot;, { unique: false });
}
function obUseKeyGenerator(db) {
  const objectStore = db.createObjectStore(OB_NAMES.UseKeyGenerator, {
    autoIncrement: true,
  });
  objectStore.createIndex(&quot;errorCode&quot;, &quot;errorCode&quot;, { unique: false });
  objectStore.createIndex(&quot;time&quot;, &quot;time&quot;, { unique: true });
  objectStore.createIndex(&quot;level&quot;, &quot;level&quot;, { unique: false });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样每次我们需要对数据库做操作的话只需要调用 &lt;code&gt;openindexedDB&lt;/code&gt; 方法就可以了。&lt;/p&gt;
&lt;h3&gt;事务&lt;/h3&gt;
&lt;p&gt;所有对数据库的操作都是建立在事务(transaction)上的，有三种模式(mode)：&lt;code&gt;readonly&lt;/code&gt;, &lt;code&gt;readewrite&lt;/code&gt;, &lt;code&gt;versionchange&lt;/code&gt;.
要修改数据库结构的 &lt;strong&gt;schema&lt;/strong&gt; ，必须在 &lt;code&gt;versionchange&lt;/code&gt; 模式下。读取和修改对应另外两种模式。
通过 &lt;code&gt;IDBDatabase.transaction&lt;/code&gt; 打开一个 &lt;code&gt;transaction&lt;/code&gt;, 接收两个参数：&lt;code&gt;storeNames&lt;/code&gt;, &lt;code&gt;mode&lt;/code&gt;.
所有对数据库的操作都遵循以下流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get database object&lt;/li&gt;
&lt;li&gt;Open transaction on database&lt;/li&gt;
&lt;li&gt;Open object store on transaction&lt;/li&gt;
&lt;li&gt;Perform operation on object store&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;加速事务操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当定义作用域时（scope）， 只定义需要的 object stores. 这样，就可以在不重叠的作用域上并行的执行多个事务。&lt;/li&gt;
&lt;li&gt;只有在需要的时候才开启一个 readwrite 事务。因为在重叠的作用域上可以并发执行多个 readonly 事务，但只能有一个 readwrite 事务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;增&lt;/h3&gt;
&lt;p&gt;方法如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先拿到 db 对象&lt;/li&gt;
&lt;li&gt;然后打开一个 &lt;code&gt;readwrite&lt;/code&gt; 事务&lt;/li&gt;
&lt;li&gt;通过事务拿到 object store 对象&lt;/li&gt;
&lt;li&gt;执行添加数据操作&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 添加数据
 * @param {array} docs 要添加数据
 * @param {string} objName 仓库名称
 */
function addData(docs, objName) {
  if (!(docs &amp;amp;&amp;amp; docs.length)) {
    throw new Error(&quot;docs must be a array!&quot;);
  }
  return openindexedDB().then((db) =&amp;gt; {
    const tx = db.transaction([objName], &quot;readwrite&quot;);
    tx.oncomplete = (e) =&amp;gt; {
      console.log(&quot;tx:addData onsuccess&quot;, e);
      return Promise.resolve(docs);
    };
    tx.onerror = (e) =&amp;gt; {
      e.stopPropagation();
      console.error(&quot;tx:addData onerror&quot;, e.target.error);
      return Promise.reject(e.target.error);
    };
    tx.onabort = (e) =&amp;gt; {
      console.warn(&quot;tx:addData abort&quot;, e.target);
      return Promise.reject(e.target.error);
    };
    const obj = tx.objectStore(objName);
    docs.forEach((doc) =&amp;gt; {
      const req = obj.add(doc);
      /**
       * NOTE:
       * request
       * 两个事件：
       * 1. success
       * 2. error
       */ // req.onsuccess = e =&amp;gt; console.log(&apos;obj:addData onsuccess&apos;, e.target)
      req.onerror = (e) =&amp;gt; {
        console.error(&quot;obj:addData onerror&quot;, e.target.error);
      };
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要把上面的 TestData 同时使用 key generator 和 key path 方式保存到数据库中，那么方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addData(TestData, OB_NAMES.UseKeyGenerator).then(() =&amp;gt;
  addData(TestData, OB_NAMES.UseKeyPath),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;删&lt;/h3&gt;
&lt;p&gt;流程和添加一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 删除指定 key 的数据
 * @param {string} objName 仓库名称
 * @param {*} key 要删除数据的 primary key 值
 */
function deleteData(objName, key) {
  return openindexedDB().then((db) =&amp;gt; {
    const tx = db.transaction([objName], &quot;readwrite&quot;);
    const obj = tx.objectStore(objName);
    const req = obj.delete(key);
    req.onsuccess = (e) =&amp;gt; {
      console.log(`readData success. key:${key},result:`, e.target.result);
      return Promise.resolve(e.target.result);
    };
    req.onerror = (e) =&amp;gt; {
      console.error(`readData error. key:${key},error: ${e.target.errorCode}`);
      return Promise.reject(e.target.error);
    };
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如要删除 &lt;code&gt;UserKeyGenerator&lt;/code&gt; 里 key 为 1 值，那么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deleteData(OB_NAMES.UseKeyGenerator, 1).then((doc) =&amp;gt; console.log(doc));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查&lt;/h3&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 读取给定 key 的数据
 * @param {string} objName 仓库名称
 * @param {*} key 要读取数据的 primary key 值
 */
function readData(objName, key) {
  return openindexedDB().then((db) =&amp;gt; {
    const tx = db.transaction([objName]);
    const obj = tx.objectStore(objName);
    const req = obj.get(key);
    req.onsuccess = (e) =&amp;gt; {
      console.log(`readData success. key:${key},result:`, e.target.result);
      return Promise.resolve(e.target.result);
    };
    req.onerror = (e) =&amp;gt; {
      console.error(`readData error. key:${key},error: ${e.target.errorCode}`);
      return Promise.reject(e.target.error);
    };
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;readData(OB_NAMES.UseKeyGenerator, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;改&lt;/h3&gt;
&lt;p&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 更新指定 key 的数据
 * @param {string} objName 仓库名称
 * @param {*} key 指定的 key
 * @param {object} changes 要修改的属性值
 */
function updateData(objName, key, changes) {
  return openindexedDB().then((db) =&amp;gt; {
    return new Promise((resolve, reject) =&amp;gt; {
      const tx = db.transaction([objName], &quot;readwrite&quot;);
      const obj = tx.objectStore(objName);
      const req = obj.get(key);
      req.onsuccess = (e) =&amp;gt; {
        let doc = e.target.result;
        let newDoc = Object.assign(doc, changes);
        const req = obj.put(newDoc);
        req.onsuccess = (e) =&amp;gt; {
          console.log(`updateData success, newDoc:`, newDoc, e);
          resolve(e.target.result);
        };
        req.onerror = (e) =&amp;gt; {
          resolve(e.target.result);
        };
      };
      req.onerror = (e) =&amp;gt; {
        reject(e.target.error);
      };
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;updateData(OB_NAMES.UseKeyGenerator, 1, { time: &quot;123&quot; })
  .then((doc) =&amp;gt; console.log(doc))
  .catch(console.error);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用游标 (cursor)&lt;/h3&gt;
&lt;p&gt;使用 get 方法需要预先知道一个 key. 如果需要步进整个 object store 的值，那么可以使用 cursor.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 通过 cursor 获取制定仓库下的所有数据
 * @param {string} objName 仓库名称
 * @param {function} [cb] 回调函数，对每次得到的 cursor 进行操作
 * @returns {Promise.&amp;lt;array.&amp;lt;object&amp;gt;} 包含所有数据的数组
 */
function getAllByCursor(objName, cb) {
  return openindexedDB().then((db) =&amp;gt; {
    const arr = [];
    const tx = db.transaction([objName]);
    const obj = tx.objectStore(objName);
    return new Promise((resolve) =&amp;gt; {
      obj.openCursor().onsuccess = (e) =&amp;gt; {
        const cursor = e.target.result;
        if (cursor) {
          arr.push(cursor.value);
          cb &amp;amp;&amp;amp; cb(cursor);
          cursor.continue();
        } else {
          return resolve(arr);
        }
      };
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;openCursor()&lt;/code&gt; 方法接收多个参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个参数，可以传入一个 key range 对象来限制需要获取值的范围。&lt;/li&gt;
&lt;li&gt;第二个参数，可以设置迭代的方向。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;成功回调函数的 &lt;code&gt;result&lt;/code&gt; 值就是 cursor 本身，当前遍历值可以通过 cursor 对象的 &lt;code&gt;key&lt;/code&gt; 和 &lt;code&gt;value&lt;/code&gt; 值获取。如果要继续往下遍历，那么调用 cursor 的 &lt;code&gt;continue()&lt;/code&gt; 方法，当遍历结束时， cursor 也就是 &lt;code&gt;event.target.result&lt;/code&gt; 的值为 &lt;code&gt;undefined&lt;/code&gt;.
如果要获取 UseKeyGenerator 仓库下所有的值，那么可以这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getAllByCursor(OB_NAMES.UseKeyGenerator).then(console.log);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了 &lt;code&gt;openCursor()&lt;/code&gt; 外，还可以使用 &lt;code&gt;openKeyCursor()&lt;/code&gt; 来获取所有存储对象的主键值，使用方法和 openCursor 一样，&lt;/p&gt;
&lt;h3&gt;使用索引 (index)&lt;/h3&gt;
&lt;p&gt;在建立 object store 时，如果我们给它创建了索引。这时，就可以使用索引来查找某个特定属性的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getByIndex(objName, indexName, key) {
  return openindexedDB().then((db) =&amp;gt; {
    const index = db.transaction(objName).objectStore(objName).index(indexName);
    return new Promise((resolve) =&amp;gt; {
      index.get(key).onsuccess = (e) =&amp;gt; {
        console.log(&quot;getByIndex&quot;, e.target.result);
        return resolve(e.target.result);
      };
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如，我们要查找 &lt;code&gt;level&lt;/code&gt; 为 &lt;code&gt;info&lt;/code&gt; 的日志，那么可以这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getByIndex(OB_NAMES.UseKeyGenerator, &quot;level&quot;, &quot;info&quot;).then((doc) =&amp;gt;
  console.log(doc),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;设置范围 (range) 和游标 (cursors) 的方向&lt;/h3&gt;
&lt;p&gt;如果需要设置 cursor 遍历的范围，可以使用 &lt;code&gt;IDBKeyRange&lt;/code&gt; 对象并把作为第一个参数给 &lt;code&gt;openCursor()&lt;/code&gt; 或者 &lt;code&gt;openKeyCursor()&lt;/code&gt; .
一个 key range 的值，可以认为是一个区间，区间的类型可以是开区间也可以是闭区间。看一些例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * NOTE:
 * 只包括给定的值
 */
getWithRangeByCursor(
  OB_NAMES.UseKeyGenerator,
  &quot;errorCode&quot;,
  IDBKeyRange.only(1000),
)
  .then(console.log)
  .catch(console.error);
/**
 * NOTE:
 * 默认是闭区间
 */
getWithRangeByCursor(
  OB_NAMES.UseKeyGenerator,
  &quot;errorCode&quot;,
  IDBKeyRange.lowerBound(1000),
)
  .then(console.log)
  .catch(console.error);
/**
 * NOTE:
 * 设置第二个可选参数为 true，则为开区间
 */
getWithRangeByCursor(
  OB_NAMES.UseKeyGenerator,
  &quot;errorCode&quot;,
  IDBKeyRange.lowerBound(1000, true),
)
  .then(console.log)
  .catch(console.error);
/**
 * NOTE:
 * 闭区间，如果索引是数字，那么按照数字大小决定升序
 */
getWithRangeByCursor(
  OB_NAMES.UseKeyGenerator,
  &quot;errorCode&quot;,
  IDBKeyRange.bound(1000, 2000),
)
  .then(console.log)
  .catch(console.error);
/**
 * NOTE:
 * 左闭右开区间：如果索引是字符串，那么安装 array.sort() 的方式决定升序
 */
getWithRangeByCursor(
  OB_NAMES.UseKeyGenerator,
  &quot;time&quot;,
  IDBKeyRange.bound(
    &quot;2017/11/8 下午4:53:042&quot;,
    &quot;2017/11/8 下午4:53:043&quot;,
    false,
    true,
  ),
)
  .then(console.log)
  .catch(console.error);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;降序迭代&lt;/h3&gt;
&lt;p&gt;默认情况下，迭代的方向是升序的。如果需要按照降序规则迭代，那么只要把 &lt;code&gt;prev&lt;/code&gt; 作为 &lt;code&gt;openCursor()&lt;/code&gt; 的第二个参数就可以了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;getWithDescendingByCursor(OB_NAMES.UseKeyGenerator, &quot;time&quot;)
  .then(console.log)
  .catch(console.error);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Flex 布局</title><link>https://ab.kisnows.com/zh-CN/2016/05/05/step-to-flexbox/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/05/05/step-to-flexbox/</guid><pubDate>Thu, 05 May 2016 00:10:43 GMT</pubDate><content:encoded>&lt;p&gt;第一次接触 flex 是在去年，当时是我第一次做手机项目，结果却因为兼容性问题给掉坑了。查了 can I use 发现它只支持到安卓 4.1, 然而我们的要求是要兼容到 4.0 才行的。
所以之后一直都没敢用，昨天听到有人说 flex 是可以支持到安卓 2.1 以上的。我不信，难道是我当时看错了？！
今天又去查了一下，发现确实是 4.1 啊。盯着屏幕想了半天，突然发现 Can I Use 上一个浏览器浏览器使用率，想着该不会直接把使用率低的浏览器给忽略掉了吧。打开设置一看，果真如此，于是把数据源改成中国，浏览器最小使用率 0.03% 果真 2.3 出来了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/step-to-flexbox/can-i-use.png&quot; alt=&quot;Can I USE&quot; /&gt;&lt;/p&gt;
&lt;p&gt;既然如此，说明 flex 还是可以在生产环境中使用了。那么就好好再过一下它的用法吧。
Flex 就是弹性布局，任何一个元素都可以设置为 flex 布局，当一个元素被设置为 flex 布局后，它的子元素上 float, vertical-align 都会失效。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
  display: flex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;基本概念&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;http://www.ruanyifeng.com/blogimg/asset/2015/bg2015071004.png&quot; alt=&quot;base&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html&quot;&gt;图片出自&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;就是说一个 flex 容器有两个轴，一个主轴一个交叉轴。每个轴都有自己对应的起始位置，主轴的起始分别为 main start 和 main end, 交叉轴的起始分别为 cross start 和 cross end.
容器内的项目会默认沿着主轴排列，从 main start 到 main end .&lt;/p&gt;
&lt;h1&gt;felx 容器&lt;/h1&gt;
&lt;p&gt;容器上可以 6 个属性，分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;flex-direction&lt;/li&gt;
&lt;li&gt;flex-wrap&lt;/li&gt;
&lt;li&gt;flex-flow&lt;/li&gt;
&lt;li&gt;justify-content&lt;/li&gt;
&lt;li&gt;align-items&lt;/li&gt;
&lt;li&gt;align-content&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;flex-direction&lt;/h2&gt;
&lt;p&gt;决定主轴方向，也就是项目的排列方向。&lt;/p&gt;
&lt;h3&gt;MDN 上的语法解释：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;/* The direction text is laid out in a line */
flex-direction: row;
/* Like &amp;lt;row&amp;gt;, but reversed */
flex-direction: row-reverse;
/* The direction in which lines of text are stacked */
flex-direction: column;
/* Like &amp;lt;column&amp;gt;, but reversed */
flex-direction: column-reverse;
/* Global values */
flex-direction: inherit;
flex-direction: initial;
flex-direction: unset;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;实例&lt;/h3&gt;
&lt;p&gt;下面从左到右分别是 row, row-reverse, colum, colum-reverse. 可以明显的看出他们之间的区别。&lt;/p&gt;
&lt;p&gt;&amp;lt;div&amp;gt;&amp;lt;script async src=&quot;https://jsfiddle.net/kisnows/1ryjp43c/embed/result,html,css/&quot;&amp;gt;&amp;lt;/script&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;flex-wrap&lt;/h2&gt;
&lt;p&gt;flex-wrap 定义内部的 item 是排列在一行还是说当超出时可以换行的属性。如果允许换行，这个属性也能让你控制堆叠的方向。&lt;/p&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;flex-wrap: nowrap;
flex-wrap: wrap;
flex-wrap: wrap-reverse;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;实例：&lt;/h3&gt;
&lt;p&gt;&amp;lt;script async src=&quot;https://jsfiddle.net/kisnows/3f7odnc6/embed/result,html,css/&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;h2&gt;flex-flow&lt;/h2&gt;
&lt;p&gt;flex-flow 是 flex-direction 和 flex-wrap 的简写。
初始值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;flex-directoin: row&lt;/li&gt;
&lt;li&gt;flex-wrap: nowrap&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;语法：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;/* flex-flow: &amp;lt;&apos;flex-direction&apos;&amp;gt; */
flex-flow: row;
flex-flow: row-reverse;
flex-flow: column;
flex-flow: column-reverse;
/* flex-flow: &amp;lt;&apos;flex-wrap&apos;&amp;gt; */
flex-flow: nowrap;
flex-flow: wrap;
flex-flow: wrap-reverse;
/* flex-flow: &amp;lt;&apos;flex-direction&apos;&amp;gt; and &amp;lt;&apos;flex-wrap&apos;&amp;gt; */
flex-flow: row nowrap;
flex-flow: column wrap;
flex-flow: column-reverse wrap-reverse;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;justify-content&lt;/h2&gt;
&lt;p&gt;justify-content 用来定义浏览器如何计算主轴上 (main-axis) flex 内部的 item 之间的空隙。
对齐的过程会在长度和自动外编剧计算后进行，也就是说只要有一个设置了 flex-grow 不为 0 的弹性 (flexible) 元素，那么 justify-content 就不会起作用了。初始值为 flex-start.&lt;/p&gt;
&lt;h3&gt;语法：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;/* Pack flex items from the start */
justify-content: flex-start;
/* Pack items from the end */
justify-content: flex-end;
/* Pack items around the center */
justify-content: center;
/* Distribute items evenly
The first item at the start, the last at the end */
justify-content: space-between;
/* Distribute items evenly
Items have equal space around them */
justify-content: space-around;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;实例：&lt;/h3&gt;
&lt;p&gt;&amp;lt;script async src=&quot;https://jsfiddle.net/kisnows/2etbfr66/embed/result,html,css/&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;h2&gt;align-item&lt;/h2&gt;
&lt;p&gt;align-item 属性用来对齐当前 flex line 上的 flex 元素，和 justify-content 一样，不过是是在主轴的垂直方向上作用。初始值为 stretch.&lt;/p&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;/* Align to cross-start */
align-items: flex-start;
/* Align to cross-end */
align-items: flex-end;
/* Center items in the cross-axis */
align-items: center;
/* Align the items&apos; baselines */
align-items: baseline;
/* Stretch the items to fit */
align-items: stretch;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;###　实例&lt;/p&gt;
&lt;p&gt;&amp;lt;script async src=&quot;https://jsfiddle.net/kisnows/jxbgepc2/embed/result,html,css/&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;h2&gt;align-content&lt;/h2&gt;
&lt;p&gt;align-content 属性定义当交叉轴上有空间时，flex 元素在交叉轴上的对齐方式。对于单行的 flex 元素，这个属性没有作用。默认值为： stretch.&lt;/p&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;/* Pack lines from the cross-axis start */
align-content: flex-start;
/* Pack lines to the cross-axis end */
align-content: flex-end;
/* Pack lines around the cross-axis center */
align-content: center;
/* Distribute lines along the cross-axis, start to end */
align-content: space-between;
/* Distribute lines along the cross-axis, equally spaced */
align-content: space-around;
/* Stretch lines to occupy the whole cross-axis */
align-content: stretch;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;实例&lt;/h3&gt;
&lt;p&gt;&amp;lt;script async src=&quot;https://jsfiddle.net/kisnows/dhjrbm1o/1/embed/result,html,css/&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;h1&gt;flex 项目（子元素）&lt;/h1&gt;
&lt;p&gt;flex 项目上同样可以设置 6 个属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;order&lt;/li&gt;
&lt;li&gt;flex-grow&lt;/li&gt;
&lt;li&gt;flex-shrink&lt;/li&gt;
&lt;li&gt;flex-basis&lt;/li&gt;
&lt;li&gt;align-self&lt;/li&gt;
&lt;li&gt;flex&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;order&lt;/h2&gt;
&lt;p&gt;定义项目在主轴上的优先级，越小越靠前。默认为 0.&lt;/p&gt;
&lt;h2&gt;flex-grow&lt;/h2&gt;
&lt;p&gt;定义项目的拉伸因子，就是计算这个项目如何来占据剩余的空间。默认为 1.&lt;/p&gt;
&lt;h2&gt;flex-shrink&lt;/h2&gt;
&lt;p&gt;定义项目的因子。默认为 1.&lt;/p&gt;
&lt;h3&gt;order,flex-grow,flex-shrink 实例&lt;/h3&gt;
&lt;p&gt;&amp;lt;script async src=&quot;https://jsfiddle.net/kisnows/Lxz9wj7n/4/embed/result,html,css/&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;h2&gt;flex-basis&lt;/h2&gt;
&lt;p&gt;flex-basis 定义在初试阶段 flex 占据主轴空间的基础。这个属性决定了 content-box 的尺寸，除非你修改了 box-sizing. 初始值为 auto.&lt;/p&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;/* Specify &amp;lt;&apos;width&apos;&amp;gt; */
flex-basis: 10em;
flex-basis: 3px;
flex-basis: auto;
/* Intrinsic sizing keywords */
flex-basis: fill;
flex-basis: max-content;
flex-basis: min-content;
flex-basis: fit-content;
/* Automatically size based on the flex item’s content */
flex-basis: content;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;实例&lt;/h3&gt;
&lt;p&gt;&amp;lt;script async src=&quot;https://jsfiddle.net/kisnows/3zmppe13/embed/result,html,css/&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;h2&gt;flex&lt;/h2&gt;
&lt;p&gt;flex 是一个定义项目改变尺寸能力的一个缩写属性。
默认值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;flex-grow: 0&lt;/li&gt;
&lt;li&gt;flex-shrink: 1&lt;/li&gt;
&lt;li&gt;flex-basis: auto&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;/* 0 0 auto */
flex: none;
/* One value, unitless number: flex-grow */
flex: 2;
/* One value, width/height: flex-basis */
flex: 10em;
flex: 30px;
flex: auto;
flex: content;
/* Two values: flex-grow | flex-basis */
flex: 1 30px;
/* Two values: flex-grow | flex-shrink */
flex: 2 2;
/* Three values: flex-grow | flex-shrink | flex-basis */
flex: 2 2 10%;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;align-self&lt;/h2&gt;
&lt;p&gt;定义项目的对齐方式，会覆盖 align-items 属性。如果只要有一个项目的交叉轴外边距设置为 auto, 那么 align-self 就会被忽略。默认值为 auto.&lt;/p&gt;
&lt;h3&gt;语法&lt;/h3&gt;
&lt;p&gt;和 align-items 相同&lt;/p&gt;
&lt;h3&gt;实例&lt;/h3&gt;
&lt;p&gt;&amp;lt;script async src=&quot;https://jsfiddle.net/kisnows/raagzpbz/embed/result,html,css/&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;style&amp;gt;
iframe {
min-height: 300px;
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>【翻译向】走进 Redux 之基础篇(一)</title><link>https://ab.kisnows.com/zh-CN/2016/04/27/step-to-redux-1/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/04/27/step-to-redux-1/</guid><pubDate>Wed, 27 Apr 2016 06:17:16 GMT</pubDate><content:encoded>&lt;h1&gt;Redux 的三个原则&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;整个应用的状态都以一个对象树的形式保存在一个单一的 store 中&lt;/li&gt;
&lt;li&gt;唯一去改变这个对象树的方法就是通过去触发 action,一个用来描述发生了什么事情的对象&lt;/li&gt;
&lt;li&gt;通过编写纯函数 reducers，去描述这个 action 如何改变了整个对象树&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Action&lt;/h1&gt;
&lt;p&gt;Action 就是承载了要从你的应用传送给 store 的信息。它们只是 store 的信息数据来源。可以通过 &lt;code&gt;store.dispatch()&lt;/code&gt; 来传递 action.
action 可能长这个样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ADD_TODO = &apos;ADD_TODO&apos;
{
    type: ADD_TODO,
    text: &apos;Build my first Redux App&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Actions 就是普通的 JavaScript 对象，但是必须有 &lt;code&gt;type&lt;/code&gt; 属性，用来指定是发生了哪种类型的操作。 Types 应该被定义为字符串常量。当你的项目变的庞大起来时，你可能需要把它们移动到一个单独隔离的模块里面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ADD_TODO, REMOVE_TODO } from &quot;../actionTypes&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与 &lt;code&gt;type&lt;/code&gt; 不同的是，整个 action 的结构完全由你来决定。不过可以参考 &lt;a href=&quot;https://github.com/acdlite/flux-standard-action&quot;&gt;Flux Standard Action&lt;/a&gt; 里面的指南来了解如何更好的组织 action 的结构。&lt;/p&gt;
&lt;h2&gt;Action Creators&lt;/h2&gt;
&lt;p&gt;Action Creators 就是用来生成 action 的函数。
在 Redux 中 action creators 简单的返回一个 action 对象就可以了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function addTodo(text) {
  return {
    type: ADD_TODO,
    text: &quot;Some text&quot;,
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以更加容易的创建一类 action, 而且更易于测试。&lt;/p&gt;
&lt;h2&gt;Dispatch&lt;/h2&gt;
&lt;p&gt;实际的初始化一个 dispatch 可以通过把结果传递给 &lt;code&gt;dispatch()&lt;/code&gt; 函数：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;store.dispatch(addTodo(text));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者，你可以通过创建个一个被的绑定 action creator 用来自动 dispatches：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const boundAddTodo = (text) =&amp;gt; dispatch(addTodo(text));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在你能够直接调用它们了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boundAddTodo(text);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dispatch()&lt;/code&gt; 函数可以直接访问 store 通过 &lt;code&gt;store.dispatch()&lt;/code&gt;，但你可能更喜欢通过使用一个像 react-redux 的 &lt;code&gt;connect()&lt;/code&gt; 这样辅助函数来访问。可以通过 &lt;code&gt;bindActionCreators()&lt;/code&gt; 来自动绑定很多 action creators 到 &lt;code&gt;dispatch()&lt;/code&gt; 函数上。&lt;/p&gt;
&lt;h1&gt;Reducers&lt;/h1&gt;
&lt;p&gt;Actions 描述一个发生了什么事情的事实，但是没有指定如何去改变应用的 state. 这个就是 reducer 要做的。&lt;/p&gt;
&lt;h2&gt;设计 State 结构&lt;/h2&gt;
&lt;p&gt;在 Redux 里面，应用的所有状态都被存储在一个单一的对象中。所以在写代码之前考虑一下如何设计 state 是很重要的。如何用最简单的方法来把应用状态描述为一个对象。
对于一个 todo 应用来说，我们想要存储两个不同的事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前选中的显示过滤条件&lt;/li&gt;
&lt;li&gt;实际的 todos 列表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单的 state 就是下面这个样子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
   visibilityFilter: &apos;SHOW_ALL&apos;,
   todos: [
       {
           text: &apos;Consider using Redux&apos;,
           completed: true
       },{
           text: &apos;Keep all state in a single tree&apos;,
           completed: false
       }
   ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;处理 Actions&lt;/h2&gt;
&lt;p&gt;Reducer 是一个纯函数，接受之前的 state 和 action 作为参数，返回下一个 state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(previousState, action) =&amp;gt; newState;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之所以叫做 reducer 是因为它要被传递给 &lt;code&gt;Array.prototype.reduce(reducer,?initialValue)&lt;/code&gt; 这个函数。所以保持 reduce 的纯净非常重要。永远不要在 reducer 里面做下面的事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;转换它的函数参数&lt;/li&gt;
&lt;li&gt;做一些带有副作用的操作，比如 API 的调用和路由的跳转&lt;/li&gt;
&lt;li&gt;调用不纯净的函数，比如 &lt;code&gt;Date.now()&lt;/code&gt; 和 &lt;code&gt;Math.random()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;了解了这些以后，我们来开始 reducer 函数。通过指明初始 state 来开始。 Redux 将会在第一次调用 reducer 时传递一个 &lt;code&gt;undefined&lt;/code&gt; state. 这时我们需要返回一个初始化的 state：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { VisibilityFilters } from &quot;./actions&quot;;

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todo: [],
};

function todoApp(state = initialState, action) {
  //采用 ES2015 写法，当 state 传递为 undefined 时，会被赋值为 initialState
  return state;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来处理 &lt;code&gt;SET_VISIBILITY_FILTER&lt;/code&gt;. 需要做的就是在 state 上改变 &lt;code&gt;visibilityFilter&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.SET_VISIBILITY_FILTER,
      });
    default:
      return state;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们不修改原有的 state. 通过 &lt;code&gt;Object.assign()&lt;/code&gt; 创建一个原有 state 和 要改变内容合并后的副本。&lt;/li&gt;
&lt;li&gt;当找不到状况也就是 default 时，必须返回之前的 state&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;处理更多的 Actions&lt;/h2&gt;
&lt;p&gt;还有一些 action 需要去处理，我们也都一一加上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter,
      });
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false,
          },
        ],
      });
    default:
      return state;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以通过分离 reducer 函数来使它更加容易理解，毕竟把 todos 相关的处理逻辑和 visibilityFilter 的处理逻辑放在一块不是很清晰。分离 reducer 也是很简单的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false,
        },
      ];
    default:
      return state;
  }
}

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter;
    default:
      return state;
  }
}

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，每个 reducer 都管理着整个 state 中属于自己的部分。每个 reducer 的 &lt;code&gt;state&lt;/code&gt; 参数都不同，分别对应他们自己管理部分的 state.
当应用变得庞大时，我们可以把 reducer 分离到多个不同的文件中，保持独立性并管理不同的数据源。
最后，Redux 提供了一个 &lt;code&gt;combineReducers()&lt;/code&gt; 函数来做和上面 todoApp 同样逻辑的事情，合并多个 reducer, 这样可以省略很多样板代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { combineReducers } from &quot;redux&quot;;

const todoApp = combineReducers({
  visibilityFilter,
  todos,
});

export default todoApp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它和下面的写法是完全等价的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;combineReducers()&lt;/code&gt; 所做的就是生成一个函数，并给每个 reducer 函数并传入对应的 state，并把它们合并成一个单独的对象。这并不是&lt;a href=&quot;https://github.com/reactjs/redux/issues/428#issuecomment-129223274&quot;&gt;黑魔法&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;combineReducers 原理&lt;/h3&gt;
&lt;p&gt;15 年看 Redux 时，没有读文档，这里一直没能理解，不知道 &lt;code&gt;combineReducers()&lt;/code&gt; 到底是怎么做的，以为他就是个黑魔法。而且还能通过 &lt;code&gt;combineReducers()&lt;/code&gt; 后的函数生成初始化的 store.&lt;/p&gt;
&lt;p&gt;今天重新读了文档和 &lt;a href=&quot;https://github.com/reactjs/redux/issues/428#issuecomment-129223274&quot;&gt;黑魔法&lt;/a&gt; 这个 issue 后理解了。
其实 &lt;code&gt;combineReducers()&lt;/code&gt; 是这样工作的。假设你有两个 reducer ， 一个用来管理 todo 列表，另一个用来管理当前选中的过滤状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; function todos(state = [], action) {
   // Somehow calculate it...
   return nextState
 }
function visibleTodoFilter(state = &apos;SHOW_ALL&apos;, action) {
   // Somehow calculate it...
   return nextState
 }
let todoApp = combineReducers({
   todos,
   visibleTodoFilter
 })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，每个 reducer 中都定义了默认的 state:
&lt;code&gt;todos&lt;/code&gt; 中为 &lt;code&gt;[]&lt;/code&gt;, 而 &lt;code&gt;visibleTodoFilter&lt;/code&gt; 中为 &lt;code&gt;SHOW_ALL&lt;/code&gt;。
当触发一个 action 的时候， 通过 combineReducers 返回的 todoApp 将会调用&lt;strong&gt;整个&lt;/strong&gt; reducer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let nextTodos = todos(state.todos, action);
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终它会把每个 reducer 返回的 state 合并成一个单独的 state 树&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
  todos: nextTodos,
  visibleTodoFilter: nextVisibleTodoFilter,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以理解 combineReducers 的工作原理了。当然你也可以选择不用 combineReducers ，毕竟他只是一个官方提供的辅助工具，你可以自己来实现你的根 reducer.&lt;/p&gt;
&lt;h1&gt;Store&lt;/h1&gt;
&lt;p&gt;Store 是一个对象，用来把它们联系起来。Store 的职责如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保存整个应用的 state&lt;/li&gt;
&lt;li&gt;可以通过 &lt;code&gt;getState()&lt;/code&gt; 来获取 state&lt;/li&gt;
&lt;li&gt;可以通过 &lt;code&gt;dispatch(action)&lt;/code&gt; 来更新 state&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;subscribe(listener)&lt;/code&gt; 来注册监听器&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;subscribe(listener)&lt;/code&gt; 返回的值来处理没有注册的监听器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意的只能有一个 store 在 Redux 应用中。当你想要分离数据逻辑时，你可以通过创建更多的 reducer 来代替更多的 store.
当你拥有一个 reducer 时，创建 store 是很容易的。我们可以通过 &lt;code&gt;combineReducers()&lt;/code&gt; 后创建的根 reducer 来创建 store.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createStore } from &quot;redux&quot;;

import todoApp from &quot;./reducers&quot;;

let store = createStore(todoApp);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以通过传递可选参数来初始化 state. 当你开发一个同构应用时这很有用，可以把服务端传来的 state 作为客户端初始化 state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let store = createStore(todoApp, window.STATE_FROM_SERVER);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;理解了这些内容，就大概可以知道 Redux 是怎么运作的了。可见读文档还是很有用的，不像去年直接去看官方例子里的源码，结果看的一愣一愣的，最后也不明白是个怎么回事。
接下来，要继续学习数据流以及和 React 的配合，并动手写一个 TodoList 实例来加深理解。&lt;/p&gt;
&lt;h3&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;http://redux.js.org/docs/basics&quot;&gt;Basics|Redux&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【翻译向】走进 Redux 之基础篇(二)</title><link>https://ab.kisnows.com/zh-CN/2016/05/03/step-to-redux-2/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/05/03/step-to-redux-2/</guid><pubDate>Tue, 03 May 2016 18:02:50 GMT</pubDate><content:encoded>&lt;p&gt;紧接上一篇&lt;a href=&quot;https://www.kisnows.com/2016/04/26/step-to-redux-1/&quot;&gt;走进 Redux 之基础篇(一)&lt;/a&gt;,接下来会跟着文档从下面三个点开始。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据流&lt;/li&gt;
&lt;li&gt;与 React 配合&lt;/li&gt;
&lt;li&gt;实例：TodoList&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;数据流&lt;/h1&gt;
&lt;p&gt;Redux 的架构围绕着如何搭建一个&lt;strong&gt;严格的单向数据流&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这意味着所有的数据都要遵循一个同样的生命周期模式，可以让整个应用变得可预测也更加易于理解。在 Redux 应用中，所有的数据生命周期分为如下四个步骤：&lt;/p&gt;
&lt;h2&gt;1 你主动调用 &lt;code&gt;store.dispatch(action)&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Action 是一个用来描述发生了事情的对象。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{type:&apos;ADD_TODO&apos;,text:&apos;todo\&apos;s content&apos;}
{type: &apos;FETCH_USER_SUCCESS&apos;, response: {id: 3, name: &apos;Mary&apos;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以认为 action 是一个事件的简短片段描述。你可以在任何地方调用 &lt;code&gt;store.dispatch(action)&lt;/code&gt; ，包括组件和 XHR 中，甚至可以在定时器中调用。&lt;/p&gt;
&lt;h2&gt;2 Redux store 调用你提供的 reducer 函数&lt;/h2&gt;
&lt;p&gt;store 会传递两个参数给 reducer：当前的 state 和 action. 比如，在一个 todo 应用中，根 reducer 会收到类似下面这样的参数：&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let previousState = {
  visibleTodoFilter: &quot;SHOW_ALL&quot;,
  todos: [
    {
      text: &quot;Read the docs&quot;,
      complete: false,
    },
  ],
};
let action = {
  type: &quot;ADD_TODO&quot;,
  text: &quot;Understand the flow&quot;,
};
// nextState 由 todoApp 这个根 reducer 来生成
let nextState = todoApp(previousState, action);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是 reducer 是一个纯函数，没有副作用的，仅仅是计算下一个 state . 他应该是可以完全预测的，对于同样的参数，无论调用多少次都应该返回一个同样值。类似于 API 调用或者路由的变化都应该&lt;strong&gt;在 action 被 dispatch 之前&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;3 根 reducer 可以合并多个子 reducer 的输出来生成一个单一的 state 树。&lt;/h2&gt;
&lt;p&gt;如何组合根 reducer 完全由你来决定。 Redux 提供了 combineReducers() 这个帮助函数，在把根 reducer 分离成独立的函数去管理整个 state 树的一个分支时很有帮助。
我们来看一下 combineReducers() 这个函数怎么工作。假设你有两个 reducer ， 一个用来管理 todo 列表，另一个用来管理当前选中的过滤状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; function todos(state = [], action) {
   // Somehow calculate it...
   return nextState
 }
function visibleTodoFilter(state = &apos;SHOW_ALL&apos;, action) {
   // Somehow calculate it...
   return nextState
 }
let todoApp = combineReducers({
   todos,
   visibleTodoFilter
 })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当触发一个 action 的时候， 通过 &lt;code&gt;combineReducers&lt;/code&gt; 返回的 &lt;code&gt;todoApp&lt;/code&gt; 将会调用&lt;strong&gt;所有&lt;/strong&gt; reducer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let nextTodos = todos(state.todos, action)
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会把每个 reducer 返回的 state 合并成一个单独的 state 树&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return {
   todos: nextTodos,
   visibleTodoFilter: nextVisibleTodoFilter
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你也可以选择不用 &lt;code&gt;combineReducers&lt;/code&gt; ，毕竟他只是好用的辅助工具，你可以自己来实现你的根 reducer.&lt;/p&gt;
&lt;h2&gt;4 Redux store 会保存由根 reducer 返回的整个 state 树&lt;/h2&gt;
&lt;p&gt;现在这个新的 state 树就是当前应用的下一个 state 。所有通过 &lt;code&gt;store.subscribe(listener)&lt;/code&gt; 注册了监听事件的监听函数都会被调用； 监听对象可能会调用 &lt;code&gt;store.getState()&lt;/code&gt; 来获取当前的 state .
现在，整个视图都可以通过新的 state 来更新。如果你使用 React Redux 作为视图绑定工具，那么这就是 component.setState(newState) 调用的节点。&lt;/p&gt;
&lt;h1&gt;和 React 配合使用&lt;/h1&gt;
&lt;p&gt;虽然 Redux 和 React 之间没有联系，但是 Redux 确实和类似于 React 和 Deku 这类可以用数据状态来描述 UI 的框架配合的很好。我们将会使用 React 来搭建一个简单的 Todo 应用。&lt;/p&gt;
&lt;h2&gt;安装 React Redux&lt;/h2&gt;
&lt;p&gt;React bindings 并没有默认包括在 Redux 中，我们需要单独的安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save react-redux
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;展示型组件 (Presentational Components) 和容器型组件 (Container Components)&lt;/h2&gt;
&lt;p&gt;Redux 的 React bindings 信奉 &lt;strong&gt;展示型组件和容器型组件分离&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;---&lt;/th&gt;
&lt;th&gt;展示型组件&lt;/th&gt;
&lt;th&gt;容器型组件&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;目的&lt;/td&gt;
&lt;td&gt;界面看起来的样子（标记，样式）&lt;/td&gt;
&lt;td&gt;事情是怎样工作的（数据获取，状态更新）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;意识到 Redux&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;读取数据&lt;/td&gt;
&lt;td&gt;从父 props 中读取&lt;/td&gt;
&lt;td&gt;顶 Redux state 中订阅获取&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修改数据&lt;/td&gt;
&lt;td&gt;从 props 中调用回调函数&lt;/td&gt;
&lt;td&gt;分发 (dispatch) Redux actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are written&lt;/td&gt;
&lt;td&gt;手写&lt;/td&gt;
&lt;td&gt;通常由 React Redux 生成&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;大多数组件都应该被写成展示型组件，但是我们也需要生成一些容器型组件类把它们和 Redux store 连接起来。&lt;/p&gt;
&lt;p&gt;技术上说，你可以通过使用 &lt;code&gt;store.subscribe()&lt;/code&gt; 来手写容器型组件。但是我们不建议这样做，因为 React Redux 做了很多你很难直接手写出来的性能优化。所以，比起手写容器型组件，我们更建议通过 React Redux 提供的 &lt;code&gt;connect()&lt;/code&gt; 函数来生成它。&lt;/p&gt;
&lt;h2&gt;设计组件层级&lt;/h2&gt;
&lt;p&gt;我们的设计很简单。我们想要展示一个 todo 内容列表。一个按钮，用来标记一个 todo 内容是否完成。一个用来添加新 todo 的地方。在页脚部，我们需要一个切换键来展示所有的、完成的或者未完成的 todos.&lt;/p&gt;
&lt;h3&gt;展示型组件&lt;/h3&gt;
&lt;p&gt;通过下面这些展示型组件，我们可以概括出一个 props 层级。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TodoList&lt;/code&gt; 是一个用来展示可用 Todos 的列表
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;todos: Array&lt;/code&gt; 一个内容为 { id, text, completed } 样子的列表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onTodoClick(id: number)&lt;/code&gt; 一个回调函数，当一个 todo 被点击的时候&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Todo&lt;/code&gt; 一个单独的 todo 项目
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text: string&lt;/code&gt; 要展示的文字&lt;/li&gt;
&lt;li&gt;&lt;code&gt;completed: boolean&lt;/code&gt; todo 是否被完成的状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onClick&lt;/code&gt; 一个回调函数，当一个 todo 被点击时调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Link&lt;/code&gt; 一个用回调的链接
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onClick&lt;/code&gt; 这个链接被点击时的回调&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Footer&lt;/code&gt; 用来改变当前显示 todo 内容的地方&lt;/li&gt;
&lt;li&gt;&lt;code&gt;App&lt;/code&gt; 根组件，用来渲染所有其它的东西&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们描述这个应用看起来的样子，但是不知道数据从哪里来，也不知道如何改变他们。只是渲染我们给它的数据。如果你要从 Redux 迁移到其它框架，这些组件几乎都是可以不用改动的。它们和 Redux 之间没有联系。&lt;/p&gt;
&lt;h3&gt;容器型组件&lt;/h3&gt;
&lt;p&gt;我们同样需要一些容器型组件来把展示型组件连接到 Redux. 比如，&lt;code&gt;TodoList&lt;/code&gt; 组件需要一个容器 &lt;code&gt;VisibleTodoList&lt;/code&gt; 用来从 Redux 去订阅数据来知道如何使用当前的可视性筛选条件 (visibility filter). 为了更改可视性筛选条件，我们提供一个 &lt;code&gt;FilterLink&lt;/code&gt; 容器来渲染 &lt;code&gt;Link&lt;/code&gt; 用来 dispatches 一个合适的 action 在点击的时候：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VisibleTodoList&lt;/code&gt; 通过当前的可视性筛选条件来过滤要展示的内容并渲染一个 &lt;code&gt;TodoList&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FilterLink&lt;/code&gt; 获得当前的可视性筛选条件并渲染 &lt;code&gt;Link&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;filter: string&lt;/code&gt; 表示一个可视性筛选条件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;其它组件&lt;/h3&gt;
&lt;p&gt;有时，我们难以去区分一个组件是展示型还是容器型的。比如，有时表单和函数是互相依赖的， 就像这个小型的组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AddTodo&lt;/code&gt; 一个有 ‘添加’ 按钮的输入框&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;技术上说，我们可以把它分离成两个组件，但是这样明显太繁琐了。当项目变得复杂和庞大的时候，我们可以把它分离开，但是现在的话，就这样让他们混合在一起吧。&lt;/p&gt;
&lt;h3&gt;实现组件&lt;/h3&gt;
&lt;h4&gt;展示型组件&lt;/h4&gt;
&lt;p&gt;就是普通的 React 组件，就不详细介绍了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TodoList.js
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import React, { PropTypes } from &quot;react&quot;;

import Todo from &quot;./Todo&quot;;

const TodoList = ({ todos, onTodoClick }) =&amp;gt; (
  &amp;lt;ul&amp;gt;
    {todos.map((todo) =&amp;gt; (
      &amp;lt;Todo key={todo.id} {...todo} onClick={() =&amp;gt; onTodoClick(todo.id)} /&amp;gt;
    ))}
  &amp;lt;/ul&amp;gt;
);
TodoList.propTypes = {
  todos: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      completed: PropTypes.bool.isRequired,
      text: PropTypes.string.isRequired,
    }).isRequired,
  ).isRequired,
  onTodoClick: PropTypes.func.isRequired,
};
export default TodoList;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;容器型组件&lt;/h4&gt;
&lt;p&gt;接下来我们通过生成容器型组件来把展示型组件和 Redux 链接起来。技术上说，一个容器型组件只是一个 React 组件，通过使用 &lt;code&gt;store.subscribe()&lt;/code&gt; 来读取一部分 Redux state tree 并且提供 props 给一个展示型组件用来渲染。你可以手写这个组件，但是我们建议使用 Redux 提供的 &lt;code&gt;connect()&lt;/code&gt; 函数来生成这些容器型组件，它提供了很多有用的优化去阻止一些不必要重新渲染。&lt;/p&gt;
&lt;p&gt;为了使用它 &lt;code&gt;connect()&lt;/code&gt; , 你需要定义一个叫做 &lt;code&gt;mapStateToProps&lt;/code&gt; 的特殊函数，用来指明如何转换当前的 Redux store state 成为你想要传递给展示型组件的 props. 比如， &lt;code&gt;VisibleTodoList&lt;/code&gt; 需要去计算 &lt;code&gt;todos&lt;/code&gt; 来传递给 &lt;code&gt;TodoList&lt;/code&gt;, 所以我们定义一个函数通过 &lt;code&gt;state.visibilityFilter&lt;/code&gt; 用来过滤 &lt;code&gt;state.todos&lt;/code&gt;, 并且在 &lt;code&gt;mapStateToProps&lt;/code&gt; 中使用它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const getVisibleTodos = (todos, filter) =&amp;gt; {
  switch (filter) {
    case &quot;SHOW_ALL&quot;:
      return todos;
    case &quot;SHOW_COMPLETED&quot;:
      return todos.filter((t) =&amp;gt; t.completed);
    case &quot;SHOW_ACTIVE&quot;:
      return todos.filter((t) =&amp;gt; !t.completed);
  }
};
const mapStateToProps = (state) =&amp;gt; {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter),
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了读取 state 之外， 容器型组件可以 dispatch actions. 通过同样的方式，你可以定义一个名为 &lt;code&gt;mapDispatchToProps()&lt;/code&gt; 的函数去接收 &lt;code&gt;dispatch()&lt;/code&gt; 方法，并返回一个回调 props 用来注入到你想要的展示型组件中。比如，我们想要 &lt;code&gt;VisibleTodoList&lt;/code&gt; 去注入一个名为 &lt;code&gt;onTodoClick&lt;/code&gt; 的 prop 到 &lt;code&gt;TodoList&lt;/code&gt; 组件中，而且我们想要 &lt;code&gt;onTodoClick&lt;/code&gt; 去 dispatch 一个 `TOGGLE_TODO&apos; action:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mapDispatchToProps = (dispatch) =&amp;gt; {
  return {
    onTodoClick: (id) =&amp;gt; {
      dispatch(toggleTodo(id));
    },
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们通过调用 &lt;code&gt;connect()&lt;/code&gt; 来生成一个 &lt;code&gt;VisibleTodoList&lt;/code&gt; 并传递这两个函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { connect } from &quot;react-redux&quot;;

const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList);
export default VisibleTodoList;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些都是 React Redux 的基础 API, 但是这有一些快捷方式和强大的选项，所以我们鼓励你去仔细查看 &lt;a href=&quot;https://github.com/reactjs/react-redux&quot;&gt;这个文档&lt;/a&gt;. 如果你担心 &lt;code&gt;mapStateToProps&lt;/code&gt; 创建新对象的过程，你可能需要去了解 &lt;a href=&quot;http://redux.js.org/docs/recipes/ComputingDerivedData.html&quot;&gt;computing derived data&lt;/a&gt; with &lt;a href=&quot;https://github.com/rackt/reselect&quot;&gt;reselect&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;传递到 Store&lt;/h4&gt;
&lt;p&gt;所有的容器型组件需要连接到 Redux store 这样它们才能订阅它。一个方式是把它作为一个 prop 传递给每一个容器型组件。然而那样过于繁琐，
我们推荐的方式是通过使用特定的 React Redux 组件 &lt;code&gt;&amp;lt;Provider&amp;gt;&lt;/code&gt; 去黑魔法般的使 store 可用给应用中的所有容器型组件而不用专门去传递它。你只需要在渲染根组件的时候调用它一次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Index.js
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import { render } from &quot;react-dom&quot;;
import { Provider } from &quot;react-redux&quot;;
import { createStore } from &quot;redux&quot;;

import App from &quot;./components/App&quot;;
import todoApp from &quot;./reducers&quot;;

let store = createStore(todoApp);
render(
  &amp;lt;Provider store={store}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/Provider&amp;gt;,
  document.getElementById(&quot;root&quot;),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;实例：TodoList&lt;/h1&gt;
&lt;p&gt;直接看文档，虽然了解到很多专有名词和方法，但还是看不出个所以然来。所以还是得看代码，这里是官方的 &lt;a href=&quot;http://redux.js.org/docs/basics/ExampleTodoList.html&quot;&gt;TodoList&lt;/a&gt; 源码，结合这两篇文章应该就能很好的理解了。
我在这里遇到了几个问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;webpack 一直报错找不到入口文件 &apos;./index&apos; , 最后发现 babel 的坑。 Babel 升级到 6.0 以后需要在 .babelrc 这个文件中指明要编译 js 的文件类型，否则编译会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
&quot;presets&quot;: [&quot;es2015&quot;, &quot;react&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;页面渲染出来后，点击添加什么都是可以正常运行的。但是点击筛选的时候老是报错，而且报的错莫名其妙。于是一步一步打断点来看，最后发现是 &lt;code&gt;Footer.js&lt;/code&gt; 里面 filter 对应的 action 拼写错误导致后面的组件找不到对应的 action&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;

import FilterLink from &quot;../containers/FilterLink&quot;;

const Footer = () =&amp;gt; (
  &amp;lt;p&amp;gt;
    Show: &amp;lt;FilterLink filter=&quot;SHOW_ALL&quot;&amp;gt;All&amp;lt;/FilterLink&amp;gt;
    {&quot;, &quot;}
    &amp;lt;FilterLink filter=&quot;SHOW_ACTIVE&quot;&amp;gt;Active&amp;lt;/FilterLink&amp;gt;
    {&quot;, &quot;}
    &amp;lt;FilterLink filter=&quot;SHOW_COMPLETED&quot;&amp;gt;Completed&amp;lt;/FilterLink&amp;gt;
  &amp;lt;/p&amp;gt;
);
export default Footer;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见应用的 action 还是需要专门单独维护一个文件，将所有的 action 都定义为变量然后暴露出来，这样如果后面有拼写错误的话，那么在编译阶段就可以发现问题了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;http://redux.js.org/docs/basics&quot;&gt;Basics|Redux&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【翻译向】走进 Redux 之高级篇</title><link>https://ab.kisnows.com/zh-CN/2016/05/22/step-to-redux-3/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/05/22/step-to-redux-3/</guid><pubDate>Sun, 22 May 2016 04:14:46 GMT</pubDate><content:encoded>&lt;h1&gt;异步的 Actions&lt;/h1&gt;
&lt;p&gt;前几篇的教程里面，我们搭建了一个 todo 应用。这是一个完全同步的，每次一个 action 被 dispatched, state 都会即时更新。
解析来，我们要搭建一个不同的，异步的应用。它使用 Reddit API 来展示一个选中栏目的头条。&lt;/p&gt;
&lt;h2&gt;Actions&lt;/h2&gt;
&lt;p&gt;当你调用一个异步的 API 时，有两个至关重要的时刻：你开始调用的时刻，和你收到答复的时刻。&lt;/p&gt;
&lt;p&gt;这两个时刻通常都会需要对应用的 state 做出变化；为此，你需要 dispatch 普通的将会被 reducers 同步处理的 actions. 通常，对于任意 API 请求，你将需要 dispatch 至少三次不同的 actions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个通知 reducers 请求开始的 action
reducers 可能通过切换一个 state 中的 &lt;code&gt;isFetching&lt;/code&gt; flag 来处理这个 action. 用来告诉 UI 是时候显示一个等待标识了。&lt;/li&gt;
&lt;li&gt;一个通知 reducers 请求已经成功完成的 action
reducers 可能通过把新数据合并到它控制的 state 中并重置 &lt;code&gt;isFetching&lt;/code&gt; 来处理这个 action. UI 会隐藏等待标识，并展示获取到的数据。&lt;/li&gt;
&lt;li&gt;一个通知 reducers 请求失败的 action
reducers 可能通过重置 &lt;code&gt;isFetching&lt;/code&gt; 来处理这个 action. 另外，有些 reducers 可能想要存储这个错误信息，这样可以让 UI 展示出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可能需要在 actions 添加一个专用的 &lt;code&gt;status&lt;/code&gt; 字段：&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{type: &apos;FETCH_POSTS&apos;}
{type: &apos;FETCH_POSTS&apos;, status: &apos;error&apos;, error: &apos;Oops&apos;}
{typs: &apos;FETCH_POSTS&apos;, status: &apos;success&apos;, response: { ... }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者为它们定义单独的 types：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ type: &apos;FETCH_POSTS_REQUEST&apos; }
{ type: &apos;FETCH_POSTS_FAILURE&apos;, error: &apos;Oops&apos; }
{ type: &apos;FETCH_POSTS_SUCCESS&apos;, response: { ... } }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选择一个拥有 flags 的单独的 action type, 或者多个 actions types, 这都取决与你。多个 types 会有更少的犯错空间，但这不是一个问题，如果你使用 &lt;a href=&quot;https://github.com/acdlite/redux-actions&quot;&gt;redux-actions&lt;/a&gt; 这样的辅助库来生成 action creator 和 reducers 的话。&lt;/p&gt;
&lt;h1&gt;异步的数据流&lt;/h1&gt;
&lt;p&gt;不使用 middlerware, Redux store 只提供同步的数据流。这是你通过 createStore() 得到的默认结果。&lt;/p&gt;
&lt;p&gt;可以使用 applyMiddleware() 来增强 createStore(). 这不是必须的，但它可以让你通过一个便利的方法描述异步的 action 。&lt;/p&gt;
&lt;p&gt;异步的 middleware 比如 redux-thunk 或 redux-promise 都包装了 store 的 dispatch() 方法，允许你 dispatch 除了 action 以外的内容，比如函数或者 Promise。你所使用的 middleware 可以以自己的方式解析你 dispatch 的任何内容，并继续传递 actions 给下一个 middleware。比如，支持 Promise 的 middleware 能够拦截 Promise，然后为每个 Promise 异步地 dispatch 一对 begin/end actions。&lt;/p&gt;
&lt;p&gt;当数据流上最后一个 middleware dispatches 一个 actions 时，它必须是一个普通的对象。这是同步的 Redux 数据流开始的地方。&lt;/p&gt;
&lt;h1&gt;中间件（Middleware）&lt;/h1&gt;
&lt;p&gt;中间件提供一个第三方的扩展点，在 dispatching 一个 action 和 它到达 reducer 的中间时刻。人们使用中间件来打印日志，记录崩溃报告，调用一个异步的 API, 路由……
这里有&lt;a href=&quot;http://redux.js.org/docs/advanced/Middleware.html#seven-examples&quot;&gt;一些例子&lt;/a&gt;来展示中间件的强大作用&lt;/p&gt;
&lt;h2&gt;理解中间件&lt;/h2&gt;
&lt;p&gt;中间件可以做很多事情，理解它是从哪来的非常重要。我们通过使用 logging 和 crash reporting 这两个例子来展示一个使用中间件的思维过程。&lt;/p&gt;
&lt;h3&gt;问题: 日志记录&lt;/h3&gt;
&lt;h4&gt;尝试#1：手动记录&lt;/h4&gt;
&lt;p&gt;最原始的解决方案就是每次在调用 &lt;code&gt;store.dispatch(action)&lt;/code&gt; 时记录 action 和 下一个 state. 代码可能就是下面的样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let action = addTodo(&quot;Use Redux&quot;);

console.log(&quot;dispatching&quot;, action);
store.dispatch(action);
console.log(&quot;next state&quot;, store.getState());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这真的是一种很搓的办法。&lt;/p&gt;
&lt;h4&gt;尝试#2：包装 Dispatch&lt;/h4&gt;
&lt;p&gt;把 logging 提取到一个函数里面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function dispatchAndLog(store, action) {
  console.log(&quot;dispatching&quot;, action);
  store.dispatch(action);
  console.log(&quot;next state&quot;, store.getState());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样每次调用的时候通过使用这个函数来替换 &lt;code&gt;store.dispatch()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dispatchAndLog(store, addTOdo(&quot;USe Redux&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样已经能解问题了，但是这样并不优雅。&lt;/p&gt;
&lt;h4&gt;尝试#3： Monkeypatching Dispatch&lt;/h4&gt;
&lt;p&gt;如果我们只在 store 里面替换 &lt;code&gt;dispatch&lt;/code&gt; 函数呢？ Redux 的 store 只是一个拥有几个方法的普通对象，所以我们可以改写 &lt;code&gt;dispatch&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let next = store.dispatch
store.dispath = fucntion dispatchAndLog (action){
    console.log(&apos;dispatching&apos;,action)
    let result = next(action)
    console.log(&apos;next state&apos;,soter.getState())
    return reslut
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样基本上已经达到了我们的妖要求，不论你在哪里 dispatch 一个 action, 它都保证会被记录下来。虽然我们通过 Monkeypatching 改写了 store 内部的方法，但暂时先这样。&lt;/p&gt;
</content:encoded></item><item><title>泰国懒人游</title><link>https://ab.kisnows.com/zh-CN/2018/06/09/thailand-travel-notes/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2018/06/09/thailand-travel-notes/</guid><pubDate>Sat, 09 Jun 2018 23:57:12 GMT</pubDate><content:encoded>&lt;h1&gt;泰国&lt;/h1&gt;
&lt;h2&gt;落地签&lt;/h2&gt;
&lt;p&gt;我是飞到曼谷素万那普机场，下机后跟着人流直走，头上有指示牌，可以一路走到办理落地签的地方。
办理落地签有两个通道，一个普通通道，一个快速通道。快速通道 2200🐖，普通通道 2000🐖。
建议走快速通道，能省很多时间。
&lt;img src=&quot;/imgs/thailand-travel-notes/1.jpg&quot; alt=&quot;机场&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;机场大巴&lt;/h2&gt;
&lt;p&gt;从扶梯下来，好像是到一楼，然后会看到大巴售票点。
&lt;img src=&quot;/imgs/thailand-travel-notes/2.png&quot; alt=&quot;机场&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后会看到大巴售票点，就长下图这个样子，有到芭堤雅和华欣的大巴，大概每小时一班车。
&lt;img src=&quot;/imgs/thailand-travel-notes/3.jpg&quot; alt=&quot;大巴售票点&quot; /&gt;
然后面向大巴售票点左手边有吃饭的地方，在这个位置。
&lt;img src=&quot;/imgs/thailand-travel-notes/4.jpg&quot; alt=&quot;机场饭厅&quot; /&gt;&lt;/p&gt;
&lt;p&gt;里面东西很便宜，进去要换票，一个人的话 100🐖 就够了。能吃很多东西，我当时吃了一碗面，一罐可乐，还剩了 15🐖。
&lt;img src=&quot;/imgs/thailand-travel-notes/5.jpg&quot; alt=&quot;机场&quot; /&gt;
&lt;img src=&quot;/imgs/thailand-travel-notes/6.jpg&quot; alt=&quot;机场&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;曼谷 -&amp;gt; 芭堤雅&lt;/h2&gt;
&lt;p&gt;在大巴售票点买好票，售票员说 1.30 发车，我问了下在哪里，他指了个地方，说到 1.30 会有人领你们过去。
从曼谷到芭堤雅大概两个小时的样，在芭堤雅有好几个下车点，分别是 North Pattaya Road（芭提雅北路）和 Central Pattaya Road（芭提雅中路）、South Pattaya Road（芭提雅南路）、 Thepprasit Road 和 Thappraya Road。下车点其实挺多的，特别是 Pattaya Road 的北、中、南路（都靠近海边的 Beach Road），就是芭提雅的中心地带了，在这三个地点下车都很方便。
我当时在 Central Pattaya Road（芭提雅中路）下车，然后就有一个摩的师傅上来拉活。我问了下价格，他说 one two zero ，我一开始半天没理解他们说什么，以为是 ‘three hundred’ 300 🐖。就拿出计算器按数字给他们看，他说不是，按了个 120 上去，我一想也行，就上车了。女生的话不怎么建议座这个，总感觉不是很安全的样子。
&lt;img src=&quot;/imgs/thailand-travel-notes/7.jpg&quot; alt=&quot;摩的-芭堤雅&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;芭堤雅&lt;/h1&gt;
&lt;p&gt;摩的直接把我载到了酒店，给钱的时候发生了一件很有意思的事情。我拿出一张 50 和 20 给他，我以为那张 50 是 100. 然后师傅一直在说什么我听不懂的语言，我还以为要坑我。两人瞎扯了半天，我才发现我给的钱不对，赶紧补上了 100 。
进酒店办理入住，放下东西就先睡了一觉，哈哈。&lt;/p&gt;
&lt;p&gt;一觉睡到了五点多，给饿醒了。就出去觅食，附近随便找了找，吃的东西还是很多的，也不是很贵，下面这个 200 🐖。
&lt;img src=&quot;/imgs/thailand-travel-notes/8.jpg&quot; alt=&quot;菠萝饭&quot; /&gt;&lt;/p&gt;
&lt;p&gt;吃完出去到海边走了走，就回去了。
&lt;img src=&quot;/imgs/thailand-travel-notes/9.jpg&quot; alt=&quot;海滩-芭堤雅&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;交通&lt;/h2&gt;
&lt;p&gt;芭堤雅海边的交通还是很方便的，直接座双条车就好了，但是都是单行。
&lt;img src=&quot;/imgs/thailand-travel-notes/10.jpg&quot; alt=&quot;双条车-芭堤雅&quot; /&gt;
南北走向， 1st rd 南行，2nd rd 北行，双条招手即停，只要不问司机目的地和加钱，默认 10🐖，看好下车点按铃下车，先下车再付费。如果问了司机能不能去哪里，默认为包车价，但是要记得砍价。
&lt;img src=&quot;/imgs/thailand-travel-notes/11.jpg&quot; alt=&quot;地图-芭堤雅&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;趣事&lt;/h2&gt;
&lt;p&gt;在芭堤雅的第二天就海边瞎逛了一天，中午在大众点评上定了个西餐，就从酒店出发去餐厅了。&lt;/p&gt;
&lt;p&gt;里面的服务员都很礼貌，我上去说操着蹩脚的英语 I have a preorder ,结果他们没听懂。正当我打算打开翻译看看我提前预定了这句话怎么说时，一位服务员拿了一打订单过来问我叫什么名字，我说 ab 他们疑惑了一下 指着 ba 问是不是我，哈哈，我都忘了要先名后姓。
然后就发生了很有意思的事情，他们端上来了一个很大的汉堡，就像图里这样：
&lt;img src=&quot;/imgs/thailand-travel-notes/food.jpg&quot; alt=&quot;food-芭堤雅&quot; /&gt;
我一看这么大，犹豫了半天不知道怎么吃，然后就做出了很可笑的事情。叫来服务员，问了一句：&quot;How to eat it?&quot;
女服务员姐姐直接笑了，然后跟我说用刀叉切，我傻逼的就去拿刀叉切了。刚切了一块，经理过来说你想怎么吃就怎么么吃，it’s up to you. 然后我就直接上手了。
我都服了我，怎么能问出怎么吃这种问题，哈哈。&lt;/p&gt;
&lt;p&gt;中途进来了几个跟团游的国人，我还帮他们当了下翻译，就我这蹩脚的英语，哈哈。不过不得不说，国人确实一进来大吵大闹的，感觉很不好，可能是因为都是大爷大妈，平时都习惯了的原因吧。&lt;/p&gt;
&lt;p&gt;吃完逛了一圈，去商场买了条腰带，顺便帮一个网友到了她要的面膜。就回酒店休整了。&lt;/p&gt;
&lt;h2&gt;人&lt;/h2&gt;
&lt;p&gt;下午去海边散步，认识了两个国内来的妹子，聊的很开心。
&lt;img src=&quot;/imgs/thailand-travel-notes/12.jpg&quot; alt=&quot;妹子&quot; /&gt;&lt;/p&gt;
&lt;p&gt;晚上打算去吃饭，被妹子拉着去步行街（红灯区）逛了一圈，红灯区里一看你是中国人，就会有很多人拿着不堪入目的牌子上来问你，说看不看什么表演之类的。不要去理就好了，就算你真抱着猎奇的心理去看，也不建议跟着这些推销的人去，很容易被宰。
&lt;img src=&quot;/imgs/thailand-travel-notes/13.jpg&quot; alt=&quot;步行街-芭堤雅&quot; /&gt;
&lt;img src=&quot;/imgs/thailand-travel-notes/14.jpg&quot; alt=&quot;步行街-芭堤雅&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不过吃饭的时候遇到了一点不愉快，我们大老远从芭堤雅市区走了好久然后打车才到那个店打算吃海鲜。
结果我们一进去人家说了什么一堆乱七八糟的英语，我们三个人没一个人听懂，然后说我们找错地方了，带我们出去给我指路说在那边。
然而在她手指的方向根本什么都没有，看起来应该是被餐厅莫名其妙的拒绝接待了。就下图这个餐厅： Maggic Restaurant, 最后只能打车回市区吃饭。
&lt;img src=&quot;/imgs/thailand-travel-notes/maggic.jpg&quot; alt=&quot;拒绝我们的餐厅&quot; /&gt;
吃完饭就跟妹子分道扬镳了。&lt;/p&gt;
&lt;h2&gt;可兰岛&lt;/h2&gt;
&lt;p&gt;第三天报了个可兰岛一日游，玩的很嗨，全程有人接送，不用操什么心，哈哈。什么滑翔伞、海底漫步、浮潜乱七八糟的加起来才不到四百块，简直太便宜了。玩的也很舒服，就是师傅游艇开的很快，晕船的人要提前备好晕船药，看到船上有人晕的不行。
&lt;img src=&quot;/imgs/thailand-travel-notes/15.jpg&quot; alt=&quot;可兰岛-芭堤雅&quot; /&gt;
这中间又认识了一个妹子，太强了，一个人出来玩，还玩了跳伞。佩服的不行，我本来也想玩的，可最后还是怂了，一是真的贵 3000 大洋，而是三千米跳下来还是很可怕的，我承认我一个人有点怂，哈哈。&lt;/p&gt;
&lt;h2&gt;芭堤雅 -&amp;gt; 曼谷&lt;/h2&gt;
&lt;p&gt;第四天早上就从芭堤雅往曼谷走，还是做大巴。酒店前台帮忙找了个摩的师傅把我拉过去，80🐖。
&lt;img src=&quot;/imgs/thailand-travel-notes/16.jpg&quot; alt=&quot;车站-芭堤雅&quot; /&gt;
进站买票后，就在候车室等候，车票上会写发车时间和座位号，按照要求走就行了。
&lt;img src=&quot;/imgs/thailand-travel-notes/17.jpg&quot; alt=&quot;候车室-芭堤雅&quot; /&gt;
&lt;img src=&quot;/imgs/thailand-travel-notes/18.jpg&quot; alt=&quot;车票-芭堤雅&quot; /&gt;&lt;/p&gt;
&lt;p&gt;又是两个小时，到了车站下来，我就去找 bts ，跟着地图走就行，下图这个就是 ekkamai 站入口。
&lt;img src=&quot;/imgs/thailand-travel-notes/19.jpg&quot; alt=&quot;bts-曼谷&quot; /&gt;
进去后买票等车，买票的话可以选择自动售卖机，但是只收硬币。也可以选择人工窗口，给纸币然后说要去那个站点就好了。
&lt;img src=&quot;/imgs/thailand-travel-notes/20.jpg&quot; alt=&quot;bts-曼谷&quot; /&gt;
候车的时候问了个泰妹，我要去的站是从那边做，因为怕做错。泰妹跟我说就这里，然而上车后还是怕做错站，就盯着这个指示和到站语音提醒看我做的方向对不对，还好是对的，果然漂亮的小姐姐不会骗人。
&lt;img src=&quot;/imgs/thailand-travel-notes/21.jpg&quot; alt=&quot;bts-曼谷&quot; /&gt;
因为住在老城区，bts 不直达，到站后打了个的士才到酒店，座的士去酒店的路上中间还发生了一点小插曲。
提醒一下，打的的话要注意，一定让他打表 &apos;by meter&apos; ，如果他不打表，那么要提前讲好价，否则容易被宰。或者选择用 Grab 这个打车软件，后来几天我就是用这个的，很方便，不过一定要看准定位，毕竟你们语言不通，司机就是打电话找你你也不知道对方在说什么。&lt;/p&gt;
&lt;h1&gt;曼谷&lt;/h1&gt;
&lt;p&gt;到酒店收拾了一下，就去跟在芭堤雅新认识的一个也来了曼谷的朋友汇合。在暹罗广场吃了个饭，瞎逛了一会。商场里面竟是豪车，然而可能这辈子都买不起了，悲伤啊。
&lt;img src=&quot;/imgs/thailand-travel-notes/22.jpg&quot; alt=&quot;商场-曼谷&quot; /&gt;&lt;/p&gt;
&lt;p&gt;晚上和之前约好的一个妹子去考山路逛了一圈，真的是纯逛了一圈，都没有坐下来喝点东西感受一下气氛。
&lt;img src=&quot;/imgs/thailand-travel-notes/23.jpg&quot; alt=&quot;考山路-曼谷&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;水上市场&lt;/h2&gt;
&lt;p&gt;第二天报了一个水上市场一日游，全程有人接送，懒懒散散的逛了一圈，挺适合我，不用赶时间。里面可以买些小物件，挺好的。
&lt;img src=&quot;/imgs/thailand-travel-notes/24.jpg&quot; alt=&quot;水上市场-曼谷&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虽然行程懒散，但时间还是挺长的，晚上看完萤火虫已经八点多了，回酒店休整了一下，然后在附近按了下脚就回酒店睡觉了。&lt;/p&gt;
&lt;h2&gt;市区&lt;/h2&gt;
&lt;p&gt;第三天在泰国市区逛了一圈，有个地方好多日本人，店里面的点单也都是日文加泰文，可能附近很多日本人定居在这里吧。&lt;/p&gt;
&lt;p&gt;晚上从 phone 逛完，本来打算座 bts 然后再打车回去的，路上却碰到了一排摩的师傅，就上去问了下去考山路多少钱。一个师傅说 200，我说太贵，150 行不行，他说不行，至少 170，我也懒得坎价了，就打算跟他走了。结果有意思的是，不是跟我讲价师傅载我，而是另一个人载我，我还纳闷着呢，结果在我的师傅说我们两个一起送我过去，我一脸懵逼，我这么牛逼吗。然后带上头盔就上了车。那个跟我讲价的师傅就先走了，在我的师傅跟在后面。坐摩的感觉真的是很危险，穿梭在各种车与车之间，好几次我都害怕说这他妈撞了怎么办。庆幸自己戴头盔是一个非常正确的选择。路上还看到了一个 bmw 的摩托出了车祸，被一辆小轿车撞了，还好人都没事，带了头盔。&lt;/p&gt;
&lt;p&gt;到了一个红灯前，跟我讲价的师傅和在我的师傅在那用泰语说什么，看起来像指路的样子。&lt;/p&gt;
&lt;p&gt;然后讲价的师傅跟我说， I am teacher , I teach him. 才知道，原来他们是兄弟，弟弟第一次出来开摩的，我坐的就是弟弟骑的，哥哥跟我说我坐的这个是刚新买的摩托。我一看，还真是，本田的踏板摩托。&lt;/p&gt;
&lt;p&gt;曼谷这边基本都是本田丰田，无论轿车或者摩托，目测百分之八十以上都是这两个日本品牌。
红灯完了，一群摩的 嗡 的就都冲出去了。那个新司机师傅一直在跟我说什么，然而我一句没听懂，都不知道他说的是泰语还是英语。
&lt;img src=&quot;/imgs/thailand-travel-notes/30.jpg&quot; alt=&quot;车流中的摩的-曼谷&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;射击&lt;/h2&gt;
&lt;p&gt;第四天去附近陆军练习场玩了下射击，真的很爽，那手枪后坐力，bong~ bong~ bong~ 的。玩完那个军官跟我说想不想玩长枪，当然想啊，毕竟淘宝上只有手枪的套餐。不过，50 发 M4A1 3200🐖，相当于一发子弹十几块人民币，还是挺贵的，不过想想来都来了，玩！
遂打算刷卡，结果人家只收现金，而我只有可怜的 500🐖，无奈就只能回去了。
&lt;img src=&quot;/imgs/thailand-travel-notes/25.jpg&quot; alt=&quot;射击场-曼谷&quot; /&gt;
&lt;img src=&quot;/imgs/thailand-travel-notes/26.jpg&quot; alt=&quot;帅哥-曼谷&quot; /&gt;&lt;/p&gt;
&lt;p&gt;谁知出门走出去 200 米就下起了暴雨，我竟然没带伞，尴尬。在路边蹲了一个小时等雨停，雨停了赶紧跑了回去，果然以后出门得看天气预报啊。
&lt;img src=&quot;/imgs/thailand-travel-notes/27.jpg&quot; alt=&quot;暴雨-曼谷&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;交通&lt;/h2&gt;
&lt;p&gt;说说曼谷的交通吧，这边摩的很多，也很方便，因为曼谷的交通非常堵，如果打的的话经常被堵在路上好久，但是座摩的就不一样，很多路都可以走。
&lt;img src=&quot;/imgs/thailand-travel-notes/28.jpg&quot; alt=&quot;摩托-曼谷&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不过女生一个人玩的话，不是很推荐座摩的，不怎么安全。如果一定要做摩的记得认准这个小马甲，这种是正规的，每个人身后都写着名字和代号，晚上座摩的的话，可以拍照发家人以免万一。
上车一定要带头盔，因为路上看到了有人出了交通事故，要是不带头盔还是很危险的。价格方面，摩的跟打的价格差不多，上车前一定要问价格，一定要砍价,价格水分还是很大的。
&lt;img src=&quot;/imgs/thailand-travel-notes/29.png&quot; alt=&quot;摩的-曼谷&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;最后&lt;/h1&gt;
&lt;p&gt;第五天早上就用 Grab 打了个车去机场回国了。&lt;/p&gt;
&lt;p&gt;整体下来 9 天 8 晚的样子，花了大概 1.2W. 主要都花在酒店和机票上了，因为一个人出来玩，所以酒店都定的比较好，毕竟安全第一。
整体上玩的还是很开心的，人都很亲切，毕竟微笑国度。而且也挺便宜，在那边花钱我也没有什么概念，基本就是大手大脚的花了，比国内便宜多了。&lt;/p&gt;
&lt;p&gt;回国就要继续搬砖了，哈哈。&lt;/p&gt;
</content:encoded></item><item><title>财务不自由之路 ———— 美股期权简介（一）</title><link>https://ab.kisnows.com/zh-CN/2020/11/11/stock-options/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2020/11/11/stock-options/</guid><pubDate>Wed, 11 Nov 2020 04:16:00 GMT</pubDate><content:encoded>&lt;h2&gt;期权的概念&lt;/h2&gt;
&lt;p&gt;美股顾名思义就是美国交易所进行交易的股票, 那么期权（Option）是什么？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;期权（Option）是一种权力，持有期权的人可以在未来的某个时间，以期权约定的价格，来买入或者卖出相应的股票。
这种权力在美股市场上是可以交易的，交易该权利的价格被称作权利金（premium）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;期权的操作&lt;/h2&gt;
&lt;p&gt;期权的最小交易单位为手，一手是 100 张，对应 100 股相应正股的交易权力。期权分为看涨（Call）和看跌（Put）的期权。&lt;/p&gt;
&lt;p&gt;在美股市场上，整个交易机制是完全公平的。你可以买入看涨期权，也可以卖出看涨期权，对应到看跌期权也一样，也就是说对于期权总共有四种操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;买入看涨期权（Long Call&lt;/li&gt;
&lt;li&gt;卖出看涨期权（Short Call&lt;/li&gt;
&lt;li&gt;买入看跌期权（Long Put&lt;/li&gt;
&lt;li&gt;卖出看跌期权（Short Put&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时期权根据约定的价格和目前的股价对比会有价内和价外两种概念，价内是指当前期权具有内在价值，即行权后获利的情况，相对应的价外就是没有价值的期权，废纸一张，对于价外期权一般持有人不会行权。&lt;/p&gt;
&lt;p&gt;接下来来结合价内价外来介绍期权的四种情况：&lt;/p&gt;
&lt;h3&gt;买入看涨期权（Long Call&lt;/h3&gt;
&lt;p&gt;买入看涨期权，意味着你拥有了在未来某个时间点以期权约定价格买入股票的权力。
现在时间是 2020.11.10 ，微软一股 &lt;code&gt;200$&lt;/code&gt; ，你以 &lt;code&gt;20$&lt;/code&gt; 的价格买入一张 2020.12.10 到期的微软 &lt;code&gt;220$&lt;/code&gt; 的期权，花费为 &lt;code&gt;2000$&lt;/code&gt;。到了 12.10 这天，会有两种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;价内
微软涨到了 &lt;code&gt;300$&lt;/code&gt; ，高于期权约定的 &lt;code&gt;220&lt;/code&gt;，属于价内期权，你会选择行权，因为有利可图。
行权就是以期权约定的 &lt;code&gt;220$&lt;/code&gt; 的价格买入 100 股微软的股票，然后转手就可以以现在的市价 300$ 卖掉。&lt;/p&gt;
&lt;p&gt;你的收益为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;（股票当前价 - 行权价） * 股数 - 期权权利金价格 * 张数 * 100 ：
（300 - 220）* 100 - 20 * 100 = 6000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是收入为 &lt;code&gt;6000$&lt;/code&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;价外&lt;/p&gt;
&lt;p&gt;微软跌到了 &lt;code&gt;180$&lt;/code&gt;, 低于期权约定的 &lt;code&gt;220$&lt;/code&gt; ，此时期权已经没有获利空间了，属于价外期权。
如果你要行权的话，意味着你要以期权约定的 &lt;code&gt;220$&lt;/code&gt; 的价格买入 100 股微软的股票。然而现在市价才 &lt;code&gt;180$&lt;/code&gt;, 买入就是亏，所以此时期权持有人是不会行权的。不行权的话，如果过了期权到期时间，那么你损失的就是 &lt;code&gt;20$&lt;/code&gt; 的期权权利金。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;期权交易&lt;/h4&gt;
&lt;p&gt;当然期权不是说一定要行权才可以获利，期权本身也是可以交易的。&lt;/p&gt;
&lt;p&gt;同样拿上面的例子来说，当微软涨到了 &lt;code&gt;300$&lt;/code&gt; , 如果行权的话，是要买入 100 股微软的股票然后卖出才能获利 &lt;code&gt;6000$&lt;/code&gt; ，你需要用 &lt;code&gt;220 * 100 = 22000$&lt;/code&gt; 的资金全部买入微软，但如果没有这么多钱呢？&lt;/p&gt;
&lt;p&gt;可以选择直接交易期权，当微软一股 &lt;code&gt;200$&lt;/code&gt; 的时候，2020.12.10 到期的 &lt;code&gt;220$&lt;/code&gt; 的看涨期权卖 &lt;code&gt;20$&lt;/code&gt; ，那么当微软涨到 &lt;code&gt;300$&lt;/code&gt; 的时候，这张期权本身也会涨到至少 &lt;code&gt;300 - 220 = 80$&lt;/code&gt; （期权的价格受到很多因素影响，主要因素为当前的股价和到期时间）。此时你选择不行权，直接卖掉这张期权，那么你的收益为：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(期权当前价 - 期权买入价) * 100 :
(80 - 20) * 100 = 6000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，你的收益依然为 &lt;code&gt;6000$&lt;/code&gt; ，而你的资金成本为: &lt;code&gt;20 * 100 = 2000$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;和行权相比，你的收益都是 &lt;code&gt;6000$&lt;/code&gt; , 但是行权需要的资金成本为 &lt;code&gt;22000$&lt;/code&gt; , 而不行权直接卖出期权的资金成本仅为 &lt;code&gt;2000$&lt;/code&gt; ,基本上是 10 倍的杠杆，因此很多人会选择用期权来投机。&lt;/p&gt;
&lt;h4&gt;收益曲线&lt;/h4&gt;
&lt;p&gt;收益曲线可以直观的了解期权和当前股价的价值关系：
&lt;img src=&quot;/imgs/stock-options/LongCall.png&quot; alt=&quot;LongCall&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最大亏损为期权价格&lt;/li&gt;
&lt;li&gt;最大收益随着股价上涨而上涨，无上限&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;卖出看涨期权（Short Call&lt;/h3&gt;
&lt;p&gt;就是说我卖出了一张看涨期权，期权买入人会在过期时间内的某一时间点以行权价从我这里买入股票。一般情况下，当你认为这只股票涨不到
某个价格的时候，就会选择卖出一张这个价位的看涨期权。&lt;/p&gt;
&lt;h4&gt;收益曲线&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/stock-options/ShortCall.png&quot; alt=&quot;ShortCall.png&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最大亏损无限大&lt;/li&gt;
&lt;li&gt;最大收益为期权的权利金&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;买入看跌期权（Long Put&lt;/h3&gt;
&lt;p&gt;买入看跌期权，就是说在过期时间内，你可以以行权价卖出约定数量的股票。&lt;/p&gt;
&lt;h4&gt;收益曲线&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/stock-options/LongPut.png&quot; alt=&quot;LongPut.png&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最大亏损为期权的权利金&lt;/li&gt;
&lt;li&gt;最大收益随着股价的下跌而上升，当股价跌为零时收益最大&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;卖出看跌期权（Short Put&lt;/h3&gt;
&lt;p&gt;需要以行权价买入约定数量的股票。&lt;/p&gt;
&lt;p&gt;其中卖出看涨期权和卖出看跌期权都属于做空行为，意味着在期权过期时间之前，如果你没有平仓，那么你需要承担行使期权的义务：卖出看涨和看跌分别需要
以行权价卖出相应的股票和买入相应的股票，都是需要有保证金在账户上，属于高风险操作，因为亏损有可能无限大。&lt;/p&gt;
&lt;h4&gt;收益曲线&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/stock-options/ShortPut.png&quot; alt=&quot;ShortPut.png&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;随着股价的下跌而下跌，当股价跌为零时亏损最大化&lt;/li&gt;
&lt;li&gt;最大收益为期权的权利金&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;影响期权的价格因素&lt;/h2&gt;
&lt;p&gt;影响期权价格的关键因素为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;行权价，期权的持有人和卖出人根据这个价格进行股票的交割&lt;/li&gt;
&lt;li&gt;过期时间，即期权的持有方如果在过期时间后没有行使权力，那么这张期权就过期了，在过期时间内期权持有放可以随时选择行权&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;过期时间，行权价以及当前正股的价格，三者决定了期权的价格。&lt;/p&gt;
&lt;p&gt;比如，微软现价 &lt;code&gt;200$&lt;/code&gt; 一股，那么一张看涨的行权价为 &lt;code&gt;180$&lt;/code&gt; 的期权价格，都是高于 &lt;code&gt;200-180=20$&lt;/code&gt; 块钱的。
同时根据过期时间的长短，价格也会有不同，一般情况下过期时间越长，价格越高。&lt;/p&gt;
&lt;h2&gt;期权的作用&lt;/h2&gt;
&lt;p&gt;期权的用法很多，如果你不是投机的话，一般都会和正股进行配合。&lt;/p&gt;
&lt;h3&gt;一个例子&lt;/h3&gt;
&lt;p&gt;比如我持有微软 100 股，成本是 &lt;code&gt;200$&lt;/code&gt; ，我半年的目标收益率为 10% 。即半年内微软从 &lt;code&gt;200$&lt;/code&gt; 涨到 &lt;code&gt;220$&lt;/code&gt; 就达到了我的目标，我该卖出了，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接卖出正股，获得 &lt;code&gt;220*100&lt;/code&gt; ，收益为 &lt;code&gt;(220*100 - 200*100) = 2000$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;卖出一张微软半年后到期行权价为 &lt;code&gt;220$&lt;/code&gt; 的看涨期权，权利金为 &lt;code&gt;25$&lt;/code&gt; ，那么同样股价上涨到 &lt;code&gt;220$&lt;/code&gt;。期权持有者行权，我的收益为直接卖出正股的收益加上权利金 &lt;code&gt;25$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果半年内股价没有涨到 &lt;code&gt;220$&lt;/code&gt; ，那么我不会卖出正股。相较于没有卖出期权的情况，可以多获得 &lt;code&gt;25$&lt;/code&gt; 的期权权利金。&lt;/p&gt;
&lt;p&gt;这只是一个使用期权的简单例子，还有其他很多用来应对一些复杂场景的组合策略可以探索。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;期权属于股票的衍生品，可以双向双边操作，即前文提到期权的四种操作。一般是用来和正股配合做对冲来降低风险的。&lt;/p&gt;
&lt;p&gt;但由于期权本身的杠杆属性，能够放大收益和亏损，所以经常会用人通过期权交易来投机。
而我就是投机中的一员：
&lt;img src=&quot;/imgs/stock-options/intel-option.png&quot; alt=&quot;intel-option.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后如果你对美股感兴趣，可以用我的邀请注册长桥证券来进行美股交易。长桥证券是一家专业的美股券商，提供美股交易服务，注册链接：&lt;a href=&quot;https://activity.lbkrs.com/pipeline/2023welcomegift/index.html?account_channel=lb&amp;amp;app_id=longbridge&amp;amp;org_id=1&amp;amp;businessType=activity&amp;amp;lbFitsStatusBar=0&amp;amp;lang=zh-CN&amp;amp;channel=HM2023002&amp;amp;code=YQ1709210200&amp;amp;refer-uri=https%253A%252F%252Factivity.lbkrs.com%252Fpipeline%252F2023welcomegift%252Findex.html%253Faccount_channel%253Dlb%2526app_id%253Dlongbridge%2526org_id%253D1%2526businessType%253Dactivity%2526lbFitsStatusBar%253D1%2526channel%253DHM2023001%2526lang%253Dzh-HK&amp;amp;sac=lb&amp;amp;redirect_url=https%3A%2F%2Factivity.lbkrs.com%2Fpipeline%2F2023welcomegift%2Findex.html%3Faccount_channel%3Dlb%26app_id%3Dlongbridge%26org_id%3D1%26channel%3DHM2023002%26code%3D%26refer-uri%3Dhttps%25253A%25252F%25252Factivity.lbkrs.com%25252Fpipeline%25252F2023welcomegift%25252Findex.html%25253Faccount_channel%25253Dlb%252526app_id%25253Dlongbridge%252526org_id%25253D1%252526businessType%25253Dactivity%252526lbFitsStatusBar%25253D1%252526channel%25253DHM2023001%252526lang%25253Dzh-HK%26sac%3Dlb%26businessType%3Dactivity%26lbFitsStatusBar%3D0%26lang%3Dzh-CN&amp;amp;invite-code=265638&quot;&gt;长桥证券&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>阿里工作三年总结</title><link>https://ab.kisnows.com/zh-CN/2021/06/04/three-years-summary-for-alibaba-work-experience/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2021/06/04/three-years-summary-for-alibaba-work-experience/</guid><pubDate>Fri, 04 Jun 2021 16:55:54 GMT</pubDate><content:encoded>&lt;p&gt;今天是阿里入职三周年，借机回顾一下这三年的工作。
回想入职之前，内推的朋友跟说我过来可能负责日本站的开发，当时还挺激动的，国际化业务啊， international 。
结果我入职的前几天组织架构发生了巨大的变化，带我的师兄也换了，负责的业务域也换了。除了刚入职做了一段时间 detail ，后面主要就是做国际站店铺了。&lt;/p&gt;
&lt;h2&gt;业务开发&lt;/h2&gt;
&lt;p&gt;国际店铺这个业务虽然已经很稳定了，但还是有很多可以改进和优化的地方。三年来大的项目主要就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;千人千面&lt;/li&gt;
&lt;li&gt;CP 结构化展示&lt;/li&gt;
&lt;li&gt;双赛道&lt;/li&gt;
&lt;li&gt;管理后台升级&lt;/li&gt;
&lt;li&gt;无线店铺 2.0&lt;/li&gt;
&lt;li&gt;内容化店铺&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也算是为跨境国际贸易做出了自己的贡献。&lt;/p&gt;
&lt;h2&gt;效率&amp;amp;架构升级&lt;/h2&gt;
&lt;h3&gt;开发提效&lt;/h3&gt;
&lt;p&gt;一开始负责 detail 的开发，接到的第一个需求是做一个什么物流相关的。我一看很简单自评了 2 天，师兄跟我说评少了，一个星期吧。我当时内心 OS：这么简单的功能需要一周？&lt;/p&gt;
&lt;p&gt;结果真正着手做的时候发现，我靠一周都悬。当时的 detail 还是一个完全依赖后端 vm 渲染的古典 web 业务，前后端完全不分离。几乎任何功能都需要前后端一起改，并且改完部署到预发才能调试，而一次部署需要将近 20 分钟到半个小时。可以想想这样的开发效率，那是相当的低下，最终导致我入职第一周就体验了通宵工作，干的我差点想当场离职。都 2018 年，还是这么落后的技术，难以想象。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;但还是忍住了，存在问题就是我大展身手的时候。于是开始动手进行效率提升的改进，做了一个本地调试工具。支持 vm 本地调试以及同步和异步的数据 mock 功能。把调试成本从之前的每次代码修改 20 分钟看效果缩短到了 3s 直接本地预览。&lt;/p&gt;
&lt;p&gt;后面接手店铺，第一个需求就是做国际化改造，需要改到几十个模块。改一个模块需要做的事情：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;拉取模块代码
  -&amp;gt; 安装相关依赖
    -&amp;gt; 修改多语言文案
      -&amp;gt; 构建代码
        -&amp;gt; 提交到预发环境
          -&amp;gt; 预发环境测试
            -&amp;gt; 发布代码到线上
              -&amp;gt; 走内部发布平台发布审批
                -&amp;gt; 发布代码上线
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;流程繁琐无比，发一个模块都得大半天，要是我改几十个那不得上天了。于是写了个一个批量操作脚本，将流程中除了预发测试这一环必须人工介入的，其他的基本都自动化掉了。最后整个需求差不多四天就全搞定了。&lt;/p&gt;
&lt;h3&gt;无线店铺技术体系升级&lt;/h3&gt;
&lt;p&gt;再后来接手无线店铺，据说这个业务在我之前已经干走了两个人了。接手后发现我靠，确实是深坑，项目是基于 weex 开发，然后整体架构上也非常神奇，常人难以理解。无论开发、调试到发布的操作都神奇的不行，难怪之前的人搞不定。&lt;/p&gt;
&lt;p&gt;接手后一边慢慢的承接业务，一边开始熟悉整个无线店铺的代码。同时也在调研用 H5 重构整个无线店铺的可行性。经过长久的调研以及一次线上实验，加上一年的努力，最终把整个 weex 店铺用 H5 技术重构掉了。&lt;/p&gt;
&lt;p&gt;不但开发效率大幅提升，而且业务数据上也有不错的效果。同时作为我们 ICBU 第一个在端上用 H5 承接核心业务的场景，落地过程中跨大组和端上的同学打通了 App 上 H5 开发的整个链路，包括 zCache、JSbridge、打点统计、沉浸式以及其他端上需要的能力基本都支撑了 H5 场景。最重要的是证明了端上偏展示型页面用 H5 来承接是完全可行的，大家担忧的性能和体验的问题都是可以被解决的。&lt;/p&gt;
&lt;p&gt;由于有了我们场景的成功经验，后续其他业务团队也都开始从 weex 向 H5 进行迁移。&lt;/p&gt;
&lt;h3&gt;性能优化&lt;/h3&gt;
&lt;p&gt;另一件投入了很大精力去做的就是性能优化了，当时整个店铺的海外访问性能都不够如人意。经过一年多的优化改进，也把 PC 和无线的店铺都从原来 **s+ 优化到了现在海外均值 1.5s 内，还是很有挑战的。具体优化策略可以参考 {% post_link shop-performance-improve 店铺体验优化总结 %}。&lt;/p&gt;
&lt;p&gt;整个性能优化推进的过程中，需要频繁的和外部门已经其他不同角色进行协作，沟通协调能力获得了不错的成长，同时也对我们整个业务系统的全链路有了更深的理解。&lt;/p&gt;
&lt;h2&gt;阿里的同于不同&lt;/h2&gt;
&lt;h3&gt;同&lt;/h3&gt;
&lt;p&gt;在阿里工作和其他公司工作的相同点，就是作为一个普通技术开发，你需要接需求、写代码、测试，发布等一个技术人需要做的事情。&lt;/p&gt;
&lt;h3&gt;不同&lt;/h3&gt;
&lt;p&gt;工作的不同点还是要更多一点。由于阿里有 361 制度，也就是强制 10% 的人要拿最差的绩效 3.25 ，拿了 3.25 意味着没有晋升、没有涨薪也没有年终奖，而且连续两次会面临劝退的风险。&lt;/p&gt;
&lt;p&gt;而考核本身也不是说我一个技术就做技术需要的做的事情就好了。公司要求技术人员要有思考，有业务 sense ，要有技术影响力，要技术引领业务，要有产出等等这一些在其他公司一般不会做强要求的点。至于你说代码写的好不好看，提测质量高不高设计巧妙不巧妙，是不是我这些地方做的好也能拿一个不错的绩效，就切身感受来说，不行。就这个事情我也和主管沟通过，主管当时的回答：这是对你的基本要求。当然，可能不同部门的要求不一样。&lt;/p&gt;
&lt;p&gt;这就导致在阿里工作你压力很大，很心累。几乎从入职第一天开始，你就要考虑你这试用期三个月能做出什么产出，以供转正答辩。转正完了，要考虑出这个财年的规划，也就是说这一年我能做出什么产出来，当然一般的接业务需求是几乎不算你的产出的，那是基本要求。&lt;/p&gt;
&lt;p&gt;所以你就看到很多人一到四月份就开始发愁，今年要做什么啊，做这个有没有产出，这一块我是不是主要负责人啊等等。有了方向以后，白天和开会评审各种产品需求，晚上写代码，然后还要想着自己今年的目标有没有进展。&lt;/p&gt;
&lt;p&gt;整一个心累。&lt;/p&gt;
&lt;h2&gt;个人感受&lt;/h2&gt;
&lt;p&gt;在阿里呆了三年，很累，但确实有成长。&lt;/p&gt;
&lt;p&gt;累是真的累，身心俱疲，虽然我们部门已经算很不卷的了，但大部分下班时间还是在 9 点多，而且背着绩效压力每天都要想着怎么搞 kpi 。在网易的时候下班回家后，会看看书、逛逛 GitHub、写写 DEMO 或者打打游戏刷刷论坛吹吹水，happy 的不得了。到了阿里，下班以后几乎就只能打游戏，其他的只要是需要动脑子的事情就都不想干，稍微需要懂点脑子的游戏都不想玩，而我以前是很喜欢类似群星、文明以及各种策略类的游戏的。现在回家真的就是纯休息，除了周末基本没有个人生活而言。&lt;/p&gt;
&lt;p&gt;成长吧，确实是成长了不少。综合职业能力比如沟通协调规划等、以及技术视野和深度上都有比较大的提升。在公司的发展也和自我预期的差不多，入职第二年提名晋升到了 P7，还是比较满意的。&lt;/p&gt;
&lt;p&gt;但一切都是有代价的，伴随着快速的成长，身体机能大不如前。入职不到两个月，体重就涨了将近 20 斤，我称之为过劳肥。而且最近明显感觉记性越来越差了，历年体检上的异常项也是一年比一年多了。这个状态发展下去肯定是不行的，所以最近在调整整个人的工作状态，希望尽可能通过自己的努力平衡工作和生活。&lt;/p&gt;
&lt;p&gt;最后，祝自己三周年快乐。&lt;/p&gt;
</content:encoded></item><item><title>Rax 代码转换到 React</title><link>https://ab.kisnows.com/zh-CN/2019/08/01/transform-rax-to-react/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2019/08/01/transform-rax-to-react/</guid><pubDate>Thu, 01 Aug 2019 20:15:38 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;最近接手公司的一个移动端项目，是通过 &lt;a href=&quot;http://rax.alibaba-inc.com/&quot;&gt;Rax&lt;/a&gt; 作为 dsl 开发的，在发布的时候构建多分代码，在 APP 端编译为能够运行在 weex 上的代码，在 H5（跑在浏览器或者 webview 里面，不管什么技术我们统称 H5） 端使用降级的 weex&lt;/p&gt;
&lt;p&gt;这一套开发体系，看起来很完美，一次开发，三端运行。但是真实在开发的时候，就不是这么完美了。由于毕竟是跑在 weex 上的，而不是浏览器。所以在开发方式上也很难直接从 web 端的开发方式平移过去，为了实现跨端运行，所以在样式上只实现了 Css 的子集， DOM API 也是如此，开发的时候，在浏览器里面调试的时候，一切正常，但是等发布到 APP 端用 weex 跑的时候又是各种问题，开发体验很不流畅。&lt;/p&gt;
&lt;p&gt;当然，有人会说那是因为你对 weex 的 api 不了解，也对，&lt;s&gt;一直以来对与这种自己搞一套非标准体系来实现各种魔法功能的东西都不怎么感兴趣&lt;/s&gt; 但是如果了解它的成本大于了它带来的收益，那么对我们来说，就没有必要做这件事情。&lt;/p&gt;
&lt;p&gt;weex 相对于 H5 ，最大的优点在于交互性能上要更好一点。&lt;/p&gt;
&lt;p&gt;而随着手机性能的提升，以及 webview 的不断优化，H5 的页面也越来越流畅了，尤其是纯展示形页面上。而且相较于 H5 ，weex 天生不具备 seo 能力，同时存在分享传播困难的缺点，这样看来，使用 weex 的理由就更少了。而且我们在一个新业务上使用 H5 开发了一个页面，借助同构以及提前缓存的能力，已经把首屏打开速度做到了全球秒开，而且业务数据也达到预期，所以我们打算把现有的存量业务都迁移到 H5 上。&lt;/p&gt;
&lt;p&gt;这就是我为什么要把基于 Rax 开发的模块代码转换为 React 代码，也就有了本篇文章。
本文针对的 rax 版本是 0.6.8 ，1.x 的版本改动很大，不再本文讨论范围内。&lt;/p&gt;
&lt;h2&gt;期望的目标&lt;/h2&gt;
&lt;p&gt;对于一个 rax 模块，我们期望通过编译后：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;能够在 react 下运行&lt;/li&gt;
&lt;li&gt;尽可能提取样式到 css 文件里，不使用内联&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;不同之处&lt;/h2&gt;
&lt;p&gt;Rax 在开发之处，就是为了能够使用 React 的语法来开发 weex ，所以一开始在语法上和 React 几乎一致。后续随着 rax 的不断迭代，渐渐和 react 有了一些不一样的地方，但是差距不大。
我们对比一下同一个模块在 rax 和 react 的实现代码：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;rax module&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Component, createElement, findDOMNode } from &quot;rax&quot;;
import Text from &quot;rax-text&quot;;
import View from &quot;rax-view&quot;;

import styles from &quot;./index.css&quot;;

class Kisnows extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1,
    };
  }

  handleClick = () =&amp;gt; {
    this.setState({
      count: this.state.count + 1,
    });
  };

  render() {
    const { count } = this.state;
    const { name } = this.props;
    return (
      &amp;lt;View style={styles.wrap}&amp;gt;
        &amp;lt;Text style={[styles.name, { color: &quot;red&quot; }]}&amp;gt;{name}&amp;lt;/Text&amp;gt;
        &amp;lt;View onClick={this.handleClick}&amp;gt;
          怕什么真理无穷，进一步有进一步的好。
        &amp;lt;/View&amp;gt;
        &amp;lt;View&amp;gt;点击进步：{count}&amp;lt;/View&amp;gt;
      &amp;lt;/View&amp;gt;
    );
  }
}

export default Kisnows;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;react module&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Component } from &quot;react&quot;;
import { findDOMNode } from &quot;react-dom&quot;;

import styles from &quot;./index.css&quot;;

class Kisnows extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1,
    };
  }

  handleClick = () =&amp;gt; {
    this.setState({
      count: this.state.count + 1,
    });
  };

  render() {
    const { count } = this.state;
    const { name } = this.props;
    return (
      &amp;lt;div className=&quot;wrap&quot;&amp;gt;
        &amp;lt;h1 className=&quot;name&quot; style={{ color: red }}&amp;gt;
          {name}
        &amp;lt;/h1&amp;gt;
        &amp;lt;div onClick={this.handleClick}&amp;gt;
          怕什么真理无穷，进一步有进一步的好。
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;点击进步：{count}&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
}

export default Kisnows;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到他们的区别如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;引用的框架不同&lt;/p&gt;
&lt;p&gt;这个当然是废话，
rax 模块从 &lt;code&gt;rax&lt;/code&gt; 上引入 &lt;code&gt;Component&lt;/code&gt;, &lt;code&gt;createElement&lt;/code&gt; 等组件的， react 模块从 &lt;code&gt;react&lt;/code&gt; 上引入。&lt;/p&gt;
&lt;p&gt;还有一点的不同的地方，就是 &lt;code&gt;findDOMNode&lt;/code&gt; ， &lt;code&gt;rax&lt;/code&gt; 上是直接挂载在 &lt;code&gt;rax&lt;/code&gt; 模块上的，而 &lt;code&gt;findDOMNode&lt;/code&gt; 从 &lt;code&gt;react-dom&lt;/code&gt; 上获取。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用的基础元件不同&lt;/p&gt;
&lt;p&gt;rax 本身是为了使用 react 语法去写 weex 的而诞生的，weex 为了兼容 ios 和 android ，所以在两个系统中抽了一些组件出来，在上面做了一层适配。&lt;/p&gt;
&lt;p&gt;而 rax 为了跨 Web 和 Native 在 weex 之上又包了一层，于是就有了 &lt;code&gt;Text&lt;/code&gt;, &lt;code&gt;View&lt;/code&gt;, &lt;code&gt;Image&lt;/code&gt; 等基础元件，而无法使用普通 Web 开发使用的 &lt;code&gt;span&lt;/code&gt;, &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;img&lt;/code&gt; 等 html 标签。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;样式使用的不同&lt;/p&gt;
&lt;p&gt;rax 下所有的样式文件都是 css in js ，即使后面通过构建等支持了引用外部 css 文件的样式，但依然还是 css in js ，所有的样式都是内联的。&lt;/p&gt;
&lt;p&gt;同时从上面的代码可以看到， rax 不知道从哪个版本开始支持了 &lt;code&gt;style&lt;/code&gt; 属性传入 &lt;code&gt;Array&lt;/code&gt; 类型。这一块和 react 下不一致，react 只支持 &lt;code&gt;Object&lt;/code&gt; 类型。&lt;/p&gt;
&lt;p&gt;而一旦传入的是个 Array ，js 又是动态语言，所以无法在编译时判断里面元素的类型，就不好判断里面的元素哪些是要转换为 class ，哪些是直接内联的样式了。我们转换上很多的精力也是花在了这里。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;如何转换&lt;/h2&gt;
&lt;p&gt;知道了二者的不同，那么如何转换我们也就有方向了。&lt;/p&gt;
&lt;p&gt;对代码进行转换，现在最常见的方法就是基于 babel 的插件来进行，首先把代码转换成 ast 语法树，然后在此基础上对 ast 进行转换，最后把 ast 转换成我们需要的代码。&lt;/p&gt;
&lt;p&gt;通过开发 babel 插件来转换代码，在现在的前端开发中很常见，我也不做过多介绍。但之前我也仅限于使用上， 没有自己开发过，这次也是现学现卖。&lt;/p&gt;
&lt;p&gt;介绍几个学习中参考的文档和工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jamiebuilds/babel-handbook&quot;&gt;babel-handbook&lt;/a&gt;
几乎涵盖开发 babel 插件需要的一切。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://astexplorer.net/&quot;&gt;AST Explorer&lt;/a&gt;
一个在线进行语法解析的网站，可以实时验证你的想法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于 babel-handbook 已经对如何开发 babel 插件讲解的很详细了，对一些基本概念本文就不再赘述，默认往下看的读者都已经有了这些知识基础。&lt;/p&gt;
&lt;h3&gt;引用的框架不同&lt;/h3&gt;
&lt;p&gt;这个很好解决，我们需要的就是把:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Component, createElement, findDOMNode, PureComponent } from &quot;rax&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的代码转换成:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Component, createElement, PureComponent } from &quot;react&quot;;
import { findDOMNode } from &quot;react-dom&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来很简单是吧，我们打开 &lt;a href=&quot;https://astexplorer.net/&quot;&gt;AST Explorer&lt;/a&gt; ，在这上面输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Component, createElement, findDOMNode, PureComponent } from &quot;rax&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会得到如下结果：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- @import &quot;./transform-rax-to-react/import.png&quot; --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/transform-rax-to-react/import.png&quot; alt=&quot;import&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中左侧的 import 语句转换成 ast 后就是图中右侧红框中内容。
整个 import 语句是一个 ImportDeclaration , 引入的每个方法 Component、 createElement 等对应一个 ImportSpecifier 节点。
我们要做的就是把 ImportDeclaration.source.value 改为 react, 然后把 findDOMNode 从 rax 中抽出来，改为从 react-dom 里面引入就好了。&lt;/p&gt;
&lt;p&gt;具体到操作上，就是我们在 visitor 里面添加对 ImportDeclaration 类型节点的遍历：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;visitor: {
  ImportDeclaration(path) {
    // 对于不符合我们要修改条件的节点，直接 return ，节省无用递归调用的时间
    if (
      path.node.source.value !== &apos;rax&apos; ||
      path.node.source.type !== &apos;StringLiteral&apos;
    ) {
      return;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后区分哪些模块是要从 react 里面引用的，哪些是需要从 react-dom 里面引用的，再对 ImportSpecifier 节点进行遍历，找到里面符合我们符合条件的模块，方便后面生成新的 import 语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; visitor: {
  ImportDeclaration(path) {
    if (
      path.node.source.value !== &apos;rax&apos; ||
      path.node.source.type !== &apos;StringLiteral&apos;
    ) {
      return;
    }
    const REACT_METHODS = [
      &apos;createElement&apos;,
      &apos;Component&apos;,
      &apos;PureComponent&apos;,
      &apos;PropTypes&apos;
    ];
    const REACT_DOM_METHODS = [&apos;findDOMNode&apos;];
    const reactMethods = new Set();
    const reactDOMMethods = new Set();
    path.traverse({
      ImportSpecifier(importSpecifierPath) {
        importSpecifierPath.traverse({
          Identifier(identifierPath) {
            const methodName = identifierPath.node.name;
            // console.log(&apos;importSpecifierPath:Identifier:methodName&apos;, methodName)
            if (REACT_DOM_METHODS.includes(methodName)) {
              reactDOMMethods.add(methodName);
            } else if (REACT_METHODS.includes(methodName)) {
              reactMethods.add(methodName);
            } else {
              reactMethods.add(methodName);
              console.warn(
                `当前方法 ${methodName} 没有进行配置，直接从React上获取，如有问题请检查此方法。`
              );
            }
          }
        });
      }
    });
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后一步，用前面找到的 react 和 react-dom 的模块，借助 bable 提供 template 来重新生成 import 语句，并删除原有对 rax 的引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;visitor: {
  ImportDeclaration(path) {
    if (
      path.node.source.value !== &apos;rax&apos; ||
      path.node.source.type !== &apos;StringLiteral&apos;
    ) {
      return;
    }
    const REACT_METHODS = [
      &apos;createElement&apos;,
      &apos;Component&apos;,
      &apos;PureComponent&apos;,
      &apos;PropTypes&apos;
    ];
    const REACT_DOM_METHODS = [&apos;findDOMNode&apos;];
    const reactMethods = new Set();
    const reactDOMMethods = new Set();
    path.traverse({
      ImportSpecifier(importSpecifierPath) {
        importSpecifierPath.traverse({
          Identifier(identifierPath) {
            const methodName = identifierPath.node.name;
            // console.log(&apos;importSpecifierPath:Identifier:methodName&apos;, methodName)
            if (REACT_DOM_METHODS.includes(methodName)) {
              reactDOMMethods.add(methodName);
            } else if (REACT_METHODS.includes(methodName)) {
              reactMethods.add(methodName);
            } else {
              reactMethods.add(methodName);
              console.warn(
                `当前方法 ${methodName} 没有进行配置，直接从React上获取，如有问题请检查此方法。`
              );
            }
          }
        });
      }
    });
    // 使用前面的 reactMethods 和 reactDOMMethods ，来生成新的 import 语句。
    const importReactTemplate = template.ast(`
          import {${Array.from(reactMethods).join(&apos;,&apos;)} } from &apos;react&apos;;
        `);
    const importReactDOMTemplate = template.ast(`
          import { ${Array.from(reactDOMMethods).join(
            &apos;,&apos;
          )}  } from &apos;react-dom&apos;;
        `);
    // 插入到当前 path 前面
    path.insertBefore(importReactTemplate);
    path.insertBefore(importReactDOMTemplate);
    // 删除当前 path ，也就是 rax 的 import 语句
    path.remove();
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们就完成了引入模块的转换，如图：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- @import &quot;./transform-rax-to-react/import-result.png&quot; --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/transform-rax-to-react/import-result.png&quot; alt=&quot;import&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;使用的基础元件不同&lt;/h3&gt;
&lt;p&gt;对于基础元件这一块，我们可以不做任何处理。因为 rax 下每个元件本身就是一个普通的 rax 组件，我们可以通过 babel 直接把他们当作其他 rax 组件转换为 react 的代码来使用。&lt;/p&gt;
&lt;p&gt;但是我们转换后的代码只需要在 Web 上跑，而如果你看过基础元件比如用 rax-text 举例的代码：&lt;a href=&quot;https://github.com/raxjs/rax-text/blob/master/src/index.tsx&quot;&gt;rax-text&lt;/a&gt; 。
里面有很多代码是仅在 weex 下运行的，我们根本不需要，所以可以通过一些粗暴的方法来精简这些元件。直接使用 Webpack 的 alias ，来把对 rax-text 等基础元件的引用直接指定到我们的自己的一个组件下，比如我们自己就把以下元件加入到了 webpack 的 alias 里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  resolve: {
    modules: [&apos;node_modules&apos;],
    extensions: [&apos;.json&apos;, &apos;.js&apos;, &apos;.jsx&apos;],
    alias: {
      rax: &apos;react&apos;,
      &apos;rax-image&apos;: require.resolve(&apos;./components/rax-image&apos;),
      &apos;rax-view&apos;: require.resolve(&apos;./components/rax-view&apos;),
      &apos;rax-scrollview&apos;: require.resolve(&apos;./components/scroll-view&apos;),
      &apos;@ali/ike-splayer&apos;: require.resolve(&apos;./components/ike-splayer&apos;),
      &apos;@ali/ike-image&apos;: require.resolve(&apos;./components/ike-image&apos;)
    },
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于命中了 alias 规则的元件，直接替换为我们自己精简的组件。删除里面的对 weex 的判断，以及仅仅会在 weex 下运行的代码。&lt;/p&gt;
&lt;p&gt;这一块很简单，没什么好说的。&lt;/p&gt;
&lt;h3&gt;样式使用的不同&lt;/h3&gt;
&lt;p&gt;这里是处理起来比较麻烦的地方，首先看看我们诉求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;样式提取&lt;/p&gt;
&lt;p&gt;外部引入的 css 文件，不再通过 css in js 的方式内联到每一个标签上，而是提取为常规 css 文件，并配合 class 来实现常规的样式布局方式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;style 支持 Array&lt;/p&gt;
&lt;p&gt;支持 rax 的特殊语法，即 style 可以传入一个 Array 类型&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;样式提取&lt;/h4&gt;
&lt;p&gt;&amp;lt;!-- 听闻集团内负责 rax 的 @亚城 同学也在做 css 提取的事情，就在钉钉上交流了一下，毕竟如果有成熟的方案我就不用重复开发了。但是交流以后，发现 @亚城 同学是想把在兼容 weex 的情况下去抽离，说不太好办，也没有现成解决方案，所以还是得自己搞一下。 --&amp;gt;&lt;/p&gt;
&lt;p&gt;我们看一下 rax 下引用和使用外部 css 的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Component, createElement, findDOMNode } from &quot;rax&quot;;
import Text from &quot;rax-text&quot;;
import View from &quot;rax-view&quot;;

// 引入样式文件
import styles from &quot;./index.css&quot;;

class Kisnows extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1,
    };
  }

  handleClick = () =&amp;gt; {
    this.setState({
      count: this.state.count + 1,
    });
  };

  render() {
    const { count } = this.state;
    const { name } = this.props;
    return (
      // 使用样式文件
      &amp;lt;View style={styles.wrap}&amp;gt;
        &amp;lt;Text style={[styles.name, { color: &quot;red&quot; }]}&amp;gt;{name}&amp;lt;/Text&amp;gt;
        &amp;lt;View onClick={this.handleClick}&amp;gt;
          怕什么真理无穷，进一步有进一步的好。
        &amp;lt;/View&amp;gt;
        &amp;lt;View&amp;gt;点击进步：{count}&amp;lt;/View&amp;gt;
      &amp;lt;/View&amp;gt;
    );
  }
}

export default Kisnows;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;View style={styles.wrap}&amp;gt;&lt;/code&gt; 这种使用方式和社区的 css modules 几乎一致，那我们就可以用 css module 来解决这件事情。&lt;/p&gt;
&lt;p&gt;思路是这样的，对于赋值到 style 属性上的外部 css 属性（即从 css 文件里面引入，而非直接 js 里面定义的样式），我们给它生成一个唯一字符串，然后组合起来放到 className 属性上。对于 js 内部定义的 css ，继续作为内联样式赋值到 style 属性上。&lt;/p&gt;
&lt;p&gt;在 webpack 的 css rule 里面引入 css-loader，然后开启 css module 功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    test: /\.(css)$/,
    use: [
      {
        loader: require.resolve(&apos;css-loader&apos;),
        options: {
          importLoaders: 2,
          modules: {
            mode: &apos;local&apos;,
            localIdentName: &apos;[path][name]__[local]--[hash:base64:5]&apos;,
          },
          // sourceMap: !!DEV,
        },
      }
    ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样配置了的话，通过 &lt;code&gt;import styles from &apos;./index.css&apos;&lt;/code&gt; 引入的 styles 就会被 css-loader 处理，并且里面对类似 styles.wrap 的引入，就会变成 &lt;code&gt;_3zyde4l1yATCOkgn-DBWEL&lt;/code&gt; 这样的计算出来的唯一 id 字符串，然后赋值到 class 属性上就好了。&lt;/p&gt;
&lt;p&gt;至于如何把这个 id 放到 className 上，我们放到后面一起说。
转换后的效果如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- @import &quot;./transform-rax-to-react/class-1.png&quot; --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/transform-rax-to-react/class-1.png&quot; alt=&quot;import&quot; /&gt;&lt;/p&gt;
&lt;p&gt;目前还面临两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;非标准 css 文件处理
rax 下使用 weex 为了做移动端适配，css 写起来和普通的 css 有一点区别，也就是 rax 的 css 属性都是没有单位的。而我们提取出来的 css 肯定是需要单位才行的；&lt;/li&gt;
&lt;li&gt;样式优先级问题
以前所有的样式都是内联的，每个样式的优先级也都是一样的，无论是直接写在 js 里面的还是从 css 文件里面引入的，最终会在 js 内部根据处理内联 styles 的 Object.assign 入參的顺序决定最终优先级。但是我们提取出来以后就不一样了，外链的 css 通过 class 作用与元素上，它的优先级是低于内联样式的，这样就会导致&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;非标准 css 文件处理&lt;/h5&gt;
&lt;p&gt;rax 下 css 文件样例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.wrap {
  width: 750;
}

.name {
  width: 750;
  height: 124;
  font-size: 24;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到宽高之类属性都是没有单位的，这里需要处理成对应的单位。处理起来也很简单，postcss 是一个用来对 css 做后处理的工具，我们经常使用的 autoprefix 就是 postcss 一个有名的插件。这里我们也借助它的插件能力来处理 css, 插件代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const postcss = require(&quot;postcss&quot;);
const _ = require(&quot;lodash&quot;);
// 定义所有需要添加单位的属性
const props = [
  &quot;width&quot;,
  &quot;height&quot;,
  &quot;padding&quot;,
  &quot;margin&quot;,
  &quot;margin-top&quot;,
  &quot;margin-bottom&quot;,
  &quot;top&quot;,
  &quot;bottom&quot;,
  &quot;right&quot;,
  &quot;left&quot;,
  &quot;border&quot;,
  &quot;box-shadow&quot;,
  &quot;border-radius&quot;,
  &quot;font-size&quot;,
];
/**
 * main function
 */
module.exports = postcss.plugin(&quot;realCss&quot;, function (options) {
  return function (css) {
    options = options || {};
    const reg = /(\d+)\b(?!%)/gm;
    css.walkDecls((decl) =&amp;gt; {
      // 1. 遍历所有 css 属性，找到我们定义的需要添加单位的项
      if (
        _.find(props, (props) =&amp;gt; {
          return decl.prop.includes(props);
        })
      ) {
        // 2. 简单粗暴，直接添加 px
        decl.value = decl.value.replace(reg, (a) =&amp;gt; a + &quot;px&quot;);
      }
      // 3. 给所有属性添加 !important ，提高优先级
      decl.value += &quot;!important&quot;;
    });
  };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相应的我们在 webpack 的 css rule 里面加上 postcss 的配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  loader: require.resolve(&apos;postcss-loader&apos;),
  options: {
    plugins: [
      postcssPresetEnv(),
      // 我们自己的开发 postcss 插件， realCss
      realCss(),
      post2Rem({ remUnit: 100 }),
    ],
    sourceMap: !!DEV,
  },
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;样式优先级问题&lt;/h5&gt;
&lt;p&gt;一开始还没注意到这个问题，后来再测试的时候才发现有的样式总是不生效，排查后才发现原来是因为样式优先级的问题。
举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Component } from &quot;rax&quot;;

class View extends Component {
  render() {
    let props = this.props;
    let styleProps = {
      ...styles.initial,
      ...props.style,
    };
    return &amp;lt;div {...props} style={styleProps} /&amp;gt;;
  }
}

const styles = {
  initial: {
    border: &quot;0 solid black&quot;,
    position: &quot;relative&quot;,
    boxSizing: &quot;border-box&quot;,
    display: &quot;flex&quot;,
    flexDirection: &quot;column&quot;,
    alignContent: &quot;flex-start&quot;,
    flexShrink: 0,
  },
};

export default View;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面是 rax-view 的代码，可以看到它有一个初始化的样式，直接内联到了元素标签上。如果都是用 rax 开发，所有样式内联，那么外部通过 props 传下来的样式 &lt;code&gt;props.style&lt;/code&gt; 的优先级是高于它自己的初始化样式 &lt;code&gt;styles.initial&lt;/code&gt; ，这样是没有问题的。&lt;/p&gt;
&lt;p&gt;一旦我们把外部的 css 提取出去，那么这里的 &lt;code&gt;props.styles&lt;/code&gt; 也基本就是我们要提取出来的东西，这里的一旦提取为外联 css 文件，那么它的优先级就会永远低于 &lt;code&gt;styles.initial&lt;/code&gt; , 这样我们的样式就会出错。&lt;/p&gt;
&lt;p&gt;这里我没有想到什么好的办法来提升外联 css 的优先级，所以直接粗暴的给每个外联 css 的属性添加了 &lt;code&gt;!important&lt;/code&gt; ，参考上面 realCss 的注释 3 。方法搓了一点，但是很能解决问题。而且不是硬编码到 css 里，后续去掉也很简单。&lt;/p&gt;
&lt;h4&gt;style 支持 Array&lt;/h4&gt;
&lt;p&gt;这里我们跟着上面提取外联 css 的属性并放到 class 属性上一起说。
我们要处理的 style 标签有如下四种：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1`&amp;lt;View style={{color: red}}&amp;gt;`;
2`&amp;lt;View style={styles.wrap}&amp;gt;`;
3`&amp;lt;Text style={[styles.name, { color: &apos;red&apos; }, {fontSize: 24}]}&amp;gt;{name}&amp;lt;/Text&amp;gt;`;
4`&amp;lt;View style={sty}&amp;gt;`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于这三种情况我们期望的结果应该是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;保持不变，就是一个普通的 js 内联样式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;View style={{color: red}}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们认为 styles 都是从外部 css 文件引入的，所以需要替换 styles.wrap 为 class 字符串，并赋值到 class 属性上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const cls = &quot;className&quot;;
return &amp;lt;View className={cls} /&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这种情况是前面两种的组合，对于内联样式，继续内联到 style 属性上，对于外部引用的 styles ，替换为 class 字符串放到 className 上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const cls = &quot;className&quot;;
const style = Object.assign({}, { color: &quot;red&quot; }, { fontSize: 24 });
return &amp;lt;View className={cls} style={style} /&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要向上查找当前变量的类型，如果是 Array，那就参照上一条处理，否则认为是内联样式，按照 1 处理。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;有了期望的结果，那解决问题的思路也就比较容易了。我们要做的就是对现有 style 标签的属性做判断：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果是 object ，对应 ast 解析后的 ObjectExpression ，那么直接当作内联样式处理&lt;/li&gt;
&lt;li&gt;如果是一个从 styles （为了简化判断，我们默认 styles 是从外部 css 引入的）上读值，对应 ast 解析后的 MemberExpression， 认为是从外部 css 引入的样式&lt;/li&gt;
&lt;li&gt;如果是一个 Array ，对应 ast 解析后的 ArrayExpression ，把里面的东西遍历一边，找到要转换成 class 字符串的需要内联的样式，经过处理后，再放到当前元素的 className 属性和 style 属性上&lt;/li&gt;
&lt;li&gt;还有一种是值是另外一个定义的变量，也就是 ast 解析后的 Identifier ，这种需要判断里面的值是不是 Array ，是的话按照上一条处理，否则认为是内联样式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对上面三种情况的操作抽象一下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到有 style 元素的标签，然后判断 style 的值，提取处理里面要内联的 css 和要转换为 class 的样式，&lt;/li&gt;
&lt;li&gt;根据上一步的结果重建 style 属性，并添加 className 属性&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;打开 &lt;a href=&quot;https://astexplorer.net/&quot;&gt;https://astexplorer.net/&lt;/a&gt; 看一下，每个元素对应的都是一个 JSXOpeningElement ，我们要做的就是遍历 JSXOpeningElement ，然后对每个标签|组件做处理。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- @import &quot;./transform-rax-to-react/jsx-element.png&quot; --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/transform-rax-to-react/jsx-element.png&quot; alt=&quot;import&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具体实现逻辑：&lt;/p&gt;
&lt;p&gt;第一步：找内联 css 和需要转换的 class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      JSXOpeningElement: {
        enter(path) {
          const node = path.node;
          // 用来存放找到的内联 css
          const styles = [];
          // 用来存放被转换的 class
          const classNames = [];
          let newStyleAttr = null;
          let newClassNameAttr = null;
          let styleAttrPath = null;

          path.traverse({
            JSXAttribute(path) {
              if (path.node.name.name !== &apos;style&apos;) return;
              styleAttrPath = path;
              path.traverse({
                /**
                 * 从离元素最近的一个方法往下找，判断 style 的值是否是一个 Array，
                 * 仅限查找直接的变量，而非从对象上读取的。
                 * eg: style={[list, obj.arr]} ,则只查找 list 而不管 obj.arr
                 */
                Identifier(identifyPath) {
                  const name = identifyPath.node.name;
                  const parent = identifyPath.parent;
                  if (t.isMemberExpression(parent)) return false;
                  let isArray = false;
                  // 从当前 path 向上查找最近的一个方法
                  const par = identifyPath.findParent(p =&amp;gt; {
                    if (t.isClassMethod(p) || t.isFunction(p)) {
                      // 从 render  方法里面往下找当前变量的定义，
                      p.traverse({
                        VariableDeclarator(path) {
                          if (
                            t.isArrayExpression(path.node.init) &amp;amp;&amp;amp;
                            path.node.id.name === name
                          ) {
                            isArray = true;
                          }
                        }
                      });
                    }
                  });

                  if (isArray) {
                    // TODO: 如果是 Array ，则重新走一下后面的 ArrayExpression 的处理
                    // 创建当前作用域下的唯一变量
                    const arrayStyle = identifyPath.scope.generateUidIdentifier(
                      &apos;arrayStyle&apos;
                    );
                    // 生成新的变量定义语句，
                    // 如果是 Array ，那么认为里面每个元素都是内联样式，通过 Object.assign 把它们组合到一起
                    const preformArrayStyle = template.ast(`
                      const ${arrayStyle.name} = {}
                      ${name}.forEach(sty =&amp;gt; {
                        if (typeof sty === &apos;object&apos;) {
                          Object.assign(${arrayStyle.name}, sty)
                        }
                      })
                    `);
                    const jsxParent = identifyPath.findParent(p =&amp;gt; {
                      if (
                        t.isReturnStatement(p) ||
                        t.isVariableDeclaration(p)
                      ) {
                        return true;
                      }
                    });
                    // 在最近的 return 语句上插入生成的语句
                    jsxParent.insertBefore(preformArrayStyle);
                    // 把当前 style 的值赋值为我们新建的变量名 arrayStyle
                    identifyPath.node.name = arrayStyle.name;
                  }
                },
                /**
                 * 如果是变量上读取的属性，则认为是从外部 css 引入的样式。通过 css-loader 的处理后，
                 * 引入的值已经变成了一个包含所有 class 的 object，我们直接把它替换为 style 就好了
                 * */
                MemberExpression(path) {
                    // function replaceStyle(path) {
                    //   if (!path.parentPath.parent.name) return;
                    //   path.parentPath.parent.name.name = &apos;className&apos;;
                    // }
                  replaceStyle(path);
                },
                /**
                 *  如果是 Array ，那么判断里面的值，规则按照上面两种处理方式处理。
                 * */
                ArrayExpression(arrayExpressionPath) {
                  const eles = arrayExpressionPath.node.elements;
                  // 遍历 Array 里面的元素
                  eles.forEach(e =&amp;gt; {
                    // MemberExpression 认为是处理后的 class string
                    if (t.isMemberExpression(e)) {
                      classNames.push(e);
                    } else if (t.isObjectExpression(e)) {
                      // 如果是 Object 表达式，认为是内联样式
                      styles.push(e);
                    } else if (t.isIdentifier(e)) {
                      // 如果是自定义变量，粗暴的认为是内联样式
                      styles.push(e);
                    } else if (t.isLogicalExpression(e)) {
                      // 由于不好判断最终返回的值类型， 所以直接假定返回的 string ，当作 className处理
                      classNames.push(e);
                    }
                  });
                }
              });
            }
          });
      }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们就可以拿到对应 &lt;code&gt;styles&lt;/code&gt; 和 &lt;code&gt;classNames&lt;/code&gt; ,接下来就是用它们来重建我们的 style 和 className 属性，看代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!styles.length &amp;amp;&amp;amp; !classNames.length) return;
/**
 * NOTE: 重建样式属性
 * 1. 删除 style 属性节点
 * 2. 用 styles 创建新的 style 节点
 * 3. 用 classNames 创建 className 节点
 */
// 尝试获取最近的一个 render 方法
const renderPath = getRenderPath(path);
// 获取最近的一个 return 方法
let returnPath = getReturnPath(path);

// NOTE: 生成唯一 id ,并插入合并 styles 的代码，
styleAttrPath.remove();
if (styles.length) {
  if (!renderPath) return false;
  // 为 style 值创建当前作用域唯一变量名
  const styleUid = path.scope.generateUidIdentifier(&quot;style_UID&quot;);

  function buildStyleScript(styleUidName, styles) {
    const test = t.callExpression(
      t.memberExpression(t.identifier(&quot;Object&quot;), t.identifier(&quot;assign&quot;)),
      styles,
    );
    const newScript = t.variableDeclaration(&quot;const&quot;, [
      t.variableDeclarator(styleUidName, test),
    ]);
    return newScript;
  }

  const newScript = buildStyleScript(styleUid, styles);
  // 在 return 语句前添加当前 style_UID 的变量定义
  returnPath.insertBefore(newScript);
  newStyleAttr = t.jsxAttribute(
    t.jsxIdentifier(&quot;style&quot;),
    getAttributeValue({ value: styleUid.name, literal: true }),
  );
  path.node.attributes.push(newStyleAttr);
}
if (classNames.length) {
  // 构建并插入 className 字段
  if (!renderPath) return;
  // 为  className 创建当前作用域唯一变量名
  const classNameUid = path.scope.generateUidIdentifier(&quot;className_UID&quot;);
  function buildClassNameScript(classNameUid, nodes) {
    // DONE: 构建一个 List ，用来创建 className 字符串
    const array = t.arrayExpression(nodes);
    const call = t.callExpression(
      t.memberExpression(array, t.identifier(&quot;join&quot;)),
      [t.stringLiteral(&quot; &quot;)],
    );
    const newScript = t.variableDeclaration(&quot;const&quot;, [
      t.variableDeclarator(classNameUid, call),
    ]);
    return newScript;
  }

  const newScript = buildClassNameScript(classNameUid, classNames);
  // 在 return 前插入当前 className_UID 的变量定义
  returnPath &amp;amp;&amp;amp; returnPath.insertBefore(newScript);

  //  构建 className 属性节点
  newClassNameAttr = t.jsxAttribute(
    t.jsxIdentifier(&quot;className&quot;),
    getAttributeValue({ value: classNameUid.name, literal: true }),
  );
  // 给当前 jsx 标签添加 className 属性节点
  path.node.attributes.push(newClassNameAttr);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样处理下来，整个 rax 组件就可以编译到 react 了，涉及到的工具有 webpack ，babel 。&lt;/p&gt;
&lt;p&gt;最终的转化效果是很完美的，毕竟除了样式相关，也没处理什么东西。
不过倒是通过这次实践，学到了 babel 插件的开发方法，以后碰到类似问题就又多了一种处理方式。 --&amp;gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;还是要面向标准开发，背靠社区才更有前途。&lt;/p&gt;
</content:encoded></item><item><title>【翻译向】webpack2 指南（上）</title><link>https://ab.kisnows.com/zh-CN/2017/01/17/webpack2-guide-1/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/01/17/webpack2-guide-1/</guid><pubDate>Tue, 17 Jan 2017 21:49:35 GMT</pubDate><content:encoded>&lt;h1&gt;前置定义&lt;/h1&gt;
&lt;p&gt;Bundle 代码包
Chunk 代码块&lt;/p&gt;
&lt;h1&gt;安装&lt;/h1&gt;
&lt;p&gt;npm install webpack --save-dev&lt;/p&gt;
&lt;h1&gt;代码分割&lt;/h1&gt;
&lt;p&gt;代码分割是 webpack 中最引人注目的功能之一。它允许你把代码分割成各种可以根据需求载入的代码包，就像一个用户浏览器去匹配路由一样，或者一个用户发出的事件。这允许你小的模块，允许你控制资源的载入优先级，如果使用得当的话，可以大大影响（降低）你应用的加载时间。&lt;/p&gt;
&lt;h2&gt;缓存和并行加载的资源分割&lt;/h2&gt;
&lt;h3&gt;第三方代码分割&lt;/h3&gt;
&lt;p&gt;一个典型的应用会依赖很多第三方的框架和库文件。不像应用代码本身，这些第三方代码的变更非常频繁。
如果我们保持这些库在它本身的代码包中，从应用代码本身分离出来，那么我们就可以使用浏览器的缓存策略去在一个长时间内把这些代码缓存到最终用户的机器上。&lt;/p&gt;
&lt;p&gt;为了达到这个效果，第三方代码的 vendor 包的 hash 部分必须保持不变，不管应用代码如何变化。学习 &lt;a href=&quot;https://webpack.js.org/guides/code-splitting-libraries&quot;&gt;如何通过 &lt;code&gt;CommonsChunkPlugin&lt;/code&gt; 来分割 vendor/library 代码&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;CSS 分割&lt;/h3&gt;
&lt;p&gt;你可能也想把样式文件分割成为一个单独的包，从应用逻辑总独立出来。这可以增强样式文件的可缓存性，并且允许浏览器在加载应用代码时并行加载你的样式文件，因此也可以避免 FOUC （一个无样式内容的闪屏）。
学习 &lt;a href=&quot;https//webpack.js.org/guides/code-splitting-css&quot;&gt;如何去分割 CSS&lt;/a&gt; 通过使用 &lt;code&gt;ExtractTextWebpackPlugin&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;按需代码分割&lt;/h2&gt;
&lt;p&gt;虽然前面的资源分割需要用户在配置文件中预先指定分割点，但是也可以在应用代码中创建动态的分割点。&lt;/p&gt;
&lt;p&gt;这个功能在有很多细微颗粒代码块时会很有用，举个例子，每一个应用的路由或者按照用户的预测行为。这可以使用户按需加载需要的资源。&lt;/p&gt;
&lt;h3&gt;通过 &lt;code&gt;require.ensure()&lt;/code&gt; 来分割代码&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;require.ensure&lt;/code&gt; 是一个 CommonJS 风格的方式去异步加载资源。通过添加 &lt;code&gt;require.ensure([&amp;lt;fileurl&amp;gt;])&lt;/code&gt; , 我们可以在代码中定义一个分割点。 Webpack 可以创建一个包含在这个分割点中的所有代码的代码包。学习 &lt;a href=&quot;https://webpack.js.org/guides/code-splitting-require&quot;&gt;如何分割代码&lt;/a&gt; 通过使用 &lt;code&gt;require.ensure()&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TODO System.import()&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h1&gt;代码分割 - CSS&lt;/h1&gt;
&lt;p&gt;在 webpack 中，当你使用 css-loader 并且在 JavaScript 中引入 CSS 文件，那么 CSS 文件会被打包在你的 JavaScript 文件中。这有一个不好的地方，就是你无法使用浏览器异步并行加载 CSS 的能力。相反，你的页面会等到整个 JavaScript 文件加载完成，才完成了样式文件的加载。Webpack 可以通过使用 extract-text-webpack-plugin 和 css-loader 来把样式文件分离出来去解决这个问题。&lt;/p&gt;
&lt;h2&gt;使用 &lt;code&gt;css-loader&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;引入 css 到你的 JavaScript 中，需要使用 css-loader 去配置 webpack 的配置文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//webpack.config.js

modules.exports = function(env){
    entry: &apos;..&apos;,
    ...
    module: {
        rules: [{
            test: /\.css$/,
            exclude: /node_modules/,
            loader: &apos;css-loader&apos;
        }]
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用 &lt;code&gt;extract-text-webpack-plugin&lt;/code&gt; - ExtractTextPlugin&lt;/h2&gt;
&lt;p&gt;安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm I --save-dev extract-text-webpack-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要使用这个 &lt;code&gt;ExtractTextPlugin&lt;/code&gt;,需要通过两个步骤配置在 &lt;code&gt;webpack.config.js&lt;/code&gt; 中。&lt;/p&gt;
&lt;h3&gt;在 loader 里面&lt;/h3&gt;
&lt;p&gt;从之前的 &lt;code&gt;css-loader&lt;/code&gt; 中适配，我们应该如下添加 &lt;code&gt;ExtractTextPlugin&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loader: ExtractTextPlugin.extract(&apos;css-loader?sourceMap&apos;) //Can be used without sourcemaps too.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;在 plugin 里面&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;new ExtractTextPlugin({ filename: &apos;bundle.css&apos;, disable: false, allChunks: true })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这两步，就可以生成一个新的包含所有 CSS 模块的代码包，然后把他们添加到 &lt;code&gt;index.html&lt;/code&gt; 的 &lt;code&gt;head&lt;/code&gt; 中去。可以通过 &lt;a href=&quot;https://github.com/webpack/extract-text-webpack-plugin#api&quot;&gt;ExtractTextPlugin&lt;/a&gt; 去了解关于它 api 的更多信息。&lt;/p&gt;
&lt;p&gt;完整的配置文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ExtractTextPlugin = require(&quot;extract-text-webpack-plugin&quot;);
module.exports = function () {
  return {
    entry: &quot;./main.js&quot;,
    output: {
      path: &quot;./dist&quot;,
      filename: &quot;bundle.js&quot;,
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          exclude: /node_modules/,
          loader: ExtractTextPlugin.extract({
            loader: &quot;css-loader?sourceMap&quot;,
          }),
        },
      ],
    },
    devtool: &quot;source-map&quot;,
    plugins: [
      new ExtractTextPlugin({
        filename: &quot;bundle.css&quot;,
        disable: false,
        allChunks: true,
      }),
    ],
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;代码分割-库文件&lt;/h1&gt;
&lt;p&gt;一个典型的应用会依赖很多第三方来提供框架或功能支持。项目中使用的固定版本的库/框架文件的代码一般不会有变动，然而应用本身的业务逻辑代码却经常会有变动。&lt;/p&gt;
&lt;p&gt;把应用代码和库文件的代码打包在一起是一件非常低效的事情。这是因为浏览器可以根据缓存头缓存这些资源文件到本地而不用每次都去服务端或者 cdn 上去发请求重新获取，如果文件内容没有变动的话。为了能够享受这个好处，我们需要保持第三方文件的 hash 不变，无论应用本身的代码如何变化。&lt;/p&gt;
&lt;p&gt;我们只有把应用代码和第三方代码分离开才可以达到这样的效果。&lt;/p&gt;
&lt;p&gt;我们考虑一个一个简单的应用，它使用了 momentjs ，一个通常用来时间格式化的库。&lt;/p&gt;
&lt;p&gt;安装 &lt;code&gt;moment&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save moment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Index 文件会引用 &lt;code&gt;moment&lt;/code&gt; 作为一个依赖并且打印当前的时间：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Index.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var moment = require(&quot;moment&quot;);
console.log(moment().format());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以通过如下这个配置文件来打包这个应用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = function (env) {
  return {
    entry: &quot;./index.js&quot;,
    output: {
      filename: &quot;[chunkhash].[name].js&quot;,
      path: &quot;./dist&quot;,
    },
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当运行 &lt;code&gt;webpack&lt;/code&gt; 命令的时候，如果你检查打包后的文件，你会发现 &lt;code&gt;moment&lt;/code&gt; 和 &lt;code&gt;index.js&lt;/code&gt; 都被打包在了 &lt;code&gt;bundle.js&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;这不是一个很好的解决方案。如果 &lt;code&gt;index.js&lt;/code&gt; 修改了，那么这打包文件会重新构建，浏览器就需要重新去加载这个文件，即使 moment.js 文件并没有任何改动。&lt;/p&gt;
&lt;h2&gt;多个入口&lt;/h2&gt;
&lt;p&gt;让我们缓和这个问题，我们给 moment 添加一个新的入口命名为 vendors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = function (env) {
  return {
    entry: {
      main: &quot;./index.js&quot;,
      vendor: &quot;moment&quot;,
    },
    output: {
      filename: &quot;[chunkhash].[name].js&quot;,
      path: &quot;./dist&quot;,
    },
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在执行 webpack 命令，我们会看到两个打包后的文件。如果你检查里面代码的话，你会看到 &lt;code&gt;moment&lt;/code&gt; 的代码同时出现在两个代码包中。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们需要使用 &lt;a href=&quot;https://webpack.js.org/plugins/commons-chunk-plugin&quot;&gt;CommonsChunkPlugin&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;CommonsChunksPlugin&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;这是一个相当复杂的插件。它从根本上允许你从不同的代码包中提取出所有的相同模块并且把它们加入到共同的代码包中。如果这个相同的代码包不存在，那么就创建一个新的。&lt;/p&gt;
&lt;p&gt;我们可以修改 webpack 的配置文件来使用这个 &lt;code&gt;CommonsCunksPlugin&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var webpack = require(&quot;webpack&quot;);
module.exports = function (env) {
  return {
    entry: {
      main: &quot;./index.js&quot;,
      vendor: &quot;moment&quot;,
    },
    output: {
      filename: &quot;[chunkhash].[name].js&quot;,
      path: &quot;./dist&quot;,
    },
    plugins: [
      new webpack.optimize.CommonsChunkPlugin({
        name: &quot;vendor&quot;, // Specify the common bundle&apos;s name.
      }),
    ],
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话， &lt;code&gt;moment&lt;/code&gt; 的代码就只会出现在 vendor 代码包中了。&lt;/p&gt;
&lt;h2&gt;清单文件（Manifest File）&lt;/h2&gt;
&lt;p&gt;但是，如果我们可以修改应用的代码并且再次执行 &lt;code&gt;webpack&lt;/code&gt; 命令，我们看到 vendors 文件的 hash 还是变化了。即使我们已经分离了 &lt;code&gt;vendor&lt;/code&gt; 和 &lt;code&gt;main&lt;/code&gt; 代码包，但是当应用代码发生修改的时候 &lt;code&gt;vendor&lt;/code&gt; 还是变化了。 这意味着我们依旧不能享受浏览器缓存带来的好处，因为每一次重新编译都会修改 vendors 的 hash 值。&lt;/p&gt;
&lt;p&gt;这个问题是因为每一次编译，webpack 生成一些 webpack 运行时代码，用来帮助 webpack 来做它的工作。当那里存在一个单独的代码包，运行时会驻留在其中。但当多个代码包被生成的时候，运行时代码会被提取到公共的模块中，就是这个 &lt;code&gt;vendor&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;为了阻止这个，我们需要提取出运行时到一个分离的清单文件（Manifest File）。虽然我们又多创建另一个代码包，但它的开销也被我们在 vendor 文件上获得的长期缓存所带来的好处所抵消了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var webpack = require(&quot;webpack&quot;);
module.exports = function (env) {
  return {
    entry: {
      main: &quot;./index.js&quot;,
      vendor: &quot;moment&quot;,
    },
    output: {
      filename: &quot;[chunkhash].[name].js&quot;,
      path: &quot;./dist&quot;,
    },
    plugins: [
      new webpack.optimize.CommonsChunkPlugin({
        names: [&quot;vendor&quot;, &quot;manifest&quot;], // Specify the common bundle&apos;s name.
      }),
    ],
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面这个配置文件，我们会看到三个代码包被生成。&lt;code&gt;vendor&lt;/code&gt;,&lt;code&gt;main&lt;/code&gt; 和 &lt;code&gt;manifest&lt;/code&gt;. 这样当应用代码修改的时候，重新打包后，修改的就只有 &lt;code&gt;main&lt;/code&gt; 和 &lt;code&gt;manifest&lt;/code&gt; 了。 &lt;code&gt;manifest&lt;/code&gt; 被修改是因为里面有对生成文件 hash 值的引用。&lt;/p&gt;
&lt;h1&gt;代码分割-使用 RequireJS&lt;/h1&gt;
&lt;p&gt;在这个章节，我们讨论 webpack 如何通过 &lt;code&gt;require.ensure()&lt;/code&gt; 分割代码。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;require.ensure()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Webpack 静态分析给 &lt;code&gt;require.ensure()&lt;/code&gt; 在代码中当构建和添加模块到分离的代码块中。这个新的代码块会被 webpack 在需要的时候通过 jsonp 引入。&lt;/p&gt;
&lt;p&gt;它的语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require.ensure(dependencies: String[], callback: function(require), chunkName: String)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;依赖(dependencies)&lt;/h3&gt;
&lt;p&gt;这是一个字符串数组，用来声明所有需要在执行回调函数之前就需要预先加载好且可用的模块。&lt;/p&gt;
&lt;h3&gt;回调函数(callback)&lt;/h3&gt;
&lt;p&gt;一个回调函数会被 webpack 执行一次当所有依赖(dependencies)都被加载以后。Require 对象的实现作为一个参数传递给这个回调函数。这样，我们可以更进一步 &lt;code&gt;require&lt;/code&gt; 需要的依赖(dependencies)和其他需要执行的模块。&lt;/p&gt;
&lt;h3&gt;代码块名字(chunkName)&lt;/h3&gt;
&lt;p&gt;代码块名字是一个用来命名通过 &lt;code&gt;require.ensure()&lt;/code&gt; 创建的代码块。通过给不同的 &lt;code&gt;require.ensure()&lt;/code&gt; 创建的代码分割点分割出来的代码块一个相同的名字，我们可以确保所有的依赖都被打包到同一个代码块中。&lt;/p&gt;
&lt;p&gt;我们来看一下如下结构的一个项目&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\\ file structure
    |
    js --|
    |    |-- entry.js
    |    |-- a.js
    |    |-- b.js
    webpack.config.js
    |
    dist
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// entry.js

require(&quot;a&quot;);
require.ensure([], function (require) {
  require(&quot;b&quot;);
});

// a.js
console.log(&quot;***** I AM a *****&quot;);

// b.js
console.log(&quot;***** I AM b *****&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js

module.exports = function (env) {
  return {
    entry: &quot;./js/entry.js&quot;,
    output: {
      filename: &quot;bundle.js&quot;,
      path: &quot;./dist&quot;,
    },
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当运行 webpack 命令的时候，我们发现 webpack 创建了两个新的代码包，&lt;code&gt;bundle.js&lt;/code&gt; 和 &lt;code&gt;0.bundle.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;entry.js&lt;/code&gt; 和 &lt;code&gt;a.js&lt;/code&gt; 被打包到了 &lt;code&gt;bundle.js&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;b.js&lt;/code&gt; 被打包到了 &lt;code&gt;0.bundle.js&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;require.ensure()&lt;/code&gt; 的陷阱&lt;/h2&gt;
&lt;h3&gt;空数组作为一个参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;require.ensure([], function (require) {
  require(&quot;./a.js&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码确保一个分割点被创建， &lt;code&gt;a.js&lt;/code&gt; 会被 webpack 单独的打包成一个文件。&lt;/p&gt;
&lt;h3&gt;依赖作为参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;require.ensure([&quot;./a.js&quot;], function (require) {
  require(&quot;./b.js&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码，&lt;code&gt;a.js&lt;/code&gt; 和 &lt;code&gt;b.js&lt;/code&gt; 会被一起打包并且从主代码包中分离出来。但是只有 &lt;code&gt;b.js&lt;/code&gt; 的内容被执行了。 &lt;code&gt;a.js&lt;/code&gt; 的内容只是可用的但并没有被执行。为了执行 &lt;code&gt;a.js&lt;/code&gt;, 我们需要 require 它作为一个同步的方式比如 &lt;code&gt;require(&apos;./a.js)&lt;/code&gt; ，这样 JavaScript 就可以执行它了。&lt;/p&gt;
&lt;h1&gt;依赖管理&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;Ø es6 module
Ø Commonjs
Ø Amd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;表达式依赖（require with expression）&lt;/h2&gt;
&lt;p&gt;当你通过表达式去引入一个模块的时候，就会创建一个上下文，所以当编译的时候我们并不知道准确的模块是哪个。&lt;/p&gt;
&lt;p&gt;例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require(&quot;./template/&quot; + name + &quot;.ejs&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Webpack 解析 &lt;code&gt;require()&lt;/code&gt; 的调用，并且提取出来一些信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Directory:./template
Regular expression:/^.*\.ejs$/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;上下文模块（context module）&lt;/h3&gt;
&lt;p&gt;一个上下文模块被生成。它包含了在这个文件夹下所有可以被上面的正则匹配所匹配到的模块的引用。上下文模块包含了一个把请求解释到模块 id 的 map.
例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;./table.ejs&quot;: 42,
  &quot;./table-row.ejs&quot;: 43,
  &quot;./directory/folder.ejs&quot;: 44
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上下文模块同样包含了一些运行时代码用来访问这个 map.&lt;/p&gt;
&lt;p&gt;这意味着动态的引用可以被支持，但是会导致所有可能被用到的模块都被打包到了最终的代码包中。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;require.context&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;你可以通过 &lt;code&gt;require.context()&lt;/code&gt; 方法创建你自己的上下文。它允许你传入一个用来查询的文件夹，一个用来决定是否递归查找子文件夹的标识，还有一个用来匹配文件的正则表达式。&lt;/p&gt;
&lt;p&gt;Webpack 会在代码打包的时候解析 &lt;code&gt;require.context()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;它的语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require.context(directory, (useSubdirectories = false), (regExp = /^\.\//));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require.context(&quot;./test&quot;, false, /\.test\.js$/);
// a context with files from the test directory that can be required with a request endings with `.test.js`.

```

```javascript
require.context(&quot;../&quot;, true, /\.stories\.js$/);
// a context with all files in the parent folder and descending folders ending with `.stories.js`.

```

## 上下文模块API（context module API）
一个上下文模块暴露一个方法，它接收一个参数：请求的内容。
暴露出来的函数有三个属性：`resolve`,`key`,`id`
	• `resolve` 是一个函数，执行后返回解析后的请求内容的模块 id
	• `keys`是一个函数，执行后返回一个数组，包含所有可能被上下文模块所请求的所有的模块的 id
	当你想要通过正则匹配引入一个文件夹下所有模块时，这会非常有用：

```javascript
function importAll (r) {
  r.keys().forEach(r);
}
importAll(require.context(&apos;../components/&apos;, true, /\.js$/))

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;var cache = {};
function importAll (r) {
  r.keys().forEach(key =&amp;gt; cache[key] = r(key));
}
importAll(require.context(&apos;../components/&apos;, true, /\.js$/));
// At build-time cache will be populated with all required modules.

```

	• `id` 是上下文模块生成的模块 id. 当使用 `module.hot.accept` 时，这会非常有用。


&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【翻译向】webpack2 指南（中）</title><link>https://ab.kisnows.com/zh-CN/2017/01/18/webpack2-guide-2/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/01/18/webpack2-guide-2/</guid><pubDate>Wed, 18 Jan 2017 19:49:14 GMT</pubDate><content:encoded>&lt;h1&gt;动态模块替换（Hot Module Repalcement -React）&lt;/h1&gt;
&lt;p&gt;就像之前 &lt;a href=&quot;https://webpack.js.org/concepts/hot-module-repalcement&quot;&gt;理念页面&lt;/a&gt; 中解析的细节那样，动态模块替换（HMR）会在应用运行时动态的替换、添加或者删除模块而不用重新刷新页面。 HMR 非常有用，当应用只有一个状态树（single state tree）时。&lt;/p&gt;
&lt;p&gt;下面介绍的方法描述中使用了 Babel 和 React ,但这并不是使用 HMR 所必须的工具。&lt;/p&gt;
&lt;h2&gt;项目配置&lt;/h2&gt;
&lt;p&gt;这里会指导你如何用 Babel， React 和 PostCss 一起使用 HMR 去演示一个项目。为了能够跟着下面走下去，需要把这些依赖添加到 &lt;code&gt;package.json&lt;/code&gt; 中去。&lt;/p&gt;
&lt;p&gt;为了使用 HMR，你需要如下这些依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev babel@6.5.2 babel-core@6.13.2 babel-loader@6.2.4 babel-preset-es2015@6.13.2 babel-preset-react@6.11.1 babel-preset-stage-2@6.13.0 css-loader@0.23.1 postcss-loader@0.9.1 react-hot-loader@3.0.0-beta.6 style-loader@0.13.1 webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，为了达到我们演示的目的，还需要：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save react@15.3.0 react-dom@15.3.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Babel Config&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;.babelrc&lt;/code&gt; 文件应该如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;presets&quot;: [
    [&quot;es2015&quot;, { &quot;modules&quot;: false }],
    // webpack understands the native import syntax, and uses it for tree shaking

    &quot;stage-2&quot;,
    // Specifies what level of language features to activate.
    // Stage 2 is &quot;draft&quot;, 4 is finished, 0 is strawman.
    // See https://tc39.github.io/process-document/

    &quot;react&quot;
    // Transpile React components to JavaScript
  ],
  &quot;plugins&quot;: [
    &quot;react-hot-loader/babel&quot;
    // Enables React code to work with HMR.
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Webpack Config&lt;/h3&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { resolve } = require(&quot;path&quot;);
const webpack = require(&quot;webpack&quot;);

module.exports = {
  entry: [
    &quot;react-hot-loader/patch&quot;,
    // activate HMR for React

    &quot;webpack-dev-server/client?http://localhost:8080&quot;,
    // bundle the client for webpack-dev-server
    // and connect to the provided endpoint

    &quot;webpack/hot/only-dev-server&quot;,
    // bundle the client for hot reloading
    // only- means to only hot reload for successful updates

    &quot;./index.js&quot;,
    // the entry point of our app
  ],
  output: {
    filename: &quot;bundle.js&quot;,
    // the output bundle

    path: resolve(__dirname, &quot;dist&quot;),

    publicPath: &quot;/&quot;,
    // necessary for HMR to know where to load the hot update chunks
  },

  context: resolve(__dirname, &quot;src&quot;),

  devtool: &quot;inline-source-map&quot;,

  devServer: {
    hot: true,
    // enable HMR on the server

    contentBase: resolve(__dirname, &quot;dist&quot;),
    // match the output path

    publicPath: &quot;/&quot;,
    // match the output `publicPath`
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: [&quot;babel-loader&quot;],
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [&quot;style-loader&quot;, &quot;css-loader?modules&quot;, &quot;postcss-loader&quot;],
      },
    ],
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // enable HMR globally

    new webpack.NamedModulesPlugin(),
    // prints more readable module names in the browser console on HMR updates
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面有很多配置，但不是所有都和 HMR 有关。可以通过查阅 webpack-dev-server options 和 concept pages 来加深理解。&lt;/p&gt;
&lt;p&gt;我们基础设想是这样的，你的 JavaScript 入口文件在 &lt;code&gt;./src/index.js&lt;/code&gt; 且你使用 CSS Module 来编写样式文件。&lt;/p&gt;
&lt;p&gt;配置文件中需要重点关注的是 &lt;code&gt;devServer&lt;/code&gt; 和 &lt;code&gt;entry&lt;/code&gt; key. &lt;code&gt;HotModueReplacementPlugin&lt;/code&gt; 同样需要被包含在 &lt;code&gt;plugins&lt;/code&gt; key 中。&lt;/p&gt;
&lt;p&gt;为了达到目的，我们引入了两个模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;react-hot-loader&lt;/code&gt; 添加到了入口中， 是为了能够使 React 支持 HMR&lt;/li&gt;
&lt;li&gt;为了更好的理解 HMR 每次更新的时候做了哪些事情，我们添加了 &lt;code&gt;NamedModulePlugin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Code&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// ./src/index.js
import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom&quot;;
import { AppContainer } from &quot;react-hot-loader&quot;;

// AppContainer is a necessary wrapper component for HMR

import App from &quot;./components/App&quot;;

const render = (Component) =&amp;gt; {
  ReactDOM.render(
    &amp;lt;AppContainer&amp;gt;
      &amp;lt;Component /&amp;gt;
    &amp;lt;/AppContainer&amp;gt;,
    document.getElementById(&quot;root&quot;),
  );
};

render(App);

// Hot Module Replacement API
if (module.hot) {
  module.hot.accept(&quot;./components/App&quot;, () =&amp;gt; {
    const NewApp = require(&quot;./components/App&quot;).default;
    render(NewApp);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// ./src/components/App.js
import React from &quot;react&quot;;

import styles from &quot;./App.css&quot;;

const App = () =&amp;gt; (
  &amp;lt;div className={styles.app}&amp;gt;
    &amp;lt;h2&amp;gt;Hello, &amp;lt;/h2&amp;gt;
  &amp;lt;/div&amp;gt;
);

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.app {
  text-size-adjust: none;
  font-family: helvetica, arial, sans-serif;
  line-height: 200%;
  padding: 6px 20px 30px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个需要特别注意的是 &lt;code&gt;module&lt;/code&gt; 的引用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Webpack 会暴露出 &lt;code&gt;module.hot&lt;/code&gt; 给我们的代码，当我们设置 &lt;code&gt;devServer: { hot: true }&lt;/code&gt; 时；&lt;/li&gt;
&lt;li&gt;这样我们可以使用 &lt;code&gt;module.hot&lt;/code&gt; 来给特定的资源弃用 HMR （这里是 &lt;code&gt;App.js&lt;/code&gt;）. 这里有一个非常重要的 API &lt;code&gt;module.hot.accept&lt;/code&gt; ,用来决定如何处理这些特定的依赖。&lt;/li&gt;
&lt;li&gt;需要注意的是，webpack2 内建支持 ES2015 模块语法，你不需要在 &lt;code&gt;module.hot.accept&lt;/code&gt; 中重新引用根组件。为了达到这个目的，需要在 &lt;code&gt;.babelrc&lt;/code&gt; 配置 Babel ES2015 的预先设置：&lt;pre&gt;&lt;code&gt;[&quot;es2015&quot;, { modules: false }];
&lt;/code&gt;&lt;/pre&gt;
就像我们在之前 Babel Config 中配置的那样。需要注意，禁用 Babel 的模块功能 不仅仅是为了启用 HMR。如果你不关掉这个配置，那么你会碰到需要问题。&lt;/li&gt;
&lt;li&gt;如果你在 webpack2 的配置文件中使用 ES6 模块，并且你按照 #3 修改了 &lt;code&gt;.babelrc&lt;/code&gt;，那么你需要使用 &lt;code&gt;require&lt;/code&gt; 语法来创建两个 &lt;code&gt;.babelrc&lt;/code&gt; 文件：
&lt;ol&gt;
&lt;li&gt;一个放在根目录下面并配置为 &lt;code&gt;&quot;presets: [&quot;es2015&quot;]&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一个放在 webpack 要编译的文件夹下，比如在这个例子中，就是 &lt;code&gt;src/&lt;/code&gt;
所以在这个案例中，&lt;code&gt;module.hot.accept&lt;/code&gt; 会执行 &lt;code&gt;render&lt;/code&gt; 方法无论 &lt;code&gt;src/components/App.js&lt;/code&gt; 或者其它的依赖文件变化的时候 ——这意味着当 &lt;code&gt;App.css&lt;/code&gt; 被引入到 &lt;code&gt;App.js&lt;/code&gt; 中以后，即使是 &lt;code&gt;App.css&lt;/code&gt; 被修改，
&lt;code&gt;render&lt;/code&gt; 方法同样会被执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Index.html&lt;/h3&gt;
&lt;p&gt;入口页面需要被放在页面 &lt;code&gt;dist&lt;/code&gt; 下面，webpack-dev-server 的运行需要这个文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Example Index&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;bundle.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Package.json&lt;/h3&gt;
&lt;p&gt;最后，我们需要启动 &lt;code&gt;webpack-dev-server&lt;/code&gt; 来打包我们的代码并且看看 HMR 是如何工作的。我们可以使用如下的 &lt;code&gt;package.json&lt;/code&gt; 入口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;webpack-dev-server&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行 &lt;code&gt;npm start&lt;/code&gt;, 打开浏览器输入 &lt;code&gt;http://localhost:8080&lt;/code&gt;, 应该可以看到下面这些项展示在 console.log 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dev-server.js:49[HMR] Waiting for update signal from WDS…
only-dev-server.js:74[HMR] Waiting for update signal from WDS…
client?c7c8:24 [WDS] Hot Module Replacement enabled.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编辑并且修改 &lt;code&gt;App.js&lt;/code&gt; 文件，你会在 console.log 中看到类似如下的日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[WDS] App updated. Recompiling…
client?c7c8:91 [WDS] App hot update…
dev-server.js:45 [HMR] Checking for updates on the server…
log-apply-result.js:20 [HMR] Updated modules:
log-apply-result.js:22 [HMR]  - ./components/App.js
dev-server.js:27 [HMR] App is up to date.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 HMR 指出了更新模块的路径。这是因为我们使用了 &lt;code&gt;NamedModulesPlugin&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;开发环境（Development）&lt;/h1&gt;
&lt;p&gt;这个章节介绍在开发过程中可以使用的一些工具。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要注意，不能在生产环境使用&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Source Map&lt;/h2&gt;
&lt;p&gt;当 JS 发生异常的时候，我们需要知道是哪一个文件的哪一行出错了。但是当文件都被 webpack 打包以后，找问题会变得很麻烦。
Source Map 就是为了解决这个问题的。它有很多不同的&lt;a href=&quot;https://webpack.js.org/configuration/devtool&quot;&gt;选项&lt;/a&gt;，每一种都有的好处和不足。在一开始，我们使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;devtool: &quot;cheap-eval-source-map&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;选择一个工具（Choosing a Tool）&lt;/h2&gt;
&lt;p&gt;Webpack 可被用于监视模式（watch mode）。这种模式下， webpack 会监视你的文件，当它们有变动的时候就会重编译。Webpack-dev-server 提供了一个很方便使用的开发环境的服务，并且支持自动刷新功能。如果你已经有了一个开发环境的服务，并且希望能够拥有更好的适应性，那么 webpack-dev-middleware 可以被用作一个中间件来达到这个目的。&lt;/p&gt;
&lt;p&gt;Webpack-dev-server 和 webpack-dev-middleware 实在内存中进行编译的，这意味着打包后的代码包并不会保存到本地磁盘中。这回使打包变得很快，同时不会产生很多临时文件来污染你的本地文件系统。&lt;/p&gt;
&lt;p&gt;大多数情况下，你都会想要去使用 webpack-dev-server， 因为它使用起来很方便，而且提供了许多开箱即用的功能。&lt;/p&gt;
&lt;h3&gt;Webpack 监视模式（wtach mode）&lt;/h3&gt;
&lt;p&gt;Webpack 的监视模式会检测文件的变动。只要变动被检测到，它就会重新进行一次编译。我们希望它的编译过程能有一个很好的进度展示。那么就执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;webpack --progress --watch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随便修改一个文件然后保存，你就会看到重新编译的过程。&lt;/p&gt;
&lt;p&gt;监视模式没有考虑任何和服务有关的问题，所以你需要自己提供一个服务。一个简单的服务就是 &lt;code&gt;[server](https://github.com/tj/serve)&lt;/code&gt;. 当安装好后（&lt;code&gt;npm i server -g&lt;/code&gt;），在你打包后的文件目录下运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当每次重新编译后，你都需要手动的去刷新浏览器。&lt;/p&gt;
&lt;h3&gt;webpack-dev-server&lt;/h3&gt;
&lt;p&gt;webpack-dev-server 提供一个支持自动刷新的服务。&lt;/p&gt;
&lt;p&gt;首先，确认你 &lt;code&gt;index.html&lt;/code&gt; 页面里面已经引用了你的代码包。我们先假设 &lt;code&gt;output.filename&lt;/code&gt; 设置为 &lt;code&gt;bundle.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;/bundle.js&quot;&amp;gt;&amp;lt;/srcipt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 npm 安装 &lt;code&gt;webpack-dev-server&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install webpack-dev-server --save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就可以执行 &lt;code&gt;webpack-dev-server&lt;/code&gt; 的命令了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;webpack-dev-server --open
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的命令会自动打开你的浏览器并指定到 &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;修改一下你的文件并保存。你会发现代码被重新打包了，当打包完成的时候，页面会自动刷新。如果没有如愿达到效果，那么你需要调整 &lt;code&gt;watchOptions(https://webpack.js.org/configuration/dev-server#devserver-watchoptions-)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;现在你有了一个可以自动刷新的服务，接下来我们看如何启用动态模块替换（Hot Module Replacement）。这是一个可以提供不刷新页面替换模块的接口，查看&lt;a href=&quot;https://webpack.js.org/guides/hmr-react&quot;&gt;这里&lt;/a&gt;了解更多 。&lt;/p&gt;
&lt;p&gt;webpack-dev-server 可以做很多的事情，比如代理请求到你的后端服务。想了解更多的配置项，那就查看 &lt;a href=&quot;https://webpack.js.org/configuration/dev-server&quot;&gt;devServer 的文档吧&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;webpack-dev-middleware&lt;/h3&gt;
&lt;p&gt;webpack-dev-middleware 适用于基于中间件的链接堆栈（好难翻译）。当你已经有一个 Node.js 服务或者你想要完全的控制服务的时候会很有用。&lt;/p&gt;
&lt;p&gt;这个中间件会让文件编译在内存中进行。当一个编译在进行过程中，它会延迟一个文件请求，直到它编译完成。&lt;/p&gt;
&lt;p&gt;首先从 npm 上安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install express webpack-dev-server --save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作为一个例子，我们可以这样使用中间件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var express = require(&quot;express&quot;);
var webpackDevMiddleware = require(&quot;webpack-dev-middleware&quot;);
var webpack = require(&quot;webpack&quot;);
var webpackConfig = require(&quot;./webpack.config&quot;);

var app = express();
var compiler = webpack(webpackConfig);

app.use(
  webpackDevMiddleware(compiler, {
    publicPath: &quot;/&quot;, // Same as `output.publicPath` in most cases.
  }),
);

app.listen(3000, function () {
  console.log(&quot;Listening on port 3000!&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据你在 &lt;code&gt;output.publicPath&lt;/code&gt; 和 &lt;code&gt;output.filename&lt;/code&gt; 中的配置，你打包的代码应该可以通过 &lt;code&gt;http://localhost:3000/bundle.js&lt;/code&gt; 访问。&lt;/p&gt;
&lt;p&gt;默认情况下使用的是监视模式。它还支持懒模式（lazy mode），只有在有请求进来的时候才会重新编译。&lt;/p&gt;
&lt;p&gt;配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.use(
  webpackDevMiddleware(compiler, {
    lazy: true,
    filename: &quot;bundle.js&quot;, // Same as `output.filename` in most cases.
  }),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有许多其它有用的选项，详细内容可以查看 &lt;a href=&quot;https://webpack.js.org/configuration/dev-server&quot;&gt;文档&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;为生产环境构建（Building for Production）&lt;/h1&gt;
&lt;p&gt;本章介绍如何用 webpack 来做生产环境的构建。&lt;/p&gt;
&lt;h2&gt;一条自动化的方式&lt;/h2&gt;
&lt;p&gt;执行 &lt;code&gt;webpack -p&lt;/code&gt;(等同于 &lt;code&gt;webpack --optimize--minimize --define process.env.NODE_ENV=&quot;production&quot;&lt;/code&gt;).
这会执行以下几个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;UglifyJsPlugin&lt;/code&gt; 压缩文件&lt;/li&gt;
&lt;li&gt;执行了 &lt;code&gt;LoaderOptionsPlugin&lt;/code&gt;, 查看&lt;a href=&quot;https://webpack.js.org/plugins/loader-options-plugin&quot;&gt;文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;设置 Node 的环境变量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;源码压缩&lt;/h3&gt;
&lt;p&gt;webpack 使用 &lt;code&gt;UglifyJsPlugin&lt;/code&gt; 来压缩源码，通过执行 &lt;a href=&quot;http://lisperator.net/uglifyjs/&quot;&gt;UglifyJs&lt;/a&gt; 来达到压缩输出代码的目的。这个插件支持所有 UglifyJs 的功能。在命令行里输入 &lt;code&gt;--optimize-minimize&lt;/code&gt; ，那么相当于在配置文件中添加了以下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js
const webpack = require(&quot;webpack&quot;);

module.exports = {
  /*...*/
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      sourceMap:
        options.devtool &amp;amp;&amp;amp;
        (options.devtool.indexOf(&quot;sourcemap&quot;) &amp;gt;= 0 ||
          options.devtool.indexOf(&quot;source-map&quot;) &amp;gt;= 0),
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，基于 &lt;a href=&quot;https://webpack.js.org/configuration/devtool/&quot;&gt;devtools option&lt;/a&gt; ，在打包的时候会生成 Source Map.&lt;/p&gt;
&lt;h3&gt;资源映射（Source Map）&lt;/h3&gt;
&lt;p&gt;我们推荐在开发环境启用 Source Map. 因为在 debug 或者测试的时候很有用。Webpack 可以生成包含在代码包或者分离文件中的 inline Source Map.&lt;/p&gt;
&lt;p&gt;在配置文件中，通过修改 &lt;code&gt;devtools&lt;/code&gt; 配置来设置 Source Map 类型。目前我们支持七种不同类型的 Source Map. 可以在&lt;a href=&quot;https://webpack.js.org/configuration/devtool&quot;&gt;具体文档&lt;/a&gt;中找到更加详细的介绍。&lt;/p&gt;
&lt;p&gt;一个比较好好的选择是使用 &lt;code&gt;cheap-module-source-map&lt;/code&gt;，可以将源映射简化为每行映射(simplifies the Source Maps to a single mapping per line)。&lt;/p&gt;
&lt;h3&gt;Node 环境变量&lt;/h3&gt;
&lt;p&gt;执行 &lt;code&gt;webpack -p&lt;/code&gt;( &lt;code&gt;--define process.env.NODE_EMV=&quot;production&quot;&lt;/code&gt;) 会通过如下的配置调用 &lt;code&gt;DefinePlugin&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js
const webpack = require(&quot;webpack&quot;);

module.exports = {
  /*...*/
  plugins: [
    new webpack.DefinePlugin({
      &quot;process.env.NODE_ENV&quot;: JSON.stringify(&quot;production&quot;),
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DefindPlugin&lt;/code&gt; 会在源码中进行查找和替换的工作。所有找到的 &lt;code&gt;process.env.NODE_ENV&lt;/code&gt; 都会被替换为 &lt;code&gt;production&lt;/code&gt;. 这样，类似与 &lt;code&gt;if(process.env.NODE_ENV !==&apos;procution&apos;) console.log(…)&lt;/code&gt; 这样的代码就会被 &lt;code&gt;UnglifyJs&lt;/code&gt; 认为等同于 &lt;code&gt;if(false) console.log(…)&lt;/code&gt; .&lt;/p&gt;
&lt;h2&gt;一个手动的方式：为 webpack 配置不同环境变量下的配置文件&lt;/h2&gt;
&lt;p&gt;一个最简单的方式来为 webpack 配置不同环境变量下的配置文件的方法就是建立多个配置文件。比如：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dev.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 此处官网文档有语法错误，我改了一下
module.exports = function (env) {
  return {
    devtool: &quot;cheap-module-source-map&quot;,
    output: {
      path: path.join(__dirname, &quot;/../dist/assets&quot;),
      filename: &quot;[name].bundle.js&quot;,
      publicPath: publicPath,
      sourceMapFilename: &quot;[name].map&quot;,
    },
    devServer: {
      port: 7777,
      host: &quot;localhost&quot;,
      historyApiFallback: true,
      noInfo: false,
      stats: &quot;minimal&quot;,
      publicPath: publicPath,
    },
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;prod.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = function (env) {
  return {
    output: {
      path: path.join(__dirname, &quot;/../dist/assets&quot;),
      filename: &quot;[name].bundle.js&quot;,
      publicPath: publicPath,
      sourceMapFilename: &quot;[name].map&quot;,
    },
    plugins: [
      new webpack.LoaderOptionsPlugin({
        minimize: true,
        debug: false,
      }),
      new UglifyJsPlugin({
        beautify: false,
        mangle: {
          screw_ie8: true,
          keep_fnames: true,
        },
        compress: {
          screw_ie8: true,
        },
        comments: false,
      }),
    ],
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把我们的 webpack.config.js 的内容改成下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function buildConfig(env) {
  return require(&quot;./config/&quot; + env + &quot;.js&quot;)({ env: env });
}

module.exports = buildConfig(env);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，在 &lt;code&gt;package.json&lt;/code&gt; 中添加如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;build:dev&quot;: &quot;webpack --env=dev --progress --profile --colors&quot;,
 &quot;build:dist&quot;: &quot;webpack --env=prod --progress --profile --colors&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，我们把环境变量传给了 webpack.config.js 文件。从这里我们使用一个简单的方式通过传递环境变量来决定使用正确的配置文件。&lt;/p&gt;
&lt;p&gt;一个更加高级的途径是我们有一个基础配置文件，里面有所有共通的功能，然后在不同环境变量下的不同功能通过指定特定的文件，然后使用 &lt;code&gt;webpack-merge&lt;/code&gt; 来合并成一个完整的配置。这样可以避免写很多
重复的代码。比如，类似与解析 js,ts,png,jpeg 等都是共通的功能，需要放在基础配置文件里面：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;base.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = function () {
  return {
    entry: {
      polyfills: &quot;./src/polyfills.ts&quot;,
      vendor: &quot;./src/vendor.ts&quot;,
      main: &quot;./src/main.ts&quot;,
    },
    output: {
      path: path.join(__dirname, &quot;/../dist/assets&quot;),
      filename: &quot;[name].bundle.js&quot;,
      publicPath: publicPath,
      sourceMapFilename: &quot;[name].map&quot;,
    },
    resolve: {
      extensions: [&quot;&quot;, &quot;.ts&quot;, &quot;.js&quot;, &quot;.json&quot;],
      modules: [path.join(__dirname, &quot;src&quot;), &quot;node_modules&quot;],
    },
    module: {
      loaders: [
        {
          test: /\.ts$/,
          loaders: [&quot;awesome-typescript-loader&quot;, &quot;angular2-template-loader&quot;],
          exclude: [/\.(spec|e2e)\.ts$/],
        },
        {
          test: /\.css$/,
          loaders: [&quot;to-string-loader&quot;, &quot;css-loader&quot;],
        },
        {
          test: /\.(jpg|png|gif)$/,
          loader: &quot;file&quot;,
        },
        {
          test: /\.(woff|woff2|eot|ttf|svg)$/,
          loader: &quot;url-loader?limit=100000&quot;,
        },
      ],
    },
    plugins: [
      new ForkCheckerPlugin(),

      new webpack.optimize.CommonsChunkPlugin({
        name: [&quot;polyfills&quot;, &quot;vendor&quot;].reverse(),
      }),
      new HtmlWebpackPlugin({
        template: &quot;src/index.html&quot;,
        chunksSortMode: &quot;dependency&quot;,
      }),
    ],
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用 &lt;code&gt;webpack-merge&lt;/code&gt; 来合并特定环境变量下指定的配置文件。来看一个合并生产环境下特定配置的例子（和上面 prod.js 对比以下）：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;prod.js(updated)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const webpackMerge = require(&quot;webpack-merge&quot;);

const commonConfig = require(&quot;./base.js&quot;);

module.exports = function (env) {
  return webpackMerge(commonConfig(), {
    plugins: [
      new webpack.LoaderOptionsPlugin({
        minimize: true,
        debug: false,
      }),
      new webpack.DefinePlugin({
        &quot;process.env&quot;: {
          NODE_ENV: JSON.stringify(&quot;prod&quot;),
        },
      }),
      new webpack.optimize.UglifyJsPlugin({
        beautify: false,
        mangle: {
          screw_ie8: true,
          keep_fnames: true,
        },
        compress: {
          screw_ie8: true,
        },
        comments: false,
      }),
    ],
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以注意到，在 &apos;prod.js&apos; 中主要有三处更新，分别是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;• 通过 &apos;webpack-meger&apos; 合并了 `base.js`
• 把 `output` 属性移到了 `base.js` 中。我们只要关心在 `base.js` 中以外的不同的配置就可以了
• 通过 `DefinePlugin` 把 `process.env.NODE_ENV` 设置为 `prod`. 这样，整个应用代码中的 `process.env.NODE_ENV` 都有一个为 `prod` 的值了。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哪些需要在不同的环境变量下保持一致都由你来决定。这里只是通过一个 DEMO 来典型的说明一下如何在不同的环境变量下保持部分配置的统一。&lt;/p&gt;
&lt;p&gt;可以看到，&lt;code&gt;webpack-merge&lt;/code&gt; 是多么强大，可以让我们避免写很多重复的代码（外国人话真多）。&lt;/p&gt;
&lt;h1&gt;React 懒加载（Lazy Loading - React）&lt;/h1&gt;
&lt;p&gt;通过使用高阶函数可以使一个组件懒加载它的依赖而不需要它的消费者知道，或者使用一个接收函数或者模块的组件，可以使一个消费者可以懒加载它的子组件而不需要它的子组件知道。&lt;/p&gt;
&lt;h2&gt;组件懒加载&lt;/h2&gt;
&lt;p&gt;先看一个消费者选择去懒加载一些组件。&lt;code&gt;importLazy&lt;/code&gt; 是一个返回 &lt;code&gt;defualt&lt;/code&gt; 属性的函数，这是为了能和 Babel/ES2015 互通。如果你不需要，可以忽略掉 &lt;code&gt;importLazy&lt;/code&gt; 方法。&lt;code&gt;importLazy&lt;/code&gt; 只是简单的返回了通过 &lt;code&gt;export default&lt;/code&gt; 暴露出的模块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;LazilyLoad
  modules={{
    TodoHandler: () =&amp;gt; importLazy(import(&quot;./components/TodoHandler&quot;)),
    TodoMenuHandler: () =&amp;gt; importLazy(import(&quot;./components/TodoMenuHandler&quot;)),
    TodoMenu: () =&amp;gt; importLazy(import(&quot;./components/TodoMenu&quot;)),
  }}
&amp;gt;
  {({ TodoHandler, TodoMenuHandler, TodoMenu }) =&amp;gt; (
    &amp;lt;TodoHandler&amp;gt;
      &amp;lt;TodoMenuHandler&amp;gt;
        &amp;lt;TodoMenu /&amp;gt;
      &amp;lt;/TodoMenuHandler&amp;gt;
    &amp;lt;/TodoHandler&amp;gt;
  )}
&amp;lt;/LazilyLoad&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;高阶组件（Higher Order Component）&lt;/h2&gt;
&lt;p&gt;作为一个组件，你可以确保整个组件本身的依赖是懒加载的。当一个组件依赖一个非常大的库文件的时候会很有用。假设我们要写一个支持代码高亮的 Todo 组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Todo extends React.Component {
  render() {
    return (
      &amp;lt;div&amp;gt;
        {this.props.isCode ? &amp;lt;Highlight&amp;gt;{content}&amp;lt;/Highlight&amp;gt; : content}
      &amp;lt;/div&amp;gt;
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以确保只有当我们需要代码高亮功能的时候才去加载这个代价高昂的库文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Highlight.js
class Highlight extends React.Component {
  render() {
    const {Highlight} = this.props.highlight;
    // highlight js is now on our props for use
  }
}
export LazilyLoadFactory(Highlight, {
  highlight: () =&amp;gt; import(&apos;highlight&apos;),
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这个 Highlight 组件的消费者是如何在不知情的情况下被懒加载的。&lt;/p&gt;
&lt;h2&gt;完整的代码&lt;/h2&gt;
&lt;p&gt;LazilyLoad 组件的源码，暴露了组件接口和高阶组件接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;

class LazilyLoad extends React.Component {
  constructor() {
    super(...arguments);
    this.state = {
      isLoaded: false,
    };
  }

  componentWillMount() {
    this.load(this.props);
  }

  componentDidMount() {
    this._isMounted = true;
  }

  componentWillReceiveProps(next) {
    if (next.modules === this.props.modules) return null;
    this.load(next);
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  load(props) {
    this.setState({
      isLoaded: false,
    });

    const { modules } = props;
    const keys = Object.keys(modules);

    Promise.all(keys.map((key) =&amp;gt; modules[key]()))
      .then((values) =&amp;gt;
        keys.reduce((agg, key, index) =&amp;gt; {
          agg[key] = values[index];
          return agg;
        }, {}),
      )
      .then((result) =&amp;gt; {
        if (!this._isMounted) return null;
        this.setState({ modules: result, isLoaded: true });
      });
  }

  render() {
    if (!this.state.isLoaded) return null;
    return React.Children.only(this.props.children(this.state.modules));
  }
}

LazilyLoad.propTypes = {
  children: React.PropTypes.func.isRequired,
};

export const LazilyLoadFactory = (Component, modules) =&amp;gt; {
  return (props) =&amp;gt; (
    &amp;lt;LazilyLoad modules={modules}&amp;gt;
      {(mods) =&amp;gt; &amp;lt;Component {...mods} {...props} /&amp;gt;}
    &amp;lt;/LazilyLoad&amp;gt;
  );
};

export const importLazy = (promise) =&amp;gt; promise.then((result) =&amp;gt; result.default);

export default LazilyLoad;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提示&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过使用 bundle loader 可以语义化命名代码块，一次来智能的加载一组代码&lt;/li&gt;
&lt;li&gt;确保你使用了 babel-preset-2015, 并且设置 modules 为 false，这允许 webpack 去处理 modules&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;公开路径？（Public Path）&lt;/h1&gt;
&lt;p&gt;Webpack 提供了一个很长有用的功能，可以设置你应用中所有资源引用的基础路径。它被称之为 &lt;code&gt;publicPath&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;使用场景（Use case）&lt;/h2&gt;
&lt;p&gt;这里有一些真实应用中的场景，通过这个功能来达到目的。&lt;/p&gt;
&lt;h3&gt;在构建的时候设置值&lt;/h3&gt;
&lt;p&gt;在开发模式下，我们通常会把 &lt;code&gt;assets/&lt;/code&gt; 目录放在和入口页同级的目录下面。这样没有问题，但是假如在生产环境下你的静态资源是存放在 CDN 上那又该怎么办呢？&lt;/p&gt;
&lt;p&gt;可以很方便的通过环境变量来解决这个问题。假设我们有一个变量 &lt;code&gt;ASSET_PATH&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这里看起来好像有问题
import webpack from &quot;webpack&quot;;

// Whatever comes as an environment variable, otherwise use root
const ASSET_PATH = process.env.ASSET_PATH || &quot;/&quot;;

export default {
  output: {
    publicPath: ASSET_PATH,
  },

  plugins: [
    // This makes it possible for us to safely use env vars on our code
    new webpack.DefinePlugin({
      &quot;process.env.ASSET_PATH&quot;: JSON.stringify(ASSET_PATH),
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;在开发中设置值（Set Value on the fly）&lt;/h3&gt;
&lt;p&gt;另一种方式是在开发过程成设置 public 路径。Webpack 暴露了一个全局变量 &lt;code&gt;__webpack_public_path__&lt;/code&gt; 来让我们达到这个目的。所以在你的入口文件中，你可以这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__webpack_publick_path__ = process.en.ASSET_PATH;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如何来做都取决于你。当我们通过 &lt;code&gt;DefinePlugin&lt;/code&gt; 进行了配置以后， &lt;code&gt;process.env.ASSET_PATH&lt;/code&gt; 在任何地方都可以直接拿来使用。&lt;/p&gt;
</content:encoded></item><item><title>【翻译向】webpack2 指南（下）</title><link>https://ab.kisnows.com/zh-CN/2017/01/19/webpack2-guide-3/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/01/19/webpack2-guide-3/</guid><pubDate>Thu, 19 Jan 2017 01:14:35 GMT</pubDate><content:encoded>&lt;h1&gt;缓存（Caching）&lt;/h1&gt;
&lt;p&gt;为了能够使 webpack 处理后的静态资源能够长期缓存下来，需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 &lt;code&gt;[chunkhash]&lt;/code&gt; 给每一个文件创建基于内容变化的缓存标识&lt;/li&gt;
&lt;li&gt;在 HTML 文件中引入文件时使用编译状态来拿到文件名称&lt;/li&gt;
&lt;li&gt;在载入资源之前生成 chunk-manifest JSON 文件并写入到 HTML 页面中&lt;/li&gt;
&lt;li&gt;确保包含启动代码的入口代码块的 hash 值不会被修改，当它的依赖没有变化的时候&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;存在的问题&lt;/h2&gt;
&lt;p&gt;每当我们代码中有一些东西需要被更新的时候，它需要在服务上部署然后由客户端重新下载这些文件。当网络状况不太好的时候，这是一件非常低效的事情。这也是为什么浏览器要缓存静态资源的原因。&lt;/p&gt;
&lt;p&gt;这会导致一个陷阱：当我们发布一个新的版本的时候不去更新的文件名，这会让浏览器认为文件没有变化，导致客户端拿不到最新的资源。&lt;/p&gt;
&lt;p&gt;一个简单解决问题的方式就是告诉浏览器一个新的文件名。在没有 webpack 的时候我们会使用构建版本来标识此次的更新：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;application.js?build=1
application.css?build=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 webpack 中这样做也很简单：每一次 webpack 的构建都会生成一个可以用来构成文件名的独一无二的 hash 值。下面这个配置文件会生成两个带有 hash 值的文件名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js
const path = require(&quot;path&quot;);

module.exports = {
  entry: {
    vendor: &quot;./src/vendor.js&quot;,
    main: &quot;./src/index.js&quot;,
  },
  output: {
    path: path.join(__dirname, &quot;build&quot;),
    filename: &quot;[name].[hash].js&quot;,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;执行 webpack 命令会得到以下输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hash: 55e783391098c2496a8f
Version: webpack 1.10.1
Time: 58ms
Asset Size Chunks Chunk Names
main.55e783391098c2496a8f.js 1.43 kB 0 [emitted] main
vendor.55e783391098c2496a8f.js 1.43 kB 1 [emitted] vendor
[0] ./src/index.js 46 bytes {0} [built]
[0] ./src/vendor.js 40 bytes {1} [built]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但存在的问题是，每次我们重新编译，所有的文件名都会变化，这会导致客户端每次都重新把整个应用的代码重新下载一遍。那么我们如何才能时客户端每次只下载有变动的文件。&lt;/p&gt;
&lt;h2&gt;给每个文件生成独特的 hash&lt;/h2&gt;
&lt;p&gt;当一个文件的内容没有变化的时候，如何保证它的文件名不会在每次编译中变化。比如，一个用来放所有的我们的所有依赖库文件的代码包。&lt;/p&gt;
&lt;p&gt;Webpack 允许根据文件的内容生成 hash 值。这是更新后的配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js
module.exports = {
  /*...*/
  output: {
    /*...*/
    filename: &quot;[name].[chunkhash].js&quot;,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个配置同样会生成两个文件，但是每个文件都拥有自己的 hash 值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main.155567618f4367cd1cb8.js 1.43 kB 0 [emitted] main
vendor.c2330c22cd2decb5da5a.js 1.43 kB 1 [emitted] vendor
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;不要在开发环境中使用 [chunkhash]，这会导致每次的编译时间变长。把开发环境和生成环境的配置文件分开，在开发环境使用 [name].js ，在生产环境中使用 [name].[chunkhash].js&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从 webpack 编译状态（compilation stats）中获取文件名
在开发环境中，你只需要在 HTML 中引入你的文件就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;main.js&quot;&amp;gt;&amp;lt;/srcipt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而，在生产环境中，我们每次都会得到一个不同的文件名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&quot;main.12312123t234.js&quot;&amp;gt;&amp;lt;/srcipt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了能够在 HTML 中引入正确的文件，我们需要了解一些构建的信息。这可以通过下面这个插件来从 webpack 编译状态（compliation stats） 中提取出来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js
const path = require(&quot;path&quot;);

module.exports = {
  /*...*/
  plugins: [
    function () {
      this.plugin(&quot;done&quot;, function (stats) {
        require(&quot;fs&quot;).writeFileSync(
          path.join(__dirname, &quot;…&quot;, &quot;stats.json&quot;),
          JSON.stringify(stats.toJson()),
        );
      });
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者，通过下面这些插件来暴露 JSON 文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/webpack-manifest-plugin&quot;&gt;https://www.npmjs.com/package/webpack-manifest-plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/assets-webpack-plugin&quot;&gt;https://www.npmjs.com/package/assets-webpack-plugin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个简单的通过 webpack-manifest-plugin 输出的文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;main.js&quot;: &quot;main.155567618f4367cd1cb8.js&quot;,
  &quot;vendor.js&quot;: &quot;vendor.c2330c22cd2decb5da5a.js&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来的事情就由你的服务来决定了。这有一个很不错的例子  [walk through for Rails-based projects](
walk through for Rails-based projects). 使用 Node.js 的服务端渲染的话可以使用 &lt;a href=&quot;https://github.com/halt-hammerzeit/webpack-isomorphic-tools&quot;&gt;webpack-isomorphic-tools&lt;/a&gt;. 如果你的应用不需要依赖服务端渲染的话，那完全可以直接生成一个 &lt;code&gt;index.html&lt;/code&gt;. 可以使用下面这两个插件来完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ampedandwired/html-webpack-plugin&quot;&gt;https://github.com/ampedandwired/html-webpack-plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/szrenwei/inline-manifest-webpack-plugin&quot;&gt;https://github.com/szrenwei/inline-manifest-webpack-plugin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;确定的 hashes (Deterministic hashes)&lt;/h2&gt;
&lt;p&gt;为了压缩生成文件的大小，webpack 使用 id 代替名字来识别模块。再编译过程中，id 已经被生成，映射到代码块的名字并且放到一个 JavaScript 对象中，被叫做代码块清单（ chunk manifest）。它会被放到入口文件中，确保打包后的文件能够正常工作。&lt;/p&gt;
&lt;p&gt;这会有和之前一样的问题：无论修改任何地方的文件，即使其它地方都没有修改，更新后的入口需要包含清单文件。这会生成一个新的 hash 值，导致我们的入口文件名修改，无法享受长期缓存带来的好处。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们应该使用 &lt;a href=&quot;https://github.com/diurnalist/chunk-manifest-webpack-plugin&quot;&gt;https://github.com/diurnalist/chunk-manifest-webpack-plugin&lt;/a&gt; ，一个可以把清单文件提取出来单独放到一个 JSON 文件中。这是更新后的配置文件，会生成 chunk-manifest.json 并放到我们打包后的文件夹下面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js
var ChunkManifestPlugin = require(&quot;chunk-manifest-webpack-plugin&quot;);

module.exports = {
  // your config values here
  plugins: [
    new ChunkManifestPlugin({
      filename: &quot;chunk-manifest.json&quot;,
      manifestVariable: &quot;webpackManifest&quot;,
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们从入口文件中移除掉清单文件后，那么我们就需要手动把这个文件提供给 webpack 使用。在上面的例子中你可能注意到 &lt;code&gt;manifestVariable&lt;/code&gt; 这个选项。这是一个全局变量，一个供 webpack 来查找清单 JSON 文件，这也是为什么我们需要在代码包前面引入到 HTML 中。在 HTML 中写入 JSON 文件的内容是很容易，那么我们的 HTML 文件的 head 部分就会向下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;script&amp;gt;
      //&amp;lt;![CDATA[
      window.webpackManifest = {
        0: &quot;main.3d038f325b02fdee5724.js&quot;,
        1: &quot;1.c4116058de00860e5aa8.js&quot;,
      };
      //]]&amp;gt;
    &amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的 webpack.config.js 文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var path = require(&quot;path&quot;);
var webpack = require(&quot;webpack&quot;);
var ManifestPlugin = require(&quot;webpack-manifest-plugin&quot;);
var ChunkManifestPlugin = require(&quot;chunk-manifest-webpack-plugin&quot;);
var WebpackMd5Hash = require(&quot;webpack-md5-hash&quot;);

module.exports = {
  entry: {
    vendor: &quot;./src/vendor.js&quot;,
    main: &quot;./src/index.js&quot;,
  },
  output: {
    path: path.join(__dirname, &quot;build&quot;),
    filename: &quot;[name].[chunkhash].js&quot;,
    chunkFilename: &quot;[name].[chunkhash].js&quot;,
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: &quot;vendor&quot;,
      minChunks: Infinity,
    }),
    new WebpackMd5Hash(),
    new ManifestPlugin(),
    new ChunkManifestPlugin({
      filename: &quot;chunk-manifest.json&quot;,
      manifestVariable: &quot;webpackManifest&quot;,
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果使用 &lt;a href=&quot;https://github.com/ampedandwired/html-webpack-plugin&quot;&gt;webpack-html-plugin&lt;/a&gt; ,那么你可以使用 &lt;a href=&quot;https://github.com/szrenwei/inline-manifest-webpack-plugin&quot;&gt; inline-manifest-webpack-plugin &lt;/a&gt; 来做这件事。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用这个配置，那么第三方代码块（vendors chunkd）将不会变化，除非你修改它的代码或者依赖。&lt;/p&gt;
&lt;h1&gt;垫片（Shimming）&lt;/h1&gt;
&lt;p&gt;webpack 作为一个模块打包工具，可以支持的模块系统包括 ES2015 modules, CommonJS 和 AMD. 但是很多情况下，当我们使用第三方库的时候，我们看到他们会依赖一个全局变量比如 &lt;code&gt;$&lt;/code&gt; 或者说 &lt;code&gt;jquery&lt;/code&gt;. 它们也可能创建一些需要暴露出来的新的全局变量。我们来看几种不同的方式来使 webpack 能够理解这些非模块（broken modules）的文件。&lt;/p&gt;
&lt;p&gt;最好使用在 &lt;code&gt;dist&lt;/code&gt; 文件下没有打包压缩过的 CommonJS/AMD 文件（Prefer unminified CommonJS/AMD files over bundled dist versions.）
大多数模块会在 &lt;code&gt;package.json&lt;/code&gt; 的 &lt;code&gt;main&lt;/code&gt; 字段中指定它们的 &lt;code&gt;dist&lt;/code&gt; 版本。这对大多数开发者来说都是非常有用的，对 webpack 来说最好设置一个别名到它们的 &lt;code&gt;src&lt;/code&gt; 目录下面，这样能够使 webpack 更好的优化依赖。但是，在很多情况下使用 &lt;code&gt;dist&lt;/code&gt; 版本也不会有什么大的问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js

module.exports = {
  ...
  resolve: {
  alias: {
    jquery: &quot;jquery/src/jquery&quot;
  }
}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;provide-plugin&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;通过使用 &lt;code&gt;[provide-plugin](https://webpack.js.org/plugins/provide-plugin)&lt;/code&gt; 使这个模块在所有通过 &lt;code&gt;webpack&lt;/code&gt; 引用的模块中作为一个可用的变量。只有当你使用了这个变量后，对应的模块才会被引用进来。很多古老的模块通过使用特定的全局变量，比如 jQuery 的 &lt;code&gt;$&lt;/code&gt; 和或者 &lt;code&gt;jQuery&lt;/code&gt;. 在这个场景下，你可以提前在 webpack 中配置为 &lt;code&gt;var $=require(&apos;jquery&apos;)&lt;/code&gt; ，在每一次遇到全局 &lt;code&gt;$&lt;/code&gt; 标识符。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: &quot;jquery&quot;,
      jQuery: &quot;jquery&quot;,
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;imports-loader&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;[imports-loader](https://webpack.js.org/loaders/imports-loader/)&lt;/code&gt; 将必须要的全局变量插入到传统模块中。比如，一些传统模块依赖 &lt;code&gt;this&lt;/code&gt; 指向 &lt;code&gt;window&lt;/code&gt; 对象。这会导致一个问题，当模块被执行在 CommonJS 上下文的时候， &lt;code&gt;this&lt;/code&gt; 指向为 &lt;code&gt;module.exports&lt;/code&gt;.在这种情况下，你可以通过 &lt;code&gt;imports-loader&lt;/code&gt;重写 &lt;code&gt;this&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  module: {
    rules: [
      {
        test: require.resolve(&quot;some-module&quot;),
        use: &quot;imports-loader?this=&amp;gt;window&quot;,
      },
    ],
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它支持不同的模块类型，比如 AMD，CommonJS 同时也支持传统模块。可是，通常情况下它会去检查 &lt;code&gt;define&lt;/code&gt; 变量，然后使用一些奇怪（quirky）的代码去暴露这些属性。在这种情况下，通过设置 &lt;code&gt;define = false&lt;/code&gt; 来强制 CommonJS 路径可能会有一些帮助。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  module: {
    rules: [
      {
        test: require.resolve(&quot;some-module&quot;),
        use: &quot;imports-loader?define=&amp;gt;false&quot;,
      },
    ],
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;exports-loader&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;假设一个库文件创建了一个全局变量，期待它的消费者去使用。在这种情况下，我们应该使用 &lt;code&gt;[exports-loader](https://webpack.js.org/loaders/exports-loader/)&lt;/code&gt;, 来暴露一个 CommonJS 风格的全局变量。比如，为了暴露 &lt;code&gt;file&lt;/code&gt; 为 &lt;code&gt;file&lt;/code&gt;，&lt;code&gt;helpers.parse&lt;/code&gt; 为 &lt;code&gt;parse&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  module: {
    rules: [
      {
        test: require.resolve(&quot;some-module&quot;),
        use: &quot;exports-loader?file,parse=helpers.parse&quot;,
        // adds below code the the file&apos;s source:
        //  exports[&quot;file&quot;] = file;
        //  exports[&quot;parse&quot;] = helpers.parse;
      },
    ],
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;script-loader&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;[script-loader](https://webpack.js.org/loaders/script-loader/)&lt;/code&gt; 会在全局上下文里面解析代码，就和你在 HTML 中添加了一个 &lt;code&gt;script&lt;/code&gt; 标签一样。在这种情况下，理论上所有的模块都应该正常的运行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个文件会被作为字符串打包在代码中。不会被 &lt;code&gt;webpack&lt;/code&gt; 压缩，所以请使用压缩后的版本。同样这种情况无法使用 webpack 提供的开发工具。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设你有一个 &lt;code&gt;legacy.js&lt;/code&gt; 文件包含：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GLOBAL_CONFIG = {};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用 &lt;code&gt;script-loader&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;require(&quot;script-loader!legacy.js&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般会得到这样的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eval(&quot;GLOBAL_CONFIG = {}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;noParse&lt;/code&gt; 选项&lt;/h2&gt;
&lt;p&gt;当没有 AMD/CommonJS 风格的模块，同时你需要在 &lt;code&gt;dist&lt;/code&gt; 中引入，你可以把这个模块标识为 &lt;code&gt;[noParse](https://webpack.js.org/configuration/module/#module-noparse)&lt;/code&gt;. 这样 &lt;code&gt;webpack&lt;/code&gt; 就只会引入这个模块但是不会去做任何处理，这样也可以减少构建的时间。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;任何需要 AST 支持的，比如 &lt;code&gt;ProvidePlugin&lt;/code&gt;, 都是不会工作的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  module: {
    noParse: /jquery|backbone/,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;编写一个库文件&lt;/h1&gt;
&lt;p&gt;webpack 是一个工具，可以用来打包应用代码，同样也可以用来打包库代码。如果你是一个 JavaScript 库的作者，正在寻找精简打包代码的流程，那么这个章节的内容会对你很有帮助。&lt;/p&gt;
&lt;h2&gt;编写一个库（Author a Library）&lt;/h2&gt;
&lt;p&gt;我们这有一个精简的包装库来把数字 1 到 5 转换到对应的单词，反之亦然。 看起来可能是这样的：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;src/index.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import _ from &quot;lodash&quot;;

import numRef from &quot;./ref.json&quot;;

export function numToWord(num) {
  return _.reduce(
    numRef,
    (accum, ref) =&amp;gt; {
      return ref.num === num ? ref.word : accum;
    },
    &quot;&quot;,
  );
}

export function wordToNum(word) {
  return _.reduce(
    numRef,
    (accum, ref) =&amp;gt; {
      return ref.word === word &amp;amp;&amp;amp; word.toLowerCase() ? ref.num : accum;
    },
    -1,
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;库的使用规范如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//ES2015modules

import*aswebpackNumbersfrom&apos;webpack-numbers&apos;;

...
webpackNumbers.wordToNum(&apos;Two&apos;)//outputis2
...

//CommonJSmodules

varwebpackNumbers=require(&apos;webpack-numbers&apos;);

...
webpackNumbers.numToWord(3);//outputisThree
...

//Asascripttag

&amp;lt;html&amp;gt;
...
&amp;lt;scriptsrc=&quot;https://unpkg.com/webpack-numbers&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
...
/*webpackNumbersisavailableasaglobalvariable*/
webpackNumbers.wordToNum(&apos;Five&apos;)//outputis5
...
&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的库配置和代码放在这里 &lt;a href=&quot;https://github.com/kalcifer/webpack-library-example&quot;&gt; webpack-library-example&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;配置 webpack&lt;/h2&gt;
&lt;p&gt;那么接下来的事情就是打包这个库
• 不打包 lodash，但是会被它的消费者引入
• 命名这个库为 &lt;code&gt;webpack-numbers&lt;/code&gt;, 并且变量为 &lt;code&gt;webpackNumbers&lt;/code&gt;
• 库可以通过 &lt;code&gt;import webpackNumbers from &apos;webpack-numbers&apos;&lt;/code&gt; 或者 &lt;code&gt;require(&apos;webpack-numbers&apos;)&lt;/code&gt; 来引入
• 当通过 &lt;code&gt;script&lt;/code&gt; 标签引入的时候，可以通过全局变量 &lt;code&gt;webpackNumbers&lt;/code&gt; 来访问
• 可以在 Node.js 中使用&lt;/p&gt;
&lt;h3&gt;添加 webpack&lt;/h3&gt;
&lt;p&gt;添加基础 webpack 配置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  entry: &quot;./src/index.js&quot;,
  output: {
    path: &quot;./dist&quot;,
    filename: &quot;webpack-numbers.js&quot;,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将添加一个基础配置来打包这个库。&lt;/p&gt;
&lt;h3&gt;添加 Loaders&lt;/h3&gt;
&lt;p&gt;但是如果没有对应 loaders 去解析代码是没有办法工作的。这里，我们添加 &lt;code&gt;json-loader&lt;/code&gt; 来添加对 json 文件的解析。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  // ...
  module: {
    rules: [{
      test: /.json$/,
      use: &apos;json-loader&apos;
    }]
  }
};
 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加 &lt;code&gt;externals&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;现在，如果执行 &lt;code&gt;webpack&lt;/code&gt; 命令，你会发现一个体积较大的代码包被生成。如果你去检查代码，会发现 lodash 被打包到了代码包中。对于你的库来说把 &lt;code&gt;lodash&lt;/code&gt; 打包在一起是完全没有必要的。&lt;/p&gt;
&lt;p&gt;可以通过 &lt;code&gt;externals&lt;/code&gt; 配置：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  ...
    externals: {
  &quot;lodash&quot;: {
    commonjs: &quot;lodash&quot;,
      commonjs2: &quot;lodash&quot;,
      amd: &quot;lodash&quot;,
      root: &quot;_&quot;
  }
}
...
};
 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这意味着在使用者的环境下你的库会期望依赖 &lt;code&gt;lodash&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;添加 &lt;code&gt;libraryTarget&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;为了这个库能够被广泛的使用，我们需要让它在不同的环境下有相同的表现。比如， CommonJS，AMD，Node.js 或者作为一个全局变量。&lt;/p&gt;
&lt;p&gt;为了达到这个目的，需要在 webpack 配置中添加 &lt;code&gt;library&lt;/code&gt; 属性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  ...
    output: {
...
  library: &apos;webpackNumbers&apos;
}
...
};
 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这能够让你的库被引入的时候可以作为一个全局变量被访问。为了能够在其它情况下使用，在配置中继续添加 &lt;code&gt;libraryTarget&lt;/code&gt; 的值：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  ...
    output: {
...
  library: &apos;webpackNumbers&apos;,
    libraryTarget:&apos;umd&apos; // Possible value - amd, commonjs, commonjs2, commonjs-module, this, var
}
...
};
 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;library&lt;/code&gt; 设置了，但是 &lt;code&gt;libraryTarget&lt;/code&gt;没有配置，那么 &lt;code&gt;libraryTarget&lt;/code&gt; 默认为 &lt;code&gt;var&lt;/code&gt; 就像在&lt;a href=&quot;https://webpack.js.org/configuration/output&quot;&gt; config reference&lt;/a&gt; 中指定的一样。&lt;/p&gt;
&lt;h3&gt;最后一步&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://webpack.js.org/guides/production-build&quot;&gt;调整生产环境下的配置文件&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;将打包后的文件添加到 &lt;code&gt;package.json&lt;/code&gt; 中指定的字段里面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;package.json&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
{
    ...
    &quot;main&quot;: &quot;dist/webpack-numbers.js&quot;,
    &quot;module&quot;: &quot;src/index.js&quot;, // To add as standard module as per https://github.com/dherman/defense-of-dot-js/blob/master/proposal.md#typical-usage
    ...
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在你可以把它作为一个 npm 模块发布了，并且在 unpkg.com 里面向你的用户传播了。&lt;/p&gt;
</content:encoded></item><item><title>【翻译向】Webpack 1 到 2 升级指南</title><link>https://ab.kisnows.com/zh-CN/2017/01/20/webpack2-migrating-v1-v2/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2017/01/20/webpack2-migrating-v1-v2/</guid><pubDate>Fri, 20 Jan 2017 01:00:33 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;code&gt;resolve.root&lt;/code&gt;,&lt;code&gt;resolve.fallback&lt;/code&gt;,&lt;code&gt;resolve.modulesDirectories&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;这些配置项项都被一个单独的配置所替代 &lt;code&gt;resolve.modules&lt;/code&gt;. 查看 &lt;a href=&quot;https://webpack.js.org/configuration/resolve&quot;&gt;resolving&lt;/a&gt; 了解更多。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resolve: {
-   root: path.join(__dirname, &quot;src&quot;)
+   modules: [
+     path.join(__dirname, &quot;src&quot;),
+     &quot;node_modules&quot;
+   ]
}
 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;resolve.extensions&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;这个选项已经不再需要传一个空字符串了。它的行为已经被移动到了 &lt;code&gt;resolve.enforceExtension&lt;/code&gt;. 查看 &lt;a href=&quot;https://webpack.js.org/configuration/resolve&quot;&gt;resolving&lt;/a&gt; 了解更多。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;resolve.*&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;这里修改了很多内容。因为被用到的不是很多，所以就不一一列举出来了。查看 &lt;a href=&quot;https://webpack.js.org/configuration/resolve&quot;&gt;resolving&lt;/a&gt; 了解更多。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;module.loaders&lt;/code&gt; 现在是 &lt;code&gt;module.rules&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;老的 loader 配置被一个更加强大的 rules 系统所取代了，允许配置 loader 和更多内容。为了保证一致性，旧的 &lt;code&gt;module.loaders&lt;/code&gt; 语法依然可以合法且可以使用的。新的命名规范更加易于理解，这是一个使用 &lt;code&gt;module.rules&lt;/code&gt; 去升级配置的很好的理由。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  module: {
-   loaders: [
+   rules: [
      {
        test: /\.css$/,
-       loaders: [
+       use: [
          {
            loader: &quot;style-loader&quot;
          },
          {
            loader: &quot;css-loader&quot;,
-           query: {
+           options: {
              modules: true
            }
        ]
      },
      {
        test: /\.jsx$/,
        loader: &quot;babel-loader&quot;, // Do not use &quot;use&quot; here
        options: {
          // ...
        }
      }
    ]
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;把 loader 链接起来（Chaining loaders）&lt;/h2&gt;
&lt;p&gt;就像在 webpack1 中那样，loader 可以被链接起来，把上一个 loader 的结果传递给下一个 loader. 使用 &lt;a href=&quot;https://webpack.js.org/configuration/module#rule-use&quot;&gt;rule.use&lt;/a&gt;， &lt;code&gt;use&lt;/code&gt; 可以设置为一个 loaders 的列表。在 webpack1 中，loader 通过 &lt;code&gt;!&lt;/code&gt; 来链接再一起。现在只会在 &lt;code&gt;module.loaders&lt;/code&gt; 里面才会支持这样的风格。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  module: {
-   loaders: {
+   rules: {
      test: /\.less$/,
-     loader: &quot;style-loader!css-loader!less-loader&quot;
+     use: [
+       &quot;style-loader&quot;,
+       &quot;css-loader&quot;,
+       &quot;less-loader&quot;
+     ]
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自动添加 &lt;code&gt;-loader&lt;/code&gt; 后辍的特性被移除了&lt;/h2&gt;
&lt;p&gt;在引入 loader 的时候不能再省略后面的 &lt;code&gt;-loader&lt;/code&gt; 了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module: {
  rules: [
    {
      use: [
        -&quot;style&quot;,
        +&quot;style-loader&quot;,
        -&quot;css&quot;,
        +&quot;css-loader&quot;,
        -&quot;less&quot;,
        +&quot;less-loader&quot;,
      ],
    },
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过依旧可以使用 &lt;code&gt;resolveLoader.moduleExtensions&lt;/code&gt; 配置来达到以前的目的，但是我们不推荐这样做。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ resolveLoader: {
+   moduleExtensions: [&quot;-loader&quot;]
+ }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不再需要 &lt;code&gt;json-loader&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;当没有给 json 文件配置对应的 loader 的时候，webpack 会自动使用 &lt;code&gt;json-loader&lt;/code&gt; 来载入 JSON 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  module: {
    rules: [
-     {
-       test: /\.json/,
-       loader: &quot;json-loader&quot;
-     }
    ]
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;webpack 选择这样做的目的是想要抹平在 webpack，nodejs 和 browserify 不同环境下的区别。&lt;/p&gt;
&lt;h2&gt;配置中的 loaders 会相对于上下文来解析（Loaders in configuration resolve relative to context）&lt;/h2&gt;
&lt;p&gt;在 webpack1 中配置的 loader 会相对于匹配到的文件解析。但是 webpack2 中配置的 loader 会根据 &lt;code&gt;context&lt;/code&gt; 的设置来解析。&lt;/p&gt;
&lt;p&gt;这解决了当由于使用 &lt;code&gt;npm link&lt;/code&gt; 或者引用 &lt;code&gt;context&lt;/code&gt; 之外导致模块重复引入的问题。&lt;/p&gt;
&lt;p&gt;你可能会通过 hack 的方式解决这个问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  module: {
    rules: [
      {
        // ...
-       loader: require.resolve(&quot;my-loader&quot;)
+       loader: &quot;my-loader&quot;
      }
    ]
  },
  resolveLoader: {
-   root: path.resolve(__dirname, &quot;node_modules&quot;)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;module.preLoades&lt;/code&gt; 和 &lt;code&gt;module.postLoaders&lt;/code&gt; 被移除了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  module: {
-   preLoaders: [
+   rules: [
      {
        test: /\.js$/,
+       enforce: &quot;pre&quot;,
        loader: &quot;eslint-loader&quot;
      }
    ]
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;UglifyJsPlugin&lt;/code&gt; sourceMap&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;UglifyJsPlugin&lt;/code&gt; 中 &lt;code&gt;sourceMap&lt;/code&gt; 的默认选项从 &lt;code&gt;true&lt;/code&gt; 改为了 &lt;code&gt;false&lt;/code&gt;. 这意味着如果你需要在压缩后的代码中使用 sourceMap 的功能，需要手动去配置 &lt;code&gt;sourceMap:false&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  devtool: &quot;source-map&quot;,
  plugins: [
    new UglifyJsPlugin({
+     sourceMap: true
    })
  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;UglifyJsPlugin&lt;/code&gt; 警告&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;UglifyJsPlugin&lt;/code&gt; 中 &lt;code&gt;compress.warnings&lt;/code&gt; 和上一条一样，默认选项从 &lt;code&gt;true&lt;/code&gt; 改为了 &lt;code&gt;false&lt;/code&gt;.如果需要看到 uglifyjs 的警告，需要设置 &lt;code&gt;compress.warnings&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  devtool: &quot;source-map&quot;,
  plugins: [
    new UglifyJsPlugin({
+     compress: {
+       warnings: true
+     }
    })
  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;UglifyJsPlugin&lt;/code&gt; 压缩 loader（minimize loaders）&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;UglifyJsPlugin&lt;/code&gt; 不会在压缩模式下切换 loaders 了。&lt;code&gt;minimize: true&lt;/code&gt; 需要一直被设置在配置来传给 loader 了，查看文档以了解更多的内容。&lt;/p&gt;
&lt;p&gt;在 webpack3 或者后续的版本中，loaders 的压缩模式这个特性会被移除掉。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;BannerPlugin&lt;/code&gt; - breaking change&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;BannerPlugin&lt;/code&gt; 不再支持传递两个参数，而是传递一个对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  plugins: [
-    new webpack.BannerPlugin(&apos;Banner&apos;, {raw: true, entryOnly: true});
+    new webpack.BannerPlugin({banner: &apos;Banner&apos;, raw: true, entryOnly: true});
  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;OccurrenceOrderPlugin&lt;/code&gt; 被作为默认设置&lt;/h2&gt;
&lt;p&gt;不再需要手动去配置了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  plugins: [
-   new webpack.optimize.OccurrenceOrderPlugin()
  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;ExtractTextWebpackPlugin&lt;/code&gt; - breaking change&lt;/h2&gt;
&lt;p&gt;ExtractTextWebpackPlugin 1.0.0 无法在 webpack2 中正常工作，需要安装 ExtractTextPlugin V2 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --save-dev extract-text-webpack-plugin@beta
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置的区别主要是在语法上。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ExtractTextPlugin.extract&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module: {
  rules: [
    test: /.css$/,
-    loader: ExtractTextPlugin.extract(&quot;style-loader&quot;, &quot;css-loader&quot;, { publicPath: &quot;/dist&quot; })
+    loader: ExtractTextPlugin.extract({
+      fallbackLoader: &quot;style-loader&quot;,
+      loader: &quot;css-loader&quot;,
+      publicPath: &quot;/dist&quot;
+    })
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;new ExtractTextPlugin({options})&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plugins: [
-  new ExtractTextPlugin(&quot;bundle.css&quot;, { allChunks: true, disable: false })
+  new ExtractTextPlugin({
+    filename: &quot;bundle.css&quot;,
+    disable: false,
+    allChunks: true
+  })
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;全动态 require 会默认失败（Full dynamic requires now fail by default）&lt;/h2&gt;
&lt;p&gt;一个由表达式决定的依赖会创建一个空的上下文来替代之前创建的一个包含完成文件夹的上下文。&lt;/p&gt;
&lt;p&gt;最好重构这里的代码，因为它不会在 ES2015 模块里面工作。如果这对你来说不太可能，可以使用 &lt;code&gt;ContextReplacementPlugin&lt;/code&gt; 来指定编译器找到正确的地址。&lt;/p&gt;
&lt;h2&gt;在 CLI 和配置文件中使用自定义参数&lt;/h2&gt;
&lt;p&gt;如果你滥用 CLI 参数去传递自定义参数到配置中，像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;webpack --custom-stuff
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// webpack.config.js
var customStuff = process.argv.indexOf(&quot;--custom-stuff&quot;) &amp;gt;= 0;
/* ... */
module.exports = config;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会发现这样不再被允许了，现在 CLI 比之前更加严格了。&lt;/p&gt;
&lt;p&gt;作为替代，现在有一个接口用来给配置传递参数。未来的工具会基于这个。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;webpack --env.customStuff
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;module.exports = function (env) {
  var customStuff = env.customStuff;
  /* ... */
  return config;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看 &lt;a href=&quot;https://webpack.js.org/api/cli&quot;&gt;CLI&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;require.ensure&lt;/code&gt; 和 AMD &lt;code&gt;require&lt;/code&gt; 是异步的了&lt;/h2&gt;
&lt;p&gt;这些函数变成了异步的，代替之前如果代码块已经被加载后它们的回调函数会同步执行的行为。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;require.ensure&lt;/code&gt; 现在依赖于原生的 &lt;code&gt;Promise&lt;/code&gt;, 如果在一个不支持 &lt;code&gt;Promise&lt;/code&gt; 的环境下使用 &lt;code&gt;require.ensure&lt;/code&gt; ，那么你需要一个 polyfill.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Loader 的配置要通过 &lt;code&gt;options&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;不能再通过在 &lt;code&gt;webpack.config.js&lt;/code&gt; 中一个自定义的属性来配置 loader 了。必须通过 &lt;code&gt;options&lt;/code&gt;. 下面这个 &lt;code&gt;ts&lt;/code&gt; 属性的配置在 webpack2 中已经不合法了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  ...
    module: {
  rules: [{
    test: /\.tsx?$/,
    loader: &apos;ts-loader&apos;
  }]
},
// does not work with webpack 2
ts: { transpileOnly: false }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;什么是 &lt;code&gt;options&lt;/code&gt;？&lt;/h2&gt;
&lt;p&gt;好问题。严格的说，它可能是两个东西；都是用来配置一个 loader 的方式。典型的 &lt;code&gt;options&lt;/code&gt; 被称为 &lt;code&gt;query&lt;/code&gt;, 是一个可以被添加 loader 名字后面的字符串。更像一个 查询字符（query string），但实际上是 &lt;a href=&quot;https://github.com/webpack/loader-utils#parsequery&quot;&gt;greater powers&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  ...
  module: {
  rules: [{
    test: /\.tsx?$/,
    loader: &apos;ts-loader?&apos; + JSON.stringify({ transpileOnly: false })
   }]
  }
}
 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样可以是一个指定的对象，和 loader 一同提供：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  ...
    module: {
  rules: [{
    test: /\.tsx?$/,
    loader: &apos;ts-loader&apos;
    options:  { transpileOnly: false }
  }]
}
}
 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;LoaderOptionsPlugin&lt;/code&gt; 上下文&lt;/h2&gt;
&lt;p&gt;一些 loader 需要从配置文件中读取上下文信息。这个需要长期的设置在 loader 的选项中。&lt;/p&gt;
&lt;p&gt;为了兼容老的 loader，可以通过这个插件来传递给 loader：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plugins: [
  +   new webpack.LoaderOptionsPlugin({
    +     options: {
  +       context: __dirname
  +     }
+   })
]
 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;debug&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;在 webpack1 中 &lt;code&gt;debug&lt;/code&gt; 选项用来把 loader 切换到 debug 模式。&lt;/p&gt;
&lt;p&gt;在 webpack3 或更后面的版本中，这个模式会被移除掉。&lt;/p&gt;
&lt;p&gt;为了兼容老的 loader，可以通过这个插件来传递参数给 loader：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- debug: true,
  plugins: [
+   new webpack.LoaderOptionsPlugin({
+     debug: true
+   })
]

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ES2015 代码分割&lt;/h2&gt;
&lt;p&gt;在 webpack1 中，可以通过 &lt;code&gt;require.ensure&lt;/code&gt; 来懒加载一些代码块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require.ensure([], function (require) {
  var foo = require(&quot;./module&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 ES2015 中我们使用 &lt;code&gt;import()&lt;/code&gt; 作为一个方法在运行时动态的加载 ES2015.&lt;/p&gt;
&lt;p&gt;webpack 会把 &lt;code&gt;import()&lt;/code&gt; 作为一个分割点，并且把加载的代码分离出来作为一个单独的代码块。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;import()&lt;/code&gt; 需要一个模块的名称作为参数，并返回一个 Promise 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function onClick() {
  import(&quot;./module&quot;)
    .then((module) =&amp;gt; {
      return module.default;
    })
    .catch((err) =&amp;gt; {
      console.log(&quot;Chunk loading failed&quot;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好消息：现在代码块加载失败可以被处理了，因为它们是基于 Promise 的。&lt;/p&gt;
&lt;p&gt;警告（Caveat）：&lt;code&gt;require.ensure&lt;/code&gt; 可以方便的指定代码块的名字，通过第三个参数，但是 &lt;code&gt;import&lt;/code&gt; API 还不支持这个特性。如果你还依赖这个功能，那么你可以继续使用 &lt;code&gt;require.ensure&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require.ensure(
  [],
  function (require) {
    var foo = require(&quot;./module&quot;);
  },
  &quot;custom-chunk-name&quot;,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想要和 Babel 结合使用 &lt;code&gt;import&lt;/code&gt;, 你需要安装 &lt;a href=&quot;http://babeljs.io/docs/plugins/syntax-dynamic-import/&quot;&gt;dynamic-import&lt;/a&gt; 这个还处于 Stage 3 的语法插件来绕过语法解析错误。当这个提案被加到标准里面后，就不需要这么做了。&lt;/p&gt;
&lt;h2&gt;动态表达式（Dynamic expressions）&lt;/h2&gt;
&lt;p&gt;很有可能你会需要传递一个表达式给 &lt;code&gt;import()&lt;/code&gt;. 这里处理的模式跟 CommonJS 中很相似。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;import()&lt;/code&gt; 创建一个单独的代码块给每个可能的模块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function route(path, query) {
  return import(`./routes/${path}/route`).then(
    (route) =&amp;gt; new route.Route(query),
  );
}
// This creates a separate chunk for each possible route
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;混合使用 ES2015，AMD 和 CommonJS&lt;/h2&gt;
&lt;p&gt;如果是 AMD 和 CommonJS ，那么它们完全可以很自由的混合使用。在这种情况下，Webpack 的处理的行为和 babel 或者 node-esm 相似。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// CommonJS consuming ES2015 Module
var book = require(&quot;./book&quot;);

book.currentPage;
book.readPage();
book.default === &quot;This is a book&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// ES2015 Module consuming CommonJS
// module.exports map to default
import fs, { readFileSync } from &quot;fs&quot;;

// named exports are read from returned object+

typeof fs.readFileSync === &quot;function&quot;;
typeof readFileSync === &quot;function&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很重要的一点是，你需要告诉 babel 不要解析这些模块符号（module symbols），这样 webpack 才能使用它们。在 &lt;code&gt;.babelrc&lt;/code&gt; 中这样配置就可以了：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;babelrc&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;presets&quot;: [
    [&quot;es2015&quot;, { &quot;modules&quot;: false }]
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提示&lt;/h2&gt;
&lt;p&gt;不需要修改什么，但是会很方便。&lt;/p&gt;
&lt;h3&gt;字符串模板&lt;/h3&gt;
&lt;p&gt;webpack 现在支持在表达式中使用字符串模板了。这意味着可以在 webpack constructs 中使用他们：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- require(&quot;./templates/&quot; + name);
+ require(`./templates/${name}`);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Configuration Promise&lt;/h3&gt;
&lt;p&gt;webpack 现在支持从配置文件中返回一个 &lt;code&gt;Promise&lt;/code&gt; 了。这意味着我们可以在配置文件中做一些异步的处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;webpack.config.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = function () {
  return fetchLangs().then((lang) =&amp;gt; ({
    entry: &quot;...&quot;,
    // ...
    plugins: [new DefinePlugin({ LANGUAGE: lang })],
  }));
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;高级 loader 匹配&lt;/h3&gt;
&lt;p&gt;webpack 现在支持更多的方式来让 loader 去匹配文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module: {
  rules: [
    {
      resource: /filename/, // matches &quot;/path/filename.js&quot;
      resourceQuery: /querystring/, // matches &quot;/filename.js?querystring&quot;
      issuer: /filename/, // matches &quot;/path/something.js&quot; if requested from &quot;/path/filename.js&quot;
    },
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更多的命令行选项&lt;/h3&gt;
&lt;p&gt;添加了一些新的命令行选项：
&lt;code&gt;--define process.env.NODE_ENV=&quot;production&quot;&lt;/code&gt; ,查看 &lt;code&gt;DefinePlugin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--display-depth&lt;/code&gt; 展示每个模块和入口的距离&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--display-used-exports&lt;/code&gt; 展示模块使用了那个模块的暴露的信息&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--display-max-modules&lt;/code&gt; 设置输出中展示的最大模块数量&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-p&lt;/code&gt; 同样设置了 &lt;code&gt;process.env.NODE_ENV&lt;/code&gt; 为 &quot;production&quot;&lt;/p&gt;
&lt;h3&gt;Loader 修改&lt;/h3&gt;
&lt;p&gt;只和 loader 的作者有关。&lt;/p&gt;
&lt;h3&gt;可缓存（Cacheable）&lt;/h3&gt;
&lt;p&gt;Loader 现在默认是可以缓存的。Loader 必须选择 return 如果它们是不可缓存的话。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // Cacheable loader
  module.exports = function(source) {
-   this.cacheable();
    return source;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;  // Not cacheable loader
  module.exports = function(source) {
+   this.cacheable(false);
    return source;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;复杂的选项&lt;/h3&gt;
&lt;p&gt;webpack1 只支持可以 &lt;code&gt;JSON.stringify&lt;/code&gt; 选项的 loader. webpack2 支持所有的 plugins 接收一个 JS 对象作为参数。&lt;/p&gt;
&lt;p&gt;使用复杂的选项会带来一个限制。需要给设置对象一个 &lt;code&gt;ident&lt;/code&gt; 使他可以被其它 loaders 引用。&lt;/p&gt;
&lt;p&gt;拥有一个 &lt;code&gt;ident&lt;/code&gt; 在选项对象上，意味着他可以被其它 inline loader 引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require(&apos;some-loader??by-ident!resource&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  test: /.../,
  loader: &quot;...&quot;,
  options: {
    ident: &quot;by-ident&quot;,
    magic: () =&amp;gt; return Math.random()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种 inline 写法不应该经常使用，但是它可以被 loader 生成的代码使用。例子：style-loader 生成一个模块，&lt;code&gt;require&lt;/code&gt; 了剩下的请求（比如用来暴露 CSS）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// style-loader generated code (simplified)
var addStyle = require(&quot;./add-style&quot;);
var css = require(&quot;-!css-loader?{&quot;modules&quot;:true}!postcss-loader??postcss-ident&quot;);

addStyle(css);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以如果你要使用复杂的选项，那么告诉你的用户有关 &lt;code&gt;ident&lt;/code&gt; 的事情。&lt;/p&gt;
</content:encoded></item><item><title>知乎前端面试题解答</title><link>https://ab.kisnows.com/zh-CN/2015/01/15/zhihu-interview-question/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2015/01/15/zhihu-interview-question/</guid><pubDate>Thu, 15 Jan 2015 07:33:06 GMT</pubDate><content:encoded>&lt;h3&gt;第一题：请使用 Javascript 实现以下动态效果&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;可以使用任何你喜欢的方式和库，尽量使用你认为优雅的实现，并在代码完成说简要说明此方案的优点和不足。
这是我用原生&lt;code&gt;js&lt;/code&gt;实现的，用到了&lt;code&gt;input&lt;/code&gt;标签的&lt;code&gt;readonly&lt;/code&gt;属性，实现如下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;title&amp;gt;第一题&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        width: 900px;
        margin: 0 auto;
        line-height: 30px;
      }
      a {
        text-decoration: none;
      }
      .head {
        background-color: #059ed3;
      }
      .row {
        height: 30px;
        border-bottom: 1px solid #888;
      }
      input {
        margin-left: 10px;
        margin-top: 6px;
        border: none;
        outline: none;
        float: left;
      }
      .title {
        border: 1px solid #999;
      }
      a {
        float: right;
      }
      ###add {
        background-color: #cccccc;
        width: 100%;
        height: 30px;
      }
      ###add a {
        display: block;
        text-align: right;
        font-size: 14px;
        font-weight: bolder;
        color: #008000;
        margin-right: 10px;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;head&quot;&amp;gt;现在共有:&amp;lt;strong&amp;gt;4&amp;lt;/strong&amp;gt; 个条目&amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;list&quot;&amp;gt;
      &amp;lt;div class=&quot;row&quot;&amp;gt;
        &amp;lt;input type=&quot;text&quot; value=&quot;标题&quot; /&amp;gt;&amp;lt;a href=&quot;javascript:;&quot; class=&quot;change&quot;
          &amp;gt;修改&amp;lt;/a
        &amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;row&quot;&amp;gt;
        &amp;lt;input type=&quot;text&quot; value=&quot;标题&quot; /&amp;gt;&amp;lt;a href=&quot;javascript:;&quot; class=&quot;change&quot;
          &amp;gt;修改&amp;lt;/a
        &amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;row&quot;&amp;gt;
        &amp;lt;input type=&quot;text&quot; value=&quot;标题&quot; /&amp;gt;&amp;lt;a href=&quot;javascript:;&quot; class=&quot;change&quot;
          &amp;gt;修改&amp;lt;/a
        &amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;row&quot;&amp;gt;
        &amp;lt;input type=&quot;text&quot; value=&quot;标题&quot; /&amp;gt;&amp;lt;a href=&quot;javascript:;&quot; class=&quot;change&quot;
          &amp;gt;修改&amp;lt;/a
        &amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;add&quot;&amp;gt;&amp;lt;a href=&quot;javascript:;&quot;&amp;gt;新增条目&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script&amp;gt;
      window.onload = function () {
        change();
        add();
      };
      change = function () {
        var list = document.getElementById(&quot;list&quot;);
        var titles = list.getElementsByTagName(&quot;input&quot;);
        var changes = list.getElementsByTagName(&quot;a&quot;);
        for (var i = titles.length - 1; i &amp;gt;= 0; i--) {
          titles[i].readOnly = &quot;readonly&quot;;
        }
        for (var j = changes.length - 1; j &amp;gt;= 0; j--) {
          changes[j].index = j;
          changes[j].onclick = function () {
            if (this.text == &quot;修改&quot;) {
              this.text = &quot;保存&quot;;
              titles[this.index].readOnly = &quot;&quot;;
              titles[this.index].className = &quot;title&quot;;
            } else {
              this.text = &quot;修改&quot;;
              titles[this.index].readOnly = &quot;readonly&quot;;
              titles[this.index].className = &quot;&quot;;
            }
          };
        }
      };
      add = function () {
        var addBtn = document
          .getElementById(&quot;add&quot;)
          .getElementsByTagName(&quot;a&quot;)[0];
        addBtn.onclick = function () {
          var newRow = document.createElement(&quot;div&quot;);
          newRow.className = &quot;row&quot;;
          newRow.innerHTML =
            &apos;&amp;lt;input type=&quot;text&quot; value=&quot;未命名&quot; /&amp;gt;&amp;lt;a href=&quot;javascript:;&quot; class=&quot;change&quot;&amp;gt;修改&amp;lt;/a&amp;gt;&apos;;
          var list = document.getElementById(&quot;list&quot;);
          // console.log(&quot;click&quot;,list);
          // e = e || window.event;
          list.appendChild(newRow);
          change();
          // e.cancelBubble = true;
          update();
        };
      };
      update = function () {
        var list = document.getElementById(&quot;list&quot;);
        var lth = list.getElementsByTagName(&quot;div&quot;).length;
        var count = document.getElementsByTagName(&quot;strong&quot;)[0];
        count.innerHTML = lth;
      };
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第二题：请说明要输出正确的 myName 的值要如何修改程序?并解释原因&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;foo = function () {
  this.myName = &quot;Foo function.&quot;;
};
foo.prototype.sayHello = function () {
  alert(this.myName);
};
foo.prototype.bar = function () {
  setTimeout(this.sayHello, 1000);
};
var f = new foo();
f.bar();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;code&gt;this&lt;/code&gt;指向的是当前调用这个方法的对象,代码中&lt;code&gt;setTimeout&lt;/code&gt;是一个全局函数,全写是&lt;code&gt;window.setTimeout&lt;/code&gt;，所以&lt;code&gt;setTimeout(func,time)&lt;/code&gt;中的 func 参数中的 this 应该指向 window。可以用如下的修改方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foo.prototype.bar = function () {
  var That = this;
  setTimeout(function () {
    That.sayHello();
  }, 1000);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第三题：请按下列要求写出相应的 Html 和 CSS&lt;/h3&gt;
&lt;p&gt;现有并列的三列布局结构，从左至右依次为 A, B, C, 宽度分别为 180px, 600px, 180px。要求在不改变 Html 结构的情况下用 CSS 实现：ABC，CBA，BAC 三种布局及在 CBA 排列下使 B 宽度自适应（三列总宽度 100%），不能使用针对浏览器的 CSS Hack.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;zh-cn&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;title&amp;gt;第三题&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
      .content {
        width: auto;
        margin: 0 auto;
        position: relative;
        height: 50px;
        overflow: auto;
      }
      .a {
        background-color: red;
        width: 160px;
      }
      .b {
        background-color: blue;
        width: 600px;
      }
      .c {
        background-color: yellow;
        width: 160px;
      }
      .a,
      .b,
      .c {
        height: 50px;
      }
      /*		ABC*/
      .a1,
      .b1,
      .c1 {
        float: left;
      }
      /*		CBA*/
      .c2 {
        position: absolute;
        top: 0;
        left: 0;
      }
      .a2 {
        float: right;
      }
      .b2 {
        margin-left: 160px;
        width: auto;
      }
      /*		BAC*/
      .b3 {
        float: left;
      }
      .a3 {
        position: absolute;
        top: 0;
        left: 600px;
      }
      .c3 {
        float: right;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;p&amp;gt;
      从左至右依次为 A, B, C, 宽度分别为180px, 600px,
      180px。用CSS实现：ABC，CBA，BAC
      三种布局及在CBA排列下使B宽度自适应（三列总宽度100%）.
    &amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;ABC，结构&amp;lt;/p&amp;gt;
    &amp;lt;div class=&quot;content&quot;&amp;gt;
      &amp;lt;div class=&quot;a a1&quot;&amp;gt;A&amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;b b1&quot;&amp;gt;B&amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;c c1&quot;&amp;gt;C&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;p&amp;gt;CBA,B宽度自适应&amp;lt;/p&amp;gt;
    &amp;lt;div class=&quot;content&quot;&amp;gt;
      &amp;lt;div class=&quot;a a2&quot;&amp;gt;A&amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;b b2&quot;&amp;gt;B&amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;c c2&quot;&amp;gt;C&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;p&amp;gt;BAC结构&amp;lt;/p&amp;gt;
    &amp;lt;div class=&quot;content&quot;&amp;gt;
      &amp;lt;div class=&quot;a a3&quot;&amp;gt;A&amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;b b3&quot;&amp;gt;B&amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;c c3&quot;&amp;gt;C&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;第一题中，因为用到了 readonly 属性，实现起来比较简单。但是根据原题目的演示，猜测到他的实现方法是动态的添加 input 标签，同时控制 input 的 display 属性来实现，感觉太麻烦就没有用那种方法。
第二题中，关于 js 中 this 的指向问题，我的理解还是不够深刻，需要继续深入了解。
第三题，很轻松的就解决了。&lt;/p&gt;
</content:encoded></item><item><title>博客重构之-从hexo 迁移到 next+contentLayer</title><link>https://ab.kisnows.com/zh-CN/2023/12/20/%E5%8D%9A%E5%AE%A2%E9%87%8D%E6%9E%84%E4%B9%8B-%E4%BB%8Ehexo-%E8%BF%81%E7%A7%BB%E5%88%B0-nextcontentlayer/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2023/12/20/%E5%8D%9A%E5%AE%A2%E9%87%8D%E6%9E%84%E4%B9%8B-%E4%BB%8Ehexo-%E8%BF%81%E7%A7%BB%E5%88%B0-nextcontentlayer/</guid><pubDate>Wed, 20 Dec 2023 17:25:56 GMT</pubDate><content:encoded>&lt;p&gt;基于 hexo 的博客用了多年了，有很多主题和插件可以用，用的还是挺舒服的。但是最近打算把它从 hexo 迁移走，原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对博客的掌控感太弱，想要加一些功能就需要等 hexo 更新或者主题作者更新&lt;/li&gt;
&lt;li&gt;单纯就是想折腾一下而已（这才是主要原因吧&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终打算迁移到 next.js 的方案，前后端一体，既可以纯静态站点也可以有作为普通的有后端服务的网站，挺符合我的诉求。而且对网站的掌控力也会比用 hexo 高很多，当然代价就是要花费更多的时间。&lt;/p&gt;
&lt;h2&gt;目标&lt;/h2&gt;
&lt;p&gt;迁移的目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保持原有博文链接不变&lt;/li&gt;
&lt;li&gt;迁移评论系统：原本使用的 disqus ，对墙内来说可用性太差了&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;备份&lt;/h2&gt;
&lt;p&gt;备份：采用&lt;a href=&quot;https://github.com/jiacai2050/blog-backup&quot;&gt;jiacai2050/blog-backup: Backup blogposts to PDF for offline storage, built with Puppeteer and ClojureScript (github.com)&lt;/a&gt; 备份老的博客为 pdf 文件，留个纪念。&lt;/p&gt;
&lt;h2&gt;方案&lt;/h2&gt;
&lt;p&gt;技术选型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;框架： next.js&lt;/li&gt;
&lt;li&gt;内容生产： contentlayer.js&lt;/li&gt;
&lt;li&gt;样式：tailwindcss&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;hexo markdown 语法兼容&lt;/h3&gt;
&lt;p&gt;为了兼容 hexo 中的一些特有语法比如 &lt;code&gt;&amp;lt;!-- more --&amp;gt;&lt;/code&gt; &lt;code&gt;{% asset_img me.jpg 搬家 %}&lt;/code&gt; 等类似的私有语法，需要通过 remark 来开发一些插件来进行兼容。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;&amp;lt;!-- more --&amp;gt;&lt;/code&gt; 的兼容&lt;/h4&gt;
&lt;p&gt;对于 &lt;code&gt;&amp;lt;!-- more --&amp;gt;&lt;/code&gt; 的兼容，一开始写 remark 插件来找到 markdown 文件中的 &lt;code&gt;&amp;lt;!-- more --&amp;gt;&lt;/code&gt; 来后把前面的部分拆分出来，挂在一个自定义变量上比如叫 brief ，后面渲染的时候从上面读就好了。&lt;/p&gt;
&lt;p&gt;后来实现的时候还是碰到了一些问题，拿到的都是没有渲染的 markdown 文件而非渲染好的 html 。由于使用了 contentlayer，从 markdown 转换到 html 的过程是由 contentlayer 控制的，你可以在其中添加插件。但是不知道是插件顺序的问题还是怎么的总是调不对，索性放弃这条路。&lt;/p&gt;
&lt;p&gt;直接在 contentlayer 中添加了一个字段，对已经渲染好的 html 文件做截取，把前 500 个字符作为文章的 brief ，虽然自定义性差了点，但是先能跑。具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** @type {import(&apos;contentlayer/source-files&apos;).ComputedFields} */
const computedFields: import(&quot;contentlayer/source-files&quot;).ComputedFields = {
  permalink: {
    type: &quot;string&quot;,
    resolve: (doc) =&amp;gt; {
		...
  },
  brief: {
    type: &quot;string&quot;,
    resolve: (doc) =&amp;gt; {
      // TODO: 使用 remark 插件来处理文章中 &amp;lt;!-- more --&amp;gt; 注释
      const htmlContent = doc.body.html
      return htmlContent.substring(0, 500)
    },
  },
  readingTime: { type: &quot;json&quot;, resolve: (doc) =&amp;gt; readingTime(doc?.body?.raw) },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;图片引入语法兼容&lt;/h4&gt;
&lt;p&gt;hexo 中有很多类似 &lt;code&gt;{% asset_img me.jpg 搬家 %}&lt;/code&gt; 类似这种语法来引入图片，一开始是尝试通过 remark 插件来把这些东西转换成普通的 markdown 语法，然后走正常的 markdown 转换。但实际操作下来，总是有问题，最终输出的内容还是 &lt;code&gt;![搬家](me.jpg)&lt;/code&gt; ,并不是期望的 &lt;code&gt;&amp;lt;img /&amp;gt;&lt;/code&gt; 标签。&lt;/p&gt;
&lt;p&gt;我理解的是我先把 markdown 中的不标准语法转换成了标准语法，然后再从标准语法走普通的 markdown 渲染到 html，但是不知道是哪里的问题，可能是插件顺序也好，或者我对 remark 的插件原理上理解有问题，导致无论如何也无法渲染成最终的 &lt;code&gt;&amp;lt;img /&amp;gt;&lt;/code&gt; 标签，除非我在插件里面直接把 &lt;code&gt;{% asset_img me.jpg 搬家 %}&lt;/code&gt; 修改为 &lt;code&gt;&amp;lt;img alt=&quot;搬家&quot; src=&quot;/me.jpg&quot; /&amp;gt;&lt;/code&gt; ，但是总觉得这样不好，怕会影响到其他 remark 插件的处理。&lt;/p&gt;
&lt;p&gt;原本的插件代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { visit } from &quot;unist-util-visit&quot;;
import fs from &quot;fs&quot;;
import path from &quot;path&quot;;

const targetImgDir = path.join(process.cwd(), &quot;public/imgs&quot;);
const markdownPath = path.join(process.cwd(), &quot;source/posts&quot;);
const isLoggingEnabled = true; // 设置为true以启用日志，为false则禁用日志

function log(message: string) {
  if (isLoggingEnabled) {
    console.log(message);
  }
}

function handleAssetReference(
  sourceFilePath: string,
  imgFileName: string
): string | null {
  // 获取当前Markdown文件的名称（不包括后缀）
  const filenameWithoutExtension = path.basename(
    sourceFilePath,
    path.extname(sourceFilePath)
  );

  // 获取当前Markdown文件的路径
  const mdDir = path.dirname(sourceFilePath);

  // 根据文件名动态构建可能的图片路径
  const possiblePaths = [
    path.join(mdDir, filenameWithoutExtension, imgFileName),
    path.join(mdDir, &quot;../imgs&quot;, imgFileName),
  ];
  let sourcePath: string | null = null;

  for (const possiblePath of possiblePaths) {
    if (fs.existsSync(possiblePath)) {
      sourcePath = possiblePath;
      break;
    }
  }

  if (!sourcePath) {
    log(`Image ${imgFileName} not found for file ${sourceFilePath}`);
    return null;
  }

  // 复制图片到 public/imgs 文件夹下的相应目录
  const targetPath = path.join(
    targetImgDir,
    filenameWithoutExtension,
    imgFileName
  );
  fs.mkdirSync(path.dirname(targetPath), { recursive: true }); // 确保目录存在
  fs.copyFileSync(sourcePath, targetPath);
  log(`Copied image from ${sourcePath} to ${targetPath}`);

  // 返回新的相对路径
  return `/imgs/${filenameWithoutExtension}/${imgFileName}`;
}

function replaceAssetImgPlugin() {
  return (tree: any, file: any) =&amp;gt; {
    visit(tree, &quot;text&quot;, (node: any) =&amp;gt; {
      // 处理自定义 asset_img 语法
      const customAssetImgPattern = /\{% asset_img (.*?)\s+(.*?) %\}/g;
      node.value = node.value.replace(
        customAssetImgPattern,
        (_, imgFileName, imgAltText) =&amp;gt; {
          return `![${imgAltText}](${imgFileName})`;
        }
      );
    });

    // 处理标准的 markdown 图像引用
    visit(tree, &quot;image&quot;, (node: any) =&amp;gt; {
      const sourceFilePath = path.join(
        markdownPath,
        file.data.rawDocumentData.flattenedPath
      );
      const imgFileName = path.basename(node.url);
      const newURL = handleAssetReference(sourceFilePath, imgFileName);
      if (newURL) {
        node.url = newURL;
      }
    });
  };
}

export default replaceAssetImgPlugin;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;里面干了几件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;找到所有的自定义图片引用语法，并解析出图片名称&lt;/li&gt;
&lt;li&gt;根据图片名称，从给定的目录中去找具体的图片文件&lt;/li&gt;
&lt;li&gt;把找到的图片文件复制到 &lt;code&gt;public/imgs/&lt;/code&gt; 目录下，然后把自定义语法修改为标准的 markdown 图片引用语法，同时把里面的图片应用地址修改为复制后的图片地址，确保 next.js 能够正常访问到这些图片&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但这条路没有走通，最后索性一想，何必这么绕圈子。反正已经要从 hexo 迁移走了，它原本的不标准语法已经没有存在的意义了， 那么直接替换就好了。所以写了个脚本，直接把目录下的所有 markdown 文件中的不标准图片引用语法修改为了标准的语法，并替换了应用地址，这样就一劳永逸了。脚本代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);

// 定义目标图片文件夹路径
const targetImgDir = path.join(process.cwd(), &quot;public/imgs&quot;);
// 定义 Markdown 文件路径
const markdownPath = path.join(process.cwd(), &quot;source/posts&quot;);

function handleAssetReference(sourceFilePath, imgFileName) {
  // 获取当前Markdown文件的名称（不包括后缀）
  const filenameWithoutExtension = path.basename(
    sourceFilePath,
    path.extname(sourceFilePath)
  );
  // 获取当前Markdown文件的路径
  const mdDir = path.dirname(sourceFilePath);
  // 定义可能的图片路径数组
  const possiblePaths = [
    path.join(mdDir, filenameWithoutExtension, imgFileName),
    path.join(mdDir, &quot;../imgs&quot;, imgFileName),
  ];
  let sourcePath = null;

  // 遍历所有可能的图片路径，找到存在的图片路径
  for (const possiblePath of possiblePaths) {
    if (fs.existsSync(possiblePath)) {
      sourcePath = possiblePath;
      break;
    }
  }

  // 如果没有找到图片路径，打印错误信息
  if (!sourcePath) {
    console.log(`Image ${imgFileName} not found for file ${sourceFilePath}`);
    return imgFileName;
  }

  // 定义目标图片路径，并创建该路径的文件夹
  const targetPath = path.join(
    targetImgDir,
    filenameWithoutExtension,
    imgFileName
  );
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
  // 将源图片复制到目标路径
  fs.copyFileSync(sourcePath, targetPath);
  console.log(`Copied image from ${sourcePath} to ${targetPath}`);

  // 返回新的图片URL
  return `/imgs/${filenameWithoutExtension}/${imgFileName}`;
}

function processMarkdownFile(filePath) {
  // 读取Markdown文件内容
  let content = fs.readFileSync(filePath, &quot;utf-8&quot;);

  // 定义自定义图片引用的正则表达式
  const customAssetImgPattern = /\{% asset_img (.*?)\s+(.*?) %\}/g;
  // 替换Markdown文件中的自定义图片引用
  content = content.replace(
    customAssetImgPattern,
    (_, imgFileName, imgAltText) =&amp;gt; {
      const newURL = handleAssetReference(filePath, imgFileName);
      return `![${imgAltText}](${newURL})`;
    }
  );

  // 将处理后的内容写回文件
  fs.writeFileSync(filePath, content);
  console.log(`Processed file: ${filePath}`);
}

function processMarkdownFilesInDir(dir) {
  // 读取目录下的所有文件
  const files = fs.readdirSync(dir);
  for (const file of files) {
    const filePath = path.join(dir, file);
    // 如果是Markdown文件，则处理该文件
    if (path.extname(filePath) === &quot;.md&quot;) {
      processMarkdownFile(filePath);
    }
  }
}

// 处理指定目录下的所有Markdown文件
processMarkdownFilesInDir(markdownPath);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;内容获取&lt;/h3&gt;
&lt;p&gt;采用新的解决方案（坑啊，当时我迁移的时候还不知道，刚迁移完才发现已经不维护了）：&lt;a href=&quot;https://contentlayer.dev/docs/getting-started-cddd76b7&quot;&gt;Getting Started – Contentlayer&lt;/a&gt; , 通过把内容转换成类型安全的 JSON 文件，方便在系统中 import 调用。
特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;验证文件格式：通过提前声明要求的 frontMeta 的字段，来规范字段&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// contentlayer.config.ts
import { defineDocumentType, makeSource } from &quot;contentlayer/source-files&quot;;

export const Post = defineDocumentType(() =&amp;gt; ({
  name: &quot;Post&quot;,
  filePathPattern: `**/*.md`,
  fields: {
    title: { type: &quot;string&quot;, required: true },
    date: { type: &quot;date&quot;, required: true },
  },
  computedFields: {
    url: {
      type: &quot;string&quot;,
      resolve: (post) =&amp;gt; `/posts/${post._raw.flattenedPath}`,
    },
  },
}));

export default makeSource({ contentDirPath: &quot;posts&quot;, documentTypes: [Post] });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用起来还挺好用的，相当于把不标准的 markdown 文件通过一个预先定义的 schema 定义好，然后根据这些 schema 生成 ts 类型定义文件，这样就可以直接在你的代码中获得类型安全的文档模型。&lt;/p&gt;
&lt;p&gt;最终我的 contentlayer 配置文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { writeFileSync } from &quot;fs&quot;
import { defineDocumentType, makeSource } from &quot;contentlayer/source-files&quot;
import readingTime from &quot;reading-time&quot;
import rehypeAutolinkHeadings from &quot;rehype-autolink-headings&quot;
import rehypeCodeTitles from &quot;rehype-code-titles&quot;
import rehypeHighlight from &quot;rehype-highlight&quot;
import rehypePrism from &quot;rehype-prism-plus&quot;
import rehypeSlug from &quot;rehype-slug&quot;
import remarkGfm from &quot;remark-gfm&quot;

const root = process.cwd()
const isProduction = process.env.NODE_ENV === &quot;production&quot;
/**
 * Count the occurrences of all tags across blog posts and write to json file
 */
function createTagCount(allBlogs) {
  const tagCount: Record&amp;lt;string, number&amp;gt; = {}
  // const slugger = new GithubSlugger()
  allBlogs.forEach((file) =&amp;gt; {
    if (file.tags &amp;amp;&amp;amp; (!isProduction || file.draft !== true)) {
      file.tags.forEach((tag) =&amp;gt; {
        if (tag in tagCount) {
          tagCount[tag] += 1
        } else {
          tagCount[tag] = 1
        }
      })
    }
  })
  writeFileSync(&quot;./app/tag-data.json&quot;, JSON.stringify(tagCount))
}

/** @type {import(&apos;contentlayer/source-files&apos;).ComputedFields} */
const computedFields: import(&quot;contentlayer/source-files&quot;).ComputedFields = {
  slug: {
    type: &quot;string&quot;,
    // 返回当前文件所在目录的相对路径，比如 posts/2021-01-01-hello-world.md
    resolve: (doc) =&amp;gt; `/${doc?._raw?.flattenedPath}`,
  },
  slugAsParams: {
    type: &quot;string&quot;,
    resolve: (doc) =&amp;gt; {
      // 返回当前文件所在目录的相对路径去除 posts 后的路径，比如 2021-01-01-hello-world.md
      return doc?._raw?.flattenedPath?.split(&quot;/&quot;)?.slice(1)?.join(&quot;/&quot;)
    },
  },
  permalink: {
    type: &quot;string&quot;,
    resolve: (doc) =&amp;gt; {
      const date = new Date(doc.date)
      const year = date.getFullYear()

      // 确保月份和日期始终有两位数字
      const month = (date.getMonth() + 1).toString().padStart(2, &quot;0&quot;)
      const day = date.getDate().toString().padStart(2, &quot;0&quot;)

      const slugAsParams = doc?._raw?.flattenedPath
        ?.split(&quot;/&quot;)
        ?.slice(1)
        ?.join(&quot;/&quot;)

      return `/${year}/${month}/${day}/${slugAsParams}`
    },
  },
  brief: {
    type: &quot;string&quot;,
    resolve: (doc) =&amp;gt; {
      // TODO: 使用 remark 插件来处理文章中 &amp;lt;!-- more --&amp;gt; 注释
      const htmlContent = doc.body.html
      return htmlContent.substring(0, 500)
    },
  },
  readingTime: { type: &quot;json&quot;, resolve: (doc) =&amp;gt; readingTime(doc?.body?.raw) },
}

export const Post = defineDocumentType(() =&amp;gt; ({
  name: &quot;Post&quot;,
  filePathPattern: `posts/**/*.md`,
  contentType: &quot;markdown&quot;,
  fields: {
    title: {
      type: &quot;string&quot;,
      required: true,
    },
    description: {
      type: &quot;string&quot;,
    },
    date: {
      type: &quot;date&quot;,
      required: true,
    },
    update: {
      type: &quot;date&quot;,
      required: false,
    },
    tags: {
      type: &quot;list&quot;,
      of: { type: &quot;string&quot; },
      default: [],
    },
    category: {
      type: &quot;string&quot;,
      required: true,
      default: &quot;&quot;,
    },
    image: {
      type: &quot;string&quot;,
      required: false,
    },
    draft: {
      type: &quot;boolean&quot;,
      required: false,
    },
  },
  computedFields,
}))

export default makeSource({
  contentDirPath: &quot;./source&quot;,
  documentTypes: [Post],
  markdown: {
    rehypePlugins: [
	// 各种问题导致只能先把所有插件禁用掉了
      // rehypeSlug,
      // rehypeCodeTitles,
      // @ts-ignore
      // [rehypeHighlight],
      // rehypeHighlight,
      // [rehypePrism, { ignoreMissing: true }],
      // [
      //   rehypeAutolinkHeadings,
      //   {
      //     properties: {
      //       className: [&quot;anchor&quot;],
      //     },
      //   },
      // ],
    ],
    // remarkPlugins: [remarkGfm],
  },
  onSuccess: async (importData) =&amp;gt; {
    const { allPosts } = await importData()
    createTagCount(allPosts)
    // createSearchIndex(allPosts)
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合 next-contentlayer 这个插件，就可以直接 next.js 应用中使用 post 这个我定义好的类型了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Post } from &quot;contentlayer/generated&quot;

export function PostList({
  posts,
  pageSize = 10,
  currentPage = 1,
}: {
  posts: Post[]
  pageSize?: number
  currentPage?: number
}) {
  const currentPageList = getSpecificPagePosts(posts, {
    page: currentPage,
    pageSize,
  })
	……
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;路由结构&lt;/h2&gt;
&lt;p&gt;整体路由结构大致是下面这个样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/[year]/[month]/[day]/[post]

/tag/
/tag/[tag]/
/tag/[tag]/page/[pageNum]

/category/
/category/[category]/
/category/[category]/page/[pageNum]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文章的路径采用：&lt;code&gt;/[year]/[month]/[day]/{blogName}&lt;/code&gt; 这种格式是为了兼容 hexo 下的文章永久链接。&lt;/p&gt;
&lt;p&gt;实现思路：
如果以前用 hexo 的永久链接(permalink)的话，那么文章中都会有 &lt;code&gt;date&lt;/code&gt; 这个 frontMeta 属性。基于这个属性，在 contentLayer 中给所有 post 添加一个 permalink 的属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  permalink: {
    type: &quot;string&quot;,
    resolve: (doc) =&amp;gt; {
      const date = new Date(doc.date)
      const year = date.getFullYear()
      const month = date.getMonth() + 1
      const day = date.getDate()
      const slugAsParams = doc?._raw?.flattenedPath
        ?.split(&quot;/&quot;)
        ?.slice(1)
        ?.join(&quot;/&quot;)
      console.log(&quot;permalink&quot;, `/${year}/${month}/${day}/${slugAsParams}`)
      return `/${year}/${month}/${day}/${slugAsParams}`
    },
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个属性返回的路径就和 hexo 以前的 permalink 保持一致了，类似：&lt;code&gt;2023/01/01/2022-year-end-summary/&lt;/code&gt; 这种格式。&lt;/p&gt;
&lt;p&gt;然后在你的 postList 中将所有的 post 跳转地址都指定到这个 permalink 属性上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;CardHeader&amp;gt;
  &amp;lt;CardTitle className=&quot;m-0&quot;&amp;gt;
    // 使用 permalink 属性
    &amp;lt;Link href={post.permalink} className=&quot;no-underline&quot;&amp;gt;
      {post.title}
    &amp;lt;/Link&amp;gt;
  &amp;lt;/CardTitle&amp;gt;
  &amp;lt;CardDescription className=&quot;space-x-1 text-xs&quot;&amp;gt;
    &amp;lt;span&amp;gt;{format(parseISO(post.date), &quot;MMMM dd, yyyy&quot;)}&amp;lt;/span&amp;gt;
    &amp;lt;span&amp;gt;{` • `}&amp;lt;/span&amp;gt;
    &amp;lt;span&amp;gt;{post.readingTime.text}&amp;lt;/span&amp;gt;
    &amp;lt;span&amp;gt;{` • `}&amp;lt;/span&amp;gt;
    &amp;lt;span&amp;gt;
      &amp;lt;Link
        href={`/categories/${encodeURIComponent(
          post.categories?.toLowerCase()
        )}`}
        className=&quot;underline underline-offset-2&quot;
      &amp;gt;
        {post.categories}
      &amp;lt;/Link&amp;gt;
    &amp;lt;/span&amp;gt;
  &amp;lt;/CardDescription&amp;gt;
&amp;lt;/CardHeader&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后创建对应的路由结构：&lt;code&gt;/[year]/[month]/[day]/[post]&lt;/code&gt; 的文件目录，在 最底层目录新建 &lt;code&gt;page.jsx&lt;/code&gt; 用来渲染文章。&lt;/p&gt;
&lt;p&gt;实现上，在 next.js 中，就需要在 app 目录中，根据这个路由结构建立对应的嵌套目录。非常的麻烦，尤其是下面的 tag 以及 category 相关的 page 最终都是和首页一样的文章列表，但是用了上面这种目录后就需要一遍遍的把基本重复的内容引用来引用去。&lt;/p&gt;
&lt;p&gt;为什么翻页要用这种路径的形式而非 url 参数呢，是为了能够生成静态的页面，这样在 build 后可以作为静态博客部署，不需要服务器运行，和 hexo 类似。&lt;/p&gt;
&lt;h2&gt;评论系统&lt;/h2&gt;
&lt;p&gt;当时备选的有三个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;giscus: https://github.com/giscus/giscus&lt;/li&gt;
&lt;li&gt;disqusJS： https://github.com/SukkaW/DisqusJS&lt;/li&gt;
&lt;li&gt;twikoo: https://github.com/twikoojs/twikoo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终还是选择了 giscus ，使用起来比较简单，而且依托 github 的能力，编辑体验很不错。缺点是如果没有 github 账户就无法评论了。&lt;/p&gt;
&lt;p&gt;不过评论本身也不是刚需，所以先解决有无的问题。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;目标：代码推送到仓库的主分支，那么就会触发自动部署功能。&lt;/p&gt;
&lt;p&gt;虽然我自己有服务器，但是最终直接选择了部署在 vercel 上了，因为实在是太方便了。我的博客访问量也不高， vercel 免费账户远远够了，等有空了再打通自动化部署到自己服务器的流程吧。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;说要重构博客说了快一年，然后一直在拖。虽然重构工作量本身并不大，但是要静下心来，还要有大块的时间，最终一直拖到了现在总算勉强完成博客的重构，先给自己个赞吧。&lt;/p&gt;
&lt;p&gt;然而有点尴尬的是，我在调代码高亮功能的时候发现 rehype-highlight 插件怎么调都有问题，后来去查 contentlayer 的文档和 issue 发现好家伙，这个仓库已经不维护了。当然原因也能理解，因为有个赞助项目的公司不赞助了。失去了经济来源了，所以暂时搁置维护也是情理之中。只是我这边找不到问题解决方案了，所以后续会下掉 contentlayer 。&lt;/p&gt;
&lt;p&gt;不过时间就不得而知了。&lt;/p&gt;
&lt;h1&gt;参考文章&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.skk.moe/post/use-nextjs-and-hexo-to-rebuild-my-blog/&quot;&gt;使用 Next.js + Hexo 重构我的博客 | Sukka&apos;s Blog (skk.moe)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/SpectreAlan/blog-nextjs&quot;&gt;SpectreAlan/blog-nextjs: 基于 nextjs 搭建的 SSR 个人博客 (github.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kejiweixun.com/blog/migrate-my-blog-from-gatsbyjs-to-nextjs&quot;&gt;把博客从 GatsbyJS 迁移至 NextJS (kejiweixun.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.kpman.cc/2018/06/08/%E5%BE%9E-Hexo-%E5%88%B0-Gatsby/&quot;&gt;從 Hexo 到 Gatsby - kpman | code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://liyangzone.com/article/2022-05-17-use-nextjs-rebuild-blog/&quot;&gt;使用 next.js 重构博客 | liyang&apos;s blog (liyangzone.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/enjidev/enji.dev&quot;&gt;enjidev/enji.dev: a monorepo for my personal website and projects, built with Turborepo + pnpm 📚 (github.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mirsazzathossain/mirsazzathossain.me&quot;&gt;github.com/mirsazzathossain/mirsazzathossain.me&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/js-template/metablog-free&quot;&gt;github.com/js-template/metablog-free&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/stevenspads/next-app-router-blog&quot;&gt;stevenspads/next-app-router-blog: A Next.js markdown blog template for developers. It uses the new Next.js App Router. (github.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shadcn/next-contentlayer&quot;&gt;shadcn/next-contentlayer: A template with Next.js 13 app dir, Contentlayer, Tailwind CSS and dark mode. (github.com)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>手机上键盘弹出时的一些问题</title><link>https://ab.kisnows.com/zh-CN/2016/01/13/%E8%A7%A3%E5%86%B3%E6%89%8B%E6%9C%BA%E9%94%AE%E7%9B%98%E8%A6%86%E7%9B%96%E5%BC%B9%E7%AA%97%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://ab.kisnows.com/zh-CN/2016/01/13/%E8%A7%A3%E5%86%B3%E6%89%8B%E6%9C%BA%E9%94%AE%E7%9B%98%E8%A6%86%E7%9B%96%E5%BC%B9%E7%AA%97%E9%97%AE%E9%A2%98/</guid><pubDate>Wed, 13 Jan 2016 04:27:52 GMT</pubDate><content:encoded>&lt;p&gt;做手机商城接到一个需求。
具体大概是这样子的，要求用户点击购买按钮后，直接显示输入密码的弹窗并弹出键盘。
拿到需求一想，这挺简单的。
就麻利的写了个模拟的密码输入框，然后在用户点击购买按钮的时候让他弹出来，并且让焦点 focus 在 input 框上。
Chrome 上一试，完全没问题，觉得自己简直太棒了有没有。可是拿出自己的手机一试，oh no ~,键盘直接把页面给推了上去，导致完全看不到弹窗弹的是个什么东西。
&lt;img src=&quot;/imgs/2016-01-12-1.png&quot; alt=&quot;解决前&quot; /&gt;
但是在 Android 上是没有任何问题的。观察了下是因为 Android 和 ios 上键盘弹出的行为是不一样的，Android 上键盘弹出来，直接覆盖在原来的页面上，而 ios 上键盘弹出来的时候会直接把整个页面推上去。
怎么解决呢？其实也挺好解决，就是针对不同系统 hack 一下。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;购买页面这个弹窗的定位是这么定位的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 后面的遮罩
.dialog-wrap {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.7);
  position: relative;
}
// 密码输入框
.password-dialog {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  margin: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在弹窗弹出的时候，判断当前设备是 Android 还是 ios ，如果是 ios 设备，调整一下密码输入框定位就 OK 了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$$(&quot;.go-buy&quot;).click(function () {
  showPassword(); //调出弹窗
  if (!device.android) {
    //  如果不是安卓，那就调整定位
    $$(&quot;.password-dialog&quot;).css({
      top: &quot;initial&quot;,
      bottom: &quot;20px&quot;,
    });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终就达到了目标，
&lt;img src=&quot;/imgs/2016-01-12-2.png&quot; alt=&quot;解决后&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之前也遇到过类似的问题，比如页面上有一个输入框，在 Android 上点击输入框调出键盘的时候，因为键盘不会把页面推上去，导致如果这个输入框在当前页面比较靠下的位置，就可能被弹出的键盘覆盖掉，看不到当前选中的输入框，影响用户体验。解决方法其实也很简单，就是当点击输入框弹出键盘的时候，调整页面 scrollTop 值，让页面的可视区域刚好滚动到输入框这一块，也就解决了这个问题。&lt;/p&gt;
&lt;p&gt;这些坑，在事后看起来都很简单，然而当刚遇到的时候就是不知道怎么解决。比如密码输入框那个，当时总是在想怎么才能让键盘不把页面推上去，如果从这个角度去想解决办法那恐怕很难想出办法来，因为手机操作系统在实现的时候人家就那么规定了，你总不能用 js 去改这个键盘弹出方式吧，除非你写个模拟键盘，但那就是舍近求远了。但是如果换个角度，从调整页面上元素定位来解决，那就很简单了。
所以碰到问题，一定要从多种角度去想解决办法，这样才能更容易的找到问题的解决办法。一定要放开眼界，放开眼界啊！&lt;/p&gt;
</content:encoded></item></channel></rss>