Vue – 尚品汇前台开发实例

简介

本文主要讲解尚硅谷《尚品汇》实践课程中的前台开发实例步骤,仅供后续开发资料查询使用。

 

项目搭建

使用Vue CLi 创建项目

vue create sph

 

配置路径映射

在js.config.json文件中,加入路径映射

{
  "compilerOptions": {
    "baseUrl":"./"
    "paths": {
      // 映射所有目录的src由 @ 表示
      "@/*": ["src/*"]
    },
  }
  // 排除一些第三方组件的src目录
  "exclude":["node_modules", "dist"]
}

 

主体结构路由分析

一个网站田【上】、【中】【下】、三部分组成,

【上】:包含顶栏、搜索框、三组联动菜单、logo等组成,定义为Header组件

【中】:包含多个页面的内容,其中主要是Home首页内容,Register注册页内容、Login登陆页内容、Search搜索结果页内容

【下】:包含Footer页尾信息,定义为Footer

其中主要路由页面分别,

/home => 主页(包含上和下)

/register => 注册页(包含上,不包含下)

/search => 搜索结果页(包含上和下)

/login => 登陆页(包含上,不包含下)

 

组件开发

拆分Header和Footer组件

把HTML中的头部和尾部结构拆分出来,并创建对应的Vue组件

Header.vue初步分拆后的代码

<template>
  <!-- 头部 -->
  <header class="header">
    <!-- 头部的第一行 -->
    <div class="top">
      <div class="container">
        <div class="loginList">
          <p>尚品汇欢迎您!</p>
          <p>
            <span>请</span>
            <a href="###">登录</a>
            <a href="###" class="register">免费注册</a>
          </p>
        </div>
        <div class="typeList">
          <a href="###">我的订单</a>
          <a href="###">我的购物车</a>
          <a href="###">我的尚品汇</a>
          <a href="###">尚品汇会员</a>
          <a href="###">企业采购</a>
          <a href="###">关注尚品汇</a>
          <a href="###">合作招商</a>
          <a href="###">商家后台</a>
        </div>
      </div>
    </div>
    <!--头部第二行 搜索区域-->
    <div class="bottom">
      <h1 class="logoArea">
        <a class="logo" title="尚品汇" href="###" target="_blank">
          <img src="./images/logo.png" alt="">
        </a>
      </h1>
      <div class="searchArea">
        <form action="###" class="searchForm">
          <input type="text" id="autocomplete" class="input-error input-xxlarge"/>
          <button class="sui-btn btn-xlarge btn-danger" type="button">搜索</button>
        </form>
      </div>
    </div>
  </header>
</template>

<script>
export default {
  name: "Header"
}
</script>

<style lang="less" scoped>
.header {
  & > .top {
    background-color: #eaeaea;
    height: 30px;
    line-height: 30px;

    .container {
      width: 1200px;
      margin: 0 auto;
      overflow: hidden;

      .loginList {
        float: left;

        p {
          float: left;
          margin-right: 10px;

          .register {
            border-left: 1px solid #b3aeae;
            padding: 0 5px;
            margin-left: 5px;
          }
        }
      }

      .typeList {
        float: right;

        a {
          padding: 0 10px;

          & + a {
            border-left: 1px solid #b3aeae;
          }
        }

      }

    }
  }

  & > .bottom {
    width: 1200px;
    margin: 0 auto;
    overflow: hidden;

    .logoArea {
      float: left;

      .logo {
        img {
          width: 175px;
          margin: 25px 45px;
        }
      }
    }

    .searchArea {
      float: right;
      margin-top: 35px;

      .searchForm {
        overflow: hidden;

        input {
          box-sizing: border-box;
          width: 490px;
          height: 32px;
          padding: 0px 4px;
          border: 2px solid #ea4a36;
          float: left;

          &:focus {
            outline: none;
          }
        }

        button {
          height: 32px;
          width: 68px;
          background-color: #ea4a36;
          border: none;
          color: #fff;
          float: left;
          cursor: pointer;

          &:focus {
            outline: none;
          }
        }
      }
    }
  }
}
</style>

 

 

Footer初步分拆后的代码

