Рендеринг на стороне сервера React (строительство проекта)

React.js

предисловие

В настоящее время одностраничное приложение (SPA) очень популярно, но оно также приносит некоторые проблемы, такие как недружественный SEO и медленная загрузка первого экрана в случае плохой сети. Чтобы решить эти проблемы, мы как бы вернулись к традиционной модели веб-разработки, вернуться назад невозможно, и невозможно вернуться, если мы уже попали в яму. Как среда разработки приложений SPA, React также поддерживает рендеринг на стороне сервера.Эта серия статей расскажет, как создать проект рендеринга на стороне сервера React, исходя из следующих пунктов.

Если вы предпочитаете нестандартные решения, попробуйте решения более высокого уровня.Next.js, Next.js и предоставляет некоторые дополнительные функции. Эта серия статей предназначена для того, чтобы дать вам знать, как настроить рендеринг на стороне сервера. Когда вы ознакомитесь с ним, вы сможете более непосредственно управлять приложением. Прежде чем читать его, вам необходимо иметь следующие технические навыки.

внешний интерфейс

  1. Ведро семейства React (React, React-Router, Redux) (знакомое)
  2. Вебпак (знакомый)
  3. Бабель (понимаю)
  4. Эслинт (понимает)
  5. ES6 (понять)
  6. Обещание (понять)

задняя часть

  • Экспресс (понять)

См. адрес исходного кода в конце статьи.

Если вы используете webpack4, полный штамп исходного кода babel7здесь

Конфигурация веб-пакета

Примечание. Версия 3.x

Рендеринг на стороне сервера позволяет серверу сгенерировать html-строку, а затем отправить сгенерированную html-строку в браузер, и браузер отобразит ее после получения html, в то время как клиент выполняет только привязку событий DOM. На данный момент нам нужно упаковать два фрагмента кода, один для рендеринга html сервером, а другой для браузера.Большую часть кода можно найти вСервера такжеклиентвоплощать в жизнь

Структура каталогов

+---config                          配置目录
|       dev.env.js                  开发环境配置
|       prod.env.js                 生产环境配置
|       util.js
|       webpack.config.base.js      公用打包配置
|       webpack.config.client.js    客户端打包配置
|       webpack.config.server.js    服务端打包配置
+---public
|       favicon.ico
+---src                             源码目录
|   +---assets                      资源目录
|       App.jsx                     根组件
|       entry-client.js             客户端打包入口
|       entry-server.js             服务端打包入口
|       server.js                   服务端启动js
|       setup-dev-server.js         开发环境打包服务
|   .babelrc                        babel配置文件
|   .eslintignore
|   .eslintrc.js                    eslint配置文件
|   index.html                      模板html
|   package-lock.json
|   package.json

общая конфигурация

Сначала напишите общую конфигурацию сервера и клиента, включая различные загрузчики, соответствующие js, шрифты, картинки, аудио и другие файлы. В общедоступной конфигурации различайте, является ли это средой разработки или производственной средой.Если это производственная среда, используйте подключаемый модуль UglifyJsPlugin для js uglification и используйте подключаемый модуль DefinePlugin для определения конфигурации в разных средах.

webpack.config.base.js

const webpack = require("webpack");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");

let env = "dev";
let isProd = false;
let prodPlugins = [];
if (process.env.NODE_ENV === "production") {
  env = "prod";
  isProd = true;
  prodPlugins = [
    new UglifyJsPlugin({sourceMap: true})
  ];
}

const baseWebpackConfig = {
  devtool: isProd ? "#source-map" : "#cheap-module-source-map",
  resolve: {
    extensions: [".js", ".jsx", ".json"]
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        loader: ["babel-loader", "eslint-loader"],
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/,
        loader: "url-loader",
        options: {
          limit: 10000,
          name: "static/img/[name].[hash:7].[ext]"
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: "static/fonts/[name].[hash:7].[ext]"
        }
      }
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      "process.env": require("./" + env + ".env")
    }),
    ...prodPlugins
  ]
}

module.exports = baseWebpackConfig;

Конфигурация клиента

