IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Vue项目开发知识点总结 -> 正文阅读

[JavaScript知识库]Vue项目开发知识点总结

目录

一. 创建项目 github建仓库

1.1. 创建项目:

  • 创建个文件夹LearnVuejs05。
  • 创建项目通过脚手架3 vue create supermall(改成supermall1了),选择最简单的,之后用到什么再加,这样不会生成乱七八糟的目录结构。
  • 跑一下 npm run serve(因为项目目录没有config,所以是脚手架3,所以是npm run serve,如果脚手架2是npm run dev,不信可以去package.json看一下)

1.2. 为项目加git:

  1. 先在github中建个仓库:
  • 在github上托管整个代码,需要先在github中建个仓库,把代码全放到仓库里。进入github,登录才能建仓库。
  • 登录后点右上角+。名字:supermall 描述:a vuejs supermall 。public。 不需要初始化README,因为通过脚手架创建的项目有README(里面写了怎么用这个项目)。不需要添加.gitignore 已经有了。许可协议,一般情况下比较开放的选择MIT,如果别人用必须声明选择apache。
  • 因为先在vscode中建的项目,项目创建完之后。远程创建完仓库之后。
  1. 把本地的项目和远程的仓库联系起来。两种方法:
    1. 方法一:
  • 先从github上面把你仓库里的项目复制地址,在本地项目终端执行git clone https://github.com/libuding/supermall.git 将远程仓库代码拷贝下来,会生成一个supermall文件夹。
  • 把supermall1中对应要加入到仓库里的文件拖进去。打开计算机里的supermall1的文件夹,把除了.git文件夹(supermall文件夹里面仅有一个文件就是.git文件,这个文件夹下的.git文件已经和远程仓库有联系了)和node_modules文件夹(因为这个东西是被忽略的)之外的文件夹都复制到supermall文件夹里面。
  • 因为项目中的node_modules没复制,所以在终端执行npm install,将项目里面相关东西安装一下。
  • 要装git,使用前要配置很多东西,任一文件右键git bush here。输入git config --global user.name “lqh” git config --global user.email “1778328739@qq.com” 这时候在c–>用户看到 .gitconfig 文件
  • 打开vscode红色,因为刚开始进到这个仓库里,这些东西都没有在我的代码管理之下。终端 cd supermall 。git status 看状态 。git add .将所有的东西加一下(.前有空格)暂存区。git commit -m ’初始化项目‘ 向本地仓库提交代码 本地仓库。接下来提交到服务器,git push就可以push到远程仓库了。回到github刷新看到代码传上去了。之后只需要改完代码,在终端执行git add . | git commit -m ‘’ | git push把代码push到远程仓库就行了
    1. 方法二:
  • git remote add origin https://github.com/libuding/testmall.git(复制下来的远程仓库的地址)将本地的仓库(本地项目代码里的.git文件夹和远程的仓库联系起来)和远程仓库地址连接起来。git push -u origin master将本地相关的代码直接推到远程仓库里
  • 演示2,新建个仓库名字 testmall 描述文件testtest 在终端输入cd supermall1之后git remote add origin https://github.com/libuding/testmall.git 之后git push -u origin master把它push到远程仓库(orgin是给地址起别名)
    如果这个执行报错,那么在supermall1文件执行git status git add . git commit -m ’初始化项目‘ 之后执行git push -u origin master(先add 再commit 再push这样)
    (没有.git文件夹的要先运行git init生成 才能上传到仓库)

二. 划分目录结构

