iKcamp

iKcamp 查看完整档案

上海编辑  |  填写毕业院校美团点评广告平台  |  前端 编辑 www.ikcamp.com 编辑
编辑

iKcamp由热爱原创和热翻译的小伙伴发起,成员来自美团点评、沪江等。成立于2016年7月,"iK"代表布兰登·艾克(JavaScript之父)。 追随JavaScript这门语言所秉持的精神,崇尚开放和自由的我们一同工作、分享、创作,等候更多有趣跳动的灵魂。
原创著作:《移动Web前端高效开发实战》
官网:https://www.ikcamp.com

个人动态

iKcamp 收藏了文章 · 2018-12-27

iKcamp新书上市《Koa与Node.js开发实战》

Koa与Node.js开发实战

内容摘要

Node.js 10已经进入LTS时代!其应用场景已经从脚手架、辅助前端开发(如SSR、PWA等)扩展到API中间层、代理层及专业的后端开发。Node.js在企业Web开发领域也日渐成熟,无论是在API中间层,还是在微服务中都得到了非常好的落地。本书将通过Web开发框架Koa2,引领你进入Node.js的主战场!
本书系统讲解了在实战项目中使用Koa框架开发Web应用的流程和步骤。第1章介绍Node.js的安装、开发工具及调试。第2章和第3章介绍搭建Koa实战项目的雏形。第4章详细介绍HTTP基础知识及其实战应用。第5章介绍MVC、模板引擎和文件上传等实用功能。第6~8章介绍数据库、单元测试及项目的优化与部署。第9~13章介绍从零开始搭建时下火爆的微信小程序前端及后台管理应用的全部过程,以及最终的服务器部署,包括HTTPS、Nginx。
本书示例丰富、侧重实战,以完整的实战项目贯穿全部章节,并提供书中涉及的所有源码及部分章节的配套视频教程,将是前端开发人员立足新领域和后端开发人员了解Node.js并使用Koa2开发Web应用的得力助手。

前言

Node.js诞生于2009年,到本书出版时已经有近10个年头。它扩充了JavaScript的应用范围,使JavaScript也能像其他语言一样操作各种系统资源,因此,前端工程化开发的大量工具都开始运行在Node.js环境中。由于Node.js采用事件驱动、非阻塞I/O和异步输出来提升性能,因此大量I/O密集型的应用也采用Node.js开发。掌握Node.js开发,既能极大地拓宽前端开发者的技术知识面,也能拓展前端开发者的生存空间,从目前前端开发者越来越多的环境中脱颖而出。

由于Node.js仅提供基础的类库,开发者需要自主合理地设计应用架构,并调用大量基础类库来进行开发。为了提升开发效率和降低开发门槛,相关技术社区涌现出不少基于Node.js的Web框架。

Express框架在Node.js诞生之初出现,并迅速成为主流的Web应用开发框架。在社区中,大量的第三方开发者开发了丰富的Express插件,极大地降低了基于Node.js的Web应用开发成本,同时也带动了大量的开发者选择使用Express框架开发Web应用。但Express框架采用传统的回调方式处理异步调用,对于经验不足的开发者来说,很容易将代码写成“回调地狱”,使开发的应用难以持续维护。在ECMAScript 6的规范中提出了Generator函数,依据该规范,Express的作者TJ Holowaychukhttps://github.com/tj巧妙地开发了co库https://github.com/tj/co,使开发者能够通过yield关键词,像编写同步代码一样开发异步应用,从而解决了“回调地狱”问题。2014年,他基于co库开发了新一代的Web应用开发框架Koa,用官方语言来描述这个框架就是“next generation web framework for Node.js”。

社区开发者为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框架。
尽管目前Koa非常流行,但“纯天然”支持ECMAScript 7语法的Node.js 8在2017年10月才正式发布。目前,市面上介绍Koa的书籍几乎没有,大多介绍的是Express框架,本书可以说是第一本介绍Koa的书籍。本书从Node.js基础、HTTP、Koa框架、数据库、单元测试和运维部署等方面全方位地介绍了应用开发所应具备的知识体系。通过阅读本书,读者可以了解Node.js开发的方方面面,减少实际开发中出现的问题。同时,本书的重点章节也提供了线上代码讲解和视频,读者可以在阅读本书的同时,结合线上代码讲解和视频,更容易地理解本书介绍的知识。

特别感谢杜珂珂、哈志辉、姜帅、李波、李益、盛瀚钦、田小虎、徐磊、闫萌、赵晨雪(排名不分先后)对线上培训音视频课程资源的开发和支持。

本书特色

  • 重点章节附带教学视频。

为了便于读者理解本书的内容,一些基础、重点的内容配有视频教程。读者可以访问https://ikcamp.com,结合书中内容观看视频。

  • 所有源码托管于GitHub。

