赞
踩
项目网络教学视频链接:尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili
目录
三十、轮播图:watch+nextTick( )(第二种解决方案)
准备:提前安装好node、webpack、淘宝镜像(最好有)
1. 找到文件夹目录,输入cmd,出现下面内容,输入“ vue create 项目名”,回车确认
2. 选择vue2版本,创建好之后,使用VS打开该文件夹。
3. 分析目录组成
1. 如何让浏览器自动打开这个项目?找到package.json这个文件,找到 "serve": "vue-cli-service serve",将其改为"serve": "vue-cli-service serve --open",如图所示:
2. 关闭eslint校验功能:在根目录下,创建一个vue.config.js文件。配置以下内容:
- module.exports = {
- //关闭校验工具
- lintOnSave:false,
- }
3. 设置src文件夹简写方式,配置别名:@。在根目录下,创建jsconfig.json文件,配置以下内容:
- {
- "compilerOptions": {
- "target": "es5",
- "module": "esnext",
- "baseUrl": "./",
- "moduleResolution": "node",
- "paths": {
- "@/*": [
- "src/*"
- ]
- },
- "lib": [
- "esnext",
- "dom",
- "dom.iterable",
- "scripthost"
- ]
- }
- }
前端路由:类似于【key--value键值对】的形式,其中key表示URL,value表示对应的路由组件
这里需要使用vue-router来实现。
项目结构主要分为上、中、下三部分
路由组件包括:Home首页路由组件、Search搜索路由组件、login登录路由组件、注册路由组件
非路由组件包括:Header组件【首页、搜索、登录、注册】、Footer组件【在首页、搜索页】
(1)在开发项目的时候:1. 书写静态页面(HTML+CSS)
2. 拆分组件
3. 获取服务器的数据动态展示
4. 完成相应的动态业务逻辑
(2)那么非路由组件创建在哪里?在src文件夹下创建components文件夹,在该文件夹中分别创建Header和Footer文件夹,用于实现非路由组件。
(在创建组件时,需要注意三要素:组件结构+组件的样式+图片资源)
(3)在非路由组件文件夹中,创建vue类型的文件:index.vue
对于样式,如果采用的是less样式,浏览器不能识别less样式,需要通过less、less-loader进行处理,把less样式变为css样式,这样浏览器才能识别。
1. 先安装less-loader依赖(这里需要注意,版本不能过高,否则不能使用,这里选择5版本,如果不说明默认是最高版本)
2. 还需要在style标签的身上加上lang=lees
对于图片资源,在非路由组件文件中创建一个images文件夹,用于存放数据
(4)当组件创建好之后,就要使用该组件了,步骤为:引入----注册----使用
(1)安装vue-router插件
(2)通过上面分析,路由组件应该有四个:Home、Search、Login、Register,
那么路由组件通常创建在哪里呢?在src文件夹下创建pages文件夹,在该文件夹中分别创建Home、Search、Login、Register文件夹,用于实现路由组件。
(3)配置路由
在src文件夹下创建router文件夹,在该文件夹中创建一个index.js文件,用来配置路由信息
配置路由的时候,还要实现【重定向】,即在项目跑起来的时候,当访问 / 时,会立马定位到首页
(4)接着,在main.js文件中【引入路由】和【注册路由】
PS:当这里书写router的时候,不管是路由组件还是非路由组件,身上都拥有$route、$router属性
$route:一般获取路由信息【路径、query、params】
$router:一般进行编程式路由导航进行路由跳转【push | replace】
(5)最后还要展示路由,即在App.vue文件中设置【路由组件出口的地方】
(6)【总结】路由组件和非路由组件的区别?
1. 路由组件一般
(7)进行路由跳转
有两种形式:1.声明式导航router-link,可以进行路由的跳转
2.编程式导航push|replace,可以进行路由跳转
声明式导航能做的,编程式导航都能做,但是编程式导航除了可以进行路由跳转,还可以做一些其他的业务逻辑。
在【index.vue】中设置路由跳转
分析Footer组件:实现它在Home、Search中显示,在Register、Login中隐藏
(1)方法一(不推荐):在上节中,我们知道这时组件已经具备$route属性,可以获取路由路径
显示或者隐藏组件:v-if、v-show(这里采用v-show,性能更好,不频繁操作DOM)
(2)方法二(推荐):即利用【路由元信息】
这里放上有关路由元信息的官方文档内容:路由元信息 | Vue Router
找到router文件夹中的index.js文件,将【谁可以具有Footer组件的信息】通过接收属性对象的meta属性来实现,并且它可以在路由地址和导航守卫上都被访问到。
然后在App.vue文件中,进行$route.meta.show判断,如果为真则显示,如果为假则隐藏
我们已经了解到路由跳转有两种方式:声明式导航、编程式导航
路由进行传参时,参数一般有种写法:
params参数:属于路径当中的一部分,在配置路由的时候需要【占位】
query参数:不属于路径当中的一部分,类似于ajax中的queryString,不需要占位
(1)第一种路由传递参数的方式:【字符串形式】
1.先在路由配置信息中进行占位
2.进行路由push跳转,跳转到search页面时传递相应的【路由参数】
3.这时在Search页面中,通过【路由信息】就可以获取到params参数
(2)第二种路由传递参数的方式:【模板字符串】
1.第一步和上个方法相同
2.和上个方法的第二部有些区别,采用模板字符串的方式
3.接收参数和上个方法相同
(3)第三种路由传递参数的方式:【对象】
1. 当使用【对象】的方式进行传参,传入的参数又是params参数时,需要在路由配置信息中 为路由设置【名字】,name: "XXX"
2.传递参数,形式如下图所示
3.接收参数和上个方法相同
【问题】:编程式路由导航跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误(但不影响最终的结果)?而声明式导航是没有这类问题的,因为vue-router底层就已经处理好了。
【原因】:最新的vue-router引入了promise,即调用push方法会返回promise对象,但没有向其中传入成功的回调和失败的回调。
【解决方法1】:在调用push方法时,就传入成功和失败的回调。(可以捕获出error看看错误类型)但是这种方法治标不治本。将来在别的组件中,不管是push还是replace,编程式导航还是有类似的错误。这样一次次解决下去太麻烦了。
【解决方法2】:首先搞清楚上段代码中的this是什么、this.$router是什么、push是什么
this:当前组件实例
this.$router属性:这个属性的属性值是VueRouter类的一个实例,即当在入口文件注册路由的时候,给组件实例添加的$router和$route属性
push:VueRouter类原型上的方法
为了更好的理解this.$router.push( )方法,我们根据这三个的特性实现简单的伪代码
- //构造函数VueRouter
- function VueRouter(){
-
- }
- //原型对象上的方法
- VueRouter.prototype.push = function(){
- //函数的上下位为VueRouter类的一个实例
- }
- //实例化一个VueRouter对象
- let $router = new VueRouter();
-
- $router.push(xxx);
因此想要治本,必须重写VueRouter原型上的push方法。在有【路由配置信息】的文件中进行重写,因为在这个文件中,我们是可以获取到VueRouter类的
(replace方法重写和上述类似)
【第一个组件】:因为【三级联动组件】在很多页面中都使用了,因此将其拆分成一个全局组件,哪里想用就用哪里(红色框出来的就是三级联动的展示)。
【第二个组件】:轮播图+尚品汇快报
【第三个组件】:今日推荐
【第四个组件】:排行榜
【第五个组件】:猜你喜欢
【第六个组件】:家用电器|手机通讯等,组件可被复用
【第七个组件】:商品logo
(1)在page文件夹中的Home文件夹下,新建一个文件夹TypeNav,在该文件夹中创建index.vue文件,用来配置【三级联动组件】的内容
(2)在HTML静态资源中找到有关【三级联动】的结构代码,把代码内容放入到index.vue文件的template标签中。
(3)在css|less静态资源中找到有关【三级联动】的代码,将代码内容放入到index.vue文件的style标签中,并设置lang属性,以便能够正常处理less
(4)将该组件注册为全局组件:找到入口文件main.js,在该文件中将【三级联动组件】注册为全局组件。
(5)此时【三级联动组件】已经注册为全局组件,在其他地方使用它时,不需要进行引入和注册,直接使用即可。
拆分时要注意三部分:HTML、CSS、图片资源
(1)创建一个名为ListContainer的组件,按上小节的步骤对HTML和CSS进行拆分,这里需要注意的是:HTML中图片资源的路径可能已经发生了变化,需要根据目前的路径进行修改。
(2)该组件创建好之后,在Home组件中进行【引入】、【注册】和【使用】
(Recommend组件、Rank组件、TypeNav组件、Like组件的【创建、引入、注册和使用方式】和上述相同,这里不再赘述)
测试后端给的接口是不是可用,后端通常会给出服务器地址、请求地址、请求方式等等信息。根据这些信息,在POSTMAN工具中配置好这些信息。
首先,搞清楚为什么要进行二次封装?因为我们想使用请求拦截器和响应拦截器
【请求拦截器】:在发请求之前可以处理一些业务
【响应拦截器】:当服务器返回数据之后,可以处理一些业务
使用前先进行安装:npm install --save axios
可以在package.json中查看是否已经安装成功,如下
在项目中通常使用API文件夹放置【axios】相关内容,因此在src文件夹中创建一个api文件夹
在api文件夹中创建一个request.js的文件,在其中实现axios的二次封装,代码如下
- //对axios进行二次封装,
- import axios from 'axios'
-
- // 利用axios对象得方法create,去创建一个axios实例
- // request就是axios,只不过稍微配置一下
- const requests = axios.create({
- //配置对象
- //基础路径,发送请求的时候,路径当中会出现api
- baseURL:'/api',
- //代表请求超时的时间5S
- timeout:5000
- });
- // 请求拦截器
- requests.interceptors.request.use((config)=>{
- //config:配置对象,对象里面有一个属性很重要,header请求头
-
- return config;
- });
- // 响应拦截器
- requests.interceptors.response.use((res)=>{
- //成功的回调函数:服务器相应数据回来以后,响应拦截器可以检测到,可以做一些事情
- return res.data;
- },(error)=>{
- console.log(error)
- //响应失败的回调函数
- return Promise.reject(new Error('faile'))
- })
-
- //对外暴露
- export default requests;
如果项目规模很小,完全可以在组件的生命周期函数中发请求
如果项目规模比较大,会存在这样一种情况:有几十个组件使用了这个接口,后期接口变动了,就得一个个去修改组件当中接口的内容,很不方便。因此采用【接口统一管理】
在api文件夹中新创建一个js文件,名为index,在其中进行接口的统一管理
- //当前这个模块:API进行统一管理
- import requests from './request';
-
- //三级联动接口
- //暴露这个函数,外面拿到这个函数,直接调用,就能发送请求获取数据了
- export const reqCategoryList = ()=>{
- //返回的结果是promise对象 当前函数执行需要把服务器返回结果进行返回
- return requests({
- url:'/product/getBaseCategoryList',
- method:'get'
- })
- }
测试之后,发现请求发生404错误,这是因为【跨域问题】
解决跨域问题的方法有很多,这里采用【代理服务器】去解决,在vue.config.js文件中进行配置
- module.exports = {
- //打包时不要有map文件
- productionSourceMap:false,
- //关闭校验工具
- lintOnSave:false,
- //代理跨域
- devServer:{
- proxy:{
- '/api':{ //遇到带有api的请求,代理服务器才会将其转发
- target:'http://gmall-h5-api.atguigu.cn',
- // pathRewrite:{'^/api':''},
- }
- }
- }
- }
注意:这是一个配置文件,写好之后需要重新运行一下才可以~
先下载nprogress进度条:npm install --save nprogress,
下载完成之后在package.json中查看是否安装成功。
nprogress进度条需要在请求拦截器和响应拦截器中去使用
先引入进度条:import nprogress from 'nprogress'
还要引入进度条样式:import "nprogress/nprogress.css"
【请求拦截器】:启动进度条 nprogress.start( )
【响应拦截器】:结束进度条nprogress.done( )
vuex:并不是所有的项目都需要vuex,如果项目很小,则不需要;如果项目比较大,则需要使用vuex进行数据的统一管理
先安装vuex:npm install --save vuex,下载完成之后在package.json中查看是否安装成功
在src中新建一个文件夹store,用来实现vuex,创建index.js文件进行配置
- import Vue from 'vue'
- import Vuex from 'vuex'
- //需要使用插件一次
- Vue.use(Vuex)
- //state:仓库存储数据的地方
- const state = {}
- //mutation:修改state的唯一手段
- const mutation = {}
- //actions:可以书写自己的业务逻辑,也可以处理异步
- const actions = {}
- //getters:可以理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
-
- //对外暴露Store类的一个实例
- export default new Vuex.Store({
- state,
- mutations,
- actions,
- getters
- })
还要在入口文件main.js中引入这个仓库:import store from '@/store' 并进行注册
- import Vue from 'vue'
- import App from './App.vue'
- //三级联动组件+全局组件
- import TypeNav from "@/components/TypeNav"
-
- //第一个参数:全局组件的名字 第二个参数:哪一个组件
- Vue.component(TypeNav.name, TypeNav)
-
- //测试
- //引入仓库
- import store from '@/store'
-
- new Vue({
- render: h => h(App),
- //注册路由:底下的写法KV一致省略V
- //注册路由信息:当这里书写router的时候,组件身上都拥有$route,$router属性
- router,
- //注册仓库:组件实例的身上会多一个$store属性
- store
- }).$mount('#app')
接下来就要进行vuex的模块化开发了
为什么需要模块化开发?如果项目过大,组件过多,接口也很多,数据也很多,store对象会变得相当臃肿,因此可以让vuex实现模块化开发,即把一个大仓库拆分成一个个的小仓库。
可以给home、search等这样的模块单独设置一个store小模块,然后再把小模块混入到大模块中
- //home模块的小仓库
- const state = {};
- const mutations = {};
- const actions = {};
- const getters = {};
- export default {
- state,
- mutations,
- actions,
- getters
- }
- //大仓库
- 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
- }
- })
【三级联动】组件是一个全局组件,放在components文件夹中。
下面这个图就很好地展现出组件是如何获取数据的、仓库是如何去请求数据的
对三级联动组件TypeNav进行配置
- <script>
- import {mapState} from 'vuex';
-
- export default {
- name:'TypeNav',
-
- //组建挂载完毕:可以向服务器发请求
- mounted() {
- //通知vuex发请求,获取数据,存储于仓库中
- this.$store.dispatch('categoryList')
- },
-
- computed:{
- ...mapState({
- //右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立即执行一次
- //注入一个参数state,这指的是大仓库中的数据
- categoryList:(state)=>{
- return state.home.categoryList;
- }
- })
- }
- };
- </script>
找到home模块的小仓库,进行配置
- import {reqCategoryList} from '@/api';
- //home模块的小仓库
- const state = {
- //state中数据默认初始值别瞎写 【根据接口的返回值去初始化】
- categoryList:[],
- };
- const mutations = {
- CATEGORYLIST(state,categoryList){
- state.categoryList = categoryList
- },
- };
- const actions = {
- //通过API里面的接口函数调用,向服务器发送请求,获取服务器的数据
- async categoryList({commit}){ //对commit进行解构赋值
- let result = await reqCategoryList();
- if(result.code === 200){
- commit("CATEGORYLIST",result.data);
- }
- }
- };
- const getters = {};
- export default {
- state,
- mutations,
- actions,
- getters
- }
通过以上步骤,三级联动组件TypeNav就已经获取到数据啦!接下来就要把数据展示到页面上了。
对代码进行分析,发现一级目录很多,如下图这样:
因此可以只留一个,并通过v-for进行优化
<div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId">
则一级目录的a标签名称也要改
<a href=" ">{{c1.categoryName}}</a>
二级分类也很多,同样采用v-for进行优化
<div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId" >
则二级目录的a标签名称也要改变
<a>{{c2.categoryName}}</a>
三级分类也很多,同样采用v-for进行优化
<em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
则三级目录的a标签名称也要改变
<a>{{c3.categoryName}}</a>
第一种解决方案:直接添加CSS样式(这里不用,因为很简单,来些具有挑战性的,哈哈哈)
第二种解决方案:动态添加类名
先来理一下思路:
1. 在data中定义一个变量,名为currentIndex,初始值设置为-1(不能设置为0-15之间的数,总共有16个标题)
- data() {
- return {
- //存储用户鼠标移上哪一个一级分类
- currentIndex: -1
- }
- },
2. 为标题绑定一个原生JS事件mouseenter,并传入index,事件的回调函数定义在methods中,在回调函数中,将传入的值赋给currentIndex,这样就能拿到鼠标移动到的当前标题的index了
<h3 @mouseenter="changeIndex(index)">
- methods:{
- enterShow(){
- this.show = true
- },
- }
3. 在一级标题的循环中,判断currentIndex==index是否成立,成立的话就添加一个类,这个类就实现了添加背景色的效果。
<div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId" :class="{cur:currentIndex == index}">
实现完成之后,发现存在一个问题,鼠标移除之后还有背景颜色,这是不合理的,应该背景颜色去掉才可以。出现问题不用慌,解决就是了,再给标题添加一个鼠标移除事件喽,
但是又出现了一个问题,鼠标移到“全部商品分类”上,背景颜色应该还是存在的。(个人觉得这个实现完全没必要,看起来更像是个BUG,为了练手,还是实现一下吧)
其实就用到了事件委派,就“全部商品分类”和“三级联动”放在同一个div中,且二者是兄弟关系
- <!-- 事件的委派 -->
- <div @mouseleave="leaveShow">
- <h2 class="all">全部商品分类</h2>
- <!-- 三级联动 -->
- <div class="sort">
- </div>
- </div>
鼠标移动到哪个标题,就展示哪个标题下的二三级分类列表
第一种解决方案:直接改变CSS样式
第二种解决方案:通过JS实现
思路:在上一节中,我们已经通过事件监听将一级标题的index传递给了data中的currentIndex变量,如果index==currentIndex,则将二三级分类的样式设置为display:'block',否则设置为“none”
<div class="item-list clearfix" :style="{display:(currentIndex == index ? 'block':'none')}">
防抖:前面的所有的触发都被取消,最后一次执行在规定时间之后才会触发,也就是说如果连续快速地触发,只会执行一次。
节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发。
实现的时候利用一个插件,叫做lodash,里面封装了防抖与节流的业务【闭包+延时器】
这里举一个防抖的小栗子:输入框输入数据时,进行Ajax请求
如果不采用防抖的话,每输入一个字就要发一次请求,假如我们输入“梅西世界杯”,会发送五次请求。这并不满足我们的实际需求,我们想要输入完这五个字,才会发送请求,因此采用防抖技术进行解决。
- let input = document.querySelector('imput')
- //不加防抖
- input.oninput = function(){
- //这里放ajax发请求的代码
- }
- //加了防抖
- input.oninput = _.debounce(function(){
- //这里放ajax发请求的代码
- },1000);
这里举一个节流的小栗子:实现一个简单的计时器,即点击按钮,实现数字元素的增加
- <h1>我是计时器<span>0</span></h1>
- <button>点击我加上1</button>
- ....
-
- let span = document.querySelector('span');
- let button = document.querySelector('button');
- let count = 0;
- //未加节流
- button.onclick = function(){
- count++;
- span.innerHTML = count;
- }
- //加了节流
- button.onclick = _.throttle(function(){
- count++;
- span.innerHTML = count;
- },1000);
在项目中实现节流:三级联动这里用户的交互操作可能会过快,导致浏览器反应不过来,如果当前回调函数中有一些大量业务,有可能出现卡顿现象。
vue脚手架中已经下载好了lodash,可直接全部引入lodash内容: import _ from 'lodash'
这里我们可以按需引入,只引入节流:import throttle from 'lodash'
- //未加节流的代码
- changeIndex(index){
- this.currentIndex = index;
- }
- //加了节流的代码
- //throttle回调函数别用箭头函数,可能会出现上下文this
- changeIndex:throttle(function(index){
- //index:鼠标移上某一个一级分类的元素的索引值
- //正常情况(用户慢慢地操作):鼠标进入,每一个一级分类h3,都会触发鼠标进入事件
- //非正常情况(用户操作很快):本身全部的一级分类都应该触发鼠标进入事件,但是经过测试,只有部分h3触发了
- //就是由于用户的行为过快,导致浏览器反应不过来,如果当前回调函数中有一些大量业务,有可能出现卡顿现象。
- this.currentIndex = index;
- },50),
关于路由,我发了一篇vue-router思维导图的文章,可以帮助大家回忆起相关内容
链接在此:vue路由知识点概括--思维导图_yuran1的博客-CSDN博客
对于三级联动,用户可以点击的:一级分类、二级分类、三级分类,当我们从Home模块跳转到Search模块时,一级会把用户选中的产品(比如产品的名字、产品的ID)在路由跳转的时候进行相应的传递。
注意:这里如果使用的是声明式路由导航,可以实现路由的跳转与传递参数,但需要注意,会出现卡顿的现象,这是为什么呢?
原因:router-link可以看作是组件,当服务器的数据返回之后,由于v-for的设置,会循环出很多的router-link组件,这种方法很消耗内存,所以会出现卡顿的现象。因此这里采用编程式路由导航。
但是那么多a标签,都给它们绑定click事件的回调函数的话,肯定太繁琐、太消耗内存了。
事件委派又派上用场了,我们把click事件的回调函数放在父元素身上,不用再一一绑定了。
<div class="all-sort-list2" @click="goSearch">
但是利用事件委派之后,还存在一些问题:
1. 你怎么知道点击的一定是a标签的?也有可能是div、h3等标签
2. 如何获取参数呢?【1、2、3级分类的产品的名字、id】,如何区分1、2、3级分类的标签?
解决方法看下一节
为了解决上述问题,这里利用【自定义属性】来解决
为解决第一个问题:为a标签加上自定义属性data-categoryName,其余的子节点是没有的。
- //一级分类
- <a :data-categoryName="c1.categoryName">{{ c1.categoryName }}</a>
- //二级分类
- <a :data-categoryName="c2.categoryName">{{ c2.categoryName }}</a>
- //三级分类
- <a :data-categoryName="c3.categoryName">{{ c3.categoryName }}</a>
在前面的章节中,我们可以知道goSearch( )函数中放置的是进行路由跳转的方法
我们点击子节点就可以触发goSearch( )这个回调函数,在函数中通过event.target拿到被点击的节点元素element,节点身上有一个属性dataset属性,可以获取节点的自定义属性与属性值,可以通过解构赋值取出来,如果有categoryname属性,那么被点击的就是a标签了
注意:有些同学有疑惑了,自定义属性为data-categoryName,那么判断条件应该这样写
if(data-categoryName) {......}
然而实际上是这样写的:
if(categoryname) {......}
原因是:需要在定义属性的时候在前面加上data-才能被dataset函数获取,因此data-只是一个前缀,其次浏览器会自动将属性名转化为小写。
为解决第二个问题:分别为1、2、3级的a标签加上自定义属性data-category1Id、data-category2Id、data-category3Id,其余的子节点是没有的。
- <a :data-categoryName="c1.categoryName"
- :data-category1Id="c1.categoryId"
- >{{ c1.categoryName }}</a>
-
- <a :data-categoryName="c2.categoryName"
- :data-category1Id="c2.categoryId"
- >{{ c2.categoryName }}</a>
-
- <a :data-categoryName="c3.categoryName"
- :data-category1Id="c3.categoryId"
- >{{ c3.categoryName }}</a>
采取和判断a节点一样的方法,判断点击的节点是1级、2级还是3级,这里不再赘述了。
到此,问题就解决了,接下来就要实现在路由跳转中携带参数了,下面直接上代码:
- goSearch(event) {
- //获取到当前触发这个事件的节点,从中筛选出带有data-categoryname这样的节点
- //节点有一个属性dataset属性,可以获取节点的自定义属性和属性值
- let element = event.target;
- //获取到的变量已经不是驼峰形式了,自动改变的
- let { categoryname, category1id, category2id, category3id } =
- element.dataset;
- if (categoryname) {
- //整理路由跳转的参数
- let location = { name: "search" };
- let query = { categoryName: categoryname };
- //一级分类、二级分类、三级分类的a标签
- if (category1id) {
- query.category1Id = category1id;
- } else if (category2id) {
- query.category2Id = category2id;
- } else {
- query.category3Id = category3id;
- }
- location.query = query;
- //路由跳转
- this.$router.push(location);
- }
- },
从Home主页点击三级分类的内容,就可以跳转到Search模块
Search模块也有三级联动组件,但是它在Search模块中默认情况下是隐藏的,但是在Home模块下默认是显示的。因而这里使用v-show属性对三级联动组件进行修改,
当处于Home模块下,v-show = true;当处于Search模块下,v-show = false;(通过路由信息判断)
- //三级联动
- <div class="sort" v-show="show">
- ......
- </div>
- .
- .
- .
- data() {
- return {
- //存储用户鼠标移上哪一个一级分类
- currentIndex: -1,
- show: true,
- };
- },
- //组建挂载完毕:可以向服务器发请求
- mounted() {
- //通知vuex发请求,获取数据,存储于仓库中
- // this.$store.dispatch('categoryList') 考虑到性能将其挪到了【App.vue】
- //当组件挂载完毕,让show的属性变为false
- //如果不是Home路由组件,将typeNav进行隐藏
- if (this.$route.path != "/home") {
- this.show = false;
- }
- },
但是它总不能一直隐藏吧,当鼠标移入到 “全部商品分类” 那里,就要显示三级联动的内容了,而鼠标移出后,又要隐藏了。
- <div @mouseleave="leaveShow" @mouseenter="enterShow">
- <h2 class="all">全部商品分类</h2>
- //三级联动
- <div class="sort" v-show="show">
- ......
- </div>
- </div>
- .
- .
- .
- //当鼠标移入的时候,让商品分类列表进行展示
- enterShow() {
- this.show = true;
- },
- //当鼠标离开的时候,让商品分类列表进行隐藏
- leaveShow() {
- this.currentIndex = -1;
- //判断不是Home路由组件的时候才会执行
- if (this.$route.path != "/home") {
- this.show = false;
- }
- },
接下来实现过渡动画
前提:组件或元素务必要有v-if或v-show指令才可以进行过渡动画
- //过渡动画
- <transition name="sort">
- //三级联动
- <div class="sort" v-show="show">
- ...
- </div>
- </transition>
- //过渡动画的样式
- //过渡动画开始状态(进入)
- .sort-enter {
- height: 0px;
- }
- //过渡动画结束状态(进入)
- .sort-leave {
- height: 461px;
- }
- //定义动画时间、速率
- .sort-enter-active {
- transition: all 0.5s linear;
- }
从Home模块跳转到Search模块:首先TypeNav在Home模块中挂载时,会向后台请求数据,当跳转到Search模块时,Home组件销毁,当中的TypeNav也销毁,Search组件挂载,当中的TypeNav也挂载,挂载时又要发一次请求。
综上可知,发了两次请求,性能不够好。在这个应用中,我就只想请求一次,怎么办?
先来分析一下:首先执行入口文件main.js,其中有App路由组件,她是唯一一个根组件,因此不管如何,她都只会挂载一次。那我们把TypeNav中派发action的操作(用于请求数据)放在App.vue中,就能实现仅请求一次的效果了。
如果放在main.js中可行吗?不行,因为main.js不是一个组件,而是一个js文件,派发action时,this为undefined
前面我们已经实现了点击三级联动分类,从Home主页跳转到Search模块,携带了query参数。如果这时我们在输入框输入内容进行搜索时,会发现携带的query参数没有了,只有刚刚请求的params参数了。两者是不能同时存在的,这显然不符合我们应用场景的。
假如:在三级分类中选择“手机”进入到了Search模块,这时我想在此基础上搜“华为”,如果只携带华为这个参数,那返回来的数据可能会包含华为手表、华为汽车等不相关信息。
首先,如果路由跳转的时候,带有params参数,要和query参数一起捎带过去
-
- goSearch(event) {
- .
- .
- .
- //判断:如果路由跳转的时候,带有params参数,携带参数传递过去
- if (this.$route.params) {
- location.params = this.$route.params;
- //整理完参数
- location.query = query;
- //路由跳转
- this.$router.push(location);
- }
- }
- },
然后,在head组件中,点击搜索时进行路由跳转,如果有query参数,要和params一起捎带过去
- goSearch(){
- .
- .
- .
- //如果有query也携带过去
- if(this.$route.query){
- let location = {name:'search',params:{keyword:this.keyword || undefined}}
- location.query = this.$route.query;
- this.$router.push(location)
- }
- },
服务器返回的数据(接口)只有商品分类菜单分类数据,对于ListContainer组件与Floor组件数据,服务器都没有提供,因此这里使用mock.js去模拟一些数据。
官网对Mock.js的解释:生成随机数据,拦截Ajax请求。
安装mock.js:cnpm install --save mock.js
使用步骤:
1. 在项目中src文件夹中创建mock文件夹
2. 准备预先设置好的JSON数据(mock文件夹中创建相应的JSON文件)
举个例子,下面是有关轮播图的JSON数据
- [{
- "id": "1",
- "imageUrl": "/images/banner1.jpg"
- },
- {
- "id": "2",
- "imageUrl": "/images/banner2.jpg"
- },
- {
- "id": "3",
- "imageUrl": "/images/banner3.jpg"
- },
- {
- "id": "4",
- "imageUrl": "/images/banner4.jpg"
- }
- ]
注意:JSON数据需要格式化一下,别留有空格,否则跑不起来
3. 把mock数据需要的图片资源放置到public文件夹中,因为public文件夹在打包的时候,会把相应的资源原封不动地打包到dist文件夹中。
4. 开始mock,通过mockjs模块实现,在mock文件下创建一个名为mockServer.js文件
- /*
- 利用mockjs提供mock接口
- */
- import Mock from 'mockjs'
- // JSON数据格式根本没有对外暴露,但是可以引入
- // webpack默认对外暴露的:图片、JSON数据格式
- import floors from './floors.json'
- import banners from './banners.json'
-
- // 提供广告轮播接口 第一个参数是请求地址,第二个参数是请求数据
- Mock.mock('/mock/banners', {code: 200, data: banners})//模拟首页大的轮播图的数据
- // 提供floor接口
- Mock.mock('/mock/floors', {code: 200, data: floors})
- console.log('MockServer')
5. mockServer.js文件在入口文件main.js中引入(至少需要执行一次,才能模拟数据)
在api文件夹中创建一个名为mockAjax.js的文件,专门用来请求mock数据。
需要注意:baseURL要改为'/mock'
- //对axios进行二次封装,
- import axios from 'axios'
- //引入进度条
- import nprogress from 'nprogress'
- //在当前模块中引入store
- //引入进度条的样式
- import "nprogress/nprogress.css"
-
- // 利用axios对象得方法create,去创建一个axios实例
- // request就是axios,只不过稍微配置一下
- const requests = axios.create({
- //配置对象
- //基础路径,发送请求的时候,路径当中会出现api
- baseURL:'/mock',
- //代表请求超时的时间5S
- timeout:5000
- });
- //请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
- requests.interceptors.request.use((config)=>{
- //config:配置对象,对象里面有一个属性很重要,header请求头
- //进度条开始动
- nprogress.start();
- return config;
- });
- //响应拦截器
- requests.interceptors.response.use((res)=>{
- //成功的回调函数:服务器相应数据回来以后,响应拦截器可以检测到,可以做一些事情
- nprogress.done();
- return res.data;
- },(error)=>{
- console.log(error)
- //响应失败的回调函数
- return Promise.reject(new Error('faile'))
- })
-
- //对外暴露
- export default requests;
-
在同文件夹下的index.js文件中写【Home首页轮播图接口】,切记url地址中不带mock,因为前面已经配置过了
export const reqGetBannerList = () => mockRequests.get('/banners') //简写形式
mock数据以及接口都准备完毕后,就要发送请求去获取数据啦
当ListContainer组件挂载时(mounted),派发action,通过vue发起ajax请求,将数据存储在仓库中:
- mounted() {
- this.$store.dispatch('getBannerList');
- }
之后在store文件夹下的home文件夹下的index.js中,进行vuex的配置
- const state = {
- ...
- //轮播图的数据
- bannerList:[]
- };
- const actions = {
- .
- .
- .
- //获取首页轮播图的数据
- async getBannerList({commit}){
- let result = await reqGetBannerList();
- if(result.code == 200){
- commit('GETBANNERLIST',result.data)
- }
- }
- };
- const mutations = {
- ...
- GETBANNERLIST(state,bannerList){
- state.bannerList = bannerList;
- }
- };
这个时候还没有结束哦,ListContainer组件还没拿到这个数据呢,因此可以使用mapState
- import {mapState} from 'vuex';
-
- export default {
- name:'ListContainer',
- mounted() {
- this.$store.dispatch('getBannerList');
- }
- computed:{
- ...mapState({
- bannerList:state => state.home.bannerList
- })
- },
- }
在swiper官网下载5版本:下载Swiper - Swiper中文网
关于使用过程,官网给的教程非常详细,自己看看实际操作一下,这里就不再赘述了。
需要注意:
1. 在new Swiper实例之前,页面中的结构必须有,因为我们要操作DOM
2. 第一个参数可以是字符串(选择器)也可以是真实DOM节点
1. 首先安装Swiper插件:选择5版本,6版本会有一些问题:npm install --save swiper@5
2. 引包(相应JS|CSS):
在组件文件中引入:import Swiper from ‘swiper’ --->引入了JS内容
对于样式来说,可以在每个相关组件中引入,但是因为很多地方都用到了轮播图,且样式是一样的,因此可以在入口文件main.js中引入样式,会更加简洁。
即:import "swiper/css/swiper.css"
注意:引入样式的时候,不用import ... from ... ,没有对外进行暴露
3.在模板语法中,我们发现目前只使用一张图片,但是轮播图却是很多张,因此需要使用v-for进行遍历
- <div class="swiper-container" id="mySwiper">
- <div class="swiper-wrapper">
- <div class="swiper-slide" v-for="(carousel,indx) in bannerList" :key="carousel.id">
-
- <img :src="carousel.imgUrl" />
-
- </div>
- </div>
- </div>
4. 使用Swiper
new Swiper这个过程要放在哪里写呢?放在mounted( )钩子函数中写,因为这个时候页面结构已经实现好了,符合条件。
但是写了之后,发现没有效果!那这又是因为什么呢?因为结构还不完整!
什么!结构怎么还不完整?原因就在于上面那段代码,我们使用v-for去遍历图片,图片的数据是通过axios请求获得的,涉及到了异步,只有请求数据回来了,此时的结构才能是完整的!
因此可以添加一个延迟函数,延迟使用new Swiper,但是这个方法不好用,延迟效果比较鸡肋。比如轮播图中间的小点点得等待一段时间才能够显示出来。
- setTimeout(()=>{
- var mySwiper = new Swiper(document.querySelector(".swiper-container"),{
- loop:true,
- //如果需要分页器
- pagination:{
- el:".swiper-pagination",
- },
- //如果需要前进后退按钮
- navigation:{
- nextEl:'.swiper-button-next',
- prevEl:'.swiper-button-prev',
- },
- });
- },1000)
当然,我们也可以把new Swiper放在updated( )钩子函数中,但是如果vue组件中有其他数据的话,其他数据发生改变,就要实现这个new Swiper操作,很浪费内存,不推荐使用,但是效果是正常的。
点击轮播图中的小球,不发生图片的转换,这里就要配置一个属性:clickable:true,放在pagination里。
如果大家不知道nextTick( )是什么,可以看一下我之前发的相关文章
链接在这里:VUE中nextTick( )函数思维导图_yuran1的博客-CSDN博客
使用watch监听bannerList的变化,如果有变化,就会触发watch属性中的handle回调函数,我们可以把new Swiper的过程放在这个回调函数中执行。
但是运行的结果还是不行,说明new Swiper前,页面结构还是不完整的,虽然说数据获取成功了,但是不能保证v-for执行完毕。
为了解决这个问题,就要使用nextTick( )函数了
用法【官方解释】:在下次DOM更新循环结束之后,执行延迟回调。在修改数据之后,立即使用这个方法,获取更新后的DOM
1. 首先编写API接口,获取floor数据
- //获取floor数据
- export const reqFloorList = () => mockRequests.get('/floors')
2. 写VUEX三连环
- import {reqCategoryList, reqGetBannerList,reqFloorList} from '@/api';
- //home模块的小仓库
- const state = {
- //state中数据默认初始值别瞎写 【根据接口的返回值去初始化】
- categoryList:[],
- //轮播图的数据
- bannerList:[],
- //floor组件的数据
- floorList:[],
- };
- const mutations = {
- CATEGORYLIST(state,categoryList){
- state.categoryList = categoryList
- },
- GETBANNERLIST(state,bannerList){
- state.bannerList = bannerList;
- },
- REQFLOORLIST(state,floorList){
- state.floorList = floorList
- }
- };
- const actions = {
- //通过API里面的接口函数调用,向服务器发送请求,获取服务器的数据
- async categoryList({commit}){ //对commit进行解构赋值
- let result = await reqCategoryList();
- if(result.code === 200){
- commit("CATEGORYLIST",result.data);
- }
- },
- //获取首页轮播图的数据
- async getBannerList({commit}){
- let result = await reqGetBannerList();
- if(result.code == 200){
- commit('GETBANNERLIST',result.data)
- }
- },
- //获取floors数组
- async getFloorList({commit}){
- let result = await reqFloorList();
- if(result.code == 200){
- commit('REQFLOORLIST',result.data)
- }
- }
- };
- const getters = {};
- export default {
- state,
- mutations,
- actions,
- getters
- }
3. 在Home组件中触发action,为什么不在Floor组件中去触发。因为Floor组件要进行复用,如果在Floor组件中通过mapState收到了返回的数据,那将无法创建出不同的Floor组件。而Home组件正是使用Floor组件的地方,可以在这里去触发action,从而拿到相应的数据,通过v-for赋给不同的Floor组件不同的数据。
- <template>
- <div>
- <!-- 三级联动全局组件,已经注册为全局组件 -->
- ......
- <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/>
- ......
- </div>
- </template>
-
- <script>
- import ........
-
- export default {
- name:'HomeIndex',
- components:{
- ......
- },
- mounted() {
- //派发action,获取floor组件的数据
- this.$store.dispatch("getFloorList")
- ......
- },
- computed:{
- ...mapState({
- floorList:state => state.home.floorList
- })
- }
- }
- </script>
- <style>
- </style>
4. 从上面代码中可以看出父组件Home向子组件Floor传递数据 :list="floor"
子组件用props接收数据
- export default {
- name:'FloorMsg',
- props:['list'],
- ......
- }
首先通过浏览器的vue网络工具检查组件的各种属性和方法
根据上图这些内容实现【Floor组件的动态展示】
比如:
<h3 class="fl">{{list.name}}</h3> //标题
- <li class="active" v-for="(nav,index) in list.navList" :key="index">
- <a href="#tab1" data-toggle="tab">{{nav.text}}</a>
- </li>
等等,只要需要动态展示的内容都需要进行相应处理,这里不再一一进行解释
在上述处理中,我们还发现需要设置轮播图,章节二十八、二十九已经介绍过swiper具体的适用步骤,这里也不再赘述了。
但需要注意一点:上次书写Swiper的时候,在mounted( )函数中书写是不可以的,但是为什么在这里就可以了!
原因:上次书写轮播图的时候,是在当前组件内部发请求,动态渲染结构【前台至少服务器数据需要回来】,因此这里的写法在当时是不可行的。现在的这种写法为什么可以?因为请求是父组件发的,父组件通过props传递过来的,而且结构都已经都有了的情况下执行mounted( ),此时页面结构已经是完整的了。
把首页中的轮播图拆分成一个共用全局组件Carsouel,在components文件夹中新建一个名为Carsouel的文件夹,用来书写轮播图组件
- <template>
- <!-- 轮播图的地方 -->
- <div class="swiper-container" ref="cur">
- <div class="swiper-wrapper">
- <div class="swiper-slide" v-for="(carousel,index) in list" :key="carousel.id">
- <img :src="carousel.imageUrl" />
- </div>
- </div>
- <!-- 如果需要分页器 -->
- <div class="swiper-pagination"></div>
- <!-- 如果需要导航按钮 -->
- <div class="swiper-button-prev"></div>
- <div class="swiper-button-next"></div>
- </div>
- </template>
-
- <script>
- //引包
- import Swiper from 'swiper';
- export default {
- name:'Carousel',
- props:['list'],
- watch:{
- list:{
- //立即监听,不管数据有没有变化,我上来就监听一次
- //为什么watch监听不到list:因为这个数据从来没有发生过变化,父亲给的时候就是一个对象,对象里面该有的数据都是有的
- immediate:true,
- handler(){
- //只能监听到数据已经有了,但是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',
- },
- });
- });
- }}
- }};
- </script>
需要注意的点:
1. v-for循环(v-for="(carousel,index) in list" )中的list是通过props传递过来的
2. 为什么watch监听不到list?因为这个数据从来没有发生过变化,父亲给的时候就是一个对象,对象里面该有的数据都是有的。因此设置immediate:true,即无论如何都得监测一次
3. 只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定,因此还是需要用到nextTick(vue异步更新机制)
Carousel是一个全局组件,需要在全局文件main.js中引入和注册
- //引入轮播图组件
- import Carousel from "@/components/Carousel"
- //注册轮播图组件
- Vue.component(Carousel.name, Carousel)
然后回到Floor组件中,在轮播图的地方使用Carousel组件
- <div class="floorBanner">
- <!-- 轮播图的地方 -->
- <Carousel :list="list.carouselList"/>
- </div>
注意,要传递数据:list.carouselList
切记:以后在开发项目的时候,如果看到某一个组件在很多地方都使用,你把它变为全局组件,注册一次,可以在任意地方使用,公用的组件|非路由组件放在components文件夹中
先理清一下Search模块开发步骤
1. 先静态页面 + 静态组件拆分出来
2. 发请求(API)
3. VUEX(三连环)
4. 组件获取仓库数据,动态展示数据
静态组件的拆分很简单,就是把相应的html代码和css代码拆分出来,放在一个组件里。这里就不再赘述了
首先查阅api前台接口文档,确定请求方式、请求URL以及请求参数等
- //当前这个函数需要接受外部传递参数
- //当前这个接口,给服务器传递参数params,至少得是一个空对象
- //如果连空对象都没有,那么请求会失败的
- export const reqGetSearchInfo = (params) => requests(
- {
- url:"/list",
- method:'post',
- data:params
- }
- )
注意:
1. 当前这个函数需要接受外部传递参数
2. 当前这个接口,给服务器传递参数params,至少得是一个空对象。如果连空对象都没有,那么请求会失败的
在store文件夹中的search.js文件中进行【vuex三连环】
- import { reqGetSearchInfo } from "@/api";
- //search模块的小仓库
- const state = {
- //仓库初始状态
- searchList:{},
- };
- const mutations = {
- GETSEARCHLIST(state,searchList){
- state.searchList = searchList
- }
- };
- const actions = {
- //获取search模块的数据
- async getSearchList({commit},params={}){
- //当前这个reqGetSearchInfo这个函数在调用获取服务器数据的时候,至少传递一个参数(空对象)
- //params形参,是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象
- let result = await reqGetSearchInfo(params)
- if(result.code == 200){
- commit('GETSEARCHLIST',result.data)
- console.log(result.data)
- }
- }
- };
- export default {
- state,
- mutations,
- actions,
- getters
- }
注意:仓库初始状态 searchList:{ },为什么是一个对象而不是一个数组呢?
这当然不是让我们进行凭空猜测啦,需要进行验证:在Search组件中mounted( )中去派发相应的action(getSearchList),this.$store.dispatch('getSearchList', { })
然后通过浏览器的network工具就可以查看请求回来的数据了,从而可以判断数据是什么格式
- import {mapState} from 'vuex'
- computed:{
- ...mapState({
- goodsList:state => state.search.searchList.goodsList
- })
- }
上述这段代码虽然可以获取到数据,但是太麻烦,写了一连串的内容,不仅容易出错还不美观
接下来使用getters进行优化,
在项目中,VUEX中的getters是为了简化仓库中的数据而生,想让其他组件捞数据的时候更简单一些,可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候就方便了】
- const getters = {
- //当前形参state是当前仓库中的state,并非大仓库中的state
- goodsList(state){
- //如果网络不给力,返回的是undefined,这样不能遍历
- //计算新的属性的属性值至少是一个数组
- return state.searchList.goodsList || [];
- },
- trademarkList(state){
- return state.searchList.trademarkList || [];
- },
- attrsList(state){
- return state.searchList.attrsList || [];
- }
- };
- import {mapGetters} from 'vuex'
-
- computed: {
- //mapGetters里面的写法:传递的数据,因为getter计算是没有划分模块【home、search】
- //补充:state是划分模块了state.home / state.search
- ...mapGetters(["goodsList", "trademarkList", "attrsList"]),
- }
分析页面的结构,对于【销售产品列表】,结构都是一样的,可以使用【v-for】进行遍历
<li class="yui3-u-1-5" v-for="(good, index) in goodsList":key="good.id">
<li></li>标签内部的动态数据也需要更改,比如图片、价格等,比较简单,不再详细叙述了
在前面内容中,在Search模块中,我们是在mounted( )钩子函数中去dispatch action 从而获取到相应的数据,但是这里存在一个问题,由于mounted( )钩子函数只能挂载一次,这导致只能请求一次数据,这并不符合应用的实际需求。
解决方法:在methods中创建一个函数getData( ),只要想请求数据就调用该函数,根据不同的参数返回不同的数据进行展示。
- methods: {
- //向服务器发送请求获取search模块数据(根据参数不同返回不同的数据进行展示)
- //把这次请求封装为一个函数,当你需要在调用的时候调用即可
- getData() {
- //先测试接口返回的数据模式
- this.$store.dispatch("getSearchList", this.searchParams); //dispatch是异步操作
- },
- }
由于组件挂载的时候,要获取相应的数据,因此在mounted( )去调用getData( )
(至于什么情况下再调用getData去获取数据,这里先不说,请看之后的章节)
对于请求参数而言,从项目开发文档中能发现【携带的参数】至少是10个,参数必须是可以变动的(ps:需要根据不同的参数请求不同的数据),因此把这些参数放入到data中。
下面对各个参数进行解释:
- data() {
- return {
- //带给服务器的参数
- searchParams: {
- //一级分类的id
- category1Id: "",
- //二级分类的id
- category2Id: "",
- //三级分类的id
- category3Id: "",
- //分类名字
- categoryName: "",
- //关键字
- keyword: "",
- //排序:初始状态应该是综合|降序
- order: "1:desc",
- //分页器用的:代表的是当前是第几页
- pageNo: 1,
- //代表的是每一页展示数据的个数
- pageSize: 10,
- //平台售卖属性操作带的参数
- props: [],
- //品牌
- trademark: "",
- },
- };
- },
在data中,参数是初始化的,还没有对参数进行赋值。因此需要在正式请求之前,对参数进行更新。更新这一过程需要在mounted( )之前进行,因此将放在beforeMount( )钩子函数中。
- //当组件挂载完毕之前执行一次【先与mounted之前】
- beforeMount() {
- //在发送请求之前,把接口需要传递的参数,进行整理
- //复杂的写法
- // this.searchParams.category1Id = this.$route.query.category1Id;
- // this.searchParams.category2Id = this.$route.query.category2Id;
- // this.searchParams.category3Id = this.$route.query.category3Id;
- // this.searchParams.categoryName = this.$route.query.categoryName;
- // this.searchParams.keyword = this.$route.params.keyword;
- Object.assign(this.searchParams, this.$route.params, this.$route.query);
- },
在Search模块中有一个子组件SearchSelector,在这个子组件中通过【mapGetters】获取vuex中的数据,然后对template中的数据进行更改
为了可以【再次】发请求获取不同的数据,这里首先要确定【再次发请求】的时机:也就是说当路由发生变化的时候,说明需要再次发请求了。因此需要对路由的变化进行监测,即使用【watch】
- //数据监听:监听组件实例身上的属性的属性值变化
- watch: {
- //监听路由的信息是否发生变化,如果发生变化,则再次发送请求
- $route(newValue, oldValue) {
- //再次发送请求之前整理带给服务器的参数
- Object.assign(this.searchParams, this.$route.params, this.$route.query);
- //再次发起ajax请求
- this.getData();
- //每一次请求完毕,应该把相应的1、2、3级分类的id置空,让他接受下一次的相应1、2、3id
- //分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
- this.searchParams.category1Id = "";
- this.searchParams.category2Id = "";
- this.searchParams.category3Id = "";
- },
- },
- };
PS:这里老师说关键字keyword不需要置空,但是从真正使用上来说,应该要置空的,否则会影响用户的体验。(京东就对此进行了清空)
面包屑总共有四类:【分类的面包屑】、【关键字的面包屑】、【品牌的面包屑】、【平台的售卖的属性值展示】
此外,面包屑这部分不应该是死的,应该是动态的。
在Search模块中通过searchParams可以拿到【商品分类】的数据,可作为分类面包屑
在这里通过v-if进行显示判断
- <!-- 分类的面包屑 -->
- <li class="with-x" v-if="searchParams.categoryName">
- {{searchParams.categoryName}}
- <i @click="removecategoryName">×</i>
- </li>
上面代码中给 i标签添加了一个点击事件,即删除该面包屑,那么就要重新去请求数据了
- //删除分类的名字
- removecategoryName() {
- //把带给服务器的参数置空了,还需要向服务器发请求
- //带给服务器参数的说明是可有可无的,属性值为空的字符串还是会把相应的字段带给服务器
- //但是你把相应的字段变为undefined。当前这个字段不会带给服务器,减少带宽消耗
- this.searchParams.categoryName = undefined;
- this.searchParams.category1Id = undefined;
- this.searchParams.category2Id = undefined;
- this.searchParams.category3Id = undefined;
- this.getData();
- //地址栏也需要修改,进行路由的跳转(现在的路由跳转只是跳转到自己这里)
- //严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着params参数
- if (this.$route.params) {
- this.$router.push({ name: "search", params: this.$route.params });
- }
- },
【关键字面包屑】和【分类面包屑】的实现原理是一样的
首先通过v-if进行显示判断
- <!-- 关键字的面包屑 -->
- <li class="with-x" v-if="searchParams.keyword">
- {{ searchParams.keyword }}
- <i @click="removeKeyword">×</i>
- </li>
再给 i标签 绑定一个监听事件,即去除这个面包屑时,需要重新请求数据
- //删除关键字
- removeKeyword() {
- //给服务器带的参数searchParams的keyword置空
- this.searchParams.keyword = undefined;
- this.getData();
- if (this.$route.query) {
- this.$router.push({ name: "search", query: this.$route.query });
- }
- //将搜索框中的内容置空,同级组件之间进行通信
- //通知兄弟组件Header删除关键字
- this.$bus.$emit("clear");
- },
从上面代码中可以看出:为了将搜索框中的内容清空,需要search组件和home组件进行通信,
这两个组件属于兄弟组件,可以使用【全局事件总线】进行通信
- //Home组件
- mounted() {
- //通过全局事件总线清楚关键字
- this.$bus.$on('clear',() => {
- this.keyword = " ";
- })
- },
这部分和前两部分有一些区别,
首先需要注意,品牌这部分内容不在Search组件中,而是在Search组件的子组件SearchSelector中。先给各个品牌绑定一个点击事件tradeMarkHandler,并传入参数trademark
- <ul class="logo-list">
- <li v-for="(trademark,index) in trademarkList" :key="trademark.tmId"
- @click="tradeMarkHandler(trademark)">{{trademark.tmName}}
- </li>
- </ul>
- methods: {
- //品牌的事件处理函数
- tradeMarkHandler(trademark){
- //点击了品牌,还是需要整理参数,向服务器发送请求获取相应的数据,并进行展示
- //为什么是Search发请求,为什么呢?因为父组件中searchParams参数是带给服务器的,子组件把你
- //点击的品牌的信息给父组件传递过去
- this.$emit('trademarkInfo', trademark);
- },
- }
从上面的代码中可以看出子向父通信使用自定义事件,子组件通过$emit触发自定义事件trademarkInfo,并传递相应的参数
而在父组件Search中绑定自定义事件,并设置自定义事件的回调函数,并接收传递过来的参数
<SearchSelector @trademarkInfo="trademarkInfo" />
- //自定义事件的回调
- trademarkInfo(trademark) {
- //整理品牌字段的参数(按照固定的格式)
- this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
- //需要再次发送请求,获取
- this.getData();
- },
除此之外,还要将【品牌面包屑】进行展示,首先通过v-if进行显示判断
- <!-- 品牌的面包屑 -->
- <li class="with-x" v-if="searchParams.trademark">
- {{ searchParams.trademark.split(":")[1]}}
- <i @click="removetrademark">×</i>
- </li>
再给 i标签 绑定一个监听事件,即删除这个品牌面包屑后,需要重新发请求去获取数据
- //删除品牌
- removetrademark() {
- this.searchParams.trademark = undefined;
- this.getData();
- },
【平台售卖属性】这部分的内容不在Search组件中,而是在Search的子组件SearchSelector中,
先给平台售卖属性绑定一个点击事件,并传入两个相应的参数(attr, attrvalue)
- <li v-for="(attrvalue,index) in attr.attrValueList" :key="index"
- @click="attrInfo(attr,attrvalue)" >
-
- <a>{{attrvalue}}</a>
-
- </li>
- methods: {
-
- ....
-
- //平台售卖属性值的点击事件
- attrInfo(attr,attrvalue){
- //["属性ID:属性值:属性名"]
- this.$emit("attrInfo",attr,attrvalue)
- }
- },
从上面的代码中可以看出子向父通信使用自定义事件,子组件通过$emit触发自定义事件attrInfo,并传递相应的参数。
而在父组件Search中绑定自定义事件,并设置自定义事件的回调函数,并接收传递过来的参数
<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo" />
- //收集平台属性的回调函数(自定义事件)
- attrInfo(attr, attrvalue) {
- //参数的格式先整理好
- let props = `${attr.attrId}:${attrvalue}:${attr.attrName}`;
- //数组去重----常见面试题
- if (this.searchParams.props.indexOf(props) == -1) {
- this.searchParams.props.push(props);
- }
- //再次发送请求
- this.getData();
- },
从上面代码中可以看到,进行了【数组去重】操作,为什么这么做呢?
因为如果不进入数组去重的话,多次点击同一个平台售卖属性,会出现多个重复的面包屑。
除此之外,还要将【平台售卖属性】面包屑进行展示,注意这里不再使用v-if,而是使用v-for,因为props是一个数组
- <!-- 平台的售卖的属性值展示 -->
- <li class="with-x" v-for="(attrvalue, index) in searchParams.props" :key="index">
- {{ attrvalue.split(":")[1] }}
- <i @click="removeAttr(index)">×</i>
- </li>
再给 i标签 绑定一个监听事件,即删除这个平台售卖属性后,需要重新发请求去获取数据
- //removeAttr删除售卖的属性
- removeAttr(index) {
- //再次整理参数
- this.searchParams.props.splice(index, 1);
- //再次发送请求
- this.getData();
- },
分析api接口文档,发现searchParams中【order参数】就是用来指定排序方式的,下面讲讲它的具体含义
1表示综合;2表示价格;asc表示升序;desc表示降序,因此有如下四种组合:
1:asc 2:desc 1:desc 2:asc (注意:初始状态为1:desc)
上图是关于排序的两个位置(综合/价格),点击哪个位置,哪个位置就有对应的样式。由此我们知道,这个类是动态添加的。那怎么实现呢?这就需要用到【v-bind指令】喽~当isOne或isTwo为true时,li元素才拥有active这个类。
上图中出现了箭头,先不考虑箭头的指向,什么时候箭头才出现呢?谁有类名active,谁就有箭头呗。根据这种关系,可以考虑使用【v-if】或者【v-show】
箭头可以使用【阿里巴巴矢量图标库】中的素材,具体的使用方法可以进行百度
但是上述过程只能通过手动修改order参数,才能控制显示效果,这肯定不能满足实际需求
因此还是要为【综合】和【价格】两个a标签绑定点击事件,
下面是具体实现的代码,需要注意的事项在注释中写出,一定要好好理解,
- <li :class="{ active: isOne }" @click="changOrder('1')">
- <a>综合<span
- v-show="isOne"
- class="iconfont"
- :class="{
- 'icon-xiangshang': isAsc,
- 'icon-paixu': isDesc,
- }">
- </span>
- </a>
- </li>
- <li :class="{ active: isTwo }" @click="changOrder('2')">
- <a>综合<span
- v-show="isTwo"
- class="iconfont"
- :class="{
- 'icon-xiangshang': isAsc,
- 'icon-paixu': isDesc,
- }">
- </span>
- </a>
- </li>
- computed: {
- ......
- isOne() {
- return this.searchParams.order.indexOf("1") != -1; //返回值为布尔值
- },
- isTwo() {
- return this.searchParams.order.indexOf("2") != -1; //返回值为布尔值
- },
- isAsc() {
- return this.searchParams.order.indexOf("asc") != -1; //返回值为布尔值
- },
- isDesc() {
- return this.searchParams.order.indexOf("desc") != -1; //返回值为布尔值
- },
- ......
- },
- methods:{
- //排序的操作
- changOrder(flag) {
- //flag形参:它是一个标记,代表用户点击的是综合还是价格 (用户点击的时候传递过来的)
- //这里获取的是最开始的状态【需要根据初始状态去判断接下来做什么】
- let originFlag = this.searchParams.order.split(":")[0];
- let originSort = this.searchParams.order.split(":")[1];
- //准备一个新的order属性值
- let newOrder = "";
- //这个语句能够确定这次点击和上次点击的地方是【同一个】,将排序颠倒过来
- if (flag == originFlag) {
- newOrder = `${originFlag}:${originSort == "desc" ? "asc" : "desc"}`;
- } else {
- //这次点击和上次点击的地方【不是同一个】,默认排序向下
- newOrder = `${flag}:${"desc"}`;
- }
- //将新的order赋予searchParams【重新赋值】
- this.searchParams.order = newOrder;
- //再次发送请求
- this.getData();
- },
- }
因为分页器不止在一个地方使用,所以需要将分页器的内容作为全局组件来使用
因此在components文件夹下新建一个文件夹【Pagination】,用来存放分页器组件
并在main.js文件中引入该组件并注册,且在Search组件中使用该组件
分页器静态组件的内容在api开发接口文档中已经给出,直接使用就可以了,有些小地方需要进行修改,比较简单,这里就不再进行赘述了
为什么很多项目采用分页功能?因为电商平台同时展示的数据有很多(上万条)
我们知道ElementUI实现了分页器,使用起来非常简单,但是在这个项目中不使用它,因为想锻炼一下自身是否掌握了【自定义分页器】的功能
实现分页器之前,先思考分页器都需要哪些数据(条件)呢?
1. 需要知道当前是第几页:pageNo字段代表当前页数
2. 需要知道每页需要展示多少条数据:pageSize字段
3. 需要知道分页器一共有多少条数据:total字段--【获取另外一条信息:一共多少页】
4. 需要知道分页器连续的页码个数:continues字段,一般是5或者7,为什么是奇数呢?因为对称,比较好看
举个栗子 本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/69881
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。