流式数据

1,418 阅读8分钟

EventStream 服务器推送技术

介绍

EventStream 是一种基于 HTTP 的服务器推送技术,允许服务器向客户端持续发送数据,实现了服务器到客户端的单向通信。

基本原理

  1. 持久连接:客户端发起一个普通的 HTTP 请求,服务器保持连接开放
  2. 流式传输:服务器可以随时通过这个连接发送数据片段
  3. 文本协议:使用简单的文本格式,每条消息由若干行组成,以两个换行符结束

技术特点

  • 单向通信:仅服务器向客户端推送数据
  • 基于 HTTP:无需特殊协议,兼容现有网络基础设施
  • 轻量级:相比 WebSocket 更简单
  • 自动重连:客户端在连接断开时会自动尝试重新连接

与相关技术对比

特性EventStream (SSE)WebSocket长轮询
通信方向单向(服务器→客户端)双向单向
协议HTTPWS/WSSHTTP
数据格式文本文本/二进制文本/JSON
自动重连
实现复杂度中高

EventStream 是构建简单实时应用的理想选择,特别是当只需要服务器向客户端推送数据的场景。

接收 Server-Sent Events (SSE) 数据,实现方式

原生 EventSource API

特点

  • 浏览器原生支持的 API
  • 最简单的实现方式
  • 自动重连机制
  • 只能发送 GET 请求
  • 不能自定义请求头

代码示例

const eventSource = new EventSource('/sse-endpoint');

eventSource.onmessage = (event) => {
  console.log('Received message:', event.data);
};

eventSource.onerror = () => {
  console.error('Connection error');
};

优点

  • 使用简单,浏览器内置支持
  • 自动处理连接和重连
  • 轻量级

缺点

  • 功能有限(不能设置请求头、只能GET)
  • 不兼容IE
  • 错误处理能力有限

使用 fetch API 实现

特点

  • 完全手动控制
  • 可以使用任何HTTP方法和请求头
  • 需要自行处理流解析和重连
  • 更灵活但更复杂

代码示例

async function connectSSE() {
  const response = await fetch('/sse-endpoint', {
    headers: {
      'Accept': 'text/event-stream'
    }
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const chunk = decoder.decode(value);
    // 手动解析SSE格式
    const lines = chunk.split('\n');
    let data = '';
    
    lines.forEach(line => {
      if (line.startsWith('data:')) {
        data = line.replace('data:', '').trim();
      }
    });
    
    if (data) {
      console.log('Received:', data);
    }
  }
}

connectSSE().catch(console.error);

优点

  • 完全控制请求和响应
  • 可以使用POST和其他HTTP方法
  • 可以设置自定义请求头

缺点

  • 实现复杂
  • 需要手动解析SSE格式
  • 没有内置重连机制
  • 需要手动处理连接状态

使用 @microsoft/fetch-event-source 库

特点

  • 结合了fetch的灵活性和EventSource的易用性
  • 支持自定义请求头和方法
  • 内置智能重连机制
  • 支持中止控制器(AbortController)

安装

npm install @microsoft/fetch-event-source

代码示例

import { fetchEventSource } from '@microsoft/fetch-event-source';

const ctrl = new AbortController();

await fetchEventSource('/sse-endpoint', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token'
  },
  body: JSON.stringify({ query: 'value' }),
  signal: ctrl.signal,
  
  onopen(response) {
    if (response.ok) {
      console.log('Connection opened');
    } else {
      throw new Error('Connection failed');
    }
  },
  
  onmessage(event) {
    console.log('Received event:', event);
  },
  
  onclose() {
    console.log('Connection closed');
  },
  
  onerror(err) {
    console.error('Error:', err);
    // 抛出错误会触发重连
    throw err;
  }
});

// 需要时中止连接
// ctrl.abort();

参数说明

参数类型描述
urlstring要连接的SSE服务的URL。
methodstringHTTP 方法,默认为 'GET',但可以设置为 'POST' 等
headersRecord<string, string>自定义请求头对象
bodyBodyInit请求体内容,可用于 POST 请求
signalAbortSignal一个AbortSignal对象,可用于中止请求。通过AbortController创建。
openWhenHiddenboolean当页面隐藏时是否保持连接
onopen(response: Response) => void连接成功建立时调用
onmessage(event: MessageEvent) => void收到消息时调用
onclose() => void连接关闭时调用
onerror(err: any) => number | void发生错误时调用,返回数字可指定重连延迟

