在同构 React 应用程序中渲染 HTML 字符串
Render HTML string in isomorphic React app
存在非 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 中解决这个问题?
您可以使用 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()
通过解析 html 字符串并将生成的节点转换为 React 元素,
Sanitized 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 或适合您的用例的内容。
确保客户端包获得完全相同的 App
组件,否则您可能会 React 的客户端脚本重新创建 DOM 树,您将失去所有好处服务器端渲染。
您也可以通过上述方法利用 React 16 的流式传输。
解决道具问题
道具将作为属性从树中提供给您,并且可以作为道具传递(当然要仔细考虑您的用例)。
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
)
);
不过要小心自定义属性 - 您可能需要遵循 this rfc。如果可能,坚持使用驼峰命名法。
存在非 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 中解决这个问题?
您可以使用 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()
通过解析 html 字符串并将生成的节点转换为 React 元素,
Sanitized 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 或适合您的用例的内容。
确保客户端包获得完全相同的 App
组件,否则您可能会 React 的客户端脚本重新创建 DOM 树,您将失去所有好处服务器端渲染。
您也可以通过上述方法利用 React 16 的流式传输。
解决道具问题
道具将作为属性从树中提供给您,并且可以作为道具传递(当然要仔细考虑您的用例)。
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
)
);
不过要小心自定义属性 - 您可能需要遵循 this rfc。如果可能,坚持使用驼峰命名法。