在同构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()