博客 / 詳情

返回

Vue2/Vue3 遷移頭禿?Renderless 架構讓組件 “無縫穿梭”

本文由體驗技術團隊劉坤原創。

"一次編寫,到處運行" —— 這不是 Java 的專利,也是 Renderless 架構的座右銘!

開篇:什麼是 Renderless 架構?

🤔 傳統組件的困境

想象一下,你寫了一個超棒的 Vue 3 組件:

<!-- MyAwesomeComponent.vue -->
<template>
  <div>
    <button @click="handleClick">{{ count }}</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
const handleClick = () => {
  count.value++
}
</script>

問題來了:這個組件只能在 Vue 3 中使用!如果你的項目是 Vue 2,或者你需要同時支持 Vue 2 和 Vue 3,怎麼辦?

✨ Renderless 的解決方案

Renderless 架構將組件拆分成三個部分:

┌─────────────────────────────────────────┐
|             模板層(pc.vue)             |
|         "我只負責展示,不關心邏輯"        |
└─────────────────────────────────────────┘
              ↕️
┌─────────────────────────────────────────┐
│         邏輯層(renderless.ts)          │
│       "我是大腦,處理所有業務邏輯"        │
└─────────────────────────────────────────┘
              ↕️
┌─────────────────────────────────────────┐
│            入口層 (index.ts)           │
│         "我是門面,統一對外接口"          │
└─────────────────────────────────────────┘

核心思想:將 UI(模板)和邏輯(業務代碼)完全分離,邏輯層使用 Vue 2 和 Vue 3 都兼容的 API。

📊 為什麼需要 Renderless?

特性 傳統組件 Renderless 組件
Vue 2 支持
Vue 3 支持
邏輯複用 困難 簡單
測試友好 一般 優秀
代碼組織 耦合 解耦

🎯 適用場景

  • ✅ 需要同時支持 Vue 2 和 Vue 3 的組件庫
  • ✅ 邏輯複雜,需要模塊化管理的組件
  • ✅ 需要多端適配的組件(PC、移動端、小程序等)
  • ✅ 需要高度可測試性的組件

第一步:理解 @opentiny/vue-common(必須先掌握)

⚠️ 重要提示:為什麼必須先學習 vue-common?

在學習 Renderless 架構之前,你必須先理解 @opentiny/vue-common,因為:

  1. 它是基礎工具:Renderless 架構完全依賴 vue-common 提供的兼容層
  2. 它是橋樑:沒有 vue-common,就無法實現 Vue 2/3 的兼容
  3. 它是前提:不理解 vue-common,就無法理解 Renderless 的工作原理

打個比方vue-common 就像是你學開車前必須先了解的"方向盤、剎車、油門",而 Renderless 是"如何駕駛"的技巧。沒有基礎工具,再好的技巧也無法施展!

🤔 為什麼需要 vue-common?

想象一下,Vue 2 和 Vue 3 就像兩個説不同方言的人:

  • Vue 2this.$refs.inputthis.$emit('event')Vue.component()
  • Vue 3refs.inputemit('event')defineComponent()

如果你要同時支持兩者,難道要寫兩套代碼嗎?當然不! 這就是 @opentiny/vue-common 存在的意義。

✨ vue-common 是什麼?

@opentiny/vue-common 是一個兼容層庫,它:

  1. 統一 API:提供一套統一的 API,自動適配 Vue 2 和 Vue 3
  2. 隱藏差異:讓你無需關心底層是 Vue 2 還是 Vue 3
  3. 類型支持:提供完整的 TypeScript 類型定義

簡單來説vue-common 是一個"翻譯官",它讓 Vue 2 和 Vue 3 能夠"説同一種語言"。

🛠️ 核心 API 詳解

1. defineComponent - 組件定義的統一入口

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

// 這個函數在 Vue 2 和 Vue 3 中都能工作
export default defineComponent({
  name: 'MyComponent',
  props: { ... },
  setup() { ... }
})

工作原理

  • Vue 2:內部使用 Vue.extend()Vue.component()
  • Vue 3:直接使用 Vue 3 的 defineComponent()
  • 你只需要寫一套代碼,vue-common 會自動選擇正確的實現

2. setup - 連接 Renderless 的橋樑

import { setup } from '@opentiny/vue-common'

// 在 pc.vue 中
setup(props, context) {
  return setup({ props, context, renderless, api })
}

工作原理

  • 接收 renderless 函數和 api 數組
  • 自動處理 Vue 2/3 的差異(如 emitslotsrefs 等)
  • renderless 返回的 api 對象注入到模板中

關鍵點

// vue-common 內部會做類似這樣的處理:
function setup({ props, context, renderless, api }) {
  // Vue 2: context 包含 { emit, slots, attrs, listeners }
  // Vue 3: context 包含 { emit, slots, attrs, expose }

  // 統一處理差異
  const normalizedContext = normalizeContext(context)

  // 調用 renderless
  const apiResult = renderless(props, hooks, normalizedContext)

  // 返回給模板使用
  return apiResult
}

3. $props - 通用 Props 定義

import { $props } from '@opentiny/vue-common'

export const myComponentProps = {
  ...$props, // 繼承通用 props
  title: String
}

提供的基礎 Props

  • tiny_mode:組件模式(pc/saas)
  • customClass:自定義類名
  • customStyle:自定義樣式
  • 等等...

好處

  • 所有組件都有統一的 props 接口
  • 減少重複代碼
  • 保證一致性

4. $prefix - 組件名前綴

import { $prefix } from '@opentiny/vue-common'

export default defineComponent({
  name: $prefix + 'SearchBox' // 自動變成 'TinySearchBox'
})

作用

  • 統一組件命名規範
  • 避免命名衝突
  • 便於識別組件來源

5. isVue2 / isVue3 - 版本檢測

import { isVue2, isVue3 } from '@opentiny/vue-common'

if (isVue2) {
  // Vue 2 特定代碼
  console.log('運行在 Vue 2 環境')
} else if (isVue3) {
  // Vue 3 特定代碼
  console.log('運行在 Vue 3 環境')
}

使用場景

  • 需要針對特定版本做特殊處理時
  • 調試和日誌記錄
  • 兼容性檢查

🔍 深入理解:vue-common 如何實現兼容?

場景 1:響應式 API 兼容

// 在 renderless.ts 中
export const renderless = (props, hooks, context) => {
  const { reactive, computed, watch } = hooks

  // 這些 hooks 來自 vue-common 的兼容層
  // Vue 2: 使用 @vue/composition-api 的 polyfill
  // Vue 3: 直接使用 Vue 3 的原生 API

  const state = reactive({ count: 0 })
  const double = computed(() => state.count * 2)

  watch(
    () => state.count,
    (newVal) => {
      console.log('count changed:', newVal)
    }
  )
}

兼容原理

  • Vue 2:vue-common 內部使用 @vue/composition-api 提供 Composition API
  • Vue 3:直接使用 Vue 3 的原生 API
  • 對開發者透明,無需關心底層實現

場景 2:Emit 兼容

export const renderless = (props, hooks, { emit }) => {
  const handleClick = () => {
    // vue-common 會自動處理 Vue 2/3 的差異
    emit('update:modelValue', newValue)
    emit('change', newValue)
  }
}

兼容原理

// vue-common 內部處理(簡化版)
function normalizeEmit(emit, isVue2) {
  if (isVue2) {
    // Vue 2: emit 需要特殊處理
    return function (event, ...args) {
      // 處理 Vue 2 的事件格式
      this.$emit(event, ...args)
    }
  } else {
    // Vue 3: 直接使用
    return emit
  }
}

場景 3:Refs 訪問兼容

export const renderless = (props, hooks, { vm }) => {
  const focusInput = () => {
    // vue-common 提供了統一的訪問方式
    const inputRef = vm?.$refs?.inputRef || vm?.refs?.inputRef
    inputRef?.focus()
  }
}

兼容原理

  • Vue 2:vm.$refs.inputRef
  • Vue 3:vm.refs.inputRef
  • vue-common 提供統一的訪問方式,自動適配

📊 vue-common 提供的常用 API 列表

API 作用 Vue 2 實現 Vue 3 實現
defineComponent 定義組件 Vue.extend() defineComponent()
setup 連接 renderless Composition API polyfill 原生 setup
$props 通用 props 對象展開 對象展開
$prefix 組件前綴 字符串常量 字符串常量
isVue2 Vue 2 檢測 true false
isVue3 Vue 3 檢測 false true

🎯 使用 vue-common 的最佳實踐

