老友记13会议纪要

272 阅读7分钟

组件封装

MemorableSearch

  • 自带搜索历史记录的搜索框

SortableFilterGroup/SortablePanelGroup

  • 可拖拽排序+折叠展开的组合筛选器
  • 可拖拽排序+折叠展开的面板组
  • 参考 如何实现拖拽排序

SmartPatchTable

  • 基于Table做二次封装的带有全选和批量操作功能的表格
<!-- 
注入数据 
:dataArr="billArr 账单模块丢入账单数组给SmartPatchTable做渲染
:actionBtnText="批量收款" 批量操作按钮文字
@onActionBtnClick="patchCollect" 用户点击批量操作时SPT子组件会发送自定义事件 携带参数为选中行的id数组
-->
<SmartPatchTable
  :dataArr="billArr"
  :actionBtnText="批量收款"
  @onActionBtnClick="patchCollect"
></SmartPatchTable>

/* 处理SPT发送的自定义事件 入参id数组 */
patchCollect(ids=[]) {
    //ids例如[1,2,5]代表用户在
    ids.forEach(billId=>collect(billId))
}

自定义Hook

useMousePosition

  • 对需要实时鼠标位置的页面提供响应式数据: x + y
  • 数据响应式逻辑:组件挂载即监听window的mousemove事件,将e.pageX和e.pageY实时同步到响应式数据中;
  • 组件卸载时移除mousemove事件监听;
  • hook代码
import {ref,toRef,toRefs,reactive,computed,onMounted,onUnmounted,} from "vue";

function useMousePosition() {
        const state = reactive({
                x: 1,
                y: 2,
        });

        const updateMousePosition = (e)=>{
                state.x = e.pageX
                state.y = e.pageY
        }

        onMounted(
                ()=>window.addEventListener("mousemove",updateMousePosition)
        )

        onUnmounted(
                ()=>window.removeEventListener("mousemove",updateMousePosition)
        )

        // return 响应式数据 {x,y}
        return toRefs(state);
}
  • 调用代码
import useScroll from '@/hooks/useMousePosition.js'

const { x, y } = useMousePosition();

useScroll

  • 对需要纵向滚动的页面提供三个响应式数据:scrollTop + scrollBottom + bodyHeight;
  • 对需要纵向滚动的页面提供【返回顶部】+【移动到任意位置】两个功能:toTop + yScrollTo;
  • 响应式逻辑:组件挂载即建立window.onscroll事件监听,将e.scrollTop同步到scrollTop,同时手动计算出scrollBottom和bodyHeight两个属性的值并更新;
  • 组件卸载时自动移除scroll事件监听;
  • hook代码:
import { onMounted, onUnmounted, reactive, toRefs } from 'vue'

export default function useScroll() {
  const state = reactive({
    scrollTop: 0,
    scrollBottom: 0,
    bodyHeight: 0
  })

  const onScroll = () => {
    state.scrollTop = document.documentElement.scrollTop
    state.scrollBottom =
            document.body.clientHeight - state.scrollTop - window.innerHeight
    state.bodyHeight = document.body.clientHeight
  }

  let timer = null
  const toTop = (millis = 1000) => {
    yScrollTo(0, millis)
  }

  const yScrollTo = (y, millis = 1000) => {
    if (!timer) {
      const offset = document.documentElement.scrollTop - y
      const frameOffset = Math.abs(offset / (millis / 40))

      timer = setInterval(() => {
        if (
          offset > 0 &&
                    document.documentElement.scrollTop - y > frameOffset
        ) {
          document.documentElement.scrollTop -= frameOffset
        } else if (
          offset < 0 &&
                    y - document.documentElement.scrollTop > frameOffset
        ) {
          document.documentElement.scrollTop += frameOffset
        } else {
          document.documentElement.scrollTop = y
          clearInterval(timer)
          timer = null
        }
      }, 40)
    }
  }

  onMounted(() => {
    window.addEventListener('scroll', onScroll)
  })

  onUnmounted(() => window.removeEventListener('scroll', onScroll))

  // 对外返回一堆ref
  return { ...toRefs(state), toTop, yScrollTo }
}
  • 调用代码:
import useScroll from '@/hooks/useScroll.js'

const {
  scrollTop: stRef,
  scrollBottom: sbRef,
  // bodyHeight: bhRef,
  toTop,
  yScrollTo
} = useScroll()

useAxios

  • 对需要网络通信的页面提供三个响应式数据:loading + data/err
  • 响应式变化监听逻辑:加载即发起axios/ajax通信,loading为true
  • 通信成功或失败时,data/err一实一空
  • hook核心代码 参考:axios的取消
function useAxios(ajaxConf){

  const {url,method,data,onSuccess,onFail} = ajaxConf

  const state = reactive({
    loading:false,
    data:null,
    err:null
  })

  /* 组件挂载时 从localStorage中读取数据 如果数据不存在则使用useAxios */
  let source = null;
  onMounted(()=>{
    state.loading = true

    const CancelToken = axios.CancelToken;
    source = CancelToken.source();

    axios.get(url,{...ajaxConf,
      cancelToken: source.token
    }).then(
      data=>{
        state.loading = false
        state.data = data
        state.err = null
      }
    ).catch(
      err=>{
        state.loading = false
        state.data = null
        state.err = err
      }
    )

  })

  /* 组件卸载时 取消未完成的通信请求 */
  onBeforeUnmount(()=>{
    state.loading && source.cancel('Operation canceled by the user.');
  })

  return toRefs(state)
}

useEventTarget

  • 为页面提供自动的事件绑定与解绑
  • 此hook无响应式数据逻辑,仅仅是抽离了事件绑定与解绑逻辑
  • 组件挂载时自动绑定DOM事件到指定元素身上,组件卸载时再执行解绑
  • 可配置事件传播方向与防抖/节流功能
  • 核心代码:
function useEventTarget(conf){
  let {domSelector,eventType,eventHandler,useCapture,useDebouce,useThrottle} = conf

  // 找出元素
  const element = document.querySelector(domSelector)

  /* 加入防抖或节流逻辑(可借助lodash或自定义) */
  if(useDebouce){
    eventHandler = debounce(eventHandler)
  }else if(useThrottle){
    eventHandler = throttle(eventHandler)
  }

  /* 挂载时建立监听 */
  onMounted(()=>{
    element.addEventListener(
      eventType,
      eventHandler,
      useCapture
    )
  })

  /* 卸载时移除监听 */
  onUnmounted(()=>{
    element.removeEventListener(eventType,eventHandler)
  })
}

其它小hook(不计其数)

  • 核心思想就是返回响应式数据及其附属的二手数据和操作函数等
  • 几乎任意一个Vue的项目中都一定能用到
  • 示例代码:商品价格自变化逻辑hook
function usePrice() {

  // 定义响应式数据
  let price = ref(10);

  /* 自动计算折扣价 */
  let discountedPrice = computed(() => {
    return {
      half: price.value / 2,
      bazhe: price.value * 0.8,
      jiuzhe: price.value * 0.9,
    };
  });

  /* 响应式数据操作函数 */
  const editPrice = (value) => {
    price.value = value;
  };

  // 对外返回响应式数据
  return {
    price,
    discountedPrice,
    editPrice,
  };
}

// 使用hook
const {price,discountedPrice,editPrice} = usePrice()

性能优化

数据缓存

  • 如无手动刷新,则5分钟以内使用Vuex内的持久化数据缓存;
  • 在vuex中设置一个map为每个数据配置一个缓存时效+定时更新的timerId,每当利用action获取数据成功后立即拉起一个延时定时器,执行下一次的自动更新;
  • 入用户在延时更新启动前再次调用action,则先取消上一次的timerId,然后再次设置延时;

事件防抖:防止重复提交表单;