Настройка клиента аналогична настройке обычных одностраничных приложений.Используйте плагин HtmlWebpackPlugin, чтобы внедрить упакованные стили и js в шаблон index.html, укажитеdistЭто корневой каталог после упаковки, и последующий экспресс будет использовать этот каталог в качестве статического каталога ресурсов для сопоставления ресурсов.util.jsсерединаstyleLoadersКонфигурация загрузчика, такая как css, postcss, sass, less и стилус, записывается в функцию, а подключаемый модуль ExtractTextPlugin используется в производственной среде для извлечения стилей в файл css.

webpack.config.client.js

const path = require("path");
const merge = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const baseWebpackConfig = require("./webpack.config.base");
const util = require("./util");

const isProd = process.env.NODE_ENV === "production";

const webpackConfig = merge(baseWebpackConfig, {
  entry: {
    app: "./src/entry-client.js"
  },
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "static/js/[name].[chunkhash].js",
    publicPath: "/dist/"  // 打包后输出路径以/dist/开头
  },
  module: {
    rules: util.styleLoaders({
        sourceMap: isProd ? true : false,
        usePostCSS: true,
        extract: isProd ? true : false
      })
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: "index.html"
    })
  ]
});

if (isProd) {
  webpackConfig.plugins.push(
    new ExtractTextPlugin({
      filename: "static/css/[name].[contenthash].css"
    })
  );
}

module.exports = webpackConfig;

Конфигурация сервера

Конфигурация сервера отличается от конфигурации клиента. Сервер работает в узле, не поддерживает Babel, не поддерживает стили и не поддерживает некоторые глобальные объекты браузера, такие как окно и документ. Для Babel используйте для преобразования babel-loader , и используйте плагины для стилей. После извлечения сервер запускает js только для создания фрагментов html, а стили упаковываются клиентом, загружаются и выполняются браузером. Некоторые люди будут использовать babel-register или babel-node, оба из которых преобразуются с помощью babel в node, и оба перекодируются в реальном времени, поэтому это будет иметь определенное влияние на производительность.Рекомендуется использовать его в среде разработки. , и его следует использовать в производственной среде. Заранее преобразуйте код

webpack.config.server.js

const path = require("path");
const webpack = require("webpack");
const merge = require("webpack-merge");
const baseWebpackConfig = require("./webpack.config.base");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const util = require("./util");

const webpackConfig = merge(baseWebpackConfig, {
  entry: {
    app: "./src/entry-server.js"
  },
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "entry-server.js",
    libraryTarget: "commonjs2"  // 打包成commonjs2规范
  },
  target: "node",  // 指定node运行环境
  module: {
    rules: util.styleLoaders({
        sourceMap: true,
        usePostCSS: true,
        extract: true
      })
  },
  plugins: [
    new webpack.DefinePlugin({
      "process.env.REACT_ENV": JSON.stringify("server")  // 指定React环境为服务端
    }),
    // 服务端不支持window document等对象,需将css外链
    new ExtractTextPlugin({
      filename: "static/css/[name].[contenthash].css"
    })
  ]
});

module.exports = webpackConfig;

Бабель и Эслинт

React нужно преобразовать с помощью плагина babel, установить babel-core, babel-preset-env, babel-preset-react, babel-loader. Конфигурация Babel выглядит следующим образом

{
  "presets": ["env", "react"]
}

env содержит es2015, es2016, es2017 и последнюю версию, а react используется для преобразования React

Примечание: Babel использует версию 6.x.

Хорошая спецификация кода — основа совместной разработки В этой статье eslint используется для проверки спецификации кода и установки eslint, eslint-plugin-react, babel-eslint и eslint-loader. Конфигурация eslint выглядит следующим образом

module.exports = {
    root: true,
    parser: "babel-eslint",
    env: {
      es6: true,
      browser: true,
      node: true
    },
    extends: [
      "eslint:recommended",
      "plugin:react/recommended"
    ],
    parserOptions: {
      sourceType: "module",
      ecmaFeatures: {
        jsx: true
      }
    },
    rules: {
      "no-unused-vars": 0,
      "react/display-name": 0,
      "react/prop-types": 0
    },
    settings: {
      react: {
        version: "16.4.2"
      }
    }
  }

настроитьjsx:trueвключить поддержку jsx, настроитьeslint:recommendedВключить основные правила eslint, настроитьplugin:react/recommendedВключите поддержку семантики реагирования.

официальный китайский сайт eslint:cn.eslint.org
плагин eslint-plugin-реагировать:GitHub.com/Yanni Passenger Access/О…

