meituan-taro

302 阅读7分钟

taro简单介绍(美团外卖首页)

taro 简介

  • 2018 年:一套遵循 react 语法规范的多端统一开发框架。
  • 背景是 2016 年腾讯推出微信小程序,整合了开发体系,给开发者提供了很多的 API,开发者也可以在小程序上进行扩展。目的是消灭安卓和 ios 的分歧,能以微信作为入口去打开应用,这样写一套代码就可以在安卓、ios 上进行使用。以微信作为流量入口,由他来进行分发。taro 野心更大一些,一套代码来适用多端,一套代码处处运行比微信更为广义

基本介绍

  • 在 render 中跟传统的 h5 开发不同的是并不是使用传统的 h5 标签,因为如果写 h5 标签如 div,放到小程序里面可能就不好使了
  • 如果只做 h5 段,完全可以把 taro 当作 react 来用(实际上不会这么使用)。但是如果要适配多端,就得按照 taro 的规范来使用
  • state
    • taro 中的状态管理也是通过 state。react 中 state 的更新有可能是同步,有可能是异步;但是在 taro 中 state 的更新一定是异步的
      • 设置状态必须用 setState
      • 想拿到最新状态需要在回调函数当中
  • 注意引入组件的名称必须和定义组件的名称保持一致(小程序的要求)
  • props
    • 通过 props 传入的属性如果在 dom 中显示:this.props.name, 可能会报错:"cannot read property 'value' of null",需要加上默认值 类组件加默认值的方式: Child.defaultProps={name: "aa"}
    • props 是只读的
    • 当传递 props 的值是函数时,必须用on+函数名的规范来命名
    • 在 taro 中写方法不能直接写箭头函数:<Button onClick={()=>{}}>aaa会报错 需要通过 onClick=this.handleClick 来实现
  • 生命周期
    • h5 和小程序有差别
    • 凡是 react 中需要的生命周期在 taro 中都可以使用 (完全支持 h5)
    • 新增支持小程序的几个生命周期(对小程序端的生命周期进行了适配)

  • 小程序端是区分页面和组件的,h5 在开发的时候认为一切皆组件

taro 实战-美团外卖首页

  • less 中 尺寸设置成 height: 28PX, 像素用 PX 大写,防止转换成 rem
  • 模糊背景的样式: filter: blur(6PX);
  • 头部布局:
    • Top 组件 className='top'=>头部操作栏 左边是返回按钮 右边是一排图标
    • 放置背景图
    • 商户信息 左边图片 右边描述
.top {
  // 绝对定位 脱离文档流
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 2; // 防止被盖住
}
.head {
  .back {
    width: 100%;
    height: 150px;
    filter: blur(6px);
  }
  .store {
    display: flex;
    flex-direction: row;
    margin-left: 20px;
    margin-top: -90px; // 商户信息整体向上移,盖在背景图上
    .store_img {
      width: 80px;
      height: 80px;
      border-radius: 6px;
    }
    .store_text {
      z-index: 2; // 因为上移到背景图上 字体被盖住了
      margin-left: 10px;
      display: flex;
      flex-direction: column;
      font-size: 18px;
    }
  }
}
  • 美团首页-中部区域
    • 用到 taro-ui 实现 tab 切换
    • 注意: 数组循环渲染 dom 只能放在 render 里面来做,不能抽离成方法放在外面去做。因为 taro 中不单单是有 h5,还有小程序。小程序是模板化的,现在的编译支持不了。
    • 整个包裹在 Food 组件中
      • 上面是标签页 保存 tabs 数组 当前所在的标签页 current
      • 菜单分类组件 Cata
      • 菜品列表组件 FoodList(与菜品分类组件存在交互 展示菜品列表)
      • 加减菜品组件 AddCut 是放在 FoodList 组件的列表循环里面 每一条数据上都有一个 AddCut 组件
    • Cata 和 FoodList 组件间数据传递采用父组件向下传递属性的方式来实现
      • Cata 点击的分类发生变化 通知父组件
      • 父组件加载数据 切换分类的方法 changeCata 传递给 Cata 组件
      • 父组件将数据传递给 FoodList 组件中进行渲染
    • 加减菜品组件
      • 缓存数据,重新进入的时候仍然能够渲染已经加到多少 每一条菜品右边都会有菜品数量信息
      • 缓存数据 因为是多端,有 h5,有小程序,所以这时候缓存不能用 h5 的方法也不能用小程序的方法,需要用 Taro 中提供的方法 Taro.getStorageSync
    • 右边的列表每一项中都有一个 AddCut 组件,当左侧点击分类的时候,要重新去渲染 AddCut 中的数字内容, 是有很多而不是一个,所以不要通过上层父组件传值的方式来解决,会导致所有的列表组件都必须强制的去更新,父组件去调自组建的方法,这样做并不好
      • 好的方式是: 定义一个大的事件池,在 AddCut 子组件中定义事件,左侧的分类当点击的时候触发事件,就会执行 AddCut 中定义的方法,实现菜品数量的更新
