1. src目录调整
2. git管理项目
3. 别名路径联想提示
在编写代码的过程中,一旦 输入 @/,VSCode会立刻 联想出src下的所有子目录和文件,统一文件路径访问不容易出错
根目录下新增 jsconfig.json——只做联想提示
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
vite.config.js ——实际的路径转换 @ -> src
4. 添加ElementPlus到项目
- 安装
npm install element-plus --save
- 按需导入
npm install -D unplugin-vue-components unplugin-auto-import
- 使用
vite:
// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
5. 定制主题
- 安装scss
npm i sass -D
- 准备定制样式文件——styles/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,
),
)
);
3.对ElementPlus样式进行覆盖——通知Element采用scss语言 → 导入定制scss文件覆盖
vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// element-plus 按需导入
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
// 1. 通知Element采用scss语言
resolvers: [ElementPlusResolver({importStyle:"sass"})],
}),
],
resolve: {
alias: {
// 实际的路径转换 @ -> src
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
// 2. 导入定制scss文件覆盖
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "@/styles/element/index.scss" as *;
`,
}
}
}
})
6. axios基础配置
- 安装axios
npm i axios
- 配置基础实例(统一接口配置)
1. 接口基地址
2. 接口超时时间
3. 请求拦截器
4. 响应拦截器
utils/https.js
// axios 基础封装
import axios from "axios";
const httpInstance = axios.create({
// 接口基地址
baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
// 接口超时时间
timeout: 10000,
})
// 拦截器
// axios 请求拦截器
httpInstance.interceptors.request.use(config => {
// 在发送请求之前做些什么
return config;
}, e => Promise.reject(e)); // 对请求错误做些什么
// axios 响应拦截器
httpInstance.interceptors.response.use(res => res.data, e => {
return Promise.reject(e)
})
export default httpInstance
apis/testAPI.js
import httpInstance from "@/utils/http.js";
export function getCategory(){
return httpInstance({
url: "home/category/head"
})
}
main.js
// 测试接口函数
import { getCategory } from '@/apis/testAPI.js'
getCategory().then(res => {
console.log(res)
})
7. 项目路由设计
1、设计首页和登录页的路由(一级路由)
路由设计原则:找内容切换的区域,如果是页面整体切换,则为一级路由
路径 path:#/ → 路径 path: #/login
(1)创建两个组件 Layout/index.vue 和 Login/index.vue
(2)配置路由 router/index.js
// createRouter:创建路由实例
// createWebHistory:创建history模式路由
import {createRouter, createWebHistory} from 'vue-router'
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// path 和 component 对应关系的位置
routes: [
{
path: '/',
component: Layout,
},
{
path: '/login',
component: Login
},
]
})
export default router
2、设计分类页和默认Home页路由(二级路由)
路由设计原则: 找内容切换的区域,如果是在一级路由页的内部切换,则为二级路由
路径 path:#/ → 路径 path: #/category
(1)创建两个组件 Home/index.vue 和 Category/index.vue
(2)配置路由 router/index.js
routes: [
{
path: '/',
component: Layout,
children: [
{
// 默认 置空
path: '',
component: Home
},
{
path: 'category/:id',
component: Category
}
]
},
{
path: '/login',
component: Login
},
],
(3)配置路由出口
views/Layout/index.vue
<template>
<div>
首页
<!-- 二级路由出口-->
<RouterView />
</div>
</template>
.eslintrc.cjs
rules: {
'vue/multi-word-component-names': 0, // 不再强制要求组件命名
},
8. 图片资源和样式资源
资源说明
- 实际工作中的图片资源通常由 UI设计师 提供,常见的图片格式有png,svg等都是由UI切图交给前端
- 样式资源通常是指项目初始化的时候进行样式重置,常见的比如开源的 normalize.css 或者手写
资源操作
- 图片资源-把 images 文件夹放到 assets 目录下
- 样式资源-把 common.scss 文件放到 styles 目录下
main.js
// 引入css
import '@/styles/common.scss'
9. scss文件自动导入
为什么要自动导入
在项目里一些组件共享的色值会以scss变量的方式统一放到一个名为var.scss 的文件中,正常组件中使用,需要先导入scss交件,再使用内部的变量,比较繁琐,自动导入可以免去手动导入的步骤,直接使用内部的变量。
自动导入配置
- 新增一个 styles/var.scss 文件,存入色值变量
$xtxColor:#27ba9b;
- 通过 vite.config.js 配置自动导入文件
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "@/styles/element/index.scss" as *;
@use "@/styles/var.scss" as *;
`,
}
}
}
10. iconfont引入
font-class 引用
第一步:拷贝项目下面生成的fontclass代码:
//at.alicdn.com/t/font_8d5l8fzk5b87iudi.css
第二步:挑选相应图标并获取类名,应用于页面:
<i class="iconfont icon-xxx"></i> 在这里插入代码片
11. 吸顶交互
要求:浏览器在上下滚动的过程中,如果距离顶部的滚动距离大于78px,吸顶导航显示,小于78px隐藏
准备吸顶导航组件 → 获取滚动距离 → 以滚动距离做判断条件控制组件盒子展示隐藏
// vueUse 反应式滚动位置和状态
import {useScroll} from '@vueuse/core'
const {y} = useScroll(window
<div :class="{ show: y> 78 }" class="app-header-sticky">
...
</div>
.app-header-sticky {
...
// 此处为关键样式!!!
// 状态一:往上平移自身高度 + 完全透明
transform: translateY(-100%);
opacity: 0;
// 状态二:移除平移 + 完全不透明
&.show {
transition: all 0.3s linear;
transform: none;
opacity: 1;
}
}
12. Pinia优化重复请求
俩个导航中的列表是完全一致的,但是要发送俩次网络请求,存在浪费。通过Pinia集中管理数据,再把数据给组件使用
index.vue
// 触发获取导航列表的action
import {useCategoryStore} from "@/stores/category.js";
import {onMounted} from "vue";
const categroyStore = useCategoryStore()
onMounted(() => categroyStore.getCategory())
LayoutHeader.vue
import {useCategoryStore} from "@/stores/category.js";
// 使用pinia中的数据
const categoryStore = useCategoryStore()
13. 组件封装
核心思路:把可复用的结构只写一次,把可能发生变化的部分抽象成组件参数(props/插槽)
实现步骤:
- 不做任何抽象,准备静态模版
- 抽象可变的部分
(1)主标题和副标题是纯文本,可以抽象成prop传入
//定义props
defineProps({
// 主标题
title: {
type: String
},
// 副标题
subTitle: {
type: String
}
})
<!-- 主标题和副标题 -->
<h3>
{{ title }}<small>{{ subTitle }}</small>
</h3>
(2)主体内容是复杂的模版,抽象成插槽传入
<!-- 主体内容区域 -->
<div>
<slot/>
</div>
<HomePanel sub-title="新鲜好物 好多商品" title="新鲜好物">
<div>新鲜好物的插槽</div>
</HomePanel>
14. 图片懒加载指令
场景和指令用法
场景:电商网站的首页通常会很长,用户不一定能访问到页面靠下面的图片,这类图片通过懒加载优化手段可以做到只有进入视口区域才发送图片请求
指令用法:在图片img身上绑定指令,该图片只有在正式进入到视口区域时才会发送图片网络请求
熟悉指令语法 --> 判断图片是否进入视口(vueUse) --> 测试图片监控是否生效 --> 测试图片资源是否发出 --> 如果图片进入视口,发送图片资源请求(img.src=url)
重复监听问题
useIntersectionObserver对于元素的监听是一直存在的,除非手动停止监听,存在内存浪费
解决思路:在监听的图片第一次完成加载之后就停止监听stop()
directives/index.js
// 定义懒加载插件
import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = {
install (app) {
// 懒加载指令逻辑
app.directive('img-lazy', {
mounted (el, binding) {
// el: 指令绑定的那个元素 img
// binding: binding.value 指令等于号后面绑定的表达式的值 图片url
// console.log(el, binding.value)
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
console.log(isIntersecting)
if (isIntersecting) {
// 进入视口区域
el.src = binding.value
stop()
}
},
)
}
})
}
}
main.js
// 引入懒加载指令插件并且注册
import { lazyPlugin } from '@/directives'
app.use(lazyPlugin)
HomeHot.vue使用
<img v-img-lazy="item.picture" alt="">
15. 响应路由参数变化
使用带有参数的路由时需要注意的是,当用户从/users/johnny导航到 /users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。
问题:一级分类的切换正好满足上面的条件,组件实例复用,导致分类数据无法更新
解决问题的思路:
- 让组件实例不复用,强制销毁重建
- 监听路由变化,变化之后执行数据更新操作
方案一:给 router-view 添加 key
以当前路由完整路径为key的值,给router-view组件绑定
最常见的用例是与 v-for结合
也可以用于强制替换一个元素/组件而不是复用它。当你想这么做时它可能会很有用:
在适当的时候触发组件的生命周期钩子
触发过渡
<RouterView :key="$route.fullPath"/>
问题:banner 轮播图重复请求
方案二:使用 beforeRouteUpdate 导航钩子
beforeRouteUpdate 钩子函数可以在每次路由更新之前执行,在回调中执行需要数据更新的业务逻辑即可
或者,使用 beforeRouteUpdate 导航守卫,它也可以取消导航
import {onBeforeRouteUpdate} from "vue-router";
// 获取数据
const categoryData = ref({})
const route = useRoute()
const getCategory = async (id = route.params.id) => {
const res = await getCategoryAPI(id)
categoryData.value = res.result
}
onMounted(() => getCategory())
// 目标:路由参数变化的时候 可以把分类数据接口重新发送
onBeforeRouteUpdate((to) => {
// 存在问题:使用最新的路由参数请求最新的最新的分类数据
// to 目标路由对象
getCategory(to.params.id)
})
16. 封装业务代码
17. 列表无限加载功能实现
核心实现逻辑:使用elementPlus提供的 v-infinite-scroll指令监听是否满足触底条件,满足加载条件时让页数参数加一获取下一页数据,做新老数据拼接渲染
<div v-infinite-scroll="load" :infinite-scroll-disabled="disabled" class="body">
<!-- 商品列表-->
<GoodsItem v-for="goods in goodList" :key="goods.id" :goods="goods"/>
</div>
// 加载更多
const disabled = ref(false)
const load = async () => {
// 获取下一页数据
reqData.value.page++
// 获取新数据
const res = await getSubCategoryAPI(reqData.value)
// 新老数据拼接
goodList.value = [...goodList.value, ...res.result.items]
// 加载完毕 停止监听
if (res.result.items.length === 0) {
disabled.value = true
}
}
18. 定制路由行为
在不同路由切换的时候,可以自动滚动到页面的顶部,而不是停留在原先的位置
如何配置:vue-router支持 scrollBehavior配置项,可以指定路由切换时的滚动位置
// 路由滚动行为定制
scrollBehavior() {
return {
left: 0,
top: 0
}
}
19. 渲染模版时遇到对象的多层属性访问可能出现什么问题?
<!--1. 可选链的语法?.-->
<el-breadcrumb-item :to="{ path: `/category/${goods.categories?.[1].id}` }">{{ goods.categories?.[1].name }}
</el-breadcrumb-item>
<!--2. v-if 手动控制渲染时机 保证只有数据存在才渲染-->
<div v-if="goods.details" class="container">
20. 通过小图切换大图实现
思路:维护一个数组图片列表,鼠标划入小图记录当前小图下标值,通过下标值在数组中取对应图片,显示到大图位置
主页使用
<ImageView/>
组件内
// 1. 小图切换大图显示
const activeIndex = ref(0)
const enterhandler = (i) => {
activeIndex.value = i
}
<!-- 左侧大图-->
<div ref="target" class="middle">
<img :src="imageList[activeIndex]" alt=""/>
<!-- 蒙层小滑块 -->
<div :style="{ left: `0px`, top: `0px` }" class="layer"></div>
</div>
<!-- 小图列表 -->
<ul class="small">
<li v-for="(img, i) in imageList" :key="i" :class="{active: i == activeIndex}" @mouseenter="enterhandler(i)">
<img :src="img" alt=""/>
</li>
</ul>
21. 放大镜效果实现
功能拆解
- 左侧滑块跟随鼠标移动
- 右侧大图放大效果实现
- 鼠标移入控制滑块和大图显示隐藏
思路:获取到当前的鼠标在盒子内的相对位置(useMouselnElement),控制滑块跟随鼠标移动(left/top)
// 2. 获取鼠标相对位置
const target = ref(null)
const {elementX, elementY, isOutside} = useMouseInElement(target)
// 3. 控制滑块跟随鼠标移动(监听 elementX/Y 变化,一旦变化,重新设置 left/top )
const left = ref(0)
const top = ref(0)
watch([elementX, elementY], () => {
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 :style="{ left: `${left}px`, top: `${top}px` }" class="layer"></div>
22. 放大镜效果实现-大图效果实现
效果:为实现放大效果,大图的宽高是小图的俩倍
思路:大图的移动方向和滑块移动方向相反,且数值为2倍
// 控制大图的显示
positionX.value = -left.value * 2
positionY.value = -top.value * 2
<div :style="[
{
backgroundImage: `url(${imageList[0]})`,
backgroundPositionX: `${positionX}px`,
backgroundPositionY: `${positionY}px`,
},
]" class="large"></div>
23. 放大镜效果实现-鼠标移入控制显隐
思路:鼠标移入盒子(isOutside),滑块和大图才显示(v-show)
<!-- 蒙层小滑块 -->
<div v-show="!isOutside" :style="{ left: `${left}px`, top: `${top}px` }" class="layer"></div>
<!-- 放大镜大图 -->
<div v-show="!isOutside" :style="[
{......},
]" class="large"></div>
24. SKU
存货单位(英语:stock keeping unit,SKU/,.cs,kelju:/),也翻译为库存单元,是一个会计学名词,定义为库存管理中的最小可用单元,例如纺织品中一个SKU通常表示规格、颜色、款式,而在连锁零售门店中有时称单品为一个SKU
SKU组件的作用:产出当前用户选择的商品规格,为加入购物车操作提供数据信息
SKU组件的使用:
问:在实际工作中,经常会遇到别人写好的组件,熟悉一个三方组件,首先重点看什么?
答:props和emit,props决定了当前组件接收什么数据,emit决定了会产出什么数据
25. 全局组件注册
为什么要优化?
背景:components目录下有可能还会有很多其他通用型组件,有可能在多个业务模块中共享,所有统一进行全局组件注册比较好
components/index.js
// 把components中所有的组件都进行全局化注册
// 通过插件的方式
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)
}
}
main.js
// 引入全局组件插件
import {componentPlugin} from '@/components'
app.use(componentPlugin)