在使用 renderMarkdown 和实际写入 ReactWebChat 中的 DOM 之间会发生什么?

What happens between using renderMarkdown and actually writing to the DOM in ReactWebChat?

我们最近从 WebChatV3 切换到了 V4。 作为一项持续的努力,我们正在将所有自定义功能移植到新客户端。 其中一项功能是检查特定域的 URLs 并将 a 标记上的目标设置为“_parent”。

为了实现这一点,我们添加了对 markdown-it 的依赖,因为 ReactWebChat 元素可以将其作为参数,如下所述: 我们没有添加表情符号渲染器,而是在其中构建了一个规则,并按照上面答案中给出的示例将其传递给 ReactWebChat。 该代码如下所示:

export const getConfiguredMarkdownIt = () => {
    const markdownIt = new MarkdownIt.default({ html: false, xhtmlOut: true, breaks: true, linkify: true, typographer: true });
    const defaultRender = markdownIt.renderer.rules.link_open || ((tokens, idx, options, env, self) => {
        return self.renderToken(tokens, idx, options);
    });

    markdownIt.renderer.rules.link_open = (tokens, idx, options, env, self) => {
        let href = '';
        const hrefIndex = tokens[idx].attrIndex('href');
        if (hrefIndex >= 0) {
            href = tokens[idx].attrs[hrefIndex][1];
        }
        const newTarget = Helper.getTargetForUrl(href);
        const targetIndex = tokens[idx].attrIndex('target');
        if (targetIndex < 0) {
            tokens[idx].attrPush(['target', newTarget]);
        } else {
            tokens[idx].attrs[targetIndex][1] = newTarget;
        }
        const relIndex = tokens[idx].attrIndex('rel');
        const rel = 'noopener noreferrer';
        if (relIndex < 0) {
            tokens[idx].attrPush(['rel', rel]);
        } else {
            tokens[idx].attrs[relIndex][1] = rel;
        }
        console.log(tokens[idx]);
        return defaultRender(tokens, idx, options, env, self);
    };
    return markdownIt;
}

然后用于传递到 ReactWebChat 元素(为简洁起见省略了很多):

import { getConfiguredMarkdownIt } from './MarkdownSetup'
const md = getConfiguredMarkdownIt();
...
<ReactWebChat renderMarkdown={ md.render.bind(md) } />   

我们的机器人 returns 向用户发送的第一条消息 URL 应该以“_parent”为目标。 但是,它始终显示为“_blank”,而“rel”属性完全是通过我们的自定义方法设置的。 对我来说,这证实了我们的自定义规则正在运行,但发生了一些奇怪的事情。 我已经调试了发生的事情,渲染的 HTML,包括“target”属性在一段时间内保持正确的值,但最终被切换到“_blank”。 后来的消息都正确地呈现了他们的目标,我已经将开头 activity 中的 URL 替换为其中一个以查看会发生什么,结果是相同的:“_blank”。

Javascript 并不是我的专长,当我在 chrome 调试工具中单步执行代码时,我很难理解会发生什么。 但我确实设法观察到正确的 HTML 一直到 card-elements.ts。 当我到达那里时,在 isBleedingAtBottom 函数的末尾,我发现 HTML 突然在“目标”属性中包含“_blank”。 我完全不知道为什么会这样。

这是错误还是我遗漏了什么?

版本:

"botframework-webchat": "^4.7.0",
"markdown-it": "8.3.1",

这是消息的(略有修改)JSON:

{
  "type": "message",
  "serviceUrl": "http://localhost:57714",
  "channelId": "emulator",
  "from": {
    "id": "63700ba0-e2ca-11ea-8243-4773a3b07af6",
    "name": "Bot",
    "role": "bot"
  },
  "conversation": {
    "id": "63727ca0-e2ca-11ea-b639-bf8d0ffe9da8|livechat"
  },
  "recipient": {
    "id": "3952a99d-87de-4b22-a1b3-04fd8c9f141b",
    "role": "user"
  },
  "locale": "en-US",
  "inputHint": "acceptingInput",
  "attachments": [
    {
      "contentType": "application/vnd.microsoft.card.hero",
      "content": {
        "text": "Go to [this page](REMOVED URL) bla bla blah. click start to continue",
        "buttons": [
          {
            "type": "imBack",
            "title": "Start",
            "value": "Start"
          }
        ]
      }
    }
  ],
  "entities": [],
  "replyToId": "654f52f0-e2ca-11ea-b639-bf8d0ffe9da8",
  "id": "688a0b90-e2ca-11ea-b639-bf8d0ffe9da8",
  "localTimestamp": "2020-08-20T11:49:15+02:00",
  "timestamp": "2020-08-20T09:49:15.336Z"
}