<template>
  <!-- 底部 -->
  <div class="footer">
    <div class="footer-container">
      <div class="footerList">
        <div class="footerItem">
          <h4>购物指南</h4>
          <ul class="footerItemCon">
            <li>购物流程</li>
            <li>会员介绍</li>
            <li>生活旅行/团购</li>
            <li>常见问题</li>
            <li>购物指南</li>
          </ul>

        </div>
        <div class="footerItem">
          <h4>配送方式</h4>
          <ul class="footerItemCon">
            <li>上门自提</li>
            <li>211限时达</li>
            <li>配送服务查询</li>
            <li>配送费收取标准</li>
            <li>海外配送</li>
          </ul>
        </div>
        <div class="footerItem">
          <h4>支付方式</h4>
          <ul class="footerItemCon">
            <li>货到付款</li>
            <li>在线支付</li>
            <li>分期付款</li>
            <li>邮局汇款</li>
            <li>公司转账</li>
          </ul>
        </div>
        <div class="footerItem">
          <h4>售后服务</h4>
          <ul class="footerItemCon">
            <li>售后政策</li>
            <li>价格保护</li>
            <li>退款说明</li>
            <li>返修/退换货</li>
            <li>取消订单</li>
          </ul>
        </div>
        <div class="footerItem">
          <h4>特色服务</h4>
          <ul class="footerItemCon">
            <li>夺宝岛</li>
            <li>DIY装机</li>
            <li>延保服务</li>
            <li>尚品汇E卡</li>
            <li>尚品汇通信</li>
          </ul>
        </div>
        <div class="footerItem">
          <h4>帮助中心</h4>
          <img src="./images/wx_cz.jpg">
        </div>
      </div>
      <div class="copyright">
        <ul class="helpLink">
          <li>关于我们
            <span class="space"></span>
          </li>
          <li>联系我们
            <span class="space"></span>
          </li>
          <li>关于我们
            <span class="space"></span>
          </li>
          <li>商家入驻
            <span class="space"></span>
          </li>
          <li>营销中心
            <span class="space"></span>
          </li>
          <li>友情链接
            <span class="space"></span>
          </li>
          <li>关于我们
            <span class="space"></span>
          </li>
          <li>营销中心
            <span class="space"></span>
          </li>
          <li>友情链接
            <span class="space"></span>
          </li>
          <li>关于我们</li>
        </ul>
        <p>地址:北京市昌平区宏福科技园综合楼6层</p>
        <p>京ICP备19006430号</p>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Footer"
}
</script>

<style lang="less" scoped>
.footer {
  background-color: #eaeaea;

  .footer-container {
    width: 1200px;
    margin: 0 auto;
    padding: 0 15px;

    .footerList {
      padding: 20px;
      border-bottom: 1px solid #e4e1e1;
      border-top: 1px solid #e4e1e1;
      overflow: hidden;
      padding-left: 40px;

      .footerItem {
        width: 16.6666667%;
        float: left;

        h4 {
          font-size: 14px;
        }

        .footerItemCon {
          li {
            line-height: 18px;
          }
        }

        &:last-child img {
          width: 121px;
        }
      }
    }

    .copyright {
      padding: 20px;

      .helpLink {
        text-align: center;

        li {
          display: inline;

          .space {
            border-left: 1px solid #666;
            width: 1px;
            height: 13px;
            background: #666;
            margin: 8px 10px;
          }
        }
      }

      p {
        margin: 10px 0;
        text-align: center;
      }
    }
  }
}


</style>

注意:要先把默认css进行清理,否则css默认样式会影响自定义样式。

 

 

路由配置

路由基本知识

路由规则中,每一个路由规则为一个对象,每个路由对象中有常用几个属性

path => 路由路径,定义路由在url中的路径

name => 路由名称,使用编程式路由导航时,需要指定name名称来判断跳转的路由

component => 路由页面,路由跳转后的页面组件

meta => 路由元信息,它用于把一些特定的信息传递给路由页面,如用于判断页面某些组件是否需要隐藏等(请求参数以外的内部使用的参数)

props => 是否传递参数,props传递参数有三种方法

1.使用boolean型,定义是否允许传参,true时,则支持params参数传到路由页面中

2.使用对象型,定义传参,可在路由页面中使用props接收

3.使用函数方法,函数中返回一个对象,可在路由页面中使用props接收

 

参数占位符

当路由组件中需要接收参数时,参数分为两种,一种是params参数,一种是query参数

params参数是路径参数,如“/search/aaa/bbb”,其中包含了三个params参数(如果search是path,则只有两个params参数)

params参数需要使用占位符来获得参数值,如

{
    name: "search"
    path: "/search/:keyword",
    ...
},

