Implementação de um Mecanismo de Renderização para Plataformas de Baixo Código com Vue

  1. Arquitetura do Núcleo

1.1 Visão Geral da Arquitetura

// Definição da interface principal do mecanismo de renderização
interface MotorDeRenderizacao {
  schema: SchemaPagina;           // Schema da página
  componentes: MapaDeComponentes;     // Mapa de componentes
  fonteDados: FonteDados;       // Fonte de dados
  eventos: SistemaEventos;          // Sistema de eventos
  renderizar(): VNode;              // Método de renderização
}

// Estrutura do Schema da Página
interface SchemaPagina {
  id: string;
  nome: string;
  componentes: SchemaComponente[];
  fonteDados: ConfigFonteDados;
  eventos: ConfigEventos;
  estilo: EstiloPagina;
}

// Schema do Componente
interface SchemaComponente {
  id: string;
  tipo: string;                // Tipo do componente
  nomeComponente: string;       // Nome do componente
  props: Record<string, any>;  // Propriedades do componente
  eventos?: EventoComponente[];   // Eventos do componente
  filhos?: SchemaComponente[]; // Componentes filhos
  estilo?: PropriedadesCSS;       // Estilo
  vinculacaoDados?: VinculacaoDados;   // Vinculação de dados
}
  1. Design do Schema

2.1 Definição do Schema da Página

{
  "pagina": {
    "id": "pag_001",
    "nome": "Página de Gestão de Usuários",
    "layout": "flex",
    "estilo": {
      "espacamento": "20px",
      "corFundo": "#f5f5f5"
    },
    "fonteDados": {
      "listaUsuarios": {
        "tipo": "api",
        "url": "/api/usuarios",
        "metodo": "GET",
        "autoCarregar": true
      }
    },
    "componentes": [
      {
        "id": "form_busca",
        "tipo": "container",
        "nomeComponente": "ElForm",
        "props": {
          "inline": true,
          "modelo": "{{formularioBusca}}"
        },
        "filhos": [
          {
            "id": "input_busca",
            "tipo": "input",
            "nomeComponente": "ElInput",
            "props": {
              "textoPlaceholder": "Digite o nome do usuário",
            },
            "eventos": [
              {
                "nome": "change",
                "acao": "BUSCAR_USUARIO"
              }
            ]
          }
        ]
      }
    ]
  }
}

2.2 Extensão do Schema do Componente

// Definição detalhada do Schema do Componente
interface SchemaComponente {
  id: string;
  tipo: TipoComponente;
  nomeComponente: string;
  props: {
    // Propriedades estáticas
    [chave: string]: any;
    // Vinculação dinâmica de propriedades
    valorModelo?: string; // {{dados.campo}}
    // Propriedades de estilo
    classe?: string;
    estilo?: PropriedadesCSS;
  };
  eventos?: Array<{
    nome: string;        // Nome do evento
    acao: string;      // Tipo da ação
    cargaUtil?: any;       // Parâmetros da ação
    manipulador?: string;    // Função de manipulação personalizada
  }>;
  filhos?: SchemaComponente[];
  // Configuração de vinculação de dados
  vinculacaoDados?: {
    tipo: 'api' | 'local' | 'computado';
    origem: string;
    campo?: string;
    transformar?: string; // Função de transformação de dados
  };
  // Renderização condicional
  condicoes?: Condicao[];
  // Renderização em loop
  loop?: {
    fonteDados: string;
    nomeItem: string;
    nomeIndice?: string;
  };
  // Configuração de slots
  slots?: {
    [nomeSlot: string]: SchemaComponente[];
  };
}

// Configuração de renderização condicional
interface Condicao {
  expressao: string;    // Expressão JS
  operador: '==' | '!=' | '>' | '<' | 'includes' | string;
  valor: any;
}
  1. Implementação do Mecanismo de Renderização

3.1 Renderizador Principal

<template>
  <div class="renderizador-baixo-codigo">
    <template v-for="componente in componentesNormalizados" :key="componente.id">
      <renderizador-componente
        :schema="componente"
        :dados="dadosPagina"
        @acao="tratarAcao"
      />
    </template>
  </div>
</template>

<script setup lang="ts">
import { ref, calculado, aoMontar } from 'vue';
import RenderizadorComponente from './RenderizadorComponente.vue';

