一. 创建项目 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:
- 先在github中建个仓库:
- 在github上托管整个代码,需要先在github中建个仓库,把代码全放到仓库里。进入github,登录才能建仓库。
- 登录后点右上角+。名字:supermall 描述:a vuejs supermall 。public。 不需要初始化README,因为通过脚手架创建的项目有README(里面写了怎么用这个项目)。不需要添加.gitignore 已经有了。许可协议,一般情况下比较开放的选择MIT,如果别人用必须声明选择apache。
- 因为先在vscode中建的项目,项目创建完之后。远程创建完仓库之后。
- 把本地的项目和远程的仓库联系起来。两种方法:
-
- 方法一:
- 先从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到远程仓库就行了
-
- 方法二:
- 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是函数形式。
-
- 导入之前代码
-
项目的模块划分: 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要跳到某个路由。 -
- 路由相关的应用
-
路由搭建:所以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代码的。
八. 封装导航独立组件
-
- 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,可以设置出文字上下居中效果。只有文字和行高也能撑起来这个盒子,可以不需要设置高。
-
- 在首页使用导航独立组件
- 在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
-
---------------------------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的方式去引用。
- 下载源码方法,去github中(以后找开源项目的源码都可以去github上,找最新的tags,找到dist文件夹(dist里面就是最新打包的源码)。即找到tags,选了v1.13.0 ,clone or download,打开dist文件夹,里面有个bscroll.js文件
- 在项目中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
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
- 如果不封装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有依赖

- 对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. 非父子组件的通信方法总结

-
通过子传父把事件传给GoodsList.vue,之后让这个组件把事件传给Home.vue(麻烦) -
搞个vuex对象,vuex记录了一些状态,每一次一旦发生这个事件,也就是图片加载完成的话,就改变vuex里面的某一个属性,再让首页里面引用这个vuex属性,并且实时监听vuex属性的改变,一旦属性发生改变就执行this.$refs.scroll.scroll.refresh.--------(vuex可以保存整个应用程序的状态 GoodsListItem.vue 通过this.$store就可以改vue里面的属性了,home.vue监听这个属性。)这个方法无论多少层都能通信。 -
事件总线。公共的东西。事件总线和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的理解
- offsetTop:元素到offsetParent顶部的距离
- offsetParent:距离元素最近的一个具有定位的祖宗元素(relative,absolute,fixed),若祖宗都不符合条件,offsetParent为body。
- 在本案例中<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')
}
}
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信息即可.
- 在这里可以用这两个生命周期函数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标签放图片,或者直接<;实现后退图片<,或者用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>
四. 商品基本信息展示
- 先获取这部分数据
- 看一下数据都在各个地方,如果子组件这样接收数据 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)
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进行格式化,转成对应的字符串
- data.getFullYear()+data.getMonth()+…
- 开发中因为把日期格式化太常见了,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刷新这不合理。
- 可以通过路由判断,有/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");
}
},
- 另一种思路都发出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也不行,数据还没有获取到
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)
}
}
十六. 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中写的名字。这里搞个对象,你自己写个你想叫的名称
<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布局三栏
五. 购物车的全选按钮
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);
}
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>
|