前端学习笔记 7:小兔鲜
准备工作
创建项目
创建项目:
npm init vue@latest
相关选项如下:
在src
目录下添加以下目录:
别名路径联想
默认情况下在 VSCode 中输入import xxx from '@...'
时不会启用路径联想功能,要启用需要在项目根目录下添加 VSCode 配置文件jsconfig.json
:
{
"compilerOptions" : {
"baseUrl" : "./",
"paths" : {
"@/*":["src/*"]
}
}
}
如果 VSCode 已经自动创建该文件,可以跳过这一步。
添加 ElementPlus
ElementPlus 加入的方式分为全部引入和按需引入,后者可以减少项目打包后的体积,所以这里采用按需引入。
安装 ElementPlus:
npm install element-plus --save
安装插件:
npm install -D unplugin-vue-components unplugin-auto-import
修改vite.config.js
,添加以下内容:
// vite.config.ts
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()],
}),
],
})
修改App.vue
进行验证:
<script setup>
</script>
<template>
<el-button type="primary">Primary</el-button>
</template>
定制主题色
安装 sass:
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,
),
)
)
修改vite.config.js
:
export default defineConfig({
plugins: [
// ...
Components({
resolvers: [ElementPlusResolver({
importStyle: 'sass' })],
}),
],
// ...
css: {
preprocessorOptions: {
scss: {
// 自动导入定制化样式文件进行样式覆盖
additionalData: `
@use "@/styles/element/index.scss" as *;
`,
}
}
}
})
Axios 基础配置
最好在框架代码中创建 Axios 实例,并进行统一配置,这样可以对所有接口调用都要用的配置信息进行统一管理。
安装:
npm i axios
添加utils/http.js
:
import axios from 'axios'
// 创建axios实例
const http = axios.create({
baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
timeout: 5000
})
// axios请求拦截器
http.interceptors.request.use(config => {
return config
}, e => Promise.reject(e))
// axios响应式拦截器
http.interceptors.response.use(res => res.data, e => {
return Promise.reject(e)
})
export default http
添加测试代码apis/test.js
:
import http from '@/utils/http'
export const getCategoryService = () => {
return http.get('home/category/head')
}
在 App.vue
中进行测试:
import {
getCategoryService } from '@/apis/test'
getCategoryService().then((res) => {
console.log(res)
})
路由设计
添加views/layout/index.vue
作为首页:
<template>
首页
</template>
依次添加:
views/login/index.vue
,登录页views/home/index.vue
,Home页views/category/index.vue
,分类页
eslint 会报错,提示文件命名不符合标准,可以修改.eslintrc.cjs
关闭报错:
module.exports = {
// ...
rules: {
'vue/multi-word-component-names': "off"
}
}
修改路由配置router/index.js
:
import {
createRouter, createWebHistory } from 'vue-router'
import LayoutVue from '@/views/layout/index.vue'
import LoginVue from '@/views/login/index.vue'
import HomeVue from '@/views/home/index.vue'
import CategoryVue from '@/views/category/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: LayoutVue,
children: [
{
path: '', component: HomeVue },
{
path: '/category', component: CategoryVue }
]
},
{
path: '/login', component: LoginVue }
]
})
export default router
值得注意的是,代表 Home 页的子路由 path 设置为空字符串,这样可以让/
路径默认展示 Home 页。
修改App.vue
,添加路由出口:
<template>
<RouterView />
</template>
修改views/layout/index.vue
,添加路由出口:
<template>
首页
<RouterView />
</template>
现在项目的路由是:
/
,Home 页/category
,分类页/login
,登录页
引入静态资源和样式
将图片相关资源 images 添加到assets
目录下,将样式文件common.scss
添加到styles
目录下。
修改 main.js
,导入样式:
// import './assets/main.css'
import '@/styles/common.scss'
为了方便查看错误提示信息,可以添加插件:
sass 自动导入
添加一个存放颜色相关变量的 sass 文件styles/var.scss
:
$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;
修改 vite.config.js
:
css: {
preprocessorOptions: {
scss: {
// 自动导入scss文件
additionalData: `
@use "@/styles/element/index.scss" as *;
@use "@/styles/var.scss" as *;
`,
}
}
}
测试,修改App.vue
:
<template>
<div class="test">Hello World!</div>
<RouterView />
</template>
<style scoped lang="scss">
.test{
color: $helpColor;
}
</style>
Layout
页面搭建
在vies/layout
中添加以下视图:LayoutNav.vue、LayoutHeader.vue、LayoutFooter.vue。
修改views/layout/index.vue
,使用这些视图填充页面:
<script setup>
import LayoutNav from './components/LayoutNav.vue'
import LayoutHeader from './components/LayoutHeader.vue'
import LayoutFooter from './components/LayoutFooter.vue'
</script>
<template>
<LayoutNav />
<LayoutHeader />
<RouterView />
<LayoutFooter />
</template>
字体图标引入
修改根目录下的index.html
,添加:
<link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
这里使用的是阿里的素材库,具体的使用方式可以参考这个视频。
一级导航渲染
封装接口调用,添加apis/layout.js
:
import http from "../utils/http";
export const getCategorysService = ()=>{
return http.get('/home/category/head')
}
调用接口,将返回值填充进响应式数据,用响应式数据完成页面渲染。
修改LayoutHeader.vue
:
<script setup>
import { getCategorysService } from "@/apis/layout.js";
import {ref} from 'vue'
const categorys = ref([])
const getCategorys = async ()=>{
const result = await getCategorysService()
categorys.value = result.result
}
getCategorys()
</script>
<template>
<li v-for="cat in categorys" :key="cat.id"> <RouterLink to="/">{
{ cat.name }}</RouterLink> </li>
</template>
吸顶导航栏
添加views/layout/component/LayoutFixed.vue
。
在 views/layout/index.vue
中引入:
<script setup>
import LayoutNav from './components/LayoutNav.vue'
import LayoutHeader from './components/LayoutHeader.vue'
import LayoutFooter from './components/LayoutFooter.vue'
import LayoutFixed from '@/views/layout/components/LayoutFixed.vue'
</script>
<template>
<LayoutFixed />
<LayoutNav />
<LayoutHeader />
<RouterView />
<LayoutFooter />
</template>
吸顶导航栏中,用show
类别控制是否显示:
<div class="app-header-sticky show">
需要知道鼠标在y轴的滚动距离,这里用一个函数库 vueuse 获取。
安装:
npm i @vueuse/core
使用函数获取滚动距离:
<script setup>
import { useWindowScroll } from '@vueuse/core'
const { y } = useWindowScroll()
</script>
<div class="app-header-sticky" :class="{ show: y > 78 }">
Pinia 优化重复请求
吸顶导航与普通的导航栏使用相同的商品分类数据,为了避免重复请求接口,可以使用 Pinia 存储数据。
创建分类的数据存储:
import {
defineStore } from 'pinia'
import {
ref } from 'vue'
import {
getCategorysService } from '@/apis/layout'
export const useCategoryStore = defineStore('category', () => {
const categorys = ref([])
const loadCategorys = async () => {
const result = await getCategorysService()
categorys.value = result.result
}
return {
categorys, loadCategorys }
})
在吸顶导航和普通导航共同的父组件layout/index.vue
中触发 Store 的 action 以加载分类数据:
<script setup>
// ...
import { useCategoryStore } from '@/stores/category'
const categoryStore = useCategoryStore()
categoryStore.loadCategorys()
</script>
在固定导航栏中使用数据填充导航栏:
<script setup>
import { useWindowScroll } from '@vueuse/core'
import {useCategoryStore} from '@/stores/category'
const categoryStore = useCategoryStore()
const { y } = useWindowScroll()
</script>
<template>
<div class="app-header-sticky" :class="{ show: y > 78 }">
<div class="container">
<RouterLink class="logo" to="/" />
<!-- 导航区域 -->
<ul class="app-header-nav ">
<li class="home">
<RouterLink to="/">首页</RouterLink>
</li>
<li v-for="cat in categoryStore.categorys" :key="cat.id">
<RouterLink to="/">{
{ cat.name }}</RouterLink>
</li>
</ul>
<div class="right">
<RouterLink to="/">品牌</RouterLink>
<RouterLink to="/">专题</RouterLink>
</div>
</div>
</div>
</template>
普通导航栏中的使用方式是相同的,这里不再赘述。
Home
整体结构拆分
将 Home 页拆分成以下几部分:
<script setup>
import HomeBannerVue from './components/HomeBanner.vue'
import HomeCategoryVue from './components/HomeCategory.vue'
import HomeHotVue from './components/HomeHot.vue'
import HomeNewVue from './components/HomeNew.vue'
import HomeProductVue from './components/HomeProduct.vue'
</script>
<template>
<div class="container">
<HomeCategoryVue />
<HomeBannerVue />
</div>
<HomeNewVue />
<HomeHotVue />
<HomeProductVue />
</template>
分类
分类组件的基本实现见这里。
所依赖的数据可以从 Pinia 中的分类信息获取:
<script setup>
import { useCategoryStore } from '@/stores/category'
const categoryStore = useCategoryStore()
</script>
<template>
<div class="home-category">
<ul class="menu">
<li v-for="cat in categoryStore.categorys" :key="cat.id">
<RouterLink to="/">{
{ cat.name }}</RouterLink>
<RouterLink v-for="child in cat.children.slice(0, 2)" :key="child.id" to="/">{
{ child.name }}</RouterLink>
<!-- 弹层layer位置 -->
<div class="layer">
<h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4>
<ul>
<li v-for="good in cat.goods" :key="good.id">
<RouterLink to="/">
<img alt="" :src="good.picture"/>
<div class="info">
<p class="name ellipsis-2">
{
{ good.name }}
</p>
<p class="desc ellipsis">{
{ good.desc }}</p>
<p class="price"><i>¥</i>{
{ good.price }}</p>
</div>
</RouterLink>
</li>
</ul>
</div>
</li>
</ul>
</div>
</template>
轮播图
基本实现代码可以从这里获取。
封装接口:
import http from '@/utils/http'
export const getHomeBannerService = ()=>{
return http.get('/home/banner')
}
加载数据:
<script setup>
import { getHomeBannerService } from '@/apis/home';
import { ref } from 'vue';
const banner = ref([])
const loadHomeBanner = async ()=>{
const result = await getHomeBannerService()
banner.value = result.result
}
loadHomeBanner()
</script>
<template>
<div class="home-banner">
<el-carousel height="500px">
<el-carousel-item v-for="item in banner" :key="item.id">
<img :src="item.imgUrl" alt="">
</el-carousel-item>
</el-carousel>
</div>
</template>
面板组件封装
面板组件HomePannel.vue
的基本实现可以从这里获取。
将简单信息封装成 props(属性),将复杂信息封装成 slot(插槽):
<script setup>
defineProps({
title: {
type: String
},
subTitle: {
type: String
}
})
</script>
<template>
<div class="home-panel">
<div class="container">
<div class="head">
<!-- 主标题和副标题 -->
<h3>
{
{ title }}<small>{
{ subTitle }}</small>
</h3>
</div>
<!-- 主体内容区域 -->
<slot></slot>
</div>
</div>
</template>
测试:
<HomePannelVue title="新鲜好物" subTitle="更多商品">
新鲜好物
</HomePannelVue>
<HomePannelVue title="热销商品" subTitle="更多商品">
热销商品
</HomePannelVue>
新鲜好物
新鲜好物页面HomeNew.vue
的基本实现见这里。
封装接口:
//新鲜好物
export const getNewService = ()=>{
return http.get('/home/new')
}
从接口获取数据渲染页面:
<script setup>
import { getNewService } from '@/apis/home'
import { ref } from 'vue'
import HomePannelVue from './HomePannel.vue';
const newGoods = ref([])
const loadNewGoods = async () => {
const result = await getNewService()
newGoods.value = result.result
}
loadNewGoods()
</script>
<template>
<HomePannelVue title="新鲜好物" subTitle="新鲜出炉 品质靠谱">
<ul class="goods-list">
<li v-for="good in newGoods" :key="good.id">
<RouterLink to="/">
<img :src="good.picture" alt="" />
<p class="name">{
{ good.name }}</p>
<p class="price">¥{
{ good.price }}</p>
</RouterLink>
</li>
</ul>
</HomePannelVue>
</template>
图片懒加载
需要实现一个自定义指令v-img-lazy
。
修改main.js
:
// import './assets/main.css'
import '@/styles/common.scss'
import {
createApp } from 'vue'
import {
createPinia } from 'pinia'
import {
useIntersectionObserver } from '@vueuse/core'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.directive('img-lazy', {
mounted(el, binding) {
//el,指令绑定的对象
//binding.value,指令 = 后的表达式的值
console.log(el, binding.value)
useIntersectionObserver(
el,
([{
isIntersecting }]) => {
if (isIntersecting) {
el.src = binding.value
}
},
)
},
})
app.mount('#app')
插件封装
在入口文件中写入懒加载逻辑是不合适的,应当封装成插件。
创建插件文件directives/img-lazy.js
:
import {
useIntersectionObserver } from '@vueuse/core'
//图片懒加载插件
export const imgLazyPlugin = {
install(app) {
// 配置此应用
app.directive('img-lazy', {
mounted(el, binding) {
//el,指令绑定的对象
//binding.value,指令 = 后的表达式的值
console.log(el, binding.value)
useIntersectionObserver(
el,
([{
isIntersecting }]) => {
if (isIntersecting) {
el.src = binding.value
}
},
)
},
})
}
}
这里的useIntersectionObserver
函数是 vueuse 库中用于监听某个控件是否在 Window 中显示的函数。
修改main.js
,使用插件:
// import './assets/main.css'
import '@/styles/common.scss'
import {
createApp } from 'vue'
import {
createPinia } from 'pinia'
import {
imgLazyPlugin } from './directives/img-lazy'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(imgLazyPlugin)
app.mount('#app')
避免重复监听
如果不在图片加载后手动停止监听,监听行为就一直存在。
修改img-lazy.js
,手动停止监听:
const {
stop } = useIntersectionObserver(
el,
([{
isIntersecting }]) => {
if (isIntersecting) {
el.src = binding.value
stop()
}
},
)
useIntersectionObserver
会返回一个停止的函数,在合适的时候调用即可。
商品列表
商品列表控件HomeProduct.vue
的初始代码可以从这里获取。
封装接口:
export const getGoodsService = ()=>{
return http.get('/home/goods')
}
渲染数据:
<script setup>
import HomePanel from './HomePannel.vue'
import { getGoodsService } from '@/apis/home'
import { ref } from 'vue'
const goodsProduct = ref([])
const loadGoods = async () => {
const res = await getGoodsService()
goodsProduct.value = res.result
}
loadGoods()
</script>
<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 :src="cate.picture" />
<strong class="label">
<span>{
{ cate.name }}馆</span>
<span>{
{ cate.saleInfo }}</span&