尚品汇项目笔记
- git代码地址
- 前端Vue核心
- 1、vue文件目录分析
- 2、项目配置
- 3、组件页面样式
- 4、清除vue页面默认的样式
- 5、pages文件夹
- 6、footer组件显示与隐藏
- 7、路由传参
- 8、多次执行相同的push问题
- 9、定义全局组件
- 10、代码改变时实现页面自动刷新
- 11、Home首页其它组件
- 12、封装axios
- 13、前端通过代理解决跨域问题
- 14、请求接口统一封装
- 15、nprogress进度条插件
- 16、手动引入vuex
- 17、async await使用
- 18、vuex(*)
- 19、loadsh插件防抖和节流
- 20、编程式导航+事件委托实现路由跳转
- 21、Vue路由销毁问题
- 22、mock插件使用
- 23、vuex数据存储与使用
- 24、swiper插件实现轮播图
- 25、props父子组件通信
- 26、将轮播图模块提取为公共组件
- 27、getters使用
- 28、Object.asign实现对象拷贝
- 29、对象深拷贝
- 30、利用路由信息变化实现动态搜索
- 31、面包屑相关操作
- 32、组件通信方式
- 33、SearchSelector子组件传参及面包屑操作
- 34、商品排序
- 35、手写分页器
- 36、字符串拼接
- 37、滚动条
- 38、undefined细节(*)
- 39、商品详情
- 40、失焦事件
- 41、加入购物车成功路由
- 42、购物车组件开发
- 43、购物车商品数量修改及个人疑问
- 44、购物车状态修改和商品删除
- 45、删除多个商品(actions扩展)
- 46、注册登录业务(ES6 const新用法)
- 47、导航守卫
- 48、交易模块
- 49、个人中心
- 50、路由独享的守卫(*)
- 51、图片懒加载
- 52、表单验证
- 53、路由懒加载
- 54、打包项目
- 55、Vue新知识点(只针对个人)
git代码地址
我自己关于本项目的git仓库地址
前端Vue核心
1、vue文件目录分析
public文件夹:静态资源,webpack进行打包的时候会原封不动打包到dist文件夹中。
pubilc/index.html是一个模板文件,作用是生成项目的入口文件,webpack打包的js,css也会自动注入到该页面中。我们浏览器访问项目的时候就会默认打开生成好的index.html。
src文件夹(程序员代码文件夹)
assets: 存放公用的静态资源 components: 非路由组件(全局组件),其他组件放在views或者pages文件夹中 App.vue: 唯一的跟组件 main.js: 程序入口文件,最先执行的文件
babel.config.js: 配置文件(babel相关)
package.json: 项目的详细信息记录
package-lock.json: 缓存性文件(各种包的来源)
2、项目配置
2.1 项目运行,浏览器自动打开
package.json "scripts": {
"serve": "vue-cli-service serve --open", "build": "vue-cli-service build", "lint": "vue-cli-service lint" },
2.2 关闭eslint校验工具(不关闭会有各种规范,不按照规范就会报错)
- 根目录下创建vue.config.js,进行配置
module.exports = {
//关闭eslint lintOnSave: false }
2.3 src文件夹配置别名,创建jsconfig.json,用@/代替src/,exclude表示不可以使用该别名的文件
{
"compilerOptions": {
"baseUrl": "./", "paths": {
"@/*": [ "src/*" ] } }, "exclude": [ "node_modules", "dist" ] }
3、组件页面样式
4、清除vue页面默认的样式
vue是单页面开发,我们只需要修改public下的index.html文件
5、pages文件夹
- 非路由组件放在components中,路由组件放在pages或views中
- 非路由组件通过标签使用,路由组件通过路由使用
- 在main.js注册玩路由,所有的路由和非路由组件身上都会拥有$router $route属性
- $router:一般进行编程式导航进行路由跳转
- $route: 一般获取路由信息(name path params等)
5.3 路由跳转方式
- 声明式导航router-link标签 ,可以把router-link理解为一个a标签,它 也可以加class修饰
- 编程式导航 :声明式导航能做的编程式都能做,而且还可以处理一些业务
6、footer组件显示与隐藏
- footer在登录注册页面是不存在的,所以要隐藏,v-if 或者 v-show
- 这里使用v-show,因为v-if会频繁的操作dom元素消耗性能,v-show只是通过样式将元素显示或隐藏
- 配置路由的时候,可以给路由配置元信息meta,
- 在路由的原信息中定义show属性,用来给v-show赋值,判断是否显示footer组件
7、路由传参
7.1、query、params
- query、params两个属性可以传递参数
query参数:不属于路径当中的一部分,类似于get请求,地址栏表现为 /search?k1=v1&k2=v2
query参数对应的路由信息path: "/search"
params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位 ,地址栏表现为 /search/v1/v2
params参数对应的路由信息要修改为path: "/search/:keyword"这里的/:keyword就是一个params参数的占位符 - params传参问题
(1)、如何指定params参数可传可不传
如果路由path要求传递params参数,但是没有传递,会发现地址栏URL有问题,详情如下: Search路由项的path已经指定要传一个keyword的params参数,如下所示: path: "/search/:keyword", 执行下面进行路由跳转的代码: this.$router.push({ name:"Search",query:{ keyword:this.keyword}}) 当前跳转代码没有传递params参数 地址栏信息:http://localhost:8080/#/?keyword=asd 此时的地址信息少了/search 正常的地址栏信息: http://localhost:8080/#/search?keyword=asd 解决方法:可以通过改变path来指定params参数可传可不传 path: "/search/:keyword?",?表示该参数可传可不传 this.$router.push({ name:"Search",query:{ keyword:this.keyword},params:{ keyword:''}}) 出现的问题和1中的问题相同,地址信息少了/search 解决方法: 加入||undefined,当我们传递的参数为空串时地址栏url也可以保持正常 this.$router.push({ name:"Search",query:{ keyword:this.keyword},params:{ keyword:''||undefined}}) - 字符串形式
this.$router.push(“/search/”+this.params传参+“?k=”+this.query传参) - 模板字符串
this. r o u t e r . p u s h ( ” / s e a r c h / + router.push(“/search/+ router.push(“/search/+{this.params传参}?k=${this.query传参}”)
注意: 上面字符串的传参方法可以看出params参数和’/’结合,query参数和?结合
http://localhost:8080/#/search/asd?keyword=asd
上面url中asd为params的值,keyword=asd为query传递的值。 - 对象(常用)
this.$router.push({name:“路由名字”,params:{传参},query:{传参})。
以对象方式传参时,如果我们传参中使用了params,只能使用name,不能使用path,如果只是使用query传参,可以使用path 。
8、多次执行相同的push问题
let result = this.$router.push({ name:"Search",query:{ keyword:this.keyword}}) console.log(result) //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,() => { },() => { }) } } 9、定义全局组件
我们的三级联动组件是全局组件,全局的配置都需要在main.js中配置
//将三级联动组件注册为全局组件 import TypeNav from '@/pages/Home/TypeNav'; //第一个参数:全局组件名字,第二个参数:全局组件 Vue.component(TypeNav.name,TypeNav);
在Home组件中使用该全局组件
<template> <div> <!-- 三级联动全局组件已经注册为全局组件,因此不需要引入--> <TypeNav/> </div> </template>
10、代码改变时实现页面自动刷新
根目录下vue.config.js文件设置
module.exports = {
//关闭eslint lintOnSave: false, devServer: {
// true 则热更新,false 则手动刷新,默认值为 true inline: true, // development server port 8000 port: 8001, } }
注意:修改完该配置文件后,要重启一下项目
11、Home首页其它组件
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>
12、封装axios
import axios from "axios"; //1、对axios二次封装 const requests = axios.create({
//基础路径,requests发出的请求在端口号后面会跟改baseURl baseURL:'/api', timeout: 5000, }) //2、配置请求拦截器 requests.interceptors.request.use(config => {
//config内主要是对请求头Header配置 //比如添加token return config; }) //3、配置相应拦截器 requests.interceptors.response.use((res) => {
//成功的回调函数 return res.data; },(error) => {
//失败的回调函数 console.log("响应失败"+error) return Promise.reject(new Error('fail')) }) //4、对外暴露 export default requests;
13、前端通过代理解决跨域问题
module.exports = {
//关闭eslint lintOnSave: false, devServer: {
// true 则热更新,false 则手动刷新,默认值为 true inline: false, // development server port 8000 port: 8001, //代理服务器解决跨域 proxy: {
//会把请求路径中的/api换为后面的代理服务器 '/api': {
//提供数据的服务器地址 target: 'http://39.98.123.211', } }, } }
14、请求接口统一封装
如下所示:
//当前模块,API进行统一管理,即对请求接口统一管理 import requests from "@/api/request"; //首页三级分类接口 export const reqCateGoryList = () => {
return requests({
url: '/product/getBaseCategoryList', method: 'GET' }) }
当组件想要使用相关请求时,只需要导入相关函数即可,以上图的reqCateGoryList 为例:
import {
reqCateGoryList} from './api' //发起请求 reqCateGoryList();
15、nprogress进度条插件
import axios from "axios"; //引入进度条 import nprogress from 'nprogress'; //引入进度条样式 import "nprogress/nprogress.css"; //1、对axios二次封装 const requests = axios.create({
//基础路径,requests发出的请求在端口号后面会跟改baseURl baseURL:'/api', timeout: 5000, }) //2、配置请求拦截器 requests.interceptors.request.use(config => {
//config内主要是对请求头Header配置 //比如添加token //开启进度条 nprogress.start(); return config; }) //3、配置相应拦截器 requests.interceptors.response.use((res) => {
//成功的回调函数 //响应成功,关闭进度条 nprogress.done() return res.data; },(error) => {
//失败的回调函数 console.log("响应失败"+error) return Promise.reject(new Error('fail')) }) //4、对外暴露 export default requests;
16、手动引入vuex
首先确保安装了vuex,根目录创建store文件夹,文件夹下创建index.js,内容如下:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) //对外暴露store的一个实例 export default new Vuex.Store({
state:{
}, mutations:{
}, actions:{
}, })
import store from './store' new Vue({
render: h => h(App), //注册路由,此时组件中都会拥有$router $route属性 router, //注册store,此时组件中都会拥有$store store }).$mount('#app')
17、async await使用
import {
reqCateGoryList} from '@/api' export default {
actions:{
categoryList(){
let result = reqCateGoryList() console.log(result) } } }
actions:{
categoryList(){
let result = reqCateGoryList().then( res=>{
console.log("res") console.log(res) return res } ) console.log("result") console.log(result) } }
async categoryList(){
let result = await reqCateGoryList() console.log("result") console.log(result) }
18、vuex(*)
async addOrUpdateShopCart({
commit},{
skuId,skuNum}){
let result = await reqAddOrUpdateShopCart(skuId,skuNum) console.log(result) if(result.data === 200){
}
19、loadsh插件防抖和节流
import {
throttle} from 'lodash' methods: {
//鼠标进入修改响应元素的背景颜色 //采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次 changeIndex: throttle(function (index){
this.currentIndex = index },50), //鼠标移除触发时间 leaveIndex(){
this.currentIndex = -1 } }
20、编程式导航+事件委托实现路由跳转

如上图所示,三级标签列表有很多,每一个标签都是一个页面链接,我们要实现通过点击表现进行路由跳转。
路由跳转的两种方法:导航式路由,编程式路由。
解决方法:
对于问题1:为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex"> <div class="item" v-for="(c1,index) in categoryList" v-show="index!==16" :key="c1.categoryId" :class="{cur:currentIndex===index}"> <h3 @mouseenter="changeIndex(index)" > <a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" >{
{
c1.categoryName}}</a> </h3> <div class="item-list clearfix" :style="{display:currentIndex===index?'block':'none'}"> <div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId"> <dl class="fore"> <dt> <a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{
{
c2.categoryName}}</a> </dt> <dd> <em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId"> <a :data-categoryName="c2.categoryName" :data-category3Id="c3.categoryId">{
{
c3.categoryName}}</a> </em> </dd></dl></div></div></div></div>
注意:event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。
//函数使用 <div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex"> //函数定义 goSearch(event){
console.log(event.target) }

