Advertisement
  1. Code
  2. HTML & CSS

Introducción a Shadow DOM

Scroll to top
Read Time: 13 min

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

Tome cualquier página web moderna y se dará cuenta de que, invariablemente, contiene contenido unido de una variedad de fuentes diferentes; puede incluir los widgets de redes sociales de Twitter o Facebook o un widget de reproducción de video de Youtube, puede servir un anuncio personalizado de algún servidor de anuncios o puede incluir algunos scripts o estilos de utilidad de una biblioteca de terceros alojada en CDN y así sucesivamente. Y si todo está basado en HTML (como se prefiere en estos días), existe una alta probabilidad de colisiones entre el marcado, los scripts o los estilos servidos desde varias fuentes. Generalmente, los espacios de nombres se emplean para evitar estas colisiones que resuelven el problema en cierta medida, pero no ofrecen Encapsulación.

La encapsulación es uno de los pilares sobre los cuales se fundó el paradigma de Programación Orientada a Objetos y se usa normalmente para restringir la representación interna de un objeto desde el mundo exterior.

Volviendo a nuestro problema, seguramente podemos encapsular el código JavaScript usando closures o usando el patrón módulo, pero ¿podemos hacer lo mismo para nuestro marcado HTML? Imagine que tenemos que crear un widget de interfaz de usuario, ¿podemos ocultar los detalles de la implementación de nuestro widget del código JavaScript y CSS que se incluye en la página, que consume nuestro widget? Alternativamente, ¿podemos evitar que el código de consumo arruine la funcionalidad o apariencia de nuestro widget?


Shadow DOM al rescate

La única solución existente que crea un límite entre el código que usted escribe y el código que consume, es fea, y funciona mediante un iFrame voluminoso y restrictivo, que trae consigo otro conjunto de problemas. Entonces, ¿nos vemos obligados a adaptarnos a este enfoque siempre?

¡Ya no! Shadow DOM nos proporciona una forma elegante de superponer el subárbol DOM normal con un fragmento de documento especial que contiene otro subárbol de nodos, que son impregnables de scripts y estilos. ¡Lo interesante es que no es algo nuevo! Varios navegadores ya han estado utilizando esta metodología para implementar widgets nativos como fecha, controles deslizantes, audio, reproductores de video, etc.

Habilitando Shadow DOM

En el momento de escribir este artículo, la versión actual de Chrome (v29) admite la inspección de Shadow DOM mediante Chrome DevTools. Abra Devtools y haga clic en el botón de engranaje en la parte inferior derecha de la pantalla para abrir el panel de Configuración, desplácese hacia abajo un poco y verá una casilla de verificación para mostrar Shadow DOM.

Turn on Shadow DOMTurn on Shadow DOMTurn on Shadow DOM

Ahora que hemos habilitado nuestro navegador, echemos un vistazo a las partes internas del reproductor de audio predeterminado. Sólo escribe:

1
<audio width="300" height="32" src="http://developer.mozilla.org/@api/deki/files/2926/=AudioTest_(1).ogg" autoplay="autoplay" controls="controls">
2
 Your browser does not support the HTML5 Audio.
3
 </audio>

En su marcado HTML. Muestra el siguiente reproductor de audio nativo en los navegadores compatibles:

audio_player

Ahora ve y revisa el widget del reproductor de audio que acabas de crear.

Shadow DOM of Native Date WidgetShadow DOM of Native Date WidgetShadow DOM of Native Date Widget

¡Guauu! Muestra la representación interna del reproductor de audio, que de otro modo estaba oculto. Como podemos ver, el elemento de audio utiliza un fragmento de documento para contener el contenido interno del widget y lo agrega al elemento contenedor (que se conoce como Shadow Host).

Shadow Host y Shadow Root

  • Shadow Host: es el elemento DOM que aloja el subárbol Shadow DOM o es el nodo DOM que contiene la raíz de Shadow.
  • Shadow Root: es la raíz del subárbol DOM que contiene los nodos DOM de sombra. Es un nodo especial, que crea el límite entre los nodos DOM normales y los nodos Shadow DOM. Es este límite, que encapsula los nodos DOM de Shadow desde cualquier código JavaScript o CSS en la página consumidora.
  • Shadow DOM: permite que múltiples subárboles de DOM se compongan en un árbol más grande. Las siguientes imágenes del borrador de trabajo del W3C explican mejor el concepto de superposición de nodos. Así es como se ve antes de que el contenido de Shadow Root se adjunte al elemento Shadow Host:
    Normal Document Tree Shadow DOM SubtreesNormal Document Tree Shadow DOM SubtreesNormal Document Tree Shadow DOM Subtrees

    Cuando se procesa, el árbol de la sombra tiene lugar del contenido del Shadow Host.

    Composition CompleteComposition CompleteComposition Complete

    Este proceso de superposición de los nodos a menudo se denomina Composición.

  • Límite de sombra: se denota por la línea de puntos en la imagen de arriba. Esto denota la separación entre el mundo DOM normal y el mundo DOM de Sombra. Los scripts de cualquier lado no pueden cruzar este límite y crear un caos en el otro lado.

