赞
踩
然后就开始下载了
可以设置接下来浏览器自动打开
在package.json文件中
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
创建vue.config.js文件:需要对外暴露
module.exports = {
lintOnSave:false,
}
因为项目大的时候src(源代码文件夹):里面目录会很多,找文件不方便,设置src文件夹的别名的好处,找文件会方便一些
创建jsconfig.json文件
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}
辉洪老师的静态页面
项目采用的less样式,浏览器不识别less语法,需要一些loader进行处理,把less语法转换为CSS语法
切记less-loader安装5版本的,不要安装在最新版本,安装最新版本less-loader会报错,报的错误setOption函数未定义
创建router文件夹,里面创建index.js
// 配置路由的地方 import Vue from 'vue'; import VueRouter from 'vue-router'; // 使用插件 Vue.use(VueRouter) // 引入路由组件 import Home from '@/pages/Home' import Search from '@/pages/Search' // 配置路由 export default new VueRouter({ routes:[ { path: "/home", component: Home, }, { path: "/search", component: Search, }, ] })
然后去main.js 里面注册
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
// 引入路由
import router from '@/router'
new Vue({
render: h => h(App),
// 注册路由
// 注册路由信息:当这里书写router的时候,组件身上都拥有$route、$router属性
router
}).$mount('#app')
路由组件一般需要在router文件夹进行注册(使用的是组件的名字)
非路由组件在使用时,一般是以标签的形式使用
$route: 一般获取路由信息【路径、query、params等等】
$router: 一般进行编程式导航进行路由跳转
设置 meta 元信息
根据$route获取当前路由信息
路由的跳转就两种形式:
编程式导航更好用:因为可以书写自己的业务逻辑
路由传递参数的三种方式
// 路由传递参数 // 第一种:字符串形式 // 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() }, } );
params参数:路由需要占位,程序就崩了,属于URL当中一部分
如果用对象形式传params参数,需要给路由命名,如下所示:
query参数:路由不需要占位,写法类似于ajax当中query参数
类似于 ?k=v&k2=v2&k3=v3
/home?name=lsh&id=666&sex=1
{
path: "/search/:keyword",
name: 'search',
component: Search,
meta: {
show: true
},
},
路由传递参数先关面试题
1:路由传递参数(对象写法)path是否可以结合params参数一起使用?
不可以:不能这样书写,程序会崩掉
2:如何指定params参数可传可不传?
3:params参数可以传递也可以不传递,但是如果传递是空串,如何解决?
4:如果指定name与params配置, 但params中数据是一个"", 无法跳转,路径会出问题
5: 路由组件能不能传递props数据?
多次执行相同的push问题,控制台会出现警告
例如:使用this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}})时,如果多次执行相同的push,控制台会出现警告。
let result = this.$router.push({name:"Search",query:{keyword:this.keyword}})
console.log(result)
执行一次上面代码:
多次执行出现警告:
原因:push是一个promise,promise需要传递成功和失败两个参数,我们的push中没有传递。
方法:this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}},()=>{},()=>{})后面两项分别代表执行成功和失败的回调函数。
这种写法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误
push是VueRouter.prototype的一个方法,在router中的index重写该方法即可(看不懂也没关系,这是前端面试题)
//1、先把VueRouter原型对象的push,保存一份
let originPush = VueRouter.prototype.push;
//2、重写push|replace
//第一个参数:告诉原来的push,跳转的目标位置和传递了哪些参数
VueRouter.prototype.push = function (location,resolve,reject){
if(resolve && reject){
originPush.call(this,location,resolve,reject)
}else{
originPush.call(this,location,() => {},() => {})
}
}
我们的三级联动组件是全局组件,全局的配置都需要在main.js中配置
//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);
在Home组件中使用该全局组件
<template>
<div>
<!-- 三级联动全局组件已经注册为全局组件,因此不需要引入-->
<TypeNav/>
</div>
</template>
全局组件可以在任一页面中直接使用,不需要导入声明
下面全部商品分类就是三级联动组件
home文件夹index.vue
<template> <div> <!-- 三级联动全局组件已经注册为全局组件,因此不需要引入--> <TypeNav/> <!-- 轮播图列表--> <ListContainer/> <!-- 今日推荐--> <Recommend/> <!-- 商品排行--> <Rank/> <!-- 猜你喜欢--> <Like/> <!-- 楼层 --> <Floor/> <Floor/> <!-- 商标--> <Brand/> </div> </template> <script> import ListContainer from './ListContainer' import Recommend from './Recommend' import Rank from './Rank' import Like from './Like' import Floor from './Floor' import Brand from './Brand' export default { name: "index", components: { ListContainer, Recommend, Rank, Like, Floor, Brand, } } </script> <style scoped> </style>
首先安装axios
cnpm i --save axios
在根目录下创建api文件夹,创建request.js文件。
// 对axios进行二次封装 import axios from "axios"; // 1、利用axios对象的方法create,去创建一个axios实例 ///2、request就是axios,只不过稍微配置一下 const request = axios.create({ // 配置对象 // 基础路径,发请求时,路径会出现api baseURL: "/api", // 代表请求超时时间5s timeput: 5000, }); // 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情 request.interceptors.request.use((config) => { // config:配置对象,对象里面有一个属性很重要,headers请求头 return config; }); // 响应拦截器 request.interceptors.response.use((res) => { // 成功的回调函数:服务器响应数据回来之后,响应拦截器可以做一些事情 return res.data; }, (error) => { // 响应失败的回调函数 return Promise.reject(new Error('faile')); }); // 对外暴露 export default request;
在根目录下的vue.config.js中配置,proxy为通过代理解决跨域问题。
我们在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。
module.exports = {
productionSourceMap:false,
// 关闭ESLINT校验工具
lintOnSave: false,
//配置代理跨域
devServer: {
proxy: {
"/api": {
target: "http://39.98.123.211",
},
},
},
};
安装进度条
cnpm i --save nprogress
在发起请求时,开启进度条,请求成功后,关闭进度条
所以在request.js进行配置
// 对axios进行二次封装 import axios from "axios"; // 引入进度条 import nprogress from "nprogress"; // 引入进度条样式 import "nprogress/nprogress.css"; // 1、利用axios对象的方法create,去创建一个axios实例 ///2、request就是axios,只不过稍微配置一下 const request = axios.create({ // 配置对象 // 基础路径,发请求时,路径会出现api baseURL: "/api", // 代表请求超时时间5s timeput: 5000, }); // 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情 request.interceptors.request.use((config) => { // 进度条开始 nprogress.start(); // config:配置对象,对象里面有一个属性很重要,headers请求头 return config; }); // 响应拦截器 request.interceptors.response.use((res) => { // 成功的回调函数:服务器响应数据回来之后,响应拦截器可以做一些事情 // 进度条结束 nprogress.done(); return res.data; }, (error) => { // 响应失败的回调函数 return Promise.reject(new Error('faile')); }); // 对外暴露 export default request;
也可以修改进度条的样式
首先,先下载Vuex
cnpm i --save vuex
如果想要使用vuex,还要再main.js中引入
main.js:
(1) 引入文件
(2) 注册store
但凡是在main.js中的Vue实例中注册的实体,在所有的组件中都会有(this.$.实体名)属性
// 引入仓库
import store from '@/store'
new Vue({
render: h => h(App),
// 注册路由
// 注册路由信息:当这里书写router的时候,组件身上都拥有$route、$router属性
router,
// 注册仓库:组件实例身上会多一个属性$store属性
store,
}).$mount('#app')
根目录创建store文件夹,文件夹下创建index.js
建立多个小仓库,比如home、search小仓库,然后大仓库引入小仓库
大仓库如下
import Vue from 'vue'; import Vuex from 'vuex'; // 需要使用插件一次 Vue.use(Vuex); // 引入小仓库 import home from './home'; import search from './search'; // 对外暴露Store类的一个实例 export default new Vuex.Store({ moudles: { home, search } });
小仓库如下
home
// home模块小仓库 // state:仓库存储数据的地方 const state = { a:1 }; // mutations:修改states的唯一手段 const mutations = {}; // actions:处理action,可以书写自己的业务逻辑,也可以处理异步 const actions = {}; // getters理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便 const getters = {}; export default { state, mutations, actions, getters, }
search
// search模块小仓库 const state = { b:2 }; const mutations = {}; const actions = {}; const getters = {}; export default { state, mutations, actions, getters, }
Vuex工作原理图
发请求到Actions
页面加载时,通过Vuex发请求,获取数据
mounted() {
// 派发action || 获取商品分类的三级列表的数据
this.$store.dispatch("categoryList");
},
Actions调用 commit 方法提交到Mutations
首先发 ajax 请求得到需要的数据
将commit结构赋值出来,调用commit 将数据提交到Mutations
在Mutations里面修改State里面的值
下图的 categoryList 就是上图的 result.data
State里面应该设置好初始值,数据类型应该与实际的数据类型保持一致
设置初始值categoryList
最后在相应的页面上借用辅助函数MapState来获取
import { mapState } from "vuex";
computed: {
...mapState({
categoryList: (state) => state.home.categoryList.slice(0, 16),
}),
},
我们可以在TypeNav里面看到 categoryList 这个数据
正常情况下:事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很可能出现浏览器卡顿)
防抖:前面的所有触发都被取消,最后一次执行在规定时间之后才会触发, 就是说如果连续快速的触发,只会执行最后一次
节流:在规定间隔时间内不会重复触发回调,只有大于这个时间间隔才会触发,把频繁触发变为少量触发
不需要安装,有些依赖需要loadsh,所以之前就已经引入loadsh了
下面代码就是将changeIndex设置了节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。
//引入lodash:是把lodash全部封装好的函数全都引入进来了
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";
methods: {
// 鼠标进入修改currentIndex属性
changeIndex: throttle(function (index) {
this.currentIndex = index;
},50),
leaveIndex() {
this.currentIndex = -1;
},
},
三级联动用户可以点击:一级分类、二级分类、三级分类,当你点击的时候,Home模块跳转到Search模块,同时会把你选中的产品(产品名字、ID)传递过去
路由跳转:
声明式导航: router-link
编程式导航:push || replace
三级联动: 如果使用声明式导航,可以实现跳转和参数传递,但是会出现卡顿现象。
router-link:router-link 是一个组件,当服务器返回数据之后,会循环出很多个router-link组件【需要创建组件实例,还要讲虚拟DOM转化成真实DOM】
创建组件实例的时候,一瞬间创建1000+需要很多内存的,因此出现卡顿现象。
所以最好利用编程式导航进行跳转
直接写在a标签里面不好,因为a标签是在v-for里面的,这样会有很多个a标签,很多个goSearch回调函数,所以使用事件委派,将goSearch写在a标签的父元素里面。这样只需要一个回调函数goSearch就可以解决。
所以最后是利用 编程式导航+事件委派 进行跳转
事件委派带来的问题:
1、点击的是a标签时,才可以跳转,如何确保是a标签
2、如何获取a标签的商品名称、商品Id
解决方法:
1、给a标签添加自定义属性data-categoryName,标识a标签
1、分别给a标签添加自定义属性data-category1Id、data-category2Id、data-category3Id,获取到不一样的商品Id,用于路由跳转。
我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。
// 进行路由跳转 goSearch(event) { // 编程式导航 + 事件委派 // 利用事件委派存在一些问题: // 1:点击的一定是a标签,才进行跳转 // 2.如何区分一级、二级、三级分类的标签 // 第一个问题:把子节点中的a标签,加上自定义属性data-categoryName,其余节点没有 let element = event.target; console.log(element); // 获取到当前触发这个事件的节点【h3、a、dt、dl】带有自定义属性data-categoryName就是a标签 let { categoryname, category1id, category2id, category3id } = element.dataset; //当前这个if语句:一定是a标签才会进入 if (categoryname) { let location = { name: 'search'}; let query = { categoryName: categoryname}; if (category1id) { query.category1Id = category1id; } else if (category2id) { query.category2Id = category2id; } else { query.category3Id = category3id; } // 整理完参数 location.query = query; // 路由跳转 this.$router.push(location); } },
注意:event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">
//函数定义
goSearch(event){
console.log(event.target)
}
做一个简要的使用总结:
(1)安装swiper
(2)在需要使用轮播图的组件内导入swpier和它的css样式
引入swiper
引入样式,因为其他地方也需要这些样式,所以就全局引入
(3)在组件中创建swiper需要的dom标签(html代码,参考官网代码)
<!--banner轮播--> <div class="swiper-container" id="mySwiper"> <div class="swiper-wrapper"> <div class="swiper-slide" v-for="(carousel, index) in bannerList" :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>
(4)创建swiper实例
接下来就是考虑在哪里创建swiper实例
直接在mounted里面创建是失败的
mounted() { //请求数据 this.$store.dispatch("getBannerList") //创建swiper实例 let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{ pagination:{ el: '.swiper-pagination', clickable: true, }, // 如果需要前进后退按钮 navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev', }, // 如果需要滚动条 scrollbar: { el: '.swiper-scrollbar', }, }) },
因为mounted里面先去异步请求了轮播图数据,然后再去创建swiper实例,由于请求数据是异步的,所以数据还没请求回来,swiper实例就创建好了,所以就导致轮播图展示失败了
解决方法一:等我们的数据请求完毕后再创建swiper实例。只需要加一个1000ms时间延迟再创建swiper实例.。将上面代码改为:
mounted() { this.$store.dispatch("getBannerList") setTimeout(()=>{ let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{ pagination:{ el: '.swiper-pagination', clickable: true, }, // 如果需要前进后退按钮 navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev', }, // 如果需要滚动条 scrollbar: { el: '.swiper-scrollbar', }, }) },1000) },
上面的方法实现了功能,但不是最完美的
解决方法二:我们可以使用watch监听bannerList轮播图列表属性,因为bannerList初始值为空,当它有数据时,我们就可以创建swiper对象
watch:{ bannerList(newValue,oldValue){ let mySwiper = new Swiper(this.$refs.cur,{ pagination:{ el: '.swiper-pagination', clickable: true, }, // 如果需要前进后退按钮 navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev', }, // 如果需要滚动条 scrollbar: { el: '.swiper-scrollbar', }, }) } }
但是还是不能实现轮播图的功能。因为我们的watch只能保证bannerList有数据,但是不能保证此时v-for已经执行完了,v-for执行也需要时间。假如watch监听到bannerList有数据变化了,执行回调函数创建swiper实例,之后v-for才执行,这样是无法渲染轮播图的
完美解决方案:使用watch+this.$nextTick()
this.$nextTick()解析:this. $nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。
watch:{ bannerList(newValue,oldValue){ //this.$nextTick()使用 this.$nextTick(()=>{ let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{ pagination:{ el: '.swiper-pagination', clickable: true, }, // 如果需要前进后退按钮 navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev', }, // 如果需要滚动条 scrollbar: { el: '.swiper-scrollbar', }, }) }) } }
先在 api/index.js 准备好接口
// 获取floor数据
export const reqFloorList = () => mockRequests.get("/floor");
在 store/home/index.js 准备好Vuex三连环
在 pages/Home/index.vue 触发actions,然后v-for循环Floor组件
<!-- 楼层 -->
<Floor v-for="(floor,index) in floorList" :key="floor.id"/>
mounted() {
this.$store.dispatch("getFloorList")
},
computed:{
...mapState({
floorList:state => state.home.floorList
})
}
获取到数据之后,将数据从index.vue传到Floor组件(父传子)
<!-- 楼层 -->
<Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/>
name: "floor",
props: ["list"],
然后就是将对应的数据绑定到对应的位置上(只展示一部分的)
<div class="title clearfix">
<h3 class="fl">{{ list.name }}</h3>
<div class="fr">
<ul class="nav-tabs clearfix">
<li
class="active"
v-for="(nav, index) in list.navList"
:key="index"
>
<a href="#tab1" data-toggle="tab">{{nav.text}}</a>
</li>
</ul>
</div>
</div>
因为很多地方都用到轮播图,因此我们直接将它提取为公共组件
<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> //引入Swiper import Swiper from "swiper"; export default { name: "Carousel", props: ["list"], watch: { list: { //立即监听:不管你数据有没有变化,我上来立即监听一次 //为什么watch监听不大list:因为这个数据从来没有发生变化(数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的) immediate: true, handler() { //只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定的,因此还是需要用nextTick this.$nextTick(() => { var mySwiper = new Swiper(this.$refs.cur, { loop: true, // 如果需要分页器 pagination: { el: ".swiper-pagination", //点击小球的时候也切换图片 clickable: true, }, // 如果需要前进后退按钮 navigation: { nextEl: ".swiper-button-next", prevEl: ".swiper-button-prev", }, }); }); }, }, }, }; </script> <style scoped></style>
然后在 main.js 中注册为公共组件
import Carousel from '@/components/Carousel';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(Carousel.name,Carousel);
然后再需要轮播图的地方使用组件
<!-- 轮播图的地方 -->
<Carousel :list="list.carouselList"/>
静态界面直接使用资料中准备好的文件,不再一一拆分了
然后使用Vuex发请求获取数据
import { reqGetSearchInfo } from "@/api"; // search模块小仓库 const state = { //仓库初始状态 searchList: {} }; const mutations = { GETSEARCHLIST(state, searchList) { state.searchList = searchList; }, }; const actions = { //获取search模块数据 async getSearchList({ commit }, params = {}) { //当前这个reqGetSearchInfo这个函数在调用获取服务器数据的时候,至少传递一个参数(空对象) //params形参:是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象 let result = await reqGetSearchInfo(params); if (result.code == 200) { commit("GETSEARCHLIST", result.data); } }, };
mounted() {
this.$store.dispatch('getSearchList', {});
},
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
个人理解:getters将获取store中的数据封装为函数,代码维护变得更简单(和我们将请求封装为api一样)。而且getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
注意:仓库中的getters是全局属性,是不分模块的。
//计算属性 //项目当中getters主要的作用是:简化仓库中的数据(简化数据而生) //可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候就方便了】 const getters = { goodsList(state) { return state.searchList.goodsList || []; }, trademarkList(state) { return state.searchList.trademarkList || []; }, attrsList(state) { return state.searchList.attrsList || []; }, };
在Search组件中使用getters获取仓库数据
import { mapGetters } from "vuex";
computed: {
...mapGetters(["goodsList"]),
},
如果在每个三级分类列表和搜索按钮加一个点击按钮事件,只要点击了就执行搜索函数
但是这样子做会生成很多回调函数,很消耗性能。
最好解决方法:用watch监听路由信息变化
我们每次进行新的搜索时,我们的query和params参数中的部分内容会发生变化,而且这两个参数都是路由的属性,所以可以通过监听路由信息变化来动态发起搜索请求。
search组件watch部分代码。
// 数据监听:监听组件实例身上的属性的属性值是否变化
watch: {
$route(newValue, oldValue) {
// 再次处理请求参数
Object.assign(this.searchParams, this.$route.query, this.$route.params);
console.log(this.searchParams);
this.getData();
//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
},
},
在点击删除分类时,我们需要categoryName和 category3Id(或者是category1Id、category2Id)删除掉,但是params中的keyword参数(华为)不需要删除。
所以我们需要把 categoryName 、category3Id、category1Id 、category2Id 赋值为 undefined ;接着我们再次发请求更新页面上的数据
这个时候我们点击分类,页面上的数据的确发生变化了,但是地址栏上的内容并没有变化,事实上页面上地址栏同样也需要改变,而且路径中的params不应该删除,路由跳转的时候应该带着
我们应该重新跳转当前页面,并携带params参数
<!-- 分类的面包屑 --> <li class="with-x" v-if="searchParams.categoryName"> {{ searchParams.categoryName }}<i @click="removeCategoryName">×</i> </li> // 删除分类的名字 removeCategoryName() { // 带给服务器的参数变为undefined,参数就不会传递给服务器 this.searchParams.categoryName = undefined; this.searchParams.category1Id = undefined; this.searchParams.category2Id = undefined; this.searchParams.category3Id = undefined; this.getData(); // 地址栏也需要更改 // 注意:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着 if (this.$route.params) { this.$router.push({ name: "search", params: this.$route.params }); } },
最终的效果是这样的(对比一下上图就可以看出区别了)
这一步实质就是在处理params参数,与处理query参数类似
<!-- 关键字的面包屑 --> <li class="with-x" v-if="searchParams.keyword"> {{ searchParams.keyword }}<i @click="removeKeyword">×</i> </li> // 删除关键字 removeKeyword() { // 带给服务器的参数变为undefined,参数就不会传递给服务器 this.searchParams.keyword = undefined; this.getData(); // 通知兄弟组件Header清除关键字 this.$bus.$emit("clear"); //进行路由的跳转 if (this.$route.query) { this.$router.push({ name: "search", query: this.$route.query }); } },
唯一不同的就是,这一步需要去清除输入框中的关键字
输入框是在Header组件中的
Header和Search是兄弟组件,所以需要兄弟组件通信才可以完成该功能
这里通过$bus实现header和search组件的通信。
全局事件总线$bus的使用
(1)在main.js中注册
new Vue({
render: h => h(App),
// 全局事件总线$bus配置
beforeCreate() {
Vue.prototype.$bus = this;
},
// 注册路由
// 注册路由信息:当这里书写router的时候,组件身上都拥有$route、$router属性
router,
// 注册仓库:组件实例身上会多一个属性$store属性
store,
}).$mount('#app')
(2)Search组件中使用$bus通信。第一个参数可以理解为通信的暗号,第二个参数可以传递数据。这里只是通知header组件进行相应操作,因此不需要第二个参数。
// 删除关键字
removeKeyword() {
// 带给服务器的参数变为undefined,参数就不会传递给服务器
this.searchParams.keyword = undefined;
this.getData();
// 通知兄弟组件Header清除关键字
this.$bus.$emit("clear");
//进行路由的跳转
if (this.$route.query) {
this.$router.push({ name: "search", query: this.$route.query });
}
},
(3)header组件接受$bus通信
注意:组件挂载时就监听clear事件
mounted() {
this.$bus.$on("clear", () => {
this.keyword = '';
});
},
此处生成面包屑时会涉及到子组件向父组件传递信息操作,之后的操作和前面讲的面包屑操作原理相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。
首先在子组件中定义点击品牌的处理函数,函数体里面触发子组件的自定义事件、传递数据
<!-- 品牌地方 --> <ul class="logo-list"> <li v-for="(trademark, index) in trademarkList" :key="trademark.tmId" @click="tradeMarkHandler(trademark)" > {{ trademark.tmName }} </li> </ul> // 品牌处理函数 tradeMarkHandler(trademark) { //点击了品牌(苹果),还是需要整理参数,向服务器发请求获取相应的数据进行展示 //老师问题:在那个组件中发请求,父组件? //为什么那,因为父组件中searchParams参数是带给服务器参数,子组件组件把你点击的品牌的信息,需要给父组件传递过去---自定义事件 this.$emit('tradeMarkInfo', trademark); },
自定义事件的回调函数跟自定义事件的名字一样
<!--selector-->
<SearchSelector @tradeMarkInfo="tradeMarkInfo" />
// 自定义事件回调
tradeMarkInfo(trademark) {
//1:整理品牌字段的参数 "ID:品牌名称"
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
this.getData();
},
品牌面包屑的展示与删除
<!-- 品牌的面包屑 -->
<li class="with-x" v-if="searchParams.trademark">
{{ searchParams.trademark.split(":")[1]
}}<i @click="removeTradeMark">×</i>
</li>
// 删除品牌
removeTradeMark() {
//将品牌信息置空
this.searchParams.trademark = undefined;
//再次发请求
this.getData();
},
处理售卖属性与上面的处理品牌信息是类似的,也是用到了子传父
不过在展示处理属性标签时,需要用到数组去重,因为属性标签不能重复
// 收集平台属性地方回调函数(自定义事件)
attrInfo(attr, attrValue) {
//["属性ID:属性值:属性名"]
console.log(attr, attrValue);
//参数格式整理好
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
//数组去重
//if语句里面只有一行代码:可以省略大花括号
if (this.searchParams.props.indexOf(props) == -1) {
this.searchParams.props.push(props);
}
//再次发请求
this.getData();
},
// 删除售卖属性
removeAttr(index) {
this.searchParams.props.splice(index, 1);
//再次发请求
this.getData();
},
排序的逻辑比较简单,只是改变一下请求参数中的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_2994457_qqwrvmss9l9.css">
在search模块使用该图标
这里isOne、isTwo、isAsc、isDesc是计算属性,如果不使用计算属性要在页面中写很长的代码
<!-- 排序结构 --> <ul class="sui-nav"> <!-- 这里isOne、isTwo、isAsc、isDesc是计算属性,如果不使用计算属性要在页面中写很长的代码--> <li :class="{ active: isOne }" @click="changeOrder('1')"> <a >综合<span v-show="isOne" class="iconfont" :class="{ 'icon-up': isAsc, 'icon-down': isDesc }" ></span ></a> </li> <li :class="{ active: isTwo }" @click="changeOrder('2')"> <a >价格<span v-show="isTwo" class="iconfont" :class="{ 'icon-up': isAsc, 'icon-down': isDesc }" ></span ></a> </li> </ul>
isOne、isTwo、isAsc、isDesc计算属性代码
isOne() {
return this.searchParams.order.indexOf("1") != -1;
},
isTwo() {
return this.searchParams.order.indexOf("2") != -1;
},
isDesc() {
return this.searchParams.order.indexOf("desc") != -1;
},
isAsc() {
return this.searchParams.order.indexOf("asc") != -1;
},
点击‘综合’或‘价格’的触发函数changeOrder
// 排序操作 changeOrder(flag) { //flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2) console.log(flag); //现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】 let originOrder = this.searchParams.order; let originFlag = this.searchParams.order.split(":")[0]; let originSort = this.searchParams.order.split(":")[1]; console.log(originFlag, originSort); //新的排序方式 let newOrder = ""; //判断的是多次点击的是不是同一个按钮 if (originFlag == flag) { newOrder = `${originFlag}:${originSort == "desc" ? "asc" : "desc"}`; } else { //点击不是同一个按钮 newOrder = `${flag}:${"desc"}`; } this.searchParams.order = newOrder; //再次发请求 this.getData(); },
一、传递需要的数据
分页器需要哪些数据
由pageSize和total可以得到另一信息:共页数
这些数据父组件传递到子组件
父组件代码:
<!-- 分页器 -->
<Pagination
:pageNo="searchParams.pageNo"
:pageSize="searchParams.pageSize"
:total="total"
:continues="5"
@getPageNo="getPageNo"
/>
子组件代码:
props: ["pageNo", "pageSize", "total", "continues"],
总页数通过计算属性computed计算得到
computed: {
// 总共多少页
totalPage() {
// 向上取整
return Math.ceil(this.total / this.pageSize);
},
二、处理连续页码
接下来要做的是:算出连续页码数字(起始页数和结束页数)
连续页码数字一般是基数,基数对称(比较好看)
if (continues > this.totalPage) {
start = 1;
end = this.totalPage;
}
连续页码数字的代码如下:
computed: { // 计算出连续的页码的起始数字与结束数字 startNumAndEndNum() { const { continues, pageNo, totalPage } = this; // 先定义两个变量储存起始数字与结束数字的 let start = 0, end = 0; // 连续页码数字至少是5(至少5页),如果不够5页 if (continues > this.totalPage) { start = 1; end = this.totalPage; } else { //正常现象【连续页码5,但是你的总页数一定是大于5的】 //起始数字 start = pageNo - parseInt(continues / 2); //结束数字 end = pageNo + parseInt(continues / 2); //把出现不正常的现象【start数字出现0|负数】纠正 if (start < 1) { start = 1; end = continues; } //把出现不正常的现象[end数字大于总页码]纠正 if (end > totalPage) { end = totalPage; start = totalPage - continues + 1; } return { start, end }; } }, },
三、 动态展示分页器(使用v-for)
v-for不仅可以遍历数组,还可以遍历number、object、string等等
因为分页器是子组件,这里点击某一页时,需要用到子传父,使用自定义事件将信息传递到父组件(传递参数,赋值给pageNo)
子组件: <div class="pagination"> <!-- 上 --> <button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)"> 上一页 </button> <button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)"> 1 </button> <button v-if="startNumAndEndNum.start > 2">···</button> <!-- 中间部分 --> <span v-for="(page, index) in startNumAndEndNum.end" :key="index"> <button v-if="page >= startNumAndEndNum.start" @click="$emit('getPageNo', page)" > {{ page }} </button> </span> <!-- 下 --> <button v-if="startNumAndEndNum.end < totalPage - 1">···</button> <button v-if="startNumAndEndNum.end < totalPage" @click="$emit('getPageNo', totalPage)" > {{ totalPage }} </button> <button :disabled="pageNo == totalPage" @click="$emit('getPageNo', pageNo + 1)" > 下一页 </button> <button style="margin-left: 30px">共 {{ total }} 条</button> </div> 父组件: // 自定义事件的回调函数--获取当前第几页 getPageNo(pageNo) { this.searchParams.pageNo = pageNo; // 再次发请求 this.getData(); }
注意:当前页是第一页时,不能点击上一页
<button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">
上一页
</button>
<button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)">
1
</button>
<span v-for="(page, index) in startNumAndEndNum.end" :key="index">
<button
v-if="page >= startNumAndEndNum.start"
@click="$emit('getPageNo', page)"
>
{{ page }}
</button>
</span>
<button
v-if="startNumAndEndNum.end < totalPage"
@click="$emit('getPageNo', totalPage)"
>
{{ totalPage }}
</button>
注意:当前页是最后一页时,不能点击下一页
<button
:disabled="pageNo == totalPage"
@click="$emit('getPageNo', pageNo + 1)"
>
下一页
</button>
四、给点击的页数添加样式
这一步比较简单,就是给点击的页数添加一个样式而已
添加样式: .active { background: skyblue; } <button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)" :class="{ active: pageNo == 1 }" > 1 </button> <span v-for="(page, index) in startNumAndEndNum.end" :key="index"> <button v-if="page >= startNumAndEndNum.start" @click="$emit('getPageNo', page)" :class="{ active: pageNo == page }" > {{ page }} </button> </span> <button v-if="startNumAndEndNum.end < totalPage" @click="$emit('getPageNo', totalPage)" :class="{ active: pageNo == totalPage }" > {{ totalPage }} </button>
首先引入Detail组件
src\router\routes.js
{
path: "/detail/:skuid",
component: Detail,
// 路由元信息Key不能乱写,只能是meta
meta: {
show: true
},
},
点击商品的图片时,进行跳转,需要传递id
src\pages\Search\index.vue
<div class="p-img">
<!-- 路由跳转时携带params参数 -->
<router-link :to="`/detail/${good.id}`">
<img :src="good.defaultImg" />
</router-link>
</div>
它让你可以自定义路由切换时页面如何滚动。
src\router\index.js
// 配置路由
export default new VueRouter({
routes,
// 滚动行为
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
// y=0,代表滚动条在最上方
return { y: 0 }
},
})
与前面获取数据是一样的,先准备好借口,然后就是Vuex获取数据
app\src\api\index.js
// 获取产品详情信息的接口 /api/item/{ skuId } 请求方式:get
export const reqGoodsInfo = (skuId) => requests({ url: `/item/${skuId}`, method: "get" });
app\src\store\detail\index.js
import { reqGoodsInfo } from "@/api"; // search模块小仓库 const state = { //仓库初始状态 goodInfo:{} }; const mutations = { GETGOODINFO(state, goodInfo) { state.goodInfo = goodInfo } }; const actions = { //获取产品详情信息(detail模块) async getGoodInfo({ commit }, skuId) { //params形参:是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象 let result = await reqGoodsInfo(skuId); if (result.code == 200) { commit("GETGOODINFO", result.data); } }, }; export default { state, mutations, actions, getters, }
app\src\pages\Detail\index.vue
mounted() {
// 派发action获取产品详情信息
this.$store.dispatch("getGoodInfo", this.$route.params.skuid);
},
获取数据之后就是将数据动态展示在页面上,步骤很多,但都是比较简单的,所以就省略掉了。
<div class="previewWrap">
<!--放大镜效果-->
<Zoom :skuImageList="skuImageList" />
<!-- 轮播小图列表 -->
<ImageList :skuImageList="skuImageList" />
</div>
因为轮播小图和放大镜效果图是兄弟组件,在轮播图组件中设置一个currendIndex,当轮播小图的currendIndex改变时,通知放大镜效果图也要做出改变
轮播小图:
<div class="swiper-slide" v-for="(slide, index) in skuImageList" :key="slide.id" > <img :src="slide.imgUrl" :class="{ active: currentIndex == index }" @click="changeCurrentIndex(index)" /> </div> changeCurrentIndex(index) { //修改响应式数据 this.currentIndex = index; //通知兄弟组件:当前的索引值为几 this.$bus.$emit("getIndex", this.currentIndex); },
放大镜效果图:
currentIndex改变之后,放大镜效果图就会切换
<img :src="imgObj.imgUrl" />
computed: {
imgObj() {
return this.skuImageList[this.currentIndex] || {};
},
},
mounted() {
//全局事件总线:获取兄弟组件传递过来的索引值
this.$bus.$on("getIndex", (index) => {
//修改当前响应式数据
this.currentIndex = index;
});
},
这里可以点击 “+” 或者 “-” ,也可以在输入框输入
<div class="controls"> <input autocomplete="off" class="itxt" v-model="skuNum" @change="changeSkuNum" /> <a href="javascript:" class="plus" @click="skuNum++">+</a> <a href="javascript:" class="mins" @click="skuNum > 1 ? skuNum-- : (skuNum = 1)" >-</a > </div> //表单元素修改产品个数 changeSkuNum(event) { //用户输入进来的文本 * 1 let value = event.target.value * 1; //如果用户输入进来的非法,出现NaN或者小于1 if (isNaN(value) || value < 1) { this.skuNum = 1; } else { //正常大于1【大于1整数不能出现小数】 this.skuNum = parseInt(value); } },
输入框绑定的change事件,需要判断用户输入是否合法,不能输入非数字或负数,当输入小数时,应该取整
点击加入购物车,向后端发请求,只需要根据状态码code判断是否跳转到“加入购物车成功页面”
跳转‘加入购物车成功页面’的同时要携带商品的信息,但是detail组件和‘加入购物车成功页面’组件毫无关系,要想传递数据,可以使用路由的query传递参数,但是query只适合传递单个数值的简单参数,若想传递对象之类的复杂信息,需要使用Web Storage实现
sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
注意:无论是session还是local存储的值都是字符串形式的,如果我们需要存储对象,需要在存储之前JSON.stringfy()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象。
detail store对应代码:
//加入购物车的||修改某一个产品的个数
async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
//发请求:前端带一些参数给服务器【需要存储这些数据】,存储成功了,没有给返回数据
//不需要在三连环(仓库存储数据了)
//注意:async函数执行返回的结果一定是一个promise【要么成功,要么失败】
let result = await reqAddOrUpdateShopCart(skuId, skuNum);
if (result.code == 200) {
//返回的是成功的标记
return "ok";
} else {
//返回的是失败的标记
return Promise.reject(new Error("faile"));
}
},
src\pages\Detail\index.vue
//加入购物车 async addShopcar() { //1:在点击加入购物车这个按钮的时候,做的第一件事情,将参数带给服务器(发请求),通知服务器加入购车的产品是谁 //this.$store.dispatch('addOrUpdateShopCart'),说白了,它是在调用vuex仓库中的这个addOrUpdateShopCart函数。 //2:你需要知道这次请求成功还是失败,如果成功进行路由跳转,如果失败,需要给用户提示 try { //成功 await this.$store.dispatch("addOrUpdateShopCart", { skuId: this.$route.params.skuid, skuNum: this.skuNum, }); //3:进行路由跳转 //4:在路由跳转的时候还需要将产品的信息带给下一级的路由组件 //一些简单的数据skuNum,通过query形式给路由组件传递过去 //产品信息的数据【比较复杂:skuInfo】,通过会话存储(不持久化,会话结束数据在消失) //本地存储|会话存储,一般存储的是字符串 sessionStorage.setItem("SKUINFO", JSON.stringify(this.skuInfo)); this.$router.push({ name: "addcartsuccess", query: { skuNum: this.skuNum }, }); } catch (error) { //失败 alert(error.message); } },
这里使用sessionStorage传递的是一个对象
里面有AddCartSuccess组件需要的数据(detail组件中已经通过mapGeters获取到,所以传递过去给AddCartSuccess组件,这样就不需要重新发请求获取了)
AddCartSuccess**组件代码截图:
AddCartSuccess组件通过计算属性获取
computed: {
skuInfo() {
return JSON.parse(sessionStorage.getItem("SKUINFO"));
},
},
一个网站是有很多用户的,每个用户自己的购物车都不一样,所以每一个人的购物车页面展示的东西都不一样
当你以游客身份访问网站时:
每个用户需要一个uuidToken,用来验证用户身份,让服务器知道你是谁,但是这个请求函数没有参数,所以我们把uuidToken加在请求头中
根据api接口文档封装请求函数
export const reqGetCartList = () => {
return requests({
url:'/cart/cartList',
method:'GET'
})}
创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid)。
生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储
单例模式,只赋值一次就不再赋值了
app\src\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 }
用户的uuid_token定义在store中的detail模块
app\src\store\detail.js
//封装游客身份模块uuid--->生成一个随机字符串(不能在变了)
import {getUUID} from '@/utils/uuid_token';
const state = {
goodInfo: {},
//游客临时身份
uuid_token:getUUID()
};
在request.js中设置请求头
app\src\api\request.js
import store from '@/store'; // 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情 request.interceptors.request.use((config) => { if (store.state.detail.uuid_token) { //请求头添加一个字段(userTempId):和后台老师商量好了 config.headers.userTempId = store.state.detail.uuid_token; } // 进度条开始 nprogress.start(); // config:配置对象,对象里面有一个属性很重要,headers请求头 return config; });
将上一步获取到的数据展示在相应的地方(比较简单,直接省略)
这里有三个操作,减一、加一、中间是修改输入框的数字,统一使用一个回调函数
传三个参数,第一个表示操作类型、第二个是disNum(变化量)、第三个表示哪一个产品(身上有id)
<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>
handler函数,修改商品数量时,加入节流操作。(防止用户快速点击,请求还没回来,导致输入框变为负数)
节流操作:在规定时间范围内不会重复触发回调函数,只有大于这个时间间隔才会触发下一次
//修改某一个产品的个数[节流] handler: throttle(async function (type, disNum, cart) { //type:为了区分这三个元素 //disNum形参:+ 变化量(1) -变化量(-1) input最终的个数(并不是变化量) //cart:哪一个产品【身上有id】 //向服务器发请求,修改数量 switch (type) { //加号 case "add": disNum = 1; break; case "minus": //判断产品的个数大于1,才可以传递给服务器-1 //如果出现产品的个数小于等于1,传递给服务器个数0(原封不动) disNum = cart.skuNum > 1 ? -1 : 0; break; case "change": // //用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零 if (isNaN(disNum) || disNum < 1) { disNum = 0; } else { //属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数 disNum = parseInt(disNum) - cart.skuNum; } // disNum = (isNaN(disNum)||disNum<1)?0:parseInt(disNum) - cart.skuNum; break; } //派发action try { //代表的是修改成功 await this.$store.dispatch("addOrUpdateShopCart", { skuId: cart.skuId, skuNum: disNum, }); //再一次获取服务器最新的数据进行展示 this.getData(); } catch (error) {} }, 1000),
这部分都比较简单,这里不多做赘述,唯一需要注意的是当store的action中的函数返回值data为null时,应该采用下面的写法(重点是if,else部分)
action部分:以修改某个产品的勾选状态为例
//修改购物车某一个产品的选中状态
async updateCheckedById({ commit }, { skuId, isChecked }) {
let result = await reqUpdateCheckedByid(skuId, isChecked);
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("faile"));
}
},
method部分:(重点是try、catch)
//修改某个产品的勾选状态 async updateChecked(cart, event) { //带给服务器的参数isChecked,不是布尔值,应该是0|1 try { //如果修改数据成功,再次获取服务器数据(购物车) let isChecked = event.target.checked ? "1" : "0"; await this.$store.dispatch("updateCheckedById", { skuId: cart.skuId, isChecked, }); this.getData(); } catch (error) { //如果失败提示 alert(error.message); } },
由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。
我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。
actions扩展
官网的教程,一个标准的actions函数如下所示:
deleteAllCheckedById(context) {
console.log(context)
}
context中包含commit、dispatch、getters、state,之前我们只在actions函数中使用过commit,事实上也可以使用dispatch、getters和state
这样我们的批量删除就简单了,对应的actions函数代码让如下
//删除全部勾选的产品 deleteAllCheckedCart({ dispatch, getters }) { //context:小仓库,commit【提交mutations修改state】 getters【计算属性】 dispatch【派发action】 state【当前仓库数据】 //获取购物车中全部的产品(是一个数组) let PromiseAll = []; getters.cartList.cartInfoList.forEach((item) => { let promise = item.isChecked == 1 ? dispatch("deleteCartListBySkuId", item.skuId) : ""; //将每一次返回的Promise添加到数组当中 PromiseAll.push(promise); }); //只要全部的p1|p2....都成功,返回结果即为成功 //如果有一个失败,返回即为失败结果 return Promise.all(PromiseAll); },
上面代码使用到了Promise.all
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
ShopCart购物车组件method批量删除函数
//删除全部选中的产品
//这个回调函数咱门没办法手机到一些有用数据
async deleteAllCheckedCart() {
try {
//派发一个action
await this.$store.dispatch("deleteAllCheckedCart");
//再发请求获取购物车列表
this.getData();
} catch (error) {
alert(error.message);
}
},
修改商品的全部状态和删除多个商品的原理相同
src\store\shopcart.js
//修改购物车某一个产品的选中状态 async updateCheckedById({ commit }, { skuId, isChecked }) { let result = await reqUpdateCheckedByid(skuId, isChecked); if (result.code == 200) { return "ok"; } else { return Promise.reject(new Error("faile")); } }, //修改全部产品的状态 updateAllCartIsChecked({ dispatch, state }, isChecked) { //数组 let promiseAll = []; state.cartList[0].cartInfoList.forEach((item) => { let promise = dispatch("updateCheckedById", { skuId: item.skuId, isChecked, }); promiseAll.push(promise); }); //最终返回结果 return Promise.all(promiseAll); },
app\src\pages\ShopCart\index.vue
//修改全部产品的选中状态
async updateAllCartChecked(event) {
try {
let isChecked = event.target.checked ? "1" : "0";
//派发action
await this.$store.dispatch("updateAllCartIsChecked", isChecked);
this.getData();
} catch (error) {
alert(error.message);
}
},
先获取验证码,再输入密码,再点击注册
app\src\store\user.js
//获取验证码 async getCode({ commit }, phone) { //获取验证码的这个接口:把验证码返回,但是正常情况,后台把验证码发到用户手机上【省钱】 let result = await reqGetCode(phone); if (result.code == 200) { commit("GETCODE", result.data); return "ok"; } else { return Promise.reject(new Error("faile")); } }, //用户注册 async userRegister({ commit }, user) { let result = await reqUserRegister(user); if (result.code == 200) { return "ok"; } else { return Promise.reject(new Error("faile")); } },
app\src\pages\Register\index.vue
//获取验证码 async getCode() { //简单判断一下---至少用数据 try { //如果获取到验证码 const { phone } = this; phone && (await this.$store.dispatch("getCode", phone)); //将组件的code属性值变为仓库中验证码[验证码直接自己填写了] this.code = this.$store.state.user.code; } catch (error) {} }, //用户注册 async userRegister() { const { phone, password, code } = this; try { phone && password && code && (await this.$store.dispatch("userRegister", { phone, password, code, })); //注册成功跳转到登陆页面,并且携带用户账号 await this.$router.push({ path: "/login", query: { name: this.phone }, }); } catch (error) { alert(error); } },
用户登录时,会向服务器发请求(组件派发action:userLogin),登录成功的话服务器就会返回一个token,将token储存在vuex里面
app\src\pages\Login\index.vue
//登录的回调函数
async userLogin() {
try {
//登录成功
const { phone, password } = this;
phone &&
password &&
(await this.$store.dispatch("userLogin", { phone, password }));
//登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
// let toPath = this.$route.query.redirect||"/home";
this.$router.push("/home");
} catch (error) {
alert(error.message);
}
},
服务器返回token字段 ,将token保存在vuex里面
app\src\store\user.js
USERLOGIN(state, token) { state.token = token; }, //登录业务 async userLogin({ commit }, data) { let result = await reqUserLogin(data); console.log(result, 'result'); //服务器下发token,用户唯一标识符(uuid) //将来经常通过带token找服务器要用户信息进行展示 if (result.code == 200) { //用户已经登录成功且获取到token commit("USERLOGIN", result.data.token); //持久化存储token // setToken(result.data.token); return "ok"; } else { return Promise.reject(new Error("faile")); } },
(token代表一个用户的身份,不同token获取不同的用户信息)
这时,我们只是将token保存在仓库,还需要将token添加到请求头,这样就可以获取用户信息
app\src\api\request.js
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情
request.interceptors.request.use((config) => {
if (store.state.detail.uuid_token) {
//请求头添加一个字段(userTempId):和后台老师商量好了
config.headers.userTempId = store.state.detail.uuid_token;
}
if (store.state.user.token) {
//请求头添加一个字段(token)
config.headers.token = store.state.user.token;
}
// 进度条开始
nprogress.start();
// config:配置对象,对象里面有一个属性很重要,headers请求头
return config;
});
当跳转到首页时,请求头已经添加token字段,所以发请求可以获取到用户信息,将用户信息展示在首页
app\src\pages\Login\index.vue
mounted() {
this.$store.dispatch("getFloorList")
// 获取用户信息在首页展示
this.$store.dispatch("getUserInfo")
},
app\src\store\user.js
GETUSERINFO(state, userInfo) {
state.userInfo = userInfo;
},
//获取用户信息
async getUserInfo({ commit }) {
let result = await reqUserInfo();
if (result.code == 200) {
//提交用户信息
commit("GETUSERINFO", result.data);
return 'ok';
} else {
return Promise.reject(new Error('faile'));
}
},
但是vuex储存数据不是持久化的 ,一旦刷新页面,vuex里面的数据就没了,即token也会清空,这样就没有token去发请求获取用户信息
因此我们需要持久化储存token
获取到token后,将token保存在本地(localStorage),点击刷新也可以在本地获取token
代码如下:
app\src\store\user.js
//登录与注册的模块 const state = { code: "", token: getToken(), userInfo: {}, }; USERLOGIN(state, token) { state.token = token; }, //登录业务 async userLogin({ commit }, data) { let result = await reqUserLogin(data); console.log(result, 'result'); //服务器下发token,用户唯一标识符(uuid) //将来经常通过带token找服务器要用户信息进行展示 if (result.code == 200) { //用户已经登录成功且获取到token commit("USERLOGIN", result.data.token); //持久化存储token setToken(result.data.token); return "ok"; } else { return Promise.reject(new Error("faile")); } },
app\src\utils\token.js
//存储token
export const setToken = (token) => {
localStorage.setItem("TOKEN", token);
};
//获取token
export const getToken = () => {
return localStorage.getItem("TOKEN");
};
//清除本地存储的token
export const removeToken=()=>{
localStorage.removeItem("TOKEN");
}
这样点击刷新页面,vuex里面的token也不会被清空,实现了持久化储存token
但是目前还是存在一些问题的
token已经持久化储存,但是用户信息没有持久化储存,一刷新用户信息就会被清空
// 获取用户信息在首页展示
this.$store.dispatch("getUserInfo")
因为只有在首页有上面这一句代码,才可以获取到用户信息
导航守卫
全局守卫: 只要发生路由变化,守卫就可以监听到
举例子:紫禁城【皇帝、太后、妃子】,紫禁城大门守卫,全要排查
全局前置路由守卫(比较常用)
有三个参数
( path 前面肯定有/ 例子:/login /home)
问题:
之前提到过,在首页之外的页面点击刷新,无法获取用户信息,因为其他页面没有派发action去获取用户信息,所以我们通过使用前置路由守卫来解决这个问题
解决方法:
在用户已经登录的情况下(访问的是非登录与注册),在每次路由跳转之前,判断一下是否拥有用户信息,如果没有用户信息,就先去派发action获取用户信息再放行
获取用户信息需要token,如果token失效,就需要重新登录获取并保存token
app\src\router\index.js
// 全局守卫: 前置守卫(路由跳转之前进行判断) router.beforeEach(async (to, from, next) => { // to:获取到要跳转到的路由信息 // from:获取到从哪个路由跳转过来来的信息 // next: next() 放行 next(path) 放行 // console.log(to); // console.log(from); let token = store.state.user.token; let name = store.state.user.userInfo.name; // 用户已经登录 if (token) { // path 前面肯定有/ 例子:/login /home // 用户已经登录还想去login【不能去,停留在首页】 if (to.path == '/login') { next('/home') } else { //已经登陆了,访问的是非登录与注册 //登录了且拥有用户信息放行 if (name) { next(); } else { //登陆了且没有用户信息 //在路由跳转之前获取用户信息且放行 try { await store.dispatch('getUserInfo'); next(); } catch (error) { //token失效从新登录 await store.dispatch('userLogout'); next('/login') } } } } else { // 未登录 next() } })
用户未登录时,不能去交易、支付相关【pay|paysuccess】、个人中心
如果点击前往这些页面(例子:pay页面),首先会跳转到登录页面,并把未去成的信息存储在地址栏中,登陆之后就跳转到该页面(例子:pay页面)
app\src\router\index.js
else {
//未登录:不能去交易相关、不能去支付相关【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();
}
}
登录的回调函数里面,需要判断一下路由当中是否包含query参数
有query参数跳转到指定的路由,没有就跳转到home
app\src\pages\Login\index.vue
//登录的回调函数
async userLogin() {
try {
//登录成功
const { phone, password } = this;
phone &&
password &&
(await this.$store.dispatch("userLogin", { phone, password }));
//登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
let toPath = this.$route.query.redirect||"/home";
this.$router.push(toPath);
} catch (error) {
alert(error.message);
}
},
举例子:紫禁城【皇帝、太后、妃子】,是相应的【皇帝、太后、妃子】路上的守卫(只排查自己负责的皇帝或太后或妃子,而且是跳转路上的)
如果想跳转支付页面,必须是从交易页面跳转过来的
app\src\router\routes.js
{ path: '/pay', component: Pay, meta: { show: true }, // 路由独享守卫 beforeEnter: (to, from, next) => { // 去支付页面,必须是从交易页面而来 if (from.path == '/trade') { next() } else { next(false) // console.log('不111可以跳转'); } } },
如果要跳转交易页面,必须是从购物车跳转过来的
app\src\router\routes.js
{ path: '/trade', component: Trade, meta: { show: true }, // 路由独享守卫 beforeEnter: (to, from, next) => { // 去交易页面,必须是从购物车页面而来 if (from.path == '/shopcart') { next() } else { next(false) // console.log('不111可以跳转'); } } },
组件内守卫: 要去皇帝屋子
举例子:已经来到皇帝屋子外面了(进入了)的守卫
有三种情况:
app\src\pages\PaySuccess\index.vue(进入Paysuccess前调用)
name: "PaySuccess",
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
if (from.path == "/pay") {
next();
} else {
next(false);
}
},
app\src\pages\PaySuccess\index.vue
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
console.log("12313131311313");
},
app\src\pages\PaySuccess\index.vue
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
next();
},
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
原本路由是这样的
import AddCartSuccess from '@/pages/AddCartSuccess'
{
path: '/addcartsuccess',
component: AddCartSuccess,
},
路由懒加载:
const UserDetails = () => import('./views/UserDetails')
{
path: '/users/:id',
component: UserDetails
}
更简洁的写法:
{
path: "/register",
component: () => import('@/pages/Register'),
},
map文件可以准确输出哪一行哪一列报错,但是对于项目上线无意义
在vue.config.js设置项目打包时去掉map文件
module.exports = {
// 打包时去掉map文件
productionSourceMap:false,
// 关闭ESLINT校验工具
lintOnSave: false,
//配置代理跨域
devServer: {
proxy: {
"/api": {
target: "http://39.98.123.211",
},
},
},
};
然后将项目打包好
npm run build
购买CentOS的服务器,比较好用
新建安全组,开放服务器的一下端口号22,80,443,3389
下面需要使用Xftp 和 Xshell,在下面这个链接下载
Xftp 和 Xshell免费下载
创建会话
把本地的dist复制到Xftp创建的会话中去
创建会话
连接成功如下图所示:
切换到nginx目录
cd / => cd etc => cd nginx
vim nginx.conf
INSERT进入编辑模式,添加以下内容
esc退出编辑,:wq保存编辑的内容
XSHELL7启动nginx服务器
systemctl start nginx.service
具体指令介绍:
#启动nginx服务
systemctl start nginx.service
#停止nginx服务
systemctl stop nginx.service
#重启nginx服务
systemctl restart nginx.service
#重新读取nginx配置(这个最常用, 不用停止nginx服务就能使修改的配置生效)
systemctl reload nginx.service
这样就可以通过自己的服务器的ip地址访问尚品汇项目
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。