对应的goSearrch函数
goSearch(event){
let element = event.target //html中会把大写转为小写 //获取目前鼠标点击标签的categoryname,category1id,category2id,category3id, // 通过四个属性是否存在来判断是否为a标签,以及属于哪一个等级的a标签 let {
categoryname,category1id,category2id,category3id} = element.dataset //categoryname存在,表示为a标签 if(categoryname){
//category1id一级a标签 //整理路由跳转的参数 let location = {
name:'Search'}//跳转路由name let query = {
categoryName:categoryname}//路由参数 if(category1id){
query.category1Id = category1id }else if(category2id){
//category2id二级a标签 query.category2Id = category2id }else if(category3id){
//category3id三级a标签 query.category3Id = category3id } //整理完参数 location.query = query //路由跳转 this.$router.push(location) } },
21、Vue路由销毁问题
Vue在路由切换的时候会销毁旧路由。
我们在三级列表全局组件TypeNav中的mounted进行了请求一次商品分类列表数据。
由于Vue在路由切换的时候会销毁旧路由,当我们再次使用三级列表全局组件时还会发一次请求。
如下图所示:当我们在包含三级列表全局组件的不同组件之间进行切换时,都会进行一次信息请求。

由于信息都是一样的,出于性能的考虑我们希望该数据只请求一次,所以我们把这次请求放在App.vue的mounted中。(根组件App.vue的mounted只会执行一次)
注意:虽然main.js也是只执行一次,但是不可以放在main.js中。因为只有组件的身上才会有$store属性。
22、mock插件使用
import Mock from 'mockjs' //webpack默认对外暴露:json、图片 import banner from './banner.json' import floor from './floor.json' //mock数据:第一个参数请求地址、第二个参:请求数据 Mock.mock("/mock/banner",{
code:200,data:banner}) Mock.mock("/mock/floor",{
code:200,data:floor}) //记得要在main.js中引入一下 //import ''@/mock/mockServer
23、vuex数据存储与使用
mounted() {
this.$store.dispatch("getBannerList") },
2、请求实际是在store中的actions中完成的
actions:{
//获取首页轮播图数据 async getBannerList({
commit}){
let result = await reqGetBannerList() if(result.code === 200){
commit("BANNERLIST",result.data) } } }
3、获取到数据后存入store仓库,在mutations完成
/唯一修改state的部分 mutations:{
BANNERLIST(state,bannerList){
state.bannerList = bannerList } },
<script> import {
mapState} from "vuex"; export default {
name: "index", //主键挂载完毕,请求轮播图图片 mounted() {
this.$store.dispatch("getBannerList") }, computed:{
...mapState({
bannerList: (state => state.home.bannerList) }) } } </script>
总结:只要是公共数据都会放在store中,之后的实现步骤就是上面的固定步骤。
24、swiper插件实现轮播图
注意:在创建swiper对象时,我们会传递一个参数用于获取展示轮播图的DOM元素,官网直接通过class(而且这个class不能修改,是swiper的css文件自带的)获取。但是这样有缺点:当页面中有多个轮播图时,因为它们使用了相同的class修饰的DOM,就会出现所有的swiper使用同样的数据,这肯定不是我们希望看到的。
解决方法:在轮播图最外层DOM中添加ref属性
通过ref属性值获取DOM
let mySwiper = new Swiper(this.$refs.cur,{...})
<!--banner轮播--> <div class="swiper-container" id="mySwiper" ref="cur"> <div class="swiper-wrapper"> <div class="swiper-slide" v-for="(carouse,index) in bannerList" :key="carouse.id"> <img :src="carouse.imgUrl" /> </div> </div> <!-- 如果需要分页器 --> <div class="swiper-pagination"></div> <!-- 如果需要导航按钮 --> <div class="swiper-button-prev" ></div> <div class="swiper-button-next"></div> </div> <script> //引入Swiper import Swiper from 'swiper' //引入Swiper样式 import 'swiper/css/swiper.css' </script>
我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。
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', }, }) },
解决方法一:等我们的数据请求完毕后再创建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', }, }) } }
即使这样也还是无法实现轮播图,原因是,我们轮播图的html中有v-for的循环,我们是通过v-for遍历bannerList中的图片数据,然后展示。我们的watch只能保证在bannerList变化时创建swiper对象,但是并不能保证此时v-for已经执行完了。假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片(因为swiper对象生效的前提是html即dom结构已经渲染好了)。
完美解决方案:使用watch+this.$nextTick()
官方介绍:this. $nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。
个人理解:无非是等我们页面中的结构都有了再去执行回调函数
完整代码
<template> <!--列表--> <div class="list-container"> <div class="sortList clearfix"> <div class="center"> <!--banner轮播--> <div class="swiper-container" id="mySwiper"> <div class="swiper-wrapper"> <div class="swiper-slide" v-for="(carouse,index) in bannerList" :key="carouse.id"> <img :src="carouse.imgUrl" /> </div> </div> <!-- 如果需要分页器 --> <div class="swiper-pagination"></div> <!-- 如果需要导航按钮 --> <div class="swiper-button-prev" ></div> <div class="swiper-button-next"></div> </div> </div> </div> </div> </div> </template> <script> //引入Swiper import Swiper from 'swiper' //引入Swiper样式 import 'swiper/css/swiper.css' import {
mapState} from "vuex"; export default {
name: "index", //主键挂载完毕,ajax请求轮播图图片 mounted() {
this.$store.dispatch("getBannerList") }, computed:{
...mapState({
//从仓库中获取轮播图数据 bannerList: (state) => {
return state.home.bannerList} }) }, watch:{
bannerList(newValue,oldValue){
//this.$nextTick()使用 this.$nextTick(()=>{
let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{
pagination:{
el: '.swiper-pagination', clickable: true, }, // 如果需要前进后退按钮 navigation: {
nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev', }, // 如果需要滚动条 scrollbar: {
el: '.swiper-scrollbar', }, }) }) } } } </script>
注意:之前我们在学习watch时,一般都是监听的定义在data中的属性,但是我们这里是监听的computed中的属性,这样也是完全可以的,并且如果你的业务数据也是从store中通过computed动态获取的,也需要watch监听数据变化执行相应回调函数,完全可以模仿上面的写法。
25、props父子组件通信
<template> <div> //...省略 <!-- 父组件通过自定义属性list给子组件传递数据--> <Floor v-for="floor in floorList" :key="floor.id" :list="floor"/> <!-- 商标--> </div> </template>
子组件:Floor下的index.vue
<template> <!--楼层--> <div class="floor"> //...省略 </div> </template> <script> export default {
name: "floor", //子组件通过props属性接受父组件传递的数据 props:['list'] } </script>

