As equações de Fresnel descrevem o reflexo da luz quando incidente em uma interface entre diferentes meios ópticos.
- https://en.wikipedia.org/wiki/fresnel_equations
# React 18+
yarn add @artsy/fresnel
# React 17
yarn add @artsy/fresnel@6Índice
Ao escrever componentes responsivos, é comum o uso de consultas de mídia para ajustar a tela quando determinadas condições forem atendidas. Historicamente, isso ocorreu diretamente no 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 " /> Ao entrar em uma definição de ponto de interrupção, @artsy/fresnel adota essa abordagem declarativa e a traz para o mundo do 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" ) ) A primeira coisa importante a ser observada é que, ao renderizar o servidor com @artsy/fresnel , todos os pontos de interrupção são renderizados pelo servidor. Cada componente Media é embrulhado por CSS simples que mostrará apenas esse ponto de interrupção se corresponder ao tamanho atual do navegador do usuário. Isso significa que o cliente pode começar com precisão a renderizar o HTML/CSS enquanto recebe a marcação, que é muito antes do aplicativo React ser inicializado. Isso melhora o desempenho percebido para os usuários finais.
Por que não apenas renderizar o que o dispositivo atual precisa? Não podemos identificar com precisão qual ponto de interrupção seu dispositivo precisa no servidor. Poderíamos usar uma biblioteca para farejar o agente do usuário do navegador, mas eles nem sempre são precisos, e eles não nos dariam todas as informações que precisamos saber quando estamos renderizando o servidor. Depois que as botas JS e o React do lado do cliente se conectam, ele simplesmente lava o DOM e remove a marcação desnecessária, por meio de uma chamada matchMedia .
Primeiro, configure @artsy/fresnel em um arquivo Media que pode ser compartilhado em todo o aplicativo:
// 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 Crie um novo arquivo de App que será o ponto de lançamento para o nosso aplicativo:
// 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 >
)
} Mount <App /> no cliente:
// client.tsx
import React from "react"
import ReactDOM from "react-dom"
import { App } from "./App"
ReactDOM . render ( < App /> , document . getElementById ( "react" ) ) Em seguida, no servidor, configure a renderização do SSR e passe mediaStyle para uma tag <style> no cabeçalho:
// 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" )
} )E é isso! Para testar, desative o JS e escala a janela do navegador até um tamanho móvel e recarregue; Ele renderizará corretamente o layout móvel sem a necessidade de usar um agente de usuário ou outras "dicas" do lado do servidor.
@artsy/fresnel funciona muito bem com a abordagem híbrida estática do Gatsby ou Next.JS para renderizar. Veja os exemplos abaixo para obter uma implementação simples.
Existem quatro exemplos que se pode explorar na pasta /examples :
Embora os exemplos Basic e SSR cheguem a um bem longe, @artsy/fresnel pode fazer muito mais. Para um mergulho exaustivo em seus recursos, confira o aplicativo da pia da cozinha.
Se você estiver usando o Gatsby, também pode experimentar o Gatsby-Plugin-Fresnel para facilitar a configuração.
Outras soluções existentes adotam uma abordagem condicionalmente renderizada, como react-responsive ou react-media , então onde essa abordagem é diferente?
Renderização do lado do servidor!
Mas primeiro, o que é a renderização condicional?
No ecossistema do React, uma abordagem comum para escrever componentes responsivos declarativos é usar a API do matchMedia do navegador:
< Responsive >
{ ( { sm } ) => {
if ( sm ) {
return < MobileApp />
} else {
return < DesktopApp />
}
} }
</ Responsive >No cliente, quando um determinado ponto de interrupção é correspondente, o React renderiza condicionalmente uma árvore.
No entanto, essa abordagem tem algumas limitações para o que queríamos alcançar com nossa configuração de renderização do lado do servidor:
É impossível conhecer com segurança o ponto de interrupção atual do usuário durante a fase de renderização do servidor, pois isso requer um navegador.
A definição de tamanhos do ponto de interrupção com base no sniffing agente do usuário é propenso a erros devido à incapacidade de corresponder precisamente aos recursos do dispositivo ao tamanho. Um dispositivo móvel pode ter maior densidade de pixels que outro, um dispositivo móvel pode se encaixar em vários pontos de interrupção ao levar em consideração a orientação do dispositivo e, nos clientes da área de trabalho, não há como saber. Os melhores desenvolvedores podem fazer é adivinhar o ponto de interrupção atual e preencher <Responsive> com o estado assumido.
Artsy decidiu o que achamos que faz as melhores compensações. Abordamos esse problema da seguinte maneira:
Renderize a marcação para todos os pontos de interrupção no servidor e envie -o no fio.
O navegador recebe marcação com o estilo de consulta de mídia adequado e começará imediatamente a renderizar o resultado visual esperado para qualquer largura de viewport em que o navegador esteja.
Quando todo o JS carregar e reagir inicia a fase de reidratação, consultamos o navegador sobre o ponto de interrupção que está atualmente em e depois limitamos os componentes renderizados às consultas de mídia correspondentes. Isso impede que os métodos do ciclo de vida disparem em componentes ocultos e HTML não utilizado seja reescrito no DOM.
Além disso, registramos ouvintes de eventos no navegador para notificar o MediaContextProvider quando um ponto de interrupção diferente é correspondido e, em seguida, renderize a árvore usando o novo valor para o onlyMatch suporte.
Vamos comparar como seria uma árvore de componentes usando matchMedia com a nossa abordagem:
| Antes | Depois |
|---|---|
< 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 o aplicativo de renderização do lado do servidor para obter um exemplo de funcionamento.
Primeiras coisas primeiro. Você precisará definir os pontos de interrupção e a interação necessários para o seu design produzir o conjunto de componentes de mídia que você pode usar em todo o aplicativo.
Por exemplo, considere um aplicativo que tenha os seguintes pontos de interrupção:
sm .md .lg .xl .E as seguintes interações:
hover .notHover .Você então produziria o conjunto de componentes de mídia como assim:
// 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 você pode ver, os pontos de interrupção são definidos pelo deslocamento do início , onde o primeiro deve começar em 0.
O componente MediaContextProvider influencia como os componentes Media serão renderizados. Monte -o na raiz da sua árvore componente:
import React from "react"
import { MediaContextProvider } from "./Media"
export const App = ( ) => {
return < MediaContextProvider > ... </ MediaContextProvider >
} O componente Media criado para o seu aplicativo possui alguns acessórios mutuamente exclusivos que compõem a API que você usará para declarar seus layouts responsivos. Todos esses adereços operam com base nos pontos de interrupção nomeados que foram fornecidos quando você criou os componentes da mídia.
import React from "react"
import { Media } from "./Media"
export const HomePage = ( ) => {
return (
< >
< Media at = "sm" > Hello mobile! </ Media >
< Media greaterThan = "sm" > Hello desktop! </ Media >
</ >
)
}Os exemplos fornecidos para cada suporte usam definições de ponto de interrupção, conforme definido na seção 'Configuração' acima.
Se você deseja evitar a div subjacente que é gerada por <Media> e, em vez disso, use seu próprio elemento, use o formulário Render-Props, mas não deixe de renderizar nenhum filho quando não for necessário:
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: Isso é usado apenas quando a renderização do SSR
Além dos componentes Media e MediaContextProvider , há uma função createMediaStyle que produz o estilo CSS para todas as consultas de mídia possíveis das quais a instância Media pode usar enquanto a marcação está sendo passada do servidor para o cliente durante a hidratação. Se apenas um subconjunto de teclas do ponto de interrupção for usado, elas podem ser opcionais especificadas como um parâmetro para minimizar a saída. Certifique -se de inserir isso dentro de uma tag <style> na <head> do seu documento.
É aconselhável fazer essa configuração em seu próprio módulo para que possa ser facilmente importado em todo o seu aplicativo:
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 A renderização pode ser restringida a pontos de interrupção/interações específicos especificando uma lista de consultas de mídia para corresponder. Por padrão, tudo será renderizado.
Por padrão, quando renderizado, o lado do cliente, a API do matchMedia do navegador será usada para restringir ainda mais a lista onlyMatch a apenas as consultas de mídia atualmente correspondentes. Isso é feito para evitar desencadear ganchos de ciclo de vida relacionados a montagem de componentes ocultos.
Desativar esse comportamento é principalmente destinado a fins de depuração.
Use isso para declarar que as crianças devem ser visíveis apenas em um ponto de interrupção específico, o que significa que a largura da viewport é maior ou igual ao deslocamento de início do ponto de interrupção, mas menor que o próximo ponto de interrupção, se houver.
Por exemplo, os filhos desta declaração Media só serão visíveis se a largura da viewport estiver entre 0 e 768 (768 não incluídos) pontos:
< Media at = "sm" > ... </ Media >A regra CSS correspondente:
@media not all and ( min-width : 0 px ) and ( max-width : 767 px ) {
. fresnel-at-sm {
display : none !important ;
}
}Use isso para declarar que as crianças só devem ser visíveis enquanto a largura da viewport é menor que o deslocamento de inicialização do ponto de interrupção especificado.
Por exemplo, os filhos desta declaração Media só serão visíveis se a largura da viewport estiver entre 0 e 1024 (1024 não incluídos) pontos:
< Media lessThan = "lg" > ... </ Media >A regra CSS correspondente:
@media not all and ( max-width : 1023 px ) {
. fresnel-lessThan-lg {
display : none !important ;
}
}Use isso para declarar que as crianças devem ser visíveis apenas enquanto a largura da viewport é igual ou maior que o deslocamento de inicialização do próximo ponto de interrupção.
Por exemplo, os filhos dessa declaração de Media só serão visíveis se a largura da janela de janela for igual ou superior a 1024 pontos:
< Media greaterThan = "md" > ... </ Media >A regra CSS correspondente:
@media not all and ( min-width : 1024 px ) {
. fresnel-greaterThan-md {
display : none !important ;
}
}Use isso para declarar que as crianças devem ser visíveis apenas enquanto a largura da viewport é igual ao deslocamento de inicialização do ponto de interrupção especificado ou maior.
Por exemplo, os filhos desta declaração Media só serão visíveis se a largura da viewport for 768 pontos ou mais:
< Media greaterThanOrEqual = "md" > ... </ Media >A regra CSS correspondente:
@media not all and ( min-width : 768 px ) {
. fresnel-greaterThanOrEqual-md {
display : none !important ;
}
}Use isso para declarar que as crianças devem ser visíveis apenas enquanto a largura da viewport é igual ao deslocamento de inicialização do primeiro ponto de interrupção especificado, mas menor que o deslocamento inicial do segundo ponto de interrupção especificado.
Por exemplo, os filhos desta declaração Media só serão visíveis se a largura da viewport estiver entre 768 e 1192 (1192 não incluídos) pontos:
< Media between = { [ "md" , "xl" ] } > ... </ Media >A regra CSS correspondente:
@media not all and ( min-width : 768 px ) and ( max-width : 1191 px ) {
. fresnel-between-md-xl {
display : none !important ;
}
}Prós:
Contras:
<Media> em que se encontram. Esse último ponto apresenta um problema interessante. Como podemos representar um componente que é de maneira diferente em diferentes pontos de interrupção? (Vamos imaginar um exemplo 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 } />
}Ainda estamos descobrindo os padrões para isso, por isso, informe -nos se você tiver sugestões.
Este projeto usa liberação automática para lançar automaticamente todos os PR. Cada relações públicas deve ter um rótulo que corresponda a um dos seguintes
Major, Menor e Patch farão com que um novo lançamento seja gerado. Use o Major para quebrar mudanças, Minor para novos recursos não quebrantes e patch para correções de bugs. O Trivial não causará uma liberação e deve ser usado ao atualizar a documentação ou o código sem projeto.
Se você não deseja lançar um PR específico, mas as alterações não são triviais, use a tag Skip Release ao lado da tag de versão apropriada.