Вход

Напишите компонент входа App.jsx

import React from "react";
import "./assets/app.css";

class Root extends React.Component {
  render() {
    return (
      <div>
        <div className="title">This is a react ssr demo</div>
        <ul className="nav">
          <li>Bar</li>
          <li>Baz</li>
          <li>Foo</li>
          <li>TopList</li>
        </ul>
        <div className="view">
        </div>
      </div>
    );
  }
}

export default Root;

Получить корневой компонент на клиентском портале и смонтировать его

entry-client.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.hydrate(<App />, document.getElementById("app"));

React16 предоставляет функциюhydrate(), используется для замены рендера при рендеринге на стороне сервера, hydrate не будет патчить dom а только патчит текст, если текст отличается, используйте текстовый контент клиента

Запись на стороне сервера может быть компонентом приложения module.exports.

entry-server.js

import React from "react";
import App from "./App";

module.exports = <App/>;

Используйте экспресс в server.js для запуска службы и обработки любых запросов на получение. Получите корневой компонент из упакованного js на сервере, прочитайте упакованный шаблон index.html, сопоставьте dist с каталогом экспресс-статических ресурсов и используйте/distв качестве префикса URL (и в конфигурации клиентского пакетаoutput.publicPathбыть последовательным). React предоставляет функцию renderToString() для рендеринга на стороне сервера, которая используется для рендеринга компонента в строку html, вызова этой функции для передачи корневого компонента и замены возвращенной строки html заполнителем в шаблоне.

server.js

const express = require("express");
const fs = require("fs");
const path = require("path");
const ReactDOMServer = require("react-dom/server");
const app = express();

let serverEntry = require("../dist/entry-server");
let template = fs.readFileSync("./dist/index.html", "utf-8");

// 静态资源映射到dist路径下
app.use("/dist", express.static(path.join(__dirname, "../dist")));

app.use("/public", express.static(path.join(__dirname, "../public")));

