尚硅谷前端项目开发笔记

B站视频直达,这个项目亮点在于所有 API 请求都并非在组件内编写,而是在组件内使用this.$store.dispatch() 派发任务,再由 Vuex(actions、mutations、state三连操作) 获取后端数据后,渲染页面数据。

起步:

一、安装 Vue 脚手架

npm install -g @vue/cli

二、创建项目

vue create project(项目名称)

目录详解:

|- public         // 静态页面目录,Webpack进行打包的时候会原封不动打包到dist文件夹中|- index.html // 项目入口文件 (Webpack打包的js,css会自动注入到该页面中)
|- src            // 源码目录 (程序员开发代码文件夹)|- assets         // 存放项目中用到的静态资源文件,例如:css 样式表、图片资源|- components     //  非路由组件、全局组件 (封装的、可复用的组件,都要放到 components 目录下)|- views         //  存放路由组件|- App.vue        //  唯一根组件|- main.js        //  项目的入口文件。项目的运行时最先执行的文件
|- babel.config.js    // 配置文件(babel相关)
|- package.json       // 项目的详细信息记录
|- package-lock.json  // 缓存性文件(各种包的来源)

vue 项目的运行流程:

通过 main.jsApp.vue渲染到index.html的指定区域中。

vue-cli 打开项目流程

index.html  ==>  main.js ==> render(App.vue)
// 网页访问的是 index.html  ,它引入了  main.js
// main.js 作为入口文件,它渲染了 App.vue。

项目配置

1. 项目运行时,自动打开浏览器

package.json"scripts": {"serve": "vue-cli-service serve --open", // --open 运行项目时自动打开浏览器"build": "vue-cli-service build","lint": "vue-cli-service lint"},

2. 开启 代码 CSS source maps和 关闭eslint校验工具

Vue项目中的vue.config.js文件就是我们之前使用的 webpack.config.js

  • 关闭 eslint 校验工具(不关闭会有各种规范,不按照规范就会报错)
  • 开启 CSS source maps 可以定位到样式位置 (在哪个组件和在哪一行)

根目录下vue.config.js文件设置:

module.exports = {css: {sourceMap: true, // 开启 CSS source maps}, lintOnSave: false  //关闭eslint
}

3. 代码改变时实现页面自动刷新

vue.config.js加入:

module.exports = {//关闭eslintlintOnSave: false,devServer: {// 默认值为true开启热更新,false 则手动刷新inline: true,// 设置本地调试端口port: 8001,}
}

注意:修改完该配置文件后,要重启一下项目。

4. 设置路径别名,用@/代替src/

在根目录创建 jsconfig.json文件并写入:

 {"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"]}},"exclude": ["node_modules","dist"]}

注意: 上面这个方法每次新建项目都要创建一个文件,如果你使用的是 vscode,使用下面这个方法可以一劳永逸:

1.在 vscode 扩展工具里安装 Path Autocomplete 插件(插件不唯一,也可以使用其他插件)

2.然后对其进行配置, 找到配置项Settings.json配置文件加入:

// 配置 @ 的路径提示"path-autocomplete.pathMappings": {"@": "${folder}/src"},

清除vue页面默认的样式

我们可以需要修改public下的index.html文件:

<link rel="stylesheet" href="reset.css">

或者在 main.js 引入

import '@/assets/css/reset.css';
* {box-sizing: border-box;font: inherit;vertical-align: baseline;/** 这个属性只用于iOS, 当你点击一个链接或者通过Javascript定义的可点击元素的时候* 它就会出现一个半透明的灰色背景*/-webkit-tap-highlight-color: rgba(0,0,0,0);
}html{background-color:#fff;color:#000;font-size: 12px;-webkit-text-size-adjust: 100%; /* 禁止字体变化 */
}body {-webkit-overflow-scrolling: touch; /* 设置滚动容器的滚动效果 */-webkit-font-smoothing: antialiased; /* 字体抗锯齿渲染 */
}html,body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,
input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0;
}body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height: 1.5;font-family: tahoma,arial,"Hiragino Sans GB",simsun,sans-serif;
}h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%;
}h1,h2,h3,h4,h5,h6{font-family: tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif;
}h1,h2,h3,h4,h5,h6,b,strong{font-weight:normal;
}address,cite,dfn,em,i,optgroup,var{font-style:normal;
}table{border-collapse:collapse;border-spacing:0;text-align:left;
}caption,th{text-align:inherit;
}ul,ol,menu{list-style:none;
}fieldset,img{border:0;
}img,object,input,textarea,button,select{vertical-align:middle;
}article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block;
}audio,canvas,video{display:inline-block;*display:inline;*zoom:1;
}blockquote:before,
blockquote:after,
q:before,
q:after{content:"\0020";
}textarea{overflow:auto;resize:vertical;
}input,textarea,button,select,a{outline:0 none;border: none;
}button::-moz-focus-inner,
input::-moz-focus-inner{padding:0;border:0;
}mark{background-color:transparent;
}a,ins,s,u,del{text-decoration:none;
}sup,sub{vertical-align:baseline;
}a, a:active, a:hover {/*** 某些浏览器会给 a 设置默认颜色*/color: unset;text-decoration: none;
}
ol, ul, li {list-style: none;
}
input, textarea, select {outline: none; /*去掉fouce时边框高亮效果*/background: unset; /*去掉默认背景*/-webkit-appearance: none; /* 去除ios输入框阴影 */appearance: none;
}

路由组件和非路由组件区别

  • 非路由组件放在components中,通过标签使用
  • 路由组件 放在pages 或views 中,通过配置路由使用
  • 所有的组件身上都会拥有$router$route属性
    • $router: 进行路由跳转
    • $route: 获取路由信息(name、path、params等)

路由跳转方式

声明式导航:通过<router-link/>标签 ,(理解为一个 a 标签,可以添加 class )

编程式导航 :声明式导航能做的编程式都能做,而且还可以处理一些业务

通过路由元信息设置组件显示与隐藏

我们在 App.vue 中导入了 Footer 组件,但是有些页面是不需要展示的,此时我们可以通过设置路由原信息 meta和搭配 v-show 按需在页面展示 Footer 组件:

<!-- 在Home与Search可见的,但是Login|Register不可见 -->
<!-- 利用路由元信息解决当前问题好处:一行代码就可以解决 -->
<Footer v-show="$route.meta.isHideFooter" />

在路由中设置meta :

 {path: '/home',component: Home,meta: {isHideFooter: true}},

为什么使用 v-show 而不使用 v-if ?

v-show 与v-if 区别 :

1. 展示形式不同
v-if是 创建一个dom节点,通过元素上树与下树进行操作
v-show 是display:none 、 block,通过样式display控制2. 使用场景不同
初次加载v-if要比v-show好,页面不会做加载盒子
频繁切换v-show要比v-if好,创建和删除的开销太大了,显示和隐藏开销较小

因为v-if会频繁的操作dom元素消耗性能,v-show只是通过样式将元素显示或隐藏。

路由传参

显示传值 (query)

query参数:不属于路径当中的一部分,路由不需要占位,写法类似于 ajax 当中query参数。

地址栏表现为: /about?k1=v1&k2=v2

1. 显式  ==> 参数在url上 http://localhost:8080/about?a=1传:this.$router.push({path:'/about',query:{a:1}})接:this.$route.query.a

隐式传值 (params)

params参数:属于路径当中的一部分,需要注意配置路由的时候需要占位 。地址栏表现为 /search/v1/v2

需要单独配置路由信息,如 :

{name: 'search', // 当前路由的标识名称path: '/search/:keyword?', // /:keyword就是一个params参数的占位符,?表示该参数可传可不传component: Search,},{name: 'detail',path: '/detail/:skuId', // 配置路由跳转 params参数 component: Detail},

注意: 因为使用隐式传值跳转刷新界面后参数丢失问题无法解决,所以路径传值一定是显示的。

params传参问题

1. 如果路由path已经要求传递 params 参数,但是没有传递,会发现地址栏URL有问题, 详情如下:
1. Search路由项的path已经指定要传一个keyword的params参数:path: "/search/:keyword",2. 在Search组件中 执行下面进行路由跳转的代码:this.$router.push({name:"Search",query:{keyword:this.keyword}})当前跳转代码没有传递params参数地址栏信息:http://localhost:8080/#/?keyword=asd此时的地址信息少了/search正常的地址栏信息: http://localhost:8080/#/search?keyword=asd解决方法:可以通过改变path来指定params参数可传可不传 path: "/search/:keyword?",  ?表示该参数可传可不传

参考链接

2. 右上面可以知道 params 可传可不传,但是如果传递的时空串,如何解决 ?

错误写法:

 this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''}})

出现的问题和 1 中的问题相同, 地址信息少了/search,解决方法: 加入|| undefined,当我们传递的参数为空串时地址栏url 也可以保持正常:

this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''||undefined}})
3. 路由组件能不能传递props数据?

可以,但是只能传递params参数, 使用方法如下:

{name: 'search', // 当前路由的标识名称path: '/search/:keyword?', // /:keyword就是一个params参数的占位符,?表示该参数可传可不传component: Search,// 将params参数和query参数映射成 props 传入路由组件props: route => ({ keyword3: route.params.keyword, keyword4: route.query.keyword2 })},

路由传参方式:

1. 字符串形式
this.$router.push("/search/"+this.params传参+" ? k= "+this.query传参)
2.模板字符串
this.$router.push(`/search/"+this.params 传参 +" ? k= "+this.query传参`)
3.对象(常用)
this.$router.push({name:“路由名字”,params:{传参},query:{传参})

注意: 对象方式传参时,如果我们传参中使用了params,要跳转的路由只能使用name,不能使用path,query传参才可以使用 path。

多次跳转同一个链接控制台出现警告

多次执行相同的push问题,控制台会出现警告, 例如 :

let result = this.$router.push({name:"Search",query:{keyword:this.keyword}})
console.log(result)  //返回了一个Promise

原因:因为 push返回的是一个Promise 对象,而Promise对象需要传递成功和失败两个参数,我们的push中并没有传递。

解决方法:

this.$router.push({name:‘Search’,params:{keyword: '3'||undefined}},()=>{},()=>{})
// 后面两项分别代表执行成功和失败的回调函数

这种写法治标不治本,将来在别的组件中使用编程式导航(push|replace)还是会有类似错误

push是VueRouter.prototype的一个方法,在router中的index重写该方法即可:

//重写VueRouter.prototype原型对象身上的push|replace方法
//先把VueRouter.prototype身上的push|replace方法进行保存一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;VueRouter.prototype.push = function(location, resolve, reject) {//第一个形参:路由跳转的配置对象(query|params)//第二个参数:undefined|箭头函数(成功的回调)//第三个参数:undefined|箭头函数(失败的回调)if (resolve && reject) {//push方法传递第二个参数|第三个参数(箭头函数)//originPush:利用call修改上下文,变为(路由组件.$router)这个对象,第二参数:配置对象、第三、第四个参数:成功和失败回调函数originPush.call(this, location, resolve, reject);} else {//push方法没有产地第二个参数|第三个参数originPush.call(this,location,() => {},() => {});}
};VueRouter.prototype.replace = function(location, resolve, reject) {if (resolve && reject) {originReplace.call(this, location, resolve, reject);} else {originReplace.call(this,location,() => {},() => {});}
};

第二种写法:

// 解决设置路由拦截时 跳转到登入页面时候的报错(router版本问题)
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace
// push
VueRouter.prototype.push = function push(location, onResolve, onReject) {if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)return originalPush.call(this, location).catch(err => err)
}
// replace
VueRouter.prototype.replace = function push(location, onResolve, onReject) {if (onResolve || onReject) return originalReplace.call(this, location, onResolve, onReject)return originalReplace.call(this, location).catch(err => err)
}

定义全局组件

在 main.js 中注册:

//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);

在组件中使用: 已经注册为全局组件,因此不需要引入:

<template>
<div><TypeNav/>
</div>
</template>

封装 axios,设置请求和响应拦截器

为什么要设置拦截器?
  • 在请求或响应被 then 或 catch 处理前拦截它们,减少服务器不必要的请求。

  • 统一处理错误及配置请求信息

在请求拦截器中:携带token令牌(设置在请求头中)、Loding效果开始
在响应拦截器中: 统一处理弹窗,结束Loding效果

在根目录下创建api/request.js文件 :

