Node.js + React Native毕设:农业物联网监测系统的开发手记

毕设大概是大学四年里最坑爹之一的事情了,毕竟一旦选题不好,就很容易浪费一年的时间做一个并没有什么卵用,又不能学到什么东西的鸡肋项目。所幸,鄙人所在的硬件专业,指导老师并不懂软件,他只是想要一个农业物联网的监测系统,能提供给我的就是一个Oracle 11d数据库,带着一个物联网系统运行一年所保存的传感器数据…That’s all。然后,因为他不懂软件,所以他显然以结果为导向,只要我交出一个移动客户端和一个服务端,并不会关心我在其中用了多少坑爹的新技术。

那还说什么?上!我以强烈的恶搞精神,决定采用业界最新最坑爹最有可能烂尾的技术,组成一个 Geek 大杂烩,幻想未来那个接手我工作的师兄的一脸懵逼,我露出了邪恶的笑容,一切只为了满足自己的上新欲。

全部代码在 GPL 许可证下开源:
服务端代码:https://github.com/CauT/the-wall
客户端代码:https://github.com/CauT/NightWatch

由于数据库是学校实验室所有,所以不能放出数据以供运行,万分抱歉~。理论上应该有一份文档,但事实上太懒,不知道什么时候会填坑~。

总体架构

OK,上图说明技术框架。
总体结构

该物联网监测系统整体上可分为三层:数据库层,服务器层和客户端层。

数据库和代码层

