TDD WordPress Theme

Tanto al comenzar a desarrollar un tema desde cero como cuando ya adquirió mayor complejidad tenemos que hacer una serie de chequeos de forma manual para asegurarnos que no nos olvidamos de incluir ninguna funcionalidad o no quedó código muerto.

Obviamente una buena checklist y una organización bien planificada de nuestro código simplifica mucho el tema pero siempre es menester automatizar este tipo de tareas.

WordPress provee una Test Suit pero está orientada principalmente al testeo del funcionamiento de WordPress más que a las cuestiones atinentes al desarrollo de themes.

Si bien puede ser utilizada como base para testear nuestros temas, a mi entender, agrega demasiado overhead para lo que nos aporta. Partiendo de este presupuesto decidí comenzar a desarrollar algunos tests propios con PHPUnit.

Primeramente, basado en el checklist que antes mencioné, definí una serie de chequeos que me interesa automatizar:

  • Existe los archivos functions.php, style.css y el screenshot
  • No hay archivos sin usar
  • Existe el template correspondiente a los template tags usados. Por ej.: get_header y header.php
  • Todas las uris locales son relativas
  • Todos los scripts/styles registrados son usados y existen
  • Hay sidebars y todas son usadas en algún lado
  • Todos los espacios de menu son usados en algún momento
  • Las páginas/artículos y el sitio tienen metadatos
  • El favicon está definido y la ruta existe
  • El theme no tiene que tomar funciones de plugin:
    • Dashboard widgets
    • Custom Post Types
    • Custom Taxonomies
    • Shortcodes
    • Metaboxes
    • Social integrations
    • Bloques de Gutenberg
  • La paginación respeta la configuración de Ajustes > Lectura
  • Customizer sanitization
  • Scripts/styles externos cargados de forma agnóstica
  • No se usa @import en los stylesheet
  • Soporte para title tag
  • Uso de $content_width
  • CSS básicos definidos:
    • alignleft, aligncenter, alignright,
    • wp-caption,
    • size-full, size-large, size-medium, size-thumbnail
  • Uso de WP_Filesystem en vez de las PHP File Functions: mkdir, fopen, fread, fwrite, fputs
  • Sin estilos o scripts hardcodeados
  • Scripts cargados únicamente en footer
  • i18n

A primera vista, algunos son sencillos como la verificación de la existencia de archivos, otros van a requerir análisis del código y algunos que podrían ser verificados utilizando la API que trae WordPress contra un servidor de desarrollo o staging.

Por ahora voy a ir haciendo el desarrollo y pruebas contra la copia de desarrollo del theme de un sitio que está en producción para tener un objetivo más real aunque la idea sería, a largo plazo, constituir un repositorio agnóstico que pueda ser rápidamente clonado en nuestro proyecto.

La primera aproximación quedaría con la siguiente estructura dentro de mi workflow habitual:

  • tests/bootstrap.php
  • tests/FaviconTest.php
  • tests/MissingFilesTest.php
  • tests/StylesheetTest.php
  • composer.json
  • phpunit.xml

Y el código propiamente de cada archivo sería:

Próximamente estaré publicando más novedades.

Recursos útiles para el desarrollo web

Serie de herramientas y bibliotecas online que son útiles durante el proceso de desarrollo web.

Google Analytics

Podemos usar otro pero definitivamente este es el gestor de estadísticas gratuito más completo. Información en tiempo real e histórica sobre los horarios de acceso, distribución geográfica, origen, étc. Y, más interesante al momento de pensar la, datos sobre el tipo de dispositivos usados para conectarse (mobile, tablet o desktop), el navegador o la resolución de pantalla. Por último, si tenés instalado el plugin (ahora sin soporte pero todavía funcional) podés incluso analizar el click heatmap de tu sitio.

Can I Use

Una vez que ya sabemos todo sobre los dispositivos que los usuarios van a usar para conectarse al sitio, es momento de empezar a delimitar las tecnologías que vamos a tener disponibles. En 10 años Can I Use se ha vuelto el standard de facto para obtener este tipo de información. Alcanza con ingresar en el buscador el nombre de la tecnología, un comando o etiqueta relacionado y tendremos información sobre el soporte por versión en los distintos navegadores, el uso relativo de los mismos y notas al pie sobre cosas a tener en cuenta.

MDN Web Docs

