当我在 Vue Conf 大会中看到 Vue Vine 这种新的开发方式之后,我非常的激动。因为我确实非常喜欢这种语法。因此在周日当天,我就通过自己的摸索跑通了一个 demo,并写了一篇文章跟大家介绍它。
这篇文章传播有点广泛,甚至还被 Vue Vine 作者看到。当然对于这种新的开发方式,从评论区中能够感受到,许多人并不是那么欢迎它的出现。所以我决定更加深入的使用它之后,再重新写一篇文章,结合它与 React 的差异,跟大家分享一下深入使用之后的真实感受。
在这几天时间里,我使用 Vue Vine,写了一个常用技术点覆盖面还算齐全的小网站。目前长这个样子。当然这个只是我本地的一个 demo,主要用于我自己学习和练习使用。
接下来,我就以完成的这个网站为例,给大家介绍 Vue Vine 的深度使用体验。也进一步跟大家分享为什么我会如此喜欢它。在这个项目中,我做的事情主要包括:
在完成项目的过程中我遇到了很多问题,因此这几天我与 Vue Vine 的开发团队在 issue 上进行了大量的沟通。Vue Vine 的版本也从 v0.1.5 发到了 v0.1.8。很显然,他们确实有非常认真的在对待这个方案。对 issue 的反馈比较及时,调整也比较快。
从最开始的 Vue-vine 插件因为崩溃问题完全不能用,到现在我感觉可以勉强支撑起日常开发,只过去了几天的时间。
由于开发团队需要专门针对 .vine.ts
的文件后缀做兼容处理,因此可能除了代码编译之外,vue-vine
插件的开发也是一个比较大的工作量。可能目前依然存在一些开发体验不够好的情况
例如,不支持如下写法
export default function Button() {}
仅支持这种写法
function Button() {}
export default Button
或者目前在 vine
模板中,还不支持给目标代码添加注释的快捷键等一些细节问题。
但是相信未来很快就会得到解决。
我们先来简单看一下,在 vue vine 中,声明一个组件的方式,与声明一个函数的方式一模一样,只不过返回的内容必须是 vine
模板。
import {ref} from 'vue'
function HelloWorld() {
const count = ref(0)
return vine`
<button @click="count++">
counter++
</button>
<div>{{count}}</div>
`
}
export default HelloWorld
有的同学不太喜欢写 return vine
,因此,我们可以在 ts 的 snippet 配置文件中,新增如下字段
"return vine": {
"prefix": "vine",
"body": [
"return vine`",
" $1",
"`"
],
"description": "return vine"
},
这样,就可以有如下快捷输入
道友们可以用同样的方式定义其他更多的快捷指令。我们还可以通过如下方式在 settings.json
中配置 snippet 提示的顺序
"editor.snippetSuggestions": "inline",
在我的付费小册《React19》中,我已经自定义好了一个 Button 组件。
一模一样的功能,一模一样的入参,一模一样的样式,我使用 vue-vine 重新封装之后的完整代码如下
import {twMerge} from 'tailwind-merge'
import clsx from 'clsx'
function Button(props: {
class?: string,
primary?: boolean,
danger?: boolean,
sm?: boolean,
lg?: boolean,
signal?: boolean,
success?: boolean,
}) {
const {class: className, primary, danger, sm, lg, signal, success} = props
const base = 'rounded-md border border-transparent font-medium cursor-pointer transition relative'
// type
const normal = 'bg-gray-100 hover:bg-gray-200'
// size
const md = 'text-xs py-2 px-4'
const cls = twMerge(clsx(base, normal, md, {
// type
['bg-blue-500 text-white hover:bg-blue-600']: primary,
['bg-red-500 text-white hover:bg-red-600']: danger,
['bg-green-500 text-white hover:bg-green-600']: success,
['text-sky-500 bg-white border border-sky-300 hover:bg-sky-50']: signal,
// size
['text-xs py-1.5 px-3']: sm,
['text-lg py-2 px-6']: lg,
}, className))
return vine`
<button :class="cls">
<slot />
<span v-if="signal" class="absolute flex h-3 w-3 right-[-5px] top-[-5px]">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-sky-500"></span>
</span>
</button>
`
}
export default Button
与 React 相比,中间的逻辑几乎一模一样,我们主要关心一下返回的差别。
return (
<button className={cls} {...other}>
{props.children}
{signal && (
<span className="absolute flex h-3 w-3 right-[-5px] top-[-5px]">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-sky-500"></span>
</span>
)}
</button>
)
很明显, vue-vine 会更简洁一些。这里的差别主要有
1、Vue 支持属性透传到内部元素的父节点,因此我们可以不用像 React 那样使用 {...other}
展开所有的其他属性。
2、 可以使用 slot
插槽取代 {props.children}
3、 样式名在 React 中需要写成 className
,这是历史遗留的问题,在 Vue 中可以直接写成本来的样子 class
4、 可以使用指令 v-if
代替如下逻辑判断
{signal && (<span>...</span>)}
很显然,从结果上来看,vue-vine 更加的简洁。但是这其中包含了许多的小的知识点,站在新人的角度上来说,理解成本会偏高一些。而 React 则只遵循一个概念,那就是把 jsx
当成表达式使用,并在这个标准之下写出来的代码理解起来一致性会更强一些,灵活性也会更高一点。
✓许多 Vue 的三方 UI 库依然使用 JSX 来封装,实际上就是看中了 JSX 理念下的灵活性
先来看一下我实现的功能的演示效果。我支持了初始化加载列表和点击按钮更新列表的能力。
抛开底层机制不谈,vue-vine 在开发方式上基本上与 React 保持了一致的开发体验。因此,异步编程的逻辑上也基本上是一致.
初始化请求的逻辑,都在组件首次渲染完成之后执行
// vue-vine
onMounted(() => {
api().then(res => {
loading.value = false
data.value = res
})
})
// react
useEffect(() => {
// api 请求
}, [])
更新的逻辑都在点击事件或者其他交互事件的回调中执行。
但是 React 19 在这个基础之上更进一步,新提出了 use + Suspense
的使用方式。代码的整体简洁度又提高了一个档次。
// React 19
export default function Demo01() {
const [promise, update] = useState(getMessage)
function __handler() {
update(getMessage())
}
return (
<>
<div className='text-right mb-4'>
<Button onClick={__handler}>更新数据</Button>
</div>
<Suspense fallback={<Skeleton />}>
<Content promise={promise} />
</Suspense>
</>
)
}
但是在 Vue-vine 中不支持这套机制,那应该怎么办呢?
好在几年前,我曾经在公众号发表过一篇付费文章 React 哲学,文章中提到的开关思维,可以让 vue-vine 的代码实现结果拥有不亚于 React use
的简洁性。
代码如下,注意观察细节
<!--vue-vine-->
function Dashboard() {
const {loading, data} = useFetch(fetchUsersApi)
return vine`
<div class="w-[700px] mx-auto mt-10">
<Button @click="loading=true" :disabled="loading">更新列表</Button>
<Skeleton v-if="loading" class="mt-4" />
<template v-else v-for="item in data?.results">
<ListItem :data="item" />
</template>
</div>
`
}
我们可以把 .value
等操作,通过自定义 hook,封装到底层去,眼不见心不烦。封装好自定义 hook 之后,就把他当成一个共用的,长期的,稳定的公共 api 使用,未来在应用层的页面,则直接在 template
中使用 ref
定义的状态。
这样,我们的应用层页面中,大多数时候就看不见 .value
的使用了。注意看按钮的点击逻辑
很显然,我在 React 哲学中提到的开关思维,非常契合 vue-vine
,它比在 React 中使用更简洁,更能大放异彩。
分页列表是一个比较复杂的逻辑。但是我们依然可以使用开关思维把他的代码处理成非常简单的结果。
注意看我的演示效果,我使用加载更多的按钮充当分页加载的执行时机。
我在底层封装了一个共用方法 usePagination
用于处理状态的定义和接口请求的逻辑。然后在应用层直接使用
你需要注意观察的是,loading 是使用 ref 定义的状态,对应初始化的 UI 变化。incrementing 则对应加载更多时的 UI 变化。在应用层,我们可以直接在点击回调中 @click
,修改他们的值,就能轻松的完成完整的逻辑。
function Dashboard() {
const {loading, incrementing, data} = usePagination(fetchUsersApi)
return vine`
<div class="w-full max-w-[500px] mx-auto mt-10">
<Button @click="loading=true" :disabled="loading">更新列表</Button>
<Skeleton v-if="loading" class="my-4" />
<template v-else v-for="item in data?.results">
<ListItem :data="item" />
</template>
<Skeleton v-if="incrementing" class="my-4" />
<div v-if="!incrementing" class="flex justify-center">
<Button @click="incrementing=true" signal>点击加载更多</Button>
</div>
</div>
`
}
✓当然,vue3 中也可以不需要 pinia,只是 vue-vine 改成的函数式的语法中,这种倾向会更明显,也更自然
这将是 vue-vine 语法变化后,一个比较重要的倾向。当你需要将状态保存在全局时,我们可以很自然的在一个单独的 ts 文件中,定义 ref
例如,我定义一个名为 useCounter.ts
的模块
export const count = ref(0)
export function increment() {
count.value++
}
然后在需要的组件中引入并使用即可
import {count, increment} from './useCounter'
function Home() {
return vine`
<button @click="increment">count++</button>
<div class="text-green-600">
{{count}}
</div>
`
}
export default Home
此时的 count
是响应性的,所有组件,不管任何层级,都能通过同样的方式引入和使用,他是全局共享的。
因此,我们只需要合理的把 useCounter.ts
放到合适的位置,就可以非常轻松的替代 pinia 的作用。最关键的是,这样的方式非常的简洁,理解成本也非常低。
这也将是 vue-vine 与 React 在应用层面最大的差别。当 react 开发者还在苦苦思索哪一个状态管理库是最佳实践时,vue-vine
开发可以用最简单最直白的方式做到同样的事情。
不管你对于 vue-vine 语法长得那么像 react 是何种看法,但是我们得相信的是,它确实有自己独特的魅力。
vue-vine 目前的完成度也非常高。我尝试过大多数常用的能力和生态都能够成功接入。
深度使用几天之后,我的总体感受就是非常舒服,它和 react 有高度一致的开发体验。因为 vue-vine 彻底拥抱函数式的原因,我也非常认可 vue 往这个方向转变。对于老手来说,他大多数时候比 react 拥有更简洁的代码结果。我相信这种方式一定会得到许多 vue 和 react 开发者的喜爱。