玩命加载中 . . .

Vue3移动图书商城项目实战


项目结构

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
})

文章作者: 鹿卿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 鹿卿 !
评论
  目录