项目演示地址:https://hujiyi.github.io/acme-world-web/


LeanCloud 数据存储

LeanCloud 数据存储 的两种 Class

LeanCloud 数据存储的 Class 有两种,一种是创建应用时就自带的,这一种Class的名字以 下划线开头,可以在这些 Class 中添加字段, 但一般不要删除;
另一种是用户自己创建的 Class,可以由用户自定义需要的字段, 同时,自定义的每个 Class 还会有四个系统自带的字段:objectId, ACL, createdAt, updatedAt, 说明如下:

  • objectId: 主键字段,实际使用时,可以通过 类名.id 获取值;

  • ACL: 访问控制列表, 用于权限管理开发,免费用户不可用;

  • createdAt: 记录创建时间,添加记录时自动产生,不能更改;

  • updatedAt: 记录更新时间,记录有更新时自动修改,不能由用户进行修改;

在数据存储中创建 Class

在数据存储中创建 Class 有两个方法:

方法一:在 LeanCloud 数据存储 的管理界面 手工 添加 Class, 然后再逐个添加字段(也可以从一个 数据存储 中导出 Class, 然后到另一个应用中导入);

方法二:直接在程序中创建一个 JSON 格式的数据,通过 LeanStorage 数据存储 SDK 保存, LeanCloud 服务器端即可根据JSON数据格式自动添加 Class, 以及添加 Class 中没有的字段。 总的来说,我现在比较喜欢用这一种方法。

通用的数据存储服务类

除了创建 LeanCloud 应用时系统自动的 Class 以外,其他用户自行添加的 Class 进行查、改、增、删 时执行的操作都是类似的,所以可以定义一个通用的数据存储服务基类,然后每个 Class 再继承这个基类,就可以实现大部分代码共用了。

src/api/service/ 文件夹下添加文件:base_service.js

打开文件 src/api/service/base_service.js, 编辑其代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import LC from 'leancloud-storage';  // 导入 leancloud 数据存储模块


/**
* Leancloud 数据存储的基类
* 所有函数使用 async/await 实现异步操作
* 虽然有个别函数不需要使用异步,但调用时难以区分,所以就全部都加上异步了
* 调用时,函数名前面一定要加上 await, 否则得不到正确的结果
*/
class BaseService {
/**
* 构造函数
* @param {*} table_name
*/
constructor(table_name) {
this.TABLE_NAME = table_name;
}

/**
获取所有数据
* @param {*} limit 每页记录数量
* @param {*} skip 跳过记录数
* @param {*} include 关联到其他 Class 的字段名,例如: 'createdBy', 'lastEditor'
* @param {*} sort_fields 降序排列的排序字段名列表,例如:['-level', '-updatedAt'], 降序的字段名前面加负号
* sort_fields 和 include 的默认值都是: [], 只包含一个字段时也要加: []
* @returns
*/
async fetchAll(limit = 10, skip = 0, include = [], sort_fields = []) {
try {
let query = new LC.Query(this.TABLE_NAME);
if (include[0]) {
// 关联查询其他表
query.include(include);
}

// 判断是否包含排序的字段名
if (sort_fields.length > 0) {
for (let i = 0; i < sort_fields.length; i++) {
// 多字段排序时, 第一个排序字段 和之后用的方法名不同
if (i === 0) {
if (this.isMinus(sort_fields[0])) {
// 去掉负号后的字段名降序
query.descending(sort_fields[0].substr(1));
}
else {
query.ascending(sort_fields[0]); // 升序
}
} else {
// 第二个及以后的排序字段和第一个字段调用的方法不一样
if (this.isMinus(sort_fields[i])) {
// 去掉负号后的字段名降序
query.addDescending(sort_fields[i].substr(1));
}
else {
query.addAscending(sort_fields[i]); // 升序
}
}
}
}
let total = await query.count(); // 记录总数
// 分页查询的结果
let response = await query.limit(limit).skip(skip).find();
// 返回的查询结果由三个部分组成;
return {
"status_code": "ok", // 返回结果中的状态码
"totalCount": total, // 记录总数
"reslut": response, // 返回的记录
};
} catch (e) {
console.log('查询数据错误:', e.code, e);
return {
"status_code": e.code,
"totalCount": 0,
"reslut": e,
};
}

}


/**
* 添加数据
* @param {*} data 以对象形式提供的数据
* @returns 创建成功的对象
*/
async create(data) {
try {
let Collection = LC.Object.extend(this.TABLE_NAME);
let query = new Collection(data);
let response = await query.save();
return response;
} catch (e) {
console.log('添加数据错误:', e.code, e);
}
return null;
}

/**
* 修改记录
* @param {*} id 记录的id
* @param {*} data 除 id 以外的其他字段组成的对象类型数据
*/
async update(id, data) {
try {
let query = LC.Object.createWithoutData(this.TABLE_NAME, id);
query.set(data); // 修改数据
let response = await query.save();
return response;
} catch (e) {
console.log('修改数据错误:', e.code, e);
}
return null;
}

/**
* 批量修改数据
* @param {*} items 包含待更新值的记录数组
* @returns
*/
async updateBatch(items) {
console.log('items:', items);
try {
// map遍历数组,由返回值组成一个新数组, 原数组不变
let id_list = items.map(item => { return item.id });
let query = new LC.Query(this.TABLE_NAME);
let response = await query
.containedIn('objectId', id_list)
.find();
// map() 遍历数组,不改变原数组
let result = response.map(item => {
let data = items.find(it => {
console.log('update batch:', it);
return it.id === item.id
});
// console.log('update batch:', data);
item.set(data);
return item;
});
return await LC.Object.saveAll(result);
} catch (e) {
console.log('批量修改数据错误:', e.code, e);
}
return null;
}

/**
* 删除数据
* @param {*} id
* @returns
*/
async delete(id) {
try {
let query = LC.Object.createWithoutData(this.TABLE_NAME, id);
let response = await query.destroy();
return response;
} catch (e) {
console.log('删除数据错误:', e.code, e);
}
return null;
}


/**
* 批量删除,先按 id 查询出所删除的数据,再调用destroyAll() 进行删除
* 其他批量操作类似
* @param {*} items 待删除记录的数组
* @returns
*/
async deleteBatch(items) {
try {
// 从要删除的记录中取出 id 值,组成一个新数组
let id_list = items.map(item => { return item.id });
let query = new LC.Query(this.TABLE_NAME);
// 根据多个 id 组成的数组查询符合条件的记录
let response = await query
.containedIn('objectId', id_list)
.destroyAll();
console.log('deleteBatch:', response)
return true;
} catch (e) {
console.log('批量删除数据错误:', e.code, e);
}
return null;
}

/**
* 检查指定的字段值是否已经存在, 字母区分大小写
* @param {*} field 字段名
* @param {*} val 字段值
* @returns 字段值已存在,返回 true, 否则返回 false
*/
async existsFieldValue(field = 'title', val) {
try {
let query = new LC.Query(this.TABLE_NAME);
let count = await query.equalTo(field, val).count();
console.log(count);
return count > 0;
} catch (e) {
console.log('查询错误:', e.code, e);
}
return false;
}

/**
* 当前已登录用户
* @returns 返回当前已登录用户
*/
async currentUser() {
return LC.User.current();
}


/**
* 判断字符串是不是负号开头, 以便确定是升序还是降序
* @param {*} val
* @returns 负号:true; 非负号: false
*/
isMinus(val) {
if (val[0] === '-') {
return true;
}
return false;
}
}

