用ReactDOM管理DOM组件

990 阅读12分钟

每当你创建一个React项目时,你首先需要和react 包一起安装的东西之一是react-dom 包。你有没有想过你为什么需要它?

这可能是一个惊喜,但我们不能只用react 包来渲染UI组件。为了向浏览器渲染用户界面,我们必须使用react-dom 。在本指南中,我们将通过创建一个示例应用程序来探索用ReactDOM管理DOM组件。在你跟随的过程中,我们将对我们的代码做一些修改,以了解你可以使用的多种方法。

你可以在Github部署的网站 上查看示例应用程序的代码

ReactDOM

ReactDOM将组件或JSX元素渲染到DOM中。ReactDOM对象只有少数几个方法;你可能已经使用了render() 方法,它负责在浏览器中渲染应用程序。

react-dom 包作为DOM的入口点。我们在项目的顶部像这样导入它。

import ReactDOM from 'react-dom';

在我们讨论ReactDOM方法的细节之前,让我们先了解为什么我们需要ReactDOM而不是仅仅使用DOM。

虚拟DOM(VDOM)vs DOM

JavaScript和HTML不能直接沟通,所以DOM的开发就是为了处理这个问题。当你打开一个网页时,浏览器引擎会将其所有的内容翻译成JavaScript可以理解的格式--DOM树。

这个树的结构与相应的HTML文档的结构是相同的。如果一个元素在HTML代码中被嵌套在另一个元素中,这将反映在DOM树中。

你可以在浏览器的开发工具面板上打开Elements标签,自己看看DOM是什么样子的。你所看到的将与HTML代码非常相似,只是你看到的不是HTML标签,而是DOM树中的元素。

DOM是由用户的浏览器创建并存储在其中的网页的逻辑表示。浏览器将网站的HTML转化为DOM,然后将DOM显示在用户的屏幕上,使用户看到网站。

让我们看看DOM的作用。图中显示了DOM如何看待HTML。

<body>
   <nav> 
       <ul>
          <li>Home</li>
          <li>Contact</li>
      </ul>
  </nav>
    <section class="cards">
         <img src="" alt="" />
            <div class="post-content">
                <h1>Virtual DOM vs DOM</h4> 
            </div>
    </section>
</body>

Diagram of DOM tree structure

不过,DOM也有一些问题。想象一下,用户点击了一个按钮来删除一个项目。那个节点和所有依赖于它的其他节点都将从DOM中删除。

每当浏览器检测到DOM的变化时,它就会用新的版本重新绘制整个页面。但我们真的需要重新绘制整个页面吗?比较两个DOM以确定哪些部分发生了变化是非常耗费时间的。

因此,对于浏览器来说,每当用户与网站互动时,简单地重绘整个页面实际上是更快的。这就是虚拟DOM的作用。

虚拟DOM是指React以JavaScript对象的形式创建他们自己的DOM表示。每当DOM发生变化时,库就会为这个JavaScript对象制作一个副本,对该副本进行修改,并比较这两个JavaScript对象,看看有什么变化。然后,它将这些变化告知浏览器,只有DOM的那些部分被重新绘制出来。

对JavaScript对象进行修改和比较,要比对DOM做同样的工作快得多。由于这个DOM的副本是作为一个JavaScript对象存储在内存中的,所以它被称为虚拟DOM。

虚拟DOM通过只对更新的元素和组进行重绘来防止不必要的重绘。VDOM是一个轻量级的、快速的、内存中的实际DOM的代表。

即使React尽可能地使用VDOM,它仍然会定期与实际的DOM交互。React更新实际DOM以使其与VDOM保持一致的过程被称为调和

ReactDOM.render()

现在我们对DOM和VDOM有了更好的理解,我们可以开始学习我们的第一个方法:ReactDOM.render 。渲染方法的用法如下。

ReactDOM.render(element, container[, callback])
ReactDOM.render(<h1>ReactDOM</h1>, document.getElementById("app"))

第一个参数是我们要渲染的元素或组件,第二个参数是我们要追加的HTML元素(目标节点)。

id 一般来说,当我们用create-react-app 创建我们的项目时,它会给我们一个div ,里面有rootindex.html ,我们把我们的React应用包在这个根div里面。

因此,当我们使用ReactDOM.render() 方法时,我们将我们的组件作为第一个参数传入,并以document.getElementById("root") 作为第二个参数来引用id="root"

 public/index.html 
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';

// create App component
const App = () => {
   return <div>Render Me!</div>
}

// render App component and show it on screen
ReactDOM.render(<App />, document.getElementById('root'));

再见ReactDOM.render()

在六月,React团队宣布了React 18,随着新的更新,我们将不再使用ReactDOM.render() 。相反,我们将使用ReactDOM.createRoot

React 18的alpha版本已经推出,但公开测试版还需要几个月的时间。如果你想试验一下React 18的alpha版本,请像这样安装它。

npm install react@alpha react-dom@alpha

使用React 18,我们将使用ReactDOM.createRoot 来创建一个根,然后将根传递给渲染函数。当你切换到createRoot ,你将默认获得React 18的所有新功能。

import ReactDOM from "react-dom";
import App from "App";

const container = document.getElementById("app");
const root = ReactDOM.createRoot(container);

root.render(<App />);

ReactDOM.createPortal()

我们在ReactDOM上的第二个方法是createPortal

你是否曾经需要创建一个叠加,或模态?React有不同的函数来处理模态、工具提示或其他类似性质的功能。一个是ReactDOM.createPortal() 函数。

为了渲染一个模态,或覆盖,我们需要使用z-index 属性来管理元素在屏幕上出现的顺序。z-index 允许我们在深度方面,沿着Z轴定位元素。

然而,正如你所知,我们只能渲染一个div,所有其他元素都嵌套在我们的根div里面。在createPortal 函数的帮助下,我们可以在主组件树之外渲染我们的模态。模态将是body元素的子元素。让我们来看看怎么做。

在我们的index.html ,我们将为模态添加一个div。

// index.html
<body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <div id="modal"></div>
 </body>

ReactDOM.createPortal() 函数需要两个参数:第一个参数是JSX,或者说是我们想要在屏幕上渲染的东西,第二个参数是对我们想要附加到模态的元素的引用。

// Modal.js

import { createPortal } from 'react-dom';
const modalRoot = document.querySelector('#modal');

const Modal = ({ children }) => createPortal(children, modalRoot);

export default Modal;

现在,为了渲染我们的组件,我们可以在模态组件的开头和结尾标签之间传递我们想要显示的东西。这将在模态组件中呈现为children 。我已经在App.js 中渲染了模态。

在我们的App.js 文件中,我们有一个按钮来打开模态。当用户与该按钮交互时,我们将显示模态和一个关闭按钮。

// App.js

import React, { useCallback, useState } from 'react';
import Modal from '../Modal';

const App = () => {
  const [showModal, setShowModal] = useState(false);

  const openModal = useCallback(() => setShowModal(true), []);
  const closeModal = useCallback(() => setShowModal(false), []);

  return (
    <div className='App'>
      <button onClick={openModal} className='button node'>
        Click To See Modal
      </button>
      {showModal ? (
        <Modal>
          <div className='modal-container'>
            <div class='modal'>
              <h1>I'm a modal!</h1>
              <button onClick={closeModal} className='close-button'></button>
            </div>
          </div>
        </Modal>
      ) : null}
    </div>
  );
};

export default App;

Gif of a modal popping up after clicking a button

ReactDOM.unmountComponentAtNode()

当我们需要在一个DOM节点安装后将其移除,并清理其事件处理程序和状态时,我们会使用这个方法。

我们将再次继续我们的代码,这次我们将卸载我们的root div

ReactDOM.unmountComponentAtNode(container)

对于容器,我们要传入根div,所以当用户点击按钮时,它将卸载应用程序。

