← Назад к вопросам
Как сделать приложение на Vue с динамическими UI и config структуры UI без пересбора приложения?
1.7 Middle🔥 111 комментариев
#Vue.js
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Динамический UI в Vue без пересборки: архитектура и практика
Это продвинутый вопрос на конфигурируемость. Покажу несколько паттернов от простого к сложному.
1. Проблема и её суть
Без динамичности:
// hardcoded UI - нужно пересбирать при каждом изменении
<template>
<div>
<Input label="Username" name="username" />
<Input label="Email" name="email" />
<Button>Submit</Button>
</div>
</template>
С динамичностью:
// config-driven - меняем config, UI перестраивается сам
<template>
<div>
<component
v-for="field in formConfig"
:key="field.name"
:is="getComponent(field.type)"
:config="field"
/>
</div>
</template>
2. Простой подход: Config + v-for
Самый популярный паттерн:
// config.ts
export const formConfig = [
{
type: "input",
name: "username",
label: "Username",
required: true,
validation: "minLength:3"
},
{
type: "input",
name: "email",
label: "Email",
required: true,
validation: "email"
},
{
type: "select",
name: "role",
label: "Role",
options: ["admin", "user", "guest"]
},
{
type: "checkbox",
name: "terms",
label: "I agree to terms"
}
];
// App.vue
<template>
<form @submit.prevent="handleSubmit">
<component
v-for="field in formConfig"
:key="field.name"
:is="componentMap[field.type]"
:field="field"
:value="formData[field.name]"
@update="(value) => formData[field.name] = value"
/>
<button type="submit">Submit</button>
</form>
</template>
<script setup>
import { reactive } from "vue";
import FormInput from "./components/FormInput.vue";
import FormSelect from "./components/FormSelect.vue";
import FormCheckbox from "./components/FormCheckbox.vue";
const formConfig = ref(/* JSON config */);
const formData = reactive({});
const componentMap = {
input: FormInput,
select: FormSelect,
checkbox: FormCheckbox
};
const handleSubmit = () => {
console.log(formData);
};
</script>
Преимущества:
- Нет пересборки
- Config может быть JSON с сервера
- Легко расширять новые поля
3. Решение с динамическими компонентами
// components/DynamicField.vue
<template>
<div class="field-wrapper">
<label>{{ field.label }}</label>
<component
:is="getComponent(field.type)"
:field="field"
:value="modelValue"
@update:model-value="$emit("update:modelValue", $event)"
/>
<span v-if="error" class="error">{{ error }}</span>
</div>
</template>
<script setup>
import { defineAsyncComponent } from "vue";
const props = defineProps({
field: Object,
modelValue: [String, Number, Boolean, Array]
});
const emit = defineEmits(["update:modelValue"]);
// Ленивая загрузка компонентов
const components = {
input: defineAsyncComponent(() => import("./fields/InputField.vue")),
select: defineAsyncComponent(() => import("./fields/SelectField.vue")),
checkbox: defineAsyncComponent(() => import("./fields/CheckboxField.vue")),
textarea: defineAsyncComponent(() => import("./fields/TextAreaField.vue")),
date: defineAsyncComponent(() => import("./fields/DateField.vue")),
radio: defineAsyncComponent(() => import("./fields/RadioField.vue"))
};
const getComponent = (type) => {
return components[type] || components.input;
};
// Валидация
const error = computed(() => {
if (!props.field.validation) return "";
// Валидирруем на лету
return validateField(props.field, props.modelValue);
});
</script>
4. Сложный случай: Иерархические конфиги
// Для сложных форм с условиями
export const complexConfig = {
title: "User Registration",
sections: [
{
id: "personal",
title: "Personal Info",
fields: [
{ name: "firstName", type: "input", label: "First Name" },
{ name: "lastName", type: "input", label: "Last Name" }
]
},
{
id: "account",
title: "Account Info",
fields: [
{ name: "email", type: "input", label: "Email" },
{
name: "accountType",
type: "select",
label: "Account Type",
options: ["personal", "business"]
},
{
name: "companyName",
type: "input",
label: "Company Name",
condition: (formData) => formData.accountType === "business" // Условное отображение!
}
]
}
]
};
// App.vue
<template>
<form>
<div v-for="section in config.sections" :key="section.id" class="section">
<h3>{{ section.title }}</h3>
<DynamicField
v-for="field in section.fields"
v-show="evaluateCondition(field.condition)"
:key="field.name"
:field="field"
:value="formData[field.name]"
@update:modelValue="formData[field.name] = $event"
/>
</div>
</form>
</template>
<script setup>
const evaluateCondition = (condition) => {
if (!condition) return true;
return typeof condition === "function"
? condition(formData)
: condition;
};
</script>
5. Config с API и Real-Time обновлением
// services/configService.ts
export const useFormConfig = () => {
const config = ref(null);
const loading = ref(false);
const error = ref(null);
const loadConfig = async (formId) => {
loading.value = true;
try {
const response = await fetch(`/api/forms/${formId}/config`);
config.value = await response.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
};
// WebSocket для real-time обновлений
const subscribeToUpdates = (formId) => {
const ws = new WebSocket(`ws://api/forms/${formId}/updates`);
ws.onmessage = (event) => {
const newConfig = JSON.parse(event.data);
config.value = newConfig; // Vue реактивно обновит UI!
};
return ws;
};
return { config, loading, error, loadConfig, subscribeToUpdates };
};
// App.vue
<script setup>
const { config, loading } = useFormConfig();
const ws = ref(null);
onMounted(async () => {
await loadConfig("form-123");
ws.value = subscribeToUpdates("form-123");
});
onUnmounted(() => {
ws.value?.close();
});
</script>
<template>
<div v-if="loading">Loading...</div>
<DynamicForm v-else :config="config" />
</template>
6. Кеширование и оптимизация
// composables/useConfigCache.ts
import { ref, computed } from "vue";
const cache = new Map();
const TTL = 5 * 60 * 1000; // 5 минут
export const useConfigCache = () => {
const getConfig = async (formId) => {
const cached = cache.get(formId);
if (cached && Date.now() - cached.timestamp < TTL) {
return cached.data;
}
const data = await fetchConfig(formId);
cache.set(formId, { data, timestamp: Date.now() });
return data;
};
const invalidateCache = (formId) => {
cache.delete(formId);
};
return { getConfig, invalidateCache };
};
// Применение в компоненте
<script setup>
const { getConfig, invalidateCache } = useConfigCache();
onMounted(async () => {
config.value = await getConfig("form-123");
});
// Когда админ обновит конфиг
const handleConfigUpdate = async () => {
invalidateCache("form-123");
config.value = await getConfig("form-123");
};
</script>
7. Валидация и трансформация данных
// utils/validator.ts
export const createValidator = (rules) => {
return (value) => {
for (const rule of rules) {
const result = rule.validate(value);
if (!result.valid) {
return result;
}
}
return { valid: true };
};
};
// Config с валидацией
const fieldConfig = {
name: "email",
type: "input",
label: "Email",
rules: [
{
validate: (v) => ({
valid: v.length > 0,
message: "Email is required"
})
},
{
validate: (v) => ({
valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
message: "Invalid email format"
})
}
],
transform: (v) => v.toLowerCase().trim() // Трансформация на лету
};
// Компонент
<script setup>
const validator = createValidator(props.field.rules);
const handleInput = (value) => {
const transformed = props.field.transform?.(value) || value;
const validation = validator(transformed);
if (validation.valid) {
emit("update:modelValue", transformed);
} else {
error.value = validation.message;
}
};
</script>
8. Plugin для автоматической регистрации компонентов
// plugins/dynamicFieldsPlugin.ts
import { defineAsyncComponent } from "vue";
import DynamicField from "@/components/DynamicField.vue";
export default {
install(app) {
// Автоматическая регистрация всех компонентов полей
const modules = import.meta.glob("/components/fields/*.vue");
for (const [path, module] of Object.entries(modules)) {
const name = path.split("/").pop().replace(".vue", "");
app.component(name, defineAsyncComponent(module));
}
app.component("DynamicField", DynamicField);
}
};
// main.ts
import dynamicFieldsPlugin from "@/plugins/dynamicFieldsPlugin";
app.use(dynamicFieldsPlugin);
9. Практический пример: Dashboard с конфигом
// dashboardConfig.json
{
"layout": "grid",
"columns": 3,
"widgets": [
{
"id": "sales",
"type": "chart",
"title": "Sales",
"dataSource": "/api/sales",
"refreshInterval": 60000
},
{
"id": "users",
"type": "metric",
"title": "Active Users",
"dataSource": "/api/users/active",
"format": "number"
}
]
}
// App.vue
<template>
<div class="dashboard" :style="{ gridTemplateColumns: `repeat(${config.columns}, 1fr)` }">
<DynamicWidget
v-for="widget in config.widgets"
:key="widget.id"
:widget="widget"
/>
</div>
</template>
<script setup>
const config = ref();
onMounted(async () => {
const response = await fetch("/config/dashboard.json");
config.value = await response.json();
});
</script>
10. Чек-лист для собеседования
// 1. Как избежать пересборки?
// Ответ: Config-driven UI - меняем config, не код
// 2. Как обновлять config на лету?
// Ответ: WebSocket, гидрация реактивного объекта Vue
// 3. Как кешировать config?
// Ответ: Map с TTL + invalidation
// 4. Как работать со сложными иерархиями?
// Ответ: Рекурсивные компоненты и условное отображение
// 5. Производительность при 1000+ полей?
// Ответ: virtual scroll, ленивая загрузка, мемоизация
Итог
Динамический UI в Vue - это мощный паттерн:
- Config-first подход: JSON конфиги вместо hardcoded компонентов
- Ленивая загрузка: только нужные компоненты загружаются
- Real-time обновления: WebSocket для синхронизации с сервером
- Кеширование: оптимизация производительности
Используй <component :is="..." /> + рефреш конфига при необходимости.