/* eslint-disable no-console */
const render = (req, res) => {
  console.log("======enter server======");
  console.log("visit url: " + req.url);

  let html = ReactDOMServer.renderToString(serverEntry);
  let htmlStr = template.replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>`);
  // 将渲染后的html字符串发送给客户端
  res.send(htmlStr);
}

app.get("*", render);

app.listen(3000, () => {
  console.log("Your app is running");
});

index.html перед упаковкой выглядит следующим образом

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <link rel="shortcut icon" href="/public/favicon.ico">
    <title>React SSR</title>
</head>
<body>
    <!--react-ssr-outlet-->
</body>
</html>

бегать

Пишите скрипты в package.json

"scripts": {
    "start": "node src/server.js",
    "build": "rimraf dist && npm run build:client && npm run build:server",
    "build:client": "webpack --config config/webpack.config.client.js",
    "build:server": "webpack --config config/webpack.config.server.js"
}

беги первымnpm run buildУпакуйте клиент и сервер, затем запуститеnpm run startЗапустите службу, откройте браузер и введитеhttp://localhost:3000. Откройте сеть браузера, чтобы просмотреть содержимое, возвращаемое сервером.Вы можете видеть, что это окончательный отображаемый HTML-контент.

Горячее обновление среды разработки

Есть некоторые проблемы с запуском вышеописанным способом, каждый раз при смене кода нужно упаковывать клиент и сервер, а потом перезапускать сервис. При перезапуске сервера необходимо просмотреть измененный код, чтобы он вступил в силу, что сильно влияет на эффективность разработки и опыт работы в режиме разработки. В одностраничных приложениях React предоставляет создание шаблонов create-react-app, которое внутренне использует webpack-dev-server в качестве службы среды разработки для поддержки горячих обновлений. Некоторые люди будут использовать скаффолдинг в качестве клиента, а затем использовать экспресс или коа в качестве сервера. Таким образом, клиент и сервер занимают два служебных порта, которые нельзя использовать совместно. Если клиент не использует скаффолдинг для использования веб-пакета для упаковки и добавления функции просмотра служба Терминал использует упакованные ресурсы в качестве сопоставления ресурсов Хотя сервер и клиент совместно используют одну и ту же службу, браузер нельзя обновить Лучше использовать webpack-dev-middleware и webpack промежуточное ПО. Промежуточное ПО webpack-dev-middleware не будет записывать упакованные ресурсы на диск, а будет обрабатывать их в памяти.При изменении содержимого файла он будет перекомпилирован.Промежуточное ПО webpack-hot-middleware используется для горячих обновлений.

Сначала измените package.json

"scripts": {
    "dev": "node src/server.js",
    "start": "cross-env NODE_ENV=production node src/server.js",
    "build": "rimraf dist && npm run build:client && npm run build:server",
    "build:client": "cross-env NODE_ENV=production webpack --config config/webpack.config.client.js",
    "build:server": "cross-env NODE_ENV=production webpack --config config/webpack.config.server.js"
}

Измените клиентские и серверные сценарии упаковки на рабочую среду и добавьте идентификатор рабочей среды в сценарий запуска службы. существуетserver.jsВ текущей среде определяется, является ли производственная среда, чтобы сохранить исходную логику производственной среды, используя непроизводственную среду webpack-dev-middleware и тепловое обновление webpack-hot-middleware.

const isProd = process.env.NODE_ENV === "production";

let serverEntry;
let template;
if (isProd) {
  serverEntry = require("../dist/entry-server");
  template = fs.readFileSync("./dist/index.html", "utf-8");
  // 静态资源映射到dist路径下
  app.use("/dist", express.static(path.join(__dirname, "../dist")));
} else {
  require("./setup-dev-server")(app, (entry, htmlTemplate) => {
    serverEntry = entry;
    template = htmlTemplate;
  });
}

Приведенный выше код вызываетsetup-dev-server.jsВ функцию из module.exports передайте объект приложения экспресс-экземпляра и функцию обратного вызова после успешной упаковки. существуетsetup-dev-server.js, клиент использует webpack-dev-middleware и webpack-hot-middleware, а сервер использует webpack для упаковки и просмотра

webpack-dev-middlewareПримечание: webpack-dev-middleware использует версию 1.x.


[webpack-hot-middleware](https://www.npmjs.com/package/webpack-hot-middleware)

клиент

Прежде чем использовать функцию webpack для упаковки, добавьте в запись webpack-hot-middleware/client, а затем добавьте подключаемый модуль HotModuleReplacementPlugin (этот подключаемый модуль используется для включения горячих обновлений). ДатьclientCompilerОбъект устанавливает обратный вызов после завершения упаковки.Программное обеспечение webpack-dev-middleware упаковывается в память.Ему необходимо прочитать index.html из файловой системы, а затем вызвать входящую функцию обратного вызова для передачи шаблона.

const webpack = require("webpack");
const clientConfig = require("../config/webpack.config.client");

// 修改入口文件,增加热更新文件
clientConfig.entry.app = ["webpack-hot-middleware/client", clientConfig.entry.app];
clientConfig.output.filename = "static/js/[name].[hash].js";
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin());

// 客户端打包
const clientCompiler = webpack(clientConfig);

const devMiddleware = require("webpack-dev-middleware")(clientCompiler, {
publicPath: clientConfig.output.publicPath,
noInfo: true
});
// 使用webpack-dev-middleware中间件服务webpack打包后的资源文件
app.use(devMiddleware);

clientCompiler.plugin("done", stats => {
  const info = stats.toJson();
  if (stats.hasWarnings()) {
    console.warn(info.warnings);
  }
    
  if (stats.hasErrors()) {
    console.error(info.errors);
    return;
  }
  // 从webpack-dev-middleware中间件存储的内存中读取打包后的inddex.html文件模板
  template = readFile(devMiddleware.fileSystem, "index.html");
  update();
});

// 热更新中间件
app.use(require("webpack-hot-middleware")(clientCompiler));

Сервер

webpack можно упаковать не только на диск, но и в пользовательские файловые системы, такие как файловые системы в памяти, используяoutputFileSystem Свойство указывает файловую систему для упакованного вывода. При упаковке сервера используется функция watch для обнаружения изменений в файле.После завершения упаковки из памяти также берется содержимое файла entry-server.js.Здесь считывается строка, иReactDOMServer.renderToString(serverEntry)Входящий является компонентным объектом, нам нужно использовать узел вmoduleСкомпилируйте, создайте экземпляр вызова модуля_compileметод, первый параметр — это код javascript, второе пользовательское имя и, наконец, получитьentry-server.jsОбъект из module.exports

const webpack = require("webpack");
const MFS = require("memory-fs");
const serverConfig = require("../config/webpack.config.server");

// 监视服务端打包入口文件,有更改就更新
const serverCompiler = webpack(serverConfig);
// 使用内存文件系统
const mfs = new MFS();
serverCompiler.outputFileSystem = mfs;
serverCompiler.watch({}, (err, stats) => {
  const info = stats.toJson();
  if (stats.hasWarnings()) {
    console.warn(info.warnings);
  }

  if (stats.hasErrors()) {
    console.error(info.errors);
    return;
  }

  // 读取打包后的内容并编译模块
  const bundle = readFile(mfs, "entry-server.js");
  const m = new module.constructor();
  m._compile(bundle, "entry-server.js");
  serverEntry = m.exports;
  update();
});
module.exports = function setupDevServer(app, callback) {
  let serverEntry;
  let template;
  const update = () => {
    if (serverEntry && template) {
      callback(serverEntry, template);
    }
  }
  ...
}

Упаковка и синхронизация доступа

бегатьnpm run devПосле того, как вывод терминала будет следующим, откройте браузер для доступаhttp://localhost:3000

E:\react-ssr>npm run dev

> react-ssr@1.0.0 dev E:\react-ssr
> node src/server.js

Your app is running

В это время произойдет следующая ошибка

E:\react-ssr>npm run dev

> react-ssr@1.0.0 dev E:\react-ssr
> node src/server.js

Your app is running
======enter server======
visit url: /
TypeError: Cannot read property 'replace' of undefined
    at render (E:\react-ssr\src\server.js:32:26)
    at Layer.handle [as handle_request] (E:\react-ssr\node_modules\express\lib\r
outer\layer.js:95:5)
    at next (E:\react-ssr\node_modules\express\lib\router\route.js:137:13)
    at Route.dispatch (E:\react-ssr\node_modules\express\lib\router\route.js:112
:3)
    at Layer.handle [as handle_request] (E:\react-ssr\node_modules\express\lib\r
outer\layer.js:95:5)
    at E:\react-ssr\node_modules\express\lib\router\index.js:281:22
    at param (E:\react-ssr\node_modules\express\lib\router\index.js:354:14)
    at param (E:\react-ssr\node_modules\express\lib\router\index.js:365:14)
    at Function.process_params (E:\react-ssr\node_modules\express\lib\router\ind
ex.js:410:3)

Код, где находится проблемная строка

let htmlStr = template.replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>`);

