尚品汇项目笔记

尚品汇项目笔记
github代码地址

 

我的git的仓库地址

项目主要步骤

(1)静态页面,拆分组件
(2)向服务器发请求(API),捞取数据
(3)Vuex三连环(state,actions,mutations)
(4)组件获取仓库数据,动态展示数据

1.脚手架目录分析

public文件夹:静态资源,webpack进行打包的时候会原封不动打包到dist文件夹中。
src文件夹:(程序员代码文件夹)
    assets文件夹:经常放置一些静态资源(图片)assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面)
    components文件夹:一般放置非路由组件(或者项目共用的组件)
App.vue 唯一的根组件
main.js 入口文件【程序最先执行的文件】
babel.config.js:babel配置文件
package.json:看到项目描述、项目依赖、项目运行指令
README.md:项目说明文件

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文件夹的别名的设置

因为项目大的时候src(源代码文件夹):里面目录会很多,找文件不方便,设置src文件夹的别名的好处,找文件会方便一些
创建jsconfig.json文件

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

    "exclude": [
        "node_modules",
        "dist"
    ]
 }

3.项目主要界面搭建

3.1创建Header、Footer 组件

选择辉洪老师的静态页面

3.2引入静态页面,引入样式,引入图片静态资源,在APP.vue上注册使用

项目采用的less样式,浏览器不识别less语法,需要一些loader进行处理,把less语法转换为CSS语法

3.3安装less less-loader@5

切记less-loader安装5版本的,不要安装在最新版本,安装最新版本less-loader会报错,报的错误setOption函数未定义

需要在style标签的身上加上lang=“less”

4.配置路由

router文件夹下创建index.js

import Vue  from 'vue';
import VueRouter from 'vue-router';
//使用插件
Vue.use(VueRouter);
//对外暴露VueRouter类的实例
export default new VueRouter({
    routes:[
         {
              path:'/home',
              component:Home
         }
    ]
})

5.路由组件总结

$router:进行编程式导航的路由跳转
this.$router.push|this.$router.replace
$route:可以获取路由的信息|参数
this.$route.path
this.$route.params|query
this.$route.meta

编程式导航路由跳转到当前路由(参数不变), 多次执行会抛出NavigationDuplicated的警告错误?
注意:编程式导航(push|replace)才会有这种情况的异常,声明式导航是没有这种问题,因为声明式导航内部已经解决这种问题。
这种异常,对于程序没有任何影响的。
为什么会出现这种现象:
由于vue-router最新版本3.5.2,引入了promise,当传递参数多次且重复,会抛出异常,因此出现上面现象,
第一种解决方案:是给push函数,传入相应的成功的回调与失败的回调
第一种解决方案可以暂时解决当前问题,但是以后再用push|replace还是会出现类似现象,因此我们需要从‘根’治病;

6.定义全局组件

我们的三级联动组件是全局组件,全局的配置都需要在main.js中配置

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

在Home组件中使用该全局组件

7.Home首页其他组件

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

8.封装axios

没安装的小伙伴先安装axios

Axios官方地址

在根目录下创建api文件夹,创建request.js文件

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;

9、前端通过代理解决跨域问题

我们在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。

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

            }
        },
    }
}

10.封装请求接口

在文件夹api中创建index.js文件,用于封装所有请求

export const reqCategoryList = () => requests({ url: '/product/getBaseCategoryList', method: 'get' });

11.nprogress进度条插件

打开一个页面时,往往会伴随一些请求,并且会在页面上方出现进度条。它的原理时,在我们发起请求的时候开启进度条,在请求成功后关闭进度条,所以只需要在request.js中进行配置。

在api下创建一个名为request.js的文件

//对于axios进行二次封装
import axios from "axios"
//在当前模块中引入store
import store from "@/store"
//引入进度条
import nprogress from 'nprogress'
//引入进度条样式
import "nprogress/nprogress.css"

//1.利用axios对象的方法create,去创建一个axios实例
//2.request就是axios,只不过稍微配置一下
const request = axios.create({
    //配置对象
    baseURL: "/api",
    //代表请求超时的时间5s
    timeout: 5000,
})

