MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 服务端渲染中的条件逻辑处理

2021-03-312.1k 阅读

React 服务端渲染基础回顾

在深入探讨 React 服务端渲染(SSR)中的条件逻辑处理之前,我们先来简要回顾一下 React SSR 的基本概念。React SSR 允许我们在服务器端生成 HTML 页面,然后将其发送到客户端。这样做的好处是,页面在加载时就已经包含了完整的 DOM 结构,无需等待 JavaScript 加载和执行后再生成,从而大大提高了首屏加载速度。

例如,我们有一个简单的 React 应用,使用 create - react - app 创建:

// src/App.js
import React from 'react';

function App() {
  return (
    <div>
      <h1>My React App</h1>
    </div>
  );
}

export default App;

然后我们可以使用 react - dom/server 中的 renderToString 方法在服务器端渲染这个组件:

// server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react - dom/server');
const App = require('./src/App');

const app = express();

app.get('*', (req, res) => {
  const html = ReactDOMServer.renderToString(<App />);
  const page = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>My SSR App</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/client - bundle.js"></script>
      </body>
    </html>
  `;
  res.send(page);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

这是一个非常基础的 React SSR 示例,在实际应用中,我们往往需要处理更复杂的业务逻辑,其中就包括条件逻辑。

服务端渲染中条件逻辑的重要性

在 React SSR 中,条件逻辑处理尤为重要。因为服务器端渲染的 HTML 需要尽可能准确地反映应用在不同条件下的状态,这样才能避免客户端和服务器端渲染结果不一致的问题(即所谓的“水合(hydration)”问题)。

例如,我们的应用可能需要根据用户的登录状态显示不同的 UI。如果在服务器端渲染时不能正确处理这种条件逻辑,当用户首次加载页面时,看到的可能是未登录状态的 UI,而当 JavaScript 加载并执行后,UI 可能会突然切换到登录状态,这会给用户带来不好的体验。

简单条件渲染

在 React 中,最常见的条件渲染方式就是使用 JavaScript 的 if 语句或者 && 逻辑运算符。在 SSR 环境中,这些方法同样适用。

假设我们有一个组件,需要根据一个布尔值来显示不同的内容:

import React from'react';

function ConditionalComponent() {
  const isLoggedIn = true;
  return (
    <div>
      {isLoggedIn && <p>Welcome, user!</p>}
      {!isLoggedIn && <p>Please log in.</p>}
    </div>
  );
}

export default ConditionalComponent;

在服务器端渲染时,这段代码会根据 isLoggedIn 的值正确生成相应的 HTML。如果 isLoggedIntrue,服务器端生成的 HTML 会包含 <p>Welcome, user!</p>,否则会包含 <p>Please log in.</p>

使用 if - else 进行复杂条件渲染

当条件逻辑变得更加复杂时,使用 if - else 语句会更加清晰。

比如,我们有一个根据用户角色显示不同导航栏的组件:

import React from'react';

function Navbar() {
  const userRole = 'admin';
  let navItems;
  if (userRole === 'admin') {
    navItems = (
      <ul>
        <li>Dashboard</li>
        <li>Users</li>
        <li>Settings</li>
      </ul>
    );
  } else if (userRole === 'user') {
    navItems = (
      <ul>
        <li>Profile</li>
        <li>Orders</li>
      </ul>
    );
  } else {
    navItems = <p>Invalid user role</p>;
  }
  return <div>{navItems}</div>;
}

export default Navbar;

在服务器端渲染这个组件时,会根据 userRole 的值生成不同的导航栏 HTML。这种方式在处理多种条件分支时非常有效,确保了服务器端渲染的准确性。

条件渲染与数据获取

在 SSR 中,数据获取是一个常见的操作,并且往往与条件逻辑紧密相关。我们可能需要根据不同的条件获取不同的数据。

假设我们有一个博客应用,需要根据文章的分类获取不同的文章列表。首先,我们创建一个 API 来获取文章数据:

// api.js
const articles = [
  { id: 1, title: 'Article 1', category: 'tech' },
  { id: 2, title: 'Article 2', category: 'life' },
  { id: 3, title: 'Article 3', category: 'tech' }
];

function getArticlesByCategory(category) {
  return articles.filter(article => article.category === category);
}

export { getArticlesByCategory };

然后在 React 组件中,我们根据用户请求的分类来获取并显示文章:

import React from'react';
import { getArticlesByCategory } from './api';

function ArticleList({ category }) {
  const articles = getArticlesByCategory(category);
  return (
    <div>
      <h2>{category} Articles</h2>
      {articles.map(article => (
        <div key={article.id}>
          <h3>{article.title}</h3>
        </div>
      ))}
    </div>
  );
}

export default ArticleList;

在服务器端渲染时,我们可以通过请求参数获取分类信息,并传递给 ArticleList 组件:

// server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react - dom/server');
const ArticleList = require('./src/ArticleList');

const app = express();

app.get('/articles/:category', (req, res) => {
  const category = req.params.category;
  const html = ReactDOMServer.renderToString(<ArticleList category={category} />);
  const page = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>${category} Articles</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/client - bundle.js"></script>
      </body>
    </html>
  `;
  res.send(page);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

这样,服务器端会根据不同的分类条件获取并渲染相应的文章列表,保证了首屏显示的内容与用户请求的条件相符。

条件逻辑与样式处理

在 React SSR 中,样式处理也常常涉及条件逻辑。我们可能需要根据不同的条件应用不同的样式。

例如,我们有一个按钮组件,根据其是否处于活动状态显示不同的样式。我们可以使用 CSS - in - JS 库如 styled - components 来实现:

import React from'react';
import styled from'styled - components';

const StyledButton = styled.button`
  background - color: ${props => (props.active? 'blue' : 'gray')};
  color: white;
  padding: 10px 20px;
  border: none;
  border - radius: 5px;
`;

function Button() {
  const isActive = true;
  return <StyledButton active={isActive}>Click me</StyledButton>;
}

export default Button;

在服务器端渲染时,styled - components 会根据 isActive 的值生成正确的内联样式,确保客户端和服务器端渲染的样式一致性。

处理异步条件逻辑

在实际应用中,很多条件逻辑依赖于异步操作,比如异步数据获取或者异步 API 调用。在 React SSR 中处理异步条件逻辑需要一些额外的技巧。

假设我们有一个组件,需要根据用户是否订阅了某个服务来显示不同的内容。订阅状态需要通过异步 API 调用获取:

// api.js
function checkSubscriptionStatus() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(true);
    }, 1000);
  });
}

