yliu

时来天地皆同力,运去英雄不自由

怎么用 Vue Composition 造轮子


wallhaven-zmmwzw

最近项目临近尾声,终于有时间来对这段工作总结。其实之前使用的一直是 Vue 但是现在公司的主要业务使用是 React 为此还特意看了许多文章,加上实际上这两个框架有很多类似的地方,所以就有了这篇文章。

因为主要是分享经验所以下面的示例主要作为抛砖引玉的作用,在正式分享之前先科普两个小知识

  • hooks 默认以 use 为前缀,这个是 React 的官方推荐做法,约束大于配置也方便 eslint 等解析工具识别,这里也遵循这个做法;
  • watchEffect 与 useEffect 最大的区别就是,watchEffect 可以自动检测依赖,而 useEffect 需要你显示指定;
const count = ref(0);
const onClick = () => {
  count.value++;
};
watchEffect(() => {
  console.log(`被点击`);
});

例如上面这段代码,当 onClick 被触发的时候 watchEffect 不会重复执行,因为它内部有一个收集相关依赖的过程,只有依赖变化才会重新执行,这一点很重要,请记住。

示例

根据常见场景划分了三个类型,如果你有更好的例子欢迎补充

DOM

修改页面 title

import { ref, watchEffect } from 'vue';

const useTitle = (title) => {
  const str = ref(title);
  watchEffect(() => {
    document.title = str.value;
  });
};

调用useTitle即可更新标题

监听页面大小变化

import { watchEffect, reactive, toRefs } from 'vue';

const useResize = () => {
  const size = reactive({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  const onChange = () => {
    Object.assign(size, {
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };
  watchEffect((onInvalidate) => {
    window.addEventListener('resize', onChange);
    onInvalidate(() => {
      window.removeEventListener('resize', onChange);
    });
  });
  return toRefs(size);
};

这里调用 toRefs 的原因是为了解构的情况也可以使用,例如

const { width } = useResize();

监听网络是否断开

import { ref, watchEffect } from 'vue';

const useLineState = () => {
  const line = ref(window.navigator.onLine);
  const onLine = () => {
    line.value = true;
  };
  const onOffline = () => {
    line.value = false;
  };

  watchEffect((onInvalidate) => {
    window.addEventListener('online', onLine);
    window.addEventListener('offline', onOffline);
    onInvalidate(() => {
      window.removeEventListener('online', onLine);
      window.removeEventListener('offline', onOffline);
    });
  });

  return line;
};

跟上面的 useResize 类似,监听相关事件来决定 state 的相关状态

监听 dom 元素变化

import { watchEffect, reactive, toRefs } from 'vue';

const isObject = (obj) => typeof obj === 'object' && obj;
const isElement = (obj) => isObject(obj) && obj.nodeType === Node.ELEMENT_NODE;
const useResizeObserver = (dom) => {
  if (!isElement(dom)) {
    throw new Error(`DOM is not an element!`);
  }
  const size = reactive(dom.getBoundingClientRect());
  watchEffect((onInvalidate) => {
    const resizeObserver = new ResizeObserver(() => {
      Object.assign(size, dom.getBoundingClientRect());
    });
    onInvalidate(() => {
      resizeObserver.disconnect();
    });
  });
  return toRefs(size);
};

这里监听元素使用了还在实验阶段的 ResizeObserver ,为了兼容性请使用 polyfill

封装请求

ajax 的请求很常见,不过在 vue2.x 中我们很容易写出下面的代码

mounted(() => {
  fn().then().catch().finally();
});

这样的代码最大的问题就是不够清晰,因为变量在 data 中定义,而如果切换成下面这种形式,看起来就会直观许多。

更多的例子还可以结合 formtable 进行更加深度的 hooks 封装

const { data, loading, error } = useRequest(() => {
  //...
});
if (loading) {
  //...
}
if (error) {
  // ...
}
import { watchEffect, reactive, toRefs } from 'vue';

const useRequest = (fn, { manual } = {}) => {
  const obj = reactive({
    loading: false,
    data: undefined,
    error: undefined,
  });
  const run = () => {
    obj.loading = true;
    Promise.resolve(fn())
      .then((data) => {
        obj.data = data;
      })
      .catch((err) => {
        obj.error = err;
      })
      .finally(() => {
        obj.loading = false;
      });
  };
  watchEffect(() => {
    if (!manual) {
      run();
    }
  });

  return {
    ...toRefs(obj),
    run,
  };
};

模拟生命周期

这里 Vue 官方的自带生命周期已经很齐全了,不过有的生命周期还是可以通过 watchEffect 做到模拟。

例如:mountedbeforeUnmount,这里实现的原理主要就是利用 watchEffect 自动检测依赖,那如果不对响应式变量做收集的相关操作其实就是一个 mounted

import { watchEffect, nextTick } from 'vue';

const useMounted = (fn) => {
  watchEffect(() => {
    if (typeof fn !== 'function') {
      return;
    }
    nextTick().then(() => {
      fn();
    });
  });
};

const useBeforeUnmount = (fn) => {
  watchEffect((onInvalidate) => {
    onInvalidate(() => {
      if (typeof fn !== 'function') {
        return;
      }
      fn();
    });
  });
};

封装持久化

在一些登录页面中很容易看到记住相关账号和密码,传统的做法就是 login 之后存储相关的账号和密码,然后在页面渲染的时候查看相关 localStorage 有没有对应的数据。

这样写做大的问题还是分离,以及书写的代码量很多,下面就介绍一下如何结合 localStorage 做到一个支持对象的 hooks

import { ref, watchEffect } from 'vue';
const isObject = (obj) => typeof obj === 'object' && obj;
const useLocalStorageState = (key, defaultValue) => {
  const value = ref(undefined);
  try {
    value.value = window.localStorage.getItem(key);
    if (value.value === undefined) {
      value.value = defaultValue;
    }
    value.value = JSON.parse(value.value);
  } catch {}

  watchEffect(() => {
    const v = value.value;
    window.localStorage.setItem(key, isObject(v) ? JSON.stringify(v) : v);
  });
  return value;
};
const name = useLocalStorageState('name', 'admin');

最后

以上代码全部上传到了codesandbox,如果对你有帮助可以star一下

参考文章