//请求拦截器:在发送请求之前,请求拦截器可以检测到,可以在请求发出之前做一些事情
request.interceptors.request.use((config) => {
    //config:配置对象,对象里面有一个属性很重要,headers请求头
    //进度条开始动
    if (store.state.detail.uuid_token) {
        //请求头添加一个字段(userTempId):和后台商量好了
        config.headers.userTempId=store.state.detail.uuid_token
    }
    //需要携带token带给服务器
    if (store.state.user.token) {
        config.headers.token=store.state.user.token
    }
    nprogress.start()
    return config
})

//响应拦截器
request.interceptors.response.use((res) => {
    //成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,可以做一些事
    //进度条结束
    nprogress.done()
    return res.data;
}, (error) => {
    //响应失败的回调函数
    return Promise.reject(new Error('faile'))
})


//对外暴露
export default request

可以通过样式来调整进度条的颜色(我这儿设置的天蓝色,大家看情况自己更改就好)

12、Vuex

没有下载的小伙伴先下载Vuex

cnpm i --save vuex

首先在根目录创建一个名为store的文件夹,文件下创建index.js作为我们的大仓库,未来会有很多的小仓库

 大仓库的代码如下

import Vue from 'vue'
import Vuex from 'vuex'
//需要使用插件一次
Vue.use(Vuex)

import home from './home'
import search from './search'
import detail from './detail'
import shopcart from './shopcart'
import user from './user'
import trade from './trade'

//对外暴露Store类的一个实例
export default new Vuex.Store({
    //实现Vuex仓库模块式开发存储数据
    modules: {
        home,
        search,
        detail,
        shopcart,
        user,
        trade
    }
})

小仓库的代码如下(这儿以home为例)

import { reqCategoryList,reqFloorList,reqGetBannerList } from "@/api"
//home模块的小仓库
const state = {
    //state中数据默认初始值别瞎写,服务器返回的是对象,起始的就是对象,返回的是数组,起始值就是数组
    categoryList: [],
    //轮播图的数据
    bannerList: [],
    //floor组件的数据
    floorList:[]
}
const actions = {
    //通过API里面的接口函数调用,向服务器发请求,获取服务器的数据
    async categoryList({ commit }) {
        let result = await reqCategoryList()
        if (result.code == 200) {
            commit("CATEGORYLIST",result.data.slice(0,16))
        }
    },
    //获取首页轮播图的数据
    async getBannerList({ commit }) {
        let result = await reqGetBannerList()
        if (result.code == 200) {
            commit('GETBANNERLIST',result.data)
        }
    },
    async getFloorList({ commit }) {
        let result = await reqFloorList()
        if (result.code == 200) {
            commit('GETFLOORLIST',result.data)
        }
    }
}
const mutations = {
    CATEGORYLIST(state, categoryList) {
        state.categoryList=categoryList
    },
    GETBANNERLIST(state, bannerList) {
        state.bannerList=bannerList
    },
    GETFLOORLIST(state, floorList) {
        state.floorList=floorList
    }
}
//计算属性
const getters = {}
export default {
    state,
    actions,
    mutations,
    getters
}

注意:这儿使用action时,函数的第一个参数,我用的是{commit},这样下面可以直接commit(),当然,第一个参数也可以commit,不过下面就得context.commit(),这儿使用{commit}更简便一些,后面会用到在action里面继续dispatch调用actions,我们可以打印context,就会发现它里面包含的东西,如图:

13.loadsh插件防抖和节流

防抖

指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

节流

指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率

//引入lodash:是把lodash全部封装好的函数全都引入进来了
//按需引入:只是引入节流函数,其他的函数没有引入(模块),这样做的好处是,当你打包项目的时候体积会小一些
import throttle from "lodash/throttle";

  methods: {
    // 鼠标进入修改currentIndex属性
    changeIndex: throttle(function (index) {
      //index:鼠标一段移上某一个一级分类的元素的索引值
      //正常情况(用户慢慢的操作):鼠标进入,每一个一级分类h3都会进入事件
      //非正常情况(用户操作很快):本身全部的一级分类都应该触发鼠标进入事件,但是经过测试,只有部分h3触发了
      //就是由于用户行为过快,导致浏览器反应不过来,如果当前回调函数中有一些大量业务,也可能出现卡顿现象
      this.currentIndex = index;
    }, 50),
    leaveIndex() {
      this.currentIndex = -1;
    },
  },

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

用户可以点击三级联动,Home模块跳转到Search模块,同时会把你选中的产品(产品名字、ID)传递过去

我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。

 <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是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。

