import React, { useContext, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import get from 'lodash.get' import reactDeclarations from './declarations' import Tanfu, { CoreEngine, TanfuAdapter, TanfuPlugin } from 'tanfu-core'; import { HOST_LIFECYCLE_ID } from 'tanfu-core/es/constants' import { nanoid } from 'nanoid'; class TanfuReactAdapter extends TanfuAdapter { createRenderView(view: any, props: any, type: number, engine?: CoreEngine) { const tId = props?.['t-id'] let RenderUI = view; const reactProps: any = {} Object.keys(props ?? {}).forEach(key => { if(key === 'class'){ reactProps['className'] = props['class'] } else reactProps[formatToHump(key)] = props?.[key] }) if (Tanfu.isTanfuView(view)) { const { view: ui, engine: tanfuEngine } = Tanfu.translateTanfuView(view, props) RenderUI = React.memo(function (props: any) { return <ReactView props={props} engine={tanfuEngine} children={ui} /> }) if(tId) RenderUI = createElement(RenderUI, engine) return <RenderUI key={nanoid()} {...reactProps} /> } if (type === Node.TEXT_NODE) { return view } else if (type === Node.ELEMENT_NODE && view) { if (tId) RenderUI = createElement(view, engine) return <RenderUI key={nanoid()} {...reactProps} /> } return <React.Fragment key={nanoid()}></React.Fragment>; } } export default class TanfuReactPlugin extends TanfuPlugin { install(tanfu: Tanfu) { tanfu.addDeclarations(reactDeclarations) tanfu.addAdapter(new TanfuReactAdapter()) } } interface ReactViewProps { children: React.ReactChild; engine: CoreEngine; props: Record<string, any> } /** 包裹React视图组件,生成CoreEngine并执行控制器 */ function ReactView({ children, engine, props }: ReactViewProps) { useMemo(() => { engine.props = props engine.willMountHook.call(HOST_LIFECYCLE_ID) }, []) useMemo(() => { engine.props = props engine.updateHook.call(HOST_LIFECYCLE_ID) }, [props]) useEffect(() => { engine.didMountHook.call(HOST_LIFECYCLE_ID) return () => { engine.willUnmountHook.call(HOST_LIFECYCLE_ID) } }, []) return <>{children}</> } type UIComponent<P> = React.NamedExoticComponent<React.ComponentProps<React.ComponentType<P>> & { tId?: string }> type Element<P> = UIComponent<P> /** 创建视图元素 */ function createElement<P = {}>( UI: React.ComponentType<P>, engine?: CoreEngine ): Element<P> { return React.memo(function (props: React.ComponentProps<typeof UI> & { tId?: string }) { const { tId: id, ...otherProps } = props const [tId] = useState(id || String(Date.now())) useMemo(() => { tId && engine && engine.willMountHook.call(tId) }, [tId]) // 构建injectCallback生成的回调属性 const callbackFnProps = useMemo(() => { const callback: any = {} if (tId) { engine?.callbackHook.fork(tId).forEach((fnName) => { callback[fnName] = (...args: any[]) => { // @ts-ignore otherProps?.[fnName]?.(...args) engine.callbackHook.call([tId, fnName], ...args) } }) } return callback }, [otherProps]) // 获取元素state const state = tId ? engine?.getState(tId) : {} // 元素属性,注意:直接值元素设置的属性值会覆盖使用engine.setState设置的元素值, // 如果想两种方式都生效,需要使用受控的模式进行更新属性 const currentState = Object.assign({ ...otherProps }, state, { ...callbackFnProps }) const preState = usePrevious(currentState) const forceUpdate = useForceUpdate(); useEffect(() => { if (tId) { engine?.watchElementHook?.fork(tId).forEach((dep) => { if (get(currentState, dep) !== get(preState, dep)) { engine?.watchElementHook.call([tId, dep]) } }) } }) useEffect(() => { if (tId) { engine?.forceUpdateHook.on(tId, forceUpdate) engine?.didMountHook?.call(tId) } return () => { if (tId) { engine?.forceUpdateHook.off(tId, forceUpdate) engine?.willUnmountHook?.call(tId) } } }, []) // @ts-ignore return <UI {...currentState} /> }) } function useForceUpdate() { const [_, forceUpdate] = useReducer(c => ++c, 0); return forceUpdate } function usePrevious<ValueType = any>(value: ValueType) { const ref = useRef<ValueType>() useEffect(() => { ref.current = value }) return ref.current } // 字符串中间线转驼峰 const formatToHump = (value: string) => { return value.replace(/\-(\w)/g, (_, letter) => letter.toUpperCase()) } // 字符串驼峰转中间线 const formatToLine = (value: string) => { return value.replace(/([A-Z])/g, '-$1').toLowerCase() }