欢迎光临
我们一直在努力

Vue2/Vue3 迁移头秃?Renderless 架构让组件 “无缝穿梭”

Vue2/Vue3 迁移头秃?Renderless 架构让组件 “无缝穿梭”

    • 前言:当组件需要跨版本复用
    • 一、传统组件开发的痛点
    • 二、Renderless 架构:UI 与逻辑的完美解耦
      • 架构优势对比
    • 三、基础准备:@opentiny/vue-common
      • 核心 API 速览
      • 关键原理:API 兼容层
    • 四、实战:三文件架构详解
      • 文件结构
      • 1. 入口文件:index.ts
      • 2. 逻辑层:renderless.ts
      • 3. 模板层:pc.vue
    • 五、进阶技巧:让组件更强大
      • 1. 逻辑模块化
      • 2. 处理异步场景
      • 3. 访问 DOM 元素
    • 六、最佳实践与避坑指南
      • ✅ 推荐做法
      • ⚠️ 常见问题
    • 七、适用场景评估
      • ✅ 推荐使用
      • ⚠️ 谨慎使用
    • 八、总结
    • 学习资源
    • 互动讨论

前言:当组件需要跨版本复用

大家好,我是木斯佳,是 OpenTiny 开源社区的布道师和华为云 HDE 认证专家,最近在重构公司组件库时,遇到了一个棘手问题:如何在 Vue 2 和 Vue 3 之间实现组件无缝迁移?传统方案往往需要维护两套代码,不仅开发成本高,测试和维护更是噩梦。

今天要介绍的 Renderless 架构,完美解决了这个问题。它让「一次编写,到处运行」在 Vue 生态中成为现实!我结合官方的教程博文进行二次创作,带大家一起动手实践一下

一、传统组件开发的痛点

在这里插入图片描述 我们之前有一个甲方项目,原方案是基于ruoyi的vue2管理后端,已经交付了好几年了,在二期开发中,我们又陆续基于数据池为甲方做了vue3的可视化大屏。近期的需求中要求几个功能进行打通,两个项目要集成,在实际开发中大屏有很多定制化组件,如果集成到管理后端的看板中,集成难度比较大。

问题来了:

  • 这个组件只能在 Vue 3 项目中使用
  • 如果公司既有 Vue 2 老项目,又有 Vue 3 新项目怎么办?
  • 难道要为每个版本都维护一套代码?

二、Renderless 架构:UI 与逻辑的完美解耦

Renderless 的核心思想很简单:将 UI 展示与业务逻辑彻底分离。

┌─────────────────────────┐
│ 模板层 (pc.vue) │ ← 只负责渲染,不关心逻辑
└─────────────────────────┘

┌─────────────────────────┐
│ 逻辑层 (renderless.ts) │ ← 处理所有业务逻辑,纯函数
└─────────────────────────┘

┌─────────────────────────┐
│ 入口层 (index.ts) │ ← 统一出口,适配不同框架
└─────────────────────────┘

架构优势对比

特性传统组件Renderless 组件
Vue 2 支持 ❌ 需要适配 ✅ 天然支持
Vue 3 支持
逻辑复用性 困难 优秀
测试友好度 一般 极佳
代码维护性 耦合度高 高度解耦

三、基础准备:@opentiny/vue-common

在这里插入图片描述

在深入 Renderless 之前,我们要学习一下 @opentiny/vue-common的实现。这是实现跨版本兼容的「翻译官」。

核心 API 速览

import {
defineComponent, // 统一组件定义
setup, // 连接 renderless
$props, // 通用 props
$prefix, // 组件名前缀
isVue2, // 版本检测
isVue3
} from '@opentiny/vue-common'

关键原理:API 兼容层

vue-common 在底层做了智能适配:

// 在 Vue 2 环境下
if (isVue2) {
// 使用 @vue/composition-api 提供响应式 API
return Vue.extend(options)
}

// 在 Vue 3 环境下
if (isVue3) {
// 直接使用 Vue 3 原生 API
return defineComponent(options)
}

四、实战:三文件架构详解

文件结构

counter/
├── index.ts # 组件入口
├── pc.vue # 纯 UI 模板
└── renderless.ts # 业务逻辑

1. 入口文件:index.ts

import { $props, $prefix, defineComponent } from '@opentiny/vue-common'
import template from './pc.vue'

// 定义 Props
export const counterProps = {
$props, // 继承通用属性
initialValue: { type: Number, default: 0 },
step: { type: Number, default: 1 }
}

// 导出组件
export default defineComponent({
name: $prefix + 'Counter', // → TinyCounter
props: counterProps,
template
})

2. 逻辑层:renderless.ts

export const api = ['count', 'increment', 'decrement', 'isEven']

