使用 compostion function 来对逻辑进行拆分

使用 composition API 可以将特定的逻辑抽离成具有特定功能的函数,然后再将该函数引入到 setup 中来执行。而无需将所有逻辑都直接写到 setup 中去,这样会加大维护的难度。让 composition 函数作为一个数据的流转中心,直接传入数据之后,输出对应的 refs, functions 或者 objects

  1. // 通用模式
  2. export function myCompositionFunction(arg1, arg2) {
  3. /* where amazing happens */
  4. return {
  5. refs,
  6. functions,
  7. objects
  8. }
  9. }
  10. // 示例
  11. function useSubmit(valid, formData, projectId, requiredFiledsValid) {}
  12. // 使用
  13. //...
  14. setup () {
  15. const { error, submitting, submit } = useSubmit(
  16. valid,
  17. formData,
  18. projectId,
  19. requiredFiledsValid
  20. )
  21. }
  22. // ...

采用上述的模式来组织代码,可以极大的提升逻辑的复用和维护效率。可以传入参数,也可以不传入参数,根据实际需求进行选择和拆分。

处理参数中的 ref

在函数的参数中,同时接收 ref 和普通的参数值。

  1. type MaybeRef<T> = T | Ref<T>
  2. export function useEvent(
  3. el: MaybeRef<Element>,
  4. name: string,
  5. listener: EventListener,
  6. options?: boolean | AddEventListenerOptions
  7. ) {
  8. const element = wrap(el)
  9. onMounted(() => element.value!.addEventListener(name, listener, options))
  10. onUnmounted(() => element.value!.removeEventListener(name, listener))
  11. }
  12. const wrap = <T>(val: MaybeRef<T>) => {
  13. return isRef(val) ? val : ref(val)
  14. }

经过这一番改造之后,用户想要传入的参数既可以是 ref 的形式,也可以是直接获取到的 dom.

生命周期函数和 watch

还是上面的例子👆,我们需要同时考虑两个问题:

  1. 如果在执行 onMounted 生命周期函数的时候,element 是空的情况
  2. 如果 element 发生了改变,我们需要怎么处理?

针对以上的情况,我们可以考虑使用 watch 来进行解决。

  1. export function useEvent(_el, name, listener, options) {
  2. const element = wrap(_el)
  3. watch(element, (el, _, onCleanup) => {
  4. el && el.addEventListener(name, listener, options)
  5. onCleanup(() => el && el.removeEventListener(name, listener))
  6. })
  7. }

使用这种写法后,只有当 element 存在时,才会被添加监听函数。当 element 发生变化、或者被卸载时,会执行 cleanup 函数,去掉监听函数。只有产生新的非空 element 时,才会在此重新挂载事件监听函数。

返回值设置成 computed 而非 ref

返回值设置成 computed可以确保其是只读的,可以避免其被篡改。

  1. export function useOnline() {
  2. const isOnline = ref(false)
  3. isOnline.value = window.navigator ? window.navigator.onLine : true
  4. const onlineHandler = () => isOnline.value = true
  5. const offlineHandler = () => isOnline.value = false
  6. window.addEventListener('online', onlineHandler, false)
  7. window.addEventListener('offline', offlineHandler, false)
  8. onUnmounted(() => {
  9. window.removeEventListener('online', onlineHandler, false)
  10. window.removeEventListener('offline', offlineHandler, false)
  11. })
  12. // return computed(() => isOnline.value)
  13. // 或者设置成
  14. return readonly({
  15. isOnline,
  16. a: 'A',
  17. b: 'B'
  18. })
  19. }

自定义的组合函数可采用简洁的命名方式

对自定义的组合函数进行封装的时候,对其中的函数名进行命名的时候可以使用简略的命名方式。例如,使用 enter来代替 enterFullscreen. 进行使用的时候可以使用对象解构的方式来使用当前语境合适的命名。

  1. // 创建
  2. export function useFullscreen(target: Ref<HTMLElement | null>) {
  3. const isActive = ref(false)
  4. function exit() {
  5. if(document.fullscreenElement) {
  6. document.exitFullscreen()
  7. }
  8. isActive.value = false
  9. }
  10. async function enter() {
  11. exit()
  12. if (!target.value) return
  13. await target.value.requestFullscreen()
  14. isActive.value = true
  15. }
  16. return {
  17. isActive,
  18. exit,
  19. enter
  20. }
  21. }
  22. // 使用
  23. const el = ref<HTMLElement | null>(null)
  24. const fullscreen = useFullscreen(el)
  25. onMounted(fullscreen.enter)
  26. // 或者使用重命名的方式进行使用
  27. const { enter: enterFullscreen } = useFullscreen(el)
  28. onMounte(enterFullscreen)

Functional core, imperative shell

编写 composable 函数时,其内部拆分成一个一个的小函数,彼此独立并且仅封装单独的功能。当实际在组件中使用时,再使用 imperative 的方式来进行调用。
对于不包含响应式数据的单体函数,可以将其放到普通的 utils 中来进行使用。
考虑创建不同类型的组合函数:

  • 全局单例的响应式组合函数
  • 特定场景相关的
  • 非特定场景相关的

遇到衍生数据的时候,尽可能的使用 computed 来进行创建
如何判定在什么时候使用单独的函数而什么时候使用组合?

  • 纯函数很容易进行测试和使用
  • functional core, imperative shell
  • 使用响应式的组合函数来对纯函数和业务逻辑来进行包裹