使用SQLite

2018-05-25 23:27 更新

通訊錄做到這個程度,應(yīng)該考慮增刪改功能了。但是,增刪改功能的前提是能進(jìn)行相應(yīng)的數(shù)據(jù)持久化操作。因為需要先研究在 Cordova 中使用 SQLite。

為 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 

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

deviceready

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

由于之前把引入 cordova.js 的 <script> 標(biāo)簽從 index.html 中刪掉了,所以現(xiàn)在得加回來。直接加在所有 <script> 的最前面就好

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

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

<meta charset="utf-8" />

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

Logcat 和 mLogcat

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 都會變)。

clipboard.png

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

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

兼容瀏覽器和 Android

即使有了日志式的調(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ù)服務(wù)兼容

原來的數(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 之后就可以用瀏覽器測試了,所以先實現(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);
};

改造 onDeviceReady

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

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

實現(xiàn) SqliteData

cordova-sqlite-storage

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 主要有如下幾個步驟

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

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

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

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

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

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ā)布前壓縮腳本的時候千萬要小心,因為注釋可能會被壓縮工具刪除掉

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

實現(xiàn) all()

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

定義 get(id)

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) 一樣,這和 !! 把一個值變成布爾值是一樣的道理。

在手機(jī)上測試

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

cordova run android

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號