其中 :keyword 则是这个params参数的占位符,可以通过 this.$route.params.keyword 来获得占位符接收的参数值

 

 

设置可传可不传params参数

针对params参数,有可能会出现不传参的情况,如下面的传参,则会出现问题:

this.$router.push({
name: "search"
// 传出去为空参
params:{}
})

// 路由配置
{
    name: "search"
    path: "/search/:keyword",
    ...
},

此时,path 会接收失败。

要解决这个问题,可以在占位符后面加上【?】来表示,该params参数可传可不传

this.$router.push({
name: "search"
params:{}
})

{
    name: "search"
    path: "/search/:keyword?",
    ...
},

 

 

query参数是查询参数,如“/search?key=value”,其中的key和value则是query参数

 

 

配置Home页面的路由

    {
        path: '/home',
        name: 'home',
        component: Home,
        // 使用元信息定义home页面需要显示头和尾页
        meta: {
            header: true,
            footer: true
        }
    },

这里指示,需要访问“/home”地址时,才会跳转路由,因此,对于“/”根地址时,应当也需要跳转到“home”页面,使用重定向路由

{
        // 重定向到首页
        path: "/",
        redirect: "/home"
    }

 

 

配置Search页面的路由

{
        // 对于params参数允许可传可不传的问题,可以在param参数上加上【?】,即可表示可传可不传
        path: "/search/:keyword?",
        name: "search",
        component: Search,
        meta: {
            header: true,
            footer: true
        },
        /**
         * 关于路由传参数到路由页面
         * 传参到路由页面有三种方式
         * 1.使用boolean型,定义是否允许传参,true时,则支持params参数传到路由页面中
         * 2.使用对象型,定义传参,可在路由页面中使用props接收
         * 3.使用函数方法,函数中返回一个对象,可在路由页面中使用props接收
         */
        // 第一种
        // props: true

        // 第二种
        // props: {
        //     keyword: this.$route.params.keywork,
        //     queryKey: this.$route.query.k
        // }

        // 第三种(常用)
        props: ($route) => {
            return {
                keyword: this.$route.params.keywork,
                queryKey: this.$route.query.k
            }
        }
    },

 

 

解决NavigationDuplicated报错问题

当在一个路由页面中,再次跳转同一个路田页面时,会弹了【NavigationDuplicated】报错信息。

问题原因:

从Vue-Router 3 开始,Vue-Router引入了Promise的异步运行机制,所有的 push 或 replace 等跳转,都使用了异步操作,因此,push()和 replace() 方法,所执行后返回的结果均为 Promise 对象。

因此,push方法应提供 resolve 方法,和 reject 方法,来表示,当页面跳转成功,或失败时的执行情况

而我们在代码中,并没有指定 resolve 方法和 reject 方法,在同一个页面中跳转,会产生Promise报错,其本身只是因为 resolve 方法和 reject 方法没有被定义,但是 push 跳转还是能正常使用的。

// 按正常来说,push 是一个 Promise 方法,因此它包含 resolve 方法和 reject 方法
this.$router.push(location, resolve, reject);

 

解决方法一:在push 方法中,添加对push方法的 resolve 方法和 reject 方法

this.$router.push(location, () => { },  () => { });

 

解决方法二:重写 push 方法,使得 resolve 方法和 reject 方法在任何时候都能被调用

router.js 文件

/**
 * 在VueRouter使用之前,重写 push 和 replace 方法
 * 1.先保存好原来的push函数方法
 * 2.重写新的push方法
 * 3.调用原来的push函数方法,并添加resolve 方法和 reject 方法
 */
// 1.保存原来的函数方法

const originPush = VueRouter.prototype.push;
const originReplace = VueRouter.prototype.replace;

// 2.重写新的对应方法,3.调用原来的方法
VueRouter.prototype.push = function (location, resolve, reject) {
    if (resolve && reject) {
        originPush.call(this, location, resolve, reject);
    } else {
        console.log(this);
        originPush.call(this, location, () => {}, () => {});
    }

}
VueRouter.prototype.replace = function (location, resolve, reject) {
    if (resolve && reject) {
        originReplace.call(this, location, resolve, reject);
    } else {
        originReplace.call(this, () => {}, () => {});
    }
}

注意:有关 this 上下文的问题

箭头函数中没有自己的 this ,因此,箭头函数中的 this 则往上查找,本例中的外层是 window,如果使用箭头函数定义,则this 为 window

