VUE3写电商系统(4)

1.环境

1.使用create-vue创建项目:npm init vue@latest,然后选择并按步骤运行(使用到了router,pinia,eslint)。npm run dev 运行程序。
在src下建文件目录:apis,composables,directives,styles,utils。
然后做git仓库管理:

git init
git add . 
git commit -m 'init'

给google浏览器安装一个vue调试插件:Vue.js devtools

2.知识点

1.基础

1.组合式API:<script setup>在beforCreate钩子函数之前自动执行;
2.接受对象类型数据的参数传入并返回一个响应式的对象reactive(),ref()
3.computed函数:在回调参数中return基于响应式数据做计算的值,用变量接收。

<template>
<div>原始数据{{ list }}</div>
<div>计算数据{{clist  }}</div>
</template>
<script setup>
import {ref,computed} from 'vue'
const list=ref([1,2,3,4,5,6])
const clist=computed(()=>{
  return list.value.filter(item=>item>2)
})
setTimeout(()=>{list.value.push(8,9)},3000)
</script>

4.watch函数watch(num,(newValue,oldValue)=>{})
immediate属性:在侦听器创建时立即触发回调, 响应式数据变化之后继续执行回调。
deep属性:通过watch监听的ref对象默认是浅层侦听的,直接修改嵌套的对象属性不会触发回调执行,需要开启deep。(会监听对象的所有属性)
只想监听对象的某个属性:把第一个参数写成函数的写法,返回要监听的具体属性。

const info=ref({name:'zoe',age:27})
watch(
  ()=>info.value.age,//监听info对象的age属性
  ()=>{监听到后的操作}
)

5.父传子的数据,父是模板绑定属性(动态数据用v-bind或者:),子接收的方法是:defineProps({属性名:属性类型});
6.子传父的数据,子首先通过defineEmits宏编译器生成emit方法:const emit=defineEmites(['get-message']),然后绑定事件触发emit,并传递参数:const sendMsg=()=>{emit('get-message','参数')},然后父组件通过绑定事件接收:<sonComVue @get-message="getMessage"/>,script里面拿数据:const getMessage=(msg)=>{};
7.通过ref标识获取dom对象或组件实例对象:在组件挂载完毕时执行,即onMounted ;通过defineExpose编译宏暴露组件内部的属性和方法。

<template>
<h1 ref="ref1">ref获取dom对象</h1>
<SonCom ref="ref2"></SonCom>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import SonCom from './sonCom.vue'
const ref1=ref(null);
const ref2=ref(null);
onMounted(()=>{
  console.log(ref1.value)//就会打印出:<h1>ref获取dom对象</h1>
  console.log(ref2.value)})//Proxy(Object) {name: RefImpl, __v_skip: true, setPerson: ƒ}
</script>
<template>
    <div>子组件</div>
</template>
<script setup>
import {ref,defineExpose} from 'vue';
const name=ref("zoe");//属性
const setPerson=()=>{//方法
    name.value='abc'
};
defineExpose({name,setPerson})
</script>

8.跨层组件通信:
顶层组件通过provide函数提供数据:provide('key',顶层组件数据/ref对象/方法名)
底层组件通过inject函数获取数据:const message=inject('key')

2.Pinia

Pinia 是 Vue 的专属的最新状态管理库,是 Vuex 状态管理工具的替代品。先安装:npm install pinia
然后在main.js里面导入,实例化,然后注册到app上:

import {createPinia} from 'pinia'
const pinia=createPinia()
app.use(pinia)

使用步骤包括两步,首先是在src/store下面建一个js文件,定义stores数据和方法,然后在组件中使用,eg:在src/stores/counter.js里面使用defineStore方法定义数据和方法,然后在APP.vue组件里使用这里的数据和方法:

import {defineStore} from 'pinia'
import {ref} from 'vue'
export const useCounterStore=defineStore(
    'counter',()=>{
        const count=ref(0)//数据
        const increment=()=>{count.value++}//方法
        return {count,increment}//返回这个数据和方法
    }
)
<template>
<button @click="counterStore.increment">{{ counterStore.count }}</button>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter';
const counterStore=useCounterStore()//执行方法实例化对象
</script>

使用storeToRefs方法解构出响应式数据和方法。

3.项目

1.基础

1.设置别名路径联想提示:在项目根目录下建一个jsconfig.json文件,在里面添加json格式的配置项,eg:

{
  "compilerOptions" : {
    "baseUrl" : "./",
    "paths" : {
      "@/*":["src/*"]
    }
  }
}

