第一节:项目结构介绍、图标的使用、axios封装思路、jwt校验、Vuex的应用

一. 项目结构介绍

1. 结构截图

 

2. 结构说明

  node_modules (各种插件包)

  public (存放浏览器标题favicon.ico、静态json数据)

  src (存放视图、工具类、image)

       assets:(本地静态资源:图片、svg等)

       components: (存放公用全局组件)

       hooks: 对一些第三方插件进行抽离封装,比如:elementPlus、url集合、vuex等

       layout: (存放框架布局视图)

       router:  (存放路由信息)

       store:  (存放组件的状态 pinia 或 vuex)

       theme:   (存放框架样式)

       utils:   (存放工具类函数)

       views:   (存放页面视图)

       App.vue   主页面

       main.js   入口js

  .env (全局默认配置文件,无论什么环境都会加载合并)

  .env.development (开发环境的配置文件)

  .env.production (生产环境的配置文件)

  .eslintignore (eslint忽略配置)

  .eslintrc.js (eslint配置)

  .gitignore (git提交忽略配置)

  .prettierignore (prettier格式化的忽略配置)

  .prettierrc.js (prettier代码格式化配置)

  index.html (用户页面访问入口)

  jsconfig.json (ts配置文件)

  package-lock.json (npm锁定安装时的包的版本号)

  package.json (包的依赖管理配置文件)

  vite.config.ts (vite配置文件)

 

 

二. 图标的使用

1.  注册方法封装

import * as svg from '@element-plus/icons-vue';
import SvgIcon from '/@/components/svgIcon/index.vue';

/**
 * 导出全局注册 element plus svg 图标
 * @param app vue 实例
 * @description 使用:https://element-plus.gitee.io/zh-CN/component/icon.html
 */
export function elSvg(app) {
	//1. 全局注册element plus图标
	const icons = svg;
	for (const i in icons) {
		app.component(`ele-${icons[i].name}`, icons[i]);
	}
	//2. 全局注册自行封装的 SvgIcon
	app.component('SvgIcon', SvgIcon);
	// 注:上面的1和2之间是独立的,只不过放到一个方法中了
}

2. 使用

(1).直接使用elementplus全局注册的组件,以ele-开头,然后直接在style里写属性

<ele-Top style="width: 2em; height: 2em; margin-left: 8px; color: red"></ele-Top>
<ele-Delete style="width: 15px; height: 15px"></ele-Delete>
     或者用el-icon包裹一下,然后可以设置size和color属性
<el-icon size="25px" color="red"><ele-Top />  </el-icon>

(2). svgIcon的使用,对于使用elementplus的图标的时候,name属性中需要加个前缀:ele-

  <SvgIcon name="ele-Top" size="20" color="red" />
  <SvgIcon name="ele-Brush" size="20" color="red" />

(3). 按钮中使用elementplus图标,直接在icon属性中传入全局注册的组件名称,但是这种模式样式有点问题,需要将margin-right清空一下

  <el-button type="danger" icon="ele-Star" circle />
  需要重写样式:
  .el-button i.el-icon {  margin-right: 0px !important;}

(4). 其它图标库的使用【待补充?】

 

三. axios封装思路

 1. 两种提交格式

(1). 表单提交:接口可以分参数接收,也可以用实体接收,不用加[FromBody], 使用封装myAxios

 注:由于是表单提交,自己通过transForm转换的,所以提交数组的时候,会把它以字符串-逗号分隔的方式提交了上去哦

const myAxios = axios.create({
	baseURL: import.meta.env.VITE_API_URL,
	timeout: 20000,
	headers: { 'X-Requested-With': 'XMLHttpRequest' },
	method: 'post', //默认是post请求,可以自行覆盖
	//默认是'application/json'提交,下面代码是手动转换成表单提交
	transformRequest: [
		function (data) {
			let ret = '';
			for (let it in data) {
				ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&';
			}
			return ret.substring(0, ret.length - 1);
		},
	],
});

如果想提交数组,需要用myAxios2进行json格式的提交,接口要用实体接收,接口实体中的某个属性定义为List接收即可(详见:SetUserMenuColumnsUrl)

let newColumnData = [];	
const { status, msg } = await myAxios2({
				                    url: proxy.$url.SetUserMenuColumnsUrl,
				                    data: { menuName: props.pageName, columnData: newColumnData },
                        });

(2). json格式提交:接口只能用实体接收,且必须加[FromBody],使用封装myAxios2

代码分享: 

import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Session } from '/@/utils/storage';

// 配置新建一个 axios 实例1
const myAxios = axios.create({
    baseURL: import.meta.env.VITE_API_URL,
    timeout: 20000,
    headers: { 'X-Requested-With': 'XMLHttpRequest' },
    method: 'post', //默认是post请求,可以自行覆盖
    //默认是'application/json'提交,下面代码是手动转换成表单提交
    transformRequest: [
        function (data) {
            let ret = '';
            for (let it in data) {
                ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&';
            }
            return ret.substring(0, ret.length - 1);
        },
    ],
});

// 添加请求拦截器
myAxios.interceptors.request.use(
    config => {
        // 在发送请求之前做些什么 token
        if (Session.get('token')) {
            config.headers.common['auth'] = `${Session.get('token')}`;
        }
        return config;
    },
    error => {
        // 对请求错误, 针对页面async/await的写法,需要用try-catch包裹获取
        return Promise.reject(error);
    }
);

// 添加响应拦截器
myAxios.interceptors.response.use(
    response => {
        // 响应数据的正常处理
        const res = response.data;
        // 情况1:`token` 过期--通过过滤器返回的
        if (res.status && res.status == 'expired') {
            Session.clear();
            ElMessageBox.alert('您的登录信息已过期,请重新登录', '提示', {});
            setTimeout(() => {
                window.location.href = '/'; // 去登录页
            }, 1500);
            return Promise.reject(res);
        }
        // 情况2:文件流模式的excel导出
        else if (response?.config?.responseType === 'blob') {
            try {
                //需要在服务端配置app.UseCors中配置WithExposedHeaders("Content-Disposition")
                let fileName = decodeURI(response.headers['content-disposition'].split("filename*=UTF-8''")[1]);
                if (!fileName) fileName = `${new Date().getTime()}.xls`;
                let blob = new Blob([res], { type: res.type });
                if (window.navigator && window.navigator.msSaveOrOpenBlob) {
                    window.navigator.msSaveOrOpenBlob(res, fileName);
                } else {
                    let downloadElement = document.createElement('a');
                    let href = window.URL.createObjectURL(blob); //创建下载的链接
                    downloadElement.href = href;
                    downloadElement.download = fileName; //下载后文件名
                    document.body.appendChild(downloadElement);
                    downloadElement.click(); //点击下载
                    document.body.removeChild(downloadElement); //下载完成移除元素
                    window.URL.revokeObjectURL(href); //释放blob对象
                }
            } catch (error) {
                ElMessage.error(res.msg);
            }
        }
        // 情况3:正常请求,但是接口返回status返回error(业务上的错误)
        else if (res.status && res.status == 'error') {
            res.msg && ElMessage.error(res.msg); //当msg中有内容(除了 "" 0 NaN undefined外)
            return Promise.reject(res); //针对页面async/await的写法,需要用try-catch包裹获取
        }
        // 情况4:响应数据的正常返回
        else {
            return res;
        }
    },
    error => {
        // 异常数据的响应处理
        const res = error.response;
        if (res.status === 401) {
            // 情况1:token检验不通过(和文件流下载excel的时候,catch中的返回)
            if (typeof res.data === 'string') ElMessage.error(res.data);
            //情况2: 这里主要是处理文件流形式的excel下载token没有通过校验的情况
            else {
                let reader = new FileReader();
                reader.readAsText(res.data);
                reader.onload = e => {
                    let resText = e.target.result;
                    ElMessage.error(resText);
                };
            }
        }
        return Promise.reject(res);
    }
);

// ----------------------------------------------------------axios 实例2(用于处理json提交)-------------------------------------------------------
// 配置新建一个 axios 实例2
const myAxios2 = axios.create({
    baseURL: import.meta.env.VITE_API_URL,
    timeout: 50000,
    headers: { 'X-Requested-With': 'XMLHttpRequest' },
    method: 'post', //默认是post请求,可以自行覆盖
});

// 添加请求拦截器
myAxios2.interceptors.request.use(
    config => {
        // 在发送请求之前做些什么 token
        if (Session.get('token')) {
            config.headers.common['auth'] = `${Session.get('token')}`;
        }
        return config;
    },
    error => {
        // 对请求错误做些什么
        return Promise.reject(error);
    }
);