数据库层除了原有的Oracle 11d数据库以外,还额外增加了一个Redis数据库。之所以增加第二个数据库,原因为:
1. Node.js 的 Oracle 官方依赖 node-oracledb 没有ORM,也就是说,所有的对数据库的操作,都是直接执行SQL语句,简单粗暴,我担心自己孱弱的数据库功底(本行是 Android 开发)会引发锁表问题,所以通过限制只读来避开这个问题。
2. 由于该系统服务于农业企业的内部管理人员,因此其账号数量和总体数据量必然有限,因此使用 redis 这种内存型数据库,可以不必考虑非关系型数据库在容量占用上的劣势。读取速度反而较传统的 SQL 数据库有一定的优势。
3. 使用非关系型数据库比关系型数据库好玩多了(雾
4. 之所以写了右边的Git部分,是因为原本打算利用docker技术搞一个持续集成和部署的程序,实现提交代码=>自动测试=>更新服务器部署更新=>客户端自动更新 这样一整套持续交付的流程,然而最后并没有时间写。

服务器层

服务器层,采用 Node.js 的 Express 框架作为客户端的 API 后台。因为 Node.js 的单线程异步并发结构使之可以轻松实现较高的 QPS,所以非常适合 API 后端这一特点。其框架设计和主要功能如下图所示:
服务端结构

像网关层:鉴权模块这么装逼的说法,本质也就是app.use(jwt({secret: config.jwt_secret}).unless({path: ['/signin']}));一行而已。因为是直接从毕业论文里拿下来的图,毕业论文都这尿性你们懂的,所以一些故弄玄虚敬请谅解。

客户端层

客户端层绝大部分是 React Native 代码,但是监控数据的图表生成这一块功能(如下图),由于 React Native 目前没有开源的成熟实现;试图通过 Native 代码来画图表,需要实现一个 Native 和 React Native 互相嵌套的架构,又面临一些可能的困难;故而最终选择了内嵌一个 html 页面,前端代码采用百度的 Echarts 框架来绘制图表。最终的结构就是大部分 React Native + 少部分 Html5 的客户端结构。

另外就是采用了 Redux 来统一应用的事件分发和 UI 数据管理了。可以说,React Native 若能留名青史,Redux 必定是不可或缺的一大原因。这一点我们后文再述。

细节详述

服务端层

服务端接口表:
服务端接口表

服务端程序的编写过程中,往往涉及到了大量的异步操作,如数据库读取,网络请求,JSON解析等等。而这些异步操作,又往往会因为具体的业务场景的要求,而需要保持一定的执行顺序。此外,还需要保证代码的可读性,显然此时一味嵌套回调函数,只会使我们陷入代码几乎不可读的回调地狱(Callback Hell)中。最后,由于JavaScript单线程的执行环境的特性,我们还需要避免指定不必要的执行顺序,以免降低了程序的运行性能。因此,我在项目中使用Promise模式来处理多异步的逻辑过程。如下代码所示:

这是生成指定N个传感器在一段时间内的折线图的逻辑。显然,剖析业务可知,我们需要在数据库中查询N次传感器,获得N个值对象数组,然后才能去用N组数据渲染出图表的HTML页面。 可以看到,外部核心的Promise控制的流程只集中于下面的几行之中:Promise.all(queryPromises()).then(renderGraph()).catch()。即,只有获取完N个传感器的数值之后,才会去渲染图表的HTML页面,但是这N个传感器的获取过程却又是并发进行的,由Promise.all()来实现的,合理地利用了有限的机器性能资源。
然而,推入queryPromises数组中的每个Promise对象,又构成了自己的一条Promise逻辑链,只有这些子Promise逻辑链被处理完了,才可以说整个all()函数都被执行完了。子Promise逻辑链大致地可以总结为以下形式:

其中的难点在于:

  1. 合理地切分整套业务逻辑到不同的then()函数中,且一个then()中只能有一个异步过程。
  2. 函数体内的异步过程所产生的新的Promise逻辑链必须被通过return的方式挂载到父函数的Promise逻辑链中,否则即可能形成一个有先有后的控制流程。
  3. catch()函数必须要做好捕捉和输出错误的处理,否则代码编写过程中的错误即不可能被发现,异步编程的整个过程也就无从继续下去了。
  4. 值得一提的是Promise模式的引入。Node.js 自身不带有Promise,可以引入标准的ECMAScript的Promise实现,然而其功能较为简陋,对于各种API的实现过于匮乏,因此最后选择了bluebird库来引入Promise模式的语言支持。

由此我们可以看到,没有无缘无故的高性能。Node.js 的高并发的优良表现,是用异步编程的高复杂度换来的。当然,你也可以选择不要编程复杂度,即不采用 Promise,Asnyc 等等异步编程模式,任由代码沦入回调地狱之中,那么这时候的代价就是维护复杂度了。其中取舍,见仁见智。

客户端层

客户端主要功能如下表所示:
功能设计表

接下来简单介绍下几个主要页面。可以发现 iOS 明显比 Android 要来的漂亮,因为只对 iOS 做了视觉上的细调,直接迁移到 Android 上,就会由于屏幕显示的色差问题,显得非常粗糙。所以,对于跨平台的 React Native App 来说,做两套色值配置文件,以供两个平台使用,还是很有必要的。

当前数据界面

上图即是土壤墒情底栏的当前数据页面,分别在Android和iOS上的显示效果,默认展示所有当前的传感器的数值,可以通过选择传感器种类或监测站编号进行筛选,两个条件可以分别设置,选定后再点击查找,即向服务器发起请求,得到数据后刷新页面。由于React Native 的组件化设计,刷新将只刷新下侧的DashBoard部分,且,若有上次已经渲染过的MonitorView,则会复用他们,不再重复渲染,从而实现了降低CPU占用的性能优化。MonitorView,即每一个传感器的展示小方块,自上至下依次展示了传感器种类,传感器编号,当前的传感器数值以及该传感器显示数值的单位。MonitorView和Dashboard均被抽象为一个一般化,可复用的组件,使之能够被利用在气象信息、病虫害监测之中,提升了开发效率,降低了代码的重复率。

查询历史界面
上图是土壤墒情界面的历史数据界面,分别在Android和iOS上的展示效果,默认不会显示数据,直到输入了传感器种类和监测站编号,选择了年月日时间后,再点击查找,才会得到结果并显示出来。该界面并非如同当前数据界面一样,Android和iOS代码完全共用。原因在于选择月日和选择时间的控件,Android和iOS系统有各自的控件,它们也被封装为React Native中不同的控件,因此,两条绿色的选择时间的按钮,被封装为HistoricalDateSelectPad,分别放在componentIOS和componentAndroid文件夹中。界面下侧的数据监测板,即代码中的Dashboard,是复用当前数据中的Dashboard。

图表界面

上图是土壤墒情界面的图表生成界面,分别在Android和iOS上的展示效果。时间选择界面,查找按钮,选择框,均可复用前两个界面的代码,因此无需多提。值得说的是,生成的折线图,事实上是通过内嵌的WebView来显示一个网页的。图表网页的生成,则依靠的百度Echarts 第三方库,然后服务端提供了一个预先写好的前端模板,Express框架填入需要的数据,最后下发到移动客户端上,渲染生成图表。图表支持了多曲线的删减,手指选取查看具体数据点,放大缩小等功能。

Screen Shot 2016-10-06 at 8.54.14 P

上图则是实际项目应用中的Redux相关文件的结构。stores中存放全局数据store相关的实现。

actions中则存放根据模块切割开的各类action生成函数集合。在 Redux 中,改变 State 只能通过 action。并且,每一个 action 都必须是 Javascript Plain Object。事实上,创建 action 对象很少用这种每次直接声明对象的方式,更多地是通过一个创建函数。这个函数被称为Action Creator。

reducers中存放许多reducer的实现,其中RootReducer是根文件,它负责把其他reducer拼接为一整个reducer,而reducer就是根据 action 的语义来完成 State 变更的函数。Reducer 的执行是同步的。在给定 initState 以及一系列的 actions,无论在什么时间,重复执行多少次 Reducer,都应该得到相同的 newState。

性能测试

服务端

测试工具:OS X Activity Monitor
http_load
serve

客户端

iOS

测试工具:Xcode 7.3
iOS

Android

测试工具:Android Studio 1.2.0
Android

代码量相关

code

简单总结

React Native 尽管在开发上具有这样那样的坑,但是因其天生的跨平台,和优于 Html5的移动性能表现,使得他在写一些不太复杂的 App 的时候,开发速度非常快,自带两倍 buff。

React Native 蛮荒开发生存指南

引言

React Native,在过去一年大红大紫,一下子成为了许多人追捧的新兴技术热点。然而,除却蜻蜓点水般运行一个 Hello World 式的 React Native 小 App,真正想要用 React Native 写一个商用的 App,却要面临很多困难。其中最主要的,就是缺少技术资料,缺少像 Android 这样发展七八年的技术一般,在博客和技术社区上存留的大量的技术资料。因而,面向百度编程,面向 Google 编程,面向 StackOverFlow 编程这三大杀手锏,均对 React Native 开发中遇到困惑表示无可奈何。加之 Facebook 开发组的文档更新速度远远跟不上开发的速度,使得 React Native 的工程化应用之路,恍若在蛮荒生存。笔者不才,为某一商业项目开发 React Native App 已近半年,以自己的踩坑和爬坑经验撰写此文,取名蛮荒开发生存指南。

文档

1. 立足 React Native 英文文档,参考中文翻译的文档。再简陋的文档好歹也是文档。

官方英文文档地址在这里。官方文档需要注意的是,左上角有一个蓝色的版本号,点击可以翻阅过去版本号的文档。文档中有写到的技术点肯定都已经在这个版本实现了,但文档没写的技术点,则有可能也实现了,只是没写上去。

目前找到的 React Native 中文文档有两份:一份是 React Native 中文网的,另一份是极客学院上的。前者一直在持续更新,后者似乎已经很久没有更新了。可在拿不准英文文档意思的时候作为参考。

2. React 文档同样重要。

由于 React Native 实质上是 React.js 的开发思想在移动端的实现,因此,许多如 Component, Props, State, flux 等等概念,在 React Native 的文档中均没有提及,相反在 React 的文档中有着详细的讲解。关于 React.js 和 React Native 之间的关系,知乎上这篇回答讲的鞭辟入里,值得深思。React 官方英文文档地址在这里,国内志愿者翻译的中文文档在这里

3. 搜索文档的方法
由于上述文档中除 React Native 文档以外,其他文档均无配置文档搜索框。因此,有必要使用 Google 或 Baidu 加上 site:url 来全局搜索文档。

社区

4. 像对待官方文档一样认真阅读 React Native 所有版本的 release note。
React Native 文档更新速度缓慢,且不能保证覆盖所有的 feature。与之相反,release note 则会告知你新的 feature 和 bug fix,虽然很多只有一句话,但是真正有帮助的是,release note 会给出相关的 commit 链接,从而我们可以阅读代码和注释,以此来了解该 feature 或 bug fix 的内容。但是同理,总会有一些改动没有统计到 release note 中。深深的怨念…

5. 不要对 StackOverFlow 抱太大期望。
截至目前,StackOverFlow 上 React Native 相关的,且得到了满意回答的问题寥寥无几,而且大多集中于 React Native 开发环境搭建等入门踩坑问题上。这种情况是完全可以预料到的,因为从本质来说,类似 StackOverFlow 这种问答社区的优质问答积累需要漫长的时间,何况技术的细节无穷无尽,非数年之功不能处处兼顾。这是针对搜索现有问答来说,StackOverFlow 无法满足 React Native 的开发问题。同理,如果自己发帖提问,同样不能保证快速地得到满意的解答,我认为最关键的问题在于,现有的 React Native 开发者的活跃社区,不在 StackOverFlow,而在 Github 上。因此,引出第六条指南。

6. 在 Github issue 中搜索出现的问题的关键字。
react native 的Github Issue中的问题和解决极为丰富,迄今已有4000多条 issue,与 StackOverFlow 判若云泥。然而由于其是论坛的性质,因此需要耐心阅读英文对话内容,才可能找到解决的方法。此外,如不能找到相关内容,另开一个 issue 寻求帮助也不失为一种良策。

6. 遇到无法解决的问题,就升级 React Native 版本。
虽然很无脑,但的确有时候很有奇效。比如笔者在实现 React Native 内嵌 WebView 时,React Native for Android v0.18.0中 WebView 的 javaScriptEnabled 属性即便设置为 true 也依然无效。升级到 v0.22.0 即解决该问题。然而,从v0.19.0 到 v0.22.0 的 release note 对该 bug fix 根本没提 T_T。

7. 阅读源代码,是求生的最后工具。
8. 源代码中的注释往往透露了非常关键的信息。
与文档相比,React Native 的源代码结构非常清晰,代码风格干净,其注释也往往包含了使用的说明,而这些说明又往往是文档中未曾包含的。因此,阅读源代码,不失为无计可施情况下的一种解决方法。拙作初窥基于 react-art 库的 React Native SVG
即是通过阅读 React Native 源代码而有所收获的。

工具

8. 快速运行他人代码的神器 – iOS RNPlayNative
https://rnplay.org 实现了令人惊叹的 React Native 实时运行的效果,即,你可以在网页上输入 React Native 代码,然后在网页上的模拟器中直接运行代码。你也可以在 iPhone 上安装RNPlayNative 应用,扫描网站上的二维码,然后直接就可以在自己的 iPhone 上运行该代码了,完全免除了 NPM 的依赖下载 和 Xcode 编译的冗长时间。此外,该网站还提供了 React Native 框架版本的切换,Amazing!

rnplay 可以帮助 React Native 开发者快速地运行和体验他人的代码,同时也可以用于排除自身环境的错误,还可以用于快速排除旧版本引入的 bug。如此神器,然而国内却很少有人知道,希望经笔者介绍后,能被更多的 React Native 开发者所用。

和 Web 一样的代码部署速度,却有着远超 Web 的流畅手感,既让人感到不可思议,仔细想想 React Native 的早已宣传的快速部署特性,却又在情理之中。只能感慨老外们应用新技术的速度太快了。

9. react-native-logcat
一个开源的 React Native Android Log输出工具,免去了繁杂的adb命令。

调试

10. 注释调试法:虽然很 Low 但是很有效。
这里不得不提一个 React Native 在捕捉错误上的一个缺陷。如果错误是在 ComponentDidMount 之前出现的,那么 backtrace 上只会有一堆神神叨叨的 React Native 库函数,完全无法定位到你的代码中,即便只是一些小语法错误。

那么此时,除了肉眼复查代码,唯一的方法也就是注释调试了。逐行注释掉新加入的代码,观察 bug 是否会复现。

探究 React Native 中 Props 驱动的 SVG 动画和 Value 驱动动画

引言

一直以来,动画都是移动开发中极为特殊的一块。一方面,动画在交互体验上有着不可替代的优越处,然而另一方面,动画的开发又极为的耗时,需要消耗工程师大量的时间用于开发和调试。再来看前端,前端的动画实现,经过多年的发展,已分为 CSS3 动画和 JavaScript 动画。

React Native 作为一个复用前端思想的移动开发框架,并没有完整实现CSS,而是使用JavaScript来给应用添加样式。这是一个有争议的决定,可以参考这个幻灯片来了解 Facebook 做的理由。自然,在动画上,因为缺少大量的 CSS 属性,React Naive 中的动画均为 JavaScript 动画,即通过 JavaScript 代码控制图像的各种参数值的变化,从而产生时间轴上的动画效果。

React Native 的官方文档已经详细地介绍了 React Native 一般动画的使用方法和实例,在此不再赘述。然而阅读官方文档后可知,官方的动画往往是给一个完整的物体添加各种动画效果,如透明度,翻转,移动等等。但是对于物体的自身变化,比如如下这个进度条,明显是在旋转的同时也在伸缩,则缺乏必要的实现方法。这是因为,动画的本质既是图形的各种参数的数值变化的过程,文档中的 Animated.Value 就是用作被驱动的参数,可以,想要让一个圆环能够伸缩,就必须让数值变化的过程,深入到图形生成的过程中,而不是如官方文档的例子一样,仅仅是施加于图形生成完毕后的过程,那么也就无法实现改变图形自身的动画效果了。

拙作初窥基于 react-art 库的 React Native SVG已讨论了 React Native 中静态 SVG 的开发方法,本文则致力于探究 React Native 中 SVG 与 Animation 结合所实现的 SVG 动画。也就是可以改变图形自身的动画效果。此外还探究了 Value 驱动动画在实现方法上的不同之处。

Props 驱动的 SVG 动画

本节即以实现一个下图所示的旋转的进度条的例子,讲述 React Native SVG 动画的开发方法。

rotating-wedge

Wedge.art.js 位于 react-art 库下 lib/ 文件夹内,提供了 SVG 扇形的实现,然而缺乏对 cx, cy 属性的支持。另外拙作之前也提到了,Wedge中的扇形较为诡异,只有一条半径,为了实现进度条效果我把另一条半径也去掉了。我将 Wedge.art.js 拷贝到工程中,自行小修改后的代码如下。

然后就是实现的主体。其中值得关注的点是:

  1. 并非任何 Component 都可以直接用 Animated.Value 去赋值 Props,而需要对 Component 做一定的改造。AnimatedAnimated.createAnimatedComponent(Component component),是 Animated 库提供的用于把普通 Component 改造为 AnimatedComponent 的函数。阅读 React Native 源代码会发现,Animated.Text, Animated.View, Animated.Image,都是直接调用了该函数去改造系统已有的组件,如Animated.createAnimatedComponent(React.Text)
  2. Easing 库较为隐蔽,明明在react-native/Library/Animated/路径下,却又需要从React中直接引出。它为动画的实现提供了许多缓动函数,可根据实际需求选择。如 linear() 线性,quad() 二次(quad明明是四次方的意思,为毛代码实现是t*t….),cubic() 三次等等。官方文档中吹嘘 Easing 中提供了 tons of functions(成吨的函数),然而我数过了明明才14个,233333。
  3. 该动画由起始角度和终止角度两个变化的参数来控制,因此,两个Animated.Value需要同时启动,这涉及到了动画的组合问题。React Native 为此提供了 parallelsequencestaggerdelay 四个函数。其主要实现均可在react-native/Library/Animated/Animate中找到,官方文档中亦有说明。这里用的是Animated.parallel

开发中遇到的问题有:

  1. 该动画在 Android 上可以运行,但是刷新频率看上去只有两帧,无法形成一个自然过渡的动画,笔者怀疑是 React Native Android 对 SVG 的支持仍有缺陷。
  2. SVG 图形和普通 React Native View 的叠加问题,目前我还没有找到解决方法。感觉只能等 React Native 开发组的进一步支持。
  3. 动画播放总会有一个莫名其妙的下拉回弹效果,然而代码上没有任何额外的控制。

Value 驱动的动画

接下来看 Value 驱动的 SVG 动画。先解释一下 Value 和 Props 的区别。<Text color='black'></Text>,这里的 color 就是 Props,<Text>black</Text>这里的 black 就是 value。

为什么要特意强调这一点呢,如果我们想要做一个如下图所示的从1到20变动的数字,按照上节所述的方法,直接调用 Animated.createAnimatedComponent(React.Text)所生成的 Component ,然后给 Value 赋值一个Animated.Value(),然后Animated.timing…,是无法产生这样的效果的。

rising-number

必须要对库中的createAnimatedComponent()函数做一定的改造。改造后的函数如下:

为了获取必须要用到的AnimatedProps,笔者甚至违背了道德的约束,访问了双下划线前缀的变量Animated.__PropsOnlyForTests,真是罪恶啊XD。

言归正传,重要的修改有:

  1. 修改了 attachProps 函数。对于任何变动的 props,原来的代码会试图使用 setNativeProps 函数进行更新,若 setNativeProps 函数为空,才会使用 forceUpdate() 函数。对于 props,setNativeProps 函数是可行的,然而对 value 无效。我猜测,setNativeProps 方法在 Android 底层可能就是 setColor() 类似的 Java 方法,然而并没有得到实证。目前这种 forceUpdate,由注释知,是彻底更新了整个 Component,相当于先从 DOM 树上取下一个旧节点,再放上一个新节点,在性能的利用上较为浪费。
  2. 使用 PropTypes.xxx.isRequired 来进行参数的类型检查。PropTypes 检查支持的类型可在 react-native/node_modules/react/lib/ReactPropTypes.js 中看到,在此不再赘述。
  3. Animated.value() 从1到20变化的过程是一个随机采样的过程,并不一定会卡在整数值上,因此还需要做一些小处理。

值得注意的是,该动画在 Android 上虽然可以正常运行,但也存在丢帧的问题,远远不能如 iOS 上流畅自然。对于这一点,只能等待 Facebook 的进一步优化。

全部的代码如下:

初窥基于 react-art 库的 React Native SVG

技术背景

在移动应用的开发过程中,绘制基本的二维图形或动画是必不可少的。然而,考虑到Android和iOS均有一套各自的API方案,因此采用一种更普遍接受的技术方案,更有利于代码的双平台兼容。

art是一个旨在多浏览器兼容的Node style CommonJS模块。在它的基础上,Facebook又开发了react-art ,封装art,使之可以被react.js所使用,即实现了前端的svg库。然而,考虑到react.js的JSX语法,已经支持将<circle> <svg>等等svg标签直接插入到dom中(当然此时使用的就不是react-art库了)此外还有HTML canvas的存在,因此,在前端上,react-art并非不可替代。

然而,在移动端,考虑到跨平台的需求,加之web端的技术积累,react-art成为了现成的绘制图形的解决方案。react-native分别在0.10.0和0.18.0上添加了iOS和Android平台上对react-art的支持,当然,没有文档。在文档基本等于没有的情况下,笔者苦逼地翻源代码,为大家带来了(全球首发?=_=)的入门文档。

示例代码

推荐大家采用react-art自带的Example: Vector-Widget。React.js和React-Native的区别,只在于下文所述的ART获取上,然后该例子就可以同时应用在Web端和移动端上了。

成功运行Vector-Widget后的效果图:
rotating-react

Vector-Widget额外实现了旋转,以及鼠标点击事件的旋转加速响应。Web端可以看到点击加速,但是在移动端无效,原因是React Native并未对Group中onMouseDown和onMouseUp属性作处理。本文着重于静态svg的实现,暂时无视动画部分效果即可。

此外还可以用该例子感受一下叹为观止的svg动画的性能占用(摊手)。

原理和调用

获取ART

package.json中需要引入art库,笔者的版本设置是art: ^0.10.0

Android与iOS

或者使用ES6的Destructuring特性:

Web端

基本组件

获取方式

接下来的所述的代码,web端和移动端都是通用的,这也是React Native的诱惑所在。

Surface

所有的svg component必须被一个Surface标签所包含。
Props如下:

  • width: Surface的宽度。
  • height: Surface的高度。
  • style: margin系列和padding系列都生效。

Group

Group用于组合art component。比如在一个函数中返回多个svg component的情况,此时就必须要用<Group>包一下,否则即报错。<Group>可以嵌套使用。

style:margin和padding系列均无效,我怀疑不接受style。

Shape

Shape用于生成路径,语法与svg中的<path>很相似。Shape的Props如下:

  • d: 语法与svg规范相同
  • stroke: 线条颜色,”#FFFFFF”的形式
  • strokeWidth: 线条宽度,{3}的形式
  • transform:接受 new ART.Transform()生成的object,具体见下文Transform条目。

Path

语法更近似于移动端。使用方法:

可以看到,取出的Path是一个构造函数。Path对象的中的函数功能如下,大多与svg规范一致,我就再啰嗦一遍了。svg规范中<path>的d属性请参见https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d :

  • push():
  • reset(): 清空Path
  • move(x, y): 等同于’m’,移动到目的坐标,参数x和y是相对目标下的目的坐标
  • moveTo(x, y): 等同于’M’,与move只差别在x和y是绝对坐标。
  • line(x, y): 等同于’l’,从一个坐标点到另一个坐标点画直线,参数x和y是相对坐标下的目的坐标
  • lineTo(x, y): 等同于’L’,与line只差别在x和y是绝对坐标。
  • arc(x, y, rx, ry, outer): 等同于’a’,从一个坐标点向另一个坐标点画椭圆曲线,x和y是相对坐标下的目的坐标,rx和ry是椭圆的长轴半径和短轴半径,outer只有0和1两个数字,代表是大角度还是小角度。
  • arcTo(x, y, rx, ry, outer): 等同于’A’,与arc只差别在x和y是绝对坐标。
  • curve(2个,4个或6个参数): 从一个坐标点向另一个坐标点画贝塞尔曲线。
    • 当参数为两个时,等同于’t’,绘制光滑二次贝塞尔曲线。
    • 当参数为4个时,等同于’q’,绘制二次贝塞尔曲线。
    • 当参数为6个时,等同于’c’,绘制三次贝塞尔曲线。
    • 有些精通SVG的同学这时候可能就要问我了,不对啊,二次贝塞尔曲线和光滑三次贝塞尔曲线的参数都是4个,你这里没有光滑三次啊?因为开发的同学留坑没写了呀(微笑)。

Transform

实现代码路径:art/core/transform.js

Transform对象中的函数:

  • transform(xx, yx, xy, yy, x, y): transform的相对坐标版本
  • transformTo: 完整的矩阵变换,把这张位图上所有的点都做一次矩阵乘法,得到的新位图,公式如下图所示

matrix

  • translate(x, y): 位移
  • move: 相对于原参考点坐标,增减参考点的x,y坐标
  • moveTo: 等同于translateTo,容易误以为是move的绝对左边版本,吐糟不能
  • scale(x, y): 将一个元素拉伸或者压缩指定的倍数,x是宽度缩放比例,y是长度缩放比例,如果只有一个参数,则x = y。如
  • scaleTo: 在缩放的同时保持原来的长宽比。例子和效果图如下:

three-rec

  • rotate(deg, x, y): 将一个元素旋转角度deg。x和y则是用于指定旋转的原点。

效果如下图所示:

rotate-rec2
rotate-rec1

react-art中的lib

引入lib中的module

在react-art的库中,有个神奇的lib文件夹,下面除了ReactART.js以外,还有Circle.art.js,Rectangle.art.js,Wedge.art.js等,其中Circle和Rectangle分别对应于svg规范中的圆形<circle>,矩形<rect>,而Wedge则是用于生成扇形。当然,这些module都很不完善,很有可能需要二次开发,如果这样,推荐拷贝库文件到工程中再行修改。

引入它们的语句为var Circle = require('react-art/lib/Circle.art');。这种不同寻常地引用方式是Facebook开发和维护的fbjs引入的,react-native依赖了fbjs,而所有需要被输出的js文件的头部都会以// @providesModule Circle.art
的形式标明。

Circle

使用示例:

值得一提的是,Circle.art.js是个半成品,可以看到它根本没有实现svg规范中的cx和cy,因此画出来的圆的圆心始终在左上角,显示出来的也就只有半个圆。请在实际应用中自行实现,或者使用笔者提供的修改版本

当然,不用cx和cy的话,也可以设置tranform来实现平移。如:

两种方法都可以达到平移效果,但是cx, cy的方式相对来说更简洁,可读性也更好。

Rectangle

使用示例:

使用上述代码,就很直观看到这个module的缺陷了,矩形四条边不等宽(扶额)。
rectangle
此外还接受的props有:

  • radius
  • radiusTopLeft
  • radiusTopRight
  • radiusBottomLeft
  • radiusBottomRight

这里的radius指的是圆角矩形的圆角半径,接受的值类型为数字,radius为四个角的通用半径,但如果设置了具体某个角的半径,则用后者。

Wedge

Wedge是楔子的意思,然而在这里却是生成各种角度的扇形=_=。
使用示例:

生成的图形如下图:

wedge

可选Props为innerRadius,用于生成一个圆环扇形,如下图。

binWedge

呐,一看这个module也是半成品,如果stroke有颜色而fill为白色就露馅了,曲线没闭合。因此,如有需求,请自行弥补。

技术缺陷

除了之前所提到的各种各样的实现上的不完善以外,svg规范在React Native上应用的最大问题在于,有一大批web上的svg支持的css属性,还没有在React Native上实现。

以stroke类的一大批属性为例,目前可用的只有线条颜色stroke和strokeWidth,而如stroke-dasharray,stroke-dashoffset这样的可以用之实现神奇的描边等效果的CSS属性,目前还没有被支持。

在性能占用方面, 静态svg尚可接受,但如果是svg annimation,实测红米Note 2上的CPU占用率即可达50%左右,故其在生产环境的大规模应用,恐怕还需进一步的性能优化。

多React Native项目时依赖管理的最佳实践

在实际开发过程中,经常需要同时运行和修改多个React Native工程,比如运行github上的开源项目以观察某种控件的实际效果。那么此时,各项目下的初始化(npm install)就会非常的痛苦,因为React Native的文件非常大,以0.17.0为例,安装后达到309MB。尽管,我们可以通过阿里npm等镜像站的方式加速下载的过程,但是下载后的进一步编译也非常地耗时。

此外,多React Native工程还带来了React Native自身的冗余,如果创建了十几个工程,那么多占用的空间轻松达到3GB以上,非常地不友好。

npm link原理

我的解决思路是:用npm link替代npm install。npm link [package-name]命令的原理是,去检索是否已经全局安装该package,去prefix/lib/node_modules/下检索是否已经安装了当前的package,如果是,则直接用软链接的方法在本地路径指向全局package。如果没检索到,则会先在全局路径下安装该package,再去建立软链接。npm获取全局路径的命令是:npm config get prefix

需要注意的是,有package.json的路径下,不要类比npm install,就这么执行npm link。此时npm link会把当前路径作为一个本地package,在全局路径下创建一个软链接。由此可知,npm link并不会像npm install一样,读取package.json中的依赖并自动配置。

配置过程

简单三步搞定。然后运行react-native run-android,打个Android包检测一下。

纳尼,报错如下:

Looks like you installed react-native globally, maybe you meant react-native-cli?
To fix the issue, run:
npm uninstall -g react-native
npm install -g react-native-cli

原因很简单,react-native框架其实由两个部分组成:react-native和react-native-cli,前者用于提供编译环境,后者则是封装了react-native开发过程中所要用到的命令,如react-native start,实质就是封装了sh ./node_modules/react-native/packager/packager.sh

官方文档要求全局安装react-native-cli,但是局部安装react-native,这是有原因的。如果你先全局安装了react-native-cli,会在/usr/local/bin下生成一个名为react-native的软链接,其指向为:react-native -> ../lib/node_modules/react-native-cli/index.js*。而随后再次”全局”安装react-native的时候,又会生成一个名为react-native的软链接,覆盖了react-native-cli安装时生成的软链接,其指向是:../lib/node_modules/react-native/local-cli/wrong-react-native.js。由此可见,React Native官方已经意识到了这个问题,然而不知何原因并不推荐全局安装React Native。然而笔者从节约硬盘空间和加快初始化的角度,认为还是有必要全局安装React Native,从而快速npm link的,所以有必要研究该报错的原理。

因此,针对这个报错,两种解决方法:

  1. npm install -g react-native,再npm install -g react-native-cli。然而,如果以后使用过程中又升级了全局React Native,此时需看方案2。
  2. cd /usr/local/bin ln -s react-native ../lib/node_modules/react-native-cli/index.js,即可重新创建一个指向react-native-cli的软链接。如果prefix的地址不是默认的,则ln -s prefix/lib/node_modules/react-native-cli/index.js

缺陷

当前这个自动添加统一依赖的方法,存在一个问题。所有依赖于全局路径下的React Native都必须是一个版本的,npm link并没有提供多版本号依赖的解决方法。因此,还是建议选择一个常用的React Native版本安装在全局路径,个别需求其他版本号的React Native的项目,使用npm install来配置局部依赖。

插说一句,npm自身的依赖管理设计还是非常优秀的,然而React Native实在是太大了,而且我们完全有理由相信,他会更大。他其实应该是与Android SDK, Java SDK一般重量级的开发SDK,因此更应该借鉴rvm,设计一个React Native Version Manager。然而却委身于node_modules,因而产生了这种无奈的冗余。