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
1.组合式API:在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','参数')}
,然后父组件通过绑定事件接收:
,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)//就会打印出:ref获取dom对象
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')
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方法解构出响应式数据和方法。
1.设置别名路径联想提示:在项目根目录下建一个jsconfig.json文件,在里面添加json格式的配置项,eg:
{
"compilerOptions" : {
"baseUrl" : "./",
"paths" : {
"@/*":["src/*"]
}
}
}
这个只是路径提示,实际的路径转换是在项目的vite.config.js文件里配置的。
首先按需引入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 *;`,}
}
},
})
首先安装: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)
})
在.eslintrc.cjs里面添加:
/* eslint-env node */
module.exports = {
...
rules: {
'vue/multi-word-component-names': 0, // 不再强制要求组件命名
},
}
首先安装: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里面写二级路由出口:
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 *;
`}
}
},
})
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>
首先下载vueuse:npm 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 }">
因为上面对导航菜单进行了两次访问,所以在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">
非复杂的模版抽象成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">¥{{ item.price }}</p>
</RouterLink>
</li>
</ul>
</template>
</HomePanel>
</template>
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" />
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>
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())
缓存问题:当路由path一样,参数不同的时候会选择直接复用路由对应的组件
解决方案:
在复用的二级路由出口,添加属性
<!-- 添加key,破坏复用机制,强制销毁重建 -->
<RouterView :key="$route.fullPath"/>
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())
<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()
}
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
}
}
在src\router\index.js里面,对router添加滚轮属性
scrollBehavior(){
return{top:0}
}
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
}
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>
有些组件在多个组件中使用,可以全局注册到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)
这里一共有三级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: '/' })
}
})
}
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))
1.学习视频
2.学习文档