菲涅尔方程描述了在不同光学介质之间界面上的光的反射。
- https://en.wikipedia.org/wiki/fresnel_equations
# React 18+
yarn add @artsy/fresnel
# React 17
yarn add @artsy/fresnel@6目录
在编写响应式组件时,通常使用媒体查询在满足某些条件时调整显示。从历史上看,这直接在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 " />通过进入断点定义, @artsy/fresnel采用了这种声明的方法,并将其带入了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" ) ) 要注意的第一个重要的事情是,当使用@artsy/fresnel进行服务器渲染时,所有断点都会由服务器渲染。每个Media组件都由普通CSS包裹,只有与用户的当前浏览器大小匹配时,才会显示该断点。这意味着客户端可以在收到标记时准确地开始渲染HTML/CSS,这是在React应用程序启动之前很长时间。这改善了最终用户的感知性能。
为什么不只是渲染当前设备需要的呢?我们无法准确确定您在服务器上设备需求的断点。我们可以使用库来嗅探浏览器用户代理,但是这些并不总是准确的,并且在服务器渲染时,它们不会为我们提供所有我们需要知道的信息。一旦客户端的JS靴子并附加了反应,它就可以通过matchMedia调用在DOM上洗涤并删除不需要的标记即可。
首先,在可以通过应用程序共享的Media文件中配置@artsy/fresnel :
// 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创建一个新的App文件,这将是我们应用程序的启动点:
// 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 >
)
}在客户端上安装<App /> :
// client.tsx
import React from "react"
import ReactDOM from "react-dom"
import { App } from "./App"
ReactDOM . render ( < App /> , document . getElementById ( "react" ) )然后在服务器上,设置SSR渲染,然后将mediaStyle传递到标题中的<style>标签中:
// 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" )
} )就是这样!要测试,禁用JS并将浏览器窗口缩小到移动尺寸并重新加载;它将正确地渲染移动布局,而无需使用用户代理或其他服务器端“提示”。
@artsy/fresnel与Gatsby或Next.js的静态混合渲染方法非常有用。有关简单的实现,请参见下面的示例。
/examples文件夹中有四个示例可以探索:
虽然Basic和SSR示例将相当远, @artsy/fresnel可以做更多的事情。要深入研究其功能,请查看厨房水槽应用程序。
如果您使用的是Gatsby,也可以尝试使用Gatsby-Plugin-Fresnel,以方便配置。
其他现有的解决方案采用有条件渲染的方法,例如react-responsive或react-media ,那么这种方法在哪里有所不同?
服务器端渲染!
但是首先,有条件的渲染是什么?
在React生态系统中,编写声明性响应组件的一种常见方法是使用浏览器的matchMedia API:
< Responsive >
{ ( { sm } ) => {
if ( sm ) {
return < MobileApp />
} else {
return < DesktopApp />
}
} }
</ Responsive >在客户端上,当给定的断点匹配时,有条件地呈现一棵树。
但是,这种方法对我们想要实现的服务器端渲染设置有一些局限性:
由于需要浏览器,因此不可能可靠地知道用户在服务器渲染阶段的当前断点。
基于用户代理的嗅探设置断点大小,由于无法将设备功能与大小匹配,因此容易出现错误。一个移动设备的像素密度可能比另一个设备更大,在考虑设备方向时,移动设备可能适合多个断点,并且在桌面客户端上根本无法知道。最好的开发人员可以做到的是猜测当前的断点,并以假定状态填充<Responsive>式>。
Artsy确定了我们认为是最好的权衡。我们以以下方式解决此问题:
为服务器上的所有断点渲染标记并将其发送到电线。
浏览器以适当的媒体查询样式接收标记,并将立即开始对浏览器所处的视口宽度的预期视觉结果呈现。
当所有JS都加载并启动React启动补液阶段时,我们会查询浏览器当前的断点,然后将渲染组件限制在匹配的媒体查询中。这样可以防止生命周期方法启动隐藏的组件,而未使用的HTML被重新编写为DOM。
此外,我们在浏览器中注册事件侦听器,以在匹配其他断点时通知MediaContextProvider ,然后使用onlyMatch使用新值重新渲染树。
让我们比较使用matchMedia的组件树与我们的方法的样子:
| 前 | 后 |
|---|---|
< Responsive >
{ ( { sm } ) => {
if ( sm ) return < SmallArticleItem { ... props } />
else return < LargeArticleItem { ... props } />
} }
</ Responsive > | < >
< Media at = "sm" >
< SmallArticleItem { ... props } />
</ Media >
< Media greaterThan = "sm" >
< LargeArticleItem { ... props } />
</ Media >
</ > |
有关工作示例,请参见服务器端渲染应用。
第一件事首先。您需要定义设计所需的断点和交互,以生成在整个应用程序中可以使用的媒体组件集。
例如,考虑一个具有以下断点的应用程序:
sm 。md 。lg 。xl 。以及以下互动:
hover 。notHover 。然后,您将生成类似的媒体组件集:
// 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 } = ExampleAppMedia如您所见,断点是由它们的起始偏移定义的,其中第一个偏移量将在0开始。
MediaContextProvider组件会影响Media组件的渲染方式。将其安装在组件树的根部:
import React from "react"
import { MediaContextProvider } from "./Media"
export const App = ( ) => {
return < MediaContextProvider > ... </ MediaContextProvider >
}为您的应用程序创建的Media组件有一些相互排斥的道具,可以构成您用来声明响应式布局的API。这些道具均基于创建媒体组件时提供的命名断点。
import React from "react"
import { Media } from "./Media"
export const HomePage = ( ) => {
return (
< >
< Media at = "sm" > Hello mobile! </ Media >
< Media greaterThan = "sm" > Hello desktop! </ Media >
</ >
)
}在上述“设置”部分中定义的每个Prop使用断点定义给出的示例。
如果您想避免由<Media>生成的基础DIV并使用您自己的元素,请使用渲染 - 绘画表格,但请确保在不需要时不要渲染任何孩子:
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 >
</ >
)
} 注意:仅在SSR渲染时才使用
除了Media和MediaContextProvider组件外,还有一个createMediaStyle功能,可为所有可能的媒体查询生成CSS造型,而在水合过程中, Media实例可以利用媒体实例可以利用这些查询。如果仅使用断点键的子集,则可以将其指定为最小化输出的参数。确保将其插入文档的<head>中的<style>标签中。
建议在自己的模块中进行此设置,以便在您的整个应用程序中都可以轻松导入:
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 通过指定要匹配的媒体查询列表,可以将渲染限制为特定的断点/交互。默认情况下,所有内容都将被渲染。
默认情况下,当渲染客户端时,将使用浏览器的matchMedia API将onlyMatch列表进一步限制为当前匹配的媒体查询。这样做是为了避免触发隐藏组件的相关生命周期钩。
禁用此行为主要用于调试目的。
使用它来声明只有在特定断点上可见孩子,这意味着视口宽度大于或等于断点的起始偏移,但如果存在,则小于下一个断点。
例如,此Media声明的孩子只有在观察口宽度在0到768(不包括768个)点时才可见:
< Media at = "sm" > ... </ Media >相应的CSS规则:
@media not all and ( min-width : 0 px ) and ( max-width : 767 px ) {
. fresnel-at-sm {
display : none !important ;
}
}用它来宣布只有在视口宽度小于指定断点的开始偏移时才能看到儿童。
例如,只有当视口宽度在0到1024(不包括1024)点时,该Media声明的儿童才能看到:
< Media lessThan = "lg" > ... </ Media >相应的CSS规则:
@media not all and ( max-width : 1023 px ) {
. fresnel-lessThan-lg {
display : none !important ;
}
}使用它来声明只有在视口宽度等于或大于下一个断点的开始偏移时才能看到儿童。
例如,只有在视口宽度等于或大于1024分的情况下,该Media声明的儿童才能看到:
< Media greaterThan = "md" > ... </ Media >相应的CSS规则:
@media not all and ( min-width : 1024 px ) {
. fresnel-greaterThan-md {
display : none !important ;
}
}使用它来声明只有在视口宽度等于指定断点或更大的开始偏移时才能看到儿童。
例如,此Media声明的孩子只有在视口宽度为768点或UP时才能看到:
< Media greaterThanOrEqual = "md" > ... </ Media >相应的CSS规则:
@media not all and ( min-width : 768 px ) {
. fresnel-greaterThanOrEqual-md {
display : none !important ;
}
}用它来声明只有在视口宽度等于第一个指定断点的开始偏移时才能看到儿童,但小于第二指定断点的开始偏移。
例如,此Media声明的儿童只有在观察口宽度在768至1192(不包括1192)点时才可见:
< Media between = { [ "md" , "xl" ] } > ... </ Media >相应的CSS规则:
@media not all and ( min-width : 768 px ) and ( max-width : 1191 px ) {
. fresnel-between-md-xl {
display : none !important ;
}
}优点:
缺点:
<Media>组件的道具。最后一点提出了一个有趣的问题。我们如何代表在不同断点上使用不同样式的组件? (让我们想象一个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 } />
}我们仍在弄清楚这模式,因此,如果您有建议,请告诉我们。
该项目使用自动释放来自动在每个PR上释放。每个公关都应具有与以下一个相匹配的标签
专业,次要和补丁将导致生成新的版本。使用专业进行破坏变化,用于新的非破坏功能的次要变化以及用于错误修复的补丁。 Trivial不会引起释放,并且在更新文档或非项目代码时应使用。
如果您不想在特定的PR上释放,但更改并不是微不足道,请在适当版本标签的侧面使用Skip Release标签。