JsStore是IndexedDB的包装器。它提供了简单的SQL像api,这是容易学习和使用。 IndexedDb查询可以在web worker内部执行,JsStore通过提供一个单独的worker文件来保持这种功能。
最近有位叫Pioneer网友一直在问我关于事务的实现方式,关于这点的确在看JsStore时,并没太重视。由于近期疫情放开新冠病毒迅速传播,我也不例外18日中招了。近日身体已基本恢复,利用在家休息期间,了解了JsStore中的transaction事务。对于事务,简单的理解就是,一个事务里的操作,要么全部执行成功,要么全部执行失败。 这里将修改之前IndexedDB写的登录功能,来作为案例讲解,界面细节就不细讲了,有不清楚可以查看之前案例,地址:本地数据库IndexedDB - 学员管理系统之登录(一)_觉醒法师的博客-CSDN博客_indexdb实现管理系统
一、框架搭建
1.1 项目结构
由于该Demo主要是以之前 “本地数据库IndexedDB - 学员管理系统之登录(一)” 中代码实现,并且只讲登录功能,以及事务处理,所以有些地方不会做过多细讲,该篇重点会讲到api、db、store几处。
1.2 路由定义
路由这块并不复杂,只之前多于部分删除,保留现在Pages中Error、login、menge部分即可。
Error页面代码如下:
<template>
<div class="error-box">
<h3>页面出错了 404 ~</h3>
<p>当前页面不存在,<span class="blue" @click="backEvent">点击返回</span></p>
</div>
</template>
<script>
export default{
data(){
return {}
},
methods: {
backEvent(){
this.$router.go(-1);
}
}
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>
由于该篇不讲主页功能,这里只保留极少代码,以实现登录后跳转到首页即可,mange页面代码:
<template>
<div class="index-wrap">
Home
</div>
</template>
<script>
export default {
data () {
return {
}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
</style>
登录页面login样式代码如下:
.login-box{
width: 600px;
height: 390px;
padding: 50px 70px;
box-sizing: border-box;
box-shadow: 0 0 10px rgba(0, 0, 0, .1);
border-radius: 10px;
overflow: hidden;
position: absolute;
left: 50%;
top: 50%;
margin-left: -300px;
margin-top: -200px;
z-index: 10;
h3, h4{
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
text-align: center;
}
h3{
font-size: 26px;
color: #409eff;
}
h4{
font-size: 14px;
color: #999999;
font-weight: normal;
padding: 10px 0 40px;
span{
display: inline-block;
vertical-align: middle;
&.tit{
padding: 0 26px;
}
}
}
}
登录页面先实现校验等功能,具体实现待后期再详细讲解,login代码如下:
<template>
<div class="login-box">
<h3>学员管理系统</h3>
<h4><span>———</span> <span class="tit">安全登录</span> <span>———</span></h4>
<el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
<el-form-item label="用户名" prop="username">
<el-input type="text" v-model="ruleForm.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="disabledButton" @click="submitForm('ruleForm')">登录</el-button>
<el-button :disabled="disabledButton" @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data(){
var validateUsername = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入用户名'));
} else {
callback();
}
};
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
} else {
if (this.ruleForm.checkPass !== '') {
this.$refs.ruleForm.validateField('checkPass');
}
callback();
}
};
return {
disabledButton: false,
ruleForm: {
username: '',
password: '',
},
rules: {
username: [
{ validator: validateUsername, trigger: 'blur' }
],
password: [
{ validator: validatePass, trigger: 'blur' }
]
},
}
},
methods: {
/**
* 提交数据
* @param {Object} formName
*/
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
}
});
},
/**
* 重置表单
* @param {Object} formName
*/
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>
路由中配置代码如下:
import Vue from 'vue'
import Router from 'vue-router'
import { TOKEN } from '@/store/mutationsType'
import Layout from '@/components/Layout'
import Error404 from '@/pages/Error/err404'
import Mange from '@/pages/mange'
import Login from '@/pages/login'
import store from '@/store'
Vue.use(Router);
let _router = new Router({
routes: [
{
path: '/',
name: "Home",
component: Layout,
redirect: '/sys/index',
children: [
{
path: '/sys/index',
name: 'Index',
component: Mange,
}
]
},
{
path: '/login',
name: 'Login',
component: Login,
},
{
path: '*',
name: 'Error404',
component: Error404,
},
]
});
//存储push
let originPush=Router.prototype.push
let originReplace=Router.prototype.replace
//重写
Router.prototype.push=function(location,resole,reject){
if(resole&&reject){
originPush.call(this,location,resole,reject)
}else{
originPush.call(this,location,()=>{},()=>{})
}
}
Router.prototype.replace=function(location,resole,reject){
if(resole&&reject){
originReplace.call(this,location,resole,reject)
}else{
originReplace.call(this,location,()=>{},()=>{})
}
}
_router.beforeEach((toRoute, fromRoute, next) => {
store.dispatch('checkIsLogin').then(() => {
next();
}).catch(() => {
if(toRoute.path=='/login'){
next();
}else{
next('/login');
}
});
});
export default _router;
1.3 store状态管理器
state.js代码如下:
/**
* 状态,变量库
*/
const state = {
/**
* 访问令牌
*/
token: "",
/**
* 用户信息
*/
userInfo: null
}
export default state;
getters.js代码如下:
/**
* 计算器
*/
const getters = {
/**
* 用户信息
*/
userInfo(state){
return state.userInfo;
},
/**
* 访问令牌
*/
accessToken(state){
return state.token;
}
}
export default getters;
mutationsType.js代码如下:
/**
* 用户信息
*/
export const USERINFO = "USERINFO";
/**
* 访问令牌
*/
export const TOKEN = "TOKEN";
mutations.js代码如下:
import { USERINFO, TOKEN } from './mutationsType'
/**
* 裂变器
*/
const mutations = {
/**
* 修改访问令牌信息
*/
[TOKEN](state, param){
state.token = param;
},
/**
* 修改用户信息
*/
[USERINFO](state, param){
state.userInfo = param;
}
}
export default mutations;
在这里tokenIsFailure函数还未实现,所以代码中暂不体现,actions.js代码如下:
import Vue from 'vue'
import { USERINFO, TOKEN } from './mutationsType'
import { Loading } from 'element-ui'
import { tokenIsFailure } from '@/api'
/**
* 业务层
*/
const actions = {
/**
* 重新加载缓存中用户信息
*/
reloadUserInfo({commit}){
let token = Vue.ls.get(TOKEN),
userInfo = Vue.ls.get(USERINFO);
if(token) {
commit(TOKEN, token);
}
if(userInfo) {
commit(USERINFO, userInfo);
}
},
/**
* 检查是否登录
*/
checkIsLogin({commit}){
let token = Vue.ls.get(TOKEN);
return new Promise((resolve, reject) => {
if(token){
Vue.ls.set(TOKEN, token, 24 * 60 * 60 * 1000); //重新计时,缓存1天
resolve();
}else{
reject();
}
});
},
/**
* 保存登录信息
*/
saveLoginInfo({commit}, param){
if(param['token']) {
commit(TOKEN, param.token);
Vue.ls.set(TOKEN, param.token, 24 * 60 * 60 * 1000);
}
if(param['userinfo']) {
commit(USERINFO, param.userinfo);
Vue.ls.set(USERINFO, param.userinfo);
}
},
/**
* 退出登录
*/
exitLogin({commit}, param){
commit(TOKEN, '');
commit(USERINFO, '');
Vue.ls.remove(TOKEN);
Vue.ls.remove(USERINFO);
}
}
export default actions;
store/index.js代码如下:
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'
Vue.use(Vuex);
export default new Vuex.Store({
state,
getters,
actions,
mutations
})
在main.js中引入,代码如下:
import Vue from 'vue'
import App from './App'
import router from './router'
import elementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import 'element-ui/lib/theme-chalk/base.css'
import store from '@/store/index'
import Storage from 'vue-ls'
Vue.use(elementUI);
Vue.use(Storage, {
namespace: 'jsstoredemo_',
name: 'ls',
storeage: 'local'
});
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
以上操作完成,基本框架已经完成,剩下就是功能实现了。
二、数据库模型
2.1 创建数据库链接
首先在db目录下创建index.js文件,用来实现数据库连接和增删改查功能,代码如下:
import { Connection } from "jsstore";
import workerInjector from "jsstore/dist/worker_injector";
let connection = new Connection();
connection.addPlugin(workerInjector);
export default connection;
2.2 定义数据表
在db目录下创建service.js文件,在这里定义数据中使用到的数据表,数据库初始化函数,以及版本迭代等功能操作,代码如下:
import connection from './index.js';
import { DATA_TYPE } from "jsstore";
const getDatabase = () => {
//用户表
const UserTable = {
name: "Users",
columns: {
id: { primaryKey: true, autoIncrement: true },
username: { notNull: true, dataType: DATA_TYPE.String },
password: { notNull: true, dataType: DATA_TYPE.String },
role: { notNull: true, dataType: DATA_TYPE.Number },
token: { notNull: false, dataType: DATA_TYPE.String },
createtime: { notNull: true, dataType: DATA_TYPE.DateTime },
updatetime: { notNull: true, dataType: DATA_TYPE.DateTime },
}
}
const dataBase = {
name: "demo_manage",
tables: [UserTable],
version: 1
};
return dataBase;
}
export const initJsStore = async () => {
const dataBase = await getDatabase();
return await connection.initDb(dataBase);
}
2.3 定义实例操作对象
在db/model目录下,创建user.js文件,用来定义用户数据表模型;由于登录功能需要使用事务来实现,这里暂不展示,待后续再讲解。代码如下:
import connection from "@/db/index.js";
import { hex_md5 } from '@/utils/md5'
export class UserService {
constructor(){
this.tableName = "Users";
}
/**
* 获取所有用户信息
* @param {Object} token
*/
getUsers(){
return connection.select({
from: this.tableName
});
}
/**
* 判断是否存在token
* @param {Object} token
*/
hasToken(token){
return connection.select({
from: this.tableName,
where: {
token
}
})
}
/**
* 退出
* @param {Object} token
*/
logout(token){
return connection.update({
in: this.tableName,
set: {
token: null
},
where: {
token
}
});
}
/**
* 登录
* @param {Object} params
*/
login(params){
}
/**
* 添加用户数据
* @param {Object} data
*/
insertUser(data){
//增加创建和更新时间
Object.assign(data, {
createtime: new Date(),
updatetime: new Date()
});
//加密密码
if('undefined'!==typeof data['password']){
data['password'] = hex_md5(data.password);
}
//if end
return connection.insert({
into: this.tableName,
values: [data]
});
}
}
后面登录功能实现,将全部通过user.js模型中函数实现。
2.4 api接口
这里虽然无后台服务接口调用,但为方便使用user实例,还是通过api方式实现功能调用。代码方式如下:
import { UserService } from '@/db/model/user'
// 实例Users对象
const Users = new UserService();
/**
* 登录
*/
export const loginInfo = (params) => {
return Users.login(params);
}
/**
* 退出
*/
export const logoutInfo = token => {
return Users.logout(token);
}
/**
* 判断数据库中token是否失效
*/
export const tokenIsFailure = token => {
return Users.hasToken(token);
}
/**
* 添加用户信息
*/
export const addUserInfo = param => {
return Users.insertUser(param);
}
/**
* 获取用户列表
*/
export const userAllList = keyword => {
return Users.getUsers(keyword);
}
2.5 连接数据库
在App.vue中,实现数据库的初始化,并且当数据库连接成功后,判断用户是否存在,不存在则创建一个默认用户,代码如下:
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { initJsStore } from '@/db/service.js'
import { addUserInfo, userAllList } from '@/api'
export default {
name: 'App',
data(){
return {}
},
created() {
this.$store.dispatch('reloadUserInfo');
//初始化数据库
initJsStore().then(() => {
//判断用户是否存在,不存在默认添加一个
userAllList().then(res => {
if(res.length==0){
//添加管理员
addUserInfo({
username: "admin",
password: "123456",
role: 1
});
}
})
});
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}
#app {
width: 100%;
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 12px;
color: #2c3e50;
}
</style>
三、功能实现
3.1 检测token是否存在
在2.3中的代码,已实现了hasToken函数,在2.4中的代码,api接口函数已定义了tokenIsFailure()函数,此时我们修改actions.js中的checkIsLogin()函数即可。代码如下:
checkIsLogin({commit}){
let token = Vue.ls.get(TOKEN);
return new Promise((resolve, reject) => {
if(token){
//判断数据库中token是否存在
tokenIsFailure(token).then(() => {
Vue.ls.set(TOKEN, token, 24 * 60 * 60 * 1000);
resolve();
}).catch(e => {
commit(TOKEN, '');
Vue.ls.remove(TOKEN);
reject();
});
}else{
reject();
}
});
}
这样一来,当数据库中的token因某些原因失效后,即可将localStorge中缓存及时清除,并在路由跳转时,及时跳转到登录页面。
3.2 登录功能 - 事务处理
用户的登录功能相对较为复杂,完成登录这一步,需要实现以下几个步骤:
- 查询用户 - 判断用户名是否存在,
- 判断输入的密码是否正确
- 生成token,并将token保存到相对用户信息中
- 返回登录成功的用户信息和token信息
此时,我们在db目录中创建transaction.js文件,代码发如下:
import { randomStrName } from '@/utils/utils'
const userTableName = "Users";
/**
* 登录并生成token信息
* @param {Object} ctx
*/
async function login2accesstoken(ctx) {
ctx.start(); // 开始事务
//1.获取用户信息
const userRes = await ctx.select({
from: userTableName,
where: {
username: ctx.data.username
}
});
let userinfo;
//用户存在
if(userRes.length==1){
userinfo = userRes[0];
ctx.setResult('userinfo', userinfo);
}
//用户不存在
else{
ctx.setResult('code', -1);
ctx.setResult('msg', "用户名不存在");
ctx.abort();
return;
}
//2.判断密码是否正确
if(userinfo.password!=ctx.data.password){
ctx.setResult('code', -1);
ctx.setResult('msg', "密码错误");
ctx.abort();
return;
}else{
//删除密码信息
delete userinfo.password;
}
//3.生成token
const accesstoken = randomStrName(60);
//4.存入token 信息
ctx.update({
in: userTableName,
set: {
token: accesstoken
},
where: {
id: userinfo.id
}
});
//5.返回结果信息
ctx.setResult('code', 0);
ctx.setResult('msg', "登录成功");
ctx.setResult('data', {
accesstoken,
userinfo
});
}
// 将函数 login2accesstoken 赋值到window对象上。
window.login2accesstoken = login2accesstoken;
如上代码,我们按照实现步骤,依次完成用户查询、账号和密码校验、token生成和保存,以及登录成功后数据返回一系列操作。
此时,我们可以完成user.js中的login()函数了,首先是将transaction.js文件引入,代码如下:
import connection from "@/db/index.js";
import { hex_md5 } from '@/utils/md5'
import '../transaction.js'
export class UserService {
constructor(){
this.tableName = "Users";
}
/**
* 获取所有用户信息
* @param {Object} token
*/
getUsers(){
return connection.select({
from: this.tableName
});
}
/**
* 判断是否存在token
* @param {Object} token
*/
hasToken(token){
return connection.select({
from: this.tableName,
where: {
token
}
})
}
/**
* 退出
* @param {Object} token
*/
logout(token){
return connection.update({
in: this.tableName,
set: {
token: null
},
where: {
token
}
});
}
/**
* 登录
* @param {Object} params
*/
login(params){
return connection.transaction({
tables: [this.tableName],
method: "login2accesstoken",
data: {
username: params.username,
password: hex_md5(params.password)
}
});
}
/**
* 添加用户数据
* @param {Object} data
*/
insertUser(data){
//增加创建和更新时间
Object.assign(data, {
createtime: new Date(),
updatetime: new Date()
});
//加密密码
if('undefined'!==typeof data['password']){
data['password'] = hex_md5(data.password);
}
//if end
return connection.insert({
into: this.tableName,
values: [data]
});
}
}
将transaction.js中定义的login2accesstoken()函数,赋值到transaction的method参数上即可。
3.3 实现登录功能
通过以上功能实现,用户模型中已完成登录接口功能,在2.4的代码中,我们也实现登录接口函数定义,这里我们直接使用loginInfo()函数即可。我们找到登录页并打开,代码如下:
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
//调用登录函数,实现用户登录
loginInfo(this.ruleForm).then(res => {
if(res.code==0){
//缓存用户信息
this.$store.dispatch('saveLoginInfo', {
token: res.data.accesstoken,
userinfo: res.data.userinfo
})
this.$message.success(res.msg);
//跳转到首页
setTimeout(() => {
this.$router.push('/');
}, 200);
}else{
this.$message.error(res.msg);
this.resetForm('ruleForm');
}
}).catch(e => {
this.$message.error(e.message);
this.resetForm('ruleForm');
});
} else {
console.log('error submit!!');
return false;
}
});
}
四、属性
JsStore在事务api中接受一个方法名,该方法名通过上下文调用。
这个上下文所包含的属性如下:
start | 启动事务 |
select | 查询数据表 |
count | 统计查询数据总条数 |
update | 更新数据表信息 |
remove | 移除 |
insert | 插入 |
setResult | setResult接受键和值。setResult用于保存事务完成时返回的值。事务返回一个对象,该对象是键和值的形式,使用setResult设置。 |
abort | Abort用于中止事务。 |
getResult | getResult用于获取setResult设置的值。 |
data | 在事务API中作为数据传递的值。 |
这里通过事务实现的功能相对较为简单,大家可以了解下JsStore官网提供的案例。希望此篇对大家有所帮助。