Поля смешивания реагирующего шлема во время renderStatic

Я выполняю рендеринг приложения React на стороне сервера. Я использую express для этих целей. Весь код рендеринга на стороне сервера выглядит так:

import * as React from "react"
import * as ReactDOMServer from "react-dom/server"
import * as express from "express"
import { StaticRouter } from "react-router-dom"
import walker = require("react-tree-walker")
import { useStaticRendering } from "mobx-react"

import Helmet from "react-helmet"
import Provider from "../src/Provider"
import { StaticRouterContext } from "react-router"
import version = require("../version")

var _template: string = require(`../dist/${version.v()}/index.html`)

interface IRenderResponse {
    statusCode: number,
    template: string,
    redirect?: string
}

const run = (url: string, locale?: string): Promise<IRenderResponse> => {
    var template: string
    var html: string = ""
    var head: object
    var context: StaticRouterContext = {}

    useStaticRendering(true)

    var routing = (
        <StaticRouter 
            location={url} 
            context={context}
        >
            <Provider defaultLocale={locale} />
        </StaticRouter>
    )

    return new Promise((resolve) => {
        walker(routing, (element, instance) => {
            if (instance && typeof instance._prepare == typeof (() => {}))
                return instance._prepare()
        }).then(() => {
            html = ReactDOMServer.renderToString(routing)
            head = Helmet.renderStatic()
            template = _template.replace(/\${__rh-([a-z]+)}/gi, (match, group) => {
                return head[group].toString()
            })
            template = template.replace(/\${__body}/gi, (match) => {
                return html
            })
            if (context.url)
                context["statusCode"] = 301

            resolve({
                statusCode: context["statusCode"] || 200, 
                template, 
                redirect: context.url
            })
        }).catch((error) => {
            template = _template.replace(/\${__rh-([a-z]+)}/gi, "")
            template = template.replace(/\${__body}/gi, error.stack || error.toString())
            resolve({
                statusCode: 500, 
                template
            })
        })
    })
}

var app = express()

app.get("*", (req, res) => {
    var accepted = req.acceptsLanguages()
    var locale = accepted ? (accepted[0] || "ru").split("-")[0] : "ru"
    run(req.originalUrl, locale).then((data) => {
        if (data.redirect)
            res.redirect(data.redirect)
        else
            res.status(data.statusCode).send(data.template)
    })
})

app.listen(1239)

Вы можете видеть, что здесь используется react-tree-walker. Но эта проблема возникает независимо от того, что я использую для рендеринга на стороне сервера.

Проблема в том, что если мой node-js сервер работает в одном потоке, то если одновременно выполняются два разных запроса, тогда react-helmet смешивает поля. Например, если есть два представления:

class FirstView extends React.Component {
    render() {
        return (
            <Helmet>
                <title>This is first title</title>
                <meta name="description" content="My first view description" />
            </Helmet>
        )
    }
}

и

class SecondView extends React.Component {
    render() {
        return (
            <Helmet>
                <title>This is second title</title>
                <meta name="description" content="My second view description" />
            </Helmet>
        )
    }
}

тогда я могу получить голову примерно так:

<title>This is first title</title>
<meta name="description" content="My second view description" />

Очевидно, это происходит из-за того, что react-helmet использует статические поля, я полагаю. Таким образом, если два запроса обрабатываются параллельно, эти поля меняются хаотично.

Как я могу это победить? Эта проблема часто возникает в проектах с высокой нагрузкой, и это может привести к сбою SEO, поскольку поисковый робот может получать неверные данные.


webpack.config.js:

var webpack = require("webpack")

var config = {
    mode: "production",
    target: "node",
    entry: __dirname + "/index.tsx",
    output: {
        path: __dirname,
        filename: `index.js`
    },
    module: {
        rules: [
            {
                test: require.resolve("phoenix"),
                use: "imports-loader?window=>global"
            },
            {
                test: /\.tsx?$/,
                loader: "awesome-typescript-loader?configFileName=tsconfig.server.json",
                exclude: /node_modules/
            },
            {
                test: /\.js$/,
                enforce: "pre",
                loader: "source-map-loader"
            },
            {
                test: /\.html$/,
                loader: "html-loader"
            },
            {
                test: /\.(svg|woff|woff2|ttf|otf|png|jpg)$/,
                loader: "url-loader"
            },
            {
                test: /\.(css|sass)$/,
                loader: "ignore-loader"
            }
        ]
    },
    resolve: {
        modules: [
            "node_modules",
            `${__dirname}/../src`
        ],
        extensions: [".js", ".jsx", ".sass", ".json", ".css", ".ts", ".tsx"]
    },
    parallelism: 2,
    plugins: [
        new webpack.DefinePlugin({
            "ENV": JSON.stringify("server"),
            "process.env.ENV": JSON.stringify("server"),
            "process.env.VERSION": JSON.stringify("n/a"),
            "process.env.NODE_ENV": JSON.stringify("production"),
            "global.GENTLY": false
        })
    ]
}

