创建项目
这里我们使用脚手架3进行开发
划分目录结构
一般在src文件夹下创建几个文件夹
- assets资源,比如css样式,img图片等
- views:存放一些大的视图,比如首页视图,分类视图等
- components:一般存放一些公共组件,比如有的组件既在首页使用,又在分类视图里使用,就存放在’组件’文件夹中。
- common(公共的组件):指可以在多个项目中重复使用的组件
- content(与当前项目业务相关的组件):只可在当前项目中使用的
- router:路由相关的js文件
- store: vuex公共状态管理相关的文件
- network:网路相关文件的封装
- common:公共的js文件,如一些常量(const.js)、封装一些工具的方法(utils.js)等等
css文件的引入
现在大部分项目都会引入一个名为normalize.css的样式表,它是一个可以定制的CSS文件,它让不同的浏览器在渲染网页元素的时候形式更统一。
Normalize.css 能干什么?
- 保留有用的默认值,不同于许多 CSS 的重置
- 标准化的样式,适用范围广的元素。
- 纠正错误和常见的浏览器的不一致性。
- 一些细微的改进,提高了易用性。
- 使用详细的注释来解释代码。
除此之外,一般还会建一个自己的css样式,名为base.css
,比如padding、margin等等。
:root
作用是获取根元素html:root { --color-text: #666; --color-high-text: #ff5777; --color-tint: #ff8198; --color-background: #fff; --font-size: 14px; --line-height: 1.5; }
其中的
--color-text
相当于是定义变量,之后设置颜色等属性可以使用color: var(--color-text);
配置文件
别名:vue.config.js
正如前面所说,引入一些文件时需要提供绝对路径,这会比较麻烦。
别名注意事项
所以我们使用别名,首先需要在src目录下新建自己的vue.config.js
文件。
- 注意:CLI3可以互相调用别名,比如
'assets': '@/assets',
- CLI4中不支持相互调用别名,比如
'assets': 'src/assets',
;然后在引入时,需要在路径最前面加上@/
保存之后,即可直接使用别名。
注意:在css中使用,需要加上
~
,并且不要写成字符串{ background: url(~@/assets/img/04_2.jpg); background: url('~@/assets/img/04_2.jpg');//错了,这里有个坑,不能写成字符串,我就是因为这样写错了 }
html中使用,可以加入
~
也可以不加入~
。
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
代码规范:.editorconfig
用于确保小组代码编写的统一。
tabbar引入和模块划分
像我们之前抽离的两个组件MainTabBar
和tabbar
,就可以引入其中了,问题来了,这两个组件应该放在common和content哪个文件夹中呢?
上文提过,common是放公共组件的,而content是存放与我们项目业务逻辑相关的组件的。因此,tabbar应该放在common文件夹下,MainTabBar放在content文件夹下。
此时,别名的好处就体现出来了。使用@/assets
就可以代替../../...
去寻找绝对路径。
此时,在App.vue
中导入该组件,此时就需要安装vue-router插件,npm install vue-router --save
。
- 在router文件夹下创建index.js,安装插件,创建router对象,进行导出
- 去main.js中进行挂载
网页图标的修改
在public文件夹里加入心仪的图标即可。
此处有一个原始模板语法<% BASE_URL %>
,目的是为了动态获取路径。在打包过后会替换为当前路径。
首页开发
首页导航栏的封装和使用
- 首先,考虑该组件应该封装到哪个目录下?
因为多个页面都需要用到该组件,所以封装到components文件夹下;并且导航栏大部分项目都会用到,所以放到common目录下。
注意:文件夹一般为小写,组件名称为驼峰式。
此时导航栏我们使用flex
布局。需要使用到具名插槽,另外给每个插槽外包装一个class用于固定其样式和位置。
NavBar.vue如下
Home.vue如下
请求首页的多个数据
需要用到我们之前提及的axios插件,这里需要注意的是,我们重新建了一个home.js
文件,用于抽离这些函数,使得函数之间尽可能的分离。
轮播图的组件
调用写好的组件,粘贴到components下common文件夹里。
此处的index.js
目的是为了导入时不需要分开导入
接着,在Home.vue
里调用该组件Swiper
但是一般情况下,Home.vue
都是存放主要组件,类似这种轮播图我们可以抽取到views
文件夹的home
文件夹里,创建childComps
文件夹,创建HomeSwiper.vue
,代码如下:
注意:
- banners数据需要通过
props
属性通过父传子获取数据
这样一来,封装起来后,更加清晰,如果要去修改轮播图的组件,只需要进入HomeSwiper.vue
中进行修改,而不回影响到其它组件
多体会这种封装思想,不要把代码都写到一块。
推荐信息的展示
同样的,在childComps文件夹下新建一个vue组件。
文件命名:可以是
RecommendView
,但有的公司会要求在文件名最前面添加上所在界面名,比如我们要放在Home界面下,就命名成HomeRecommendView
;有的公司还要求加上公司名缩写,比如TXRecommendView
等等。
通过v-for
等属性进行显示:
再调试下样式(使用flex布局)。
FeatureView的封装
因为功能相似,所以这里简便处理,使用图片。
TabControl的封装
只需要修改文字的情况下,没有必要设置插槽,可以通过传递数组,来提供文字的元素。
粘性定位:会使组件移动到某个位置后停留不动。
.tab-control { position: sticky; top: 44px; }
保存商品的数据结构设计
根据TabControl的样式,可知我们需要保存三种数据(流行、新款、精选)。我们可以设计如下方式:记录当前页面以及当前list中已经请求了多少条数据。
首页数据的请求与保存
此时应该去网络处理的文件夹(network)编写home.js
。
回到Home.vue
中,编写create函数,此时我们会发现一个问题:
- create函数的作业是:当组件一旦创建完成后所执行的函数。
- 所以在该函数中做事情的时候,最好只在里面写主要逻辑,详细逻辑不要在里面实现,一般抽取出来。
- 因此,可以在
methods
属性中编写发送请求的函数,然后在create
中调用她。
将新数组的元素全部存入另一个数组
如上图所示,可以使用push方法
js扩展运算符(spread)是三个点(…)
作用:将一个数组转为用逗号分隔的参数序列。// ES6 的写法 Math.max(...[14, 3, 77]) // 等同于 Math.max(14, 3, 77);
此时我们可以将请求到的数据保存到我们先前创建的变量中:
由于我们暂时只请求了pop
的数据,所以运行后我们只在pop变量中发现存储了30条数据。
首页商品数据的展示
数据已经获取成功,接下来就是将其展示在首页。
TabControl点击切换商品
子传父事件,使用$emit
当然,需要创建变量,这里我们是currentType
Better-Scroll滚动插件
BetterScroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。它的核心是借鉴的 iscroll 的实现,它的 API 设计基本兼容 iscroll,在 iscroll 的基础上又扩展了一些 feature 以及做了一些性能优化。
生命周期回顾
生命周期 | 是否获取dom节点 | 是否可以获取data | 是否获取methods |
beforeCreate | 否 | 否 | 否 |
created | 否 | 是 | 是 |
beforeMount | 否 | 是 | 是 |
mounted | 是 | 是 | 是 |
created: 在模块渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
mounted: 在模块渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
因此,需要注意的点是,因为在 Vue 模板中列表渲染还没完成时,是没有生成列表 DOM 元素的,所以需要在确保列表渲染完成以后,才能创建 BScroll 实例,因此在 Vue 中,初始化 BScroll 的最佳时机是 mouted 的 nextTick。
安装和使用
npm install better-scroll –save
进行安装,官方文档指路,笔记在这
我们用分类界面做演示:
- 由生命周期可知,我们要在mounted中才能获取dom节点。
效果如下:具有弹簧效果,更加顺滑。
BScroll的封装以及使用
如果直接用BScroll插件在源码上更改,当改插件不再进行维护时我们就需要对每一个使用了该插件的源代码进行修改,会非常麻烦。所以我们进行封装,需要修改时只需要对一个文件进行修改即可。
因为多个页面都可以用该组件,所以将其放置在公共组件文件夹下
注意
- 尽量避免直接查找dom元素,比如用
document.querySelector('.wrapper')
去查找wrapper,原因是多个页面里可能会有多个类名为wrapper的,会导致不能明确使用的是哪一个。
在Vue中想要明确拿到哪个组件,给组件绑定
ref
。回顾:
- ref如果是绑定在组件中的,那么通过
this.$refs.refName
获取的是一个组件对象- ref如果是绑定在普通的元素中,那么通过
this.$refs.refName
获取的是一个元素对象
- 样式中vh: viewport height(视口高度);
封装后的文件如下:
注意此处scroll标签的类标签名也是"content",与
Scroll.vue
中的类标签名并不会冲突,因为它的样式表为scoped
因为现在滚动利用的是BScroll而不是原生的滚动,系统是无法检查tabControl所在的位置,所以.tab-control
中的sticky是无效的了,
回到顶部的按钮
因为和我们的业务有关,所以封装到components下的content文件夹下,命名为backTop
。
如果要监听某个组件的点击,需要加上修饰符
.native
- 在我们需要监听一个组件的原生事件时,必须给对应的事件加上
.native
修饰符,才能进行监听。
这里我们需要使用到的是scrollTo方法
scrollTo(x, y, time, easing, extraTransform)
- 参数:
- {number} x 横轴坐标(单位 px)
- {number} y 纵轴坐标(单位 px)
- {number} time 滚动动画执行的时长(单位 ms)
- {Object} easing 缓动函数,一般不建议修改,如果想修改,参考源码中的
packages/shared-utils/src/ease.ts
里的写法 - 只有在你想要修改 CSS transform 的一些其他属性的时候,你才需要传入此参数,结构如下:
BackTop的显示和隐藏
根据之前的经验,若想要图片根据位置进行显示或隐藏,那么就可以根据probeType
获取当前x,y值。
Scroll.vue中:
Home.vue中,
完成上拉加载更多
调用pullingUp方法,上拉加载更多。
但是会遇到一个bug,就是上拉拖动不了。
原因在于初始设计的固定高度与最终使用的高度不一致,有出入。
- 如wrapper高度只有400px,而插入的内容有1000px,因此BScroll会帮助计算滚动的范围,即600px。
- 但是我们此处因为使用了接口,需要加载数据
GoodsListItem组件
(图片标题等)。但可能由于网络或其它原因,图片没有加载出来(异步处理),所以计算插入内容(item)的高度时没有包括图片的高度。 - 如举的例子,此时计算出来的高度比如只有600px,所以滚动的范围只有200px。
解决方法也很简单:
- 监听图片加载完成后,在某个地方拿到scroll对象
refresh
一下,刷新的作用是重新计算最新可滚动的区域。
- 监听图片何时加载完
- 调用
this.$refs.scroll.scroll.refresh()
一. FeatureView
- 独立组件封装FeatureView
- div>a>img
二. TabControl
- 独立组件的封装
- props -> titles
- div>根据titles v-for遍历 div -> span{{title}}
- css相关
- 选中哪一个tab, 哪一个tab的文字颜色变色, 下面border-bottom
- currentIndex
三. 首页商品数据的请求
3.1. 设计数据结构, 用于保存数据
goods: {
pop: page/list
new: page/list
sell: page/list
}
3.2. 发送数据请求
- 在home.js中封装getHomeGoods(type, page)
- 在Home.vue中, 又在methods中getHomeGoods(type)
- 调用getHomeGoods(‘pop’)/getHomeGoods(‘new’)/getHomeGoods(‘sell’)
- page: 动态的获取对应的page
- 获取到数据: res
- this.goods[type].list.push(…res.data.list)
- this.goods[type].page += 1
goods: {
pop: page1:/list[30]
new: page1/list[30]
sell: page1/list[30]
}
四. 对商品数据进行展示
4.1. 封装GoodsList.vue组件
- props: goods -> list[30]
- v-for goods -> GoodsListItem[30]
- GoodListItem(组件) -> GoodsItem(数据)
4.2. 封装GoodsListItem.vue组件
- props: goodsItem
- goodsItem 取出数据, 并且使用正确的div/span/img基本标签进行展示
五. 对滚动进行重构: Better-Scroll
5.1. 在index.html中使用Better-Scroll
- const bscroll = new BScroll(el, { })
- 注意: wrapper -> content -> 很多内容
- 1.监听滚动
- probeType: 0/1/2(手指滚动)/3(只要是滚动)
- bscroll .on(‘scroll’, (position) => {})
- 2.上拉加载
- pullUpLoad: true
- bscroll .on(‘pullingUp’, () => {})
- 3.click: false
- button可以监听点击
- div不可以
5.2. 在Vue项目中使用Better-Scroll
- 在Profile.vue中简单的演示
- 对Better-Scroll进行封装: Scroll.vue
- Home.vue和Scroll.vue之间进行通信
- Home.vue将probeType设置为3
- Scroll.vue需要通过$emit, 实时将事件发送到Home.vue
六. 回到顶部BackTop
6.1. 对BackTop.vue组件的封装
6.2. 如何监听组件的点击
- 直接监听back-top的点击, 但是可以直接监听?
- 不可以, 必须添加修饰.native
- 回到顶部
- scroll对象, scroll.scrollTo(x, y, time)
- this.$refs.scroll.scrollTo(0, 0, 500)
6.3. BackTop组件的显示和隐藏
- isShowBackTop: false
- 监听滚动, 拿到滚动的位置:
- -position.y > 1000 -> isShowBackTop: true
- isShowBackTop = -position.y > 1000
七. 解决首页中可滚动区域的问题
- Better-Scroll在决定有多少区域可以滚动时, 是根据scrollerHeight属性决定
- scrollerHeight属性是根据放Better-Scroll的content中的子组件的高度
- 但是我们的首页中, 刚开始在计算scrollerHeight属性时, 是没有将图片计算在内的
- 所以, 计算出来的高度是错误的(1300+)
- 后来图片加载进来之后有了新的高度, 但是scrollerHeight属性并没有进行更新.
- 所以滚动出现了问题
- 如何解决这个问题了?
- 监听每一张图片是否加载完成, 只要有一张图片加载完成了, 执行一次refresh()
- 如何监听图片加载完成了?
- 原生的js监听图片: img.onload = function() {}
- Vue中监听: @load=‘方法’
- 调用scroll的refresh()
- 如何将GoodsListItem.vue中的事件传入到Home.vue中
- 因为涉及到非父子组件的通信, 所以这里我们选择了事件总线
- bus ->总线
- Vue.prototype.$bus = new Vue()
- this.bus.emit(‘事件名称’, 参数)
- this.bus.on(‘事件名称’, 回调函数(参数))
- 问题一: refresh找不到的问题
- 第一: 在Scroll.vue中, 调用this.scroll的方法之前, 判断this.scroll对象是否有值
- 第二: 在mounted生命周期函数中使用 this.$refs.scroll而不是created中
- 需求: 对于refresh非常频繁的问题, 进行防抖操作
- 防抖debounce/节流throttle
- 防抖函数起作用的过程:
- 如果我们直接执行refresh, 那么refresh函数会被执行30次.
- 可以将refresh函数传入到debounce函数中, 生成一个新的函数.
- 之后在调用非常频繁的时候, 就使用新生成的函数.
- 而新生成的函数, 并不会非常频繁的调用, 如果下一次执行来的非常快, 那么会将上一次取消掉
八. 上拉加载更多的功能
- 监听滚到底部
九. tabControl的吸顶效果
9.1. 获取到tabControl的offsetTop
- 必须知道滚动到多少时, 开始有吸顶效果, 这个时候就需要获取tabControl的offsetTop
- 但是, 如果直接在mounted中获取tabControl的offsetTop, 那么值是不正确.
- 如何获取正确的值了?
- 监听HomeSwiper中img的加载完成.
- 加载完成后, 发出事件, 在Home.vue中, 获取正确的值.
- 补充:
- 为了不让HomeSwiper多次发出事件,
- 可以使用isLoad的变量进行状态的记录.
- 注意: 这里不进行多次调用和debounce的区别
9.2. 监听滚动, 动态的改变tabControl的样式
- 问题:动态的改变tabControl的样式时, 会出现两个问题:
- 问题一: 下面的商品内容, 会突然上移
- 问题二: tabControl虽然设置了fixed, 但是也随着Better-Scroll一起滚出去了.
- 其他方案来解决停留问题.
- 在最上面, 多复制了一份PlaceHolderTabControl组件对象, 利用它来实现停留效果.
- 当用户滚动到一定位置时, PlaceHolderTabControl显示出来.
- 当用户滚动没有达到一定位置时, PlaceHolderTabControl隐藏起来.
十. 让Home保持原来的状态
10.1. 让Home不要随意销毁掉
- keep-alive
10.2. 让Home中的内容保持原来的位置
- 离开时, 保存一个位置信息saveY.
- 进来时, 将位置设置为原来保存的位置saveY信息即可.
- 注意: 最好回来时, 进行一次refresh()
十一.推荐数据的展示
- 请求推荐数据
- GoodsList展示数据
十二.mixin的使用
- 创建混入对象:const mixin = {}
- 组件对象中:mixins[mixin]
十四.标题和内容的联动效果
14.1. 点击标题,滚动到对应的主题
- 在detail中监听标题的点击,获取index
- 滚动到对应的主题:
- 获取所有主题的offsetTop
- 问题:在哪里才能获取到正确的offsetTop
- 1.created肯定不行,压根不能获取元素
- 2.mounted也不行,数据还没有获取到
- 3.获取到数据的回调中也不行,DOM还没渲染完
- 4.$nextTick也不行,因为图片的高度没有被计算在内
- 5.在图片加载完成后,获取的高度才是正确
14.2.内容滚动,显示正确的标题
普通做法:
hack做法:
十五.顶部工具栏的封装
十六.详情页的回到顶部
- home.vue和detail.vue回到顶部:mixin
十七.点击加入购物车
17.1监听加入购物按钮的点击,并且获取商品信息
- 监听
- 获取商品信息:iid/price/image/title/desc
17.2将商品添加到Vuex中
- 安装Vuex
- 配置Vuex
- 定义mutations,将商品添加到state.cartList
- 重构代码
- 将mutations中的代码抽取actions中(定义两个mutations)
- 将mutations/actions单独抽取到文件中
十八.购物车的展示
18.1购物车的导航栏的展示
18.2购物车商品的展示
- CartList -> Scroll(样式问题)
- CartListItem -> CheckButton
18.3商品的选中和不选中切换
- 修改模型对象,改变选中和不选中
18.4底部工具栏的汇总
- 全选按钮
- 计算总价格
- 去计算
十九.购物车全选按钮
- 显示的转态
- 判断是否有一个不选中,全选
- 点击全选按钮
- 如果原来都是选中,点击一次,全部不选中
- 如果原来都是不选中(某些不选中),全部选中
二十.添加购物车弹窗
20.1Vuex的补充
- Actions可以返回一个Promise
- mapActions的映射关系
Toast(吐司)封装
- 普通封装方式
- 插件封装方式
二十一.补充一些细节
21.1 fastClick减少点击延迟
- 安装fastclick
- 导入
- 调用attach函数
22.2图片的懒加载
- 什么是图片懒加载
- 图片需要显示在屏幕上时再加载图片
- 使用vue-lazyload
- 安装
- 导入
- Vue-use
- 修改img.src -> v-lazy
22.3px2vw插件使用
- 按照插件
- postcss.config.js中配置