15.swiper插件实现轮播图

Swiper官网

官网有具体实例,小伙伴们有不懂的可以直接去官网查看

(1)安装swiper
(2)在需要使用轮播图的组件内导入swpier和它的css样式
(3)在组件中创建swiper需要的dom标签(html代码,参考官网代码)
(4)创建swiper实例

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

完美解决方案:使用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>

16.props父子通信

父组件:home文件下的index.js

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

17、将轮播图模块提取为公共组件

注意:
(1)组件在main.js中引入,并定义为全局组件。我这里只是在使用到该组件的地方引入并声明
(2)引用组件时要在components中声明引入的组件。
(3)我们将轮播图组件已经提取为公共组件Carouse,所以我们只需要在Carouse中引入swiper和相应css样式。

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

18.开发 Search 模块

静态界面直接使用资料中准备好的文件,不再一一拆分了

然后使用Vuex发请求获取数据

import { reqGetSearchInfo } from "@/api";

// search模块小仓库

const state = {
    //仓库初始状态
    searchList: {}
};
const mutations = {
    GETSEARCHLIST(state, searchList) {
        state.searchList = searchList;
    },
};
const actions = {
    //获取search模块数据
    async getSearchList({ commit }, params = {}) {
        //当前这个reqGetSearchInfo这个函数在调用获取服务器数据的时候,至少传递一个参数(空对象)
        //params形参:是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象
        let result = await reqGetSearchInfo(params);
        if (result.code == 200) {
            commit("GETSEARCHLIST", result.data);
        }
    },
};
mounted() {
      this.$store.dispatch('getSearchList', {})
},

19.getters使用

如果不使用getters属性,我们在组件获取state中的数据表达式为:this.$store.state.子模块.属性

个人理解:getters将获取store中的数据封装为函数,代码维护变得更简单(和我们将请求封装为api一样)。而且getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
注意:仓库中的getters是全局属性,是不分模块的。即store中所有模块的getter内的函数都可以通过$store.getters.函数名获取
下图为store内容

 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>

20.利用watch监听路由信息变化实现动态搜索

如果在每个三级分类列表和搜索按钮加一个点击按钮事件,只要点击了就执行搜索函数
但是这样子做会生成很多回调函数,很消耗性能
最好解决方法:用watch监听路由信息变化
我们每次进行新的搜索时,我们的queryparams参数中的部分内容会发生变化,而且这两个参数都是路由的属性,所以可以通过监听路由信息变化来动态发起搜索请求。

search组件watch部分代码

// 数据监听:监听组件实例身上的属性的属性值是否变化
  watch: {
    $route(newValue, oldValue) {
      // 再次处理请求参数
      Object.assign(this.searchParams, this.$route.query, this.$route.params);
      console.log(this.searchParams);
      this.getData();

      //分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
      this.searchParams.category1Id = undefined;
      this.searchParams.category2Id = undefined;
      this.searchParams.category3Id = undefined;
    },
  },

21.面包屑处理分类(处理query参数,地址栏的处理)

SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。如下图所示

 

在点击删除分类时,我们需要categoryName和 category3Id(或者是category1Id、category2Id)删除掉,但是params中的keyword参数(华为)不需要删除。

所以我们需要把 categoryName 、category3Id、category1Id 、category2Id 赋值为 undefined ;接着我们再次发请求更新页面上的数据

这个时候我们点击分类,页面上的数据的确发生变化了,但是地址栏上的内容并没有变化,事实上页面上地址栏同样也需要改变,而且路径中的params不应该删除,路由跳转的时候应该带着

我们应该重新跳转当前页面,并携带params参数

至此面包屑部分内容结束。
总结:面包屑由四个属性影响:parads、query、品牌、手机属性
面包屑生成逻辑
判断searchParams相关属性是否存在,存在即显示。
面包屑删除逻辑
Search.vue js代码()

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

22.商品排序(计算属性)

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

order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。

升降序是通过箭头图标来辨别的,如图所示:

图标是iconfont网站的图标,通过引入在线css的方式引入图标

iconfont-阿里巴巴矢量图标库

挑选一个自己喜欢的箭头

在public文件index引入该css

<link rel="stylesheet" href="https://at.alicdn.com/t/font_2994457_qqwrvmss9l9.css">

 在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();
      }

23.手写分页器

实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。但是我们还是要知道手写该怎么写(日历也是一样的)

