首页 > 解决方案 > 在 ReactWebChat 中使用 renderMarkdown 和实际写入 DOM 之间会发生什么?

问题描述

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

为了实现这一点,我们添加了对 markdown-it 的依赖项,因为 ReactWebChat 元素可以将其作为参数,如下所述:BotFramework-Webchat Middleware for renderMarkdown 我们没有添加表情符号渲染器,而是在其中构建了一个规则,并且根据上面答案中给出的示例将其传递给 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) } />   

我们的机器人返回给用户的第一条消息发送了一个应该以“_parent”为目标的 URL。但是,它始终显示为“_blank”,而“rel”属性绝对是通过我们的自定义方法设置的。对我来说,这证实了我们的自定义规则正在起作用,但发生了一些奇怪的事情。我已经调试了会发生什么,并且呈现的 HTML,包括“目标”属性会在一段时间内保持正确的值,但最终会切换到“_blank”。后来的消息都正确呈现了它们的目标,我已将打开活动中的 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"
}

标签: reactjsbotframeworkweb-chatmarkdown-it

解决方案


由于网络聊天会将所有卡片转换为自适应卡片,因此您需要使用自适应卡片解决此问题。您可以在此处看到,Web Chat 使用的 Adaptive Cards SDK 在应用 Markdown后将所有锚点转换为“_blank” 。

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;
        }
    }
}

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

方案一:拦截点击事件

关于如何在网络聊天中为自适应卡片渲染器实现自定义处理程序,您需要阅读一些内容:BotFramework-WebChat - 自适应卡片

首先要了解的是,Web Chat 使用Adaptive Cards JavaScript SDK,它以 npm 包的形式提供。网络聊天主要使用 SDK 的开箱即用呈现功能,但它改变的一件重要事情是如何处理操作。如果不提供自定义处理程序,提交操作将不会发送到机器人。

adaptiveCard.onExecuteAction = handleExecuteAction;

这就是应用程序应该如何使用自适应卡的方式。虽然大部分功能都是在 SDK 端处理的,但应用程序需要做一些事情才能使自适应卡片适用于该特定应用程序。虽然您可以看到 Web Chat 将函数分配给onExecuteAction特定 Adaptive Card 实例的“事件”属性,但也有一个静态对应物onExecuteAction可以像这样访问:

AdaptiveCard.onExecuteAction = handleExecuteAction;

使用静态事件将为所有自适应卡片应用一个处理程序,而不仅仅是一个,但它会被应用于特定实例的任何处理程序覆盖。我告诉你这个的原因是因为有更多的静态事件,并且有一些特别对你的情况有用:

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 打开链接,那么您将需要创建自己的元素类型,因为我们可以看到文本块和文本跑步不允许你做你想做的事。如果您创建自己类型的基于文本的元素,例如文本块,那么您可以覆盖internalRender并应用您喜欢的 Markdown,而无需将目标更改为 _blank。有关此选项的更多信息,请参阅文档。请注意,在这种情况下,您需要明确使用自适应卡片来使用您的自定义元素,因为如果您给它一个英雄卡片,Web Chat 将不知道将自定义元素放入自适应卡片中。


推荐阅读