仿饿了么-开发笔记
一、移动端开发准备
1.引入reset.css
1.1 下载到本地:下载网址 cssreset.com 或者 cssdeck.com
1.2 外部引入
<link rel="stylesheet" href="http://...css">
2.viewport设置
<meta name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">
3.解决点击响应延迟0.3s问题
3.1
0.3s延迟问题是由双击缩放引起的,当用户点击后浏览器会等待0.3s判断是单击事件还是双击事件,
在viewport中设置user-scalable=no禁止缩放就能消除这0.3s的延迟
具体参考文章:https://blog.csdn.net/qq_37730829/article/details/109155753
3.2 也可以通过下面的方式来消除0.3s延迟,在index.html中添加:
<script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script>
<script>
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
</script>
4.大小适配 px转rem
网页的单位:
(1)px 相对长度单位,px是相对屏幕的分辨率的
特点:以px为单位的字体大小几乎是固定的,可以理解成绝对大小
(2)em 相对长度单位, 相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。
特点:em会继承父级元素的字体大小
注:任意浏览器的默认字体大小都是16px。所有未经调整的浏览器都符合: 1em=16px。那么10px=0.625em,为了简化font-size的换算,需要在css中的body选择器中声明font-size=62.5%,这就使em值变为 16px62.5%=10px。这样,在单位换算时就很方便:设x是以px为单位的数值,y是以em为单位的数值,则x(px)/10=y(em)*
(3)rem CSS3新增的一个相对长度单位(root em),rem和em最大的区别在于:rem是相对于html
根元素的
特点: 这个单位可谓集相对大小和绝对大小的优点于一身,通过它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。
注:已经知道rem是相对于html根元素的font-size大小的单位,那么为了适配不同的移动设备,需要自动更改html的font-size,此时有两种方式,一种是通过js控制,另一种是通过CSS3的媒体查询。
步骤1:html根元素的font-size自适应
既然已经知道移动端适配是采用rem单位,即相对html根元素的font-size来控制字体大小,那么首先就要实现html根元素font-size自适应,当屏幕宽度变化时,根元素的font-size大小随之变化。
方式一:js更改font-size
在html页面中添加js:
// 假设移动端屏幕宽375px, 则1rem=100px .16rem=16px
// 假设移动端屏幕宽414px, 则1rem=110.4px .16rem=17.664px
function setFS(){
let htmlWidth = document.documentElement.clientWidth || document.body.clientWidth;
var fontSize = htmlWidth / 3.75 ;
if(fontSize>200){
fontSize = 200
}
document.getElementsByTagName('html')[0].style.fontSize = fontSize +'px';
}
setFS()
window.onresize = setFS
方式二:通过css3媒体查询来调整font-size
在html页面添加style:
// 屏幕宽>=1200px (电脑)
@media screen and (min-width: 1200px) {
html {
font-size: 200px;
}
}
// 屏幕宽:960px~1199px (电脑)
@media screen and(min-width: 960px) and (max-width: 1199px) {
html {
font-size: 180px;
}
}
// 屏幕宽:768px~959px (平板)
@media screen and(min-width: 768px) and (max-width: 959px) {
html {
font-size: 150px;
}
}
// 屏幕宽:480px~767px (平板)
@media screen and(min-width: 480px) and (max-width: 767px) {
html {
font-size: 120px;
}
}
// 屏幕宽<=479px (手机)
@media screen and (max-width: 479px) {
html {
font-size: 100px;
}
}
body {
font-size: .12rem;
line-height: 1.5;
}
步骤2:px转rem
实现步骤1后,开发中就要使用rem单位了,如果想使用px单位,则需要进行转换。实现设计稿的px单位换算成rem的两种方式
方式一:插件方式
参考https://www.shuzhiduo.com/A/gVdnPoOEJW/
安装开发依赖postcss-pxtorem
npm install postcss-pxtorem -D
在postcss 的配置文件 .postcssrc.js(或者 postcss.config.js )中修改
module.exports = {
plugins: {
'autoprefixer': {
browsers: ['Android >= 4.0', 'iOS >= 7']
},
'postcss-pxtorem': {
rootValue: 16,
propList: ['*']
}
}
}
这里我这样修改后,vue报错了,运行不起来,猜测是版本的问题(具体原因不知道)!
方式二:手动
创建一个全局scss文件 :util.scss,在文件中定义一个工具函数:
@function px2rem($px) {
$n: 100;
@return ($px/$n)+rem;
}
在Vue中使用scss函数:(20px -> 0.2rem)
<style lang="scss" scoped>
@import "../assets/scss/util.scss";
.title {
font-size: px2rem(20);
}
</style>
二、样式
1.重置浏览器的html原本样式
2.定制(覆盖)UI组件样式
3.使用sass/less 定义变量、函数、样式片段。
例如,建立一个全局样式文件mixin.scss,使用sass定义:
// 定义颜色变量
$primary:#3190e8;
$grey:#aaa;
$black:#333;
$dark:#222;
$white:#fff;
$bgGrey:#EDEDED;
// 定义混入(样式片段)
@mixin text-ellipse{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// 该方法适用于WebKit浏览器及移动端, 如果文本是数字的话这种做法还会失效
@mixin two-line-ellipsis{
overflow:hidden;
text-overflow:ellipsis;
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:2;
}
在某个vue页面中使用:
<style lang='scss' scoped>
@import '../../styles/mixin.scss';
.shopname{
display: inline-block;
width: 2.4rem;
@include text-ellipse;
}
</style>
4.UI框架提供的全局样式利用起来
比如vant定义了一些样式类,例如:
<!-- 最多显示一行 -->
<div class="van-ellipsis">这是一段最多显示一行的文字,多余的内容会被省略</div>
5)JS中使用sass变量
// 定义颜色变量
$primary:#3190e8;
:export {
primary: $primary
}
定义并导出sass变量后,在js中使用
import varStyles from '../../../styles/var.scss'
console.log(varStyles.primary) //#3190e8
三、路由vue-router
1.vue-router 从v3到v4
注意vue-router v3的写法与vue-router v4的写法不同,从v3 到 v4的变化
- new Router 变成 createRouter()
- mode 变成 history
具体参考 vue-router v3迁移到v4
v3路由写法:
//v3路由写法:
const router = new VueRouter({
mode:'hash',
routes
})
2.用params传参的弊端
在vue-router中,可以通过params来传很多参数,但这是有弊端的,主要体现在H5中
路由:
{
path: '/example/:id',
name: 'example',
component: Example
}
跳转路由:
this.$router.push({
name: 'example',
params: { //这个params有除了id以外的数据
id: '123',
name:'anything',
desc:'anything'
}
})
浏览器显示的地址:http://1localhost:8080/#/example/123
。此时如果我对浏览器进行刷新,那么params中的name和desc两个参数就会丢失。
总结:在vue开发的H5应用中不要用params传多余(不显示在路径中的)参数。如果是将H5打包成App,用户不能控制刷新浏览器的话还是可以这么操作的。
四、组件
1.组件设计原则
1)视图与数据分离
无论是公共组件,还是页面内的特有组件,都应该遵循视图和数据分离,这样可维护性更高。把视图显示交给子组件,把异步获取数据操作交给子组件调用者,组件调用者获取到数据后传给子组件。
案例:有一个复杂的页面,现打算把复杂页面拆成A,B两部分,把B放到子组件实现,那么建议此时将获取数据的操作放在A中获取,再传给B。因为如果在B中获取数据,万一后面需求变动要求A也能拿到B中的部分数据那么此时就完蛋了。
2.实现组件的双向绑定 v-model
在实现购物车的计数器时,如何控制数量呢?
首先分析通过props把数量count传入counter
组件,这个时候如果要更新count,不能在counter
内部修改,因为props是单向数据流,Vue不允许这样修改。此时只能让counter
通过$emit()来触发事件,父组件绑定事件并在绑定函数里面修改count,这样做是比较麻烦的,尤其是创建的是一个counter组件列表,如下:
此时可以通过实现组件的v-model来进行数据双向绑定,从而实现在counter
内部进行数据修改。
首先需要学习Vue的model
下面给出counter的实现:
<template>
<div class="elm-counter">
<img v-if="value!=0" class="elm-counter-icon"
src="../../../images/icon-sub.png"
@click="sub"
>
<span v-if="value!=0" class="elm-counter-value">{{value}}</span>
<img src="../../../images/icon-add.png"
class="elm-counter-icon"
@click="add"
>
</div>
</template>
<script>
export default {
model:{
prop:'value',
event:'updateValue' //相当于声明value来接受v-model的值,同时监听'updateValue'事件修改value的值
},
props:{
// 数量
value: 0,
// 规格
specification:{}
},
methods:{
// +1
add(){
this.$emit('updateValue', this.value+1)
},
// -1
sub(){
if(this.value<1)
return
this.$emit('updateValue', this.value-1)
}
}
}
</script>
<style lang='scss' scoped>
.elm-counter{
.elm-counter-icon{
width: .19rem;
height: auto;
vertical-align: middle;
}
.elm-counter-value{
display: inline-block;
height: .2rem;
line-height: .2rem;
margin: 0 .06rem;
vertical-align: middle;
}
}
</style>
五、vuex应用
1.store中的一些概念
概念 | 解释 |
---|---|
state | 状态树,唯一数据源。 |
getters | store的计算属性,计算结果会被缓存。可以通过闭包的方式传参。 |
mutations | 更改state的方式,定义如何修改state。修改state的唯一方式就是提交mutation。devtools会捕捉mutations里面的快照,Vuex要求mutations内的代码必须是同步的。 |
actions | actions用来执行异步代码,可以通过dispatch方法发布一个action。 |
modules | 分模块 |
2.mutations的两种提交风格:
1)对象风格
this.$store.commit({
type:'updateXX',
payload:data
})
2)payload风格
this.$store.commit('updateXX', payload)
3.Vue响应式系统 & mutation最佳实践
1)数组不能通过索引来修改,这样是不能实现响应式的。
2)Vue.set(obj, attr, value) 添加属性,同时添加到响应式系统中
3)Vue.delete(obj, attr) 删除属性,同时从响应式系统中删除
当需要添加属性并使属性加入响应式系统有两种方式:
方式一:用Vue.set()。
方式二:重新拷贝一份({…}或Object.assign)
Vue.set()存在弊端!!更推荐重新拷贝的方式
案例:添加至购物车
store里面:
const state = {
// 购物车记录
shopcarts:{},
}
const mutations = {
// 添加商品
[ADD_PRODUCT](state,{restaurant_id, product}){
const shopcarts = state.shopcarts
const shopcart = shopcarts[restaurant_id] || {}
shopcart[product.id] = product
// Vue.set(state.shopcarts, restaurant_id, shopcart) //方式一:有弊端,当购物车新增时mapState的数据不会响应
shopcarts[restaurant_id] = shopcart
state.shopcarts = {...shopcarts} //方式二:让shopcarts发生变化,这样在mapState中的数据才可以响应
},
}
购物车页面:
export default{
computed:{
...mapState({
shopcarts: state=>state.cart.shopcarts
}),
// 购物车总费用
cartTotalPrice(){
const shopcart = this.shopcarts[this.shopId]
let sum = 0
for(let i in shopcart){
sum = sum + (shopcart[i].price * shopcart[i].quantity)
}
return sum
}
},
}
在上述案例中,如果使用方式一,当从第二次起添加新商品到购物车,由于state.shopcarts的引用没有变(相当于Vue认为shopcarts没有改变),所以mapState()映射到computed中的数据不会发生改变,会出现下面的现象:
4.将mutations和actions的type进行常量化
可以避免type书写错而发生错误。
5.为何要actions这一层?
背景:Vuex建议将异步操作放在action中,页面通过分发action来实现修改数据,action的本质就是执行异步操作然后提交mutation。
问:那么为什么不直接这样呢,在页面中执行异步操作然后提交mutation,省去action这一层?
答:为了分离复用。
1)假如一个非常复杂的异步业务,如果写在页面中会使页面变大,分离出来更清晰。
2)提交mutation肯定是修改共享状态,这说明这个业务可能不止一个页面在使用,那么如果把这个复杂的异步业务分离出来成单独一个公共函数(即action),能达到复用效果。
6.记一次错误:在mutations外修改state错误
这是因为store.state中的值被你的v-model绑定并发生了修改。
解决办法:
方法1) 在将v-model相关的对象传给mutations修改时,请将对象深拷贝一份传过去。
方法2) 你在mutations中不要直接将对象添加到state中,而是将对象解构然后赋值给一个新建对象,最后将新建对象添加到state中
7.state中对象优先还是数组优先?
凡是需要通过id来进行索引的,用对象代替数组,因为此时检索数组要遍历,而检索对象直接引用(映射)。
六、实践
1.超小字体
由于浏览器支持的最小字体大小是12px,如果想要更小字体可以通过缩小实现:
.text{
transform:scale(.n)
}
2.滚动吸顶
分类菜单区域和长列表在一个div里面滚动,当长列表的标题栏触顶时,自动吸顶,这是很常见的一种布局,如下图所示:
使用van-sticky实现的代码:
<!-- 你的头部组件,假设是0.45rem -->
<header></header>
<div class="scroller">
<!-- 分类菜单 -->
<van-swipe :loop='false'>
<van-swipe-item>
1
</van-swipe-item>
<van-swipe-item>
2
</van-swipe-item>
</van-swipe>
<!-- 长列表的标题栏 -->
<van-sticky offset-top="0.44rem">
<div class="list-header">
<span>附近商家</span>
</div>
</van-sticky>
<!-- 你的长列表 -->
<my-list></my-list>
</div>
<!-- 你的底部导航,假设是50px -->
<footer></footer>
<style>
//样式
.scroller{
flex: 1;
margin-bottom: 50px;
overflow: auto;
}
</style>
3.处理 ios端 滚动冲突
问题描述:当app内部存在滚动区域时,有时滚动会和ios的滚动缓冲发生冲突,比如出现头部Header滑出可视区域(如果Header没有fixed).
初步探讨解决方案:在每个vue页面中阻止触摸移动事件
//这种方式会导致整个页面无法滚动,不可行
mounted(){
document.getElementById("home_contianer").addEventListener('touchmove', function (e) {
e.preventDefault();
}, {passive: false});
}
比较满意的解决方案:给vue应用最顶层的元素加上fixed,这样整个vue应用就不会作为一个整体在ios上滚动了。不过此时需要特别注意:position: fixed;
后导致子元素高度或宽度异常,此时一定要将顶部元素充满整个屏幕!
vue中的顶层元素就是App.vue,下面修改App.vue
<template>
<div id="app" class="fixed">
<router-view ></router-view>
<!-- tabbar -->
<van-tabbar v-model="active" @change="handleTabChange">
<van-tabbar-item name="home" icon="home-o">外卖</van-tabbar-item>
<van-tabbar-item name="search" icon="search">搜索</van-tabbar-item>
<van-tabbar-item name="order" icon="friends-o">订单</van-tabbar-item>
<van-tabbar-item name="profile" icon="setting-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default{
data(){
return {
active: 'home'
}
},
methods:{
handleTabChange(name){
this.$router.push({name})
}
}
}
</script>
<style lang="scss">
#app{
// app-root高度占满全屏
height: 100vh;
width: 100vw;
}
.fixed{
position: fixed;
left: 0;
top: 0;
}
</style>
如上修改后,发现内容区域滚动不是特别灵敏,有时触摸滑动时没反应。
最终解决方案:
让上下固定,中间充满,溢出滑动
上面:header {position:fixed}
中间:content {overflow:auto}
下面:footer {position:fixed}
补充说明:
#app{
// height: 100vh; //避免使用100vh, 在ios端高度有点差异
height: 100%; //推荐
}
4.接口、存储都要解耦合
1)api接口解耦合
2)避免在项目中直接使用localStorage等持久化存储,因为万一哪一天项目升级更换了持久层存储方式,所有地方都要修改。因此建议自己再封装一层持久化。
5.并发请求
使用场景:实现一个功能时需要同时发出2个及以上的异步请求,成功拿到所有异步请求的结果后才能继续后面的操作(写代码)
场景一:比如说一个页面上需要多个异步请求的数据回来以后才正常显示,在此之前只显示loading图标。
场景二:比如“饿了么”项目中:筛选面板需要同时获取送货模式和商家属性,即如图两个红色方框内的内容。
此时,可以采用Promise.all()或者axios.all()来并发请求。
首先介绍一下Promise.all(),首先定义3个Promise,传给Promise.all(),只有当所有的Promise都成功那么Promise.all()就是成功的,即走then代码块,任一个Promise失败则Promise.all()失败,即走catch代码块。
let p1 = new Promise((resolve, reject) => {
resolve('p1 成功了')
})
let p2 = new Promise((resolve, reject) => {
resolve('p2 成功了')
})
let p3 = Promse.reject('p3 失败')
Promise.all([p1, p2]).then((result) => {
console.log(result) //打印 [p1 成功了', 'p2 成功了']
}).catch((error) => {
console.log(error)
})
Promise.all([p1,p3,p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 打印 'p3 失败'
})
axios.all()的运行原理和Promise.all()十分相似,下面就用axios.all()来实现场景二:
// 获取配送方式和 商家属性
getModesAndAttrs(){
this.$axios.all([
getShopDeliveryModes(),
getShopActivityAttrs()
]).then(result=>{
console.log(result);
}).catch(err=>{
console.log(err);
})
}
//打印:[Array(1), Array(6)]
//可以通过axios.spread()来进行解构
getModesAndAttrs(){
this.$axios.all([
getShopDeliveryModes(),
getShopActivityAttrs()
]).then(this.$axios.spread((modes, attrs )=>{
console.log(modes);
console.log(attrs);
})).catch(err=>{
console.log(err);
})
},
//打印:[{…}]
// [{…}, {…}, {…}, {…}, {…}, {…}]
5.svg的应用
svg是万维网标准中的一部分,是通过xml的形式来绘制矢量图
参考:html5中使用svg
场景一:作为图标,通过id使用
结合vue使用,在一个vue公共组件中定义好所有的svg图标,然后在App.vue中引入,最后可以就在各个页面中使用了,使用方式如下:
定义全局组件svg.vue
<template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0;visibility:hidden">
<defs>
<symbol viewBox="0 0 32 32" id="circle-icon">
<circle cx="30" cy="50" r="25" />
</symbol>
</defs>
</svg>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
在App.vue中引入:
<template>
<div id="app-root" >
<router-view ></router-view>
<!-- tabbar -->
<van-tabbar v-show="$route.meta.showTabbar" v-model="active" @change="handleTabChange">
<van-tabbar-item name="home" icon="home-o">外卖</van-tabbar-item>
<van-tabbar-item name="search" icon="search">搜索</van-tabbar-item>
<van-tabbar-item name="order" icon="friends-o">订单</van-tabbar-item>
<van-tabbar-item name="profile" icon="setting-o">我的</van-tabbar-item>
</van-tabbar>
<!-- 矢量图 -->
<svg-icon></svg-icon>
</div>
</template>
<script>
import svg from './components/svg'
export default{
components:{
'svg-icon':svg
},
data(){
return {
active: 'home'
}
},
methods:{
handleTabChange(name){
this.$router.push({name})
}
}
}
</script>
在页面中使用,通过xlink:href="svg图标的id"来引用
<template>
<svg style="width:.2rem;height:.2rem;opacity: 1;">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#fengniao"></use>
</svg>
</template>
场景二:当作图片引入使用
例如,在实现骨架屏时,用svg图片来实现骨架屏
<ul v-if="shops.length===0" style="width:100%">
<li v-for="item in 10" :key="item" class="list_back_li">
<img src="../../images/shopback.svg" class="list_back_svg">
</li>
</ul>
效果:
6.解决van-swipe滑动翻页和点击事件的冲突
<van-swipe :loop='false'>
<van-swipe-item>
<food-grid :geohash="geohash" :cates="cateList1"></food-grid>
</van-swipe-item>
<van-swipe-item>
<food-grid :geohash="geohash" :cates="cateList2"></food-grid>
</van-swipe-item>
</van-swipe>
组件foot-grid :
<template>
<div class="grid-box">
<div class="cate-item"
v-for="cate in cates" :key="cate.id"
@click="navFood(cate)">
<img class="cate-img" :src="cateImgOrigin+cate.image_url" alt="">
<span>{{cate.title}}</span>
</div>
</div>
</template>
在翻页时,会触发到click事件,那么要解决这个问题就是要区分是滑动事件还是点击事件,可以通过起始触点的位置来判断。其实van-swipe应该有做这方面的处理,但在这种场景下翻页过快时还是出现了跳转路由的情况,所以需要自己重新处理下!
- 原生js获取事件坐标
获取坐标 | |
---|---|
mouseup | event.pageX |
mousedown | event.pageX |
mousemove | event.pageX |
touchstart | event.touches[0].pageX & event.changedTouches[0].pageX & event.targetTouches[0].pageX |
touchmove | event.touches[0].pageX & event.changedTouches[0].pageX & event.targetTouches[0].pageX |
touchend | event.touches[0].pageX & event.changedTouches[0].pageX & event.targetTouches[0].pageX |
关于touch触摸列表:
touches :当前位于屏幕上的所有手指的一个列表。
targetTouches :位于当前DOM元素上的手指的一个列表。
changedTouches :涉及当前事件的手指的一个列表。
- 事件流
<template>
<div class="grid-box">
<div class="cate-item"
v-for="cate in cates" :key="cate.id"
@click="navFood(cate)" @touchstart="touchstart" @touchend="touchend">
<img class="cate-img" :src="cateImgOrigin+cate.image_url" alt="">
<span>{{cate.title}}</span>
</div>
</div>
</template>
<scirpt>
export default {
methods:{
navFood(cate){
console.log('跳转');
},
touchstart(e){
console.log('start');
},
touchend(e){
console.log('end');
},
touchmove(e){
console.log('move');
},
}
}
</scirpt>
//打印顺序:start move(调用14次) end 跳转
上述例子可以说明:事件流式touchstart -> touchmove -> touchend -> click
值得注意的是在touchend中e.preventDefault() 不能阻止click事件,而touchstart里面e.preventDefault() 可以阻止click事件。
3)滑动翻页和点击事件冲突解决方案
取消click,把跳转路由方法放到touchend里面,在touchend里面通过x轴移动的距离判断是否触发跳转路由方法,代码如下:
<template>
<div class="grid-box">
<div class="cate-item"
v-for="cate in cates" :key="cate.id"
@touchstart="touchstart" @touchend="touchend($event,cate)" >
<img class="cate-img" :src="cateImgOrigin+cate.image_url" alt="">
<span>{{cate.title}}</span>
</div>
</div>
</template>
<script>
let startTouchX = 0
export default {
props:{
geohash:String,
cates:Array,
},
data(){
return {
cateImgOrigin:'https://fuss10.elemecdn.com/'
}
},
methods:{
// 导航
navFood(cate){
this.$router.push({path:'/food',query:{
geohash:this.geohash,
id:cate.id,
title:cate.title
} })
},
touchstart(e){
startTouchX = e.changedTouches[0].pageX
console.log('start');
},
touchend(e,cate){
const distance = startTouchX - e.changedTouches[0].pageX
console.log(distance);
if(distance>=-2 && distance<=2){
this.navFood(cate)
}
}
}
}
</script>
7.嵌套flex:1中overflow失效
针对垂直滚动中overflow:auto失效的解决方案:
给所有{flex:1}
所在元素的父元素加上min-height:0
,这样不管flex嵌套了多少层,都可以配合overflow:auto
实现区域内滚动(水平方向类似的让min-width:0即可)
8.Vue的混入 mixins
通俗的讲,就是定义组件数据模板
,可以把这个模板加入任一个组件供其使用,从而达到逻辑复用的效果。
例如在饿了么这个应用中,需要对请求获得的图片路径进行处理得到实际存储的图片路径,这时需要一个getImgPath()方法,显然这个方法在涉及到显示图片的页面都会使用到,那么可以通过mixins把这个方法给到每个页面。
定义一个mixins.js文件:
export const getImgPath = {
methods:{
// 通过path获取图片路径,注:这里的path是一串不含'/'字符串,通过这个函数解析出正确的带'/'的路径字符串
getImgPath(path){
let suffix;
if (!path) {
return '//elm.cangdu.org/img/default.jpg'
}
if (path.indexOf('jpeg') !== -1) {
suffix = '.jpeg'
} else {
suffix = '.png'
}
let url = '/' + path.substr(0, 1) + '/' + path.substr(1, 2) + '/' + path.substr(3) + suffix;
return 'https://fuss10.elemecdn.com' + url
}
}
}
在一个.vue文件中引入,引入后就可以像methods里的方法一样进行使用了:
import {getImgPath} from '../../../components/mixins'
export default {
mixins:[getImgPath],
data(){
return {}
}
}
9.元素多个class切换
以饿了么应用为例,红色区域中的Tag显然有三种样式
样式的类如下:
.rate-tag{
background-color: #ebf5ff;
color: #6d7885;
padding: .07rem;
margin-right: .1rem;
margin-bottom: .05rem;
border-radius: .04rem;
}
.rate-tag-unsatisfied{
background-color: #f5f5f5;
color: #aaa;
}
.rate-tag-active{
background-color: $primary;
color: #FFF;
}
html代码:
<span
:class="{'rate-tag':true, 'rate-tag-unsatisfied':tag.unsatisfied, 'rate-tag-active':activeTagId===tag._id}"
v-for="tag in rateTags" :key="tag._id"
>
{{tag|tagText}}
</span>
通常大家可能会使用三目运算符来切换样式,但当样式类达到3个及以上时,三目运算符并不方便。最终是通过Vue的class对象绑定来实现,语法如下:
给class传对象,通过true/false控制类是否起作用
:class="{class1: true, class2:false}"
给class传class对象数组
:class="[class1, class2]"
10.html5 dataset前缀 data-
html5中 通过data-为元素添加非标准属性,通过这个属性我们可以给事件传递数据集,举例:
<div :data-tag-id="101" @click="handleClick">
点我
</div>
<script>
handleClick(e){
console.log(e.dataset.tagId); //打印:101
}
</script>
11.事件委托解决大量元素绑定事件的性能问题
上面一篇英语文章,点击每个英文单词可以查询释义,一般的做法是给每个单词span绑定一个click事件,但当span特别多时这势必会影响性能。此时可以通过事件委托来解决,关于什么是事件委托,请看我的这篇文章 事件捕获、冒泡和委托
下面通过二个案例来演示事件委托的做法:
1)在饿了么应用中的城市定位页面,有几百个城市供选择,每个城市选项可以点击
如果为每个城市绑定点击事件势必影响性能,此时可以通过事件委托来解决,只要给城市的父元素绑定一个点击事件就可以了。
代码如下:
<template>
<div class="table-container" >
<div class="table-header" :style="'color:'+titleColor">
{{title}}
</div>
<!-- 1.在父元素上绑定点击事件 -->
<div class="table-content" :style="'color:'+textColor" @click="onSelect">
<div class="table-item" v-for="item in cities" :key="item.id"
:data-id="item.id" :data-name="item.name"
>
{{item.name}}
</div>
</div>
</div>
</template>
<script>
export default {
props:{
title: String,
titleColor:{
type: String,
default: '#666'
},
textColor: {
type: String,
default: '#666'
},
cities:{
type: Array,
default: _=>[]
}
},
methods:{
// 事件委托
onSelect(e){
const target = e.target?e.target:e.srcElement
if(target && target.className === 'table-item' ){
this.$emit('onSelect', {
id: target.dataset.id,
name: target.dataset.name
})
}
}
}
}
</script>
2)在饿了么应用中,我采用了事件委托来处理span的点击事件,如下图:红色区域内的span元素可以通过点击变成深蓝色。
代码:
<!-- 1.在父元素上绑定点击事件 -->
<div class="rate-tags" @click="handleTagClick">
<span
:data-tag-id="tag._id"
:class="{'rate-tag':true, 'rate-tag-unsatisfied':tag.unsatisfied, 'rate-tag-active':activeTagId===tag._id}"
v-for="tag in rateTags" :key="tag._id"
>{{tag|tagText}}
</span>
</div>
//2.处理点击事件
handleTagClick(e){
const target = e.target?e.target:e.srcElement
if(target){
//3.判断事件源
if(target.tagName==='SPAN'){
this.activeTagId = e.target.dataset.tagId
}
}
}
12.Vue中this指向和箭头函数
我记得react中,组件的方法默认是this指向调用它的对象,此时通常需要做一件事:就是把组件和this进行绑定。
其中一种绑定方式是bind(),另一种是箭头函数,箭头函数的this指向语义最近的对象。
react中:
class Counter extends React.Component{
constructor(props) {
super(props);
this.state = {clickCount: 0};
// 绑定this
this.handleClick = this.handleClick.bind(this);
}
//方法
handleClick() {
this.setState(function(state) {
return {clickCount: state.clickCount + 1};
});
}
render () {
return (<h2 onClick={this.handleClick}>点我!点击次数为: {this.state.clickCount}</h2>);
}
}
如果将方法handleClick
bind()后,此时我把组件Counter
的handleClick
方法给到父组件,父组件调用该方法时该方法内的this是指向组件Counter的,而不是调用它的父组件,若没有bind()则是指向父组件。
同样的效果在Vue中:
<template>
<h2 onClick={this.handleClick}>点我!点击次数为: {this.state.clickCount}</h2>
</template>
<script>
export default {
data(){
return {
clickCount: 0
}
},
methods:{
//方法
handleClick(){
this.clickCount++
}
}
}
</script>
在vue中,方法中的this默认就是绑定当前组件,因此不需要像react那样bind()或使用箭头函数。记住:Vue中方法的this永远指向定义该方法的组件!
13.饿了么的购物车分析
需求:每个商家都有一个购物车、选择商品后分类上有徽标显示数量
实现:购物车要记录 商家id-分类id-商品 三层信息,其中商品包含不同规格,故还要记录商品规格id(即item_id)
14.订单-支付逻辑
根据App的支付逻辑,画了个简单的支付时序图。如下:
因为后台没有实现"生成订单"接口,所以仿饿了么这个项目在前端模拟生成订单,显示在界面。
15.验证码逻辑
后端生成随机字符串 --> 对字符串绘图 --> 将图片转码成Base64编码 --> 发送图片给前端 --> 前端使用img标签显示
首先需要了解,图片格式的Base64编码应包含两部分,图片格式申明data:image/png;base64,
,图片数据编码字符串iVBO...Jggg==
。前端代码:
<img src="data:image/png;base64,iVBO...Jggg==" alt="">
16.keep-alive使用场景
**1) keep-alive是用来对页面进行缓存的,被缓存的页面在第一次加载数据后就不会再加载数据。**为什么呢?
当组件设置了keep-alive(用keep-alive包起来),生命周期钩子如下:
第一次进入:beforeRouterEnter -> created -> … -> activated -> … -> deactivated -> beforeRouteLeave
后续进入时:beforeRouterEnter -> activated -> deactivated -> beforeRouteLeave
因为通常我们把加载数据的操作放在了created钩子中,显然,第一次进入时会调用created,而后续进入时不会调用created,所以这个被缓存了的页面数据就不会重新加载了(注:只有设置了keep-alive的组件才有activated和deactivated两个钩子)
如果缓存了的页面想要重新加载数据,就需要把created钩子中的初始化操作放到activated中进行。
使用场景:如果一个页面数据Web会话期间不会变化,可以给这个页面加上keep-alive,避免重复加载这个页面数据,优化性能。例如在饿了么这个应用里,获取城市数据页面就可以缓存起来
代码如下:
App.vue中:
<keep-alive >
<router-view v-if="this.$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!this.$route.meta.keepAlive"></router-view>
js中:
// 位置模块
{
path:'/msite',
name:'msite',
component: ()=> import('../pages/msite'),
meta:{
keepAlive: true
}
},
- 常见的需求,路由为:首页 -> 列表页A -> 详情页B,我希望首页->A时A不缓存,A->B时A缓存(即从B返回A时A的数据不会重新加载)。**简而言之: 一个前进刷新、后退缓存的功能 。
解决方案一:
<div id="app" >
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
路由
const routes = [
{
path:'/home',
name:'home',
component: ()=> import('../pages/home'),
},
{
path:'/list',
name:'list',
component: ()=> import('../pages/list'),
meta: {
useCache: false,
keepAlive: true
}
},
{
path:'/detail',
name:'detail',
component: ()=> import('../pages/detail'),
}
]
列表页面
export default {
activated(){
if(!this.$route.meta.useCache){ //useCache决定是否重新加载数据
this.shopId = this.$route.query.id
this.getShopDetail()
}
},
beforeRouteLeave(to , from ,next){
if(to.name == 'detail'){ //去下一级路由
from.meta.useCache = true //动态改变useCache的值
}else{ //去上一级路由
from.meta.useCache = false //动态改变useCache的值
}
next()
},
}
弊端:
- 实现复杂;
- 页面所有状态都被缓存,以致于即使走activated钩子但是原来的状态还是保留了。
解决方案二:
vue-router 嵌套视图,用v-show来控制显示哪一块,当从父页面进入子页面,把父页面隐藏起来。
{
path:'/list',
name:'list',
component: ()=> import('../pages/list'),
children:[
{
path:'detail',
name:'detail',
component: ()=> import('../pages/detail')
},
]
},
list页面:
<template>
<!-- 外面多包一层div -->
<div style="height:100%">
<!-- 父页面 -->
<div class="container" v-show="$route.name != 'list'">
...
</div>
<!-- 子页面 -->
<router-view v-show="$route.name != 'list'"></router-view>
</div>
</template>
export default {
created(){
//这里做一个限制,避免定位到子路由并刷新浏览器时引起created钩子内代码重新执行。
if($route.name == 'list'){
//异步获取数据
this.getList()
}
},
}
detail页面:
<template>
<div class="container" >
...
</div>
</template>
export default {
created(){
//异步获取数据
this.getDetail()
},
}
弊端:若当前定位到了子路由,刷新页面时父路由对应的的生命周期钩子都将被触发,需要单独去控制。
3) 此外,我还研究了一些知名的vue网站:饿了么、搜狐、B站。发现这些网站很少使用keep-alive做缓存,大多数页面都是跳转过来就重新拉数据。
这说明了H5与原生应用的路由要求上有所区别:
- 从父级路由到子级路由时,父级路由的页面是缓存的。(H5和原生的区别)
- 从子级路由到父级路由,子级路由时不缓存的。
- 同级路由之间的switch,是不缓存的。
17.transition组件实现抛物线动画
在Vue中,使用transition标签实现加入购物车时的抛物线效果,查看源码和效果演示请点这个 Demo
想要了解如何实现抛物线的原理,请参考我的另一篇文章css3和js实现抛物线运动
vue的transition的用法请参考 vue官网
18.列表滑动白屏&better-scroll优化
当列表项复杂且数量较多时,浏览器就会渲染卡顿,出现白屏现象。
使用better-scroll可以优化长列表滚动,下面对比一下普通(不做任何处理的)滚动和使用了better-scroll的滚动效果。
better-scroll踩坑
1)使用better-scroll出现的问题,如果在PC端用鼠标滚动将导致列表项定位不准确,scroll等方法达不到预期效果!但是使用(鼠标模拟)手指触摸滚动是正常的。
2)使用了better-scroll的列表,当滑动的父元素生成transform属性后,如果子元素有应用到fixed定位的,会将fixed定位转成absolute定位,即:fixed不再是相对于可视窗口,而是相对于与长列表。
3)吸顶问题,在better-scroll里使用position:sticky会无效
19.better-scroll实现分组吸顶列表
1.分组的思路:在滚动内容里面包含多个子列表,每个子列表都有子列表头。
2.吸顶的思路:在滚动内容外再设置一个固定的Header,通过监听滚动内容的位置y值来控制Header的内容。
分组吸顶列表结构:
代码结构如下:
<div class="wrapper">
<!-- 滚动内容 -->
<div class="content">
<!-- 子列表 -->
<ul class="sub-list">
<li class="sub-list-header">head one</li>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<!-- 子列表 -->
<ul class="sub-list">
<li class="sub-list-header">head two</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
</div>
<!-- 外置标题头,用于吸顶 -->
<div ref="listHeaderRef" class="sub-list-header sticky-fixed">
</div>
</div>
<script>
export default {
mounted(){
// 创建BScroll
this.scroller = new BScroll('.wrapper',{
probeType: 2, //2: 不会监听惯性滑动(即只有当手指触摸期间才监听); 3监听所有滑动
click: true
})
// 记录吸顶头的位置
const arr = document.querySelectorAll('.sub-list')
let sum = 0
this.tops.push(0) //tops: 记录是是子列表头与列表顶部的距离,是取的负值。例:[0,-100,-300]
for(let item of arr){
sum = sum-item.clientHeight
this.tops.push(sum)
}
// 监听滑动事件
this.scroller.on('scroll', this.scrollHook)
this.scroller.on('scrollEnd', this.scrollHook)
},
}
</script>
<style>
.{
height: 100%;
}
.sticky-fixed{
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
</style>
3.另外,当list-header遇到公共的Header时,需要让Header往上滑动,如下图:
因此,监听滑动的函数如下:
// 滑动的钩子
scrollHook({x,y}){
const headerHeight = this.$refs.listHeaderRef.clientHeight
let index = 0
// 如果y超过阈值,改变分类下标
for(let i in this.tops){
const bottomValue = this.tops[i]
const topValue = this.tops[i]+headerHeight
if(y<=topValue && y>bottomValue){ //下一个list-header遇到吸顶的Header,让吸顶的Header移动
const offsetY = y - topValue
this.$refs.listHeaderRef.style.transform=`translateY(${offsetY}px)`
break //注意要结束循环
}else{
this.$refs.listHeaderRef.style.transform=`none`
}
}
}
4.注意事项和最佳实践
4.1 列表内的元素尽量固定高度,否则会引起第一进入页面获取的元素高度出现较大误差(小于预期的高度)。
例如,在仿饿了么项目中,出现了这样的问题,第一次路由进入商家页面,记录的tops和之后记录的tops不一致,打印值如下:
后经排查,发现是elm-counter组件引起的,随后给.elm-counter
设置了固定高度.22rem
,这样第一次和之后获取到的元素高度是一致的,正确的。
4.2 Better-scroll在PC端的问题:
1)使用鼠标滚轮滚动,会导致内部元素的positon:absolute定位失效
2)无法监听scroll
事件
3)在PC端 通过控制台切换成移动端,此时也会导致上述1) 2)问题,(如果一开始是移动端则没有问题)
20.vue打包
参考我的文章 Vue打包优化
结束了!!