尚品汇项目总结

  1. 利用JWT实现了登陆注册功能,登录成功后返回两个标识tokenrefresh tokentoken是登录凭证,有效使其非常短。refresh token是刷新凭证时间比token长,当token过期时请求新的token。保存在localstorage和vuex里面。退出登录的时候清除。
  2. 封装了axios,在请求拦截器里向请求头添加token,响应拦截器里做token过期的处理
  3. 利用全局路由守卫beforeEach做登录鉴权操作,利用token判断该路径是否可以跳转。

页面展示主要实现的是性能优化

  1. 一级菜单绑定鼠标事件监听,当鼠标频繁进入时,事件回调被频繁执行。当用户操作很快时,移入的一级分类都应该触发鼠标进入事件,但是经过测试,只有部分的一级分类被触发了。原因是用户行为过快,导致浏览器没有反应过来。如果当前回调中有大量业务,有可能出现浏览器卡顿现象。所以采用节流防抖、事件委托进行性能优化
  2. 图片展示页面利用图片懒加载技术,当图片快进入视口时才请求图片资源。

做了什么事情

登录与注册 JWT(Json Web Token)

如何保证安全?加密+短时间刷新token
在传输中,不允许明文传输用户隐私数据
在本地,不允许明文保存用于隐私数据
在服务器,不允许明文保存用户隐私数据

  • 服务器-注册接口:接收客户端传来的账号和密码,将其保存在数据库中;
  • 服务器-登录接口:接收客户端传来的账号和密码,与数据库比对,完全命中则登录成功,否则登录失败;
    • 登录成功后,生成或更新 token 和过期时间,保存在数据库, token 返回给客户端;
    • 服务器定期清除 token
  • 客户端-注册模块:向服务器注册接口发送账号和密码;
  • 客户端-登录模块:向服务器登录接口发送账号和密码;
    • 登录成功后,保存 token 到本地;
    • 退出登录后,清除 token;

如何防止token泄露?
类似数字签名机制,利用时间戳和token加密算法 = token签名?

token在前台表示已登陆,在后台表示有权限访问接口。
如果有更需要保护的可以将加入其他验证,如手机号、邮箱等。

token主动刷新
如果token过期,主动刷新token,利用与token相关联的refresh token,refresh token作用是获取新的token,过期时间比token的时间长。

将token过期的处理放在响应拦截器中,当返回的响应码为401时,说明token过期了,需要利用refresh token重新获取新的token。重新获取token之后,重发请求。

  • 新token请求成功
    • 更新本地token
    • 再发一次请求A
  • 新token请求失败
    • 清空vuex中的token
    • 携带请求地址,跳转到登陆页

Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Token 时才会验证

实现介绍
登录时后端为了区分用户,会返回给前端token,token是用户的唯一标识符。

  1. 浏览器登录后,服务器利用用户信息,经过加密后生成Token字符串(Token中包含了用户信息),将生成的Token字符串返回给客户端
  2. 客户端通过代码将Token存储到LocalStorage或SessionStorage

vuex存储数据不是持久化,刷新之后数据会消失

  1. 客户端再次发送请求时,通过请求头的Authorization字段(手动设置),将Token发送给服务器
    每一次发请求时,将token放在请求头中一起发送,在请求拦截器中实现。
//请求拦截器
requestAxios.interceptors.request.use((config)=>{
    if(store.state.detail.uuid_token) {
        config.headers.userTempId = store.state.detail.uuid_token;
    }
    let token = localStorage.getItem('TOKEN');
    if(token){
       config.headers['Authorization'] = 'Bearer ' + token; 
    }
    return config;
})

对axios进行二次封装

封装的目的

  • 请求拦截器:设置发送请求前的统一操作
  • 响应拦截器:请求响应后进行统一操作
  • 对不同的需求(请求前缀)创建不同的axios请求实例
    • api请求(从后台获取数据),’/api‘开头的前缀
    • mock请求(mockjs模拟的数据),/mock开头的前缀

进行了哪些封装?
请求拦截器

  • 请求头中添加token给服务器
  • 设置请求超时时间
    响应拦截器
  • 请求成功直接获取res.data,请求失败终止promise链
  • 无权限处理,主动刷新token

使用mockjs模拟数据

mockjs 生成随机数据,当前端使用mock模拟的数据接口时,mockjs进行数据返回,并拦截ajax请求不发送给后台。

封装一个mock请求的axios

const mockRequests = axios.create({
    baseURL:"/mock",
    timeout:5000, //请求超时的时间5s
})

//请求拦截器
mockRequests.interceptors.request.use((config)=>{
    //config:配置对象,对象里面有一个属性很重要,header请求头
    nprogress.start();//进度条开始
    return config;
})

//响应拦截器
//参数1成功的回调,参数2失败的回调
mockRequests.interceptors.response.use((res)=>{
    nprogress.done();//进度条结束
   return res.data;//返回服务器返回的数据

},(error)=>{
    return Promise.reject(new Error('fail')) //终止promise链
})

export default mockRequests;

//采用mock发送请求
import mockRequests from "./request";
//获取Home首页轮播图banner的结构
export const getBannerList = () => mockRequests.get('/banner');

分页器 封装通用组件

分页器需要哪些数据?
1.当前是第几页 pageNo
2.每一页需要展示多少数据 pagesize
3.分页器一共有多少条数据 total
4.分页器显示的连续页码个数:5 | 7
对于分页器,很重要的点是计算出连续显示页面号起始数字和结束数字。 当前页在连续页的正中间

  • 计算总共多少页:Math.ceil(total/pagesize)
  • 计算出连续的页码的起始数字与结束数字
 computed: {
    //总共多少页
    totalPage() {
      return Math.ceil(this.total / this.pageSize);
    },
    //计算出连续的页码的起始数字与结束数字[连续页码的数字:至少是5]
    startNumAndEndNum() {
      const { continues, pageNo, totalPage } = this;
      //先定义两个变量存储起始数字与结束数字
      let start = 0,
        end = 0;
      //不正常现象【总页数没有连续页码多】
      if (continues > totalPage) {
        start = 1;
        end = totalPage;
      } else {
        //正常现象【连续页码5,但是你的总页数一定是大于5的】
        start = pageNo - parseInt(continues / 2);
        end = pageNo + parseInt(continues / 2);
        //把出现不正常的现象【start数字出现0|负数】纠正
        if (start < 1) {
          start = 1;
          end = continues;
        }
        //把出现不正常的现象[end数字大于总页码]纠正
        if (end > totalPage) {
          end = totalPage;
          start = totalPage - continues + 1;
        }
      }
      return { start, end };
    },
  }

性能优化

事件委托

将多个子元素的同类事件监听委托给(绑定在)共同的一个父组件上。
好处
①减少内存占用(事件监听的回调变少)
②动态添加的内部元素也能响应

节流和防抖 --遇见的问题也可以回答:卡顿现象

问题描述
给一级菜单绑定了鼠标的事件监听,当鼠标频繁进入时,事件回调被频繁执行。当用户操作很快时,移入的一级分类都应该触发鼠标进入事件,但是经过测试,只有部分的一级分类被触发了。原因是用户行为过快,导致浏览器没有反应过来。如果当前回调中有大量业务,有可能出现浏览器卡顿现象。

问题解决

  • 节流(throttle):控制事件执行的时间间隔,在函数需要频繁触发时: 函数执行一次后,只有大于设定的执行周期后才会执行第二次
    适合多次事件按时间做平均分配触发:窗口调整(resize)+ 页面滚动(scroll)等
  • 防抖(debounce):在函数需要频繁触发时: 在规定时间内,只让最后一次生效,前面的不生效。
    适合多次事件一次响应的情况:输入框实时搜索联想(keyup/input)
节流函数实现

节流(throttle)函数
控制的是给事件绑定的回调函数执行的频率,那么该函数的返回值应该是一个函数。
参数有两个1.获取到的回调函数 2. 设置的时间间隔

注意点
1.返回函数使用了闭包,闭包会永远在内存中保存所以这个pre都是记录的上一次的结果
2.修改this的目的是让函数的指向绑定事件的DOM

//使用形式,绑定时候throttle函数就会执行,所以this是window
window.addEventListener('scroll',throttle(()=>{},500))

//自定义
function throttle(callback,wait){
	let pre=0;
	//console.log(this);window
	//节流函数/真正的事件回调函数
	return function(...args){
		const now = Date.now();
		if(now-pre>wait){
			//callback()是window调用的,所以callback函数里的this是window,这里要修改指向事件源,
			//console.log('this2',this); //DOM
			callback.apply(this,args);
			pre = now;
		}
	}
}
防抖函数实现

防抖(debounce)函数
控制的是给事件绑定的回调函数执行的频率,那么该函数的返回值应该是一个函数。
参数有两个1.获取到的回调函数 2. 设置的规定时间

思路
1.返回一个函数,在函数中设置定时器,在定时器中执行回调函数,注意this指向的改变
2.当频繁点击的时候,如果此时已经开启定时器了说明之前触发了回调,我们需要删除定时器
3. 注意定时器的timeId不能在返回函数中定义,如果在返回函数中定义,那么每次触发回调的时候,都会重新定义。而我们的需求是对于当前触发的回调,timeId需要记录之前的结果,通过timeId来判断之前是不是已经开启了定时器。所以这里需要利用闭包实现。

//使用形式:绑定时debounce立即执行
window.addEventListener('scroll',debounce(()=>{},500)); 
//防抖函数
function debounce(callback, wait){
 	let timeId=null;
 	return funtion(...args){
		if(timeId){//之前已经有一个定时器了,这里再一次触发事件,重新开始即使
			clearTimeout(timer)}
		timeId = setTimeout(()=>{
			callback.apply(this,args)//执行成功之后,重置timeId,所以这里可以起作用
            timeId = null;
		},wait)
				
	}
}

导航条 只发送一次请求

当组件之间进行切换时,会销毁旧组件,创建新组件。所以组件中的子组件导航也会重新创建实例、重新挂载,重新发送数据请求。

如何优化
对于导航组件来说,一般都是不变的,所以我们希望只发送一次数据请求。所以可以把数据请求放在根组件,根组件只会实例化一次

图片懒加载和路由懒加载

图片懒加载:https://blog.csdn.net/qq_41370833/article/details/125284975 重点
路由懒加载:https://blog.csdn.net/qq_41370833/article/details/125299151

遇见的问题

1.编程式路由跳转的NavigationDuplicated警告报错 --重写push和replace方法

  • 声明式导航router-link(需要有to属性),可以进行路由的跳转
  • 编程式导航利用组件实例的$router.push/replace,可以进行路由跳转

问题描述
编程式路由重复点击(参数不变),多次执行会抛出NavigationDuplicated警告报错

问题分析,为什么会报错?
Vue router3.1之后,$router.push()返回Promise,返回的promise没有设置失败的回调,没有对错误进行处理

解决办法
1.对每个router.push()进行错误捕获

router.push('xxxx').catch(err => {err})

push方法还可以传入成功和失败的回调

this.$router.push({
		name:'search',//路由记得命名
		params:{keyword:this.keyword},
		query:{keyword:this.keyword.toUpperCase()}
},()=>{},(err)=>{if(如果是NavigationDuplicated错误)console.log(err)})

2.重写push()方法
①先保存VueRouter原型上的push方法
②重写push|repalce

 import VueRouter from 'vue-router' //引入插件
 Vue.use(VueRouter) //使用插件
let origiPush = VueRouter.prototype.push
VueRouter.prototype.push = function(location,onResolved=()=>{},onRejected=(err )=>err){
return origiPush.call(this,location,onResolved,onRejected)
}

2.利用swiper插件实现轮播图时,不生效 --引出$nextTick原理的知识点

原因
在new Swiper实例之前,页面中的结构必须存在
刚开始将数据请求放在mounted里,但是ajax请求是异步的,数据是动态获取的,new Swiper时可能数据还没有获取到,或者说页面还没有根据数据重新渲染,结构还不完整。

 mounted() {
      //派发action,通过vuex发起ajax请求
      this.$store.dispatch('home/reqBannerList');
      new Swiper(document.querySelector('.swiper-container'),{
        loop:true,
        pagination:{
          el:".swiper-pagination"
        },
        navigator:{
          nextEl:".swiper-button-next",
          prevEl:".swiper-button-prev"
        }
      })
   }

解决办法
watch: 数据监听,监听已有数据变化。 此时只能保证数据已经获取到了,不能保证v-fordom渲染完毕了
$nextTick:将回调延迟到下次DOM更新之后执行
本质:将回调添加到任务队列中延迟执行

更新DOM的回调vm.$nextTick注册的回调,都是添加到微队列中。所以DOM会先更新完毕,然后再执行$nextTick的回调

$nextTick原理:https://blog.csdn.net/qq_41370833/article/details/124830714

滚动条保持原有位置

问题描述
当从页面跳转到新路由时,滚动条保持原有位置

原因
路由切换时没有重新刷新页面

解决办法
使用前端路由,当切换到新路由时,想要页面滚动到顶部或者保持原先的滚动位置,vue-router可以实现,只支持在history.pushState的浏览器使用。 —引出H5接口的新方法pushStatereplaceState,或者hash模式和history模式的区别

//配置路由
export default new VueRouter({
    routes,
    scrollBehavior (to, from, savedPosition) {
        return {x:0,y:0} //每次路由切换时的滚动条位置
    }
})
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