import axios from "axios";
//1、对axios二次封装
const requests = axios.create({//基础路径,requests发出的请求在端口号后面会跟改baseURlbaseURL:'/api',timeout: 5000,
})
//2、配置请求拦截器 (可以携带请求头 token)
requests.interceptors.request.use(config => {// console.log("请求拦截器----------------",config);/*  config内主要是对请求头Header配置比如添加token(如果token在本地存储还在,就携带在请求头中)--- config.headers.Authorization = token */return config;
})
//3、配置响应拦截器
requests.interceptors.response.use((res) => {// console.log("响应拦截器=------------------", res.data)/*let { code, msg } = res.data;if ( msg) {// 成功弹窗if (code == 0) return  Message({type: "success",message: msg})Message.error(msg);} */return  res.data;  //成功的回调函数
},(error) => {// console.log("响应失败回调--------------",error);return Promise.reject(new Error('fail'))
})
//4、对外暴露一个axios实例
export default requests;

解决开发时请求 API 跨域问题

配置vue.config.js文件:

module.exports = {lintOnSave: false,devServer: {inline: false,port: 8001,//代理服务器解决跨域proxy: {//会把请求路径中的/api换为后面的代理服务器'/api': {//提供数据的服务器地址target: 'http://39.98.123.211',}},}
}

注意:这里和以往配置跨域不同,因为上面封装axios的时候, baseURL已经设置为了/api,所以我们在写请求的时候都会携带/api,在配置文件中我们就将/api进行了转换。在使用接口的时候就会拼接成http://39.98.123.211/api

封装接口并挂载到Vue原型

将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,方便统一修改接口。

在项目根目录下创建 api/index.js文件

//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/ajax";//首页三级分类接口
export const reqCateGoryList = () => {return  requests({url: '/product/getBaseCategoryList',method: 'GET'})
}

组件中按需使用 (不推荐):

import { reqCateGoryList } from './api'
reqCateGoryList(); //发起请求

每个页面都需要导入接口,是一件麻烦的事情,我们是否可以把所有接口挂载到vue原型上,在组件中按需导入,实现一劳永逸呢?

在main.js中导入所有接口:

import Vue from "vue";
import App from "./App.vue";
import router from "@/router";
import store from "@/store";
import * as API from '@/api'; //统一接口api文件夹里面全部请求函数new Vue({render: (h) => h(App),beforeCreate() {Vue.prototype.$API = API;},router,store,
}).$mount("#app");

使用:


