在同构React应用程序中呈现HTMLstring

有一个非SPA的情况下消毒,但随机的HTMLstring作为input:

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

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

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

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

问题是子字符像

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

应该成为

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

JSX / TSX组件在某些时候,我不知道什么是正确的方式来做到这一点,但我希望它是一个共同的任务。

在React中如何解决这个问题?

通过parsinghtmlstring并将生成的节点转换为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输出,否则不需要它来回转换。

请原谅简化的htmlparsing。 它仅用于说明目的。 您可能需要使用更符合规范的库来parsinginput的html或符合您的用例的东西。

确保客户端软件包得到完全相同的App组件,否则React的客户端脚本可能会重新创buildDOM树,您将失去服务器端呈现的所有好处。

用上面的方法你也可以利用React 16的stream出。

解决道具问题

道具将作为属性从树中提供给你,并可作为道具传递(仔细考虑你的使用案例)。

 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 。 如果可能的话,坚持用camelCase。

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

如果您抛弃<lovercase>自定义组件约定,那么可以让您的生活更轻松 ,因为在JSX中,它们被视为DOM标记,因此如果您可以让用户使用<Gallery>而不是<gallery>可以将自己从很多麻烦。

我为你创build了一个工作(但丑陋)的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()