【问题标题】:How to detect the device on React SSR App with Next.js?如何使用 Next.js 检测 React SSR App 上的设备?
【发布时间】:2020-04-17 00:39:57
【问题描述】:

在一个网络应用程序上,我想显示两个不同的菜单,一个用于移动设备,一个用于桌面浏览器。 我使用带有服务器端渲染的 Next.js 应用程序和库 react-device-detect

这里是CodeSandox link

import Link from "next/link";
import { BrowserView, MobileView } from "react-device-detect";

export default () => (
  <div>
    Hello World.{" "}
    <Link href="/about">
      <a>About</a>
    </Link>
    <BrowserView>
      <h1> This is rendered only in browser </h1>
    </BrowserView>
    <MobileView>
      <h1> This is rendered only on mobile </h1>
    </MobileView>
  </div>
);

如果您在浏览器中打开它并切换到移动视图并查看控制台,您会收到以下错误:

警告:文本内容不匹配。服务器:“这只是渲染 在浏览器“客户端:”这仅在移动设备上呈现“

发生这种情况是因为服务器的渲染检测到了浏览器,而在客户端,他是移动设备。我发现的唯一解决方法是生成两者并像这样使用 CSS:

.activeOnMobile {
  @media screen and (min-width: 800px) {
    display: none;
  }
}

.activeOnDesktop {
  @media screen and (max-width: 800px) {
    display: none;
  }
}

而不是图书馆,但我不太喜欢这种方法。有人知道直接在反应代码中处理 SSR 应用程序上的设备类型的良好做法吗?

【问题讨论】:

  • 我认为该 atm 没有解决方案,因为在服务器端渲染然后浏览器信息是不可知的,它还与您的 html 响应的缓存时间有关

标签: reactjs next.js server-side-rendering device-detection


【解决方案1】:

我认为您应该通过在您的页面中使用 getInitialProps 来做到这一点,因为它同时在服务器和客户端上运行,并通过首先检测您是否只是收到对网页的请求来获取设备类型(所以您是仍然在服务器上),或者如果您正在重新渲染(所以您在客户端上)。

// index.js

IndexPage.getInitialProps = ({ req }) => {
  let userAgent;
  if (req) { // if you are on the server and you get a 'req' property from your context
    userAgent = req.headers['user-agent'] // get the user-agent from the headers
  } else {
    userAgent = navigator.userAgent // if you are on the client you can access the navigator from the window object
  }
}

现在您可以使用正则表达式来查看设备是移动设备还是桌面设备。

// still in getInitialProps

let isMobile = Boolean(userAgent.match(
  /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
))

return { isMobile }

现在您可以访问将返回 true 或 false 的 isMobile 道具

const IndexPage = ({ isMobile }) => {
  return ( 
    <div>
     {isMobile ? (<h1>I am on mobile!</h1>) : (<h1>I am on desktop! </h1>)} 
    </div>
  )
}

我从this article here 得到了这个答案 希望对你有帮助

更新

自 Next 9.5.0 起,getInitialProps 将被 getStaticPropsgetServerSideProps 取代。 getStaticProps 用于获取静态数据,将用于在构建时创建一个 html 页面,getServerSideProps 在每个请求上动态生成页面,并接收带有 req 属性的 context 对象,就像 @ 987654332@。不同之处在于getServerSideProps 不会知道navigator,因为它只是服务器端。用法也有点不同,因为您必须导出异步函数,而不是在组件上声明方法。它会这样工作:

const HomePage = ({ deviceType }) => {
let componentToRender
  if (deviceType === 'mobile') {
    componentToRender = <MobileComponent />
  } else {
    componentToRender = <DesktopComponent />
  }

  return componentToRender
}

export async function getServerSideProps(context) {
  const UA = context.req.headers['user-agent'];
  const isMobile = Boolean(UA.match(
    /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
  ))
  
  return {
    props: {
      deviceType: isMobile ? 'mobile' : 'desktop'
    }
  }
}


export default HomePage

请注意,由于getServerSidePropsgetStaticProps是互斥的,你需要放弃getStaticProps提供的SSG优势才能知道用户的设备类型。如果您只需要处理几个样式细节,我建议不要为此目的使用 getServerSideProps。如果页面的结构因设备类型而有很大不同,那可能是值得的

【讨论】:

  • 谢谢,我去测试一下
  • 嗨 Dylanbob211,从任何组件获取 isMobile 道具的方法是什么?
  • 好吧,你的页面组件中有这个道具(index.js 或 getInitialProp 函数所在的任何页面组件),所以你需要传递这个道具。您可以使用 prop-drilling 技术(从父级传递到子级的分类道具)、使用状态管理器(redux、mobx 等)或使用 Context API 来完成。问题是你只能在页面组件中获得这个道具,因为你需要使用 getInitialProps
  • 请记住navigator 永远不会在getInitialProps 中定义,getInitialProps 仅在服务器端运行,因此您将始终收到req.headers['user-agent']
  • 您仍然可以使用 _app.js 应用 getInitialProps 方法,然后将其作为道具传递给您的页面。
