[译]React 在服务端渲染的实现

React服务端渲染
了解React在服务端渲染的实现及优势,掌握从客户端渲染转向服务端渲染的步骤,包括解决异步数据获取的问题。

阅读目录(Content)

回到顶部(go to top)

React 在服务端渲染的实现

React是最受欢迎的客户端 JavaScript 框架,但你知道吗(可以试试),你可以使用 React 在服务器端进行渲染?

假设你已经在客户端使用 React 构建了一个事件列表 app。该应用程序使用了您最喜欢的服务器端工具构建的API。几周后,用户告诉您,他们的页面没有显示在 Google 上,发布到 Facebook 时也显示不出来。 这些问题似乎是可以解决的,对吧?

您会发现,要解决这个问题,需要在初始加载时从服务器渲染 React 页面,以便来自搜索引擎和社交媒体网站的爬虫工具可以读取您的标记。有证据表明,Google 有时会执行 javascript 程序并且对生成的内容进行索引,但并不总是的。因此,如果您希望确保与其他服​​务(如Facebook,Twitter)有良好的SEO兼容性,那么始终建议使用服务器端渲染。

在本教程中,我们将逐步介绍服务器端的呈现示例。包括围绕与API交流的React应用程序的共同路障。
在本教程中,我们将逐步向您介绍服务器端的渲染示例。包括围绕着 APIS 交流一些在服务端渲染 React 应用程序的共同障碍。

回到顶部(go to top)

服务端渲染的优势

可能您的团队谈论到服务端渲染的好处是首先会想到 SEO,但这并不是唯一的潜在好处。

更大的好处如下:服务器端渲染能更快地显示页面。使用服务器端渲染,您的服务器对浏览器进行响应是在您的 HTML 页面可以渲染的时候,因此浏览器可以不用等待所有的 JavaScript 被下载和执行就可以开始渲染。当浏览器下载并执行页面所需的 JavaScript 和其他资源时,不会出现 “白屏” 现象,而 “白屏” 这是在完全有客户端呈现的 React 网站中可能发生的情况。

回到顶部(go to top)

入门

接下来让我们来看看如何将服务器端渲染添加到一个基本的客户端渲染的使用Babel和Webpack的React应用程序中。我们的应用程序将增加从第三方 API 获取数据的复杂性。我们在GitHub上提供了相关代码,您可以在其中看到完整的示例。

提供的代码中只有一个 React 组件,`hello.js`,这个文件将向 ButterCMS 发出异步请求,并渲染返回的 JSON 列表的博文。ButterCMS 是一个基于API的博客引擎,可供个人使用,因此它非常适合测试现实生活中的用例。启动代码中连接着一个 API token,如果你想使用你自己的 API token 可以使用你的 GitHub 账号登入 ButterCMS

import React from 'react';
import Butter from 'buttercms'

const butter = Butter('b60a008584313ed21803780bc9208557b3b49fbb');

var Hello = React.createClass({
  getInitialState: function() {
    return {loaded: false};
  },
  componentWillMount: function() {
    butter.post.list().then((resp) => {
      this.setState({
        loaded: true,
        resp: resp.data
      })
    });
  },
  render: function() {
    if (this.state.loaded) {
      return (
        <div>
          {this.state.resp.data.map((post) => {
            return (
              <div key={post.slug}>{post.title}</div>
            )
          })}
        </div>
      );
    } else {
      return <div>Loading...</div>;
    }
  }
});

export default Hello;
 

启动器代码中包含以下内容:

  • package.json - 依赖项
  • Webpack 和 Babel 配置
  • index.html - app 的 HTML 文件
  • index.js - 加载 React 并渲染 Hello 组件

要使应用运行,请先克隆资源库:

git clone ...
cd ..
 

安装依赖:

npm install
 

然后启动服务器:

npm run start
 

浏览器输入 http://localhost:8000 可以看到这个 app: (这里译者进行补充,package.json 里的 start 命令改为如下:"start": webpack-dev-server --watch)