优点

  • 兼具灵活性和易用性
  • 支持自定义请求头和方法
  • 智能重连策略
  • 良好的错误处理
  • 支持中止控制

缺点

  • 需要额外依赖
  • 包体积比原生EventSource大

三种方式对比

特性EventSourcefetch API@microsoft/fetch-event-source
浏览器原生支持✗ (需要安装)
自定义请求头
支持POST等HTTP方法
自动重连✓ (可配置)
流解析自动手动自动
错误处理能力有限完全控制强大
中止控制(AbortController)
实现复杂度简单复杂中等

showdown库

介绍

Showdown 是一个流行的 JavaScript Markdown 解析器,可以将 Markdown 文本转换为 HTML。它是一个轻量级、高效且高度可定制的库,适用于浏览器和 Node.js 环境。

核心特性

  1. 双向转换:支持 Markdown 到 HTML 的转换
  2. 高度兼容:遵循 CommonMark 标准和 GitHub Flavored Markdown (GFM)
  3. 可扩展性:支持自定义扩展和语法修改
  4. 跨平台:可在浏览器和 Node.js 环境中使用
  5. 轻量级:压缩后仅约 25KB

使用

代码示例

const showdown = require('showdown');
const converter = new showdown.Converter();
const html = converter.makeHtml('# Hello from Node.js!');
console.log(html);

配置选项

const converter = new showdown.Converter({
  // 启用表格支持 (GFM)
  tables: true,
  // 启用任务列表支持 (GFM)
  tasklists: true,
  // 启用删除线支持 (GFM)
  strikethrough: true,
  // 启用代码块语法高亮
  ghCodeBlocks: true,
  // 自动为链接添加目标属性
  openLinksInNewWindow: true,
  // 简化自动链接生成
  simplifiedAutoLink: true,
  // 排除不需要的标签
  excludeTrailingPunctuationFromURLs: true,
  // 字面量中间强调
  literalMidWordUnderscores: true,
  // 禁用强制4空格缩进
  disableForced4SpacesIndentedSublists: true,
  // 启用emoji支持
  emoji: true,
  // 自定义HTML标签白名单
  parseImgDimensions: true,
  // 允许HTML标签
  allowUnderscoresInLinks: true
});

扩展功能 Showdown 支持通过扩展来增强功能

// 1. 创建自定义扩展
const myExtension = {
  type: 'lang', // 或 'output'
  regex: /markdown/gi,
  replace: 'awesome'
};

showdown.extension('myExtension', () => [myExtension]);

// 2. 使用现有扩展
// 加载扩展
const converter = new showdown.Converter({
  extensions: ['twitter', 'github']
});

与 Vue 集成

import Vue from 'vue';
import showdown from 'showdown';

Vue.filter('markdown', (value) => {
  if (!value) return '';
  const converter = new showdown.Converter();
  return converter.makeHtml(value);
});

性能优化

  • 复用转换器实例:避免重复创建转换器
  • 预编译常用内容:对静态内容进行预转换
  • 按需加载扩展:只加载必要的扩展
  • 使用Web Worker:在浏览器中对大量内容进行后台转换

最佳实践

  • 安全考虑:使用DOMPurify等库净化HTML输出
  • 缓存结果:对静态Markdown内容缓存转换结果
  • 渐进增强:先显示原始Markdown再替换为HTML
  • 错误处理:捕获并处理转换过程中的错误

示例:完整的使用场景

import showdown from 'showdown';
import DOMPurify from 'dompurify';

// 创建配置好的转换器
function createConverter() {
  return new showdown.Converter({
    tables: true,
    tasklists: true,
    strikethrough: true,
    emoji: true,
    extensions: ['prettify'] // 使用prettify扩展
  });
}

// 安全转换函数
function safeMarkdownToHtml(markdown) {
  try {
    const converter = createConverter();
    const rawHtml = converter.makeHtml(markdown);
    return DOMPurify.sanitize(rawHtml);
  } catch (error) {
    console.error('Markdown转换错误:', error);
    return '<p>内容解析错误</p>';
  }
}