核心属性:

  • pageNo 当前页
  • pageSize 每一页展示多少数据
  • total 一共多少数据
  • continues 分页连续页码数

由pageSize和total可以得到另一信息:共页数

  • totalPage 总页数 == (total / pageSize)
 //连续页码的起始页码、末尾页码
    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}
    }

24.undefined细节

访问undefined的属性值会引起红色警告,可以不处理(我们称之为假报错)
以获取商品categoryView信息为例,categoryView是一个对象。
对应的getters代码

const getters =  {
    categoryView(state){
        return state.goodInfo.categoryView
    }
}

对应的computed代码

 computed:{
      ...mapGetters(['categoryView'])
    }

下细节在于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||{}

25.开发Detail部分

商品详情唯一难点就是放大镜

在轮播图组件中设置一个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>

轮播图组件和放大镜组件是兄弟组件,所以要通过全局总线通信。
在轮播图组件中,点击图片触发全局事件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计算属性改变放大镜组件展示的图片下标。

computed:{
      imgObj(){
          return this.skuImageList[this.currentIndex] || {}
      }
    },

26.​​​​​​​购买产品个数的操作

 这里可以点击 “+” 或者 “-” ,也可以在输入框输入

              <div class="controls">
                <input
                  autocomplete="off"
                  class="itxt"
                  v-model="skuNum"
                  @change="changeSkuNum"
                />
                <a href="javascript:" class="plus" @click="skuNum++">+</a>
                <a
                  href="javascript:"
                  class="mins"
                  @click="skuNum > 1 ? skuNum-- : (skuNum = 1)"
                  >-</a
                >
              </div>

    //表单元素修改产品个数
    changeSkuNum(event) {
      //用户输入进来的文本 * 1
      let value = event.target.value * 1;
      //如果用户输入进来的非法,出现NaN或者小于1
      if (isNaN(value) || value < 1) {
        this.skuNum = 1;
      } else {
        //正常大于1【大于1整数不能出现小数】
        this.skuNum = parseInt(value);
      }
    },

26.加入购物车(sessionStorage存储数据)

点击加入购物车时,会向后端发送API请求,但是该请求的返回值中data为null,所以我们只需要根据状态码code判断是否跳转到‘加入购物车成功页面’。
detail组件‘加入购物车’请求函数:

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()将字符串转为对象。

27.购物车组件开发(请求头添加一个uuid_Token)

一个网站是有很多用户的,每个用户自己的购物车都不一样,所以每一个人的购物车页面展示的东西都不一样

当你以游客身份访问网站时:

每个用户需要一个uuidToken,用来验证用户身份,让服务器知道你是谁,但是这个请求函数没有参数,所以我们把uuidToken加在请求头中

根据api接口文档封装请求函数

export const reqGetCartList = () => {
return requests({
	url:'/cart/cartList',
	method:'GET'
})}

 创建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;
})

28.购物车商品数量修改(函数节流)

这里有三个操作,减一、加一、中间是修改输入框的数字,统一使用一个回调函数
传三个参数,第一个表示操作类型、第二个是disNum(变化量)、第三个表示哪一个产品(身上有id)

 <li class="cart-list-con5">
     <a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cartInfo)">-</a>
     <input autocomplete="off" type="text" :value="cartInfo.skuNum" @change="handler('change',$event.target.value,cartInfo)" minnum="1" class="itxt">
     <a href="javascript:void(0)" class="plus" @click="handler('add',1,cartInfo)">+</a>
 </li>

handler函数,修改商品数量时,加入节流操作。(防止用户快速点击,请求还没回来,导致输入框变为负数)