/* 
模板中使用自定义指定+修饰符 
form提交时 对submit事件加500毫秒的防抖处理
事件处理器设置为onSubmit函数
*/
<form v-submit.debounce="{handler:onSubmit,delay:500}">
  <input/>
  <button>提交</button>
</form>

/* 定义自定义指令 */
app.directive("submit",{

  // 组件一挂载就给el添加submit事件处理器
  mounted:(el,binding)=>{

    // 从binding.value中解构出事件处理器与要防抖的延时
    let {handler,delay} = binding.value

    // 根据是否有debounce或throttle修饰符 决定是不是要对handler加防抖或节流处理
    if(binding.modifiers["debounce"]){
      handler = debounce(handler,delay)
    }else if(binding.modifiers["throttle"]){
      handler = throttle(handler,delay)
    }

    // 给应用自定义指令的元素添加submit事件监听器
    el.addEventListener(
      "submit",
      handler
    )
  }

})

数据预加载:登录后通过Promise队列预加载后续菜单数据;

  • 在管理页主菜单加载完毕后,使用Promise.allSettled拉起一个并发的ajaxPromise数组;
  • 在任何一个Promise任务成功回调中更新Vuex中的数据;

频繁的组合筛选:使用自带结果缓存的高阶函数;

  • 定义一个专门用于从既定数组中筛选结果的函数cachedArrFilter(arr,filters);
  • 该函数在执行之前会看看相同入参的执行结果是否曾经缓存过,是则直接调用缓存,否则调用arr.filter执行筛选并缓存结果;
  • 该缓存的具体实现为一个LRUCache,即自动对最近最常使用的查询结果进行缓存而非无度缓存;
  • 参见 Javascript中如何实现函数缓存
  • 参见 JavaScript实现LRUCache
  function cachedFn(fn) {
    // 闭包内存储缓存数据
    const cacheObj = {};

    // 返回函数的函数 闭包函数
    // 这个函数就是cadd
    return (...args) => {
      console.log("args", args);

      // 如果cacheObj中没有[100,200]的结果缓存
      if (!cacheObj[args]) {
        // 调用add函数重新计算add(100,200)的结果
        let result = fn.apply(null, args);

        // 以[100,200]做key 以result做value 将计算结果缓存在闭包内
        cacheObj[args] = result;

        console.log("重新计算结果");
        return cacheObj[args];
      } else {
        console.log("读取缓存结果");
        return cacheObj[args];
      }
    };
  }

  function add(a, b) {
    return a + b;
  }

  const cadd = cachedFn(add);
  console.log(cadd(100, 200));
  console.log(cadd(100, 200));

对上传的房源图片使用自动压缩;

  • 对需要上传的房源图片默认执行一下压缩
  • 监听文件上传的input框中的onchange事件,将拿到的文件信息使用github上的第三方库 img-compressor进行压缩上传;
  • 通常5M级别的图片会被压缩到500多K;
  • 还未来得及研读该库的核心原理;
  • 参考 # JS实现图片压缩上传

快搜租客/社区:按拼音索引将大数组拆分成26个小组;

  • 使用v-autoindex自定义指令实现input框的中文输入自动提取英文首字母作为查询索引;
  • 后台给回的房源列表和租客列表均有按拼音字段,前端根据该字段自动将大列表拆分为包含26个子数组的对象;
  • 当用户执行搜索时,会在大数组的缓存结果中根据首字母首先拿到对应的小数组,然后再执行进一步搜索,极大地提高了搜索效率;
  • 参考 Vue自定义指令

特色功能

大量的批量操作优化体验,如批量催租、批量上下架、批量置顶等,使用SmartPatchTable;

支持用户自定义常用功能面板;

  • 每个菜单项右侧包含该菜单的常用操作;
  • 将这些面板分为常用和不常用两组,不常用组可以折叠,点击面板右上角的转移按钮可以将面板从当前组转移到另一组;
  • 常用组面板支持拖拽排序;
  • 参考 如何实现拖拽排序