项目基本情况
使用uni-app开发,客户端本地数据存储选择sqllite
uni-app参考地址
sqllite参考地址
HTML5+ API Reference (html5plus.org)
解决异步的方法
异步执行的代码一般都有回调,在外层方法中创建自己的回调函数,通过回调实现业务操作,曲折实现“同步执行”的逻辑。
开发过程
由于业务需要,在app客户端要进行本地数据存储。本人常年开发java,面向对象理念深入人心,于是需要在合适的位置写一个dbUtils.js的工具类,提供数据库增删改查的方法,返回执行结果。但是在调试过程中发现sqllite操作数据库的方法是异步的,所以按照传统的返回值方式无法取得正确结果。
初始思路:先定义返回结果变量,操作完成后返回结果
// 此方法行不通!
export function executeSql(sql){
let result = false;
// 打开数据库
plus.sqlite.openDatabase({
// 数据库名称
name: 'xxxxx',
// 数据库文件路径
path: '_doc/xxxxx.db',
// 打开数据库成功
success: function(e){
result = true;
},
fail:function(e){
console.log(e);
}
});
if (result) {
// 执行sql
plus.sqlite.executeSql({
name:'xxxxx',
sql: sql,
success(){
result = true;
},
fail(e) {
console.log(e);
}
});
}
// 关闭数据库
plus.sqlite.closeDatabase({name: 'xxxxx'});
return result;
}
通过调试发现方法永远返回false,在执行plus.sqllite.xxx时,js继续执行了,没有等待,导致执行是数据库已经关闭,后来我把执行sql写在打开数据库的回调中,关闭数据库写在执行sql的回调中,但只能解决数据库操作部分的执行顺序正确,返回值仍然不对。
这时我想暴力解决这个异步问题,产生了一个天真大胆的想法,在异步执行时阻断程序执行,监听异步操作是否执行完成
let executeFinished = false;
......各种数据库操作,完成时赋值executeFinished = true;
while(true) {
if (executeFinished) {
break;
}
}
......后续操作
然后发现程序卡在while(true)那里了,永远进不去数据库操作回调方法,executeFinished自然永远不会变。
经过查阅资料,发现js是单线程的,那么while(true)一直循环程序就阻断了。js中的异步处理只是合适的时机去执行,底层并不是并行的。
最后,通过给公共方法传回调事件,曲折解决了sqllite的异步问题
解决sql注入问题
由于页面上允许输入任意字符串,存在sql注入问题。解决方法有两种,1是页面校验,不允许输入特殊字符;2是将sql中字符串参数进行转义。本次采用第2种,封装sql?参数,用户通过参数顺序逐步替换?内容。转义的部分只处理了单引号',后续逐步增加。
while (sql.indexOf('?') > - 1) {
// 取出sql参数
let sqlParam = sqlParams[sqlParamsIndex++];
let sqlParamStr = '';
if (sqlParam.constructor == String) {
// 将字符串中单引号'转义为反斜杠单引号\',防止注入
sqlParamStr = '\'' + sqlParam.replace(/\'/g, '\\\'').replace(/\'/g, '\\\'') + '\'';
} else if (sqlParam.constructor == Number) {
sqlParamStr = sqlParam.toString();
} else if (sqlParam.constructor == Date) {
sqlParamStr = 'datetime('+sqlParam.getTime()+', \'unixepoch\', \'localtime\')';
} else {
result.success = false;
result.data = '此sql参数类型暂不支持!';
callback(result);
return;
}
// 将?替换为真实sql内容
sql.replace('?', sqlParamStr);
}
最终代码
工具类:dbUtils.js
/**
* 增/删/改 sql执行
* @param {Array[String] | String} sql 可以为单条sql语句或多条sql语句数组
* @param {Array} sqlParams 数组,按照占位符顺序
* @param {Function} callback 回调函数
*/
export function executeSql(sql, sqlParams, callback){
// 处理sql参数
// 字符串和数组都转换成数组
let finalSql = [];
if (sql.constructor == String) {
finalSql.push(sql);
} else if (sql.constructor == Array) {
finalSql = sql;
}
// 参数存在时,按顺序替换sql中的占位符
if (sqlParams && sqlParams.length > 0) {
// sql参数序号
let sqlParamsIndex = 0;
for (let i = 0; i < finalSql.length; i++) {
while (finalSql[i].indexOf('?') > - 1) {
// 取出sql参数
let sqlParam = sqlParams[sqlParamsIndex++];
let sqlParamStr = '';
if (sqlParam.constructor == String) {
// 将字符串中单引号'转义为反斜杠单引号\',防止注入
sqlParamStr = '\'' + sqlParam.replace(/\'/g, '\\\'').replace(/\'/g, '\\\'') + '\'';
} else if (sqlParam.constructor == Number) {
sqlParamStr = sqlParam.toString();
} else if (sqlParam.constructor == Date) {
sqlParamStr = 'datetime('+sqlParam.getTime()+', \'unixepoch\', \'localtime\')';
} else {
result.success = false;
result.data = '此sql参数类型暂不支持!';
callback(result);
return;
}
// 将?替换为真实sql内容
finalSql[i].replace('?', sqlParamStr);
}
}
}
let result = {};
// 打开数据库
plus.sqlite.openDatabase({
// 数据库名称
name: 'xxxxx',
// 数据库文件路径
path: '_doc/xxxxx.db',
// 打开数据库成功
success: function(e){
plus.sqlite.executeSql({
name:'xxxxx',
sql: finalSql,
success(){
result.success = true;
closeDatabase();
callback(result);
},
fail(e) {
result.success = false;
result.data = e;
closeDatabase();
callback(result);
}
});
},
fail:function(e){
result.success = false;
result.data = e;
closeDatabase();
callback(result);
}
});
}
/**
* 数据库查询select
* @param {String} sql 可以为单条sql语句或多条sql语句数组
* @param {Array} sqlParams 数组,按照占位符顺序
* @param {Function} callback 回调函数
*/
export function selectSql(sql, sqlParams, callback){
// 处理sql参数
// 参数存在时,按顺序替换sql中的占位符
if (sqlParams && sqlParams.length > 0) {
// sql参数序号
let sqlParamsIndex = 0;
while (sql.indexOf('?') > - 1) {
// 取出sql参数
let sqlParam = sqlParams[sqlParamsIndex++];
let sqlParamStr = '';
if (sqlParam.constructor == String) {
// 将字符串中单引号'转义为反斜杠单引号\',防止注入
sqlParamStr = '\'' + sqlParam.replace(/\'/g, '\\\'').replace(/\'/g, '\\\'') + '\'';
} else if (sqlParam.constructor == Number) {
sqlParamStr = sqlParam.toString();
} else if (sqlParam.constructor == Date) {
sqlParamStr = 'datetime('+sqlParam.getTime()+', \'unixepoch\', \'localtime\')';
} else {
result.success = false;
result.data = '此sql参数类型暂不支持!';
callback(result);
return;
}
// 将?替换为真实sql内容
sql.replace('?', sqlParamStr);
}
}
let result = {};
// 打开数据库
plus.sqlite.openDatabase({
// 数据库名称
name: 'xxxxx',
// 数据库文件路径
path: '_doc/xxxxx.db',
// 打开数据库成功
success: function(e){
plus.sqlite.selectSql({
name:'xxxxx',
sql: sql,
success(data){
result.success = true;
result.data = data;
closeDatabase();
callback(result);
},
fail(e) {
result.success = false;
result.data = e;
closeDatabase();
callback(result);
}
});
},
fail:function(e){
result.success = false;
result.data = e;
closeDatabase();
callback(result);
}
});
}
/**
* 关闭数据库
*/
function closeDatabase() {
plus.sqlite.closeDatabase({name: 'xxxxx'});
}
引入:
import { executeSql } from '@/common/db/dbUtils.js';
调用:
let sql = "insert into xx values (?, ?, ?.........";
let sqlParams = ["string字符串", 1, new Date()];
executeSql(sql, sqlParams, function(result) {
if (result.success) {
// 业务操作
} else {
// 执行失败操作
}
});