因为网络聊天会将所有卡片转换为自适应卡片,您将需要使用自适应卡片解决此问题。可以看到here that the Adaptive Cards SDK that Web Chat is using converts all anchors to "_blank" after Markdown is applied.

let anchors = element.getElementsByTagName("a");

for (let i = 0; i < anchors.length; i++) {
    let anchor = <HTMLAnchorElement>anchors[i];
    anchor.classList.add(hostConfig.makeCssClassName("ac-anchor"));
    anchor.target = "_blank";
    anchor.onclick = (e) => {
        if (raiseAnchorClickedEvent(this, e.target as HTMLAnchorElement)) {
            e.preventDefault();
            e.cancelBubble = true;
        }
    }
}

我认为您有几个选项可以强制 link 使用“_parent”打开。

方案一:拦截点击事件

您需要阅读一些有关如何在网络聊天中为自适应卡片呈现器实现自定义处理程序的阅读资料:

The first thing to understand is that Web Chat uses the Adaptive Cards JavaScript SDK, available as an npm package. Web Chat mostly uses the out-of-the-box rendering functionality of the SDK, but one important thing it changes is how actions are handled. Without providing a customized handler, submit actions wouldn't be sent to the bot.

adaptiveCard.onExecuteAction = handleExecuteAction;

这就是应用程序应该如何使用自适应卡片。尽管 大部分功能都在 SDK 端处理,有一些 应用程序需要做的事情才能使自适应卡片发挥作用 那个特定的应用程序。虽然您可以看到网络聊天将功能分配给 特定自适应卡的 onExecuteAction“事件”属性 例如,还有一个 onExecuteAction 的静态对应物 可以这样访问:

AdaptiveCard.onExecuteAction = handleExecuteAction;

使用静态事件将为所有自适应卡片应用处理程序 而不是只有一个,但它将被应用的任何处理程序覆盖 到具体情况。我之所以告诉你这是因为 有many more static events,还有几个在 特别是对你的情况有用的:

static onAnchorClicked: (element: CardElement, anchor: HTMLAnchorElement) => boolean = null;
static onExecuteAction: (action: Action) => void = null;
static onElementVisibilityChanged: (element: CardElement) => void = null;
static onImageLoaded: (image: Image) => void = null;
static onInlineCardExpanded: (action: ShowCardAction, isExpanded: boolean) => void = null;
static onInputValueChanged: (input: Input) => void = null;
static onParseElement: (element: CardElement, json: any, errors?: Array<HostConfig.IValidationError>) => void = null;
static onParseAction: (element: Action, json: any, errors?: Array<HostConfig.IValidationError>) => void = null;
static onParseError: (error: HostConfig.IValidationError) => void = null;
static onProcessMarkdown: (text: string, result: IMarkdownProcessingResult) => void = null;

你可能已经猜到我们想要的事件是onAnchorClicked,我们可以这样使用:

adaptiveCardsPackage.AdaptiveCard.onAnchorClicked = (element, anchor) => {
  console.log('anchor clicked', anchor);
  
  // Since it looks like you only want to use _parent for certain links
  // you can put that logic here
  window.open(anchor.href, '_parent', 'noreferrer');
  
  // Returning true will prevent the default behavior
  return true;
}

选项 2:创建自定义元素

如果你真的想确保锚标签在检查 HTML 时看起来像你想要的那样,并且你不想用 JavaScript 打开 link 那么你将需要创建您自己的元素类型,因为我们可以看到文本块和文本运行不允许您做您想做的事情。如果您创建自己类型的 text-based 元素,例如文本块,那么您可以覆盖 internalRender 并根据需要应用 Markdown,而无需将目标更改为 _blank。有关此选项的更多信息,请参阅 docs。请注意,在这种情况下,您需要明确使用自适应卡片才能使用您的自定义元素,因为如果您给它一张英雄卡片,网络聊天将不知道将自定义元素放入自适应卡片中。