毕设大概是大学四年里最坑爹之一的事情了,毕竟一旦选题很差,就很容易浪费一年的时间作一个并无什么卵用,又不能学到什么东西的鸡肋项目。所幸,鄙人所在的硬件专业,指导老师并不懂软件,他只是想要一个农业物联网的监测系统,能提供给个人就是一个Oracle 11d数据库,带着一个物联网系统运行一年所保存的传感器数据...That's all。而后,由于他不懂软件,因此他显然以结果为导向,只要我交出一个移动客户端和一个服务端,并不会关心我在其中用了多少坑爹的新技术。html
那还说什么?上!我以强烈的恶搞精神,决定采用业界最新最坑爹最有可能烂尾的技术,组成一个 Geek 大杂烩,幻想将来那个接手我工做的师兄的一脸懵逼,我露出了邪恶的笑容,一切只为了知足本身的上新欲。前端
所有代码在 GPL 许可证下开源:node
服务端代码:https://github.com/CauT/the-wallgit
客户端代码:https://github.com/CauT/Night...github
因为数据库是学校实验室全部,因此不能放出数据以供运行,万分抱歉~。理论上应该有一份文档,但事实上太懒,不知道何时会填坑~。redis
OK,上图说明技术框架。docker

该物联网监测系统总体上可分为三层:数据库层,服务器层和客户端层。数据库
数据库层除了原有的Oracle 11d数据库之外,还额外增长了一个Redis数据库。之因此增长第二个数据库,缘由为:编程
Node.js 的 Oracle 官方依赖 node-oracledb 没有ORM,也就是说,全部的对数据库的操做,都是直接执行SQL语句,简单粗暴,我担忧本身孱弱的数据库功底(本行是 Android 开发)会引起锁表问题,因此经过限制只读来避开这个问题。json
因为该系统服务于农业企业的内部管理人员,所以其帐号数量和整体数据量必然有限,所以使用 redis 这种内存型数据库,能够没必要考虑非关系型数据库在容量占用上的劣势。读取速度反而较传统的 SQL 数据库有必定的优点。
使用非关系型数据库比关系型数据库好玩多了(雾
之因此写了右边的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模式来处理多异步的逻辑过程。以下代码所示:
function renderGraph(req, res, filtereds) { var x = []; var ys = []; var titles = []; filtereds[0].forEach(function(row) { x.push(getLocalTime(row.RECTIME)); }); filtereds.forEach(function(filtered){ if (filtered[0] == undefined) // even if at least one of multi query was succeed // fast-fail is essential for secure throw new Error('数据库返回结果为空'); var y = []; filtered.forEach(function(row) { y.push(row.ANALOGYVALUE); }); ys.push(y); titles.push(filtered[0].DEVICENAME + ': ' + filtered[0].DEVICECODE); }); res.render('graph', { titles: titles, dataX: x, dataY: ys, height: req.query.height == undefined ? 200 : req.query.height, width: req.query.width == undefined ? 300 : req.query.width, }); } function resFilter(resolve, reject, connection, resultSet, numRows, filtered) { resultSet.getRows( numRows, function (err, rows) { if (err) { console.log(err.message); reject(err); } else if (rows.length == 0) { resolve(filtered); process.nextTick(function() { oracle.releaseConnection(connection); }); } else if (rows.length > 0) { filtered.push(rows[0]); resFilter(resolve, reject, connection, resultSet, numRows, filtered); } } ); } function createQuerySingleDeviceDataPromise(req, res, device_id, start_time, end_time) { return oracle.getConnection() .then(function(connection) { return oracle.execute( "SELECT\ DEVICE.DEVICEID,\ DEVICECODE,\ DEVICENAME,\ UNIT,\ ANALOGYVALUE,\ DEVICEHISTROY.RECTIME\ FROM\ DEVICE INNER JOIN DEVICEHISTROY\ ON\ DEVICE.DEVICEID = DEVICEHISTROY.DEVICEID\ WHERE\ DEVICE.DEVICEID = :device_id\ AND DEVICEHISTROY.RECTIME\ BETWEEN :start_time AND :end_time\ ORDER\ BY RECTIME", [ device_id, start_time, end_time ], { outFormat: oracle.OBJECT, resultSet: true }, connection ) .then(function(results) { var filtered = []; var filterGap = Math.floor( (end_time - start_time) / (120 * 100) ); return new Promise(function(resolve, reject) { resFilter(resolve, reject, connection, results.resultSet, filterGap, filtered); }); }) .catch(function(err) { res.status(500).json({ status: 'error', message: err.message }); process.nextTick(function() { oracle.releaseConnection(connection); }); }); }); } function secureCheck(req, res) { let qry = req.query; if ( qry.device_ids == undefined || qry.start_time == undefined || qry.end_time == undefined ) { throw new Error('device_ids或start_time或end_time参数为undefined'); } if (req.query.end_time < req.query.start_time) { throw new Error('终止时间小于起始时间'); } }; router.get('/', function(req, res, next) { try { var device_ids; var queryPromises = []; secureCheck(req, res); device_ids = req.query.device_ids.toString().split(';'); for(let i=0; i<device_ids.length; i++) { queryPromises.push(createQuerySingleDeviceDataPromise( req, res, device_ids[i], req.query.start_time, req.query.end_time)); }; Promise.all(queryPromises) .then(function(filtereds) { renderGraph(req, res, filtereds); }).catch(function(err) { res.status(500).json({ status: 'error', message: err.message }); }) } catch(err) { res.status(500).json({ status: 'error', message: err.message }); } });
这是生成指定N个传感器在一段时间内的折线图的逻辑。显然,剖析业务可知,咱们须要在数据库中查询N次传感器,得到N个值对象数组,而后才能去用N组数据渲染出图表的HTML页面。 能够看到,外部核心的Promise控制的流程只集中于下面的几行之中:Promise.all(queryPromises()).then(renderGraph()).catch()
。即,只有获取完N个传感器的数值以后,才会去渲染图表的HTML页面,可是这N个传感器的获取过程却又是并发进行的,由Promise.all()来实现的,合理地利用了有限的机器性能资源。
然而,推入queryPromises数组中的每一个Promise对象,又构成了本身的一条Promise逻辑链,只有这些子Promise逻辑链被处理完了,才能够说整个all()函数都被执行完了。子Promise逻辑链大体地能够总结为如下形式:
function() { return new Promise().then().catch(); }
其中的难点在于:
合理地切分整套业务逻辑到不一样的then()函数中,且一个then()中只能有一个异步过程。
函数体内的异步过程所产生的新的Promise逻辑链必须被经过return的方式挂载到父函数的Promise逻辑链中,不然便可能造成一个有先有后的控制流程。
catch()函数必需要作好捕捉和输出错误的处理,不然代码编写过程当中的错误即不可能被发现,异步编程的整个过程也就无从继续下去了。
值得一提的是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框架填入须要的数据,最后下发到移动客户端上,渲染生成图表。图表支持了多曲线的删减,手指选取查看具体数据点,放大缩小等功能。

上图则是实际项目应用中的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)

测试工具:Xcode 7.3

测试工具:Android Studio 1.2.0


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