又是很久都没有整理过了,这次也不立什么一定要坚持写博客的flag了。这一段时间忙了一些别的事情,技术上把Vue全家桶用于项目中实践了一下,也算是收获不少。

大概需求

先放一张大概效果图。

需求简单点讲就是学科期刊,以及热点论文什么的查询。内容上分为六个模块,其中一个ESI学科期刊模块,两个顶级论文模块内容基本一样。查询时左侧会分别用关键词、月份、年份、学科进行过滤。然后还要支持导出excel,就是把数据整合成excel表格导出。

需求并不是很复杂,但是实际写的时候确实也是考虑了很多东西,算是在代码规范上更近了一步吧。

结构设计

组件抽象

学科期刊和顶级论文这三个模块内容基本一致,返回数据不同,因此可以把他们归为一类。

左侧过滤组件

1
2
3
4
5
6
7
<FilterForm 
v-if="isRenderTime"
filterType="timeMonth"
ref="checkedMonth"
:filterItems="formData.filterMonth"
@give-conditions="getArticaleData"
></FilterForm>

左侧抽象出来过滤组件,其中关键词查询都有不用做过多抽象。

分类检索部分分三块,也可以抽象成一个小的过滤组件。通过父级组件传值filterType,来确定渲染成哪一部分的过滤模块

其中由于月份的显示方式不太一样,为了节省空间,一行放了两个。所以需要绑定一个特殊的class,再用一个计算属性isMonth来判断是否是月份过滤,来判断有display: inline-block的class是否显示该组件上

月份、年份、学科这三个过滤条件并不是每个页面都需要,因此根据获取的路由数据来判断哪一个过滤条件需要显示

由于点击一个过滤按钮,需要获取其所有兄弟过滤组件的选中数据,所以这里我通过在过滤组件点击时,向父级元素传递信号,然后在父级元素上通过ref获取了三个过滤组件的值。

vuex数据管理

把Vuex的相关代码分割成了数个模块,便于管理。然后提交方式都是通过action异步提交(这样更规范一些?此处后面还需要深入理解一下)

查询里面的条件全部都用了Vuex来管理,关键词页码月份年份学科(此处可能还需要深入思考一下,关键词月份年份学科这四个确实还有别的兄弟组件渲染的时候需要使用,但仅仅是读取并不会修改,放在Vuex里面管理代码方便了很多,但是究竟有没有更合适的方法还需要深入思考一下。但是页码这个并没有别的组件需要共享,放在里面纯属是因为和后台传参的时候直接读取Vuex的数据就行,图个方便,是否合理确实还要三思一下)

搜索结果,就是后端传回来的文章数据,我也把他放在了Vuex里面管理。当时想的是,获取结果有四种方式,分别是页面跳转关键词查询过滤条件页码这四个部分需要从后端获取数据,后端返回数据以后直接分发提交到Vuex里面,然后页面直接响应式渲染出来(其实也是图个方便,而且也确实目前的水准想不出什么更好的方式来组织结构)。

文章下载全选功能,这个应该是用的比较的合适了。就是点击全选后,所有文章前面的多选框都要确定上,然后若某一个多选框取消了,全选前面的框也要取消。从这个角度看,双方共同维护这个选中的文章数组,两方都是可读可写,此处用的应该可以说是非常合适了。

因此,总结一下,Vuex用了之后非常的方便,但是也可能造成一些Vuex的滥用,目前也确实没有想到比较好的方式来管理,此处需要在项目结束之后深入思考一下。

关于css

css嵌套问题

之前为了图方便,往往把子元素都写在其父元素内,导致父元素的class拉的很长,这样一是不便查看,二是不便于组件复用(只有位于特定父元素下,该class才会生效,不便于复用)。
重构后,发现代码可读性并不是很高。于是再次重构,将联系紧密的嵌套一起,嵌套层数不超过2层,感觉可读性更强了一些。

还有css不要嵌套选择器,比如说导航 .list a ,像这样可能会导致后面改动的问题,比如说list内又加了一个item,item里加了一个a,这时候就会影响到item a,不便于维护。

将less的全局变量文件抽离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 深灰
@deep-grey: #e6e6e6;
// 浅灰
@light-grey: #F1F1F1;
// 深色边框
@border-deep: #bababa;
// 浅色边框
@border-light: #d3d3d3;
// header底色
@header-blue: #2a4c90;
// 字体颜色
@font-color: #4b505a;
// 搜索结果面板背景
@content-color: #f8f8f8;
// 按钮颜色
@button-color: #e4e5ec;

为了方便项目颜色的调整,很多时候取的颜色都是相同的,这里可以使用less的变量定义。但是这里如果每个vue文件都要引入一遍less文件,会很麻烦,也不方便维护。这里使用style-resources-loader插件来管理,npm安装好后,在vue.config.js里面配置

1
2
3
4
5
6
7
8
9
10
11
12
const path = require("path");

module.exports = {
pluginOptions: {
// 配置less全局变量
"style-resources-loader": {
preProcessor: "less",
patterns: [path.resolve(__dirname, "src/global.less")]
}
}
};

配置好后,就可以直接在每个vue中使用了(此处需要研究一下原理)。

axios二次封装

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
import axios from "axios";
// import qs from "qs"; // 序列化请求数据,视服务端的要求
// import router from "../router";

const Axios = axios.create({
baseURL: "http://106.14.153.164:6374",
timeout: 10000,
responseType: "json",
withCredentials: false, // 是否允许带cookie这些
headers: {
"Content-Type": "application/json;charset=utf-8"
}
});

//POST传参序列化(添加请求拦截器)
Axios.interceptors.request.use(
config => {
// 在发送请求之前做某件事
// if (config.method === "post") {
// // 序列化
// config.data = qs.stringify(config.data);
// }

// 若是有做鉴权token , 就给头部带上token
// 若是需要跨站点,存放到 cookie 会好一点,限制也没那么多,有些浏览环境限制了 localstorage 的使用
// 这里localStorage一般是请求成功后我们自行写入到本地的,因为你放在vuex刷新就没了
// 一些必要的数据写入本地,优先从本地读取

// 这里在api.js里面加了一个参数,控制是否需要传递鉴权
// 当有requiresAuth且为true是加上鉴权
if (
config.requiresAuth &&
config.requiresAuth === true &&
localStorage.loginUserBaseInfo &&
JSON.parse(localStorage.loginUserBaseInfo).jwtCode
) {
let token = JSON.parse(localStorage.loginUserBaseInfo).jwtCode;
config.headers.Authorization = token;
}
console.log("config:", config);
return config;
},
error => {
// error 的回调信息
console.log("request:", error);
return Promise.reject(error);
}
);

//返回状态判断(添加响应拦截器)
Axios.interceptors.response.use(
res => {
//对响应数据做些事
if (res.data && !(res.data.code === 200)) {
console.log("返回状态判断");
// console.log("res:", res);
alert(res.data.msg);
return Promise.reject(res.data.msg);
}
console.log("res:", res);
return res;
},
error => {
if (error && error.response) {
let msg = error.response.data.msg;
switch (error.response.status) {
case 400:
alert("请求错误:" + msg);
break;
case 401:
alert("未经授权的" + msg);
break;
case 403:
alert("拒绝访问" + msg);
break;
case 404:
alert("请求地址出错" + msg);
break;
case 500:
alert("服务器内部错误");
break;
default:
alert("错误");
}
}
console.log("response:", error.response);
return Promise.reject(error.response);
}
);

export default Axios;

这里由于每次请求分到各个逻辑里面处理的话,代码不好管理,而且代码会很乱,工作量也大。这里把axios做一个二次封装,做一个请求和相应的统一拦截。

比如说很多请求(除了登录)都需要鉴权验证,在请求前做一个请求头统一加上Authorization。然后也可以设置一下统一的Content-Type,这里需要和后端协商一下,让他统一接收参数或者json。如果接收参数的话,也可以使用qs做一个统一序列化。

然后可以共同约定一下返回的错误code,然后把返回的code值根据约定作相应的处理,比如说401鉴权失败,然后重定向到/login

然后也可以把接口统一封装一下,便于修改

然后如果想要在全局使用这个封装好的api,在main.js中引入api文件,然后使用Vue.prototype.$api = api;,(这里应该是把$api放到了Vue的原型上),就可以在全局通过this.$api调用了

JWT鉴权验证

JWT之前一直没用过,这次和后端用了一下,在前后端分离里面用的比较多吧。

那么鉴权验证的流程是什么呢?简单讲就是,前端用户登陆后,后端会返回一个token,然后前端把这个token存到localStorage或者session再或者Vuex里面管理,不同位置有不同的适用范围吧。考虑到session以及Vuex里面管理的话,刷新后就没有了,所以我存到了localStorage里面。

然后每次需要鉴权的时候(比如说这里面的查询就需要鉴权,但是登陆不需要鉴权),把token加到请求头里面,发给后端进行身份验证。然后后端返回code状态码,或者直接重定向什么的都行。

JWT按照我的理解主要用于签名验证,JWT分为三段,验证前两段编码后和第三段是否相同,即可判断是否是目标签名。

具体的一些实现

axios拦截

1
2
3
4
5
6
7
if (
localStorage.loginUserBaseInfo &&
JSON.parse(localStorage.loginUserBaseInfo).jwtCode
) {
let token = JSON.parse(localStorage.loginUserBaseInfo).jwtCode;
config.headers.Authorization = token;
}

