畢設大概是大學四年裏最坑爹之一的事情了,畢竟一旦選題很差,就很容易浪費一年的時間作一個並無什麼卵用,又不能學到什麼東西的雞肋項目。所幸,鄙人所在的硬件專業,指導老師並不懂軟件,他只是想要一個農業物聯網的監測系統,能提供給個人就是一個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。