- 记一次 react 项目中,在渲染阶段以字符串形式添加 html+script 的问题以及解决方案
背景
- 在搭建一个在线编辑器的项目中(效仿 jupyter),通过 webSocket 向后端发送 python code,后端返回执行结果;
- 输入代码
import altair as alt
import numpy as np
import pandas as pd
x, y = np.meshgrid(range(-5, 5), range(-5, 5))
z = x ** 2 + y ** 2
source = pd.DataFrame({'x': x.ravel(),
'y': y.ravel(),
'z': z.ravel()})
alt.Chart(source).mark_rect().encode(
x='x:O',
y='y:O',
color='z:Q'
)
- webSocket 返回的结果
{
"content": {
"data": {
"text/html": "\n<div id=\"altair-viz-c034cdf34e9b4cd2ab59a844a03ab866\"></div>\n<script type=\"text/javascript\">\n (function(spec, embedOpt){\n let outputDiv = document.currentScript.previousElementSibling;\n if (outputDiv.id !== \"altair-viz-c034cdf34e9b4cd2ab59a844a03ab866\") {\n outputDiv = document.getElementById(\"altair-viz-c034cdf34e9b4cd2ab59a844a03ab866\");\n }\n const paths = {\n \"vega\": \"https://cdn.jsdelivr.net/npm//vega@5?noext\",\n \"vega-lib\": \"https://cdn.jsdelivr.net/npm//vega-lib?noext\",\n \"vega-lite\": \"https://cdn.jsdelivr.net/npm//vega-lite@4.8.1?noext\",\n \"vega-embed\": \"https://cdn.jsdelivr.net/npm//vega-embed@6?noext\",\n };\n\n function loadScript(lib) {\n return new Promise(function(resolve, reject) {\n var s = document.createElement('script');\n s.src = paths[lib];\n s.async = true;\n s.onload = () => resolve(paths[lib]);\n s.onerror = () => reject(`Error loading script: ${paths[lib]}`);\n document.getElementsByTagName(\"head\")[0].appendChild(s);\n });\n }\n\n function showError(err) {\n outputDiv.innerHTML = `<div class=\"error\" style=\"color:red;\">${err}</div>`;\n throw err;\n }\n\n function displayChart(vegaEmbed) {\n vegaEmbed(outputDiv, spec, embedOpt)\n .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));\n }\n\n if(typeof define === \"function\" && define.amd) {\n requirejs.config({paths});\n require([\"vega-embed\"], displayChart, err => showError(`Error loading script: ${err.message}`));\n } else if (typeof vegaEmbed === \"function\") {\n displayChart(vegaEmbed);\n } else {\n loadScript(\"vega\")\n .then(() => loadScript(\"vega-lite\"))\n .then(() => loadScript(\"vega-embed\"))\n .catch(showError)\n .then(() => displayChart(vegaEmbed));\n }\n })({\"config\": {\"view\": {\"continuousWidth\": 400, \"continuousHeight\": 300}}, \"data\": {\"name\": \"data-1487c93b91dee61ec4c167e470e512f4\"}, \"mark\": \"rect\", \"encoding\": {\"color\": {\"type\": \"quantitative\", \"field\": \"z\"}, \"x\": {\"type\": \"ordinal\", \"field\": \"x\"}, \"y\": {\"type\": \"ordinal\", \"field\": \"y\"}}, \"$schema\": \"https://vega.github.io/schema/vega-lite/v4.8.1.json\", \"datasets\": {\"data-1487c93b91dee61ec4c167e470e512f4\": [{\"x\": -5, \"y\": -5, \"z\": 50}, {\"x\": -4, \"y\": -5, \"z\": 41}, {\"x\": -3, \"y\": -5, \"z\": 34}, {\"x\": -2, \"y\": -5, \"z\": 29}, {\"x\": -1, \"y\": -5, \"z\": 26}, {\"x\": 0, \"y\": -5, \"z\": 25}, {\"x\": 1, \"y\": -5, \"z\": 26}, {\"x\": 2, \"y\": -5, \"z\": 29}, {\"x\": 3, \"y\": -5, \"z\": 34}, {\"x\": 4, \"y\": -5, \"z\": 41}, {\"x\": -5, \"y\": -4, \"z\": 41}, {\"x\": -4, \"y\": -4, \"z\": 32}, {\"x\": -3, \"y\": -4, \"z\": 25}, {\"x\": -2, \"y\": -4, \"z\": 20}, {\"x\": -1, \"y\": -4, \"z\": 17}, {\"x\": 0, \"y\": -4, \"z\": 16}, {\"x\": 1, \"y\": -4, \"z\": 17}, {\"x\": 2, \"y\": -4, \"z\": 20}, {\"x\": 3, \"y\": -4, \"z\": 25}, {\"x\": 4, \"y\": -4, \"z\": 32}, {\"x\": -5, \"y\": -3, \"z\": 34}, {\"x\": -4, \"y\": -3, \"z\": 25}, {\"x\": -3, \"y\": -3, \"z\": 18}, {\"x\": -2, \"y\": -3, \"z\": 13}, {\"x\": -1, \"y\": -3, \"z\": 10}, {\"x\": 0, \"y\": -3, \"z\": 9}, {\"x\": 1, \"y\": -3, \"z\": 10}, {\"x\": 2, \"y\": -3, \"z\": 13}, {\"x\": 3, \"y\": -3, \"z\": 18}, {\"x\": 4, \"y\": -3, \"z\": 25}, {\"x\": -5, \"y\": -2, \"z\": 29}, {\"x\": -4, \"y\": -2, \"z\": 20}, {\"x\": -3, \"y\": -2, \"z\": 13}, {\"x\": -2, \"y\": -2, \"z\": 8}, {\"x\": -1, \"y\": -2, \"z\": 5}, {\"x\": 0, \"y\": -2, \"z\": 4}, {\"x\": 1, \"y\": -2, \"z\": 5}, {\"x\": 2, \"y\": -2, \"z\": 8}, {\"x\": 3, \"y\": -2, \"z\": 13}, {\"x\": 4, \"y\": -2, \"z\": 20}, {\"x\": -5, \"y\": -1, \"z\": 26}, {\"x\": -4, \"y\": -1, \"z\": 17}, {\"x\": -3, \"y\": -1, \"z\": 10}, {\"x\": -2, \"y\": -1, \"z\": 5}, {\"x\": -1, \"y\": -1, \"z\": 2}, {\"x\": 0, \"y\": -1, \"z\": 1}, {\"x\": 1, \"y\": -1, \"z\": 2}, {\"x\": 2, \"y\": -1, \"z\": 5}, {\"x\": 3, \"y\": -1, \"z\": 10}, {\"x\": 4, \"y\": -1, \"z\": 17}, {\"x\": -5, \"y\": 0, \"z\": 25}, {\"x\": -4, \"y\": 0, \"z\": 16}, {\"x\": -3, \"y\": 0, \"z\": 9}, {\"x\": -2, \"y\": 0, \"z\": 4}, {\"x\": -1, \"y\": 0, \"z\": 1}, {\"x\": 0, \"y\": 0, \"z\": 0}, {\"x\": 1, \"y\": 0, \"z\": 1}, {\"x\": 2, \"y\": 0, \"z\": 4}, {\"x\": 3, \"y\": 0, \"z\": 9}, {\"x\": 4, \"y\": 0, \"z\": 16}, {\"x\": -5, \"y\": 1, \"z\": 26}, {\"x\": -4, \"y\": 1, \"z\": 17}, {\"x\": -3, \"y\": 1, \"z\": 10}, {\"x\": -2, \"y\": 1, \"z\": 5}, {\"x\": -1, \"y\": 1, \"z\": 2}, {\"x\": 0, \"y\": 1, \"z\": 1}, {\"x\": 1, \"y\": 1, \"z\": 2}, {\"x\": 2, \"y\": 1, \"z\": 5}, {\"x\": 3, \"y\": 1, \"z\": 10}, {\"x\": 4, \"y\": 1, \"z\": 17}, {\"x\": -5, \"y\": 2, \"z\": 29}, {\"x\": -4, \"y\": 2, \"z\": 20}, {\"x\": -3, \"y\": 2, \"z\": 13}, {\"x\": -2, \"y\": 2, \"z\": 8}, {\"x\": -1, \"y\": 2, \"z\": 5}, {\"x\": 0, \"y\": 2, \"z\": 4}, {\"x\": 1, \"y\": 2, \"z\": 5}, {\"x\": 2, \"y\": 2, \"z\": 8}, {\"x\": 3, \"y\": 2, \"z\": 13}, {\"x\": 4, \"y\": 2, \"z\": 20}, {\"x\": -5, \"y\": 3, \"z\": 34}, {\"x\": -4, \"y\": 3, \"z\": 25}, {\"x\": -3, \"y\": 3, \"z\": 18}, {\"x\": -2, \"y\": 3, \"z\": 13}, {\"x\": -1, \"y\": 3, \"z\": 10}, {\"x\": 0, \"y\": 3, \"z\": 9}, {\"x\": 1, \"y\": 3, \"z\": 10}, {\"x\": 2, \"y\": 3, \"z\": 13}, {\"x\": 3, \"y\": 3, \"z\": 18}, {\"x\": 4, \"y\": 3, \"z\": 25}, {\"x\": -5, \"y\": 4, \"z\": 41}, {\"x\": -4, \"y\": 4, \"z\": 32}, {\"x\": -3, \"y\": 4, \"z\": 25}, {\"x\": -2, \"y\": 4, \"z\": 20}, {\"x\": -1, \"y\": 4, \"z\": 17}, {\"x\": 0, \"y\": 4, \"z\": 16}, {\"x\": 1, \"y\": 4, \"z\": 17}, {\"x\": 2, \"y\": 4, \"z\": 20}, {\"x\": 3, \"y\": 4, \"z\": 25}, {\"x\": 4, \"y\": 4, \"z\": 32}]}}, {\"mode\": \"vega-lite\"});\n</script>"
}
}
}
- 我需要将 content.data.text/html 的内容渲染到页面中。
- 正常的运行效果会通过运行 js,插入的 div 下生成一个图表。
遇到的问题
<div
dangerouslySetInnerHTML={{
__html: outContent?.data?.['text/html'],
}}
></div>
- 因为需要将 string 渲染到 dom 中,首选想到的是以上的这种方式
- 但是采用这种方式后,我发现插入的 script 标签的内容并没有运行
原因
- 我猜测的原因,是因为是在渲染阶段插入的 script,所以 js 内容没有运行时机,那么问题就转移到如何让 script 脚本运行了。
解决方案
- 想让脚本运行,我想到的方式就是 evel
export function evalScripts(text: string) {
var script, scripts;
scripts = [];
var regexp = /<script[^>]*>([\s\S]*?)<\/script>/gi;
while ((script = regexp.exec(text))) scripts.push(script[1]);
scripts = scripts.join("\n");
if (scripts) evel(scripts, 0);
// if (scripts) window.setTimeout(scripts, 0);
}
- 于是封装了以上的方法,将字符串中 script 内部的内容单独取出来,运行一下。(此刻我认为问题一定会解决)
- 但是控制台却抛出以下错误
- 仔细读一下生成的 js 代码,发现报错的地方是以下代码
let outputDiv = document.currentScript.previousElementSibling;
if (outputDiv.id !== "altair-viz-dd7814960c6f4b43a49063ef0975d399") {
outputDiv = document.getElementById(
"altair-viz-dd7814960c6f4b43a49063ef0975d399"
);
}
- 上边的代码之所以报错,是因为代码通过当前的 script 标签(document.currentScript)去查找相邻他的 div 了。那么我通过 evel 运行的代码肯定是无法找到 script 标签的,也就是(document.currentScript.previousElementSibling)这段代码肯定会报错。
解决报错
export function evalScripts(allText: string) {
const text = allText.replace(
"currentScript.previousElementSibling",
"currentScript?.previousElementSibling||{}"
);
var script, scripts;
scripts = [];
var regexp = /<script[^>]*>([\s\S]*?)<\/script>/gi;
while ((script = regexp.exec(text))) scripts.push(script[1]);
scripts = scripts.join("\n");
if (scripts) window.setTimeout(scripts, 0);
}
-
解决方案主要是通过 replace 将原来的"document.currentScript.previousElementSibling"替换为"document.currentScript?.previousElementSibling||{}"。
-
最后:虽然问题解决了,但是我感觉这种方案并不友好,最好的方式还是能让插入的 script 标签的内容能运行,希望看到的同学能帮我解决这个疑惑。