深度使用 Vue Vine 四天之后,谈谈我的使用体验

文摘   2024-07-11 21:49   广东  

当我在 Vue Conf 大会中看到 Vue Vine 这种新的开发方式之后,我非常的激动。因为我确实非常喜欢这种语法。因此在周日当天,我就通过自己的摸索跑通了一个 demo,并写了一篇文章跟大家介绍它。

又一次变革,Vue 更彻底的拥抱了函数式

这篇文章传播有点广泛,甚至还被 Vue Vine 作者看到。当然对于这种新的开发方式,从评论区中能够感受到,许多人并不是那么欢迎它的出现。所以我决定更加深入的使用它之后,再重新写一篇文章,结合它与 React 的差异,跟大家分享一下深入使用之后的真实感受。

在这几天时间里,我使用 Vue Vine,写了一个常用技术点覆盖面还算齐全的小网站。目前长这个样子。当然这个只是我本地的一个 demo,主要用于我自己学习和练习使用。

接下来,我就以完成的这个网站为例,给大家介绍 Vue Vine 的深度使用体验。也进一步跟大家分享为什么我会如此喜欢它。在这个项目中,我做的事情主要包括:

  • 0、一些常用的学习案例,counter/todoList/tab
  • 1、自定义组件
  • 2、集成 vue router 并已正常使用
  • 3、集成 pinia 并已正常使用
  • 4、集成 tailwindcss
  • 5、自定义 hook
  • 6、一个文件中定义多个组件
  • 7、普通接口请求
  • 8、列表请求
  • 9、分页列表请求
  • 10、其他三方工具的兼容尝试,例如 mdx

一、并不顺利的体验过程

在完成项目的过程中我遇到了很多问题,因此这几天我与 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 模板中,还不支持给目标代码添加注释的快捷键等一些细节问题。

但是相信未来很快就会得到解决。

二、配置一些独特的 snippet

我们先来简单看一下,在 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>
  `
}

六、我们可能不再需要 Pinia 了

当然,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 开发者的喜爱。

当然,React 19 新提出的 use hook 确实独树一帜,他在 React 并发模式的底座之下,又开辟出来了一种新的开发思路。

如果你想要进一步学习我在文中提到的开关思维,你可以点击下面这篇文章购买

React 哲学

如果你想要系统的学习 React 19,你可以赞赏我的任意文章 30 元,即可购买我的付费小册《React 19》,之后请添加我的微信好友 icanmeetu 获取激活码。

usehook.cn

这波能反杀
往者不可谏,来者犹可追
 最新文章