SGG前端项目
1、新建文件夹
利用vue-cli脚手架搭创建一个新的项目,打开终端,定位到桌面,然后再进行创建,不然可能会出现找不到新建的项目文件夹
~ % cd desktop
desktop % vue create review
2、项目基础配置
module.exports = {
// 关闭语法检查
lintOnSave: false,
}
- src文件夹配置别名,在jsconfig.json文件中,用@/代替src/,如今用脚手架创建的基础项目中都默认配置好了别名,自行选择添加不可以使用该别名的文件即可,exclude表示不可以使用该别名的文件
// 不可以使用该别名的文件
"exclude": [
"node_modules",
"dist"
]
3、组件页面样式
组件页面的样式使用的是less样式,浏览器不识别该样式,需要下载相关依赖,注意下载6版本npm install --save less less-loader@6
如果想让组件识别less样式,则在组件中设置<script scoped lang="less">
注意:在终端开启本地服务器之后,zsh会变成node,此时新加一个,在新的zsh中进行npm下载操作更为方便,无需删除正在运行的node
tips:–save 和 --save-dev的作用和区别
简单来说:
1、使用命令 --save 或者说不写命令 --save ,都会把信息记录到 dependencies 中;dependencies 中记录的都是项目在运行时需要的文件;
2、使用命令 --save-dev 则会把信息记录到 devDependencies 中;devDependencies 中记录的是项目在开发过程中需要使用的一些文件,在项目最终运行时是不需要的,也就是说我们开发完成后,最终的项目中是不需要这些文件的;
4、清除页面默认样式
在public文件夹下的index.html文件中配置如下:<link rel="stylesheet" href="<%= BASE_URL %>reset.css">
5、创建views文件夹,创建路由组件
- 首先得下载路由插件
npm install --save vue-router@3
,需要注意的是,在vue2中需要用3版本的vue-router - 配置路由器,在router文件夹下的index.js文件中作如下配置:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home'
import Search from '@/views/search'
// 使用路由插件
Vue.use(VueRouter)
// 创建一个路由实例,并暴露出去
export default new VueRouter({
routes: [{
path: '/home',
component: Home,
}
})
但是考虑到之后还会用到很多的路由组件,都写在一个组件中太过于冗长,所以单独把路由配置部分拆分出来,单独形成一个routes.js文件,相应的,import部分也要带走:
import Home from '@/views/Home'
import Search from '@/views/search'
export default [{
path: '/home',
component: Home,
},
{
path: '/search',
component: Search
}
]
那么在index.js文件中只要引入routes.js文件即可:
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from '@/router/routes'
// 使用路由插件
Vue.use(VueRouter)
// 创建一个路由实例,并暴露出去
export default new VueRouter({
routes // KV一致省略V
})
- 在入口文件main.js中引入路由器:
import router from '@/router'
...
new Vue({
render: h => h(App),
// 注册路由器,触发简写形式,组件身上都会拥有$route和$router属性,$router是VueRouter的实例对象
router,
}).$mount('#app')
- 在App.vue组件中设置路由出口:
<template>
<div>
...
<!-- 路由组件出口 -->
<router-view></router-view>
...
</div>
</template>
总结:
路由组件和非路由组件区别:
- 非路由组件放在components中,路由组件放在pages或views中
- 非路由组件通过标签使用,路由组件通过路由使用
- 在main.js注册完路由,所有的路由和非路由组件身上都会拥有$router $route属性
- $router:一般进行编程式导航进行路由跳转
- $route: 一般获取路由信息(name path params等)
路由跳转方式:
1、声明式导航router-link标签 :可以把router-link理解为一个a标签,它也可以加class修饰
2、编程式导航 :声明式导航能做的编程式都能做,而且还可以处理一些业务,比如说点击button跳转路由
tips:$router 和 push 方法
- $router
let $router = new VueRouter()
- r o u t e r 是 V u e R o u t e r 的实例对象,当入口文件 n e w V u e 时写入 r o u t e r 就生成了 router是VueRouter的实例对象,当入口文件new Vue时写入router就生成了 router是VueRouter的实例对象,当入口文件newVue时写入router就生成了router和$route,并且挂载到vm、vc上了
-
push方法
1、VueRouter本身上没有push方法
2、是VueRouter原型对象上的方法VueRouter.prototype.push()
3、所以 r o u t e r 上也有 p u s h 方法 ‘ ‘ ‘ router上也有push方法``` router上也有push方法‘‘‘router.push()```
4、push的this指向$router
export default new VueRouter({
routes,
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { y: 0 }
},
})
6、Footer组件显示与隐藏
{
path: '/home',
component: () => import('@/views/Home'),
Meta: { show: true }
}
// 注意这里用到了路由懒加载,后面会讲到
<template>
<div>
...
<Footer v-show="$route.Meta.show" />
...
</div>
</template>
这里使用v-show,因为v-if会频繁的操作dom元素消耗性能,v-show只是通过样式将元素显示或隐藏
7、重定向以及路由传参
{
path: '*',
redirect: '/home'
}
- 路由传参
-
路由传第参数可以通过query参数和params参数实现
1、query参数:不属于路径当中的一部分,类似于get请求,地址栏表现为 /search?k1=v1&k2=v2,query参数对应的路由信息 path: “/search”
2、params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位 ,地址栏表现为 /search/v1/v2,params参数对应的路由信息要修改为path: “/search/:keyword” 这里的/:keyword就是一个params参数的占位符,传递params参数时只能用name,不能用path -
此项目中search传递的是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)、如果传递的是空串,如何解决:
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传递参数:
(a)、props值为对象,该对象中所有的key-value的组合最终都会通过props传给子组件:
props:{a:900}
(b)、props值为布尔值,为true时,则把路由收到的所有params参数通过props传给子组件
props:true
©、props值为函数,该函数返回的对象中每一组key-value都会通过props传给子组件
props($route){
return {
id: $route.query.id,
title: $route.query.title
}
}
子组件用的时候只需要:
props:['id','title']
8、解决多次点击push||replace报错问题
因为push是一个promise,promise需要传递成功和失败两个参数,我们的push中没有传递。
在router/index.js文件中:
- 先把VueRouter原型对象的push||relace先保存一份
- 重写一个push||replace
第一个参数location:告诉原来的push方法,往哪里跳转(传递哪些参数){name: 'search', params: {…}}
第二个参数:成功的回调
第三个参数:失败的回调
这里用.call()而不是直接originPush()是因为全局变量originPush的this是window,要改为$router
let originPush = VueRouter.prototype.push
let originReplace = VueRouter.prototype.replace
VueRouter.prototype.push = function (location, resolve, reject) {
if (resolve && reject) {
originPush.call(this, location, resolve, reject)
} else {
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, () => { }, () => { })
}
}
- 也可以简写为:
VueRouter.prototype.push = function (location, resolve = () => { }, reject = () => { }) {
originPush.call(this, location, resolve, reject)
}
9、定义全局组件
// 引入全局组件
import TypeNav from '@/components/TypeNav'
// 注册全局组件
Vue.component('TypeNav', TypeNav)
之后哪个组件需要用直接写组件标签即可,无需引入,因为已经全局引入
10、封装axios
- axios中文文档:https://www.kancloud.cn/yunye/axios/234845
- 下载axios:npm install --save axios
- 根目录下创建api文件夹,专门用来存放二次封装的axios和统一管理的API接口模块
// 1、引入axios
import axios from 'axios'
// 2、对axios进行二次封装
const requests = axios.create({
baseUrl: '/api',
timeout: 5000
})
// 3、配置请求拦截器
requests.interceptors.request.use(config => {
return config
})
// 4、配置相应拦截器
requests.interceptors.response.use(res => {
return res.data
}, error => {
throw new Error('faile')
})
// 5、导出二次封装的axios
export default requests
11、vue脚手架配置代理来解决跨域问题
devServer:{
proxy:"http://localhost:5000"
}
说明:
- 优点:配置简单,请求资源时直接发给前端(8080)即可。
- 缺点:不能配置多个代理,不能灵活的控制请求是否走代理。
- 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)
方法二
编写vue.config.js配置具体代理规则:
module.exports = {
devServer: {
proxy: {
'/api1': {// 匹配所有以 '/api1'开头的请求路径
target: 'http://localhost:5000',// 代理目标的基础路径
changeOrigin: true,
pathRewrite: {'^/api1': ''}
},
'/api2': {// 匹配所有以 '/api2'开头的请求路径
target: 'http://localhost:5001',// 代理目标的基础路径
changeOrigin: true,
pathRewrite: {'^/api2': ''}
}
}
}
}
/*
pathRewrite的目的是去除掉因为想要匹配路径而设置的/api1,因为请求数据路径中没有/api1
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
changeOrigin默认值为true
*/
说明:
- 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
- 缺点:配置略微繁琐,请求资源时必须加前缀。
-
此项目中,代理服务器先去寻找请求地址是否带有
/api
,如果有就走代理,在后面拼接上想要请求数据的地址,比如'product/getBaseCategoryList'
,也就是说最终地址是'http://gmall-h5-api.atguigu.cn/api/product/getBaseCategoryList'
-
因为之前在
ajax.js
中二次封装axios时配置了基础路径baseURL: '/api'
,所以index.js
中的'product/getBaseCategoryList'
之前不用加上/api
了 -
因为在此项目中每一个数据的请求地址,也就是后面添加的
/api/'product/getBaseCategoryList'
都是带有/api
的,所以pathRewrite: { '^/api': '' }
不写 -
总结一下整个流程:
- 是先在ajax.js中二次封装一个axios用于发送请求,在里面配置一个基础路径/api,请求拦截器和响应拦截器,在拦截器里面可以做一些事情,比如说进度条nprogress
- 因为有很多个API接口,在index.js中定义一个模块对所有API接口进行统一管理
- 解决请求跨域问题,因为自己电脑的端口和请求地址存在跨域问题,在vue.config.js中配置一个代理服务器
'http://gmall-h5-api.atguigu.cn'
12、引入Vuex
1、下载vuex:npm install --save vuex@3
注意,vue2中只能下在vuex3版本
2、新建一个store文件夹,用于存放总的store和各个组件的store
各组件的小store:
const actions = {}
const mutations = {}
const state = {}
const getters = {}
export default {
actions,
mutations,
state,
getters
}
总的store中:
import Vue from 'vue'
import Vuex from 'vuex'
// 引入各个组件的小store
import home from './Home'
Vue.use(Vuex)
// 千万不要忘了这里的 new Vuex.Store 不然发请求的时候会报this.$store.dispatch不是个函数
export default new Vuex.Store({
// 模块化,别忘记这里的modules
modules: {
home
}
})
// 引入vuex的store
import store from '@/store'
new Vue({
render: h => h(App),
// 注册路由器,触发简写形式,组件身上都会拥有$route和$router属性,$router是VueRouter的实例对象
router,
// 注册仓库:组件实例的身上会多一个$store属性
store,
}).$mount('#app')
13、统一封装请求接口
在文件夹api中创建index.js文件,用于封装所有请求
将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们的接口比较多时,如果需要修改只需要修改该文件即可。
import requests from './ajax'
export const reqGetBaseCategoryList = () => {
// 发请求:axios发请求返回结果是Promise对象
return requests({
// 这里前面不用加/api,因为二次封装axios的时候配置了基础路径/api
// 这就相当于和vue.config,js里面配置的代理服务器地址进行拼接
url: '/product/getBaseCategoryList',
method: 'get' // 注意这里是method,组件中方法的集合叫做methods,别混淆
})
}
也可以简写成:
export const reqGetBaseCategoryList = () => requests.get('/product/getBaseCategoryList')
当组件想要使用相关请求时,只需要导入相关函数即可,以上图的reqGetBaseCategoryList 为例,在Home的store中:
// 注意这是分别暴露,得用{}包裹
import { reqGetBaseCategoryList } from '@/api'
const actions = {
// {commit}这里是解构赋值不管要不要commit,得占个位,不然之后传参数会受影响
getBaseCategoryList({commit}) {
reqGetBaseCategoryList()
}
}
14、async await使用
- axios返回的是一个Promise对象
reqCategoryList()
,Promise是异步的,那么context.commit('CATEGORYLIST', result.data)
就会先执行,但此时明显是没有获得到result,肯定会报错,那么就需要引入async和await,async写在函数名前,await写在接口函数categoryList
前面。等待awit后面的reqCategoryList()
执行完,result得到返回值后,才会执行后面的commit操作。
const actions = {
//通过API里面的接口函数调用,向服务器发请求,获取服务器的数据
async categoryList(context) {
let result = await reqCategoryList()
console.log(result) // 要的是result.data
context.commit('CATEGORYLIST', result.data)
}
}
- 当然这里可以采用解构赋值,因为contex是个对象,只要里面的commit属性:
const actions = {
//通过API里面的接口函数调用,向服务器发请求,获取服务器的数据
async categoryList({ commit }) {
let result = await reqCategoryList()
console.log(result) // 要的是result.data
commit('CATEGORYLIST', result.data)
}
}
tips:关于vuex
- 加上了的情况:
- 只要在用到数据的地方,都要加上name,比如说TypeNav里面的index.js中
this.$store.dispatch('home/categoryList')
而此时不能用mapActions
,因为它是使用生命周期钩子一挂载就触发,不是@click等事件触发
- 没加的情况:
- 只能采用mapState对象的写法,在里面传递函数形式,如下
tips:mapState生成计算属性可以有三种形式:
- 数组形式:
computed:{
...mapState('home',['categoryList'])
}
- 对象形式:
computed:{
...mapState('home',{categoryList:'categoryList'})
}
- 其中对象形式里面还可以传递函数:
computed:{
...mapState({
// 这里要传进去一个state,state是最大的库,包含了home和search
categoryList: (state) => {
// 传递函数形式的话可以不用在前面加上home,因为可以写在里面
return state.home.categoryList
}
})
}
- 当然也有简化形式:
computed:{
...mapState({
categoryList: state => state.home.categoryList
})
}
15、利用lodash插件进行节流防抖
- 需要安装lodash插件,官网:https://www.lodashjs.com node_modules默认带了
- 该插件提供了防抖和节流的函数,我们可以引入js文件,直接调用。当然也可以自己写防抖和节流的函数(闭包+延时器)。
- lodash函数库对外暴露 _ 函数,类比于jQuery的 $ 函数
- 首先得引用:
import { throttle } from 'lodash'
- 引用之后就可以直接写
throttle(function)
,不用再._
let result = _.debounce(function(){
console.log('我在一秒之后执行)
},1000)
result()
let result = _.throttle(function(){
console.log('我在一秒之后执行)
},1000)
result()
- 注意:
- 节流方法只能写成ES5的形式,所以在vue里面得更改一下
- 不能使用箭头函数,因为会存在上下文this问题
- 按需引入lodash中的方法后,_.throttle 就得写成 throttle
- lodash里面的throttle方法和debounce方法都是默认暴露的,所以引入的时候不用{ }
methods: {
changeIndex: throttle(function () {
this.currentIndex = index
})
}
- 关于手写防抖节流可以参照之前的博客:https://blog.csdn.net/weixin_53261199/article/details/125053993?spm=1001.2014.3001.5501
16、编程式路由导航+事件委派实现路由跳转(解决声明式路由组件跳转出现卡顿问题)
三级列表中有很多声明式路由导航标签,每一个标签都是一个页面链接,我们要实现通过点击进行路由跳转。
路由跳转的两种方法:导航式路由,编程式路由。
- 对于声明式路由导航来说,router-link是一个组件,router-link会创建组件实例(a标签),并且把虚拟DOM转化为真实DOM,多次操作就会创建多个组件实例(a标签),所以会出现卡顿
- 对于编程式路由,我们是通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。
- 解决办法:编程式路由导航 + 事件委派
事件委派即把子节点的触发事件都委托给父节点。这样只需要一个回调函数goSearch就可以解决
1、给每一个a标签都加上一个自定义属性data-categoryName
,只要有这个属性,就证明这个是需要进行跳转的a标签
2、给一二三级a标签都分别添加一个123级id属性,判断点击是几级的商品标签,页面跳转的时候也要携带商品名称和id。我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息,从而获得商品名和id。
注意:event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。
tips:自定义属性
- 自定义属性:
<div data-index="1"></div>
- vue项目中的自定义属性:
<div :data-categoryName="c1.categoryName"></div>
- 通过
dataset
属性,获取节点的属性信息 - 注意:html页面中不识别大小写,自定义属性的小驼峰在页面上全是小写
17、三级列表优化
- 三级联动模块需要向服务器发请求获取数据,如果放在TypeNav组件的mounted中,每次跳转路由组件都会请求一次,没有必要,只需要在页面生成的时候发起一次请求就足够
- 优化办法:将向服务器发请求的dispatch放在App组件上,因为自始自终App只执行一次,之后路由跳转只会向仓库store要数据,而不用再向服务器发请求要数据了
- 注意1:虽然main.js也是只执行一次,但是不可以放在main.js中。因为只有组件的身上才会有$store属性。
- 注意2:三级联动模块中点击标签会跳转到search路由组件,并且携带参数,搜索框中输入搜索也会跳转并携带参数,要考虑到这两个地方的params、query参数合并问题。参考:
components/Header/index.vue
和components/TypeNav/index.vue
18、mock插件使用
使用步骤:
- 下载mockjs:npm install --save mockjs
- 在项目src文件夹中创建mock文件夹
- 准备JSON数据,注意不能留有空格
- 把mock数据需要的图片放置到public文件夹中
- 创建mockServer.js,通过mockjs插件实现模拟数据
// 引入mockjs模块
import Mock from 'mockjs'
// 引入JSON格式数据
import banner from './banner.json'
import floor from './floor.json'
// 第一个参数是地址,第二个参数是请求数据
Mock.mock('/mock/banner', { code: 200, data: banner })
Mock.mock('/mock/floor', { code: 200, data: floor })
// 引入mock数据,因为没有对外暴露,所以直接引用
import '@/mock/mockServer'
tips:webpack默认对外暴露的数据
- 不需要export,直接引用即可
- 图片
- JSON格式数据
19、请求mock数据(banner、floor)
1、在api下新建一个mockAjax.js文件,二次封装axios,用于发送mock数据的请求,注意此时的baseUrl不是/api了,因为此时不是真实的向服务器发请求,而是/mock
2、在同文件夹下index.js文件中引入mockAjax.js,并且暴露一个发送请求的函数
3、在home小仓库中发送请求,获取banner数据,并保存在state中
20、利用swiper插件实现轮播图(解决轮播图无法展示问题)
1、安装swiper:npm install --save swiper@5
2、在轮播图组件引入swiper插件和css样式,因为之后多次用到轮播图组件,所以将其注册为全局组件,css样式也在main.js中引入
3、在组件中穿件swiper需要的DOM元素(HTML代码,参考官网)
4、创建swiper实例
5、组件要用到轮播图组件的时候,要向轮播图组件传递相应的参数
注意:在创建swiper对象时,我们会传递一个参数用于获取展示轮播图的DOM元素,官网直接通过class(而且这个class不能修改,是swiper的css文件自带的)获取。但是这样有缺点:当页面中有多个轮播图时,因为它们使用了相同的class修饰的DOM,就会出现所有的swiper使用同样的数据,这肯定不是我们希望看到的。
解决方法:在轮播图最外层DOM中添加ref属性
<!--banner轮播-->
<div class="swiper-container" 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(this.$refs.cur,{
loop: true,
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
})
},
解决方法一:等我们的数据请求完毕后再创建swiper实例。只需要加一个1000ms时间延迟再创建swiper实例.。将上面代码改为:
mounted() {
this.$store.dispatch("getBannerList")
setTimeout(()=>{
let mySwiper = new Swiper(this.$refs.cur,{
pagination:{
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
})
},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',
},
})
}
}
即使这样也还是无法实现轮播图,原因是,我们轮播图的html中有v-for的循环,我们是通过v-for遍历bannerList中的图片数据,然后展示。我们的watch只能保证在bannerList变化时创建swiper对象,但是并不能保证此时v-for已经执行完了。假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片(因为swiper对象生效的前提是html即dom结构已经渲染好了)。
完美解决方案:使用watch+this.
n
e
x
t
T
i
c
k
(
)
官方介绍:
t
h
i
s
.
nextTick() 官方介绍: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: "index",
//主键挂载完毕,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',
},
})
})
}
}
}
</script>
21、将轮播图模块提取为全局组件
需要注意的是我们要把定义swiper对象放在mounted中执行,并且还要设置immediate:true属性,这样可以实现,无论数据有没有变化,上来立即监听一次。
利用props实现父组件向子组件传递消息,这里同样也会将轮播图列表传递给子组件,原理相同。
全局组件Carousel代码
<template>
<!--banner轮播-->
<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>
import Swiper from 'swiper'
export default {
name: 'Carousel',
props: ['list'],
watch: {
list: {
immediate: true,
handler() {
this.$nextTick(() => {
let mySwiper = new Swiper(this.$refs.cur, {
loop: true,
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
})
})
},
},
},
}
</script>
Floor组件引用Carousel组件,并传入轮播图数据:<Carousel :carouselList="list.carouselList"/>
我们还记得在首页上方我们的ListContainer组件也使用了轮播图,同样我们替换为我们的公共组件。
ListContainer组件引用Carousel组件,并传入轮播图数据:<Carouse :carouselList="bannerList"/>
注意:
(1)引用组件时要在components中声明引入的组件。
(2)我们将轮播图组件已经提取为全局组件Carouse,所以在入口文件引入轮播图组件和轮播图样式,在Carouse中只需要引入swiper即可。
tips:引包 组件件通信方式 JSON解析 assign组件自定义事件
1、关于引包
如果一个包在后面很多组件都会用到,那么应该在入口文件中引入,例如swiper的样式
2、组件间通信的方式
(1)props:用于父组件 ==> 子组件
(2)自定义事件:可以实现子组件 ==> 父组件 $on
e
m
i
t
(
3
)全局时间总线:
emit (3)全局时间总线:
emit(3)全局时间总线:bus 全能
(4)pubsub-js:第三方库,在vue中几乎不用,功能和
b
u
s
一样全能(
5
)插槽:用于父组件
=
=
>
子组件(
6
)
v
u
e
x
:全能(
7
)
bus一样 全能 (5)插槽:用于父组件 ==> 子组件 (6)vuex:全能 (7)
bus一样全能(5)插槽:用于父组件==>子组件(6)vuex:全能(7)ref:用于父组件 ==> 子组件
3、JSON在线解析及格式化验证网站:https://www.json.cn/
4、使用assign合并对象
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
5、组件的自定义事件
- 在父组件中 <Demo @事件名=‘方法’/>或<Demo v-on:事件名=‘方法’/>
- 在父组件中 this. r e f s . d e m o . refs.demo. refs.demo.on(‘事件名’,方法)
<Demo ref="demo">
......
mounted(){
this.$refs.demo.$on('atguigu',this.test)
}
- 用第一种方法就不要在methods里面在$on了
6、使用assign合并对象
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
22、使用getters的注意事项
- 向服务器发请求的时候,因为数据返回需要一定时间,在数据还没返回到仓库的时候就用getters调取仓库里的数据就会报错,因为初始状态goodInfo就是一个空对象,空对象的属性值是undefined,undefined里面是没有我们要的categoryView属性值的。解决办法就是||一个空对象或者空数组(根据返回的数据类型决定)
- 同样的,computed里面也要注意这种情况
const getters = {
categoryView(state) {
// 防止没有网络或网路不好的时候,请求不到数据,goodInfo里面没有categoryView,至少传一个空对象
return state.goodInfo.categoryView || {}
}
}
- 如果遇到连续使用数据中的数据,比如说以下情况,不能直接在getters里面拼接
const getters = {
categoryView(state) {
// console.log(state)
return state.goodInfo.categoryView || {}
},
skuInfo(state) {
return state.goodInfo.skuInfo || {}
},
//从这以下不能这样写,因为不确定skuInfo能不能获取到,如果获取不到那自然也就没有里面的skuImageList
skuImageList(state) {
return state.goodInfo.skuInfo.skuImageList || []
},
// 同上,不确定能不能获取到skuImageList,自然也就没办法获取到里面的skuImageList[0]
imgObj(state) {
return state.goodInfo.skuInfo.skuImageList[0] || {}
}
}
- 所以以上这种情况,得在组件中用computed进行拼接:
computed: {
...mapGetters(['categoryView', 'skuInfo']),
skuImageList() {
return this.skuInfo.skuImageList || []
},
},
// 如果有传给子组件的要求,那就得在子组件中props获取以后再次拼接
props: ['skuImageList'],
computed: {
imgObj() {
return this.skuImageList[0] || {}
},
},
注意:仓库中的getters是全局属性,是不分模块的。即store中所有模块的getter内的函数都可以通过$store.getters.函数名
获取
23、Object.assign实现对象拷贝(浅拷贝)
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() 。
24、对象深拷贝
针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。
let obj1 = { a: 0 , b: { c: 0}};
let obj2 = Object.assign({}, obj1);
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}
obj1.a = 1;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}}
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}
obj2.a = 2;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}}
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 0}}
obj2.b.c = 3;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}}
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}}
最后一次赋值的时候,b是值是对象的引用,只要修改任意一个,其他的也会受影响
// Deep Clone (深拷贝)
obj1 = { a: 0 , b: { c: 0}};
let obj3 = JSON.parse(JSON.stringify(obj1));
obj1.a = 4;
obj1.b.c = 4;
console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}
25、利用路由信息变化实现动态搜索
-
我们每次进行新的搜索时,我们的query和params参数中的部分内容肯定会改变,而且这两个参数是路由的属性。我们可以通过监听路由信息的变化来动态发起搜索请求。
-
方法:将query和params参数合并到组件的searchParams属性中,监视路由的变化,只要路由一发生改变(query和params改变)就调用getData函数,去更新页面
methods: {
getData() {
this.$store.dispatch('getSearchInfo', this.searchParams)
},
}
search组件watch部分代码:
watch: {
// 如果在这里监视searchParams是不会发生变化的,因为只有页面更新了searchParams才会改变
// 但是监视searchParams是为了数据变化了才去更新页面,所以无解,只能是监视$route,因为它是实时变化的
$route() {
Object.assign(this.searchParams, this.$route.query, this.$route.params)
this.getData()
//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数
//所以每次请求结束后将相应参数制空或undefined,最好是undefined,因为空也会带给服务器,undefeated则不会
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
},
},
26、面包屑相关操作
- 本次项目的面包屑操作主要就是两个删除逻辑。
分为: - 当分类属性(query)删除时删除面包屑同时修改路由信息。
- 当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。
1、query删除时 - 因为此部分在面包屑中是通过categoryName展示的,所所以删除时应将该属性值制空或undefined。
- 可以通过路由再次跳转修改路由信息和url链接
//删除三级列表中的关键词
removeCategoryName() {
this.searchParams.categoryName = undefined
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
this.getData()
if (this.$route.params) {
this.$router.push({ name: 'search', params: this.$route.params })
}
},
removekeyword() {
this.searchParams.keyword = undefined
this.getData()
this.$bus.$emit('clear')
if (this.$route.query) {
this.$router.push({ name: 'search', query: this.$route.query })
}
},
- SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。
- 此处生成面包屑时会涉及到子组件向父组件传递信息操作,之后的操作和上面的面包屑操作原理相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。
在具体的操作内还会涉及一些小的知识点,例如字符串拼接,使用方法如下
var a = 1;
console.log(`a的值是:${a}`); //a的值是:1
27、$bus的使用
1、在Vue实例原型上挂载全局事件总线
main.js
new Vue({
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this
},
...
}).$mount('#app')
2、在子组件中触发事件,声明一个事件的名称
views/Search/index.vuethis.$bus.$emit('clear')
3、在有数据的地方
o
n
,本项目的搜索框中的内容是在
H
e
a
d
组件中,所以在
H
e
a
d
组件中
on,本项目的搜索框中的内容是在Head组件中,所以在Head组件中
on,本项目的搜索框中的内容是在Head组件中,所以在Head组件中on,在Search组件中$emit
components/Head/index.vue
mounted() {
this.$bus.$on('clear', () => {
this.searchMsg = ''
})
},
28、自定义事件和全局事件总线
由于SearchSelector组件中也有商品品牌以及商品属性,想要以面包屑的形式展示到Search组件中,但是数据存在子组件中,这就涉及到子组件向副组件传数据的问题,本项目采的是自定义事件的方式,其实也是可以采用全局事件总线$bus的方式
1、自定义事件
(1)先在子组件商品名称的li中定义一个点击事件,并且传递品牌名@click="TrademarkHandler(Trademark)"
(2)在子组件方法中定义:
methods: {
TrademarkHandler(Trademark) {
this.$emit('TrademarkInfo', Trademark)
}
},
<SearchSelector @TrademarkInfo="TrademarkInfo" />
methods: {
TrademarkInfo(Trademark) {
this.searchParams.Trademark = `${Trademark.tmId}:${Trademark.tmName}`
this.getData()
}
}
2、全局事件总线
(1)先在子组件商品名称的li中定义一个点击事件,并且传递品牌名@click="TrademarkHandler(Trademark)"
(2)在子组件方法中定义:
methods: {
TrademarkHandler(Trademark) {
this.$bus.$emit('TrademarkInfo', Trademark)
}
},
(3)在父组件的生命周期钩子中接收:
mounted() {
this.$bus.$on('TrademarkInfo', (Trademark) => {
this.searchParams.Trademark = `${Trademark.tmId}:${Trademark.tmName}`
this.getData()
})
this.getData()
},
- 要注意的是,在这个例子中,两者的前两个方法都是一样的,都是在子组件中定义点击事件,传递数据$emit
- 不同的是:
1、全局事件总线是 b u s . bus. bus.emit,而自定义事件仅仅是$emit
2、利用全局事件总线在父组件接受数据一般是在生命周期钩子中接收,mounted,而自定义事件则是像组件的方法一样,定义在methods中
3、自定是事件得在父组件中的子组件标签内定义这个自定义事件才可以在下面的methods中定义这个方法
29、商品排序
-
排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。
-
order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。
-
我们的升降序是通过箭头图标来辨别的,图标是iconfont网站的图标,通过引入在线css的方式引入图标
-
在public文件index引入该css
<link rel="stylesheet" href="https://at.alicdn.com/t/font_3390091_pqnuj9i8pi.css">
-
在search模块使用该图标
<li :class="{ active: isOne }" @click="changeOrder('1')">
<a
>综合<span
v-show="isOne"
class="iconfont"
:class="{
'icon-arrowup': isAsc,
'icon-arrowdown': isDesc,
}"
></span
></a>
</li>
<li :class="{ active: isTwo }" @click="changeOrder('2')">
<a
>价格<span
v-show="isTwo"
class="iconfont"
:class="{
'icon-arrowup': isAsc,
'icon-arrowdown': isDesc,
}"
></span
></a>
</li>
这里isOne、isTwo、isAsc、isDesc是计算属性,如果不使用计算属性要在页面中写很长的代码
isOne、isTwo、isAsc、isDesc计算属性代码:
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
},
}
点击‘综合’或‘价格’的触发函数changeOrder:
changeOrder(flag) {
let originFlag = this.searchParams.order.split(':')[0]
let originSort = this.searchParams.order.split(':')[1]
let newOrder = ''
if (flag == originFlag) {
newOrder = `${originFlag}:${originSort == 'desc' ? 'asc' : 'desc'}`
} else {
newOrder = `${flag}:${originSort}`
}
this.searchParams.order = newOrder
this.getData()
},
30、手写分页器
- 实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。但是这个知识还是值得学习一下的。
- 核心属性:pageNo(当前页码)、pageSize(每页展示多少数据)、total(总共多少条数据)、continues(连续展示的页码)
- 核心逻辑:获取连续页码的起始页码和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式返回)
- 当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。
Pagination/index.vue
name: 'Pagination',
props: ['pageNo', 'pageSize', 'total', 'continues'],
computed: {
totalPage() {
return Math.ceil(this.total / this.pageSize)
},
startNumAndEndNum() {
let start = 0,
end = 0
// 总页数小于连续页
const { pageNo, continues, totalPage } = this
if (continues > totalPage) {
start = 1
end = totalPage
} else {
start = pageNo - parseInt(continues / 2)
end = pageNo + parseInt(continues / 2)
// 首页为负数
if (start < 1) {
start = 1
end = continues
}
// 尾页超过总数页数
if (end > totalPage) {
end = totalPage
start = totalPage - continues + 1
}
}
return { start, end }
},
},
<div class="pagination">
<button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">
上一页
</button>
<button v-show="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)">
1
</button>
<button v-show="startNumAndEndNum.start > 2">···</button>
<button
v-for="(page, index) in startNumAndEndNum.end"
:key="index"
v-show="page >= startNumAndEndNum.start"
@click="$emit('getPageNo', page)"
:class="{ active: pageNo == page }"
>
{{ page }}
</button>
<button v-show="totalPage > startNumAndEndNum.end + 1">···</button>
<button
v-show="totalPage > startNumAndEndNum.end"
@click="$emit('getPageNo', totalPage)"
>
{{ totalPage }}
</button>
<button
:disabled="pageNo == totalPage"
@click="$emit('getPageNo', pageNo + 1)"
>
下一页
</button>
<button style="margin-left: 30px">共 {{ total }} 条</button>
<!-- <h1>{{ startNumAndEndNum }}--{{ pageNo }}</h1> -->
</div>
其中getPageNo是自定义事件,在父组件中定义,当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。
views/Search/index.vue
getPageNo(pageNo) {
this.searchParams.pageNo = pageNo
this.getData()
},
tips:解构赋值
props:['pageNo','pageSize','total','continues'],
computed:{
totalPage(){
...
}
}
// 想要用props里面或者是计算属性里面的值可以用解构赋值
const {pageNo,pageSize,continues,totalPage} = this
31、商品详情中的排他思想
想要点击某一个售卖属性,使其具有高亮效果,得利用排他思想,首先得知道点击的谁,还得知道点击的哪一类的其他属性,给遍历出来的每一个属性绑定一个点击事件
@click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)"
methods: {
changeActive(spuSaleAttrValue, spuSaleAttr) {
// 排他
spuSaleAttr.forEach((item) => {
item.isChecked = 0
})
// 被点击的那个获得active
spuSaleAttrValue.isChecked = 1
},
}
32、商品详情中的放大镜
- 商品详情唯一难点就是点击轮播图图片时,改变放大镜组件展示的图片。
- 老师的方法很巧妙:在轮播图组件中设置一个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>
changeImg(index){
//将点击的图片标识位高亮
this.currentIndex = index
//通知兄弟组件修改大图图片
this.$bus.$emit("getIndex",index)
}
- 对应的放大镜组件,首先在mounted监听该全局事件
mounted() {
this.$bus.$on("getIndex",(index)=>{
//修改当前响应式图片
this.currentIndex = index;
})
},
- 放大镜组件中也会有一个currentIndex,他用表示大图中显示的图片的下标(因为放大镜组件只能显示一张图片),全局事件传递的index赋值给currentIndex ,通过computed计算属性改变放大镜组件展示的图片下标。
computed:{
imgObj(){
return this.skuImageList[this.currentIndex] || {}
}
},
<img :src="imgObj.imgurl " />
接下来就是放大镜部分代码
<div class="event" @mousemove="handler"></div> 原大小的图片框
<div class="big"> 放大后的图片框
<img :src="imgObj.imgurl" ref="big" />
</div>
<div class="mask" ref="mask"></div> 原图片框中的遮罩层
methods: {
handler(event) {
let mask = this.$refs.mask
let big = this.$refs.big
let left = event.offsetX - mask.offsetWidth / 2
let top = event.offsetY - mask.offsetHeight / 2
// 限制遮罩范围
if (left <= 0) left = 0
if (left >= mask.offsetWidth) left = mask.offsetWidth
if (top <= 0) top = 0
if (top >= mask.offsetHeight) top = mask.offsetHeight
// 改变遮罩层和大图的位置
mask.style.left = left + 'px'
mask.style.top = top + 'px'
big.style.left = -2 * left + 'px' // -2* 是因为大图是两倍关系,且和鼠标移动方向相反
big.style.top = -2 * top + 'px'
},
},
其中
offsetX:鼠标坐标到元素的左侧的距离
offsetY:鼠标坐标到元素的顶部的距离
offsetWidth: width + padding-left + padding-right + border-left + border-right
offsetHeight: height + padding-top + padding-bottom + border-top + border-bottom
与这几个相关的还有
pageX: 页面X坐标位置
pageY: 页面Y坐标位置
screenX: 屏幕X坐标位置
screenY: 屏幕Y坐标位置
clientX: 鼠标的坐标到页面左侧的距离
clientY: 鼠标的坐标到页面顶部的距离
clientWidth:可视区域的宽度
clientHeight:可视区域的高度
offsetLeft: 该元素外边框距离包含元素内边框左侧的距离
offsetTop:该元素外边框距离包含元素内边框顶部的距离
注意:以上这几个都是不带单位的,要+‘px’
tips:ref和:ref ref和$refs
1、ref和:ref
ref=“big”,这里的big就是普通的字符串,:ref=“big”,这里的big就是vue里面的变量
2、ref和$refs
- ref被用来给元素或子组件注册引用信息,引用信息将会注册在父组件的$refs上,如果在普通的DOMM元素上使用,那么就是指向普通的DOM元素。
- ref的三种用法:
(1)ref加在普通元素上,用this.$refsrefs.name
或者this.refs['name']
获取的是DOM元素
(2)ref加在组件上,用this. refs.name
或者this.refs['name']
获取的是组件实例
(3)ref在v-for中使用,得到的是一个二维数组,数组里面的每一个值是DOM元素
33、购买商品个数的表单可以输入的情况
因为是购买商品个数的表单元素,所以一定是至少为一个,且只能是整数类型。
给输入框定义一个change方法
<input v-model="skuNum" @change="changeSkuNum" />
changeSkuNum(event) {
let value = event.target.value * 1 // 让它变成数字类型的
// 不含字符串或者小于1
if (isNaN(value) || value < 1) {
this.skuNum = 1
} else {
// 保证是整数
this.skuNum = parseInt(value)
}
},
tips:失焦事件
blur与change事件在绝大部分情况下表现都非常相似,输入结束后,离开输入框,会先后触发change与blur,唯有两点例外。
(1)没有进行任何输入时,不会触发change。
在这种情况下,输入框并不会触发change事件,但一定会触发blur事件。在判断表单修改状态时,这种差异会非常有用,通过change事件能轻易地找到哪些字段发生了变更以及其值的变更轨迹。
(2)输入后值并没有发生变更。
这种情况是指,在没有失焦的情况下,在输入框内进行返回的删除与输入操作,但最终的值与原值一样,这种情况下,keydown、input、keyup、blur都会触发,但change依旧不会触发。
34、加入购物车成功路由
- 点击加入购物车时,会向后端发送API请求,但是该请求的返回值中data为null,所以我们只需要根据状态码code判断是否跳转到‘加入购物车成功页面’。
- 因为dispatch其实也就是在调用store中的addOrUpdateShopCart函数,他也是有返回值的,因为它用了async函数,所以返回值也是一个promise,想要获取到返回值,也还是得用async函数,而且我要判断返回过来的是ok还是一个错误,不确定是什么,所以得用try catch来获取。
- 如果是ok那么就将一部复杂的分商品信息(字符串类型的)存入到会话存储
- 简单的商品信息(数字类型的),如添加的商品个数,通过路由信息传递,跳转路由,到支付成功的页面
- 如果失败,那么就打印加入购物车失败信息。
detail组件‘加入购物车’请求函数:
async addShopCar() {
try{
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: this.$route.params.skuId,
skuNum: this.skuNum
});
//一些简单的数据,比如skuNum通过query传过去
//复杂的数据通过session存储,
//sessionStorage、localStorage只能存储字符串
sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo))
this.$router.push({name:'addcartsuccess',query:{'skuNum':this.skuNum}})
}catch (error){
console.log('添加购物车失败!', error.message)
}
}
store/detail/index.js对应代码
async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
const result = await reqAddOrUpdateShopCart(skuId, skuNum)
// console.log(result);
// 加入购物车以后,前台将参数带给服务器,服务器写入成功并没有带来参数,只是告诉写入成功了,所以不用在仓库存了
if (result.code == 200) {
return 'ok'
} else {
throw new Error('faile')
}
}
-
其实这里当不满足result.code === 200条件时,也可以返回字符串‘faile’,自己在addShopCar中判断一下返回值,如果为‘ok’则跳转,如果为‘faile’(或者不为‘ok’)直接提示错误。当然这里出错时返回一个Promise.reject或者直接throw new Error更加符合程序的逻辑。
-
当我们想要实现两个毫无关系的组件传递数据时,首先想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的复杂信息,就可以通过sessionStorage/localStorage实现。
-
sessionStorage、localStorage概念:
sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
注意:无论是session还是local存储的值都是字符串形式。如果我们想要存储对象,需要在存储前JSON.stringify()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象。
35、显示购物车列表,uuid的使用
mounted() {
this.getData()
},
methods: {
// 因为之后要多次修改购物车,多次发请求,所以封装一个函数可以多次调用
getData() {
this.$store.dispatch('getCartList')
},
}
- 虽然请求已经发给服务器,服务器也成功接收到了请求,但是返回的数据却是空的,
{code: 200, message: '成功', data: Array(0), ok: true}
,这是因为每个人都有自己特定的购物车,服务器中存储了所有人的购物车信息,但是发送请求要数据的时候,因为没有传给服务器个人的id,服务器不知道要发哪一条数据,所以为空。
解决办法:
(1)为了使添加到购物车之后就能知道是哪一个用户添加的数据,这个需要用uuid随机生成这个用户的id,存储到localstorage中去,因为用户的id必须每次都是一样的,且是永久存储,不然识别用户身份的目的就达不到了,所以要将生成的id存到本地存储,新建一个utils文件夹,里面专门存放uuid或者token等数据,把往localstorage中存数据封装成一个函数,注意首先要判断localstorage中是否存在uuid,如果没有再存入,要引入uuid文件,返回值为随机id
utils/uuid_token.js
import {v4 as uuidv4} from 'uuid'
//生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储
export const getUUID = () => {
//1、判断本地存储是否由uuid
let uuid_token = localStorage.getItem('UUIDTOKEN')
//2、本地存储没有uuid
if(!uuid_token){
//2.1生成uuid
uuid_token = uuidv4()
//2.2存储本地
localStorage.setItem("UUIDTOKEN",uuid_token)
}
//当用户有uuid时就不会再生成
return uuid_token
}
并且在detail仓库中调用,这样仓库中就有了用户的id。
store/detail/index.js
const state = {
goodInfo: {},
// 在这里获取uuid生成的随机id作为用户标识
uuid_token: getUUID()
}
(2)将用户的id发送给服务器,但是接口文档中的请求地址并没有传递参数的要求,那么就得在之前二次封装axios的模块中的请求拦截器中以请求头的形式传递给服务器,发送之前得确保仓库中此时确实有一个用户id,得进行一个判断。
api/ajax.js
import store from '@/store';
requests.interceptors.request.use(config => {
//config内主要是对请求头Header配置
//1、先判断uuid_token是否为空
if(store.state.detail.uuid_token){
//2、userTempId字段和后端统一 config.headers.userTempId一样的
config.headers['userTempId'] = store.state.detail.uuid_token
}
//后续再添加token
//开启进度条
nprogress.start();
return config;
})
注意:二次封装axios模块中也可以引入store.js,因为仓库已经对外暴露,只要引入就可以使用仓库中的数据。
36、购物车商品数量修改
1、购物车商品信息展示比较简单,就不多做赘述。
2、every函数使用
//判断底部勾选框是否全部勾选
isAllCheck() {
//every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
return this.cartInfoList.every(item => item.isChecked === 1)
}
3、修改商品数量前端代码部分:
注意:通过@click、@change触发handler函数改变商品数量。
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cartInfo)">-</a>
<input autocomplete="off" type="text" :value="cartInfo.skuNum" @change="handler('change',$event.target.value,cartInfo)" minnum="1" class="itxt">
<a href="javascript:void(0)" class="plus" @click="handler('add',1,cartInfo)">+</a>
</li>
methods: {
// 因为之后要多次修改购物车,多次发请求,所以封装一个函数可以多次调用
getData() {
this.$store.dispatch('getCartList')
},
// 要传入三个参数,第一个就是点击的按钮是+还是-还是输入框
// 第二个参数是改变的值,+就传1,-就传-1,这里不用操心,只要将数据改变的量传给服务器就好,服务器收到变化的量会在后端操作,改变数值
// 第三个参数就是点击的是哪一个商品的+和-
handler: throttle(async function (type, disNum, cart) {
// console.log(type, disNum, cart)
switch (type) {
case 'add':
disNum = 1
break
case 'minus':
disNum = cart.skuNum > 1 ? -1 : 0
break
case 'change':
if (isNaN(disNum) || disNum < 1) {
disNum = 0
} else {
disNum = parseInt(disNum) - cart.skuNum
}
break
}
// try...catch必须加上,因为得获得请求结果,服务器返回仓库的不是一个值,是一个状态,仓库返回的是promise的ok或者error,如果成功了,就更新页面,如果失败了就提示错误信息
try {
await this.$store.dispatch('addOrUpdateShopCart', {
skuId: cart.skuId,
skuNum: disNum,
})
//成功之后再去发请求更新页面
this.getData()
} catch (error) {
console.log('更新购物车失败!', error.message)
}
}, 500),
}
37、购物车商品状态修改及删除
这部分都比较简单,这里不多做赘述,唯一需要注意的是当store的action中的函数返回值data为null时,应该采用下面的写法(重点是if,else部分)
action部分:以删除购物车某个商品数据为例
//修改购物车某一个产品的选中状态
async updateCheckedById({ commit }, { skuId, isChecked }) {
const result = await reqUpdateCheckedById(skuId, isChecked)
// console.log(result);
if (result.code == 200) {
return 'ok'
} else {
throw new Error('faile')
}
},
method部分:(重点是try、catch)
async updateChecked(cart, event) {
// console.log(event.target.checked)
try {
// 声明一个isChecked变量,下面可以触发简写形式
// 注意:这边接口文档要求isChecked要么是1要么是0,不能是布尔值
let isChecked = event.target.checked ? 1 : 0
await this.$store.dispatch('updateCheckedById', {
skuId: cart.skuId,
isChecked,
})
this.getData()
} catch (error) {
console.log('勾选失败!', error.message)
}
},
38、删除多个商品
由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。
我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。
actions扩展
官网的教程,一个标准的actions函数如下所示:
deleteCheckedCart(context) {
console.log(context)
}
context中是包含dispatch、getters、state的,即我们可以在actions函数中通过dispatch调用其他的actions函数,可以通过getters获取仓库的数据。
这样我们的批量删除就简单了,对应的actions函数代码让如下
//删除选中的所有商品
deleteCheckedCart({ dispatch, getters }) {
// 因为没有一键删除所有选中的商品的接口,所以得利用之前写的删除某一个商品的接口,利用循环遍历出选中的商品并且删除
let result = [] // let放在forEach里面一样的
getters.cartList.cartInfoList.forEach(item => {
if (item.isChecked == 1) {
// dispatch返回的是一个Promise对象,把返回的多个Promise对象放在数组里,然后用.all方法
result.push(dispatch('deleteCartById', item.skuId))
}
// 这边也可以使用三元表达式
// result.push(item.isChecked==1?dispatch('deleteCartById', item.skuId):'')
})
return Promise.all(result)
},
上面代码使用到了Promise.all
- Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
async deleteCheckedCart() {
try {
await this.$store.dispatch('deleteCheckedCart')
this.getData()
} catch (error) {
console.log('删除商品失败!', error.message)
}
}
updateallCartChecked({ dispatch, getters }, isChecked) {
let result = []
getters.cartList.cartInfoList.forEach(item => {
result.push(dispatch('updateCheckedById', { skuId: item.skuId, isChecked }))
})
return Promise.all(result)
}
methods
async updateallCartChecked(event) {
let isChecked = event.target.checked == 1 ? '1' : '0'
try {
await this.$store.dispatch('updateallCartChecked', isChecked)
this.getData()
} catch (error) {
console.log('勾选失败!', error.message)
}
},
tips:关于路径引用
- 在jsconfig.json文件中用@简化了我们引用路径时候的./和…/等操作,在js文件中引用路径的时候可以直接@/,但是在css文件中,也就是样式中,虽然也可以用@/但是得在前面加一个 ~ ,也就是~@/
39、注册、登录业务
1、验证码部分
输入手机号码,后台会返回一个验证码,正常情况下是给用户的手机发送验证码,但本次项目是直接后台返回,我们要做的是将验证码直接显示到页面上去。
组件中派发请求,判断是否有手机号输入,try catch获取到后台返回来的验证码,展示到页面
views/Register/index.vue
methods: {
async getCode(phone) {
try {
phone && (await this.$store.dispatch('getCode', phone))
this.code = this.$store.state.user.code
} catch (error) {
console.log('获取验证码失败!', error.message)
}
},
}
store/user/index.js
actions = {
async getCode({ commit }, phone) {
const result = await reqGetCode(phone)
// 其实现实情况到发完请求就结束了,等着用户自己输入验证码,但是后台没做这个功能,所以这边自己写
if (result.code == 200) {
commit('GETCODE', result.data)
return 'ok'
} else {
throw new Error('faile')
}
},
}
2、注册部分
带上手机号,密码以及确认密码给后台发送请求,返回数据存到vuex
views/Register/index.vue
methods: {
async userRegister() {
let { phone, password, password1, code } = this
try {
if (phone && password == password1 && code) {
await this.$store.dispatch('userRegister', { phone, password, code })
this.$router.push('/login')
}
} catch (error) {
alert('注册失败!', error.message)
}
},
}
this.$store.dispatch('userRegister',{phone,password,code})
因为K 、V相同,所以只传K
注册成功之后跳转路由到登录页面重新登录
views/Register/index.vue
methods: {
async userRegister() {
let { phone, password, password1, code } = this
try {
if (phone && password == password1 && code) {
await this.$store.dispatch('userRegister', { phone, password, code })
this.$router.push('/login')
}
} catch (error) {
alert('注册失败!', error.message)
}
},
}
store/user/index.js
async userRegister({ commit }, user) {
let result = await reqUserRegister(user)
// console.log(result)
if (result.code == 200) {
return 'ok'
} else {
throw new Error('faile')
}
},
3、登录部分
用户输入手机号和密码之后,点击登录会像后台发送请求,后台返回用户的唯一身份标识token,将token在localStorage中永久存储,带着token进行登录,登录成功后进入到主页,显示用户信息
views/Login/index.vue
methods: {
async Gologin() {
const { phone, password } = this
try {
if (phone && password) {
await this.$store.dispatch('userLogin', { phone, password })
// this.$router.push('/home')
// 判断路径中是否带有redirect字段,如果有就跳转到相应的路由组件
let toPath = this.$route.query.redirect || '/home'
this.$router.push(toPath)
}
} catch (error) {
console.log('登录失败!', error.message)
}
},
},
这里注意,登陆的html结构是个form表单,如果使用@click触发登录事件,form表单会执行默认事件action实现页面跳转。这里我们使用@click.prevent,它可以阻止自身默认事件的执行。
<form >
<div class="input-text clearFix">
<span></span>
<input type="text" placeholder="邮箱/用户名/手机号" v-model="phone">
</div>
<div class="input-text clearFix">
<span class="pwd"></span>
<input type="password" placeholder="请输入密码" v-model="password">
</div>
<div class="setting clearFix">
<label class="checkBox inline">
<input name="m1" type="checkBox" value="2" checked="">
自动登录
</label>
<span class="forget">忘记密码?</span>
</div>
<button class="btn" @click.prevent="Gologin">登 录</button>
</form>
store/user/index.js
const actions = {
async userLogin({ commit }, user) {
let result = await reqUserLogin(user)
if (result.code == 200) {
// 成功了就把服务器返回的token存到仓库中去
commit('USERLOGIN', result.data.token)
// 在这里将token存在本地存储,下次刷新不会token不会消失
// 在utils里面封装好的添加本地存储的函数,增加逼格
setToken(result.data.token)
return 'ok'
} else {
throw new Error('faile')
}
},
}
const mutations = {
USERLOGIN(state, token) {
state.token = token
},
}
const state = {
token: getToken(), // 如果没有就是null,和‘’效果一样
}
其中setToken()是将后台返回的token存储到localStorage中的操作,getToken()就是从localStorage中获取token操作
utils/token.js
export const setToken = (token) => {
localStorage.setItem('USERTOKEN', token)
}
export const getToken = () => {
// 切记要return!!!!!
return localStorage.getItem('USERTOKEN')
}
export const removetoken = () => {
localStorage.removeItem('USERTOKEN')
}
这里要注意,虽然存在本地存储了,但是登录跳转后还是获取不到用户信息,原因就是本地存储的token并没有发送给后台,因为接口中没有token的位置,所以要在请求拦截其的请求头中发送token
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config) => {
// 判断仓库中是否有token,然后再把token以请求头的方式发给服务器
if (store.state.user.token) {
config.headers.token = store.state.user.token
}
// 这是我写的,意思就是不把token存在仓库里,而是直接存在本地存储,之后刷新也持久保存
/* if (localStorage.getItem('USERTOKEN')) {
config.headers.token = localStorage.getItem('USERTOKEN')
} */
// 请求拦截器捕获到请求时进度条开始动
nprogress.start()
// config:配置对象,对象里面有一个属性很重要:headers请求头
return config
})
tips:注册登录之后一刷新用户名消失问题
-
因为vuex仓库里面的存的数据不是持久化的,一刷新,state、getters里的数据都没了,所以之前存在state里面的token没有了,没有token服务器就没有办法返回用户信息,进而在home的header中就没有办法展示,也就是一刷新就没了。
-
要解决这个问题,得持久化存储token,localStorage,建议像uuid那样专门创建一个token的js文件,里面对外暴露一个函数,函数里面的操作就是将传进来的token参数存进本地存储,之后有用到的地方直接调用即可。
-
关于在哪里派发actions的问题,因为是要改变home里面的请登录和免费注册的状态,所以肯定是在Home组件mounted的时候,但是做的时候犯了一个错误,在Header组件中派发了,所以导致服务器没有返回用户信息,因为Header组件在登录页面和Home页面都有,如果在Header组件mounted的时候派发,那么就只能在Header组件刚生成的第一次就派发一次,此时是没有token的请求,之后点击了登陆,生成了token,跳转路由到Home页面,但是由于Header组建并没有重新mounted,所以token还是没有发给服务器,这就是为什么服务器没有返回用户信息的原因,所以应该在Home组件mounted的时候派发actions,登录成功后生成token,路由跳转,Home组件再次mounted,携带token给服务器发请求,服务器返回用户信息。
const actions = {
// 因为token存在这里,向服务器发请求时需要携带token,自然要在这个仓库写三连环
async getUserInfo({ commit }) {
let result = await reqGetUserInfo()
if (result.code == 200) {
commit('GETUSERINFO', result.data)
return 'ok'
} else {
throw new Error('faile')
}
},
}
在home组件中派发action
mounted() {
try {
await this.$store.dispatch('getUserInfo')
} catch (error) {
console.log(error.message)
}
},
// 优化之后不在这里发请求了,登录之后每个组件都要获得用户信息,所以在全局路由守卫那边发请求
Head组件页面展示
computed: {
userName() {
return this.$store.state.user.userInfo.name
},
},
面试提问:token为什么不存到cookie中而是要存在localStorage中?
一、token的存放位置
token就是一个凭证,用户成功登陆之后返回给客户端,客户端主要有以下几种存储方式
1、存储在localStorage中,每次调用接口的时候都把它当作一个字段传给后台
2、存储在cookie中,让它自动发送,不过缺点就是不能跨域
3、拿到之后存储在localStorage中,每次调用接口的时候放在HTTP请求头的Authorization字段里面。token在客户端一般存放在localStorage、cookie或者sessionStorage中。
二、token放在cookie、localStorage、sessionStorage中的不同点
1、将token放在localStorage或sessionStorage
Web存储(localStorage/sessionStorage)可以通过同一域商Javascript访问。这意味着任何在你的网站上的运行的JavaScript都可以访问Web存储,所以容易受到XSS攻击。尤其是项目中用到了很多第三方JavaScript类库。如果js脚本被盗用,攻击者就可以轻易访问你的网站, webStroage作为一种储存机制,在传输过程中不会执行任何安全标准。
为了防止XSS,一般的处理是避开和编码所有不可信的数据。但这并不能百分百防止XSS。比如我们使用托管在CDN或者其它一些公共的JavaScript库,还有像npm这样的包管理器导入别人的代码到我们的应用程序中。
- xss攻击: cross-site Scripting(跨站脚本攻击)是一种注入代码攻击。恶意攻击者在目标网站上注入script代码,当访问者浏览网站的时候通过执行注入的script代码达到窃取用户信息,盗用用户身份等。
2、将Token存储与cookie
优点:可以指定 httponly,来防止被Javascript读取,也可以指定secure,来保证token只在HTTPS下传输。
缺点:不符合Restful最佳实践,容易受到CSRF攻击。
CSRF跨站点请求伪造(Cross-Site Request Forgery),跟xsS攻击一样,存在巨大的危害性。简单来说就是恶意攻击者盗用已经认证过的用户信息,以用户信息名义进行一些操作〈如发邮件、转账、购买商品等等)。由于身份已经认证过,所以目标网站会认为操作都是真正的用户操作的。CSRF并不能拿到用户信息,它只是盗用的用户凭证去进行操作。
总结:
localStorage具有更灵活,更大空间,天然免疫 CSRF的特征。Cookie空间有限,而JWT一半都占用较多字节,而且有时你不止需要存储一个JWT。
确保你的代码以及第三方库的代码有足够的XSS检查,在此之上将token存放在localStorage中。在XSS面前,即便你的httpOnly cookie无法被获取,黑客依然可以诱导或者在用户毫不知情的情况下做任何事情。记住!黑客的代码和你的代码一样被用户信任!XSS只要存在那么无论将信息存储在cookie还是localStorage,都是一样脆弱不堪,唯一的区别只是获取难度。XSS漏洞很难被发现,因为一个网站的构建不仅仅是基于你自己的代码,第三方的代码同样已可能存在XSS。
40、全局前置路由守卫,显示用户信息
以上我们获取到了用户信息并且在主页进行展示,但是一旦我们切换到其他页面,比如说Search页面,用户信息就消失了,原因就是刚刚一直在home组件中dispatch,在其他组件中并没有派发,所以其他组件获取不到用户信息。
解决的三个办法:
1、简单粗暴的方法,那就是在每一个用到用户信息的组件中都派发一下,但那不是我们想要的,我们想要派发一次,只要用到用户信息的组件都可以获取到。
2、在App组件的mounted中派发,好处是只用派发一次,所有组件都可以显示用户信息了,但是组建挂载完毕才发请求,传过来的用户信息并没有及时的展现在页面上,必须得再次刷新页面,用户信息才会展示,这显然也不合理
3、利用全局前置路由守卫router.beforeEach(),在路由守卫中派发一次,以后只要用用户信息的组件都可以获取到,且不用刷新,第一次就可以获取到
注意:全局前置路由守卫需要传递一个箭头函数,里面可以传三个参数,分别是to、from和next
to:就是要跳转的路由地址
from:就是从哪个路由跳转过来的
next:进行跳转
to和from并不是要求全都要传,但是next必不可少,不然无法进行跳转。
router/index.js
// 全局前置路由守卫
router.beforeEach(async (to, from, next) => {
next() // 首先要放行,不然全都给拦住了
let token = store.state.user.token
if (token) {
if (to.path == '/login' || to.path == '/register') { // 用户登录了就不能再去登录也注册页面了
next('/home')
} else {
try {
await store.dispatch('getUserInfo')
} catch (error) {
// 用户登录了,想去非登录和注册页面,但是token失效了,得先退出登录,清除token,然后再重新登录
await store.dispatch('userlogout')
// 返回登录页面
next('/login')
}
}
} else {
let toPath = to.path
if (toPath.indexOf('/center') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/Trade') != -1) {
// 没有登录时,点击想要去的地址以query参数的形式在地址栏传给login,登录成功之后直接跳转到想去的页面而不是主页
next('/login?redirect=' + toPath)
} else {
next() // 没有登录,且用户并不想去center、pay或者Trade页面,那就直接跳转
}
}
})
在登录界面也要进行修改,不能一味的向主页跳,而是要判断路径中是否含有redirect字段
views/Login/index.vue
methods: {
async Gologin() {
const { phone, password } = this
try {
if (phone && password) {
await this.$store.dispatch('userLogin', { phone, password })
// this.$router.push('/home')
// 判断路径中是否带有redirect字段,如果有就跳转到相应的路由组件
let toPath = this.$route.query.redirect || '/home'
this.$router.push(toPath)
}
} catch (error) {
console.log('登录失败!', error.message)
}
},
},
tips:向服务器发请求的另一种方法
1、之前的所有请求都是经过vuex仓库来发,而如果没有vuex的话,比如说后台操作,那就得依靠全局事件总线来做,在入口文件main.js中引入所有接口的模块,并且把它挂载到Vue的原型对象上,这样所有的组件中就都不用在引入接口文件,直接使用Vue原型对象上的接口即可。
main.js
// 引入统一接口API
import * as API from '@/api'
new Vue({
render: h => h(App),
// 注册路由器,触发简写形式,组件身上都会拥有$route和$router属性,$router是VueRouter的实例对象
router,
// 注册仓库:组件实例的身上会多一个$store属性
store,
beforeCreate() {
Vue.prototype.$bus = this
Vue.prototype.$API = API // 把接口都挂载到vm上
}
}).$mount('#app')
用的时候直接在组件中调用接口去向后台索要数据,返回的数据直接存在当前组件的data中
methods: {
async getorderInfo() {
let result = await this.$API.reqOrderPayInfo(this.orderId)
if (result.code == 200) {
this.payInfo = result.data
}
},
}
2、此项目从提交订单开始就是用全局事件总线$bus来替代vuex了
3、切记:不要在生命周期钩子上使用async|await,可以在methods里面定义一份函数,然后在mounted里调用这个函数,可以在methods中使用async,这样就可以达到目的。
mounted() {
this.getorderInfo()
},
methods: {
async getorderInfo() {
let result = await this.$API.reqOrderPayInfo(this.orderId)
if (result.code == 200) {
this.payInfo = result.data
}
},
}
41、支付订单 Element UI
利用Element UI做支付二维码的弹出框
因为展示付款二维码之后,页面不能只刷新一次,要一直刷新,去查看是否已经完成了支付,没有支付就一直刷新,直到支付完成了之后才进行跳转,所以需要一个定时器
views/pay/index.vue
methods: {
open() {
this.$alert(
`<img height="200px" width="200px" src="${require('@/assets/images/pay.jpg')}">`,
'请用微信支付',
{
dangerouslyUseHTMLString: true,
center: true,
showClose: false,
showCancelButton: true,
// element ui自带api
beforeClose: (action, instance, done) => {
if (action == 'cancel') {
alert('请联系管理员')
clearInterval(this.timer)
this.timer = null
done() // 关闭弹出框
} else {
// 这里应该判断一下code是否为200,但是要付钱,所以我设置成直接通过了
// if (this.code == 200) {
clearInterval(this.timer)
this.timer = null
done()
this.$router.push('/paySuccess')
// }
}
},
}
)
if (!this.timer) {
this.timer = setInterval(async () => {
let result = await this.$API.reqOrderPayStatus(this.orderId)
console.log(result)
if (result.code == 200) {
clearInterval(this.timer) // 清除定时器
this.timer = null // 清空timer
this.code = result.code // 把支付状态码存起来
this.$msgBox.close() // 关闭遮罩层
this.$router.push('/paySuccess') // 跳转路由
}
}, 1000)
}
},
},
tips:Vue图片引入
非js内引入图片(html):一般都是通过路径引入,例如:。
js内引入图片: 通过路径方式在vue中的js引入图片,必须require引入。
例如:js中引入个人支付二维码可以通过下面方式实现
this.$alert(
`<img height="200px" width="200px" src="${require('@/assets/images/pay.jpg')}" / >`,
'请使用微信扫码',
{
dangerouslyUseHTMLString: true,
center: true,
showClose: false,
showCancelButton: true,
...
});
42、查看订单(个人中心center)
复习一下二级路由
注意:二级路由要么不写/,要写的话就要把路径写全 ‘/center/myOrder’
export default [
{
path: '/center',
component: () => import('@/views/Center'),
Meta: { show: true },
children: [{
path: 'myOrder',
component: () => import('@/views/Center/MyOrder')
},
{
path: 'groupOrder',
component: () => import('@/views/Center/GroupOrder')
},
// 默认显示
{
path: '/center',
redirect: 'myOrder'
}
]
},
]
其中,{ path: '', redirect: 'myorder' }
表示当我们访问center路由时,center中的router-view部分默认显示myorder二级路由内容,不然右侧部分就是空白的。
总结警告缘由:当某个路由有子级路由时,父级路由须要一个默认的路由,因此父级路由不能定义name属性,解决办法是去掉name:'Center’
就好了。
43、独享路由守卫
全局导航守卫已经帮助我们限制未登录的用户不可以访问相关页面。但是还会有一个问题。
例如:用户已经登陆,用户在home页直接通过地址栏访问Trade结算页面,发现可以成功进入该页面,正常情况,用户只能通过在shopcart页面点击去结算按钮才可以到达Trade页面。我们可以通过路由独享守卫解决该问题。
路由独享的守卫:只针对一个路由的守卫,所以该守卫会定义在某个路由中。
以上面问题为例,我们可以通过路由独享的守卫解决。
在Trade路由信息中加入路由独享守卫beforeEnter(),独享路由守卫和全局前置守卫一样,也有三个参数,to、from、next
{
path: '/Trade',
component: () => import('@/views/Trade'),
Meta: { show: true },
beforeEnter: (to, from, next) => {
if (from.path == '/shopcart') {
next()
} else {
next(false)
}
},
},
其中next(false)
是终断当前的导航,也就是回到from路由
但是,上面的代码还会有bug,就是当我们在shopcart页面通过地址栏访问Trade时还是会成功。正常情况应该是只有当我们点击去结算按钮后才可以进入到Trade页面。
解决办法:
在shopcart路由信息Meta中加一个flag,初始值为false。当点击去结算按钮后,将flag置为true。在Trade的独享路由守卫中判断一下flag是否为true,当flag为true时,代表是通过点击去结算按钮跳转的,所以就放行。
shopcart路由信息
//购物车
{
path: "/shopcart",
name: 'ShopCart',
component: ()=> import('../pages/ShopCart'),
Meta:{show: true,flag: false},
},
shopcart组件去结算按钮触发事件
<a class="sum-btn" @click="toTrade">结算</a>
toTrade(){
this.$route.Meta.flag = true
this.$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){
from.Meta.flag = false
next()
}else{
next(false)
}
}
},
注意,判断通过后,在跳转之前一定要将flag置为false。
tips:常用插件(npm中都可以找到)
44、Vue使用插件原理
每个插件都会对外暴露一个对象,且都会有一个install方法,install后就可以在我们的代码中可以使用该插件。这个install有两类参数,第一个为Vue实例,后面的参数可以自定义。
vue使用插件的步骤
1、引入插件 import VueLazyload from “vue-lazyload”;
2、注册插件Vue.use(VueLazyload)
这里的Vue.use()实际上就是调用了插件的install方法。如此之后,我们就可以使用该插件了。
45、路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
{
path: '/addcartsuccess',
name: 'addcartsuccess',
component: () => import('@/views/AddCartSuccess'),
Meta: { show: true }
},
{
path: '/detail/:skuId',
component: () => import('@/views/Detail'),
Meta: { show: true }
},
46、文件打包
- npm run build
- dist文件下的js文件存放我们所有的js文件,并且经过了加密,并且还会生成对应的map文件。
- map文件的作用:因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了map就可以向未加密的代码一样,准确的输出是哪一行那一列有错。
- 当然map文件也可以去除(map文件大小还是比较大的),在vue.config.js配置
productionSourceMap: false
即可。 - 注意:vue.config.js配置改变,需要重启项目