Hola mundo Shadow DOM

Basta de charlar, digo, ensuciémonos escribiendo un código. Supongamos que tenemos el siguiente marcado, que muestra un mensaje de bienvenida simple.

1
<div id="welcomeMessage">Welcome to My World</div>

Agregue el siguiente código JavaScript o use este Fiddle:

1
var shadowHost = document.querySelector("#welcomeMessage");
2
var shadowRoot = shadowHost.webkitCreateShadowRoot();
3
shadowRoot.textContent = "Hello Shadow DOM World";

Aquí creamos una Shadow Raíz usando la función webkitCreateShadowRoot(), la adjuntamos a un Shadow Host y luego simplemente cambiamos el contenido.

Observe el prefijo específico del proveedor webkit antes del nombre de la función. Esto indica que esta funcionalidad es actualmente compatible con algunos navegadores basados en webkit solamente.

Si sigue adelante y ejecuta este ejemplo en un navegador compatible, verá "Hello Shadow DOM World" en lugar de "Welcome to My World", ya que los nodos DOM de Shadow han superado a los normales.

Descargo de responsabilidad: como algunos de ustedes notarán, estamos mezclando el marcado con los scripts, lo que generalmente no se recomienda y Shadow DOM no es una excepción. Hemos evitado deliberadamente el uso de plantillas tan temprano en el juego para evitar cualquier confusión. De lo contrario, Shadow DOM brinda una solución elegante a este problema y llegaremos muy pronto.


Respetando el límite de la sombra

Si intentas acceder al contenido del árbol renderizado utilizando JavaScript, así:

1
var shadowHost = document.querySelector("#welcomeMessage");
2
var shadowRoot = shadowHost.webkitCreateShadowRoot();
3
shadowRoot.textContent = "Hello Shadow DOM World";
4
5
console.log(shadowHost.textContent);
6
 // Prints "Welcome to My World" as the shadow DOM nodes are encapsulated and cannot be accessed by JavaScript

Obtendrá el contenido original "Bienvenido a mi mundo" y no el contenido que realmente se representa en la página, ya que el árbol DOM de Shadow está encapsulado desde cualquier script. Esto también significa que el widget que crea utilizando Shadow DOM está a salvo de cualquier script no deseado / conflictivo ya presente en la página.

Encapsulación de Estilos

Del mismo modo, cualquier selector de CSS está prohibido cruzar el límite de la sombra. Verifique el siguiente código en el que hemos aplicado color rojo a los elementos de la lista, pero ese estilo solo se aplica a los nodos que forman parte de la página principal, y los elementos de la lista que son parte de Shadow Root no se ven afectados con este estilo.

1
<div class="outer">
2
  <div id="welcomeMessage">Welcome to My World</div>
3
  <div class="normalTree">Sample List
4
  <ul>
5
      <li>Item 1</li>
6
      <li>Item 2</li>
7
  </ul>
8
  </div>
9
</div>
10
<style>
11
   div.outer li {  
12
      color: red;  
13
   } 
14
   div.outer{  
15
      border: solid 1px;  padding: 1em; 
16
   }
17
</style>
18
<script type="text/javascript">
19
    var shadowHost = document.querySelector("#welcomeMessage");
20
    var shadowRoot = shadowHost.webkitCreateShadowRoot();
21
    shadowRoot.innerHTML = ["<div class='shadowChild'>",
22
                            "Shadow DOM offers us Encapsulation from",
23
                            "<ul>",
24
                            "<li>Scripts</li>",
25
                            "<li>Styles</li>",
26
                            "</ul>",
27
                            "</div>"
28
                            ].join(',').replace(/,/g,"");
29
</script>

Puedes ver el código en acción en Fiddle. Esta encapsulación se aplica incluso si invertimos la dirección de recorrido. Cualquier estilo que se defina dentro del Shadow DOM no afecta al documento principal y permanece dentro del ámbito de la Shadow Shadow solamente. Consulte este Fiddle para ver un ejemplo, donde aplicamos el color azul a los elementos de la lista en Shadow DOM pero los elementos de la lista del documento principal no se ven afectados.

Sin embargo, hay una excepción notable aquí; Shadow DOM nos da la flexibilidad de diseñar el Shadow Host, el nodo DOM que contiene el Shadow DOM. Lo ideal es que se encuentre fuera del límite de la Sombra y no sea parte de la Raíz de la Sombra, pero al usar la regla @host, se pueden especificar los estilos que se pueden aplicar a la Anfitriona de la Sombra, ya que hemos estilizado el mensaje de bienvenida en el siguiente ejemplo.

1
<div id="welcomeMessage">Welcome to My World</div>
2
<script type="text/javascript">
3
  var shadowHost = document.querySelector("#welcomeMessage");
4
  var shadowRoot = shadowHost.webkitCreateShadowRoot();
5
  shadowRoot.innerHTML = ["<style>",
6
                          "@host{ ",
7
                             "#welcomeMessage{ ",
8
                                "font-size: 28px;",
9
                                "font-family:cursive;",
10
                                "font-weight:bold;",
11
                             "}",
12
                          "}",
13
                          "</style>",
14
                          "<content select=''></content>"
15
                          ].join(',').replace(/,/g,"");
16
</script>

Verifique este Fiddle a medida que diseñamos el mensaje de bienvenida de Shadow Host usando los estilos definidos en Shadow DOM.

Crear de ganchos de estilo

Como desarrollador de widgets, es posible que desee que el usuario de mi widget pueda diseñar ciertos elementos. Esto se puede lograr conectando un agujero en el límite de la sombra mediante pseudo elementos personalizados. Esto es similar a cómo algunos navegadores crean enlaces de estilo para que el desarrollador diseñe algunos elementos internos de un widget nativo. Por ejemplo, para diseñar el pulgar y la pista del control deslizante nativo, puede utilizar ::-webkit-slider-thumb y ::webkit-slider-runnable-track de la siguiente manera:

1
input[type=range]{
2
    -webkit-appearance:none;
3
 }
4
 input[type=range]::-webkit-slider-thumb {
5
    -webkit-appearance:none;
6
    height:12px;
7
    width:12px;
8
    border-radius:6px;
9
    background:yellow;
10
    position:relative;
11
    top:-5px;
12
 }
13
 input[type=range]::-webkit-slider-runnable-track {
14
    background:red;
15
    height:2px;
16
 }

¡Bifurca este Fiddle y aplica tus propios estilos!

Redireccionamiento de eventos

Si un evento que se origina en uno de los nodos en Shadow DOM cruza el límite de Shadow, entonces se redirige para referirse al Shadow Host para mantener la encapsulación. Considere el siguiente código:

1
<input id="normalText" type="text" value="Normal DOM Text Node" />
2
<div id="shadowHost"></div>
3
<input id="shadowText" type="text" value="Shadow DOM Node" />
4
<script type="text/javascript">
5
    var shadowHost = document.querySelector('#shadowHost');
6
    var shadowRoot = shadowHost.webkitCreateShadowRoot();
7
    var template = document.querySelector('template');
8
    shadowRoot.appendChild(template.content.cloneNode(true));
9
    template.remove();
10
    document.addEventListener('click', function(e) { 
11
                                 console.log(e.target.id + ' clicked!'); 
12
                              });
13
</script>

Presenta dos elementos de entrada de texto, uno a través de DOM normal y otro a través de DOM de sombra y luego escucha un evento de click en el document. Ahora, cuando se hace clic en el segundo ingreso de texto, el evento se origina desde Shadow DOM y cuando cruza el límite de Shadow, el evento se modifica para cambiar el elemento de destino al elemento <div> de Shadow Host en lugar de la entrada de texto <input>. También hemos introducido un nuevo elemento <template> aquí; Esto es conceptualmente similar a las soluciones de plantillas del lado del cliente como Handlebars y Underscore, pero no es tan evolucionado y carece de soporte para el navegador. Dicho esto, usar plantillas es la forma ideal de escribir Shadow DOM en lugar de usar etiquetas de script como se ha hecho hasta ahora en este artículo.


Separación de intereses

Ya sabemos que siempre es una buena idea separar el contenido real de la presentación; Shadow DOM no debe incrustar ningún contenido, que finalmente se mostrará al usuario. Más bien, el contenido siempre debe estar presente en la página original y no oculto dentro de la plantilla de DOM de Shadow. Cuando se produce la composición, este contenido debe proyectarse en los puntos de inserción apropiados definidos en la plantilla de Shadow DOM. Reescribamos el ejemplo de Hello World, teniendo en cuenta la separación anterior: se puede encontrar un ejemplo en Fiddle.

