Vue 3.0 我馋了😍, Composition-api 真香

2,889 阅读5分钟

多灾多难的 2020 转眼过半。Vue 3.0 正式发布 beta 版,算是为数不多令人高兴的事情。

本文不谈 Vue 3.0 会带来什么性能提升,也不谈有无必要升级到 Vue 3.0。主要说说怎么在 Vue 2.x 利用 composition-api 怎么解决 option-base 的一些痛点。

我对 composition-api 的理解

对于业务开发来说,通常是面对一个个 Single-File-Component。为什么喜欢用 Vue,因为 Vue 帮我们在 Javascript 和 Views 间搭了一条高速列车。通过响应式的数据来反馈视图的变化,我们可以在 Vue 中改变数据的状态来改变视图。这么一来,我们需要利用 Vue 响应式的特性实现的逻辑,就必须在 Vue File 里面。一些简单且常用的逻辑,也只能在每个 Vue File 中 copy paste。

而 compostion-api,就是将这种响应式的特性,延伸到 Vue File 外。使得我们可以复用那些 copy paste work。

一个常见的业务实现,控制 Dialog 的显示隐藏

通常来说,控制 dialog 的显示隐藏很简单,在 data 中定义一个 showDialog 的状态即可,然后需要写一个方法去改变这个状态就能实现了。

// option-base
<template>
	<dialog :visible="showDialog"></dialog>
</template>

<script>
export default {
	data() {
  	return {
    	showDialog: false
    }
  },
  
  methods: {
    toggleDialog() {
    	this.showDialog = !this.showDialog
    }
  }
}
</script>

如果页面只有一个需要控制的 dialog,代码就如上方那么简单。但是,夸张点,一个页面有 10 个不同的类 dialog 交互需要实现,对于这么简单的逻辑,copy 就完事了。所以代码会变成下面这样

// option-base
<template>
	<dialog0 :visible="showDialog0" @close="toggleDialog0"></dialog0>
  <dialog1 :visible="showDialog1" @close="toggleDialog1"></dialog1>
  <dialog2 :visible="showDialog2" @close="toggleDialog2"></dialog2>
  ...
  <dialog9 :visible="showDialog9" @close="toggleDialog9"></dialog9>
</template>

<script>
export default {
    data() {
        return {
            showDialog0: false,
            showDialog1: false,
            showDialog2: false,
          // ...
            showDialog9: false,
        }
    },
  
  methods: {
    toggleDialog0() {
    	this.showDialog0 = !this.showDialog0
    },
    toggleDialog1() {
    	this.showDialog1 = !this.showDialog1
    },
    toggleDialog0() {
    	this.showDialog2 = !this.showDialog2
    },
    // ...
    toggleDialog0() {
    	this.showDialog9 = !this.showDialog9
    },
  }
}
</script>

这样,就能在一个页面上控制 10 这个类 dialog 的交互了。逻辑过于简单而忍不住 copy 😳

由于利用了 data 的响应式,我不能在外部抽象一个逻辑,去声明 show 个状态和返回一个改变状态的方法,这些逻辑算是在 option-base 的思维下最精简的代码了。当然对于这一系列状态可以用数组去优化,但是这些方法对于 composition-api 同样适用,所以不纳入比较范围,接下演示一下 composition-api 怎么复用这个响应式的状态

useToggle 逻辑

// reuse logic
export function useToggle(initState = false) {
	const state = ref(initState)
  const toggle = function(outerState) {
  	if (typeof outerState === 'boolean') {
    	state.value = outState
    } else {
    	state.value = !state.value
    }
  }
  
  return {
  	state,
    toggle
  }
}

业务使用

// compostion-api
<template>
	<dialog0 :visible="showDialog0" @close="toggleDialog0"></dialog0>
  <dialog1 :visible="showDialog1" @close="toggleDialog1"></dialog1>
  <dialog2 :visible="showDialog2" @close="toggleDialog2"></dialog2>
  ...
  <dialog9 :visible="showDialog9" @close="toggleDialog9"></dialog9>
</template>

<script>
export default {
  setup() {
    const { state: showDialog0, toggle: toggleDialog0 } = useToggle()
    const { state: showDialog1, toggle: toggleDialog1 } = useToggle()
    const { state: showDialog2, toggle: toggleDialog2 } = useToggle()
    // ...
    const { state: showDialog9, toggle: toggleDialog9 } = useToggle()
    
    return {
    	showDialog0,
      showDialog1,
      showDialog2,
      // ...
      showDialog9,
      toggleDialog0,
      toggleDialog1,
      toggleDialog2,
      // ...
      toggleDialog9,
    }
  },
}
</script>