这个只是路径提示,实际的路径转换是在项目的vite.config.js文件里配置的。

2.Element的主题颜色定制

首先按需引入elementplus,然后进行颜色定制:先安装scss:npm i sass -D,然后在style/element/index.scss里面写自己项目的主题色:

/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      // 主色
      'base': #27ba9b,
    ),
    'success': (
      // 成功色
      'base': #1dc779,
    ),
    'warning': (
      // 警告色
      'base': #ffb302,
    ),
    'danger': (
      // 危险色
      'base': #e26237,
    ),
    'error': (
      // 错误色
      'base': #cf4444,
    ),
  )
)

最后在项目的vite.config.js里面添加配置并声明自定义的文件位置:

export default defineConfig({
  plugins: [
   ...
    Components({
      resolvers: [ElementPlusResolver({importStyle:"sass"})],
    }),
  ],
  ...
  css: {
    preprocessorOptions: {
      scss: {additionalData: `@use "@/styles/element/index.scss" as *;`,}
    }
  },
})

3.axios网络请求的配置封装

首先安装:npm install axios -S,然后在src/utils/http.js里面配置封装,主要是四部分(接口基地址,超时时间,请求拦截器和响应拦截器):

import axios from 'axios';
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css'
//1.设置接口的基地址和超时时间
const httpInstance=axios.create({baseURL:'http://pcapi-xiaotuxian-front-devtest.itheima.net',timeout:5000})
//3.axios的请求拦截器
httpInstance.interceptors.request.use(config=>{return config},e=>Promise.reject(e));
//4.axios的响应拦截器
httpInstance.interceptors.response.use(res=>res.data,e=>{
    ElMessage({
        type:'warning',
        message:e.response.data.message
    })
    return Promise.reject(e)});
//5.导出这个httpInstance
export default httpInstance;

然后在src/apis/testAPI.js里面写接口测试:

import httpInstance  from "@/utils/http";
export function getCategory(){
    return httpInstance({
        url:'home/category/head'
    })
}

最后在main.js里面调用这个接口,然后访问查看响应:

import {getCategory} from '@/apis/testAPI'
getCategory().then(res=>{
    console.log(res)
})

4.降低eslint的组件命名规则

在.eslintrc.cjs里面添加:

/* eslint-env node */
module.exports = {
	...
  rules: {
    'vue/multi-word-component-names': 0, // 不再强制要求组件命名
  },
}

5.router配置一级二级路由

首先安装:npm install vue-router -S,然后在src\router\index.js里面配置一级和二级路由,并导出router:

import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/login/index.vue'
import Layout from '@/views/layout/index.vue'
import Home from '@/views/home/index.vue'
import Category from '@/views/category/index.vue'
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      component: Layout,
      children: [
        {
          path:'',
          component:Home,
        },
        {
          path:'category',
          component:Category,
        },
      ],
    },
    {
      path: '/login',
      component: Login
    }
  ]
})
export default router

最后在App里面写一级路由出口,在Layout里面写二级路由出口: <RouterView/>

6.scss文件的自动导入

scss定义了一些样式和样式变量,在vite.config.js里面自动导入,之后直接引用:

import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
	...
  css: {
    preprocessorOptions: {
      scss: {additionalData:`
         @use "@/styles/element/index.scss" as *;
        @use "@/styles/var.scss" as *;
      @use "@/styles/common.scss" as *;
    `}
    }
  },
})

7.用axios访问后端数据渲染到前端页面

1.首先在src/apis/layout.js里面定义并导出一个函数:它利用axios封装的函数访问后端地址拿到前端列表渲染的json数据:

import httpInstance from "@/utils/http"
export function getCategoryAPI(){
    return httpInstance({
        url:'/home/category/head'
    })
}

2.在LayoutHeader.vue里面调用这个函数,拿到数据,然后将这个过程的函数挂载到onmounted上,然后进行前端渲染

<script setup>
import {getCategoryAPI} from '@/apis/layout'
import {onMounted,ref} from 'vue'
const categoryList=ref([])
const getCategory =async()=>{
    const res=await getCategoryAPI()
    categoryList.value=res.result
}
onMounted(()=>{
    getCategory()
})
</script>
<li class="home" v-for="item in categoryList" :key="item.id">
<RouterLink to="/">{{item.name}}</RouterLink>
</li>

8.用vueUse实现吸顶导航

首先下载vueusenpm i @vueuse/core,然后在LayoutFixed.vue里面导入使用它拿到滚动的y值,然后判断show

