简体   繁体   English

如何使用 Next.js 在 React SSR App 上检测设备?

[英]How to detect the device on React SSR App with Next.js?

on a web application I want to display two different Menu, one for the Mobile, one for the Desktop browser.在 web 应用程序上,我想显示两个不同的菜单,一个用于移动设备,一个用于桌面浏览器。 I use Next.js application with server-side rendering and the library react-device-detect .我将 Next.js 应用程序与服务器端渲染和库react-device-detect一起使用。

Here is the CodeSandox link .这是CodeSandox 链接

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>
);

If you open this in a browser and switch to mobile view and look the console you get this error:如果您在浏览器中打开它并切换到移动视图并查看控制台,您会收到此错误:

Warning: Text content did not match.警告:文本内容不匹配。 Server: " This is rendered only in browser " Client: " This is rendered only on mobile "服务器:“仅在浏览器中呈现”客户端:“仅在移动设备上呈现”

This happen because the rendering by the server detects a browser and on the client, he is a mobile device.发生这种情况是因为服务器的渲染检测到浏览器,而在客户端,他是移动设备。 The only workaround I found is to generate both and use the CSS like this:我发现的唯一解决方法是同时生成并使用 CSS,如下所示:

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

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

Instead of the library but I don't really like this method.而不是图书馆,但我真的不喜欢这种方法。 Does someone know the good practice to handle devices type on an SSR app directly in the react code?有人知道直接在反应代码中处理 SSR 应用程序上的设备类型的良好做法吗?

LATEST UPDATE:最新更新:

So if you don't mind doing it client side you can use the dynamic importing as suggested by a few people below.因此,如果您不介意在客户端执行此操作,则可以按照以下几个人的建议使用动态导入。 This will be for use cases where you use static page generation.这将适用于您使用静态页面生成的用例。

i created a component which passes all the react-device-detect exports as props (it would be wise to filter out only the needed exports because then does not treeshake)我创建了一个组件,它将所有react-device-detect导出作为道具传递(明智的做法是仅过滤掉所需的导出,因为这样就不会进行 treeshake)

// 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

and then when you want to make use of the component you can just do然后当你想使用组件时,你可以这样做

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

Personally I just use a hook to do this, although the initial props method is better.我个人只是使​​用一个钩子来做到这一点,虽然初始 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

I had the problem that scroll animation was annoying on mobile devices so I made a device based enabled scroll animation component;我遇到了滚动动画在移动设备上很烦人的问题,所以我制作了一个基于设备的滚动动画组件;

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}
    />
  )
}

UPDATE: 更新:

so after further going down the rabbit hole, the best solution i came up with is using the react-device-detect in a useEffect, if you further inspect the device detect you will notice that it exports const's that are set via the ua-parser-js lib因此,在进一步深入兔子洞之后,我想出的最佳解决方案是在 useEffect 中使用 react-device-detect,如果您进一步检查设备检测,您会注意到它导出通过ua-parser-js设置的 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);

This results in the initial device being the server which causes false detection.这导致初始设备成为导致错误检测的服务器。

I forked the repo and created and added a ssr-selector which requires you to pass in a user-agent.我分叉了 repo 并创建并添加了一个ssr-selector ,它需要你传入一个用户代理。 which could be done using the initial props这可以使用初始道具完成


UPDATE: 更新:

Because of Ipads not giving a correct or rather well enough defined user-agent, see this issue , I decided to create a hook to better detect the device由于 Ipad 没有提供正确或足够好的定义用户代理,请参阅此问题,我决定创建一个钩子来更好地检测设备

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

Because I require the package each time I call that hook, the UA info is updated, it also fixes to SSR out of sync warnings.因为我每次调用该钩子时都需要该包,所以更新了 UA 信息,它还修复了 SSR 不同步警告。

I think you should do it by using getInitialProps in your page, as it runs both on the server and on the client, and getting the device type by first detecting if you are just getting the request for the webpage (so you are still on the server), or if you are re-rendering (so you are on the client).我认为您应该通过在您的页面中使用 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
  }
}

Now you can use a regex to see if the device is a mobile or a desktop.现在您可以使用正则表达式查看设备是移动设备还是台式机。

// still in getInitialProps

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

return { isMobile }

Now you can access the isMobile prop that will return either true or false现在您可以访问将返回 true 或 false 的 isMobile 道具

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

I got this answer from this article here I hope that was helpful to you我从这篇文章中得到了这个答案,希望对你有帮助

UPDATE更新

Since Next 9.5.0, getInitialProps is going to be replaced by getStaticProps and getServerSideProps .从 Next 9.5.0 开始, getInitialProps将被getStaticPropsgetServerSideProps取代。 While getStaticProps is for fetching static data, which will be used to create an html page at build time, getServerSideProps generates the page dynamically on each request, and receives the context object with the req prop just like getInitialProps .虽然getStaticProps用于获取静态数据,它将用于在构建时创建 html 页面,但getServerSideProps在每个请求上动态生成页面,并像getInitialProps一样接收带有req属性的context对象。 The difference is that getServerSideProps is not going to know navigator , because it is only server side.不同之处在于getServerSideProps不会知道navigator ,因为它只是服务器端。 The usage is also a little bit different, as you have to export an async function, and not declare a method on the component.用法也有点不同,因为您必须导出异步函数,而不是在组件上声明方法。 It would work this way:它会这样工作:

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

Please note that since getServerSideProps and getStaticProps are mutually exclusive, you would need to give up the SSG advantages given by getStaticProps in order to know the device type of the user.请注意,由于getServerSidePropsgetStaticProps是互斥的,因此您需要放弃getStaticProps提供的 SSG 优势才能了解用户的设备类型。 I would suggest not to use getServerSideProps for this purpose if you need just to handle a couple of styiling details.如果您只需要处理一些样式细节,我建议不要为此目的使用 getServerSideProps。 If the structure of the page is much different depending on the device type than maybe it is worth it如果页面结构因设备类型而异,则可能值得

Load only the JS files needed dynamically只加载动态需要的 JS 文件

You can load components dynamically with next/dynamic, and only the appropriate component will be loaded.您可以使用 next/dynamic 动态加载组件,并且只会加载适当的组件。

You can use react-detect-device or is-mobile and in my case.您可以使用 react-detect-device 或 is-mobile ,就我而言。 In this scenario, I created separate layout for mobile and desktop, and load the appropriate component base on device.在这个场景中,我为移动和桌面创建了单独的布局,并根据设备加载了适当的组件。

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

You can view the codesandbox .您可以查看代码和 Only the required component.JS will be loaded.只会加载所需的 component.JS。

Edit:编辑:

How different is the above from conditionally loading component?以上与条件加载组件有何不同? eg例如

isMobile ? <MobileComponent /> : <NonMobileComponent />

The first solution will not load the JS file, while in second solution, both JS files will be loaded.第一个解决方案不会加载 JS 文件,而在第二个解决方案中,两个 JS 文件都会被加载。 So you save one round trip.所以你节省了一次往返。

With current Next.js (v 9.5+) I accomplished that using next/dynamic and react-detect-device .使用当前的 Next.js (v 9.5+),我使用next/dynamicreact-detect-device实现了这一点。

For instance, on my header component:例如,在我的header组件上:

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

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

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

Then on MobileMenuHandler , which is only called on the client:然后在MobileMenuHandler ,它只在客户端上调用:

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

With that, the react-detect-device is only active on the client side and can give a proper reading.这样, react-detect-device仅在客户端处于活动状态,并且可以提供正确的读数。

See Next.js docs .请参阅Next.js 文档

When I was working on one of my next.js projects, I came across a similar situation.当我在我的一个 next.js 项目上工作时,我遇到了类似的情况。 I have got some ideas from the answers.我从答案中得到了一些想法。 And I did solve it with the following approach.我确实用以下方法解决了它。

Firstly, I made custom hook using react-device-detect首先,我使用react-device-detect制作了自定义钩子

//hooks/useDevice.ts
import { isDesktop, isMobile } from 'react-device-detect';

interface DeviceDetection {
  isMobile: boolean;
  isDesktop: boolean;
}

const useDevice = (): DeviceDetection => ({
  isMobile,
  isDesktop
});

export default useDevice;

Secondly, I made a component which uses of custom hook其次,我制作了一个使用自定义钩子的组件

//Device/Device.tsx
import { ReactElement } from 'react';

import useDevice from '@/hooks/useDevice';

export interface DeviceProps {
  desktop?: boolean;
  mobile?: boolean;
  children: ReactElement;
}

export const Device = ({ desktop, mobile, children }: DeviceProps): ReactElement | null => {
  const { isMobile } = useDevice();

  return (isMobile && mobile) || (!isMobile && desktop) ? children : null;
};

Thirdly, I import the component dynamically using next.js next/dynamic第三,我使用 next.js next/dynamic导入组件

//Device/index.tsx
import dynamic from 'next/dynamic';
    
import type { DeviceProps } from './Device';
    
export const Device = dynamic<DeviceProps>(() => import('./Device').then((mod) => mod.Device), {
      ssr: false
    });

Finally, I used it following way in pages.最后,我在页面中按照以下方式使用它。

//pages/my-page.tsx
import { Device } from '@/components/Device';
<Device desktop>
    <my-component>Desktop</my-component>
 </Device>
<Device mobile>
    <my-component>Mobile</my-component>
 </Device>

If you don't mind rendering always desktop version and figuring the logic on the front-end, then the hook logic can be pretty straightforward.如果您不介意始终渲染桌面版本并在前端计算逻辑,那么钩子逻辑可以非常简单。

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
  };
};
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>

This always works.这总是有效的。 (I used this package after trying the above technique and it didn't work for me.) (我在尝试上述技术后使用了这个包,但它对我不起作用。)

The advantage: The component renders server side so there's no flashing on client side when trying to detect user agent.优点:组件呈现服务器端,因此在尝试检测用户代理时客户端不会闪烁。

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

just import the package and create your layout.只需导入包并创建您的布局。

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;

Was able to avoid dynamic importing or component props, by using React state instead.能够通过使用 React state 来避免动态导入或组件道具。 For my use case, I was trying to detect if it was Safari, but this can work for other ones as well.对于我的用例,我试图检测它是否是 Safari,但这也适用于其他的。

Import code导入代码

import { browserName } from 'react-device-detect';

Component code组件代码

const [isSafari, setIsSafari] = useState(false);

useEffect(() => {
  setIsSafari(browserName === 'Safari');
}, [browserName]);

// Then respect the state in the render
return <div data-is-safari={isSafari} />;

I solved a case like this using next-useragent .我使用next-useragent解决了这样的案例。

const mobileBreakpoint = 1280;
/**
 * 
 * @param userAgent - the UserAgent object from `next-useragent`
 */
export const useIsMobile = (userAgent?: UserAgent): boolean => {
  const [isMobile, setIsMobile] = useState(false);
  // Some front-end hook that gets the current breakpoint, but returns undefined, if we don't have a window object.
  const { breakpoint } = useResponsive();
  useEffect(() => {
    if (breakpoint) {
      setIsMobile(breakpoint.start < mobileBreakpoint);
    }
    else if (userAgent) {
      setIsMobile(userAgent.isMobile);
    } else if (!isBrowser) {
      setIsMobile(false);
    }
  }, [userAgent, breakpoint]);
  return isMobile;
};

And the usage of it is:它的用法是:

// Inside react function component.
const isMobile = useIsMobile(props.userAgent);
export const getServerSideProps = (
  context: GetServerSidePropsContext,
): GetServerSidePropsResult<{ userAgent?: UserAgent }> => ({
  // Add the user agent to the props, so we can use it in the window hook.
  props: {
    userAgent: parse(context.req.headers["user-agent"] ?? ""),
  },
});

This hook always returns a boolean isMobile .这个钩子总是返回 boolean isMobile When you run it server-side, it uses the user-agent header to detect a mobile device in the SSR request.当您在服务器端运行它时,它使用用户代理 header 来检测 SSR 请求中的移动设备。 When this gets to client side, it uses the breakpoints (in my case), or any other logic for width detection to update the boolean.当它到达客户端时,它使用断点(在我的例子中)或任何其他用于宽度检测的逻辑来更新 boolean。 You could use next-useragent to also detect the specific device type, but you can't make resolution-based rendering server-side.您也可以使用next-useragent来检测特定的设备类型,但不能在服务器端进行基于分辨率的渲染。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM