又是很久都没有整理过了,这次也不立什么一定要坚持写博客的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-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: { "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";
const Axios = axios.create({ baseURL: "http://106.14.153.164:6374", timeout: 10000, responseType: "json", withCredentials: false, headers: { "Content-Type": "application/json;charset=utf-8" } });
Axios.interceptors.request.use( config => {
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 => { console.log("request:", error); return Promise.reject(error); } );
Axios.interceptors.response.use( res => { if (res.data && !(res.data.code === 200)) { console.log("返回状态判断"); 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请求文件里面,多加上一个参数requiresAuth
为true
则表示需要鉴权。然后在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
|
router.beforeEach((to, from, next) => { 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")) { 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 { 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";
export const getEsi = (firDirectory, secDirectory) => { if (firDirectory === "periodical") { if (secDirectory === "all") { api.search .searchAll() .then(response => { console.log(response.data.data); let { totalElemNums, data } = response.data.data; let articleTotal = totalElemNums; let articleList = data; let checkedArr = articleList.map(obj => obj.esiId);
store.dispatch("getArticleTotal", articleTotal); store.dispatch("getArticleListList", articleList); store.dispatch("getCheckedArr", checkedArr);
store.dispatch("getCheckedId", []); }) .catch(error => { console.log(error); }); } else { const periodicalTimeTxt = { current: "current", new: "newAddition", decrease: "fellOut" }; console.log(periodicalTimeTxt[secDirectory]); api.search .searchCurrent(periodicalTimeTxt[secDirectory]) .then(response => { let { totalElemNums, data } = response.data.data; let articleTotal = totalElemNums; let articleList = data; let checkedArr = articleList.map(obj => obj.esiId);
store.dispatch("getArticleTotal", articleTotal); store.dispatch("getArticleListList", articleList); store.dispatch("getCheckedArr", checkedArr);
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 }); }, 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);
|
该组件代码参考了一片文章。只贴了一部分代码。首先,开发插件要使用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.$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);
来调用。
虽然不知道合不合理,但是确实解决了这一问题。