为了降低读者获取源码的难度,本书的所有源码都托管于GitHub(https://github.com/ ikcamp),读者也可通过GitHub直接和本书作者沟通。

  • 一线互联网公司Node.js技术栈实战经验总结。

本书补充了前端开发者所不具备的后端开发技能和规范,介绍了如何开发Koa应用,如何通过ORM(Object Relational Mapping,对象关系映射)类库读写数据库,如何通过单元测试来保障代码质量,如何通过PM2、CI等方式启动并部署Node.js应用,以及如何采用日志、监控来保障线上应用的稳定运行等内容。

  • 典型项目案例解析,实战性强。

本书第3篇通过云相册小程序开发项目介绍了目前流行的小程序技术,包括小程序登录流程、扫码登录、文件上传、相册管理等功能。通过学习本书的相关内容,读者可以独立开发时下流行的小程序和其需要的后端服务。

本书知识体系

第1篇 基础知识(第1~4章)

这部分介绍了开发Koa应用需要具备的预备知识,包括Node.js入门、遇见Koa、路由和HTTP共4个章节。
在第1章中,介绍了Node.js的历史和发展过程,以及Node.js基础和环境准备。介绍了NPM(Node Package Manager,Node.js的第三方包管理工具),通过该包管理工具,开发者能够方便地使用大量的第三方软件包。本章还介绍了微软公司推出的免费开发工具:Visual Studio Code编辑器,以及如何使用该编辑器调试Node.js应用。
在第2章中介绍了Koa的发展历程和作为Koa核心技术的中间件。
在第3章中介绍了路由的概念,以及Koa中最流行的路由中间件koa-router。
在第4章中介绍了HTTP的基础知识,以及HTTP的后续协议HTTP/2;介绍了在Node.js中如何获取客户端传递来的数据,如何通过koa-bodyparser中间件获取请求中的body数据等。

第2篇 应用实战(第5~8章)

这部分介绍了应用开发各个环节的知识,包含构建Koa Web应用、数据库、单元测试、优化与部署共4个章节。
在第5章中介绍了MVC架构、模板引擎、静态资源,以及如何输出JSON数据,如何通过koa-multer中间件上传文件等。
在第6章中介绍了数据库的概念和以MySQL为代表的关系型数据库,以及如何通过ORM类库操作MySQL数据库;介绍了以MongoDB为代表的非关系型数据库,以及如何在Node.js中操作MongoDB;介绍了以Redis为代表的新型缓存数据库,以及如何在Node.js中利用Redis实现Session持久化。
在第7章中介绍了Chai断言库,它用来检测单元测试过程中的结果是否符合预期;介绍了Mocha测试框架,使用该框架可以编写和运行单元测试代码;介绍了使用SuperTest工具测试HTTP服务,以及通过Nock库模拟HTTP服务请求响应;最后,介绍了Nyc工具,用以检查单元测试的覆盖率、提升代码质量。
在第8章中介绍了如何记录日志和统一捕获异常,以及如何输出自定义错误页;介绍了如何通过PM2、Docker启动应用,如何通过CI集成发布应用,如何通过Nginx提供HTTPS支持;介绍了如何利用日志等途径监控服务器运行情况,以及如何利用PM2提供的Keymetrics监控云服务器。

第3篇 项目实战:从零开始搭建微信小程序后台(第9~13章)

这部分通过介绍时下最流行的小程序开发,结合具体的相册小程序来说明如何开发一个完整的小程序,以及如何部署小程序。其中,汇总本书前面章节的知识介绍了小程序的功能模块、接口开发、小程序开发、管理后台开发和服务部署。
在第9章中介绍了小程序应具备的产品功能及如何开发小程序门户网站。
在第10章中介绍了小程序登录流程,扫码登录的逻辑和实现方式,小程序中用到的接口和后台管理系统需要的接口。具体包括如何通过中间件来鉴权,如何统一控制后台管理系统的权限,如何通过Mongoose来定义数据模型和访问、存储数据,如何使用log4js记录日志。
在第11章中介绍了开发微信小程序的流程,以及如何借助微信开发者工具开发小程序。
在第12章中介绍了开发后台管理系统的整体架构和设计思路,并提供了一套登录与鉴权的技术方案。
在第13章中介绍了小程序相关服务的线上部署过程,包括对数据库、Nginx、HTTPS、和Koa服务的部署,具体包括如何通过Nginx实现把多个域名解析到同一台云服务器上,如何通过PM2管理应用。

本书适合读者

  • Web前端开发人员
  • 对Node.js应用感兴趣的开发人员
  • Node.js开发的自学者
  • 大中专院校相关专业的教师和学生
  • 相关培训机构的学员

本书由陈达孚、金晶、干珺、张利涛、戴亮、周遥、薛淑英编写。本书涉及的技术知识点较多,作者团队成员虽竭力争取奉献好的作品以使技术得到更好的普及,但难免存在疏漏和不足,读者如有问题或建议,可以直接到iKcamp的GitHub上留言。本书源码也可前往GitHub上获取,地址为https://github.com/ikcamp。本书部分内容配有视频,可前往https://camp.qianduan.group/k...

本书已经在各大电商网站开始上架,感谢对iKcamp的支持!

查看原文

iKcamp 发布了文章 · 2018-12-27

iKcamp新书上市《Koa与Node.js开发实战》

Koa与Node.js开发实战

内容摘要

Node.js 10已经进入LTS时代!其应用场景已经从脚手架、辅助前端开发(如SSR、PWA等)扩展到API中间层、代理层及专业的后端开发。Node.js在企业Web开发领域也日渐成熟,无论是在API中间层,还是在微服务中都得到了非常好的落地。本书将通过Web开发框架Koa2,引领你进入Node.js的主战场!
本书系统讲解了在实战项目中使用Koa框架开发Web应用的流程和步骤。第1章介绍Node.js的安装、开发工具及调试。第2章和第3章介绍搭建Koa实战项目的雏形。第4章详细介绍HTTP基础知识及其实战应用。第5章介绍MVC、模板引擎和文件上传等实用功能。第6~8章介绍数据库、单元测试及项目的优化与部署。第9~13章介绍从零开始搭建时下火爆的微信小程序前端及后台管理应用的全部过程,以及最终的服务器部署,包括HTTPS、Nginx。
本书示例丰富、侧重实战,以完整的实战项目贯穿全部章节,并提供书中涉及的所有源码及部分章节的配套视频教程,将是前端开发人员立足新领域和后端开发人员了解Node.js并使用Koa2开发Web应用的得力助手。

前言

Node.js诞生于2009年,到本书出版时已经有近10个年头。它扩充了JavaScript的应用范围,使JavaScript也能像其他语言一样操作各种系统资源,因此,前端工程化开发的大量工具都开始运行在Node.js环境中。由于Node.js采用事件驱动、非阻塞I/O和异步输出来提升性能,因此大量I/O密集型的应用也采用Node.js开发。掌握Node.js开发,既能极大地拓宽前端开发者的技术知识面,也能拓展前端开发者的生存空间,从目前前端开发者越来越多的环境中脱颖而出。

由于Node.js仅提供基础的类库,开发者需要自主合理地设计应用架构,并调用大量基础类库来进行开发。为了提升开发效率和降低开发门槛,相关技术社区涌现出不少基于Node.js的Web框架。

Express框架在Node.js诞生之初出现,并迅速成为主流的Web应用开发框架。在社区中,大量的第三方开发者开发了丰富的Express插件,极大地降低了基于Node.js的Web应用开发成本,同时也带动了大量的开发者选择使用Express框架开发Web应用。但Express框架采用传统的回调方式处理异步调用,对于经验不足的开发者来说,很容易将代码写成“回调地狱”,使开发的应用难以持续维护。在ECMAScript 6的规范中提出了Generator函数,依据该规范,Express的作者TJ Holowaychukhttps://github.com/tj巧妙地开发了co库https://github.com/tj/co,使开发者能够通过yield关键词,像编写同步代码一样开发异步应用,从而解决了“回调地狱”问题。2014年,他基于co库开发了新一代的Web应用开发框架Koa,用官方语言来描述这个框架就是“next generation web framework for Node.js”。

社区开发者为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框架。
尽管目前Koa非常流行,但“纯天然”支持ECMAScript 7语法的Node.js 8在2017年10月才正式发布。目前,市面上介绍Koa的书籍几乎没有,大多介绍的是Express框架,本书可以说是第一本介绍Koa的书籍。本书从Node.js基础、HTTP、Koa框架、数据库、单元测试和运维部署等方面全方位地介绍了应用开发所应具备的知识体系。通过阅读本书,读者可以了解Node.js开发的方方面面,减少实际开发中出现的问题。同时,本书的重点章节也提供了线上代码讲解和视频,读者可以在阅读本书的同时,结合线上代码讲解和视频,更容易地理解本书介绍的知识。

特别感谢杜珂珂、哈志辉、姜帅、李波、李益、盛瀚钦、田小虎、徐磊、闫萌、赵晨雪(排名不分先后)对线上培训音视频课程资源的开发和支持。

本书特色

  • 重点章节附带教学视频。

为了便于读者理解本书的内容,一些基础、重点的内容配有视频教程。读者可以访问https://ikcamp.com,结合书中内容观看视频。

  • 所有源码托管于GitHub。

为了降低读者获取源码的难度,本书的所有源码都托管于GitHub(https://github.com/ ikcamp),读者也可通过GitHub直接和本书作者沟通。

  • 一线互联网公司Node.js技术栈实战经验总结。

本书补充了前端开发者所不具备的后端开发技能和规范,介绍了如何开发Koa应用,如何通过ORM(Object Relational Mapping,对象关系映射)类库读写数据库,如何通过单元测试来保障代码质量,如何通过PM2、CI等方式启动并部署Node.js应用,以及如何采用日志、监控来保障线上应用的稳定运行等内容。

  • 典型项目案例解析,实战性强。

本书第3篇通过云相册小程序开发项目介绍了目前流行的小程序技术,包括小程序登录流程、扫码登录、文件上传、相册管理等功能。通过学习本书的相关内容,读者可以独立开发时下流行的小程序和其需要的后端服务。

本书知识体系

第1篇 基础知识(第1~4章)

这部分介绍了开发Koa应用需要具备的预备知识,包括Node.js入门、遇见Koa、路由和HTTP共4个章节。
在第1章中,介绍了Node.js的历史和发展过程,以及Node.js基础和环境准备。介绍了NPM(Node Package Manager,Node.js的第三方包管理工具),通过该包管理工具,开发者能够方便地使用大量的第三方软件包。本章还介绍了微软公司推出的免费开发工具:Visual Studio Code编辑器,以及如何使用该编辑器调试Node.js应用。
在第2章中介绍了Koa的发展历程和作为Koa核心技术的中间件。
在第3章中介绍了路由的概念,以及Koa中最流行的路由中间件koa-router。
在第4章中介绍了HTTP的基础知识,以及HTTP的后续协议HTTP/2;介绍了在Node.js中如何获取客户端传递来的数据,如何通过koa-bodyparser中间件获取请求中的body数据等。

第2篇 应用实战(第5~8章)

这部分介绍了应用开发各个环节的知识,包含构建Koa Web应用、数据库、单元测试、优化与部署共4个章节。
在第5章中介绍了MVC架构、模板引擎、静态资源,以及如何输出JSON数据,如何通过koa-multer中间件上传文件等。
在第6章中介绍了数据库的概念和以MySQL为代表的关系型数据库,以及如何通过ORM类库操作MySQL数据库;介绍了以MongoDB为代表的非关系型数据库,以及如何在Node.js中操作MongoDB;介绍了以Redis为代表的新型缓存数据库,以及如何在Node.js中利用Redis实现Session持久化。
在第7章中介绍了Chai断言库,它用来检测单元测试过程中的结果是否符合预期;介绍了Mocha测试框架,使用该框架可以编写和运行单元测试代码;介绍了使用SuperTest工具测试HTTP服务,以及通过Nock库模拟HTTP服务请求响应;最后,介绍了Nyc工具,用以检查单元测试的覆盖率、提升代码质量。
在第8章中介绍了如何记录日志和统一捕获异常,以及如何输出自定义错误页;介绍了如何通过PM2、Docker启动应用,如何通过CI集成发布应用,如何通过Nginx提供HTTPS支持;介绍了如何利用日志等途径监控服务器运行情况,以及如何利用PM2提供的Keymetrics监控云服务器。

第3篇 项目实战:从零开始搭建微信小程序后台(第9~13章)

这部分通过介绍时下最流行的小程序开发,结合具体的相册小程序来说明如何开发一个完整的小程序,以及如何部署小程序。其中,汇总本书前面章节的知识介绍了小程序的功能模块、接口开发、小程序开发、管理后台开发和服务部署。
在第9章中介绍了小程序应具备的产品功能及如何开发小程序门户网站。
在第10章中介绍了小程序登录流程,扫码登录的逻辑和实现方式,小程序中用到的接口和后台管理系统需要的接口。具体包括如何通过中间件来鉴权,如何统一控制后台管理系统的权限,如何通过Mongoose来定义数据模型和访问、存储数据,如何使用log4js记录日志。
在第11章中介绍了开发微信小程序的流程,以及如何借助微信开发者工具开发小程序。
在第12章中介绍了开发后台管理系统的整体架构和设计思路,并提供了一套登录与鉴权的技术方案。
在第13章中介绍了小程序相关服务的线上部署过程,包括对数据库、Nginx、HTTPS、和Koa服务的部署,具体包括如何通过Nginx实现把多个域名解析到同一台云服务器上,如何通过PM2管理应用。

本书适合读者

  • Web前端开发人员
  • 对Node.js应用感兴趣的开发人员
  • Node.js开发的自学者
  • 大中专院校相关专业的教师和学生
  • 相关培训机构的学员

本书由陈达孚、金晶、干珺、张利涛、戴亮、周遥、薛淑英编写。本书涉及的技术知识点较多,作者团队成员虽竭力争取奉献好的作品以使技术得到更好的普及,但难免存在疏漏和不足,读者如有问题或建议,可以直接到iKcamp的GitHub上留言。本书源码也可前往GitHub上获取,地址为https://github.com/ikcamp。本书部分内容配有视频,可前往https://camp.qianduan.group/k...

本书已经在各大电商网站开始上架,感谢对iKcamp的支持!

查看原文

赞 19 收藏 13 评论 1

iKcamp 分享了头条 · 2018-12-27

Node.js 10已经进入LTS时代!其应用场景已经从脚手架、辅助前端开发(如SSR、PWA等)扩展到API中间层、代理层及专业的后端开发。Node.js在企业Web开发领域也日渐成熟,无论是在API中间层,还是在微服务中都得到了非常好的落地。本书将通过Web开发框架Koa2,引领你进...

赞 0 收藏 5 评论 0

iKcamp 发布了文章 · 2018-04-23

React 深入系列4:组件的生命周期

文:徐超,《React进阶之路》作者

授权发布,转载请注明作者及出处


React 深入系列4:组件的生命周期

React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。

组件是构建React应用的基本单位,组件需要具备数据获取、业务逻辑处理、以及UI呈现的能力,而这些能力是要依赖于组件不同的生命周期方法的。组件的生命周期分为3个阶段:挂载阶段、更新阶段、卸载阶段,每个阶段都包含相应的生命周期方法。因为是深入系列文章,本文不会仔细介绍每个生命周期方法的使用,而是会重点讲解在使用组件生命周期时,经常遇到的疑问和错误使用方式。

服务器数据请求

初学者在使用React时,常常不知道何时向服务器发送请求,获取组件所需数据。对于组件所需的初始数据,最合适的地方,是在componentDidMount方法中,进行数据请求,这个时候,组件完成挂载,其代表的DOM已经挂载到页面的DOM树上,即使获取到的数据需要直接操作DOM节点,这个时候也是绝对安全的。有些人还习惯在constructor或者componentWillMount中,进行数据请求,认为这样可以更快的获取到数据,但它们相比componentDidMount的执行时间,提前的时间实在是太微乎其微了。另外,当进行服务器渲染时(SSR),componentWillMount是会被调用两次的,一次在服务器端,一次在客户端,这时候就会导致额外的请求发生。

组件进行数据请求的另一种场景:由父组件的更新导致组件的props发生变化,如果组件的数据请求依赖props,组件就需要重新进行数据请求。例如,新闻详情组件NewsDetail,在获取新闻详情数据时,需要传递新闻的id作为参数给服务器端,当NewsDetail已经处于挂载状态时,如果点击其他新闻,NewsDetail的componentDidMount并不会重新调用,因而componentDidMount中进行新闻详情数据请求的方法也不会再次执行。这时候,应该在componentWillReceiveProps中,进行数据请求:

componentWillReceiveProps(nextProps) {
  if(this.props.newId !== nextProps.newsId) {
    fetchNewsDetailById(nextProps.newsId)  // 根据最新的新闻id,请求新闻详情数据
  }
}

如果进行数据请求的时机是由页面上的交互行为触发的,例如,点击查询按钮后,查询数据,这时只需要在查询按钮的事件监听函数中,执行数据请求即可,这种情况一般是不会有疑问的。

更新阶段方法的调用

组件的更新是组件生命周期中最复杂的阶段,也是涉及到最多生命周期方法的阶段。

正常情况下,当组件发生更新时,组件的生命周期方法的调用顺序如下:

componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate

// 组件收到新的props(props中的数据并不一定真正发生变化)-> 决定是否需要继续执行更新过程 -> 组件代表的虚拟DOM即将更新 -> 组件重新计算出新的虚拟DOM -> 虚拟DOM对应的真实DOM更新到真实DOM树中

父组件发生更新或组件自身调用setState,都会导致组件进行更新操作。父组件发生更新导致的组件更新,生命周期方法的调用情况同上所述。如果是组件自身调用setState,导致的组件更新,其生命周期方法的调用情况如下:

shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate

可见,这种情况下componentWillReceiveProps并不会被调用。

当组件的shouldComponentUpdate返回false时,组件会停止更新过程,这时候生命周期方法的调用顺序如下:

componentWillReceiveProps -> shouldComponentUpdate -> 结束

或(组件自身调用setState,导致的组件更新):

shouldComponentUpdate -> 结束

setState的时机

组件的生命周期方法众多,哪些方法中可以调用setState更新组件状态?哪些方法中不可以呢?

  • 可以的方法

    componentWillMount、componentDidMount、componentWillReceiveProps、componentDidUpdate

    这里有几个注意点:

    1. componentWillMount 中同步调用setState不会导致组件进行额外的渲染,组件经历的生命周期方法依次是componentWillMount -> render -> componentDidMount,组件并不会因为componentWillMount中的setState调用再次进行更新操作。如果是异步调用setState,组件是会进行额外的更新操作。不过实际场景中很少在componentWillMount中调用setState,一般可以通过直接在constructor中定义state的方式代替。
    2. 一般情况下,当调用setState后,组件会执行一次更新过程,componentWillReceiveProps等更新阶段的方法会再次被调用,但如果在componentWillReceiveProps中调用setState,并不会额外导致一次新的更新过程,也就是说,当前的更新过程结束后,componentWillReceiveProps等更新阶段的方法不会再被调用一次。(注意,这里仍然指同步调用setState,如果是异步调用,则会导致组件再次进行渲染)
    3. componentDidUpdate中调用setState要格外小心,在setState前必须有条件判断,只有满足了相应条件,才setState,否组组件会不断执行更新过程,进入死循环。因为setState会导致新一次的组件更新,组件更新完成后,componentDidUpdate被调用,又继续setState,死循环就产生了。
  • 不可以的方法

    其他生命周期方法都不能调用setState,主要原因有两个:

    1. 产生死循环。例如,shouldComponentUpdate、componentWillUpdate 和 render 中调用setState,组件本次的更新还没有执行完成,又会进入新一轮的更新,导致不断循环更新,进入死循环。
    2. 无意义。componentWillUnmount 调用时,组件即将被卸载,setState是为了更新组件,在一个即将卸载的组件上更新state显然是无意义的。实际上,在componentWillUnmount中调用setState也是会抛出异常的。

render次数 != 浏览器界面更新次数

先看下面的一个例子:

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"
    })
  }
}

