饿了么-开发笔记

仿饿了么-开发笔记

项目演示

gitee地址

一、移动端开发准备

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的变化

  1. new Router 变成 createRouter()
  2. 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状态树,唯一数据源。
gettersstore的计算属性,计算结果会被缓存。可以通过闭包的方式传参。
mutations更改state的方式,定义如何修改state。修改state的唯一方式就是提交mutation。devtools会捕捉mutations里面的快照,Vuex要求mutations内的代码必须是同步的。
actionsactions用来执行异步代码,可以通过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应该有做这方面的处理,但在这种场景下翻页过快时还是出现了跳转路由的情况,所以需要自己重新处理下!

  1. 原生js获取事件坐标
获取坐标
mouseupevent.pageX
mousedownevent.pageX
mousemoveevent.pageX
touchstartevent.touches[0].pageX & event.changedTouches[0].pageX & event.targetTouches[0].pageX
touchmoveevent.touches[0].pageX & event.changedTouches[0].pageX & event.targetTouches[0].pageX
touchendevent.touches[0].pageX & event.changedTouches[0].pageX & event.targetTouches[0].pageX

关于touch触摸列表:

touches :当前位于屏幕上的所有手指的一个列表。
targetTouches :位于当前DOM元素上的手指的一个列表。
changedTouches :涉及当前事件的手指的一个列表。

  1. 事件流
<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()后,此时我把组件CounterhandleClick方法给到父组件,父组件调用该方法时该方法内的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
    }
},
  1. 常见的需求,路由为:首页 -> 列表页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滚动

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打包优化

结束了!!

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值