Lo que resuelven los frameworks web: la alternativa básica (Parte 2)

 

 

 

  • Register!
  • Deploy Fast. Deploy Smart

  • Índice
    1. ¿Lanzar su propio marco?
    2. Opciones de vainilla
      1. Reactividad con árbol DOM estable y cascada
      2. “Enlace de datos” orientado a formularios
      3. Plantilla ChaCha y HTML
      4. Canal de Cambios (o ChaCha)
      5. El elemento de plantilla HTML para elementos de lista
    3. Poniéndolo todo junto: TodoMVC
      1. Comience con un ChaCha derivado de la especificación
      2. HTML sencillo y orientado a formularios
      3. Controlador mínimo JavaScript
      4. Reactividad con CSS
    4. Conclusión y conclusiones
      1. Resumen de patrones

    En esta segunda parte, Noam sugiere algunos patrones de cómo utilizar la plataforma web directamente como alternativa a algunas de las soluciones que ofrecen los frameworks.

     

    La semana pasada , analizamos los diferentes beneficios y costos de usar marcos, comenzando desde el punto de vista de qué problemas centrales están tratando de resolver, enfocándonos en programación declarativa, enlace de datos, reactividad, listas y condicionales. Hoy veremos si puede surgir una alternativa desde la propia plataforma web.

    ¿Lanzar su propio marco?

    Un resultado que podría parecer inevitable al explorar la vida sin uno de los marcos es desarrollar su propio marco para el enlace de datos reactivo. Habiendo probado esto antes y viendo lo costoso que puede ser, decidí trabajar con una guía en esta exploración; no para implementar mi propio marco, sino para ver si puedo usar la plataforma web directamente de una manera que haga que los marcos sean menos necesarios. Si considera implementar su propio marco, tenga en cuenta que hay una serie de costos que no se analizan en este artículo.

     

    Opciones de vainilla

    La plataforma web ya proporciona un mecanismo de programación declarativa listo para usar: HTML y CSS. Este mecanismo está maduro, bien probado, popular, ampliamente utilizado y documentado. Sin embargo, no proporciona conceptos integrados claros de enlace de datos, representación condicional y sincronización de listas, y la reactividad es un detalle sutil distribuido en múltiples funciones de la plataforma.

    Cuando hojeo la documentación de frameworks populares, encuentro inmediatamente las características descritas en la Parte 1 . Cuando leo la documentación de la plataforma web (por ejemplo, en MDN ), encuentro muchos patrones confusos sobre cómo hacer las cosas, sin una representación concluyente de enlace de datos, sincronización de listas o reactividad. Intentaré trazar algunas pautas sobre cómo abordar estos problemas en la plataforma web, sin necesidad de un marco (en otras palabras, usando Vanilla).

    Reactividad con árbol DOM estable y cascada

    Volvamos al ejemplo de la etiqueta de error. En ReactJS y SolidJS, creamos código declarativo que se traduce en código imperativo que agrega la etiqueta al DOM o la elimina. En Svelte, se genera ese código.

    Pero, ¿qué pasaría si no tuviéramos ese código en absoluto y en su lugar usáramos CSS para ocultar y mostrar la etiqueta de error?

    style label.error { display: none; } .app.has-error label.error {display: block; }/stylelabelMessage/labelscript app.classList.toggle('has-error', true);/script

    La reactividad, en este caso, se maneja en el navegador: el cambio de clase de la aplicación se propaga a sus descendientes hasta que el mecanismo interno del navegador decide si representa la etiqueta.

    Esta técnica tiene varias ventajas:

    • El tamaño del paquete es cero.
    • No hay pasos de construcción.
    • La propagación de cambios está optimizada y bien probada en código nativo del navegador y evita operaciones DOM costosas e innecesarias como appendy remove.
    • Los selectores son estables. En este caso, puede contar con que el elemento de etiqueta esté allí. Puede aplicarle animaciones sin depender de construcciones complicadas como "grupos de transición". Puede mantener una referencia a él en JavaScript.
    • Si la etiqueta se muestra u oculta, puedes ver el motivo en el panel de estilo de las herramientas de desarrollador, que te muestra toda la cascada, estando visible (u oculta) la cadena de reglas que terminaron en la etiqueta.

    Incluso si lees esto y eliges seguir trabajando con frameworks, la idea de mantener el DOM estable y cambiar de estado con CSS es poderosa. Considere dónde esto podría resultarle útil.

    “Enlace de datos” orientado a formularios

    Antes de la era de las aplicaciones de una sola página (SPA) con mucho JavaScript, los formularios eran la principal forma de crear aplicaciones web que incluyeran la entrada del usuario. Tradicionalmente, el usuario completaba el formulario y hacía clic en el botón "Enviar", y el código del lado del servidor manejaba la respuesta. Los formularios eran la versión de aplicación de varias páginas de enlace de datos e interactividad. No es de extrañar que los elementos HTML con los nombres básicos de inputy outputsean elementos de formulario.

     

    Debido a su amplio uso y su larga historia, las API de formularios acumularon varias ventajas ocultas que las hacen útiles para problemas que tradicionalmente no se consideran resueltos mediante formularios.

    Formularios y elementos de formulario como selectores estables

    Se puede acceder a los formularios por su nombre (usando document.forms) y a cada elemento del formulario se puede acceder por su nombre (usando form.elements). Además, se puede acceder al formulario asociado a un elemento (mediante el formatributo ). Esto incluye no solo elementos de entrada, sino también otros elementos de formulario como output, textareay fieldset, que permiten el acceso anidado a elementos en un árbol.

    En el ejemplo de etiqueta de error de la sección anterior, mostramos cómo mostrar y ocultar reactivamente el mensaje de error. Así es como actualizamos el texto del mensaje de error en React (y de manera similar en SolidJS):

    const [errorMessage, setErrorMessage] = useState(null);return label className="error"{errorMessage}/label

    Cuando tenemos un DOM estable y formas de árbol y elementos de formulario estables, podemos hacer lo siguiente:

    form name="contactForm" fieldset name="email" output name="error"/output /fieldset/formscript function setErrorMessage(message) { document.forms.contactForm.elements.email.elements.error.value = message; }/script

    Esto parece bastante detallado en su forma original, pero también es muy estable, directo y extremadamente eficaz.

    Formularios de entrada

    Por lo general, cuando construimos un SPA, tenemos algún tipo de API similar a JSON con la que trabajamos para actualizar nuestro servidor, o cualquier modelo que usemos.

    Este sería un ejemplo familiar (escrito en mecanografiado para facilitar la lectura):

    interface Contact { id: string; name: string; email: string; subscriber: boolean;}function updateContact(contact: Contact) { … }

    Es común en el código del marco generar este Contactobjeto seleccionando elementos de entrada y construyendo el objeto pieza por pieza. Con el uso adecuado de los formularios, existe una alternativa concisa:

    form name="contactForm" input name="id" type="hidden" value="136" / input name="email" type="email"/ input name="name" type="string" / input name="subscriber" type="checkbox" //formscript updateContact(Object.fromEntries( new FormData(document.forms.contactForm));/script

    Al utilizar entradas ocultas y la FormDataclase útil, podemos transformar sin problemas valores entre la entrada DOM y las funciones de JavaScript.

    Combinando formas y reactividad

    Al combinar la estabilidad del selector de alto rendimiento de los formularios y la reactividad de CSS, podemos lograr una lógica de interfaz de usuario más compleja:

    form name="contactForm" input name="showErrors" type="checkbox" hidden / fieldset name="names" input name="name" / output name="error"/output /fieldset fieldset name="emails" input name="email" / output name="error"/output /fieldset/formscript function setErrorMessage(section, message) { document.forms.contactForm.elements[section].elements.error.value = message; } function setShowErrors(show) { document.forms.contactForm.elements.showErrors.checked = show; }/scriptstyle input[name="showErrors"]:not(:checked) ~ * output[name="error"] { display: none; }/style

    Tenga en cuenta que en este ejemplo no se utilizan clases: desarrollamos el comportamiento del DOM y el estilo a partir de los datos de los formularios, en lugar de cambiar manualmente las clases de elementos.

     

    No me gusta abusar de las clases CSS como selectores de JavaScript. Creo que deberían usarse para agrupar elementos con estilos similares, no como un mecanismo general para cambiar los estilos de los componentes.

    Ventajas de los formularios

    • Al igual que con la cascada, los formularios están integrados en la plataforma web y la mayoría de sus funciones son estables. Eso significa mucho menos JavaScript, muchas menos discrepancias en las versiones del marco y ninguna "compilación".
    • Los formularios son accesibles de forma predeterminada. Si su aplicación utiliza formularios correctamente, habrá mucha menos necesidad de atributos ARIA, "complementos de accesibilidad" y auditorías de último momento. Los formularios se prestan para la navegación con teclado, lectores de pantalla y otras tecnologías de asistencia.
    • Los formularios vienen con funciones integradas de validación de entrada: validación por patrón de expresiones regulares, reactividad a formularios válidos y no válidos en CSS, manejo de obligatorio versus opcional, y más. No necesita que algo parezca un formulario para poder disfrutar de estas funciones.
    • El submitevento de formularios es extremadamente útil. Por ejemplo, permite capturar una tecla "Intro" incluso cuando no hay un botón de envío, y permite diferenciar varios botones de envío por el submitteratributo (como veremos en el ejemplo de TODO más adelante).
    • Los elementos están asociados con el formulario que los contiene de forma predeterminada, pero se pueden asociar con cualquier otro formulario en el documento mediante el formatributo. Esto nos permite jugar con la asociación de formularios sin crear una dependencia en el árbol DOM.
    • El uso de selectores estables ayuda con la automatización de las pruebas de UI: podemos usar la API anidada como una forma estable de conectarnos al DOM independientemente de su diseño y jerarquía. La form (fieldsets) elementjerarquía puede servir como esqueleto interactivo de su documento.

    Plantilla ChaCha y HTML

    Los marcos proporcionan su propia forma de expresar listas observables. Hoy en día, muchos desarrolladores también confían en bibliotecas que no son de framework y que proporcionan este tipo de funciones, como MobX.

    El principal problema con las listas observables de propósito general es que son de propósito general. Esto agrega conveniencia con el costo del rendimiento y también requiere herramientas de desarrollador especiales para depurar las acciones complicadas que esas bibliotecas realizan en segundo plano.

    Usar esas bibliotecas y comprender lo que hacen está bien, y pueden ser útiles independientemente del marco de interfaz de usuario elegido, pero usar la alternativa podría no ser más complicado y podría evitar algunos de los problemas que ocurren cuando intentas rodar tu propio modelo.

     

    Canal de Cambios (o ChaCha)

    El ChaCha, también conocido como Canal de Cambios , es una corriente bidireccional cuyo propósito es notificar cambios en la dirección de intención y la dirección de observación .

    • En la dirección de intención , la interfaz de usuario notifica al modelo los cambios previstos por el usuario.
    • En la dirección de observación , el modelo notifica a la interfaz de usuario los cambios que se realizaron en el modelo y que deben mostrarse al usuario.

    Quizás sea un nombre gracioso, pero no es un patrón complicado ni novedoso. Las transmisiones bidireccionales se utilizan en todas partes de la web y en el software (por ejemplo, MessagePort). En este caso, estamos creando una secuencia bidireccional que tiene un propósito particular: informar los cambios reales del modelo a la interfaz de usuario y las intenciones del modelo.

    La interfaz de ChaCha generalmente se puede derivar de la especificación de la aplicación, sin ningún código de interfaz de usuario.

    Por ejemplo, una aplicación que te permite agregar y eliminar contactos y que carga la lista inicial desde un servidor (con una opción para actualizar) podría tener un ChaCha con este aspecto:

    interface Contact { id: string; name: string; email: string;}// "Observe" Directioninterface ContactListModelObserver { onAdd(contact: Contact); onRemove(contact: Contact); onUpdate(contact: Contact);}// "Intent" Directioninterface ContactListModel { add(contact: Contact); remove(contact: Contact); reloadFromServer(); }

    Tenga en cuenta que todas las funciones en las dos interfaces son nulas y solo reciben objetos simples. Esto es intencional. ChaCha está construido como un canal con dos puertos para enviar mensajes, lo que le permite trabajar en un protocolo EventSource, HTML MessageChannel, service trabajador o cualquier otro. Todo sobre el cafe

    Lo bueno de ChaChas es que son fáciles de probar: envías acciones y esperas a cambio llamadas específicas al observador.

    El elemento de plantilla HTML para elementos de lista

    Las plantillas HTML son elementos especiales que están presentes en el DOM pero no se muestran. Su finalidad es generar elementos dinámicos.

    Cuando usamos un templateelemento, podemos evitar todo el código repetitivo de crear elementos y completarlos en JavaScript.

    Lo siguiente agregará un nombre a una lista usando template:

    ul template lilabel //li /template/ulscript function addName(name) { const list = document.querySelector('#names'); const item = list.querySelector('template').content.cloneNode(true).firstElementChild; item.querySelector('label').innerText = name; list.appendChild(item); }/script

    Al usar el templateelemento para elementos de la lista, podemos ver el elemento de la lista en nuestro HTML original; no se "representa" usando JSX ni ningún otro lenguaje. Su archivo HTML ahora contiene todo el HTML de la aplicación: las partes estáticas son parte del DOM renderizado y las partes dinámicas se expresan en plantillas, listas para ser clonadas y agregadas al documento cuando llegue el momento.

     

    Poniéndolo todo junto: TodoMVC

    TodoMVC es una especificación de aplicación de una lista TODO que se ha utilizado para mostrar los diferentes marcos. La plantilla TodoMVC viene con HTML y CSS listos para ayudarlo a concentrarse en el marco.

    Puedes jugar con el resultado en el repositorio de GitHub y el código fuente completo está disponible.

    Comience con un ChaCha derivado de la especificación

    Comenzaremos con la especificación y la usaremos para construir la interfaz ChaCha:

    interface Task { title: string; completed: boolean;}interface TaskModelObserver { onAdd(key: number, value: Task); onUpdate(key: number, value: Task); onRemove(key: number); onCountChange(count: {active: number, completed: number});}interface TaskModel { constructor(observer: TaskModelObserver); createTask(task: Task): void; updateTask(key: number, task: Task): void; deleteTask(key: number): void; clearCompleted(): void; markAll(completed: boolean): void;}

    Las funciones en el modelo de tareas se derivan directamente de la especificación y de lo que el usuario puede hacer (borrar tareas completadas, marcar todas como completadas o activas, obtener los recuentos activos y completados).

    Tenga en cuenta que sigue las pautas de ChaCha:

    • Hay dos interfaces, una que actúa y otra que observa.
    • Todos los tipos de parámetros son primitivos u objetos simples (que se traducen fácilmente a JSON).
    • Todas las funciones regresan nulas.

    La implementación de TodoMVC se utiliza localStoragecomo backend .

    El modelo es muy simple y no muy relevante para la discusión sobre el marco de la interfaz de usuario. Guarda localStoragecuando es necesario y activa devoluciones de llamada de cambio al observador cuando algo cambia, ya sea como resultado de la acción del usuario o cuando el modelo se carga localStoragepor primera vez.

    HTML sencillo y orientado a formularios

    A continuación, tomaré la plantilla TodoMVC y la modificaré para que esté orientada a formularios: una jerarquía de formularios, con elementos de entrada y salida que representan datos que se pueden cambiar con JavaScript.

    ¿Cómo sé si algo debe ser un elemento de formulario? Como regla general, si se vincula a datos del modelo, entonces debería ser un elemento de formulario.

    El archivo HTML completo está disponible, pero aquí está su parte principal:

    section header h1todos/h1 form name="newTask" input name="title" type="text" placeholder="What needs to be done?" autofocus /form /header main form/form input type="hidden" name="filter" form="main" / input type="hidden" name="completedCount" form="main" / input type="hidden" name="totalCount" form="main" / input name="toggleAll" type="checkbox" form="main" / ul template form li input name="completed" type="checkbox" checked input name="title" readonly / input type="submit" hidden name="save" / button name="destroy"X/button /li /form /template /ul /main footer output form="main" name="activeCount"0/output nav a name="/" href="#/"All/a a name="/active" href="#/active"Active/a a name="/completed" href="#/completed"Completed/a /nav input form="main" type="button" name="clearCompleted" value="Clear completed" / /footer/section

    Este HTML incluye lo siguiente:

     

    • Tenemos un mainformulario, con todas las entradas y botones globales, y un nuevo formulario para crear una nueva tarea. Tenga en cuenta que asociamos los elementos al formulario usando el formatributo , para evitar anidar los elementos en el formulario.
    • El templateelemento representa un elemento de la lista y su elemento raíz es otra forma que representa los datos interactivos relacionados con una tarea en particular. Este formulario se repetiría clonando el contenido de la plantilla cuando se agreguen tareas.
    • Las entradas ocultas representan datos que no se muestran directamente pero que se utilizan para diseñar y seleccionar.

    Tenga en cuenta que este DOM es conciso. No tiene clases repartidas entre sus elementos. Incluye todos los elementos necesarios para la aplicación, organizados en una jerarquía sensata. Gracias a los elementos de entrada ocultos, ya puedes hacerte una idea de lo que podría cambiar en el documento más adelante.

    Este HTML no sabe cómo se le aplicará el estilo ni exactamente a qué datos está vinculado. Deje que CSS y JavaScript funcionen para su HTML, en lugar de que su HTML funcione para un mecanismo de estilo particular. Esto haría mucho más fácil cambiar los diseños a medida que avanza.

    Controlador mínimo JavaScript

    Ahora que tenemos la mayor parte de la reactividad en CSS y tenemos manejo de listas en el modelo, lo que queda es el código del controlador: la cinta adhesiva que mantiene todo junto. En esta pequeña aplicación, el controlador JavaScript tiene alrededor de 40 líneas .

    Aquí tenéis una versión, con una explicación de cada parte:

    import TaskListModel from './model.js';const model = new TaskListModel(new class {

    Arriba, creamos un nuevo modelo.

    onAdd(key, value) { const newItem = document.querySelector('.todo-list template').content.cloneNode(true).firstElementChild; newItem.name = `task-${key}`; const save = () = model.updateTask(key, Object.fromEntries(new FormData(newItem))); newItem.elements.completed.addEventListener('change', save); newItem.addEventListener('submit', save); newItem.elements.title.addEventListener('dblclick', ({target}) = target.removeAttribute('readonly')); newItem.elements.title.addEventListener('blur', ({target}) = target.setAttribute('readonly', '')); newItem.elements.destroy.addEventListener('click', () = model.deleteTask(key)); this.onUpdate(key, value, newItem); document.querySelector('.todo-list').appendChild(newItem);}

    Cuando se agrega un elemento al modelo, creamos su elemento de lista correspondiente en la interfaz de usuario.

    Arriba, clonamos el contenido del elemento template, asignamos los detectores de eventos para un elemento en particular y agregamos el nuevo elemento a la lista.

    Tenga en cuenta que esta función, junto con onUpdate, onRemovey onCountChange, son devoluciones de llamada que se llamarán desde el modelo .

     

    onUpdate(key, {title, completed}, form = document.forms[`task-${key}`]) { form.elements.completed.checked = !!completed; form.elements.title.value = title; form.elements.title.blur();}

    Cuando se actualiza un elemento, configuramos sus valores completedy title, y luego blur(para salir del modo de edición).

    onRemove(key) { document.forms[`task-${key}`].remove(); }

    Cuando se elimina un elemento del modelo, eliminamos su elemento de lista correspondiente de la vista.

    onCountChange({active, completed}) { document.forms.main.elements.completedCount.value = completed; document.forms.main.elements.toggleAll.checked = active === 0; document.forms.main.elements.totalCount.value = active + completed; document.forms.main.elements.activeCount.innerHTML = `strong${active}/strong item${active === 1 ? '' : 's'} left`;}

    En el código anterior, cuando cambia la cantidad de elementos completados o activos, configuramos las entradas adecuadas para desencadenar las reacciones CSS y formateamos la salida que muestra el recuento.

    const updateFilter = () = filter.value = location.hash.substr(2);window.addEventListener('hashchange', updateFilter);window.addEventListener('load', updateFilter);

    Y actualizamos el filtro desde el hashfragmento (y al inicio). Todo lo que hacemos arriba es establecer el valor de un elemento de formulario; CSS se encarga del resto.

    document.querySelector('.todoapp').addEventListener('submit', e = e.preventDefault(), {capture: true});

    Aquí, nos aseguramos de no recargar la página cuando se envía un formulario. Esta es la línea que convierte esta app en un SPA.

    document.forms.newTask.addEventListener('submit', ({target: {elements: {title}}}) = model.createTask({title: title.value}));document.forms.main.elements.toggleAll.addEventListener('change', ({target: {checked}})= model.markAll(checked));document.forms.main.elements.clearCompleted.addEventListener('click', () = model.clearCompleted());

    Y esto maneja las acciones principales (crear, marcar todo, borrar completado).

    Reactividad con CSS

    El archivo CSS completo está disponible para que lo veas.

    CSS maneja muchos de los requisitos de la especificación (con algunas modificaciones para favorecer la accesibilidad). Veamos algunos ejemplos.

    Según la especificación, el destroybotón "X" ( ) se muestra solo al pasar el mouse. También agregué un bit de accesibilidad para que sea visible cuando la tarea está enfocada:

    .task:not(:hover, :focus-within) button[name="destroy"] { opacity: 0 }

    El filterenlace adquiere un borde rojizo cuando es el actual:

    .todoapp input[name="filter"][value=""] ~ footer a[href$="#/"],nav a:target { border-color: #CE4646;}

    Tenga en cuenta que podemos usar el hrefelemento de enlace como selector de atributos parcial; no es necesario JavaScript que verifique el filtro actual y establezca una selectedclase en el elemento adecuado.

    También usamos el :targetselector, lo que nos libera de tener que preocuparnos de si agregar filtros.

     

    El estilo de visualización y edición de la titleentrada cambia según su modo de solo lectura:

    .task input[name="title"]:read-only {…}.task input[name="title"]:not(:read-only) {…}

    El filtrado (es decir, mostrar sólo las tareas activas y completadas) se realiza con un selector:

    input[name="filter"][value="active"] ~ * .task :is(input[name="completed"]:checked, input[name="completed"]:checked ~ *),input[name="filter"][value="completed"] ~ * .task :is(input[name="completed"]:not(:checked), input[name="completed"]:not(:checked) ~ *) { display: none;}

    El código anterior puede parecer un poco detallado y probablemente sea más fácil de leer con un preprocesador CSS como Sass. Pero lo que hace es sencillo: si el filtro está marcado activey la completedcasilla de verificación está marcada, o viceversa, ocultamos la casilla de verificación y sus hermanos.

    Elegí implementar este filtro simple en CSS para mostrar hasta dónde puede llegar, pero si comienza a complicarse, entonces tendría mucho sentido moverlo al modelo.

    Conclusión y conclusiones

    Creo que los marcos proporcionan formas convenientes de lograr tareas complicadas y tienen beneficios más allá de los técnicos, como alinear a un grupo de desarrolladores con un estilo y patrón particular. La plataforma web ofrece muchas opciones, y la adopción de un marco hace que todos, al menos parcialmente, estén en la misma página para algunas de esas opciones. Hay valor en eso. Además, hay algo que decir sobre la elegancia de la programación declarativa, y la gran característica de la componenteización no es algo que haya abordado en este artículo.

    Pero recuerde que existen patrones alternativos, a menudo con menos costo y no siempre necesitan menos experiencia de desarrollador. Permítase sentir curiosidad por esos patrones, incluso si decide elegir entre ellos mientras utiliza un marco.

    Resumen de patrones

    • Mantenga estable el árbol DOM. Comienza una reacción en cadena para facilitar las cosas.
    • Confíe en CSS para la reactividad en lugar de JavaScript, cuando pueda.
    • Utilice elementos de formulario como forma principal de representar datos interactivos.
    • Utilice el elemento HTML templateen lugar de plantillas generadas por JavaScript.
    • Utilice un flujo bidireccional de cambios como interfaz para su modelo.

    Un agradecimiento especial a las siguientes personas por sus revisiones técnicas: Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal, Louis Lazaris

    (vf, il, al)Explora más en

    • Reaccionar
    • Marcos
    • javascript





    Tal vez te puede interesar:

    1. ¿Deberían abrirse los enlaces en ventanas nuevas?
    2. 24 excelentes tutoriales de AJAX
    3. 70 técnicas nuevas y útiles de AJAX y JavaScript
    4. Más de 45 excelentes recursos y repositorios de fragmentos de código

    Lo que resuelven los frameworks web: la alternativa básica (Parte 2)

    Lo que resuelven los frameworks web: la alternativa básica (Parte 2)

    Register! Deploy Fast. Deploy Smart Índice ¿Lanzar su propio marco?

    programar

    es

    https://pseint.es/static/images/programar-lo-que-resuelven-los-frameworks-web-la-alternativa-basica-parte-2-1131-0.jpg

    2024-04-04

     

    Lo que resuelven los frameworks web: la alternativa básica (Parte 2)
    Lo que resuelven los frameworks web: la alternativa básica (Parte 2)

    Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

     

     

    Top 20