安装

通过 npm 或 yarn 命令安装 X6。

# npm
npm install @antv/x6 --save

# yarn
yarn add @antv/x6

安装完成之后,使用 importrequire 进行引用。

import { Graph } from '@antv/x6';

缩放与平移

画布的拖拽、缩放也是常用操作,Graph 中通过 panningmousewheel 配置来实现这两个功能,鼠标按下画布后移动时会拖拽画布,滚动鼠标滚轮会缩放画布。

const graph = new Graph({

  ...,

  panning: true,

  mousewheel: true

})

添加节点

节点和边都有共同的基类 Cellopen in new window,除了从 Cell 继承属性外,还支持以下选项。

属性名类型默认值描述
xnumber0节点位置 x 坐标,单位为 px。
ynumber0节点位置 y 坐标,单位为 px。
widthnumber1节点宽度,单位为 px。
heightnumber1节点高度,单位为 px。
anglenumber0节点旋转角度。
graph.addNode({

  shape: 'rect',

  x: 100,

  y: 40,

  width: 100,

  height: 40,

})

内置节点

上面使用 shape 来指定了节点的图形,shape 的默认值为 rect。X6 内置节点与 shape 名称对应关系如下表。

构造函数shape 名称描述
Shape.Rectrect矩形。
Shape.Circlecircle圆形。
Shape.Ellipseellipse椭圆。
Shape.Polygonpolygon多边形。
Shape.Polylinepolyline折线。
Shape.Pathpath路径。
Shape.Imageimage图片。
Shape.HTMLhtmlHTML 节点,使用 foreignObject 渲染 HTML 片段。

定制节点

我们可以通过 markupattrs 来定制节点的形状和样式,markup 可以类比 HTMLattrs 类比 CSS。强烈建议仔细阅读 markupopen in new windowattrsopen in new window 文档。

接下来我们会遇到一个问题,定制的内容要被多个节点使用,是不是需要每个节点都重新定义一次呢?答案是否定的,X6 提供了便捷的方式,可以让不同的节点复用配置。

修改节点(大小颜色)

在渲染完成之后,我们还可以通过 API 修改节点的所有属性。我们会常用到下面两个方法:

添加边

节点和边都有共同的基类 Cellopen in new window,除了从 Cell 继承属性外,还支持以下选项。

属性名类型默认值描述
sourceTerminalData-源节点或起始点。
targetTerminalData-目标节点或目标点。
verticesPoint.PointLike[]-路径点。
routerRouterData-路由。
connectorConnectorData-连接器。
labelsLabel[]-标签。
defaultLabelLabel默认标签open in new window默认标签。
graph.addEdge({

  shape: 'edge',

  source: 'node1',

  target: 'node2',

})

配置边

下面分别看下上面的配置如何使用。

source/target

边的源和目标节点(点)。