通过前面描述的代码我们实现了父子通信,即将floorList分发给Floor组件。下图为Floor组件信息

对于父子组件的理解,我更偏向于个人理解二,因为它可以通过上面图片得到很好的解释。但是个人理解一对于新手理解起来更容易。
26、将轮播图模块提取为公共组件
<template> <div class="swiper-container" ref="cur" id="floor1Swiper"> <div class="swiper-wrapper"> <div class="swiper-slide" v-for="(carouse,index) in carouselList" :key="carouse.id"> <img :src="carouse.imgUrl"> </div> </div> <!-- 如果需要分页器 --> <div class="swiper-pagination"></div> <!-- 如果需要导航按钮 --> <div class="swiper-button-prev"></div> <div class="swiper-button-next"></div> </div> </template> <script> import Swiper from "swiper"; import 'swiper/css/swiper.css' export default {
name: "Carousel", props:["carouselList"], watch: {
carouselList: {
//这里监听,无论数据有没有变化,上来立即监听一次 immediate: true, //监听后执行的函数 handler(){
//第一次ListContainer中的轮播图Swiper定义是采用watch+ this.$nextTick()实现 this.$nextTick(() => {
let mySwiper = new Swiper(this.$refs.cur,{
loop: true, // 循环模式选项 // 如果需要分页器 pagination: {
el: '.swiper-pagination', // clickable: true }, // 如果需要前进后退按钮 navigation: {
nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev', }, // 如果需要滚动条 scrollbar: {
el: '.swiper-scrollbar', }, }) }) } } } } </script> <style scoped> </style>
Floor组件引用Carousel组件
我们还记得在首页上方我们的ListContainer组件也使用了轮播图,同样我们替换为我们的公共组件。
ListContainer组件引用Carousel组件
注意:
(1)老师将该组件在main.js中引入,并定义为全局组件。我这里只是在使用到该组件的地方引入并声明(个人认为轮播图组件还算不上全局组件)。
(2)引用组件时要在components中声明引入的组件。
(3)我们将轮播图组件已经提取为公共组件Carouse,所以我们只需要在Carouse中引入swiper和相应css样式。
27、getters使用
store中search模块代码
import {
reqGetSearchInfo} from '@/api'; const state = {
searchList:{
}, } const mutations = {
SEARCHLIST(state,searchList){
state.searchList = searchList } } const actions = {
//第二个参数data默认是一个空对象 async getSearchListr({
commit},data={
}){
let result = await reqGetSearchInfo(data) if(result.code === 200){
commit("SEARCHLIST",result.data) } } } const getters = {
goodsList(state){
//网络出现故障时应该将返回值设置为空 return state.searchList.goodsList||[] } } export default {
state, mutations, actions, getters, }
在Search组件中使用getters获取仓库数据
//只展示了使用getters的代码 <script> //引入mapGetters import {
mapGetters} from 'vuex' export default {
name: 'Search', computed:{
//使用mapGetters,参数是一个数组,数组的元素对应getters中的函数名 ...mapGetters(['goodsList']) } } </script>
后续数据的动态渲染就和之前模块相同,没有什么难度。
28、Object.asign实现对象拷贝
参考链接
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。 Object.assign(target, ...sources) 【target:目标对象】,【souce:源对象(可多个)】 举个栗子: const object1 = {
a: 1, b: 2, c: 3 }; const object2 = Object.assign({
c: 4, d: 5}, object1); console.log(object2.c, object2.d); console.log(object1) // { a: 1, b: 2, c: 3 } console.log(object2) // { c: 3, d: 5, a: 1, b: 2 } 注意: 1.如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性 2.Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标 对象的[[Set]],所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如 果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到 原型,应使用Object.getOwnPropertyDescriptor()和Object.defineProperty() 。
29、对象深拷贝
针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。 let obj1 = {
a: 0 , b: {
c: 0}}; let obj2 = Object.assign({
}, obj1); console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} obj1.a = 1; console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} obj2.a = 2; console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 0}} obj2.b.c = 3; console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}} console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}} 最后一次赋值的时候,b是值是对象的引用,只要修改任意一个,其他的也会受影响 // Deep Clone (深拷贝) obj1 = {
a: 0 , b: {
c: 0}}; let obj3 = JSON.parse(JSON.stringify(obj1)); obj1.a = 4; obj1.b.c = 4; console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}
30、利用路由信息变化实现动态搜索
watch:{
$route(newValue,oldValue){
Object.assign(this.searchParams,this.$route.query,this.$route.params) this.searchInfo() //如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数 //所以每次请求结束后将相应参数制空 this.searchParams.category1Id = ''; this.searchParams.category2Id = ''; this.searchParams.category3Id = ''; this.$route.params.keyword = ''; } },
31、面包屑相关操作
//删除分类 removeBread(){
this.searchParams.categoryName = undefined this.$router.push({
name:'Search',params:this.$route.params}) },
new Vue({
//全局事件总线$bus配置 beforeCreate() {
//此处的this就是这个new Vue()对象 //网络有很多bus通信总结,原理相同,换汤不换药 Vue.prototype.$bus = this }, render: h => h(App), //router2、注册路由,此时组件中都会拥有$router $route属性 router, //注册store,此时组件中都会拥有$store store }).$mount('#app')
(2)search组件使用$bus通信,第一个参数可以理解为为通信的暗号,还可以有第二个参数(用于传递数据),我们这里只是用于通知header组件进行相应操作,所以没有设置第二个参数。
//删除搜索关键字 removeBreadParams(){
this.searchParams.keyword = undefined //通知兄弟组件header删除输入框的keyword关键字 this.$bus.$emit("clear") this.$router.push({
name:'Search',query:this.$route.query}) },
mounted() {
// 组件挂载时就监听clear事件,clear事件在search模块中定义 // 当删除关键字面包屑时,触发该事件,同时header的输入框绑定的keyword要删除 this.$bus.$on("clear",()=>{
this.keyword = '' }) }
32、组件通信方式
第一种父子组件通信:
$ on、$emit自定义事件实现子组件给父组件传递信息。 props实现父组件给子组件传递数据。
第二种全局事件总线 $bus(适用于所有的场景)
第三种Vuex
第四中插槽(适用于父子组件通信)
组件通信方式连接
33、SearchSelector子组件传参及面包屑操作
在31小节中描述了通过query、params参数生成面包屑,以及面包屑的删除操作对应地址栏url的修改。
SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。如下图所示
此处生成面包屑时会涉及到子组件向父组件传递信息操作(在32小节有相关知识点),之后的操作和前面31小姐讲的面包屑操作原理相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。
在具体的操作内还会涉及一些小的知识点,例如
字符串拼接 ·${}·,使用方法如下
var a = 1; console.log(`a的值是:${
a}`); //a的值是:1
<script> import SearchSelector from './SearchSelector/SearchSelector' import {
mapGetters} from 'vuex' export default {
name: 'Search', components: {
SearchSelector }, data(){
return{
//动态获取searchParams searchParams:{
category1Id: "",//一级分类id category2Id: "",//二级分类id category3Id: "",//三级分类id categoryName: "", keyword: "", order: "1:desc", pageNo: 1, pageSize: 10, props: [],//平台售卖属性 trademark: ""//品牌 }, } }, //在组件挂在之前动态编辑searchParams的值,因为组件挂在之后会使用到searchParams beforeMount() {
//Object.assign方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。 //Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象 Object.assign(this.searchParams,this.$route.query,this.$route.params) }, methods:{
//搜索 searchInfo(){
this.$store.dispatch("getSearchListr",this.searchParams) }, //删除分类(query)面包屑 removeBread(){
this.searchParams.categoryName = undefined this.$router.push({
name:'Search',params:this.$route.params}) }, //删除搜索关键字(params)面包屑 removeBreadParams(){
this.searchParams.keyword = undefined //通知兄弟组件header删除输入框的keyword关键字 this.$bus.$emit("clear") this.$router.push({
name:'Search',query:this.$route.query}) }, //获取子组件传递的品牌信息(自定义事件) tradeMarkInfo(tradeMark){
//接口文档中trademark的信息是"ID:品牌名称"形式 this.searchParams.trademark = `${
tradeMark.tmId}:${
tradeMark.tmName}` this.searchInfo() }, //删除品牌面包屑 removeTradeMark(){
this.searchParams.trademark = undefined this.searchInfo() }, //获取子组件传递的属性信息(自定义事件) attrInfo(attr,attrValue){
//searchParams.props元素为字符串形式,api文档有介绍 let props = `${
attr.attrId}:${
attrValue}:${
attr.attrName}` //数组去重 if(this.searchParams.props.indexOf(props)===-1){
this.searchParams.props.push(props) this.searchInfo() } }, //删除属性面包屑 removeAttr(index){
this.searchParams.props.splice(index,1) } }, mounted() {
this.searchInfo() }, computed:{
...mapGetters(['goodsList']) }, //watch可以监听组件上的属性 watch:{
$route:{
handler(newValue,oldValue){
console.log(this.$route) Object.assign(this.searchParams,this.$route.query,this.$route.params) this.searchInfo() //如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数 //所以每次请求结束后将相应参数制空 this.searchParams.category1Id = ''; this.searchParams.category2Id = ''; this.searchParams.category3Id = ''; }, } }, } </script>
34、商品排序
在search模块使用该图标
<div class="sui-navbar"> <div class="navbar-inner filter"> <ul class="sui-nav"> <!-- 这里isOne、isTwo、isAsc、isDesc是计算属性,如果不使用计算属性要在页面中写很长的代码--> <li :class="{active:isOne}" @click="changeOrder('1')"> <!-- 阿里图标前置类iconfont--> <a >综合<span v-show="isOne" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}"></span></a> </li> <li :class={
active:isTwo} @click="changeOrder('2')"> <a >价格<span v-show="isTwo" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}"></span></a> </li> </ul> </div> </div>
isOne、isTwo、isAsc、isDesc计算属性代码
computed:{
...mapGetters(['goodsList']), 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
//flag用于区分综合、价格,1:综合,2:价格 changeOrder(flag){
let newSearchOrder = this.searchParams.order //将order拆为两个字段orderFlag(1:2)、order(asc:desc) let orderFlag = this.searchParams.order.split(':')[0] let order = this.searchParams.order.split(':')[1] //由综合到价格、由价格到综合 if(orderFlag!==flag){
//点击的不是同一个按钮 newSearchOrder = `${
flag}:desc` this.searchInfo() }else{
//多次点击的是不是同一个按钮 newSearchOrder = `${
flag}:${
order==='desc'?'asc':'desc'}` } //需要给order重新赋值 this.searchParams.order = newSearchOrder; //再次发请求 this.searchInfo(); }
35、手写分页器
//连续页码的起始页码、末尾页码 startNumAndEnd(){
let start = 0 , end = 0; //规定连续页码数字5(totalPage至少5页) //不正常现象 if(this.continues > this.totalPage){
start = 1 end = this.totalPage }else{
//正常现象 Math.floor:想下取整 start = this.pageNo - Math.floor(this.continues/2) end = this.pageNo + Math.floor(this.continues/2) //start出现不正常现象纠正 if(start < 1){
start = 1 end = this.continues } //end出现不正常现象纠正 if(end > this.totalPage){
end = this.totalPage start = this.totalPage - this.continues + 1 } } return {
start,end} }
当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。
36、字符串拼接
如果你想在你的字符串内加入某个变量的值,就需要字符串拼接使用 “(飘符号),由于 飘在markdown是单行代码标记所以下面我们用··代替。
字符串拼接 ·${}·,使用方法如下:
在js中使用
var a = 1; console.log(`a的值是:${
a}`); //a的值是:1
在html中使用
<router-link :to="`/detail/${goods.id}`"></router-link>
37、滚动条
38、undefined细节(*)
const getters = {
categoryView(state){
return state.goodInfo.categoryView } }
对应的computed代码
computed:{
...mapGetters(['categoryView']) }
html代码
<div class="conPoin"> <span v-show="categoryView.category1Name" >{
{
categoryView.category1Name}}</span> <span v-show="categoryView.category2Name" >{
{
categoryView.category2Name}}</span> <span v-show="categoryView.category3Name" >{
{
categoryView.category3Name}}</span> </div>
下细节在于getters的返回值。如果getters按上面代码写为return state.goodInfo.categoryView,页面可以正常运行,但是会出现红色警告。

原因:假设我们网络故障,导致goodInfo的数据没有请求到,即goodInfo是一个空的对象,当我们去调用getters中的return state.goodInfo.categoryView时,因为goodInfo为空,所以也不存在categoryView,即我们getters得到的categoryView为undefined。所以我们在html使用该变量时就会出现没有该属性的报错。
即:网络正常时不会出错,一旦无网络或者网络问题就会报错。
总结:所以我们在写getters的时候要养成一个习惯在返回值后面加一个||条件。即当属性值undefined时,会返回||后面的数据,这样就不会报错。
如果返回值为对象加||{},数组:||[ ]。
此处categoryView为对象,所以将getters代码改为return state.goodInfo.categoryView||{}
39、商品详情
<div class="swiper-container" ref="cur"> <div class="swiper-wrapper"> <div class="swiper-slide" v-for="(skuImage,index) in skuImageList" :key="skuImage.id"> <img :src="skuImage.imgUrl" :class="{active:currentIndex===index}" @click="changeImg(index)"> </div> </div> <div class="swiper-button-next"></div> <div class="swiper-button-prev"></div> </div>
changeImg(index){
//将点击的图片标识位高亮 this.currentIndex = index //通知兄弟组件修改大图图片 this.$bus.$emit("changeImg",index) }
对应的放大镜组件,首先在mounted监听该全局事件
mounted() {
this.$bus.$on("changeImg",(index)=>{
//修改当前响应式图片 this.currentIndex = index; }) },
放大镜组件中也会有一个currentIndex,他用表示大图中显示的图片的下标(因为放大镜组件只能显示一张图片),全局事件传递的index赋值给currentIndex ,通过computed计算属性改变放大镜组件展示的图片下标。
computed:{
imgObj(){
return this.skuImageList[this.currentIndex] || {
} } },
放大镜组件展示图片的html代码
<img :src="imgObj.imgUrl " />
40、失焦事件
41、加入购物车成功路由
async addShopCar() {
try{
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: this.$route.params.skuId, skuNum: this.skuNum }); //一些简单的数据,比如skuNum通过query传过去 //复杂的数据通过session存储, //sessionStorage、localStorage只能存储字符串 sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo)) this.$router.push({
name:'AddCartSuccess',query:{
'skuNum':this.skuNum}}) }catch (error){
alert(error.message) } }
detail store对应代码
//将产品添加到购物车中 async addOrUpdateShopCart({
commit},{
skuId,skuNum}){
let result = await reqAddOrUpdateShopCart(skuId,skuNum) if(result.code === 200){
return 'ok' }else{
return Promise.reject(new Error('faile')) } }
其实这里当不满足result.code === 200条件时,也可以返回字符串‘faile’,自己在addShopCar中判断一下返回值,如果为‘ok’则跳转,如果为‘faile’(或者不为‘ok’)直接提示错误。当然这里出错时返回一个Promise.reject更加符合程序的逻辑。
跳转‘加入购物车成功页面’的同时要携带商品的信息。本项目只是传递的商品的一些标签属性,并没有传递商品的型号类别的信息,比如颜色、内存等信息,自己可以手动实现,比较简单。
当我们想要实现两个毫无关系的组件传递数据时,首相想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的复杂信息,就可以通过Web Storage实现。
sessionStorage、localStorage概念:
sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
注意:无论是session还是local存储的值都是字符串形式。如果我们想要存储对象,需要在存储前JSON.stringify()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象。
42、购物车组件开发
根据api接口文档封装请求函数
export const reqGetCartList = () => {
return requests({
url:'/cart/cartList', method:'GET' })}
但是如果想要获取详细信息,还需要一个用户的uuidToken,用来验证用户身份。但是该请求函数没有参数,所以我们只能把uuidToken加在请求头中。
创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid)。
生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储
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模块
const state = {
goodInfo:{
}, //游客身份 uuid_token: getUUID() }
在request.js中设置请求头
import store from '@/store'; requests.interceptors.request.use(config => {
//config内主要是对请求头Header配置 //1、先判断uuid_token是否为空 if(store.state.detail.uuid_token){
//2、userTempId字段和后端统一 config.headers['userTempId'] = store.state.detail.uuid_token } //比如添加token //开启进度条 nprogress.start(); return config; })
注意this.$store只能在组件中使用,不能再js文件中使用。如果要在js中使用,需要引入import store from '@/store';
43、购物车商品数量修改及个人疑问
every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
例如判断底部勾选框是否全部勾选代码部分
//判断底部勾选框是否全部勾选 isAllCheck() {
//every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false return this.cartInfoList.every(item => item.isChecked === 1) }
<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){
//减按钮判断当前数量是否为1 if(type === 'minus') {
//当商品数量为1是,不可以再减少 if(cart.skuNum === 1){
return; } } //输入框修改,难点:要判断输入的内容是否合法 if(type === 'change'){
//输入内容不合法时 if(isNaN(disNum * 1) || disNum <= 0){
disNum = 0; }else{
disNum = parseInt(disNum) - cart.skuNum } } //加按钮disNum不需要改变 try{
await this.$store.dispatch('addOrUpdateShopCart',{
skuId:cart.skuId,skuNum:disNum}) //商品数量修改成功后再次获取服务器数据 await this.$store.dispatch("getCartList") }catch (error){
alert(error) } },100),
个人疑点:在最后一步更新商品信息时,我最初的想法时使用this.$router.push('/shopcart')实现。因为,我们shopcart页面的mounted会去获取商品信息。当路由跳转时会执行mounted的内容,实现商品数据信息更新。
mounted(){
this.$store.dispatch("getCartList") },
44、购物车状态修改和商品删除
这部分都比较简单,这里不多做赘述,唯一需要注意的是当store的action中的函数返回值data为null时,应该采用下面的写法(重点是if,else部分)
action部分:以删除购物车某个商品数据为例
//修改购物车某一个产品的选中状态 async reqUpdateCheckedById({
commit},{
skuId,isChecked}){
let result = await reqUpdateCheckedById(skuId,isChecked) if(result.code === 200){
return 'ok' }else{
return Promise.reject(new Error('fail')) } }
method部分:(重点是try、catch)
async reqUpdateCheckedById(cart,event){
let isChecked = event.target.checked ? 1 :0 try{
await this.$store.dispatch("reqUpdateCheckedById",{
skuId:cart.skuId,isChecked:isChecked}) //修改成功,刷新数据 this.$store.dispatch() }catch (error){
this.$store.dispatch("getCartList") } }
45、删除多个商品(actions扩展)
deleteAllCheckedById(context) {
console.log(context) }
/删除选中的所有商品 deleteAllCheckedById({
dispatch,getters}) {
getters.getCartList.cartInfoList.forEach(item => {
let result = []; //将每一次返回值添加到数组中 result.push(item.isChecked === 1?dispatch('deleteCartById',item.skuId):'') }) return Promise.all(result) },
上面代码使用到了Promise.all
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
购物车组件method批量删除函数
//删除选中的所有商品 async deleteAllCheckedById(){
try{
await this.$store.dispatch('deleteAllCheckedById') //删除成功,刷新数据 this.$store.dispatch("getCartList") }catch (error){
alert(error) } },
//修改购物车全部产品的选中状态 async updateAllChecked({
dispatch,getters},flag){
let result = [] getters.getCartList.cartInfoList.forEach(item => {
result.push(dispatch('reqUpdateCheckedById',{
skuId:item.skuId,isChecked:flag })) }) return Promise.all(result) }
method
//修改全部商品的状态 async allChecked(event){
let flag = event.target.checked ? 1 : 0 console.log(flag) try{
await this.$store.dispatch('updateAllChecked',flag) //修改成功,刷新数据 this.$store.dispatch("getCartList") }catch (error){
alert(error) } }
bug纠正
computed中的cartInfoList没有写[ ]返回值。当后台返回的购物车数据为空时,cartInfoList 就会为undefined,会导致后面的total、isAllCheck等计算属性使用到cartInfoList时产生计算错误。
正确代码:
cartInfoList(){
return this.getCartList.cartInfoList || []; },
错误代码:
cartInfoList(){
return this.getCartList.cartInfoList; },
46、注册登录业务(ES6 const新用法)
1、ES6 const新用法
const {
comment,index,deleteComment} = this
上面的这句话是一个简写,最终的含义相当于:
const comment = this.comment const index = this.index const deleteComment = this.deleteComment
2、用户注册模块
actions部分(验证码部分省略)
//用户注册 async userRegister({
commit},data){
let result = await reqPostRegister(data) if(result.code === 200){
return 'ok' }else{
return Promise.reject(new Error(result.message)) } }
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) } },
this.$store.dispatch('userRegister',{phone,password,code})因为K 、V相同,所以只传K
3、登录模块
前端部分内容
<form > <div class="input-text clearFix"> <span></span> <input type="text" placeholder="邮箱/用户名/手机号" v-model="phone"> </div> <div class="input-text clearFix"> <span class="pwd"></span> <input type="password" placeholder="请输入密码" v-model="password"> </div> <div class="setting clearFix"> <label class="checkbox inline"> <input name="m1" type="checkbox" value="2" checked=""> 自动登录 </label> <span class="forget">忘记密码?</span> </div> <button class="btn" @click.prevent="goLogin">登 录</button> </form>
由于登录按钮的父节点是一个form表单,如果使用@click触发登录事件,form表单会执行默认事件action实现页面跳转。这里我们使用@click.prevent,它可以阻止自身默认事件的执行。
actions登陆函数
//登录 async userLogin({
commit},data){
let result = await reqPostLogin(data) //服务器会返回token if(result.code === 200){
//token存入vuex commit("SETUSERTOKEN",result.data.token) //持久化存储token localStorage.setItem('TOKEN',result.data.token) return 'ok' }else{
return Promise.reject(new Error(result.message)) } },
mutations设置用户token
//设置用户token SETUSERTOKEN(state,token){
state.token = token }
登陆组件methods登陆函数
async goLogin(){
try{
//会将this中的phone,password以对象的形式返回 const {
phone,password} = this phone && password && await this.$store.dispatch('userLogin',{
phone,password}) //路由跳转到home首页 this.$router.push('/home') }catch (error){
alert(error) } }
async getUserInfo({
commit}){
let result = await reqGetUserInfo(); //将用户信息存储到store中 if(result.code === 200){
//vuex存储用户信息 commit('SETUSERINFO',result.data) return 'ok' }else{
return Promise.reject(new Error(result.message)) } },
mutations存储用户信息
//存储用户信息 SETUSERINFO(state,data){
state.userInfo = data },
47、导航守卫

router index.js全局前置守卫代码
//设置全局导航前置守卫 router.beforeEach(async(to, from, next) => {
let token = store.state.user.token let name = store.state.user.userInfo.name //1、有token代表登录,全部页面放行 if(token){
//1.1登陆了,不允许前往登录页 if(to.path==='/login'){
next('/home') } else{
//1.2、因为store中的token是通过localStorage获取的,token有存放在本地 // 当页面刷新时,token不会消失,但是store中的其他数据会清空, // 所以不仅要判断token,还要判断用户信息 //1.2.1、判断仓库中是否有用户信息,有放行,没有派发actions获取信息 if(name) next() else{
//1.2.2、如果没有用户信息,则派发actions获取用户信息 try{
await store.dispatch('getUserInfo') next() }catch (error){
//1.2.3、获取用户信息失败,原因:token过期 //清除前后端token,跳转到登陆页面 await store.dispatch('logout') next('/login') } } } }else{
//2、未登录,首页或者登录页可以正常访问 if(to.path === '/login' || to.path === '/home' || to.path==='/register') next() else{
alert('请先登录') next('/login') } } })
48、交易模块
————————————————-分割线——————————————–
data(){
return{
payInfo:{
}, //支付二维码 payImg: 'https://img-blog.csdnimg.cn/6d82f91ecc90d9182bc57ea4ad.jpg?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5q-b5q-b6Jmr5ZGc5ZGc,size_19,color_FFFFFF,t_70,g_se,x_16', timer:null,//支付定时器 code: '',//支付状态码 }},
下面代码中涉及到计时器setInterval()与setTimeout()计时器
//点击支付按钮,弹出支付二维码 async opens(){
this.$alert(`" / >`, '请使用微信扫码', {
dangerouslyUseHTMLString: true, showCancelButton: true, center: true }); //需要知道订单是否支付成功 //成功则跳转,失败则提示信息 if(!this.timer){
//定时器间歇发送请求获取订单支付状态 this.timer = setInterval(async ()=>{
let result = await this.$API.reqGetPayStatus(this.payInfo.orderId) console.log(result) //因为每次支付都要花钱,所以直接205(支付中)就跳转了, if(result.code === 205){
//1、清除定时器 clearInterval(this.timer) this.timer = null //2、保存支付成功返回的code this.code = result.code //3、关闭弹出框 this.$msgbox.close() //4、跳转到下一个路由 this.$router.push('/paysuccess') } },1000) } }
Vue图片引入
非js内引入图片(html):一般都是通过路径引入,例如:。
js内引入图片: 可分为通过路径引入和不通过路径引入。
1、如果想要通过路径方式在vue中的js引入图片,必须require引入。
例如:js中引入个人支付二维码可以通过下面方式实现
this.$alert(`" / >`, '请使用微信扫码', {
dangerouslyUseHTMLString: true, showCancelButton: true, center: true });
<script> import payImg from '@/assets/pay.jpg' export default {
methods:{
//点击支付按钮,弹出支付二维码1 async opens(){
this.$alert(`" / >`, '请使用微信扫码', {
dangerouslyUseHTMLString: true, showCancelButton: true, center: true }); } } </script>
图片引入参考链接
49、个人中心
//个人中心 {
name: 'Center', path: '/center', component: () => import('@/pages/Center'), children: [ {
//二级路由要么不写/,要么写全:'/center/myorder' path: 'myorder', component: () => import('@/pages/Center/MyOrder') }, {
path: 'groupbuy', component: () => import('@/pages/Center/GroupOrder'), }, //默认显示 {
path: '', redirect: 'myorder' } ] }
{ path: '', redirect: 'myorder' }表示当我们访问center路由时,center中的router-view部分默认显示myorder二级路由内容。
我们的子路由最好放在父路由文件夹下,如下所示。