【解决方案2】:

最新更新:

因此,如果您不介意在客户端执行此操作,则可以使用下面一些人建议的动态导入。这适用于使用静态页面生成的用例。

我创建了一个组件,它将所有 react-device-detect 导出作为道具传递(明智的做法是只过滤掉所需的导出,因为这样就不会摇晃)

// Device/Device.tsx

import { ReactNode } from 'react'
import * as rdd from 'react-device-detect'

interface DeviceProps {
  children: (props: typeof rdd) => ReactNode
}
export default function Device(props: DeviceProps) {
  return <div className="device-layout-component">{props.children(rdd)}</div>
}

// Device/index.ts

import dynamic from 'next/dynamic'

const Device = dynamic(() => import('./Device'), { ssr: false })

export default Device

然后当你想使用该组件时,你可以这样做

const Example = () => {
  return (
    <Device>
      {({ isMobile }) => {
        if (isMobile) return <div>My Mobile View</div>
        return <div>My Desktop View</div>
      }}
    </Device>
  )
}

我个人只是用一个钩子来做这个,虽然初始的 props 方法更好。

import { useEffect } from 'react'

const getMobileDetect = (userAgent: NavigatorID['userAgent']) => {
  const isAndroid = () => Boolean(userAgent.match(/Android/i))
  const isIos = () => Boolean(userAgent.match(/iPhone|iPad|iPod/i))
  const isOpera = () => Boolean(userAgent.match(/Opera Mini/i))
  const isWindows = () => Boolean(userAgent.match(/IEMobile/i))
  const isSSR = () => Boolean(userAgent.match(/SSR/i))
  const isMobile = () => Boolean(isAndroid() || isIos() || isOpera() || isWindows())
  const isDesktop = () => Boolean(!isMobile() && !isSSR())
  return {
    isMobile,
    isDesktop,
    isAndroid,
    isIos,
    isSSR,
  }
}
const useMobileDetect = () => {
  useEffect(() => {}, [])
  const userAgent = typeof navigator === 'undefined' ? 'SSR' : navigator.userAgent
  return getMobileDetect(userAgent)
}

export default useMobileDetect

我遇到了滚动动画在移动设备上很烦人的问题,所以我制作了一个基于设备的启用滚动动画组件;

import React, { ReactNode } from 'react'
import ScrollAnimation, { ScrollAnimationProps } from 'react-animate-on-scroll'
import useMobileDetect from 'src/utils/useMobileDetect'

interface DeviceScrollAnimation extends ScrollAnimationProps {
  device: 'mobile' | 'desktop'
  children: ReactNode
}

export default function DeviceScrollAnimation({ device, animateIn, animateOut, initiallyVisible, ...props }: DeviceScrollAnimation) {
  const currentDevice = useMobileDetect()

  const flag = device === 'mobile' ? currentDevice.isMobile() : device === 'desktop' ? currentDevice.isDesktop() : true

  return (
    <ScrollAnimation
      animateIn={flag ? animateIn : 'none'}
      animateOut={flag ? animateOut : 'none'}
      initiallyVisible={flag ? initiallyVisible : true}
      {...props}
    />
  )
}

更新:

所以在进一步深入兔子洞之后,我想出的最佳解决方案是在 useEffect 中使用 react-device-detect,如果您进一步检查设备检测,您会注意到它导出通过设置的 const ua-parser-js

export const UA = new UAParser();

export const browser = UA.getBrowser();
export const cpu = UA.getCPU();
export const device = UA.getDevice();
export const engine = UA.getEngine();
export const os = UA.getOS();
export const ua = UA.getUA();
export const setUA = (uaStr) => UA.setUA(uaStr);

这导致初始设备是导致错误检测的服务器。

我分叉了 repo 并创建并添加了一个ssr-selector,它要求你传入一个用户代理。这可以使用初始道具来完成


更新:

由于 Ipad 没有提供正确或定义得足够好的用户代理,请参阅issue,我决定创建一个挂钩来更好地检测设备

import { useEffect, useState } from 'react'

function isTouchDevice() {
  if (typeof window === 'undefined') return false
  const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
  function mq(query) {
    return typeof window !== 'undefined' && window.matchMedia(query).matches
  }
  // @ts-ignore
  if ('ontouchstart' in window || (window?.DocumentTouch && document instanceof DocumentTouch)) return true
  const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('') // include the 'heartz' - https://git.io/vznFH
  return mq(query)
}

