Las ecuaciones de Fresnel describen el reflejo de la luz cuando se incide en una interfaz entre diferentes medios ópticos.
- https://en.wikipedia.org/wiki/fresnel_equations
# React 18+
yarn add @artsy/fresnel
# React 17
yarn add @artsy/fresnel@6Tabla de contenido
Al escribir componentes receptivos, es común usar consultas de medios para ajustar la pantalla cuando se cumplen ciertas condiciones. Históricamente, esto ha tenido lugar directamente en CSS/HTML:
@media screen and ( max-width : 767 px ) {
. my-container {
width : 100 % ;
}
}
@media screen and ( min-width : 768 px ) {
. my-container {
width : 50 % ;
}
} < div class =" my-container " /> Al engancharse en una definición de punto de interrupción, @artsy/fresnel adopta este enfoque declarativo y lo lleva al mundo React.
import React from "react"
import ReactDOM from "react-dom"
import { createMedia } from "@artsy/fresnel"
const { MediaContextProvider , Media } = createMedia ( {
// breakpoints values can be either strings or integers
breakpoints : {
sm : 0 ,
md : 768 ,
lg : 1024 ,
xl : 1192 ,
} ,
} )
const App = ( ) => (
< MediaContextProvider >
< Media at = "sm" >
< MobileApp />
</ Media >
< Media at = "md" >
< TabletApp />
</ Media >
< Media greaterThanOrEqual = "lg" >
< DesktopApp />
</ Media >
</ MediaContextProvider >
)
ReactDOM . render ( < App /> , document . getElementById ( "react" ) ) La primera cosa importante a tener en cuenta es que cuando el servidor renderiza con @artsy/fresnel , todos los puntos de interrupción son representados por el servidor. Cada componente Media está envuelto por CSS simple que solo mostrará ese punto de interrupción si coincide con el tamaño actual del navegador del usuario. Esto significa que el cliente puede comenzar a representar con precisión el HTML/CSS mientras recibe el marcado, que es mucho antes de que la aplicación React haya iniciado. Esto mejora el rendimiento percibido para los usuarios finales.
¿Por qué no simplemente representar el que necesita el dispositivo actual? No podemos identificar con precisión qué punto de interrupción necesita su dispositivo en el servidor. Podríamos usar una biblioteca para olfatear el agente de usuario del navegador, pero no siempre son precisos, y no nos darían toda la información que necesitamos saber cuándo estamos representando el servidor. Una vez que JS Boots y React se unen, simplemente se lava sobre el DOM y elimina el marcado que es innecesario, a través de una llamada matchMedia .
Primero, configure @artsy/fresnel en un archivo Media que se pueda compartir en la aplicación:
// Media.tsx
import { createMedia } from "@artsy/fresnel"
const ExampleAppMedia = createMedia ( {
breakpoints : {
sm : 0 ,
md : 768 ,
lg : 1024 ,
xl : 1192 ,
} ,
} )
// Generate CSS to be injected into the head
export const mediaStyle = ExampleAppMedia . createMediaStyle ( )
export const { Media , MediaContextProvider } = ExampleAppMedia Cree un nuevo archivo App que sea el punto de lanzamiento de nuestra aplicación:
// App.tsx
import React from "react"
import { Media , MediaContextProvider } from "./Media"
export const App = ( ) => {
return (
< MediaContextProvider >
< Media at = "sm" > Hello mobile! </ Media >
< Media greaterThan = "sm" > Hello desktop! </ Media >
</ MediaContextProvider >
)
} Montar <App /> en el cliente:
// client.tsx
import React from "react"
import ReactDOM from "react-dom"
import { App } from "./App"
ReactDOM . render ( < App /> , document . getElementById ( "react" ) ) Luego, en el servidor, configure la representación de SSR y pase mediaStyle en una etiqueta <style> en el encabezado:
// server.tsx
import React from "react"
import ReactDOMServer from "react-dom/server"
import express from "express"
import { App } from "./App"
import { mediaStyle } from "./Media"
const app = express ( )
app . get ( "/" , ( _req , res ) => {
const html = ReactDOMServer . renderToString ( < App /> )
res . send ( `
<html>
<head>
<title>@artsy/fresnel - SSR Example</title>
<!–– Inject the generated styles into the page head -->
<style type="text/css"> ${ mediaStyle } </style>
</head>
<body>
<div id="react"> ${ html } </div>
<script src='/assets/app.js'></script>
</body>
</html>
` )
} )
app . listen ( 3000 , ( ) => {
console . warn ( "nApp started at http://localhost:3000 n" )
} )¡Y eso es todo! Para probar, deshabilite JS y escala la ventana de su navegador a un tamaño móvil y recarga; Hará correctamente el diseño móvil sin la necesidad de usar un agente de usuario u otras "sugerencias" del lado del servidor.
@artsy/fresnel funciona muy bien con Gatsby o el enfoque híbrido estático de Next.js para la representación. Vea los ejemplos a continuación para una implementación simple.
Hay cuatro ejemplos que uno puede explorar en la carpeta /examples :
Si bien los ejemplos Basic y SSR obtendrán uno bastante lejos, @artsy/fresnel puede hacer mucho más. Para obtener una inmersión profunda exhaustiva en sus características, consulte la aplicación de fregadero de la cocina.
Si está utilizando Gatsby, también puede probar Gatsby-Plugin-Fesnel para una fácil configuración.
Otras soluciones existentes adoptan un enfoque renderizado condicionalmente, como react-responsive o react-media , entonces, ¿dónde difiere este enfoque?
Renderización del lado del servidor!
Pero primero, ¿qué es la representación condicional?
En el ecosistema React, un enfoque común para escribir componentes de sensación declarativa es utilizar la API de matchMedia del navegador:
< Responsive >
{ ( { sm } ) => {
if ( sm ) {
return < MobileApp />
} else {
return < DesktopApp />
}
} }
</ Responsive >En el cliente, cuando se coincide un punto de interrupción dado, reacciona condicionalmente hace que un árbol.
Sin embargo, este enfoque tiene algunas limitaciones para lo que queríamos lograr con nuestra configuración de representación del lado del servidor:
Es imposible conocer de manera confiable el punto de interrupción actual del usuario durante la fase de renderizado del servidor, ya que eso requiere un navegador.
La configuración de los tamaños de punto de interrupción basados en el olfateo de agente de usuario es propenso a los errores debido a la incapacidad de hacer coincidir con precisión las capacidades del dispositivo con el tamaño. Un dispositivo móvil puede tener mayor densidad de píxeles que otro, un dispositivo móvil puede ajustarse a múltiples puntos de interrupción al tener en cuenta la orientación del dispositivo, y en los clientes de escritorio no hay forma de saberlo. Lo mejor que los desarrolladores pueden hacer es adivinar el punto de interrupción actual y llenar <Responsive> con el estado asumido.
Artsy se decidió por lo que creemos que hace las mejores compensaciones. Abordamos este problema de la siguiente manera:
Renderize el marcado para todos los puntos de interrupción en el servidor y envíelo por el cable.
El navegador recibe un marcado con el estilo de consulta de medios adecuado e inmediatamente comenzará a representar el resultado visual esperado para cualquier ancho de la ventana gráfica en el que esté el navegador.
Cuando todo JS ha cargado y React comienza la fase de rehidratación, consultamos el navegador para qué punto de interrupción está actualmente y luego limitó los componentes renderizados a las consultas de medios coincidentes. Esto evita que los métodos de ciclo de vida se disparen en componentes ocultos y que HTML no utilizado se reescribe al DOM.
Además, registramos a los oyentes de eventos con el navegador para notificar al MediaContextProvider cuando se coincide con un punto de interrupción diferente y luego volvemos a retener el árbol utilizando el nuevo valor para el onlyMatch Prop.
Comparemos cómo se vería un árbol de componentes usando matchMedia con nuestro enfoque:
| Antes | Después |
|---|---|
< Responsive >
{ ( { sm } ) => {
if ( sm ) return < SmallArticleItem { ... props } />
else return < LargeArticleItem { ... props } />
} }
</ Responsive > | < >
< Media at = "sm" >
< SmallArticleItem { ... props } />
</ Media >
< Media greaterThan = "sm" >
< LargeArticleItem { ... props } />
</ Media >
</ > |
Consulte la aplicación de renderizado del lado del servidor para un ejemplo de trabajo.
Lo primero es lo primero. Deberá definir los puntos de interrupción y la interacción necesarios para su diseño para producir el conjunto de componentes de medios que puede usar durante su aplicación.
Por ejemplo, considere una aplicación que tiene los siguientes puntos de interrupción:
sm .md .lg .xl .Y las siguientes interacciones:
hover .notHover .Luego produciría el conjunto de componentes de medios como así:
// Media.tsx
const ExampleAppMedia = createMedia ( {
breakpoints : {
sm : 0 ,
md : 768 ,
lg : 1024 ,
xl : 1192 ,
} ,
interactions : {
hover : "(hover: hover)" ,
notHover : "(hover: none)" ,
landscape : "not all and (orientation: landscape)" ,
portrait : "not all and (orientation: portrait)" ,
} ,
} )
export const { Media , MediaContextProvider , createMediaStyle } = ExampleAppMediaComo puede ver, los puntos de interrupción se definen por su compensación de inicio , donde se espera que el primero comience en 0.
El componente MediaContextProvider influye en cómo se representarán los componentes Media . Montelo en la raíz de su árbol de componentes:
import React from "react"
import { MediaContextProvider } from "./Media"
export const App = ( ) => {
return < MediaContextProvider > ... </ MediaContextProvider >
} El componente Media creado para su aplicación tiene algunos accesorios mutuamente excluyentes que componen la API que usará para declarar sus diseños receptivos. Todos estos accesorios operan en función de los puntos de interrupción nombrados que se proporcionaron cuando creó los componentes de los medios.
import React from "react"
import { Media } from "./Media"
export const HomePage = ( ) => {
return (
< >
< Media at = "sm" > Hello mobile! </ Media >
< Media greaterThan = "sm" > Hello desktop! </ Media >
</ >
)
}Los ejemplos dados para cada accesorio utilizan definiciones de punto de interrupción como se define en la sección 'Configuración' anterior.
Si desea evitar el DIV subyacente generado por <Media> y, en su lugar, use su propio elemento, use el formulario Render-Props pero asegúrese de no renderizar a ningún niño cuando no sea necesario:
export const HomePage = ( ) => {
return (
< >
< Media at = "sm" > Hello mobile! </ Media >
< Media greaterThan = "sm" >
{ ( className , renderChildren ) => {
return (
< MySpecialComponent className = { className } >
{ renderChildren ? "Hello desktop!" : null }
</ MySpecialComponent >
)
} }
</ Media >
</ >
)
} NOTA: Esto solo se usa cuando se repite SSR
Además de los componentes Media y MediaContextProvider , existe una función createMediaStyle que produce el estilo CSS para todas las consultas de medios posibles que la instancia Media puede utilizar mientras el marcado se pasa del servidor al cliente durante la hidratación. Si solo se usa un subconjunto de teclas de punto de interrupción, se pueden especificar opcionales como un parámetro para minimizar la salida. Asegúrese de insertar esto dentro de una etiqueta <style> en <head> de su documento.
Es aconsejable hacer esta configuración en su propio módulo para que pueda importarse fácilmente a lo largo de su aplicación:
import { createMedia } from "@artsy/fresnel"
const ExampleAppMedia = createMedia ( {
breakpoints : {
sm : 0 ,
md : 768 ,
lg : 1024 ,
xl : 1192 ,
} ,
} )
// Generate CSS to be injected into the head
export const mediaStyle = ExampleAppMedia . createMediaStyle ( ) // optional: .createMediaStyle(['at'])
export const { Media , MediaContextProvider } = ExampleAppMedia La representación puede limitarse a puntos de interrupción/interacciones específicos especificando una lista de consultas de medios para que coincidan. Por defecto, todos se presentarán.
De manera predeterminada, cuando se reproduce del lado del cliente, la API de matchMedia del navegador se utilizará para restringir aún más la lista onlyMatch de las consultas de medios que coinciden actualmente. Esto se hace para evitar activar ganchos de ciclo de vida relacionados con el montaje de componentes ocultos.
Deshabilitar este comportamiento está destinado principalmente a fines de depuración.
Use esto para declarar que los niños solo deben ser visibles en un punto de ruptura específico, lo que significa que el ancho de la ventana de la ventana gráfica es mayor o igual a la compensación de inicio del punto de interrupción, pero menos que el siguiente punto de interrupción, si existe.
Por ejemplo, los niños de esta declaración Media solo serán visibles si el ancho de la ventana gráfica está entre 0 y 768 (768 no incluidos) puntos:
< Media at = "sm" > ... </ Media >La regla CSS correspondiente:
@media not all and ( min-width : 0 px ) and ( max-width : 767 px ) {
. fresnel-at-sm {
display : none !important ;
}
}Use esto para declarar que los niños solo deben ser visibles, mientras que el ancho de la ventana gráfica es menor que la compensación de inicio del punto de interrupción especificado.
Por ejemplo, los niños de esta declaración Media solo serán visibles si el ancho de la ventana gráfica está entre 0 y 1024 (1024 no incluido) puntos:
< Media lessThan = "lg" > ... </ Media >La regla CSS correspondiente:
@media not all and ( max-width : 1023 px ) {
. fresnel-lessThan-lg {
display : none !important ;
}
}Use esto para declarar que los niños solo deben ser visibles, mientras que el ancho de la ventana gráfica es igual o mayor que la compensación de inicio del siguiente punto de interrupción.
Por ejemplo, los niños de esta declaración Media solo serán visibles si el ancho de la ventana gráfica es igual o mayor que 1024 puntos:
< Media greaterThan = "md" > ... </ Media >La regla CSS correspondiente:
@media not all and ( min-width : 1024 px ) {
. fresnel-greaterThan-md {
display : none !important ;
}
}Use esto para declarar que los niños solo deben ser visibles, mientras que el ancho de la ventana gráfica es igual al desplazamiento de inicio del punto de interrupción especificado o mayor.
Por ejemplo, los niños de esta declaración Media solo serán visibles si el ancho de la ventana gráfica es de 768 puntos o más:
< Media greaterThanOrEqual = "md" > ... </ Media >La regla CSS correspondiente:
@media not all and ( min-width : 768 px ) {
. fresnel-greaterThanOrEqual-md {
display : none !important ;
}
}Use esto para declarar que los niños solo deben ser visibles, mientras que el ancho de la ventana gráfica es igual al desplazamiento de inicio del primer punto de interrupción especificado pero menos que el desplazamiento de inicio del segundo punto de interrupción especificado.
Por ejemplo, los niños de esta declaración Media solo serán visibles si el ancho de la ventana gráfica está entre 768 y 1192 (1192 no incluido) puntos:
< Media between = { [ "md" , "xl" ] } > ... </ Media >La regla CSS correspondiente:
@media not all and ( min-width : 768 px ) and ( max-width : 1191 px ) {
. fresnel-between-md-xl {
display : none !important ;
}
}Pros:
Contras:
<Media> en el que se encuentran. Ese último punto presenta un problema interesante. ¿Cómo podríamos representar un componente que se diseñe de manera diferente en diferentes puntos de interrupción? (Imaginemos un ejemplo matchMedia ).
< Sans size = { sm ? 2 : 3 } > < >
< Media at = "sm" > { this . getComponent ( "sm" ) } </ Media >
< Media greaterThan = "sm" > { this . getComponent ( ) } </ Media >
</ > getComponent ( breakpoint ?: string ) {
const sm = breakpoint === 'sm'
return < Sans size = { sm ? 2 : 3 } />
}Todavía estamos descubriendo patrones para esto, así que háganos saber si tiene sugerencias.
Este proyecto utiliza la liberación automática para liberarse automáticamente en cada PR. Cada PR debe tener una etiqueta que coincida con una de las siguientes
Mayor, Minor y Patch hará que se genere una nueva versión. Use especialización para romper los cambios, menor para nuevas funciones no rompientes y parche para correcciones de errores. Trivial no causará una versión y debe usarse al actualizar la documentación o el código sin proyectos.
Si no desea lanzar en un PR en particular, pero los cambios no son triviales, entonces use la etiqueta Skip Release junto con la etiqueta de versión apropiada.