egreact
Version:
A react render for egret 一个为 egret 而生的 react 渲染器
326 lines (298 loc) • 11.8 kB
text/typescript
import { Reconciler } from 'react-reconciler'
import { getCanvas, findEgretAncestor, is, getActualInstance, throttle } from './utils'
import { findTargetByPosition } from './outside'
import { catalogueMap } from './Host/index'
import { IRenderInfo, Instance } from './type'
import { CONSTANTS } from './constants'
/**
* @description dom px 和 egret 坐标值换算比例
*/
function calculateScale() {
const canvas = getCanvas()
const rect = canvas.getBoundingClientRect()
return rect.width / egret.lifecycle.stage.stageWidth
}
const [storeOriginComputedStyle, getOriginComputedStyle] = (function () {
var getComputedStyle: typeof window.getComputedStyle
return [() => getComputedStyle || (getComputedStyle = window.getComputedStyle), () => getComputedStyle]
})()
const getEmptyCSSStyleSheet = (function () {
var emptyCSSStyleSheet: ReturnType<typeof getComputedStyle>
return () =>
emptyCSSStyleSheet ||
(emptyCSSStyleSheet = {
...getOriginComputedStyle()(document.createElement('div')),
borderLeftWidth: '0',
borderRightWidth: '0',
borderTopWidth: '0',
borderBottomWidth: '0',
marginLeft: '0',
marginRight: '0',
marginTop: '0',
marginBottom: '0',
paddingLeft: '0',
paddingRight: '0',
paddingTop: '0',
paddingBottom: '0',
})
})()
// 记录最近的一个显示对象信息
let latestEgretRect = { w: 0, h: 0, x: 0, y: 0 }
/**
* @description 挂载 getBoundingClientRect,给 react devtool 调用
*/
export function getBoundingClientRect(instance: Instance<egret.DisplayObject>) {
const egretInstance = findEgretAncestor(instance)
const canvasRect = getCanvas().getBoundingClientRect()
if (!egretInstance) return canvasRect
const point = egretInstance.localToGlobal(0, 0)
// 因为 egret 中的坐标系比例与dom中不同,所以需要换算
const scale = calculateScale()
const x = point.x * scale + canvasRect.left
const y = point.y * scale + canvasRect.top
const width = egretInstance.width * scale
const height = egretInstance.height * scale
latestEgretRect = { w: instance.width, h: instance.height, x: instance.x, y: instance.y }
return { x, y, width, height, left: x, top: y }
}
let isSelectedEgret = false
/**
* @description 代理 window.getComputedStyle,使它也能计算某个函数的宽高大小
* @link https://github.com/facebook/react/blob/29c2c633159cb2171bb04fe84b9caa09904388e8/packages/react-devtools-shared/src/backend/views/utils.js#L113
*/
function proxyGetComputedStyle() {
storeOriginComputedStyle()
window.getComputedStyle = function (el, pseudo) {
if (Object.entries(catalogueMap).some(([n, catalogue]) => catalogue.__Class && el instanceof catalogue.__Class)) {
isSelectedEgret = true
return getEmptyCSSStyleSheet()
} else {
isSelectedEgret = false
return getOriginComputedStyle().call(this, el, pseudo)
}
}
}
function unProxyGetComputedStyle() {
window.getComputedStyle = getOriginComputedStyle()
}
const [storeOriginListeners, getOriginListeners] = (function () {
var listeners: { addEventListener: typeof addEventListener; removeEventListener: typeof removeEventListener }
return [
() =>
listeners ||
(listeners = { addEventListener: window.addEventListener, removeEventListener: window.removeEventListener }),
() => listeners,
]
})()
type EventHandler = EventListener | EventListenerObject
export type ProxyEventInfo = [
// 前三个是事件参数,
string,
EventListener,
(boolean | { capture?: boolean })?,
EventListener?, // 参数是代理的事件处理
any?, // 第五个放置其他信息
]
// 收集代理事件信息
const eventInfoCollection: ProxyEventInfo[] = []
/**
* @description 找到相应的监听,判断条件 type,handle,capture 相同即为同个监听
* @link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener#matching_event_listeners_for_removal
*/
export function findMatchEventIndex(
[type1, handler1, options1]: ProxyEventInfo,
collection = eventInfoCollection, // 此参数方便测试
) {
return collection.findIndex(([type2, handler2, options2]) => {
const capture1 = typeof options1 === 'boolean' ? options1 : options1?.capture ?? false
const capture2 = typeof options2 === 'boolean' ? options2 : options2?.capture ?? false
if (type1 === type2 && handler1 === handler2 && capture1 === capture2) {
return true
}
return false
})
}
/**
* @description 找到相应的监听并提取出来
*/
export function extraMatchEvent(info: ProxyEventInfo): ProxyEventInfo | null {
const index = findMatchEventIndex(info)
if (index > -1) {
return eventInfoCollection.splice(index, 1)[0]
} else return null
}
let rafId: number | null = null
function specifiedShadowInfoForEgret() {
const shadows = document.querySelectorAll('div[style^="z-index: 10000000;"]')
for (let i = 0; i < shadows.length; i++) {
const shadow = shadows[i] as HTMLDivElement
if (i === 0) {
shadow.style.display = 'block'
const rectSpan = shadow.firstChild?.lastChild
// 修改dom,显示之前记录的宽高轴
if (isSelectedEgret && rectSpan)
rectSpan.textContent = `w:${latestEgretRect.w} h:${latestEgretRect.h} x:${latestEgretRect.x} y:${latestEgretRect.y} (in egret)`
} else shadow.style.display = 'none'
}
rafId = requestAnimationFrame(specifiedShadowInfoForEgret)
}
/**
* @description 代理window监听器,目的是代理 react-devtool 添加的事件
* @link https://github.com/facebook/react/blob/c3d7a7e3d72937443ef75b7e29335c98ad0f1424/packages/react-devtools-shared/src/backend/views/Highlighter/index.js#L41
*/
function proxyListener() {
// 对 shadow 显示 egret 对象特殊化
if (!rafId) {
rafId = requestAnimationFrame(specifiedShadowInfoForEgret)
}
storeOriginListeners()
window.addEventListener = function (type: string, listener: EventHandler, options?: boolean | { capture?: boolean }) {
if (
is.fun(listener) &&
['click', 'mousedown', 'mouseover', 'mouseup', 'pointerdown', 'pointerover', 'pointerup'].includes(type)
) {
const proxyHandler = function (e: MouseEvent) {
const { pageX: x, pageY: y } = e
const r = getCanvas().getBoundingClientRect()
r.x += window.scrollX
r.y += window.scrollY
// 判断点击点是否处于画布中
let target: egret.DisplayObject | null = null
if (x > r.x && x < r.x + r.width && y > r.y && y < r.y + r.height) {
const scale = calculateScale()
target = findTargetByPosition(egret.lifecycle.stage, (x - r.x) / scale, (y - r.y) / scale)
// 模拟一个新的 event,目的是改变 target
if (target) {
e = {
...e,
preventDefault: e.preventDefault.bind(e),
stopPropagation: e.stopPropagation.bind(e),
target: target as any,
}
}
}
listener.call(this, e)
} as EventListener
// 只有非相同的事件才被加入
if (findMatchEventIndex([type, listener, options]) === -1) {
getOriginListeners().addEventListener.call(window, type, proxyHandler, options)
const info: ProxyEventInfo = [type, listener, options, proxyHandler]
if (type === 'pointerover') {
// 特殊处理,react devtool 没有监听 pointermove,但是进入画布的话,只有一次 pointerover
// 为了鼠标移动就能判断,在 canvas 上监听 pointermove
const throttlerProxyHandler = throttle(proxyHandler, 150)
const canvas = getCanvas()
canvas.addEventListener('pointermove', throttlerProxyHandler, options)
// 保存一下最后这个参数
info.push(throttlerProxyHandler)
}
eventInfoCollection.push(info)
}
} else {
getOriginListeners().addEventListener.call(window, type, listener, options)
}
}
window.removeEventListener = function (
type: string,
listener: EventHandler,
options: boolean | { capture?: boolean } = false,
) {
if (is.fun(listener)) {
const info = extraMatchEvent([type, listener, options])
if (info) {
getOriginListeners().removeEventListener.call(window, type, info[3]!, options)
if (type === 'pointerover' && info[4]) {
getCanvas().removeEventListener('pointermove', info[4], options)
}
} else {
getOriginListeners().removeEventListener.call(window, type, listener, options)
}
} else {
getOriginListeners().removeEventListener.call(window, type, listener, options)
}
}
}
function unProxyListener() {
if (rafId) {
cancelAnimationFrame(rafId)
rafId = null
}
window.addEventListener = getOriginListeners().addEventListener
window.removeEventListener = getOriginListeners().removeEventListener
while (eventInfoCollection.length) {
const info = eventInfoCollection.pop()!
// 移除代理
window.removeEventListener(info[0], info[3] as EventHandler, info[2])
// 使用原生重新挂载
window.addEventListener(info[0], info[1], info[2])
}
}
export function findFiberByHostInstance(instance: Instance) {
return instance?.[CONSTANTS.INFO_KEY]?.fiber ?? null
}
/**
* @description 兼容 devtool 用到的 dom 属性
*/
export function addCompatibleDomAttributes(instance: any) {
if (is.obj(instance)) {
// devtool need to get rect of element
// https://github.com/facebook/react/blob/29c2c633159cb2171bb04fe84b9caa09904388e8/packages/react-devtools-shared/src/backend/views/utils.js#L108
instance.getBoundingClientRect = () => getBoundingClientRect(instance)
// https://github.com/facebook/react/blob/327e4a1f96fbb874001b17684fbb073046a84938/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js#L193
instance.nodeType = 1
// https://github.com/facebook/react/blob/327e4a1f96fbb874001b17684fbb073046a84938/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js#L233
instance.nodeName = instance.__class__
instance.ownerDocument = document
}
}
export function deleteCompatibleDomAttributes(instance: any) {
if (is.obj(instance)) {
delete instance.getBoundingClientRect
delete instance.nodeType
delete instance.nodeName
delete instance.ownerDocument
}
}
import packageJson from '../package.json'
export function injectIntoDevTools(reconciler: Reconciler<any, any, any, any, any>) {
reconciler.injectIntoDevTools({
bundleType: 0,
rendererPackageName: 'egreact',
version: packageJson.version,
findFiberByHostInstance,
})
}
let isProxyDevTools = false
export function proxyHackForDevTools() {
if (!isProxyDevTools) {
proxyGetComputedStyle()
proxyListener()
isProxyDevTools = true
}
}
export function unProxyHackForDevTools() {
if (isProxyDevTools) {
unProxyGetComputedStyle()
unProxyListener()
isProxyDevTools = false
}
}
export function injectMemoizedProps(instance: Instance, info: IRenderInfo) {
if (!info.fiber) return
info.fiber.memoizedProps = {
...info.fiber.memoizedProps,
[CONSTANTS.STATE_NODE_KEY]: instance,
[CONSTANTS.TARGET_KEY]: getActualInstance(instance),
[CONSTANTS.INFO_KEY]: instance[CONSTANTS.INFO_KEY],
[CONSTANTS.FIBER_KEY]: info.fiber,
}
if (info.fiber.alternate && is.obj(info.fiber.alternate.memoizedProps)) {
info.fiber.alternate.memoizedProps = {
...info.fiber.alternate.memoizedProps,
[CONSTANTS.STATE_NODE_KEY]: instance,
[CONSTANTS.TARGET_KEY]: getActualInstance(instance),
[CONSTANTS.INFO_KEY]: instance[CONSTANTS.INFO_KEY],
[CONSTANTS.FIBER_KEY]: info.fiber.alternate,
}
}
}