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的模块化开发,详情可查看以下文章
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
}
防抖与节流
三级联动菜单中,当鼠标快速滑动菜单时,会使得浏览器不停的渲染元素引起页面卡顿。
解决方法最好就是通过节流,强制使浏览器必须在每间隔内执行一次渲染
有关防抖与节流,可以参考以下文章
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)
}
}
共有 0 条评论