// Food组件
state = {
  current: 0,
  tabList: [{title: '点菜'},{title:'评价'},{title: '商家'}],
  foodList: [], //所有数据
  currentList: [], // 当前选中分类下的数据
}
changeTab(value) {
  this.setState({current: value})
}
changeCata(selectCata) { // 切换分类
  // 查询数据 当这个分类下数据已经有了就不需要加载
  // 在state中存储 foodList:[] 放置所有分类的数据 数组这里的每一项是一个对象,pid为分类的id
  if(this.state.foodList.some(item => item.pid === selectCata.id)) {
    this.setState({
      currentList: this.state.foodList.filter(item => item.pid === selectCata.id)
    })
  }else{
    // 请求数据
    const data = this.getData(selectCata);
    this.setState({foodList: this.state.foodList.concat(data)}, () => {
      this.setState({
      currentList: this.state.foodList.filter(item => item.pid === selectCata.id)
    })
    })
  }
}
render() {
  let {current, tabList, currentList} = this.state;
  return (
    <View>
      <AtTabs
        current={current}
        onClick={this.changeTab.bind(this)}
        tabList={tabList}
      >
        <AtTabsPane>
          <View>
            <Cata onChangeCata={this.changeCata.bind(this)}/>  // 左侧是分类
            <FoodList currentList={currentList}/> // 右侧是列表
          </View>
        </AtTabsPane>
        <AtTabsPane>
          评价
        </AtTabsPane>
        <AtTabsPane>
          商家
        </AtTabsPane>
      </AtTabsPane>
    </View>
  )
}
// 两个公共方法
import Taro from '@tarojs/taro'
const foodKey = 'taro_meituan'
// 获取菜品数量 通过传入的信息 来统计当前有多少个菜品
function getFoodCount(food) {
  let store = Taro.getStorageSync(foodKey)
  if (store) {
    if (store[food.id]) {
      return store[food.id].num
    } else {
      return 0
    }
  } else {
    return 0
  }
}
// 设置菜品数量 当加菜或者减菜时触发 num是由加减菜组件自身的state存储
// 当减的数量等于0时 要删除菜品的数据结构
// 当加菜时 发现数据结构中没有该菜品 新增该菜品数据结构
function setFoodCount(food, num, type, callback) {
  if (food) {
    let store = Taro.getStorageSync(foodKey)
    if (!store) {
      store = {}
    }
    if (type == 'cut') {
      if (num === 1) {
        if (store[food.id]) {
          delete store[food.id]
        }
      } else {
        if (store[food.id]) {
          store[food.id].num = num - 1
        }
      }
      Taro.setStorageSync(foodKey, store) // 缓存数据更新
      callback && callback()
    }
    if (type == 'add') {
      if (store[food.id]) {
        store[food.id].num = num + 1
      } else {
        store[food.id] = { ...food, num: 1 }
      }
      Taro.setStorageSync(foodKey, store) // 缓存数据更新
      callback && callback()
    }
  }
}