// 添加响应拦截器
myAxios2.interceptors.response.use(
    response => {
        // 响应数据的正常处理
        const res = response.data;
        // 情况1:`token` 过期--通过过滤器返回的
        if (res.status && res.status == 'expired') {
            Session.clear();
            ElMessageBox.alert('您的登录信息已过期,请重新登录', '提示', {});
            setTimeout(() => {
                window.location.href = '/'; // 去登录页
            }, 1500);
            return Promise.reject(res);
        }
        // 情况2:文件流模式的excel导出
        else if (response?.config?.responseType === 'blob') {
            try {
                //需要在服务端配置app.UseCors中配置WithExposedHeaders("Content-Disposition")
                let fileName = decodeURI(response.headers['content-disposition'].split("filename*=UTF-8''")[1]);
                if (!fileName) fileName = `${new Date().getTime()}.xls`;
                let blob = new Blob([res], { type: res.type });
                if (window.navigator && window.navigator.msSaveOrOpenBlob) {
                    window.navigator.msSaveOrOpenBlob(res, fileName);
                } else {
                    let downloadElement = document.createElement('a');
                    let href = window.URL.createObjectURL(blob); //创建下载的链接
                    downloadElement.href = href;
                    downloadElement.download = fileName; //下载后文件名
                    document.body.appendChild(downloadElement);
                    downloadElement.click(); //点击下载
                    document.body.removeChild(downloadElement); //下载完成移除元素
                    window.URL.revokeObjectURL(href); //释放blob对象
                }
            } catch (error) {
                ElMessage.error(res.msg);
            }
        }
        // 情况3:正常请求,但是接口返回status返回error(业务上的错误)
        else if (res.status && res.status == 'error') {
            res.msg && ElMessage.error(res.msg); //当msg中有内容(除了 "" 0 NaN undefined外)
            return Promise.reject(res); //针对页面async/await的写法,需要用try-catch包裹获取
        }
        // 情况4:响应数据的正常返回
        else {
            return res;
        }
    },
    error => {
        // 异常数据的响应处理
        const res = error.response;
        if (res.status === 401) {
            // 情况1:token检验不通过(和文件流下载excel的时候,catch中的返回)
            if (typeof res.data === 'string') ElMessage.error(res.data);
            //情况2: 这里主要是处理文件流形式的excel下载token没有通过校验的情况
            else {
                let reader = new FileReader();
                reader.readAsText(res.data);
                reader.onload = e => {
                    let resText = e.target.result;
                    ElMessage.error(resText);
                };
            }
        }
        return Promise.reject(res);
    }
);

// 导出 axios 实例
export { myAxios, myAxios2 };
View Code

 

 2. 返回值各种情况封装总结

(1). 异常数据的响应处理,如401未授权,在全局拦截中进行处理;也调用的时候写catch也可用捕获401,但是全局拦截还是会处理

    注:这里区分不同的401未授权 和 导出Excel的401,都是弹框提示 + Promise.reject抛出。

(2). 正常数据的响应处理

    A. token过期: 弹框提示 + Promise.reject抛出 + 退回登陆页

    B. 文件流模式的excel导出:封装excel的下载

    C. 正常请求,但是接口返回status返回error(业务上的错误):这里全局封装,进行弹框提示+Promise.reject抛出,就不需要在每次调用的时候再处理error错误了。

详见上述代码

 

3. 业务上的错误的特殊情况

(1).如果有时候报错,我不想弹框提示怎么办?

   封装判断时候,msg为"", 不提示,这种情况接口配合,msg返回空字符串即可,这样对于每个调用页面就可以省略error情况的冗余代码了

 【这样写会把错误在浏览器中输出:Uncaught (in promise) {status: 'error', msg: '修改失败'}    无关紧要】

(2).有些特殊情况,必须在子页面中处理error的情况,

    A. 比如登录页面,那么可以起名error2,就可以进入到子页面了  【详见登录页面】

   B. 提示错误后,我还想处理其它业务,针对页面async/await的写法,需要用try-catch包裹获取【详见用户管理→编辑页面→保存方法】

   注:即使用try-catch包裹,页面控制台上依旧会输出 “Post http://xx  401 (Unauthorized)”类似的错误,无关紧要

 

 

4. 总结一下各种情况的非法校验

   [PS: 模拟场景,登录系统后,手动修改token值,改成错的,进行下面操作]

   (1). 导航菜单:

        弹框提示“非法请求,未通过校验” + 控制台输出“Uncaught (in promise) xxxxxx” + 表格空白

   (2). 表格加载(点击搜索):弹框提示“非法请求,未通过校验” + 控制台输出“Uncaught (in promise) xxxxxx”

   (3). 弹出框的保存事件:弹框提示“非法请求,未通过校验” + 控制台输出“Post http://xx  401 (Unauthorized)”

   (4). 点击弹框按钮、表格上链接:弹框提示“非法请求,未通过校验” + 控制台输出“Uncaught (in promise) xxxxxx”

   (5). 导出Excel表格:弹框提示“非法请求,未通过校验” + 控制台输出“Uncaught (in promise) xxxxxx”

   (6). F5刷新页面:

        弹框提示“非法请求,未通过校验” + 控制台输出“Post http://xx  401 (Unauthorized)” + 退回到登陆页

 

三. JWT校验

1. 普通ajax请求

(1).  新增、编辑、修改、设置类的ajax请求

   a. token缺失或token错误

     【解决方案:接口返回 new ContentResult() { StatusCode = 401};,前端在响应拦截器中的error处获取,直接弹框提示失败】

    b. token过期:

     【解决方案:接口返回 new JsonResult(new { status = "expired"});前端在响应拦截器中的response处获取,提示的同时,跳转到登陆页】            

(2). 表格类的加载, 即当请求的数据校验未过,表格如何友好展示呢?(即token错误的时候,点击左侧一个新的菜单) 【未完成!!!】

        【目前的问题是? 表格样式会错乱,因为表格显示列也需要从接口中获取哦】

(3). 如何自定义返回码? 比如status="ypfError" ?

        【解决方案:通上述A中的token过期的模式,返回 new JsonResult(new { status = "ypfError"});前端在响应拦截器中的response处获取】  

2. windows.location.href

  A. 比如Excel的下载 (现在已经替换采用流的模式!)

       a. 当token校验失败  【未彻底解决!!!】

       【临时方案: 采用这种模式new ContentResult() { StatusCode = 401, Content = "非法请求,未通过校验" }; 单独跳转到一个页面】

       b. 接口中报错,进catch

       【解决方法:直接跳转到前端error页面,需要在接口里配置前端地址哦,前端页面刷新一下,退回前一个页面】

3. 文件流模式的Excel下载【推荐】

     A. token缺失或错误

        【解决方案:过滤器中返回异常,前端axios的封装中,通过error通过FileReader进行接收,进行提示】

     B. token过期

         【同上,进行提示】

 

四. Vuex的使用

1 有哪些用途?

   A. keepAliveNames.js :tagView缓存页面相关

   B. requestOldRoutes.js:存储接口原始路由(未处理component的转换),根据需求选择使用

   C. routesList.js:菜单相关(左侧或横向)

   D. tagsViewRoutes.js:tagView对应的路由信息

   E. themeConfig.js:系统的各种样式配置信息

   F. userInfos.js:用户相关的信息(含基础信息、菜单权限、按钮权限等)

2. vuex模块注册规则?

   这里全部用的模块话,父级Vuex中并没有增加属性,只有一个modules用来注册子类module

   这里的注册规则是,子类xxx.js的名字叫什么,注册进去的名字就是什么,比如:keepAliveNames.js ,注册后的名字为keepAliveNames

3. 系统中如何调用?

(1). 前置补充创建store对象的两种方式?

     a. 直接使用createStore返回的对象store. (如:/store/index.js 中store)

     b. 先引入 import { useStore } from 'vuex';

        然后   const store = useStore()

(2).  本系统大部分采用的是:最初始的方式, 这里以userInfos模块为例

    const store = useStore();  【或者直接导入store:  import { store } from '/@/store/index';】

     a.  子模块state的调用:store.state.userInfos.userInfos

     b.  子模块getters的调用:store.getters['userInfos/xxx']

     c.  子模块mutations的调用:store.commit('userInfos/getUserInfos');

     d.  子模块actions的调用:store.dispatch('userInfos/setUserInfos');

(3). 使用自定义封装的时候,获取vuex中的写法 【目前只封装了state的获取,其它后续补充】

     const { userInfos } = useState('userInfos', ['userInfos']);

     console.log(userInfos.value.userAccount);

4. 疑问?

    A. account.vue 中的useStore是从哪里导入的?

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2022-12-13 09:38  Yaopengfei  阅读(200)  评论(2编辑  收藏  举报