解决添加 html+script 符串,script不运行的问题

460 阅读2分钟
  • 记一次 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 内部的内容单独取出来,运行一下。(此刻我认为问题一定会解决)
  • 但是控制台却抛出以下错误

image.png

  • 仔细读一下生成的 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 标签的内容能运行,希望看到的同学能帮我解决这个疑惑。