在本文中,我们将了解编写自适应组件的复杂性,讨论代码拆分,考虑几种组织代码结构的方法,评估它们的优缺点,并尝试选择最佳的方法(但这并不准确)。首先,让我们处理术语。我们经常听到自适应和响应式的术语。他们的意思是什么?有什么区别?这与我们的组件有什么关系?自适应(自适应)是为特定屏幕尺寸创建的多种视觉界面。响应式(Responsive)是适合任何屏幕尺寸的单个界面。而且,当界面分解成小片段时,自适应和响应之间的差异变得越来越模糊,直到完全消失为止。在开发布局时,我们的设计师和开发人员通常不共享这些概念,而是将自适应和响应逻辑结合在一起。此外,我将包含自适应和响应逻辑的组件简称为adaptive。首先,因为我更喜欢这个词,而不是“响应”。其次,我发现它更为普遍。我将专注于显示界面的两个领域-移动和桌面。通过移动显示,我们指的是宽度,例如,≤991像素(数字本身并不重要,它只是一个常数,取决于您的设计系统和应用程序),并且在桌面显示下-宽度大于所选阈值。我会故意错过平板电脑和宽屏显示器的显示屏,因为,首先,并不是每个人都需要它们,其次,这样放置起来会更容易。但是,我们将要讨论的模式对于任何数量的“映射”均等地扩展。另外,我几乎不会谈论CSS,主要是我们将谈论组件逻辑。前端@youla
我将简短地讨论在Yulia中的堆栈,以便清楚地了解在什么条件下创建组件。我们使用React / Redux,我们在monorep中工作,我们使用Typescript,并且在样式组件上编写CSS。作为示例,让我们看一下三个包(monoreps概念中的包是相互连接的NPM包,可以是单独的应用程序,库,实用程序或组件-您可以自行选择分解程度)。我们将看两个应用程序和一个UI库。@ youla / ui-组件库。它们不仅被我们使用,而且还需要“ Yulian”界面的其他团队使用。该库有很多东西,从按钮和输入字段开始,到以标题或授权表单(更确切地说,是其UI部分)结尾。我们将此库视为应用程序的外部依赖项。@ youla-web / app-classified-负责目录/产品/授权部分的应用程序。根据业务需求,此处的所有接口都应是自适应的。@ youla-web / app-b2b是负责专业用户个人帐户部分的应用程序。此应用程序的界面专用于桌面。此外,我们将考虑使用这些程序包的示例编写自适应组件。但首先您需要处理isMobile
。移动性定义为isMobile && <Component />
import React from 'react'
const App = (props) => {
const { isMobile } = props
return (
<Layout>
{isMobile && <HeaderMobile />}
<Content />
<Footer />
</Layout>
)
}
在开始编写自适应组件之前,您需要学习如何定义“移动性”。有许多方法可以实现移动性的定义。我想谈谈一些要点。通过屏幕宽度和用户代理确定移动性
你们大多数人都知道如何实现这两种选择,但是让我们再次简要介绍一下要点。在使用屏幕宽度时,通常会设置边界点,在此之后,应用程序应表现为移动设备或桌面设备。步骤如下:- 创建带有边界点的常量并将其保存在主题中(如果CSS解决方案允许)。价值观可能是您的设计师认为最适合您的UI系统的价值观。
- 我们将当前屏幕尺寸保存在redux / mobx /上下文/任何数据源中。无论何时何地,只有组件(最好是应用程序逻辑)都可以访问此数据。
- 我们订阅了resize事件,并将屏幕宽度的值更新为将触发组件树更新链的宽度。
- 我们创建了简单的辅助函数,这些函数使用屏幕宽度和常量来计算当前状态(
isMobile
,isDesktop
)。
这是实现此工作模型的伪代码:const breakpoints = {
mobile: 991
}
export const state = {
ui: {
width: null
}
}
const handleSubscribe = () => {
state.ui.width = window.innerWidth
}
export const onSubscribe = () => {
window.addEventListener('resize', handleSubscribe)
}
export const offSubscribe = () =>
window.removeEventListener('resize', handleSubscribe)
export const getIsMobile = (state: any) => {
if (state.ui.width <= breakpoints.mobile) {
return true
}
return false
}
export const getIsDesktop = (state) => !getIsMobile(state)
export const App = () => {
React.useEffect(() => {
onSubscribe()
return () => offSubscribe()
}, [])
return <MyComponentMounted />
}
const MyComponent = (props) => {
const { isMobile } = props
return isMobile ? <MobileComponent /> : <DesktopComponent />
}
export const MyComponentMounted = anyHocToConnectComponentWithState(
(state) => ({
isMobile: getIsMobile(state)
})
)(MyComponent)
屏幕更改时,props
组件的in值将被更新,并将正确重绘。有许多实现此功能的库。对于某人来说,使用现成的解决方案(例如react-media,react-active等)会更加方便,而对于某人来说,编写自己的解决方案会更容易。与屏幕尺寸不同,user-agent
它不能在应用程序运行时动态更改(严格来说,可能是通过开发人员的工具,但这不是用户情况)。在这种情况下,我们不需要在存储值和重新计数时使用复杂的逻辑,只需将字符串解析一次window.navigator.userAgent,
即可保存值,就可以完成。有很多库可以帮助您解决此问题,例如,移动检测,react-device-detect等。该方法比较user-agent
简单,但是仅使用它是不够的。任何认真开发自适应界面的人都知道iPad和类似设备的“魔力转折”,它在垂直位置属于移动设备的定义,而在水平位置(台式机)则属于user-agent
移动设备,但同时具有移动设备。还要注意的是,在完全自适应/响应的应用程序中,仅user-agent
当用户使用例如台式机浏览器但将窗口压缩为“移动”大小时,才可能基于有关该信息的移动性来确定移动性。另外,不要忽略有关的信息user-agent
。通常,您可以在代码中找到以下常量isSafari
,isIE
等处理这些设备和浏览器的“功能”。最好将两种方法结合起来。在我们的代码库中,我们使用一个常量isCheesySafari
,顾名思义,该常量定义user-agent
了Safari浏览器系列中的成员身份。但是除此之外,我们还有一个常量isSuperCheesySafari
,它表示对应于iOS版本11的移动Safari,该版本因许多错误而出名,例如:https : //hackernoon.com/how-to-fix-the-ios-11-input-element在固定模态错误aaf66c7ba3f8中。export const isMobileUA = (() => magicParser(window.navigator.userAgent))()
import isMobileUA from './isMobileUA'
const MyComponent = (props) => {
const { isMobile } = props
return (isMobile || isMobileUA) ? <MobileComponent /> : <DesktopComponent />
}
媒体查询呢?是的,的确,CSS具有用于适应性的内置工具:媒体查询及其类似方法window.matchMedia
。可以使用它们,但是仍然必须实现在调整大小时“更新”组件的逻辑。尽管对于我个人而言,使用媒体查询的语法而不是JS中针对应用程序逻辑和组件的常规比较操作是可疑的优势。组件结构的组织
我们已经确定了移动性的定义,现在让我们思考一下所获得数据的使用以及组件代码结构的组织。通常,在我们的代码中,存在两种组件。第一种是在手机下方或桌面下方锐化的组件。在此类组件中,名称通常包含“移动/桌面”一词,这清楚地表明该组件属于其中一种类型。作为这样的组件的一个例子可以被认为是<MobileList />
从@youla/ui
。import { Panel, Cell, Content, afterBorder } from './styled'
import Group from './Group'
import Button, { IMobileListButtonProps } from './Button'
import ContentOrButton, { IMobileListContentOrButton } from './ContentOrButton'
import Action, { IMobileListActionProps } from './Action'
export default { Panel, Group, Cell, Content, Button, ContentOrButton, Action }
export {
afterBorder,
IMobileListButtonProps,
IMobileListContentOrButton,
IMobileListActionProps
}
除了非常冗长的导出外,此组件是一个包含数据,分隔符,按块分组等的列表。我们的设计师非常喜欢此组件,并且可以在Ula界面中随处使用它。例如,在产品页面上的说明中或在我们新的收费功能中:
在站点周围的N个地方。我们也有一个类似的组件<DesktopList />
,该组件实现了用于桌面分辨率的此列表功能。第二种类型的组件包含桌面和移动设备的逻辑。让我们看一下组件渲染的简化版本,该组件位于@ youla / app-classified中。<HeaderBoard />
我们自己发现,将一个组件的所有样式化组件s放在一个文件中并将其导入到命名空间S下以将代码与其他组件分开是非常方便的import * as S from ‘./styled’
。因此,``S''是一个对象,其键是样式化组件的名称,而值是组件本身。 return (
<HeaderWrapper>
<Logo />
{isMobile && <S.Arrow />}
<S.Wraper isMobile={isMobile}>
<Video src={bgVideo} />
{!isMobile && <Header>{headerContent}</Header>}
<S.WaveWrapper />
</S.Wraper>
{isMobile && <S.MobileHeader>{headerContent}</S.MobileHeader>}
<Info link={link} />
<PaintingInfo isMobile={isMobile} />
{isMobile ? <CardsMobile /> : <CardsDesktop />}
{isMobile ? <UserNavigation /> : <UserInfoModal />}
</HeaderWrapper>
)
在这里isMobile
,它是组件的依赖关系,组件本身将根据该依赖关系决定要呈现的接口。为了更方便地进行缩放,我们经常在代码的重用部分中使用控件反转模式,但请注意不要使不必要的逻辑过载顶级抽象。现在,让我们从“ Yulian”组件中抽象一下自己,并仔细看一下这两个组件:<ComponentA />
-严格隔离桌面和移动逻辑。<ComponentB />
-结合。
<ComponentA />与<ComponentB />
文件夹结构和根index.ts文件:./ComponentA
- ComponentA.tsx
- ComponentADesktop.tsx
- ComponentAMobile.tsx
- index.ts
- styled.desktop.ts
- styled.mobile.ts
import ComponentA from './ComponentA'
import ComponentAMobile from './ComponentAMobile'
import ComponentADesktop from './ComponentADesktop'
export default {
ComponentACombined: ComponentA,
ComponentAMobile,
ComponentADesktop
}
多亏了新技术摇摇树的webpack(或使用任何其他收集器),即使通过根文件重新导出,您也可以丢弃未使用的模块(ComponentADesktop
,ComponentACombined
):import ComponentA from ‘@youla/ui’
<ComponentA.ComponentAMobile />
只有./ComponentAMobile文件代码进入最终捆绑包。该组件<ComponentA />
包含异步导入,该异步导入针对特定情况使用该组件的React.Lazy
特定版本<ComponentAMobile /> || <ComponentADesktop />
。在Yule,我们尝试遵循通过索引文件进入组件的单个入口点的模式。这使得查找和重构组件更加容易。如果未通过根文件重新导出组件的内容,则可以安全地对其进行编辑,因为我们知道它不在该组件的上下文之外使用。好吧,打字稿将在紧要关头对冲。具有组件的文件夹具有其自己的“接口”:根文件中模块级别的导出,并且不公开其实现细节。因此,在重构时,您不必担心保存接口。import React from 'react'
const ComponentADesktopLazy = React.lazy(() => import('./ComponentADesktop'))
const ComponentAMobileLazy = React.lazy(() => import('./ComponentAMobile'))
const ComponentA = (props) => {
const { isMobile } = props
return (
<React.Suspense fallback={props.fallback}>
{isMobile ? (
<ComponentAMobileLazy {...props} />
) : (
<ComponentADesktopLazy {...props} />
)}
</React.Suspense>
)
}
export default ComponentA
此外,该组件<ComponentADesktop />
包含桌面组件的导入:import React from 'react'
import { DesktopList, UserAuthDesktop, UserInfo } from '@youla/ui'
import Banner from '../Banner'
import * as S from './styled.desktop'
const ComponentADesktop = (props) => {
const { user, items } = props
return (
<S.Wrapper>
<S.Main>
<Banner />
<DesktopList items={items} />
</S.Main>
<S.SideBar>
<UserAuthDesktop user={user} />
<UserInfo user={user} />
</S.SideBar>
</S.Wrapper>
)
}
export default ComponentADesktop
组件<ComponentAMobile />
包含移动组件的导入:import React from 'react'
import { MobileList, MobileTabs, UserAuthMobile } from '@youla/ui'
import * as S from './styled.mobile'
const ComponentAMobile = (props) => {
const { user, items, tabs } = props
return (
<S.Wrapper>
<S.Main>
<UserAuthMobile user={user} />
<MobileList items={items} />
<MobileTabs tabs={tabs} />
</S.Main>
</S.Wrapper>
)
}
export default ComponentAMobile
该组件是<ComponentA />
自适应的:通过标记,它isMobile
可以决定要绘制的版本,只能异步下载所需的文件,也就是说,移动版本和桌面版本可以分别使用。现在让我们看一下组件<ComponentB />
。在其中,我们不会深入分解移动和桌面逻辑,而是将所有条件保留在一个功能的框架内。同样,我们不会分离样式的组成部分。这是文件夹结构。根index.ts文件只是重新导出./ComponentB
:./ComponentB
- ComponentB.tsx
- index.ts
- styled.ts
export { default } from './ComponentB'
具有组件本身的./ComponentB文件:
import React from 'react'
import {
DesktopList,
UserAuthDesktop,
UserInfo,
MobileList,
MobileTabs,
UserAuthMobile
} from '@youla/ui'
import * as S from './styled'
const ComponentB = (props) => {
const { user, items, tabs, isMobile } = props
if (isMobile) {
return (
<S.Wrapper isMobile={isMobile}>
<S.Main isMobile={isMobile}>
<UserAuthMobile user={user} />
<MobileList items={items} />
<MobileTabs tabs={tabs} />
</S.Main>
</S.Wrapper>
)
}
return (
<S.Wrapper>
<S.Main>
<Banner />
<DesktopList items={items} />
</S.Main>
<S.SideBar>
<UserAuthDesktop user={user} />
<UserInfo user={user} />
</S.SideBar>
</S.Wrapper>
)
}
export default ComponentB
让我们尝试估计这些组件的优缺点。
总共三个优点和缺点的论点从他们的每一个中脱颖而出。是的,我注意到在优点和缺点中都立即提到了一些标准:这是有目的的,每个人都会将它们从错误的组中删除。我们在@youla的经验
我们在组件库@ youla / ui中尝试不要将桌面组件和移动组件混合在一起,因为这是许多程序包和其他程序包的外部依赖项。这些组件的生命周期尽可能长,我想让它们尽可能的轻薄。有两点需要注意。首先,汇编的JS文件越小,将其交付给用户的速度就越快,这是显而易见的,众所周知。但是,此特性仅对于文件的首次下载很重要,在重复访问期间,将从高速缓存中交付文件,并且不会出现代码交付问题。在这里,我们继续讲第二点,这可能很快成为或已经成为大型Web应用程序的主要问题。许多人已经猜到了:是的,我们正在谈论解析的持续时间。像V8这样的现代引擎可以缓存和解析结果,但是到目前为止,它并不是很有效。埃迪·奥斯曼尼(Eddie Osmani)对此主题有一篇很棒的文章:https : //v8.dev/blog/cost-of-javascript-2019 。您还可以订阅V8博客:https : //twitter.com/v8js。我们将大大缩短解析时间,这对于处理器较弱的移动设备而言尤其重要。在应用程序包@ youla-web / app-中,开发更加“面向业务”。并且出于速度/简单/个人喜好的考虑,选择了在这种情况下开发人员自己认为最正确的决定。通常,在开发小型MVP功能时,最好先编写一个更简单,更快的版本(<ComponentB />),因为在这样的组件中,行数只有一半。而且,正如我们所知,更多的代码-更多的错误。在检查了功能的相关性之后,如有必要,可以使用更优化和更具生产性的版本<ComponentA />替换组件。我还建议您看一下该组件。如果移动版本和桌面版本的UI截然不同,则可能应该将它们分开,并将一些通用逻辑放在一个地方。这将使您摆脱编写复杂CSS时的痛苦,在重构或更改另一个CSS时,其中一个显示出现错误的问题。反之亦然,如果UI越近越好,那为什么还要做额外的工作呢?结论
总结一下。我们了解了自适应/响应界面的术语,研究了确定移动性的几种方法以及组织自适应组件代码结构的几种选择,并确定了每种组件的优缺点。当然,您已经知道很多上述内容,但是重复是巩固的最佳方法。希望您自己学到了一些新知识。下次,我们将发布一组有关编写渐进式Web应用程序的建议,其中包括有关组织,重用和维护代码的技巧。