// 使用示例
const markdownContent = '# 标题\n\n这是一段**加粗**的文本。';
const cleanHtml = safeMarkdownToHtml(markdownContent);
document.getElementById('content').innerHTML = cleanHtml;

几种库对比

特性ShowdownMarkedRemarkCommonMark
大小25KB22KB较大中等
速度很快中等
GFM支持插件部分
可扩展性中等很高中等
浏览器支持主要Node
活跃度很高中等

vue-draggable-resizable可拖拽拉伸库

介绍

Vue-Draggable-Resizable 是一个 Vue.js 组件,它提供了可拖拽和可调整大小的功能,非常适合用于构建可视化编辑工具、仪表盘、布局设计器等需要元素自由定位和调整大小的场景。

主要特性

  1. 拖拽功能:允许元素在容器内自由移动
  2. 调整大小:可以从多个方向调整元素大小
  3. 限制边界:可以设置拖拽和调整大小的边界限制
  4. 网格对齐:支持网格对齐功能
  5. 比例锁定:可以锁定宽高比例进行调整
  6. 事件支持:提供丰富的事件回调

基本使用

<template>
  <div id="app">
    <VueDraggableResizable
      :w="200"
      :h="200"
      :x="0"
      :y="0"
      @dragging="onDrag"
      @resizing="onResize"
    >
      <p>可拖拽和调整大小的元素</p>
    </VueDraggableResizable>
  </div>
</template>

<script>
import VueDraggableResizable from 'vue-draggable-resizable'

export default {
  components: {
    VueDraggableResizable
  },
  methods: {
    onDrag(x, y) {
      console.log(`拖拽到位置: ${x}, ${y}`)
    },
    onResize(x, y, width, height) {
      console.log(`调整到大小: ${width}x${height}, 位置: ${x}, ${y}`)
    }
  }
}
</script>

<style>
#app {
  width: 100%;
  height: 500px;
  position: relative;
  border: 1px solid #ccc;
}
</style>

常用属性和事件

常用属性

属性类型说明
wNumber初始宽度
hNumber初始高度
xNumber初始x位置
yNumber初始y位置
minWidthNumber最小宽度
minHeightNumber最小高度
maxWidthNumber最大宽度
maxHeightNumber最大高度
draggableBoolean是否可拖拽 (默认true)
resizableBoolean是否可调整大小 (默认true)
parentBoolean是否限制在父元素内 (默认false)
gridArray网格对齐 [x, y]
lockAspectRatioBoolean锁定宽高比 (默认false)

常用事件

事件参数说明
activated-组件激活时触发
deactivated-组件取消激活时触发
drag-startx, y开始拖拽时触发
draggingx, y拖拽过程中触发
drag-endx, y拖拽结束时触发
resize-startx, y, width, height开始调整大小时触发
resizingx, y, width, height调整大小过程中触发
resize-endx, y, width, height调整大小结束时触发

xss库预防攻击

介绍

xss 是一个专门用于预防 XSS(跨站脚本攻击)的 JavaScript 库,由中国开发者开发维护,在中文社区广泛使用。它通过白名单机制对 HTML 内容进行过滤,有效防御 XSS 攻击。

核心特性

  1. 白名单机制:默认只允许安全的 HTML 标签和属性通过
  2. 高度可配置:可以自定义允许的标签和属性
  3. 上下文感知:能识别不同上下文环境(HTML、属性、CSS等)进行相应过滤
  4. 轻量高效:不依赖其他库,压缩后仅约 6KB
  5. 支持 Node.js 和浏览器:全平台适用

使用

基本过滤

const xss = require('xss');
const html = '<script>alert("xss")</script><p>安全内容</p>';
console.log(xss(html));
// 输出: <p>安全内容</p>

在 Vue 中使用

// html
<div v-html="sanitize(userContent)"></div>

// javascript
import filterXSS from 'xss';

export default {
  methods: {
    sanitize(input) {
      return filterXSS(input, {
        whiteList: {  // 自定义白名单
          a: ['href', 'title', 'target'],
          p: [],
          span: ['class']
        }
      });
    }
  }
}