[學習筆記] Cordova+AmazeUI+React 作個通信錄 - 使用 SQLite

[學習筆記] Cordova+AmazeUI+React 作個通信錄 系列文章javascript

目錄

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

傳送門:所有章節 示例代碼html


通信錄作到這個程度,應該考慮增刪改功能了。可是,增刪改功能的前提是能進行相應的數據持久化操做。由於須要先研究在 Cordova 中使用 SQLite。前端

爲 Cordova 添加 SQLite 插件

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

Apache Cordova Plugin Search 打開以後會須要一些時間來加載數據,因此得等一等纔會出現搜索框。android

雖然搜索是在這裏搜,可是安裝是在控制檯下。進入 contacts 目錄(也就是 www 的上級目錄),而後運行這個命令git

cordova plugin add cordova-sqlite-storage

準備試運行和調試

deviceready

cordova-sqlite-storage 插件會爲 window 添加 sqliteDatebase 屬性,但必須在設備準備好以後才能使用,因此須要等等觸發 Cordova 的 deviceready 事件。以前生成的 index.js 尚未刪除掉,因此能夠看到註冊和響應 deviceready 事件的代碼。github

示例代碼中定義了 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> 的最前面就好sql

<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
相關文章
相關標籤/搜索