如果您查看渲染页面的源代码,您将看到发送到浏览器的标记只是一个到 JavaScript 文件的链接。这意味着页面的内容不能保证被搜索引擎和社交媒体平台抓取:

回到顶部(go to top)

增加服务器端渲染

接下来,我们将实现服务器端渲染,以便将完全生成的HTML发送到浏览器。如果要同时查看所有更改,请查看GitHub上的差异

To get started, we'll install Express, a Node.js server side application framework:
开始前,让我们安装 Express,一个 Node.js 的服务器端应用程序框架:

npm install express --save
 

我们要创建一个渲染我们的 React 组件的服务器:

import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Hello from './Hello.js';

function handleRender(req, res) {
  // 把 Hello 组件渲染成 HTML 字符串
  const html = ReactDOMServer.renderToString(<Hello />);

  // 加载 index.html 的内容
  fs.readFile('./index.html', 'utf8', function (err, data) {
    if (err) throw err;

    // 把渲染后的 React HTML 插入到 div 中
    const document = data.replace(/<div id="app"><\/div>/, `<div id="app">${html}</div>`);

    // 把响应传回给客户端
    res.send(document);
  });
}

const app = express();

// 服务器使用 static 中间件构建 build 路径
app.use('/build', express.static(path.join(__dirname, 'build')));

// 使用我们的 handleRender 中间件处理服务端请求
app.get('*', handleRender);

// 启动服务器
app.listen(3000);
 

让我们分解下程序看看发生了什么事情...

handleRender 函数处理所有请求。在文件顶部导入的 ReactDOMServer 类提供了将 React 节点渲染成其初始 HTML 的 renderToString() 方法

ReactDOMServer.renderToString(<Hello />);
 

这将返回 Hello 组件的 HTML ,我们将其注入到 index.html 的 HTML 中,从而生成服务器上页面的完整 HTML 。

const document = data.replace(/<div id="app"><\/div>/,`<div id="app">${html}</div>`);
 

To start the server, update the start script in package.json and then run npm run start:
要启动服务器,请更新 `package.json` 中的起始脚本,然后运行 npm run start :

"scripts": {
  "start": "webpack && babel-node server.js"
},
 

浏览 http://localhost:3000 查看应用程序。瞧!您的页面现在正在从服务器渲染出来了。但是有个问题,
如果您在浏览器中查看页面源码,您会注意到博客文章仍未包含在回复中。这是怎么回事?如果我们在Chrome中打开网络标签,我们会看到客户端上发生API请求。

虽然我们在服务器上渲染了 React 组件,但是 API 请求在 componentWillMount 中异步生成,并且组件在请求完成之前渲染。所以即使我们已经在服务器上完成渲染,但我们只是完成了部分。事实上,React repo 有一个 issue,超过 100 条评论讨论了这个问题和各种解决方法。

回到顶部(go to top)

在渲染之前获取数据

要解决这个问题,我们需要在渲染 Hello 组件之前确保 API 请求完成。这意味着要使 API 请求跳出 React 的组件渲染循环,并在渲染组件之前获取数据。我们将逐步介绍这一步,但您可以在GitHub上查看完整的差异

To move data fetching before rendering, we'll install react-transmit:
要在渲染之前获取数据,我们需安装 react-transmit

npm install react-transmit --save
 

React Transmit 给了我们优雅的包装器组件(通常称为“高阶组件”),用于获取在客户端和服务器上工作的数据。

这是我们使用 react-transmit 后的组件的代码:

import React from 'react';
import Butter from 'buttercms'
import Transmit from 'react-transmit';

const butter = Butter('b60a008584313ed21803780bc9208557b3b49fbb');

var Hello = React.createClass({
  render: function() {
    if (this.props.posts) {
      return (
        <div>
          {this.props.posts.data.map((post) => {
            return (
              <div key={post.slug}>{post.title}</div>
            )
          })}
        </div>
      );
    } else {
      return <div>Loading...</div>;
    }
  }
});