mounted() {this.getPayInfo()
},
methods: {async getPayInfo() {let result = await this.$API.reqPayInfo(this.orderId)
}

async await 使用

如果项目中没有封装请求api,而是直接调用 axios ,就不需要使用async await,因为 axios 返回的就是一个Promise对象。

没有将函数封装前我们都会通过 then() 回调函数拿到服务器返回的数据,封装后依旧可以使用then获取数据:


categoryList(){let result =  reqCateGoryList().then(res=>{console.log(res)//   return res})console.log(result) // 返回的是一个 Promise对象
}

在vuex中使用使用挂载到vue原型方法

上面我们已经把封装的所有接口都挂载在 vue原型中了,但是我们在vuex中也不想按需导入接口了,就想像组件中那样直接使用,使用方法如下:


// import { reqGetBannerList } from "@/api";
state:{bannerList: [],
};
mutations : {GETBANNERLIST(state, bannerList) {state.bannerList = bannerList;},
};
actions :{async this._vm.$API.getBannerList({ commit }) {let result = await reqGetBannerList();if (result.code == 200) {commit("GETBANNERLIST", result.data);}}
}

说明:当我们在 vuex中 打印 this 时可以看见 store 对象中有一个 _vm 就是vue 的实例对象,所以我们才可以直接通过 this._vm.$API 使用。

使用到的插件:

1. nprogress 进度条插件

nprogress 进度条插件链接

发起请求时页面上方会出现蓝色(默认)进度条。

使用: 在请求拦截器前开启进度条,在响应拦截器成功后关闭。

对应的ajax.js设置:

import axios from "axios";
//引入进度条
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";
//1、对axios二次封装
const requests = axios.create({//基础路径,requests发出的请求在端口号后面会跟改baseURlbaseURL:'/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;

可以通过修改node_modules/nprogress/nprogress.css文件的background来修改进度条样式。

2. vue-lazyload 图片懒加载插件

懒加载vue-lazyload插件官网

下载:

cnpm i vue-lazyload -S

1. 引入插件 import VueLazyload from "vue-lazyload";
2、注册插件 Vue.use(VueLazyload)

vuex 注意事项

async addOrUpdateShopCart({commit},{skuId,skuNum}){let result = await reqAddOrUpdateShopCart(skuId,skuNum)
}

注意:使用action时,函数的第一个参数,必须是{commit},即使不涉及到 mutations 操作,也必须加上该参数,否则会报错。

详情见[Vuex] 官网: 和传值方法 跳转至参考链接→

getters使用

如果不使用getters属性,我们在组件获取state中的数据表达式为:this.$store.state.子模块.属性,又长又不方便复用,Vuex 允许我们在 store 中定义 getters(可以认为是 store 的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

我们在Search模块中获取商品列表数据就是通过getters实现,需要注意的是当网络出现故障时应该将返回值设置为空,如果不设置返回值就变成了undefined:

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获取仓库数据:

<script>//引入mapGettersimport {mapGetters} from 'vuex'export default {name: 'Search',computed:{//使用mapGetters,参数是一个数组,数组的元素对应getters中的函数名...mapGetters(['goodsList'])}}
</script>

使用getters 数据控制台出现 undefined 红色警告

访问undefined的属性值会引起红色警告,网络正常时不会出错,一旦无网络或者网络问题就会gg。

下细节在于getters的返回值。如果getters按上面代码写为return state.goodInfo.categoryView,页面可以正常运行,可以不处理,但是要明白红色警告警告的原因。

所以我们在写getters的时候要养成一个习惯在返回值后面加一个 || 条件。即当属性值undefined时,会返回 || 后面的数据,这样就不会报错。当然,如果返回值为对象加|| {},数组:|| [ ]

防抖和节流

什么是防抖和节流?
防抖:用户操作很频繁,但是只执行一次,减少业务负担。
节流:用户操作很频繁,但是把频繁的操作变为少量的操作,使浏览器有充分时间解析代码

[防抖和节流详情]https://www.jianshu.com/p/c8b86b09daf0

使用loadsh插件实现防抖和节流

  • loadsh官网

    – 防抖函数

  • – 节流函数

下面代码为设置节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。

// throttle是节流函数
import {throttle} from 'lodash' methods: {//鼠标进入修改响应元素的背景颜色//采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次changeIndex: throttle(function (index){this.currentIndex = index},50),//鼠标移除触发时间leaveIndex(){this.currentIndex = -1}}

上面并没有通过 npm 下载 loadsh插件,因为vue脚手架自带,直接引入即可。

使用编程式导航+事件委托实现路由跳转

如图所示,三级标签每一个标签都是一个页面链接,要实现通过点击表现进行路由跳转。

解决思路

  • 使用导航式路由:有多少个a标签就会生成多少个router-link标签,router-link是vue提供的组件,这样当我们频繁操作时会出现卡顿现象。
  • 使用编程式路由:通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。

解决办法:

使用编程时导航+事件委派 的方式实现路由跳转 :

事件委派即把子节点的触发事件都委托给父节点,这样只需要一个回调函数 goSearch 就可以解决。

事件委派问题:

1. 如何确定我们点击的一定是a标签呢和且通过点击才实现跳转呢?

为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。

2. 跳转时如何获取子节点标签的商品名称和商品id ?

为三个等级的 a 标签分别 添加自定义属性data-category1Id(一级)、data-category2Id(二级)、data-category3Id(三级) 并获取各级商品id,用于路由跳转。

<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>

然后再通过goSearch() 函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过 dataset 属性获取节点的属性信息。

//函数使用
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">//函数定义goSearch(event){console.log(event.target)  // 点击后会输出当前元素的 dom节点
}

注意: event是系统属性,只需要在函数定义的时候作为参数传入,函数使用的时候不需要传参。

完整代码:

goSearch(event){let element = event.target//html中会把大写转为小写// 获取和解构 4个自定义属性 let {categoryname,category1id,category2id,category3id} = element.dataset//categoryname存在,表示为a标签if(categoryname){//整理路由跳转的参数let location = {name:'Search'}   //跳转路由namelet query = {categoryName:categoryname} //路由参数// 判断属于哪一个等级的a标签if(category1id){query.category1Id = category1id    // 一级}else if(category2id){query.category2Id = category2id    // 二级}else if(category3id){ query.category3Id = category3id    // 三级}location.query = query //整理完参数this.$router.push(location) //路由跳转}}

Vue 路由销毁问题

Vue 在路由切换的时候会销毁旧路由。

我们在三级列表全局组件中的mounted进行了请求一次商品分类列表数据,当我们在包含三级列表全局组件的不同组件之间进行切换时,都会进行一次信息请求。

由于信息都是一样的,处于性能考虑我们希望该数据只请求一次,所以我们把这次请求放在 App.vuemounted中,根组件App.vue 的mounted只会执行一次。

Mock 插件使用

mock.js官网

为什么使用 mock?

在开发中,有时候后端的接口还未完成,而前端开发者可以mock一些数据(模拟的一些假的接口),当后端接口完成时,再把mock数据变为后台给的接口数据替换。

插件使用:

下载和引入:

cnpm i mockjs -S

mock用来拦截前端ajax请求,返回我们自定义的数据用于测试前端接口。我们可以将不同的数据类型封装为不同的json文件,创建mock/mockServer.js文件:

//先引入mockjs模块
import Mock from 'mockjs';
//把JSON数据格式引入进来[JSON数据格式根本没有对外暴露,但是可以引入]
//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});
让项目启动时能访问到 mock 接口

mock 接口书写完毕后,mock当中 mockServer.js 需要执行一次,如果不执行,和你没有书写一样的。

回到入口文件,引入mockServer.js :

import "@/mock/mockServe"; //引入MockServer.js----mock数据

vuex 数据存储与使用

  • 在store 中的actions 中发起请把数据提交给mutations
  • mutations 中把数据存放在 state

以我们的首页轮播图数据为例:

(1). 在轮播图组件加载完毕后向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的部分
state : {bannerList: [],
};
mutations:{BANNERLIST(state,bannerList){state.bannerList = bannerList}
}

由于数据是通过异步请求获得的,在轮播图组件使用时,我们要通过计算属性computed 获取vuex中的state

 <script>
import {mapState} from "vuex";
export default {//主键挂载完毕,请求轮播图图片mounted() {this.$store.dispatch("getBannerList")},computed:{...mapState({bannerList: (state => state.home.bannerList)})}
}
</script>

swiper 插件实现轮播图并封装成公共组件

1.安装swiper、高版本问题多。

cnpm i swiper@5 -S

2.在需要使用轮播图的组件内导入swpier和它的css样式

3.在组件中创建swiper需要的dom标签(html代码,参考官网代码)

4.创建swiper实例

解决在 mounted 创建 swiper实例图片无法加载问题

我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。

解决思路:

  1. 使用定时器 (不完美)
  2. 使用 watch,只能保证在bannerList变化时创建swiper对象,不能保证此时v-for已经执行完了。

swiper对象生效的前提是dom结构已经渲染好了,假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片。

完美解决方案:使用 watch 侦听器 +this.$nextTick()

<template><div class="swiper-container" ref="cur"><div class="swiper-wrapper"><div class="swiper-slide" v-for="carousel in list" :key="carousel.id"><img :src="carousel.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div>
</template><script>
//引入Swiper
import Swiper from 'swiper'
export default {name: 'Carousel',props: ['list'],watch: {list: {//立即监听:不管你数据有没有变化,我上来立即监听一次//为什么watch监听到list:因为这个数据从来没有发生变化(数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的)immediate: true,handler() {//只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定的,因此还是需要用nextTickthis.$nextTick(() => {var mySwiper = new Swiper(this.$refs.cur, {autoplay: {delay: 3000,stopOnLastSlide: false,disableOnInteraction: false},loop: true,// 如果需要分页器pagination: {el: '.swiper-pagination',//点击小球的时候也切换图片clickable: true},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev'}})})}}}
}
</script>

注意:之前我们在学习watch时,一般都是监听的定义在data中的属性,但是我们这里是监听的computed中的属性,这样也是完全可以的,并且如果你的业务数据也是从store中通过computed动态获取的,也需要watch监听数据变化执行相应回调函数。

使用 ref 来避免页面中多个轮播图组件返回的是同样的数据

// 在轮播图最外层DOM中添加ref属性
<div class="swiper-container" id="mySwiper" ref="cur">
// 通过ref属性值获取DOMnew Swiper(this.$refs.cur,{...})

使用watch监听路由变化实现动态搜索

最初想法:在每个三级列表和收缩按钮加一个点击触发事件,只要点击了就执行搜索函数。

但是这是一个很蠢的想法,如果这样就会生成很多回调函数,很耗性能。

最佳方法:我们每次进行新的搜索时,我们的query和params参数中的部分内容肯定会改变,而且这两个参数是路由的属性。我们可以通过监听路由信息的变化来动态发起搜索请求。


//数据监听:监听组件实例身上的属性的属性值变化watch: {//监听路由的信息是否发生变化,如果发生变化,再次发起请求$route(newValue, oldValue) {//每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3// 合并参数对象,再次发请求之前整理带给服务器参数Object.assign(this.searchParams, this.$route.query, this.$route.params)//再次发起ajax请求this.getData()//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据//所以每次请求结束后将相应参数制空this.searchParams.category1Id = undefined// 使用 undefined是为了提示性能,路由将不会携带undefined参数,如果使用空字符串还是会被传入this.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefined}}

搜索页面包屑相关操作

本次项目的面包屑操作主要就是两个删除逻辑:

  • 当分类属性(query)删除时删除面包屑同时修改路由信息。
  • 当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。

删除分类:

//删除分类的名字
removeCategoryName() {//把带给服务器的参数置空了,还需要向服务器发请求//带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器//但是你把相应的字段变为undefined,当前这个字段不会带给服务器this.searchParams.categoryName = undefinedthis.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefinedthis.getData()//地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)this.$router.push({ name: 'search', params: this.$route.params })}

删除搜索关键字:

//删除关键字
removeKeyword() {//给服务器带的参数searchParams的keyword置空this.searchParams.keyword = undefined//通知兄弟组件Header清除关键字this.$bus.$emit('clear')//进行路由的跳转if (this.$route.query) {this.$router.push({ name: 'search', query: this.$route.query })}
}

header组件接受$bus通信:

mounted() {//  组件挂载时就监听clear事件,clear事件在search模块中定义//  当删除关键字面包屑时,触发该事件,同时header的输入框绑定的keyword要删除this.$bus.$on("clear",()=>{this.keyword = ''})}

搜索页子组件传参及面包屑操作

SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性,原理与搜索页相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化会影响路由地址变化。

总结:面包屑由四个属性影响:parads、query、品牌、手机属性
面包屑生成逻辑:判断相关属性是否存在,存在即显示。

商品排序

排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。

order属性值为字符串:1:asc     1代表综合
2:desc     2代表价格asc代表升序,desc代表降序

升降序时改变上下箭头图标:

下载阿里图标并引入:

// 在public/index引入该 css
<link rel="stylesheet" href="https://at.alicdn.com/t/font_2994457_qqwrvmss9l9.css">
<div class="sui-navbar"><div class="navbar-inner filter"><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></div>
</div>

搜索页逻辑代码:


<script>
import SearchSelector from './SearchSelector.vue'
import { mapGetters, mapState } from 'vuex'
export default {name: 'Search',data() {return {searchParams: {//产品相应的idcategory1Id: '',category2Id: '',category3Id: '',//产品的名字categoryName: '',//搜索的关键字keyword: '',//排序:初始状态应该是综合且降序order: '1:desc',//第几页pageNo: 1,//每一页展示条数pageSize: 3,//平台属性的操作props: [],//品牌trademark: ''}}},components: {SearchSelector},//在挂载之前调用一次|可以在发请求之前将带有参数进行修改beforeMount() {//在发请求之前,把接口需要传递参数,进行整理(在给服务器发请求之前,把参数整理好,服务器就会返回查询的数据)Object.assign(this.searchParams, this.$route.query, this.$route.params)},mounted() {//在发请求之前咱们需要将searchParams里面参数进行修改带给服务器this.getData()},methods: {//把发请求的这个action封装到一个函数里面//将来需要再次发请求,你只需要在调用这个函数即可getData() {this.$store.dispatch('getSearchList', this.searchParams)},//删除分类的名字removeCategoryName() {//把带给服务器的参数置空了,还需要向服务器发请求//带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器//但是你把相应的字段变为undefined,当前这个字段不会带给服务器this.searchParams.categoryName = undefinedthis.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefinedthis.getData()//地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)//严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着/**   这里永远成立,即使是空对象会跳转 */if (this.$route.params) {this.$router.push({ name: 'search', params: this.$route.params })}},//删除关键字removeKeyword() {//给服务器带的参数searchParams的keyword置空this.searchParams.keyword = undefined//再次发请求,(其实没必要,路由跳转后属性变化,watch内的函数会重新发起请求)this.getData()//通知兄弟组件Header清除关键字// ps: 这里也可以通过检测路由里的 keyword是否为空,如果为空就修改header组件的值this.$bus.$emit('clear')//进行路由的跳转if (this.$route.query) {this.$router.push({ name: 'search', query: this.$route.query })}},//自定义事件回调trademarkInfo(trademark) {//1:整理品牌字段的参数  "ID:品牌名称"this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`//再次发请求获取search模块列表数据进行展示this.getData()},//删除品牌的信息removeTradeMark() {//将品牌信息置空this.searchParams.trademark = undefined//再次发请求this.getData()},//收集平台属性地方回调函数(自定义事件)attrInfo(attr, attrValue) {//["属性ID:属性值:属性名"]console.log(attr, attrValue)//参数格式整理好let props = `${attr.attrId}:${attrValue}:${attr.attrName}`//数组去重  splice(index,1) set include indexOf 都可以if (this.searchParams.props.indexOf(props) == -1) {this.searchParams.props.push(props)//再次发请求this.getData()}},//removeAttr删除售卖的属性removeAttr(index) {//再次整理参数this.searchParams.props.splice(index, 1)//再次发请求this.getData()},//排序的操作changeOrder(flag) {//flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2)//现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】let originOrder = this.searchParams.orderlet orginsFlag = originOrder.split(':')[0]let originSort = originOrder.split(':')[1]//新的排序方式let newOrder = ''// 获取flag取asc和desc是否存在  不存在取反//判断的是多次点击的是不是同一个按钮if (flag == orginsFlag) {newOrder = `${orginsFlag}:${originSort == 'desc' ? 'asc' : 'desc'}`} else {//点击不是同一个按钮newOrder = `${flag}:${'desc'}`}//需要给order重新赋值this.searchParams.order = newOrder//再次发请求this.getData()},//自定义事件的回调函数---获取当前第几页getPageNo(pageNo) {//整理带给服务器参数this.searchParams.pageNo = pageNo//再次发请求this.getData()}},computed: {//mapGetters里面的写法:传递的数组,因为getters计算是没有划分模块【home,search】...mapGetters(['goodsList']),isOne() {return this.searchParams.order.indexOf('1') != -1},isTwo() {return this.searchParams.order.indexOf('2') != -1},isAsc() {return this.searchParams.order.indexOf('asc') != -1},isDesc() {return this.searchParams.order.indexOf('desc') != -1},//获取search模块展示产品一共多少数据...mapState({total: (state) => state.search.searchList.total})},//数据监听:监听组件实例身上的属性的属性值变化watch: {//监听路由的信息是否发生变化,如果发生变化,再次发起请求$route(newValue, oldValue) {//每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3//再次发请求之前整理带给服务器参数Object.assign(this.searchParams, this.$route.query, this.$route.params)//再次发起ajax请求this.getData()//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据this.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefined}}
}
</script>

组件通信方式总结

 props  父向子$on、$emit 子向父$bus  全局事件总线(通常用于兄弟组件传值)Vuex     全局组件共享slot插槽  适用于父子组件通信

手写分页器

开发分页器必须的核心属性:

pageNo    当前页码
pageSize  每一页展示多少条数据
total     总数据
continues 连续展示的页码 (连续页码数一般为5、7、9 奇数,对称好看)totalPage 总页数 Math.ceil(总数/ 每一页多少条数据)

核心逻辑: 获取连续页码的起始页码和末尾页码,通过计算属性获得。

// 父组件传递子组件的数据: 当前页、每一页展示多少条数据、总数据、连续页码数props: ['pageNo', 'pageSize', 'total', 'continues'],computed: {//总共多少页totalPage() {//向上取整(计算总页数)return Math.ceil(this.total / this.pageSize)},//计算出连续的页码的起始数字与结束数字[连续页码的数字:至少是5]startNumAndEndNum() {// 解构出 连续页码数、当前页码、总页数const { continues, pageNo, totalPage } = this//先定义两个变量存储起始数字与结束数字let start = 0,end = 0//连续页码数字5【就是至少五页】,如果出现不正常的现象【就是不够五页】//不正常现象【总页数小于连续页码数】if (continues > totalPage) {start = 1end = totalPage} else {//正常现象【连续页码5,但是你的总页数一定是大于5的】   Math.floor:向下取整//起始、结束数字start = pageNo - Math.floor(continues/2)end = pageNo + Math.floor(continues/2)//把出现不正常的现象【start数字出现0|负数】纠正if (start < 1) {start = 1end = continues}//把出现不正常的现象[当前页码数大于总页码]纠正if (end > totalPage) {end = totalPagestart = totalPage - continues + 1}}return { start, end }}}

当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。

字符串拼接

// 在js中使用
var a = n;
console.log(`a的值是:${a}`); //a的值是:n// 在html中使用
<router-link :to="`/detail/${goods.id}`"></router-link>

解决使用vue-router跳转后不回顶部问题

router滚动行为详情

import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes.js'Vue.use(VueRouter)// 向外默认暴露路由器对象
let router = new VueRouter({routes, // 注册所有路由//router-link跳转时回到顶部scrollBehavior(to, from, savedPosition) {//返回的这个y=0,代表的滚动条在最上方return { y: 0 }}
})

商品详情难点

1. 点击轮播图图片时,改变放大镜组件展示的图片

豪哥的方法很巧妙:在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex 实现点击图片边框高亮设置,当符合图片的下标满足currentIndex===index时,该图片就会被标记为选中。

  <div class="swiper-container" ref="cur"><div class="swiper-wrapper"><div class="swiper-slide" v-for="(skuImage,index) in skuImageList" :key="skuImage.id"><img :src="skuImage.imgUrl" :class="{active:currentIndex===index}" @click="changeImg(index)"></div></div><div class="swiper-button-next"></div><div class="swiper-button-prev"></div></div>

2. 轮播图组件和放大镜组件是兄弟组件,所以要通过全局总线通信

在轮播图组件中,点击图片触发全局事件changeImg,参数为图片所在数组的下标:

 changeImg(index){//将点击的图片标识位高亮this.currentIndex = index//通知兄弟组件修改大图图片this.$bus.$emit("changeImg",index)}

对应的放大镜组件,在mounted监听该全局事件

mounted() {this.$bus.$on("changeImg",(index)=>{//修改当前响应式图片this.currentIndex = index;})},

放大镜组件中也会有一个currentIndex,他用表示大图中显示的图片的下标(因为放大镜组件只能显示一张图片),全局事件传递的 index 赋值给currentIndex ,通过computed计算属性改变放大镜组件展示的图片下标。

// 放大镜组件展示图片的html代码
<img :src="imgObj.imgUrl " />computed:{imgObj(){return this.skuImageList[this.currentIndex] || {}}},

参考链接 : JavaScript 实现放大镜功能

失焦事件

blur 与 change: 输入结束后,离开输入框,会先后触发 change 与 blur1. 没有进行任何输入时:不会触发change,但是会触发 blur2. 输入后值并没有发生变更时:change依旧不会触发keydown、input、keyup、blur都会触发

路由跳转时的复杂数据传参

当我们想要实现路由跳转并传递数据时,首相想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的复杂信息,就可以通过Web Storage实现。

本地存储与会话存储区别

共性: 都是已字符串形式存储:sessionStorage  会话存储,当前窗口关闭后就会删除。
localStorage    本地存储,存储在浏览器中,再次打来还存在。存储前: JSON.stringify()将对象转为字符串
取数据: JSON.parse()将字符串转为对象

删除多个商品(actions扩展)

问题点: 由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。

解决思路: 我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。

// 官网的教程,一个标准的actions函数如下所示:deleteAllCheckedById(context) {console.log(context)}

我们可以看一下context到底是什么:

可以看到 context中包含有:

commit、dispatch、getters、state

所以我们可以在actions函数中通过dispatch调用其他的actions函数,可以通过getters获取仓库的数据:

//删除选中的所有商品
deleteAllCheckedById({dispatch,getters}) {getters.getCartList.cartInfoList.forEach(item =>  {let result = [];//将每一次返回值添加到数组中result.push(item.isChecked === 1?dispatch('deleteCartById',item.skuId):'')   })return Promise.all(result)
},

购物车组件method批量删除函数:

//删除选中的所有商品
async deleteAllCheckedById(){try{await this.$store.dispatch('deleteAllCheckedById')//删除成功,刷新数据this.$store.dispatch("getCartList")}catch (error){alert(error)}
}

修改商品的全部状态和批量删除的原理相同:

// --------- vuex 中的 actions ----------------->
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 : 0console.log(flag)try{await this.$store.dispatch('updateAllChecked',flag)this.$store.dispatch("getCartList")}catch (error){alert(error)}
}
购物车数据为空时控制台报错bug 纠正:

虽然getters中在获取 getCartList 时已经设置了 || {},但在组件中我们通过 computed 获取的是 getters 中的cartInfoList,它是一个数组,所以我们还需设置默认返回值。

问题原因: 组件的 computed 中的cartInfoList没有写 || []返回值

 cartInfoList(){return this.getCartList.cartInfoList || [];
},

总结: 即使在getters设置了默认返回值,但是在组件中使用时还要使用计算属性筛选数据,必须再次设置默认返回值。

阻止from表单@click触发登录事件时候的默认行为

由于登录按钮的父节点是一个form表单,如果使用@click触发登录事件,form表单会执行默认事件action实现页面跳转。这里我们使用@click.prevent,它可以阻止自身默认事件的执行。

actions登陆函数:

//登录async userLogin({commit},data){let result = await reqPostLogin(data)//服务器会返回tokenif(result.code === 200){//token存入vuexcommit("SETUSERTOKEN",result.data.token)//持久化存储tokenlocalStorage.setItem('TOKEN',result.data.token)return 'ok'}else{return Promise.reject(new Error(result.message))}},

mutations设置用户token:

//设置用户tokenSETUSERTOKEN(state,token){state.token = token}

登陆组件methods登陆函数:

 async goLogin(){try{//会将this中的phone,password以对象的形式返回const {phone,password} = thisphone && password && await this.$store.dispatch('userLogin',{phone,password})//路由跳转到home首页this.$router.push('/home')}catch (error){alert(error)}}

登陆成功后获取用户信息:

//-----------actions函数
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}

配置导航守卫

//--------------------router index.js全局前置守卫代码
router.beforeEach(async(to, from, next) =>  {let token = store.state.user.tokenlet 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')}}
})

路由独享的守卫

引出问题:

全局导航守卫已经帮助我们限制未登录的用户不可以访问相关页面。但是还会有一个问题

例如:

用户已经登陆,用户在home页直接通过地址栏访问trade结算页面,发现可以成功进入该页面,正常情况,用户只能通过在shopcart页面点击去结算按钮才可以到达trade页面。

通过路由独享守卫解决该问题:

// 在trade路由信息中加入路由独享守卫
//交易组件{name: 'Trade',path: '/trade',meta: {show:true},component:  () => import('@/pages/Trade'),//路由独享首位beforeEnter: (to, from, next) => {if(from.path ===  '/shopcart' ){next()}else{next(false) // 指回到from路由}}}

此时又一个bug , 当我们在shopcart页面通过地址栏访问trade时还是会成功 !!!

解决办法:在shopcart 添加一个路由元信息 meta:{flag: false}

当点击去结算按钮后,将flag置为true。在trade的独享路由守卫中判断一下flag是否为true,当flag为true时,代表是通过点击去结算按钮跳转的,所以就放行。

 //购物车{path: "/shopcart",name: 'ShopCart',component: ()=> import('../pages/ShopCart'),meta:{show: true,flag: false},},
// shopcart组件去结算按钮触发事件
toTrade(){this.$route.meta.flag = truethis.$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){// 注意,判断通过后,在跳转之前一定要将flag置为false。from.meta.flag = falsenext()}else{next(false)}}}

二级路由(子路由)问题

配置二级路由:

//个人中心{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'就好了。

项目中使用到的 JavaScript方法回顾:

Object.asign()     浅拷贝
JSON.stringify()   深拷贝

every函数使用

every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false

【例】:判断底部勾选框是否全部勾选:

//判断底部勾选框是否全部勾选
isAllCheck() {//every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回falsereturn this.cartInfoList.every(item => item.isChecked === 1)
}

Promise.all:

Promise.all可以将多个Promise实例包装成一个新的Promise实例。成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

//删除选中的所有商品
deleteAllCheckedById({dispatch,getters}) {getters.getCartList.cartInfoList.forEach(item =>  {let result = [];//将每一次返回值添加到数组中result.push(item.isChecked === 1?dispatch('deleteCartById',item.skuId):'')   })return Promise.all(result)
},

ES6 中的 const 新用法:

const {comment,index,deleteComment} = this

上面的这句话是一个简写,最终的含义相当于:

const  comment = this.comment
const  index = this.index
const   deleteComment = this.deleteComment

打包项目

打包时取消输出 map 代码定位文件:

. map文件的作用:

可以理解为代码地图,因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时哪行的代码报错。而map文件就是用来代码定位,有了map才能准确的输出是哪一行那一列有错。

npm run build     打包项目因为map文件较大,上线前需要删除map文件,
也可以通过在`vue.config.js`配置 `productionSourceMap: false`实现打包时不输出map文件。

开发中使用 Vue 的总结:

1. 通过props实现父向之传值时为什么传入数字也需要使用 v-bind 呢 ?

为了告诉 Vue,传入的是 这是一个 JavaScript 表达式,如果不使用v-bind 将会被解析成一个字符串。 正确写法: <Son :likes="42"/>

2.$ref $ children 和 $parent使用


$refs:父组件访问子组件如果在普通的DOM元素上使用,引用指向的是DOM元素;如果用在子组件上,引用的是组件实例$children:父组件访问子组件 (如果是多次的$refs操作,我们可以使用$children属性)
$parent:  子组件访问父组件

3.解析Vue实例对象

$root 根实例
$options 每一个Vue实例都有一个实例对象 options
$children  子组件实例 (是一个数组)
$parent    父组件实例$emint 子组件抛出的自定义事件
$on 通过on接收自定义事件
$attrs和$props 加起来才是所有子组件的所有自定义属性:$attrs  能获取到子组件所有未被props接收的属性 (排除了$props、class、style以外的其他属性,极端情况下使用,注意不能直接用模板字符串使用$attrs的数据)
$props  获取到子组件的所有props,父子传参时使用,正常情况下都用prpos传参,因为是响应式的数据也更安全。
$data 拿到组件完整的data对象
$refs  如果是HTML标签,拿到的是dom对象,如果是组件标签,拿到的是组件实例对象
$vnode 当前组件的虚拟节点
$router 拿到VueRouter实例
$route  获取组件的路由信息(name、meta、path、query等参数)注意: App.vue  并不是根实例 ,是`$root根实例`的子组件。

尚硅谷前端项目开发笔记相关推荐

  1. 尚硅谷VUE项目-前端项目问题总结07--产品详情页【vuex-排他操作foreach-放大镜-轮播图-兄弟组件通信$bus-购物车-路由跳转传参-路由传参+会话存储】-游客身份-节流

    尚硅谷VUE项目-前端项目问题总结07---产品详情页 1.静态组件(详情页还未注册为路由组件) 2.发请求 3.vuex-获取产品详情信息 3.1放大镜 3.2 属性值[排他操作] 3.3轮播图[j ...

  2. 尚医通项目学习笔记Part1

    尚医通项目学习笔记 前言 一.目前学习进度 二.学习记录 1.项目简介 1.1 项目所会用到的技术栈 1.2 业务流程 2.项目学习笔记 2.1MyBatis-Plus相关 2.2搭建项目框架 2.3 ...

  3. 尚硅谷-离线数仓-笔记

    尚硅谷-离线数仓-笔记 一.数仓建模理论 第一章 数仓概述 1.1 数仓概念 数据仓库是一个为数据分析而设计的企业级数据管理系统.数据仓库可集中.整合多个信息源的大量数据,借助数据仓库的分析能力,企业 ...

  4. 尚硅谷云原生学习笔记(76~143集)

    笔记列表: 尚硅谷云原生学习笔记(1-75集) 尚硅谷云原生学习笔记(76~143集) 尚硅谷云原生学习笔记(144~172集) 尚硅谷云原生学习笔记(173~XXX集) 目录 76.为什么用kube ...

  5. 尚硅谷云原生学习笔记(1-75集)

    笔记列表: 尚硅谷云原生学习笔记(1-75集) 尚硅谷云原生学习笔记(76~143集) 尚硅谷云原生学习笔记(144~172集) 尚硅谷云原生学习笔记(173~XXX集) 目录 1.什么是云计算 1. ...

  6. 前端 | ( 九)尚品汇实操练习 | 尚硅谷前端html+css零基础教程2023最新

    学习来源:尚硅谷前端html+css零基础教程,2023最新前端开发html5+css3视频 系列笔记: [HTML4](一)前端简介 [HTML4](二)各种各样的常用标签 [HTML4](三)表单 ...

  7. 前端 | ( 十一)CSS3简介及基本语法(上) | 尚硅谷前端html+css零基础教程2023最新

    学习来源:尚硅谷前端html+css零基础教程,2023最新前端开发html5+css3视频 系列笔记: [HTML4](一)前端简介 [HTML4](二)各种各样的常用标签 [HTML4](三)表单 ...

  8. 前端 | (二)各种各样的常用标签 | 尚硅谷前端html+css零基础教程2023最新

    学习来源:尚硅谷前端html+css零基础教程,2023最新前端开发html5+css3视频 系列笔记: [HTML4](一)前端简介 [HTML4](二)各种各样的常用标签 [HTML4](三)表单 ...

  9. 京东投票项目开发笔记

    京东投票项目开发笔记 打开项目 $yarn install / $ npm install: 跑环境(把项目依赖的插件进行安装) $node admin.js: 启服务(把自己的计算机作为服务器,创建 ...

最新文章

  1. mysql练习题及答案_MySQL经典练习题及答案,常用SQL语句练习50题
  2. 「3」Java开发环境搭建
  3. Linux系统管理员修炼三层次
  4. 2015年 第6届 蓝桥杯 Java B组 省赛解析及总结
  5. 打开工程会提示下载的可能原因和可能解决方法
  6. RabbitMQ 的概念
  7. 基于stm32简易计算机电路图,基于STM32的简易电子计算器设计与实现(DOC).doc
  8. 【Python】编写一个类,求圆的周长和面积
  9. (86)FPGA读文件激励(readmemh)
  10. SuSE Linux 应用与安装
  11. 【转载】 MySQL之用户资源限制
  12. 修改了一个YUV/RGB播放器
  13. JavaWeb之Request与Response详解
  14. 解决ios7.x越狱后静态壁纸变为空白
  15. 如何判断一个单链表是否有环?
  16. 科大讯飞实现了APP用自己的声音听故事
  17. 人工智能软件工程师软件清单
  18. java orm全称_[Java-基础] 什么是ORM
  19. 【综合类型第 10 篇】什么是时间戳
  20. STM32开发笔记48:STM32F4+DP83848以太网通信指南系列(二):系统时钟

热门文章

  1. 计算机中存储单位的编号称号是什么,KB、MB、GB的中文单位名称是什么?
  2. @GeneratedValue与@GenericGenerator区别
  3. 企业局域网无线认证解决方案
  4. 不属于python标准库的是_Python标准库笔记(11) — Op
  5. Lemur的参数文件
  6. 建立live555海思编码推流服务
  7. 直流功率传感器行业调研报告 - 市场现状分析与发展前景预测
  8. Win10电脑有网其他联网软件能正常使用但打不开浏览器怎么办?
  9. SpringBoot拦截器失效问题excludePathPatterns失效问题
  10. 学好编程之GOC语言快速入门(1)