IndexedDB是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB允许存储大量数据,提供查找接口,还能建立索引。这些都是LocalStorage或Cookie不具备的。就数据库类型而言,IndexedDB不属于关系型数据库(不支持SQL查询语句),更接近NoSQL数据库。
IndexedDB具有以下特点:
- 键值对存储:IndexedDB内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括JS对象。对象仓库中,数据以”键值对“的形式保存,每一个数据记录都对应的主键,主键是独一无二的,不能有重复,否则会抛出错误。
- 异步:IndexedDB操作时不会锁死浏览器,用户依然可以进行其他操作,这与LocalStorage形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
- 支持事务:IndexedDB支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 同源限制:IndexedDB受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
- 存储空间大:IndexedDB的存储空间比LocalStore大很多,一般来说不少于250MB,甚至没有上限。
- 支持二进制存储:IndexedDB不仅可以存储字符串,还可以存储二进制数据(ArrayBuffer对象和Blob对象)。
通过上篇讲解,大家已经熟悉了IndexedDB的一些基本用法。这里将使用IndexedDB写个简单的”学员管理系统(本地版)“,如对IndexedDB基础还不了解的,请翻看上一篇,地址如下:
本地数据库IndexedDB - 初学者(一)_觉醒法师的博客-CSDN博客
使用的技术框架是Vue.js + element-ui + vuex + axios + sass + indexedDB,系统栏目分类有:管理员列表、年级管理、班级管理、教师列表和学员列表。
如下图所示:
一、搭建项目
Vue项目的搭建这里也不再阐述,如有对vue全家桶不了解的同学,可以先阅读以下几篇文章:
Vue.js快速入门之一:安装和配置_觉醒法师的博客-CSDN博客_vue.js配置
Vue.js快速入门之二:使用状态管理工具Vuex_觉醒法师的博客-CSDN博客
Vue.js快速入门之三:Vue-Router路由_觉醒法师的博客-CSDN博客
Vue.js快速入门之四:axios安装和使用_觉醒法师的博客-CSDN博客_axios怎么安装
二、数据库创建
Vue项目搭建好后,需要在src目录下创建db目录,用来存储操作IndexedDB数据库文件。比如数据库操作文件、对应表封装类等。
2.1 定义表
虽然IndexedDB不属性于“关系性数据库”,这里还是按“关系性数据库”进行表的定义和关联。
系统管理员表:
字段名 | 类型 | 描述 |
id | int | ID |
name | varchar | 管理员账号 |
password | varchar | 密码 |
phone | varchar | 手机号 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
年级表:
字段名 | 类型 | 描述 |
id | int | ID |
name | varchar | 年级名称 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
班级表:
字段名 | 类型 | 描述 |
id | int | ID |
name | varchar | 班级名称 |
mid | varchar | 班主任 |
gid | int | 关联年级 |
ts | Array | 关联授权教师 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
教师表:
字段名 | 类型 | 描述 |
id | int | ID |
name | varchar | 教师姓名 |
registration | varchar | 户籍 |
address | varchar | 现居住地 |
phone | varchar | 手机号 |
cids | char | 关联班级ID,通过逗号分隔 |
birthday | datetime | 出生年月日 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
学员表:
字段名 | 类型 | 描述 |
id | int | ID |
name | varchar | 学员姓名 |
registration | varchar | 户籍 |
address | varchar | 现居住地 |
phone | varchar | 手机号 |
cid | int | 关联班级ID |
birthday | datetime | 出生年月日 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
2.2 MD5加密
由于IndexedDB是浏览器中数据库,当项目运行后可直接查看数据库中的数据,这时相关私密性的内容就需要通过加密进行处理了。所以在创建数据库表前,我们先在src目录下创建utils工具类目录,添加md5.js文件,用来给登录密码进行加密处理。
md5.js代码如下:
/*
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
/*
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }
/*
* Perform a simple self-test to see if the VM is working
*/
function md5_vm_test()
{
return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length
*/
function core_md5(x, len)
{
/* append padding */
x[len >> 5] |= 0x80 << ((len) % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
for(var i = 0; i < x.length; i += 16)
{
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return Array(a, b, c, d);
}
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5_cmn(q, a, b, x, s, t)
{
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}
/*
* Calculate the HMAC-MD5, of a key and some data
*/
function core_hmac_md5(key, data)
{
var bkey = str2binl(key);
if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);
var ipad = Array(16), opad = Array(16);
for(var i = 0; i < 16; i++)
{
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5C5C5C5C;
}
var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
return core_md5(opad.concat(hash), 512 + 128);
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y)
{
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function bit_rol(num, cnt)
{
return (num << cnt) | (num >>> (32 - cnt));
}
/*
* Convert a string to an array of little-endian words
* If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
*/
function str2binl(str)
{
var bin = Array();
var mask = (1 << chrsz) - 1;
for(var i = 0; i < str.length * chrsz; i += chrsz)
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
return bin;
}
/*
* Convert an array of little-endian words to a string
*/
function binl2str(bin)
{
var str = "";
var mask = (1 << chrsz) - 1;
for(var i = 0; i < bin.length * 32; i += chrsz)
str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
return str;
}
/*
* Convert an array of little-endian words to a hex string.
*/
function binl2hex(binarray)
{
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for(var i = 0; i < binarray.length * 4; i++)
{
str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
}
return str;
}
/*
* Convert an array of little-endian words to a base-64 string
*/
function binl2b64(binarray)
{
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var str = "";
for(var i = 0; i < binarray.length * 4; i += 3)
{
var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16)
| (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
| ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
for(var j = 0; j < 4; j++)
{
if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
}
}
return str;
}
export {
hex_md5,
b64_md5,
str_md5
}
这里除了直接引用MD5文件外,也可以通过npm或yarn等一些包管理工具,使用命令进行安装,这部分自行选择适合自己项目的方法即可。
2.2 打开数据库
以上将所有表结构定义好后,咱们可以封装操作数据库文件了。在src目录下创建db目录,然后新建index.js文件,用来定义数据库打开及表结构创建等等。
在index.js文件中,我们封装一个DBIsLoadSuccess函数,用来判断IndexedDB是否打开成功,因为open请求不会立即打开数据库或者开始一个事务,而是在onsuccess执行时;并且结合Promise,来实现此功能。具体代码如下:
let Database;
let Request = window.indexedDB.open("TestDatabase", 2);
let isError = false;
// 数据库打开失败
Request.onerror = function(event) {
isError = true;
throw "Why didn't you allow my web app to use IndexedDB?!";
};
// 数据库打开成功
Request.onsuccess = function(event) {
isError = false;
Database = event.target.result;
};
let handle = null;
/**
* 数据库增删改查 封装
*/
export const DBIsLoadSuccess = () => {
return new Promise((resolve, reject) => {
//如果数据库打开成功,直接回调resolve函数,并返回实例对象
if(Database){
resolve(Database);
}
//如果数据库还未打开,添加定时器 进行查询,打开成功后,
else{
handle = setInterval(() => {
if(Database){
clearInterval(handle);
resolve(Database);
}
//如果数据库打开失败,执行reject函数
else if(isError){
clearInterval(handle);
reject();
}
}, 200);
}
});
}
2.3 表结构创建
在onupgradeneeded事件中,允许你在处理函数中更新数据库模式;所以我们在onupgradeneeded执行时,来创建对应的表及表结构。
在管理员表创建时,需要默认添加一个管理员账号,并且通过hex_md5对密码进行处理后再存储到用户表中。创建管理员账号时,无须添加accesstoken键值对,因为此时用户还未登录,无token信息;如默认添加空值,则索引accesstoken中则会有相应关联值;这里accesstoken索引是用来判断用户是否登录的,是唯一的,只有在登录状态下才会赋值。
在index.js文件中增加以下代码:
import { hex_md5 } from '@/utils/md5'
//数据库表名
let usersName = "users"; //用户表
let gradeName = "grade"; //年级表
let classifyName = "classify"; //班级表
let studentName = "student"; //学生表
let teacherName = "teacher"; //老师表
let buildingName = "building"; //楼栋
// 执行success前执行函数
Request.onupgradeneeded = function(e){
let db = e.target.result;
//用户表
if(!db.objectStoreNames.contains(usersName)){
let store = db.createObjectStore(usersName, {keyPath: "id", autoIncrement: true});
//创建索引 - 用户名
store.createIndex('name', 'name', {unique: true});
//创建索引 - 登录token
store.createIndex('accesstoken', 'accesstoken', {unique: true});
//添加默认账号
store.add({
name: "admin",
password: hex_md5('123456'),
phone: "13233332222",
cratetime: new Date().getTime(),
updatetime: new Date().getTime()
});
}
//年级表
if(!db.objectStoreNames.contains(gradeName)){
let store = db.createObjectStore(gradeName, {keyPath: "id", autoIncrement: true});
//创建索引 - 年级名称(年级为唯一,故不能重复)
store.createIndex('name', 'name', {unique: true});
}
//班级表
if(!db.objectStoreNames.contains(classifyName)){
let store = db.createObjectStore(classifyName, {keyPath: "id", autoIncrement: true});
//创建索引 - 班级名称
store.createIndex('name', 'name', {unique: false});
//创建索引 - 年级名称
store.createIndex('grade', 'grade', {unique: true});
}
//学生表
if(!db.objectStoreNames.contains(studentName)){
let store = db.createObjectStore(studentName, {keyPath: "id", autoIncrement: true});
//创建索引 - 学生姓名
store.createIndex('name', 'name', {unique: false});
}
//老师表
if(!db.objectStoreNames.contains(teacherName)){
let store = db.createObjectStore(teacherName, {keyPath: "id", autoIncrement: true});
//创建索引 - 老师姓名
store.createIndex('name', 'name', {unique: false});
}
//楼栋表
if(!db.objectStoreNames.contains(buildingName)){
let store = db.createObjectStore(buildingName, {keyPath: "id", autoIncrement: true});
//创建索引 - 楼栋名称
store.createIndex('name', 'name', {unique: true, multiEntry: true});
}
//if end
}
注意:onupgradeneeded只会在第一次打开数据库时触发,后期想要触onupgradeneeded监听事件,则需要修改open中第二位参数的版本号。
我们在项目中引用index.js文件并执行,此时我们F12显示控制台,切换到“Application”,在IndexedDB中,则可以看到TestDatabase数据库了,以及创建相应的表及索引。
如下图:
三、项目结构
在项目中,如下图在src目录下,创建相应的项目文件。
在router以上部分,会在后面会详细讲解,这里我们先讲下如果定义路由和状态管理仓库。
3.1 vue-ls
Vue-ls用来控制数据存储在localStorage或者sessionStorage中,并且可以控制其存储时效性。
安装:
npm install vue-ls --save
main.js中引入:
import Storage from 'vue-ls'
Vue.use(Storage, {
namespace: 'system_',
name: 'ls',
storeage: 'local'
});
基本用法如下:
//存储数据
Vue.ls.set('foo', 'boo', 60 * 60 * 1000); //缓存1小时
Vue.ls.get('foo');
Vue.ls.get('boo', 10); //如果没有获取到boo数据,默认返回10
Vue.ls.on('foo', callback) //监听foo值变化,触发Callback回调函数
Vue.ls.off('foo', callback) //卸载foo监听事件
//移除foo对应缓存数据
Vue.ls.remove('foo');
3.2 store状态管理
Vuex是一个专为Vue.js应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件状态,并在相应的规则保证状态以一种可预测的方式发生变化。
什么情况下会使用到Vuex:比如登录的用户信息,接口的统一访问令牌等,会频繁使用和全站通用的数据,可以寄存在状态管理器中。
现在在store目录创建相应文件:
3.2.1 创建state.js文件
代码如下:
/**
* 状态,变量库
*/
const state = {
/**
* 访问令牌
*/
token: "",
/**
* 用户信息
*/
userInfo: null
}
export default state;
3.2.2 创建mutationsType.js文件
代码如下:
/**
* 用户信息
*/
export const USERINFO = "USERINFO";
/**
* 访问令牌
*/
export const TOKEN = "TOKEN";
3.2.3 创建mutations.js文件
代码如下:
import { USERINFO, TOKEN } from './mutationsType'
const mutations = {
/**
* 修改访问令牌信息
*/
[TOKEN](state, param){
state.token = param;
},
/**
* 修改用户信息
*/
[USERINFO](state, param){
state.userInfo = param;
}
}
export default mutations;
3.2.4 创建getters.js文件
代码如下:
const getters = {
/**
* 用户信息
*/
userInfo(state){
return state.userInfo;
},
/**
* 访问令牌
*/
accessToken(state){
return state.token;
}
}
export default getters;
3.2.5 创建actions.js文件
代码如下:
import Vue from 'vue'
import { USERINFO, TOKEN } from './mutationsType'
/**
* 业务层
*/
const actions = {
/**
* 检查是否登录
*/
checkIsLogin(){
let token = Vue.ls.get(TOKEN);
return new Promise((resolve, reject) => {
if(token){
resolve();
}else{
reject();
}
});
},
/**
* 保存登录信息
*/
saveLoginInfo({commit}, param){
if(param['token']) {
commit(TOKEN, param.token);
Vue.ls.set(TOKEN, param.token);
}
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;
3.2.6 创建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
})
3.2.7 main.js中引入
代码如下:
import Vue from 'vue'
import App from './App'
import elementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import 'element-ui/lib/theme-chalk/base.css'
import Storage from 'vue-ls'
import store from '@/store/index'
Vue.use(elementUI);
Vue.use(Storage, {
namespace: 'system_',
name: 'ls',
storeage: 'local'
});
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
components: { App },
template: '<App/>'
})
这里store本地状态管理器中文件就创建和定义完了,后续可直接使用this.$store进行调用即可。
3.3 router路由定义
在定义路由前,先在pages目录下,对应创建所有页面文件,如管理员列表页、年级列表页、班级列表页、教师列表页、学员列表页,以及错误页和登录页。
另外,我们需要在components组件中,新建Layout.vue文件,用来定义菜单和内容区域,作为公共部分。代码如下:
html代码部分:
<template>
<div class="layout-wrap">
<el-container class="container">
<el-aside width="200px" class="aside-box">
<div class="title">
<h3>学员管理系统</h3>
</div>
<el-menu default-active="1" class="el-menu-vertical" :router="true">
<el-menu-item index="1" :route="{path: '/sys/mange'}">
<i class="el-icon-s-custom"></i>
<span slot="title">管理员列表</span>
</el-menu-item>
<el-menu-item index="2" :route="{path: '/sys/grade'}">
<i class="el-icon-house"></i>
<span slot="title">年级管理</span>
</el-menu-item>
<el-menu-item index="3" :route="{path: '/sys/classify'}">
<i class="el-icon-collection-tag"></i>
<span slot="title">班级管理</span>
</el-menu-item>
<el-menu-item index="4" :route="{path: '/sys/teacher'}">
<i class="el-icon-user"></i>
<span slot="title">教师列表</span>
</el-menu-item>
<el-menu-item index="5" :route="{path: '/sys/student'}">
<i class="el-icon-user"></i>
<span slot="title">学员列表</span>
</el-menu-item>
</el-menu>
<div class="btn-box">
<el-button type="danger" size="mini" @click="logoutEvent">退出登录</el-button>
</div>
</el-aside>
<el-main>
<transition name="el-zoom-in-center">
<router-view />
</transition>
</el-main>
</el-container>
</div>
</template>
JS部分:
<script>
export default {
name: 'Layout',
data () {
return { }
},
methods: {
/**
* 退出登录
*/
logoutEvent(){
}
}
}
</script>
样式部分:
<style lang="scss" scoped>
.container{ height: 100vh; }
.el-menu{ border-right: 0; }
.el-menu-vertical{ height: 100%; }
.aside-box{
position: relative;
padding-top: 80px;
border-right: 1px solid #e6e6e6;
.title{
width: 100%;
padding: 30px 0;
text-align: center;
position: absolute;
left: 0;
top: 0;
z-index: 10;
h3{
font-size: 20px;
color: #409EFF;
}
}
}
</style>
待这些页页创建成功后,我们将在router/index.js中引入这些模块,用来定义页面跳转路由路径,代码如下:
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '@/components/Layout'
import Error404 from '@/pages/Error/err404'
import Index from '@/pages/index'
import Student from '@/pages/student'
import Mange from '@/pages/mange'
import Grade from '@/pages/grade'
import Classify from '@/pages/classify'
import Teacher from '@/pages/teacher'
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: Index,
},
{
path: '/sys/student',
name: 'Student',
component: Student,
},
{
path: '/sys/mange',
name: 'Mange',
component: Mange,
},
{
path: '/sys/grade',
name: 'Grade',
component: Grade,
},
{
path: '/sys/classify',
name: 'Classify',
component: Classify,
},
{
path: '/sys/teacher',
name: 'Teacher',
component: Teacher,
}
]
},
{
path: '/login',
name: 'Login',
component: Login,
},
{
path: '*',
name: 'Error404',
component: Error404,
},
]
});
_router.beforeEach((toRoute, fromRoute, next) => {
next();
});
export default _router;
此时,我们可以点击页面左侧菜单,进行页面跳转了。关于登录功能,和登录页面跳转,将会在后面继续讲解。
四、登录功能
4.1 权限校验
在前面我们定义好了路由相关跳转路径,那我们如何跳转到登录页呢。此时大家可以往前翻看“3.2.5 创建actions.js文件”中,定义了checkIsLogin函数,用来判断用户是否登录了。在里我们则可以对router/index.js中的 路由卫士进行 稍微调整即可,代码如下:
_router.beforeEach((toRoute, fromRoute, next) => {
store.dispatch('checkIsLogin').then(() => {
next();
}).catch(() => {
if(toRoute.path=='/login'){
next();
}else{
next('/login');
}
});
});
通过执行checkIsLogin函数,来判断用户是否已登录,如登录则直接跳转到下一路由页面中,否则跳转到登录页,完成登录后,则可以正常访问系统页面。为什么要在此判断呢,因为所有页面的访问都要经过“路由卫士”,系统登录是有时效性的,一旦超时token则会自动失效;所以当token失败时,用户点击下一步操作,则会自动跳转到登录页面。
另外,由于这里引用了Promise,所以会出现以下错误:
vue-router Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current
这时按以下方法修改即可:
//存储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');
}
});
});
4.2 登录页面
此时开始着手完成登录页部分的代码,这里直接贴代码了。
html部分:
<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="demo-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>
js部分:
<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) {
console.log(this.ruleForm);
} else {
console.log('error submit!!');
return false;
}
});
},
/**
* 重置表单
* @param {Object} formName
*/
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
样式部分:
.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: "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;
}
}
}
}
登录页界面如下图:
4.3 添加打开事务功能
在2.2中我们完成了“数据库打开”操作文件的代码,此时需要在内部添加两个打开事务的执行函数,以及一个读写的常量值(IndexedDB中有对应常量值,但已废弃,这里自己定义即可),在db/index.js文件中增加代码如下:
/**
* 返回是只读或读写模式
*/
export const CONST_READ = {
READONLY: "readonly",
READWRITE: "readwrite"
}
/**
* 打开索引用户名游标
*/
export const openTransactionIndex = (storeName, indexName, mode) => {
if(!storeName){
throw '请指定打开事务的表名!';
}
if(!indexName){
throw '请指定需查询索引名!';
}
mode = mode || CONST_READ.READONLY;
//开启事务
let transaction = Database.transaction([storeName], mode);
//连接对象仓库
let store = transaction.objectStore(storeName);
//获取对应索引
let index = store.index(indexName);
//返回游标
let cursor = index.openCursor();
return {
store,
index,
cursor
}
}
4.4 api请求定义
在src目录下新建api/index.js文件,用来定义接口请求功能函数。我们先在db/model目录下创建user.js,用来操作管理员表的增删改查;创建好后,先定义好login和ogout函数,用来处理登录和退出功能,代码如下:
/**
* 通过索引获取对应数据
*/
export const login = data => {}
/**
* 退出登录
*/
export const logout = token => {}
在db/model/user.js创建好后,我们可以在api/index.js中定义登录和退出接口请求了,代码如下:
import { login, logout } from '@/db/model/user'
/**
* 登录
*/
export const loginInfo = (params) => {
return login(params);
}
/**
* 退出
*/
export const logoutInfo = params => {
return logout(params);
}
4.5 登录和退出业务功能
在4.3中,我们在db/index.js中添加了添加了处理索引事务函数,我们将其引入到db/model/user.js表中,用来实现登录用户查询和accesstoken信息查询。
另外,用户密码保存时是通过md5加密处理的,所以登录时匹配用户密码时,也需要使用到md5处理函数,这里也需要引入。
用户登录成功时候,需要生成accesstoken随机字符串,这里咱们把这类功能函数放在utils/utils.js工具类中;还有在所有数据请求成功后,需要返回一个统一的数据格式JSON文件,我们也在工具类文件中定义个rJson函数,工具类文件代码如下:
/**
* 随机生成字符串
* @param {*} _len
*/
export const randomStrName = _len => {
let _string = 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ1234567890',
_returnStr = '';
_len = 'number'===typeof _len ? _len : 50;
for(var i = 0; i < _len; i++){
_returnStr += _string.charAt( Math.floor(Math.random() * (_string.length-2)) )
}
return _returnStr;
}
/**
* 返回数据对象
* @param code 状态码, 0 表示错误, 1表示正常
* @param data 返回数据集
* @param msg 返回提示信息
*/
export const rJson = (code, data, msg) => {
code = code || '0';
data = data || null;
msg = msg || '';
return {
code,
data,
msg
}
}
以上准备工具做好后,我们还要在user.js文件中定义storeName,指定当前文件操作的表。login和logout功能函数实现上,使用Promise来异步操作,这样使回调数据更为灵活点。
代码如下:
import { openTransactionIndex, CONST_READ } from '@/db'
import { rJson, randomStrName } from '@/utils/utils'
import { hex_md5 } from '@/utils/md5'
let storeName = 'users';
/**
* 通过索引获取对应数据
*/
export const login = data => {
return new Promise((resolve, reject) => {
});
}
/**
* 退出登录
*/
export const logout = token => {
return new Promise((resolve, reject) => {
});
}
4.6 实现登录功能
现在各项关系的文件都创建好了,并且已关联上,咱们可以找到“4.2 登录页面”的js部分,引入api/index.js中的loginInfo,来实现登录数据上传并校验。另外,之前在“3.2.5 创建actions.js文件”中,定义了登录保存用户信息业务函数saveLoginInfo,在登录成功后,需调用此函数将用户信息和accesstoken保存为全局变量中,以及缓存本地存储中。打开pages/login/index.vue文件,代码如下:
<script>
import { loginInfo } from '@/api'
export default {
data(){
// ...
return {
disabledButton: false, //是否禁用按钮
ruleForm: {
username: '',
password: '',
},
rules: {
username: [
{ validator: validateUsername, trigger: 'blur' }
],
password: [
{ validator: validatePass, trigger: 'blur' }
]
},
}
},
methods: {
/**
* 提交数据
*/
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
loginInfo(this.ruleForm).then(res => {
if(res.code==1){
//缓存数据
this.$store.dispatch('saveLoginInfo', {
token: res.data.accesstoken,
userinfo: res.data.userinfo
})
}
this.$message.success(res.msg);
this.$router.push('/');
}).catch(e => {
this.$message.error(e.msg);
this.resetForm('ruleForm');
});
} else {
return false;
}
});
},
// ...
}
}
</script>
这里输入用户名和密码进行登录,是没有返回数据的,因为我们还没实现数据库查询功能,现在大家打开db/model/user.js文件,实现login函数部分代码。
在前面“2.3 表结构创建”中,有创建name和accesstoken两个索引;又在“4.3 添加事务功能”中新增了事务创建相关功能函数,这里咱们就可以用上了。首先登录用户名是唯一的,这里通过name索引进行匹配查询,这里openTransactionIndex函数获取相应处理对象。
代码如下:
export const login = data => {
return new Promise((resolve, reject) => {
//打开事务
let { index, store } = openTransactionIndex(storeName, 'name', CONST_READ.READWRITE);
//通过用户名,获取用户数据
let indexData = index.get(data.username);
indexData.onerror = function(e){
reject(
rJson(0, e, '查询出错了~')
);
}
indexData.onsuccess = function(e){
let result = e.target.result;
if(result){
//判断密码是否一致
if(result.password==hex_md5(data.password)){
let accesstoken = randomStrName(80);
//记录token值
result['accesstoken'] = accesstoken;
//保存token
store.put(result);
//返回结果数据
resolve(
rJson(1, {
accesstoken,
userinfo: {
id: result.id,
username: result.name,
phone: result.phone
}
}, '登录成功~')
)
}else{
reject(
rJson(0, null, '密码错误~')
)
}
}else{
reject(
rJson(0, null, '用户名错误~')
)
}
//if end
}
});
}
以上功能完成后,我们就可以实现登录了,这时我们在登录界面输入用户名和密码,则可以跳转到首页了。在“2.3 表结构创建”中,默认添加了admin账号,暂时可以通过这个账号登录系统,登录后界面如下:
4.7 实现退出功能
在“3.3 router路由定义”中的JS部分,我们调用下退出接口,完成退出功能,并通过“3.2.5 创建actions.js文件”中定义的exitLogin函数,清除本地缓存数据。代码如下:
<script>
import { logoutInfo } from '@/api'
import { TOKEN } from '@/store/mutationsType'
export default {
name: 'Layout',
data () {
return { }
},
methods: {
/**
* 退出登录
*/
logoutEvent(){
this.$confirm('确认好退出登录吗?', '提示', {
confirmButtonText: '退出',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
logoutInfo(this.$ls.get(TOKEN)).then(res => {
this.$store.dispatch('exitLogin');
this.$message.success(res.msg);
this.$router.push('/login');
}).catch(e => {
this.$message.error(e.msg);
this.$router.push('/login');
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消退出'
})
})
}
}
}
</script>
同样,现在点击退出按钮不会得到响应,需要将db/model/user.js中的logout功能实现后,才能真正实现退出功能。
代码如下:
export const logout = token => {
return new Promise((resolve, reject) => {
//打开事务
let {index} = openTransactionIndex(storeName, 'accesstoken', CONST_READ.READWRITE);
//通过token,获取对应用户数据
let indexData = index.get(token);
indexData.onerror = function(e){
reject(
rJson(0, e, '查询出错了~')
);
}
indexData.onsuccess = function(e){
let result = e.target.result,
source = e.target.source;
if(result){
let store = source.objectStore;
//判断token信息是否匹配
if(result.accesstoken == token){
//删除accesstoken
delete result['accesstoken'];
//更新数据
store.put(result);
resolve(
rJson(1, null, '退出成功~')
);
}else{
reject(
rJson(0, null, '登录失效~')
)
}
} else{
reject(
rJson(0, null, '未查询到登录信息~')
)
}
}
});
}
这里我们在登录状态下,点击退出按钮,退出成功后就会直接跳转到登录界面了。
4.8 校验缓存数据中的登录信息
我们又要回到 “ 3.2.5 创建actions.js文件 ”中,为什么当初在定里定义函数判断token失效,而不是直接放到 路由卫生中获取token,直接进行判断呢;因为这里除了要校验本地是否缓存token信息,同时也要判断数据库中token是否存在或失效,话不多说,直接上代码。
在db/model/user.js中添加checkToken函数,代码如下:
/**
* 校验token是否存在
*/
export const checkToken = token => {
}
在api/index.js中添加tokenIsFailure函数,代码如下:
import { login, logout, checkToken } from '@/db/model/user'
/**
* 判断数据库中token是否失效
*/
export const tokenIsFailure = token => {
return checkToken(token);
}
store/actions.js文件中的checkIsLogin进行改造,代码如下:
checkIsLogin(){
let token = Vue.ls.get(TOKEN);
return new Promise((resolve, reject) => {
if(token){
tokenIsFailure(token).then(() => {
resolve();
}).catch(() => {
commit(TOKEN, '');
Vue.ls.remove(TOKEN);
reject();
});
}else{
reject();
}
});
}
当读取到本地缓存的token信息后,还需要进行数据库校验;如果数据库中不存在,则会直接跳转到登录页,并清除本地缓存的token信息。
另外,我们之前在“2.2 打开数据库”中定义过DBIsLoadSuccess函数,因为IndexedDB的open()函数不是执行后就返回实例对象的,所有刚进入系统,很有可能会遇到实例对象为空的情况。这时我们需要特别小心,在调用事务前,先判断数据库实例对象是否存在。
接下来让我们完成db/model/user.js中的checkToken函数功能,代码如下:
export const checkToken = token => {
return new Promise((resolve, reject) => {
//判断数据库是否打开
DBIsLoadSuccess().then(() => {
//打开事务
let {index} = openTransactionIndex(storeName, 'accesstoken');
//通过token,查询数据
let indexKey = index.getKey(token);
indexKey.onerror = function(e){
reject(
rJson(0, e, '查询出错了~')
);
}
indexKey.onsuccess = function(e){
let result = e.target.result;
if(result){
resolve(
rJson(1, e, '校验成功~')
)
}else{
reject(
rJson(0, e, '登录失效~')
);
}
}
}).catch(() => {
reject(
rJson(0, e, '数据库打开失败~')
);
})
});
}
这里我们先手动清除数据表中的accesstoken,然后点击任意页面跳转,此时会发现页面直接跳转到登录页了。
正常情况下,在登录时创建的token,是带有时效的,这块这里就不细讲,大家可以通过自己的理解,去完善此部分。
至此,该篇内容已讲解完了,该系统其他功能完善请看后续篇幅。