在同构 React 应用程序中渲染 HTML 字符串

IT技术 javascript node.js reactjs isomorphic-javascript
2021-04-04 20:22:20

有一个非 SPA 场景,使用经过消毒但随机的 HTML 字符串作为输入:

<p>...</p>
<p>...</p>
<gallery image-ids=""/>
<player video-id="..."/>
<p>...</p>

该字符串源自 WYSIWYG 编辑器,包含嵌套的常规 HTML 标记和数量有限的应呈现给小部件的自定义元素(组件)。

目前,像这样的 HTML 片段应该在服务器端 (Express) 单独呈现,但最终也会作为同构应用程序的一部分在客户端呈现。

我打算使用 React(或类似 React 的框架)来实现组件,因为它大概适合这种情况——它是同构的并且可以很好地呈现部分。

问题是子串像

<gallery image-ids="[1, 3]"/>

应该成为

<Gallery imageIds={[1, 3]}/>

JSX/TSX 组件在某些时候,我不确定这样做的正确方法是什么,但我希望它是一项常见的任务。

这种情况如何在 React 中解决?

2个回答

通过解析 html 字符串并将结果节点转换为 React 元素,可以将清理过的 HTML 变成可以在服务器和客户端上运行的 React 组件。

const React = require('react');
const ReactDOMServer = require('react-dom/server');

const str = `<div>divContent<p> para 1</p><p> para 2</p><gallery image-ids="" /><player video-id="" /><p> para 3</p><gallery image-ids="[1, 3]"/></div>`;


var parse = require('xml-parser');

const Gallery = () => React.createElement('div', null, 'Gallery comp');
const Player = () => React.createElement('div', null, 'Player comp');

const componentMap = {
  gallery: Gallery,
  player: Player
};


const traverse = (cur, props) => {
  return React.createElement(
    componentMap[cur.name] || cur.name,
    props,
    cur.children.length === 0 ? cur.content: Array.prototype.map.call(cur.children, (c, i) => traverse(c, { key: i }))
  );
};

const domTree = parse(str).root;
const App = traverse(
   domTree
);

console.log(
  ReactDOMServer.renderToString(
    App
  )
);

但是请注意,正如您所提到的,您真正需要的并不是 JSX/TSX,而是用于 React 渲染器的 React 节点树(在本例中为 ReactDOM)。JSX 只是语法糖,除非您想在代码库中维护 React 输出,否则不需要来回转换它。

请原谅过度简化的 html 解析。它仅用于说明目的。您可能希望使用更符合规范的库来解析输入 html 或适合您的用例的内容。

确保客户端 bundle 获得完全相同的App组件,否则你可能 React 的客户端脚本会重新创建 DOM 树,你将失去服务器端渲染的所有好处。

您也可以通过上述方法利用 React 16 的流式传输。

解决props问题

props将作为属性从树中提供给您,并且可以作为props传递(当然要仔细考虑您的用例)。

const React = require('react');
const ReactDOMServer = require('react-dom/server');

const str = `<div>divContent<p> para 1</p><p> para 2</p><gallery image-ids="" /><player video-id="" /><p> para 3</p><gallery image-ids="[1, 3]"/></div>`;


var parse = require('xml-parser');

const Gallery = props => React.createElement('div', null, `Gallery comp: Props ${JSON.stringify(props)}`);
const Player = () => React.createElement('div', null, 'Player comp');

const componentMap = {
  gallery: Gallery,
  player: Player
};

const attrsToProps = attributes => {
  return Object.keys(attributes).reduce((acc, k) => {

    let val;
    try {
      val = JSON.parse(attributes[k])
    } catch(e) {
      val = null;
    }

    return Object.assign(
      {},
      acc,
      { [ k.replace(/\-/g, '') ]: val }
    );
  }, {});
};


const traverse = (cur, props) => {

  const propsFromAttrs = attrsToProps(cur.attributes);
  const childrenNodes = Array.prototype.map.call(cur.children, (c, i) => {

    return traverse(
      c,
      Object.assign(
        {},
        {
          key: i
        }
      )
    );
  });

  return React.createElement(
    componentMap[cur.name] || cur.name,
      Object.assign(
        {},
        props,
        propsFromAttrs
      ),
    cur.children.length === 0 ? cur.content: childrenNodes
  );
};

const domTree = parse(str).root;
const App = traverse(
  domTree
);

console.log(
  ReactDOMServer.renderToString(
    App
  )
);

不过要小心自定义属性 - 您可能想要遵循这个 rfc如果可能的话,坚持使用驼峰式命名法。

我比我自己更喜欢这个答案。:)
2021-06-01 20:22:20
谢谢,这就是我正在寻找的东西。对允许的组件集的限制导致更容易遍历,这很棒。
2021-06-13 20:22:20
谢谢:) 用道具更新我的答案
2021-06-21 20:22:20

您可以使用 Babel 的 API 将字符串转换为可执行的 JavaScript。

如果你放弃自定义组件约定,你可以让你的生活更轻松<lovercase>,因为在 JSX 中它们被视为 DOM 标签,所以如果你能让你的用户使用<Gallery>而不是<gallery>你会避免很多麻烦。

为您创建了一个有效(但丑陋)的CodeSandbox这个想法是使用 Babel 将 JSX 编译为代码,然后评估该代码。不过要小心,如果用户可以编辑它,他们肯定可以注入恶意代码!

JS代码:

import React from 'react'
import * as Babel from 'babel-standalone'
import { render } from 'react-dom'

console.clear()

const state = {
  code: `
  Hey!
  <Gallery hello="world" />
  Awesome!
`
}


const changeCode = (e) => {
  state.code = e.target.value
  compileCode()
  renderApp()
}

const compileCode = () => {
  const template = `
function _render (React, Gallery) {
  return (
    <div>
    ${state.code}
    </div>
  )
}
`
  state.error = ''
  try {
    const t = Babel.transform(template, {
      presets: ['react']
    })

    state.compiled = new Function(`return (${t.code}).apply(null, arguments);`)(React, Gallery)  
  } catch (err) {
    state.error = err.message
  }
}

const Gallery = ({ hello }) =>
  <div>Here be a gallery: {hello}</div>

const App = () => (
  <div>
    <textarea style={{ width: '100%', display: 'block' }} onChange={changeCode} rows={10} value={state.code}></textarea>
    <div style={{ backgroundColor: '#e0e9ef', padding: 10 }}>
    {state.error ? state.error : state.compiled}
    </div>
  </div>
)


const renderApp = () =>
  render(<App />, document.getElementById('root'));

compileCode()
renderApp()
是的,这需要使用 eval,因此对于当前形式的用户输入是不切实际的。你能解释一下你所说的“更容易”是什么意思吗?无论如何<Gallery>都会是一个字符串,不是吗?
2021-05-24 20:22:20
您将不得不检查 AST 中的函数调用或其他缓解方法,实际上没有其他方法。关于<Gallery>,不,JSX 转换为 ,React.createElement(Gallery, ...)<gallery>转换为React.createElement('gallery', ...).
2021-05-24 20:22:20
我不知道我怎样才能让我的生活更轻松。编译后的 HTML 是从客户端检索并存储在 DB 中的字符串。它只能是一个字符串。因此这个问题。
2021-06-04 20:22:20
当然可以,但该字符串可能包含恶意 JS。关于“更简单”,我的意思是 React 不会理解,'gallery'因为它不是原生 DOM 元素,areasGallery将被视为需要在范围内的自定义组件(因此它传递给函数的原因)。如果您想使用gallery,则必须将其每次出现都替换为Gallery
2021-06-07 20:22:20
如果它始终是纯 HTML,则可以使用 HTML 解析器将 HTML 转换为实际的 JSX,然后可以使用此答案中的方法对其进行编译。这使您可以根据需要转换属性和标签名称。
2021-06-10 20:22:20