Через некоторое время вывод выглядит следующим образом

    ...
    at next (E:\react-ssr\node_modules\express\lib\router\index.js:275:10)
    at middleware (E:\react-ssr\node_modules\webpack-hot-middleware\middleware.j
s:37:48)
    at Layer.handle [as handle_request] (E:\react-ssr\node_modules\express\lib\r
outer\layer.js:95:5)
    at trim_prefix (E:\react-ssr\node_modules\express\lib\router\index.js:317:13
)
    at E:\react-ssr\node_modules\express\lib\router\index.js:284:7
    at Function.process_params (E:\react-ssr\node_modules\express\lib\router\ind
ex.js:335:12)
    at next (E:\react-ssr\node_modules\express\lib\router\index.js:275:10)
webpack built 545c3865aff0cdac2a64 in 3570ms

webpack built 545c3865aff0cdac2a64 in 3570msУказывает, что веб-пакет был упакован

Это связано с тем, что webpack упаковывает клиент и сервер асинхронно, а функция обратного вызова вызывается после завершения пакета.templateНазначение, экспресс-сервис запущен в процессе упаковки, при обращении к серверуtemplateне определено. Чтобы синхронизировать запросы браузера и синхронизацию пакетов webpack, используйтеPromise

setup-dev-server.js

module.exports = function setupDevServer(app, callback) {
  let serverEntry;
  let template;
  let resolve;
  const readyPromise = new Promise(r => { resolve = r });
  const update = () => {
    if (serverEntry && template) {
      callback(serverEntry, template);
      resolve(); // resolve Promise让服务端进行render
    }
  }
  
  ...
  
  return readyPromise;
}