export default Transmit.createContainer(Hello, {
  // 必须设定 initiallVariables 和 ftagments ,否则渲染时会报错
  initialVariables: {},
  // 定义的方法名将成为 Transmit props 的名称
  fragments: {
    posts() {
      return butter.post.list().then((resp) => resp.data);
    }
  }
});
 

我们已经使用 Transmit.createContainer 将我们的组件包装在一个高级组件中,该组件可以用来获取数据。我们在 React 组件中删除了生命周期方法,因为无需两次获取数据。同时我们把 render 方法中的 state 替换成 props,因为 React Transmit 将数据作为 props 传递给组件。

为了确保服务器在渲染之前获取数据,我们导入 Transmit 并使用 Transmit.renderToString 而不是 ReactDOM.renderToString 方法

import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Hello from './Hello.js';
import Transmit from 'react-transmit';

function handleRender(req, res) {
  Transmit.renderToString(Hello).then(({reactString, reactData}) => {
    fs.readFile('./index.html', 'utf8', function (err, data) {
      if (err) throw err;

      const document = data.replace(/<div id="app"><\/div>/, `<div id="app">${reactString}</div>`);
      const output = Transmit.injectIntoMarkup(document, reactData, ['/build/client.js']);

      res.send(document);
    });
  });
}

const app = express();

// 服务器使用 static 中间件构建 build 路径
app.use('/build', express.static(path.join(__dirname, 'build')));

// 使用我们的 handleRender 中间件处理服务端请求
app.get('*', handleRender);

// 启动服务器
app.listen(3000);
 

重新启动服务器浏览到 http://localhost:3000。查看页面源代码,您将看到该页面现在完全呈现在服务器上!

回到顶部(go to top)

更进一步

我们做到了!在服务器上使用 React 可能很棘手,尤其是从 API 获取数据时。幸运的是,React社区正在蓬勃发展,并创造了许多有用的工具。如果您对构建在客户端和服务器上渲染的大型 React 应用程序的框架感兴趣,请查看 Walmart Labs 的 Electrode 或 Next.js。或者如果要在 Ruby 中渲染 React ,请查看 AirBnB 的 Hypernova 。

作者:牧云云 
出处:http://www.cnblogs.com/MuYunyun/" 
本文版权归作者和博客园所有,欢迎转载,转载请标明出处。  

