与服务器协作
JavaScript Kanban 支持同时处理客户端和服 务器端数据。该组件对后端没有特殊要求,可以连接任何支持 REST(RESTful)API 的后端平台。
该组件自带内置的 Go 和 Node 后端。您同样可以使用自定义的服务器脚本。
RestDataProvider
JavaScript Kanban 提供了 RestDataProvider 服务,完全支持与后端通信的 REST API。该 provider 发送和接收以下数据操作:
"add-card""add-column""add-comment""add-row""add-link""delete-card""delete-column""delete-comment""delete-row""delete-link""move-card""move-column""move-row""update-card""update-column""update-comment""update-row""add-vote""delete-vote"
REST 方法
RestDataProvider 服务提供以下 REST 方法:
getCards()— 获取 cards 数据的 PromisegetColumns()— 获取 columns 数据的 PromisegetHandlers()— 返回 provider 使用的默认 action handlergetIDResolver()— 返回一个将临时客户端 ID 解析为后端 ID 的函数getLinks()— 获取 links 数据的 PromisegetQueue()— 返回 provider 处理的内部 action 队列getRows()— 获取 rows 数据的 PromisegetUsers()— 获取 users 数据的 Promisesend()— 发送自定义 HTTP 请求并返回 PromisesetHeaders()— 设置附加到每个请求的自定义 HTTP 请求头
自定义 RestDataProvider
要自定义 RestDataProvider 向服务器发送数据操作的方式,可以继承该类并覆盖其中的某个方法。最常见的自定义目标是默认 action handler——例如,为自定义事件添加 handler,或扩展现有操作的 payload。
若要在不丢失默认 handler 的情况下添加自定义 handler,请覆盖 getHandlers() 方法,并将自定义条目合并到 super.getHandlers() 的结果之上:
const url = "https://some_backend_url";
class MyDataProvider extends kanban.RestDataProvider {
getHandlers() {
const handlers = super.getHandlers();
return {
...handlers,
// custom or overridden handlers go here
};
}
}
const restProvider = new MyDataProvider(url);
board.api.setNext(restProvider);
覆盖时务必调用 super.getHandlers() 并展开其结果。不要手动将默认 handler 复制到覆盖方法中——action 映射可能在不同版本间发生变化, 硬编码的副本可能在不知不觉中与当前默认值脱节。
另一个常见的自定义目标是 send() 方法,每个默认 handler 都会调用该方法。覆盖 send() 可注入额外请求头、重写 URL,或为每个服务器请求添加自定义逻辑。
与后端交互
要与服务器交互,需要将 RestDataProvider 连接到后端脚本。可使用内置后端,也可以创建自定义后端:
如果使用自定义后端,请参考 REST API routes 参考文档。
要将 RestDataProvider 连接到后端,需要调用 kanban.RestDataProvider 构造函数并传入后端 URL。以下代码片段创建一个 provider,获取初始数据,并将 provider 绑定到 Kanban Event Bus:
const url = "https://some_backend_url";
const restProvider = new kanban.RestDataProvider(url);
Promise.all([
restProvider.getUsers(),
restProvider.getCards(),
restProvider.getColumns(),
restProvider.getLinks(),
restProvider.getRows()
]).then(([users, cards, columns, links, rows]) => {
const board = new kanban.Kanban("#root", {
cards,
columns,
links,
rows,
rowKey: "type",
editorShape: [
...kanban.defaultEditorShape,
{
type: "multiselect",
key: "users",
label: "Users",
values: users
}
]
});
board.api.setNext(restProvider);
});
通过 api.setNext() 方法将 RestDataProvider 添加到 Event Bus 中。此步骤使数据操作(添加、删 除等)能够触发相应的服务器请求。
示例
以下演示将 RestDataProvider 连接到 Go 后端并加载服务器数据:
多用户后端
多用户后端允许多个用户无需刷新页面即可实时编辑同一看板。组件通过 WebSocket 连接到服务器,自定义处理函数将服务器传来的变更应用到看板。
要启用多用户后端,需在初始化 Kanban 前在服务器上完成用户授权。以下 login(url) 函数用于获取并缓存会话 token:
const login = (url) => {
var token = sessionStorage.getItem("login-token");
if (token) {
return Promise.resolve(token);
}
return fetch(url + "/login?id=1")
.then(raw => raw.text())
.then(token => {
sessionStorage.setItem("login-token", token);
return token;
});
};
该函数用于模拟授权(演示中登录请求硬编码了 id=1,因此每个获取的会话均使用 ID 1)。授权成功后,服务器会返回一个 token,后续请求都需要携带该 token。
要将 token 自动附加到每个请求,请调用 RestDataProvider.setHeaders()。默认情况下,服务器将 token 存储在 "Remote-Token": <value> header 中:
login(url).then(token => {
// rest provider 初始化
const restProvider = new kanban.RestDataProvider(url);
// 设置 token 为自定义 header
restProvider.setHeaders({
"Remote-Token": "eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6ImJpem9uZSIsImlhdC...",
});
// 组件初始化...
});
获取 token 后,初始化组件。以下代码片段获取数据并创建 Kanban 看板:
// 组件初始化...
Promise.all([
restProvider.getCards(),
restProvider.getColumns(),
restProvider.getLinks(),
restProvider.getRows(),
]).then(([cards, columns, links, rows]) => {
const board = new Kanban("#root", {
cards,
columns,
links,
rows,
rowKey: "row",
cardShape,
editorShape,
});
// 将客户端数据保存到服务器
board.api.setNext(restProvider);
// 多用户初始化...
});
看板创建完成后,附加 WebSocket 以监听服务器事件。以下代码片段配置 RemoteEvents 处理函数:
// 多用户初始化...
// 获取服务器事件的客户端处理函数
const handlers = kanbanUpdates(
board.api,
restProvider.getIDResolver()
);
// 连接服务器事件
const events = new RemoteEvents(url + "/api/v1", token);
// 绑定客户端处理函数到服务器事件
events.on(handlers);
该代码片段中使用了以下标识符:
handlers— 处理服务器事件的客户端处理函数events— 监听服务器传入事件的RemoteEvents实例
events.on(handlers) 调用将客户端处理函数注册到服务器端事件。组件现在可以实时响应服务器端的变更。
示例
以下演示配置了多用户后端,用于实时跟踪其他用户的变更:
自定义服务器事件
要为服务器事件定义自定义逻辑,需将 handlers 对象传递给 RemoteEvents.on(handlers)。该对象的结构如下:
{
cards?: (obj: any) => void;
columns?: (obj: any) => void;
links?: (obj: any) => void;
rows?: (obj: any) => void;
comments?: (obj: any) => void;
votes?: (obj: any) => void;
}
服务器发生变更后,响应中包含被修改元素的名称。这些名称取决于服务器逻辑。
客户端更新的数据会通过 function(obj: any) 回调的 obj 参数传入。type: string 字段指定操作类型,允许的值如下:
- 对于 cards:
"add-card","update-card","delete-card","move-card" - 对于 columns:
"add-column","update-column","delete-column","move-column" - 对于 links:
"add-link","delete-link" - 对于 rows:
"add-row","update-row","delete-row","move-row" - 对于 comments:
"add-comment","update-comment","delete-comment" - 对于 votes:
"add-vote","delete-vote"
以下代码片段展示了实现细节:
// 初始化 kanban
const board = new kanban.Kanban(...);
const restProvider = new kanban.RestDataProvider(url);
const idResolver = restProvider.getIDResolver();
const TypeCard = 1;
const TypeRow = 2;
const TypeCol = 3;
const cardsHandler = (obj: any) => {
obj.card.id = idResolver(obj.card.id, TypeCard);
obj.card.row = idResolver(obj.card.row, TypeRow);
obj.card.column = idResolver(obj.card.column, TypeCol);
switch (obj.type) {
case "add-card":
board.api.exec("add-card", {
card: obj.card,
select: false,
skipProvider: true, // 防止客户端再次向服务器发送请求
})
break;
// 其他操作
}
}
// 添加自定义处理函数
const handlers = {
cards: cardsHandler,
};
const remoteEvents = new kanban.RemoteEvents(remoteEventsURL, token);
remoteEvents.on(handlers);
RestDataProvider.getIDResolver() 方法返回一个函数,用于同步客户端 ID 与服务器 ID。当客户端创建新对象(card、column、row 或 link)时,该对象会获得一个临时 ID,同时数据存储中保存对应的服务器 ID。idResolver(id: TID, type: number) 函数将临时 ID 解析为服务器 ID。
type 参数标识模型类型:
CardID—1RowID—2ColumnID—3LinkID—4CommentID—5
要防止请求发送到服务器,调用 board.api.exec() 时需传入 skipProvider: true。remoteEvents.on(handlers) 调用负责注册自定义处理函数。
将状态分组到同一列
将来自不同列的卡片显示在同一列中。例如,可以将 todo 和 unassigned 状态的卡片放在同一列。
要实现分组,需添加一个自定义字段(如 status)用于存储卡片当前状态,column 字段则存储公共状态。
定义分组规则。下例使用以下状态进行分组:
todo,unassigned— 属于 Open 列dev,testing— 属于 Inprogress 列merged,released— 属于 Done 列
有两种实现方式:
服务器端分组
服务器端分组要求服务器能够通过 WebSockets 向客户端推送数据(见 多用户后端)。
当服务器处理卡片更新请求时,需检查 status 字段。下例使用 Go 语言,但任何后端技术均可使用。
以下代码片段在服务器端将 status 字段映射到目标列:
func Update(id int, c Card) error {
// ...
oldColumn := c.Column
s := data.Status
if s == "todo" || s == "unassigned" {
c.Column = "open"
} else if s == "dev" || s == "testing" {
c.Column = "inprogress"
} else if s == "merged" || s == "released" {
c.Column = "done"
}
db.Save(&c)
if oldColumn != c.Column {
// 如果因 status 字段更新了 column,
// 通知客户端将卡片移动到对应列
// 更新卡片索引
updateCardIndex(&c)
// 通知客户端更新列
ws.Publish("card-update", &c)
}
// ...
}
当用户更改 status 字段值时,服务器会检查其值并将卡片放入目标列,然后通过 WebSocket 通知客户端移动该卡片。
服务器端 + 客户端混合分组
对于混合方案,从服务器获取分组规则。客户端根据这些规则,依据 status 字段的值确定目标列。
以下代码片段用于获取规则:
const groupingRules = await fetch("http://server.com/rules");
规则对象的格式如下:
{
"open": ["todo", "unassigned"],
"progress": ["dev", "testing"],
"done": ["merged", "released"],
}
定义检测卡片变更并将其移动到对应列的逻辑。以下代码片段拦截 move-card 和 update-card 事件:
const updateColumn = card => {
for (let col in groupingRules) {
if (groupingRules[col].includes(card.status)) {
card.column = col;
break;
}
}
};
kanban.api.intercept("move-card", ev => {
kanban.api.exec("update-card", {
id: ev.id,
card: { status: groupingRules[ev.columnId][0] },
});
});
kanban.api.intercept("update-card", ev => {
updateColumn(ev.card);
});
该方式可根据其他字段的值为卡片指定所属列。
示例
以下演示配置了服务器端,将两个或更多状态实时分组到同一列: