1、整体路由配置
路由设计原则:找内容切换的区域,如果是页面整体切换,则为一级路由。如果是一级路由页的内部切换,则为二级路由。默认路由将path项置为空。
(1)views下新增两个文件夹:Login(登录页)和Layout(首页),分别新增index.vue,输入简单的结构
(2)配置路由:
先引入组件(router/index.js)
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
然后配置path和component
path: '/',
component: Layout,
最后,设置路由出口(App.vue)
<template>
<!-- 一级路由出口组件 -->
<RouterView />
</template>
2、scss文件自动引入
一些组件共享的色值会以scss变量的方式统一放到一个名为var.scss的文件中,正常组件中使用,需要先导入scss文件,再使用内部的变量,自动导入可以免去手动导入的步骤,直接使用内部的变量。
(1)styles下新建var.scss,放入色值变量:
$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;
(2)vite.config.js添加配置:
(3)app.vue中使用:
(4)测试结果
3、字体图标引入
字体图标库有很多种,在这里,我选择用Iconfont字体图标库,具体使用方法如下,附上链接:iconfont-阿里巴巴矢量图标库
点击链接,进入首页->帮助->代码应用,可以看到不同的使用方法,推荐使用font-class引入
(1)引入样式css文件(index.html)
<link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
(2)在项目中使用
<i class=" iconfont icon-user">
4、一级导航渲染
(1)根据接口文档封装接口数据(apis/layout.js)
import httpInstance from "@/utils/http";
export function getCategoryAPI() {
return httpInstance({
url: '/home/category/head'
})
}
(2)发送请求获取列表数据
<script setup>
import { getCategoryAPI} from '@/apis/layout'
import { onMounted } from 'vue'
const getCategory = async () => {
const res = await getCategoryAPI()
console.log(res)
}
onMounted(() =>{
getCategory()
})
</script>
(3)v-for渲染页面
const categoryList = ref([])
categoryList.value = res.result
保存,打开vue调试器:
可以看到,响应式数据被存储到了后台
最后一步,添加列表渲染:
可以看到,列表数据被渲染了出来:
5、吸顶导航交互
浏览器在上下滚动的过程中,如果距离顶部的滚动距离大于78px,吸顶导航显示,小于78px,隐藏
(1)准备吸顶导航组件
LayoutFixed.vue
(2)导入静态结构
在index.vue中导入并渲染
(3)获取滚动距离
使用vueuse中的usescroll函数(官网:vueuse.org)
1)安装
2)引入:
import { useScroll } from '@vueuse/core'
解构出y值:
<script setup lang="ts">
import { useScroll } from '@vueuse/core'
const el = ref<HTMLElement | null>(null)
const { x, y, isScrolling, arrivedState, directions } = useScroll(el)
</script>
<template>
<div ref="el" />
</template>
3)以滚动距离做判断条件控制组件盒子显示隐藏
<div class="app-header-sticky " :class="{ show: y > 78}" >
6、Pinia优化重复请求
两个导航中的列表是完全一致的,但是要发送两次网络请求,通过pinia集中管理数据,再把数据给组件使用。
实现原理:
实现步骤:
(1)Stores下新建category.js文件,将逻辑代码拆分出去:
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { getCategoryAPI } from '@/apis/layout'
export const useCategoryStore = defineStore('category', () => {
// 导航列表的数据管理
// state 导航列表数据
const categoryList = ref([])
// action 获取导航数据的方法
const getCategory = async () => {
const res = await getCategoryAPI()
categoryList.value = res.result
}
return {
categoryList,
getCategory
}
})
(2)在index.vue中编写获取数据的方法
import { useCategoryStore } from '@/stores/categoryStore'
import { onMounted } from 'vue'
const categoryStore = useCategoryStore()
onMounted(() => categoryStore.getCategory())
(3)在要使用这部分数据的组件中导入使用:
1)Layoutfixed.vue
import { useCategoryStore } from '@/stores/category'
const categoryStore = useCategoryStore()
import { useCategoryStore } from'@/stores/category'
constcategoryStore=useCategoryStore()
2)Layoutheader.vue
import { useCategoryStore } from '@/stores/category'
const categoryStore = useCategoryStore()
7、整体结构拆分和分类列表渲染
导入静态结构,在index.vue中引入渲染,
商品分类内容渲染:调用pinia中的数据,拿到一级标题
import { useCategoryStore } from '@/stores/category'
const categoryStore = useCategoryStore()
根据后台返回的结果进行渲染:
8、轮播图实现
(1)准备静态模板
(2)封装接口(apis/home.js)
export function getBannerAPI (params = {}) {
// 轮播图数据,1为首页, 2为商品分类页,默认为1
const { distributionSite = '1' } = params
return httpInstance({
url: '/home/banner',
params: {
distributionSite
}
})
}
(3)homebanner.vue中编写逻辑代码获取接口数据:
<script setup>
import { getBannerAPI } from '@/apis/home'
import { onMounted, ref } from 'vue'
const bannerList = ref([])
const getBanner = async () => {
const res = await getBannerAPI()
console.log(res)
bannerList.value = res.result
}
onMounted(() => getBanner())
</script>
浏览器返回:
(4)根据返回的数据进行列表渲染:
<div class="home-banner">
<el-carousel height="500px">
<el-carousel-item v-for="item in bannerList" :key="item.id">
<img :src="item.imgUrl" alt="">
</el-carousel-item>
</el-carousel>
</div>
</template>
9、新鲜好物/人气推荐实现(面板组件封装)
使用面板组件封装,不仅解决了复用问题,而且让业务维护变得更加容易。
对于一些在结构上非常相似,只是内容不同的组件,通过组件封装可以实现复用结构的效果。
组件封装的一般思路为:把可复用的结构只写一次,把可能发送变化的部分抽象成组件参数(props/插槽),主标题和副标题抽象成prop传入,主体内容抽象成插槽传入。
对于本项目来说,新鲜好物和人气推荐两个板块,主体结构是一模一样的,只是内容不同(调用的后台接口不同),在这里,我们可以考虑把他们封装成一个组件,在需要的位置引入使用即可。
详细步骤如下:
Home/components/Homepanel.vue
(1)首先,引入静态模板,固定不变的部分用props传递参数
<script setup>
defineProps({
//主标题
title: {
type: String,
},
//副标题
subTitle: {
type: String,
}
})
</script>
<div class="head">
<!-- 主标题和副标题 -->
<h3>
{{title}}<small>{{subTitle}}</small>
</h3>
</div>
(2)封装好之后,在Index.vue中测试面板组件:
<!-- 测试面板组件 -->
<HomePanel title="新鲜好物" sub-title="新鲜好物-好多商品">
<div>我是新鲜好物</div>
</HomePanel>
<HomePanel title="人气推荐" sub-title="人气推荐-好多商品">
<div>我是人气推荐</div>
运行结果:
变化的部分请求接口返回:
新鲜好物实现:
(1)导入静态模板,根据需求定制主标题和副标题
(2)封装接口,Apis/home.js中添加:
/**
* @description: 获取新鲜好物
* @param {*}
* @return {*}
*/
export const findNewAPI = () => {
return httpInstance({
url: '/home/new'
})
}
(3)调用并返回数据
import HomePanel from './HomePanel.vue'
import { findNewAPI } from '@/apis/home'
import { onMounted, ref } from 'vue'
const newList = ref([])
// 获取数据
const getNewList = async () => {
const res = await findNewAPI()
newList.value = res.result
}
onMounted(() => getNewList())
(4)在页面中渲染:
<HomePanel title="新鲜好物" sub-title="新鲜出炉-品质多的">
<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>
</HomePanel>
效果展示:
人气推荐实现:
(1)封装接口:
/**
* @description: 获取人气推荐
* @param {*}
* @return {*}
*/
export const getHotAPI = () => {
return httpInstance({
url: '/home/hot'
})
}
(2)调用并返回数据:
import HomePanel from './HomePanel.vue'
import { getHotAPI } from '@/apis/home'
import { onMounted, ref } from 'vue'
const hotList = ref([])
const getHotList = async () => {
const res = await getHotAPI()
hotList.value = res.result
}
onMounted(() => getHotList())
(3)在页面中渲染:
<template>
<HomePanel title="人气推荐" sub-title="人气爆款 不容错过">
<ul class="goods-list">
<li v-for="item in hotList" :key="item.id">
<RouterLink to="/">
<img v-img-lazy="item.picture" alt="">
<p class="name">{{ item.title }}</p>
<p class="desc">{{ item.alt }}</p>
</RouterLink>
</li>
</ul>
</HomePanel>
</template>
效果展示:
10、图片懒加载指令实现与优化
使用场景:
有些网站的首页通常很长,例如电商类的网站,如果用户打开网站就看到了自己想要的商品,然后直接点击进去完成下单,再没有浏览更多的页面进行挑选操作,此时,用户就不一定会访问到页面靠下的内容,这类图片等资源通过懒加载优化手段可以做到只有用户进入视口区域才发送图片请求,大幅节省网络资源。
实现原理:在图片img上绑定指令,该图片只有正式进入视口区域才会发送网络请求
具体实现步骤如下:
首先,在main.js中定义全局指令
app.directive('img-lazy', {
mounted(el,binding) {
//el,指令绑定的那个元素
//binding,binding.value指令等于号后面绑定的表达式的值
console.log(el,binding.value)
useIntersectionObserver(
el,
([{ isIntersecting }]) => {
console.log(isIntersecting)
},
)
}
})
在HomeHot.vue中使用
<img v-img-lazy="item.picture" :src="item.picture" alt="">
紧接着,使用useIntersectionObserver判断是否进入视口区域:(vueuse.org)
<script setup>
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
const target = ref(null)
const targetIsVisible = ref(false)
const { stop } = useIntersectionObserver(
target,
([{ isIntersecting }], observerElement) => {
targetIsVisible.value = isIntersecting
},
)
</script>
控制台查看运行结果:上下滑动页面
如果进入视口区域,让它显示,反之则不显示
if(isIntersecting) {
el.src = binding.value
}
最后,修改img属性,去掉原来的src
<img v-img-lazy="item.picture" alt="">
上述代码,虽然实现了图片懒加载的功能,但是,也存在一些问题。首先,懒加载指令的逻辑写到了入口文件中,很显然是不合理的,入口文件主要用来存放一些初始化的逻辑,不应该包含太多的逻辑代码,在这里,我们可以将懒加载指令封装成一个插件,main.js只需要负责注册插件即可。
实现步骤如下:
(1)directives/新建index.vue
把懒加载指令相关逻辑代码挪过来,
import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = {
install(app){
app.directive('img-lazy', {
mounted(el,binding) {
//el,指令绑定的那个元素
//binding,binding.value指令等于号后面绑定的表达式的值
console.log(el,binding.value)
useIntersectionObserver(
el,
([{ isIntersecting }]) => {
console.log(isIntersecting)
//进入视口区域
if(isIntersecting) {
el.src = binding.value
}
},
)
}
})
}
}
(2)main.js中注册使用
import { lazyPlugin } from '@/directives'
app.use(lazyPlugin)
解决了这个问题之后,我们还需要考虑另外一个问题:在浏览器页面滚动滚轮可以发现,图片已经加载完毕,但监听仍在持续进行:存在内存浪费。为什么会出现这样的现象呢?因为useIntersectionObserver对于元素的监听是一直存在的,除非手动停止。
我们怎样做呢?可以·在图片第一次加载完毕后就停止监听,在useIntersectionObserver中有一个方法叫stop(),可以从中解构出来,第一次加载完毕后,调一下这个方法,让它停止监听。
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
console.log(isIntersecting)
//进入视口区域
if(isIntersecting) {
el.src = binding.value
stop()
}
11、产品列表实现
主要步骤如下:
详细步骤:
(1)准备静态模板
(2)封装接口(apis/home.js)
/**
* @description: 获取所有商品模块
* @param {*}
* @return {*}
*/
export const getGoodsAPI = () => {
return httpInstance({
url: '/home/goods'
})
}
(3)获取数据相关逻辑代码(HomeProduct.vue)
<script setup>
import HomePanel from './HomePanel.vue'
import { getGoodsAPI } from '@/apis/home'
import { onMounted, ref } from 'vue'
import GoodsItem from './GoodsItem.vue'
// 获取数据列表
const goodsProduct = ref([])
const getGoods = async () => {
const res = await getGoodsAPI()
goodsProduct.value = res.result
}
onMounted(() => getGoods())
</script>
浏览器返回:
(4)页面中渲染
<template>
<div class="home-product">
<HomePanel :title="cate.name" v-for="cate in goodsProduct" :key="cate.id">
<div class="box">
<RouterLink class="cover" to="/">
<img v-img-lazy="cate.picture" />
<strong class="label">
<span>{{ cate.name }}馆</span>
<span>{{ cate.saleInfo }}</span>
</strong>
</RouterLink>
<ul class="goods-list">
<li v-for="good in cate.goods" :key="good.id">
<RouterLink to="/" class="goods-item">
<img v-img-lazy="good.picture" alt="" />
<p class="name ellipsis">{{ good.name }}</p>
<p class="desc ellipsis">{{ good.desc }}</p>
<p class="price">¥{{ good.price }}</p>
</RouterLink>
</li>
</ul>
</div>
</HomePanel>
</div>
</template>
结果演示:
12、GoodsItem组件封装
使用场景:很多个业务模块中都需要用到同样的商品展示模块,没必要重复定义,封装起来,方便复用
如何封装?把要显示的数据对象设计成props参数,传入什么参数就显示什么数据
详细步骤:
(1)Home下新增goodsitem.vue
HomeProduct.vue中与每个面板相关的代码剪切过去,
<RouterLink to="/" class="goods-item">
<img :src="goods.picture" alt="" />
<p class="name ellipsis">{{ goods.name }}</p>
<p class="desc ellipsis">{{ goods.desc }}</p>
<p class="price">¥{{ goods.price }}</p>
</RouterLink>
(2)定义props
<script setup>
defineProps({
goods: {
tppe: Object,
default: () => { }
}
})
</script>
(3)Homeproduct.vue中引入:
import GoodsItem from './GoodsItem.vue'
(4)渲染
总结一下:封装的思路:抽象props参数,传入什么就显示什么。
下期见~