前言
大家好,我是webfansplz.首先跟大家分享一个好消息,我退出VueUse团队啦,感激@antfu的邀请,很开心成为团队的一员.明天跟大家聊聊VueUse的设计与实现.
介绍
大家都晓得Vue3引入了组合式API,大大晋升了逻辑复用能力.VueUse基于组合式API实现了很多易用、实用且乏味的性能.比方:
useMagicKeys
useMagicKeys 监听按键状态,并提供了组合热键的性能,十分的神奇和乏味.应用它,咱们能够很容易的监听咱们应用CV大法的次数 :)
useScroll
useScroll 提供了一些响应式的状态和值,比方滚动状态、到达状态、滚动方向以及以后滚动地位.
useElementByPoint
useElementByPoint 用于实时获取以后坐标地位最顶层的元素,配合 useMouse,咱们能够做一些乏味的交互和成果.
用户体验
使用者体验
VueUse无论是面向使用者还是开发者都做到了很棒的用户体验.咱们先来看看使用者体验:
强类型反对
VueUse采纳了TypeScript进行编写并且带有残缺的TS文档,有良好的TS反对能力.
SSR反对
咱们对SSR进行了敌对的反对,它能够在服务端渲染场景工作的很好.
易用性
一些反对传入配置选项的函数咱们会为使用者提供一套罕用的默认选项,这样能够保障用户在大多数利用 场景下并不需要过多的关注你的性能实现和细节.以 useScroll 为例:
<script setup lang="ts">import { useScroll } from '@vueuse/core'const el = ref<HTMLElement | null>()// 只需传入滚动元素就能够工作const { x, y } = useScroll(el)// 节流反对选项const { x, y } = useScroll(el, { throttle: 200 })</script>
useScroll 对一些有性能要求的开发者提供了节流选项.然而咱们心愿的是用户有需要的时候才关注到有这个配置,因为当配置参数一多的时候,了解参数含意和配置其实是一种心智累赘.另外,通用默认配置其实也是开箱能力的一种体现 !
应用文档
应用文档咱们提供了可交互的Demo和精简的Usage,用户能够通过把玩Demo进一步理解性能,也能够通过CV大法复制Usage很容易的就用上性能.真香 !
兼容性
后面咱们提到了Vue3引入了组合式API的概念,然而得益于composition-api插件的实现,咱们也能在Vue2我的项目应用组合式API.为了让更多的用户可能应用VueUse,Anthony Fu 实现了vue-demi ,它通过判断用户装置环境 (Vue2我的项目 援用composition-api
插件,Vue3我的项目援用官网包),这样Vue2用户也能用上VueUse
啦,奈斯 !
开发者体验
目录构造
在基于Monorepo
的根底上,我的项目采纳了扁平化目录构造,便于开发者查找相应的函数.
咱们为每个函数的实现创立了一个独立的文件夹,这样开发者在修复Bug和新增性能的时候,只须要关注该文件夹下具体函数的实现,并不需要关注我的项目自身实现的细节,大大降低了上手的老本.Demo和文档的编写也在该文件夹下实现,防止了高低重复横跳寻找目录构造文件的蹩脚研发体验.
奉献指南
咱们提供了十分具体的奉献指南帮忙想要奉献的开发者疾速开始并且编写了一些自动化脚本帮忙开发者防止一些手动的工作.
原子化CSS
我的项目应用原子化CSS作为CSS的编写计划,我集体感觉原子化CSS能够帮忙咱们疾速的编写出演示Demo,并且每个函数的Demo独立不耦合,不会产生形象复用的心智累赘.
设计思维
可组合的函数
可组合的函数简略来说就是函数间能够建设组合关系,举个例子:
useScroll 的实现组合了三个函数,将一个个繁多职责的函数组合造成另一个函数,达到逻辑复用的能力,我感觉这也便是组合式函数的魅力所在吧.当然,每个函数也都能够进行独立应用,用户能够依据本人的须要进行抉择.
开发者在解决性能函数的时候能够做到更好的关注点拆散,比方解决 useScroll 时咱们只须要关注滚动性能的实现,并不需要关注防抖节流及事件绑定外部的逻辑与实现.
建设"连结"
Anthony Fu 在 Vue Conf 2021中分享了这样一个模式:
- 建设输出->输入的连结
- 输入会主动依据输出的扭转而扭转
咱们在编写可组合式函数的时候建设数据和逻辑的连结,这样咱们就不必关怀如何更新数据,什么时候更新.举个例子:
<script setup lang="ts">import { ref } from 'vue'import { useDateFormat, useNow } from '@vueuse/core'const now = useNow() // 返回一个ref值const formatted = useDateFormat(now) // 将数据传入与逻辑建设连结</script>// useDateFormat实现function useDateFormat(date, formatStr = 'HH:mm:ss') { return computed(() => formatDate(normalizeDate(unref(date)), unref(formatStr)))}
从下面这个例子中咱们能够看出, useDateFormat 在外部逻辑中应用了计算属性对输出进行包裹,这样咱们就能够做到输入主动依据输出扭转而扭转,而用户只需传入一个响应式值,不须要关注具体更新逻辑.
尽可能应用ref
代替reactive
ref
和reactive
有各自的优缺点,这里次要从用户角度谈谈我集体的认识 :
// reactivefunction useScroll(element){ const position = reactive({ x: 0, y: 0 }); // impl... return { position,...}}// 解构失落响应性const { position } = useScroll(element)// 用户需手动toRefs放弃响应性const { x, y } = toRefs(position)// reffunction useScroll(element){ const x = ref(0); const y = ref(0); // impl... return {x,y,...}}// 不会失落响应性,用户可间接拿来渲染,watch..const { x, y } = useScroll(element)
从下面这个例子中咱们能够看到,如果咱们应用reactive
的话,用户须要思考解构会失落响应性的问题,这也从肯定水平上限度了用户应用解构的自由度和升高了这个函数的易用性.
可能有的人会吐槽ref
的.value
应用,其实在大多数状况下,咱们能够通过一些技巧缩小它的应用:
unref
API
const x = ref(0)console.log(unref(x)) // 0
- 应用
reactive
解包ref
const x = ref(0)const y = ref(0)const position = reactive({x, y})console.log(position.x, position.y) // 0 0
- 还在试验阶段的
Reactivity Transform
<script setup>let count = $ref(0)count++</script>
应用options对象作为参数
在实现一个函数时,如果有选项参数的场景,咱们通常倡议开发者应用对象来作为入参,举个例子 :
// goodfunction useScroll(element, { throttle, onScroll, ...}){...}// badfunction useScroll(element, throttle, onScroll, ....){...}
大家能够很清晰的看到两者的区别,毫无疑问第一种写法的扩展性会更强,在之后迭代中也不容易对性能自身造成一些破坏性的改变.
文档实现
对于函数的具体实现就不细说了,毕竟咱们有200个那么多 . 这里跟大家分享一下VueUse
构建文档局部比拟有意思的实现,我感觉做的很棒.
文档组成
咱们先来看下一个性能函数文档的组成部分 :
构建流程
VueUse 应用了 VitePress 作为文档构建工具,上面咱们来看下比拟有意思的局部:
- 以packages文件夹为入口启动 VitePress 服务
VitePress 应用了约定式路由 (文件即路由),所以拜访http://xxx.com/core/onClickOutside
实际上就会解析咱们对应的index.md
文件.看到这里大家就会有疑难了,index.md
文件里只蕴含了usage
啊,其余的信息是哪里来的呢 ? 乏味的局部来了~
- 编写 Vite 插件
MarkdownTransform
对Markdown文件进行解决 :
export function MarkdownTransform(): Plugin { return { name: 'vueuse-md-transform', enforce: 'pre', async transform(code, id) { if (!id.endsWith('.md')) return null const [pkg, name, i] = id.split('/').slice(-3) if (functionNames.includes(name) && i === 'index.md') { // 对index.md进行解决 // 应用拼接字符串的形式拼接Demo,类型申明,贡献者信息和更新日志 const { footer, header } = await getFunctionMarkdown(pkg, name) if (hasTypes) code = replacer(code, footer, 'FOOTER', 'tail') if (header) code = code.slice(0, sliceIndex) + header + code.slice(sliceIndex) } return code }, }}
通过这个 Vite 插件的解决,咱们的文档局部就残缺了.这里又有一个疑难,贡献者的数据和更新日志数据是怎么来的呢 ? 这两个数据处理的形式都差不多,我就拿其中一个来阐明实现 :
- 获取 git 提交者信息
import Git from 'simple-git'export async function getContributorsAt(path: string) { const list = (await git.raw(['log', '--pretty=format:"%an|%ae"', '--', path])) .split('\n') .map(i => i.slice(1, -1).split('|') as [string, string]) return list}
咱们通过simple-git
插件读取到相干文件提交者的信息,有了数据之后,那么咱们怎么将它们渲染到页面中呢 ? 还是应用 Vite 插件,不过这次咱们要做的是注册虚构模块.
- 注册虚构模块
const ID = '/virtual-contributors'export function Contributors(data: Record<string, ContributorInfo[]>): Plugin { return { name: 'vueuse-contributors', resolveId(id) { return id === ID ? ID : null }, load(id) { if (id !== ID) return null return `export default ${JSON.stringify(data)}` }, }}
将咱们方才获取到的数据在注册虚构模块的时候传入就能够了,接下来咱们就能够在组件中引入虚构模块对数据进行拜访.
- 应用数据
<script setup lang="ts">import _contributors from '/virtual-contributors'import { computed } from 'vue'const props = defineProps<{ fn: string }>()const contributors = computed(() => _contributors[props.fn] || [])</script>
拿到数据后,咱们就能够进行页面渲染了. 这就是文档中 Contributors 和 Changelog 局部的实现原理. 咱们来看下成果 :
看完这个是不是感觉还蛮有意思的,Vite 插件其实还是能够用来搞很多事件的.
V8.0来啦
咱们在前两天正式公布了 V8.0, 次要带来了:
- 对一些函数的命名进行了规范化,并应用别名做了向下兼容
- 新增了几个函数,目前函数数量达到了 200 +
@vueuse/core/nuxt
=>@vueuse/nuxt
- 对一些函数做了指令反对,欢送应用
结语
最初,感激Anthony Fu对本文的斧正和倡议,瑞思拜 ! 如果我的文章对你有帮忙,欢送关注我一起学习.