如果你试图卸载modal id ,你会看到一个错误。这是因为modal没有被挂载,所以它将返回false

// App.js

const App = () => {
  ReactDOM.unmountComponentAtNode(document.getElementById("root"));

  return (
    <button onClick={handleUnmount} className='button'>
      Unmount App
    </button>
  )
}

这段代码足以解除对根的挂载。

Gif of modal appearing at button click with "unmount app" button

ReactDOM.findDOMNode()

我们知道,我们可以通过render 方法来渲染我们的DOM元素。我们还可以借助findDOMNode 方法来访问底层的DOM节点。根据React文档,这个方法是不鼓励的,因为它刺穿了组件的抽象。

注意:findDOMNode 方法在StrictMode中已经被弃用。

一般来说,如果你需要引用任何DOM元素,建议使用 useRef 钩子。在大多数情况下,你可以给DOM节点附加一个引用,而完全避免使用findDOMNode

另一个关键点是你想访问的Node元素必须被挂载,这意味着它必须在DOM中。如果它没有被加载,findDOMNode ,返回null。一旦你获得了对已安装的DOM节点的访问权,你就可以使用熟悉的DOM API来检查该节点。

findDOMNode 需要一个参数,它就是组件。

ReactDOM.findDOMNode(component)

我们将继续使用我们在createPortal 方法上使用的代码。我创建了一个新的按钮并添加了文本Find The Modal Button and Change its Background Color ,并添加了一个onClick 处理函数。通过这个函数,我用document.querySelector 方法访问了它的className ,并将其背景颜色改为黑色。

const App = () => {
  const handleFindDOMNode = () => {
    const node = document.querySelector('.node');
    ReactDOM.findDOMNode(node).style.backgroundColor = 'black';
  };

 return (
  <button onClick={handleFindDOMNode} className='button'>
    Find The Modal Button and Change its Background Color
  </button>
  )
}

Same gif as before, but with an additional button that reads "find the modal button and make it black"

ReactDOM.hydrate() 和服务器端渲染(SSR)

hydrate 方法将帮助我们在服务器端预渲染一切,然后向用户发送完整的标记。它被用来向一个由ReactDOMServer渲染的容器添加内容。

现在听起来可能是胡言乱语,但主要的收获是,我们可以在客户端或服务器端渲染我们的React应用程序。下面是对客户端渲染(CSR)和服务器端渲染(SSR)的主要区别的快速概述。

客户端渲染(CSR)

当我们用create-react-app ,创建并运行我们的项目时,它不会显示页面的内容。

Screenshot of the code for the example app

正如你从截图中看到的,我们只有我们的divs和对我们的JavaScript bundle的引用,没有其他内容。所以,这实际上是一个空白页面。这意味着当我们第一次加载我们的页面时,服务器会对HTML、CSS和JavaScript进行请求。在最初的渲染之后,服务器检查我们捆绑的JavaScript(或React代码,在我们的例子中)并绘制UI。这种方法有一些优点和缺点。

优点。

  • 快速
  • 静态部署
  • 支持单页应用(SPA)。

缺点。

  • 初次加载时呈现空白页面
  • 捆绑的大小可能很大
  • 不利于SEO

服务器端渲染(SSR)

有了服务器端渲染,我们就不会再渲染一个空页面。通过这种方法,服务器创建静态HTML文件,由浏览器进行渲染。

这就是SSR的工作原理:当用户请求一个网站时,服务器会渲染静态版本的应用程序,让用户看到网站被加载。网站还不是互动的,所以当用户与应用程序互动时,服务器会下载JavaScript并执行它。

通过用动态内容替换静态内容,网站就变成了响应式的。ReactDOM.hydrate() 函数实际上是在这些脚本的加载事件中被调用,并将功能与渲染的标记挂钩。

如果你感到好奇,当我们用SSR创建我们的项目时,我们可以看到在初始加载时呈现的HTML和JavaScript代码。

Screenshot of code for the example app with SSR