Сначала создайте экземпляр Promise,resolveприсвоение функции внешней переменнойresolve, и, наконец, вернутьсяreadyPromise. Вызывается в функции обратного вызоваresolveСделайте обещание выполненным

server.js

let serverEntry;
let template;
let readyPromise;
if (isProd) {
  serverEntry = require("../dist/entry-server");
  template = fs.readFileSync("./dist/index.html", "utf-8");
  // 静态资源映射到dist路径下
  app.use("/dist", express.static(path.join(__dirname, "../dist")));
} else {
  readyPromise = require("./setup-dev-server")(app, (entry, htmlTemplate) => {
    serverEntry = entry;
    template = htmlTemplate;
  });
}
app.get("*", isProd ? render : (req, res) => {
  // 等待客户端和服务端打包完成后进行render
  readyPromise.then(() => render(req, res));
});

получение экспресс-запроса на получение, когда состояние вызывается только readyPromise становится выполненнымrenderфункция.

Написать код горячего обновления

бегатьnpm run dev, доступ через браузерhttp://localhost:3000, откройте сетевую панель вашего браузера

Видетьhttp://localhost:3000/__webpack_hmr запрос и [HMR] в консолиозначает, что горячее обновление вступило в силу, но возможно ли горячее обновление сейчас? Давайте попробуем. ОткрытьApp.jsxИсправлять<div className="title">This is a react ssr demo</div>для<div className="title">This is updated title</div>, см. следующий вывод в терминале

...
webpack building...
webpack built 6d23c952cd6c3bf01ed6 in 299ms

Я не вижу никаких изменений на странице браузера, но вижу предупреждение в панели консоли

После перепаковки сервера в браузер отправляется уведомление Браузер получил уведомление, но обновленный модуль./src/App.jsxОбновления не внедряются, поэтому горячие обновления невозможны.

Плагин webpack-hot-middleware просто создает мост для связи между браузером и сервером.Клиент будет уведомлен, когда сервер изменится.На самом деле, горячее обновление это не то, что делает этот плагин, его нужно использоватьwebpack's HMR APIнаписать код горячего обновления

Инструкции по горячему обновлению Webpack

Веб-пакет. Просто .org/concepts/ Хорошо…
Веб-пакет Just.org/guides/hot-…

На самом деле, многие плагины-загрузчики webpack сами реализуют горячие обновления.Ниже приведена часть исходного кода плагина style-loader

style-loader/index.js

var hmr = [
	// Hot Module Replacement,
	"if(module.hot) {",
	// When the styles change, update the <style> tags
	"	module.hot.accept(" + loaderUtils.stringifyRequest(this, "!!" + request) + ", function() {",
	"		var newContent = require(" + loaderUtils.stringifyRequest(this, "!!" + request) + ");",
	"",
	"		if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];",
	"",
	"		var locals = (function(a, b) {",
	"			var key, idx = 0;",
	"",
	"			for(key in a) {",
	"				if(!b || a[key] !== b[key]) return false;",
	"				idx++;",
	"			}",
	"",
	"			for(key in b) idx--;",
	"",
	"			return idx === 0;",
	"		}(content.locals, newContent.locals));",
	"",
	// This error is caught and not shown and causes a full reload
	"		if(!locals) throw new Error('Aborting CSS HMR due to changed css-modules locals.');",
	"",
	"		update(newContent);",
	"	});",
	"",
	// When the module is disposed, remove the <style> tags
	"	module.hot.dispose(function() { update(); });",
	"}"
].join("\n");

мы вentry-client.jsнаписать код горячего обновления вApp.jsxКак запись зависимости горячего обновления

// 热更新
if (module.hot) {
  module.hot.accept("./App.jsx", () => {
    const NewApp = require("./App").default;
    ReactDOM.hydrate(<NewApp />, document.getElementById("app"));
  });
}

Открой сейчасApp.jsxИсправлять<div className="title">This is a react ssr demo</div>для<div className="title">This is updated title</div>, браузер автоматически обновляет содержимое страницы

Суммировать

В этом разделе описываются конфигурации на стороне клиента и на стороне сервера при связывании с webpack. Представлено, как использовать webpack в сочетании с экспрессом для горячего обновления и как использовать HMR API webpack для горячего обновления.

Исходный код этой главы

Следующий раздел:Изоморфизм внешней и внутренней маршрутизации