数据的种类多,数据间的关联性强
60+ Schemas,40+ 关联
②
①
⑦
③
⑤
④
③
③
③
⑥
⑧
{
"_id": "58b42f74428e764903e86395",
"_creatorId": "54cb6200d1b4c6af47abe570",
"_executorId": "556466f750e9c0503d58a37b",
"_projectId": "58b42ef1c655de4203cad186",
"_tasklistId": "58b42ef1428e764903e8636e",
"_stageId": "58b42ef1428e764903e8636f",
"involveMembers": [
"54cb6200d1b4c6af47abe570",
"556466f750e9c0503d58a37b"
],
"dueDate": "2017-02-01T10:00:00.000Z",
"note": "我逾期啦",
"content": "在 Demo 中逾期的任务",
"project": {
"_id": "58b42ef1c655de4203cad186",
"name": "ReactiveDB Demo"
},
"creator": {
"_id": "54cb6200d1b4c6af47abe570",
"name": "龙逸楠",
"avatarUrl": "https://striker.teambition.net/thumbnail/110f238c339c7dacefbc792723a067fbf188/w/200/h/200"
},
"tagIds": [ "58b42ef1428e724303e2636f" ],
"stage": {
"_id": "58b42ef1428e764903e8636f",
"name": "待处理"
},
"executor": {
"_id": "556466f750e9c0503d58a37b",
"name": "太狼",
"avatarUrl": "https://striker.teambition.net/thumbnail/110q6f384916ce773e9352b8bf190b5dadca/w/200/h/200"
}
}
实时性要求高
几乎所有数据都需要通过 WebSocket 更新
Teambition 技术演进史
Backbone
Backbone + jQuery
class TaskModel extends Model {
listen() {
this.socket.on('change', patch => {
this.set(patch)
})
}
}
class TaskView {
listenTo() {
this.taskModel.on('change', taskModel => {
this.renderTask(taskModel)
})
this.subtaskCollection.on('new remove change:_taskId', () => {
this.taskModel.set({
subtasksCount: this.subtaskCollection.length
})
})
this.stageModel.on('change: name', stageModel => {
this.renderStage(stageModel)
})
this.tasklistModel.on('change', tasklistModel => {
this.renderTasklist(tasklistModel)
})
this.projectModel.on('change', projectModel => {
this.renderProject(projectModel)
})
}
}
TaskCardView
TaskDetailView
TaskModel
diff,emit change
Others Models
emit more changes
???
Model
View
Redux + Normalzr + Reselect + React
Redux Store
Backend
Normalzr
Http & Websocket
Connected Container
Reselect
Component
// redux-observable middleware
const epic = action$ => action$
.ofType(`${someAction}`)
.switchMap(action => {
const { param } = action.payload
return Ajax$.get(param)
.map(response => otherAction(normalize(response, someSchema)))
.catch(err => Observable.of(errorAction(err)))
.takeUntil(`${componentUnmount}`)
})
// reselect
const selector = createSelector(
state => state.someModule.entities,
state => state.someOtherModule.entities.something,
(dataYouCareAbout, relatedData) =>
// filter and reselect
)
// container
const mapStateToProps = state => ({
selector1: selector1(state),
selector2: selector2(state),
selector3: selector3(state),
// ...
})
connect(mapStateToProps)(component)
// reselect
/**
* 存储未开始的任务
* 定义:
* & 未被完成
* & 未被归档
* & _projectId 字段匹配
* &
* (
* | 没有截止日期
* | 有开始时间 & 开始时间在今天以后
* | 没有开始时间
* | 有截止日期 & 截止日期在今天以后
* | 有开始时间 & 开始时间在今天以后
* | 没有开始时间
* )
*/
const taskFilter = taskData => {
return data._projectId === projectId &&
!data.isArchived &&
!data.isDone &&
(
!data.dueDate &&
(
!data.startDate ||
(
data.startDate &&
new Date(data.startDate).valueOf() > now
)
) ||
(
data.dueDate &&
new Date(data.dueDate).valueOf() < now &&
(
(
data.startDate &&
new Date(data.startDate).valueOf() > now
) ||
!data.startDate
)
)
)
}
K-V based Cache + RxJS
class Database {
cache = new Map()
save(data, primaryKey = '_id') {
// normalize
}
update(primaryKey, patch) {
// ...
}
get(primaryKey) {
// reselect
return Observable.create(observer => {
observer.next(this.cache.get(primaryKey))
})
/**
* updateStream$ = socket.getUpdate(primaryKey)
* .merge(http.getUpdate(primaryKey))
**/
.combineLatest(updateStream$)
.map(([data, patch]) => { ...data, patch })
.takeUtil(deleteStream$)
}
}
class TaskSchema {
@primaryKey _id: string
_exectorId: string
_tasklistId: string
_stageId: string
_projectId: string
content: string
note: string
@assosiate('_exectorId') exector: ExectorSchema
@assosiate('_projectId') project: ProjectSchema
@assosiate('_tasklistId') tasklist: TasklistSchema
...
}
class ProjectSchema {
@primaryKey _id: string
_id: string
_organizationId: string | null
name: string
avatarUrl: string
@assosiate('_organizationId') organization: OrganizationSchema
@assosiateMany('_projectId') tasklists: TasklistSchema[]
....
}
View
Model
1. 中心化的数据管理:
2. 查询逻辑降噪
3. Observing & Cancelling
Backend Database
API Service
Nested Data
????
Observed Nested Data
Nomalized Data
Components
Frontend
Nomalized Data
Basic
More
// Define Schema and Connect
const schemaBuilder = lf.schema.create('teambition', 1)
schemaBuilder.createTable('Task')
.addColumn('_id', lf.Type.STRING)
.addColumn('content', lf.Type.STRING)
.addColumn('created', lf.Type.STRING)
.addColumn('dueDate', lf.Type.STRING)
.addColumn('priority', lf.Type.INTEGER)
// ...
.addPrimaryKey(['_id'])
.addNullable(['content', 'created', 'dueDate', 'priority'])
schemaBuilder.createTable('Member')
.addColumn('_id', lf.Type.STRING)
.addColumn('name', lf.Type.STRING)
.addColumn('avatarUrl', lf.Type.STRING)
// ...
.addPrimaryKey(['_id'])
.addNullable([/** ... */])
//...
// Schema is defined, now connect to the database instance.
schemaBuilder
.connect({ /** storeType: lf.DataStoreType.INDEXED_DB */ })
.then(db => {
// Schema is not mutable once the connection to DB has established.
})
// Select
const taskTable = db.getSchema().table('Task')
const memberTable = db.getSchema().table('Member')
const query = db.select(
taskTable._id, taskTable.content, memberTable._id,
memberTable.name, memberTable.avatarUrl
)
// join
.from(taskTable, memberTable)
// oh no....
.where(lf.op.and(
taskTable.created.lt(moment().startOf('day').valueOf()),
taskTable._executorId.eq(member._id)
))
.orderBy(taskTable.priority, lf.Order.DESC)
.orderBy(taskTable.dueDate, lf.Order.DESC)
// 第二页,每页20条数据
// Sad story,因为查询的结果是笛卡尔积,这里不能这么查询
.limit(20)
.skip(20)
// Select Data
query.exec().then(data => // 笛卡尔积,Graph it)
// Observe
db.observe(query, diff => // effect)
//Unobserve
db.unobserve(query, callback)
const Tasks = [Task1, Task2, Task3 ...]
const Subtasks = [Subtask1, Subtask2, Subtask3 ...]
const Projects = [Project1]
const result = [
{
Task: Task1,
Subtask: Subtask1,
Project: Project1
},
{
Task: Task1,
Subtask: Subtask2,
Project: Project1
},
{
Task: Task2,
Subtask: Subtask3,
Project: Project1
},
{
Task: Task2,
Subtask: Subtask4,
Project: Project1
}
// ....
]
// 定义 Schema
ReactiveDB.defineSchema('Task', {
_creatorId: {
type: RDBType.STRING
},
_executorId: {
type: RDBType.STRING
},
_id: {
type: RDBType.STRING,
primaryKey: true
},
_projectId: {
type: RDBType.STRING
},
_stageId: {
type: RDBType.STRING
},
_tasklistId: {
type: RDBType.STRING
},
accomplished: {
type: RDBType.DATE_TIME
},
content: {
type: RDBType.STRING
},
created: {
type: RDBType.DATE_TIME
},
customfields: {
type: RDBType.OBJECT
},
dueDate: {
type: RDBType.DATE_TIME
},
executor: {
type: Relationship.oneToOne,
virtual: {
name: 'Member',
where: memberTable => ({
_executorId: memberTable._id
})
}
},
involveMembers: {
type: RDBType.LITERAL_ARRAY
},
isArchived: {
type: RDBType.BOOLEAN
},
isDone: {
type: RDBType.BOOLEAN
},
note: {
type: RDBType.STRING
},
pos: {
type: RDBType.NUMBER
},
priority: {
type: RDBType.NUMBER
},
project: {
type: Relationship.oneToOne,
virtual: {
name: 'Project',
where: projectTable => ({
_projectId: projectTable._id
})
}
},
stage: {
type: Relationship.oneToOne,
virtual: {
name: 'Stage',
where: stageTable => ({
_stageId: stageTable._id
})
}
},
startDate: {
type: RDBType.DATE_TIME
},
subtaskCount: {
type: RDBType.NUMBER
},
subtaskIds: {
type: RDBType.LITERAL_ARRAY
},
subtasks: {
type: Relationship.oneToMany,
virtual: {
name: 'Subtask',
where: subtaskTable => ({
_id: subtaskTable._taskId
})
}
},
tagIds: {
type: RDBType.LITERAL_ARRAY
},
tasklist: {
type: Relationship.oneToOne,
virtual: {
name: 'Tasklist',
where: tasklistTable => ({
_tasklistId: tasklistTable._id
})
}
}
})
// 查询数据
const QueryToken = ReactiveDB.get('Task', {
where: {
created: {
$gt: moment().startOf('day').valueOf()
}
},
fields: [
'_id',
'content',
{
member: ['_id', 'name', 'avatarUrl']
}
],
// yes it works
skip: 20,
limit: 20,
orderBy: [
{ fieldName: 'priority', orderBy: 'DESC' },
{ fieldName: 'dueDate', orderBy: 'DESC' }
]
})