这里我一开始把所有的请求都加上了token,这里就是做一个简单的判断,判断本地有没有保存token信息,保存的话就加到请求头里面。

但是后来有一个问题,那就是比如登录并不需要加上token,虽然后端没有判断,一开始并没有什么问题。但是当超过了过期时间之后,我发现登录的时候有莫名的报错,看了好半天才找到了这个问题。那就是后端应该是把这个鉴权统一处理了,那只能我这边来改。

解决方法,在封装过的api请求文件里面,多加上一个参数requiresAuthtrue则表示需要鉴权。然后在axios拦截器里面多加上一个判断config.requiresAuth && config.requiresAuth === true此处需要深入理解一下Promise和ajax的区别

Promise主要用于异步编程,和ajax并无联系,只不过是因为Promise用于ajax异步后,能解决回调地狱问题,更多实践查看js异步编程

vue-router全局导航守卫

这里每次跳转前都要做一个判断,就是判断当前是否有鉴权信息,以及是否过期

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
// vue-router导航守卫,全局守卫
// 并不是所有页面请求都需要加上token,所以需要做一个全局守卫
// 在路由meta加一个字段requiresAuth,设置为true则必须加上鉴权
// 登录页不需要鉴权
router.beforeEach((to, from, next) => {
// 如果检测到meta含有字段
if (to.matched.some(res => res.meta.requiresAuth)) {
// 检测是否有鉴权信息
if (localStorage.loginUserBaseInfo) {
let lifeTime = JSON.parse(localStorage.loginUserBaseInfo).lifeTime;
let nowTime = new Date().getTime();
// 比较当前时间和过期时间
if (nowTime < lifeTime) {
// 有鉴权信息而且未过期
next();
} else {
// 鉴权已过期,跳转到登录页
alert("登录状态过期,请重新登录");
next({
path: "/login"
});
}
} else {
// 没有鉴权信息,跳转到登录页
alert("登录状态过期,请重新登录");
next({
path: "/login"
});
}
} else {
// 无需鉴权信息,继续
next();
}
});

由于后端返回的过期时间是时间段,所以这里判断过期采用的方法是,login登录后获取的过期时间加上getTime(),得到过期时间戳存入localStrage。然后在路由处设置meta字段,来控制路由跳转时是否需要鉴权判断过期时间,如果过期,直接跳转到/login

二进制流文件下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log(response.headers);
let content = response.data;
let blob = new Blob([content]);
let fileName = response.headers["content-disposition"].split("=")[1];
if ("download" in document.createElement("a")) {
// 非IE下载
let elink = document.createElement("a");
elink.download = fileName;
elink.style.display = "none";
elink.href = URL.createObjectURL(blob);
document.body.appendChild(elink);
elink.click();
URL.revokeObjectURL(elink.href);
} else {
// IE 10+ 下载
navigator.msSaveBlob(blob, fileName);
}

这里后端传我的是二进制流,虽然之前写的项目也是这样处理的,但是一个是当时没有认真研究,而且当时没有使用axios,导致这次也卡了一小段时间。

然后就是Blob对象,Blob 对象表示一个不可变、原始数据的类文件对象。这里创建了一个文本节点,并隐藏,触发点击事件,触发下载。而且需要把api封装的接口设置responseType: "blob"

这里获取的是excel文件,后端通过Content-Disposition把文件名返回给我,这里其实有一个问题。虽然从network里面分析到了该请求头,但是axios并没有拦截到该请求头,这里需要后端设置Access-Control-Expose-Headers才能获取到该请求头。

但是目前有一个问题还没有解决,那就是后端直接返回我的是data: Blob(),似乎是不能再多返回一个code状态码?没有状态码的话,我前面axios的拦截就不能统一根据返回的code来采取不同的处理。这里暂时没有解决,后面需要尝试写一下后端研究一下。

解决方案:直接判断返回类型,如果返回的是Blob型,直接另一套操作

1
if (res.data && !(res.data instanceof Blob) && !(res.data.code === 200))

将共同方法抽离

情景:左侧过滤,翻页,页面跳转,排序四处操作都需要发送查询结果的请求,其返回结果都差不多,只是参数数量和数据不同。根据不同模块,其调用接口不相同,而且每处操作有略微不同,这是抽离的难处。但是,每次改动后,都需要将这些方法逐个修改,代码可维护性极差,因此,将其抽离出来势在必行。

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
import store from "@/store/index";
import api from "@/request/api";