export const renderless = (
props, // 组件 props
{ reactive, computed, watch, onMounted }, // Vue hooks
{ emit, vm } // 上下文
) => {
const api = {} as any

// 响应式状态
const state = reactive({
count: props.initialValue
})

// 业务方法
const increment = () => {
state.count += props.step
emit('change', state.count)
}

const decrement = () => {
state.count -= props.step
emit('change', state.count)
}

// 计算属性
const isEven = computed(() => state.count % 2 === 0)

// 生命周期
onMounted(() => {
console.log('Counter mounted')
})

// 暴露给模板
Object.assign(api, {
count: state.count,
increment,
decrement,
isEven
})

return api
}

3. 模板层:pc.vue

<template>
<div class="tiny-counter">
<div class="display" :class="{ even: isEven }">
{{ count }}
<small>{{ isEven ? '(偶数)' : '(奇数)' }}</small>
</div>
<div class="controls">
<button @click="decrement">-</button>
<button @click="increment">+</button>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent, setup, $props } from '@opentiny/vue-common'
import { renderless, api } from './renderless'

export default defineComponent({
props: { …$props, initialValue: Number, step: Number },
emits: ['change'],
setup(props, context) {
// 关键连接点
return setup({ props, context, renderless, api })
}
})
</script>

五、进阶技巧:让组件更强大

1. 逻辑模块化

// composables/useCounter.ts
export function useCounter({ state, props, emit }) {
const methods = {
increment: () => {
state.count += props.step
emit('update:count', state.count)
},
decrement: () => {
state.count -= props.step
emit('update:count', state.count)
}
}

return methods
}

// 在 renderless 中组合使用
export const renderless = (props, hooks, context) => {
const state = reactive({ count: props.initialValue })
const { increment, decrement } = useCounter({ state, props, emit: context.emit })
// … 其他逻辑
}

2. 处理异步场景

export const renderless = (props, { nextTick }, { emit }) => {
const loadData = async () => {
state.loading = true
try {
const data = await fetchData(props.url)
state.data = data
await nextTick() // 等待 DOM 更新
emit('loaded', data)
} catch (error) {
emit('error', error)
} finally {
state.loading = false
}
}
}

3. 访问 DOM 元素

export const renderless = (props, hooks, { vm }) => {
const focusInput = () => {
// 兼容 Vue 2/Vue 3 的 refs 访问
const inputRef = vm?.$refs?.input || vm?.refs?.input
inputRef?.focus()
}

return { focusInput }
}

六、最佳实践与避坑指南

✅ 推荐做法

  • 统一使用 vue-common API

    // ✅ 正确
    import { defineComponent } from '@opentiny/vue-common'

    // ❌ 错误
    import { defineComponent } from 'vue' // 不兼容 Vue 2

  • 明确声明暴露的 API

    export const api = ['visible', 'show', 'hide', 'toggle']

  • 善用 TypeScript 类型提示

    interface CounterState {
    count: number
    history: number[]
    }

    const state: CounterState = reactive({
    count: 0,
    history: []
    })

  • ⚠️ 常见问题

    Q:响应式数据不更新?

    // ❌ 错误:直接暴露整个 state
    return { state }

    // ✅ 正确:展开 state 或使用 computed
    return {
    count: state.count,
    doubleCount: computed(() => state.count * 2)
    }

    Q:如何调试?

    export const renderless = (props, hooks, context) => {
    console.log('[Renderless] Props:', props)
    console.log('[Renderless] Context:', context)

    // 使用 Vue DevTools 查看状态
    const state = reactive({
    count: 0,
    _debug: true // 调试标记
    })
    }

    七、适用场景评估

    ✅ 推荐使用

    • 需要同时支持 Vue 2/3 的组件库
    • 复杂业务逻辑的组件
    • 需要多端适配(PC/移动/小程序)
    • 对测试覆盖率要求高的项目

    ⚠️ 谨慎使用

    • 简单展示型组件(可能过度设计)
    • 个人小项目(学习成本 vs 收益)
    • 已经确定只用单一 Vue 版本的项目

    八、总结

    Renderless 架构通过 关注点分离 的思想,将组件拆分为:

  • UI 层:专注视觉呈现
  • 逻辑层:处理所有业务逻辑
  • 适配层:抹平框架差异
  • 这种架构带来的好处是显而易见的:

    • 🚀 开发效率:一套代码,多版本运行
    • 🧪 可测试性:纯函数逻辑,易于单元测试
    • 🔧 可维护性:逻辑与 UI 解耦,修改互不影响
    • 📦 可复用性:逻辑层可跨组件、跨项目复用

    学习资源

    • @opentiny/vue-common 源码
    • OpenTiny 跨端组件库
    • Vue 2 迁移指南
    • Vue 3 Composition API

    互动讨论

    你所在的项目是否面临 Vue 2/3 迁移问题?在评论区分享你的经验和困惑,一起探讨最佳实践!


    如果觉得本文有帮助,欢迎点赞收藏🌟 关注我,获取更多前端架构实践干货!

    技术永不眠,架构无止境。选择适合的,而不是最潮的。

    赞(0)
    未经允许不得转载:171主机测评 » Vue2/Vue3 迁移头秃?Renderless 架构让组件 “无缝穿梭”
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址