function 函数有自己的 this,其 this 值是调用 function 函数者,本例中调用 push 方法的是 Vue.$router,因此如果使用 function 来定义函数,则函数体内的 this 为 VueRouter 对象。

 

有关 Promise 的运行机制可参考对应文章。

 

Axios 二次封装

为了在发生请求时让网页顶部产生一条进度条,我们需要配合nprogress插件和Axios拦截器进行控制。

控制原理:通过定义Axios拦截器在Axios发生请求开始和结束时,调用一个拦截器方法,其间,可以在拦截器方法中调用nprogress进度条插件方法。

request.js > 用于重写Axios的js

/**
 * 配置Axios拦截器,用于调用顶栏进度条
 */
// 引入nprogress进度条插件
import nprogress from "nprogress"
// 引入nprogress进度条样式
import "nprogress/nprogress.css"
// 请求拦截过滤器
requests.interceptors.request.use((config)=>{

    // 进行请求成功时,对nprogress进度条进行设置
    nprogress.start();
    return config;
},(error)=>{
    nprogress.done();
    // 请求失败时
    return Promise.reject("请求失败")
})

// 响应拦截过滤器
requests.interceptors.response.use((success)=>{
    nprogress.done();
    // 当响应成功时
    return success;
},(error)=>{
    nprogress.done();
    // 当响应失败时
    return Promise.reject(new Error("响应失败"));
})

 

请求接口统一管理

我们的项目中,需要用到Axios请求的地方非常多,如果我们每次发请求,都要在调用Axios的地方导入Axios和添加api请求地址的话

那么项目中的api请求将耦合在各个组件中,后期维护非常麻烦。

因此,我们可以把所有的api请求,写到一个js文件中统一存放并暴露,需要调用api请求时,只需要直接引入该js文件,调用对应的请求方法即可,如下面的请求模板

// 引入重新封装后的Axios
import requests from "@/api/request";

// 获取三级联动菜单
export const reqGetBaseCategoryList = () => requests({url: "/product/getBaseCategoryList", method: "GET"});
// 后续可以在文采里添加更多网络请求方法

 

TypeNav 三级联动菜单开发

采用VueX方式存储菜单

三级联动菜单需要通过网络请求服务器取得,而这些数据,可以通过Vuex来做管理。

Vuex的模块化开发,详情可查看以下文章

VueX 是专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 Vue 应用中多个组件的共享状态进行集中式的管理(读、写),也是一种组件间通信的方式,且适用于任意组件间的通信。
2022-11-22
store.js VueX总配置文件

// 引入 Vue
import Vue from 'vue'
// 引入 VueX
import Vuex from 'vuex'
// 使用 VueX 插件
Vue.use(Vuex)

/**
 * 引入模块仓库
 */
import home from "@/store/home/home";
import search from "@/store/search/search";
import detail from "@/store/detail/detail";
import TypeNav from "@/store/TypeNav/TypeNav";

export default new Vuex.Store({
    modules: {
        m_Home: home,
        m_Search: search,
        m_Detail: detail,
        m_TypeNav: TypeNav
    }
})

三级联动被分到一个模块文件中。其中,获取三级菜单操作,在Vuex的actions里进行,也可以在 TypeNav.vue 中的 mounted() 方法中先请求,再对Vuex进行任务派发。

import {reqGetBaseCategoryList} from "@/api/apis";

/**
 * TypeNav 三级联动菜单模块 VueX 仓库
 */

const state = {
    categoryList: []
};
const actions = {
    async getCategoryList(context, props) {
        let result = await reqGetBaseCategoryList();
        if (result.data.code === 200){
            context.commit("GETCATEGORYLIST", result.data);
        }
    }
};
const mutations = {
    GETCATEGORYLIST(state, result) {
        state.categoryList = result;
    }
};
const getters = {
    getCategoryList(state) {
        return state.categoryList;
    }
};

export default {
    namespaced: true,
    state,
    actions,
    mutations,
    getters
}

 

防抖与节流

三级联动菜单中,当鼠标快速滑动菜单时,会使得浏览器不停的渲染元素引起页面卡顿。

解决方法最好就是通过节流,强制使浏览器必须在每间隔内执行一次渲染

有关防抖与节流,可以参考以下文章

简介 防抖与节流在企业项目用得非常多,它可以保证网页在浏览器中不因用户的故意或过失操作而引起的性能……
2023-02-14
  methods: {
    // 设置节流
    mouseenter: throttle(function (index) {
      this.currentIndex = index;
    }, 50),
}