优点。

  • 性能更好
  • 对SEO非常有利,可以帮助我们创建容易被索引和抓取的网站
  • 快速的互动性
  • 在向用户提供请求之前在服务器上运行React,加快了上传时间。

缺点。

  • 会产生大量的服务器请求
  • 如果你的网站上有很多互动元素,它可能会减慢渲染速度。

演示ReactDOM.hydrate()

正如你可以想象的那样,为了使其发挥作用,我们需要创建一个服务器。我们将用Express创建服务器,但首先我们需要做一些清理工作。

要使用hydrate 运行 Node.js 服务器,我们需要删除所有对windowdocument 的引用,因为我们将在服务器中渲染我们的标记,而不是在浏览器中。

让我们进入Modal.js 文件,把document.querySelector 移到模态组件里面。

// Modal.js

import { createPortal } from 'react-dom';
let modalRoot;

const Modal = ({ children }) => {
  modalRoot = modalRoot ? modalRoot : document.querySelector('#modal');
  return createPortal(children, modalRoot);
};

export default Modal;

接下来,我们需要在src/index.js 文件中把ReactDOM.render 改为ReactDOM.hydrate

ReactDOM.hydrate(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

现在,我们可以创建我们的服务器。创建一个名为server 的新文件夹,并在此文件夹内创建一个名为server.js 的文件。用npm install express 安装Express。

// server/server.js

import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from '../src/App';

const app = express();

app.use('^/$', (req, res, next) => {
  fs.readFile(path.resolve('./build/index.html'), 'utf-8', (err, data) => {
    if (err) {
      console.error(err);
      return res.status(500).send('Error');
    }
    return res.send(
      data.replace(
        '<div id="root"></div>',
        `<div id="root">${renderToString(<App />)}</div>`
      )
    );
  });
});

app.use(express.static(path.resolve(__dirname, '..', 'build')));

app.listen(3000, () => {
  console.log('Listening on port 3000');
});

在这里,我们需要Express、fs (文件系统)模块、路径、React、ReactDOMServer.renderToString ,以及来自我们src文件夹的App

ReactDOMServer.renderToString 返回我们应用程序的静态HTML版本。

接下来,运行构建命令npm run build ,创建一个build 文件夹。配置Babel,然后安装npm i @babel/preset-env @babel/preset-react @babel/register ignore styles 。最后,创建一个名为server/index.js 的新文件。

// server/index.js

require('ignore-styles');
require('@babel/register')({
  ignore: [/(node_modules)/],
  presets: ['@babel/preset-env', '@babel/preset-react'],
});
require('./server');

package.json 中为SSR添加一个脚本:"ssr": "node server/index.js" 。用npm run ssr 运行服务器。

记住,如果你在你的应用程序中做了修改,先运行npm run build ,然后再运行npm run ssr

使用React 18对hydrate 的改变

在React 18中,一个新的基于Suspense的SSR架构被引入hydrate 方法也将被hydrateRoot 所取代。

总结

我们已经涵盖了很多关于ReactDOM的内容。总结一下,以下是我们在这篇文章中了解到的关键信息。

  • React使用一个虚拟DOM,这有助于我们防止不必要的DOM重绘,并且只更新UI中的变化。
  • 我们使用render 方法来向浏览器渲染我们的UI组件,这是最常用的ReactDOM方法。
  • 在React 18中,我们使用createRoot 方法而不是render 方法。
  • 我们可以用createPortal 方法来创建模态和工具提示。
  • 我们可以用unmountComponentAtNode 方法来卸载一个组件
  • 我们可以用findDOMNode 方法访问任何DOM节点,但最好的方法是用ref 来代替。
  • 我们可以借助hydrate 方法在React中使用SSR,只是要为React 18和基于Suspense的SSR架构做好准备。
  • SSR帮助我们在服务器端预先渲染所有内容,以便更好地进行SEO优化

The postManaging DOM components with ReactDOMappeared first onLogRocket Blog.