本文由體驗技術團隊劉坤原創。
"一次編寫,到處運行" —— 這不是 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,因為:
- 它是基礎工具:Renderless 架構完全依賴
vue-common提供的兼容層 - 它是橋樑:沒有
vue-common,就無法實現 Vue 2/3 的兼容 - 它是前提:不理解
vue-common,就無法理解 Renderless 的工作原理
打個比方:vue-common 就像是你學開車前必須先了解的"方向盤、剎車、油門",而 Renderless 是"如何駕駛"的技巧。沒有基礎工具,再好的技巧也無法施展!
🤔 為什麼需要 vue-common?
想象一下,Vue 2 和 Vue 3 就像兩個説不同方言的人:
- Vue 2:
this.$refs.input、this.$emit('event')、Vue.component() - Vue 3:
refs.input、emit('event')、defineComponent()
如果你要同時支持兩者,難道要寫兩套代碼嗎?當然不! 這就是 @opentiny/vue-common 存在的意義。
✨ vue-common 是什麼?
@opentiny/vue-common 是一個兼容層庫,它:
- 統一 API:提供一套統一的 API,自動適配 Vue 2 和 Vue 3
- 隱藏差異:讓你無需關心底層是 Vue 2 還是 Vue 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 的差異(如
emit、slots、refs等) - 將
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(推薦)
-
始終使用 vue-common 提供的 API
// ✅ 好 import { defineComponent, setup } from '@opentiny/vue-common' // ❌ 不好 import { defineComponent } from 'vue' // 這樣只能在 Vue 3 中使用 -
使用 $props 繼承通用屬性
// ✅ 好 export const props = { ...$props, customProp: String } -
使用 $prefix 統一命名
// ✅ 好 name: $prefix + 'MyComponent'
❌ DON'T(不推薦)
-
不要直接使用 Vue 2/3 的原生 API
// ❌ 不好 import Vue from 'vue' // 只能在 Vue 2 中使用 import { defineComponent } from 'vue' // 只能在 Vue 3 中使用 -
不要硬編碼組件名前綴
// ❌ 不好 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函數接收三個參數:props:組件屬性hooks:Vue 的響應式 API(reactive, computed, watch 等)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 組件?
技巧:
- 使用 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
}
-
使用 Vue DevTools:
- 在模板中添加調試信息
- 使用
state存儲調試數據
-
斷點調試:
- 在
renderless.ts中設置斷點 - 檢查
api對象的返回值
- 在
最佳實踐
✅ DO(推薦做法)
-
模塊化組織代碼
src/ ├── index.ts ├── pc.vue ├── renderless.ts ├── composables/ │ ├── use-feature1.ts │ └── use-feature2.ts └── utils/ └── helpers.ts -
明確聲明 API
// 在文件頂部聲明所有暴露的 API export const api = ['count', 'increment', 'decrement', 'isEven'] -
使用 TypeScript
interface State { count: number message: string } const initState = ({ reactive, props }): State => { return reactive({ count: props.initialValue || 0, message: 'Hello' }) } -
處理邊界情況
const handleClick = () => { if (props.disabled) { return // 提前返回 } try { // 業務邏輯 } catch (error) { console.error('Error:', error) emit('error', error) } }
❌ DON'T(不推薦做法)
-
不要在模板中寫邏輯
<!-- ❌ 不好 --> <template> <div>{{ count + 1 }}</div> </template> <!-- ✅ 好 --> <template> <div>{{ nextCount }}</div> </template>const nextCount = computed(() => state.count + 1) -
不要直接修改 props
// ❌ 不好 props.count++ // 不要這樣做! // ✅ 好 state.count = props.count + 1 emit('update:count', state.count) -
不要忘記清理資源
// ❌ 不好 onMounted(() => { document.addEventListener('click', handler) // 忘記清理 }) // ✅ 好 onMounted(() => { document.addEventListener('click', handler) }) onBeforeUnmount(() => { document.removeEventListener('click', handler) })
🎓 總結
Renderless 架構的核心思想是關注點分離:
- 模板層:只負責 UI 展示
- 邏輯層:處理所有業務邏輯
- 入口層:統一對外接口
通過這種方式,我們可以:
- ✅ 同時支持 Vue 2 和 Vue 3
- ✅ 提高代碼的可維護性
- ✅ 增強代碼的可測試性
- ✅ 實現邏輯的模塊化複用
🚀 下一步
- 查看
@opentiny/vue-search-box的完整源碼 - 嘗試改造自己的組件
- 探索更多高級特性
📚 參考資源
- @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 標籤,一起參與開源貢獻~