当鼠标进入菜单时,产生节流地渲染。

 

菜单导航跳转

当点击三级菜单中,应导航到search搜索页。

问题一:如果在菜单的所有链接中,都使用 router-link 组件跳转,那么在非常多菜单的情况下,Vue 需要渲染非常多的 router-link 组件,占用资源

问题二:不使用 router-link 而是保持使用 a 标签,同时在 a 标签中使用 @click 来绑定跳转方法,这样同样会使得多个 a 标签绑定方法。

解决方法:通过事件传递,和 a 标签判断,来判断用户是否点击了 a 标签

事件传递:指的是网页中的嵌套 div,当父 div 中被点击时,点击事件,除了会触发父 div 外,还会往下传到子 div 中(前提是鼠标点击的位置在子 div 范围内)

那么我们就可以把 @click 事件绑在父 div 上,然后在 a 标签中添加自定义属性,来区分鼠标是否点击了 a 标签,且点击了那个级的 a 标签

<h2 class="all">全部商品分类</h2>
        <div class="sort">
          <!-- 这里需要使用点击链接跳转路由 -->
          <!-- 但是如果在每一个 a 标签中都加入一个 router-link 的话,会因元素过多大量占用内存和cpu资源 -->
          <!-- goSearch 方法通过事件传递,把整个 div 都能触发 -->
          <!-- 然后在每一个 a 标签中,定义自定义属性,来判断鼠标点击的是否为 a 标签,且判断点击的是几级菜单 -->
          <div class="all-sort-list2" @click="goSearch">
            <div class="item" v-for="(category1,index) in getCategoryList.data" :key="category1.categoryId">
              <h3 @mouseenter="mouseenter(index)" :class="{cur:index === currentIndex}">
                <a :data-category="category1.categoryName" :data-categoryId="1">{{ category1.categoryName }}</a>
              </h3>
              <div class="item-list clearfix" :style="{ display:index === currentIndex ? 'block' : 'none' }">
                <div class="subitem" v-for="category2 in category1.categoryChild" :key="category2.categoryId">
                  <dl class="fore">
                    <dt>
                      <a :data-category="category2.categoryName" :data-categoryId="2">{{ category2.categoryName }}</a>
                    </dt>
                    <dd>
                      <em v-for="category3 in category2.categoryChild" :key="category3.categoryChild">
                        <a :data-category="category3.categoryName" :data-categoryId="3">{{ category3.categoryName }}</a>
                      </em>
                    </dd>
                  </dl>
                </div>
              </div>
            </div>
          </div>
        </div>

通过在父 div 中绑定 @click 事件,这样只要鼠标点击父 div 中的任意位置,都会产生事件触发

父 div 中有多种元素,为了判断鼠标点击时,是否点击的是 a 标签,可以使用 :data-xxx 来定义自定义属性,在方法中,对 data-xxx 进行判断,就可以判断出鼠标是否点击了 a 标签

 

goSearch(event) {
      //  通过 event.target 找到鼠标点击目标,取出目标中是否包含 data-category 元素
      //  从 event.target.dataset 中找到自定义属性
      //  如果存在 data-category 元素,则说明,点击的是 a 标签
      const target = event.target; 
      //  是否存在 categoryid 如果是,说明点击的是 a 标签,如果为 undefined 说明点击的是父 div 的其它元素
      if (target.dataset.categoryid) {
        const categoryLevel = target.dataset.categoryid;
        const categoryName = target.dataset.category;
        const location = {name: "search"};
        let query = {}
        switch (categoryLevel){
          case "1":
            query.category1Id = categoryName;
            break;
          case "2":
            query.category2Id = categoryName;
            break;
          case "3":
            query.category3Id = categoryName;
            break;
        }
        location.query = query;
        this.$router.push(location)
      }
    }

 

 

如果您喜欢本站,点击这儿不花一分钱捐赠本站

这些信息可能会帮助到你: 下载帮助 | 报毒说明 | 进站必看

修改版本安卓软件,加群提示为修改者自留,非本站信息,注意鉴别

THE END
分享
二维码
打赏
海报
Vue – 尚品汇前台开发实例
简介 本文主要讲解尚硅谷《尚品汇》实践课程中的前台开发实例步骤,仅供后续开发资料查询使用。   项目搭建 使用Vue CLi 创建项目 vue create sph   配置路径映射 在js.config.json……
<<上一篇
下一篇>>