通訊錄做到這個程度,應(yīng)該考慮增刪改功能了。但是,增刪改功能的前提是能進(jìn)行相應(yīng)的數(shù)據(jù)持久化操作。因為需要先研究在 Cordova 中使用 SQLite。
在 Apache Cordova Plugin Search 頁面搜索 sqlite
。排名靠前的有 cordova-sqlite-storage 和 cordova-plugin-sqlite 等,從下載量來看,我選擇了前者。
Apache Cordova Plugin Search 打開之后會需要一些時間來加載數(shù)據(jù),所以得等一等才會出現(xiàn)搜索框。
雖然搜索是在這里搜,但是安裝是在控制臺下。進(jìn)入 contacts 目錄(也就是 www 的上級目錄),然后運行這個命令
cordova plugin add cordova-sqlite-storage
cordova-sqlite-storage 插件會為 window 添加 sqliteDatebase
屬性,但必須在設(shè)備準(zhǔn)備好之后才能使用,所以需要等等觸發(fā) Cordova 的 deviceready 事件。之前生成的 index.js 還沒有刪除掉,所以可以看到注冊和響應(yīng) deviceready 事件的代碼。
示例代碼中定義了 app 對象,其 initialize 方法是入口,在最下面調(diào)用。而 initialize 只干了一件事就是 bindEvents,bindEvents 也只干了一件事就是將 deviceready 事件綁定到處理函數(shù) this.onDeviceReady
。這整個過程實在復(fù)雜,所以用立即執(zhí)行的函數(shù)簡化一下
(function() {
function onDeviceReady() {
console.log("device is ready");
}
document.addEventListener("deviceready", onDeviceReady, false);
})();
由于之前把引入 cordova.js 的 <script>
標(biāo)簽從 index.html 中刪掉了,所以現(xiàn)在得加回來。直接加在所有 <script>
的最前面就好
<script type="text/javascript" charset="utf-8" src="cordova.js"></script>
這個 <script>
的 type
和 charset
部分都可以省略掉,不過最好在 <head>
的最前面加上
<meta charset="utf-8" />
之前雖然忘了加,但也運行得好好的,不過加上總不是壞事,畢竟我們所有源文件都是 utf8 編碼的。
Cordova 的調(diào)試是件比較痛苦的事情,雖然也有專用的調(diào)試工具,但是好用的收費,不收費的難用。Eclipse 到是可以調(diào)試,就是太重量級了。幸好前端開發(fā)養(yǎng)成了使用 console.log()
的調(diào)試習(xí)慣。
console.log()
的輸出已經(jīng)由 Cordova 封裝成了 Android 上的 Logcat 輸出,只需要找一個 Logcat 的查看器就行。
Windows 下可以用 adb logcat | findstr
來過濾和查看需要的日志。grep
后面要跟需要過濾的字符串作為參數(shù),更詳情的用法可以運行運行命令 findstr /?
查看幫助信息。
- findstr 在 Win8 和 Win10 下可用,Win7 和更早的版本沒有嘗試過。
不過命令行查看輸出不是很方便。我找了很多 logcat 工具之后,決定使用 mLogcat。
先把手機(jī)連上電腦,然后打開 mLogcat,這時候默認(rèn)會顯示全部的日志,在消息窗口右鍵,菜單中選擇 “Find/Refilter Item [Ctrl+F]”,會打開一下過濾窗口,輸入要過濾(顯示出來)的內(nèi)容,比如 cn.jamesfancy.contacts
,就可以看到相關(guān)的日志了?!癛efilter Item [Alt+R]” 可能更詳細(xì)的設(shè)置過濾,但是沒有按“Process Name”過濾的選項。但是如果找到了應(yīng)用和 TID 或 PID,用這個過濾還是挺好的(注意,每次啟動 PID 和 TID 都會變)。
通過 console.log()
輸出的日志在 mLogcat 中很容易看到,它會有一個前綴 [INFO:CONSOLE(#)]
,其中 #
表示數(shù)。
如果大家發(fā)現(xiàn)有其它好用的輕量 Logcat 查看工具,請介紹給我哦
即使有了日志式的調(diào)試方法和 mLogcat,在手機(jī)或模擬器上調(diào)試應(yīng)用也是個復(fù)雜的過程,因為還需要編譯、安裝等步驟。cordova run android
可以一步完成,但是需要些時間。所以最好的辦法還是在瀏覽器上進(jìn)行初步調(diào)試成功之后再到手機(jī)上調(diào)試運行。
這需要做一些兼容處理
app.jsx 中使用 R.run()
作為應(yīng)用的入口?,F(xiàn)在考慮到需要做一些準(zhǔn)備才能啟動路由,所以先把原來的立即執(zhí)行的函數(shù)變成一個不立即執(zhí)行的函數(shù) startRouting()
,再在 onDeviceReady
中調(diào)用。
onDeviceReady 也需要進(jìn)行特殊處理,在 Corodva 中會通過 deviceready 事件觸發(fā)執(zhí)行該函數(shù),但是在瀏覽器中不會,所以需要進(jìn)行一個簡單的判斷
function onDeviceReady() {
startRouting();
}
if (isCordova()) {
document.addEventListener("deviceready", onDeviceReady, false);
} else {
onDeviceReady();
}
關(guān)于 isCordova()
的實現(xiàn),參考 這篇文章(英文)
原來的數(shù)據(jù)是通過 AJAX 獲取的。而現(xiàn)在,需要考慮兩種情況,在瀏覽器用 JSON 數(shù)據(jù)(Web Database 操作起來有點復(fù)雜,反正都是為了調(diào)試,所以直接用 JSON 數(shù)據(jù)了),在手機(jī)中用 SQLite。
首先需要設(shè)計一個接口,描述如下(非 JavaScript 語法)
interface IDataService {
load(); // 初始加載,比如瀏覽器中加載 JSON,手機(jī)上打開數(shù)據(jù)庫等
all(); // 返回所有數(shù)據(jù)
get(id: string); // 返回指定ID的數(shù)據(jù)
}
考慮到數(shù)據(jù)庫存取有可能是異步處理,所以所有接口方法都應(yīng)該按照異步處理的方式,返回一個 Promise 對象,用 jQuery 的 $.when()
或 $.Deferred().promise()
很容易產(chǎn)生 Promise 對象。
非強類型的 JavaScript 不需要定義接口,但是針對瀏覽器和手機(jī)兩種情況,需要提供兩個數(shù)據(jù)服務(wù)對象,參照上面的接口描述實現(xiàn)。假設(shè)這兩個服務(wù)對象分別叫 jsonData 和 sqliteData,那么會有一個直接的服務(wù)對象 dataService,通過橋接模式使用 jsonData 或 sqliteData 中的一個來實際完成數(shù)據(jù)服務(wù)。
可以邀請 @癲笑哭走 寫一下橋接模式
// 這里用 ES2015 語法描述,但在編碼時應(yīng)該用 ES5 語法,否則在手機(jī)上可能不能運行
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 中根據(jù) isCordova()
的結(jié)果進(jìn)行調(diào)用。
if (isCordova()) {
dataService.setup(SqliteData);
document.addEventListener("deviceready", onDeviceReady, false);
} else {
dataService.setup(JsonData);
onDeviceReady();
}
注意
dataDevice.setup()
的實現(xiàn)中使用了new
,所以參數(shù)應(yīng)該傳入一個類(構(gòu)建函數(shù))而非對象。
實現(xiàn) JsonData 之后就可以用瀏覽器測試了,所以先實現(xiàn) JsonData。
下面是我習(xí)慣的一個在 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()
實現(xiàn),本來可以直接返回 $.getJSON()
的結(jié)果,但是為了避免錯誤(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 加載了數(shù)據(jù)之后,all 和 get 的實現(xiàn)就簡單了
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);
};
由于需要在 load 完成之后(即數(shù)據(jù)服務(wù)準(zhǔn)備好之后)才啟動應(yīng)用,所以需要改造一下 onDeviceReady
function onDeviceReady() {
dataService.load().then(function() {
startRouting();
});
}
看 cordova-sqlite-storage 的文檔,安裝之后,可以使用 window.sqliteDatabase
來進(jìn)行數(shù)據(jù)庫的相關(guān)操作。
var db = sqliteDatabase.openDatabase({ name: "database_file" })
打開數(shù)據(jù)庫sqliteDatabase.deleteDatabase({ name: "database_file" })
刪除數(shù)據(jù)庫db.transaction(function(tx) {...})
開始一個事務(wù)tx.executeSql(sql, [], callback)
執(zhí)行 SQL 語句實現(xiàn) load 主要有如下幾個步驟
按這個步驟,實現(xiàn) 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) {
// 如果沒有數(shù)據(jù),則執(zhí)行插入語句
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
判斷在表不存在時創(chuàng)建表。SQL_INSERT 則是批量插入 3 條演示數(shù)據(jù)的 SQL 語句。
如果沒有參數(shù),需要給 []
。有參數(shù)的情況在實現(xiàn) get
時演示。
如果需要從 select
語句取得返回的數(shù)據(jù),則需要定義回調(diào)函數(shù)。回調(diào)函數(shù)第 1 個參數(shù)是 tx,第 2 個參數(shù)才是結(jié)果集。通過結(jié)果集的 rows.length
可以判斷是否有數(shù)據(jù)行。關(guān)于數(shù)據(jù)行的獲取,在實現(xiàn) all
時演示。
ES2015 之前,在 JavaScritp 中寫 SQL 最難受的問題就是沒有多行字符串。一般情況下是使用 +
連接,但是非常阻礙閱讀。既然目前考慮兼容性問題不能使用 ES2015 的語法,那么就別想辦法解決這個問題——function + 注釋大法
function f() {/*
line 1
line 2
line 3
*/}
上面這絕對是一段合法的 JavaScript 代碼,定義了一個空函數(shù),只包含注釋。用 f.toString()
可以得到這個函數(shù)的源碼。這時候再用正則表達(dá)式去掉注釋符號和注釋符號前后的內(nèi)容,就是我們需要的多行字符串了。為此專門定義一個 getString()
,很容易就能得到我們想要的內(nèi)容
function getString(s) {
return s.toString().replace(/^\s*function.*?\/\*|\*\/\s*\}\s*$/g, "");
}
var text = getString(function f() {/*
line 1
line 2
line 3
*/}).trim();
唯一的問題是:發(fā)布前壓縮腳本的時候千萬要小心,因為注釋可能會被壓縮工具刪除掉。
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();
這次數(shù)據(jù)沒有緩存在內(nèi)存中,需要數(shù)據(jù)都必須從數(shù)據(jù)庫讀取。這不是問題,問題在于取得的結(jié)果的 rows
屬性不是一個數(shù)組,連偽數(shù)組都不是。它通過 length
獲取數(shù)據(jù)行數(shù),但取每行數(shù)據(jù)得用 rows.item(i)
——注意這里是圓括號不是方括號,item()
是一個方法。
之所以通過 item(i) 來獲取數(shù)據(jù),可能和 Java(Android) 或 C++(IOS) 獲取數(shù)據(jù)的方式有關(guān),一般來說,Java 返回的數(shù)據(jù)集是通過游標(biāo)逐行獲取數(shù)據(jù)的。
因為我們需要的是一個數(shù)組,所以需要定義一個 toModels()
來轉(zhuǎn)換。另外,注意到數(shù)組庫字段 is_man
,是按某數(shù)據(jù)庫字符命名規(guī)范命名的,而需要的數(shù)據(jù)模型屬性叫 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;
};
現(xiàn)在可以定義 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();
};
cordova-sqlite-storage 支持在 SQL 中通過 ?
占位,然后依次在參數(shù)列表(executeSql 的第 2 個參數(shù),是個數(shù)組)中把參數(shù)值給出來,所以 get(id)
的實現(xiàn)如下
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
這個小細(xì)節(jié),它干的事情和 parseInt(id)
一樣,這和 !!
把一個值變成布爾值是一樣的道理。
關(guān)鍵的內(nèi)容都說完了,代碼完成之后先用 jshint 檢查一下,然后再用瀏覽器調(diào)試一下。沒問題了就直接上手機(jī)——接上手機(jī),打開 mLogcat,運行
cordova run android
更多建議: