项目总结
[TOC]
小城夏天电商平台线上地址:http://jay1124.web3v.work/#/
GitHub:https://github.com/BoLinJay/xiaochengxiaitian
后台管里系统-权限控制
前端权限的意义
如果仅从能够修改服务器中数据库中的数据层面上讲, 确实只在后端做控制就足够了, 那为什么越来越多的项目也进行了前端权限的控制, 主要有这几方面的好处1.降低非法操作的可能性
2.尽可能排除不必要清求, 减轻服务器压力
3.提高用户体验
实现步骤,方法
接口访问的权限控制,这个就是利用
axios
拦截器,判断token是否存在,访问有关页面时携带token,
菜单列表的权限控制,分为两种:
- 显示所有菜单,用户访问不在自己权限中的页面时,提醒无权限
- 只显示当前用户能访问的权限内菜单,如果用户通过URL强制访问页面则返回404
1.先创建一个不需要权限访问的路由表,比如登录页,404页面,在把需要权限的路由表创建出来,
这里的404页面要写在路由列表的最后,所有使用路由懒加载的方式创建
这里的权限路由表可以不创建,直接从后端获取,但是后期维护和添加新需求麻烦。2.获取后端传送来的路由信息,和路由表作比较,生成最总用户可以访问的路由表
3.使用
router.addRoutes
添加用户所需要的路由信息4.可以使用vuex管理路由表,进行永久存储,然后从vuex中获取路由表进行渲染
5.数据操作权限可以加载路由元数据中
meta
中 ,使用v-if/v-show,根据数据进行动态显示,也可注册一个自定义指令
接口访问的接口控制,这个就是利用axios
拦截器,判断token是否存在,访问有关页面时携带token,
// 每次请求都为http头增加Authorization字段,其内容为token
service.interceptors.request.use(
config => {
if (store.state.user.token) {
config.headers.Authorization = `token ${store.state.user.token}`;
}
return config
},
err => {
return Promise.reject(err)
}
);
2.菜单列表的权限控制,分为两种:
- 显示所有菜单,用户访问不在自己权限中的页面时,提醒无权限
- 只显示当前用户能访问的权限内菜单,如果用户通过URL强制访问页面则返回404
很显然,第一种方法不合适,那咱们梳理一下第二种方法,大致流程为:
配置自定义指令代码
//main.js
//按扭权限指令
Vue.directive('allow', {
inserted: (el, binding, vnode) => {
let permissionList = vnode.context.$route.meta.permission;
if (!permissionList.includes(binding.value)) {
el.parentNode.removeChild(el)
}
}
})
1.路由信息匹配代码
// router/index.js
/**
* 根据权限匹配路由
* @param {array} permission 权限列表(菜单列表)
* @param {array} asyncRouter 异步路由对象
*/
function routerMatch(permission, asyncRouter) {
return new Promise((resolve) => {
const routers = [];
// 创建路由
function createRouter(permission) {
// 根据路径匹配到的router对象添加到routers中即可
permission.forEach((item) => {
if (item.children && item.children.length) {
createRouter(item.children)
}
let path = item.path;
// 循环异步路由,将符合权限列表的路由加入到routers中
asyncRouter.find((s) => {
if (s.path === '') {
s.children.find((y) => {
if (y.path === path) {
y.meta.permission = item.permission;
routers.push(s);
}
})
}
if (s.path === path) {
s.meta.permission = item.permission;
routers.push(s);
}
})
})
}
createRouter(permission)
resolve([routers])
})
}
2.axios的封装代码
// 1. 创建一个新的axios实例
// 2. 请求拦截器,如果有token进行头部携带
// 3. 响应拦截器:1. 剥离无效数据 2. 处理token失效
// 4. 导出一个函数,调用当前的axsio实例发请求,返回值promise
import axios from 'axios'
import store from '@/store'
import router from '@/router'
// 导出基准地址,原因:其他地方不是通过axios发请求的地方用上基准地址
export const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/'
const instance = axios.create({
// axios 的一些配置,baseURL timeout
baseURL,
timeout: 5000
})
instance.interceptors.request.use(config => {
// 拦截业务逻辑
// 进行请求配置的修改
// 如果本地又token就在头部携带
// 1. 获取用户信息对象
const { profile } = store.state.user
// 2. 判断是否有token
if (profile.token) {
// 3. 设置token
config.headers.Authorization = `Bearer ${profile.token}`
}
return config
}, err => {
return Promise.reject(err)
})
// res => res.data 取出data数据,将来调用接口的时候直接拿到的就是后台的数据
instance.interceptors.response.use(res => res.data, err => {
// 401 状态码,进入该函数
if (err.response && err.response.status === 401) {
// 1. 清空无效用户信息
// 2. 跳转到登录页
// 3. 跳转需要传参(当前路由地址)给登录页码
store.commit('user/setUser', {})
// 当前路由地址
// 组件里头:`/user?a=10` $route.path === /user $route.fullPath === /user?a=10
// js模块中:router.currentRoute.value.fullPath 就是当前路由地址,router.currentRoute 是ref响应式数据
const fullPath = encodeURIComponent(router.currentRoute.value.fullPath)
// encodeURIComponent 转换uri编码,防止解析地址出问题
router.push('/login?redirectUrl=' + fullPath)
}
return Promise.reject(err)
})
// 请求工具函数
export default (url, method, submitData) => {
// 负责发请求:请求地址,请求方式,提交的数据
return instance({
url,
method,
// 1. 如果是get请求 需要使用params来传递submitData ?a=10&c=10
// 2. 如果不是get请求 需要使用data来传递submitData 请求体传参
// [] 设置一个动态的key, 写js表达式,js表达式的执行结果当作KEY
// method参数:get,Get,GET 转换成小写再来判断
// 在对象,['params']:submitData ===== params:submitData 这样理解
[method.toLowerCase() === 'get' ? 'params' : 'data']: submitData
})
}
小城夏天电商平台
1.vueX持久化方法
- 使用插件 vuex-persistedstate
- 存储本地 localStorage ,
2.骨架屏封装
步骤: 基础布局,props,接收参数:高度,宽度,背景色,是否开启动画
<template>
<div class="xtx-skeleton" :style="{width,height}" :class="{shan:animated}">
<!-- 1 盒子-->
<div class="block" :style="{backgroundColor:bg}"></div>
<!-- 2 闪效果 xtx-skeleton 伪元素 --->
</div>
</template>
3.轮播图封装
完成基础布局,逻辑封装有下一页,上一页,自动播放,自动播放的间隔时间
步骤:
props接收:数据信息,是否自动播放,自动播放的间隔时间
使用
ref
定义一个num类型的响应式数据,用来控制显示哪张图片,v-for
遍历数据,v-bind
绑定class
样式,判断当前图片索引和定义的数据相等,就给他加样式样式opacity:1
和z-index:
,默认样式都是不显示的,上下页按钮绑定事件,改变响应式数据的值,从而实现图片的切换。自动播放:开启一个定时器,改边这个响应式数据的值,实现自动切换,自动播放的间隔时间就是传进来的props值
4.数据懒加载
步骤:
进入可视区时才调用
API
函数获取数据,使用
@vueuse/core
中的useIntersectionObserver
的插件监听DOM元素是否进入可视区,封装一个函数,接收内观察的对象和API
函数,return数据和该DOM元素
分析useIntersectionObserver
的参数
// stop 是停止观察是否进入或移出可视区域的行为
const { stop } = useIntersectionObserver(
// target 是观察的目标dom容器,必须是dom容器,而且是vue3.0方式绑定的dom对象
target,
// isIntersecting 是否进入可视区域,true是进入 false是移出
// observerElement 被观察的dom
([{ isIntersecting }], observerElement) => {
// 在此处可根据isIntersecting来判断,然后做业务
},
)
封装的函数
// hooks 封装逻辑,提供响应式数据。
import { useIntersectionObserver } from '@vueuse/core'
import { ref } from 'vue'
// 数据懒加载函数
export const useLazyData = (apiFn) => {
// 需要
// 1. 被观察的对象
// 2. 不同的API函数
const target = ref(null)
const result = ref([])
const { stop } = useIntersectionObserver(
target,
([{ isIntersecting }], observerElement) => {
if (isIntersecting) {
stop()
// 调用API获取数据
apiFn().then(data => {
result.value = data.result
})
}
}
)
// 返回--->数据(dom,后台数据)
return { target, result }
}
5.图片懒加载
步骤
使用webAPI:
IntersectionObserver
判断图片是否进入可视区,封装了一个自定义指令,进行src
的替换,在img
上使用使用v-lazyload
值为图片地址,不设置src
属性封装自定义指令的方法vue2:vue.directive,vue3:app.directive
介绍一下IntersectionObserver
// 创建观察对象实例
const observer = new IntersectionObserver(callback[, options])
// callback 被观察dom进入可视区离开可视区都会触发
// - 两个回调参数 entries , observer
// - entries 被观察的元素信息对象的数组 [{元素信息},{}],信息中isIntersecting判断进入或离开
// - observer 就是观察实例
// options 配置参数
// - 三个配置属性 root rootMargin threshold
// - root 基于的滚动容器,默认是document
// - rootMargin 容器有没有外边距
// - threshold 交叉的比例
// 实例提供两个方法
// observe(dom) 观察哪个dom
// unobserve(dom) 停止观察那个dom
自定义指令的封装
import defaultImg from '@/assets/images/200.png'
const DirectiveImage = (app) => {
// 图片懒加载指令
app.directive('lazyload', {
mounted(el, binding) {
const observer = new IntersectionObserver(([{ isIntersecting }]) => {
if(isIntersecting) {
// 进入可视区后停止观察
observer.unobserve(el)
// 图片加载失败显示默认图片
//onerror 事件会在文档或图像加载过程中发生错误时被触发。
el.onerror = () => {
el.src = defaultImg
}
// 替换src
el.src = binding.value
}
},
{
threshold:0.01
})
// 开始观察
observer.observe(el)
}
})
}
如何自定义指令
- 定义局部自定义指令
局部自定义指令需要在组件的
directives
结构中定义,它是一个单独的结构
//vue3和vue2的组件自定义指令方法相同,只是钩子函数不同
directives:{
指令名称:{
钩子函数
}
- 自定义全局指令
//vue2
Vue.directive('directiveName', {
//钩子函数
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
})
// vue3全局指令
app.directive('directiveName', {
// 在绑定元素的 attribute 或事件监听器被应用之前调用, 在指令需要附加须要在普通的 v-on 事件监听器前调用的事件监听器时,这很有用
created() {},
// 当指令第一次绑定到元素并且在挂载父组件之前调用
beforeMount() {},
// 在绑定元素的父组件被挂载后调用
mounted() {},
// 在更新包含组件的 VNode 之前调用
beforeUpdate() {},
// 在包含组件的 VNode 及其子组件的 VNode 更新后调用
updated() {},
// 在卸载绑定元素的父组件之前调用
beforeUnmount() {},
// 当指令与元素解除绑定且父组件已卸载时, 只调用一次
unmounted() {},
});
如何自定义全局方法或属性
原理:在
Vue.prototype
上添加了一个方法
- 使用
Vue.prototype
// 在main.js中写
Vue.prototype.getData = (params) => {
...
}
- 使用install +
Vue.prototype
// 在你的全局函数文件fun.js中写
export default {
install (Vue) {
Vue.prototype.getData = () => {
return { name: 'scout'}
}
}
}
// main.js 引入
import getData from './fun'
Vue.use(getData)
如何自定义全局组件
- vue2
// 公共vue组件: components文件夹下面的Loading.vue文件:
import LoadingComponent from '@/components/Loading'
export default {
install (Vue) {
Vue.component('Loading', LoadingComponent)
}
}
// 全局组件: public文件夹下面的Loading.js文件。在main.js中引入:
import Loading from "@/public/Loading"
Vue.use(Loading)
// 在vue任何组件上都可以直接使用:<Loading />
- vue3
/* 以下两种二选一 */
const app = createApp(App);
app.use(ElementPlus)
app.use(router)
app.mount('#app')
//组件全局注册: app.component('组件名 用其调用 短横线分割命名',组件对象 name 首字母大写命名)
app.component('side-box',sideBox)
6.面包屑组件的封装
总结,一下知识点
render 是vue提供的一个渲染函数,优先级大于el,template等选项,用来提供组件结构。
注意:
- vue2.0 render函数提供h(createElement)函数用来创建节点
- vue3.0 h(createElement)函数有 vue 直接提供,需要按需导入
this.$slots.default() 获取默认插槽的node结构,按照要求拼接结构。
h函数的传参 tag 标签名|组件名称, props 标签属性|组件属性, node 子节点|多个节点
具体参考 render
注意:不要在 xtx-bread 组件插槽写注释,也会被解析。
<script>
import { h } from 'vue'
export default {
name: 'XtxBread',
render () {
// 用法
// 1. template 标签去除,单文件组件
// 2. 返回值就是组件内容
// 3. vue2.0 的h函数传参进来的,vue3.0 的h函数导入进来
// 4. h 第一个参数 标签名字 第二个参数 标签属性对象 第三个参数 子节点
// 需求
// 1. 创建xtx-bread父容器
// 2. 获取默认插槽内容
// 3. 去除xtx-bread-item组件的i标签,因该由render函数来组织
// 4. 遍历插槽中的item,得到一个动态创建的节点,最后一个item不加i标签
// 5. 把动态创建的节点渲染再xtx-bread标签中
const items = this.$slots.default()
const dymanicItems = []
items.forEach((item, i) => {
dymanicItems.push(item)
if (i < (items.length - 1)) {
dymanicItems.push(h('i', { class: 'iconfont icon-angle-right' }))
}
})
return h('div', { class: 'xtx-bread' }, dymanicItems)
}
}
</script>
7.批量注册组件
步骤:
- 使用
require
提供的函数context
加载某一个目录下的所有.vue
后缀的文件。- 然后context函数会返回一个导入函数importFn
- 它有一个属性
keys()
获取所有的文件路径- 通过文件路径数组,通过遍历数组,再使用
importFn
根据路径导入组件对象- 遍历的同时进行全局注册即可
// 批量导入需要使用一个函数 require.context(dir,deep,matching)
// 参数:1. 目录 2. 是否加载子目录 3. 加载的正则匹配
const importFn = require.context('./', false, /\.vue$/)
// console.dir(importFn.keys()) 文件名称数组
import Message from './Message'
export default {
install(app) {
// 全自动批量注册 牛逼克拉斯
importFn.keys().forEach(key => {
// 导入组件
const component = importFn(key).default
// 注册组件
app.component(component.name, component)
});
总结:
require.context(参数1,参数2,参数3) 是webpack提供的一个自动导入的API
参数1:加载的文件目录
参数2:是否加载子目录
参数3:正则,匹配文件
返回值:导入函数 fn
keys() 获取读取到的所有文件列表
#04-顶级类目-基础布局搭建
8.无限加载
无限加载其实就是根据页码显示数据的另一种表现形式
步骤
- 判断是否进入可视区,进入可视区后调用函数获取数据,每获取一组数据将页码+1,没有数据则返回FALSE,并把阻止请求
落地代码
- 封装的无限加载组件
<template>
<div class="xtx-infinite-loading" ref="container">
<div class="loading" v-if="loading">
<span class="img"></span>
<span class="text">正在加载...</span>
</div>
<div class="none" v-if="finished">
<span class="img"></span>
<span class="text">亲,没有更多了</span>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
export default {
name: 'XtxInfiniteLoading',
props: {
loading: {
type: Boolean,
default: false
},
finished: {
type: Boolean,
default: false
}
},
setup (props, { emit }) {
const container = ref(null)
useIntersectionObserver(
container,
([{ isIntersecting }], dom) => {
if (isIntersecting) {
if (props.loading === false && props.finished === false) {
emit('infinite')
}
}
},
{
threshold: 0
}
)
return { container }
}
}
</script>
- 使用
<XtxInfiniteLoading :loading="loading" :finished="finished" @infinite="getData" />
8.商品详情放大镜组件
步骤
- 首先准备大图容器和遮罩容器
- 然后使用
@vueuse/core
的useMouseInElement
方法获取基于元素的偏移量- 计算出 遮罩容器定位与大容器北京定位 暴露出数据给模板使用
放大镜效果落地代码
// 放大镜效果
const usePreviewImg = () => {
// 是否显示遮罩和大图
const show = ref(false)
const target = ref(null)
// elementX 鼠标基于容器左上角X轴偏移
// elementY 鼠标基于容器左上角Y轴偏移
// isOutside 鼠标是否在模板容器外
const { elementX, elementY, isOutside} = useMouseInElement(target)
// 遮罩的位置
const position = reactive({
left: 0,
top: 0
})
// 大图的位置
const bgPosition = reactive({
backgroundPositionX: 0,
backgroundPositionY: 0
})
watch([elementX, elementY, isOutside], () => {
// 控制X轴方向的定位 0-200 之间
if (elementX.value < 100) position.left = 0
else if (elementX.value > 300) position.left = 200
else position.left = elementX.value - 100
// 控制Y轴方向的定位 0-200 之间
if (elementY.value < 100) position.top = 0
else if (elementY.value > 300) position.top = 200
else position.top = elementY.value - 100
// 设置大背景的定位
bgPosition.backgroundPositionX = -position.left * 2 + 'px'
bgPosition.backgroundPositionY = -position.top * 2 + 'px'
// 设置遮罩容器的定位
position.left = position.left + 'px'
position.top = position.top + 'px'
// 设置是否显示预览大图
show.value = !isOutside.value
})
return { position, bgPosition, show, target }
}
9.本地购物车操作和合并线上购物车
购物车实现步骤:
当用户进行购物车操作时,下判断是否登录
未登录状态下,通过mutations修改vuex数据,这里vuex已实现数据持久化。
当用户登录后,在actions中调用后台接口,响应成功后通过mutations修改vuex中的数据,然后将本地购物车和线上购物车合并,并且清除掉本地的购物车,
没登录状态下就是本地操作,登录状态下的是调用后台接口进行操作的
10.路由导航守卫
router.beforeEach((to, from,next) => {
// ...
// 返回 false 以取消导航
return false
})
`to: 即将要进入的目标`
`from: 当前导航正要离开的路由`
`next:放行`