可以看到 composition-api 可以优雅的复用这些简单的逻辑,但是对于 vue-template 来说是没什么改变的。所以我更愿意把 composition-api 看作更加灵活利用 Vue 的方案,而不是 Vue 3.0 代码的语法变革。只不过刚好它随着 Vue 3.0 的实现而出现在大家眼前。

这是一个状态的复用,如果像异步操作的 loading,success,error 的交互逻辑,触发方法时显示 loading,回调成功,显示数据,回调失败显示失败文案,这样的逻辑也是很常见,而往往也只能一次又一次的重新实现,因为它不难,而且在 option-base 中不好复用。大家看到这里可以思考一下怎么去复用这样的逻辑。

除此以外还有分页操作的逻辑,需要 total、currentPage、pageSize、data 几个状态,同时需要切换页码更新数据的方法,虽然复杂一丢丢,但是也只能一遍又一遍的重复实现。用上 composition-api 的思维,就可以复用这些逻辑,不仅项目内,甚至是整个 Vue 生态内的复用。切切实实的提示了业务开发的效率。

如何用上 composition-api

Vue 3.0 处于 bata 阶段,不是很适合公司应用上生产。但是令我大喊「真香」的不是 Vue 3.0 的性能优化,而是随之诞生的 composition-api 方案。所以 vue/composition-api 这个库是不错的选择。既可以提前熟悉 Vue 3.0 的语法,也能够在开发上看到比较实在的效率提示,当然前提是已经熟悉了 composition-api 的相关语法。

我踩过的坑,让大家少走一点弯路

使用 css-module 怎么获取 $style

issue: github.com/vuejs/compo…

import { getCurrentInstance } from '@vue/composition-api';

export default {
  setup () {
     const { $style } = getCurrentInstance()
  }
}

需要注意的是,这个方法在 Vue 3.0 是不提供的,但是如果需要使用 js 来获取 css-module 注入的变量,就需要使用这个方法获取,如果在 template 中直接使用 $style 是不需要调用这个方法的

怎么使用 template ref 获取 dom 的引用

由于底层还是 Vue 2.x ,这块的实际逻辑和 Vue 3.0 的 RFC 是有出入的。详情可以看官方文档

而我是这样用的

const Demo = defineComponent({
  setup(props, ctx) {
    const [count, { inc, dec, set, reset }] = useCounter();
    // useClickAway 返回一个 ref
    const { element } = useClickAway(() => inc());
    return {
      element,
      count,
      inc,
      dec,
      set,
      reset,
    };
  },

  render(this: Vue & Inject) {
    const { count } = this;
    return (
      <div>
        click outside the button to increase counter
        <br />
        <button ref="element">count: {count}</button>
      </div>
    );
  },
});

这里是实现了一个当点击 button 外部时触发一个事件。通常可以实现类似点击 dialog 外部关闭 dialog 的逻辑。

怎么获取响应式的路由呢

先说说根据文档是怎么获取 router 和  route 的

export default {
	setup(props, ctx) {
  	const { $router, $route } = ctx.root
    // 现在我想监听 route 上的 params id
   	// 第一次尝试
    const id = computed(() => {
    	return route.params.id
    })
    
    watchEffet(() => {
    	console.log(id)
    })
  }
}

上面的代码,在改变了路由的 params 时,并不会作出响应,打印出 id 的。因为 route 只是一个普通 object,并没有作出响应式的处理。下面的代码才能在 route 改变时,代码也能触发响应

export default {
	setup(props, ctx) {
    const route = computed(() => ctx.root.$route)
    const id = computed(() => {
    	return route.value.params.id
    })
    
    watchEffet(() => {
    	console.log(id)
    })
  }
}

推荐几个 hooks 库

封装了一些常用的浏览器 API 比如 FullScreen、VisibilityChange 等

注意封装了 router 和 vuex 的 hooks

另外由于 u3u/vue-hooks 的作者回应比较慢,所以本人 fork 了仓库,准备近期增加一些业务常用的 hooks,同时也更新了最新的 composition-api。大家可以来提需求 github.com/lianghx-319…

如果 u3u/vue-hooks 的作者看到,可以回应一下我,还是想继续维护原本的仓库