export default function useIsTouchDevice() {
  const [isTouch, setIsTouch] = useState(false)
  useEffect(() => {
    const { isAndroid, isIPad13, isIPhone13, isWinPhone, isMobileSafari, isTablet } = require('react-device-detect')
    setIsTouch(isTouch || isAndroid || isIPad13 || isIPhone13 || isWinPhone || isMobileSafari || isTablet || isTouchDevice())
  }, [])

  return isTouch

因为我每次调用该钩子时都需要该包,所以更新了 UA 信息,它还修复了 SSR 不同步警告。

【讨论】:

  • 我觉得应该改成if ('ontouchstart' in window || (window?.Document &amp;&amp; document instanceof Document)) return true;。不是100%肯定,但是TS抱怨,还有this
  • 啊,我明白了,这打破了包装。我暂时将interface Window { DocumentTouch: any; } 添加到我的global.d.ts。这暂时有效:if ('ontouchstart' in window || (window?.DocumentTouch &amp;&amp; document instanceof Document)) return true;
【解决方案3】:

对于当前的 Next.js (v 9.5+),我使用 next/dynamicreact-detect-device 实现了这一点。

例如,在我的header 组件上:

...
import dynamic from 'next/dynamic';
...

const MobileMenuHandler = dynamic(() => import('./mobileMenuHandler'), {
 ssr: false,
});

return (
...
    <MobileMenuHandler
        isMobileMenuOpen={isMobileMenuOpen}
        setIsMobileMenuOpen={setIsMobileMenuOpen}
    />
)
...

然后在MobileMenuHandler,只在客户端调用:

import { isMobile } from 'react-device-detect';
...
return(
   {isMobile && !isMobileMenuOpen ? (
       <Menu
          onClick={() => setIsMobileMenuOpen(true)}
          className={classes.menuIcon}
       />
   ) : null}
)

这样,react-detect-device 仅在客户端处于活动状态,并且可以给出正确的读数。

Next.js docs

【讨论】:

    【解决方案4】:

    只动态加载需要的JS文件

    你可以用next/dynamic动态加载组件,只会加载合适的组件。

    在我的情况下,您可以使用 react-detect-device 或 is-mobile。在这种情况下,我为移动和桌面创建了单独的布局,并根据设备加载了相应的组件。

    import dynamic from 'next/dynamic';
    const mobile = require('is-mobile');
    
    const ShowMobile = dynamic(() => mobile() ? import('./ShowMobile.mobile') : import('./ShowMobile'), { ssr: false })
    
    
    const TestPage = () => {
    
       return <ShowMobile />
    }
    
    export default TestPage
    

    您可以查看codesandbox 。只会加载所需的 component.JS。

    编辑:

    以上内容与条件加载组件有何不同?例如

    isMobile ? <MobileComponent /> : <NonMobileComponent />
    

    第一个解决方案不会加载JS文件,而在第二个解决方案中,两个JS文件都会被加载。这样您就可以节省一次往返行程。

    【讨论】:

      【解决方案5】:

      如果您不介意总是渲染桌面版本并在前端计算逻辑,那么挂钩逻辑可以非常简单。

      export const useDevice = () => {
        const [firstLoad, setFirstLoad] = React.useState(true);
        React.useEffect(() => { setFirstLoad(false); }, []);
      
        const ssr = firstLoad || typeof navigator === "undefined";
      
        const isAndroid = !ssr && /android/i.test(navigator.userAgent);
        const isIos = !ssr && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
      
        return {
          isAndroid,
          isIos,
          isDesktop: !isAndroid && !isIos
        };
      };
      

      【讨论】:

        【解决方案6】:
        import React, { useState, useEffect }
        import { isMobile } from 'react-device-detect'
        
        ...
        
        
        const [_isMobile, setMobile] = useState();
        
            useEffect(() => {
                setMobile(isMobile);
            }, [setMobile]);
        
        <div hidden={_isMobile}> Desktop View</div>
        
        <div hidden={!_isMobile}> MobileView </div>
        

        【讨论】:

          【解决方案7】:

          这总是有效的。 (我在尝试了上述技术后使用了这个包,但它对我不起作用。)

          优点:组件呈现服务器端,因此客户端在尝试检测用户代理时不会闪烁。

          import { isMobile } from "mobile-device-detect";
          

          只需导入包并创建您的布局。

          import { isMobile } from "mobile-device-detect";
          
          const Desktop = () => {
            return (
              <>
                desktop
              </>
            );
          };
          
          Desktop.layout = Layout;
          
          const Mobile = () => {
            return (
              <>
                mobile
              </>
            );
          };
          
          Mobile.layout = LayoutMobile;
          
          const Page = isMobile ? Desktop : Mobile;
          
          export default Page;
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-09-20
            • 2019-10-18
            • 2019-03-29
            • 1970-01-01
            • 1970-01-01
            • 2021-01-12
            • 2022-06-12
            • 1970-01-01
            相关资源
            最近更新 更多