简体   繁体   中英

Render HTML string in isomorphic React app

There is non-SPA scenario with sanitized yet random HTML string as an input:

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

The string originates from WYSIWYG editor and contains nested regular HTML tags and a limited number of custom elements (components) that should be rendered to widgets.

Currently HTML snippets like this one are supposed to be rendered on server side (Express) separately but will eventually be rendered on client side too as a part of isomorphic application.

I intend use React (or React-like framework) to implement components because it presumably suits the case - it is isomorphic and renders partials well.

The problem is that substrings like

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

should become

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

JSX/TSX component at some point, and I'm not sure what is the right way to do this, but I would expect it to be a common task.

How can this case be solved in React?

Sanitized HTML can be turned into React Components that can be run both on server and client by parsing the html string and transforming the resulting nodes into React elements.

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
  )
);

Note however, it is not JSX/TSX that you really need, as you mentioned, but a tree of React Nodes for the React renderer (ReactDOM in this case). JSX is just syntactic sugar, and transforming it back and forth is unnecessary unless you want to maintain the React output in your codebase.

Pardon the over simplified html parsing. Its only for illustrative purposes. You might want to use a more spec-compliant library to parse the input html or something that fits your use case.

Make sure, the client side bundle get the exact same App component, or else you might React's client side script would re-create the DOM tree and you'll lose all the benefits of server side rendering.

You can take advantage of the React 16's streaming out too with the above approach.

Addressing the props problem

Props will be available to you from the tree as attributes and can be passed as props (on careful consideration of your use case ofcourse).

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
  )
);

Careful with custom attributes though - you might want to follow this rfc . Stick with camelCase if possible.

You can use Babel's API to transform the string into executable JavaScript.

You can make your life way easier if you ditch the <lovercase> custom component convention, because in JSX they are treated like DOM tags, so if you can make your users use <Gallery> instead of <gallery> you will save yourself from a lot of trouble.

I created a working (but ugly) CodeSandbox for you. The idea is to use Babel to compile the JSX to code, then evaluate that code. Be careful though, if users can edit this, they can surely inject malicious code!

The JS code:

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()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM