Merge branch 'feat/rax-miniapp' of http://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine into feat/rax-miniapp

This commit is contained in:
wanying.jwy 2020-08-16 15:28:35 +08:00
commit 4e6f0a3fbb
6 changed files with 445 additions and 64 deletions

View File

@ -1,21 +1,44 @@
{"componentsTree": [{
"componentName": "Page",
"fileName": "home",
"props": {},
"children": [
{
"componentsTree": [
{
"componentName": "Container",
"componentName": "Page",
"fileName": "home1",
"props": {},
"children": [
{
"componentName": "Text",
"props": {
"type": "primary"
},
"children": ["Welcome to Your Rax App!"]
"componentName": "Container",
"props": {},
"children": [
{
"componentName": "Text",
"props": {
"type": "primary",
"content": "Page 1"
}
}
]
}
]
},
{
"componentName": "Page",
"fileName": "home2",
"props": {},
"children": [
{
"componentName": "Container",
"props": {},
"children": [
{
"componentName": "Text",
"props": {
"type": "primary",
"content": "Page 2"
}
}
]
}
]
}
]
}]
}
}

View File

@ -23,8 +23,10 @@
"@recore/obx": "^1.0.8",
"classnames": "^2.2.6",
"driver-universal": "^3.1.3",
"history": "^5.0.0",
"lodash": "^4.17.19",
"rax-find-dom-node": "^1.0.0",
"rax-use-router": "^3.0.0",
"react": "^16",
"react-dom": "^16.7.0"
},

View File

@ -0,0 +1,259 @@
// Inspired by react-router and universal-router
import { useState, useEffect, useLayoutEffect, createElement } from 'rax';
import pathToRegexp from 'path-to-regexp';
const cache = {};
function decodeParam(val) {
try {
return decodeURIComponent(val);
} catch (err) {
return val;
}
}
function matchPath(route, pathname, parentParams) {
let { path, routes, exact: end = true, strict = false, sensitive = false } = route;
// If not has path or has routes that should do not exact match
if (path == null || routes) {
end = false;
}
// Default path is empty
path = path || '';
const regexpCacheKey = `${path}|${end}|${strict}|${sensitive}`;
const keysCacheKey = regexpCacheKey + '|';
let regexp = cache[regexpCacheKey];
let keys = cache[keysCacheKey] || [];
if (!regexp) {
regexp = pathToRegexp(path, keys, {
end,
strict,
sensitive
});
cache[regexpCacheKey] = regexp;
cache[keysCacheKey] = keys;
}
const result = regexp.exec(pathname);
if (!result) {
return null;
}
const url = result[0];
const params = { ...parentParams, history: router.history, location: router.history.location };
for (let i = 1; i < result.length; i++) {
const key = keys[i - 1];
const prop = key.name;
const value = result[i];
if (value !== undefined || !Object.prototype.hasOwnProperty.call(params, prop)) {
if (key.repeat) {
params[prop] = value ? value.split(key.delimiter).map(decodeParam) : [];
} else {
params[prop] = value ? decodeParam(value) : value;
}
}
}
return {
path: !end && url.charAt(url.length - 1) === '/' ? url.substr(1) : url,
params,
};
}
function matchRoute(route, baseUrl, pathname, parentParams) {
let matched;
let childMatches;
let childIndex = 0;
return {
next() {
if (!matched) {
matched = matchPath(route, pathname, parentParams);
if (matched) {
return {
done: false,
$: {
route,
baseUrl,
path: matched.path,
params: matched.params,
},
};
}
}
if (matched && route.routes) {
while (childIndex < route.routes.length) {
if (!childMatches) {
const childRoute = route.routes[childIndex];
childRoute.parent = route;
childMatches = matchRoute(
childRoute,
baseUrl + matched.path,
pathname.substr(matched.path.length),
matched.params,
);
}
const childMatch = childMatches.next();
if (!childMatch.done) {
return {
done: false,
$: childMatch.$,
};
}
childMatches = null;
childIndex++;
}
}
return { done: true };
},
};
}
let _initialized = false;
let _routerConfig = null;
const router = {
history: null,
handles: [],
errorHandler() { },
addHandle(handle) {
return router.handles.push(handle);
},
removeHandle(handleId) {
router.handles[handleId - 1] = null;
},
triggerHandles(component) {
router.handles.map((handle) => {
handle && handle(component);
});
},
match(fullpath) {
if (fullpath == null) return;
router.fullpath = fullpath;
const parent = router.root;
const matched = matchRoute(
parent,
parent.path,
fullpath
);
function next(parent) {
const current = matched.next();
if (current.done) {
const error = new Error(`No match for ${fullpath}`);
return router.errorHandler(error, router.history.location);
}
let component = current.$.route.component;
if (typeof component === 'function') {
component = component(current.$.params, router.history.location);
}
if (component instanceof Promise) {
// Lazy loading component by import('./Foo')
return component.then((component) => {
// Check current fullpath avoid router has changed before lazy loading complete
if (fullpath === router.fullpath) {
router.triggerHandles(component);
}
});
} else if (component != null) {
router.triggerHandles(component);
return component;
} else {
return next(parent);
}
}
return next(parent);
}
};
function matchLocation({ pathname }) {
router.match(pathname);
}
function getInitialComponent(routerConfig) {
let InitialComponent = [];
if (_routerConfig === null) {
if (process.env.NODE_ENV !== 'production') {
if (!routerConfig) {
throw new Error('Error: useRouter should have routerConfig, see: https://www.npmjs.com/package/rax-use-router.');
}
if (!routerConfig.history || !routerConfig.routes) {
throw new Error('Error: routerConfig should contain history and routes, see: https://www.npmjs.com/package/rax-use-router.');
}
}
_routerConfig = routerConfig;
}
if (_routerConfig.InitialComponent) {
InitialComponent = _routerConfig.InitialComponent;
}
router.history = _routerConfig.history;
return InitialComponent;
}
export function useRouter(routerConfig) {
const [component, setComponent] = useState(getInitialComponent(routerConfig));
if (routerConfig) {
_routerConfig = routerConfig;
const routes = _routerConfig.routes;
router.root = Array.isArray(routes) ? { routes } : routes;
}
useLayoutEffect(() => {
if (_initialized) throw new Error('Error: useRouter can only be called once.');
_initialized = true;
const history = _routerConfig.history;
const routes = _routerConfig.routes;
router.root = Array.isArray(routes) ? { routes } : routes;
const handleId = router.addHandle((component) => {
setComponent(component);
});
// Init path match
if (!_routerConfig.InitialComponent) {
matchLocation(history.location);
}
const unlisten = history.listen(({ location }) => {
matchLocation(location);
});
return () => {
router.removeHandle(handleId);
unlisten();
};
}, []);
return { component };
}
export function withRouter(Component) {
function Wrapper(props) {
const history = router.history;
return createElement(Component, { ...props, history, location: history.location });
};
Wrapper.displayName = 'withRouter(' + (Component.displayName || Component.name) + ')';
Wrapper.WrappedComponent = Component;
return Wrapper;
}

