2

前言

本来这个系列应该不会这么快更新,然而昨晚写完前一篇后才发现我的思路中有一个巨大的漏洞。导致我在前一篇中花费大量时间实现了一个复杂的Transaction类——其实完全有更简单的方式来完成这一切。
前篇:http://segmentfault.com/a/1190000003982058
项目地址:https://github.com/woodensail/indexedDB

昨天的疏忽

昨天在设计封装库时,一开始是打算利用《co模块的前端实现》中实现的async库来完成数据库访问的同步化。然而尝试之后就发现并不可行,问题在前篇中提到过,出在Promise的机制上。于是得出结论:indexedDB不可以用原生Promise进行封装。
在不可以用Promise的前提下,无论是同步化或者链式操作都用不了。于是昨天我自己实现了一个简单的同步执行的Promise,并以此为基础实现了链式操作。
我在昨天的文章写完之后才突然想到,既然没有Promise就没法实现同步化链式操作,那么我自己实现完Promise后完全可以不用使用链式操作,直接一步到位实现同步化。所以这篇文章就是关于如何完成indexedDB同步化封装。

使用样例

这是一段最基础的用法,依然和昨天一样实现了get,put,getKV,putKV,四个函数。
这种写法下每个操作是单独事务的,无法保证原子性。

async(function* () {
    var db = yield new DB('test');
    yield db.put('info', {k: 'async', v: 1});
    var result1 = yield db.get('info', 'async');
    console.log(result1.v);
    
    yield db.putKv('info', 'async', '1');
    var result2 = yield db.getKv('info', 'async');
    console.log(result2);
}).catch(function (e) {
    console.log(e);
});

首先打开一个事务然后作为参数传递给数据库访问函数即可启用事务支持。
下面这两个操作是在同一个事务中的。

async(function* () {
    var db = yield new DB('test');
    var tx = db.transaction('info', 'readwrite');
    yield db.put('info', {k: 'async', v: 1}, tx);
    var result1 = yield db.get('info', 'async', tx);
    console.log(result1.v);
}).catch(function (e) {
    console.log(e);
});

在开启事务时,第三个参数填true,可以将当前事务作为默认事务。后面的操作将自动使用该事务。需要在使用完毕后调用transactionEnd手动清除默认事务。

async(function* () {
    var db = yield new DB('test');
    db.transaction('info', 'readwrite', true);
    yield db.put('info', {k: 'async', v: 1});
    var result1 = yield db.get('info', 'async');
    console.log(result1.v);
    db.transactionEnd();
}).catch(function (e) {
    console.log(e);
});

实现Promise

这是一个不完全版的Promise实现,只提供了最基本的then和catch以及他们的链式调用。反正也够async用了。
暂时没有提供对Promise.all的支持,我估计要支持得修改async库才行,所以就以后再说吧。

var DbPromise = function (fun) {
    this.state = 'pending';
    this.resolveList = [];
    this.rejectList = [];
    var _this = this;
    fun(function () {
        _this.resolve.apply(_this, arguments)
    }, function () {
        _this.reject.apply(_this, arguments)
    });
};
DbPromise.prototype = {};
DbPromise.prototype.resolve = function (data) {
    this.state = 'resolved';
    this.data = data;
    var _this = this;
    this.resolveList.forEach(function (fun) {
        _this.data = fun(_this.data)
    });
};
DbPromise.prototype.reject = function (data) {
    this.state = 'rejected';
    this.error = data;
    this.rejectList.forEach(function (fun) {
        fun(data);
    });
};
DbPromise.prototype.then = function (fun) {
    if (this.state === 'pending') {
        this.resolveList.push(fun);
    } else {
        this.data = fun(this.data);
    }
    return this;
};
DbPromise.prototype.catch = function (fun) {
    if (this.state === 'pending') {
        this.rejectList.push(fun);
    } else {
        fun(this.error);
    }
    return this;
};

实现数据库封装类

定义class DB,打开数据库的操作定义在_open中,会在后面介绍

var DB = function (name, upgrade, version) {
    var _this = this;
    // 可以在其他js文件中通过向DB.dbMap添加成员的方式来预定义数据库
    // 如果是已经预定义过的数据库就可以调用new DB(name)来打开
    // 否则需要调用new DB(name, upgrade, version),填写upgrade响应函数和版本号
    if (DB.dbMap[name]) {
        var map = DB.dbMap[name];
        return _open(name, map.upgrade, map.version).then(function (db) {
            _this.db = db;
            return _this;
        }).then(map.nextStep);
    } else {
        return _open(name, upgrade, version).then(function (db) {
            _this.db = db;
            return _this;
        });
    }
};
DB.prototype = {};

打开事务和取消关闭事务的方法。

DB.prototype.transaction = function (table, type, asDefault) {
    // 根据给定的目标和类型打开当前数据库的事务
    var tx = _transaction(this.db, table, type);
    // 如果asDefault为真,则缓存该事务将其作为默认事务。
    // 注意目前同时设置多个默认事务是会冲突的,
    // 这种情况理论上有极小的可能性在异步操作中出现,我会在接下来的版本中改正这一点。
    if (asDefault) {
        this.tx = tx;
    }
    return tx;
};
//取消默认事务
DB.prototype.transactionEnd = function () {
    this.tx = void 0;
};
function _transaction(db, table, type) {
    return db.transaction(table, type);
}

具体的数据库操作函数。其实是对_put等函数的封装。

// tx || this.tx 指的是优先使用参数指定的事务,其次使用默认事务
DB.prototype.put = function (table, data, tx) {
    return _put(this.db, table, data, tx || this.tx);
};
DB.prototype.get = function (table, name, tx) {
    return _get(this.db, table, name, tx || this.tx);
};
DB.prototype.clear = function (table, tx) {
    return _clear(this.db, table, tx || this.tx);
};

//这两个是对get和put的特殊封装,多了参数和结果的预处理,简化了参数和返回值的格式
DB.prototype.getKv = function (table, k, tx) {
    return _get(this.db, table, k, tx).then(o=>(o || {}).v);
};
DB.prototype.putKv = function (table, k, v, tx) {
    return _put(this.db, table, {k, v}, tx);
};

_open,_put,_get,_clear的实现由于后三者类似,所以只贴了_put。需要后两点代码请查看github。

function _open(name, upgrade, version) {
    // 返回自定义Promise供async库调用
    return new DbPromise(function (resolve, reject) {
        // 打开指定数据库的指定版本
        var request = indexedDB.open(name, version);
        // 设置升级操作
        request.onupgradeneeded = upgrade;
        // 绑定success和error,其中成功后会返回打开的数据库对象
        request.onsuccess = function (e) {
            resolve(request.result);
        };
        request.onerror = reject;
    });
}
function _put(db, table, data, tx) {
    // 返回自定义Promise供async库调用
    return new DbPromise(function (resolve, reject) {
        // 如果没有提供事务则创建新事务
        tx = tx || db.transaction(table, 'readwrite');
        // 打开store并进行操作
        var store = tx.objectStore(table);
        var request = store.put(data);
        // 设置回调
        request.onsuccess = function () {
            resolve(request.result);
        };
        request.onerror = reject;

    });
}

总结

基本上,在实现了DbPromise之后其他部分的实现方式就是按老一套来就行了,非常的简单。我昨天只是光棍节脑袋抽筋没反应过来而已。
现在的库已经可以当做基本的Key-Value数据库来用了,以后我会进一步添加更多的方法。各位亲们,推荐或者收藏一下呗


WoodenSail
1.3k 声望18 粉丝