Definidas las tecnologías empieza el momento de picar código y comienzan, también las dudas que si un checkbox lanza un evento click o un change, que cual era el comando para agregar un elemento al principio de un Array, étc. Mientras más tecnologías están involucradas en el desarrollo web más difícil es acordarse al dedillo de todo y ahí entra a jugar MDN Web Docs una de las referencias más completas en el tema.

CSS-Tricks

Si bien MDN Web Docs tiene un apartado para CSS, la autoridad en el tema es de CSS-Tricks. Desde snippets a, como su nombre lo indica, trucos y consejos relacionados con el uso de los estilos y una galería de ejemplos de los más interesantes.

CSS Triggers

Una vez que ya empieza a tomar forma el sitio, empezamos a agregar más interactividad al mismo mediante la captura de eventos y manipulación del DOM. Estas modificaciones del modelos de objetos repercuten directamente en el CSSOM y pueden implicar un reflow, un repaint o un relayout cada uno con sus respectivas demoras. Cuando necesitamos saber si es mejor usar transform o modificar directamente el width de un objeto ahí entra CSS Triggers para no equivocarse.

Regular Expressions 101

Hablando de tópicos donde hace falta un ayuda memoria, llegamos a las expresiones regulares. Karma de muchos programadores, Regular Expressions 101 trae tanto una referencia como un entorno de pruebas para poder generar ese pattern perfecto.

Debuggex

Ya ni nos acordamos que matchea ese pattern que tenemos delante o estamos leyendo código de otra persona y vamos medio perdidos nada mejor que Debuggex, un tester visual que nos permite ver de qué va la cosa.

CSS Stats

Recién habíamos terminado de armar todo cuando el cliente nos pidió “unos pocos cambios, algo acá y allá” o llevamos tiempo en producción y hasta el hijo de la vecina, “que sabe un poco de computadoras”, metió mano. Por lo que sea terminamos con 402 colores únicos o 12k de reglas y no lo sabemos. Llevar un buen control sobre la situación de nuestros archivos de estilo es una tarea que se ve tremendamente simplicada con CSS Stats.

Pingdom

Siguiendo en la línea de controlar la performance del sitio, entramos en el área de las herramientas más generales como Pingdom que nos provee rápidamente con un resumen de la situación general. Vistoso por demás, también resulta útil para “robar” algunos gráficos para presentar al cliente.

WebPagetest

Lo que Pingdom tiene de bonito, WebPagetest lo tiene de detallado. Nunca lo vamos a usar como imagen en un Power Point pero si necesitamos estadísticas precisas para ver dónde está el cuello de botella en la carga o qué script está consumiendo demasiado tiempo este sitio nos va a permitir encontrar la información que buscamos.

WordPress y PHPUnit

PHPUnit es uno de los frameworks para test unitarios automáticos para PHP más usados y el elegido por los desarrolladores de WordPress junto con QUnit para probar la plataforma. Para facilitar las pruebas, los desarrolladores han creado ya todo un conjunto de test y clases auxiliares que nos resultarán muy útiles en nuestro workflow.

Instalación

Primero, necesitamos instalar PHPUnit en su versión 6 ya que la 7 no es soportada por WordPress. Hay distintas formas documentadas en el sitio del framework y podemos elegir la que más se adapte a nuestro entorno.

Segundo, creamos una nueva base de datos separada para los tests porque que la suite va a borrar todos los datos de las tablas de la instalación donde se ejecute.

Tercero, no es necesario pero definitivamente es la mejor forma de manejar el entorno de pruebas vamos a instalar wp-cli. Nuevamente, hay varías formas de llevar adelante la tarea y va a depender de nuestro entorno el método que elijamos.

Por mi parte, me ha dado mejores resultados la instalación recomendada: descargar el .phar, darle permisos de ejecución y ponerlo en una carpeta que esté en PATH.

Cuarto, nos situamos en la carpeta de la instalación de wordpress para utilizar las opciones del paquete scaffold de wp-cli para generar los archivos necesarios para instalar los tests según sea un plugin o un theme.

wp scaffold plugin-tests <nombre plugin>
wp scaffold theme-tests <nombre theme>

Quinto, ejecutamos el archivo generado dentro de la carpeta del plugin/theme en el paso anterior para instalar el entorno de pruebas. Vamos a necesitar las credenciales y datos de acceso a la base de datos que creamos antes.

bin/install-wp-tests.sh <nombre bbdd> <usuario> <contraseña> <host>

Con esto queda todo listo para que empecemos a desarrollar nuestros tests y ponerlos en la carpeta homónima para que PHPUnit los encuentre.

Para más información sobre los unit test en WordPress estos artículos van a dar una idea de las posibilidades:

Workflow en WordPress

La estructura de directorios que uso en mi workflow se aprovecha de una feature de WordPress que permite distribuir grupos de themes. La idea es que en vez de almacenar un theme por directorio se puedan poner varios usando subdirectorios.

El funcionamiento de esta feature es bastante sencillo, cuando WP lista las carpetas en wp-content/themes para ver que themes hay instalados, revisa si en la raíz  de las mismas está el archivo style.css; si no lo encuentra busca en los subdirectorios de primer nivel de la misma por si es un grupo de themes.

La idea entonces es que en nuestro entorno de desarrollo ese subdirectorio, que WP va a identificar como el que contiene el theme, sea el de las builds y que cualquier otro subdirectorio de primer nivel no tenga un archivo style.css.

  • build
  • dist/theme
  • src
    • images
    • languages
    • scripts
      • admin
      • customize
      • modules
    • styles
    • templates
      • admin
      • customize
      • modules
    • vendor
  • tasks

Build y Dist

Como se deduce de sus nombre, build, contiene las builds con todo el código sin comprimir y preparado para debug mientras que en dist se guarda el theme listo para producción.

Para evitar confundir a WP, es importante que en la raiz del directorio dist no se encuentre el style.css sino en un subdirectorio de este. Esto, además, nos simplifica el proceso de crear el zip / tar.gz para distribuir porque ya nos queda todo en un directorio listo para comprimir.

Src

Los “fuentes” del theme en el que estamos trabajando: php, imágenes y scss sin procesar, archivos de traducción, étc. Casi todo lo que está en este directorio va a ser procesado, optimizado, comprimido y demás operaciones que van a dar como resultado nuestro theme.

La idea original del directorio images era, justamente, guardar imágenes que se usaran en el diseño del theme pero, hoy en día, con los avances de CSS únicamente almacena el screenshot.

La carpeta templates contiene todo el código php, el ordenamiento interno tiene que cumplir con las limitaciones y recomendaciones de cualquier theme.

Me gusta mucho trabajar con el customizer así que suelo generar bastante código para este y, por lo tanto, tanto en la carpeta de scripts como de templates lo guardo en un directorio separado.

Además, a lo largo del tiempo, he creado algunos snipets de código que utilizo tan habitualmente como preload y lazyload, google fonts, étc que los mantego de forma separada y que cuando los incluyo en un proyecto los guardo en la carpeta modules.

Por una cuestión de separación también guardo en otro directorio, admin, todo el código que está pensado únicamente para ejecutarse en el dashboard de WP.

Por último, como no siempre hay paquetes con la librería que necesito, la versión que busco o no quiero usar un CDN, o por lo que sea, tengo el directorio vendor que se copia íntegramente tanto a las build como a la dist.

Tasks

Las tasks de Gulp.js: build, clean, clean-vendor, images, scripts, styles, templates, translation, vendor, watch.

Mantengo separadas las partes de código propio del código de vendor: build y clean para uno y vendor y clean-vendor para el otro. Ídem con las traducciones: translation.

Normalmente, build se encarga tanto de generar tanto la versión de pruebas como la versión para distribución. Antes las generaba con tareas específicas pero siempre se terminaban dando dos escenarios: dos tareas muy parecidas que había que mantener sincronizadas manualmente o una tarea con un montón de código duplicado y un enredo de condicionales.

La más importante de las tareas es watch que monitorea los cambios y actualiza la build. Para esto utilizo el modulo que trae Gulp por defecto en conjunto con BrowserSync para la inyección del nuevo CSS pero sin actualización automática. Pero, cuando trabajo sobre el customizer, desactivo completamente BS porque no se llevan bien y se suele quedar con pantallas en blanco u errores.

Gists

WP_Deduplicator: Clase estática que permite llevar el control de los post mostrados para evitar duplicar el contenido.

WP_Keepalive: Función que agrega o modifica la cabecera Connection para informar al cliente que es posible reutilizar la conexión abierta ahorrándonos establecer una conexión nueva para cada archivo.

WP_Clipboard: Función que usa una lista blanca para filtrar las etiquetas HTML del texto que se pega en el editor.

Errores comunes cuando empezamos a utilizar React

Olvidar la clave de los elementos al usar map

Si bien no es un error, por eso únicamente genera una advertencia, el uso de keys (claves) es una práctica altamente recomendada. Las keys en los elementos de una lista permiten individualizarlos y tratarlos de forma independiente del resto de la lista y de su orden dentro de la misma.

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