当我们观察浏览器渲染出的页面时,页面中Test所在div的背景色,是先显示红色,再变成黄色呢?还是直接就显示为黄色呢?

答案是:直接就显示为黄色!

这个过程中,组件的生命周期方法被调用的顺序如下:

constructor -> componentWillMount -> render -> componentDidMount -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate

组件在挂载完成后,因为setState的调用,将立即执行一次更新过程。虽然render方法被调用了两次,但这并不会导致浏览器界面更新两次,实际上,两次DOM的修改会合并成一次浏览器界面的更新。React官网介绍componentDidMount方法时也有以下说明:

Calling setState() in this method will trigger an extra rendering, but it will happen before the browser updates the screen. This guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state.

这说明,组件render的次数 不一定等于 浏览器界面更新次数。虽然JS的执行和DOM的渲染分别由浏览器不同的线程完成,但JS的执行会阻塞DOM的渲染,而上面的两次render是在一个JS事件周期内执行的,所以在两次render结束前,浏览器不会更新界面。

下篇预告:

React 深入系列5:事件处理


新书推荐《React进阶之路》

作者:徐超

毕业于浙江大学,硕士,资深前端工程师,长期就职于能源物联网公司远景智能。8年软件开发经验,熟悉大前端技术,拥有丰富的Web前端和移动端开发经验,尤其对React技术栈和移动Hybrid开发技术有深入的理解和实践经验。