1
<div id="welcomeMessage">Welcome to Shadow DOM World</div>
2
<script type="text/javascript">
3
    var shadowRoot = document.querySelector("#welcomeMessage").webkitCreateShadowRoot();
4
    var template = document.querySelector("template");
5
    shadowRoot.appendChild(template.content); 
6
    template.remove();
7
</script>

Cuando se representa la página, el contenido de Shadow Host se proyecta en el lugar donde aparece el elemento <content> . Este es un ejemplo muy simplista en el que <content> recoge todo lo que hay dentro del Shadow Host durante la composición. Pero puede ser muy selectivo al seleccionar el contenido de Shadow Host utilizando el atributo select como se muestra a continuación.

1
<div id="outer">How about some cool demo, eh ?
2
    <div class="cursiveButton">My Awesome Button</div>
3
</div>
4
<button>
5
  Fallback Content
6
</button>
7
<style>
8
button{ 
9
   font-family: cursive;  
10
   font-size: 24px;
11
   color: red; 
12
}
13
</style>
14
<script type="text/javascript">
15
    var shadowRoot = document.querySelector("#outer").webkitCreateShadowRoot(); 
16
    var template = document.querySelector("template"); 
17
    shadowRoot.appendChild(template.content.cloneNode(true));
18
    template.remove();
19
</script>

Vea la demostración en vivo y juegue con ella para comprender mejor el concepto de puntos de inserción y proyecciones.


Componentes web

Como ya sabrá, Shadow DOM es parte de la especificación de componentes web, que ofrece otras características interesantes, como:

  1. Plantillas: se utilizan para mantener el marcado inerte, que se utilizará en un momento posterior. Por inerte, queremos decir que todas las imágenes en el marcado no se descargan, los scripts incluidos no están presentes hasta que el contenido de la plantilla se convierte en parte de la página.
  2. Decoradores: se utilizan para aplicar las plantillas basadas en los selectores de CSS y, por lo tanto, pueden verse como decorar los elementos existentes al mejorar su presentación.
  3. Importaciones de HTML: nos brinda la capacidad de reutilizar otros documentos HTML en nuestro documento sin tener que hacer llamadas XHR explícitamente y escribir controladores de eventos para ello.
  4. Elementos personalizados: nos permite definir nuevos tipos de elementos HTML que luego se pueden usar de forma declarativa en el marcado. Por ejemplo, si desea crear su propio widget de navegación, defina su elemento de navegación, heredando de HTMLElement y proporcionando ciertas devoluciones de llamadas de ciclo de vida que implementan ciertos eventos como la construcción, el cambio, la destrucción del widget y simplemente utiliza ese widget en su marca. como <myAwesomeNavigation attr1="value1"..></myAwesomeNavigation>. Así que los elementos personalizados esencialmente nos dan una forma de agrupar toda la magia de Shadow DOM, ocultando los detalles internos y los paquetes de todos juntos.

No balbucearía mucho sobre otros aspectos de la especificación de componentes web en este artículo, pero nos haría bien recordar que juntos nos permiten crear widgets de interfaz de usuario reutilizables que son portátiles en todos los navegadores en apariencia y están completamente encapsulados por todos los Scripts y estilos de la página consumidora.


Conclusión

La especificación de componentes web es un trabajo en progreso y el código de muestra incluido que funciona hoy puede no funcionar en una versión posterior. Como ejemplo, los textos anteriores sobre este tema utilizan el método webkitShadowRoot() que ya no funciona; En su lugar, use createWebkitShadowRoot() para crear una raíz de sombra. Entonces, si desea usar esto para crear algunas demostraciones geniales usando Shadow DOM, siempre es mejor consultar la especificación para obtener más detalles.

Actualmente, solo Chrome y Opera lo admiten, por lo que desconfiaría de incluir Shadow DOM en mi instancia de producción, pero con Google con Polymer, que se basa en componentes web y Polyfills para apoyar a Shadow DOM de forma nativa, esto es Sin duda algo con lo que todo desarrollador web debe ensuciarse las manos.

También puedes mantenerte actualizado con los últimos acontecimientos en Shadow DOM siguiendo este canal de Google+. También revisa la herramienta Shadow DOM Visualizer, que te ayuda a visualizar cómo se reproduce Shadow DOM en el navegador.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.