// Props
interface Props {
  schema: SchemaPagina;
  componentes: MapaDeComponentes;
}
const props = definirProps<Props>();

// Dados da página
const dadosPagina = ref<Record<string, any>>({});

// Componentes normalizados
const componentesNormalizados = calculado(() => {
  return processarComponentes(props.schema.componentes, dadosPagina.value);
});

// Processar componentes (tratar loop, condições, etc.)
function processarComponentes(componentes: SchemaComponente[], dados: any) {
  return componentes.flatMap(componente => {
    // Verificação de renderização condicional
    if (componente.condicoes && !verificarCondicoes(componente.condicoes, dados)) {
      return [];
    }

    // Processamento de renderização em loop
    if (componente.loop) {
      const lista = obterValorPorCaminho(dados, componente.loop.fonteDados) || [];
      return lista.map((item: any, indice: number) => ({
        ...componente,
        id: `${componente.id}_${indice}`,
        props: processarProps(componente.props, { 
          ...dados, 
          [componente.loop.nomeItem]: item,
          [componente.loop.nomeIndice || 'indice']: indice 
        })
      }));
    }

    // Componente comum
    return [{
      ...componente,
      props: processarProps(componente.props, dados)
    }];
  });
}

// Processar propriedades com vinculação dinâmica
function processarProps(props: Record<string, any>, dados: any) {
  const processado: Record<string, any> = {};
  
  Object.keys(props).forEach(chave => {
    const valor = props[chave];
    
    if (typeof valor === 'string' && valor.startsWith('{{') && valor.endsWith('}}')) {
      // Processar vinculação dinâmica {{dados.campo}}
      const expressao = valor.slice(2, -2).trim();
      processado[chave] = obterValorPorCaminho(dados, expressao);
    } else {
      processado[chave] = valor;
    }
  });
  
  return processado;
}

// Tratamento de ações
function tratarAcao(acao: string, cargaUtil: any) {
  switch (acao) {
    case 'BUSCAR_USUARIO':
      tratarBusca(cargaUtil);
      break;
    case 'ENVIAR_FORMULARIO':
      tratarEnvio(cargaUtil);
      break;
    // Outras ações...
  }
}

// Inicializar dados
aoMontar(async () => {
  await carregarFontesDados();
});

// Carregar fontes de dados
async function carregarFontesDados() {
  if (props.schema.fonteDados) {
    for (const [chave, config] of Object.entries(props.schema.fonteDados)) {
      if (config.autoCarregar) {
        dadosPagina.value[chave] = await buscarFonteDados(config);
      }
    }
  }
}
</script>

3.2 Renderizador de Componente

<template>
  <componente
    :is="resolverComponente(schema.nomeComponente)"
    v-bind="propsFundidos"
    v-on="manipuladoresEventos"
  >
    <!-- Renderizar componentes filhos -->
    <template v-if="schema.filhos && schema.filhos.length">
      <renderizador-componente
        v-for="filho in filhosNormalizados"
        :key="filho.id"
        :schema="filho"
        :dados="dados"
        @acao="$emit('acao', $event)"
      />
    </template>
    
    <!-- Renderizar slots -->
    <template v-for="(componentesSlot, nomeSlot) in schema.slots" :key="nomeSlot">
      <template #[nomeSlot]>
        <renderizador-componente
          v-for="componente in componentesSlot"
          :key="componente.id"
          :schema="componente"
          :dados="dados"
          @acao="$emit('acao', $event)"
        />
      </template>
    </template>
  </componente>
</template>

<script setup lang="ts">
import { calculado } from 'vue';

interface Props {
  schema: SchemaComponente;
  dados: Record<string, any>;
}

const props = definirProps<Props>();
const emitir = definirEmit<{
  acao: [acao: string, cargaUtil: any];
}>();

// Mapeamento de componentes
const mapaComponentes = {
  // Componentes de layout
  'ElRow': () => import('element-plus').then(mod => mod.ElRow),
  'ElCol': () => import('element-plus').then(mod => mod.ElCol),
  'ElForm': () => import('element-plus').then(mod => mod.ElForm),
  // Componentes de formulário
  'ElInput': () => import('element-plus').then(mod => mod.ElInput),
  'ElButton': () => import('element-plus').then(mod => mod.ElButton),
  'ElTable': () => import('element-plus').then(mod => mod.ElTable),
  // Componentes de negócio personalizados
  'AvatarUsuario': () => import('@/componentes/AvatarUsuario.vue'),
  // ...
};

