项目结构
src目录下建立了各个文件夹,便于分类存放各种文件:
- assets:静态资源文件夹,包括css、图片资源等;
- components:通用组件文件夹,存放一些通用的、耦合性低的组件文件;
- network:封装网络请求的文件夹,前端项目中不可避免地会调用后台接口,这个文件夹中存放此类文件,管理接口调用;
- router:路由配置文件夹;
- store:Vuex文件夹,一般会有多个文件,用于分模块共享数据;
- utils:存放第三方库的文件;
- views:页面级别的组件存放文件夹;
配置network接口文件夹
在network文件夹下建立三个文件:
- cart.js
- home.js:包含获取首页所有数据的接口调用
- request.js:管理所有的接口调用,比如请求拦截、响应拦截等
// request.js
import axios from 'axios'
export function request(config) {
const instance = axios.create({
baseURL: 'http://api.shop.eduwork.cn',
timeout: 5000,
})
// 请求拦截
instance.interceptors.request.use(config => {
// 如果有一个接口需要认证才可以访问,就在这里统一设置
return config
}, err => {
})
// 响应拦截
instance.interceptors.response.use(res => {
return res
}, err => {
// 如果有需要授权才可以访问的接口,统一去login授权
// 如果有错误,这里会进行处理
})
return instance(config)
}
由于我们接下来第一个要实现的就是首页功能,因此,我们需要先配置好首页中接口调用函数,放在home.js文件中:
// home.js
import { request } from "./request";
function getHomeAllData() {
return request({
// 拿到首页所有数据,无需任何参数,所以只提供url就可以
url: '/api/index'
})
}
小试牛刀:获取首页所有数据
我们在Home.vue组件中获取数据,在setup函数中调用onMounted生命周期钩子,希望在页面加载时就将拿到的数据显示到页面,故在该钩子中发起axios请求获取数据:
setup() {
const banner = ref([])
onMounted(() => {
getHomeAllData()
.then((res) => {})
.catch((err) => {})
})
return {
banner,
}
}
配置路由
我们在views目录下创建所有页面级的文件夹,用于存放各个页面功能的组件:
- category:商品分类页面
- detail:商品详情页面
- home:主页
- profile:个人中心页面
- shopcart:购物车页面
在配置路由之前,为了使得路由跳转效果明显,我们需要建立以上各个页面的组件文件,比如category文件夹下建立Category.vue文件,其中简单写几句:
<template>
<div>
<h1>商品分类</h1>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
接下来我们在router文件夹下的index.js文件中设置路由。
首先,为了实现懒加载,我们先导入各个组件文件:
const Home = () => import('../views/home/Home.vue')
const Category = () => import('../views/category/Category.vue')
const Detail = () => import('../views/detail/Detail.vue')
const Profile = () => import('../views/profile/Profile.vue')
const ShopCart = () => import('../views/shopcart/ShopCart.vue')
其次,在routes数组中补充路由配置:
const routes = [
{
path: '',
name: 'DefaultHome',
component: Home
},
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/category',
name: 'Category',
component: Category
},
{
path: '/detail',
name: 'Detail',
component: Detail
},
{
path: '/shopcart',
name: 'ShopCart',
component: ShopCart
},
{
path: '/profile',
name: 'Profile',
component: Profile
},
]
制作导航栏
该移动端移动图书商城的导航栏在最底层,分为四个选项“首页-分类-购物车-我的”,因此,我们直接在App.vue组件中实现。
在阿里下载好图标后,导入到页面中,由于路由跳转在上面已经实现,故接下来关键只剩两步了:
- 使用flex布局使四个选项在同一行均匀显示,呈现分栏状;
- 使用fixed定位,将导航栏置于页面最底部。
App.vue组件内容如下:
<template>
<div>
<router-view />
<div id="nav">
<router-link class="tab-bar-item" to="/">
<div class="icon"><i class="iconfont icon-shouye"></i></div>
<div>首页</div>
</router-link>
<router-link class="tab-bar-item" to="/category">
<div class="icon"><i class="iconfont icon-fenlei"></i></div>
<div>分类</div>
</router-link>
<router-link class="tab-bar-item" to="/shopcart">
<div class="icon"><i class="iconfont icon-gouwuche"></i></div>
<div>购物车</div>
</router-link>
<router-link class="tab-bar-item" to="/profile">
<div class="icon"><i class="iconfont icon-xiazai"></i></div>
<div>我的</div>
</router-link>
</div>
</div>
</template>
<style lang="scss">
@import 'assets/css/base.css';
@import 'assets/css/iconfont.css';
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
background-color: #f6f6f6;
display: flex;
position: fixed;
left: 0;
right: 0;
bottom: 0;
box-shadow: 0 -3px 1px rgba(100, 100, 100, 0.1);
a {
color: var(--color-text);
&.router-link-exact-active {
color: #42b983;
}
}
.tab-bar-item {
flex: 1;
text-align: center;
height: 50px;
font-size: var(--font-size);
}
.tab-bar-item .icon {
width: 24px;
height: 24px;
margin-top: 3px;
display: inline-block;
vertical-align: middle;
}
}
</style>
首页推荐商品组件的实现
首页推荐商品组件,即四本横向排列的书籍,我们单独把这一部分作为一个组件(.vue)来实现。
在view文件夹的home/ChildComps文件夹下新建Recommend.vue文件,视图部分肯定要用到flex布局:
<!--Recommend.vue-->
<template>
<div class="recommend">
<div class="recommend-item" v-for="item in recommends.slice(0, 4)" :key="item.id">
<a href="" @click.prevent="goD(item.id)">
<img src="~assets/images/youshi1.png" alt="">
<div>{{ item.title }}</div>
</a>
</div>
</div>
</template>
<style lang="scss" scoped>
.recommend {
display: flex;
width: 100%;
text-align: center;
padding: 15px 0 30px;
border-bottom: 8px solid #eee;
font-size: 12px;
}
.recommend-item {
flex: 1;
img {
width: 70px;
height: 70px;
margin-bottom: 10px;
}
}
</style>
数据来自网络请求,也即获取首页数据,这网络请求的相关代码已经封装在了network/home.js中了;我们在Home组件中发起请求,获取首页中需要的全部信息,然后将推荐的书籍信息传递给Recommend子组件——使用props属性。
props用法
在vue3中,父组件传递数据给子组件,需要用到props。而父组件传过来的数据,子组件的模板、setup函数中的引入方式略有不同。
模板使用props
与setup同级使用props配置项即可。此时,也有两种常见的用法:
字符串数组写法
<script> export default { props: ['title', 'author'], setup() {...} } </script>
上面的props配置项数组中,每一个字符串中的内容都是父组件传来的数据变量名(title,author)。
对象写法
对象写法允许我们在指定传来的属性名同时,指定它需要传递的类型,以及是否必需,默认值等等。
<script> export default { props: { title: { type: String, required: true, }, author: { type: String, default() { return '' } } } } </script>
setup函数使用props
setup函数接收两个参数props,context,其中第一个参数就可以接收到父组件传来的数据,以供在setup函数中使用。
<script>
export default {
setup(props, context) {
...
}
}
</script>
首页选项卡的实现
注意到首页的选项卡并不是首页独有的,而是出现在多个组件中的,因此可以作为公用组件出现在components目录下:在components/common下新建tabControl文件夹,在其中新建tabControl.vue文件,用于制作选项卡。
<template>
<div class="tab-control">
<div
class="tab-control-item"
v-for="(item, index) in titles"
:key="index"
@click="itemClick(index)"
:class="{ active: index==currentIndex }"
>
<span>{{ item }}</span>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'TabControl',
props: {
titles: {
type: Array,
default() {
return []
},
},
},
setup() {
let currentIndex = ref(0)
// 当点击某一个选项卡时,把active类赋给它
const itemClick = (index) => {
currentIndex.value = index
}
return {
currentIndex,
itemClick,
}
},
}
</script>
<style scoped lang="scss">
.tab-control {
display: flex;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 14px;
width: 100%;
.tab-control-item {
flex: 1;
span {
padding: 5px;
}
}
.active {
color: var(--color-tint);
span {
border-bottom: 3px solid var(--color-tint);
}
}
}
</style>
虽然首页中的选项卡只有三项,但是其他组件中的选项卡跟首页并不相同,因此使用组件传值的方式,用变量接收选项卡数据,同时使用v-for指令动态生成若干选项卡。
样式方面,对选项卡进行flex布局设置,另外,由于点击某一个选项卡会触发特殊样式,因此对样式使用属性绑定写法:
:class="{ active: index==currentIndex }"
而currentIndex是设置的标记变量,用于记录当前被点击的选项卡序号,index是每一个选项卡固有的序号属性,两者比对即可实现对样式是否触发的检验。
接下来,我们需要实现点击某一个选项卡后,显示该项下的内容。由于选项卡是子组件TabControl.vue实现的,但是该选项卡要展示的内容却是在Home组件中显示的,因此涉及了子组件向父组件传递数据。
emit
emit可以实现子组件与父组件的通信。emit(触发),当子组件中完成某个操作时,调用回调函数,触发(emit)一个自定义事件,该API可以携带多个需要传给父组件的参数:
// 子组件中的JavaScript代码片段
setup(props, { emit }) {
let currentIndex = ref(0)
// 当点击某一个选项卡时,把active类赋给它,并触发tabClick事件
const itemClick = (index) => {
currentIndex.value = index
emit('tabClick', index)
}
return {
currentIndex,
itemClick,
}
},
接下来把目光转移到父组件:
在父组件中,我们需要自定义上述事件(即自定义事件),并给该自定义事件绑定一个回调函数,它可以以参数形式接收子组件传来的数据:
<template>
<div>
...
<TabControl @tabClick="tabClick" :titles="['畅销', '新书', '精选']"></TabControl>
</div>
</template>
<script>
...
export default {
name: 'Home',
components: {
NavBar,
Recommend,
TabControl,
},
setup() {
...
const tabClick = (index) => {
console.log(index);
}
return {
...,
tabClick,
}
}
}
</script>
<style scoped>
.banners img {
width: 100%;
height: auto;
margin-top: 45px;
}
</style>
首页选项卡数据切换
首页组件(Home.vue)中获取后台数据涉及了三个接口,于是在首页组件中定义一个goods对象,用它保存首页选项卡三个选项各自对应的数据(畅销、新书、精选),而上面已经实现了子组件将被点击的选项序号传递给父组件,故在父组件中,只需要实现将对应的数据再传给子组件即可。
分析父组件的JavaScript代码块:
首先,有一个变量保存当前点击的选项卡内容;
let currentType = ref('sales')
我们把目光再次回到之前定义过的tabClick回调函数(点击选项卡时触发):
const tabClick = (index) => {
console.log(index)
let types = ['sales', 'new', 'recommend']
currentType.value = types[index]
}
注意,这个tabClick函数就是Home父组件中自定义事件tabClick绑定的回调函数。
获取三个选项卡需要的后台数据,也即发送三个网络请求,并保存结果,当然在此之前,需要在network文件夹下的home.js文件中封装好相关的网络请求函数。
// network/home.js
// 获取首页三个选项卡涉及的所有商品数据
export function getHomeGoods(type='sales', page=1) {
return request({
url: '/api/index?'+type+'=1&page='+page
})
}
接下来就可以在Home组件中使用了。
// Home.vue中的JavaScript代码
// 商品列表数据模型
const goods = reactive({
sales: {
page: 0,
list: [],
},
new: {
page: 0,
list: [],
},
recommend: {
page: 0,
list: [],
},
})
// 用于记录当前点击的选项卡内容
let currentType = ref('sales')
// 展示当前点击的选项卡的数据,发送给子组件
const showGoods = computed(() => {
return goods[currentType.value].list
})
onMounted(() => {
getHomeAllData().then((res) => {
recommends.value = res.goods.data
})
// 按销量排行
getHomeGoods('sales').then((res) => {
goods.sales.list = res.goods.data
})
// 按推荐排行
getHomeGoods('recommend').then((res) => {
goods.recommend.list = res.goods.data
})
// 按新品排行
getHomeGoods('new').then((res) => {
goods.new.list = res.goods.data
})
})
上拉加载更多数据
需要用到移动端滚动条的第三方库——BetterScroll。
在首页的选项卡中,由于是移动端,不可能像PC端点击分页器获取更多数据,而是通过往上滑动,加载更多数据,因此,需要对滑动事件进行优化、处理(绑定网络请求)。Home组件的JavaScript代码如下:
// 创建BetterScroll对象
bscroll = new BScroll(document.querySelector('.wrapper'), {
probeType: 3, // 只要在运动,就触发scroll事件
click: true, // 是否允许点击
pullUpLoad: true, // 是否上拉加载更多,true为是
})
// 绑定滚动的回调函数
bscroll.on('scroll', (position) => {})
// 上拉加载数据,触发pullingUp
bscroll.on('pullingUp', () => {
console.log('加载更多')
bscroll.refresh()
})
上拉情况下的导航条固定
由于首页的选项卡组件使用了固定定位,但是这是当往下滑动时候的布局样式(一直往下滑动获取新的书籍信息时,此时选项卡固定在页面最上方),对此,我们单独再复制一份选项卡组件,使用v-show指定控制它的显示与否;而显示条件需要我们获取到临界高度,一旦大于这个高度时,就让组件固定展示。
// Home.vue中的JavaScript代码片段
// 控制选项卡是否被固定
let isTabFixed = ref(false)
// 绑定滚动的回调函数
bscroll.on('scroll', (position) => {
isTabFixed.value = -position.y>banref.value.offsetHeight
})