Render HTML string in isomorphic React app

前端 未结 2 1150
暖寄归人
暖寄归人 2020-12-09 23:12

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

...

...

&l
相关标签:
2条回答
  • 2020-12-10 00:00

    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()
    
    0 讨论(0)
  • 2020-12-10 00:10

    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.

    0 讨论(0)
提交回复
热议问题