目录
2.2 关闭eslint校验工具(不关闭会有各种规范,不按照规范就会报错)
5.1创建router文件夹,并创建index.js进行路由配置,最终在main.js中引入注册
28.3、品牌信息----SearchSelector子组件传参及面包屑操作
29.1谁应该有类名:通过order属性值当中包含1(综合)还是2(价格)
29.4点击‘综合’或‘价格’的触发函数changeOrder
个人git仓库
前端Vue核心
开发一个前端模块可以概括为以下几个步骤:
(1)写静态页面、拆分为静态组件;
(2)发请求(API);
(3)vuex(actions、mutations、state三连操作);
(4)组件获取仓库数据,动态展示;
1、vue文件目录分析
public文件夹:静态资源,webpack进行打包的时候会原封不动打包到dist文件夹中。
pubilc/index.html是一个模板文件,
作用是生成项目的入口文件,webpack打包的js,css也会自动注入到该页面中。我们浏览器访问项目的时候就会默认打开生成好的index.html。
src文件夹(程序员代码文件夹)
assets: 存放公用的静态资源
components: 非路由组件(全局组件),其他组件放在views或者pages文件夹中
App.vue: 唯一的跟组件
main.js: 程序入口文件,最先执行的文件
babel.config.js: 配置文件(babel相关)
package.json: 项目的详细信息记录
package-lock.json: 缓存性文件(各种包的来源)
2、项目配置
2.1 项目运行,浏览器自动打开
package.json
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
2.2 关闭eslint校验工具(不关闭会有各种规范,不按照规范就会报错)
- 根目录下创建vue.config.js,进行配置
module.exports = { //关闭eslint lintOnSave: false }
2.3 src文件夹配置别名
创建jsconfig.json,用@/代替src/,exclude表示不可以使用该别名的文件
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"exclude": ["node_modules", "dist"]
}
3、组件页面样式
组件页面的样式使用的是less样式,浏览器不识别该样式,需要下载相关依赖
npm install --save less less-loader@5
如果想让组件识别less样式,则在组件中设置
<style lang="less" scoped>
4、清除vue页面默认的样式
vue是单页面开发,我们只需要修改public下的index.html文件,引入reset.css样式
<link rel="stylesheet" href="reset.css">
- reset.css代码内容
/* @import "./iconfont.css"; */
/* 清除内外边距 */
body,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
blockquote,
dl,
dt,
dd,
ul,
ol,
li,
pre,
fieldset,
lengend,
button,
input,
textarea,
th,
td {
margin: 0;
padding: 0;
}
/* 设置默认字体 */
body,
button,
input,
select,
textarea {
/* for ie */
/*font: 12px/1 Tahoma, Helvetica, Arial, "宋体", sans-serif;*/
font: 12px/1.3 "Microsoft YaHei", Tahoma, Helvetica, Arial, "\5b8b\4f53", sans-serif;
/* 用 ascii 字符表示,使得在任何编码下都无问题 */
color: #333;
}
h1 {
font-size: 18px;
/* 18px / 12px = 1.5 */
}
h2 {
font-size: 16px;
}
h3 {
font-size: 14px;
}
h4,
h5,
h6 {
font-size: 100%;
}
address,
cite,
dfn,
em,
var,
i {
font-style: normal;
}
/* 将斜体扶正 */
b,
strong {
font-weight: normal;
}
/* 将粗体扶细 */
code,
kbd,
pre,
samp,
tt {
font-family: "Courier New", Courier, monospace;
}
/* 统一等宽字体 */
small {
font-size: 12px;
}
/* 小于 12px 的中文很难阅读,让 small 正常化 */
/* 重置列表元素 */
ul,
ol {
list-style: none;
}
/* 重置文本格式元素 */
a {
text-decoration: none;
color: #666;
}
/* 重置表单元素 */
legend {
color: #000;
}
/* for ie6 */
fieldset,
img {
border: none;
}
button,
input,
select,
textarea {
font-size: 100%;
/* 使得表单元素在 ie 下能继承字体大小 */
}
/* 重置表格元素 */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* 重置 hr */
hr {
border: none;
height: 1px;
}
.clearFix::after {
content: "";
display: block;
clear: both;
}
/* 让非ie浏览器默认也显示垂直滚动条,防止因滚动条引起的闪烁 */
html {
overflow-y: scroll;
}
a:link:hover {
color: rgb(79, 76, 212) !important;
text-decoration: underline;
}
/* 清除浮动 */
.clearfix::after {
display: block;
height: 0;
content: "";
clear: both;
visibility: hidden;
}
5、views文件夹
在src文件夹下创建views文件夹,并创建路由组件,如下:
5.1创建router文件夹,并创建index.js进行路由配置,最终在main.js中引入注册
// 配置路由的地方
import Vue from 'vue'
import VueRouter from 'vue-router'
// 使用插件
Vue.use(VueRouter)
//引入路由
import Home from '@/views/Home/Home.vue'
import Login from '@/views/Login/Login.vue'
import Register from '@/views/Register/Register.vue'
import Search from '@/views/Search/Search.vue'
//配置路由
export default new VueRouter({
routes: [
//重定向
{ path: '*', redirect: '/home' },
{ path: "/home", component: Home},
{ path: "/login", component: Login },
{ path: "/register", component: Register },
{ path: '/search/', component: Search},
]
})
main.js
import Vue from 'vue'
import App from './App.vue'
//引入路由
import router from '@/router'
new Vue({
render: h => h(App),
//注册路由
router
}).$mount('#app')
5.2 总结
路由组件和非路由组件区别:
- 非路由组件放在components中,路由组件放在pages或views中
- 非路由组件通过标签使用,路由组件通过路由使用
- 在main.js注册玩路由,所有的路由和非路由组件身上都会拥有$router $route属性
- $router:一般进行编程式导航进行路由跳转
- $route: 一般获取路由信息(name path params等)
5.3 路由跳转方式
- 声明式导航router-link标签 ,可以把router-link理解为一个a标签,它 也可以加class修饰
- 编程式导航 :声明式导航能做的编程式都能做,而且还可以处理一些业务
6、footer组件显示与隐藏
footer在登录注册页面是不存在的,所以要隐藏,v-if 或者 v-show
这里使用v-show,因为v-if会频繁的操作dom元素消耗性能,v-show只是通过样式将元素显示或隐藏
配置路由的时候,可以给路由配置元信息meta,
//配置路由
export default new VueRouter({
routes: [
//重定向
{ path: '*', redirect: '/home' },
{ path: "/home", component: Home, meta: { show: true } },
{ path: "/login", component: Login, meta: { show: false } },
{ path: "/register", component: Register, meta: { show: false } },
{ path: '/search/', component: Search, meta: { show: true }, name: "search" },
]
})
在路由的原信息App.vue中定义show属性,用来给v-show赋值,判断是否显示footer组件
7、路由传参
7.1、query、params
- query、params两个属性可以传递参数
query参数:不属于路径当中的一部分,类似于get请求,地址栏表现为 /search?k1=v1&k2=v2
query参数对应的路由信息 path: "/search"
params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位 ,地址栏表现为 /search/v1/v2
params参数对应的路由信息要修改为path: "/search/:keyword?" 这里的/:keyword就是一个params参数的占位符
- params传参问题
(1)、如何指定params参数可传可不传
如果路由path要求传递params参数,但是没有传递,会发现地址栏URL有问题,详情如下:
Search路由项的path已经指定要传一个keyword的params参数,如下所示:
path: "/search/:keyword",
执行下面进行路由跳转的代码:
this.$router.push({name:"Search",query:{keyword:this.keyword}})
当前跳转代码没有传递params参数
地址栏信息:http://localhost:8080/#/?keyword=asd
此时的地址信息少了/search
正常的地址栏信息: http://localhost:8080/#/search?keyword=asd
解决方法:可以通过改变path来指定params参数可传可不传
path: "/search/:keyword?",?表示该参数可传可不传
(2)、由(1)可知params可传可不传,但是如果传递的时空串,如何解决 。
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''}})
出现的问题和1中的问题相同,地址信息少了/search
解决方法: 加入||undefined,当我们传递的参数为空串时地址栏url也可以保持正常
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''||undefined}})
(3)路由组件能不能传递props数据?
可以,但是只能传递params参数,具体知识为props属性 。
7.2、传参方法
- 字符串形式
this.$router.push("/路由名字/"+this.params传参+"?k="+this.query传参)
this.$router.push('/search/' + this.keyword + '?k=' + this.keyword.toUpperCase())
- 模板字符串
this.r o u t e r . p u s h ( " / s e a r c h / + router.push("/search/+router.push("/search/+{this.params传参}?k=${this.query传参}")
this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)
- 对象(常用) 此处的name是路由规则里路由的名字
this.$router.push({name:“路由名字”,params:{传参},query:{传参})。
this.$router.push({ name: 'search', params: { keyword: this.keyword }, query: { k: this.keyword.toUpperCase() } })
以对象方式传参时,如果我们传参中使用了params,只能使用name,不能使用path,如果只是使用query传参,可以使用path 。
7.3、代码整理
- 在Heard.vue找到 "搜素"区域,并添加事件
- return 'keyword' 并构造点击事件跳转函数 goSearch
<script> export default { name: 'Header', data() { return { keyword: '' } }, methods: { //搜索按钮的回调函数:需要想search路由进行跳转 goSearch() { //路由传递参数 // 第一种字符串形式 // this.$router.push('/search/' + this.keyword + '?k=' + this.keyword.toUpperCase()) // 第二种:模板字符串 // this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`) //第三种:对象 常用 此处的name是路由规则里路由的名字 this.$router.push({ name: 'search', params: { keyword: this.keyword }, query: { k: this.keyword.toUpperCase() } }) } } } </script>
8、多次执行相同的push问题
多次执行相同的push问题,控制台会出现警告
例如:使用this.$router.push({name:‘Search’,params:{keyword:"…"||undefined}})时,如果多次执行相同的push,控制台会出现警告。
let result = this.$router.push({name:"Search",query:{keyword:this.keyword}})
console.log(result)
执行一次上面的代码:
多次执行出现警告:
原因:push是一个promise,promise需要传递成功和失败两个参数,我们的push中没有传递。
方法:this.$router.push({name:‘Search’,params:{keyword:"…"||undefined}},()=>{},()=>{})后面两项分别代表执行成功和失败的回调函数。
这种写法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误
push是VueRouter.prototype的一个方法,在router中的index.js重写该方法即可(看不懂也没关系,这是前端面试题)
//先把VueRouter原型对象的push保存一份
let originPush = VueRouter.prototype.push
let originReplace = VueRouter.prototype.push
//重写push|replace
//第一个参数:告诉原来的push方法,你往哪里挑转(传递哪些参数)
//第二个参数:成功的参数
//第三个参数:失败的参数
VueRouter.prototype.push = function(location, resolve, reject) {
if (resolve && reject) {
//call||apply区别
//相同点:都可以调用函数一次,都可以篡改函数的上下文一次
//不同点:call和apply传递参数:call传递参数用逗号隔开,apply方法执行,传递数组
originPush.call(this, location, resolve, reject)
} else { originPush.call(this, location, () => {}, () => {}) }
}
VueRouter.prototype.push = function(location, resolve, reject) {
if (reject && resolve) {
originReplace.call(this, location, resolve, reject)
} else { originReplace.call(this, location, resolve, reject), () => {}, () => {} }
}
9、定义全局组件
我们的三级联动组件是全局组件,全局的配置都需要在main.js中配置
//三级联动组件--全局组件
import TypeNav from '@/components/TypeNav/TypeNav.vue'
//第一个参数:全局组件的名字,第二个参数:哪一个组件
Vue.component(TypeNav.name, TypeNav)
在Home组件中使用该全局组件
<template>
<div>
<!-- 三级联动全局组件已经注册为全局组件,因此不需要引入-->
<TypeNav/>
</div>
</template>
全局组件可以在任一页面中直接使用,不需要导入声明
下面全部商品分类就是三级联动组件
10、Home首页其他组件
<template>
<div>
<!-- 三级联动全局组件:已经注册为全局组件,因此不需要引入 -->
<TypeNav></TypeNav>
<!-- 轮播图列表-->
<List></List>
<!-- 今日推荐-->
<Today></Today>
<!-- 商品排行-->
<Rank></Rank>
<!-- 猜你喜欢-->
<Like></Like>
<!-- 楼层 -->
<Floor></Floor>
<Floor></Floor>
<!-- 商标-->
<Brand></Brand>
</div>
</template>
<script>
import List from '@/views/Home/List/List.vue'
import Today from '@/views/Home/Today/Today.vue'
import Rank from '@/views/Home/Rank/Rank.vue'
import Like from '@/views/Home/Like/Like.vue'
import Floor from '@/views/Home/Floor/Floor.vue'
import Brand from '@/views/Home/Brand/Brand.vue'
export default {
name: 'Home',
components: {
List,
Today,
Rank,
Like,
Floor,
Brand
}
}
</script>
<style lang="less" scoped>
</style>
11、封装axios
axios中文文档,包含详细信息。
在根目录下创建api文件夹,创建request.js文件。
内容如下,当前文件代码还比较少,后续有需求可以增添内容。
//对axios进行二次封装
import axios from 'axios'
// 1.利用axios对象的方法create,去创建一个axios实例
// 2.requests就是axios 只不过稍微配置一下
const requests = axios.create({
//配置对象
// 基础路径,发起请求的时候,路径中会出现api
baseURL: "/api",
// 代表请求超时的时间5s
timeout: 5000,
})
// 请求拦截器:发起请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事
requests.interceptors.request.use((config) => {
// config:配置对象,对象里面有一个属性很重要,headers请求头
return config
})
//响应拦截器
requests.interceptors.response.use((res) => {
// 成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,做一些事情
return res.data
}, (error) => {
// 响应失败的回调函数
return Promise.reject(new Error('请求失败了'))
})
// 对外暴露
export default requests
12、前端通过代理解决跨域问题
在根目录下的vue.config.js中配置,proxy为通过代理解决跨域问题。
我们在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
//关闭eslint
lintOnSave: false,
// 代理跨域
devServer: {
proxy: {
"/api": {
target: "http://gmall-h5-api.atguigu.cn",
// pathRewrite: { "^/api": "" }
}
},
// true 则热更新,false 则手动刷新,默认值为 true
inline: true,
}
})
网站中的webpack.config.js就是vue.config.js文件。
13、请求接口统一封装
在文件夹api中创建index.js文件,用于封装所有请求
将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们的接口比较多时,如果需要修改只需要修改该文件即可。
如下所示:
//当前模块对api接口进行统一管理
import requests from './request'
//三级联动接口
// /api/product / getBaseCategoryList get请求 无参数
// 发请求:axios发请求返回的是Promise对象
export const reqCategoryList = () => {
//发请求
return requests({ url: '/product/getBaseCategoryList', methods: 'get' })
}
当组件想要使用相关请求时,只需要导入相关函数即可,以上图的reqCateGoryList 为例:
import {reqCateGoryList} from './api'
//发起请求
reqCateGoryList();
14、nprogress进度条插件
打开一个页面时,往往会伴随一些请求,并且会在页面上方出现进度条。它的原理时,在我们发起请求的时候开启进度条,在请求成功后关闭进度条,所以只需要在request.js中进行配置。
如下图所示,我们页面加载时发起了一个请求,此时页面上方出现蓝色进度条
对应的request.js设置
//对axios进行二次封装
import axios from 'axios'
//引入进度条
import nprogress from 'nprogress'
//引入进度条样式
import 'nprogress/nprogress.css'
//start:进度条开始 done:进度条结束
// console.log(nprogress);
// 1.利用axios对象的方法create,去创建一个axios实例
// 2.requests就是axios 只不过稍微配置一下
const requests = axios.create({
//配置对象
// 基础路径,发起请求的时候,路径中会出现api
baseURL: "/api",
// 代表请求超时的时间5s
timeout: 5000,
})
// 请求拦截器:发起请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事
requests.interceptors.request.use((config) => {
// config:配置对象,对象里面有一个属性很重要,headers请求头
//进度条开始动
nprogress.start()
return config
})
//响应拦截器
requests.interceptors.response.use((res) => {
// 成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,做一些事情
//进度条结束
nprogress.done()
return res.data
}, (error) => {
// 响应失败的回调函数
return Promise.reject(new Error('请求失败了'))
})
// 对外暴露
export default requests
可以通过修改nprogress.css文件的background来修改进度条颜色。
15、手动引入vuex
- 15.1 首先确保安装了vuex,根目录创建store文件夹,文件夹下创建index.js,内容如下:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
//对外暴露store的一个实例
export default new Vuex.Store({
state:{},
mutations:{},
actions:{},
})
如果想要使用vuex,还要再main.js中引入
main.js:(此处代码包含之前的代码步骤)
(1) 引入文件
(2) 注册store
但凡是在main.js中的Vue实例中注册的实体,在所有的组件中都会有(this.$.实体名)属性
import Vue from 'vue'
import App from './App.vue'
//三级联动组件--全局组件
import TypeNav from '@/components/TypeNav/TypeNav.vue'
//第一个参数:全局组件的名字,第二个参数:哪一个组件
Vue.component(TypeNav.name, TypeNav)
//引入路由
import router from '@/router'
//引入仓库
import store from '@/store'
// Vue.config.productionTip = false
//测试 接口能否使用
// import { reqCategoryList } from '@/api'
// reqCategoryList()
new Vue({
render: h => h(App),
//注册路由
router,
// 注册仓库:组件实例的身上会多一个属性$store属性
store
}).$mount('#app')
但是 为了代码规范,统一管理,复用性,实现模块式开发存储数据,分别创建不同模块的小仓库
- 15.2 在store文件夹中新建home、search等文件夹,并分别新建index.js(小仓库模块)
- 15.3 在store文件夹下的 index.js(大仓库)下引入小仓库模块,并对外暴露
import Vue from 'vue'
import Vuex from 'vuex'
//需要使用一次插件
Vue.use(Vuex)
//引入小仓库
import home from './home'
import search from './search'
//对外暴露store类的一个实例
export default new Vuex.Store({
//实现vuex仓库模块式开发的存储数据
modules: {
home,
search
}
})
- 15.4 配置home的小仓库
//home仓库的数据
import { reqCategoryList } from '@/api'
//state:仓库存储数据的地方
const state = {
//state中的数据默认初始值不要瞎写,服务器返回对象,服务器返回数组. 根据接口返回值初始化的
categoryList: []
}
//mutations:修改state的唯一手段
const mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList = categoryList
}
}
// action: 处理action, 可以书写自己的业务逻辑, 也可以处理异步
const actions = {
// 通过api里面的接口函数调用,向服务器发起请求,获取服务器的数据
async categoryList({ commit }) {
let result = await reqCategoryList()
if (result.code = 200) {
commit('CATEGORYLIST', result.data)
}
}
}
// getters:理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {}
//对外暴露store类的一个实例
export default {
state,
mutations,
actions,
getters
}
针对上述代码,介绍async await使用
如果我们没有封装请求api,而是直接调用axios,就不需要使用async await。
案例:我们将一个axios请求封装为了函数,我们在下面代码中调用了该函数:
import {reqCateGoryList} from '@/api'
export default {
actions:{
categoryList(){
let result = reqCateGoryList()
console.log(result)
}
}
}
浏览器结果
返回了一个promise,证明这是一个promise请求,但是我们想要的是图片中的data数据。
没有将函数封装前我们都会通过then()回调函数拿到服务器返回的数据,现在我们将其封装了,依然可以使用then获取数据,代码如下:
actions:{
categoryList(){
let result = reqCateGoryList().then(
res=>{
console.log("res")
console.log(res)
return res
}
)
console.log("result")
console.log(result)
}
}
结果
由于我们的promis是异步请求,我们发现请求需要花费时间,但是它是异步的,所有后面的console.log(“result”);console.log(result)会先执行,等我们的请求得到响应后,才执行console.log(“res”);console.log(res),这也符合异步的原则,但是我们如果在请求下面啊执行的是将那个请求的结果赋值给某个变量,这样就会导致被赋值的变量先执行,并且赋值为undefine,因为此时promise还没有完成。
所以我们引入了async await,async写在函数名前,await卸载api函数前面。await含义是async标识的函数体内的并且在await标识代码后面的代码先等待await标识的异步请求执行完,再执行。这也使得只有reqCateGoryList执行完,result 得到返回值后,才会执行后面的输出操作。
async categoryList(){
let result = await reqCateGoryList()
console.log("result")
console.log(result)
}
结果
- 15.5 使用时在组件中引入import { mapState } from 'vuex',并进行挂载
computed: {
...mapState({
//右侧需要的是一个函数,当使用计算属性的时候,右侧函数会立即执行一次
//注入一个参数state,其实即为大仓库中的数据
categoryList: state => {
return state.home.categoryList
}
})
}
- 15.6 组件挂载完毕,向服务器发起请求
// 组件挂在完毕,可以向服务器发起请求
mounted() {
//通过vuex发起请求,获取数据,存储在仓库当中
this.$store.dispatch('categoryList');
// 组件挂在完毕,让show属性变为false
this.show=false
},
此时,接口数据调用成功,可以在三级联动中使用v-for动态获取数据 .
Vuex辅助函数
state、actions、mutations、getters的辅助函数使用,当多次访问store中的上述属性时,要使用个属性的辅助函数,可以减少代码量。
在使用上面的函数时,如果需要传递多个参数,需要把多个参数组合为一个对象传入(vuex是不允许多个参数分开传递的)。
async addOrUpdateShopCart({commit},{skuId,skuNum}){
let result = await reqAddOrUpdateShopCart(skuId,skuNum)
console.log(result)
if(result.data === 200){
}
注意:使用action时,函数的第一个参数,必须是{commit},即使不涉及到mutations操作,也必须加上该参数,否则会报错。
16、loadsh插件防抖和节流
在进行窗口的resize、scroll,输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果。
安装lodash插件,该插件提供了防抖和节流的函数,我们可以引入js文件,直接调用。当然也可以自己写防抖和节流的函数
//引入方式是把lodash全部功能函数引入
// import _ from 'lodash'
// 最好的方式是按需加载
import throttle from 'lodash/throttle'
防抖:用户操作很频繁,但是只执行一次,减少业务负担。
节流:用户操作很频繁,但是把频繁的操作变为少量的操作,使浏览器有充分时间解析代码
例如:下面代码就是将changeIndex设置了节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。
methods: {
//鼠标介入修改响应式数据currentIndex属性
//节流 throttle
// throttle回调函数别用箭头函数,可能会出现上下文this
changeIndex:throttle(function(index){
//index鼠标移上某一个以及元素的索引值
this.currentIndex = index
},50),
//index鼠标移出某一个以及元素的索引值
leaveIndex() {
this.currentIndex = -1
}
}
17、编程式导航+事件委托实现路由跳转
如上图所示,三级标签列表有很多,每一个标签都是一个页面链接,我们要实现通过点击表现进行路由跳转。
路由跳转的两种方法:导航式路由,编程式路由。
对于导航式路由,我们有多少个a标签就会生成多少个router-link标签,这样当我们频繁操作时会出现卡顿现象。
对于编程式路由,我们是通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。
上面两种方法无论采用哪一种,都会影响性能。我们提出一种:编程时导航+事件委派 的方式实现路由跳转。事件委派即把子节点的触发事件都委托给父节点。这样只需要一个回调函数goSearch就可以解决。
事件委派问题:
(1)如何确定我们点击的一定是a标签呢?如何保证我们只能通过点击a标签才跳转呢?
(2)如何获取子节点标签的商品名称和商品id(我们是通过商品名称和商品id进行页面跳转的)
解决方法:
- 对于问题1:为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
- 对于问题2:为三个等级的a标签再添加自定义属性data-category1Id、data-category2Id、data-category3Id来获取三个等级a标签的商品id,用于路由跳转。
- 我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex"> <div class="item" v-for="(c1,index) in categoryList" v-show="index!==16" :key="c1.categoryId" :class="{cur:currentIndex===index}"> <h3 @mouseenter="changeIndex(index)" > <a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" >{{c1.categoryName}}</a> </h3> <div class="item-list clearfix" :style="{display:currentIndex===index?'block':'none'}"> <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId"> <dl class="fore"> <dt> <a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{c2.categoryName}}</a> </dt> <dd> <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId"> <a :data-categoryName="c2.categoryName" :data-category3Id="c3.categoryId">{{c3.categoryName}}</a> </em> </dd></dl></div></div></div></div>
注意:event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。
//函数使用
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">
//函数定义
goSearch(event){
console.log(event.target)
}
对应的goSearrch函数
goSearch(event){
//最好的解决方案:编程式导航+事件委派
// 存在一些问题:事件委派,是把全部的子节点【h3 dt dl em】的事件委派给父节点
// 点击a标签的时候,才会进行路由跳转【怎么能确定点击的一定是a标签】
// 存在另外一个问题:即使你能确定店点击的是a标签,如何区分是一级、二级、三级分类的标签
// 第一个问题:把子节点当中a标签,加上自定义属性data-categoryName,其余的子节点是没有的
let element =event.target;
// 获取到当前触发这个事件的节点【h3 dt dl em】需要带有data-categoryname这样的节点【一定是a标签】
// 节点有一个属性dataset属性,可以获取节点的自定义属性与属性值
let {categoryname,category1id,category2id,category3id}=element.dataset;
// 如果标签身上拥有categoryname一定是a标签
if(categoryname){
// 整理路由参数
let location={name:'search'}
console.log(query);
var query ={categoryName:categoryname}
console.log(query);
//一级、二级、三级分类的a标签
if(category1id){
query.category1Id=category1id
}else if(category2id){
query.category2Id=category2id
}else{
query.category3Id=category3id
}
// 整理完参数
location.query=query
// 路由跳转
this.$router.push(location)
}
}
18、为三级联动添加过渡动画--transition
过度动画:前提组件|元素务必有v-if或v-show指令才可以进行过度动画。
实现:跳转到search页,三级联动中 鼠标移入展开全部商品分类,鼠标移出隐藏全部商品分类,并且展开、隐藏时,有过渡动画。
- 在三级联动的一级div上使用v-show,进行展示与隐藏,并定义show的初始值为true
data() {
return {
//存储用户鼠标移上哪一个一级分类
currentIndex: -1,
show:true
}
},
此时,不管在home首页还是search搜索页,三级列表都是展开状态。因此需要在组件挂载完毕后,让show的值变为false,在mounted中加入代码
mounted() {
// 组件挂在完毕,让show属性变为false
// 如果不是Home路由组件,将typeNav进行隐藏
if(this.$route.path!='/home'){
this.show=false
}
},
此时状态变为,home页展开,search隐藏,但是在search中没有鼠标移入移出操作。
- 在三级联动的父节点中,使用事件委派,为其绑定移入、移出事件
鼠标移入enterShow
// 当鼠标移入时,让商品分类typeNav列表进行展示
enterShow(){
this.show=true;
}
鼠标移出leaveShow
// 当鼠标离开时,让商品分类typeNav列表进行隐藏
leaveShow(){
//index鼠标移出某一个以及元素的索引值
this.currentIndex=-1;
// 如果不是Home路由组件,将typeNav进行隐藏
if(this.$route.path!='/home'){
this.show=false;
}
},
- 为三级列表添加过渡动画效果,在三级列表外部嵌套一层transition,并绑定name属性,为其设置css样式
修改后的代码如下
<!-- 事件委派 -->
<div @mouseleave="leaveShow" @mouseenter="enterShow">
<h2 class="all">全部商品分类</h2>
<!-- 过渡动画 -->
<transition name="sort">
<!-- 三级联动 -->
<div class="sort" v-show="show">
<div class="all-sort-list2" @click="goSearch">
<div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId" :class="{cur:currentIndex==index}">
<h3 @mouseenter="changeIndex(index)">
<a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{c1.categoryName}}</a>
</h3>
<!-- 二级、三级分类 -->
<div class="item-list clearfix" :style="{display:currentIndex==index?'block':'none'}">
<div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{c2.categoryName}}</a>
</dt>
<dd>
<em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
<a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId">{{c3.categoryName}}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
添加的css样式为,可根据自己爱好,进行样式修改
// 过度动画样式
// 过度动画开始状态(进入)
.sort-enter{
height: 0;
// background: #333;
// transform: rotate(0deg);
}
// 过度动画结束状态(进入)
.sort-enter-to{
height: 461px;
// transform: rotate(360deg);
}
// 定义动画时间 速率
.sort-enter-active{
transition: all .3s linear;
}
// 过度动画开始状态(移出)
.sort-leave{
height: 461px;
// transform: rotate(0deg);
}
// 过度动画结束状态(移出)
.sort-leave-to{
height: 0px;
// background: #e1251b;
// transform: rotate(360deg);
}
// 定义动画时间 速率
.sort-leave-active{
transition: all .3s linear;
}
19、优化三级联动列表——路由销毁
路由在进行跳转的时候,旧路由会进行销毁,我们在三级列表全局组件TypeNav中的mounted进行了请求一次商品分类列表数据。
由于我们在home组件与Search组件中都使用了TypeNav组件,所以我们每切换一次路由时,都会发起一次请求。多次频繁的发起请求会造成不必要的数据冗余,耗内存等问题。
解决办法 :
由于信息都是一样的,出于性能的考虑我们希望该数据只请求一次,所以我们把这次请求放在App.vue的mounted中。因为根组件App.vue的mounted只会执行一次。
注意:虽然main.js也是只执行一次,但是不可以放在main.js中。因为只有组件的身上才会有$store属性。
20、合并参数---合并query、params
我们在搜索时,可以在三级联动进行搜索,也可以在搜索栏进行搜索
但是我们以不同的搜索方式进行搜索,所传递的参数不一样,需要将query与params一起传递过去,所以要进行代码的修正。
heaerd里的goSearch代码调整
goSearch() {
//路由传递参数
// 第一种字符串形式
// this.$router.push('/search/' + this.keyword + '?k=' + this.keyword.toUpperCase())
// 第二种:模板字符串
// this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)
//第三种:对象 常用 此处的name是路由规则里路由的名字
if(this.$route.query){
// 代表的是如果有query参数也带过去
let location={ name: 'search', params: { keyword: this.keyword||undefined }}
location.query=this.$route.query
this.$router.push(location)
}
}
TypeNav里goSearch代码调整
//判断 如果路由跳转的时候,带有params参数,也要带过去
if(this.$route.params){
location.params=this.$route.params
// 动态给location配置对象添加query参数
location.query=query
// 路由跳转
this.$router.push(location)
}
21、mock数据(模拟)的使用
想要mock数据需要用到mock.js插件,作用:生成随机数据,拦截Ajax请求
注意:mock数据只能在前端自己玩耍,不会向服务器发起请求
包含各种插件,打开可以直接搜索mock也可以直接点击此处进入mock官网
使用场景
场景:开发项目,产品经理画出原型,前端与后端人员需要介入(开发项目),
leader(老大)刚开完会,前端与后端负责哪些模块,后端人员(....开发服务器),
前端人员【项目起步、开发静态页面、查分静态组件】,回首一看回台‘哥哥’,接口没有写好,
向这种情况,前端人员可以mock一些数据【前端程序员自己模拟的一些假的接口】,当中工作中项目上线,需要把mock数据变为后台哥哥给的接口数据替换。
- 安装mockjs插件
cnpm install mockjs
- 使用步骤
- 在src文件夹中创建mock文件夹
- 准备json数据(mock文件夹中创建相应的文件)
理解JSON数据结构:
结构: 名称, 数据类型
value
value可以变, 但结构不能变
编写模拟JSON数据:
首页广告轮播数据: src/mock/banners.json--------代码一定要格式化一下,不能留有空格(不然跑不起来)
[
{
"id":"1",
"imgUrl":"/images/banner1.jpg"
},
{
"id":"2",
"imgUrl":"/images/banner2.jpg"
},
{
"id":"3",
"imgUrl":"/images/banner3.jpg"
},
{
"id":"4",
"imgUrl":"/images/banner4.jpg"
}
]
首页楼层数据: src/mock/floors.json--------代码一定要格式化一下,不能留有空格(不然跑不起来)
[{
"id": "001",
"name": "家用电器",
"keywords": ["节能补贴", "4K电视", "空气净化器", "IH电饭煲", "滚筒洗衣机", "电热水器"],
"imgUrl": "/images/floor-1-1.png",
"navList": [{
"url": "#",
"text": "热门"
},
{
"url": "#",
"text": "大家电"
},
{
"url": "#",
"text": "生活电器"
},
{
"url": "#",
"text": "厨房电器"
},
{
"url": "#",
"text": "应季电器"
},
{
"url": "#",
"text": "空气/净水"
},
{
"url": "#",
"text": "高端电器"
}
],
"carouselList": [{
"id": "0011",
"imgUrl": "/images/floor-1-b01.png"
},
{
"id": "0012",
"imgUrl": "/images/floor-1-b02.png"
},
{
"id": "0013",
"imgUrl": "/images/floor-1-b03.png"
}
],
"recommendList": [
"/images/floor-1-2.png",
"/images/floor-1-3.png",
"/images/floor-1-5.png",
"/images/floor-1-6.png"
],
"bigImg": "/images/floor-1-4.png"
},
{
"id": "002",
"name": "手机通讯",
"keywords": ["节能补贴2", "4K电视2", "空气净化器2", "IH电饭煲2", "滚筒洗衣机2", "电热水器2"],
"imgUrl": "/images/floor-1-1.png",
"navList": [{
"url": "#",
"text": "热门2"
},
{
"url": "#",
"text": "大家电2"
},
{
"url": "#",
"text": "生活电器2"
},
{
"url": "#",
"text": "厨房电器2"
},
{
"url": "#",
"text": "应季电器2"
},
{
"url": "#",
"text": "空气/净水2"
},
{
"url": "#",
"text": "高端电器2"
}
],
"carouselList": [{
"id": "0011",
"imgUrl": "/images/floor-1-b01.png"
},
{
"id": "0012",
"imgUrl": "/images/floor-1-b02.png"
},
{
"id": "0013",
"imgUrl": "/images/floor-1-b03.png"
}
],
"recommendList": [
"/images/floor-1-2.png",
"/images/floor-1-3.png",
"/images/floor-1-5.png",
"/images/floor-1-6.png"
],
"bigImg": "/images/floor-1-4.png"
}
]
3.把mock需要的图片放到public文件夹下(public文件夹在打包的时候,会把相应的资源原封不动的打包到dist文件夹下)
4.开始mock(虚拟数据),通过mockjs模块实现
在mock文件夹下创建mockServe.js
// 先引入mockjs模块
import Mock from 'mockjs'
//把json数据引入(JSON数据格式根本没有对外暴露,但是可以引入)
//webpack默认对外暴露:图片、JSON数据格式
import banner from './banner.json'
import floors from './floors.json'
//mock数据:第一个参数:请求地址 第二个参数:请求数据
Mock.mock("/mock/banner", { code: 200, data: banner }) //模拟首页大的轮播图的数据
Mock.mock("/mock/floors", { code: 200, data: floors })
5.将mockServe.js文件在入口文件main.js中引入(至少需要执行一次,才能模拟数据)
main.js文件
//引入MockServe.js----mock数据
import '@/mock/mockServe'
6、为了与真正的ajax请求区分,在src/api/文件夹下,新建mockAjax.js文件,作为mock虚拟数据请求时调用的接口,并在api文件夹下的index.js文件导入该接口,并对外暴露reqGetBannerList函数,mockAjax.js文件的内容与最初创建的request.js文件代码内容类似,只需要将 基础路径baseURL: "/api",改为baseURL: "/mock"即可。操作完成后,存储数据,存储于vuex 使用VueX,”三连环操作“。完成最终的请求与数据获取
(步骤本文11-15章节思路一样,不再过多赘述,)补充代码如下:
mockAjax.js
//对axios进行二次封装
import axios from 'axios'
//引入进度条
import nprogress from 'nprogress'
//引入进度条样式
import 'nprogress/nprogress.css'
//start:进度条开始 done:进度条结束
// console.log(nprogress);
// 1.利用axios对象的方法create,去创建一个axios实例
// 2.requests就是axios 只不过稍微配置一下
const requests = axios.create({
//配置对象
// 基础路径,发起请求的时候,路径中会出现api
baseURL: "/mock",
// 代表请求超时的时间5s
timeout: 5000,
})
// 请求拦截器:发起请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事
requests.interceptors.request.use((config) => {
// config:配置对象,对象里面有一个属性很重要,headers请求头
//进度条开始动
nprogress.start()
return config
})
//响应拦截器
requests.interceptors.response.use((res) => {
// 成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,做一些事情
//进度条结束
nprogress.done()
return res.data
}, (error) => {
// 响应失败的回调函数
return Promise.reject(new Error('请求失败了' + error))
})
// 对外暴露
export default requests
index.js
//当前模块对api接口进行统一管理
import mockRequest from './mockAjax'
//对外暴露一个函数,只要外部调用这个函数,就会像服务器发起Ajax请求、获取三级菜单数据,当前这个函数只需要把服务器返回的结果返回即可。
//获取banner(home首页轮播图接口)
export const reqGetBannerList = () => mockRequest.get('/banner')
在轮播图组件List.vue组件加载完毕后发起轮播图数据请求。
mounted() {
this.$store.dispatch("getBannerList")
},
请求实际是在store中的actions中完成的
定义空的数组
//state:仓库存储数据的地方
const state = {
//state中的数据默认初始值不要瞎写,服务器返回对象,服务器返回数组. 根据接口返回值初始化的
bannerList: []
}
actions:{
//获取首页轮播图数据
async getBannerList({commit}){
let result = await reqGetBannerList()
if(result.code === 200){
commit("BANNERLIST",result.data)
}
}
}
获取到数据后存入store仓库,在mutations完成
//唯一修改state的部分
mutations:{
BANNERLIST(state,bannerList){
state.bannerList = bannerList
}
},
轮播图组件List.vue组件在store中获取轮播图数据。由于在这个数据是通过异步请求获得的,所以我们要通过计算属性computed获取轮播图数据。
List.vue代码
<script>
import {mapState} from "vuex";
export default {
name: "index",
//主键挂载完毕,请求轮播图图片
mounted() {
this.$store.dispatch("getBannerList")
},
computed:{
...mapState({
bannerList: (state => state.home.bannerList)
})
}
}
</script>
现在后台就可以拿到banner.json的数据
22、轮播图的实现---Swiper插件
官网中给出了代码实例:
做一个简要总结:(代码可以直接复制本小节最后面的代码)
(1)在官网下载并安装swiper
(2)在需要使用轮播图的组件内导入swpier和它的css与js样式
(3)在组件中创建swiper需要的dom标签(html代码,参考官网代码)
(4)创建swiper实例
注意:在创建swiper对象时,我们会传递一个参数用于获取展示轮播图的DOM元素,官网直接通过class(而且这个class不能修改,是swiper的css文件自带的)获取。但是这样有缺点:当页面中有多个轮播图时,因为它们使用了相同的class修饰的DOM,就会出现所有的swiper使用同样的数据,这肯定不是我们希望看到的。
解决方法:在轮播图最外层DOM中添加ref属性
<div class="swiper-container" id="mySwiper" ref="cur">
通过ref属性值获取DOM
let mySwiper = new Swiper(this.$refs.cur,{...})
<!--banner轮播-->
<div class="swiper-container" id="mySwiper" ref="cur">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(carouse,index) in bannerList" :key="carouse.id">
<img :src="carouse.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev" ></div>
<div class="swiper-button-next"></div>
</div>
<script>
//引入Swiper
import Swiper from 'swiper'
//引入Swiper样式
import 'swiper/css/swiper.css'
</script>
接下来要考虑的是什么时候去加载这个swiper,我们第一时间想到的是在mounted中创建这个实例。
但是会出现无法加载轮播图片的问题。
原因:
我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。
mounted() {
//请求数据
this.$store.dispatch("getBannerList")
//创建swiper实例
let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{
pagination:{
el: '.swiper-pagination',
//点击小圆点也可以切换图片
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
},
解决方法一:等我们的数据请求完毕后再创建swiper实例。只需要加一个1000ms时间延迟再创建swiper实例.。将上面代码改为:
mounted() {
this.$store.dispatch("getBannerList")
setTimeout(()=>{
let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
},1000)
},
方法一肯定不是最好的,但是我们开发的第一要义就是实现功能,之后再完善。
解决方法二:我们可以使用watch监听bannerList轮播图列表属性,因为bannerList初始值为空,当它有数据时,我们就可以创建swiper对象
watch:{
bannerList(newValue,oldValue){
let mySwiper = new Swiper(this.$refs.cur,{
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
}
}
使这样也还是无法实现轮播图,原因是,我们轮播图的html中有v-for的循环,我们是通过v-for遍历bannerList中的图片数据,然后展示。我们的watch只能保证在bannerList变化时创建swiper对象,但是并不能保证此时v-for已经执行完了。假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片(因为swiper对象生效的前提是html即dom结构已经渲染好了)。
完美解决方案:使用watch+this.$nextTick()
官方介绍:this. $nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。
个人理解:无非是等我们页面中的结构都有了再去执行回调函数
完整代码
<template>
<!--列表-->
<div class="list-container">
<div class="sortList clearfix">
<div class="center">
<!--banner轮播-->
<div class="swiper-container" id="mySwiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(carouse,index) in bannerList" :key="carouse.id">
<img :src="carouse.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev" ></div>
<div class="swiper-button-next"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
//引入Swiper
import Swiper from 'swiper'
//引入Swiper样式
import 'swiper/css/swiper.css'
import {mapState} from "vuex";
export default {
name: "List",
//主键挂载完毕,ajax请求轮播图图片
mounted() {
this.$store.dispatch("getBannerList")
},
computed:{
...mapState({
//从仓库中获取轮播图数据
bannerList: (state) => {return state.home.bannerList}
})
},
watch:{
bannerList(newValue,oldValue){
//this.$nextTick()使用
this.$nextTick(()=>{
let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
})
})
}
}
}
</script>
注意:之前我们在学习watch时,一般都是监听的定义在data中的属性,但是我们这里是监听的computed中的属性,这样也是完全可以的,并且如果你的业务数据也是从store中通过computed动态获取的,也需要watch监听数据变化执行相应回调函数,完全可以模仿上面的写法。
23、props父子组件通信
- 自定义事件:@on @emit 可以实现子给父通信
- 全局事件总线:$bus 全能
- pubsub-js:vue中几乎不用 全能
- 插槽
- vuex
props官方文档https://cn.vuejs.org/v2/guide/components-props.html
本项目的
父组件:home文件下的home.vue
<template>
<div>
//...省略
<!-- 父组件通过自定义属性list给子组件传递数据-->
<Floor v-for="floor in floorList" :key="floor.id" :list="floor"/>
<!-- 商标-->
</div>
</template>
子组件:Floor下的Floor.vue
<template>
<!--楼层-->
<div class="floor">
//...省略
</div>
</template>
<script>
export default {
name: "floor",
//子组件通过props属性接受父组件传递的数据
props:['list']
}
</script>
个人理解
Floor是子组件,我们在home组件中调用了Floor,我们把home组件认为父组件,我们在home组件中实现了由home组件向Floor组件传递信息的操作,即父组件向子组件传递信息。 在子组件中动态获取数据时使用list.XXXX来调取数据
24、将轮播图提取为公用组件
只用功能与书写的代码结构,高度相同时,为了提炼代码,才可以提升为公用组件,在首页的banner轮播图与Floor的轮播图,样式结构完全一样,因此,将轮播图部分的代码注册为公用组件。
需要注意的是我们要把定义swiper对象放在mounted中执行,并且还要设置immediate:true属性,这样可以实现,无论数据有没有变化,上来立即监听一次。
上一小节刚刚讲了props实现父组件向子组件传递消息,这里同样也会将轮播图列表传递给子组件,原理相同。
公共组件Carousel.vue代码
<template>
<div class="swiper-container" ref="cur">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(carouse,index) in carouselist" :key="carouse.id">
<img :src="carouse.imgUrl">
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</template>
<script>
// 引入Swiper包
import Swiper from "swiper";
import 'swiper/css/swiper.css'
export default {
name: "Carousel",
props:["carouselist"],
watch:{
carouselist:{
// 立即监听:不管你数据有没有变化,一上来就立即监听 immediate
// 为什么watch监听不了list:因为这个数据从来没有发生过变化(数据是父亲给的,是一个对象,对象里面该有的数据都是有的)
immediate:true,
handler(newValue, oldValue){
// 只监听到已经有的数据,但是v-for动态渲染结构还是没办法确认的,因此还需要nextTick
this.$nextTick(()=>{
var mySwiper=new Swiper(this.$refs.cur,{
loop:true,
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
}
)
})
}
}
}
}
</script>
<style scoped>
</style>
Floor组件引用Carousel组件<Carousel :carouselList="list.carouselList"/>
我们还记得在首页上方我们的ListContainer组件也使用了轮播图,同样我们替换为我们的公共组件。
ListContainer组件引用Carousel组件<Carouse :carouselList="bannerList"/>
注意:
(1)可以将该组件在main.js中引入,并定义为全局组件。也可以在使用到该组件的地方引入并声明(个人认为轮播图组件还算不上全局组件的时候)。
(2)如果没有注册为全局组件,引用组件时要在components中声明引入的组件。
(3)我们将轮播图组件已经提取为公共组件Carouse,所以我们只需要在Carouse中引入swiper和相应css样式。
25、VueX---getters使用
getters是vuex store中的计算属性。
getters:理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
如果不使用getters属性,我们在组件获取state中的数据表达式为:this.$store.state.子模块.属性,
如果有多个组件需要用到此属性,我们要么复制这个表达式,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getters”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
个人理解:getters将获取store中的数据封装为函数,代码维护变得更简单(和我们将请求封装为api一样)。而且getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
注意:仓库中的getters是全局属性,是不分模块的。即store中所有模块的getter内的函数都可以通过$store.getters.函数名获取
我们在Search模块中获取商品列表数据就是通过getters实现,需要注意的是当网络出现故障时应该将返回值设置为空,如果不设置返回值就变成了undefined。
store中search模块代码
//search仓库的数据
import { reqGetSearchInfo } from '@/api'
//state:仓库存储数据的地方
const state = {
searchList: {}
}
//mutations:修改state的唯一手段
const mutations = {
GETSEARCHLIST(state, searchList) {
state.searchList = searchList
}
}
// action: 处理action, 可以书写自己的业务逻辑, 也可以处理异步
const actions = {
//获取search模块数据
async getSearchList({ commit }, params = {}) {
//当前这个reqGetSearchInfo函数在调用获取服务器数据的时候,至少传递一个参数(空对象)
//params形参:是当前用户派发action的时候,第二个参数传递过来的,至少是一个空对象
let request = await reqGetSearchInfo(params)
if (request.code === 200) {
commit("GETSEARCHLIST", request.data)
}
}
}
// getters:理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {
// 当前形参data, 是当前仓库中的state,并非大仓库中的那个state
goodsList(state) {
// state.searchList.goodsList如果服务器数据回来了,没问题,是一个数组
//假如网络出现故障时应该将返回值设置为空
//计算新的属性的属性值至少给人家一个数组
return state.searchList.goodsList || []
},
trademarkList(state) {
return state.searchList.trademarkList || []
},
attrsList(state) {
return state.searchList.attrsList || []
}
}
//对外暴露store类的一个实例
export default {
state,
mutations,
actions,
getters
}
在Search组件中使用getters获取仓库数据
<script>
//引入mapGetters
import { mapGetters } from 'vuex'
export default {
name: 'Search',
mounted() {
this.$store.dispatch('getSearchList',{});
},
computed: {
// 这样获取数据太麻烦了,容易出错,使用vuex里面的getters
// ...mapState({
// //右侧需要的是一个函数,当使用计算属性的时候,右侧函数会立即执行一次
// //注入一个参数state,其实即为大仓库中的数据
// goodsList: state => {return state.search.searchList.goodsList }
// })
//使用mapGetters,参数是一个数组,数组的元素对应getters中的函数名,因为getters计算属性没有划分模块,直接使用
...mapGetters(['goodsList'])
},
}
</script>
26、Object.asign实现对象拷贝
Object.assign:ES6新增语法,合并对对象
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, ...sources) 【target:目标对象】,【souce:源对象(可多个)】
举个栗子:
const object1 = {
a: 1,
b: 2,
c: 3
};
const object2 = Object.assign({c: 4, d: 5}, object1);
console.log(object2.c, object2.d);
console.log(object1) // { a: 1, b: 2, c: 3 }
console.log(object2) // { c: 3, d: 5, a: 1, b: 2 }
注意:
1.如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性
2.Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标
对象的[[Set]],所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如
果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到
原型,应使用Object.getOwnPropertyDescriptor()和Object.defineProperty() 。
修改完善的search代码:
<script>
//引入mapGetters
import { mapGetters } from 'vuex'
// 引入SearchSelector.vue组件
import SearchSelector from './SearchSelector/SearchSelector'
export default {
name: 'Search',
components: {
SearchSelector
},
data() {
return {
//带给服务器参数
searchParams: {
// 一级分类的id
"category1Id": "",
// 二级分类的id
"category2Id": "",
// 三级分类的id
"category3Id": "",
// 分类名字
"categoryName": "",
// 关键字
"keyword": "",
// 排序 升序、降序
"order": "",
// 分页器使用的参数 代表当前是第几页
"pageNo": 1,
// 代表每一页展示的数据个数
"pageSize": 10,
// 平台售卖属性操作戴的参数
"props": [],
// 品牌
"trademark": ""
}
}
},
//当组件挂载完毕之前执行一次(优先于mounted之前)
beforeMount() {
//复杂的写法 如下
// this.searchParams.category1Id=this.$route.query.category1Id
// 简便写法 Object.assign:ES6新增语法,合并对对象
Object.assign(this.searchParams,this.$route.query,this.$route.params)
},
//组件挂载完毕执行一次(仅仅执行一次)
mounted() {
//在发请求之前带给服务器参数[searchParams参数发生变化有数值带给服务器]
this.getData()
},
methods: {
// 向服务器发起请求获取search模块数据(根据参数不同返回不同的数据进行展示)
// 把这次请求封装为一个函数:当你需要在调用的时候调用即可
getData() {
this.$store.dispatch('getSearchList', this.searchParams);
}
},
computed: {
// 这样获取数据太麻烦了,容易出错,使用vuex里面的getters
// ...mapState({
// //右侧需要的是一个函数,当使用计算属性的时候,右侧函数会立即执行一次
// //注入一个参数state,其实即为大仓库中的数据
// goodsList: state => {return state.search.searchList.goodsList }
// })
//使用mapGetters,参数是一个数组,数组的元素对应getters中的函数名,因为getters计算属性没有划分模块,直接使用
...mapGetters(['goodsList'])
},
}
</script>
其实就是对象的拷贝,this.searchParams是目标对象,后面的是源对象,后面的属性等会拷贝到目标对象
27、监听路由的变化再次发起请求获取数据
我们每次进行新的搜索时,我们的query和params参数中的部分内容肯定会改变,而且这两个参数是路由的属性。我们可以通过监听路由信息的变化来动态发起搜索请求。
如下图所示,$route是组件的属性,所以watch是可以监听的(watch可以监听组件data中所有的属性)
注意:组件中data的属性包括:自己定义的、系统自带的(如 $route)、父组件向子组件传递的等等。
search组件watch部分代码。
//数据监听:监听组件实例身上的属性的数值变化
watch: {
// 监听路由的信息是否发生变化,若有变化,再次发起请求
$route(newValue, oldValue) {
// 再次发起请求之前,整理带给服务器参数
Object.assign(this.searchParams, this.$route.query, this.$route.params)
//再次发起ajax请求
this.getData()
}
}
此时,我们在进行搜索时便可以进行多条件动态搜索了
但是我们会发现一个问题,再次搜索别的商品,上一次的数据会保留,并不会清空,假设,我们在左侧 “全部商品分类” 里搜索“数码相机”,会发现,上一次的“华为”还会保留
因此,我们需要清空上次的数据,在watch监听是补充代码
watch: {
// 监听路由的信息是否发生变化,若有变化,再次发起请求
$route(newValue, oldValue) {
// 再次发起请求之前,整理带给服务器参数
Object.assign(this.searchParams, this.$route.query, this.$route.params)
//再次发起ajax请求
this.getData()
//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数
//所以每次请求结束后将相应参数制空
//分类名字与关键字不用清理,因为每一次路由发生变化的时候,都会给他赋予新的数据
this.searchParams.category1Id = '';
this.searchParams.category2Id = '';
this.searchParams.category3Id = '';
}
}
28、面包屑处理分类操作
本次项目的面包屑操作主要就是两个删除逻辑。
分为:
当分类属性(query)删除时删除面包屑同时修改路由信息。
当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。
28.1、分类的面包屑--删除query
选中分类,在搜索结果处显示相应的名字
search组件中html代码
<!--bread 面包屑:带有x的结构-->
<div class="bread">
<ul class="fl sui-breadcrumb">
<li>
<a href="#">全部结果</a>
</li>
</ul>
<ul class="fl sui-tag">
<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName}}<i
@click="removeCategoryName">x</i></li>
</ul>
</div>
query删除时
因为此部分在面包屑中是通过removeCategoryName展示的,所所以删除时应将该属性值制空或undefined。并且删除后,路径需要变化,利用路由跳转实现。
点击事件removeCategoryName代码
methods: {
// 向服务器发起请求获取search模块数据(根据参数不同返回不同的数据进行展示)
// 把这次请求封装为一个函数:当你需要在调用的时候调用即可
getData() {
this.$store.dispatch('getSearchList', this.searchParams);
},
// 删除分类名字
removeCategoryName() {
// 把带给服务器的参数置空了,还需要向服务器发起请求
//带给服务器的参数说明{可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器,但是设置为undefined,当前这个字段不会带给服务器}
this.searchParams.categoryName = undefined
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
this.getData()
//地址栏也需要修改:进行路由跳转
// 当前自己路由下自己挑自己 现在的路由跳转只是跳转到自己这里
//严谨:本意是删除query参数,如果路径中出现params参数,不需要删除
if (this.$route.params) {
this.$router.push({ name: 'search', params: this.$route.params })
}
},
28.1、关键字的面包屑--删除params
在分类中搜索手机并在关键字处搜索华为,都可以自动现,并且点击x号删除
html中补充代码
<!--bread 面包屑:带有x的结构-->
<div class="bread">
<ul class="fl sui-breadcrumb">
<li>
<a href="#">全部结果</a>
</li>
</ul>
<ul class="fl sui-tag">
<!-- 分类的面包屑 -->
<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName}}<i
@click="removeCategoryName">x</i></li>
<!-- 关键字的面包屑 -->
<li class="with-x" v-if="searchParams.keyword">{{ searchParams.keyword}}<i @click="removeKeyword">x</i>
</li>
</ul>
</div>
params删除时
和query删除的唯一不同点是此部分会多一步操作:删除输入框内的关键字(因为params参数是从输入框内获取的)
输入框实在Header组件中的
header和search组件是兄弟组件,要实现该操作就要通过兄弟组件之间进行通信完成。
这里通过$bus实现header和search组件的通信。
$bus使用
(1)在main.js中注册全局总线
new Vue({
//全局事件总线$bus配置
beforeCreate() {
//此处的this就是这个new Vue()对象
//网络有很多bus通信总结,原理相同,换汤不换药
Vue.prototype.$bus = this
},
render: h => h(App),
//注册路由
router,
// 注册仓库:组件实例的身上会多一个属性$store属性
store
}).$mount('#app')
(2)search组件使用$bus通信,第一个参数可以理解为为通信的暗号,还可以有第二个参数(用于传递数据),我们这里只是用于通知header组件进行相应操作,所以没有设置第二个参数。
//删除关键字
removeKeyword() {
// 把带给服务器的参数searchParams的keyword置空了,还需要向服务器发起请求
this.searchParams.keyword = undefined
//再次发起请求
this.getData()
//通知兄弟组件header删除输入框的keyword关键字
this.$bus.$emit("clear")
// 本意是删除params参数,如果路径中出现query参数,不需要删除
if (this.$route.query) {
this.$router.push({ name: 'search', query: this.$route.query })
}
},
},
(3)header组件接受$bus通信
注意:组件挂载时就监听clear事件
mounted() {
// 组件挂载时就监听clear事件,clear事件在search模块中定义
// 当删除关键字面包屑时,触发该事件,同时header的输入框绑定的keyword要删除
this.$bus.$on("clear",()=>{
this.keyword = ''
})
}
28.3、品牌信息----SearchSelector子组件传参及面包屑操作
上述描述了通过query、params参数生成面包屑,以及面包屑的删除操作对应地址栏url的修改。
SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。如下图所示
(1)在SearchSelector子组件中,找到 “品牌”,为其绑定点击事件trademarkHandler,通过点击相应品牌,进行搜索
<template>
<div class="clearfix selector">
<div class="type-wrap logo">
<div class="fl key brand">品牌</div>
<div class="value logos">
<ul class="logo-list">
<li v-for="(trademark,index) in trademarkList" :key="trademark.tmId" @click="trademarkHandler(trademark)">{{trademark.tmName}}
</li>
</ul>
</div>
<div class="ext">
<a href="javascript:void(0);" class="sui-btn">多选</a>
<a href="javascript:void(0);">更多</a>
</div>
</div>
<div class="type-wrap" v-for="(attr,index) in attrsList" :key="attr.attrId">
<div class="fl key">{{ attr.attrName}}</div>
<div class="fl value">
<ul class="type-list">
<li v-for="(attrValue,index) in attr.attrValueList" :key="index">
<a>({{ attrValue}})</a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'SearchSelector',
computed: {
...mapGetters(["trademarkList","attrsList"])
}
}
</script>
<style lang="less" scoped>
.selector {
border: 1px solid #ddd;
margin-bottom: 5px;
overflow: hidden;
.logo {
border-top: 0;
margin: 0;
position: relative;
overflow: hidden;
.key {
padding-bottom: 87px !important;
}
}
.type-wrap {
margin: 0;
position: relative;
border-top: 1px solid #ddd;
overflow: hidden;
.key {
width: 100px;
background: #f1f1f1;
line-height: 26px;
text-align: right;
padding: 10px 10px 0 15px;
float: left;
}
.value {
overflow: hidden;
padding: 10px 0 0 15px;
color: #333;
margin-left: 120px;
padding-right: 90px;
.logo-list {
li {
float: left;
border: 1px solid #e4e4e4;
margin: -1px -1px 0 0;
width: 105px;
height: 52px;
text-align: center;
line-height: 52px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 700;
color: #e1251b;
font-style: italic;
font-size: 14px;
img {
max-width: 100%;
vertical-align: middle;
}
}
}
.type-list {
li {
float: left;
display: block;
margin-right: 30px;
line-height: 26px;
a {
text-decoration: none;
color: #666;
}
}
}
}
.ext {
position: absolute;
top: 10px;
right: 10px;
.sui-btn {
display: inline-block;
padding: 2px 14px;
box-sizing: border-box;
margin-bottom: 0;
font-size: 12px;
line-height: 18px;
text-align: center;
vertical-align: middle;
cursor: pointer;
padding: 0 10px;
background: #fff;
border: 1px solid #d5d5d5;
}
a {
color: #666;
}
}
}
}
</style>
此处生成面包屑时会涉及到子组件向父组件传递信息操作
(2)在父组件serach中找到子组件路由SearchSelector位置,添加自定义事件
<!-- 自定义事件 子向父传递信息 -->
<SearchSelector @trademarkInfo="trademarkInfo" />
(3)在子组件SearchSelector中接收
methods: {
//品牌点击事件处理函数
trademarkHandler(trademark) {
// 点击了品牌(华为),还需要整理参数,向服务器发起请求获取相应数据进行展示
//子向父传递,因为父组件SearchParmas参数是带给服务器的,子组件将点击的品牌信息,给父组件传递过去-----自定义事件
this.$emit("trademarkInfo", trademark)
}
}
(4)父组件中编写trademarkInfo函数,添加的代码如下
methods: {
//获取子组件传递的品牌信息(自定义事件)
trademarkInfo(trademark) {
//接口文档中trademark的信息是"ID:品牌名称"形式
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
this.getData()
},
},
之后的操作和前面讲的面包屑操作原理相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。
总结:面包屑由四个属性影响:parads、query、品牌、手机属性
面包屑生成逻辑
判断searchParams相关属性是否存在,存在即显示。
面包屑删除逻辑
Search.vue js代码() 全部代码
<script>
//引入mapGetters
import { mapGetters } from 'vuex'
// 引入SearchSelector.vue组件
import SearchSelector from './SearchSelector/SearchSelector'
export default {
name: 'Search',
components: {
SearchSelector
},
data() {
return {
//带给服务器参数
searchParams: {
// 一级分类的id
"category1Id": "",
// 二级分类的id
"category2Id": "",
// 三级分类的id
"category3Id": "",
// 分类名字
"categoryName": "",
// 关键字
"keyword": "",
// 排序 升序、降序
"order": "",
// 分页器使用的参数 代表当前是第几页
"pageNo": 1,
// 代表每一页展示的数据个数
"pageSize": 10,
// 平台售卖属性操作戴的参数
"props": [],
// 品牌
"trademark": ""
}
}
},
//当组件挂载完毕之前执行一次(优先于mounted之前)
beforeMount() {
//复杂的写法 如下
// this.searchParams.category1Id=this.$route.query.category1Id
// 简便写法 Object.assign:ES6新增语法,合并对对象
Object.assign(this.searchParams, this.$route.query, this.$route.params)
},
//组件挂载完毕执行一次(仅仅执行一次)
mounted() {
//在发请求之前带给服务器参数[searchParams参数发生变化有数值带给服务器]
this.getData()
},
methods: {
// 向服务器发起请求获取search模块数据(根据参数不同返回不同的数据进行展示)
// 把这次请求封装为一个函数:当你需要在调用的时候调用即可
getData() {
this.$store.dispatch('getSearchList', this.searchParams);
},
// 删除分类名字(query关键字)
removeCategoryName() {
// 把带给服务器的参数置空了,还需要向服务器发起请求
//带给服务器的参数说明{可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器,但是设置为undefined,当前这个字段不会带给服务器}
this.searchParams.categoryName = undefined
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
//再次发起请求
this.getData()
//地址栏也需要修改:进行路由跳转
// 当前自己路由下自己挑自己 现在的路由跳转只是跳转到自己这里
//严谨:本意是删除query参数,如果路径中出现params参数,不需要删除
if (this.$route.params) {
this.$router.push({ name: 'search', params: this.$route.params })
}
},
//删除关键字(params关键字)
removeKeyword() {
// 把带给服务器的参数searchParams的keyword置空了,还需要向服务器发起请求
this.searchParams.keyword = undefined
//再次发起请求
this.getData()
//通知兄弟组件header删除输入框的keyword关键字
this.$bus.$emit("clear")
// 本意是删除params参数,如果路径中出现query参数,不需要删除
if (this.$route.query) {
this.$router.push({ name: 'search', query: this.$route.query })
}
},
//删除品牌
removeTrademark() {
// 把带给服务器的参数searchParams的trademark置空了,还需要向服务器发起请求
this.searchParams.trademark = undefined
//再次发起请求
this.getData()
},
//获取子组件传递的品牌信息(自定义事件)
trademarkInfo(trademark) {
//接口文档中trademark的信息是"ID:品牌名称"形式
this.searchParams.trademark =`${trademark.tmId}:${trademark.tmName}`
// 再次发起请求
this.getData()
},
},
computed: {
// 这样获取数据太麻烦了,容易出错,使用vuex里面的getters
// ...mapState({
// //右侧需要的是一个函数,当使用计算属性的时候,右侧函数会立即执行一次
// //注入一个参数state,其实即为大仓库中的数据
// goodsList: state => {return state.search.searchList.goodsList }
// })
//使用mapGetters,参数是一个数组,数组的元素对应getters中的函数名,因为getters计算属性没有划分模块,直接使用
...mapGetters(['goodsList'])
},
//数据监听:监听组件实例身上的属性的数值变化
watch: {
// 监听路由的信息是否发生变化,若有变化,再次发起请求
$route(newValue, oldValue) {
// 再次发起请求之前,整理带给服务器参数
Object.assign(this.searchParams, this.$route.query, this.$route.params)
//再次发起ajax请求
this.getData()
//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数
//所以每次请求结束后将相应参数制空
//分类名字与关键字不用清理,因为每一次路由发生变化的时候,都会给他赋予新的数据
this.searchParams.category1Id = '';
this.searchParams.category2Id = '';
this.searchParams.category3Id = '';
}
}
}
</script>
29、排序操作
排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。
order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。
我们的升降序是通过箭头图标来辨别的,如图所示:
考虑的问题
- 谁应该有类名?
- 谁应该有箭头?
29.1谁应该有类名:通过order属性值当中包含1(综合)还是2(价格)
在search组件中找到排序结构的html代码,为active样式绑定动态属性,利用searchParams.order.indexOf('1') != -1进行判断,为了代码简洁,将其编写为计算属性,然后以样式的形式使用
computed: {
isOne() {
return this.searchParams.order.indexOf('1') != -1
},
isTwo() {
return this.searchParams.order.indexOf('2') != -1
},
},
对应的html代码
<div class="sui-navbar">
<div class="navbar-inner filter">
<!-- 排序的结构 -->
<ul class="sui-nav">
<li :class="{ active: isOne }">
<a href="#">综合</a>
</li>
<li :class="{ active: isTwo }">
<a href="#">价格</a>
</li>
</ul>
</div>
</div>
29.2谁应该有箭头:谁有类名谁有箭头
判断谁应该有箭头,也是通过上述编写的两个计算属性利用v-show进行显示隐藏的,html代码改为
<div class="navbar-inner filter">
<!-- 排序的结构 -->
<ul class="sui-nav">
<li :class="{ active: isOne }">
<a>综合<span v-show="isOne"></span></a>
</li>
<li :class="{ active: isTwo }">
<a>价格<span v-show="isTwo"></span></a>
</li>
</ul>
</div>
29.3使用iconfont图标
- 图标是iconfont网站的图标,通过引入在线css的方式引入图标
- 在public文件index引入该css
<link rel="stylesheet" href="https://at.alicdn.com/font_2758111_9hehe1sgko.css">
- 在search模块使用该图标
使用时排序箭头有升序有降序,判断思路与1.1相同,利用searchParams.order.indexOf('asc') != -1进行判断,且定义为计算属性,相应的计算属性代码为
isAsc() {
return this.searchParams.order.indexOf('asc') != -1
},
isDesc() {
return this.searchParams.order.indexOf('desc') != -1
},
相应的html代码修改为:
<div class="sui-navbar">
<div class="navbar-inner filter">
<!-- 排序的结构 -->
<ul class="sui-nav">
<li :class="{ active: isOne }">
<a>综合<span v-show="isOne" class="iconfont"
:class="{ 'icon-UP': isAsc, 'icon-Down': isDesc}"></span></a>
</li>
<li :class="{ active: isTwo }">
<a>价格<span v-show="isTwo" class="iconfont"
:class="{ 'icon-UP': isAsc, 'icon-Down': isDesc}"></span></a>
</li>
</ul>
</div>
</div>
注意:图标的使用可以是其他的哦,不一定用iconfont
29.4点击‘综合’或‘价格’的触发函数changeOrder
为俩个<li/> bi标签绑定单击事件,并传参 @click="changeOrder('1')"和@click="changeOrder('2')"
点击事件的changeOrder的代码为
// 排序的操作
changeOrder(flag) {
//flag形参:用于区分综合、价格,1:综合,2:价格
//这里获取到的是最开始的状态
//将order拆为两个字段orderFlag(1:2)、order(asc:desc)
let orderFlag = this.searchParams.order.split(':')[0]
let orderStor = this.searchParams.order.split(':')[1]
//准备一个新的order值
let newOrder = '';
//由综合到价格、由价格到综合
//点击的是综合
if (flag == orderFlag) {
newOrder = `${flag}:${orderStor == 'desc' ? 'asc' : 'desc'}`
this.getData()
}else {
//点击的是价格
newOrder = `${flag}:${'desc'}`
}
//需要给order重新赋值
this.searchParams.order = newOrder;
//再次发请求
this.getData();
}
30、自定义分页器
实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。但是这个知识还是值得学习一下的。
核心属性:
pageNo(当前页码)、pageSize、total、continues(连续展示的页码)
通过父给子传递数据的方式(此处不过多赘述)
核心逻辑是获取连续页码的起始页码和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式返回)
export default {
name: "Pagination",
props: ["pageNo", "pageSize", "total", "continues"],
computed: {
//总共多少页
totalPage() {
// 向上取整
return Math.ceil(this.total / this.pageSize)
},
//连续页码的起始页码、末尾页码
startNumAndEndNum(){
const { continues, totalPage, pageNo } = this;
// 先定义两个起始变量,存储开始和结束页码
let start = 0, end = 0;
// 规定连续页码数字5(totalPage至少5页)
//不正常现象(总页数没有起始页码)
if (this.continues > this.totalPage) {
start = 1
end = this.totalPage
} else {
//正常现象 Math.floor:向下取整
start = this.pageNo - Math.floor(this.continues / 2)
end = this.pageNo + Math.floor(this.continues / 2)
//start出现不正常现象纠正
if (start < 1) {
start = 1
end = this.continues
}
//end出现不正常现象纠正
if (end > this.totalPage) {
end = this.totalPage
start = this.totalPage - this.continues + 1
}
}
return {start,end}
}
}
}