目录
1:搭建vue-cli脚手架
2:对vue文件的一些认识
3写好各组件的的样式,结构
4:路由组件的搭建vue-router
4.1:配置路由
4.2小结
4.3路由跳转的形式
4.4路由传参
4.4全局组件的注册与使用
5:对axios进行二次封装
6:nprogress进度条的使用
7:vuex的使用
7.1:vuex实现模块式开发,其实就是在store里面为每个模块建一个小仓库
8动态渲染三级联动的数据
9节流与防抖:
10:三级联动的路由跳转以及参数的传递。
11:三级联动在不同组件中实现显示与隐藏
11.1显示与隐藏的动画实现
12:利用swiper组件写轮播图
13:Search模块的开发。
14:getter()方法简化获取数据
15:面包屑相关问题
15.1:组件间的通信
15.2:子向父传递数据
16 排序模块
17:Detail详情模块的开发(步骤:静态组件,路由,请求接口数据,传递参数)
17.1引入静态组件
17.2路由模块
17.3获取api中的数据,之前类似的写过很多次了,这里就简单介绍下步骤
17.4动态展示数据
17.5点击谁,谁高亮的效果
17.6 放大镜
18:购物车模块
19购物车列表模块
20登录注册业务
20.1注册模块
20.2登录模块
21交易模块
22支付模块
22.1生成二维码
23个人中心
24图片懒加载vue-lazyload
25:表单验证
26:路由懒加载
27打包文件(前台项目完结撒花!!!)
28:购买服务器,发布项目
首先要确定我们电脑有安装node,webpack,以及淘宝镜像cnpm
搭建vue-cli脚手架初始化项目
创建一个名为app的vue项目
接着选择vue2,等待下载
关闭eslint校验工具,比如我们申明一个变量但是未使用,这时vue工具会给我们报错,关闭此校验就不会有这个问题。
在根目录下创建vue.config.js文件
module.exports = {
// 关闭ESLINT校验工具
lintOnSave: false,
};
可以给src配置别名为@,因为后期项目经常会用到src,查找的时候也方便
具体方法,在jsconfig.json里面配置,代码如下
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}
(此处省略一万行代码)
先安装vue-router ,在黑窗口中输入cnpm install --save vue-router
路由组件一般放在pages文件夹里面
然后创建router文件夹,对路由组件进行管理
// 配置路由
import Vue from 'vue';
import VueRouter from 'vue-router';
// 使用插件
Vue.use(VueRouter);
// 引入路由组件
import Home from '@/pages/Home'
import Search from '@/pages/Search'
import Login from '@/pages/Login'
import Register from '@/pages/Register'
export default new VueRouter({
// 配置路由
routes: [
{
path: "/home",
component: Home
},
{
path: "/search",
component: Search
},
{
path: "/login",
component: Login
},
{
path: "/register",
component: Register
}
}
]
})
再回到文件的入口文件中引入路由
// 引入路由
import router from '@/router'
new Vue({
render: h => h(App),
//注册路由
router
}).$mount('#app')
最后回到app.vue中展示路由(使用router-view)
路由组件与非路由组件的区别?
1:路由组件一般放置在pages|views文件夹,非路由组件一般放置components文件夹中
2:路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以标签的形式使用
3:注册完路由,不管路由路由组件、还是非路由组件身上都有$route、$router属性
$route:一般获取路由信息【路径、query、params等等】
$router:一般进行编程式导航进行路由跳转【push|replace】
声明式导航和编程式导航
声明式导航:
//to代表要去的地方
登录
编程式导航:
methods: {
// 向Search路由进行跳转
goSearch() {
//按钮中的的gosearch方法
this.$router.push("/search")
}
}
路由原信息meta
例如
routes: [
{
path: "/home",
component: Home,
meta: {
show: true
}
},
访问时可以通过$route.meta.show
goSearch() {
// 路由传递参数:
// 第一种:字符串形式
// this.$router.push("/search/"+this.keyword+"?k="+this.keyword.toUpperCase());
// 第二种:模板字符串
// this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()})`);
// 第三种:对象(最常用)
// 对象形式传参要写name!!
this.$router.push({
name: "search",
params: { keyword: this.keyword },
query: { k: this.keyword.toUpperCase() },
});
},
},
params传参要进行占位
// 传params参数要进行占位,问号代表params参数可传可不传
path: "/search/:keyword?",
import TypeNav from '@/components/TypeNav'
// 第一个参数 全局组件的名字 第二个参数 哪一个组件
Vue.component(TypeNav.name, TypeNav)
使用时直接写标签,不需要引入了。
向服务器发请求的方法有:
XMLHttpRequest、fetch、JQ、axios(最常用)
为什么需要进行二次封装axios?
请求拦截器、响应拦截器:请求拦截器,可以在发请求之前可以处理一些业务、响应拦截器,当服务器数据返回以后,可以处理一些事情。
通常在src下新建api文件夹,然后在request.js文件中对axios进行二次封装
// 对axios进行二次封装
import axios from 'axios';
// 1 利用axios对象的方法create,去创建一个axios实例
// 2:request就是axios,只不过稍微配置一下
const requests = axios.create({
// 配置对象
// 基础路径,发请求的时候,路径当中会出现api
baseURL: "/api",
// 请求超时的时间
timeout: 5000
});
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
// interceptors拦截
requests.interceptors.request.use((config) => {
// config:配置对象,独享里面有一个属性很重要,包含header请求头
return config;
});
// 响应拦截器
requests.interceptors.response.use((res) => {
//成功的回调函数:服务器响应的数据回来后,响应拦截器可以检测到,可以做一些事情
return res.data;
}, (error) => {
// 响应失败的回调函数
// 比如终结promise
return Promise.reject(new Error('fail'));
});
export default requests;
当我们需要向后台发送数据时,可以在api文件夹下的index.js文件写发送请求的代码
// 当前这个模块: 对API进行统一的管理
import requests from './request';
// 三级联动接口 api/product/getBaseCategoryList get方法 无参数
export const reqCategoryList = () => {
// 发请求 axios发请求返回结果是promise对象
return requests({ url: '/product/getBaseCategoryList', method: 'get' });
}
在main.js中引入,并测试能否从后台服务器获取到数据
import { reqCategoryList } from '@/api'
reqCategoryList()
测试后我们发现会遇到跨域的问题,跨域就是协议,域名,端口号不同的请求。
一般的解决方法有jsonp,cros,代理
这里我们用代理的方法来解决
我们在项目中的vue.config.js文件中引入如下代码段
// 解决跨域 webpack devserve里面赋值一段代码
// 这里是代理跨域
devServer: {
proxy: {
'/api': {
target: 'http://39.98.123.211',
// pathRewrite: { '^/api': '' },
},
},
}
安装nprogress插件 cnpm install --save nprogress
我们在二次封装axios的文件中去引入nprogress
当请求拦截器捕获到请求的时候,进度条开始动
当服务器返回数据成功后进度条结束
// 引入进度条
import nprogress from 'nprogress'
// 引入进度条样式
import "nprogress/nprogress.css"
// start 进度条开始 done: 进度条结束
const requests = axios.create({
baseURL: "/api",
timeout: 5000
});
// interceptors拦截
requests.interceptors.request.use((config) => {
// 进度条开始动
nprogress.start()
return config;
});
requests.interceptors.response.use((res) => {
// 进度条结束
nprogress.done();
return res.data;
}, (error) => {
return Promise.reject(new Error('fail'));
});
效果图
进度条颜色可以在nprogress.css文件中修改
vuex是什么?vuex是官方提供的一个插件,状态管理库,集中式管理项目中组件共用的数据,但并不是所有的项目都需要vuex,项目小就不需要。
首先安装vuex: cnpm install --save vuex
这里我们需要注意安装vuex的版本,我之前下的是4.0.2的版本导致崩了,下回3的版本才解决错误。。。。。
vuex的使用:
在store文件夹中,index.js代码如下:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
// state:仓库存储数据的地方
const state = {};
// mutations:修改state的唯一手段
const mutations = {};
// action:处理action,可以书写自己的业务逻辑,也可以处理异步任务
const actions = {};
// getters 理解为计算属性,用于简化仓库数据的,让仓库获取数据更加简单
const getters = {};
// 对外暴露Store类的一个实例
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
同样,需要在入口文件中引入并注册。
// home模块的小仓库
// state:仓库存储数据的地方
const state = {
a: 1
};
// mutations:修改state的唯一手段
const mutations = {};
// action:处理action,可以书写自己的业务逻辑,也可以处理异步任务
const actions = {};
// getters 理解为计算属性,用于简化仓库数据的,让仓库获取数据更加简单
const getters = {};
export default {
state,
mutations,
actions,
getters
}
在store里面暴露一下就好啦
// 对外暴露Store类的一个实例
export default new Vuex.Store({
modules: {
home,
search
}
})
三级联动的组件是TypeNav,首先在TypeNav组件中向vuex发送请求
// 组件挂载完毕,可以向服务器发送请求
mounted() {
// 通知vuex发送请求,将数据存储于仓库中
this.$store.dispatch("categoryList");
},
由于是在home模块,所以找到home模块的小仓库,对actions,mutations,state函数进行完善
// home模块的小仓库
import { reqCategoryList } from '@/api'
const state = {
categoryList: []
};
const mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList = categoryList
}
};
const actions = {
//通知API里面的接口函数,向服务器发送请求,获取服务器的数据
async categoryList({ commit }) { //解构commit
let result = await reqCategoryList();
if (result.code == 200) {
commit("CATEGORYLIST", result.data) //向mutations发送数据
}
}
};
最后回到TypeNav中即可获取到数据
computed: {
...mapState({
// 注入了一个参数,即为大仓库中的数据
categoryList: (state) => {
return state.home.categoryList
},
}),
},
然后在上面进行渲染就ok啦
{{ c1.categoryName }}
三级联动的动态背景:
首先给元素绑定一个鼠标经过触发事件 @mouseenter="changeindex(index)"
函数为changeindex(index) { this.currentIndex = index;}
然后通过判断给盒子动态添加样式(cur为自定义样式)
最后记得定义鼠标离开事件mouseleave。
9节流与防抖:
节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发 只会执行一次节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发。
简单来说就是
防抖:用户操作很频繁,但是只是执行一次
节流:用户操作很频繁,但把频繁的操作变为少量操作(可以给浏览器有充裕的时间解析代码)
三级联动的节流:使用node包里面的lodash
1 引入lodash: import _ from 'lodash'
但是最好按需引入
import { throttle } from "lodash";
//下面是es5的写法 为了写节流函数
changeindex: throttle(function (a) {
this.currentIndex = a;
}, 50),
throttle函数不要用箭头函数,防止出现this指向问题
10:三级联动的路由跳转以及参数的传递。
①不能用声明式导航,声明式导航会出现卡顿现象,因为router-link是一个组件,当服务器返回数据时会循环出很多组件实例,导致卡顿。所以我们使用编程式导航,结合事件委派来写。
存在一些问题:
①事件委派,是把全部的子节点【h3、dt、dl、em】的事件委派给父亲节点
②点击a标签的时候,才会进行路由跳转【怎么能确定点击的一定是a标签】
③存在另外一个问题:即使你能确定点击的是a标签,如何区分是一级、二级、三级分类的标签。
我们可以给每一个子节点当中a标签加上自定义属性data-categoryName,其余的子节点是没有的
例如 :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId"
goSearch(event) {
//获取节点
let element = event.target;
//节点有一个属性dataset属性,可以获取节点的自定义属性与属性值
//注意自定义属性会自动变成小写!
let { categoryname,category1id,category2id,category3id }=element.dataset;
if (categoryname) {
// 整理路由跳转的参数
let location = { name: "search" };
let query = { categoryName: categoryname };
//一级分类、二级分类、三级分类的a标签
if (category1id) {
query.catrgory1Id = category1id;
} else if (category2id) {
query.catrgory2Id = category2id;
} else {
query.catrgory3Id = category3id;
}
// 整理完参数
// console.log("12", location, query);
location.query = query;
// 路由跳转
this.$router.push(location);
}
},
},
11:三级联动在不同组件中实现显示与隐藏
在三级联动组件中利用mounted挂载来实现
if (this.$route.path != "/home") {
this.show = false;
}
再结合mouseenter与mouseleave来实现。
11.1显示与隐藏的动画实现
①在需要动画的地方包裹上transition标签(最好加上名字)
②写动画效果
// 开始状态
.sort-enter {
height: 0px;
// transform: rotate(0deg);
}
// 结束状态
.sort-enter-to {
height: 461px;
// transform: rotate(360deg);
}
// 定义动画的时间,速率
.sort-enter-active {
transition: all 0.5s linear;
}
性能优化:由于会多次用到TypeNav组件,导致会多次发送同样的请求,所以我们将请求放在根组件里面。即App.vue
mounted() {
// 通知vuex发送请求,将数据存储于仓库中
this.$store.dispatch("categoryList");
},
};
这样就只会调用一次。
12:利用swiper组件写轮播图
①安装swiper : cnpm install --save swiper@5
②引入 css和js文件和js代码段
import Swiper from'swiper'
import "swiper/css/swiper.css"
js代码段放在mounted里面。
但是这样做以后会发现没有效果,为什么呢?因为我们遇到了异步的问题
我们期待的顺序时①②③④,因为这样就能正常的渲染数据
而调试完才发现顺序是①②④③,
可以看到bannerlist数据还没回来,mounted就挂载完毕了,所以获取不到数据,关键就是dispatch方法,这是一个异步语句,导致v-for遍历的时候结构还不完全,所以我们需要解决这个问题。
我们需要用watch监听属性和$nextTick来解决这个问题
官方介绍nextTick:在下次DOM更新 循环结束之后 执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
当执行了handler方法,代表组件实例身上这个属性的属性更新了,因为一修改数据之后,watch就会检测到,然后使用nextTick方法,获取更新后的 DOM.当你执行这个回调的时候,服务器的数据回来了,v-for执行完毕了,所以轮播图的结构就一定有了。
watch: {
//监听bannerList数据的变化:因为这条数据发生过变化----由空数组变为数组里面有四个元素
bannerList: {
handler(newValue, oldValue) {
this.$nextTick(() => {
//swiper的模板
var mySwiper = new Swiper(
this.$refs.mySwiper,
{
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
}
);
});
},
},
},
props传递数据:
父组件Home传给子组件Floor数据
子组件接收:
floor模块继续使用swiper组件,但是这里却可以直接把js代码放在mounted中, 之前不能放在mounted中是因为在组件内部发送请求,动态渲染,这里的轮播图数据是父组件中传来的,所以可直接放在mounted中,不用使用watch+$nextTick。
mounted() {
// 之前不能放在mounted中,因为是在组件内部发送请求,动态渲染的,这里是父组件中传来的数据,所以可以直接放在mounted中,不用使用nextTick
var mySwiper = new Swiper(this.$refs.cur, {
// direction: "vertical", // 垂直切换选项
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
clickable: true,
},
// 如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
},
13:Search模块的开发。
在前面的学习中,我们基本可以了解开发一个模块的大概流程。
①静态页面和静态组件
②发送请求(API)
③vuex的三连环
④组件获取仓库数据,动态展示数据。
这里的第一步就先省略了,来到第二步的api
现在API中发送请求,获取数据。
通过查阅API文档我们知道当前这个函数需要接收外部传递参数,要给服务器传送一个默认参数(至少是一个空对象),所以在api/index.js文件下的代码如下:
export const reqGetSearchInfo = (params) => requests({ url: "/list", method: "post", data: params })
还要再main.js中引入
import { reqGetSearchInfo } from '@/api'
③vuex三连环
第一步在search仓库中写以下代码
第二步在Search组件中派发getSearchList方法
vuex三连环比较简单,就不过多讲解了。
14:getter()方法简化获取数据
之前我们用mapState获取数据时比较繁琐,需要state.仓库名.数据.属性来获取数据。
import {mapState} from 'vuex'
.....
computed{
...mapState({
goodsList:state=>state.search.searchList.goodsList
})
}
所以使用getter可来简化仓库中的数据
首先在仓库中使用getter方法:
const getters = {
goodsList(state) {
//这里需要定义一个空数组是因为加入当前网络不给力的话,goodlist返回的是undefined,就会遍历出错
return state.searchList.goodsList || [];
}
};
然后再回到组件中调用以下就好了
import { mapGetters } from "vuex";
.......
computed: {
...mapGetters(["goodsList"]),
},
因为getters是全局属性,所以数据部分模块,我们直接...mapGetters["数据名"]就能获取数据。
下一步,根据不同的参数获取数据并渲染
这是我们需要的数据
我们在组件挂载之前获取一次数据,把接口需要的参数进行整理,这里我们用到了ES6新增的语法,用Object.assign来合并对象
//当组件挂载完毕之前执行一次【先与mounted之前】
beforeMount() {
//复杂的写法
// this.searchParams.category1ld =this.$route.query.categorylld;
// this.searchParams.category2Id =this.$route.query.category2Id;
// this.searchParams.category3Id =this.$route.query.category3Id;
// this.searchParams.categoryName = this.$route.query.categoryName;
// this.searchParams.keyword = this.$route.params.keyword;//0bject.assign:ES6新增的语法,合并对象
// Object.assign:ES6新增语法,合并对象
Object.assign(this.searchParams, this.$route.query, this.$route.params);
},
之前放在mounted里面的发送数据方法,要放到methods里面,方便调用
mounted() {
// 发请求之前把参数带给服务器
this.getData();
},
methods: {
getData() {
this.$store.dispatch("getSearchList", this.searchParams);
},
},
18.2:search模块优化
上面的方法只能搜索一次,这里我们使用watch监测数据,一旦路由的参数改变我们就再次发送请求。
watch: {
$route(newValue, oldValue) {
// 再次发送请求之前整理好带给服务器的参数
Object.assign(this.searchParams, this.$route.query, this.$route.params);
// 再次发起ajax请求
this.getData();
},
},
15:面包屑相关问题
这些带有‘X’的我们称为面包屑,进入分类界面或者输入关键字进行搜索时,都会出现面包屑,且分类会带有params参数,关键字搜索带有query参数,所以只要参数发生变化,watch就能监测到,从而发起ajax请求。
当我们点击‘X’时,要调用函数来删除面包屑,同时重新渲染页面,让网页的参数变回之前的样子,这时候我们就需要调用一个自定义函数,具体代码如下:
{{ searchParams.categoryName}}x
// 删除分类的名字
removeCategoryName() {
// 把带给服务器的参数置空后,还需向服务器发送请求
// 带给服务器的参数可有可无:应该把字段变为undefined,变为空的话还会带给服务器,而undefined不会
this.searchParams.categoryName = undefined;
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
this.getData();
// 地址栏也需要修改:可以用路由跳转的方法(跳到自己原来的路由)
// 但是这个方法不够严谨,这样会把params和query参数都置空,而这里params不需要置空,应该这么写:
if (this.$route.params) {
this.$router.push({ name: "search", params: this.$route.params });
}
},
15.1:组件间的通信
在搜索时,我们的关键字会出现在面包屑上,当我们点击删除面包屑,也要使搜索栏的关键字置空,我们知道搜索栏是属于Header组件的,而面包屑是属于Search组件的,这时候就需要用到组件间的通信,$bus
首先我们在main.js中配置全局事件总线
然后在Search模块中通知Header组件去触发函数
最后在Hearer模块中的mounted挂载里面实现关键字的清除,组件挂载时就监听clear事件
15.2:子向父传递数据
父组件绑定一个自定义函数
子组件中定义一个函数并传递数据
子组件中触发父组件的函数传递数据
父组件中自定义事件的测试
这样就完成了子向父传递数据,当我们点击子组件时,会向父组件传递数据,并得到数据
点击苹果(一个子组件)
可以在父组件中得到数据
最终我们是要在父组件中获取子组件的一些参数
品牌的面包屑删除也是同理
{{ searchParams.trademark.split(":")[1]}}
x
removeTradeMark() {
// 将品牌信息置空
this.searchParams.trademark = undefined;
// 再次发送请求
this.getData();
},
search模块基本结束
16 排序模块
先在阿里图标找到我们需要的图标,生成代码,复制到public文件夹中,在index文件里面引入,记得加https:
按钮的升序降序
data(){
return{
.......
// 排序
order: "1:asc",
}
}
//排序的操作
changeOrder(flag) {
//这里获取到的是最开始的状态
let originFlag = this.searchParams.order.split(":")[0];
let orginSort = this.searchParams.order.split(":")[1];
//准备一个新的order属性值
let newOrder = "";
// 若点击的是同一个按钮,改变升序和降序
if (flag == originFlag) {
newOrder = `${flag}:${orginSort == "desc" ? "asc" : "desc"}`;
// newOrder = `${originFlag}:${orginSort == "desc" ? "asc" : "desc"}`;
}
else {
//不是同一个按钮,取传入的按钮,然后默认为降序
newOrder = `${flag}:${"desc"}`;}
//将新的order赋了searchParams
this.searchParams.order = newOrder;
//再次发请求
this.getData();
}
17:Detail详情模块的开发(步骤:静态组件,路由,请求接口数据,传递参数)
17.1引入静态组件
17.2路由模块
①引入路由
import Detail from '@/pages/Detail'
②配置路由
routes: [
{
//params参数占位
path: "/detail/:skuid",
component: Detail,
meta: {
show: true
}
},
我们从search页面跳转到detail页面时,要传递参数,我们的效果是点击图片进入详情页面,所以这里我们可以借用声明式路由导航跳转,并传递参数。
当路由配置信息太多的时候,我们可以新建一个路由配置文件routes.js,再引入router文件夹下的index.js文件。
当我们从一个路由跳到另一个路由时,如果我们想要滚动条置顶,我们可以在路由管理文件中加入scrollBehavior函数
export default new VueRouter({
// 配置路由
routes,
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { y: 0 }
}
})
17.3获取api中的数据,之前类似的写过很多次了,这里就简单介绍下步骤
①在api文件中获取详情的数据
export const reqGoodsInfo = (skuId) => requests({ url: "/item/${`skuId`}", method: "get" })
②在仓库中获取数据
import { reqGoodsInfo } from '@/api';
const state = {
goodInfo: {},
};
const mutations = {
GETGOODINFO(state, goodInfo) {
state.goodInfo = goodInfo
}
};
const actions = {
async getGoodInfo({ commit }, skuid) {
let result = await reqGoodsInfo(skuid)
if (result.code == 200) {
commit('GETGOODINFO', result.data)
}
}
};
const getters = {};
export default {
state,
mutations,
actions,
getters
}
在挂载时派发请求
mounted() {
this.$store.dispatch("getGoodInfo", this.$route.params.skuid);
},
17.4动态展示数据
用getter来简化数据
const getters = {
categoryView(state) {
return state.goodInfo.categoryView
}
};
获取数据
import { mapGetters } from "vuex";
.....
computed: {
...mapGetters(["categoryView"]),
},
渲染数据
{{categoryView.category1Name}}
{{categoryView.category2Name}}
{{categoryView.category3Name}}
这样写数据能够显示出来,但是我们发现会有报错,
原因其实很简单,因为第一次获取goodInfo时,数据是空的,但是我们还继续获取了goodInfo.categoryView,得到的数据是undefined,所以会报错,我们应该加一个空对象{}
17.5点击谁,谁高亮的效果
这里我们要用到排他思想,要获取到当前点击的选项的数组,还有当前选项,然后把当前数组的ischeck全部换成非选中状态,然后令当前点击的选项设置为选中状态即可。
17.6 放大镜
先完成轮播图,之前做过,这里简单回顾一下,先引入swiper文件,再引入swiper样式,然后将js代码写入watch中(为什么不写入mounted之前有说过)
watch: {
// 监听数据:可以保证数据一定ok,但不能保证v-for循环是否完成,所以需要$nextTick来等待v-for完成
skuImageList(newValue, oldValue) {
// $nextTick() 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
this.$nextTick(() => {
new Swiper(this.$refs.cur, {
loop: true, // 循环模式选项
// 如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
});
},
接下来用js实现点击图片添加背景,之前也写过类似的
首先默认选中第一张
然后写判断和点击事件
最后实现点击事件
methods: {
changeCurrentIndex(index) {
this.currentIndex = index;
},
}
接下来实现点击下面轮播图图片,上面大图也跟着变
因为这涉及到两个兄弟组件传递数据,所以要使用$bus,之前也实现过类似的功能
通知兄弟组件
兄弟组件接收数组,在mounted中接收即可
mounted() {
// 全局事件总线,获取兄弟组件传递过来的索引值
this.$bus.$on("getIndex", (index) => {
//修改当前响应式数据
this.currentIndex=index
});
},
放大镜移动效果,这里就不解释了
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;
//修改元素的left|top属性值
mask.style.left = left + "px";
mask.style.top = top + "px";
big.style.left = -2 * left + "px";
big.style.top = -2 * top + "px";
},
18:购物车模块
我们需要向购物车的借接口发送请求,但是这里我们并不需要从接口中获取数据
第一步仍然是在api中写好接口
export const reqAddOrUpdateShopCart = (skuId,skuNum) =>
requests({ url: `/cart/addToCart/${ skuId }/${ skuNum }`, method: "post"})
因为是在Detail文件中实现购物车功能,所以在detail的vuex文件中发送请求获取数据
// 将产品添加到购物车中
async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
// 加入购物车返回的结果
let result = await reqAddOrUpdateShopCart(skuId, skuNum)
console.log(result);
}
当点击“加入购物车”按钮时,我们派发请求,
// 加入购物车的回调函数
addShopcar() {
// 1发请求--将产品加入数据库(通知服务器)
this.$store.dispatch("addOrUpdateShopCart",
{skuId: this.$route.params.skuid,skuNum: this.skuNum,});
// 2存储成功,进行路由跳转
// 3存储失败,给用户提示
},
这里我们并不需要vuex三连环,因为服务器并没有返回数据,我们只要成功发送请求就好。
接下来我们要进行加入购物车成功与失败的判断及相关操作
先回到仓库中进行成功与失败的判断
再回到点击加入购物车的函数中,我们要知道,我们派发的函数最后得到的结果一定是一个promise,因为我们要调用的函数带有async,所以在addShopcar函数中,我们要等待vuex中addOrUpdateShopCart函数返回结果,所以加上await等待promise返回结果
将result进行打印,如果成功就会打印ok,失败会打印faile
所以这里我们用try catch 来对不同结果进行处理
利用会话存储进行参数传递
我们在进行路由跳转并把产品信息带给下一级路由组件时,简单的参数可以通过query参数带过去,但是复杂的数据要通过会话存储传递(不持久话,会话结束数据就消失)。
本地存储/会话存储 一般存储的是字符串
获取数据:
之后再根据数据渲染就可以了
19购物车列表模块
还是一样的操作,写好路由,在api文件中获取接口,组件挂载派发action请求,建小仓库vuex三连环。
首先封装请求函数
export const reqCartList = () => requests({ url: '/cart/cartList', method: 'get' })
因为这里我们要访问API获取详细信息,所以需要验证身份,我们用一个uuidToken来验证,
我们建一个utils文件夹专门放uuid_token,
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;
}
上面这样写是利用本地存储生成一个唯一的id,然后在detail文件下获取uuid_token
import { getUUID } from '@/utils/uuid_token'
const state = {
goodInfo: {},
// 游客临时身份
uuid_token: getUUID()
};
接着我们在请求拦截器中获取uuid_token
在api/requests.js文件中首先引入store仓库
import store from '@/store'
然后再请求拦截器中写以下代码(if语句)
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
// interceptors拦截 config是配置对象
requests.interceptors.request.use((config) => {
if (store.state.detail.uuid_token) {
// 请求头添加一个字段userTempId(该变量是项目中定义好的)
config.headers.userTempId = store.state.detail.uuid_token;
}
nprogress.start()
// config:配置对象,独享里面有一个属性很重要,包含header请求头
return config;
});
可以在请求头中成功找到数据
修改购物车中商品数量:
首先给两个按钮和输入框绑定同一个事件handler,分别传指令,数字和参数
-
+
handler函数:
async handler(type, disNum, cart) {
switch (type) {
case "add": disNum = 1; break;
// 如果是减,要判断产品的个数是否小于1,大于1才传给服务器,小于1就传0
case "minus":
disNum = cart.skuNum > 1 ? -1 : 0; break;
}
try {
//等待返回结果,用await,响应的handler要加async
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: cart.skuId,
skuNum: disNum,
});
//再次发送请求
this.getData();
} catch (error) {}
},
这样的话看似没有问题,但是如果用户疯狂点击减按钮,数量会变成负数,所以我们要使用节流,就是限制单位时间内点击的次数。 引入一下节流的文件然后稍微修改一下函数即可
import { throttle } from "lodash";
handler:throttle (async function (type, disNum, cart) {
......
},500),
删除购物车产品
①封装接口
export const reqDeleteCartById = (skuId) => requests({
url: `/cart/deleteCart/${skuId}`, method: 'delete' })
②vuex:
const actions = {
async deleteCartListBySkuId({ commit }, skuId) {
let result = await reqDeleteCartById(skuId);
if (result.code == 200) {
return 'ok';
} else {
return Promise.reject(new Error('faile'));
}
}
};
vue文件中定义一个点击事件来触发请求xiuga
methods:{
async deleteCartById(cart) {
try {
//删除成功发送请求
await this.$store.dispatch("deleteCartListBySkuId", cart.skuId);
this.getData();
} catch (error) {
console.log(error.message);
}
},
}
修改产品的选中状态与删除购物车基本一致,就不再介绍了。
一次删除多个商品
由于该项目没有删除多个商品的API请求,所以我们只能多次调用删除单个商品的接口
我们在删除按钮派发一个dispatch,这里用到一个新的参数context,我们不妨看看context内容是什么(默认传递的参数就是context)
deleteAllCheckedCart(context) {
console.log(context);
}
可以看到有dispatch,getters,commit等,我们可以理解context为一个小仓库
然后我们在vuex文件中调用
deleteAllCheckedCart({ dispatch, getters }) {
// context可以理解为小仓库
let PromiseAll = [];
// 获取购物车中的全部产品(一个数组)
getters.cartList.cartInfoList.forEach(item => {
let promise = item.isChecked == 1 ?dispatch('deleteCartListBySkuId', item.skuId):'';
PromiseAll.push(promise);// 将每一次返回的Promise添加到数组中
});
// Promise.all([p1,p2,p3]) 函数:全部成功才返回成功,只要一个失败就返回失败
return Promise.all(PromiseAll)
}
// 删除全部选中商品
async deleteAllCheckedCart() {
try {
await this.$store.dispatch("deleteAllCheckedCart");
this.getData();
} catch (error) {
error.message;
}
},
这里用到一个专门用于promise的api,Promise.all ( [ p1 , p2 , p3] ),只有全部成功才返回成功。
修改全部商品的选中状态
与一次删除所有商品类似,修改全部商品选中状态也要调用修改单个商品的选中状态函数,
// 修改全部产品的状态
updateAllCartIsChecked({ dispatch, getters }, isChecked) {
let promiseAll = []
getters.cartList.cartInfoList.forEach((item) => {
let promise = dispatch('updateCheckedById', { skuId: item.skuId, isChecked })
promiseAll.push(promise)
})
return Promise.all(promiseAll)
}
在全选框定义点击事件updateAllCartChecked
// 修改全部商品的选中状态
async updateAllCartChecked(event) {
try {
let isChecked = event.target.checked ? "1" : "0";
await this.$store.dispatch("updateAllCartIsChecked", isChecked);
this.getData();
} catch (error) {
alert("error.message");
}
},
最后有一个小bug,就是点击全选按钮后再逐个删除商品,商品删除完后全选按钮依旧是选中状态,所以当商品个数为0时我们要使全选框为不勾选状态
20登录注册业务
注意:assets放置全部组件共用的静态资源
20.1注册模块
封装接口,建立仓库,调用接口,派发action
仓库中获取验证码和点击注册的action
const actions = {
// h获取验证码
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'))
}
}
};
这里用v-model来双向绑定数据
派发action,
这里解释一下 const { phone, code, password, password1 } = this;(参考某位大佬的)
const { phone, code, password, password1 } = this;
等于
const phone=this.phone
const phone=this.code
const passwored1=this.password1
const passwored1=this.password1
//用户注册
async userRegister() {
try {
const { phone, code, password, password1 } = this;
phone &&code &&password == password1 &&(await this.$store.dispatch("userRegister",
{phone,code,password,}));
this.$router.push("/login");
} catch (error) {
console.log(error.message);
}
},
20.2登录模块
action模块
//用户登录
async userLogin({ commit }, data) {
let result = await reqUserLogin(data)
// 服务器会下发一个token,是用户的位移标识符(类似之前的uuid)
// 将来经常通过token来找服务器要用户的信息进行展示
if (result.code == 200) {
commit("USERLOGIN", result.data.token)
return 'ok'
} else {
return Promise.reject(new Error("faile"))
}
}
登录函数
// 登录按钮
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);
}
},
在登录时会收到一个token,由于vuex不是持久化存储, 刷新一下就没有了,所以为了持久化保存token,要用到本地存储
action部分代码:
//用户登录
async userLogin({ commit }, data) {
let result = await reqUserLogin(data)
// 服务器会下发一个token,是用户的位移标识符(类似之前的uuid)
// 将来经常通过token来找服务器要用户的信息进行展示
if (result.code == 200) {
commit("USERLOGIN", result.data.token);
// 持久换存储token
localStorage.setItem('TOKEN', result.data.token)
return 'ok'
} else {
return Promise.reject(new Error("faile"))
}
},
state部分的代码
const state = {
code: '',
//获取本地存储的token
token: localStorage.getItem('TOKEN'),
// token: '',
userInfo: {}
};
成功登陆后获取用户信息
// 获取用户信息
async getUserInfo({ commit }) {
let result = await reqUserInfo()
console.log(result);
// 提交用户信息
if (result.code == 200) {
commit("GETUSERINFO", result.data)
return 'ok'
}
}
登录部分 还有很多未完善的地方,比如跳转到别的模块刷新一下,又不会显示登录人信息了
退出登录
action部分
// 退出登录
async userLogout({ commit }) {
let result = await reqLogout()
if (result.code == 200) {
commit('CLEAR');
return 'ok';
} else {
return Promise.reject(new Error('faile'))
}
}
函数部分(记得跳转回首页)
async logout() {
try {
await this.$store.dispatch("userLogout");
//回到首页,路由跳转
this.$router.push("/home");
} catch (error) {
console.log(error.message);
}
},
路由守卫
先简单复习一下各种路由守卫
21交易模块
这里仍然是前面的四个步骤:
①封装API
②建立小仓库写好vuex三件套
③dispatch派发请求
④数据的渲染
所以就不详细说明了,只讲一些讲得少的地方。
这里我们需要一个点击谁谁高亮的效果,所以用到排他思想
首先绑定点击事件
再写函数(address是当前数组的,addressInfo是整个数组)
changeDefault(address, addressInfo) {
// 排他思想,全部的default变为0,当前点击的变为1
addressInfo.forEach((item) => (item.isDefault = 0));
address.isDefault = 1;
},
下面的收货人信息也要根据我们所选择的信息改变
我们要用到find方法来选择数据
// 将来提交订单最终选中的地址
userDefaultAddress() {
// find查找数组中符合条件的元素返回,就为最终的结果
return this.addressInfo.find((item) => item.isDefault == 1)||{};
},
提交订单模块
我们依旧是封装好提交订单的API,但是假设我们不用vuex来做,那么应该怎么做呢?
首先在main.js文件中统一引入API文件
// 统一接收api文件夹里面的全部函数请求,统一引入
import * as API from '@/api'
然后挂载到原型对象身上
定义一个点击事件
这里就会用到$API
完整代码 (这里需要传的参数有点多,一定要细心)
// 提交订单
async submitOrder() {
// 交易编码
let { tradeNo } = this.orderInfo;
// 其余6个参数
let data = {
consignee: this.userDefaultAddress.consignee, //最终收件人名字
consigneeTel: this.userDefaultAddress.phoneNum, //手机号
deliveryAddress: this.userDefaultAddress.fullAddress, //收件人地址
paymentWay: "ONLINE", //支付方式
orderCommit: this.msg, //买家留言
orderDetailList: this.orderInfo.detailArrayList, //商品清单
};
// 需要带参数的tradeNo
let result = await this.$API.reqSubmitOrder(tradeNo, data);
if (result.code == 200) {
this.orderId = result.data;
// 路由跳转+路由传递参数
this.$router.push("/pay?orderId=" + this.orderId);
} else {
alert(result.data);
}
},
22支付模块
封装支付的接口
// 获取支付信息
export const reqPayInfo = (orderId) => requests({ url: `/payment/weixin/createNative/${orderId}`, method: 'get' })
尽量不要在生命周期函数写async await,所以在method中用
// 尽量不要在生命周期函数写async await,所以在method中用
// async mounted() {
// await this.$API.reqPayInfo(this.orderId)
// },
mounted() {
this.getPayInfo();
},
methods: {
async getPayInfo() {
let result = await this.$API.reqPayInfo(this.orderId);
console.log(result, "12121");
},
},
后面就是获取数据渲染数据,大同小异
async getPayInfo() {
let result = await this.$API.reqPayInfo(this.orderId);
if (result.code == 200) {
this.payInfo = result.data;
}
}
使用element-ui 先下载
cnpm install --save element-ui
再去element-ui官网看怎么使用
推荐一个插件vue-helper,输入el就会有提示
在main.js中引入
import { Button, MessageBox } from 'element-ui'
// 注册element-ui 全局组件
Vue.component(Button.name, Button)
// 注册element-ui 另一种方法 挂在原型
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
找到对应的组件并使用
open() {
this.$alert("这是 HTML 片段", "HTML 片段", {
dangerouslyUseHTMLString: true,
center: true,
showCancelButton: true,
cancelButtoText: "支付遇见问题",
confirmButtonText: "已支付成功",
showClose: false,
});
},
22.1生成二维码
npm 中找qrcode
下载 cnpm i qrcode
引入qrcode
import QRCode from "qrcode";
传字符串即可生成二维码
async open() {
let url = await QRCode.toDataURL(this.payInfo.codeUrl);
this.$alert(``, "请支付", {
dangerouslyUseHTMLString: true,
center: true,
showCancelButton: true,
cancelButtoText: "支付遇见问题",
confirmButtonText: "已支付成功",
showClose: false,
});
},
在open函数中,我们先定义一个定时器timer,先在data中定义为null,然后根据返回的结果判断是否跳转到支付成功页面。
if (!this.timer) {
this.timer = setInterval(async () => {
// 发请求获取用户支付状态
let result = await this.$API.reqPayStatus(this.orderId);
if (result.code == 200) {
// 第一步,清除定时器
clearInterval(this.timer);
this.timer = null;
// 保存支付成功返回的code
this.code = result.code;
// 关闭弹出框
this.$msgbox.close();
//跳转到下一路由
this.$router.push("/paysuccess");
}
}, 1000);
}
弹出窗口按钮的配置
23个人中心
这里我们来建立二级路由
首先引入二级路由
import Center from '@/pages/Center'
// 引入二级路由
import MyOrder from '@/pages/Center/myOrder'
import GroupOrder from '@/pages/Center/groupOrder'
引入二级路由的组件
export default [
{
// 路由路径都是小写
path: "/center",
component: Center,
meta: {
show: true
},
// 引入二级组件路由
children: [
{
// 不用写/
path: 'myorder',
component: MyOrder
},
{
path: 'groupOrder',
component: GroupOrder
}
],
},
在vue文件中跳转
我的订单
团购订单
并且写路由文件的出口
当进入center路由时,我们可以规定一开始就展示myorder组件,可以使用重定向
后面一些功能就很类似了。
接下来讲一讲性能的优化
24图片懒加载vue-lazyload
直接搜索vue-lazyload,并安装cnpm i vue-lazyload
首先引入插件并注册,还有定义好懒加载的图片是什么(图片和json都是暴露的,不需要手动去暴露)
import james from '@/assets/images/james.gif'
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
// 懒加载的图片
loading: james
})
使用v-lazy标签
25:表单验证
大佬们都推荐用element-ui的from表单验证
elemen-ui表单验证
26:路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
vue官网路由懒加载
//普通形式
// import Home from '@/pages/Home'
// 懒加载形式
const foo = () => import('@/pages/Home')
{
path: "/home",
//普通形式
// component: Home,
//懒加载
component: foo,
meta: {
show: true
}
},
再升级,最懒的
{
path: "/home",
//最懒的
component: () => import('@/pages/Home'),
meta: {
show: true
}
},
27打包文件(前台项目完结撒花!!!)
项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。
但是我们项目发布后不需要知道哪里出错了,所以不需要map文件。
我们可以在vue.config.js中添加如下代码
productionSourceMap:false;
28:购买服务器,发布项目
由于囊中羞涩,省略该步骤。。。