[学习笔记] 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
相关文章
相关标签/搜索