用vue3做了两个项目电商和后台,总结一下项目里的亮点

小城夏天电商平台线上地址:小城夏天电商平台线上地址

GitHub:GItHub

后台管里系统-权限控制

前端权限的意义
如果仅从能够修改服务器中数据库中的数据层面上讲, 确实只在后端做控制就足够了, 那为什么越来越多的项目也进行了前端权限的控制, 主要有这几方面的好处

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:1z-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/coreuseMouseInElement方法获取基于元素的偏移量
  • 计算出 遮罩容器定位与大容器北京定位 暴露出数据给模板使用

放大镜效果落地代码

// 放大镜效果
  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中的数据,然后将本地购物车和线上购物车合并,并且清除掉本地的购物车,

没登录状态下就是本地操作,登录状态下的是调用后台接口进行操作的
在这里插入图片描述
在这里插入图片描述

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超人不会飞~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值