Este código devuelve la siguiente lista:

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul>

Si agregamos el elemento 0 (cero) al arreglo numbers, obtendríamos

<ul>
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul>

Es el resultado que queríamos obtener, ¿qué diferencia hacen entonces las keys?

Las keys lo que hacen es cambiar drásticamente la eficiencia en la React hace los cambios al DOM. Sin las keys, se hace una comparación elemento a elemento de la lista anterior con la nueva basados en su posición dentro de la misma, el primer elemento de la vieja se compara con el de la nueva y así sucesivamente. Si hay una diferencia con entre estos elementos React se encarga de mutar el DOM para reflejarlo.

En este caso, el primer elemento de la lista vieja es el 1 y el de la nueva el 0 por lo que se cambia el DOM; los siguientes elementos a comparar son el 2 de la vieja y el 1 de la nueva con lo que se vuelve a cambiar el DOM. Así hasta recorrer toda la lista. En la práctica para agregar un nuevo elemento en una lista con cinco ítems, hacemos 6 cambios en el DOM.

Si tenemos las keys asignadas, cada elemento de la lista vieja se compara con el elemento que tenga la misma key en la lista nueva. Entonces, los elementos preexistentes que no hayan sido modificados, todos en este caso, se mantienen, no generan mutaciones en el DOM. Únicamente se agrega un elemento para reflejar el nuevo ítem del arreglo y se hace un cambio para actualizar el DOM.

Devolver más de un componente o elemento

El método render de React espera recibir un componente o elemento, todo el demás contenido que queramos incluir tiene que estar anidado dentro de este. Construcciones al estilo:

<img src="..." alt="..."><br>

<label for="...">...</label><input type="...">

<h3>...</h3>
<p>...</p>

Todas son correctas en lo que a HTML respecta pero son erróneas como return del método render.

Olvidarnos de exportar los componentes

// Mensaje.js
import React, {Component} from 'react';

class Mensaje extends Component {
    render() {
        return (
            <div className="mensaje">{ this.props.texto }</div>
        );
    }
}
// index.js
import React, { Component } from "react";
import { render } from "react-dom";

import Mensaje from './Mensaje.js';

class App extends Component {
    render() {
        return (
            <div>
                <h3>Mi mensaje</h3>
                <Mensaje texto="Hola Mundo!" />
            </div>
        );
    }
}

render( <App />, document.getElementById('root') );

Al correr este código obtendremos el siguiente error:

Warning: React.createElement: type should not be null, undefined, boolean, or number. It should be a string (for DOM elements) or a ReactClass (for composite components). Check the render method of `App`.

Podemos revisar letra por letra el contenido del método render del componente App y no vamos a encontrar ningún error porque el problema está en Mensaje.js. Si lo revisamos atentamente podemos ver que nos faltó exportar la clase por defecto por lo que cuando importamos en App, terminamos con una variable apuntando a la nada misma en vez de al componente esperado.

En este caso, al tener 4 líneas únicamente el componente App es muy sencillo darnos cuenta que el error no se encuentra en este pero, en una aplicación más grande, con varios componentes externos y un par de operadores ternarios puede ser muy difícil detectarlo.

Modificar el estado de forma directa

Uno de los principios de funcionamiento de React es la inmutabilidad de la propiedad state del componente. Aunque dicha inmutabilidad no es (era) posible de garantizar en tiempo de ejecución por lo que el siguiente código será ejecutado sin generar errores ni advertencias de ningún tipo:

this.state.contador = 0;

Pero en la próxima llamada a setState el cambio será descartado de forma silenciosa provocando que el código no funcione de la forma esperada pero sin generar ningún tipo de error.

Añadir cosas al estado que no están relacionadas con el renderizado del componente

Cada vez que se realiza un cambio en el estado de un componente este va a ser renderizado de nuevo por lo que es importante no incluir en el mismo atributos que no tengan que ver con el render.

Por ejemplo, si creamos un componente encargado de subir un archivo vía ajax e incluimos una barra de progreso que permite ver el porcentaje de carga y usamos un atributo del estado para llevar el control del porcentaje, cada vez que este varíe el componente va a volver a ser renderizado provocando la interrupción de la carga en el peor de los casos y que en el componente sea renderizado 100 veces en el mejor.

Ídem si creamos un componente que ejecute una acción cada x segundos y guardamos un contador de las veces que fue ejecutado en el estado.

La forma correcta de guardar este tipo de datos es utilizar variables específicas de la instancia.

var counter = 0;
var timer;

