在React中,如何检测我的组件是从客户端渲染还是服务器端渲染?

51

我正在构建一个同构应用程序,但我正在使用一个仅在客户端渲染的第三方组件。因此,特别针对此组件,我需要仅在客户端渲染它。

如何检测当前是在客户端还是服务器端?我正在寻找类似于 isClient()isServer() 的东西。


2
你能不能检查一下像 window 或者 process 这样的全局变量? - elclanrs
相似:https://dev59.com/JWYr5IYBdhLWcg3wfKL_#13644360 - zerkms
谢谢@elclanrs和@zerkms。那是我想到的第一件事,但我试图使用if(windows) {},而实际上我应该使用typeof window - Andre Pena
ReactDOM.render( <Component />, document.getElementById('root') , () => console.log('render!)) - Tony Jin
9个回答

51

在内部,React使用一个名为ExecutionEnvironment的实用程序来处理这个问题。它实现了几个有用的属性,如canUseDOMcanUseEventListeners。本质上,解决方案就是这里所建议的。

canUseDOM的实现

var canUseDOM = !!(
  (typeof window !== 'undefined' &&
  window.document && window.document.createElement)
);

我在我的应用程序中像这样使用它

var ExecutionEnvironment = require('react/node_modules/fbjs/lib/ExecutionEnvironment');
...
render() {
  <div>{ ExecutionEnvironment.canUseDOM ? this.renderMyComponent() : null }</div>
}

编辑:这是一个未记录的功能,不应直接使用。它的位置可能会在不同版本中发生变化。我分享这个功能只是为了表明“这是你能做到的最好的”,通过展示Facebook团队内部使用的东西。您可能想将此代码(它很小)复制到自己的项目中,以便不必担心跟随版本更新或潜在的破坏性变更。

另一个编辑:有人为此创建了一个npm包。建议使用该包。

npm install exenv --save

1
只是好奇,为什么在开头加上!!? - Jacopo
3
!! 将值转换为布尔类型,以确保返回的结果是 truefalse 而不是可能的 undefined。您可以使用 Boolean 替换上面的 !!,并获得相同的结果。 - Charlie Martin
由于我们在这里使用了 && 运算符,我看不到任何可能会返回 undefined 或其他值而不是 true 或 false 的路径。我错了吗? - Jacopo
1
如果window.document存在但没有createElement方法,它将返回undefined。如果它有一个createElement方法,它将返回该方法,也就是一个函数。为了测试这个,你可以在你的javascript控制台中复制粘贴它(不包括!!),并查看它是否返回一个函数。 - Charlie Martin
嘿,我在想,我们能否在单元测试中使用类似这样的东西来检查组件是从客户端还是服务器端加载的。 - Fahmi Jabbar
这将告诉您代码当前是否在浏览器中运行。您是否在浏览器中执行单元测试?我怀疑不会。因此,我想在您的单元测试中,这将始终为“false”。 - Charlie Martin

16

您可以使用React的生命周期事件(例如: componentDidMount)来检测服务器/客户端的渲染。

示例

作为钩子

import { useState, useEffect } from 'react'

function useIsServer () {
  const [isServer, setIsServer] = useState(true)
  useEffect(() => {
    setIsServer(false)
  }, [])
  return isServer
}

使用说明

请参见下面(函数组件)

作为函数组件

import useIsServer from './above'

function ServerOnly ({ children = null, onClient = null }) {
  const isServer = useIsServer()
  return isServer
    ? children
    : onClient
}

使用方法

<ServerOnly
  children='This String was rendered on the server'
  onClient='This String was rendered on the client'
/>

作为类组件

class ServerOnly extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      isServer: true
    }
  }

  componentDidMount() {
    this.setState({
      isServer: false
    })
  }

  render () {
    const { isServer } = this.state
    const { children, onClient } = this.props
    return isServer
      ? children
      : onClient
  }
}

使用方法

<ServerOnly
  children='This String was rendered on the server'
  onClient='This String was rendered on the client'
/>

这个解决方案会在客户端产生意想不到的影响。如果你使用它,你应该考虑到这将导致立即重新渲染。 - undefined

11

可能相关的两件事:

许多项目使用一些约定,其中他们设置一个全局的SERVER或CLIENT布尔值,以便您的所有代码都可以根据它进行切换。在您的服务器包中,设置一些全局变量,就像这个项目中一样

global.__SERVER__ = true;

在你的客户端包中,将一些全局客户端设置为true,你可以通过一种方式实现这个目标使用Webpack的DefinePlugin

new webpack.DefinePlugin({
  __CLIENT__: true
})

使用上述方法,您可以在willMount或render中基于该变量进行切换,在服务器端执行一项操作,在客户端执行另一项操作。
第二个可能有帮助的事情是componentDidMount仅在客户端运行,而不在服务器端运行。

你能否请审核一下我的答案?如果有任何错误,请指出来。 - Ramesh Pareek

5

您也可以使用componentDidMount(),因为此生命周期方法不会在页面进行服务器端渲染时运行。


4

你也可以使用use-ssr React 钩子函数

import useSSR from 'use-ssr'

const App = () => {
  var { isBrowser, isServer } = useSSR()

  // Want array destructuring? You can do that too!
  var [isBrowser, isServer] = useSSR()

  /*
   * In your browser's chrome devtools console you should see
   * > IS BROWSER: 
   * > IS SERVER: 
   *
   * AND, in your terminal where your server is running you should see
   * > IS BROWSER: 
   * > IS SERVER: 
   */
  console.log('IS BROWSER: ', isBrowser ? '' : '')
  console.log('IS SERVER: ', isServer ? '' : '')
  return (
    <>
      Is in browser? {isBrowser ? '' : ''}
      <br />
      Is on server? {isServer ? '' : ''}
    </>
  )
}

示例


数组解构对我不起作用。我已经使用了对象解构。 - AntonAL

2
你可以检查全局变量window是否已定义,因为在浏览器中它应该始终被定义。"最初的回答"
var isBrowser = window!==undefined

0
在服务器元素层次结构的最高级别上,可以添加一个 ServerContext ,例如:
class ServerContext extends React.Component {
  getChildContext() { return { isServer: true }; }
  render() { return React.Children.only(this.props.children); }
}

ServerContext.propTypes = {
  children: React.PropTypes.node.isRequired,
};

ServerContext.childContextTypes = {
  isServer: React.PropTypes.bool.isRequired,
};

// Create our React application element.
const reactAppElement = (
  <ServerContext>
    <CodeSplitProvider context={codeSplitContext}>
      <ServerRouter location={request.url} context={reactRouterContext}>
        <DemoApp />
      </ServerRouter>
    </CodeSplitProvider>
  </ServerContext>
);

这样做,就可以通过上下文读取isServer,像这样:
const Layout = (_, { isServer }) => (
  // render stuff here
);

0
if (typeof window === "undefined") { //client side code }

如果没有使用typeof,你将会收到一个错误提示。


0

借助exenv包,您可以创建一个有用的实用程序。

import { canUseDOM } from 'exenv';

export function onClient(fn: (..._args: any[]) => any): (..._args: any[]) => any {
    if (canUseDOM) {
        return fn;
    }

    if (process.env.NODE_ENV === 'development') {
        console.log(`Called ${fn.name} on client side only`);
    }

    return (): void => {};
}

并像这样使用它

function my_function_for_browser_only(arg1: number, arg2: string) {}

onClient(my_function_for_browser_only)(123, "Hi !");

该函数仅在客户端调用,并且如果设置了NODE_ENV=development,它将在服务器端记录该函数已在客户端上调用。

(这是 TypeScript,将类型移除以获得 JS :))


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接