coolink-ts
Version:
Coocaa Coolink APP JavaScript library base on Thresh-lib.
549 lines (505 loc) • 19.5 kB
text/typescript
/**
* MIT License
*
* Copyright (c) 2020 ManBang Group
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
import Widget from './Widget'
import { ListView } from './basicWidget'
import Util from '../shared/Util'
import { RenderData, ThreshProvider } from '../types/type'
import threshApp from './ThreshApp'
import appContainer from './AppContainer'
import TimerManager from '../manager/TimerManager'
import UtilManager from '../manager/UtilManager'
export type PropChildrenVNodeType = VNode | VNode[]
const ChildrenKey: string = 'children'
type W = Widget<any, any>
type Key = string | number | void
type Ref = ((widget: W) => void) | void
export interface PropChildrenVNode {
[childrenPropName: string]: PropChildrenVNodeType
}
interface NodeEvents {
[eventName: string]: Function
}
interface VNodeUpdateInfo {
updateNodeId: string, // flutter 端需要被更新的原子组件 node id
invokeUpdateNodeId: string // js 端需要出发 update 的自定义组件 node id
}
export default class VNode {
// 节点类型
type: string
// 节点属性
props: any
// 父节点
parent: VNode | void
// 子节点
children: VNode[] = []
// 原子节点属性上的子节点
basicWidgetPropChildren: PropChildrenVNode = {}
// 节点的 widget 实例
widget: W
// 节点实例构造器
widgetBuilder: any
// 是否原子节点
isBasicWidget: boolean
// 原子节点 children 属性对应的 name
childrenMapedName: string = ChildrenKey
// 原子节点 children 属性对应的类型是否为 array
childrenIsArray: boolean = true
// 节点 id
nodeId: string
// 节点为 NativeView 时的 viewId
nativeViewId?: string
// 节点上的事件
events: NodeEvents = {}
// 节点所属页面
pageName?: string
// 子节点中存在 Page 时保存 Page Node
pageNode: VNode | void
// 节点是否已挂载
hasMount: boolean = false
// context id
contextId: string = appContainer.contextId as string
isPageNode: boolean = false
isNativeViewNode: boolean = false
isInputNode: boolean = false
key?: Key
parentKey?: Key
ref?: Ref
updateInfo?: VNodeUpdateInfo
shouldRender: boolean = true
private _refHasMount: boolean = false
static nodeIdIndex: number = 0
static getNodeId(type: string): string {
return `${type}#${++VNode.nodeIdIndex}`
}
static asyncEventTypes: string[] = [
'onRefresh',
'onLoadMore',
'onStopBack',
]
static eventTypes: string[] = [
'onTap',
'onLongTap',
'onLongTapStart',
'onLongTapEnd',
'onPan',
'onScroll',
'onChange',
'onLoad',
'onLayout',
'onFocus',
'onBlur',
'onActionsOpen',
'onActionsClose',
'willDragStatusChange',
'onDragStatusChange',
'onDragPositionChange',
'onOpen',
'onSubmitted',
'onClicked',
'onEnterClicked',
'onRawKeyFocusChanged',
'dispatchKeyEvent',
...VNode.asyncEventTypes,
]
static throttledEventTypes: string[] = ['onTap']
constructor({
type,
props,
widgetBuilder,
isBasicWidget,
childrenMapedName,
childrenIsArray,
parent,
key,
ref,
}: {
type: string,
props: any,
widgetBuilder: any,
isBasicWidget: boolean,
childrenMapedName: string
childrenIsArray: boolean
parent?: VNode | void,
key?: Key,
ref?: Ref,
}) {
this.type = type
this.props = props
this.widgetBuilder = widgetBuilder
this.isBasicWidget = isBasicWidget
this.childrenMapedName = childrenMapedName
this.childrenIsArray = childrenIsArray
this.parent = parent
this.key = key
this.ref = ref
this.nodeId = VNode.getNodeId(type)
if (isBasicWidget) {
this.isPageNode = type === 'Page'
this.isNativeViewNode = type === 'NativeView'
this.isInputNode = type === 'Input'
}
if (this.isNativeViewNode) {
this.nativeViewId = `${this.nodeId}#${this.props.type}#${Date.now()}`
}
this.findVNodeInProps()
}
// 在 props 中查找所有的 vnode
findVNodeInProps() {
for (let key in this.props) {
const item: any = this.props[key]
if (item instanceof VNode && this.isBasicWidget) {
item.parent = this
this.basicWidgetPropChildren[key] = item
}
if (Array.isArray(item)) {
item.forEach((subItem: any) => {
if (subItem instanceof VNode) {
subItem.parent = this
if (key === ChildrenKey) this.appendChild(subItem)
else if (this.isBasicWidget) this.appendChildInArrayProps(subItem, key)
}
})
}
}
}
// 在 props 中查找事件
findEventsInProps() {
if (!this.isBasicWidget) return
this.events = {}
let eventId: number = 0
VNode.eventTypes.forEach(key => {
let prop: any = this.props[key]
if (prop instanceof Function) {
const eventCacheId: string = `event/${++eventId}`
if (VNode.throttledEventTypes.includes(key)) prop = Util.throttle(prop)
this.events[eventCacheId] = prop
this.props[`_${key}Id`] = eventCacheId
}
})
}
// 触发组件实例 render
doRender() {
if (!this.shouldRender) return
this.shouldRender = false
if (!this.widget) {
this.widget = new this.widgetBuilder(this.props)
this.widget.__vNode__ = this
}
this.widget.props = this.props
if (!this.isBasicWidget) {
const node: VNode | void = this.widget.render()
if (!node) throw new Error(`Widget's render method must return a widget. Error in: <${this.type} />`)
node.parent = this
this.children = [node]
}
}
// 执行组件更新
doUpdate(): VNode | void {
if(!this.widget) {
this.widget = new this.widgetBuilder(this.props)
this.widget.__vNode__ = this
}
const newRenderNode: VNode | void = this.widget.render()
if (!newRenderNode) throw new Error(`Widget's render method must return a widget. Error in: <${this.type} />`)
return newRenderNode
}
// 新旧节点融合
doMerge(oldNode: VNode) {
this.contextId = oldNode.contextId
this.hasMount = true
this.nodeId = oldNode.nodeId
this.nativeViewId = oldNode.nativeViewId
this.widget = oldNode.widget
this.widget.__vNode__ = this
}
// 将 vNode 转换为 flutter 可用的 json
toRenderData(pageName: string, setStateful: boolean = true): RenderData {
this.pageName = pageName
if (this.pageNode) this.pageNode = void 0
this.findEventsInProps()
this.doRender()
if (!this.isBasicWidget) {
// 非原子组件,children 只有 1 个元素
// flutter version 1.1.0 及以上,所有自定义组件都是 stateful
// flutter version 1.1.0 以下,只有声明了 state 的自定义组件才是 stateful
if (this.children.length) {
const child: VNode = this.children[0]
if (Util.isNil(child.key)) child.parentKey = this.key || this.parentKey
return child.toRenderData(pageName, true)
}
} else {
let childrenRenderData: any = {}
this.mapChildren((vNode: VNode, key: string, fromArray: boolean) => {
if (!fromArray) childrenRenderData[key] = vNode.toRenderData(pageName, false)
else {
if (!childrenRenderData[key]) childrenRenderData[key] = []
childrenRenderData[key].push(vNode.toRenderData(pageName, false))
}
if (key === ChildrenKey && !this.childrenIsArray) {
childrenRenderData[this.childrenMapedName] = childrenRenderData[key][0]
delete childrenRenderData[key]
}
})
if (this.isPageNode) {
const rootNode: VNode = this.fetchRootNode()
rootNode.pageNode = this
}
if (this.isNativeViewNode) {
const params = this.props.params || {}
if (!params.__viewId__) {
params.__viewId__ = this.nativeViewId
this.props.params = params
}
}
return {
name: this.type,
widgetId: this.nodeId,
isStateful: setStateful,
pageName,
props: this.execPropsProvider(Object.assign({}, this.props, childrenRenderData)),
key: this.key || this.parentKey
}
}
}
private execPropsProvider(props: Object): Object {
const propsProviders: ThreshProvider[] = threshApp.providers.propsProvider || []
let index = propsProviders.length - 1
while (index > -1) {
const provider = propsProviders[index--]
const tempProps = provider.propsProvider(props)
if (tempProps) props = tempProps
}
return props
}
// 在当前 vnode 上查找目标节点
fetch(targetNodeId: string): VNode {
if (this.nodeId === targetNodeId) return this
let targetVNode: VNode
this.mapChildren((vNode: VNode) => {
if (vNode.nodeId === targetNodeId) {
targetVNode = vNode
return false
}
targetVNode = vNode.fetch(targetNodeId)
return !targetVNode
})
return targetVNode
}
// 查找距离当前节点向上最近的可更新的原子节点
fetchNearlyCanUpdateBasicNode(): VNode {
const node: VNode = this.fetchNearlyCustomNode()
let firstChildNode: VNode = node.children[0]
while (firstChildNode && !firstChildNode.isBasicWidget) {
firstChildNode = firstChildNode.children[0]
}
return firstChildNode
}
// 查找距离当前节点向上最近的非原子节点
fetchNearlyCustomNode(): VNode {
if (!this.isBasicWidget) return this
const parent: VNode | void = this.parent
if (parent) return parent.fetchNearlyCustomNode()
return this
}
// 查找距离当前节点向下最近的原子节点
fetchNearlyBasicNode(): VNode {
if (this.isBasicWidget) return this
return this.children[0].fetchNearlyBasicNode()
}
// 查找根节点
fetchRootNode(): VNode {
if (!this.parent) return this
const parent: VNode | void = this.parent
if (parent) return parent.fetchRootNode()
return this
}
// 查找页面名
fetchNodePageName(): string {
if (this.pageName) return this.pageName
if (this.parent) return this.parent.fetchNodePageName()
return ''
}
// 毕竟两个节点的 type & key 是否相等
isSameAs(otherNode: VNode) {
return this.type === otherNode.type && this.key === otherNode.key
}
// 触发组件上的事件
async invokeEvent(targetNodeId: string, eventId: string, eventType: string, params: any) {
const targetNode: VNode = this.fetch(targetNodeId)
if (!targetNode) return
const eventFn: Function = targetNode.events[eventId]
if (!eventFn) return
if (!eventType || !VNode.asyncEventTypes.includes(eventType)) eventFn(params)
else {
try {
await eventFn(params)
} finally {
// Hack fix
// 异步操作完成后的32毫秒再发送停止异步操作的通知
// 异步操作中可能含有setState,从而保证停止通知在setState后再发送
TimerManager.setTimeout(() => {
this.stopAsyncEvent(targetNode, eventType)
}, 32)
}
}
}
// 停止异步事件
stopAsyncEvent(node: VNode, eventType: string) {
if (!node || !eventType || !VNode.asyncEventTypes.includes(eventType)) return
if (node.type === 'ListView') {
(node.widget as unknown as ListView).stopAsyncOperate(eventType === 'onRefresh' ? 'refresh' : 'loadMore')
}
}
// 触发组件生命周期方法
invokeLifeCycle(lifeStep: string) {
if (!this.widget || !LifeCycle.isExist(lifeStep)) return
const lifeStepIsDidUnmount = lifeStep === LifeCycle.widgetDidUnmount
// 当生命周期不是卸载时,从子组件向父组件依次触发
if (!lifeStepIsDidUnmount) {
this._invokeChildrenLifeCycle(lifeStep)
}
if (!this.hasMount && lifeStep === LifeCycle.widgetDidUpdate) {
this.hasMount = true
this._invokeMountRef()
this.widget.widgetDidMount()
} else {
this.hasMount = !lifeStepIsDidUnmount
this._invokeMountRef()
this.widget[lifeStep]()
}
// 当组件卸载时,didUnmount从父组件向子组件触发
if (lifeStepIsDidUnmount) {
this._invokeChildrenLifeCycle(lifeStep)
}
}
private _invokeChildrenLifeCycle(lifeStep: string) {
this.mapChildren((vNode: VNode) => {
vNode.invokeLifeCycle(lifeStep)
})
}
private _invokeMountRef() {
if (!this.ref) return
if (this.hasMount && !this._refHasMount) {
this.ref(this.widget)
this._refHasMount = true
}
else if (!this.hasMount && this._refHasMount) {
this.ref(undefined)
this._refHasMount = false
}
}
// 向数组尾部添加子节点
appendChild(child: VNode, target: VNode[] = this.children) {
if (target.includes(child)) this.removeChild(child)
target.push(child)
}
// 移除数组中的某个节点
removeChild(child: VNode, target: VNode[] = this.children) {
const removeIndex: number = target.indexOf(child)
if (removeIndex > -1) target.splice(removeIndex, 1)
}
// 向 basicWidgetPropChildren 中的某个数组属性添加子节点
appendChildInArrayProps(child: VNode, propName: string) {
this.appendChild(child, this.getTargetChildrenArrayInPropChildren(propName))
}
// 遍历当前节点或目标中的所有子节点
private mapChildren(
cb: (vNode: VNode, key: string, fromArray: boolean) => boolean | void,
mapTarget?: PropChildrenVNode | VNode[]
) {
if (!mapTarget) {
for (let i = 0; i < this.children.length; i++) {
const item: VNode = this.children[i]
if (item instanceof VNode && cb(item, ChildrenKey, true) === false) return
}
for (let key in this.basicWidgetPropChildren) {
const item: PropChildrenVNodeType = this.basicWidgetPropChildren[key]
if (!Array.isArray(item)) {
if (item instanceof VNode && cb(item, key, false) === false) return
} else {
for (let i = 0; i < item.length; i++) {
if (item[i] instanceof VNode && cb(item[i], key, true) === false) return
}
}
}
} else {
if (Util.isArray(mapTarget)) {
for (let i = 0; i < (mapTarget as VNode[]).length; i++) {
const item: VNode = mapTarget[i]
if (item instanceof VNode && cb(item, '', true) === false) return
}
} else {
for (let key in mapTarget) {
const item: PropChildrenVNodeType = mapTarget[key]
if (!Array.isArray(item)) {
if (item instanceof VNode && cb(item, key, false) === false) return
} else {
for (let i = 0; i < item.length; i++) {
if (item[i] instanceof VNode && cb(item[i], key, true) === false) return
}
}
}
}
}
}
// 获取 basicWidgetPropChildren 中的某个数组属性
private getTargetChildrenArrayInPropChildren(propName: string): VNode[] {
if (!this.basicWidgetPropChildren[propName]) this.basicWidgetPropChildren[propName] = []
return this.basicWidgetPropChildren[propName] as VNode[]
}
}
/**
* 生命周期类
*/
export class LifeCycle {
private static lifes: string[] = [
'widgetDidMount',
'widgetDidUpdate',
'widgetDidUnmount',
/// Called when the current route has been pushed.
/// 当前的页面被push显示到用户面前 viewWillAppear.
'pageDidPush',
/// Called when the current route has been popped off.
/// 当前的页面被pop viewWillDisappear.
'pageDidPop',
/// Called when the top route has been popped off, and the current route
/// shows up.
/// 上面的页面被pop后当前页面被显示时 viewWillAppear.
'pageDidPopNext',
/// Called when a new route has been pushed, and the current route is no
/// longer visible.
/// 从当前页面push到另一个页面 viewWillDisappear.
'pageDidPushNext',
]
static widgetDidMount: string = LifeCycle.lifes[0]
static widgetDidUpdate: string = LifeCycle.lifes[1]
static widgetDidUnmount: string = LifeCycle.lifes[2]
static pageDidPush: string = LifeCycle.lifes[3]
static pageDidPop: string = LifeCycle.lifes[4]
static pageDidPopNext: string = LifeCycle.lifes[5]
static pageDidPushNext: string = LifeCycle.lifes[6]
static isExist(lifeStep: string): boolean {
return LifeCycle.lifes.indexOf(lifeStep) > -1
}
}