Vue3 - 小兔仙 - day2
0.学习大纲
1.pinia快速入门
pinia介绍和初始化
Pinia 是 Vue 的专属状态管理库,可以实现跨组件或页面共享状态,是 vuex 状态管理工具的替代品,和 Vuex相比,具备以下优势
-
提供更加简单的API (去掉了 mutation )
-
提供符合组合式API风格的API (和 Vue3 新语法统一)
-
去掉了modules的概念,每一个store都是一个独立的模块
-
搭配 TypeScript 一起使用提供可靠的类型推断
官方文档
介绍 | Pinia 中文文档您将喜欢使用的 Vue 存储库https://pinia.web3doc.top/introduction.html安装 pinia 并注册
npm i pinia
在 main.js 中
import { createPinia } from 'pinia'
const app = createApp(App)
// 以插件的形式注册
app.use(createPinia())
app.use(router)
app.mount('#app')
counter案例
核心步骤:
-
定义store
-
组件使用store
示例
1.定义store
// counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', ()=>{
// 数据 (state)
const count = ref(0)
// 修改数据的方法 (action)
const increment = ()=>{
count.value++
}
// 以对象形式返回
return {
count,
increment
}
})
2.组件使用store
<script setup>
// 1. 导入use方法
import { useCounterStore } from '@/stores/counter'
// 2. 执行方法得到store store里有数据和方法
const counterStore = useCounterStore()
</script>
<template>
<button @click="counterStore.increment">
{{ counterStore.count }}
</button>
</template>
实现getters
getters直接使用计算属性即可实现
// 数据(state)
const count = ref(0)
// getter (computed)
const doubleCount = computed(() => count.value * 2)
异步action
思想:action函数既支持同步也支持异步,和在组件中发送网络请求写法保持一致步骤:
-
store中定义action
-
组件中触发action
1- store中定义action
const API_URL = 'http://geek.itheima.net/v1_0/channels'
export const useCounterStore = defineStore('counter', ()=>{
// 数据
const list = ref([])
// 异步action
const loadList = async ()=>{
const res = await axios.get(API_URL)
list.value = res.data.data.channels
}
return {
list,
loadList
}
})
2- 组件中调用action
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// 调用异步action
counterStore.loadList()
</script>
<template>
<ul>
<li v-for="item in counterStore.list" :key="item.id">{{ item.name }}</li>
</ul>
</template>
storeToRefs保持响应式解构
直接基于store进行解构赋值,响应式数据(state和getter)会丢失响应式特性,使用storeToRefs辅助保持响应式
<script setup>
import { storeToRefs } from "pinia";
import { useCounterStore } from "@/stores/counter";
const counterStore = useCounterStore();
// 数据使用它storeToRefs包裹之后解构保持响应式
const { count } = storeToRefs(counterStore);
// 方法不用
const { increment } = counterStore;
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
2.项目起步
项目创建+目录整理+git提交
项目创建
目录整理
git提交Gitee作为远程仓库保存Vue项目_本地vue项目配置gitee仓库-CSDN博客文章浏览阅读496次,点赞6次,收藏8次。【代码】Gitee作为远程仓库保存Vue项目。_本地vue项目配置gitee仓库https://blog.csdn.net/2301_76354366/article/details/135698204
jsconfig.json配置别名路径@提示符
新版本的vue3自己做了这个配置,现在不需要手动自己去配了
elementPlus引入
官方文档
安装 | Element PlusA Vue 3 based component library for designers and developershttps://element-plus-docs.bklab.cn/zh-CN/guide/installation.html装elementPlus和自动按需导入插件配置
npm i element-plus
npm install -D unplugin-vue-components unplugin-auto-import
配置自动按需导入,在 vite.config.js 中添加
// 引入插件
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()],
}),
]
})
测试组件
<template>
<el-button type="primary">i am button</el-button>
</template>
elementPlus主题替换(scss变量替换)
实现步骤
安装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,
),
)
)
自动导入配置
这里自动导入需要深入到elementPlus的组件中,按照官方的配置文档来
-
自动导入定制化样式文件进行样式覆盖
-
按需定制主题配置 (需要安装 unplugin-element-plus)
npm i unplugin-element-plus
// 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://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [
ElementPlusResolver({ importStyle: "sass" }),
]
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
css: {
preprocessorOptions: {
scss: {
// 自动导入定制化样式文件进行样式覆盖
additionalData: `
@use "@/styles/element/index.scss" as *;
`,
}
}
}
})
axios安装+基础封装
安装axios
npm i axios
基础配置
官方文档地址:起步 | Axios Docs
基础配置通常包括:
-
实例化 - baseURL + timeout
-
拦截器 - 携带token 401拦截等
// 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/testAPI.js
import http from '@/utils/http'
export function getCategoryAPI () {
return http({
url: 'home/category/head'
})
}
调用测试
// main.js
import { getCategoryAPI } from '@/apis/testAPI'
getCategoryAPI().then(res => console.log(res))
路由整体设计
路由设计原则:
找页面的切换方式,如果是整体切换,则为一级路由
如果是在一级路由的内部进行的内容切换,则为二级路由
<template>
<div>
<h1>这是首页</h1>
<!-- 添加二级路由出口 -->
<router-view></router-view>
</div>
</template>
<template>
<div>
<h1>这是登录页</h1>
</div>
</template>
<template>
<h1>我是home页</h1>
</template>
<template>
<h1>我是分类页</h1>
</template>
配置路由
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: () => import('@/views/Layout/index.vue'),
children:
[
{
path: '',
name: 'home',
component: () => import('@/views/Home/index.vue')
},
{
path: 'category',
component: () => import('@/views/Category/index.vue')
}
]
},
{
path: '/login',
component: () => import('@/views/Login/index.vue')
}
],
})
export default router
静态资源初始化
静态资源引入
-
图片资源 - 把 images 文件夹放到 assets 目录下
-
样式资源 - 把 common.scss 文件放到 styles 目录下
Error Lens插件安装
scss共享样式文件自动导入
为什么要自动导入
在项目里一些组件共享的色值会以scss变量的方式统一放到一个名为 var.scss 文件中,正常组件中使用,需要先导入 scss 文件,比较繁琐,自动导入可以免去手动导入的步骤,直接使用内部的变量。
自动导入配置
1.新增一个 var.scss 文件,存入色值变量
$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;
2.通过 vite.config.js 配置自动导入文件
css: {
preprocessorOptions: {
scss: {
// 自动导入scss文件
additionalData: `
@use "@/styles/element/index.scss" as *;
@use "@/styles/var.scss" as *;
`,
}
}
}
3.Layout模块
组件结构快速熟悉搭建
// LayoutNav.vue
<script setup>
</script>
<template>
<nav class="app-topnav">
<div class="container">
<ul>
<template v-if="true">
<li><a href="javascript:;"><i class="iconfont icon-user"></i>周杰伦</a></li>
<li>
<el-popconfirm title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">
<template #reference>
<a href="javascript:;">退出登录</a>
</template>
</el-popconfirm>
</li>
<li><a href="javascript:;">我的订单</a></li>
<li><a href="javascript:;">会员中心</a></li>
</template>
<template v-else>
<li><a href="javascript:;">请先登录</a></li>
<li><a href="javascript:;">帮助中心</a></li>
<li><a href="javascript:;">关于我们</a></li>
</template>
</ul>
</div>
</nav>
</template>
<style scoped lang="scss">
.app-topnav {
background: #333;
ul {
display: flex;
height: 53px;
justify-content: flex-end;
align-items: center;
li {
a {
padding: 0 15px;
color: #cdcdcd;
line-height: 1;
display: inline-block;
i {
font-size: 14px;
margin-right: 2px;
}
&:hover {
color: $xtxColor;
}
}
~ li {
a {
border-left: 2px solid #666;
}
}
}
}
}
</style>
// LayoutHeader.vue
<script setup>
</script>
<template>
<header class="app-header">
<div class="container">
<h1 class="logo">
<RouterLink to="/">小兔鲜</RouterLink>
</h1>
<ul class="app-header-nav">
<li class="home">
<RouterLink to="/">首页</RouterLink>
</li>
<li><RouterLink to="/">居家</RouterLink></li>
<li><RouterLink to="/">美食</RouterLink></li>
<li><RouterLink to="/">服饰</RouterLink></li>
</ul>
<div class="search">
<i class="iconfont icon-search"></i>
<input type="text" placeholder="搜一搜" />
</div>
<!-- 头部购物车 -->
</div>
</header>
</template>
<style scoped lang='scss'>
.app-header {
background: #fff;
.container {
display: flex;
align-items: center;
}
.logo {
width: 200px;
a {
display: block;
height: 132px;
width: 100%;
text-indent: -9999px;
background: url("@/assets/images/logo.png") no-repeat center 18px /
contain;
}
}
.app-header-nav {
width: 820px;
display: flex;
padding-left: 40px;
position: relative;
z-index: 998;
li {
margin-right: 40px;
width: 38px;
text-align: center;
a {
font-size: 16px;
line-height: 32px;
height: 32px;
display: inline-block;
&:hover {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}
.active {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}
}
.search {
width: 170px;
height: 32px;
position: relative;
border-bottom: 1px solid #e7e7e7;
line-height: 32px;
.icon-search {
font-size: 18px;
margin-left: 5px;
}
input {
width: 140px;
padding-left: 5px;
color: #666;
}
}
.cart {
width: 50px;
.curr {
height: 32px;
line-height: 32px;
text-align: center;
position: relative;
display: block;
.icon-cart {
font-size: 22px;
}
em {
font-style: normal;
position: absolute;
right: 0;
top: 0;
padding: 1px 6px;
line-height: 1;
background: $helpColor;
color: #fff;
font-size: 12px;
border-radius: 10px;
font-family: Arial;
}
}
}
}
</style>
// LayoutFooter.vue
<template>
<footer class="app_footer">
<!-- 联系我们 -->
<div class="contact">
<div class="container">
<dl>
<dt>客户服务</dt>
<dd><i class="iconfont icon-kefu"></i> 在线客服</dd>
<dd><i class="iconfont icon-question"></i> 问题反馈</dd>
</dl>
<dl>
<dt>关注我们</dt>
<dd><i class="iconfont icon-weixin"></i> 公众号</dd>
<dd><i class="iconfont icon-weibo"></i> 微博</dd>
</dl>
<dl>
<dt>下载APP</dt>
<dd class="qrcode"><img src="@/assets/images/qrcode.jpg" /></dd>
<dd class="download">
<span>扫描二维码</span>
<span>立马下载APP</span>
<a href="javascript:;">下载页面</a>
</dd>
</dl>
<dl>
<dt>服务热线</dt>
<dd class="hotline">
400-0000-000 <small>周一至周日 8:00-18:00</small>
</dd>
</dl>
</div>
</div>
<!-- 其它 -->
<div class="extra">
<div class="container">
<div class="slogan">
<a href="javascript:;">
<i class="iconfont icon-footer01"></i>
<span>价格亲民</span>
</a>
<a href="javascript:;">
<i class="iconfont icon-footer02"></i>
<span>物流快捷</span>
</a>
<a href="javascript:;">
<i class="iconfont icon-footer03"></i>
<span>品质新鲜</span>
</a>
</div>
<!-- 版权信息 -->
<div class="copyright">
<p>
<a href="javascript:;">关于我们</a>
<a href="javascript:;">帮助中心</a>
<a href="javascript:;">售后服务</a>
<a href="javascript:;">配送与验收</a>
<a href="javascript:;">商务合作</a>
<a href="javascript:;">搜索推荐</a>
<a href="javascript:;">友情链接</a>
</p>
<p>CopyRight © 小兔鲜儿</p>
</div>
</div>
</div>
</footer>
</template>
<style scoped lang='scss'>
.app_footer {
overflow: hidden;
background-color: #f5f5f5;
padding-top: 20px;
.contact {
background: #fff;
.container {
padding: 60px 0 40px 25px;
display: flex;
}
dl {
height: 190px;
text-align: center;
padding: 0 72px;
border-right: 1px solid #f2f2f2;
color: #999;
&:first-child {
padding-left: 0;
}
&:last-child {
border-right: none;
padding-right: 0;
}
}
dt {
line-height: 1;
font-size: 18px;
}
dd {
margin: 36px 12px 0 0;
float: left;
width: 92px;
height: 92px;
padding-top: 10px;
border: 1px solid #ededed;
.iconfont {
font-size: 36px;
display: block;
color: #666;
}
&:hover {
.iconfont {
color: $xtxColor;
}
}
&:last-child {
margin-right: 0;
}
}
.qrcode {
width: 92px;
height: 92px;
padding: 7px;
border: 1px solid #ededed;
}
.download {
padding-top: 5px;
font-size: 14px;
width: auto;
height: auto;
border: none;
span {
display: block;
}
a {
display: block;
line-height: 1;
padding: 10px 25px;
margin-top: 5px;
color: #fff;
border-radius: 2px;
background-color: $xtxColor;
}
}
.hotline {
padding-top: 20px;
font-size: 22px;
color: #666;
width: auto;
height: auto;
border: none;
small {
display: block;
font-size: 15px;
color: #999;
}
}
}
.extra {
background-color: #333;
}
.slogan {
height: 178px;
line-height: 58px;
padding: 60px 100px;
border-bottom: 1px solid #434343;
display: flex;
justify-content: space-between;
a {
height: 58px;
line-height: 58px;
color: #fff;
font-size: 28px;
i {
font-size: 50px;
vertical-align: middle;
margin-right: 10px;
font-weight: 100;
}
span {
vertical-align: middle;
text-shadow: 0 0 1px #333;
}
}
}
.copyright {
height: 170px;
padding-top: 40px;
text-align: center;
color: #999;
font-size: 15px;
p {
line-height: 1;
margin-bottom: 20px;
}
a {
color: #999;
line-height: 1;
padding: 0 10px;
border-right: 1px solid #999;
&:last-child {
border-right: none;
}
}
}
}
</style>
// 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>
注意要把 main.js 里原来的css文件删除,不要会移位
运行项目
阿里字体图标引入
官网地址
iconfont-阿里巴巴矢量图标库iconfont-国内功能很强大且图标内容很丰富的矢量图标库,提供矢量图标下载、在线存储、格式转换等功能。阿里巴巴体验团队倾力打造,设计和前端开发的便捷工具https://www.iconfont.cn/字体图标采用的是阿里的字体图标库,样式文件已经准备好,在 index.html
文件中引入即可
<link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
使用的时候按需引入
一级导航数据渲染
实现步骤
-
封装接口函数
-
调用接口函数
-
v-for渲染模版
封装接口函数
// /apis/layout.js
import http from '@/utils/http'
export function getCategoryAPI() {
return http({
url: '/home/category/head'
})
}
调用接口函数
// LayoutHeader.vue
<script setup>
import { ref, onMounted } from "vue";
import { getCategoryAPI } from "@/apis/Layout";
const categoryList = ref([]);
const getCategory = async () => {
const res = await getCategoryAPI();
categoryList.value = res.result;
};
onMounted(() => getCategory());
</script>
列表渲染
// LayoutHeader.vue
<template>
<header class="app-header">
<div class="container">
<h1 class="logo">
<RouterLink to="/">小兔鲜</RouterLink>
</h1>
<ul class="app-header-nav">
<li class="home" v-for="item in categoryList" :key="item.id">
<RouterLink to="/">{{ item.name }}</RouterLink>
</li>
</ul>
<div class="search">
<i class="iconfont icon-search"></i>
<input type="text" placeholder="搜一搜" />
</div>
<!-- 头部购物车 -->
</div>
</header>
</template>
吸顶导航交互实现
要求:浏览器在上下滚动的过程中,如果距离顶部的滚动距离大于 78px,吸顶导航显示,小于 78px隐藏。
准备组件静态结构
// LayoutFixed.vue
<script setup>
</script>
<template>
<div class="app-header-sticky show">
<div class="container">
<RouterLink class="logo" to="/" />
<!-- 导航区域 -->
<ul class="app-header-nav">
<li class="home">
<RouterLink to="/">首页</RouterLink>
</li>
<li>
<RouterLink to="/">居家</RouterLink>
</li>
<li>
<RouterLink to="/">美食</RouterLink>
</li>
<li>
<RouterLink to="/">服饰</RouterLink>
</li>
<li>
<RouterLink to="/">母婴</RouterLink>
</li>
<li>
<RouterLink to="/">个护</RouterLink>
</li>
<li>
<RouterLink to="/">严选</RouterLink>
</li>
<li>
<RouterLink to="/">数码</RouterLink>
</li>
<li>
<RouterLink to="/">运动</RouterLink>
</li>
<li>
<RouterLink to="/">杂项</RouterLink>
</li>
</ul>
<div class="right">
<RouterLink to="/">品牌</RouterLink>
<RouterLink to="/">专题</RouterLink>
</div>
</div>
</div>
</template>
<style scoped lang='scss'>
.app-header-sticky {
width: 100%;
height: 80px;
position: fixed;
left: 0;
top: 0;
z-index: 999;
background-color: #fff;
border-bottom: 1px solid #e4e4e4;
// 此处为关键样式!!!
// 状态一:往上平移自身高度 + 完全透明
transform: translateY(-100%);
opacity: 0;
// 状态二:移除平移 + 完全不透明
&.show {
transition: all 0.3s linear;
transform: none;
opacity: 1;
}
.container {
display: flex;
align-items: center;
}
.logo {
width: 200px;
height: 80px;
background: url("@/assets/images/logo.png") no-repeat right 2px;
background-size: 160px auto;
}
.right {
width: 220px;
display: flex;
text-align: center;
padding-left: 40px;
border-left: 2px solid $xtxColor;
a {
width: 38px;
margin-right: 40px;
font-size: 16px;
line-height: 1;
&:hover {
color: $xtxColor;
}
}
}
}
.app-header-nav {
width: 820px;
display: flex;
padding-left: 40px;
position: relative;
z-index: 998;
li {
margin-right: 40px;
width: 38px;
text-align: center;
a {
font-size: 16px;
line-height: 32px;
height: 32px;
display: inline-block;
&:hover {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}
.active {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
}
}
</style>
获取滚动距离
使用 VueUse ,官方定义的一系列函数
官网地址
nullCollection of essential Vue Composition Utilitieshttps://v4-11-2.vueuse.org/
npm i @vueuse/core
以滚动距离做判断条件控制组件盒子展示隐藏
核心逻辑:根据滚动距离判断当前show类名是否显示,大于78显示,小于78,不显示
<script setup>
// vueUse
import { useScroll } from '@vueuse/core'
const { y } = useScroll(window)
</script>
<template>
<div class="app-header-sticky" :class="{ show: y > 78 }">
<!-- 省略部分代码 -->
</div>
</template>
Pinia优化重复请求
结论:两个导航中的列表是完全一致的,但是要发送两次网络请求,存在浪费。通过Pinia集中管理数据,再把数据给组件使用。
定义 pinia 进行状态管理
// /stores/category.js
import { defineStore } from 'pinia'
import { getCategoryAPI } from "@/apis/layout";
import { ref } from 'vue'
export const useCategoryStore = defineStore('category', () => {
// state 导航列表数据
const categoryList = ref([]);
// action 获取导航列表方法
const getCategory = async () => {
const res = await getCategoryAPI();
categoryList.value = res.result;
};
return { categoryList, getCategory }
})
在 index.vue 中进行触发
在需要用的地方进行导入调用