day1
1.1 项目结构介绍
node_modules文件夹:项目依赖文件夹
public文件夹:一般放置一些静态资源(图片),需要注意的是:放在public文件夹中的静态资源,webpack进行打包的时候会原封不动的打包到dist文件夹中
src文件夹(程序员源代码文件夹):
assets文件夹:一般也是放置静态资源(一般放置多个组件共用的静态资源),需要注意:放置在assets文件夹里面的静态资源,在webpack打包的时候,webpack会把静态资源当做一个模块,打包到JS文件中
components文件夹:一般放置的是非路由组件(全局组件)
App.vue:唯一的根组件,Vue当中的组件(.vue)
babel.config.js:配置文件(babel相关)
package.json文件:认为是项目的“身份证”,记录项目叫做什么、项目中有哪些依赖、项目怎么运行
package-lock.json:缓存性文件
README.md:说明性文件
1.2 项目配置
项目运行起来的时候,让浏览器自动打开:
找到package.json文件
修改scripts配置项的serve命令(添加--open)
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
}
关闭eslint校验:
添加下列语句即可
lintOnSave:false
src文件夹简写方法:配置别名 @(在jsconfig.json文件夹中)
webstorm里自带下列代码,有需要的朋友可以复制一下放到jsonconfig.json中
@代表的是src文件夹,这样将来即使文件过多,找的时候也很方便
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
}
1.3 路由分析
项目路由的分析:vue-router
前端所谓路由:KV键值对
key:URL(地址栏中的路径)
value:相应的路由组件
项目为上中下结构
路由组件:Home首页路由组件、Search路由组件、Login登录组件、Register注册组件
非路由组件:Header头部组件(首页、搜索页)、Footer尾部组件(在首页、搜索页有,但是在登录页面是没有的)
开发项目的顺序:
1.书写静态页面(HTML+CSS)
2.拆分组件
3.获取服务器的数据动态展示
4.完成相应的动态业务逻辑
路由组件的搭建
下载vue-router:注意vue2对应vue-router@3,vue3对应vue-router@4
由上述分析,路由组件应该有四个:Home、Search、Login、Register
components文件夹:经常放置的是非路由组件(共用全局组件)
pages|views文件夹:经常放置路由组件
配置路由:项目中配置的路由一般放置在router文件夹中
总结:
路由组件与非路由组件的区别:
1.路由组件一般放置在pages|views文件夹下,非路由组件一般放置在components文件夹中
2.路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以标签的形式使用
3.注册完路由,不管路由组件、还是非路由组件,身上都有$route、$router属性
$route:一般用于获取路由信息(路径、query、params等等)
$router:一般进行编程式导航,进行路由跳转(push、replace)
路由的跳转:
路由的跳转有两种形式:
声明式导航router-link,可以进行路由的跳转
编程式导航push|replace,可以进行路由跳转
编程式导航:
声明式导航能做的,编程式导航都能做
但是编程式导航除了可以进行路由跳转,还可以做一些其他的业务处理
//搜索按钮的回调函数,需要向search路由进行跳转
goSearch(){
this.$router.push('/search')
}
声明式导航:
<router-link to="/login">登录</router-link>
1.4 Footer组件的显示与隐藏
显示或者隐藏组件:v-if|v-show
Footer组件:在Home、Search显示Footer组件
Footer组件:在登录、注册的时候隐藏
<Footer v-show="$route.path=='/home'||$route.path=='/search'"/>
我们可以根据组件身上的$route获取当前路由信息,通过路由路径判断Footer的显示与隐藏
在配置路由的时候,可以给路由添加路由元信息[meta]
routes:[
{
path:'/home',
component:Home,
meta:{show:true}
},
{
path:"/search",
component:Search,
meta:{show:true}
},
{
path:"/login",
component:Login,
meta:{show:false}
},
{
path:"/register",
component:Register,
meta:{show:false}
},
//重定向:在项目跑起来的时候,访问"/",立马让它定向到首页
{
path:'*',
redirect:"/home"
}
]
<Footer v-show="$route.meta.show"/>
1.5 路由传参
路由跳转有几种方式?
比如:A->B
声明式导航:router-link(务必要有to属性),可以实现路由的跳转
编程式导航:利用的是组件实例的$router.push|$router.replace方法,可以实现路由的跳转(可以书写一些自己的业务)
路由传参,参数有几种写法?
params参数:属于路径当中的一部分。需要注意,在配置路由的时候,需要占位
{
path:"/search/:keyword",
component:Search,
meta:{show:true}
}
query参数:不属于路径当中的一部分,类似于ajax中的queryString /home?k=v&k=v,不需要占位
路由传递参数:
字符串形式:
this.$router.push('/search/'+this.keyword+"?k="+this.keyword.toUpperCase())
模板字符串形式:
this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)
对象的形式:
注意:使用对象的形式时,需要给要跳转的路由命名,而不能使用路径来跳转
this.$router.push({
name:'search',
params:{
keyword:this.keyword
},
query:{
k:this.keyword.toUpperCase()
}
})
1.6 面试题
面试题1:路由传递参数(对象写法)path是否可以结合params参数一起使用?
不能,路由跳转传参的时候,对象的写法可以是name、path形式,但需要注意的是,path这种写法不能和params参数一起写
面试题2:如何指定params参数可传可不传?
如果路由要求传递params参数,但你不传,那么url会出现问题
如果想要指定params参数可传可不传,需要在配置路由的时候在占位的后面加上一个问号
{
name:'search',
path:"/search/:keyword?",
component:Search,
meta:{show:true}
}
面试题3:params参数可以传递也可以不传递,但如果传递的是空串,如何解决?
使用undefined解决:params参数可以传递、不传递或传递空的字符串
this.$router.push({
name:'search',
params:{keyword:""||undefined},
query:{k:this.keyword.toUpperCase()}
})
面试题4:路由组件能不能传递props数据?
布尔值写法:只能传递params参数
props:true
对象写法:额外的给路由组件传递一些props
props:{a:1,b:2}
函数写法:可以params参数、query参数,通过props传递给路由组件
props:($route)=>{
return {keyword:$route.params.keyword,k:$route.query.k}
}
day2
2.1 重写push和replace方法
编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误
首先,底层push方法会返回一个Promise对象
function push(){
return new Promise((resolve,reject)=>{
})
}
解决方法:
1.通过给push方法传递相应的成功、失败的回调函数,可以捕获到当前的错误并解决
这种写法治标不治本,将来在别的组件当中push|replace,编程式导航还有类似错误
this.$router.push({
name:'search',
params:{keyword:this.keyword},
query:{k:this.keyword.toUpperCase()},
},()=>{},()=>{})
2.
this:当前组件实例(search)
this.$router属性:当前的这个属性,属性值VueRouter类的一个实例,当在入口文件注册路由的时候,给组件实例添加$router|$route属性
function VueRouter(){
}
//原型对象的方法
VueRouter.prototype.push=function(){
//函数的上下文为VueRouter类的一个实例
}
let $router=new VueRouter()
$router.push(xxx)
重写方法:
//先把VueRouter原型对象的push保存一份
let originPush=VueRouter.prototype.push
//重写push|replace
//第一个参数:告诉原来的push方法,你往哪里跳转(传递哪些参数
VueRouter.prototype.push=function(location,resolve,reject){
if(resolve&&reject){
//call|apply区别
//相同点:都可以调用函数依次,都可以篡改函数的上下文一次
//不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行传递数组
originPush.call(this,location,resolve,reject)
}else{
originPush.call(this,location,()=>{},()=>{})
}
}
2.2 三级联动组件
由于三级联动,在Home、Search、Detail中都有使用,所以把三级联动注册为全局组件
好处:只需要注册一次,就可以在项目的任何地方使用
全局组件的注册:
第一个参数:全局组件的名字
第二个参数:指定是哪一个组件
Vue.component(TypeNav.name,TypeNav)
2.3 axios的二次封装
为什么要对axios进行二次封装?
请求拦截器、响应拦截器:请求拦截器,可以在发请求之前处理一些业务
响应拦截器,当服务器数据返回以后,可以处理一些事情
在项目中API文件夹常用来放置axios相应文件
接口当中,路径都带有/api
==>baseURL:"/api"
import axios from "axios";
const requests = axios.create({
baseURL:"/api",
timeout:5000
})
//请求拦截器
requests.interceptors.request.use((config)=>{
//config是headers的请求头
return config
})
//响应拦截器
requests.interceptors.response.use((res)=>{
//成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,并做一些处理
return res.data
},
(error)=>{
//响应失败的回调函数
return Promise.reject(new Error(error))
})
export default requests
2.4 跨域问题
什么是跨域?
协议、域名、端口号不同的请求,称之为跨域
http://localhost:8080/#/home ---前端项目本地服务器
http://39.98.123.211 ---后台服务器
//配置代理跨域
devServer:{
proxy:{
"/api":{
target:"http://gmall-h5-api.atguigu.cn",
}
}
}
2.5 nprogress进度条的使用
//引入进度条
import nprogress from 'nprogress'
//引入进度条的样式
import 'nprogress/nprogress.css'
进度条的开始:
nprogress.start()
进度条的结束:
nprogress.end()
2.6 vuex状态管理库
vuex是什么?
vuex是官方提供的一个插件,状态管理库,集中式管理项目中共用的数据
2.7 mapState
mapState传入参数是一个对象,对象的键值对中,值是一个函数。当时用这个计算属性的时候,右侧的函数会立即执行一次
函数注入一个参数为state,即为大仓库中的数据
...mapState({
categoryList:(state)=>{
return state.typeNav.categoryList
}
})
2.8 三级联动动态背景颜色改变
动态添加类:
<div class="item"
v-for="(c1,index) in categoryList"
:key="c1.categoryId"
:class="{cur:currentIndex===index}"
>
currentIndex的改变,初始值为-1:
changeIndex(index){
this.currentIndex=index
},
leaveIndex(){
this.currentIndex=-1
}
类:
.cur{
background-color: skyblue;
}
2.9 通过JS控制二三级分类的显示与隐藏
<div class="item-list clearfix"
:style="{display:currentIndex===index?'block':'none'}">
2.10 演示卡顿现象并引入防抖和节流
卡顿现象:事情触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很有可能出现浏览器卡顿)
函数的节流与防抖:
节流:在规定的间隔时间范围内不会重复处罚回调,只有大于这个时间才会触发回调,把频繁触发变为少量触发
防抖:前面所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,则只会执行一次
插件lodash:封装了函数的防抖与节流业务
三级联动节流:
引入throttle函数:
import throttle from 'lodash/throttle'
使用节流函数throttle:
changeIndex:throttle(function(index){
this.currentIndex=index
},50),
day3
3.1 三级联动组件的路由跳转与参数传递
三级联动用户可以点击:一级分类、二级分类、三级分类
当你点击的时候,Home模块跳转到Search模块,一级会把用户选中的产品(产品的名字、产品的ID)在路由跳转的时候,进行传递
router-link是一个组件,当服务器的数据返回值后,会循环出很多的router-link组件实例
创建组件实例的时候,一瞬间创建1000+组件实例是很耗用内存的,因此可能出现卡顿现象
最好的解决方案:编程式导航+事件委派 实现路由的跳转和参数的传递
利用事件委派存在的一些问题:
1.点击的不一定是a标签
事件委派是把所有的子节点(h3/dt/dl/em)的时间委派给父亲结点。而只有点击a标签的时候,才会进行路由跳转,但如何确定点击的一定是a标签呢?
即使你能确定点击的是a标签,如何区分是一级、二级还是三级分类的a标签呢?
2.如何获取参数【1/2/3级分类的产品名字、id】
解决方案:
1.把子节点当中的a标签加上自定义属性data-categoryName,而其他的子节点是没有的
<a :data-categoryName="c1.categoryName">{{c1.categoryName}}</a>
2.给每一个a标签添加上对应的data-category1Id/data-category2Id/data-category3Id,用来区分不同级别的分类
总的解决方案:
goSearch(event){
let {categoryname,category1id,category2id,category3id} = event.target.dataset
if(categoryname){
//整理路由跳转的参数
let location={name:'search'}
let query={categoryName:categoryname}
if(category1id){
query.categoryName=category1id
}else if(category2id){
query.categoryName=category2id
}else{
query.categoryName=category3id
}
//整理参数
location.query=query
//路由跳转
this.$router.push(location)
}
}
day4
4.1 三级联动效果在不同路由组件中的显示情况
需求:
在home组件下,会默认显示二级菜单和三级菜单
但search组件下,只默认显示一级菜单
解决方案:
通过v-show动态控制二级菜单和三级菜单的显示与隐藏(v-show="show")
通过路由控制,决定show属性的true/false
原理:每跳转到一个组件,如果有TypeNav,都会重新创建一个TypeNav的实例,也就是说mounted函数会再次执行。可以将路由判断的逻辑写在mounted钩子里
mounted(){
//通知Vuex发送请求,获取数据,存储于仓库之中
this.$store.dispatch('categoryList')
if(this.$route.path!='/home'){
this.show=false
}
}
在鼠标滑过“全部商品分类”时,应该有对应的二级分类、三级分类的展示
对于home组件,鼠标离开时二级分类不会消失
对于search组件,鼠标离开时只展示一级分类
逻辑判断:
TypeNav模块:
<div @mouseleave="leaveShow" @mouseenter="enterShow">
enterShow(){
this.show=true
},
leaveShow(){
this.currentIndex=-1
if(this.$route.path!='/home'){
this.show=false
}
}
4.2 search模块中三级路由组件的过渡效果
过渡动画:前提是组件/元素务必要有v-if/v-show指令,才可以进行过渡动画
<!--过渡动画-->
<transition name="sort">
<div class="sort" v-show="show">
……中间内容省略
</div>
</transition>
//过渡动画的样式
.sort-enter{
height:0;
}
.sort-enter-to{
height:461px;
}
.sort-enter-active{
transition:all .7s linear
}
4.3 TypeNav商品分类列表的优化
由于每次home/search组件的销毁和挂载都会导致TypeNav的销毁和挂载,即都会执行mounted函数,从而频繁地向服务器发送请求。但是三级联动组件获取到的内容是不变的,所以可以做优化
可以将三级联动的数据请求放在App.vue的钩子函数中:
mounted(){
//通知Vuex发送请求,获取数据,存储于仓库之中
this.$store.dispatch('categoryList')
}
4.4 合并参数
捎带params参数:
//判断:如果路由跳转的时候带有params参数,也要捎带传递过去
if(this.$route.params){
location.params=this.$route.params
}
合并参数:
//搜索按钮的回调函数,需要向search路由进行跳转
goSearch(){
let location={
name:'search',
params:{keyword:this.keyword||undefined}
}
if(this.$route.query){
location.query=this.$route.query
}
this.$router.push(location)
}
4.5 mockjs模拟数据
开发Home首页当中的ListContainer组件与Floor组件:
因为服务器返回的数据只有商品分类和菜单分类的数据,对于ListContainer组件与Floor组件数据服务器是没有提供的。可以通过mockjs模拟
使用步骤:
1)在项目当中src文件夹中创建mock文件夹
2)第二步准备JSON数据(mock文件夹中创建相应的JSON文件)
3)把mock数据需要的图片放置到public文件夹中(public文件夹在打包的时候,会把相应资源原封不动地放在dist文件夹下)
4)创建mockServer.js,开始实现mock虚拟数据
5)把mockServer.js文件在入口文件中引入(至少需要执行一次,才能模拟数据)
问:JSON数据格式文件根本没有对外暴露,为什么可以直接引入?
答:webpack默认对外暴露图片和JSON数据格式文件
Mock.mock()函数有两个参数
第一个参数是请求的地址
第二个参数是请求的数据
//先引入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
})
封装mockServer.js
import axios from 'axios'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'
let requests=axios.create({
baseURL:"/mock",
timeout:5000
})
requests.interceptors.request.use((config)=>{
nprogress.start()
return config
})
requests.interceptors.response.use(
(res)=>{
nprogress.done()
return res.data
},
(err)=>{
return Promise.reject(new Error(err))
}
)
export default requests
发送请求的函数:
//获取banner(Home首页轮播图接口)
export const reqGetBannerList=()=>mockRequest.get('/banner')
//获取floor
export const reqGetFloor=()=>mockRequest.get('/floor')
获取banner轮播图的数据:
vuex处理:
import { reqGetBannerList} from "@/api";
const state={
bannerList:[]
}
const mutations={
GETBANNERLIST(state,bannerList){
state.bannerList=bannerList
}
}
const actions={
async getBannerList({commit}){
let result=await reqGetBannerList()
if(result.code===200){
commit('GETBANNERLIST')
}
}
}
const getters={}
export default{
state,mutations,actions,getters
}
获取数据:
computed:{
...mapState({
bannerList:state=>state.home.bannerList
})
}
4.6 swiper
第一步:引包(引入相应的JS/CSS)
在main.js引入一次即可:
import 'swiper/css/swiper.css'
import 'swiper/js/swiper'
第二步:页面中的结构
<div class="swiper-slide"
v-for="(carousel,index) in bannerList" :key="carousel.id"
>
<img :src="carousel.imgUrl"/>
</div>
第三步:new Swiper实例,给轮播图添加动态效果
new Swiper(document.querySelector('.swiper-container'),{
loop:true,
pagination:{
el:'.swiper-pagination',
//点击小球的时候也切换图片
clickable:true
},
//前进后退的按钮
navigation:{
nextEl:".swiper-button-next",
prevEl:".swiper-button-prev"
}
})
安装swiper5版本比较稳定
4.7 Banner轮播图的实现
首先需要明确,如果直接将swiper实例写在mounted函数中,尽管mounted是在页面结构加载完成后执行,但此时从服务器拿到的数据还没有放到仓库中,故swiper实例中拿不到bannerList的数据
解决方式一:
可以通过setTimeout等待一段时间,这段时间里从服务器拿到的数据就已经放到仓库中了
setTimeout({
//swiper实例的实现
},2000)
解决方式二:
watch+nextTick
watch:数据监听,监听已有数据的变化
监听bannerList数据的变化:从空数组变为数组里有四个元素
如果执行handler方法,代表组件实例身上的这个属性的属性值已经有了
但是仅有watch是不够的,因为数据更新了,但是页面渲染可能还没结束
nextTick:
Vue.nextTick([callback,context])
用法:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立刻使用这个方法,获取更新后的DOM
$nextTick可以保证页面中的结构一定是有的,经常和很多需要DOM已经存在才能实现功能的插件一起使用
watch+nextTick实现功能:
watch:{
bannerList:{
handler(newValue,oldValue){
this.$nextTick(()=>{
var mySwiper=new Swiper(document.querySelector('.swiper-container'),{
loop:true,
pagination:{
el:'.swiper-pagination',
//点击小球的时候也切换图片
clickable:true
},
//前进后退的按钮
navigation:{
nextEl:".swiper-button-next",
prevEl:".swiper-button-prev"
}
})
})
}
}
}
4.8 通过ref获取要操作的DOM
<div class="swiper-container" ref="mySwiper">
new Swiper(this.$refs.mySwiper,{
day5
5.1 开发floor组件
由于Floor组件在Home中被调用两次,如果是在Floor组件中触发getFloorList,很难实现两次调用数据不同
故应该在Home组件中触发
v-for可以在自定义组件上使用
组件间通信的方式有哪些?
props:用于父子组件通信
自定义事件:@on 与 @emit
全局事件总线:$bus 全能
pubsub-js:vue中几乎不用(react使用的较多)
插槽
vuex
List组件中的轮播图,是当前组件内部发送请求、动态渲染解构服务器返回的数据,因此必须使用watch+nextTick
Floor组件中的轮播图,请求由父组件Home发送,并且数据是父组件发送过来的,此时结构已经完全解析好了,所以可以在mounted里写轮播图实例
mounted(){
new Swiper(this.$refs.cur,{
loop:true,
pagination:{
el:'.swiper-pagination',
//点击小球的时候也切换图片
clickable:true
},
//前进后退的按钮
navigation:{
nextEl:".swiper-button-next",
prevEl:".swiper-button-prev"
}
})
}
5.2 共用组件Carousel
切记:以后在开发项目的时候,如果看到某一个组件在很多地方都使用,可以把它变成全局组件
<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.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(()=>{
new Swiper(this.$refs.cur,{
loop:true,
autoplay:true,
pagination:{
el:'.swiper-pagination',
//点击小球的时候也切换图片
clickable:true
},
//前进后退的按钮
navigation:{
nextEl:".swiper-button-next",
prevEl:".swiper-button-prev"
}
})
})
}
}
}
}
</script>
<style scoped>
</style>
5.3 search仓库(主讲getters)
getters,相当于计算属性
项目中getters的主要作用:简化仓库中的数据(简化数据而生)
可以把我们将来在组件当中需要用的数据简化一下【将来组件获取数据就简单多了】
数据的返回:
以goodsList为例,如果服务器数据回来了,返回一个数组
如果网络不给力/没有网,state.searchList.goodsList返回的就是undefined
const getters={
goodsList(state){
return state.searchList.goodsList||[]
},
trademarkList(state){
return state.searchList.trademarkList||[]
},
attrsList(state){
return state.searchList.attrsList||[]
}
}
与getters对应的是mapGetters,此函数接收一个数组,不像state那样划分模块
computed:{
...mapGetters(['goodsList'])
}
5.4 Search中的数据处理
使用Object.assign方法,快速合并数据
在beforeMount里整理数据,在mounted发送请求
export default {
name: 'Search',
components: {
SearchSelector
},
data(){
return {
//带给服务器的参数
searchParams:{
category1Id:"",
category2Id:"",
category3Id:"",
categoryName:"",
keyword:"",
order:"",
pageNo:1,
pageSize:3,
props:[],
trademark:""
}
}
},
beforeMount(){
Object.assign(this.searchParams,this.$route.query,this.$route.params)
},
mounted(){
this.getData()
},
methods:{
getData(){
this.$store.dispatch('getSearchList',this.searchParams)
}
},
computed:{
...mapGetters(['goodsList'])
}
}
5.5 监听路由变化再次发送请求数据
当点击三级联动组件或者搜索框时,路径中的query或params会发生改变。那么路由就会改变,监听路由即可。
每一次请求完毕,应该把相应的1/2/3级分类的id只看,让它接收下一次相应的1/2/3级分类
分类名字和关键字不用清理:因为每一次路由发生变化,都会赋予它新的数据
<!-- 分类的面包屑-->
<li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}} <i @click="removeCategoryName">x</i></li>
watch:{
$route(newValue,oldValue){
Object.assign(this.searchParams,this.$route.query,this.$route.params)
this.getData()
this.searchParams.category1Id=''
this.searchParams.category2Id=''
this.searchParams.category3Id=''
}
}
5.6 动态删除面包屑
即使带给服务器的参数是为空的字符串,仍然会把相应的字段带给服务器
但如果把相应的字段变为undefined,当前这个字段就不会带给服务器了(性能优化)
骚操作:
对自己所在组件进行路由跳转,可以清除地址栏的指定参数(比如query参数)
//删除分类的名字
removeCategoryName(){
//把带给服务器的参数置空,并给服务器发请求
this.searchParams.categoryName=undefined
this.searchParams.category1Id=undefined
this.searchParams.category2Id=undefined
this.searchParams.category3Id=undefined
this.getData()
this.$router.push({name:'search',params:this.$route.params})
}
5.6 动态开发面包屑中的分类名
<!-- 关键字的面包屑-->
<li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}} <i @click="removeQueryName">x</i></li>
removeQueryName(){
this.searchParams.keyword=undefined
this.getData()
}
5.7 动态开发面包屑中的关键字
当面包屑中的关键字清除以后,需要让兄弟组件Header组件中的关键字清除
设计组件间通信:
props:父子
自定义事件:子父
vuex:仓库数据
插槽:父子
$bus:全局事件总线
注册全局事件总线:
new Vue({
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus=this
},
router,
store,
}).$mount('#app')
在Search组件中触发事件:
this.$bus.$emit('clear')
在Header组件中绑定事件:
mounted(){
this.$bus.$on("clear",()=>{
this.keyword=""
})
}
更新params参数:(Search组件)
this.$router.push({name:'search',query:this.$route.query})
5.8 面包屑处理品牌信息
在Search组件中展示品牌的面包屑:
注意trademark.tmName的展示方式
<!-- 品牌的面包屑-->
<li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(":")[1]}} <i @click="removeTrademark">x</i></li>
子父组件间通信:
父传子自定义事件:
<!--selector-->
<SearchSelector @trademarkInfo="trademarkInfo"/>
由于传递过来的trademark是个对象,而传递给服务器的trademark是个字符串,所以要进行字符串拼接
trademarkInfo(trademark){
console.log(trademark)
this.searchParams.trademark=`${trademark.tmId}:${trademark.tmName}`
this.getData()
}
子触发父中的函数:
trademarkHandler(trademark){
this.$emit('trademarkInfo',trademark)
}
5.9 平台售卖属性的操作
父组件:
<!-- 平台售卖的属性值展示-->
<li class="with-x" v-for="(attrValue,index) in searchParams.props" :key="index">{{attrValue.split(":")[1]}} <i @click="removeAttr(index)">x</i> </li>
</ul>
attrInfo(attr,attrValue){
let props=`${attr.attrId}:${attrValue}:${attr.attrName}`
//数组去重
if(this.searchParams.props.indexOf(props)===-1)
this.searchParams.props.push(props)
},
removeAttr(index){
//再次整理参数
this.searchParams.props.splice(index,1)
//再次发送请求
this.getData()
}
父子组件间通信:
<!--selector-->
<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo"/>
attrInfo(attr,attrValue){
this.$emit("attrInfo",attr,attrValue)
}
day6
6.1 排序操作(上)
排序方式:
1:综合
2:价格
asc:升序
desc:降序
①order属性的属性值最多有4种写法:
1:asc
1:desc
2:asc
2:desc
//排序的初始状态:综合:降序
order:"1:desc",
②综合和价格谁应该具有类名active?
通过order属性值当中是包含1还是包含2来判断
<ul class="sui-nav">
<li :class="{active:searchParams.order.indexOf('1')!==-1}">
<a href="#">综合</a>
</li>
<li :class="{active:searchParams.order.indexOf('2')!==-1}">
<a href="#">价格</a>
</li>
</ul>
由于模板中不建议使用太长的表达式语句,可以用计算属性书写:
<ul class="sui-nav">
<li :class="{active:isOne}">
<a href="#">综合</a>
</li>
<li :class="{active:isTwo}">
<a href="#">价格</a>
</li>
</ul>
computed:{
...mapGetters(['goodsList']),
isOne(){
return this.searchParams.order.indexOf('1')!==-1
},
isTwo(){
return this.searchParams.or.indexOf('2')!==-1
}
}
③综合和价格,谁应该有箭头?
谁有类名active,谁就有箭头
④箭头用什么制作?
阿里图标库iconfont
在复制的地址前加 https:
放入index.html静态页面即可使用
<link rel="stylesheet" href="https://at.alicdn.com/t/c/font_3941403_5qqhi0nusus.css">
图标的基本使用:
<ul class="sui-nav">
<li :class="{active:isOne}">
<a href="#">综合 <span v-show="isOne" class="iconfont icon-up-arrow"></span></a>
</li>
<li :class="{active:isTwo}">
<a href="#">价格 <span v-show="isTwo" class="iconfont icon-arrow_down"></span></a>
</li>
</ul>
通过计算属性动态决定箭头上下:
isAsc(){
return this.searchParams.order.indexOf("asc")!==-1
},
isDesc(){
return this.searchParams.order.indexOf("desc")!==-1
}
<ul class="sui-nav">
<li :class="{active:isOne}">
<a href="#">综合 <span v-show="isOne" class="iconfont" :class="{'icon-up-arrow':isAsc,'icon-arrow_down':isDesc}"></span></a>
</li>
<li :class="{active:isTwo}">
<a href="#">价格 <span v-show="isTwo" class="iconfont" :class="{'icon-up-arrow':isAsc,'icon-arrow_down':isDesc}"></span></a>
</li>
</ul>
6.2 排序操作(下)
设定改变上下箭头的规则:
changeOrder(flag){
let originFlag=this.searchParams.order.split(":")[0]
let originSort=this.searchParams.order.split(":")[1]
let newOrder=''
if(flag===originFlag){
originSort=originSort==="desc"?"asc":"desc"
newOrder=`${originFlag}:${originSort}`
}else{
newOrder=`${flag}:desc`
}
this.searchParams.order=newOrder
this.getData()
}
大家注意newOrder=`${flag}:desc`中的冒号一定要用英文字符,否则切换失败!
day7
7.1 分页功能分析
为什么很多项目采用分页功能?
因为比如电商平台同时展示的数据有很多(1w+),需要采用分页功能一次加载少量数据,防止卡顿
分页器展示,需要哪些数据(条件)?
需要知道当前是第几页:pageNo字段代表当前页数
需要知道每一页需要展示多少条数据:pageSize字段进行代表
需要知道整个分页器一共有多少条数据:total字段进行代表---【获取另外一条信息:总共有多少页】
需要知道分页器连续页面个数:5|7(一般是奇数,对称比较好看)
7.2 分页器起始与结束数字计算
①总页数小于连续页数
仅展示总页数即可
②总页数大于/等于连续页数
根据当前页算起始、结束页码:
i.起始页码小于1,此时置起始页码为1,结束页码为连续页码数
ii.结束页码大于总页码,此时置结束页码为总页码,起始页码为:总页码-连续页码+1
iii.正常情况下,直接计算
computed:{
totalPage(){
return Math.ceil(this.total/this.pageSize)
},
startNumAndEndNum(){
let start=1,end=1;
//不正常现象:总页数没有连续页码数多
if(this.continues>this.totalPage){
end=this.totalPage
}else{
start=this.pageNo-parseInt(this.continues)/2
end=this.pageNo+parseInt(this.continues)/2
if(start<1){
start=1
end=this.continues
}
if(end>this.totalPage){
start=end-this.continues+1
end=this.totalPage
}
}
return {start,end}
}
7.3 分页器的动态展示
v-for也可以用于循环数字。指定要循环的数字,比如说10
则v-for会遍历0~10
如果希望从指定的数字开始遍历,可以用v-if加以限制
<template>
<div class="fr page">
<div class="sui-pagination clearfix">
<ul>
<li class="prev disabled">
<a href="#">«上一页</a>
</li>
<li class="active" v-if="startNumAndEndNum.start>1">
<a href="#">1</a>
</li>
<li>
<a href="#" v-if="startNumAndEndNum.start>2">...</a>
</li>
<li v-for="(page,index) in startNumAndEndNum.end" :key="index" v-if="page>=startNumAndEndNum.start">
<a href="#">{{page}}</a>
</li>
<li class="dotted" v-if="startNumAndEndNum.end<(totalPage-1)"><span>...</span></li>
<li>
<a href="#" v-if="startNumAndEndNum.end<totalPage">{{totalPage}}</a>
</li>
<li>
<a href="#">{{totalPage}}</a>
</li>
<li class="next">
<a href="#">下一页»</a>
</li>
</ul>
<div><span>共{{total}}条 </span></div>
</div>
</div>
</template>
7.4 分页器的完成
<template>
<div class="fr page">
<div class="sui-pagination clearfix">
<ul>
<li class="prev" :disabled="pageNo===1" @click="$emit('getPageNo',pageNo-1)">
<a>«上一页</a>
</li>
<li class="active" v-if="startNumAndEndNum.start>1" @click="$emit('getPageNo',1)">
<a>1</a>
</li>
<li>
<a v-if="startNumAndEndNum.start>2">...</a>
</li>
<li
v-for="(page,index) in startNumAndEndNum" :key="index"
v-if="page>=startNumAndEndNum.start"
@click="$emit('getPageNo',page)"
>
<a>{{page}}</a>
</li>
<li class="dotted" v-if="startNumAndEndNum.end<(totalPage-1)"><span>...</span></li>
<li>
<a
v-if="startNumAndEndNum.end<totalPage"
@click="$emit('getPageNo',totalPage)"
>{{totalPage}}</a>
</li>
<li>
<a>{{totalPage}}</a>
</li>
<li class="next" :disabled="pageNo===totalPage" @click="$emit('getPageNo',pageNo+1)">
<a>下一页»</a>
</li>
</ul>
<div><span>共{{total}}条 </span></div>
</div>
</div>
</template>
7.5 分页器类名的添加
:class="{active:pageNo===page}"
7.6 滚动行为
开发某一个产品的详情页面?
1.静态组件
2.发请求
3.vuex
4.动态展示组件
当点击商品的图片的时候,跳转到详情页面,在路由跳转的时候需要带上产品的ID给详情页面
detail路由:
{
path:'/detail/:id',
component:Detail,
meta:{isShow:true}
},
路由跳转:
<router-link :to="`/detail/${good.id}`">
<img :src="good.defaultImg" />
</router-link>
滚动行为:
使用前端路由,当切换到新的路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。vue-router能做到,且做得更好,他可以让你自定义路由切换时页面如何滚动
注意:这个功能只在支持history.pushState的浏览器中可用
export default new VueRouter({
routes,
scrollBehavior(to,from,savePosition){
//返回的这个y=0,代表页面在最上面
return {y:0}
}
})
7.7 产品详情数据获取
获取数据的接口:
//获取产品详细信息的接口 URL:/api/item/{skuId} 请求方式:get
export const reqGoodsInfo=(skuId)=>requests({
url:`/item/${skuId}`,
method:'get'
})
vuex仓库捞数据:
import {reqGoodsInfo} from "@/api";
const state={
goodsInfo:{}
}
const mutations={
GETGOODSINFO(state,goodsInfo){
state.goodsInfo=goodsInfo
}
}
const actions={
//获取产品信息的action
async getGoodsInfo({commit},skuId){
let result=await reqGoodsInfo(skuId)
if(result.code===200){
commit(GETGOODSINFO(result.data))
}
}
}
const getters={}
export default {
state,mutations,actions,getters
}
7.8 产品详情展示动态数据
报错,但不影响程序运行
这是因为仓库中的getters方法:
state.goodsInfo初始状态为空对象,空对象的categoryView属性值为undefined。undefined.category1Id肯定会报错。
const getters={
categoryView(state){
return state.goodsInfo.categoryView
}
}
可以改为:
那么此时计算出来的categoryView属性值至少是一个空对象,假的报错就不会有了
const getters={
categoryView(state){
return state.goodsInfo.categoryView||{}
}
}
Detail组件和Zoom组件通信的时候,通用会报错,可以修改为:
skuImageList(){
return this.skuInfo.skuImageList||[{}]
}
day8
8.1 产品售卖属性值排他操作
先通过遍历父数组将所有子元素的isChecked取消
再将点中的子元素isChecked点亮
changeActive(spuSaleAttrValue,spuSaleAttrValueList){
spuSaleAttrValueList.forEach(item=>{
item.isChecked='0'
})
spuSaleAttrValue.isChecked='1'
}
8.2 Zoom和ImgList兄弟组件间通信
通过ImgList传递给Zoom的index,使得轮播图中的图片改变导致Zoom中展示的大图一起改变(联动效应)
ImgList:
changeCurrentIndex(index){
this.currentIndex=index
this.$bus.$emit('getIndex',this.currentIndex)
}
Zoom:
data(){
return {
currentIndex:0
}
},
computed:{
imgObj(){
return this.skuImageList[this.currentIndex]||{}
}
},
mounted(){
this.$bus.$on('getIndex',(index)=>{
this.currentIndex=index
})
}
8.3 轮播图实例的完成
注意没见过的新属性slidesPereView和slidesPerGroup
watch:{
//监听数据:虽然可以保证skuImageList数据已经传递过来,但v-for未必已经遍历完成
skuImageList(newValue,oldValue){
this.$nextTick(()=>{
new Swiper(".swiper-container",{
navigation:{
nextEl:".swiper-button-next",
prevEl:".swiper-button-prev"
},
//显示一次显示3个图片
slidesPerView:3,
//设置一次切换1张图片
slidesPerGroup:1
})
})
}
}
8.4 放大镜的实现
遮罩层方法剖析:
big是用来显示mask滑过的区域,mask向右的时候,big对应的图片应该向左,大小为两倍差
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
else if(left>=mask.offsetWidth) left=mask.offsetWidth
if(top<=0) top=0
else if(top>=mask.offsetHeight) top=mask.offsetHeight
//修改元素的left|top
mask.style.left=left+'px'
mask.style.top=top+'px'
big.style.left=-2*left+'px'
big.style.top=-2*top+'px'
}
}
总代码:
<template>
<div class="spec-preview">
<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>
</div>
</template>
<script>
export default {
name: "Zoom",
props:['skuImageList'],
data(){
return {
currentIndex:0
}
},
computed:{
imgObj(){
return this.skuImageList[this.currentIndex]||{}
}
},
mounted(){
this.$bus.$on('getIndex',(index)=>{
this.currentIndex=index
})
},
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
else if(left>=mask.offsetWidth) left=mask.offsetWidth
if(top<=0) top=0
else if(top>=mask.offsetHeight) top=mask.offsetHeight
//修改元素的left|top
mask.style.left=left+'px'
mask.style.top=top+'px'
big.style.left=-2*left+'px'
big.style.top=-2*top+'px'
}
}
}
</script>
<style lang="less">
.spec-preview {
position: relative;
width: 400px;
height: 400px;
border: 1px solid #ccc;
img {
width: 100%;
height: 100%;
}
.event {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 998;
}
.mask {
width: 50%;
height: 50%;
background-color: rgba(0, 255, 0, 0.3);
position: absolute;
left: 0;
top: 0;
display: none;
}
.big {
width: 100%;
height: 100%;
position: absolute;
top: -1px;
left: 100%;
border: 1px solid #aaa;
overflow: hidden;
z-index: 998;
display: none;
background: white;
img {
width: 200%;
max-width: 200%;
height: 200%;
position: absolute;
left: 0;
top: 0;
}
}
.event:hover~.mask,
.event:hover~.big {
display: block;
}
}
</style>
8.5 购买产品个数的操作
//表单元素修改产品个数
changeSkuNum(event){
//用户输入进来的文本*1
let value=event.target.value*1
//如果用户输入进来的非法
if(isNaN(value)||value<1){
this.skuNum=1
}else{
this.skuNum=parseInt(value)
}
}
8.6 加入购物车
接口api:
血的教训,千万不要把method写成methods!!!!
export const reqAddOrUpdateShopCart=(skuId,skuNum)=>requests({
url:`/cart/addToCart/${skuId}/${skuNum}`,
method:"post"
})
加入购物车成功的路由:
{
name:'addcartsuccess',
path:'/addcartsuccess',
component:AddCartSuccess,
meta:{isShow:true}
}
仓库中的action:
//将产品添加到购物车中
async addOrUpdateShopCart({commit},{skuId,skuNum}){
//服务器写入数据成功以后,并没有返回其他的数据,只是返回了code=200,代表此处操作成功
let result= await reqAddOrUpdateShopCart(skuId,skuNum)
if(result.code==200){
return 'ok'
}else{
return Promise.reject(new Error('fail'))
}
}
浏览器存储功能:
本地存储:持久化的——5M
会话存储:并非持久——会话结束就消失
路由传递参数结合会话存储:
进行路由跳转的时候需要将产品信息带给下一级的路由组件
一些简单的数据skuNum,通过query形式给路由组件传递过去
复杂的产品信息数据(比如skuInfo),通过会话存储即可(不持久化,会话结束数据消失)
async addShopCart(){
//1.发请求——将产品加入到数据库(通知服务器)
try {
await this.$store.dispatch('addOrUpdateShopCart', {
skuId: this.$route.params.skuId,
skuNum: this.skuNum})
//2.服务器存储成功——进行路由跳转传递参数
sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))
this.$router.push({
name: 'addcartsuccess',
query:{
skuNum:this.skuNum
}
})
}catch(err) {
//3.失败——给用户进行提示
alert(err.message)
}
}
AddCartSuccess组件中使用到sessionStorage存储的item:
computed:{
skuInfo(){
return JSON.parse(sessionStorage.getItem('SKUINFO'))
}
}
day9
9.1 购物车静态组件与修改
购物车静态组件——需要修改样式结构
1.调整css让各个项目对齐
2.向服务器发送ajax,获取购物车数据
3.UUID临时游客身份
4.动态展示购物车
9.2 uuid游客身份获取购物车数据
获取购物车数据的接口:
//获取购物车列表数据接口
export const reqCartList=()=>requests({
url:"/cart/cartList",
method:"get"
})
向服务器发送ajax请求获取购物车数据:
发请求的时候无法获取你购物车里面的数据,因为服务器不知道你是谁
可以使用UUID临时游客身份
uuid函数逻辑:
随机生成一个字符串,且每次执行不能发生变化,游客身份持久存储
①从本地存储中获取uuid
②如果没有
i.生成游客临时身份
ii.本地存储
一定要有返回值!!!
import {v4 as uuidv4} from 'uuid'
export const getUUID=()=>{
let uuid_token=localStorage.getItem("UUIDTOKEN")
if(!uuid_token){
uuid_token=uuidv4()
localStorage.setItem('UUIDTOKEN',uuid_token)
}
return uuid_token
}
在请求拦截器中将存储好的uuid发往后台:
//请求拦截器
requests.interceptors.request.use((config)=>{
if(store.state.detail.uuid_token){
config.headers.userTempId=store.state.detail.uuid_token
}
//config是headers的请求头
nprogress.start()
return config
})
9.3 修改购物车产品的数量完成
<a class="mins" @click="handler('minus',-1,cart)">-</a>
<input
autocomplete="off"
type="text"
:value="cart.skuNum"
minnum="1"
class="itxt"
@change="handler('change',$event.target.value*1,cart)"
>
<a class="plus" @click="handler('add',1,cart)">+</a>
//修改某一个产品的个数
async handler(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
}
}
try {
await this.$store.dispatch('addOrUpdateShopCart', {
skuId: cart.skuId,
skuNum: disNum
})
this.getData()
}catch(error){
alert('修改失败')
}
}
特注:因为改变数量的时候如果点击过快可能会发生意想不到的结果,所以可以进行节流处理
//修改某一个产品的个数
handler:throttle(async function(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
}
}
try {
await this.$store.dispatch('addOrUpdateShopCart', {
skuId: cart.skuId,
skuNum: disNum
})
this.getData()
}catch(error){
alert('修改失败')
}
})
},
9.4 删除购物车产品的操作
接口:
//删除购物产品的接口
export const reqDeleteCartById=(skuId)=>requests({
url:`/cart/deleteCart/${skuId}`,
method:"delete"
})
具体的删除操作:
//删除某一个产品的操作
async deleteCartById(cart){
try{
await this.$store.dispatch('deleteCartListBySkuId',cart.skuId)
//如果删除成功,再次发请求获取新的数据展示
this.getData()
}catch(error){
alert(error.message)
}
}
9.5 修改产品状态
接口:
//修改商品的选中状态
export const reqUpdateCheckedById=(skuId,isChecked)=>requests({
url:`/cart/checkCart/${skuId}/${isChecked}`,
method:"get"
})
仓库:
//修改购物车某一个产品的选中状态
async updateCheckedById({commit},{skuId,isChecked}){
let result = await reqUpdateCheckedById(skuId,isChecked)
if(result.code===200){
return 'ok'
}else{
return Promise.reject(new Error('fail'))
}
}
修改template模板内容:
<input
type="checkbox"
name="chk_list"
:checked="cart.isChecked==1"
@change="updateChecked(cart,$event)"
>
修改script脚本内容:
async updateChecked(cart,event){
try{
let checked=event.target.checked?"1":"0"
await this.$store.dispatch('updateCheckedById',{skuId:cart.skuId,isChecked:checked})
this.getData()
}catch(error){
}
}
9.6 复习
1.加入购物车
UUID:点击加入购物车的时候,通过请求头给服务器带临时身份给服务器,存储某一个用户购物车数据
会话存储:去存储产品的信息以及展示功能
2.购物车功能
修改产品的数量
删除某一个产品的接口
某一个产品的勾选状态切换
day10
10.1 删除全部选中的商品
context:小仓库,包含commit(提交mutations修改state)、getters(计算属性)、dispatch(派发action)、state(当前仓库数据)
//删除全部勾选的产品
deleteAllCheckedCart(context) {
}
Promise.all([p1,p2,p3])
p1|p2|p3:每一个都是Promise对象,如果有一个Promise失败,都失败;如果都成功,返回成功
删除全部选中的商品
action逻辑:
//删除全部勾选的产品
deleteAllCheckedCart({dispatch,getters}) {
let promiseAll=[]
//获取购物车中的全部产品
getters.cartList.cartInfoList.forEach(item=>{
let promise=item.isChecked===1?dispatch("deleteCartListBySkuId",item.skuId):''
promiseAll.push(promise)
})
return Promise.all(promiseAll)
}
组件里的逻辑:
async deleteAllCheckedCart(){
try{
await this.$store.dispatch("deleteAllCheckedCart")
this.getData()
}catch(error){
alert(error.message)
}
}
10.2 全部商品的勾选状态修改
actions里的逻辑:
updateAllCartIsChecked({dispatch,state},isChecked){
let promiseAll=[]
state.cartList[0].cartInfoList.forEach(item=>{
let promise=dispatch("updateCheckedById",{
skuId:item.skuId,
isChecked:isChecked
})
promiseAll.push(promise)
return Promise.all(promiseAll)
})
}
组件里的逻辑:
//修改全部产品选中的状态
async updateAllCartChecked(event){
try{
let isChecked=event.target.checked?"1":"0"
await this.$store.dispatch("updateAllCartIsChecked",isChecked)
this.getData()
}catch(error){
alert(error.message)
}
}
组件的input全选框判断:
<div class="select-all">
<input class="chooseAll"
type="checkbox"
:checked="isAllChecked&&cartInfoList.length>0"
@change="updateAllCartChecked($event)"
>
<span>全选</span>
</div>
10.3 注册业务
assets文件夹打包以后,整个项目在dist目录下,assets文件夹会消失
assets文件夹——放置全部组件共用的静态资源
在样式中也可以使用@符号:
url中使用@代表src路径:
background-image: url(~@/assets/images/icons.png);
接口api:
//获取验证码
export const reqGetCode=(phone)=>requests({
url:`/user/passport/sendCode/${phone}`,
method:"get"
})
仓库:
import {reqGetCode} from "@/api";
const state={
code:''
}
const mutations={
GETCODE(state,code){
state.code=code
}
}
const actions={
//获取验证码
async getCode({commit},phone){
let result=await reqGetCode(phone)
if(result.code===200){
commit("GETCODE",result.data)
}else{
return Promise.reject(new Error('fail'))
}
}
}
const getters={}
export default {
state,mutations,actions,getters
}
发送请求到仓库:
<button style="width:100px;height:38px;" @click="$store.dispatch('getCode',phone)">获取验证码</button>
或:
async getCode(){
try{
this.phone&&(await this.$store.dispatch("getCode",this.phone))
console.log(this.$store.state.user.code)
}catch(error){
alert(error.message)
}
}
注册成功后跳转到登录页面:
接口api:
//注册
export const reqUserRegister=(data)=>requests({
url:`/user/passport/register`,
data,
method:"post"
})
仓库里的actions逻辑:
//用户注册
async userRegister({commit},user){
let result=await reqUserRegister(user)
if(result.code===200){
return 'ok'
}else{
return Promise.reject(new Error('fail'))
}
}
组件里的逻辑:
async userRegister(){
try{
const {phone,code,password,rePassword}=this
phone&&code&&password===rePassword&&(await this.$store.dispatch("userRegister",{phone,code,password}))
this.$router.push("/login")
}catch(error) {
alert(error.message)
}
}
10.4 登录业务
登录业务:
注册:通过数据库存储用户信息(名字、密码)
登录:登录成功的时候,后台为了区分你这个用户是谁,会让服务器下发token(令牌:唯一标识符)
登陆接口:一般登陆成功后服务器会下发token,前台持久化存储token,然后前台带着token 去找服务器登录
接口api:
//登录
export const reqUserLogin=(data)=>requests({
url:"/user/passport/login",
data,
method:"post"
})
组件逻辑:
async userLogin(){
try{
const {phone,password}=this
(phone&&password)&&(await this.$store.dispatch("userLogin",{phone,password}))
this.$router.push("/home")
}catch(error){
alert(error.message)
}
}
仓库逻辑:
actions:
//登录业务
async userLogin({commit},data){
let result=await reqUserLogin(data)
if(result.code===200){
commit("USERLOGIN",result.data.token)
return 'ok'
}else{
return Promise.reject(new Error('fail'))
}
}
mutations:
USERLOGIN(state,token){
state.token=token
}
前台携带token获取用户信息:
export const reqUserInfo=()=>requests.get("/user/passport/auth/getUserInfo")
处理请求拦截器:
//请求拦截器
requests.interceptors.request.use((config)=>{
if(store.state.detail.uuid_token){
config.headers.userTempId=store.state.detail.uuid_token
}
if(store.state.user.token){
config.headers.token=store.state.user.token
}
//config是headers的请求头
nprogress.start()
return config
})
获取服务器返回的用户信息:
async getUserInfo({commit}){
let result=await reqUserInfo()
if(result.code===200){
commit("GETUSERINFO",result.data)
return 'ok'
}else{
return Promise.reject(new Error('fail'))
}
}
GETUSERINFO(state,userInfo){
state.userInfo=userInfo
}
处理Header组件:
<div class="loginList">
<p>尚品汇欢迎您!</p>
<p v-if="!userName">
<span>请</span>
<router-link to="/login">登录</router-link>
<router-link to="/register" class="register">免费注册</router-link>
</p>
<p v-else>
<a>{{userName}}</a>
<a class="register">退出登录</a>
</p>
</div>
vuex仓库存储的数据并不持久化,一刷新数据就没了。可以通过本地存储持久化token:
//登录业务
async userLogin({commit},data){
let result=await reqUserLogin(data)
if(result.code===200){
commit("USERLOGIN",result.data.token)
localStorage.setItem("TOKEN",result.data.token)
return 'ok'
}else{
return Promise.reject(new Error('fail'))
}
}
const state={
code:'',
token:localStorage.getItem("TOKEN"),
userInfo:{}
}
目前存在的bug:
①多个组件展示用户信息需要在每一个组件的mounted中触发this.$store.dispatch("getUserInfo")
②用户已经登陆,就不能再跳转到登录页
10.5 退出登录
退出登录需要做的事:
①需要发送请求,通知服务器退出登录(清除一些数据,比如token)
②清除项目当中的数据
接口api:
export const reqLogout=()=>requests.get("/user/passport/logout")
清除记录的数据:
async userLogout({commit}){
let result=await reqLogout()
if(result.code===200){
commit("CLEAR")
return "ok"
}else{
return Promise.reject(new Error("fail"))
}
}
CLEAR(state){
state.token=""
state.userInfo={}
localStorage.clear()
}
派发action:
async logout(){
try{
await this.$store.dispatch("userLogout")
this.$router.push("/home")
}catch(error){
alert(error.message)
}
}
10.6 导航守卫用户登录操作
导航:表示路由正在发生变化,进行路由跳转
导航守卫可以简单地分为全局守卫、路由独享守卫、组件内守卫
全局前置守卫:
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫resolve完之前一直处于等待中
参数剖析:
to:可以获取到你要跳转到的那个路由的信息
from:可以获取到你从哪个路由而来的信息
next:放行函数
next()放行
next("/login")放行到指定的路由中
next(false)中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的地址
router.beforeEach((to, from, next)=>{
})
全局路由守卫逻辑:
1.如果已经登录
i.想去的是login路由,跳转到主页
ii.想去的不是login路由
①如果有用户名,直接放行(不能用userInfo判断,因为空对象为真值)
②如果没有用户名,可能是刷新导致仓库的数据消失了,再去捞一下数据,然后放行
如果捞不到数据,就直接退出登录,回到登录界面
2.如果没有登陆
直接放行(此时还没有做处理,后续可能会有追加)
router.beforeEach(async (to, from, next)=>{
let token=store.state.user.token
let name=store.state.user.userInfo.name
if(token) {
//登陆了订单还想去login或register(不能去,停留在首页)
if (to.path === '/login'||to.path === '/register') {
next("/home")
} else {
//登陆了,去的不是login
//如果用户信息
if (name) {
next()
} else {
try {
//没有用户信息,派发action让仓库存储用户信息再跳转
await store.dispatch("getUserInfo")
next()
} catch (error) {
//token失效,获取不到用户信息,需要重新登陆
//清除token
await store.dispatch("userLogout")
next("/login")
}
}
}
}else{
next()
}
})
10.7 获取交易页数据及展示
api接口:
//获取用户地址信息
export const reqAddressInfo=()=>requests({
url:"/user/userAddress/auth/findUserAddressList",
method:"GET"
})
//获取商品清单
export const reqOrderInfo=()=>requests({
url:"/order/auth/trade",
method:"GET"
})
仓库:
import {reqAddressInfo, reqOrderInfo} from "@/api";
const state={
address:[],
orderInfo:{}
}
const mutations={
GETUSERADDRESS(state,address){
state.address=address
},
GETORDERINFO(state,orderInfo){
state.orderInfo=orderInfo
}
}
const actions={
async getUserAddress({commit}){
let result = await reqAddressInfo()
if(result.code===200){
commit("GETUSERADDRESS",result.data)
}
},
async getOrderInfo({commit}){
let result = await reqOrderInfo()
if(result.code===200){
commit("GETORDERINFO",result.data)
}
}
}
const getters={}
export default {
state,mutations,actions,getters
}
组件逻辑:
注意mapState和mapGetters的区别
<script>
import {mapState} from "vuex";
export default {
name: 'Trade',
data(){
return {
msg:""
}
},
mounted(){
this.$store.dispatch("getUserAddress"),
this.$store.dispatch("getOrderInfo")
},
computed:{
...mapState({
addressInfo:state=>state.trade.address,
orderInfo:state=>state.trade.orderInfo
}),
//将来提交订单最终选中地址
userDefaultAddress(){
return this.addressInfo.find(item=>item.isDefault==1)||{}
},
detailArrayList(){
return this.orderInfo.detailArrayList||[]
}
},
methods:{
changeDefault(address,addressInfo){
//find:查找数组当中符合条件的元素作为返回值返回
addressInfo.forEach(item=>item.isDefault=0)
address.isDefault=1
}
}
}
</script>
day11
11.1 提交订单
①静态组件
②点击提交订单的按钮时,还需要向服务器发送一次请求,把一些支付的相关信息传递给服务器
接口:
export const reqSubmitOrder=(tradeNo,data)=>requests({
url:`/order/auth/submitOrder?tradeNo=${tradeNo}`,
data,
method:"post"
})
统一接口api文件夹里面全部的请求函数:
import * as API from "@/api"
new Vue({
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus=this
Vue.prototype.$API=API
},
router,
store,
}).$mount('#app')
发送请求到支付页面:
async submitOrder(){
let {tradeNo}=this;
let data={
consignee:this.userDefaultAddress.consignee,
consigneeTel:this.userDefaultAddress.phoneNum,
deliveryAddress:this.userDefaultAddress.fullAddress,
paymentWay:"ONLINE",
orderComment:this.msg,
orderDetailList:this.detailArrayList
}
let result=await this.$API.reqSubmitOrder(tradeNo,data)
console.log(result)
if(result.code===200){
this.orderId=result.data
this.$router.push(`/pay?orderId=${this.orderId}`)
}else{
alert(result.data)
}
}
11.2 获取订单号与展示支付信息
接口:
//获取支付的信息
export const reqPayInfo=(orderId)=>requests({
url:`/payment/weixin/createNative/${orderId}`,
method:"get"
})
生命周期函数中不能使用async
<script>
export default {
name: 'Pay',
data(){
return {
payInfo:{}
}
},
computed:{
orderId(){
return this.$route.query.orderId
}
},
mounted(){
this.getPayInfo()
},
methods:{
async getPayInfo(){
let result=await this.$API.reqPayInfo(this.orderId)
if(result.code===200){
this.payInfo=result.data
}
}
}
}
</script>
11.3 支付页面中使用ElementUI以及按需引入
//注册全局组件
import {Button,MessageBox} from 'element-ui'
//ElementUI注册组件的时候还有一种写法:挂在原型上
Vue.use(Button)
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
ElementUI按需引入:
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的
①安装 babel-plugin-component
yarn add babel-plugin-component -D
②将babel.config.js修改为:
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
"plugins":[
[
"component",
{
libraryName:'element-ui',
"StyleLibraryName":'theme-chalk'
}
]
]
}
直接使用:
<el-button class="btn" @click="open">立即支付</el-button>
//弹出框
open(){
this.$alert(`<strong>这是 <i>HTML</i> 片段</strong>`,'HTML片段',{
dangerouslyUseHTMLString:true,
//中间布局
center:true,
//是否显示取消按钮
showCancelButton:true,
//取消按钮的文本内容
cancelButtonText:"支付遇见问题",
//确定按钮的文本内容
confirmButtonText:"已经支付成功",
//右上角的叉叉是否显示
showClose:false
})
}
11.4 微信支付业务
使用qrcode,一个用于生成二维码的JavaScript库
获取支付订单状态的接口:
//获取支付订单状态
export const reqPayStatus=(orderId)=>requests({
url:`/payment/weixin/queryPayStatus/${orderId}`,
method:"get"
})
弹出框业务:
//弹出框
async open(){
//生成二维码地址
let url=await QRCode.toDataURL(this.payInfo.codeUrl)
this.$alert(`<img src="${url}"/>`,'HTML片段',{
dangerouslyUseHTMLString:true,
//中间布局
center:true,
//是否显示取消按钮
showCancelButton:true,
//取消按钮的文本内容
cancelButtonText:"支付遇见问题",
//确定按钮的文本内容
confirmButtonText:"已经支付成功",
//右上角的叉叉是否显示
showClose:false,
//关闭弹出框的配置值
beforeClose:(type,instance,done)=>{
//type:区分取消|确定按钮
//instance:当前组件实例
//done:关闭弹出框的方法
if(type==='cancel'){
alert('请联系管理员')
clearInterval(this.timer)
this.timer=null
//关闭弹出框
done()
}else{
//判断是否真的支付了
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.reqPayStatus(this.orderId)
//如果code===200
if(result.code===200){
//第一步:清除定时器
clearInterval(this.timer)
this.timer=null
//保存支付成功返回的code
this.code=result.code
//关闭弹出窗
this.$msgbox.close()
//跳转到下一路由
this.$router.push('/paysuccess')
}
},1000)
}
}
day12
12.1 个人中心二级路由的搭建
如果只进入/center,可能会出现右侧没有内容的情况,所以需要重定向到/center/myorder
{
path:'/center',
component:Center,
meta:{isShow:true},
children:[
{
path:'myorder',
component:MyOrder
},
{
path:'grouporder',
component:GroupOrder
},
{
path:'/center',
redirect:"/center/myorder"
}
]
}
12.2 我的订单
接口api:
//获取个人中心的数据
export const reqMyOrderList=(page,limit)=>requests({
url:`/order/auth/${page}/${limit}`,
method:"get"
})
12.3 未登录的导航守卫判断
全局前置守卫逻辑处理:
//未登录:不能去交易相关、支付相关的页面(pay|paysuccess)、不能去个人中心
//未登录若去上述页面---跳转至登录页面
let toPath=to.path
if(toPath.indexOf('/trade')!==-1||toPath.indexOf('/pay')!==-1||toPath.indexOf('/center')!==-1){
next('/login?redirect='+toPath)
}else {
//去的不是上面这些路由(home|search|shopCart)---放行
next()
}
对login的处理:增加了对query参数的判断
async userLogin(){
try{
const {phone,password}=this;
(phone&&password)&&(await this.$store.dispatch("userLogin",{phone,password}))
let toPath=this.$route.query.redirect||'/home'
this.$router.push(toPath)
}catch(error){
alert(error.message)
}
}
12.4 用户登录(路由独享与组件内守卫)
路由独享守卫:
只有从购物车界面才能跳转到交易页面(创建订单)
{
path:"/trade",
component: Trade,
meta:{isShow:true},
//路由独享守卫
beforeEnter:(to,from,next)=>{
if(from.path==='/shopcart'){
next()
}else{
next(false)
}
}
}
只有从交易页面(创建订单)页面才能跳转到支付页面
{
path:"/pay",
component:Pay,
meta:{isShow:true},
beforeEnter:(to,from,next)=>{
if(from.path==='/trade'){
next()
}else{
next(false)
}
}
}
只有从支付页面才能跳转到支付成功页面
组件内守卫:
beforeRouterEnter(to,from,next){ }:
在渲染该组件的对应路由被confirm前调用
不能获取组件的实例“this”
因为当守卫执行前,组件实例还没有被创建
export default {
name: 'PaySuccess',
beforeRouteEnter(to,next,from){
if(from.path==='/pay'){
next()
}else{
next(false)
}
}
}
beforeRouteUpdate(to,from,next){ }:
在当前路由改变、但是该组件被复用时调用
举例来说,对于一个带有动态参数的路径/foo/:id,在/foo/1和/foo/2之间跳转的时候
由于会渲染同样的Foo组件,因此组件实例会被复用,而这个钩子就是这种情况下被调用
可以访问组件实例“this”
beforeRouteLeave(to,from,next){ }:
导航离开该组件的对应路由时调用
可以访问组件实例的“this”
12.5 图片懒加载
①下载插件vue-lazyload
注意vue2对应1.3.3版本的vue-lazyload,否则会报路径错误
yarn add vue-lazyload@1.3.3
②图片、json不需要对外暴露就可引入
import VueLazyLoad from 'vue-lazyload'
import winter from '@/assets/winter.jpg'
Vue.use(VueLazyLoad,{
//懒加载默认图片
loading:winter
})
在需要默认图片的模板中插入:
<img v-lazy="good.defaultImg" />
12.6 vee-validate表单验证的使用
①安装:注意vue2对应vee-validate2版本
yarn add vee-validate@2
②在main.js中引入表单校验插件
//引入表单校验插件
import '@/plugins/validate'
③编写表单校验插件内容
import Vue from 'vue'
import VeeValidate from 'vee-validate'
import zh_CN from 'vee-validate/dist/locale/zh_CN'
Vue.use(VeeValidate)
//表单验证
VeeValidate.Validator.localize("zh_CN",{
messages:{
...zh_CN.messages,
is:(field)=>`${field}必须与密码相同`,
},
attributes:{
phone:"手机号",
code:"验证码",
password:"密码",
rePassword:"确认密码",
agree:"协议"
}
})
④改写模板里的代码(以手机号校验为例)
<div class="content">
<label>手机号:</label>
<input type="text"
placeholder="请输入你的手机号"
v-model="phone"
name="phone" v-validate="{required:true,regex:/^1\d{10}$/}" :class="{invalid:errors.has('phone')}"
>
<span class="error-msg">{{errors.first("phone")}}</span>
</div>
⑤效果图
不输入时:
输入错误时:
格式正确时警告会消失:
验证码校验:
<div class="content">
<label>验证码:</label>
<input type="text"
placeholder="请输入验证码"
v-model="code"
name="code" v-validate="{required:true,regex:/^\d{6}$/}" :class="{invalid:errors.has('code')}"
>
<button style="width:100px;height:38px;" @click="getCode">获取验证码</button>
<span class="error-msg">{{errors.first("code")}}</span>
</div>
密码校验:
<div class="content">
<label>登录密码:</label>
<input type="password"
placeholder="请输入你的登录密码"
v-model="password"
name="password" v-validate="{required:true,regex:/^[0-9a-zA-Z]{8,20}$/}" :class="{invalid:errors.has('password')}"
>
<span class="error-msg">{{errors.first("password")}}</span>
</div>
确认密码校验:
<div class="content">
<label>确认密码:</label>
<input type="password"
placeholder="请输入确认密码"
v-model="rePassword"
name="rePassword" v-validate="{required:true,is:password}" :class="{invalid:errors.has('rePassword')}"
>
<span class="error-msg">{{errors.first("rePassword")}}</span>
</div>
效果图:
对于勾选同意协议的复选框,必须自定义规则
//自定义校验规则
VeeValidate.Validator.extend("agree",{
validate:value=>{
return value
},
getMessage:field=>field+"必须同意"
})
修改模板里的内容:
<div class="controls">
<input
type="checkbox"
:checked="agree"
name="agree" v-validate="{required:true,'agree':true}" :class="{invalid:errors.has('agree')}"
>
<span>同意协议并注册《尚品汇用户协议》</span>
<span class="error-msg">{{errors.first("agree")}}</span>
</div>
如果所有的表单验证都通过,再向服务器发送请求进行注册
async userRegister(){
const success=await this.$validator.validateAll()
if(success){
try{
const {phone,code,password,rePassword}=this
await this.$store.dispatch("userRegister",{phone,code,password})
this.$router.push("/login")
}catch(error) {
alert(error.message)
}
}
}
12.7 路由懒加载
当打包构建应用时,JavaScript包会变的非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,就会更加高效
以Search组件为例:
{
name:'search',
path:"/search/:keyword?",
component:()=>import("@/pages/Search.vue"),
meta:{show:true}
}
12.8 处理map文件
打包:yarn build
项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列又错了
文件如果项目不需要可以去掉
Vue.config.js配置中productionSourceMap:false可以不生成体积很大的map文件
productionSourceMap:false
12.9 购买服务器
可以去阿里云或者腾讯云购买服务器,腾讯云更便宜
完结撒花❀❀~