警告

图中内容如下:
Named Route ‘Center’ has a default child route. When navigating to this named route (:to=“{name: ‘Center’”), the default child route will not be rendered. Remove the name from this route and use the name of the default child route for named links instead.
对应的代码
//个人中心 {
name: 'Center', path: '/center', component: () => import('@/pages/Center'), children: [ {
//二级路由要么不写/,要么写全:'/center/myorder' path: '/center/myorder', component: () => import('@/pages/Center/MyOrder') }, {
path: '/center/groupbuy', component: () => import('@/pages/Center/GroupOrder'), }, //默认显示 {
path: '', redirect: 'myorder' } ] }
总结警告缘由:当某个路由有子级路由时,父级路由须要一个默认的路由,因此父级路由不能定义name属性,解决办法是去掉name:’Center’就好了。
50、路由独享的守卫(*)
用户已经登陆,用户在home页直接通过地址栏访问trade结算页面,发现可以成功进入该页面,正常情况,用户只能通过在shopcart页面点击去结算按钮才可以到达trade页面。我们可以通过路由独享守卫解决该问题
//交易组件 {
name: 'Trade', path: '/trade', meta: {
show:true}, component: () => import('@/pages/Trade'), //路由独享首位 beforeEnter: (to, from, next) => {
if(from.path === '/shopcart' ){
next() }else{
next(false) } } },
上面的代码已经实现了trade路由只能从shopcart路由跳转。next(false)指回到from路由。
但是,上面的代码还会有bug,就是当我们在shopcart页面通过地址栏访问trade时还是会成功。正常情况应该是只有当我们点击去结算按钮后才可以进入到trade页面。(这只是我个人观点)
解决办法:
在shopcart路由信息meta中加一个flag,初始值为false。当点击去结算按钮后,将flag置为true。在trade的独享路由守卫中判断一下flag是否为true,当flag为true时,代表是通过点击去结算按钮跳转的,所以就放行。
shopcart路由信息
//购物车 {
path: "/shopcart", name: 'ShopCart', component: ()=> import('../pages/ShopCart'), meta:{
show: true,flag: false}, },
shopcart组件去结算按钮触发事件
toTrade(){
this.$route.meta.flag = true this.$router.push('/trade') }
trade路由信息
//交易组件 {
name: 'Trade', path: '/trade', meta: {
show:true}, component: () => import('@/pages/Trade'), //路由独享首位 beforeEnter: (to, from, next) => {
if(from.path === '/shopcart' && from.meta.flag === true){
from.meta.flag = false next() }else{
next(false) } } },
注意,判断通过后,在跳转之前一定要将flag置为false。
51、图片懒加载
vue使用插件的原理
52、表单验证
53、路由懒加载
原来我一直使用的import()是路由懒加载,哈哈!,
//详情页面组件 {
//需要params传参(产品id) path: "/detail/:skuId", name: 'Detail', component: ()=> import('../pages/Detail'), meta:{
show: true}, }, //添加购物车成功 {
path: "/addcartsuccess", name: 'AddCartSuccess', component: ()=> import('../pages/AddCartSuccess'), meta:{
show: true}, },
54、打包项目
项目到此基本就完成了,接下来就是打包上线。在项目文件夹下执行npm run build。会生成dist打包文件。

dist就是我们打包好的项目文件

dist文件下的js文件存放我们所有的js文件,并且经过了加密,并且还会生成对应的map文件。
map文件作用:因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了map就可以向未加密的代码一样,准确的输出是哪一行那一列有错。
55、Vue新知识点(只针对个人)
1、DOM、事件、props个人理解
<button @click="handle" :msg="msg"></button>
(2)自定义组件。比如我们之前自定义的分页器组件例如:
<PageNation @click="getPageNo" :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="totals" :continues="5"/>
注意:刚开始这里有一个问题,我们上面:continues=”5″传递的明明是一个常量为什么还需要v-bind绑定事件呢?
官方解释
传递字符串常量:
传入一个数字 即便42是常量,我们仍然需要v-bind来告诉 Vue。这是一个 JavaScript 表达式而不是一个字符串
2、ref使用
使用步骤:
ref使用参考链接
3、$ children $parent使用
在56小节中讲到,如果我们父组件想要获取修改子组件信息,可以通过ref实现。但是,当子组件较多时,就会出现多次的$refs操作,会十分的麻烦。所以,引入了children属性。
children 属性
每个组件都有children属性,可以通过this.$ children操作,该属性会返回当前组件的所有子组件信息,接下来就可以实现子组件信息的获取和修改。
parent属性
了解了children属性,那么parent也比较好理解。子组件可以通过parent属性获取父组件的全部信息,同样也可以修改父组件的信息。
例题:想要通过点击子组件,使得父组件的money变量减100。
4、插槽使用
<template> <div> <slot :item1="{'a':1,'b':2}" item2="asd1">e了吗</slot> </div> </template>
父组件
<template> <div> <HintButton title="提示" icon="el-icon-delete" type="danger" @click="handler"> <template v-slot:default="slopProps" > <p>{
{
slopProps}}</p> <p>{
{
slopProps.item1}}</p> <p v-for="(item,index) in slopProps.item1">{
{
index}}----{
{
item}}</p> </template> </HintButton> </div> </template>
插槽的原理就是在子组件(HintButton)内定义一个slot(插槽),父组件可以向该插槽内插入数据。
父组件向子组件传递信息还是通过props传递,这里就不多做赘述。
子组件想父组件传递信息时可以通过插槽传递。
插槽官方链接
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/177114.html原文链接:https://javaforall.net