src文件夹里面的划分一下,其他文件不需要划分

  • network(网络相关所有东西的封装放在这)
  • components(放公共的组件,如有个组件即在home里用又在category里用,这时候把组件抽离出去,放这) -> common(不仅当前项目用,下个项目也可以用的组件)/content(和当前项目相关的公共组件)
  • pages -> 路由分层
  • common (公共的js文件)【例:->const.js(项目里面抽出来常量,多个地方都用到的常量抽出来)/utils.js(在开发里面会封装一些工具类(方法)公共的方法,比如export function sum(num1,num2){return num1+num2}
  • assets(资源。放图片、css资源)-> src/css
  • router(路由相关的东西)->index.js里面配置映射关系
  • store(vuex公共状态管理相关的东西)
  • views(下面放大的视图,比如首页视图,分类视图,购物车视图)->home/ category

三. 设置CSS初始化和全局样式

  • initialize.css github搜一下,supermall选coderwhy的仓库
  • base.css 下载coderwhy的文件。把base.css复制过来。
  • 发现base.css文件第一行是@import “./normalize.css”; 是因为只有引入才能有效,而base.css又需要别人引,去App.vue的style里面引用,代码是@import “./assets/css/base.css”;
  • App.vue已经被main.js引用了,最终项目在打包的时候,就会去入口main.js先找到App.vue,而这里又引用了base.css,而base.css引用了initialize.css,所以到时候这些东西都能被打包。
  • 在base.css里面有个伪类 :root,用于获取根元素html,根元素里面定义了好多变量,css里面定义变量的方式 –font-size:14px 之后可以在其他css属性里面用body{font-size:var(–font-size)
  • line-height:1.5 行高是字体的5倍

四. 给项目配置路径别名

  • 脚手架2是在webpack,base.config.js设置,脚手架3配置隐藏了,要改去node_modules改,但是一般这个文件是不修改的,在supermall项目下创建个vue.config.js文件,在这个文件里面改,到时候会将公共的配置和这个文件下的配置做个合并。
  • 脚手架3起别名讲过在脚手架ppt的23页。
    先导出我们的配置,写个configureWebpack表示你准备配置webpack的config。
  • extensions:[]不用写了,默认配置过了。
  • ‘@’:src 也默认有了,别手贱再配着一遍。
  • router不需要,因为只引用一次,在App.vue中,之后在router文件夹的index.js里配置映射关系。
    因为在所有的组件里面都可以通过this.$router拿到router对象
  • store也不需要,因为不需要在其他地方引用,所有的组件里面都可以通过this.$store拿到store对象
  • 配好之后,比如去App.vue文件之前引用是@import “./assets/css/base.css”,现在只需@import “assets/css/base.css”
    注意vue.config.js和src同级。如果刚刚配置的vue.config.js文件,注意一定要先npm run build

五. editorconfig

  • 通过脚手架2创建项目的时候,默认多生成个.editorconfig文件,对代码做些统一的风格。缩进,文件结尾加空行等。如果没有打包没问题,但维护时看着很乱。脚手架3没了,我们要从codewhy的项目里拖进来这个文件,统一代码风格。

六. tabar使用

  • 总结:

    • 在大大组件里调用大组件,在里面调用四个小组件,四个小组件里面有路径、图片及点击后图片、文字。
    • 大组件里面搞一个整体大插槽。小组件里面搞个插槽,但不是一个大整体插槽,因为要对图片显示,字体改颜色等。<div slot=“item-text”>分类</div>这个会替换掉slot <slot name=“item-text”></slot>
    • 调用子组件传递数据时,如果不是字符串就加:,如果是字符串不需要加:
    • <tab-control class=“tab-control” :titles=“[‘流行’,‘新款’,‘精选’]” @tabClick=“tabClick” />如果不加,别人会当你是字符串
    • <tab-bar-item path=“/home” activeColor=”blue”>, 你要是加:,他会当你是变量,但是你并没有定义blue变量啊,因为你就是字符串就不加: props: {activeColor: {type: String, default: “red“}}这就接收到了。如果默认值是对象或者数组,这个default是函数形式。
    1. 导入之前代码
  • 项目的模块划分: tabbar -> 路由映射关系

  • 整体结果搭建,从底部tabbar开始做,这样模块就划分好了。

  • 底部tabbar之前做过,直接用,把tabbar文件夹(完全独立的,因为连放几个item都不知道)放到common文件里,把mainTaber,这个跟业务有关了,放到content。

  • 直接拖过来肯定报错,文件夹没图片呢

  • 从codewhy代码拖图片进来,所有的图片文件也分了不同的文件夹。

  • 改一下MainTaber.vue中代码的路径,在dom里面使用别名路径的话,前面需要加~。 因为:import引用路径和标签里面引用路径的属性src=“”有区别:如果<img src= ‘assets/img’>标签里面最终写个这个东西,还是找不到的,这时候这里前面要加~。<img src=’~assets/img’> 因为这属于html中某个属性里面去找某个路径。在dom中,使用起的别名前面必须加~,而import就不需要加。

  • 在App.vue中使用tabbar。导入@import MainTaber from ‘components/…/MainTaber (就写这一个组件,不管你这个组件里面写了对少个其他组件)之后components:{MainTaber} 之后<main-tabr-bar><main-tabr-bar/>还管理了路由相关的映射关系,点击某个item要跳到某个路由。

    1. 路由相关的应用
  • 路由搭建:所以App.vue这里要用到路由相关的东西。当前项目还没有路由相关代码,npm install vue-router@3.0.2 --save,之后在router index.js 中写代码 mode: 'history’用history模式。

  • 配置映射关系:先创建home,之前创建了,复制过来。先把home文件懒加载进来,之后配置映射关系,映射关系之前tabber文件那里配置了,复制过来。

  • 之后在App.vue中使用<router-view/> 说明点击这个标签url发生改变,通过路径改变,去路由去找对应组件,把对应的组件渲染到router-view的位置。

  • npm run serve看看代码是否报错

七. 换浏览器地址小图标

  • 在index.html中改一下这个图片即可<link rel=“icon” href="<%= BASE_URL %>favicon.ico">
  • jsp语法<%= BASE_URL %>动态获取当前文件所在的路径,这段的意思是在当前文件所在的路径里面找favicon.ico文件
  • 这个是jsp的语法,前端是不支持的,之前在html写个这种语法是识别不了的。现在不用担心,因为最终public文件夹下的这两个文件会被打包到dist文件夹,打包到文件夹之后最终html页面是不存在这种jsp语法的。
  • 试一下,打包npm run build,之后看一下dist文件夹的inde.html代码。pubic文件夹里面的文件会原封不动的复制到dist文件夹。其实也不是原封不动,dist文件夹里面的index.html是以public文件夹下的index.html作为摸板的。看一下这个dist文件夹下的index.html是没有jsp代码的。

八. 封装导航独立组件

    1. navbar.vue文件(整个最外部的大组件)
  • 本来打算做首页,先做导航,发现要封装独立的导航组件
  • 在components里面的common文件夹里,建个文件夹navbar,在navbar文件夹里建个比较独立的组件navbar。文件夹一般是小写,组件的名字一般是大写。
  • 首先我想组件必须有个根,<div></div>。这时候起id就不好了,因为多个地方都有导航,id这个名字有可能会重复。之前tabbar那里TabBar.vue起了个id,但是你想想整个项目只可能有一个tabbar 。所以这里起class。<div class=”nav-bar”></div>
  • 里面三个插槽<slot></slot>,这些插槽要布局,因为每个插槽大小不同。但是不能直接在slot上面写class布局。要搞个div包住插槽,然后对外面的东西布局,到时候里面的东西也布局好了。<div class=”left”><slot></slot></div>。
  • 因为有三个插槽,要给每个插槽起个名字(具名插槽),到时候才能指定替换的是哪一个。<div class=”left”><slot name=”left></slot></div>
  • 为了能看到这个东西直接的效果,直接给class比如给left设置样式,到时候东西替换掉插槽的话,就有个居左的效果
  • 用flax布局设置这个样式,整体这个.nav-bar先搞个flex,display:flex。之后给左边.left一个固定宽度,60px ,右边也60px。左右固定,中间把剩余的部分全部占掉,中间写flex:1(因为左右都有确定宽度,只剩中间了,把整个剩余部分全部占据)啊。导航的高度一般44,底部tabber的高度一般是49,设置高。设置line-height,可以设置出文字上下居中效果。只有文字和行高也能撑起来这个盒子,可以不需要设置高。
    1. 在首页使用导航独立组件
  • 在home.vue中演示一下,给三个div加颜色。用起来,在首页用,在Home.vue文件 import导入 components注册 给home也搞个<div id=”home”></div>,给id是因为,home这个东西肯定是唯一的。
  • 调用 等一下要往这里面写东西,因为要往中间插槽插东西。
  • npm run serve 报错了,调试vuex,看一下app组件里有个home组件有个navbar组件。这就确定了组件被加进来了,之后来到element看具体元素,看到底哪错了。
  • 首页这个导航条只有一个标题,替换中间的内容就可<div id=”home”><nav-bar class=“home-nav”><div slot=”center>购物街</div></nav-bar></div>。
  • 文字没居中,在NavBar.vue文件设置,在.nav-bar里设置text-align=”center’。
  • 在NavBar.vue文件内删掉左中右的颜色,之后给.nav-bar一个阴影,box-shadow:0 1px 1px rgba(100,100,100,.1)
  • 想设置导航栏的背景颜色,不建议在.nav-bar里面直接设置,因为一旦设置了,所有的背景颜色都统一了,但其实其他页面导航栏不是这个背景色。即不是完全公共的东西就不要封装的时候直接写死。所以我们单独针对home设置背景色,去到Home.vue,可以调用插件的时候再绑定一个class,然后设置背景颜色。因为在App.vue导入了css样式@import ‘assets/css/base.css’,在这个Home.vue可以不引用就用,用一下里面的颜色变量.home-nav{ background-color:var(–color-tint)}

导航栏固定住

  • better-scroll之后用,导航栏滚动。原生的跑在移动端浏览器卡顿。
  • 给首页的导航固定定位fixed。但脱离标准流,后面东西直接顶上去了,所以.home {padding-top: 44px;}
  • z-index仅能在定位元素上奏效,值越大离我们越近,默认值为0.

九. 网络封装

  • 打算先请求数据,再根据数据创造标签等
  • 请求数据需要先封装网络模块,之后直接使用网络模块进行数据请求
  • 在network文件夹里面写个request.js直接把上一节学axios写的代码复制过来。(注意看拦截器位置,响应拦截器返回的是res.data)
  • 当前项目还没安装axios,npm install axios@ --save
  • 首页(Home.vue)发生网络请求,可以面向request文件直接发送网络请求
  • 但是我的习惯是,我会对他再做一次封装。url不要直接写在home.js中
  • 在network文件夹里再建个home.js文件。这个文件里面封装了所有我对首页数据的请求(我写了好几个函数)。因为首页肯定好多请求,所以这样封装肯定好。之后Home.vue首页就是面向home.js开发了,就是面向这里面的函数开发。
  • 所以之后首页Home.vue面向的不是request.js的开发,而是面向的home.js的开发
  • 把函数导入到home.vue。import {getHomeMultidata} from “network/home”
  • 只有default导出,导入时才可以不带{}

十. 请求数据

  • 组件创建完created(),就发送网络请求, getHomeMultidata().then(res=>{console.log(res) ;this.result=res)};
  • date用于存储请求过来的数据。data(){return {result:null}}
  • 现在请求来的数据,在函数created()里面,是局部变量,函数执行完会销毁。 所以要在销毁之前存到data中
  • this在箭头函数里面this的指向,在最近的外层作用域中,去找最近的上一层created函数的this就是当前的组件对象
creeted(){
   getHomeMultidata().then(res=>{
       console.log(res) ;
       this.result=res
     });
 }
  • 看一下请求过来的res,就是变量,res指向请求来的对象。一旦函数执行完,函数里的变量res被会回收了,(请求来的数据是个对象,相当于res={},res指向对象),则指向不存在了,发现某一个对象不再有引用指向的时候,会进行垃圾回收。而this.result=res,相当于res把它指向的内存地址复制给this.result,这样就始终有东西指向请求来的对象了。
  • 验证result里面有没有请求来的数据,上图这样打印验证肯定不对,因为向服务器发请求是异步操作,所以在函数还没执行完的时候就执行下面的打印了,大概率这个this.result是个null
  • 通过工具验证,打开浏览器vuex,点开里面的home组件,这里能看到关于这个组件的变量的。
  • 这个工具方便我们vue开发调试的,你可以在这里看到我们组件里有哪些数据,没哪些数据。
  • 发现拿到的数据有点多,既有banner数据又有keyword等,如果数据只用一个变量保存看起来有点乱,所以一般不这样写
 data(){
 return {result:null} 
 
 getHomeMultidata().then(res=>{
 console.log(res) ;
 this.result=res)

而是写
data(){
return {banner:[]}    
getHomeMultidata().then(res=>{
this.banner=res.data.banner)
  • 之后在浏览器里能看,看vuex,找到home组件,能看到home组件里的数据。
  • js是没有数据类型的,就算你先给变量赋值为字符串,之后还可以给变量赋值为对象或字符串等.

十一. 轮播图展示

  • 有了数据之后要做相关的展示了,封装对应的组件,然后对他进行展示。
  • 轮播图封装成了组件直接用
  • 轮播图组件暴露了一些属性,props 接收人家传的值。多久滚动一张图片,延迟多久开始轮播,滑动换页比例,指示器。(定时器时间改长一点就能轮播了,因为接口还没加载,定时器时间很短就开始工作了,所以后面都没有执行)(看一下控制台,超时异常,把timeout放大,swiper.vue定时器设置久点就能解决图片全部加载而不轮播的问题)(因为home组件里获取数据的周期在定时器之前)(swiper.vue里面的mounted中的定时器改一下,改大就行了)
  • 轮播图里面文件index.js 导入之后统一导出。
  • home.vue 文件导入 ,之后注册,使用,使用的时候因为上面有插槽,所以之后在使用的时候做个插入即可,只需从下面的banners里面取出数据,使用
  • 案例是四个轮播图,但你肯定不能直接写四个,因为取过来的数据是不确定的,不能写死,用v-for
  • 用v-for肯定要绑定个:key,因为最新的vue使用v-for必须绑定key
  • 注意里面好多属性都要:动态绑定
  • 搜vue的ui库,可以发现好多这种组件
  • Home.vue里面想直接调用轮播图,需要写很多代码,所以home调用时还要单独封装一层。只要是独立的功能(轮播图)就对他做一层独立的封装。
  • 这个调用时的单独封装抽取肯定不是放到components文件夹里,因为这个组件不是公共的。这个东西属于home,home.vue的代码抽出去的。
  • home文件夹建个childComps,所有关于home的子组件都放这个文件夹。
  • 在这个文件夹里建个HomeSwiper.vue。
  • 关于swiper,swiperitem组件的调用都在HomeSwiper.vue文件里。导入组件,注册组件,使用组件。
  • 把home.vue需要用到的轮播图的数据都传过去给HomeSwiper.vue,而HomeSwiper.vue接收数据是通过(搞个props)给默认值,默认值是个函数函数返回数组,之后就可以直接用了。
//带有默认值的对象
props:{
  type:Object,
  //对象或数组默认值必须从一个工厂函数获取
  default:function(){
     return {message:'hello'}
 }
  • 在home.vue中引用组件HomeSwiper 导入组件,注册组件,使用组件。

十二. 推荐栏封装

  • 推荐栏也是个独立的组件,所以要封装个独立组件,在home文件夹-childComps文件夹下建组件RecommendView.vue
  • 要根据数据展示,所以要从Home.vue中传进去数据。
  • Home.vue中引用组件需要导入组件,注册组件,使用组件。
  • RecommendView.vue别忘了写根元素<div class=“recommend”>
  • :src="item.image"别忘了href和src都是动态获取的,所以加:
  • <div v-for="(item,index) in recommends" :key=“index” class=“recommend-item”>
  • flex布局
  • 设置父recommend的宽为100%还是不行,图片太大都撑开了所以这里要设置图片大小宽高w70 h70(有人说直接给图片设置width: 100%;)
  • 之后想让图片居中,在父级也就是recommend,写个text-align=”center”>。
  • 并且下面文字太大,因为文字样式是继承的所以在父写font-size:12px
  • 图片和文字想要有距离,margin-bottom:10px
  • 整体上面有空白,下面也有空白=>在父padding:10px 0px 20px,这样就会把这个撑大。
  • 父下面设置border-bottom:10px solid #eee

十三. 本周流行FeatureVie的封装

  • 就是张图片,直接从官网拿的。
  • 虽然就是张图片,也不建议直接在home.vue中写<img src=””>,不好,因为这也属于一个独立功能模块,最好也封装成一个独立的组件。
  • 也属于首页的一个小组件,直接在home文件夹下的childComps文件夹建个组件,FeatureView.vue
    独立组件都别忘了搞个根<div class=””></div> 里面直接给个a标签,给个img。div>a>img
  • 之前已经将当前项目的所有图片拿过来了,就在assets的img的home文件夹里有图片。
  • 组件封装好之后,home.vue引入注册使用组件。
  • 图片太大了,img设置成100%即可

十四. 流行 新款 精选 选项卡TabControl的封装

  • 总结:

    • props -> titles
    • div>根据titles v-for遍历 div -> span{{title}}
    • css相关
    • 选中哪一个tab, 哪一个tab的文字颜色变色, 下面border-bottom
      • currentIndex
  • ---------------------------vue项目中-------------------------

  • 公共的组件,另一个界面也用到这个组件但只属于当前项目的组件,放到content文件夹里面。这里在content文件夹里面建个tabControl(选项卡)文件夹,建个tabControl.vue文件。只有首页界面用这个子组件,放到home文件夹了。

  • 使用插槽装三个选项吗?因为要别人决定里面填什么东西。但其实没必要,样式都是一样的,就文字不一样。其实就算放插槽我们放的都是一样的,都是一个div标签里面包裹个span标签。所以不用特意用插槽了。如果只是文字不一样没必要搞插槽,只需要把准备写的文字通过props中数组形式传进来,根据数组里面有几个决定显示几个文字。首页那里不止文字不一样,图标等样式都不一样。

  • tabControl.vue中写 <div v-for="(item,index) in titles" :key=“index”> <span>{{item}}</div>啊 ,好几个div里面每个都包一个span。

  • <div v-for="(item,index) in recommends" :key=“index”> 好多个div

  • 直接在home.vue里面用起来,看哪里不合适在调整

  • 导入的组件和方法空格隔开

  • :titles="[‘流行’,‘新款’,‘精选’]" 不能省略:,省略会传字符串过去,写:这个传数组过去。:recommends=“recommends”

  • 调整样式,在tabcontrol.vue中改,给每个小div加个class。 style里面,多个类中间空格隔开,一个类多个样式用;隔开。

  • flex布局。文字居中。因为高度是文字撑起来的高,所以 设置line-height和height,他俩可以都在父级设置,因为line-height这些东西都是可以继承的。

  • 不能用a:hover,用这个没有默认选中项。 因为选中之后这个状态是一直的。

  • 之前讲过,一个列表,点击列表中的哪一个,哪个就变成红色。
    做法:在data中搞个变量(data是函数,因为子组件,返回个对象),记录当前谁处于选中 currentIndex:0

  • 标签动态绑定class, :class="{active:index===currentIndex}"。active是个类 <div v-for="(item,index) in titles" :key=“index”> 这个index是for循环的那个index索引。默认情况下索引刚开始为0,所以刚开始第一个处于点击状态.

  • 写样式 .active{} 点击谁,谁加active这个类。

  • @click="itemClick(index)"监听div的点击,把当前监听到的点击的index传过去赋值给currentIndex。itemClick(index){this.currentIndex=index},这时候所有在用的currentIndex的值都改了,改了之后上面就会做个最新的判断。

  • 不能给大的div添加border-bottom,因为一个div占1/3(flex布局),会很长,所以给span添加boreder.active span{} 注意是给处于点击状态的span添加。还要给span加padding,是给.tab-control-item span{}添加,整体sapn添加padding ,真的可以。

选项卡吸顶效果

  • 吸顶效果,用position:sticky。
  • 监听滚动,一旦滚到某个位置,把这个选项卡改成position:fixed,这样选项卡就停留在这里不跟着一起滚动了。之后还要监听向下滚动,一旦滚到位置,把fixed属性删除掉,没了这个属性,她就随着我们一起滚动了。
  • 首页home.vue调用这个组件时加这个功能,即加个class ,因为我们在tabControl.vue中改的话,所有用我们这个组件的都有这个功能,所以我们在home.vue中改,只让首页有这个功能。
  • class里面写这个属性,position:sticky top:44px;用这个属性sticky必须设置top。
  • position:sticky,也是监听滚动,一旦滚到某个位置,这个position会变成fixed的效果。不会脱标导致突然向上吗
  • 这个选项卡现在是透明的,要改一下,背景颜色改成不透明 bgc:#fff,在tabControl.vue改。
  • 移动端sticty可以,web不行,因为ie不行,兼容性不好。后面用betterscroll。

十五. 展示商品列表页

15.1. 保存请求来的商品数据的结构设计

  • 首先,先把数据请求过来,必须保存下来,再展示。
  • 复用:数据多,分成三类数据。点击选项卡,数据好像没切,因为vue内部会对很多组件进行复用。如果不想让他复用,这时候要绑定个key,这里把图片的名字作为key。
  • 三类数据,流行,新款,精选数据,点击流行,展示流行的数据。不同点击,展示不同数据。
  • 数据要都有,至于展示哪个,根据用户点击的。
  • 搞个变量,里面存三种数据流行/新款/精选(不然你每次点击,要赶快向服务器端发生请求,那时用户延迟较长,所以一次性请求这三个数据,并且在一个地方做个存储。流行的数据有多少页,超级多,不应该一次性将所有数据全部请求下来)之后根据用户的点击,比如点击流行,这时把变量里面已经存储的流行的数据取出来,做个展示。
  • 数据保存的模型goods: {}goods是个变量里面有三类数据。goods是个对象
    ‘pop’: {page: 5, list: [150]}150个对象里面存了每个数据详细信息]},‘pop’里面对应的也是个对象。
    ‘news’: {page: 2, list: [60]},记录当前新款在加载数据时加载到第几页,新款对应的数据都放在数组里,每一类数据对应一个数组
    ‘sell’: {page: 1, list: [30]}}
  • page加1,数据30条变成60条
    这三类数据在保存数据时都是用数组保存,要想确定数组里面保存的,到底是几页了,要搞个页码
  • 因为这三类数据每类数据的情况不一样,第一类数据才到5页,第二类数据才到2页,第三类数据才到3页
    上面这样写就是为了把这复杂的情况表示出来。
  • page变量用于记录你当前加载到第几页了,另一个list变量用于记录这个类型(比如新款)当前已经加载到多少数据了。
  • 这个数据模型应该写到home.vue中的data中,因为这些数据都属于首页的数据。
  • 设计模型:home.vue中的data中写goods,默认情况下是第0页,因为 当前什么都没有请求,默认数据list为空数组
  • 先把数据模型设计好,之后就可以去请求数据了,请求到数据往list里面放,并且改page。
  • 把请求到的数据放到设计好的数据模型里面。
  • 加引号了
goods:{
  ‘pop’:{page:2,list:[60 ]},
  ‘news’:{page:3,list:[ 180]},
}
  • 刚开始的时候所有的page和list都是0
  • 模型设计好-请求数据-请求好的数据塞进模型里面

15.2. 请求数据之前先网络封装

  • 请求数据,默认情况下是把每类数据的第一页请求过来,第二页第三页只有上拉之后再请求更多的数据。
  • 请求数据应该从哪写代码,网络封装那里,进行网络相关的操作时在home.js中封装getHomeGoods(type, page),都进行了相关的网络封装。所以我们也在那里封装个函数。
  • 这个函数要不要传参数,需要,因为要针对不同的情况请求不同的数据。…?type=sell&page=1 总共三个类型,请求精选里面的第一页数据。所以形参是(type,page)
  • 因为有baseurl,所以这里写个url就行。地址栏中要有参数,传参数需要写params:{type,page}
export function getHomeGoods(type, page) {
    return request({
        url: 'home/data',
        params: {
            type,//就是上面通过形参传过来的实参page:page
            page
        }
    })

15.3. 请求数据

  • 在Home.vue里面导入方法。import{…,…} from …导入同一个页面里面的两个方法可以这样写
  • 什么时候发送这个请求,请求商品数据,即调这个方法,组件一旦创建就请求商品数据,可以写在created中。
  • res是局部变量,在函数执行完之后就消失了,会被回收,所以把res保存 到data中。
  • 关于保存数据的代码,如果也写在created中,那么created的代码看起来有点多乱。created里面建议只写做了什么,比如我准备发送这个请求,只写主要的逻辑。这个处理逻辑的搞个methods,抽到methods中,又在methods中getHomeGoods(type)。实际项目业务处理不可能直接写在created中。created() {}
  • created调用methods里面的方法必须要加this即this. getHomeGoods,(注意methods里面包裹一层方法,包裹里面调方法后面跟then)要不然和导入的名字一样了,不加this相当于还是调用外面导入的方法,而不是methods里的方法。
  • 这样写还有个好处,可以多次调用。
  • methods里面的方法页数传1不合适,因为到时候想复用这个getHomeGoods 方法,比如下拉加载第二页还调用这个方法,那个页数不一定是第一页,应该是原来的页码加1,第一次created的时候,之前page是0,之后当前页加1,即请求第一页数据。
  • 以上,因为请求多了一组数据。所以页码要加1.所以要动态获取page,const page=this.goods[type].page+1(原来page的基础上加上1)啊
  • 之后上拉加载更多,直接调用这个方法就行了,会直接取的下一页的数据。
  • js中可以使用两种方法获取对象中属性值,[]和.都可以。变量的形式要用[],type是变量 ,所以写成对象[变量]的形式。如果用.就写死了,[type]是函数的形参,当key是动态时,用[]获取属性值。[]里面可传变量,参数,表达式,而.后面只能跟固定属性。
  • 虽然说const定义的基本类型值不能改变,但这里用const没错,每次执行完函数里面的变量就会清除,等于又调用则重新创建一个常量赋值
getHomeGoods(type){
  const page=this.goods[type].page+1
}
  • 把数据存起来,push进数组中即可。(比如把page=2调出来的数据,push进之前的数组)怎么把请求来的一个数组放到另一个数组里面,扩展运算符直接push。arr.push(…arr2) 内部把数组里的东西依次拿出来,放到另一个数组。 …arr2相当于也是个解构,会将数组里的元素依次解析出来,之后依次塞进数组中。 其实是因为push函数可以传可变的参数,就是push的时候可以这样传很多个数据arr1.push(10,20,30)一旦你看到某个函数前面有…意味着你可以传很多个数字,然后内部会以,分割,最终相当于他们会把这些参数放到数组里,之后完成一些操作。 但是你想直接放数组不加…是不行的,arr1.push([10,20,30]),到时候会变成[1,2,[10,20,30]]
  • 如果直接赋值,改变的 是指向,arr1=arr2。直接赋值会导致只有一页,第二次覆盖上次的数据。this.goods[type].list=res.data.data.list是不对的。是追加而不应该赋值。
  • 可以遍历for(let n of arr2){arr1.push(n)}遍历新数组for of,一个个push进之前的数组中
  • concat也可以,但是不会改变原数组,返回一个新数组,你可以把新数组的地址返回回去。有人说不行,数组不是动态的,视图层检测不到变化。
  • this.goods[type].list.push(…res.data.list)
  • 之后pop类型就多了一组数组,之后把data中页码页加1,一开始进来加1,只是为了请求时传参,是局部的页码,data中页码也要加
  • 看一下当前请求到的数据,在浏览器的vuex中看。

十六. 数据展示

16.1. 封装GoodsList.vue组件

  • 准备展示的数据有了,之后封装对应的组件,用对应的组件对数据进行对应的展示
  • 准备个大组件,根据数据的多少,遍历多少个小的goodslistitems,塞多少个小组件,通过遍历的方式(for)塞小组件。v-for遍历,整个是一个组件,然后包小组件
  • 展示商品列表这个东西不是在当前文件夹写,因为多个页面用,所以写到content中(和业务有关)
  • 建个Goods文件夹GoodsList.vue文件(大组件)GoodsListItem.vue文件(小组件)
    先看看怎么用。Home.vue导入GoodsList.vue ,之后注册,之后使用< good-list/>
  • GoodsList.vue文件是要展示商品的,要求home.vue把对应的商品数据给GoodsList.vue文件, GoodsList.vue里面需要home.vue传数组, GoodsList.vue文件props接收父传来的
  • home.vue里面传 <good-list :goods="goods[‘pop’].list/>
    这样GoodsList.vue拿到商品list数据,就遍历展示即可。 props: {goods: {}}
  • 因为是数组,所以默认值这里是函数形式,返回数组。
props: {
   goods: {
      type: Array,
      default() {
        return [];
      }
   }
}
  • 在GoodsList.vue文件中查看数据{{goods}}

  • 意味着刚开始从Home.vue导入全部商品列表到大组件里,根据这个数据遍历,进而调用多个小组件。然后传单一数据到小组件

  • 全部商品数据数组->单独一个商品数据对象 父之前是个数组里面有30个对象,之后遍历这多少对象,把每个对象传给子,由子那边展示

  • 之后封装goodslistitem.vue,之后展示数据

  • 根据goodslist.vue中的goods进行v-for循,决定遍历多少个Goodslistitem.vue 。每个goodslistitem.vue展示一个具体数据。

  • 把goodslistitem.vue导入import到 goodslist.vue,注册组件。使用。使用单标签就可以< goods-list-item />,因为里面不插入插槽,只有有插槽时用双标签数据是死的不需要修改,不需要用插槽。结构不同才用插槽,只是数据不同,传参数替代就行了。

  • 这段代码还是写在GoodsList.vue文件,使用子组件,并传数据到子组件,哈<goods-list-item v-for=“(item,index) in goods” :key=“index” :goods-item=“item” /> 之后根据goods遍历出来好多的goods-list-item小组件。每个小组件都需要分配小数据,即每个小组件都需要传进去对象。 :good-item=“item” 是传数据到goodslistitem.vue文件

  • 这个<goods-list-item/>用的是单标签,因为里面没有插槽不需要双标签

  • v-for是根据数据goods遍历出来很多小组件

16.2. 封装GoodsListItem.vue组件

  • 所以小组件goodslistitem.vue里面也需要数据,里面需要写props接收父传来的数据
  • 因为是对象,所以默认值这里是函数形式,返回对象。
  • goodsItem
props: {
    goodsItem: {
      type: Object,
      default() {
        return {};
     }
}
  • goodslistitem.vue里面拿到数据了,动态的从对象里面取出图片地址<img :src=“goodsItem.show.img”> 这样goodslist.vue就显示出来一个个图片了。div/span/img基本标签进行展示。

goodslistitem.vue商品展示页布局

  • goodslistitem.vue写代码。刚开始写是一行里面有一个商品。
  • 给这个商品展示的组件,整个的底部设置了padding-bottom: 40px 。这样放图片时不是贴着组件底部放。
  • 里面图片占这个组件100%,里面图片四个边框设置了一点圆角效果
  • 之前padding-bottom搞40px因为:下面整个文字部分用的是定位。子绝父相。即下面文字这个整体占40px,直接把它定位到底部空白区域。
  • 文字那里设置样式主要是让文字超出部分以…形式显示
  • 收藏这个文字的前面有小图标,用伪元素做。之后用定位设置位置。
  • 现在这样是一个商品占一行,想让两个商品占一行,而且均等分。我们在goodslist.vue中写。让整个大组件flex布局。如果希望在flex布局里,让一行显示两个首先小组件要设置固定的宽度,如50%。如果没有宽度,设置flex布局之后,到时候均分的时候,全部往一行塞的。(一旦把父组件设置成flex,默认是全部往一行塞)所以在goodslistitem.vue设置width:50%。
  • 设置goodslist.vue display:flex; flex-wrap:wrap (flex-wrap即问是否换行,不在一行显示)(默认是nowrap则全部东西往一行塞)justify-content:space-around(设置空格均等分,左右就算是边框边缘都有空白)啊(因为宽度是48%,如果不加这个属性还不是均等分)(但是这个均等分还不合适,可以看一下justify-content:space-around的定义,因为中间是均等分的两份,两边是一份。这就导致中间缝隙比左右两边大)(为了让两个空白也大一点,给整个大组件设置了一个padding:2px)(其实前面没必要,想要均分可以用justify-content: space-evenly)
.goods {//这里中间没符号
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  padding: 2px;
}
  • 往上滑动发现商品展示把tab-control栏盖上了,则在home.vue文件中找到tab-control样式设置为z-index:9(没单位)
  • 如果发现变透明了是因为默认背景颜色就是透明,手动设置个背景颜色即可

十七. TabContol点击切换商品

  • 之前往goodlist.vue传入数据的时候是直接写死的这样写死是不行的,你点流行首页精选数据都没反应,都是这几个数据
  • 根据点击了谁,动态决定选择哪个类型。内部监听点击
  • 之前也监听点击了,之前发生点击自己做一些切换点击谁,谁下面有粉色下划线。现在还要外部也知道点击了谁,然后请求数据。(即把点击事件子传父发给home.vue确定是哪个按钮点击了,这里会把索引值发过去,然后在home.vue根据索引用case确定’pop’,'new’等 ,然后请求对应的商品数据,然后传给其他组件显示商品数据)。
  • 根据点击谁,决定从谁里面取数据。
  • 当发生点击时,告诉父组件,子组件点击了谁,发送事件这里对应点击的数据是谁,给父组件传过去,父向服务器请求对应的数据this.$emit(“tabClick”, index)
itemClick(index) {
   this.currentIndex = index;
   this.$emit(“tabClick”, index)//TabControl.vue中写这样一句
}
  • 在Home.vue中父监听子发射的事件v-on:语法糖@,即@tabClick=“tabClick”(这个后面是个函数)这里不传参数不写index也没事,接收$emit传出来的,可以不用写参数。默认传进来了。<tab-control class=“tab-control” :titles="[‘流行’,‘新款’,‘精选’]" @tabClick=“tabClick” />
  • tabClick(index)注意事件别有大写,不然容易报错
  • 根据索引设置请求的值是‘pop’还是‘new’还是‘sell’ 这里用switch
tabClick(index){
   console.log(index);//0  1  2 
}
  • goods[‘pop’].list 值不能写死了,由index动态决定。在data中写个属性currentType:‘pop’默认当前是pop
  • 是<good-list :goods=“goods[currentType].list” />
    感觉这段代码太长了,正常情况下会搞个计算属性
    <good-list :goods=“showGoods” />
 computed: {
    showGoods() {
      return this.goods[this.currentType].list;
    }

十八. 对滚动进行重构: Better-Scroll

18.1. Better-Scroll基本使用

  • 原生的滚动在移动端十分卡顿,所以对滚动做一个重构,用这个插件实现原生的滚动
  • 注意: wrapper -> content -> 很多内容
  • Better-scroll使用的前提是必须有个高度,这个要设置的
  • Github上搜一下Better-Scroll
    branch选master看一下提交的tags看一下他的版本
  • 为了不干扰首页代码的使用,目前在Category.vue中演示, Better-Scroll演示代码都在这个文件。
  • 既然用这个框架(即依赖这个框架),第一步需要安装这个框架。1.可以在项目中直接安装。2.还可以下载源码,通过script的方式去引用。
    1. 下载源码方法,去github中(以后找开源项目的源码都可以去github上,找最新的tags,找到dist文件夹(dist里面就是最新打包的源码)。即找到tags,选了v1.13.0 ,clone or download,打开dist文件夹,里面有个bscroll.js文件
    2. 在项目中supermall中npm install better-scroll@1.13.2 –save

局部滚动

  • Category.vue中演示,ul超多li。
  • 想实现局部滚动,原生js也可以
  • 父标签ul给个固定高度,加个class。给个height。 background-color: red好观察。给个overflow-y:scroll(设置这个之后,在y轴上面是可滚动的,这样设置之后超出部分会隐藏的)
  • 之后先学会怎么用bette-scroll,但后面所有组件都不直接用,后面要做个封装,否则万一第三方插件不更新了,之后其他地方用封装的组件就可以了。
  • 不想用原生的滚动了,先删掉overflow-y:scroll
  • 准备用Better-Scroll,先导进来import BScroll from 'better-scroll‘
  • 在created(组件创建完之后调用)中写,一旦组件创建好就执行这个函数。不对。要在mouted函数里写, mounted函数在初始化页面完成后,再对dom节点进行相关的操作而created函数里面的dom没有开始加载,拿不到dom元素的,如果在created组件创建完之后调用,没有挂载摸板,拿不到dom。要在mouted里面拿dom
  mouted() {
    new BScroll(".wrpper", {});
  }
  • 传入个类,比如.wrpper,会自己查对应的标签。用Better-Scroll去管理这个东西。

  • 注意这个插件用的时候,外部要有个大.wrapper包住,第一个.content才能滚动

  • 如果没有变量来接收,mounted函数内部不确定会不会把内部创建的对象保留下来,如果内部没有引用指向它,mounted函数执行完之后,内部的对象会被销毁的。不确定内部有没有搞个东西专门引用这个对象,如果有引用,就不销毁(在这里不需要也可)为了保险,搞了个变量接收 。

  • <div class=“wrapper” ref=“aaa”>不需要ref=“aaa”也可。这是为了防止有好多个class=“wrapper” 这样能确保取到它

  • overflow: hidden;

<div class="wrapper" ref="aaa">
  <ul class="content">
      <li>分类1</li>
      <li>分类2</li>
  </ul>
<li>
data(){
   return: {
    scroll: null
  }
}
mounted() {
    console.log(this.$refs.aaa);
    console.log(document.querySelector(“.wrapper”));
    // this.scroll = new BScroll(this.$refs.aaa, {});
    new BScroll(".wrapper", {});
  }
 overflow: hidden;
  • 注意这个想看到效果要在手机模式下,用鼠标滑动才有效
  • 注意这个height不是无限高度,无限高度是不能滚动的。即传入new Bscroll这里的东西的内容不能是无限高度
.content {
    height:100px;
    background-color:red;
    overflow:hidden
}

在index.html中使用Better-Scroll

  • 文件夹learnBS 随便建一个index.html,在index.html演示一下怎么用
  • 去github下载Better-Scroll源码bscroll.js 。或者因为之前npm安装了Better-Scroll,则node_modules中有这个文件bscroll.js拿出来。
  • 重新创建个小项目。写个index.html。通过script引用<script src=“./bscroll.js”></script>
  • 你可以打印一下console.log(BScroll)打印出来明显是个构造函数。
  • 所以用的时候直接const bscroll=new Bscroll(‘.wrapper’)(注意这个.wrapper可以不叫这个名字 )第一个值就是写你要挂载什么元素,跟new Vue()里的el一样。你去浏览器试一下确实可以滚动了

在这里插入图片描述

  • 里面是些可选的属性
  • Better-Scroll使用const bscroll = new BScroll(‘.wrapper’, { probeType: 2,click: true(默认是false)}
<div class="wrapper" ref=“aaa”>
  <ul class="content">
  <button @click="btnClick">按钮</button>
      <li>分类1</li>
      <li>分类2</li>
  </ul>
  
   
 const bscroll = new BScroll('.content', {
    probeType: 2,
    //想监听实时滚动到哪个位置了,就来这里传入这个属性。同时下面监听scroll事件,在事件回调函数里面就能拿到position属性。
    click: true,
    // Button无论是true和false都可以点击,而div还有组件就必须设置true才可以点击。 
    pullUpLoad: true
    //想实现上拉加载更多,就来这里传入这个属性。同时下面监听pullingUp事件,并且上拉加载更多做完的话,一定要调用一下 bscroll.finishPullUp()方法
 })

methods:{
   btnClick(){
     console.log(1)
   }
}

Better-Scroll{}里面的属性

  • 1.监听滚动
    • probeType: 0/1/2(手指滚动)/3(只要是滚动)
    • bscroll .on(‘scroll’, (position) => {})
  • 2.上拉加载
    • pullUpLoad: true
    • bscroll .on(‘pullingUp’, () => {})
  • 3.click: false
    • button可以监听点击
    • div不可以

Better-Scroll{}里面的属性-监听用户实时滚动到哪个位置

  • Better-Scroll想监听用户实时滚动到哪个位置了。(像我们之前可以通过jQuery/原生拿到一个滚动的x和y值
  • 监听滚动事件scroll,本身会传个position ,实时记录当前滚到哪了。即bscroll.on(‘scroll’,(position)=>{console.log(position)} 去浏览器看一下,并没有效果。因为默认情况下bscroll是不可以实时的监听滚动位置。因为想要监听实时滚动位置,必须在new Bscroll(‘.wrpper’,{}) 1的第二个参数里面传参数(第二个参数是个可选参数,有时候可以不传
  • 我们强调一下第二个参数里面有个非常重要的属性probeType要不要实时的侦测滚动的位置如果probeType:0代表不侦测默认值 1不实时侦测 2实时侦测 在手指滚动的过程中侦测,手指离开后的惯性滚动过程中不侦测 3 只要滚动都侦测

Better-Scroll{}里面的属性-监听上拉加载更多

  • 想监听上拉加载更多,去 better-scroll使用手册中看。(新版本的需要用插件,现在用这个版本better-scroll不需要使用插件。首先引入 pulldown 插件,并通过静态方法Bscroll.use(PullDown)初始化插件。然后, )实例化 BetterScroll 时, new Bscroll(‘.wrpper’,{})的第二个参数里面数需要传入参数pullUpLoad:true。才可以实现上拉加载更多事件这里的.wrpper是外层那个div
  • (上个演示案例监听了scroll事件,是为了确定位置到哪了)想实现上拉加载更多就可以监听pullingUp事件这个事件是上拉加载的动作后触发,监听到这个事件后回调后面的函数。bscroll.on(‘pullingUp’,()=>{})
  • 想实现上拉加载更多,要监听什么时候better-scroll已经滚动到最底部了,一旦滚到最底部回调个事件,(之前展示第一页数据,赶快去请求第二页数据,第二页数据追加到数组中,一起展示出来。
  • 第一页的数据一直拉完,没东西了,滚到最底部了,会触发这个上拉加载更多
  • 注意这个上拉加载更多只会触发一次。只触发一次肯定不对,因为我们有很多页数据。所以监听到这个事件,先去发送网络请求,请求更多页数据,等数据请求完成,并且将新的数据展示出来后(新的数组塞进之前数组后页面会自动发送刷新,新数据会自动展示出来),之后必须做一件事情,让他能进行下一次的上拉加载更多。调用bscroll.finishPullUp(),调用这个代表这一次上拉加载更多事件已经做完了,要展示的数据也展示完了。之后才能让你做下一次的上拉加载更多。我们可以把bscroll.finishPullUp()事件写在延时setTimeout中,等待2s之后才能做下一次上拉加载更多事件。
  • 比如现在我们如果没写这个bscroll.finishPullUp(),无论拖几次只会在console输出一次上拉加载更多,但你写了这个之后,你一直拉会一直console输出上拉加载更多。但是你2s内拖多次只会打印一次,因为2s内,没有调用bscroll.finishPullUp()之前, 不能再次进行上拉加载更多的,即bscroll.on(‘pullingUp’,()=>{})这个实现不了
  • 只有调用bscroll.finishPullUp()之后,才能进行下一次上拉加载更多。
  • 监听这个on事件,必须调用这个函数bscroll.finishPullUp(),才能进行下一次上拉加载更多

Better-Scroll{}里面的属性-监听点击

  • click:true Button无论是true和false都可以点击,而div还有组件就必须设置true才可以点击

18.2. 在Vue项目中使用Better-Scroll

  1. 如果不封装Better-Scroll,在Profile.vue中简单的演示
  • 在首页用Better-Scroll,对首页里面可滚动的那部分区域进行重构,之前用的原生滚动,现在不想用原生滚动
  • 在Home.vue中import引入Better-Scroll。
  • 在Home组件的mounted函数中创建实例Better-Scroll,并且到时候让他绑定首页除了导航之外下面的所有内容
  • <div class=“wraper”><div class=“content”></dvi></div>包裹住导航下面所有东西。
  • 然后给.wrapper设置个固定高度,把.wrapper给我们创建的Better-Scroll实例。
  • 这样不好,这个组件对better-scroll依赖太强了,如果所有页面只要涉及到滚动的界面都用better-scroll,肯定不行,所以把better-scroll封装一下,以免不再维护后,我们可以换成其他的滚动框架
  • 我们先封装一层,封装的对Better-Scroll依赖,我们只需要对我们封装的东西有依赖,如果有天Better-Scroll变了,我们只需要对封装层改一下他的依赖就行了
  • 封装个scroll.vue文件,只让他自己对Better-Scroll有依赖
  • 在这里插入图片描述
  1. 对Better-Scroll进行封装: Scroll.vue
  • 在多个页面用,封装到公共组件common文件夹,Better-Scroll的封装所有项目都可用

  • scroll文件夹scroll.vue文件

  • 以后所有项目想用这个组件,只需复制scroll文件夹,import导入,然后注册组件,之后把需要滚动的文件放到标签中<scroll class=“content”> (home.vue)不过这个还是要加个class,为了设置高度。因为设置高度这种东西还是应该由你自己决定。注意没有被包进去的东西是不可以滚动的

  • 给home.vue中<scroll></scroll>固定的高度,里面的东西就可以滚动了为啥高度设置到scroll还有效果哦?这个scroll 不是会被替换掉吗?不该是设置到.content 上吗

  • 为什么需要设置高,因为可以决定内部是可以滚动的,但是我不知道你用的时候打算设置的可滚动的区域是多少

  • scroll.vue搞个插槽 <div class=“wrapper”><div class=“content”><slot></slot></div></div> 到时候调用的时候 ,home.vue页面中 <scroll>里面的东西即滚动区域会代替插槽

  • home.vue中<scroll class=“content”></scroll>这样home.vue中有个class=“content”,scroll.vue中也有个class=“content” <div class=“wrapper” ref=“wrapper”><div class=“content”>不会同时设置两个吗,不会的因为样式只在当前组件有效。 <style scoped>这个scoped是作用域的意思,加了这个样式只在当前组件有效,否则都有效

  • <style scoped>这个scoped还挺重要的,注意看组件中有没有加上

  • -----------设置height确定可滚动区域-------

  • content{height:300px;overflow:hidden} 设置height的目的是确定可滚动区域。

  • 这个高度应该是视口高度-44(导航栏)-49(最底部tab栏)css3有个calc函数calc(100vh-44px-49px)100vh表示整个浏览器窗口高度,vh表示视口,100vh表示百分百视口,50表示50%视口注意calc函数运算符前后必须加空格 100%是相对于父组件。

  • 老师讲的:有点麻烦了。不过老师最终也没用这个。最终用的第二种方法。

    • home搞height: 100vh;如果不搞可以看一下height很高的,被撑开了。我们希望home的高不是这么高,我们打算给home个确定的高度,给个视口的高。
    • 这样100%相对于父 就是也是视口高度了
    • 把padding-top: 44px去掉。因为一旦设置padding,我们内容区域的高度就没100个视口了。即子height的100%,是不包括这个导航栏homenav的44px的。为了效果去掉padding。
    • 加上margin-top就行了,因为导航栏是浮动的,这部分区域要空出来
    • 注意阴影是不占位置的
#home {
  /* padding-top: 44px; */
  height: 100vh;
  position: relative;
}
 .content {
    height: calc(100% - 93px);
    overflow: hidden;
    margin-top: 44px;
 }
  • 还有一种方法,定位absoloute(子绝父相,父要设置成相对定位.home设置成相对定位)
  • 上面空出来44,下面空出来49,到时候内容就是中间的那部分
  • 不过第二种方法脱标了,之前外围写的为了撑开的内容都在下面了,这时候我们删掉文字,或者给.content一个白色背景色也可以(但没必要)
  • 第二种方法也是可以不设置 padding-top: 44px,因为内容区是相对定位, top: 44px; 根本就不会被固定定位的导航元素盖住。
  • 最终用这个,就是用了这个自此之后,整个内容区可滚动区由标准流变成浮动的了。本来没设置better-scroll高度
<style scoped>
#home {
  /* padding-top: 44px; */
  height: 100vh;
  position: relative;
}
.content {
  position: absolute;
  top: 44px;
  bottom: 49px;
  left: 0;
  right: 0;
  overflow: hidden;
}

关于ref

  • 在scroll.vue中this.scroll = new BScroll(querySelector(‘.wrapper’),{});这样写不好,因为querySelector获取的是第一个wrapper类,如果App.vue和Home.vue中(即引用scroll.vue 的父中)也有一个同名的类,那么这个获取是不准确的
  • 在vue里面想明确的拿到一个元素,就是给这个东西绑定ref
  • 之前讲这个ref属性,说的是这个属性一般绑定给子组件,如下例子
  • 通过父组件获取子组件的两个属性1.chirdren 数组获取所有子组件,2.另一个ref去明确拿到某一个子组件。
  • 之前讲的ref都是绑定到组件上面。举例,在首页app.vue中里面想拿home-swiper这个组件在app.vue中引用子组件<home-swiper ref=“swiper”>,之后在下面的某个位置mounted(){this.$refs.swiper}这样就拿到那个组件了
  • 但是div这种普通的元素上面也可以绑定ref ,如下例子<div ref=“aaa”> mounted(){this.$refs.aaa}这样就拿到这个元素了
  • ref如果是绑定在组件中的,那么通过this.$refs.refname获得到的是一个组件对象
  • ref如果是绑定在普通的元素中,那么通过this.$refs.refname获得到的是一个元素对象
  • this.scroll = new BScroll(this.$refs.wrapper,{});这个this指当前组件,也就是拿到当前组件里ref里绑定的wrapper

父组件动态决定是否使用better-scroll的某个属性

  • scroll.vue里面(需要用到滚动的页面都用这个组件),并不是所有的都要封装进去,因为有的调用这个组件的不想要这个效果,比如click:true有的并不想要实时监听。 如probeType:3,有的不需要probeType:3,不需要下面的监听滚动位置的事件。你会想你不想用,不用这个position不就行了。但是,你不需要还设置probeType:3会影响性能的,因为当你设置3之后,会实时的回调下面的函数。用不到,还回调,影响性能。
  • 所以这个值不写死,搞个props,让用户决定。根据传入的东西决定是否监听滚动
 <div class="wrapper" ref="wrapper">
    <div class="content">
      <slot></slot>
    </div>
  </div>
  
props: { 
      probeType: { 
      //这里是驼峰,在home.vue那边传值的时候需要用-分割probe-type
        type: Number,
        default: 0  
      },
    pullUpLoad: {
       type: Boolean,
       default: false
    }
},
 mounted() {
    // this.scroll = new BScroll(".wrapper");
    this.scroll = new BScroll(this.$refs.wrapper, {
      click: true,
      probeType: this.probeType,
     //这里写01都可,效果一样,都是什么都不监听
      pullUpLoad: this.pullUpLoad
     //这个代表引用用户传进来的值,由用户决定 rue或false
    });
    this.scroll.on("scroll", position => {
      this.$emit("scroll", position);
    });
    this.scroll.on("pullingUp", () => {
      this.$emit("pullingUp");
    });
  }
  
Home.vue
<scroll class="content" ref="scroll" :probe-type="3" @scroll="contentScroll"  :pull-up-load="true"  @pullingUp="loadMore">
 //这段代码目的是动态决定能否监听滚动位置

  • 这里前面写:如果不写:传值的时候可以传过去,但是你传的是个字符串,而写:就会把里面东西当做number类型,会当成一个变量
  • 这个position并不是让你在scroll.vue组件中用的,而是需要传到你要用的地方,如home.vue中
  • this.$emit(“scroll”, position); 自定义事件名字叫scroll。子组件将自定义事件发出去
    哪个组件想用这个东西就监听这个自定义事件,如Home.vue <scroll class=“content” ref=“scroll” :probe-type=“3” @scroll=“contentScroll”>

十九. BackTop组件

布局

  • 滚动到某个位置,添加回到顶部按钮,即滚到某个地方决定回到顶部按钮的隐藏和显示
  • 发现之前tab栏新款流行那个,粘性布局不起效果了,随着滚动一起滚动。因为现在用了better-scroll之后,原生滚动不起效果了,是better-scroll在帮我们滚动,所以系统没办法帮我们检查tab栏滚到哪里了。
  • 这个BackTop组件封装到content文件夹backtop文件夹 BackTop.vue 就是个小图片
  • Home.vue中用import ,注册,这个东西并不随着大家的滚动而滚动,不需要滚动,所以不需要放到scroll标签里面
  • 这个小组件前面的组件除了导航栏都脱标了,这个小组件的位置,一直跑到导航栏下面。
  • 因为发现这个小组件在这个项目的任何界面都有,且差不多在一个位置,所以我们可以统一的在BackTop.vue设置他的位置样式
  • 我们可以给这个小组件设置position:fixed;right:8px;bottom:55px; fixed布局会脱标。而且这个写在position:absolute的后面,所以最终呈现出来的效果是在上面。Z-index
  • fixed定位top等是相对于浏览器的

回顶部

  • 监听点击,里面回到顶部事件。
  • 在BackTop.vue中监听点击能实现。但回到顶部意味着滚动区域回到顶部。意味着你要拿到better-scroll对象,因为现在滚动都是依赖better-scroll的。
  • 回到顶部实现:之前better-scroll那里(scroll.vue文件)new了个对象,这个对象有个很简单的方法scrollTo,实现方法是this.scroll.scrollTo(x,y)a,根据x和y去到某个位置。scroll.scrollTo(0,0)去顶部 scroll.scrollTo(0,0,500)500毫秒内回到顶部
  • BackTop.vue点击事件操作 scroll.vue里面代码,我的思路$emit把事件发出去给他父亲,父亲通过$refs可以访问到scroll组件了scroll组件滚动到顶部
BackTop.vue 
<div class="back-top" @click="backClick">  

methods: {
    backClick() {
      this.$emit('backClick')
    }
}

Home.vue 
<back-top @backClick="backClick" />
  • codewhy的思路:他感觉很多东西多余了, BackTop.vue 中写的代码删掉,写的点击事件删掉了。干脆直接在Home.vue中监听 <back-top/>的点击,因为目的就是为了监听BackTop.vue里面最外层的div的点击,干脆就监听组件的点击,因为这个组件就是返回顶部的小按钮。这样就不需要像之前那样内部先监听再把事件发出去,在外面再监听了<back-top @click=“backClick” />
  • 但是组件能不能直接监听点击事件?原生元素<button><div>这种可以。但是组件这种我们可以验证一下backClick(){console.log(‘backClick’)}不行,组件是不能直接监听点击的,想监听加个修饰符.native(原生的) <back-top @click.native=“backClick” />
  • 修饰符.native什么时候用?在我们需要监听一个组件的原生事件时(不仅是click事件也包括其他事件,只要是原生事件),必须给对应的事件加上.native修饰符才能进行监听
  • Home.vue <back-top @backClick=“backClick” />
  • 想办法拿到scroll组件里的scroll对象,就是scroll.vue中new的那个,new完的数据存到data中了,拿到data中的scroll数据
  • 厉害的一点,把Scroll.vue中的new Scroll 保存在一个变量里,然后父组件通过$refs拿到
Home.vue
 <scroll class="content" ref="scroll">
 
Scroll.vue 
data() {
    return {
      scroll: null
      //message:‘这个你也能拿到’
    }
}

Home.vue 
backClick() {
      // this.$refs.scroll.scroll;
      //拿到组件对象,就是拿到<scroll>…</scroll>的所有,这个组件对象里面有个属性叫scroll,可以这样通过这样拿到
      // this.$refs.scroll.message;
      //这里面有个message也可以这样拿到
      this.$refs.scroll.scroll.scrollTO(0, 0);
  • 啥时候用父子通信(如父传数据要在子组件展示),啥时候用refs(访问子组件内部的属性及方法)

  • scroll对象, scroll.scrollTo(x, y, time) this.$refs.scroll.scroll.scrollTo(0, 0, 500);可读性差,在scroll.vue中再封装,在methods中scrollTo(x,y,time=300){别人到时候不传这个值的时候默认是300,这是es6语法)this.scroll.scrollTo(x,y,time)}到时候直接调用this.$refs. scroll.scrollTo(0, 0);

  • 项目跑在手机上做法,连同一个wifi情况下,登录第二个网址

  • 图片没渲染完成就实例化scroll导致往下拉的时候图片卡顿bug。每次下拉都会拉不动,因为图片还在请求。

BackTop的隐藏和显示

  • 临界值,达到显示,小于隐藏。如>1000显示,<1000不显示
  • isShowBackTop: false
  • 监听滚动, 拿到滚动的位置:
    • -position.y > 1000 -> isShowBackTop: true
    • isShowBackTop = -position.y > 1000
    • 在标签中写v-show=“true” 则显示
props: { 
      probeType: { 
      //这里是驼峰,在home.vue那边传值的时候需要用-分割probe-type
      type: Number,
      default: 0  
      //这里写01都可,效果一样,都是什么都不监听
}
this.scroll.on("scroll", position => {
      this.$emit("scroll", position);
});

  
Home.vue
<scroll class="content" ref="scroll" :probe-type="3" @scroll="contentScroll"  :pull-up-load="true">
 //这段代码目的是动态决定能否监听滚动位置
<back-top @click.native="backClick" v-show="isShowBackTop" />
 //这段代码上节讲的监听点击,回顶部。后面半段是位置小于多少就隐藏 
  
contentScroll(position){
     console.log(position); 
     //输出结果 {0,-1000}
     this.isShowBackTop =(-position.y)>1000
     //注意这个position.y是负值,这个判断会返回boolean值
    // 这是我写的多余this.isShowBackTop = -position.y > 1000 ? true : false;不简洁
}
  • 打印position值 {0,-1000},注意这个position.y是负值
  • <back-top @click.native=“backClick” v-show=“true”/>v-show属性,当你为true这个组件显示
  • 直接写true false太硬了,不行,需要搞个变量,把true和false放在变量中,改变量就能控制显示隐藏isShowBackTop

二十. 上拉加载更多

  • 有的界面不需要这个功能—上拉事件,所以scroll.vue中不写死,不要直接在new BScroll中写上pulllUpLoad:true。最好在scroll.vue中,把这个true定义个属性。由外部传来props, Boolean类型。 而且可以做个判断if(this.pullUpLoad),也可以不判断,又不会报错,瞎写的更严谨。监听滚动那里也可以加上if (this.probeType === 2 || this.probeType === 3)
pullUpLoad: this.pullUpLoad  
props:{
   pullUpLoad: {
      type: Boolean,
      default: false
 }
 mounted() {
    this.scroll = new BScroll(this.$refs.wrapper, {
      pullUpLoad: this.pullUpLoad
    });
    if(this.pullUpLoad){ this.scroll.on("pullingUp", () => {
      this.$emit("pullingUp");
    });
    }
}

 

<scroll class="content" ref="scroll":probe-type="3"@scroll="contentScroll”
:pull-up-load="true" @pullingUp=”loadMore”>
  • 这里发现父向子传参数时,probeType在这个标签中就要由驼峰变成写小写-,发现监听事件时pullingUp,事件可以用驼峰不用小写-
  • 要做上拉加载更多函数里面的事情了,但是肯定不会在scroll.vue文件中做,因为请求数据肯定在home.vue中,要把事件传出去。 this.scroll.on(“pullingUp”, () => {this.$emit(“pullingUp”);
  • 先监听什么时候滚到底部了,通过scroll对象监听滚到底部this.scroll.on(“pullingUp”, () => {this.$emit(“pullingUp”);
  • 滚动到底部后要做事情。监听事件滚动到底部后,把事件发出去
  • 在home.vue中监听事件,之后针对当前选中的类型加载更多数据。如果当前在新款这里,加载更多就是加载更多新款数据。
  • loadMore (){this.getHomeGoods(this.currentType);}加了this说明不是import的方法,而是自己写在methods中的方法
  • this.currentType记录当前选中的类型
  • scroll这东西只能上拉加载一次,数据由30变60,想要继续上拉加载更多,在数据加载完之后,就完成了上拉加载更多,如果还想再次上拉加载,则scroll要调用finishPullUp()
  • 在getHomeGoods方法(这个是网络请求的方法)中写,数据加载完成之后,调用这个scroll对象里面的方法this.$scroll.scroll.finishPullUp(),才能进行下一次上拉加载更多。但是一般不会搞这么长,一般在Scroll里面封装一个finishPullUp(){ this.scroll &&this.$refs.scroll.finishPullUp()}
  • 如果把这句代码放在loadMore中就不是同步任务了,只能放在网络请求完成的then里。
loadMore() {
      this.getHomeGoods(this.currentType);
    }
    
getHomeGoods(type) {
      const page = this.goods[type].page + 1;
      getHomeGoods(type, page).then(res => {
        this.goods[type].list.push(...res.data.list);
        this.goods[type].page += 1;

        this.$refs.scroll.finishPullUp();
      });
    }
  • 我在getHomeGoods的 getHomeGoods.then这里用this没问题,因为我用的是箭头函数,箭头函数的this指向会向上找,看一下 const page = this.goods[type].page + 1的this指向当前的组件对象,所以 getHomeGoods(type, page).then(res => { this.goods[type].list.push(…res.data.list);中的this也是指向组件对象

二十一. 解决首页中可滚动区域的问题

分析

在这里插入图片描述

  • 图片是异步加载的。之前计算item是没包含图片的,计算item出来,可滚动区域的高度是2000,设置死了。但是后来图片加载过来了,放在item中,每个item的高度被撑高了。所以最终可滚动区域变高了。但是他不会重新计算的,因为他不知道图片加载完了,依然设置2000。(图的高度不确定,所以有瀑布流效果)
  • 如果图片加载速度很快,在还没计算可滚动区域2000之前,图片就加载过来了,这时候算出来的高度就是正确的,那就没这个bug了
  • 原来没图片计算出来一个可滚动区域,后来我图片来了,他说原来你可滚动区域是2000啊,我只让你滚动2000。
  • 可滚动区域的是图四中间白色部分,这块区域就是scroll.vue了。根据内容决定多少可滚动区域,这个内容怎么决定?better-scroll自动遍历里面的子组件,把里面所有子组件的内容高度进行计算,里面有很多图片没加载出来确实高度只有这么高,但是加载完之后…
  • Scroll对象中有个属性叫scrollHeight记录了当前可滚动区域。
  • 写死肯定不行this.scroll. scrollHeight=10000不行,可能比这还大

Better-Scroll在决定有多少区域可以滚动时, 是根据scrollerHeight属性决定

  • scrollerHeight属性是根据放Better-Scroll的content中的子组件的高度
  • 但是我们的首页中, 刚开始在计算scrollerHeight属性时, 是没有将图片计算在内的
  • 所以, 计算出来的告诉是错误的(1300+)
  • 后来图片加载进来之后有了新的高度, 但是scrollerHeight属性并没有进行更新.
  • 所以滚动出现了问题

如何解决这个问题了?

  • 监听每一张图片是否异步加载完成, 只要有一张图片加载完成了, 执行一次refresh()
  • 如何监听图片加载完成了?(mouted不行,它是组件挂载,而渲染是在它之后的事情)
    • 原生的js监听图片: img.onload = function() {}
    • Vue中监听: @load=‘方法’
  • Window.οnlοad=function(){搞loading样式的时候经常用}
  • 我们监听图片加载完是监听哪些图片?就是那个商品图片啊。在GoodsListitem.vue中 <img :src=“goodsItem.show.img” @load=“imageLoad”/>
methods: {
    imageLoad() {
      console.log(“ddd”);//打印了30次,因为30张照片,之后要调用scroll的refresh
    }
 }
  • 调用scroll的refresh()
    • this.scroll.refresh()图片加载一次执行一次refresh,一旦执行refresh(),内部会根据最新的子组件重新计算可滚动区域高度。(刷新一次,重新计算高度)
  • 如何将GoodsListItem.vue中的事件传入到Home.vue中(因为之后要调用scroll的refresh方法,用到Scroll.vue中的scroll对象)
  • 因为涉及到非父子组件的通信, 所以这里我们选择了事件总线
  • 在GoodsListItem.vue中发射事件this.$bus.$emit(“itemImagLoad”); 一旦发射事件就是发射到事件总线那里了。
imageLoad() {
   this.$bus.$emit("itemImagLoad");
}
  • 之后可以在Home.vue的created中监听它发射出来的事件,也就是一旦组件创建完就开始监听图片加载完成,就等着你发射事件this.$bus. $on(“itemImagLoad”, () => {});
  • 默认情况下没$bus。怎么有$bus且让所有组件都能用$bus,原型链上加
  • 在main.js中Vue.prototype.$bus = new Vue()。Vue实例可以作为事件总线,能发射事件。 $bus就是个Vue实例,可以用new出来的vue实例发射事件,并且用new出来的vue实例监听事件
  • 其实是自己设计一个vue实例进行事件监听的思路和方法

21.1. 非父子组件的通信方法总结

在这里插入图片描述

  1. 通过子传父把事件传给GoodsList.vue,之后让这个组件把事件传给Home.vue(麻烦)

  2. 搞个vuex对象,vuex记录了一些状态,每一次一旦发生这个事件,也就是图片加载完成的话,就改变vuex里面的某一个属性,再让首页里面引用这个vuex属性,并且实时监听vuex属性的改变,一旦属性发生改变就执行this.$refs.scroll.scroll.refresh.--------(vuex可以保存整个应用程序的状态 GoodsListItem.vue 通过this.$store就可以改vue里面的属性了,home.vue监听这个属性。)这个方法无论多少层都能通信。

  3. 事件总线。公共的东西。事件总线和vuex很像,但不是管理状态的,是管理事件的。
    bus ->总线

    • Vue.prototype.$bus = new Vue()
    • this.$bus.$emit(‘事件名称’, 参数)
    • this.$bus.$on(‘事件名称’, 回调函数(参数))

问题一: refresh找不到的问题

  • 第一: 在Scroll.vue中, 调用this.scroll的方法之前, 判断this.scroll对象是否有值
  • 第二: 在mounted生命周期函数中使用 this.$refs.scroll而不是created中
  • -----------在vue项目中运用----------

  • 这个调用refresh时报错了.
    难道是因为我们准备执行refresh时home.vue中scroll组件还没挂载,所以拿不到scroll元素。难道是因为子组件挂载完成之后父组件才会挂载。难道是因为官方说$refs只会在组件渲染完成之后生效,并且他们不是响应式的,仅作为一个用于直接操作子组件的逃生舱。

  • 组件一旦创建created就发送网络请求了,图片来的非常快,图片加载完也快,而scroll在mounted中还没new初始化呢,你在home.vue中就想用了,拿到的就是null this.$refs.scroll.refresh();调用null的refresh肯定不行。就是还没new出来scroll呢,你就急着调用scroll的方法肯定不行

  • 解决办法:在调用scroll之前先判断有没有这个值,如果有值才会去做后面的事情。

  • 逻辑&&,先判断1是否正确,一旦前面为false,&&后面的东西就不继续执行了

  • scroll.vue中scrollTo(x, y, time = 300) {this.scroll.scrollTo(x, y, time);}改成
    scrollTo(x, y, time = 300) {
    this.scroll &&this.scroll.scrollTo(x, y, time);}

  • 箭头函数this指向的是他父级作用域的this。就是你自己看看在created函数里,你的兄弟this指向哪,箭头函数this就指向哪。

created() {
  this.$bus.$on("itemImagLoad", () => {     
  this.$refs.scroll.refresh();
  }
}
  • 在create查询元素this.$refs…或者通过document.query查询到的东西好多时候都是空的
this.$bus.$on("itemImagLoad", () => {
     this.$refs.scroll.refresh();
   });
  • 所以老师把这段代码放在mounted中了,但是这样放在mounted中,一直切换下面的tabber,还是会出错
  • 我自己认为的解决办法是把这个写在mounted中再加上了this.$refs.scroll &&为了先判断有没有取到
 this.$bus.$on("itemImagLoad", () => {
      this.$refs.scroll && this.$refs.scroll.refresh();
    });
  • 即Home.vue和Scroll.vue中 都加上这个判断this.$refs.scroll&& 和this.scroll&&

问题二: 对于refresh非常频繁的问题, 进行防抖操作

21.2. 防抖函数

  • 防抖debounce/节流throttle(之后研究一下)
  • 防抖函数起作用的过程:
    • 如果我们直接执行refresh, 那么refresh函数会被执行30次.
    • 可以将refresh函数传入到debounce函数中, 生成一个新的函数.
    • 之后在调用非常频繁的时候, 就使用新生成的函数.
    • 而新生成的函数, 并不会非常频繁的调用, 如果下一次执行来的非常快, 那么会将上一次取消掉

      debounce(func, delay) {
        let timer = null
        return function (...args) {
          if (timer) clearTimeout(timer)
          timer = setTimeout(() => {
            func.apply(this, args)
          }, delay)
        }
      },
  • ----------在vue项目中运用----------

  • 刷新频繁解决办法

    • refresh()调用的很频繁,加载一个图片调一次。我们希望如果1s内多张图片更新了,需要多次调用refresh()了,我们就等待一起来发生一次刷新。而不是只要有一张图片就发生一次刷新。
    • 真实项目需求:搜索中有个输入框,搜yifu,根据yifu关键词向服务器发生请求,展示搜索结果,但你输入y就去服务器请求相关进而展示肯定不行,监听这个输入框的改变 用改变事件@change,执行后面方法,只有value变化就向服务器发送请求,但这肯定不行,服务器压力很大。对这个东西做个防抖动的操作。我等500ms看他有没有继续输东西,本来一改变了就该发请求的,但我先不发,等一会如果新输入了,把上一次准备发的请求取消掉,只让在最后一次这里发送一次请求
  • 怎么进行防抖?

    • 封装一个函数。 在this.$refs.scroll.refresh();这个操作之前,对refresh搞一个定时器,让他在定时器前等一会,先不要进行下一步操作。单位事件内没有下一步操作,再把这个函数refresh在内部做个执行
  • 这不关流量的事,只要一个函数执行的太频繁,都可以用防抖函数处理,让他调用的没那么频繁。

  • 箭头函数的this指向环境中的this,箭头函数this指向最近的作用域,匿名函数的this才指向window。

  • func待会准备执行的函数,准备对这个函数进行防抖操作。把函数本身传进来。这个func传的时候传函数,不要加(),加()传的是返回值,不加()传的是函数。

  • delay给个时间,待会准备等多久。(确定这个函数单位事件等多久)好像不带单位,默认是毫秒。

 debounce(func, delay) {
      let timer = null;//timer记录有没有定时器。//这段只执行一次,之后再执行就是返回值里的函数
      return function(...args) {//这个 debounce函数本身又返回一个新的函数
        if (timer) clearTimeout(timer);//我如果发现你又进来下一次,我就把之前你准备做事情的timer清除掉
        timer = setTimeout(() => {//清除之后在这里重新计时
          func.apply(this, args);//因为是箭头函数指向最近的作用域,指向func。匿名函数才指向window
        }, delay);//如果在这个时间内,没有把这个timer取消掉,就可以执行这里面的了
      };
  }
  • 这个timer一直没有销毁,timer是个局部变量因为在函数里面。为什么timer没被销毁,因为下面是个函数是个闭包,这个闭包对外层的timer做了个引用,所以一旦有引用指向timer的时候,他就不用销毁了。
  • 在methods中封装个函数–防抖函数。是个闭包。
之前是
 mounted() {
    this.$bus.$on("itemImagLoad", () => {
      this.$refs.scroll && this.$refs.scroll.refresh();
      console.log(22);
    });
  }
之后是
mounted() {
    const refresh = this.debounce(this.$refs.scroll && this.$refs.scroll.refresh, 500);
    this.$bus.$on("itemImagLoad", () => {
      refresh();
    });
  }

最后一次延时时间到了则执行
func 即this.$refs.scroll && this.$refs.scroll.refresh.apply(this, args)

单线程,原来的函数处理完之后,才有可能去执行setTimeout里面的代码
  • 上面这个 const refresh是个局部变量,但他不会被销毁,因为他有个引用refresh(),下面这个包含refresh()的箭头函数是个闭包,闭包对上面的refresh变量有个引用
  • func 是$refs.scroll && this. $refs.scroll.refresh();
  • Const refresh= debounce(this.$refs.scroll.refresh, 500)
  • refresh()就相当于调用return的这个函数,相当于const refresh= function(…args) {} 如果refresh(‘111’)相当于这个args是111。
  • 写…是因为可以传多个参数refresh(‘111’,‘222’,‘333’),如果不写…意味着只能传一个参数。写了这个…args里的args就是多个参数了,就能传入apply里面args里面了
  • 怎么验证这个refresh最后只调用了一次,去到scroll.vue的refresh函数里console.log(’—’)看一下输出几次
  • -------------可以不给定时器传delay值-------------
  • 发现有个厉害的事情,就是即使不给定时器传delay值,加载30张照片也就刷新了15次,而不是30次,按理说没有定时器应该是30张图片加载一次就刷新一次啊。就是因为setTimeout本身有延迟时间,本身的延迟时间内部还没来的的时候,上面又执行了下一次refresh(),就把上次的给取消掉了。
  • 原因是因为setTimeout里面的异步函数(这里指刷新),是在我们下一次事件循环的时候才会执行的。这个牵扯到了js的执行顺序。这里定时器是宏任务也就是异步的,需要等同步代码走完。
    单线程,原来的函数处理完之后,才有可能去执行setTimeout里面的代码
    时间轮询机制。event loop。事件队列。执行栈。异步队列。js异步编程:单线程+事件 队列
setTimeout(()=>{
   (本例是刷新)
})
  • 一般来说setTimeout里面的函数是异步函数,这个异步函数在我们下一次事件循环的时候才会执行的。
    setTimeout里面的代码永远会放到最后执行,即()=>{}这个函数永远放到后面执行。
    等到整个事件循环真正闲下来的时候才会去执行这个函数()=>{},虽然没写延迟时间,但是这个函数也会放到后面执行
  • 定时器是异步操作,所以会被拎出来放到事件队列里,等待执行栈执行完以后再执行。
    先执行完同步代码,再执行异步
    延时器里面的代码都会被延迟到程序空闲时执行
    setTimeout里的代码即使没有延时时间也会等一会再执行的
    就是等栈空了之后再将setTimeout回调弹回去
console.log('aaa')
setTimeout(()=>{
   console.log('bbb')
})
console.log('ccc')
aaa ccc bbb
别看我没写时间,我连0都没写,依然最后执行
  • ---------------防抖函数封装---------
  • 防抖函数,在其他组件也能用到,所以最好写在单独的文件。在common文件夹建个utils.js(工具)。export function 把函数导出,之后把这个debounce函数复制到这个utils.js文件。导出之后,其他地方可以直接导入,之后其他地方都可以用这个东西了
export  function debounce(func, delay) {
      let timer = null;
      return function(...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
          func.apply(this, args);
        }, delay);
      };
  }
  • 之后在home.vue中import { debounce } from “common/utils”
  • 去vue.config.js中看一下我们有没有配置别名’common’: '@/common’配置过别名
  • 因为配置过别名,所以不是这种import {debounce} from "…/…/common/utils"而是这种import { debounce } from “common/utils”;
  • 导入组件import BackTop from “components/content/backtop/BackTop”;
  • 导入function函数 import { getHomeMultidata, getHomeGoods } from “network/home”;这是home.js导出的export function getHomeGoods(type, page){}
  • 使用外面导入的这个函数直接调用debounce(…),把this删掉。this是调用组件内methods里面写的方法的

在methods调用防抖函数没效果

  • 有人说把调用防抖函数写在方法里没效果,因为是函数里面imageLoad() ,你每次进来相当于生成新的refresh,就不是之前那个,跟之前的没有关系的,就达不到防抖的效果。
  • methods中你想要达到防抖效果最重要的一点是把变量refresh保存到data中,不要只写在函数中用完就销毁。
  • 这里不是闭包,他们在同一个函数中,因为着每次调用这个函数,都会生成一个新的
<detail-goods-info :detail-info="detailInfo" @imageLoad="imageLoad" />

methods: {
    imageLoad() {
      let refresh = debounce(this.$refs.scroll && this.$refs.scroll.refresh,50);
      refresh();
    }
  }
  • 另一个方法这样能达到效果也不需要在data中保存变量是因为:在mounted中监听事件总线this.$bus.$on(“itemImagLoad”,() => { refresh(); })
  • 这里可以是因为这个闭包 () => { refresh(); }在引用外面的变量,在闭包里面每次引用的都是同一个变量
 mounted() {
        const refresh = debounce(
            this.$refs.scroll && this.$refs.scroll.refresh,
            50
        );
        this.itemImgListener = () => { refresh(); }
        this.$bus.$on("itemImagLoad", this.itemImgListener);//取消的时候这个函数也要传进去
    },

refresh两种调用时间对比

  • 这个是在Home.vue请求数据后,向GoodListItem.vue传去数据,它接收到数据后,图片会自动加载,我则监听图片加载,并把事件传给Home.vue,监听到时间刷新一张,refresh一次,重新计算高度一次。

  • 之前的做法没那么好,之前是在实现下拉加载更多时监听下拉到底部,监听到后执行loadmore()函数,在函数中调用方法请求下一页数据后,请求之后,实现refresh刷新。
    getHomeGoods就是发请求图片的异步方法,把refresh方法应该放到getHomeGoods方法里面去调。

二十二. tabControl的吸顶效果

  • 之前的粘性定位失效了,可以删掉了Home.vue中。粘性定位positon:sticky有兼容问题,既然如此就不用了。

22.1. 获取到tabControl的offsetTop

  • 关于offsetTop的理解

    1. offsetTop:元素到offsetParent顶部的距离
    2. offsetParent:距离元素最近的一个具有定位的祖宗元素(relative,absolute,fixed),若祖宗都不符合条件,offsetParent为body。
    3. 在本案例中<scroll>元素是绝对定位,这整个home是相对定位,所以offsetTop是相对于<scroll>顶部来说的。
  • 必须知道滚动到多少时, 开始有吸顶效果, 这个时候就需要获取tabControl的offsetTop获取到这个值之后保存起来,再和滚动的区域作对比(比如滚动的区域>500了就吸顶positon:fixed)

  • data中自定义这个值tabOffsetTop:0(默认值是0,之后会给个具体值)

  • 之后在哪里给tabOffsetTop赋值呢?在mounted()中赋值

    • 首先我们肯定要拿到tabControl,才能获取tabControl的offsetTop,所以给tabControl标签加个ref.<tab-control class=“tab-control” :titles="[‘流行’,‘新款’,‘精选’]" @tabClick=“tabClick” ref=“tabControl”/>
    • 如果在created()中想拿到tabControl,this.$refs.tabControl拿到的很有可能是undefined,所以==不在created()==中拿
  • 但是, 如果直接在mounted中获取tabControl的offsetTop, 那么值是不正确.并不是在mounted中获取

    • this.tabOffsetTop=this.$refs.tabControl.offsetTop这样根本就不对
    • 如果是普通div,直接拿到div,拿到offsetTop即可
    • 因为this.$refs.tabControl拿到的是个组件对象tabControl,不是个普通的div。组件没有offsetTop属性(不信可以试一下),我们应该拿组件对应的元素,即tabControl.vue里面<template>里面的元素。因为只有元素才有offsetTop属性
    • 所有的组件都有一个属性$el:用于获取组件中的元素
    • 是元素了就有offsetTop属性了,this.$refs.tabControl.$el.offsetTop
    • 但是这个拿到的值是不对的,因为这个写在mounted()中。mounted就是挂载的意思,挂载意味着我们所有组件都挂载到这个上面了,但是图片不一定加载完。可能挂载的时候上面下面那里只有一个普通的高度,图片还没将高度撑起来。
    • 只有等图片全部加载外之后,再去拿这个offsetTop才是正确的
  • 如何获取正确的值了?

    • 监听HomeSwiper中img的加载完成.
    • 加载完成后, 发出事件, 在Home.vue中, 获取正确的offsetTop值.
    • 补充:
      • 为了不让HomeSwiper多次发出事件,
      • 可以使用isLoad的变量进行状态的记录.
    • 注意: 这里不进行多次调用和debounce的区别

  • 先获取轮播图图片有没有加载完,之后获取本周流行图片有没有加载完,等这两个图片都加载完,就来计算最终的offsetTop。中间的四个小图片没计算在内,因为感觉小图片加载速度快,一般都是大图片影响。测试发现本周流行图片几乎不影响,它的加载速度非常快。所以我们主要是看轮播图的图片什么时候加载完。
  • 在HomeSwiper.vue中<img>中监听图片加载完
HomeSwiper.vue
<img :src="item.image" @load="imageLoad" />

methods:{
    imageLoad(){
      console.log(1); 
       this.$emit('swiperImageLoad')
    }
  }
  • 会打印四次1,因为四张图片。难道又用到防抖函数。
Home.vue 
<home-swiper :banners="banners" @swiperImageLoad="swiperImageLoad"/>

methods: {
  swiperImageLoad() {
    console.log(this.$refs.tabControl.$el.offsetTop);
    this.tabOffsetTop = this.$refs.tabControl.$el.offsetTop;
  }
}
  • 这里打印四个一样的数字614,其实内部来说HomeSwiper.vue中事件只需要发一次就行,因为对于获取高度来说, 我有一个图片就能获取高度了。
  • isLoad:false这个用法绝了,不进行多次调用(这就是节流)
HomeSwiper.vue
data(){
    return {
      isLoad:false//默认值是false
    }
  }

methods: {
    imageLoad() {
      if (!this.isLoad) {
        this.$emit("swiperImageLoad");
        this.isLoad = true;
        //如果没有加载过,肯定是false,做个取反
        //之后再有图片加载完也没有用,这个东西只会执行一次,之后进不去if了
      }
    }
  }
  • 比如输入框输入y,输入框这里使用防抖的目的是让我们稍微等会,等我们输入完yifu再做发请求这个事情。
  • 而这里没必要用防抖,因为这里相当于,我输入完y之后你就给我计算出来结果就可以了,输入之后的ifu了这些文字,压根就不让它发出事件。这里只要第一次这个东西,我拿到的就已经是个确切的结果了,后面的东西事件没必要发出去
  • 防抖就是输入完之后等一会y,等一会看后面还有没有东西yifu

22.2. 监听滚动, 动态的改变tabControl的样式

  • 之前已经监听过滚动了(为了隐藏显示回顶部按钮),能得到滚动的位置了。contentScroll函数。
  • 还可以在这个函数中,判断决定tabCotrol是否吸顶(是否给他一个position:fixed)
  • 是否有这个属性(吸顶)的话,我们可以这样做,在data中给个变量isTabFixed:false(默认情况下不吸顶,所以默认值设置个false)
  • >吸顶,<不吸顶
  • 之后再根据isTabFixed这个变量,动态的决定有没有position:fixed这个属性
  • 只需要<tab-control>动态的绑定样式:class。因为true false,所以要用对象语法:class="{fixed:isTabFixed}"当变量isTabFixed为true时,有fixed这个class样式
Home.vue
<scroll class="content" ref="scroll"  :probe-type="3" @scroll="contentScroll"  :pull-up-load="true" @pullingUp="loadMore">

<tab-control  class="tab-control" :titles="['流行','新款','精选']" @tabClick="tabClick"ref="tabControl" :class="{fixed:isTabFixed}"/>

<back-top @click.native="backClick" v-show="isShowBackTop" />

methods: {
  contentScroll(position) {
     // 1.判断BackTop是否显示
     this.isShowBackTop = -position.y > 1000;

     // 2.决定tabCotrol是否吸顶(position:fixed)
     this.isTabFixed = -position.y > this.tabOffsetTop;
   },
}

.fixed {
   position: fixed;
   left: 0;
   right: 0;
   top: 44px;
}
  • position:fixed用不了

    • 为实现局部滚动,better-scroll组件往<div class=“content”>即第二层 里面放了很多属性如tranfrom,better-scroll内部实现原理是通过tranfrom:translate(0px,-567px)的改变来实现滚动问题的。
    • <scroll>标签里<tab-control>标签有fixed,translate的改变也会把fixed的位置改变。到时候一起改变。才不管你有没有固定呢。
    • 你可以试一下,把某个东西fixed一下,你改一下tranfrom:translate(0px,-567px),本来你已经固定了,你改变这个translate照样会随着滚动的。
    • position:fixed用不了,所以上面的一部分代码要删掉了,position:fixed没效果。.fixed样式删掉,<tab-control class=“tab-control” :titles="[‘流行’,‘新款’,‘精选’]" @tabClick=“tabClick"ref=“tabControl” :class=”{fixed:isTabFixed}"/>这个:class="{fixed:isTabFixed}"类删除掉。关于滚动区域>offsetTop则设置变量为true那部分没删。
  • 问题:动态的改变tabControl的样式时, 会出现两个问题:

    • 问题一: 下面的商品内容, 会突然上移 (因为原来的布局里tabControl是占有位置的,fixed之后,脱标了,相当于没有了,后面东西就顶上来了)
    • 问题二: tabControl虽然设置了fixed, 但是也随着Better-Scroll一起滚上去了.
  • 其他方案来解决停留问题.

    • 在最上面, 多复制了一份PlaceHolderTabControl组件对象, 利用它来实现停留效果.
    • 当用户滚动到一定位置时, PlaceHolderTabControl显示出来.
    • 当用户滚动没有达到一定位置时, PlaceHolderTabControl隐藏起来.
  • 就是复制一份<tab-control :titles="[‘流行’,‘新款’,‘精选’]" @tabClick=“tabClick” ref=“tabControl” /> 放到滚动区域以外,平时隐藏起来。 放到导航栏标签下面,没放到scroll标签内。

  • 但是打开浏览器,发现复制的这一份被导航栏盖上了。因为导航这里做了fixed,脱标了。而scroll中间内容区那里是position:absolute top:44 bottome:49。所以复制的<tab-control>就跑到上边去了。导航栏里面。

  • 其实导航这里没必要让它脱标,因为除了导航和最下面tabbar之外,中间的内容区的滚动都是通过better-scroll做的局部滚动。局部滚动的时候,对其他区域根本不会造成任何影响。

  • 导航这里没必要让它脱标,因为滚动的时候,只是在中间区域滚动的,设置better-scroll的时候就是设置的中间这个区域,所以滚动区域就是这个区域。

  • 之前之所以设置导航是fixed,是因为用的原生的滚动,相当于整个网页在滚动,不设置fixed,导航会随着网页一起滚动。

  • 所以解决方法1,删掉导航栏的样式position:fixed(最后用这个)

  • 解决方法2,给<tab-control>标签设置margin-top

  • 设置之后复制的<tab-control>标签会藏在scroll内容的下面,因为scroll内容是absolute

  • 所以这是我们给后复制的<tab-control>设置个class<tab-control :titles="[‘流行’,‘新款’,‘精选’]" @tabClick=“tabClick” ref=“tabControl” class=“tab-control”/>而scroll里面的<tab-control>标签没设置class

  • 给复制的这个<tab-control>设置position:relative;z-index:9;

    • 这样写之后就能不让复制的<tab-control>标签隐藏在scroll内容后面了,就能改变层级关系了。
    • 因为这个定位之后,设置top,left都是相对于原来位置的。如果你不设置top,left,那么,她会一直在这,也不会随着你滚动(那是肯定的啊,复制的<tab-control>标签又没写在scroll里面,才不会随着滚动,本来我就是局部滚动啊)。
  • 给复制的<tab-control>设置v-show=“false”,一旦滚到具体的位置显示出来,之前已经设置了变量isTabFixed(之前这个变量用于决定tabControl要不要fixed,现在决定要不要显示就可以了)进行保存false和true属性。即v-show=“isTabFixed”

  • 原生的滚动可以用positon:sticky。还可以使用position:fixed。但在better-scroll中,这两种都不好用。

  • 这里原来的tabControl滚上去了,复制的显示隐藏

  • 出问题了,复制的<tab-control>和原来的<tab-control>新款流行等状态不能保持一致

    • 手动设置两个选中currentIndex
    • 拿到<tab-control>,<tab-control>里面有个currentIndex记录谁被选中了。但是两个<tab-control>的ref都一样,就没办法确定的设置了,所以我们给复制的<tab-control>的ref改一下,ref=“tabControl1”,原来的<tab-control>的ref改一下,ref=“tabControl2”,同样的 swiperImageLoad() 中改为this.$refs.tabControl2
methods:{
  swiperImageLoad() {
     this.tabOffsetTop = this.$refs.tabControl2.$el.offsetTop;
   },
}
  • 之后,我们在你监听点击的tabClick方法中<tab-control :titles="[‘流行’,‘新款’,‘精选’]" @tabClick=“tabClick” ref=“tabControl2” />
methods: {
   // 事件监听相关的方法
   tabClick(index) {
     switch (index) {
       case 0:
         this.currentType = "pop";
         break;
       case 1:
         this.currentType = "new";
         break;
       case 2:
         this.currentType = "sell";
         break;
     }
     this.$refs.tabControl1.currentIndex = index;
     this.$refs.tabControl2.currentIndex = index;
   }
  • index就是最新点击的,因为不能保证用户点击的是第一个还是第二个tabControl进行切换的。两个都设置了,他们的状态就能保持一致了,即让他们的状态都和最新点击的index保持一致。

二十三. 让Home保持原来的状态

  • 在首页切到购物车,再回到首页,重新开始了。
  • 路由管理对象。
  • 离开首页会销毁,会调用销毁函数,destroyed(){},再进去首页会重新创建。
  • 注意周期函数不是写在methods里面的,是写在外面

23.1. 让Home不要随意销毁掉

  • keep-alive
  • 在App.vue中用<keey-alive>包住<router-view>
  • <keey-alive><router-view><\keey-alive>
  • 包住之后首页没被销毁,但是切换购物车,再回去,发现首页没停在原来的位置。(目前已经不存在这个问题了,包住之后就能停在原来的位置了
  • 首页没被销毁,但是没有保留之前的状态,即Home中的内容没保持原来的位置
  • 这是better-scroll内部的问题

23.2. 让Home中的内容保持原来的位置

  • 离开时, 保存一个位置信息saveY.
  • 进来时, 将位置设置为原来保存的位置saveY信息即可.
    • 注意: 最好回来时, 进行一次refresh()

  • 在这里可以用这两个生命周期函数activated和deactivated
  • 没有keep-alive 不能用activated/deactivated这两个函数。这两个函数,只有该组件被保持了状态,使用了keep-alive时,才是有效的。
  • 可以离开的时候在activated周期函数中,把data中定义的变量saveY,保存到data中。data(){saveY:0},默认情况下是0。
  • 注意这个saveY一般情况下是负值, this.saveY = -100;还没单位(就是那个position)
  • 为了确定离开时的y值,可以在Home.vue中那个监听滚动位置的方法里,设置this.saveY=position.y,但这样保存的次数有点多了,没必要,只需要离开时哪一个位置。
  • 其实better-scroll里有个属性:this.scroll.y
  • activated() {
    this. $refs.scroll.scrollTo(0, this.saveY, 0);
    //有人说这个时间设置为0,可能会出问题。
    this.$refs.scroll.refresh();
    //滚到这里最好刷新一下,不然容易出问题,滚不动问题,一下跳到顶部问题。(有同学说先刷新能解决问题,学生说的,建议那这个放到定位位置的上面)
    },
    deactivated() {
    this.saveY = this.$refs.scroll.scroll.y;
    //这里为啥不用position,因为positon是监听得到位置信息,这里是设置位置
    },
  • 可以再封装一层
getScrollY() {
      return this.scroll ? this.scroll.y : 0;
    }


Scroll.vue
getScrolly() {
      return this.scroll ? this.scroll.y : 0;
      //这里的目的是为了判断this.scroll有没有值,怕调用这个方法时,scroll还没new出来。有值返回值,没值返回0
    }
 Home.vue
activated() {
    this.$refs.scroll.refresh();//事件证明写在这比较好
    this.$refs.scroll.scrollTo(0, this.saveY, 0);//实践证明写0不好,至少写个1毫秒 1
    //this.$refs.scroll.refresh();
  },
deactivated() {
    this.saveY = this.$refs.scroll.getScrollY();
  }

一. 跳转到详情页并且携带id

  • 一个路由对应一个组件,这里地址是detail/,我们建个detail组件
  • 点击商品获取商品id,根据id请求对应的数据,展示。
  • 准备做详情页,首先肯定先跳转到详情页,点击某个商品跳转到详情页,点击某个商品,一个商品就是一个<good-list-item>
  • 监听<good-list-item>点击 ,GoodListIitem.vue中<div class=“goods-item” @click=“itemClick”>这个标签里面包裹了img-span
  • 验证一下能不能监听到点击i,GoodListIitem.vue中temClick() {console.log(1);}
  • 准备跳转到详情页,如何跳转?给详情页配置一个路由,配置路由之后,一旦进行跳转就是路由之间的跳转
  • 所以我觉得详情页也属于我们大的views里的一个模块,所以我们在views中建个detail文件夹。之前只有四个大的视图,现在有五个大视图了。在detail文件夹建个Detail.vue组件
  • 这个组件并没有配置对应路由映射关系。来到index.js
const Detail = () => import('../views/datail/Detail')


{
        path: '/detail',
        component: Detail
    }
  • GoodListIitem.vue中
    itemClick() {
    this.$router.push("/detail");
    }
    用push,到时候能返回,到时候在详情页导航栏搞个返回按钮,用replace不能返回用push的话,到时候只要调用back就能返回。(浏览器中好像都能返回的)
  • 点击跳转到到详情页需要传递一些参数,点击的商品id给我,根据id请求详细数据
  • 路由跳转传递参数的方式有两种:
    • 动态路由的方式: 就是在home.js页由{path:’/detail’,component:Detail}变为 {path:’/detail/:iid’,component:Detail}
      GoodsListItem.vue中
      this.$router.push("/detail/"+1111)
      地址栏变成http://localhost:8080/detail/111
      本案例是 this.$router.push("/detail/" + this.goodsItem.iid);
      在detail.vue中获取这个id {{$router.params.iid}}

    • query方式:
      GoodsListItem.vue中
      itemClick() {
      //this.$router.push("/detail");
      this.$router.push({
      path:"/detail",
      query:{iid:this.this.goodsItem.iid}

      })
      }
      在detail.vue中获取这个id {{$router.query.iid}}

  • 在详情页拿到iid,然后向服务器端发送网络请求,请求数据
  • 搞个变量存一下取出来的iid,所以Detail.vue组件中data(){return iid:null}
  • 在Detail.vue的created函数中
    created() {
    console.log(this.$route.params);
    this.iid = this.$route.params.iid;
    }
  • 在上面写 {{iid}} 看一下那到没

二. 详情页导航栏的封装

  • Detail.vue中写有个根<div id=“detail”></div>

  • 导入NacBar 注册 使用标签<nav-bar>

  • 使用里面的具名插槽 <div slot=“center”>详情页</div> 即<nav-bar><div slot=“center”>详情页</div><\nav-bar>

  • 不行,发现这个导航还是有点复杂的,不是你直接使用组件就能简单实现的,所以在detail文件夹中建个childComps文件夹建个DetailNavBar.vue在这里实现

  • 第三步第四步那些步骤都写在DetailNavBar.vue中实现,注意搞个根<div></div>

  • 在Detail.vue中导入DetailNacBar 注册 使用标签<detail-nav-bar/> 注意:这里标签不加/会报错

  • DetailNavBar.vue
    <nav-bar>
    <div slot=“center”>导航</div>
    </nav-bar>

  • 中间是标题的格式,直接把标题写出来做个遍历即可。

  • 直接把这几个标题在DetailNavBar.vue中的data中
    data() {
    return {
    titles: [“商品”, “参数”, “评论”, “推荐”]
    };
    }

  • DetailNavBar.vue
    <nav-bar>
    <div slot=“center”>
    <div v-for="(item,index) in titles" :key=“index”>{{item}}</div>
    </div>
    </nav-bar>

  • 是垂直分布的,要布局一下。给<div slot=“center”>加个类class=“title” 在这里设置flex布局display:flex,就一行显示了,但是很挤

  • 想让他们均等分,给<div v-for="(item,index) in titles" :key=“index”>{{item}}</div>加个类title-item 在这里设置flex:1

  • 这个标题文字是可点击的,点击谁谁有点击效果,下面文字变颜色

  • data中定义currentIndex: 0 默认0

  • 直接把这个判断写在标签中<div v-for="(item,index) in titles" :key=“index” class=“title-item” @click=“titleClick (index):class="{active:index===currentIndex}">

  • .active {
    color: var(–color-high-text);
    }

  • 左侧返回小按钮实现<div class=“left”></div>,里面放一个img标签放图片,或者直接&lt;实现后退图片<,或者用css画三角形大于小于,或者用字体图标。

  • <div slot=“left”><img src="~assets/img/common/back.svg" alt class=“back” /></div>

  • 位置不对,所以加个class=“back”调位置.back {margin-top: 10px;}

  • 这个要监听点击返回首页

  • 监听img外层的div,这样即使点到图片附近也能返回

  • <div slot=“left” @click=“backClick”>

  • 你准备返回上一页,两个方法

    • this.$router.go(-1)
    • this.$router.back()

三. 数据请求及轮播图展示

3.1. 数据请求

  • 根据id请求数据Detail.vue
  • 直接在created请求不好,请求做个封装,之前首页的几个请求都封装了
  • netWork文件夹-detail.js
  • 有参数,所以把iid传到这个detail.js中的函数中
import { request } from './request'

export function getDetail(iid) {
    return request({
        url: '/detail',
        params: {
            iid
        }
  • Detail.vue中导入,在created中直接使用函数就可以
created() {
    this.iid = this.$route.params.iid;

    getDetail(this.iid).then(res => {
      this.topImages = res.result.itemInfo.topImages;
    });
  }

3.2. 轮播图展示

  • 把请求来数据分离开,在data中定义topImages:[] 存轮播图
  • 拿到轮播图数据,展示轮播图数据
  • 在detail文件-chileComps文件夹-建个DetailSwiper.vue
    props: {
    topImage: {
    type: Array,
    default() {
    return [];//为了严谨,搞个default值
    }
  • Detail.vue中导入注册使用组件,传数据进子组件DetailSwiper.vue <detail-swiper :top-images=“topImages” />注意看这里传数据时不能用驼峰,用-隔开
  • 拿到数据后用轮播图进行展示,在DetailSwiper.vue中导入Swiper和SwiperItem,注册,使用,进而展示数据import { Swiper, SwiperItem } from "components/common /swiper ";
  • 这样导入的原因是在swiper文件夹有个index.js把这两个文件统一导出了
import Swiper from './Swiper'
import SwiperItem from './SwiperItem'

export {
  Swiper, SwiperItem
}
  • DetailSwiper.vue这里可以不加根div,因为这里正常使用时的有个标签能把全部的包住,也有根的功能,不需要多次一举。
<template>
  <swiper class="detail-swiper">
    <swiper-item v-for="(item,index) in topImages" :key="index">
      <img :src="item" alt />
    </swiper-item>
  </swiper>
</template>
  • 轮播图太大了
    • 内部标签<swiper-item>加个class,给它做个截取。即设置固定高度height:300px,overflow:hidden
    • 我们还可以给外部标签<swiper>加class,设置固定高度height:300px overflow:hidden。因为底部圈圈在定位的时候是相对于整个<swiper>。
    • 发现两中都一样,其实都能实现
  • 发现 一个问题,首页随便点一张图进去都是同一个详情页。同一个人。明明id不一样啊。为啥数据没变,还展示一样的东西。
    • 因为:detail页面也keep-alive了,所以不会再次执行desturyed和created,只会执行一次。 而获取地址栏iid的代码也写在created中 this.iid = this.$route.params.iid;由iid获取信息的请求也写在created中,只执行一次。
    • 我希望每点进详情都是一个新的,跟之前的首页那种不一样。详情这里我希望每次都建一个新的组件的。
  • detail不要让它keep-alive。把Detail.vue里面的name这个东西填进exclude=“”这里面 name就是用在这里的。用exclude需要组件对应的name值
App.vue
<keep-alive exclude="Detail">
  <router-view />
</keep-alive>
  • 轮播图不滚是图片没加载完,可以自己监听图片加载,然后再轮播。

  • 轮播图里swiper的setTimeout时间设置长一点,之前是100ms

四. 商品基本信息展示

  • 先获取这部分数据
  • 看一下数据都在各个地方,如果子组件这样接收数据 props:属性1 属性2 属性3 肯定是太麻烦了
  • 想想要把商品信息的基本展示封装成一各独立的组件。
  • 首先从首页里面把我们组件需要用到的数据先抽离出去,即抽离组件需要的数据。之后把数据传给组件,直接展示
  • res->组件 肯定不行很麻烦
  • 抽离组件需要的数据->组件
  • 可以搞个对象,往对象里填好这些数据,然后将对象传给组件.(搞个自定义对象存数据)
  • 在给组件传数据的时候,尽量把数据整合好(把杂乱无章的数据整合成一个对象),之后只把这一个对象给组件传过去。
  es5定义clss
function Person() {
    
}
  es6定义class
class CoodsInfo{
    
}

里面有个最重要的东西叫构造函数,构造函数就是在你new的时候
class Person{//表示创建了个person类
    constructor(name,age){
      this.name=name;
      this.age=age
}
const p=new Person('why',18)
  • 创建了个person类之后,你就可以new了,即来创建Person
    如果你需要在这里传一些参数的话,需要在上面搞一个构造函数 constructor,到时候这个p对象就有name属性和age属性

  • 三个地方的数据整合到一个对象

network-detail.js
export class Goods {
    constructor(itemInfo, columns, shopInfo) {//在我们创建class的同时,要求你给我传一些东西
        this.title = itemInfo.title;
        this.price = itemInfo.price;
        this.oldPrice = itemInfo.oldPrice;
        this.lowNowPrice = itemInfo.lowNowPrice;
        this.discountDesc = itemInfo.discountDesc;
        this.columns = columns;
        this.services = shopInfo.services;
    }
}
  • 之后,const g=new Goods(传入参数) 则可通过g.title拿到属性值,到时候把这一个对象g给组件传过去,让组件面向这一个对象开发即可。
  • 把服务器返回的数据封装到一个类里面,再来创建对应类的对象。把对象传到下一层组件里面。
  • Detail.vue 导入,data中定义goods:{},之后使用
  • 注意,使用的时候要放到 getDetail的then里面,因为要用到请求来的全部详情页数据 中的几个数据。
Detail.vue
import { getDetail, Goods } from "network/detail"; 
 data() {
    return {
      iid: null,
      topImages: [],
      goods: {}
    };
created{
getDetail(this.iid).then(res => {
      console.log(res);
      //   1.获取顶部的图片轮播数据
      this.topImages = res.result.itemInfo.topImages;
      //   2.获取商品信息
      this.goods = new Goods(res.result.itemInfo,res.result.columns,res.result.shopInfo.services)
    });
  • detail文件夹-childComp文件夹-建个DetailBaseInfo.vue,用来展示商品基本数据。拿来codwhy这个文件。详解
  • <div v-for=“item in title”>遍历数组,这个item是一个个值。
  • 除了遍历数组对象这里也可以写数字<div v-for=“item in 10”>{{item}}<div>到时候会从1位置遍历到10
  • <div v-if=“goods.dicount”>{{good.discount}}</div>v-if指如果存在,则展示
  • 利用数字遍历,可以在遍历数组的时候,不遍历你不想遍历的最后一个元素。比如service:[{},{},{},{},{}]你不想遍历最后一个元素。<div v-for=“index in service.length-1”> 则index会是1-4
  • 很多信息,我们希望有数据的时候展示,没数据的时候不要展示,所以可以搞个v-if
  • 怎么判断obj是个空对象const obj={}
    Object.keys(obj).length===0 ; 如果等于0,表示里面没有key,说明是个空对象
var obj = { 'a': '你好', 'name': '嗨', 'num': '哈喽' }
var keyValue = Object.keys(obj)
console.log(keyValue);//["a","name","num"
  • 判断goods是不是一个空对象<div v-if=“Object.keys(obj).length!==0”> 如果为false,后面标签里的东西都不渲染了,为true渲染
  • 之后来到Detail.vue,导入,注册使用子组件DetailBaseInfo.vue,并把这goods数据传给子组件
  • 问题:延误必赔那里没数据shopInfo.services[0].icon detail.js文件DetailBaseInfo
<img :src="goods.services[index-1].icon" alt />
改为
<img :src="goods.services[index-1].icon? goods.services[index-1].icon : '//s11.mogucdn.com/p1/160607/upload_ie4tkmbtgqztomjqhezdambqgqyde_44x44.png'"  />

五. 店铺信息展示

  • 把店铺信息汇总到一块,之后传到组件中进行展示。数据太复杂了,想搞一起汇聚成一个对象。
  • 在detail.js中封装个类,之后在detail.vue中导入,使用,new 这个类,就完成了零散数据汇聚成对象。把这个对象存进data中
  • this.shop = new Shop(data.shopInfo);
  • 之后把这个对象传递给店铺信息展示的组件
  • 建组件DetailShopInfo.vue 先判断对象是否为空,不为空再渲染里面的东西<div class=“shop-info” v-if=“Object.keys(shop).length!==0”>
  • 在Detail.vue中把DetailShopInfo.vue导入 注册 使用
  • 在这里店铺分数那里,服务器还返回了个isBetter数据,为了动态写class,若isBetter是true,则意味着是绿色。<td class=“score” :class="{‘score-better’: item.isBetter}">{{item.score}}根据isBetter动态绑定class。默认颜色是绿色,如果有class是红色。
  • <div class=“sells-count”>{{shop.sells | sellCountFilter}}</div>后面是过滤器,大于多少显示万
filters: {
    sellCountFilter: function(value) {
      if (value < 10000) {
        return value;
      }
      return (value / 10000).toFixed(1) + "万";
    }
  }
}

六. 加入滚动的效果Scroll

  • 出问题了,导航跟着一起滚,因为现在这个页面是原生的滚动。而导航没有fixed,所以会一起滚。等下引用better-scroll解决问题
  • 在详情页里面,下面的tabbar不用显示了。但是目前tabbar是fixed,脱离标准流,覆盖标准流在最上面。
  • 目前来说是底部是fixed,其余都是标准流
  • 详情页想要盖住底部fixed的tabbar,需要搞个定位,则搞个position:relative(这个定位是相对原来的位置,方便),z-index:9 ; 但是没有完全盖上,因为详情页没有背景颜色,有的地方是透明的,所以我们给它加个背景颜色。
  • 用better-scroll做滚动效果,防止移动端问题。
  • 用better-scroll就可以实现局部滚动,导航就不需要设置fixed使他固定住,就可以不随着一起滚动了。用better-scroll想要上面的固定住,则下面所有除了导航栏,用一下<scroll>包裹住。导入注册使用
  • 到时候<scroll>里的东西会替换掉scroll.vue中的slot插槽。别忘了给固定高度:1.calc 2.定位
  • height:calc(100%-44px) 100%是相对于父元素,而父元素没设置固定高度,意味着父元素是由内容直接撑高的,所以父元素很高,但我们希望父元素是固定高度,所以给父设置height:100vh
  • 注意calc中间减号附近要加空格,如果不加没效果
Detail.vue
<div id="detail">
   <detail-nav-bar />
   <scroll class="content">
     <detail-swiper :top-images="topImages" />
     <detail-base-info :goods="goods" />
     <detail-shop-info :shop="shop" />
   </scroll>
 </div>
#detail {
 position: relative;
 z-index: 9;
 background-color: #fff;
 height: 100vh;
}
.content {
 height: calc(100%-44px);
 overflow: hidden;
}
  • 发现导航栏被挡住了,给导航栏加个class,设置个相对定位,z-index 不要随便用绝对定位或固定定位,因为相对定位仍然保持在原来的位置。但是导航没有背景颜色,设置一下#fff

七. 商品详情数据展示

  • 获取数据,展示。
  • 除了详情页的图片数据还有一些小碎的文字。但是不需要整合成一个对象保存这些数据。因为虽然这些数据碎但是总体还是在一个对象中。我们从服务器请求到这个对象,传到展示页,让展示页自己从这个对象中自己取数据。
  • Detail.vue data中搞个变量存储从服务器请求来的对象this.detailInfo = data.detailInfo;
  • 封装个组件展示数据DetailGoodsInfo.vue
  • Detail.vue导入注册使用传入数据
  • <detail-goods-info :detail-info=“detailInfo” /> 看一下这里咋写的,子组件是DetailGoodsInfo,数据是detailInfo,子组件接收数据是props{detailInfo: {}
之前代码 就展示了对象里第一个数组的代码
<div class="info-key">{{detailInfo.detailImage[0].key}}</div>
    <div class="info-list">
      <img v-for="(item, index) in detailInfo.detailImage[0].list" :key="index"  :src="item"  @load="imgLoad" />
    </div>

之后代码  遍历展示对象里所有数组的代码
<div v-for="(item,index) in detailInfo.detailImage" :key="index">
      <div class="info-key">{{item.key}}</div>
      <div class="info-list">
        <img v-for="(item, index) in item.list" :key="index" :src="item" @load="imgLoad" alt />
      </div>
    </div>
  • 在这里滚动也有问题,滚到这里不能滚了,原因是最早计算可滚动区域就计算了一部分高度,没把后来加载的图片算在内。
  • 在DetailGoodsInfo.vue监听图片每加载完一张,重新做一次刷新 ,重新计算一次高度。
  • <img @load=“imgLoad”…>
  • if (++this.counter === this.imagesLength)实现最终只回调一次,否则有几张图片执行几次。这里用防抖函数效果是一样的,防抖函数可能会发射多次,这样判断里面肯定就发射一次。
  • 轮播图只要有一个图片加载完,整个的高度就确定了。这里需要把所有图片都加载完才能确定高度,每一张图片都会影响高度,影响可滚动区域。
  • 为啥用这个watch:
    • 其实可以这样 methods: {imgLoad() {if (++this.counter === this.detailInfo.detailImage[0].list.lengt),但是这样每次都是后面调用一长串才能获取到图片的长度,每次加载完一张调一次,也不好 ,性能低
    • 想在data中写imagesLength:this.detailInfo.detailImage[0].list.length 获取一次就可以了,这样也不好。因为传来的 detailInfo可能还没请求到,此时Detail.vue中的这个变量默认是空对象,则传来的可能是空对象。
    • watch可以监听属性的变化,我们可以监听datailInfo对象的变化,一旦你发生变化就用你最新的datailInfo,去获取你的长度。刚开始你是空对象{}
    • watch是一个对象,用来监听一个值的变化,并调用变化需要执行的方法。watch是监听属性的变化,只要属性值发生变化就会调用watch对应的函数
DetailGoodsInfo.vue
props: {
    detailInfo: {
      type: Object,
      default() {
        return {};
      }
    }
  },
 data() {
    return {
      counter: 0,//记录当前加载到第几张了
      imagesLength: 0//图片个数
    };
  }
methods: {
    imgLoad() {
      // 判断 当所有的图片都加载完了,才能进行一次回调
      if (++this.counter === this.imagesLength) {
        console.log("de-goods-imageload");
        this.$emit("imageLoad");
      }
    }
},
  watch: {
    detailInfo() {
      //获取图片的个数
      this.imagesLength = this.detailInfo.detailImage[0].list.length;
    }
  }
};
  • 监听子组件发射出来的事件,refresh,给<scroll>加上ref=“scroll” this.$refs.scroll.refresh();

这里用防抖做一下

DetailGoodsInfo.vue
<img v-for="(item, index) in item.list" :key="index" :src="item" @load="imgLoad" alt />

 methods: {
    imgLoad() {
        this.$emit('detailImageLoad')
    }
  }

Detail.vue
<detail-goods-info :detail-info="detailInfo" @detailImageLoad="detailImageLoad" />

 methods: {

      this.refresh();
    }
  }
};

mixin.js
  data() {
        return {
            itemImgListener: null,
            refresh: null
        }
    },
    mounted() {
        this.refresh = debounce(
            this.$refs.scroll && this.$refs.scroll.refresh,
            50
        );
        this.itemImgListener = () => { this.refresh(); }
        this.$bus.$on("itemImagLoad", this.itemImgListener);//取消的时候这个函数也要传进去
        // console.log('混入');

    },
  • 这里不是闭包,能这样写如果data没定义refresh 肯定不行的。之前写在mounted中是事件总线,是形成闭包了,refresh用的都是同一个。而这个methods没形成闭包,每次都会重建refresh,达不到防抖作用。必须要用一个refresh变量,我们呢可以在外面data中定义refresh。结合之前Detail.vue中用了的混入,就在混入里面定义即可
你想直接这样写就不对了
  methods: {
    detailImageLoad() {
      let refresh = debounce(this.$refs && this.$refs.scroll.refresh, 100);
      refresh();
    }
  }
这是Home.vue和Detail.vue(针对推荐数据展示的bug)的混入
mounted() {
        this.refresh = debounce(
            this.$refs.scroll && this.$refs.scroll.refresh,
            50
        );
        this.itemImgListener = () => { this.refresh(); }
        this.$bus.$on("itemImagLoad", this.itemImgListener);//取消的时候这个函数也要传进去
        // console.log('混入');

    },

八. 商品参数数据展示

  • 拿数据,展示,数据汇总成对象
  • detail.js写个类,导入到detail.vue中 detail.vue中new创建对象,保存起来,传递给展示的组件
  • //注:images可能没有值
    this.image = info.images ? info.images[0] : ‘’;
  • 突然想起来,之前服务器返回的数据好像是没有一项退货那个没参数,我们也可以这样设置个默认值
  • 建个DetailParamInfo.vue组件
  • 在detail.vue中 导入注册使用
  • ^:1.13.2这里指前面两个版本必须保存一致 ~:1.13.2这里指只保存第一个版本一致

九. 商品评论信息展示

  • 拿到评论信息存在data中
  • 有些商品没评论,判断评论个数不为0之后,通过下标取出一条数据展示肯定是没问题的,因为已经判断了,所以搞这个下标也没问题,不会越界。
    if (data.rate.cRate !== 0) {
    this.commentInfo = data.rate.list[0];
    }
  • 建组件DetailCommentInfo.vue props接数据,在Detail.vue中导入注册使用传数据
  • ------评论日期的展示-------
  • 里面日期格式很奇怪,一般服务器返回时间都不会返回这个格式2021-7-7 19:23:22,因为某些地方展示格式不一样,比如只想展示2021-7-7 2021/7/7。通常返回一串数字,以1970.1.1为起点的对应的时间戳
  • 时间戳是秒,而传入new Data中的需要是毫秒
  • 如何将时间戳转成时间格式化的字符串(常用)
    • 将时间戳转成Data对象
      const data=new Date(1535694719*1000)
    • 将data进行格式化,转成对应的字符串
    1. data.getFullYear()+data.getMonth()+…
    2. 开发中因为把日期格式化太常见了,data->FormatString(太常见),JAVA语言中提供了日期格式字符串的方法。js原生里面没提供。
      format(data,‘yyyy-MM-dd hh:mm:ss’)
  • 请求来的数据直接展示肯定不对,搞个过滤器。想要用,使用人家封装好的,复制到utils.js中。导入使用
 <span class="date">{{commentInfo.created | showDate}}</span>

import { formatDate } from "common/utils";
filters: {
    showDate: function(value) {
      let date = new Date(value * 1000);
      return formatDate(date, "yyyy-MM-dd hh:mm:ss");
    }
  }
  • 正则表达式:字符串匹配
    == y+:一个或多个y==
    RegExp.$1: 这个东西表示匹配到的yyy
    到时候这里匹配到的‘yyyy’ 替换成后面的结果
    date.getFullYear() + ''获取到的年份变成字符串。数字加空字符串变成字符串。
    substr做个截取,因为我们可能传来的yy是两个。则2020截成20
    如果传来4个yyyy则,4-4=0,表示不截取(2020).substr(0)
    因为年份处理和月日时间不同,所以分开写的。时间他们是如果获取到的是一位数字,就要在前面补0
    做一个匹配,匹配到后拿到对应的数字,替换到原来的位置
utils.js
export function formatDate(date, fmt) {
    //1、获取年份
    if (/(y+)/.test(fmt)) {
        fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
        //RegExp.$1 这个东西表示匹配到的'yyyy'
        //到时候这里匹配到的‘yyyy’ 替换成后面的结果
        //date.getFullYear() + ''获取到的年份变成字符串。数字加空字符串变成字符串。
        //substr做个截取,因为我们可能传来的yy是两个。则2020截成20
        //如果传来4个yyyy则,4-4=0,表示不截取(2020).substr(0)
    }
    //2、获取
    let o = {
        'M+': date.getMonth() + 1,
        'd+': date.getDate(),
        'h+': date.getHours(),
        'm+': date.getMinutes(),
        's+': date.getSeconds()
    };
    for (let k in o) {
        if (new RegExp(`(${k})`).test(fmt)) {
            let str = o[k] + '';
            fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
            //为1,代表hms都只有个,表示只需要显示一位数,只要传的参数不为1就代表需要两位数的显示方式,走后面那个方法
        }
    }
    return fmt;
};

//假如04:04:04
//这个方法是怎么样都会给你补齐两位的
function padLeftZero(str) {
    return ('00' + str).substr(str.length);
    //假如是传来的是04,则 ‘00’+04 变成0004,后面截取长度是2 最后是04
    //假如是传来的是4,则 00’+4 变成004,后面截取长度是1 最后是04
};

十. 商品推荐信息的展示

  • 推荐数据的接口不是detail是recommend
  • 在detail.js中写一下请求的接口,这个请求不需要传参数
export function getRecommend() {
    return request({
        url: '/recommend'
    })
}
  • Detail.vue导入,使用,recommends中存请求来的推荐信息数据
  • 展示数据不需要搞个子组件了,用GoodList.vue,GoodList.vue中要求传的数据也是数组,数组里面也是要展示的商品。
  • Detail.vue导入GoodsList.vue组件,注册,使用<goods-list :goods=“recommends” /> 注意这里传数据要传goods,因为组件那边就是这样接收的
  • 发现有报错,说读不到数据img,发现错误在GoodsListItem.vue中<img :src=“goodsItem.show.img” @load=“imageLoad” alt />,这个组件用来展示首页数据,现在展示详情页的时候发现没有show这个属性,因为详情页的数据不是这样存的,详情页的数据直接存在image中。
  • 要做一个判断,写个computed,逻辑或||,前面为空就执行后面的
  • 逻辑或当有一个为false,返回false一侧。当有两个false,返回之前。两个为true,返回之后。
  • 逻辑与当有一个为true,返回true一侧,当有两个true,返回之前。两个为false,返回之后。
  • 逻辑非,先转化为布尔值,再取反。
  • return this.goodsItem.image || this.goodsItem.show.img;
<img :src=“goodsItem.show.img” @load=“imageLoad” alt />

由之前的代码改成这个之后的代码

<img :src="showImage" @load="imageLoad" alt />

computed: {
    showImage() {
      return this.goodsItem.image || this.goodsItem.show.img;
    }
  }

十一. 首页和详情页监听全局事件和minxin的使用

  • 滚动有些问题卡了
  • 还有一个问题首页商品和详情页推荐那里用的一个展示组件。之前首页监听图片加载搞了事件总线,首页scroll刷新。现在详情页也在用,推荐图片加载完,首页scroll刷新这不合理。
  1. 可以通过路由判断,有/home这个路由发送homeItemImagLoad事件总线。有/detail这个路由发送detailItemImagLoad事件总线。if (this.$router.path.indexOf("/home") !== -1) {}
GoodsList.vue
<div class="goods-item" @click="itemClick">
    <img :src="showImage" @load="imageLoad" />
    
 methods: {
    imageLoad() {
      this.$bus.$emit("itemImagLoad");
    },
    itemClick() {
      this.$router.push("/detail/" + this.goodsItem.iid);
    }
  }

修改

GoodsList.vue
imageLoad() {
      if (this.$router.path.indexOf("/home") !== -1) {
        this.$bus.$emit("homeItemImagLoad");
      } else if (this.$router.path.indexOf("/detail") !== -1) {
        this.$bus.$emit("detailItemImagLoad");
      }
    },
  1. 另一种思路都发出itemImagLoad事件,不管在详情还是在首页里面都发出这种事件,但是我一旦进入详情,首页就不需要监听这个事件了。
    来到Home.vue首页这里,一旦我发现你离开首页,在deactivated()中取消全局事件的监听。有个问题,取消事件监听这里不能只传一个事件,一旦你只传一个事件,意味着所有地方关于这个事件的监听都会取消掉。你得告诉我你要取消哪一个函数。需要在后面传一个函数,就是你监听的那个函数。
    这个函数保存到一个data 变量中起名itemImgListener:null,这样才能取消的时候才能拿到。
    this.$bus.$off(‘itemImgLoad’,this.itemImgListener)
GoodsList.vue
methods: {
    imageLoad() {
      this.$bus.$emit("itemImagLoad");
    }

Home.vue
mounted() {
    // 1.图片加载完成的事件监听
    const refresh = debounce(
      this.$refs.scroll && this.$refs.scroll.refresh,
      50
    );
    this.$bus.$on("itemImagLoad", () => {
      refresh();
    });//取消的时候这个函数也要传进去
  },
deactivated() {
    this.saveY = this.$refs.scroll.getScrollY();
    // console.log(this.saveY);
  }


之后
data(){
  return {itemImgListener:null}
  }
mounted() {
    // 1.图片加载完成的事件监听
    const refresh = debounce(
      this.$refs.scroll && this.$refs.scroll.refresh,
      50
    );
    this.itemImgListener=()=>{ refresh();}
    this.$bus.$on("itemImagLoad", this.itemImgListener);//取消的时候这个函数也要传进去
  },
deactivated() {
    this.saveY = this.$refs.scroll.getScrollY();
    // console.log(this.saveY);
    this.$bus.$off('itemImgLoad',this.itemImgListener)
  }
  • 想在详情页里面也监听这样的事情,可以去mounted(){}用,这里可以借鉴Home.vue中的方式刷新,导入一下用一下防抖函数。
  • 也要把这个事件取消掉,先对函数做个保存,注意取消是在destoryed里,因为详情页这里没有keep-alive
  • 详情页没有keep-alive,一旦destoryed之后,就监听不到事件总线上的事件了。难道不是这样,难道不会自己取消,如果不会自己取消,多次触发mounted,会多次on监听。
Detail.vue
import { debouce } from "common/utils";
data() {itemImgListener: null}
mounted() {
    // 1.图片加载完成的事件监听
    const refresh = debounce(
      this.$refs.scroll && this.$refs.scroll.refresh,
      50
    );
    this.itemImgListener = () => {
      refresh();
    };
    this.$bus.$on("itemImgLoad", this.itemImgListener);
  },
destroyed() {
    this.$bus.$off("itemImgLoad", this.itemImgListener);
  },
  • 混入:组件里面有公共代码,做个抽取,用到新技术混入mix in。vue提供的https://cn.vuejs.org/v2/api/#mixins
  • 创建混入对象:const mixin={}
  • 组件对象中:mixins:[]
  • 在这里插入图片描述
  • 继承也可以减少一些重复的代码,继承是减少类里面的重复代码的。我们这里是两个对象,对象里面的重复代码不能用继承
继承使用场景
class Person{
    run(){}
}
class Dog{
    run(){}
}
则使用继承
class Animal{
    run(){}
}
class Person extends Animal{
    
}
class Dog extends Animal{
    
}
  • 本例用混入,common建个mixiin.js,把Home.vue和Detail.vue中的mounted中的公共的东西剪切,全部抽进去
mixin.js
import {debounce} from './utils'
//不加这个{}会报错

export const itemListenerMixin = {
    data() {
        return {
            itemImgListener: null
        }
    },
    mounted() {
        console.log(1);

        const refresh = debounce(
            this.$refs.scroll && this.$refs.scroll.refresh,
            50
        );
        this.itemImgListener = () => {
            refresh();
        };
        this.$bus.$on('itemImgLoad', this.itemImgListener);
        console.log('mounted混入操作');

    }
}
  • 之后在Home.vue和Detail.vue中先导入后使用,如何验证mounted混入了,可以 console.log(‘mounted混入操作’);
Home.vue   Scroll.vue
import { itemListenerMixin } from "common/mixin";

mixins:[itemListenerMixin]
data(){
  return {}
},
mounted(){
}

  • data中相同的属性也可以混入itemImgListener: null。验证方法,去到vuex查看这个属性

十二. 标题和内容的联动效果

  • 点击上面某个位置滚动到哪(上点下滚)
  • 滚到哪个,上面相应变化(下滚上点)

12.1. 点击标题,滚到对应的主题

DetailNavBar.vue

  • 监听点击,之前DetailNavBar.vue中为了点击改样式,有监听<div v-for="(item,index) in titles" @click=“titleClick(index)” > this.$emit(“titleClick”, index);

Detail.vue

  • <detail-nav-bar class=“detail-nav” @titleClick=“titleClick” />
  • data中定义 themeTopYs: [0, 1000, 2000, 3000]创建一个数组,再把获取到的对象数据放进数组里
  • this.$refs.scroll.scrollTo(0, -this.themeTopYs[1], 200);
  • 数组值应该是对应的offsetTop的值,不是死的。 themeTopYs: []
themeTopYs: []
titleClick(index) {
      this.$refs.scroll.scrollTo(0, -this.themeTopYs[index], 200);
}

获取所有主题的offsetTop
1. created肯定不行,压根不能获取元素

  • 如何动态获取offsetTop,监听图片都加载完拿到的才行,created可能拿不到组件对象,写在mounted

2.mounted也不行,数据还没有获取到

  • 给参数、评论、推荐的组件都加上ref
mounted() {
      this.themeTopYs.push(0)
      this.themeTopYs.push(参数的offsetTop)
      this.themeTopYs.push(评论的offsetTop)
      this.themeTopYs.push(推荐的offsetTop)
      this.themeTopYs.push(this.$refs.params.$el.offsetTop);
      this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
      this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
  }
  • 这个$el就是组件的根元素
  • 上面这样写不行,因为根组件有个v-if,如果数据还没请求到整个标签都不渲染了,这时候this.$refs.comment.$el取到的是undefined

3. 获取到数据的回调中也不行,DOM还没有渲染完

  • 所以上面代码必须写到能拿到数据且图片渲染完的地方
  • 比如写在created中请求数据的代码里面getDetail(this.iid).then,这里确保能拿到数据,不会出现在mounted那种拿不到数据的情况,但是图片没渲染完,取到的offsetTop不准确

4. $nextTick也不行,因为图片的高度没有被计算在内

  • 之前讲过有了数据之后会执行updata方法,在里面对界面进行更新 ,做完更新之后会回调updated。updated可以确保肯定里面是有数据的。只要有数据变化都会执行updated,updated更新的比较频繁。
updated(){
    this.themeTopYs = [];
    this.themeTopYs.push(this.$refs.params.$el.offsetTop);
    this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
    this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
    console.log(this.themeTopYs);
  }
  • 发现打印了很多次,updated会更新好几次,所以我们在前面先赋空值,下次更新的时候往空的数组里面放。不至于到时候一个数组里面有8个值。打印了两次,刚开始也是有的值是undefined

  • 为啥明明都请求到数据了也赋值了,子组件也拿到值了还是不行,为什么this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);这样拿的时候还是没值呢?

  • 因为那个组件不是立即更新的,要渲染一会,渲染之后再更新我们的dom,更新dom之后这个东西才有值。那边正在渲染你就直接想拿值是拿不到值的。只有等那边渲染完,我们才能拿到值。

  • 什么时候渲染完呢$nextTick()

  • 等数据请求到了了赋值过去后,稍微等一会,等组件真正把数据渲染完之后 ,再计算offsetTop

  • 有个东西this.$nextTick() 下一帧要求你传回调函数,等到组件东西渲染完,他会回到这里回调这个函数。官方通俗理解:当数据更新了,在dom中渲染后,自动执行该函数


themeTopYs: []

created() {
  getDetail(this.iid).then(res => {
      //   1.获取顶部的图片轮播数据
      const data = res.result;
      this.topImages = data.itemInfo.topImages;
      。。。。。
       this.$nextTick(() => {
        this.themeTopYs = [];
        this.themeTopYs.push(0);
        this.themeTopYs.push(this.$refs.params.$el.offsetTop);
        this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
        this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
        console.log(this.themeTopYs);
      });
    });
 
  titleClick(index) {
      this.$refs.scroll.scrollTo(0, -this.themeTopYs[1], 200);
    }
  • offsetTop定义:
    https://blog.csdn.net/jinxi1112/article/details/90692484
    元素到offsetParent顶部的距离,offsetParent:距离元素最近的一个具有定位的祖宗元素(relative,absolute,fixed),若祖宗都不符合条件,offsetParent为body
  • 这里<scroll>是没有定位的,所以这个offsetTop是相对于这个大的根组件,而我们真正的滚动距离是相对于scroll顶部来滚动的,所以这里要减去导航栏44px???这里没=
  • 在这里这样写还是不对,这仅仅是根据最新的数据,把最新的dom渲染出来了,但是里面的图片还没加载完。上面这样的写法获取到的offsetTop是不包含其中的图片的。之前是数据加载完成,图片加载未完成,图片加载完位置就变了。
  • 之外几次方法取值不对的原因是,this.$refs.params.$el这个东西根本就没有值,压根没渲染
  • 这次方法获取值不对的原因是,图片没有计算在内。

5. 图片加载完成后,获取的高度才是正确的

  • 详情页做工图片那里图片每加载完计算一次offsetTop,但是太频繁了,要用防抖。
methods: {
    detailImageLoad() {
      //其实这里用了防抖函数,用了混入,代码在mixin.js中
      this.refresh();

      //每次图片加载完就计算
      this.themeTopYs = [];
      this.themeTopYs.push(0);
      this.themeTopYs.push(this.$refs.params.$el.offsetTop - 44);
      this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
      this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
      console.log(this.themeTopYs);
    }
  • 定义个函数变量getThemeTopY: null
  • 在created函数里面单独调用防抖函数,赋值给getThemeTopY
themeTopYs: [],
getThemeTopY: null

created(){
     。。。
      // 给getThemeTopY赋值(对给this.themeTopYs 赋值的操作进行防抖)
      this.getThemeTopY = debounce(() => {
      this.themeTopYs = [];
      this.themeTopYs.push(0);
      this.themeTopYs.push(this.$refs.params.$el.offsetTop );
      this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
      this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
      console.log(this.themeTopYs);
      //看看打印几次,说明函数调用几次。
    },100);
  },
}

methods: {
    // imageLoad() {
    //   this.$refs.scroll.refresh();
    // }
    detailImageLoad() {
      //其实这里用了防抖函数,用了混入,代码在mixin.js中
      this.refresh();

      //每次图片加载完就计算
      //这里用了防抖函数之后,就不会每次都调用了
      this.getThemeTopY();
    }
 }
  • 你要是不想每次给防抖函数时间,可以给个默认值,es6语法export function debounce(func, delay = 50) {。。。}

12.2. 滚动内容,标题发生改变

  • 先获取滚动的位置,监听滚动,所以内容都放在scroll里,所以是监听scroll的滚动事件

  • <scroll class=“content” ref=“scroll” @scroll=“contentScroll” :probe-type=“3” >

  • 这里传的有参数,带了position。注意这里要传数字,要加:,否则以为传的是字符串

  • 获取y值,拿y值和主题中的值进行对比,决定当前显示那个index

contentScroll(position) {
      //   1.获取y值
      const positionY = -position.y;

      // 2.positionY和主题中的值进行对比
      // [0,7938,9120,9452]
      // positionY在0和7938之间,index=0
      // positionY在7938和9120之间,index=1
      // positionY在9120和9452之间,index=2
      // positionY超过9452,index=3
      for (let i in this.themeTopYs) {
        //问题1:这个i是str。如果i是0,那么i+1是01 console.log(i+1);
        //字符串转成数字类型 i*1 或者 parseInt(i)
        if (
          positionY > this.themeTopYs[i] &&
          positionY < this.themeTopYs[i + 1]
        ) {
          console.log(i);
        }

        // 或者另一个方法
        for (let i = 0; i < this.themeTopYs.length; i++) {
          if (
            positionY > this.themeTopYs[i] &&
            positionY < this.themeTopYs[i + 1]
          ) {
            console.log(i);
          }
          //   问题2:i+1越界了,最后一次根本进不来这个语句
        }
      }
    }
  }
};

<scroll class="content" ref="scroll" @scroll="contentScroll"  :probe-type="3" >
    <detail-nav-bar class="detail-nav" @titleClick="titleClick" ref="nav" />

contentScroll(position) {
 //   1.获取y值
 const positionY = -position.y;

// 2.positionY和主题中的值进行对比
let length = this.themeTopYs.length;
for (let i = 0; i < length; i++) {
        //   if(()||()){}
        //   if((A&&B)||(C&&D)){}
        if((i<length-1&&positionY > this.themeTopYs[i] &&positionY < this.themeTopYs[i + 1])||(i===length-1&&positionY>this.themeTopYs[i])){
            console.log(i);    
            this.currentIndex = i;
            this.$refs.nav.currentIndex = this.currentIndex;
        }
      }
  }
  • 可以去详情页滚动验证,滚到相应位置,会输出相应的值。打印太频繁了
  • data中搞一个currentIndex:0 下面搞个this.currentIndex = i;
  • 让它不打印这么频繁if(this.currentIndex!==i &&。。。){}
变成 if(this.currentIndex!==i &&((A&&B)||(C&&D))){}
变成if(C&&((A&&B)||(C&&D)))
if(this.currentIndex!==i &&((i<length-1&&positionY > this.themeTopYs[i] &&positionY < this.themeTopYs[i + 1])||(i===length-1&&positionY>this.themeTopYs[i]))){
  • 把currentIndex传到DetailNavBar.vue即可实现,一直都是currentIndex主导到底哪个有点击效果
  • 还有一种奇特写法
  • 注意这种写法需要往数组中多加一个值[0,7938,9120,9452,最大值]
  • 获取js里能表示的最大值Number.MAX_VALUE
created(){
 请求数据
 // 给getThemeTopY赋值
    this.getThemeTopY = debounce(() => {
      this.themeTopYs = [];
      this.themeTopYs.push(0);
      this.themeTopYs.push(this.$refs.params.$el.offsetTop);
      this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
      this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
      this.themeTopYs.push(Number.MAX_VALUE);
    }, 100);
  }
  methods: {
   detailImageLoad() {
      //其实这里用了防抖函数,用了混入,代码在mixin.js中
      this.refresh();
      //每次图片加载完就计算
      //这里用了防抖函数之后,就不会每次都调用了
      this.getThemeTopY();
    },
  },
  contentScroll(position) {
    //   1.获取y值
    const positionY = -position.y;
    // 2.positionY和主题中的值进行对比
     let length = this.themeTopYs.length;
      for (let i = 0; i < length - 1; i++) {
        if (
          this.currentIndex !== i &&
          positionY >= this.themeTopYs[i] && positionY < this.themeTopYs[i + 1]
        ) {
          this.currentIndex = i;
          this.$refs.nav.currentIndex = this.currentIndex;
        }
      }
  • 注意,这里for这里i < length - 1,因为我们给数组最后一个数字赋值的是无穷大,遍历了没意义,不减去还会越界

十三. 底部工具栏封装

  • DetailBottomBar.vue 根<div class=“bottom-bar”>
  • Detail.vue中导入注册使用
  • Detail.vue是导航 内容框,而底部工具栏在整个页面的下面,但是永远也滚不上去,因为我们上滑,滚动的是内容区域。
  • DetailBottomBar.vue我们可以采取定位:height:49px 1.fixed,left0 right0 bottom0 2.relative bottom:49 z-index:1000(之前的滚动没设置z-index,那这里也没必要设置,因为我加载scroll后面,会盖上前面的)。
  • 发现用relative 底部可以拖动,很不好,还是用fixed吧
  • 现在底部工具栏是透明的,要改一下background-color: #fff;
  • .content {height: calc(100% - 44px - 49px);}

十四. BackTop回到顶部混入封装

  • 导入‘’注册‘’使用组件,有个变量’‘true false决定什么时候显示隐藏v-show,方法是滚动位置大于1000,变量为true(这个不能抽),注意这个scroll标签里面有 :probe-type=“3”@scroll="contentScroll"监听滚动。点击回到顶部的方法’’.
  • 使用混入 export const backTopMixin = {…}
  • 哪些东西不能抽:如果是生命周期函数,你可以即在混入里面写,又在组件里面写,到时候会合并的并不会覆盖。但是methods仅仅合并里面的每个函数,但是函数内部contentScroll(position) {},组件里面写了A代码,混入里面写了B代码,到时候不会合并两个contentScroll(position) {},只会覆盖
  • Detail.vue导入混入的对象,在components下面写mixins:[backTopMixin]
  • 注意看导入组件和导入变量函数区别import {debounce} from “./utils”;
    import BackTop from “components/content/backTop/BackTop”;

十五. 将商品添加到购物车

  • 点击会将当前商品加入购物车,之后在购物车页面展示我们的所有加购的商品
  • DetailBottomBar.vue在加入购车的div里面监听点击发射事件,在Detail.vue中监听自定义事件
  • 将商品加入购物车,首先只需要获取在购物车里需要展示的信息,之后加入购物车。
  • (扩展:点击加入购物车显示子组件,选择哪个颜色等)
  • 图片,标题,描述,价格,注意iid也一定要传进去,因为iid是商品的唯一标识,到时候用户真的购买时,你需要告诉服务器用户真正购买的是哪个商品的iid,在任何地方标识商品的,都需要把id一起拿过去。
  • 像购物车这个东西就可以vuex来管理所有的商品
  • product对象由这个界面到另一个界面不好传,他俩没有直接的关系。想用事件总线,但不好,事件总线要想拿到,要保证购物车组件那里创建出来了。我们要把点击添加购物车的商品数据存到一个临时的地方,全局的地方,全局的状态很多状态需要共享,放到vuex里面。假如说另一个页面也将商品加入购物车就更方便了,点击加入vuex。购物车相关的数据在多个界面进行共享。
methods 点击加入购物车会触发
 addToCart() {
      //   1.获取购物车需要展示的信息
      const product = {};
      product.image = this.topImages[0];
      product.desc = this.goods.desc;
      product.title = this.goods.title;
      product.price = this.goods.realPrice;
      product.iid = this.iid;

      //   2.将商品添加到购物车里面
      //   this.$store.cartList.push(product)
      this.$store.commit("addCart", product);
    }
  • 添加到购物车需要用到vuex,还没安装 npm install vuex@3.1.0 --save
  • 在store文件夹建个index.js文件
import Vue from "vue"
import Vuex from "vuex"
// 1、安装插件
Vue.use(Vuex)
// 2、创建store对象
const store = new Vuex.Store({
    state: {

    },
    mutations: {

    }
})
// 3、挂载到Vue实例上
export default store
main.js
import store from './store'

new Vue({
  render: h => h(App),
  router,
  store
}).$mount('#app')

  • state中搞个变量cartList: []用于存储我们添加进来的东西,搞个数组,里面存商品1,商品2 可以直接push进来
  • 修改任何state里的东西都要通过mutations
  • 修改的方法addCart,写在mutations中,默认有第一个参数state,后面可以传一个额外的参数palyLoad。 addCart(state, palyLoad) {state.cartList.push(palyLoad)}
  • 通过commit调用这个方法this.$store.commit(“addCart”, product);
  • 看一下有没有添加上<div>{{$store.state.cartList.length}}</div>
const store = new Vuex.Store({
    state: {
        cartList: []
    },
    mutations: {
        addCart(state, palyLoad) {
            state.cartList.push(palyLoad)

        }
    }
})
  • 在一个商品里多次加入购物车不应该一直push进去多个对象,因为是同一件商品,push进一次即可。不直接push,做个判断
  • payload新添加的商品,判断这个对象有没有在cartList:[]里。其实就是对比payload的id和数组里面东西的id看看一样不。
  • 方法一:用for循环
mutations: {
        addCart(state, palyLoad) {
            //palyLoad新添加的商品
            let oldProduct = null,
            for (let item of state.cartList) {
                if (item.iid === palyLoad.iid) {
                    oldProduct = item
                    //oldProduct指向那个与我传进来的商品一样的商品
                }
            }
            if (oldProduct) {
                oldProduct.count += 1
            } else {
                palyLoad.count = 1
                //没有count这个属性,这样就有了
                state.cartList.push(palyLoad)
            }
        }
    }
  • 方法二:不想用for循环也可以用indexof,但是indexof有个不好的事情,因为我们还有拿到旧的商品
mutations: {
        addCart(state, palyLoad) {
        let index = state.cartList.indexOf(palyLoad)

         if (index !== -1) {
                let oldProduct = state.cartList[index]
                oldProduct.count += 1
        } else {
                palyLoad.count = 1
                state.cartList.push(palyLoad)
        }
}
  • 方法三:数组方法find() 方法返回通过测试(函数内判断)的数组的第一个元素的值。后面传function
mutations: {
        addCart(state, palyLoad) {
            // 查找之前数组中是否有该商品
            let oldProduct = state.cartList.find(function (item) {
                return item.iid === palyLoad.iid
                // item=>item.iid===palyLoad.iid
            })
            if (oldProduct) {
                oldProduct.count += 1
            } else {
                palyLoad.count = 1
                //没有count这个属性,这样就有了
                state.cartList.push(palyLoad)
            }
      }
  • 用vue的devtools工具可以看数据的变化

十六. Vuex中代码的重构

  • moutations唯一的目的就是修改state中状态
  • moutations中的每个方法尽可能完成的事件比较单一一点。
  • 现在是商品加一和添加新商品都在 moutations中的addCart中做。说白了就是两个操作一个方法,跟踪不确定性。
  • 一般把有判断逻辑的东西放在actiona里面,之前讲过异步操作放在action里面,其实不止异步操作,一般有判断逻辑的话,也放在actions里面
  • 把mountations里的东西都放在actions里。注意actions中函数第一个参数不是state,而是contextaddCart(context, palyLoad) {},其实还可以这样,对象的解构contextaddCart({state.commit}, palyLoad) {}。
  • actions里面代码也要改一下。这里也不能直接修改content.state
  • 因此着我在Detail.vue中不是之前的调用方式了。actions的调用方式是this.$store.dispatch(‘addCart’,product)
 mutations: {
        addCounter(state, palyLoad) {
            // palyLoad.count += 1
            palyLoad.count++
        },
        addToCart(state, palyLoad) {
            state.cartList.push(palyLoad)
        }
    }
actions: {
        addCart(context, palyLoad) {
            let oldProduct = context.state.cartList.find(function (item) {
                return item.iid === palyLoad.iid
                // item=>item.iid===palyLoad.iid
            })
            if (oldProduct) {
                // oldProduct.count += 1
                context.commit('addCounter', oldProduct)
            } else {
                palyLoad.count = 1
                // context.state.cartList.push(palyLoad)
                context.commit('addToCart', palyLoad)
            }
        }
    }
  • 继续重构,之前讲的最好不让state,mutations,actions全部放到一起,放到一起代码有点乱。
  • 在store文件夹建个mutations.js export default {…} 而这个store文件夹index.js 仅仅保留mutations 导入import mutations from “./mutations”
  • state之也改
const store = new Vuex.Store({
    state: {
        cartList: []
    },
    mutations,
改成
const state={
     cartList: []
}
const store = new Vuex.Store({
    state,
    mutations,

  • mutations里面的方法名可以抽成常量,在store文件夹建个mutation-types.js

一. 购物车导航栏

  • 做导航导入注册使用
  • 导航栏 购物车有数字,里面的数字根据vuex中cartList:[]数组的个数决定<div slot=“center”>购物车({{$store.state.cartList.length}})</div>
  • 可以搞个计算属性<div slot=“center”>购物车({{cartLength}})</div>
computed: {
    cartLength() {
      return this.$store.state.cartList.length;
    }
  }
  • 如果你觉得拿我们cartLength比较多的情况下,可以封装一个getter,写在vuex中的getter中,很多地方就可以直接用这个getter
  • store文件夹建个getter.js
getter.js
export default {
    cartLength(state) {
        return this.state.cartList.length;
    }
}
Cart.vue
 computed: {
    cartLength() {
      return this.$store.getters.cartLength;//这个还是挺长的
    }
  }
  • 我现在有个希望是,我在Cart.vue中压根不写计算属性,我想把getters.js中的东西直接搬过来用。就像用计算属性一样直接用getters.js
getter.js
export default {
    cartLength(state) {
        return this.state.cartList.length;
    },
    cartList(state){
    ...
    }
}

Cart.vue
用法一,写数组表示是一一对应的关系,名字是一样的
<div slot="center">购物车({{cartLength}})</div>
import { mapGetters } from "vuex";
computed: {
    ...mapGetters(['cartLength','cartList'])
    //这里搞个数组,告诉它当前哪些getter需要转化成计算属性
}

用法二,在这里你想换个名字用,不想用getter中写的名字。这里搞个对象,你自己写个你想叫的名称
<div slot="center">购物车({length}})</div>
import { mapGetters } from "vuex";
computed: {
    ...mapGetters({
        length:'cartLength',
        list:'cartList'
    })
}

二. 购物车商品展示

  • 是从vuex里面拿到cartList:[],取数据遍历展示
  • childComps建CartLis.vue,写个根组件class=“cart-list”,自己从vuex获取数据展示
  • 在cart.vue中导入注册使用
  • getters.js中export default { cartList(state) {return state.cartList}}
  • CartLis.vue 中import { mapGetters } from “vuex”
    computed: {
    …mapGetters([“cartList”])
    }
  • 可以验证一下取到数据没 <div class=“cart-list”> <li v-for="(item,index) in cartList" :key=“index”>{{item}}</li> </div>
  • 注意我们不用原生滚动,则就在CartLis.vue 中导入注册使用scroll。注意一定要写高才能用,注意这样写的前提是父要有个确定高度,但现在父的高度根据内容撑开的,不行 .cart{ height: 100vh;} ,这个cart是cart.vue的根组件的类
CartList.vue
 <div class="cart-list">//注意这里根标签,scroll写在根标签里面
   <scoll class="content">
     <li v-for="(item,index) in cartList" :key="index">{{item}}</li>
   </scoll>
 </div>
 
.cart-list {
 height: calc(100% - 44px - 49px);
}
.content {
 height: 100%;
 overflow: hidden;
}
  • 发现每次codewhy每次设置其实都很多余,其实给这个标签设置100vh最合适了.content {height: calc(calc(100vh - 44px - 49px);}
  • 想了一下,导致这次这么复杂的原因是,之前都是直接在Home.vue Scroll.vue中套scroll,这次竟然跑到一个子组件里面开始设置,怪不得麻烦。不仅麻烦,发现下面还有不刷新卡住的问题。

2.1. 创建子组件展示单个商品

  • 建个CartListItem.vue
  • 在CartList.vue中遍历,遍历的单个数据放到CartListItem.vue展示
  • CartList.vue导入注册使用,遍历子组件,传数据<cart-list-item v-for="(item,index) in cartList" :key=“index” :item-info=“item” />
  • CartListItem.vue props接收。展示一下试试 <div>{{itemInfo}</div>- 出问题了,购物车划不动了:原因是最初进到购物车没东西,有个可滚动高度0。之后往购物车添加数据,有新数据后,better-scroll并不知道添加了新数据,它仍然认为我的整个可滚动区域还是0,它没给我重新计算高度
  • 解决办法CoodList.vue中,处于活跃状态就刷新一次activated(){this.$refs.scroll.refresh()}
  • 发现详情页导航栏的背景颜色老是没有变,可能是代码复用了,我在mounted(){this.$refs.scroll.refresh();}
  • 在CartListItem.vue 引入样式布局

三. 商品的选中不选中切换

  • components-content-checkButton文件夹CheckButton.vue
  • 根组件 class check-button
  • 对钩图 引入图
  • CartListItem.vue导入注册使用
    使用的外面包装了一层div,方便写样式布局
<div id="shop-item">
    <div class="item-selector">
      <CheckButton @click.native="checkClick" :is-checked="itemInfo.checked"/>
    </div>
    <div class="item-img">

#shop-item {
  width: 100%;
  display: flex;
  font-size: 0;
  padding: 5px;
  border-bottom: 1px solid #ccc;
}

.item-selector {
  width: 20px;//给固定宽度是因为总体flex布局
  display: flex;//这里写flex是为了里面的内容可以居中显示
  justify-content: center;
  align-items: center;
}
  • CheckButton.vue 为了让他有效果,加了背景颜色是background-color:red.
  • border-radius:50%会变圆角
  • 因为只有选中时才有效果,给最外层加个边框。去掉背景颜色,加了边框,这样看着就是一个圆圈了。
  • 但是选中的时候还是一个灰圆圈挺难看的,这是我们借助父传来的值,如果为true,则说明选中了,这时动态给标签加上class,让外部圆圈变成红色,且背景颜色变红色,表示选中了。不选中状态下还是默认的无背景色,灰色圆圈。
<div class="check-button" :class="{check:isCheck}">
   <img src="~assets/img/cart/tick.svg" alt />//图是正方形
 </div>

props: {
   isCheck: {
     type: Boolean,
     default: false
   }
 }
 
.check-button {
 border-radius: 50%;
 border: 2px solid #aaa;
}
.check {
 background-color: red;
 border-color: red;
}

选中和不选中

  • 选中和不选中根据 什么来决定?搞个属性记录选中不选中可以吗?
  • 不可以,一定是在这个商品对应的对象模型里面记录的。修改对象模型里的某个属性让界面发生改变。
  • 对象模型就是那个商品模型cartList[商品1,商品2,商品3]在checked属性里面记录选中不选中。
  • 默认情况下是选中,新添加一个商品,直接给这个商品添加这个属性checked=true,之后一旦用户点击购物车的按钮取消选中,改模型里面的属性checked=false(vuex中的),不要直接改界面CheckButton。
  • 在mutations.js中添加一句,payload.checked=true 这样默认对象中按钮是选中状态。
  • 在CheckListItem.vue中,用了CheckButton,在使用这个标签时给这个标签传递对象的数<check-button :is-checked=“itemInfo.checked” />
  • 之前是个数组,遍历里面对象放到checkListItem.vue展示,把对象里面的属性调用子组件CheckButton传过去,那边有接收props: { isCheck:}

点击不选中

  • 在CheckButton页监听点击,把事件发出去。点击后如果选中则取反checked,如果不选中则取反checked。
  • 也可以在CheckListItem.vue监听调用的元素的点击<check-button :is-checked=“itemInfo.checked” @click.native=“checkClick” />根据取出来的属性决定是否展示选中。
  • 之前是cartList: []–里面对象itemInfo
  • 点击改变状态,变成相反的状态checkClick() {
    this.itemInfo.checked = !this.itemInfo.checked;
    }
    这算是子组件修改父组件传来的props值吗?用mutaituons难以找到当前的state吧。这不就等于直接修改state的数据了吗?可以修改但是不建议这样做,但这是对象模型都是指向CartList应该没问题。这样做devtools监听不到checked的变化。好像是引用类型,可以直接修改,不过没经过mutations。通过mutations好像麻烦,要传id过去,然后用map方法,根据旧数组生成新数组。

四. 封装底部工具栏

  • childComps-CartBottomBar.vue 搞个根<div class=“bottom-bar”>
  • 在Cart.vue中导入注册使用
  • 之前按钮组件导入进来
  • height和line-height对img在盒子内垂直居中好像可以
  • line-height是设置文本和图片居中的,对盒子没有用,盒子可以用margin或定位
  • 而对于盒子在盒子中垂直居中可以用:display:flex align-items:center justify-content:center可以水平居中
<div class="bottom-bar">
    <div class="check-content">
      <check-button class="check-button" />
      <span>全选</span>
    </div>
  </div>

.bottom-bar {
  height: 40px;
  background-color: #eee;
  position: relative;
  line-height: 40px;
}
.check-content {
  display: flex;
  align-items: center;
  margin-left: 10px;
}
.check-button {
  width: 20px;
  height: 20px;
  line-height: 20px;
  margin-left: 5px;
}

合计

  • 不是计算所有的,做个过滤,再求和
 <div class="price">合计:{{totalPrice}}</div>
 computed: {
    totalPrice() {
      return (
        "¥" +
        this.$store.state.cartList
          .filter(item => {
            return item.checked;
          })
          .reduce((preValue, item) => {
            return item.price * item.count + preValue;
          }, 0)
          .toFixed(2)
      );
    }

去计算

<div class="calculate">去计算({{checkLength}})</div>

 checkLength() {
      //   return this.$store.state.cartList.filter(item => item.checked).length;
      return this.$store.state.cartList
        .filter(item => item.checked)
        .reduce((preValue, item) => {
          return preValue + item.count;
        }, 0);
    }

flex布局三栏

  • 左右固定宽度,中间flex:1

五. 购物车的全选按钮

5.1. 显示状态

  • 下面全选按钮的状态取决于上面商品是否都是选中状态,只要有一个不是则不选中
  • CartBottomBar.vue中 <check-button class=“check-button” />这里传变量true或false到子组件展示选中不选中,我们不要写死传true还是false,搞个变量存一下,搞个计算属性,因为这是根据各种计算得到的结果
  • 遍历做判断是否全部为true
isSelectAll() {{
  if (this.$store.state.cartList.length === 0) return false;
  for (let item of this.$store.state.cartList) {
        if (!item.checked) {
          return false;
        }
      }
      return true;
  }     
}
  • 数字可以取反,取反的结果为true或false,取反前会先转化为布尔值的。const num=10;console.log(!num) 结果是false。如果num为0,则结果为true。
<check-button class="check-button" :isChecked="isSelectAll" />
isSelectAll() {
      //   return !this.$store.state.cartList.filter(item => !item.checked).length;
      if (this.$store.state.cartList.length === 0) return false;
      return !this.$store.state.cartList.find(item => !item.checked);
    }
  • return false也可以停止for循环

5.2. 全选按钮的点击效果

  • <check-button class=“check-button” :isChecked=“isSelectAll” @click.native=“checkClick” />
  • 如果原来都是选中,点击一次,全部不选中
  • 如果原来都是不选中(某些不选中),点击一次,全部选中
  • foreach方法会遍历,内部如果做计算会改变原数组。map方法类似,但是不会改变原数组,map的返回结果是个新数组。
  • isSelectAll这个计算属性,代表的是全选按钮,只有两种选中和不选中,所以可以拿来当参数
methods: {
    checkClick() {
      if (this.isSelectAll) {
        //全部选中
        this.$store.state.cartList.forEach(item => (item.checked = false));
      } else {
        //部分或全部不选中
        this.$store.state.cartList.forEach(item => (item.checked = true));
      }
      //   this.$store.state.cartList.forEach(
      //     item => (item.checked = !this.isSelectAll)
      //   );
      //  这样不行,因为foreach一直在遍历,过程中会改变某个item.checked这时isSelectAll值会被改掉
    }
  }
  • item是对象,对象是引用类型,引用它时是同一个地址

六.添加购物车弹窗Toast

6.1. Actions可以返回一个promise

  • 很常用,比如加入购物车弹出。比如购物车没选中任何商品点击去计算。单独封装个组件。
  • 先做一个加入购物车的,再根据代码封装起来
  • 需求,点击加入购物车,当商品加入购物车的时候弹出来东西。
  • 首先监听有没有把商品加入购物车里面
  • Detail.vue <detail-bottom-bar @addCart=“addToCart” />并不是一点击按钮,立马就添加购物车成功了,而是里面完成操作之后,才添加购物车成功。
  • 如何知道完成了操作,因为actions里面返回promise
  • 在action.js中包裹一层,return new Promise((resolve,reject)=> {。。。resolve(‘当前的商品数量+1’)。。。})
  • this.$store.dispatch(“addCart”, product).then(res => {
    console.log(res);//这个res就是通过resolve传过来的信息
    });
Detail.vue
<detail-bottom-bar @addCart="addToCart" />

addToCart() {
     //   1.获取购物车需要展示的信息
     const product = {};
     product.image = this.topImages[0];
     product.desc = this.goods.desc;
     product.title = this.goods.title;
     product.price = this.goods.realPrice;
     product.iid = this.iid;

     //   2.将商品添加到购物车里面
     //   this.$store.cartList.push(product)
     //   this.$store.commit("addCart", product);
     this.$store.dispatch("addCart", product).then(res => {
       console.log(res);
     });
     // 3.添加购物车成功
}
action.js
export default {
    addCart(context, payload) {
        return new Promise((resolve, reject) => {
            let oldProduct = context.state.cartList.find(function (item) {
                return item.iid === payload.iid
                // item=>item.iid===payLoad.iid
            })
            if (oldProduct) {
                // oldProduct.count += 1
                context.commit('addCounter', oldProduct)
                resolve('当前的商品数量+1')
            } else {
                payload.count = 1
                // context.state.cartList.push(payLoad)
                context.commit('addToCart', payload)
                resolve('添加了新的商品')
            }
        })
    }
}

6.2. mapActions的映射关系

  • 之前学了把getter映射到计算属性里面。import { mapGetters } from “vuex”;
getter.js
export default {
    cartLength(state) {
        return this.state.cartList.length;
    },
    cartList(state){
    ...
    }
}

Cart.vue
用法一,写数组表示是一一对应的关系,名字是一样的
import { mapGetters } from "vuex";
computed: {
    ...mapGetters(['cartLength','cartList'])
    //这里搞个数组,告诉它当前哪些getter需要转化成计算属性
}

用法二,在这里你想换个名字用,不想用getter中写的名字。这里搞个对象,你自己写个你想叫的名称
import { mapGetters } from "vuex";
computed: {
    ...mapGetters({
        length:'cartLength',
        list:'cartList'
    })
}
  • 还可以把actions里面的方法映射到普通组件的methods里面
  • 看起来像是在调自己的方法,但我没这个方法。但我们可以把方法映射到这里。import { mapActions } from “vuex”;
actions.js本来就是这么多代码
export default {
    addCart(context, payload) {
        return new Promise((resolve, reject) => {
            let oldProduct = context.state.cartList.find(function (item) {
                return item.iid === payload.iid
                // item=>item.iid===payLoad.iid
            })
            if (oldProduct) {
                // oldProduct.count += 1
                context.commit('addCounter', oldProduct)
                resolve('当前的商品数量+1')
            } else {
                payload.count = 1
                // context.state.cartList.push(payLoad)
                context.commit('addToCart', payload)
                resolve('添加了新的商品')
            }
        })
    }
}

Detail.vue
import { mapActions } from "vuex";
methods: {
      ...mapActions(["addCart"]),
      //还可以放对象,取其他名字
      ...mapActions({add:"addCart"}),
      好多不相关方法
     // 这样写是指定调用vuex里面actions里面的方法
      this.$store.dispatch("addCart", product).then(res => {
        console.log(res);
      });
      // 这样写看起来像是在调自己的方法,但我没这个方法。但我们可以把方法映射到这里
      this.addCart(product).then(res => {
        console.log(res);
      });


6.3. 普通方式的封装

  • 创建个组件components-common-toast-Toast
  • 根 class=“toast” 我显示的内容肯定是需要别人告诉我 props:{message:{ 页面展示一下试试{{message}}
  • Detail.vue中导入注册使用
  • <toast message=“你好” />传字符串可以不用加:,直接传到子组件了=
  • toast组件显示在中间,我们使用fixed布局 position: fixed;top: 50%;left: 50%;
  • 背景是黑色,而且有些透明background-color: rgba(0, 0, 0, 0.75),没给宽高,内容撑开padding: 8px 10px; transform: translate(-50%,-50%)根据自己的宽度和高度让他偏一下
  • 这个东西不是一直显示,控制这个东西的显示和隐藏。标签上加上v-show,真正的值是变量show,也是props传来的,默认是false
  • <div class=“toast” v-show=“show”> 注意这个v-show不要加乱七八糟的:
  • 接下来就是把加入购物车成功和使用这个组件联系在一起。加入购物车成功会resolve(。。。)里面就是要显示在的小组件里的内容。
  • Detail.vue使用标签<toast>时传递两个变量到子组件即可实现显示隐藏toast,和显示什么信息了。
  • 注意传的这两个东西也是要变化的,所以我们还要在Detail.vue中搞两个变量存储需要传递的东西。data(){show:false,message:’’} 即<toast :message=“message” :show=“show” />
  • 添加购物车成功的then里面,立马写成this.show=true;this.message=res;
  • 不过这个过两秒就让他消失,定时器,其实内容没必要清空setTimeout(() => {
    this.show = false;
    this.message = “”;
    },1500);
Toast.vue
<div class="toast" v-show="show">{{message}}</div>
props: {
    message: {
      type: String,
      default: ""
    },
    show: {
      type: Boolean,
      default: false
    }
  }
  .toast {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 8px 10px;
  color: #fff;
  background-color: rgba(0, 0, 0, 0.75);
}
导入注册
<detail-bottom-bar @addCart="addToCart" />组件里面有点击事件
<toast :message="message" :show="show" />
data() {
    return {
    message: "",
      show: false
    };
  },
addToCart() {
      //   1.获取购物车需要展示的信息
      const product = {};
      product.image = this.topImages[0];
      product.desc = this.goods.desc;
      product.title = this.goods.title;
      product.price = this.goods.realPrice;
      product.iid = this.iid;
      //   2.将商品添加到购物车里面
      // 这样写看起来像是在调自己的方法,但我没这个方法。但我们可以把方法映射到这里
      this.addCart(product).then(res => {
        this.show = true;
        this.message = res;
        setTimeout(() => {
          this.show = false;
          this.message = "";
        }, 1500);
      });
      // 3.添加购物车成功
  • 假如点击去计算也要用到toast
  • CartBottomBar.vue中监听去计算点击,如果 全部选中取反即没有全部选中,calcClick() { if (!this.isSelectAll)…}
  • 发现很麻烦,因为我们toast组件要显示在页面中间,我们想的是添加到cart.vue中。那我们就要把去计算这个事件发出去,cart中监听在来做其他事情。

6.4. 插件方式的封装

  • 我不想每次 用这个组件都那么麻烦,我想this.$toast.show(res,2000)即可实现,即传了要显示的东西,又能设置显示多久消失this.$toast.show(‘请选择需要购买的商品’,2000)
  • 封装成这种形式,也是必须有这样的一个组件,把这个组件先引用起来,之后把组件添加到某个位置,到时候组件里的内容才能显示。
  • 我会把这个Toast.vue封装到一个插件里面,然后去安装这个插件,一旦我安装这个插件之后,就把这个组件Toast.vue先创建出来,并且把这个组件在最开始的时候添加到package里面。之后在这里show这个函数的时候this.$toast.show(res,2000),我只是把这个东西Toast.vue显示出来。也就是说Toast.vue早就预备好了。我是在插件的安装函数里面把这个东西Toast.vue先给他装好
  • toast文件里面建index.js const obj = {}
    export default obj
  • 这个东西在最开始预备好是指,去main.js(起步文件)导入import toast from 'components/common/toast
    导入这个文件夹的时候默认将index.js导入,而这个index.js导出一个obj,obj我们可以在这给它起名字叫toast,因为导出时写的是default,default这里是可以在这里改名字
  • 之后在main.js安装toast插件,Vue.use(toast)
  • 即一启动main.js,就把toast安装好了,安装好就是把这个东西给预备好了。
  • 之前讲过一旦调用Vue.use(),本质上是调用toast对象的install,toast对象就是刚刚在index.js导出的那个对象
  • 所以会去执行obj的install函数。obj.install = function () { console.log(‘执行了obj的install函数’);}验证一下
  • 所以我们在这个install函数里面把所有要预备的东西全部预备好
  • 执行install函数的时候,其实会有个参数的就是Vue。install函数在执行的时候默认会传过来一个参数Vue。默认传的,所以不用特意import导入Vue。obj.install = function (Vue) {console.log(‘执行了obj的install函数’, Vue);}
  • index.js可以在原型上面加上toast,一旦这样做,到时候很多地方都有这样的toast对象Vue.prototype.$toast = 对象。
  • 希望这个$toast对象最好就是Toast.vue对象,到时候就可以在Toast.vue对象里面封装一个methods:{show(message,duration){}}
  • 如何实现 index.js导入import Toast from ‘./Toast’,这就把对象导进来了。Vue.prototype.$toast = Toast
  • 但有个问题,只是把这个对象放到原型上面,意味着原型上面有这么个东西$toast是指向这个对象的Toast。但是Toast对象有自己的摸板<template>,要自己显示内容,这些内容没有被添加到body上。所以这个东西<template>不会显示的,因为你没有把<template>添加到body上
    怎么把Toast组件的摸板放到body中
  • 我们可以这样做把这个东西添加到body上面 document.body.appendChild(Toast.$el) .这里不行,是因为模板可能已经变成render函数了,目前来看这里是没有东西的undefined,console.log(Toast.$el),之后需要给这个东西赋值的时候才有东西。当执行install函数时,我们在这个$el还没挂载,toast所有东西可能还没挂载。
  • 上面方法不一定行,一般情况下是要求我们创建一个组件构造器Vue.extend(Toast),把Toast组件对象传进来。
    // document.body.appendChild(Toast.$el)
    // 1.创建组件构造器
    const toastContrustor = Vue.extend(Toast)
    // 2.new的方式,根据组件构造器,可以创建出来一个组件对象
    const toast = new toastContrustor()
    // 3.将组件对象,手动挂载到某一个元素上
    toast.$mount(document.createElement(‘div’))
    // 4.toast.$el对应的就是div
    document.body.appendChild(toast.$el)
  • 之后我们拿this.$toast拿到的其实是拿到的是Toast.vue的对象了。
  • 到时候传的时候是this.$toast.show(‘请选择需要购买的商品’,2000),用的就是Toast.vue中的methods: {show(message, duration) {} }。就不需要别人组件传的message了,不是按照组件方式传的。
  • Toast.vue定义data() {return { message: “”,isShow:false};}
Toast.vue
<div class="toast" v-show="isShow">{{message}}</div>
data() {
    return {
      message: "",
      isShow: false
    };
  },
  methods: {
    show(message='默认文字', duration=2000) {
      this.isShow = true;
      this.message = message;

      setTimeout(() => {
        this.isShow = false;
        this.message = "";
      }, duration);
    }
  }
  • 注意这个z-index:99。因为之前给整个详情页.detail设置相对定位的时候设置了z-index:1
  • 直接使用this.addCart(product).then(res => {this.$toast.show(res, 1500);})
  • 如果不想传duration和message都可以不传,可以设置默认的duration=2000这是es6语法,之前可以设置duration=duration||2000 有值会使用前面的值,没值会使用后面的值。
  • 去计算那个页面还可以,CartButtonBar.vue监听点击调用
 calcClick() {
      // if (!this.isSelectAll) {//这里不对,这是不全选
      if (!this.$store.state.cartList.find(item => item.checked)) {
        this.$toast.show("请选择购买的商品", 1500);
      }
    }

七. fastClick解决移动端300ms延迟

  • promise—polyfil补丁 不支持打个补丁
  • 安装 npm install fastclick@1.0.6 --save
  • es6语法
  • main.js 导入 import FastClick from ‘fastclick’ 调用attach函数 FastClick.attach(document.body)
  • fastclick点击过快会报错,解决办法:全局css定义 *{touch-action:pan-y}

八. 图片懒加载-vue-lazyload加载

  • 什么是图片懒加载:图片需要显示在屏幕上时,再加载这张图片。
  • github搜一下vue-lazyload
  • 安装npm install vue-lazyload@1.0.6 --save
  • main.js 导入 import VueLazyLoad from ‘vue-lazyload’
  • 使用懒加载插件Vue.use(VueLazyLoad)
  • 修改img:src->v-lazy 之前<img src="">现在<img v-lazy="">
  • 可以把GoodsListItem.vue中的mg:src->v-lazy
  • 可以设置好多属性,比如设置占位符。main.js Vue.use(VueLazyLoad,{loading:require(’./assets/img/common/placeholder.png’)})
  • 在js中如何导入一张图片使用,import()不行,可以require,require是可以加载图片的=。
  • 导入的方式就两种,老师说import()是个函数,可以用。其实这里不可以而import。。。这种语法必须在有export时才能用

九. px2vm-css单位转化插件

  • 现在都是px单位。我想都改成vw。这样导航栏就不是固定在44px了,当然你想固定在44px也可以做个限制。
  • 插件-webpack强大的地方
  • 可以搜一下px2vm px2rem webpack 插件
  • 安装 npm install postcss-px-to-viewport@1.1.0 --save-dev
  • 配置,不需要去vue.config.js额外配置,去之前自动生成的postcss-config.js中配置把px-vm,这是转化css的配置,一般不动。 改文件plugins:{} 就是对插件做些配置
  • babel.config.js es6-es5不需要手动配置了
  • 一点两个像素 设计常用稿(retina) 750->30px 真实手机大小375->15px
  • 375/445->15px/16px 按375的稿转化成vm 等比缩放
  • 想要卸载插件 npm uninstall 插件名
  • 在js中使用正则,用//,里面写正则表达式
  • exclude: [/^TabBar/]表示以TabBar开头的东西不转化
  • exclude: [/TabBar/]表示只有包含TabBar的东西都不转化,比如TabBar.vue和MainTabBar.vue
  • 插槽里填充的东西是不属于这个文件的
module.exports = {
	plugins: {
		autoprefixer: {},
		"postcss-px-to-viewport": {
			viewportWidth: 375,  // 视窗的宽度,对应的是我们设计稿的宽度.
			viewportHeight: 667, // 视窗的高度,对应的是我们设计稿的高度.(也可以不配置)
			unitPrecision: 5, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
			viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
			selectorBlackList: ['ignore', 'tab-bar', 'cart-bottom-bar'], // 指定不需要转换的类。对于你不想做转化,在页面中写个.ignore类即可
			minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位.
			mediaQuery: false,// 允许在媒体查询中转换`px`
			exclude: [/TabBar/] //必须是正则,匹配文件的
		},
	}
}
// 1.在js中使用正则:/正则相关规则/
// 2.exclude中存放的元素必须是正则表达式
// 3.按照排除的文件写对应的正则:
// 1 >^ abc: 表示匹配的内容,必须是以什么内容开头(以abc开头)
// 2 > abc$: 表示匹配的内容,必须是以什么内容结尾(以abc结尾)

十. nginx- 项目在window下的部署

  • 项目写完先打包npm run build
  • 部署很多种方式,tomcat部署很简单,webapp里面可以放很多静态资源,可以把打包的东西直接放到这里面,到时候就能进行相关的访问了
  • 服务器:一台电脑(没显示器,就是一台主机)24小时开着,为用户提供服务。
  • 小公司没自己的服务主机->租借 阿里云/华为云/腾讯云(配置)
  • 主机->必然需要装操作系统->window(.net)/Linux->tomcat/nginx(软件/反向代理)
  • 打包之后有个dist文件夹,只需要把这个dist文件夹放到服务器
  • 第一:将自己的电脑作为服务器->window->nginx(这样就可以直接在浏览器访问我们部署的项目了)
  • 第二:远程部署
    自己的电脑作为服务器
  • httpa://nginx.org 官网下载nginx软件。 找stable version下载稳定nginx-1.14.2 。解压,解压时不能包含中文,否则不能正常启动,我们把它放在d://fuwuqi。双击启动接口。在浏览器输入localhost不要用端口,默认就是80端口。有Welcome to nginx!页面说明安装成功.(如果你启动不了,端口可能是被占有了,可以关闭进程。或者可以改一下nginx端口。可以去看logs文件夹的日志)
  • 当然我们不希望它显示这个网页,希望他显示我们自己打包的项目
  • 把自己的项目的dist文件夹复制一份,来到刚刚解压的nginx文件夹
  • 1.来到nginx文件夹的html文件夹,删除里面的件,把我们dist文件夹里面的文件复制进去,替换掉他自己的html那个页面。
  • 之后直接在浏览器输入localhost,不要加端口号。
  • 路由会自动跳转到http://localhost/home,不可以直接刷新,因为这个东西并不是我们真正的主机地址,这仅仅是个路由。
  • tomcat是动态资源服务器,也就是不同请求是不同的网页,而nginx是静态资源服务器
  • 如果是本地服务器,一个局域网内可以访问。
  • 还有一种方法就是,整个dist文件夹放进去,然后改配置,到nginx.config,注意这里面#是注释。找到server { location / {
    root html;
    index index.html index.htm;
    }我们改一下,root改成dist。
  • 第二种方法改完配置必须重启,来到windows任务管理器找到进程,关闭nginx,好像杀不掉。
  • cmd执行,有个nginx -s stop 首先要cd到对应的目录。先关掉再打开start nginx 再输入localhost即可。
  • 发现个神奇的,直接进到地址栏,在地址栏最前面输入cmd,就能直接在当前目录打开了,不用cd
  • 有时候可以看一下错误日志解决问题。
  • 有的进程杀了还会再生,只能通过命令行杀
  • 用cmd可以查找谁在占用8080端口就可以找到pid了,执行kill pid命令就可以杀掉这个进程了。根据pid杀死进程。
    远程部署
  • linux ubunto图形 学习用 linux centos 终端
  • 远程主机->linux centos->nginx=>终端命令的方式去装nginx
  • 在这上面装nginx就不像在windows装nginx那么简单
  • yarn/npm yum->linux安装包管理工具
  • 首先要装yum,但一般装完linux就自带这个工具。
  • 在远程主机上面执行这个终端命令yum install nginx
  • 如何去远程主机上执行呢:先通过ssh登录到远程主机上,之后在你自己电脑的终端操作,就可以操作远程主机上了。
  • 在自己电脑终端上输ssh,windows上面没这个东西,要借助一些工具(win10已经有了)
  • 往远程主机上拷贝文件,ssh是进行终端连接的,想往里面拷贝东西,用=ssh不行,借助WinSCP软件,这个文件还能修改服务器的文件
  • 连接上主机需要装个ssh,可以使用SecureCRTPortable软件,这样就可以借助ssh帮你连接了
  • 个人推荐用xshell和xfto,个人使用都是免费的。
  • 一旦你买个主机,有自己的公网ip。 私有ip只能在你当时的局域网访问,像192.168.1.11.
  • 打开软件,file connect-new session-SSH2-主机名就是公网ip地址 用户名设置成root。就成功了,就多了个session,双击连接输密码。连接成功
  • ls -l就看到当前在root目录下面有哪些文件
  • 在这个软件执行yum install nginx到时候就可以在远程服务器上安装nginx了
  • systemctl start nginx.service # 启动nginx服务
  • 想之后这个服务跟随系统一起来启动就执行systemctl enable nginx.service # 之后系统每次重启,nginx会跟着一起启动
  • 想看一下这个nginx安装到哪里了,可以执行cd…返回上一层
  • 执行ls-l 如果看到nginx可以 cd nginx/进去里面
  • ls-l
  • 可以看到nginx里面的文件如nginx.config
  • 打开这个文件,进行修改,
  • 两个方式,一、借助WinSCP软件修改nginx.config,还可以拖到本地修改,再给拖回主机覆盖掉。
  • 二、还在这个页面,
  • vim nginx.config
  • 可以看到当前文件的内容了,想要修改的话,敲一下i,就可以直接改了改完之后按一下esc,
  • 之后按住shift键+:,输出:wq 意味着保存并且退出
  • 借助WinSCP软件把打包好的dist文件拖过去,因为改了一些配置,所以重启一下远程服务器,linux下面重启命令。
  • 如果是本地主机访问localhost,相当于访问本地主机127.0.0.1或者写0.0.0.0也表示是本地主机。这两个ip地址都是指向当前主机的。
  • 如果我们想访问远程,123.207.32.32
  • 如果你部署到远程服务器在任何地方都能访问

十一.Vue响应式原理

  • 不要任认为数据发生改变,界面跟着更新,并不是理所当然的
  • Object.defineProperty里面会对两个属性进行劫持,先拿到Object.keys(obj),因为每个属性都需要劫持。其实在这个对象里面做的事情是,给我们一个属性创建一个Dep对象一一对应关系。比如三个地方用message,message发生改变,就通知Dep里面所有的subs
  • set中监听就是监听到属性的改变,找到对应的dep(一个属性对应一个dep),调用notify方法,notify方法的内部实现遍历subs调用各自的update方法,update根据改变后的值在网页上更新
  • 数据改变调用set方法,通知发布者调用notify方法,所有watcher执行update方法。如何收集依赖,调用get方法,通知发布者执行addSub方法,把所有的watcher收集起来。
  • const reg=/{{(.+)}}/
  • .匹配任何内容(除了特殊符号)
  • * 0个或多个
  • + 1个或多个
  • {}在正则中有特殊的含义
  • regex:正则表达式
  • if(reg.test(node.nodeValue)){const name=RegExp.$1.trim()
  • trim()去除左右两边字符串
  • $1是用来拿const reg=/{{(.+)}}/中的()的,()是进行分组的,就是去拿你第一组的东西。如果还有个()可以通过$2拿
<body>
    <!-- 
    1.app.message修改数据,Vue内部是如何监听message数据的改变
    Object.defineProperty->监听对象属性的改变
    2.当数据发生改变,Vue是如何知道要通知那些人,界面发生刷新
    发布订阅者模式
     -->

    <!-- 每次属性发生改变的时候,在set里面拿到dep,在之前所有监听的对象都已经加到dep里面了,一旦有一天属性发生改变,去通知那些人dep.notify() -->
    <!-- 多个订阅者对象添加到发布者里面,之后值变了,发布者调用自己notify,立马通知所有订阅者更新自己的update -->
    <div id="app">
        {{message}}
        {{message}}
        {{message}}

        {{name}}
    </div>
    <script>
        // 内部相当于做这个事情
        const obj = {
            message: '哈哈哈',
            name: 'why'
        }
        Object.keys(obj).forEach(key => {
            let value = obj[key]

            // 虽然原来有这个属性,但是不好监听,把原来的属性全部重新定义一下
            Object.defineProperty(obj, key, {
                set(newValue) {
                    console.log('监听' + key + '改变');
                    // 告诉谁了?谁用告诉谁?谁在用了?
                    // 根据解析html代码,获取到哪些用的人在用我们的属性 比如{{name}}
                    // 张三、李四、王五 div那里 {{message}}命名叫张三
                    //div那里谁用{{message}}一次,就就会调一次get
                    // 一旦发现newValue发生改变,通知张三李四王五这三人,即这三个人订阅属性的改变
                    value = newValue

                    // dep.notify()
                },
                get() {
                    console.log('获取' + key + '对应的值');
                    return value
                    // 张三用一次调一次get,如果值改变到时候要通知他们->update  界面更新
                    // 李四:get
                    // 王五:get
                }
            })
        })
        // obj.name = 'kobe'//这里重新赋值了,会执行里面的set,内部有监听函数就监听到值改变了
        // 你在set方法里面监听到值改变了,就要通知使用这些值的人,谁用它了,赶快把界面改一下吧
        // obj.name//会执行后面那个 获取name对应的值obj.name = 'why'照样执行set

        // 发布者订阅者
        // Dep处理订阅者依赖收集,各个Watcher处理新值的更新
        // 发布者
        class Dep {//记录所有订阅者
            constructor() {
                this.subs = []//记录谁订阅我们的属性
            }
            adddSub(wather) {//我怎么知道订阅者在哪里
                this.subs.push(wather)//之后订阅者只要用了get,都会创建个对象,都会调用这个方法,都把对象被加到数组里
            }
            notify() {//如果有一天值发生改变了,即执行set了,则set里面调用 dep.notify()
                this.subs.forEach(item => {
                    item.update()
                })
            }
        }
        // 订阅者
        class Watcher {//创建张三李四王五,根据类创建出来对象
            constructor(name) {
                this.name = name;
            }
            update() {
                console.log(this.name + '发生update');
            }
        }
        const dep = new Dep()

        // 之后谁一旦调用get了,就在get里面创建个watcher
        //意味着张三用了一次get,就new了个张三,到时候就知道张三用我们这个属性了
        const w1 = new Watcher('张三')
        //我们的订阅者就被发布者记录了,张三李四王五全被加到数组里面了
        dep.addSub(w1)

        const w2 = new Watcher('李四')
        dep.addSub(w2)
        const w3 = new Watcher('王五')
        dep.addSub(w3)

        dep.notify()

    </script>
    <script src="./node_modules/vue/dist/vue.js"></script>
    <script>
        const app = new Vue({
            el: '#app',
            data: {
                message: '哈哈哈',
                name: 'why'
            }
        })
    </script>

</body>
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-14 10:46:04  更:2021-07-14 10:48:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/9 9:02:22-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码