module.exports = config

Редактировать

Похоже, что react-helmet небезопасно для потоков согласно этой проблеме < / а>. Есть ли возможность создать обходной путь на основе этого знания?


person Limbo    schedule 16.02.2019    source источник
comment
FirstView и SecondView на разных маршрутах?   -  person Gaurav Saraswat    schedule 18.02.2019
comment
@GauravSaraswat да, конечно   -  person Limbo    schedule 18.02.2019
comment
Я сомневаюсь, что _1 _ .. / dist / $ {version.v ()} / index.html) дает вам строковый объект, который изменяет содержимое экспортированного /index.html. Можете ли вы сказать мне, какой загрузчик вы используете, чтобы требовать .html файлов   -  person Gaurav Saraswat    schedule 19.02.2019
comment
@GauravSaraswat о, я думаю, я начинаю понимать, в чем трюк. Я обновил свой вопрос с помощью webpack.config.js (я использую html-loader)   -  person Limbo    schedule 19.02.2019
comment
Я не думаю, что это должно создать какие-либо проблемы. can you try to just require the html file only once at top with imports instead of again and again in run function и assign it to template private variable inside the promise returned in run function   -  person Gaurav Saraswat    schedule 19.02.2019
comment
это помогает?   -  person Gaurav Saraswat    schedule 21.02.2019
comment
@GauravSaraswat Извините, я не могу сейчас это проверить. Скажу, когда проверят :) но мое 6-е чувство подсказывает, что ты прав   -  person Limbo    schedule 22.02.2019
comment
Конечно. Я только что обновил свой ответ ниже. дайте мне знать, сработало ли это :)   -  person Gaurav Saraswat    schedule 22.02.2019


Ответы (1)


Мне кажется, require(../dist/${version.v()}/index.html); импортирует только один экземпляр шаблона.

Повторно запрашивая шаблон во всех сетевых вызовах, вы можете обновлять одну и ту же строковую ссылку снова и снова.

так. Вы можете сделать следующее:

  1. import(require) template только один раз вверху файла.
  2. назначьте его новой переменной строкового типа внутри обещания, возвращаемого функцией run, и выполните над ним операции замены.

базовый фрагмент кода:

...
import version = require("../version");

import appTemplate = require(`../dist/${version.v()}/index.html`)


interface IRenderResponse {
    statusCode: number,
    template: string,
    redirect?: string
}

const run = (url: string, locale?: string): Promise<IRenderResponse> => {
    var template: string = "";
    var html: string = ""
    var head: object
    var context: StaticRouterContext = {}

    useStaticRendering(true)

    var routing = (
        <StaticRouter 
            location={url} 
            context={context}
        >
            <Provider defaultLocale={locale} />
        </StaticRouter>
    )

    return new Promise((resolve) => {
        walker(routing, (element, instance) => {
            if (instance && typeof instance._prepare == typeof (() => {}))
                return instance._prepare()
        }).then(() => {
            html = ReactDOMServer.renderToString(routing)
            head = Helmet.renderStatic()
            template = appTemplate.replace(/\${__rh-([a-z]+)}/gi, (match, group) => {
                return head[group].toString()
            })
            template = template.replace(/\${__body}/gi, (match) => {
                return html
            })
            if (context.url)
                context["statusCode"] = 301

            resolve({
                statusCode: context["statusCode"] || 200, 
                template, 
                redirect: context.url
            })
        })....

person Gaurav Saraswat    schedule 22.02.2019
comment
Наконец, я проверил это, и это не работает. И я думаю, что здесь нет никакого подвоха в импорте или что-то в этом роде. appTemplate импортируется, поэтому webpack загружает его как строку во время компиляции. В течение run() операции файловой системы не выполняются. Но поля все еще перемешиваются. Думаю, проблема именно в react-helmet - person Limbo; 10.05.2019
comment
И да, react-helmet небезопасно для потоков - person Limbo; 10.05.2019