节流操作:在规定时间范围内不会重复触发回调函数,只有大于这个时间间隔才会触发下一次

    //修改某一个产品的个数[节流]
    handler: throttle(async function (type, disNum, cart) {
      //type:为了区分这三个元素
      //disNum形参:+ 变化量(1)  -变化量(-1)   input最终的个数(并不是变化量)
      //cart:哪一个产品【身上有id】
      //向服务器发请求,修改数量
      switch (type) {
        //加号
        case "add":
          disNum = 1;
          break;
        case "minus":
          //判断产品的个数大于1,才可以传递给服务器-1
          //如果出现产品的个数小于等于1,传递给服务器个数0(原封不动)
          disNum = cart.skuNum > 1 ? -1 : 0;
          break;
        case "change":
          // //用户输入进来的最终量,如果非法的(带有汉字|出现负数),带给服务器数字零
          if (isNaN(disNum) || disNum < 1) {
            disNum = 0;
          } else {
            //属于正常情况(小数:取证),带给服务器变化的量 用户输入进来的 - 产品的起始个数
            disNum = parseInt(disNum) - cart.skuNum;
          }
          // disNum = (isNaN(disNum)||disNum<1)?0:parseInt(disNum) - cart.skuNum;
          break;
      }
      //派发action
      try {
        //代表的是修改成功
        await this.$store.dispatch("addOrUpdateShopCart", {
          skuId: cart.skuId,
          skuNum: disNum,
        });
        //再一次获取服务器最新的数据进行展示
        this.getData();
      } catch (error) {}
    }, 1000),

29.购物车状态修改和商品删除

这里唯一需要注意的是当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")
        }
      }

30.删除多个商品(actions扩展)

由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。
我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。
actions扩展,这儿我们打印一下content(有印象的小伙伴应该记得12有提到)

context中是包含dispatch、getters、state的,即我们可以在actions函数中通过dispatch调用其他的actions函数,可以通过getters获取仓库的数据。

actions函数代码如下:

/删除选中的所有商品
    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)
        }
      },

修改商品的全部状态和批量删除的原理相同,直接贴代码。

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

ShopCart购物车组件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)
         }
      }

31.注册登录业务

 app\src\store\user.js

    //获取验证码
    async getCode({ commit }, phone) {
        //获取验证码的这个接口:把验证码返回,但是正常情况,后台把验证码发到用户手机上【省钱】
        let result = await reqGetCode(phone);
        if (result.code == 200) {
            commit("GETCODE", result.data);
            return "ok";
        } else {
            return Promise.reject(new Error("faile"));
        }
    },
    //用户注册
    async userRegister({ commit }, user) {
        let result = await reqUserRegister(user);
        if (result.code == 200) {
            return "ok";
        } else {
            return Promise.reject(new Error("faile"));
        }
    },

app\src\pages\Register\index.vue

    //获取验证码
    async getCode() {
      //简单判断一下---至少用数据
      try {
        //如果获取到验证码
        const { phone } = this;
        phone && (await this.$store.dispatch("getCode", phone));
        //将组件的code属性值变为仓库中验证码[验证码直接自己填写了]
        this.code = this.$store.state.user.code;
      } catch (error) {}
    },
    //用户注册
    async userRegister() {
      const { phone, password, code } = this;
      try {
        phone &&
          password &&
          code &&
          (await this.$store.dispatch("userRegister", {
            phone,
            password,
            code,
          }));
        //注册成功跳转到登陆页面,并且携带用户账号
        await this.$router.push({
          path: "/login",
          query: { name: this.phone },
        });
      } catch (error) {
        alert(error);
      }
    },

32.登录(持久化储存token)

用户登录时,会向服务器发请求(组件派发action:userLogin),登录成功的话服务器就会返回一个token,将token储存在vuex里面 

app\src\pages\Login\index.vue

    //登录的回调函数
    async userLogin() {
      try {
        //登录成功
        const { phone, password } = this;
        phone &&
          password &&
          (await this.$store.dispatch("userLogin", { phone, password }));
        //登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
        //  let toPath = this.$route.query.redirect||"/home";
        this.$router.push("/home");
      } catch (error) {
        alert(error.message);
      }
    },

服务器返回token字段 ,将token保存在vuex里面

app\src\store\user.js

    USERLOGIN(state, token) {
        state.token = token;
    },

    //登录业务
    async userLogin({ commit }, data) {
        let result = await reqUserLogin(data);
        console.log(result, 'result');
        //服务器下发token,用户唯一标识符(uuid)
        //将来经常通过带token找服务器要用户信息进行展示
        if (result.code == 200) {
            //用户已经登录成功且获取到token
            commit("USERLOGIN", result.data.token);
            //持久化存储token
            // setToken(result.data.token);
            return "ok";
        } else {
            return Promise.reject(new Error("faile"));
        }
    },

(token代表一个用户的身份,不同token获取不同的用户信息)

这时,我们只是将token保存在仓库,还需要将token添加到请求头,这样就可以获取用户信息
app\src\api\request.js

// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求之前做一些事情
request.interceptors.request.use((config) => {
    if (store.state.detail.uuid_token) {
        //请求头添加一个字段(userTempId):和后台老师商量好了
        config.headers.userTempId = store.state.detail.uuid_token;
    }
    if (store.state.user.token) {
        //请求头添加一个字段(token)
        config.headers.token = store.state.user.token;
    }
    // 进度条开始
    nprogress.start();
    // config:配置对象,对象里面有一个属性很重要,headers请求头
    return config;
});

当跳转到首页时,请求头已经添加token字段,所以发请求可以获取到用户信息,将用户信息展示在首页

app\src\pages\Login\index.vue

  mounted() {
    this.$store.dispatch("getFloorList")
    // 获取用户信息在首页展示
    this.$store.dispatch("getUserInfo")
  },

app\src\store\user.js

    GETUSERINFO(state, userInfo) {
        state.userInfo = userInfo;
    },

    //获取用户信息
    async getUserInfo({ commit }) {
        let result = await reqUserInfo();
        if (result.code == 200) {
            //提交用户信息
            commit("GETUSERINFO", result.data);
            return 'ok';
        } else {
            return Promise.reject(new Error('faile'));
        }
    },

但是vuex储存数据不是持久化的 ,一旦刷新页面,vuex里面的数据就没了,即token也会清空,这样就没有token去发请求获取用户信息

因此我们需要持久化储存token

获取到token后,将token保存在本地(localStorage),点击刷新也可以在本地获取token

这儿我在utils文件夹下创建了token.js的文件中转一下

 

 

 33.全局前置守卫(登录与未登录两种情况)

导航守卫

  • 导航:表示路由正在发生改变,进行路由跳转
  • 守卫:可以把它当做“紫禁城护卫”

全局守卫: 只要发生路由变化,守卫就可以监听到
举例子:紫禁城【皇帝、太后、妃子】,紫禁城大门守卫,全要排查

全局前置路由守卫(比较常用)
有三个参数

  • to:获取到要跳转到的路由信息
  • from:获取到从哪个路由跳转过来来的信息
  • next: next() 代表放行 next(path) 代表放行

( path 前面肯定有/ 例子:/login /home)

已登录时的路由守卫
问题:

之前提到过,在首页之外的页面点击刷新,无法获取用户信息,因为其他页面没有派发action去获取用户信息,所以我们通过使用前置路由守卫来解决这个问题

解决方法:

在用户已经登录的情况下(访问的是非登录与注册),在每次路由跳转之前,判断一下是否拥有用户信息,如果没有用户信息,就先去派发action获取用户信息再放行

获取用户信息需要token,如果token失效,就需要重新登录获取并保存token

app\src\router\index.js

// 全局守卫: 前置守卫(路由跳转之前进行判断)
router.beforeEach(async (to, from, next) => {

    // to:获取到要跳转到的路由信息
    // from:获取到从哪个路由跳转过来来的信息
    // next: next() 放行  next(path) 放行 
    // console.log(to);
    // console.log(from);
    let token = store.state.user.token;
    let name = store.state.user.userInfo.name;
    // 用户已经登录
    if (token) {
        // path  前面肯定有/  例子:/login  /home
        // 用户已经登录还想去login【不能去,停留在首页】
        if (to.path == '/login') {
            next('/home')
        } else {
            //已经登陆了,访问的是非登录与注册
            //登录了且拥有用户信息放行
            if (name) {
                next();
            } else {
                //登陆了且没有用户信息
                //在路由跳转之前获取用户信息且放行
                try {
                    await store.dispatch('getUserInfo');
                    next();
                } catch (error) {
                    //token失效从新登录
                    await store.dispatch('userLogout');
                    next('/login')
                }
            }
        }
    } else {
        // 未登录
        next()
    }
})

2.未登录时的路由守卫
用户未登录时,不能去交易、支付相关【pay|paysuccess】、个人中心

如果点击前往这些页面(例子:pay页面),首先会跳转到登录页面,并把未去成的信息存储在地址栏中,登陆之后就跳转到该页面(例子:pay页面)
app\src\router\index.js

else {
        //未登录:不能去交易相关、不能去支付相关【pay|paysuccess】、不能去个人中心
        //未登录去上面这些路由-----登录
        let toPath = to.path;
        if (toPath.indexOf('/trade') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') != -1) {
            //把未登录的时候向去而没有去成的信息,存储于地址栏中【路由】
            next('/login?redirect=' + toPath);
        } else {
            //去的不是上面这些路由(home|search|shopCart)---放行
            next();
        }

    }