componentDidMount() {
    timer = setInterval(this.tick, 1000);
}

tick() {
    this.counter++;
}

componentWillUnmount() {
    this.clearInterval(this.timer);
}

render() {
  <div>Ejecuciones: { this.counter } </div>
}

En la misma línea de lo antedicho, cualquier dato que pueda ser calculado a través de las propiedades pasadas al componente tampoco debería ser almacenada en el estado.

No usar propTypes

Muchos errores en el funcionamiento de nuestros componentes pueden ser difíciles de detectar si no utilizamos propTypes. Esta librería nos permite especificar exactamente el tipo de dato que esperamos recibir y tira un error cuando esto no ocurre permitiendo saber dónde empezar a buscar el problema.

Pasar props numéricos como strings

Si pasamos a nuestro componente a través de las propiedades un valor numérico entrecomillado, ese valor va a ser pasado como un string. Si queremos que el tipo sea correcto tenemos que recurrir a las llaves como cuando pasamos el valor de las variables.

<Componente num={13} />

Olvidarse que el método setState es asincrónico

Las llamadas a setState se ejecutan de forma asincrónica por lo que no podemos fiarnos que el estado se modifique inmediatamente y, por lo tanto, que las evaluaciones que hagamos basados en el mismo reciban el estado actualizado en el momento de realizado el cambio.

setState({contador: this.state.contador + 1});
if (contador >= 5) {
    // ...
}

El bloque condicional del código anterior no se puede asegurar que se vaya a ejecutar cuando contador sea igual a 5. Dependiendo de lo que tarde en actualizarse el estado puede que cuando lo haga, el contador valga 6 o 7.

Usar class en vez de className

A menos que estemos utilizando React en conjunto con los Web Components [note]Por ahora únicamente tienen soporte en Chrome por lo que es poco probable.[/note], si queremos asignar una clase CSS a un elemento HTML, tenemos que utilizar className.

<div className="container">...</div>

No comenzar el nombre de un componente con mayúscula

Para React, los componentes se diferencian de las etiquetas HTML mediante el nombre. Si el nombre comienza con mayúscula se considera un componente y se lo compila como los demás. Si el nombre comienza con minúscula se considera una etiqueta HTML y se pasa como un string, es decir, no va a ser compilado con todas sus consecuencias.

Cuándo (no) usar módulos o librerías externas en JavaScript

Cuándo no sabemos cómo programar la funcionalidad

Los módulos o librerías tienen que simplificar el proceso de desarrollo, no reemplazarlo. Su función es agilizar la escritura de código aportando una capa de abstracción sobre una funcionalidad cuyo desarrollo es complejo o engorroso. Si no sabemos cómo se hace algo, difícilmente podamos resolver los errores que surjan o sacar el máximo provecho a la herramienta que estamos usando.

Cuándo la funcionalidad aportada por el módulo o librería es mínima

Para entender esto nada mejor que referirnos a la historia de left-pad y como la baja por parte de su autor del módulo de 11 líneas de código provocó lo más parecido a un apocalipsis informático desde el Y2K. Casos similar es el módulo isarray con una línea de código y 10 millones de descargas semanales. Es necesario encontrar un balance, externalizar funciones muy sencillas nos expone a ciertos riesgos que no los corremos con módulos o librerías más complejas; no hacerlo implica tener que escribir más código con lo cual podemos cometer más errores pero nos aseguramos que esté siempre disponible y sea funcional en nuestro ecosistema.

Cuándo vamos a utilizar una única función de una librería multipropósito o general

Es muy común ver en una página, Bootstrap y sus dependencias jQuery, Hammer.js y Popper.js al completo para utilizar una grilla para acomodar los contenidos o jQuery, jQuery UI, y Select2 para implementar un select con autocomplete o, más últimamente, React + Babel para mostrar un sitio a todos los efectos estático del lado del cliente. Si únicamente vamos a necesitar una funcionalidad específica es más que probable que existan algunas librerías específicas o que su implementación sea lo suficientemente sencilla para que podamos crear nuestro propio módulo.

Cuándo necesitamos la máxima velocidad y el mínimo tamaño

Raramente es necesario lograr tan alto nivel de optimización pero, de hacer falta, siempre se logran mejores resultados mientras menos capas de abstracción y recursos externos utilicemos.

Propiedades no enumerables en JavaScript

Hasta la especificación ES5 de JavaScript las propiedades de un objeto consistían únicamente de un nombre y un valor. A partir de 2009, fecha de publicación, se agregó la posibilidad de establecer 3 atributos de las propiedades que permiten controlar sus características:

  • configurable
  • writable
  • enumerable