✅ DO(推薦)

  1. 始終使用 vue-common 提供的 API

    // ✅ 好
    import { defineComponent, setup } from '@opentiny/vue-common'
    
    // ❌ 不好
    import { defineComponent } from 'vue' // 這樣只能在 Vue 3 中使用
  2. 使用 $props 繼承通用屬性

    // ✅ 好
    export const props = {
      ...$props,
      customProp: String
    }
  3. 使用 $prefix 統一命名

    // ✅ 好
    name: $prefix + 'MyComponent'

❌ DON'T(不推薦)

  1. 不要直接使用 Vue 2/3 的原生 API

    // ❌ 不好
    import Vue from 'vue' // 只能在 Vue 2 中使用
    import { defineComponent } from 'vue' // 只能在 Vue 3 中使用
  2. 不要硬編碼組件名前綴

    // ❌ 不好
    name: 'TinyMyComponent' // 硬編碼前綴
    
    // ✅ 好
    name: $prefix + 'MyComponent' // 使用變量

🔗 總結

@opentiny/vue-common 是 Renderless 架構的基石

  • 🎯 目標:讓一套代碼在 Vue 2 和 Vue 3 中都能運行
  • 🛠️ 手段:提供統一的 API 和兼容層
  • 結果:開發者無需關心底層差異,專注於業務邏輯

記住:使用 Renderless 架構時,必須使用 vue-common 提供的 API,這是實現跨版本兼容的關鍵!

🎓 學習檢查點

在繼續學習之前,請確保你已經理解:

  • defineComponent 的作用和用法
  • setup 函數如何連接 renderless
  • $props$prefix 的用途
  • vue-common 如何實現 Vue 2/3 兼容

如果你對以上內容還有疑問,請重新閲讀本節。理解 vue-common 是學習 Renderless 的前提!

第二步:核心概念 - 三大文件

現在你已經理解了 vue-common,我們可以開始學習 Renderless 架構的核心了!

📋 文件結構

一個標準的 Renderless 組件包含三個核心文件:

my-component/
├── index.ts          # 入口文件:定義組件和 props
├── pc.vue            # 模板文件:只負責 UI 展示
└── renderless.ts     # 邏輯文件:處理所有業務邏輯

1. 三大核心文件詳解

📄 index.ts - 組件入口

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

// 定義組件的 props
export const myComponentProps = {
  ...$props, // 繼承通用 props
  title: {
    type: String,
    default: 'Hello'
  },
  count: {
    type: Number,
    default: 0
  }
}

// 導出組件
export default defineComponent({
  name: $prefix + 'MyComponent', // 自動添加前綴
  props: myComponentProps,
  ...template // 展開模板配置
})

