mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-01-13 17:48:13 +00:00
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:
commit
4e6f0a3fbb
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
259
packages/rax-simulator-renderer/src/rax-use-router.js
Normal file
259
packages/rax-simulator-renderer/src/rax-use-router.js
Normal 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;
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
74
packages/rax-simulator-renderer/src/utils/url.ts
Normal file
74
packages/rax-simulator-renderer/src/utils/url.ts
Normal 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}`;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user