# golishop-0425 **Repository Path**: newsegmentfault/golishop-0425 ## Basic Information - **Project Name**: golishop-0425 - **Description**: 谷粒商城 - 客户端项目 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-09-17 - **Last Updated**: 2025-07-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # golishop ## Project setup ``` npm install ``` ### Compiles and hot-reloads for development ``` npm run serve ``` ### Compiles and minifies for production ``` npm run build ``` ### Lints and fixes files ``` npm run lint ``` ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). ## day01 基础 VueBase0425: https://gitee.com/newsegmentfault/0425vuebase VueComponent0425: https://gitee.com/newsegmentfault/vuecomponent0425 前台项目 项目地址: http://101.43.227.123/home golishop0425: https://gitee.com/newsegmentfault/golishop-0425 加入项目连接: https://gitee.com/newsegmentfault/golishop-0425/invite_link?invite=db04249586f425a31f91dfecb7618f7d212489c00c849c38e511b1509403e86ec34dc14029760a2fe4858b4e9c77fd4b ### 介绍项目 商城模块:首页、商品列表页、商品详情页、加入购物车页面、提交订单页面、支付页面、个人中心查看订单、登录注册 技术栈:vue全家桶(vuex、vuerouter)、axios、webpack ### 创建项目 使用 vue-cli 构建项目 `vue create 项目名称` #### 目录介绍 package-lock.json 这个文件是package.json中的依赖包依赖其他的包的一些信息,这个文件是可以删除的,下次 npm install 的时候,会自动的生成出来 public 文件夹下的内容,不会被webpack处理,打包的时候直接打包到dist目录下 scr/assets 文件夹 会被webpack处理 vue.config.js -> 脚手架4版本是没有的,5版本有了 jsconfig.json -> 脚手架4版本是没有的,5版本有了 #### ESLint 语法校验 在 vue.config.js 文件中配置 ```js module.exports = { lintOnSave: false, } ``` #### 配置@别名 在 jsconfig.json 中配置 ```js { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["src/*"] } }, "exclude": ["node_modules", "dist"] } ``` ### git 基本操作 * 自己本地的操作 git add . git commit -m '信息' * 和远端相关的操作 git pull origin master git push origin master * 合并分支的操作 * 自己本地合并 切换到主分支,使用 `git merge 分支` 来合并分支,合并的时候有冲突解决冲突合并,没有冲突会生成一个新的节点(会弹出一个vim编辑器的框让输入合并信息内容) * 远端合并(公司中往往使用这种方式合并) 将自己的分支推送到远端,提出一个 `pull requests` 请求(合并请求),等待管理员的审批 > 注意: 往往这种情况下有冲突管理员是不会给你合并的,需要自己解决完冲突再次发起`pull requests`请求 * 配置 ssh 公钥 (配置ssh公钥是为了免密推送和拉取) 1. 去 gitee 个人设置中找到配置 ssh 的页面 2. 按照提示一步步配置公钥 `ssh-keygen -t ed25519 -C "xxxxx@xxxxx.com" ` (配置公钥的时候一路回车) > windows用户配置出公钥的位置 `C:\Users\用户名\.ssh`,会生成两个文件,一个私钥文件(不需要动)一个`.pub`的公钥文件 3. 打开.pub公钥文件把内容复制到 gitee 的ssh设置页面保存即可 此时就可以使用 ssh 进行代码的拉取和推送 ## day02 ### Home组件拆分 #### Header、Footer组件拆分 * 定义 非路由组件,定义在components当中 需要拆html、css、图片(注意存放位置) css使用的是less、需要单独处理 ```js 需要安装less,注意: less-loader7版本和less最新版本适配 npm i less less-loader@7 ``` * 注册 在App组件注册 * 使用 在App组件使用 #### 拆分Home、Search、Login、Register * 定义 在pages文件夹(views文件夹)下定义 * 注册 路由组件注册,在路由中注册 ```js 1. 安装路由 npm i vue-router@3 2. 引入使用 import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) 3. 创建并暴露 export default new VueRouter({ // 在routes中注册路由组件 routes: [ { path: '/home', componet: Home // Home组件需要引入 }, ... { path: '/', redirect: '/home' } ] }) 4. 在创建vm的时候关联router(在main.js中操作) ``` * 使用 在Header组件中设置点击(声明式导航 - Home、Login、Register,编程式导航 - Search) 在App组件中使用 router-view 展示页面 #### 拆分首页组件 TypeNav、ListContainer、Like ...... Floor,Brand - 拆html、css(less)、图片 ### TypeNav 拿真实数据 #### 使用 postman 测试接口 - postman的使用 #### 发请求 - axios二次封装 要满足的条件: 1. 配置基础路径和超时限制 2. 添加进度条信息 nprogress 3. 返回的响应不再需要从data属性当中拿数据,而是响应就是我们要的数据 4. 统一处理请求错误, 具体请求也可以选择处理或不处理 ```js 安装 npm i axios 创建 utils/request.js文件 import axios from 'axios' const request = axios.create({ baseUrl: '/api', timeout: 20000 }) // 请求拦截器 request.interceptors.request.use((config) => { return config }, (err) => {}) // 响应拦截器 request.interceptors.response.use((response) => { return response.data }, (err) => {}) export default request; ``` 数据放哪 ? 数据放到 store #### vuex 使用 ```js 1. 安装 npm i vuex@3 2. 引入使用 import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) import home from './modules/home.js' 3. 创建并暴露 export default new Vuex.Store({ // 模块化 modules: { home } }) 4. 创建vm的时候进行关联(main.js中操作) ``` 模块化 home.js 文件 ```js import { reqCategoryList } from '@/api' const state = { categoryList: [], // 三级分类数据 } const mutations = { SET_CATEGORYLIST(state, categoryList) { state.categoryList = categoryList; } } const actions = { async getCategoryList({ commit }) { // let result = await request.get('/product/getBaseCategoryList') let result = await reqCategoryList(); // result.code 这个code值是后端定义的,不是HTTP状态码 // 后端定义 code 值为200代表成功 if (result && result.code == 200) { commit('SET_CATEGORYLIST', result.data) } else { // 如果后端返回的code值不是200证明请求是成功的,只是参数有问题 alert('获取三级分类数据失败'); } } } const getters = {} export default { state, mutations, actions, getters, } ``` #### 解决请求跨域 - 测试了请求,发现跨域 配置webpack-dev-server,在vue.config.js文件中 ```js devServe: { proxy: { '/api': { target: 'http://gmall-h5-api.atguigu.cn', // pathRewrite: { '^/api': '' } } } } ``` #### 封装api文件 ```js // 所有api请求函数都写在这个文件中,方便维护 import request from "@/utils/request"; export const reqCategoryList = () => { return request.get('/product/getBaseCategoryList') } ``` #### 获取TypeNav数据 在mounted中触发actions中的方法,获取数据 得到数据之后,三连环存在store当中 将store当中的数据映射到组件中,展示即可 > 以后获取数据步骤: > > * 书写 api 文件中请求函数 > * 书写 store 三连环 > * 组件 mounted 触发 actions 发请求(可以从 vue-devtools 中看到数据是否存到了store中) > * 将数据映射到组件中展示使用 ## day03 #### TypeNav 展示 - 展示右侧列表 - 高亮 使用参考值思想,需要哪一个数据来记录下标,当记录的下标和实际item的下标一样的时候,展示右侧列表,高亮显示,高亮显示用的动态绑定class(3中方式) #### TypeNav 跳转search页面隐藏 sort 整个三级分类使用一个变量 isShow 开控制显示隐藏,但是在首页不能隐藏,只在其他的页面隐藏,默认把这个数据设置成 true ,判断当前路由如果不是 Home 让 isShow 变为 false,如果是 Home 鼠标移出的时候不能隐藏,也是通过路由判断 TypeNav 快速移动鼠标 - 计算机跟不上计算 #### 防抖和节流 防抖:函数的多次执行变成一次执行,给函数防抖 节流:函数的多次执行变成少量执行,给函数节流 使用场景: * 抢购按钮可以使用节流 * input输入内容联想词汇,此时用防抖 * 页面的响应式,使用防抖或节流 * 轮播图 ![](note/day03-防抖节流.png) ```js // 防抖 function debounce(fn, delay) { var timer = null; // 用来存定时器 return function () { if (timer) { clearTimeout(timer) } timer = setTimeout(fn, delay) } } // 节流 function throttle (fn, delay) { var flag = true; // 默认进来这个值是true return function () { if (flag) { // 默认设置定时器 setTimeout(function () { fn(); flag = true; }, 1000) flag = false; } } } ``` #### 给每个item的鼠标移入加节流 使用的 lodash 进行节流的 ```js import { throttle } from 'lodash' methods: { ..., moveIn: throttle(function(index) { this.currentIndex = index; }, 50, { 'trailing': false }), } ``` leading: 节流开始之前执行 trailing: 节流结束之后执行 #### TypeNav 过渡效果 1. 将过渡的元素放到 transition 标签中 2. 书写类名 v-enter、v-enter-to、v-enter-active 3. 如果有多个过渡效果,给transition标签加name属性,类名 v- 开头变成 name- 开都 > 注意类名的权重 #### TypeNav 携带参数跳转 点击一级分类: /search?category1Id=2&categoryName=手机 点击二级分类: /search?category2Id=13&categoryName=手机通讯 点击三级分类: /search?category3Id=61&categoryName=手机 跳转方式3种类: 1. router-link 声明式导航跳转 性能不好,router-link是组件,本质在页面中使用的时候是new了一个实例,我们这里通过v-for循环出来的router-link有多少个?集具体没数过,就很多,有上百个 相当于在内存中new了上百个组件吧,组件在不断的切换显示隐藏,造成了卡顿 2. $router.push 编程式导航跳转 不是最优解,也有性能问题,比router-link强点, 问题是,a标签循环渲染了上百个,他们绑定的回调也是上百个 这上百个回调函数也要占内存 3. 事件委派 给每个点击的元素设置属性 data-category1Id、data-category2Id、data-category3Id 和 data-categoryName 通过事件冒泡,事件会传递到共同的父元素上 给父元素绑定事件,通过e.target拿到点击的元素,取出参数来进行跳转传递 使用 编程式导航进行跳转 > 注意: > > 给标签绑定 data-属性使用 `元素.dataset` 去取绑定的属性值 > > 取的时候,拿到的属性是小写的,转成小写是 dataset 方式的特性 #### TypaNav参数丢失问题 有哪些地方可以进入search页面? 两个入口 1. 点三级分类参数携带 -> 通过query携带参数到search页面(完成) 2. 点击搜索按钮参数携带 -> Header组件当中,点击搜索,通过params携带参数到search页面即可 存在的问题: 1. 先点击三级分类,参数query携带到search页面了,再点击搜索按钮,丢失掉了query参数 解决: 点击三级分类没啥问题,点击搜索按钮的时候,查看当前路由是不是有query参数,如果有携带上这个参数即可 2. 先点击搜索携带params参数到search页面,再点击三级分类,丢失掉paramas参数 解决: 点击搜索按钮没啥问题,点击三级分类的时候,查看当前路由是不是有params参数,如果有携带上这个参数即可 #### TypeNav - 切换 home、search 页面重复发请求问题 把请求放到App.vue组件当中,把请求放到一个不会被销毁的组件中即可 至此为止TypeNav结束 ### Mock 作用:拦截 ajax 请求,模拟随机数据 ![](note/day03-mock.js原理.png) 步骤: 1. 下载安装 `npm i mockjs` 2. 引入使用 ```js import Mock from 'mockjs' Mock.mock('/mock/userinfo', function () { return { code: 20000, data: '数据', message: 'success' } }) ``` 3. 在main.js中引入mock文件 4. 测试mock接口 ## day04 ### mock ListContainer和Floor 数据 使用mock把,ListContainer和Floor组件的数据加载进来 ```js Mock.mock('/mock/getBannerList', function() { return { code: 200, data: bannerData, message: 'success' } }) Mock.mock('/mock/getFloorList', function() { return { code: 200, data: floorData, message: 'success' } }) ``` 请求mock接口专门使用一个mocRrequest.js封装一个axios 书写api请求函数 三连环调用api将数据存到store当中 展示页面 ### Swiper 使用 1. 安装 npm i swiper@6 2. 引入文件 ```js import Swiper from 'swiper/swiper-bundle.min.js' import 'swiper/swiper-bundle.min.css' ``` 3. 准备DOM结构(已存在) 4. 创建swiper实例 注意: 创建swiper的时候一定要到DOM完全加载完毕才能创建 * setTimeout -> 不行 等数据回来页面更新,请求的时候,数据不知道多长时间能响应回来,所以不能使用定时器 * 在updated创建实例 -> 不行 当页面中有其他数据的时候,其他数据更新的时候也会走updated,重复创建实例了 * watch + $nextTick watch是在监听数据,当数据回来之后DOM并没更新呢,使用 $nextTick 等带DOM更新完毕之后,再创建swiper实例 此时,ListContainer组件的轮播就展示出来,问题来了floor组件也要展示轮播,怎么办? ### 封装Swiper组件 把之前创建swiper实例的过程单独拿出来封装成了一个组件 ```html ``` > 注意: > > 需要传的参数有,渲染轮播图的数据,轮播图的配置项 > > * 数据 - 监视数据的时候要考虑数据是同步进来组件的还是异步进来组件的,ListContainer数据就是异步进来的(先有组件,后有数据),Floor的数据就是同步进来的(先有数据,后又组件),需要给监视的数据加 `immediate: true` > * 轮播图的配置项 - 配置项去官网找 ### Floor - 轮播展示 直接拿封装好的SwiperList组件去渲染即可 至此为止,首页结束 ### Search 页面 * 拆分组件 * 初始化数据 * 查看接口文档,书写api函数 * 调用api三连环 * mounted触发actions发请求拿数据,映射到页面中展示 * 交互 #### vuex 命名空间 再模块化的基础上,配置`namespaced: true`开启命名空间,开启命名空间之后store中每个模块中的内容都独立了,包括 state、mutations、actions、getters 这些都独立了 开启命名空间的意义所在: 防止命名冲突,例如:A模块中有一个fn,B模块中也有一个fn,此时就命名冲突了 开启命名空间之后,除了 state 调用方式和之前模块化的时候一样,其他的调用方式都变了 ##### count 案例 - vuex命名空间 ```js store/index.js import search from './modules/search.js' export default new Vuex.Store({ modules: { home, search // --> 开启命名空间 } }) store/modules/search.js 这个文件之前的写法都没变,唯独往外暴露的时候,加了 `namespaced: true` export default { namespaced: true, // ----> 开启命名空间 state, mutations, actions, getters } ``` 调用: ```js state调用 基本使用: this.$store.state.search.count 辅助函数: ...mapState({ count: state => state.search.count }) getters 基本使用: this.$store.getters['search/twoCount'] 辅助函数 参数一: 命名空间的名称,这个名称是模块化 modules 中配置决定的 参数二: 数组,数组中放该命名空间下计算出来的属性 ...mapGetters('search', ['twoCount']) 调用: this.twoCount actions 基本使用 this.$store.dispatch('search/increment') 辅助函数 ...mapActions('search', ['increment']) 调用: this.increment() mutations 基本使用 this.$store.commit('ADD') 辅助函数 ...mapMutaions('search', ['ADD']) 调用: this.ADD() ``` 命名空间讲完之后,把search页面的三连环搞定了,现在数据在store中 ## day05 ### Search 页面交互 1. 三级分类 * 组装数据,发送请求 数据从路由中来的,监视当前路由的改变,当路由发生改变的时候拿到query参数, 放到请求参数 searchParams 中,调用 actions 发送请求 * 面包屑交互 面包屑的展示 从 `searchParams.category1Id` `searchParams.category2Id` `searchParams.category3Id` `searchParams.categoryName`当中拿数据进行展示,删除的时候重新跳转路由到当前页面,可以在watch中监听到,重新组装数据发请求渲染 2. 搜索框 * 组装数据,发送请求 数据从路由中来的,监视当前路由的改变,当路由发生改变的时候拿到 params 参数, 放到请求参数 searchParams 中,调用 actions 发送请求 * 面包屑交互 面包屑的展示 从 `searchParams.keyword` 当中拿数据进行展示,删除的时候重新跳转路由到当前页面,可以在watch中监听到,重新组装数据发请求渲染 3. 品牌 - 品牌在单独的搜索器组件当中 * 组装数据,发送请求 在组件当中点击某一个品牌,把参数传给父组件,这里用的父子组件通信-自定义事件 父组件在自定义事件的回调中得到参数,组装数据,发送请求 > 自定义事件 > > 1. 事件类型 - > 2. 触发机制 - 自己触发 $emit 触发这个 abc 事件 > > 如果想要自定义事件变成原生事件,加一个修饰符 .native ,它会把事件绑定在子组件的根标签上 例如: > > 参数: > > 在模板中,书写methods方法的时候,例如 > > ```html > 1. > 2. > 3. > methods: { > clickHandler(e) { > 1. 当 clickHandler 没有加小括号的时候 e是 事件对象 > 2. 当 clickHandler 加小括号的时候 e是参数 > 3. 当 clickHandler 加小括号且希望有事件对象的时候 在模板中使用 $event > } > } > > > 1. > 2. > 3. > methods: { > abcHandler(e) { > 1. 这个e是触发自定义事件时候传递的参数 > 例如: $emit('abc', true) 此时的e就是true > 2. 加小括号传参数 true 的时候,这个e就是参数 true > 3. 这里的 $event 是子组件触发时候传递的参数,例如: > $emit('abc', 'i love you') > } > } > ``` * 面包屑交互 拿 `searchmarams.trademark` (值是 '1:小米' )在模板中展示面包屑,通过split分割一下拿到'小米'文本,进行展示 删除的时候也是组装数据,发送请求(把`searchmarams.trademark`置为空,发送请求) 4. 平台属性 - ['106:安装手机:手机一级'] * 组装数据,发送请求 在组件当中点击某一个平台属性,把参数传给父组件,这里用的父子组件通信-自定义事件 父组件需要接收平台属性(拿ID和Name)和平台属性值(拿attrValue) * 面包屑交互 组装数据,发送请求 5. 优化 1. 发送请求之前把空字段的干掉 目的:减小发送请求的包的体积(给服务器省的) 现在不好整,如果在调用发请求之前添加代码把空串的字段都删除掉,需要改的地方太多了 就是 this.getSearchInfo(this.searchParams); 调用这行代码之前加其他代码,每个调用之前都加改的太多了 调用actions的方法变了,加一层函数即可,在这层函数中加自己的逻辑 2. 从home跳转到search页有历史记录,从search页内部跳转没有历史记录 1. 需要把 删除 三级分类 和 删除 搜索框 的历史记录改成 replace 2. 需要把 Header 组件中 搜索按钮跳转,根据来源保存历史记录 从首页来的执行跳转的,保存(push),从搜索页来执行跳转的,不保存(replace) 3. 需要把 TypeNav 组件中 三级分类点击跳转,根据来源保存历史记录 从首页来的执行跳转的,保存(push),从搜索页来执行跳转的,不保存(replace) 6. 排序 参数: searchParams.order --> '1: desc' (默认参数) 1 综合 2 价格 desc 降序 asc 升序 ```js orderParams(type) { // 组装数据 let text = ''; // 存组装数据的结果 // orderType 排序类型 1 2 // orderRank 排序方式 desc asc let [orderType, orderRank] = this.searchParams.order.split(':'); // 点击同一个 if (type == orderType) { if (orderRank == 'desc') { text = `${ type }:asc` } else { text = `${ type }:desc` } } else { // 点击不同的,只要点击不同的,默认展示降序 text = `${ type }:desc` } this.searchParams.order = text; // 发送请求 this.getSearchInfo(); } ``` 排序的展示 使用iconfont展示,需要考虑 高亮 和 箭头显示,计算属性优化了模板中使用 数据的时候写的太长 ### 分页 1. 组件的拆分 - 定义、注册(全局注册)、使用 2. 初始化数据展示 3. 交互 ## day06 #### 分页 - 初始化数据 分析需要哪些数据: * 当前页 * 每页条数 - 计算总页数 = 向上取整(总条数 / 每页条数) * 总条数 * 连续数 - 要连续显示几个按钮(必须是一个奇数) 计算总页数 计算开始页码、和结束页码 ```js 特殊情况 - 连续数大于总页码的时候 start = 1 end = pageToal 否则 - 正常计算 start = pageNo - (连续数 - 1) / 2 end = pageNo + (连续数 - 1) / 2 ``` #### 分页交互 \3. 交互 3.1 上一页、下一页(这两个写完之后,再进行下一步) 3.2 中间连续数的每个点击,切换页面 做这个的时候把页码假设为中间的一个数 ​ 把 searchParams.pageNo 设置成一个 中间的数,别整个边界值 3.3 点击切换页面 往页码为1的位置开始切换 ​ 切换的时候看什么时候隐藏 ... 和 1 往页码为 "总页码" 的位置开始切换 ​ 切换的时候看什么时候隐藏 ... 和 pageTotal 3.4 1 和 pageTotal 都可以点击,加上点击事件 3.5 上一页、下一页禁用状态 3.6 高亮 3.7 切换页码的自定义事件,需要判断是不是当前页 #### bug - 当其他参数发生改变的时候,跳转第一页 使用一个值记录 recordPage ,当每次翻页完成的时候记录当前的页码 当下一次操作页面的时候,如果是翻页,searchParams.PageNo 发生改变,此时和 recordPage 不相等,证明是翻页 如果是其他参数该百年,此时 searchParams.PageNo 和 recordPage 相等,证明是其他参数改变, 此时给页码重置为1 search页码至此为止结束 ### detail 1. 静态页面展示 - 定义(粘贴组件过来的)、注册(路由中)、使用 2. 初始化数据展示 发请求,拿数据,此时需要当前商品的id,商品的id通过路由传递过来,拿到`skuId(商品id)`发请求 api函数的封装 三连环 把数据映射到组件中展示: 展示面包屑、放大镜图片、小图列表、商品信息、销售属性 3. 交互 * 销售属性 - 排他思想(只有交互,没有实际意义) * 放大镜交互 1. 获取鼠标的实时位置,减去mask蒙层宽高的一般,得到蒙层实时的位置,赋值给蒙层就能跟随鼠标移动 2. 判断边界条件 - 以水平方向为例 左侧不能小于0,如若小于0赋值为0 右侧不能大于(容器宽度 - 蒙层宽度),如果大于的,赋值这个最大值 3. 小图和大图的关系 -2倍 让大图页跟随变化 * 小图列表 使用 swiper 来展示,swiperList公用组件需要修改才能让图片列表使用 给 swiperList公用组件 加一个插槽 * 点击小图大图要修改 兄弟组件间的通信 - 总线 1. 安装总线 2. 接收数据 - 绑定事件,留下回调 3. 发送数据 - 触发事件,发送参数 点击小图的时候告诉放大镜我们点的是第几张图片 * 加入购物车 加减按钮限制数量 - 不能小于1 输入input框限制数量 - 字符串、小数、负数 点击"加入购物车"按钮调接口,发请求,携带商品ID和商品数量 > 加入购物车接口成功之后没有返回的数据,返回了null > > 在store当中如何让组件知道已经调用成功了? > > 强调async、await的使用 > > async函数规则 > > async函数返回一个promise,这个promise由return的返回值决定 > > 1. 返回一个非promise值,async函数的promise一定是成功的 > > 2. 返回一个promise值 > > 返回成功的promise,async函数的promise就是成功的 > > 返回失败的promise,async函数的promise就是失败的 > > 返回pending的promise,async函数的promise就是pending的 > > 3. 抛出错误,async函数的promise是失败的 > > await规则 > > await必须放在async函数中 > > 1. 跟一个普通值,就是这个普通值 > 2. 跟一个成功的promise,结果是成功promise的值 > 3. 跟一个失败的promise,抛错 - 因为这里要报错,所以要使用 try...catch > > ----------- > > 在store当中的 async 修饰的actions函数如果接口调用成功返回一个 普通值,此时外部接收到了 > > ```js > store/modules/cart.js > const actions = { > async setAddCart ({ commit }, { skuId, skuNum }) { > try { > let result = await reqToCart(skuId, skuNum); > console.log(result); > if (result && result.code == 200) { > return 'ok' > } > return Promise.reject('参数错误', result.data); > } catch (error) { > return Promise.reject(error) > } > } > } > detail组件 > async addShopCart() { > try { > await this.$store.dispatch('cart/setAddCart', params) > alert('添加购物车成功,即将跳转添加购物车成功页面'); > > sessionStorage.setItem('SKUINFO', JSON.stringify(this.skuInfo)) > this.$router.push(`/addCartSuccess/${ this.skuNum }`); > > } catch (error) { > console.error(error) > } > } > ``` > > ## day07 #### detai添加购物车交互 ### 添加购物车成功页面 1. 静态页面展示 - 定义、注册(路由注册,注册的时候需要接收params参数,接商品数量)、使用(点击"添加购物车"调用接口成功之后,跳转到该页面) 2. 初始化数据展示 商品数量 - 路由的params参数 商品信息 - sessionStroage > 拓展了解:cookie、session、token、JWT token > > 1. cookie > > 状态保持(校验用户身份) > > 当登录的时候,后端给返回一个cookie存到浏览器当中,在次发请求的时候请求头会自动携带这个cookie > 用来校验用户身份做状态保持的,为什么需要做状态保持? > 因为HTTP请求是无状态请求 > > 可以存储数据 - 通过 document.cookie 来保存和读取数据,存储4K左右的数据,每个域名下只能存储50个cookie,可以设置过期时间 > > 2. session > > session是一个服务端的技术,和浏览器没关系 > 为什么会由session这个技术?因为cookie不安全,所以诞生了session > > 当登录的时候,后端校验用户成功之后,会在服务器生成一个session ID,通过响应头设置cookie,把这个sessionID带回浏览器 > 当再次发送请求的时候,携带者这个cookie(存的是sessionID)发送到服务器, > 服务器接收到请求之后,拿着这个sessionID 去 session这个容器中对比,可以识别出你的身份 > session相当于是给cookie做了一个补充 > > 3. token > 后来再发生出现了token(令牌) > 当登录的时候,后端校验完用户名密码之后,生成一个token,通过加密算法生成的,例如 base64 SHA256 等一些列加密方式 > 生成之后返回给前端,当再次发送请求的时候,请求头携带token到服务器 > 服务器解密可以知道你是谁 > > 1. 跨域,token是允许跨域的 > 2. token不需要再服务器创建容器,直接拿着令牌进行解密即可 > session在服务器要创建容器的,服务器有可能是集群,集群需要涉及到这个session容器内容的信息同步问题 > 4. JWT Token > > 简单理解,就是token加密更复杂了 > > 由三部分组成 头部(header)、有效载荷(payload)、签名(signature) > xxxxxxxxx.xxxxxxxxxx.xxxxxxxxx > > ​ 主要是用来做 多点登录的(PC、pad、phone同时登录就叫多点登录) 3. 交互 查看商品详情 去购物车结算 ### 购物车 步骤: 1. 静态页面显示 - 定义、注册、使用 2. 初始化数据展示 3. 交互 #### 初始化数据展示 调接口、三连环,发现没有数据返回,为什么没有数据?因为不知道谁买的什么商品,需要区分用户,这里添加一个用户的临时标识 ![](note/day07-用户临时标识.png) 1. 获取(创建)用户临时标识 在 `utils/userabout.js` 当中 ```js import { v4 as uuidv4 } from 'uuid'; // 标识特点: // 唯一 -> uuid // 不能老变 -> 从localStorage中去取,取到就用,取不到就创建 // 获取用户临时标识的函数 export const getUserTempId = () => { // 创建UUID的函数 let userTempId = localStorage.getItem('USERTEMPID'); // 期望的是获取到uuid if (!userTempId) { userTempId = uuidv4(); localStorage.setItem('USERTEMPID', userTempId) } return userTempId } ``` 2. 将用户的临时标识存到store当中 创建 store/modules/user.js 模块 ```js import { getUserTempId } from "@/utils/userabout" const state = { // 刷新页面的时候store会重新初始化创建,在创建store的时候就把userTempId放到了store(内存)当中 userTempId: getUserTempId(), } const mutations = {} const actions = {} const getters = {} export default { namespaced: true, state, mutations, actions, getters, } ``` 3. 在请求拦截器当中设置请求头 ```js // 请求拦截 request.interceptors.request.use((config) => { // 携带userTempId let userTempId = store.state.user.userTempId; if (userTempId) { // config.headers.userTempId ---> 必须叫userTempId config.headers.userTempId = userTempId; } NProgress.start(); // 在请求开始的时候start return config }, (err) => { NProgress.done(); // 只要开始过之后,不管成功或失败,都应该结束掉 return Promise.reject(err) }) ``` > 有了用户的临时标识之后,在添加购物车的时候,就可以区分出是谁添加的,请求购物车列表的时候就可以拿到数据了 直接展示 - 循环数据,直接展示商品信息 简介展示 - 需要自己手动计算 * 全选 - every * 总商品数 - reduce * 总价 - reduce ## day08 交互 1. 每个商品的选中状态 2. 每个商品数量的修改 -- 放一放 3. 每个商品的删除 4. 全选(商品选中状态批量修改) 5. 删除已选中(商品选中状态批量删除) ### 注册 1. 静态页面展示 2. 初始化数据展示(没有) 3. 交互 收集表单数据,发请求、调接口 > 注意:验证码逻辑 ![](note/day08-短信服务.png) ​ ### 登录 *** 1. 静态页面展示 2. 初始化数据展示(没有) 3. 交互 收集表单数据,点击登录,调用接口提交数据 调用接口,发送请求,返回一个Token,把Token存在Store中,用来识别用户身份,此时应该跳转至首页,首页要显示用户信息,登录接口并没有获取,拿着Token去获取用户信息 #### 获取用户信息 将store当中存的Token取出来,放到请求头当中,去调用获取用户信息的接口,那么此时就可以获取到用户信息了,获取到用户信息需要把用户信息存到store当中 问:什么时候获取用户信息? 在路由跳转的时候获取用户信息 #### 路由守卫 在路由切换的时候可以拦截这个过程,分为**全局前置**、全局解析、全局后置、**路由独享**、组件内守卫(三个) ![](note/day08-路由守卫介绍.png) ##### 执行过程: ![](note/day08-路由守卫的执行顺序-2.png) 这里比较重要的是 全局前置、路由独享,关于参数: ```js router.beforeEach(function (to, from, next) { // to 去哪的路由对象 // from 从拿来的路由对象 // next 是否放行的函数 // next() 放行 next(false)不放行 next('/')去首页 next({ path: '/' }) 去首页 next(); }) ``` #### 路由跳转的过程中获取个人信息*** ```js // 当登录之后要跳转到首页,登录只获取token,不获取个人用户信息,个人用户信息在路由跳转的过程中获取 // 目前要往首页跳转,并且要拿个人信息 import store from '@/store' router.beforeEach(async function (to, from, next) { const token = store.state.user.token; // 拿到登录存的token if (token) { // 如果有token就获取个人信息 // 判断有没有个人用户信息 let username = store.state.user.userinfo.name; // userinfo 这个userinfo是调用获取用户信息接口的时候,接口返回的数据存到store当中 if (username) { // 证明store当中有个人信息,直接放行 next(); } else { // 证明store当中没有个人信息,此时有token // 获取用户信息 try { await store.dispatch('user/getUserInfo'); // 触发actions获取用户信息 next(); // 如果获取到了用户信息直接放行 } catch (err) { // 如果走到catch中代表没有获取到用户信息,什么情况下会走到catch? // 1. 接口错误 // 2. token过期 // 此时是没有获取到个人信息但是要展示个人信息信息,需要登录 // next('/login'); // 直接跳登录页是死循环,重新走前置守卫,此时token有值,用户名没有,又去获取用户信息 // 又获取不到,又进入catch,又跳转登录............ // 那么怎么做? // 获取用户信息失败了,要么接口错误(概率很小),要么token过期,token已经过期了,获取不到用户信息了,清除调用,通过登录重新 // 步骤: // 1. 清除token // 2. 跳转登录 store.dispatch('user/clearToken'); next('/login'); } } } else { // 第一个进入页面的时候,一定是没有token的 // 这里不能直接跳login,死循环了 // next('/login'); // 这里需要直接放行,项目当中有一些页面是不需要登录就可以让用户看到的,所以直接放行 next(); } }) ``` > 总结登录整体步骤: > > 1. 先点击登录按钮得到token,存到store当中 > > 2. 准备好获取个人用户信息的一套逻辑,把token放到请求头当中,调用获取个人信息接口,返回数据,将个人信息存储到store当中 > > 3. 在路由跳转的过程中获取用户信息 > > router.beforeEach(function(to, from, next) { > > ​ // todo... > > }) ## day09 ### 退出登录 调用接口,清除token(清除store当中、还有localStorage中) 点击结算跳转到结算页面、结算页面是需要登录的,如果没有登录不能让跳转到结算页面的,这里目前没有加限制,在所有的页面逻辑都写完了再加,需要加登录限制的页面不光结算一个页面,有结算、支付、个人中心这些页面都需要登录 ### 结算 1. 静态页面展示 2. 初始化数据展示 3. 交互 #### 静态页面展示 定义、注册(路由)、使用(点击 - 购物车页面点击结算跳转页面、展示) #### 初始化数据展示 * 商品列表 api、三连环、映射数据展示 展示数据的时候分直接展示(直接展示商品列表)、间接展示(总商品数量和总金额) * 地址信息 api、三连环、映射数据展示 这里地址信息在商品列表接口中是有数据的,但是由于我们没有修改地址信息的地方,防止使用同一个测试账号数据比较混乱,需要在其他账号登录的时候mock地址列表 #### 交互 地址列表的选择,下方“配送至”应该显示选择的地址,使用排他思想做 ##### 提交订单 需要调用接口创建订单,然后跳转页面,从这个功能开始api函数不再走三连环,把api挂在了Vue的原型对象上 ```js import * as api from '@/api' Vue.prototype.$api = api ``` 调用接口的时候需要 组装数据 发送请求 ###### 三种暴露方式 * 分别暴露 ```js // 分别暴露 export const a = 100; export fn = function() {} // 暴露出去是一个对象,对象的长什么样子? { a: 100, fn: function() {} } // 引入的时候 import { a, fn } from 'xxx' import * as obj from 'xxx' obj.a obj.fn ``` * 统一暴露 ```js // 统一暴露 export { a: 200, fnc: function() {} } // 暴露出去是一个对象,对象的长什么样子? { a: 200, fnc: function() {} } // 引入的时候 import { a, fnc } from 'xxx' import * as obj from 'xxx' obj.a obj.fnc ``` * 默认暴露 ```js // 默认暴露 export default { a: 300, func: function() {} } // 暴露出去一个对象,对象长什么样子? { default: { a: 300, func: function() {} } } // 引入的时候 import { default } from 'xxx'; // 这个不对,不能这么,为什么?语法是没有问题的,default是一个关键字 import { default as def } from 'xxx' import def from 'xxx' // 简写 ``` 当点击"提交订单"按钮的时候调用接口返回的数据是订单号,这个订单号需要通过路由的query参数携带到支付页面 ### 支付 #### 静态页面展示 #### 初始化数据展示 从路由的query参数中拿到订单编号,调用api接口返回数据(这里没有走store,直接把数存到了当前组件)、展示 订单号、总金额 #### 交互 点击"立即支付"弹框一个二维码 * 二维码 1. 安装qrcode(注意在GitHub搜索的时候搜索 node-qrcode) `npm i qrcode -S` 2. 引入 `import QRCode from 'qrcode'` 3. 使用 `await QRCode.toDataURL(text)` * 弹框 使用element-ui,有完整引入、按需引入 完整引入 ```js import Vue from 'vue' import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); ``` 按需引入 ```js import { MessageBox, Message, Button, Row } from 'element-ui' import 'element-ui/lib/theme-chalk/index.css'; Vue.component(Button.name, Button); // 或 Vue.use(Button) Vue.component(Row.name, Row); Vue.prototype.$msgbox = MessageBox; Vue.prototype.$alert = MessageBox.alert; Vue.prototype.$confirm = MessageBox.confirm; Vue.prototype.$prompt = MessageBox.prompt; Vue.prototype.$message = Message; ``` 支付的逻辑: 1. 点击"立即支付",准备好二维码(二维码转的字符串是调用支付接口返回的一个支付的url) 2. 弹窗,显示二维码 ```js this.$confirm(``, '使用微信支付', { dangerouslyUseHTMLString: true, cancelButtonText: '支付遇到问题', // 取消按钮 confirmButtonText: '我已支付成功', // 确认按钮 center: true, // 居中 showClose: false, // 右上角关闭按钮是否显示,默认未true closeOnClickModal: false, // 点击遮罩是否关闭弹框(默认是true) closeOnPressEscape: false, // 点击ESC是否可以关闭弹框(默认是true) // function(action, instance, done) // action 的值为'confirm', 'cancel'或'close' // instance 为 MessageBox 实例,可以通过它访问实例上的属性和方法 // done 用于关闭 MessageBox 实例(done是个函数) beforeClose: (action, instance, done) => { console.log(action); if (action == 'confirm') { // if (this.payStatus == 200) { // 支付成功,为什么要记录支付状态,就是为了拦截住,不弹框消失、跳走页面 // // 关闭弹窗 // done(); // // 清除定时器 // clearInterval(this.timer); // // 跳转页面 // this.$router.push('/paySuccess') // } // 后门 done(); clearInterval(this.timer); this.$router.push('/paySuccess'); } else if (action == 'cancel') { this.$message.warning(`请联系尚硅谷客服小姐姐 13366667777`); // 也需要清除定时器 clearInterval(this.timer); done(); // 弹框消失 } } }) ``` 3. 轮询(本质上就是一个定时器) 使用 setInterval 调用"查询订单支付状态"接口,询问后端"那小子付钱没", 如果没有付钱,不做操作 如果付钱需要,关闭弹窗、清除定时器、跳转页面 在这里同时记录了一下支付状态,在弹窗"我已支付成功"按钮点击处拦截,如果没有支付成功是不能跳走的 ```js if (!this.timer) { this.timer = setInterval(async () => { try { let result = await this.$api.reqPayStatus(this.orderId); if (result && result.code == 200) { // 200 代表支付成功 205 未支付(支付中) // 跳走了 // 1. 隐藏弹框 this.$msgbox.close(); // 2. 清除定时器 clearInterval(this.timer); // 3. 跳转页面 this.$router.push('/paySuccess'); // 4. 记录支付成功的状态 this.payStatus = 200; } } catch (error) { console.error(error); } }, 3000); } ``` > 关于定时器的补充: > > 1. 变量存储定时器的时候存的是定时器的编号,清除定时器的时候直接可以通过编号清除,一般不这么做,因为不知道页面中定时器有几个,编号是可变的 > 2. clearTimerout可以清除setInterval; clearInterval可以清除 setTimeout 支付成功之后跳转"支付成功"页面 ## day10 ### 支付成功页面 * 继续购物 - 首页 * 查看订单 - 个人中心 #### 个人中心展示 使用二级路由组件,把myOrder和groupOrder拆分出来展示 ##### myOrder 静态页面展示 初始化数据展示 交互 - 分页 #### $set `this.$set(obj, 'age', 33)` 参数一: 目标对象 参数二: 属性 参数三: 属性值 作用: 给当前组件设置响应式数据,并且更新视图 ### 如果没登录,访问 (交易相关、支付相关、用户中心相关)跳转到登录页面 登录后会自动跳转前面想去而没到的页面 全局守卫 如果是跳转多个页面都要进行同一个检测,那么必然使用的是全局守卫(前置) * 只有携带了skuNum和sessionStorage内部有skuInfo数据 才能看到添加购物车成功的界面 路由独享守卫和组件守卫使用 如果是跳转单独的一个页面,需要检测,那么使用这两个守卫 * 只有从购物车界面才能跳转到交易页面(/trade)(创建订单) * 只有从交易页面(创建订单)页面才能跳转到支付页面 * 只有从支付页面才能跳转到支付成功页面 ### 图片懒加载(参考代码) ### 路由懒加载(参考代码) ### 注册页表单校验 - vee-validate(参考文档) ### 上线 - 参考文档