關鍵點

  • $props:提供 Vue 2/3 兼容的基礎 props
  • $prefix:統一的組件名前綴(如 Tiny
  • defineComponent:兼容 Vue 2/3 的組件定義函數

🎨 pc.vue - 模板文件

<template>
  <div class="my-component">
    <h1>{{ title }}</h1>
    <button @click="handleClick">點擊了 {{ count }} 次</button>
    <p>{{ message }}</p>
  </div>
</template>

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

export default defineComponent({
  props: {
    ...$props,
    title: String,
    count: Number
  },
  setup(props, context) {
    // 關鍵:通過 setup 函數連接 renderless
    return setup({ props, context, renderless, api })
  }
})
</script>

關鍵點

  • 模板只負責 UI 展示
  • 所有邏輯都從 renderless 函數獲取
  • setup 函數是連接模板和邏輯的橋樑

🧠 renderless.ts - 邏輯層

// 定義暴露給模板的 API
export const api = ['count', 'message', 'handleClick']

// 初始化狀態
const initState = ({ reactive, props }) => {
  const state = reactive({
    count: props.count || 0,
    message: '歡迎使用 Renderless 架構!'
  })
  return state
}

// 核心:renderless 函數
export const renderless = (props, { reactive, computed, watch, onMounted }, { emit, nextTick, vm }) => {
  const api = {} as any
  const state = initState({ reactive, props })

  // 定義方法
  const handleClick = () => {
    state.count++
    emit('update:count', state.count)
  }

  // 計算屬性
  const message = computed(() => {
    return `你已經點擊了 ${state.count} 次!`
  })

  // 生命週期
  onMounted(() => {
    console.log('組件已掛載')
  })

  // 暴露給模板
  Object.assign(api, {
    count: state.count,
    message,
    handleClick
  })

  return api
}

關鍵點

  • api 數組:聲明要暴露給模板的屬性和方法
  • renderless 函數接收三個參數:

    1. props:組件屬性
    2. hooks:Vue 的響應式 API(reactive, computed, watch 等)
    3. context:上下文(emit, nextTick, vm 等)
  • 返回的 api 對象會被注入到模板中

第三步:實戰演練 - 從零開始改造組件

現在你已經掌握了:

  • vue-common 的核心 API
  • ✅ Renderless 架構的三大文件

讓我們通過一個完整的例子,將理論知識轉化為實踐!

🎯 目標

將一個簡單的計數器組件改造成 Renderless 架構,支持 Vue 2 和 Vue 3。

📝 步驟 1:創建文件結構

my-counter/
├── index.ts          # 入口文件
├── pc.vue            # 模板文件
└── renderless.ts     # 邏輯文件

📝 步驟 2:編寫入口文件

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

export const counterProps = {
  ...$props,
  initialValue: {
    type: Number,
    default: 0
  },
  step: {
    type: Number,
    default: 1
  }
}

export default defineComponent({
  name: $prefix + 'Counter',
  props: counterProps,
  ...template
})

📝 步驟 3:編寫邏輯層

// renderless.ts
export const api = ['count', 'increment', 'decrement', 'reset', 'isEven']

const initState = ({ reactive, props }) => {
  return reactive({
    count: props.initialValue || 0
  })
}

export const renderless = (props, { reactive, computed, watch }, { emit, vm }) => {
  const api = {} as any
  const state = initState({ reactive, props })

  // 增加
  const increment = () => {
    state.count += props.step
    emit('change', state.count)
  }

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

  // 重置
  const reset = () => {
    state.count = props.initialValue || 0
    emit('change', state.count)
  }

  // 計算屬性:是否為偶數
  const isEven = computed(() => {
    return state.count % 2 === 0
  })

  // 監聽 count 變化
  watch(
    () => state.count,
    (newVal, oldVal) => {
      console.log(`計數從 ${oldVal} 變為 ${newVal}`)
    }
  )

  // 暴露 API
  Object.assign(api, {
    count: state.count,
    increment,
    decrement,
    reset,
    isEven
  })

  return api
}

📝 步驟 4:編寫模板

<!-- pc.vue -->
<template>
  <div class="tiny-counter">
    <div class="counter-display">
      <span :class="{ 'even': isEven, 'odd': !isEven }">
        {{ count }}
      </span>
      <small v-if="isEven">(偶數)</small>
      <small v-else>(奇數)</small>
    </div>

    <div class="counter-buttons">
      <button @click="decrement">-</button>
      <button @click="reset">重置</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>

<style scoped>
.tiny-counter {
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  text-align: center;
}

.counter-display {
  font-size: 48px;
  margin-bottom: 20px;
}

.counter-display .even {
  color: green;
}

.counter-display .odd {
  color: blue;
}

.counter-buttons button {
  margin: 0 5px;
  padding: 10px 20px;
  font-size: 18px;
  cursor: pointer;
}
</style>

🎉 完成!

現在這個組件可以在 Vue 2 和 Vue 3 中無縫使用了!

<!-- Vue 2 或 Vue 3 都可以 -->
<template>
  <tiny-counter :initial-value="10" :step="2" @change="handleChange" />
</template>

第四步:進階技巧

恭喜你!如果你已經完成了實戰演練,説明你已經掌握了 Renderless 架構的基礎。現在讓我們學習一些進階技巧,讓你的組件更加優雅和強大。

1. 模塊化:使用 Composables

當邏輯變得複雜時,可以將功能拆分成多個 composables:

// composables/use-counter.ts
export function useCounter({ state, props, emit }) {
  const increment = () => {
    state.count += props.step
    emit('change', state.count)
  }

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

  return { increment, decrement }
}

// composables/use-validation.ts
export function useValidation({ state }) {
  const isEven = computed(() => state.count % 2 === 0)
  const isPositive = computed(() => state.count > 0)

  return { isEven, isPositive }
}

// renderless.ts
import { useCounter } from './composables/use-counter'
import { useValidation } from './composables/use-validation'

export const renderless = (props, hooks, context) => {
  const api = {} as any
  const state = initState({ reactive, props })

  // 使用 composables
  const { increment, decrement } = useCounter({ state, props, emit })
  const { isEven, isPositive } = useValidation({ state })

  Object.assign(api, {
    count: state.count,
    increment,
    decrement,
    isEven,
    isPositive
  })

  return api
}

2. 訪問組件實例(vm)

有時候需要訪問組件實例,比如獲取 refs:

export const renderless = (props, hooks, { vm }) => {
  const api = {} as any

  const focusInput = () => {
    // Vue 2: vm.$refs.inputRef
    // Vue 3: vm.refs.inputRef
    const inputRef = vm?.$refs?.inputRef || vm?.refs?.inputRef
    if (inputRef) {
      inputRef.focus()
    }
  }

  // 存儲 vm 到 state,方便在模板中使用
  state.instance = vm

  return api
}

3. 處理 Slots

在 Vue 2 中,slots 的訪問方式不同:

export const renderless = (props, hooks, { vm, slots }) => {
  const api = {} as any
  const state = initState({ reactive, props })

  // 存儲 vm 和 slots
  state.instance = vm

  // Vue 2 中需要手動設置 slots
  if (vm && slots) {
    vm.slots = slots
  }

  return api
}

在模板中檢查 slot:

<template>
  <div v-if="state.instance?.$slots?.default || state.instance?.slots?.default">
    <slot></slot>
  </div>
</template>

4. 生命週期處理

export const renderless = (props, hooks, context) => {
  const { onMounted, onBeforeUnmount, onUpdated } = hooks

  // 組件掛載後
  onMounted(() => {
    console.log('組件已掛載')
    // 添加事件監聽
    document.addEventListener('click', handleDocumentClick)
  })

  // 組件更新後
  onUpdated(() => {
    console.log('組件已更新')
  })

  // 組件卸載前
  onBeforeUnmount(() => {
    console.log('組件即將卸載')
    // 清理事件監聽
    document.removeEventListener('click', handleDocumentClick)
  })

  return api
}

5. 使用Watch監聽

export const renderless = (props, hooks, context) => {
  const { watch } = hooks

  // 監聽單個值
  watch(
    () => state.count,
    (newVal, oldVal) => {
      console.log(`count 從 ${oldVal} 變為 ${newVal}`)
    }
  )

  // 監聽多個值
  watch([() => state.count, () => props.step], ([newCount, newStep], [oldCount, oldStep]) => {
    console.log('count 或 step 發生了變化')
  })

  // 深度監聽對象
  watch(
    () => state.user,
    (newUser) => {
      console.log('user 對象發生了變化', newUser)
    },
    { deep: true }
  )

  // 立即執行
  watch(
    () => props.initialValue,
    (newVal) => {
      state.count = newVal
    },
    { immediate: true }
  )

  return api
}

常見問題與解決方案

❓ 問題 1:為什麼我的響應式數據不更新?

原因:在 renderless 中,需要將響應式數據暴露到 api 對象中。

// ❌ 錯誤:直接返回 state
Object.assign(api, {
  state // 這樣模板無法訪問 state.count
})

// ✅ 正確:展開 state 或明確暴露屬性
Object.assign(api, {
  count: state.count, // 明確暴露
  message: state.message
})

// 或者使用 computed
const count = computed(() => state.count)
Object.assign(api, {
  count // 使用 computed 包裝
})

❓ 問題 2:如何在模板中訪問組件實例?

解決方案:將 vm 存儲到 state 中。

export const renderless = (props, hooks, { vm }) => {
  const state = initState({ reactive, props })
  state.instance = vm // 存儲實例

  return api
}

在模板中:

<template>
  <div>
    <!-- 訪問 refs -->
    <input ref="inputRef" />
    <button @click="focusInput">聚焦</button>
  </div>
</template>
const focusInput = () => {
  const inputRef = state.instance?.$refs?.inputRef || state.instance?.refs?.inputRef
  inputRef?.focus()
}

❓ 問題 3:Vue 2 和 Vue 3 的 emit 有什麼區別?

解決方案:使用 @opentiny/vue-common 提供的兼容層。

export const renderless = (props, hooks, { emit: $emit }) => {
  // 兼容處理
  const emit = props.emitter ? props.emitter.emit : $emit

  const handleClick = () => {
    // 直接使用 emit,兼容層會處理差異
    emit('update:modelValue', newValue)
    emit('change', newValue)
  }

  return api
}

❓ 問題 4:如何處理異步操作?

解決方案:使用 nextTick 確保 DOM 更新。

export const renderless = (props, hooks, { nextTick }) => {
  const handleAsyncUpdate = async () => {
    // 執行異步操作
    const data = await fetchData()
    state.data = data

    // 等待 DOM 更新
    await nextTick()

    // 此時可以安全地操作 DOM
    const element = state.instance?.$el || state.instance?.el
    if (element) {
      element.scrollIntoView()
    }
  }

  return api
}

❓ 問題 5:如何調試 Renderless 組件?

技巧

  1. 使用 console.log
export const renderless = (props, hooks, context) => {
  console.log('Props:', props)
  console.log('State:', state)
  console.log('Context:', context)

  // 在關鍵位置添加日誌
  const handleClick = () => {
    console.log('Button clicked!', state.count)
    // ...
  }

  return api
}
  1. 使用 Vue DevTools

    • 在模板中添加調試信息
    • 使用 state 存儲調試數據
  2. 斷點調試

    • renderless.ts 中設置斷點
    • 檢查 api 對象的返回值

最佳實踐

✅ DO(推薦做法)

  1. 模塊化組織代碼

    src/
    ├── index.ts
    ├── pc.vue
    ├── renderless.ts
    ├── composables/
    │   ├── use-feature1.ts
    │   └── use-feature2.ts
    └── utils/
        └── helpers.ts
  2. 明確聲明 API

    // 在文件頂部聲明所有暴露的 API
    export const api = ['count', 'increment', 'decrement', 'isEven']
  3. 使用 TypeScript

    interface State {
      count: number
      message: string
    }
    
    const initState = ({ reactive, props }): State => {
      return reactive({
        count: props.initialValue || 0,
        message: 'Hello'
      })
    }
  4. 處理邊界情況

    const handleClick = () => {
      if (props.disabled) {
        return // 提前返回
      }
    
      try {
        // 業務邏輯
      } catch (error) {
        console.error('Error:', error)
        emit('error', error)
      }
    }

❌ DON'T(不推薦做法)

  1. 不要在模板中寫邏輯

    <!-- ❌ 不好 -->
    <template>
      <div>{{ count + 1 }}</div>
    </template>
    
    <!-- ✅ 好 -->
    <template>
      <div>{{ nextCount }}</div>
    </template>
    const nextCount = computed(() => state.count + 1)
  2. 不要直接修改 props

    // ❌ 不好
    props.count++ // 不要這樣做!
    
    // ✅ 好
    state.count = props.count + 1
    emit('update:count', state.count)
  3. 不要忘記清理資源

    // ❌ 不好
    onMounted(() => {
      document.addEventListener('click', handler)
      // 忘記清理
    })
    
    // ✅ 好
    onMounted(() => {
      document.addEventListener('click', handler)
    })
    
    onBeforeUnmount(() => {
      document.removeEventListener('click', handler)
    })

🎓 總結

Renderless 架構的核心思想是關注點分離

  • 模板層:只負責 UI 展示
  • 邏輯層:處理所有業務邏輯
  • 入口層:統一對外接口

通過這種方式,我們可以:

  • ✅ 同時支持 Vue 2 和 Vue 3
  • ✅ 提高代碼的可維護性
  • ✅ 增強代碼的可測試性
  • ✅ 實現邏輯的模塊化複用

🚀 下一步

  1. 查看 @opentiny/vue-search-box 的完整源碼
  2. 嘗試改造自己的組件
  3. 探索更多高級特性

📚 參考資源

  • @opentiny/vue-common 源碼
  • @opentiny/vue-search-box 文檔
  • Vue 2 官方文檔
  • Vue 3 官方文檔

Happy Coding! 🎉

記住:Renderless 不是魔法,而是一種思維方式。當你理解了它,你會發現,原來組件可以這樣寫!

關於OpenTiny

歡迎加入 OpenTiny 開源社區。添加微信小助手:opentiny-official 一起參與交流前端技術~
OpenTiny 官網:https://opentiny.design
OpenTiny 代碼倉庫:https://github.com/opentiny
TinyVue 源碼:https://github.com/opentiny/tiny-vue
TinyEngine 源碼: https://github.com/opentiny/tiny-engine
歡迎進入代碼倉庫 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~ 如果你也想要共建,可以進入代碼倉庫,找到 good first issue 標籤,一起參與開源貢獻~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.