import { useScroll } from '@vueuse/core'
const { y } = useScroll(window)
<div class="app-header-sticky" :class="{ show: y > 78 }">

9.用pinia统一管理state和action

因为上面对导航菜单进行了两次访问,所以在pinia中统一管理,状态和触发函数,然后在它两的父组件中触发,再把值分发给子组件即可实现优化。
1.首先在src/stores/category.js中用pinia管理菜单列表值和访问后端拿到值的方法。

import {ref} from 'vue'
import {defineStore} from 'pinia'
import {getCategoryAPI} from '@/apis/layout'
export const useCategoryStore=defineStore('category',()=>{
    const categoryList=ref([])
    const getCategory =async()=>{
    const res=await getCategoryAPI()
    categoryList.value=res.result
    }
    return {
        categoryList,
        getCategory
    }
})

2.在Layout/index.vue里面触发函数拿到值:

import {useCategoryStore} from '@/stores/category'
import {onMounted} from 'vue'
const categoryStore=useCategoryStore()
onMounted(()=>categoryStore.getCategory())//运行函数

3.最后分别在子组件中直接使用全局的categoryList值:

import {useCategoryStore} from '@/stores/category'
const categoryStore=useCategoryStore()
<li class="home" v-for="item in categoryStore.categoryList" :key="item.id">

10.用props和slot对封装组件传参实现复用

非复杂的模版抽象成props,复杂的结构模版抽象为插槽。
1.在\src\views\home\components\HomePanel.vue里面写模板框架,并定义props和slot:

<script setup>
defineProps({
  title:{
    type:String,
    default:''
  },
  subTitle:{
    type:String,
    default:''
  }
})
</script>
<template>
  {{title}}<small>{{subTitle}}</small>
  <slot name="main"></slot>
</template>

2.通过axios访问后端拿到slot里面要插入的数据:

export function getNewAPI(){
    return httpInstance({
        url:'/home/new'
    })
}

3.最后在\src\views\home\components\HomeNew.vue里面引用框架,访问后台拿到数据,让后将props和slot数据传给模板实现渲染:

<script setup>
import HomePanel from './HomePanel.vue'
import {getNewAPI} from '@/apis/home'
import {onMounted,ref} from 'vue'
const newList=ref([])
const getNew=async()=>{
    const res=await getNewAPI()
    newList.value=res.result
}
onMounted(()=>getNew())
</script>
<template>
    <HomePanel title="新鲜好物" subTitle="新鲜出炉 品质可靠">
        <template v-slot:main>
            <ul class="goods-list">
                <li v-for="item in newList" :key="item.id">
                <RouterLink to="/">
                    <img :src="item.picture" alt="" />
                    <p class="name">{{ item.name }}</p>
                    <p class="price">&yen;{{ item.price }}</p>
                </RouterLink>
                </li>
            </ul>
        </template>
    </HomePanel>
</template>

11.用vueUse和全局指令实现图片懒加载

1.在src\directives\index.js里面定义全局指令并在指令处理函数里利用vueuse监听加载图片,然后写成插件形成:

import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = {
    install (app) {
        app.directive('img-lazy',{ // el: 指令绑定的那个元素 img;binding: binding.value  指令等于号后面绑定的表达式的值  图片url
            mounted(el,binding){
                const {stop}= useIntersectionObserver(
                    el,
                    ([{isIntersecting}])=>{
                        if(isIntersecting){
                            el.src=binding.value;
                            stop();
                        }
                    }
                )   
            }
        })
    }
}

2.在main.js里面把自定义的插件注册到app上:

import { lazyPlugin } from '@/directives'
app.use(lazyPlugin)

3.最后在组件里面使用插件,懒加载图片:

<img v-img-lazy="item.picture" :alt="item.alt" />

12.获取路由上的参数

1.面包屑上用useRoute获取路由参数

1.首先在apis/category.js进行路由访问:

import httpInstance from "@/utils/http"
export function getCategoryAPI(id){
    return httpInstance({
        url:'/category',
        params:{
            id
        }
    })
}

2.然后利用useRoute拿到路由参数并访问后端进行渲染:

<script setup>
import {getCategoryAPI} from  '@/apis/category'
import {useRoute} from 'vue-router'
import {onMounted,ref} from 'vue'
const categoryData=ref({})
const route=useRoute()
const getCategory=async()=>{
    const res=await getCategoryAPI(route.params.id)
    categoryData.value = res.result
}
onMounted(()=>getCategory())
</script>
<el-breadcrumb-item>{{categoryData.name}}</el-breadcrumb-item>