graph.addEdge({  source: rect1, // 源节点  target: rect2, // 目标节点})
graph.addEdge({  source: 'rect1', // 源节点 ID  target: 'rect2', // 目标节点 ID})
graph.addEdge({  source: { cell: rect1, port: 'out-port-1' }, // 源节点和连接桩 ID  target: { cell: 'rect2', port: 'in-port-1' }, // 目标节点 ID 和连接桩 ID})
graph.addEdge({  source: 'rect1', // 源节点 ID  target: { x: 100, y: 120 }, // 目标点})

router

路由 router 将对 vertices 进一步处理,并在必要时添加额外的点,然后返回处理后的点。例如,经过 orth 路由open in new window处理后,边的每一条链接线段都是水平或垂直的。

graph.addEdge({

  source: rect1,

  target: rect2,

  vertices: [

    { x: 100, y: 200 },

    { x: 300, y: 120 },

  ],

  // 如果没有 args 参数,可以简写为 router: 'orth'

  router: {

    name: 'orth',

    args: {},

  },

})

X6 默认提供了以下几种路由,点击下面的链接查看每种路由的使用方式。

另外,我们也可以注册自定义路由

使用箭头

我们定义了 sourceMarkertargetMarker 两个特殊属性来为边定制起始和终止箭头。例如,对 Shape.Edge 我们可以通过 line 选择器来指定起始和终止箭头。

内置箭头

X6 提供了以下几种内置箭头,使用时只需要指定箭头名和参数(可省略)即可。

graph.addEdge({

  shape: 'edge',

  sourece: [100, 100],

  target: [500, 500],

  attrs: {

    line: {

      sourceMarker: 'block', // 实心箭头

      targetMarker: {

        name: 'ellipse', // 椭圆

        rx: 10, // 椭圆箭头的 x 半径

        ry: 6, // 椭圆箭头的 y 半径

      },

    },

  },

})

连接桩

阅读时间约 6 分钟

在本章节中主要介绍连接桩相关的知识,通过阅读你可以了解到

  • 如何在节点中配置连接桩
  • 连接桩的增、删、改
  • 如何配置连接桩的位置
  • 如何配置连接桩上标签的位置

配置连接桩

首先我们将具有相同行为和外观的连接桩归为同一组,并通过 groups 选项来设置分组,该选项是一个对象 { [groupName: string]: PortGroupMetadata },组名为键,值为每组连接桩的默认选项,支持的选项如下:

interface PortGroupMetadata {

  markup?: Markup // 连接桩 DOM 结构定义。

  attrs?: Attr.CellAttrs // 属性和样式。

  zIndex?: number | 'auto' // 连接桩的 DOM 层级,值越大层级越高。

  // 群组中连接桩的布局。

  position?: [number, number] | string | { name: string; args?: object }

  label?: {

    // 连接桩标签

    markup?: Markup

    position?: {

      // 连接桩标签布局

      name: string // 布局名称

      args?: object // 布局参数

    }

  }

}

然后我们配置 itemsitems 是一个数组 PortMetadata[],数组的每一项表示一个连接桩,连接桩支持的选项如下:

interface PortMetadata {

  id?: string // 连接桩唯一 ID,默认自动生成。

  group?: string // 分组名称,指定分组后将继承分组中的连接桩选项。

  args?: object // 为群组中指定的连接桩布局算法提供参数, 我们不能为单个连接桩指定布局算法,但可以为群组中指定的布局算法提供不同的参数。

  markup?: Markup // 连接桩的 DOM 结构定义。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。

  attrs?: Attr.CellAttrs // 元素的属性样式。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。

  zIndex?: number | 'auto' // 连接桩的 DOM 层级,值越大层级越高。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。

  label?: {

    // 连接桩的标签。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。

    markup?: Markup // 标签 DOM 结构

    position?: {

      // 标签位置

      name: string // 标签位置计算方法的名称

      args?: object // 标签位置计算方法的参数

    }

  }

}

从下面例子代码中可以清晰看到连接桩的定义方式。

修改连接桩

节点上有丰富的 APIopen in new window 对连接桩进行增、删、改操作。

// 添加连接桩node.addPort({  group: 'top',  attrs: {    text: {      text: 'xx',    },  },})
// 删除连接桩node.removePort(portId)
// 更新连接桩node.portProp(portId, 'attrs/circle/stroke', color)

连接桩位置

连接桩布局算法只能通过 groups 中的 position 选项来指定,因为布局算法在计算连接桩位置时需要考虑到群组中的所有连接桩,我们在单个连接桩中可以通过 args 选项来影响该连接桩的布局结果。

我们默认提供了下面几种连接桩布局算法,同时支持自定义连接桩布局算法并注册使用open in new window,点击下面的链接可以了解每种布局算法的使用方法。

连接桩标签位置

groupslabel.position 选项和节点的 items.label.position 选项中都可以指定标签的位置。

我们默认提供了下面几种标签位置,也支持自定义标签位置并注册使用open in new window,点击下面的链接了解每种标签位置的使用方法。

交互

连线

连线交互规则都是通过 connecting 配置来完成,完整的配置参考 APIopen in new window。下面介绍一些常用的功能。

allowXXX

可以通过 allowXXX 配置来定义连线能否连接到对应的位置。默认支持以下项:

  • allowBlank:是否允许连接到画布空白位置的点,默认为 true
  • allowLoop:是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true
  • allowNode:是否允许边连接到节点(非节点上的连接桩),默认为 true
  • allowEdge:是否允许边连接到另一个边,默认为 true
  • allowPort:是否允许边连接到连接桩,默认为 true
  • allowMulti:是否允许在相同的起始节点和终止之间创建多条边,默认为 true

它们的值都支持以下两种类型:

new Graph({
  connecting: {
    allowNode: true, // boolean
  },
})

// 函数形式,多用于动态控制连接限制
new Graph({
  connecting: {
    allowNode(args) {
      return true
    },
  },
})

提示

allowMulti 支持设置为字符串 withPort,代表在起始和终止节点的相同连接桩之间只允许创建一条边(即起始和终止节点之间可以创建多条边,但必须要连接在不同的连接桩上)。

视图交互事件

通过鼠标、键盘或者各种可交互的组件与应用产生交互时触发的事件。

鼠标事件

事件cell 节点/边node 节点port 连接桩edge 边blank 画布空白区域
单击cell:clicknode:clicknode:port:clickedge:clickblank:click
双击cell:dblclicknode:dblclicknode:port:dblclickedge:dblclickblank:dblclick
右键cell:contextmenunode:contextmenunode:port:contextmenuedge:contextmenublank:contextmenu
鼠标按下cell:mousedownnode:mousedownnode:port:mousedownedge:mousedownblank:mousedown
移动鼠标cell:mousemovenode:mousemovenode:port:mousemoveedge:mousemoveblank:mousemove
鼠标抬起cell:mouseupnode:mouseupnode:port:mouseupedge:mouseupblank:mouseup
鼠标滚轮cell:mousewheelnode:mousewheel-edge:mousewheelblank:mousewheel
鼠标进入cell:mouseenternode:mouseenternode:port:mouseenteredge:mouseentergraph:mouseenter
鼠标离开cell:mouseleavenode:mouseleavenode:port:mouseleaveedge:mouseleavegraph:mouseleave

注意

需要注意的是,这里的 mousemove 事件和通常的鼠标移动事件有所区别,它需要在鼠标按下后移动鼠标才能触发。

除了 mouseentermouseleave 外,事件回调函数的参数都包含鼠标相对于画布的位置 xy 和鼠标事件对象 e 等参数。

graph.on('cell:click', ({ e, x, y, cell, view }) => {})
graph.on('node:click', ({ e, x, y, node, view }) => {})
graph.on('edge:click', ({ e, x, y, edge, view }) => {})
graph.on('blank:click', ({ e, x, y }) => {})

graph.on('cell:mouseenter', ({ e, cell, view }) => {})
graph.on('node:mouseenter', ({ e, node, view }) => {})
graph.on('edge:mouseenter', ({ e, edge, view }) => {})
graph.on('graph:mouseenter', ({ e }) => {})

自定义点击事件

我们可以在节点/边的 DOM 元素上添加自定义属性 eventdata-event 来监听该元素的点击事件,例如:

node.attr({

  // 表示一个删除按钮,点击时删除该节点

  image: {

    event: 'node:delete',

    xlinkHref: 'trash.png',

    width: 20,

    height: 20,

  },

})

可以通过绑定的事件名 node:delete 或通用的 cell:customeventnode:customeventedge:customevent 事件名来监听。

graph.on('node:delete', ({ view, e }) => {
  e.stopPropagation()
  view.cell.remove()
})

graph.on('node:customevent', ({ name, view, e }) => {
  if (name === 'node:delete') {
    e.stopPropagation()
    view.cell.remove()
  }
})
import React from 'react'
import { Graph } from '@antv/x6'
import './index.less'

Graph.registerNode(
  'custom-click-node',
  {
    markup: [
      {
        tagName: 'rect',
        selector: 'body',
      },
      {
        tagName: 'text',
        selector: 'label',
      },
      {
        tagName: 'g',
        children: [
          {
            tagName: 'text',
            selector: 'btnText',
          },
          {
            tagName: 'rect',
            selector: 'btn',
          },
        ],
      },
    ],
    attrs: {
      btn: {
        refX: '100%',
        refX2: -28,
        y: 4,
        width: 24,
        height: 18,
        rx: 10,
        ry: 10,
        fill: 'rgba(255,255,0,0.01)',
        stroke: 'red',
        cursor: 'pointer',
        event: 'node:delete',
      },
      btnText: {
        fontSize: 14,
        fill: 'red',
        text: 'x',
        refX: '100%',
        refX2: -19,
        y: 17,
        cursor: 'pointer',
        pointerEvent: 'none',
      },
      body: {
        stroke: '#8f8f8f',
        strokeWidth: 1,
        fill: '#fff',
        rx: 6,
        ry: 6,
        refWidth: '100%',
        refHeight: '100%',
      },
      label: {
        fontSize: 14,
        fill: '#333333',
        refX: '50%',
        refY: '50%',
        textAnchor: 'middle',
        textVerticalAnchor: 'middle',
      },
    },
  },
  true,
)

export default class Example extends React.Component {
  private container: HTMLDivElement

  componentDidMount() {
    const graph = new Graph({
      container: this.container,
      background: {
        color: '#F2F7FA',
      },
    })

    const source = graph.addNode({
      shape: 'custom-click-node',
      x: 40,
      y: 80,
      width: 120,
      height: 40,
      attrs: {
        label: {
          text: 'Source',
        },
      },
    })

    const target = graph.addNode({
      shape: 'custom-click-node',
      x: 360,
      y: 80,
      width: 120,
      height: 40,
      attrs: {
        label: {
          text: 'Target',
        },
      },
    })

    graph.addEdge({
      source,
      target,
      attrs: {
        line: {
          stroke: '#8f8f8f',
          strokeWidth: 1,
        },
      },
    })

    graph.on('node:delete', ({ view, e }: any) => {
      e.stopPropagation()
      view.cell.remove()
    })
  }

  refContainer = (container: HTMLDivElement) => {
    this.container = container
  }

  render() {
    return (
      <div className="custom-click-app">
        <div className="app-content" ref={this.refContainer} />
      </div>
    )
  }
}

节点/边

添加/删除/修改

当节点/边被添加到画布时,触发以下事件:

  • added
  • cell:added
  • node:added(仅当 cell 是节点时才触发)
  • edge:added(仅当 cell 是边时才触发)

当节点/边被移除时,触发以下事件:

  • removed
  • cell:removed
  • node:removed(仅当 cell 是节点时才触发)
  • edge:removed(仅当 cell 是边时才触发)

当节点/边发生任何改变时,触发以下事件:

  • changed
  • cell:changed
  • node:changed(仅当 cell 是节点时才触发)
  • edge:changed(仅当 cell 是边时才触发)

可以在节点/边上监听:

cell.on('added', ({ cell, index, options }) => {})

cell.on('removed', ({ cell, index, options }) => {})

cell.on('changed', ({ cell, options }) => {})

或者在 Graph 上监听:

graph.on('cell:added', ({ cell, index, options }) => {})
graph.on('cell:removed', ({ cell, index, options }) => {})
graph.on('cell:changed', ({ cell, options }) => {})

graph.on('node:added', ({ node, index, options }) => {})
graph.on('node:removed', ({ node, index, options }) => {})
graph.on('node:changed', ({ node, options }) => {})

graph.on('edge:added', ({ edge, index, options }) => {})
graph.on('edge:removed', ({ edge, index, options }) => {})
graph.on('edge:changed', ({ edge, options }) => {})

change:xxx

当调用 setXxx(val, options)removeXxx(options) 方法去改变节点/边的数据时,并且 options.silent 不为 true 时,都将触发对应的 change 事件,并触发节点/边重绘。例如:

cell.setZIndex(2)

cell.setZIndex(2, { silent: false })

cell.setZIndex(2, { anyKey: 'anyValue' })

将触发 Cell 上的以下事件:

  • change:*
  • change:zIndex

和 Graph 上的以下事件:

  • cell:change:*
  • node:change:*(仅当 cell 是节点时才触发)
  • edge:change:*(仅当 cell 是边时才触发)
  • cell:change:zIndex
  • node:change:zIndex(仅当 cell 是节点时才触发)
  • edge:change:zIndex(仅当 cell 是边时才触发)

可以在节点/边上监听:

// 当 cell 发生任何改变时都将被触发,可以通过 key 来确定改变项
cell.on(
  'change:*',
  (args: {
    cell: Cell
    key: string // 通过 key 来确定改变项
    current: any // 当前值
    previous: any // 改变之前的值
    options: any // 透传的 options
  }) => {
    if (key === 'zIndex') {
      //
    }
  },
)

cell.on(
  'change:zIndex',
  (args: {
    cell: Cell
    current?: number // 当前值
    previous?: number // 改变之前的值
    options: any // 透传的 options
  }) => {},
)

或者在 Graph 上监听:

graph.on(
  'cell:change:zIndex',
  (args: {
    cell: Cell
    current?: number // 当前值
    previous?: number // 改变之前的值
    options: any // 透传的 options
  }) => {},
)

// 当 cell 为节点时触发
graph.on(
  'node:change:zIndex',
  (args: {
    cell: Cell
    node: Node
    current?: number // 当前值
    previous?: number // 改变之前的值
    options: any // 透传的 options
  }) => {},
)

// 当 cell 为边时触发
graph.on(
  'edge:change:zIndex',
  (args: {
    cell: Cell
    edge: Edge
    current?: number // 当前值
    previous?: number // 改变之前的值
    options: any // 透传的 options
  }) => {},
)

其他 change 事件如下列表,回调函数的参数与上面提到的 change:zIndex 的参数结构一致。

  • Cell
    • change:*
    • change:attrs
    • change:zIndex
    • change:markup
    • change:visible
    • change:parent
    • change:children
    • change:tools
    • change:view
    • change:data
  • Node
    • change:size
    • change:angle
    • change:position
    • change:ports
    • change:portMarkup
    • change:portLabelMarkup
    • change:portContainerMarkup
    • ports:added
    • ports:removed
  • Edge
    • change:source
    • change:target
    • change:terminal
    • change:router
    • change:connector
    • change:vertices
    • change:labels
    • change:defaultLabel
    • vertexs:added
    • vertexs:removed
    • labels:added
    • labels:removed

除了上述这些内置的 Key,我们也支持监听自定义的 Key,例如

cell.on('change:custom', ({ cell, current, previous, options }) => {

  console.log(current)

})

当通过 cell.prop('custom', 'any data') 方法修改 custom 属性的值时将触发 change:custom 事件。

视图

由于 X6 实现了异步的渲染调度算法,所以节点的添加不一定意味着挂载到画布上。节点在被挂载到画布时以及从画布上卸载时会分别触发单独的事件。

事件名回调参数说明
view:mounted{ view: CellView }节点被挂载到画布上时触发。
view:unmounted{ view: CellView }节点从画布上卸载时触发。
graph.on('view:mounted', ({ view }) => {})

graph.on('view:unmounted', ({ view }) => {})

大家还有经常需要在调用 fromJSON 或者 resetCells 后监听画布完成渲染事件,这时候可以使用 render:done 事件来监听 (2.15.1 版本新增)。

graph.on('render:done', () => {  // pass})
graph.fromJSON([...])

数据

导出

我们可以调用 graph.toJSON() 方法来导出图中的节点和边,返回一个具有 { cells: [] } 结构的对象,其中 cells 数组按渲染顺序保存节点和边。

其中,导出的节点结构如下:

{

  id: string,

  shape: string,

  position: {

    x: number

    y: number

  },

  size: {

    width: number

    height: number

  },

  attrs: object,

  zIndex: number,

}

边的结构如下:

{

  id: string,

  shape: string,

  source: object,

  target: object,

  attrs: object,

  zIndex: number,

}
{
  "cells": [
    {
      "position": {
        "x": 0,
        "y": 0
      },
      "size": {
        "width": 100,
        "height": 40
      },
      "attrs": {
        "text": {
          "text": "Hello"
        },
        "body": {
          "stroke": "#8f8f8f",
          "strokeWidth": 1,
          "fill": "#fff",
          "rx": 6,
          "ry": 6
        }
      },
      "visible": true,
      "shape": "rect",
      "id": "f56d0228-1b2d-4d1f-a549-624a112f1013",
      "zIndex": 1
    },
    {
      "position": {
        "x": 0,
        "y": 240
      },
      "size": {
        "width": 100,
        "height": 40
      },
      "attrs": {
        "text": {
          "text": "World"
        },
        "body": {
          "stroke": "#8f8f8f",
          "strokeWidth": 1,
          "fill": "#fff",
          "rx": 6,
          "ry": 6
        }
      },
      "visible": true,
      "shape": "ellipse",
      "id": "4aa790be-3370-4735-bc1d-15e6f9aeb9f7",
      "zIndex": 2
    },
    {
      "shape": "edge",
      "attrs": {
        "line": {
          "stroke": "#8f8f8f",
          "strokeWidth": 1
        }
      },
      "id": "a363d4ea-b0bf-468d-9dfc-0afd87d8691f",
      "source": {
        "cell": "f56d0228-1b2d-4d1f-a549-624a112f1013"
      },
      "target": {
        "cell": "4aa790be-3370-4735-bc1d-15e6f9aeb9f7"
      },
      "labels": [
        {
          "attrs": {
            "label": {
              "text": "X6"
            }
          }
        }
      ],
      "zIndex": 3
    }
  ]
}

导入

支持节点/边元数据数组 graph.fromJSON(cells: (Node.Metadata | Edge.Metadata)[])

graph.fromJSON([

  {

    id: 'node1',

    x: 40,

    y: 40,

    width: 100,

    height: 40,

    label: 'Hello',

    shape: 'rect',

  },

  {

    id: 'node2',

    x: 40,

    y: 40,

    width: 100,

    height: 40,

    label: 'Hello',

    shape: 'ellipse',

  },

  {

    id: 'edge1',

    source: 'node1',

    target: 'node2',

    shape: 'edge',

  },

])

或者提供一个包含 cellsnodesedges 的对象,按照 [...cells, ...nodes, ...edges] 顺序渲染。

graph.fromJSON({

  nodes: [],

  edges: [],

})

通常,我们通过 graph.fromJSON(...) 来渲染 graph.toJSON() 导出的数据。

提示

当数据中没有提供 zIndex 时,则按照节点/边在数组中的顺序渲染,也就是说越靠前的节点/边,其 zIndex 越小,在画布中的层级就越低

内置工具

工具是渲染在节点/边上的小部件,用于增强节点/边的交互能力,我们分别为节点和边提供了以下内置工具:

节点:

边:

Server-Sent Events 教程

服务器向浏览器推送信息,除了 WebSocketopen in new window,还有一种方法:Server-Sent Events(以下简称 SSE)。本文介绍它的用法。

img

一、SSE 的本质

严格地说,HTTP 协议open in new window无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。

也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。

二、SSE 的特点

SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。

总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。

img

但是,SSE 也有自己的优点。

  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
  • SSE 默认支持断线重连,WebSocket 需要自己实现。
  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
  • SSE 支持自定义发送的消息类型。

因此,两者各有特点,适合不同的场合。

三、客户端 API

3.1 EventSource 对象

SSE 的客户端 API 部署在EventSource对象上。下面的代码可以检测浏览器是否支持 SSE。

if ('EventSource' in window) {
  // ...
}

使用 SSE 时,浏览器首先生成一个EventSource实例,向服务器发起连接。

var source = new EventSource(url);

上面的url可以与当前网址同域,也可以跨域。跨域时,可以指定第二个参数,打开withCredentials属性,表示是否一起发送 Cookie。

var source = new EventSource(url, { withCredentials: true });

EventSource实例的readyState属性,表明连接的当前状态。该属性只读,可以取以下值。

  • 0:相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。
  • 1:相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。
  • 2:相当于常量EventSource.CLOSED,表示连接已断,且不会重连。

3.2 基本用法

连接一旦建立,就会触发open事件,可以在onopen属性定义回调函数。

source.onopen = function (event) {
  // ...
};

// 另一种写法
source.addEventListener('open', function (event) {
  // ...
}, false);

客户端收到服务器发来的数据,就会触发message事件,可以在onmessage属性的回调函数。

source.onmessage = function (event) {
  var data = event.data;
  // handle message
};

// 另一种写法
source.addEventListener('message', function (event) {
  var data = event.data;
  // handle message
}, false);

上面代码中,事件对象的data属性就是服务器端传回的数据(文本格式)。

如果发生通信错误(比如连接中断),就会触发error事件,可以在onerror属性定义回调函数。

source.onerror = function (event) {
  // handle error event
};

// 另一种写法
source.addEventListener('error', function (event) {
  // handle error event
}, false);

close方法用于关闭 SSE 连接。

source.close();

3.3 自定义事件

默认情况下,服务器发来的数据,总是触发浏览器EventSource实例的message事件。开发者还可以自定义 SSE 事件,这种情况下,发送回来的数据不会触发message事件。

source.addEventListener('foo', function (event) {
  var data = event.data;
  // handle message
}, false);

上面代码中,浏览器对 SSE 的foo事件进行监听。如何实现服务器发送foo事件,请看下文。

四、服务器实现

4.1 数据格式

服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam

每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。

[field]: value\n

上面的field可以取四个值。

  • data
  • event
  • id
  • retry

此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。

: This is a comment

下面是一个例子。

: this is a test stream\n\n

data: some text\n\n

data: another message\n
data: with two lines \n\n

4.2 data 字段

数据内容用data字段表示。

data:  message\n\n

如果数据很长,可以分成多行,最后一行用\n\n结尾,前面行都用\n结尾。

data: begin message\n
data: continue message\n\n

下面是一个发送 JSON 数据的例子。

data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

4.3 id 字段

数据标识符用id字段表示,相当于每一条数据的编号。

id: msg1\n
data: message\n\n

浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。

4.4 event 字段

event字段表示自定义的事件类型,默认是message事件。浏览器可以用addEventListener()监听该事件。

event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: bar\n
data: a bar event\n\n

上面的代码创造了三条信息。第一条的名字是foo,触发浏览器的foo事件;第二条未取名,表示默认类型,触发浏览器的message事件;第三条是bar,触发浏览器的bar事件。

下面是另一个例子。

event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}

event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}

4.5 retry 字段

服务器可以用retry字段,指定浏览器重新发起连接的时间间隔。

retry: 10000\n

两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。

五、Node 服务器实例

SSE 要求服务器与浏览器保持连接。对于不同的服务器软件来说,所消耗的资源是不一样的。Apache 服务器,每个连接就是一个线程,如果要维持大量连接,势必要消耗大量资源。Node 则是所有连接都使用同一个线程,因此消耗的资源会小得多,但是这要求每个连接不能包含很耗时的操作,比如磁盘的 IO 读写。

下面是 Node 的 SSE 服务器实例open in new window

var http = require("http");

http.createServer(function (req, res) {
  var fileName = "." + req.url;

  if (fileName === "./stream") {
    res.writeHead(200, {
      "Content-Type":"text/event-stream",
      "Cache-Control":"no-cache",
      "Connection":"keep-alive",
      "Access-Control-Allow-Origin": '*',
    });
    res.write("retry: 10000\n");
    res.write("event: connecttime\n");
    res.write("data: " + (new Date()) + "\n\n");
    res.write("data: " + (new Date()) + "\n\n");

    interval = setInterval(function () {
      res.write("data: " + (new Date()) + "\n\n");
    }, 1000);

    req.connection.addListener("close", function () {
      clearInterval(interval);
    }, false);
  }
}).listen(8844, "127.0.0.1");

请将上面的代码保存为server.js,然后执行下面的命令。

$ node server.js

上面的命令会在本机的8844端口,打开一个 HTTP 服务。

然后,打开这个网页open in new window,查看客户端代码并运行。