美团点评广告平台大前端团队招收20192020年前端实习生(偏动效方向)

有意者邮件:yao.zhou@meituan.com

查看原文

赞 4 收藏 7 评论 1

iKcamp 发布了文章 · 2018-04-16

React 深入系列3:Props 和 State

文:徐超,《React进阶之路》作者

授权发布,转载请注明作者及出处


React 深入系列3:Props 和 State

React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。

React 的核心思想是组件化的思想,而React 组件的定义可以通过下面的公式描述:

UI = Component(props, state)

组件根据props和state两个参数,计算得到对应界面的UI。可见,props 和 state 是组件的两个重要数据源。

本篇文章不是对props 和state 基本用法的介绍,而是尝试从更深层次解释props 和 state,并且归纳使用它们时的注意事项。

Props 和 State 本质

一句话概括,props 是组件对外的接口,state 是组件对内的接口。组件内可以引用其他组件,组件之间的引用形成了一个树状结构(组件树),如果下层组件需要使用上层组件的数据或方法,上层组件就可以通过下层组件的props属性进行传递,因此props是组件对外的接口。组件除了使用上层组件传递的数据外,自身也可能需要维护管理数据,这就是组件对内的接口state。根据对外接口props 和对内接口state,组件计算出对应界面的UI。

组件的props 和 state都和组件最终渲染出的UI直接相关。两者的主要区别是:state是可变的,是组件内部维护的一组用于反映组件UI变化的状态集合;而props是组件的只读属性,组件内部不能直接修改props,要想修改props,只能在该组件的上层组件中修改。在组件状态上移的场景中,父组件正是通过子组件的props,传递给子组件其所需要的状态。

如何定义State

定义一个合适的state,是正确创建组件的第一步。state必须能代表一个组件UI呈现的完整状态集,即组件对应UI的任何改变,都可以从state的变化中反映出来;同时,state还必须是代表一个组件UI呈现的最小状态集,即state中的所有状态都是用于反映组件UI的变化,没有任何多余的状态,也不需要通过其他状态计算而来的中间状态。

组件中用到的一个变量是不是应该作为组件state,可以通过下面的4条依据进行判断:

  1. 这个变量是否是通过props从父组件中获取?如果是,那么它不是一个状态。
  2. 这个变量是否在组件的整个生命周期中都保持不变?如果是,那么它不是一个状态。
  3. 这个变量是否可以通过state 或props 中的已有数据计算得到?如果是,那么它不是一个状态。
  4. 这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个普通属性(除了props 和 state以外的组件属性 ),例如组件中用到的定时器,就应该直接定义为this.timer,而不是this.state.timer。

请务必牢记,并不是组件中用到的所有变量都是组件的状态!当存在多个组件共同依赖同一个状态时,一般的做法是状态上移,将这个状态放到这几个组件的公共父组件中。

如何正确修改State

1.不能直接修改State。

直接修改state,组件并不会重新重发render。例如:

// 错误
this.state.title = 'React';

正确的修改方式是使用setState():

// 正确
this.setState({title: 'React'});

2. State 的更新是异步的。

调用setState,组件的state并不会立即改变,setState只是把要修改的状态放入一个队列中,React会优化真正的执行时机,并且React会出于性能原因,可能会将多次setState的状态修改合并成一次状态修改。所以不能依赖当前的state,计算下个state。当真正执行状态修改时,依赖的this.state并不能保证是最新的state,因为React会把多次state的修改合并成一次,这时,this.state还是等于这几次修改发生前的state。另外需要注意的是,同样不能依赖当前的props计算下个state,因为props的更新也是异步的。

举个例子,对于一个电商类应用,在我们的购物车中,当点击一次购买按钮,购买的数量就会加1,如果我们连续点击了两次按钮,就会连续调用两次this.setState({quantity: this.state.quantity + 1}),在React合并多次修改为一次的情况下,相当于等价执行了如下代码:

Object.assign(
  previousState,
  {quantity: this.state.quantity + 1},
  {quantity: this.state.quantity + 1}
)

于是乎,后面的操作覆盖掉了前面的操作,最终购买的数量只增加了1个。

如果你真的有这样的需求,可以使用另一个接收一个函数作为参数的setState,这个函数有两个参数,第一个参数是组件的前一个state(本次组件状态修改成功前的state),第二个参数是组件当前最新的props。如下所示:

// 正确
this.setState((preState, props) => ({
  counter: preState.quantity + 1; 
}))

3. State 的更新是一个浅合并(Shallow Merge)的过程。

当调用setState修改组件状态时,只需要传入发生改变的状态变量,而不是组件完整的state,因为组件state的更新是一个浅合并(Shallow Merge)的过程。例如,一个组件的state为:

this.state = {
  title : 'React',
  content : 'React is an wonderful JS library!'
}

当只需要修改状态title时,只需要将修改后的title传给setState

this.setState({title: 'Reactjs'});

React会合并新的title到原来的组件state中,同时保留原有的状态content,合并后的state为:

{
  title : 'Reactjs',
  content : 'React is an wonderful JS library!'
}

State与Immutable

React官方建议把state当作不可变对象,一方面是如果直接修改this.state,组件并不会重新render;另一方面state中包含的所有状态都应该是不可变对象。当state中的某个状态发生变化,我们应该重新创建一个新状态,而不是直接修改原来的状态。那么,当状态发生变化时,如何创建新的状态呢?根据状态的类型,可以分成三种情况:

1. 状态的类型是不可变类型(数字,字符串,布尔值,null, undefined)

这种情况最简单,因为状态是不可变类型,直接给要修改的状态赋一个新值即可。如要修改count(数字类型)、title(字符串类型)、success(布尔类型)三个状态:

this.setState({
  count: 1,
  title: 'Redux',
  success: true
})

2. 状态的类型是数组

如有一个数组类型的状态books,当向books中增加一本书时,使用数组的concat方法或ES6的数组扩展语法(spread syntax):

// 方法一:使用preState、concat创建新数组
this.setState(preState => ({
  books: preState.books.concat(['React Guide']);
}))

// 方法二:ES6 spread syntax
this.setState(preState => ({
  books: [...preState.books, 'React Guide'];
}))

当从books中截取部分元素作为新状态时,使用数组的slice方法:

// 使用preState、slice创建新数组
this.setState(preState => ({
  books: preState.books.slice(1,3);
}))

当从books中过滤部分元素后,作为新状态时,使用数组的filter方法:

// 使用preState、filter创建新数组
this.setState(preState => ({
  books: preState.books.filter(item => {
    return item != 'React'; 
  });
}))

注意不要使用push、pop、shift、unshift、splice等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat、slice、filter会返回一个新的数组。

3. 状态的类型是简单对象(Plain Object)

如state中有一个状态owner,结构如下:

this.state = {
  owner = {
    name: '老干部',
    age: 30
  }  
}

当修改state时,有如下两种方式:

1) 使用ES6 的Object.assgin方法

this.setState(preState => ({
  owner: Object.assign({}, preState.owner, {name: 'Jason'});
}))

2) 使用对象扩展语法(object spread properties

this.setState(preState => ({
  owner: {...preState.owner, name: 'Jason'};
}))

总结一下,创建新的状态的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。当然,也可以使用一些Immutable的JS库,如Immutable.js,实现类似的效果。

那么,为什么React推荐组件的状态是不可变对象呢?一方面是因为不可变对象方便管理和调试,了解更多可参考这里;另一方面是出于性能考虑,当组件状态都是不可变对象时,我们在组件的shouldComponentUpdate方法中,仅需要比较状态的引用就可以判断状态是否真的改变,从而避免不必要的render方法的调用。当我们使用React 提供的PureComponent时,更是要保证组件状态是不可变对象,否则在组件的shouldComponentUpdate方法中,状态比较就可能出现错误。

下篇预告:

React 深入系列4:组件的生命周期


新书推荐《React进阶之路》

作者:徐超

毕业于浙江大学,硕士,资深前端工程师,长期就职于能源物联网公司远景智能。8年软件开发经验,熟悉大前端技术,拥有丰富的Web前端和移动端开发经验,尤其对React技术栈和移动Hybrid开发技术有深入的理解和实践经验。



美团点评广告平台大前端团队招收20192020年前端实习生(偏动效方向)

有意者邮件:yao.zhou@meituan.com

查看原文

赞 12 收藏 27 评论 3

iKcamp 发布了文章 · 2018-04-08

React 深入系列2:组件分类

文:徐超,《React进阶之路》作者

授权发布,转载请注明作者及出处


React 深入系列2:组件分类

React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。

React 组件有很多种分类方式,常见的分类方式有函数组件和类组件,无状态组件和有状态组件,展示型组件和容器型组件。好吧,这又是一篇咬文嚼字的文章。但是,真正把这几组概念咬清楚、嚼明白后,对于页面的组件划分、组件之间的解耦是大有裨益的。

函数组件和类组件

函数组件(Functional Component )和类组件(Class Component),划分依据是根据组件的定义方式。函数组件使用函数定义组件,类组件使用ES6 class定义组件。下面是函数组件和类组件的简单示例:

// 函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 类组件
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

上面的两种写法是等价的,但函数组件的写法要比类组件简洁,不过类组件比函数组件功能更加强大。类组件可以维护自身的状态变量,即组件的state,类组件还有不同的生命周期方法,可以让开发者能够在组件的不同阶段(挂载、更新、卸载),对组件做更多的控制。

类组件有这么多优点,是不是我们在开发中应该首选使用类组件呢?其实不然。函数组件更加专注和单一,承担的职责也更加清晰,它只是一个返回React 元素的函数,只关注对应UI的展现。函数组件接收外部传入的props,返回对应UI的DOM描述,仅此而已。当然,如上面例子所示,使用只包含一个render方法的类组件,可以实现和函数组件相同的效果。但函数组件的使用可以从思想上迫使你在设计组件时多做思考,更加关注逻辑和显示的分离,设计出更加合理的页面上组件树的结构。实际操作上,当一个组件不需要管理自身状态时,可以把它设计成函数组件,当你有足够的理由发现它需要“升级”为类组件时,再把它改造为类组件。因为函数组件“升级”为类组件是有一定成本的,这样就会要求你做这个改造前更认真地思考其合理性,而不是仅仅为了一时的方便就使用类组件。

无状态组件和有状态组件

无状态组件(Stateless Component )和有状态组件(Stateful Component),划分依据是根据组件内部是否维护state。无状态组件内部不使用state,只根据外部组件传入的props返回待渲染的React 元素。有状态组件内部使用state,维护自身状态的变化,有状态组件根据外部组件传入的props和自身的state,共同决定最终返回的React 元素。

很容易知道,函数组件一定是无状态组件,类组件则既可以充当无状态组件,也可以充当有状态组件。但如上文所述,当一个组件不需要管理自身状态时,也就是无状态组件,应该优先设计为函数组件。

展示型组件和容器型组件

展示型组件(Presentational Component)和容器型组件(Container Component),划分依据是根据组件的职责。

展示型组件的职责是:组件UI长成什么样。展示型组件不关心组件使用的数据是如何获取的,以及组件数据应该如何修改,它只需要知道有了这些数据后,组件UI是什么样子的即可。外部组件通过props传递给展示型组件所需的数据和修改这些数据的回调函数,展示型组件只是它们的使用者。展示型组件一般是无状态组件,不需要state,因为展示型组件不需要管理数据,但当展示型组件需要管理自身的UI状态时,例如控制组件内部弹框的显示与隐藏,是可以使用state的,这时的state属于UI state。既然大部分情况下展示型组件不需要state,应该优先考虑使用函数组件实现展示型组件。

容器型组件的职责是:组件数据如何工作。容器型组件需要知道如何获取子组件所需数据,以及这些数据的处理逻辑,并把数据和逻辑通过props提供给子组件使用。容器型组件一般是有状态组件,因为它们需要管理页面所需数据。

例如,下面的例子中,UserListContainer是一个容器型组件,它获取用户列表数据,然后把用户列表数据传递给展示型组件UserList,由UserList负责UI的展现。

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>
  )  
}

展示型组件和容器型组件是可以互相嵌套的,展示型组件的子组件既可以包含展示型组件,也可以包含容器型组件,容器型组件也是如此。例如,当一个容器型组件承担的数据管理工作过于复杂时,可以在它的子组件中定义新的容器型组件,由新组件分担数据的管理。展示型组件和容器型组件的划分完全取决于组件所做的事情。

总结

通过上面的介绍,可以发现这三组概念有很多重叠部分。这三组概念都体现了关注点分离的思想:UI展现和数据逻辑的分离。函数组件、无状态组件和展示型组件主要关注UI展现,类组件、有状态组件和容器型组件主要关注数据逻辑。但由于它们的划分依据不同,它们并非完全等价的概念。它们之间的关联关系可以归纳为:函数组件一定是无状态组件,展示型组件一般是无状态组件;类组件既可以是有状态组件,又可以是无状态组件,容器型组件一般是有状态组件。

下篇预告:

React 深入系列3:State 和 Props


新书推荐《React进阶之路》

作者:徐超

毕业于浙江大学,硕士,资深前端工程师,长期就职于能源物联网公司远景智能。8年软件开发经验,熟悉大前端技术,拥有丰富的Web前端和移动端开发经验,尤其对React技术栈和移动Hybrid开发技术有深入的理解和实践经验。



美团点评广告平台大前端团队招收20192020年前端实习生

有意者邮件:yao.zhou@meituan.com

查看原文

赞 2 收藏 6 评论 0

iKcamp 发布了文章 · 2018-04-04

React 深入系列1:React 中的元素、组件、实例和节点

文:徐超,《React进阶之路》作者

授权发布,转载请注明作者及出处


React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。

React 中的元素、组件、实例和节点,是React中关系密切的4个概念,也是很容易让React 初学者迷惑的4个概念。现在,老干部就来详细地介绍这4个概念,以及它们之间的联系和区别,满足喜欢咬文嚼字、刨根问底的同学(老干部就是其中一员)的好奇心。

元素 (Element)

React 元素其实就是一个简单JavaScript对象,一个React 元素和界面上的一部分DOM对应,描述了这部分DOM的结构及渲染效果。一般我们通过JSX语法创建React 元素,例如:

const element = <h1 className='greeting'>Hello, world</h1>;

element是一个React 元素。在编译环节,JSX 语法会被编译成对React.createElement()的调用,从这个函数名上也可以看出,JSX语法返回的是一个React 元素。上面的例子编译后的结果为:

const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

最终,element的值是类似下面的一个简单JavaScript对象:

const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world'
  }
}

React 元素可以分为两类:DOM类型的元素和组件类型的元素。DOM类型的元素使用像h1、div、p等DOM节点创建React 元素,前面的例子就是一个DOM类型的元素;组件类型的元素使用React 组件创建React 元素,例如:

const buttonElement = <Button color='red'>OK</Button>;

buttonElement就是一个组件类型的元素,它的值是:

const buttonElement = {
  type: 'Button',
  props: {
    color: 'red',
    children: 'OK'
  }
}

对于DOM类型的元素,因为和页面的DOM节点直接对应,所以React知道如何进行渲染。但是对于组件类型的元素,如buttonElement,React是无法直接知道应该把buttonElement渲染成哪种结构的页面DOM,这时就需要组件自身提供React能够识别的DOM节点信息,具体实现方式在介绍组件时会详细介绍。

有了React 元素,我们应该如何使用它呢?其实,绝大多数情况下,我们都不会直接使用React 元素,React 内部会自动根据React 元素,渲染出最终的页面DOM。更确切地说,React元素描述的是React虚拟DOM的结构,React会根据虚拟DOM渲染出页面的真实DOM。

组件 (Component)

React 组件,应该是大家最熟悉的React中的概念。React通过组件的思想,将界面拆分成一个个可以复用的模块,每一个模块就是一个React 组件。一个React 应用由若干组件组合而成,一个复杂组件也可以由若干简单组件组合而成。

React组件和React元素关系密切,React组件最核心的作用是返回React元素。这里你也许会有疑问:React元素不应该是由React.createElement() 返回的吗?但React.createElement()的调用本身也是需要有“人”负责的,React组件正是这个“责任人”。React组件负责调用React.createElement(),返回React元素,供React内部将其渲染成最终的页面DOM。

既然组件的核心作用是返回React元素,那么最简单的组件就是一个返回React元素的函数:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

Welcome是一个用函数定义的组件。如果使用类(class)定义组件,返回React元素的工作具体就由组件的render方法承担,例如:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

其实,使用类定义的组件,render方法是唯一必需的方法,其他组件的生命周期方法都只不过是为render服务而已,都不是必需的。

现在来考虑下面这个例子:

class Home extends React.Component {
  render() {
    return (
      <div>
        <Welcome name='老干部' />
        <p>Anything you like</p>
      </div>
    )
  }
}

Home 组件使用了Welcome组件,返回的React元素为:

{
  type: 'div',
  props: {
    children: [
      {
        type: 'Welcome',
        props: {
          name: '老干部'
        }
      },
      {
        type: 'p',
        props: {
          children: 'Anything you like'
        }
      },
    ]
  }
}

对于这个结构,React 知道如何渲染type = 'div' 和 type = 'p' 的节点,但不知道如何渲染type='Welcome'的节点,当React 发现Welcome 是一个React 组件时(判断依据是Welcome首字母为大写),会根据Welcome组件返回的React 元素决定如何渲染Welcome节点。Welcome组件返回的React 元素为:

{
  type: 'h1',
  props: {
      children: 'Hello, 老干部'
  }
}

这个结构中只包含DOM节点,React是知道如何渲染的。如果这个结构中还包含其他组件节点,React 会重复上面的过程,继续解析对应组件返回的React 元素,直到返回的React 元素中只包含DOM节点为止。这样的递归过程,让React 获取到页面的完整DOM结构信息,渲染的工作自然就水到渠成了。

另外,如果仔细思考的话,可以发现,React 组件的复用,本质上是为了复用这个组件返回的React 元素,React 元素是React 应用的最基础组成单位

实例 (Instance)

这里的实例特指React组件的实例。React 组件是一个函数或类,实际工作时,发挥作用的是React 组件的实例对象。只有组件实例化后,每一个组件实例才有了自己的props和state,才持有对它的DOM节点和子组件实例的引用。在传统的面向对象的开发方式中,实例化的工作是由开发者自己手动完成的,但在React中,组件的实例化工作是由React自动完成的,组件实例也是直接由React管理的。换句话说,开发者完全不必关心组件实例的创建、更新和销毁。

节点 (Node)

在使用PropTypes校验组件属性时,有这样一种类型:

MyComponent.propTypes = { 
  optionalNode: PropTypes.node,
}

PropTypes.node又是什么类型呢?这表明optionalNode是一个React 节点。React 节点是指可以被React渲染的数据类型,包括数字、字符串、React 元素,或者是一个包含这些类型数据的数组。例如:

// 数字类型的节点
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;
}

最后总结一下,React 元素和组件的概念最重要,也最容易混淆;React 组件实例的概念大家了解即可,几乎使用不到;React 节点有一定使用场景,但看过本文后应该也就不存在理解问题了。

下篇预告:

React 深入系列2:组件分类


新书推荐《React进阶之路》

作者:徐超

毕业于浙江大学,硕士,资深前端工程师,长期就职于能源物联网公司远景智能。8年软件开发经验,熟悉大前端技术,拥有丰富的Web前端和移动端开发经验,尤其对React技术栈和移动Hybrid开发技术有深入的理解和实践经验。

查看原文

赞 4 收藏 12 评论 0

iKcamp 收藏了文章 · 2018-04-03

从Nest到Nesk -- 模块化Node框架的实践

文: 达孚(沪江Web前端架构师)

本文原创,转至沪江技术

首先上一下项目地址(:>):

Nest:https://github.com/nestjs/nest

Nesk:https://github.com/kyoko-df/nesk

Nest初认识

Nest是一个深受angular激发的基于express的node框架,按照官网说明是一个旨在提供一个开箱即用的应用程序体系结构,允许轻松创建高度可测试,可扩展,松散耦合且易于维护的应用程序。

在设计层面虽然说是深受angular激发,但其实从后端开发角度来说类似于大家熟悉的Java Spring架构,使用了大量切面编程技巧,再通过装饰器的结合完全了关注上的分离。同时使用了Typescript(也支持Javascript)为主要开发语言,更保证了整个后端系统的健壮性。

强大的Nest架构

那首先为什么需要Nest框架,我们从去年开始大规模使用Node来替代原有的后端View层开发,给予了前端开发除了SPA以外的前后端分离方式。早期Node层的工作很简单-渲染页面代理接口,但在渐渐使用中大家会给Node层更多的寄托,尤其是一些内部项目中,你让后端还要将一些现有的SOA接口进行包装,对方往往是不愿意的。那么我们势必要在Node层承接更多的业务,包括不限于对数据的组合包装,对请求的权限校验,对请求数据的validate等等,早期我们的框架是最传统的MVC架构,但是我们翻阅业务代码,往往最后变成复杂且很难维护的Controller层代码(从权限校验到页面渲染一把撸到底:))。

那么我们现在看看Nest可以做什么?从一个最简单的官方例子开始看:

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

这里就启动了一个nest实例,先不看这个ValidationPipe,看ApplicationModule的内容:

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewaresConsumer): void {
    consumer
      .apply(LoggerMiddleware)
      .with('ApplicationModule')
      .forRoutes(CatsController);
  }
}
@Module({
  controllers: [CatsController],
  components: [CatsService],
})
export class CatsModule {}

这里看到nest的第一层入口module,也就是模块化开发的根本,所有的controller,component等等都可以根据业务切分到某个模块,然后模块之间还可以嵌套,成为一个完整的体系,借用张nest官方的图:

在nest中的component概念其实一切可以注入的对象,对于依赖注入这个概念在此不做深入解释,可以理解为开发者不需要实例化类,框架会进行实例化且保存为单例供使用。

@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);
  }
}

Controller的代码非常精简,很多重复的工作都通过guards和interceptors解决,第一个装饰器Controller可以接受一个字符串参数,即为路由参数,也就是这个Controller会负责/cats路由下的所有处理。首先RolesGuard会进行权限校验,这个校验是自己实现的,大致结构如下:

@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;
    }

    // 自行实现
  }
}

context可以获取controller的相关信息,再通过反射拿到handler上是否有定义roles的元信息,如果有就可以在逻辑里根据自己实现的auth方法或者用户类型来决定是否让用户访问相关handler。

