# SAM-shop **Repository Path**: sam9029/vue-shop ## Basic Information - **Project Name**: SAM-shop - **Description**: 预览地址: http://sam9029.gitee.io/vue-shop - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-06-16 - **Last Updated**: 2022-12-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue-shop - **PC端 -- 使用vue2构建基础电商项目** ## 前言 - vuex-state操作数据 - 页面负责页面操作,路由跳转等视图操作 - vue组件大写命名,页面小写命名 ## 功能说明 - 菜单导航-动态渲染 (unfinished-!!首页其他部分,全部跳转搜索页) - 轮播商品图-动态渲染-mock模拟数据 - 楼层商品-动态渲染-mock模拟数据 - 可能使用多个楼层,所以异步网络请求应该在父组件home身上请求 - 楼层组件引入轮播图 - apis请求库统一管理接口 - 封装axios--request.js - 实际开发中,可能由多个不一样的接口地址 - 可以axios.create()创建多个axios实例 - 封装公共地址, - 统一自行判断 param 与 data 的使用,直接调用使用 data 传 - 视频-P10 - Axios统一封装 请求拦截器 响应拦截器 - 请求拦截器 -`Request.interceptors.request.use(config=>{},err=>{})` -统一为所有请求添加 头部信息,例如(token,游客的暂时id) - 响应拦截器 - `Request.interceptors.response.use(res=>{},err=>{})` - 统一处理网路响应返回的错误 - 响应成功 - 含各类http码,在处理,提出200码的响应结果 - 响应失败 - lodash 插件节流(节流:`一定时间内只触发一次`) - 触发 - ⭐mockjs 插件使用 创建虚拟数据 - 在main.js中引入 ~~~JS // 引入mockjs import '@/mock/mock' ~~~ - 创建mockRequest文件定义 新的封装请求 mockRequest ~~~js import Axios from 'axios' const mockRequest = Axios.create({ // 配置公共请求地址 baseURL:'/mock', // 请求超时时间 timeout:'5000' }); // Request // 直接暴露一个匿名函数, 三个重要参数 export default ({method,url,data}) => { // 记得 return 也是 mockRequest return mockRequest({ method, url, // 若 method为post 则用 data,否则用param [method.toLowerCase() === "get"? 'param' : 'data' ]:data }) } ~~~ - 创建mockjs文件(定义需要的模拟数据)接口, ~~~js import Mock from 'mockjs' // 导入模拟数据 import bannerMockData from './banner.json' // 创建 虚拟接口 及 虚拟的接口返回数据 Mock.mock('/testMock',[1,2,3]) // 轮播图数据模拟 Mock.mock('/mock/banner',{ code:200, message:'成功', ok:'true', data:bannerMockData }) ~~~ - 在api处引入模拟数据请求函数 ~~~js import mockRequest from '@/utils/mockRequest' //--模拟的接口-------------------- // 暴露轮播图数据接口 export const getBanner = ()=>{ return mockRequest({ method:'GET', url:'/banner', }) } ~~~ - 组件中正常引入接口并使用模拟数据请求函数 ~~~js import api from '@/api/api' ~~~ - swiper插件 轮播封装全局组件 注意使用nextTick,和数据的初始化 - 轮播封装全局组件Slide,接受值,需要被监听 - 移入图片上方,图片停止轮播,移出时,轮播开始 - ⭐轮播图组件 - 面包屑的关闭处理全部逻辑一个人完成,为参考视频,用时大约6-7h - ⭐面包屑处理 - 结合搜索函数,使用的搜索数据 - 搜索页面将搜索数据数据,传递给面包屑组件 - 面包屑组件读取搜索数据数据,同时按顺序(分类,搜索值,品牌,属性值)渲染 - 关闭面包屑处理 - 注意所有参数中(分类categoryName,搜索值keyword,品牌trademark,属性props)都存在于vuex的searchData中 - 注意!分类categoryName,搜索值keyword还存在于路由中,要对路由进行处理!! - categoryName,keyword要处理路由信息,!方法重载路由(我选择this.$route.replace,不记录历史) - 品牌直接在vuex的searchData中赋空值 - 属性直接在vuex的searchData中处理 !处理对应数组的对应值 - 排序完成 - 一个事件重复点击改变升降序规则 - 思路存储 前者,与 现在值 进行对比,来改变 - 待优化-综合与价格的升降序箭头同时改变 - 搜索逻辑处理 - 点击首页,清除所有搜索参数(防止返回后,搜索参数依旧存在,面包屑会渲染) - 搜索框 输入搜索值 点击搜索后 清除所有(若之前有品牌,属性,除了分类)参数 - !!!进入商品页时,页面自动回到顶部!!! - 利用导航守卫,dom页面滚动scrollTo - 商品详情页 - 使用vuex储存网络方法和数据,并处理商品数据信息,供detail组件调用 - 全局路由导航-跳转新页面时,页面置顶 - 官方文档 [滚动行为](https://v3.router.vuejs.org/zh/guide/advanced/scroll-behavior.html#%E5%BC%82%E6%AD%A5%E6%BB%9A%E5%8A%A8 ) - (推荐)方法一:跳转页面,页面顶置,回退页面时,页面回退至相应位置 scrollBehavior 方法接收 to 和 from 路由对象。第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。 ~~~js const router = new VueRouter({ routes, //scrollBehavior 函数接收 to和 from 路由对象,如 Navigation Guards。 //第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。 scrollBehavior (to, from, savedPosition) { if (savedPosition) { //此处意思 即若回退页面,返回savedPosition,且页面不置顶 return savedPosition } else { //此处意思 若页面跳转新页面,则页面置顶 return { x: 0, y: 0 ,behavior:'smooth'} } } }) ~~~ - 方法二:(退回页面,不会记录历史位置) ~~~js router.beforeEach((to,from,next) => { window.scrollTo({ top:0, behavior:'smooth' }) next() }) ~~~ - 只要 大于零的 整数 ~~~js //排除字符串,undefined,NaN等非数字字符 //排除小数,小于零的数 if(isNaN(value) || value%1 !== 0 || value < 0){ //...code }else{ //...code } ~~~ - isNaN() 函数用于检查其参数是否是非数字值。 如果参数值为 NaN 或字符串、对象、undefined等非数字值则返回 true, 否则返回 false。 - ⭐UN游客模式下-添加购物车 - 写api 统一管理 - 使用uuid-v4作为游客的暂时id,存储游客的购物车信息 - axios统一头部添加暂时的uuid - uuid v4 写成util 工具包调用,(存在浏览器本地内存中) - uuidV4 工具包逻辑:先读取本地储存,若有userTempId,及return;若无,新建一个存进本地,并return - (注意,不能使得每次调用都生成新的id,应该是本机生成后,不在改变本机的userTempId) - ## 项目结构 ~~~ - 首页 - 搜索页-商品分类 - 商品详情 - 购物车 - 登录-注册 - 订单确认页 》 支付订单页 》 支付成功页 - 我的订单页 ~~~ ### vue-组件结构 ~~~ - layout - 引入AppHeader、AppHeader(局部组件) - home - 引入TypeNav - 引入 首页组成的 List、Rank、Like、Floor、Brand(局部组件) - search - TypeNav(全局组件-在home中使用) ~~~ # 总结 ## router-link ~~~html ~~~ ## button 使用路由的时候 , 绑定@click='$router.push("/xx")' ~~~html ~~~ ## vue-router 路由方法的 push 重写--UN - 解决 编程式`.$router.push()`写的 同一路由重复跳转中的 警告⚠ ## vue-cli中的全局组件使用 - 定义全局组件(`GlobalName.vue`)文件后,需要在main.js 中注册 - 之后在其他 vue文件中直接使用 - 无需export default中 components中 注册 ` import GlobalName from './xxx/xxx' Vue.component('GlobalName', GlobalName) ` ## UN--vue 的 transition 动画便签 ,配合css的tranition使用 ## swiper图片 移入图片上方,图片停止轮播,移出时,轮播开始 是 mouse 不是 mounse ~~~js @mouseenter="autoplaySwiper.autoplay.stop()" @mouseleave="autoplaySwiper.autoplay.start()" ~~~ ## 在渲染可跳转菜单时,(菜单项目多了-不要用router-link,会产生)----详情见TypeNav 渲染菜单导航 - 视频 [p8](https://www.bilibili.com/video/BV1Lg411R7WK?p=8&spm_id_from=333.851.header_right.history_list.click&vd_source=41f5451f27195a55e47c35f50a0e3772) ## 衍生--多次绑定事件-可换成事件委托-优化性能 ## 设置在app组件里的网络请求,在页面加载时只会加载一次,此处适用于菜单项目导航数据的获取 ## vuex ## TapeNav 全局使用时,首页和搜索页转换时,鼠标浮动的菜单动画展开 - 结合mouseenter mouseleave事件来使用 v-show 来(转换isShow的值) - 通过直接在mounted中路由判断(this.$route.path)是否为首页,来修改isShow的值,确认是否初始隐藏菜单 - 优化处理首页 离开时,菜单消失,绑定一个函数(判断是否首页,是则中断函数) ## 使用this.$route.push({})传值 - 一:query this.$route.push({ path:'/xxx', query:{xxx:xxxx} }) - 二:params this.$route.push({ name:'xxx', params:{xxx:xxxx} }) - (注意!!)params传值最好使用name声明路径,使用path时this.$route对象中的params接收不到传递的值 ## 使用组件内导航守卫时,beforeRouteUpdate(to,from,next){},注意this.$route.params.value获取的值不是实时的,是上一次的传进来的params.value - 场景:使用在 search/index.vue中 - ~~~js //已弃用-改用watch // 组件路由导航改变时 beforeRouteUpdate(to,from,next){ // 在动态路由中,路由信息发生改变时,路由不会刷新,要检测路由变化,执行搜索函数 this.getCommdity({ //使用 to.params.searchValue来获取参数 keyword:to.params.searchValue }) console.log('路由改变,搜索执行') next() }, ~~~ ## 购物车全选有三个细节功能---promise.all(数组对象) (⭐注意--promise.all 本身也是promise,调用时要async和await处理) - 1.全选按钮-自动匹配(是否全选):计算属性通过筛选 所有购物车 商品的isChecked ,全所有都是true ,则全选自动选中,不满足就不选中 - 按钮点击功能-提取未isChecked 为0/1的商品,取得skuId,并将其传入函数,再将函数传入数组-供promise.all()调用 - 2.全选: - 3.取消全选: ## 购物车选中删除, - 待优化-最后的商品选中后,点击无法网络请求后无法---更新页面, - 已解决- 原本删除后,新的购物车数组数据没有值,报错 - 判断新返回的购物车数组数据是否有值,返回数据和空数组,避免删除所有商品后不刷新 - 有选中,有未选时,效果则可行 ------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------ # 问题 ## ⭐(特级)vuex 中state数据更新后,常规的属性调用 getter 读取state的值不更新 的解决 > [vuex getters 通过方法访问](https://vuex.vuejs.org/zh/guide/getters.html#%E9%80%9A%E8%BF%87%E5%B1%9E%E6%80%A7%E8%AE%BF%E9%97%AE) - 一般情况下,getters(作用类似computed)通过属性来访问!! - 此请况可以通过 方法来访问(实现调用getters方法时,每次调用都是调用方法,数据就会重新调用) ## 例子 ### vuex文件写入 ```js state: { // commdityData数据会由mutations的方法进行更新 commdityData:{} }, getters:{ // 第一个参数是state // 写入一个箭头函数 cateNav:(state) => () =>{ return state.commdityData.categoryView }, } ``` ### 组件调用(方式都是默认使用vuex模块的) #### 方式一: 使用辅助函数 ```js import{createNamespacedHelpers} from 'vuex' const{mapGetters:detailMapGetters} = createNamespacedHelpers('detail') export default{ methods:{ ...detailMapGetters(['cateNav']) } mounted(){ this.cateNav() } } ``` #### 方式二 : ```js import{mapGetters} from 'vuex' export default{ methods:{ // ...mapGetters('模块名',['getters方法名']) ...mapGetters('detail',['cateNav']) } mounted(){ this.cateNav() } } ``` ## eslint 命名的语法 报错 解决 ~~~js const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, // 关闭命名语法检测 lintOnSave: false }) ~~~ ## 使用编程式导航:重复跳转同一个路径,会产生错误 - 可以正常使用,但是会有 log 报错 ![01](./note/01.jpg) - 解决方法 ~~~js goSearch(){ this.$router.push('/search') .then(res=>{console.log('跳转成功')}) .catch(res=>{console.log('跳转失败')}) } ~~~ ~~~js goSearch(){this.$router.push('/search',res=>{console.log('跳转成功')},res=>{console.log('跳转失败')})} ~~~ ## 在mockRequest 暴露的函数中,也是return mockRequest() - 之前还是 ,之前还是写的 原来的 return Request - 报错 `Failed to construct 'Request': Please use the 'new' operator, this DOM object constructor cannot be called as a function.` // 记得 return 也是 mockRequest ## ⭐⭐this.$nextTick()的使用 ## swiper插件 轮播图拖不动的原因 - 在 list 组件中,使用异步获取的banner数据 - 初始化轮播图时,轮播图列表数据还没有渲染 - 使用nextTick() 来获取更新后 的数据 - ⭐异步方法只能用 async await 来获取数据 ~~~js async mounted(){ const res = await getBanner() console.log(res); this.bannerData = res.data.data; console.log('list',this.bannerData) // 初始化轮播图,轮播列表的数据还未渲染 this.$nextTick()等待页面渲染后 this.$nextTick( ()=>{ new Swiper(this.$refs.mySwiper,{ loop:true, autoplay:true }); }) } ~~~ promise.then的方式(下面这种方式)图片也拖不动 ~~~js mounted(){ getBanner().then(res => { console.log(res); this.bannerData = res.data.data; console.log('list',this.bannerData) }) // 初始化轮播图,轮播列表的数据还未渲染 this.$nextTick()等待页面渲染后 this.$nextTick( ()=>{ new Swiper(this.$refs.mySwiper,{ loop:true, autoplay:true }); }) } ~~~ ## ?????vue 项目v-for动态渲染 时 绑定 本地图片-UN - 目前投机取巧 绑了实例网站的公共域名地址来获取数据 `:src="'http://zhi.zeng.pub/zshop'+floorMockData.imgUrl"` ## 深度监听的写法, ~~~js watch: { // 是冒号啊。注意注意啊!!! 'person.name': { handler(newVal,oldVal) { // ... }, deep: true, immediate: true } } ~~~ ## 传递路由 (route文件和使用路由的组件要同时改query,params的信息) route--index.js - 问号用于正则匹配(具体作用发挥不知) ` {path:'/search/:keyword?',name:'Search' ,component:Search}, ` - 组件使用 ~~~js this.$router.push({ //(注意!!)params传值最好使用name声明路径 // 使用path时this.$route对象中的params接收不到传递的值 name:'Search', params:{keyword:this.keyword || undefined } // query:this.$route.query }) ~~~ ## 父子组件传方法使用,@xxFoo='xxFoo', - 注意是@,不是: - 子组件接收 @click='$emit('xxFoo',val)' ## 注意 组件使用data 时,在其他的地方(computed,methods)中操作时不要覆盖了原值 ## !!在HTML5中添加了data-的方式来自定义属性, ⭐⭐ 命名可以用驼峰命名方式,但取值是必需全部使用小写(后面会说) - !!!!驼峰命名取值时要使用小写 - `data-categoryName` 取值时 const {categoryname} = e.target.dataset - !!!!单杠线命名取值才使用大写 - `data-category-name` 取值时 const {categoryName} = e.target.dataset ## 组件 传递 当前路由信息 给 vuex 注意的问题 - 组件使用了vuex的mutations方法 通过函数传值 - 目的在于 提取 当前路由信息中的 params和query信息 传递给vuex - 不可传的写法: - 写法一:params可传,!!但query始终为undefined: - `this.CHANGE_SEARCHDATA(this.$route.params,this.$route.query)` - 写法二:params,query 都不可传 - `this.CHANGE_SEARCHDATA( {params,query} = this.$route)` - 可传的写法:params和query都可传: - `this.CHANGE_SEARCHDATA({...this.$route})` 这样写可以接受到值,之后可在vuex做解构 ## //对象中是 属性:值 // 不是 属性 = 值!!! ## 使用reduce报TypeError: Reduce of empty array with no initial value处理方法 - 增加初始值,0 `Array.reduce( Foo(){//code}, 0 )` ## 增加商品数量的api有逻辑问题,不能直接改变商品为指定数量 - 传递正数就添加(会累积) - 指定数量传递只会被认为是正数传递 - 传递负数就减少(会累积) -------------------------------------------------------------------- -------------------------------------------------------------------- # 拓展: 路由使用时,注意undefined 不会通过url 传递!!! ## 渲染时,渲染多个数据中包含绑定事件,可采用事件委托的方式,优化性能提升 ## 获取data-xxx自定义属性要使用 e.target.data.xxx -------------------------------------------------------------------- # 待优化: ## 待优化-综合与价格的升降序箭头同时改变 - 已解决:被点击的(选中的参数就显示),没有选中就不显示升降序箭头 ## 面包屑,分页器可采用封装组件的思想