前言
大家在做後台系統開發時,有沒有遇到過這樣的場景?運營同事跑過來説:“我想要個簡單的庫存報表,這就這幾個字段,能不能馬上弄好?”
這時候,你看着手頭堆積如山的需求,心裏可能在想:要是能直接跟電腦説一句“給我個庫存表”,界面就能自己長出來該多好啊!
這就是 NL2UI (Natural Language to User Interface) 的終極夢想——用自然語言直接生成界面。但説實話,讓 AI 直接寫 Vue 代碼稍微有點“嚇人”,代碼質量不可控不説,改起來還費勁。
今天,我們換個思路。我們不追求一步到位的“全自動”,而是基於 華為雲 DevUI MateChat 組件,打造一個“受限但絕對可靠”的 UI 生成引擎。
我們會用一套自己定義的 JSON DSL(領域特定語言) 作為中間層,讓 AI 做“填空題”,而不是“作文題”。這樣既利用了 AI 的理解能力,又保證了生成的界面是 100% 可用的。
為了方便大家驗證,我把這個引擎的完整代碼都開源了。大家可以去 GitCode 倉庫 https://gitcode.com/kaminono/MateChatNL2UIEngine 看看源碼,或者直接點這個 https://mate-chat-nl-2-ui-engine-components.vercel.app/ 在線體驗一下“説話變界面”。
一、 架構思考:為什麼我們需要一個“中間商”?
在動手寫代碼之前,我們得先定個基調。要實現 NL2UI,我們面臨兩個選擇:是讓 AI 直接吐出 Vue 代碼,還是讓它生成一個 JSON 數據?
我們堅定地選擇了後者。直接生成代碼就像是“開盲盒”,你永遠不知道 AI 會不會引入什麼奇怪的依賴。
而生成 JSON DSL 就穩妥多了。我們把 DevUI 的組件——比如 d-card、d-form、d-chart——看作是樂高積木。我們只允許 AI 挑選這些積木來搭建頁面。
我們可以把整個流程看作一個流水線:
- Input: 用户在 MateChat 輸入自然語言。
- Reasoning: LLM 基於 System Prompt 進行意圖識別,轉化為標準 JSON。
- Parser: 前端引擎攔截消息,正則清洗數據,校驗 JSON 合法性。
- Render: 遞歸組件讀取 JSON,動態映射為 DevUI 組件。
這種“控制反轉”的設計,是我們保證系統高可靠性的基石。
二、 核心實現:給 AI 立規矩,教它説“DSL”
搞定了架構,我們來看看核心代碼是怎麼實現的。這個引擎的“大腦”在 useNlParser.ts 文件裏。
我們需要利用 Prompt Engineering(提示詞工程),把我們的 DSL 語法“喂”給大模型。我們得明確告訴它:你只能用白名單裏的組件,輸出格式必須是標準的 JSON。
看看這段真實的代碼,我們在 System Prompt 裏做了非常嚴格的約束:
// playground/src/nl2ui-engine/composables/useNlParser.tsconst systemPrompt = `
你是一個專業的前端 UI 構建專家。你的任務是將用户的自然語言需求轉換為特定的 UI DSL (JSON 格式)。
### 🔴 嚴禁使用不存在的組件!只能使用以下白名單:
1. 佈局: "d-row", "d-col"
2. 容器: "d-card" (必須包含 children), "d-form" (children 必須是 d-form-item)
3. 表單項: "d-form-item" (props: label), "d-input", "d-select", "d-button"
4. 圖表: "simple-stat", "simple-chart"
### 輸出格式規範 (JSON)
必須嚴格遵守以下 JSON 結構,不要包含 markdown 代碼塊標記:
{
"page": { "title": "頁面標題", "layout": "grid" | "default" },
"components": [
{
"component": "d-card",
"props": { "title": "卡片標題" },
"children": [ ... ]
}
]
}
`;
通過這種方式,無論用户怎麼描述需求,AI 最終吐出來的都是我們能看懂、能渲染的標準數據。這就像是給 AI 戴上了“緊箍咒”,讓它的創造力在規則的軌道上運行。
在實際開發中,LLM 經常會在返回的 JSON 外面包裹 Markdown 標記(如 ```json ... ```)。如果不處理,JSON.parse 必掛。 我們在解析層做了一層“清洗”:
// 解析邏輯片段
const parseResponse = (content: string) => {
// 1. 利用正則提取最外層的 {} 內容,去除廢話和 markdown 符號
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (!jsonMatch) return null;
try {
return JSON.parse(jsonMatch[0]);
} catch (e) {
console.error("JSON 解析失敗,AI 生成了非法格式", e);
// 這裏甚至可以觸發一個重試機制
return null;
}
}
三、 渲染引擎:把 JSON 變現為 DevUI 組件
拿到了 JSON 數據,下一步就是把它變成真實的界面。我們在 DslRenderer.vue 裏實現了一個遞歸渲染器。
這個組件的設計非常巧妙,它利用了 Vue 的 h() 函數和 defineAsyncComponent。我們建立了一個組件註冊表,按需加載 DevUI 的組件。
這裏有個關鍵點:對於 AI 可能產生的“幻覺”(比如生成了不存在的組件),我們做了兜底處理。
// playground/src/nl2ui-engine/components/DslRenderer.vue// 1. 建立組件白名單映射const componentRegistry: Record<string, any> = {
'd-card': defineAsyncComponent(() => import('vue-devui/card')),
'd-form': defineAsyncComponent(() => import('vue-devui/form')),
'd-input': defineAsyncComponent(() => import('vue-devui/input')),
// ... 其他組件
};
// 2. 核心渲染函數const renderNode = (node: any): any => {
// 兜底策略:如果 AI 生成了純文本,直接渲染文本if (typeof node === 'string') return String(node);
let Component = componentRegistry[node.component];
// 錯誤處理:遇到未知組件,渲染一個紅框提示,而不是讓頁面崩潰if (!Component) {
return h('div', { style: 'border: 1px dashed red;' }, `[未知: ${node.component}]`);
}
// 遞歸渲染子節點const children = node.children?.map(renderNode);
return h(Component, node.props, { default: () => children });
};
這段代碼保證了渲染器的健壯性。哪怕 AI 偶爾“發瘋”,我們的頁面也不會白屏,開發者一眼就能看出是哪裏出了問題。
遞歸渲染樹 (The Recursive Magic)是引擎最精妙的地方。因為 UI 結構是樹形的(Card 裏有 Row,Row 裏有 Col,Col 裏有 Button),我們的渲染函數必須是遞歸的。
const renderNode = (node: any): any => {
if (typeof node === 'string') return node;
const Component = componentRegistry[node.component];
if (!Component) return h('div', { style: 'color:red' }, `[未知組件: ${node.component}]`);
// 核心:處理 props 和 children
// 1. 透傳 AI 生成的屬性 (如 label, placeholder)
const props = { ...node.props };
// 2. 遞歸構建子節點
const children = node.children
? { default: () => node.children.map(renderNode) } // 插槽形式傳遞子節點
: null;
return h(Component, props, children);
};
這段代碼僅用十幾行,就實現了理論上無限嵌套的 UI 構建能力。
四、 價值閉環:不僅能看,還能“帶走”
如果只能在預覽裏看,那這個工具充其量只是個玩具。為了讓它真正產生價值,我們必須實現“從對話到源碼”的閉環。
試想一下,你讓 AI 生成了一個複雜的表單,覺得效果不錯。這時候,你肯定不想照着預覽圖再去手寫一遍代碼吧?
所以,我們開發了 useCodeGenerator.ts。它能把當前的 JSON DSL 逆向編譯成標準的 Vue SFC(單文件組件)代碼。
// playground/src/nl2ui-engine/composables/useCodeGenerator.tsconst generateVueCode = (dsl: UiDsl) => {
// 1. 逆向生成 Templateconst templateBody = dsl.components
.map(node => generateTemplateNode(node, 2))
.join('\n');
// 2. 智能分析依賴,生成 Scriptconst imports = analyzeImports(dsl.components);
// 3. 拼接成完整的 Vue 文件字符串return `<template>
<div class="generated-page">
${templateBody}
</div>
</template>
<script setup>
import { ${imports.join(', ')} } from 'vue-devui';
</script>`;
};
在我們的 Demo 右側,專門做了一個“查看源碼”的 Tab。點擊它,你就能複製這段生成的代碼,直接粘貼到你的項目裏。這才是真正的提效。
五、 場景演示:MateChat 的“雙面”能力
最後,我們看看這套系統在實際場景中的表現。我們設計了一個“左指令、右預覽”的佈局。
左邊是大家熟悉的 MateChat 聊天窗口,它作為交互的入口。用户在這裏輸入自然語言,比如“幫我生成一個銷售看板,要看總收入和活躍用户”。
MateChat 會顯示“正在構建組件樹...”,幾秒鐘後,右邊的預覽區就會實時渲染出一個包含數據卡片和圖表的 Dashboard。
如果你輸入“創建一個用户註冊表單,包含用户名和密碼”,右側瞬間就會變成一個帶有校驗規則的 DevUI 表單。
這種“即問即答、即答即現”的體驗,徹底改變了我們構建 UI 的方式。
六、 總結與展望
通過這個項目,我們驗證了一個核心觀點:受限的 DSL 反而是 AI 落地的最佳路徑。
我們沒有追求讓 AI 直接寫出完美的代碼,而是利用 MateChat 做交互,利用 DSL 做約束,利用 DevUI 做渲染。這套組合拳打下來,既保證了系統的穩定性,又發揮了 AI 的靈活性。
未來,這套架構還有很大的想象空間。比如,我們可以把 DSL 餵給後端,直接生成數據庫模型;或者結合語音識別,實現“動動嘴做軟件”的科幻場景。
希望這個開源項目能給大家帶來一點啓發,也歡迎大家來 GitCode 提 PR,我們一起把這個 NL2UI 引擎打磨得更強大!
附官方鏈接:
DevUI
MateChat - 輕鬆構建你的AI應用