interceptors即拦截器,它可以:

  • 在方法执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 根据所选条件完全重写函数 (例如, 缓存目的)

本示例有两个拦截器一个用来记录函数执行的时间,另一个对结果进行一层包装,这两个需求都是开发中很常见的需求,而且拦截器会提供一个rxjs的观察者流来处理函数返回,支持异步函数,我们可以通过map()来mutate这个流的结果,可以通过do运算符来观察函数观察序列的执行状况,另外可以通过不返回流的方式,从而阻止函数的执行,LoggingInterceptor例子如下:

@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`),
    );
  }
}

回到最初的ValidationPipe,它是一个强大的校验工具,我们看到前面的controller代码中插入操作中有一个CreateCatDto,dto是一种数据传输对象,一个dto可以这样定义:

export class CreateCatDto {
  @IsString() readonly name: string;

  @IsInt() readonly age: number;

  @IsString() readonly breed: string;
}

然后ValidationPipe会检查body是否符合这个dto,如果不符合就会就会执行你在pipe中设置的处理方案。具体是如何实现的可以再写一篇文章了,所以我推荐你看nest中文指南(顺便感谢翻译的同学们)

示例的完整代码可以看01-cats-app

也就是说业务团队中的熟练工或者架构师可以开发大量的模块,中间件,异常过滤器,管道,看守器,拦截器等等,而不太熟练的开发者只需要完成controller的开发,在controller上像搭积木般使用这些设施,即完成了对业务的完整搭建。

Nesk-一个落地方案的尝试

虽然我个人很喜欢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对于我们来说只是一个更完美的开发范式,不承接任何公共模块。

所以我们需要的工作可以简单总结为:

  1. 支持Koa
  2. 适配Aconite

支持Koa我们在Nest的基础上做了一些小改动完成了Nesk来兼容Koa体系。我们只需要完成Nesk和Aconite中间的Adapter层,就可以完成Nesk的落地,最后启动处的代码变成:

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);
}

最后Nest有很多@nest scope下的包,方便一些工具接入nest,如果他们与express没有关系,我们其实是可以直接使用的。但是包内部往往依赖@nest/common或者@nesk/core,这里可以使用module-alias,进行一个重指向(你可以尝试下graphql的例子):

"_moduleAliases": {
  "@nestjs/common": "node_modules/@neskjs/common",
  "@nestjs/core": "node_modules/@neskjs/core"
}

Nesk的地址Nesk,我们对Nesk做了基本流程测试目前覆盖了common和core,其它的在等待改进,欢迎一切愿意一起改动的开发者。

不足与期待

其实从一个更好的方面来说,我们应当允许nest接受不同的底层框架,即既可以使用express,也可以使用koa,通过一个adapter层抹平差异。不过这一块的改造成本会大一些。

另一方面nest有一些本身的不足,在依赖注入上,还是选择了ReflectiveInjector,而Angular已经开始使用了StaticInjector,理论上StaticInjector减少了对Map层级的查找,有更好的性能,这也是我们决定分叉出一个nesk的原因,可以做一些更大胆的内部代码修改。另外angular的依赖注入更强大,有例如useFactory和deps等方便测试替换的功能,是需要nest补充的.

最后所有的基于Koa的框架都会问到一个问题,能不能兼容eggjs(:)),其实无论是Nest还是Nesk都是一个强制开发规范的框架,只要eggjs还建立在koa的基础上,就可以完成集成,只是eggjs在启动层面的改动较大,而且开发范式和nest差异比较多,两者的融合并没有显著的优势。

总之Node作为一个比较灵活的后端开发方式,每个人心中都有自己觉得合适的开发范式,如果你喜欢这种方式,不妨尝试下Nest或者Nesk。

查看原文

iKcamp 发布了文章 · 2018-04-03

从Nest到Nesk -- 模块化Node框架的实践

文: 达孚(沪江Web前端架构师)

本文原创,转至沪江技术

首先上一下项目地址(:>):

Nest:https://github.com/nestjs/nest

Nesk:https://github.com/kyoko-df/nesk

Nest初认识

Nest是一个深受angular激发的基于express的node框架,按照官网说明是一个旨在提供一个开箱即用的应用程序体系结构,允许轻松创建高度可测试,可扩展,松散耦合且易于维护的应用程序。

在设计层面虽然说是深受angular激发,但其实从后端开发角度来说类似于大家熟悉的Java Spring架构,使用了大量切面编程技巧,再通过装饰器的结合完全了关注上的分离。同时使用了Typescript(也支持Javascript)为主要开发语言,更保证了整个后端系统的健壮性。

强大的Nest架构

那首先为什么需要Nest框架,我们从去年开始大规模使用Node来替代原有的后端View层开发,给予了前端开发除了SPA以外的前后端分离方式。早期Node层的工作很简单-渲染页面代理接口,但在渐渐使用中大家会给Node层更多的寄托,尤其是一些内部项目中,你让后端还要将一些现有的SOA接口进行包装,对方往往是不愿意的。那么我们势必要在Node层承接更多的业务,包括不限于对数据的组合包装,对请求的权限校验,对请求数据的validate等等,早期我们的框架是最传统的MVC架构,但是我们翻阅业务代码,往往最后变成复杂且很难维护的Controller层代码(从权限校验到页面渲染一把撸到底:))。

那么我们现在看看Nest可以做什么?从一个最简单的官方例子开始看:

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

这里就启动了一个nest实例,先不看这个ValidationPipe,看ApplicationModule的内容:

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewaresConsumer): void {
    consumer
      .apply(LoggerMiddleware)
      .with('ApplicationModule')
      .forRoutes(CatsController);
  }
}
@Module({
  controllers: [CatsController],
  components: [CatsService],
})
export class CatsModule {}

这里看到nest的第一层入口module,也就是模块化开发的根本,所有的controller,component等等都可以根据业务切分到某个模块,然后模块之间还可以嵌套,成为一个完整的体系,借用张nest官方的图:

在nest中的component概念其实一切可以注入的对象,对于依赖注入这个概念在此不做深入解释,可以理解为开发者不需要实例化类,框架会进行实例化且保存为单例供使用。

@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);
  }
}

Controller的代码非常精简,很多重复的工作都通过guards和interceptors解决,第一个装饰器Controller可以接受一个字符串参数,即为路由参数,也就是这个Controller会负责/cats路由下的所有处理。首先RolesGuard会进行权限校验,这个校验是自己实现的,大致结构如下:

@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;
    }

    // 自行实现
  }
}

context可以获取controller的相关信息,再通过反射拿到handler上是否有定义roles的元信息,如果有就可以在逻辑里根据自己实现的auth方法或者用户类型来决定是否让用户访问相关handler。

interceptors即拦截器,它可以:

  • 在方法执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 根据所选条件完全重写函数 (例如, 缓存目的)

本示例有两个拦截器一个用来记录函数执行的时间,另一个对结果进行一层包装,这两个需求都是开发中很常见的需求,而且拦截器会提供一个rxjs的观察者流来处理函数返回,支持异步函数,我们可以通过map()来mutate这个流的结果,可以通过do运算符来观察函数观察序列的执行状况,另外可以通过不返回流的方式,从而阻止函数的执行,LoggingInterceptor例子如下:

@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`),
    );
  }
}

回到最初的ValidationPipe,它是一个强大的校验工具,我们看到前面的controller代码中插入操作中有一个CreateCatDto,dto是一种数据传输对象,一个dto可以这样定义:

export class CreateCatDto {
  @IsString() readonly name: string;

  @IsInt() readonly age: number;

  @IsString() readonly breed: string;
}

然后ValidationPipe会检查body是否符合这个dto,如果不符合就会就会执行你在pipe中设置的处理方案。具体是如何实现的可以再写一篇文章了,所以我推荐你看nest中文指南(顺便感谢翻译的同学们)

示例的完整代码可以看01-cats-app

也就是说业务团队中的熟练工或者架构师可以开发大量的模块,中间件,异常过滤器,管道,看守器,拦截器等等,而不太熟练的开发者只需要完成controller的开发,在controller上像搭积木般使用这些设施,即完成了对业务的完整搭建。

Nesk-一个落地方案的尝试

