尚品汇项目笔记

尚品汇项目笔记,第1张

尚品汇项目笔记
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首页其他组件




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属性获取节点的属性信息。

 
          
            
              {{c1.categoryName}}
            
            
              
                
                  
                    {{c2.categoryName}}
                  
                  
                    
                      {{c3.categoryName}}
                    
                  
                
              
             
          

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

15.swiper插件实现轮播图

Swiper官网

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

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


        

          
            
              
            
          

          
          

          
          
          
        

完美解决方案:使用watch+this.$nextTick()
官方介绍:this. $nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。


16.props父子通信

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


子组件:Floor下的index.vue



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

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





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的代码
20.利用watch监听路由信息变化实现动态搜索

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

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代码()


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

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

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

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

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

iconfont-阿里巴巴矢量图标库

挑选一个自己喜欢的箭头

在public文件index引入该css


 在search模块使用该图标


            
              

                

                  综合
                
                
                  价格
                
              
            
          

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时,该图片就会被标记为选中。 

  
    
      
        
      
    
    
    
  

轮播图组件和放大镜组件是兄弟组件,所以要通过全局总线通信。
在轮播图组件中,点击图片触发全局事件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.​​​​​​​购买产品个数的 *** 作

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

              
                
                +
                 1 ? skuNum-- : (skuNum = 1)"
                  >-
              

    //表单元素修改产品个数
    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)

 
     -
     
     +
 

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


 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1295241.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-10
下一篇 2022-06-10

发表评论

登录后才能评论

评论列表(0条)

保存