// Resolver componente
async function resolverComponente(nome: string) {
  const resolvedor = mapaComponentes[nome];
  if (!resolvedor) {
    console.warn(`Componente ${nome} não encontrado`);
    return null;
  }
  return await resolvedor();
}

// Propriedades processadas
const propsFundidos = calculado(() => {
  const propsBase = { ...props.schema.props };
  
  // Processar estilo
  if (props.schema.estilo) {
    propsBase.estilo = props.schema.estilo;
  }
  
  return propsBase;
});

// Manipuladores de eventos
const manipuladoresEventos = calculado(() => {
  const manipuladores: Record<string, Funcao> = {};
  
  props.schema.eventos?.forEach(evento => {
    manipuladores[evento.nome] = (...args: any[]) => {
      emitir('acao', evento.acao, {
        ...evento.cargaUtil,
        $evento: args[0],
        $dados: props.dados
      });
    };
  });
  
  return manipuladores;
});

// Processar componentes filhos
const filhosNormalizados = calculado(() => {
  return processarComponentes(props.schema.filhos || [], props.dados);
});
</script>
  1. Gerenciamento de Fontes de Dados

4.1 Gerenciador de Fontes de Dados

class GerenciadorFontesDados {
  private dados: Map<string, any> = new Map();
  private assinantes: Map<string, Funcao[]> = new Map();

  // Registrar fonte de dados
  async registrarFonteDados(config: ConfigFonteDados) {
    const { chave, tipo, opcoes } = config;
    
    switch (tipo) {
      case 'api':
        return await this.configurarFonteApi(chave, opcoes);
      case 'local':
        return this.configurarFonteLocal(chave, opcoes);
      case 'computado':
        return this.configurarFonteComputada(chave, opcoes);
    }
  }

  // Fonte de dados API
  private async configurarFonteApi(chave: string, opcoes: OpcoesFonteApi) {
    const { url, metodo = 'GET', parametros, autoCarregar = true } = opcoes;
    
    if (autoCarregar) {
      try {
        const resposta = await this.buscarDados(url, metodo, parametros);
        this.definirDados(chave, resposta);
      } catch (erro) {
        console.error(`Falha ao carregar fonte de dados ${chave}:`, erro);
      }
    }
    
    // Retornar função de obtenção de dados
    return {
      recarregar: () => this.buscarDados(url, metodo, parametros).then(dados => {
        this.definirDados(chave, dados);
        return dados;
      })
    };
  }

  // Definir dados
  definirDados(chave: string, valor: any) {
    this.dados.set(chave, valor);
    this.notificarAssinantes(chave, valor);
  }

  // Obter dados
  obterDados(chave: string) {
    return this.dados.get(chave);
  }

  // Assinar alterações de dados
  assinar(chave: string, callback: Funcao) {
    if (!this.assinantes.has(chave)) {
      this.assinantes.set(chave, []);
    }
    this.assinantes.get(chave)!.push(callback);
    
    // Retornar função para cancelar assinatura
    return () => {
      const callbacks = this.assinantes.get(chave) || [];
      const indice = callbacks.indexOf(callback);
      if (indice > -1) {
        callbacks.splice(indice, 1);
      }
    };
  }

  private notificarAssinantes(chave: string, valor: any) {
    const callbacks = this.assinantes.get(chave) || [];
    callbacks.forEach(callback => callback(valor));
  }
}
  1. Sistema de Eventos

5.1 Processador de Ações

class ProcessadorAcoes {
  private gerenciadorDados: GerenciadorFontesDados;
  private roteador: Roteador;

  constructor(deps: { gerenciadorDados: GerenciadorFontesDados; roteador: Roteador }) {
    this.gerenciadorDados = deps.gerenciadorDados;
    this.roteador = deps.roteador;
  }

  async processar(acao: string, cargaUtil: any) {
    const [tipoAcao, ...resto] = acao.split('.');
    
    switch (tipoAcao) {
      case 'NAVEGAR':
        return this.processarNavegacao(cargaUtil);
      case 'API':
        return this.processarChamadaApi(cargaUtil);
      case 'DADOS':
        return this.processarOperacaoDados(cargaUtil);
      case 'MODAL':
        return this.processarModal(cargaUtil);
      default:
        console.warn(`Ação desconhecida: ${acao}`);
    }
  }

  // Navegação de rotas
  private processarNavegacao(cargaUtil: any) {
    const { caminho, consulta, parametros } = cargaUtil;
    this.roteador.push({ path: caminho, query: consulta, params: parametros });
  }

  // Chamada à API
  private async processarChamadaApi(cargaUtil: any) {
    const { chave, url, metodo, dados, aoSucesso, aoErro } = cargaUtil;
    
    try {
      const resposta = await fetch(url, {
        method: metodo,
        headers: { 'Content-Type': 'application/json' },
        body: dados ? JSON.stringify(dados) : undefined
      });
      
      const resultado = await resposta.json();
      
      if (aoSucesso) {
        await this.processar(aoSucesso, { $resposta: resultado });
      }
      
      return resultado;
    } catch (erro) {
      if (aoErro) {
        await this.processar(aoErro, { $erro: erro });
      }
      throw erro;
    }
  }

  // Operações com dados
  private processarOperacaoDados(cargaUtil: any) {
    const { operacao, chave, valor } = cargaUtil;
    
    switch (operacao) {
      case 'DEFINIR':
        this.gerenciadorDados.definirDados(chave, valor);
        break;
      case 'ATUALIZAR':
        const atual = this.gerenciadorDados.obterDados(chave);
        this.gerenciadorDados.definirDados(chave, { ...atual, ...valor });
        break;
      case 'EXCLUIR':
        this.gerenciadorDados.definirDados(chave, undefined);
        break;
    }
  }
}
  1. Registro Dinâmico de Componentes

6.1 Gerenciador de Biblioteca de Componentes

class BibliotecaComponentes {
  private componentes: Map<string, DefinicaoComponente> = new Map();
  private metadados: Map<string, MetadadosComponente> = new Map();

  // Registrar componente
  registrarComponente(nome: string, definicao: DefinicaoComponente) {
    this.componentes.set(nome, definicao);
  }

  // Registrar metadados do componente
  registrarMetadados(nome: string, metadados: MetadadosComponente) {
    this.metadados.set(nome, metadados);
  }

  // Obter componente
  obterComponente(nome: string): DefinicaoComponente | undefined {
    return this.componentes.get(nome);
  }

  // Obter metadados do componente
  obterMetadados(nome: string): MetadadosComponente | undefined {
    return this.metadados.get(nome);
  }

  // Obter todos os componentes
  obterTodosComponentes(): MetadadosComponente[] {
    return Array.from(this.metadados.values());
  }
}

// Definição de metadados do componente
interface MetadadosComponente {
  nome: string;
  titulo: string;
  categoria: string; // Categoria: formulário, layout, negócio, etc.
  props: MetadadosProp[];
  eventos?: MetadadosEvento[];
  slots?: MetadadosSlot[];
}

interface MetadadosProp {
  nome: string;
  tipo: 'string' | 'number' | 'boolean' | 'array' | 'object';
  padrao?: any;
  obrigatorio?: boolean;
  rotulo: string;
  componente?: string; // Componente para edição da propriedade
  opcoes?: any[]; // Valores possíveis
}

// Inicializar biblioteca de componentes
export const bibliotecaComponentes = new BibliotecaComponentes();

// Registrar componentes Element Plus
bibliotecaComponentes.registrarMetadados('ElInput', {
  nome: 'ElInput',
  titulo: 'Campo de Entrada',
  categoria: 'formulário',
  props: [
    {
      nome: 'valorModelo',
      tipo: 'string',
      rotulo: 'Valor',
      obrigatorio: true
    },
    {
      nome: 'textoPlaceholder',
      tipo: 'string',
      rotulo: 'Texto Placeholder'
    },
    {
      nome: 'desabilitado',
      tipo: 'boolean',
      rotulo: 'Desabilitado',
      padrao: false
    }
  ],
  eventos: [
    {
      nome: 'change',
      rotulo: 'Valor Alterado'
    },
    {
      nome: 'input',
      rotulo: 'Entrada'
    }
  ]
});
  1. Otimização de Renderização

7.1 Cache e Otimização de Componentes

// Gerenciador de cache de componentes
class CacheComponentes {
  private cache = new Map<string, any>();
  private componentesEstaticos = new Set<string>();

  // Pré-compilar componentes estáticos
  preCompilar(schema: SchemaPagina) {
    schema.componentes.forEach(componente => {
      if (this.isComponenteEstatico(componente)) {
        this.compilarComponente(componente);
      }
    });
  }

  // Verificar se é componente estático
  private isComponenteEstatico(componente: SchemaComponente): boolean {
    return !componente.vinculacaoDados && 
           !componente.condicoes && 
           !componente.loop &&
           !temPropsDinamicas(componente.props);
  }

  // Compilar componente
  private compilarComponente(componente: SchemaComponente) {
    const chave = this.gerarChave(componente);
    // Aqui pode-se pré-compilar para função de renderização
    this.cache.set(chave, this.criarFuncaoRenderizacao(componente));
  }

  // Obter componente do cache
  obter(chave: string) {
    return this.cache.get(chave);
  }
}

// Otimização de lista virtual (para listas longas)
const RenderizadorListaVirtual = definirComponente({
  props: {
    componentes: Array as PropType<SchemaComponente[]>,
    dados: Object
  },
  configuracao(props) {
    const refContainer = ref<HTMLElement>();
    const alcanceVisivel = ref({ inicio: 0, fim: 50 });
    
    // Monitorar rolagem, calcular área visível
    const tratarRolagem = usarThrottleFn(() => {
      if (!refContainer.value) return;
      
      const posicaoRolagem = refContainer.value.scrollTop;
      const alturaCliente = refContainer.value.clientHeight;
      
      const inicio = Math.floor(posicaoRolagem / 50);
      const fim = inicio + Math.ceil(alturaCliente / 50) + 5; // Buffer de 5 itens
      
      alcanceVisivel.value = {
        inicio: Math.max(0, inicio),
        fim: Math.min(props.componentes.length, fim)
      };
    }, 16);
    
    const componentesVisiveis = calculado(() => {
      const { inicio, fim } = alcanceVisivel.value;
      return props.componentes.slice(inicio, fim);
    });
    
    return {
      refContainer,
      componentesVisiveis,
      tratarRolagem
    };
  },
  template: `
    <div ref="refContainer" class="container-virtual" @scroll="tratarRolagem">
      <div class="conteudo-virtual" :estilo="{ height: alturaTotal + 'px' }">
        <div
          v-for="componente in componentesVisiveis"
          :key="componente.id"
          class="item-virtual"
          :estilo="{ transform: \`translateY(${componente.indice * 50}px)\` }"
        >
          <renderizador-componente
            :schema="componente"
            :dados="dados"
          />
        </div>
      </div>
    </div>
  `
});
  1. Integração com o Designer

8.1 Comunicação entre Designer e Renderizador

// Contexto do designer
interface ContextoDesigner {
  componenteSelecionado: string | null;
  componenteSobCursor: string | null;
  isModoDesign: boolean;
  atualizarComponente: (id: string, atualizacoes: Partial<SchemaComponente>) => void;
  excluirComponente: (id: string) => void;
  adicionarComponente: (idPai: string, componente: SchemaComponente) => void;
}

// Fornecer contexto do designer
export const usarDesigner = () => {
  return injetar<ContextoDesigner>('designer');
};

// Wrapper de componente editável
const WrapperEditavel = definirComponente({
  props: {
    schema: Object as PropType<SchemaComponente>,
    dados: Object
  },
  configuracao(props, { slots }) {
    const designer = usarDesigner();
    const estaSelecionado = calculado(() => 
      designer?.componenteSelecionado === props.schema.id
    );
    
    const tratarClique = (e: MouseEvent) => {
      e.stopPropagation();
      designer?.selecionarComponente(props.schema.id);
    };
    
    return () => h('div', {
      classe: [
        'componente-editavel',
        { 'selecionado': estaSelecionado.value }
      ],
      onClick: tratarClique
    }, slots.padrao?.());
  }
});

Esta arquitetura fornece uma estrutura de renderização de baixo código altamente extensível, suportando cenários de negócios complexos, mantendo ao mesmo tempo um bom desempenho e experiência de desenvolvimento.

Tags: vue low-code render-engine TypeScript frontend-architecture

Publicado em 7-14 23:36