export default BaseService;

LeanCloud 数据存储的基类创建完毕等待应用。

路由守卫的使用

后台管理(仪表盘)一般来说,都要求用户成功登录后才能访问,但在之前的代码中,并没有做这样的限制, 所以任何人都可以直接进入后台管理。

这里使用 vue router路由守卫来实现进入后台管理的权限验证

打开文件 src/router/index.js, 给后台管理的的父路由项添加路由元信息 meta: { requiresAuth: true }, , 同时添加相应的路由守卫。修改其如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import Vue from 'vue';
import VueRouter from 'vue-router';


Vue.use(VueRouter);

const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/home/Index.vue'),
},
{
path: '/account',
redirect: '/login',
component: () => import('../views/account/Index.vue'),
children: [
{
path: '/login',
name: 'Login',
component: () => import('../views/account/pages/Login.vue'),
},
{
path: '/signup',
name: 'SignUp',
component: () => import('../views/account/pages/SignUp.vue'),
},
{
path: '/password_reset',
name: 'PasswordReset',
component: () => import('../views/account/pages/PasswordReset.vue'),
},
]
},
{
path: '/dashboard',
component: () => import('../views/dashboard/Index.vue'),
// 添加路由元信息 meta: { requiresAuth: true, } 用于标识进入该路由必须登录
meta: { requiresAuth: true },
children: [
{
path: '',
alias: 'index',
name: 'Dashboard',
component: () => import('@/views/dashboard/pages/MainIndex.vue'),
},
{
path: 'topic_manager',
name: 'TopicManager',
component: () => import('@/views/dashboard/forum/TopicManager.vue'),
},
{
path: 'comment_manager',
name: 'CommentManager',
component: () => import('@/views/dashboard/forum/CommentManager.vue'),
},

],
}
];

const router = new VueRouter({
routes
});



// 使用路由守卫,具体内容见官方文档:https://router.vuejs.org/zh/guide/advanced/meta.html
// 特别注意事项:确保 router.beforeEach 中只有一个 next() 会被执行
// 如果有多个 next() 被执行,将会出现重复路由的异常
router.beforeEach((to, from, next) => {
let login_router = ['/account', '/login'];
// 如果 跳转的目标 path 是登录页(看上面的路由设置 )
if (to.path in login_router) {
// 保存跳转到登录页之前的地址, 登录成功后跳转回该页
// 回跳功能在 登录页 "src/views/account/pages/Login.vue" 实现
localStorage.setItem("preRoute", router.currentRoute.fullPath);
}

// 检查跳转的目标路由是否需要登录(包含 meta: {requiresAuth: true, },)
if (to.matched.some(record => record.meta.requiresAuth)) {
// 检查用户是否登录
if (!localStorage.getItem('token')) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next();
}
} else {
next();
}
});


export default router;

路由守卫 通过 路由元信息 判断该路由项是否需要登录后才能访问,如果需要,但当前没有已登录用户,则自动跳转到 登录界面,登录成功后,返回之前的页面(返回前一页在登录页实现,之前完成的登录页已经包含相应代码)

后继内容:
使用 Element UI 和 Leancloud 的 Vue.js 项目开发 VIII

===END===