// 事件管理器(也就是发布订阅)
class Event {
  constructor() {
    this.events = {}
  }
  //监听
  on(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName].push(callback)
    } else {
      this.events[eventName] = [callback]
    }
  }
  // 发布
  emit(eventName, params) {
    if (this.events[eventName]) {
      this.events[eventName].forEach((callback) => {
        callback(params)
      })
    }
  }
}
let myEvent = new Event()
function getEvent() {
  return myEvent
}
// 定义加减菜单组件 AddCut
// 在每一条菜品里面都会渲染 AddCut组件,并且将food数据通过props传递给AddCut组件
import {getEvent} from '';
let myEvent = getEvent();
class AddCut extends Component {
  constructor() {
    super(...arguments)
    this.state = {
      num: 0,
    }
  }
  componentDidMount() {
    this.setState({ num: getFoodCount(this.props.food) })
    myEvent.on('changeCata', () => { // 监听到分类tab改变 进行菜品数量刷新
      this.setState({ num: getFoodCount(this.props.food) })
    })
  }
  // 减1逻辑
  CutFood() {
    if (this.props.food) {
      if (this.state.num > 0) {
        setFoodCount(this.props.food, this.state.num, 'cut', () => {
          // 设置完数量之后 更新state
          this.setState({ num: getFoodCount(this.props.food) })
          myEvent.emit('addcut')
        })
      }
    }
  }
  // 加1逻辑
  AddFood() {
    if (this.props.food) {
      setFoodCount(this.props.food, this.state.num, 'add', () => {
        this.setState({ num: getFoodCount(this.props.food) })
        myEvent.emit('addcut')
      })
    }
  }
}

// 左侧菜单栏组件Cata:在左侧菜单栏切换的方法中触发“changeCata”
// Cata组件中 state会保存 cata 所有菜单数组  selectCata 选中的菜单
const myEvent = getEvent();
clickHandle(item) {
  const { selectCata } = this.state;
  if(selectCata && selectCata.id !== item.id) {
    this.setState({ selectCata: item}, () => {
      this.props.onChangeCata&&this.props.onChangeCata(this.state.selectCata)
    })
    myEvent.emit('changeCata'); // 触发事件
  }else if(!selectCata) {
    this.setState({ selectCata: item}, () => {
      this.props.onChangeCata&&this.props.onChangeCata(this.state.selectCata)
    })
     myEvent.emit('changeCata'); // 触发事件
  }
}

  • 美团首页-底部区域
//  公共方法中增加 获取所有的菜品数量以及价格
export function getAllFoodInfo() {
  let allPrice = 0 // 总价格
  let allNum = 0 // 总数量
  let store = Taro.getStorageSync(foodKey)
  if (store) {
    Object.keys(store).map((key) => {
      if (store[key]) {
        allPrice += store[key].price * store[key].num
        allNum += store[key].num
      }
    })
  }
  return { allPrice, allNum }
}
// 在bottom组件中
// 加减菜品数量的时候 更新总数量和总价格 同样使用事件监听
let myEvent = getEvent();
class AddCut extends Component {
  constructor() {
    super(...arguments)
    this.state = {
      allPrice: 0.
      allNum: 0
    }
  }
  componentDidMount() {
    let {allPrice, allNum} = getAllFoodInfo();
    this.setState({ allPrice, allNum})
    // 组件初始化的时候监听 加减菜品事件
    myEvent.on('addcut', () => {
      // 菜品发生变化就重新执行
      let {allPrice, allNum} = getAllFoodInfo();
      this.setState({ allPrice, allNum})
    })
  }
}

// 事件触发是在AddCut组件中 点击加减的时候触发
myEvent.emit('addcut')