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