[學習筆記] Cordova AmazeUI React 做個通訊錄 – 使用 SQLite

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

[學習筆記] Cordova AmazeUI React 做個通訊錄 系列文章

目錄

  1. 準備
  2. 聯絡人列表(1)
  3. 聯絡人列表(2)
  4. 聯絡人詳情
  5. 單頁應用 (With Router)
  6. 使用 SQLite

傳送門:全部章節 示例程式碼


通訊錄做到這個程度,應該考慮增刪改功能了。但是,增刪改功能的前提是能進行相應的資料持久化操作。因為需要先研究在 Cordova 中使用 SQLite。

為 Cordova 新增 SQLite 外掛

Apache Cordova Plugin Search 頁面搜尋 sqlite。排名靠前的有 cordova-sqlite-storage 和 cordova-plugin-sqlite 等,從下載量來看,我選擇了前者。

Apache Cordova Plugin Search 開啟之後會需要一些時間來載入資料,所以得等一等才會出現搜尋框。

雖然搜尋是在這裡搜,但是安裝是在控制檯下。進入 contacts 目錄(也就是 www 的上級目錄),然後執行這個命令

cordova plugin add cordova-sqlite-storage 

準備試執行和除錯

deviceready

cordova-sqlite-storage 外掛會為 window 新增 sqliteDatebase 屬性,但必須在裝置準備好之後才能使用,所以需要等等觸發 Cordova 的 deviceready 事件。之前生成的 index.js 還沒有刪除掉,所以可以看到註冊和響應 deviceready 事件的程式碼。

示例程式碼中定義了 app 物件,其 initialize 方法是入口,在最下面呼叫。而 initialize 只幹了一件事就是 bindEvents,bindEvents 也只幹了一件事就是將 deviceready 事件繫結到處理函式 this.onDeviceReady。這整個過程實在複雜,所以用立即執行的函式簡化一下

(function() {
function onDeviceReady() {
console.log("device is ready");
}
document.addEventListener("deviceready", onDeviceReady, false);
})();

引入 cordova.js

由於之前把引入 cordova.js 的 <script> 標籤從 index.html 中刪掉了,所以現在得加回來。直接加在所有 <script> 的最前面就好

<script type="text/javascript" charset="utf-8" src="cordova.js"></script>

這個 <script>typecharset 部分都可以省略掉,不過最好在 <head> 的最前面加上

<meta charset="utf-8" />

之前雖然忘了加,但也執行得好好的,不過加上總不是壞事,畢竟我們所有原始檔都是 utf8 編碼的。

Logcat 和 mLogcat

Cordova 的除錯是件比較痛苦的事情,雖然也有專用的除錯工具,但是好用的收費,不收費的難用。Eclipse 到是可以除錯,就是太重量級了。幸好前端開發養成了使用 console.log() 的除錯習慣。

console.log() 的輸出已經由 Cordova 封裝成了 Android 上的 Logcat 輸出,只需要找一個 Logcat 的檢視器就行。

Windows 下可以用 adb logcat | findstr 來過濾和檢視需要的日誌。grep 後面要跟需要過濾的字串作為引數,更詳情的用法可以執行執行命令 findstr /? 檢視幫助資訊。

  • findstr 在 Win8 和 Win10 下可用,Win7 和更早的版本沒有嘗試過。

不過命令列檢視輸出不是很方便。我找了很多 logcat 工具之後,決定使用 mLogcat

先把手機連上電腦,然後開啟 mLogcat,這時候預設會顯示全部的日誌,在訊息視窗右鍵,選單中選擇 “Find/Refilter Item [Ctrl F]”,會開啟一下過濾視窗,輸入要過濾(顯示出來)的內容,比如 cn.jamesfancy.contacts,就可以看到相關的日誌了。“Refilter Item [Alt R]” 可能更詳細的設定過濾,但是沒有按“Process Name”過濾的選項。但是如果找到了應用和 TID 或 PID,用這個過濾還是挺好的(注意,每次啟動 PID 和 TID 都會變)。

clipboard.png

通過 console.log() 輸出的日誌在 mLogcat 中很容易看到,它會有一個字首 [INFO:CONSOLE(#)],其中 # 表示數。

如果大家發現有其它好用的輕量 Logcat 檢視工具,請介紹給我哦

相容瀏覽器和 Android

即使有了日誌式的除錯方法和 mLogcat,在手機或模擬器上除錯應用也是個複雜的過程,因為還需要編譯、安裝等步驟。cordova run android 可以一步完成,但是需要些時間。所以最好的辦法還是在瀏覽器上進行初步除錯成功之後再到手機上除錯執行。

這需要做一些相容處理

不同的入口

app.jsx 中使用 R.run() 作為應用的入口。現在考慮到需要做一些準備才能啟動路由,所以先把原來的立即執行的函式變成一個不立即執行的函式 startRouting(),再在 onDeviceReady 中呼叫。

onDeviceReady 也需要進行特殊處理,在 Corodva 中會通過 deviceready 事件觸發執行該函式,但是在瀏覽器中不會,所以需要進行一個簡單的判斷

function onDeviceReady() {
startRouting();
}
if (isCordova()) {
document.addEventListener("deviceready", onDeviceReady, false);
} else {
onDeviceReady();
}

關於 isCordova() 的實現,參考 這篇文章(英文)

資料服務相容

原來的資料是通過 AJAX 獲取的。而現在,需要考慮兩種情況,在瀏覽器用 JSON 資料(Web Database 操作起來有點複雜,反正都是為了除錯,所以直接用 JSON 資料了),在手機中用 SQLite。

首先需要設計一個介面,描述如下(非 JavaScript 語法)

interface IDataService {
load(); // 初始載入,比如瀏覽器中載入 JSON,手機上開啟資料庫等
all();  // 返回所有資料
get(id: string);  // 返回指定ID的資料
}

考慮到資料庫存取有可能是非同步處理,所以所有介面方法都應該按照非同步處理的方式,返回一個 Promise 物件,用 jQuery 的 $.when()$.Deferred().promise() 很容易產生 Promise 物件。

非強型別的 JavaScript 不需要定義介面,但是針對瀏覽器和手機兩種情況,需要提供兩個資料服務物件,參照上面的介面描述實現。假設這兩個服務物件分別叫 jsonData 和 sqliteData,那麼會有一個直接的服務物件 dataService,通過橋接模式使用 jsonData 或 sqliteData 中的一個來實際完成資料服務。

可以邀請 @癲笑哭走 寫一下橋接模式

// 這裡用 ES2015 語法描述,但在編碼時應該用 ES5 語法,否則在手機上可能不能執行
dataService = {
setup(Service) {
this.service = new Service();
},
load() {
return this.service.load();
},
all() {
return this.service.all();
},
get(id) {
return this.service.get(id);
}
};

其中 dataDevice.setup() 需要在 app.jsx 中根據 isCordova() 的結果進行呼叫。

if (isCordova()) {
dataService.setup(SqliteData);
document.addEventListener("deviceready", onDeviceReady, false);
} else {
dataService.setup(JsonData);
onDeviceReady();
}

注意 dataDevice.setup() 的實現中使用了 new,所以引數應該傳入一個類(構建函式)而非物件。

實現 JsonData

實現 JsonData 之後就可以用瀏覽器測試了,所以先實現 JsonData。

下面是我習慣的一個在 JavaScript 定義類的模板(和 TypeScript 編譯出來的很像,但不同)。

var JsonData = (function() {
function JsonData() {        
}
(function(fn) {
fn.load = function() { ... };
fn.all = function() { ... };
fn.get = function(id) { ... };
})(JsonData.prototype);
return JsonData;
})();

load()$.getJSON() 實現,本來可以直接返回 $.getJSON() 的結果,但是為了避免錯誤(fail)處理,重新封裝了 Promise。

fn.load = function() {
var deferred = $.Deferred();
function done(data) {
this.data = data || [];
deferred.resolve();
}.bind(this);
$.getJSON("js/data.json").then(done, function() {
done();
});
return deferred.promise();
};

從 load 載入了資料之後,all 和 get 的實現就簡單了

fn.all = function() {
return $.when(this.data);
};
fn.get = function(id) {
var person = this.data.filter(function(p) {
return p.id === id;
})[0];
return $.when(person);
};

改造 onDeviceReady

由於需要在 load 完成之後(即資料服務準備好之後)才啟動應用,所以需要改造一下 onDeviceReady

function onDeviceReady() {
dataService.load().then(function() {
startRouting();
});
}

實現 SqliteData

cordova-sqlite-storage

cordova-sqlite-storage 的文件,安裝之後,可以使用 window.sqliteDatabase 來進行資料庫的相關操作。

  • var db = sqliteDatabase.openDatabase({ name: "database_file" }) 開啟資料庫
  • sqliteDatabase.deleteDatabase({ name: "database_file" }) 刪除資料庫
  • db.transaction(function(tx) {...}) 開始一個事務
  • tx.executeSql(sql, [], callback) 執行 SQL 語句

實現 load()

實現 load 主要有如下幾個步驟

  1. 刪除資料庫
    因為沒 ROOT 的手機不能訪問 /data/data 目錄,所以不能手工刪除資料庫,考慮到目前資料都是預先加入的,所以先刪除資料庫保證資料庫在除錯修改的過程中一直保持最新。
  2. 開啟(建立)資料庫
  3. 建立表
    如果不考慮刪除資料庫,則需要在表不存在的時候建立
  4. 插入演示資料
    如果不考慮刪除資料庫,則需要檢查是空表的時候插入資料

按這個步驟,實現 load

fn.load = function() {
sqlitePlugin.deleteDatabase({ name: "contacts.sqlite" });
var db = sqlitePlugin.openDatabase({ name: "contacts.sqlite" });
var deferred = $.Deferred();
db.transaction(function(tx) {
tx.executeSql(SQL_CREATE);
tx.executeSql("select id from persons limit 1", [], function(tx, r) {
// 如果沒有資料,則執行插入語句
if (r.rows.length === 0) {
tx.executeSql(SQL_INSERT);
}
});
deferred.resolve();
}, function(e) {
console.log("ERROR: "   e.message);
deferred.resolve();
});
this.db = db;
return deferred.promise();
};

原始碼中 SQL_CREATE 通過 if not exists 判斷在表不存在時建立表。SQL_INSERT 則是批量插入 3 條演示資料的 SQL 語句。

如果沒有引數,需要給 []。有引數的情況在實現 get 時演示。

如果需要從 select 語句取得返回的資料,則需要定義回撥函式。回撥函式第 1 個引數是 tx,第 2 個引數才是結果集。通過結果集的 rows.length 可以判斷是否有資料行。關於資料行的獲取,在實現 all 時演示。

小技巧:ES2015 之前的多行字串

ES2015 之前,在 JavaScritp 中寫 SQL 最難受的問題就是沒有多行字串。一般情況下是使用 連線,但是非常阻礙閱讀。既然目前考慮相容性問題不能使用 ES2015 的語法,那麼就別想辦法解決這個問題——function 註釋大法

function f() {/*
line 1
line 2
line 3
*/}

上面這絕對是一段合法的 JavaScript 程式碼,定義了一個空函式,只包含註釋。用 f.toString() 可以得到這個函式的原始碼。這時候再用正規表示式去掉註釋符號和註釋符號前後的內容,就是我們需要的多行字串了。為此專門定義一個 getString(),很容易就能得到我們想要的內容

function getString(s) {
return s.toString().replace(/^\s*function.*?\/\*|\*\/\s*\}\s*$/g, "");
}
var text = getString(function f() {/*
line 1
line 2
line 3
*/}).trim();

唯一的問題是:釋出前壓縮指令碼的時候千萬要小心,因為註釋可能會被壓縮工具刪除掉

SQL_CREATE 和 SQL_INSERT

var SQL_CREATE = getString(function() {/*
CREATE TABLE IF NOT EXISTS [persons] (
[id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 
[name] CHAR(20) NOT NULL, 
[tel] CHAR(20), 
[is_man] INTEGER NOT NULL DEFAULT 0,
[city] CHAR(50)
)*/}).trim();
var SQL_INSERT = getString(function() {/*
insert into persons
(name, tel, is_man, city)
values
('張三', '13812345678', 1, '四川省綿陽市'),
('李四', '18087654321', 0, '廣東省深圳市'),
('王麻子', '15234567890', 0, '北京市')*/}).trim();

實現 all()

這次資料沒有快取在記憶體中,需要資料都必須從資料庫讀取。這不是問題,問題在於取得的結果的 rows 屬性不是一個陣列,連偽陣列都不是。它通過 length 獲取資料行數,但取每行資料得用 rows.item(i)——注意這裡是圓括號不是方括號,item() 是一個方法。

之所以通過 item(i) 來獲取資料,可能和 Java(Android) 或 C (IOS) 獲取資料的方式有關,一般來說,Java 返回的資料集是通過遊標逐行獲取資料的。

因為我們需要的是一個陣列,所以需要定義一個 toModels() 來轉換。另外,注意到陣列庫欄位 is_man,是按某資料庫字元命名規範命名的,而需要的資料模型屬性叫 isMan,所以還需要定義一個 toModel 來處理屬性名稱

function toModel(item) {
var model = {};
Object.keys(item).forEach(function(key) {
// 將下劃線名稱替換為 camel 命名法名稱
var k = /_/.test(key) ? key.replace(/_(.)/g, function(m) {
return m[1].toUpperCase();
}) : key;
model[k] = item[key];
});
return model;
};
functin toModels(rows) {
var models = [];
for (var i = 0; i < rows.length; i  ) {
models.push(toModel(rows.item(i)));
}
return models;
};

現在可以定義 all() 了

fn.all = function() {
var deferred = $.Deferred();
var _this = this;
this.db.transaction(function(tx) {
tx.executeSql("select * from persons", [], function(tx, r) {
var rows = toModels(r.rows);
deferred.resolve(rows);
});
});
return deferred.promise();
};

定義 get(id)

cordova-sqlite-storage 支援在 SQL 中通過 ? 佔位,然後依次在引數列表(executeSql 的第 2 個引數,是個陣列)中把引數值給出來,所以 get(id) 的實現如下

fn.get = function(id) {
var deferred = $.Deferred();
var _this = this;
this.db.transaction(function(tx) {
tx.executeSql("select * from persons where id = ?", [~~id], function(tx, r) {
var m = r.rows.length == 0 ? null : _this.toModel(r.rows.item(0));
deferred.resolve(m);
});
});
return deferred.promise();
};

不要在意 ~~id 這個小細節,它乾的事情和 parseInt(id) 一樣,這和 !! 把一個值變成布林值是一樣的道理。

在手機上測試

關鍵的內容都說完了,程式碼完成之後先用 jshint 檢查一下,然後再用瀏覽器除錯一下。沒問題了就直接上手機——接上手機,開啟 mLogcat,執行

cordova run android

相關文章

IOS開發 最新文章