Web Component用得少?在大模型中寻找使用场景

237 阅读4分钟

前言

目前各厂的大模型几乎都支持了markdown输出,虽然语法简单,但是提供给用户的UI和交互得丰富。产品经理们也各显神通。举例表格:UI得好看,交互上支持左右滑动、保存为图片、表格、复制文本、移动端还需要支持横向全屏展示。为了满足产品大大们各种奇思妙想,现在的markdown语法已经扩展了很多,支持了更多复杂的数据与交互。

推荐一个markdown文本解析器markedjs,有机会的话可以再单独聊聊它的使用

将markdown文本通过解析器后输出富文本,再前端渲染。例如:

1. **加粗** => <strong>加粗</strong>
2. # 标题1 => <h1>标题1</h1>

复杂一点的表格语法:

| 表头1 | 表头2 |
| ---- | ---- |
| 内容1 | 内容2 |

对应的富文本如下:
<table>
    <thead>
        <tr>
            <th>表头1</th>
            <th>表头2</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>内容1</td>
            <td>内容2</td>
        </tr>
    </tbody>
</table>

如何添加交互

在一个实际业务场景中,大模型答案流式输出,通过解析器处理成富文本后,前端再innerHTML渲染。

例如:如何给markdown table添加以下交互?

  • 展示表格标题
  • 展示表格生成时间
  • 与表格相关的功能按钮:点击按钮可以保存表格为图片、下载表格为excel文档、全屏展示

截屏2024-12-23 17.22.09.png

在table渲染时为它插入/拼接上DOM片段、绑定事件、渲染的时间还需要靠JS先计算出来。此时想到一个由来已久却很少使用的Web API:Web Component

例如原生video标签,开发者只需要设置src,就可以拥有播放、暂停、进度条等UI与交互功能,这些功能被封装在了video组件内部,在控制台-审查元素中无法看到组件内部结构,需要额外开启选项才可查看。

控制台设置里面开启shadow dom选项。

截屏2024-12-19 15.47.54.png

审查元素:可以看到多了#shadow-root节点,其下面的就是封装在video内部的UI。

截屏2024-12-19 15.49.12.png

对于开发者,一个自定义Web组件,在组件内实现UI与交互,即独立又可复用。Web组件原生JS即可支持,不依赖React、Vue等前端框架。一个大模型要对接多个平台输出展示,各个平台间也可复用一个Web组件避免重复开发,实现UI与交互的统一。

自定义my-table组件

1.提供html模板,实现DOM结构与样式,结合slot方便复用。

<template id='my-table'>
  <style>
    .wrapper {
      border: 1px solid #eee;
      border-radius: 4px;
    }
    .table-title {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 10px 12px;
      background: #f8f8f8;
    }
    .content {
      overflow: auto;
    }
  </style>
  <div class="wrapper">
    <div class="table-title">
      <span><slot name="title" class="title">默认标题</slot> (<span class="time"></span>)</span>
      <span class="icon" hidden>🔍</span>
    </div>
    <div class="content"><slot></slot></div>
  </div>
</template>

<my-table>
  <span slot="title">表格</span>
  <table>
    <thead>
      <tr>
        <th>表头1</th>
        <th>表头2</th>
        <th>表头3</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>内容1</td>
        <td>内容2</td>
        <td>内容3</td>
      </tr>
    </tbody>
  </table>
</my-table>

2.定义my-table组件

class MyTable extends HTMLElement {
  constructor(){
    super();
  }
  // 元素添加到文档时
  connectedCallback(){
    const template = document.getElementById('my-table');
    // 通过cloneNode复制一份模板,避免使用源模板
    const content = template.content.cloneNode(true);
    const shadow = this.attachShadow({mode: 'open'});
    shadow.appendChild(content);
  }
}
// 注册组件
customElements.define('my-table', MyTable);

3.获取组件的生成时间

某些业务场景下,组件需要二次回显。如果二次回显时再次计算当前渲染的时间是不合理的,因此time需要记录下来,通过attribute记录

class MyTable extends HTMLElement {
  constructor(){
    super();
  }
+ get time(){
+   const time = this.getAttribute('time');
+   if(time){
+     return time;
+   }
+   // 时间格式处理
+   const str = new Date().toLocaleTimeString();
+   // 记录时间,二次渲染使用
+   this.setAttribute('time', str);
+   return str;
+ }
  // 元素添加到文档时
  connectedCallback(){
    const template = document.getElementById('my-table');
    const content = template.content.cloneNode(true);
+   content.querySelector('.time').textContent = this.time;
    const shadow = this.attachShadow({mode: 'open'});
    shadow.appendChild(content);
  }
}

4.功能按钮事件绑定

class MyTable extends HTMLElement {
  constructor(){
    super();
  }
  ...
  // 元素添加到文档时
  connectedCallback(){
    const template = document.getElementById('my-table');
    const content = template.content.cloneNode(true);
    content.querySelector('.time').textContent = this.time;
    const shadow = this.attachShadow({mode: 'open'});
+   const icon = shadow.querySelector('.icon');
+   icon.onclick = () => {
+       console.log('点击icon')
+   }
    shadow.appendChild(content);
  }
}

5.折叠屏适配

例如:功能按钮是全屏展示,期望当表格水平方向展示不全时展示全屏按钮,否则隐藏按钮。

constructor(){ 
    super();
    // 注意this指向
    this.fullscreenHandler = this.fullscreenHandler.bind(this);
}
// 处理全屏按钮显示或隐藏
fullscreenHandler(){
    // 当出现横向滚动条时,展示icon,否则隐藏
    const wrapper = this.shadowRoot.querySelector('.content');
    const icon = this.shadowRoot.querySelector('.icon');
    // 有滚动条
    if(wrapper.scrollWidth > wrapper.clientWidth){
      icon.removeAttribute('hidden');
      icon.onclick = () => {
        console.log('点击icon')
      }
    } else {
      icon.setAttribute('hidden', true);
      icon.onclick = null;
    }
}
connectedCallback(){
    ...
-   const icon = shadow.querySelector('.icon');
-   icon.onclick = () => {
-       console.log('点击icon')
-   }
+   this.fullscreenHandler();
}

由于现在很多移动设备支持折叠屏了,因此在实现这种功能时要考虑到折叠屏情况。通过监听resize事件,再次计算是否展示全屏按钮

// 元素添加到文档时
connectedCallback(){
    ...
    this.fullscreenHandler();
    // 适配折叠屏切换时
    window.addEventListener('resize', this.fullscreenHandler);
}
// 元素从文档中移除时
disconnectedCallback(){
    window.removeEventListener('resize', this.fullscreenHandler);
}

6.使用my-table组件

在大模型答案流式输出中,实时通过markedjs解析器输出富文本,在输出表格时实时替换,将原表格包裹在自定义组件内。浏览器渲染这段富文本时,就可以直接看到我们添加的UI与交互。

'这是一段大模型答案<table>...</table>'
=>
'这是一段大模型答案<my-table><table>...</table></my-table>'

猛戳下方查看完整代码:

最后

Web Component浏览器天然支持,只不过接口是后面版本才开放的,在使用时也要关注兼容性问题。