View File

@ -1,12 +1,13 @@
import { Fragment, Component, createElement } from 'rax';
// import { observer } from './obx-rax/observer';
import RaxEngine from '@ali/lowcode-rax-renderer/lib/index';
import { History } from 'history';
import { Component, createElement, Fragment, useState } from 'rax';
import { useRouter } from './rax-use-router';
// import RaxEngine from '../../rax-render/lib/index';
import { SimulatorRendererContainer, DocumentInstance } from './renderer';
import { host } from './host';
import { DocumentInstance, SimulatorRendererContainer } from './renderer';
import './renderer.less';
// patch cloneElement avoid lost keyProps
const originCloneElement = (window as any).Rax.cloneElement;
(window as any).Rax.cloneElement = (child: any, { _leaf, ...props }: any = {}, ...rest: any[]) => {
@ -40,27 +41,38 @@ const originCloneElement = (window as any).Rax.cloneElement;
export default class SimulatorRendererView extends Component<{ rendererContainer: SimulatorRendererContainer }> {
render() {
const { rendererContainer } = this.props;
rendererContainer.onDocumentChange(() => {
this.forceUpdate();
});
return (
<Layout rendererContainer={rendererContainer}>
<Routes rendererContainer={rendererContainer} />
<Routes rendererContainer={rendererContainer} history={rendererContainer.history} />
</Layout>
);
}
}
export class Routes extends Component<{ rendererContainer: SimulatorRendererContainer }> {
render() {
const { rendererContainer } = this.props;
return (
<Fragment>
{rendererContainer.documentInstances.map((instance, index) => {
console.log('Routes');
return <Renderer key={index} rendererContainer={rendererContainer} documentInstance={instance} />;
})}
</Fragment>
);
}
export const Routes = (props: {
rendererContainer: SimulatorRendererContainer,
history: History
}) => {
const { rendererContainer, history } = props;
const { documentInstances } = rendererContainer;
const routes = {
history,
routes: documentInstances.map(instance => {
return {
path: instance.path,
component: (props: any) => <Renderer rendererContainer={rendererContainer} documentInstance={instance} {...props} />
};
})
};
const { component } = useRouter(routes);
return component;
}
function ucfirst(s: string) {
return s.charAt(0).toUpperCase() + s.substring(1);
}
@ -83,7 +95,7 @@ function getDeviceView(view: any, device: string, mode: string) {
class Layout extends Component<{ rendererContainer: SimulatorRendererContainer }> {
shouldComponentUpdate() {
return false;
return true;
}
render() {
const { rendererContainer, children } = this.props;
@ -114,14 +126,13 @@ class Renderer extends Component<{
});
}
shouldComponentUpdate() {
return false;
return true;
}
render() {
const { documentInstance } = this.props;
const { container } = documentInstance;
const { designMode, device } = container;
const { rendererContainer: renderer } = this.props;
// const { device, designMode } = renderer;
return (
<RaxEngine

View File

@ -5,6 +5,8 @@ import { computed, obx } from '@recore/obx';
import DriverUniversal from 'driver-universal';
import { EventEmitter } from 'events';
// @ts-ignore
import { createMemoryHistory, MemoryHistory } from 'history';
// @ts-ignore
import { ComponentType, createElement, render as raxRender, shared } from 'rax';
import Leaf from './builtin-components/leaf';
import Slot from './builtin-components/slot';
@ -13,6 +15,7 @@ import SimulatorRendererView from './renderer-view';
import { raxFindDOMNodes } from './utils/find-dom-nodes';
import { getClientRects } from './utils/get-client-rects';
import loader from './utils/loader';
import { parseQuery, withQueryParams } from './utils/url';
const { Instance } = shared;
@ -223,8 +226,9 @@ export class DocumentInstance {
export class SimulatorRendererContainer implements BuiltinSimulatorRenderer {
readonly isSimulatorRenderer = true;
private dispose?: () => void;
// TODO: history
readonly history: any;
readonly history: MemoryHistory;
private emitter = new EventEmitter();
@obx.ref private _documentInstances: DocumentInstance[] = [];
get documentInstances() {
@ -258,47 +262,48 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer {
}
return inst;
});
this.emitter.emit('documentChange');
const path = host.project.currentDocument ? documentInstanceMap.get(host.project.currentDocument.id)!.path : '/';
if (firstRun) {
initialEntry = path;
} else {
// if (this.history.location.pathname !== path) {
// this.history.replace(path);
// }
if (this.history.location.pathname !== path) {
this.history.replace(path);
}
}
});
// const history = createMemoryHistory({
// initialEntries: [initialEntry],
// });
// this.history = history;
// history.listen((location, action) => {
// host.project.open(location.pathname.substr(1));
// });
const history = createMemoryHistory({
initialEntries: [initialEntry],
});
this.history = history;
history.listen(({ location }) => {
host.project.open(location.pathname.substr(1));
});
host.componentsConsumer.consume(async (componentsAsset) => {
if (componentsAsset) {
await this.load(componentsAsset);
this.buildComponents();
}
});
// this._appContext = {
// utils: {
// router: {
// push(path: string, params?: object) {
// history.push(withQueryParams(path, params));
// },
// replace(path: string, params?: object) {
// history.replace(withQueryParams(path, params));
// },
// },
// legaoBuiltins: {
// getUrlParams() {
// const search = history.location.search;
// return parseQuery(search);
// }
// }
// },
// constants: {},
// };
this._appContext = {
utils: {
router: {
push(path: string, params?: object) {
history.push(withQueryParams(path, params));
},
replace(path: string, params?: object) {
history.replace(withQueryParams(path, params));
},
},
legaoBuiltins: {
getUrlParams() {
const search = history.location.search;
return parseQuery(search);
}
}
},
constants: {},
};
host.injectionConsumer.consume((data) => {
// sync utils, i18n, contants,... config
});
@ -420,6 +425,13 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer {
cursor.release();
}
onDocumentChange(cb: () => void) {
this.emitter.on('documentChange', cb);
return () => {
this.emitter.removeListener('renderer', fn);
};
}
createComponent(schema: ComponentSchema): Component | null {
return null;
// TODO: use ComponentEngine refactor

View File

@ -0,0 +1,74 @@
/**
* Parse queryString
* @param {String} str '?q=query&b=test'
* @return {Object}
*/
export function parseQuery(str: string): object {
const ret: any = {};
if (typeof str !== 'string') {
return ret;
}
const s = str.trim().replace(/^(\?|#|&)/, '');
if (!s) {
return ret;
}
s.split('&').forEach((param) => {
const parts = param.replace(/\+/g, ' ').split('=');
let key = parts.shift()!;
let val: any = parts.length > 0 ? parts.join('=') : undefined;
key = decodeURIComponent(key);
val = val === undefined ? null : decodeURIComponent(val);
if (ret[key] === undefined) {
ret[key] = val;
} else if (Array.isArray(ret[key])) {
ret[key].push(val);
} else {
ret[key] = [ret[key], val];
}
});
return ret;
}
/**
* Stringify object to query parammeters
* @param {Object} obj
* @return {String}
*/
export function stringifyQuery(obj: any): string {
const param: string[] = [];
Object.keys(obj).forEach((key) => {
let value = obj[key];
if (value && typeof value === 'object') {
value = JSON.stringify(value);
}
param.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
});
return param.join('&');
}
export function uriEncode(uri: string) {
return encodeURIComponent(uri);
}
export function uriDecode(uri: string) {
return decodeURIComponent(uri);
}
export function withQueryParams(url: string, params?: object) {
const queryStr = params ? stringifyQuery(params) : '';
if (queryStr === '') {
return url;
}
const urlSplit = url.split('#');
const hash = urlSplit[1] ? `#${urlSplit[1]}` : '';
const urlWithoutHash = urlSplit[0];
return `${urlWithoutHash}${~urlWithoutHash.indexOf('?') ? '&' : '?'}${queryStr}${hash}`;
}