虽然我个人很喜欢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对于我们来说只是一个更完美的开发范式,不承接任何公共模块。

所以我们需要的工作可以简单总结为:

  1. 支持Koa
  2. 适配Aconite

支持Koa我们在Nest的基础上做了一些小改动完成了Nesk来兼容Koa体系。我们只需要完成Nesk和Aconite中间的Adapter层,就可以完成Nesk的落地,最后启动处的代码变成:

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);
}

最后Nest有很多@nest scope下的包,方便一些工具接入nest,如果他们与express没有关系,我们其实是可以直接使用的。但是包内部往往依赖@nest/common或者@nesk/core,这里可以使用module-alias,进行一个重指向(你可以尝试下graphql的例子):

"_moduleAliases": {
  "@nestjs/common": "node_modules/@neskjs/common",
  "@nestjs/core": "node_modules/@neskjs/core"
}

Nesk的地址Nesk,我们对Nesk做了基本流程测试目前覆盖了common和core,其它的在等待改进,欢迎一切愿意一起改动的开发者。

不足与期待

其实从一个更好的方面来说,我们应当允许nest接受不同的底层框架,即既可以使用express,也可以使用koa,通过一个adapter层抹平差异。不过这一块的改造成本会大一些。

另一方面nest有一些本身的不足,在依赖注入上,还是选择了ReflectiveInjector,而Angular已经开始使用了StaticInjector,理论上StaticInjector减少了对Map层级的查找,有更好的性能,这也是我们决定分叉出一个nesk的原因,可以做一些更大胆的内部代码修改。另外angular的依赖注入更强大,有例如useFactory和deps等方便测试替换的功能,是需要nest补充的.

最后所有的基于Koa的框架都会问到一个问题,能不能兼容eggjs(:)),其实无论是Nest还是Nesk都是一个强制开发规范的框架,只要eggjs还建立在koa的基础上,就可以完成集成,只是eggjs在启动层面的改动较大,而且开发范式和nest差异比较多,两者的融合并没有显著的优势。

总之Node作为一个比较灵活的后端开发方式,每个人心中都有自己觉得合适的开发范式,如果你喜欢这种方式,不妨尝试下Nest或者Nesk。

查看原文

赞 4 收藏 3 评论 0

iKcamp 收藏了文章 · 2018-03-17

如何在原生微信小程序中实现数据双向绑定

官网:https://qiu8310.github.io/minapp/

作者:Mora

在原生小程序开发中,数据流是单向的,无法双向绑定,但是要实现双向绑定的功能还是蛮简单的!

下文要讲的是小程序框架 minapp 中实现双向绑定的原理,在 minapp 中,你只需要在 wxml 模板中给组件的属性名后加上 .sync 就可以实现双向绑定。下面为了解释其原理,过程可能会说的稍微复杂些,但其实 minapp 框架已经处理了那些繁杂的细节!

首先,要使数据双向绑定,应该避免过多的数据源
在数据从上到下自然流动的情况下,如果每个组件中都维护它们自己的数据,而又要保持它们数据值的一致,这虽然可以做到,但实现过程并不会简单。
但是也没必要说为了有一个统一的数据源就使用 mobxredux 来全局管理数据,这就有点杀鸡用牛刀的感觉了。
由于双向绑定只存在于父子组件之间,而数据又是从父到子传递的,所以可以优先使用父组件中的数据为数据源,
子组件每次更新数据并不更新它自己内部的数据,而是通过事件机制触发父组件更新它的数据,而父组件更新数据后又会将更新的数据自然地传给子组件,
由此达到数据的双向流动!

data-stream

并不是所有数据都需要双向绑定,也并不是所有数据都是对外的,子组件还可以有它自己的一个内部数据。所以这就涉及到我们要说的第二个问题:区分哪些数据需要双向绑定,哪些数据又需要子组件自己维护

用过 vue 的应该都知道,在 vue 中要实现双向绑定,需要在模板中做特殊处理。比如要将父组件的 parentAttr 双向绑定到子组件的 childAttr 上,需要在父组件的模板中这样写:

<child childAttr.sync="parentAttr" />

但是小程序并没有这样的简单的语法,小程序的 wxml 语言的属性名中甚至都不允许出现 " . " 这样的字符。回到我们的问题上来,子组件需要知道哪些属性需要双向绑定,哪些属性需要自己维护
给模板加个字段(syncAttrMap)专门来告诉子组件需要双向绑定的数据集合不就行了么。如,可以将上面的示例写成微信小程序支持的写法:

<child childAttr="{{parentAttr}}" syncAttrMap="childAttr=parentAttr" />

<!--
  如果同时存在多个双向绑定和不需要双向绑定的属性时,可以写成下面这样:
  p1, p2 分别双向绑定到子组件的 c1, c2,而 p3 单向绑定到 c3 上
-->

<child c1="{{p1}}" c2="{{p2}}" c3="{{p3}}" syncAttrMap="c1=p1&c2=p2" />

接着,就需要处理子组件数据更新的问题了,在子组件中有两部分数据,一部分是内部数据,另一部分是父组件中的数据,
子组件可以通过读取属性 syncAttrMap 来得到哪些数据是内部的数据,哪些数据是父组件的数据,并且可以知道对应
的父组件中的数据的键名是什么。由于原生的组件方法 setData 不会管你是内部数据,还是父组件中的数据,只要
你调用它去更新数据,它只会更新内部的数据。所以需要另外实现一个新的方法,来自动判断数据源,如果是内部数据,
则直接调用 setData ;如果是双向绑定中的父组件数据,则可以触发一个事件去通知父组件去更新对应的值。

所以根据上面的描述,父组件需要有个监听函数,子组件需要有个智能的 setData 函数。不防将父组件的监听函数
命名为 onSyncAttrUpdate,将子组件的智能 setData 函数命名为 setDataSmart,则可以有如下代码:

// 父组件
Component({
  methods: {
    onSyncAttrUpdate(e) {
      this.setData(e.detail) // 子组件传来的需要更新的数据
    }
  }
})
<!-- 父组件的模板 -->
<child childAttr="{{parentAttr}}" syncAttrMap="childAttr=parentAttr" bind:syncAttrUpdate="onSyncAttrUpdate" />
// 子组件
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)
      }
    }
  }
})

到此,一个简单的双向绑定功能就完成了。但是由于子组件也有可能包含其它组件,也就是说子组件也可以是父组件,而父组件同样也
可以是子组件。所以上面的 onSyncAttrUpdatesetDataSmart 函数需要在每个组件中都实现,所以不防
定义一个公共对象 BaseComponent 来实现上面的所有功能,如:

// BaseComponent
const BaseComponent = {
  properties: {
    syncAttrMap: String
  },
  methods: {
    setDataSmart() {
      // ...
    },
    onSyncAttrUpdate() {
      // ...
    }
  }
}

然后将 BaseComponent minin 到每个组件的对象上去就可以了;另外小程序中还有一个特殊的组件:Page,虽然 Page 和 Component 结构是两样的,
但它也应该算是一个组件,不过它一定是父组件,不可能是别的组件的子组件,所以还需要将 onSyncAttrUpdate 方法写了所有的 Page 定义中。
所有这些就是 minapp 的双向绑定的基本原理了。

等等,最后还有一件事:wxml 模板,不能让用户每次写双向绑定的时候都要写那么复杂语句吧?当然不用,minapp 在编译时,会将模板做个简单的转化:

<child childAttr.sync="parentAttr" />

<!-- 由于属性名 syncAttrMap 是固定的,所以完全可以通过编译手段,将上面的模板转成下面这个模板 -->

<child childAttr="{{parentAttr}}" syncAttrMap="childAttr=parentAttr" />

谢谢,文章到此结束,欢迎关注 minapp:重新定义微信小程序的开发

查看原文

认证与成就

  • 获得 670 次点赞
  • 获得 21 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 21 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • 《移动Web前端高效开发实战》

    一线互联网公司Web前端团队实战经验总结,涵盖移动Web前端开发各个关键技术环节,包括移动开发核心技术、常用布局方案、MV*类新时代框架、预编译技术、性能优化、开发调试、混合式应用、单元测试、工程化等

注册于 2017-08-28
个人主页被 4.4k 人浏览