2.轮播图定义不同的参数

1.首页的轮播图和category的轮播图,用参数来做分别

export function getBannerAPI(params = {}){
    const { distributionSite = '1' } = params//默认为1,首页轮播图, 当为2时,category
    return httpInstance({
        url:'/home/banner',
        params: {
            distributionSite
          }
    })
}

2.category里面给参数设为2,然后访问后台拿数据

const bannerList = ref([])

const getBanner = async () => {
  const res = await getBannerAPI({
    distributionSite: '2'
  })
  console.log(res)
  bannerList.value = res.result
}

onMounted(() => getBanner())

13.路由缓存

缓存问题:当路由path一样,参数不同的时候会选择直接复用路由对应的组件
解决方案:

1. 给 routerv-view 添加key属性,破坏缓存

在复用的二级路由出口,添加属性

<!-- 添加key,破坏复用机制,强制销毁重建 -->
<RouterView :key="$route.fullPath"/>

2. 使用 onBeforeRouteUpdate钩子函数,做精确更新

import {onBeforeRouteUpdate, useRoute} from 'vue-router'
// 路有变化时,只把category接口的数据重新发送
onBeforeRouteUpdate((to)=>{
    getCategory(to.params.id)
})
const categoryData=ref({})
const route=useRoute()
const getCategory=async(id=route.params.id)=>{
    const res=await getCategoryAPI(id)
    categoryData.value = res.result
}
onMounted(()=>getCategory())

14.排序方式按钮的双向绑定

<el-tabs v-model="reqData.sortField" @tab-change="tabChange">
        <el-tab-pane label="最新商品" name="publishTime"></el-tab-pane>
        <el-tab-pane label="最高人气" name="orderNum"></el-tab-pane>
        <el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane>
</el-tabs>
//绑定之后,点击哪个el-tab-pane,就会把name赋值给请求参数reqData的sortField,然后再用这个新的请求参数请求一次后端
const tabChange=()=>{
    getGoodList()
}

15.列表无限滚动加载并定制路由滚动行为

1.列表无限滚动加载

1.使用infinite-scroll-disabled属性监听滚轮滚动

<div class="body" v-infinite-scroll="load" :infinite-scroll-disabled="disabled">
         <GoodsItem v-for="good in goodList" :good="good" :key="good.id"/>
 </div>

2.滚轮滚下来时就再访问下一页数据,让后将新老数据拼接进行前端渲染

const disabled = ref(false)
const load = async () => {
  reqData.value.page++
  const res = await getSubGoodsAPI(reqData.value)
  goodList.value = [...goodList.value, ...res.result.items]//新老数据拼接
  // 加载完毕 停止监听
  if (res.result.items.length === 0) {
    disabled.value = true
  }
}

2.跳转路由时滚轮回顶

在src\router\index.js里面,对router添加滚轮属性

scrollBehavior(){
    return{top:0}
  }

16.小图切换大图

1.首先在小图列表里监听鼠标移动进来,并把小图的i传递进处理函数

<li v-for="(img, i) in imageList" :key="i" @mouseenter="enterhandler(i)" :class="{active:i===activeIndex}">

2.在函数处理中拿到i,并在大图显示中使用

const activeIndex=ref(0)
const enterhandler=(i)=>{
    activeIndex.value=i
}

17.放大镜

1。首先是蒙层滑块对大图的box的鼠标位置监听,然后是跟随鼠标移动的有效和边界位置坐标。

//  控制滑块跟随鼠标移动(监听elementX/Y变化,一旦变化 重新设置left/top)
const left = ref(0)
const top = ref(0)
// 获取鼠标相对位置
const target = ref(null)
const { elementX, elementY, isOutside } = useMouseInElement(target)
watch([elementX, elementY, isOutside], () => {
  console.log('xy变化了')
  // 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑
  if (isOutside.value) return
  console.log('后续逻辑执行了')
  // 有效范围内控制滑块距离
  // 横向
  if (elementX.value > 100 && elementX.value < 300) {
    left.value = elementX.value - 100
  }
  // 纵向
  if (elementY.value > 100 && elementY.value < 300) {
    top.value = elementY.value - 100
  }

  // 处理边界
  if (elementX.value > 300) { left.value = 200 }
  if (elementX.value < 100) { left.value = 0 }

  if (elementY.value > 300) { top.value = 200 }
  if (elementY.value < 100) { top.value = 0 }
})
<div class="layer" v-show="!isOutside" :style="{ left: `${left}px`, top: `${top}px` }"></div>

2.然后通过判断显示放大图像