React 核心理解 react16之前更新 React Fiber是16版本之后的一种更新机制,使用链表取代了树,是一种fiber数据结构,其有三个指针,分别指向了父节点、子节点、兄弟节点,当中断的时候会记录下当前的节点,然后继续更新,而15版本中的DOM stack不能有中断操作,它把组件渲染的工作分片,到时会主动让出渲染主线程;提炼fiber的关键词,大概给出如下几点: fiber是一种数据结构。 fiber使用父子关系以及next的妙用,以链表形式模拟了传统调用栈。 fiber是一种调度让出机制,只在有剩余时间的情况下运行。 fiber实现了增量渲染,在浏览器允许的情况下一点点拼凑出最终渲染效果。 fiber实现了并发,为任务赋予不同优先级,保证了一有时间总是做最高优先级的事,而不是先来先占位死板的去执行。 fiber有协调与提交两个阶段,协调包含了fiber创建与diff更新,此过程可暂停。而提交必须同步执行,保证渲染不卡顿。 react17更新 1、新的JSX转换,不需要手动引入react React 16: babel-loader会预编JSX为 React.createElement(...) React 17: React 17中的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入react并调用。 另外此次升级不会改变 JSX 语法,旧的 JSX 转换也将继续工作。 2、事件代理更改 在React 17中,将不再在后台的文档级别附加事件处理程序,不在document对象上绑定事件,改为绑定于每个react应用的rootNode节点,因为各个应用的rootNode肯定不同,所以这样可以使多个版本的react应用同时安全的存在于页面中,不会因为事件绑定系统起冲突。react应用之间也可以安全的进行嵌套。 3、事件池(event pooling)的改变 React 17去除了事件池(event pooling),不在需要e.persist(),现在可以直接在异步事件中(回掉或timeout等)拿到事件对象,操作更加直观,不会令人迷惑。e.persist()仍然可用,但是不会有任何效果。 4、异步执行 React 17将副作用清理函数(useEffect)改为异步执行,即在浏览器渲染完毕后执行。 5、forwardRef 和 memo组件的行为 React 17中forwardRef 和 memo组件的行为会与常规函数组件和class组件保持一致。它们在返回undefined时会报错。 react18更新 并发模式 v18的新特性是使用现代浏览器的特性构建的,彻底放弃对 IE 的支持。 v17 和 v18 的区别就是:从同步不可中断更新变成了异步可中断更新,v17可以通过一些试验性的API开启并发模式,而v18则全面开启并发模式。 并发模式可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整,该模式通过使渲染可中断来修复阻塞渲染限制。在 Concurrent 模式中,React 可以同时更新多个状态。 这里参考下文区分几个概念: 并发模式是实现并发更新的基本前提 v18 中,以是否使用并发特性作为是否开启并发更新的依据。 并发特性指开启并发模式后才能使用的特性,比如:useDeferredValue/useTransition 更新 render API v18 使用 ReactDOM.createRoot() 创建一个新的根元素进行渲染,使用该 API,会自动启用并发模式。如果你升级到v18,但没有使用ReactDOM.createRoot()代替ReactDOM.render()时,控制台会打印错误日志要提醒你使用React,该警告也意味此项变更没有造成breaking change,而可以并存,当然尽量是不建议。 自动批处理 批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能 在v17的批处理只会在事件处理函数中实现,而在Promise链、异步代码、原生事件处理函数中失效。而v18则所有的更新都会自动进行批处理。 Suspense 支持 SSR SSR 一次页面渲染的流程: 服务器获取页面所需数据 将组件渲染成 HTML 形式作为响应返回 客户端加载资源 (hydrate)执行 JS,并生成页面最终内容 上述流程是串行执行的,v18前的 SSR 有一个问题就是它不允许组件"等待数据",必须收集好所有的数据,才能开始向客户端发送HTML。如果其中有一步比较慢,都会影响整体的渲染速度。 v18 中使用并发渲染特性扩展了Suspense的功能,使其支持流式 SSR,将 React 组件分解成更小的块,允许服务端一点一点返回页面,尽早发送 HTML和选择性的 hydrate, 从而可以使SSR更快的加载页面: startTransition Transitions 是 React 18 引入的一个全新的并发特性。它允许你将标记更新作为一个 transitions(过渡),这会告诉 React 它们可以被中断执行,并避免回到已经可见内容的 Suspense 降级方案。本质上是用于一些不是很急迫的更新上,用来进行并发控制 在v18之前,所有的更新任务都被视为急迫的任务,而Concurrent Mode 模式能将渲染中断,可以让高优先级的任务先更新渲染React 的状态更新可以分为两类: 紧急更新:比如点击按钮、搜索框打字是需要立即响应的行为,如果没有立即响应给用户的体验就是感觉卡顿延迟 过渡/非紧急更新:将 UI 从一个视图过渡到另一个视图。一些延迟可以接受的更新操作,不需要立即响应 startTransition API 允许将更新标记为非紧急事件处理,被startTransition包裹的会延迟更新的state,期间可能被其他紧急渲染所抢占。因为 React 会在高优先级更新渲染完成之后,才会渲染低优先级任务的更新 React 无法自动识别哪些更新是优先级更高的。比如用户的键盘输入操作后,setInputValue会立即更新用户的输入到界面上,是紧急更新。而setSearchQuery是根据用户输入,查询相应的内容,是非紧急的。 React无法自动识别,所以它提供了 startTransition让我们手动指定哪些更新是紧急的,哪些是非紧急的,从而让我们改善用户交互体验。 useTransition 当有过渡任务(非紧急更新)时,我们可能需要告诉用户什么时候当前处于 pending(过渡) 状态,因此v18提供了一个带有isPending标志的 Hook useTransition来跟踪 transition 状态,用于过渡期。 useTransition 执行返回一个数组。数组有两个状态值: isPending: 指处于过渡状态,正在加载中 startTransition: 通过回调函数将状态更新包装起来告诉 React这是一个过渡任务,是一个低优先级的更新 直观感觉这有点像 setTimeout,而防抖节流其实本质也是setTimeout,区别是防抖节流是控制了执行频率,让渲染次数减少了,而 v18的 transition 则没有减少渲染的次数。 useDeferredValue useDeferredValue 和 useTransition 一样,都是标记了一次非紧急更新。useTransition是处理一段逻辑,而useDeferredValue是产生一个新状态,它是延时状态,这个新的状态则叫 DeferredValue。所以使用useDeferredValue可以推迟状态的渲染 useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到紧急更新之后。如果当前渲染是一个紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值。 JavaScript 运行代码 复制代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function Typeahead() { const query = useSearchQuery(''); const deferredQuery = useDeferredValue(query); // Memoizing 告诉 React 仅当 deferredQuery 改变, // 而不是 query 改变的时候才重新渲染 const suggestions = useMemo(() => <SearchSuggestions query={deferredQuery} />, [deferredQuery] ); return ( <> <SearchInput query={query} /> <Suspense fallback="Loading results..."> {suggestions} </Suspense> </> ); } 这样一看,useDeferredValue直观就是延迟显示状态,那用防抖节流有什么区别呢? 如果使用防抖节流,比如延迟300ms显示则意味着所有用户都要延时,在渲染内容较少、用户CPU性能较好的情况下也是会延迟300ms,而且你要根据实际情况来调整延迟的合适值;但是useDeferredValue是否延迟取决于计算机的性能。 useId useId支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不匹配,原理就是每个 id 代表该组件在组件树中的层级结构: JavaScript 运行代码 复制代码 1 2 3 4 5 6 7 8 9 function Checkbox() { const id = useId() return ( <> <label htmlFor={id}>Do you like React?</label> <input id={id} type="checkbox" name="react" /> </> ) } React.memo 和性能优化。当某个组件状态更新时,它的所有子组件树将会重新渲染React.memo 和记忆化数据 React.memo 和 React.useMemo 优化性能 React.memo 和 React.useCallback 优化性能 React useEffect cleanup。在这段代码中,示例演示 cleanup 的时机 React 中可以以数组的 index 作为 key 吗?。在这段代码中,使用 index 作为 key,其中夹杂了 input,引发 bug React 中以数组的 index 作为 key。在这段代码中,使用 index 作为 key,其中夹杂了随机数,引发了 bug React 兄弟组件通信。兄弟组件在 React 中如何通信 React 中合成事件。React 中事件为合成事件,你可以通过 e.nativeEvent 获取到原生事件,观察 e.nativeEvent.currentTarget 你将会发现 React 将所有事件都绑定在了 #app(React 应用挂载的根组件) React 中 input.onChange 的原生事件是什么?。观察 e.nativeEvent.type 可知 React hooks 如何实现一个计数器 Counter React FiberNode 数据结构。贯彻 element._owner 可知 FiberNode 数据结构 React 点击按钮时自增三次。此时需使用回调函数,否则会报错 React 不可变数据的必要性。 React 不可变数据的必要性之函数组件。当在 React hooks 中 setState 两次为相同数据时,不会重新渲染 React 状态批量更新之事件处理。事件处理中的状态会批量更新,减少渲染次数 React 状态批量更新之异步请求。异步请求中的状态不会批量更新,将会造成多次渲染 React18 状态批量更新。在 React 18 中所有状态将会批量更新 React capture value 整理成markerdown代码风格写出来 我copy
08-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值