尚硅谷前端项目开发笔记_尚硅谷前端笔记-程序员宅基地

技术标签: es6/es7  周记  折腾日记  学习笔记  vue  前端  vue.js  javascript  

尚硅谷前端项目开发笔记

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 = {
    
    //关闭eslint
    lintOnSave: 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发出的请求在端口号后面会跟改baseURl
    baseURL:'/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发出的请求在端口号后面会跟改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;

可以通过修改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>
  //引入mapGetters
  import {
    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插件实现防抖和节流

下面代码为设置节流,如果操作很频繁,限制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'}   //跳转路由name
        let 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()
  • this. $nextTick 会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)

<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动态渲染结构我们还是没有办法确定的,因此还是需要用nextTick
        this.$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属性值获取DOM
 new 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 = undefined
      this.searchParams.category3Id = undefined
    }
  }
搜索页面包屑相关操作

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

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

删除分类:

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

删除搜索关键字:

//删除关键字
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: {
    
        //产品相应的id
        category1Id: '',
        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 = undefined
      this.searchParams.category1Id = undefined
      this.searchParams.category2Id = undefined
      this.searchParams.category3Id = undefined
      this.getData()
      //地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)
      //严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着
      /**   这里永远成立,即使是空对象会跳转 */
      if (this.$route.params) {
    
        this.$router.push({
     name: 'search', params: this.$route.params })
      }
    },
    //删除关键字
    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.order
      let 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 = undefined
      this.searchParams.category2Id = undefined
      this.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 = 1
        end = totalPage
      } else {
    
        //正常现象【连续页码5,但是你的总页数一定是大于5的】   Math.floor:向下取整
        //起始、结束数字
        start = pageNo - Math.floor(continues/2)
        end = pageNo + Math.floor(continues/2)
        //把出现不正常的现象【start数字出现0|负数】纠正
        if (start < 1) {
    
          start = 1
          end = continues
        }
        //把出现不正常的现象[当前页码数大于总页码]纠正
        if (end > totalPage) {
    
          end = totalPage
          start = 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 与 blur


1. 没有进行任何输入时:
	不会触发change,但是会触发 blur
	
2. 输入后值并没有发生变更时:
	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 : 0
    console.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)
        //服务器会返回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)
        }
      }

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

//-----------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.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')
        }
    }
})

路由独享的守卫

引出问题:

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

例如:

用户已经登陆,用户在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 = 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){
    
                // 注意,判断通过后,在跳转之前一定要将flag置为false。
                from.meta.flag = false
                next()
            }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,否则返回false
    return 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根实例`的子组件。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_45731256/article/details/122787785

智能推荐

9.任务段(TSS)_tss段-程序员宅基地

文章浏览阅读1k次。在调用门、中断门与陷阱门中,一旦出现权限切换,那么就会有堆栈的 ,切换。而且,由于CS的CPL发生改变,也导致了SS也必须要切换。切换时,会有新的ESP和SS(CS是由中断门或者调用门指定)这2个值从哪里来的呢?答案: TSS (Task-state segment ),任务状态段TSS就是一块内存,大小104个节不要把TSS与任务切换联系到一起TSS的意义就在于可以同时换掉一堆寄存器..._tss段

学习使用简单的php-程序员宅基地

文章浏览阅读47次。配置文件在:/etc/php5/$中,不同的模式含有自己的php.ini配置文件。php可以运行于多种模式:cgi、fastcgi、cli、web模块模式等4种;我现在使用的模式是cli模式,这里进行一次测试。在ubuntu下需要安装sudo apt-get install php5-devphp应该是php5的链接。修改config.m4文件:..._php 简单项目 为学习用

解决ios,iphone的safari浏览器h5自动放大,input获得焦点页面被放大_ios浏览器小于15像素的时候会进行放大-程序员宅基地

文章浏览阅读1.2k次。得到焦点之前设置font-size:16像素。_ios浏览器小于15像素的时候会进行放大

SpringBootAdmin 服务搭建记录_spring boot admin client与spring boot admin server都-程序员宅基地

文章浏览阅读520次。搭建过程网上很多, 主要是各个依赖的版本, 导致的各种 jar 包问题, 此处记录下我的 pom 和 yml 文件目录SpringAdmin server pom文件1. SpringAdmin server 配置(1) pom文件<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-p..._spring boot admin client与spring boot admin server都配置spring-boot-starter-

python3中图像识别的应用open-CV库_python open-cv 小图搜大图-程序员宅基地

文章浏览阅读1.4k次。python3中图像识别的应用open-CV库什么是open-CV?OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法(百度百科)。代码:定义图像识别的类import cv2import osfrom PIL import ImageGr_python open-cv 小图搜大图

Linux下安装anaconda3_anaconda do you wish to process the-程序员宅基地

文章浏览阅读1w次,点赞2次,收藏19次。1.下载anaconda清华大学开源软件镜像站anaconda下载地址:https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/2.安装Anaconda$ sudo sh Anaconda3-5.3.0-Linux-x86_64.sh [sudo] andrew 的密码: Welcome to Anaconda3 5.3.0In o..._anaconda do you wish to process the

随便推点

k8s 安装 kubernetes-dashboard-2.X_kubernetes 2.x-程序员宅基地

文章浏览阅读10w+次。安装使用 k8s 原生的 web图形化界面_kubernetes 2.x

saltstack自动化运维管理——saltstack之salt远程执行_salt 远程执行命令-程序员宅基地

文章浏览阅读287次。目录一、Salt命令的构成1、target2、funcation3、arguments二、编写远程执行模块1、编写模块2、了解YAML语法3、SLS4、配置管理(1)方法一(2)方法二(3)方法三(4)一些例子一、Salt命令的构成Salt命令由三个主要部分构成:salt '<target>' <function> [arguments]1、targettarget: 指定哪些minion, 默认的规则是使用glob匹配minion id。salt '*' test._salt 远程执行命令

关于协方差,协方差矩阵的个人理解_协方差矩阵的ρ-程序员宅基地

文章浏览阅读1.9k次,点赞3次,收藏10次。文章目录协方差协方差定义举例说明方差相关系数协方差矩阵(covariance matrix)举例说明数学符号表示协方差矩阵的应用马氏距离数学符号定义PCA降维使用sklearn中的np.cov遇到的坑协方差协方差定义协方差(Covariance)在概率论和统计学中用于衡量两个变量的总体误差。设有随机变量XXX和随机变量YYY,则协方差定义为:Cov(X,Y)=E((X−E[x])(Y−E[Y]))=E((Y−E[Y])(X−E[X]))Cov(X,Y)=E((X-E[x])(Y-E[Y]))_协方差矩阵的ρ

【LaTeX】LaTeX/Algorithms 伪代码_latex algorithm return-程序员宅基地

文章浏览阅读1.5k次,点赞2次,收藏4次。algorithmic和algorithmicx介绍下algorithmic和algorithmicx,这两个包很像,很多命令都是一样的,只是algorithmic的命令都是大写,algorithmicx的命令都是首字母大写,其他小写(EndFor两个大写)。下面是algorithmic的基本命令。\STATE <text>\IF{<condition>} \STATE{<text>} \ENDIF\FOR{<condition>} \STATE{_latex algorithm return

Linux和Windows操作系统,MySQL数据库备份(导出)和恢复(导入)_.nb3文件如何打开-程序员宅基地

文章浏览阅读1.3k次。方式一:通过终端执行命令(适用于Linux操作系统)备份:将DATABASENAME数据库备份到/opt目录生成DATABASENAME.db备份文件mysqldump -uUSERNAME-pPASSWORD--routines --databases DATABASENAME> /opt/DATABASENAME.db登录MySQL:mysql -uUSERNAME -pPASSWORD删除数据库:drop database DATABASENAME;创建数据库:crea..._.nb3文件如何打开

推荐文章

热门文章

相关标签