EventStream 服务器推送技术
介绍
EventStream 是一种基于 HTTP 的服务器推送技术,允许服务器向客户端持续发送数据,实现了服务器到客户端的单向通信。
基本原理
- 持久连接:客户端发起一个普通的 HTTP 请求,服务器保持连接开放
- 流式传输:服务器可以随时通过这个连接发送数据片段
- 文本协议:使用简单的文本格式,每条消息由若干行组成,以两个换行符结束
技术特点
- 单向通信:仅服务器向客户端推送数据
- 基于 HTTP:无需特殊协议,兼容现有网络基础设施
- 轻量级:相比 WebSocket 更简单
- 自动重连:客户端在连接断开时会自动尝试重新连接
与相关技术对比
| 特性 | EventStream (SSE) | WebSocket | 长轮询 |
|---|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向 | 单向 |
| 协议 | HTTP | WS/WSS | HTTP |
| 数据格式 | 文本 | 文本/二进制 | 文本/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();
参数说明
| 参数 | 类型 | 描述 |
|---|---|---|
| url | string | 要连接的SSE服务的URL。 |
| method | string | HTTP 方法,默认为 'GET',但可以设置为 'POST' 等 |
| headers | Record<string, string> | 自定义请求头对象 |
| body | BodyInit | 请求体内容,可用于 POST 请求 |
| signal | AbortSignal | 一个AbortSignal对象,可用于中止请求。通过AbortController创建。 |
| openWhenHidden | boolean | 当页面隐藏时是否保持连接 |
| onopen | (response: Response) => void | 连接成功建立时调用 |
| onmessage | (event: MessageEvent) => void | 收到消息时调用 |
| onclose | () => void | 连接关闭时调用 |
| onerror | (err: any) => number | void | 发生错误时调用,返回数字可指定重连延迟 |
优点:
- 兼具灵活性和易用性
- 支持自定义请求头和方法
- 智能重连策略
- 良好的错误处理
- 支持中止控制
缺点:
- 需要额外依赖
- 包体积比原生EventSource大
三种方式对比
| 特性 | EventSource | fetch API | @microsoft/fetch-event-source |
|---|---|---|---|
| 浏览器原生支持 | ✓ | ✓ | ✗ (需要安装) |
| 自定义请求头 | ✗ | ✓ | ✓ |
| 支持POST等HTTP方法 | ✗ | ✓ | ✓ |
| 自动重连 | ✓ | ✗ | ✓ (可配置) |
| 流解析 | 自动 | 手动 | 自动 |
| 错误处理能力 | 有限 | 完全控制 | 强大 |
| 中止控制(AbortController) | ✗ | ✓ | ✓ |
| 实现复杂度 | 简单 | 复杂 | 中等 |
showdown库
介绍
Showdown 是一个流行的 JavaScript Markdown 解析器,可以将 Markdown 文本转换为 HTML。它是一个轻量级、高效且高度可定制的库,适用于浏览器和 Node.js 环境。
核心特性
- 双向转换:支持 Markdown 到 HTML 的转换
- 高度兼容:遵循 CommonMark 标准和 GitHub Flavored Markdown (GFM)
- 可扩展性:支持自定义扩展和语法修改
- 跨平台:可在浏览器和 Node.js 环境中使用
- 轻量级:压缩后仅约 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;
几种库对比
| 特性 | Showdown | Marked | Remark | CommonMark |
|---|---|---|---|---|
| 大小 | 25KB | 22KB | 较大 | 中等 |
| 速度 | 快 | 很快 | 中等 | 快 |
| GFM支持 | 是 | 是 | 插件 | 部分 |
| 可扩展性 | 高 | 中等 | 很高 | 中等 |
| 浏览器支持 | 好 | 好 | 主要Node | 好 |
| 活跃度 | 高 | 高 | 很高 | 中等 |
vue-draggable-resizable可拖拽拉伸库
介绍
Vue-Draggable-Resizable 是一个 Vue.js 组件,它提供了可拖拽和可调整大小的功能,非常适合用于构建可视化编辑工具、仪表盘、布局设计器等需要元素自由定位和调整大小的场景。
主要特性
- 拖拽功能:允许元素在容器内自由移动
- 调整大小:可以从多个方向调整元素大小
- 限制边界:可以设置拖拽和调整大小的边界限制
- 网格对齐:支持网格对齐功能
- 比例锁定:可以锁定宽高比例进行调整
- 事件支持:提供丰富的事件回调
基本使用
<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>
常用属性和事件
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
w | Number | 初始宽度 |
h | Number | 初始高度 |
x | Number | 初始x位置 |
y | Number | 初始y位置 |
minWidth | Number | 最小宽度 |
minHeight | Number | 最小高度 |
maxWidth | Number | 最大宽度 |
maxHeight | Number | 最大高度 |
draggable | Boolean | 是否可拖拽 (默认true) |
resizable | Boolean | 是否可调整大小 (默认true) |
parent | Boolean | 是否限制在父元素内 (默认false) |
grid | Array | 网格对齐 [x, y] |
lockAspectRatio | Boolean | 锁定宽高比 (默认false) |
常用事件
| 事件 | 参数 | 说明 |
|---|---|---|
activated | - | 组件激活时触发 |
deactivated | - | 组件取消激活时触发 |
drag-start | x, y | 开始拖拽时触发 |
dragging | x, y | 拖拽过程中触发 |
drag-end | x, y | 拖拽结束时触发 |
resize-start | x, y, width, height | 开始调整大小时触发 |
resizing | x, y, width, height | 调整大小过程中触发 |
resize-end | x, y, width, height | 调整大小结束时触发 |
xss库预防攻击
介绍
xss 是一个专门用于预防 XSS(跨站脚本攻击)的 JavaScript 库,由中国开发者开发维护,在中文社区广泛使用。它通过白名单机制对 HTML 内容进行过滤,有效防御 XSS 攻击。
核心特性
- 白名单机制:默认只允许安全的 HTML 标签和属性通过
- 高度可配置:可以自定义允许的标签和属性
- 上下文感知:能识别不同上下文环境(HTML、属性、CSS等)进行相应过滤
- 轻量高效:不依赖其他库,压缩后仅约 6KB
- 支持 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']
}
});
}
}
}