Configurable

Permite establecer si, luego de su creación, se podrán editar los atributos, eliminar la propiedad o modificar las funciones de acceso. Hay que tener en cuenta que una vez establecido a false, no puede volver ser modificado.

Writable

Indica si se puede modificar el valor de la propiedad mediante el uso de operadores de asignación.

Enumerable

Configura si la propiedad va a aparecer en el listado de propiedades del objeto. Por ejemplo, mediante el uso de Object.keys(). Si bien, en principio, no parece tener mucha utilidad es importante considerar qué otras funciones utilizan la enumeración de propiedades para ver su potencial.

Ya mencioné con anterioridad que el operador de propagación y Object.assign() únicamente copian aquellas propiedades enumerables. Además, permite ocultar propiedades de los bucles for … in, ya que al no ser enumeradas no son recorridas por el mismo.

Pero, una de las consecuencias más útiles de establecer una propiedad como no enumerable es que serán ocultadas a JSON.stringify(). Esto nos permite tener, de forma sencilla, total control sobre la información que exponemos del objeto.

var obj = {
    'prop1': 'value1'
};
Object.defineProperty(obj, 'prop2', { value: 'value2', enumerable: false });

JSON.stringify(obj); // { prop1: value1 }

Limitaciones y diferencias de Object.assign() y el operador de propagación en JavaScript

Con el (nuevo) auge de la programación funcional y de librerías como Redux ha resurgido el concepto de inmutabilidad. La existencia de estructuras de datos que no pueden cambiar su valor luego de haber sido iniciadas. Cualquier manipulación de la información requiere que creemos un nuevo objeto que copie los datos del original e introduzca los cambios deseados.

Más allá de las ventajas y desventajas de la inmutabilidad, en la práctica, cuando lo implementamos en nuestras aplicaciones nos encontramos en la necesidad de copiar con cierta asiduidad estructuras de datos. En ES6, como siempre en JavaScript, la forma elegida para la representación de estos datos son los objetos. Y aquí llegamos al tema del post, existen varias formas de copiar objetos.

Dejando de lado la forma más obvia y chabacana de copiar “manualmente” propiedad por propiedad, existen dos métodos para obtener una copia modificada de un objeto:

Object.assign()

obj = Object.assign( target, source1, ..., sourceN )

Como su nombre lo indica, esta función no está pensada originalmente para clonar objetos sino para asignar nueva información a un objeto existente pero pasando como target un objeto “vacio” podemos lograr el mismo efecto.

Operador de propagación

obj = { ...source1, ..., ...sourceN }

Ídem que con el caso anterior, la idea original de el operador de propagación no era copiar objetos sino simplificar la expansión de expresiones en situaciones donde se esperaba una cantidad múltiple y, generalmente, desconocida de parámetros. En este caso, la creación de un nuevo objeto que reciba como contenido el objeto original expandido es el truco para lograr  la copia deseada.

Diferencias

La diferencia entre estos métodos es que el primero setea nuevos valores en las propiedades de un objeto mientras que, el segundo, crea nuevas propiedades y les asigna información.

Es decir, si tenemos setter definidos en nuestro objeto, Object.Assign() los va a llamar y el operador de propagación, no. Esto también tiene como consecuencia que si nuestro objeto tiene propiedades de solo lectura el operador de propagación no las va a respetar, únicamente el Object.Assign().

Limitaciones

Las principales limitaciones que nos encontramos con ambos métodos es que la copia realizada es superficial y que únicamente se consideran las propiedades propias y enumerables del objeto.

Es decir, el nuevo objeto no ninguna propiedad definida en prototype, heredada o que se haya indicado explícitamente como no enumerable. Además, si tenemos algún objeto anidado este no se copiará sino que lo hará su referencia.

 

En conclusión ambas opciones son muy similares y su rendimiento es casi idéntico, aunque un poco mejor el del operador de propagación, por lo que el decantarse por uno u otro depende más de cuestiones subjetivas.

Particularmente, encuentro el funcionamiento del operador de propagación más acorde a lo que significa trabajar con objetos inmutables ya que si o si crea un nuevo objeto. Además, al momento de tratar con objetos anidados, resulta mucho más legible:

cons obj = {
    prop1: value1,
    obj1: {
        prop2: value2,
        prop3: value3
    }
}

const copia = {
    ...obj
    obj1: {
        ...obj.obj1
    }
};