登录的回调函数里面,需要判断一下路由当中是否包含query参数

有query参数跳转到指定的路由,没有就跳转到home

app\src\pages\Login\index.vue

    //登录的回调函数
    async userLogin() {
      try {
        //登录成功
        const { phone, password } = this;
        phone &&
          password &&
          (await this.$store.dispatch("userLogin", { phone, password }));
        //登录的路由组件:看路由当中是否包含query参数,有:调到query参数指定路由,没有:调到home
         let toPath = this.$route.query.redirect||"/home";
        this.$router.push(toPath);
      } catch (error) {
        alert(error.message);
      }
    },

34.路由独享守卫(定义在对应的路由身上)

如果想跳转支付页面,必须是从交易页面跳转过来的

app\src\router\routes.js

    {
        path: '/pay',
        component: Pay,
        meta: {
            show: true
        },
        // 路由独享守卫
        beforeEnter: (to, from, next) => {
            // 去支付页面,必须是从交易页面而来
            if (from.path == '/trade') {
                next()
            } else {
                next(false)
                // console.log('不111可以跳转');
            }
        }
    },

如果要跳转交易页面,必须是从购物车跳转过来的

app\src\router\routes.js

    {
        path: '/trade',
        component: Trade,
        meta: {
            show: true
        },
        // 路由独享守卫
        beforeEnter: (to, from, next) => {
            // 去交易页面,必须是从购物车页面而来
            if (from.path == '/shopcart') {
                next()
            } else {
                next(false)
                // console.log('不111可以跳转');
            }
        }
    },

35.组件内守卫(定义在对应的组件身上)

有三种情况:

        1.beforeRouteEnter(在进入这个组件之前调用)

app\src\pages\PaySuccess\index.vue(进入Paysuccess前调用)

  name: "PaySuccess",
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
    if (from.path == "/pay") {
      next();
    } else {
      next(false);
    }
  },

        2.beforeRouteUpdate(该组件被复用时调用)

app\src\pages\PaySuccess\index.vue

  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
    console.log("12313131311313");
  },

        3.beforeRouteLeave(导航离开该组件时调用)

app\src\pages\PaySuccess\index.vue

  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
    next();
  },

36.路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了

原本路由是这样的

import AddCartSuccess from '@/pages/AddCartSuccess'

    {
        path: '/addcartsuccess',
        component: AddCartSuccess,
    },

路由懒加载:

    {
        path: "/register",
        component: () => import('@/pages/Register'),
    },

37.项目上线

37.1项目打包

npm run build

map文件可以准确输出哪一行哪一列报错,但是对于项目上线无意义

在vue.config.js设置项目打包时去掉map文件

module.exports = {
  // 打包时去掉map文件
  productionSourceMap:false,
  // 关闭ESLINT校验工具
  lintOnSave: false,
  //配置代理跨域
  devServer: {
    proxy: {
      "/api": {
        target: "http://39.98.123.211",
      },
    },
  },
};

37.2服务器的购买与使用

我目前白嫖一个月的阿里云(天翼云也可以一个月,不过服务器没弄成功),需要的小伙伴记得每天早上10点去抢名额

37.3Xftp 和 Xshell

项目上线需要下载这两个软件,小伙伴们自行下载

Xftp的使用

创建会话,把本地的dist复制到Xftp创建的会话中去

Xshell的使用

创建会话,切换到nginx目录
/ => etc => nginx

vim nginx.conf

INSERT进入编辑模式,添加以下内容
esc退出编辑,:wq保存编辑的内容(根据自己的API地址进行修改)

XSHELL7启动nginx服务器

systemctl start nginx.service

具体指令介绍:

#启动nginx服务
systemctl start nginx.service
#停止nginx服务
systemctl stop nginx.service
#重启nginx服务
systemctl restart nginx.service
#重新读取nginx配置(这个最常用, 不用停止nginx服务就能使修改的配置生效)
systemctl reload nginx.service

这样小伙伴们就可以通过自己的服务器的ip地址访问项目了

希望大家都能成功完成自己的项目

最后附带我的项目的ip地址

​​​​​​​        47.96.15.112


 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值