export { checkSubscriptionStatus };
import React, { useState, useEffect } from'react';
import { checkSubscriptionStatus } from './api';

function SubscriptionComponent() {
  const [isSubscribed, setIsSubscribed] = useState(null);

  useEffect(() => {
    checkSubscriptionStatus().then(status => {
      setIsSubscribed(status);
    });
  }, []);

  if (isSubscribed === null) {
    return <p>Loading...</p>;
  }

  return (
    <div>
      {isSubscribed && <p>You are subscribed!</p>}
      {!isSubscribed && <p>Please subscribe.</p>}
    </div>
  );
}

export default SubscriptionComponent;

在服务器端渲染时,我们不能直接使用 useEffect,因为它是在客户端执行的。我们需要在服务器端手动调用 checkSubscriptionStatus 并将结果传递给组件。

// server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react - dom/server');
const SubscriptionComponent = require('./src/SubscriptionComponent');
const { checkSubscriptionStatus } = require('./src/api');

const app = express();

app.get('*', async (req, res) => {
  const isSubscribed = await checkSubscriptionStatus();
  const html = ReactDOMServer.renderToString(<SubscriptionComponent isSubscribed={isSubscribed} />);
  const page = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>Subscription Status</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/client - bundle.js"></script>
      </body>
    </html>
  `;
  res.send(page);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

这样,我们在服务器端和客户端都能正确处理异步条件逻辑,保证了渲染结果的一致性。

条件逻辑与 React 上下文(Context)

React 上下文(Context)是一种在组件树中共享数据的方式,在处理条件逻辑时也非常有用。

假设我们有一个应用,需要根据当前语言环境显示不同的文本。我们可以创建一个语言上下文:

import React from'react';

const LanguageContext = React.createContext('en');

export { LanguageContext };

然后在一个组件中,根据上下文的值来显示不同的文本:

import React from'react';
import { LanguageContext } from './LanguageContext';

function Greeting() {
  const language = React.useContext(LanguageContext);
  return (
    <div>
      {language === 'en' && <p>Hello!</p>}
      {language === 'fr' && <p>Bonjour!</p>}
    </div>
  );
}

export default Greeting;

在服务器端渲染时,我们需要在渲染组件树之前,将正确的语言上下文值传递进去:

// server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react - dom/server');
const Greeting = require('./src/Greeting');
const { LanguageContext } = require('./src/LanguageContext');

const app = express();

app.get('*', (req, res) => {
  const html = ReactDOMServer.renderToString(
    <LanguageContext.Provider value="fr">
      <Greeting />
    </LanguageContext.Provider>
  );
  const page = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>Greeting</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/client - bundle.js"></script>
      </body>
    </html>
  `;
  res.send(page);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

通过这种方式,我们可以在整个应用中基于上下文的条件逻辑来进行一致的服务器端渲染。

条件逻辑与路由

路由是 React 应用中重要的一部分,条件逻辑在路由处理中也扮演着关键角色。

例如,我们可能需要根据用户的权限来决定是否允许访问某个路由。假设我们使用 react - router - dom 进行路由管理:

import React from'react';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import AdminDashboard from './AdminDashboard';
import UserDashboard from './UserDashboard';
import Login from './Login';

function AppRouter() {
  const isLoggedIn = true;
  const isAdmin = false;

  return (
    <Router>
      <Routes>
        {isLoggedIn && (
          <>
            {isAdmin && <Route path="/admin" element={<AdminDashboard />} />}
            <Route path="/user" element={<UserDashboard />} />
          </>
        )}
        {!isLoggedIn && <Route path="/login" element={<Login />} />}
      </Routes>
    </Router>
  );
}

export default AppRouter;

在服务器端渲染时,我们同样需要根据这些条件来生成正确的路由结构。我们可以结合服务器端路由库如 react - router - dom/server 来实现:

// server.js
const express = require('express');
const React = require('react');
const { renderToString } = require('react - dom/server');
const { StaticRouter } = require('react - router - dom/server');
const AppRouter = require('./src/AppRouter');

const app = express();

app.get('*', (req, res) => {
  const context = {};
  const html = renderToString(
    <StaticRouter location={req.url} context={context}>
      <AppRouter />
    </StaticRouter>
  );
  const page = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>My App</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/client - bundle.js"></script>
      </body>
    </html>
  `;
  res.send(page);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

这样,服务器端就能根据用户的登录状态和权限生成相应的路由结构,保证了服务器端和客户端路由的一致性。

避免客户端与服务器端不一致

在 React SSR 中处理条件逻辑时,一个重要的目标是避免客户端和服务器端渲染结果不一致。这种不一致可能导致水合问题,影响用户体验。

为了避免这种情况,我们需要确保在服务器端和客户端执行相同的条件逻辑。例如,在数据获取方面,我们在服务器端和客户端使用相同的 API 调用逻辑,并且在条件判断上保持一致。

另外,我们还需要注意一些在客户端和服务器端行为不同的 API。比如,浏览器特有的 API 如 windowdocument 不能在服务器端使用。如果我们的条件逻辑依赖于这些 API,需要进行特殊处理,比如使用条件导入或者在服务器端模拟这些 API 的行为。

测试条件逻辑

对于 React SSR 中的条件逻辑,进行有效的测试是非常重要的。我们可以使用测试框架如 Jest 和 React Testing Library 来测试条件渲染。

例如,对于前面的 ConditionalComponent,我们可以编写如下测试:

import React from'react';
import { render } from '@testing - library/react';
import ConditionalComponent from './ConditionalComponent';

test('renders welcome message when logged in', () => {
  const { getByText } = render(<ConditionalComponent isLoggedIn={true} />);
  expect(getByText('Welcome, user!')).toBeInTheDocument();
});

test('renders login message when not logged in', () => {
  const { getByText } = render(<ConditionalComponent isLoggedIn={false} />);
  expect(getByText('Please log in.')).toBeInTheDocument();
});

对于涉及异步条件逻辑的组件,我们可以使用 async/awaitjest.fn() 来模拟异步操作并进行测试:

import React from'react';
import { render, waitFor } from '@testing - library/react';
import SubscriptionComponent from './SubscriptionComponent';
import { checkSubscriptionStatus } from './api';

jest.mock('./api');

test('renders subscription message when subscribed', async () => {
  checkSubscriptionStatus.mockResolvedValue(true);
  const { getByText } = render(<SubscriptionComponent />);
  await waitFor(() => expect(getByText('You are subscribed!')).toBeInTheDocument());
});

test('renders subscribe message when not subscribed', async () => {
  checkSubscriptionStatus.mockResolvedValue(false);
  const { getByText } = render(<SubscriptionComponent />);
  await waitFor(() => expect(getByText('Please subscribe.')).toBeInTheDocument());
});

通过这些测试,我们可以确保条件逻辑在不同情况下都能正确工作,提高应用的稳定性和可靠性。

在 React 服务端渲染中,条件逻辑处理贯穿于应用开发的各个方面,从简单的 UI 显示到复杂的数据获取和路由控制。通过正确处理条件逻辑,我们能够保证服务器端和客户端渲染结果的一致性,提高应用的性能和用户体验。同时,合理的测试策略也能帮助我们及时发现和修复条件逻辑中的问题,确保应用的质量。