SegmentFault iKcamp最新的文章
2018-12-27T17:33:14+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
iKcamp新书上市《Koa与Node.js开发实战》
https://segmentfault.com/a/1190000017540384
2018-12-27T17:33:14+08:00
2018-12-27T17:33:14+08:00
iKcamp
https://segmentfault.com/u/ikcamp
19
<p><img src="/img/remote/1460000017540387?w=500&h=644" alt="Koa与Node.js开发实战" title="Koa与Node.js开发实战"></p>
<h2>内容摘要</h2>
<p>Node.js 10已经进入LTS时代!其应用场景已经从脚手架、辅助前端开发(如SSR、PWA等)扩展到API中间层、代理层及专业的后端开发。Node.js在企业Web开发领域也日渐成熟,无论是在API中间层,还是在微服务中都得到了非常好的落地。本书将通过Web开发框架Koa2,引领你进入Node.js的主战场!<br>本书系统讲解了在实战项目中使用Koa框架开发Web应用的流程和步骤。第1章介绍Node.js的安装、开发工具及调试。第2章和第3章介绍搭建Koa实战项目的雏形。第4章详细介绍HTTP基础知识及其实战应用。第5章介绍MVC、模板引擎和文件上传等实用功能。第6~8章介绍数据库、单元测试及项目的优化与部署。第9~13章介绍从零开始搭建时下火爆的微信小程序前端及后台管理应用的全部过程,以及最终的服务器部署,包括HTTPS、Nginx。<br>本书示例丰富、侧重实战,以完整的实战项目贯穿全部章节,并提供书中涉及的所有源码及部分章节的配套视频教程,将是前端开发人员立足新领域和后端开发人员了解Node.js并使用Koa2开发Web应用的得力助手。</p>
<h2>前言</h2>
<p>Node.js诞生于2009年,到本书出版时已经有近10个年头。它扩充了JavaScript的应用范围,使JavaScript也能像其他语言一样操作各种系统资源,因此,前端工程化开发的大量工具都开始运行在Node.js环境中。由于Node.js采用事件驱动、非阻塞I/O和异步输出来提升性能,因此大量I/O密集型的应用也采用Node.js开发。掌握Node.js开发,既能极大地拓宽前端开发者的技术知识面,也能拓展前端开发者的生存空间,从目前前端开发者越来越多的环境中脱颖而出。</p>
<p>由于Node.js仅提供基础的类库,开发者需要自主合理地设计应用架构,并调用大量基础类库来进行开发。为了提升开发效率和降低开发门槛,相关技术社区涌现出不少基于Node.js的Web框架。</p>
<p>Express框架在Node.js诞生之初出现,并迅速成为主流的Web应用开发框架。在社区中,大量的第三方开发者开发了丰富的Express插件,极大地降低了基于Node.js的Web应用开发成本,同时也带动了大量的开发者选择使用Express框架开发Web应用。但Express框架采用传统的回调方式处理异步调用,对于经验不足的开发者来说,很容易将代码写成“回调地狱”,使开发的应用难以持续维护。在ECMAScript 6的规范中提出了Generator函数,依据该规范,Express的作者TJ Holowaychuk<a href="https://link.segmentfault.com/?enc=KSSVUz1AdZHMfndTTNOIeg%3D%3D.D5PTG%2F2Pgub3nXgbpIOKRsJU2DKiJBD6FgVgo5l%2FY6s%3D" rel="nofollow">https://github.com/tj</a>巧妙地开发了co库<a href="https://link.segmentfault.com/?enc=0FAddokq4xq%2FX0%2B11u3MGw%3D%3D.a166TBhn76O0ipQ%2FdLY5mlbMzpOh6O4cF86XK5BAEMc%3D" rel="nofollow">https://github.com/tj/co</a>,使开发者能够通过yield关键词,像编写同步代码一样开发异步应用,从而解决了“回调地狱”问题。2014年,他基于co库开发了新一代的Web应用开发框架Koa,用官方语言来描述这个框架就是“next generation web framework for Node.js”。</p>
<p>社区开发者为Koa开发了大量的插件,与Express相比,两者的处理机制存在根本上的差异。Express的插件是顺序执行的,而Koa的中间件基于“洋葱模型”,可以在中间件中执行请求处理前和请求处理后的代码。ECMAScript 7提供了Async/Await关键词,从语法层面更好地支持了异步调用。TJ Holowaychuk在Koa的基础上,采用Async/Await取代co库处理异步回调,发布了Koa第2版(简称Koa2)。随着Node 8 LTS(Long Term Support,长期支持)的发布,LTS版本正式支持ECMAScript 7规范,选择使用Koa开发框架开发的Node.js Web应用也越来越多,Koa框架逐步取代了Express框架。<br>尽管目前Koa非常流行,但“纯天然”支持ECMAScript 7语法的Node.js 8在2017年10月才正式发布。目前,市面上介绍Koa的书籍几乎没有,大多介绍的是Express框架,本书可以说是第一本介绍Koa的书籍。本书从Node.js基础、HTTP、Koa框架、数据库、单元测试和运维部署等方面全方位地介绍了应用开发所应具备的知识体系。通过阅读本书,读者可以了解Node.js开发的方方面面,减少实际开发中出现的问题。同时,本书的重点章节也提供了线上代码讲解和视频,读者可以在阅读本书的同时,结合线上代码讲解和视频,更容易地理解本书介绍的知识。</p>
<p>特别感谢杜珂珂、哈志辉、姜帅、李波、李益、盛瀚钦、田小虎、徐磊、闫萌、赵晨雪(排名不分先后)对线上培训音视频课程资源的开发和支持。</p>
<h3>本书特色</h3>
<ul><li>重点章节附带教学视频。</li></ul>
<p>为了便于读者理解本书的内容,一些基础、重点的内容配有视频教程。读者可以访问<a href="https://link.segmentfault.com/?enc=KkoPPHx1XnK8GAeH2Y5fRA%3D%3D.5Z0lQMhGSnEOj2b78KZGu%2BEHLhOHMcpTt6zlzLFa6V0%3D" rel="nofollow">https://ikcamp.com</a>,结合书中内容观看视频。</p>
<ul><li>所有源码托管于GitHub。</li></ul>
<p>为了降低读者获取源码的难度,本书的所有源码都托管于GitHub(<a href="https://link.segmentfault.com/?enc=JAAVnjX%2FqPK%2FEG%2BFfvm%2B%2BQ%3D%3D.IMEWUhuxjGWXP3TvxKsO9LEmhbMZitE0K38viNEbURo%3D" rel="nofollow">https://github.com/</a> ikcamp),读者也可通过GitHub直接和本书作者沟通。</p>
<ul><li>一线互联网公司Node.js技术栈实战经验总结。</li></ul>
<p>本书补充了前端开发者所不具备的后端开发技能和规范,介绍了如何开发Koa应用,如何通过ORM(Object Relational Mapping,对象关系映射)类库读写数据库,如何通过单元测试来保障代码质量,如何通过PM2、CI等方式启动并部署Node.js应用,以及如何采用日志、监控来保障线上应用的稳定运行等内容。</p>
<ul><li>典型项目案例解析,实战性强。</li></ul>
<p>本书第3篇通过云相册小程序开发项目介绍了目前流行的小程序技术,包括小程序登录流程、扫码登录、文件上传、相册管理等功能。通过学习本书的相关内容,读者可以独立开发时下流行的小程序和其需要的后端服务。</p>
<h3>本书知识体系</h3>
<h4>第1篇 基础知识(第1~4章)</h4>
<p>这部分介绍了开发Koa应用需要具备的预备知识,包括Node.js入门、遇见Koa、路由和HTTP共4个章节。<br>在第1章中,介绍了Node.js的历史和发展过程,以及Node.js基础和环境准备。介绍了NPM(Node Package Manager,Node.js的第三方包管理工具),通过该包管理工具,开发者能够方便地使用大量的第三方软件包。本章还介绍了微软公司推出的免费开发工具:Visual Studio Code编辑器,以及如何使用该编辑器调试Node.js应用。<br>在第2章中介绍了Koa的发展历程和作为Koa核心技术的中间件。<br>在第3章中介绍了路由的概念,以及Koa中最流行的路由中间件koa-router。<br>在第4章中介绍了HTTP的基础知识,以及HTTP的后续协议HTTP/2;介绍了在Node.js中如何获取客户端传递来的数据,如何通过koa-bodyparser中间件获取请求中的body数据等。</p>
<h4>第2篇 应用实战(第5~8章)</h4>
<p>这部分介绍了应用开发各个环节的知识,包含构建Koa Web应用、数据库、单元测试、优化与部署共4个章节。<br>在第5章中介绍了MVC架构、模板引擎、静态资源,以及如何输出JSON数据,如何通过koa-multer中间件上传文件等。<br>在第6章中介绍了数据库的概念和以MySQL为代表的关系型数据库,以及如何通过ORM类库操作MySQL数据库;介绍了以MongoDB为代表的非关系型数据库,以及如何在Node.js中操作MongoDB;介绍了以Redis为代表的新型缓存数据库,以及如何在Node.js中利用Redis实现Session持久化。<br>在第7章中介绍了Chai断言库,它用来检测单元测试过程中的结果是否符合预期;介绍了Mocha测试框架,使用该框架可以编写和运行单元测试代码;介绍了使用SuperTest工具测试HTTP服务,以及通过Nock库模拟HTTP服务请求响应;最后,介绍了Nyc工具,用以检查单元测试的覆盖率、提升代码质量。<br>在第8章中介绍了如何记录日志和统一捕获异常,以及如何输出自定义错误页;介绍了如何通过PM2、Docker启动应用,如何通过CI集成发布应用,如何通过Nginx提供HTTPS支持;介绍了如何利用日志等途径监控服务器运行情况,以及如何利用PM2提供的Keymetrics监控云服务器。</p>
<h4>第3篇 项目实战:从零开始搭建微信小程序后台(第9~13章)</h4>
<p>这部分通过介绍时下最流行的小程序开发,结合具体的相册小程序来说明如何开发一个完整的小程序,以及如何部署小程序。其中,汇总本书前面章节的知识介绍了小程序的功能模块、接口开发、小程序开发、管理后台开发和服务部署。<br>在第9章中介绍了小程序应具备的产品功能及如何开发小程序门户网站。<br>在第10章中介绍了小程序登录流程,扫码登录的逻辑和实现方式,小程序中用到的接口和后台管理系统需要的接口。具体包括如何通过中间件来鉴权,如何统一控制后台管理系统的权限,如何通过Mongoose来定义数据模型和访问、存储数据,如何使用log4js记录日志。<br>在第11章中介绍了开发微信小程序的流程,以及如何借助微信开发者工具开发小程序。<br>在第12章中介绍了开发后台管理系统的整体架构和设计思路,并提供了一套登录与鉴权的技术方案。<br>在第13章中介绍了小程序相关服务的线上部署过程,包括对数据库、Nginx、HTTPS、和Koa服务的部署,具体包括如何通过Nginx实现把多个域名解析到同一台云服务器上,如何通过PM2管理应用。</p>
<h2>本书适合读者</h2>
<ul>
<li>Web前端开发人员</li>
<li>对Node.js应用感兴趣的开发人员</li>
<li>Node.js开发的自学者</li>
<li>大中专院校相关专业的教师和学生</li>
<li>相关培训机构的学员</li>
</ul>
<p>本书由陈达孚、金晶、干珺、张利涛、戴亮、周遥、薛淑英编写。本书涉及的技术知识点较多,作者团队成员虽竭力争取奉献好的作品以使技术得到更好的普及,但难免存在疏漏和不足,读者如有问题或建议,可以直接到iKcamp的GitHub上留言。本书源码也可前往GitHub上获取,地址为<a href="https://link.segmentfault.com/?enc=5TVTHJEIsTBkyP9miCvybA%3D%3D.XFHnZhRcK5XwH8pwkn8cxkIuC1QOstjHjwJAAd6czTI%3D" rel="nofollow">https://github.com/ikcamp</a>。本书部分内容配有视频,可前往<a href="https://link.segmentfault.com/?enc=AkIlec6nwmNqlNGcEoug%2Fw%3D%3D.d4Jkq6qcQI12LIRJ9F4A9bSTWuJFTm5If73fGUnuR3XqYFTrSkdaSKVlo6WNzQG1ZHrlbpFKlBqKb1mK1c9iGQ%3D%3D" rel="nofollow">https://camp.qianduan.group/k...</a>。</p>
<h2>本书已经在各大电商网站开始上架,感谢对iKcamp的支持!</h2>
React 深入系列4:组件的生命周期
https://segmentfault.com/a/1190000014547923
2018-04-23T18:10:33+08:00
2018-04-23T18:10:33+08:00
iKcamp
https://segmentfault.com/u/ikcamp
4
<blockquote>文:徐超,《React进阶之路》作者<p>授权发布,转载请注明作者及出处</p>
</blockquote>
<hr>
<h2>React 深入系列4:组件的生命周期</h2>
<blockquote>React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。</blockquote>
<p>组件是构建React应用的基本单位,组件需要具备数据获取、业务逻辑处理、以及UI呈现的能力,而这些能力是要依赖于组件不同的生命周期方法的。组件的生命周期分为3个阶段:挂载阶段、更新阶段、卸载阶段,每个阶段都包含相应的生命周期方法。因为是深入系列文章,本文不会仔细介绍每个生命周期方法的使用,而是会重点讲解在使用组件生命周期时,经常遇到的疑问和错误使用方式。</p>
<h3>服务器数据请求</h3>
<p>初学者在使用React时,常常不知道何时向服务器发送请求,获取组件所需数据。对于组件所需的初始数据,最合适的地方,是在componentDidMount方法中,进行数据请求,这个时候,组件完成挂载,其代表的DOM已经挂载到页面的DOM树上,即使获取到的数据需要直接操作DOM节点,这个时候也是绝对安全的。有些人还习惯在constructor或者componentWillMount中,进行数据请求,认为这样可以更快的获取到数据,但它们相比componentDidMount的执行时间,提前的时间实在是太微乎其微了。另外,当进行服务器渲染时(SSR),componentWillMount是会被调用两次的,一次在服务器端,一次在客户端,这时候就会导致额外的请求发生。</p>
<p>组件进行数据请求的另一种场景:由父组件的更新导致组件的props发生变化,如果组件的数据请求依赖props,组件就需要重新进行数据请求。例如,新闻详情组件NewsDetail,在获取新闻详情数据时,需要传递新闻的id作为参数给服务器端,当NewsDetail已经处于挂载状态时,如果点击其他新闻,NewsDetail的componentDidMount并不会重新调用,因而componentDidMount中进行新闻详情数据请求的方法也不会再次执行。这时候,应该在componentWillReceiveProps中,进行数据请求:</p>
<pre><code>componentWillReceiveProps(nextProps) {
if(this.props.newId !== nextProps.newsId) {
fetchNewsDetailById(nextProps.newsId) // 根据最新的新闻id,请求新闻详情数据
}
}</code></pre>
<p>如果进行数据请求的时机是由页面上的交互行为触发的,例如,点击查询按钮后,查询数据,这时只需要在查询按钮的事件监听函数中,执行数据请求即可,这种情况一般是不会有疑问的。</p>
<h3>更新阶段方法的调用</h3>
<p>组件的更新是组件生命周期中最复杂的阶段,也是涉及到最多生命周期方法的阶段。</p>
<p>正常情况下,当组件发生更新时,组件的生命周期方法的调用顺序如下:</p>
<pre><code>componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate
// 组件收到新的props(props中的数据并不一定真正发生变化)-> 决定是否需要继续执行更新过程 -> 组件代表的虚拟DOM即将更新 -> 组件重新计算出新的虚拟DOM -> 虚拟DOM对应的真实DOM更新到真实DOM树中</code></pre>
<p>父组件发生更新或组件自身调用setState,都会导致组件进行更新操作。父组件发生更新导致的组件更新,生命周期方法的调用情况同上所述。如果是组件自身调用setState,导致的组件更新,其生命周期方法的调用情况如下:</p>
<pre><code>shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate</code></pre>
<p>可见,这种情况下componentWillReceiveProps并不会被调用。</p>
<p>当组件的shouldComponentUpdate返回false时,组件会停止更新过程,这时候生命周期方法的调用顺序如下:</p>
<pre><code>componentWillReceiveProps -> shouldComponentUpdate -> 结束</code></pre>
<p>或(组件自身调用setState,导致的组件更新):</p>
<pre><code>shouldComponentUpdate -> 结束</code></pre>
<h3>setState的时机</h3>
<p>组件的生命周期方法众多,哪些方法中可以调用setState更新组件状态?哪些方法中不可以呢?</p>
<ul>
<li>
<p><strong>可以的方法</strong></p>
<p>componentWillMount、componentDidMount、componentWillReceiveProps、componentDidUpdate</p>
<p>这里有几个注意点:</p>
<ol>
<li>componentWillMount 中<strong>同步</strong>调用setState不会导致组件进行额外的渲染,组件经历的生命周期方法依次是componentWillMount -> render -> componentDidMount,组件并不会因为componentWillMount中的setState调用再次进行更新操作。如果是<strong>异步</strong>调用setState,组件是会进行额外的更新操作。不过实际场景中很少在componentWillMount中调用setState,一般可以通过直接在constructor中定义state的方式代替。</li>
<li>一般情况下,当调用setState后,组件会执行一次更新过程,componentWillReceiveProps等更新阶段的方法会再次被调用,但如果在componentWillReceiveProps中调用setState,并不会额外导致一次新的更新过程,也就是说,当前的更新过程结束后,componentWillReceiveProps等更新阶段的方法<strong>不会</strong>再被调用一次。(注意,这里仍然指<strong>同步</strong>调用setState,如果是异步调用,则会导致组件再次进行渲染)</li>
<li>componentDidUpdate中调用setState要格外小心,在setState前必须有条件判断,只有满足了相应条件,才setState,否组组件会不断执行更新过程,进入死循环。因为setState会导致新一次的组件更新,组件更新完成后,componentDidUpdate被调用,又继续setState,死循环就产生了。</li>
</ol>
</li>
<li>
<p><strong>不可以的方法</strong></p>
<p>其他生命周期方法都不能调用setState,主要原因有两个:</p>
<ol>
<li>产生死循环。例如,shouldComponentUpdate、componentWillUpdate 和 render 中调用setState,组件本次的更新还没有执行完成,又会进入新一轮的更新,导致不断循环更新,进入死循环。</li>
<li>无意义。componentWillUnmount 调用时,组件即将被卸载,setState是为了更新组件,在一个即将卸载的组件上更新state显然是无意义的。实际上,在componentWillUnmount中调用setState也是会抛出异常的。</li>
</ol>
</li>
</ul>
<h3>render次数 != 浏览器界面更新次数</h3>
<p>先看下面的一个例子:</p>
<pre><code>class App extends React.Component {
constructor(props) {
super(props)
this.state = {
bgColor: "red"
}
}
render() {
var {bgColor} = this.state
return (
<div style = {{backgroundColor: bgColor}}>
Test
</div>
);
}
componentDidMount() {
this.setState({
bgColor: "yellow"
})
}
}</code></pre>
<p>当我们观察浏览器渲染出的页面时,页面中Test所在div的背景色,是先显示红色,再变成黄色呢?还是直接就显示为黄色呢?</p>
<p>答案是:直接就显示为黄色!</p>
<p>这个过程中,组件的生命周期方法被调用的顺序如下:</p>
<pre><code>constructor -> componentWillMount -> render -> componentDidMount -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate</code></pre>
<p>组件在挂载完成后,因为setState的调用,将立即执行一次更新过程。虽然render方法被调用了两次,但这并不会导致浏览器界面更新两次,实际上,两次DOM的修改会合并成一次浏览器界面的更新。React官网介绍componentDidMount方法时也有以下说明:</p>
<blockquote>Calling <code>setState()</code> in this method will trigger an extra rendering, but it will happen before the browser updates the screen. This guarantees that even though the <code>render()</code> will be called twice in this case, the user won’t see the intermediate state.</blockquote>
<p>这说明,组件render的次数 <strong>不一定等于</strong> 浏览器界面更新次数。虽然JS的执行和DOM的渲染分别由浏览器不同的线程完成,但JS的执行会阻塞DOM的渲染,而上面的两次render是在一个JS事件周期内执行的,所以在两次render结束前,浏览器不会更新界面。</p>
<h3>下篇预告:</h3>
<p>React 深入系列5:事件处理</p>
<hr>
<p>新书推荐《React进阶之路》</p>
<p>作者:徐超</p>
<p>毕业于浙江大学,硕士,资深前端工程师,长期就职于能源物联网公司远景智能。8年软件开发经验,熟悉大前端技术,拥有丰富的Web前端和移动端开发经验,尤其对React技术栈和移动Hybrid开发技术有深入的理解和实践经验。</p>
<p><img src="/img/remote/1460000014177338?w=297&h=387" alt="" title=""></p>
<hr>
<p><img src="/img/remote/1460000014229430?w=1500&h=854" alt="" title=""></p>
<hr>
<p>美团点评广告平台大前端团队招收20192020年前端实习生(偏动效方向)</p>
<p>有意者邮件:yao.zhou@meituan.com</p>
React 深入系列3:Props 和 State
https://segmentfault.com/a/1190000014411837
2018-04-16T16:05:47+08:00
2018-04-16T16:05:47+08:00
iKcamp
https://segmentfault.com/u/ikcamp
12
<blockquote>文:徐超,《React进阶之路》作者<p>授权发布,转载请注明作者及出处</p>
</blockquote>
<hr>
<h2>React 深入系列3:Props 和 State</h2>
<blockquote>React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。</blockquote>
<p>React 的核心思想是组件化的思想,而React 组件的定义可以通过下面的公式描述:</p>
<pre><code>UI = Component(props, state)</code></pre>
<p>组件根据props和state两个参数,计算得到对应界面的UI。可见,props 和 state 是组件的两个重要数据源。</p>
<p><strong>本篇文章不是对props 和state 基本用法的介绍,而是尝试从更深层次解释props 和 state,并且归纳使用它们时的注意事项。</strong></p>
<h3>Props 和 State 本质</h3>
<p><strong>一句话概括,props 是组件对外的接口,state 是组件对内的接口。</strong>组件内可以引用其他组件,组件之间的引用形成了一个树状结构(组件树),如果下层组件需要使用上层组件的数据或方法,上层组件就可以通过下层组件的props属性进行传递,因此props是组件对外的接口。组件除了使用上层组件传递的数据外,自身也可能需要维护管理数据,这就是组件对内的接口state。根据对外接口props 和对内接口state,组件计算出对应界面的UI。</p>
<p>组件的props 和 state都和组件最终渲染出的UI直接相关。两者的主要区别是:state是可变的,是组件内部维护的一组用于反映组件UI变化的状态集合;而props是组件的只读属性,组件内部不能直接修改props,要想修改props,只能在该组件的上层组件中修改。在组件<strong>状态上移</strong>的场景中,父组件正是通过子组件的props,传递给子组件其所需要的状态。</p>
<h3>如何定义State</h3>
<p>定义一个合适的state,是正确创建组件的第一步。state必须能代表一个组件UI呈现的<strong>完整状态集</strong>,即组件对应UI的任何改变,都可以从state的变化中反映出来;同时,state还必须是代表一个组件UI呈现的<strong>最小状态集</strong>,即state中的所有状态都是用于反映组件UI的变化,没有任何多余的状态,也不需要通过其他状态计算而来的中间状态。</p>
<p>组件中用到的一个变量是不是应该作为组件state,可以通过下面的4条依据进行判断:</p>
<ol>
<li>这个变量是否是通过props从父组件中获取?如果是,那么它不是一个状态。</li>
<li>这个变量是否在组件的整个生命周期中都保持不变?如果是,那么它不是一个状态。</li>
<li>这个变量是否可以通过state 或props 中的已有数据计算得到?如果是,那么它不是一个状态。</li>
<li>这个变量是否在组件的render方法中使用?如果<strong>不是</strong>,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个<strong>普通属性</strong>(除了props 和 state以外的组件属性 ),例如组件中用到的定时器,就应该直接定义为this.timer,而不是this.state.timer。</li>
</ol>
<p><strong>请务必牢记,并不是组件中用到的所有变量都是组件的状态!</strong>当存在多个组件共同依赖同一个状态时,一般的做法是<strong>状态上移</strong>,将这个状态放到这几个组件的公共父组件中。</p>
<h3>如何正确修改State</h3>
<h4>1.不能直接修改State。</h4>
<p>直接修改state,组件并不会重新重发render。例如:</p>
<pre><code>// 错误
this.state.title = 'React';</code></pre>
<p>正确的修改方式是使用<code>setState()</code>:</p>
<pre><code>// 正确
this.setState({title: 'React'});</code></pre>
<h4>2. State 的更新是异步的。</h4>
<p>调用<code>setState</code>,组件的state并不会立即改变,<code>setState</code>只是把要修改的状态放入一个队列中,React会优化真正的执行时机,并且React会出于性能原因,可能会将多次<code>setState</code>的状态修改合并成一次状态修改。所以不能依赖当前的state,计算下个state。当真正执行状态修改时,依赖的this.state并不能保证是最新的state,因为React会把多次state的修改合并成一次,这时,this.state还是等于这几次修改发生前的state。另外需要注意的是,同样不能依赖当前的props计算下个state,因为props的更新也是异步的。</p>
<p>举个例子,对于一个电商类应用,在我们的购物车中,当点击一次购买按钮,购买的数量就会加1,如果我们连续点击了两次按钮,就会连续调用两次<code>this.setState({quantity: this.state.quantity + 1})</code>,在React合并多次修改为一次的情况下,相当于等价执行了如下代码:</p>
<pre><code>Object.assign(
previousState,
{quantity: this.state.quantity + 1},
{quantity: this.state.quantity + 1}
)</code></pre>
<p>于是乎,后面的操作覆盖掉了前面的操作,最终购买的数量只增加了1个。</p>
<p>如果你真的有这样的需求,可以使用另一个接收一个函数作为参数的<code>setState</code>,这个函数有两个参数,第一个参数是组件的前一个state(本次组件状态修改成功前的state),第二个参数是组件当前最新的props。如下所示:</p>
<pre><code>// 正确
this.setState((preState, props) => ({
counter: preState.quantity + 1;
}))</code></pre>
<h4>3. State 的更新是一个浅合并(Shallow Merge)的过程。</h4>
<p>当调用<code>setState</code>修改组件状态时,只需要传入发生改变的状态变量,而不是组件完整的state,因为组件state的更新是一个浅合并(Shallow Merge)的过程。例如,一个组件的state为:</p>
<pre><code>this.state = {
title : 'React',
content : 'React is an wonderful JS library!'
}</code></pre>
<p>当只需要修改状态<code>title</code>时,只需要将修改后的<code>title</code>传给<code>setState</code>:</p>
<pre><code>this.setState({title: 'Reactjs'});</code></pre>
<p>React会合并新的<code>title</code>到原来的组件state中,同时保留原有的状态<code>content</code>,合并后的state为:</p>
<pre><code>{
title : 'Reactjs',
content : 'React is an wonderful JS library!'
}</code></pre>
<h3>State与Immutable</h3>
<p>React官方建议把state当作不可变对象,一方面是如果直接修改this.state,组件并不会重新render;另一方面state中包含的所有状态都应该是不可变对象。当state中的某个状态发生变化,我们应该重新创建一个新状态,而不是直接修改原来的状态。那么,当状态发生变化时,如何创建新的状态呢?根据状态的类型,可以分成三种情况:</p>
<h4>1. 状态的类型是不可变类型(数字,字符串,布尔值,null, undefined)</h4>
<p>这种情况最简单,因为状态是不可变类型,直接给要修改的状态赋一个新值即可。如要修改count(数字类型)、title(字符串类型)、success(布尔类型)三个状态:</p>
<pre><code>this.setState({
count: 1,
title: 'Redux',
success: true
})</code></pre>
<h4>2. 状态的类型是数组</h4>
<p>如有一个数组类型的状态books,当向books中增加一本书时,使用数组的concat方法或ES6的数组扩展语法(spread syntax):</p>
<pre><code>// 方法一:使用preState、concat创建新数组
this.setState(preState => ({
books: preState.books.concat(['React Guide']);
}))
// 方法二:ES6 spread syntax
this.setState(preState => ({
books: [...preState.books, 'React Guide'];
}))</code></pre>
<p>当从books中截取部分元素作为新状态时,使用数组的slice方法:</p>
<pre><code>// 使用preState、slice创建新数组
this.setState(preState => ({
books: preState.books.slice(1,3);
}))</code></pre>
<p>当从books中过滤部分元素后,作为新状态时,使用数组的filter方法:</p>
<pre><code>// 使用preState、filter创建新数组
this.setState(preState => ({
books: preState.books.filter(item => {
return item != 'React';
});
}))</code></pre>
<p>注意不要使用push、pop、shift、unshift、splice等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat、slice、filter会返回一个新的数组。</p>
<h4>3. 状态的类型是简单对象(Plain Object)</h4>
<p>如state中有一个状态owner,结构如下:</p>
<pre><code>this.state = {
owner = {
name: '老干部',
age: 30
}
}</code></pre>
<p>当修改state时,有如下两种方式:</p>
<p><strong>1) 使用ES6 的Object.assgin方法</strong></p>
<pre><code>this.setState(preState => ({
owner: Object.assign({}, preState.owner, {name: 'Jason'});
}))</code></pre>
<p><strong>2) 使用对象扩展语法(<a href="https://link.segmentfault.com/?enc=qc6LvwyQuNO55C4ok69F7Q%3D%3D.1pntKHOrMdPIEtfb%2FAdWHkdbGskQafKxFI7o38CwtGkvt6BjLi17Br3IdPr%2BPKDIKFT3oAMWCluKu%2BkQAWjLqg%3D%3D" rel="nofollow">object spread properties</a>)</strong></p>
<pre><code>this.setState(preState => ({
owner: {...preState.owner, name: 'Jason'};
}))</code></pre>
<p>总结一下,创建新的状态的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。当然,也可以使用一些Immutable的JS库,如<a href="https://link.segmentfault.com/?enc=1hZpYk8G5AtkPZUL2C79nA%3D%3D.z%2B78oHOeupVadG8rssZckZv7q2XzJ9IfaPQnAE6tzjPRQkxF7WyrBC5q544HQi8a" rel="nofollow">Immutable.js</a>,实现类似的效果。</p>
<p>那么,为什么React推荐组件的状态是不可变对象呢?一方面是因为不可变对象方便管理和调试,了解更多可<a href="https://link.segmentfault.com/?enc=Qg846XGS7sz1%2FZ%2BJmnQosQ%3D%3D.3VqUlLh1lbR4fUhhrKWXarjpmtGzqtcQiPbzmeJ9wAkeVXBZKlCe8uWZ3rMoR%2B9WOZ53f0yyRNvE%2BK38wwFmkHe6Mst8wJzjYQbqt7ypu88%3D" rel="nofollow">参考这里</a>;另一方面是出于性能考虑,当组件状态都是不可变对象时,我们在组件的<code>shouldComponentUpdate</code>方法中,仅需要比较状态的引用就可以判断状态是否真的改变,从而避免不必要的<code>render</code>方法的调用。当我们使用React 提供的<code>PureComponent</code>时,更是要保证组件状态是不可变对象,否则在组件的<code>shouldComponentUpdate</code>方法中,状态比较就可能出现错误。</p>
<h3>下篇预告:</h3>
<p>React 深入系列4:组件的生命周期</p>
<hr>
<p>新书推荐《React进阶之路》</p>
<p>作者:徐超</p>
<p>毕业于浙江大学,硕士,资深前端工程师,长期就职于能源物联网公司远景智能。8年软件开发经验,熟悉大前端技术,拥有丰富的Web前端和移动端开发经验,尤其对React技术栈和移动Hybrid开发技术有深入的理解和实践经验。</p>
<p><img src="/img/remote/1460000014177338?w=297&h=387" alt="" title=""></p>
<hr>
<p><img src="/img/remote/1460000014229430?w=1500&h=854" alt="" title=""></p>
<hr>
<p>美团点评广告平台大前端团队招收20192020年前端实习生(偏动效方向)</p>
<p>有意者邮件:yao.zhou@meituan.com</p>
React 深入系列2:组件分类
https://segmentfault.com/a/1190000014229425
2018-04-08T11:06:44+08:00
2018-04-08T11:06:44+08:00
iKcamp
https://segmentfault.com/u/ikcamp
2
<blockquote>文:徐超,《React进阶之路》作者<p>授权发布,转载请注明作者及出处</p>
</blockquote>
<hr>
<h3>React 深入系列2:组件分类</h3>
<blockquote>React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。</blockquote>
<p>React 组件有很多种分类方式,常见的分类方式有函数组件和类组件,无状态组件和有状态组件,展示型组件和容器型组件。好吧,这又是一篇咬文嚼字的文章。但是,真正把这几组概念咬清楚、嚼明白后,对于页面的组件划分、组件之间的解耦是大有裨益的。</p>
<h4>函数组件和类组件</h4>
<p>函数组件(Functional Component )和类组件(Class Component),划分依据是根据组件的定义方式。函数组件使用函数定义组件,类组件使用ES6 class定义组件。下面是函数组件和类组件的简单示例:</p>
<pre><code>// 函数组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// 类组件
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}</code></pre>
<p>上面的两种写法是等价的,但函数组件的写法要比类组件简洁,不过类组件比函数组件功能更加强大。类组件可以维护自身的状态变量,即组件的state,类组件还有不同的生命周期方法,可以让开发者能够在组件的不同阶段(挂载、更新、卸载),对组件做更多的控制。</p>
<p>类组件有这么多优点,是不是我们在开发中应该首选使用类组件呢?其实不然。函数组件更加专注和单一,承担的职责也更加清晰,它只是一个返回React 元素的函数,只关注对应UI的展现。函数组件接收外部传入的props,返回对应UI的DOM描述,仅此而已。当然,如上面例子所示,使用只包含一个render方法的类组件,可以实现和函数组件相同的效果。但函数组件的使用可以从思想上迫使你在设计组件时多做思考,更加关注逻辑和显示的分离,设计出更加合理的页面上组件树的结构。实际操作上,当一个组件不需要管理自身状态时,可以把它设计成函数组件,当你有足够的理由发现它需要“升级”为类组件时,再把它改造为类组件。因为函数组件“升级”为类组件是有一定成本的,这样就会要求你做这个改造前更认真地思考其合理性,而不是仅仅为了一时的方便就使用类组件。</p>
<h4>无状态组件和有状态组件</h4>
<p>无状态组件(Stateless Component )和有状态组件(Stateful Component),划分依据是根据组件内部是否维护state。无状态组件内部不使用state,只根据外部组件传入的props返回待渲染的React 元素。有状态组件内部使用state,维护自身状态的变化,有状态组件根据外部组件传入的props和自身的state,共同决定最终返回的React 元素。</p>
<p>很容易知道,函数组件一定是无状态组件,类组件则既可以充当无状态组件,也可以充当有状态组件。但如上文所述,当一个组件不需要管理自身状态时,也就是无状态组件,应该优先设计为函数组件。</p>
<h4>展示型组件和容器型组件</h4>
<p>展示型组件(Presentational Component)和容器型组件(Container Component),划分依据是根据组件的职责。</p>
<p>展示型组件的职责是:组件UI长成什么样。展示型组件不关心组件使用的数据是如何获取的,以及组件数据应该如何修改,它只需要知道有了这些数据后,组件UI是什么样子的即可。外部组件通过props传递给展示型组件所需的数据和修改这些数据的回调函数,展示型组件只是它们的使用者。展示型组件一般是无状态组件,不需要state,因为展示型组件不需要管理数据,但当展示型组件需要管理自身的UI状态时,例如控制组件内部弹框的显示与隐藏,是可以使用state的,这时的state属于UI state。既然大部分情况下展示型组件不需要state,应该优先考虑使用函数组件实现展示型组件。</p>
<p>容器型组件的职责是:组件数据如何工作。容器型组件需要知道如何获取子组件所需数据,以及这些数据的处理逻辑,并把数据和逻辑通过props提供给子组件使用。容器型组件一般是有状态组件,因为它们需要管理页面所需数据。</p>
<p>例如,下面的例子中,UserListContainer是一个容器型组件,它获取用户列表数据,然后把用户列表数据传递给展示型组件UserList,由UserList负责UI的展现。</p>
<pre><code>class UserListContainer extends React.Component{
constructor(props){
super(props);
this.state = {
users: []
}
}
componentDidMount() {
var that = this;
fetch('/path/to/user-api').then(function(response) {
response.json().then(function(data) {
that.setState({users: data})
});
});
}
render() {
return (
<UserList users={this.state.users} />
)
}
}
function UserList(props) {
return (
<div>
<ul className="user-list">
{props.users.map(function(user) {
return (
<li key={user.id}>
<span>{user.name}</span>
</li>
);
})}
</ul>
</div>
)
}</code></pre>
<p>展示型组件和容器型组件是可以互相嵌套的,展示型组件的子组件既可以包含展示型组件,也可以包含容器型组件,容器型组件也是如此。例如,当一个容器型组件承担的数据管理工作过于复杂时,可以在它的子组件中定义新的容器型组件,由新组件分担数据的管理。展示型组件和容器型组件的划分完全取决于组件所做的事情。</p>
<h4>总结</h4>
<p>通过上面的介绍,可以发现这三组概念有很多重叠部分。这三组概念都体现了关注点分离的思想:UI展现和数据逻辑的分离。函数组件、无状态组件和展示型组件主要关注UI展现,类组件、有状态组件和容器型组件主要关注数据逻辑。但由于它们的划分依据不同,它们并非完全等价的概念。它们之间的关联关系可以归纳为:函数组件一定是无状态组件,展示型组件一般是无状态组件;类组件既可以是有状态组件,又可以是无状态组件,容器型组件一般是有状态组件。</p>
<h4>下篇预告:</h4>
<p>React 深入系列3:State 和 Props</p>
<hr>
<p>新书推荐《React进阶之路》</p>
<p>作者:徐超</p>
<p>毕业于浙江大学,硕士,资深前端工程师,长期就职于能源物联网公司远景智能。8年软件开发经验,熟悉大前端技术,拥有丰富的Web前端和移动端开发经验,尤其对React技术栈和移动Hybrid开发技术有深入的理解和实践经验。</p>
<p><img src="https://user-gold-cdn.xitu.io/2018/4/4/1628e5ca2fd17f5b?w=297&h=387&f=png&s=70401" alt="" title=""></p>
<hr>
<p><img src="https://user-gold-cdn.xitu.io/2017/8/31/434a88713a614c84b89fb683b85d2c9e" alt="" title=""></p>
<hr>
<p>美团点评广告平台大前端团队招收20192020年前端实习生</p>
<p>有意者邮件:yao.zhou@meituan.com</p>
React 深入系列1:React 中的元素、组件、实例和节点
https://segmentfault.com/a/1190000014177333
2018-04-04T10:06:51+08:00
2018-04-04T10:06:51+08:00
iKcamp
https://segmentfault.com/u/ikcamp
4
<blockquote>文:徐超,《React进阶之路》作者<p>授权发布,转载请注明作者及出处</p>
</blockquote>
<hr>
<blockquote>React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。</blockquote>
<p>React 中的元素、组件、实例和节点,是React中关系密切的4个概念,也是很容易让React 初学者迷惑的4个概念。现在,老干部就来详细地介绍这4个概念,以及它们之间的联系和区别,满足喜欢咬文嚼字、刨根问底的同学(老干部就是其中一员)的好奇心。</p>
<h4>元素 (Element)</h4>
<p><strong>React 元素其实就是一个简单JavaScript对象,一个React 元素和界面上的一部分DOM对应,描述了这部分DOM的结构及渲染效果</strong>。一般我们通过JSX语法创建React 元素,例如:</p>
<pre><code>const element = <h1 className='greeting'>Hello, world</h1>;</code></pre>
<p>element是一个React 元素。在编译环节,JSX 语法会被编译成对React.createElement()的调用,从这个函数名上也可以看出,JSX语法返回的是一个React 元素。上面的例子编译后的结果为:</p>
<pre><code>const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);</code></pre>
<p>最终,element的值是类似下面的一个简单JavaScript对象:</p>
<pre><code>const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
}</code></pre>
<p>React 元素可以分为两类:DOM类型的元素和组件类型的元素。DOM类型的元素使用像h1、div、p等DOM节点创建React 元素,前面的例子就是一个DOM类型的元素;组件类型的元素使用React 组件创建React 元素,例如:</p>
<pre><code>const buttonElement = <Button color='red'>OK</Button>;</code></pre>
<p>buttonElement就是一个组件类型的元素,它的值是:</p>
<pre><code>const buttonElement = {
type: 'Button',
props: {
color: 'red',
children: 'OK'
}
}</code></pre>
<p>对于DOM类型的元素,因为和页面的DOM节点直接对应,所以React知道如何进行渲染。但是对于组件类型的元素,如buttonElement,React是无法直接知道应该把buttonElement渲染成哪种结构的页面DOM,这时就需要组件自身提供React能够识别的DOM节点信息,具体实现方式在介绍组件时会详细介绍。</p>
<p>有了React 元素,我们应该如何使用它呢?其实,绝大多数情况下,我们都不会直接使用React 元素,React 内部会自动根据React 元素,渲染出最终的页面DOM。更确切地说,React元素描述的是React虚拟DOM的结构,React会根据虚拟DOM渲染出页面的真实DOM。</p>
<h4>组件 (Component)</h4>
<p>React 组件,应该是大家最熟悉的React中的概念。React通过组件的思想,将界面拆分成一个个可以复用的模块,每一个模块就是一个React 组件。一个React 应用由若干组件组合而成,一个复杂组件也可以由若干简单组件组合而成。</p>
<p>React组件和React元素关系密切,<strong>React组件最核心的作用是返回React元素</strong>。这里你也许会有疑问:React元素不应该是由React.createElement() 返回的吗?但React.createElement()的调用本身也是需要有“人”负责的,React组件正是这个“责任人”。React组件负责调用React.createElement(),返回React元素,供React内部将其渲染成最终的页面DOM。</p>
<p>既然组件的核心作用是返回React元素,那么最简单的组件就是一个返回React元素的函数:</p>
<pre><code>function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}</code></pre>
<p>Welcome是一个用函数定义的组件。如果使用类(class)定义组件,返回React元素的工作具体就由组件的render方法承担,例如:</p>
<pre><code>class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}</code></pre>
<p>其实,使用类定义的组件,render方法是唯一必需的方法,其他组件的生命周期方法都只不过是为render服务而已,都不是必需的。</p>
<p>现在来考虑下面这个例子:</p>
<pre><code>class Home extends React.Component {
render() {
return (
<div>
<Welcome name='老干部' />
<p>Anything you like</p>
</div>
)
}
}</code></pre>
<p>Home 组件使用了Welcome组件,返回的React元素为:</p>
<pre><code>{
type: 'div',
props: {
children: [
{
type: 'Welcome',
props: {
name: '老干部'
}
},
{
type: 'p',
props: {
children: 'Anything you like'
}
},
]
}
}</code></pre>
<p>对于这个结构,React 知道如何渲染type = 'div' 和 type = 'p' 的节点,但不知道如何渲染type='Welcome'的节点,当React 发现Welcome 是一个React 组件时(判断依据是Welcome首字母为大写),会根据Welcome组件返回的React 元素决定如何渲染Welcome节点。Welcome组件返回的React 元素为:</p>
<pre><code>{
type: 'h1',
props: {
children: 'Hello, 老干部'
}
}</code></pre>
<p>这个结构中只包含DOM节点,React是知道如何渲染的。如果这个结构中还包含其他组件节点,React 会重复上面的过程,继续解析对应组件返回的React 元素,直到返回的React 元素中只包含DOM节点为止。这样的递归过程,让React 获取到页面的完整DOM结构信息,渲染的工作自然就水到渠成了。</p>
<p>另外,如果仔细思考的话,可以发现,<strong>React 组件的复用,本质上是为了复用这个组件返回的React 元素,React 元素是React 应用的最基础组成单位</strong>。</p>
<h4>实例 (Instance)</h4>
<p>这里的实例特指React组件的实例。React 组件是一个函数或类,实际工作时,发挥作用的是React 组件的实例对象。只有组件实例化后,每一个组件实例才有了自己的props和state,才持有对它的DOM节点和子组件实例的引用。在传统的面向对象的开发方式中,实例化的工作是由开发者自己手动完成的,但在React中,组件的实例化工作是由React自动完成的,组件实例也是直接由React管理的。换句话说,开发者完全不必关心组件实例的创建、更新和销毁。</p>
<h4>节点 (Node)</h4>
<p>在使用PropTypes校验组件属性时,有这样一种类型:</p>
<pre><code>MyComponent.propTypes = {
optionalNode: PropTypes.node,
}</code></pre>
<p>PropTypes.node又是什么类型呢?这表明optionalNode是一个React 节点。React 节点是指可以被React渲染的数据类型,包括数字、字符串、React 元素,或者是一个包含这些类型数据的数组。例如:</p>
<pre><code>// 数字类型的节点
function MyComponent(props) {
return 1;
}
// 字符串类型的节点
function MyComponent(props) {
return 'MyComponent';
}
// React元素类型的节点
function MyComponent(props) {
return <div>React Element</div>;
}
// 数组类型的节点,数组的元素只能是其他合法的React节点
function MyComponent(props) {
const element = <div>React Element</div>;
const arr = [1, 'MyComponent', element];
return arr;
}
// 错误,不是合法的React节点
function MyComponent(props) {
const obj = { a : 1}
return obj;
}</code></pre>
<p>最后总结一下,React 元素和组件的概念最重要,也最容易混淆;React 组件实例的概念大家了解即可,几乎使用不到;React 节点有一定使用场景,但看过本文后应该也就不存在理解问题了。</p>
<h4>下篇预告:</h4>
<p>React 深入系列2:组件分类</p>
<hr>
<p>新书推荐《React进阶之路》</p>
<p>作者:徐超</p>
<p>毕业于浙江大学,硕士,资深前端工程师,长期就职于能源物联网公司远景智能。8年软件开发经验,熟悉大前端技术,拥有丰富的Web前端和移动端开发经验,尤其对React技术栈和移动Hybrid开发技术有深入的理解和实践经验。</p>
<p><img src="/img/remote/1460000014177338?w=297&h=387" alt="" title=""></p>
从Nest到Nesk -- 模块化Node框架的实践
https://segmentfault.com/a/1190000014151375
2018-04-03T10:19:08+08:00
2018-04-03T10:19:08+08:00
iKcamp
https://segmentfault.com/u/ikcamp
5
<blockquote>文: 达孚(沪江Web前端架构师)<p>本文原创,转至沪江技术</p>
</blockquote>
<p>首先上一下项目地址(:>):</p>
<p>Nest:<a href="https://link.segmentfault.com/?enc=NmcNhb%2BOpg5vgmSoolA2hA%3D%3D.q9k2zkp1GINXRxKon6aIoLIs6ZTjOmRiI05i2s759LA%3D" rel="nofollow">https://github.com/nestjs/nest</a></p>
<p>Nesk:<a href="https://link.segmentfault.com/?enc=CkOwdxAwiKC7WnPL5YOy6w%3D%3D.R%2B0LPSdigsVEta5KB3n7eFjlvtrZS%2FZmS0PI6AO6xOF8qE3Sh6t1UaTsaHerj%2FsL" rel="nofollow">https://github.com/kyoko-df/nesk</a></p>
<h2>Nest初认识</h2>
<p>Nest是一个深受angular激发的基于express的node框架,按照官网说明是一个旨在提供一个开箱即用的应用程序体系结构,允许轻松创建高度可测试,可扩展,松散耦合且易于维护的应用程序。</p>
<p>在设计层面虽然说是深受angular激发,但其实从后端开发角度来说类似于大家熟悉的Java Spring架构,使用了大量切面编程技巧,再通过装饰器的结合完全了关注上的分离。同时使用了Typescript(也支持Javascript)为主要开发语言,更保证了整个后端系统的健壮性。</p>
<h2>强大的Nest架构</h2>
<p>那首先为什么需要Nest框架,我们从去年开始大规模使用Node来替代原有的后端View层开发,给予了前端开发除了SPA以外的前后端分离方式。早期Node层的工作很简单-渲染页面代理接口,但在渐渐使用中大家会给Node层更多的寄托,尤其是一些内部项目中,你让后端还要将一些现有的SOA接口进行包装,对方往往是不愿意的。那么我们势必要在Node层承接更多的业务,包括不限于对数据的组合包装,对请求的权限校验,对请求数据的validate等等,早期我们的框架是最传统的MVC架构,但是我们翻阅业务代码,往往最后变成复杂且很难维护的Controller层代码(从权限校验到页面渲染一把撸到底:))。</p>
<p>那么我们现在看看Nest可以做什么?从一个最简单的官方例子开始看:</p>
<pre><code class="ts">async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();</code></pre>
<p>这里就启动了一个nest实例,先不看这个ValidationPipe,看ApplicationModule的内容:</p>
<pre><code class="ts">@Module({
imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewaresConsumer): void {
consumer
.apply(LoggerMiddleware)
.with('ApplicationModule')
.forRoutes(CatsController);
}
}</code></pre>
<pre><code class="ts">@Module({
controllers: [CatsController],
components: [CatsService],
})
export class CatsModule {}</code></pre>
<p>这里看到nest的第一层入口module,也就是模块化开发的根本,所有的controller,component等等都可以根据业务切分到某个模块,然后模块之间还可以嵌套,成为一个完整的体系,借用张nest官方的图:</p>
<p><img src="/img/remote/1460000014151380" alt="" title=""></p>
<p>在nest中的component概念其实一切可以注入的对象,对于依赖注入这个概念在此不做深入解释,可以理解为开发者不需要实例化类,框架会进行实例化且保存为单例供使用。</p>
<pre><code class="ts">@Controller('cats')
@UseGuards(RolesGuard)
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Get(':id')
findOne(
@Param('id', new ParseIntPipe())
id,
): Promise<Cat> {
return this.catsService.findOne(id);
}
}</code></pre>
<p>Controller的代码非常精简,很多重复的工作都通过guards和interceptors解决,第一个装饰器Controller可以接受一个字符串参数,即为路由参数,也就是这个Controller会负责/cats路由下的所有处理。首先RolesGuard会进行权限校验,这个校验是自己实现的,大致结构如下:</p>
<pre><code class="ts">@Guard()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(request, context: ExecutionContext): boolean {
const { parent, handler } = context;
const roles = this.reflector.get<string[]>('roles', handler);
if (!roles) {
return true;
}
// 自行实现
}
}</code></pre>
<p>context可以获取controller的相关信息,再通过反射拿到handler上是否有定义roles的元信息,如果有就可以在逻辑里根据自己实现的auth方法或者用户类型来决定是否让用户访问相关handler。</p>
<p>interceptors即拦截器,它可以:</p>
<ul>
<li>在方法执行之前/之后绑定额外的逻辑</li>
<li>转换从函数返回的结果</li>
<li>转换从函数抛出的异常</li>
<li>根据所选条件完全重写函数 (例如, 缓存目的)</li>
</ul>
<p>本示例有两个拦截器一个用来记录函数执行的时间,另一个对结果进行一层包装,这两个需求都是开发中很常见的需求,而且拦截器会提供一个rxjs的观察者流来处理函数返回,支持异步函数,我们可以通过map()来mutate这个流的结果,可以通过do运算符来观察函数观察序列的执行状况,另外可以通过不返回流的方式,从而阻止函数的执行,LoggingInterceptor例子如下:</p>
<pre><code class="ts">@Interceptor()
export class LoggingInterceptor implements NestInterceptor {
intercept(dataOrRequest, context: ExecutionContext, stream$: Observable<any>): Observable<any> {
console.log('Before...');
const now = Date.now();
return stream$.do(
() => console.log(`After... ${Date.now() - now}ms`),
);
}
}</code></pre>
<p>回到最初的ValidationPipe,它是一个强大的校验工具,我们看到前面的controller代码中插入操作中有一个CreateCatDto,dto是一种数据传输对象,一个dto可以这样定义:</p>
<pre><code class="ts">export class CreateCatDto {
@IsString() readonly name: string;
@IsInt() readonly age: number;
@IsString() readonly breed: string;
}</code></pre>
<p>然后ValidationPipe会检查body是否符合这个dto,如果不符合就会就会执行你在pipe中设置的处理方案。具体是如何实现的可以再写一篇文章了,所以我推荐你看<a href="https://link.segmentfault.com/?enc=ry8T9OKOG49cMnfQtRX8CQ%3D%3D.g%2FXzZSuO3FgrFDHGAn%2B3nyXeDB2QNAGrfGZJqmHyqqU%3D" rel="nofollow">nest中文指南</a>(顺便感谢翻译的同学们)</p>
<p>示例的完整代码可以看<a href="https://link.segmentfault.com/?enc=0%2BOZprp3nqysoFZRfAwWaA%3D%3D.NpurO78e20UjZmCXWzir%2BdPzqdK1G4SQXe3FwlBp48%2FQHy%2F%2B6pAHP4i9C6exICCBsybl59yCZ7QCoq5gPQ989A%3D%3D" rel="nofollow">01-cats-app</a></p>
<p>也就是说业务团队中的熟练工或者架构师可以开发大量的模块,中间件,异常过滤器,管道,看守器,拦截器等等,而不太熟练的开发者只需要完成controller的开发,在controller上像搭积木般使用这些设施,即完成了对业务的完整搭建。</p>
<h2>Nesk-一个落地方案的尝试</h2>
<p>虽然我个人很喜欢Nest,但是我们公司已经有一套基于koa2的成熟框架Aconite,而Nest是基于express的,查看了下Nest的源码,对express有一定的依赖,但是koa2和express在都支持async语法后,差异属于可控范围下。另外nest接受一个express的实例,在nesk中我们只需要调整为koa实例,那么也可以是继承于koa的任何项目实例,我们的框架在2.0版本也是一个在koa上继承下来的node框架,基于此,我们只需要一个简单的adapter层就可以无缝接入Aconite到nesk中,这样减少了nesk和内部服务的捆绑,而将所有的公共内部服务整合保留在Aconite中。Nest对于我们来说只是一个更完美的开发范式,不承接任何公共模块。</p>
<p>所以我们需要的工作可以简单总结为:</p>
<ol>
<li>支持Koa</li>
<li>适配Aconite</li>
</ol>
<p>支持Koa我们在Nest的基础上做了一些小改动完成了Nesk来兼容Koa体系。我们只需要完成Nesk和Aconite中间的Adapter层,就可以完成Nesk的落地,最后启动处的代码变成:</p>
<pre><code class="ts">import { NeskFactory } from '@neskjs/core';
import { NeskAconite } from '@hujiang/nesk-aconite';
import { ApplicationModule } from './app.module';
import { config } from './common/config';
import { middwares } from './common/middlware';
async function bootstrap() {
const server = new NeskAconite({
projectRoot: __dirname,
middlewares,
config
});
const app = await NeskFactory.create(ApplicationModule, server);
await app.listen(config.port);
}</code></pre>
<p>最后Nest有很多@nest scope下的包,方便一些工具接入nest,如果他们与express没有关系,我们其实是可以直接使用的。但是包内部往往依赖@nest/common或者@nesk/core,这里可以使用module-alias,进行一个重指向(你可以尝试下graphql的例子):</p>
<pre><code>"_moduleAliases": {
"@nestjs/common": "node_modules/@neskjs/common",
"@nestjs/core": "node_modules/@neskjs/core"
}</code></pre>
<p>Nesk的地址<a href="https://link.segmentfault.com/?enc=sIkgGFt4Ku0l4mfuKuVwJA%3D%3D.hUtwQiK7F0MVnShIpe7OnerCR9MjB0UF2OMRVC9VAbyN%2BkX7Oz6C9lZH7Rj%2BiJ1k" rel="nofollow">Nesk</a>,我们对Nesk做了基本流程测试目前覆盖了common和core,其它的在等待改进,欢迎一切愿意一起改动的开发者。</p>
<h2>不足与期待</h2>
<p>其实从一个更好的方面来说,我们应当允许nest接受不同的底层框架,即既可以使用express,也可以使用koa,通过一个adapter层抹平差异。不过这一块的改造成本会大一些。</p>
<p>另一方面nest有一些本身的不足,在依赖注入上,还是选择了ReflectiveInjector,而Angular已经开始使用了StaticInjector,理论上StaticInjector减少了对Map层级的查找,有更好的性能,这也是我们决定分叉出一个nesk的原因,可以做一些更大胆的内部代码修改。另外angular的依赖注入更强大,有例如useFactory和deps等方便测试替换的功能,是需要nest补充的.</p>
<p>最后所有的基于Koa的框架都会问到一个问题,能不能兼容eggjs(:)),其实无论是Nest还是Nesk都是一个强制开发规范的框架,只要eggjs还建立在koa的基础上,就可以完成集成,只是eggjs在启动层面的改动较大,而且开发范式和nest差异比较多,两者的融合并没有显著的优势。</p>
<p>总之Node作为一个比较灵活的后端开发方式,每个人心中都有自己觉得合适的开发范式,如果你喜欢这种方式,不妨尝试下Nest或者Nesk。</p>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
如何在原生微信小程序中实现数据双向绑定
https://segmentfault.com/a/1190000013802890
2018-03-17T19:36:38+08:00
2018-03-17T19:36:38+08:00
iKcamp
https://segmentfault.com/u/ikcamp
3
<blockquote>官网:<a href="https://link.segmentfault.com/?enc=DrMrX7emVqEl5ZesST6ALg%3D%3D.R2kTuAJlUsK%2Fhe9ukt6S4CFGapKssKXIAg%2BXOznEnlI0%2BycNHnoHL3stkNEeKl7A" rel="nofollow">https://qiu8310.github.io/minapp/</a><p>作者:<a href="https://link.segmentfault.com/?enc=g7x0GPcnmmRqB37dnQfP6A%3D%3D.Zog4ipVuGuZpU%2FvI7Rs5LbLhUqTpOjypG%2F%2BgoQKEZWE%3D" rel="nofollow">Mora</a></p>
</blockquote>
<p>在原生小程序开发中,数据流是单向的,无法双向绑定,但是要实现双向绑定的功能还是蛮简单的!</p>
<blockquote>下文要讲的是小程序框架 <a href="https://link.segmentfault.com/?enc=03debokjluhAkgo91m3ZnQ%3D%3D.YYAjGTfQl8YMerpIiC8M66YC9TNHpZR3pXeenM4nfxadv%2FRxPJmezZjnJS42sfCt" rel="nofollow">minapp</a> 中实现双向绑定的原理,在 <a href="https://link.segmentfault.com/?enc=ANxmJZWJsZfQIkYCP5WoaQ%3D%3D.S1nnXdt3DZxEy32DJRnpIkcdtLlRvj1vvbNOhiXzyzfMRK2AaAxJHgRayLgL9qQ8" rel="nofollow">minapp</a> 中,你只需要在 wxml 模板中给组件的属性名后加上 <code>.sync</code> 就可以实现双向绑定。下面为了解释其原理,过程可能会说的稍微复杂些,但其实 <a href="https://link.segmentfault.com/?enc=ANZ2Ql%2FABM03tLpdH30aRQ%3D%3D.4E3h7IhtIZ10yiWyouXiJjyusssczWEKP%2F0uuDfOQT6z%2FyBuS6R1gqiAx3zhaYCx" rel="nofollow">minapp</a> 框架已经处理了那些繁杂的细节!</blockquote>
<p>首先,<strong>要使数据双向绑定,应该避免过多的数据源</strong>。<br>在数据从上到下自然流动的情况下,如果每个组件中都维护它们自己的数据,而又要保持它们数据值的一致,这虽然可以做到,但实现过程并不会简单。<br>但是也没必要说为了有一个统一的数据源就使用 <strong>mobx</strong> 或 <strong>redux</strong> 来全局管理数据,这就有点杀鸡用牛刀的感觉了。<br>由于双向绑定只存在于父子组件之间,而数据又是从父到子传递的,所以可以优先使用父组件中的数据为数据源,<br>子组件每次更新数据并不更新它自己内部的数据,而是通过事件机制触发父组件更新它的数据,而父组件更新数据后又会将更新的数据自然地传给子组件,<br>由此达到数据的双向流动!</p>
<p><img src="/img/remote/1460000013802895?w=320&h=320" alt="data-stream" title="data-stream"></p>
<p>并不是所有数据都需要双向绑定,也并不是所有数据都是对外的,子组件还可以有它自己的一个内部数据。所以这就涉及到我们要说的第二个问题:<strong>区分哪些数据需要双向绑定,哪些数据又需要子组件自己维护</strong>。</p>
<p>用过 <strong>vue</strong> 的应该都知道,在 vue 中要实现双向绑定,需要在模板中做特殊处理。比如要将父组件的 <code>parentAttr</code> 双向绑定到子组件的 <code>childAttr</code> 上,需要在父组件的模板中这样写:</p>
<pre><code class="html"><child childAttr.sync="parentAttr" /></code></pre>
<p>但是小程序并没有这样的简单的语法,小程序的 wxml 语言的属性名中甚至都不允许出现 " . " 这样的字符。回到我们的问题上来,<strong>子组件需要知道哪些属性需要双向绑定,哪些属性需要自己维护</strong>,<br>给模板加个字段(<code>syncAttrMap</code>)专门来告诉子组件需要双向绑定的数据集合不就行了么。如,可以将上面的示例写成微信小程序支持的写法:</p>
<pre><code class="html"><child childAttr="{{parentAttr}}" syncAttrMap="childAttr=parentAttr" />
<!--
如果同时存在多个双向绑定和不需要双向绑定的属性时,可以写成下面这样:
p1, p2 分别双向绑定到子组件的 c1, c2,而 p3 单向绑定到 c3 上
-->
<child c1="{{p1}}" c2="{{p2}}" c3="{{p3}}" syncAttrMap="c1=p1&c2=p2" /></code></pre>
<p>接着,就需要处理<strong>子组件数据更新的问题</strong>了,在子组件中有两部分数据,一部分是内部数据,另一部分是父组件中的数据,<br>子组件可以通过读取属性 <code>syncAttrMap</code> 来得到哪些数据是内部的数据,哪些数据是父组件的数据,并且可以知道对应<br>的父组件中的数据的键名是什么。由于原生的组件方法 <code>setData</code> 不会管你是内部数据,还是父组件中的数据,只要<br>你调用它去更新数据,它只会更新内部的数据。所以需要另外实现一个新的方法,来自动判断数据源,如果是内部数据,<br>则直接调用 <code>setData</code> ;如果是双向绑定中的父组件数据,则可以触发一个事件去通知父组件去更新对应的值。</p>
<p>所以根据上面的描述,父组件需要有个监听函数,子组件需要有个智能的 <code>setData</code> 函数。不防将父组件的监听函数<br>命名为 <code>onSyncAttrUpdate</code>,将子组件的智能 <code>setData</code> 函数命名为 <code>setDataSmart</code>,则可以有如下代码:</p>
<pre><code class="js">// 父组件
Component({
methods: {
onSyncAttrUpdate(e) {
this.setData(e.detail) // 子组件传来的需要更新的数据
}
}
})
</code></pre>
<pre><code class="html"><!-- 父组件的模板 -->
<child childAttr="{{parentAttr}}" syncAttrMap="childAttr=parentAttr" bind:syncAttrUpdate="onSyncAttrUpdate" /></code></pre>
<pre><code class="js">// 子组件
Component({
properties: {
childAttr: String,
syncAttrMap: String
},
methods: {
// 子组件更新数据时,只要调用此方法即可,而不是 `setData`
setDataSmart(data) {
// splitDataBySyncAttrMap 函数的实现过程就不说了,只是将对象拆分,大家应该都能实现
let {parentData, innerData} = splitDataBySyncAttrMap(data, this.data.syncAttrMap)
// 内部数据使用 setData 更新
if (Object.keys(innerData).length) {
this.setData(innerData) // setData 中还支持 callback 的回调,为了简化代码,这里不讨论
}
// 双向绑定的父组件数据触发事件让父组件自己去更新
if (Object.keys(parentData).length) {
this.triggerEvent('syncAttrUpdate', parentData)
}
}
}
})
</code></pre>
<p>到此,一个简单的双向绑定功能就完成了。但是由于子组件也有可能包含其它组件,也就是说子组件也可以是父组件,而父组件同样也<br>可以是子组件。所以上面的 <code>onSyncAttrUpdate</code> <code>setDataSmart</code> 函数需要在每个组件中都实现,所以不防<br>定义一个公共对象 <code>BaseComponent</code> 来实现上面的所有功能,如:</p>
<pre><code class="js">// BaseComponent
const BaseComponent = {
properties: {
syncAttrMap: String
},
methods: {
setDataSmart() {
// ...
},
onSyncAttrUpdate() {
// ...
}
}
}</code></pre>
<p>然后将 BaseComponent minin 到每个组件的对象上去就可以了;另外小程序中还有一个特殊的组件:<strong>Page</strong>,虽然 Page 和 Component 结构是两样的,<br>但它也应该算是一个组件,不过它一定是父组件,不可能是别的组件的子组件,所以还需要将 <code>onSyncAttrUpdate</code> 方法写了所有的 Page 定义中。<br>所有这些就是 <a href="https://link.segmentfault.com/?enc=I2CCAn0sKVl%2BLcIysW397Q%3D%3D.46CCDA3SpegBesldEVO%2BSzwVFguvJ4e1aMe34of9j1Zt8lsvSauVbE2wb25eU3lG" rel="nofollow">minapp</a> 的双向绑定的基本原理了。</p>
<p>等等,最后还有一件事:<strong>wxml 模板</strong>,不能让用户每次写双向绑定的时候都要写那么复杂语句吧?当然不用,<a href="https://link.segmentfault.com/?enc=5rQkBlZksenidxWIUiageQ%3D%3D.G%2FTk5EoR9ya1bORS%2B6ZCHVvo1pw%2BQNns3QCL9e4qmU%2FaY108yNY6%2BJFkxeRi1QjE" rel="nofollow">minapp</a> 在编译时,会将模板做个简单的转化:</p>
<pre><code class="html"><child childAttr.sync="parentAttr" />
<!-- 由于属性名 syncAttrMap 是固定的,所以完全可以通过编译手段,将上面的模板转成下面这个模板 -->
<child childAttr="{{parentAttr}}" syncAttrMap="childAttr=parentAttr" /></code></pre>
<p>谢谢,文章到此结束,欢迎关注 <a href="https://link.segmentfault.com/?enc=F0l58JcCA1cNw5n09FX%2Bzw%3D%3D.TlyBCEoeYN8JV%2B0ksrYCcbfz%2BGd160N%2BUh1xeOCQGAaG3h0V7KdPabhBiWatckEr" rel="nofollow">minapp:重新定义微信小程序的开发</a></p>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
追溯 React Hot Loader 的实现
https://segmentfault.com/a/1190000013571760
2018-03-07T10:53:07+08:00
2018-03-07T10:53:07+08:00
iKcamp
https://segmentfault.com/u/ikcamp
6
<blockquote>文:萝卜(沪江金融前端开发工程师)<p>本文原创,转载请注明作者及出处</p>
</blockquote>
<p>如果你使用 React ,你可以在各个工程里面看到 <a href="https://link.segmentfault.com/?enc=RghDFncWnMV6UzHRCwEAIg%3D%3D.Y7FogVwPn45QzZuZwKcefTcBw3NkNVej4l8CCUVD67hOvTe6kHG%2FEwCJtKwBIvnO" rel="nofollow">Dan Abramov</a> 的身影。他于 2015 年加入 facebook,是 React Hot Loader 、React Transform、redux-thunk、redux-devtools 等等的开发者。同样也是 React、Redux、Create-React-App 的联合开发者。从他的签名 <em>Building tools for humans.</em> 或许表明了他想打造高效的开发环境以及调试过程。</p>
<p>作为 Dan 的小迷妹,如他说 <em>is curious where the magic comes from</em>。这篇文章会带你们去了解 React Hot Loader 的由来,它实现的原理,以及在实现中遇到的问题对应的解决方法。也许你认为这篇文章太过于底层,对日常的业务并没有帮助,但希望你和我一样能通过了解一个实现得到乐趣,以及收获一些思路。</p>
<h2>首先,React Hot Loader 的产生</h2>
<p>Dan 在自己的文章里面说到。React Hot Loader 起源一个来自 <a href="https://link.segmentfault.com/?enc=86eSCtUtQY5Bto%2BkDZKUMg%3D%3D.52%2FpUSGAGe45KzYKmhk9YF5pbI%2FpKLQQ4iXbBadasZ%2BLRXVlu%2B4a7YnAm1W8oAO2v23LuKlZYitJjuucotZ4Kw6N1MnrHVNbpLd8BRzuV40xsnkMMt6EMdW3QQDTdwIn" rel="nofollow">stackoverflow 上的一个问题</a> —— <strong>what exactly is hot module replacement in webpack</strong>,这个问题解释了 webpack 的 hot module replacement(下面简称 HMR)到底是什么,以及我们可以利用它做什么,Dan 当时想到也 React 可以和 webpack hot module 以一种有趣的方式结合在一起。</p>
<p>于是他在 Twitter 上录制了一个简单的视频(请看下面),事实上视频中的实现依赖于它在 React 源代码里面插入了很多自己的全局变量。他本没指望到这个视频能带来多大的关注,但结果是他收到了很多点赞,并且粉丝狂增,他意识到必须以一个真正的工程去实现。</p>
<p><img src="/img/remote/1460000013571765?w=300&h=148" alt="上传大小有限制= =" title="上传大小有限制= ="></p>
<p><a href="https://link.segmentfault.com/?enc=j7IOzYFXLt3qTKBHuM80Qw%3D%3D.wyyPvFY4C2GShO4fqChKjc6mkrylrV72Blb9zfZTZ5xkGz2Yn11NNKaQo8q1n7R%2F" rel="nofollow">大图请戳</a></p>
<h2>初步尝试, 直接使用 HMR</h2>
<p>HMR 是属于 webpack 范畴内的实现,你可以在 <a href="https://link.segmentfault.com/?enc=CJYouy%2B0d5v56cev7%2F13VA%3D%3D.KLfeUdSQFhVwwng0LPY5BUW7CYdRLY9VpW%2F5DtYBaQUxHcVfNaC5OYgBj7iW4BUTf1bX5zbKDFOPnNjbCa31VA%3D%3D" rel="nofollow">webpack 的官方文档</a> 看到如何开启它以及它提供的接口。如果你有印象,你会记得使用它需要<br> 在 webpack config 或者 webpack-dev-server cli 里面指定开启 hot reloading 模式,并且在你的代码里写上 <code>module.hot.accept(xxx)</code>。但 HMR 到底是什么?我们可以用一句话总结:当一个 import 进来的模块发生了变化,HMR 提供了一个接口让我们使用 callback 回调去做一些事情。</p>
<p>一个使用 HMR 实现自动刷新的 React App 像下面这样:</p>
<pre><code class="javascript">// index.js
var App = require('./App')
var React = require('react')
var ReactDOM = require('react-dom')
// 像通常一样 render Root Element
var rootEl = document.getElementById('root')
ReactDOM.render(<App />, rootEl)
// 我们是不是在 dev 环境 ?
if (module.hot) {
// 当 App.js 更新了
module.hot.accept('./App', function () {
// require 进来更新的 App.js 重新render
var NextApp = require('./App')
ReactDOM.render(<NextApp />, rootEl)
})
}</code></pre>
<p><strong>请注意,这个实现没有使用 React Hot Loader 或者 React Transform 或者任何其他的,这仅仅是 webpack 的HMR 的 api</strong>。而这里的 callback 回调函数当然是 re-render 我们的 app。</p>
<p>得益于 HMR API 的设计,在嵌套的组件也能实现更新。如果一个模块没有指明如何去更新自己,那么引入这个模块的另一个模块也会被包含在热更新的 bundle 里,这些更新会”冒泡“,直到某个 import 它们的模块 "接收" 更新。如果有些模块最终没有被"接受",那么热更新失败,控制台会打印出警告。为了“接受”更新,你只需要调用 <code>module.hot.accept('./name', callback) </code> 。</p>
<p>因为我们在 index.js 里的接受了 App.js 的更新 ,这使得我们隐性的接受了所有从 App.js 引入的所有模块(component)的更新。打个比方,假如我编辑了 Button.js 组件,而它被 UserProfile.js 以及 Navbar.js import, 而这两个模块都被 App.js import 引入了。因为 index.js import 了 App.js,并且它包含了 <code>module.hot.accept('./App', callback)</code> ,Webpack 会自动产生一个包含以上所有文件的 “updated bundle”, 并且运行我们提供的 callback。</p>
<p>你以为 hot reloading 就到此为止了吗,当然远远不够 ? 。</p>
<h2>问题:组件的 state 和 DOM 被销毁。</h2>
<p>当我们的 App.js 更新,实际上是有个新的 App.js 用 script 标签注入到 html, 并且重新执行了一次。此时新生成的 component 和之前的是一个组件的不同版本,它们是不同版本的同一个组件,但是 NextApp !== App。</p>
<p>如果你了解 React ,你会知道当下一个 component 的 type 和之前的不一样,它会 unmount 之前那个。这就是为什么 state 和 DOM 会被销毁。</p>
<p>在解决 state 保留的问题上,有人认为如果工程依赖一个单一的 state 树,那没有必要费大精力去保留组件自身的 state。因为在这种类型的 app 里面我们关注的更多的是全局的这个 state 树,而去保存这个全局的 state 树是很容易做到的,比如你可以把它保存到 localstorage里面,当 store 初始化的时候你去读取它,这样的话连刷新都不会丢失状态。</p>
<p>Dan 接受了这个意见,并且在自己的文章里面总结,如果你使用 redux ,并且主要的状态保存在 redux 的 store 上,这时也许你不需要使用 React-Hot-Loader。</p>
<p>但他并没有因为仅仅 <strong>有些人</strong> 可能不需要用到而放弃了 React-Hot-Loader。这才有了下文 ? 。</p>
<h2>如何解决 state 和 DOM 销毁问题</h2>
<p>当你从上面了解了为什么 DOM 和 state 会丢失,也许你就会 和 Dan 一样想到了两种方法。</p>
<ol>
<li>找到一种方式把 React 的实例和 Dom nodes 以及 state 分离,创建一个新组件的新实例,然后用一种方式把它递归地和现有的 Dom 和 state 结合在一起。</li>
<li>另外一种,代理 component 的 type,这样能让 React 认为 type 没有变。事实上每次 hot update 实现引用的是新的 component type。</li>
</ol>
<p>第一种方式看上去好一点,但是 React 暂时没有提供可以分离(聚合)state 以及不销毁 DOM、不运行生命周期去替换一个实例。即使深入到使用 React 的私有 API 达到这个目的,采用第一个方案任然面临着一些细微的问题。</p>
<p>比如,React components 经常 在 componentDidmount 时候订阅 Flux stores 或者其他数据源。即使我们做到不销毁 Dom 以及 state, 偷偷地用一个新的实例替换旧的实例,旧的实例仍然会继续保持订阅,而新的实例将不会订阅。</p>
<p>结论是,如果 React 的 state 的订阅是申明式,并且独立于生命周期之外,或者 React 没有那么依赖 class 和 instance, 第一个方法才可行。这些也许会出现在以后的 React 版本里,但是现在并没有。</p>
<p>于是 Dan 采用了第二种,这也是之后的 React Hot Loader 和 React Transform 所使用的到技巧。</p>
<p>为此,Dan 建立了一个独立的工程(react-proxy)去做 proxy,你可以在<a href="https://link.segmentfault.com/?enc=Ai9IOh2aQyT5mBskc0AOFg%3D%3D.G0nJLEY87XHCg4JGJeJhGo2l%2FuM%2FKp3QgZ0d%2BVMGtH30ejQhMZ3GaCoYghq3YVe%2B" rel="nofollow">这里</a> 看到它。create-proxy 只是一个底层的工程,它不依赖 wepback 也不依赖 babel。React Hot Loader 和 React Transform 依赖它,它把 React Component 包装到一个个 proxy 里面,这些 “proxy” 只是些 class, 它们表现的就像你自己的class,但是提供你一些钩子让你能对 class 注入新的实现方法,这样相当于让一个已经存在的实例表现的像新的 class,从而不会销毁 state 和 DOM。</p>
<h2>在哪里 proxy ?</h2>
<p>Dan 首先所做的是在 wepback 的 loader 里面 proxy。</p>
<blockquote>补充,很多人认为 React Hot Loader 不是一个 “loader”,因为它只是实现 hot reloading 的。这是一个普遍的误解?。</blockquote>
<p>之所以叫 “loader” 是因为 webpack 是这么称呼它,而其他 bundlers(打包器)称呼为 “transform”。打个比方,json-loader 把JSON 文件 “transform” 成 javascript modules,style-loader 把 CSS 文件 “transform” 成 js code 然后把它们以 stylesheets 的形式注入。</p>
<p>而关于 React Hot Loader 你可以在<a href="https://link.segmentfault.com/?enc=CsQcwGyxa%2Fv%2BjUI789PVOA%3D%3D.9ReSYoY9SWoeazFrLOIE3LGbeH7xHX0ZixYMPouV6ugolTMhP9wEqNDlXsCI2k1GpQqz%2FnGVVktDV%2BXbPkJVRNuTApCvdR4217DD%2Fx6pWP8f9VSucVN8VbEpjvdkMDfrumDYmv4BxeTCus0Zze8C%2FA%3D%3D" rel="nofollow">这里</a> 看到,在编译的时候它通过 export 找到 component,并且“静默” 的包裹它,然后 export 一个代理的 component 取而代之原来的。</p>
<p>通过 module.exports 去寻找 components 开始听上去是合理的。开发者们经常把每个组件单独储存在一个文件,自然而然组件将会被exported。然而,随着时间变化,React 社区发生了一些变化,采取了一些新的写法或者思想,这导致了一些问题。</p>
<ul>
<li>随着高阶组件变得流行,大家开始 export 出来的是一个高阶组件,而不是实际上自己写的组件。 结果导致, React Hot Loader 没有“发现” module.export 里面包裹的组件,所以没有给它们创建 proxy。它们的 DOM 以及 local state 将会被在这些文件每次修改后销毁。这尤其影响像 <a href="https://link.segmentfault.com/?enc=slI60ZOC5LWZKg6ER1MGKg%3D%3D.YprQZI8j6RSdQgJQaU4yWrJAWt0dORg0Qd6jwJmcOrKPdR0X2Mz%2FSTUTVzkweOXl" rel="nofollow">React JSS</a> 一样利用高阶组件实现样式。</li>
<li>React 0.14 引进了函数式组件,并且鼓励在一个文件里面最小化拆分组件。即使React Hot Loader 能检测到导出的组件,它也“看”不到那些未被导出的本地的component。所以这些component 将不会包裹在proxy里面,所以会导致在它以及它下面的子树丢失 DOM 以及 state。</li>
</ul>
<p>这显然是使得从 <code>module.exports</code> 去找组件是不可靠的。</p>
<h2>React Transform 的出现</h2>
<p>除了上面提到的从 <code>module.exports</code> 不可靠之外,第一版的 React-Hot-Loader 还存在一些其他的问题。比如 webpack 的依赖问题,Dan 想做的是一个通用的工具,而不仅限于 webpack,而现在的工具只是一个 webpack 的 loader。</p>
<p>虽然目前为止只有 webpack 实现了HMR, 但是一旦有其他的编译工具也实现了 HMR,那现有的 <code>loader</code> 如何集成到新的编译工具里面 ?</p>
<p>基于这些问题 Dan 曾经写过一篇 <a href="https://link.segmentfault.com/?enc=AFKwKf2j2M9jvr%2FX4aRkjw%3D%3D.Vhv1lbMf2JwhXUvot17HQYT%2Fr4BmKNy85Eh1%2BztvT4AqH0Y02X1edx%2BaZGeePQ3NadWt8lq2%2BnVI637mVSFpBUlUaUHvohjeKVvFw8MpHJg%3D" rel="nofollow">React-Hot-Loader</a> 之死的文章,文章中提到虽然 React-Hot-Loader 得到了巨大的关注,并且有很多工程也采取了他的思想,他仍然认为这不是他所想要的。</p>
<p>此时 <code>Babel</code> 如浪潮一般突然占领了整个 javascript 世界。Dan 意识到可以采用静态分析的方法去找到这些 component,而 babel 正好很适合做这些。不仅如此,Dan 同样想做一个错误处理的方式,因为当 render() 方法报错的时候,此时组件会处于一种无效状态,而此时 hot reload 是没办法工作的,Dan 想一起 fix 掉这个问题。</p>
<p>把 component 包裹在一个 proxy 里或者把 component render() 包裹在一个 try/catch 里,听上去都像 “一个函数接受一个component class 并且在它身上做些修改"。</p>
<p>那为什么不创造一个 Babel plugin 在你的基准代码里去定位 React Component 并且包裹它们,这样就可以进行随意的 transform。</p>
<h2>React Transform 的实现</h2>
<p>如果你在 github 去搜 React Transform ,你可以搜到 gearaon ( dan 在github上的名字,也是唯一一个不使用真名的账号哦~) 几个工程。 这是因为在开始设定 Transform 实现的时候不确定哪些想法最终会有实质作用,所以他拆分了 React Transform 为以下 5 个子工程:</p>
<ul>
<li>React Proxy 实现了对 React Component 的底层代理的功能</li>
<li>React Transform HMR 为每一个传入的 component 创建了一个代理,并且在全局对象里面保持了一个代理的清单,当同一个组件再次经历 transform,它去更新这些 component</li>
<li>React Transform Catch Error 在 render() 方法外面包了一层t ry/catch, 当出现错误可以显示一个自己配置的组件。</li>
<li>Babel Plugin for React Transform 会在你的基准代码里找到所有的React component ,在编译的时候提取它们的信息,并且把它们包裹在你选择使用的 Transform 里(比如,React Transform HMR)</li>
<li>React Transform Boilerplate 是个模板,展示如何将这些技术组合在一起使用</li>
</ul>
<p>这种模块化带了好处,同时也带来了弊端,弊端就是使用者在不清楚原理的情况下,不知道这些工程到底如何关联起来使用。并且这里有太多的概念暴露给了使用者, “proxies”, “HMR”, “hot middleware”, “error catcher”, 这使得用户感到很迷惑。</p>
<h3>问题:高阶组件还是存在问题</h3>
<p><em>当你解决了这些问题,尽量避免引入由解决它们带来的新的问题</em>。</p>
<p>还记得当年 React-Hot-Loader 在高阶组件上面束手无策吗,它没办法通过 <code>module.export</code> 导出的,包裹在高阶组件里面的组件。而 React Transform 通过静态检查这些组件的生命去“fix”这个问题,寻找继承自<br> React.Component 或者使用 React.createClass() 申明的 class。</p>
<pre><code>
// React Hot Loader 找不到它
// React Transform 找得到它
class Counter extends Component {
constructor(props) {
super(props)
this.state = { counter: 0 }
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div className={this.props.sheet.container} onClick={this.handleClick}>
{this.state.counter}
</div>
)
}
}
const styles = {
container: {
backgroundColor: 'yellow'
}
}
// React Hot Loader 找到到它
// React Transform 找不到它
export default useSheet(styles)(Counter)
</code></pre>
<p>猜猜这里我们遗漏了什么?被导出的 components! 在这个例子中,React Transform 会保留 <em>Counter</em> 的 state , hot reload 会改变<br><em>render()</em> 和 <em>handleClick()</em> 这些方法,但是任何对 <em>styles</em> 的改变不会体现,因为它不知道 <em>useSheet(styles)(Counter)</em> 正好 return 一个 React component, 这个组件也需要被 proxy。</p>
<p>很多人<a href="https://link.segmentfault.com/?enc=%2BuGh3EE402xbvMmhKWGzwA%3D%3D.fdpRxaXNvVrGQ%2B%2B%2BCquviGu1HNRMLRnoG0G%2Bsy%2Bfk3Nfk%2BCXzo7kQlP%2B3EX9gLnokbMHzoQFugSH%2FxEW8XbngPtunTOunP%2Bc3HM2DQRVkrQ%3D" rel="nofollow">发现了这个问题</a>,当他们注意到他们在 redux 里面 selectors 以及 action creators 不再会 hot reload。这是因为 React Transform 没有发现 <em>connect()</em> 返回一个组件,然后并没有一个简单的方法去识别。</p>
<h3>问题:使用静态方法检查太过于入侵性</h3>
<p>找到通过继承自 <em>React.Component</em> 或者使 <em>React.createClass()</em> 创建的class <a href="https://link.segmentfault.com/?enc=8UfMjVVGDTm6KYlyE3gTYA%3D%3D.LJPueF9fFonwkwdLoz54t5IBZFd3nMmFCDzLHXIhU%2F73GXfN%2BJ6DdVrw%2BEXWwBnhGqPh7p6z6L508ABeXcYvMY7QWUmc9zza25v3bEN%2BlaxD67%2Babq%2FJ0CysRFKOD31CABK299kzZZ0m1YFb8S5WfGv2ubQopR7JDBiggQ9euHo%3D" rel="nofollow">不是很难</a> 。然而,它可能出错,你也不想 <a href="https://link.segmentfault.com/?enc=ZZBtvL797pDSEMfOHEp4KQ%3D%3D.rWxxTj2OC0P9JB%2BZyAKzR0hSUcNBzJvMqdZ%2BBvHvsFlE2Me5x9lxHfGAZ0VeAOmsSakcRkVEhcab0%2BOQHr%2FlO1EwJsNEMAF3K%2Fn5InTnxNo%3D" rel="nofollow">带来误判</a>。</p>
<p>随着React 0.14的发布,这个任务变得更加艰难。任何 functions,如果<br> return 出来的是一个有效的 ReactElement 那就可能是一个组件。由于你不能肯定,所以你不得不采用探索法。比如说,你可在判断在顶级作用域的 function,如果是以驼峰命名,使用JSX, 并且接受不超过两个以上(props 和 context)参数,那它可能是个React component。这样会误判吗?是,可能会。</p>
<p>更糟糕的是,你必须让所有的 “transform” 去处理 classes 和 functions。如果React 在v16版本里面引进<a href="https://link.segmentfault.com/?enc=yxbiuOUMC7INmTB7NpggOg%3D%3D.4YKvAWtYnl3ZEB23YwrRCJI%2FCag6amoHg3hM2CHj1RqKe%2F5zi2LaVj1aDUEUkf%2BULg4jz7qolkYoZEio63whEfsZEwgdqdyfLgvVhus5OY0rS%2FL%2FGfPJupOj09pq77U3Kv8pIkLY0z8VjubV7zh9Yt4EEouChzR6WyX1A0SRw38%3D" rel="nofollow">另外一种</a> 一种方式去声明组件呢,我们将要重写所有的transform吗?</p>
<p>最后得出结论,用静态方法 <em>包裹</em> 组件相当复杂。你将要对 functions 和 classes 可能的 export 方式取使用各种方法去处理,包括 default 和 named 的 exports,function声明,箭头函数,class声明,class表达式,createClass() 形式调用,以及等等。每种情况你都需要用一种方法针对相同的变量或者表达式去绑定不同的值。</p>
<p>想办法支持 functional components 是<a href="https://link.segmentfault.com/?enc=%2FcyiOrvpaWy8CmSE1cW5Rg%3D%3D.0TNz9tFtdbnFPWRl1aJd7JeLZVF%2FAC7YGHnj7NNqOHO9bW%2B96lrH2a1%2Fmw9kM823mXE1oXJdiX1e7oVaqWXSIQk0i5hhtGQOzC%2FqZOUq%2Fks%3D" rel="nofollow">最多的提议</a>, 我现在不会考虑在 React Transform 支持它,因为实现的复杂程度会给工程以及它的维护者带来巨大困难,并且可能由于一些边缘情况导致彻底的破坏。</p>
<h2>React Hot Loader 3</h2>
<p>以上总结是出自 Dan 的一篇在medium上的<a href="https://link.segmentfault.com/?enc=AJC1KYpo%2BzxcGQBlhh%2B7Yw%3D%3D.Ffsm8U%2FolCiek09y0I5V3W2Hzu0lDag3cOkNceyopq9R2BU%2B3Cu%2Fw199f%2BEcyNXcDQCbxmedRzFLEyAIG1jR2VELZDRbqS02mqk9S3oB2KA%3D" rel="nofollow">文章</a>,他称呼 React Hot Loader 是一个 Accidental Complexity,其中还提到它对 compile-to-js 语言 (其他通过编译转成JS的语言)的考虑,以及中途遇到的 babel 的问题等。文章中 Dan 表明他会在几个月内停止 React Transform 而使用一个新的工程代替,新的工程会解决大多数残留的问题,末尾给了一些提示在新工程里面需要做到的。在这篇文章的一个月后,React-Hot-Loader 3 release了,让我们大致的过一下 3 的到底做了些什么。</p>
<h3>在调用的时候 proxy</h3>
<p>在源码中找到并且包裹React components是非常难做到的,并且有可能是破坏性的。这真的会破坏你的代码,但标记它们相对来说是比较安全。比如我们可以通过 babel-plugin 检查一个文件,针对顶层 class、function 以及 被 export 出来的模块在文件末尾做个标记:</p>
<pre><code>class Counter extends Component {
constructor(props) {
super(props)
this.state = { counter: 0 }
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
counter: this.state.counter + 1
})
}
render() {
return (
<div className={this.props.sheet.container} onClick={this.handleClick}>
{this.state.counter}
</div>
)
}
}
const styles = {
container: {
backgroundColor: 'yellow'
}
}
const __exports_default = useSheet(styles)(Counter)
export default __exports_default
// 我们 generate 的标记代码:
// 在 *远端* 标记任何看上去像 React Component 的东西
register('Counter.js#Counter', Counter)
register('Counter.js#exports#default', __exports_default) // every export too</code></pre>
<p>register() 至少会判断传进来的值是不是一个函数,如果是,创建一个 React Proxy 包裹它。它不会替换你的 class 或者 function,这个proxy将会待在全局的map里面,等待着,直到你使用React.createElement()。</p>
<p>仅仅真正的组件才会经历 React.createElement,这就是我们为什么 monkeyPatch React.createElement()。</p>
<pre><code>import createProxy from 'react-proxy'
let proxies = {}
const UNIQUE_ID_KEY = '__uniqueId'
export function register(uniqueId, type) {
Object.defineProperty(type, UNIQUE_ID_KEY, {
value: uniqueId,
enumerable: false,
configurable: false
})
let proxy = proxies[uniqueId]
if (proxy) {
proxy.update(type)
} else {
proxy = proxies[id] = createProxy(type)
}
}
// Resolve 发生在 element 被创建的时候,而不是声明的时候
const realCreateElement = React.createElement
React.createElement = function createElement(type, ...args) {
if (type[UNIQUE_ID_KEY]) {
type = proxies[type[UNIQUE_ID_KEY]].get()
}
return realCreateElement(type, ...args)
}</code></pre>
<p>在调用端包裹组件解决了很多问题,比如 functional component 不会误判,包裹的逻辑只要考虑 function 和 class,因为我们把生成的代码移到底部这样不会污染代码。</p>
<h3>给 compile-to-js 语言提供了一种兼容方式</h3>
<p>Dan 提供了类似于 React-Hot-Loader 1 的 webpack loader, 即 <code>react-hot-loader/webpack</code>。在不使用 babel 做静态分析的情况下,你可以通过它找到 <code>module.export</code> 出来的 component,并且 register 到全局,然后在调用端实现真正的代理。所以这种方式只能针对<a href="https://link.segmentfault.com/?enc=cd2KJEEpf3JoIK1Esz%2FXCA%3D%3D.GDEuvQrBGU7crdyTAdMUSXZ0OeXHojo%2BaQd0D5lIz1CdyD%2BjAyBF43UeAxgVBRejwcpXKI5SBg98dgoXNuXO7A%3D%3D" rel="nofollow">实际 export 出来的组件做保留 state 以及 DOM 的 hot reloading</a>。</p>
<p>什么情况下会使用这种方式,那就是针对其他 compile-to-js 的语言比如 <a href="https://link.segmentfault.com/?enc=khF4OKcJDVGbutQrPEDGTg%3D%3D.qQ%2BzD0HPPMqF36WJ4dlViHZuaghT05J3fL1kBNdW7Lg3WyrDkhqmd%2Bjpc246ZGaC" rel="nofollow">Figwheel</a> 和 <a href="https://link.segmentfault.com/?enc=AHvaEYMdvVxZBGMZvZ8I0Q%3D%3D.JdH5Fpd55w4mGhVDX0Uol4jUHJW31k1SbBipBon2ByFHg2MbFBWjKU1pZzwYTcB9" rel="nofollow">Elm Reactor</a>。在这些语言里面有自己的类的实现等,所以 Babel 没有针对源码办法去做静态检查,所以必须在编译之后去处理。</p>
<h3>错误处理</h3>
<p>还记得 React Transform 里面的React Transform Catch Error 吗。React-Hot-Loader 把处理 render 出错的逻辑放到 AppContainer 。因为 React V16 增加了 <a href="https://link.segmentfault.com/?enc=hlHGKOBUyRN8YAD%2F8vomdg%3D%3D.XZPQQedh0xKuJNbPPSQ5JJLSCOov%2BkGCdWLhx27UakdJ%2FV05FjNHP5T3%2FTJtTCRz" rel="nofollow">error boundaries </a>,相信在未来的版本 React-Hot-Loader 也会做相应调整。</p>
<h2>写在最后</h2>
<p>这就是对 React-Hot-Loader 的实现的一个追溯,如果你真的理解了,那么你在配置 React-Hot-Loader 到你的应用代码里面的每个步骤会有一个重新的认识。我不确定大家是否读懂了,或者存在还存在什么疑问,欢迎来沟通讨论。截止写文现在 React-Hot-Loader 4 已经在进行中,我比较偏向于 4 会和 React 迭代保持更亲密的同步( 从之前 <a href="https://link.segmentfault.com/?enc=B1TZD%2BRZZRhLMHrgK2Wpug%3D%3D.fWjpkWqp5kSiuxXwiAiR4%2FjqQp%2BVJ5SqBzxV1VI6uh0RBGsz6B3ZmwzVM%2B0q%2Bhgj" rel="nofollow">error boundaries </a> 和 <a href="https://link.segmentfault.com/?enc=T2h%2BH72NM0ZkkBXvwjV%2BAA%3D%3D.hb91XkhryvCtBnf0KdK3VGjD9Z19ECKbF5qcgQyLlUE3LKyT3DHaBnURPISAAeTQ" rel="nofollow">official instrumentation API</a> 来看),到时候拭目以待吧。</p>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
【推荐】开源项目minapp-重新定义微信小程序的开发
https://segmentfault.com/a/1190000013510485
2018-03-04T15:18:22+08:00
2018-03-04T15:18:22+08:00
iKcamp
https://segmentfault.com/u/ikcamp
6
<h2>minapp</h2>
<p><strong>重新定义微信小程序的开发</strong></p>
<blockquote>官网:<a href="https://link.segmentfault.com/?enc=gaQxNIA2rxKp01Kk7SwHKA%3D%3D.X2onQ1NL3wTbcuZblneFNF8ZgpwlFvqf1FyLjQG1%2BmHBvJ0cvwgLIUbGUgklcC2X" rel="nofollow">https://qiu8310.github.io/minapp/</a><p>作者:<a href="https://link.segmentfault.com/?enc=BCVXn1Nnoftn9Eu1gwcEXw%3D%3D.T4vVzcPnfBjv1FVMuk50PAkjP%2BCCAkkWExljSgXrNXg%3D" rel="nofollow">Mora</a></p>
</blockquote>
<h2>minapp</h2>
<p><strong>重新定义微信小程序的开发</strong></p>
<h3>使用</h3>
<ol>
<li>用 npm 安装命令行工具: <code>npm install -g @minapp/cli --registry "https://registry.npmjs.org/"</code> (避免从淘宝镜像上安装,它上面的还是老版本,已经给他们提了一个 <a href="https://link.segmentfault.com/?enc=rDvFfQS3IaQCNcLSGcCWbA%3D%3D.wRIwKT%2BQu45GZfmwCdiqcVP%2B3ntniD1LUYuRw3svhMMel4lA3c%2FB%2F9x0r%2BkfPvrq" rel="nofollow">issue</a>)</li>
<li>初始化项目:<code>minapp init <你要创建项目的文件夹></code> (同时支持创建 js 和 ts 项目)</li>
<li>安装两个 vscode 插件:<a href="https://link.segmentfault.com/?enc=jqCEBfRcc2OzcuKYE9brfA%3D%3D.3DoljMjDgwG%2BAYaWi4A%2BAq18Gx932kdl66bK21Im%2F8J%2FIHWwHNw1sSd5jd%2FnBTepGsYvwzh%2BZo17WubCDG1%2F%2FM9yodyn9kG5Wrw1XENnkTI%3D" rel="nofollow">minapp</a> 和 <a href="https://link.segmentfault.com/?enc=a4EX2Os7CYu5Z1PN3wb6kA%3D%3D.SUdVTxoCCno5hocO08QsrlKkuYQIqKhyRP5h2UPs9ozdG6Su72pZoPRLV0HSlrk6EA0ZxtykacU3YJJ1EiEnARYgk6rVEy%2FA4zriyZbuedk%3D" rel="nofollow">dot-template</a>(可选,但建议安装)</li>
</ol>
<h3>功能概览(在 vscode 编辑器下)</h3>
<h4>wx 所有接口都有智能的提醒,同时包括接口的参数,和返回值</h4>
<p><img src="/img/remote/1460000013510490?w=996&h=440" alt="wx接口示例" title="wx接口示例"></p>
<h4>提供一个 promise 版的 wx 接口 wxp,和 wx 一样,只是它会将 wx 中所有需要 success/fail/complete 三个参数的函数 promise 化</h4>
<ul>
<li>wxp 中也支持使用 success 回调</li>
<li>wxp 给 Promise 添加了一个 finally 方法;如,你可以这样用 <code>wxp.getUserInfo().finally(() => { /* do something */ })</code>
</li>
</ul>
<p><img src="/img/remote/1460000013510491?w=996&h=394" alt="wxp示例" title="wxp示例"></p>
<h4>集成 mobx,可以非常方便的修改全局数据,并自动更新当前页面状态</h4>
<ul>
<li>注入 Store 只需要在 appify 函数中添加 Store 对象即可</li>
<li>Page 和 Component 中都默认注入了 Store 对象,你可以使用 <code>this.store</code> 获取</li>
</ul>
<p><img src="/img/remote/1460000013510492?w=996&h=514" alt="mobx" title="mobx"></p>
<h4>wxml 模板语言支持语法高亮,组件智能提示,组件属性智能提示(需要安装 vscode 插件 <a href="https://link.segmentfault.com/?enc=Xl3Gnny87DKyEF5Oq52rdw%3D%3D.TckB6uLQmOZKTSDqv2UfAQGluNSYJRIBMOQdw2xk6FU2OE5aQu2qfR1CdCY7vSbI0lwUNlYifZ2DGtYnkTp64Y0m6KYPOAIAa9ospMztyYQ%3D" rel="nofollow">minapp</a>)</h4>
<p><img src="/img/remote/1460000013510493?w=996&h=460" alt="wxml" title="wxml"></p>
<h4>json 文件支持自动提示</h4>
<p><img src="/img/remote/1460000013510494?w=996&h=460" alt="json" title="json"></p>
<h4>新建一个 page 文件夹时,自动生成相关文件(需要安装 vscode 插件 <a href="https://link.segmentfault.com/?enc=cC08TLg%2B7d7sW5PQAktpHQ%3D%3D.IFYclPF4xq5%2FMwvs6LzKYoPf8x04U8zZM5S2bIF2NdLqj%2BFGy3PfSn6%2F%2Bs9xZ%2FZ5Vhz4G%2FzHYINk1KImzsGo39fzipay%2BSNn4F6A7Lv0Zso%3D" rel="nofollow">dot-template</a>)</h4>
<ul>
<li>自动为你创建相关的同名的文件,包括 js/json/wxml/scss,并且这些模板文件你可以随时在 .dtpl 文件夹下修改</li>
<li>自动将新建的 page 路径注入到 app.json 文件夹中</li>
</ul>
<p><img src="/img/remote/1460000013510495?w=996&h=546" alt="新建 Page 示例" title="新建 Page 示例"></p>
<h4>小程序 Page 中支持函数自动提示</h4>
<p><img src="/img/remote/1460000013510496?w=996&h=440" alt="Page 中的函数自动提示示例" title="Page 中的函数自动提示示例"></p>
<h4>同理,新建组件文件夹时,也会创建相关的文件;同时组件中的生命周期函数也会自动提示</h4>
<p><img src="/img/remote/1460000013510497" alt="Component 示例" title="Component 示例"></p>
<h3>关于此仓库说明</h3>
<p>这不是一个项目,是有好几个项目组合而成的,用的是 <a href="https://link.segmentfault.com/?enc=dmidqd35qo8KXfJGis2dfg%3D%3D.OG8GBgqfE7LIKEW%2Fq9zoSwKgg5jokYks9u7nhCTzGkA%3D" rel="nofollow">lerna</a> 开发工具,其它项目在 <a>packages 目录下</a>,这里对其中的几个主要项目做个简要概述</p>
<ul>
<li>
</li>
<li>
</li>
<li>
</li>
<li>
</li>
<li>
</li>
<li>
</li>
<li>
</li>
</ul>
<h3>TODO</h3>
<ul>
<li>[ ] 小程序中的静态资源自动上传到 七牛 (完成我的 file-uploader 组件)</li>
<li>[ ] 实现类似于 vue 的功能,可以将所有文件写在一个页面上</li>
<li>[ ] webpack 升级到 4.0</li>
<li>[ ] 写一个小程序的自动化测试框架</li>
</ul>
<blockquote>下一篇:作者亲著,重新定义微信小程序开发 —— 上篇</blockquote>
【完结汇总】iKcamp出品基于Koa2搭建Node.js实战共十一堂课(含视频)
https://segmentfault.com/a/1190000013457617
2018-03-01T11:01:34+08:00
2018-03-01T11:01:34+08:00
iKcamp
https://segmentfault.com/u/ikcamp
14
<h3>?? 与众不同的学习方式,为你打开新的编程视角</h3>
<ul>
<li>
<p>独特的『同步学习』方式</p>
<ul><li>文案讲解+视频演示,文字可激发深层的思考、视频可还原实战操作过程。</li></ul>
</li>
<li>
<p>云集一线大厂有真正实力的程序员</p>
<ul><li>iKcamp 团队云集一线大厂经验丰厚的码农,开源奉献各教程。</li></ul>
</li>
<li>
<p>改版自真实的线上项目</p>
<ul><li>教程项目并非网上随意 <code>Demo</code>,而是来源于真实线上项目,并改版定制为教程项目</li></ul>
</li>
<li>
<p>源码开放</p>
<ul><li>课程案例代码完全开放给你,你可以根据所学知识自行修改、优化。</li></ul>
</li>
</ul>
<p><img src="https://user-gold-cdn.xitu.io/2017/12/14/16053048dbd23e9c?w=1122&h=692&f=png&s=132060" alt="" title=""></p>
<h3>?? 玩转 Node.js 同时全面掌握潮流技术</h3>
<ul>
<li>采用新一代的 Web 开发框架—— Koa2 ——更小、更富有表现力、更健壮。</li>
<li>使用 fs、buffer、http、path 等 Node.js 最核心 API。</li>
<li>融合多种常见的需求场景:网络请求、JSON 解析、模板引擎、静态资源、日志记录、错误请求处理。</li>
<li>结合 async await (ES6/7) 语句中转中间件控制权,解决回调地狱问题。</li>
</ul>
<p><img src="https://user-gold-cdn.xitu.io/2017/12/14/1605305617fe1e02?w=450&h=608&f=png&s=94160" alt="" title=""></p>
<h3>?? 适合人群及技术储备要求</h3>
<blockquote>如果你是一个有全栈梦想的前端开发者,或是想要入门 <code>Node.js</code>,那么来学习本课程,学完不仅实现你的全栈梦想,更让你无缝衔<br>接 <code>Node</code> 应用公司的现代前端开发体系和流程。</blockquote>
<ul>
<li>Node.js</li>
<li>ES6/7 语法知识</li>
<li>了解 HTTP 协议</li>
</ul>
<p><img src="https://user-gold-cdn.xitu.io/2017/12/14/160530634e39a06c?w=340&h=408&f=png&s=97478" alt="" title=""></p>
<h3>?? 亮点的课程设计,让你对 Node.js 豁然开朗</h3>
<p>本课程项目GitHub地址:<a href="https://link.segmentfault.com/?enc=JFOxFijK7KqJ%2BLkPUvPyDg%3D%3D.pdenXXzjA7HO00exJ9YwVgHLHk8Dj9uF1%2F0R%2BQRjnLs389WFaGt2qh3Vz2AaCW%2Fo" rel="nofollow">https://github.com/ikcamp/koa2-tutorial</a></p>
<blockquote>P.S. 不要吝啬你的Star,你的Star是iKcamp的动力!</blockquote>
<ul>
<li>
<p>基础篇</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=NCPorl%2BgJg4Izn4TVUSS%2BA%3D%3D.ZNT6tJ5j7w4BY4XgT5A9WV0VwVgpxLihzG4Cq65LEYC2eyYXk6%2BJtqcWmVxMOGzJ" rel="nofollow">环境准备——安装搭建项目的开发环境</a></li>
<li><a href="https://link.segmentfault.com/?enc=ggdlAbK851K1TUPRbGbruQ%3D%3D.82qFRTSduzRbPOakiqq7jRX9s54P74kTGa395briOcY42SYo%2B2%2BlDvI0%2BDdFh2sR" rel="nofollow">中间件用法——讲解 Koa2 中间件的用法及如何开发中间件</a></li>
<li><a href="https://link.segmentfault.com/?enc=xrBp3CL%2FT5agdpUmvMzeRg%3D%3D.l3XfjUPFYR7evw0G8jzh6%2FlzpnCCcd8sR%2BUmC6jyyd0OYMaP2eNaqPjfhDfHoUbh" rel="nofollow">路由koa-router——MVC 中重要的环节:Url 处理器</a></li>
<li><a href="https://link.segmentfault.com/?enc=9pMwJk%2B5BPb6P8yUrypD4Q%3D%3D.Zau%2BLPijXxT112qcw231dT1ccl2FW8gstG9u4AvQGA35WwVTpFVoCI0JmXSOkJBl" rel="nofollow">POST/GET请求——常见请求方式处理</a></li>
<li><a href="https://link.segmentfault.com/?enc=GfS9XjJF3uRzj7KwnYOKfQ%3D%3D.Hb8uDEyl667BaBWg3GhYkMGyrSMxFluteu2A%2FKnXElfblkH1RGx3m9IZjYA2alSC" rel="nofollow">代码分层——梳理代码,渐近于 MVC 分层模式</a></li>
<li><a href="https://link.segmentfault.com/?enc=cbh2NrSh8uXE55ctFzi2pQ%3D%3D.wdsnGrkQcamYuSYhQ1hAIyw3qjQfUwvmDVfPnWu7CVEro6ZtGaFKNcCluUYaA6U8" rel="nofollow">视图nunjucks——Koa 默认支持的模板引擎</a></li>
<li><a href="https://link.segmentfault.com/?enc=ado2mpOUKeoVdXmqwL9moQ%3D%3D.JthUWfeg1zwyC%2BlyWomuL3fx9C%2FB8%2Fpu9I2wGOvn6%2FIEDCxuWZgcNmxz%2Be%2Ff261N" rel="nofollow">处理静态资源——指定静态文件目录,设定缓存</a></li>
</ul>
</li>
<li>
<p>提升篇</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=QQ7BwNVnu1ViORU7PCYtbg%3D%3D.CHA3lFEUApxJNnmK51FGtQRAcvPRFDMvdtfJ4w0JOpNrtSsEbmFX30A%2FP3fb0rAy" rel="nofollow">解析JSON——让 Koa2 支持响应 JSON 数据</a></li>
<li><a href="https://link.segmentfault.com/?enc=ly7s%2B%2FzbYTLYjLBh0mqeUw%3D%3D.WbUen2yM%2Bpq%2BqUa9uqBzY4iLUu2z4bGMXI1sSUD4YBKEzIpH6euQ7EGTr8MGTqay" rel="nofollow">记录日志——开发日志中间件,记录项目中的各种形式信息</a></li>
<li><a href="https://link.segmentfault.com/?enc=va04a0Gv6C%2FefF3gPzcdpA%3D%3D.RdeI%2BYC7CjCQIZDGRbuOrEGvnpxDUNeHsP5sg6zeBvnXCpbDbSWiEEMtPWfMYgMs" rel="nofollow">错误处理——处理 HTTP 特定错误请求场景</a></li>
<li><a href="https://link.segmentfault.com/?enc=uFdXwztQUK7aSAZbJgA8Jg%3D%3D.%2F6FIEqzOFaP0RfShKE5Uzq4Q3CCCo9Xd9g8Wp9CwnonGfZyUvAsYKyWs5bOcyWFK" rel="nofollow">规范与部署——制定合适的团队规范,提升开发效率</a></li>
</ul>
</li>
</ul>
<h2>大纲介绍</h2>
<p>视频地址:<a href="https://link.segmentfault.com/?enc=L9JExZndIhRwnCKbQxn%2BAQ%3D%3D.YUF6j5yCVJFboAllgSa3WKYtO6FAbdQUgGFNfxX%2BK2iZKu5kg0dymSNkbpTo2KHG" rel="nofollow">https://www.cctalk.com/v/15114357769946</a></p>
<p><img src="https://user-gold-cdn.xitu.io/2017/12/14/1605313a0e066424?w=1242&h=718&f=png&s=568755" alt="" title=""></p>
<h3>?? 以 git 分布式版本控制系统,来学习和管理项目代码</h3>
<ol><li>通过 <code>git</code> 把项目复制到本地</li></ol>
<pre><code class="git">git clone https://github.com/ikcamp/koa2-tutorial</code></pre>
<ol><li>切换目录</li></ol>
<pre><code class="shell">cd koa2-tutorial</code></pre>
<ol><li>在当前目录下切换分支</li></ol>
<pre><code class="git">git checkout 0-start</code></pre>
<ol><li>进入到项目目录 <code>code</code>
</li></ol>
<pre><code class="shell">cd code/</code></pre>
<p><strong>注意:</strong> 所有的分支命名上,都以数字开头,序号就是我们的开发顺序和讲解顺序。</p>
<p><strong>注意:</strong> 分支中的 <code>code/</code> 目录为当节课程后的完整代码。 </p>
<h3>?? 下载完整项目代码</h3>
<blockquote>教程的完整代码在主干 <code>master</code> 中,请自行<a href="https://link.segmentfault.com/?enc=BKjZjQu01AatEUoE%2BfsqFQ%3D%3D.OUEa7o70QqI4pj4A%2BuWLYpiEAhMpLTSyDk4O9Yc1tOHAWsdIMnV%2FYtgdIfmI%2FX4VxBBBNpPVQNKxIAphzfVJaQ%3D%3D" rel="nofollow">查阅? </a>
</blockquote>
<h3>?? iKcamp 制作团队</h3>
<p>原创作者:<a href="https://link.segmentfault.com/?enc=CMeRzsIUTjgihGIduhHqtg%3D%3D.BryE4rg5uC09vrMb7t%2FW%2BQRqGtd7P9qBozJyblhHmJ0%3D" rel="nofollow">大哼</a>、<a href="https://link.segmentfault.com/?enc=zxQcWxnYJDD%2FSSZelI8ipw%3D%3D.YcD1xZDOZ97v43yE7BCndcsqTrjT7uKlmuWZ35rr%2FqU%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=zCC%2FapC%2FPN7N26iQo4efMw%3D%3D.eMWeh1ySwQ72ctmomrEBGF%2Fl5v9nGmG1VgJX1%2F%2B4Xyk%3D" rel="nofollow">三三</a>、<a href="https://link.segmentfault.com/?enc=bY7XV4XKtHPaqSedz0N9cg%3D%3D.QD4x8XIDTWFq5tp%2F9TgZaoAcASWrX9SQ%2BcKo39UgKHY%3D" rel="nofollow">小虎</a>、<a href="https://link.segmentfault.com/?enc=LCehgd5Zk3QcVCzDpwALGg%3D%3D.M2ebEJQuViBV%2BrUiBebWzT81gZgRw80b79kmrehJ3hk%3D" rel="nofollow">胖子</a>、<a href="https://link.segmentfault.com/?enc=Xmq4GcU2MmK0ISDsXGFYow%3D%3D.qNdhOJ2pppaP%2BAYyoG9RAQ%3D%3D" rel="nofollow">小哈</a>、<a href="https://link.segmentfault.com/?enc=%2Bn3POGJZFr1%2FJSWaUHjPBg%3D%3D.aWXEQIAJKgXkKocv2X2UYAekM5tNXRuF3GVNa5012Kk%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=X37E%2B6idbBLgEJITav0zDQ%3D%3D.JSuT7X22%2ByNv2bvwSxRgESb1iSLjEf03CCSyqd7iZKI%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=3Usp%2FryEJpaPPppXiiIO2A%3D%3D.rAFcwqPhcHEGgyk9R0xWpYo83Vt0%2FVl%2BiuSCgw0QAhc%3D" rel="nofollow">晃晃</a> <br>文案校对:<a href="https://link.segmentfault.com/?enc=qa8gVA177maHoN2ZEhJ0xQ%3D%3D.MWQzHASRDEAxlg8Hq%2FpsnlgnZQAJ3TX9r2m5q8C1hcQ%3D" rel="nofollow">李益</a>、<a href="https://link.segmentfault.com/?enc=PlFptqcVFZWdQrnahnISPA%3D%3D.PP9UlSmS6uqtn1zEqSODojAPXQL%2BWXiS1B5JMGvi9v0%3D" rel="nofollow">大力萌</a>、<a href="https://link.segmentfault.com/?enc=7Kciu3Nm1Hq%2FEgqZ1BT3ZA%3D%3D.sTjcmxxp4FWzdaI4ZYGCYXJMe0lpa03P%2BRftGHkhfDw%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=uqnT7tvJVMSzqEBNOw81Zg%3D%3D.m07jNM3DddeUEqvhjhOrKZDfy57poA7nxazKvabvYKk%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=gclig1xLMkDqEuari1zYew%3D%3D.6zm17%2Bsw9UXbT4cMbMpdVVb7eZpIZxXEeHnLI725h5s%3D" rel="nofollow">小溪里</a>、<a href="https://link.segmentfault.com/?enc=XKPCaN%2BolzmiS7x8dERdig%3D%3D.6btR1jj6djtK8qrx%2Fi%2B85g%3D%3D" rel="nofollow">小哈</a> <br>风采主播:<a href="https://link.segmentfault.com/?enc=bcUyezkLVtyZ59iUNz44gQ%3D%3D.3Wv08IFejopgdKC7bDNz302FN8BvnrA33zG8Anw%2BmmI%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=NSMMGx%2B6b52X9cnHyqouwg%3D%3D.QZytx1vcRFo5MBhJsIfsW%2BchryaLQruo1PYzDrTNz8Y%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=4cUGTZFwj%2BUQdNn6hi0J0g%3D%3D.TqKiSPnotpvEQYO0t%2BRr%2B%2Fobn%2FaIbFHdflmOlJfeAXM%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=DP%2BN%2BuU8wXWk%2FAOTT6hEew%3D%3D.NTh5dWMxNHXCeyoIQ2b5uBX7DxU3StQwDY5eWDrXdaQ%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=RM8x49a2E7Al78kYRQBj4A%3D%3D.au1R5rIuvSmjwsR9dDHwXw%3D%3D" rel="nofollow">小哈</a> <br>视频剪辑:<a href="https://link.segmentfault.com/?enc=PiAKFVVfVZ%2F3fy7GUCDy4g%3D%3D.Eg4DzNruPL2Egg6BPCZ0U5TDuQJc3LkVzK8nMfOvfqI%3D" rel="nofollow">小溪里</a> <br>主站运营:<a href="https://link.segmentfault.com/?enc=nDo9fOubPKyMmUbjk2nsLQ%3D%3D.9hrz%2BZsLmXCFM6XrU6RSB9rLA4sqDTER0Bv3OR5Dd0E%3D" rel="nofollow">给力xi</a>、<a href="https://link.segmentfault.com/?enc=0lmqRMx8bfCJKkOHX4sYFA%3D%3D.%2BY8yZsefxTvN6Tm7tTyMo3D1O6AqDCaYtcLEBJhSnf0%3D" rel="nofollow">xty</a> <br>教程主编:<a href="https://link.segmentfault.com/?enc=otK7KBm6BK5ziFmiOFLT1g%3D%3D.jotsyjwyWcsBE7IfjKrhSUzBg%2F%2Bsk4TN1%2B4aRsr8dek%3D" rel="nofollow">张利涛</a> </p>
<p><img src="https://user-gold-cdn.xitu.io/2017/12/14/1605309af5a7dfa2?w=1426&h=778&f=png&s=414615" alt="" title=""></p>
iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 规范与部署
https://segmentfault.com/a/1190000013189459
2018-02-07T14:10:26+08:00
2018-02-07T14:10:26+08:00
iKcamp
https://segmentfault.com/u/ikcamp
1
<p>沪江CCtalk视频地址:<a href="https://link.segmentfault.com/?enc=esds7p45dEweK1Q1Kjdccw%3D%3D.I50oS%2B9U4aElOsckJ0CSL5l1HrNxdwM5b59gzemim1xDUxaWFOT2kGuOibXYGCJ0" rel="nofollow">https://www.cctalk.com/v/15114923889450</a></p>
<p><img src="/img/remote/1460000013229246?w=1718&h=962" alt="" title=""></p>
<h2>规范与部署</h2>
<blockquote>懒人推动社会进步。</blockquote>
<p>本篇中,我们会讲述三个知识点</p>
<ul>
<li>定制书写规范</li>
<li>开发环境运行</li>
<li>如何部署运行</li>
</ul>
<h3>定制书写规范</h3>
<blockquote>文中所说的书写规范,仅供参考,非项目必需。</blockquote>
<p>随着 <code>Node</code> 流行,<code>JavaScript</code> 编码规范已经相当成熟,社区也产生了各种各样的编码规范。但是在这里,我们要做的不是『限制空格的数量』,也不是『要不要加分号』。我们想要说的规范,是项目结构的规范。</p>
<p>目前我们的项目结构如下:</p>
<pre><code class="txt">├─ controller/ // 用于解析用户的输入,处理后返回相应的结果
├─ service/ // 用于编写业务逻辑层,比如连接数据库,调用第三方接口等
├─ errorPage/ // http 请求错误时候,对应的错误响应页面
├─ logs/ // 项目运用中产生的日志数据
├─ middleware/ // 中间件集中地,用于编写中间件,并集中调用
│ ├─ mi-http-error/
│ ├─ mi-log/
│ ├─ mi-send/
│ └── index.js
├─ public/ // 用于放置静态资源
├─ views/ // 用于放置模板文件,返回客户端的视图层
├─ router.js // 配置 URL 路由规则
└─ app.js // 用于自定义启动时的初始化工作,比如启动 https,调用中间件,启动路由等</code></pre>
<p>当架构师准备好项目结构后,开发人员只需要修改业务层面的代码即可,比如当我们增加一个业务场景时候,我们大概需要修改三个地方:</p>
<ol>
<li>
<code>service/</code> 目录下新建文件,处理逻辑层的业务代码,并返回给 <code>controller</code> 层</li>
<li>
<code>controller/</code> 目录下新建文件,简单处理下请求数据后,传递给 <code>service</code>
</li>
<li>修改路由文件 <code>router.js</code>,增加路由对应的处理器</li>
</ol>
<p>随着业务量的增大,我们就会发现有一个重复性的操作——『不断的 <code>require</code> 文件,不断的解析文件中的函数』。当业务量达到一定程度时候,可能一个文件里面要额外引入十几个外部文件:</p>
<pre><code class="js">const controller1 = require('...')
const controller2 = require('...')
const controller3 = require('...')
const controller4 = require('...')
...
app.get('/fn1', controller1.fn1() )
app.get('/fn2', controller2.fn2() )
app.get('/fn3', controller3.fn3() )
app.get('/fn4', controller4.fn4() )</code></pre>
<p>单是起名字就已经够头疼的! </p>
<p>所以,我们要做的事情就是,约定代码结构规范,省去这些头疼的事情,比如 <code>router.js</code>:</p>
<pre><code class="js">// const router = require('koa-router')()
// const HomeController = require('./controller/home')
// module.exports = (app) => {
// router.get( '/', HomeController.index )
// router.get('/home', HomeController.home)
// router.get('/home/:id/:name', HomeController.homeParams)
// router.get('/user', HomeController.login)
// router.post('/user/register', HomeController.register)
// app.use(router.routes())
// .use(router.allowedMethods())
// }
const router = require('koa-router')()
module.exports = (app) => {
router.get( '/', app.controller.home.index )
router.get('/home', app.controller.home.home)
router.get('/home/:id/:name', app.controller.home.homeParams)
router.get('/user', app.controller.home.login)
router.post('/user/register', app.controller.home.register)
app.use(router.routes())
.use(router.allowedMethods())
}</code></pre>
<p>聪明的同学可能已经发现了,<code>app.controller.home.index</code> 其实就是 <code>cotroller/home.js</code> 中的 <code>index</code> 函数。</p>
<h4>设计思路</h4>
<p>实现思路很简单,当应用程序启动时候,读取指定目录下的 <code>js</code> 文件,以文件名作为属性名,挂载在实例 <code>app</code> 上,然后把文件中的接口函数,扩展到文件对象上。 </p>
<p>一般有两种方式入手,一种是程序启动时候去执行,另外一种是请求过来时候再去读取。 </p>
<p>而在传统书写方式中,项目启动时候会根据 <code>require</code> 加载指定目录文件,然后缓存起来,其思路与第一种方式一致。如果以中间件的方式,在请求过来时候再去读取,则第一次读取肯定会相对慢一起。综合考虑,我们采用了第一种方式:程序启动时候读取。</p>
<h4>代码实现</h4>
<p>新建目录文件 <code>middleware/mi-rule/index.js</code>, 实现代码如下:</p>
<pre><code class="js">const Path = require("path");
const fs = require('fs');
module.exports = function (opts) {
let { app, rules = []} = opts
// 如果参数缺少实例 app,则抛出错误
if (!app) {
throw new Error("the app params is necessary!")
}
// 提取出 app 实例对象中的属性名
const appKeys = Object.keys(app)
rules.forEach((item) => {
let { path, name} = item
// 如果 app 实例中已经存在了传入过来的属性名,则抛出错误
if (appKeys.includes(name)) {
throw new Error(`the name of ${name} already exists!`)
}
let content = {};
//读取指定文件夹下(dir)的所有文件并遍历
fs.readdirSync(path).forEach(filename => {
//取出文件的后缀
let extname = Path.extname(filename);
//只处理js文件
if (extname === '.js') {
//将文件名中去掉后缀
let name = Path.basename(filename, extname);
//读取文件中的内容并赋值绑定
content[name] = require(Path.join(path, filename));
}
});
app[name] = content
})
}</code></pre>
<p><code>opts</code> 是参数对象,里面包含了实例 <code>app</code>,用来挂载指定的目录文件。<code>rules</code> 是我们指定的目录规则。 </p>
<p>用法如下,修改 <code>middleware/index.js</code>:</p>
<pre><code class="js">// 引入规则中件间
const miRule = require('./mi-rule')
module.exports = (app) => {
/**
* 在接口的开头调用
* 指定 controller 文件夹下的 js 文件,挂载在 app.controller 属性
* 指定 service 文件夹下的 js 文件,挂载在 app.service 属性
*/
miRule({
app,
rules: [
{
path: path.join(__dirname, '../controller'),
name: 'controller'
},
{
path: path.join(__dirname, '../service'),
name: 'service'
}
]
})
// 以下代码省略
}</code></pre>
<h4>业务代码应用</h4>
<h5>1. 修改 <code>router.js</code>:</h5>
<pre><code class="js">const router = require('koa-router')()
module.exports = (app) => {
router.get( '/', app.controller.home.index )
router.get('/home', app.controller.home.home)
router.get('/home/:id/:name', app.controller.home.homeParams)
router.get('/user', app.controller.home.login)
router.post('/user/register', app.controller.home.register)
app.use(router.routes()).use(router.allowedMethods())
}</code></pre>
<h5>2. 修改 <code>controller/home.js</code>:</h5>
<pre><code class="js">module.exports = {
index: async(ctx, next) => {
await ctx.render("home/index", {title: "iKcamp欢迎您"})
},
home: async(ctx, next) => {
ctx.response.body = '<h1>HOME page</h1>'
},
homeParams: async(ctx, next) => {
ctx.response.body = '<h1>HOME page /:id/:name</h1>'
},
login: async(ctx, next) => {
await ctx.render('home/login', {
btnName: 'GoGoGo'
})
},
register: async(ctx, next) => {
// 解构出 app 实例对象
const { app } = ctx
let params = ctx.request.body
let name = params.name
let password = params.password
// 留意 service 层的调用方式
let res = await app.service.home.register(name,password)
if(res.status == "-1"){
await ctx.render("home/login", res.data)
}else{
ctx.state.title = "个人中心"
await ctx.render("home/success", res.data)
}
}
}</code></pre>
<p>项目中引入这个结构规范,并不是必须的,毕竟大家的想法不一样。<code>iKcamp</code> 团队在提出此想法时候,也是有不少分歧。提出这样一个思路,仅供大家参考。</p>
<h3>开发环境运行</h3>
<p>作为后端代码语言,开发环境中每次修改文件,都需要手动的重启应用,不能像前端浏览器那样清爽。为了减轻手工重启的成本,我们建议采用 <code>nodemon</code> 来代替 <code>node</code> 以启动应用。当代码发生变化时候,<code>nodemon</code> 会帮我们自动重启。 </p>
<p>全局安装 <code>nodemon</code>:</p>
<pre><code class="js">npm i nodemon -g</code></pre>
<p>本地项目中也需要安装:</p>
<pre><code class="js">npm i nodemon -S</code></pre>
<p>更多细节用法,请查阅<a href="https://link.segmentfault.com/?enc=cafzrkRM13Gna5aSyENoiQ%3D%3D.9VHrUUzrOXhAZis05hn1n8k6fNa2Au09sSKAdJmGFULwZgsPN%2B6EziiVVTHoDNWb" rel="nofollow">官方文档</a></p>
<h3>部署运行</h3>
<p>线上部署运行的话,方法也有很多,我们推荐使用 <code>pm2</code>。 </p>
<p><code>pm2</code> 是一个带有负载均衡功能的Node应用的进程管理器。</p>
<p>安装方法与 <code>nodemon</code> 相似,需要全局安装:</p>
<pre><code class="js">npm i pm2 -g</code></pre>
<p>运行方法:</p>
<pre><code class="js">pm2 start app.js</code></pre>
<p>更多细节用法,请查阅<a href="https://link.segmentfault.com/?enc=5vQDXm2TaY4PUymPqzOCdQ%3D%3D.uV3Ee0k1jin%2Bc2Y%2F8%2B1RhY507GwfKr9deJ1ABENw%2F0c3fPjY%2Fn5JK9j6XTMIsIfxTD6F5K8XTNE0DiiKGREAaw%3D%3D" rel="nofollow">官方文档</a></p>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=9rvcb0SlJ6Zy3BmNlonaKQ%3D%3D.%2B0kXtvyxNSvm4awKAEGRWH9gyGdOtbUvByTHugAskFK4Az%2FYnJhiY2zI8kLEh9K2" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 错误处理
https://segmentfault.com/a/1190000013096182
2018-02-02T11:20:35+08:00
2018-02-02T11:20:35+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<p>沪江CCtalk视频地址:<a href="https://link.segmentfault.com/?enc=ZFabn%2FE3PyVXBGUCdCUMkA%3D%3D.fzIe%2FZhvaW2avZswFfxOT4JwHqsqsFEtB4pl5Yta7PAK6xiGlKHKn98eTXJrBMm5" rel="nofollow">https://www.cctalk.com/v/15114923887518</a></p>
<p><img src="/img/remote/1460000013096340?w=1282&h=714" alt="" title=""></p>
<h2>处理错误请求</h2>
<blockquote>爱能遮掩一切过错。</blockquote>
<p>当我们在访问一个站点的时候,如果访问的地址不存在(404),或服务器内部发生了错误(500),站点会展示出某个特定的页面,比如: </p>
<p><img src="/img/remote/1460000013096188?w=600&h=283" alt="" title=""></p>
<p>那么如何在 <code>Koa</code> 中实现这种功能呢?其实,一个简单的中间件即可实现,我们把它称为 <code>http-error</code>。实现过程并不复杂,拆分为三步来看:</p>
<ul>
<li>第一步:确认需求</li>
<li>第二步:整理思路</li>
<li>第三步:代码实现</li>
</ul>
<p><br/></p>
<h3>确认需求</h3>
<blockquote>打造一个事物前,需要先确认它具有什么特性,这就是需求。</blockquote>
<p><br/></p>
<p>在这里,稍微整理下即可得到几个基本需求:</p>
<ul>
<li>在页面请求出现 <code>400</code> 、 <code>500</code> 类错误码的时候,引导用户至错误页面;</li>
<li>提供默认错误页面;</li>
<li>允许使用者自定义错误页面。</li>
</ul>
<p><br/></p>
<h3>整理思路</h3>
<p>现在,从一个请求进入 <code>Koa</code> 开始说起:</p>
<ol>
<li>一个请求访问 <code>Koa</code>,出现了错误;</li>
<li>该错误会被 <code>http-error</code> 中间件捕捉到;</li>
<li>错误会被中间件的错误处理逻辑捕捉到,并进行处理;</li>
<li>错误处理逻辑根据错误码状态,调用渲染页面逻辑;</li>
<li>渲染页面逻辑渲染出对应的错误页面。</li>
</ol>
<p>可以看到,关键点就是捕捉错误,以及实现错误处理逻辑和渲染页面逻辑。</p>
<p><br/></p>
<h3>代码实现</h3>
<h4>建立文件</h4>
<p>基于教程目录结构,我们创建 <code>middleware/mi-http-error/index.js</code> 文件,存放中间件的逻辑代码。初始目录结构如下:</p>
<pre><code>middleware/
├─ mi-http-error/
│ └── index.js
└─ index.js</code></pre>
<p><strong>注意:</strong> 目录结构不存在,需要自己创建。</p>
<p><br/></p>
<h4>捕捉错误</h4>
<p>该中间件第一项需要实现的功能是捕捉到所有的 <code>http</code> 错误。根据中间件的洋葱模型,需要做几件事:</p>
<h5>1. 引入中间件</h5>
<p>修改 <code>middleware/index.js</code>,引入 <code>mi-http-error</code> 中间件,并将它放到洋葱模型的最外层</p>
<pre><code class="js">const path = require('path')
const ip = require("ip")
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
const staticFiles = require('koa-static')
const miSend = require('./mi-send')
const miLog = require('./mi-log')
// 引入请求错误中间件
const miHttpError = require('./mi-http-error')
module.exports = (app) => {
// 应用请求错误中间件
app.use(miHttpError())
app.use(miLog(app.env, {
env: app.env,
projectName: 'koa2-tutorial',
appLogLevel: 'debug',
dir: 'logs',
serverIp: ip.address()
}));
app.use(staticFiles(path.resolve(__dirname, "../public")))
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, '../views'),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
app.use(miSend())
}</code></pre>
<h5>2. 捕获中间件异常情况</h5>
<p>修改 <code>mi-http-error/index.js</code>,在中间件内部对内层的其它中间件进行错误监听,并对捕获 <code>catch</code> 到的错误进行处理</p>
<pre><code class="js">module.exports = () => {
return async (ctx, next) => {
try {
await next();
/**
* 如果没有更改过 response 的 status,则 koa 默认的 status 是 404
*/
if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
} catch (e) {
/*此处进行错误处理,下面会讲解具体实现*/
}
}
}</code></pre>
<p><br/></p>
<p>上面的准备工作做完,下面实现两个关键逻辑。</p>
<p><br/></p>
<h4>错误处理逻辑</h4>
<p>错误处理逻辑其实很简单,就是对错误码进行判断,并指定要渲染的文件名。这段代码运行在错误 <code>catch</code> 中。 </p>
<p>修改 <code>mi-http-error/index.js</code>:</p>
<pre><code class="js">module.exports = () => {
let fileName = 'other'
return async (ctx, next) => {
try {
await next();
/**
* 如果没有更改过 response 的 status,则 koa 默认的 status 是 404
*/
if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
} catch (e) {
let status = parseInt(e.status)
// 默认错误信息为 error 对象上携带的 message
const message = e.message
// 对 status 进行处理,指定错误页面文件名
if(status >= 400){
switch(status){
case 400:
case 404:
case 500:
fileName = status;
break;
// 其它错误 指定渲染 other 文件
default:
fileName = 'other'
}
}
}
}
}</code></pre>
<p>也就是说,对于不同的情况,会展示不同的错误页面:</p>
<pre><code class="txt">├─ 400.html
├─ 404.html
├─ 500.html
├─ other.html</code></pre>
<p>这几个页面文件我们会在后面创建,接下来我们开始讲述下页面渲染的问题。 </p>
<p><br/></p>
<h4>渲染页面逻辑</h4>
<p>首先我们创建默认的错误页面模板文件 <code>mi-http-error/error.html</code>,这里采用 <code>nunjucks</code> 语法。</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<title>Error - {{ status }}</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>Looks like something broke!</p>
{% if (env === 'development') %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html></code></pre>
<p><br/> </p>
<p>因为牵涉到文件路径的解析,我们需要引入 <code>path</code> 模块。另外,还需要引入 <code>nunjucks</code> 工具来解析模板。<code>path</code> 是 <code>node</code> 模块,我们只需从 <code>npm</code> 上安装<code>nunjucks</code> 即可。 </p>
<p><br/></p>
<p>安装 <code>nunjucks</code> 模块来解析模板文件:</p>
<pre><code class="js">npm i nunjucks -S</code></pre>
<p><br/> </p>
<p>修改 <code>mi-http-error/index.js</code>,引入 <code>path</code> 和 <code>nunjucks</code> 模块:</p>
<pre><code class="js">// 引入 path nunjucks 模块
const Path = require('path')
const nunjucks = require('nunjucks')
module.exports = () => {
// 此处代码省略,与之前一样
}</code></pre>
<p><br/> </p>
<p>为了支持自定义错误文件目录,原来调用中间件的代码需要修改一下。我们给中间件传入一个配置对象,该对象中有一个字段 <code>errorPageFolder</code>,表示自定义错误文件目录。 </p>
<p>修改 <code>middleware/index.js</code>:</p>
<pre><code class="js">// app.use(miHttpError())
app.use(miHttpError({
errorPageFolder: path.resolve(__dirname, '../errorPage')
}))</code></pre>
<p><strong>注意:</strong> 代码中,我们指定了 <code>/errorPage</code> 为默认的模板文件目录。 </p>
<p><br/> </p>
<p>修改 <code>mi-http-error/index.js</code>,处理接收到的参数:</p>
<pre><code class="js">const Path = require('path')
const nunjucks = require('nunjucks')
module.exports = (opts = {}) => {
// 400.html 404.html other.html 的存放位置
const folder = opts.errorPageFolder
// 指定默认模板文件
const templatePath = Path.resolve(__dirname, './error.html')
let fileName = 'other'
return async (ctx, next) => {
try {
await next()
if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
} catch (e) {
let status = parseInt(e.status)
const message = e.message
if(status >= 400){
switch(status){
case 400:
case 404:
case 500:
fileName = status;
break;
default:
fileName = 'other'
}
}else{// 其它情况,统一返回为 500
status = 500
fileName = status
}
// 确定最终的 filePath 路径
const filePath = folder ? Path.join(folder, `${fileName}.html`) : templatePath
}
}
}</code></pre>
<p><br/> </p>
<p>路径和参数准备好之后,我们需要做的事情就剩返回渲染的页面了。</p>
<p><br/> </p>
<p>修改 <code>mi-http-error/index.js</code>,对捕捉到的不同错误返回相应的视图页面:</p>
<pre><code class="js">const Path = require('path')
const nunjucks = require('nunjucks')
module.exports = (opts = {}) => {
// 增加环境变量,用来传入到视图中,方便调试
const env = opts.env || process.env.NODE_ENV || 'development'
const folder = opts.errorPageFolder
const templatePath = Path.resolve(__dirname, './error.html')
let fileName = 'other'
return async (ctx, next) => {
try {
await next()
if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
} catch (e) {
let status = parseInt(e.status)
const message = e.message
if(status >= 400){
switch(status){
case 400:
case 404:
case 500:
fileName = status;
break;
default:
fileName = 'other'
}
}else{
status = 500
fileName = status
}
const filePath = folder ? Path.join(folder, `${fileName}.html`) : templatePath
// 渲染对应错误类型的视图,并传入参数对象
try{
// 指定视图目录
nunjucks.configure( folder ? folder : __dirname )
const data = await nunjucks.render(filePath, {
env: env, // 指定当前环境参数
status: e.status || e.message, // 如果错误信息中没有 status,就显示为 message
error: e.message, // 错误信息
stack: e.stack // 错误的堆栈信息
})
// 赋值给响应体
ctx.status = status
ctx.body = data
}catch(e){
// 如果中间件存在错误异常,直接抛出信息,由其他中间件处理
ctx.throw(500, `错误页渲染失败:${e.message}`)
}
}
}
}</code></pre>
<p>上面所做的是使用渲染引擎对模板文件进行渲染,并将生成的内容放到 <code>Http</code> 的 <code>Response</code> 中,展示在用户面前。感兴趣的同学可以去中间件源码中查看 <code>error.html</code> 查看模板内容(其实是从 <code>koa-error</code> 那里拿来稍作修改的)。 </p>
<p><br/> </p>
<p>在代码的最后,我们还有一个异常的抛出 <code>ctx.throw()</code>,也就是说,中间件处理时候也会存在异常,所以我们需要在最外层做一个错误监听处理。 </p>
<p>修改 <code>middleware/index.js</code>:</p>
<pre><code class="js">const path = require('path')
const ip = require("ip")
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
const staticFiles = require('koa-static')
const miSend = require('./mi-send')
const miLog = require('./mi-log')
const miHttpError = require('./mi-http-error')
module.exports = (app) => {
app.use(miHttpError({
errorPageFolder: path.resolve(__dirname, '../errorPage')
}))
app.use(miLog(app.env, {
env: app.env,
projectName: 'koa2-tutorial',
appLogLevel: 'debug',
dir: 'logs',
serverIp: ip.address()
}));
app.use(staticFiles(path.resolve(__dirname, "../public")))
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, '../views'),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
app.use(miSend())
// 增加错误的监听处理
app.on("error", (err, ctx) => {
if (ctx && !ctx.headerSent && ctx.status < 500) {
ctx.status = 500
}
if (ctx && ctx.log && ctx.log.error) {
if (!ctx.state.logged) {
ctx.log.error(err.stack)
}
}
})
}</code></pre>
<p><br/> </p>
<p>下面,我们增加对应的错误渲染页面: </p>
<p>创建 <code>errorPage/400.html</code>:</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<title>400</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>错误码 400 的描述信息</p>
{% if (env === 'development') %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html></code></pre>
<p><br/> </p>
<p>创建 <code>errorPage/404.html</code>:</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<title>404</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>错误码 404 的描述信息</p>
{% if (env === 'development') %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html></code></pre>
<p><br/> </p>
<p>创建 <code>errorPage/500.html</code>:</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<title>500</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>错误码 500 的描述信息</p>
{% if (env === 'development') %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html></code></pre>
<p><br/> </p>
<p>创建 <code>errorPage/other.html</code>:</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<title>未知异常</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>未知异常</p>
{% if (env === 'development') %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html></code></pre>
<p><br/> </p>
<p><code>errorPage</code> 中的页面展示内容,可以根据自己的项目信息修改,以上仅供参考。 </p>
<p><br/> </p>
<p>至此,我们基本完成了用来处理『请求错误』的中间件。而这个中间件并不是固定的形态,大家在真实项目中,还需要多考虑自己的业务场景和需求,打造出适合自己项目的中间件。</p>
<blockquote>下一节中,我们将学习下规范与部署——制定合适的团队规范,提升开发效率。</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=G7Fo2tYCIntxOE%2BMYA%2FhVg%3D%3D.LZk%2FdeBU%2FV7BHUlpDXD76PCT038OtuQsfY7BuS5IROYv8RHjTRb6JsSP9aQBK%2F8c" rel="nofollow">iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 处理静态资源</a>
</blockquote>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=vIw6pzIBy5smHzRpFZlS%2BQ%3D%3D.4dJt64wpjmhHx64VWAQzIuWYpSa%2BFlfp57l%2BVob%2Fl66QENYlQP5SNEDPAfVY%2BzbT" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
系列3|走进Node.js之多进程模型
https://segmentfault.com/a/1190000013030634
2018-01-29T11:30:09+08:00
2018-01-29T11:30:09+08:00
iKcamp
https://segmentfault.com/u/ikcamp
4
<blockquote>文:正龙(沪江网校Web前端工程师)<p>本文原创,转载请注明作者及出处</p>
</blockquote>
<p>之前的文章“<a href="https://link.segmentfault.com/?enc=MtXIxCBI%2BLq6vJqNyLs2Ow%3D%3D.hcecMmNdRBClSCd%2B1qR%2BvwW6eA6TaOx5HJweNEKUM4w5SuQs%2FWg%2F0PuEgFfh5BAY83A9OIyp1BEfVx1AtW1CHYGfvLldDNfknbghUuo%2BTU6aqzN%2BkYDRhVqHgXlMgFDC1qNgiqPgklIYq4%2B5DCcwZH5L0qQw3m7gL0RIzfwSNgw3udPb6hhXKB2Bui7TCmrnyxbYD72uFnnynQjN6S2iuXsBPdK7Sy0tnUpSVSpd8MglWsAtHTYQkmP%2FatJFl2MuAO6H6Mk09fClIiQPwpfLAbkZlXkoBmCSL3RV%2F7%2BLL%2BflFii%2B8xT5GtXAl1ZIglte" rel="nofollow">走进Node.js之HTTP实现分析</a>”中,大家已经了解 Node.js 是如何处理 HTTP 请求的,在整个处理过程,它仅仅用到单进程模型。那么如何让 Web 应用扩展到多进程模型,以便充分利用CPU资源呢?答案就是 Cluster。本篇文章将带着大家一起分析Node.js的多进程模型。</p>
<p>首先,来一段经典的 Node.js 主从服务模型代码:</p>
<pre><code class="javascript">const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
require('http').createServer((req, res) => {
res.end('hello world');
}).listen(3333);
}</code></pre>
<p>通常,主从模型包含一个主进程(master)和多个从进程(worker),主进程负责接收连接请求,以及把单个的请求任务分发给从进程处理;从进程的职责就是不断响应客户端请求,直至进入等待状态。如图 3-1 所示:</p>
<p><img src="/img/remote/1460000013030639?w=1090&h=360" alt="" title=""></p>
<p>围绕这段代码,本文希望讲述清楚几个关键问题:</p>
<ol>
<li>从进程的创建过程;</li>
<li>在使用同一主机地址的前提下,如果指定端口已经被监听,其它进程尝试监听同一端口时本应该会报错(EADDRINUSE,即端口已被占用);那么,Node.js 如何能够在主从进程上对同一端口执行 listen 方法?</li>
</ol>
<h2>进程 fork 是如何完成的?</h2>
<p>在 Node.js 中,<a href="https://link.segmentfault.com/?enc=qNNsKkt%2FGmA7kyMGx5CFUA%3D%3D.xFzr%2FjbBbu4r0i%2FKxjBYIqz6EfPQYPCmIQASgjgLIiuf6MFRbTAp3gI8l3ORHwO1s9e%2BMmW%2FkZP0bwah7mI7sX4EcE6b5DiMJREBln%2FiJJyPKUCtBgvKoN5GOleG0oXL" rel="nofollow">cluster.fork</a> 与 POSIX 的 <a href="https://link.segmentfault.com/?enc=c1aOBr5xcuh%2FUOL4VYC3ug%3D%3D.nkCjkLa8iMVyNcU9eEd1az%2FEWC68G%2FIbY5sHa%2Brevr31AcfhZqehpaFeMTlbrf%2Bg" rel="nofollow">fork</a> 略有不同:虽然从进程仍旧是 fork 创建,但是并不会直接使用主进程的进程映像,而是调用系统函数 <a href="https://link.segmentfault.com/?enc=AfKbZvLUFS7l01wnhC%2B7HA%3D%3D.%2ByeXbx3JcdJkakd%2FA9x%2BGsh5vTiA%2B9doqCpcxSev3D6tVjvBR1vB%2FfN2m7yCL0f9" rel="nofollow">execvp</a> 让从进程使用新的进程映像。另外,每个从进程对应一个 Worker 对象,它有如下状态:none、online、listening、dead和disconnected。</p>
<p>ChildProcess 对象主要提供进程的创建(spawn)、销毁(kill)以及进程句柄引用计数管理(ref 与 unref)。在对Process对象(process_wrap.cc)进行封装之外,它自身也处理了一些细节问题。例如,在方法 spawn 中,如果需要主从进程之间建立 IPC 管道,则通过环境变量 NODE_CHANNEL_FD 来告知从进程应该绑定的 IPC 相关的文件描述符(fd),这个特殊的环境变量后面会被再次涉及到。</p>
<p>以上提到的三个对象引用关系如下:</p>
<p><img src="/img/remote/1460000013030640?w=950&h=371" alt="" title=""></p>
<p>cluster.fork 的主要执行流程:</p>
<ol>
<li>调用 child_process.spawn;</li>
<li>
<p>创建 ChildProcess 对象,并初始化其 _handle 属性为 Process 对象;Process 是 process_wrap.cc 中公布给 JavaScript 的对象,它封装了 libuv 的进程操纵功能。附上 Process 对象的 C++ 定义:</p>
<pre><code class="c++">interface Process {
construtor(const FunctionCallbackInfo<Value>& args);
void close(const FunctionCallbackInfo<Value>& args);
void spawn(const FunctionCallbackInfo<Value>& args);
void kill(const FunctionCallbackInfo<Value>& args);
void ref(const FunctionCallbackInfo<Value>& args);
void unref(const FunctionCallbackInfo<Value>& args);
void hasRef(const FunctionCallbackInfo<Value>& args);
}</code></pre>
</li>
<li>调用 ChildProcess._handle 的方法 spawn,并会最终调用 libuv 库中 <a href="https://link.segmentfault.com/?enc=7vwWC2MRem%2F04vxThZywaA%3D%3D.gB8HqGPxU6WyN2iLYMTobKdFnzPDLmD%2FNKBebjSDm6B%2BF4I6Wtk0OyfRBi0rzovG" rel="nofollow">uv_spawn</a>。</li>
</ol>
<p>主进程在执行 cluster.fork 时,会指定两个特殊的环境变量 NODE_CHANNEL_FD 和 NODE_UNIQUE_ID,所以从进程的初始化过程跟一般 Node.js 进程略有不同:</p>
<ol>
<li>bootstrap_node.js 是运行时包含的 JavaScript 入口文件,其中调用 internal\process.setupChannel;</li>
<li>如果环境变量包含 NODE_CHANNEL_FD,则调用 child_process._forkChild,然后移除该值;</li>
<li>调用 internal\child_process.setupChannel,在子进程的全局 process 对象上监听消息 internalMessage,并且添加方法 send 和 _send。其中 send 只是对 _send 的封装;通常,_send 只是把消息 JSON 序列化之后写入管道,并最终投递到接收端。</li>
<li>如果环境变量包含 NODE_UNIQUE_ID,则当前进程是 worker 模式,加载 cluster 模块时会执行 workerInit;另外,它也会影响到 net.Server 的 listen 方法,worker 模式下 listen 方法会调用 cluster._getServer,该方法实质上向主进程发起消息 {"act" : "queryServer"},而不是真正监听端口。</li>
</ol>
<h2>IPC实现细节</h2>
<p>上文提到了 Node.js 主从进程仅仅通过 IPC 维持联络,那这一节就来深入分析下 IPC 的实现细节。首先,让我们看一段示例代码:</p>
<p><strong>1-master.js</strong></p>
<pre><code class="javascript">const {spawn} = require('child_process');
let child = spawn(process.execPath, [`${__dirname}/1-slave.js`], {
stdio: [0, 1, 2, 'ipc']
});
child.on('message', function(data) {
console.log('received in master:');
console.log(data);
});
child.send({
msg: 'msg from master'
});</code></pre>
<p><strong>1-slave.js</strong></p>
<pre><code class="javascript">process.on('message', function(data) {
console.log('received in slave:');
console.log(data);
});
process.send({
'msg': 'message from slave'
});</code></pre>
<pre><code>node 1-master.js</code></pre>
<p>运行结果如下:</p>
<p><img src="/img/remote/1460000013030641?w=633&h=146" alt="" title=""></p>
<p>细心的同学可能发现控制台输出并不是连续的,master和slave的日志交错打印,这是由于并行进程执行顺序不可预知造成的。</p>
<h3>socketpair</h3>
<p>前文提到从进程实际上通过系统调用 execvp 启动新的 Node.js 实例;也就是说默认情况下,Node.js 主从进程不会共享文件描述符表,那它们到底是如何互发消息的呢?</p>
<p>原来,可以利用 <a href="https://link.segmentfault.com/?enc=gv6VPsls7DaFaYKLFnfh0w%3D%3D.a1CptPNFjRnW4LrYRoq0MEJ289OjWDXIteXhLKyYIUkQrqUxe%2FnLz%2BwPLnJwvty7" rel="nofollow">socketpair</a> 创建一对全双工匿名 socket,用于在进程间互发消息;其函数签名如下:</p>
<pre><code class="c">int socketpair(int domain, int type, int protocol, int sv[2]);</code></pre>
<p>通常情况下,我们是无法通过 socket 来传递文件描述符的;当主进程与客户端建立了连接,需要把连接描述符告知从进程处理,怎么办?其实,通过指定 socketpair 的第一个参数为 AF_UNIX,表示创建匿名 UNIX 域套接字(UNIX domain socket),这样就可以使用系统函数 <a href="https://link.segmentfault.com/?enc=zoHgDzJkadwZxrDcJ1MNFg%3D%3D.m7xw3jX2VmVSyEtEsDgzJJmTmx1jAFj3PaGvU0yP3%2FUxFFm9jSJfl6H5bsIAK6fn" rel="nofollow">sendmsg</a> 和 <a href="https://link.segmentfault.com/?enc=caSrZAnv%2FZQbv1Nf8SzChg%3D%3D.wwAZAMkNkvE48T7LE%2BwofsaEfnm3zcRz4%2FYy0SJA0U45NIlpXul%2BeOQjxUre3grv" rel="nofollow">recvmsg</a> 来传递/接收文件描述符了。</p>
<p>主进程在调用 cluster.fork 时,相关流程如下:</p>
<ol>
<li>创建 Pipe(pipe_wrap.cc)对象,并且指定参数 ipc 为 true;</li>
<li>调用 uv_spawn,options 参数为 uv_process_options_s 结构体,把 Pipe 对象存储在结构体的属性 stdio 中;</li>
<li>调用 uv__process_init_stdio,通过 socketpair 创建全双工 socket;</li>
<li>调用 uv__process_open_stream,设置 Pipe 对象的 iowatcher.fd 值为全双工 socket 之一。</li>
</ol>
<p>至此,主从进程就可以进行双向通信了。流程图如下:</p>
<p><img src="/img/remote/1460000013030642?w=1201&h=899" alt="" title=""></p>
<p>我们再回看一下环境变量 NODE_CHANNEL_FD,令人疑惑的是,它的值始终为3。进程级文件描述符表中,0-2分别是标准输入stdin、标准输出stdout和标准错误输出stderr,那么可用的第一个文件描述符就是3,socketpair 显然会占用从进程的第一个可用文件描述符。这样,当从进程往 fd=3 的流中写入数据时,主进程就可以收到消息;反之,亦类似。</p>
<p><img src="/img/remote/1460000013030643?w=805&h=602" alt="" title=""></p>
<p>从 IPC 读取消息主要是流操作,以后有机会详解,下面列出主要流程:</p>
<ol>
<li>StreamBase::EditData 回调 onread;</li>
<li>StreamWrap::OnReadImpl 调用 StreamWrap::EditData;</li>
<li>StreamWrap 的构造函数会调用 set_read_cb 设置 OnReadImpl;</li>
<li>StreamWrap::set_read_cb 设置属性 StreamWrap::read_cb_;</li>
<li>StreamWrap::OnRead 中引用属性 read_cb_;</li>
<li>StreamWrap::ReadStart 调用 uv_read_start 时传递 Streamwrap::OnRead 作为第3个参数:</li>
</ol>
<pre><code class="c">int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb)</code></pre>
<p>涉及到的类图关系如下:</p>
<p><img src="/img/remote/1460000013030644?w=505&h=570" alt="" title=""></p>
<h2>服务器主从模型</h2>
<p>以上大概分析了从进程的创建过程及其特殊性;如果要实现主从服务模型的话,还需要解决一个基本问题:从进程怎么获取到与客户端间的连接描述符?我们打算从 process.send(只有在从进程的全局 process 对象上才有 send 方法,主进程可以通过 worker.process 或 worker 访问该方法)的函数签名着手:</p>
<pre><code class="javascript">void send(message, sendHandle, callback)</code></pre>
<p>其参数 message 和 callback 含义也许显而易见,分别指待发送的消息对象和操作结束之后的回调函数。那它的第二个参数 sendHandle 用途是什么?</p>
<p>前文提到系统函数 socketpair 可以创建一对双向 socket,能够用来发送 JSON 消息,这一块主要涉及到流操作;另外,当 sendHandle 有值时,它们还可以用于传递文件描述符,其过程要相对复杂一些,但是最终会调用系统函数 sendmsg 以及 recvmsg。</p>
<h3>传递与客户端的连接描述符</h3>
<p>在主从服务模型下,主进程负责跟客户端建立连接,然后把连接描述符通过 <a href="https://link.segmentfault.com/?enc=DmYpYco7lwHrmRI2bE4mrw%3D%3D.dog4GQbVdqlXVvF9j9DIgNxkhbo1OqrzRNHQZ0981tgMWs28sCXOq9Iy6NtPxavL" rel="nofollow">sendmsg</a> 传递给从进程。我们来看看这一过程:</p>
<p><strong>从进程</strong></p>
<ol>
<li>调用 http.Server.listen 方法(继承至 net.Server);</li>
<li>
<p>调用 cluster._getServer,向主进程发起消息:</p>
<pre><code class="json">{
"cmd": "NODE_HANDLE",
"msg": {
"act": "queryServer"
}
}</code></pre>
</li>
</ol>
<p><strong>主进程</strong></p>
<ol>
<li>
<p>接收处理这个消息时,会新建一个 RoundRobinHandle 对象,为变量 handle。每个 handle 与一个连接端点对应,并且对应多个从进程实例;同时,它会开启与连接端点相应的 TCP 服务 socket。</p>
<pre><code class="js">class RoundRobinHandle {
construtor(key, address, port, addressType, fd) {
// 监听同一端点的从进程集合
this.all = [];
// 可用的从进程集合
this.free = [];
// 当前等待处理的客户端连接描述符集合
this.handles = [];
// 指定端点的TCP服务socket
this.server = null;
}
add(worker, send) {
// 把从进程实例加入this.all
}
remove(worker) {
// 移除指定从进程
}
distribute(err, handle) {
// 把连接描述符handle存入this.handles,并指派一个可用的从进程实例开始处理连接请求
}
handoff(worker) {
// 从this.handles中取出一个待处理的连接描述符,并向从进程发起消息
// {
// "type": "NODE_HANDLE",
// "msg": {
// "act": "newconn",
// }
// }
}
}</code></pre>
</li>
<li>调用 handle.add 方法,把 worker 对象添加到 handle.all 集合中;</li>
<li>当 handle.server 开始监听客户端请求之后,重置其 onconnection 回调函数为 RoundRobinHandle.distribute,这样的话主进程就不用实际处理客户端连接,只要分发连接给从进程处理即可。它会把连接描述符存入 handle.handles 集合,当有可用 worker 时,则向其发送消息 { "act": "newconn" }。如果被指派的 worker 没有回复确认消息 { "ack": message.seq, accepted: true },则会尝试把该连接分配给其他 worker。</li>
</ol>
<p>流程图如下:</p>
<p><em>从进程上调用listen</em></p>
<p><img src="/img/remote/1460000013030645?w=987&h=705" alt="" title=""></p>
<p><em>客户端连接处理</em></p>
<p><img src="/img/remote/1460000013030646?w=981&h=392" alt="" title=""></p>
<h3>从进程如何与主进程监听同一端口?</h3>
<p>原因主要有两点:</p>
<p><strong> I. 从进程中 Node.js 运行时的初始化略有不同</strong></p>
<ol>
<li>因为从进程存在环境变量 NODE_UNIQUE_ID,所以在 bootstrap_node.js 中,加载 cluster 模块时执行 workerInit 方法。这个地方与主进程执行的 masterInit 方法不同点在于:其一,从进程上没有 cluster.fork 方法,所以不能在从进程继续创建子孙进程;其二,Worker 对象上的方法 disconnect 和 destroy 实现也有所差异:我们以调用 worker.destroy 为例,在主进程上时,不能直接把从进程杀掉,而是通知从进程退出,然后再把它从集合里删除;当在从进程上时,从进程通知完主进程然后退出就可以了;其三,从进程上 cluster 模块新增了方法 _getServer,用于向主进程发起消息 {"act": "queryServer"},通知主进程创建 RoundRobinHandle 对象,并实际监听指定端口地址;然后自身用一个模拟的 TCP 描述符继续执行;</li>
<li>调用 cluster._setupWorker 方法,主要是初始化 cluster.worker 属性,并监听消息 <strong>internalMessage</strong>,处理两种消息类型:newconn 和 disconnect;</li>
<li>向主进程发起消息 { "act": "online" };</li>
<li>因为从进程额环境变量中有 NODE_CHANNEL_FD,调用 internal\process.setupChannel时,会连接到系统函数 socketpair 创建的双向 socket ,并监听 <strong>internalMessage</strong> ,处理消息类型:NODE_HANDLE_ACK和NODE_HANDLE。</li>
</ol>
<p><strong> II. listen 方法在主从进程中执行的代码略有不同。</strong></p>
<p>在 net.Server(net.js)的方法 listen 中,如果是主进程,则执行标准的端口绑定流程;如果是从进程,则会调用 cluster._getServer,参见上面对该方法的描述。</p>
<p>最后,附上基于libuv实现的一个 C 版 Master-Slave 服务模型,<a href="https://link.segmentfault.com/?enc=7zU5MT2E0lZF7vJhDT7FTw%3D%3D.OaxYxwRLy6o0lbIlXKjF6jG5EOkq1NLU4yR0jnvvIjY%2F1cB7s1ZUo%2B2BiP%2Bw%2BwQ6mUOI4aaWdGEIjY%2Bv0F5LPg%3D%3D" rel="nofollow">GitHub地址</a>。</p>
<p>启动服务器之后,访问 <a href="https://link.segmentfault.com/?enc=iBbiq8HaLVOV0V0SuYMMHw%3D%3D.FDW5ORR%2BLfaWZM2ArdtmAo1tQjilKmxtccDWQ%2BOmhT4%3D" rel="nofollow">http://localhost</a>:3333 的运行结果如下:</p>
<p><img src="/img/remote/1460000013030647?w=825&h=363" alt="" title=""></p>
<p>相信通过本篇文章的介绍,大家已经对Node.js的Cluster有了一个全面的了解。下一次作者会跟大家一起深入分析Node.js进程管理在生产环境下的可用性问题,敬请期待。</p>
<h3>相关文章</h3>
<p><a href="https://link.segmentfault.com/?enc=zxblMHTHlXjNoMHdoFIpoQ%3D%3D.fZI0jpx7Fa5BHVAzzaDm0PM%2FvZxrLAKJdLeKEBlbo%2B0Kx52deQAjkSDqiwkumX9l" rel="nofollow">系列1|走进Node.js之启动过程剖析</a></p>
<p><a href="https://link.segmentfault.com/?enc=j4YtLUUhnF1otR0GzNWkcw%3D%3D.ic4W2Ood2qN%2B1nckUCYM%2FAUd%2BVeVDDU%2FkGIGbutQgpfQrWjWkzK%2FvBJBEc7rpg21" rel="nofollow">系列2|走进Node.js 之 HTTP实现分析</a></p>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=FmTlciyazeDOy12tTA0dkg%3D%3D.kEldXl6srg%2BFsfbVvuvXh4JOWgNmsCnZgZJHnDbe7MoUwYG1OFhWW0ZpFVnq6cgI" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
<h4>3. <a href="https://link.segmentfault.com/?enc=bkYJqmuim9v3n%2Fe4hzqxcw%3D%3D.BBeKMExNC8%2BdD%2FDc0xzGKaVluQ975fxS6DLxYq5vnxKa8ejyk%2Bczzk7ocuZCZFI0" rel="nofollow">开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍</a>
</h4>
手把手教你撸一个 Webpack Loader
https://segmentfault.com/a/1190000012990122
2018-01-25T15:46:53+08:00
2018-01-25T15:46:53+08:00
iKcamp
https://segmentfault.com/u/ikcamp
9
<blockquote>文:小 boy(沪江网校Web前端工程师)<p>本文原创,转载请注明作者及出处</p>
</blockquote>
<p><img src="/img/remote/1460000012990131?w=1083&h=420" alt="webpack" title="webpack"></p>
<p>经常逛 webpack 官网的同学应该会很眼熟上面的图。正如它宣传的一样,webpack 能把左侧各种类型的文件(webpack 把它们叫作「模块」)统一打包为右边被通用浏览器支持的文件。webpack 就像是魔术师的帽子,放进去一条丝巾,变出来一只白鸽。那这个「魔术」的过程是如何实现的呢?今天我们从 webpack 的核心概念之一 —— loader 来寻找答案,并着手实现这个「魔术」。看完本文,你可以:</p>
<ul>
<li>知道 webpack loader 的作用和原理。</li>
<li>自己开发贴合业务需求的 loader。</li>
</ul>
<h2>什么是 Loader ?</h2>
<p>在撸一个 loader 前,我们需要先知道它到底是什么。本质上来说,loader 就是一个 node 模块,这很符合 webpack 中「万物皆模块」的思路。既然是 node 模块,那就一定会导出点什么。在 webpack 的定义中,loader 导出一个函数,loader 会在转换源模块(resource)的时候调用该函数。在这个函数内部,我们可以通过传入 <code>this</code> 上下文给 Loader API 来使用它们。回顾一下头图左边的那些模块,他们就是所谓的源模块,会被 loader 转化为右边的通用文件,因此我们也可以概括一下 loader 的功能:把源模块转换成通用模块。</p>
<h2>Loader 怎么用 ?</h2>
<p>知道它的强大功能以后,我们要怎么使用 loader 呢?</p>
<h3>1. 配置 webpack config 文件</h3>
<p>既然 loader 是 webpack 模块,如果我们要使其生效,肯定离不开配置。我这里收集了三种配置方法,任你挑选。</p>
<h4>单个 loader 的配置</h4>
<p>增加 <code>config.module.rules</code> 数组中的规则对象(rule object)。</p>
<pre><code class="js">let webpackConfig = {
//...
module: {
rules: [{
test: /\.js$/,
use: [{
//这里写 loader 的路径
loader: path.resolve(__dirname, 'loaders/a-loader.js'),
options: {/* ... */}
}]
}]
}
}</code></pre>
<h4>多个 loader 的配置</h4>
<p>增加 <code>config.module.rules</code> 数组中的规则对象以及 <code>config.resolveLoader</code>。</p>
<pre><code class="js">let webpackConfig = {
//...
module: {
rules: [{
test: /\.js$/,
use: [{
//这里写 loader 名即可
loader: 'a-loader',
options: {/* ... */}
}, {
loader: 'b-loader',
options: {/* ... */}
}]
}]
},
resolveLoader: {
// 告诉 webpack 该去那个目录下找 loader 模块
modules: ['node_modules', path.resolve(__dirname, 'loaders')]
}
}</code></pre>
<h4>其他配置</h4>
<p>也可以通过 <code>npm link</code> 连接到你的项目里,这个方式类似 node CLI 工具开发,非 loader 模块专用,本文就不多讨论了。</p>
<h3>2. 简单上手</h3>
<p>配置完成后,当你在 webpack 项目中引入模块时,匹配到 rule (例如上面的 <code>/\.js$/</code>)就会启用对应的 loader (例如上面的 a-loader 和 b-loader)。这时,假设我们是 a-loader 的开发者,a-loader 会导出一个函数,这个函数接受的唯一参数是一个包含源文件内容的字符串。我们暂且称它为「source」。</p>
<p>接着我们在函数中处理 source 的转化,最终返回处理好的值。当然返回值的数量和返回方式依据 a-loader 的需求来定。一般情况下可以通过 <code>return</code> 返回一个值,也就是转化后的值。如果需要返回多个参数,则须调用 <code>this.callback(err, values...)</code> 来返回。在异步 loader 中你可以通过抛错来处理异常情况。Webpack 建议我们返回 1 至 2 个参数,第一个参数是转化后的 source,可以是 string 或 buffer。第二个参数可选,是用来当作 SourceMap 的对象。</p>
<h3>3. 进阶使用</h3>
<p>通常我们处理一类源文件的时候,单一的 loader是不够用的(loader 的设计原则我们稍后讲到)。一般我们会将多个 loader 串联使用,类似工厂流水线,一个位置的工人(或机器)只干一种类型的活。既然是串联,那肯定有顺序的问题,webpack 规定 use 数组中 loader 的执行顺序是从最后一个到第一个,它们符合下面这些规则:</p>
<ul>
<li>顺序最后的 loader 第一个被调用,它拿到的参数是 source 的内容</li>
<li>顺序第一的 loader 最后被调用, webpack 期望它返回 JS 代码,source map 如前面所说是可选的返回值。</li>
<li>夹在中间的 loader 被链式调用,他们拿到上个 loader 的返回值,为下一个 loader 提供输入。</li>
</ul>
<p>我们举个例子:</p>
<p>webpack.config.js</p>
<pre><code class="js"> {
test: /\.js/,
use: [
'bar-loader',
'mid-loader',
'foo-loader'
]
}</code></pre>
<p>在上面的配置中:</p>
<ul>
<li>loader 的调用顺序是 foo-loader -> mid-loader -> bar-loader。</li>
<li>foo-loader 拿到 source,处理后把 JS 代码传递给 mid,mid 拿到 foo 处理过的 “source” ,再处理之后给 bar,bar 处理完后再交给 webpack。</li>
<li>bar-loader 最终把返回值和 source map 传给 webpack。</li>
</ul>
<h2>用正确的姿势开发 Loader</h2>
<p>了解了基本模式后,我们先不急着开发。所谓磨刀不误砍柴工,我们先看看开发一个 loader 需要注意些什么,这样可以少走弯路,提高开发质量。下面是 webpack 提供的几点指南,它们按重要程度排序,注意其中有些点只适用特定情况。</p>
<h3>1.单一职责</h3>
<p>一个 loader 只做一件事,这样不仅可以让 loader 的维护变得简单,还能让 loader 以不同的串联方式组合出符合场景需求的搭配。</p>
<h3>2.链式组合</h3>
<p>这一点是第一点的延伸。好好利用 loader 的链式组合的特型,可以收获意想不到的效果。具体来说,写一个能一次干 5 件事情的 loader ,不如细分成 5 个只能干一件事情的 loader,也许其中几个能用在其他你暂时还没想到的场景。下面我们来举个例子。</p>
<p>假设现在我们要实现通过 loader 的配置和 query 参数来渲染模版的功能。我们在 “apply-loader” 里面实现这个功能,它负责编译源模版,最终输出一个导出 HTML 字符串的模块。根据链式组合的规则,我们可以结合另外两个开源 loader:</p>
<ul>
<li>
<code>jade-loader</code> 把模版源文件转化为导出一个函数的模块。</li>
<li>
<code>apply-loader</code> 把 loader options 传给上面的函数并执行,返回 HTML 文本。</li>
<li>
<code>html-loader</code> 接收 HTMl 文本文件,转化为可被引用的 JS 模块。</li>
</ul>
<blockquote>事实上串联组合中的 loader 并不一定要返回 JS 代码。只要下游的 loader 能有效处理上游 loader 的输出,那么上游的 loader 可以返回任意类型的模块。</blockquote>
<h3>3.模块化</h3>
<p>保证 loader 是模块化的。loader 生成模块需要遵循和普通模块一样的设计原则。</p>
<h3>4.无状态</h3>
<p>在多次模块的转化之间,我们不应该在 loader 中保留状态。每个 loader 运行时应该确保与其他编译好的模块保持独立,同样也应该与前几个 loader 对相同模块的编译结果保持独立。</p>
<h3>5.使用 Loader 实用工具</h3>
<p>请好好利用 <code>loader-utils</code> 包,它提供了很多有用的工具,最常用的一个就是获取传入 loader 的 options。除了 <code>loader-utils</code> 之外包还有 <code>schema-utils</code> 包,我们可以用 <code>schema-utils</code> 提供的工具,获取用于校验 options 的 JSON Schema 常量,从而校验 loader options。下面给出的例子简要地结合了上面提到的两个工具包:</p>
<pre><code class="js">import { getOptions } from 'loader-utils';
import { validateOptions } from 'schema-utils';
const schema = {
type: object,
properties: {
test: {
type: string
}
}
}
export default function(source) {
const options = getOptions(this);
validateOptions(schema, options, 'Example Loader');
// 在这里写转换 source 的逻辑 ...
return `export default ${ JSON.stringify(source) }`;
};
</code></pre>
<h3>loader 的依赖</h3>
<p>如果我们在 loader 中用到了外部资源(也就是从文件系统中读取的资源),我们必须声明这些外部资源的信息。这些信息用于在监控模式(watch mode)下验证可缓存的 loder 以及重新编译。下面这个例子简要地说明了怎么使用 <code>addDependency</code> 方法来做到上面说的事情。<br>loader.js:</p>
<pre><code class="js">import path from 'path';
export default function(source) {
var callback = this.async();
var headerPath = path.resolve('header.js');
this.addDependency(headerPath);
fs.readFile(headerPath, 'utf-8', function(err, header) {
if(err) return callback(err);
//这里的 callback 相当于异步版的 return
callback(null, header + "\n" + source);
});
};</code></pre>
<h3>模块依赖</h3>
<p>不同的模块会以不同的形式指定依赖。比如在 CSS 中我们使用 <code>@import</code> 和 <code>url(...)</code> 声明来完成指定,而我们应该让模块系统解析这些依赖。</p>
<p>如何让模块系统解析不同声明方式的依赖呢?下面有两种方法:</p>
<ul>
<li>把不同的依赖声明统一转化为 <code>require</code> 声明。</li>
<li>通过 <code>this.resolve</code> 函数来解析路径。</li>
</ul>
<p>对于第一种方式,有一个很好的例子就是 <code>css-loader</code>。它把 <code>@import</code> 声明转化为 <code>require</code> 样式表文件,把 <code>url(...)</code> 声明转化为 <code>require</code> 被引用文件。</p>
<p>而对于第二种方式,则需要参考一下 <code>less-loader</code>。由于要追踪 less 中的变量和 mixin,我们需要把所有的 <code>.less</code> 文件一次编译完毕,所以不能把每个 <code>@import</code> 转为 <code>require</code>。因此,<code>less-loader</code> 用自定义路径解析逻辑拓展了 less 编译器。这种方式运用了我们刚才提到的第二种方式 —— <code>this.resolve</code> 通过 webpack 来解析依赖。</p>
<blockquote>如果某种语言只支持相对路径(例如 <code>url(file)</code> 指向 <code>./file</code>)。你可以用 <code>~</code> 将相对路径指向某个已经安装好的目录(例如 <code>node_modules</code>)下,因此,拿 <code>url</code> 举例,它看起来会变成这样:<code>url(~some-library/image.jpg)</code>。</blockquote>
<h3>代码公用</h3>
<p>避免在多个 loader 里面初始化同样的代码,请把这些共用代码提取到一个运行时文件里,然后通过 <code>require</code> 把它引进每个 loader。</p>
<h3>绝对路径</h3>
<p>不要在 loader 模块里写绝对路径,因为当项目根路径变了,这些路径会干扰 webpack 计算 hash(把 module 的路径转化为 module 的引用 id)。<code>loader-utils</code> 里有一个 <code>stringifyRequest</code> 方法,它可以把绝对路径转化为相对路径。</p>
<h3>同伴依赖</h3>
<p>如果你开发的 loader 只是简单包装另外一个包,那么你应该在 package.json 中将这个包设为同伴依赖(peerDependency)。这可以让应用开发者知道该指定哪个具体的版本。<br>举个例子,如下所示 <code>sass-loader</code> 将 <code>node-sass</code> 指定为同伴依赖:</p>
<pre><code class="json">"peerDependencies": {
"node-sass": "^4.0.0"
}</code></pre>
<h2>Talk is cheep</h2>
<p>以上我们已经为砍柴磨好了刀,接下来,我们动手开发一个 loader。</p>
<p>如果我们要在项目开发中引用模版文件,那么压缩 html 是十分常见的需求。分解以上需求,解析模版、压缩模版其实可以拆分给两给 loader 来做(单一职责),前者较为复杂,我们就引入开源包 <code>html-loader</code>,而后者,我们就拿来练手。首先,我们给它取个响亮的名字 —— <code>html-minify-loader</code>。</p>
<p>接下来,按照之前介绍的步骤,首先,我们应该配置 <code>webpack.config.js</code> ,让 webpack 能识别我们的 loader。当然,最最开始,我们要创建 loader 的 文件 —— <code>src/loaders/html-minify-loader.js</code>。</p>
<p>于是,我们在配置文件中这样处理:<br><code>webpack.config.js</code></p>
<pre><code class="js">module: {
rules: [{
test: /\.html$/,
use: ['html-loader', 'html-minify-loader'] // 处理顺序 html-minify-loader => html-loader => webpack
}]
},
resolveLoader: {
// 因为 html-loader 是开源 npm 包,所以这里要添加 'node_modules' 目录
modules: [path.join(__dirname, './src/loaders'), 'node_modules']
}</code></pre>
<p>接下来,我们提供示例 html 和 js 来测试 loader:</p>
<p><code>src/example.html</code>:</p>
<pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
</body>
</html></code></pre>
<p><code>src/app.js</code>:</p>
<pre><code class="js">var html = require('./expamle.html');
console.log(html);</code></pre>
<p>好了,现在我们着手处理 <code>src/loaders/html-minify-loader.js</code>。前面我们说过,loader 也是一个 node 模块,它导出一个函数,该函数的参数是 require 的源模块,处理 source 后把返回值交给下一个 loader。所以它的 “模版” 应该是这样的:</p>
<pre><code class="js">module.exports = function (source) {
// 处理 source ...
return handledSource;
}</code></pre>
<p>或</p>
<pre><code class="js">module.exports = function (source) {
// 处理 source ...
this.callback(null, handledSource)
return handledSource;
}</code></pre>
<blockquote>注意:如果是处理顺序排在最后一个的 loader,那么它的返回值将最终交给 webpack 的 <code>require</code>,换句话说,它一定是一段可执行的 JS 脚本 (用字符串来存储),更准确来说,是一个 node 模块的 JS 脚本,我们来看下面的例子。</blockquote>
<pre><code class="js">// 处理顺序排在最后的 loader
module.exports = function (source) {
// 这个 loader 的功能是把源模块转化为字符串交给 require 的调用方
return 'module.exports = ' + JSON.stringify(source);
}</code></pre>
<p>整个过程相当于这个 loader 把源文件</p>
<pre><code class="txt">这里是 source 模块</code></pre>
<p>转化为</p>
<pre><code class="js">// example.js
module.exports = '这里是 source 模块';</code></pre>
<p>然后交给 require 调用方:</p>
<pre><code class="js">// applySomeModule.js
var source = require('example.js');
console.log(source); // 这里是 source 模块</code></pre>
<p>而我们本次串联的两个 loader 中,解析 html 、转化为 JS 执行脚本的任务已经交给 <code>html-loader</code> 了,我们来处理 html 压缩问题。</p>
<p>作为普通 node 模块的 loader 可以轻而易举地引用第三方库。我们使用 <code>minimize</code> 这个库来完成核心的压缩功能:</p>
<pre><code class="js">// src/loaders/html-minify-loader.js
var Minimize = require('minimize');
module.exports = function(source) {
var minimize = new Minimize();
return minimize.parse(source);
};</code></pre>
<p>当然, minimize 库支持一系列的压缩参数,比如 comments 参数指定是否需要保留注释。我们肯定不能在 loader 里写死这些配置。那么 <code>loader-utils</code> 就该发挥作用了:</p>
<pre><code class="js">// src/loaders/html-minify-loader.js
var loaderUtils = require('loader-utils');
var Minimize = require('minimize');
module.exports = function(source) {
var options = loaderUtils.getOptions(this) || {}; //这里拿到 webpack.config.js 的 loader 配置
var minimize = new Minimize(options);
return minimize.parse(source);
};</code></pre>
<p>这样,我们可以在 webpack.config.js 中设置压缩后是否需要保留注释:</p>
<pre><code class="js"> module: {
rules: [{
test: /\.html$/,
use: ['html-loader', {
loader: 'html-minify-loader',
options: {
comments: false
}
}]
}]
},
resolveLoader: {
// 因为 html-loader 是开源 npm 包,所以这里要添加 'node_modules' 目录
modules: [path.join(__dirname, './src/loaders'), 'node_modules']
}</code></pre>
<p>当然,你还可以把我们的 loader 写成异步的方式,这样不会阻塞其他编译进度:</p>
<pre><code class="js">var Minimize = require('minimize');
var loaderUtils = require('loader-utils');
module.exports = function(source) {
var callback = this.async();
if (this.cacheable) {
this.cacheable();
}
var opts = loaderUtils.getOptions(this) || {};
var minimize = new Minimize(opts);
minimize.parse(source, callback);
};
</code></pre>
<p>你可以在<a href="https://link.segmentfault.com/?enc=w84n4H8bp459f4iPMtRYSg%3D%3D.FMyIjPnQE6FAfulq894bWGKF7fCoHF3zGXG1ctwBlLRp9WIgjXIzNIoTd5ARMTkln4DJK8%2B21DtjoKkAtQja4g%3D%3D" rel="nofollow">这个仓库</a>查看相关代码,<code>npm start</code> 以后可以去 <code>http://localhost:9000</code> 打开控制台查看 loader 处理后的内容。</p>
<h2>总结</h2>
<p>到这里,对于「如何开发一个 loader」,我相信你已经有了自己的答案。总结一下,一个 loader 在我们项目中 work 需要经历以下步骤:</p>
<ul>
<li>创建 loader 的目录及模块文件</li>
<li>在 webpack 中配置 rule 及 loader 的解析路径,并且要注意 loader 的顺序,这样在 <code>require</code> 指定类型文件时,我们能让处理流经过指定 laoder。</li>
<li>遵循原则设计和开发 loader。</li>
</ul>
<p>最后,Talk is cheep,赶紧动手撸一个 loader 耍耍吧~</p>
<h2>参考</h2>
<blockquote><a href="https://link.segmentfault.com/?enc=B4sDKMhA5hN6ME7CSeeX5w%3D%3D.HAMOTUnK26nZDPEg8FpxoG05xjV3VFvTuguRrT6NRQ5sTyfEaXlDYU1C5gI8QP%2F44O8SzTbSPdA1zPiK1896tg%3D%3D" rel="nofollow">Writing a loader</a></blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<h2>推荐: 翻译项目Master的自述:</h2>
<h3>1. <a href="https://link.segmentfault.com/?enc=RZbBvchQqpz3evznSOHWEw%3D%3D.veT7CANPip%2BnYHsEF2Vp5auJMJnUunxLLOtvO5dOTK61wF8TgaISIm%2Bdo3u4iaz3" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h3>
<h3>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h3>
<h3>3. <a href="https://link.segmentfault.com/?enc=1iYgmCPyDg1xKmUJU2rS7Q%3D%3D.kz6XrpKKYH5I5JxAtLtjrDt6%2BwM7XTOcl0qvd%2BgJy2zkfyuvs5lh2f7qSuTky33c" rel="nofollow">开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍</a>
</h3>
iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 记录日志
https://segmentfault.com/a/1190000012932469
2018-01-22T12:55:22+08:00
2018-01-22T12:55:22+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<p>沪江CCtalk视频地址:<a href="https://link.segmentfault.com/?enc=i86Ur2YvySjHHldnsIon2w%3D%3D.hZ1VSQQPyyp%2Fr%2B7Nh5aUvpOAP8xhWE6%2FZ1%2BEXQJ0n3sNSvlrNlj8Fu2IEtVRF0kG" rel="nofollow">https://www.cctalk.com/v/15114923883523</a></p>
<p><img src="/img/remote/1460000012932474?w=1606&h=968" alt="" title=""></p>
<h2>log 日志中间件</h2>
<blockquote>最困难的事情就是认识自己。</blockquote>
<p>在一个真实的项目中,开发只是整个投入的一小部分,版本迭代和后期维护占了极其重要的部分。项目上线运转起来之后,我们如何知道项目运转的状态呢?如何发现线上存在的问题,如何及时进行补救呢?记录日志就是解决困扰的关键方案。正如我们每天写日记一样,不仅能够记录项目每天都做了什么,便于日后回顾,也可以将做错的事情记录下来,进行自我反省。完善的日志记录不仅能够还原问题场景,还有助于统计访问数据,分析用户行为。</p>
<h3>日志的作用</h3>
<ul>
<li>显示程序运行状态</li>
<li>帮助开发者排除问题故障</li>
<li>结合专业的日志分析工具(如 ELK )给出预警</li>
</ul>
<h3>关于编写 log 中间件的预备知识</h3>
<h4>log4js</h4>
<p>本项目中的 <code>log</code> 中间件是基于 <code>log4js 2.x</code> 的封装,<a href="https://link.segmentfault.com/?enc=7O3TcJ1fB16FSR3EVkXqUA%3D%3D.Ko2VgMnaLUITjyw2oVfVA6J8R3jjPPWtMY%2F5oGu050FhR1nCjESSg%2FlnTR6HHh14" rel="nofollow">Log4js</a> 是 <code>Node.js</code> 中一个成熟的记录日志的第三方模块,下文也会根据中间件的使用介绍一些 <code>log4js</code> 的使用方法。</p>
<h4>日志分类</h4>
<p>日志可以大体上分为访问日志和应用日志。访问日志一般记录客户端对项目的访问,主要是 <code>http</code> 请求。这些数据属于运营数据,也可以反过来帮助改进和提升网站的性能和用户体验;应用日志是项目中需要特殊标记和记录的位置打印的日志,包括出现异常的情况,方便开发人员查询项目的运行状态和定位 <code>bug</code> 。应用日志包含了<code>debug</code>、<code>info</code>、<code>warn</code> 和 <code>error</code>等级别的日志。</p>
<h4>日志等级</h4>
<p><code>log4js</code> 中的日志输出可分为如下7个等级:</p>
<p><img src="/img/remote/1460000012932475?w=699&h=551" alt="LOG_LEVEL.957353bf.png" title="LOG_LEVEL.957353bf.png"></p>
<p>在应用中按照级别记录了日志之后,可以按照指定级别输出高于指定级别的日志。</p>
<h4>日志切割</h4>
<p>当我们的项目在线上环境稳定运行后,访问量会越来越大,日志文件也会越来越大。日益增大的文件对查看和跟踪问题带来了诸多不便,同时增大了服务器的压力。虽然可以按照类型将日志分为两个文件,但并不会有太大的改善。所以我们按照<strong>日期</strong>将日志文件进行分割。比如:今天将日志输出到 <code>task-2017-10-16.log</code> 文件,明天会输出到 <code>task-2017-10-17.log</code> 文件。减小单个文件的大小不仅方便开发人员按照日期排查问题,还方便对日志文件进行迁移。</p>
<h3>代码实现</h3>
<h5>安装 <code>log4js</code> 模块</h5>
<pre><code class="js">npm i log4js -S</code></pre>
<h5>
<code>log4js</code> 官方简单示例</h5>
<p>在 <code>middleware/</code> 目录下创建 <code>mi-log/demo.js</code>,并贴入官方示例代码:</p>
<pre><code class="js">var log4js = require('log4js');
var logger = log4js.getLogger();
logger.level = 'debug';
logger.debug("Some debug messages");
</code></pre>
<p>然后在 <code>/middleware/mi-log/</code> 目录下运行:</p>
<pre><code class="js">cd ./middleware/mi-log/ && node demo.js</code></pre>
<p>可以在终端看到如下输出:</p>
<pre><code class="txt">[2017-10-24 15:45:30.770] [DEBUG] default - Some debug messages</code></pre>
<p>一段带有日期、时间、日志级别和调用 <code>debug</code> 方法时传入的字符串的文本日志。实现了简单的终端日志输出。</p>
<h5>
<code>log4js</code> 官方复杂示例</h5>
<p>替换 <code>mi-log/demo.js</code> 中的代码为如下:</p>
<pre><code class="js">const log4js = require('log4js');
log4js.configure({
appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
categories: { default: { appenders: ['cheese'], level: 'error' } }
});
const logger = log4js.getLogger('cheese');
logger.trace('Entering cheese testing');
logger.debug('Got cheese.');
logger.info('Cheese is Gouda.');
logger.warn('Cheese is quite smelly.');
logger.error('Cheese is too ripe!');
logger.fatal('Cheese was breeding ground for listeria.');</code></pre>
<p>再次在 <code>/middleware/mi-log/</code> 目录下运行:</p>
<pre><code class="js">node demo.js</code></pre>
<p>运行之后,在当前的目录下会生成一个日志文件 <code>cheese.log</code>文件,文件中有两条日志并记录了 <code>error</code> 及以上级别的信息,也就是如下内容:</p>
<pre><code class="txt">[2017-10-24 15:51:30.770] [ERROR] cheese - Cheese is too ripe!
[2017-10-24 15:51:30.774] [FATAL] cheese - Cheese was breeding ground for listeria.</code></pre>
<p><strong>注意:</strong> 日志文件产生的位置就是当前启动环境的位置。</p>
<p>分析以上代码就会发现,<code>configure</code> 函数配置了日志的基本信息</p>
<pre><code class="js">{
/**
* 指定要记录的日志分类 cheese
* 展示方式为文件类型 file
* 日志输出的文件名 cheese.log
*/
appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
/**
* 指定日志的默认配置项
* 如果 log4js.getLogger 中没有指定,默认为 cheese 日志的配置项
* 指定 cheese 日志的记录内容为 error 及 error 以上级别的信息
*/
categories: { default: { appenders: ['cheese'], level: 'error' } }
}</code></pre>
<h5>改写为log中间件</h5>
<p>创建 <code>/mi-log/logger.js</code> 文件,并增加如下代码:</p>
<pre><code class="js">const log4js = require('log4js');
module.exports = ( options ) => {
return async (ctx, next) => {
const start = Date.now()
log4js.configure({
appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
categories: { default: { appenders: ['cheese'], level: 'info' } }
});
const logger = log4js.getLogger('cheese');
await next()
const end = Date.now()
const responseTime = end - start;
logger.info(`响应时间为${responseTime/1000}s`);
}
}</code></pre>
<p>创建 <code>/mi-log/index.js</code> 文件,并增加如下代码:</p>
<pre><code class="js">const logger = require("./logger")
module.exports = () => {
return logger()
}</code></pre>
<p>修改 <code>middleware/index.js</code> 文件,并增加对 <code>log</code> 中间件的注册, 如下代码:</p>
<pre><code class="js">const path = require('path')
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
const staticFiles = require('koa-static')
const miSend = require('./mi-send')
// 引入日志中间件
const miLog = require('./mi-log')
module.exports = (app) => {
// 注册中间件
app.use(miLog())
app.use(staticFiles(path.resolve(__dirname, "../public")))
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, '../views'),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
app.use(miSend())
}</code></pre>
<p>打开浏览器并访问 <code>http://localhost:3000</code>, 来发送一个<code>http</code> 请求。</p>
<p>如上,按照前几节课程中讲解的中间件的写法,将以上代码改写为中间件。 基于 <code>koa</code> 的洋葱模型,当 <code>http</code> 请求经过此中间件时便会在 <code>cheese.log</code> 文件中打印一条日志级别为 <code>info</code> 的日志并记录了请求的响应时间。如此,便实现了访问日志的记录。</p>
<h5>实现应用日志,将其挂载到 <code>ctx</code> 上</h5>
<p>若要在其他中间件或代码中通过 <code>ctx</code> 上的方法打印日志,首先需要在上下文中挂载 <code>log</code> 函数。打开 <code>/mi-log/logger.js</code> 文件:</p>
<pre><code class="js">const log4js = require('log4js');
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
module.exports = () => {
const contextLogger = {}
log4js.configure({
appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
categories: { default: { appenders: ['cheese'], level: 'info' } }
});
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
// 记录请求开始的时间
const start = Date.now()
// 循环methods将所有方法挂载到ctx 上
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
logger[method](message)
}
})
ctx.log = contextLogger;
await next()
// 记录完成的时间 作差 计算响应时间
const responseTime = Date.now() - start;
logger.info(`响应时间为${responseTime/1000}s`);
}
}
</code></pre>
<p>创建 <code>contextLogger</code> 对象,将所有的日志级别方法赋给对应的 <code>contextLogger</code> 对象方法。在将循环后的包含所有方法的 <code>contextLogger</code> 对象赋给 <code>ctx</code> 上的 <code>log</code> 方法。</p>
<p>打开 <code>/mi-send/index.js</code> 文件, 并调用 <code>ctx</code> 上的 <code>log</code> 方法:</p>
<pre><code class="js">module.exports = () => {
function render(json) {
this.set("Content-Type", "application/json")
this.body = JSON.stringify(json)
}
return async (ctx, next) => {
ctx.send = render.bind(ctx)
// 调用ctx上的log方法下的error方法打印日志
ctx.log.error('ikcamp');
await next()
}
}</code></pre>
<p>在其他中间件中通过调用 <code>ctx</code> 上的 <code>log</code> 方法,从而实现打印应用日志。</p>
<pre><code class="js">const log4js = require('log4js');
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
module.exports = () => {
const contextLogger = {}
const config = {
appenders: {
cheese: {
type: 'dateFile', // 日志类型
filename: `logs/task`, // 输出的文件名
pattern: '-yyyy-MM-dd.log', // 文件名增加后缀
alwaysIncludePattern: true // 是否总是有后缀名
}
},
categories: {
default: {
appenders: ['cheese'],
level:'info'
}
}
}
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
const start = Date.now()
log4js.configure(config)
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
logger[method](message)
}
})
ctx.log = contextLogger;
await next()
const responseTime = Date.now() - start;
logger.info(`响应时间为${responseTime/1000}s`);
}
}
</code></pre>
<p>修改日志类型为日期文件,按照日期切割日志输出,以减小单个日志文件的大小。这时候打开浏览器并访问 <code>http://localhost:3000</code>,这时会自动生成一个 <code>logs</code> 目录,并生成一个 <code>cheese-2017-10-24.log</code> 文件, 中间件执行便会在其中中记录下访问日志。</p>
<pre><code class="txt">├── node_modules/
├── logs/
│ ├── cheese-2017-10-24.log
├── ……
├── app.js</code></pre>
<h5>抽出可配置量</h5>
<pre><code class="js">const log4js = require('log4js');
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
// 提取默认公用参数对象
const baseInfo = {
appLogLevel: 'debug', // 指定记录的日志级别
dir: 'logs', // 指定日志存放的目录名
env: 'dev', // 指定当前环境,当为开发环境时,在控制台也输出,方便调试
projectName: 'koa2-tutorial', // 项目名,记录在日志中的项目信息
serverIp: '0.0.0.0' // 默认情况下服务器 ip 地址
}
const { env, appLogLevel, dir } = baseInfo
module.exports = () => {
const contextLogger = {}
const appenders = {}
appenders.cheese = {
type: 'dateFile',
filename: `${dir}/task`,
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true
}
// 环境变量为dev local development 认为是开发环境
if (env === "dev" || env === "local" || env === "development") {
appenders.out = {
type: "console"
}
}
let config = {
appenders,
categories: {
default: {
appenders: Object.keys(appenders),
level: appLogLevel
}
}
}
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
const start = Date.now()
log4js.configure(config)
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
logger[method](message)
}
})
ctx.log = contextLogger;
await next()
const responseTime = Date.now() - start;
logger.info(`响应时间为${responseTime/1000}s`);
}
}
</code></pre>
<p>代码中,我们指定了几个常量以方便后面提取,比如 <code>appLogLevel</code>、<code>dir</code>、<code>env</code> 等。 。并判断当前环境为开发环境则将日志同时输出到终端, 以便开发人员在开发是查看运行状态和查询异常。</p>
<h5>丰富日志信息</h5>
<p>在 <code>ctx</code> 对象中,有一些客户端信息是我们数据统计及排查问题所需要的,所以完全可以利用这些信息来丰富日志内容。在这里,我们只需要修改挂载 <code>ctx</code> 对象的 <code>log</code> 函数的传入参数:</p>
<pre><code class="js">logger[method](message)</code></pre>
<p>参数 <code>message</code> 是一个字符串,所以我们封装一个函数,用来把信息与上下文 <code>ctx</code> 中的客户端信息相结合,并返回字符串。 </p>
<p>增加日志信息的封装文件 <code>mi-log/access.js</code>:</p>
<pre><code class="js">module.exports = (ctx, message, commonInfo) => {
const {
method, // 请求方法 get post或其他
url, // 请求链接
host, // 发送请求的客户端的host
headers // 请求中的headers
} = ctx.request;
const client = {
method,
url,
host,
message,
referer: headers['referer'], // 请求的源地址
userAgent: headers['user-agent'] // 客户端信息 设备及浏览器信息
}
return JSON.stringify(Object.assign(commonInfo, client));
}</code></pre>
<p><strong>注意:</strong> 最终返回的是字符串。 </p>
<p>取出 <code>ctx</code> 对象中请求相关信息及客户端 <code>userAgent</code> 等信息并转为字符串。</p>
<p>在 <code>mi-log/logger.js</code> 文件中调用:</p>
<pre><code class="js">const log4js = require('log4js');
// 引入日志输出信息的封装文件
const access = require("./access.js");
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
const baseInfo = {
appLogLevel: 'debug',
dir: 'logs',
env: 'dev',
projectName: 'koa2-tutorial',
serverIp: '0.0.0.0'
}
const { env, appLogLevel, dir, serverIp, projectName } = baseInfo
// 增加常量,用来存储公用的日志信息
const commonInfo = { projectName, serverIp }
module.exports = () => {
const contextLogger = {}
const appenders = {}
appenders.cheese = {
type: 'dateFile',
filename: `${dir}/task`,
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true
}
if (env === "dev" || env === "local" || env === "development") {
appenders.out = {
type: "console"
}
}
let config = {
appenders,
categories: {
default: {
appenders: Object.keys(appenders),
level: appLogLevel
}
}
}
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
const start = Date.now()
log4js.configure(config)
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
// 将入参换为函数返回的字符串
logger[method](access(ctx, message, commonInfo))
}
})
ctx.log = contextLogger;
await next()
const responseTime = Date.now() - start;
logger.info(access(ctx, {
responseTime: `响应时间为${responseTime/1000}s`
}, commonInfo))
}
}</code></pre>
<p>重启服务器并访问 <code>http://localhost:3000</code> 就会发现,日志文件的记录内容已经变化。代码到这里,已经完成了大部分的日志功能。下面我们完善下其他功能:自定义配置参数和捕捉错误。</p>
<h5>项目自定义内容</h5>
<p>安装依赖文件 <code>ip</code>:</p>
<pre><code class="js">npm i ip -S</code></pre>
<p>修改 <code>middleware/index.js</code> 中的调用方法</p>
<pre><code class="js">const path = require('path')
const ip = require('ip')
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
const staticFiles = require('koa-static')
const miSend = require('./mi-send')
const miLog = require('./mi-log/logger')
module.exports = (app) => {
// 将配置中间件的参数在注册中间件时作为参数传入
app.use(miLog({
env: app.env, // koa 提供的环境变量
projectName: 'koa2-tutorial',
appLogLevel: 'debug',
dir: 'logs',
serverIp: ip.address()
}))
app.use(staticFiles(path.resolve(__dirname, "../public")))
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, '../views'),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
app.use(miSend())
}</code></pre>
<p>再次修改 <code>mi-log/logger.js</code> 文件:</p>
<pre><code class="js">const log4js = require('log4js');
const access = require("./access.js");
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
const baseInfo = {
appLogLevel: 'debug',
dir: 'logs',
env: 'dev',
projectName: 'koa2-tutorial',
serverIp: '0.0.0.0'
}
module.exports = (options) => {
const contextLogger = {}
const appenders = {}
// 继承自 baseInfo 默认参数
const opts = Object.assign({}, baseInfo, options || {})
// 需要的变量解构 方便使用
const { env, appLogLevel, dir, serverIp, projectName } = opts
const commonInfo = { projectName, serverIp }
appenders.cheese = {
type: 'dateFile',
filename: `${dir}/task`,
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true
}
if (env === "dev" || env === "local" || env === "development") {
appenders.out = {
type: "console"
}
}
let config = {
appenders,
categories: {
default: {
appenders: Object.keys(appenders),
level: appLogLevel
}
}
}
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
const start = Date.now()
log4js.configure(config)
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
logger[method](access(ctx, message, commonInfo))
}
})
ctx.log = contextLogger;
await next()
const responseTime = Date.now() - start;
logger.info(access(ctx, {
responseTime: `响应时间为${responseTime/1000}s`
}, commonInfo))
}
}</code></pre>
<p>将项目中自定义的量覆盖默认值,解构使用。以达到项目自定义的目的。</p>
<h5>对日志中间件进行错误处理</h5>
<p>对于日志中间件里面的错误,我们也需要捕获并处理。在这里,我们提取一层进行封装。</p>
<p>打开 <code>mi-log/index.js</code> 文件,修改代码如下:</p>
<pre><code class="js">const logger = require("./logger")
module.exports = (options) => {
const loggerMiddleware = logger(options)
return (ctx, next) => {
return loggerMiddleware(ctx, next)
.catch((e) => {
if (ctx.status < 500) {
ctx.status = 500;
}
ctx.log.error(e.stack);
ctx.state.logged = true;
ctx.throw(e);
})
}
}</code></pre>
<p>如果中间件里面有抛出错误,这里将通过 <code>catch</code> 函数捕捉到并处理,将状态码小于 <code>500</code> 的错误统一按照 <code>500</code> 错误码处理,以方便后面的 <code>http-error</code> 中间件显示错误页面。 调用 <code>log</code> 中间件打印堆栈信息并将错误抛出到最外层的全局错误监听进行处理。 </p>
<p>到这里我们的日志中间件已经制作完成。当然,还有很多的情况我们需要根据项目情况来继续扩展,比如结合『监控系统』、『日志分析预警』和『自动排查跟踪机制』等。可以参考一下<a href="https://link.segmentfault.com/?enc=RUn0w4auH7CZ71ZRaxcFZA%3D%3D.a8HuWr8mkErCdPRqyNmX5DXKSx0iBP6uJrho0cHeLgLA3x3evaaoqEF%2BxqvSmqpf" rel="nofollow">官方文档</a>。</p>
<blockquote>下一节中,我们将学习下如何处理请求错误。</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=S%2FuPwRSvowEA7T%2BKLETnUw%3D%3D.BO8NBjHegvQWfke64YjK1S03ZzIwdpu0BISRjQFe4U%2FNlUj0HNdO64aQYOtWpQPS" rel="nofollow">iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 处理静态资源</a>
</blockquote>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=qBNI7RIzMXTJqP5dI2aKOA%3D%3D.8%2FLq3rWkkQxaw3aVmsvXoL7oeINxNQmIdneL%2BXrUUKHlDFX1OfoJZfiyel9NuGf8" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
React Native 网络层分析
https://segmentfault.com/a/1190000012902394
2018-01-19T11:25:27+08:00
2018-01-19T11:25:27+08:00
iKcamp
https://segmentfault.com/u/ikcamp
4
<p>端)</p>
<blockquote>本文原创,转载请注明作者及出处</blockquote>
<p>在使用<code>React Native</code>开发中,我们熟练的采用<code>JavaScript</code>的方式发送请求的方式发送一个请求到服务端,但是处理这个请求的过程其实和处理<code>Web</code>应用中发送的请求的过程是不一样的。因为处理这个请求的目标不是浏览器,而是嵌入这个应用的原生操作系统。</p>
<p><img src="/img/bV2meW?w=1000&h=571" alt="clipboard.png" title="clipboard.png"></p>
<p>在处理<code>React Native</code>的请求时,分为两部分:一部分是<code>JavaScript</code>的运行环境,另一部分是嵌入<code>JavaScript</code>的<code>Native</code>(即原生<code>Android</code>和<code>IOS</code>)运行环境。<code>React Native</code>内置了三种发送网络请求的方式:<code>fetch</code>, <code>XMLHttpRequest</code> 和 <code>WebSocket</code>。但是<code>React Native</code>的运行环境和Web应用的运行环境不一样,所以需要在原生应用层采用自定义函数来拓展运行时(runtime)环境来处理<code>JavaScript</code>发出的网络请求。</p>
<h2>请求发送方式及过程</h2>
<p><img src="/img/bV2me0?w=1000&h=575" alt="clipboard.png" title="clipboard.png"></p>
<p>对于常用的网络请求对象:XMLHttpRequest(XHR)、Fetch及WebSocket,熟悉前端开发的同学应该非常了解。<code>XHR</code>是Web开发中用得比较多的发送请求的方式,<code>Fetch</code>和<code>Websocket</code>也是后起之秀,在很多现代Web应用中得以采用。但是,在<code>React Native</code>中,这些对象的使用和Web应用是有差别的。当你在JS层调用网络请求时,其实是经历了两个过程才到达真正的服务器端。就像头部banner表示的那样。</p>
<h4>XMLHttpRequest(XHR)</h4>
<p>在<code>React Native</code>中, <code>XMLHttpRequest(XHR)</code>由两部分组成: “前端”(front-end)和“后端”(back-end)。前端负责与<code>JavaScript</code>交互,后端负责在原生平台上转换<code>JavaScript</code>发送过来的请求为原生系统自己的请求。</p>
<p>这里的后端其实是一个原生平台顶层抽象的统一API层,使得<code>JavaScript</code>层可以调用原先系统的网络模块。例如<code>IOS</code>下内置的<a href="https://link.segmentfault.com/?enc=vz3hXZBu5PcdHuFg%2BXDNmQ%3D%3D.YCV4kGvciGmhn0WsJ7PQYBO2YIO05NGoOyRawKlZLGULFrm%2BXB%2Bq7Xd%2Bw5C1hFPbe%2F3g%2FcuB0UOlQkaw7627uA%3D%3D" rel="nofollow">URLSession</a>模块和<code>Android</code>下的<a href="https://link.segmentfault.com/?enc=3FYiKjrrgdTkdsOWAGPDHQ%3D%3D.SCMTL7hq1xmvCY%2Blz%2Bfk9rMP6s9%2BluMUyic1bI5rwPk%3D" rel="nofollow">OKHTTP</a>模块。</p>
<h4>Fetch</h4>
<p>在现代Web浏览器中,<code>Fetch</code>API提供了和<code>XHR</code>大部分相同的功能,但是<code>Fetch</code>提供了一种更加简单,高效的方式来跨网络异步获取资源,同时可操纵<code>Request</code>和<code>Response</code>对象来复用请求。</p>
<p>但是在<code>React Native</code>中,为了兼容两种平台的差异,采用了依赖于<code>XMLHttpRequest</code>的<a href="https://link.segmentfault.com/?enc=jvD38Ck7uBcovAaQR8sUpg%3D%3D.V%2FWdSfxYRQ8Ufy8Rao%2FkEbQJoKfoIL%2F25ipneMPWTUU%3D" rel="nofollow">Fetch Polyfill</a>来实现这个请求对象。这就意味着我们不能像实用Web平台下的<code>Fetch</code>对象一样来实用<code>React Native</code>下的该对象。比如采用这个对象来发送binary数据。当然可以采用第三方的库比如<a href="https://link.segmentfault.com/?enc=WayiRgRdse0kk4QOOb%2B42Q%3D%3D.gw414xtNt%2BADAQGjczY4ABKY9TFd95m58JlPYwvYPd6SODFX8KVZNL0KR7ROVInhK%2FXpZ4kcpMuL8JO2d9sxaw%3D%3D" rel="nofollow">react-native-fetch-blob</a>来实现相应的功能。</p>
<h4>Websocket</h4>
<p><code>Websocket</code>作为一种新的通信协议,采用全双工通讯方式与服务器间进行通信的网络技术。</p>
<p>在<code>React Native</code>中,<code>Websocket</code>并不是一个独立的请求,和<code>XMLHttpRequest(XHR)</code>一样由两部分组成: “前端”(front-end)和“后端”(back-end)。前端负责与<code>JavaScript</code>交互,后端负责在原生平台上转换<code>JavaScript</code>发送过来的请求为原生系统自己的请求。在IOS中采用的是自己开发的<a href="https://link.segmentfault.com/?enc=Mb50nSNW9f2JdsgwSLM%2FBw%3D%3D.jm7vqQHveRhPo8oxRGvqcCQO410r69BPlbX0LvM5epptbbBCWbs95TM1y9dyqjPxl0G19LtGHS1TLZNLBNhbVw%3D%3D" rel="nofollow">NSStream</a>,而在Android系统中则是<a href="https://link.segmentfault.com/?enc=Bd6YvefjYn7JSjExYZ%2FJwg%3D%3D.vAvUFN6Gc4F0%2FBDaHp0We%2FqD0o2Hz3%2Fs5QZyLDweKa4%3D" rel="nofollow">OKHTTP</a>模块。</p>
<h2>查看React Native中的网络请求</h2>
<p>在<code>React Native</code>开发中,你可以通过<code>Chrome Developer Tools (CDT)</code>的<code>Sources</code>面板中调试<code>javascript</code>部分的代码,包括断点、输出信息、断点调试等一切<code>javascript</code>调试所需的信息。但是,唯一缺少的就是<em>网络请求</em>的跟踪调试。我们没办法像Web开发那样,可以通过<code>CDT</code>中的网络面板(<code>Network</code>)来查看应用的网络请求的相关信息。</p>
<h4>使用代理调试网络请求</h4>
<p>虽然没有办法通过<code>CDT</code>查看应用的网络请求,但是我们可以通过<code>Fiddler</code>、<code>CharlesProxy</code>及<code>Wireshark</code>等软件设置代理,来查看追踪调试网络请求。这里使用<code>Fiddler</code>来作为代理。</p>
<ol><li>首先设置<code>Fiddler</code>的代理端口:<br> 打开Filddler -> Tool -> Options -> Connects,在监听端口处填写相应的端口号,</li></ol>
<p><img src="/img/bV2mfd?w=590&h=501" alt="clipboard.png" title="clipboard.png"></p>
<ol><li>在调试机器上、<code>Android</code>或者<code>IOS</code>模拟器模拟器中设置代理:<br> 找到调试的机器上的网络设置中,设置当前连接的WIFI的代理地址</li></ol>
<p><img src="/img/bV2mfg?w=502&h=872" alt="clipboard.png" title="clipboard.png"></p>
<ol><li>刷新应用,在<code>fiddler</code>中查看网络请求(提示:右键,在新页签中打开可查看清晰图片):</li></ol>
<p>看请求头,返回头,返回结果等相关的网络信息。当然,还可以根据相关代理软件拦截请求,重新设置后发送。</p>
<p><img src="/img/remote/1460000012902403" alt="fiddler设置" title="fiddler设置"></p>
<h4>使用<a href="https://link.segmentfault.com/?enc=hMhjjldXfcxkYksoOuIEcQ%3D%3D.0yUwEoTV2kF8beTWfdioGkOo2Byk9I4tzqcxcOFP%2F0IuJp58mS0DioisFuLwKaAG" rel="nofollow">Reactotron</a>调试网络</h4>
<p>上面通过设置代理的方式来查看和追踪网络请求,虽然功能强大,但是实际操作起来有些难度,上手成本比较高。通过使用<code>Reactotron</code>,可以将调试的配置信息集成到应用中,方便在不同的开发环境下有相同的调试配置,节约开发配置成本。</p>
<p><code>Reactotron</code>由两部分组成,一部分是调试应用,一部分是调试配置。</p>
<ol><li>
<p>调试应用分别有各个操作系统的<a href="https://link.segmentfault.com/?enc=PE1hdYY1j%2F7oFQ7ioTYSIQ%3D%3D.1QZU4Fp6o%2F0i%2FX20KCU6iVUmrIwf%2BvPvGWac8nBMM30%3D" rel="nofollow">GUI安装版本</a>。当然,如果习惯使用命令行,也可以使用<code>NPM</code>安装<code>reactotron-cli</code></p>
<pre><code>npm install -g reactotron-cli</code></pre>
</li></ol>
<p><img src="/img/bV2mgb?w=650&h=800" alt="clipboard.png" title="clipboard.png"></p>
<ol><li>
<p>设置调试配置:</p>
<p>在你的<code>React Native</code>应用中安装<code>reactotron-react-native</code></p>
<pre><code>npm i --save-dev reactotron-react-native</code></pre>
<pre><code>然后,在你的应用的添加配置文件,定制调试内容:
```
import Reactotron, {
trackGlobalErrors,
openInEditor,
overlay,
asyncStorage,
networking
} from 'reactotron-react-native'
Reactotron
.configure({
name: 'xxx' // 调试的名称
})
.use(trackGlobalErrors()) // 设置监听全局错误
.use(openInEditor()) // 设置在编辑器中打开错误
.use(overlay()) // 设置图片遮盖图片(用于UI还原度对比)
.use(asyncStorage()) // 设置异步存储调试
.use(networking()) // 设置网络调试
.connect() // 连接应用(必须)
```
然后在你的应用的入口文件中引入这个配置文件。然后重新启动应用。当然,还可以使用正则表达式过滤请求的`contentType`的类型和要忽略的请求的`url`,见下面的配置:
```
.use(networking({
ignoreContentTypes: /^(image)\/.*$/i, // 设置reactotron要忽略的文件类型
ignoreUrls: /\/(logs|symbolicate)$/, // 设置reactotron要忽略的url请求路径
}))
```</code></pre>
<p>![reactotron设置]</p>
</li></ol>
<p><img src="/img/bV2mgn?w=627&h=457" alt="clipboard.png" title="clipboard.png"><br><img src="/img/bV2mgm?w=751&h=537" alt="clipboard.png" title="clipboard.png"><br><img src="/img/remote/1460000012902407" alt="reactotron结果" title="reactotron结果"></p>
<p><code>reactotron</code>调试网络只是他的一个功能之一,其他还有很多强大的功能。有兴趣可以查看他的<a href="https://link.segmentfault.com/?enc=UD%2Fgndjntl8aLLmWONQvog%3D%3D.Mk6baW%2BV3Fc%2FMx38QWONQPq82B72mLt5fqPvyOk10AxMrUytxswmq0cBzv9Tami8" rel="nofollow">文档</a>。</p>
<h4>使用Chrome Developer Tools网络面板调试网络</h4>
<p><code>React Native</code>默认暴露出来的接口中,是没有直接在<code>Chrome Developer Tools</code>查看网络请求的方法的,查看 RN 源码 Libraries/Core/InitializeCore.js,注释中写着:</p>
<blockquote>Sets an object’s property. If a property with the same name exists, this will<br> replace it but maintain its descriptor configuration. By default, the property<br> will replaced with a lazy getter.<br> *<br> The original property value will be preserved as original[PropertyName] so<br> that, if necessary, it can be restored. For example, if you want to route<br> network requests through DevTools (to trace them):<br> *<br> global.XMLHttpRequest = global.originalXMLHttpRequest;<br> *<br> @see <a href="https://link.segmentfault.com/?enc=P2PSE1TXcMLhsTNuHDns7g%3D%3D.vAcRQFTFoH6og49z8KVjHbgbj5tXZrYISKSCuOpJAvlgezwg98a1K6zxacdxo8hltL3hPzVkSFg3vCjWvMkTlw%3D%3D" rel="nofollow">https://github.com/facebook/r...</a>
</blockquote>
<p>具体实现在<a href="https://link.segmentfault.com/?enc=xMl8XVh16PUKhATz9DUKvg%3D%3D.EchUm7b%2FwuMNaqiFNxcaR3wCwOUKin7fF9KubKGqHKbeMJbR4WVC1f%2B87RQo1N9E%2BpfSwCquXDRdJkGDHywSMaDg7g3A2rzPxgU%2BwCnSu1Qqk8vrSOLC40Zy5Wo%2BjjYNljTjWpBtLksC2umUzNi9aDiyV4fSN%2FSls9bOFdCEp%2FY%3D" rel="nofollow">XHRInterceptor.js</a>中。原来的<code>XMLHttpRequest </code>被改写成了 <code>originalXMLHttpRequest</code>,所以要在<code>Chrome</code> 中显示<code>network</code> 只需要替换<code> XMLHttpRequest</code> 为 <code>originalXMLHttpRequest</code>。在入口文件处设置:</p>
<pre><code>if (__DEV__) {
GLOBAL.XMLHttpRequest = GLOBAL.originalXMLHttpRequest || GLOBAL.XMLHttpRequest
}</code></pre>
<p>当然,这样有可能会产生<code>CORS</code>, <code>Chrome</code> 会限制跨域请求。这时要么后端配合一下去除限制,要么使用 <a href="https://link.segmentfault.com/?enc=jV5yEMA133zJGQyfahV8XA%3D%3D.NfAmEdTHfaqKuBGxOt6A7CN2ZeApVje2BAdAKjl5PWGJ0ddjLMO46rFDf2UDyDY6h3tJU1wz0uQfb36qVPpBzTxa254RwICqB9oAbCBGw7ScmPp%2FCyF0dHRzgb%2FRH6%2FfTQRBTlk2Yn%2BQIVvWJNx%2FkA%3D%3D" rel="nofollow">Allow-Control-Allow-Origin: *</a> 插件。</p>
<h2>React Native发送二进制数据(binary data )</h2>
<p>由于<code>React Native</code>中<code>Fetch</code>对象的底层采用的是<code>XHR</code>实现,这就限制了发送二进制数据的功能。当然<code>React Native</code>提供了一系列的方式来解决这个问题,比如: 转换二进制文件为base64字符串或者采用第三方库<a href="https://link.segmentfault.com/?enc=x8AuEt5%2FDnT7l3mhrWDwAg%3D%3D.GRKoz0BNtwie1kbFuoZUEXVldxHnVVQWRdwF7K7x%2FTHjkX%2B3vFuBwsLoYJK%2FjmfyhTyhl7AkuiSbwyTR4bazog%3D%3D" rel="nofollow"> react-native-fetch-blob</a>。但是并没有从底层解决这个问题。</p>
<h4>转换二进制为base64发送</h4>
<p>到目前为止,<code>React Native</code>不能发送非序列化的数据,所以,要发送二进制数据,采用Base64编码的字符串是个不错的选择。</p>
<p><img src="/img/bV2mgF?w=1000&h=571" alt="clipboard.png" title="clipboard.png"></p>
<p>例如,你从服务器下载一张图片(注意:不是通过<code>url</code>从服务器获取),请求通过JavaScript线程,再通过<code>React Native</code>提供的桥接器,最后通过原生系统的网络模块发送到服务端。服务端返回一个Base64编码过的图片,<code>JavaScript</code>线程收到返回的字符串后,会分配相应的内存,然后<code>React Native</code>会调用相应的原生模块渲染成相应图片。但是值得主要的是,这种方式会造成典型的性能问题——内存泄漏。</p>
<p>通过Base64编码的方式传输二进制文件,这里会造成一系列性能问题,<a href="https://link.segmentfault.com/?enc=xo56OBclyg%2F6dWODbuTu9w%3D%3D.BD5jg7BCLXkTUFlLCG21KErayDg5MhbmIkp05TOu3jGUIXvnP0hvFhVtWPT51hOdSoKXbHAUFrm1iP0f59T1V51xmDzrWOQ434lv%2BsZ0OwNgz3YL0my%2FjR0VfqRUsuCVLxdkIeKLZfPZ5F%2FykJGcJw%3D%3D" rel="nofollow">这篇文章</a>中列出了大部分性能问题及提出了相应的解决方案。</p>
<p>现在使用的各种方法发送二进制文件都存在各种问题,最终的解决方式是要相应的标准能够实现二进制的传输。目前,<code>WebSocket</code>已经支持了二进制传输。在最新版本的<code>React Native</code>层也已经支持<code>WebSocket</code>协议来传输二进制文件,但是,相应的原生平台的网络模块暂时还不支持。</p>
<h2>总结</h2>
<p><code>React Native</code>开发方式是非常不错的体验,但是,受各个平台差异和标准的限制,不得不折中处理一些问题。随之而来的是相应的性能、效率的问题。另外,采用开发,性能上和用户体验上和原生应用还是有一定差距。但是如果在原生应用中能够集成<code>React Native</code>,会显著提高开发效率。</p>
<h2>参考</h2>
<p><a href="https://link.segmentfault.com/?enc=d%2BU8tR9Aqk2Yxq2Fr%2FkEmA%3D%3D.KMQlV8FAYcrV%2FBrS0Ft4hYwZZtNmN%2BcICQJPLEGhPsoWQb4jtwbizZq5O%2Bd5ajOW9Saa8hRGVfgI5pjNdxAnZAB1B16z7EYgXgyZzndIubM%3D" rel="nofollow">Network layer in React Native</a></p>
<p><a href="https://link.segmentfault.com/?enc=EisGb6%2Bg6BIh9V0wl7lVmg%3D%3D.HvLdI9%2FTnOoJhbYFKnta5gbzpU4lxse4pdgKA5mEaXM%3D" rel="nofollow">reactotron介绍</a></p>
<p><a href="https://link.segmentfault.com/?enc=IAiwqXDHp80FWp2FziGhUg%3D%3D.a43yW42NL1HCVJfJMjYkFSh9jLpr%2FejOOXNMak7RuQSPAZf%2FREuzyakOBeUZqM2f" rel="nofollow">reactotron</a></p>
<p><a href="https://link.segmentfault.com/?enc=9N%2BKMVJqsNE804z9XEVyNg%3D%3D.dvIDyvR1Zl8ZOrP6w9jqoCv9ER%2FjzqInwDHhriqick8atZ1%2FpNxtjqbgB0LW1l%2BuRZQds733XTUXVGiBQP3YNKK5iitdYRH9%2BI%2Fg0D3LjMxN9CGkaL%2FI%2Bn12nTxNLLpD" rel="nofollow">Reactotron on React Native</a></p>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<h2>推荐: 翻译项目Master的自述:</h2>
<h3>1. <a href="https://link.segmentfault.com/?enc=aB4N%2BJolxMslYOENkvwksw%3D%3D.R5WslNyOmgS93QnS%2FpwkUhIftX4HhceGEq7CdNombpxnFeOC0aRn19m60OCEzSvF" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h3>
<h3>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h3>
<h3>3. <a href="https://link.segmentfault.com/?enc=6myzA6B7RhWmTkGPSPUDJw%3D%3D.giRXE5OsL1%2ByBfuIrEinOTWWmE8JgIoApwtzN0e%2BwmyIiJOXBT4VJgpcpQlg9lsr" rel="nofollow">开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍</a>
</h3>
如何实现VM框架中的数据绑定
https://segmentfault.com/a/1190000012870387
2018-01-17T10:21:43+08:00
2018-01-17T10:21:43+08:00
iKcamp
https://segmentfault.com/u/ikcamp
2
<blockquote>作者:佳杰<p>本文原创,转载请注明作者及出处</p>
</blockquote>
<h2>如何实现VM框架中的数据绑定</h2>
<h3>一:数据绑定概述</h3>
<pre><code>视图(view)和数据(model)之间的绑定
</code></pre>
<h3>二:数据绑定目的</h3>
<pre><code>不用手动调用方法渲染视图,提高开发效率;统一处理数据,便于维护
</code></pre>
<h3>三:数据绑定中的元素</h3>
<pre><code>视图(view):说白了就是html中dom元素的展示
数据(model):用于保存数据的引用类型
</code></pre>
<h3>四:数据绑定分类</h3>
<pre><code>view > model的数据绑定:view改变,导致model改变
model > view的数据绑定:model改变,导致view改变
</code></pre>
<h3>五:数据绑定实现方法</h3>
<pre><code>view > model的数据绑定实现方法
修改dom元素(input,textarea,select)的数据,导致model产生变化,
只要给dom元素绑定change事件,触发事件的时候修改model即可,不细讲
model > view的数据绑定实现方法
1.发布订阅模式(backbone.js用到);
2.数据劫持(vue.js用到);
3.脏值检查(angular.js用到);
</code></pre>
<h3>六:model > view数据绑定demo讲解 (如何实现数据改变,导致UI界面重新渲染)</h3>
<pre><code>简易思路
> 1.通过defineProperty来监控model中的所有属性(对每一个属性都监控)
> 2.编译template生成DOM树,同时绑定dom节点和model(例如<div id="{{model.name}}"></div>),
defineProperty中已经给“model.name”绑定了对应的function,
一旦model.name改变,该funciton就操作上面这个dom节点,改变view
主要js模块:Observer,Compile,ViewModel
1.Observer
用到了发布订阅模式和数据监控,defineProperty用于“监控model", dom元素执行"订阅"操作,给model中
的属性绑定function;model中属性变化的时候,执行"发布"这个操作,执行之前绑定的那个function
源码如下:
var Observer = function(opts) {
this.id = (opts && opts.id) ? opts.id : +new Date();
this.opts = opts;
this.subs = []; //观察者数组
/*this.subs包含了所有观察者,每个观察者的结构如下:
{
key:"person.age.range",//这个key代表model.person.age.range这个属性
/*
和key绑定的函数数组,每个函数操作一个dom节点,
一个key对应多个dom节点,所以actionList是个function数组;
*/
actionList:[function(){},function(){}]
}*/
}
Observer.prototype = {
//遍历model中所有的属性,每个属性用defineKey来监控所有属性
monit: function(data, baseUrl) {
var me = this;
baseUrl = baseUrl || "";
var isTypeMatch = (data && typeof data === "object");
if (isTypeMatch) {
Object.keys(data).forEach(function(key) {
var base = baseUrl ? (baseUrl + "." + key) : key;
me.defineKey(data, key, data[key], baseUrl); //定义自己
me.monit(data[key], base); //递归【定义的是下一层】
});
}
},
//用到了Object.defineProperty来定义属性,这样属性改变的时候,就会自动执行里面的set方法
defineKey: function(data, key, val, baseUrl) {
var me = this;
var base = baseUrl ? (baseUrl + "." + key) : key;
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function() {
return val;
},
//更新并监控新的值,执行publish函数
set: function(newVal) {
if (newVal !== val) {
val = newVal;
//设置新值需要重新监控
me.monit(newVal, base);
//(baseUrl+"."+key)作为观察者模式中的监听的那个key,也可以说是监听的那个事件
me.publish(base, newVal);
}
}
});
},
/*
根据key来执行绑定在这个key上的所有函数,比如说person.age.range这个key,
它变动的时候,publish会执行绑定在person.age.range这个key上所有的function
*/
publish: function(key, newVal) {
(this.subs || []).forEach(function(sub) {
if (sub.key == key) {
(sub.actionList || []).forEach(function(action) {
action(newVal);
});
}
});
},
//给model中的某个key(例如person.age.range)添加绑定的function
subscribe: function(key, callback) {
var tgIdx;
var hasExist = this.subs.some(function(unit, idx) {
tgIdx = (unit.key === key) ? idx : -1;
return (unit.key === key)
});
if (hasExist) {
if (Object.prototype.toString.call(this.subs[tgIdx].actionList)=="[object Array]"){
this.subs[tgIdx].actionList.push(callback);
} else {
this.subs[tgIdx].actionList = [callback];
}
} else {
this.subs.push({
key: key,
actionList: [callback]
});
}
},
//取消订阅
remove: function(key) {
var removeIdx;
this.subs.forEach(function(sub, idx) {
removeIdx = sub.key === key ? idx : -1;
return sub.key === key
});
if (removeIdx !== -1) {
this.subs.splice(removeIdx, 1);
}
},
isObject: function(data) {
return data && typeof data === "object"
}
};
2.Compile: 模板编译器
var Compile = function(opts) {
this.opts = opts;
this.data = this.opts.data;
this.observer = this.opts.observer;
this.regExp = /\{\{([\s\S]*)\}\}/;
this.ele = document.createElement("div");
this.ele.innerHTML = opts.template; //渲染页面
this.fragment = this.transToFrament(this.ele);
this.travelAllNodes(this.fragment);
this.ele.appendChild(this.fragment);
};
Compile.prototype = {
//把页面上的dom节点转化成文档碎片,防止dom频繁操作影响页面性能
transToFrament: function(el) {
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
//遍历文档碎片节点下所有的node节点(用到了函数递归调用),执行compileNode
travelAllNodes: function(ele) {
this.compileNode(ele);
([].slice.call(ele.childNodes) || []).forEach(function(node) {
this.compileNode(node);
if (node.childNodes && node.childNodes.length) {
this.travelAllNodes(node);
}
}.bind(this));
},
/*包含功能
1.渲染node节点
2.给key设置callback函数,函数内操作node节点
*/
compileNode: function(node) {
if (this.isElement(node)) {
this.compileElementNode(node);
} else if (this.isText(node)) {
this.compileTextNode(node);
}
},
/*
编译element类型的node节点,
需要处理属性绑定v-bind="{{data.name}}"和
事件v-event="{{data.event}}"
*/
compileElementNode: function(node) {
var me = this,
nodeAttrs = node.attributes;
[].slice.call(nodeAttrs).forEach(function(attr) {
var attrName = attr.name;
var attrValue = attr.value;
var key = me.getKey(attrValue);
me.bindKeyToNode(key, attr);
attr.value = me.compileString(attrValue); //渲染node
});
},
//编译文本类型的node节点,里面放了对应的"{{data.name}}"这种数据格式
compileTextNode: function(ele) {
var key = this.getKey(ele.textContent);
this.bindKeyToNode(key, ele);
ele.textContent = this.compileString(ele.textContent);
},
//解析“{{}}”,把它变成对应的数据值
compileString: function(str) {
var key = this.getKey(str);
return str.replace(this.regExp, this.getValueByKey(key));
},
//绑定key和node节点,key一旦改变,就会触发对应的函数,修改node节点
bindKeyToNode: function(key, node) {
if (!!key.trim()) {
console.log(key);
var nodeType = node.nodeType;
var regExp = new RegExp("\\{\\{" + key + "\\}\\}");
var originTextConetnt;
if (nodeType === 2) {
originTextConetnt = node.value;
} else if (nodeType === 3) {
originTextConetnt = node.textContent;
}
this.observer.subscribe(key, function(newVal) {
var tgValue = originTextConetnt.replace(regExp, newVal);
if (nodeType === 2) {
node.value = tgValue;
} else if (nodeType === 3) {
node.textContent = tgValue;
}
});
}
},
//从{{name.age.sex}}中获取name.age.sex
getKey: function(str) {
return str.match(this.regExp) ? str.match(this.regExp)[1] : "";
},
//获取key对应的value值
getValueByKey: function(key) {
var arr = key ? key.split(".") : [];
var temp = this.data;
for (var i = 0; i < arr.length; i++) {
if (temp) {
temp = temp[arr[i]];
} else {
temp = undefined;
break
}
}
return temp;
},
isElement: function(ele) {
return ele.nodeType === 1 ? true : false;
},
isText: function(ele) {
return ele.nodeType === 3 ? true : false;
},
getElement: function() {
return this.ele;
}
}
3.ViewModel:结合Observer与Compile,实现model > view的数据单向绑定
var ViewModel = function(opts) {
this.opts = opts;
this.data = opts.data;
this.wrapper = opts.wrapper;
this.template = opts.template;
this.Observer = (typeof Observer != undefined) ? Observer : opts.Observer;
this.Compile = (typeof Compile != undefined) ? Compile : opts.Compile;
this.init();
}
ViewModel.prototype = {
init: function() {
var opts = this.opts;
this.observer = new this.Observer(opts);
this.observer.monit(this.data); //监控数据变化,数据已经改变了
this.compiler = new this.Compile(Object.assign(opts, {
observer: this.observer
})); //编译生成节点
if (this.wrapper) {
this.wrapper.appendChild(this.compiler.getElement());
}
},
get: function() {
return this.compiler.getElement();
}
};
</code></pre>
<h2>总结</h2>
<pre><code>简单地调用new ViewModel({data:data,template:template}),完成了model和view的绑定,
ViewModel内部大致执行顺序是:
1. 创建数据监控对象this.observer,该对象监控data(监控以后,data的属性改变,
就会执行defineProperty中的set函数,set函数里面添加了publish发布函数)
2. 创建模板编译器对象this.compiler,该对象编译template,生成最终的dom树,
并且给每个需要绑定数据的dom节点添加了subscribe订阅函数
3. 最后,改变data里面的属性,会自动触发defineProperty中的set函数,set函数调用publish函数,
publish会根据key的名称,找到对应的需要执行的函数列表,依次执行所有函数
</code></pre>
<h2>Git地址</h2>
<pre><code>https://github.com/devil1989/databind/
</code></pre>
<h2>demo</h2>
<pre><code> <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" type="text/css" href="demo.css">
<script type="text/javascript" src="./observe.js"></script>
</head>
<body>
<template id="inner" type="text/template">
<div title="{{des}}">
<div>
<ul id="list">
<li >
<span >age:</span>
<input type="text" name="" value="{{age}}" >
<span id="age" style="float: left;">+</span>
</li>
<li>
<span>name:</span>
<input id="firstName" type="text" name="" value="{{name}}">
</li>
<li><span>{{name}}</span></li>
</ul>
</div>
</div>
</template>
<script type="text/javascript">
(function(){
window.data={name:"jeffrey",age:28,des:"测试"};
var vm=new VM({
data:data,
template:document.getElementById("inner").innerHTML
/* wrapper:document.body//可以指定对应容器,也可以不指定容器,
直接获取元素,再手动插入对应dom元素*/
});
document.body.appendChild(vm.get());
document.getElementById("age").addEventListener("click",function(){
data.age++;//只需要修改属性,html就会重新渲染
});
document.getElementById("firstName").addEventListener("keyup",function(e){
data.name=this.value;//只需要修改属性,html就会重新渲染
});
})();
</script>
</body>
</html>
</code></pre>
<h2>使用场景说明:</h2>
<pre><code>当我们想要修改页面某个元素的信息,但又不想费劲地查找dom元素再去修改元素的值,
这种情况下,可以用demo中的数据绑定,只需修改数据的值,就实现了页面元素重新渲染
请看下面的gif动画中展示的,只要修改data.age和data.name,页面元素就自动重新渲染了
</code></pre>
<p><img src="/img/remote/1460000012870392?w=264&h=568" alt="avatar" title="avatar"></p>
<h2>结束语</h2>
<p>本demo只是简单实现数据绑定,很多功能并未实现,只是提供一种思路,抛砖引玉;<br>如果对上述代码中的Observer类的代码不是很理解,可以先了解下观察者模式以及实现原理;<br>最后,感谢大家的阅读!!</p>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=uKeI5z2hABSbHQO32C04Rg%3D%3D.DJDQ7O6m4wX4NwjvFUXNNTeBXjwbvqzuRIiupKwXLNSo%2BLV4yepAoIzIE%2FiPeZNH" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
<h4>3. <a href="https://link.segmentfault.com/?enc=f9E4Li8apRoITCS9xTTv%2FA%3D%3D.tPF6CbRZ3ufjizouzLnqDGdHpK%2Fmbd0Im29%2BI0wvAz0S9eujuJtUvsyYhVtpEs1S" rel="nofollow">开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍</a>
</h4>
iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 解析JSON
https://segmentfault.com/a/1190000012840992
2018-01-15T11:13:12+08:00
2018-01-15T11:13:12+08:00
iKcamp
https://segmentfault.com/u/ikcamp
1
<p>视频地址:<a href="https://link.segmentfault.com/?enc=A5WsxFjg4GNoSn0Ul037OQ%3D%3D.fKtDjwCTFfxNwwX2WC7J8tIDvLa8VX3x7M2esat8KSiIcB5VhBq65awPggEDHwZp" rel="nofollow">https://www.cctalk.com/v/15114923886141</a></p>
<p><img src="/img/remote/1460000012840997?w=1604&h=964" alt="" title=""></p>
<h2>JSON 数据</h2>
<blockquote>我颠倒了整个世界,只为摆正你的倒影。</blockquote>
<p>前面的文章中,我们已经完成了项目中常见的问题,比如 <code>路由请求</code>、<code>结构分层</code>、<code>视图渲染</code>、<code>静态资源</code>等。 <br>那么,<code>JSON</code> 呢?<code>JSON</code> 格式数据的传输,已经深入到了我们的码里行间,脱离了 <code>JSON</code> 的人想必是痛苦的。那么,复合吧! </p>
<p><img src="/img/remote/1460000012840998?w=200&h=198" alt="" title=""></p>
<h3>如何设置 JSON 格式</h3>
<p>伟大的武术家——李小龙先生——说过这样一段话:</p>
<pre><code class="txt">Empty your mind, Be formless,shapeless like water.
You put water in a cup, it becomes the cup.
You put water in a bottle, it becomes the bottle.
You put water in a teapot , it becomes the teapot.
Water can flow or crash. </code></pre>
<p>翻译成中文意思就是:</p>
<pre><code class="txt">清空你的思想,像水一样无形。
你将水倒入水杯,水就是水杯的形状。
你将水倒入瓶子,水就是瓶子的形状。
你将水倒入茶壶,水就是茶壶的形状。
你看,水会流动,也会冲击。</code></pre>
<p>在数据传输过程中,传输的资源都可以称之为『数据』,而『数据』之所以展示出不同的形态,是因为我们已经设置了它的格式。 </p>
<p>传输的数据像是『水』一样,没有任何的格式和形状。 </p>
<p>我们的设置像是『器』一样,赋予它指定的形态。 </p>
<p>所以,我们只需要设置把数据挂载在响应体 <code>body</code> 上,同时告诉客户端『返回的是 <code>JSON</code> 数据』,客户端就会按照 <code>JSON</code> 来解析了。代码如下:</p>
<pre><code class="js">ctx.set("Content-Type", "application/json")
ctx.body = JSON.stringify(json)</code></pre>
<h3>提取中间件</h3>
<p>我们把上面的代码提取成一个中间件,这样更方便代码的维护性和扩展性 </p>
<p>增加文件 <code>/middleware/mi-send/index.js</code>:</p>
<pre><code class="js">module.exports = () => {
function render(json) {
this.set("Content-Type", "application/json")
this.body = JSON.stringify(json)
}
return async (ctx, next) => {
ctx.send = render.bind(ctx)
await next()
}
}</code></pre>
<p><strong>注意:</strong> 目录不存在,需要自己创建。 </p>
<p>代码中,我们把 <code>JSON</code> 数据的处理方法挂载在 <code>ctx</code> 对象中,并起名为 <code>send</code>。当我们需要返回 <code>JSON</code> 数据给客户端时候,只需要调用此方法,并把 <code>JSON</code> 对象作为参数传入到方法中就行了,用法如下:</p>
<pre><code class="js">ctx.send({
status: 'success',
data: 'hello ikcmap'
})</code></pre>
<h3>应用中间件</h3>
<p>代码的实现过程和调用方法我们已经知道了,现在我们需要把这个中间件应用在项目中。</p>
<ol><li>增加文件 <code>middleware/index.js</code>,用来集中调用所有的中间件:</li></ol>
<pre><code class="js">const miSend = require('./mi-send')
module.exports = (app) => {
app.use(miSend())
}</code></pre>
<ol><li>修改 <code>app.js</code>,增加中间件的引用</li></ol>
<pre><code class="js">const Koa = require('koa')
const path = require('path')
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
const staticFiles = require('koa-static')
const app = new Koa()
const router = require('./router')
const middleware = require('./middleware')
middleware(app)
app.use(staticFiles(path.resolve(__dirname, "./public")))
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, 'views'),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
router(app)
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<h3>中间件迁移</h3>
<p>随着项目的步步完善,将会产生有更多的中间件。我们把 <code>app.js</code> 中的中间件代码迁移到 <code>middleware/index.js</code> 中,方便后期维护扩展</p>
<ol><li>修改 <code>app.js</code>
</li></ol>
<pre><code class="js">const Koa = require('koa')
const app = new Koa()
const router = require('./router')
const middleware = require('./middleware')
middleware(app)
router(app)
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<ol><li>修改 <code>middleware/index.js</code>
</li></ol>
<pre><code class="js">const path = require('path')
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
const staticFiles = require('koa-static')
const miSend = require('./mi-send')
module.exports = (app) => {
app.use(staticFiles(path.resolve(__dirname, "../public")))
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, '../views'),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
app.use(miSend())
}</code></pre>
<p>后面我们还会开发更多的中间件,比如日志记录、错误处理等,都会放在 <code>middleware/</code> 目录下处理。</p>
<blockquote>下一篇:记录日志——开发日志中间件,记录项目中的各种形式信息</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=RQe6nDmJrEvVsG5aMnqh6w%3D%3D.uMgMWUFXot%2Blb%2FAcqCjCbh2i2njpEUv902kLK2UYi3Vh83ZafR4oACpim4%2FtOWsh" rel="nofollow">iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 处理静态资源</a>
</blockquote>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=mvOAOTSEVpOxDmp8hKCq%2BA%3D%3D.oX01%2FVaQMU%2FdfRp2W3IqE13Z6TA5jZQQE8cl7yLDhBSk3gsuTat9pvzb4xJscsVb" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 处理静态资源
https://segmentfault.com/a/1190000012812763
2018-01-12T10:49:20+08:00
2018-01-12T10:49:20+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<p>视频地址:<a href="https://link.segmentfault.com/?enc=7l9tB4FKoeJFHouPj1m9vQ%3D%3D.Qaig7i3PRu0rdKHE3nDuzzlA%2FWCL3vqkVN0yqqt%2BCvtf8aJR4bOLoXawIg14Ztvz" rel="nofollow">https://www.cctalk.com/v/15114923882788</a></p>
<p><img src="/img/remote/1460000012812768?w=1600&h=964" alt="" title=""></p>
<h2>处理静态资源</h2>
<blockquote>无非花开花落,静静。</blockquote>
<h3>指定静态资源目录</h3>
<p>这里我们使用第三方中间件: <code>koa-static</code></p>
<h4>安装并使用</h4>
<p>安装 <code>koa-static</code>:</p>
<pre><code class="js">npm i koa-static -S</code></pre>
<p>修改 <code>app.js</code>,增加并指定 <code>/public</code> 目录为静态资源目录。</p>
<pre><code class="js"> const Koa = require('koa')
const path = require('path')
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
// 引入 koa-static
const staticFiles = require('koa-static')
const app = new Koa()
const router = require('./router')
// 指定 public目录为静态资源目录,用来存放 js css images 等
app.use(staticFiles(path.resolve(__dirname, "./public")))
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, 'views'),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
router(app)
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<p>之后我们对项目的视图进行美化,使之更为赏心悦目。</p>
<h4>增加样式文件</h4>
<p>在 <code>/public/home/</code> 目录下新增样式文件 <code>main.css</code>,内容如下:</p>
<pre><code class="css"> *{
padding: 0;
margin: 0;
}
body,html{
font-size: 14px;
color: #000;
background: #fff;
font-family: Helvetica Neue,Helvetica,Segoe UI,Arial,Hiragino Sans GB,Microsoft YaHei;
-webkit-font-smoothing: antialiased;
position: relative;
}
.fn-clear:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0
}
.fn-clear {
zoom:1}
a {
color: #0366d6;
text-decoration: none;
}
a:hover {
text-decoration: none;
}
.header{
width: 100%;
background-color: #474747;
}
.header-box{
height: 30px;
line-height: 30px;
font-size: 12px;
letter-spacing: 2px;
color: #d5d5d5;
transition: color .3s;
}
.header-box>.logo{
letter-spacing: 0;
font-size: 12px;
}
.wraper{
width: 1200px;
margin: 0 auto;
}
.container{
min-height: 500px;
padding: 80px 0;
}
.footer{
background: #262a30;
padding: 50px 0;
border-top: 1px solid #ddd;
color: #999;
font-size: 16px;
}
.footer-box{
width: 800px;
margin: 0 auto;
text-align: center;
}
.banner_box{
width: 100%;
min-width: 1200px;
height: 438px;
background: url(https://res.hjfile.cn/cc/cctalk.hujiang.com/home/images/banner-2QEtv.jpg?2QEtv) 50% no-repeat;
background-size: cover;
}
.banner_box>.banner_inner{
width: 1200px;
margin: 0 auto;
padding-top: 112px;
}
.banner_inner>.slogan{
width: 427px;
height: 54px;
background: url(https://res.hjfile.cn/cc/cctalk.hujiang.com/home/images/slogan@2x-3x9xM.png?3x9xM);
background-size: 100% auto;
margin: 0 auto 25px;
text-indent: -99999rem;
}
.banner_inner>.des{
margin-bottom: 24px;
font-size: 16px;
line-height: 1.9;
color: #fff;
text-align: center;
}
.banner_inner>.btn{
display: block;
margin: 0 auto;
width: 220px;
height: 48px;
font-size: 20px;
line-height: 48px;
border-radius: 4px;
background-color: #15a9ff;
color: #fff;
text-align: center;
text-decoration: none;
box-shadow: 0 2px 6px rgba(0,0,0,.3);
}
.show_time>.feature-con{
background: #fff;
border-bottom: 2px solid #f8f8f8;
min-width: 1200px;
}
.feature-con>.feature{
list-style: none;
margin: 0 auto;
padding: 40px 0 60px;
width: 1200px;
}
.feature>.feature-item{
float: left;
width: 160px;
margin: 0;
padding: 0;
margin-right: 132px;
}
.feature>.feature-item:first-child{
margin-left: 88px;
}
.feature>.feature-item:last-child{
margin-right: 0;
}
.feature .ico{
display: inline-block;
width: 160px;
height: 130px;
background: url(https://res.hjfile.cn/cc/cctalk.hujiang.com/home/images/feature-icon1@2x-BvNad.png?BvNad);
background-size: 100% auto;
}
.feature>.feature-item:nth-child(2) .ico{
background-image: url(https://res.hjfile.cn/cc/cctalk.hujiang.com/home/images/feature-icon2@2x-1raFv.png);
}
.feature>.feature-item:nth-child(3) .ico{
background-image: url(https://res.hjfile.cn/cc/cctalk.hujiang.com/home/images/feature-icon3@2x-2y1F0.png);
}
.feature>.feature-item:nth-child(4) .ico{
background-image: url(https://res.hjfile.cn/cc/cctalk.hujiang.com/home/images/feature-icon4@2x-27VL5.png);
}
.feature-item>.tit{
padding: 0;
margin: 0;
font-size: 16px;
line-height: 26px;
color: #333;
text-align: center;
font-weight: 400;
}
.feature-item>.des{
padding: 0;
margin: 0;
font-size: 16px;
line-height: 26px;
color: #333;
text-align: center;
opacity: .5;
}
.hp-overlay{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 99999;
opacity: .5;
filter: Alpha(opacity=50);
background-color: #000;
}
.hp-dialog{
width: 370px;
border-radius: 5px;
background-color: #fff;
outline: 0;
box-shadow: 0 5px 30px rgba(0,0,0,.2);
z-index: 1000000;
position: fixed;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
}
.hp-box{
padding: 12px 30px 30px;
color: #333;
}
.hp-box h1{
line-height: 48px;
text-align: center;
font-size: 20px;
font-weight: 400;
margin-bottom: 12px;
}
.hp-box .error{
color: red;
line-height: 30px;
}
.hp-box input{
display: block;
width: 100%;
height: 42px;
padding: 10px 10px 10px 10px;
border-radius: 3px;
border: 1px solid #e5e5e5;
font-size: 14px;
line-height: 20px;
outline: 0;
-webkit-appearance: none;
appearance: none;
-webkit-transition: border .2s ease;
transition: border .2s ease;
margin-bottom: 30px;
box-sizing: border-box;
}
.hp-box button{
display: block;
width: 100%;
height: 42px;
background-color:#44b336;
border: 0;
border-radius: 3px;
color: #fff;
font-size: 18px;
line-height: 42px;
text-align: center;
outline: 0;
cursor: pointer;
}
.hp-box input:focus,.hp-box input:focus:hover {
border: 1px solid #44b336
}
.hp-box input:hover {
border: 1px solid #ddd
}
.hp-box input::-webkit-input-placeholder {
color: #ddd
}
.hp-box input::-ms-input-placeholder {
color: #ddd
}
.hp-box input::-ms-reveal {
display: none
}
.hp-box input::-ms-clear {
display: none
}
.footer .title{
font-size: 24px;
}
.footer .info{
letter-spacing: 2px;
}</code></pre>
<p>然后修改 <code>views</code> 视图文件,按照继承的方式提取出公用部分。</p>
<h4>增加公用视图</h4>
<p>新建 <code>/views/common/header.html</code></p>
<pre><code class="html"> <header class="header">
<div class="header-box wraper">Node实战教程 | <span class="logo">© iKcamp</span></div>
</header></code></pre>
<p>新建 <code>/views/common/footer.html</code></p>
<pre><code class="html"> <footer class="footer">
<div class="footer-box wraper">
<p class="title">沪江Web前端团队倾情奉献</p>
<p><a href="https://github.com/ikcamp">https://github.com/ikcamp</a></p>
<br>
<p class="info">iKcamp由沪江Web前端团队中热爱原创和翻译的小伙伴发起,成立于2016年7月,"iK"代表布兰登·艾克(JavaScript之父)。 追随JavaScript这门语言所秉持的精神,崇尚开放和自由的我们一同工作、分享、创作,等候更多有趣跳动的灵魂。</p>
<p class="police">沪ICP备17041059号 ©2017-2018 </p>
</div>
</footer></code></pre>
<p>新建 <code>/views/common/layout.html</code>。注意,此处有模板变量 <code>title</code> 。</p>
<pre><code class="html"> <!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{% block head %} {% endblock %}
</head>
<body>
{% include "./header.html" %}
{% block body %}
{% endblock %}
{% include "./footer.html" %}
{% block content %}
{% endblock %}
</body>
</html></code></pre>
<p><code>layout.html</code> 就是我们的基础页面。现在我们再为 <code>home</code> 创建专用的 <code>layout-home.html</code>,并在里面引用之前创建的样式表: </p>
<p>新建 <code>/views/common/layout-home.html</code>。注意,我们在 <code>body</code> 模块里又增加了一个 <code>homeBanner</code> 模块:</p>
<pre><code class="html">{% extends "./layout.html" %}
{% block head %}
<link rel="stylesheet" href="/home/main.css">
{% endblock %}
{% block body %}
{% block homeBanner %}
{% endblock %}
<div class="show_time">
<div class="feature-con">
<ul class="feature fn-clear">
<li class="feature-item"><i class="ico"></i>
<h4 class="tit">免费资源</h4>
<p class="des">为天地立心</p>
</li>
<li class="feature-item"><i class="ico"></i>
<h4 class="tit">体系知识</h4>
<p class="des">为科技立命</p>
</li>
<li class="feature-item"><i class="ico"></i>
<h4 class="tit">实战项目</h4>
<p class="des">为大牛继绝学</p>
</li>
<li class="feature-item"><i class="ico"></i>
<h4 class="tit">线下交流</h4>
<p class="des">为教育开太平</p>
</li>
</ul>
</div>
</div>
{% endblock %}</code></pre>
<p>公用部分提取完成之后,重写 <code>home</code> 交互页面。此时我们对登录功能的视图进行美化,有主页,登录,以及登录后的响应页面。</p>
<h4>重写 home 业务的视图</h4>
<p>新增 <code>/views/home/index.html</code> 首页</p>
<pre><code class="html">{% extends "common/layout-home.html" %}
{% block homeBanner %}
<div class="banner_box">
<div class="banner_inner">
<h2 class="slogan">汇聚天下英才</h2>
<p class="des">iKcamp是由沪江Web前端团队发起的自由组织<br>我们追随JavaScript这门语言所秉持的精神,为ITer提供完善的在线学习平台和知识体系</p>
<a href="/user" title="gogogo" class="btn" id="gogogo">进入战场</a>
</div>
</div>
{% endblock %}</code></pre>
<p>修改 <code>/views/home/login.html</code> 登录页面</p>
<pre><code class="html"> {% extends "common/layout-home.html" %}
{% block homeBanner %}
<div class="banner_box">
<div class="banner_inner">
<h2 class="slogan">汇聚天下英才</h2>
<p class="des">iKcamp是由沪江Web前端团队发起的自由组织<br>我们追随JavaScript这门语言所秉持的精神,为ITer提供完善的在线学习平台和知识体系</p>
<a href="/login" title="gogogo" class="btn" id="gogogo">进入战场</a>
</div>
</div>
{% endblock %}
{% block content %}
<div class="hp-dialog">
<div class="hp-box">
<form action="/user/register" method="post">
<h1>到达战场</h1>
<p class="error">{{content}}</p>
<input type="text" name="name" placeholder="请输入用户名:ikcamp">
<input type="password" name="password" placeholder="请输入密码:123456">
<button>GoGoGo</button>
</form>
</div>
</div>
<div class="hp-overlay"></div>
{% endblock %}</code></pre>
<p>新增 <code>/views/home/success.html</code> 成功页面</p>
<pre><code class="html">{% extends "common/layout-home.html" %}
{% block homeBanner %}
<div class="banner_box">
<div class="banner_inner">
<h2 class="slogan">汇聚天下英才</h2>
<p class="des">iKcamp是由沪江Web前端团队发起的自由组织<br>我们追随JavaScript这门语言所秉持的精神,为ITer提供完善的在线学习平台和知识体系</p>
<a href="javascript:;" title="gogogo" class="btn" id="gogogo">成功进入战场</a>
</div>
</div>
{% endblock %}</code></pre>
<p>增加完成后,需要对 <code>home</code> 的处理逻辑进行修改</p>
<h4>重写 home 处理逻辑</h4>
<p>修改 <code>/service/home.js</code></p>
<pre><code class="js"> module.exports = {
register: async function(name, pwd) {
let data
if(name == 'ikcamp' && pwd == '123456'){
data = {
status: 0,
data: {
title: "个人中心",
content: "欢迎进入个人中心"
}
}
}else{
data = {
status: -1,
data: {
title: '登录失败',
content: "请输入正确的账号信息"
}
}
}
return data
}
}</code></pre>
<p>修改 <code>/controller/home.js</code> 中的 <code>index</code> 和 <code>register</code> 方法:</p>
<pre><code class="js"> const HomeService = require("../service/home")
module.exports = {
// 修改 index 方法
index: async function (ctx, next) {
await ctx.render("home/index", {title: "iKcamp欢迎您"})
},
// 修改 register 方法
register: async function (ctx, next){
let params = ctx.request.body
let name = params.name
let password = params.password
let res = await HomeService.register(name,password)
if(res.status == "-1"){
await ctx.render("home/login", res.data)
}else{
ctx.state.title = "个人中心"
await ctx.render("home/success", res.data)
}
}
}</code></pre>
<p>运行代码,并通过浏览器访问 <code>localhost:3000</code>: </p>
<p><img src="/img/remote/1460000012813155?w=2490&h=920" alt="" title=""></p>
<p>点击进入战场 </p>
<p><img src="/img/remote/1460000012813156?w=1518&h=878" alt="" title=""></p>
<p>验证失败 </p>
<p><img src="/img/remote/1460000012813296?w=1062&h=818" alt="" title=""></p>
<p>验证成功</p>
<p><img src="/img/remote/1460000012813297?w=1332&h=700" alt="" title=""></p>
<p>目前,项目的基本功能都已完善。结构目录如下:</p>
<pre><code class="txt"> ├── controller/
│ ├── home.js
├── service/
│ ├── home.js
├── views/
│ ├── common/
│ ├── header.html
│ ├── footer.html
│ ├── layout.html
│ ├── layout-home.html
│ ├── home/
│ ├── index.html
│ ├── login.html
│ ├── success.html
├── public/
│ ├── home/
│ ├── main.css
├── app.js
├── router.js
├── package.json</code></pre>
<p>在后面的章节中,我们将进一步完善其他功能,例如 <code>JSON</code> 数据传递,错误处理机制,日志记录功能等。</p>
<blockquote>下一篇:提升篇 - 解析JSON——让 Koa2 支持响应 JSON 数据</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=Emr5IEFPU%2FlR22bJ4caTVg%3D%3D.nwgac6eGDmFy%2FqbnUk1fkczlhG8G2tPZYwlEbo72WMIZJojQcQoFk472LNepWZm3" rel="nofollow">iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 视图Nunjucks</a>
</blockquote>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=mdzpPYto%2BkNVcDLbjpP1eQ%3D%3D.X%2BAu76NezV2m%2FO3nHPDJ1kCVJgacxOMN6TCEgd7RyAhOQjiu%2FGYbluOP3venZvlZ" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 视图Nunjucks
https://segmentfault.com/a/1190000012744402
2018-01-08T11:39:08+08:00
2018-01-08T11:39:08+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<p>视频地址:<a href="https://link.segmentfault.com/?enc=8VGGjgB%2FjJdGQB0A%2BxknWQ%3D%3D.za1gpxVoORGsMy5%2FTgqSjQH4ePJQoYnXTd9qK0AmeHVGPFU%2Bj6hXM%2BTyogoy7%2BBs" rel="nofollow">https://www.cctalk.com/v/15114923888328</a></p>
<p><img src="/img/remote/1460000012744407?w=1602&h=966" alt="" title=""></p>
<h2>视图 Nunjucks</h2>
<blockquote>彩虹是上帝和人类立的约,上帝不会再用洪水灭人。</blockquote>
<p>客户端和服务端之间相互通信,传递的数据最终都会展示在视图中,这时候就需要用到『<strong>模板引擎</strong>』。</p>
<h3>什么是模板引擎?</h3>
<p>模板引擎是为了使用户界面与业务数据分离而产生的,可以生成特定格式的文档。例如,用于网站的模板引擎会生成一个标准的 <code>HTML</code> 文档。 </p>
<p>市面上常见的模板引擎很多,例如:<code>Smarty</code>、<code>Jade</code>、<code>Ejs</code>、<code>Nunjucks</code> 等,可以根据个人喜好进行选择。<code>koa-views</code>、<code>koa-nunjucks-2</code> 等支持 <code>Koa</code> 的第三方中间件也可以自行选择。 </p>
<p>本项目中,我们使用 <code>koa-nunjucks-2</code> 作为模板引擎。<code>Nunjucks</code> 是 <code>Mozilla</code> 开发的,纯 <code>js</code> 编写的模板引擎,既可以用在 <code>Node</code> 环境下,也可以运行在浏览器端。<code>koa-nunjucks-2</code> 是基于 <code>Nunjucks</code> 封装出来的第三方中间件,完美支持 <code>Koa2</code>。</p>
<h3>Nunjucks 介绍</h3>
<p>首先我们需要了解 <code>Nunjucks</code> 的几个特性</p>
<h4>简单语法</h4>
<p>变量</p>
<pre><code class="js"> {{ username }}
{{ foo.bar }}
{{ foo["bar"] }}</code></pre>
<p>如果变量的值为 <code>undefined</code> 或 <code>null</code> ,将不予显示。 </p>
<p>过滤器</p>
<pre><code class="js"> {{ foo | title }}
{{ foo | join(",") }}
{{ foo | replace("foo", "bar") | capitalize }}</code></pre>
<p><code>if</code> 判断</p>
<pre><code class="js"> {% if variable %}
It is true
{% endif %}
{% if hungry %}
I am hungry
{% elif tired %}
I am tired
{% else %}
I am good!
{% endif %}</code></pre>
<p><code>for</code> 循环</p>
<pre><code class="js"> var items = [{ title: "foo", id: 1 }, { title: "bar", id: 2}]</code></pre>
<pre><code class="js"> <h1>Posts</h1>
<ul>
{% for item in items %}
<li>{{ item.title }}</li>
{% else %}
<li>This would display if the 'item' collection were empty</li>
{% endfor %}
</ul></code></pre>
<p><code>macro</code> 宏 </p>
<p>宏:定义可复用的内容,类似于编程语言中的函数</p>
<pre><code class="js"> {% macro field(name, value='', type='text') %}
<div class="field">
<input type="{{ type }}" name="{{ name }}"
value="{{ value | escape }}" />
</div>
{% endmacro %}</code></pre>
<p>接下来就可以把 <code>field</code> 当作函数一样使用:</p>
<pre><code class="js"> {{ field('user') }}
{{ field('pass', type='password') }}</code></pre>
<p>更多语法内容请查阅<a href="https://link.segmentfault.com/?enc=YOmVvDN6pbUe8d2b%2BWOVdQ%3D%3D.OYvmzO75XijndWB%2BxC174akqL%2Fye5CgYXO%2BNWm%2FA38fHZm5lSQySLAE1F6lUuoM8zJZT1%2F17QyUFffdtoH1VCA%3D%3D" rel="nofollow">官方文档</a></p>
<h4>继承功能</h4>
<p>网页常见的结构大多是头部、中间体加尾部,同一个网站下的多个网页,头部和尾部内容通常来说基本一致。于是我们可以采用<strong>继承</strong>功能来进行编写。 </p>
<p>先定义一个 <code>layout.html</code></p>
<pre><code class="js"> <html>
<head>
{% block head %}
<link rel="stylesheet">
{% endblock %}
</head>
<body>
{% block header %}
<h1>this is header</h1>
{% endblock %}
{% block body %}
<h1>this is body</h1>
{% endblock %}
{% block footer %}
<h1>this is footer</h1>
{% endblock %}
{% block content %}
<script>
//this is place for javascript
</script>
{% endblock %}
</body>
</html></code></pre>
<p><code>layout</code> 定义了五个模块,分别命名为:<code>head</code>、<code>header</code>、<code>body</code>、<code>footer</code>、<code>content</code>。<code>header</code> 和 <code>footer</code> 是公用的,因此基本不动。业务代码的修改只需要在 <code>body</code> 内容体中进行、业务样式表和业务脚本分别在头部 <code>head</code> 和底部 <code>content</code> 中引入。 </p>
<p>接下来我们再定义一个业务级别的视图页面:<code>home.html</code></p>
<pre><code class="html"> {% extends 'layout.html' %}
{% block head %}
<link href="home.css">
{% endblock %}
{% block body %}
<h1>home 页面内容</h1>
{% endblock %}
{% block content %}
<script src="home.js"></script>
{% endblock%}</code></pre>
<p>最终的 <code>home.html</code> 输出后如下所示:</p>
<pre><code class="html"> <html>
<head>
<link href="home.css">
</head>
<body>
<h1>this is header</h1>
<h1>home 页面内容</h1>
<h1>this is footer</h1>
<script src="home.js"></script>
</body>
</html></code></pre>
<h4>安全性</h4>
<p>请对特殊字符进行转义,防止 <code>Xss</code> 攻击。若在页面上写入 <code>Hello World<script>alert(0)</script></code> 这类字符串变量,并且不进行转义,页面渲染时该脚本就会自动执行,弹出提示框。 </p>
<h3>安装并运行</h3>
<p>安装 <code>koa-nunjucks-2</code>:</p>
<pre><code class="js">npm i koa-nunjucks-2 -S</code></pre>
<p>修改 <code>app.js</code>,引入中间件,并指定存放视图文件的目录 <code>views</code>:</p>
<pre><code class="js"> const Koa = require('koa')
const path = require('path')
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
const app = new Koa()
const router = require('./router')
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, 'views'),// 指定视图目录
nunjucksConfig: {
trimBlocks: true // 开启转义 防Xss
}
}));
app.use(bodyParser())
router(app)
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<p>在之前的项目中,视图被写在了 <code>controller/home</code> 里面,现在我们把它迁移到 <code>views</code> 中: </p>
<p>新建 <code>views/home/login.html</code>:</p>
<pre><code class="html"> <!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<form action="/user/register" method="post">
<input name="name" type="text" placeholder="请输入用户名:ikcamp" />
<br/>
<input name="password" type="text" placeholder="请输入密码:123456" />
<br/>
<button>{{btnName}}</button>
</form>
</body>
</html></code></pre>
<p>重写 <code>controller/home</code> 中的 <code>login</code> 方法:</p>
<pre><code class="js"> login: async(ctx, next) => {
await ctx.render('home/login',{
btnName: 'GoGoGo'
})
},</code></pre>
<p><strong>注意:</strong> 这里我们使用了 <code>await</code> 来异步读取文件。因为需要等待,所以必须保证读取文件之后再进行请求的响应。 </p>
<p>增加了 <code>views</code> 层之后,视图功能还不算完善,我们还需要增加静态资源目录。当然,如果能直接使用静态服务器的话更好。下一节中,我们将讲述下如何增加静态文件及美化项目视图。</p>
<blockquote>下一篇:处理静态资源——指定静态文件目录,设定缓存</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=%2FvV7SXdJ6dMjCQxvr40RDQ%3D%3D.rsrmDOGiIjfiO0PVkkCgEmEuNzGEh%2BliPTvG8RcoQg6R2RMYeg7tuws9Rtb8ymVC" rel="nofollow">iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 代码分层</a>
</blockquote>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=5zLl%2Br7ULc5KWHzugs0aCw%3D%3D.FfARMr4euH8XnneHN%2BiAzTDwpUHknYJY3LfxpxAk%2FbQR66GXCVnpBsVko%2BvAgiHw" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 代码分层
https://segmentfault.com/a/1190000012682159
2018-01-03T11:46:00+08:00
2018-01-03T11:46:00+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<p>视频地址:<a href="https://link.segmentfault.com/?enc=bwRoQnFWffp%2FrfMU4OnrEA%3D%3D.l7J40J5u4ifstkxNk0mgKv33TfYJHkShwtFrDKdSOymBdGXh9HVLdvQ65Yyysv5S" rel="nofollow">https://www.cctalk.com/v/15114923889408</a></p>
<p><img src="/img/remote/1460000012682164?w=1604&h=964" alt="" title=""></p>
<h2>文章</h2>
<p>在前面几节中,我们已经实现了项目中的几个常见操作:启动服务器、路由中间件、<code>Get</code> 和 <code>Post</code> 形式的请求处理等。现在你已经迈出了走向成功的第一步。 </p>
<p>目前,整个示例中所有的代码都写在 <code>app.js</code> 中。然而在业务代码持续增大,场景更加复杂的情况下,这种做法无论是对后期维护还是对患有强迫症的同学来说都不是好事。所以我们现在要做的就是:『分梨』。</p>
<h3>分离 router</h3>
<p>路由部分的代码可以分离成一个独立的文件,并根据个人喜好放置于项目根目录下,或独立放置于 <code>router</code> 文件夹中。在这里,我们将它命名为 <code>router.js</code>并将之放置于根目录下。</p>
<h4>修改路由 router.js</h4>
<pre><code class="js"> const router = require('koa-router')()
module.exports = (app) => {
router.get('/', async(ctx, next) => {
ctx.response.body = `<h1>index page</h1>`
})
router.get('/home', async(ctx, next) => {
console.log(ctx.request.query)
console.log(ctx.request.querystring)
ctx.response.body = '<h1>HOME page</h1>'
})
router.get('/home/:id/:name', async(ctx, next)=>{
console.log(ctx.params)
ctx.response.body = '<h1>HOME page /:id/:name</h1>'
})
router.get('/user', async(ctx, next)=>{
ctx.response.body =
`
<form action="/user/register" method="post">
<input name="name" type="text" placeholder="请输入用户名:ikcamp"/>
<br/>
<input name="password" type="text" placeholder="请输入密码:123456"/>
<br/>
<button>GoGoGo</button>
</form>
`
})
// 增加响应表单请求的路由
router.post('/user/register',async(ctx, next)=>{
let {name, password} = ctx.request.body
if( name == 'ikcamp' && password == '123456' ){
ctx.response.body = `Hello, ${name}!`
}else{
ctx.response.body = '账号信息错误'
}
})
app.use(router.routes())
.use(router.allowedMethods())
}</code></pre>
<h4>修改 app.js</h4>
<pre><code class="js"> const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const app = new Koa()
const router = require('./router')
app.use(bodyParser())
router(app)
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<p>代码看起来清爽了很多。</p>
<p>然而到了这一步,还是不能够高枕无忧。<code>router</code> 文件独立出来以后,应用的主文件 <code>app.js</code> 虽然暂时看起来比较清爽,但这是在只有一个路由,并且处理函数也非常简单的情况下。如果有多个路由,每个处理函数函数代码量也都繁复可观,这就不是主管们喜闻乐见的事情了。</p>
<p>接下来我们对结构进行进一步优化。</p>
<h3>分离 controller 层</h3>
<blockquote>我们把路由对应的业务逻辑也分离出来。</blockquote>
<h4>新增 controller/home.js</h4>
<p>新建 <code>controller</code> 文件夹,增加一个 <code>home.js</code> 文件,并从 <code>router.js</code> 中提取出业务逻辑代码。</p>
<pre><code class="js"> module.exports = {
index: async(ctx, next) => {
ctx.response.body = `<h1>index page</h1>`
},
home: async(ctx, next) => {
console.log(ctx.request.query)
console.log(ctx.request.querystring)
ctx.response.body = '<h1>HOME page</h1>'
},
homeParams: async(ctx, next) => {
console.log(ctx.params)
ctx.response.body = '<h1>HOME page /:id/:name</h1>'
},
login: async(ctx, next) => {
ctx.response.body =
`
<form action="/user/register" method="post">
<input name="name" type="text" placeholder="请输入用户名:ikcamp"/>
<br/>
<input name="password" type="text" placeholder="请输入密码:123456"/>
<br/>
<button>GoGoGo</button>
</form>
`
},
register: async(ctx, next) => {
let {
name,
password
} = ctx.request.body
if (name == 'ikcamp' && password == '123456') {
ctx.response.body = `Hello, ${name}!`
} else {
ctx.response.body = '账号信息错误'
}
}
}</code></pre>
<h4>修改路由 router.js</h4>
<p>修改 <code>router.js</code> 文件,在里面引入 <code>controler/home</code>:</p>
<pre><code class="js"> const router = require('koa-router')()
const HomeController = require('./controller/home')
module.exports = (app) => {
router.get( '/', HomeController.index )
router.get('/home', HomeController.home)
router.get('/home/:id/:name', HomeController.homeParams)
router.get('/user', HomeController.login)
router.post('/user/register', HomeController.register)
app.use(router.routes())
.use(router.allowedMethods())
}</code></pre>
<p>如此,将每个路由的处理逻辑分离到 <code>controller</code> 下的独立文件当中,便于后期维护。 </p>
<p>目前的代码结构已经比较清晰了,适用于以 <code>node</code> 作为中间层、中转层的项目。如果想要把 <code>node</code> 作为真正的后端去操作数据库等,<strong>建议</strong>再分出一层 <code>service</code>,用于处理数据层面的交互,比如调用 <code>model</code> 处理数据库,调用第三方接口等,而<code>controller</code> 里面只做一些简单的参数处理。</p>
<h3>分离 service 层</h3>
<blockquote>这一层的分离,<strong>非必需</strong>,可以根据项目情况适当增加,或者把所有的业务逻辑都放置于 <code>controller</code> 当中。</blockquote>
<h4>新建 service/home.js</h4>
<p>新建 <code>service</code> 文件夹,并于该文件夹下新增一个 <code>home.js</code> 文件,用于抽离 <code>controller/home.js</code> 中的部分代码:</p>
<pre><code class="js"> module.exports = {
register: async(name, pwd) => {
let data
if (name == 'ikcamp' && pwd == '123456') {
data = `Hello, ${name}!`
} else {
data = '账号信息错误'
}
return data
}
}</code></pre>
<h4>修改 controller/home.js</h4>
<pre><code class="js">// 引入 service 文件
const HomeService = require('../service/home')
module.exports = {
// ……省略上面代码
// 重写 register 方法
register: async(ctx, next) => {
let {
name,
password
} = ctx.request.body
let data = await HomeService.register(name, password)
ctx.response.body = data
}
}</code></pre>
<h4><strong>重构完成</strong></h4>
<p>下一节我们将引入视图层 <code>views</code>,还会介绍使用第三方中间件来设置静态资源目录等。新增的部分前端资源代码会让我们的用例更加生动,尽情期待吧。</p>
<blockquote>下一篇:视图nunjucks——Koa 默认支持的模板引擎</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=3%2F2wNt51SfxsY4UaDRSz%2Fw%3D%3D.QVYYZM%2FBLQuqLrSYDlaAfw6AvFcsXzjWUZgkbjgnHMdQdqps0Hi1741jxhv9gtUA" rel="nofollow">iKcamp|基于Koa2搭建Node.js实战(含视频)☞ HTTP请求</a>
</blockquote>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=3pYCc%2BIQV9CvP23MTKqUDA%3D%3D.55A%2B7N29fZ%2BEC78KXzdnXHshzaDxNMRDFRf4XYpvFIny4DwGhz18mubUOPGyCDva" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
iKcamp|基于Koa2搭建Node.js实战(含视频)☞ HTTP请求
https://segmentfault.com/a/1190000012622975
2017-12-28T14:37:48+08:00
2017-12-28T14:37:48+08:00
iKcamp
https://segmentfault.com/u/ikcamp
5
<h3>POST/GET请求——常见请求方式处理</h3>
<h3>?? iKcamp 制作团队</h3>
<p>原创作者:<a href="https://link.segmentfault.com/?enc=YMpxwVU2CFLPJxoaySn3Ag%3D%3D.3g6VAqMetLV2GbHWspsNKInLH%2BIIW%2FnmQYLnhAgGlUo%3D" rel="nofollow">大哼</a>、<a href="https://link.segmentfault.com/?enc=TdcVwDtnRt9q%2FEXq%2BukJMQ%3D%3D.SzzyU%2BeGkXmLoThxb7NA24%2FX5mJZc96MPK%2FjteT6GKg%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=bbej344RlKkzszKXAIBesA%3D%3D.TzDW%2BZ7p8umr2AOwFaaBa6v2B9ZJTwXvAZcQSCASouE%3D" rel="nofollow">三三</a>、<a href="https://link.segmentfault.com/?enc=escHX7W%2Bawf6KdukgUcQTg%3D%3D.JbGoUTlGWqehcUfDeEPQLBhNu1vExtDd1WgIoiAmlSI%3D" rel="nofollow">小虎</a>、<a href="https://link.segmentfault.com/?enc=%2FYAqhljOcJhlAI9Ly3vbwg%3D%3D.r1WncONwNoDev6%2FycHaKPXkGynpqTQfM9hKtCs%2FXq%2B0%3D" rel="nofollow">胖子</a>、<a href="https://link.segmentfault.com/?enc=YXTQwnrqgEi2KJ7tyFh0tA%3D%3D.VxdrZ2krG7JoRSd%2BsYVkpg%3D%3D" rel="nofollow">小哈</a>、<a href="https://link.segmentfault.com/?enc=%2F6ymoAuPj6aM9dV166gSSA%3D%3D.j53Z6F3m1fxdzVtD2nX86eeOe3qUlUo%2F6GFKViFH89o%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=oAjuY%2FdN2UaAFpCTaWgSLg%3D%3D.0%2FttmxumSTtxmRGs0jpQKZ%2FNwdEoeIzA%2BEXrucZc%2BIM%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=9RN7JIddS1c2G3vDUW0b%2Fg%3D%3D.c2hzbVdU%2Bwa4f1ilNiMCb8QCOAtPCICFefVI5MxORlo%3D" rel="nofollow">晃晃</a> <br>文案校对:<a href="https://link.segmentfault.com/?enc=joZZ3ejcui3XWfd0b%2F%2Fkxg%3D%3D.eS0d6NupWFO40b8bazcbA2Sm13yaLJv4H6FQQb023nE%3D" rel="nofollow">李益</a>、<a href="https://link.segmentfault.com/?enc=Zoint4b%2F%2FDBd8vLiotPNvg%3D%3D.ImrQEIr0lRRVxbFmCCKv4vs2w9WzkPP29e8Lv0xQHlY%3D" rel="nofollow">大力萌</a>、<a href="https://link.segmentfault.com/?enc=dv75Jglwz%2B70MlvhiRPFYg%3D%3D.4POCNqQE%2BHrAR45HyRXnmYnXQFgJZTm7Fk4niV3Mn00%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=NYQV4UwKzBHU7V14yejfmw%3D%3D.YyASoa8LqBBelSS8NHre7bbqkjR7hxCRYI8q0r4vCE8%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=QLgo7JqxYEHSsHfDROu8VQ%3D%3D.fKc9%2BmxgbC9BbqK3Qa1gMaf43z3Dtb68kqyBANj13Ic%3D" rel="nofollow">小溪里</a>、<a href="https://link.segmentfault.com/?enc=gWGI41meUTUA3ZYaekUBTA%3D%3D.vwD5ja%2FvkNIQV8A7Nxzyow%3D%3D" rel="nofollow">小哈</a> <br>风采主播:<a href="https://link.segmentfault.com/?enc=yfQi1hDeqIwXg84oqp0Brw%3D%3D.vc3N5X4CEN1w%2Bhi%2B17ImQyHGTsczw6wHXWMXZwPigQQ%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=GGlQlQna2xmOhXtlDl5jkQ%3D%3D.GUGpE2Vb6sXI%2B2XKsIi3KE9VQnrGqulil9L7F46R05c%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=4spKsLQ5p2LvR3mbdYMErw%3D%3D.Fu5B5ClxOBETFS1g7I4lqcI1C2EN5HVFfZ1k%2F%2B0m0ls%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=yF1IphLRgrZmk5D9KwB1ow%3D%3D.Yy6Rilz2yRoSdtCRQhKTSToSzvprVk2OnST59rc4ck8%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=HAomWrt7S9nQ1Y2vOAGJwg%3D%3D.L3ULh6cOx6rO8tWoO%2FDxoA%3D%3D" rel="nofollow">小哈</a> <br>视频剪辑:<a href="https://link.segmentfault.com/?enc=608lf0hWLdeqhMhaHc5upw%3D%3D.2iptcUqMPxRd6oR7bUQT54uI1fxps%2F0FjG4CLk5wVU4%3D" rel="nofollow">小溪里</a> <br>主站运营:<a href="https://link.segmentfault.com/?enc=UxV1n0%2FmWgkfWvt4Qg4prA%3D%3D.irt3%2BRj%2B9M4ICJPHXifvfHMyp8BFru0CM2ydjTEZnJI%3D" rel="nofollow">给力xi</a>、<a href="https://link.segmentfault.com/?enc=cItX%2Bcysy070j4ji7SMaWg%3D%3D.UHr8ZRknrMv5MxoJwQqiZstbplZS%2FiFtf83m3m32F8w%3D" rel="nofollow">xty</a> <br>教程主编:<a href="https://link.segmentfault.com/?enc=eOnGg7vmkedws%2FYLNVHLKQ%3D%3D.H96133krdkgwjkNYQJ9peefvpBRST89npy%2BfGKYEdzU%3D" rel="nofollow">张利涛</a></p>
<hr>
<p>视频地址:<a href="https://link.segmentfault.com/?enc=bunJmB1px4aD7e8s78xPug%3D%3D.xxDte9gFznoe%2Fpw%2Fpdq4qFBSTrkApJdGblFO6OMuVZMBLVy4pLXT04D7Z%2FbmoLII" rel="nofollow">https://www.cctalk.com/v/15114357765870</a></p>
<p><img src="/img/remote/1460000012622980?w=1602&h=964" alt="" title=""></p>
<h2>文章</h2>
<h2>Http 请求</h2>
<blockquote>在学习了 <code>koa-router</code> 之后,我们就可以用它来处理一些常见的请求了,比如 <code>POST/GET</code> 。</blockquote>
<p><br/> </p>
<p><code>koa-router</code> 提供了 <code>.get</code>、<code>.post</code>、<code>.put</code> 和 <code>.del</code> 接口来处理各种请求,但实际业务上,我们大部分只会接触到 <code>POST</code> 和 <code>GET</code>,所以接下来只针对这两种请求类型来说明。 </p>
<p><br/></p>
<p>当我们捕获到请求后,一般都需要把请求带过来的数据解析出来。数据传递过来的方式一般有三种: </p>
<p><br/></p>
<h3>请求参数放在 <code>URL</code> 后面</h3>
<pre><code class="txt">http://localhost:3000/home?id=12&name=ikcamp</code></pre>
<p><br/></p>
<p><code>koa-router</code> 封装的 <code>request</code> 对象,里面的 <code>query</code> 方法或 <code>querystring</code> 方法可以直接获取到 <code>Get</code> 请求的数据,唯一不同的是 <code>query</code> 返回的是对象,而 <code>querystring</code> 返回的是字符串。 </p>
<p>修改 <code>app.js</code>,我们加入解析方式:</p>
<pre><code class="js"> const Koa = require('koa')
const router = require('koa-router')()
const app = new Koa()
router.get('/', async(ctx, next) => {
ctx.response.body = `<h1>index page</h1>`
})
router.get('/home', async(ctx, next) => {
console.log(ctx.request.query)
console.log(ctx.request.querystring)
ctx.response.body = '<h1>HOME page</h1>'
})
router.get('/404', async(ctx, next) => {
ctx.response.body = '<h1>404 Not Found</h1>'
})
// add router middleware:
app.use(router.routes())
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<p><br/></p>
<p>运行代码,并通过浏览器访问 <code>http://localhost:3000/home?id=12&name=ikcamp</code>,然后打开控制台会看到下面的输出内容:</p>
<pre><code class="txt">{ id: '12', name: 'ikcamp' }
id=12&name=ikcamp</code></pre>
<p><br/></p>
<h3>请求参数放在 <code>URL</code> 中间</h3>
<pre><code class="txt">http://localhost:3000/home/12/ikcamp</code></pre>
<p><br/></p>
<p>这种情况下,解析方式肯定与上面的不一样了,<code>koa-router</code> 会把请求参数解析在 <code>params</code> 对象上,我们修改 <code>app.js</code> 文件,增加新的路由来测试下:</p>
<pre><code class="js"> // 增加如下代码
router.get('/home/:id/:name', async(ctx, next)=>{
console.log(ctx.params)
ctx.response.body = '<h1>HOME page /:id/:name</h1>'
})</code></pre>
<p><br/> </p>
<p>运行代码,并通过浏览器访问 <code>http://localhost:3000/home/12/ikcamp</code>,然后查看下控制台显示的日志信息:</p>
<pre><code class="txt">{ id: '12', name: 'ikcamp' } </code></pre>
<p><br/></p>
<h3>请求参数放在 <code>body</code> 中</h3>
<p><br/></p>
<p>当用 <code>post</code> 方式请求时,我们会遇到一个问题:<code>post</code> 请求通常都会通过表单或 <code>JSON</code> 形式发送,而无论是 <code>Node</code> 还是 <code>Koa</code>,都 <strong>没有提供</strong> 解析 <code>post</code> 请求参数的功能。 </p>
<p><br/></p>
<h4>koa-bodyparser 说:『是时候登场了!』</h4>
<p><br/> </p>
<p>首先,安装 <code>koa-bodyparser</code> 包:</p>
<pre><code class="js">npm i koa-bodyparser -S</code></pre>
<p><br/> </p>
<p>安装完成之后,我们需要在 <code>app.js</code> 中引入中间件并应用:</p>
<pre><code class="js"> const Koa = require('koa')
const router = require('koa-router')()
const bodyParser = require('koa-bodyparser')
const app = new Koa()
app.use(bodyParser())
router.get('/', async(ctx, next) => {
ctx.response.body = `<h1>index page</h1>`
})
router.get('/home', async(ctx, next) => {
console.log(ctx.request.query)
console.log(ctx.request.querystring)
ctx.response.body = '<h1>HOME page</h1>'
})
router.get('/home/:id/:name', async(ctx, next)=>{
console.log(ctx.params)
ctx.response.body = '<h1>HOME page /:id/:name</h1>'
})
router.get('/404', async(ctx, next) => {
ctx.response.body = '<h1>404 Not Found</h1>'
})
app.use(router.routes())
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<p>然后我们来试着写一个简单的表单提交实例。修改 <code>app.js</code> 增加如下代码,实现增加表单页面的路由:</p>
<pre><code class="js"> // 增加返回表单页面的路由
router.get('/user', async(ctx, next)=>{
ctx.response.body =
`
<form action="/user/register" method="post">
<input name="name" type="text" placeholder="请输入用户名:ikcamp"/>
<br/>
<input name="password" type="text" placeholder="请输入密码:123456"/>
<br/>
<button>GoGoGo</button>
</form>
`
})</code></pre>
<p><br/></p>
<p>继续修改 <code>app.js</code> 增加如下代码,实现 <code>post</code> 表单提交对应的路由:</p>
<pre><code class="js"> // 增加响应表单请求的路由
router.post('/user/register',async(ctx, next)=>{
let {name, password} = ctx.request.body
if( name === 'ikcamp' && password === '123456' ){
ctx.response.body = `Hello, ${name}!`
}else{
ctx.response.body = '账号信息错误'
}
})</code></pre>
<p><br/> </p>
<p>常见的几种请求,以及相应的参数传递解析,我们已经学习过了。下一节中,我们会把项目整理重构下,做个分层,并引入视图层。</p>
<blockquote>下一篇:代码分层——梳理代码,渐近于 MVC 分层模式</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=GtEIFA6%2Bfx3SBWL6ki2zig%3D%3D.kYCc1b3Fdl0XQmZR%2BdET0RttJoWSLHaZk%2BKXsbSlBwDj%2FZ55kSDiY1r84tZt%2BnfT" rel="nofollow">iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 路由koa-router</a>
</blockquote>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=%2FjJdCKUXvT37zzf2r89F3g%3D%3D.yTUdjlmpJqzfIqZhApGOdn0oQ7MxHUImjLBTSjAv9AvNO%2Bjyseyt49PwaEJ2CxCU" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
iKcamp团队制作|基于Koa2搭建Node.js实战(含视频)☞ 路由koa-router
https://segmentfault.com/a/1190000012568114
2017-12-25T10:58:57+08:00
2017-12-25T10:58:57+08:00
iKcamp
https://segmentfault.com/u/ikcamp
4
<h3>路由koa-router——MVC 中重要的环节:Url 处理器</h3>
<h3>?? iKcamp 制作团队</h3>
<p>原创作者:<a href="https://link.segmentfault.com/?enc=shmvqkShIrXTbIg9akZAiQ%3D%3D.Ry5m6Wf6uTw%2FhsG2TAgx4TTnN0t3yrk441djAyVhjf0%3D" rel="nofollow">大哼</a>、<a href="https://link.segmentfault.com/?enc=VQaDt%2F2G2UPNq4JPBwkofQ%3D%3D.nRUO%2B4PwM4ZMlKPi78Q5fY9yDDixpb2CaMwsCpLIfpA%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=NdXFOw%2F6QLEqgDeKZpdVww%3D%3D.nzjUz4zhYlZR9vIrz2CmWxLEnUqaGPCd%2Bl7ysLR%2FVDI%3D" rel="nofollow">三三</a>、<a href="https://link.segmentfault.com/?enc=5bGTlmcdPAV65A5rOqJwRA%3D%3D.pAyk2b8XoGk2d%2BQrVOsoIeMhgTT5wGcaNlRfa4ASoMo%3D" rel="nofollow">小虎</a>、<a href="https://link.segmentfault.com/?enc=L8Mj%2BYqW9UO5aJu0B%2B2vCg%3D%3D.R4nuYmFQX0ILVeL3Rjv2bgCzBa2aeazmvNTYGm%2B4jjc%3D" rel="nofollow">胖子</a>、<a href="https://link.segmentfault.com/?enc=6IJLdvfQQlQb30bmwL4gjw%3D%3D.cBfxz7vtD2W7NAVUSEFQxQ%3D%3D" rel="nofollow">小哈</a>、<a href="https://link.segmentfault.com/?enc=mnFCGWXJm7rPtTb0kSxkTg%3D%3D.2IEyU25UjlN03ecnulhXlqbwkZkYGKPzMragQtO2WFw%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=jxD52DLfJESti%2FhwB2lrhw%3D%3D.YzrOcdqGYdihzcsXAGaUpkA%2B2hcaqtIVb1vkoo8mwhI%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=h3oIgYYknQx6GluKH2LU0g%3D%3D.BDeXkp7BwQx9iv8SELqSa1AtbHs9N%2F%2BMyT1T%2F2z8QkI%3D" rel="nofollow">晃晃</a> <br>文案校对:<a href="https://link.segmentfault.com/?enc=8iNf6%2BGK6FMKHoGFVp6M4w%3D%3D.fYa5VABBe6pHzRiZ%2BdMze24bMtbclsXfWRj1ZQtgABs%3D" rel="nofollow">李益</a>、<a href="https://link.segmentfault.com/?enc=NZh67iPee3YLgJ%2FdwNQh0w%3D%3D.Qrbg3oO2nOZrmtfRCV6Edu%2F2wn%2BkmoqxMq0zzO8lP04%3D" rel="nofollow">大力萌</a>、<a href="https://link.segmentfault.com/?enc=0v%2B82WPVSaMHQlo4iKYMAQ%3D%3D.L72lk5rLiWgVf94fXYfla31lg5Te88Cpoz2anr1qN2s%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=z9NkKy6m%2F8IebCQlBxWe0w%3D%3D.2QWhnEzMzY8YwPbVPxP5HIhleblVvj1j3%2BtsC7DfMcA%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=BbSyyds2Z8EHLfSKwsU%2Bbw%3D%3D.8Uv5setr19f98UmDe%2B2z%2BaMCkWY6b1nKA1cQaRJr820%3D" rel="nofollow">小溪里</a>、<a href="https://link.segmentfault.com/?enc=VOS1uyp38Yin%2BewJEiS1Mw%3D%3D.EN2UZALzGn%2BMu0pzQ9uk8w%3D%3D" rel="nofollow">小哈</a> <br>风采主播:<a href="https://link.segmentfault.com/?enc=367d9nUb3q7BxivVirKcbw%3D%3D.RJ4DBQP2pVEjA%2BKawtd%2FSMRiR9FIasYBqBvQkU908Yw%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=I2iKIiYrM2lC6NdVYIVfqg%3D%3D.izBjreDn97PXUFjO%2FXnNJOMh1gw7Yc77yEnf9Y5BTzU%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=B7J1tBFOxX%2FyF5xhz9WP9w%3D%3D.hrXD1r086%2BhIqMVl1WAilWKGGMIKBJbuWEsAp4o7YcE%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=pDaVk%2FK%2FKR5s%2Be%2FjUvggDQ%3D%3D.85YP5YgGnoVYcY32cSZEFgt25HbBH1MmmDLgTDlJe6I%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=Yi6ZIuw93V2Oz1%2F%2FA4rFjw%3D%3D.voDQGlp8DTCCRAZEpHi2aA%3D%3D" rel="nofollow">小哈</a> <br>视频剪辑:<a href="https://link.segmentfault.com/?enc=p6HKthIO6SQDQBunj78EmA%3D%3D.44%2B0lOZXCwm8ZZ0k5JqUC90ZluxWmqqlrAQnEcsYa4I%3D" rel="nofollow">小溪里</a> <br>主站运营:<a href="https://link.segmentfault.com/?enc=cwcRa0EzKaub0%2BPNEfhKwA%3D%3D.s5frNFINDHQWH60od0OFW1SN%2Bh5zGw9VQWixw9YX5Dk%3D" rel="nofollow">给力xi</a>、<a href="https://link.segmentfault.com/?enc=8%2BC4eh52HnQzSKJXdeqcYg%3D%3D.BGQcy9KP2OxgEqcqOzszvOzv%2F8pWp2RSaYGlN3YrDGM%3D" rel="nofollow">xty</a> <br>教程主编:<a href="https://link.segmentfault.com/?enc=lp9zjp%2FVl32iXFA%2BNaMIvg%3D%3D.MurD%2FxNnDTyl1Y1qAIaMR1%2BhcSPJhcqk1fCcLoF0ElA%3D" rel="nofollow">张利涛</a></p>
<hr>
<p>视频地址:<a href="https://link.segmentfault.com/?enc=n9KNBmjFw5EMRhQ6wX%2FTiA%3D%3D.EctNe3VUJzd9oDTUze%2FD9Qh7fKquh6cLtwS%2FT0Dbe8%2BCu3H09%2BhlVHRDzRf3xGwE" rel="nofollow">https://www.cctalk.com/v/15114923889954</a></p>
<p><img src="/img/remote/1460000012568119?w=1608&h=968" alt="" title=""></p>
<h2>文章</h2>
<h2>路由 koa-router</h2>
<blockquote>上一节我们学习了中间件的基本概念,本节主要带大家学习下 <code>koa-router</code> 路由中间件的使用方法。</blockquote>
<p><br/></p>
<p>路由是用于描述 <code>URL</code> 与处理函数之间的对应关系的。比如用户访问 <code>http://localhost:3000/</code>,那么浏览器就会显示 <code>index</code> 页面的内容,如果用户访问的是 <code>http://localhost:3000/home</code>,那么浏览器应该显示 <code>home</code> 页面的内容。</p>
<p><br/> </p>
<p>要实现上述功能,如果不借助 <code>koa-router</code> 或者其他路由中间件,我们自己去处理路由,那么写法可能如下所示:</p>
<pre><code class="js">const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
if (ctx.request.path === '/') {
ctx.response.body = '<h1>index page</h1>';
} else {
await next();
}
});
app.use(async (ctx, next) => {
if (ctx.request.path === '/home') {
ctx.response.body = '<h1>home page</h1>';
} else {
await next();
}
});
app.use(async (ctx, next) => {
if (ctx.request.path === '/404') {
ctx.response.body = '<h1>404 Not Found</h1>';
} else {
await next();
}
});
app.listen(3000, ()=>{
console.log('server is running at http://localhost:3000')
})</code></pre>
<p><br/></p>
<p>把上述代码复制并覆盖到 <code>app.js</code> 中,然后执行以下命令启动 <code>node</code> 程序:</p>
<pre><code class="js">node app.js</code></pre>
<p><br/></p>
<p>启动之后在浏览器中分别访问 <code>http://localhost:3000/</code>、<code>http://localhost:3000/home</code>、<code>http://localhost:3000/404</code> 就能看到相应的页面了。</p>
<p><br/></p>
<p>上述 <code>app.js</code> 的代码中,由 <code>async</code> 标记的函数称为『异步函数』,在异步函数中,可以用 <code>await</code> 调用另一个异步函数,<code>async</code> 和 <code>await</code> 这两个关键字将在 ES7 中引入。参数 <code>ctx</code> 是由 <code>koa</code> 传入的,我们可以通过它来访问 <code>request</code> 和 <code>response</code>,<code>next</code> 是 <code>koa</code> 传入的将要处理的下一个异步函数。</p>
<p><strong>注意:</strong> 由于 <code>node</code> 在 <code>v7.6.0</code> 中才支持 <code>async</code> 和 <code>await</code>,所以在运行 <code>app.js</code> 之前请确保 node 版本正确,或者使用一些第三方的 <code>async</code> 库来支持。 </p>
<p><br/></p>
<p>这样的写法能够处理简单的应用,但是,一旦要处理的 <code>URL</code> 多起来的话就会显得特别笨重。所以我们可以借助 <code>koa-router</code> 来更简单的实现这一功能。<br>下面来介绍一下如何正确的使用 <code>koa-router</code>。</p>
<p><br/></p>
<h3>安装 koa-router</h3>
<p>通过 <code>npm</code> 命令直接安装:</p>
<pre><code>npm i koa-router -S</code></pre>
<p><code>-S</code> 或者 <code>--save</code> 是为了安装完成之后能够在 <code>package.json</code> 的 <code>dependencies</code> 中保留 <code>koa-router</code>,以便于下次只需要执行 <code>npm i</code> 或者 <code>npm install</code> 就能够安装所有需要的依赖包。</p>
<p><br/></p>
<h3>基本使用方法</h3>
<p>如果要在 <code>app.js</code> 中使用 <code>koa-router</code> 来处理 <code>URL</code>,可以通过以下代码来实现:</p>
<pre><code class="js">const Koa = require('koa')
// 注意 require('koa-router') 返回的是函数:
const router = require('koa-router')()
const app = new Koa()
// 添加路由
router.get('/', async (ctx, next) => {
ctx.response.body = `<h1>index page</h1>`
})
router.get('/home', async (ctx, next) => {
ctx.response.body = '<h1>HOME page</h1>'
})
router.get('/404', async (ctx, next) => {
ctx.response.body = '<h1>404 Not Found</h1>'
})
// 调用路由中间件
app.use(router.routes())
app.listen(3000, ()=>{
console.log('server is running at http://localhost:3000')
})</code></pre>
<p>运行 <code>app.js</code>:</p>
<pre><code class="js">node app.js</code></pre>
<p>执行完上面的操作之后,我们在浏览器中访问 <code>http://localhost:3000/</code>:</p>
<p><img src="/img/remote/1460000012568120?w=405&h=226" alt="" title=""></p>
<p>在浏览器中访问 <code>http://localhost:3000/home</code>:</p>
<p><img src="/img/remote/1460000012568121?w=399&h=220" alt="" title=""></p>
<p>在浏览器中访问 <code>http://localhost:3000/404</code>:</p>
<p><img src="/img/remote/1460000012568122?w=402&h=225" alt="" title=""></p>
<p>通过上面的例子,我们可以看到和之前不使用 <code>koa-router</code> 的显示效果是一样的。不过使用了 <code>koa-router</code> 之后,代码稍微简化了一些,而且少了 <code>if</code> 判断,还有省略了 <code>await next()</code>(因为没有其他中间件需要执行,所以这里就先省略了)。</p>
<p>当然,除了 <code>GET</code> 方法,<code>koa-router</code> 也支持处理其他的请求方法,比如:</p>
<pre><code class="js">router
.get('/', async (ctx, next) => {
ctx.body = 'Hello World!';
})
.post('/users', async (ctx, next) => {
// ...
})
.put('/users/:id', async (ctx, next) => {
// ...
})
.del('/users/:id', async (ctx, next) => {
// ...
})
.all('/users/:id', async (ctx, next) => {
// ...
});</code></pre>
<p>在 <code>HTTP</code> 协议方法中,<code>GET</code>、<code>POST</code>、<code>PUT</code>、<code>DELETE</code> 分别对应 <code>查</code>,<code>增</code>,<code>改</code>,<code>删</code>,这里 <code>router</code> 的方法也一一对应。通常我们使用 <code>GET</code> 来查询和获取数据,使用 <code>POST</code> 来更新资源。<code>PUT</code> 和 <code>DELETE</code> 使用比较少,但是如果你们团队采用 <code>RESTful架构</code>,就比较推荐使用了。我们注意到,上述代码中还有一个<code>all</code> 方法。<code>all</code> 方法用于处理上述方法无法匹配的情况,或者你不确定客户端发送的请求方法类型。</p>
<p>举个例子,假设客户端使用 <code>jQuery</code> 来开发,有如下几个 <code>ajax</code> 请求:</p>
<pre><code class="js">// 优先匹配和 router.get 方法中 url 规则一样的请求,如果匹配不到的话就匹配和 router.all 方法中 url 规则一样的请求。
$.ajax({
method: "GET",
url: "some.php",
data: { name: "John" }
}).done(function( msg ) {
// do something
});
// 优先匹配和 router.post 方法中 url 规则一样的请求,如果匹配不到的话就匹配和 router.all 方法中 url 规则一样的请求。
$.ajax({
method: "POST",
url: "some.php",
data: { name: "John" }
}).done(function( msg ) {
// do something
});</code></pre>
<p>上面例子中两个方法最主要的区别就是 <code>ajax</code> 中 <code>method</code> 的值,<code>method</code> 的值和 <code>router</code> 的方法一一对应。上述代码中没有处理异常,当请求都无法匹配的时候,我们可以跳转到自定义的 <code>404</code> 页面,比如:</p>
<pre><code class="js">router.all('/*', async (ctx, next) => {
ctx.response.status = 404;
ctx.response.body = '<h1>404 Not Found</h1>';
});</code></pre>
<p><code>*</code> 号是一种通配符,表示匹配任意 <code>URL</code>。这里的返回是一种简化的写法,真实开发中,我们肯定要去读取 <code>HTML</code> 文件或者其他模板文件的内容,再响应请求。关于这部分的内容后面的章节中会详细介绍。</p>
<h3>其他特性</h3>
<h4>命名路由</h4>
<p>在开发过程中我们能够很方便的生成路由 <code>URL</code>:</p>
<pre><code class="js">router.get('user', '/users/:id', function (ctx, next) {
// ...
});
router.url('user', 3);
// => 生成路由 "/users/3"
router.url('user', { id: 3 });
// => 生成路由 "/users/3"
router.use(function (ctx, next) {
// 重定向到路由名称为 “sign-in” 的页面
ctx.redirect(ctx.router.url('sign-in'));
})</code></pre>
<p><code>router.url</code> 方法方便我们在代码中根据路由名称和参数(可选)去生成具体的 <code>URL</code>,而不用采用字符串拼接的方式去生成 <code>URL</code> 了。</p>
<h4>多中间件</h4>
<p><code>koa-router</code> 也支持单个路由多中间件的处理。通过这个特性,我们能够为一个路由添加特殊的中间件处理。也可以把一个路由要做的事情拆分成多个步骤去实现,当路由处理函数中有异步操作时,这种写法的可读性和可维护性更高。比如下面的示例代码所示:</p>
<pre><code class="js">router.get(
'/users/:id',
function (ctx, next) {
return User.findOne(ctx.params.id).then(function(user) {
// 首先读取用户的信息,异步操作
ctx.user = user;
next();
});
},
function (ctx) {
console.log(ctx.user);
// 在这个中间件中再对用户信息做一些处理
// => { id: 17, name: "Alex" }
}
);</code></pre>
<h4>嵌套路由</h4>
<p>我们可以在应用中定义多个路由,然后把这些路由组合起来用,这样便于我们管理多个路由,也简化了路由的写法。</p>
<pre><code class="js">var forums = new Router();
var posts = new Router();
posts.get('/', function (ctx, next) {...});
posts.get('/:pid', function (ctx, next) {...});
forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
// 可以匹配到的路由为 "/forums/123/posts" 或者 "/forums/123/posts/123"
app.use(forums.routes());</code></pre>
<h4>路由前缀</h4>
<p>通过 <code>prefix</code> 这个参数,我们可以为一组路由添加统一的前缀,和嵌套路由类似,也方便我们管理路由和简化路由的写法。不同的是,前缀是一个固定的字符串,不能添加动态参数。</p>
<pre><code class="js">var router = new Router({
prefix: '/users'
});
router.get('/', ...); // 匹配路由 "/users"
router.get('/:id', ...); // 匹配路由 "/users/:id" </code></pre>
<h4>URL 参数</h4>
<p><code>koa-router</code> 也支持参数,参数会被添加到 <code>ctx.params</code> 中。参数可以是一个正则表达式,这个功能的实现是通过 <code>path-to-regexp</code> 来实现的。原理是把 <code>URL</code> 字符串转化成正则对象,然后再进行正则匹配,之前的例子中的 <code>*</code> 通配符就是一种正则表达式。</p>
<pre><code class="js">router.get('/:category/:title', function (ctx, next) {
console.log(ctx.params);
// => { category: 'programming', title: 'how-to-node' }
});</code></pre>
<p>通过上面的例子可以看出,我们可以通过 <code>ctx.params</code> 去访问路由中的参数,使得我们能够对参数做一些处理后再执行后续的代码。</p>
<p>使用了 <code>koa-router</code> 之后,代码简洁了很多。下一节中,我们将学习下如何响应浏览器的各种请求。</p>
<blockquote>下一篇:POST/GET请求——常见请求方式处理</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=MMts4AuEU4wo89uwr%2F04vw%3D%3D.eJ%2F8Q96XMQQ%2BikkEVwOn0dFJ1llMcN1sfr5SaqR433EgZ6MaLk5G%2B9h4bQv9wM08" rel="nofollow">iKcamp团队制作|基于Koa2搭建Node.js实战(含视频)☞ 中间件用法</a>
</blockquote>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4>1. <a href="https://link.segmentfault.com/?enc=7AJViNXsvIzGD81gJD53LQ%3D%3D.ZcmUWsHANiKOGbU8wfTbxjCue26jOqYoy13bgZ27gchug7ZHgc3eRed5GqehmRnC" rel="nofollow">干货|人人都是翻译项目的Master</a>
</h4>
<h4>2. <a>iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a>
</h4>
iKcamp团队制作|基于Koa2搭建Node.js实战(含视频)☞ 中间件用法
https://segmentfault.com/a/1190000012539418
2017-12-22T11:11:50+08:00
2017-12-22T11:11:50+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<h3>中间件用法——讲解 Koa2 中间件的用法及如何开发中间件</h3>
<h3>?? iKcamp 制作团队</h3>
<p>原创作者:<a href="https://link.segmentfault.com/?enc=TBsJrZZIwWFNls58ubofZQ%3D%3D.1aEJtp3rRnJ5r%2F8ycuHkejZXWONNmL4t96FRranPfwo%3D" rel="nofollow">大哼</a>、<a href="https://link.segmentfault.com/?enc=tHT%2BHgE3z%2BRUjwP1lk5L8A%3D%3D.MkexFULFg3N7iVTNVgbZlzYkERJpjEgpzsXL%2FCEHcrc%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=N8pMCZyH%2FEnZST8yisYImw%3D%3D.llj%2FNcE0ssAbQlbgIMGla06pRli%2Bxoy8evXIing%2ByBg%3D" rel="nofollow">三三</a>、<a href="https://link.segmentfault.com/?enc=lxXduVpH7ng1vN3VC3yyAg%3D%3D.IursIcGwl60p2U5f91i%2B42%2B25Toiol06pRq3H33sVHY%3D" rel="nofollow">小虎</a>、<a href="https://link.segmentfault.com/?enc=i7kI30aRzg7n%2FV6pErES5Q%3D%3D.WJDPArt%2FO0DO%2F46DzDlCe2CW2EoF85pwB1fzhacWq5I%3D" rel="nofollow">胖子</a>、<a href="https://link.segmentfault.com/?enc=qwwnI%2BL9Em%2FGfo%2B4QpNOzA%3D%3D.PirzLNO1hbrc790i55i9%2Bg%3D%3D" rel="nofollow">小哈</a>、<a href="https://link.segmentfault.com/?enc=VR4uHIVBjftG7ZbjMEodNw%3D%3D.AgLflkR4I7%2FgiRzKY5QefozRLEL5fQ5b0CK9hip7aH8%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=xIkMtl7zYgTFp860v8GZCA%3D%3D.qtHMGb5aqO8aFocUc9yM0p4pNtkSx%2FxS5R2r%2F%2F42gz8%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=y%2BXuS7ek8w7pPXnPTBp3FA%3D%3D.R3udR%2F6pts9driYCWN8XZYr%2BHTrFyPFV%2FpMcx2tk4GA%3D" rel="nofollow">晃晃</a> <br>文案校对:<a href="https://link.segmentfault.com/?enc=%2BWKpw11ACO1gOfPpC5GKbg%3D%3D.Cy9W%2FiPuyAO74wmiJI3je3uTt01vubxIzfBLP0EbfAU%3D" rel="nofollow">李益</a>、<a href="https://link.segmentfault.com/?enc=fbUF5ygUWCsphhpZj5ELgw%3D%3D.u3sXLkmFBdla3s8p3mxH1ltkS3IVqxjvxxMEuiKjF7Y%3D" rel="nofollow">大力萌</a>、<a href="https://link.segmentfault.com/?enc=szCeuQPqBfqsv%2BnT1D7CeQ%3D%3D.gAWw7iL0MNY1vgYCS3ABqQEcb8yPBNjA41TS6XjpDMQ%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=H3DkwCCd3kTZZG9WX3Pd1A%3D%3D.4nPqtWLI5qAv1FmOolPzkEzvdG0fvncgrAyWcpdDLw0%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=dmPXBG5UQJtDtMlJ8LOJrQ%3D%3D.bxyT4hXdbXL0i3%2F6XUnWrpZnU9lcPjL3L5YTxYrY%2FDs%3D" rel="nofollow">小溪里</a>、<a href="https://link.segmentfault.com/?enc=Sr7JG%2FP2qY5%2BoIDoAbLbNw%3D%3D.NBUTX1HRs3UFTGHJ8ZKTBQ%3D%3D" rel="nofollow">小哈</a> <br>风采主播:<a href="https://link.segmentfault.com/?enc=1vQR6LaPEEk6aKqEu6aXmg%3D%3D.MTT0zsfOHYMVa%2FSnMuOvOAGAks5TZwpMiYzNy38djDE%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=qRG%2FVm%2Bo4SEzquBDtyPacw%3D%3D.pVO0nTKJCEYPRSOofYNb1gQY9r4%2BUaGzEk8juQ35r%2FE%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=EfVUflUYtqRkA%2Buq8%2BUoKA%3D%3D.kJ2sMT2Z5Jvr7VDk1xg8MQ1%2FFBVijq0Eigc1XIPPhQM%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=X6PTc8fgJiU0s2v%2FdKG7dg%3D%3D.TSmiXzDShdMDUtV5N0EVZJEMQLV7ocLUiTS%2BX%2F7aVVA%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=5FRdx8nWDr04SuBQE%2FsTQw%3D%3D.NDe2Euagb6R8uVBoY44JdQ%3D%3D" rel="nofollow">小哈</a> <br>视频剪辑:<a href="https://link.segmentfault.com/?enc=sN4B1TAEurqYrmJfPOICPQ%3D%3D.fL7s42WSF9Qnb4VVwYmFNhykZR1HxKJf%2FQuX36YmrKQ%3D" rel="nofollow">小溪里</a> <br>主站运营:<a href="https://link.segmentfault.com/?enc=DxAUmS67vTR4CJ5PD72OOg%3D%3D.ZJlwn6veJmPFR1s7QmV9Cq572DOw0s%2FSa8C%2FCcEWiEk%3D" rel="nofollow">给力xi</a>、<a href="https://link.segmentfault.com/?enc=INPBlBvkXI6Grna1SZQBog%3D%3D.Vz33NrFQNPET6lzg9FOD8YTZs6SPIdgei3cY1FTjXqs%3D" rel="nofollow">xty</a> <br>教程主编:<a href="https://link.segmentfault.com/?enc=Tiv1fU420S9kLkD0lTjFBA%3D%3D.XzVdGFZK8ecVqGZ%2B8QfZZbdlGFigsQnBXrJrnoNWsZ0%3D" rel="nofollow">张利涛</a></p>
<hr>
<p>视频地址:<a href="https://link.segmentfault.com/?enc=xMELwfRZHBjWvWLUhKgaDw%3D%3D.XGoEMLhUxLReddKad0eQSIDk2lFZkGFnv0duXFrL3OuuBrJl%2FYN5LBFllZ7UIIhf" rel="nofollow">https://www.cctalk.com/v/15114357763623</a></p>
<p><img src="/img/remote/1460000012539423?w=1614&h=968" alt="" title=""></p>
<h2>文章</h2>
<h3>middleware 中间件</h3>
<blockquote>正是因为中间件的扩展性才使得 <code>Koa</code> 的代码简单灵活。</blockquote>
<p>在 <code>app.js</code> 中,有这样一段代码:</p>
<pre><code class="js">app.use(async (ctx, next)=>{
await next()
ctx.response.type = 'text/html'
ctx.response.body = '<h1>Hello World</h1>'
})</code></pre>
<p>它的作用是:每收到一个 <code>http</code> 请求,<code>Koa</code> 都会调用通过 <code>app.use()</code> 注册的 <code>async</code> 函数,同时为该函数传入 <code>ctx</code> 和 <code>next</code> 两个参数。而这个 <code>async</code> 函数就是我们所说的中间件。</p>
<p>下面我们简单介绍一下传入中间件的两个参数。</p>
<h3>ctx</h3>
<p><code>ctx</code> 作为上下文使用,包含了基本的 <code>ctx.request</code> 和 <code>ctx.response</code>。另外,还对 <code>Koa</code> 内部对一些常用的属性或者方法做了代理操作,使得我们可以直接通过 <code>ctx</code> 获取。比如,<code>ctx.request.url</code> 可以写成 <code>ctx.url</code>。</p>
<p>除此之外,<code>Koa</code> 还约定了一个中间件的存储空间 <code>ctx.state</code>。通过 <code>state</code> 可以存储一些数据,比如用户数据,版本信息等。如果你使用 <code>webpack</code> 打包的话,可以使用中间件,将加载资源的方法作为 <code>ctx.state</code> 的属性传入到 <code>view</code> 层,方便获取资源路径。</p>
<h3>next</h3>
<p><code>next</code> 参数的作用是将处理的控制权转交给下一个中间件,而 <code>next()</code> 后面的代码,将会在下一个中间件及后面的中间件(如果有的话)执行结束后再执行。</p>
<p><strong>注意:</strong> 中间件的顺序很重要! </p>
<p>我们重写 <code>app.js</code> 来解释下中间件的流转过程:</p>
<pre><code class="js">// 按照官方示例
const Koa = require('koa')
const app = new Koa()
// 记录执行的时间
app.use(async (ctx, next) => {
let stime = new Date().getTime()
await next()
let etime = new Date().getTime()
ctx.response.type = 'text/html'
ctx.response.body = '<h1>Hello World</h1>'
console.log(`请求地址: ${ctx.path},响应时间:${etime - stime}ms`)
});
app.use(async (ctx, next) => {
console.log('中间件1 doSoming')
await next();
console.log('中间件1 end')
})
app.use(async (ctx, next) => {
console.log('中间件2 doSoming')
await next();
console.log('中间件2 end')
})
app.use(async (ctx, next) => {
console.log('中间件3 doSoming')
await next();
console.log('中间件3 end')
})
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<p>运行起来后,控制台显示:</p>
<pre><code class="txt">server is running at http://localhost:3000</code></pre>
<p>然后打开浏览器,访问 <code>http://localhost:3000</code>,控制台显示内容更新为:</p>
<pre><code class="txt">server is running at http://localhost:3000
中间件1 doSoming
中间件2 doSoming
中间件3 doSoming
中间件3 end
中间件2 end
中间件1 end
请求地址: /,响应时间:2ms</code></pre>
<p>从结果上可以看到,流程是一层层的打开,然后一层层的闭合,像是剥洋葱一样 —— 洋葱模型。</p>
<p>此外,如果一个中间件没有调用 <code>await next()</code>,会怎样呢?答案是『后面的中间件将不会执行』。 </p>
<p>修改 <code>app.js</code> 如下,我们去掉了第三个中间件里面的 <code>await</code>:</p>
<pre><code class="js">const Koa = require('koa')
const app = new Koa()
// 记录执行的时间
app.use(async (ctx, next)=>{
let stime = new Date().getTime()
await next()
let etime = new Date().getTime()
ctx.response.type = 'text/html'
ctx.response.body = '<h1>Hello World</h1>'
console.log(`请求地址: ${ctx.path},响应时间:${etime - stime}ms`)
});
app.use(async (ctx, next) => {
console.log('中间件1 doSoming')
await next();
console.log('中间件1 end')
})
app.use(async (ctx, next) => {
console.log('中间件2 doSoming')
// 注意,这里我们删掉了 next
// await next()
console.log('中间件2 end')
})
app.use(async (ctx, next) => {
console.log('中间件3 doSoming')
await next();
console.log('中间件3 end')
})
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<p>重新运行代码后,控制台显示如下:</p>
<pre><code class="txt">server is running at http://localhost:3000
中间件1 doSoming
中间件2 doSoming
中间件2 end
中间件1 end
请求地址: /,响应时间:1ms</code></pre>
<p>与我们的预期结果『后面的中间件将不会执行』是一致的。</p>
<blockquote>下一篇:我们将学习下如何响应浏览器的各种请求。</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=EbtE9cpWoIeApeZEs2F8Wg%3D%3D.lqFyfMMNvHIbY1Se0agyUKNI%2BzSA0ZL%2F7vgoBcC6mw1qXzj83YNtNbPjehVM%2FPXQ" rel="nofollow">iKcamp团队制作|基于Koa2搭建Node.js实战(含视频)☞ 环境准备</a>
</blockquote>
<h3>推荐: 翻译项目Master的自述:</h3>
<h4><a href="https://link.segmentfault.com/?enc=N8mdUsbdNN8T5Y4dpcLjjA%3D%3D.nJSxdtt49VAb0CufjVWxDzDFpbkwRe7waGxf87Q3TzjavMryaRWkj9933LD3vgue" rel="nofollow">干货|人人都是翻译项目的Master</a></h4>
带你玩转小程序开发实践|含直播回顾视频
https://segmentfault.com/a/1190000012520676
2017-12-21T10:37:17+08:00
2017-12-21T10:37:17+08:00
iKcamp
https://segmentfault.com/u/ikcamp
1
<blockquote>作者:张利涛,视频课程《微信小程序教学》、《基于Koa2搭建Node.js实战项目教学》主编,沪江前端架构师<p>本文原创,转载请注明作者及出处</p>
</blockquote>
<ul>
<li><a href="#xcx1">小程序和 H5 区别</a></li>
<li><a href="#xcx2">小程序的运行过程</a></li>
<li><a href="#xcx3">解决小程序接口不支持 Promise 的问题</a></li>
<li><a href="#xcx4">小程序组件化开发及通信</a></li>
</ul>
<h2>小程序和 H5 区别</h2>
<blockquote>我们不一样,不一样,不一样。</blockquote>
<h3>运行环境 runtime</h3>
<p>首先从官方文档可以看到,小程序的运行环境并不是浏览器环境:</p>
<pre><code class="txt">小程序框架提供了自己的视图层描述语言 WXML 和 WXSS,以及基于 JavaScript 的逻辑层框架,并在视图层与逻辑层间提供了数据传输和事件系统,可以让开发者可以方便的聚焦于数据与逻辑上。
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。同一进程内的 WebView 实际上会共享一个 JS VM,如果 WebView 内 JS 线程正在执行渲染或其他逻辑,会影响 evaluateJavascript 脚本的实际执行时间,另外多个 WebView 也会抢占 JS VM 的执行权限;另外还有 JS 本身的编译执行耗时,都是影响数据传输速度的因素。</code></pre>
<p>而所谓的运行环境,对于任何语言的运行,它们都需要有一个环境——runtime。浏览器和 Node.js 都能运行 JavaScript,但它们都只是指定场景下的 runtime,所有各有不同。而小程序的运行环境,是微信定制化的 runtime。</p>
<p>大家可以做一个小实验,分别在浏览器环境和小程序环境打开各自的控制台,运行下面的代码来进行一个 20 亿次的循环:</p>
<pre><code class="js">var k
for (var i = 0; i < 2000000000; i++) {
k = i
}</code></pre>
<p>浏览器控制台下运行时,当前页面是完全不能动,因为 JS 和视图共用一个线程,相互阻塞。</p>
<p>小程序控制台下运行时,当前视图可以动,如果绑定有事件,也会一样触发,只不过事件的回调需要在 『循环结束』 之后。</p>
<p>视图层和逻辑层如果共用一个线程,优点是通信速度快(离的近就是好),缺点是相互阻塞。比如浏览器。</p>
<p>视图层和逻辑层如果分处两个环境,优点是相互不阻塞,缺点是通信成本高(异地恋)。比如小程序的 <code>setData</code>,通信一次就像是写情书!</p>
<p>所以,严格来说,小程序是微信定制的混合开发模式。</p>
<h3>在 JavaScript 的基础上,小程序做了一些修改,以方便开发小程序。</h3>
<ul>
<li>增加 App 和 Page 方法,进行程序和页面的注册。【增加了 Component】</li>
<li>增加 getApp 和 getCurrentPages 方法,分别用来获取 App 实例和当前页面栈。</li>
<li>提供丰富的 API,如微信用户数据,扫一扫,支付等微信特有能力。【调用原生组件:Cordova、ReactNative、Weex 等】</li>
<li>每个页面有独立的作用域,并提供模块化能力。</li>
<li>由于框架并非运行在浏览器中,所以 JavaScript 在 web 中一些能力都无法使用,如 document,window 等。【小程序的 JsCore 环境】</li>
<li>开发者写的所有代码最终将会打包成一份 JavaScript,并在小程序启动的时候运行,直到小程序销毁。类似 ServiceWorker,所以逻辑层也称之为 App Service。</li>
</ul>
<h3>与传统的 HTML 相比,WXML 更像是一种模板式的标签语言</h3>
<p>从实践体验上看,我们可以从小程序视图上看到 Java FreeMarker 框架、Velocity、smarty 之类的影子。</p>
<p>小程序视图支持如下</p>
<pre><code class="txt">数据绑定 {{}}
列表渲染 wx:for
条件判断 wx:if
模板 tempalte
事件 bindtap
引用 import include
可在视图中应用的脚本语言 wxs
...</code></pre>
<p>Java FreeMarker 也同样支持上述功能。</p>
<pre><code class="txt">数据绑定 ${}
列表渲染 list指令
条件判断 if指令
模板 FTL
事件 原生事件
引用 import include 指令
内建函数 比如『时间格式化』
可在视图中应用的脚本语言 宏 marco
...</code></pre>
<h2> 小程序的运行过程</h2>
<ol>
<li>我们在微信上打开一个小程序 <br>微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。</li>
<li>微信 App 从微信服务器下载小程序的文件包 <br>为了流畅的用户体验和性能问题,小程序的文件包不能超过 2M。另外要注意,小程序目录下的所有文件上传时候都会打到一个包里面,所以尽量少用图片和第三方的库,特别是图片。</li>
<li>解析 app.json 配置信息初始化导航栏,窗口样式,包含的页面列表</li>
<li>加载运行 app.js<br>初始化小程序,创建 app 实例</li>
<li>根据 app.json,加载运行第一个页面初始化第一个 Page</li>
<li>路由切换 <br>以栈的形式维护了当前的所有页面。最多 5 个页面。出栈入栈</li>
</ol>
<h2> 解决小程序接口不支持 Promise 的问题</h2>
<p>小程序的所有接口,都是通过传统的回调函数形式来调用的。回调函数真正的问题在于他剥夺了我们使用 return 和 throw 这些关键字的能力。而 Promise 很好地解决了这一切。</p>
<p>那么,如何通过 Promise 的方式来调用小程序接口呢?</p>
<p>查看一下小程序的官方文档,我们会发现,几乎所有的接口都是同一种书写形式:</p>
<pre><code class="js">wx.request({
url: "test.php", //仅为示例,并非真实的接口地址
data: {
x: "",
y: ""
},
header: {
"content-type": "application/json" // 默认值
},
success: function(res) {
console.log(res.data)
},
fail: function(res) {
console.log(res)
}
})</code></pre>
<p>所以,我们可以通过简单的 Promise 写法,把小程序接口装饰一下。代码如下:</p>
<pre><code class="js">wx.request2 = (option = {}) => {
// 返回一个 Promise 实例对象,这样就可以使用 then 和 throw
return new Promise((resolve, reject) => {
option.success = res => {
// 重写 API 的 success 回调函数
resolve(res)
}
option.fail = res => {
// 重写 API 的 fail 回调函数
reject(res)
}
wx.request(option) // 装饰后,进行正常的接口请求
})
}</code></pre>
<p>上述代码简单的展现了如何把一个请求接口包装成 Promise 形式。但在实战项目中,可能有多个接口需要我们去包装处理,每一个都单独包装是不现实的。这时候,我们就需要用一些技巧来处理了。</p>
<p>其实思路很简单:我们把需要 Promise 化的『接口名字』存放在一个『数组』中,然后对这个数组进行循环处理。</p>
<p>这里我们利用了 ECMAScript5 的特性 Object.defineProperty 来重写接口的取值过程。</p>
<pre><code class="js">let wxKeys = [
// 存储需要Promise化的接口名字
"showModal",
"request"
]
// 扩展 Promise 的 finally 功能
Promise.prototype.finally = function(callback) {
let P = this.constructor
return this.then(
value => P.resolve(callback()).then(() => value),
reason =>
P.resolve(callback()).then(() => {
throw reason
})
)
}
wxKeys.forEach(key => {
const wxKeyFn = wx[key] // 将wx的原生函数临时保存下来
if (wxKeyFn && typeof wxKeyFn === "function") {
// 如果这个值存在并且是函数的话,进行重写
Object.defineProperty(wx, key, {
get() {
// 一旦目标对象访问该属性,就会调用这个方法,并返回结果
// 调用 wx.request({}) 时候,就相当于在调用此函数
return (option = {}) => {
// 函数运行后,返回 Promise 实例对象
return new Promise((resolve, reject) => {
option.success = res => {
resolve(res)
}
option.fail = res => {
reject(res)
}
wxKeyFn(option)
})
}
}
})
}
})</code></pre>
<p><strong>注:</strong> Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。</p>
<p>用法也很简单,我们把上述代码保存在一个 js 文件中,比如 utils/toPromise.js,然后在 app.js 中引入就可以了:</p>
<pre><code class="js">import "./util/toPromise"
App({
onLoad() {
wx
.request({
url: "http://www.weather.com.cn/data/sk/101010100.html"
})
.then(res => {
console.log("come from Promised api, then:", res)
})
.catch(err => {
console.log("come from Promised api, catch:", err)
})
.finally(res => {
console.log("come from Promised api, finally:")
})
}
})</code></pre>
<h2>小程序组件化开发</h2>
<p>小程序从 1.6.3 版本开始,支持简洁的组件化编程</p>
<h3>官方支持组件化之前的做法</h3>
<pre><code class="js">// 组件内部实现
export default class TranslatePop {
constructor(owner, deviceInfo = {}) {
this.owner = owner;
this.defaultOption = {}
}
init() {
this.applyData({...})
}
applyData(data) {
let optData = Object.assign(this.defaultOption, data);
this.owner && this.owner.setData({
translatePopData: optData
})
}
}
// index.js 中调用
translatePop = new TranslatePop(this);
translatePop.init();</code></pre>
<p>实现方式比较简单,就是在调用一个组件时候,把当前环境的上下文 content 传递给组件,在组件内部实现 setData 调用。</p>
<h3>应用官方支持的方式来实现</h3>
<p>官方组件示例:</p>
<pre><code class="js">Component({
properties: {
// 这里定义了innerText属性,属性值可以在组件使用时指定
innerText: {
type: String,
value: "default value"
}
},
data: {
// 这里是一些组件内部数据
someData: {}
},
methods: {
// 这里是一个自定义方法
customMethod: function() {}
}
})</code></pre>
<h3>结合 Redux 实现组件通信</h3>
<p>在 React 项目中 Redux 是如何工作的</p>
<ul>
<li>
<p>单一数据源</p>
<blockquote>整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。</blockquote>
</li>
<li>
<p>State 是只读的</p>
<blockquote>惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象</blockquote>
</li>
<li>
<p>使用纯函数来执行修改</p>
<blockquote>为了描述 action 如何改变 state tree ,你需要编写 reducers。</blockquote>
</li>
<li>Props 传递 —— Render 渲染</li>
</ul>
<p>如果你有看过 Redux 的源码就会发现,上述的过程可以简化描述如下:</p>
<ol>
<li>订阅:监听状态————保存对应的回调</li>
<li>发布:状态变化————执行回调函数</li>
<li>同步视图:回调函数同步数据到视图</li>
</ol>
<p>第三步:同步视图,在 React 中,State 发生变化后会触发 Render 来更新视图。 </p>
<p>而小程序中,如果我们通过 setData 改变 data,同样可以更新视图。</p>
<p>所以,我们实现小程序组件通信的思路如下:</p>
<ol>
<li>观察者模式/发布订阅模式</li>
<li>装饰者模式/Object.defineProperty (Vuejs 的设计路线)</li>
</ol>
<h3>在小程序中实现组件通信</h3>
<p>先预览下我们的最终项目结构:</p>
<pre><code class="txt">├── components/
│ ├── count/
│ ├── count.js
│ ├── count.json
│ ├── count.wxml
│ ├── count.wxss
│ ├── footer/
│ ├── footer.js
│ ├── footer.json
│ ├── footer.wxml
│ ├── footer.wxss
├── pages/
│ ├── index/
│ ├── ...
│ ├── log/
│ ├── ...
├── reducers/
│ ├── counter.js
│ ├── index.js
│ ├── redux.min.js
├── utils/
│ ├── connect.js
│ ├── shallowEqual.js
│ ├── toPromise.js
├── app.js
├── app.json
├── app.wxss</code></pre>
<h4>1. 实现『发布订阅』功能</h4>
<p>首先,我们从 cdn 或官方网站获取 redux.min.js,放在结构里面</p>
<p>创建 reducers 目录下的文件:</p>
<pre><code class="js">// /reducers/index.js
import { createStore, combineReducers } from './redux.min.js'
import counter from './counter'
export default createStore(combineReducers({
counter: counter
}))
// /reducers/counter.js
const INITIAL_STATE = {
count: 0,
rest: 0
}
const Counter = (state = INITIAL_STATE, action) => {
switch (action.type) {
case "COUNTER_ADD_1": {
let { count } = state
return Object.assign({}, state, { count: count + 1 })
}
case "COUNTER_CLEAR": {
let { rest } = state
return Object.assign({}, state, { count: 0, rest: rest+1 })
}
default: {
return state
}
}
}
export default Counter</code></pre>
<p>我们定义了一个需要传递的场景值 <code>count</code>,用来代表例子中的『点击次数』,<code>rest</code> 代表『重置次数』。</p>
<p>然后在 app.js 中引入,并植入到小程序全局中:</p>
<pre><code class="js">//app.js
import Store from './reducers/index'
App({
Store,
})</code></pre>
<h4>2. 利用 『装饰者模式』,对小程序的生命周期进行包装,状态发生变化时候,如果状态值不一样,就同步 setData</h4>
<pre><code class="js">// 引用了 react-redux 中的工具函数,用来判断两个状态是否相等
import shallowEqual from './shallowEqual'
// 获取我们在 app.js 中植入的全局变量 Store
let __Store = getApp().Store
// 函数变量,用来过滤出我们想要的 state,方便对比赋值
let mapStateToData
// 用来补全配置项中的生命周期函数
let baseObj = {
__observer: null,
onLoad() { },
onUnload() { },
onShow() { },
onHide() { }
}
let config = {
__Store,
__dispatch: __Store.dispatch,
__destroy: null,
__observer() {
// 对象中的 super,指向其原型 prototype
if (super.__observer) {
super.__observer()
return
}
const state = __Store.getState()
const newData = mapStateToData(state)
const oldData = mapStateToData(this.data || {})
if (shallowEqual(oldData, newData)) {// 状态值没有发生变化就返回
return
}
this.setData(newData)
},
onLoad() {
super.onLoad()
this.__destroy = this.__Store.subscribe(this.__observer)
this.__observer()
},
onUnload() {
super.onUnload()
this.__destroy && this.__destroy() & delete this.__destroy
},
onShow() {
super.onShow()
if (!this.__destroy) {
this.__destroy = this.__Store.subscribe(this.__observer)
this.__observer()
}
},
onHide() {
super.onHide()
this.__destroy && this.__destroy() & delete this.__destroy
}
}
export default (mapState = () => { }) => {
mapStateToData = mapState
return (options = {}) => {
// 补全生命周期
let opts = Object.assign({}, baseObj, options)
// 把业务代码中的 opts 配置对象,指定为 config 的原型,方便『装饰者调用』
Object.setPrototypeOf(config, opts)
return config
}
}</code></pre>
<p>调用方法:</p>
<pre><code class="js">// pages/index/index.js
import connect from "../../utils/connect"
const mapStateToProps = (state) => {
return {
counter: state.counter
}
}
Page(connect(mapStateToProps)({
data: {
innerText: "Hello 点我加1哦"
},
bindBtn() {
this.__dispatch({
type: "COUNTER_ADD_1"
})
}
}))</code></pre>
<p>最终效果展示: </p>
<p><img src="/img/remote/1460000012520681?w=270&h=480" alt="" title=""></p>
<p>项目源码地址:<br><a href="https://link.segmentfault.com/?enc=inbHxl%2BJ%2BczPBBWIPs5PWQ%3D%3D.TmwUbQwl14fIHt2CwwCApxiRH%2BX9zasOT4wzJof92pADQ%2BzO9nhXvvhJzOFbaNb4" rel="nofollow">https://github.com/ikcamp/xcx-redux</a></p>
<p>直播视频地址:<br><a href="https://link.segmentfault.com/?enc=KuNy8l3NBDbh0kFKwIfQTQ%3D%3D.02V6MBaxinmB%2BvkDBrA%2Bz5bWenqXJ49GMLzrbfVt31UQalnZkcegxjoh2rCWZ1dA" rel="nofollow">https://www.cctalk.com/v/15137361643293</a></p>
<blockquote>iKcamp官网:<a href="https://link.segmentfault.com/?enc=9UdBA2T12uD6MHUF4WPtOw%3D%3D.guQ2xjY3o0U%2BSKqv%2FRJ5TmuIITZCLueHGtMIF3CNzEo%3D" rel="nofollow">https://www.ikcamp.com</a>
</blockquote>
<p><img src="/img/remote/1460000012362370?w=1426&h=778" alt="" title=""></p>
<blockquote>iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=BnEkq2XozzyhgO%2F4N7f87g%3D%3D.%2BcAY%2FM5n8Xt7N6NZZenE1T9Hdnp%2FZVnaxP%2FIeq8YHfj22L8ZKGSSiq6yMfMo6K3k" rel="nofollow">开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍</a><p><a href="https://link.segmentfault.com/?enc=2iDpkJyBlKodM9nOuWrTgA%3D%3D.Jc0loySaJrF85y%2F%2BXlZDl37MvdATEm5k7J7949SsUd43t%2FnXQopWLF%2BAi7zIdNEE" rel="nofollow">沪江iKcamp出品微信小程序教学共5章16小节汇总(含视频)</a></p>
</blockquote>
iKcamp&掘金Podcast直播回顾(12月16号的两场)
https://segmentfault.com/a/1190000012502389
2017-12-20T10:58:06+08:00
2017-12-20T10:58:06+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<h2>12月16号-第一场-戴亮-沪江基于Node.js大规模应用实践</h2>
<blockquote>戴亮:《移动Web前端高效开发实战》作者之一,GMTC荣誉讲师,沪江Web前端架构师。<p>在线地址:<a href="https://link.segmentfault.com/?enc=dRXGmvPalRAf%2FzEC6e0HEw%3D%3D.y5bVq4iVKrl2XOSscAEa6Q%2BxKAtJ0Hy0Cy5Z80SGeOKoxnCK5PKzjpnK63gdaV80" rel="nofollow">https://www.cctalk.com/v/15137361642948</a></p>
</blockquote>
<p><img src="/img/remote/1460000012502394?w=1588&h=914" alt="" title=""></p>
<h2>12月16号-第二场-张利涛-带你玩转小程序开发实践</h2>
<blockquote>张立涛:视频教程<a href="https://link.segmentfault.com/?enc=ZUl2CQpxM6Xc2QlsPBOSdA%3D%3D.FGIkKtjXJOSbt%2BZIPPBvIK50V2w%2BXVPNmAeGGJDpV0QCkN7b3A8lQLIOzXJqorqJ" rel="nofollow">《微信小程序课程》</a>、<a href="https://link.segmentfault.com/?enc=7yN0RCMZrzvJBxGW2OB1VA%3D%3D.SMv3vcsLZj5ZjMPvUtdpsDadCPVrFVwsVcS4MOdy48u8DB7X%2FZsbXjdyw3WmhzwT" rel="nofollow">《基于Koa2搭建Node.js实战项目教程》</a>主编,沪江Web前端架构师。<p>在线地址:<a href="https://link.segmentfault.com/?enc=pQsq4aQHUAETk3lJOZ9s9Q%3D%3D.VS3ZQKBifngs3RFfk2x%2B4Pq5ECccey3U4dl01MhwUv5gqOOMCOPCb9ppXsF6wOqJ" rel="nofollow">https://www.cctalk.com/v/15137361643293</a></p>
</blockquote>
<p><img src="/img/remote/1460000012502395?w=1580&h=924" alt="" title=""></p>
<blockquote>iKcamp官网:<a href="https://link.segmentfault.com/?enc=xkEBBvUlycEK9ycutaxisA%3D%3D.p5tzEupD0LmLPKFAoSgcqgeVIQrWsYPZ2W2XhLUpI%2BM%3D" rel="nofollow">https://www.ikcamp.com</a>
</blockquote>
<p><img src="/img/remote/1460000012362370?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000012362371?w=860&h=860" alt="" title=""></p>
iKcamp团队制作|基于Koa2搭建Node.js实战项目教学(含视频)☞ 环境准备
https://segmentfault.com/a/1190000012470011
2017-12-18T11:30:50+08:00
2017-12-18T11:30:50+08:00
iKcamp
https://segmentfault.com/u/ikcamp
3
<h3>安装搭建项目的开发环境</h3>
<p>视频地址:<a href="https://link.segmentfault.com/?enc=cpJJG0opCQXxvYFXARWvqA%3D%3D.WSdAvtwMyLAhuYNy3DifVWd67lhj63CQpCzA3Z086w%2BpBEsjuwjJBwfhPNicfm1c" rel="nofollow">https://www.cctalk.com/v/15114357764004</a></p>
<p><img src="/img/remote/1460000012470016?w=1214&h=718" alt="" title=""></p>
<h2>文章</h2>
<h3>Koa 起手 - 环境准备</h3>
<p>由于 <code>koa2</code> 已经开始使用 <code>async/await</code> 等新语法,所以请保证 <code>node</code> 环境在 <code>7.6</code> 版本以上。</p>
<h4>安装node.js</h4>
<ul>
<li>直接安装 node.js :node.js官网地址 <a href="https://link.segmentfault.com/?enc=8nJymbvO9P%2BWjWQMAEcG9w%3D%3D.zP4yoDiLdx32mdJmv%2BX4jYl%2Fdj0nghHn49VkIszaVXs%3D" rel="nofollow">https://nodejs.org</a>
</li>
<li>
<p>nvm管理多版本 node.js :可以用nvm 进行node版本进行管理</p>
<pre><code>- Mac 系统安装 nvm [https://github.com/creationix/nvm#manual-install](https://github.com/creationix/nvm#manual-install)
- windows 系统安装 nvm [https://github.com/coreybutler/nvm-windows](https://github.com/coreybutler/nvm-windows)
- Ubuntu 系统安装 nvm [https://github.com/creationix/nvm](https://github.com/creationix/nvm)
</code></pre>
</li>
</ul>
<h3>项目初始化</h3>
<blockquote>身为程序员,初入江湖第一招:『Hello World』</blockquote>
<p>首先,创建一个目录 <code>koa2-tutorial/</code> 用来存放我们的代码。然后开始初始化项目:</p>
<pre><code class="js">// 创建 package.json 文件。该文件用于管理项目中用到一些安装包
npm init</code></pre>
<p>项目初始化完成后,在创建的目录里,新建文件 <code>app.js</code> 并在里面写下:</p>
<pre><code class="js">console.log('Hello World')</code></pre>
<p>现在,我们的项目结构应该如下:</p>
<pre><code class="txt">├── app.js
├── package.json</code></pre>
<p>打开控制台,进入目录 <code>koa2-tutorial/</code> 并输入:</p>
<pre><code class="js">node app.js</code></pre>
<p>成功输出 <code>Hello World</code>,说明环境正常。至此,我们的准备工作完成。</p>
<p>下面我们会基于 <code>Koa2</code> 启动服务器。</p>
<h3>启动服务器</h3>
<p>运行如下命令,安装 <code>Koa</code> (版本信息会自动保存在 <code>package.json</code> 中)</p>
<pre><code class="js">// 安装 koa,并将版本信息保存在 package.json 中
npm i koa -S</code></pre>
<p>重写 <code>app.js</code>,增加如下代码:</p>
<pre><code class="js">const Koa = require('koa')
const app = new Koa()
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<p>运行 <code>node app.js</code> 并打开浏览器访问 <code>localhost:3000</code>,页面显示 <code>Not Found</code>。 </p>
<p>因为在启动服务器后,代码并没有做其他的事情,也就没有了交互。</p>
<p>我们继续修改 <code>app.js</code> 文件:</p>
<pre><code class="js">const Koa = require('koa')
const app = new Koa()
// 增加代码
app.use(async (ctx, next) => {
await next()
ctx.response.type = 'text/html'
ctx.response.body = '<h1>Hello World</h1>'
})
app.listen(3000, () => {
console.log('server is running at http://localhost:3000')
})</code></pre>
<p>重启服务器并再次访问,这时页面将正常显示 <code>Hello World</code>。</p>
<p>在增加的代码里面,用到了 <code>Koa</code> 的「中间件」,那么什么是「中间件」呢?下一节我们会为大家详细讲述。</p>
<blockquote>下一篇:《中间件用法——讲解 Koa2 中间件的用法及如何开发中间件(含视频)》</blockquote>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<blockquote>上一篇:iKcamp新课程推出啦~~~~~<a href="https://link.segmentfault.com/?enc=nliz8%2FpgfgJmdvwIuVG3dw%3D%3D.m6Y5WiA5OdfGz4oddlZ8sUVsHuZ47EMXEc8WzpgvdxdCTPkm37X%2FoHBraM6sbb2m" rel="nofollow">开始免费连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍</a>
</blockquote>
开始连载啦~每周2更共11堂iKcamp课|基于Koa2搭建Node.js实战项目教学(含视频)| 课程大纲介绍
https://segmentfault.com/a/1190000012423295
2017-12-14T11:52:47+08:00
2017-12-14T11:52:47+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<h3>?? 与众不同的学习方式,为你打开新的编程视角</h3>
<ul>
<li>
<p>独特的『同步学习』方式</p>
<ul><li>文案讲解+视频演示,文字可激发深层的思考、视频可还原实战操作过程。</li></ul>
</li>
<li>
<p>云集一线大厂有真正实力的程序员</p>
<ul><li>iKcamp 团队云集一线大厂经验丰厚的码农,开源奉献各教程。</li></ul>
</li>
<li>
<p>改版自真实的线上项目</p>
<ul><li>教程项目并非网上随意 <code>Demo</code>,而是来源于真实线上项目,并改版定制为教程项目</li></ul>
</li>
<li>
<p>源码开放</p>
<ul><li>课程案例代码完全开放给你,你可以根据所学知识自行修改、优化。</li></ul>
</li>
</ul>
<p><img src="/img/remote/1460000012423300?w=1122&h=692" alt="" title=""></p>
<h3>?? 玩转 Node.js 同时全面掌握潮流技术</h3>
<ul>
<li>采用新一代的 Web 开发框架—— Koa2 ——更小、更富有表现力、更健壮。</li>
<li>使用 fs、buffer、http、path 等 Node.js 最核心 API。</li>
<li>融合多种常见的需求场景:网络请求、JSON 解析、模板引擎、静态资源、日志记录、错误请求处理。</li>
<li>结合 async await (ES6/7) 语句中转中间件控制权,解决回调地狱问题。</li>
</ul>
<p><img src="/img/remote/1460000012423301?w=450&h=608" alt="" title=""></p>
<h3>?? 适合人群及技术储备要求</h3>
<blockquote>如果你是一个有全栈梦想的前端开发者,或是想要入门 <code>Node.js</code>,那么来学习本课程,学完不仅实现你的全栈梦想,更让你无缝衔<br>接 <code>Node</code> 应用公司的现代前端开发体系和流程。</blockquote>
<ul>
<li>Node.js</li>
<li>ES6/7 语法知识</li>
<li>了解 HTTP 协议</li>
</ul>
<p><img src="/img/remote/1460000012423302?w=340&h=408" alt="" title=""></p>
<h3>?? 亮点的课程设计,让你对 Node.js 豁然开朗</h3>
<p>本课程项目GitHub地址:<a href="https://link.segmentfault.com/?enc=azo%2BrKCiOJTDwcTblWkSSQ%3D%3D.XHZEC0e%2Bqq4kN%2F1cFhIbwQVzxrP%2Fh4nwUsX84ZOqbW2rQUTwObTA9tPAujAoEt8r" rel="nofollow">https://github.com/ikcamp/koa2-tutorial</a></p>
<blockquote>P.S. 不要吝啬你的Star,你的Star是iKcamp的动力!</blockquote>
<ul>
<li>
<p>基础篇</p>
<ul>
<li>环境准备——安装搭建项目的开发环境</li>
<li>中间件用法——讲解 Koa2 中间件的用法及如何开发中间件</li>
<li>路由koa-router——MVC 中重要的环节:Url 处理器</li>
<li>POST/GET请求——常见请求方式处理</li>
<li>代码分层——梳理代码,渐近于 MVC 分层模式</li>
<li>视图nunjucks——Koa 默认支持的模板引擎</li>
<li>处理静态资源——指定静态文件目录,设定缓存</li>
</ul>
</li>
<li>
<p>提升篇</p>
<ul>
<li>解析JSON——让 Koa2 支持响应 JSON 数据</li>
<li>记录日志——开发日志中间件,记录项目中的各种形式信息</li>
<li>错误处理——处理 HTTP 特定错误请求场景</li>
<li>规范与部署——制定合适的团队规范,提升开发效率</li>
</ul>
</li>
</ul>
<h2>大纲介绍</h2>
<p>视频地址:<a href="https://link.segmentfault.com/?enc=8okLtU%2FRXXbjO9pwsIQnRA%3D%3D.AZb9IYqfaU9iLlItwnmnNn7hiQf81YM3WmAkcjXDhreL7KWAgfauqgFSZCWXmzDm" rel="nofollow">https://www.cctalk.com/v/15114357769946</a></p>
<p><img src="/img/remote/1460000012423303?w=1242&h=718" alt="" title=""></p>
<h3>?? 以 git 分布式版本控制系统,来学习和管理项目代码</h3>
<ol><li>通过 <code>git</code> 把项目复制到本地</li></ol>
<pre><code class="git">git clone https://github.com/ikcamp/koa2-tutorial</code></pre>
<ol><li>切换目录</li></ol>
<pre><code class="shell">cd koa2-tutorial</code></pre>
<ol><li>在当前目录下切换分支</li></ol>
<pre><code class="git">git checkout 0-start</code></pre>
<ol><li>进入到项目目录 <code>code</code>
</li></ol>
<pre><code class="shell">cd code/</code></pre>
<p><strong>注意:</strong> 所有的分支命名上,都以数字开头,序号就是我们的开发顺序和讲解顺序。</p>
<p><strong>注意:</strong> 分支中的 <code>code/</code> 目录为当节课程后的完整代码。 </p>
<h3>?? 下载完整项目代码</h3>
<blockquote>教程的完整代码在主干 <code>master</code> 中,请自行<a href="https://link.segmentfault.com/?enc=xgNKWLA8kkfVSmN65zSueg%3D%3D.Ow0PF8NR9idPEifVob0TS7TgfIy87%2By7XiKVCSGS1rP8emeN0deoy3FNYgYC04yWVsbAWxTHTbfiREUj%2F2U7fg%3D%3D" rel="nofollow">查阅? </a>
</blockquote>
<h3>? 问答交流专区</h3>
<p>关于课程的问题都可随时在 <a href="https://link.segmentfault.com/?enc=yIWdkk4lpL8NMLcIIfsSMA%3D%3D.NhHiVFn9%2FJIyVw7FTnrAGN3wiKynaIvpGs60xmcTU2cRztrU0K7KXcnaH7z2ahlL" rel="nofollow">GitHub</a> 或 QQ群(661407609) 提问,iKcamp会集中答疑。</p>
<blockquote><a href="https://link.segmentfault.com/?enc=8dJi%2BEqRv1PB4v1OagUb9Q%3D%3D.QK1%2F9RMxk0m6TPSZ%2FspWiHzQln7nbdRTpV0KvYEMlnk%3D" rel="nofollow">https://www.ikcamp.com</a></blockquote>
<h3>?? iKcamp 制作团队</h3>
<p>原创作者:<a href="https://link.segmentfault.com/?enc=P7ofGAQYq1DdqbpNryL9LA%3D%3D.CjnBRuFTNf5rYYewoQMuPcglqr1i%2B2tLomXJzQgBMN8%3D" rel="nofollow">大哼</a>、<a href="https://link.segmentfault.com/?enc=XAjjK8ZQaYFuYjxS9rQZXw%3D%3D.v%2BShcx0Z%2FkkANUGWbJm1ScLIwXVklakeVb6uwlgv5UU%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=UJ9FGG5c7O18Tz5Ityo7vQ%3D%3D.UgnIGtg%2BbZh17aTmi2eg4qPGg4LO%2FNMD8WY5yXnaaCw%3D" rel="nofollow">三三</a>、<a href="https://link.segmentfault.com/?enc=7skLrQgAmM0XpsnUDtlV2A%3D%3D.Pwz%2BWzMaO5tZ6Y%2Bwtii5jNJMCtWZNHMt00whjF%2FpUAM%3D" rel="nofollow">小虎</a>、<a href="https://link.segmentfault.com/?enc=UOi9dQYPDgTUSGQGxO9nZg%3D%3D.dGm35bkdoQGMAmqadb8Vzkij62YL9rT2w34D90uDhmA%3D" rel="nofollow">胖子</a>、<a href="https://link.segmentfault.com/?enc=Uu9JwXIi7jWodVzRrmQstw%3D%3D.R48ib%2B5Z%2BB%2FGlwnCfZwoVQ%3D%3D" rel="nofollow">小哈</a>、<a href="https://link.segmentfault.com/?enc=ZzhBjmEvcdvgFVZjz%2FRXXQ%3D%3D.CunDz7i4HPzCXo%2BO5B6tGy9oB0%2B97%2BPktRnpeaZxYo4%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=MGdcKxhh%2Bsasis4AXGfUtQ%3D%3D.3C9B5Qv4a4L0th4K6ZS8wPvHXfqhfhXx%2BmbWXIOeTOk%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=zYMWjtcZNvZWWvfKO8qLEw%3D%3D.4XpDS%2FO%2FtOSXBlJ%2F5ntzULdnVp8yaMg8KoHK48G%2BVRk%3D" rel="nofollow">晃晃</a> <br>文案校对:<a href="https://link.segmentfault.com/?enc=1v4EiLi%2FW4%2FWtSnkK%2FFW%2FA%3D%3D.KaqBrUL0eU5a9RlwFwNnckMBw1nlAGEAFxU5ga9fYmI%3D" rel="nofollow">李益</a>、<a href="https://link.segmentfault.com/?enc=vZxFhVglS8PCy7hQpUKOLA%3D%3D.6VNgJrydNQ%2BTvq%2BAOgvss10j0KQ261p5WBbpRQHTGsk%3D" rel="nofollow">大力萌</a>、<a href="https://link.segmentfault.com/?enc=TjwRNI1Mgz8hkIdlzZ6XFQ%3D%3D.ysJEu5lxacUGWSKdTBku1q069YGZPyCsPvt9L%2BVVpAU%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=2aWazlb2I6m8NUHtqv5fHQ%3D%3D.K0dY3Pi7dEhzqEd9m67dckDc4oY4X7hIDLCYgdh2ztg%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=CY9VmcLZeovk12W5CXE0hA%3D%3D.7LPoKkgGRq4%2BhWNNdyyzVwT2RdWiPjm4QF%2FNl6C8lXU%3D" rel="nofollow">小溪里</a>、<a href="https://link.segmentfault.com/?enc=YeTaxt1akPezU3s4ay1WOQ%3D%3D.qajHWYnTEoWQ4ISRY%2Fp0iA%3D%3D" rel="nofollow">小哈</a> <br>风采主播:<a href="https://link.segmentfault.com/?enc=gQ8p%2FcM1NsY5Ol4hsUe%2FOA%3D%3D.n9cSE6DLgoWSDiGU%2FIJSq2S2DSc%2FQIf8JS8%2FKHuMoTU%3D" rel="nofollow">可木</a>、<a href="https://link.segmentfault.com/?enc=kEfcPLxr0OV73E8PYgX4wg%3D%3D.uE0EE1sBNZ6nIR5mFTu7N4ntaD96aQfz23DuNuCMUKU%3D" rel="nofollow">阿干</a>、<a href="https://link.segmentfault.com/?enc=16tFsgdqt6snBxVJy%2FgWow%3D%3D.RJ8lIkhGfLB4mS7pl6z%2BNDyBw5Aq%2BFqv%2FgwQ1%2BR%2BaEo%3D" rel="nofollow">Au</a>、<a href="https://link.segmentfault.com/?enc=QyAmGJtk%2FWMZQdk0Ljtlnw%3D%3D.je9T00Pd7qdJUAdeCPXN50C6J2mIoKhv8n9vesZaT7k%3D" rel="nofollow">DDU</a>、<a href="https://link.segmentfault.com/?enc=IaF%2B3OMus66uYH%2FJWFpjNQ%3D%3D.Msqi%2FC2awNg8re%2BoVZpj0w%3D%3D" rel="nofollow">小哈</a> <br>视频剪辑:<a href="https://link.segmentfault.com/?enc=v6ie6iSLUqLDTpofOLjSEA%3D%3D.b1JFPuguo2yBGEJS1wX6mPR98yS8wB%2FmmY6npibirJE%3D" rel="nofollow">小溪里</a> <br>主站运营:<a href="https://link.segmentfault.com/?enc=Nl5rDHUQrsWeNw%2FiUzVpOg%3D%3D.9YuycIxguo49Y7H2Ikjrx5OJeXzd%2Ff3ALrJkeUy9msg%3D" rel="nofollow">给力xi</a>、<a href="https://link.segmentfault.com/?enc=z%2BYZcoVdli2zOZ4Gzv%2FPwQ%3D%3D.8F2FdT7NrjXZ9sNva%2BS%2BbNFbQ5EMLVJGJ2A1zbZvzBo%3D" rel="nofollow">xty</a> <br>教程主编:<a href="https://link.segmentfault.com/?enc=3470r%2FMN9ee4ih3QkWjvDg%3D%3D.khCDPS2aJR5Sw2ixMjelRUo5ASL2u%2BmLbw5%2B4Cew9Gk%3D" rel="nofollow">张利涛</a></p>
<h3>"大前端课堂"小程序(含所有iKcamp出品免费课程!!!)</h3>
<p><img src="/img/remote/1460000012423304?w=860&h=860" alt="" title=""></p>
<p><img src="/img/remote/1460000012423305?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
全本 | iKcamp翻译 | 《JavaScript 轻量级函数式编程》|《你不知道的JS》姊妹篇
https://segmentfault.com/a/1190000012394641
2017-12-12T17:36:39+08:00
2017-12-12T17:36:39+08:00
iKcamp
https://segmentfault.com/u/ikcamp
26
<ul>
<li>原文地址:<a href="https://link.segmentfault.com/?enc=cJ5qhG3MAYJzVh%2BFUsx9bQ%3D%3D.pl3Ukg1gGtxehAtR%2Fudv%2Ba4NryaVX1cTCVzgd7LMNmCbrZKLznG3iMgQytJYfHaB" rel="nofollow">Functional-Light-JS</a>
</li>
<li>原文作者:<a href="https://link.segmentfault.com/?enc=vKF3JtgZT2lIKpaDKatSFw%3D%3D.4wVxmcoCjWBHntkxHLDKP3CsFbeHeGESLBPzOs4dCBc%3D" rel="nofollow">Kyle Simpson - 《You-Dont-Know-JS》作者</a>
</li>
</ul>
<blockquote>
<p>译者团队(排名不分先后):<a href="https://link.segmentfault.com/?enc=ZYhkFrOQhA0Q8D6SFSxyhg%3D%3D.A685y0rYUqwVUYZxGNN87YF1PRISzWSAxf7ufi3Dnlw%3D" rel="nofollow">阿希</a>、<a href="https://link.segmentfault.com/?enc=%2BW5d9K677YGY%2Fr%2B2F1BfhA%3D%3D.vnbyebCX0y9TWrOU%2BZsT5Z1ATHsQ1lTyqCM%2BMlIDuRM%3D" rel="nofollow">blueken</a>、<a href="https://link.segmentfault.com/?enc=iq15N%2B7Xe1fQx%2FxKBHOMFA%3D%3D.KDEKn8JilZr1SnwPLldt3HmLleXxXohZux2ziwFu5Jw%3D" rel="nofollow">brucecham</a>、<a href="https://link.segmentfault.com/?enc=wT5glOXF1Ck9qxfRGiUByQ%3D%3D.VwqaXppiYnsH6UiFNFEUJe4nYodUgoRojWwb027Mr0Q%3D" rel="nofollow">cfanlife</a>、<a href="https://link.segmentfault.com/?enc=uiHRTQPSTci4Q3ooyjn%2FDw%3D%3D.Ad55%2FC25SEn%2BZXkvO5BwYpq1UAkDzu2R3%2Fr3E0Zyaf4%3D" rel="nofollow">dail</a>、<a href="https://link.segmentfault.com/?enc=Z6%2FemV15eKAstUkS3yMdSw%3D%3D.SKo2lJv6HpAYqWT7QKNLM2c60RNF3jzIOrMSWnVxzMg%3D" rel="nofollow">kyoko-df</a>、<a href="https://link.segmentfault.com/?enc=9FgmhfP2prNsCpKbG3RPrQ%3D%3D.meESKxB3lqpCEZl7f1rkvVs8HDgGXOxm0je%2F1z7azUg%3D" rel="nofollow">l3ve</a>、<a href="https://link.segmentfault.com/?enc=xe24I%2FOrVfTXt6w9wW6LsA%3D%3D.Vsk6KNh%2BxsN%2FD2JNGgppE8jg3JTayyM1WPRG4hziLs4%3D" rel="nofollow">lilins</a>、<a href="https://link.segmentfault.com/?enc=%2BYIr40V0OOaDn%2BEsiWDlng%3D%3D.S9PTT%2FqqGn4FKHVKP4FfBazYermUuwvTjX8iD1MLiJi9zqqDjxw48uOQQ7oJ1r1D" rel="nofollow">LittlePineapple</a>、<a href="https://link.segmentfault.com/?enc=NJX6eXyrT4MCfwki%2BFVPeA%3D%3D.PUJsQMki0bd0dZysIquYnI1kVN7ej7Igj5vUGpcaHf0%3D" rel="nofollow">MatildaJin</a>、<a href="https://link.segmentfault.com/?enc=NZnfwWxuGoJrzd8nUX7XCA%3D%3D.UQYl0grGlb8puhJjlkpxc2ShU0P3kSRXOnP%2FTuwjgEU%3D" rel="nofollow">冬青</a>、<a href="https://link.segmentfault.com/?enc=CgpkOyJkNIqwoEVQvwfrXA%3D%3D.RJUGPKnXN%2FtEJEh%2FCNmVtt42uI2LvTj52yvakQqR1hM%3D" rel="nofollow">pobusama</a>、<a href="https://link.segmentfault.com/?enc=%2BeyoIneOL73kQBXjDxE1wA%3D%3D.IeWqfr1sNAPhFqjOjORCJp%2B5hhqr8jui9mAVx7bpNCp%2Fx0BSEvoojj9Uu5gFmuHy" rel="nofollow">Cherry</a>、<a href="https://link.segmentfault.com/?enc=lefOqEL1BOADIjlS0s5EeQ%3D%3D.1h3bpCF2iJ5nCID7SyyaxKLVlcfc5cw74kRDLbJ%2BgEM%3D" rel="nofollow">萝卜</a>、<a href="https://link.segmentfault.com/?enc=fqjvt4mNoPjF1Rl8ZGb0SA%3D%3D.VCsAF%2BdPXQQ5mO0%2BTk2U5KVYjDG%2Bt4xsc7MqnRwUfwg%3D" rel="nofollow">vavd317</a>、<a href="https://link.segmentfault.com/?enc=XQ38zM5PHXaKQGoibnWAPA%3D%3D.9wnPrMBPplBU9CZmsy1zGgrXRm9dG6f9grCWzwwY9Kw%3D" rel="nofollow">vivaxy</a>、<a href="https://link.segmentfault.com/?enc=HlNXc8FrWfkvK0WdHMfNrg%3D%3D.B2jbBE14tKlkbgykb6UvX3L4ss7GUNwbvA%2FlLb0pChQ%3D" rel="nofollow">萌萌</a>、<a href="https://link.segmentfault.com/?enc=z9nS0M9tkcNekxxg70LaIg%3D%3D.bKyhL92RVsSdjfJwZG2K5VrwI0a5Jw91dfTCnhO3AQU%3D" rel="nofollow">zhouyao</a></p>
<p>关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。</p>
</blockquote>
<p>本书主要探索函数式编程<sup><a href="#note1">[1]</a></sup>(FP)的核心思想。在此过程中,作者不会执着于使用大量复杂的概念来进行诠释,这也是本书的特别之处。我们在 JavaScript 中应用的仅仅是一套基本的函数式编程概念的子集。我称之为“轻量级函数式编程(FLP)”。</p>
<p><strong>注释:</strong> 题目中使用了“轻量”二字,然而这并不是一本“轻松的”“入门级”书籍。本书是严谨的,充斥着各种复杂的细节,适合拥有扎实 JS 知识基础的阅读者进行研读。“轻量”意味着范围缩小。通常来说,关于函数式编程的 JavaScript 书籍都热衷于拓展阅读者的知识面,并企图覆盖更多的知识点。而本书则对于每一个话题都进行了深入的探究,尽管这种探究是小范围进行的。</p>
<p>让我们面对这个事实:除非你已经是函数式编程高手中的一员(至少我不是!),否则类似“一个单子仅仅是自函子中的幺半群”这类说法对我们来说毫无意义。</p>
<p>这并不是说,各种复杂繁琐的概念是<strong>无意义</strong>的,更不是说,函数式编程者滥用了它们。一旦你完全掌握了轻量的函数式编程内容,你将会/但愿会想要对函数式编程的各种概念进行更正式更系统的学习,并且你一定会对它们的意义和原因有更深入的理解。</p>
<p>但是我更想要让你能够<strong>现在</strong>就把一些函数式编程的基础运用到 JavaScript 编程过程中去,因为我相信这会帮助你写出更优秀的,更<strong>符合逻辑</strong>的代码。</p>
<p><strong>更多关于本书背后的动机和各种观点讨论,请参看[前言]。</strong></p>
<h2>JavaScript 轻量级函数式编程</h2>
<h3>目录</h3>
<ul>
<li>
<a href="https://link.segmentfault.com/?enc=EnnHRZj6RweQ2Sdv39ThDw%3D%3D.gLRvq3jtuzIPhfxOJrIXs01TFryiRIuMTwWiR5CuB%2FZ%2F0aASDpvRDqsXrZczgwwQHYn%2BKOL8ptqBrv84BKo7tHSIeGMZwweB1ROoTmwX0T0%3D" rel="nofollow">引言</a> (by <a href="https://link.segmentfault.com/?enc=YlE1x0wakLN%2Bm5Q5bNJOcg%3D%3D.XZOWaU3W823zaoWK%2Bl6qEPmuNKVmRuzy1KRXdP60ZfU%3D" rel="nofollow">Brian Lonsdorf aka "Prof Frisby"</a>)</li>
<li><a href="https://link.segmentfault.com/?enc=9YQTPHrfJcz67vzHhCc8ig%3D%3D.c9aKvPXHF%2BpYYXPWzyl9xFkmQo78je6Hs1bqF92Dr02QEhwXqt9jIItkIWSpyPNI%2Fc9RcVGqBxXyvxCO51QdxK0I%2BzqZq4a1zNHLQ9rYyMQ%3D" rel="nofollow">前言</a></li>
<li>
<p><a href="https://link.segmentfault.com/?enc=vBcqF9c1QqzZ%2BXhKmq%2BhPg%3D%3D.nf2wsgnIOXdJrkKo44vK3Q7LFBBV44iVJfTGI0YZsJYER%2B%2BVVv1YJEHNI0Fh5ZHW0aTZLfV0lVNSoD2OSzCVPw%3D%3D" rel="nofollow">第 1 章:为什么使用函数式编程?</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=p0TJ5rro7%2BIS6z9aZmDfLA%3D%3D.sga1tZEgkzUENXVwFJZM7Y0r3Fsj7FoSBcQFuHiHZFy%2B4brtU6uOUUJrKPA5RHAE2nZKftX1YET4%2BR5DEJp%2FFVYc4sGD%2BcTf45PaND7iPy8AKiGZi%2BbRtJt2MGCvJdIt" rel="nofollow">置信度</a></li>
<li><a href="https://link.segmentfault.com/?enc=z3GXwofAef01bVuhh1zaoQ%3D%3D.KHAkjoDOMGKQJGOxJ5t3S0l2HlYDF8emiow%2B5%2FwOA3iRdQZNJVkQMGZD4xhMlBeMg9aLUn7ZseDRcCP%2FUXaNKlcVH7ubSlCWiK5D%2BJ5MZ5%2FmBXfFqhy12RMXUAEJsOL1S3do93t3Rw6ppNX%2BzS7fFg%3D%3D" rel="nofollow">交流渠道</a></li>
<li><a href="https://link.segmentfault.com/?enc=q5DQi7BU3w%2BGlEi39fwF5A%3D%3D.%2FGRWllgzE%2FOdaZmP8GFKoyPPl85LqzZL2DFVIRYdMAO0axXBH4fS5uCYQRUxcFSeDlLiokQph0MD6sVn%2Fl8Rku%2FBn6ZpfbJdK%2FoPex%2FyKBq%2BVYEncrRqQUMDpldbZGPc%2FQLtcH0Lkv%2B8XUhchF%2B5EA%3D%3D" rel="nofollow">可读性曲线</a></li>
<li><a href="https://link.segmentfault.com/?enc=3CWfAKmFnAztaAK%2FzT6G9g%3D%3D.taTY0TDDPJ9VKXnQqbLL8G3AnqYDKQ6psL3Dq8DVSuoiE7dWDVGl7s%2BBN9PcFtZsU6S4WW32Chka5Mf0Zg77qcZEok1cJ%2BqnYESqT7vRlO%2FrpaiDZnU1aCPn1xZHnvhA" rel="nofollow">接受</a></li>
<li><a href="https://link.segmentfault.com/?enc=yU0K2fy01EENhstTALtwuA%3D%3D.pQwW0BgF29Y%2BM9bNU5FUiQr3Hu9WW6Fl2KRbp7WHzQgxnxK9%2BRCcFoiCfrnXxR9cdN4dHH%2BV3u9q%2BNrgvy3CVrP0i%2BI54%2BV65ubm571whsRwB8N5Ktu3L0F9qeHmaeHMGp9jBnZC2yAETsVcBmOpUA%3D%3D" rel="nofollow">你不需要它</a></li>
<li><p><a href="https://link.segmentfault.com/?enc=ghWCQ9VsmMXBHUo0IPgBYg%3D%3D.nsY%2F7qNxHTtx34qG683DkHtO0CbdlqkI%2BJ3bULZShyFGoJrnOt1H%2BrRBw40rNh2%2Feu2Zk2ajyUdoUdAlOup7WSPp8Vz2X%2FjhpRa37hgCWLzHNDCdZbYjxCxJv7oA19Ww" rel="nofollow">资源</a></p></li>
<li><ul>
<li><a href="https://link.segmentfault.com/?enc=gkRvZy2w7Xrcsa8uTtdcqg%3D%3D.Evz4pecaWNQmv4UVzhaX4M5IE37CbENxIAExk2WL21JJhdySuVoGlRAEXWs2wAk84%2Fx%2FC4PlGPcUBv%2FRFri5D5JFfBc9W4%2FQl0jK2Fbx2dDvON6K0l430noKFhm8J9ZWPHAdlid98xQnj2HMJ2JP9g%3D%3D" rel="nofollow">书籍推荐</a></li>
<li><a href="https://link.segmentfault.com/?enc=Gmqx80pUBmAn%2FBDJi1yRdw%3D%3D.v%2FkMDUaPx1kXMFr%2BdBW4jaz%2FY%2BFmLJ5gTqbleqH1d9dcsbsixdWacmnSIgUmYOufoXL5Y3k%2F%2FqC7EqCwWaMuqIoMJ1yMHRy8jufdL%2BfYwiQjPEivXHAzFFM4U4OnTNUTLPmrXI0Qky2%2F1Bl%2FGR23eQ%3D%3D" rel="nofollow">博客和站点</a></li>
<li><a href="https://link.segmentfault.com/?enc=cCjymtF2MK3dSurca8L25g%3D%3D.3g7JZPe1zgP0OHYOfnoHty9rtQvToFnPhWYQx%2FxZg7xpYq3RKg6SoZ6fwlqZeSQznS1fCLwia4uPnbJVXfweXxA11ua%2F91gvm9QcBBm50EnX9%2BlOzZbBssoqGFHsS3vs" rel="nofollow">一些库</a></li>
</ul></li>
<ul><li><a href="https://link.segmentfault.com/?enc=qCFdQU%2BXSy%2BfqrhMV4jRsw%3D%3D.OmwHHD1xXpnIv46AAz5sKy%2BPR2PRRnxP38EKheuGmyVzUergIIzgqAQPNJ5hhH9jww6Fez0yBG3wHWJg82OfESW48qAM4nnmlryPh0pu3zMbIPN2lcj9S8ow2HVN95xY" rel="nofollow">总结</a></li></ul>
</ul>
</li>
</ul>
<li>
<p><a href="https://link.segmentfault.com/?enc=q5U1T1lk3jLV6g%2FEZyhddQ%3D%3D.6W%2B8UpONOqvqCHTtxZmYQXYE%2BB1GdEwIN2cQOWIIS9siPHiDlWGChwBYPDAU9SCfIg3qcH8%2FxrU%2BBbYNJV8s6Q%3D%3D" rel="nofollow">第 2 章:函数基础</a></p>
<ul>
<li>
<p><a href="https://link.segmentfault.com/?enc=Ihtr4hbZfxdU1EV72tSHHw%3D%3D.dMWzEtRYYKPXtILJrcJ%2BLjtmSv1TirHUqlnycvwNTDjC558fN1Z0bGjUffxDBsGCW5KkXSLtgu3OZ2riPO4sNQRM7GVahDPnlvK5CzNlMWy2rdxMaatwQfdKD13hl%2BdeRygHIdOz1%2FSAFlXqTnyNdw%3D%3D" rel="nofollow">什么是函数?</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=lLAB5jSYnppVJDy7ScoOXg%3D%3D.g8VXMy14BxBJ6gdicr9kd8fImRjoOU%2BZjLLyHtSLT0mDItAObeOT9%2F5NjyBnU56qaZLyHk6rMzoBRW6EtTBBStUViN8Z0%2FvtD5ezkn97LG%2B%2BgjVTuj9E%2BOZs9sPjw3UvLLv1Yfop9Y37jdLGpMD26PtHE%2B7Xmr%2BFxEnDAkRuGQM%3D" rel="nofollow">简要的数学回顾</a></li>
<li><a href="https://link.segmentfault.com/?enc=2C5h1n4oCinmS1IGO1H%2BHQ%3D%3D.kjZttWq8bq7cFvJ9tl4BnsTbzdDjgsRJfk5mKhntL5L0r2ixSKPVxIlgH9fd%2BtBHFTL6XqARLfejk0HC2TnZckBjIZ6RTuwHuS9JSPHUrD78jDHK8l2a3a5ESVZ%2Fq2aC%2F7NURl1etTBA%2FYyX%2BQKZCA%3D%3D" rel="nofollow">函数 vs 程序</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=Vyq4JhAbpnCKm0eB1x6%2Biw%3D%3D.2J9hqmicFjcVkwXwPTa%2Bu6xFOUtLvqNsVtjqDi8nRwqqIogr1hnu%2BFYVE3L1wA9HVgNF2YWrrx8cO8ZJKPPwPDEOkIaDc1y5k68N1vLT4E23mDpFoir5muf6AiYntwQYNE4lpnfLahkraeTmoHe%2Fkg%3D%3D" rel="nofollow">函数输入</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=JW0Zd2fvICLu1TWkVTWqKQ%3D%3D.b3mnzXigAlVAgTA2gTuaBEmWUCsMJOb7TGjeWTd%2BWtJ0tGfiM4HkziUwuTzc0XyDFokbep6HKTtmx%2F12efIcJ6f4o%2FZT%2FxczoUh0OAoV1cz0LgEclsKh1lyya%2FMDhFyyTVHYr6JxoUTrztWn6YaydA%3D%3D" rel="nofollow">输入计数</a></li>
<li><a href="https://link.segmentfault.com/?enc=%2FEZFFbdelQPxCBoD7DjtrA%3D%3D.1tPPMV8MT0DOJ5HbuI1z4Z6bTQ%2Fdjv5UcZiUyTnRT06qiEj3aOXnnbIM1AV%2BSUj81cCjUJDRsB3aSWFkDakVcE8kThDUelB8MKVClR8tTAvs89Wcr5mor6B9edJ%2FsVgsWT%2B9sJNAQ7LSmBJA3po6iTVvIv%2FbGCvZtJDfLr5mB7TO2C5GRrMdzmoCcEdH%2F3HN39ZGDib65SqKW%2F71yg242w%3D%3D" rel="nofollow">随着输入而变化的函数</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=gQBro74YKL63ZKgfF2kbuQ%3D%3D.FmkGu%2BtQ4mFqxxwmD07VLgsK6I7x9eQiuJfBhKPhOQaNOSNQ6DVy1TaQOfj5J4JjMCHigso7hrcg2T7l1RrnN%2BWLiRBOw5uOh2JHv2huxZTR3hFYNc94a9the75ysnIhV5gpEjvM2OibAgaMA8jDrA%3D%3D" rel="nofollow">函数输出</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=LDIHBVv8pbqw6KwErs%2FtAQ%3D%3D.P4oFDWijZogYECMxhoyr84vVnwglr7IQImtrw7g%2Fd1UJtYJqUnSNPBkWZ4LhKxaEHvV1Sz9U673bTrLNU%2FcTvOgFhxakf8y88HV6rhmMPxLHdbWUmbDZp6i9SU2AK56e" rel="nofollow">提前 return</a></li>
<li><a href="https://link.segmentfault.com/?enc=RNuhuSWWPV8c5kfGtpQk9Q%3D%3D.HDulOtGTrNSakALPN8dNpD5VeK9grrjo%2BgNpekFBvWIKq94Yn3ndR%2BIhfne8qykuGmXQC0mH8HBY4BlWgNbTBTtnloIA%2BHxUftn3nw%2BTPLNhR%2BYQtdGTSj%2FNt66TyUpP1ueuJEukb1iv7OoVeklwsg%3D%3D" rel="nofollow">未 return 的输出</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=KkyLfkJI4cUigLaCGI8IfA%3D%3D.A%2BuVvSHW0kyShKOU3A8Scjo60u2NpsqeLUu%2B2oQPHF88%2FlvmORsChlyW%2BcQVbCsjx1VCwD%2FwNRLXZ%2FtfnZzUBhWrbHS3jOGW%2FHJmHj6v%2FiOjP%2FTmLAfuZCx5pUx%2BLXExrihkOmfZw%2BUxoTPuI2xPNA%3D%3D" rel="nofollow">函数功能</a></p>
<ul><li><a href="https://link.segmentfault.com/?enc=fFVY%2BXx920rRa0jpRKER8w%3D%3D.ysoJ1V%2FaxIaFcYSVLskUevMVeuOSn9Olp2k2yIp5%2BlgS372w1o58eE0z2MfHUt6inQ7pq02oRdiy22kc9LAC6skX3RFtyBCiq50uCeT5%2F6Xh49mBg%2B4ZCgdv1C13Gp8%2FICrEfixr4G5LMJPuCSHc5Q%3D%3D" rel="nofollow">保持作用域</a></li></ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=XQlEKspC8xKw69SBZ%2FEaNA%3D%3D.7kcluctOrjdb%2FWW07Co8zMHBanuphvz%2BLD3eluFx1uMjCZ%2BTb833A5fMHEijPC4JBDTFLP%2Fxlwmj8XajFecPb5kXIkiKPYA3DibC5rMaqdU7NB3lPTyk7e3YQ1zN%2FImu" rel="nofollow">句法</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=jfcwPw3CGaQZIiDWupo1Sw%3D%3D.hXugxNyn%2FJB95lW%2Fsfs2VfPBzl1SQNuBGk7MQAMozPafIZuHUpOgE%2BkUdE1C4QQNqKP%2Fp7z9wKi%2Fcyk8KNvfDNAKFaY4B6ef4GMFIksLxXza1mHEZoDsPFtDLDm9lLXTidFxR7jJtEF9fcUCX6yjPw%3D%3D" rel="nofollow">什么是名称?</a></li>
<li><a href="https://link.segmentfault.com/?enc=w6fo%2F9de3Ap%2BZ4plZ0ffQw%3D%3D.dM%2Bz5FVX6lGFnWBMJsXUosKgPec4Tq0rOol%2FoI%2BO5s3V4aT7vUfd8TtzshsmWWtNuDMhK5%2BrwFkGlNEQWNJPOAqK23s%2FWbCjzzMLJJ25DJCpg6rHbZnL%2FrAr1Z1fnih2NdEnNmRuqkD698NXtAoyDPyq2bb0iOxbEw3UFtYa%2BPk%3D" rel="nofollow">没有 function 的函数</a></li>
</ul>
</li>
<li><a href="https://link.segmentfault.com/?enc=ESEgV5G1L8JA%2FswWkACx0A%3D%3D.u68gfNLxudExxxDKW1TrOJMobIVr46zAA7cypYSkvlbRpiqGuDxJkoqa%2BqPN8bIo0DV9exU9N4wZEEDCIYsRWX17qiSOL%2FAWixIW11BUASz0v2ZcawqcgNVTnjQh6PsGawaXhU97BK9bl3B6tgF%2B8A%3D%3D" rel="nofollow">来说说 This ?</a></li>
<li><a href="https://link.segmentfault.com/?enc=6qgq6XuIRyDmjmqPWHJqVg%3D%3D.kCIL%2FI1OaXfVm3a56rBx0gb7%2B%2B5ZX4gTCbcIdblNUifolt0BXRLn8umq%2FQtLIQywMIgJSf8nsDoc%2FT8P%2FXUsjuzbO1pK0xOQYcRql8aXPz7c0TIxVy2PKZdx7IjblNkr" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=t4alAgSu7S1ApPVCxCH6rA%3D%3D.jLKGM%2F8sBzjrvOa3AcvgU44Zjbe3RZkQh7KSbLGDqpcHZUzlRNr8%2BGyz4%2BSXe0L336T2M4XDf%2FiAX1jmrjlj2g%3D%3D" rel="nofollow">第 3 章:管理函数的输入(Inputs)</a></p>
<ul>
<li>
<p><a href="https://link.segmentfault.com/?enc=x174yPZbbeAahTYdzuld0Q%3D%3D.GU087XCn9zJ5gW%2ByL7s5KKSUgNa9rcCo2ae3qiCa7PHthWIqUPLsAIFvxn7yVGvN97V3rAtOEgPwsbgNGfTI2Vs5HplM3ycAWMWwUzHZ9bI89LWxlRb%2FA6V%2BaaaWUh3LjdQUDPNGPsPPfsRyuEh9MFIMcXzdjuQQhY06TWQmb79syPO9kBU5gZnjcULHbdOVJlfL9aM3JoUmNA%2B%2B0tgbkw%3D%3D" rel="nofollow">立即传参和稍后传参</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=vJqlEeUWGDUQwGdklsD6%2Fw%3D%3D.ldQrH53Y5ml%2BBLyk7riUrhnLX1XHVtikGqfRl4QeXtHbfakLpztZv6SHKJKcQzKbt600IPWUX5g4ZBGMIYd%2FqeJBZlpqMsqfL1YkGIoqIAA%3D" rel="nofollow">bind(..)</a></li>
<li><a href="https://link.segmentfault.com/?enc=DfQQHt1tFRbs0gy7OcR0hg%3D%3D.%2FKvLtQCe3X2qG%2BB%2FBlCLm8fnXwYowOepzkT6X1ZzSFrzmqzvTy%2F1DWhEGg9Yg7ypmddZmZaqzB0hizbPI6La9RrcazQ%2B1QGyEfw8J1CrQ6ATrwnjYnW8cuZ%2F4oKuUg17d0sc%2BKL3VesTwNxRZ0e2hG2br0b%2B3yks5RBtvDhr%2F1g%3D" rel="nofollow">将实参顺序颠倒</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=0CTHtN1gIBnkCMM%2FBjbc0A%3D%3D.43SR2nH1ZiTJJI2UJoudwnP%2BbpOul2WJm%2BbzY455TeYUv1qQVDki2cRPR6vXXhjgS9NVUWBHOy4qSgN8mo1i0MIzZ7tyUw%2Ft7v4Hvftyg18JjO0yZSZ7L2tAPcIecOLLbRZDG1T05%2BppPGVVQeZqNQ%3D%3D" rel="nofollow">一次传一个</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=NdvTifvzC20zJZ4%2F2GkVEg%3D%3D.GaMnikZ73gYgW%2Fd7Rd64YWvkzoRh%2FPi0cKZJilpTBXjEdfzBvL4GnTgDRu2zPMEzsUtdcLg7uCxU7556Q6D8B1OzTtjiAbXZGVgoN%2FKmfVHu7OzlwUGUIXCjWtToIdRacsoNtR89EEF3mysrD3d3tR%2B3J7SaaSTlG40Q6V7EdKRN2Vj5CCEoCkxsUefgnbQN0Q%2FEb%2BFghxhDyz%2F5KsBxcA8jVRNFRaN7CySVqy9zP38%3D" rel="nofollow">柯里化和偏应用有什么用?</a></li>
<li><a href="https://link.segmentfault.com/?enc=m1CGUDotSc6RbA2VzCJF%2Fw%3D%3D.p9wG4BtMYcTgDIbKToiTcfwXslIPQLa0UXnx6o0Bg08KioEy55KTzSUvq7Y382pNB%2B8pFOg44x3a7tr%2FQ4FFwdQnMVGpYEtCxL4Hot0Cl3x3%2FpmvItjLZPCQwYOSJcx82DKZlfCOa%2BM74JLy%2BwfaZ4rQI2nD0xnJT7tMMi9%2FYdiwrF2Q2bd4a9zFUe5SUvy9ca6ND1TLLKEQYcwhg3I2zA%3D%3D" rel="nofollow">如何柯里化多个实参?</a></li>
<li><a href="https://link.segmentfault.com/?enc=nY49R70qX9PIJ7yFbXY%2Bgg%3D%3D.2jAn0M%2FnlR2TvrQa7Yq58NU5rb3Vdl0c2gUbX2cKyrj6u197GBSIub%2BroT5m8UmQ1Z8cgp%2FgJrhEdQIT4d1IU2Pf8M%2F8rRYnlBwtLfVEZqBb8kh7MEQOFb6iar%2BjwnsyRPrjenMMxum3GOFUBHvFVg%3D%3D" rel="nofollow">反柯里化</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=l03sp8R9sNZvvB3LXjzfPQ%3D%3D.6E3DBFL4oCnRaE%2B9glHJRdlaXOicAfXmUHmk1qYmlgSFv2l3HtIuE%2FUVkMmG%2F8qSsIUMmi3i7%2B%2FzuOrvkbLgmSJKaCewpXX8VKqyhOlFkVjcHiV8odpNywlj4k80l9TpBnJXeMdtAdhlnAFejbWO7DvDZa4N58nxugKeHbWNFy0%3D" rel="nofollow">只要一个实参</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=p%2Bf98cqGY5hOUsBeFO%2BIlA%3D%3D.Ffil1mVZ4AIsZXvhdcr8w9Fof2A6kkfcYZkKP5hWanWy94xkarszcDASycVqKGo5qpYqqAm9BQsd8dwk8Z%2B8QmzaDDi4xEZN7Fl%2FUKVpfGAwB6r5kh2vyABVdUaXctgGw7Ztoa3v5fYCtR9Mw9Mo6gxhtNfc97%2FXStNfXLHCOYQ%3D" rel="nofollow">传一个返回一个</a></li>
<li><a href="https://link.segmentfault.com/?enc=UuptroSKr25CQX%2Bl3MeiJg%3D%3D.NDMnTCMmLiPm%2BdaX34lvRJURgN0Fq%2FRFcCVtYZUM5aJjPrGOPBcOftX00kP190r2G2K2ho%2B%2FPFWimcrCFrJmjUZgNAR2%2BKhCT9inbtHZvmBMjpQe72BHNJEW34YWYRqgS8CrnmB54pdMGczfnO84LA%3D%3D" rel="nofollow">恒定参数</a></li>
</ul>
</li>
<li><a href="https://link.segmentfault.com/?enc=ajbT6nPYC%2FGOqLrBFZc0zA%3D%3D.xnB%2F4v4sft09KdUd3IaC8GQs6TMSGd0cTJGVBl4caDp8PtAcOEKS4GLFvM0cDNXMPWSBzCp3MoTX%2FwUZkN9Bf1t4BN2%2BfxpXWoKmAnbTZzSuPf70%2FWtR9BVXKn59GgvQ5kwK403xCKTP7FdavARPgh%2B1HNTTdKcz7azOWjo4%2F%2F0%2BlQnV%2BuHuAs3fYTlcfMedDVlqsrj6JcBRHxhOSzQ0ow%3D%3D" rel="nofollow">扩展在参数中的妙用</a></li>
<li>
<p><a href="https://link.segmentfault.com/?enc=4K8DTpDoZZKne4whsgfJQw%3D%3D.9AaR2LfCITOr3YcN4qyHwfIFDcQ8QOrIAi1REauKCquGbSKQEUNA%2FgAi5ZMTjh%2FjCu30N7e1Ssf6JTRV3j6K0MdbYLBa9Xfo3n4Mf01Ce0cbGLSLdB9XXgV35987o%2B0%2FmRLhZfKYC4kziFHa34B9p5j059cQA%2BersPnaRguWg%2FWNX%2B56RwY0GM3vKKeSDT5dkhGSCsQiuBQ7R9e3rZqM%2Bg%3D%3D" rel="nofollow">参数顺序的那些事儿</a></p>
<ul><li><a href="https://link.segmentfault.com/?enc=Rad3%2Bk9VQVXL7Xn%2FbX0K4g%3D%3D.%2B9m8kqYNmYJ1Y2iVjrTQAlVSRRrKJJFXwZJvJQob%2Facvpzi%2Bqm%2BSvBE8psfg%2B%2Blgxt%2FBl7ESS2OjVZVAvDy9c%2FpJLTIzFNwRswScUO5IXF5b8hD9slkKDgWvpzTNGLPTV5unAVHgG4V2OCFp%2BWnTJA%3D%3D" rel="nofollow">属性扩展</a></li></ul>
</li>
<li><a href="https://link.segmentfault.com/?enc=kFqg5dZFlsG3HfUAR2rrWQ%3D%3D.GCEhjE9D6%2Fa7RkqXXvjHmcuMUvWvdMl3R6FiGsYdgqYh14635VJNHabOMfqA7FnCrI88ZL4KHzr0klnpVB7Oq4Tvb88Wd%2FaNBTEF5yUdCAucG6tIXp4i8lqQd1V4hXKJTV8vVxF8YExY42G3eEYM2Q%3D%3D" rel="nofollow">无形参风格</a></li>
<li><a href="https://link.segmentfault.com/?enc=ktsnS75KHlMK04qKuwIoNA%3D%3D.Euoky2muFe%2B78EwnB3Np94cPj2TNyN0Mit1pO46Py5wQaI0pKO%2FiCozvISCiRYr%2BKLAlMu3smS1SoODDBaZ1D7eFxyJ88moUA%2FuhCxndAxLrlsaLyMJE27wzxsnsWUhI" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=ylnINTUUfIykPnpaJLhXnw%3D%3D.x9aqXQvtWpC5pRNrNyit7zQgwl8bVioC7NyNfysHF0VOWTQh5We8vcWexRsDJtgP3EFUjP7YCBAy4BX5M75o2A%3D%3D" rel="nofollow">第 4 章:组合函数</a></p>
<ul>
<li>
<p><a href="https://link.segmentfault.com/?enc=%2Bl9TamL9f5I1nIno8a%2BNbw%3D%3D.35W0Ua9EuBBqKK0UILa4yMk3mGylQViQjtQEelq4t2EQGoTocpTyJOzqnFECdLWOjx6LP8NMXiih9lVSbGNq45oV%2Fpv%2BhqObzUkfj8wTZQLDK8Fndy5fdLFJQXpV92FVmBO9j8PNiAQgmhaZqQLG%2BQ%3D%3D" rel="nofollow">输出到输入</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=BmqBGSwDUdgRduPnYVwMPg%3D%3D.WRA6U%2FkCdzjdwWmIhAO2qDZF4Slj25rOIWlMkFp%2BbgOpb2VSULaBfsxzVjOJp2tq0cCkuOStYCkb8jCuDKhnypzMwEENmMDRXFs%2Bx%2Fp%2F8VE%2F2uJheJC%2FFCGh0HtFXpMfeIlqi0UzVIzDUeWs7w7M6w%3D%3D" rel="nofollow">制造机器</a></li>
<li><a href="https://link.segmentfault.com/?enc=yBvNEKNUSURfq2feD9OE%2Bw%3D%3D.VqZsLsUJvBay3eRhp%2FpbZCM2ljz37ocCQ4YT7k0QrEu015LkprDapqCulNIVXz5Co7R9ivddZP6caIsyonTV1FGzjWXVXrZZ%2B1TMLplkzrkyy2X3607w9QO6%2BUvpNWDpeVFuW8B%2F5XK8E2E9nxI2%2FQ%3D%3D" rel="nofollow">组合的变体</a></li>
<li><a href="https://link.segmentfault.com/?enc=9TY0XcnG25JcdgS7VtqupA%3D%3D.CccxZgCaK9RSZuRdpHdsoeT1LMl09zbGfBJzFe0Q%2BuC4NMgR24XkpV%2BHKQBHte4XovtUQskziw6ttUDY9UBDdAnicGXWVZwvLYn9MXxxOeNApgPYi5rwDPQQKGBI%2FCwA7nJoBzLkgvsrVyowPNPuVw%3D%3D" rel="nofollow">通用组合</a></li>
<li><a href="https://link.segmentfault.com/?enc=zQQaCERkF%2BP8qCTitXpkHw%3D%3D.zopdko%2FILR42BxKwbjwPNiUtSJcdwevOIlxGAj8g1ZnKoJ4WD39AiWELLLKvXWwb5mAcv7l9LQuws5OotZ96u8LhkPG5Pe2lPP1d%2FT2CY0h7Ma8LE07PJ00tuC2wI4xt6%2Fs2ZBnBv0YI%2BkLV9MC2IQ%3D%3D" rel="nofollow">不同的实现</a></li>
</ul>
</li>
<li><a href="https://link.segmentfault.com/?enc=ahbRGMHTQdtdcVVB7DMmlw%3D%3D.WXJ1QotM0RimR6yyquH2Ei8E0j0ThkJ9Yf19qyh6ikDeiaGVxuAvdzUsy%2BJyUNmIdwZkEdAGE820gB2mK2Dd3aIeoM9leT1WaXB9cVpZqzIMdqObInF%2F5Y9nZPdM4wLkskbcGVm3sT9ksQR6QVC7gg%3D%3D" rel="nofollow">重排序组合</a></li>
<li>
<p><a href="https://link.segmentfault.com/?enc=VXdp9iDJjymkFLsVGLdoZQ%3D%3D.iHHuutPNSbAFHXzfde%2BUsHSpqgDrxmop4vWa19Q1DKgOwqmF5f3gMyLfNZta4NrnZvOI3A3RSDzxzpD40oUdYUsZVtkiWf9ivCcQBu6euayLH9sLoRxSbGE0Q1B0ZY9X" rel="nofollow">抽象</a></p>
<ul><li><a href="https://link.segmentfault.com/?enc=7OgrjU2Hm1ccdL9Qb6%2BF0Q%3D%3D.C9cNWpuD0wX5RTy9lbjCKLGdNbGdhJuI3S7IpUn9eR5VxzRjeei2k1Ajy9mjhfepuBv9l7GltqnV9kmhjBvLVUoW4cZFkuqH4vU%2F%2FxnREWBiNpIAWaB6M4%2FYe%2B7SQdYcQEcWRAdgEpwhRNxdNwBXFMdGbyVB2nfjKUxW2RJBFbc%3D" rel="nofollow">将组合当作抽象</a></li></ul>
</li>
<li><a href="https://link.segmentfault.com/?enc=2wkuvqPDAOigDPX7M80sAg%3D%3D.OYIXIN82f4L2MIBS%2B%2BrENyGE3sBik9ek6DEKEvhlvcK%2BkgaXyC0d0ZViV3BJ86sSBJ%2FSm6fgNshD0xDB2SDQM9aKuv6EVLEpFp9j7hYmQTr%2Bfr%2Bxun%2F3H7bk7G5RHj18OvUt00TqIZBcqc8m%2BQSUbg%3D%3D" rel="nofollow">回顾形参</a></li>
<li><a href="https://link.segmentfault.com/?enc=Z60vJQxXBiTYS9tWCNcICA%3D%3D.wnydp1cCF7eP6pxmvzAt3R3%2BqTCWaqJKquTChiMe%2FMA%2F2d4FCcWydavCVDcz9cG8yzu7jYMVoNWeRdftzYAK%2FARE%2BqZPxks9aTNX4EirtmOKUB0M1IsybBKYhUxaoacE" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=gudht4osQFhcsVL29SaY1A%3D%3D.8jWbUs5ko489dks7aA7TYE1%2F%2FrQLHUNY1eGIKcG3UAWkCBCPZhx33zzzwMihT33AO2k5nmn%2BcWgZmNqfmNslqg%3D%3D" rel="nofollow">第 5 章:减少副作用</a></p>
<ul>
<li>
<p><a href="https://link.segmentfault.com/?enc=fZgcDQqMzLKKCI%2FK7IPkXA%3D%3D.mszstZkbZ1sDhM8aJNF9dn1G%2B%2BO37mW%2Be%2FAF%2BPlraCnQ0eiUvge%2FafVf0A2t28OTeZ3jEffDfOZOvYkN0umglJjciNVPAV%2BJyhSqhc%2FtvJWlGnJuUScoxHHuyyjTYrnopuTuCNkUqFI%2B%2Fj2cRv5nxRdHkVb0YBop7PArluyvuxA%3D" rel="nofollow">什么是副作用</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=Cr20J8dWXG%2BgTWjc%2BLlYcQ%3D%3D.L9ryGRLJNLwW9x6ZciOma25CuK1mMTSDSbvomRtDBcqxucRzWFoY9D2Htm6fWJl%2BJp3MshT42fsi08ST8zZVVbRc6ntJuYfJ5sHeWjVmwVK%2F3BT9Vt7THZOpP%2BJ9O2dVZYjq8Y8Msf%2FtKReP6cPw2w%3D%3D" rel="nofollow">潜在的原因</a></li>
<li><a href="https://link.segmentfault.com/?enc=VsXAsXhrcemTos4M4pxDZw%3D%3D.qBcJ13ud1uShLHQxVhXg9M5esVkm8SzEKh5TBGW2CSDO24ZodM3YqXYUSwwuoHyqPZ3LA%2FFe%2FkzZRUqmSv8fFrjdKxWYVutDQvFMcoMbMO3xNVGRDif2Fu8jFR2C3uam" rel="nofollow">I/O 效果</a></li>
<li><a href="https://link.segmentfault.com/?enc=AdsHGE6RMqny3K%2Fqiy6b%2BQ%3D%3D.NuP4ZcgV5rgXiGlqu0wgiuuTu6ilCqvm0Y4HhTaIR55%2F0Z4LTbhMNGMtc2%2F6Zkblf7kl8VajLO1CmI6ko3qJDQfQMF2Z6xp5Y9HBdgDongT%2Fx3qvjU7C4Gb14foWiz2uA7dnF9lydZNnYf1%2FNcX4Vg%3D%3D" rel="nofollow">其他的错误</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=Atp2kO4nvNbKjiat4I0wJg%3D%3D.nwPH%2BxsCL9lvj5QCWxqma9yvNM6u%2F9YU%2FYsY7KcyFHaEh8lv2EolEMORbMDUH9tifdg8%2FNoMEjSEqNI8vsc7LXafhqIg1xZmtbUqr8DQPJoXXdXWfnnS1w463fib0A%2FkcoX%2BJChdIoAPIgUAu2GqLw%3D%3D" rel="nofollow">一次就好</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=%2FnmtPjpZPLf4WQOFWRxVQQ%3D%3D.dtVJ46Mxr5wISGRsz1Y8ODTrQmd5ANY7CTRoh0ff59r8cwojtFmexcs1cB%2Famp%2BOO5An9dxMt7Ni5Qd3H9lGQiRteb%2FpOCgHDiY%2B6Btm4GPbLHj%2BJLIDNwzuEwKGnKA6ZRMqFswPzHH2Z5Cj88Iw1oZGBaR3US78eDn%2BIwokTjA%3D" rel="nofollow">数学中的幂等</a></li>
<li><a href="https://link.segmentfault.com/?enc=PV7nPHEH60nOvEoFqIiHAw%3D%3D.IbNiz%2FGU1yro5kkiowaCK71kyFM7B31Imdo2FzribCQo55ddLvBIEnS%2BKrAdB5M0Hpp35j30g8hO2UkDBqyxnxfhzBMumHEwCwvQPtaf%2Fg4zUd8x7twaZmFAD%2Fd2aBE3cgHpVaNH7jtwlqZ7QBGCS98Pc8fcL73QI%2BxZNW%2F6Kvk%3D" rel="nofollow">编程中的幂等</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=vcelZZK5du1xjpu03%2FkgKQ%3D%3D.mBvQiGi1kh1wtVjcRigP1Vt0gUh7g7JwjVnT6N7ARMBi1xDQBLzwjV0u7CJZyDVCGM3UfNJA8Zx9gFkaSVE%2BykwhTB3qrW%2F9i4xF4qf6%2FwwkF0HYKxSFt8XO30hhMqtcisXSKC1JBANaD9r89W5dtw%3D%3D" rel="nofollow">纯粹的快乐</a></p>
<ul><li><a href="https://link.segmentfault.com/?enc=YmQjncp4D9F6%2Bro7%2BbmoUA%3D%3D.K7cAPnlkxIUz%2BcMlnS1d5J5BuOh%2FgXy88CM2GczINZyLU4ptdnItcJcrT3K8%2FWo5XmYqtf%2B7DHSARTSdtaDrZVAGML7HqcfmUaWjPMin5Rn1kq88%2FrIxt0Siq1iDEQXZGY9SDRkDoaTKj752qxxDqg%3D%3D" rel="nofollow">相对的纯粹</a></li></ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=LfO0ZrcMRtuQrKxrCscEcw%3D%3D.CPtVj372yjt9MHBv%2BQUQCiZkXoARZvirvN3b7988BzLNtWvpQsoJ4TrCeoqd0PvljU2IKiQdLHdpuEI9eYgdoOo%2BwyyuRnJbr0Kyp62xxzljbo8WTQOzXvmVh1J1%2FjCMEzpbZbbWTcbycG2a51giKA%3D%3D" rel="nofollow">有或者无</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=0t%2FuQItM1lEcswFOODe4kQ%3D%3D.Ub%2BzAm70j9AXgu94LO0BIhlNa1cQ6oWonbM0HfxgLfirTUyxUPLzZ6r21sol44JCUF%2BMZuEOmeTRvxzjUkHW%2BJWFlOna9WiCV5szPXix4CUpOupOaOcfWqDOngSJoHCM4D6s5YdPp8UaUb88mHO25j53chJ%2BUFjTxEI8EZbEWI0%3D" rel="nofollow">思考上的透明</a></li>
<li><a href="https://link.segmentfault.com/?enc=JxdBiRa4J0iTBTLNaXxU%2Bg%3D%3D.wPfjUFyPT7YbgWknZ8gcGUzhMrA0K2gsJYRtry5j1slYbqNmVfCBnecZ7PJhggvFHO1HboRzQHzkMcSIVScKQh1edtsmGp0aGSrdA%2BBA6KSUazC%2BNMPrbt%2BwX3OHVm%2B8QHnv5BU3DtIQ8Zmg6hiYvQ%3D%3D" rel="nofollow">不够透明?</a></li>
</ul>
</li>
<li><p><a href="https://link.segmentfault.com/?enc=BnsuhuUBvT4JEhPL8uu1wA%3D%3D.MzV98FF%2FFn6GHYWv29gzk4WLuFeq%2FxLpCFpiZMYL6lHgusD8who6uC5HMKNj3W5etr%2BF1fkd%2FP0C0RxEEM%2FE1XkSyhF8bdhIJBa843imVUW7K7XHYK6rXimZRLbgeBId" rel="nofollow">纯化</a></p></li>
<li><ul>
<li><a href="https://link.segmentfault.com/?enc=f5TjmtuTLyOWKyIKDJiNJA%3D%3D.RhsplFPcQx4HI8IZpfo3GnCURem12j0SaHml38N06gwHInNM4WcfPtvWNoA2MJD1hfvzI16GEkSKUciCthtIG3CiANx8p2r%2Be6CjWJFlzgQyyJFL6eGMfs%2F78YpUD3f5odk2ig%2Fa6swlYA2I5wz66Q%3D%3D" rel="nofollow">封闭的影响</a></li>
<li><a href="https://link.segmentfault.com/?enc=q5T6MvdNb3nNpaZefwVDeg%3D%3D.UXkElQU9JHsbTUxS9Tupd7%2BqcpuhkpnXLAij%2BbqwWDQvhQB9ZRNXLaPUFOFvvVOeHLewjQsiCcxfF6pN%2BsCrLLZOfQZMQIk%2BLUYXlfY32ZIYjukaDE9Vgkq8d8CNKfF7kuSBnamTwg47mlYtfBM%2FEQ%3D%3D" rel="nofollow">覆盖效果</a></li>
<li><a href="https://link.segmentfault.com/?enc=dHlhyT%2FuPHSbY8fNwiOlUg%3D%3D.acpSsD9A4M73PB4pJBPn1LQtR6xEqOXP3iGzR0ZePHyyqqtDD7KTUxKD0wygLeh7vcJU3RPhtZJ9I1zBsToiRFvfWWN9yhZPQSg5TpSJk55eYcpG8%2FarXkxEHELVb9Mz%2FHmxsZAMWAqXeXH5yxd0lg%3D%3D" rel="nofollow">回避影响</a></li>
</ul></li>
<ul><li><a href="https://link.segmentfault.com/?enc=BqO%2FPRv7q5FY3WBnoMGrbw%3D%3D.FqMGJm7drb8XD1pIBAyMBnEqrzER%2FKYH9xmWar2ZzJDKnVoarZxu7PIYUGEXkUt1JMClgX4AZdt4dFa8SWoWrYIJHPic8X8IuZFKLhGbn4VZAM2g2QjSSlw0OcbNf9se" rel="nofollow">总结</a></li></ul>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=kERc%2FU5NTcIvSzK3kY932g%3D%3D.h6HfHaddplfx6fKGtaskuV95UYm1DpGFzC2To%2B%2BR%2FaIjtbH7Wb2AV0zDfYnFplHZjGH1pkmtqn4r34UXpTbA0g%3D%3D" rel="nofollow">第 6 章:值的不可变性</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=znWephQ5Y5hTBFvVpKIPHw%3D%3D.k2SGOnm1juR6btoIrVNpCHtwZ8EBSDKzdC%2Ff6YgOA3otxpdQ5wY%2B4V0o4ruciP%2FcKYE%2BEffMRN%2FUNPvAXTKgRJuYekHvQblXT3C6DdT1AuvRM%2FQxOZJ4x%2FltNntg%2B%2BPt%2F7w5esPI1XhqK8C49KswuNnoTPmKP5EJ%2BhH0XdQUHMTJaVFe3GgeDUC6sDBeH%2Ft%2B" rel="nofollow">原始值的不可变性</a></li>
<li>
<p><a href="https://link.segmentfault.com/?enc=lC8bAd1QakXELKHESnNa6w%3D%3D.uAoV53yz3OBL%2FPI0P4KwzR5jYrAqYJve8E9dHcG6Nx657MphU7j%2FK7OSMGMIR%2B8Z%2FxPV%2BryegtUT%2FerkhAuypDMJjpKvQ25kynrdwkmYYaU78CeTCO8lSe9R59wKTZKimVWPg8RBQp0OMZ3GGTufOg%3D%3D" rel="nofollow">从值到值</a></p>
<ul><li><a href="https://link.segmentfault.com/?enc=fTXmA2oky3q3Pehu7xsSMg%3D%3D.I50MHft6BWs4ln1ez7xqKxzjDSiN9KFaqP%2FFYL%2FVl2CAomjBKFGFiGtc01mgsLS3cNoj8Xl1AF41v7ngMDe%2FGlTEdP33ZYCbYEkG2TajRXWPWW28ks28b3gnyhEjX1u%2BnAg01Nh9T6WRH5tUGbQjbYa49hnQXNex0czc3Yn7bLw%3D" rel="nofollow">消除本地影响</a></li></ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=M6Oq4JLPdowKNStOBQSFOQ%3D%3D.vu7ark82oFoqLSGUwjdfrx1CXyQAthg4vms0QkDY0c4rIW7YHqClEFsPga6cZJ%2FjJHHw1MabsH8m2HHOnqU7zst6okDHIRhIpkpAW9ijCEe%2FcX3YUd2WOkpi38LSTMVY0f6GhnNoBBe%2F%2B5TGipS0tw%3D%3D" rel="nofollow">重新赋值</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=rzyH3HxzjG9n9asgYEbWsw%3D%3D.65oz4Zjk2lZqrgqCCLDA9ik5tXyY4E1I0NO724%2BxdVdNLZOqNjOrqgXhl11YE89JWbCbLv3yMGc%2BU%2BudzItD%2FdZNOD%2FVo7f6ffPE4weYLE8QQKW7s8ElzjlWRj7JGT4J" rel="nofollow">意图</a></li>
<li><a href="https://link.segmentfault.com/?enc=sk7JKIfi6x%2BByxlvEN9JmA%3D%3D.d4D8dS9Az2cTHx180IAcZp%2FHmO4Oj8BR3k2zasgfPUFSeb4PU1jKDk4JiIBG4%2BWrAjCsx%2BQWPDNzSpwgR0%2B3bOP3lbj0g5B8639tisqHuFuLPvq9digDQr3GXVO4gJOz" rel="nofollow">冻结</a></li>
</ul>
</li>
<li><a href="https://link.segmentfault.com/?enc=VS%2BkBPFoQiSwYZ%2F8THhKVQ%3D%3D.QT%2FuNbdqnmBbwc28sSVi%2F7crTtF54z9gaHCrS9Bxys52lmpwav1ZejsmBGUCP%2B7TowFdC6qTGQ0%2FWiCtRLzEbY%2FEakgijvosRcNA18JIBb%2B04lXonAgQKrJxPnBaAAHq" rel="nofollow">性能</a></li>
<li><a href="https://link.segmentfault.com/?enc=EFV84zMBM8Q7BAh9az%2FYYg%3D%3D.qDUIRi4C1Cy%2Bbl%2FPQIbhJ3FPyije%2BLRtkKLkRMFEdPKdHLhIhDkYypC2b8U6T0VKFu%2F9971FguNpV7mKIL73O8y3AXbyTalZS9LEvXA8J73hGs2K%2B9OVPwBaYDgQEV5BO49KL6Gjv6QJECi%2FGOO%2F03sJoQCduEydkhYxbDSwcIF%2F5lkFE0ul3cQNI1Va3jR4fbi8O%2Fe7KcXbP2Tp60RNCuFb%2Bbzyga%2FUAhwAIwE4ocM%3D" rel="nofollow">以不可变的眼光看待数据</a></li>
<li><a href="https://link.segmentfault.com/?enc=f%2BTQaiY9GRomEvDfNo4IVw%3D%3D.lEgzJYG0J3tFzYsU8xzUlSL6rmPlZGlXBzGdvgqoEmOR%2Fk63PL7g9s4uOQDeWyvHur%2Bj0SqnKMOij4OK085UM%2ByJYFeIBkFaXQxICz5HkHUeyg9F0XzNj3iCuSv6wWZB" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=np5eDiFjqni5FU0SuONFjg%3D%3D.bEOyh6ZC%2BkaPdiYpvZsMW87ViK6gUoxg45XPom%2FKX798pMtcsGFJcvFaWDu6d5wC3PIQlWhW5%2FxCBMjUIuNs4w%3D%3D" rel="nofollow">第 7 章: 闭包 vs 对象</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=bxyDRc8Fs3%2FTzWqnhFHAxg%3D%3D.VQcgcyl4Zxh%2BBMXdnV9ru%2FhKey7d6QpGNC0fJaQJ6%2B2QqL%2F5xw6igq4C9VMVMiHuU1n1CDA3zUAEHRYQnvI0ZfQyAddOeJjW%2BaW2dN160m3CzA1nX4bxH%2FgI4dKRI%2FgMn89%2FvATH%2FkgDfGd0MrXqzw%3D%3D" rel="nofollow">达成共识</a></li>
<li>
<p><a href="https://link.segmentfault.com/?enc=vokTe78THuk%2FOtZRRB%2BN3Q%3D%3D.m0XL1MC6ns1BaXYHuPzKe7QqOyoGTjF0ac1Rm0xIS2lsUx6zddRoW4xVxvMSFYBkh%2BJDr8iSt7V7DBhPsaeJQg0D%2FJvHc1k6F0CiMddYAF8ltYl%2FGzkmxaXP2jf2hy5Y" rel="nofollow">相像</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=ePYhr6VSMnTOndllwgYb7w%3D%3D.DKyrn%2FvJdvAdxieRMTraoG1fC0ONcfZe4K4W0y1hISmlAztfwReDU29XZ5FQjecYcxIbMRbiJ1T5Cx6SYeRNtDYo3vnSRvqTbV6%2B%2FjtcOmy5IjbyROBGgdLqrY257Qnt" rel="nofollow">状态</a></li>
<li><a href="https://link.segmentfault.com/?enc=DcDqAJuWLGbxzyF9bMpb9g%3D%3D.Z47P%2BvcZslb654%2F3PuqZnh0x32nboBPUoen2ksmT9Ppt3wofiofnPf2haIR0FwjH5LIjPuHmDYNZLHPAaU1OC7NNlY7SGfTjWPlwVYRgo%2BD5P0telc9JAB8P%2B21xaFm2CNQxycpQRd%2FVA%2FMqmR%2FtY%2FzH7dumCzYKYtUtm3yE5pI%3D" rel="nofollow">行为,也是一样!</a></li>
<li><a href="https://link.segmentfault.com/?enc=YAUCJJsaInkEKsTY2srSIw%3D%3D.0eX3ZgU%2FIIjRkxXT3qTLSgYAjZQHYnvPYqKfNevsVe74iuaq%2BXlMFaJQAMzqwXFgGJ8p4D6on0vFuSAHBr7DqgCdYfsuCB5HJxSXwzba%2FG24LJtSivUuLsHX6KbGbzsf" rel="nofollow">(不)可变</a></li>
<li><a href="https://link.segmentfault.com/?enc=j0yS61p%2BO3onEhfq9TEWfA%3D%3D.SxEWdBMf27Fatse1OacySjiN%2FgsuoG%2Fsy%2Fg7GojvTi6%2FicRkPhGIjacYeWw6HgofVXcA5SS0A9vsuJzOvIjy8Z43s8QitSnH%2FNKr712e1N4R57pZ6J1LAAEmpui0bOom" rel="nofollow">同构</a></li>
</ul>
</li>
<li><p><a>同根异枝</a></p></li>
<li><ul>
<li><a href="https://link.segmentfault.com/?enc=X3F73EdtsMFA37xvJnfZZA%3D%3D.vHRiAJPulA5B4ofN9c6Rs1Vb45IasgDiYDQbEefJj86H1FUot5oAdeCMGB9ogSboJlNWjm0K8xltqG%2B4Lvnq3LCnXUOGhRCzgs2tU4i%2BjwSsipYiRBfNrjyyowgrXdONulWMMQlg7TpcRiajCJu8CA%3D%3D" rel="nofollow">结构可变性</a></li>
<li><a href="https://link.segmentfault.com/?enc=xM9dHDajhEesz8opCzldsQ%3D%3D.G0k%2FdLGtY0ECSeFzmV7P7EbtvfO2FuJaZDxjyDBHplp8IZEiT1YQOJAQCGRDKMlV%2BFPKfRxOPxb8txTYiPj%2FZI%2F05wivFcoy66CuI71CD4rdFa23UqeRSt91MrFAeZ83" rel="nofollow">私有</a></li>
<li><a href="https://link.segmentfault.com/?enc=AuUffhwREA3k5LeDevbQcA%3D%3D.L25DL9yo%2BfFy3PTRUGLO6n1FRgUtwJvwqaodMFPmAoMkEYuFYQN%2BeZMwcMFIH8i1sRv5%2FLkuVBbIoqKP8ebhMnwJffDEiDFipPt5F%2FMh1iTn8xq%2BKk2JErff5HHv819C%2FVplrzNlMJfTXekYy8LGWw%3D%3D" rel="nofollow">状态拷贝</a></li>
<li><a href="https://link.segmentfault.com/?enc=6oYBZRNCBL5001IctW9cHg%3D%3D.PzEELL2%2F%2Fwqfm1eOHHIuBPJpxWnZEc3AeX39YjtNDbn00QKkG1d3wGtWKWX1cVM5byEeNQnkX0Ui7HbnbtzXstTJa5IMldkrsige5hUV27rvDXQA8WQ9Itr9qNkD6wXu" rel="nofollow">性能</a></li>
</ul></li>
<ul><li><a href="https://link.segmentfault.com/?enc=RxZwUsHALSD3oEYaYPVZWw%3D%3D.sk0NQGjev6ysauf3cEs75Vi4NafHd5Icka4mcznS2HhVs77Ie5nkzehL2xkqI5677JhExQXI53p4%2B7X%2Bi50gE8iYuFAYBzZfwPlf3wfE9DYHCnQagkjwjiCdS%2F3iCv0I" rel="nofollow">总结</a></li></ul>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=ki9KlIM8NclxLCUh8Ls6PQ%3D%3D.1CtoI7s2LqOrXnOaEkCwt42pwx13%2Fimbf3kcx5wBErOS9%2FpEzK40OfPKN2somUMleDBZDK%2Bxy9omxqFvabnHhA%3D%3D" rel="nofollow">第 8 章:列表操作</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=s%2B92q40xUQkPBKUp9imPpQ%3D%3D.hvjiOHmA6Sb2IIJBeq9VhHQCMG751s7OC3e9WKamxhB7INf5PVSj7kTg8jpF7mWTOGxlEG%2FsPih%2FmMG%2BiNWKB%2Fxnpk1DwlJwtrYxYniHIMQIR6AA1rmSm%2Bx94xQYQFKVrh6MqwUgb39GByRUlJPhGTk37ZvPOS0AH0xErQHihC29Ukw%2BVOZOrlovyh1CuTDCsgdv1ozCMCVYFHh%2Fo1WfHQ%3D%3D" rel="nofollow">非函数式编程列表处理</a></li>
<li>
<p><a href="https://link.segmentfault.com/?enc=pNWMUqxwhHJdK6BTe52kCw%3D%3D.ZaBeQK5Pb3fqmaiBEcA8zKvydzjMo8SaEcyzbsR5sKGqBM28KjgkW4TGRzG522T0ulkgGExVGzhsGTnn7YfPu%2FRinOH4ays8xszFVh7s5GUSh1rSq%2FPxkIAnPoirVqbn" rel="nofollow">映射</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=eGdGfBFojDj7a8%2FPeTK%2F1A%3D%3D.C0g6Bix1a6z4Z1WP%2FYfY04r%2BVv3E4cAIMezucO1JorYyJqfdnt3UzW5mGsYXslsX5Cd1g3Dzwrta5SYolSEcSpMmCYnaCRMk3G4QqLi1a9yxFu3KU%2FCJjrD2fb9ylJVcN2QFqB1gfHS46ke32%2BjaSw%3D%3D" rel="nofollow">映射 vs 遍历</a></li>
<li><a href="https://link.segmentfault.com/?enc=jxqskuCaXPkDZUT5PaoIxg%3D%3D.9cr9chqh2shiHh2199YgoP9ggtwnMJzLpctvEc82DE%2F21EUSv5z6FoIzrPjXaB6xIxkheC1pWxPSMDCxdw38jVr6t9JR%2BEaD4FVH2YjwW1OTTV%2F63raENAh0zmlFYh9zblIBDr0mDMF3Iik9LUMs3A%3D%3D" rel="nofollow">一个词:函子</a></li>
</ul>
</li>
<li><a href="https://link.segmentfault.com/?enc=IU%2FEmfAaMuSeOcH1gdt0fg%3D%3D.1Epv9bVG%2FttXNuw90odp5%2BfHxkjfEa%2Fp%2F9HL5s6Nx7rkGEzpXnrA%2BxUftFOSpg%2FS61nZ%2FBYR6e68EtuOEVUV7IhZmLxXIFJL7WAWIsGmYdpqwmeOAfTMT3ioMTMSrceQ" rel="nofollow">过滤器</a></li>
<li>
<p><a href="https://link.segmentfault.com/?enc=P0w3R2XR0Labk5fYmIFLtA%3D%3D.58z6oeCs6oRfls7%2FhZvj9cAqMD3bTuP40ePAh9y4mv5Cu8y1tESn%2BiVg3%2B9oxRWKyolxZfeApBx8pIUUdwHuKNirc2TUsfmySVoUMbnHKK8%3D" rel="nofollow">Reduce</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=0Ml8W6BD%2BdCHUl94ovKtQA%3D%3D.lwRqCM%2B8jYwE77%2FoiVhdcMewrcrBxPsUOjOIYpoibBU7ppkOZ0iqgMoo56VjuWi7nHzAL17vR84m%2FmHpaQlDt8j2%2FoNAzGV%2Bg1uhxYroe%2BjIqzjSfH1db7WrYSGm3KOc" rel="nofollow">Map 也是 Reduce</a></li>
<li><a href="https://link.segmentfault.com/?enc=qAQySlLvqWJjKSSivWnvIA%3D%3D.2r%2FDDOLXcY3uGSf2y0C509Vyb6nDjOlA%2BC%2FrArqRX7JxogdAHADJrUl3vJ4N8bw70%2Bi8QnfiWimTOMQETdCx6e%2FranjQG4fjXWjbnjeK4M0xS6Lm7tYOeSaeUy%2FDedTaROIcYwSlvyyGe0KZpX0%2Btg%3D%3D" rel="nofollow">Filter 也是 Reduce</a></li>
</ul>
</li>
<li><a href="https://link.segmentfault.com/?enc=nFmW6T%2BpEhieupfl002HBw%3D%3D.5iSXxotYF%2BfnW8Zir3iEl7Sdz0It3IMzSw4vhjDOb7eYlB72lmAaq3IIqzh3wesMIDb8TSESH6FkLxj7n3UjdyfY8i2%2BwDpokPcUN3WXB1rnd2uRfHpU72eATn8GDyJF%2B6LbRUcqNdJTIL75j0x9MMxxQWNsG2d9i%2FBK%2B61wkE4%3D" rel="nofollow">高级列表操作</a></li>
<li><a href="https://link.segmentfault.com/?enc=IVBQQAojwls5pVTLZs7CEg%3D%3D.pC6z7A3gvxjSd%2BKHLW8JCZZ8nceqRTizpI5vzeN6xUl3AxVKaNj3hh4zRKTFs8qw1SG8c33zxYs0Sclt2p4SB0EzyB%2BtCEXPKUq3N8R5Y4QWzuLwH1m3F6IpvqLBBafeM3GoaB1ZkEuwwbA8gDOiJA%3D%3D" rel="nofollow">方法 vs 独立</a></li>
<li><a href="https://link.segmentfault.com/?enc=F3nB7Aqq%2FMQZOOWIXcSBmw%3D%3D.V2wrtd1O7oAAGnc962HOXl9%2BCDfapj9viAt9wX7HSZCH5537yiVH%2B2f0d%2FAkKQgthtDy3yF4oq0COarNWfmWJqYw%2Be2JSlhWHgTpu%2Bhka%2BCbZ6kqP7KZVIhS2Sg%2BEO0J2%2BiDX2B%2FuqYkS7B9bl3TOw%3D%3D" rel="nofollow">查寻列表</a></li>
<li><a href="https://link.segmentfault.com/?enc=uS4oPp1FAW0uNyfNl7JTLQ%3D%3D.Ea%2B50a%2FyZk6IlisH55Hu7drVbKsKkPIWF00HuYurfJZE7MYBahFAcE0jgS60f4NqMemEP2VNYA0dDNKOCfP8iA7AIBKYuc%2FlpFOz9EfNtyecpH6YxdDhdEMkJqDtbW4S" rel="nofollow">融合</a></li>
<li><a href="https://link.segmentfault.com/?enc=%2FPoP2QpCbZel1TPMGKpzHg%3D%3D.4I2W%2Fw%2FnXKiVTZfVPBMJbPGziLUc0dDFD8iFn9ip4BHG96oUeO%2BZ1%2BUPhFy459V8ou1uOQpTPu3HCT0ebUYnt69gjZz6S76KmfThdi61lAWdxoP3pY2JO1TgUn1RfMOaWiNjifRBDoe8kr64N3ZpJQ%3D%3D" rel="nofollow">列表之外</a></li>
<li><a href="https://link.segmentfault.com/?enc=z29dDGZ25w0aPV4yi2zNpA%3D%3D.1kdiv8PvCIS8hwRWVuYPp2MBf%2BhcVwvh%2Br%2FpZk71NQL%2B68rJN6VrC1Ee%2BANKRgQyAZPvhEcTSrZ1PrX2x4KSAgKeTcHPsT%2Bk4R%2FbiTflaOAn%2Bv3tEPOyC9URhDkw8i0P" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=Md1OToZnrDUcXyz%2Bcuwi2Q%3D%3D.bs5k9UGPaCM6DKJs6BgQZ7lCdEJ8V9ISM%2BS3Yu9DMKBQP7EtGCKrVN9RNRZjWGg1eRwSzOvBzYwTBuOhpCF83w%3D%3D" rel="nofollow">第 9 章:递归</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=k8rruwnUdzBALUPQ9yG%2Bag%3D%3D.lSaE%2B1nGW%2BoOug7jQbFVHLXVBJIx8xKKARKsegRCkPOjFCvtNML1v4xv3ITcAlPhc3WA62vK18wAqR6Qc8KBjvHHIWyaI99hC0i3xD1BYEwV61o%2FHJvub62R%2FheOljwa" rel="nofollow">定义</a></li>
<li><a href="https://link.segmentfault.com/?enc=VewaeBRywJysHtkmASCb7g%3D%3D.%2BS%2FZJtNVoZbYPzME7iQ9u6HX7ucT%2BQvvOAT58spzLKv6HZdZ9Agk93Km8E1NihsFtFjbLVzVoXrw28ZXAbMiNpyPh9JqqldJjdSXdP3b5EzU%2BfYTDyUJ4IlyIXiknzoL3MVAavpP6YXimG6lydzOk54MmbkqNFLxeo1dwJ5SDiQ%3D" rel="nofollow">为什么选择递归</a></li>
<li><a href="https://link.segmentfault.com/?enc=Ddlb%2B8zYUBGBgUDlWQkYnw%3D%3D.ka5qIU4IAFGeUgFiwBa0DsGbFEA%2BwAfKmbpANc777z0f5QRSrmts%2Fscl7WaAv8Pi8EIZSaeU%2BWb5Z4QQBBaz53EzlGljNRzF0rSwBFhIWY8osvpSd3g7nQvyxZjPXr1vuA3h3K562%2BzFh8blIO2wyQ%3D%3D" rel="nofollow">声明式递归</a></li>
<li><a href="https://link.segmentfault.com/?enc=%2FsRtyQlMUgd4YEwLN30d5g%3D%3D.4ECfhlr9JgZvY59llmo3HsOUfct4jDuJGXKQsLcnaE%2BrwUgwuEVwkwPkotWOAkY4nwd6yYL15fORh9cNdCT9XcfF9jZMmDW8LGxYZlcQ5eVaWsk7x2u5atE%2BoDu95iHCGYsqaGkZhCW0eja4M9BRoA%3D%3D" rel="nofollow">重构递归</a></li>
<li><a href="https://link.segmentfault.com/?enc=2leYPoRPdPhmZe4%2FdZhBYg%3D%3D.2cfNW1%2BvfZtCG9jCgyeGFW5avqRa%2FoSPLjnffMcGDqJHuHrZURwzm5PN%2BlX1JQwm60k1MR2mPY4y47XxJRhrnLreZsZ4BFjLwqizIHoaiMZsvTSB%2FmpThDRAnNT7Qt2N" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=AyskE1ZBFQfaT2KmppoqqA%3D%3D.CSahwIh7mi79pHqLN0o2slplkPZqI%2FLJk0GqoNiCpeyE4xugaG1DilU0HA%2BMN2DLCGb56eLBlOrLjI2KbLoKAsnR5om%2BNF79AYMQVUbI79Y%3D" rel="nofollow">第 10 章:异步的函数式</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=gXJ2GK9r7ImerKshbfpBHQ%3D%3D.N4EB1XcUO0iyue%2FZnk42GMi%2BsQzAUapMpTnMsTSvUJflpE8diuYNAcTp9H5kD%2BU8yepX5r8xHzME2%2FL3vKe2Yawsm67VF7kMMhR9A4TGNkchFkXY2tYWP9TGsFcH%2FEvtE03d%2BE5ExmXLuA9sR%2Bos2Q%3D%3D" rel="nofollow">时间状态</a></li>
<li><a href="https://link.segmentfault.com/?enc=41JxqIWzTcAzi3n66jO8yg%3D%3D.GKkDrJtDCLGkSI%2BvsMM%2BCoukVuN0dTcVQrwbaUMzRUEB3w%2FzZwyRCp8JhtfQkxteLzQ6GqRyOjn4d3fmtKk5zqDrDLKH%2BQNvAx3g2OmDBt%2BzgHOFOvYdeFtCOmAer3HehwICd9hZER6RpJbCSgFFLpayrQ08XSRUua%2F2i8AyIV4%3D" rel="nofollow">积极的 vs 惰性的</a></li>
<li><a href="https://link.segmentfault.com/?enc=FbmOOhtVlXdKzO%2FypDw9NQ%3D%3D.z6r14lbMANuIQsPy0SEBbRJxXqKofJf5zfjJ2ZBl5W92i6t9FAXexDIuJMJ5CunDKCFTZ%2FMnA2C5et6tsPxxSZN4mPRXwDlts%2BNzBcv1Y6nBtNv5ALG1G0ML1sDsBEsYuWRs38GkAIKMq1Ve53G06gwr6pE4%2B7AFptMvZAuPjXP1jK3pDG0B1XEjQ0VVxT%2Br" rel="nofollow">响应式函数式编程</a></li>
<li><a href="https://link.segmentfault.com/?enc=I%2BcsKOKnikksLLWXMNggwQ%3D%3D.H87pnTazuOZklFGrhZGTdKW3ZihhVP0nI9U59DCeA8tikDYUshOhk%2B8m3pLbP40SryrOyfBjZE8zP1yS%2FQn3lvk9shohabefmtSlwXQGljYzQ%2FEtqk1Z36%2BYIWZGt414" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=yt31IbBGi4HZ9CKtlgvHTA%3D%3D.vZrud1HwSv7bEXLFh6ZJp7rPiV4xbUv4e9u6ty7V2Ahdm%2B14HZQ%2FsXhXyCP92M9YikiX2GFjJ5%2BuBBv2Y%2BvUQjszp0nIDj2uahjKYRVWFQY%3D" rel="nofollow">第 11 章:融会贯通</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=VJO%2FwsQ%2BvuIo7yKaii9Aww%3D%3D.pxsm0gziHeosGztQ2gHeGlhXEKNm%2Bt%2BkJNwmMkim6sjvzorAJpuuahuEayk%2B%2FE6TFauzyCNGiUl7%2FcCI%2FAbXV4FW00tcJ0haknAeS3UE3ZVgoh97j5b8wvnRONgzvB9D" rel="nofollow">准备</a></li>
<li><a href="https://link.segmentfault.com/?enc=GW2zigtAumq49XViBG9DCQ%3D%3D.4HQCHUMo0hLo%2BYYvWUrVwQv3NOurLFLlI56uceicfYPgiiFXH5QEUTwsCEK71waDKUEJiFX3wxglwbPz62RSQPJN5%2BZr5UfQyCWaq3eKIHIQkbzKPu90BFl07tLGsF2FoP5DsOvPH1kx8pX28q7HaQ%3D%3D" rel="nofollow">股票信息</a></li>
<li><a href="https://link.segmentfault.com/?enc=mgV4n4HjOBeLA3tqu9SyxA%3D%3D.dp15BLwCPP%2BlnUDgPAUUkY7xHs8n3hLzJeFq0ReCRqVlTFfFKdp%2FFoPScrREC2GwduwfRALfgvE3VUBtuAuZsGjXbzdR7ZTxpVu%2F0grehT08WbuBnkutpl3Y2gQtgsdoDABn6EQ6MBsQi6M4ocyoIy4B7MMtE84FGMx8j2BO%2Bqk%3D" rel="nofollow">股票行情界面</a></li>
<li><a href="https://link.segmentfault.com/?enc=a7SC7M6S03f43plzcrv%2Flg%3D%3D.BvbYojrbdiykC2soEwzxKyE3sBhw6D26jBI0Mf2Z9SlUIg1kpqFeUR6tfyVVadxjRbeVcGfeB7Uh5NYq8Ua8OlsL%2FiDcsunxlP%2FegAnYkuyHZ15yQjAmnxOXRuIjsM%2BO" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=9TVtPaUjD1mrfrZC9f62LA%3D%3D.9kUHyF3xUzWJyiIHOUJ4ZfKuNoO4WG7mfIHIaTlVJdrBLNhpj4bIdcwQhrXC%2FWOY3UqGtP2J%2F4Fc%2F%2FOojDF6kA%3D%3D" rel="nofollow">附录 A: Transducing</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=W3pU6aYlevR3OVxEkeJfpQ%3D%3D.c4BkZvCdJBb%2B4kQblT5HcDZmObJjy5rlGEaTaCoH6ee%2B%2FcIJd%2FUxkinMnvvdr8I0PGWfRl%2FKZVcjB%2Fl9KlhZR2cVCSMjnqaY5LjxiomZiq%2FE2ophAhmUvgS5dB1Fhwcr4r66jGrY1IhRShNeSHcBqw%3D%3D" rel="nofollow">首先,为什么</a></li>
<li><a href="https://link.segmentfault.com/?enc=Q3n7daCh45XQnA5olXHUrA%3D%3D.2H8iGuzzqJysqbqp8eozZa4H0VLSYUHbQDkk4Hb3%2FajG9CUUTRXq1ifvWqS7en%2BNVznbzn2lyEDClt1yR8IAQuAnbSE92QM%2FfIhCaQgFuuq0RXTMCtSqIIqbHi4RFBu00rR8I%2FpizOtfIwUDDtPOjA%3D%3D" rel="nofollow">如何,下一步</a></li>
<li><a href="https://link.segmentfault.com/?enc=h7bys1%2BVR5Q57xIKZStAgA%3D%3D.nOqMSGIAyauqqaf7CPHzvslH5UwUhnjcVCoB9ReGiplcJaXIZJwtDaOtxCNyJtM8ddLTPXVQ2%2FvjWYvsfJFyzw2V%2BzBetJZE37P0Ej%2BcCq1JmV2kkUcUTY2kMUN8q7Qs" rel="nofollow">最后</a></li>
<li><a href="https://link.segmentfault.com/?enc=pLgD2GLGjlrlKMebNaQEhQ%3D%3D.iReV681lK7MnD3OtQr8xUqlz1pQMoZkzLOiSY95M8laAAmbSJo7Z9dUDp%2B%2FaA%2BeMQ6wLL3dBqmqkTzSmMHS8hAPsU8QBpsQl1nzFjwRfz73W5u9%2B2BEt6Z8z1C8q%2FKZQ" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=%2BNJSuVUWCTsu3pL%2Bkxf4uw%3D%3D.Mj15gKFaK1d%2FTd3mM9EYnZYEew5cn4y5kwYicK6GVwaBRXQyP1Pk69AXlgqvGoBB%2FuZGory6qOJ7sYJRGQ9t5Q%3D%3D" rel="nofollow">附录 B: 谦虚的 Monad</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=nRbMy7Jxs0FnG2wwzpuYmg%3D%3D.N8faHuELSI5PyIfsiPbF2kesqblSO%2BLMlg%2BcodiXjbwzbGvbVWenx5d0k2Jk4V%2FsgVaa2qVk0V%2FQIP1q31lyExSWixoBK8oDPkq%2FlcxB0vUBi84hAnFdK0UYr42TYVaF" rel="nofollow">类型</a></li>
<li><a href="https://link.segmentfault.com/?enc=2p6dkAaounFrHpuPAq0uYw%3D%3D.ywGGQFCnFlzBN2%2FNLDgtQNinMR9wKdk0hEEAjAilSOk81YtP9HU7sySk5iTRNSSpiuCF1A7rVcuy5NF8vFdQTSw07I80PkPr3QU18m%2BiUbDgdEirrynlZXndzQZWSx%2BVXmMTvhXm24%2BcsXDzQybFVQ%3D%3D" rel="nofollow">松散接口</a></li>
<li><a href="https://link.segmentfault.com/?enc=U2fsEmFmzWfjUaubujlKbg%3D%3D.NCPHWzSaNp2dlfMVzqckbeYd595VsyDqqUL%2BZ%2BuVpjR1zLpERh5pXoNiWy9tWpdAuG4tD0RRNj4kPblgjK8uzNe9OQvbchGCvn5l678sPHE%3D" rel="nofollow">Maybe</a></li>
<li><a href="https://link.segmentfault.com/?enc=UKkvmLnUWMhPqFdsiiz4dQ%3D%3D.EQT6uEVd0wKZxk2aIPJ1PbJi%2FVszOG8ZTjxyxGCeUoijJVQcIOIkKZRn7rFfIOoMKw5GYqePa5LbeiUbtzv0Wm%2FpfapXDiEhO53IUSlMW%2Fg%3D" rel="nofollow">Humble</a></li>
<li><a href="https://link.segmentfault.com/?enc=EVFiFXVRD%2FLJh5%2BWLWkf7w%3D%3D.vlqN%2BADIeKeEub5neuD7aFCCLJtI0sffYD5xuAi4fXbevXG%2B%2BbsrkBh01GofJ5hAEuVkq2KcL9RvOCSifEau2NgVftwpkH%2BMx5EZoQyZ0aE%3D" rel="nofollow">Humility</a></li>
<li><a href="https://link.segmentfault.com/?enc=YbtnEsI5FaXdyXTq1l97Iw%3D%3D.pRMR5HBNRBiOx%2FjJMIuMZhdETGFQZZTvIS%2BNgoG6OzQsJNvnuvID%2F1tkC9NcSlOWoikbAwGQNPO8M4haBKAN71E22jB4fM1fnmkLrPeWb%2BZvM8Ciov%2FqNTTvR9Df%2BULF" rel="nofollow">总结</a></li>
</ul>
</li>
<li>
<p><a href="https://link.segmentfault.com/?enc=Rr8BjrEWh4r1C341idjviA%3D%3D.AC%2BTjmI8IwlZkD6j1zkfeu9Wi%2BeW%2BBq9W4m8%2BJrgvlsp90guo3KMSSEJ4zjlCBonbpEU9okCeLvjRdPTmVMEDQ%3D%3D" rel="nofollow">附录 C: 函数式编程函数库</a></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=nH7wT6MBx2EvzfAco9A8UA%3D%3D.E6mszlE4b2t30QKnGheHTiVhXed%2F9pEhOzNxc3g%2FUBs1AmrHkemoFdFtVKp1Eu%2BKcVOcL8047LROXvxk2LHJqTXukpcLLPOOEACTXeSIkSE%3D" rel="nofollow">Ramda (0.23.0)</a></li>
<li><a href="https://link.segmentfault.com/?enc=a%2F9%2FP8bLfc3Qy%2F9jbiS4qw%3D%3D.jEr1O0I5L%2BCkt%2Fc9FY83q%2BWCoKKf22r%2B5ZnjHNcLzVCAcxoI%2Bp7VpuCSknjKAer7xUD%2F%2FO7Qd5OgLlgIKTR3meF%2BJeafQXSSc%2FMhpw%2B%2FBmE%3D" rel="nofollow">Lodash/fp (4.17.4)</a></li>
<li><a href="https://link.segmentfault.com/?enc=%2BhsfL07ERq8N8bsi7VtubQ%3D%3D.GPhMsscd9kGTMaGdKX7P3d9OaSmNxsgnDEQmh1TQD73oq3Gr1IJes4%2FTOnQwdmZvShUXzSb0aI2FAgq%2Fv%2BShv%2Fdhi9JkmOV%2FdLcJ08Gl75I%3D" rel="nofollow">Mori (0.3.2)</a></li>
<li><a href="https://link.segmentfault.com/?enc=HZX4Zd2dtEZeQVgsAsdpUw%3D%3D.NxEYXMclw%2BNgRcNq3MpkAvZa7Ey3r8P66f9PNeA87zRa8E%2BDlsvcu8lN%2FVwjz%2FMPdsFx8kh%2FldTMlhzCXgWct8Jb76OUL5ES5i2hqTyCOmikzx3f9vR6DJRYYjsagscy" rel="nofollow">总结</a></li>
</ul>
</li>
<h2>关于出版</h2>
<p>本书主要在 <a href="https://link.segmentfault.com/?enc=30BBFwBvOhZMeO0dqOCvZg%3D%3D.mngn5x0NVU2AUrAJKOQCEV31SP9oSrzO1Ue7Jjdvqok%3D" rel="nofollow">on Leanpub</a> 平台上以电子版本的形式进行出版。我也尝试出售本书的纸质版本,但没有确定的方案。</p>
<p>除了购买本书以外,如果你想要对本书作一些物质上的捐赠,请在 <a href="https://link.segmentfault.com/?enc=ONtUq%2BnUEchwudabBwDPIg%3D%3D.3gsGC%2Fuox4CGxGkAJULGdlGlHKry6p8aoOdrSvzr9Do%3D" rel="nofollow">patreon</a> 上进行操作。本书作者感谢你的慷慨解囊。</p>
<p><img src="/img/remote/1460000010887895" alt="Patreon" title="Patreon"><br><a href="https://link.segmentfault.com/?enc=KlSmH5n%2FCBxTeuo8HDVqGw%3D%3D.1r0jUpgp%2FyVEmQ41H%2FXfZn340t3LOPc%2B8HxE2VRXlCI%3D" rel="nofollow">Patreon</a></p>
<h2>Contributions</h2>
<p><strong>非常欢迎</strong>对于本书的任何内容贡献。但是在提交 PR 之前<strong>请务必</strong>认真阅读 <a href="https://link.segmentfault.com/?enc=ULZu%2FjsEZFksviupWaum%2BQ%3D%3D.ZigJC08N6LNbDP8VwyMWGK3o9rcBWMfekdQGWKShOWLdHL4WoXaPyhew3gZW8Y3Q89XhLriSwRjhFpIGzfl3%2FoAsRm024lZm4cAzQL2jkE0%3D" rel="nofollow">Contributions Guidelines</a>。</p>
<h2>License & Copyright</h2>
<p>本书所有的材料和内容都归属 (c) 2016-2017 Kyle Simpson 所有。</p>
<p><a rel="license" href="http://creativecommons.org/licenses/by-nc-nd/4.0/"><img alt="Creative Commons License" style="border-width:0" src="</a><a href="https://link.segmentfault.com/?enc=ujnvwoN6PuRnQA6fdSOYpg%3D%3D.8hwSLQSwsHk0H5lrqahs04UVpRy7Y1eDIM1kjmBWG98BpCKVY3mQzoEcxCU1amtMVlEUN4ObVlceI%2F%2BI4pzq7TXmtW%2F521Gg2hV6JljCope9I7U8%2BQLxqc2LMqSiptjO" rel="nofollow">https://user-gold-cdn.xitu.io...</a>; /><br>本书根据<a rel="license" href="http://creativecommons.org/licenses/by-nc-nd/4.0/">Creative Commons Attribution-NonCommercial-NoDerivs 4.0 Unported License</a> 进行授权许可.</p>
<ol><li>
<a></a> FP,本书统称为函数式编程。</li></ol>
<ol><li>
<a></a> FPer,本书统称为函数式编程者。</li></ol>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
<p><img src="/img/remote/1460000012394646?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000012394647" alt="" title=""></p>
<blockquote><p>P.S. 整理的好辛苦 %》——《%</p></blockquote>
翻译连载 | 附录 C:函数式编程函数库-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
https://segmentfault.com/a/1190000012371068
2017-12-11T11:12:20+08:00
2017-12-11T11:12:20+08:00
iKcamp
https://segmentfault.com/u/ikcamp
3
<ul>
<li>原文地址:<a href="https://link.segmentfault.com/?enc=lZYcyci8vMQVpuDnWfZBNg%3D%3D.Xb9T5prT9snrYJskzbLdXsYfTxNs4sVgNjxcJ7R5xB5Atbj8QuwaavjmAhzXfUyL" rel="nofollow">Functional-Light-JS</a>
</li>
<li>原文作者:<a href="https://link.segmentfault.com/?enc=Q%2BMUlHB9UwshqsxUUk1tgA%3D%3D.UoZcCXkW6vQfEnbTkURDcUCCm%2FOAOrcQS3KcYp%2BnVxw%3D" rel="nofollow">Kyle Simpson-《You-Dont-Know-JS》作者</a>
</li>
</ul>
<blockquote>
<p>关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。</p>
<p>译者团队(排名不分先后):<a href="https://link.segmentfault.com/?enc=PYgKkgdYMTuKxnGK2PyVcg%3D%3D.VazCmC2Me86vRPqWrGiFPbrUX6Y2F5HDNio1TBK8ow4%3D" rel="nofollow">阿希</a>、<a href="https://link.segmentfault.com/?enc=1NHDm8hAANXCzQAF6aotIQ%3D%3D.E5KxoKAKZ4hhHOLmo9pZXea2EyAb%2FnMOxvRh0451dBI%3D" rel="nofollow">blueken</a>、<a href="https://link.segmentfault.com/?enc=jTIG4iKCvBNh9ADZ0%2FIy8A%3D%3D.ifghWKLh6crDDk%2BAydKW4qh7syQgAgoWPlKpfxzv0Pc%3D" rel="nofollow">brucecham</a>、<a href="https://link.segmentfault.com/?enc=XmT%2F8Zt7Dj%2FNTq68FvKWWg%3D%3D.sFcHx1Nv0hfDY56cTKwe%2FQ7o1ktyFuy5a8hXTlulp2U%3D" rel="nofollow">cfanlife</a>、<a href="https://link.segmentfault.com/?enc=TXdH3r5YeHGrf6C8Tj1FiQ%3D%3D.CWJdIJ0Ykzg%2B%2B5gAOsqhlgX7DtcQYlm0Cn4N3ZpVZQ8%3D" rel="nofollow">dail</a>、<a href="https://link.segmentfault.com/?enc=ZT6vFtpBDDdq6V1Vb6Gc3g%3D%3D.dwBAcxCbXDnw6e6CzfYUVIfHhO0Rwb8rNP28gVfDfKw%3D" rel="nofollow">kyoko-df</a>、<a href="https://link.segmentfault.com/?enc=wy4aE7JFFn3KzyoZbCHYHA%3D%3D.BYQ8eQafV0ttPI%2FIkVxtyNJ1HHx5a6GTKbCTQ%2BeeslU%3D" rel="nofollow">l3ve</a>、<a href="https://link.segmentfault.com/?enc=uq4UrHuIOLTi1YhGzSe1Gg%3D%3D.Rrx3UC9fbVDxuTuZ33FI0g42S9wsR1NMniCURI2FE7o%3D" rel="nofollow">lilins</a>、<a href="https://link.segmentfault.com/?enc=7rcU00taz7BFN09gMpU7SA%3D%3D.4mzHt6ViKPruL%2BUxxEN8CFws4y9S0qUH8AvK0MJaJSYu1P%2BxHFnQZbjFd5G6qr1A" rel="nofollow">LittlePineapple</a>、<a href="https://link.segmentfault.com/?enc=mVj0AWnDy5FnFsVWA0B%2BDg%3D%3D.hy2P65Bd0wBsYCi2suDwAwDcNcsK%2FGDsXNdOLSHq2lg%3D" rel="nofollow">MatildaJin</a>、<a href="https://link.segmentfault.com/?enc=UEdqB3NTkb6oObcxIoieHg%3D%3D.RbnWt2ATpKDk9acLDUfcwKPGY3h96oDFfQvPbmsGObw%3D" rel="nofollow">冬青</a>、<a href="https://link.segmentfault.com/?enc=MG%2BDPnl%2Fmajy8PCy15OJbw%3D%3D.B9mnTU7qD9gl9xrcDwMhUipsPEqQMItJKDwVXBpa1aY%3D" rel="nofollow">pobusama</a>、<a href="https://link.segmentfault.com/?enc=%2BFO5A7fzVdJJMCeYg%2FjonQ%3D%3D.f4Bj8vgdZxTyVPVns06G3%2Fw8Np4uX9dv8GIFibVSHIw6TRX37QUQZbl5PBN6yFeq" rel="nofollow">Cherry</a>、<a href="https://link.segmentfault.com/?enc=NmCef1cAQKYAXcKSi1rQZQ%3D%3D.sXS%2FqDkY3r8PqrhJb%2FCcrr3WBZvNFqabo%2B1K94NneLA%3D" rel="nofollow">萝卜</a>、<a href="https://link.segmentfault.com/?enc=GvR81t%2B3ATRHTXeG4WvkDg%3D%3D.ZWae48Gzi12wBcQ9aOCbQTt%2BghzS4wc6oG%2FgX7D50nI%3D" rel="nofollow">vavd317</a>、<a href="https://link.segmentfault.com/?enc=xOY5YhTRtPe34D%2BdZQdtIQ%3D%3D.ekyvPqJpTx%2BX0YUIwBaFmaMaB56cBLo8SqAGBIVRq5E%3D" rel="nofollow">vivaxy</a>、<a href="https://link.segmentfault.com/?enc=1x8W974i2m%2BHFrhgeEsA%2Bg%3D%3D.sR5CiaduDI8BHi62IG7IMWUCALWATUe47DwVrRjO%2BMc%3D" rel="nofollow">萌萌</a>、<a href="https://link.segmentfault.com/?enc=K8duha6GTNDD0Uu8W5XnVQ%3D%3D.kIqQPXeFKXaSmwRdwQlaI5i2KcPAwdoVYZhKcGF6rCA%3D" rel="nofollow">zhouyao</a></p>
</blockquote>
<h2>JavaScript 轻量级函数式编程</h2>
<h2>附录 C:函数式编程函数库</h2>
<p>如果您已经从头到尾通读了此书,请花一分钟的时间停下来回顾一下从第 1 章到现在的收获。相当漫长的一段旅程,不是吗?希望您已经收获了大量新知识,并用函数式的方式思考你的程序。</p>
<p>在本书即将完结时,我想给你提供一些关于使用官方函数式编程函数库的快速指南。注意这并不是一个详细的文档,而是将你在结束“轻量级函数式编程”后进军真正的函数式编程时应该注意的东西快速梳理一下。</p>
<p>如果有可能,我建议你<strong>不要</strong>做重新造轮子这样的事情。如果你找到了一个能满足你需求的函数式编程函数库,那么用它就对了。只有在你实在找不到合适的库来应对你面临的问题时,才应该使用本书提供的辅助实用函数 —— 或者自己造轮子。</p>
<h3>目录</h3>
<p>在本书第 1 章曾列出了一个函数式编程库的列表,现在我们来扩展这个列表。我们不会涉及所有的库(它们之中有许多重复的内容),但下面这些你应该有所关注:</p>
<ul>
<li>
<a href="https://link.segmentfault.com/?enc=8vfxAQWgbiLp%2Be5I8wwilg%3D%3D.Zb7caPaAfBKPKi8PQBNLaExz1x3SXYfMMXw5L2IN0P8%3D" rel="nofollow">Ramda</a>:通用函数式编程实用函数</li>
<li>
<a href="https://link.segmentfault.com/?enc=gbq0tqHedwtfwKaYcVkGJA%3D%3D.3uvUTHU%2BtcJR3yuXngzVnb04TRRbgzzXvedOUh4hIoWYXVDARocj9n7AoTzmhgXt" rel="nofollow">Sanctuary</a>:函数式编程类型 Ramda 伴侣</li>
<li>
<a href="https://link.segmentfault.com/?enc=WvcqYEUTeG7gJWx%2BOWWEkg%3D%3D.t%2BSLxlYicaXmw5acijzs0LbMGsMLUb%2FHkMS1HvWpMMrHGHAVq%2FMo0T3azq9wMVf0" rel="nofollow">lodash/fp</a>:通用函数式编程实用函数</li>
<li>
<a href="https://link.segmentfault.com/?enc=PMz5V9OWiMTXfnAJJHUcdg%3D%3D.Ppv19xrr1aH4MyL8qBPAXSOhkjwkKgaeAt18lsYhukw%3D" rel="nofollow">functional.js</a>:通用函数式编程实用函数</li>
<li>
<a href="https://link.segmentfault.com/?enc=6yBVGWo39Eenr3RGDd6tdw%3D%3D.svXiOQ8kHVpvyKkhOJuvzl8Gn4jfqGyqmBIaD2Ep4cxxdAgJ5Vgs5xQWiCOrZ%2B9%2B" rel="nofollow">Immutable</a>:不可变数据结构</li>
<li>
<a href="https://link.segmentfault.com/?enc=CJxgz6qroyy5G0JfFEebLA%3D%3D.6jAPdouBkillvD2P9ip00TAf5vrxeDSdz5lawPlCrDw6%2Ft0IXqw20i5PXGBHILa7" rel="nofollow">Mori</a>:(受到 ClojureScript 启发)不可变数据结构</li>
<li>
<a href="https://link.segmentfault.com/?enc=CL6duCF97sP%2FyAbzrkxVFw%3D%3D.estmJFOLqW79URObGetpyrEE7HETDb9uZ8x9966EOjPP7EWCnunBNSqQrecL0hfB" rel="nofollow">Seamless-Immutable</a>:不可变数据助手</li>
<li>
<a href="https://link.segmentfault.com/?enc=NknNVQFKF0OiqXqKpUGjCQ%3D%3D.g3%2FCv9kpDY6Nqzoq6K1RNhO4hzTYe1%2BpL8zhsU35lECbKwh1Lj82jTln1173oW2ApWyT%2BLCAI7Cm4PEn%2Bf%2F4KQ%3D%3D" rel="nofollow">tranducers-js</a>:数据转换器</li>
<li>
<a href="https://link.segmentfault.com/?enc=YR0KR%2FiMs08hRC4sCkK2cA%3D%3D.t9qq9VQ72%2FElrhrYHK%2F13LpfyWkl4tplxZ%2Bsh4otEeNJMwF4aKZAts1Yl8OeLWZI" rel="nofollow">monet.js</a>:Monad 类型</li>
</ul>
<p>上面的列表只列出了所有函数式编程库的一小部分,并不是说没有在列表中列出的库就不好,也不是说列表中列出的就是最佳选择,总之这只是 JavaScript 函数式编程世界中的一瞥。您可以前往<a href="https://link.segmentfault.com/?enc=pAFb4jh%2FgExm93BWYb2cog%3D%3D.B7zbT8aMC1u88QBiz1jmzpaWmg7prLZuDkfC3BE0PKgWGqZ0pJAdinovoTZ2YEDC" rel="nofollow">这里</a>查看更完整的函数式编程资源。</p>
<p><a href="https://link.segmentfault.com/?enc=rtzqd3XKBwAbNCdb58J01Q%3D%3D.Agwv1Y8AJ17qelQEXm1WUgBzDdb65ZyDLaFQZZuKv1qW2n8B3XM1L6E1%2BW92WRcI" rel="nofollow">Fantasy Land</a>(又名 FL)是函数式编程世界中十分重要的学习资源之一,与其说它是一个库,不如说它是一本百科全书。</p>
<p>Fantasy Land 不是一份为初学者准备的轻量级读物,而是一个完整而详细的 JavaScript 函数式编程路线图。为了尽可能提升互通性,FL 已经成为 JavaScript 函数式编程库遵循的实际标准。</p>
<p>Fantasy Land 与“轻量级函数式编程”的概念相反,它以火力全开的姿态进军 JavaScript 的函数式编程世界。也就是说,当你的能力超越本书时,FL 将会成为你接下来前进的方向。我建议您将其保存在收藏夹中,并在您使用本书的概念进行至少 6 个月的实战练习之后再回来。</p>
<h3>Ramda (0.23.0)</h3>
<p>摘自 <a href="https://link.segmentfault.com/?enc=WZf67iZ7sZP8FDg%2FOd4hXg%3D%3D.fk9MJQlT1JrCZ78v72kIShikuhussdr4%2B5QQjOpFFvw%3D" rel="nofollow">Ramda 文档</a>:</p>
<blockquote>
<p>Ramda 函数自动地被柯里化。</p>
<p>Ramda 函数的参数经过优化,更便于柯里化。需要被操作的数据往往放在最后提供。</p>
</blockquote>
<p>我认为合理的设计是 Ramda 的优势之一。值得注意的是,Ramda 的柯里化形式(似乎大多数的库都是这种形式)是我们在第 3 章中讨论过的“松散柯里化”。</p>
<p>第 3 章的最后一个例子 —— 我们定义无值(point-free)工具函数 <code>printIf()</code> —— 可以在 Ramda 中这样实现:</p>
<pre><code class="js">function output(msg) {
console.log( msg );
}
function isShortEnough(str) {
return str.length <= 5;
}
var isLongEnough = R.complement( isShortEnough );
var printIf = R.partial( R.flip( R.when ), [output] );
var msg1 = "Hello";
var msg2 = msg1 + " World";
printIf( isShortEnough, msg1 ); // Hello
printIf( isShortEnough, msg2 );
printIf( isLongEnough, msg1 );
printIf( isLongEnough, msg2 ); // Hello World</code></pre>
<p>与我们在第 3 章中的实现相比有几处不同:</p>
<ul>
<li>我们使用 <code>R.complement(..)</code> 而不是 <code>not(..)</code> 在 <code>isShortEnough(..)</code> 周围新建一个否定函数 <code>isLongEnough(..)</code>。</li>
<li>使用 <code>R.flip(..)</code> 而不是 <code>reverseArgs(..)</code> 函数,值得一提的是,<code>R.flip(..)</code> 仅交换头两个参数,而 <code>reverseArgs(..)</code> 会将所有参数反向。在这种情景下,<code>flip(..)</code> 更加方便,所以我们不再需要使用 <code>partialRight(..)</code> 或其他投机取巧的方式进行处理。</li>
<li>
<code>R.partial(..)</code> 所有的后续参数以单个数组的形式存在。</li>
<li>因为 Ramda 使用松散柯里化,因此我们不需要使用 <code>R.uncurryN(..)</code> 来获得一个包含所有参数的 <code>printIf(..)</code>。如果我们这样做了,就相当于使用 <code>R.uncurryN(2, ..)</code> 包裹 <code>R.partial(..)</code> 进行调用,这是完全没有必要的。</li>
</ul>
<p>Ramda 是一个受欢迎的、功能强大的库。如果你想要在你的代码中实践 FP,从 Ramda 开始是个不错的选择。</p>
<h3>Lodash/fp (4.17.4)</h3>
<p>Lodash 是整个 JS 生态系统中最受欢迎的库。Lodash 团队发布了一个“FP 友好”的 API 版本 —— <a href="https://link.segmentfault.com/?enc=4hCELkqCGVmwxGZ0tdJDVw%3D%3D.%2BLPB3nE3GSNBY%2FN8D2cEqoxiovoAPgIeGRMYakvQh9rNY%2BWgUcsNec7Eb96wdD3D" rel="nofollow">"lodash/fp"</a>。</p>
<p>在第 8 章中,我们讨论了合并独立列表操作(<code>map(..)</code>、<code>filter(..)</code> 以及 <code>reduce(..)</code>)。使用“lodash/fp”时,你可以这样做:</p>
<pre><code class="js">var sum = (x,y) => x + y;
var double = x => x * 2;
var isOdd = x => x % 2 == 1;
fp.compose( [
fp.reduce( sum )( 0 ),
fp.map( double ),
fp.filter( isOdd )
] )
( [1,2,3,4,5] ); // 18</code></pre>
<p>与我们所熟知的 <code>_.</code> 命名空间前缀不同,“lodash/fp”将 <code>fp.</code> 定义为其命名空间前缀。我发现一个很有用的区别,就是 <code>fp.</code> 比 <code>_.</code> 更容易识别。</p>
<p>注意 <code>fp.compose(..)</code>(在常规 lodash 版本中又名 <code>_.flowRight(..)</code>)接受一个函数数组,而不是独立的函数作为参数。</p>
<p>lodash 拥有良好的稳定性、广泛的社区支持以及优秀的性能,是你探索 FP 世界时的坚实后盾。</p>
<h3>Mori (0.3.2)</h3>
<p>在第 6 章中,我们已经快速浏览了一下 Immutable.js 库,该库可能是最广为人知的不可变数据结构库了。</p>
<p>让我们来看一下另一个流行的库:<a href="https://link.segmentfault.com/?enc=Hi2szQ1TXGuA1kxfrZKmzA%3D%3D.85K5HHUTS4qz4xmrgEQhVQEfN%2BXzu9WfbLFazKWgf4oeSTgvauyEEIzqBhSaVt4F" rel="nofollow">Mori</a>。Mori 设计了一套与众不同(从表面上看更像函数式编程)的 API:它使用独立的函数而不直接在值上操作。</p>
<pre><code class="js">var state = mori.vector( 1, 2, 3, 4 );
var newState = mori.assoc(
mori.into( state, Array.from( {length: 39} ) ),
42,
"meaning of life"
);
state === newState; // false
mori.get( state, 2 ); // 3
mori.get( state, 42 ); // undefined
mori.get( newState, 2 ); // 3
mori.get( newState, 42 ); // "meaning of life"
mori.toJs( newState ).slice( 1, 3 ); // [2,3]</code></pre>
<p>这是一个指出关于 Mori 的一些有趣的事情的例子:</p>
<ul>
<li>使用 <code>vector</code> 而不是 <code>list</code>(你可能会想用的),主要是因为文档说它的行为更像 JavaScript 中的数组。</li>
<li>不能像在操作原生 JavaScript 数组那样在任意位置设置值,在 vector 结构中,这将会抛出异常。因此我们必须使用 <code>mori.into(..)</code>,传入一个合适长度的数组来扩展 vector 的长度。在上例中,vector 有 43 个可用位置(4 + 39),所以我们可以在最后一个位置(索引为 42)上写入 <code>"meaning of life"</code> 这个值。</li>
<li>使用 <code>mori.into(..)</code> 创建一个较大的 vector,再用 <code>mor.assoc(..)</code> 根据这个 vector 创建另一个 vector 的做法听起来效率低下。但是,不可变数据结构的好处在于数据不会进行克隆,每次“改变”发生,新的数据结构只会追踪其与旧数据结构的不同之处。</li>
</ul>
<p>Mori 受到 ClojureScript 极大的启发。如果您有 ClojureScript 编程经验,那您应该对 Mori 的 API 感到非常熟悉。由于我没有这种编程经验,因此我感觉 Mori 中的方法名有点奇怪。</p>
<p>但相比于在数据上直接调用方法,我真的很喜欢调用独立方法这样的设计。Mori 还有一些自动返回原生 JavaScript 数组的方法,用起来非常方便。</p>
<h3>总结</h3>
<p>JavaScript 不是作为函数式编程语言来特别设计的。不过其自身的确拥有很多对函数式编程非常友好基础语法(例如可作为变量的函数、闭包等)。本章提及的库将使你更方便的进行函数式编程。</p>
<p>有了本书中函数式编程概念的武装,相信你已经准备好开始处理现实世界的代码了。找一个优秀的函数式编程库来用,然后练习,练习,再练习。</p>
<p>就是这样了。我已经将我目前所知道的知识分享给你了。我在此正式认证您为“JavaScript 轻量级函数式编程”程序员!好了,是时候结束我们一起学习 FP 这部分的“章节”了,但我的学习之旅还将继续。我希望,你也是!</p>
<p><strong>【上一章】<a href="https://link.segmentfault.com/?enc=cijjDU6sh1KeAvuAuNOxlw%3D%3D.5FQkyfFZVBcGS48zYhvZJKG4RiAoT5HWAA8l4WfqM5kk2rLfuzDnWRFgnr%2BY7KCq" rel="nofollow">翻译连载 | 附录 B: 谦虚的 Monad-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇</a> </strong></p>
<p><img src="https://user-gold-cdn.xitu.io/2017/12/11/160438cc9db18373?w=1426&h=778&f=png&s=414615" alt="" title=""></p>
<blockquote>
<p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=oUxq%2FumDL6vaK1B19ATtTA%3D%3D.17wEKfhKTTRP%2BVLWqEt8i1onAsI0ouRXteBlDWM7k38%3D" rel="nofollow">https://www.ikcamp.com</a><br>访问官网更快阅读全部免费分享课程:<br>《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》<br>《iKcamp出品|基于Koa2搭建Node.js实战项目教程》<br>包含:文章、视频、源代码<br><img src="https://dn-mhke0kuv.qbox.me/3326ff01e1f06fd6dc3c.png" alt="" title=""></p>
</blockquote>
<p><img src="https://user-gold-cdn.xitu.io/2017/12/11/16043969b791f026?w=860&h=860&f=jpeg&s=201392" alt="" title=""></p>
iKcamp&掘金Podcast直播回顾(12月2号和9号的两场)
https://segmentfault.com/a/1190000012362363
2017-12-10T15:12:10+08:00
2017-12-10T15:12:10+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<h2>12月2号-哈志辉-前端工程化沪江CCtalk实践</h2>
<blockquote>
<p>哈志辉:沪江CCtalk前端架构师,《移动Web前端高效开发实战》作者之一</p>
<p>在线地址:<a href="https://link.segmentfault.com/?enc=yJ571qMlOcqK549%2BqgaPkg%3D%3D.9AD%2BsD5MrF0A7h4wT3PvleWggGMGsC6cvjd6GvnNAQVEpVC%2F0Mb9B9XsRO9qRavv" rel="nofollow">https://www.cctalk.com/v/15123890788754</a></p>
</blockquote>
<p><img src="/img/remote/1460000012362368?w=1730&h=964" alt="" title=""></p>
<h2>12月9号-陈达孚-PWA实现渐进式Web应用</h2>
<blockquote>
<p>陈达孚:香港中文大学计算机硕士,《移动Web前端高效开发实战》作者之一,《前端开发者指南2017》译者之一,中国前端开发者大会、中生代技术大会荣誉讲师,沪江横向开发架构组成员。</p>
<p>在线地址:<a href="https://link.segmentfault.com/?enc=LssSoZbse9huCezGwtlBrA%3D%3D.Yn0AG7nnJF9oLRTZKoidhU3EIVkHWb1a3tjXpnfqB1xJka4vJNW1M4Y8PlrvI4Nw" rel="nofollow">https://www.cctalk.com/v/15128864673116</a></p>
</blockquote>
<p><img src="/img/remote/1460000012362369?w=1722&h=962" alt="" title=""></p>
<blockquote><p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=zzhybgGx2T4g3ApwphLB3w%3D%3D.yknFyfmrwWYjMVMzBIi81YH12ui23XZUgTAvgBhJjbI%3D" rel="nofollow">https://www.ikcamp.com</a></p></blockquote>
<p><img src="/img/remote/1460000012362370?w=1426&h=778" alt="" title=""></p>
<p><img src="/img/remote/1460000012362371?w=860&h=860" alt="" title=""></p>
译|调整JavaScript抽象的迭代方案
https://segmentfault.com/a/1190000012338478
2017-12-08T10:27:33+08:00
2017-12-08T10:27:33+08:00
iKcamp
https://segmentfault.com/u/ikcamp
1
<blockquote><ul>
<li>原文作者:Kaloyan Kosev</li>
<li>原文链接:<a href="https://link.segmentfault.com/?enc=XR%2BX7QQQvZXKeHTGhZVcpg%3D%3D.01jpVR%2BziAMm4LiRVadnD4iOGmJfdHs95uKS%2FvAFbPBmb169m%2BBLp7MYvaMrwsyTSSfOeKIeUsdIg1ggDw24wQ%3D%3D" rel="nofollow">https://css-tricks.com/adapting-javascript-abstractions-time/</a>
</li>
<li>翻译译者:小溪里</li>
<li>校对:华翔、小冬</li>
</ul></blockquote>
<p>即使还没有读过我的文章《<a href="https://link.segmentfault.com/?enc=Q0wldg1WabyHgENQRS4dNw%3D%3D.f4KameQNhkvNmG29960vfjN5QfDpnW4l%2BH%2B2EeArEeuyBVvK5Iw5q8kfXHUyOf1I3EAHQW3B4ZH1pdRkuogr%2BwqjfTJHbF2VErG89uNhrX8%3D" rel="nofollow">在处理网络数据的 JavaScript 抽象的重要性</a>》,你也很有可能已经意识到代码的可维护性和可扩展性很重要,这也是介绍 <code>JavaScript</code> 抽象的目的。</p>
<p>为了更加清楚的说明,我们假设在 <code>JavaScript</code> 中抽象是一个模块。</p>
<p>一个模块的最初实现只是它们漫长(也许是持久的)的生命周期过程的开始。我将一个模块的生命周期分成 3 个重要阶段。</p>
<ol>
<li>引入模块。在项目中编写该模块或复用该模块;</li>
<li>调整模块。随时调整模块;</li>
<li>移除模块。</li>
</ol>
<p>在我先前的<a href="https://link.segmentfault.com/?enc=AfA%2FfUkBaiS1pv9ftyPV6w%3D%3D.JJ6Tl4gOs93Q%2FoY6v92jPr2WBejS22QkQROjMA4PGHg6HsNz5lJiDd39sJ197ApHUw0VqTOMFoZ7hO3YpSmQ3ZUG4nIiZGZVmpCLGOplCz0%3D" rel="nofollow">文章</a>中,重心放在了第一点上。而在这篇文章中,我将把重点放在第二点上。</p>
<p>模块更改是我经常碰到的一个难题。与引入模块相比,开发者维护和更改模块的方式对保证项目的可维护性和可拓展性是同等重要甚至是更加重要。我看过一个写得很好、抽象得很好的模块随着时间推移历经多次更改后被彻底毁了。我自己也经常是造成那种破坏性更改的其中一个。</p>
<p>当我说破坏性,我指的是对可维护性和可扩展性方面的破坏。我也明白,当面临项目最后交付期限的压力时,放慢速度以进行更好的修改设计并不是优先选择。</p>
<p>开发者做出非最优修改的原因可能有很多种,我在这里想特别强调一个:</p>
<h3>以可维护的方式进行修改的技巧</h3>
<p>这种方法让你的修改显得更专业。</p>
<p>让我们从一个 <code>API</code> 模块的代码示例开始。之所以选择这个示例,是因为与外部 <code>API</code> 通信是我在开始项目时定义的最基本的抽象之一。这里的想法是将所有与 <code>API</code> 相关的配置和设置(如基本 <code>URL</code>,错误处理逻辑等)存储在这个模块中.</p>
<p>我将编写一个设置 <code>API.url</code>、一个私有方法 <code>API._handleError()</code> 和一个公共方法 <code>API.get()</code>:</p>
<pre><code class="js">class API {
constructor() {
this.url = 'http://whatever.api/v1/';
}
/**
* API 数据获取的特有方法
* 检查一个 HTTP 返回的状态码是否在成功的范围内
*/
_handleError(_res) {
return _res.ok ? _res : Promise.reject(_res.statusText);
}
/**
* 获取数据
* @return {Promise}
*/
get(_endpoint) {
return window.fetch(this.url + _endpoint, { method: 'GET' })
.then(this._handleError)
.then( res => res.json())
.catch( error => {
alert('So sad. There was an error.');
throw new Error(error);
});
}
};</code></pre>
<p>在这个模块中,公共方法 <code>API.get()</code> 返回一个 <code>Promise</code>。我们使用我们抽象出来的 <code>API</code>模块,而不是通过 <code>window.fetch()</code> 直接调用 <code>Fetch API</code> 。例如,获取用户信息 <code>API.get('user')</code>或当前天气预报 <code>API.get('weather')</code>。实现这个功能的重要意义在于<strong>Fetch API与我们的代码没有紧密耦合。</strong></p>
<p>现在,我们面临一个修改!技术主管要求我们把获取远程数据的方式切换到<a href="https://link.segmentfault.com/?enc=fR2NEB%2Fcw1piEMpRHbEEtg%3D%3D.YVhkqs5GfeNXbquVR8OHkDHxxflUOY55staaRU2hLlw%3D" rel="nofollow">Axios</a>上。我们该如何应对呢?</p>
<p>在我们开始讨论方法之前,我们先来总结一下什么是不变的,什么是需要修改的:</p>
<ol>
<li>
<p>更改:在公共 <code>API.get()</code> 方法中</p>
<ul>
<li>需要修改 <code>axios()</code> 的 <code>window.fetch()</code>调用;需要再次返回一个 <code>Promise</code>, 以保持接口的一致, 好在 <code>Axios</code> 是基于 <code>Promise</code> 的,太棒了!</li>
<li>服务器的响应的是 <code>JSON</code>。通过 <code>Fetch API</code> 并通过链式调用 <code>.then( res => res.json())</code> 语句来解析响应的数据。使用 <code>Axios</code>,服务器响应是在 <code>data</code> 属性中,我们不需要解析它。因此,我们需要将<code>.then</code>语句改为<code>.then(res => res.data)</code>。</li>
</ul>
</li>
<li>
<p>更改:在私有 <code>API._handleError</code> 方法中:</p>
<ul><li>在响应对象中缺少 <code>ok</code> 布尔标志,但是,还有 <code>statusText</code> 属性。我们可以通过它来串起来,如果它的值是 <code>OK</code>,那么一切将没什么问题(附注:在 <code>Fetch API</code> 中 <code>OK</code> 为 <code>true</code> 与在 <code>Axios</code> 中的 <code>statusText</code> 为 <code>OK</code> 是不一样的。但为了便于理解,为了不过于宽泛,不再引入任何高级错误处理。)</li></ul>
</li>
<li>不变之处:<code>API.url</code> 保持不变,我们会发现错误并以愉快的方式提醒他们。</li>
</ol>
<p>讲解完毕!现在让我们深入应用这些修改的实际方法。</p>
<h3>方法一:删除代码。编写代码。</h3>
<pre><code class="js">class API {
constructor() {
this.url = 'http://whatever.api/v1/'; // 一模一样的
}
_handleError(_res) {
// DELETE: return _res.ok ? _res : Promise.reject(_res.statusText);
return _res.statusText === 'OK' ? _res : Promise.reject(_res.statusText);
}
get(_endpoint) {
// DELETE: return window.fetch(this.url + _endpoint, { method: 'GET' })
return axios.get(this.url + _endpoint)
.then(this._handleError)
// DELETE: .then( res => res.json())
.then( res => res.data)
.catch( error => {
alert('So sad. There was an error.');
throw new Error(error);
});
}
};</code></pre>
<p>听起来很合理。 提交、上传、合并、完成。</p>
<p>不过,在某些情况下,这可能不是一个好主意。想象以下情景:在切换到 <code>Axios</code> 之后,你会发现有一个功能并不适用于 <a href="https://link.segmentfault.com/?enc=7ydkNTeSnMl%2FjusnCd776A%3D%3D.Mbh8IHHHk1sK9joQN3U8WKeeQxMdEtcLdEq8I4RoRRC4hbiTk0h0NrEHK6TYl2zSCvFBFSFMphCQd6m%2BNzTHBQ%3D%3D" rel="nofollow">XMLHttpRequests</a>( <code>Axios</code> 的获取数据的方法),但之前使用 <code>Fetch API</code> 的新型浏览器工作得很好。我们现在该怎么办?</p>
<p>我们的技术负责人说,让我们使用旧的 <code>API</code> 实现这个特定的用例,并继续在其他地方使用 <code>Axios</code> 。你该做什么?在源代码管理历史记录中找到旧的 <code>API</code> 模块。还原。在这里和那里添加 <code>if</code> 语句。这样听起来并不太友好。</p>
<p>必须有一个更容易,更易于维护和可扩展的方式来进行更改!那么,下面的就是。</p>
<h3>方法二:重构代码,做适配!</h3>
<p>重构的需求马上来了!让我们重新开始,我们不再删除代码,而是让我们在另一个抽象中移动 <code>Fetch</code> 的特定逻辑,这将作为所有 <code>Fetch</code> 特定的适配器(或包装器)。</p>
<blockquote><p>HEY!???对于那些熟悉适配器模式(也被称为包装模式)的人来说,是的,那正是我们前进的方向!如果您对所有的细节感兴趣,请参阅这里我的介绍。</p></blockquote>
<p>如下所示:<br><img src="/img/remote/1460000012338483?w=1000&h=328" alt="" title=""></p>
<h4>步骤1</h4>
<p>将跟 <code>Fetch</code> 相关的几行代码拿出来,单独抽象为一个新的方法 <code>FetchAdapter</code>。</p>
<pre><code class="js">class FetchAdapter {
_handleError(_res) {
return _res.ok ? _res : Promise.reject(_res.statusText);
}
get(_endpoint) {
return window.fetch(_endpoint, { method: 'GET' })
.then(this._handleError)
.then( res => res.json());
}
};</code></pre>
<h4>步骤2</h4>
<p>重构API模块,删除 <code>Fetch</code> 相关代码,其余代码保持不变。添加 <code>FetchAdapter</code> 作为依赖(以某种方式):</p>
<pre><code class="js">class API {
constructor(_adapter = new FetchAdapter()) {
this.adapter = _adapter;
this.url = 'http://whatever.api/v1/';
}
get(_endpoint) {
return this.adapter.get(_endpoint)
.catch( error => {
alert('So sad. There was an error.');
throw new Error(error);
});
}
};</code></pre>
<p>现在情况不一样了!这种结构能让你处理各种不同的获取数据的场景(适配器)改。最后一步,你猜对了!写一个 <code>AxiosAdapter</code>!</p>
<pre><code class="js">const AxiosAdapter = {
_handleError(_res) {
return _res.statusText === 'OK' ? _res : Promise.reject(_res.statusText);
},
get(_endpoint) {
return axios.get(_endpoint)
then(this._handleError)
.then( res => res.data);
}
};</code></pre>
<p>在 <code>API</code> 模块中,将默认适配器改为 <code>AxiosAdapter</code>:</p>
<pre><code class="js">class API {
constructor(_adapter = new /*FetchAdapter()*/ AxiosAdapter()) {
this.adapter = _adapter;
/* ... */
}
/* ... */
};</code></pre>
<p>真棒!如果我们需要在这个特定的用例中使用旧的 <code>API</code> 实现,并且在其他地方继续使用<code>Axios</code>?没问题!</p>
<pre><code class="js">//不管你喜欢与否,将其导入你的模块,因为这只是一个例子。
import API from './API';
import FetchAdapter from './FetchAdapter';
//使用 AxiosAdapter(默认的)
const API = new API();
API.get('user');
// 使用FetchAdapter
const legacyAPI = new API(new FetchAdapter());
legacyAPI.get('user');</code></pre>
<p>所以下次你需要改变你的项目时,评估下面哪种方法更有意义:</p>
<ul>
<li>删除代码,编写代码。</li>
<li>重构代码,写适配器。</li>
</ul>
<p><strong>总结</strong>请根据你的场景选择性使用。如果你的代码库滥用适配器和引入太多的抽象可能会导致复杂性增加,这也是不好的。</p>
<p>愉快的去使用适配器吧!</p>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote>
<p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=WPf17QjXPIIY%2B%2BIx3iOmNA%3D%3D.feWMoWZWLXyh0946hKFEvboz2pr%2FriUi%2BTofj%2FQ3hQI%3D" rel="nofollow">https://www.ikcamp.com</a><br>访问官网更快阅读全部免费分享课程:<br>《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》<br>《iKcamp出品|基于Koa2搭建Node.js实战项目教程》<br>包含:文章、视频、源代码<br><img src="/img/remote/1460000012338484?w=860&h=860" alt="" title=""></p>
</blockquote>
<p><img src="/img/remote/1460000010887896" alt="" title=""></p>
PWA之Workbox缓存策略分析
https://segmentfault.com/a/1190000012326428
2017-12-07T15:18:56+08:00
2017-12-07T15:18:56+08:00
iKcamp
https://segmentfault.com/u/ikcamp
6
<blockquote>
<p>作者:陈达孚</p>
<blockquote><p>香港中文大学研究生,《移动Web前端高效开发实战》作者之一,《前端开发者指南2017》译者之一,在中国前端开发者大会,中生代技术大会等技术会议发表过主题演讲, 专注于新技术的调研和使用.</p></blockquote>
<p>本文为原创文章,转载请注明作者及出处</p>
</blockquote>
<h2>PWA之Workbox缓存策略分析</h2>
<blockquote><p>本文主要分析通过workbox(基于1.x和2.x版本,未来3.x版本会有新的结构)生成Service-Worker的缓存策略,workbox是GoogleChrome团队对原来sw-precache和sw-toolbox的封装,并且提供了Webpack和Gulp插件方便开发者快速生成sw.js文件。</p></blockquote>
<h3>precache(预缓存)</h3>
<p>首先看一下 workbox 提供的 Webpack 插件 workboxPlugin 的三个最主要参数:</p>
<ul>
<li>globDirectory</li>
<li>staticFileGlobs</li>
<li>swDest</li>
</ul>
<p>其中 <code>globDirectory</code> 和 <code>staticFileGlobs</code> 会决定需要缓存的静态文件,这两个参数也存在默认值,插件会从compilation参数中获取开发者在 Webpack 配置的 <code>output.path</code> 作为 globDirectory 的默认值,<code>staticFileGlobs</code> 的默认配置是 html,js,css 文件,如果需要缓存一些界面必须的图片,这个地方需要自己配置。</p>
<p>之后 Webpack 插件会将配置作为参数传递给 workbox-build 模块,workbox-build 模块中会根据 globDirectory 和 staticFileGlobs 读取文件生成一份配置信息,交给 precache 处理。需要注意的是,precache里不要存太多的文件,workbox-build 对文件会有一个过滤, 该模块会读取利用 node 的 fs 模块读取文件,如果文件大于2M则不会加入配置中(可以通过配置 maximumFileSize 修改),同时会根据文件的 buffer 生成一个 hash 值,也就是说就算开发者不改变文件名,只要文件内容修改了,也会生成一个新的配置内容,让浏览器更新缓存。</p>
<p>那么说了那么多,precache 到底干了什么,看一下生成的sw文件:</p>
<pre><code class="javascript">const fileManifest = [
{
'url': 'main.js',
'revision': '0e438282dc400829497725a6931f66e3'
},
{
'url': 'main.css',
'revision': '02ba19bb320adb687e08dded3e71408d'
}
];
const workboxSW = new self.WorkboxSW();
workboxSW.precache(fileManifest);</code></pre>
<p>那还是需要看一下 precache 的代码:</p>
<pre><code class="javascript">precache(revisionedFiles) {
this._revisionedCacheManager.addToCacheList({
revisionedFiles,
})
}</code></pre>
<p>是的,workbox会提供一个对象 <code>revisionedCacheManager</code> 来管理所有的缓存,先不管里面具体怎么处理的,往下看有个 <code>registerInstallActivateEvents</code>。</p>
<pre><code class="javascript">_registerInstallActivateEvents(skipWating, clientsClaim) {
self.addEventListener('install', (event) => {
const cachedUrls = this._revisionedCacheManager.getCachedUrls();
event.waitUntil(
this._revisionedCacheManager.install().then(() => {
if (skipWaiting) {
return self.skipWaiting();
}
})
)
}</code></pre>
<p>这里可以看出,所有的 precache 都会在 service worker 的 install 事件中完成。<code>event.waitUntil</code> 会根据内部promise的结果来确定安装是否完成。如果安装失败,则会舍弃这个ServiceWorker。</p>
<p>现在看一下 <code>_revisionedCacheManager.install</code> 里干了什么,首先 <code>revisionedFiles</code> 会被放在一个 Map 中,当然这个 <code>revisionedFiles</code> 是已经被处理过了, 在经过 <code>addToCacheList</code> -><code> _addEntries</code> -> <code>_parseEntry</code> 的过程后,会返回:</p>
<pre><code class="javascript">{
entryID,
revision,
request: new Request(url),
cacheBust
}</code></pre>
<p>entryID 不主动传入可以视为用户传入的url,将用来作为IndexDB中的key存储revision,而request则用来提供给之后的fetch请求,cacheBust默认为true,功能等会再分析。</p>
<p>Map 的set 过程在 <code>_addEntries</code> 的 <code>_addEntryToInstallList</code> 函数中,这里只需注意因为 fileManifest 中不能存放具有相同 url (或者说entryID)的值,不然会被警告。</p>
<p>现在回来看install,install是一个async函数,返回一个包含一系列Promise请求的Promise.all,符合waitUntil的要求。每一个需要缓存的文件会到 cacheEntry 函数中处理:</p>
<pre><code class="javascript">async _cacheEntry(precacheEntry) {
const isCached = await this._isAlreadyCached(precacheEntry);
const precacheDetails = {
url: precacheEntry.request.url,
revision: precacheEntry.revision,
wasUpdated: !isCached,
};
if (isCached) {
return precacheDetails;
}
try {
await this._requestWrapper.fetchAndCache({
request: precacheEntry.getNetworkRequest(),
waitOnCache: true,
cacheKey: precacheEntry.request,
cleanRedirects: true,
});
await this._onEntryCached(precacheEntry);
return precacheDetails;
} catch (err) {
throw new WorkboxError('request-not-cached', {
url: precacheEntry.request.url,
error: err,
});
}
}</code></pre>
<p>对于每一个请求会去通过 <code>_isAlreadyCached</code> 方法访问indexDB 得知是否被缓存过。这里可能有读者会疑惑,我们不是不能在 fileManifest 中不允许存储同样的url,为什么还要查是否缓存过,这是因为当你sw文件更新后,原来的缓存还是存在的,它们或许持有相同的url,如果它们的revision也相同,就不用获取了。</p>
<p>在 _cacheEntry 内部,还有两个异步操作,一个是通过包装后的 <code>requestWrapper</code> 的 <code>fetchAndCache</code> 请求并缓存数据,一个是通过 <code> _onEntryCached</code> 方法更新indexDB,可以看到虽然catch了错误,但依旧会throw出来,意味着任何一个precache的文件请求失败,都会终止此次install。</p>
<p>这里另一个需要注意的地方是 <code>_requestWrapper.fetchAndCache</code>,所有请求最后都会在 <code>requestWrapper</code>中处理,这里调用的实例方法是 <code>fetchAndCache</code> ,说明这次请求会涉及到网络请求和缓存处理两部分。在发出请求后,首先会判断请求结果是否需要加入缓存中:</p>
<pre><code class="js">const effectiveCacheableResponsePlugin =
this._userSpecifiedCachableResponsePlugin ||
cacheResponsePlugin ||
this.getDefaultCacheableResponsePlugin();</code></pre>
<p>如果没有插件配置,会使用 <code>getDefaultCacheableResponsePlugin() </code>来取得默认配置,即缓存返回状态为200的请求。</p>
<p>在上面的代码中可以看到在 precache 环境下,会有两个参数为 true, 一个是 waitOnCache,另一个是cleanRedirects。waitOnCache保证在需要缓存的情况下返回网络结果时必须完成缓存的处理,cleanRedirects则会重新包装一下请求重定向的结果。</p>
<p>最后用_onEntryCached把缓存的路径凭证信息存在indexDB中。</p>
<p>在activate阶段,会对precache在cache里的内容进行clean,因为前面只做了更新,如果是新的precache没有的资源地址,在这里会删除。</p>
<p>所以 precache 就是在 service-worker 的 install 事件下完成一次对配置资源的网络请求,并在请求结果返回时完成对结果的缓存。</p>
<h3>runtimecache(运行时缓存)</h3>
<p>在了解 runtimecache 前,先看下 workbox-sw 的实例化过程中比较重要的部分:</p>
<pre><code class="js">this._runtimeCacheName = getDefaultCacheName({cacheId});
this._revisionedCacheManager = new RevisionedCacheManager({
cacheId,
plugins,
});
this._strategies = new Strategies({
cacheId,
});
this._router = new Router(
this._revisionedCacheManager.getCacheName(),
handleFetch
);
this._registerInstallActivateEvents(skipWaiting, clientsClaim);
this._registerDefaultRoutes(ignoreUrlParametersMatching, directoryIndex);</code></pre>
<p>所以看出 workbox-sw 实例化的过程主要有生成缓存对应空间名,缓存空间,挂载缓存策略,挂载路由方法(用于处理对应路径的缓存策略),注册安装激活方法,注册默认路由。</p>
<p>precache 对应的就是 runtimecache,runtimecache 顾名思义就是处理所有运行时的缓存,runtimecache 往往应对着各种类型的资源,对于不同类型的资源往往也有不同的缓存策略,所以在 workbox 中使用 runtimecache 需要调用方法,<code>workbox.router.registerRoute</code> 也是说明 runtimecache 需要路由层面的细致划分。</p>
<p>看到最后一步的 <code>_registerDefaultRoutes </code>,看一下其中的代码,可以发现 workbox 有一个最基本的cache,这个 cache 其实处理的就是前面的 precache,这个 cache 遵从着 cacheFirst 原则:</p>
<pre><code class="js">const cacheFirstHandler = this.strategies.cacheFirst({
cacheName: this._revisionedCacheManager.getCacheName(),
plugins,
excludeCacheId: true,
});
const capture = ({url}) => {
url.hash = '';
const cachedUrls = this._revisionedCacheManager.getCachedUrls();
if (cachedUrls.indexOf(url.href) !== -1) {
return true;
}
let strippedUrl =
this._removeIgnoreUrlParams(url.href, ignoreUrlParametersMatching);
if (cachedUrls.indexOf(strippedUrl.href) !== -1) {
return true;
}
if (directoryIndex && strippedUrl.pathname.endsWith('/')) {
strippedUrl.pathname += directoryIndex;
return cachedUrls.indexOf(strippedUrl.href) !== -1;
}
return false;
};
this._precacheRouter.registerRoute(capture, cacheFirstHandler);</code></pre>
<p>简单的说,如果你一个路径能直接在 precache 中可以找到,或者在去除了部分查询参数后符合,或者去处部分查询参数添加后缀后符合,就会直接返回缓存,至于请求过来怎么处理的,稍后再看。</p>
<p>我们可以这么认为 precache 就是添加了 cache,至于真实请求时如何处理还是和 runtimecache 在一个地方处理,现在看来,在 workbox 初始化的时候就有了第一个 <code>router.registerRoute()</code>,之后的就需要手动注册了。</p>
<p>在写自己注册的策略之前,考虑下,注册了 route 后,又怎么处理呢?在实例化 Router 的时候,我们就会添加一个 <code>self.addEventListener('fetch', (event) => {...})</code>,除非你手动传入一个handleFetch参数为false。</p>
<p>在注册路由的时候,<code>registerRoute(capture, handler, method)</code>在类中接受一个捕获条件和一个句柄函数,这个捕获条件可以是字符串,正则表达式或者是直接的Route对象,当然最终都会变成 Route 对象(分别通过 ExpressRoute 和 RegExpRoute),Route对象包含匹配,处理方法,和方法(默认为 GET)。然后在注册时会使用一个 Map,以每个使用到的方法为 Key,值为包含所有Route对象的数组,在遍历时也只会遍历相应方法的值。所以你也可以给不同的方法定义同样的捕获路径。</p>
<p>这里使用了 unshift 操作,所以每个新的配置会被压入堆栈的顶部,在遍历时则会被优先遍历到。因为 workbox 实例化是在 registerRoute 之前,所以默认配置优先级最低,配置后面的注册会优先于前面的。</p>
<p>所以最终在页面上,你的每次请求都会被监听,到相应的请求方法数组里找有没有匹配的,如果没有匹配的话,也可以使用 <code>setDefaultHandler</code>,<code>setDefaultHandler</code>不是前面的 <code>_registerDefaultRoutes</code>,它需要开发者自己定义,并决定策略,如果定义了,所有没被匹配的请求就会被这个策略处理。请求还支持设置在,在请求被匹配却没有正确被方法处理情况下的错误处理,最终 event 会用处理方法(策略)处理这个请求,否则就正常请求。这些请求就是 workbox下的 runtimecache。</p>
<h3>缓存策略</h3>
<p>现在来看看 Workbox 提供的缓存策略,主要有这几种:<code>cache-first</code>,<code>cache-only</code>,<code>network-first</code>,<code>network-only</code>,<code>stale-while-revalidate</code>。</p>
<p>在前面看到,实例化的时候会给 workbox 挂载一个 Strategies 的实例。提供上面一系列的缓存策略,但在实际调用中,使用的是 <code>_getCachingMechanism</code>,然后把整个策略类放到一参中,二参则提供了配置项,在每个策略类中都有 handle 方法的实现,最终也会调用 handle方法。那既然如此还搞个 <code>_getCachingMechanism</code>干嘛,直接返回策略类就得了,这个等下看。</p>
<p>先看下各个策略,这里就简单说下,可以参考<a href="https://link.segmentfault.com/?enc=o94zmSkqubg4r9Bbeb%2B4hw%3D%3D.XhTrLvQGPH7u4KBGTvfupQHXNpzYtW4aW96s8jF3kDqcFg75G1xpGuZmYp0mmzPKr2KONd7dM7p4iwPTw4A%2FYIfvo1T4TLywNoAxEAI33taJcqpBjeCrK8oprct%2B5Uxx" rel="nofollow">离线指南</a>,虽然会有一点不一样。</p>
<p>第一个 Cache-First, 它的 handle 方法:</p>
<pre><code class="js">const cachedResponse = await this.requestWrapper.match({
request: event.request,
});
return cachedResponse || await this.requestWrapper.fetchAndCache({
request: event.request,
waitOnCache: this.waitOnCache,
});</code></pre>
<p>Cache-First策略会在有缓存的时候返回缓存,没有缓存才会去请求并且把请求结果缓存,这也是我们对于precache的策略。</p>
<p>然后是 Cache-only,它只会去缓存里拿数据,没有就失败了。</p>
<p>network-first 是一个比较复杂的策略,它接受 networkTimeoutSeconds 参数,如果没有传这个参数,请求将会发出,成功的话就返回结果添加到缓存中,如果失败则返回立即缓存。这种网络回退到缓存的方式虽然利于那些频繁更新的资源,但是在网络情况比较差的情况(无网会直接返回缓存)下,等待会比较久,这时候 networkTimeoutSeconds 就提供了作用,如果设置了,会生成一个setTimeout后被resolve的缓存调用,再把它和请求放倒一个 Promise.race 中,那么请求超时后就会返回缓存。</p>
<p>network-only,也比较简单,只请求,不读写缓存。</p>
<p>最后提供的策略是 StaleWhileRevalidate,这种策略比较接近 cache-first,代码如下:</p>
<pre><code class="js">const fetchAndCacheResponse = this.requestWrapper.fetchAndCache({
request: event.request,
waitOnCache: this.waitOnCache,
cacheResponsePlugin: this._cacheablePlugin,
}).catch(() => Response.error());
const cachedResponse = await this.requestWrapper.match({
request: event.request,
});
return cachedResponse || await fetchAndCacheResponse;
</code></pre>
<p>他们的区别在于就算有缓存,它仍然会发出请求,请求的结果会用来更新缓存,也就是说你的下一次访问的如果时间足够请求返回的话,你就能拿到最新的数据了。</p>
<p>可以看到离线指南中还提供了缓存然后访问网络再更新页面的方法,但这种需要配合主进程代码的修改,WorkBox 没有提供这种模式。</p>
<h3>自定义缓存配置</h3>
<p>回到在缓存策略里提到的,讲讲 <code>_getCachingMechanism</code>和缓存策略的参数。默认支持5个参数:'cacheExpiration', 'broadcastCacheUpdate', 'cacheableResponse', 'cacheName', 'plugins',(当然你会发现还有几个参数不在这里处理,比如你可以传一个自定义的 requestWrapper, 前面提到的 waitOnCache 和 NetworkFirst 支持的 networkTimeoutSeconds),先看一个完整的示例:</p>
<pre><code class="js">const workboxSW = new WorkboxSW();
const cacheFirstStrategy = workboxSW.strategies.cacheFirst({
cacheName: 'example-cache',
cacheExpiration: {
maxEntries: 10,
maxAgeSeconds: 7 * 24 * 60 * 60
},
broadcastCacheUpdate: {
channelName: 'example-channel-name'
},
cacheableResponse: {
stses: [0, 200, 404],
headers: {
'Example-Header-1': 'Header-Value-1',
'Example-Header-2': 'Header-Value-2'
}
}
plugins: [
// Additional Plugins
]
});</code></pre>
<p>大致可以认定的是 cacheExpiration 会用来处理缓存失效,cacheName 决定了 cache 的索引名,cacheableResponse 则决定了什么请求返回可以被缓存。</p>
<p>那么插件到底是怎么被处理,现在可以看<code>_getCachingMechanism</code>函数了,<code>_getCachingMechanism</code>函数处理了什么,它其实就是把 <code>cacheExpiration</code>,<code>broadcastCacheUpdate</code>,<code>cacheabelResponse</code>里的参数找到对应方法,传入参数实例化,然后挂在在封装后的wrapperOptions的plugins参数里,但是只是实例化了有什么用呢?这里有关键的一步:</p>
<pre><code class="js">options.requestWrapper = new RequestWrapper(wrapperOptions);</code></pre>
<p>所以最终这些插件还是会在 RequestWrapper 里处理,这里的一些操作是我们之前没有提到的,来看下 RequestWrapper 里怎么处理的。</p>
<p>看下 RequestWrapper 的构造函数,取其中涉及到 plugins 的部分:</p>
<pre><code class="js">constructor({cacheName, cacheId, plugins, fetchOptions, matchOptions} = {}) {
this.plugins = new Map();
if (plugins) {
isArrayOfType({plugins}, 'object');
plugins.forEach((plugin) => {
for (let callbackName of pluginCallbacks) {
if (typeof plugin[callbackName] === 'function') {
if (!this.plugins.has(callbackName)) {
this.plugins.set(callbackName, []);
} else if (callbackName === 'cacheWillUpdate') {
throw ErrorFactory.createError(
'multiple-cache-will-update-plugins');
} else if (callbackName === 'cachedResponseWillBeUsed') {
throw ErrorFactory.createError(
'multiple-cached-response-will-be-used-plugins');
}
this.plugins.get(callbackName).push(plugin);
}
}
});
}
}</code></pre>
<p>plugins是一个Map,默认支持以下几种Key:<code>cacheDidUpdate</code>, <code>cacheWillUpdate</code>, <code>fetchDidFail</code>, <code>requestWillFetch</code>, <code>cachedResponseWillBeUsed</code>。可以理解为 requestWrapper 提供了一些hooks或者生命周期,而插件就是在 hook 上进行一些处理。</p>
<p>这里举个缓存失效的例子看看怎么处理:</p>
<p>首先我们需要实例化CacheExpirationPlugin,CacheExpirationPlugin没有构造函数,实例化的是CacheExpiration,然后在this上添加maxEntries,maxAgeSeconds。所有的 hook 方法实现都放在了 CacheExpirationPlugin,提供了两个 hook: cachedResponseWillBeUsed 和 cacheDidUpdate,cachedResponseWillBeUsed 会在 RequestWrapper的match中执行,cacheDidUpdate 在 fetchAndCache中 执行。</p>
<p>这里可以看出,每个plugin其实就是对hook或者生命周期调用的具体实现,在把response扔到cache里之后,调用了插件的cacheDidUpdate方法,看下CacheExpirationPlugin中的cacheDidUpdate:</p>
<pre><code class="js">async cacheDidUpdate({cacheName, newResponse, url, now} = {}) {
isType({cacheName}, 'string');
isInstance({newResponse}, Response);
if (typeof now === 'undefined') {
now = Date.now();
}
await this.updateTimestamp({cacheName, url, now});
await this.expireEntries({cacheName, now});
}</code></pre>
<p>那么关键就是更新时间戳和失效条数,如果设置了更新时间戳会怎么样呢,在请求的时候,runtimecache也会添加到IndexedDB,值存入的是一个对象,包含了一个url和时间戳。</p>
<p>这个时间戳怎么生效,CacheExpirationPlugin提供了另外一个方法,cachedResponseWillBeUsed:</p>
<pre><code class="js">cachedResponseWillBeUsed({cachedResponse, cachedResponse, now} = {}) {
if (this.isResponseFresh({cachedResponse, now})) {
return cachedResponse;
}
return null;
}</code></pre>
<p>RequestWrapper中的match方法会默认从cache里取,取到的是当时的完整 response, 在cache的 response 里的 headers 里取到 date,然后把当时的date加上 maxAgeSecond 和 现在的时间比, 如果小于了就返回 false,那么自然会去发起请求了。</p>
<p>CacheableResponsePlugin用来控制 fetchAndCache 里的 cacheable,它设置了一个 cacheWillUpdate,可以设置哪些 http status 或者 headers 的 response 要缓存,做到更精细的缓存操作。</p>
<h3>如何配置我的缓存</h3>
<p>离线指南已经提供了一些缓存方式,在 workbox 中,可以大致认为,有一些资源会直接影响整个应用的框架能否显示的(开发应用的 JS,CSS 和部分图片)可以做 precache,这些资源一般不存在“异步”的加载,它们如果不显示整个页面无法正常加载。</p>
<p>那他们的更新策略也很简单,一般这些资源的更新需要发版,而在这里用更新sw文件更新。</p>
<p>对于大部分无状态(注意无状态)数据请求,推荐StaleWhileRevalidate方式或者缓存回退,在某些后端数据变化比较快的情况下,添加失效时间也是可以的,对于其它(业务图片)需求,cache-first比较适用。</p>
<p>最后需要讨论的是页面和有状态的请求,页面是一个比较复杂的情况,页面如果是纯静态的,那么可以放入precache。但要注意,如果我们的页面不是打包工具生成的,页面文件很可能不在dist目录下,那么怎么追踪变化呢,这里推荐一种方式,我们的页面往往有一个模版,和一个json串配置hash变量,那么你可以添加这种模式:</p>
<pre><code class="js">templatedUrls: {
path: [
'.../xxx.html',
'.../xxx.json'
]
}</code></pre>
<p>如果没有json,就需要关联所有可能影响生成页面的数据了,那么这些文件的变化都会改变最后生成的sw文件。</p>
<p>如果你在页面上有一些动态信息(比如用户信息等等),那就比较麻烦了,推荐使用 network-first 配合一个合适的失败时间,毕竟大家都不希望用户登录了另一个账号,显示的还是上一个账号,这同样适用于那些使用cookie(有状态)的请求,这些请求也推荐你添加失效策略,和失败状态。</p>
<p>永远记住你的目标,让用户能够更快的看到页面,但不要给用户一个错误的页面。</p>
<h3>总结</h3>
<p>在目前的网络环境下,service worker 的推送服务并不能得到很好的利用,所以使用 service worker 很大程度就是利用其强大的缓存能力给用户在弱网和无网环境的优化,甚至可以通过判断网络环境进行一些预下载,丰富页面的交互。但是一个错误的缓存策略可能会使用户得不到最新的内容,每一个致力于使用 service worker 或者 PWA 的开发者都需要了解其缓存的处理。Google 提供了一系列的工具能够快速生成优质的sw文件,但是配套文档过分简单和无本地化让这些配置如同一个黑盒,使开发者很难确定正确的配置方案。希望能够阅读本文,解决读者这方面的困惑。</p>
<p><img src="/img/remote/1460000012326433?w=1304&h=734" alt="" title=""></p>
你不知道的前端SDK开发技巧
https://segmentfault.com/a/1190000012307844
2017-12-06T14:33:23+08:00
2017-12-06T14:33:23+08:00
iKcamp
https://segmentfault.com/u/ikcamp
9
<blockquote>
<p>作者:陈达孚</p>
<blockquote><p>香港中文大学研究生,《移动Web前端高效开发实战》作者之一,《前端开发者指南2017》译者之一,在中国前端开发者大会,中生代技术大会等技术会议发表过主题演讲, 专注于新技术的调研和使用.</p></blockquote>
<p>本文为原创文章,转载请注明作者及出处</p>
</blockquote>
<p>最近在做公司内部的一个的一个SDK的重构,这里总结一些经验分享给大家。</p>
<h3>类型检查和智能提示</h3>
<p>作为一个SDK,我们的目标是让使用者能够减少查看文档的时间,所以我们需要提供一些类型的检查和智能提示,一般我们的做法是提供JsDoc,大部分编辑器可以提供快捷生成JsDoc的方式,我们比较常用的vscode可以使用<a href="https://link.segmentfault.com/?enc=3Zq9vvfJ1XUm5bBxtznKWg%3D%3D.Jirrohcup36iHxGp8Fes0Nl3bCDA341iQ6%2Fsu9K2U9BgzAcmY5qxF%2B6PdVVXlwxLRTZitSam1qdJyC94EEzHWQtz7hnV2qej8Ix%2FHvfii7E%3D" rel="nofollow">Document This</a>。</p>
<p><img src="/img/remote/1460000012307849?w=1422&h=1078" alt="" title=""></p>
<p>另一种做法是使用Flow或者TypeScript,选择TypeScript的主要原因是自动生成的JsDoc比较原始,我们仍然需要在上面进行编辑,所以JsDoc维护和代码开发是脱离的,往往会出现代码更新了,JsDoc忘记更新的情况。</p>
<p>除此之外开发过程中我们无法享受到类型检查等对SDK开发比较重要的特性,TypeScript可以让我们减少犯错,减少调试的时间,另一方面这次开发的SDK在提供出去的时候就会进行一次相对简单的压缩,保证引入后的体积,所以会希望压缩掉JsDoc,而TypeScript可以通过在tsconfig.json中将declaration设置为true单独的d.ts文件。</p>
<p>一个带提示的SDK:</p>
<p><img src="/img/remote/1460000012307850" alt="" title=""></p>
<p>最后,对于开发同学来说,就算不使用TypeScript,也强烈建议使用vscode提供<code>//@ts-check</code> 注解,它会通过一些类型推导来检查你的代码的正确性,可以减少很多开发过程中的bug。</p>
<p>还有一个小技巧,如果你使用的库没有提供智能提示,你可以通过<code>NPM/yarn</code>的<code>-D</code>安装<code>@types/{pkgname}</code>,这样你开发过程中就能够享受到vscode提供的智能提示,而<code>-D</code>安装到<code>devDependencies</code>中,也不会增加你在构建时的代码体积。</p>
<h3>接口</h3>
<p>既然提到了TypeScript,就提一下TypeScript的语法,基础类型没有必要赘述,而一些曾经的高级语法现在ES6也都能支持,这里提几点常用但是JavaScript开发者不太习惯使用的语法。</p>
<p>很多人在开始使用TypeScript的时候,会很迷恋使用any或者默认的any,推荐在开发中打开tsconfig中的strict和noImplicitAny来保证尽量少的any使用,要知道,滥用any就等于你的类型检查并没有实质效果。</p>
<p>对一些暂时不能确定内容的对象的类型,可以使用<code>{[key: string]: any}</code>,而不要直接使用any,后期可以慢慢扩展这个接口直到完全消除any,同时TypeScript的类型支持继承,在开发过程中,可以拆解接口,利用组合继承的方式减少重复定义。</p>
<p>但是接口也会带来一个小痛点,目前vscode的智能提醒不能很好的对应到接口,当你输入到对应变量的时候,虽然会高亮,但是高亮的也只是一个定义了名字的接口。没有办法直接看到接口里定义了什么。但是当你输入了接口里面定义的key的部分时,vscode会给你完整key的提示。虽然这对开发过程中有一点不够友好,但是vscode开发团队表示这是他们故意设计的,所以在API参数上可以选择将一些必要(重要)参数用基础类型直接使用,而将一些配置放入一个定义为接口的对象中。</p>
<h3>枚举</h3>
<p>你有在代码中使用过:</p>
<pre><code class="javascript">const Platform = {
ios: 0,
android: 1
}</code></pre>
<p>那你在TypeScript中就应该使用枚举:</p>
<pre><code class="typescript">enum Platform {
ios,
android
}</code></pre>
<p>这样在函数中你就可以为某个参数设置类型为number,然后传入<code>Platform.ios</code>这样,枚举可以增加代码的维护性,它可以利用智能提示保证你输入的正确,不再会出现魔数(magic number)。相对于对象,它保证了输入的类型(你定义的对象可能某一天不再只有number类型的value),不再需要额外的类型判断。</p>
<h3>装饰器</h3>
<p>对于装饰器其实很多开发者既熟悉又陌生,在redux,mobx比较流行的现在,在代码中出现装饰器的调用已经很普遍,但是大多数开发者并没有将自己代码逻辑抽成装饰器的习惯。</p>
<p>比如在这个SDK的开发中,我们需要提供一些facade来兼容不同的平台(iOS, Android或者Web),而这个facade会通过插件的形式让开发者自己注册,SDK会维护一个注入后的对象,常规的使用方法是到了使用函数后判断环境再判断对象中有没有想有的插件,有就使用插件。</p>
<p>实际来看,插件就是一个拦截器,我们只要阻止真正的函数运行就可以,大概的逻辑是这样的:</p>
<pre><code class="typescript">export function facade(env: number) {
return function(
target: object,
name: string,
descriptor: TypedPropertyDescriptor<any>
) {
let originalMethod = descriptor.value;
let method;
return {
...descriptor,
value(...args: any[]): any {
let [arg] = args;
let { param, success, failure, polyfill } = arg; // 这部分可以自定义
if ((method = polyfill[env])) {
method.use(param, success, failure);
return;
}
originalMethod.apply(this, args);
}
};
};
}</code></pre>
<p>在SDK的开发过程中另一个常会遇到的就是很多参数的校验和再封装,我们也可以使用装饰器去完成:</p>
<pre><code class="typescript">export function snakeParam(
target: object,
name: string,
descriptor: TypedPropertyDescriptor<any>
) {
let callback = descriptor.value!;
return {
...descriptor,
value(...args: any[]): any {
let [arg, ...other] = args;
arg = convertObjectName(arg, ConvertNameMode.toSnake);
callback.apply(this, [arg, ...other]);
}
};
}÷</code></pre>
<h3>泛形</h3>
<p>泛形可以根据用户的输入决定输出,最简单的例子是</p>
<pre><code class="typescript">function identity<T>(arg: T): T {
return arg;
}</code></pre>
<p>当然它没有什么特别的意义,但是它表明了返回是根据arg的类型,在一般开发过程中,你逃不开范型的是Promise或者前面的TypedPropertyDescriptor这种内建的需要类型输入的地方,不要草率的使用any,如果你的后端返回是一个标准结构体类似:</p>
<pre><code class="typescript">export interface IRes {
status: number;
message: string;
data?: object;
}</code></pre>
<p>那么你可以这样使用Promise:</p>
<pre><code class="typescript">function example(): Promise<IRes> {
return new Promise ...
}</code></pre>
<p>当然泛形有很多高级应用,例如泛形约束,泛型创建工厂函数,已经超出了本文的范围,可以去官方文档了解。</p>
<h3>构建</h3>
<p>如果你的构建工具是Webpack,在SDK的开发中,尽量使用node方式调用(即webpack.run执行),因为SDK的构建往往会应对很多不同的参数变化,node方式相比纯配置方式可以更加灵活的调整输入输出的参数,也可以考虑使用rollup,rollup的构建代码更加面向编程方式。</p>
<p>需要注意的是,在Webpack3和rollup中构建中可以使用ES6模块化的方式构建,这样业务代码引入你的SDK后,可以通过解构引入的方式减少最终业务代码的体积,如果你只是提供了commonjs的包,那么构建工具的tree sharking是无法生效的,如果使用babel的话注意关闭module的编译。</p>
<p>另外一种减少单个包体积的方式,可以使用<a href="https://link.segmentfault.com/?enc=U7t743mDY4bb2uUWAw7p9w%3D%3D.MCQlPLzOCq%2BpCYHXX9hLre%2F%2B8ispdrM55fm%2B5E5D%2Bxs%3D" rel="nofollow">lerna</a>在一个git仓库里构建多个NPM包,比起拆仓库可以更方便的使用公共部分的代码,但是也需要注意对公共部分代码的修改不要影响到别的包。</p>
<p>其实对于大多数的SDK的来说,Webpack3和rollup使用感受是差不多的,比较常用的插件都有几乎同名的对应。不过rollup有两个优势,一个是rollup的构建更细化,rollup.rollup接受inputOptions生成bundle,还可以generate生成sourcemap,write生成output,在这个过程中我们可以做一些细致的工作。</p>
<p>第二点是rollup.rollup会返回一个promise,也就意味着我们可以使用async的方式来写构建代码,而webpack.run还是使用的回调函数,虽然开发者可以封装成promise,但是个人觉得还是rollup的写法还是更爽一点。</p>
<h3>单元测试</h3>
<p>上周我同事做了一个在线的分享,我发现很多同学都对单测很感兴趣也很疑惑,在前端开发中,对涉及UI的业务代码开发单测试比较困难的,但是对于SDK,单元测试肯定是准出的一个充要条件。当然其实我也很不喜欢写单测,因为单测往往比较枯燥,但是不写单测肯定会被老司机们“教育”的~_~。</p>
<p>一般的单测使用<a href="https://link.segmentfault.com/?enc=K%2BCdd%2Bird%2Fl9sVhYyrbWfQ%3D%3D.mzj78eDYgGyw0i7cRpGeHDzB4NchvJ2YPCmI5QFrIkap4jauNHD83RhiDJVjz2Pg" rel="nofollow">mocha</a>作为测试框架,<a href="https://link.segmentfault.com/?enc=p8VbMWG5Mb9IOAA28wf61Q%3D%3D.jR75%2BJ5TuvUMYFbMcJPwOS0xs8WcSAcjWXKsJ69OM437%2FzIVYq%2BMAH3j8YvzDsPI" rel="nofollow">expect</a>作为断言库,使用<a href="https://link.segmentfault.com/?enc=%2BTcsV5diIkefruK2ba0VdA%3D%3D.n2ss2yk5rDiAUyjk03gpsS7Uy4KZE6ev%2B%2Bf5WJB5Zumwt78Tbvw8EbJa8kOUqx9J" rel="nofollow">nyc</a>提供单测报告,一个大概的单测如下:</p>
<pre><code class="typescript">describe('xxx api test', function() { // 注意如果要用this调用mocha,不要用箭头函数
this.timeout(6000);
it('xxx', done => {
SDK.file
.chooseImage({
count: 10,
cancel: () => {
console.log('选择图片取消----');
}
})
.then(res => {
console.dir(res);
expect(res).to.be.an('object');
expect(res).to.have.keys('ids');
expect(res.ids).to.be.an('array');
expect(res.ids).to.have.length.above(0);
uploadImg(res.ids);
done();
});
});
});</code></pre>
<p>同样你可以用TypeScript写单测,当然在执行过程中,不需要再编译了,我们可以直接给mocha注册ts-node来直接执行,具体方式可以参考<a href="https://link.segmentfault.com/?enc=Gsky%2B4K5Vs%2BpxksszrBfbw%3D%3D.S6CBdgAXqHLsBbHVwqYe%2FRY87ZPTNDz2%2FCOjmuu%2FlxQztr8lTh6FYFkPnPVuwDsbFfRYUPNRpPVAJtEhUSi1DcxsfRw%2FmqEKtQjazmhGoNns9UtuvzttaXqsZjRsTWQTVEWbVSuux3JKi%2F4jowuAS1ViEsk1zlXPK3n6YHoWcxY%3D" rel="nofollow">Write tests for TypeScript projects with mocha and chai — in TypeScript!</a>。但是有一点需要提醒你,写单测的时候尽量依赖文档而不是智能提示,因为你的代码出错,可能会导致你的智能提示也是错误的,你根据错误的智能提示写的单测肯定也是。。。</p>
<p>对于网络请求的模拟可以使用<a href="https://link.segmentfault.com/?enc=zZBrLzcKYJra7tb5jj9doQ%3D%3D.kooNZ0NadTHPxNAVXeWxkyr6VPgbvSvZJWoCZzU%2FiAYxXfNzCMZDzpr22hVb933K" rel="nofollow">nock</a>这个库,需要在it之前增加一个<code>beforeEach</code>方法:</p>
<pre><code class="javascript">describe('proxy', () => {
beforeEach(() => {
nock('http://test.com')
.post('/test1')
.delay(200)
.reply(200, { // body
test1: 1,
test2: 2
}, {
'server-id': 'test' // header
});
});
it(...
}</code></pre>
<p>最后我们用一个npm script加上nyc在mocha前面,就可以获得我们的单测报告了。</p>
<p>这里我还提了几个TypeScript使用中的小tips给大家参考。</p>
<h4>tips: 如何在非发包情况下给内部库添加声明</h4>
<p>这个SDK在开发过程会依赖一个内部NPM包,为了让这个NPM支持TypeScript调用,我们有几种做法:</p>
<ul>
<li>给原包添加d.ts文件,然后发布.</li>
<li>发布@types包,需要注意的是NPM不支持<code>@types/@scope/{pkgname}</code>这种写如果是私库包,可以使用<code>@types/scope_{pkgname}</code>这种写法.</li>
<li>
<p>这次使用的标注一个文件夹存放对应的d.ts文件,这种方式适合开发中进行,如果你觉得你写的d.ts还不够完美,或者这个d.ts文件目前只有这个SDK有需要,可以这么使用,在tsconfig.json中修改:</p>
<pre><code class="json">"baseUrl": "./",
"paths": {
"*": ["/type/*"]
}</code></pre>
</li>
</ul>
<h4>tips: 如何处理resolve和reject不同类型的promise回调</h4>
<p>默认的reject返回的参数类型是any,不一定能满足我们的需要,这里给一个解决方案,并非最佳,作为抛砖引玉:</p>
<pre><code class="typescript">interface IPromise<T, U> {
then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: U) => TResult2 | PromiseLike<TResult2>)
| undefined
| null
): IPromise<TResult1 , TResult2>;
catch<TResult = never>(
onrejected?:
| ((reason: U) => TResult | PromiseLike<TResult>)
| undefined
| null
): Promise<TResult>;</code></pre>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote>
<p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=82jmOlkM5eYJbKqKm3EYsQ%3D%3D.ilTn%2FZ29W0RtbQTFVrVjRHQr2YqAtPEnWS0p2Ft5Zzc%3D" rel="nofollow">https://www.ikcamp.com</a></p>
</blockquote>
<p>包含:文章、视频、源代码<br><img src="/img/remote/1460000010887896" alt="" title=""></p>
翻译连载 | 附录 B: 谦虚的 Monad-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
https://segmentfault.com/a/1190000012303323
2017-12-06T10:21:01+08:00
2017-12-06T10:21:01+08:00
iKcamp
https://segmentfault.com/u/ikcamp
2
<ul>
<li>原文地址:<a href="https://link.segmentfault.com/?enc=WZOm3zHRpfpKdUGI98TGPw%3D%3D.EsEj5YiFDP9OZElkXoycGykPPE19ZYC%2FdZjCSHvLJ%2Fbm9WrGMK%2BDgEk%2F5GOF9MEF" rel="nofollow">Functional-Light-JS</a>
</li>
<li>原文作者:<a href="https://link.segmentfault.com/?enc=H6PQ7qhoZ9f0LsXdwXNqfQ%3D%3D.C4Gmca8bhhfy5rwrJ8zuoRYl%2BXxop8jf3K%2B8uUDBXCM%3D" rel="nofollow">Kyle Simpson-《You-Dont-Know-JS》作者</a>
</li>
</ul>
<blockquote>
<p>关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。</p>
<p>译者团队(排名不分先后):<a href="https://link.segmentfault.com/?enc=TNP6xGUKIl4KWp6RpQQGlw%3D%3D.JZHwaI8VFI977P%2Fkpfp2zexDaRp8q7jSTJv34NEcSoY%3D" rel="nofollow">阿希</a>、<a href="https://link.segmentfault.com/?enc=%2Fqqq7VUA0lKNxqjDlLKBfg%3D%3D.XotOwHIpkS%2BJBXF4syVdY1A%2FyMQGnlP6E1eMEdtyNa8%3D" rel="nofollow">blueken</a>、<a href="https://link.segmentfault.com/?enc=lIBUVsj58kGJqalBn09uPQ%3D%3D.ciFiqdiEozzC38EBG7NCdJYj8d1DWNrRcOj7qBusCw4%3D" rel="nofollow">brucecham</a>、<a href="https://link.segmentfault.com/?enc=Qi35JoGzUU0vkAmNbptagA%3D%3D.%2B%2FztVOB%2Ff638%2B0s0W8LY4pDFZXGDO5H%2FuX88Vu7LNb0%3D" rel="nofollow">cfanlife</a>、<a href="https://link.segmentfault.com/?enc=Mk9TRgVt4SC8s7DI%2B92qqA%3D%3D.KkVtsPrm%2BLAyc4rkm%2BIR6tpdrKUHioJn%2FUdbfPFAXX8%3D" rel="nofollow">dail</a>、<a href="https://link.segmentfault.com/?enc=jat1b8TLOQcCLrsNEwYcEQ%3D%3D.GbtLoW8OGPR6sH11Gef%2FRQXKkaJxkygmGHY3Qag7SEc%3D" rel="nofollow">kyoko-df</a>、<a href="https://link.segmentfault.com/?enc=kPOHZ8IqY%2B6OI3iZ0O4a%2Fg%3D%3D.zQLwAwpV8SXfBAbJuTny1AfygORzinOVaLNF%2F17E8%2Fk%3D" rel="nofollow">l3ve</a>、<a href="https://link.segmentfault.com/?enc=SuZnq%2B%2BvXxySmxc%2BFTEtbA%3D%3D.2HWdW8DfaThS8K6NKUt1%2BTOKNRQpQpSGgCNzuc4S3YU%3D" rel="nofollow">lilins</a>、<a href="https://link.segmentfault.com/?enc=cEOVKioJ5T01KLVapBu8MQ%3D%3D.aGOQbC9ZtXMjxupbFUfcqqov5Y4UmAi6CVa4tywxGRXzJ%2B2l0k%2B4abWo%2BPM4MYeZ" rel="nofollow">LittlePineapple</a>、<a href="https://link.segmentfault.com/?enc=6QvgaP%2BHbaV0MtZzdKon8w%3D%3D.sIgaCmXkMwXkBagyk6LOMFz%2BVGfdGCCYPCDfxTuBVpQ%3D" rel="nofollow">MatildaJin</a>、<a href="https://link.segmentfault.com/?enc=Qm67ydrcDXGWGhEDEqe0sA%3D%3D.ewO57sxMhRc7DyAzN3Ci32rX2bj%2Bk0Uvfj2VUGZaY74%3D" rel="nofollow">冬青</a>、<a href="https://link.segmentfault.com/?enc=vVhxpxRXUelEGQb00Rhlig%3D%3D.HpYzG%2FnSeWDOCsBid7xeTch0yF4Gwgky3fjc%2BVRUlYk%3D" rel="nofollow">pobusama</a>、<a href="https://link.segmentfault.com/?enc=COyaAVFw3FUXMlTyRvP9Hw%3D%3D.vbWpdAWxFneRyssRfVPVU3fr4e%2F6sqXo9X1QXTs8lToTCalgWBzKf2m8skSGtLV5" rel="nofollow">Cherry</a>、<a href="https://link.segmentfault.com/?enc=dqDwnOUoQGOHqhoMlD9C7Q%3D%3D.xt7SCbjEx%2BBizb3k%2BXEBMui%2FagwcTpB4C%2FNTwUKhxpE%3D" rel="nofollow">萝卜</a>、<a href="https://link.segmentfault.com/?enc=vno1KyAYDpqYPntqYZkVIA%3D%3D.upv%2F7r6f8gB47lt%2Fa0q%2FYB6QCUTkALfuPsqjcKEl4PU%3D" rel="nofollow">vavd317</a>、<a href="https://link.segmentfault.com/?enc=n9LdSiXdPjmmPbBy7I%2FWJw%3D%3D.8FoB%2Bl9XSYoQ%2FMUReRo3Reft5oSKvlm22PzniQuRYMc%3D" rel="nofollow">vivaxy</a>、<a href="https://link.segmentfault.com/?enc=ty5D%2BJyCt3w9I7CCMSvGLw%3D%3D.K7pKQzeuB%2B9DlpBDps%2Bjb%2FJcoyBjmr%2BAFkHYrs4xI%2Fo%3D" rel="nofollow">萌萌</a>、<a href="https://link.segmentfault.com/?enc=sxMYM2NvTN1DlvwoYOtA5A%3D%3D.4PHtSbMjcU3Rs5vZ7wdobxPtS9J2dhLy3gXI9vkejY8%3D" rel="nofollow">zhouyao</a></p>
</blockquote>
<h2>JavaScript 轻量级函数式编程</h2>
<h2>附录 B: 谦虚的 Monad</h2>
<p>首先,我坦白:在开始写以下内容之前我并不太了解 Monad 是什么。我为了确认一些事情而犯了很多错误。如果你不相信我,去看看 <a href="https://link.segmentfault.com/?enc=r92k8p%2Bm2y2AhnKvZUjypw%3D%3D.x8cwHtvKazzWdR1YQfKpaj1%2Ft3iXwJIfL4UViUPEiV5RgfPIjJGYTM4c8W9JByI3" rel="nofollow">这本书 Git 仓库</a> 中关于本章的提交历史吧!</p>
<p>我在本书中囊括了所有涉及 Monad 的话题。就像我写书的过程一样,每个开发者在学习函数式编程的旅程中都会经历这个部分。</p>
<p>尽管其他函数式编程的著作差不多都把 Monad 作为开始,而我们却只对它做了简要说明,并基本以此结束本书。在轻量级函数式编程中我确实没有遇到太多需要仔细考虑 Monad 的问题,这就是本文更有价值的原因。但是并不是说 Monad 是没用的或者是不普遍的 —— 恰恰相反,它很有用,也很流行。</p>
<p>函数式编程界有一个小笑话,几乎每个人都不得不在他们的文章或者博客里写 Monad 是什么,把它拎出来写就像是一个仪式。在过去的几年里,人们把 Monad 描述为卷饼、洋葱和各种各样古怪的抽象概念。我肯定不会重蹈覆辙!</p>
<blockquote><p>一个 Monad 仅仅是自函子 (endofunctor) 范畴中的一个 monoid</p></blockquote>
<p>我们引用这句话来开场,所以把话题转到这个引言上面似乎是很合适的。可是才不会这样,我们不会讨论 Monad 、endofunctor 或者范畴论。这句引言不仅故弄玄虚而且华而不实。</p>
<p>我只希望通过我们的讨论,你不再害怕 Monad 这个术语或者这个概念了 —— 我曾经怕了很长一段时间 —— 并在看到该术语时知道它是什么。你可能,也只是可能,会正确地使用到它们。</p>
<h3>类型</h3>
<p>在函数式编程中有一个巨大的兴趣领域:类型论,本书基本上完全远离了该领域。我不会深入到类型论,坦白的说,我没有深入的能力,即使干了也吃力不讨好。</p>
<p>但是我要说,Monad 基本上是一个值类型。</p>
<p>数字 <code>42</code> 有一个值类型(number),它带有我们依赖的特征和功能。字符串 <code>"42"</code> 可能看起来很像,但是在编程里它有不同的用途。</p>
<p>在面向对象编程中,当你有一组数据(甚至是一个单独的离散值),并且想要给它绑上一些行为,那么你将创建一个对象或者类来表示 "type"。接着实例就成了该类型的一员。这种做法通常被称为 “数据结构”。</p>
<p>我将会非常宽泛的使用数据结构这个概念,而且我断定,当我们在编程中为一个特定的值定义一组行为以及约束条件,并且将这些特征与值一起绑定在一个单一抽象概念上时,我们可能会觉得很有用。这样,当我们在编程中使用一个或多个这种值的时候,它们的行为会自然的出现,并且会使它们更方便的工作。方便的是,对你的代码的读者来说,是更有描述性和声明性的。</p>
<p>Monad 是一种数据结构。是一种类型。它是一组使处理某个值变得可预测的特定行为。</p>
<p>回顾第 8 章,我们谈到了函子(functor):包括一个值和一个用来对构成函子的数据执行操作的类 map 实用函数。Monad 是一个包含一些额外行为的函子(functor)。</p>
<h3>松散接口</h3>
<p>实际上,Monad 并不是单一的数据类型,它更像是相关联的数据类型集合。它是一种根据不同值的需要而用不同方式实现的接口。每种实现都是一种不同类型的 Monad。</p>
<p>例如,你可能阅读 "Identity Monad"、"IO Monad"、"Maybe Monad"、"Either Monad" 或其他形形色色的字眼。他们中的每一个都有基本的 Monad 行为定义,但是它根据每个不同类型的 Monad 用例来继承或者重写交互行为。</p>
<p>可是它不仅仅是一个接口,因为它不只是使对象成为 Monad 的某些 API 方法的实现。对这些方法的交互的保障是必须的,是 monadic 的。这些众所周知的常量对于使用 Monad 提高可读性是至关重要的;另外,它是一个特殊的数据结构,读者必须全部阅读才能明白。</p>
<p>事实上,这些 Monad 方法的名字和真实接口授权的方式甚至没有一个统一的标准;Monad 更像是一个松散接口。有些人称这些方法为 <code>bind(..)</code>,有些称它为 <code>chain(..)</code>,还有些称它为 <code>flatMap(..)</code>,等等。</p>
<p>所以,Monad 是一个对象数据结构,并且有充足的方法(几乎任何名称或排序),至少满足了 Monad 定义的主要行为需求。每一种 Monad 都基于最少数量的方法来进行不同的扩展。但是,因为它们在行为上都有重叠,所以一起使用两种不同的 Monad 仍然是直截了当和可控的。</p>
<p>从某种意义上说,Monad 更像是接口。</p>
<h3>Maybe</h3>
<p>在函数式编程中,像 Maybe 这样涵盖 Monad 是很普遍的。事实上,Maybe Monad 是另外两个更简单的 Monad 的搭配:Just 和 Nothing。</p>
<p>既然 Monad 是一个类型,你可能认为我们应该定义 <code>Maybe</code> 作为一个要被实例化的类。这虽然是一种有效的方法,但是它引入了 <code>this</code> 绑定的问题,所以在这里我不想讨论;相反,我打算使用一个简单的函数和对象的实现方式。</p>
<p>以下是 Maybe 的最简单的实现:</p>
<pre><code class="js">var Maybe = { Just, Nothing, of/* 又称:unit,pure */: Just };
function Just(val) {
return { map, chain, ap, inspect };
// *********************
function map(fn) { return Just( fn( val ) ); }
// 又称:bind, flatMap
function chain(fn) { return fn( val ); }
function ap(anotherMonad) { return anotherMonad.map( val ); }
function inspect() {
return `Just(${ val })`;
}
}
function Nothing() {
return { map: Nothing, chain: Nothing, ap: Nothing, inspect };
// *********************
function inspect() {
return "Nothing";
}
}</code></pre>
<p><strong>注意:</strong> <code>inspect(..)</code> 方法只用于我们的示例中。从 Monad 的角度来说,它并没有任何意义。</p>
<p>如果现在大部分都没有意义的话,不要担心。我们将会更专注的说明我们可以用它做什么,而不是过多的深入 Monad 背后的设计细节和理论。</p>
<p>所有的 Monad 一样,任何含有 <code>Just(..)</code> 和 <code>Nothing()</code> 的 Monad 实例都有 <code>map(..)</code>、<code>chain(..)</code>(也叫 <code>bind(..)</code> 或者 <code>flatMap(..)</code>)和 <code>ap(..)</code> 方法。这些方法及其行为的目的在于提供多个 Monad 实例一起工作的标准化方法。你将会注意到,无论 <code>Just(..)</code> 实例拿到的是怎样的一个 <code>val</code> 值, <code>Just(..)</code> 实例都不会去改变它。所有的方法都会创建一个新的 Monad 实例而不是改变它。</p>
<p>Maybe 是这两个 Monad 的结合。如果一个值是非空的,它是 <code>Just(..)</code> 的实例;如果该值是空的,它则是 <code>Nothing()</code> 的实例。注意,这里由你的代码来决定 "空" 的意思,我们不做强制限制。下一节会详细介绍这一点。</p>
<p>但是 Monad 的价值在于不论我们有 <code>Just(..)</code> 实例还是 <code>Nothing()</code> 实例,我们使用的方法都是一样的。<code>Nothing()</code> 实例对所有的方法都有空操作定义。所以如果 Monad 实例出现在 Monad 操作中,它就会对 Monad 操作起短路(short-circuiting)作用。</p>
<p>Maybe 这个抽象概念的作用是隐式地封装了操作和无操作的二元性。</p>
<h4>与众不同的 Maybe</h4>
<p>JavaScript Maybe Monad 的许多实现都包含 <code>null</code> 和 <code>undefined</code> 的检查(通常在 <code>map(..)</code>中),如果是空的话,就跳过该 Monad 的特性行为。事实上,Maybe 被声称是有价值的,因为它自动地封装了空值检查得以在某种程度上短路了它的特性行为。</p>
<p>这是 Maybe 的典型说明:</p>
<pre><code class="js">// 代替不稳定的 `console.log( someObj.something.else.entirely )`:
Maybe.of( someObj )
.map( prop( "something" ) )
.map( prop( "else" ) )
.map( prop( "entirely" ) )
.map( console.log );</code></pre>
<p>换句话说,如果我们在链式操作中的任何一环得到一个 <code>null</code> 或者 <code>undefined</code> 值,Maybe 会智能的切换到空操作模式 —— 它现在是一个 <code>Nothing()</code> Monad 实例! —— 把剩余的链式操作都停止掉。如果一些属性丢失或者是空的话,嵌套的属性访问能安全的抛出 JS 异常。这是非常酷的而且很实用。</p>
<p>但是,我们这样实现的 Maybe 不是一个纯 Monad。</p>
<p>Monad 的核心思想是,它必须对所有的值都是有效的,不能对值做任何检查 —— 甚至是空值检查。所以为了方便,这些其他的实现都是走的捷径。这是无关紧要的。但是当学习一些东西的时候,你应该先学习它的最纯粹的形式,然后再学习更复杂的规则。</p>
<p>我早期提供的 Maybe Monad 的实现不同于其他的 Maybe,就是它没有空置检查。另外,我们将 <code>Maybe</code> 作为 <code>Just(..)</code> 和 <code>Nothing()</code> 的非严格意义上的结合。</p>
<p>等一下,如果我们没有自动短路,那 Maybe 是怎么起作用的呢?!?这似乎就是它的全部意义。</p>
<p>不要担心,我们可以从外部提供简单的空值检查,Maybe Monad 其他的短路行为也还是可以很好的工作的。你可以在之前做一些 <code>someObj.something.else.entirely</code> 属性嵌套,但是我们可以做的更 “正确”:</p>
<pre><code class="js">function isEmpty(val) {
return val === null || val === undefined;
}
var safeProp = curry( function safeProp(prop,obj){
if (isEmpty( obj[prop] )) return Maybe.Nothing();
return Maybe.of( obj[prop] );
} );
Maybe.of( someObj )
.chain( safeProp( "something" ) )
.chain( safeProp( "else" ) )
.chain( safeProp( "entirely" ) )
.map( console.log );</code></pre>
<p>我们设计了一个用于空值检查的 <code>safeProp(..)</code> 函数,并选择了 <code>Nothing()</code> Monad 实例。或者把值包装在 <code>Just(..)</code> 实例中(通过 <code>Maybe.of(..)</code>)。然后我们用 <code>chain(..)</code> 替代 <code>map(..)</code>,它知道如何 “展开” <code>safeProp(..)</code> 返回的 Monad。</p>
<p>当遇到空值的时候,我们得到了一连串相同的短路。只是我们把这个逻辑从 Maybe 中排除了。</p>
<p>不管返回哪种类型的 Monad,我们的 <code>map(..)</code> 和 <code>chain(..)</code> 方法都有不变且可预测的反馈,这就是 Monad,尤其是 Maybe Monad 的好处。这难道不酷吗?</p>
<h3>Humble</h3>
<p>现在我们对 Maybe 和它的作用有了更多的了解,我将会在它上面加一些小的改动 —— 我将通过设计 Maybe + Humble Monad 来添加一些转折并且加一些诙谐的元素。从技术上来说,<code>Humble(..)</code> 并不是一个 Monad,而是一个产生 Maybe Monad 实例的工厂函数。</p>
<p>Humble 是一个使用 Maybe 来跟踪 <code>egoLevel</code> 数字状态的数据结构包装器。具体来说,<code>Humble(..)</code> 只有在他们自身的水平值足够低(少于 <code>42</code>)到被认为是 Humble 的时候才会执行生成的 Monad 实例;否则,它就是一个 <code>Nothing()</code> 空操作。这听起来真的和 Maybe 很像!</p>
<p>这是一个 Maybe + Humble Monad 工厂函数:</p>
<pre><code class="js">function Humble(egoLevel) {
// 接收任何大于等于 42 的数字
return !(Number( egoLevel ) >= 42) ?
Maybe.of( egoLevel ) :
Maybe.Nothing();
}</code></pre>
<p>你可能会注意到,这个工厂函数有点像 <code>safeProp(..)</code>,因为,它使用一个条件来决定是选择 Maybe 的 <code>Just(..)</code> 还是 <code>Nothing()</code>。</p>
<p>让我们来看一个基础用法的例子:</p>
<pre><code class="js">var bob = Humble( 45 );
var alice = Humble( 39 );
bob.inspect(); // Nothing
alice.inspect(); // Just(39)</code></pre>
<p>如果 Alice 赢得了一个大奖,现在是不是在为自己感到自豪呢?</p>
<pre><code class="js">function winAward(ego) {
return Humble( ego + 3 );
}
alice = alice.chain( winAward );
alice.inspect(); // Nothing</code></pre>
<p><code>Humble( 39 + 3 )</code> 创建了一个 <code>chain(..)</code> 返回的 <code>Nothing()</code> Monad 实例,所以现在 Alice 不再有 Humble 的资格了。</p>
<p>现在,我们来用一些 Monad :</p>
<pre><code class="js">var bob = Humble( 41 );
var alice = Humble( 39 );
var teamMembers = curry( function teamMembers(ego1,ego2){
console.log( `Our humble team's egos: ${ego1} ${ego2}` );
} );
bob.map( teamMembers ).ap( alice );
// Humble 队列:41 39</code></pre>
<p>由于 <code>teamMembers(..)</code> 是柯里化的,<code>bob.map(..)</code> 的调用传入了 <code>bob</code> 自身的级别(<code>41</code>),并且创建了一个被其余的方法包装的 Monad 实例。在 <strong>这个</strong> Monad 中调用的 <code>ap(alice)</code> 调用了 <code>alice.map(..)</code>,并且传递给来自 Monad 的函数。这样做的效果是,Monad 的值已经提供给了 <code>teamMembers(..)</code> 函数,并且把显示的结果给打印了出来。</p>
<p>然而,如果一个 Monad 或者两个 Monad 实际上是 <code>Nothing()</code> 实例(因为它们本身的水平值太高了):</p>
<pre><code class="js">var frank = Humble( 45 );
bob.map( teamMembers ).ap( frank );
frank.map( teamMembers ).ap( bob );</code></pre>
<p><code>teamMembers(..)</code> 永远不会被调用(也没有信息被打印出来),因为,<code>frank</code> 是一个 <code>Nothing()</code> 实例。这就是 Maybe monad 的作用,我们的 <code>Humble(..)</code> 工厂函数允许我们根据自身的水平来选择。赞!</p>
<h4>Humility</h4>
<p>再来一个例子来说明 Maybe + Humble 数据结构的行为:</p>
<pre><code class="js">function introduction() {
console.log( "I'm just a learner like you! :)" );
}
var egoChange = curry( function egoChange(amount,concept,egoLevel) {
console.log( `${amount > 0 ? "Learned" : "Shared"} ${concept}.` );
return Humble( egoLevel + amount );
} );
var learn = egoChange( 3 );
var learner = Humble( 35 );
learner
.chain( learn( "closures" ) )
.chain( learn( "side effects" ) )
.chain( learn( "recursion" ) )
.chain( learn( "map/reduce" ) )
.map( introduction );
// 学习闭包
// 学习副作用
// 歇息递归</code></pre>
<p>不幸的是,学习过程看起来已经缩短了。我发现学习一大堆东西而不和别人分享,会使自我太膨胀,这对你的技术是不利的。</p>
<p>让我们尝试一个更好的方法:</p>
<pre><code class="js">var share = egoChange( -2 );
learner
.chain( learn( "closures" ) )
.chain( share( "closures" ) )
.chain( learn( "side effects" ) )
.chain( share( "side effects" ) )
.chain( learn( "recursion" ) )
.chain( share( "recursion" ) )
.chain( learn( "map/reduce" ) )
.chain( share( "map/reduce" ) )
.map( introduction );
// 学习闭包
// 分享闭包
// 学习副作用
// 分享副作用
// 学习递归
// 分享递归
// 学习 map/reduce
// 分享 map/reduce
// 我只是一个像你一样的学习者 :)</code></pre>
<p>在学习中分享。是学习更多并且能够学的更好的最佳方法。</p>
<h3>总结</h3>
<p>说了这么多,那什么是 Monad ?</p>
<p>Monad 是一个值类型,一个接口,一个有封装行为的对象数据结构。</p>
<p>但是这些定义中没有一个是有用的。这里尝试做一个更好的解释:Monad 是一个用更具有声明式的方式围绕一个值来组织行为的方法。</p>
<p>和这本书中的其他部分一样,在有用的地方使用 Monad,不要因为每个人都在函数式编程中讨论他们而使用他们。Monad 不是万金油,但它确实提供了一些有用的实用函数。</p>
<p><strong>【上一章】<a href="https://link.segmentfault.com/?enc=fbiZ1NdvVvC%2Fs1YHng5IzQ%3D%3D.QniEBemGT6OMhw%2FP7%2Byy7yX%2FjVgFC1YBTduvoSjVDzk0H5vMtILqBw7BJ%2B31Bumj" rel="nofollow">翻译连载 | 附录 A:Transducing(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇</a> </strong></p>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote>
<p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=vrsCSdz3roi1K1xZiJ5w0A%3D%3D.rqG6DR1%2Bmg3NgDA7tAu9tyFP8KVeQmURIyJwtN1C%2FFc%3D" rel="nofollow">https://www.ikcamp.com</a><br>访问官网更快阅读全部免费分享课程:<br>《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》<br>《iKcamp出品|基于Koa2搭建Node.js实战项目教程》<br>包含:文章、视频、源代码<br><img src="/img/remote/1460000010887896" alt="" title=""></p>
</blockquote>
翻译连载 | 附录 A:Transducing(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
https://segmentfault.com/a/1190000012303238
2017-12-06T10:18:03+08:00
2017-12-06T10:18:03+08:00
iKcamp
https://segmentfault.com/u/ikcamp
3
<ul>
<li>原文地址:<a href="https://link.segmentfault.com/?enc=BzulEm6om5UsoJVSqb5wJg%3D%3D.Q3yho3mFsZVDNIN3xTHAlKTQfAK%2B70v1rX8TNS65az7dK%2FLKtxj8F6wN4NR8u58O" rel="nofollow">Functional-Light-JS</a>
</li>
<li>原文作者:<a href="https://link.segmentfault.com/?enc=hk1lTLQO2WGhW1%2BHvN5spw%3D%3D.zDhV9LWJwgNQes9kYvpuAXAbNiKRu6K3tsqCh54k%2FnU%3D" rel="nofollow">Kyle Simpson-《You-Dont-Know-JS》作者</a>
</li>
</ul>
<blockquote>
<p>关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。</p>
<p>译者团队(排名不分先后):<a href="https://link.segmentfault.com/?enc=5ZWBDPRoHhl9rQz0Awv1zw%3D%3D.fat1L5oID8u3iftb80dqflJyXtoyefS1%2BpdIDyIH78g%3D" rel="nofollow">阿希</a>、<a href="https://link.segmentfault.com/?enc=iS9KLkadSQdFRp%2FCt39Yzg%3D%3D.xeoOTeJKNa7V9IVrYJ86f%2FhwBbFq%2BvjFHA4yKyGK8dg%3D" rel="nofollow">blueken</a>、<a href="https://link.segmentfault.com/?enc=aXOiKLxjg%2F6JC25svC5Ujg%3D%3D.387u1T8NfJt9Wf97fEl6V5ENU9UM7zKpivMoPdw%2BNzE%3D" rel="nofollow">brucecham</a>、<a href="https://link.segmentfault.com/?enc=1kb1gKXVlSj2Nv6n45JnDw%3D%3D.nEWIYGiF13YAXdOVMUh3TSQ2y3dCJ6DQrNdIVFTUW40%3D" rel="nofollow">cfanlife</a>、<a href="https://link.segmentfault.com/?enc=k66zhIn%2FoWu6LGU%2FV7bTBw%3D%3D.V11dFeXAFwLy1Qhj2f0Pek8JGey1eeU6TVq%2BAEQnNBc%3D" rel="nofollow">dail</a>、<a href="https://link.segmentfault.com/?enc=oSqOq3COqJaHBkQx5Bbw1Q%3D%3D.wYFkl6RLPs7yZwNWXLmHCtQwUOZpkZfHiOKUFfbIILo%3D" rel="nofollow">kyoko-df</a>、<a href="https://link.segmentfault.com/?enc=7BX7qLzpIXvxzyEnAle%2FuA%3D%3D.s7WvUsx2Fq7sbTo2VF%2BTQEnlwYeaoLvSESZlqyeMJRM%3D" rel="nofollow">l3ve</a>、<a href="https://link.segmentfault.com/?enc=vVjb0a6CD9fMLecqO%2B01sg%3D%3D.HUGTR%2FAy7yLfhyIIjd%2FVu3ZRklOajJqD1wQZve7x9s4%3D" rel="nofollow">lilins</a>、<a href="https://link.segmentfault.com/?enc=h3aDbFfUSb6VRfFn3HGyFA%3D%3D.H8IWN7E1tgvwoFbOBU%2FtJW8ZluYk7bIpmZ7Lxd1JqvXJwc5F05wjpaJaXS1b6odn" rel="nofollow">LittlePineapple</a>、<a href="https://link.segmentfault.com/?enc=Vzi%2F8yuCH%2FVSYJty%2FjkG0Q%3D%3D.Ho7KntcY0AW0NSSMdVW37HMTjKCWHYyb36Lx6HXDft4%3D" rel="nofollow">MatildaJin</a>、<a href="https://link.segmentfault.com/?enc=I4xspqtgGCfKoMkYgK8Svw%3D%3D.suUtoYfwFy43GdLaKjWBQbltO%2BiLr6he2s%2BbsZVKFmA%3D" rel="nofollow">冬青</a>、<a href="https://link.segmentfault.com/?enc=beFUcjNV0hymT%2FMZFkS7tw%3D%3D.%2BQh9P3hCStEc0aWrJq2iqg%2FXnbJ4J8qpCFNRs%2Bw7qzo%3D" rel="nofollow">pobusama</a>、<a href="https://link.segmentfault.com/?enc=O5XcVO4RR6lsL8erk7NSjg%3D%3D.e%2FQi3OPJ0caaELMenU13emWA3G9BfGjPyck3td1uXNl7U02Lji997Ebsxu1O%2BM3c" rel="nofollow">Cherry</a>、<a href="https://link.segmentfault.com/?enc=%2B%2BCS6pndgj9DTq9atD%2FTyA%3D%3D.eu0iQav3rJx7%2F4sL0m0XDv7Xp0anYvIT7lBYdjoP2js%3D" rel="nofollow">萝卜</a>、<a href="https://link.segmentfault.com/?enc=%2BSHuWztYerA784pahPejjA%3D%3D.t%2BIIfQhVQFGEhKLO1oZfAjMM1mt1Y4PtNmpCJeIYrOQ%3D" rel="nofollow">vavd317</a>、<a href="https://link.segmentfault.com/?enc=4lwgMxXq%2B6W4aXFybagwFw%3D%3D.Du2QJr4ajbUaASspQ0M6anGrQ5adGXEIZTEp4K0%2BXkY%3D" rel="nofollow">vivaxy</a>、<a href="https://link.segmentfault.com/?enc=l5hXfMTUDsOIUinlYNQAkw%3D%3D.3T6gswLXXNZeU7iqkiRr6vQWpzMmwYfSjPg8sgoSDNg%3D" rel="nofollow">萌萌</a>、<a href="https://link.segmentfault.com/?enc=35Ipa7TScZT0Q4MoQxXiuA%3D%3D.6nIJYcbZQgCgsUTTF4DPm%2FYBT0dtCGTElzqBc2oYxCk%3D" rel="nofollow">zhouyao</a></p>
</blockquote>
<h2>JavaScript 轻量级函数式编程</h2>
<h2>附录 A:Transducing(下)</h2>
<h4>组合柯里化</h4>
<p>这一步是最棘手的。所以请慢慢的用心的阅读。</p>
<p>让我们看看没有将 <code>listCombination(..)</code> 传递给柯里化函数的样子:</p>
<pre><code class="js">var x = curriedMapReducer( strUppercase );
var y = curriedFilterReducer( isLongEnough );
var z = curriedFilterReducer( isShortEnough );</code></pre>
<p>看看这三个中间函数 <code>x(..)</code>, <code>y(..)</code> 和 <code>z(..)</code>。每个函数都期望得到一个单一的组合函数并产生一个 reducer 函数。</p>
<p>记住,如果我们想要所有这些的独立的 reducer,我们可以这样做:</p>
<pre><code class="js">var upperReducer = x( listCombination );
var longEnoughReducer = y( listCombination );
var shortEnoughReducer = z( listCombination );</code></pre>
<p>但是,如果你调用 <code>y(z)</code>,会得到什么呢?当把 <code>z</code> 传递给 <code>y(..)</code> 调用,而不是 <code>combinationFn(..)</code> 时会发生什么呢?这个返回的 reducer 函数内部看起来像这样:</p>
<pre><code class="js">function reducer(list,val) {
if (isLongEnough( val )) return z( list, val );
return list;
}</code></pre>
<p>看到 <code>z(..)</code> 里面的调用了吗? 这看起来应该是错误的,因为 <code>z(..)</code> 函数应该只接收一个参数(<code>combinationFn(..)</code>),而不是两个参数(list 和 val)。这和要求不匹配。不行。</p>
<p>我们来看看组合 <code>y(z(listCombination))</code>。我们将把它分成两个不同的步骤:</p>
<pre><code class="js">var shortEnoughReducer = z( listCombination );
var longAndShortEnoughReducer = y( shortEnoughReducer );</code></pre>
<p>我们创建 <code>shortEnoughReducer(..)</code>,然后将它作为 <code>combinationFn(..)</code> 传递给 <code>y(..)</code>,生成 <code>longAndShortEnoughReducer(..)</code>。多读几遍,直到理解。</p>
<p>现在想想: <code>shortEnoughReducer(..)</code> 和 <code>longAndShortEnoughReducer(..)</code> 的内部构造是什么样的呢?你能想得到吗?</p>
<pre><code class="js">// shortEnoughReducer, from z(..):
function reducer(list,val) {
if (isShortEnough( val )) return listCombination( list, val );
return list;
}
// longAndShortEnoughReducer, from y(..):
function reducer(list,val) {
if (isLongEnough( val )) return shortEnoughReducer( list, val );
return list;
}</code></pre>
<p>你看到 <code>shortEnoughReducer(..)</code> 替代了 <code>longAndShortEnoughReducer(..)</code> 里面 <code>listCombination(..)</code> 的位置了吗? 为什么这样也能运行?</p>
<p><strong>因为 <code>reducer(..)</code> 的“形状”和 <code>listCombination(..)</code> 的形状是一样的。</strong> 换句话说,reducer 可以用作另一个 reducer 的组合函数; 它们就是这样组合起来的! <code>listCombination(..)</code> 函数作为第一个 reducer 的组合函数,这个 reducer 又可以作为组合函数给下一个 reducer,以此类推。</p>
<p>我们用几个不同的值来测试我们的 <code>longAndShortEnoughReducer(..)</code> :</p>
<pre><code class="js">longAndShortEnoughReducer( [], "nope" );
// []
longAndShortEnoughReducer( [], "hello" );
// ["hello"]
longAndShortEnoughReducer( [], "hello world" );
// []</code></pre>
<p><code>longAndShortEnoughReducer(..)</code> 会过滤出不够长且不够短的值,它在同一步骤中执行这两个过滤。这是一个组合 reducer!</p>
<p>再花点时间消化下。</p>
<p>现在,把 <code>x(..)</code> (生成大写 reducer 的产生器)加入组合:</p>
<pre><code class="js">var longAndShortEnoughReducer = y( z( listCombination) );
var upperLongAndShortEnoughReducer = x( longAndShortEnoughReducer );</code></pre>
<p>正如 <code>upperLongAndShortEnoughReducer(..)</code> 名字所示,它同时执行所有三个步骤 - 一个映射和两个过滤器!它内部看起来是这样的:</p>
<pre><code class="js">// upperLongAndShortEnoughReducer:
function reducer(list,val) {
return longAndShortEnoughReducer( list, strUppercase( val ) );
}</code></pre>
<p>一个字符串类型的 <code>val</code> 被传入,由 <code>strUppercase(..)</code> 转换成大写,然后传递给 <code>longAndShortEnoughReducer(..)</code>。该函数只有在 <code>val</code> 满足足够长且足够短的条件时才将它添加到数组中。否则数组保持不变。</p>
<p>我花了几个星期来思考分析这种杂耍似的操作。所以别着急,如果你需要在这好好研究下,重新阅读个几(十几个)次。慢慢来。</p>
<p>现在来验证一下:</p>
<pre><code class="js">upperLongAndShortEnoughReducer( [], "nope" );
// []
upperLongAndShortEnoughReducer( [], "hello" );
// ["HELLO"]
upperLongAndShortEnoughReducer( [], "hello world" );
// []</code></pre>
<p>这个 reducer 成功的组合了和 map 和两个 filter,太棒了!</p>
<p>让我们回顾一下我们到目前为止所做的事情:</p>
<pre><code class="js">var x = curriedMapReducer( strUppercase );
var y = curriedFilterReducer( isLongEnough );
var z = curriedFilterReducer( isShortEnough );
var upperLongAndShortEnoughReducer = x( y( z( listCombination ) ) );
words.reduce( upperLongAndShortEnoughReducer, [] );
// ["WRITTEN","SOMETHING"]</code></pre>
<p>这已经很酷了,但是我们可以让它更好。</p>
<p><code>x(y(z( .. )))</code> 是一个组合。我们可以直接跳过中间的 <code>x</code> / <code>y</code> / <code>z</code> 变量名,直接这么表示该组合:</p>
<pre><code class="js">var composition = compose(
curriedMapReducer( strUppercase ),
curriedFilterReducer( isLongEnough ),
curriedFilterReducer( isShortEnough )
);
var upperLongAndShortEnoughReducer = composition( listCombination );
words.reduce( upperLongAndShortEnoughReducer, [] );
// ["WRITTEN","SOMETHING"]</code></pre>
<p>我们来考虑下该组合函数中“数据”的流动:</p>
<ol>
<li>
<code>listCombination(..)</code> 作为组合函数传入,构造 <code>isShortEnough(..)</code> 过滤器的 reducer。</li>
<li>然后,所得到的 reducer 函数作为组合函数传入,继续构造 <code>isShortEnough(..)</code> 过滤器的 reducer。</li>
<li>最后,所得到的 reducer 函数作为组合函数传入,构造 <code>strUppercase(..)</code> 映射的 reducer。</li>
</ol>
<p>在前面的片段中,<code>composition(..)</code> 是一个组合函数,期望组合函数来形成一个 reducer;而这个 <code>composition(..)</code> 有一个特殊的标签:transducer。给 transducer 提供组合函数产生组合的 reducer:</p>
<p>// TODO:检查 transducer 是产生 reducer 还是它本身就是 reducer</p>
<pre><code class="js">var transducer = compose(
curriedMapReducer( strUppercase ),
curriedFilterReducer( isLongEnough ),
curriedFilterReducer( isShortEnough )
);
words
.reduce( transducer( listCombination ), [] );
// ["WRITTEN","SOMETHING"]</code></pre>
<p><strong>注意</strong>:我们应该好好观察下前面两个片段中的 <code>compose(..)</code> 顺序,这地方有点难理解。回想一下,在我们的原始示例中,我们先 <code>map(strUppercase)</code> 然后 <code>filter(isLongEnough)</code> ,最后 <code>filter(isShortEnough)</code>;这些操作实际上也确实按照这个顺序执行的。但在第 4 章中,我们了解到,<code>compose(..)</code> 通常是以相反的顺序运行。那么为什么我们不需要反转这里的顺序来获得同样的期望结果呢?来自每个 reducer 的 <code>combinationFn(..)</code> 的抽象反转了操作顺序。所以和直觉相反,当组合一个 tranducer 时,你只需要按照实际的顺序组合就好!</p>
<h5>列表组合:纯与不纯</h5>
<p>我们再来看一下我们的 <code>listCombination(..)</code> 组合函数的实现:</p>
<pre><code class="js">function listCombination(list,val) {
return list.concat( [val] );
}</code></pre>
<p>虽然这种方法是纯的,但它对性能有负面影响。首先,它创建临时数组来包裹 <code>val</code>。然后,<code>concat(..)</code> 方法创建一个全新的数组来连接这个临时数组。每一步都会创建和销毁的很多数组,这不仅对 CPU 不利,也会造成 GC 内存的流失。</p>
<p>下面是性能更好但是不纯的版本:</p>
<pre><code class="js">function listCombination(list,val) {
list.push( val );
return list;
}</code></pre>
<p>单独的考虑下 <code>listCombination(..)</code> ,毫无疑问,这是不纯的,这通常是我们想要避免的。但是,我们应该考虑一个更大的背景。</p>
<p><code>listCombination(..)</code> 不是我们完全有交互的函数。我们不直接在程序中的任何地方使用它,而只是在 transducing 的过程中使用它。</p>
<p>回到第 5 章,我们定义纯函数来减少副作用的目标只是限制在应用的 API 层级。对于底层实现,只要没有违反对外部是纯函数,就可以在函数内为了性能而变得不纯。</p>
<p><code>listCombination(..)</code> 更多的是转换的内部实现细节。实际上,它通常由 transducing 库提供!而不是你的程序中进行交互的顶层方法。</p>
<p>底线:我认为甚至使用 <code>listCombination(..)</code> 的性能最优但是不纯的版本也是完全可以接受的。只要确保你用代码注释记录下它不纯即可!</p>
<h4>可选的组合</h4>
<p>到目前为止,这是我们用转换所得到的:</p>
<pre><code class="js">words
.reduce( transducer( listCombination ), [] )
.reduce( strConcat, "" );
// 写点什么</code></pre>
<p>这已经非常棒了,但是我们还藏着最后一个的技巧。坦白来说,我认为这部分能够让你迄今为止付出的所有努力变得值得。</p>
<p>我们可以用某种方式实现只用一个 <code>reduce(..)</code> 来“组合”这两个 <code>reduce(..)</code> 吗? 不幸的是,我们并不能将 <code>strConcat(..)</code> 添加到 <code>compose(..)</code> 调用中; 它的“形状”不适用于那个组合。</p>
<p>但是让我们来看下这两个功能:</p>
<pre><code class="js">function strConcat(str1,str2) { return str1 + str2; }
function listCombination(list,val) { list.push( val ); return list; }</code></pre>
<p>如果你用心观察,可以看出这两个功能是如何互换的。它们以不同的数据类型运行,但在概念上它们也是一样的:将两个值组合成一个。</p>
<p>换句话说, <code>strConcat(..)</code> 是一个组合函数!</p>
<p>这意味着如果我们的最终目标是获得字符串连接而不是数组,我们就可以用它代替 <code>listCombination(..)</code> :</p>
<pre><code class="js">words.reduce( transducer( strConcat ), "" );
// 写点什么</code></pre>
<p>Boom! 这就是 transducing。</p>
<h3>最后</h3>
<p>深吸一口气,确实有很多要消化。</p>
<p>放空我们的大脑,让我们把注意力转移到如何在我们的程序中使用转换,而不是关心它的工作原理。</p>
<p>回想起我们之前定义的辅助函数,为清楚起见,我们重新命名一下:</p>
<pre><code class="js">var transduceMap = curry( function mapReducer(mapperFn,combinationFn){
return function reducer(list,v){
return combinationFn( list, mapperFn( v ) );
};
} );
var transduceFilter = curry( function filterReducer(predicateFn,combinationFn){
return function reducer(list,v){
if (predicateFn( v )) return combinationFn( list, v );
return list;
};
} );</code></pre>
<p>还记得我们这样使用它们:</p>
<pre><code class="js">var transducer = compose(
transduceMap( strUppercase ),
transduceFilter( isLongEnough ),
transduceFilter( isShortEnough )
);</code></pre>
<p><code>transducer(..)</code> 仍然需要一个组合函数(如 <code>listCombination(..)</code> 或 <code>strConcat(..)</code>)来产生一个传递给 <code>reduce(..)</code> (连同初始值)的 transduce-reducer 函数。</p>
<p>但是为了更好的表达所有这些转换步骤,我们来做一个 <code>transduce(..)</code> 工具来为我们做这些步骤:</p>
<pre><code class="js">function transduce(transducer,combinationFn,initialValue,list) {
var reducer = transducer( combinationFn );
return list.reduce( reducer, initialValue );
}</code></pre>
<p>这是我们的运行示例,梳理如下:</p>
<pre><code class="js">var transducer = compose(
transduceMap( strUppercase ),
transduceFilter( isLongEnough ),
transduceFilter( isShortEnough )
);
transduce( transducer, listCombination, [], words );
// ["WRITTEN","SOMETHING"]
transduce( transducer, strConcat, "", words );
// 写点什么</code></pre>
<p>不错,嗯! 看到 <code>listCombination(..)</code> 和 <code>strConcat(..)</code> 函数可以互换使用组合函数了吗?</p>
<h4>Transducers.js</h4>
<p>最后,我们来说明我们运行的例子,使用sensors-js库(<a href="https://link.segmentfault.com/?enc=TqMGIRyw5i%2BDUUclVNqg2Q%3D%3D.aerW7kXdPaTGFkJOW8swIJKTGBokABVo7Wzp2vhCVdZYQzoSJog9iko4eAnuUvbqxooYeuKdOMRQ0V%2FHNEDgiQ%3D%3D" rel="nofollow">https://github.com/cognitect-...</a> ):</p>
<pre><code class="js">var transformer = transducers.comp(
transducers.map( strUppercase ),
transducers.filter( isLongEnough ),
transducers.filter( isShortEnough )
);
transducers.transduce( transformer, listCombination, [], words );
// ["WRITTEN","SOMETHING"]
transducers.transduce( transformer, strConcat, "", words );
// WRITTENSOMETHING</code></pre>
<p>看起来几乎与上述相同。</p>
<p><strong>注意:</strong> 上面的代码段使用 <code>transformers.comp(..)</code> ,因为这个库提供这个 API,但在这种情况下,我们从第 4 章的 <code>compose(..)</code> 也将产生相同的结果。换句话说,组合本身不是 transducing 敏感的操作。</p>
<p>该片段中的组合函数被称为 <code>transformer</code> ,而不是 <code>transducer</code>。那是因为如果我们直接调用 <code>transformer(listCombination)</code>(或 <code>transformer(strConcat)</code>),那么我们不会像以前那样得到一个直观的 transduce-reducer 函数。</p>
<p><code>transducers.map(..)</code> 和 <code>transducers.filter(..)</code> 是特殊的辅助函数,可以将常规的断言函数或映射函数转换成适用于产生特殊变换对象的函数(里面包含了 reducer 函数);这个库使用这些变换对象进行转换。此转换对象抽象的额外功能超出了我们将要探索的内容,请参阅该库的文档以获取更多信息。</p>
<p>由于 <code>transformer(..)</code> 产生一个变换对象,而不是一个典型的二元 transduce-reducer 函数,该库还提供 <code>toFn(..)</code> 来使变换对象适应本地数组的 <code>reduce(..)</code> 方法:</p>
<pre><code class="js">words.reduce(
transducers.toFn( transformer, strConcat ),
""
);
// WRITTENSOMETHING</code></pre>
<p><code>into(..)</code> 是另一个提供的辅助函数,它根据指定的空/初始值的类型自动选择默认的组合函数:</p>
<pre><code class="js">transducers.into( [], transformer, words );
// ["WRITTEN","SOMETHING"]
transducers.into( "", transformer, words );
// WRITTENSOMETHING</code></pre>
<p>当指定一个空数组 <code>[]</code> 时,内部的 <code>transduce(..)</code> 使用一个默认的函数实现,这个函数就像我们的 <code>listCombination(..)</code>。但是当指定一个空字符串 <code>“”</code> 时,会使用像我们的 <code>strConcat(..)</code> 这样的方法。这很酷!</p>
<p>如你所见,<code>transducers-js</code> 库使转换非常简单。我们可以非常有效地利用这种技术的力量,而不至于陷入定义所有这些中间转换器生产工具的繁琐过程中去。</p>
<h3>总结</h3>
<p>Transduce 就是通过减少来转换。更具体点,transduer 是可组合的 reducer。</p>
<p>我们使用转换来组合相邻的<code>map(..)</code>、<code>filter(..)</code> 和 <code>reduce(..)</code> 操作。我们首先将 <code>map(..)</code> 和 <code>filter(..)</code> 表示为 <code>reduce(..)</code>,然后抽象出常用的组合操作来创建一个容易组合的一致的 reducer 生成函数。</p>
<p>transducing 主要提高性能,如果在延迟序列(异步 observables)中使用,则这一点尤为明显。</p>
<p>但是更广泛地说,transducing 是我们针对那些不能被直接组合的函数,使用的一种更具声明式风格的方法。否则这些函数将不能直接组合。如果使用这个技术能像使用本书中的所有其他技术一样用的恰到好处,代码就会显得更清晰,更易读! 使用 transducer 进行单次 <code>reduce(..)</code> 调用比追踪多个 <code>reduce(..)</code> 调用更容易理解。</p>
<p><strong> 【上一章】<a href="https://link.segmentfault.com/?enc=m2k6G7xQ%2B692i%2BAZUJZY4Q%3D%3D.XoQOGn4Eq02SNUGQ8VCeCMUhLLnDeP2vZk7PiKck9FYmgyp4DwIms4hSJsVJo581" rel="nofollow">翻译连载 | 附录 A:Transducing(上)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇</a> </strong></p>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote>
<p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=UzPwliBkEG4QjTXs6qSA4Q%3D%3D.JVHC4GMdxwV4eQhn6G5vWFL4zPXIxTAA4Odtfc%2Bhv2g%3D" rel="nofollow">https://www.ikcamp.com</a><br>访问官网更快阅读全部免费分享课程:<br>《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》<br>《iKcamp出品|基于Koa2搭建Node.js实战项目教程》<br>包含:文章、视频、源代码</p>
</blockquote>
翻译连载 | 附录 A:Transducing(上)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
https://segmentfault.com/a/1190000012127329
2017-11-23T12:24:43+08:00
2017-11-23T12:24:43+08:00
iKcamp
https://segmentfault.com/u/ikcamp
8
<ul>
<li><p>原文地址:<a href="https://link.segmentfault.com/?enc=qG07Dwakox2l%2BgqsUuMA2w%3D%3D.jw%2Bkha5JQ7XfhqCHVgtAURrbrccjhXzxg7bZTSlF1MUvdCUJYOcWM%2BfMLYoYFpa8" rel="nofollow">Functional-Light-JS</a></p></li>
<li><p>原文作者:<a href="https://link.segmentfault.com/?enc=DP%2FPsG2UpOlyLGY2xudj3g%3D%3D.gG726asdBgCwYyvnZDrS7KQziepD8RqQeVnIG6I%2FTt0%3D" rel="nofollow">Kyle Simpson-《You-Dont-Know-JS》作者</a></p></li>
</ul>
<blockquote>
<p>关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。</p>
<p>译者团队(排名不分先后):<a href="https://link.segmentfault.com/?enc=kR0goXITyYHtlvuiX%2FBtpQ%3D%3D.qDQz84ZbqCBimNixGyTw%2FI%2F%2Fh2GFJwg9nLJlNwNRubE%3D" rel="nofollow">阿希</a>、<a href="https://link.segmentfault.com/?enc=b3gDlcebddQ9VXQFJNfSDA%3D%3D.Ozc6Ru0WPO4GZczrosHDCyPB9wSCoTaoge%2F47vQJ%2B2c%3D" rel="nofollow">blueken</a>、<a href="https://link.segmentfault.com/?enc=19TFVRK%2FCrMjGjYP4RDLaA%3D%3D.2l1TOKX7d6api6drMExMC%2BamEs4ulosjGYgId5M5Bdo%3D" rel="nofollow">brucecham</a>、<a href="https://link.segmentfault.com/?enc=cLgBU8e1wxBOQuORwv5MSA%3D%3D.FzOMNfkCq5OAINYmSpW6mQDd4niZvR1ChSGW%2B%2FTRrwA%3D" rel="nofollow">cfanlife</a>、<a href="https://link.segmentfault.com/?enc=Z4T11qgLND3GXTMPuLM8OA%3D%3D.%2FDyHIRW4YERBD%2BkqcdsJjsmYOoU6MzYKl7UUU9lyulc%3D" rel="nofollow">dail</a>、<a href="https://link.segmentfault.com/?enc=eHNCGNXDz8Ya4wQsgf2RwA%3D%3D.U7%2B0vZWRqiIuWLN%2Bu09PsK%2BKIrvCGM8eCUcVSlXlpZw%3D" rel="nofollow">kyoko-df</a>、<a href="https://link.segmentfault.com/?enc=AXlQP3FCfcbmxtZ8eZJbyg%3D%3D.zbFyWOg9n%2B0b3yB%2BpOq%2FPSJyz%2FR20h1XXusgztFEBKk%3D" rel="nofollow">l3ve</a>、<a href="https://link.segmentfault.com/?enc=ZnxgWbSUyr%2BgmQC2D34c6A%3D%3D.C7xwNIdzPFnc%2BJk0HZScLWW2Mp3T1XZ%2B6ZH3fIuQU4w%3D" rel="nofollow">lilins</a>、<a href="https://link.segmentfault.com/?enc=8E%2F91CADKyK62h6%2F09TqNQ%3D%3D.nCruRrPsOxeBU0eNFgyTZa4Uykcn6daQhB8pIUMoR7pmqKELh4myiq5O2Qv2ZJaN" rel="nofollow">LittlePineapple</a>、<a href="https://link.segmentfault.com/?enc=wfh0T3zKnUN9RXsQxn7Ygg%3D%3D.CYZDowWa4QTEA4FtujBCQZ0xtirex7bvlVPmjkI9sTA%3D" rel="nofollow">MatildaJin</a>、<a href="https://link.segmentfault.com/?enc=LOurhHVJbnYsUPKZ%2FQ%2B8Bg%3D%3D.3a7XJUU0Or%2BwDOeeUzYJ766v8SNFINvCyxpGJYjo%2BfI%3D" rel="nofollow">冬青</a>、<a href="https://link.segmentfault.com/?enc=%2FFNHTCDsNO24wLHFfQudzg%3D%3D.epe5dbSMUAYwr%2Bde0ldg6gXR8n2XMMMFfMsFeqw7PfQ%3D" rel="nofollow">pobusama</a>、<a href="https://link.segmentfault.com/?enc=qDvuu4wJ60fmnpyqFnvc3w%3D%3D.VkyG3H57%2BbQHP6uNiJi%2FlBTIe7Anbsli4gHxfDB8R0j7PDKbYENDbXM6Ao3KtcOC" rel="nofollow">Cherry</a>、<a href="https://link.segmentfault.com/?enc=WQtQC9%2BGTV4DGShLxq6y%2Bw%3D%3D.jEHHX9%2BeumK4%2FWd%2Fbrrmdgjlm%2Fmx4L%2FtXANGU%2FTmECM%3D" rel="nofollow">萝卜</a>、<a href="https://link.segmentfault.com/?enc=mjDBhOLM24ap6NlaRN0%2FZQ%3D%3D.rNGuJ4FiZUJo0DTAolrbGgWzsktughBgdCCg8nrVvfg%3D" rel="nofollow">vavd317</a>、<a href="https://link.segmentfault.com/?enc=KPvS9nV2I%2F1lI8%2Fys9e1XA%3D%3D.z3oBpG72lhtz2bYX0qZImQTrFvF4QZOyPUr%2F9BcnrmE%3D" rel="nofollow">vivaxy</a>、<a href="https://link.segmentfault.com/?enc=b%2BjfRpALuap0hMRLb4HiFA%3D%3D.yAE9pafkZT2KHVDcJfBR2pszawaBAlDJzIL5L79j0rQ%3D" rel="nofollow">萌萌</a>、<a href="https://link.segmentfault.com/?enc=Orio5Um%2FiV81Y%2FKcCt3bmA%3D%3D.vYfN9B4UKGQ4CHai%2FAbTESVWS%2FZTx5AreHFN%2BkbmFho%3D" rel="nofollow">zhouyao</a></p>
</blockquote>
<h2>JavaScript 轻量级函数式编程</h2>
<h2>附录 A:Transducing</h2>
<p>Transducing 是我们这本书要讲到的更为高级的技术。它继承了第 8 章数组操作的许多思想。</p>
<p>我不会把 Transducing 严格的称为“轻量级函数式编程”,它更像是一个顶级的技巧。我把这个技术留到附录来讲意味着你现在很可能并不需要关心它,当你确保你已经非常熟悉整本书的主要内容,你可以再回头看看这一章节。</p>
<p>说实话,即使我已经教过 transducing 很多次了,在写这一章的时候,我仍然需要花很多脑力去理清楚这个技术。所以,如果你看这一章看的很疑惑也没必要感到沮丧。把这一章加个书签,等你觉得你差不多能理解时再回头看看。</p>
<p>Transducing 就是通过减少来转换。</p>
<p>我知道这听起来很令人费解。但是让我们来看看它有多强大。实际上,我认为这是你掌握了轻量级函数式编程后可以做的最好的例证之一。</p>
<p>和这本书的其他部分一样,我的方法是先解释<strong>为什么</strong>使用这个技术,然后<strong>如何</strong>使用,最后归结为简单的这个技术到底是什么样的。这通常会有多学很多东西,但是我觉得用这种方式你会更深入的理解它。</p>
<h3>首先,为什么</h3>
<p>让我们从扩展我们在第 3 章中介绍的例子开始,测试单词是否足够短和/或足够长:</p>
<pre><code class="js">function isLongEnough(str) {
return str.length >= 5;
}
function isShortEnough(str) {
return str.length <= 10;
}</code></pre>
<p>在第 3 章中,我们使用这些断言函数来测试一个单词。然后在第 8 章中,我们学习了如何使用像 <code>filter(..)</code> 这样的数组操作来重复这些测试。例如:</p>
<pre><code class="js">var words = [ "You", "have", "written", "something", "very", "interesting" ];
words
.filter( isLongEnough )
.filter( isShortEnough );
// ["written","something"]</code></pre>
<p>这个例子可能并不明显,但是这种分开操作相同数组的方式具有一些不理想的地方。当我们处理一个值比较少的数组时一切都还好。但是如果数组中有很多值,每个 <code>filter(..)</code> 分别处理数组的每个值会比我们预期的慢一点。</p>
<p>当我们的数组是异步/懒惰(也称为 observables)的,随着时间的推移响应事件处理(见第 10 章),会出现类似的性能问题。在这种情况下,一次事件只有一个值,因此使用两个单独的 <code>filter(..)</code> 函数处理这些值并不是什么大不了的事情。</p>
<p>但是,不太明显的是每个 <code>filter(..)</code> 方法都会产生一个单独的 observable 值。从一个 observable 值中抽出一个值的开销真的可以加起来(译者注:详情请看第 10 章的“积极的 vs 惰性的”这一节)。这是真实存在的,因为在这些情况下,处理数千或数百万的值并不罕见; 所以,即使是这么小的成本也会很快累加起来。</p>
<p>另一个缺点是可读性,特别是当我们需要对多个数组(或 observable)重复相同的操作时。例如:</p>
<pre><code class="js">zip(
list1.filter( isLongEnough ).filter( isShortEnough ),
list2.filter( isLongEnough ).filter( isShortEnough ),
list3.filter( isLongEnough ).filter( isShortEnough )
)</code></pre>
<p>显得很重复,对不对?</p>
<p>如果我们可以将 <code>isLongEnough(..)</code> 断言与 <code>isShortEnough(..)</code> 断言组合在一起是不是会更好一点呢(可读性和性能)?你可以手动执行:</p>
<pre><code class="js">function isCorrectLength(str) {
return isLongEnough( str ) && isShortEnough( str );
}</code></pre>
<p>但这不是函数式编程的方式!</p>
<p>在第 8 章中,我们讨论了融合 —— 组合相邻映射函数。回忆一下:</p>
<pre><code class="js">words
.map(
pipe( removeInvalidChars, upper, elide )
);</code></pre>
<p>不幸的是,组合相邻断言函数并不像组合相邻映射函数那样容易。为什么呢?想想断言函数长什么“样子” —— 一种描述输入和输出的学术方式。它接收一个单一的参数,返回一个 true 或 false。</p>
<p>如果你试着用 <code>isshortenough(islongenough(str))</code>,这是行不通的。因为 <code>islongenough(..) </code> 会返回 true 或者 false ,而不是返回 <code>isshortenough(..)</code> 所要的字符串类型的值。这可真倒霉。</p>
<p>试图组合两个相邻的 reducer 函数同样是行不通的。reducer 函数接收两个值作为输入,并返回单个组合值。reducer 函数的单一返回值也不能作为参数传到另一个需要两个输入的 reducer 函数中。</p>
<p>此外,<code>reduce(..)</code> 辅助函数可以接收一个可选的 <code>initialValue</code> 输入。有时可以省略,但有时候它又必须被传入。这就让组合更复杂了,因为一个 <code>reduce(..)</code> 可能需要一个 <code>initialValue</code>,而另一个 <code>reduce(..)</code> 可能需要另一个 <code>initialValue</code>。所以我们怎么可能只用某种组合的 reducer 来实现 <code>reduce(..)</code> 呢。</p>
<p>考虑像这样的链:</p>
<pre><code class="js">words
.map( strUppercase )
.filter( isLongEnough )
.filter( isShortEnough )
.reduce( strConcat, "" );
// "WRITTENSOMETHING"</code></pre>
<p>你能想出一个组合能够包含 <code>map(strUppercase)</code>, <code>filter(isLongEnough)</code>,<code>filter(isShortEnough)</code>, <code>reduce(strConcat)</code> 所有这些操作吗?每种操作的行为是不同的,所以不能直接组合在一起。我们需要把它们修改下让它们组合在一起。</p>
<p>希望这些例子说明了为什么简单的组合不能胜任这项任务。我们需要一个更强大的技术,而 transducing 就是这个技术。</p>
<h3>如何,下一步</h3>
<p>让我们谈谈我们该如何得到一个能组合映射,断言和/或 reducers 的框架。</p>
<p>别太紧张:你不必经历编程过程中所有的探索步骤。一旦你理解了 transducing 能解决的问题,你就可以直接使用函数式编程库中的 <code>transduce(..)</code> 工具继续你应用程序的剩余部分!</p>
<p>让我们开始探索吧。</p>
<h4>把 Map/Filter 表示为 Reduce</h4>
<p>我们要做的第一件事情就是将我们的 <code>filter(..)</code> 和 <code>map(..)</code>调用变为 <code>reduce(..)</code> 调用。回想一下我们在第 8 章是怎么做的:</p>
<pre><code class="js">function strUppercase(str) { return str.toUpperCase(); }
function strConcat(str1,str2) { return str1 + str2; }
function strUppercaseReducer(list,str) {
list.push( strUppercase( str ) );
return list;
}
function isLongEnoughReducer(list,str) {
if (isLongEnough( str )) list.push( str );
return list;
}
function isShortEnoughReducer(list,str) {
if (isShortEnough( str )) list.push( str );
return list;
}
words
.reduce( strUppercaseReducer, [] )
.reduce( isLongEnoughReducer, [] )
.reduce( isShortEnough, [] )
.reduce( strConcat, "" );
// "WRITTENSOMETHING"</code></pre>
<p>这是一个不错的改进。我们现在有四个相邻的 <code>reduce(..)</code> 调用,而不是三种不同方法的混合。然而,我们仍然不能 <code>compose(..)</code> 这四个 reducer,因为它们接受两个参数而不是一个参数。</p>
<p>在 8 章,我们偷了点懒使用了数组的 <code>push</code> 方法而不是 <code>concat(..)</code> 方法返回一个新数组,导致有副作用。现在让我们更正式一点:</p>
<pre><code class="js">function strUppercaseReducer(list,str) {
return list.concat( [strUppercase( str )] );
}
function isLongEnoughReducer(list,str) {
if (isLongEnough( str )) return list.concat( [str] );
return list;
}
function isShortEnoughReducer(list,str) {
if (isShortEnough( str )) return list.concat( [str] );
return list;
}</code></pre>
<p>在后面我们会来头看看这里是否需要 <code>concat(..)</code>。</p>
<h4>参数化 Reducers</h4>
<p>除了使用不同的断言函数之外,两个 filter reducers 几乎相同。让我们把这些 reducers 参数化得到一个可以定义任何 filter-reducer 的工具函数:</p>
<pre><code class="js">function filterReducer(predicateFn) {
return function reducer(list,val){
if (predicateFn( val )) return list.concat( [val] );
return list;
};
}
var isLongEnoughReducer = filterReducer( isLongEnough );
var isShortEnoughReducer = filterReducer( isShortEnough );</code></pre>
<p>同样的,我们把 <code>mapperFn(..)</code> 也参数化来生成 map-reducer 函数:</p>
<pre><code class="js">function mapReducer(mapperFn) {
return function reducer(list,val){
return list.concat( [mapperFn( val )] );
};
}
var strToUppercaseReducer = mapReducer( strUppercase );</code></pre>
<p>我们的调用链看起来是一样的:</p>
<pre><code class="js">words
.reduce( strUppercaseReducer, [] )
.reduce( isLongEnoughReducer, [] )
.reduce( isShortEnough, [] )
.reduce( strConcat, "" );</code></pre>
<h4>提取共用组合逻辑</h4>
<p>仔细观察上面的 <code>mapReducer(..)</code> 和 <code>filterReducer(..)</code> 函数。你发现共享功能了吗?</p>
<p>这部分:</p>
<pre><code class="js">return list.concat( .. );
// 或者
return list;</code></pre>
<p>让我们为这个通用逻辑定义一个辅助函数。但是我们叫它什么呢?</p>
<pre><code class="js">function WHATSITCALLED(list,val) {
return list.concat( [val] );
}</code></pre>
<p><code>WHATSITCALLED(..)</code> 函数做了些什么呢,它接收两个参数(一个数组和另一个值),将值 concat 到数组的末尾返回一个新的数组。所以这个 <code>WHATSITCALLED(..)</code> 名字不合适,我们可以叫它 <code>listCombination(..)</code> :</p>
<pre><code class="js">function listCombination(list,val) {
return list.concat( [val] );
}</code></pre>
<p>我们现在用 <code>listCombination(..)</code> 来重新定义我们的 reducer 辅助函数:</p>
<pre><code class="js">function mapReducer(mapperFn) {
return function reducer(list,val){
return listCombination( list, mapperFn( val ) );
};
}
function filterReducer(predicateFn) {
return function reducer(list,val){
if (predicateFn( val )) return listCombination( list, val );
return list;
};
}</code></pre>
<p>我们的调用链看起来还是一样的(这里就不重复写了)。</p>
<h4>参数化组合</h4>
<p>我们的 <code>listCombination(..)</code> 小工具只是组合两个值的一种方式。让我们将它的用途参数化,以使我们的 reducers 更加通用:</p>
<pre><code class="js">function mapReducer(mapperFn,combinationFn) {
return function reducer(list,val){
return combinationFn( list, mapperFn( val ) );
};
}
function filterReducer(predicateFn,combinationFn) {
return function reducer(list,val){
if (predicateFn( val )) return combinationFn( list, val );
return list;
};
}</code></pre>
<p>使用这种形式的辅助函数:</p>
<pre><code class="js">var strToUppercaseReducer = mapReducer( strUppercase, listCombination );
var isLongEnoughReducer = filterReducer( isLongEnough, listCombination );
var isShortEnoughReducer = filterReducer( isShortEnough, listCombination );</code></pre>
<p>将这些实用函数定义为接收两个参数而不是一个参数不太方便组合,因此我们使用我们的 <code>curry(..)</code> (柯里化)方法:</p>
<pre><code class="js">var curriedMapReducer = curry( function mapReducer(mapperFn,combinationFn){
return function reducer(list,val){
return combinationFn( list, mapperFn( val ) );
};
} );
var curriedFilterReducer = curry( function filterReducer(predicateFn,combinationFn){
return function reducer(list,val){
if (predicateFn( val )) return combinationFn( list, val );
return list;
};
} );
var strToUppercaseReducer =
curriedMapReducer( strUppercase )( listCombination );
var isLongEnoughReducer =
curriedFilterReducer( isLongEnough )( listCombination );
var isShortEnoughReducer =
curriedFilterReducer( isShortEnough )( listCombination );</code></pre>
<p>这看起来有点冗长而且可能不是很有用。</p>
<p>但这实际上是我们进行下一步推导的必要条件。请记住,我们的最终目标是能够 <code>compose(..)</code> 这些 reducers。我们快要完成了。</p>
<blockquote><p> 附录 A:Transducing(下)---- 四天后更新</p></blockquote>
<p>** 【上一章】[翻译连载 | 第 11 章:融会贯通 -《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇<br>](<a href="https://link.segmentfault.com/?enc=sTXXOaqF8glnW9pfNyh%2FKw%3D%3D.nROk4j64GeuV8UUS3sY%2BJ0yNoFv%2FBjWKV%2FgizVhklui7NJNZy8YnWiuJL3VJ305qhN%2B5GYzsByZrK3WiIw0ZSA%3D%3D" rel="nofollow">https://juejin.im/post/5a0cf1...</a> **</p>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote>
<p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=c%2FfGdpWtImap4Xb%2BVYunVw%3D%3D.0AkASw8NtvrlEx5Wtu2AUXLw9Ie33rezPoP3bhI0GdI%3D" rel="nofollow">https://www.ikcamp.com</a><br>访问官网更快阅读全部免费分享课程:<br>《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》<br>《iKcamp出品|基于Koa2搭建Node.js实战项目教程》<br>包含:文章、视频、源代码</p>
</blockquote>
翻译连载 | 第 11 章:融会贯通 -《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
https://segmentfault.com/a/1190000012029051
2017-11-16T10:09:42+08:00
2017-11-16T10:09:42+08:00
iKcamp
https://segmentfault.com/u/ikcamp
3
<ul>
<li><p>原文地址:<a href="https://link.segmentfault.com/?enc=7OotHZtKJ5WGQiXiLqEbhg%3D%3D.Oxr6eIPcPBQqJYUAoVPU1Ptwko6bNNSWdvyZ3VCz7tb1hr%2Ftzu7nLulVjN8uP1QJ" rel="nofollow">Functional-Light-JS</a></p></li>
<li><p>原文作者:<a href="https://link.segmentfault.com/?enc=Qig1h9u1nh7mQ2S%2BwnsrBw%3D%3D.Xnbty9x%2FabRbP1qTwMqGu5s74jla0%2F28IuCyDKFZNko%3D" rel="nofollow">Kyle Simpson-《You-Dont-Know-JS》作者</a></p></li>
</ul>
<blockquote>
<p>关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。</p>
<p>译者团队(排名不分先后):<a href="https://link.segmentfault.com/?enc=FAXYi7e42t42uyS4wM%2Fexw%3D%3D.ji5f%2B%2FoRYS7weIDUsqyOMQ7rTnUlvXITF%2FJW6st%2B6dM%3D" rel="nofollow">阿希</a>、<a href="https://link.segmentfault.com/?enc=CFm%2FvQcnM1hZOIs69Yon8g%3D%3D.%2FiK4GVOh%2BZULT569h1Hm1Q%2FMS2jWyvQWHsobTIXZa%2FQ%3D" rel="nofollow">blueken</a>、<a href="https://link.segmentfault.com/?enc=IVeleXq2Qyxjm2ESa4m3ew%3D%3D.o7lNn9ewVhTlOdXrvVigHx%2F%2FAX5Dzan7k0WS1zn79RQ%3D" rel="nofollow">brucecham</a>、<a href="https://link.segmentfault.com/?enc=xMpF5qeEMwOMmrVMlAMcOA%3D%3D.veYLnA0WBvmFSvxUKrcE8Y77PCWQ63O%2FOz3bZibQYIo%3D" rel="nofollow">cfanlife</a>、<a href="https://link.segmentfault.com/?enc=PvvrrYuokwZBwEGCY7DYNA%3D%3D.CVi8BNS%2F60Yz%2BRCxnMu2qMJIDqe6hBI1wqwIzdjPEWo%3D" rel="nofollow">dail</a>、<a href="https://link.segmentfault.com/?enc=pJE4QS4k5Rtqs8RywS%2BScQ%3D%3D.yv1ANVzlv2fCikc21Aw4eBtbY711bVza7w%2BqkwKRfEM%3D" rel="nofollow">kyoko-df</a>、<a href="https://link.segmentfault.com/?enc=gowiWwD%2BiUTQTSJYIt%2FC4g%3D%3D.gqty8hy9fJ9YEwHTbqV5NBKTp6GGowTq8li%2BQRteZy0%3D" rel="nofollow">l3ve</a>、<a href="https://link.segmentfault.com/?enc=a8ff736aOtkGFM2g9D50Aw%3D%3D.M5%2FO2tdxG3ihaEG3E8ByBO4Z2lx5Wo%2FysaC2uEfa35c%3D" rel="nofollow">lilins</a>、<a href="https://link.segmentfault.com/?enc=ufrcWnGRy07s76m%2FcYlvKg%3D%3D.0YHLZW66DedpauuXd5Nf9RKwIEtR1VNZ0K%2FQ8ZCzy%2BJ8dLtPKG4Oo5l%2F04WJMw1g" rel="nofollow">LittlePineapple</a>、<a href="https://link.segmentfault.com/?enc=%2FUsf08d8IqkfebQz5J6TIA%3D%3D.5jIO6xBm5GTOod6%2B3ig48kQokHIqSSJOP%2B9Iq8T2pSg%3D" rel="nofollow">MatildaJin</a>、<a href="https://link.segmentfault.com/?enc=9qS%2F6nmFiLX35gGMze3Kgg%3D%3D.s8qL2sKiry%2FMa3%2BwLkFHV%2F73zmWeo9%2BralVNDAWO0Fc%3D" rel="nofollow">冬青</a>、<a href="https://link.segmentfault.com/?enc=P7lBc8p6mknv3NsXlWYPSA%3D%3D.XAq%2B6JIuFh7kMsyCrCqZa5SLVfU2qag%2B%2FbxHCrAk%2FHc%3D" rel="nofollow">pobusama</a>、<a href="https://link.segmentfault.com/?enc=ILojrcIJp3%2BkEDvQNJj3Dw%3D%3D.RhLqzu6Cs31KRj7gDs3EsWA7jHUtkH2QwO2j6EE9h%2F3VN5jllZIcPVtnfHKqmzy6" rel="nofollow">Cherry</a>、<a href="https://link.segmentfault.com/?enc=UFl0uzp0HXBxgnptJQ8qAA%3D%3D.bL9Q6Qkj0MZZtYRMbkajOoSMvplbXtyVUxdyw%2BBDqkU%3D" rel="nofollow">萝卜</a>、<a href="https://link.segmentfault.com/?enc=t59TeQcUSc%2BSrSrnQZiqOw%3D%3D.X8EyjZGqlQnICFoHpV17hnrqBlAnQFpFS9FnMEDCw%2B0%3D" rel="nofollow">vavd317</a>、<a href="https://link.segmentfault.com/?enc=gszqOwA37cVc5ix7iFPWmg%3D%3D.vIjTFv6Ea3S4ygxj9mXJu6wUQVnW1YAQO%2BQ4E%2FKrREQ%3D" rel="nofollow">vivaxy</a>、<a href="https://link.segmentfault.com/?enc=qva5%2FIXk%2FblWUSQ1Dgbkyw%3D%3D.hv9Fen6UC86RAUZCAY4xEOVGSnft2WwbvNK%2FPzGxMbc%3D" rel="nofollow">萌萌</a>、<a href="https://link.segmentfault.com/?enc=ls8k27hzuBB0iquiYGHXDg%3D%3D.xtD8Mp9c2cMlc9pOW6ZWgFnyvJNPi33ZLxRkmwZlkPA%3D" rel="nofollow">zhouyao</a></p>
</blockquote>
<h2>JavaScript 轻量级函数式编程</h2>
<h2>第 11 章:融会贯通</h2>
<p>现在你已经掌握了所有需要掌握的关于 JavaScript 轻量级函数式编程的内容。下面不会再引入新的概念。</p>
<p>本章主要目标是概念的融会贯通。通过研究代码片段,我们将本书中大部分主要概念联系起来并学以致用。</p>
<p>建议进行大量深入的练习来熟悉这些技巧,因为理解本章内容对于将来你在实际编程场景中应用函数式编程原理至关重要。</p>
<h3>准备</h3>
<p>我们来写一个简单的股票行情工具吧。</p>
<p><strong>注意:</strong> 可以在本书的 GitHub 仓库(<a href="https://link.segmentfault.com/?enc=x3jZfZiSCMchyVPSg5NidQ%3D%3D.VljrqOuo9vEJC2yb4DfS3GCW%2F1Bx6VA7mUOQUKmyGAGNvirI1y3TqoL59NzpVawe" rel="nofollow">https://github.com/getify/Functional-Light-JS</a>)下的 <code>ch11-code/</code> 目录里找到参考代码。同时,在书中讨论到的函数式编程辅助函数的基础上,我们筛选了所需的一部分放到了 <code>ch11-code/fp-helpers.js</code> 文件中。本章中,我们只会讨论到其中相关的部分。</p>
<p>首先来编写 HTML 部分,这样便可以对信息进行展示了。我们在 <code>ch11-code/index.html</code> 文件中先写一个空的 <code><ul ..></code> 元素,在运行时,DOM 会被填充成:</p>
<pre><code class="html"><ul id="stock-ticker">
<li class="stock" data-stock-id="AAPL">
<span class="stock-name">AAPL</span>
<span class="stock-price">$121.95</span>
<span class="stock-change">+0.01</span>
</li>
<li class="stock" data-stock-id="MSFT">
<span class="stock-name">MSFT</span>
<span class="stock-price">$65.78</span>
<span class="stock-change">+1.51</span>
</li>
<li class="stock" data-stock-id="GOOG">
<span class="stock-name">GOOG</span>
<span class="stock-price">$821.31</span>
<span class="stock-change">-8.84</span>
</li>
</ul></code></pre>
<p>我必须要事先提醒你的一点是,和 DOM 进行交互属于输入/输出操作,这也意味着会产生一定的副作用。我们不能消除这些副作用,所以我们尽量减少和 DOM 相关的操作。这些技巧在第 5 章中已经提到了。</p>
<p>概括一下我们的小工具的功能:代码将在每次收到添加新股票事件时添加 <code><li ..></code> 元素,并在股票价格更新事件发生时更新价格。</p>
<p>在第 11 章的示例代码 <code>ch11-code/mock-server.js</code> 中,我们设置了一些定时器,把随机生成的假股票数据推送到一个简单的事件发送器中,来模拟从服务器收到的股票数据。我们暴露了一个 <code>connectToServer()</code> 接口来实现模拟,但是实际上,它只是返回了一个假的事件发送器。</p>
<p><strong>注意:</strong> 这个文件是用来模拟数据的,所以我没有花费太多的精力让它完全符合函数式编程,不建议大家花太多时间研究这个文件中的代码。如果你写了一个真正的服务器 —— 对于那些雄心勃勃的读者来说,这是一个有趣的加分练习 —— 这时你才应该考虑采用函数式编程思想来实现这些代码。</p>
<p>我们在 <code>ch11-code/stock-ticker-events.js</code> 中,创建了一些 observable(通过 RxJS)连接到事件发送器对象上。通过调用 <code>connectToServer()</code> 来获取这个事件的发射器,然后监听名称为 <code>"stock"</code> 的事件,通过这个事件来添加一个新的股票代码,同时监听名称为 <code>"stock-update"</code> 的事件,通过这个事件来更新股票价格和涨跌幅。最后,我们定义一些转换函数,来对这些 observable 传入的数据进行格式化。</p>
<p>在 <code>ch11-code/stock-ticker.js</code> 中,我们将我们的界面操作(DOM 部分的副作用)定义在 <code>stockTickerUI</code> 对象的方法中。我们还定义了各种辅助函数,包括 <code>getElemAttr(..)</code>,<code>stripPrefix(..)</code> 等等。最后,我们通过 <code>subscribe(..)</code> 监听两个 observable,来获得格式化好的数据,渲染到 DOM 上。</p>
<h3>股票信息</h3>
<p>一起看看 <code>ch11-code/stock-ticker-events.js</code> 中的代码,我们先从一些基本的辅助函数开始:</p>
<pre><code class="js">function addStockName(stock) {
return setProp( "name", stock, stock.id );
}
function formatSign(val) {
if (Number(val) > 0) {
return `+${val}`;
}
return val;
}
function formatCurrency(val) {
return `$${val}`;
}
function transformObservable(mapperFn,obsv){
return obsv.map( mapperFn );
}</code></pre>
<p>这些纯函数应该很容易理解。参见第 4 章 <code>setProp(..)</code> 在设置新属性之前复制了对象。这实践到了我们在第 6 章中学习到的原则:通过把变量当作不可变的变量来避免副作用,即使其本身是可变的。</p>
<p><code>addStockName(..)</code> 用来在股票信息对象中添加一个 <code>name</code> 属性,它的值和这个对象 <code>id</code> 一致。<code>name</code> 会作为股票的名称展示在工具中。</p>
<p>有一个关于 <code>transformObservable(..)</code> 的颇为微妙的注意事项:表面上看起来在 <code>map(..)</code> 函数中返回一个新的 observable 是纯函数操作,但是事实上,<code>obsv</code> 的内部状态被改变了,这样才能够和 <code>map(..)</code> 返回的新的 observable 连接起来。这个副作用并不是个大问题,而且不会影响我们的代码可读性,但是随时发现潜在的副作用是非常重要的,这样就不会在出错时倍感惊讶!</p>
<p>当从“服务器”获取股票信息时,数据是这样的:</p>
<pre><code class="js">{ id: "AAPL", price: 121.7, change: 0.01 }</code></pre>
<p>在把 <code>price</code> 的值显示到 DOM 上之前,需要用 <code>formatCurrency(..)</code> 函数格式化一下(比如变成 <code>"$121.70"</code>),同时需要用 <code>formatChange(..)</code> 函数格式化 <code>change</code> 的值(比如变成 <code>"+0.01"</code>)。但是我们不希望修改消息对象中的 <code>price</code> 和 <code>change</code>,所以我们需要一个辅助函数来格式化这些数字,并且要求这个辅助函数返回一个新的消息对象,其中包含格式化好的 <code>price</code> 和 <code>change</code>:</p>
<pre><code class="js">function formatStockNumbers(stock) {
var updateTuples = [
[ "price", formatPrice( stock.price ) ],
[ "change", formatChange( stock.change ) ]
];
return reduce( function formatter(stock,[propName,val]){
return setProp( propName, stock, val );
} )
( stock )
( updateTuples );
}</code></pre>
<p>我们创建了 <code>updateTuples</code> 元组来保存 <code>price</code> 和 <code>change</code> 的信息,包括属性名称和格式化好的值。把 <code>stock</code> 对象作为 <code>initialValue</code>,对元组进行 <code>reduce(..)</code>(参考第 8 章)。把元组中的信息解构成 <code>propName</code> 和 <code>val</code>,然后返回了 <code>setProp(..)</code> 调用的结果,这个结果是一个被复制了的新的对象,其中的属性被修改过了。</p>
<p>下面我们再定义几个辅助函数:</p>
<pre><code class="js">var formatDecimal = unboundMethod( "toFixed" )( 2 );
var formatPrice = pipe( formatDecimal, formatCurrency );
var formatChange = pipe( formatDecimal, formatSign );
var processNewStock = pipe( addStockName, formatStockNumbers );</code></pre>
<p><code>formatDecimal(..)</code> 函数接收一个数字作为参数(如 <code>2.1</code>)并且调用数字的 <code>toFixed( 2 )</code> 方法。我们使用了第 8 章介绍的 <code>unboundMethod(..)</code> 来创建一个独立的延迟绑定函数。</p>
<p><code>formatPrice(..)</code>,<code>formatChange(..)</code> 和 <code>processNewStock(..)</code> 都用到了 <code>pipe(..)</code> 来从左到右地组合运算(见第 4 章)。</p>
<p>为了能在事件发送器的基础上创建 observable(见第 10 章),我们将封装一个独立的柯里化辅助函数(见第 3 章)来包装 RxJS 的 <code>Rx.Observable.fromEvent(..)</code>:</p>
<pre><code class="js">var makeObservableFromEvent = curry( Rx.Observable.fromEvent, 2 )( server );</code></pre>
<p>这个函数特定地监听了 <code>server</code>(事件发送器),在接受了事件名称字符串参数后,就能生成 observable 了。我们准备好了创建 observer 的所有代码片段后,用映射函数转换 observer 来格式化获取到的数据:</p>
<pre><code class="js">var observableMapperFns = [ processNewStock, formatStockNumbers ];
var [ newStocks, stockUpdates ] = pipe(
map( makeObservableFromEvent ),
curry( zip )( observableMapperFns ),
map( spreadArgs( transformObservable ) )
)
( [ "stock", "stock-update" ] );</code></pre>
<p>我们创建了包含了事件名称(<code>["stock","stock-update"]</code>)的数组,然后 <code>map(..)</code>(见第 8 章)这个数组,生成了一个包含了两个 observable 的数组,然后把这个数组和 observable 映射函数 <code>zip(..)</code>(见第 8 章)起来,产生一个 <code>[ observable, mapperFn ]</code> 这样的元组数组。最后通过 <code>spreadArgs(..)</code>(见第 3 章)把每个元组数组展开为单独的参数,<code>map(..)</code> 到了 <code>transformObservable(..)</code> 函数上。</p>
<p>得到的结果是一个包含了转换好的 observable 的数组,通过数组结构赋值的方式分别赋值到了 <code>newStocks</code> 和 <code>stockUpdates</code> 两个变量上。</p>
<p>到此为止,我们用轻量级函数式编程的方式来让股票行情信息事件成为了 observable!在 <code>ch11-code/stock-ticker.js</code> 中我们会订阅这两个 observable。</p>
<p>回头想想我们用到的函数式编程原则。这样做有没有意义呢?你能否明白我们是如何运用前几章中介绍的各种概念的呢?你能不能想到别的方式来实现这些功能?</p>
<p>更重要的是,如果你用命令式编程的方法是如何实现上面的功能的呢?你认为两种方式相比孰优孰劣?试试看用你熟悉的命令式编程的方式去写这个功能。如果你和我一样,那么命令式编程仍然会让你感到更加自然。</p>
<p>在进行下面的学习之前,你需要<strong>明白</strong>的是,除了使你感到非常自然的命令式编程以外,你<strong>也</strong>已经能够了解函数式编程的合理性了。想想看每个函数的输入和输出,你看到它们是怎样组合在一起的了吗?</p>
<p>在你豁然开朗以前一定要持续不断地练习。</p>
<h3>股票行情界面</h3>
<p>如果你熟悉了上一章节中的函数式编程模式,你就可以开始学习 <code>ch11-code/stock-ticker.js</code> 文件中的内容了。这里会涉及相当多的重要内容,所以我们将好好地理解整个文件中的每个方法。</p>
<p>我们先从定义一些操作 DOM 的辅助函数开始:</p>
<pre><code class="js">function isTextNode(node) {
return node && node.nodeType == 3;
}
function getElemAttr(elem,prop) {
return elem.getAttribute( prop );
}
function setElemAttr(elem,prop,val) {
// 副作用!!
return elem.setAttribute( prop, val );
}
function matchingStockId(id) {
return function isStock(node){
return getStockId( node ) == id;
};
}
function isStockInfoChildElem(elem) {
return /\bstock-/i.test( getClassName( elem ) );
}
function appendDOMChild(parentNode,childNode) {
// 副作用!!
parentNode.appendChild( childNode );
return parentNode;
}
function setDOMContent(elem,html) {
// 副作用!!
elem.innerHTML = html;
return elem;
}
var createElement = document.createElement.bind( document );
var getElemAttrByName = curry( reverseArgs( getElemAttr ), 2 );
var getStockId = getElemAttrByName( "data-stock-id" );
var getClassName = getElemAttrByName( "class" );</code></pre>
<p>这些函数应该算是不言自明的。为了获得 <code>getElemAttrByName(..)</code>,我用了 <code>curry(reverseArgs( .. ))</code>(见第 3 章)而不是 <code>partialRight(..)</code>,只是为了在这种特殊情况下,稍微提高一点性能。</p>
<p>注意,我标出了操作 DOM 元素时的副作用。因为不能简单地用克隆的 DOM 对象去替换已有的,所以我们在不替换已有对象的基础上,勉强接受了一些副作用的产生。至少如果在 DOM 渲染中产生一个错误,我们可以轻松地搜索这些代码注释来缩小可能的错误代码。</p>
<p><code>matchingStockId(..)</code> 用到了闭包(见第 2 章),它创建了一个内部函数(<code>isStock(..)</code>),使在其他作用域下运行时依然能够保存 <code>id</code> 变量。</p>
<p>其他的辅助函数:</p>
<pre><code class="js">function stripPrefix(prefixRegex) {
return function mapperFn(val) {
return val.replace( prefixRegex, "" );
};
}
function listify(listOrItem) {
if (!Array.isArray( listOrItem )) {
return [ listOrItem ];
}
return listOrItem;
}</code></pre>
<p>定义一个用以获取某个 DOM 元素的子节点的辅助函数:</p>
<pre><code class="js">var getDOMChildren = pipe(
listify,
flatMap(
pipe(
curry( prop )( "childNodes" ),
Array.from
)
)
);</code></pre>
<p>首先,用 <code>listify(..)</code> 来保证我们得到的是一个数组(即使里面只有一个元素)。回忆一下在第 8 章中提到的 <code>flatMap(..)</code>,这个函数把一个包含数组的数组扁平化,变成一个浅数组。</p>
<p>映射函数先把 DOM 元素映射成它的子元素数组,然后我们用 <code>Array.from(..)</code> 把这个数组变成一个真实的数组(而不是一个 NodeList)。这两个函数组合成一个映射函数(通过 <code>pipe(..)</code>),这就是融合(见第 8 章)。</p>
<p>现在,我们用 <code>getDOMChildren(..)</code> 实用函数来定义股票行情工具中查找特定 DOM 元素的工具函数:</p>
<pre><code class="js">function getStockElem(tickerElem,stockId) {
return pipe(
getDOMChildren,
filterOut( isTextNode ),
filterIn( matchingStockId( stockId ) )
)
( tickerElem );
}
function getStockInfoChildElems(stockElem) {
return pipe(
getDOMChildren,
filterOut( isTextNode ),
filterIn( isStockInfoChildElem )
)
( stockElem );
}</code></pre>
<p><code>getStockElem(..)</code> 接受 <code>tickerElem</code> DOM 节点作为参数,获取其子元素,然后过滤,保证我们得到的是符合股票代码的 DOM 元素。<code>getStockInfoChildElems(..)</code> 几乎是一样的,不同的是它从一个股票元素节点开始查找,还使用了不同的过滤函数。</p>
<p>两个实用函数都会过滤掉文字节点(因为它们没有其他的 DOM 节点那样的方法),保证返回一个 DOM 元素数组,哪怕数组中只有一个元素。</p>
<h4>主函数</h4>
<p>我们用 <code>stockTickerUI</code> 对象来保存三个修改界面的主要方法,如下:</p>
<pre><code class="js">var stockTickerUI = {
updateStockElems(stockInfoChildElemList,data) {
// ..
},
updateStock(tickerElem,data) {
// ..
},
addStock(tickerElem,data) {
// ..
}
};</code></pre>
<p>我们先看看 <code>updateStock(..)</code>,这是三个函数里面最简单的:</p>
<pre><code class="js">var stockTickerUI = {
// ..
updateStock(tickerElem,data) {
var getStockElemFromId = curry( getStockElem )( tickerElem );
var stockInfoChildElemList = pipe(
getStockElemFromId,
getStockInfoChildElems
)
( data.id );
return stockTickerUI.updateStockElems(
stockInfoChildElemList,
data
);
},
// ..
};</code></pre>
<p>柯里化之前的辅助函数 <code>getStockElem(..)</code>,传给它 <code>tickerElem</code>,得到了 <code>getStockElemFromId(..)</code> 函数,这个函数接受 <code>data.id</code> 作为参数。把 <code><li></code> 元素(其实是数组形式的)传入 <code>getStockInfoChildElems(..)</code>,我们得到了三个 <code><span></code> 子元素,用来展示股票信息,我们把它们保存在 <code>stockInfoChildElemList</code> 变量中。然后把数组和股票信息 <code>data</code> 对象一起传给 <code>stockTickerUI.updateStockElems(..)</code>,来更新 <code><span></code> 中的数据。</p>
<p>现在我们来看看 <code>stockTickerUI.updateStockElems(..)</code>:</p>
<pre><code class="js">var stockTickerUI = {
updateStockElems(stockInfoChildElemList,data) {
var getDataVal = curry( reverseArgs( prop ), 2 )( data );
var extractInfoChildElemVal = pipe(
getClassName,
stripPrefix( /\bstock-/i ),
getDataVal
);
var orderedDataVals =
map( extractInfoChildElemVal )( stockInfoChildElemList );
var elemsValsTuples =
filterOut( function updateValueMissing([infoChildElem,val]){
return val === undefined;
} )
( zip( stockInfoChildElemList, orderedDataVals ) );
// 副作用!!
compose( each, spreadArgs )
( setDOMContent )
( elemsValsTuples );
},
// ..
};</code></pre>
<p>这部分有点难理解。我们一行行来看。</p>
<p>首先把 <code>prop</code> 函数的参数反转,柯里化后,把 <code>data</code> 消息对象绑定上去,得到了 <code>getDataVal(..)</code> 函数,这个函数接收一个属性名称作为参数,返回 <code>data</code> 中的对应的属性名称的值。</p>
<p>接下来,我们看看 <code>extractInfoChildElem</code>:</p>
<pre><code class="js">var extractInfoChildElemVal = pipe(
getClassName,
stripPrefix( /\bstock-/i ),
getDataVal
);</code></pre>
<p>这个函数接受一个 DOM 元素作为参数,拿到 class 属性的值,然后把 <code>"stock-"</code> 前缀去掉,然后用这个属性值(<code>"name"</code>,<code>"price"</code> 或 <code>"change"</code>),通过 <code>getDataVal(..)</code> 函数,在 <code>data</code> 中找到对应的数据。你可能会问:“还有这种操作?”。</p>
<p>其实,这么做的目的是按照 <code>stockInfoChildElemList</code> 中的 <code><span></code> 元素的顺序从 <code>data</code> 中拿到数据。我们对 <code>stockInfoChildElemList</code> 数组调用 <code>extractInfoChildElem</code> 映射函数,来拿到这些数据。</p>
<p>接下来,我们把 <code><span></code> 数组和数据数组压缩起来,得到一个元组:</p>
<pre><code class="js">zip( stockInfoChildElemList, orderedDataVals )</code></pre>
<p>这里有一点不太容易理解,我们定义的 observable 转换函数中,新的股票行情数据 <code>data</code> 会包含一个 <code>name</code> 属性,来对应 <code><span class="stock-name"></code> 元素,但是在股票行情更新事件的数据中可能会找不到对应的 <code>name</code> 属性。</p>
<p>一般来说,如果股票更新消息事件的数据对象不包含某个股票数据的话,我们就不应该更新这只股票对应的 DOM 元素。所以我们要用 <code>filterOut(..)</code> 剔除掉没有值的元组(这里的值在元组的第二个元素)。</p>
<pre><code class="js">var elemsValsTuples =
filterOut( function updateValueMissing([infoChildElem,val]){
return val === undefined;
} )
( zip( stockInfoChildElemList, orderedDataVals ) );</code></pre>
<p>筛选后的结果是一个元组数组(如:<code>[ <span>, ".." ]</code>),这个数组可以用来更新 DOM 了,我们把这个结果保存到 <code>elemsValsTuples</code> 变量中。</p>
<p><strong>注意:</strong> 既然 <code>updateValueMissing(..)</code> 是声明在函数内的,所以我们可以更方便地控制这个函数。与其使用 <code>spreadArgs(..)</code> 来把函数接收的一个数组形式的参数展开成两个参数,我们可以直接用函数的参数解构声明(<code>function updateValueMissing([infoChildElem,val]){ ..</code>),参见第 2 章。</p>
<p>最后,我们要更新 DOM 中的 <code><span></code> 元素:</p>
<pre><code class="js">// 副作用!!
compose( each, spreadArgs )( setDOMContent )
( elemsValsTuples );</code></pre>
<p>我们用 <code>each(..)</code> 遍历了 <code>elemsValsTuples</code> 数组(参考第 8 章中关于 <code>forEach(..)</code> 的讨论)。</p>
<p>与其他地方使用 <code>pipe(..)</code> 来组合函数不同,这里使用 <code>compose(..)</code>(见第 4 章),先把 <code>setDomContent(..)</code> 传到 <code>spreadArgs(..)</code> 中,再把执行的结果作为迭代函数传到 <code>each(..)</code> 中。执行时,每个元组被展开为参数传给了 <code>setDOMContent(..)</code> 函数,然后对应地更新 DOM 元素。</p>
<p>最后说明下 <code>addStock(..)</code>。我们先把整个函数写出来,然后再一句句地解释:</p>
<pre><code class="js">var stockTickerUI = {
// ..
addStock(tickerElem,data) {
var [stockElem, ...infoChildElems] = map(
createElement
)
( [ "li", "span", "span", "span" ] );
var attrValTuples = [
[ ["class","stock"], ["data-stock-id",data.id] ],
[ ["class","stock-name"] ],
[ ["class","stock-price"] ],
[ ["class","stock-change"] ]
];
var elemsAttrsTuples =
zip( [stockElem, ...infoChildElems], attrValTuples );
// 副作用!!
each( function setElemAttrs([elem,attrValTupleList]){
each(
spreadArgs( partial( setElemAttr, elem ) )
)
( attrValTupleList );
} )
( elemsAttrsTuples );
// 副作用!!
stockTickerUI.updateStockElems( infoChildElems, data );
reduce( appendDOMChild )( stockElem )( infoChildElems );
tickerElem.appendChild( stockElem );
}
};</code></pre>
<p>这个操作界面的函数会根据新的股票信息生成一个空的 DOM 结构,然后调用 <code>stockTickerUI.updateStockElems(..)</code> 方法来更新其中的内容。</p>
<p>首先:</p>
<pre><code class="js">var [stockElem, ...infoChildElems] = map(
createElement
)
( [ "li", "span", "span", "span" ] );</code></pre>
<p>我们先创建 <code><li></code> 父元素和三个 <code><span></code> 子元素,把它们分别赋值给了 <code>stockElem</code> 和 <code>infoChildElems</code> 数组。</p>
<p>为了设置 DOM 元素的对应属性,我们声明了一个元组数组组成的数组。按照顺序,每个元组数组对应上面四个 DOM 元素中的一个。每个元组数组中的元组由对应元素的属性和值组成:</p>
<pre><code class="js">var attrValTuples = [
[ ["class","stock"], ["data-stock-id",data.id] ],
[ ["class","stock-name"] ],
[ ["class","stock-price"] ],
[ ["class","stock-change"] ]
];</code></pre>
<p>我们把四个 DOM 元素和 <code>attrValTuples</code> 数组 <code>zip(..)</code> 起来:</p>
<pre><code class="js">var elemsAttrsTuples =
zip( [stockElem, ...infoChildElems], attrValTuples );</code></pre>
<p>最后的结果会是:</p>
<pre><code>[
[ <li>, [ ["class","stock"], ["data-stock-id",data.id] ] ],
[ <span>, [ ["class","stock-name"] ] ],
..
]</code></pre>
<p>如果我们用命令式的方式来把属性和值设置到每个 DOM 元素上,我们会用嵌套的 <code>for</code> 循环。用函数式编程的方式的话也会是这样,不过这时嵌套的是 <code>each(..)</code> 循环:</p>
<pre><code class="js">// 副作用!!
each( function setElemAttrs([elem,attrValTupleList]){
each(
spreadArgs( partial( setElemAttr, elem ) )
)
( attrValTupleList );
} )
( elemsAttrsTuples );</code></pre>
<p>外层的 <code>each(..)</code> 循环了元组数组,其中每个数组的元素是一个 <code>elem</code> 和它对应的 <code>attrValTupleList</code>,这个元组数组被传入了 <code>setElemAttrs(..)</code>,在函数的参数中被解构成两个值。</p>
<p>在外层循环内,元组数组的子数组(包含了属性和值的数组)被传递到了内层的 <code>each(..)</code> 循环中。内层的迭代函数首先以 <code>elem</code> 作为第一个参数对 <code>setElemAttr(..)</code> 进行了部分实现,然后把剩下的函数参数展开,把每个属性值元组作为参数传递进这个函数中。</p>
<p>到此为止,我们有了 <code><span></code> 元素数组,每个元素上都有了该有的属性,但是还没有 <code>innerHTML</code> 的内容。这里,我们要用 <code>stockTickerUI.updateStockElems(..)</code> 函数,把 <code>data</code> 设置到 <code><span></code> 上去,和股票信息更新事件的处理一样。</p>
<p>然后,我们要把这些 <code><span></code> 元素添加到对应的父级 <code><li></code> 元素中去,我们用 <code>reduce(..)</code> 来做这件事(见第 8 章)。</p>
<pre><code class="js">reduce( appendDOMChild )( stockElem )( infoChildElems );</code></pre>
<p>最后,用操作 DOM 元素的副作用方法把新的股票元素添加到小工具的 DOM 节点中去:</p>
<pre><code class="js">tickerElem.appendChild( stockElem );</code></pre>
<p>呼!你跟上了吗?我建议你在继续下去之前,回到开头,重新读几遍这部分内容,再练习几遍。</p>
<h4>订阅 Observable</h4>
<p>最后一个重要任务是订阅 <code>ch11-code/stock-ticker-events.js</code> 中定义的 observable,把事件传递给正确的主函数(<code>addStock(..)</code> 和 <code>updateStock(..)</code>)。</p>
<p>注意,这两个主函数接受 <code>tickerElem</code> 作为第一个参数。我们声明一个数组(<code>stockTickerUIMethodsWithDOMContext</code>)保存了两个中间函数(也叫作闭包,见第 2 章),这两个中间函数是通过部分参数绑定的函数把小工具的 DOM 元素绑定到了两个主函数上来生成的。</p>
<pre><code class="js">var ticker = document.getElementById( "stock-ticker" );
var stockTickerUIMethodsWithDOMContext = map(
curry( reverseArgs( partial ), 2 )( ticker )
)
( [ stockTickerUI.addStock, stockTickerUI.updateStock ] );</code></pre>
<p><code>reverseArgs( partial )</code> 是之前提到的 <code>partialRight(..)</code> 的替代品,优化了性能。但是这里 <code>partial(..)</code> 是映射函数的目标函数。所以我们需要事先 <code>curry(..)</code> 化,这样我们就可以先把第二个参数 <code>ticker</code> 传给 <code>partial(..)</code>,后面把主函数传进去的时候就可以用到之前传入的 <code>ticker</code> 了。数组中的这两个中间函数就可以被用来订阅 observable 了。</p>
<p>我们用闭包在这两个中间函数中保存了 <code>ticker</code> 数据,在第 7 章中,我们知道了还可以把 <code>ticker</code> 保存在对象的属性上,通过使用两个函数上的指向 <code>stockTickerUI</code> 的 <code>this</code> 来访问 <code>ticker</code>。因为 <code>this</code> 是个隐式的输入(见第 2 章),所以一般来说不推荐用对象的方式,所以我使用了闭包的方式。</p>
<p>为了订阅 observable,我们先写一个辅助函数,提供一个未绑定的方法:</p>
<pre><code class="js">var subscribeToObservable =
pipe( uncurry, spreadArgs )( unboundMethod( "subscribe" ) );</code></pre>
<p><code>unboundMethod("subscribe")</code> 已经柯里化了,所以我们用 <code>uncurry(..)</code>(见第 3 章)先反柯里化,然后再用 <code>spreadArgs(..)</code>(依然见第 3 章)来修改接受的参数的格式,所以这个函数接受一个元组作为参数,展开后传递下去。</p>
<p>现在,我们只要把 observable 数组和封装好上下文的主函数 <code>zip(..)</code> 起来。生成一个元组数组,每个元组可以用之前定义的 <code>subscribeToObservable(..)</code> 辅助函数来订阅 observable:</p>
<pre><code class="js">var stockTickerObservables = [ newStocks, stockUpdates ];
// 副作用!!
each( subscribeToObservable )
( zip( stockTickerUIMethodsWithDOMContext, stockTickerObservables ) );</code></pre>
<p>由于我们修改了这些 observable 的状态以订阅它们,而且由于我们使用了 <code>each(..)</code> —— 总是和副作用相关! —— 我们用代码注释来说明这个问题。</p>
<p>就是这样!花些时间研究比较这段代码和它命令式的替代版本,正如我们之前在股票行情信息中讨论到的一样。真的,可以多花点时间。我知道这是一本很长的书,但是完整地读下来会让你能够消化和理解这样的代码。</p>
<p>你现在打算在 JavaScript 中如何合理地使用函数式编程?继续练习,就像我们在这里做的一样!</p>
<h3>总结</h3>
<p>我们在本章中讨论的示例代码应该被作为一个整体来阅读,而不仅仅是作为章节中所展示的支离破碎的代码片段。如果你还没有完整地阅读过,现在请停下来,去完整地阅读一遍代码目录下的文件吧。确保你在完整的上下文中了解它们。</p>
<p>示例代码并不是实际编写代码的范例,只是提供了一种描述性的,教授如何用轻量级函数式的技巧来解决此类问题的方法。这些代码尽可能多地把本书中不同概念联系起来。这里提供了比代码片段更真实的例子来学习函数式编程。</p>
<p>我相信,随着我不断地学习函数式编程,我会继续改进这个示例代码。你现在看到的只是我在学习曲线上的一个快照。我希望对你来说也是如此。</p>
<p>在我们结束本书的主要内容时,我们一起回顾一下我在第 1 章中提到的可读性曲线:</p>
<p><img src="/img/remote/1460000012029056" alt="" title=""></p>
<p>在学习函数式编程的过程中,理解这张图的真谛,并且为自己设定合理的预期,是非常重要的。你已经到这里了,这已经是一个很大的成果了。</p>
<p>但是,当你在绝望和沮丧的低谷时,别停下来。前面等待你的是一种更好的思维方式,可以写出可读性更好,更容易理解,更容易验证,最终更加可靠的代码。</p>
<p>我不需要再为开发者们不断前行想出更多崇高的理由。感谢你参与到我学习 JavaScript 中的函数式编程的原理的过程中来。我希望你的学习过程和我的一样,充实而充满希望!</p>
<p><strong> 【上一章】<a href="https://link.segmentfault.com/?enc=O023WWSY3vAP%2B5FYZJG8UA%3D%3D.e%2F9OytbrhQS4yvMru%2BPbk00pvBfmlx68t%2BhRL0In3Hh6TCFMqmxPBlQ4XquN6J%2F7" rel="nofollow">翻译连载 | 第 10 章:异步的函数式(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇</a> </strong></p>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote>
<p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=YLq1y9OCv5k4kS6qzE3aaA%3D%3D.l2oqTOWtb9MCI6QfjAmlRFGNoapkT0vIztC0apN4sEU%3D" rel="nofollow">https://www.ikcamp.com</a><br>访问官网更快阅读全部免费分享课程:<br>《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》<br>《iKcamp出品|基于Koa2搭建Node.js实战项目教程》<br>包含:文章、视频、源代码</p>
</blockquote>
如何优雅的设计 React 组件
https://segmentfault.com/a/1190000011939560
2017-11-10T09:46:08+08:00
2017-11-10T09:46:08+08:00
iKcamp
https://segmentfault.com/u/ikcamp
18
<blockquote><p>作者:晓冬<br>本文原创,转载请注明作者及出处</p></blockquote>
<p>如今的 Web 前端已被 React、Vue 和 Angular 三分天下,一统江山十几年的 jQuery 显然已经很难满足现在的开发模式。那么,为什么大家会觉得 jQuery “过时了”呢?一来,文章《<a href="https://link.segmentfault.com/?enc=3htwnUnpNfXwKuJRGNW%2FWQ%3D%3D.DR%2F7PFa%2BS2BEkbHUa3QMK78ToiG4hcw%2F8dMTunwCaXcg%2BkoQNq5Qk4obr%2FPXDPji%2FIEP0Hdb3SsB%2B%2FvmKgmZlYkdAwBrjfLy0uo4z3Rpo04%3D" rel="nofollow">No JQuery! 原生 JavaScript 操作 DOM</a>》就直截了当的告诉你,现在用原生 JavaScript 可以非常方便的操作 DOM 了。其次,jQuery 的便利性是建立在有一个基础 DOM 结构的前提下的,看上去是符合了样式、行为和结构分离,但其实 DOM 结构和 JavaScript 的代码逻辑是耦合的,你的开发思路会不断的在 DOM 结构和 JavaScript 之间来回切换。</p>
<p>尽管现在的 jQuery 已不再那么流行,但 jQuery 的设计思想还是非常值得致敬和学习的,特别是 jQuery 的插件化。如果大家开发过 jQuery 插件的话,想必都会知道,一个插件要足够灵活,需要有细颗粒度的参数化设计。一个灵活好用的 React 组件跟 jQuery 插件一样,都离不开合理的属性化(<code>props</code>)设计,但 React 组件的拆分和组合比起 jQuery 插件来说还是简单的令人发指。</p>
<p>So! 接下来我们就以万能的 TODO LIST 为例,一起来设计一款 React 的 <code>TodoList</code> 组件吧!</p>
<h2>实现基本功能</h2>
<p>TODO LIST 的功能想必我们应该都比较了解,也就是 TODO 的添加、删除、修改等等。本身的功能也比较简单,为了避免示例的复杂度,显示不同状态 TODO LIST 的导航(全部、已完成、未完成)的功能我们就不展开了。</p>
<h3>约定目录结构</h3>
<p>先假设我们已经拥有一个可以运行 React 项目的脚手架(ha~ 因为我不是来教你如何搭建脚手架的),然后项目的源码目录 <code>src/</code> 下可能是这样的:</p>
<pre><code class="shell">.
├── components
├── containers
│ └── App
│ ├── app.scss
│ └── index.js
├── index.html
└── index.js</code></pre>
<p>我们先来简单解释下这个目录设定。我们看到根目录下的 <code>index.js</code> 文件是整个项目的入口模块,入口模块将会处理 DOM 的渲染和 React 组件的热更新(<a href="https://link.segmentfault.com/?enc=mFen7MSgcBNplcad9T8bDg%3D%3D.zoqc7%2Fa9qakhnOdEB0vOM2%2B%2F793SiTTC92oqdZfCuGU3uJZ5wfaVoC7Ty%2BH4ohuK" rel="nofollow">react-hot-loader</a>)等设置。然后,<code>index.html</code> 是页面的 HTML 模版文件,这 2 个部分不是我们这次关心的重点,我们不再展开讨论。</p>
<p>入口模块 <code>index.js</code> 的代码大概是这样子的:</p>
<pre><code class="react">// import reset css, base css...
import React from 'react';
import ReactDom from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import App from 'containers/App';
const render = (Component) => {
ReactDom.render(
<AppContainer>
<Component />
</AppContainer>,
document.getElementById('app')
);
};
render(App);
if (module.hot) {
module.hot.accept('containers/App', () => {
let nextApp = require('containers/App').default;
render(nextApp);
});
}</code></pre>
<p>接下来看 <code>containers/</code> 目录,它将放置我们的页面容器组件,业务逻辑、数据处理等会在这一层做处理,<code>containers/App</code> 将作为我们的页面主容器组件。作为通用组件,我们将它们放置于 <code>components/</code> 目录下。</p>
<p>基本的目录结构看起来已经完成,接下来我们实现下主容器组件 <code>containers/App</code>。</p>
<h3>实现主容器</h3>
<p>我们先来看下主容器组件 <code>containers/App/index.js</code> 最初的代码实现:</p>
<pre><code class="react">import React, { Component } from 'react';
import styles from './app.scss';
class App extends Component {
constructor(props) {
super(props);
this.state = {
todos: []
};
}
render() {
return (
<div className={styles.container}>
<h2 className={styles.header}>Todo List Demo</h2>
<div className={styles.content}>
<header className={styles['todo-list-header']}>
<input
type="text"
className={styles.input}
ref={(input) => this.input = input}
/>
<button
className={styles.button}
onClick={() => this.handleAdd()}
>
Add Todo
</button>
</header>
<section className={styles['todo-list-content']}>
<ul className={styles['todo-list-items']}>
{this.state.todos.map((todo, i) => (
<li key={`${todo.text}-${i}`}>
<em
className={todo.completed ? styles.completed : ''}
onClick={() => this.handleStateChange(i)}
>
{todo.text}
</em>
<button
className={styles.button}
onClick={() => this.handleRemove(i)}
>
Remove
</button>
</li>
))}
</ul>
</section>
</div>
</div>
);
}
handleAdd() {
...
}
handleRemove(index) {
...
}
handleStateChange(index) {
...
}
}
export default App;</code></pre>
<p>我们可以像上面这样把所有的业务逻辑一股脑的塞进主容器中,但我们要考虑到主容器随时会组装其他的组件进来,将各种逻辑堆放在一起,到时候这个组件就会变得无比庞大,直到“无法收拾”。所以,我们得分离出一个独立的 <code>TodoList</code> 组件。</p>
<h2>分离组件</h2>
<h3>TodoList 组件</h3>
<p>在 <code>components/</code> 目录下,我们新建一个 <code>TodoList</code> 文件夹以及相关文件:</p>
<pre><code class="diff">.
├── components
+│ └── TodoList
+│ ├── index.js
+│ └── todo-list.scss
├── containers
│ └── App
│ ├── app.scss
│ └── index.js
...</code></pre>
<p>然后我们将 <code>containers/App/index.js</code> 下跟 <code>TodoList</code> 组件相关的功能抽离到 <code>components/TodoList/index.js</code> 中:</p>
<pre><code class="diff">...
import styles from './todo-list.scss';
export default class TodoList extends Component {
...
render() {
return (
<div className={styles.container}>
- <header className={styles['todo-list-header']}>
+ <header className={styles.header}>
<input
type="text"
className={styles.input}
ref={(input) => this.input = input}
/>
<button
className={styles.button}
onClick={() => this.handleAdd()}
>
Add Todo
</button>
</header>
- <section className={styles['todo-list-content']}>
+ <section className={styles.content}>
- <ul className={styles['todo-list-items']}>
+ <ul className={styles.items}>
{this.state.todos.map((todo, i) => (
<li key={`${todo}-${i}`}>
<em
className={todo.completed ? styles.completed : ''}
onClick={() => this.handleStateChange(i)}
>
{todo.text}
</em>
<button
className={styles.button}
onClick={() => this.handleRemove(i)}
>
Remove
</button>
</li>
))}
</ul>
</section>
</div>
);
}
...
}</code></pre>
<p>有没有注意到上面 <code>render</code> 方法中的 <code>className</code>,我们省去了 <code>todo-list*</code> 前缀,由于我们用的是 <a href="https://link.segmentfault.com/?enc=SQqC9gHy13TVy0GuLgd2pQ%3D%3D.XbO3dG6j1N0XuXHlqxOUxOBpqjbMzmLHyOpGIYyNSK8oIzj7gNyBEl0IOUH59t%2FX" rel="nofollow">CSS MODULES</a>,所以当我们分离组件后,原先在主容器中定义的 <code>todo-list*</code> 前缀的 <code>className</code> ,可以很容易通过 webpack 的配置来实现:</p>
<pre><code class="javascript">...
module.exports = {
...
module: {
rules: [
{
test: /\.s?css/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]--[local]-[hash:base64:5]'
}
},
...
]
}
]
}
...
};</code></pre>
<p>我们再来看下该组件的代码输出后的结果:</p>
<pre><code class="html"><div data-reactroot="" class="app--container-YwMsF">
...
<div class="todo-list--container-2PARV">
<header class="todo-list--header-3KDD3">
...
</header>
<section class="todo-list--content-3xwvR">
<ul class="todo-list--items-1SBi6">
...
</ul>
</section>
</div>
</div></code></pre>
<p>从上面 webpack 的配置和输出的 HTML 中可以看到,<code>className</code> 的命名空间问题可以通过语义化 <code>*.scss</code> 文件名的方式来实现,比如 <code>TodoList</code> 的样式文件 <code>todo-list.scss</code>。这样一来,省去了我们定义组件 <code>className</code> 的命名空间带来的烦恼,从而只需要从组件内部的结构下手。</p>
<p>回到正题,我们再来看下分离 <code>TodoList</code> 组件后的 <code>containers/App/index.js</code>:</p>
<pre><code class="react">import TodoList from 'components/TodoList';
...
class App extends Component {
render() {
return (
<div className={styles.container}>
<h2 className={styles.header}>Todo List Demo</h2>
<div className={styles.content}>
<TodoList />
</div>
</div>
);
}
}
export default App;</code></pre>
<h3>抽离通用组件</h3>
<p>作为一个项目,当前的 <code>TodoList</code> 组件包含了太多的子元素,如:input、button 等。为了让组件“<strong>一次编写,随处使用</strong>”的原则,我们可以进一步拆分 <code>TodoList</code> 组件以满足其他组件的使用。</p>
<p>但是,如何拆分组件才是最合理的呢?我觉得这个问题没有最好的答案,但我们可以从几个方面进行思考:可封装性、可重用性和灵活性。比如拿 <code>h1</code> 元素来讲,你可以封装成一个 <code>Title</code> 组件,然后这样 <code><Title text={title} /></code> 使用,又或者可以这样 <code><Title>{title}</Title></code> 来使用。但你有没有发现,这样实现的 <code>Title</code> 组件并没有起到简化和封装的作用,反而增加了使用的复杂度,对于 HTML 来讲,<code>h1</code> 本身也是一个组件,所以我们拆分组件也是需要掌握一个度的。</p>
<p>好,我们先拿 input 和 button 下手,在 <code>components/</code> 目录下新建 2 个 <code>Button</code> 和 <code>Input</code> 组件:</p>
<pre><code class="diff">.
├── components
+│ ├── Button
+│ │ ├── button.scss
+│ │ └── index.js
+│ ├── Input
+│ │ ├── index.js
+│ │ └── input.scss
│ └── TodoList
│ ├── index.js
│ └── todo-list.scss
...</code></pre>
<p><code>Button/index.js</code> 的代码:</p>
<pre><code class="react">...
export default class Button extends Component {
render() {
const { className, children, onClick } = this.props;
return (
<button
type="button"
className={cn(styles.normal, className)}
onClick={onClick}
>
{children}
</button>
);
}
}</code></pre>
<p><code>Input/index.js</code> 的代码:</p>
<pre><code class="react">...
export default class Input extends Component {
render() {
const { className, value, inputRef } = this.props;
return (
<input
type="text"
className={cn(styles.normal, className)}
defaultValue={value}
ref={inputRef}
/>
);
}
}</code></pre>
<p>由于这 2 个组件自身不涉及任何业务逻辑,应该属于纯渲染组件(木偶组件),我们可以使用 React 轻量的无状态组件的方式来声明:</p>
<pre><code class="react">...
const Button = ({ className, children, onClick }) => (
<button
type="button"
className={cn(styles.normal, className)}
onClick={onClick}
>
{children}
</button>
);</code></pre>
<p>是不是觉得酷炫很多!</p>
<p>另外,从 <code>Input</code> 组件的示例代码中看到,我们使用了<a href="https://link.segmentfault.com/?enc=6gLZpYtqJLmFYaIBm6%2Bjzw%3D%3D.4OuY0hn90sBi4HkC1Nhh8vSKLKcL6PTx6wsA0kzeQeM982OfF2XfhWw6ldvUea9Au0SPRbnIXeC77ep0kzj8xA%3D%3D" rel="nofollow">非受控组件</a>,这里是为了降低示例代码的复杂度而特意为之,大家可以根据自己的实际情况来决定是否需要设计成<a href="https://link.segmentfault.com/?enc=gb5qGLVgQTErZ5QgRbEdTQ%3D%3D.tNjrBkSG7WGO9%2B8gJcrBipg7tLEDbPESd1Dh%2BlnDyyblCDUg0pCrBAfaeRUPogXlmLi%2FIzdh%2Fz9CUR30y2FuOQ%3D%3D" rel="nofollow">受控组件</a>。一般情况下,如果不需要获取实时输入值的话,我觉得使用非受控组件应该够用了。</p>
<p>我们再回到上面的 <code>TodoList</code> 组件,将之前分离的子组件 <code>Button</code>,<code>Input</code> 组装进来。</p>
<pre><code class="react">...
import Button from 'components/Button';
import Input from 'components/Input';
...
export default class TodoList extends Component {
render() {
return (
<div className={styles.container}>
<header className={styles.header}>
<Input
className={styles.input}
inputRef={(input) => this.input = input}
/>
<Button onClick={() => this.handleAdd()}>
Add Todo
</Button>
</header>
...
</div>
);
}
}
...</code></pre>
<h3>拆分子组件</h3>
<p>然后继续接着看 <code>TodoList</code> 的 items 部分,我们注意到这部分包含了较多的渲染逻辑在 <code>render</code> 中,导致我们需要浪费对这段代码与上下文之间会有过多的思考,所以,我们何不把它抽离出去:</p>
<pre><code class="react">...
export default class TodoList extends Component {
render() {
return (
<div className={styles.container}>
...
<section className={styles.content}>
{this.renderItems()}
</section>
</div>
);
}
renderItems() {
return (
<ul className={styles.items}>
{this.state.todos.map((todo, i) => (
<li key={`${todo}-${i}`}>
...
</li>
))}
</ul>
);
}
...
}</code></pre>
<p>上面的代码看似降低了 <code>render</code> 的复杂度,但仍然没有让 <code>TodoList</code> 减少负担。既然我们要把这部分逻辑分离出去,我们何不创建一个 <code>Todos</code> 组件,把这部分逻辑拆分出去呢?so,我们以“<strong>就近声明</strong>”的原则在 <code>components/TodoList/</code> 目录下创建一个子目录 <code>components/TodoList/components/</code> 来存放 <code>TodoList</code> 的子组件 。why?因为我觉得 组件 <code>Todos</code> 跟 <code>TodoList</code> 有紧密的父子关系,且跟其他组件间也不太会有任何交互,也可以认为它是 <code>TodoList</code> 私有的。</p>
<p>然后我们预览下现在的目录结构:</p>
<pre><code class="diff">.
├── components
│ ...
│ └── TodoList
+│ ├── components
+│ │ └── Todos
+│ │ ├── index.js
+│ │ └── todos.scss
│ ├── index.js
│ └── todo-list.scss</code></pre>
<p><code>Todos/index.js</code> 的代码:</p>
<pre><code class="react">...
const Todos = ({ data: todos, onStateChange, onRemove }) => (
<ul className={styles.items}>
{todos.map((todo, i) => (
<li key={`${todo}-${i}`}>
<em
className={todo.completed ? styles.completed : ''}
onClick={() => onStateChange(i)}
>
{todo.text}
</em>
<Button onClick={() => onRemove(i)}>
Remove
</Button>
</li>
))}
</ul>
);
...</code></pre>
<p>再看拆分后的 <code>TodoList/index.js</code> :</p>
<pre><code class="react">render() {
return (
<div className={styles.container}>
...
<section className={styles.content}>
<Todos
data={this.state.todos}
onStateChange={(index) => this.handleStateChange(index)}
onRemove={(index) => this.handleRemove(index)}
/>
</section>
</div>
);
}</code></pre>
<h3>增强子组件</h3>
<p>到目前为止,大体上的功能已经搞定,子组件看上去拆分的也算合理,这样就可以很容易的增强某个子组件的功能了。就拿 <code>Todos</code> 来说,在新增了一个 TODO 后,假如我们并没有完成这个 TODO,而我们又希望可以修改它的内容了。ha~不要着急,要不我们再拆分下这个 <code>Todos</code>,比如增加一个 <code>Todo</code> 组件:</p>
<pre><code class="diff">.
├── components
│ ...
│ └── TodoList
│ ├── components
+│ │ ├── Todo
+│ │ │ ├── index.js
+│ │ │ └── todo.scss
│ │ └── Todos
│ │ ├── index.js
│ │ └── todos.scss
│ ├── index.js
│ └── todo-list.scss</code></pre>
<p>先看下 <code>Todos</code> 组件在抽离了 <code>Todo</code> 后的样子:</p>
<pre><code class="react">...
import Todo from '../Todo';
...
const Todos = ({ data: todos, onStateChange, onRemove }) => (
<ul className={styles.items}>
{todos.map((todo, i) => (
<li key={`${todo}-${i}`}>
<Todo
{...todo}
onClick={() => onStateChange(i)}
/>
<Button onClick={() => onRemove(i)}>
Remove
</Button>
</li>
))}
</ul>
);
export default Todos;
</code></pre>
<p>我们先不关心 <code>Todo</code> 内是何如实现的,就如我们上面说到的那样,我们需要对这个 <code>Todo</code> 增加一个可编辑的功能,从单纯的属性配置入手,我们只需要给它增加一个 <code>editable</code> 的属性:</p>
<pre><code class="diff"><Todo
{...todo}
+ editable={editable}
onClick={() => onStateChange(i)}
/></code></pre>
<p>然后,我们再思考下,在 <code>Todo</code> 组件的内部,我们需要重新组织一些功能逻辑:</p>
<ul>
<li><p>根据传入的 <code>editable</code> 属性来判断是否需要显示编辑按钮</p></li>
<li><p>根据组件内部的编辑状态,是显示文本输入框还是文本内容</p></li>
<li><p>点击“更新”按钮后,需要通知父组件更新数据列表</p></li>
</ul>
<p>我们先来实现下 <code>Todo</code> 的第一个功能点:</p>
<pre><code class="react">render() {
const { completed, text, editable, onClick } = this.props;
return (
<span className={styles.wrapper}>
<em
className={completed ? styles.completed : ''}
onClick={onClick}
>
{text}
</em>
{editable &&
<Button>
Edit
</Button>
}
</span>
);
}</code></pre>
<p>显然实现这一步似乎没什么 luan 用,我们还需要点击 Edit 按钮后能显示 <code>Input</code> 组件,使内容可修改。所以,简单的传递属性似乎无法满足该组件的功能,我们还需要一个内部状态来管理组件是否处于编辑中:</p>
<pre><code class="react">render() {
const { completed, text, editable, onStateChange } = this.props,
{ editing } = this.state;
return (
<span className={styles.wrapper}>
{editing ?
<Input
value={text}
className={styles.input}
inputRef={input => this.input = input}
/> :
<em
className={completed ? styles.completed : ''}
onClick={onStateChange}
>
{text}
</em>
}
{editable &&
<Button onClick={() => this.handleEdit()}>
{editing ? 'Update' : 'Edit'}
</Button>
}
</span>
);
}</code></pre>
<p>最后,<code>Todo</code> 组件在点击 Update 按钮后需要通知父组件更新数据:</p>
<pre><code class="javascript">handleEdit() {
const { text, onUpdate } = this.props;
let { editing } = this.state;
editing = !editing;
this.setState({ editing });
if (!editing && this.input.value !== text) {
onUpdate(this.input.value);
}
}</code></pre>
<p>需要注意的是,我们传递的是更新后的内容,在数据没有任何变化的情况下通知父组件是毫无意义的。</p>
<p>我们再回过头来修改下 <code>Todos</code> 组件对 <code>Todo</code> 的调用。先增加一个由 <code>TodoList</code> 组件传递下来的回调属性 <code>onUpdate</code>,同时修改 <code>onClick</code> 为 <code>onStateChange</code>,因为这时的 <code>Todo</code> 已不仅仅只有单个点击事件了,需要定义不同状态变更时的事件回调:</p>
<pre><code class="diff"><Todo
{...todo}
editable={editable}
- onClick={() => onStateChange(i)}
+ onStateChange={() => onStateChange(i)}
+ onUpdate={(value) => onUpdate(i, value)}
/></code></pre>
<p>而最终我们又在 <code>TodoList</code> 组件中,增加 <code>Todo</code> 在数据更新后的业务逻辑。</p>
<p><code>TodoList</code> 组件的 <code>render</code> 方法内的部分示例代码:</p>
<pre><code class="diff"><Todos
editable
data={this.state.todos}
+ onUpdate={(index, value) => this.handleUpdate(index, value)}
onStateChange={(index) => this.handleStateChange(index)}
onRemove={(index) => this.handleRemove(index)}
/></code></pre>
<p><code>TodoList</code> 组件的 <code>handleUpdate</code> 方法的示例代码:</p>
<pre><code class="javascript">handleUpdate(index, value) {
let todos = [...this.state.todos];
const target = todos[index];
todos = [
...todos.slice(0, index),
{
text: value,
completed: target.completed
},
...todos.slice(index + 1)
];
this.setState({ todos });
}</code></pre>
<h2>组件数据管理</h2>
<p>既然 <code>TodoList</code> 是一个组件,初始状态 <code>this.state.todos</code> 就有可能从外部传入。对于组件内部,我们不应该过多的关心这些数据从何而来(可能通过父容器直接 Ajax 调用后返回的数据,或者 Redux、MobX 等状态管理器获取的数据),我觉得组件的数据属性的设计可以从以下 3 个方面来考虑:</p>
<ul>
<li><p>在没有初始数据传入时应该提供一个默认值</p></li>
<li><p>一旦数据在组件内部被更新后应该及时的通知父组件</p></li>
<li><p>当有新的数据(从后端 API 请求的)传入组件后,应该重新更新组件内部状态</p></li>
</ul>
<p>根据这几点,我们可以对 <code>TodoList </code> 再做一番改造。</p>
<p>首先,对 <code>TodoList</code> 增加一个 <code>todos</code> 的默认数据属性,使父组件在没有传入有效属性值时也不会影响该组件的使用:</p>
<pre><code class="javascript">export default class TodoList extends Component {
constructor(props) {
super(props);
this.state = {
todos: props.todos
};
}
...
}
TodoList.defaultProps = {
todos: []
};</code></pre>
<p>然后,再新增一个内部方法 <code>this.update</code> 和一个组件的更新事件回调属性 <code>onUpdate</code>,当数据状态更新时可以及时的通知父组件:</p>
<pre><code class="javascript">export default class TodoList extends Component {
...
handleAdd() {
...
this.update(todos);
}
handleUpdate(index, value) {
...
this.update(todos);
}
handleRemove(index) {
...
this.update(todos);
}
handleStateChange(index) {
...
this.update(todos);
}
update(todos) {
const { onUpdate } = this.props;
this.setState({ todos });
onUpdate && onUpdate(todos);
}
}</code></pre>
<p>这就完事儿了?No! No! No! 因为 <code>this.state.todos</code> 的初始状态是由外部 <code>this.props</code> 传入的,假如父组件重新更新了数据,会导致子组件的数据和父组件不同步。那么,如何解决?</p>
<p>我们回顾下 <a href="https://link.segmentfault.com/?enc=VOW7hIJ0s36zi4PPXs3A%2FQ%3D%3D.zYAzZPIR%2FmZewmS9KE6y9T%2BtTYcokIzcNBdJI4iWXfBSSg%2FPUuIENxcjwF13Ht5CTXNU%2FimyvwzBHxXARH3wrNqTKo%2BYQIy2z8S65du8J7I%3D" rel="nofollow">React 的生命周期</a>,父组件传递到子组件的 props 的更新数据可以在 <code>componentWillReceiveProps</code> 中获取。所以我们有必要在这里重新更新下 <code>TodoList</code> 的数据,哦!千万别忘了判断传入的 todos 和当前的数据是否一致,因为,当任何传入的 props 更新时都会导致 <code>componentWillReceiveProps</code> 的触发。</p>
<pre><code class="javascript">componentWillReceiveProps(nextProps) {
const nextTodos = nextProps.todos;
if (Array.isArray(nextTodos) && !_.isEqual(this.state.todos, nextTodos)) {
this.setState({ todos: nextTodos });
}
}</code></pre>
<p>注意代码中的 <code>_.isEqual</code>,该方法是 <a href="https://link.segmentfault.com/?enc=8BIaQG6OBHtaLFS0LNRwug%3D%3D.n3TEz2eYGNIkAPRqcseJG9b8mUeJrcvon5URQj%2BNpWE%3D" rel="nofollow">Lodash</a> 中非常实用的一个函数,我经常拿来在这种场景下使用。</p>
<h2>结尾</h2>
<p>由于本人对 React 的了解有限,以上示例中的方案可能不一定最合适,但你也看到了 <code>TodoList</code> 组件,既可以是包含多个不同功能逻辑的大组件,也可以拆分为独立、灵巧的小组件,我觉得我们只需要掌握一个度。当然,如何设计取决于你自己的项目,正所谓:<strong>没有最好的,只有更合适的</strong>。还是希望本篇文章能给你带来些许的小收获。</p>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=Eio92VAcAImcrfFuVVEnWA%3D%3D.NWC2nN9ut%2FZh4JDSqfNIg2u1iAT6DYtkU84aauUMU9o%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
iKcamp出品微信小程序教学共5章16小节汇总(含视频)
https://segmentfault.com/a/1190000011871952
2017-11-06T10:52:05+08:00
2017-11-06T10:52:05+08:00
iKcamp
https://segmentfault.com/u/ikcamp
8
<h2>?? 微信小程序课程,面向所有具备前端基础知识的同学 ??</h2>
<h3>阅读要求</h3>
<blockquote><p>读者需要具备但不限于以下技能</p></blockquote>
<ul>
<li>HTML</li>
<li>JavaScript <code>es6更佳</code>
</li>
<li>CSS</li>
</ul>
<p>一共四部分十五小节,适合七天的训练营。 <br>从现在开始,我假装你已经掌握了 <code>html</code>、 <code>css</code>以及 <code>ES6</code> ☝️ </p>
<h3>目标导向</h3>
<blockquote><p>本教程以实战项目驱动,以沪江英语微信小程序项目框架为基础,最终还原一个完整的小程序</p></blockquote>
<p>列表页面:请求接口,并展示列表页面数据</p>
<p><img src="/img/remote/1460000011871957?w=750&h=1296" alt="" title=""></p>
<p>详情页面:以文章id为参数,获取文章详情 </p>
<p><img src="/img/remote/1460000011871958?w=754&h=1290" alt="" title=""></p>
<p>详情页面,点击图片,展示大图</p>
<p><img src="/img/remote/1460000011871959?w=752&h=1292" alt="" title=""></p>
<h3>教程大纲 - <a href="https://link.segmentfault.com/?enc=TXpOhoHH%2Fi3h%2FGzSHiBIRA%3D%3D.zM90d9boFQKZBfMiFWqsfDtniXxwsA6Xz1G7DZQKJiM%3D" rel="nofollow">完整视频地址</a>
</h3>
<ul>
<li>
<p>第一章:小程序初级入门教程</p>
<ul>
<li>
<p>appID准备</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=gw%2Bw%2F%2BNHGSstx0wBLXvvew%3D%3D.Tpop11pQJeS8GIoeKz1nR39lPL7DpADNOUfJO%2BPWzI8418Yj%2F6psrqq53w2E%2FBOm" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>工具安装</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=%2FI8kZAdxC5Y4sa5DMlJDlQ%3D%3D.n1N2KZUXluHdEFOy%2BWITnx4ICFONIpd61jqyPt%2FltUfCJEc7friVGQhwD2OBcdli" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>目录说明</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=MOghpMJlPfjwQGO0zy0j7Q%3D%3D.tP8U8KvrqkK4Zi2gRs20Bu6rXXbJRrJ6ylaZY%2B%2F3P8RFByJWs0T%2FV7LiGVmqhy4S" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>小试牛刀</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=wlpuuRFEA8pNjheJe6bvPg%3D%3D.V8CBX3nr%2FU1TEgtGQ4Piv3TOj3%2BhEZQofowRwLqXK%2BeUs8vyU8G6gAQZmYVkX5lt" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>发布流程</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=GKxJDuE4HgS%2FryqVF0HRzA%3D%3D.J3mZyUeIEMUmonwvUY3c1ybhW41Wy7qXeEOXiwCRs8ORn4RWLQUn8o5K7eGKVwGp" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
</ul>
</li>
<li>
<p>第二章:小程序中级实战教程:预备篇</p>
<ul>
<li>
<p>项目结构设计</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=YwJbvTmcSGZ2bhURWyHgtg%3D%3D.EuZ6ZnupWBz6TNeEgfmcRJdgbRpyW7uCCnXZWLmkFFmvlbrgCDVG9AhBpZvNgnC6" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>提取 util 公用方法</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=Hog4tTOp5XwjzAfiGCXdGw%3D%3D.tgYXP1QFYQPJIAxFTt%2F7WQjXW8GrdjrusH9c5g4Mq7mORrToAh6j%2FZNF%2BY3V960p" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>封装网络请求及 mock 数据</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=Gy6gKzgJyVZsLnCaS5Lwtw%3D%3D.YbqbIRVFEcVbwiqTHtbE%2B5U%2FLkMC%2Bps6%2Fbch0OZ2gKi4EliFcKa3DoDsCTgHv7sR" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
</ul>
</li>
<li>
<p>第三章:小程序中级实战教程:列表篇</p>
<ul>
<li>
<p>列表-静态页面制作</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=rWkJeQU8RF%2BLsL1NH%2F9Qsw%3D%3D.5i90Jck4vtloJAcfk55J34CmCAOhlkU9ja7O2qnilwjNIJn9jYE2agPoXqlL0YpP" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>列表-页面逻辑处理</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=QhZwNdsj%2FZKsT6lpSSMdtw%3D%3D.hVOmL4kRlZwLHpcFtsJzk1tNq2jxMhS0FsO925hEM%2B%2FW6I0YkV1Z8J425PFzpwMw" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>列表-视图与数据关联</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=lQ8jgWPIeP0rrfQu59Y7RQ%3D%3D.xrJ1HedYym4CKQBvTgS22iFreRKKz7BV%2FFeb7sBurK%2F4rPeVWRwOVFewKRba4gW2" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>列表-阅读标识</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=%2Fruz3dDAGBfhP%2FVENrEqCA%3D%3D.5fb61lbaP3ZZSS%2Fe5HgEa1XpCaJPUURi1zvA3QtPUcQGObF58VjNUX1YdMzJuy9Y" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
</ul>
</li>
<li>
<p>第四章:小程序中级实战教程:详情篇</p>
<ul>
<li>
<p>详情-页面制作</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=Am75ErR27TBciDws5lBwlQ%3D%3D.%2BvoaOXGHQGW%2Bwd5Ukgl36XBYHJT49NDl1jfvlb15ot5rgL1pb8xBxAiOLJvKxjmP" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>详情-视图渲染</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=DVTt2ybCW%2B8OrImoNK%2Bzug%3D%3D.Mdt9qkp3IIAHxrysIfOQL5AUNJmfg%2FjhlN3lI25Ydvt%2FYRjhIC6mwLiJ5qEoTaZu" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
<li>
<p>详情-功能完善</p>
<ul><li>文章视频:<a href="https://link.segmentfault.com/?enc=kp6VjemosGuakverdPrTWA%3D%3D.C9jW0%2B3Ixea2R4DdWAN2FlbmeYTX46nOsFcyYEd0rY9QvYaCMrTUynKNHSQGRDOA" rel="nofollow">https://camp.qianduan.group/x...</a>
</li></ul>
</li>
</ul>
</li>
<li>第五章:<a href="https://link.segmentfault.com/?enc=r9Fz2x%2B84tVtOw0TpuegQg%3D%3D.NHvYsSRbySwuhrzhyg6pLQuHvME%2F5p1hlA0uS6TZf68hRkKPiY7BNn4vz0Q5riGg2DWUmiWL4CugWK5ITPqLrK4pLbfzRGhQFoyWN73%2BV6AStCKMlmrvufmm%2BHOTSCsU" rel="nofollow">课后作业练习</a>
</li>
</ul>
<h3>学习方式</h3>
<p>教程以 <code>git</code> 分支的方式管理,比如学习第三章第一节 <code>列表-静态页面制作</code> 时候,需要切换到 <code>ch3-1</code> 分支,然后把分支目录 <code>code/</code> 导入到微信开发工具编辑器,跟随教程进行实战代码操作。操作如下:</p>
<ol><li>通过 <code>git</code> 把项目复制到本地</li></ol>
<pre><code class="git">git clone https://github.com/ikcamp/wechat-xcx-tutorial</code></pre>
<ol><li>切换目录</li></ol>
<pre><code>cd wechat-xcx-tutorial</code></pre>
<ol><li>在当前目录下切换分支</li></ol>
<pre><code>git checkout ch3-1</code></pre>
<p><strong>注意:</strong> 每一分支的 <code>code/</code> 内容,皆是上一节内容操作完成后的结果。比如 <code>ch3-2/code</code> 就是上一节课程 <code>ch3-1/code</code> 随教程操作后的结果。 </p>
<h3><a>§ 教程完整代码</a></h3>
<blockquote><p>教程的完整代码在分支 <code>完整代码</code> 中,请自行<a href="https://link.segmentfault.com/?enc=C%2FgBeGhOrTBehnPq5OqF0A%3D%3D.C0lp47BzUYh1UiAUV%2BEBTgJXLKhcIlBfV8DDrcjz58UDq7hggIB6rBRqvbu7GXqZJj2VKOrd0Y4njiFJUr%2BSLxV6EkdEloW4MhsJs77ZWZMkawOQLYwNmKwD2INSXN%2Fe" rel="nofollow">查阅</a></p></blockquote>
<h3>课后作业</h3>
<blockquote><p>整个教程学习过后,可以切换到 <code>教程作业</code> 分支,并完成作业,目录下有相应的作业答案,请自行完成<a href="https://link.segmentfault.com/?enc=z5%2B3nquHxi2zedhoAnAgNQ%3D%3D.YaseRDePfz4U6%2B4LNG%2F36w3upgDVDhSmibbSOs%2F2lCocOJpmdlTspUSrr%2Be%2FTYxrKJoTsXMlK7BoXdmuuncd1jV60uwSqJp031T1H7FxxZ36Y2%2F7MAdWFMHy6IDrhxA3" rel="nofollow">练习</a>。</p></blockquote>
<h3>核心人员</h3>
<p><img src="/img/bVXYAz?w=1336&h=612" alt="图片描述" title="图片描述"></p>
<h3>讨论活动区</h3>
<p>QQ群:661407609</p>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=zHsUH1Dq4rNi2r92UgThmg%3D%3D.0KyMZGZ5wYoI2gdJ4L%2FbNiu0wCqAxrw%2BosnVvT4YH4k%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h3>【11月11号】上海iKcamp最新活动</h3>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=bUsYzeS9tL%2Fh%2FBoDRo7toA%3D%3D.zefy5c1PlLrp62915ymm3VbB8vsjLDhe5%2FhUworWzax1Ab2xayc1rbkflEYmKEv2" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
<h3>合作社区</h3>
<p><img src="/img/remote/1460000011872139?w=448&h=90" alt="" title=""></p>
<blockquote><p>Node.js 免费视频培训课程近期制作完毕,敬请期待,视频地址:<a href="https://link.segmentfault.com/?enc=ohgYk31jjSvWWmWMcFxp8Q%3D%3D.24TNVwGT%2BS567NBqAhwCx6VEn0C6DxmcROqF%2FDm49Rs%3D" rel="nofollow">https://camp.qianduan.group/</a></p></blockquote>
微信小程序教学第四章第三节(含视频):小程序中级实战教程:详情-功能完善
https://segmentfault.com/a/1190000011841887
2017-11-03T11:15:07+08:00
2017-11-03T11:15:07+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<h2>详情 - 功能完善</h2>
<blockquote><p>本文配套视频地址:<br><a href="https://link.segmentfault.com/?enc=J4vLJkdHvFtFKpurOSTlFA%3D%3D.RMhYCU%2FtvXpLXnJlJV%2B1qhVTY4h0HFcwiegUR8wEKY6oyU%2F0Ub9wOJOuaXNGivo8" rel="nofollow">https://v.qq.com/x/page/f0555...</a></p></blockquote>
<hr>
<blockquote><p>开始前请把 <code>ch4-3</code> 分支中的 <code>code/</code> 目录导入微信开发工具 <br>这一节中,我们把详情的其他功能完善起来:下一篇、 分享、 返回列表。</p></blockquote>
<p><br></p>
<h3>Step 1. 增加 <code>下一篇</code> 功能</h3>
<p>增加 <code>下一篇</code> 的功能,我们需要在视图中绑定一个事件,来触发代码中的响应函数,此函数会调用接口,返回下一篇文章内容数据。 </p>
<p><br></p>
<p>1、修改视图文件 <code>detail.wxml</code>,增加相应的绑定事件</p>
<pre><code class="html"><button class="footbar-btn clearBtnDefault" bindtap="next">下一篇</button></code></pre>
<p><br></p>
<p>2、修改代码 <code>detail.js</code>,增加绑定事件对应的 <code>next</code> 及相关函数:</p>
<pre><code class="js">next(){
this.requestNextContentId()
.then(data => {
let contentId = data && data.contentId || 0;
this.init(contentId);
})
},
requestNextContentId () {
let pubDate = this.data.detailData && this.data.detailData.lastUpdateTime || ''
let contentId = this.data.detailData && this.data.detailData.contentId || 0
return util.request({
url: 'detail',
mock: true,
data: {
tag:'微信热门',
pubDate: pubDate,
contentId: contentId,
langs: config.appLang || 'en'
}
})
.then(res => {
if (res && res.status === 0 && res.data && res.data.contentId) {
util.log(res)
return res.data
} else {
util.alert('提示', '没有更多文章了!')
return null
}
})
}</code></pre>
<p><br></p>
<p>大概介绍下这两个函数: <br>点击触发 <code>next</code> 函数,它会先调用 <code>requestNextContentId</code>,通过把当前文章的 <code>lastUpdateTime</code> 和 <code>contentId</code> 参数传递给后端,后端会返回下一篇文章的 <code>contentId</code>,这样我们就知道了文章 Id,然后就像刚开始一样,把 <code>contentId</code> 再次传递给 <code>init(contentId)</code> 函数,获取文章的详情数据,然后是渲染视图…… </p>
<p><br></p>
<p>这个时候,可能你已经发现了一个用户体验上的 <code>bug</code>:当页面滚动到一定程度后点击下一篇,新的页面没有滚动到顶部。所以我们需要修复这个 <code>bug</code>,当文章更新后,正常情况下,页面应该滚动到顶部,也就是滚动条在最开始位置。现在我们开始修复它: </p>
<p><br></p>
<p><code>scroll-view</code> 组件有个属性 <code>scroll-top</code>,这个属性代表着滚动条当前的位置,也就是说,当它的值为 0 时候,滚动条在最顶部,所以我们需要在数据 <code>data</code> 中记录这个值,当需要文章滚动到页面顶部时候,我们只需要修改 <code>scroll-top</code> 的值就可以了。 <br>这里我们用 <code>scrollTop</code> 来表示:</p>
<pre><code class="js">// 修改 detail.js 的数据 data
data:{
scrollTop: 0,
detailData: {}
}</code></pre>
<p><br></p>
<p>修改视图文件,注意增加 <code>enable-back-to-top</code> 属性,作用就不解释了,直接看属性名的单词应该就明白:</p>
<pre><code class="html"><scroll-view scroll-y="true" scroll-top="{{ scrollTop }}" enable-back-to-top="true" class="root-wrap"></code></pre>
<p><br></p>
<p>当我们需要文章返回到顶部时候,只要设置这个变量值就可以了。这里我们对赋值操作提取出单独的函数:</p>
<pre><code class="js">goTop () {
this.setData({
scrollTop: 0
})
}</code></pre>
<p><br></p>
<p>在函数 <code>init()</code> 中调用:</p>
<pre><code class="js">init (contentId) {
if (contentId) {
this.goTop()
this.requestDetail(contentId)
.then(data => {
this.configPageData(data);
})
//调用wxparse
.then(()=>{
this.articleRevert();
});
}
}</code></pre>
<p><br></p>
<h3>Step 2. 增加 <code>分享</code> 功能</h3>
<p>调用小程序会对分享事件做监听,如果触发分享功能后,监听事件会返回一个对象,包含了分享出去的信息内容,并且可以分别对分享成功和分享失败做处理</p>
<pre><code class="html"><!--
<button class="footbar-share clearBtnDefault">
<view class="icon footbar-share-icon"></view>
</button>
-->
<button class="footbar-share clearBtnDefault" open-type="share">
<view class="icon footbar-share-icon"></view>
</button></code></pre>
<p><br></p>
<p><code>button</code> 组件增加了 <code>open-type="share"</code> 属性后,就可以触发 <code>onShareAppMessage</code> 监听事件:</p>
<pre><code class="js">onShareAppMessage () {
let title = this.data.detailData && this.data.detailData.title || config.defaultShareText;
let contentId = this.data.detailData && this.data.detailData.contentId || 0;
return {
// 分享出去的内容标题
title: title,
// 用户点击分享出去的内容,跳转的地址
// contentId为文章id参数;share参数作用是说明用户是从分享出去的地址进来的,我们后面会用到
path: `/pages/detail/detail?share=1&contentId=${contentId}`,
// 分享成功
success: function(res) {},
// 分享失败
fail: function(res) {}
}
},</code></pre>
<p><br></p>
<p>这里需要我们注意下,此接口对部分版本不支持,所以如果版本不支持时候,我们要给用户一个提示信息。所以我们需要给分享按钮另外绑定一个点击事件,如果不支持的话,提示用户:</p>
<pre><code class="js">notSupportShare () {
// deviceInfo 是用户的设备信息,我们在 app.js 中已经获取并保存在 globalData 中
let device = app.globalData.deviceInfo;
let sdkVersion = device && device.SDKVersion || '1.0.0';
return /1\.0\.0|1\.0\.1|1\.1\.0|1\.1\.1/.test(sdkVersion);
},
share () {
if (this.notSupportShare()) {
wx.showModal({
title: '提示',
content: '您的微信版本较低,请点击右上角分享'
})
}
}</code></pre>
<p><br></p>
<p>在视图中绑定 <code>share</code> 监听事件:</p>
<pre><code class="html"><!--
<button class="footbar-share clearBtnDefault" open-type="share">
<view class="icon footbar-share-icon"></view>
</button>
-->
<button class="footbar-share clearBtnDefault" bindtap="share" open-type="share">
<view class="icon footbar-share-icon"></view>
</button></code></pre>
<p><br></p>
<h3>Step 3. 增加 <code>返回列表</code> 功能</h3>
<p>我们需要在 <code>detail.js</code> 中增加一个返回列表的函数,然后视图中绑定触发事件</p>
<pre><code class="js">// detail.js 增加以下内容
Page({
back(){
wx.navigateBack()
}
})</code></pre>
<p><br></p>
<p>在视图中绑定事件:</p>
<pre><code class="html"><!--
<button class="footbar-back clearBtnDefault">
<view class="icon footbar-back-icon"></view>
</button>
-->
<button class="footbar-back clearBtnDefault" bindtap="back">
<view class="icon footbar-back-icon"></view>
</button></code></pre>
<p><br></p>
<p>由于 <code>wx.navigateBack</code> 相当于浏览器的 <code>history</code>,通过浏览记录返回的。那么如果用户并不是从列表进来的,比如是从分享出去的详情打开呢?这时候记录是不存在的。 </p>
<p><br></p>
<p>如果是通过分享进来的,有带进来参数 <code>share=1</code>,如 <code>step 2</code> 中的分享功能,那么我们在刚进到页面时候,就可以通过 <code>share=1</code> 是否存在来标识出来:</p>
<pre><code class="js">onLoad (option) {
let id = option.contentId || 0;
this.setData({
isFromShare: !!option.share
});
this.init(id);
},</code></pre>
<p><br></p>
<p>然后修改 <code>back</code> 函数,如果是从分享入口进来的,那么我们就通过导航的方式来返回列表;如果不是,就正常的通过加载记录来返回:</p>
<pre><code class="js">// detail.js 增加以下内容
Page({
back(){
if (this.data.isFromShare) {
wx.navigateTo({
url: '../index/index'
})
} else {
wx.navigateBack()
}
}
})</code></pre>
<p><br></p>
<p>至此,我们简单的小程序实战已经完成!!!</p>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=K0zIidBYX729yXPcam8%2F3A%3D%3D.Qp%2Bg5KcqKGSTnKTYEBT2finJK90yeHe7WxKiaEBptmI%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h2>【11月11号】上海iKcamp最新活动</h2>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=b2Zi3aXvCF89WgbDMCMtaA%3D%3D.tYkmR3sqc349xcQSv7JhpszP8WPo27USqmCMoR5fmolDmt%2Fxc5URn9l%2FEegWD6fR" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
Callback 与 Promise 间的桥梁 —— promisify
https://segmentfault.com/a/1190000011815540
2017-11-01T18:14:03+08:00
2017-11-01T18:14:03+08:00
iKcamp
https://segmentfault.com/u/ikcamp
1
<blockquote><p>作者:晃晃<br>本文原创,转载请注明作者及出处</p></blockquote>
<p>Promise 自问世以来,得到了大量的应用,简直是 javascript 中的神器。它很好地解决了异步方法的回调地狱、提供了我们在异步方法中使用 return 的能力,并将 callback 的调用纳入了自己的管理,而不是交给异步函数后我们就无能为力了(经常有 callback 被莫名调用两次而导致程序出错)。</p>
<p>今天要介绍的是 Promisify,就是回调函数与 Promise 间的桥梁。</p>
<h2>1. promisify 介绍</h2>
<p>什么是 promisify 呢?顾名思义,就是“promise 化”,将一个不是promise的方法变成 promise 。举个例子:</p>
<pre><code class="javascript">// 原有的callback调用
fs.readFile('test.js', function(err, data) {
if (!err) {
console.log(data);
} else {
console.log(err);
}
});
// promisify后
var readFileAsync = promisify(fs.readFile);
readFileAsync('test.js').then(data => {
console.log(data);
}, err => {
console.log(err);
});</code></pre>
<p>这两个方法效果上是等价的,但是从掌控性来说的话,我更喜欢后面的写法。</p>
<p>那么什么样的方法可以通过 promisify 变成 promise 呢?这里就需要介绍一个名词,nodeCallback。什么样的 callback 叫 nodeCallback ?</p>
<p>nodeCallback 有两个条件:1. 回调函数在主函数中的参数位置必须是最后一个;2. 回调函数参数中的第一个参数必须是 error 。举个例子:</p>
<ol><li>回调函数在主函数中的参数位置</li></ol>
<pre><code class="javascript">// 正确
function main(a, b, c, callback) {
}
// 错误
function main(callback, a, b, c) {
}</code></pre>
<ol><li>回调函数参数中的第一个参数必须是 error</li></ol>
<pre><code class="javascript">// 正确
function callback(error, result1, result2) {
}
// 错误
function callback(result1, result2, error) {
}</code></pre>
<p>这样,通过 nodeCallback ,我们定义了一个能被 promisify 的函数的格式,即,满足 nodeCallback 形式的方法,我们可以通过 promisify 来让它变成一个返回 promise 的方法。</p>
<h2>2. promisify 的实现</h2>
<p>下面我们来根据上述条件来手动实现一个 promisify 。</p>
<p>首先 promisify 需要返回一个 function ,并且这个 function 要返回一个 promise</p>
<pre><code class="javascript">var promisify = (func) => {
return function() {
var ctx = this;
return new Promise(resolve => {
return func.call(ctx, ...arguments);
})
}
}</code></pre>
<p>其次,原 func 的最后一个参数是 callback</p>
<pre><code class="javascript">var promisify = (func) => {
return function() {
var ctx = this;
return new Promise(resolve => {
return func.call(ctx, ...arguments, function() {
resolve(arguments);
});
})
}
}</code></pre>
<p>然后,回调函数中的第一个参数是 error 标记</p>
<pre><code class="javascript">var promisify = (func) => {
return function() {
var ctx = this;
return new Promise((resolve, reject) => {
return func.call(ctx, ...arguments, function() {
var args = Array.prototype.map.call(arguments, item => item);
var err = args.shift();
if (err) {
reject(err);
} else {
resolve(args);
}
});
})
}
}</code></pre>
<p>最后,做一些优化,比如 this 作用域的自定义、回参只有一个时不返回数组</p>
<pre><code class="javascript">var promisify = (func, ctx) => {
// 返回一个新的function
return function() {
// 初始化this作用域
var ctx = ctx || this;
// 新方法返回的promise
return new Promise((resolve, reject) => {
// 调用原来的非promise方法func,绑定作用域,传参,以及callback(callback为func的最后一个参数)
func.call(ctx, ...arguments, function() {
// 将回调函数中的的第一个参数error单独取出
var args = Array.prototype.map.call(arguments, item => item);
var err = args.shift();
// 判断是否有error
if (err) {
reject(err)
} else {
// 没有error则将后续参数resolve出来
args = args.length > 1 ? args : args[0];
resolve(args);
}
});
})
};
};</code></pre>
<p>测试</p>
<pre><code class="javascript">// nodeCallback方法func1
var func1 = function(a, b, c, callback) {
callback(null, a+b+c);
}
// promise化后的func2
var func2 = promisify(func1);
// 调用后输出6
func1(1, 2, 3, (err, reuslt) => {
if (!err) {
console.log(result); //输出6
}
})
func2(1, 2, 3).then(console.log); //输出6</code></pre>
<p>以上便是 promisify 的介绍和实现,事实上有很多用 callback 来实现异步的第三方库提供的方法都是按照 nodeCallback 格式的,所以它们都可以通过 promisify 来让它变成 promise ,在遇到这些方法的时候就可以更灵活地使用啦。</p>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=pbmk7c0G%2BDhoJYl%2BgVFAug%3D%3D.VzqxQN3lM6VfR2Ogcr%2F1fzoruARa1OVNX7izKXRytS4%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h2>【11月11号】上海iKcamp最新活动</h2>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=mJyWYHUrf4aSD82DZQrfPg%3D%3D.hp29SSBzAHpt8%2FLXIdPgwGUDy0vl3HmyHrnPMiV4sh2%2BwntMF5HBkrPKU6kGb7%2FA" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
微信小程序教学第四章第二节(含视频):小程序中级实战教程:详情-视图渲染
https://segmentfault.com/a/1190000011809937
2017-11-01T14:18:13+08:00
2017-11-01T14:18:13+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<h2><a>§ 详情 - 数据渲染</a></h2>
<blockquote><p>本文配套视频地址:<br><a href="https://link.segmentfault.com/?enc=REGkQPhiMgjGSSE0nrYhjg%3D%3D.8goihJpGyx3Wgg4ugu0nHWBMANaShvjZofSFWwxPmYYEOI8QrycGs%2F6SOoIon0vz" rel="nofollow">https://v.qq.com/x/page/x0555...</a></p></blockquote>
<hr>
<blockquote><p>开始前请把 <code>ch4-2</code> 分支中的 <code>code/</code> 目录导入微信开发工具 <br>这一节中,我们开始详情的接口调用、数据加载和视图渲染过程。</p></blockquote>
<h3>Step 1. 引入公用的一些工具库,修改 <code>detail.js</code>:</h3>
<pre><code class="js">'use strict';
import util from '../../utils/index';
import config from '../../utils/config';
// WxParse HtmlFormater 用来解析 content 文本为小程序视图
import WxParse from '../../lib/wxParse/wxParse';
// 把 html 转为化标准安全的格式
import HtmlFormater from '../../lib/htmlFormater';
let app = getApp();
Page({
});</code></pre>
<h3>Step 2. 修改 <code>detail.js</code> 在页面初始化时候,请求接口,加载详情数据</h3>
<pre><code class="js">Page({
onLoad (option) {
/*
* 函数 `onLoad` 会在页面初始化时候加载运行,其内部的 `option` 是路由跳转过来后的参数对象。
* 我们从 `option` 中解析出文章参数 `contendId`,然后通过调用 `util` 中封装好的 `request` 函数来获取 `mock` 数据。
*/
let id = option.contentId || 0;
this.init(id);
},
init (contentId) {
if (contentId) {
this.requestDetail(contentId)
.then(data => {
util.log(data)
})
}
},
requestDetail(contentId){
return util.request({
url: 'detail',
mock: true,
data: {
source: 1
}
})
.then(res => {
return res
})
}
})</code></pre>
<p>运行之后,我们查看下控制台输出的数据,是不是很清晰! </p>
<p><img src="/img/remote/1460000011809942?w=2192&h=1306" alt="" title=""></p>
<h3>Step 3. 接着,把页面头部数据渲染出来</h3>
<p>修改 <code>requestDetail</code> 函数,并增加日期格式化的方法,达到我们想要的效果,然后重新返回数据</p>
<pre><code class="js">Page({
// 此处省略部分代码
requestDetail(contentId){
return util.request({
url: 'detail',
mock: true,
data: {
source: 1
}
})
.then(res => {
let formateUpdateTime = this.formateTime(res.data.lastUpdateTime)
// 格式化后的时间
res.data.formateUpdateTime = formateUpdateTime
return res.data
})
},
formateTime (timeStr = '') {
let year = timeStr.slice(0, 4),
month = timeStr.slice(5, 7),
day = timeStr.slice(8, 10);
return `${year}/${month}/${day}`;
}
})</code></pre>
<p>现在我们已经获取到了后端返回的数据,并且已经把部分数据标准处理过。下一步,我们把返回的数据同步到 <code>Model</code> 层中(也就是 <code>data</code> 对象中) <br>我们增加 <code>configPageData</code> 函数,用它来处理数据同步到 <code>data</code>的逻辑:</p>
<pre><code class="js">Page({
data: {
detailData: {
}
},
init (contentId) {
if(contentId) {
this.requestDetail(contentId)
.then(data => {
this.configPageData(data)
})
}
},
configPageData(data){
if (data) {
// 同步数据到 Model 层,Model 层数据发生变化的话,视图层会自动渲染
this.setData({
detailData: data
});
//设置标题
let title = this.data.detailData.title || config.defaultBarTitle
wx.setNavigationBarTitle({
title: title
})
}
}
})</code></pre>
<p>因为页面的标题是随着文章变化的,所以需要我们动态设置,这里我们调用了小程序自带的方法来设计</p>
<pre><code class="js">wx.setNavigationBarTitle({
title: '标题'
})</code></pre>
<p>修改视图 <code>detail.wxml</code> 的头部 <code>class="info"</code> 内容:</p>
<pre><code class="html"><view class="info">
<view class="info-title">{{ detailData.title }}</view>
<view class="info-desc cf">
<text class="info-desc-author fl">{{ detailData.author }}</text>
<text class="info-desc-date fr">{{ detailData.formateUpdateTime}}</text>
</view>
<view class="info-line under-line"></view>
</view></code></pre>
<h3>Step 4. 调用 <code>parse</code> 解析接口返回的 <code>content</code> 字段(文本内容)</h3>
<p>当详情数据返回后,我们已经对部分数据进行了过滤处理,现在修改 <code>detail.js</code> 中的 <code>init</code> 函数,增加对文章正文的处理:</p>
<pre><code class="js"> articleRevert () {
// this.data.detailData 是之前我们通过 setData 设置的响应数据
let htmlContent = this.data.detailData && this.data.detailData.content;
WxParse.wxParse('article', 'html', htmlContent, this, 0);
},
init (contentId) {
if (contentId) {
this.requestDetail(contentId)
.then(data => {
this.configPageData(data)
})
//调用wxparse
.then(()=>{
this.articleRevert()
})
}
},</code></pre>
<p>注意看上面的 <code>articleRevert</code> 函数,变量 <code>htmlContent</code> 指向文章的正文数据,当其传入到组件 <code>WxParse</code> 后,同时带入了 5 个参数</p>
<pre><code class="js">WxParse.wxParse('article', 'html', htmlContent, this, 0);</code></pre>
<p>第一个参数 <code>article</code> 很重要,在 <code>WxParse</code> 中,我们传入了当前对象 <code>this</code>,当变量 <code>htmlContent</code> 解析之后,会把解析后的数据赋值给当前对象,并命名为 <code>article</code> </p>
<p><img src="/img/remote/1460000011809943?w=1416&h=1168" alt="" title=""></p>
<p>所以当文章数据解析后,当前环境上下文中已经存在了数据 <code>article</code>,可以直接在 <code>detail.wxml</code> 中引用:</p>
<pre><code class="js">this.data.article</code></pre>
<p>修改 <code>detail.wxml</code>,引用我们的文章正文数据:</p>
<pre><code class="html"><!-- 先引入解析模板 -->
<import src="../../lib/wxParse/wxParse.wxml"/>
<!-- 修改文章正文节点 -->
<view class="content">
<template is="wxParse" data="{{wxParseData:article.nodes}}"/>
</view></code></pre>
<p>再看下页面效果,文章已经正常的显示了,但我们还需要优化下样式,比如增加一些行高、文字间距、字体大小颜色、图片居中等。修改样式文件 <code>detail.wxss</code>,<code>增加</code> 以下样式</p>
<pre><code class="css">.wrapper .content {
padding: 0 36rpx;
padding-bottom: 40rpx;
line-height: 56rpx;
color: #333;
font-size: 36rpx;
overflow: hidden;
word-wrap: break-word
}
.wrapper .content .langs_cn,.wrapper .content .para.translate {
font-size: 32rpx;
color: #666
}
.wrapper .content .langs_cn,.wrapper .content .langs_en,.wrapper .content .para,.wrapper .content .wxParse-p {
margin: 44rpx 0
}
.wrapper .content image {
max-width: 100%;
vertical-align: top
}
.wrapper .content .tip {
color: #999;
font-size: 28rpx;
text-align: center;
height: 28rpx;
line-height: 28rpx
}
.wrapper .content .tip-icon {
vertical-align: top;
margin-right: 8rpx;
width: 26rpx;
height: 26rpx;
border: 1px solid #999;
border-radius: 6rpx;
box-sizing: border-box
}
.wrapper .content .tip-icon.selected {
border: none;
background: url(https://n1image.hjfile.cn/mh/2017/06/12/20703f295b7b3ee4f5fe077c4e464283.png) 0 0 no-repeat;
background-size: contain
}</code></pre>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=o2a4bpqVrPq3LbfKSb84HA%3D%3D.CCCS%2Bto1WuZdG8USFjJO6X2xKGPZkh%2B61m5JBEx7f2w%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h2>【11月11号】上海iKcamp最新活动</h2>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=aUGBmGEd1On%2BN1Exefm9GA%3D%3D.j0Y8FA1xfoS%2BaDh7GYgwby6C%2FwdGczHzRBR295g3alBTSy2GFvo%2FhzsX02RXKI4O" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
微信小程序教学第四章第一节(含视频):小程序中级实战教程:详情-页面制作
https://segmentfault.com/a/1190000011791046
2017-10-31T11:52:23+08:00
2017-10-31T11:52:23+08:00
iKcamp
https://segmentfault.com/u/ikcamp
2
<h2>详情 - 页面制作</h2>
<blockquote><p>本文配套视频地址:<br><a href="https://link.segmentfault.com/?enc=R6HhAp5KJEmnCLaIDofKNg%3D%3D.5y2xC3AyQOzOxAweiCWYNy6BtiXEei4flRoO2lG3bY8hiE6agi6GPg9s3gkJfe0F" rel="nofollow">https://v.qq.com/x/page/o0555...</a></p></blockquote>
<hr>
<blockquote><p>开始前请把 <code>ch4-1</code> 分支中的 <code>code/</code> 目录导入微信开发工具 <br>这一章节中,主要介绍详情页的页面制作过程</p></blockquote>
<p>首先看一下我们最终要展示的页面 </p>
<p><img src="/img/remote/1460000011791051?w=740&h=1284" alt="" title=""></p>
<p>页面结构大体分为三部分,也是最常见的布局方式:头部、中间体、尾部。最顶部的是页面 <code>title</code>,也就是标题,如果是一般的页面,我们只需要在 <code>detail.json</code> 中增加如下配置即可: <br><br><br><del>"navigationBarTitleText": "Quora精选:为什么聪明人总能保持冷静"</del></p>
<p>但我们制作的详情页面信息是随着文章内容一直变化的,所以需要在代码中单独处理,就不需要在 <code>detail.json</code> 中添加 <br>这里,我们先制作出:头部和尾部。中间的内容部分,会由 <code>parse.js</code> 解析文章数据生成。 <br><br></p>
<p>开始之前,我们先修改 <code>app.wxss</code> 文件,引入需要用到的公用样式表和第三方样式</p>
<pre><code class="css">@import "./styles/base.wxss";
@import "./lib/wxParse/wxParse.wxss";
.green{
color: #26b961;
}
page{
height: 100%;
background-color: #f8f8f8;
}</code></pre>
<h3>Step 1. 页面准备</h3>
<ol><li>由于文章需要上下滚动,我们采用 <code>scroll-view</code> 组件来包括整个页面内容</li></ol>
<pre><code class="html"><!-- detail.html -->
<scroll-view scroll-y="true" enable-back-to-top="true" class="root-wrap">
</scroll-view> </code></pre>
<p><a href="https://link.segmentfault.com/?enc=Ibo3hdB%2Fs2g2Awol0SSBVg%3D%3D.%2FfAAmRA6WIOF0CsfvqfDqOM8stj%2BNtxngNCSgKiZDN2a8kUcqpDpn2OxLTEEBc5cETiO4OisO%2F5pwbj%2Ba53JEZQ%2FjIVQOH1uq%2Fz8KL2WPXU%3D" rel="nofollow">scroll-view</a> 组件,相当于我们在常规的 <code>div</code> 标签上增加了滚动功能并进行封装 <br><br></p>
<ol><li>然后调整下页面的高度和背景色</li></ol>
<pre><code class="css"> /* detail.css */
page {
background: #fbfbfb;
height: 100%
}
.root-wrap {
height: 100%
}</code></pre>
<h3>Step 2. 页面头部制作</h3>
<ol><li>头部包含三块内容:大标题、左浮动显示作者、右浮云显示日期,制作如下:</li></ol>
<pre><code class="html"> <!-- detail.html -->
<scroll-view scroll-y="true" enable-back-to-top="true" class="root-wrap">
<view class="wrapper">
<view class="info">
<view class="info-title">Quora精选:为什么聪明人总能保持冷静</view>
<view class="info-desc cf">
<text class="info-desc-author fl">哈利波特</text>
<text class="info-desc-date fr">2017/06/27</text>
</view>
<view class="info-line under-line"></view>
</view>
</view>
</scroll-view> </code></pre>
<p><br></p>
<ol><li>对应样式文件,注意: <code>fl(float:left)</code>、 <code>fr(float:right)</code>、 <code>cf(clear:float)</code> 三个样式都是在 <code>base.wxss</code> 中设置的全局样式</li></ol>
<pre><code class="css"> /* detail.css */
page {
background: #fbfbfb;
height: 100%
}
.root-wrap {
height: 100%
}
.wrapper {
padding-bottom: 96rpx
}
.wrapper .top-img {
width: 100%;
height: 470rpx;
vertical-align: top
}
.wrapper .info {
padding: 0 36rpx
}
.wrapper .info-title {
padding: 40rpx 0;
line-height: 60rpx;
font-size: 44rpx;
font-weight: 500;
color: #333
}
.wrapper .info-desc {
font-size: 28rpx;
line-height: 30rpx;
color: #c1c1c1
}
.wrapper .info-desc-author {
max-width: 65%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden
}
.wrapper .info-line {
margin-top: 24rpx
}</code></pre>
<h3>Step 3. 页面尾部制作</h3>
<blockquote><p>页尾类似于于菜单导航功能,用户可以进入 <code>下一篇</code> 或 <code>返回</code> 列表,并且当页面滚动时候,固定在底部不动</p></blockquote>
<p>修改页面 <code>detail.html</code></p>
<pre><code class="html"> <!-- 增加以下内容,footbar节点与info节点平级 -->
<view class="footbar">
<form>
<button class="footbar-back clearBtnDefault">
<view class="icon footbar-back-icon"></view>
</button>
<button class="footbar-btn clearBtnDefault">下一篇</button>
<button class="footbar-share clearBtnDefault">
<view class="icon footbar-share-icon"></view>
</button>
</form>
</view></code></pre>
<p>修改样式表</p>
<pre><code class="css"> /* detail.css 增加以下样式内容 */
.wrapper .footbar {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: #fff;
font-size: 32rpx;
color: #333
}
.wrapper .footbar-back,.wrapper .footbar-share {
position: absolute;
width: 96rpx;
height: 96rpx;
bottom: 0;
z-index: 2
}
.wrapper .footbar .icon {
position: absolute;
width: 42rpx;
height: 38rpx;
top: 30rpx
}
.wrapper .footbar-back {
left: 0
}
.wrapper .footbar-back-icon {
left: 30rpx;
background: url(https://n1image.hjfile.cn/mh/2017/06/06/1305a8ac4dc9347b59cc8c2c667122e5.png) 0 0 no-repeat;
background-size: contain
}
.wrapper .footbar-list {
left: 0
}
.wrapper .footbar-list-icon {
left: 30rpx;
background: url(https://n1image.hjfile.cn/mh/2017/06/09/1e630ac45547e6ab5260928e1d57a3c6.png) 0 0 no-repeat;
background-size: contain
}
.wrapper .footbar-btn {
text-align: center;
margin: 0 96rpx;
height: 96rpx;
line-height: 96rpx
}
.wrapper .footbar-share {
right: 0
}
.wrapper .footbar-share-icon {
right: 30rpx;
background: url(https://n1image.hjfile.cn/mh/2017/06/09/ebc3852fb865bd19182c09ca599d8ac1.png) 0 0 no-repeat;
background-size: contain
}
.wrapper .clearBtnDefault {
margin: 0;
padding: 0;
background: #fff;
border: 0;
border-radius: 0
}
.wrapper .clearBtnDefault:after {
content: '';
border: none;
border-radius: 0;
width: 0;
height: 0
}</code></pre>
<p><br><br>页面尾部制作完成,下一步我们将处理中间的文章内容部分。</p>
<h3>Step 4. 为中间的 content 内容预留位置</h3>
<blockquote><p>完整的页面代码如下</p></blockquote>
<pre><code class="html"> <scroll-view scroll-y="true" enable-back-to-top="true" class="root-wrap">
<view class="wrapper">
<view class="info">
<view class="info-title">Quora精选:为什么聪明人总能保持冷静</view>
<view class="info-desc cf">
<text class="info-desc-author fl">哈利波特</text>
<text class="info-desc-date fr">2017/06/27</text>
</view>
<view class="info-line under-line"></view>
</view>
<!-- 增加正文视图位置 -->
<view class="content">
文章正文
</view>
<view class="footbar">
<form>
<button class="footbar-back clearBtnDefault">
<view class="icon footbar-back-icon"></view>
</button>
<button class="footbar-btn clearBtnDefault">下一篇</button>
<button class="footbar-share clearBtnDefault">
<view class="icon footbar-share-icon"></view>
</button>
</form>
</view>
</view>
</scroll-view></code></pre>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=pnY85%2BDBA%2FFUB6%2BB8S%2FFmQ%3D%3D.BnCfg01%2FRqvke5OCFjtCPnGIE9XKEXvN6vVc%2BUVkbVs%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h2>【11月11号】上海iKcamp最新活动</h2>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=fTfybe1b1BnJegFm8atkPA%3D%3D.dknowujNTkXkxJHdjuH9OGSO9vpf5a%2FWYmmVXBLccs%2BkrIdGZx93xZ9FZHuvNYV4" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
微信小程序教学第三章第四节(含视频):小程序中级实战教程:下拉更新、分享、阅读标识
https://segmentfault.com/a/1190000011773805
2017-10-30T10:50:24+08:00
2017-10-30T10:50:24+08:00
iKcamp
https://segmentfault.com/u/ikcamp
2
<h2>下拉更新、分享、阅读标识</h2>
<blockquote><p>本文配套视频地址:<br><a href="https://link.segmentfault.com/?enc=cM7Ra1mYNUYFunv12R6nbQ%3D%3D.L%2FCHRCok3L7CLqnDLdeZvOtyAuhyks2l8XoEOH6SyRhKljIkxsT7v6af9QOjZMnM" rel="nofollow">https://v.qq.com/x/page/h0554...</a></p></blockquote>
<hr>
<blockquote><p>开始前请把 <code>ch3-4</code> 分支中的 <code>code/</code> 目录导入微信开发工具 <br>这一篇中,我们把列表这块的剩余功能做完:下拉更新、分享、阅读标识。</p></blockquote>
<p><br></p>
<h3>下拉更新功能</h3>
<blockquote><p>下拉更新这个功能与我们在第一章中写的小 <code>demo</code> 所用方法一致:<code>onReachBottom</code>。</p></blockquote>
<p><br></p>
<p>当用户滚动过程中触发了 <code>上拉</code> 这个动作时候,微信小程序会自动监听到并执行 <code>onReachBottom</code> 这个函数,所以我们只需要把这个监听事件写好就行了: </p>
<p>修改 <code>index.js</code>,增加 <code>onReachBottom</code> 函数:</p>
<pre><code class="js">let handler = {
// 此处省略部分代码
/*
* 每次触发,我们都会先判断是否还可以『加载更多』
* 如果满足条件,那说明可以请求下一页列表数据,这时候把 data.page 累加 1
* 然后调用公用的请求函数
*/
onReachBottom () {
if (this.data.hasMore) {
let nextPage = this.data.page + 1;
this.setData({
page: nextPage
});
this.requestArticle();
}
},
}</code></pre>
<p><br></p>
<h3>分享功能</h3>
<p>类似于 <code>onReachBottom</code>,分享功能也是微信自带的一个监听事件回调函数 <code>onShareAppMessage</code>,它返回一个对象,对象中定义了分享的各种信息及分享成功和分享失败的回调,具体细节可以查看<a href="https://link.segmentfault.com/?enc=4KKBi0aRrMFd3PZYWhSDhQ%3D%3D.IILrQAU8zq3oYL4BCkuOnn39f6TNSju5uIgn2vbtfTu%2FTPFrFEtnVTQR6ccZImMpjwxldoEcsPxQfTEpe74tRQPgE1qah332FjYEkIVwkyA%3D" rel="nofollow">分享接口官方文档</a> </p>
<p>修改 <code>index.js</code>,增加分享的回调事件:</p>
<pre><code class="js">let handler = {
// 此处省略部分代码
/*
* 分享
*/
onShareAppMessage () {
let title = config.defaultShareText || '';
return {
title: title,
path: `/pages/index/index`,
success: function(res) {
// 转发成功
},
fail: function(res) {
// 转发失败
}
}
},
}</code></pre>
<p><br></p>
<h3>阅读标识</h3>
<p>如何实现阅读标识呢?其实思路也简单。如果用户从列表中点击某篇文章阅读,此篇文章肯定是需要标识的。所以我们只需要在跳转到文章详情之前,把此篇文章的 <code>contentId</code> 缓存起来 </p>
<p>修改 <code>index.wxml</code>,视图中绑定点击事件 <code>bindtap="showDetail"</code>,同时增加三元判断,如果文章已经阅读过,我们给它增加一个 <code>class="visited"</code> 标识:</p>
<pre><code class="html"><view class="wrapper">
<!--repeat-->
<view wx:for="{{ articleList }}" wx:for-item="group" wx:key="{{ group.date }}" class="group">
<view class="group-bar">
<view class="group-title {{ group.formateDate === '今日' ? 'on' : ''}}">{{ group.formateDate }}</view>
</view>
<view class="group-content">
<!--repeat-->
<!-- 增加点击事件 bindtap="showDetail" -->
<view wx:for="{{ group.articles }}" wx:for-item="item" wx:key="{{ item.contentId }}" data-item="{{ item }}" bindtap="showDetail" class="group-content-item {{ item.hasVisited ? 'visited' : '' }}">
<view class="group-content-item-desc ellipsis-multi-line ellipsis-line-3">{{ item.title }}</view>
<image mode="aspectFill" class="group-content-item-img" src="{{ item.cover || defaultImg.coverImg }}" ></image>
</view>
</view>
</view>
<view hidden="{{ hasMore }}" class="no-more">暂时没有更多内容</view>
</view></code></pre>
<p><br></p>
<p>修改 <code>index.js</code>,增加点击事件的回调函数 <code>showDetail</code>:</p>
<pre><code class="js">let handler = {
// 此处省略部分代码
/*
* 通过点击事件,我们可以获取到当前的节点对象
* 同样也可以获取到节点对象上绑定的 data-X 数据
* 获取方法: e.currentTarget.dataset
* 此处我们先获取到 item 对象,它包含了文章 id
* 然后带着参数 id 跳转到详情页面
*/
showDetail (e) {
let dataset = e.currentTarget.dataset
let item = dataset && dataset.item
let contentId = item.contentId || 0
wx.navigateTo({
url: `../detail/detail?contentId=${contentId}`
});
},
}</code></pre>
<p><br></p>
<p>修改 <code>index.js</code>,增加处理标识功能的函数 <code>markRead</code>,并在上面的 <code>showDetail</code> 函数中调用:</p>
<pre><code class="js">let handler = {
// 此处省略部分代码
showDetail (e) {
let dataset = e.currentTarget.dataset
let item = dataset && dataset.item
let contentId = item.contentId || 0
// 调用实现阅读标识的函数
this.markRead( contentId )
wx.navigateTo({
url: `../detail/detail?contentId=${contentId}`
});
},
/*
* 如果我们只是把阅读过的文章contentId保存在globalData中,则重新打开小程序后,记录就不存在了
* 所以,如果想要实现下次进入小程序依然能看到阅读标识,我们还需要在缓存中保存同样的数据
* 当进入小程序时候,从缓存中查找,如果有缓存数据,就同步到 globalData 中
*/
markRead (contentId) {
//先从缓存中查找 visited 字段对应的所有文章 contentId 数据
util.getStorageData('visited', (data)=> {
let newStorage = data;
if (data) {
//如果当前的文章 contentId 不存在,也就是还没有阅读,就把当前的文章 contentId 拼接进去
if (data.indexOf(contentId) === -1) {
newStorage = `${data},${contentId}`;
}
}
// 如果还没有阅读 visited 的数据,那说明当前的文章是用户阅读的第一篇,直接赋值就行了
else {
newStorage = `${contentId}`;
}
/*
* 处理过后,如果 data(老数据) 与 newStorage(新数据) 不一样,说明阅读记录发生了变化
* 不一样的话,我们就需要把新的记录重新存入缓存和 globalData 中
*/
if (data !== newStorage) {
if (app.globalData) {
app.globalData.visitedArticles = newStorage;
}
util.setStorageData('visited', newStorage, ()=>{
this.resetArticles();
});
}
});
},
resetArticles () {
let old = this.data.articleList;
let newArticles = this.formatArticleData(old);
this.setData({
articleList: newArticles
});
},
}</code></pre>
<p><br></p>
<p>别急,写到这里,还没有结束呢,差最后一步了。 </p>
<p><br></p>
<p>修改 <code>app.js</code>,小程序初始化时候,我们从缓存中把数据拿出来,赋值给 globalData,这样就做到了真正的阅读标识</p>
<pre><code class="js">'use strict';
// 引入工具类库
import util from './utils/index';
let handler = {
onLaunch () {
this.getDevideInfo();
// 增加初始化缓存数据功能
util.getStorageData('visited', (data)=> {
this.globalData.visitedArticles = data;
});
},
alert (title = '提示', content = '好像哪里出了小问题~请再试一次~') {
wx.showModal({
title: title,
content: content
})
},
getDevideInfo () {
let self = this;
wx.getSystemInfo({
success: function (res) {
self.globalData.deviceInfo = res;
}
})
},
globalData: {
user: {
openId: null
},
visitedArticles: '',
deviceInfo: {}
}
};
App(handler);</code></pre>
<p><br></p>
<p>到此,列表页面的功能开发完成,下一篇我们将开始详情页面的开发</p>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=2SYrdNb5uWcljjfqG9rxjQ%3D%3D.vF0o1CMv813Gn3cWEfPKWPZHglN8dJxupMBEQqBVBO0%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h2>【11月11号】上海iKcamp最新活动</h2>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=6iOD4%2Bmujjzx6YpahfCMWQ%3D%3D.nTHQ2vnu9HCt%2FFr5bmLlhUH6b1FmLBxGIyC3Lw1spduxlDb%2FyLYazJH9ICWzzQM%2F" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
使用合适的设计模式一步步优化前端代码
https://segmentfault.com/a/1190000011744601
2017-10-27T10:57:45+08:00
2017-10-27T10:57:45+08:00
iKcamp
https://segmentfault.com/u/ikcamp
8
<blockquote><p>作者:晓飞<br>本文原创,转载请注明作者及出处</p></blockquote>
<hr>
<blockquote><p>在后端语言中,设计模式应用的较为广泛。如Spring中常见的工厂模式、装饰者模式、单例模式、迭代器模式。但是在日常的前端开发中,设计模式使用的较少,或者大家的代码已经遵循了某某设计模式但是我们并不知道。常见的设计模式有23种,如果单纯的按照模式名称+名词解释的方式来写这篇文章,可能太枯燥了或者很难理解记忆,所以我打算换一种方式。下面我们以一个例子开始我们今天的文章。</p></blockquote>
<h5>假设我们有一个这样的需求:</h5>
<pre><code class="js">let page = {
init: ()=>{
//此处(placeA)有很多业务代码或者调用了很多page中的其他初始化函数
},
....
};</code></pre>
<p>现在业务迭代,需要我们在page.init()初始化代码块的最后增加一些功能,同时不影响原先的功能。按照正常的写法,我们可能会像下面这样写:</p>
<pre><code class="js">let page = {
init: ()=>{
//placeA
page.newFunction();
},
newFunction: ()=>{
...
}
};</code></pre>
<p>这样写是可以解决我们的需求,但是这样的代码是具有侵略性的,我们不得不在原先的代码的合适位置新增我们需要的代码。但我们思考一个问题,如果我们用了某个插件或者某个被ungly、minify之后的代码呢,我们怎么在找到合适的位置添加我们需要的功能呢?大家可以先自己思考一下,再看下面的内容。</p>
<h5>首先我们先看解决方案,再思考其背后的东西。</h5>
<pre><code class="js">//我们可以在Function的原型链上定义一个扩展函数,以实现我们的需求。
Function.prototype.fnAfter = function(fn) {
var _self = this;
return function() {
_self.apply(this, arguments);
fn.apply(this, arguments);
}
};
page.init = (page.init || function() {}).fnAfter(function() {
console.log('我们要追加的功能成功啦~');
});
page.init();</code></pre>
<p>上面的代码已经能够实现我们的需要了,但是其实还是不够好或者可以写的更灵活一些。因为我希望可以可以做到像jquery的链式调用那样,可以一直往后面追加新的功能。那么我们在上面代码的基础上再扩展下,其实很简单,我们只要再Function.prototype.fnAfter中再返回自身就好了。</p>
<pre><code class="js">Function.prototype.fnAfter = function(fn) {
var _self = this;
return function() {
var fnOrigin = _self.apply(this, arguments);
fn.apply(this, arguments);
return fnOrigin;
}
};</code></pre>
<p>其实上面的代码写法还是可以优化的。比如:</p>
<pre><code class="js">//每次扩展的时候我们都需要这么写
page.init = (page.init || function() {}).fnAfter(function() {
//...
});
//我们能不能再优化下,比如容错代码 || function(){} 在一个地方统一处理
//或者我们新建一个工厂函数来帮我们统一做这样的事情,这里我们就不展开了,文章篇幅有限。</code></pre>
<h5>我们上面的扩展其实就是遵循的是面向对象程序设计中的开放-封闭原则(OCP)。官方对OCP的解释是:软件实体(类、模块、函数...)应该是可以扩展的,但是不可修改。设计模式中有很多模式都遵循了开发-封闭原则,比如:发布-订阅者模式、模板方法模式、策略模式、代理模式。</h5>
<p>有的时候我们通过扩展来提高代码的灵活性并不能解决所有的场景需要,在不可避免发生修改的时候,我们可以通过增加配置文件,让用户修改配置文件以实现个性化需求也是合理的。修改配置远比修改源代码要简单的多。</p>
<h5>有了上面的引入,我们来看几个前端开发中常见的设计模式。</h5>
<ul><li>
<p>单例模式</p>
<pre><code>单例模式顾名思义:保证一个类仅有一个实例,
并且对外暴露一个能够访问到它的访问点。</code></pre>
</li></ul>
<p>实现单例模式的核心就是保证一个类仅有一个实例,那么意思就是当创建一个对象时,我们需要判断下之前有没有创建过该实例,如果创建过则返回之前创建的实例,否则新建。</p>
<pre><code class="js">var fn = function() {
this.instance = null;
};
fn.getInstance = function() {
//写法1
if (!this.instance) {
this.instance = new fn();
}
return this.instance;
//写法2
return this.instance || (this.instance = new fn());
};
var fnA = fn.getInstance();
var fnB = fn.getInstance();
console.log(fnA === fnB); //true</code></pre>
<p>日常的业务场景中,单例模式也比较常见,比如:一个页面中的模态框只有一个,每次打开与关闭的都应该是同一个,而不是重复新建。而且为了性能优化,我们应该在需要时再创建,而不是页面初始化时就已经存在于dom中,这个就是<em>惰性单例模式</em>。</p>
<pre><code class="js">//假设我们需要点击某个按钮时就显示出模态框,那么我们可以像下面这么实现。
var createModal = (function(){
var modal = null;
return function() {
if (!modal) {
modal = document.createElement('div');
//...
modal.style.display = 'none';
document.getElementById('container').append(modal);
}
return modal;
}
})();
document.getElementById('showModal').click(function() {
var modal = createModal();
modal.style.display = 'block';
});</code></pre>
<p>上面的代码中,我们将创建对象和管理实例的逻辑都放在一个地方,违反了单一职责原则,我们应该单独新建一个用于创建单例的方法,这样我们不仅能创建唯一的modal实例,也能创建其他的,职责分开。</p>
<pre><code class="js">var createSingleInstance = function(fn) {
var instance = null;
return function() {
if (!instance) {
instance = fn.apply(this, arguments);
}
return instance;
}
};
var createModal = function() {
var modal = docuemnt.createElement('div');
//...
modal.style.display = 'none';
document.getElementById('container').append(modal);
return modal;
};
var modal = createSingleInstance(createModal);</code></pre>
<p></p>
<ul><li>
<p>观察者模式</p>
<pre><code>定义了对象与其他对象之间的依赖关系,
当某个对象发生改变的时候,所有依赖到这个对象的地方都会被通知。</code></pre>
</li></ul>
<p>像knockout.js中的ko.compute以及vue中的computed函数其实就是这个模式的实践。实现观察者模式的核心就是我们需要有一个变量来保存所有的依赖,一个listen函数用于向变量中添加依赖,一个trigger函数用于触发通知。</p>
<pre><code class="js">var observal = {
eventObj: {},
listen: function(key, fn) {
this.eventObj[key] = this.eventObj[key] || [];
this.eventObj[key].push(fn);
},
trigger: function(key) {
var eventList = this.eventObj[key];
if (!eventList || eventList.length < 1) {
return;
}
var length = eventList.length;
for (var i = 0; i < length; i++) {
var event = eventList[i];
event.apply(this, arguments);
}
}
};
//定义要监听的事件
observal.listen('command1', function() {
console.log('黑夜给了我夜色的眼睛~');
});
observal.listen('command1', function() {
console.log('我却用它寻找光明~');
});
observal.listen('command2', function() {
console.log('一花一世界~');
});
observal.listen('command2', function() {
console.log('一码一人生~');
});
//触发某个监听的事件
observal.trigger('command1');//黑夜给了我夜色的眼睛~ 我却用它寻找光明~
observal.trigger('command2');//一花一世界~ 一码一人生~</code></pre>
<p>使用观察者模式(发布-订阅模式)我们可以使得代码更灵活、健壮性更高。订阅者不需要了解消息来自哪一个发布者,发布者也不需要知道消息会发送给哪些订阅者。</p>
<p>同样的我们可以创建一个公用的函数库,里面存放创建observal的工具方法,需要用到的地方我们就用这个方法创建一个发布订阅对象。</p>
<ul><li>
<p>其他设计模式及设计原则</p>
<pre><code>设计模式有很多,这里篇幅有限就不再展开。GoF在1995年提出了23种设计模式。诸如策略者模式优化表单验证、代理模式、组合模式、装饰者模式、适配器模式...这些后期可以再简单探讨或者大家后面自己了解。常用的设计模式及设计原则可以参考下面的思维导图。
![常用设计模式](https://user-gold-cdn.xitu.io/2017/10/27/f7ab0664b5fd4e34dbb16eaab5813c9d)
![六大设计原则](https://user-gold-cdn.xitu.io/2017/10/27/038d561c77aba1ff095fa0c3cfc8c113)
</code></pre>
</li></ul>
<h5>看了上面的文章,相信大家对设计模式的好处有了直观的了解,也大致掌握了单例模式及观察者模式。</h5>
<p>设计模式都是经过了大量的代码、软件实践而总结出来的优秀的组织实践方案。每种设计模式都有它的适应场景,有的场景也会使用多种设计模式。只有了解了更多的设计模式,掌握各个设计模式自己的适应场景,才能更好的为我们所用。</p>
<p>但是<strong><em>过早的优化不一定是好事或者不是必须的</em></strong>,有时候我们可以一开始并不去优化,等到某个应用场景下出现了代码组织混乱、需要额外扩展等问题,我们再优化重构,以防过早优化导致的不必要性或者只是增加了代码不必要的复杂性。就像redux,如果一个页面组件与组件之间有数据共享、需要在任意组件内部拿到某个数据、任意一个组件中某个行为导致的数据变化需要通知到所有用到的地方,那么这个时候可以使用redux,一些简单的表单页面或者展示页完全可以不用redux。</p>
<h5>看到这里不容易,最后给大家讲一个笑话轻松一下:</h5>
<pre><code>从前有只麋鹿,它在森林里玩儿,不小心走丢了。
于是它给它的好朋友长颈鹿打电话:“喂…我迷路辣。”
长颈鹿听见了回答说:“喂~我长颈鹿辣~”</code></pre>
<blockquote><p>参考:曾探《javascript设计模式与开发实践》</p></blockquote>
<p>---</p>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=CXEhWHINQys0yyrjITvTag%3D%3D.aiAIcsaJg40eNbKrCwuzTt8wlqyRS4MiG0kNq6BnJMY%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h2>iKcamp最新活动</h2>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=DUfaJgaSIz1EMbnLBdJKtA%3D%3D.3cYm%2BufVv%2F5Ey6UQ8OZoUGRv%2F4ESth%2Fc1EEBirrsT8KwCMy9xrH%2BiHQe9JV6Ic7b" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
微信小程序教学第三章第三节(含视频):小程序中级实战教程:视图与数据关联
https://segmentfault.com/a/1190000011729459
2017-10-26T11:49:04+08:00
2017-10-26T11:49:04+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<h2><a>§ 视图与数据关联</a></h2>
<blockquote><p>本文配套视频地址:<br><a href="https://link.segmentfault.com/?enc=4gQ8VbqxD%2BPAOXcxG5kOAQ%3D%3D.uuTv3ZPkclsLtgYZ2L3F5rbdKv7A23Z458vjU16EDQZUWtIfloPrCLXwhrEIat7R" rel="nofollow">https://v.qq.com/x/page/z0554...</a></p></blockquote>
<hr>
<blockquote><p>开始前请把 <code>ch3-3</code> 分支中的 <code>code/</code> 目录导入微信开发工具 </p></blockquote>
<p><br></p>
<h3>首先</h3>
<p>首先我们要做的是什么呢?直接写模板逻辑吗?不是,给用户以良好的提示是很重要的,所以,我们要做的第一件事就是,加载中...</p>
<p>这里我们采用官方 <code>loading</code> 组件,所以现在就可以直接拿来用了。</p>
<p>修改 <code>index.wxml</code>,增加 <code>loading</code> 组件。很明显,变量 <code>hiddenLoading</code> 控制着它的展示与隐藏:</p>
<pre><code class="html"><loading hidden="{{hiddenLoading}}">数据加载中</loading></code></pre>
<p><br></p>
<p>然后修改 index.js,处理 loading 组件的状态逻辑值 hiddenLoading</p>
<pre><code class="js">// 刚进入列表页面,就展示loading组件,数据加载完成后隐藏
onLoad (options) {
this.setData({
hiddenLoading: false
})
this.requestArticle()
},
// 列表渲染完成后,隐藏 loading组件
renderArticle (data) {
if (data && data.length) {
let newList = this.data.articleList.concat(data);
this.setData({
articleList: newList,
hiddenLoading: true
})
}
}</code></pre>
<h3>分析页面结构</h3>
<p>通过分析静态页面可以看出,这个列表是按照 <strong>天</strong> 为单位来分段,在每天的文章里又按照 <strong>文章</strong> 为单位继续细分。所以可以知道这个 <code>wxml</code> 的主体结构是循环套循环。所以我们可以初步写出下面的结构:</p>
<pre><code class="html"><loading hidden="{{hiddenLoading}}">数据加载中</loading>
<view class="wrapper">
<view wx:for="{{ articleList }}" wx:for-item="group" wx:key="{{ group.date }}" class="group">
<view wx:for="{{ group }}" wx:for-item="item" wx:key="{{ item.contentId }}"></view>
</view>
</view></code></pre>
<p><br></p>
<p>这里有一点需要 <strong>注意</strong>:在 <code>wxml</code> 做循环嵌套的时候,一定要重新定义 <code>wx:for-item</code> 字段。因为 <code>wxml</code> 循环中当前项的下标变量名默认为 <code>index</code>,当前项的变量名默认为 <code>item</code>。如果没有重新定义 <code>item</code>,在内层循环里通过 <code>item</code> 取到的值其实是外层循环的值。</p>
<blockquote><p><a href="https://link.segmentfault.com/?enc=%2BIOy3WwiGyMlbbTiMihkRg%3D%3D.1DymjrRFD6yxTloyKZm66hxXdQTY%2F0AISzqqxHXLmBG5Sp5wKGhJ4hElbHa33lIgK07DsRFG20OOlLbDml48cbnvjxIBLVnBGjgOEqXD%2BJI%3D" rel="nofollow">官方 API - 列表渲染</a></p></blockquote>
<p><br></p>
<p>下面我们就详细的分析下具体的结构,首先,每一天都有一个日期做开头,然后下面是一天的 4 篇文章。每篇文章分为左右结构,左边是标题,最多 3 行,超过的文字就用 … 表示。右边是一张文章的封面图,如果没有封面图就用默认的封面图。上面的日期如果是今天就显示今天,否则就直接显示月日,所以可以把 <code>wxml</code> 结构丰富成下面的样子:</p>
<pre><code class="html"><loading hidden="{{hiddenLoading}}">数据加载中</loading>
<view class="wrapper">
<!--repeat-->
<view wx:for="{{ articleList }}" wx:for-item="group" wx:key="{{ group.date }}" class="group">
<view class="group-bar">
<view class="group-title {{ group.formateDate === '今日' ? 'on' : ''}}">{{ group.formateDate }}</view>
</view>
<view class="group-content">
<!--repeat-->
<view wx:for="{{ group.articles }}" wx:for-item="item" wx:key="{{ item.contentId }}" data-item="{{ item }}" class="group-content-item">
<view class="group-content-item-desc ellipsis-multi-line ellipsis-line-3">{{ item.title }}</view>
<image mode="aspectFill" class="group-content-item-img" src="{{ item.cover || defaultImg.coverImg }}" ></image>
</view>
</view>
</view>
</view></code></pre>
<p><br></p>
<p>这里有一个图片处理的属性可以看看相应的 API 了解下:</p>
<blockquote><p><a href="https://link.segmentfault.com/?enc=dAPZZYzviQ8X6RNoqw28aA%3D%3D.RxqWS9Ru6l%2BoJs0vOJlMlZWy6Rwydf%2Fq5Js7tozVVaBktIJgSiK30lj6MwPUwFW%2BIEM%2BwxpKXQbM1tvHF%2Beztw%3D%3D" rel="nofollow">官方 API - 图片处理</a></p></blockquote>
<p><br></p>
<p>页面结构搭建完了吗?并没有,还有一件很重要的事情要做。当我们的所有内容都展示完了,我们要友好的提醒用户,所以需要在最底端加上一个提示,把这些交互考虑进去之后的 <code>wxml</code> 就是下面这样的:</p>
<pre><code class="html"><!--index.wxml-->
<loading hidden="{{hiddenLoading}}">数据加载中</loading>
<view class="wrapper">
<!--repeat-->
<view wx:for="{{ articleList }}" wx:for-item="group" wx:key="{{ group.date }}" class="group">
<view class="group-bar">
<view class="group-title {{ group.formateDate === '今日' ? 'on' : ''}}">{{ group.formateDate }}</view>
</view>
<view class="group-content">
<!--repeat-->
<view wx:for="{{ group.articles }}" wx:for-item="item" wx:key="{{ item.contentId }}" data-item="{{ item }}" class="group-content-item">
<view class="group-content-item-desc ellipsis-multi-line ellipsis-line-3">{{ item.title }}</view>
<image mode="aspectFill" class="group-content-item-img" src="{{ item.cover || defaultImg.coverImg }}" ></image>
</view>
</view>
</view>
<view hidden="{{ hasMore }}" class="no-more">暂时没有更多内容</view>
</view></code></pre>
<p><br></p>
<p>到此,列表的页面与大体数据可以说是告一段落了,下一节我们介绍下如何增加阅读标识功能及分享功能、下拉更新功能</p>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=%2Bc%2Bkc5emdaSXfN%2BP%2FW8LBg%3D%3D.XBcqFVc6GFtywlxY7wfTbS9kWOPQ8n%2FggGJKNCeiVgU%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h2>iKcamp最新活动</h2>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=aL7x%2BgRFXT6diwD%2FjkxwHA%3D%3D.wkVcMbKYRgNZeedz9eVgzJ9Wz%2B70TDMzsuGkKF87w0yZgpkuej5st%2FTg%2BoYuctWf" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
微信小程序教学第三章(含视频):小程序中级实战教程:列表-页面逻辑处理
https://segmentfault.com/a/1190000011711583
2017-10-25T11:54:21+08:00
2017-10-25T11:54:21+08:00
iKcamp
https://segmentfault.com/u/ikcamp
1
<h2><a>§ 页面逻辑处理</a></h2>
<blockquote><p>本文配套视频地址:<br><a href="https://link.segmentfault.com/?enc=vxF6KuOxoGZVk6felLiRXA%3D%3D.WgXG0Idpt1vT1%2FcC9WOuGIatrTUqD28r591D6sZfjqqJo1EbcoRUlXY9H9es1TZk" rel="nofollow">https://v.qq.com/x/page/n0554...</a></p></blockquote>
<hr>
<blockquote><p>开始前请把 <code>ch3-2</code> 分支中的 <code>code/</code> 目录导入微信开发工具 </p></blockquote>
<h3>修改 <code>index.js</code> 文件,引入我们需要的外部资源</h3>
<pre><code class="js">'use strict';
import util from '../../utils/index';
import config from '../../utils/config';
let app = getApp();
let isDEV = config.isDev;
// 后继的代码都会放在此对象中
let handler = {
}
Page(handler)</code></pre>
<p><br></p>
<h3>数据绑定</h3>
<p>我们首先挖出和渲染相关的数据,并添加在 <code>handler</code> 对象的 <code>data</code> 字段中(Model 层) <br>修改 <code>index.js</code> 中的 <code>handler</code> 对象:</p>
<pre><code class="js">// 此处省略部分代码
let handler = {
data: {
page: 1, //当前加载第几页的数据
days: 3,
pageSize: 4,
totalSize: 0,
hasMore: true,// 用来判断下拉加载更多内容操作
articleList: [], // 存放文章列表数据,与视图相关联
defaultImg: config.defaultImg
},
}</code></pre>
<p><strong>注意:</strong> 后续添加的代码都是放在 <code>handler</code> 对象中,它会传递到 <code>Page</code> 函数中用来初始化页面组件 <br><br></p>
<h3>获取数据</h3>
<p>然后要做的就是获取列表的数据,初始化数据的工作我们一般放在生命周期的 <code>onLoad()</code> 里:</p>
<pre><code class="js">let handler = {
onLoad (options) {
this.requestArticle()
},
/*
* 获取文章列表数据
*/
requestArticle () {
util.request({
url: 'list',
mock: true,
data: {
tag:'微信热门',
start: this.data.page || 1,
days: this.data.days || 3,
pageSize: this.data.pageSize,
langs: config.appLang || 'en'
}
})
.then(res => {
console.log( res )
});
}
}</code></pre>
<h3>数据加载完成之后,我们需要对接口返回的数据进行业务方面的容错处理</h3>
<p>修改 <code>requestArticle</code> 函数:</p>
<pre><code class="js">let handler = {
// 此处省略部分代码
requestArticle () {
util.request({
url: 'list',
mock: true,
data: {
tag:'微信热门',
start: this.data.page || 1,
days: this.data.days || 3,
pageSize: this.data.pageSize,
langs: config.appLang || 'en'
}
})
.then(res => {
// 数据正常返回
if (res && res.status === 0 && res.data && res.data.length) {
// 正常数据 do something
console.log(res)
}
/*
* 如果加载第一页就没有数据,说明数据存在异常情况
* 处理方式:弹出异常提示信息(默认提示信息)并设置下拉加载功能不可用
*/
else if (this.data.page === 1 && res.data && res.data.length === 0) {
util.alert();
this.setData({
hasMore: false
});
}
/*
* 如果非第一页没有数据,那说明没有数据了,停用下拉加载功能即可
*/
else if (this.data.page !== 1 && res.data && res.data.length === 0) {
this.setData({
hasMore: false
});
}
/*
* 返回异常错误
* 展示后端返回的错误信息,并设置下拉加载功能不可用
*/
else {
util.alert('提示', res);
this.setData({
hasMore: false
});
return null;
}
})
}
}</code></pre>
<p>上面我们把 <code>wx.request</code> 重新包装成了 <code>Promise</code> 的形式,其实我们是请求的 mock 数据。但是接口请求到的数据绝大部分情况下都不会直接适用于 <code>UI</code> 展示,所以我们需要做一层数据转换,把接口数据转换成视图数据。 </p>
<h3>格式化数据</h3>
<p>先看下后端返回的数据结构 </p>
<p><img src="/img/remote/1460000011711588?w=1152&h=462" alt="" title=""></p>
<p>我们需要做两件事情</p>
<ol>
<li>遍历 <code>data</code> 数组,对返回的日期格式化,当天的显示 <code>今天</code>,如果是今年的文章,显示月日格式 <code>08-21</code> ;如果是往年的文章,显示标准的年月日格式 <code>2015-06-12</code>。</li>
<li>遍历 <code>articles</code> 数组,判断此篇文章的 <code>contentId</code> 是否已经在全局变量 <code>visitedArticles</code> 中,如果存在,说明已经访问过。</li>
</ol>
<p>修改 <code>app.js</code>,增加全局变量 <code>visitedArticles</code></p>
<pre><code class="js">globalData: {
user: {
name: '',
avator: ''
},
visitedArticles: ''
}</code></pre>
<p>修改 <code>index.js</code> 中的 <code>requestArticle</code> 函数:</p>
<pre><code class="js">let handler = {
// 此处省略部分代码
requestArticle () {
// 注意:修改此处代码
if (res && res.status === 0 && res.data && res.data.length) {
let articleData = res.data;
//格式化原始数据
let formatData = this.formatArticleData(articleData);
console.log( formatData )
}
}
}</code></pre>
<p>增加对列表数据格式化的代码:</p>
<pre><code class="js">let handler = {
// 此处省略部分代码
/*
* 格式化文章列表数据
*/
formatArticleData (data) {
let formatData = undefined;
if (data && data.length) {
formatData = data.map((group) => {
// 格式化日期
group.formateDate = this.dateConvert(group.date);
if (group && group.articles) {
let formatArticleItems = group.articles.map((item) => {
// 判断是否已经访问过
item.hasVisited = this.isVisited(item.contentId);
return item;
}) || [];
group.articles = formatArticleItems;
}
return group
})
}
return formatData;
},
/*
* 将原始日期字符串格式化 '2017-06-12'
* return '今日' / 08-21 / 2017-06-12
*/
dateConvert (dateStr) {
if (!dateStr) {
return '';
}
let today = new Date(),
todayYear = today.getFullYear(),
todayMonth = ('0' + (today.getMonth() + 1)).slice(-2),
todayDay = ('0' + today.getDate()).slice(-2);
let convertStr = '';
let originYear = +dateStr.slice(0,4);
let todayFormat = `${todayYear}-${todayMonth}-${todayDay}`;
if (dateStr === todayFormat) {
convertStr = '今日';
} else if (originYear < todayYear) {
let splitStr = dateStr.split('-');
convertStr = `${splitStr[0]}年${splitStr[1]}月${splitStr[2]}日`;
} else {
convertStr = dateStr.slice(5).replace('-', '月') + '日'
}
return convertStr;
},
/*
* 判断文章是否访问过
* @param contentId
*/
isVisited (contentId) {
let visitedArticles = app.globalData && app.globalData.visitedArticles || '';
return visitedArticles.indexOf(`${contentId}`) > -1;
},
}</code></pre>
<p>正常情况下,这个时候控制台打印出来的数据,是经过格式化的标准数据了,下一步,我们需要把它添加到 <code>data</code> 中的 <code>articleList</code> 字段里面,这样视图才有了渲染的数据 </p>
<p>修改 <code>index.js</code>,增加 <code>renderArticle</code> 函数。由于每次请求的都是某一页的数据,所以在函数中,我们需要把每次请求过来的列表数据都 <code>concat</code>(拼接)到 <code>articleList</code>中:</p>
<pre><code class="js">let handler = {
// 此处省略部分代码
renderArticle (data) {
if (data && data.length) {
let newList = this.data.articleList.concat(data);
this.setData({
articleList: newList
})
}
}
}</code></pre>
<p>在 <code>requestArticle</code> 函数中调用 <code>renderArticle</code>:</p>
<pre><code class="js">let handler = {
// 此处省略部分代码
requestArticle () {
// 注意:修改此处代码
if (res && res.status === 0 && res.data && res.data.length) {
let articleData = res.data;
//格式化原始数据
let formatData = this.formatArticleData(articleData);
this.renderArticle( formatData )
}
}
}</code></pre>
<h3>最终结果</h3>
<p>最终的 <code>index.js</code> 文件就是这样的:</p>
<pre><code class="javascript">'use strict';
import util from '../../utils/index'
import config from '../../utils/config'
let app = getApp()
let isDEV = config.isDev
// 后继的代码都会放在此对象中
let handler = {
data: {
page: 1, //当前的页数
days: 3,
pageSize: 4,
totalSize: 0,
hasMore: true,// 用来判断下拉加载更多内容操作
articleList: [], // 存放文章列表数据
defaultImg: config.defaultImg
},
onLoad(options) {
this.requestArticle();
},
/*
* 获取文章列表数据
*/
requestArticle() {
util.request({
url: 'list',
mock: true,
data: {
tag: '微信热门',
start: this.data.page || 1,
days: this.data.days || 3,
pageSize: this.data.pageSize,
langs: config.appLang || 'en'
}
})
.then(res => {
// 数据正常返回
if (res && res.status === 0 && res.data && res.data.length) {
let articleData = res.data;
//格式化原始数据
let formatData = this.formatArticleData(articleData);
this.renderArticle(formatData)
}
/*
* 如果加载第一页就没有数据,说明数据存在异常情况
* 处理方式:弹出异常提示信息(默认提示信息)并设置下拉加载功能不可用
*/
else if (this.data.page === 1 && res.data && res.data.length === 0) {
util.alert();
this.setData({
hasMore: false
});
}
/*
* 如果非第一页没有数据,那说明没有数据了,停用下拉加载功能即可
*/
else if (this.data.page !== 1 && res.data && res.data.length === 0) {
this.setData({
hasMore: false
});
}
/*
* 返回异常错误
* 展示后端返回的错误信息,并设置下拉加载功能不可用
*/
else {
util.alert('提示', res);
this.setData({
hasMore: false
});
return null;
}
})
},
/*
* 格式化文章列表数据
*/
formatArticleData(data) {
let formatData = undefined;
if (data && data.length) {
formatData = data.map((group) => {
// 格式化日期
group.formateDate = this.dateConvert(group.date);
if (group && group.articles) {
let formatArticleItems = group.articles.map((item) => {
// 判断是否已经访问过
item.hasVisited = this.isVisited(item.contentId);
return item;
}) || [];
group.articles = formatArticleItems;
}
return group
})
}
return formatData;
},
/*
* 将原始日期字符串格式化 '2017-06-12'
* return '今日' / 08-21 / 2017-06-12
*/
dateConvert(dateStr) {
if (!dateStr) {
return '';
}
let today = new Date(),
todayYear = today.getFullYear(),
todayMonth = ('0' + (today.getMonth() + 1)).slice(-2),
todayDay = ('0' + today.getDate()).slice(-2);
let convertStr = '';
let originYear = +dateStr.slice(0, 4);
let todayFormat = `${todayYear}-${todayMonth}-${todayDay}`;
if (dateStr === todayFormat) {
convertStr = '今日';
} else if (originYear < todayYear) {
let splitStr = dateStr.split('-');
convertStr = `${splitStr[0]}年${splitStr[1]}月${splitStr[2]}日`;
} else {
convertStr = dateStr.slice(5).replace('-', '月') + '日'
}
return convertStr;
},
/*
* 判断文章是否访问过
* @param contentId
*/
isVisited(contentId) {
let visitedArticles = app.globalData && app.globalData.visitedArticles || '';
return visitedArticles.indexOf(`${contentId}`) > -1;
},
renderArticle(data) {
if (data && data.length) {
let newList = this.data.articleList.concat(data);
this.setData({
articleList: newList
})
}
}
}
Page(handler)</code></pre>
<p>下一篇中,我们将会把数据与视图层结合在一起,动态的展示视图层</p>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=ztLS3mQyp7a6dik2olVT0w%3D%3D.yx%2FzmvVg8Vkg7fzLiRknlnvscHDLY6RZdNUveQij9UI%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h2>iKcamp最新活动</h2>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=vXt%2BzquKpAtdthcKTd8mYA%3D%3D.V5qZz8YuIyqXkFqveDJ2HuKxqqJ930NLHMtCcsnwTq0X8hXwC9ifopyOhPuiiVLl" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
微信小程序教学第三章(含视频):小程序中级实战教程:列表-静态页面制作
https://segmentfault.com/a/1190000011693109
2017-10-24T11:43:06+08:00
2017-10-24T11:43:06+08:00
iKcamp
https://segmentfault.com/u/ikcamp
0
<h2>§ 列表 - 开发准备</h2>
<blockquote><p>本文配套视频地址:<br><a href="https://link.segmentfault.com/?enc=YMX%2FMJXd4bAoImaVzucTtw%3D%3D.vyW8bh6CXyHjL3FpwPUnuDxDYgurMKJdPjW%2BBLotQ8dvjl0TK01i4cHqjhvY0LSc" rel="nofollow">https://v.qq.com/x/page/f0554...</a></p></blockquote>
<p><img src="/img/bVXd4G?w=1216&h=656" alt="图片描述" title="图片描述"></p>
<hr>
<blockquote><p>开始前请把 <code>ch3-1</code> 分支中的 <code>code/</code> 目录导入微信开发工具 <br>这一章主要会教大家如何用小程序制作一个可以无限加载的列表。希望大家能通过这个例子掌握制作各种列表的原理。</p></blockquote>
<h3>无限列表加载的原理</h3>
<p>其实所谓的无限列表就是将所有的数据分成一页一页的展示给用户看。我们每次只请求一页数据。当我们判断用户阅读完了这一页之后,立马请求下一页的数据,然后渲染出来给用户看,这样在用户看来,就感觉一直有内容可看。</p>
<p>当然,这其中很重要的一点就是,涉及到请求就肯定会有等待,处理好请求时的 <strong>加载状态</strong>,给用户以良好的体验也是非常重要的,否则如果网络状况不佳,而且没有给用户提示程序正在努力加载的话,用户很容易就以为他看完了,或者程序死掉了。</p>
<h3>我们的列表所提供的功能</h3>
<ol>
<li>静默加载</li>
<li>标记已读</li>
<li>提供分享</li>
</ol>
<p><br></p>
<h3>涉及的核心技术和 API</h3>
<ol>
<li>wx:for 的用法</li>
<li>onReachBottom 的用法</li>
<li>wx.storage 的用法</li>
<li>wx.request 的用法</li>
<li>Promise</li>
<li>onShareAppMessage 的用法</li>
</ol>
<p>我们将正式投入开发中,在这之前,我们修改 <code>app.json</code> 文件,并修改如下:</p>
<ol>
<li>修改 <code>pages</code> 字段,为小程序增加页面配置</li>
<li>修改 <code>window</code> 字段,调整小程序的头部样式,也就是 <code>navigationBar</code>
</li>
</ol>
<pre><code class="json">{
"pages":[
"pages/index/index",
"pages/detail/detail"
],
"window":{
"backgroundTextStyle":"light",
"navigationBarBackgroundColor": "#4abb3b",
"navigationBarTitleText": "iKcamp英语学习",
"backgroundColor": "#f8f8f8",
"navigationBarTextStyle":"white"
},
"netWorkTimeout": {
"request": 10000,
"connectSocket": 10000,
"uploadFile": 10000,
"downloadFile": 10000
},
"debug": true
}</code></pre>
<p>现在准备工作已经全部到位,我们开始列表页面的制作过程。 </p>
<p>可以预览下我们的最终制作效果图: </p>
<p><img src="/img/remote/1460000011693114?w=750&h=1296" alt="" title=""></p>
<p>分析下页面,很明显,日期是一个页面结构单位,一个单位里面的每篇文章也是一个小的单位。制作我们的页面如下,过程很简单,就不再复述了,修改 <code>index.wxml</code> 文件:</p>
<pre><code class="html"><view class="wrapper">
<view class="group">
<view class="group-bar">
<view class="group-title on">今日</view>
</view>
<view class="group-content">
<view class="group-content-item">
<view class="group-content-item-desc ellipsis-multi-line ellipsis-line-3">为什么聪明人总能保持冷静?</view>
<image class="group-content-item-img" mode="aspectFill" src="https://n1image.hjfile.cn/mh/2017/06/26/9ffa8c56cfd76cf5159011f4017f022e.jpg"/>
</view>
</view>
</view>
<view class="group">
<view class="group-bar">
<view class="group-title">06月27日</view>
</view>
<view class="group-content">
<view class="group-content-item">
<view class="group-content-item-desc ellipsis-multi-line ellipsis-line-3">为什么聪明人总能保持冷静?</view>
<image class="group-content-item-img" mode="aspectFill" src="https://n1image.hjfile.cn/mh/2017/06/26/9ffa8c56cfd76cf5159011f4017f022e.jpg"/>
</view>
</view>
</view>
<view class="no-more" hidden="">暂时没有更多内容</view>
</view> </code></pre>
<p>修改 <code>index.wxss</code> 文件:</p>
<pre><code class="css">.wrapper .group {
padding: 0 36rpx 10rpx 36rpx;
background: #fff;
margin-bottom: 16rpx
}
.wrapper .group-bar {
height: 114rpx;
text-align: center
}
.wrapper .group-title {
position: relative;
display: inline-block;
padding: 0 12rpx;
height: 40rpx;
line-height: 40rpx;
border-radius: 4rpx;
border: solid 1rpx #e0e0e2;
font-size: 28rpx;
color: #ccc;
margin-top: 38rpx;
overflow: visible
}
.wrapper .group-title:after,.wrapper .group-title:before {
content: '';
top: 18rpx;
position: absolute;
width: 32rpx;
height: 1rpx;
transform: scaleY(.5);
border-bottom: solid 1px #efefef
}
.wrapper .group-title:before {
left: -56rpx
}
.wrapper .group-title:after {
right: -56rpx
}
.wrapper .group-title.on {
border: solid 1rpx #ffc60e;
color: #ffc60e
}
.wrapper .group-title.on:after,.wrapper .group-title.on:before {
border-bottom: solid 1px #ffc60e
}
.wrapper .group-content-item {
position: relative;
width: 100%;
height: 194rpx;
margin-bottom: 28rpx
}
.wrapper .group-content-item-desc {
font-size: 36rpx;
font-weight: 500;
height: 156rpx;
line-height: 52rpx;
margin-right: 300rpx;
margin-top: 8rpx;
overflow: hidden;
color: #333
}
.wrapper .group-content-item-img {
position: absolute;
right: 0;
top: 0;
vertical-align: top;
width: 260rpx;
height: 194rpx
}
.wrapper .group-content-item.visited .group-content-item-desc {
color: #999
}
.wrapper .no-more {
height: 44rpx;
line-height: 44rpx;
font-size: 32rpx;
color: #ccc;
text-align: center;
padding: 20rpx 0
}</code></pre>
<p>静态页面已经制作完成,下一篇中,我们将带着大家开发业务流程</p>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=yeHcKyXsrFM2MtvlB3XZeA%3D%3D.1%2BHrSggDosaqL5n%2FLyuz%2BW8QCyXIDO9oVNvZPUe8cZ0%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h2>iKcamp最新活动</h2>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=Q4Gc0jgtAehha%2FyRbErCuw%3D%3D.vmvVJLqzEzHQKpDoMPIn2722a9%2FSjcTRsv2bOWIHB7fgbaxoPNsNAZP0Ncud6U7q" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>
微信小程序教学第二章(含视频):小程序中级实战教程之预备篇 - 封装网络请求及 mock 数据
https://segmentfault.com/a/1190000011677819
2017-10-23T15:03:13+08:00
2017-10-23T15:03:13+08:00
iKcamp
https://segmentfault.com/u/ikcamp
2
<h2><a>§ 封装网络请求及 mock 数据</a></h2>
<blockquote><p>本文配套视频地址:<br><a href="https://link.segmentfault.com/?enc=xJuCTxS07YYngZkNQ%2Flm5g%3D%3D.07H%2BIQu7hpwgSgiE6YH1zTshLKiszn4dwG68MPomfMltfS9e0%2BVtnR%2BCPs2RFYvI" rel="nofollow">https://v.qq.com/x/page/i0554...</a></p></blockquote>
<hr>
<blockquote><p>开始前请把 <code>ch2-3</code> 分支中的 <code>code/</code> 目录导入微信开发工具</p></blockquote>
<p>上一节中,我们对 index.js 文件中增加了 util 对象,并在对象中封装了很多公用方法</p>
<pre><code class="js">let util = {
log(){……},
alert(){……},
getStorageData(){……},
setStorageData(){……}
}</code></pre>
<p>本节中,我们对常用的网络请求方法 <a href="https://link.segmentfault.com/?enc=MyZPSTpCf716Zy3n%2F1SOdQ%3D%3D.rYPUfYy9V3Y6KWB1c5d4LaECwWF0Ehizj%2FbjVtOV%2BbTITVTuOCTc008%2Fen0lQX0078wtZg4udXX%2B%2B3Qt3kNAzGaGDZ%2FO8dK8Ha2yd6Xgsu4%3D" rel="nofollow">wx.request</a> 进行封装</p>
<pre><code class="js"> let util = {
// 此处省略部分代码
request(opt){
let {url, data, header, method, dataType} = opt
let self = this
return new Promise((resolve, reject)=>{
wx.request({
url: url,
data: data,
header: header,
method: method,
dataType: dataType,
success: function (res) {
if (res && res.statusCode == 200 && res.data) {
resolve(res.data);
} else {
self.alert('提示', res);
reject(res);
}
},
fail: function (err) {
self.log(err);
self.alert('提示', err);
reject(err);
}
})
})
}
}</code></pre>
<p>对于请求的参数,我们设置下默认值,方便调用</p>
<pre><code class="js"> const DEFAULT_REQUEST_OPTIONS = {
url: '',
data: {},
header: {
"Content-Type": "application/json"
},
method: 'GET',
dataType: 'json'
}
let util = {
// 此处省略部分代码
request (opt){
let options = Object.assign({}, DEFAULT_REQUEST_OPTIONS, opt)
let {url, data, header, method, dataType, mock = false} = options
let self = this
// 此处省略部分代码
}
}</code></pre>
<p>如果是本地开发调试,需要增加我们的 mock 假数据,对 util.request 进行修改</p>
<pre><code class="js"> let util = {
// 此处省略部分代码
request (opt){
let options = Object.assign({}, DEFAULT_REQUEST_OPTIONS, opt)
let {url, data, header, method, dataType, mock = false} = options
let self = this
return new Promise((resolve, reject)=>{
if(mock){
let res = {
statusCode: 200,
data: Mock[url]
}
if (res && res.statusCode == 200 && res.data) {
resolve(res.data);
} else {
self.alert('提示', res);
reject(res);
}
}else{
wx.request({
url: url,
data: data,
header: header,
method: method,
dataType: dataType,
success: function (res) {
if (res && res.statusCode == 200 && res.data) {
resolve(res.data);
} else {
self.alert('提示', res);
reject(res);
}
},
fail: function (err) {
self.log(err);
self.alert('提示', err);
reject(err);
}
})
}
})
}
}</code></pre>
<p>如果请求接口调用时候,包含有参数 mock = true,会自动调用相应的 mock 数据,如果没有这个参数,就走正常流程去调数据。 </p>
<p>调用方法如下:</p>
<pre><code class="js"> util.request({
url: 'list',
mock: true,
data: {
tag:'微信热门',
start: 1,
days: 3,
pageSize: 5,
langs: 'en'
}
}).then(res => {
// do something
})</code></pre>
<blockquote>
<p>iKcamp官网:<a href="https://link.segmentfault.com/?enc=OrDC%2FNnMc516BkeaZtShRw%3D%3D.x%2BLeSMTLOyQYUCAzEKq4t%2B91LUhqoePmNxoJEpxph1o%3D" rel="nofollow">http://www.ikcamp.com</a></p>
<p>访问官网更快阅读全部免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。<br>包含:文章、视频、源代码</p>
</blockquote>
<p><img src="/img/remote/1460000010953661" alt="" title=""></p>
<blockquote><p>iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。</p></blockquote>
<h3>iKcamp最新活动</h3>
<p><img src="/img/remote/1460000011612600?w=540&h=320" alt="" title=""></p>
<blockquote>
<p>报名地址:<a href="https://link.segmentfault.com/?enc=hWpkSqfizcFz9eRWUZd%2FPQ%3D%3D.KiIUh93MkF2v5lLap1OBxYAKPI8nS%2FwDuYuevx1XERSKmksKvDEYnLc6wiJjtI9Y" rel="nofollow">http://www.huodongxing.com/ev...</a></p>
<p>与<code>“天天练口语”</code>小程序总榜排名第四、教育类排名第一的研发团队,面对面沟通交流。</p>
</blockquote>