尚硅谷前端项目开发笔记
尚硅谷前端项目开发笔记
B站视频直达,这个项目亮点在于所有 API 请求都并非在组件内编写,而是在组件内使用
this.$store.dispatch()
派发任务,再由 Vuex(actions、mutations、state三连操作) 获取后端数据后,渲染页面数据。
起步:
一、安装 Vue 脚手架
npm install -g @vue/cli
二、创建项目
vue create project(项目名称)
目录详解:
|- public // 静态页面目录,Webpack进行打包的时候会原封不动打包到dist文件夹中|- index.html // 项目入口文件 (Webpack打包的js,css会自动注入到该页面中)
|- src // 源码目录 (程序员开发代码文件夹)|- assets // 存放项目中用到的静态资源文件,例如:css 样式表、图片资源|- components // 非路由组件、全局组件 (封装的、可复用的组件,都要放到 components 目录下)|- views // 存放路由组件|- App.vue // 唯一根组件|- main.js // 项目的入口文件。项目的运行时最先执行的文件
|- babel.config.js // 配置文件(babel相关)
|- package.json // 项目的详细信息记录
|- package-lock.json // 缓存性文件(各种包的来源)
vue 项目的运行流程:
通过
main.js
把App.vue
渲染到index.html
的指定区域中。
vue-cli 打开项目流程
:
index.html ==> main.js ==> render(App.vue)
// 网页访问的是 index.html ,它引入了 main.js
// main.js 作为入口文件,它渲染了 App.vue。
项目配置
1. 项目运行时,自动打开浏览器
package.json"scripts": {"serve": "vue-cli-service serve --open", // --open 运行项目时自动打开浏览器"build": "vue-cli-service build","lint": "vue-cli-service lint"},
2. 开启 代码 CSS source maps和 关闭eslint校验工具
Vue项目中的
vue.config.js
文件就是我们之前使用的webpack.config.js
。
- 关闭 eslint 校验工具(不关闭会有各种规范,不按照规范就会报错)
- 开启 CSS source maps 可以定位到样式位置 (在哪个组件和在哪一行)
根目录下vue.config.js
文件设置:
module.exports = {css: {sourceMap: true, // 开启 CSS source maps}, lintOnSave: false //关闭eslint
}
3. 代码改变时实现页面自动刷新
vue.config.js加入:
module.exports = {//关闭eslintlintOnSave: false,devServer: {// 默认值为true开启热更新,false 则手动刷新inline: true,// 设置本地调试端口port: 8001,}
}
注意:修改完该配置文件后,要重启一下项目。
4. 设置路径别名,用@/
代替src/
在根目录创建 jsconfig.json文件并写入:
{"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"]}},"exclude": ["node_modules","dist"]}
注意: 上面这个方法每次新建项目都要创建一个文件,如果你使用的是 vscode,使用下面这个方法可以一劳永逸:
1.在 vscode 扩展工具里安装 Path Autocomplete 插件(插件不唯一,也可以使用其他插件)
2.然后对其进行配置, 找到配置项Settings.json配置文件加入:
// 配置 @ 的路径提示"path-autocomplete.pathMappings": {"@": "${folder}/src"},
清除vue页面默认的样式
我们可以需要修改public下的index.html文件:
<link rel="stylesheet" href="reset.css">
或者在 main.js 引入
import '@/assets/css/reset.css';
* {box-sizing: border-box;font: inherit;vertical-align: baseline;/** 这个属性只用于iOS, 当你点击一个链接或者通过Javascript定义的可点击元素的时候* 它就会出现一个半透明的灰色背景*/-webkit-tap-highlight-color: rgba(0,0,0,0);
}html{background-color:#fff;color:#000;font-size: 12px;-webkit-text-size-adjust: 100%; /* 禁止字体变化 */
}body {-webkit-overflow-scrolling: touch; /* 设置滚动容器的滚动效果 */-webkit-font-smoothing: antialiased; /* 字体抗锯齿渲染 */
}html,body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,
input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0;
}body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height: 1.5;font-family: tahoma,arial,"Hiragino Sans GB",simsun,sans-serif;
}h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%;
}h1,h2,h3,h4,h5,h6{font-family: tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif;
}h1,h2,h3,h4,h5,h6,b,strong{font-weight:normal;
}address,cite,dfn,em,i,optgroup,var{font-style:normal;
}table{border-collapse:collapse;border-spacing:0;text-align:left;
}caption,th{text-align:inherit;
}ul,ol,menu{list-style:none;
}fieldset,img{border:0;
}img,object,input,textarea,button,select{vertical-align:middle;
}article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block;
}audio,canvas,video{display:inline-block;*display:inline;*zoom:1;
}blockquote:before,
blockquote:after,
q:before,
q:after{content:"\0020";
}textarea{overflow:auto;resize:vertical;
}input,textarea,button,select,a{outline:0 none;border: none;
}button::-moz-focus-inner,
input::-moz-focus-inner{padding:0;border:0;
}mark{background-color:transparent;
}a,ins,s,u,del{text-decoration:none;
}sup,sub{vertical-align:baseline;
}a, a:active, a:hover {/*** 某些浏览器会给 a 设置默认颜色*/color: unset;text-decoration: none;
}
ol, ul, li {list-style: none;
}
input, textarea, select {outline: none; /*去掉fouce时边框高亮效果*/background: unset; /*去掉默认背景*/-webkit-appearance: none; /* 去除ios输入框阴影 */appearance: none;
}
路由组件和非路由组件区别
非路由组件
放在components中,通过标签使用路由组件
放在pages 或views 中,通过配置路由使用- 所有的组件身上都会拥有
$router
、$route
属性$router
: 进行路由跳转$route
: 获取路由信息(name、path、params等)
路由跳转方式
声明式导航:通过<router-link/>
标签 ,(理解为一个 a 标签,可以添加 class )
编程式导航 :声明式导航能做的编程式都能做,而且还可以处理一些业务
通过路由元信息设置组件显示与隐藏
我们在 App.vue 中导入了 Footer 组件,但是有些页面是不需要展示的,此时我们可以通过设置路由原信息 meta
和搭配 v-show 按需在页面展示 Footer 组件:
<!-- 在Home与Search可见的,但是Login|Register不可见 -->
<!-- 利用路由元信息解决当前问题好处:一行代码就可以解决 -->
<Footer v-show="$route.meta.isHideFooter" />
在路由中设置meta :
{path: '/home',component: Home,meta: {isHideFooter: true}},
为什么使用 v-show 而不使用 v-if ?
v-show 与v-if 区别 :
1. 展示形式不同
v-if是 创建一个dom节点,通过元素上树与下树进行操作
v-show 是display:none 、 block,通过样式display控制2. 使用场景不同
初次加载v-if要比v-show好,页面不会做加载盒子
频繁切换v-show要比v-if好,创建和删除的开销太大了,显示和隐藏开销较小
因为v-if会频繁的操作dom元素消耗性能,v-show只是通过样式将元素显示或隐藏。
路由传参
显示传值 (query)
query参数:不属于路径当中的一部分,路由不需要占位,写法类似于 ajax 当中query参数。
地址栏表现为:
/about?k1=v1&k2=v2
1. 显式 ==> 参数在url上 http://localhost:8080/about?a=1传:this.$router.push({path:'/about',query:{a:1}})接:this.$route.query.a
隐式传值 (params)
params参数:属于路径当中的一部分,需要注意
配置路由的时候需要占位
。地址栏表现为/search/v1/v2
需要单独配置路由信息,如 :
{name: 'search', // 当前路由的标识名称path: '/search/:keyword?', // /:keyword就是一个params参数的占位符,?表示该参数可传可不传component: Search,},{name: 'detail',path: '/detail/:skuId', // 配置路由跳转 params参数 component: Detail},
注意: 因为使用
隐式传值跳转刷新界面后参数丢失问题
无法解决,所以路径传值一定是显示的。
params传参问题
1. 如果路由path已经要求传递 params 参数,但是没有传递,会发现地址栏URL有问题, 详情如下:
1. Search路由项的path已经指定要传一个keyword的params参数:path: "/search/:keyword",2. 在Search组件中 执行下面进行路由跳转的代码: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. 右上面可以知道 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参数, 使用方法如下:
{name: 'search', // 当前路由的标识名称path: '/search/:keyword?', // /:keyword就是一个params参数的占位符,?表示该参数可传可不传component: Search,// 将params参数和query参数映射成 props 传入路由组件props: route => ({ keyword3: route.params.keyword, keyword4: route.query.keyword2 })},
路由传参方式:
1. 字符串形式
this.$router.push("/search/"+this.params传参+" ? k= "+this.query传参)
2.模板字符串
this.$router.push(`/search/"+this.params 传参 +" ? k= "+this.query传参`)
3.对象(常用)
this.$router.push({name:“路由名字”,params:{传参},query:{传参})
注意: 对象方式传参时,如果我们传参中使用了params,要跳转的路由只能使用name,不能使用path,query传参才可以使用 path。
多次跳转同一个链接控制台出现警告
多次执行相同的push问题,控制台会出现警告, 例如 :
let result = this.$router.push({name:"Search",query:{keyword:this.keyword}})
console.log(result) //返回了一个Promise
原因:因为 push返回的是一个Promise 对象,而Promise对象需要传递成功和失败两个参数,我们的push中并没有传递。
解决方法:
this.$router.push({name:‘Search’,params:{keyword: '3'||undefined}},()=>{},()=>{})
// 后面两项分别代表执行成功和失败的回调函数
这种写法治标不治本,将来在别的组件中使用编程式导航(push|replace)还是会有类似错误。
push是VueRouter.prototype的一个方法,在router中的index重写该方法即可:
//重写VueRouter.prototype原型对象身上的push|replace方法
//先把VueRouter.prototype身上的push|replace方法进行保存一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;VueRouter.prototype.push = function(location, resolve, reject) {//第一个形参:路由跳转的配置对象(query|params)//第二个参数:undefined|箭头函数(成功的回调)//第三个参数:undefined|箭头函数(失败的回调)if (resolve && reject) {//push方法传递第二个参数|第三个参数(箭头函数)//originPush:利用call修改上下文,变为(路由组件.$router)这个对象,第二参数:配置对象、第三、第四个参数:成功和失败回调函数originPush.call(this, location, resolve, reject);} else {//push方法没有产地第二个参数|第三个参数originPush.call(this,location,() => {},() => {});}
};VueRouter.prototype.replace = function(location, resolve, reject) {if (resolve && reject) {originReplace.call(this, location, resolve, reject);} else {originReplace.call(this,location,() => {},() => {});}
};
第二种写法:
// 解决设置路由拦截时 跳转到登入页面时候的报错(router版本问题)
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace
// push
VueRouter.prototype.push = function push(location, onResolve, onReject) {if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)return originalPush.call(this, location).catch(err => err)
}
// replace
VueRouter.prototype.replace = function push(location, onResolve, onReject) {if (onResolve || onReject) return originalReplace.call(this, location, onResolve, onReject)return originalReplace.call(this, location).catch(err => err)
}
定义全局组件
在 main.js 中注册:
//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);
在组件中使用: 已经注册为全局组件,因此不需要引入:
<template>
<div><TypeNav/>
</div>
</template>
封装 axios,设置请求和响应拦截器
为什么要设置拦截器?
在请求或响应被 then 或 catch 处理前拦截它们,减少服务器不必要的请求。
统一处理错误及配置请求信息
在请求拦截器中:携带token令牌(设置在请求头中)、Loding效果开始
在响应拦截器中: 统一处理弹窗,结束Loding效果
在根目录下创建api/request.js
文件 :
import axios from "axios";
//1、对axios二次封装
const requests = axios.create({//基础路径,requests发出的请求在端口号后面会跟改baseURlbaseURL:'/api',timeout: 5000,
})
//2、配置请求拦截器 (可以携带请求头 token)
requests.interceptors.request.use(config => {// console.log("请求拦截器----------------",config);/* config内主要是对请求头Header配置比如添加token(如果token在本地存储还在,就携带在请求头中)--- config.headers.Authorization = token */return config;
})
//3、配置响应拦截器
requests.interceptors.response.use((res) => {// console.log("响应拦截器=------------------", res.data)/*let { code, msg } = res.data;if ( msg) {// 成功弹窗if (code == 0) return Message({type: "success",message: msg})Message.error(msg);} */return res.data; //成功的回调函数
},(error) => {// console.log("响应失败回调--------------",error);return Promise.reject(new Error('fail'))
})
//4、对外暴露一个axios实例
export default requests;
解决开发时请求 API 跨域问题
配置vue.config.js文件:
module.exports = {lintOnSave: false,devServer: {inline: false,port: 8001,//代理服务器解决跨域proxy: {//会把请求路径中的/api换为后面的代理服务器'/api': {//提供数据的服务器地址target: 'http://39.98.123.211',}},}
}
注意:这里和以往配置跨域不同,因为上面封装axios的时候,
baseURL已经设置为了/api
,所以我们在写请求的时候都会携带/api
,在配置文件中我们就将/api进行了转换。在使用接口的时候就会拼接成http://39.98.123.211/api
。
封装接口并挂载到Vue原型
将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,方便统一修改接口。
在项目根目录下创建 api/index.js
文件
//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/ajax";//首页三级分类接口
export const reqCateGoryList = () => {return requests({url: '/product/getBaseCategoryList',method: 'GET'})
}
组件中按需使用 (不推荐):
import { reqCateGoryList } from './api'
reqCateGoryList(); //发起请求
每个页面都需要导入接口,是一件麻烦的事情,我们是否可以把所有接口挂载到vue原型上,在组件中按需导入,实现一劳永逸呢?
在main.js中导入所有接口:
import Vue from "vue";
import App from "./App.vue";
import router from "@/router";
import store from "@/store";
import * as API from '@/api'; //统一接口api文件夹里面全部请求函数new Vue({render: (h) => h(App),beforeCreate() {Vue.prototype.$API = API;},router,store,
}).$mount("#app");
使用:
mounted() {this.getPayInfo()
},
methods: {async getPayInfo() {let result = await this.$API.reqPayInfo(this.orderId)
}
async await 使用
如果项目中没有封装请求api,而是直接调用 axios ,就不需要使用async await,因为 axios 返回的就是一个Promise对象。
没有将函数封装前我们都会通过 then() 回调函数拿到服务器返回的数据,封装后依旧可以使用then获取数据:
categoryList(){let result = reqCateGoryList().then(res=>{console.log(res)// return res})console.log(result) // 返回的是一个 Promise对象
}
在vuex中使用使用挂载到vue原型方法
上面我们已经把封装的所有接口都挂载在 vue原型中了,但是我们在vuex中也不想按需导入接口了,就想像组件中那样直接使用,使用方法如下:
// import { reqGetBannerList } from "@/api";
state:{bannerList: [],
};
mutations : {GETBANNERLIST(state, bannerList) {state.bannerList = bannerList;},
};
actions :{async this._vm.$API.getBannerList({ commit }) {let result = await reqGetBannerList();if (result.code == 200) {commit("GETBANNERLIST", result.data);}}
}
说明:当我们在 vuex中 打印 this 时可以看见 store 对象中有一个
_vm
就是vue 的实例对象,所以我们才可以直接通过this._vm.$API
使用。
使用到的插件:
1. nprogress 进度条插件
nprogress 进度条插件链接
发起请求时页面上方会出现蓝色(默认)进度条。
使用: 在请求拦截器前开启进度条,在响应拦截器成功后关闭。
对应的ajax.js
设置:
import axios from "axios";
//引入进度条
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";
//1、对axios二次封装
const requests = axios.create({//基础路径,requests发出的请求在端口号后面会跟改baseURlbaseURL:'/api',timeout: 5000,
})
//2、配置请求拦截器
requests.interceptors.request.use(config => {//config内主要是对请求头Header配置//比如添加token//开启进度条nprogress.start();return config;
})
//3、配置相应拦截器
requests.interceptors.response.use((res) => {//成功的回调函数//响应成功,关闭进度条nprogress.done()return res.data;
},(error) => {//失败的回调函数console.log("响应失败"+error)return Promise.reject(new Error('fail'))
})
//4、对外暴露
export default requests;
可以通过修改
node_modules/nprogress/nprogress.css
文件的background来修改进度条样式。
2. vue-lazyload 图片懒加载插件
懒加载vue-lazyload插件官网
下载:
cnpm i vue-lazyload -S
1. 引入插件 import VueLazyload from "vue-lazyload";
2、注册插件 Vue.use(VueLazyload)
vuex 注意事项
async addOrUpdateShopCart({commit},{skuId,skuNum}){let result = await reqAddOrUpdateShopCart(skuId,skuNum)
}
注意:使用
action
时,函数的第一个参数,必须是{commit},即使不涉及到mutations
操作,也必须加上该参数,否则会报错。
详情见[Vuex] 官网: 和传值方法 跳转至参考链接→
getters使用
如果不使用getters属性,我们在组件获取state中的数据表达式为:
this.$store.state.子模块.属性
,又长又不方便复用,Vuex 允许我们在 store 中定义 getters(可以认为是 store 的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
我们在Search模块中获取商品列表数据就是通过getters实现,需要注意的是当网络出现故障时应该将返回值设置为空,如果不设置返回值就变成了undefined:
store中search模块代码:
import {reqGetSearchInfo} from '@/api';
const state = {searchList:{},
}
const mutations = {SEARCHLIST(state,searchList){state.searchList = searchList}
}
const actions = {//第二个参数data默认是一个空对象async getSearchListr({commit},data={}){let result = await reqGetSearchInfo(data)if(result.code === 200){commit("SEARCHLIST",result.data)}}
}
const getters = {goodsList(state){//网络出现故障时应该将返回值设置为空return state.searchList.goodsList||[]}
}
export default {state,mutations,actions,getters,
}
在Search组件中使用getters获取仓库数据:
<script>//引入mapGettersimport {mapGetters} from 'vuex'export default {name: 'Search',computed:{//使用mapGetters,参数是一个数组,数组的元素对应getters中的函数名...mapGetters(['goodsList'])}}
</script>
使用getters 数据控制台出现 undefined 红色警告
访问undefined的属性值会引起红色警告,网络正常时不会出错,一旦无网络或者网络问题就会gg。
下细节在于getters的返回值。如果getters按上面代码写为return state.goodInfo.categoryView
,页面可以正常运行,可以不处理,但是要明白红色警告警告的原因。
所以我们在写getters的时候要养成一个习惯在返回值后面加一个
||
条件。即当属性值undefined时,会返回 || 后面的数据,这样就不会报错。当然,如果返回值为对象加|| {}
,数组:|| [ ]
。
防抖和节流
什么是防抖和节流?
防抖:用户操作很频繁,但是只执行一次,减少业务负担。
节流:用户操作很频繁,但是把频繁的操作变为少量的操作,使浏览器有充分时间解析代码
[防抖和节流详情]https://www.jianshu.com/p/c8b86b09daf0
使用loadsh插件实现防抖和节流
loadsh官网
– 防抖函数
– 节流函数
下面代码为设置节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。
// throttle是节流函数
import {throttle} from 'lodash' methods: {//鼠标进入修改响应元素的背景颜色//采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次changeIndex: throttle(function (index){this.currentIndex = index},50),//鼠标移除触发时间leaveIndex(){this.currentIndex = -1}}
上面并没有通过 npm 下载 loadsh插件,因为vue脚手架自带,直接引入即可。
使用编程式导航+事件委托实现路由跳转
如图所示,三级标签每一个标签都是一个页面链接,要实现通过点击表现进行路由跳转。
解决思路
使用导航式路由
:有多少个a标签就会生成多少个router-link
标签,router-link是vue提供的组件,这样当我们频繁操作时会出现卡顿现象。使用编程式路由
:通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。
解决办法:
使用编程时导航+事件委派
的方式实现路由跳转 :
事件委派即把子节点的触发事件都委托给父节点,这样只需要一个回调函数 goSearch 就可以解决。
事件委派问题:
1. 如何确定我们点击的一定是a标签呢和且通过点击才实现跳转呢?
为三个等级的a标签添加自定义属性
date-categoryName
绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
2. 跳转时如何获取子节点标签的商品名称和商品id ?
为三个等级的 a 标签分别 添加自定义属性
data-category1Id
(一级)、data-category2Id
(二级)、data-category3Id
(三级) 并获取各级商品id,用于路由跳转。
<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>
然后再通过goSearch() 函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过 dataset
属性获取节点的属性信息。
//函数使用
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">//函数定义goSearch(event){console.log(event.target) // 点击后会输出当前元素的 dom节点
}
注意: event是系统属性,只需要在函数定义的时候作为参数传入,函数使用的时候不需要传参。
完整代码:
goSearch(event){let element = event.target//html中会把大写转为小写// 获取和解构 4个自定义属性 let {categoryname,category1id,category2id,category3id} = element.dataset//categoryname存在,表示为a标签if(categoryname){//整理路由跳转的参数let location = {name:'Search'} //跳转路由namelet query = {categoryName:categoryname} //路由参数// 判断属于哪一个等级的a标签if(category1id){query.category1Id = category1id // 一级}else if(category2id){query.category2Id = category2id // 二级}else if(category3id){ query.category3Id = category3id // 三级}location.query = query //整理完参数this.$router.push(location) //路由跳转}}
Vue 路由销毁问题
Vue 在路由切换的时候会销毁旧路由。
我们在三级列表全局组件中的mounted进行了请求一次商品分类列表数据,当我们在包含三级列表全局组件的不同组件之间进行切换时,都会进行一次信息请求。
由于信息都是一样的,处于性能考虑我们希望该数据只请求一次,所以我们把这次请求放在
App.vue
的mounted
中,根组件App.vue 的mounted只会执行一次。
Mock 插件使用
mock.js官网
为什么使用 mock?
在开发中,有时候后端的接口还未完成,而前端开发者可以mock一些数据(模拟的一些假的接口),当后端接口完成时,再把mock数据变为后台给的接口数据替换。
插件使用:
下载和引入:
cnpm i mockjs -S
mock用来拦截前端ajax请求,返回我们自定义的数据用于测试前端接口。我们可以将不同的数据类型封装为不同的json文件,创建mock/mockServer.js
文件:
//先引入mockjs模块
import Mock from 'mockjs';
//把JSON数据格式引入进来[JSON数据格式根本没有对外暴露,但是可以引入]
//webpack默认对外暴露的:图片、JSON数据格式
import banner from './banner.json';
import floor from './floor.json';//mock数据:第一个参数请求地址 第二个参数:请求数据
Mock.mock("/mock/banner",{code:200,data:banner});//模拟首页大的轮播图的数据
Mock.mock("/mock/floor",{code:200,data:floor});
让项目启动时能访问到 mock 接口
mock 接口书写完毕后,mock当中 mockServer.js 需要执行一次,如果不执行,和你没有书写一样的。
回到入口文件,引入mockServer.js :
import "@/mock/mockServe"; //引入MockServer.js----mock数据
vuex 数据存储与使用
- 在store 中的
actions
中发起请把数据提交给mutations
- 在
mutations
中把数据存放在state
中
以我们的首页轮播图数据为例:
(1). 在轮播图组件加载完毕后向vuex派发任务,完成数据请求
mounted() {this.$store.dispatch("getBannerList")},
(2) . 请求实际是在store中的actions中完成的
actions:{//获取首页轮播图数据async getBannerList({commit}){let result = await reqGetBannerList()if(result.code === 200){commit("BANNERLIST",result.data)}}}
(3). 获取到数据后存入store仓库,在mutations完成
//唯一修改state的部分
state : {bannerList: [],
};
mutations:{BANNERLIST(state,bannerList){state.bannerList = bannerList}
}
由于数据是通过异步请求获得的,在轮播图组件使用时,我们要通过计算属性computed 获取vuex中的state。
<script>
import {mapState} from "vuex";
export default {//主键挂载完毕,请求轮播图图片mounted() {this.$store.dispatch("getBannerList")},computed:{...mapState({bannerList: (state => state.home.bannerList)})}
}
</script>
swiper 插件实现轮播图并封装成公共组件
1.安装swiper、高版本问题多。
cnpm i swiper@5 -S
2.在需要使用轮播图的组件内导入swpier和它的css样式
3.在组件中创建swiper需要的dom标签(html代码,参考官网代码)
4.创建swiper实例
解决在 mounted 创建 swiper实例图片无法加载问题
我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。
解决思路:
- 使用定时器 (不完美)
- 使用 watch,只能保证在bannerList变化时创建swiper对象,不能保证此时v-for已经执行完了。
swiper对象生效的前提是dom结构已经渲染好了,假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片。
完美解决方案:使用 watch 侦听器 +this.$nextTick()
<template><div class="swiper-container" ref="cur"><div class="swiper-wrapper"><div class="swiper-slide" v-for="carousel in list" :key="carousel.id"><img :src="carousel.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'
export default {name: 'Carousel',props: ['list'],watch: {list: {//立即监听:不管你数据有没有变化,我上来立即监听一次//为什么watch监听到list:因为这个数据从来没有发生变化(数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的)immediate: true,handler() {//只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定的,因此还是需要用nextTickthis.$nextTick(() => {var mySwiper = new Swiper(this.$refs.cur, {autoplay: {delay: 3000,stopOnLastSlide: false,disableOnInteraction: false},loop: true,// 如果需要分页器pagination: {el: '.swiper-pagination',//点击小球的时候也切换图片clickable: true},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev'}})})}}}
}
</script>
注意:之前我们在学习watch时,一般都是监听的定义在data中的属性,但是我们这里是监听的computed中的属性,这样也是完全可以的,并且如果你的业务数据也是从store中通过computed动态获取的,也需要watch监听数据变化执行相应回调函数。
使用 ref 来避免页面中多个轮播图组件返回的是同样的数据
// 在轮播图最外层DOM中添加ref属性
<div class="swiper-container" id="mySwiper" ref="cur">
// 通过ref属性值获取DOMnew Swiper(this.$refs.cur,{...})
使用watch监听路由变化实现动态搜索
最初想法:在每个三级列表和收缩按钮加一个点击触发事件,只要点击了就执行搜索函数。
但是这是一个很蠢的想法,如果这样就会生成很多回调函数,很耗性能。
最佳方法:我们每次进行新的搜索时,我们的query和params参数中的部分内容肯定会改变,而且这两个参数是路由的属性。我们可以通过监听路由信息的变化来动态发起搜索请求。
//数据监听:监听组件实例身上的属性的属性值变化watch: {//监听路由的信息是否发生变化,如果发生变化,再次发起请求$route(newValue, oldValue) {//每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3// 合并参数对象,再次发请求之前整理带给服务器参数Object.assign(this.searchParams, this.$route.query, this.$route.params)//再次发起ajax请求this.getData()//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据//所以每次请求结束后将相应参数制空this.searchParams.category1Id = undefined// 使用 undefined是为了提示性能,路由将不会携带undefined参数,如果使用空字符串还是会被传入this.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefined}}
搜索页面包屑相关操作
本次项目的面包屑操作主要就是两个删除逻辑:
- 当分类属性(query)删除时删除面包屑同时修改路由信息。
- 当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。
删除分类:
//删除分类的名字
removeCategoryName() {//把带给服务器的参数置空了,还需要向服务器发请求//带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器//但是你把相应的字段变为undefined,当前这个字段不会带给服务器this.searchParams.categoryName = undefinedthis.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefinedthis.getData()//地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)this.$router.push({ name: 'search', params: this.$route.params })}
删除搜索关键字:
//删除关键字
removeKeyword() {//给服务器带的参数searchParams的keyword置空this.searchParams.keyword = undefined//通知兄弟组件Header清除关键字this.$bus.$emit('clear')//进行路由的跳转if (this.$route.query) {this.$router.push({ name: 'search', query: this.$route.query })}
}
header组件接受$bus通信:
mounted() {// 组件挂载时就监听clear事件,clear事件在search模块中定义// 当删除关键字面包屑时,触发该事件,同时header的输入框绑定的keyword要删除this.$bus.$on("clear",()=>{this.keyword = ''})}
搜索页子组件传参及面包屑操作
SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性,原理与搜索页相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化会影响路由地址变化。
总结:面包屑由四个属性影响:parads、query、品牌、手机属性
面包屑生成逻辑:判断相关属性是否存在,存在即显示。
商品排序
排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。
order属性值为字符串:1:asc 1代表综合
2:desc 2代表价格asc代表升序,desc代表降序
升降序时改变上下箭头图标:
下载阿里图标并引入:
// 在public/index引入该 css
<link rel="stylesheet" href="https://at.alicdn.com/t/font_2994457_qqwrvmss9l9.css">
<div class="sui-navbar"><div class="navbar-inner filter"><ul class="sui-nav"><!-- 这里isOne、isTwo、isAsc、isDesc是计算属性,如果不使用计算属性要在页面中写很长的代码--><li :class="{active:isOne}" @click="changeOrder('1')"><a >综合<span v-show="isOne" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}"></span></a></li><li :class={active:isTwo} @click="changeOrder('2')"><a >价格<span v-show="isTwo" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}"></span></a></li></ul></div>
</div>
搜索页逻辑代码:
<script>
import SearchSelector from './SearchSelector.vue'
import { mapGetters, mapState } from 'vuex'
export default {name: 'Search',data() {return {searchParams: {//产品相应的idcategory1Id: '',category2Id: '',category3Id: '',//产品的名字categoryName: '',//搜索的关键字keyword: '',//排序:初始状态应该是综合且降序order: '1:desc',//第几页pageNo: 1,//每一页展示条数pageSize: 3,//平台属性的操作props: [],//品牌trademark: ''}}},components: {SearchSelector},//在挂载之前调用一次|可以在发请求之前将带有参数进行修改beforeMount() {//在发请求之前,把接口需要传递参数,进行整理(在给服务器发请求之前,把参数整理好,服务器就会返回查询的数据)Object.assign(this.searchParams, this.$route.query, this.$route.params)},mounted() {//在发请求之前咱们需要将searchParams里面参数进行修改带给服务器this.getData()},methods: {//把发请求的这个action封装到一个函数里面//将来需要再次发请求,你只需要在调用这个函数即可getData() {this.$store.dispatch('getSearchList', this.searchParams)},//删除分类的名字removeCategoryName() {//把带给服务器的参数置空了,还需要向服务器发请求//带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器//但是你把相应的字段变为undefined,当前这个字段不会带给服务器this.searchParams.categoryName = undefinedthis.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefinedthis.getData()//地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)//严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着/** 这里永远成立,即使是空对象会跳转 */if (this.$route.params) {this.$router.push({ name: 'search', params: this.$route.params })}},//删除关键字removeKeyword() {//给服务器带的参数searchParams的keyword置空this.searchParams.keyword = undefined//再次发请求,(其实没必要,路由跳转后属性变化,watch内的函数会重新发起请求)this.getData()//通知兄弟组件Header清除关键字// ps: 这里也可以通过检测路由里的 keyword是否为空,如果为空就修改header组件的值this.$bus.$emit('clear')//进行路由的跳转if (this.$route.query) {this.$router.push({ name: 'search', query: this.$route.query })}},//自定义事件回调trademarkInfo(trademark) {//1:整理品牌字段的参数 "ID:品牌名称"this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`//再次发请求获取search模块列表数据进行展示this.getData()},//删除品牌的信息removeTradeMark() {//将品牌信息置空this.searchParams.trademark = undefined//再次发请求this.getData()},//收集平台属性地方回调函数(自定义事件)attrInfo(attr, attrValue) {//["属性ID:属性值:属性名"]console.log(attr, attrValue)//参数格式整理好let props = `${attr.attrId}:${attrValue}:${attr.attrName}`//数组去重 splice(index,1) set include indexOf 都可以if (this.searchParams.props.indexOf(props) == -1) {this.searchParams.props.push(props)//再次发请求this.getData()}},//removeAttr删除售卖的属性removeAttr(index) {//再次整理参数this.searchParams.props.splice(index, 1)//再次发请求this.getData()},//排序的操作changeOrder(flag) {//flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2)//现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】let originOrder = this.searchParams.orderlet orginsFlag = originOrder.split(':')[0]let originSort = originOrder.split(':')[1]//新的排序方式let newOrder = ''// 获取flag取asc和desc是否存在 不存在取反//判断的是多次点击的是不是同一个按钮if (flag == orginsFlag) {newOrder = `${orginsFlag}:${originSort == 'desc' ? 'asc' : 'desc'}`} else {//点击不是同一个按钮newOrder = `${flag}:${'desc'}`}//需要给order重新赋值this.searchParams.order = newOrder//再次发请求this.getData()},//自定义事件的回调函数---获取当前第几页getPageNo(pageNo) {//整理带给服务器参数this.searchParams.pageNo = pageNo//再次发请求this.getData()}},computed: {//mapGetters里面的写法:传递的数组,因为getters计算是没有划分模块【home,search】...mapGetters(['goodsList']),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},//获取search模块展示产品一共多少数据...mapState({total: (state) => state.search.searchList.total})},//数据监听:监听组件实例身上的属性的属性值变化watch: {//监听路由的信息是否发生变化,如果发生变化,再次发起请求$route(newValue, oldValue) {//每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3//再次发请求之前整理带给服务器参数Object.assign(this.searchParams, this.$route.query, this.$route.params)//再次发起ajax请求this.getData()//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据this.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefined}}
}
</script>
组件通信方式总结
props 父向子$on、$emit 子向父$bus 全局事件总线(通常用于兄弟组件传值)Vuex 全局组件共享slot插槽 适用于父子组件通信
手写分页器
开发分页器必须的核心属性:
pageNo 当前页码
pageSize 每一页展示多少条数据
total 总数据
continues 连续展示的页码 (连续页码数一般为5、7、9 奇数,对称好看)totalPage 总页数 Math.ceil(总数/ 每一页多少条数据)
核心逻辑: 获取连续页码的起始页码和末尾页码,通过计算属性获得。
// 父组件传递子组件的数据: 当前页、每一页展示多少条数据、总数据、连续页码数props: ['pageNo', 'pageSize', 'total', 'continues'],computed: {//总共多少页totalPage() {//向上取整(计算总页数)return Math.ceil(this.total / this.pageSize)},//计算出连续的页码的起始数字与结束数字[连续页码的数字:至少是5]startNumAndEndNum() {// 解构出 连续页码数、当前页码、总页数const { continues, pageNo, totalPage } = this//先定义两个变量存储起始数字与结束数字let start = 0,end = 0//连续页码数字5【就是至少五页】,如果出现不正常的现象【就是不够五页】//不正常现象【总页数小于连续页码数】if (continues > totalPage) {start = 1end = totalPage} else {//正常现象【连续页码5,但是你的总页数一定是大于5的】 Math.floor:向下取整//起始、结束数字start = pageNo - Math.floor(continues/2)end = pageNo + Math.floor(continues/2)//把出现不正常的现象【start数字出现0|负数】纠正if (start < 1) {start = 1end = continues}//把出现不正常的现象[当前页码数大于总页码]纠正if (end > totalPage) {end = totalPagestart = totalPage - continues + 1}}return { start, end }}}
当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。
字符串拼接
// 在js中使用
var a = n;
console.log(`a的值是:${a}`); //a的值是:n// 在html中使用
<router-link :to="`/detail/${goods.id}`"></router-link>
解决使用vue-router跳转后不回顶部问题
router滚动行为详情
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes.js'Vue.use(VueRouter)// 向外默认暴露路由器对象
let router = new VueRouter({routes, // 注册所有路由//router-link跳转时回到顶部scrollBehavior(to, from, savedPosition) {//返回的这个y=0,代表的滚动条在最上方return { y: 0 }}
})
商品详情难点
1. 点击轮播图图片时,改变放大镜组件展示的图片
豪哥的方法很巧妙:在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex 实现点击图片边框高亮设置,当符合图片的下标满足currentIndex===index
时,该图片就会被标记为选中。
<div class="swiper-container" ref="cur"><div class="swiper-wrapper"><div class="swiper-slide" v-for="(skuImage,index) in skuImageList" :key="skuImage.id"><img :src="skuImage.imgUrl" :class="{active:currentIndex===index}" @click="changeImg(index)"></div></div><div class="swiper-button-next"></div><div class="swiper-button-prev"></div></div>
2. 轮播图组件和放大镜组件是兄弟组件,所以要通过全局总线通信
在轮播图组件中,点击图片触发全局事件changeImg,参数为图片所在数组的下标:
changeImg(index){//将点击的图片标识位高亮this.currentIndex = index//通知兄弟组件修改大图图片this.$bus.$emit("changeImg",index)}
对应的放大镜组件,在mounted监听该全局事件
mounted() {this.$bus.$on("changeImg",(index)=>{//修改当前响应式图片this.currentIndex = index;})},
放大镜组件中也会有一个currentIndex,他用表示大图中显示的图片的下标(因为放大镜组件只能显示一张图片),全局事件传递的 index 赋值给currentIndex ,通过computed计算属性改变放大镜组件展示的图片下标。
// 放大镜组件展示图片的html代码
<img :src="imgObj.imgUrl " />computed:{imgObj(){return this.skuImageList[this.currentIndex] || {}}},
参考链接 : JavaScript 实现放大镜功能
失焦事件
blur 与 change: 输入结束后,离开输入框,会先后触发 change 与 blur1. 没有进行任何输入时:不会触发change,但是会触发 blur2. 输入后值并没有发生变更时:change依旧不会触发keydown、input、keyup、blur都会触发
路由跳转时的复杂数据传参
当我们想要实现路由跳转并传递数据时,首相想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的
复杂信息
,就可以通过Web Storage实现。
本地存储与会话存储区别
共性: 都是已字符串形式存储:sessionStorage 会话存储,当前窗口关闭后就会删除。
localStorage 本地存储,存储在浏览器中,再次打来还存在。存储前: JSON.stringify()将对象转为字符串
取数据: JSON.parse()将字符串转为对象
删除多个商品(actions扩展)
问题点: 由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。
解决思路: 我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。
// 官网的教程,一个标准的actions函数如下所示:deleteAllCheckedById(context) {console.log(context)}
我们可以看一下context到底是什么:
可以看到 context中包含有:
commit、dispatch、getters、state
所以我们可以在actions函数中通过dispatch调用其他的actions函数,可以通过getters获取仓库的数据:
//删除选中的所有商品
deleteAllCheckedById({dispatch,getters}) {getters.getCartList.cartInfoList.forEach(item => {let result = [];//将每一次返回值添加到数组中result.push(item.isChecked === 1?dispatch('deleteCartById',item.skuId):'') })return Promise.all(result)
},
购物车组件method批量删除函数:
//删除选中的所有商品
async deleteAllCheckedById(){try{await this.$store.dispatch('deleteAllCheckedById')//删除成功,刷新数据this.$store.dispatch("getCartList")}catch (error){alert(error)}
}
修改商品的全部状态和批量删除的原理相同:
// --------- vuex 中的 actions ----------------->
async updateAllChecked({dispatch,getters},flag){let result = []getters.getCartList.cartInfoList.forEach(item => {result.push(dispatch('reqUpdateCheckedById',{skuId:item.skuId,isChecked:flag}))})return Promise.all(result)
}// --------- 组件中定义的 method ----------------->async allChecked(event){let flag = event.target.checked ? 1 : 0console.log(flag)try{await this.$store.dispatch('updateAllChecked',flag)this.$store.dispatch("getCartList")}catch (error){alert(error)}
}
购物车数据为空时控制台报错bug 纠正:
虽然getters中在获取 getCartList 时已经设置了 || {}
,但在组件中我们通过 computed 获取的是 getters 中的cartInfoList,它是一个数组,所以我们还需设置默认返回值。
问题原因: 组件的 computed 中的cartInfoList没有写 || []
返回值
cartInfoList(){return this.getCartList.cartInfoList || [];
},
总结: 即使在getters设置了默认返回值,但是在组件中使用时还要使用计算属性筛选数据,必须再次设置默认返回值。
阻止from表单@click触发登录事件时候的默认行为
由于登录按钮的父节点是一个form表单,如果使用@click触发登录事件,form表单会执行默认事件action实现页面跳转。这里我们使用
@click.prevent
,它可以阻止自身默认事件的执行。
actions登陆函数:
//登录async userLogin({commit},data){let result = await reqPostLogin(data)//服务器会返回tokenif(result.code === 200){//token存入vuexcommit("SETUSERTOKEN",result.data.token)//持久化存储tokenlocalStorage.setItem('TOKEN',result.data.token)return 'ok'}else{return Promise.reject(new Error(result.message))}},
mutations设置用户token:
//设置用户tokenSETUSERTOKEN(state,token){state.token = token}
登陆组件methods登陆函数:
async goLogin(){try{//会将this中的phone,password以对象的形式返回const {phone,password} = thisphone && password && await this.$store.dispatch('userLogin',{phone,password})//路由跳转到home首页this.$router.push('/home')}catch (error){alert(error)}}
登陆成功后获取用户信息:
//-----------actions函数
async getUserInfo({commit}){let result = await reqGetUserInfo();//将用户信息存储到store中if(result.code === 200){//vuex存储用户信息commit('SETUSERINFO',result.data)return 'ok'}else{return Promise.reject(new Error(result.message))}},
// -----------mutations中 存储用户信息SETUSERINFO(state,data){state.userInfo = data}
配置导航守卫
//--------------------router index.js全局前置守卫代码
router.beforeEach(async(to, from, next) => {let token = store.state.user.tokenlet name = store.state.user.userInfo.name//1、有token代表登录,全部页面放行if(token){//1.1登陆了,不允许前往登录页if(to.path==='/login'){next('/home')} else{//1.2、因为store中的token是通过localStorage获取的,token有存放在本地// 当页面刷新时,token不会消失,但是store中的其他数据会清空,// 所以不仅要判断token,还要判断用户信息//1.2.1、判断仓库中是否有用户信息,有放行,没有派发actions获取信息if(name)next()else{//1.2.2、如果没有用户信息,则派发actions获取用户信息try{await store.dispatch('getUserInfo')next()}catch (error){//1.2.3、获取用户信息失败,原因:token过期//清除前后端token,跳转到登陆页面await store.dispatch('logout')next('/login')}}}}else{//2、未登录,首页或者登录页可以正常访问if(to.path === '/login' || to.path === '/home' || to.path==='/register')next()else{alert('请先登录')next('/login')}}
})
路由独享的守卫
引出问题:
全局导航守卫已经帮助我们限制未登录的用户不可以访问相关页面。但是还会有一个问题
例如:
用户已经登陆,用户在home页直接通过地址栏访问trade结算页面,发现可以成功进入该页面,正常情况,用户只能通过在shopcart页面点击去结算按钮才可以到达trade页面。
通过路由独享守卫解决该问题:
// 在trade路由信息中加入路由独享守卫
//交易组件{name: 'Trade',path: '/trade',meta: {show:true},component: () => import('@/pages/Trade'),//路由独享首位beforeEnter: (to, from, next) => {if(from.path === '/shopcart' ){next()}else{next(false) // 指回到from路由}}}
此时又一个bug , 当我们在shopcart页面通过地址栏访问trade时还是会成功 !!!
解决办法:在shopcart 添加一个路由元信息 meta:{flag: false}
。
当点击去结算按钮后,将flag置为true。在trade的独享路由守卫中判断一下flag是否为true,当flag为true时,代表是通过点击去结算按钮跳转的,所以就放行。
//购物车{path: "/shopcart",name: 'ShopCart',component: ()=> import('../pages/ShopCart'),meta:{show: true,flag: false},},
// shopcart组件去结算按钮触发事件
toTrade(){this.$route.meta.flag = truethis.$router.push('/trade')
}
// trade路由信息{name: 'Trade',path: '/trade',meta: {show:true},component: () => import('@/pages/Trade'),//路由独享首位beforeEnter: (to, from, next) => {if(from.path === '/shopcart' && from.meta.flag === true){// 注意,判断通过后,在跳转之前一定要将flag置为false。from.meta.flag = falsenext()}else{next(false)}}}
二级路由(子路由)问题
配置二级路由:
//个人中心{name: 'Center',path: '/center',component: () => import('@/pages/Center'),children: [{//二级路由要么不写/,要么写全:'/center/myorder'path: '/center/myorder',component: () => import('@/pages/Center/MyOrder')},{path: '/center/groupbuy',component: () => import('@/pages/Center/GroupOrder'),},//默认显示{path: '',redirect: 'myorder'}]}
控制台警告问题:
总结警告缘由:当某个路由有子级路由时,父级路由必须要一个
默认的路由
,因此父级路由不能定义name属性,解决办法是去掉name:'Center'
就好了。
项目中使用到的 JavaScript方法回顾:
Object.asign() 浅拷贝
JSON.stringify() 深拷贝
every函数使用
every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
【例】:判断底部勾选框是否全部勾选:
//判断底部勾选框是否全部勾选
isAllCheck() {//every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回falsereturn this.cartInfoList.every(item => item.isChecked === 1)
}
Promise.all:
Promise.all可以将多个Promise实例包装成一个新的Promise实例。成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
//删除选中的所有商品
deleteAllCheckedById({dispatch,getters}) {getters.getCartList.cartInfoList.forEach(item => {let result = [];//将每一次返回值添加到数组中result.push(item.isChecked === 1?dispatch('deleteCartById',item.skuId):'') })return Promise.all(result)
},
ES6 中的 const 新用法:
const {comment,index,deleteComment} = this
上面的这句话是一个简写,最终的含义相当于:
const comment = this.comment
const index = this.index
const deleteComment = this.deleteComment
打包项目
打包时取消输出 map 代码定位文件:
. map
文件的作用:
可以理解为代码地图,因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时哪行的代码报错。而map文件就是用来代码定位,有了map才能准确的输出是哪一行那一列有错。
npm run build 打包项目因为map文件较大,上线前需要删除map文件,
也可以通过在`vue.config.js`配置 `productionSourceMap: false`实现打包时不输出map文件。
开发中使用 Vue 的总结:
1. 通过props实现父向之传值时为什么传入数字也需要使用 v-bind 呢 ?
为了告诉 Vue,传入的是 这是一个 JavaScript 表达式,如果不使用v-bind 将会被解析成一个字符串。 正确写法:
<Son :likes="42"/>
2.$ref $ children 和 $parent使用
$refs:父组件访问子组件如果在普通的DOM元素上使用,引用指向的是DOM元素;如果用在子组件上,引用的是组件实例$children:父组件访问子组件 (如果是多次的$refs操作,我们可以使用$children属性)
$parent: 子组件访问父组件
3.解析Vue实例对象
$root 根实例
$options 每一个Vue实例都有一个实例对象 options
$children 子组件实例 (是一个数组)
$parent 父组件实例$emint 子组件抛出的自定义事件
$on 通过on接收自定义事件
$attrs和$props 加起来才是所有子组件的所有自定义属性:$attrs 能获取到子组件所有未被props接收的属性 (排除了$props、class、style以外的其他属性,极端情况下使用,注意不能直接用模板字符串使用$attrs的数据)
$props 获取到子组件的所有props,父子传参时使用,正常情况下都用prpos传参,因为是响应式的数据也更安全。
$data 拿到组件完整的data对象
$refs 如果是HTML标签,拿到的是dom对象,如果是组件标签,拿到的是组件实例对象
$vnode 当前组件的虚拟节点
$router 拿到VueRouter实例
$route 获取组件的路由信息(name、meta、path、query等参数)注意: App.vue 并不是根实例 ,是`$root根实例`的子组件。
尚硅谷前端项目开发笔记相关推荐
- 尚硅谷VUE项目-前端项目问题总结07--产品详情页【vuex-排他操作foreach-放大镜-轮播图-兄弟组件通信$bus-购物车-路由跳转传参-路由传参+会话存储】-游客身份-节流
尚硅谷VUE项目-前端项目问题总结07---产品详情页 1.静态组件(详情页还未注册为路由组件) 2.发请求 3.vuex-获取产品详情信息 3.1放大镜 3.2 属性值[排他操作] 3.3轮播图[j ...
- 尚医通项目学习笔记Part1
尚医通项目学习笔记 前言 一.目前学习进度 二.学习记录 1.项目简介 1.1 项目所会用到的技术栈 1.2 业务流程 2.项目学习笔记 2.1MyBatis-Plus相关 2.2搭建项目框架 2.3 ...
- 尚硅谷-离线数仓-笔记
尚硅谷-离线数仓-笔记 一.数仓建模理论 第一章 数仓概述 1.1 数仓概念 数据仓库是一个为数据分析而设计的企业级数据管理系统.数据仓库可集中.整合多个信息源的大量数据,借助数据仓库的分析能力,企业 ...
- 尚硅谷云原生学习笔记(76~143集)
笔记列表: 尚硅谷云原生学习笔记(1-75集) 尚硅谷云原生学习笔记(76~143集) 尚硅谷云原生学习笔记(144~172集) 尚硅谷云原生学习笔记(173~XXX集) 目录 76.为什么用kube ...
- 尚硅谷云原生学习笔记(1-75集)
笔记列表: 尚硅谷云原生学习笔记(1-75集) 尚硅谷云原生学习笔记(76~143集) 尚硅谷云原生学习笔记(144~172集) 尚硅谷云原生学习笔记(173~XXX集) 目录 1.什么是云计算 1. ...
- 前端 | ( 九)尚品汇实操练习 | 尚硅谷前端html+css零基础教程2023最新
学习来源:尚硅谷前端html+css零基础教程,2023最新前端开发html5+css3视频 系列笔记: [HTML4](一)前端简介 [HTML4](二)各种各样的常用标签 [HTML4](三)表单 ...
- 前端 | ( 十一)CSS3简介及基本语法(上) | 尚硅谷前端html+css零基础教程2023最新
学习来源:尚硅谷前端html+css零基础教程,2023最新前端开发html5+css3视频 系列笔记: [HTML4](一)前端简介 [HTML4](二)各种各样的常用标签 [HTML4](三)表单 ...
- 前端 | (二)各种各样的常用标签 | 尚硅谷前端html+css零基础教程2023最新
学习来源:尚硅谷前端html+css零基础教程,2023最新前端开发html5+css3视频 系列笔记: [HTML4](一)前端简介 [HTML4](二)各种各样的常用标签 [HTML4](三)表单 ...
- 京东投票项目开发笔记
京东投票项目开发笔记 打开项目 $yarn install / $ npm install: 跑环境(把项目依赖的插件进行安装) $node admin.js: 启服务(把自己的计算机作为服务器,创建 ...
最新文章
- mysql练习题及答案_MySQL经典练习题及答案,常用SQL语句练习50题
- 「3」Java开发环境搭建
- Linux系统管理员修炼三层次
- 2015年 第6届 蓝桥杯 Java B组 省赛解析及总结
- 打开工程会提示下载的可能原因和可能解决方法
- RabbitMQ 的概念
- 基于stm32简易计算机电路图,基于STM32的简易电子计算器设计与实现(DOC).doc
- 【Python】编写一个类,求圆的周长和面积
- (86)FPGA读文件激励(readmemh)
- SuSE Linux 应用与安装
- 【转载】 MySQL之用户资源限制
- 修改了一个YUV/RGB播放器
- JavaWeb之Request与Response详解
- 解决ios7.x越狱后静态壁纸变为空白
- 如何判断一个单链表是否有环?
- 科大讯飞实现了APP用自己的声音听故事
- 人工智能软件工程师软件清单
- java orm全称_[Java-基础] 什么是ORM
- 【综合类型第 10 篇】什么是时间戳
- STM32开发笔记48:STM32F4+DP83848以太网通信指南系列(二):系统时钟
热门文章
- 计算机中存储单位的编号称号是什么,KB、MB、GB的中文单位名称是什么?
- @GeneratedValue与@GenericGenerator区别
- 企业局域网无线认证解决方案
- 不属于python标准库的是_Python标准库笔记(11) — Op
- Lemur的参数文件
- 建立live555海思编码推流服务
- 直流功率传感器行业调研报告 - 市场现状分析与发展前景预测
- Win10电脑有网其他联网软件能正常使用但打不开浏览器怎么办?
- SpringBoot拦截器失效问题excludePathPatterns失效问题
- 学好编程之GOC语言快速入门(1)