使用SQLite
通讯录做到这个程度,应该考虑增删改功能了。但是,增删改功能的前提是能进行相应的数据持久化操作。因为需要先研究在 Cordova 中使用 SQLite。
为 Cordova 添加 SQLite 插件
在 Apache Cordova Plugin Search 页面搜索 sqlite
。排名靠前的有 cordova-sqlite-storage 和 cordova-plugin-sqlite 等,从下载量来看,我选择了前者。
Apache Cordova Plugin Search 打开之后会需要一些时间来加载数据,所以得等一等才会出现搜索框。
虽然搜索是在这里搜,但是安装是在控制台下。进入 contacts 目录(也就是 www 的上级目录),然后运行这个命令
cordova plugin add cordova-sqlite-storage
准备试运行和调试
deviceready
cordova-sqlite-storage 插件会为 window 添加 sqliteDatebase
属性,但必须在设备准备好之后才能使用,所以需要等等触发 Cordova 的 deviceready 事件。之前生成的 index.js 还没有删除掉,所以可以看到注册和响应 deviceready 事件的代码。
示例代码中定义了 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>
的最前面就好
<script type="text/javascript" charset="utf-8" src="cordova.js"></script>
这个 <script>
的 type
和 charset
部分都可以省略掉,不过最好在 <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 都会变)。
通过 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 主要有如下几个步骤
- 删除数据库 因为没 ROOT 的手机不能访问 /data/data 目录,所以不能手工删除数据库,考虑到目前数据都是预先加入的,所以先删除数据库保证数据库在调试修改的过程中一直保持最新。
- 打开(创建)数据库
- 创建表 如果不考虑删除数据库,则需要在表不存在的时候创建
- 插入演示数据 如果不考虑删除数据库,则需要检查是空表的时候插入数据
按这个步骤,实现 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
更多建议: