Skip to content
This repository has been archived by the owner on Mar 7, 2024. It is now read-only.

Commit

Permalink
feat: 支持 App 是一个 React 组件
Browse files Browse the repository at this point in the history
  • Loading branch information
yesmeck committed Sep 2, 2019
1 parent c613127 commit 9b5bc84
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 24 deletions.
53 changes: 53 additions & 0 deletions packages/remax/src/AppContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import VNode, { Path, RawNode } from './VNode';
import { generate } from './instanceId';
import { FiberRoot } from 'react-reconciler';

interface SpliceUpdate {
path: Path;
start: number;
deleteCount: number;
items: RawNode[];
}

export default class AppContainer {
context: any;
root: VNode;
updateQueue: SpliceUpdate[] = [];
_rootContainer?: FiberRoot;

constructor(context: any) {
this.context = context;

this.root = new VNode({
id: generate(),
type: 'root',
container: this,
});
this.root.mounted = true;
}

requestUpdate(
path: Path,
start: number,
deleteCount: number,
...items: RawNode[]
) {
// ignore
}

createCallback(name: string, fn: Function) {
this.context[name] = fn;
}

appendChild(child: VNode) {
this.root.appendChild(child);
}

removeChild(child: VNode) {
this.root.removeChild(child);
}

insertBefore(child: VNode, beforeChild: VNode) {
this.root.insertBefore(child, beforeChild);
}
}
10 changes: 9 additions & 1 deletion packages/remax/src/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default class Container {
root: VNode;
updateQueue: SpliceUpdate[] = [];
_rootContainer?: FiberRoot;
updateTimer?: number | null;

constructor(context: any) {
this.context = context;
Expand All @@ -43,7 +44,7 @@ export default class Container {
items,
};
if (this.updateQueue.length === 0) {
setTimeout(() => this.applyUpdate());
this.updateTimer = setTimeout(() => this.applyUpdate());
}
this.updateQueue.push(update);
}
Expand Down Expand Up @@ -80,6 +81,13 @@ export default class Container {
this.updateQueue = [];
}

clearUpdate() {
if (this.updateTimer) {
clearTimeout(this.updateTimer);
this.updateTimer = null;
}
}

createCallback(name: string, fn: Function) {
this.context[name] = fn;
}
Expand Down
22 changes: 22 additions & 0 deletions packages/remax/src/ReactPortal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react';

const hasSymbol = typeof Symbol === 'function' && Symbol.for;

export const REACT_PORTAL_TYPE = hasSymbol
? Symbol.for('react.portal')
: 0xeaca;

export function createPortal(
children: React.ReactElement,
containerInfo: any,
key: string
) {
return {
// This tag allow us to uniquely identify this as a React Portal
$$typeof: REACT_PORTAL_TYPE,
key: key == null ? null : '' + key,
children,
containerInfo,
implementation: null,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,15 @@ Object {
"type": "plain-text",
},
],
"id": 2,
"id": 3,
"props": Object {
"class": "foo",
},
"text": undefined,
"type": "view",
},
],
"id": 0,
"id": 1,
"props": undefined,
"text": undefined,
"type": "root",
Expand Down
4 changes: 3 additions & 1 deletion packages/remax/src/__tests__/alipay/helpers/setupGlobals.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable */
import pages from './pages';
import createAppConfig from '../../../createAppConfig';

const app = createAppConfig(({ children }: any) => children);
// @ts-ignore
global['getApp'] = () => {};
global['getApp'] = () => app;
// @ts-ignore
global['getCurrentPages'] = () => pages;
// @ts-ignore
Expand Down
99 changes: 97 additions & 2 deletions packages/remax/src/createAppConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,98 @@
export default function createAppConfig(App: any) {
return new App();
import * as React from 'react';
import render from './render';
import AppContainer from './AppContainer';
import isClass from './utils/isClass';
import isClassComponent from './utils/isClassComponent';

class DefaultAppComponent extends React.Component {
render() {
return React.createElement(React.Fragment, null, this.props.children);
}
}

export default function createAppConfig(this: any, App: any) {
const createConfig = (
AppComponent: React.ComponentType = DefaultAppComponent
) => {
return {
_container: new AppContainer(this),

_pages: [] as any[],

_instance: null as any,

onLaunch(options: any) {
this._instance = this._render();

if (this._instance && this._instance.onLaunch) {
this._instance.onLaunch(options);
}
},

onShow(options: any) {
if (this._instance && this._instance.onShow) {
this._instance.onShow(options);
}
},

onHide() {
if (this._instance && this._instance.onHide) {
this._instance.onHide();
}
},

onError(error: any) {
if (this._instance && this._instance.onError) {
this._instance.onError(error);
}
},

// 支付宝
onShareAppMessage() {
if (this._instance && this._instance.onShareAppMessage) {
return this._instance.onShareAppMessage();
}
},

// 微信
onPageNotFound(options: any) {
if (this._instance && this._instance.onPageNotFound) {
return this._instance.onPageNotFound(options);
}
},

_mount(pageInstance: any) {
this._pages.push(pageInstance);
this._render();
},

_unmount(pageInstance: any) {
this._pages.splice(this._pages.indexOf(pageInstance), 1);
this._render();
},

_render() {
return render(
React.createElement(
AppComponent,
null,
this._pages.map(p => p.element)
),
this._container
);
},
};
};

// 兼容老的写法
if (isClass(App) && !isClassComponent(App)) {
// eslint-disable-next-line no-console
console.warn(
'使用非 React 组件定义 App 的方式已经废弃,详细请参考:https://remaxjs.org/guide/framework'
);
Object.assign(App.prototype, createConfig());
return new App();
} else {
return createConfig(App);
}
}
35 changes: 23 additions & 12 deletions packages/remax/src/createPageConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,45 @@ import createPageWrapper from './createPageWrapper';
import render from './render';
import { Lifecycle, callbackName } from './lifecycle';
import Container from './Container';
import { createPortal } from './ReactPortal';

let idCounter = 0;

export default function createPageConfig(Page: React.ComponentType<any>) {
const app = getApp();
const id = idCounter;
idCounter += 1;

return {
pageId: 'page_' + id,

data: {
action: {},
},

wrapper: null as any,
wrapperRef: React.createRef<any>(),

lifecycleCallback: {} as any,

onLoad(this: any, query: any) {
const PageWrapper = createPageWrapper(Page, query);
this.container = new Container(this);

this.wrapper = render(
React.createElement(PageWrapper, { page: this }),
this.container
this.element = createPortal(
React.createElement(PageWrapper, {
page: this,
ref: this.wrapperRef,
}),
this.container,
this.pageId
);

app._mount(this);
},

onUnload(this: any) {
this.unloaded = true;
if (this.requestUpdate) {
this.requestUpdate.clear();
}
render(null, this.container);
this.wrapper = null;
this.container.clearUpdate();
app._unmount(this);
},

/**
Expand Down Expand Up @@ -61,8 +72,8 @@ export default function createPageConfig(Page: React.ComponentType<any>) {
}

const callback = callbackName(lifecycle);
if (this.wrapper[callback]) {
return this.wrapper[callback](...args);
if (this.wrapperRef.current && this.wrapperRef.current[callback]) {
return this.wrapperRef.current[callback](...args);
}
},

Expand Down
2 changes: 0 additions & 2 deletions packages/remax/src/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import capitalize from './utils/capitalize';

declare const getCurrentPages: any;

export type Callback = (...args: any[]) => any;

export enum Lifecycle {
Expand Down
3 changes: 2 additions & 1 deletion packages/remax/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import ReactReconciler from 'react-reconciler';
import hostConfig from './hostConfig';
import Container from './Container';
import AppContainer from './AppContainer';

const ReactReconcilerInst = ReactReconciler(hostConfig as any);

Expand All @@ -15,7 +16,7 @@ function getPublicRootInstance(container: any) {

export default function render(
rootElement: React.ReactElement | null,
container: Container
container: Container | AppContainer
) {
// Create a root Container if it doesnt exist
if (!container._rootContainer) {
Expand Down
23 changes: 23 additions & 0 deletions packages/remax/src/utils/isClass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function fnBody(fn: any) {
return fn
.toString()
.replace(/^[^{]*{\s*/, '')
.replace(/\s*}[^}]*$/, '');
}

export default function isClass(fn: any) {
if (typeof fn !== 'function') {
return false;
}

if (/^class[\s{]/.test(toString.call(fn))) {
return true;
}

// babel.js classCallCheck() & inlined
const body = fnBody(fn);
return (
/classCallCheck/.test(body) ||
/TypeError\("Cannot call a class as a function"\)/.test(body)
);
}
8 changes: 5 additions & 3 deletions packages/remax/typings/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
declare module 'scheduler';
declare let my: any;
declare let tt: any;
declare let getApp: any;
declare module 'is-class';
declare const my: any;
declare const tt: any;
declare const getApp: any;
declare const getCurrentPages: any;

0 comments on commit 9b5bc84

Please sign in to comment.