优化
This commit is contained in:
40
client/shared/components/Portal/Consumer.tsx
Normal file
40
client/shared/components/Portal/Consumer.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import type { PortalMethods } from './context';
|
||||
|
||||
export type PortalConsumerProps = {
|
||||
hostName: string;
|
||||
manager: PortalMethods;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export class PortalConsumer extends React.Component<PortalConsumerProps> {
|
||||
_key: any;
|
||||
componentDidMount() {
|
||||
if (!this.props.manager) {
|
||||
throw new Error(
|
||||
'Looks like you forgot to wrap your root component with `PortalHost` component.\n'
|
||||
);
|
||||
}
|
||||
|
||||
this._key = this.props.manager.mount(
|
||||
this.props.hostName,
|
||||
this.props.children
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.manager.update(
|
||||
this.props.hostName,
|
||||
this._key,
|
||||
this.props.children
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.manager.unmount(this.props.hostName, this._key);
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
64
client/shared/components/Portal/Manager.tsx
Normal file
64
client/shared/components/Portal/Manager.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
export type State = {
|
||||
portals: {
|
||||
key: number;
|
||||
children: React.ReactNode;
|
||||
}[];
|
||||
};
|
||||
interface PortalManagerProps {
|
||||
renderManagerView: (children: React.ReactNode) => React.ReactElement;
|
||||
}
|
||||
export interface PortalManagerState {
|
||||
portals: any[];
|
||||
}
|
||||
/**
|
||||
* Portal host is the component which actually renders all Portals.
|
||||
*/
|
||||
export class PortalManager extends React.PureComponent<
|
||||
PortalManagerProps,
|
||||
PortalManagerState
|
||||
> {
|
||||
state: State = {
|
||||
portals: [],
|
||||
};
|
||||
|
||||
mount = (key: number, children: React.ReactNode) => {
|
||||
this.setState((state) => ({
|
||||
portals: [...state.portals, { key, children }],
|
||||
}));
|
||||
};
|
||||
|
||||
update = (key: number, children: React.ReactNode) => {
|
||||
this.setState((state) => ({
|
||||
portals: state.portals.map((item) => {
|
||||
if (item.key === key) {
|
||||
return { ...item, children };
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
unmount = (key: number) => {
|
||||
this.setState((state) => ({
|
||||
portals: state.portals.filter((item) => item.key !== key),
|
||||
}));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { renderManagerView } = this.props;
|
||||
return this.state.portals.map(({ key, children }, i) => (
|
||||
<React.Fragment key={key}>{renderManagerView(children)}</React.Fragment>
|
||||
// <View
|
||||
// key={key}
|
||||
// collapsable={
|
||||
// false /* Need collapsable=false here to clip the elevations, otherwise they appear above sibling components */
|
||||
// }
|
||||
// pointerEvents="box-none"
|
||||
// style={[StyleSheet.absoluteFill, { zIndex: 1000 + i }]}
|
||||
// >
|
||||
// {children}
|
||||
// </View>
|
||||
));
|
||||
}
|
||||
}
|
||||
10
client/shared/components/Portal/README.md
Normal file
10
client/shared/components/Portal/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## Portal
|
||||
|
||||
**暂时没有使用**
|
||||
|
||||
这是一组用于使用命令式方法将一个React DOM映射到相关的位置的方法
|
||||
可以指定不同的portal位置
|
||||
|
||||
用于处理对于每个节点都需要创建一个相关的Modal的情况
|
||||
|
||||
> 参考 https://github.com/ant-design/ant-design-mobile-rn/blob/master/components/portal/ 的实现
|
||||
206
client/shared/components/Portal/buildPortal.tsx
Normal file
206
client/shared/components/Portal/buildPortal.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useRef,
|
||||
Fragment,
|
||||
useContext,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { PortalManager } from './Manager';
|
||||
import { createPortalContext } from './context';
|
||||
import { PortalConsumer } from './Consumer';
|
||||
import _isNil from 'lodash/isNil';
|
||||
import { DefaultEventEmitter } from './defaultEventEmitter';
|
||||
|
||||
type Operation =
|
||||
| { type: 'mount'; key: number; children: React.ReactNode }
|
||||
| { type: 'update'; key: number; children: React.ReactNode }
|
||||
| { type: 'unmount'; key: number };
|
||||
|
||||
// Events const
|
||||
const addType = 'ADD_PORTAL';
|
||||
const removeType = 'REMOVE_PORTAL';
|
||||
|
||||
// For react-native
|
||||
// const TopViewEventEmitter = DeviceEventEmitter || new NativeEventEmitter();
|
||||
|
||||
const defaultRenderManagerViewFn = (children: React.ReactNode) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
interface EventEmitterFunc {
|
||||
emit: (...args: any[]) => any;
|
||||
addListener: (...args: any[]) => any;
|
||||
removeListener: (...args: any[]) => any;
|
||||
}
|
||||
export interface BuildPortalOptions {
|
||||
/**
|
||||
* 唯一标识名
|
||||
* 用于多实例的情况
|
||||
*/
|
||||
hostName?: string;
|
||||
|
||||
/**
|
||||
* 事件监听函数
|
||||
*/
|
||||
eventEmitter?: EventEmitterFunc;
|
||||
|
||||
/**
|
||||
* 负责Portal Manager如何生成函数的逻辑
|
||||
*/
|
||||
renderManagerView?: (children: React.ReactNode) => React.ReactElement;
|
||||
}
|
||||
export function buildPortal(options: BuildPortalOptions) {
|
||||
const {
|
||||
hostName = 'default',
|
||||
eventEmitter = new DefaultEventEmitter(),
|
||||
renderManagerView = defaultRenderManagerViewFn,
|
||||
} = options;
|
||||
let nextKey = 10000;
|
||||
|
||||
const add = (el: React.ReactNode): number => {
|
||||
const key = nextKey++;
|
||||
eventEmitter.emit(addType, hostName, el, key);
|
||||
return key;
|
||||
};
|
||||
|
||||
const remove = (key: number): void => {
|
||||
eventEmitter.emit(removeType, hostName, key);
|
||||
};
|
||||
|
||||
const PortalContext = createPortalContext(hostName);
|
||||
|
||||
const PortalHost: React.FC<PropsWithChildren> = React.memo((props) => {
|
||||
const managerRef = useRef<PortalManager>();
|
||||
const nextKeyRef = useRef<number>(0);
|
||||
const queueRef = useRef<Operation[]>([]);
|
||||
const hostNameRef = useRef(hostName);
|
||||
useEffect(() => {
|
||||
hostNameRef.current = hostName;
|
||||
}, [hostName]);
|
||||
|
||||
const mount: any = useCallback(
|
||||
(name: string, children: React.ReactNode, _key?: number) => {
|
||||
if (name !== hostNameRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = _key || nextKeyRef.current++;
|
||||
if (managerRef.current) {
|
||||
managerRef.current.mount(key, children);
|
||||
} else {
|
||||
queueRef.current.push({ type: 'mount', key, children });
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
(name: string, key: number, children: React.ReactNode) => {
|
||||
if (name !== hostNameRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (managerRef.current) {
|
||||
managerRef.current.update(key, children);
|
||||
} else {
|
||||
const op: Operation = { type: 'mount', key, children };
|
||||
const index = queueRef.current.findIndex(
|
||||
(o) => o.type === 'mount' || (o.type === 'update' && o.key === key)
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
queueRef.current[index] = op;
|
||||
} else {
|
||||
queueRef.current.push(op);
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const unmount = useCallback((name: string, key: number) => {
|
||||
if (name !== hostNameRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (managerRef.current) {
|
||||
managerRef.current.unmount(key);
|
||||
} else {
|
||||
queueRef.current.push({ type: 'unmount', key });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
eventEmitter.addListener(addType, mount);
|
||||
eventEmitter.addListener(removeType, unmount);
|
||||
|
||||
return () => {
|
||||
eventEmitter.removeListener(addType, mount);
|
||||
eventEmitter.removeListener(removeType, unmount);
|
||||
};
|
||||
}, [mount, unmount]);
|
||||
|
||||
useEffect(() => {
|
||||
// 处理队列
|
||||
const queue = queueRef.current;
|
||||
const manager = managerRef.current;
|
||||
|
||||
while (queue.length && manager) {
|
||||
const action = queue.pop();
|
||||
if (!action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'mount':
|
||||
manager.mount(action.key, action.children);
|
||||
break;
|
||||
case 'update':
|
||||
manager.update(action.key, action.children);
|
||||
break;
|
||||
case 'unmount':
|
||||
manager.unmount(action.key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PortalContext.Provider
|
||||
value={{
|
||||
mount,
|
||||
update,
|
||||
unmount,
|
||||
}}
|
||||
>
|
||||
<Fragment>{props.children}</Fragment>
|
||||
<PortalManager
|
||||
ref={managerRef as any}
|
||||
renderManagerView={renderManagerView}
|
||||
/>
|
||||
</PortalContext.Provider>
|
||||
);
|
||||
});
|
||||
PortalHost.displayName = 'PortalHost-' + hostName;
|
||||
|
||||
const PortalRender: React.FC<PropsWithChildren> = React.memo((props) => {
|
||||
const manager = useContext(PortalContext);
|
||||
|
||||
if (_isNil(manager)) {
|
||||
console.error('Not find PortalContext');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalConsumer hostName={hostName} manager={manager}>
|
||||
{props.children}
|
||||
</PortalConsumer>
|
||||
);
|
||||
});
|
||||
PortalRender.displayName = 'PortalRender-' + hostName;
|
||||
|
||||
return { add, remove, PortalHost, PortalRender };
|
||||
}
|
||||
14
client/shared/components/Portal/context.ts
Normal file
14
client/shared/components/Portal/context.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
export type PortalMethods = {
|
||||
mount: (name: string, children: React.ReactNode) => number;
|
||||
update: (name: string, key: number, children: React.ReactNode) => void;
|
||||
unmount: (name: string, key: number) => void;
|
||||
};
|
||||
|
||||
export function createPortalContext(name: string) {
|
||||
const PortalContext = React.createContext<PortalMethods | null>(null);
|
||||
PortalContext.displayName = 'PortalContext-' + name;
|
||||
|
||||
return PortalContext;
|
||||
}
|
||||
40
client/shared/components/Portal/defaultEventEmitter.tsx
Normal file
40
client/shared/components/Portal/defaultEventEmitter.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
export class DefaultEventEmitter {
|
||||
// 参考: https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget
|
||||
listeners: any = {};
|
||||
|
||||
emit(type: string, ...args: any[]) {
|
||||
if (!(type in this.listeners)) {
|
||||
return;
|
||||
}
|
||||
const stack = this.listeners[type];
|
||||
for (let i = 0, l = stack.length; i < l; i++) {
|
||||
stack[i].call(this, event);
|
||||
const func = stack[i];
|
||||
|
||||
if (typeof func === 'function') {
|
||||
func(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addListener(type: string, callback: (...args: any[]) => any) {
|
||||
if (!(type in this.listeners)) {
|
||||
this.listeners[type] = [];
|
||||
}
|
||||
|
||||
this.listeners[type].push(callback);
|
||||
}
|
||||
|
||||
removeListener(type: string, callback: (...args: any[]) => any): any {
|
||||
if (!(type in this.listeners)) {
|
||||
return;
|
||||
}
|
||||
const stack = this.listeners[type];
|
||||
for (let i = 0, l = stack.length; i < l; i++) {
|
||||
if (stack[i] === callback) {
|
||||
stack.splice(i, 1);
|
||||
return this.removeListener(type, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
client/shared/components/Portal/index.ts
Normal file
2
client/shared/components/Portal/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { buildPortal } from './buildPortal';
|
||||
export { DefaultEventEmitter } from './defaultEventEmitter';
|
||||
Reference in New Issue
Block a user