使用SQLite

2018-05-25 23:27 更新

通訊錄做到這個(gè)程度,應(yīng)該考慮增刪改功能了。但是,增刪改功能的前提是能進(jìn)行相應(yīng)的數(shù)據(jù)持久化操作。因?yàn)樾枰妊芯吭?Cordova 中使用 SQLite。

為 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 

準(zhǔn)備試運(yùn)行和調(diào)試

deviceready

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

由于之前把引入 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>typecharset 部分都可以省略掉,不過(guò)最好在 <head> 的最前面加上

<meta charset="utf-8" />

之前雖然忘了加,但也運(yùn)行得好好的,不過(guò)加上總不是壞事,畢竟我們所有源文件都是 utf8 編碼的。

Logcat 和 mLogcat

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ì)變)。

clipboard.png

通過(guò) console.log() 輸出的日志在 mLogcat 中很容易看到,它會(huì)有一個(gè)前綴 [INFO:CONSOLE(#)],其中 # 表示數(shù)。

如果大家發(fā)現(xiàn)有其它好用的輕量 Logcat 查看工具,請(qǐng)介紹給我哦

兼容瀏覽器和 Android

即使有了日志式的調(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),參考 這篇文章(英文)

數(shù)據(jù)服務(wù)兼容

原來(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

實(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);
};

改造 onDeviceReady

由于需要在 load 完成之后(即數(shù)據(jù)服務(wù)準(zhǔn)備好之后)才啟動(dòng)應(yīng)用,所以需要改造一下 onDeviceReady

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

實(shí)現(xiàn) SqliteData

cordova-sqlite-storage

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()

實(shí)現(xiàn) load 主要有如下幾個(gè)步驟

  1. 刪除數(shù)據(jù)庫(kù) 因?yàn)闆](méi) ROOT 的手機(jī)不能訪問(wèn) /data/data 目錄,所以不能手工刪除數(shù)據(jù)庫(kù),考慮到目前數(shù)據(jù)都是預(yù)先加入的,所以先刪除數(shù)據(jù)庫(kù)保證數(shù)據(jù)庫(kù)在調(diào)試修改的過(guò)程中一直保持最新。

  1. 打開(kāi)(創(chuàng)建)數(shù)據(jù)庫(kù)

  1. 創(chuàng)建表 如果不考慮刪除數(shù)據(jù)庫(kù),則需要在表不存在的時(shí)候創(chuàng)建

  1. 插入演示數(shù)據(jù) 如果不考慮刪除數(shù)據(jù)庫(kù),則需要檢查是空表的時(shí)候插入數(shù)據(jù)

按這個(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 之前的多行字符串

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ì)被壓縮工具刪除掉。

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, '四川省綿陽(yáng)市'),
('李四', '18087654321', 0, '廣東省深圳市'),
('王麻子', '15234567890', 0, '北京市')*/}).trim();

實(shí)現(xiàn) all()

這次數(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();
};

定義 get(id)

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è)值變成布爾值是一樣的道理。

在手機(jī)上測(cè)試

關(guān)鍵的內(nèi)容都說(shuō)完了,代碼完成之后先用 jshint 檢查一下,然后再用瀏覽器調(diào)試一下。沒(méi)問(wèn)題了就直接上手機(jī)——接上手機(jī),打開(kāi) mLogcat,運(yùn)行

cordova run android

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)