const positionX = ref(0)
const positionY = ref(0)
watch([elementX, elementY, isOutside], () => {
  console.log('xy变化了')
  // 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑
  if (isOutside.value) return
  // 控制大图的显示
  positionX.value = -left.value * 2
  positionY.value = -top.value * 2
})
<div class="large" :style="[
      {
        backgroundImage: `url(${imageList[activeIndex]})`,
        backgroundPositionX: `${positionX}px`,
        backgroundPositionY: `${positionY}px`,
      },
    ]" v-show="!isOutside"></div>

18.组件的全局注册使用

有些组件在多个组件中使用,可以全局注册到app上使用,就不用每次都导入到需要使用的父组件中。
现在在src/components目录下新建一个index.js,在里面吧这个目录下的所有组件都全局注册注册一下,然后在main.js里面全局引入,之后就可以在所有组件中直接使用而不用再导入。

import ImageView from './ImageView/index.vue'
import Sku from './XtxSku/index.vue'
export const componentPlugin = {
  install (app) {
    // app.component('组件名字',组件配置对象)
    app.component('XtxImageView', ImageView)
    app.component('XtxSku', Sku)
  }
}
import { componentPlugin } from '@/components'
app.use(componentPlugin)

19.表单验证

这里一共有三级el-form(表单和规则对象),el-form-item(字段名),el-form-input(内容)。
1.准备表单对象并绑定到el-form

const userInfo = ref({
  account: '',
  password: '',
  agree: true
})
<el-form ref="formRef" :model="userInfo" status-icon>

2.准备验证的规则对象并绑定到el-form

const rules = {
  account: [
    { required: true, message: '用户名不能为空' }
  ],
  password: [
    { required: true, message: '密码不能为空' },
    { min: 6, max: 24, message: '密码长度要求6-14个字符' }
  ],
  agree: [
    {
      validator: (rule, val, callback) => {
        return val ? callback() : new Error('请先同意协议')
      }
    }
  ]
}
<el-form ref="formRef" :model="userInfo" :rules="rules" status-icon>

3.通过prop指定表单域的校验字段名绑定到el-form-item

<el-form-item prop="account" label="账户">
<el-form-item prop="password" label="密码">
<el-form-item prop="agree" label-width="22px">

4.通过v-model把表单对象进行双向绑定到el-form-input

<el-input v-model="userInfo.account" />
<el-input v-model="userInfo.password" />
<el-checkbox v-model="userInfo.agree" size="large">
   我已同意隐私条款和服务条款
</el-checkbox>

5.最后在点击登录按钮时,对所有需要校验的表单进行统一校验

import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css'
import {loginAPI} from '@/apis/user'
import {useRouter} from 'vue-router'
const formRef=ref(null)
const router =useRouter()
const doLogin = () => {
  const { account, password } = userInfo.value
  // 调用实例方法
  formRef.value.validate(async (valid) => {
    // valid: 所有表单都通过校验  才为true
    console.log(valid)
    // 以valid做为判断条件 如果通过校验才执行登录逻辑
    if (valid) {
      // TODO LOGIN
      await loginAPI({ account, password })
      // 1. 提示用户
      ElMessage({ type: 'success', message: '登录成功' })
      // 2. 跳转首页
      router.replace({ path: '/' })
    }
  })
}

20.使用pinia统一管理用户相关数据和方法

1.在stores/user.js里面统一管理,然后在login的index.vue里面实例化方法,然后调用方法进行登录

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { loginAPI } from '@/apis/user'
export const useUserStore = defineStore('user', () => {
  const userInfo = ref({})
  const getUserInfo = async ({ account, password }) => {
    const res = await loginAPI({ account, password })
    userInfo.value = res.result
  }
  return {
    userInfo,
    getUserInfo
  }
})
import {useUserStore} from '@/stores/user'
const UserStore = useUserStore()
UserStore.getUserInfo ({ account, password })

2.使用pinia-plugin-persistedstate对用户登录后的token信息进行数据持久化。
首先是下载并注册到pinia

pip install pinia-plugin-persistedstate
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

然后在需要持久化信息的store里面添加persist为true的属性即可。
因为每次访问都需要验证token,所以在拦截器里面按照后端要求验证token

httpInstance.interceptors.request.use(config => {
    // 1. 从pinia获取token数据
    const userStore = useUserStore()
    // 2. 按照后端的要求拼接token数据
    const token = userStore.userInfo.token
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  }, e => Promise.reject(e))

4.资料

1.学习视频
2.学习文档

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我是小z呀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值