SegmentFault 思诚^_^最新的文章
2020-11-12T19:44:05+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
前端架构发展史
https://segmentfault.com/a/1190000038151875
2020-11-12T19:44:05+08:00
2020-11-12T19:44:05+08:00
jafeney
https://segmentfault.com/u/jafeney
3
<p>最初,前端是没有架构的,因为功能简单的代码毫无架构可言。通过一个简单的jQuery库操作DOM就能完成的工作,无需复杂的设计模式和代码管理机制,也就不需要架构来支持起应用。</p><p>前端开发的发展历史分为以下几个阶段:</p><ul><li>古典时代:由后端渲染出前端HTML,用Table布局,用CSS进行简单的辅助</li><li>动效时代:前端开始编写一些简单的JavaScript脚本来做动画效果,如轮播广告</li><li>Ajax异步通信时代:2005年,Google在诸多Web应用中使用了异步通信技术如 Google地图,开启了Web前端的一个新时代</li></ul><p>一旦前端应用需要从后端获取数据,就意味着前端应用在运行时是动态地渲染内容的,这便是Model(模型)UI层解耦。jQuery能够提供DOM操作方法和模型引擎等。这时的开发人员需要做下面两件事:</p><ul><li>动态生成HTML。由后端返回前端所需要的HTML,再动态替换页面的DOM页面。早期的典型架构如jQuery Mobile,事先在前端写好模板与渲染逻辑,用户的行为触发后台并返回对应的数据来渲染文件</li><li>模板分离。由后端用API返回前端所需要的JSON数据,再由前端来计算生成这些HTML。前端的模板再使用HTML,而是使用诸如 Mustache 这样的模板引擎来渲染HTML</li></ul><p>由于HTML的动态生成、模板的独立与分离,前端应用开始变得复杂。后端的MVC架构进一步影响了前端开发,便诞生了一系列操起的MVC框架,如Backbone、Knockout等。与此同时,在 Ryan Lienhart Dahl等人开发了Node.js之后,前端的软件功能便不断地改善:</p><ul><li>更好的构建工具。诞生了诸如 Grant 和 Gulp 等构建工具</li><li>包管理。产生了用于前端的包管理工具 Bower 和 Npm</li><li>模块管理。也出现了AMD、Common.js 等不同的模块管理方案</li></ul><p>随着单页面应用的流行,前后端分离框架也成为行业内的标准实践。由此,前端进入了一个新的时代,要考虑的内容也越来越多:</p><ul><li>API 管理,采用了诸如 Swagger 的 API 管理工具,各式的 Mock Server 也成为标准实践。</li><li>大前端,由前端来开发跨平台移动应用框架,采用诸如 Ionic、React Native、Flutter 等框架。</li><li>组件化,前端应用从此由一个个细小的组件结合而成,而不再是一个大的页面组件。</li></ul><p>系统变得越来越复杂,架构在前端的作用也变得越来越重要。MVC 满足不了开发人员的需求,于是采用了组件化架构。而组件化 + MV 也无法应对大型的前端应用,微前端便又出现在我们的面前,它解决了以下问题:</p><ul><li>跨框架。在一个页面上运行,可以同时使用多个前端框架。</li><li>应用拆分。将一个复杂的应用拆解为多个微小的应用,类似于微服务。</li><li>遗留系统迁移。让旧的前端框架,可以直接嵌入现有的应用运行。</li></ul><p>复杂的前端应用发展了这么久,也出现了一系列需要演进的应用:考虑重写、迁移、重构,等等。</p>
关于前端面试的一些心得
https://segmentfault.com/a/1190000022549414
2020-05-05T19:40:40+08:00
2020-05-05T19:40:40+08:00
jafeney
https://segmentfault.com/u/jafeney
5
<p><img src="/img/bVbGMdf" alt="banner-0.jpg" title="banner-0.jpg"></p>
<p>本系列的目的是帮助更多面试经验不足的前端人才更好地展现自己。在此,我分享一些以往我参加面试和参与招聘的一些心得,希望对大家有帮助。</p>
<h2>关于简历设计</h2>
<p>简历是人才的缩影,一份优质的简历是前往大公司的敲门砖。所以对于招聘,简历准备是第一环,也是最重要的一环。前端工程师的简历其实不需要视觉设计类的那般花哨,核心要点是<strong>简洁干练,突出重点</strong>。</p>
<h3>简历应该囊括的点</h3>
<p>招聘人员一般筛查简历时,第一眼关注的时候<strong>工作年限和薪资范围</strong>(确定是否是能力年龄对路的人才),第二眼看的是<strong>学校和学历</strong>(985、211、硕士当然最好,如果只是大专,再重点关注是否有大厂工作经历或者其他出彩的技术亮点),第三眼看的<strong>工作履历</strong>(如果有BAT的工作经历实在再好不过了,说明你已经受到过主流的认可,如果没有那要看看项目经验里是否有出彩的地方)</p>
<p>所以一份合格的前端简历应该囊括以下几点:</p>
<ul>
<li>基本信息(姓名、国籍、手机号、邮箱、居住地、期望工作地)</li>
<li>技能清单(注意如实写,不要夸夸其谈,每个技能点最好落实某个具体的实践)</li>
<li>工作经历(工作单位、职位、时间区间、职位描述)</li>
<li>教育经历(学校名称、专业、时间区间、学历、专业描述)</li>
<li>项目经历(项目名称、时间区间、职责描述)</li>
<li>培训经历(培训机构、培训时间、培训证书、培训内容)</li>
<li>自我认知(这个很关键,建议囊括 学习或适应能力、团队中的位置、价值观、不足之处)</li>
</ul>
<h3>如何提现亮点</h3>
<p>什么是亮点,就是在同级对比中突出的东西,说白了就是别人没有而你有的东西。一般来说,GitHub、个人博客是比较常见的亮点。当然前提是里面确实有亮点,如果面试官满怀欣喜的点开,却发现徒有其表,那亮点可能就会变成更大的失望,这样的亮点当然不推荐。</p>
<p>我总结一下我经常会关注的一些亮点:</p>
<ul>
<li>创业经历(担任核心岗位、承担过重要责任、创业达1年以上)</li>
<li>GitHub里主导或参与过重大开源项目、某个 Repositories 有三位数以上的 Star</li>
<li>个人博客粉丝或日PV过万、持续原创或有分析特别精辟的原创文章</li>
</ul>
<p>如果你实在没有以上的这些,那就把项目过程中一些你觉得可圈可点的地方着重提一下,并交代一下解决后的成效,这会加深面试官的印象。</p>
<h2>简历的几个大忌</h2>
<p>以下4点是简历设计里的大忌,一旦出现以下这些错误,简历就很难过筛选。</p>
<h3>错别字</h3>
<p>主要指明显的错别字,语句语义不通等。虽然不是语文考试,但是简历里出现此类基本错误是绝不允许的,这可能让面试官觉得你是一个粗心、思维不严谨的人,这样的人一般不敢要。所以强烈建议在投递之前仔细检查自己的简历,多读几遍。</p>
<h3>专业术语错误</h3>
<p>专业术语错误主要出现在自己不熟悉的领域。有些面试者为了自己简历好看,或者是参考别人的模板,硬生生把一些自己从来没用过或者只是简单了解的技术写进自己的简历里,结果还不小心抄错了,这其实比简单的错别字还严重。</p>
<h3>时间点不一致</h3>
<p>一份简历里最引人注目的其实是时间数字,这个看似简单,但也有简历里出现时间不一致的问题。比如在工作经历里写的是“2016年3月至2018年5月”在某一家公司,而项目经历里却把结束时间写成“2018.8月”,这里不排除个别面试者特别热心肠,离职了还在给老东家干活,但就招聘人才而言,并不倡导当这样的“好人”(领导不会希望自己的员工在公司还做着其他公司的事)。</p>
<h3>频繁乱投简历</h3>
<p>同一家公司同类职位不要频繁地投递简历,尤其是阿里巴巴相关的职位。大厂有严格的人才招聘制度,一个人在A职位的面试流程中就无法参与B职位的招聘,并且多次被拒的简历短期内将无法通过同类任何职位的简历筛选(也就是所谓的拉黑名单,时间最长可达2年)。所以大厂的职位投递前一定要深思熟虑,挑选自己最合适的职位,不要胡乱撒网。其实大厂本身就有内部调度机制,如果A职位进入面试流程,最终因为岗位契合度被拒,他们可能会和你沟通意向,继而转接给更合适的团队进行下一轮的面试安排。</p>
<h3>弄虚作假</h3>
<p>职场和商场一样,除了个人能力,诚信和人品也非常重要。大公司一般在完成人才面试后,会找调查公司对即将入职的员工进行<strong>背景调查</strong>。背景调查其实也不是什么新鲜事,主要是调查个人的 <strong>家庭信息</strong>、<strong>学历学校</strong>、<strong>以往的工作履历</strong>等。一方面,公司会对员工给出的联系人进行身份核实和私密咨询,确认信息的真实性;另一方面,公司会利用一些公示系统对员工以往、工商记录、职场圈子等进行随机电访。如果有被核实存在弄虚作假的,或者公司严令禁止的事项(比如个人存在不良工商记录、同时担任其他公司的职位等)。不仅会失去入职资格,还会被拉黑名单。</p>
<h2>关于面试技巧</h2>
<p>关于面试的技巧,仁者见仁智者见智。面试前一定要充分准备,回顾面试的常考技术点和经期项目,然后Review一下当前的工作和心理状态。如果时间有限,就重点回顾一下近期的项目,总结一下项目过程中使用的技术框架、核心功能的解决方案、难点攻克的思路和过程等(因为这些是面试官喜欢发散提问的)。下面是我总结的几个行之有效的战术:</p>
<h3>调整心态 保持自信</h3>
<p>自信非常重要,它经常能会让你超长发挥。如何保持自信,这往往和心态有关。务必树立这个职位自己完全可以胜任的心态,面试只是一次对往昔工作的告别以及对未来工作的展望。如果面试前15分钟心态还没有调整好肯定会更加紧张,建议这时不要再看任何面试考点,到阳台或天台仰望一会天空,平复一下心理,然后自我安慰一把:“放轻松,我绝对可以胜任这个职位,如果这次实在过不了,大不了回去再升升级呗~”</p>
<h3>实事求是 互相取经</h3>
<p>知之为知之不知为不知,如果面试官的问题你答不上来。先实话实说,然后如果你有思路但不确定,可以这样回答:“这个问题我没有实际遇到过,但如果现在让我来解决,我会......” 回答完了记得再补上一句:“不知道刚刚的回答是否有纰漏或错误,面试官可以分享一些您的经验吗?”。一般情况下面试官肯定会如实分享他的看法和经验,这样一方面你借此学习了知识,另一方面也会让面试官觉得你是一个很有求知欲的人,很有培养的潜质。</p>
<h3>理解而不是死记</h3>
<p>所谓上有政策,下有对策,面试的很多考点其实很多网上都有答案。面试官面试前也会网上查一些考点来准备,千万不要死记答案,一旦被打上 “面霸”的标签,其实对后续的面试并非有利。如果一时理解不了就多看几遍,实在理解不了就放弃这个题吧,然后留时间多看点别的,面试官不会因为一个问题回答不上了就给一个人定级。术业有专攻,有自己专精的领域,有不足的后续可以按需补充,这没什么。</p>
<p>null</p>
ReactNative 到 Weex 的艰难一迈
https://segmentfault.com/a/1190000011418946
2017-09-29T22:28:30+08:00
2017-09-29T22:28:30+08:00
jafeney
https://segmentfault.com/u/jafeney
4
<blockquote><p>“Write once,Run Everywhere” 一次编写,多端运行。<code>React</code>迁移到<code>MIT</code>协议,可惜<code>React Native</code>依然没改,没有<code>RN</code>的日子,还好有<code>Weex</code>这位哥们顶着。虽然没有RN那么牛逼,但也算是一个不错的小兄弟。</p></blockquote>
<p>很多人可能要问我搞了这么久的RN现在转Weex干什么?说起来,真是一个悲伤的故事</p>
<h2>为什么不用<code>RN</code>
</h2>
<p><code>Facebook</code>并没有像<code>React</code>那样把<code>ReactNative</code>也迁移到<code>MIT</code>协议,所以使用<code>ReactNative</code>开发对外产品,依然有专利风险。一般的公司其实没什么影响,但我厂情况比较特殊,有好几个核心的专利技术,老板不想冒这个险。加之隔壁的阿里<code>Weex</code>推得很火,那就用用看吧!</p>
<blockquote><p><code>React</code>专利许可证具体细节欢迎出门左转看我之前的文章:<a href="https://link.segmentfault.com/?enc=gjvbt7L0%2FkWi8R15X3pMMw%3D%3D.sek4FPGPjhv7AUQ63ln7ViQ%2Bgz0F45eTbfwvOKoNfpDDtL98Zo6gLYmBgKJDuUUq" rel="nofollow">《React专利许可证研究》</a></p></blockquote>
<h2>
<code>Weex</code>较<code>RN</code>的优势</h2>
<p>说老实话,和<code>ReactNative</code>打交道这么久,突然换一个小兄弟上,一时还真的难以适应,甚至一脸嫌弃。<code>Weex</code>和<code>ReactNative</code>的设计理念也完全不同。<code>RN</code>重点在<code>Native</code>,以<code>React</code>的方式开发跨平台<code>App</code>,它注重<code>Native</code>细腻的用户体验和强大的原生功能,运用 <code>ReactNative</code> 甚至不需要<code>Native</code>工程师就能独立开发一款功能完善的<code>App</code>。</p>
<h3>
<code>Web</code>开发体验</h3>
<p>独立开发<code>App</code>对<code>Weex</code>来说比较困难,因为它的<code>Native</code>能力没有<code>RN</code>那般强大而全面。它更注重 <code>Web开发体验</code>,顾名思义就是像开发<code>Web</code>网页一样开发跨平台<code>App</code>页面,注意了是以<code>Web</code>为主导,所以它的设计理念提倡 <strong>轻量</strong> + <strong>可拓展</strong>(至于“高性能”较<code>RN</code>并没什么太大的体现),官方也推荐用<code>Weex</code>和<code>Native</code>混合的方式开发<code>App</code>,也就是把<code>Weex</code>作为一个组件融入到<code>Native App</code>,替换传统的 <code>Hybrid</code> 模式。</p>
<h3>没有专利限制</h3>
<p><code>Weex</code>已经加入<code>ASF</code>,没有<code>ReactNative</code> 的专利协议限制,可以放心大胆地使用。当然有童鞋会反问 <code>Weex</code>目前还在使用 <code>FaceBook</code> 的 <code>Yoga</code>引擎,会不会有隐患?这个短期内不需要我们操心,首先这个问题本身边缘就很模糊,其次 像阿里这样的大厂迟早会开发一套类似的引擎来替代,时间问题。</p>
<h3>三端共用代码</h3>
<p><code>Weex</code>既保留了<code>H5</code>的灵活性,也赋予了其<code>Native</code>能力,通过<code>JavaSriptCore</code>+<code>JSbridge</code>直接和<code>Native</code>进行通信,较之 <code>Hybrid</code>的<code>WebView</code> + <code>URLScheme</code>方式性能好了很多。甚至可以实现传说中的 “三端融合”——也就是 <code>Web</code>、<code>iOS</code>、<code>Android</code>端的前端部分共用一套代码,省去了独立建站和维护的麻烦。</p>
<p>当然有得必有失,三端兼容的坑也不少,<code>Android</code>各机型 <code>hack</code> 就不提了,Web端其实就是个<code>WebApp</code>,要利用浏览器的<code>BOM</code>与三方的<code>JS-SDK</code> 来完成 <code>DOM</code>和<code>HTTP</code>以外的功能。不过使用<code>Weex</code>内建的标签和样式可以很容易实现三端布局样式和基本行为的一致,还是大大地减少了工作量。</p>
<blockquote><p>值得一提 Weex的布局单位有且只有<code>px</code>,默认的显示宽度 (<code>viewport</code>) 是 <code>750px</code>,组件都会以 <code>750px</code> 作为满屏宽度,但可以通过 <code>meta.setViewport()</code> 手动指定页面的显示宽度</p></blockquote>
<table>
<thead><tr>
<th align="center">Type</th>
<th align="center">iPhone4</th>
<th align="center">iPhoneSE</th>
<th align="center">iPhone8</th>
<th align="center">iPhone8P</th>
<th align="center">iPhoneX</th>
</tr></thead>
<tbody>
<tr>
<td align="center">物理像素</td>
<td align="center">640x960</td>
<td align="center">640x1136</td>
<td align="center">750x1334</td>
<td align="center">1080x1920</td>
<td align="center">1125x2436</td>
</tr>
<tr>
<td align="center">显示像素</td>
<td align="center">750x1125</td>
<td align="center">750x1331</td>
<td align="center">750x1334</td>
<td align="center">750x1333</td>
<td align="center">750x1624</td>
</tr>
<tr>
<td align="center">像素比</td>
<td align="center">@0.85x</td>
<td align="center">@0.85x</td>
<td align="center">@1x</td>
<td align="center">@1.44x</td>
<td align="center">@1.5x</td>
</tr>
</tbody>
</table>
<h3>对<code>CSS3</code>的支持</h3>
<p>Weex虽然也对<code>CSS</code>做了一定的阉割,但比较 <code>ReactNative</code>,它保留得更多,甚至支持大量的<code>CSS3</code>特性,这一点值得赞叹。<code>CSS3</code>作为Web开发的利器,放着不用难免可惜。下面列举Weex 和标准<code>Web</code>的样式差异:</p>
<ul>
<li>支持的<code>CSS3</code>特性包括:FlexBox、2D转换、过渡、线性渐变、阴影(仅<code>Web</code>和<code>iOS</code>)、伪类、自定义字体(<code>iconFront</code>图标也能用哦)</li>
<li>中支持单个 <strong>类名选择器</strong>,不支持 <strong>关系型选择器</strong>,也不支持 <strong>属性选择器</strong>
</li>
<li>支持组件级别的作用域,为了保持<code>Web</code>和<code>Native</code>的一致,需要 <code><style scoped></code> 声明作用域</li>
<li>不支持<code>CSS3</code>动画(动画请使用Weex内建<code>animation</code>模块)和3D转换</li>
<li>不支持 <code>display: none</code> ,用模板语法 <code>v-if</code> 替代,不建议用 <code>opacity: 0</code>(<code>Native</code>里有点透问题)</li>
<li>类似<code>RN</code>,为了提高解析效率,Weex样式属性不支持简写,比如 <code>font</code>、<code>border</code>、<code>transfrom</code>不能简写</li>
</ul>
<h2>Weex不足之处</h2>
<p>本节的最后,还是想吐槽几个<code>Weex</code>的不足之处,希望官方能尽快升级和改进:</p>
<h3>社区建设缓慢</h3>
<p>这应该是最要命的,<code>Weex</code>社区从去年开源到今天还是这般冷清不免令人神伤,虽然我知道阿里内部推广力度很大,但是既然选择开源,就应该跳出阿里的圈子,一些成功案例、成熟解决方案、优秀架构设计等就不应该藏着掖着,不然真的很难推广起来。我不希望遇到坑点 Google 几圈都找不到有价值的方案,GitHub上提问半天都等不到一个满意的答案(哈哈,说得有点激动啊,很感谢 mario 上次回答我的提问,希望能尽快反应到官方文档里)</p>
<h3>Native能力提高</h3>
<p>如果不提高Native能力,<code>Weex</code>的 <strong>全页模式</strong> 价值其实不大。不要求面面俱到,但希望能再添加一些常用的,比如:<code>statusBar</code>控制、位置信息、<code>Android</code>动态权限分配等</p>
<h3>真机调试工具升级</h3>
<p><code>Weex</code>提倡<code>Web</code>开发体验,所以开发和调试都以<code>Web</code>为主,做静态页面还好,但我要调试<code>Native</code>端的特有逻辑,就需要在Native端集成<code>weex-devtool</code>。这个方案的确另辟蹊径,不过每次改完代码需要手动刷新会不会太麻烦,能不能搞个类似RN的 <strong>热替换</strong> 或 <code>LiveReload</code>功能呢?</p>
<h3>Mac端文件权限问题</h3>
<p>在Mac端Shell工具进入<code>Weex</code>的目录,无论<code>npm</code>相关命令,还是<code>git</code>操作都需要<code>sudo</code>权限,好麻烦。我很懒的,<code>Weex</code>在创建文件的时候能不能帮我把写权限的事也做了?</p>
<hr>
<p>哈哈,是不是我太蠢没能领悟到技巧,不对的地方欢迎留言指正哈。不过前端工程师都是挑剔的,希望<code>Weex</code>能不断改进和完善!</p>
React专利许可证研究
https://segmentfault.com/a/1190000011290285
2017-09-21T10:53:31+08:00
2017-09-21T10:53:31+08:00
jafeney
https://segmentfault.com/u/jafeney
17
<p>几天前,知乎上出来一个热门话题《如何看待百度要求内部全面停止使用 React / React Native?》,一时间被邀请回答的大咖们就展开了这场没有硝烟的战争。主要有正反两方,跟个大辩论赛似的,很是激烈。我顺道总结下:</p>
<h2>洗白方</h2>
<p>洗白方的论据主要有5点:</p>
<h3>协议一直如此,而且本身是防御性的</h3>
<p>(1)<code>Facebook</code>的<code>React</code>专利授权是关于分享其代码同时保留它能够抵御的专利诉讼<br>(2)<code>React</code>本身是开源的,受保护的是React里用到的专利<br>(3)<code>Facebook</code>不能主动使用<code>React</code>里的专利起诉你,因为这个授权是<code>irrevocable</code>的,除非你主动发起专利诉讼</p>
<h3>开源条例太苛刻</h3>
<p>ASF (Apache软件基金会)要求开源软件是没有限制的,所以依赖有<code>Facebook's BSD+ Patents license</code>的软件不被允许加入<code>ASF</code>。但也人会说<code>ASF</code>这个开源条件太苛刻,而如果不是<code>ASF</code>的软件没必要遵循这个限制。</p>
<h3>程序员过分解读 license</h3>
<p>(1)本质上你的软件不是要发布到 <code>ASF</code> 上就不会有问题<br>(2)只要不与 <code>FB</code> 有专利冲突就不受影响<br>(3)美国专利不在我国申请的话,不受我国法律保护(<code>Facebook</code>在我国并没有注册<code>React</code>相关专利)<br>(4)前端库互相抄袭的现象太多,法律和道德界限模糊</p>
<h3>分析百度为什么这么做</h3>
<p>(1)百度是一家在美国上市的公司<br>(2)百度有很多核心专利如人工智能 可能在未来和<code>FB</code>有利益冲突</p>
<h3>使用替代品依然有风险</h3>
<p>(1)<code>vue</code>和<code>preact</code>某些特性对React专利有侵权嫌疑,如<code>weex</code>还在使用<code>Yoga</code>引擎<br>(2)重构有成本,很多计划要重新部署,周期加长</p>
<h2>否定方</h2>
<p>否定方的理由很简单,共2个方面:</p>
<h3>事实依据</h3>
<p>(1)<code>Apache</code>、<code>WordPress</code>、百度纷纷叫停<code>React</code>,其严重性可见一斑<br>(2)阿里也计划逐步干掉<code>React</code>及其生态圈,准备了<code>Rax</code>、<code>Vue</code>和<code>Weex</code>等替代品</p>
<h3>专利风险</h3>
<p>(1)如果不想弃用<code>React</code>,那么你不能主动起诉<code>Facebook</code>,期间<code>Facebook</code>可以免费使用你的所有专利(对,是所有)。<br>(2)如果因此诉讼 <code>Facebook</code>,就失去了使用<code>React</code>的授权(相当于把柄被别人抓住)<br>(3)<code>React</code>生态圈的东西用的越多,被<code>Facebook</code>扣的把柄也越多<br>(4)等真到了打官司的时候 完全剥离<code>React</code>难度和成本都很大,那还不如趁早。</p>
<h2>陈述</h2>
<p>正方双方说的都有一定道理,我先做下客观评论:<br>(1)<code>Facebook</code>这个专利许可证确实不地道,虽然个人没有和它有利益冲突,但作为公司管理层,站在公司的立场上还是觉得芒刺在背(尽管公司短期内不可能和<code>Facebook</code>会有利益竞争,但梦还是要有的,未来——虽然远,但多一事不如少一事)。</p>
<blockquote><p>如果公司的核心技术已申请了专利,并且非常有前景的话,而且打算几年内在美上市的,绝壁不能用React及一切生态圈的东西。不管公司<code>React</code>搞得有多6,还是要忍痛割爱,长痛不如短痛(内部系统应该还能用,人家应该也管不着)。</p></blockquote>
<p>(2)而对于国内一些创业型企业,说实话,真和你没有半毛钱关系。<code>React</code>技术虽然有专利许可证,但<code>Facebook</code>免费让你用,并且<code>React</code>这门框架设计得确实好,没有理由不用啊。</p>
<blockquote><p>这里也不藏私,我从2015年开始搞<code>React</code>,到现在也算是深度患者,<code>React</code>是我所用过最好的框架,尽管它的全家桶有些不怎么好用,但框架本身的设计真的非常优秀(同意吧)。而且现在主流的几款替代品<code>Preact</code>和<code>vue</code>也"借鉴"了不少<code>React</code>的东西,但是前端界"互相借鉴"这种事大家都是笑而不语的(哈哈,这种讨打的话各位自己去脑补吧)。</p></blockquote>
<p>(3)然后说说对未来的预测,这次风波后,国内<code>React</code>的占有率会有一定的下降,并且会持续下降(蚂蚁金服也作出回应<code>antd</code>会逐步放弃<code>React</code>向<code>Preact</code>或类似的<code>React</code>替代品方向发展)。</p>
<p><img src="/img/remote/1460000011290290" alt="这里写图片描述" title="这里写图片描述"></p>
<blockquote><p>并且阿里的的 <code>Rax</code>、<code>Vue</code>和<code>Weex</code>等技术也在不断推进,干掉<code>React</code>及<code>ReactNative</code>是时间的问题吧(如果<code>Facebook</code>不公开作出承诺不会以<code>React</code>为要挟随意使用其他公司的专利),当然一些小公司和没有对外专利技术的公司还是会继续用<code>React</code>及其生态圈,但屁股决定大脑,随着人才不断从大公司涌出渗透到小公司,<code>React</code>前端一哥的地位岌岌可危啊!</p></blockquote>
<h2>牢骚</h2>
<p>想起 <a href="https://link.segmentfault.com/?enc=ovngc4qKrJslY%2Bdkt9m3Mg%3D%3D.mCzLocvBWukibQQd0UtFaZqGjRfQ14qW%2F6%2FCSuYvwOotWxSGdUtmj5R4Knjl788YOj1DJGSy2c1dr%2FSqG4Dl9w%3D%3D" rel="nofollow">程墨Morgan</a> 在 知乎专栏里的几句话:两种公司会从此弃用<code>React</code>,第一是牛逼到足以和<code>Facebook</code>竞争的大公司,第二是装逼到自以为会和<code>Facebook</code>竞争的小公司(感觉我们公司属于第二种,哈哈)</p>
<p><img src="/img/remote/1460000011290291" alt="这里写图片描述" title="这里写图片描述"></p>
<p>最惨的应该是我,<code>md</code>我的<code>ReactNative</code>三端融合方案刚进行到一半,响应公司高层号召,现在要换成<code>vue + Weex</code>了。不过整理一下发型,想来做为一名卓越的前端工程师,也不能太依赖一门框架,没有<code>React</code>太阳照常升起,说到底还是<code>JavaScript</code>嘛,换个API,换个套路,继续搞起吧 ~_~。</p>
<p>唉,想起那年夕阳下的蹦跑,那是我逝去的青春 ——</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://link.segmentfault.com/?enc=1VwtxqAVWvM2Cxg5tsyoww%3D%3D.k%2BpS1%2BEP5svv9%2F3dP27XiCZ78aa2vJcO1%2BVyPAkxevsBNVh13pZYAECm56rDYdgdYhbqg56nHstNZi8qr2I2Tw%3D%3D" rel="nofollow">新浪:都该了解的 React license 争议</a></li>
<li><a href="https://link.segmentfault.com/?enc=reRExclsSVfsoYchp5PgMw%3D%3D.S1GqBtISksqN6U1LVmc%2FXI9EQHG5q9RqnUHgkW8Kyltw85TyM4Xz9OeIuiXbOE%2BqMaizYHp03iD396GfkrqjVg%3D%3D" rel="nofollow">酷壳:关于FACEBOOK 的 REACT 专利许可证</a></li>
<li><a href="https://link.segmentfault.com/?enc=OCybez8lPFGOxQZqkTB4vA%3D%3D.uOPmxXB0PZl67KiLptdxzB8zG0hMc5bImJ7b0kcrfcIUiOAk8qjfZvhgi%2FUePwxd" rel="nofollow">知乎:关于百度停用React</a></li>
<li><a href="https://link.segmentfault.com/?enc=EfIsMPP%2BCo%2BuFIi9ed5fmg%3D%3D.MCTj5Cz1rY8Bz%2Bx8ERrLPDbZ7NRR1ob2Q3ZEO5Tq4qA0LtnHfhGbp7IU5epkpSlp" rel="nofollow">知乎:如何看待百度要求内部全面停止使用 React / React Native?</a></li>
<li><a href="https://link.segmentfault.com/?enc=6FJl7GYpF%2F%2FiuOt3pLykkg%3D%3D.Ho7Xc2NejCsf3aap0M36Exbo3SUY9w8apDg4X6xnH6XfJWmGC9NMYVvvAOf6BLi%2B" rel="nofollow">知乎:阿里还会使用react吗?</a></li>
</ul>
<p>个人之见,大咖勿喷 ---</p>
ReactNative开发常用的三方模块
https://segmentfault.com/a/1190000008878128
2017-03-29T17:06:15+08:00
2017-03-29T17:06:15+08:00
jafeney
https://segmentfault.com/u/jafeney
59
<h2>写在前面</h2>
<p>一个好的App缺不了好的三方支持,生活在ReactNative这个活跃的开源社区,寻找合适的三方组件是一个开发者最基本的能力。不过不积跬步,无以至千里,不积小流,无以成江海。下面分享几个我收集的三方模块,希望对大家有点帮助。</p>
<h3>文件上传 <code>react-native-uploader</code>
</h3>
<p><img src="/img/remote/1460000008878131?w=355&h=614" alt="这里写图片描述" title="这里写图片描述"></p>
<p>github地址:<a href="https://link.segmentfault.com/?enc=hQS1EELMgqPRzfkViTajNw%3D%3D.r1ND6O399TUkeS57F3UtCcgkYqzcCcp%2BaO8B%2Bc%2BNjo9YV1lzFc10WmPFg6%2FHf%2Flu" rel="nofollow">https://github.com/aroth/reac...</a></p>
<blockquote><p>评价:支持多图上传和上传进度显示,demo比较粗糙。项目已经停止更新 4个月</p></blockquote>
<p><!--more--></p>
<h3>毛玻璃效果 <code>react-native-blur</code>
</h3>
<p><img src="/img/remote/1460000008878132?w=375&h=666" alt="这里写图片描述" title="这里写图片描述"></p>
<p>github 地址:<a href="https://link.segmentfault.com/?enc=i%2ByNTv9TnNTEU5fx%2BeXbAw%3D%3D.H7Bbwu%2F9t5RwXkZNZgQNYKMdlCqNaycnpOyoIA8sMx%2FIwuTW79j5hiGr4RcW6Waey8RFBSDZf0PI0TPM9UxPYw%3D%3D" rel="nofollow">https://github.com/react-nati...</a></p>
<blockquote><p>评价:星星比较多,支持3种常见的毛玻璃效果,不错的组件。</p></blockquote>
<h3>图片轮播</h3>
<ul><li><p><code>react-native-viewpager</code></p></li></ul>
<p><img src="/img/remote/1460000008878133?w=281&h=500" alt="这里写图片描述" title="这里写图片描述"><br> GitHub 地址:<a href="https://link.segmentfault.com/?enc=XsmyahkplJtcFOCENQGf7Q%3D%3D.RWRiK2ceKZIOGuNQPCx5yxlt6EmHJZ5bek6%2Fw3CzJNnciSXnnQ1Yw%2BTyT9uo8E%2FZoOrizqSDIIUJDA5fu6t4IA%3D%3D" rel="nofollow">https://github.com/race604/re...</a></p>
<blockquote><p>评价:实际使用过,轮播效果比较普通,算比较实用吧</p></blockquote>
<ul><li><p><code>react-native-looped-carousel</code></p></li></ul>
<p><img src="/img/remote/1460000008878134?w=368&h=660" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=j0m3y6H0HyJvKDphb%2B8%2BuA%3D%3D.bpSg0stDM%2F3VMl0TA3jkqhYSMuzE9UuXWJG1QbOfiaRC22LyCQJTpvx7%2Bad0kN44O%2FpAr1Oe4RkcfQVnFwiPVA%3D%3D" rel="nofollow">https://github.com/appintheai...</a></p>
<blockquote><p>评价:demo比较精致,可以尝试</p></blockquote>
<ul><li><p><code>react-native-app-intro</code></p></li></ul>
<p><img src="/img/remote/1460000008878135?w=342&h=617" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=G4Ohnx7D9tsE4IWsHPeoSQ%3D%3D.ovZfFkHR5iDjgq9LP5q9VyOtOKvvr%2BzE1B4%2F%2BDKHiXdFvRQ3WTbThCK4FM4whwEX5aOXA4Jhde1KFZzV4lAdNQ%3D%3D" rel="nofollow">https://github.com/FuYaoDe/re...</a></p>
<blockquote><p>评价:星星比较多,适合做App进入的引导页</p></blockquote>
<h3>图片选择</h3>
<ul><li><p><code>react-native-image-picker</code></p></li></ul>
<p><img src="/img/remote/1460000008878136?w=750&h=403" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=TYlOlqXeJrQdUqnIqMaOnw%3D%3D.JKG0IugfKUeTRdX9AjaXPeE0lhycGDfnrSNZ%2F9uVHPESRdpeIy40VdCyQ45%2FcpNl4Wl7vBbpcAQRaJj5pb0HyQ%3D%3D" rel="nofollow">https://github.com/marcshilli...</a></p>
<blockquote><p>评价:实际使用过,功能强大,兼容性好。但是不支持多图</p></blockquote>
<ul><li><p><code>react-native-image-crop-picker</code></p></li></ul>
<p><img src="/img/remote/1460000008878137?w=750&h=1334" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=jz6tCWa3nikerRJtaMLZXQ%3D%3D.Pb8XRP0SpS%2BlMMWJKdhFFa7ltbzt81cpbGo3a8LzC4pUsP60nQ2yyj4MjI6At7uLzQv7NKzfCD6i96oGXXwqYw%3D%3D" rel="nofollow">https://github.com/ivpusic/re...</a></p>
<blockquote><p>评价:功能类似,但支持多图</p></blockquote>
<h3>获取设备信息 <code>react-native-device-info</code>
</h3>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=lojqJVns5osbUz76bFL%2BSg%3D%3D.e31sS%2ByvroFjpSr0y52HAH65mzvdQhBXRcBjS3%2FYppuz47oSCnVnvB%2BnWbc0LdUe%2BFdSV8df7d2jEzDIvtJslQ%3D%3D" rel="nofollow">https://github.com/rebeccahug...</a></p>
<blockquote><p>评价:文档比较细致,算是靠谱的组件</p></blockquote>
<h3>ListView优化替代组件 <code>react-native-sglistview</code>
</h3>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=i86URHE2OqTI4200PwHAug%3D%3D.VL7uDm3q1EJaUCS8R%2FJauOmUUYI6szyTYxeTuj3gVEL%2B%2BTO0vE1GtOcQ%2F8W0m6NFMyokfkn%2BDhVlza%2FEdXEwXw%3D%3D" rel="nofollow">https://github.com/sghiassy/r...</a></p>
<blockquote><p>评价:用法简单,可以减小ListView运行占用的内存</p></blockquote>
<h3>二维码识别 <code>react-native-qrcode-reader</code>
</h3>
<p>github 地址:<a href="https://link.segmentfault.com/?enc=yGpwJ63mP6eNbB2dRp52Lg%3D%3D.o8x7CjDe7%2BOSrbcii5BBUfkSMQ8MfPMaxvjSibIfO7erVJWYHkR9VL%2BDoYc13KxerCcOYmYik0H3Y9JnlMYeZQ%3D%3D" rel="nofollow">https://github.com/starknx/re...</a></p>
<blockquote><p>评价:比较实用的功能</p></blockquote>
<h3>手势解锁 <code>react-native-gesture-password</code>
</h3>
<p><img src="/img/remote/1460000008878138?w=314&h=565" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=mbuXbBWipR%2FKHrT8YLBG3g%3D%3D.Vy%2BQGu88ghS1wDU0sIoQqryDncoTZeto78mTbzm4eKedj4NEBvxqSUldEJ72sn3BYtf%2BXowrqHBX1RDjn4o7EQ%3D%3D" rel="nofollow">https://github.com/SunflowerG...</a></p>
<blockquote><p>评价:为App添彩的功能</p></blockquote>
<p><img src="/img/remote/1460000008878139?w=180&h=320" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=xfH4wQkNsxRMnfVBMAQA0A%3D%3D.XOm1%2F3M5N0xaFqr36imBttAoyp0dCsKChlw3Ja%2BsqKIGim3wlWbLgbFjRczD7GWtTA0KgVRrS61RBGXcRX35ew%3D%3D" rel="nofollow">https://github.com/starknx/re...</a></p>
<blockquote><p>评价: 星星比较少,项目1年未更新,谨慎使用</p></blockquote>
<h3>键盘遮挡问题解决</h3>
<blockquote><p>评价:新版RN的<code>KeyboardAvoidingView</code>组件可以解决这个问题</p></blockquote>
<h3>图片查看 <code>react-native-gallery </code>
</h3>
<p><img src="/img/remote/1460000008878140" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=AcjkGhjitWpH5NjqowwnfA%3D%3D.PbdXc1v%2FM6QfJSvG2FBr11PYKv5vAgB8otSYxGEinfKEHa4rpFWpHDqN1fs%2FDK4LRbAQsIuZM0Z2wm5pYUgBbw%3D%3D" rel="nofollow">https://github.com/ldn0x7dc/r...</a></p>
<blockquote><p>评价:支持轮播和 放大查看</p></blockquote>
<h3>3D Touch <code>react-native-quick-actions</code>
</h3>
<p><img src="/img/remote/1460000008878141?w=750&h=403" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=%2BSawtadnws7Gy5Mrl2c06Q%3D%3D.ZZiNL6jj3EkCXiLD1F5FadgPIASSP8J4TPGUc1ZZgfIaGQCUatvHhGuCBRr32Gmp%2Bqq7ctBRFhtF77u4tiNpvQ%3D%3D" rel="nofollow">https://github.com/madriska/r...</a></p>
<blockquote><p>评价:为App添彩的功能,但不是必须的</p></blockquote>
<h3>可滑动的日历组件 <code>react-native-myCalendar</code>
</h3>
<p><img src="/img/remote/1460000008878142?w=370&h=673" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=N9ktEoyaaghn1udOzulikg%3D%3D.Tl85DU5bxDSsQn0WY158E5ro2FHIs%2FWbO8h%2BwY6UzWa%2BGEzTDtGlxwz%2BVv2H0A6eafMDh23uxPyLMjr3Vi41Mg%3D%3D" rel="nofollow">https://github.com/cqm1994617...</a></p>
<blockquote><p>评价:demo 耦合性略高</p></blockquote>
<h3>可拖拽元素 <code>react-native-gesture-recognizers</code>
</h3>
<p><img src="/img/remote/1460000008878143?w=371&h=685" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=TgLSqtw6dhZervkzFK48yg%3D%3D.1jz2r6pR%2BC5zQAdsUzOkcr13w4r8k3qXqaR7fwYkjkaa154N9m02fzhfIXomRJ7KnERDgrj4%2F36O4I0ImXfslAxt4P%2BDF8ueRObeNuXSdcQ%3D" rel="nofollow">https://github.com/johanneslu...</a></p>
<blockquote><p>评价:比较有趣的功能</p></blockquote>
<h3>下拉放大 <code>react-native-parallax-view</code>
</h3>
<p><img src="/img/remote/1460000008878144?w=373&h=663" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=ubAPlCv%2Bp%2B6IFiyx5VATkA%3D%3D.tO9afN73m0nlqZ56i8Sr47sg9al3dkmbrw0Xww4rKsK2FKS%2Fvgjzg4zZmpHB63EvltXGyBqG0Z8WDxPm%2BdAbYw%3D%3D" rel="nofollow">https://github.com/lelandrich...</a></p>
<blockquote><p>评价:这个功能适合带图片的详情页</p></blockquote>
<h3>简单图表 <code>react-native-chart</code>
</h3>
<p><img src="/img/remote/1460000008878145?w=750&h=1334" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=LkrjZWsCP5AvvJrxNmdZVg%3D%3D.VnzxmjyLhiInuaTHSs6sT%2B8gXnpseXVOpPQErcMJe%2BNWHcXFvkhqUrTWDlmQYQcm" rel="nofollow">https://github.com/tomauty/re...</a></p>
<blockquote><p>评价:比较成熟的项目,放心使用</p></blockquote>
<h3>侧滑按钮 <code>react-native-swipeout</code>
</h3>
<p><img src="/img/remote/1460000008878146?w=367&h=120" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=m4g47Np587xEq8r7rRKVBg%3D%3D.MGPnRdMN5iRKlOY7%2F3z3Bz4rICHxYPPGqemIZW3%2BI3icwagfXQeGAsdtYbDS%2BKNr4H5ST2hpzYjK5vELZRu%2BBw%3D%3D" rel="nofollow">https://github.com/dancormier...</a></p>
<blockquote><p>评价:如果放在水平滚动的容器里会有BUG</p></blockquote>
<h3>抽屉功能 <code>react-native-drawer</code>
</h3>
<p><img src="/img/remote/1460000008878147?w=563&h=1000" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=szGz1GE%2F8A50xT4jZVHhcg%3D%3D.X5v9R3jH%2FBXX%2BXJb%2B1KoQYHEpGTCX10TJjnA0XSSQ4whEynG6tJ2IFM9PwtlP3cJ" rel="nofollow">https://github.com/root-two/r...</a></p>
<blockquote><p>评价:实际使用过,性能还不错,可放心使用</p></blockquote>
<h3>加载动画 <code>react-native-spinkit</code>
</h3>
<p><img src="/img/remote/1460000008878148?w=202&h=360" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=JuuVTeiVxmtjXjCtgqxDXw%3D%3D.CH6uHbp95h%2F%2FObCVSVa9DQW2W%2Fa%2FOckh%2FfbdDuff17Jh%2BE2el2lWgc%2BTxuWAebJG" rel="nofollow">https://github.com/maxs15/rea...</a></p>
<blockquote><p>评价:比较有趣的动画,为项目添彩</p></blockquote>
<h3>登录动画 <code>react-native-login</code>
</h3>
<p><img src="/img/remote/1460000008878149?w=365&h=670" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=hWRnH24ie6b0L5bUo3PQTw%3D%3D.RVP7J8%2B%2BPtLJOuB56YkQNBBEh16Julgtlej8vzBLIqlsDPUEL44SFsJPX5t8aVBpiBtaWD%2BTjo5TMH4Q5mSu8A%3D%3D" rel="nofollow">https://github.com/brentvatne...</a></p>
<blockquote><p>评价:动画为mp4格式</p></blockquote>
<h3>动画组件 <code>react-native-animatable</code>
</h3>
<p><img src="/img/remote/1460000008878150?w=306&h=548" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=LFkN4xZMZZytqwrlD7SBPg%3D%3D.3JD4VvVH6vFbHob%2Fp7EYB3LH3OFIIhYwRD8ffo49kLk4KCW6im75CQkTnUIuA6YoykE4okJ2FKAOJEf0tYdgrQ%3D%3D" rel="nofollow">https://github.com/oblador/re...</a></p>
<blockquote><p>评价:为元素添加灵动感,比较实用</p></blockquote>
<h3>即时通讯</h3>
<ul><li><p><code>react-native-gifted-chat</code></p></li></ul>
<p><img src="/img/remote/1460000008878151?w=320&h=568" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=VFXqgt9dGMNTDnuIJNmXfw%3D%3D.%2F3mcqihRQ7mbX298suH12USymItUsGBGM9YU188QkKzf%2FuBeOCZu3sTCsLOJ7GfIwOrbKGXywwepQZLxeNp%2Bqw%3D%3D" rel="nofollow">https://github.com/FaridSafi/...</a></p>
<blockquote><p>评价:支持发送位置和图片</p></blockquote>
<ul><li><p><code>react-native-imUI</code></p></li></ul>
<p><img src="/img/remote/1460000008878152?w=250&h=445" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=a%2Bzymi2%2Fef9dlGuQr4tulA%3D%3D.c0xFi6RhbQEEhljj3Ncr2lMzm38fQQM5g4a7OUpZ38aQcefHWzxi0fDJil4vYMzy" rel="nofollow">https://github.com/Ice-MT/rea...</a></p>
<blockquote><p>评价:从项目里抽取出来的demo,UI做的挺萌,有发送语音功能</p></blockquote>
<h3>精致的输入框 <code>react-native-textinput-effects</code>
</h3>
<p><img src="/img/remote/1460000008878153?w=746&h=436" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=vjfFzoxihtqGEkBDXOto%2Fw%3D%3D.H7H6bx7YFe3Mqm%2FhlZGVrH6Se4DD1Vr5eiHk3tV0sGaTqXu4ObQTy4cQHQe%2BKeG%2FrjgUaLgZtTNhgO4Y2RshNA%3D%3D" rel="nofollow">https://github.com/halilb/rea...</a></p>
<blockquote><p>评价:为项目添彩的功能</p></blockquote>
<h3>表单验证 <code>react-native-gifted-form</code>
</h3>
<p><img src="/img/remote/1460000008878154?w=315&h=581" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=osRh7HFfkHpn%2FomIsGB7ig%3D%3D.F%2FWir%2BugcNulGon9J9dAYfaIyKOooi2CXn4FXgvO60XoGCsZqCEwTwBz6JZ%2FwpGWjbGBQcA%2Bkwdzd%2FX%2BjT9pWQ%3D%3D" rel="nofollow">https://github.com/FaridSafi/...</a></p>
<blockquote><p>评价:比较实用,适合用在复杂的表单</p></blockquote>
<h3>UI组件库</h3>
<ul><li><p><code>NativeBase</code></p></li></ul>
<p><img src="/img/remote/1460000008878155" alt="这里写图片描述" title="这里写图片描述"></p>
<p>github:<a href="https://link.segmentfault.com/?enc=8%2F3WiW4dpL3dLS2dMi2ong%3D%3D.xsXQNgyJuzTrjT7xEetofVfJTP40gS49b2RfZoSc%2Bu56WrAkWOnSbujdwIgdwTr8" rel="nofollow">https://github.com/GeekyAnts/...</a> <br><br> 在线文档:<a href="https://link.segmentfault.com/?enc=1jnddFzGTfE3OKXJt1wPIw%3D%3D.2sX%2F7FBg5s28gJncEPddF0RJAW8gCRQCKPvdJAVyjNR2aiLVyU6OQFTyhfCGM5JOhiR76V6a9IKgunL%2FyuC9hg%3D%3D" rel="nofollow">http://nativebase.io/docs/v0....</a></p>
<blockquote><p>评价:组件比较多,不过设计风格一般</p></blockquote>
<ul><li><p><code>shoutem</code></p></li></ul>
<p><img src="/img/remote/1460000008878156?w=1740&h=980" alt="这里写图片描述" title="这里写图片描述"></p>
<p>团队github: <a href="https://link.segmentfault.com/?enc=Df%2Blmej6pS%2FC9vpfhJek8Q%3D%3D.RIIOilTVZF0oZqCtnqEQukgZjYtpHxX%2Fdf%2FpEu4o8Kk%3D" rel="nofollow">https://github.com/shoutem</a> <br><br> 在线文档:<a href="https://link.segmentfault.com/?enc=k%2FHGCzAn8NJrl7cQyocBEg%3D%3D.yg%2BM9%2BpfLvUOe0X6vMnyBf6ghOb5eewFWqkDBbnPfXusVUjCPEdzVcTmb%2BSKKMgiWeiiphOEYC1B6FYFVbtVww%3D%3D" rel="nofollow">http://shoutem.github.io/docs...</a></p>
<blockquote><p>评价:组件丰富,设计风格酷炫、团队也比较牛逼</p></blockquote>
<ul><li><p><code>Teaset</code></p></li></ul>
<p><img src="/img/remote/1460000008878157?w=375&h=667" alt="这里写图片描述" title="这里写图片描述"></p>
<p>github: <a href="https://link.segmentfault.com/?enc=jdLo3T6EszSEHbR9j2QkyA%3D%3D.cwDBWyehvOEuemkC%2BcUZ412DdcAi0EQ5kNrlgjQD9hY%3D" rel="nofollow">https://github.com/rilyu/teaset</a></p>
<blockquote><p>评价:国人作品,组件丰富,设计风格简约,比较适宜新手实用吧</p></blockquote>
<ul><li><p><code>react-native-material-design</code></p></li></ul>
<p><img src="/img/remote/1460000008878158?w=1804&h=1064" alt="这里写图片描述" title="这里写图片描述"></p>
<p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=z86tQ76cMBH%2Bpp%2FaWYx%2F6Q%3D%3D.Ughgx9a3QRIr3IiffrXFljVsZ7W02i6SoTV3QydISSwjF5rzcnuuMhMT3Cd1ALTdaECcNrqz2ggZaVVQ52SGv2bi%2B%2F6rWz5XCTlHPrVD%2FIY%3D" rel="nofollow">https://github.com/react-nati...</a></p>
<blockquote><p>评价:纯js编写,没有依赖,demo用的是Android</p></blockquote>
<ul><li><p><code>react-native-elements</code></p></li></ul>
<p><img src="/img/remote/1460000008878159?w=1974&h=1200" alt="这里写图片描述" title="这里写图片描述"></p>
<p>github: <a href="https://link.segmentfault.com/?enc=Fs%2Fkn1Pf%2FryTY8tQdeTZEQ%3D%3D.HbMuq5HsF9tTixAd0HbF8g2KC8BFT7RHnFdAPbwslioBKdSfLOzs8rBGY6%2BP%2FWuCm5zJZpx%2F0s0dtypiQJl%2FKbErCGP2CQhYRtpRHQpKpMU%3D" rel="nofollow">https://github.com/react-nati...</a> <br><br> 在线文档:<a href="https://link.segmentfault.com/?enc=FU%2FobKgYRh9q7tNtj22Qrg%3D%3D.vP0LxXlbSQODbE2%2FuRjsUXEVdL5bw%2BK8cp3iFP6qAS9GkqFHUVkVd1jgRP4lxKKF" rel="nofollow">http://react-native-material-...</a></p>
<blockquote><p>评价:ReactNative作者操刀领导的作品,值得拥有</p></blockquote>
<ul><li><p><code>react-native-ui-kitten</code></p></li></ul>
<p><img src="/img/remote/1460000008878160?w=250&h=445" alt="这里写图片描述" title="这里写图片描述"></p>
<p>github: <a href="https://link.segmentfault.com/?enc=q4QP01Yc%2FYrZwVUSXnkQ6g%3D%3D.lcmDrprxvhnqRZHbDgzsPFeipeCWv8y4d%2Fh4gRlcOxr%2FBWWDUnsmD5YXjaaSHKK1" rel="nofollow">https://github.com/akveo/reac...</a> <br><br> 在线文档:<a href="https://link.segmentfault.com/?enc=0VvKbDDANCAscHV2decfpg%3D%3D.qoHdk2kGgelev2Be9mbKxPB8L3Y88csiX0fd7naVGcbHmkq0SxMRUJEJjTaMZdWOfaeoqB8xwl0xv%2Butg3iwAHEEBxXbKmrdoKOciyfGhmU%3D" rel="nofollow">https://akveo.github.io/react...</a></p>
<blockquote><p>评价:一个酷炫简单的App组件Demo,适合新手学习</p></blockquote>
<hr>
<blockquote><p>@参考:<a href="https://link.segmentfault.com/?enc=xbnIGD9slzfELPGeetKm5Q%3D%3D.2FoFY3CvNHmPS5XlpNn4CO5QHIo4nQ%2FXm7RxTeziroMktcoiqVGET8rj0i9Ts51v" rel="nofollow">React Native 项目常用第三方组件汇总</a></p></blockquote>
<p>欢迎关注我的个人博客 <a href="https://link.segmentfault.com/?enc=AltbtaxltQ8j6ys0Lt5%2Frw%3D%3D.%2Bmd1Fw9SzEKYR4sGsJbCAVJkAI%2FrkFSD4%2FFEjfTtA5k%3D" rel="nofollow">Jafeney</a></p>
JavaScript异步编程的终极演变
https://segmentfault.com/a/1190000006510526
2016-08-15T23:52:30+08:00
2016-08-15T23:52:30+08:00
jafeney
https://segmentfault.com/u/jafeney
11
<h2>写在前面</h2>
<p>有一个有趣的问题:</p>
<blockquote><p>为什么<code>Node.js</code>约定回调函数的第一个参数必须是错误对象<code>err</code>(如果没有错误,该参数就是<code>null</code>)?</p></blockquote>
<p>原因是执行回调函数对应的异步操作,它的执行分成两段,这两段之间抛出的错误程序无法捕获,所以只能作为参数传入第二段。大家知道,<code>JavaScript</code>只有一个线程,如果没有异步编辑,复杂的程序基本没法使用。在ES6诞生以前,异步编程的方式大概有下面四种:</p>
<ul>
<li><p>回调函数</p></li>
<li><p>事件监听</p></li>
<li><p>发布/订阅</p></li>
<li><p><code>Promise</code>对象</p></li>
</ul>
<p>ES6将<code>JavaScript</code>异步编程带入了一个全新的阶段,ES7中的<code>async</code>函数更是给出了异步编程的终极解决方案。下面将具体讲解异步编程的原理和值得注意的地方,待我细细道来~</p>
<h2>异步编程的演变</h2>
<h3>基本理解</h3>
<p>所谓<code>异步</code>,简单地说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好准备再回过头执行第二段。</p>
<p><strong>举个例子</strong><br>读取一个文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。</p>
<p>相应地,连续的执行就叫作同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。</p>
<h3>回调函数</h3>
<p>所谓回调函数,就是把任务的第二段单独写在一个函数中,等到重新执行该任务时直接调用这个函数。其英文名字 <code>callback</code> 直译过来就是 "重新调用"的意思。</p>
<p>拿上面的例子讲,读取文件操作是这样的:</p>
<pre><code class="JavaScript">fs.readFile(fileA, (err, data) => {
if (err) throw err;
console.log(data)
})
fs.readFile(fileB, (err, data) => {
if (err) throw err;
console.log(data)
})</code></pre>
<blockquote><p>注意:上面两段代码彼此是异步的,虽然开始执行的顺序是从上到下,但是第二段并不会等到第一段结束才执行,而是并发执行。</p></blockquote>
<p>那么问题来了,如果想<code>fileB</code>等到<code>fileA</code>读取成功后再开始执行应该怎么处理呢?最简单的办法是通过 <strong>回调嵌套</strong>:</p>
<pre><code class="JavaScript">fs.readFile(fileA, (err, data) => {
if (err) throw err;
console.log(data)
fs.readFile(fileB, (_err, _data) => {
if (_err) throw err;
console.log(_data)
})
})</code></pre>
<p>这种方式我只能容忍个位数字的嵌套,而且它使得代码横向发展,实在是丑的一笔,次数多了根本是没法看。试想万一要同步执行100个异步操作呢?疯掉算了吧!有没有更好的办法呢?</p>
<h3>使用<code>Promise</code>
</h3>
<p>要澄清一点,<code>Promise</code>的概念并不是<code>ES6</code>新出的,而是<code>ES6</code>整合了一套新的写法。同样继续上面的例子,使用<code>Promise</code>代码就变成这样了:</p>
<pre><code class="JavaScript">var readFile = require('fs-readfile-promise');
readFile(fileA)
.then((data)=>{console.log(data)})
.then(()=>{return readFile(fileB)})
.then((data)=>{console.log(data)})
// ... 读取n次
.catch((err)=>{console.log(err)})</code></pre>
<blockquote><p>注意:上面代码使用了<code>Node</code>封装好的<code>Promise</code>版本的<code>readFile</code>函数,它的原理其实就是返回一个<code>Promise</code>对象,咱也简单地写一个:</p></blockquote>
<pre><code class="JavaScript">var fs = require('fs');
var readFile = function(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
module.export = readFile</code></pre>
<blockquote><p>但是,<code>Promise</code>的写法只是回调函数的改进,使用<code>then()</code>之后,异步任务的两段执行看得更清楚,除此之外并无新意。撇开优点,<code>Promise</code>的最大问题就是代码冗余,原来的任务被<code>Promise</code>包装一下,不管什么操作,一眼看上去都是一堆<code>then()</code>,原本的语意变得很不清楚。</p></blockquote>
<p>把酒问苍天,MD还有更好的办法吗?</p>
<h3>使用<code>Generator</code>
</h3>
<p>在引入<code>generator</code>之前,先介绍一下什么叫 <strong>协程</strong></p>
<blockquote><p>"携程在手,说走就走"。哈哈,别混淆了, "<strong>协程</strong>" 非 "<strong>携程</strong>"</p></blockquote>
<h4>协程</h4>
<p>所谓 "协程" ,就是多个线程相互协作,完成异步任务。协程有点像函数,又有点像线程。其运行流程大致如下:</p>
<ul>
<li><p>第一步: 协程A开始执行</p></li>
<li><p>第二步:协程A执行到一半,暂停,执行权转移到协程B</p></li>
<li><p>第三步:一段时间后,协程B交还执行权</p></li>
<li><p>第四步:协程A恢复执行</p></li>
</ul>
<pre><code class="JavaScript">function asyncJob() {
// ... 其他代码
var f = yield readFile(fileA);
// ... 其他代码
}</code></pre>
<blockquote><p>上面的<code>asyncJob()</code>就是一个协程,它的奥妙就在于其中的<code>yield</code>命令。它表示执行到此处执行权交给其他协程,换而言之,<code>yield</code>就是异步两个阶段的分界线。</p></blockquote>
<p>协程遇到<code>yield</code>命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点就是代码的写法非常像同步操作,如果除去 <code>yield</code>命令,简直一模一样。</p>
<h4>
<code>Generator</code>函数</h4>
<p><code>Generator</code>函数是协程在ES6中的实现,最大的特点就是可以交出函数的执行权(即暂停执行)。整个<code>Generator</code>函数就是一个封装的异步任务,或者说就是异步任务的容器。</p>
<pre><code class="JavaScript">function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }</code></pre>
<p>上面的代码中,调用<code>Generator</code>函数,会返回一个内部指针(即遍历器)g,这是<code>Generator</code>函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针g的<code>next()</code>方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的<code>yield</code>语句。</p>
<p>换而言之,<code>next()</code>方法的作用是分阶段执行<code>Generator</code>函数。每次调用<code>next()</code>方法,会返回一个对象,表示当前阶段的信息(<code>value</code>属性和<code>done</code>属性)。<code>value</code>属性是<code>yield</code>语句后面表达式的值,表示当前阶段的值;<code>done</code>属性是一个布尔值,表示<code>Generator</code>函数是否执行完毕,即是否还有一个阶段。</p>
<h4>
<code>Generator</code>函数的数据交换和错误处理</h4>
<p><code>Generator</code>函数可以暂停执行和恢复执行,这是它封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的解决方案:函数体内外的数据交换和错误处理机制。</p>
<p><code>next()</code>方法返回值的<code>value</code>属性,是<code>Generator</code>函数向外输出的数据;<code>next()</code>方法还可以接受参数,向<code>Generator</code>函数体内输入数据。</p>
<pre><code class="JavaScript">function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }</code></pre>
<blockquote><p>上面的代码中,第一个<code>next()</code>方法的<code>value</code>属性,返回表达式<code>x+2</code>的值(3)。第二个<code>next()</code>方法带有参数2,这个参数可以传入<code>Generator</code>函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收,因此这一步的<code>value</code>属性返回的就是2(变量y的值)。</p></blockquote>
<p><code>Generator</code>函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。</p>
<pre><code class="JavaScript">function* gen(x) {
try {
var y = yield x + 2
} catch(e) {
console.log(e)
}
return y
}
var g = gen(1);
g.next();
g.throw('出错了');</code></pre>
<p>上面代码的最后一行,<code>Generator</code>函数体外,使用指针对象的<code>throw</code>方法抛出的错误,可以被函数体内的<code>try...catch</code> 代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。</p>
<h4>异步任务的封装</h4>
<p>下面看看如何使用<code>Generator</code>函数,执行一个真实的异步任务。</p>
<pre><code>var fetch = require('node-fetch')
function* gen() {
var url = 'https://api.github.com/usrs/github';
var result = yield fetch(url);
console.log(result.bio);
} </code></pre>
<blockquote><p>上面代码中,<code>Generator</code>函数封装了一个异步操作,该操作先读取一个远程接口,然后从<code>JSON</code>格式的数据解析信息。就像前面说过的,这段代码非常像同步操作。除了加上<code>yield</code>命令。</p></blockquote>
<p>执行这段代码的方法如下:</p>
<pre><code>var g = gen();
var result = g.next();
result.value.then(function(data) {
return data.json()
}).then(function(data) {
g.next(data)
});</code></pre>
<p>上面代码中,首先执行<code>Generator</code>函数,获取遍历器对象。然后使用<code>next()</code>方法,执行异步任务的第一阶段。由于<code>Fetch</code>模块返回的是一个<code>Promise</code>对象,因此需要用<code>then()</code>方法调用下一个<code>next()</code>方法。</p>
<p>可以看到,虽然<code>Generator</code>函数将异步操作表示得很简洁,但是流程管理却不方便(即合适执行第一阶段,何时执行第二阶段)</p>
<h3>大Boss登场之 <code>async</code>函数</h3>
<p>所谓<code>async</code>函数,其实是<code>Generator</code>函数的语法糖。</p>
<p>继续我们异步读取文件的例子,使用<code>Generator</code>实现</p>
<pre><code class="JavaScript">var fs = require('fs');
var readFile = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
var gen = function* () {
var f1 = yield readFile(fileA);
var f2 = yield readFile(fileB);
console.log(f1.toString());
console.log(f2.toString());
}</code></pre>
<p>写成<code>async</code>函数,就是下面这样:</p>
<pre><code class="JavaScript">var asyncReadFile = async function() {
var f1 = await readFile(fileA);
var f2 = await readFile(fileB);
console.log(f1.toString())
console.log(f2.toString())
}</code></pre>
<p>发现了吧,<code>async</code>函数就是将<code>Generator</code>函数的<code>*</code>替换成了<code>async</code>,将<code>yield</code>替换成<code>await</code>,除此之外,还对 <code>Generator</code>做了以下四点改进:</p>
<p>(1)内置执行器。<code>Generator</code>函数的执行比如靠执行器,所以才有了<code>co</code>模块等异步执行器,而<code>async</code>函数是自带执行器的。也就是说:<strong><code>async</code>函数的执行,与普通函数一模一样,只要一行:</strong></p>
<pre><code class="JavaScript">var result = asyncReadFile();</code></pre>
<p>(2)上面的代码调用了<code>asyncReadFile()</code>,就会自动执行,输出最后结果。这完全不像<code>Generator</code>函数,需要调用<code>next()</code>方法,或者使用<code>co</code>模块,才能得到真正执行,从而得到最终结果。</p>
<p>(3)更好的语义。<code>async</code>和<code>await</code>比起星号和<code>yield</code>,语义更清楚。<code>async</code>表示函数里有异步操作,<code>await</code>表示紧跟在后面的表达式需要等待结果。</p>
<p>(4)更广的适用性。<code>async</code>函数的<code>await</code>命令后面可以是<code>Promise</code>对象和原始类型的值(数值、字符串和布尔值,而这是等同于同步操作)。</p>
<p>(5)返回值是<code>Promise</code>,这比<code>Generator</code>函数返回的是<code>Iterator</code>对象方便多了。你可以用<code>then()</code>指定下一步操作。</p>
<blockquote><p>进一步说,<code>async</code>函数完全可以看作由多个异步操作包装成的一个<code>Promise</code>对象,而<code>await</code>命令就是内部<code>then()</code>命令的语法糖。</p></blockquote>
<h4>实现原理</h4>
<p><code>async</code>函数的实现就是将<code>Generator</code>函数和自动执行器包装在一个函数中。如下代码:</p>
<pre><code class="JavaScript">async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function*() {
// ...
})
}
// 自动执行器
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF()
} catch(e) {
return reject(e)
}
if (next.done) {
return resolve(next.value)
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v) })
},function(e) {
step(function() { return gen.throw(e) })
})
}
step(function() { return gen.next(undefined) })
})
}</code></pre>
<h4>
<code>async</code>函数用法</h4>
<p>(1)<code>async</code>函数返回一个<code>Promise</code>对象,可以是<code>then()</code>方法添加回调函数。<br>(2)当函数执行时,一旦遇到<code>await()</code>就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。</p>
<p>下面是一个延迟输出结果的例子:</p>
<pre><code class="JavaScript">function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
// 延迟500ms后输出 "Hello World!"
asyncPrint('Hello World!', 500)</code></pre>
<h4>注意事项</h4>
<p>(1)<code>await</code>命令后面的<code>Promise</code>对象,运行结果可能是<code>reject</code>,所以最好把<code>await</code>命令放在<code>try...catch</code>代码块中。</p>
<p>(2)<code>await</code>命令只能用在<code>async</code>函数中,用在普通函数中会报错。</p>
<p>(3)<code>ES6</code>将<code>await</code>增加为保留字。如果使用这个词作为标识符,在<code>ES5</code>中是合法的,但是<code>ES6</code>会抛出 <code>SyntaxError</code>(语法错误)。</p>
<h2>终极一战</h2>
<p>"倚天不出谁与争锋",上面介绍了一大堆,最后还是让我们通过一个例子来看看 <code>async</code> 函数和<code>Promise</code>、<code>Generator</code>到底谁才是真正的老大吧!</p>
<blockquote><p>需求:假定某个DOM元素上部署了一系列的动画,前一个动画结束才能开始后一个。如果当中又一个动画出错就不再往下执行,返回上一个成功执行动画的返回值。</p></blockquote>
<h3>用<code>Promise</code>实现</h3>
<pre><code class="JavaScript">function chainAnimationsPromise(ele, animations) {
// 变量ret用来保存上一个动画的返回值
var ret = null;
// 新建一个空的Promise
var p = Promise.resolve();
// 使用then方法添加所有动画
for (var anim in animations) {
p = p.then(function(val) {
ret = val;
return anim(ele);
})
}
// 返回一个部署了错误捕获机制的Promise
return p.catch(function(e) {
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
})
}</code></pre>
<p>虽然<code>Promise</code>的写法比起回调函数的写法有很大的改进,但是操作本身的语义却变得不太明朗。</p>
<h4>用<code>Generator</code>实现</h4>
<pre><code class="JavaScript">function chainAnimationsGenerator(ele, animations) {
return spawn(function*() {
var ret = null;
try {
for(var anim of animations) {
ret = yield anim(ele)
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
})
}</code></pre>
<p>使用<code>Generator</code>虽然语义比<code>Promise</code>写法清晰不少,但是用户定义的操作全部出现在<code>spawn</code>函数的内部。这个写法的问题在于,必须有一个任务运行器自动执行<code>Generator</code>函数,它返回一个<code>Promise</code>对象,而且保证<code>yield</code>语句后的表达式返回的是一个<code>Promise</code>。上面的<code>spawn</code>就扮演了这一角色。它的实现如下:</p>
<pre><code class="JavaScript">function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF()
} catch(e) {
return reject(e)
}
if (next.done) {
return resolve(next.value)
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v) })
},function(e) {
step(function() { return gen.throw(e) })
})
}
step(function() { return gen.next(undefined) })
})
}</code></pre>
<h3>使用<code>async</code>实现</h3>
<pre><code class="JavaScript">async function chainAnimationAsync(ele, animations) {
var ret = null;
try {
for(var anim of animations) {
ret = await anim(ele)
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
}</code></pre>
<p>好了,光从代码量上就看出优势了吧!简洁又符合语义,几乎没有不相关代码。完胜!</p>
<blockquote><p>注意一点:<code>async</code>属于ES7的提案,使用时请通过<code>babel</code>或者<code>regenerator</code>进行转码。</p></blockquote>
<h2>参考</h2>
<p>阮一峰 《ES6标准入门》</p>
<hr>
<p>@欢迎关注我的 <a href="https://link.segmentfault.com/?enc=3tB5umZLgbPf%2FA6ofCjMWg%3D%3D.e1SDU%2FzesgFuRQbLgVdKyRRwIlbXYe%2BW94pkBHNmx2Y%3D" rel="nofollow">github</a> 和 <a href="https://link.segmentfault.com/?enc=%2F%2FL1740dtywlm981nxl9nA%3D%3D.I1e%2BQG4MMHUU2QECX6G72MUw8bs22%2F6NnQR6uOqAcKI%3D" rel="nofollow">个人博客 -Jafeney</a></p>
基于Node的React图片上传组件实现
https://segmentfault.com/a/1190000006235845
2016-08-11T10:41:44+08:00
2016-08-11T10:41:44+08:00
jafeney
https://segmentfault.com/u/jafeney
2
<h2>写在前面</h2>
<p>红旗不倒,誓把<code>JavaScript</code>进行到底!今天介绍我的开源项目 <a href="https://link.segmentfault.com/?enc=ZefjhPQs6nNjdBFKlG7RJw%3D%3D.R7TFyJvq7ByGs5LfK8CMmI4VfooYRpY672iNsvwBjFpCVSRxti9T1i%2FeEZYGXem5" rel="nofollow"><code>Royal</code></a> 里的图片上传组件的前后端实现原理(<code>React</code> + <code>Node</code>),花了一些时间,希望对你有所帮助。</p>
<p><img src="/img/remote/1460000006787915" alt="这里写图片描述" title="这里写图片描述"></p>
<h2>前端实现</h2>
<p>遵循<code>React</code> 组件化的思想,我把图片上传做成了一个独立的组件(没有其他依赖),直接<code>import</code>即可。</p>
<pre><code>import React, { Component } from 'react'
import Upload from '../../components/FormControls/Upload/'
//......
render() {
return (
<div><Upload uri={'http://jafeney.com:9999/upload'} /></div>
)
}</code></pre>
<blockquote><p><code>uri</code> 参数是必须传的,是图片上传的后端接口地址,接口怎么写下面会讲到。</p></blockquote>
<h3>渲染页面</h3>
<p>组件<code>render</code>部分需要体现三个功能:</p>
<ul>
<li><p>图片选取(dialog窗口)</p></li>
<li><p>可拖拽功能(拖拽容器)</p></li>
<li><p>可预览(预览列表)</p></li>
<li><p>上传按钮 (button)</p></li>
<li><p>上传完成图片地址和链接 (信息列表)</p></li>
</ul>
<h4>主<code>render</code>函数</h4>
<pre><code>render() {
return (
<form action={this.state.uri} method="post" encType="multipart/form-data">
<div className="ry-upload-box">
<div className="upload-main">
<div className="upload-choose">
<input
onChange={(v)=>this.handleChange(v)}
type="file"
size={this.state.size}
name="fileSelect"
accept="image/*"
multiple={this.state.multiple} />
<span ref="dragBox"
onDragOver={(e)=>this.handleDragHover(e)}
onDragLeave={(e)=>this.handleDragHover(e)}
onDrop={(e)=>this.handleDrop(e)}
className="upload-drag-area">
或者将图片拖到此处
</span>
</div>
<div className={this.state.files.length?
"upload-preview":"upload-preview ry-hidden"}>
{ this._renderPreview(); // 渲染图片预览列表 }
</div>
</div>
<div className={this.state.files.length?
"upload-submit":"upload-submit ry-hidden"}>
<button type="button"
onClick={()=>this.handleUpload()}
class="upload-submit-btn">
确认上传图片
</button>
</div>
<div className="upload-info">
{ this._renderUploadInfos(); // 渲染图片上传信息 }
</div>
</div>
</form>
)
}</code></pre>
<h4>渲染图片预览列表</h4>
<pre><code>_renderPreview() {
if (this.state.files) {
return this.state.files.map((item, idx) => {
return (
<div className="upload-append-list">
<p>
<strong>{item.name}</strong>
<a href="javascript:void(0)"
className="upload-delete"
title="删除" index={idx}></a>
<br/>
<img src={item.thumb} className="upload-image" />
</p>
<span className={this.state.progress[idx]?
"upload-progress":
"upload-progress ry-hidden"}>
{this.state.progress[idx]}
</span>
</div>
)
})
} else {
return null
}
}</code></pre>
<h4>渲染图片上传信息列表</h4>
<pre><code>_renderUploadInfos() {
if (this.state.uploadHistory) {
return this.state.uploadHistory.map((item, idx) => {
return (
<p>
<span>上传成功,图片地址是:</span>
<input type="text" class="upload-url" value={item.relPath}/>
<a href={item.relPath} target="_blank">查看</a>
</p>
);
})
} else {
return null;
}
}</code></pre>
<h3>文件上传</h3>
<p>前端要实现图片上传的原理就是通过构建<code>FormData</code>对象,把文件对象<code>append()</code>到该对象,然后挂载在<code>XMLHttpRequest</code>对象上 <code>send()</code> 到服务端。</p>
<h4>获取文件对象</h4>
<p>获取文件对象需要借助 <code>input</code> 输入框的 <code>change</code> 事件来获取 句柄参数 <code>e</code></p>
<pre><code>onChange={(e)=>this.handleChange(e)}</code></pre>
<p>然后做以下处理:</p>
<pre><code>e.preventDefault()
let target = event.target
let files = target.files
let count = this.state.multiple ? files.length : 1
for (let i = 0; i < count; i++) {
files[i].thumb = URL.createObjectURL(files[i])
}
// 转换为真正的数组
files = Array.prototype.slice.call(files, 0)
// 过滤非图片类型的文件
files = files.filter(function (file) {
return /image/i.test(file.type)
})</code></pre>
<p>这时 <code>files</code> 就是 我们需要的文件对象组成的数组,把它 <code>concat</code> 到原有的 <code>files</code>里。</p>
<pre><code>this.setState({files: this.state.files.concat(files)})</code></pre>
<p>如此,接下来的操作 就可以 通过 <code>this.state.files</code> 取到当前已选中的 图片文件。</p>
<h4>利用<code>Promise</code>处理异步上传</h4>
<p>文件上传对于浏览器来说是异步的,为了处理 接下来的多图上传,这里引入了 <code>Promise</code>来处理异步操作:</p>
<pre><code>upload(file, idx) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()
if (xhr.upload) {
// 上传中
xhr.upload.addEventListener("progress", (e) => {
// 处理上传进度
this.handleProgress(file, e.loaded, e.total, idx);
}, false)
// 文件上传成功或是失败
xhr.onreadystatechange = (e) => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 上传成功操作
this.handleSuccess(file, xhr.responseText)
// 把该文件从上传队列中删除
this.handleDeleteFile(file)
resolve(xhr.responseText);
} else {
// 上传出错处理
this.handleFailure(file, xhr.responseText)
reject(xhr.responseText);
}
}
}
// 开始上传
xhr.open("POST", this.state.uri, true)
let form = new FormData()
form.append("filedata", file)
xhr.send(form)
})
}</code></pre>
<h4>上传进度计算</h4>
<p>利用<code>XMLHttpRequest</code>对象发异步请求的好处是可以 计算请求处理的进度,这是<code>fetch</code>所不具备的。<br>我们可以为 <code>xhr.upload</code> 对象的 <code>progress</code> 事件添加事件监听:</p>
<pre><code>xhr.upload.addEventListener("progress", (e) => {
// 处理上传进度
this.handleProgress(file, e.loaded, e.total, i);
}, false)</code></pre>
<p>说明:<code>idx</code>参数是纪录多图上传队列的索引</p>
<pre><code>handleProgress(file, loaded, total, idx) {
let percent = (loaded / total * 100).toFixed(2) + '%';
let _progress = this.state.progress;
_progress[idx] = percent;
this.setState({ progress: _progress }) // 反馈到DOM里显示
}</code></pre>
<h4>拖拽上传</h4>
<p>拖拽文件对于<code>HTML5</code>来说其实非常简单,因为它自带的几个事件监听机制可以直接做这类处理。主要用到的是下面三个:</p>
<pre><code>onDragOver={(e)=>this.handleDragHover(e)}
onDragLeave={(e)=>this.handleDragHover(e)}
onDrop={(e)=>this.handleDrop(e)}</code></pre>
<p>取消拖拽时的浏览器行为:</p>
<pre><code>handleDragHover(e) {
e.stopPropagation()
e.preventDefault()
}</code></pre>
<p>处理拖拽进来的文件:</p>
<pre><code>handleDrop(e) {
this.setState({progress:[]})
this.handleDragHover(e)
// 获取文件列表对象
let files = e.target.files || e.dataTransfer.files
let count = this.state.multiple ? files.length : 1
for (let i = 0; i < count; i++) {
files[i].thumb = URL.createObjectURL(files[i])
}
// 转换为真正的数组
files = Array.prototype.slice.call(files, 0)
// 过滤非图片类型的文件
files = files.filter(function (file) {
return /image/i.test(file.type)
})
this.setState({files: this.state.files.concat(files)})
}</code></pre>
<h4>多图同时上传</h4>
<p>支持多图上传我们需要在组件调用处设置属性:</p>
<pre><code>multiple = { true } // 开启多图上传
size = { 50 } // 一次最大上传数量(虽没有上限,为保证服务端正常,建议50以下)</code></pre>
<p>然后我们可以使用 <code>Promise.all()</code> 处理异步操作队列:</p>
<pre><code>handleUpload() {
let _promises = this.state.files.map((file, idx) => this.upload(file, idx))
Promise.all(_promises).then( (res) => {
// 全部上传完成
this.handleComplete()
}).catch( (err) => { console.log(err) })
}</code></pre>
<p>好了,前端工作已经完成,接下来就是<code>Node</code>的工作了。</p>
<h2>后端实现</h2>
<p>为了方便,后端采用的是<code>express</code>框架来快速搭建<code>Http</code>服务和路由。具体项目见我的<code>github</code> <a href="https://link.segmentfault.com/?enc=PCJEkhGK9Q894JtKxHcj7Q%3D%3D.0Zduspnt%2BJf4ELcVk%2FAJDkipCrgoH8pBnz%2B1Q%2BIAXXp%2BnyGeSfzn7HkxyJ42Jsaq" rel="nofollow"><code>node-image-upload</code></a>。逻辑虽然简单,但还是有几个可圈可点的地方:</p>
<h3>跨域调用</h3>
<p>本项目后端采用的是<code>express</code>,我们可以通过 <code>res.header()</code> 设置 请求的 <strong>"允许源"</strong> 来允许跨域调用:</p>
<pre><code>res.header('Access-Control-Allow-Origin', '*'); </code></pre>
<p>设置为 <code>*</code> 说明允许任何 访问源,不太安全。建议设置成 你需要的 二级域名,如 <code>jafeney.com</code>。</p>
<p>除了 <strong>"允许源"</strong> ,其他还有 "允许头" 、"允许域"、 "允许方法"、"文本类型" 等。常用的设置如下:</p>
<pre><code>function allowCross(res) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
}</code></pre>
<h3>ES6下的Ajax请求</h3>
<p><code>ES6</code>风格下的<code>Ajax</code>请求和<code>ES5</code>不太一样,在正式的请求发出之前都会先发一个 类型为 <code>OPTIONS</code>的请求 作为试探,只有当该请求通过以后,正式的请求才能发向服务端。</p>
<p>所以服务端路由 我们还需要 处理这样一个 请求:</p>
<pre><code>router.options('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
next();
});</code></pre>
<p>注意:该请求同样需要设置跨域。</p>
<h3>处理上传</h3>
<p>处理上传的图片引人了<code>multiparty</code>模块,用法很简单:</p>
<pre><code>/*使用multiparty处理上传的图片*/
router.post('/upload', function(req, res, next) {
// 生成multiparty对象,并配置上传目标路径
var form = new multiparty.Form({uploadDir: './public/file/'});
// 上传完成后处理
form.parse(req, function(err, fields, files) {
var filesTmp = JSON.stringify(files, null, 2);
var relPath = '';
if (err) {
// 保存失败
console.log('Parse error: ' + err);
} else {
// 图片保存成功!
console.log('Parse Files: ' + filesTmp);
// 图片处理
processImg(files);
}
});
});</code></pre>
<h3>图片处理</h3>
<p><code>Node</code>处理图片需要引入 <code>gm</code> 模块,它需要用 <code>npm</code> 来安装:</p>
<pre><code>npm install gm --save</code></pre>
<h4>BUG说明</h4>
<p>注意:<code>node</code>的图形操作<code>gm</code>模块前使用必须 先安装 <code>imagemagick</code> 和 <code>graphicsmagick</code>,<code>Linux (ubuntu)</code>上使用<code>apt-get</code> 安装:</p>
<pre><code> sudo apt-get install imagemagick
sudo apt-get install graphicsmagick --with-webp // 支持webp格式的图片</code></pre>
<p><code>MacOS</code>上可以用 <code>Homebrew</code> 直接安装:</p>
<pre><code> brew install imagemagick
brew install graphicsmagick --with-webp // 支持webp格式的图片 </code></pre>
<h4>预设尺寸</h4>
<p>有些时候除了原图,我们可能需要把原图等比例缩小作为预览图或者缩略图。这个异步操作还是用<code>Promise</code>来实现:</p>
<pre><code>function reSizeImage(paths, dstPath, size) {
return new Promise(function(resolve, reject) {
gm(dstPath)
.noProfile()
.resizeExact(size)
.write('.' + paths[1] + '@' + size + '00.' + paths[2], function (err) {
if (!err) {
console.log('resize as ' + size + ' ok!')
resolve()
}
else {
reject(err)
};
});
});
}</code></pre>
<h4>重命名图片</h4>
<p>为了方便排序和管理图片,我们按照 "年月日 + 时间戳 + 尺寸" 来命名图片:</p>
<pre><code>var _dateSymbol = new Date().toLocaleDateString().split('-').join('');
var _timeSymbol = new Date().getTime().toString();</code></pre>
<p>至于图片尺寸 使用 <code>gm</code>的 <code>size()</code> 方法来获取,处理方式如下:</p>
<pre><code>gm(uploadedPath).size(function(err, size) {
var dstPath = './public/file/' + _dateSymbol + _timeSymbol
+ '_' + size.width + 'x' + size.height + '.'
+ _img.originalFilename.split('.')[1];
var _port = process.env.PORT || '9999';
relPath = 'http://' + req.hostname + ( _port!==80 ? ':' + _port : '' )
+ '/file/' + _dateSymbol + _timeSymbol + '_' + size.width + 'x'
+ size.height + '.' + _img.originalFilename.split('.')[1];
// 重命名
fs.rename(uploadedPath, dstPath, function(err) {
if (err) {
reject(err)
} else {
console.log('rename ok!');
}
});
});</code></pre>
<h2>总结</h2>
<p>对于大前端的工作,理解图片上传的前后端原理仅仅是浅层的。我们的口号是 "把JavaScript进行到底!",现在无论是 <code>ReactNative</code>的移动端开发,还是<code>NodeJS</code>的后端开发,前端工程师可以做的工作早已不仅仅是局限于web页面,它已经渗透到了互联网应用层面的方方面面,或许,叫 <code>全栈工程师</code> 更为贴切吧。</p>
<p>当然,<code>全栈</code> 两个字的分量很重,<code>不积跬步,无以至千里</code>,功力低下的我还需要不断修炼和实践!</p>
<h2>参考</h2>
<p><a href="https://link.segmentfault.com/?enc=oligMwUGn%2BRxxcegoy7uNA%3D%3D.D7vsynPATuyJ4175jNHnKrv4oSWMde4IyKVSdoFWb55PLwZ7b00wQA5vu%2Fn63oKvLyc35RwtWUC73m0uIzybPBzzOKJg1QzwhZGNDpRUkDHyB6tuqH9%2FImWgGlNyfElQWLOIGd6heGry8lzIvyXNgdmWEZ9pBOHsoh%2BvisTXQ%2FfJ2oKy1VRpKTWpN8ixP2o%2BVr6xEb%2Bbpqhr5ahkI8bQuA%3D%3D" rel="nofollow">张鑫旭 《基于HTML5的可预览多图片Ajax上传》</a></p>
<hr>
<p>@欢迎关注我的 <a href="https://link.segmentfault.com/?enc=AAJ9P7OB6lLo%2BfxVJfZaXg%3D%3D.asqLMzMlouEbHQRn6QqhYtOe0tvXNfpH8cgtd6pw7Hc%3D" rel="nofollow">github</a> 和 <a href="https://link.segmentfault.com/?enc=Czx9QfL%2B2zTfI5WXIuc6NA%3D%3D.ePEVdsPKqSIaLl0lqWVFlDjryTt0e07DpXLIxxyQbrI%3D" rel="nofollow">个人博客 -Jafeney</a></p>
基于Redux架构的单页应用开发总结
https://segmentfault.com/a/1190000006067018
2016-07-25T17:22:35+08:00
2016-07-25T17:22:35+08:00
jafeney
https://segmentfault.com/u/jafeney
6
<h2>系统架构介绍</h2>
<p>本项目开发基于 <code>React</code> + <code>Redux</code> + <code>React-Route</code> 框架,利用 <code>webpack</code> 进行模块化构建,前端编写语言是 JavaScript ES6,利用 <code>babel</code>进行转换。</p>
<pre><code>|--- project
|--- build // 项目打包编译目录
|--- src // 项目开发的源代码
|--- actions // redux的动作
|--- components // redux的组件
|--- containers // redux的容器
|--- images // 静态图片
|--- mixins // 通用的函数库
|--- reducers // redux的store操作
|--- configureStore.js // redux的store映射
|--- index.js // 页面入口
|--- routes.js // 路由配置
|--- index.html // 入口文件
|--- .babelrc // babel配置
|--- main.js // webkit打包的壳子
|--- package.json // 包信息
|--- webpack.config.js // webpack配置文件
|--- readme.md </code></pre>
<pre><code>"dependencies": {
"babel-polyfill": "^6.7.4",
"base-64": "^0.1.0",
"immutable": "^3.7.6",
"isomorphic-fetch": "^2.2.1",
"moment": "^2.13.0",
"normalizr": "^2.0.1",
"react": "^0.14.8",
"react-datetimepicker": "^2.0.0",
"react-dom": "^0.14.8",
"react-redux": "^4.4.1",
"react-redux-spinner": "^0.4.0",
"react-router": "^2.0.1",
"react-router-redux": "^4.0.1",
"redux": "^3.3.1",
"redux-immutablejs": "0.0.8",
"redux-logger": "^2.6.1",
"redux-thunk": "^2.0.1"
},
"devDependencies": {
"babel-core": "^6.7.5",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-1": "^6.5.0",
"css-loader": "^0.23.1",
"file-loader": "^0.8.5",
"img-loader": "^1.2.2",
"less": "^2.6.1",
"less-loader": "^2.2.3",
"mocha": "^2.4.5",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.12.14"
}</code></pre>
<h3>webpack配置</h3>
<p>也算是实际体验了一把webpack,不得不说,论<code>React</code>最佳搭档,非此货莫属!真的很强大,很好用。</p>
<pre><code>var webpack = require('webpack'); // 引入webpack模块
var path = require('path'); // 引入node的path模块
var nodeModulesPath = path.join(__dirname, '/node_modules'); // 设置node_modules目录
module.exports = {
// 配置入口(此处定义了双入口)
entry: {
bundle: './src/index',
vendor: ['react', 'react-dom', 'redux']
},
// 配置输出目录
output: {
path: path.join(__dirname, '/build'),
publicPath: "/assets/",
filename: 'bundle.js'
},
module: {
noParse: [
path.join(nodeModulesPath, '/react/dist/react.min'),
path.join(nodeModulesPath, '/react-dom/dist/react-dom.min'),
path.join(nodeModulesPath, '/redux/dist/redux.min'),
],
// 加载器
loaders: [
// less加载器
{ test: /\.less$/, loader: 'style!css!less' },
// babel加载器
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' },
// 图片加载器(图片超过8k会自动转base64格式)
{ test: /\.(gif|jpg|png)$/, loader: "url?limit=8192&name=images/[name].[hash].[ext]"},
// 加载icon字体文件
{ test: /\.(woff|svg|eot|ttf)$/, loader: 'url?limit=50000&name=fonts/[name].[hash].[ext]'}
]
},
// 外部依赖(不会打包到bundle.js里)
externals: {
'citys': 'Citys'
},
// 插件
plugins: [
//new webpack.HotModuleReplacementPlugin(), // 版本上线时开启
new webpack.DefinePlugin({
// 定义生产环境
"process.env": {
NODE_ENV: JSON.stringify("production")
}
}),
//new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }), // 版本上线时开启
// 公共部分会被抽离到vendor.js里
new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.js'),
// 比对id的使用频率和分布来得出最短的id分配给使用频率高的模块
new webpack.optimize.OccurenceOrderPlugin(),
// 允许错误不打断程序
new webpack.NoErrorsPlugin()
],
};</code></pre>
<h3>
<a href="https://link.segmentfault.com/?enc=8ZLJGVs4yLh2U%2FPhKzodDA%3D%3D.LpObTEoOiBUPc8N0ev1%2BwSXvXTn3SgBW8qjd3mM%2Fv5CpVkA6JrqjhY96tQ29Dgfx" rel="nofollow">延伸</a>-Webpack性能优化</h3>
<h4>最小化</h4>
<p>为了瘦身你的js(还有你的css,如果你用到css-loader的话)webpack支持一个简单的配置项:</p>
<pre><code>new webpack.optimize.UglifyJsPlugin()</code></pre>
<p>这是一种简单而有效的方法来优化你的webapp。而webpack还提供了modules 和 chunks ids 来区分他们俩。利用下面的配置项,webpack就能够比对id的使用频率和分布来得出最短的id分配给使用频率高的模块。</p>
<pre><code>new webpack.optimize.OccurenceOrderPlugin()</code></pre>
<p>入口文件对于文件大小有较高的优先级(入口文件压缩优化率尽量的好)</p>
<h4>去重</h4>
<p>如果你使用了一些有着很酷的依赖树的库,那么它可能存在一些文件是重复的。webpack可以找到这些文件并去重。这保证了重复的代码不被大包到bundle文件里面去,取而代之的是运行时请求一个封装的函数。不会影响语义</p>
<pre><code>new webpack.optimize.DedupePlugin()</code></pre>
<p>这个功能可能会增加入口模块的一些花销</p>
<h4>对于chunks的优化</h4>
<p>当coding的时候,你可能已经添加了许多分割点来按需加载。但编译完了之后你发现有太多细小的模块造成了很大的HTTP损耗。幸运的是Webpack可以处理这个问题,你可以做下面两件事情来合并一些请求:</p>
<ul><li><p>Limit the maximum chunk count with</p></li></ul>
<pre><code>new webpack.optimize.LimitChunkCountPlugin({maxChunks: 15})</code></pre>
<ul><li><p>Limit the minimum chunk size with</p></li></ul>
<pre><code>new webpack.optimize.MinChunkSizePlugin({minChunkSize: 10000})</code></pre>
<p>Webpack通过合并来管理这些异步加载的模块(合并更多的时候发生在当前这个chunk有复用的地方)。文件只要在入口页面加载的时候没有被引入,那么就不会被合并到chunk里面去。</p>
<h4>单页</h4>
<p>Webpack 是为单页应用量身定做的 你可以把app拆成很多chunk,这些chunk由路由来加载。入口模块仅仅包含路由和一些库,没有别的内容。这么做在用户通过导航浏览表现很好,但是初始化页面加载的时候你需要2个网络请求:一个是请求路由,一个是加载当前内容。</p>
<p>如果你利用HTML5的HistoryAPI 来让URL影响当前内容页的话。你的服务器可以知道那个内容页面将被客户端请求。为了节约请求数,服务端可以把要请求的内容模块放到响应头里面:以script标签的形式来添加,浏览器将并行的加载这俩请求。</p>
<pre><code><script src="entry-chunk.js" type="text/javascript" charset="utf-8"></script>
<script src="3.chunk.js" type="text/javascript" charset="utf-8"></script></code></pre>
<p>你可以从build stas里面提取出chunk的filename (stats-webpack-plugin )</p>
<h4>多页</h4>
<p>当编译一个多页面的app时,你想要在页面之间共享一些代码。这在webpack看来很简单的:只需要和多个入口文件一起编译就好</p>
<pre><code>webpack p1=./page1 p2=./page2 p3=./page3 [name].entry-chunk.js</code></pre>
<pre><code>module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
p3: "./page3"
},
output: {
filename: "[name].entry.chunk.js"
}
}</code></pre>
<p>由上面可以产出多个入口文件</p>
<pre><code>p1.entry.chunk.js, p2.entry.chunk.js and p3.entry.chunk.js</code></pre>
<p>但是可以增加一个chunk来共享她们中的一些代码。 如果你的chunks有一些公用的modules,那我推荐一个很酷的插件CommonsChunkPlugin,它能辨别共用模块并把他们放倒一个文件里面去。你需要在你的页面里添加两个script标签来分别引入入口文件和共用模块文件。</p>
<pre><code>var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
p3: "./page3"
},
output: {
filename: "[name].entry.chunk.js"
},
plugins: [
new CommonsChunkPlugin("commons.chunk.js")
]
}</code></pre>
<p>由上面可以产出入口文件</p>
<pre><code>p1.entry.chunk.js, p2.entry.chunk.js and p3.entry.chunk.js</code></pre>
<p>和共用文件</p>
<pre><code>commons.chunk.js</code></pre>
<p>在页面中要首先加载 commons.chunk.js 在加载xx.entry.chunk.js 你可以出实话很多个commons chunks ,通过选择不同的入口文件。并且你可以堆叠使用这些commons chunks。</p>
<pre><code>var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
p3: "./page3",
ap1: "./admin/page1",
ap2: "./admin/page2"
},
output: {
filename: "[name].js"
},
plugins: [
new CommonsChunkPlugin("admin-commons.js", ["ap1", "ap2"]),
new CommonsChunkPlugin("commons.js", ["p1", "p2", "admin-commons.js"])
]
};</code></pre>
<p>输出结果:</p>
<pre><code>page1.html: commons.js, p1.js
page2.html: commons.js, p2.js
page3.html: p3.js
admin-page1.html: commons.js, admin-commons.js, ap1.js
admin-page2.html: commons.js, admin-commons.js, ap2.js</code></pre>
<p>另外你可以将多个共用文件打包到一个共用文件中。</p>
<pre><code>var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
commons: "./entry-for-the-commons-chunk"
},
plugins: [
new CommonsChunkPlugin("commons", "commons.js")
]
};</code></pre>
<h2>关于less的组织</h2>
<p>作为一个后端出身的前端工程师,写简单的css实在没有那种代码可配置和结构化的快感。所以引入less是个不错的选择,无论是针对代码后期的管理,还是提高代码的复用能力。</p>
<h3><code>global.less</code></h3>
<p>这个是全局都可以调用的方法库,我习惯把 项目的配色、各种字号、用于引入混出的方法等写在这里,其他<code>container</code>页面通过<code>@import</code>方式引入它,就可以使用里面的东西。不过定义它时要注意以下两点:</p>
<ul>
<li><p>第一,这个less里只能存放变量和方法,less编译时会忽略它们,只在调用它们的地方才编译成css。所以为了防止代码重复,请不要在这里直接定义样式,而是用一个方法把它们包起来,表示一个用途。</p></li>
<li><p>第二,这个less里的方法如果是针对某些具体标签定义样式的,只能初始化一次,建议在单页的入口<code>container</code>里做,这样好维护。比如<code>reset()</code>(页面标签样式初始化),这个方法放在入口<code>container</code>的 <code>login.less</code>里调用且全局只调用一次。</p></li>
</ul>
<p>下面是我的<code>global.less</code> 常用的一些模块</p>
<pre><code>/**
* @desc 一些全局的less
* @createDate 2016-05-16
* @author Jafeney <692270687@qq.com>
**/
// 全局配色
@g-color-active: #ff634d; //活跃状态的背景色(橘红色)
@g-color-info: #53b2ea; //一般用途的背景色(浅蓝色)
@g-color-primary: #459df5; //主要用途的背景色 (深蓝色)
@g-color-warning: #f7cec8; //用于提示的背景色 (橘红色较浅)
@g-color-success: #98cf07; //成功状态的背景色 (绿色)
@g-color-fail: #c21f16; //失败状态的背景色 (红色)
@g-color-danger: #ff634d; //用于警示的背景色 (橘红色)
@g-color-light: #fde2e1; //高饱合度淡色的背景色(橘红)
// 全局尺寸
@g-text-default: 14px;
@g-text-sm: 12px;
@g-text-lg: 18px;
// 全局使用的自定义icon(这样写的好处是webpack打包时自动转base64)
@g-icon-logo: url("../images/logo.png");
@g-icon-logoBlack: url("../images/logoBlack.png");
@g-icon-phone: url("../images/phone.png");
@g-icon-message: url("../images/message.png");
@g-icon-help: url("../images/help.png");
@g-icon-down: url("../images/down.png");
@g-icon-top: url("../images/top.png");
@g-icon-home: url("../images/home.png");
@g-icon-order: url("../images/order.png");
@g-icon-cart: url("../images/cart.png");
@g-icon-source: url("../images/source.png");
@g-icon-business: url("../images/business.png");
@g-icon-finance: url("../images/finance.png");
@g-icon-account: url("../images/account.png");
// ....
// 背景色
@g-color-grey1: #2a2f33; //黑色
@g-color-grey2: #363b3f; //深灰色
@g-color-grey3: #e5e5e5; //灰色
@g-color-grey4: #efefef; //浅灰色
@g-color-grey5: #f9f9f9; //很浅
@g-color-grey6: #ffffff; //白色
// 全局边框
@g-border-default: #e6eaed;
@g-border-active: #53b2ea;
@g-border-light: #f7dfde;
// 常用的border-box盒子模型
.border-box() {
box-sizing: border-box;
-ms-box-sizing: border-box;
-moz-box-sizing: border-box;
-o-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
// 模拟按钮效果
.btn() {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
&:hover {
opacity: .8;
}
&.disabled {
&:hover {
opacity: 1;
cursor: not-allowed;
}
}
}
// 超出部分处理
.text-overflow() {
overflow: hidden;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
-webkit-text-overflow: ellipsis;
-moz-text-overflow: ellipsis;
white-space: nowrap;
}
// reset styles
.reset() {
// ....
}
// 一些原子class
.atom() {
.cp {
cursor: pointer;
}
.ml-5 {
margin-left: 5px;
}
.mr-5 {
margin-right: 5px;
}
.ml-5p {
margin-left: 5%;
}
.mr-5p {
margin-right: 5%;
}
.mt-5 {
margin-top: 5px;
}
.txt-center {
text-align: center;
}
.txt-left {
text-align: left;
}
.txt-right {
text-align: right;
}
.fr {
float: right;
}
.fl {
float: left;
}
}</code></pre>
<h3>
<code>component</code>的less</h3>
<p>为了降低组件的耦合性,每个组件的less必须单独写,样式跟着组件走,一个组件一个less,不要有其他依赖,保证组件的高移植能力。<br>而且组件应该针对用途提供几套样式方案,比如<code>button</code>组件,我们可以针对颜色提供不同的样式,以样式组合的方式提供给外部使用。</p>
<pre><code>// 下面的变量可以针对不同的需求进行配置
@color-primary: #459df5;
@color-warning: #f7cec8;
@color-success: #98cf07;
@color-fail: #c21f16;
.btn {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
display: inline-block;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-ms-box-sizing: border-box;
-moz-box-sizing: border-box;
-o-box-sizing: border-box;
text-align: center;
// 鼠标放上时
&:hover {
opacity: .8;
}
// 按钮不可用时
&.disabled {
&:hover {
opacity: 1;
cursor: not-allowed;
}
}
// 填充式按钮
&.full {
color: #fff;
&.primary {
background-color: @color-primary;
border: 1px solid @color-primary;
}
// ....
}
// 边框式按钮
&.border {
background-color: #fff;
&.primary {
color: @color-primary;
border: 1px solid @color-primary;
}
// ...
}
}</code></pre>
<h3>
<code>container</code>的less</h3>
<p>同上,每个<code>container</code>一个less文件,可以复用的模块尽量封装成<code>component</code>,而不是偷懒复制几行样式过来,这样虽然方便一时,但随着项目的迭代,后期的冗余代码会多得超出你的想象。<br>如果遵循组件化的设计思想,你会发现<code>container</code>里其实只有一些布局和尺寸定义相关的代码,非常容易维护。</p>
<blockquote><p>这是大型项目的设计要领,除此之外就是大局观的培养,这点尤为重要,项目一拿来不要马上就动手写页面,而是应该多花些时间在代码的设计上,把全局的东西剥离出来,越细越好;把可复用的模块设计成组件,思考组件的拓展性和不同的用途,记住—— 结构上尽量减少依赖关系,保持组件的独立性,而用途上多考虑功能的聚合,即所谓的低耦合高聚合。</p></blockquote>
<p>不过实际项目不可能每个组件都是独立存在的,有时我们为了进一步减少代码量,会把一些常用的组件整合成一个大组件来使用,即复合组件。所以每个项目实际上存在一级组件(独立)和二级组件(复合)。一级组件可以随意迁移,而二级组件是针对实际场景而生的,两者并没有好坏之分,一切都为了高效地生产代码,存在即合理。</p>
<h2>关于React的组织</h2>
<p>本项目的React代码都用JavaScript的ES6风格编写,代码非常地优雅,而且语言自身支持模块化,再也不用依赖<code>Browserify</code>、<code>RequireJS</code>等工具了,非常爽。如果你不会ES6,建议去翻一翻阮一峰老师的<a href="https://link.segmentfault.com/?enc=tQnq2B7BzTjAxX0yAgE7MA%3D%3D.QXKSEKXs5zxm6j7mjJ6wZy7LA3gxbK%2B0S08kIaHlzTY%3D" rel="nofollow">《ES6标准入门》</a></p>
<h3>入口</h3>
<p>入口模块<code>index.js</code>放在<code>src</code>的根目录,是外部调用的入口。</p>
<pre><code>import React from 'react'
import { render } from 'react-dom'
// 引入redux
import { Provider } from 'react-redux'
// 引入router
import { Router, hashHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import routes from './routes'
import configureStore from './configureStore'
const store = configureStore(hashHistory) // 路由的store
const history = syncHistoryWithStore(hashHistory, store) // 路由的历史纪录(会写入到浏览器的历史纪录)
render(
(
<Provider store={store}>
<Router history={history} routes={routes} />
</Provider>
), document.getElementById('root')
)</code></pre>
<h3>路由</h3>
<p>这里主要应用了<code>react-route</code>组件来制作哈希路由,使用方式很简单,和ReactNative里的Navigator组件类似。</p>
<pre><code>import React from 'react'
import { Route } from 'react-router'
import Manager from './containers/manager'
import Login from './containers/Login/'
import Register from './containers/Register/'
import Password from './containers/Password/'
import Dashboard from './containers/Dashboard/'
const routes = (
<Route>
<Route path="" component={Manager}> // 主容器
<Route path="/" component={Dashboard} /> // 仪表盘
// .... 各模块的container
</Route>
<Route path="login" component={Login} /> // 登录
<Route path="register" component={Register} /> // 注册
<Route path="password" component={Password} /> // 找回密码
</Route>
)
export default routes
</code></pre>
<h3>了解action、store、reducer</h3>
<p>从调用关系来看如下所示:</p>
<pre><code>store.dispatch(action) --> reducer(state, action) --> final state</code></pre>
<p>来个实际的例子:</p>
<pre><code>// reducer方法, 传入的参数有两个
// state: 当前的state
// action: 当前触发的行为, {type: 'xx'}
// 返回值: 新的state
var reducer = function(state, action){
switch (action.type) {
case 'add_todo':
return state.concat(action.text);
default:
return state;
}
};
// 创建store, 传入两个参数
// 参数1: reducer 用来修改state
// 参数2(可选): [], 默认的state值,如果不传, 则为undefined
var store = redux.createStore(reducer, []);
// 通过 store.getState() 可以获取当前store的状态(state)
// 默认的值是 createStore 传入的第二个参数
console.log('state is: ' + store.getState()); // state is:
// 通过 store.dispatch(action) 来达到修改 state 的目的
// 注意: 在redux里,唯一能够修改state的方法,就是通过 store.dispatch(action)
store.dispatch({type: 'add_todo', text: '读书'});
// 打印出修改后的state
console.log('state is: ' + store.getState()); // state is: 读书
store.dispatch({type: 'add_todo', text: '写作'});
console.log('state is: ' + store.getState()); // state is: 读书,写作</code></pre>
<h4>store、reducer、action关联</h4>
<p>store:对flux有了解的同学应该有所了解,store在这里代表的是数据模型,内部维护了一个state变量,用例描述应用的状态。store有两个核心方法,分别是getState、dispatch。前者用来获取store的状态(state),后者用来修改store的状态。</p>
<pre><code>// 创建store, 传入两个参数
// 参数1: reducer 用来修改state
// 参数2(可选): [], 默认的state值,如果不传, 则为undefined
var store = redux.createStore(reducer, []);
// 通过 store.getState() 可以获取当前store的状态(state)
// 默认的值是 createStore 传入的第二个参数
console.log('state is: ' + store.getState()); // state is:
// 通过 store.dispatch(action) 来达到修改 state 的目的
// 注意: 在redux里,唯一能够修改state的方法,就是通过 store.dispatch(action)
store.dispatch({type: 'add_todo', text: '读书'});</code></pre>
<p>action:对行为(如用户行为)的抽象,在redux里是一个普通的js对象。redux对action的约定比较弱,除了一点,action必须有一个type字段来标识这个行为的类型。所以,下面的都是合法的action</p>
<pre><code>{type:'add_todo', text:'读书'}
{type:'add_todo', text:'写作'}
{type:'add_todo', text:'睡觉', time:'晚上'}</code></pre>
<p>reducer:一个普通的函数,用来修改store的状态。传入两个参数 state、action。其中,state为当前的状态(可通过store.getState()获得),而action为当前触发的行为(通过store.dispatch(action)调用触发)。reducer(state, action) 返回的值,就是store最新的state值。</p>
<pre><code>// reducer方法, 传入的参数有两个
// state: 当前的state
// action: 当前触发的行为, {type: 'xx'}
// 返回值: 新的state
var reducer = function(state, action){
switch (action.type) {
case 'add_todo':
return state.concat(action.text);
default:
return state;
}
}</code></pre>
<h2>React式编程思维</h2>
<p>在没有遁入React之前,我是一个DOM操作控,不论是<code>jQuery</code>还是<code>zepto</code>,我在页面交互的实现上用的最多的就是DOM操作,把复杂的交互一步一步通过选择器和事件委托绑定到document上,然后逐个连贯起来。</p>
<pre><code>$(document).on('event', 'element', function(e){
e.preventDefault();
var that = this;
var parent = $(this).parent();
var siblings = $(this).siblings();
var children = $(this).children();
// .....
});</code></pre>
<p>这是<code>jQuery</code>式的编程思维,<code>React</code>和它截然不同。<code>React</code>的设计是基于组件化的,每个组件通过生命周期维护统一的<code>state</code>,<code>state</code>改变,组件便<code>update</code>,重新触发<code>render</code>,即重新渲染页面。而这个过程操作的其实是内存里的<code>虚拟DOM</code>,而不是真正的DOM节点,加上其内部的差异更新算法,所以性能上比传统的DOM操作要好。</p>
<p><strong>举个简单的例子:</strong></p>
<p>现在要实现一个模态组件,如果用jQuery式的编程思维,很习惯这么写:</p>
<pre><code>/**
* @desc 全局模态窗口
**/
var $ = window.$;
var modal = {
confirm: function(opts) {
var title = opts.title || '提示',
content = opts.content || '提示内容',
callback = opts.callback;
var newNode = [
'<div class="mask" id="J_mask">',
'<div class="modal-box">',
'<h2>',
title,
'</h2>',
'<p>',
content,
'</p>',
'<div class="mask-btns">',
'<span id="J_cancel">取消</span>',
'<span id="J_confirm">确定</span>',
'</div>',
'</div>',
'</div>',
].join('');
$('#J_mask').remove();
$('body').append(newNode);
$('#J_cancel').on('click', function() {
$('#J_mask').remove();
});
$('#J_confirm').on('click', function() {
if (typeof callback === 'function') {
callback();
}
$('#J_mask').remove();
});
}
};
module.exports = modal;</code></pre>
<p>然后在页面的JavaScript里通过选择器触发模态和传递参数。</p>
<pre><code>var Modal = require('modal');
var $ = window.$;
var app = (function() {
var init = function() {
eventBind();
};
var eventBind = function() {
$(document).on('click', '#btnShowModal', function() {
Modal.confirm({
title: '提示',
content: '你好!世界',
callback: function() {
console.log('Hello World');
}
});
});
};
init();
})(); </code></pre>
<p>如果采用<code>React</code>式的编程思维,它应该是这样的:</p>
<pre><code>/**
* @desc 全局模态组件 Component
* @author Jafeney
* @createDate 2016-05-17
* */
import React, { Component } from 'react'
import './index.less'
class Modal extends Component {
constructor() {
super()
this.state = {
jsMask: 'mask hidden'
}
}
show() {
this.setState({
jsMask: 'mask'
})
}
close() {
this.setState({
jsMask: 'mask hidden'
})
}
confirm() {
this.props.onConfirm && this.props.onConfirm()
}
render() {
return (
<div className={this.state.jsMask}>
<div className="modal-box" style={this.props.style}>
<div className="header">
<h3>{ this.props.title }</h3>
<span className="icon-remove closed-mask" onClick={()=>this.close()}></span>
</div>
<div className="content">
{ this.props.children }
</div>
<div className="mask-btns">
<span className="btn-full-danger" onClick={()=>this.confirm()}>{ this.props.confirmText || '确定' }</span>
{ this.props.showCancel && (<span className="btn-border-danger" onClick={()=>this.close()}>取消</span>) }
</div>
</div>
</div>
);
}
}
export default Modal</code></pre>
<p>然后在<code>container</code>的<code>render()</code>函数里通过标签的方式引入,并通过点击触发。</p>
<pre><code>import {React, component} from 'react';
import Modal from 'Modal';
class App extends Component {
render() {
<div>
<button onClick = {()=> {this.refs.modal.show()}}
<Modal title={"提示"}
style={{width: 420, height: 200}}
ref={(ref)=> this.modal = ref}
onConfirm={()=>this.onModalConfirm()}>
<p className="tips">Hello world!</p>
</Modal>
</div>
}
}
export default App</code></pre>
<p>你会发现,上面的代码并没有刻意地操作某个DOM元素的样式,而是通过改变组件的<code>state</code>去触发自身的渲染函数。换句话说,我们不需要写繁琐的DOM操作,而是靠改变组件的<code>state</code>控制组件的交互和各种变化。这种思维方式的好处等你熟悉<code>React</code>之后自然会明白,可以大大地减少后期的代码量。</p>
<h2>优化渲染</h2>
<p>前面提到组件的<code>state</code>改变即触发<code>render()</code>,<code>React</code>内部虽然做了一些算法上的优化,但是我们可以结合<code>Immutable</code>做进一步的渲染优化,让页面更新渲染速度变得更快。</p>
<pre><code>/**
* @desc PureRender 优化渲染
**/
import React, { Component } from 'react'
import Immutable from 'immutable';
export default {
// 深度比较
deepCompare: (self, nextProps, nextState) => {
return !Immutable.is(self.props, nextProps) || !Immutable.is(self.state, nextState)
},
// 阻止没必要的渲染
loadDetection: (reducers=[])=> {
for (let r of reducers) {
if (!r.get('preload')) return (<div />)
}
}
}
</code></pre>
<p>这样我们在<code>container</code>的<code>render()</code>函数里就可以调用它进行渲染优化</p>
<pre><code>import React, { Component } from 'react'
import PureRenderMixin from '../../mixins/PureRender';
class App extends Component {
render() {
let { actions, account, accountLogs, bankBind } = this.props;
// 数据导入检测
let error = PureRenderMixin.loadDetection([account, accountLogs, bankBind])
// 如果和上次没有差异就阻止组件重新渲染
if (error) return error
return (
<div>
// something ...
</div>
);
}
}</code></pre>
<h2>全局模块的处理</h2>
<p>其实<code>Redux</code>最大的作用就是有效减少代码量,把繁琐的操作通过 <code>action ----> reducer ----> store </code> 进行抽象,最后维护统一的<code>state</code>。对于页面的全局模块,简单地封装成<code>mixin</code>来调用还是不够的,比如全局的<code>request</code>模块,下面介绍如何用<code>Redux</code>进行改造。</p>
<p>首先在<code>types.js</code>里进行声明:</p>
<pre><code>// request
export const REQUEST_PEDDING = 'REQUEST_PEDDING';
export const REQUEST_DONE = 'REQUEST_DONE';
export const REQUEST_ERROR = 'REQUEST_ERROR';
export const REQUEST_CLEAN = 'REQUEST_CLEAN';
export const REQUEST_SUCCESS = 'REQUEST_SUCCESS';</code></pre>
<p>然后编写<code>action</code>:</p>
<pre><code>/**
* @desc 网络请求模块的actions
**/
// fetch 需要使用 Promise 的 polyfill
import {
pendingTask, // The action key for modifying loading state
begin, // The action value if a "long" running task begun
end // The action value if a "long" running task ended
} from 'react-redux-spinner';
import 'babel-polyfill'
import fetch from 'isomorphic-fetch'
import Immutable from 'immutable'
import * as CONFIG from './config'; //请求的配置文件
import * as TYPES from './types';
export function request(route, params, dispatch, success=null, error=null, { method='GET', headers={}, body=null } = {}) {
dispatch({type: TYPES.REQUEST_PEDDING, [ pendingTask ]: begin})
// 处理query
const p = params ? '?' + Object.entries(params).map( (i)=> `${i[0]}=${encodeURI(i[1])}` ).join('&') : ''
const uri = `${ CONFIG.API_URI }${ route }${ p }`
let data = {method: method, headers: headers}
if (method!='GET') data.body = body
fetch(uri, data)
.then((response) => {
dispatch({type: TYPES.REQUEST_DONE, [ pendingTask ]: end})
return response.json()
})
.then((data) => {
if (String(data.code) == '0') {
if (method !== 'GET' ) dispatch({type: TYPES.REQUEST_SUCCESS});
success && success(data);
} else {
console.log(data.error)
dispatch({type: TYPES.REQUEST_ERROR, ...data})
error && error(data)
}
})
.catch((error) => {
console.warn(error)
})
}
export function requestClean() {
return { type: TYPES.REQUEST_CLEAN }
}
</code></pre>
<p>然后编写对应的<code>reducer</code>操作<code>state</code>:</p>
<pre><code>import Immutable from 'immutable';
import * as TYPES from '../actions/types';
import { createReducer } from 'redux-immutablejs'
export default createReducer(Immutable.fromJS({status: null, error: null}), {
[TYPES.REQUEST_ERROR]: (state, action) => {
return state.merge({
status: 'error',
code: action.code,
error: Immutable.fromJS(action.error),
})
},
[TYPES.REQUEST_CLEAN]: (state, action) => {
return state.merge({
status: null,
error: null,
})
},
[TYPES.REQUEST_SUCCESS]: (state, action) => {
return state.merge({
status: 'success',
error: null,
})
}
})</code></pre>
<p>然后在<code>reducers</code>的<code>index.js</code>里对外暴露接口</p>
<pre><code>export request from './request'</code></pre>
<p>为什么要做这一步呢?因为我们需要在<code>configureStore.js</code>里利用<code>combineReducers</code>对所有的<code>reducer</code>进行进一步的结合处理:</p>
<pre><code>import { createStore, combineReducers, compose, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import * as reducers from './reducers'
import { routerReducer, routerMiddleware } from 'react-router-redux'
import { pendingTasksReducer } from 'react-redux-spinner'
export default function configureStore(history, initialState) {
const reducer = combineReducers({
...reducers,
routing: routerReducer,
pendingTasks: pendingTasksReducer,
})
const store = createStore(
reducer,
initialState,
compose(
applyMiddleware(
thunkMiddleware,
routerMiddleware(history)
)
)
)
return store
}
</code></pre>
<p>接下来就可以在<code>container</code>里使用了,比如登录模块:</p>
<pre><code>/**
* @desc 登录模块 container
* @createDate 2016-05-16
* @author Jafeney<692270687@qq.com>
**/
import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { replace } from 'react-router-redux'
import { login } from '../../actions/user'
import { requestClean } from '../../actions/request'
import CheckUserMixin from '../../mixins/CheckUser'
import PureRenderMixin from '../../mixins/PureRender'
import '../style.less';
class Login extends Component {
constructor() {
super()
}
shouldComponentUpdate(nextProps, nextState) {
// 如果已经登录不触发深度比较
if (nextProps.user.getIn(['login', 'status'])=='logged') {
this.toMain()
return true
}
return PureRenderMixin.deepCompare(this, nextProps, nextState)
}
// 检查登录态
componentDidMount() {
let { user } = this.props;
if (CheckUserMixin.isLogged(user)) this.toMain()
}
// 初始化页面
toMain() {
this.props.actions.replace('/')
this.props.actions.requestClean()
}
// 执行登录
login() {
const userName = this.refs['J_username'].value, password = this.refs['J_password'].value
if (userName && password) {
this.props.actions.login({username: userName, password: password})
}
}
// 绑定回车事件
onEnter(event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
if(e && e.keyCode==13) { // enter 键
this.login()
}
}
render() {
let { user } = this.props
return (
<div className="wrapper" onKeyPress={()=>this.onEnter()}>
<div className="containers">
<div className="logo"></div>
<div className="content">
<div className="header">会员登录</div>
<div className="mainer">
<div className="input-group">
<input ref="J_username" type="text" placeholder="手机号码" className="input" />
<label className="check-info" ref="J_username-check"></label>
</div>
<div className="input-group">
<input ref="J_password" type="password" placeholder="登录密码" className="input" />
<label className="check-info" ref="J_password-check"></label>
</div>
<div className="input-group">
<span ref="J_login" onClick={()=>this.login()} className="login-btn">登录</span>
<span className="login-info">
<a ref="J_register" href="#/register" className="register">免费注册</a> |
<a ref="J_forget" href="#/password" className="forget">忘记密码 ?</a>
</span>
</div>
<div className="form-error">
{ user.getIn(['login', 'error', 'message']) }
</div>
</div>
</div>
</div>
</div>
)
}
}
// 下面是redux的核心方法
function mapStateToProps(state) {
return {
user: state.user
}
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators({ login, requestClean, replace }, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Login)</code></pre>
<blockquote><p>注意:通过以上方式,在组件内部<code>actions</code>里挂载的方法就可以通过<code>this.props</code>取得了。</p></blockquote>
<h2>参考</h2>
<ul>
<li><p><a href="https://link.segmentfault.com/?enc=IoCwMNJvTokGp7rpmg2DYg%3D%3D.nUXobf6p%2FJiSUyUYd3s9VjbtAsUvlppLyK%2Bc1sYwz7nkzOdWSgBYdAYrzqVvtN3t" rel="nofollow">《webpack 性能优化》</a></p></li>
<li><p><a href="https://segmentfault.com/a/1190000004208610">《Redux系列01:从一个简单例子了解action、store、reducer》</a></p></li>
</ul>
<hr>
<p>@欢迎关注我的 <a href="https://link.segmentfault.com/?enc=pyMQIGgtK2kwjOm3Fmd5lw%3D%3D.rKolTBXrUqUgr3U6Du4WVyX%2FqgO4ZozT3yeConYlMUo%3D" rel="nofollow"><code>github</code></a> 和 <a href="https://link.segmentfault.com/?enc=I6W%2F%2BOKi%2BiOBK6eqZh4TeQ%3D%3D.Qg%2BClv0PM8i530%2FI36%2F4kWF2bJHBsnQfdMBXmxZxzBs%3D" rel="nofollow">个人博客 -Jafeney</a></p>
深入理解 React 中的上下文 this
https://segmentfault.com/a/1190000006032223
2016-07-21T15:52:16+08:00
2016-07-21T15:52:16+08:00
jafeney
https://segmentfault.com/u/jafeney
12
<h2>写在前面</h2>
<p><code>JavaScript</code>中的作用域<code>scope</code> 和上下文 <code>context</code> 是这门语言的独到之处,每个函数有不同的变量上下文和作用域。这些概念是<code>JavaScript</code>中一些强大的设计模式的后盾。在ES5规范里,我们可以遵循一个原则——每个<code>function</code>内的上下文<code>this</code>指向该<code>function</code>的调用方。比如:</p>
<pre><code>var Module = {
name: 'Jafeney',
first: function() {
console.log(this); // this对象指向调用该方法的Module对象
var second = (function() {
console.log(this) // 由于变量提升,this对象指向Window对象
})()
},
init: function() {
this.first()
}
}
Module.init()</code></pre>
<p><img src="/img/remote/1460000006769772" alt="这里写图片描述" title="这里写图片描述"></p>
<p>但是,在ES6规范中,出现了一个逆天的箭头操作符 <code>=></code> ,它可以替代原先ES5里<code>function</code>的作用,快速声明函数。那么,在没有了<code>function</code>关键字,箭头函数内部的上下文<code>this</code>是怎样一种情况呢?</p>
<h2>ES6 中的箭头函数</h2>
<p>在阮一峰老师的<a href="https://link.segmentfault.com/?enc=vbqjBLfAJI6fKTHep%2BMQJA%3D%3D.4w3KhSgLbIX5DhIFunAkhQFhJmilxKvmaItVtHWHTkRRecxUIi7mXMycKWHSzM7qzxIGH1yHd1hkfyrpJ6y1xNh6idbLkfmaZHyRZhKuhxFYCirgltw3npmMkRHk1uL4YqfrYb%2Bi%2B7AAM%2Bw7dX%2FnAQ%3D%3D" rel="nofollow">《ECMAScript 6 入门》</a> 中,对箭头函数的做了如下介绍:</p>
<h3>箭头函数的基本介绍</h3>
<p>ES6 允许使用“箭头”<code>=></code> 定义函数。</p>
<pre><code>var f = v => v;
//上面的箭头函数等同于:
var f = function(v) {
return v;
};</code></pre>
<ul>
<li>
<p>如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分</p>
<pre><code>var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};</code></pre>
</li>
<li>
<p>如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用<code>return</code>语句返回(重要)</p>
<pre><code>var sum = (num1, num2) => { return num1 + num2; }</code></pre>
</li>
<li>
<p>由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号(重要)</p>
<pre><code>var getTempItem = id => ({ id: id, name: "Temp" });</code></pre>
</li>
<li>
<p>箭头函数可以与变量解构结合使用</p>
<pre><code>const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}</code></pre>
</li>
<li>
<p>箭头函数使得表达更加简洁</p>
<pre><code>const isEven = n => n % 2 == 0;
const square = n => n * n;</code></pre>
<blockquote><p>上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。</p></blockquote>
</li>
<li>
<p>箭头函数的一个用处是简化回调函数</p>
<pre><code>// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);</code></pre>
</li>
</ul>
<h3>箭头函数使用注意点</h3>
<ol>
<li><p>函数体内的<code>this</code>对象,就是定义时所在的对象,而不是使用时所在的对象。</p></li>
<li><p>不可以当作构造函数,也就是说,不可以使用<code>new</code>命令,否则会抛出一个错误。</p></li>
<li><p>不可以使用<code>arguments</code>对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。</p></li>
<li><p>不可以使用<code>yield</code>命令,因此箭头函数不能用作<code>Generator</code>函数。</p></li>
</ol>
<h3>
<code>this</code> 指向固定化</h3>
<p>ES5规范中,<code>this</code>对象的指向是可变的,但是在ES6的箭头函数中,它却是固定的。</p>
<pre><code>function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 }); // 输出 id: 42</code></pre>
<blockquote><p>注意:上面代码中,<code>setTimeout</code>的参数是一个箭头函数,这个箭头函数的定义生效是在<code>foo</code>函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时<code>this</code>应该指向全局对象<code>window</code>,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是<code>{id: 42}</code>),所以输出的是42。</p></blockquote>
<h3>箭头函数的原理</h3>
<p><code>this</code>指向的固定化,并不是因为箭头函数内部有绑定<code>this</code>的机制,实际原因是箭头函数根本没有自己的<code>this</code>,导致内部的<code>this</code>就是外层代码块的<code>this</code>。正是因为它没有<code>this</code>,所以也就不能用作构造函数。所以,箭头函数转成ES5的代码如下:</p>
<pre><code>// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}</code></pre>
<blockquote><p>上面代码中,转换后的ES5版本清楚地说明了,箭头函数里面根本没有自己的<code>this</code>,而是引用外层的<code>this</code>。</p></blockquote>
<h3>两道经典的面试题</h3>
<pre><code>// 请问下面有几个this
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // 输出 id: 1
var t2 = f().call({id: 3})(); // 输出 id: 1
var t3 = f()().call({id: 4}); // 输出 id: 1</code></pre>
<blockquote><p>上面代码之中,其实只有一个<code>this</code>,就是函数foo的<code>this</code>,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的<code>this</code>,它们的this其实都是最外层foo函数的this。另外,<strong>由于箭头函数没有自己的this,所以也不能用<code>call()</code>、<code>apply()</code>、<code>bind()</code>这些方法去改变this的指向</strong>。</p></blockquote>
<pre><code>// 请问下面代码执行输出什么
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });</code></pre>
<blockquote><p>上面代码中,箭头函数没有自己的<code>this</code>,所以<code>bind</code>方法无效,内部的<code>this</code>指向外部的<code>this</code>。所以上面的代码最终输出 <code>['outer']</code>。</p></blockquote>
<h3>函数绑定 <code>::</code>
</h3>
<p>箭头函数可以绑定<code>this</code>对象,大大减少了显式绑定this对象的写法(<code>call</code>、<code>apply</code>、<code>bind</code>)。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”(<code>function</code> <code>bind</code>)运算符,用来取代<code>call</code>、<code>apply</code>、<code>bind</code>调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。</p>
<p>函数绑定运算符是并排的两个双冒号(<code>::</code>),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即<code>this</code>对象),绑定到右边的函数上面。</p>
<pre><code>foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
}</code></pre>
<p>如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。</p>
<pre><code>var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);</code></pre>
<p>由于双冒号运算符返回的还是原对象,因此可以采用链式写法。</p>
<pre><code>// 例一
import { map, takeWhile, forEach } from "iterlib";
getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));
// 例二
let { find, html } = jake;
document.querySelectorAll("div.myClass")
::find("p")
::html("hahaha");</code></pre>
<h2>React 中的各种 <code>this</code>
</h2>
<p>目前<code>React</code>的编写风格已经全面地启用了ES6和部分ES7规范,所以很多ES6的坑在<code>React</code>里一个个浮现了。本篇重点介绍 <code>this</code>,也是近期跌得最疼的一个。</p>
<h3>Component 方法内部的 <code>this</code>
</h3>
<p>还是用具体的例子来解释吧,下面是我 <a href="https://link.segmentfault.com/?enc=%2BvkI0ywFsfmyaPmTyxLW4A%3D%3D.fT6eYrXNkkeF3KkpJKWFHebDVX%2BRmxDm2zpdgi50Ut%2BGyoVXj3BuYmqLp3F5wp9S" rel="nofollow"><code>Royal</code></a> 项目里一个<code>Table</code>组件(<code>Royal</code>正在开发中,欢迎<a href="https://link.segmentfault.com/?enc=SEhaIOnSsnsOLFrwSiIabw%3D%3D.5nrSgXU87IN0VA9luUmUJXyEFwUsqbVcshu9MF6nAbrjAbBvii%2FJQUvbbmNjKNZP" rel="nofollow"><code>fork</code></a>贡献代码 ^_^)</p>
<pre><code>import React, { Component } from 'react'
import Checkbox from '../../FormControls/Checkbox/'
import './style.less'
class Table extends Component {
constructor(props) {
super(props)
this.state = {
dataSource: props.dataSource || [],
columns: props.columns || [],
wrapClass: props.wrapClass || null,
wrapStyle: props.wrapStyle || null,
style: props.style || null,
className: props.className || null,
}
this.renderRow = props.renderRow || null
}
onSelectAll() {
for (let ref in this.refs) {
if (ref!=='selectAll') {
this.refs[ref].setState({checked:true})
}
}
}
offSelectAll() {
for (let ref in this.refs) {
if (ref!=='selectAll') {
this.refs[ref].setState({checked:false})
}
}
}
_renderHead() {
return this.state.columns.map((item,i) => {
return [<th>{i===0?<Checkbox ref="selectAll" onConfirm={()=>this.onSelectAll()} onCancel={()=>this.offSelectAll()} />:''}{item.title}</th>]
})
}
_renderBody() {
let _renderRow = this.renderRow;
return this.state.dataSource.map((item) => {
return _renderRow && _renderRow(item)
})
}
render() {
let state = this.state;
return (
<div className={state.wrapClass} style={state.wrapStyle}>
<table
border="0"
style={state.style}
className={"ry-table " + (state.className && state.className : "")}>
<thead>
<tr>{this._renderHead()}</tr>
</thead>
<tbody>
{this._renderBody()}
</tbody>
</table>
</div>
)
}
}
export default Table</code></pre>
<p><code>Component</code>是<code>React</code>内的一个基类,用于继承和创建<code>React</code>自定义组件。ES6规范下的面向对象实现起来非常精简,<code>class</code>关键字 可以快速创建一个类,而<code>Component</code>类内的所有属性和方法均可以通过<code>this</code>访问。换而言之,在<code>Component</code>内的任意方法内,可以通过<code>this.xxx</code>的方式调用该<code>Component</code>的其他属性和方法。</p>
<p>接着分析上面的代码,寥寥几行实现的是对一个Table组件的封装,借鉴了<code>ReactNative</code>组件的设计思路,通过外部传递<code>dataSource</code>(数据源)、<code>columns</code>(表格的表头项)、<code>renderRow</code>(当行渲染的模板函数)来完成一个Table的构建,支持全选和取消全选的功能、允许外部传递<code>className</code>和<code>style</code>对象来修改样式。</p>
<blockquote><p>从这个例子我们可以发现:只要不采用<code>function</code>定义函数,<code>Component</code>所有方法内部的<code>this</code>对象始终指向该类自身。</p></blockquote>
<h3>container 调用 component 时传递的 <code>this</code>
</h3>
<p>还是继续上面的例子,下面在一个做为Demo的<code>container</code>里调用之前 的<code>Table</code>。</p>
<pre><code>import Table from '../../components/Views/Table/' </code></pre>
<p>接着编写<code>renderRow</code>函数并传递给Table组件</p>
<pre><code>_renderRow(row) {
// ------------ 注意:这里对callback函数的写法 -----------
let onEdit = (x)=> {
console.log(x+x)
}, onDelete = (x)=> {
console.log(x*x)
}
// ---------------------------------------------------
return (
<tr>
<td><Checkbox ref={"item_" + row.key} />{row.key}</td>
<td>{row.name}</td>
<td>{row.age}</td>
<td>{row.birthday}</td>
<td>{row.job}</td>
<td>{row.address}</td>
<td>
<Button type="primary" callback={()=>onEdit(row.key)} text="编辑" />
<Button type="secondary" callback={()=>onDelete(row.key)} text="删除" />
</td>
</tr>
)
}
//... 省略一大堆代码
render() {
let dataSource = [{
key: '1',
name: '胡彦斌',
age: 32,
birthday: '2016-12-29',
job: '前端工程师',
address: '西湖区湖底公园1号'
}, {
key: '2',
name: '胡彦祖',
age: 42,
birthday: '2016-12-29',
job: '前端工程师',
address: '西湖区湖底公园1号'
}],columns = [{
title: '编号',
dataIndex: 'key',
key: 'key',
},{
title: '姓名',
dataIndex: 'name',
key: 'name',
}, {
title: '年龄',
dataIndex: 'age',
key: 'age',
}, {
title: '生日',
dataIndex: 'birthday',
key: 'birthday',
}, {
title: '职务',
dataIndex: 'job',
key: 'job',
},{
title: '住址',
dataIndex: 'address',
key: 'address',
}, {
title: '操作',
dataIndex: 'operate',
key: 'operate',
}];
return (
<div>
<Table dataSource={dataSource} columns={columns} renderRow={this._renderRow}/>
</div>
);
}</code></pre>
<p>显示效果如下:</p>
<p><img src="/img/remote/1460000006032234" alt="这里写图片描述" title="这里写图片描述"></p>
<p>分析上面的代码,有几处容易出错的地方:</p>
<ol>
<li><p><code>_renderRow</code> 作为<code>component</code>的方法来定义,然后在对应的<code>render</code>函数内通过<code>this</code>来调用。很重要的一点,这里<code>this._renderRow</code>作为的是函数名方式传递。</p></li>
<li>
<p><code>_renderRow</code> 内部<code>Button</code>组件的<code>callback</code>是按钮点击后触发的回调,也是一个函数,但是这个函数没有像上面一样放在<code>component</code>的方法里定义,而是作为一个变量定义并通过匿名函数的方式传递给子组件:</p>
<pre><code>let onEdit = (x)=> {
console.log(x+x)
}
// .....
callback={()=>onEdit(row.key)}</code></pre>
<p>这样就避开了使用<code>this</code>时上下文变化的问题。这一点是很讲究的,如果沿用上面的写法很容易这样写:</p>
<pre><code>onEdit(x) {
console.log(x+x)
}
// ...
callback={()=>this.onEdit(row.key)}</code></pre>
<p>但是很遗憾,这样写<code>this</code>传递到子组件后会变成<code>undefined</code>,从而报错。</p>
</li>
<li>
<p>父组件如要调用子组件的方法,有两种方式:</p>
<ul>
<li>
<p>第一种 通过匿名函数的方式</p>
<pre><code>callback = {()=>this.modalShow()}</code></pre>
</li>
<li>
<p>第二种 使用 <code>bind</code></p>
<pre><code>callback = {this.modalShow.bind(this)}</code></pre>
</li>
</ul>
</li>
</ol>
<blockquote><p>注意:如果要绑定的函数需要传参数,可以这么写: <code>xxx.bind(this,arg1,arg2...)</code></p></blockquote>
<h2>参考</h2>
<p><a href="https://link.segmentfault.com/?enc=ElSIk6N2jgN6TKyRfVNIDA%3D%3D.%2FmvcTcDRbtXKob27oHYI%2BN9RmyRW5l%2Bu2C0p%2FrPXsty5lWqAbiOY83X6rac%2BFhp3yogDs531Cdk3OtMmjLnXowOTqjNymu81q8gk7UjRT8ZwLzxX8H1hfYqIXqXpyDRjeVbSzf%2FN2t7I93ZOGFW0DA%3D%3D" rel="nofollow">《ECMAScript 6 入门》</a></p>
<hr>
<p>@欢迎关注我的 <a href="https://link.segmentfault.com/?enc=I%2F7Tx9jFztlj9Uzbuv84%2Fg%3D%3D.nry%2B5f2a4ik%2BXTVsxD8wEsrRW7Ji9unDxwaJT9rp%2BF8%3D" rel="nofollow"><code>github</code></a> 和 <a href="https://link.segmentfault.com/?enc=YmLoGJNR8NeNW0qV4Wn93Q%3D%3D.CNWt9f3EFbtoyFGF4Z%2BIXwAXMMO9no8MQLxU0ii5XU0%3D" rel="nofollow">个人博客 -Jafeney</a></p>