/**
* 获取ESI期刊的方法抽离
*
* 传入两个参数,一个是一级目录名,一个是二级目录名
* 这里两个api接口的传过去的参数不同,但是获取数据后的操作相同
* 所以用if判断了二级目录
* 在全期下是一个接口
* 在当期,当期新增,当期跌出是一个接口
* @param {*} firDirectory
* @param {*} secDirectory
*/

export const getEsi = (firDirectory, secDirectory) => {
if (firDirectory === "periodical") {
// 如果当前在esi期刊目录下
if (secDirectory === "all") {
// 如果在全期
// 获取数据
api.search
.searchAll()
.then(response => {
console.log(response.data.data);
// ES6变量解构
let { totalElemNums, data } = response.data.data;
let articleTotal = totalElemNums;
let articleList = data;
// map遍历文章数组,取出esiId属性重新组成数组
let checkedArr = articleList.map(obj => obj.esiId);

// 提交文章数量和文章列表
store.dispatch("getArticleTotal", articleTotal);
store.dispatch("getArticleListList", articleList);
// 提交文章id数组
store.dispatch("getCheckedArr", checkedArr);

// 初始化Vuex中的checkedId
store.dispatch("getCheckedId", []);
})
.catch(error => {
console.log(error);
});
} else {
// 在esi模块的其它栏目下
const periodicalTimeTxt = {
current: "current",
new: "newAddition",
decrease: "fellOut"
};
console.log(periodicalTimeTxt[secDirectory]);
// 获取数据
api.search
.searchCurrent(periodicalTimeTxt[secDirectory])
.then(response => {
// ES6变量解构
let { totalElemNums, data } = response.data.data;
let articleTotal = totalElemNums;
let articleList = data;
// map遍历文章数组,取出esiId属性重新组成数组
let checkedArr = articleList.map(obj => obj.esiId);

// 提交文章数量和文章列表
store.dispatch("getArticleTotal", articleTotal);
store.dispatch("getArticleListList", articleList);
// 提交文章id数组
store.dispatch("getCheckedArr", checkedArr);

// 初始化Vuex中的checkedId
store.dispatch("getCheckedId", []);
})
.catch(error => {
console.log(error);
});
}
}
};

上面只是列举了期刊的处理办法,由于模块不同,分为两级栏目,因此函数设置了两个参数,一个是一级栏目,一个是二级栏目。根据栏目不同调用不同的处理办法。

其中需要管理vuex,以及调用封装好的api请求接口,因此将这两模块引入,然后在每一处需要使用该方法的地方import导入,根据不同情况调用该方法即可。

Vue插件形式扩展一个全局组件

情景:需要做一个类似iview或者element-ui里面的一个全局提示组件,因为alert弹出提示太丑了,该组件多在ajax回调中使用,来显示请求结果成功或者失败

1
2
3
4
5
6
7
8
9
10
<template>
<transition name="fade">
<div class="message" :class="type" v-show="show">
<i v-if="type === 'info'" class="fa fa-info-circle" aria-hidden="true"></i>
<i v-else-if="type === 'success'" class="fa fa-check-circle" aria-hidden="true"></i>
<i v-else-if="type === 'error'" class="fa fa-times-circle" aria-hidden="true"></i>
<span class="text">{{text}}</span>
</div>
</transition>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let VueMessage = Vue.extend({
render(h) {
let props = {
type,
text: msg,
show: this.show
};
// return h("Message", { props: props });
// ES6简写
return h("Message", { props });
},
data() {
return {
show: false
};
}
});
let newMessage = new VueMessage();
let vm = newMessage.$mount();
console.log(vm);
let el = vm.$el;
console.log(el);
document.body.appendChild(el); // 把生成的提示的dom插入body中

该组件代码参考了一片文章。只贴了一部分代码。首先,开发插件要使用Vue公开方法install,然后使用了Vue.extend()创建一个子类,后面的Message组件都是以实例化该类形成的,extend()return的参数可以参照Vue官方文档,作者这里使用render()而并未使用template暂时没看明白原因。不过直接使用template也不会有错。

后面就是要先将实例化对象挂载到DOM上,然后通过$el来访问DOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 添加到window上,暴露三个方法(可以在js文件里直接调用)
window.$message = {
info(text, callBack) {
if (!text) return;
msg("info", text, callBack);
},
success(text, callBack) {
if (!text) return;
msg("success", text, callBack);
},
error(text, callBack) {
if (!text) return;
msg("error", text, callBack);
}
};

还要提一点,因为我把axios二次封装,所以请求回调都没有写在vue文件里面,而是写在了抽离出来的js文件,因此无法通过Vue组件来调用。因此我将方法也同样添加到了到了window上,暴露了三个方法,然后在js文件中直接使用window.$message.error(msg);来调用。

虽然不知道合不合理,但是确实解决了这一问题。