mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-02-07 05:55:44 +00:00
feat: add router
This commit is contained in:
parent
fb5de6441d
commit
03f7c76284
@ -1513,7 +1513,6 @@ webpack.config.js # 项目工程配置,包含插件配置及自定义 webpack
|
|||||||
| -------------- | ---------------------------------- | ------ | ------ | ------ | ---------------------------------------------- |
|
| -------------- | ---------------------------------- | ------ | ------ | ------ | ---------------------------------------------- |
|
||||||
| path | 当前解析后的路径 | String | - | - | 必填 |
|
| path | 当前解析后的路径 | String | - | - | 必填 |
|
||||||
| hash | 当前路径的 hash 值,以 # 开头 | String | - | - | 必填 |
|
| hash | 当前路径的 hash 值,以 # 开头 | String | - | - | 必填 |
|
||||||
| href | 当前的全部路径 | String | - | - | 必填 |
|
|
||||||
| params | 匹配到的路径参数 | Object | - | - | 必填 |
|
| params | 匹配到的路径参数 | Object | - | - | 必填 |
|
||||||
| query | 当前的路径 query 对象 | Object | - | - | 必填,代表当前地址的 search 属性的对象 |
|
| query | 当前的路径 query 对象 | Object | - | - | 必填,代表当前地址的 search 属性的对象 |
|
||||||
| name | 匹配到的路由记录名 | String | - | - | 选填 |
|
| name | 匹配到的路由记录名 | String | - | - | 选填 |
|
||||||
|
|||||||
@ -6,8 +6,10 @@
|
|||||||
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
||||||
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "",
|
"build": "tsc",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export interface AppBase {
|
|||||||
*/
|
*/
|
||||||
export interface AppContext {
|
export interface AppContext {
|
||||||
schema: AppSchema;
|
schema: AppSchema;
|
||||||
config: Map<string, any>;
|
config: PlainObject;
|
||||||
appScope: CodeScope;
|
appScope: CodeScope;
|
||||||
packageManager: PackageManager;
|
packageManager: PackageManager;
|
||||||
boosts: AppBoostsManager;
|
boosts: AppBoostsManager;
|
||||||
@ -35,14 +35,14 @@ type AppCreator<O, T extends AppBase> = (
|
|||||||
|
|
||||||
export type App<T extends AppBase = AppBase> = {
|
export type App<T extends AppBase = AppBase> = {
|
||||||
schema: Project;
|
schema: Project;
|
||||||
config: Map<string, any>;
|
config: PlainObject;
|
||||||
readonly boosts: AppBoosts;
|
readonly boosts: AppBoosts;
|
||||||
|
|
||||||
use(plugin: Plugin): Promise<void>;
|
use(plugin: Plugin): Promise<void>;
|
||||||
} & T;
|
} & T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建应用
|
* 创建 createApp 的辅助函数
|
||||||
* @param schema
|
* @param schema
|
||||||
* @param options
|
* @param options
|
||||||
* @returns
|
* @returns
|
||||||
@ -55,9 +55,9 @@ export function createAppFunction<O extends AppOptionsBase, T extends AppBase =
|
|||||||
}
|
}
|
||||||
|
|
||||||
return async (options) => {
|
return async (options) => {
|
||||||
const { schema, appScopeValue = {} } = options;
|
const { schema, appScopeValue } = options;
|
||||||
const appSchema = createAppSchema(schema);
|
const appSchema = createAppSchema(schema);
|
||||||
const appConfig = new Map<string, any>();
|
const appConfig = {};
|
||||||
const packageManager = createPackageManager();
|
const packageManager = createPackageManager();
|
||||||
const appScope = createScope({
|
const appScope = createScope({
|
||||||
...appScopeValue,
|
...appScopeValue,
|
||||||
|
|||||||
@ -1,136 +1,42 @@
|
|||||||
import { CreateContainerOptions, createContainer } from '../container';
|
import { type CreateContainerOptions, createContainer, type Container } from '../container';
|
||||||
import { createCodeRuntime, createScope } from '../code-runtime';
|
import type { PlainObject, InstanceStateApi } from '../types';
|
||||||
import { throwRuntimeError } from '../utils/error';
|
|
||||||
import { validateContainerSchema } from '../validator/schema';
|
|
||||||
|
|
||||||
export interface ComponentOptionsBase<C> {
|
export type CreateComponentBaseOptions<T extends string> = Omit<
|
||||||
componentsTree: RootSchema;
|
CreateContainerOptions<T>,
|
||||||
componentsRecord: Record<string, C | Package>;
|
'stateCreator'
|
||||||
dataSourceCreator: DataSourceCreator;
|
>;
|
||||||
}
|
|
||||||
|
|
||||||
export function createComponentFunction<C, O extends ComponentOptionsBase<C>>(options: {
|
|
||||||
stateCreator: (initState: AnyObject) => StateContext;
|
|
||||||
componentCreator: (container: Container, componentOptions: O) => C;
|
|
||||||
defaultOptions?: Partial<O>;
|
|
||||||
}): (componentOptions: O) => C {
|
|
||||||
const { stateCreator, componentCreator, defaultOptions = {} } = options;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 createComponent 的辅助函数
|
||||||
|
* createComponent = createComponentFunction(() => component)
|
||||||
|
*/
|
||||||
|
export function createComponentFunction<
|
||||||
|
ComponentT,
|
||||||
|
InstanceT,
|
||||||
|
LifeCycleNameT extends string,
|
||||||
|
O extends CreateComponentBaseOptions<LifeCycleNameT>,
|
||||||
|
>(
|
||||||
|
stateCreator: (initState: PlainObject) => InstanceStateApi,
|
||||||
|
componentCreator: (
|
||||||
|
container: Container<InstanceT, LifeCycleNameT>,
|
||||||
|
componentOptions: O,
|
||||||
|
) => ComponentT,
|
||||||
|
): (componentOptions: O) => ComponentT {
|
||||||
return (componentOptions) => {
|
return (componentOptions) => {
|
||||||
const finalOptions = Object.assign({}, defaultOptions, componentOptions);
|
const {
|
||||||
const { supCodeScope, initScopeValue = {}, dataSourceCreator } = finalOptions;
|
supCodeScope,
|
||||||
|
initScopeValue = {},
|
||||||
|
dataSourceCreator,
|
||||||
|
componentsTree,
|
||||||
|
} = componentOptions;
|
||||||
|
|
||||||
const codeRuntimeScope =
|
const container = createContainer<InstanceT, LifeCycleNameT>({
|
||||||
supCodeScope?.createSubScope(initScopeValue) ?? createScope(initScopeValue);
|
supCodeScope,
|
||||||
const codeRuntime = createCodeRuntime(codeRuntimeScope);
|
initScopeValue,
|
||||||
|
stateCreator,
|
||||||
const container: Container = {
|
dataSourceCreator,
|
||||||
get codeScope() {
|
componentsTree,
|
||||||
return codeRuntimeScope;
|
});
|
||||||
},
|
|
||||||
get codeRuntime() {
|
|
||||||
return codeRuntime;
|
|
||||||
},
|
|
||||||
|
|
||||||
createInstance(componentsTree, extraProps = {}) {
|
|
||||||
if (!validateContainerSchema(componentsTree)) {
|
|
||||||
throwRuntimeError('createComponent', 'componentsTree is not valid!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapRefToComponentInstance: Map<string, C> = new Map();
|
|
||||||
|
|
||||||
const initialState = codeRuntime.parseExprOrFn(componentsTree.state ?? {});
|
|
||||||
const stateContext = stateCreator(initialState);
|
|
||||||
|
|
||||||
codeRuntimeScope.setValue(
|
|
||||||
Object.assign(
|
|
||||||
{
|
|
||||||
props: codeRuntime.parseExprOrFn({
|
|
||||||
...componentsTree.defaultProps,
|
|
||||||
...componentsTree.props,
|
|
||||||
...extraProps,
|
|
||||||
}),
|
|
||||||
$(ref: string) {
|
|
||||||
return mapRefToComponentInstance.get(ref);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stateContext,
|
|
||||||
dataSourceCreator
|
|
||||||
? dataSourceCreator(componentsTree.dataSource ?? ({ list: [] } as any), stateContext)
|
|
||||||
: {},
|
|
||||||
) as ContainerInstanceScope<C>,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (componentsTree.methods) {
|
|
||||||
for (const [key, fn] of Object.entries(componentsTree.methods)) {
|
|
||||||
const customMethod = codeRuntime.createFnBoundScope(fn.value);
|
|
||||||
if (customMethod) {
|
|
||||||
codeRuntimeScope.inject(key, customMethod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerLifeCycle('constructor');
|
|
||||||
|
|
||||||
function triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]) {
|
|
||||||
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
|
|
||||||
if (
|
|
||||||
!componentsTree.lifeCycles ||
|
|
||||||
!Object.keys(componentsTree.lifeCycles).includes(lifeCycleName)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName];
|
|
||||||
if (isJsFunction(lifeCycleSchema)) {
|
|
||||||
const lifeCycleFn = codeRuntime.createFnBoundScope(lifeCycleSchema.value);
|
|
||||||
if (lifeCycleFn) {
|
|
||||||
lifeCycleFn.apply(codeRuntime.getScope().value, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance: ContainerInstance<C> = {
|
|
||||||
get id() {
|
|
||||||
return componentsTree.id;
|
|
||||||
},
|
|
||||||
get cssText() {
|
|
||||||
return componentsTree.css;
|
|
||||||
},
|
|
||||||
get codeScope() {
|
|
||||||
return codeRuntimeScope;
|
|
||||||
},
|
|
||||||
|
|
||||||
triggerLifeCycle,
|
|
||||||
setRefInstance(ref, instance) {
|
|
||||||
mapRefToComponentInstance.set(ref, instance);
|
|
||||||
},
|
|
||||||
removeRefInstance(ref) {
|
|
||||||
mapRefToComponentInstance.delete(ref);
|
|
||||||
},
|
|
||||||
getComponentTreeNodes() {
|
|
||||||
const childNodes = componentsTree.children
|
|
||||||
? Array.isArray(componentsTree.children)
|
|
||||||
? componentsTree.children
|
|
||||||
: [componentsTree.children]
|
|
||||||
: [];
|
|
||||||
const treeNodes = childNodes.map((item) => {
|
|
||||||
return createComponentTreeNode(item, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
return treeNodes;
|
|
||||||
},
|
|
||||||
|
|
||||||
destory() {
|
|
||||||
mapRefToComponentInstance.clear();
|
|
||||||
codeRuntimeScope.setValue({});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return componentCreator(container, componentOptions);
|
return componentCreator(container, componentOptions);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -76,17 +76,20 @@ export function createCodeRuntime(scopeOrValue: PlainObject = {}): CodeRuntime {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CodeScope {
|
export interface CodeScope<T extends PlainObject = PlainObject, K extends keyof T = keyof T> {
|
||||||
readonly value: PlainObject;
|
readonly value: T;
|
||||||
|
|
||||||
inject(name: string, value: any, force?: boolean): void;
|
inject(name: K, value: T[K], force?: boolean): void;
|
||||||
setValue(value: PlainObject, replace?: boolean): void;
|
setValue(value: T, replace?: boolean): void;
|
||||||
createSubScope(initValue?: PlainObject): CodeScope;
|
createSubScope<O extends PlainObject = PlainObject>(initValue: O): CodeScope<T & O>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createScope(initValue: PlainObject = {}): CodeScope {
|
export function createScope<T extends PlainObject = PlainObject, K extends keyof T = keyof T>(
|
||||||
|
initValue: T,
|
||||||
|
): CodeScope<T, K> {
|
||||||
const innerScope = { value: initValue };
|
const innerScope = { value: initValue };
|
||||||
const proxyValue = new Proxy(Object.create(null), {
|
|
||||||
|
const proxyValue: T = new Proxy(Object.create(null), {
|
||||||
set(target, p, newValue, receiver) {
|
set(target, p, newValue, receiver) {
|
||||||
return Reflect.set(target, p, newValue, receiver);
|
return Reflect.set(target, p, newValue, receiver);
|
||||||
},
|
},
|
||||||
@ -104,29 +107,17 @@ export function createScope(initValue: PlainObject = {}): CodeScope {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function inject(name: string, value: any, force = false): void {
|
const scope: CodeScope<T, K> = {
|
||||||
if (innerScope.value[name] && !force) {
|
|
||||||
console.warn(`${name} 已存在值`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
innerScope.value[name] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSubScope(initValue: PlainObject = {}) {
|
|
||||||
const childScope = createScope(initValue);
|
|
||||||
|
|
||||||
(childScope as any).__raw.__parent = innerScope;
|
|
||||||
|
|
||||||
return childScope;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope: CodeScope = {
|
|
||||||
get value() {
|
get value() {
|
||||||
// dev return value
|
// dev return value
|
||||||
return proxyValue;
|
return proxyValue;
|
||||||
},
|
},
|
||||||
inject,
|
inject(name, value, force = false): void {
|
||||||
|
if (innerScope.value[name] && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
innerScope.value[name] = value;
|
||||||
|
},
|
||||||
setValue(value, replace = false) {
|
setValue(value, replace = false) {
|
||||||
if (replace) {
|
if (replace) {
|
||||||
innerScope.value = { ...value };
|
innerScope.value = { ...value };
|
||||||
@ -134,7 +125,13 @@ export function createScope(initValue: PlainObject = {}): CodeScope {
|
|||||||
innerScope.value = Object.assign({}, innerScope.value, value);
|
innerScope.value = Object.assign({}, innerScope.value, value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createSubScope,
|
createSubScope<O extends PlainObject = PlainObject>(initValue: O) {
|
||||||
|
const childScope = createScope<O & T>(initValue);
|
||||||
|
|
||||||
|
(childScope as any).__raw.__parent = innerScope;
|
||||||
|
|
||||||
|
return childScope;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.defineProperty(scope, Symbol.for(SYMBOL_SIGN), { get: () => true });
|
Object.defineProperty(scope, Symbol.for(SYMBOL_SIGN), { get: () => true });
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type {
|
|||||||
ComponentTree,
|
ComponentTree,
|
||||||
InstanceDataSourceApi,
|
InstanceDataSourceApi,
|
||||||
InstanceStateApi,
|
InstanceStateApi,
|
||||||
|
NodeType,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { type CodeScope, type CodeRuntime, createCodeRuntime, createScope } from './code-runtime';
|
import { type CodeScope, type CodeRuntime, createCodeRuntime, createScope } from './code-runtime';
|
||||||
import { isJSFunction } from './utils/type-guard';
|
import { isJSFunction } from './utils/type-guard';
|
||||||
@ -48,7 +49,13 @@ export interface CreateContainerOptions<LifeCycleNameT extends string> {
|
|||||||
export function createContainer<InstanceT, LifeCycleNameT extends string>(
|
export function createContainer<InstanceT, LifeCycleNameT extends string>(
|
||||||
options: CreateContainerOptions<LifeCycleNameT>,
|
options: CreateContainerOptions<LifeCycleNameT>,
|
||||||
): Container<InstanceT, LifeCycleNameT> {
|
): Container<InstanceT, LifeCycleNameT> {
|
||||||
const { componentsTree, supCodeScope, initScopeValue, stateCreator, dataSourceCreator } = options;
|
const {
|
||||||
|
componentsTree,
|
||||||
|
supCodeScope,
|
||||||
|
initScopeValue = {},
|
||||||
|
stateCreator,
|
||||||
|
dataSourceCreator,
|
||||||
|
} = options;
|
||||||
|
|
||||||
validContainerSchema(componentsTree);
|
validContainerSchema(componentsTree);
|
||||||
|
|
||||||
@ -151,7 +158,6 @@ export function createContainer<InstanceT, LifeCycleNameT extends string>(
|
|||||||
|
|
||||||
createWidgets<Element>() {
|
createWidgets<Element>() {
|
||||||
if (!componentsTree.children) return [];
|
if (!componentsTree.children) return [];
|
||||||
|
|
||||||
return componentsTree.children.map((item) => createWidget<Element>(item));
|
return componentsTree.children.map((item) => createWidget<Element>(item));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,11 +5,14 @@ export { createCodeRuntime, createScope } from './code-runtime';
|
|||||||
export { definePlugin } from './plugin';
|
export { definePlugin } from './plugin';
|
||||||
export { createWidget } from './widget';
|
export { createWidget } from './widget';
|
||||||
export { createContainer } from './container';
|
export { createContainer } from './container';
|
||||||
|
export { createHookStore, useEvent } from './utils/hook';
|
||||||
|
export * from './utils/type-guard';
|
||||||
|
export * from './utils/value';
|
||||||
|
export * from './widget';
|
||||||
|
|
||||||
/* --------------- types ---------------- */
|
/* --------------- types ---------------- */
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type { CodeRuntime, CodeScope } from './code-runtime';
|
export type { CodeRuntime, CodeScope } from './code-runtime';
|
||||||
export type { Plugin, PluginSetupContext } from './plugin';
|
export type { Plugin, PluginSetupContext } from './plugin';
|
||||||
export type { PackageManager, PackageLoader } from './package';
|
export type { PackageManager, PackageLoader } from './package';
|
||||||
export type { Container } from './container';
|
export type { Container, CreateContainerOptions } from './container';
|
||||||
export type { Widget, TextWidget, ComponentWidget } from './widget';
|
|
||||||
|
|||||||
@ -24,9 +24,11 @@ export interface PackageManager {
|
|||||||
/** 解析组件映射 */
|
/** 解析组件映射 */
|
||||||
resolveComponentMaps(componentMaps: ComponentMap[]): void;
|
resolveComponentMaps(componentMaps: ComponentMap[]): void;
|
||||||
/** 获取组件映射对象,key = componentName value = component */
|
/** 获取组件映射对象,key = componentName value = component */
|
||||||
getComponentsNameRecord<C = unknown>(componentMaps?: ComponentMap[]): Record<string, C>;
|
getComponentsNameRecord<C = unknown>(
|
||||||
|
componentMaps?: ComponentMap[],
|
||||||
|
): Record<string, C | LowCodeComponent>;
|
||||||
/** 通过组件名获取对应的组件 */
|
/** 通过组件名获取对应的组件 */
|
||||||
getComponent<C = unknown>(componentName: string): C | undefined;
|
getComponent<C = unknown>(componentName: string): C | LowCodeComponent | undefined;
|
||||||
/** 注册组件 */
|
/** 注册组件 */
|
||||||
registerComponentByName(componentName: string, Component: unknown): void;
|
registerComponentByName(componentName: string, Component: unknown): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,9 @@ export interface AppSchema {
|
|||||||
addComponentsMap(componentName: ComponentMap): void;
|
addComponentsMap(componentName: ComponentMap): void;
|
||||||
removeComponentsMap(componentName: string): void;
|
removeComponentsMap(componentName: string): void;
|
||||||
|
|
||||||
getPages(): PageConfig[];
|
getPageConfigs(): PageConfig[];
|
||||||
addPage(page: PageConfig): void;
|
addPageConfig(page: PageConfig): void;
|
||||||
removePage(id: string): void;
|
removePageConfig(id: string): void;
|
||||||
|
|
||||||
getByKey<K extends keyof Project>(key: K): Project[K] | undefined;
|
getByKey<K extends keyof Project>(key: K): Project[K] | undefined;
|
||||||
updateByKey<K extends keyof Project>(
|
updateByKey<K extends keyof Project>(
|
||||||
@ -55,14 +55,14 @@ export function createAppSchema(schema: Project): AppSchema {
|
|||||||
removeArrayItem(schemaRef.componentsMap, 'componentName', componentName);
|
removeArrayItem(schemaRef.componentsMap, 'componentName', componentName);
|
||||||
},
|
},
|
||||||
|
|
||||||
getPages() {
|
getPageConfigs() {
|
||||||
return schemaRef.pages ?? [];
|
return schemaRef.pages ?? [];
|
||||||
},
|
},
|
||||||
addPage(page) {
|
addPageConfig(page) {
|
||||||
schemaRef.pages ??= [];
|
schemaRef.pages ??= [];
|
||||||
addArrayItem(schemaRef.pages, page, 'id');
|
addArrayItem(schemaRef.pages, page, 'id');
|
||||||
},
|
},
|
||||||
removePage(id) {
|
removePageConfig(id) {
|
||||||
schemaRef.pages ??= [];
|
schemaRef.pages ??= [];
|
||||||
removeArrayItem(schemaRef.pages, 'id', id);
|
removeArrayItem(schemaRef.pages, 'id', id);
|
||||||
},
|
},
|
||||||
@ -72,8 +72,7 @@ export function createAppSchema(schema: Project): AppSchema {
|
|||||||
},
|
},
|
||||||
updateByKey(key, updater) {
|
updateByKey(key, updater) {
|
||||||
const value = schemaRef[key];
|
const value = schemaRef[key];
|
||||||
|
schemaRef[key] = typeof updater === 'function' ? (updater as any)(value) : updater;
|
||||||
schemaRef[key] = typeof updater === 'function' ? updater(value) : updater;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
find(predicate) {
|
find(predicate) {
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { Package } from './specs/asset-spec';
|
import { Package } from './specs/asset-spec';
|
||||||
import { Project, ComponentMap } from './specs/lowcode-spec';
|
import { ComponentTree } from './specs/lowcode-spec';
|
||||||
|
|
||||||
export interface ProCodeComponent extends Package {
|
export interface ProCodeComponent extends Package {
|
||||||
package: string;
|
package: string;
|
||||||
type: 'proCode';
|
type: 'proCode';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LowCodeComponent extends Package {
|
export interface LowCodeComponent extends Omit<Package, 'schema'> {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'lowCode';
|
type: 'lowCode';
|
||||||
componentName: string;
|
componentName: string;
|
||||||
schema: Project;
|
schema: ComponentTree;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -189,20 +189,28 @@ export interface ComponentTreeNodeProps {
|
|||||||
/** 组件内联样式 */
|
/** 组件内联样式 */
|
||||||
style?: JSONObject | JSExpression;
|
style?: JSONObject | JSExpression;
|
||||||
/** 组件 ref 名称 */
|
/** 组件 ref 名称 */
|
||||||
ref?: string | JSExpression;
|
ref?: string;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NPMUtil {
|
||||||
|
name: string;
|
||||||
|
type: 'npm';
|
||||||
|
content: Omit<ComponentMap, 'componentName'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionUtil {
|
||||||
|
name: string;
|
||||||
|
type: 'function';
|
||||||
|
content: JSFunction;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#24-%E5%B7%A5%E5%85%B7%E7%B1%BB%E6%89%A9%E5%B1%95%E6%8F%8F%E8%BF%B0aa
|
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#24-%E5%B7%A5%E5%85%B7%E7%B1%BB%E6%89%A9%E5%B1%95%E6%8F%8F%E8%BF%B0aa
|
||||||
* 用于描述物料开发过程中,自定义扩展或引入的第三方工具类(例如:lodash 及 moment),增强搭建基础协议的扩展性,提供通用的工具类方法的配置方案及调用 API。
|
* 用于描述物料开发过程中,自定义扩展或引入的第三方工具类(例如:lodash 及 moment),增强搭建基础协议的扩展性,提供通用的工具类方法的配置方案及调用 API。
|
||||||
*/
|
*/
|
||||||
export interface Util {
|
export type Util = NPMUtil | FunctionUtil;
|
||||||
name: string;
|
|
||||||
type: 'npm' | 'function';
|
|
||||||
content: ComponentMap | JSFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#25-%E5%9B%BD%E9%99%85%E5%8C%96%E5%A4%9A%E8%AF%AD%E8%A8%80%E6%94%AF%E6%8C%81aa
|
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#25-%E5%9B%BD%E9%99%85%E5%8C%96%E5%A4%9A%E8%AF%AD%E8%A8%80%E6%94%AF%E6%8C%81aa
|
||||||
@ -307,7 +315,7 @@ export interface JSONObject {
|
|||||||
*/
|
*/
|
||||||
export interface JSSlot {
|
export interface JSSlot {
|
||||||
type: 'JSSlot';
|
type: 'JSSlot';
|
||||||
value: 1;
|
value: ComponentTreeNode | ComponentTreeNode[];
|
||||||
params?: string[];
|
params?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,10 +50,16 @@ export interface InstanceDataSourceApi {
|
|||||||
reloadDataSource: () => void;
|
reloadDataSource: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用级别的公共函数或第三方扩展
|
||||||
|
*/
|
||||||
export interface UtilsApi {
|
export interface UtilsApi {
|
||||||
utils: Record<string, AnyFunction>;
|
utils: Record<string, AnyFunction>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 国际化相关 API
|
||||||
|
*/
|
||||||
export interface IntlApi {
|
export interface IntlApi {
|
||||||
/**
|
/**
|
||||||
* 返回语料字符串
|
* 返回语料字符串
|
||||||
@ -72,4 +78,106 @@ export interface IntlApi {
|
|||||||
setLocale(locale: string): void;
|
setLocale(locale: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouterApi {}
|
/**
|
||||||
|
* 路由 Router API:封装了原生的 History、Location 等 api,提供统一的调用方法
|
||||||
|
* 得益于 HTML 5 新的 History api 的规范出现,SPA 大行其道,其中 SPA 的路由起到了非常重大的作用
|
||||||
|
*/
|
||||||
|
export interface RouterApi {
|
||||||
|
/**
|
||||||
|
* 获取当前解析后的路由信息
|
||||||
|
*/
|
||||||
|
getCurrentLocation(): RouteLocation;
|
||||||
|
/**
|
||||||
|
* 路由跳转方法,跳转到指定的路径或者 `Route`
|
||||||
|
*/
|
||||||
|
push(location: RawRouteLocation): void | Promise<void>;
|
||||||
|
/**
|
||||||
|
* 路由跳转方法,与 `push` 的区别在于不会增加一条历史记录而是替换当前的历史记录
|
||||||
|
*/
|
||||||
|
replace(location: RawRouteLocation): void | Promise<void>;
|
||||||
|
/**
|
||||||
|
* 返回上一页,同 `history.back`
|
||||||
|
*/
|
||||||
|
back(): void;
|
||||||
|
/**
|
||||||
|
* 跳转下一页,同 `history.forward`
|
||||||
|
*/
|
||||||
|
forward(): void;
|
||||||
|
/**
|
||||||
|
* 跳转到当前页面的相对位置,同 `history.go`
|
||||||
|
* @param delta 相对于当前页面你要去往历史页面的位置
|
||||||
|
*/
|
||||||
|
go(delta: number): void;
|
||||||
|
/**
|
||||||
|
* 路由跳转前的守卫方法
|
||||||
|
*/
|
||||||
|
beforeRouteLeave(
|
||||||
|
guard: (to: RouteLocation, from: RouteLocation) => boolean | Promise<boolean>,
|
||||||
|
): () => void;
|
||||||
|
/**
|
||||||
|
* 路由跳转后的钩子函数
|
||||||
|
*/
|
||||||
|
afterRouteChange(guard: (to: RouteLocation, from: RouteLocation) => any): () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawLocationAsPath {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
export interface RawLocationAsRelative {
|
||||||
|
params?: Record<string, string | string[]>;
|
||||||
|
}
|
||||||
|
export interface RawLocationAsName {
|
||||||
|
name: string;
|
||||||
|
params?: Record<string, string | string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RawLocation = RawLocationAsPath | RawLocationAsRelative | RawLocationAsName;
|
||||||
|
|
||||||
|
export interface RawLocationOptions {
|
||||||
|
searchParams?: URLSearchParams;
|
||||||
|
hash?: string;
|
||||||
|
state?: History['state'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许用户输入的路径参数类型
|
||||||
|
*/
|
||||||
|
export type RawRouteLocation = string | (RawLocation & RawLocationOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由的当前信息,模拟 window.location
|
||||||
|
*/
|
||||||
|
export interface RouteLocation {
|
||||||
|
/**
|
||||||
|
* 匹配到的路由记录名
|
||||||
|
*/
|
||||||
|
name: string | undefined;
|
||||||
|
/**
|
||||||
|
* 当前解析后的路径
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* 当前路径的 hash 值,以 # 开头
|
||||||
|
*/
|
||||||
|
hash: string;
|
||||||
|
/**
|
||||||
|
* 匹配到的路径参数
|
||||||
|
*/
|
||||||
|
params: Record<string, string | string[]> | undefined;
|
||||||
|
/**
|
||||||
|
* 当前的路径 URLSearchParams 对象
|
||||||
|
*/
|
||||||
|
searchParams: URLSearchParams | undefined;
|
||||||
|
/**
|
||||||
|
* 包括 search 和 hash 在内的完整地址
|
||||||
|
*/
|
||||||
|
fullPath: string;
|
||||||
|
/**
|
||||||
|
* 匹配到的路由记录元数据
|
||||||
|
*/
|
||||||
|
meta: PlainObject | undefined;
|
||||||
|
/**
|
||||||
|
* 重定向之前的路由,在跳转到当前路径之前的路由记录
|
||||||
|
*/
|
||||||
|
redirectedFrom: RouteLocation | undefined;
|
||||||
|
}
|
||||||
|
|||||||
8
runtime/renderer-core/src/utils/guid.ts
Normal file
8
runtime/renderer-core/src/utils/guid.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
let idStart = 0x0907;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate unique id
|
||||||
|
*/
|
||||||
|
export function guid(): number {
|
||||||
|
return idStart++;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { JSExpression, JSFunction, I18nNode } from '../types';
|
import type { JSExpression, JSFunction, JSSlot, I18nNode, LowCodeComponent } from '../types';
|
||||||
import { isPlainObject } from 'lodash-es';
|
import { isPlainObject } from 'lodash-es';
|
||||||
|
|
||||||
export function isJSExpression(v: unknown): v is JSExpression {
|
export function isJSExpression(v: unknown): v is JSExpression {
|
||||||
@ -13,6 +13,14 @@ export function isJSFunction(v: unknown): v is JSFunction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isJSSlot(v: unknown): v is JSSlot {
|
||||||
|
return isPlainObject(v) && (v as any).type === 'JSSlot' && (v as any).value;
|
||||||
|
}
|
||||||
|
|
||||||
export function isI18nNode(v: unknown): v is I18nNode {
|
export function isI18nNode(v: unknown): v is I18nNode {
|
||||||
return isPlainObject(v) && (v as any).type === 'i18n' && typeof (v as any).key === 'string';
|
return isPlainObject(v) && (v as any).type === 'i18n' && typeof (v as any).key === 'string';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLowCodeComponentSchema(v: unknown): v is LowCodeComponent {
|
||||||
|
return isPlainObject(v) && (v as any).type === 'lowCode' && (v as any).schema;
|
||||||
|
}
|
||||||
|
|||||||
@ -14,14 +14,14 @@ export function someValue(obj: any, predicate: (data: any) => boolean) {
|
|||||||
export function processValue(
|
export function processValue(
|
||||||
obj: any,
|
obj: any,
|
||||||
predicate: (obj: any) => boolean,
|
predicate: (obj: any) => boolean,
|
||||||
processor: (node: any, paths: Array<string | number>) => any
|
processor: (node: any, paths: Array<string | number>) => any,
|
||||||
): any {
|
): any {
|
||||||
const innerProcess = (target: any, paths: Array<string | number>): any => {
|
const innerProcess = (target: any, paths: Array<string | number>): any => {
|
||||||
if (Array.isArray(target)) {
|
if (Array.isArray(target)) {
|
||||||
return target.map((item, idx) => innerProcess(item, [...paths, idx]));
|
return target.map((item, idx) => innerProcess(item, [...paths, idx]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPlainObject(target) || isEmptyObject(target)) return target;
|
if (!isPlainObject(target) || isEmpty(target)) return target;
|
||||||
if (!someValue(target, predicate)) return target;
|
if (!someValue(target, predicate)) return target;
|
||||||
|
|
||||||
if (predicate(target)) {
|
if (predicate(target)) {
|
||||||
|
|||||||
@ -1,35 +1,32 @@
|
|||||||
import type { NodeType, ComponentTreeNode, ComponentTreeNodeProps } from './types';
|
import type { NodeType, ComponentTreeNode, ComponentTreeNodeProps } from './types';
|
||||||
import { isJSExpression, isI18nNode } from './utils/type-guard';
|
import { isJSExpression, isI18nNode } from './utils/type-guard';
|
||||||
|
import { guid } from './utils/guid';
|
||||||
|
|
||||||
export class Widget<Data, Element> {
|
export class Widget<Data, Element> {
|
||||||
protected _raw: Data;
|
|
||||||
protected proxyElements: Element[] = [];
|
protected proxyElements: Element[] = [];
|
||||||
protected renderObject: Element | undefined;
|
protected renderObject: Element | undefined;
|
||||||
|
|
||||||
constructor(data: Data) {
|
constructor(public raw: Data) {
|
||||||
this._raw = data;
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected init() {}
|
protected init() {}
|
||||||
|
|
||||||
get raw() {
|
get key(): string {
|
||||||
return this._raw;
|
return (this.raw as any)?.id ?? `${guid()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRenderObject(el: Element) {
|
mapRenderObject(mapper: (widget: Widget<Data, Element>) => Element | undefined) {
|
||||||
this.renderObject = el;
|
this.renderObject = mapper(this);
|
||||||
}
|
return this;
|
||||||
getRenderObject() {
|
|
||||||
return this.renderObject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addProxyELements(el: Element) {
|
addProxyELements(el: Element) {
|
||||||
this.proxyElements.push(el);
|
this.proxyElements.unshift(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
build(builder: (elements: Element[]) => Element) {
|
build<C>(builder: (elements: Element[]) => C): C {
|
||||||
return builder(this.proxyElements);
|
return builder(this.renderObject ? [...this.proxyElements, this.renderObject] : []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +50,11 @@ export class ComponentWidget<E = unknown> extends Widget<ComponentTreeNode, E> {
|
|||||||
private _propsValue: ComponentTreeNodeProps = {};
|
private _propsValue: ComponentTreeNodeProps = {};
|
||||||
|
|
||||||
protected init() {
|
protected init() {
|
||||||
if (this._raw.props) {
|
if (this.raw.props) {
|
||||||
this._propsValue = this._raw.props;
|
this._propsValue = this.raw.props;
|
||||||
}
|
}
|
||||||
if (this._raw.children) {
|
if (this.raw.children) {
|
||||||
this._children = this._raw.children.map((child) => createWidget<E>(child));
|
this._children = this.raw.children.map((child) => createWidget<E>(child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,20 +62,20 @@ export class ComponentWidget<E = unknown> extends Widget<ComponentTreeNode, E> {
|
|||||||
return this.raw.componentName;
|
return this.raw.componentName;
|
||||||
}
|
}
|
||||||
get props() {
|
get props() {
|
||||||
return this._propsValue;
|
return this._propsValue ?? {};
|
||||||
|
}
|
||||||
|
get condition() {
|
||||||
|
return this.raw.condition !== false;
|
||||||
|
}
|
||||||
|
get loop() {
|
||||||
|
return this.raw.loop;
|
||||||
|
}
|
||||||
|
get loopArgs() {
|
||||||
|
return this.raw.loopArgs ?? ['item', 'index'];
|
||||||
}
|
}
|
||||||
get children() {
|
get children() {
|
||||||
return this._children;
|
return this._children;
|
||||||
}
|
}
|
||||||
get condition() {
|
|
||||||
return this._raw.condition ?? true;
|
|
||||||
}
|
|
||||||
get loop() {
|
|
||||||
return this._raw.loop;
|
|
||||||
}
|
|
||||||
get loopArgs() {
|
|
||||||
return this._raw.loopArgs ?? ['item', 'index'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createWidget<E = unknown>(data: NodeType) {
|
export function createWidget<E = unknown>(data: NodeType) {
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,10 @@
|
|||||||
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
||||||
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "",
|
"build": "tsc",
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -20,8 +22,11 @@
|
|||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@alilc/runtime-router": "1.0.0-beta.0",
|
||||||
"@testing-library/react": "^14.2.0",
|
"@testing-library/react": "^14.2.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.5",
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"@types/react": "^18.2.67",
|
"@types/react": "^18.2.67",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"jsdom": "^24.0.0"
|
"jsdom": "^24.0.0"
|
||||||
|
|||||||
@ -1,34 +1,47 @@
|
|||||||
import {
|
import {
|
||||||
type App,
|
type App,
|
||||||
type RenderBase,
|
type AppBase,
|
||||||
createAppFunction,
|
createAppFunction,
|
||||||
type AppOptionsBase,
|
type AppOptionsBase,
|
||||||
} from '@alilc/renderer-core';
|
} from '@alilc/renderer-core';
|
||||||
import { type ComponentType } from 'react';
|
import { type ComponentType } from 'react';
|
||||||
import { type Root, createRoot } from 'react-dom/client';
|
import { type Root, createRoot } from 'react-dom/client';
|
||||||
|
import { createRouter } from '@alilc/runtime-router';
|
||||||
import { createRenderer } from '../renderer';
|
import { createRenderer } from '../renderer';
|
||||||
import AppComponent from '../components/app';
|
import AppComponent from '../components/app';
|
||||||
import { intlPlugin } from '../plugins/intl';
|
import { createIntl } from '../runtime-api/intl';
|
||||||
import { globalUtilsPlugin } from '../plugins/utils';
|
import { createRuntimeUtils } from '../runtime-api/utils';
|
||||||
import { initRouter } from '../router';
|
|
||||||
|
|
||||||
export interface AppOptions extends AppOptionsBase {
|
export interface AppOptions extends AppOptionsBase {
|
||||||
dataSourceCreator: DataSourceCreator;
|
dataSourceCreator: any;
|
||||||
faultComponent?: ComponentType<any>;
|
faultComponent?: ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReactRender extends RenderBase {}
|
export interface ReactRender extends AppBase {}
|
||||||
|
|
||||||
export type ReactApp = App<ReactRender>;
|
export type ReactApp = App<ReactRender>;
|
||||||
|
|
||||||
export const createApp = createAppFunction<AppOptions, ReactRender>(async (context, options) => {
|
export const createApp = createAppFunction<AppOptions, ReactRender>(async (context, options) => {
|
||||||
const renderer = createRenderer();
|
const { schema, packageManager, appScope, boosts } = context;
|
||||||
const appContext = { ...context, renderer };
|
|
||||||
|
|
||||||
initRouter(appContext);
|
// router
|
||||||
|
// todo: transform config
|
||||||
|
const router = createRouter(schema.getByKey('router') as any);
|
||||||
|
|
||||||
options.plugins ??= [];
|
appScope.inject('router', router);
|
||||||
options.plugins!.unshift(globalUtilsPlugin, intlPlugin);
|
|
||||||
|
// i18n
|
||||||
|
const i18nMessages = schema.getByKey('i18n') ?? {};
|
||||||
|
const defaultLocale = schema.getByPath('config.defaultLocale') ?? 'zh-CN';
|
||||||
|
const intl = createIntl(i18nMessages, defaultLocale);
|
||||||
|
|
||||||
|
appScope.inject('intl', intl);
|
||||||
|
|
||||||
|
// utils
|
||||||
|
const runtimeUtils = createRuntimeUtils(schema.getByKey('utils') ?? [], packageManager);
|
||||||
|
|
||||||
|
appScope.inject('utils', runtimeUtils.utils);
|
||||||
|
boosts.add('runtimeUtils', runtimeUtils);
|
||||||
|
|
||||||
// set config
|
// set config
|
||||||
if (options.faultComponent) {
|
if (options.faultComponent) {
|
||||||
@ -37,6 +50,8 @@ export const createApp = createAppFunction<AppOptions, ReactRender>(async (conte
|
|||||||
context.config.set('dataSourceCreator', options.dataSourceCreator);
|
context.config.set('dataSourceCreator', options.dataSourceCreator);
|
||||||
|
|
||||||
let root: Root | undefined;
|
let root: Root | undefined;
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const appContext = { ...context, renderer };
|
||||||
|
|
||||||
const reactRender: ReactRender = {
|
const reactRender: ReactRender = {
|
||||||
async mount(el) {
|
async mount(el) {
|
||||||
@ -56,7 +71,7 @@ export const createApp = createAppFunction<AppOptions, ReactRender>(async (conte
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderBase: reactRender,
|
appBase: reactRender,
|
||||||
renderer,
|
renderer,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
5
runtime/renderer-react/src/api/component.tsx
Normal file
5
runtime/renderer-react/src/api/component.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createComponent as internalCreate, ComponentOptions } from '../component';
|
||||||
|
|
||||||
|
export function createComponent(options: ComponentOptions) {
|
||||||
|
return internalCreate(options);
|
||||||
|
}
|
||||||
@ -1,471 +0,0 @@
|
|||||||
import {
|
|
||||||
type StateContext,
|
|
||||||
type ComponentOptionsBase,
|
|
||||||
createComponentFunction,
|
|
||||||
type ComponentTreeNode,
|
|
||||||
type ComponentNode,
|
|
||||||
someValue,
|
|
||||||
type ContainerInstance,
|
|
||||||
processValue,
|
|
||||||
createNode,
|
|
||||||
type CodeRuntime,
|
|
||||||
createCodeRuntime,
|
|
||||||
} from '@alilc/runtime-core';
|
|
||||||
import { isPlainObject } from 'lodash-es';
|
|
||||||
import {
|
|
||||||
type AnyObject,
|
|
||||||
type Package,
|
|
||||||
type JSSlot,
|
|
||||||
type JSFunction,
|
|
||||||
isJsExpression,
|
|
||||||
isJsSlot,
|
|
||||||
isLowCodeComponentPackage,
|
|
||||||
isJsFunction,
|
|
||||||
type JSExpression,
|
|
||||||
} from '@alilc/runtime-shared';
|
|
||||||
import {
|
|
||||||
useEffect,
|
|
||||||
type ComponentType,
|
|
||||||
type ReactNode,
|
|
||||||
type ElementType,
|
|
||||||
forwardRef,
|
|
||||||
ForwardedRef,
|
|
||||||
useMemo,
|
|
||||||
createElement,
|
|
||||||
type CSSProperties,
|
|
||||||
useRef,
|
|
||||||
} from 'react';
|
|
||||||
import { createSignal, watch } from '../signals';
|
|
||||||
import { appendExternalStyle } from '../helper/element';
|
|
||||||
import { reactive } from '../helper/reactive';
|
|
||||||
|
|
||||||
function reactiveStateCreator(initState: AnyObject): StateContext {
|
|
||||||
const proxyState = createSignal(initState);
|
|
||||||
|
|
||||||
return {
|
|
||||||
get state() {
|
|
||||||
return proxyState.value;
|
|
||||||
},
|
|
||||||
setState(newState) {
|
|
||||||
if (!isPlainObject(newState)) {
|
|
||||||
throw Error('newState mush be a object');
|
|
||||||
}
|
|
||||||
for (const key of Object.keys(newState)) {
|
|
||||||
proxyState.value[key] = newState[key];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 作为组件树节点在转换为 reactNode 的过程中的中间介质对象
|
|
||||||
* 提供对外拓展、修改的能力
|
|
||||||
*/
|
|
||||||
export interface ConvertedTreeNode {
|
|
||||||
type: ComponentTreeNode['type'];
|
|
||||||
/** 节点对应的值 */
|
|
||||||
raw: ComponentTreeNode;
|
|
||||||
/** 转换时所在的上下文 */
|
|
||||||
context: {
|
|
||||||
codeRuntime: CodeRuntime;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
/** 用于渲染的组件,只存在于树节点为组件节点的情况 */
|
|
||||||
rawComponent?: ComponentType<any> | undefined;
|
|
||||||
|
|
||||||
/** 获取节点对应的 reactNode,被用于渲染 */
|
|
||||||
getReactNode(): ReactNode;
|
|
||||||
/** 设置节点对应的 reactNode */
|
|
||||||
setReactNode(element: ReactNode): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateComponentOptions<C = ComponentType<any>> extends ComponentOptionsBase<C> {
|
|
||||||
displayName?: string;
|
|
||||||
|
|
||||||
beforeNodeCreateComponent?(convertedNode: ConvertedTreeNode): void;
|
|
||||||
nodeCreatedComponent?(convertedNode: ConvertedTreeNode): void;
|
|
||||||
nodeComponentRefAttached?(node: ComponentNode, instance: ElementType): void;
|
|
||||||
componentDidMount?(): void;
|
|
||||||
componentWillUnmount?(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LowCodeComponentProps {
|
|
||||||
id?: string;
|
|
||||||
/** CSS 类名 */
|
|
||||||
className?: string;
|
|
||||||
/** style */
|
|
||||||
style?: CSSProperties;
|
|
||||||
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createComponent = createComponentFunction<
|
|
||||||
ComponentType<LowCodeComponentProps>,
|
|
||||||
CreateComponentOptions
|
|
||||||
>({
|
|
||||||
stateCreator: reactiveStateCreator,
|
|
||||||
componentCreator: ({ codeRuntime, createInstance }, componentOptions) => {
|
|
||||||
const {
|
|
||||||
displayName = '__LowCodeComponent__',
|
|
||||||
componentsTree,
|
|
||||||
componentsRecord,
|
|
||||||
|
|
||||||
beforeNodeCreateComponent,
|
|
||||||
nodeCreatedComponent,
|
|
||||||
nodeComponentRefAttached,
|
|
||||||
componentDidMount,
|
|
||||||
componentWillUnmount,
|
|
||||||
|
|
||||||
...extraOptions
|
|
||||||
} = componentOptions;
|
|
||||||
|
|
||||||
const lowCodeComponentCache = new Map<string, ComponentType<any>>();
|
|
||||||
|
|
||||||
function getComponentByName(
|
|
||||||
componentName: string,
|
|
||||||
componentsRecord: Record<string, ComponentType<any> | Package>,
|
|
||||||
) {
|
|
||||||
const Component = componentsRecord[componentName];
|
|
||||||
if (!Component) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLowCodeComponentPackage(Component)) {
|
|
||||||
if (lowCodeComponentCache.has(componentName)) {
|
|
||||||
return lowCodeComponentCache.get(componentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentsTree = Component.schema as any;
|
|
||||||
const LowCodeComponent = createComponent({
|
|
||||||
...extraOptions,
|
|
||||||
displayName: componentsTree.componentName,
|
|
||||||
componentsRecord,
|
|
||||||
componentsTree,
|
|
||||||
});
|
|
||||||
|
|
||||||
lowCodeComponentCache.set(componentName, LowCodeComponent);
|
|
||||||
|
|
||||||
return LowCodeComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Component;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createConvertedTreeNode(
|
|
||||||
rawNode: ComponentTreeNode,
|
|
||||||
codeRuntime: CodeRuntime,
|
|
||||||
): ConvertedTreeNode {
|
|
||||||
let elementValue: ReactNode = null;
|
|
||||||
|
|
||||||
const node: ConvertedTreeNode = {
|
|
||||||
type: rawNode.type,
|
|
||||||
raw: rawNode,
|
|
||||||
context: { codeRuntime },
|
|
||||||
|
|
||||||
getReactNode() {
|
|
||||||
return elementValue;
|
|
||||||
},
|
|
||||||
setReactNode(element) {
|
|
||||||
elementValue = element;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (rawNode.type === 'component') {
|
|
||||||
node.rawComponent = getComponentByName(rawNode.data.componentName, componentsRecord);
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createReactElement(
|
|
||||||
node: ComponentTreeNode,
|
|
||||||
codeRuntime: CodeRuntime,
|
|
||||||
instance: ContainerInstance,
|
|
||||||
componentsRecord: Record<string, ComponentType<any> | Package>,
|
|
||||||
) {
|
|
||||||
const convertedNode = createConvertedTreeNode(node, codeRuntime);
|
|
||||||
|
|
||||||
beforeNodeCreateComponent?.(convertedNode);
|
|
||||||
|
|
||||||
if (!convertedNode.getReactNode()) {
|
|
||||||
if (convertedNode.type === 'string') {
|
|
||||||
convertedNode.setReactNode(convertedNode.raw.data as string);
|
|
||||||
} else if (convertedNode.type === 'expression') {
|
|
||||||
const rawValue = convertedNode.raw.data as JSExpression;
|
|
||||||
|
|
||||||
function Text(props: any) {
|
|
||||||
return props.text;
|
|
||||||
}
|
|
||||||
Text.displayName = 'Text';
|
|
||||||
|
|
||||||
const ReactivedText = reactive(Text, {
|
|
||||||
target: {
|
|
||||||
text: rawValue,
|
|
||||||
},
|
|
||||||
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
|
|
||||||
});
|
|
||||||
|
|
||||||
convertedNode.setReactNode(<ReactivedText key={rawValue.value} />);
|
|
||||||
} else if (convertedNode.type === 'component') {
|
|
||||||
const createReactElementByNode = () => {
|
|
||||||
const Component = convertedNode.rawComponent;
|
|
||||||
if (!Component) return null;
|
|
||||||
|
|
||||||
const rawNode = convertedNode.raw as ComponentNode;
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
componentName,
|
|
||||||
condition = true,
|
|
||||||
loop,
|
|
||||||
loopArgs = ['item', 'index'],
|
|
||||||
props: nodeProps = {},
|
|
||||||
} = rawNode.data;
|
|
||||||
|
|
||||||
// condition为 Falsy 的情况下 不渲染
|
|
||||||
if (!condition) return null;
|
|
||||||
// loop 为数组且为空的情况下 不渲染
|
|
||||||
if (Array.isArray(loop) && loop.length === 0) return null;
|
|
||||||
|
|
||||||
function createElementWithProps(
|
|
||||||
Component: ComponentType<any>,
|
|
||||||
props: AnyObject,
|
|
||||||
codeRuntime: CodeRuntime,
|
|
||||||
key: string,
|
|
||||||
children: ReactNode[] = [],
|
|
||||||
) {
|
|
||||||
const { ref, ...componentProps } = props;
|
|
||||||
|
|
||||||
const refFunction = (ins: any) => {
|
|
||||||
if (ins) {
|
|
||||||
if (ref) instance.setRefInstance(ref as string, ins);
|
|
||||||
nodeComponentRefAttached?.(rawNode, ins);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 先将 jsslot, jsFunction 对象转换
|
|
||||||
const finalProps = processValue(
|
|
||||||
componentProps,
|
|
||||||
(node) => isJsSlot(node) || isJsFunction(node),
|
|
||||||
(node: JSSlot | JSFunction) => {
|
|
||||||
if (isJsSlot(node)) {
|
|
||||||
if (node.value) {
|
|
||||||
const nodes = (Array.isArray(node.value) ? node.value : [node.value]).map(
|
|
||||||
(n) => createNode(n, undefined),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (node.params?.length) {
|
|
||||||
return (...args: any[]) => {
|
|
||||||
const params = node.params!.reduce((prev, cur, idx) => {
|
|
||||||
return (prev[cur] = args[idx]);
|
|
||||||
}, {} as AnyObject);
|
|
||||||
const subCodeScope = codeRuntime.getScope().createSubScope(params);
|
|
||||||
const subCodeRuntime = createCodeRuntime(subCodeScope);
|
|
||||||
|
|
||||||
return nodes.map((n) =>
|
|
||||||
createReactElement(n, subCodeRuntime, instance, componentsRecord),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return nodes.map((n) =>
|
|
||||||
createReactElement(n, codeRuntime, instance, componentsRecord),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isJsFunction(node)) {
|
|
||||||
return codeRuntime.parseExprOrFn(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (someValue(finalProps, isJsExpression)) {
|
|
||||||
function Props(props: any) {
|
|
||||||
return createElement(
|
|
||||||
Component,
|
|
||||||
{
|
|
||||||
...props,
|
|
||||||
key,
|
|
||||||
ref: refFunction,
|
|
||||||
},
|
|
||||||
children,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Props.displayName = 'Props';
|
|
||||||
|
|
||||||
const Reactived = reactive(Props, {
|
|
||||||
target: finalProps,
|
|
||||||
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
|
|
||||||
});
|
|
||||||
|
|
||||||
return <Reactived key={key} />;
|
|
||||||
} else {
|
|
||||||
return createElement(
|
|
||||||
Component,
|
|
||||||
{
|
|
||||||
...finalProps,
|
|
||||||
key,
|
|
||||||
ref: refFunction,
|
|
||||||
},
|
|
||||||
children,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentComponentKey = id || componentName;
|
|
||||||
|
|
||||||
let element: any = createElementWithProps(
|
|
||||||
Component,
|
|
||||||
nodeProps,
|
|
||||||
codeRuntime,
|
|
||||||
currentComponentKey,
|
|
||||||
rawNode.children?.map((n) =>
|
|
||||||
createReactElement(n, codeRuntime, instance, componentsRecord),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loop) {
|
|
||||||
const genLoopElements = (loopData: any[]) => {
|
|
||||||
return loopData.map((item, idx) => {
|
|
||||||
const loopArgsItem = loopArgs[0] ?? 'item';
|
|
||||||
const loopArgsIndex = loopArgs[1] ?? 'index';
|
|
||||||
const subCodeScope = codeRuntime.getScope().createSubScope({
|
|
||||||
[loopArgsItem]: item,
|
|
||||||
[loopArgsIndex]: idx,
|
|
||||||
});
|
|
||||||
const subCodeRuntime = createCodeRuntime(subCodeScope);
|
|
||||||
|
|
||||||
return createElementWithProps(
|
|
||||||
Component,
|
|
||||||
nodeProps,
|
|
||||||
subCodeRuntime,
|
|
||||||
`loop-${currentComponentKey}-${idx}`,
|
|
||||||
rawNode.children?.map((n) =>
|
|
||||||
createReactElement(n, subCodeRuntime, instance, componentsRecord),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isJsExpression(loop)) {
|
|
||||||
function Loop(props: any) {
|
|
||||||
if (!Array.isArray(props.loop)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return genLoopElements(props.loop);
|
|
||||||
}
|
|
||||||
Loop.displayName = 'Loop';
|
|
||||||
|
|
||||||
const ReactivedLoop = reactive(Loop, {
|
|
||||||
target: {
|
|
||||||
loop,
|
|
||||||
},
|
|
||||||
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
|
|
||||||
});
|
|
||||||
|
|
||||||
element = createElement(ReactivedLoop, {
|
|
||||||
key: currentComponentKey,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
element = genLoopElements(loop as any[]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isJsExpression(condition)) {
|
|
||||||
function Condition(props: any) {
|
|
||||||
if (props.condition) {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Condition.displayName = 'Condition';
|
|
||||||
|
|
||||||
const ReactivedCondition = reactive(Condition, {
|
|
||||||
target: {
|
|
||||||
condition,
|
|
||||||
},
|
|
||||||
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
|
|
||||||
});
|
|
||||||
|
|
||||||
return createElement(ReactivedCondition, {
|
|
||||||
key: currentComponentKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
|
||||||
};
|
|
||||||
|
|
||||||
convertedNode.setReactNode(createReactElementByNode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeCreatedComponent?.(convertedNode);
|
|
||||||
|
|
||||||
const finalElement = convertedNode.getReactNode();
|
|
||||||
// if finalElement is null, todo..
|
|
||||||
return finalElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LowCodeComponent = forwardRef(function (
|
|
||||||
props: LowCodeComponentProps,
|
|
||||||
ref: ForwardedRef<any>,
|
|
||||||
) {
|
|
||||||
const { id, className, style, ...extraProps } = props;
|
|
||||||
const isMounted = useRef(false);
|
|
||||||
|
|
||||||
const instance = useMemo(() => {
|
|
||||||
return createInstance(componentsTree, extraProps);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let styleEl: HTMLElement | undefined;
|
|
||||||
const scopeValue = instance.codeScope.value;
|
|
||||||
|
|
||||||
// init dataSource
|
|
||||||
scopeValue.reloadDataSource();
|
|
||||||
|
|
||||||
if (instance.cssText) {
|
|
||||||
appendExternalStyle(instance.cssText).then((el) => {
|
|
||||||
styleEl = el;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger lifeCycles
|
|
||||||
componentDidMount?.();
|
|
||||||
instance.triggerLifeCycle('componentDidMount');
|
|
||||||
|
|
||||||
// 当 state 改变之后调用
|
|
||||||
const unwatch = watch(scopeValue.state, (_, oldVal) => {
|
|
||||||
if (isMounted.current) {
|
|
||||||
instance.triggerLifeCycle('componentDidUpdate', props, oldVal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
isMounted.current = true;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
styleEl?.parentNode?.removeChild(styleEl);
|
|
||||||
|
|
||||||
componentWillUnmount?.();
|
|
||||||
instance.triggerLifeCycle('componentWillUnmount');
|
|
||||||
unwatch();
|
|
||||||
|
|
||||||
isMounted.current = false;
|
|
||||||
};
|
|
||||||
}, [instance]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id={id} className={className} style={style} ref={ref}>
|
|
||||||
{instance
|
|
||||||
.getComponentTreeNodes()
|
|
||||||
.map((n) => createReactElement(n, codeRuntime, instance, componentsRecord))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
LowCodeComponent.displayName = displayName;
|
|
||||||
|
|
||||||
return LowCodeComponent;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
418
runtime/renderer-react/src/component.tsx
Normal file
418
runtime/renderer-react/src/component.tsx
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
import {
|
||||||
|
createComponentFunction,
|
||||||
|
isLowCodeComponentSchema,
|
||||||
|
createCodeRuntime,
|
||||||
|
TextWidget,
|
||||||
|
ComponentWidget,
|
||||||
|
isJSExpression,
|
||||||
|
processValue,
|
||||||
|
isJSFunction,
|
||||||
|
isJSSlot,
|
||||||
|
someValue,
|
||||||
|
} from '@alilc/renderer-core';
|
||||||
|
import { isPlainObject } from 'lodash-es';
|
||||||
|
import { forwardRef, useRef, useEffect, createElement, useMemo } from 'react';
|
||||||
|
import { createSignal, watch } from './signals';
|
||||||
|
import { appendExternalStyle } from './utils/element';
|
||||||
|
import { reactive } from './utils/reactive';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CreateComponentBaseOptions,
|
||||||
|
PlainObject,
|
||||||
|
InstanceStateApi,
|
||||||
|
LowCodeComponent as LowCodeComponentSchema,
|
||||||
|
CodeRuntime,
|
||||||
|
IntlApi,
|
||||||
|
JSSlot,
|
||||||
|
JSFunction,
|
||||||
|
I18nNode,
|
||||||
|
} from '@alilc/renderer-core';
|
||||||
|
import type {
|
||||||
|
ComponentType,
|
||||||
|
ReactInstance,
|
||||||
|
CSSProperties,
|
||||||
|
ForwardedRef,
|
||||||
|
ReactNode,
|
||||||
|
ReactElement,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export type ReactComponentLifeCycle =
|
||||||
|
| 'constructor'
|
||||||
|
| 'render'
|
||||||
|
| 'componentDidMount'
|
||||||
|
| 'componentDidUpdate'
|
||||||
|
| 'componentWillUnmount'
|
||||||
|
| 'componentDidCatch';
|
||||||
|
|
||||||
|
export interface ComponentOptions<C = ComponentType<any>>
|
||||||
|
extends CreateComponentBaseOptions<ReactComponentLifeCycle> {
|
||||||
|
componentsRecord: Record<string, C | LowCodeComponentSchema>;
|
||||||
|
intl: IntlApi;
|
||||||
|
displayName?: string;
|
||||||
|
|
||||||
|
beforeElementCreate?(widget: TextWidget<C> | ComponentWidget<C>): void;
|
||||||
|
componentRefAttached?(widget: ComponentWidget<C>, instance: ReactInstance): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LowCodeComponentProps {
|
||||||
|
id?: string;
|
||||||
|
/** CSS 类名 */
|
||||||
|
className?: string;
|
||||||
|
/** style */
|
||||||
|
style?: CSSProperties;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createComponent = createComponentFunction<
|
||||||
|
ComponentType<any>,
|
||||||
|
ReactInstance,
|
||||||
|
ReactComponentLifeCycle,
|
||||||
|
ComponentOptions
|
||||||
|
>(reactiveStateCreator, (container, options) => {
|
||||||
|
const {
|
||||||
|
componentsRecord,
|
||||||
|
intl,
|
||||||
|
displayName = '__LowCodeComponent__',
|
||||||
|
beforeElementCreate,
|
||||||
|
componentRefAttached,
|
||||||
|
|
||||||
|
...extraOptions
|
||||||
|
} = options;
|
||||||
|
const lowCodeComponentCache = new Map<string, ComponentType<any>>();
|
||||||
|
|
||||||
|
function getComponentByName(componentName: string) {
|
||||||
|
const Component = componentsRecord[componentName];
|
||||||
|
if (!Component) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLowCodeComponentSchema(Component)) {
|
||||||
|
if (lowCodeComponentCache.has(componentName)) {
|
||||||
|
return lowCodeComponentCache.get(componentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LowCodeComponent = createComponent({
|
||||||
|
...extraOptions,
|
||||||
|
intl,
|
||||||
|
displayName: Component.componentName,
|
||||||
|
componentsRecord,
|
||||||
|
componentsTree: Component.schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
lowCodeComponentCache.set(componentName, LowCodeComponent);
|
||||||
|
|
||||||
|
return LowCodeComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Component;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createReactElement(
|
||||||
|
widget: TextWidget<ComponentType<any>> | ComponentWidget<ComponentType<any>>,
|
||||||
|
codeRuntime: CodeRuntime,
|
||||||
|
) {
|
||||||
|
beforeElementCreate?.(widget);
|
||||||
|
|
||||||
|
return widget.build((elements) => {
|
||||||
|
if (elements.length > 0) {
|
||||||
|
const RenderObject = elements[elements.length - 1];
|
||||||
|
const Wrappers = elements.slice(0, elements.length - 1);
|
||||||
|
|
||||||
|
const buildRenderElement = () => {
|
||||||
|
if (widget instanceof TextWidget) {
|
||||||
|
if (widget.type === 'string') {
|
||||||
|
return createElement(RenderObject, { key: widget.key, text: widget.raw });
|
||||||
|
} else {
|
||||||
|
return createElement(
|
||||||
|
reactive(RenderObject, {
|
||||||
|
target:
|
||||||
|
widget.type === 'expression' ? { text: widget.raw } : (widget.raw as I18nNode),
|
||||||
|
valueGetter(expr) {
|
||||||
|
return codeRuntime.parseExprOrFn(expr);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ key: widget.key },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (widget instanceof ComponentWidget) {
|
||||||
|
const { condition, loop, loopArgs } = widget;
|
||||||
|
|
||||||
|
// condition为 Falsy 的情况下 不渲染
|
||||||
|
if (!condition) return null;
|
||||||
|
// loop 为数组且为空的情况下 不渲染
|
||||||
|
if (Array.isArray(loop) && loop.length === 0) return null;
|
||||||
|
|
||||||
|
function createElementWithProps(
|
||||||
|
Component: ComponentType<any>,
|
||||||
|
widget: ComponentWidget<ComponentType<any>>,
|
||||||
|
codeRuntime: CodeRuntime,
|
||||||
|
key?: string,
|
||||||
|
): ReactElement {
|
||||||
|
const { ref, ...componentProps } = widget.props;
|
||||||
|
const componentKey = key ?? widget.key;
|
||||||
|
|
||||||
|
const attachRef = (ins: ReactInstance) => {
|
||||||
|
if (ins) {
|
||||||
|
if (ref) container.setInstance(ref as string, ins);
|
||||||
|
componentRefAttached?.(widget, ins);
|
||||||
|
} else {
|
||||||
|
if (ref) container.removeInstance(ref);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 先将 jsslot, jsFunction 对象转换
|
||||||
|
const finalProps = processValue(
|
||||||
|
componentProps,
|
||||||
|
(node) => isJSFunction(node) || isJSSlot(node),
|
||||||
|
(node: JSSlot | JSFunction) => {
|
||||||
|
if (isJSSlot(node)) {
|
||||||
|
if (node.value) {
|
||||||
|
const widgets = (Array.isArray(node.value) ? node.value : [node.value]).map(
|
||||||
|
(v) => new ComponentWidget<ComponentType<any>>(v),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (node.params?.length) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
const params = node.params!.reduce((prev, cur, idx) => {
|
||||||
|
return (prev[cur] = args[idx]);
|
||||||
|
}, {} as PlainObject);
|
||||||
|
const subCodeScope = codeRuntime.getScope().createSubScope(params);
|
||||||
|
const subCodeRuntime = createCodeRuntime(subCodeScope);
|
||||||
|
|
||||||
|
return widgets.map((n) => createReactElement(n, subCodeRuntime));
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return widgets.map((n) => createReactElement(n, codeRuntime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isJSFunction(node)) {
|
||||||
|
return codeRuntime.parseExprOrFn(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const childElements = widget.children.map((child) =>
|
||||||
|
createReactElement(child, codeRuntime),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (someValue(finalProps, isJSExpression)) {
|
||||||
|
const PropsWrapper = (props: PlainObject) =>
|
||||||
|
createElement(
|
||||||
|
Component,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
key: componentKey,
|
||||||
|
ref: attachRef,
|
||||||
|
},
|
||||||
|
childElements,
|
||||||
|
);
|
||||||
|
|
||||||
|
PropsWrapper.displayName = 'PropsWrapper';
|
||||||
|
|
||||||
|
return createElement(
|
||||||
|
reactive(PropsWrapper, {
|
||||||
|
target: finalProps,
|
||||||
|
valueGetter: (node) => codeRuntime.parseExprOrFn(node),
|
||||||
|
}),
|
||||||
|
{ key: componentKey },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return createElement(
|
||||||
|
Component,
|
||||||
|
{
|
||||||
|
...finalProps,
|
||||||
|
key: componentKey,
|
||||||
|
ref: attachRef,
|
||||||
|
},
|
||||||
|
childElements,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let element: ReactElement | ReactElement[] = createElementWithProps(
|
||||||
|
RenderObject,
|
||||||
|
widget,
|
||||||
|
codeRuntime,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loop) {
|
||||||
|
const genLoopElements = (loopData: any[]) => {
|
||||||
|
return loopData.map((item, idx) => {
|
||||||
|
const loopArgsItem = loopArgs[0] ?? 'item';
|
||||||
|
const loopArgsIndex = loopArgs[1] ?? 'index';
|
||||||
|
const subCodeScope = codeRuntime.getScope().createSubScope({
|
||||||
|
[loopArgsItem]: item,
|
||||||
|
[loopArgsIndex]: idx,
|
||||||
|
});
|
||||||
|
const subCodeRuntime = createCodeRuntime(subCodeScope);
|
||||||
|
|
||||||
|
return createElementWithProps(
|
||||||
|
RenderObject,
|
||||||
|
widget,
|
||||||
|
subCodeRuntime,
|
||||||
|
`loop-${widget.key}-${idx}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isJSExpression(loop)) {
|
||||||
|
function Loop(props: { loop: boolean }) {
|
||||||
|
if (!Array.isArray(props.loop)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <>{genLoopElements(props.loop)}</>;
|
||||||
|
}
|
||||||
|
Loop.displayName = 'Loop';
|
||||||
|
|
||||||
|
const ReactivedLoop = reactive(Loop, {
|
||||||
|
target: {
|
||||||
|
loop,
|
||||||
|
},
|
||||||
|
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
|
||||||
|
});
|
||||||
|
|
||||||
|
element = createElement(ReactivedLoop, {
|
||||||
|
key: widget.key,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element = genLoopElements(loop as any[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJSExpression(condition)) {
|
||||||
|
function Condition(props: any) {
|
||||||
|
if (props.condition) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Condition.displayName = 'Condition';
|
||||||
|
|
||||||
|
const ReactivedCondition = reactive(Condition, {
|
||||||
|
target: {
|
||||||
|
condition,
|
||||||
|
},
|
||||||
|
valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
|
||||||
|
});
|
||||||
|
|
||||||
|
element = createElement(ReactivedCondition, {
|
||||||
|
key: widget.key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const element = buildRenderElement();
|
||||||
|
|
||||||
|
return Wrappers.reduce((prevElement, CurWrapper) => {
|
||||||
|
return createElement(CurWrapper, { key: widget.key }, prevElement);
|
||||||
|
}, element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const LowCodeComponent = forwardRef(function (
|
||||||
|
props: LowCodeComponentProps,
|
||||||
|
ref: ForwardedRef<any>,
|
||||||
|
) {
|
||||||
|
const { id, className, style } = props;
|
||||||
|
const isMounted = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const scopeValue = container.codeRuntime.getScope().value;
|
||||||
|
|
||||||
|
// init dataSource
|
||||||
|
scopeValue.reloadDataSource();
|
||||||
|
|
||||||
|
let styleEl: HTMLElement | undefined;
|
||||||
|
const cssText = container.getCssText();
|
||||||
|
if (cssText) {
|
||||||
|
appendExternalStyle(cssText).then((el) => {
|
||||||
|
styleEl = el;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger lifeCycles
|
||||||
|
// componentDidMount?.();
|
||||||
|
container.triggerLifeCycle('componentDidMount');
|
||||||
|
|
||||||
|
// 当 state 改变之后调用
|
||||||
|
const unwatch = watch(scopeValue.state, (_, oldVal) => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
container.triggerLifeCycle('componentDidUpdate', props, oldVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
isMounted.current = true;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// componentWillUnmount?.();
|
||||||
|
container.triggerLifeCycle('componentWillUnmount');
|
||||||
|
styleEl?.parentNode?.removeChild(styleEl);
|
||||||
|
unwatch();
|
||||||
|
isMounted.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const widgets = useMemo(() => {
|
||||||
|
return container.createWidgets<ComponentType<any>>().map((widget) =>
|
||||||
|
widget.mapRenderObject((widget) => {
|
||||||
|
if (widget instanceof TextWidget) {
|
||||||
|
if (widget.type === 'i18n') {
|
||||||
|
function IntlText(props: { key: string; params: Record<string, string> }) {
|
||||||
|
return <>{intl.i18n(props.key, props.params)}</>;
|
||||||
|
}
|
||||||
|
IntlText.displayName = 'IntlText';
|
||||||
|
return IntlText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Text(props: { text: string }) {
|
||||||
|
return <>{props.text}</>;
|
||||||
|
}
|
||||||
|
Text.displayName = 'Text';
|
||||||
|
return Text;
|
||||||
|
} else if (widget instanceof ComponentWidget) {
|
||||||
|
return getComponentByName(widget.raw.componentName);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id={id} className={className} style={style} ref={ref}>
|
||||||
|
{widgets.map((widget) => createReactElement(widget, container.codeRuntime))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
LowCodeComponent.displayName = displayName;
|
||||||
|
|
||||||
|
return LowCodeComponent;
|
||||||
|
});
|
||||||
|
|
||||||
|
function reactiveStateCreator(initState: PlainObject): InstanceStateApi {
|
||||||
|
const proxyState = createSignal(initState);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get state() {
|
||||||
|
return proxyState.value;
|
||||||
|
},
|
||||||
|
setState(newState) {
|
||||||
|
if (!isPlainObject(newState)) {
|
||||||
|
throw Error('newState mush be a object');
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyState.value = {
|
||||||
|
...proxyState.value,
|
||||||
|
...newState,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { AppContext, type AppContextObject } from '../context/app';
|
import { AppContext, type AppContextObject } from '../context/app';
|
||||||
import { createComponent } from '../api/createComponent';
|
import { createComponent } from '../component';
|
||||||
import Route from './route';
|
import Route from './route';
|
||||||
|
import { createRouterProvider } from './router-view';
|
||||||
|
|
||||||
export default function App({ context }: { context: AppContextObject }) {
|
export default function App({ context }: { context: AppContextObject }) {
|
||||||
const { schema, config, renderer, packageManager, appScope } = context;
|
const { schema, config, renderer, packageManager, appScope } = context;
|
||||||
@ -15,8 +16,7 @@ export default function App({ context }: { context: AppContextObject }) {
|
|||||||
|
|
||||||
if (Component?.devMode === 'lowCode') {
|
if (Component?.devMode === 'lowCode') {
|
||||||
const componentsMap = schema.getComponentsMaps();
|
const componentsMap = schema.getComponentsMaps();
|
||||||
const componentsRecord =
|
const componentsRecord = packageManager.getComponentsNameRecord<any>(componentsMap);
|
||||||
packageManager.getComponentsNameRecord<any>(componentsMap);
|
|
||||||
|
|
||||||
const Layout = createComponent({
|
const Layout = createComponent({
|
||||||
componentsTree: Component.schema,
|
componentsTree: Component.schema,
|
||||||
@ -24,6 +24,7 @@ export default function App({ context }: { context: AppContextObject }) {
|
|||||||
|
|
||||||
dataSourceCreator: config.get('dataSourceCreator'),
|
dataSourceCreator: config.get('dataSourceCreator'),
|
||||||
supCodeScope: appScope,
|
supCodeScope: appScope,
|
||||||
|
intl: appScope.value.intl,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Layout;
|
return Layout;
|
||||||
@ -54,5 +55,11 @@ export default function App({ context }: { context: AppContextObject }) {
|
|||||||
}, element);
|
}, element);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <AppContext.Provider value={context}>{element}</AppContext.Provider>;
|
const RouterProvider = createRouterProvider(appScope.value.router);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider value={context}>
|
||||||
|
<RouterProvider>{element}</RouterProvider>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,43 +1,28 @@
|
|||||||
import type { PageSchema, PageContainerSchema } from '@alilc/runtime-shared';
|
import type { PageConfig, ComponentTree } from '@alilc/renderer-core';
|
||||||
import { useAppContext } from '../context/app';
|
import { useAppContext } from '../context/app';
|
||||||
import { createComponent } from '../api/createComponent';
|
import { createComponent } from '../component';
|
||||||
import { PAGE_EVENTS } from '../events';
|
|
||||||
|
|
||||||
export interface OutletProps {
|
export interface OutletProps {
|
||||||
pageSchema: PageSchema;
|
pageConfig: PageConfig;
|
||||||
componentsTree?: PageContainerSchema | undefined;
|
componentsTree?: ComponentTree | undefined;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Outlet({ pageSchema, componentsTree }: OutletProps) {
|
export default function Outlet({ pageSchema, componentsTree }: OutletProps) {
|
||||||
const { schema, config, packageManager, appScope, boosts } = useAppContext();
|
const { schema, config, packageManager, appScope } = useAppContext();
|
||||||
const { type = 'lowCode' } = pageSchema;
|
const { type = 'lowCode' } = pageSchema;
|
||||||
|
|
||||||
if (type === 'lowCode' && componentsTree) {
|
if (type === 'lowCode' && componentsTree) {
|
||||||
const componentsMap = schema.getComponentsMaps();
|
const componentsMap = schema.getComponentsMaps();
|
||||||
const componentsRecord =
|
const componentsRecord = packageManager.getComponentsNameRecord<any>(componentsMap);
|
||||||
packageManager.getComponentsNameRecord<any>(componentsMap);
|
|
||||||
|
|
||||||
const LowCodeComponent = createComponent({
|
const LowCodeComponent = createComponent({
|
||||||
supCodeScope: appScope,
|
supCodeScope: appScope,
|
||||||
dataSourceCreator: config.get('dataSourceCreator'),
|
dataSourceCreator: config.get('dataSourceCreator'),
|
||||||
componentsTree,
|
componentsTree,
|
||||||
componentsRecord,
|
componentsRecord,
|
||||||
|
intl: appScope.value.intl,
|
||||||
beforeNodeCreateComponent(node) {
|
|
||||||
boosts.hooks.call(PAGE_EVENTS.COMPONENT_BEFORE_NODE_CREATE, node);
|
|
||||||
},
|
|
||||||
nodeCreatedComponent(result) {
|
|
||||||
boosts.hooks.call(PAGE_EVENTS.COMPONENT_NODE_CREATED, result);
|
|
||||||
},
|
|
||||||
nodeComponentRefAttached(node, instance) {
|
|
||||||
boosts.hooks.call(
|
|
||||||
PAGE_EVENTS.COMPONENT_NODE_REF_ATTACHED,
|
|
||||||
node,
|
|
||||||
instance
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return <LowCodeComponent />;
|
return <LowCodeComponent />;
|
||||||
|
|||||||
@ -1,28 +1,21 @@
|
|||||||
import { usePageSchema } from '../context/router';
|
import { usePageConfig } from '../context/router';
|
||||||
import { useAppContext } from '../context/app';
|
import { useAppContext } from '../context/app';
|
||||||
|
import RouteOutlet from './outlet';
|
||||||
|
|
||||||
export default function Route(props: any) {
|
export default function Route(props: any) {
|
||||||
const { schema, renderer } = useAppContext();
|
const { schema, renderer } = useAppContext();
|
||||||
const pageSchema = usePageSchema();
|
const pageConfig = usePageConfig();
|
||||||
const Outlet = renderer.getOutlet();
|
const Outlet = renderer.getOutlet() ?? RouteOutlet;
|
||||||
|
|
||||||
if (Outlet && pageSchema) {
|
if (Outlet && pageConfig) {
|
||||||
let componentsTree;
|
let componentsTree;
|
||||||
const { type = 'lowCode', treeId } = pageSchema;
|
const { type = 'lowCode', mappingId } = pageConfig;
|
||||||
|
|
||||||
if (type === 'lowCode') {
|
if (type === 'lowCode') {
|
||||||
componentsTree = schema
|
componentsTree = schema.getComponentsTrees().find((item) => item.id === mappingId);
|
||||||
.getComponentsTrees()
|
|
||||||
.find(item => item.id === treeId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <Outlet {...props} pageConfig={pageConfig} componentsTree={componentsTree} />;
|
||||||
<Outlet
|
|
||||||
{...props}
|
|
||||||
pageSchema={pageSchema}
|
|
||||||
componentsTree={componentsTree}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -1,28 +1,24 @@
|
|||||||
import { type Router } from '@alilc/runtime-router';
|
import { type Router } from '@alilc/runtime-router';
|
||||||
import { useState, useLayoutEffect, useMemo, type ReactNode } from 'react';
|
import { useState, useLayoutEffect, useMemo, type ReactNode } from 'react';
|
||||||
import {
|
import { RouterContext, RouteLocationContext, PageConfigContext } from '../context/router';
|
||||||
RouterContext,
|
|
||||||
RouteLocationContext,
|
|
||||||
PageSchemaContext,
|
|
||||||
} from '../context/router';
|
|
||||||
import { useAppContext } from '../context/app';
|
import { useAppContext } from '../context/app';
|
||||||
|
|
||||||
export const createRouterProvider = (router: Router) => {
|
export const createRouterProvider = (router: Router) => {
|
||||||
return function RouterProvider({ children }: { children?: ReactNode }) {
|
return function RouterProvider({ children }: { children?: ReactNode }) {
|
||||||
const { schema } = useAppContext();
|
const { schema } = useAppContext();
|
||||||
const [location, setCurrentLocation] = useState(router.getCurrentRoute());
|
const [location, setCurrentLocation] = useState(router.getCurrentLocation());
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const remove = router.afterRouteChange(to => setCurrentLocation(to));
|
const remove = router.afterRouteChange((to) => setCurrentLocation(to));
|
||||||
return () => remove();
|
return () => remove();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pageSchema = useMemo(() => {
|
const pageSchema = useMemo(() => {
|
||||||
const pages = schema.getPages();
|
const pages = schema.getPageConfigs();
|
||||||
const matched = location.matched[location.matched.length - 1];
|
const matched = location.matched[location.matched.length - 1];
|
||||||
|
|
||||||
if (matched) {
|
if (matched) {
|
||||||
const page = pages.find(item => matched.page === item.id);
|
const page = pages.find((item) => matched.page === item.id);
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,9 +28,7 @@ export const createRouterProvider = (router: Router) => {
|
|||||||
return (
|
return (
|
||||||
<RouterContext.Provider value={router}>
|
<RouterContext.Provider value={router}>
|
||||||
<RouteLocationContext.Provider value={location}>
|
<RouteLocationContext.Provider value={location}>
|
||||||
<PageSchemaContext.Provider value={pageSchema}>
|
<PageConfigContext.Provider value={pageSchema}>{children}</PageConfigContext.Provider>
|
||||||
{children}
|
|
||||||
</PageSchemaContext.Provider>
|
|
||||||
</RouteLocationContext.Provider>
|
</RouteLocationContext.Provider>
|
||||||
</RouterContext.Provider>
|
</RouterContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import { type AppContext as AppContextType } from '@alilc/runtime-core';
|
import { type AppContext as AppContextType } from '@alilc/renderer-core';
|
||||||
import { type ReactRenderer } from '../renderer';
|
import { type ReactRenderer } from '../renderer';
|
||||||
|
|
||||||
export interface AppContextObject extends AppContextType {
|
export interface AppContextObject extends AppContextType {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { type Router, type RouteLocation } from '@alilc/runtime-router';
|
import { type Router, type RouteLocationNormalized } from '@alilc/runtime-router';
|
||||||
import { type PageSchema } from '@alilc/runtime-shared';
|
import { type PageConfig } from '@alilc/renderer-core';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
export const RouterContext = createContext<Router>({} as any);
|
export const RouterContext = createContext<Router>({} as any);
|
||||||
@ -8,10 +8,10 @@ RouterContext.displayName = 'RouterContext';
|
|||||||
|
|
||||||
export const useRouter = () => useContext(RouterContext);
|
export const useRouter = () => useContext(RouterContext);
|
||||||
|
|
||||||
export const RouteLocationContext = createContext<RouteLocation>({
|
export const RouteLocationContext = createContext<RouteLocationNormalized>({
|
||||||
name: undefined,
|
name: undefined,
|
||||||
path: '/',
|
path: '/',
|
||||||
query: {},
|
searchParams: undefined,
|
||||||
params: {},
|
params: {},
|
||||||
hash: '',
|
hash: '',
|
||||||
fullPath: '/',
|
fullPath: '/',
|
||||||
@ -24,10 +24,8 @@ RouteLocationContext.displayName = 'RouteLocationContext';
|
|||||||
|
|
||||||
export const useRouteLocation = () => useContext(RouteLocationContext);
|
export const useRouteLocation = () => useContext(RouteLocationContext);
|
||||||
|
|
||||||
export const PageSchemaContext = createContext<PageSchema | undefined>(
|
export const PageConfigContext = createContext<PageConfig | undefined>(undefined);
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
PageSchemaContext.displayName = 'PageContext';
|
PageConfigContext.displayName = 'PageConfigContext';
|
||||||
|
|
||||||
export const usePageSchema = () => useContext(PageSchemaContext);
|
export const usePageConfig = () => useContext(PageConfigContext);
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from './api/app';
|
||||||
|
export * from './api/component';
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { type ReactNode, createElement } from 'react';
|
|
||||||
import { someValue } from '@alilc/runtime-core';
|
|
||||||
import { isJsExpression } from '@alilc/runtime-shared';
|
|
||||||
import { definePlugin } from '../../renderer';
|
|
||||||
import { PAGE_EVENTS } from '../../events';
|
|
||||||
import { reactive } from '../../utils/reactive';
|
|
||||||
import { createIntl } from './intl';
|
|
||||||
|
|
||||||
export { createIntl };
|
|
||||||
|
|
||||||
declare module '@alilc/renderer-core' {
|
|
||||||
interface AppBoosts {
|
|
||||||
intl: ReturnType<typeof createIntl>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const intlPlugin = definePlugin({
|
|
||||||
name: 'intl',
|
|
||||||
setup({ schema, appScope, boosts }) {
|
|
||||||
const i18nMessages = schema.getByKey('i18n') ?? {};
|
|
||||||
const defaultLocale = schema.getByPath('config.defaultLocale') ?? 'zh-CN';
|
|
||||||
const intl = createIntl(i18nMessages, defaultLocale);
|
|
||||||
|
|
||||||
appScope.setValue(intl);
|
|
||||||
boosts.add('intl', intl);
|
|
||||||
|
|
||||||
boosts.hooks.hook(PAGE_EVENTS.COMPONENT_BEFORE_NODE_CREATE, (node) => {
|
|
||||||
if (node.type === 'i18n') {
|
|
||||||
const { key, params } = node.raw.data;
|
|
||||||
|
|
||||||
let element: ReactNode;
|
|
||||||
|
|
||||||
if (someValue(params, isJsExpression)) {
|
|
||||||
function IntlText(props: any) {
|
|
||||||
return intl.i18n(key, props.params);
|
|
||||||
}
|
|
||||||
IntlText.displayName = 'IntlText';
|
|
||||||
|
|
||||||
const Reactived = reactive(IntlText, {
|
|
||||||
target: {
|
|
||||||
params,
|
|
||||||
},
|
|
||||||
valueGetter(expr) {
|
|
||||||
return node.context.codeRuntime.parseExprOrFn(expr);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
element = createElement(Reactived, { key });
|
|
||||||
} else {
|
|
||||||
element = intl.i18n(key, params ?? {});
|
|
||||||
}
|
|
||||||
|
|
||||||
node.setReactNode(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
import { createCodeRuntime, type PackageManager } from '@alilc/runtime-core';
|
|
||||||
import {
|
|
||||||
type UtilItem,
|
|
||||||
type InternalUtils,
|
|
||||||
type ExternalUtils,
|
|
||||||
type AnyFunction,
|
|
||||||
} from '@alilc/runtime-shared';
|
|
||||||
import { definePlugin } from '../../renderer';
|
|
||||||
|
|
||||||
declare module '@alilc/runtime-core' {
|
|
||||||
interface AppBoosts {
|
|
||||||
globalUtils: GlobalUtils;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GlobalUtils {
|
|
||||||
getUtil(name: string): AnyFunction;
|
|
||||||
|
|
||||||
addUtil(utilItem: UtilItem): void;
|
|
||||||
addUtil(name: string, fn: AnyFunction): void;
|
|
||||||
|
|
||||||
addUtils(utils: Record<string, AnyFunction>): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const globalUtilsPlugin = definePlugin({
|
|
||||||
name: 'globalUtils',
|
|
||||||
setup({ schema, appScope, packageManager, boosts }) {
|
|
||||||
const utils = schema.getByKey('utils') ?? [];
|
|
||||||
const globalUtils = createGlobalUtils(packageManager);
|
|
||||||
|
|
||||||
const utilsProxy = new Proxy(Object.create(null), {
|
|
||||||
get(_, p: string) {
|
|
||||||
return globalUtils.getUtil(p);
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
has(_, p: string) {
|
|
||||||
return Boolean(globalUtils.getUtil(p));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.forEach(globalUtils.addUtil);
|
|
||||||
|
|
||||||
appScope.inject('utils', utilsProxy);
|
|
||||||
boosts.add('globalUtils', globalUtils);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function createGlobalUtils(packageManager: PackageManager) {
|
|
||||||
const codeRuntime = createCodeRuntime();
|
|
||||||
const utilsMap: Record<string, AnyFunction> = {};
|
|
||||||
|
|
||||||
function addUtil(item: string | UtilItem, fn?: AnyFunction) {
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
if (typeof fn === 'function') {
|
|
||||||
utilsMap[item] = fn;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const fn = parseUtil(item);
|
|
||||||
addUtil(item.name, fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalUtils: GlobalUtils = {
|
|
||||||
addUtil,
|
|
||||||
addUtils(utils) {
|
|
||||||
Object.keys(utils).forEach(key => addUtil(key, utils[key]));
|
|
||||||
},
|
|
||||||
getUtil(name) {
|
|
||||||
return utilsMap[name];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseUtil(utilItem: UtilItem) {
|
|
||||||
if (utilItem.type === 'function') {
|
|
||||||
const { content } = utilItem as InternalUtils;
|
|
||||||
|
|
||||||
return codeRuntime.createFnBoundScope(content.value);
|
|
||||||
} else {
|
|
||||||
const {
|
|
||||||
content: { package: packageName, destructuring, exportName, subName },
|
|
||||||
} = utilItem as ExternalUtils;
|
|
||||||
let library: any = packageManager.getLibraryByPackageName(packageName);
|
|
||||||
|
|
||||||
if (library) {
|
|
||||||
if (destructuring) {
|
|
||||||
const target = library[exportName!];
|
|
||||||
library = subName ? target[subName] : target;
|
|
||||||
}
|
|
||||||
|
|
||||||
return library;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return globalUtils;
|
|
||||||
}
|
|
||||||
@ -2,7 +2,7 @@ import {
|
|||||||
definePlugin as definePluginFn,
|
definePlugin as definePluginFn,
|
||||||
type Plugin,
|
type Plugin,
|
||||||
type PluginSetupContext,
|
type PluginSetupContext,
|
||||||
} from '@alilc/runtime-core';
|
} from '@alilc/renderer-core';
|
||||||
import { type ComponentType, type PropsWithChildren } from 'react';
|
import { type ComponentType, type PropsWithChildren } from 'react';
|
||||||
import { type OutletProps } from './components/outlet';
|
import { type OutletProps } from './components/outlet';
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
import { type Router, type RouterOptions, createRouter } from '@alilc/runtime-router';
|
|
||||||
import { createRouterProvider } from './components/router-view';
|
|
||||||
import RouteOutlet from './components/outlet';
|
|
||||||
import { type ReactRendererSetupContext } from './renderer';
|
|
||||||
|
|
||||||
declare module '@alilc/renderer-core' {
|
|
||||||
interface AppBoosts {
|
|
||||||
router: Router;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultRouterOptions: RouterOptions = {
|
|
||||||
historyMode: 'browser',
|
|
||||||
baseName: '/',
|
|
||||||
routes: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function initRouter(context: ReactRendererSetupContext) {
|
|
||||||
const { schema, boosts, appScope, renderer } = context;
|
|
||||||
const router = createRouter(schema.getByKey('router') ?? defaultRouterOptions);
|
|
||||||
|
|
||||||
appScope.inject('router', router);
|
|
||||||
boosts.add('router', router);
|
|
||||||
|
|
||||||
const RouterProvider = createRouterProvider(router);
|
|
||||||
|
|
||||||
renderer.addAppWrapper(RouterProvider);
|
|
||||||
renderer.setOutlet(RouteOutlet);
|
|
||||||
}
|
|
||||||
@ -3,13 +3,11 @@ import { createSignal, computed } from '../../signals';
|
|||||||
|
|
||||||
export function createIntl(
|
export function createIntl(
|
||||||
messages: Record<string, Record<string, string>>,
|
messages: Record<string, Record<string, string>>,
|
||||||
defaultLocale: string
|
defaultLocale: string,
|
||||||
) {
|
) {
|
||||||
const allMessages = createSignal(messages);
|
const allMessages = createSignal(messages);
|
||||||
const currentLocale = createSignal(defaultLocale);
|
const currentLocale = createSignal(defaultLocale);
|
||||||
const currentMessages = computed(
|
const currentMessages = computed(() => allMessages.value[currentLocale.value]);
|
||||||
() => allMessages.value[currentLocale.value]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
i18n(key: string, params: Record<string, string>) {
|
i18n(key: string, params: Record<string, string>) {
|
||||||
72
runtime/renderer-react/src/runtime-api/utils.ts
Normal file
72
runtime/renderer-react/src/runtime-api/utils.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
createCodeRuntime,
|
||||||
|
type PackageManager,
|
||||||
|
type AnyFunction,
|
||||||
|
type Util,
|
||||||
|
type UtilsApi,
|
||||||
|
} from '@alilc/renderer-core';
|
||||||
|
|
||||||
|
export interface RuntimeUtils extends UtilsApi {
|
||||||
|
addUtil(utilItem: Util): void;
|
||||||
|
addUtil(name: string, fn: AnyFunction): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRuntimeUtils(
|
||||||
|
utilSchema: Util[],
|
||||||
|
packageManager: PackageManager,
|
||||||
|
): RuntimeUtils {
|
||||||
|
const codeRuntime = createCodeRuntime();
|
||||||
|
const utilsMap: Record<string, AnyFunction> = {};
|
||||||
|
|
||||||
|
function addUtil(item: string | Util, fn?: AnyFunction) {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
if (typeof fn === 'function') {
|
||||||
|
utilsMap[item] = fn;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const fn = parseUtil(item);
|
||||||
|
addUtil(item.name, fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUtil(utilItem: Util) {
|
||||||
|
if (utilItem.type === 'function') {
|
||||||
|
const { content } = utilItem;
|
||||||
|
|
||||||
|
return codeRuntime.createFnBoundScope(content.value);
|
||||||
|
} else {
|
||||||
|
const {
|
||||||
|
content: { package: packageName, destructuring, exportName, subName },
|
||||||
|
} = utilItem;
|
||||||
|
let library: any = packageManager.getLibraryByPackageName(packageName!);
|
||||||
|
|
||||||
|
if (library) {
|
||||||
|
if (destructuring) {
|
||||||
|
const target = library[exportName!];
|
||||||
|
library = subName ? target[subName] : target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return library;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utilSchema.forEach((item) => addUtil(item));
|
||||||
|
|
||||||
|
const utilsProxy = new Proxy(Object.create(null), {
|
||||||
|
get(_, p: string) {
|
||||||
|
return utilsMap[p];
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
has(_, p: string) {
|
||||||
|
return Boolean(utilsMap[p]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
addUtil,
|
||||||
|
utils: utilsProxy,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { type PlainObject } from '@alilc/renderer-core';
|
||||||
import {
|
import {
|
||||||
ref,
|
ref,
|
||||||
computed,
|
computed,
|
||||||
@ -114,7 +115,7 @@ function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set<u
|
|||||||
});
|
});
|
||||||
} else if (isPlainObject(value)) {
|
} else if (isPlainObject(value)) {
|
||||||
for (const key in value) {
|
for (const key in value) {
|
||||||
traverse(value[key], depth, currentDepth, seen);
|
traverse((value as PlainObject)[key], depth, currentDepth, seen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@ -1,32 +1,26 @@
|
|||||||
import {
|
import {
|
||||||
type AnyObject,
|
processValue,
|
||||||
type AnyFunction,
|
type AnyFunction,
|
||||||
|
type PlainObject,
|
||||||
type JSExpression,
|
type JSExpression,
|
||||||
isJsExpression,
|
isJSExpression,
|
||||||
} from '@alilc/runtime-shared';
|
} from '@alilc/renderer-core';
|
||||||
import { processValue } from '@alilc/runtime-core';
|
import { type ComponentType, memo, forwardRef, type PropsWithChildren, createElement } from 'react';
|
||||||
import {
|
|
||||||
type ComponentType,
|
|
||||||
memo,
|
|
||||||
forwardRef,
|
|
||||||
type ForwardRefRenderFunction,
|
|
||||||
type PropsWithChildren,
|
|
||||||
} from 'react';
|
|
||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||||
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
||||||
import { computed, watch } from '../signals';
|
import { computed, watch } from '../signals';
|
||||||
|
|
||||||
export interface ReactiveStore<Snapshot = AnyObject> {
|
export interface ReactiveStore<Snapshot = PlainObject> {
|
||||||
value: Snapshot;
|
value: Snapshot;
|
||||||
onStateChange: AnyFunction | null;
|
onStateChange: AnyFunction | null;
|
||||||
subscribe: (onStoreChange: () => void) => () => void;
|
subscribe: (onStoreChange: () => void) => () => void;
|
||||||
getSnapshot: () => Snapshot;
|
getSnapshot: () => Snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReactiveStore<Snapshot = AnyObject>(
|
function createReactiveStore<Snapshot = PlainObject>(
|
||||||
target: Record<string, any>,
|
target: Record<string, any>,
|
||||||
valueGetter: (expr: JSExpression) => any
|
valueGetter: (expr: JSExpression) => any,
|
||||||
): ReactiveStore<Snapshot> {
|
): ReactiveStore<Snapshot> {
|
||||||
let isFlushing = false;
|
let isFlushing = false;
|
||||||
let isFlushPending = false;
|
let isFlushPending = false;
|
||||||
@ -34,25 +28,21 @@ function createReactiveStore<Snapshot = AnyObject>(
|
|||||||
const cleanups: Array<() => void> = [];
|
const cleanups: Array<() => void> = [];
|
||||||
const waitPathToSetValueMap = new Map();
|
const waitPathToSetValueMap = new Map();
|
||||||
|
|
||||||
const initValue = processValue(
|
const initValue = processValue(target, isJSExpression, (node: JSExpression, paths) => {
|
||||||
target,
|
const computedValue = computed(() => valueGetter(node));
|
||||||
isJsExpression,
|
const unwatch = watch(computedValue, (newValue) => {
|
||||||
(node: JSExpression, paths) => {
|
waitPathToSetValueMap.set(paths, newValue);
|
||||||
const computedValue = computed(() => valueGetter(node));
|
|
||||||
const unwatch = watch(computedValue, newValue => {
|
|
||||||
waitPathToSetValueMap.set(paths, newValue);
|
|
||||||
|
|
||||||
if (!isFlushPending && !isFlushing) {
|
if (!isFlushPending && !isFlushing) {
|
||||||
isFlushPending = true;
|
isFlushPending = true;
|
||||||
Promise.resolve().then(genValue);
|
Promise.resolve().then(genValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cleanups.push(unwatch);
|
cleanups.push(unwatch);
|
||||||
|
|
||||||
return computedValue.value;
|
return computedValue.value;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const genValue = () => {
|
const genValue = () => {
|
||||||
isFlushPending = false;
|
isFlushPending = false;
|
||||||
@ -89,7 +79,7 @@ function createReactiveStore<Snapshot = AnyObject>(
|
|||||||
return () => {
|
return () => {
|
||||||
store.onStateChange = null;
|
store.onStateChange = null;
|
||||||
|
|
||||||
cleanups.forEach(c => c());
|
cleanups.forEach((c) => c());
|
||||||
cleanups.length = 0;
|
cleanups.length = 0;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -102,35 +92,32 @@ function createReactiveStore<Snapshot = AnyObject>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ReactiveOptions {
|
interface ReactiveOptions {
|
||||||
target: AnyObject;
|
target: PlainObject;
|
||||||
valueGetter: (expr: JSExpression) => any;
|
valueGetter: (expr: JSExpression) => any;
|
||||||
forwardRef?: boolean;
|
forwardRef?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reactive<TProps extends AnyObject = AnyObject>(
|
export function reactive<TProps extends PlainObject = PlainObject>(
|
||||||
WrappedComponent: ForwardRefRenderFunction<PropsWithChildren<TProps>>,
|
WrappedComponent: ComponentType<TProps>,
|
||||||
{ target, valueGetter, forwardRef: forwardRefOption = true }: ReactiveOptions
|
{ target, valueGetter, forwardRef: forwardRefOption = true }: ReactiveOptions,
|
||||||
) {
|
): ComponentType<PlainObject> {
|
||||||
const store = createReactiveStore(target, valueGetter);
|
const store = createReactiveStore(target, valueGetter);
|
||||||
|
|
||||||
function WrapperComponent(props: any, ref: any) {
|
function WrapperComponent(props: any, ref: any) {
|
||||||
const actualProps = useSyncExternalStore(
|
const actualProps = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
||||||
store.subscribe,
|
|
||||||
store.getSnapshot
|
return createElement(WrappedComponent, {
|
||||||
);
|
...props,
|
||||||
return <WrappedComponent {...props} {...actualProps} ref={ref} />;
|
...actualProps,
|
||||||
|
ref,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentName =
|
const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||||
WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
|
||||||
const displayName = `Reactive(${componentName})`;
|
const displayName = `Reactive(${componentName})`;
|
||||||
|
|
||||||
const _Reactived = forwardRefOption
|
const _Reactived = forwardRefOption ? forwardRef(WrapperComponent) : WrapperComponent;
|
||||||
? forwardRef(WrapperComponent)
|
const Reactived = memo(_Reactived) as unknown as ComponentType<PropsWithChildren<TProps>>;
|
||||||
: WrapperComponent;
|
|
||||||
const Reactived = memo(_Reactived) as unknown as ComponentType<
|
|
||||||
PropsWithChildren<TProps>
|
|
||||||
>;
|
|
||||||
|
|
||||||
Reactived.displayName = WrappedComponent.displayName = displayName;
|
Reactived.displayName = WrappedComponent.displayName = displayName;
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@alilc/*": ["runtime/*/src"]
|
"@alilc/*": ["runtime/*"],
|
||||||
|
"@alilc/runtime-router": ["runtime/router"]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,20 @@
|
|||||||
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
||||||
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "",
|
"build": "tsc",
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alilc/renderer-core": "^2.0.0-beta.0"
|
"@alilc/renderer-core": "^2.0.0-beta.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"path-to-regexp": "^6.2.1",
|
||||||
|
"qs": "^6.12.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/qs": "^6.9.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,72 +1,38 @@
|
|||||||
import {
|
import { RawRouteLocation } from '@alilc/renderer-core';
|
||||||
type RouteLocation,
|
import { type RouteLocationNormalized } from './types';
|
||||||
type RouteLocationRaw,
|
|
||||||
} from '@alilc/runtime-shared';
|
|
||||||
import { isRouteLocation } from './utils/helper';
|
import { isRouteLocation } from './utils/helper';
|
||||||
|
|
||||||
export type NavigationHookAfter = (
|
export type NavigationHookAfter = (
|
||||||
to: RouteLocation,
|
to: RouteLocationNormalized,
|
||||||
from: RouteLocation
|
from: RouteLocationNormalized,
|
||||||
) => any;
|
) => any;
|
||||||
|
|
||||||
export type NavigationGuardReturn =
|
export type NavigationGuardReturn = undefined | Error | RawRouteLocation | boolean;
|
||||||
| void
|
|
||||||
| Error
|
|
||||||
| RouteLocationRaw
|
|
||||||
| boolean
|
|
||||||
| NavigationGuardNextCallback;
|
|
||||||
|
|
||||||
export type NavigationGuardNextCallback = () => any;
|
|
||||||
|
|
||||||
export interface NavigationGuardNext {
|
|
||||||
(): void;
|
|
||||||
(error: Error): void;
|
|
||||||
(location: RouteLocationRaw): void;
|
|
||||||
(valid: boolean | undefined): void;
|
|
||||||
(cb: NavigationGuardNextCallback): void;
|
|
||||||
/**
|
|
||||||
* Allows to detect if `next` isn't called in a resolved guard. Used
|
|
||||||
* internally in DEV mode to emit a warning. Commented out to simplify
|
|
||||||
* typings.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
_called?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigation guard.
|
* Navigation guard.
|
||||||
*/
|
*/
|
||||||
export interface NavigationGuard {
|
export interface NavigationGuard {
|
||||||
(to: RouteLocation, from: RouteLocation, next: NavigationGuardNext):
|
(
|
||||||
| NavigationGuardReturn
|
to: RouteLocationNormalized,
|
||||||
| Promise<NavigationGuardReturn>;
|
from: RouteLocationNormalized,
|
||||||
|
): NavigationGuardReturn | Promise<NavigationGuardReturn>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function guardToPromiseFn(
|
export function guardToPromiseFn(
|
||||||
guard: NavigationGuard,
|
guard: NavigationGuard,
|
||||||
to: RouteLocation,
|
to: RouteLocationNormalized,
|
||||||
from: RouteLocation
|
from: RouteLocationNormalized,
|
||||||
): () => Promise<void> {
|
): () => Promise<void> {
|
||||||
return () =>
|
return () =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const next: NavigationGuardNext = (
|
const next = (valid?: boolean | RawRouteLocation | Error) => {
|
||||||
valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback | Error
|
|
||||||
) => {
|
|
||||||
if (valid === false) {
|
if (valid === false) {
|
||||||
reject();
|
reject();
|
||||||
} else if (valid instanceof Error) {
|
} else if (valid instanceof Error) {
|
||||||
reject(valid);
|
reject(valid);
|
||||||
} else if (isRouteLocation(valid)) {
|
} else if (isRouteLocation(valid)) {
|
||||||
// todo
|
// todo reject (error)
|
||||||
// reject(
|
|
||||||
// createRouterError<NavigationRedirectError>(
|
|
||||||
// ErrorTypes.NAVIGATION_GUARD_REDIRECT,
|
|
||||||
// {
|
|
||||||
// from: to,
|
|
||||||
// to: valid
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
reject();
|
reject();
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
@ -74,55 +40,10 @@ export function guardToPromiseFn(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 使用 Promise.resolve 包装允许它与异步和同步守卫一起工作
|
// 使用 Promise.resolve 包装允许它与异步和同步守卫一起工作
|
||||||
const guardReturn = guard.call(
|
const guardReturn = guard.call(null, to, from);
|
||||||
null,
|
|
||||||
to,
|
|
||||||
from,
|
|
||||||
canOnlyBeCalledOnce(next, to, from)
|
|
||||||
);
|
|
||||||
let guardCall = Promise.resolve(guardReturn);
|
|
||||||
|
|
||||||
if (guard.length <= 2) guardCall = guardCall.then(next);
|
return Promise.resolve(guardReturn)
|
||||||
if (guard.length > 2) {
|
.then(next)
|
||||||
const message = `The "next" callback was never called inside of ${
|
.catch((err) => reject(err));
|
||||||
guard.name ? '"' + guard.name + '"' : ''
|
|
||||||
}:\n${guard.toString()}\n. If you are returning a value instead of calling "next", make sure to remove the "next" parameter from your function.`;
|
|
||||||
|
|
||||||
if (typeof guardReturn === 'object' && 'then' in guardReturn) {
|
|
||||||
guardCall = guardCall.then(resolvedValue => {
|
|
||||||
if (!next._called) {
|
|
||||||
console.warn(message);
|
|
||||||
return Promise.reject(new Error('Invalid navigation guard'));
|
|
||||||
}
|
|
||||||
return resolvedValue;
|
|
||||||
});
|
|
||||||
} else if (guardReturn !== undefined) {
|
|
||||||
if (!next._called) {
|
|
||||||
console.warn(message);
|
|
||||||
reject(new Error('Invalid navigation guard'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
guardCall.catch(err => reject(err));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function canOnlyBeCalledOnce(
|
|
||||||
next: NavigationGuardNext,
|
|
||||||
to: RouteLocation,
|
|
||||||
from: RouteLocation
|
|
||||||
): NavigationGuardNext {
|
|
||||||
let called = 0;
|
|
||||||
return function () {
|
|
||||||
if (called++ === 1) {
|
|
||||||
console.warn(
|
|
||||||
`The "next" callback was called more than once in one navigation guard when going from "${from.fullPath}" to "${to.fullPath}". It should be called exactly one time in each navigation guard. This will fail in production.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
next._called = true;
|
|
||||||
if (called === 1) {
|
|
||||||
next.apply(null, arguments as any);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallbacks } from '@alilc/runtime-shared';
|
import { useEvent } from '@alilc/renderer-core';
|
||||||
|
|
||||||
export type HistoryState = Record<string | number, any>;
|
export type HistoryState = Record<string | number, any>;
|
||||||
export type HistoryLocation = string;
|
export type HistoryLocation = string;
|
||||||
@ -23,7 +23,7 @@ export type NavigationInformation = {
|
|||||||
export type NavigationCallback = (
|
export type NavigationCallback = (
|
||||||
to: HistoryLocation,
|
to: HistoryLocation,
|
||||||
from: HistoryLocation,
|
from: HistoryLocation,
|
||||||
info: NavigationInformation
|
info: NavigationInformation,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,7 +94,7 @@ function buildState(
|
|||||||
current: HistoryLocation,
|
current: HistoryLocation,
|
||||||
forward: HistoryLocation | null,
|
forward: HistoryLocation | null,
|
||||||
replaced = false,
|
replaced = false,
|
||||||
position = window.history.length
|
position = window.history.length,
|
||||||
): RouterHistoryState {
|
): RouterHistoryState {
|
||||||
return {
|
return {
|
||||||
name: '__ROUTER_STATE__',
|
name: '__ROUTER_STATE__',
|
||||||
@ -110,25 +110,14 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
|||||||
const finalBase = normalizeBase(base);
|
const finalBase = normalizeBase(base);
|
||||||
const { history, location } = window;
|
const { history, location } = window;
|
||||||
|
|
||||||
let currentLocation: HistoryLocation = createCurrentLocation(
|
let currentLocation: HistoryLocation = createCurrentLocation(finalBase, location);
|
||||||
finalBase,
|
|
||||||
location
|
|
||||||
);
|
|
||||||
let historyState: RouterHistoryState = history.state;
|
let historyState: RouterHistoryState = history.state;
|
||||||
|
|
||||||
if (!historyState) {
|
if (!historyState) {
|
||||||
doDomHistoryEvent(
|
doDomHistoryEvent(currentLocation, buildState(null, currentLocation, null, true), true);
|
||||||
currentLocation,
|
|
||||||
buildState(null, currentLocation, null, true),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function doDomHistoryEvent(
|
function doDomHistoryEvent(to: HistoryLocation, state: RouterHistoryState, replace: boolean) {
|
||||||
to: HistoryLocation,
|
|
||||||
state: RouterHistoryState,
|
|
||||||
replace: boolean
|
|
||||||
) {
|
|
||||||
// 处理 hash 情况下的 url
|
// 处理 hash 情况下的 url
|
||||||
const hashIndex = finalBase.indexOf('#');
|
const hashIndex = finalBase.indexOf('#');
|
||||||
const url =
|
const url =
|
||||||
@ -150,7 +139,7 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
|||||||
history.state,
|
history.state,
|
||||||
buildState(historyState.back, to, historyState.forward, true),
|
buildState(historyState.back, to, historyState.forward, true),
|
||||||
data,
|
data,
|
||||||
{ position: historyState.position }
|
{ position: historyState.position },
|
||||||
);
|
);
|
||||||
|
|
||||||
doDomHistoryEvent(to, state, true);
|
doDomHistoryEvent(to, state, true);
|
||||||
@ -158,14 +147,9 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function push(to: HistoryLocation, data?: HistoryState) {
|
function push(to: HistoryLocation, data?: HistoryState) {
|
||||||
const currentState: RouterHistoryState = Object.assign(
|
const currentState: RouterHistoryState = Object.assign({}, historyState, history.state, {
|
||||||
{},
|
forward: to,
|
||||||
historyState,
|
});
|
||||||
history.state,
|
|
||||||
{
|
|
||||||
forward: to,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 防止当前浏览器的 state 被修改先 replace 一次
|
// 防止当前浏览器的 state 被修改先 replace 一次
|
||||||
// 将上次的state 的 forward 修改为 to
|
// 将上次的state 的 forward 修改为 to
|
||||||
@ -175,15 +159,15 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
|||||||
{},
|
{},
|
||||||
buildState(currentLocation, to, null),
|
buildState(currentLocation, to, null),
|
||||||
{ position: currentState.position + 1 },
|
{ position: currentState.position + 1 },
|
||||||
data
|
data,
|
||||||
);
|
);
|
||||||
|
|
||||||
doDomHistoryEvent(to, state, false);
|
doDomHistoryEvent(to, state, false);
|
||||||
currentLocation = to;
|
currentLocation = to;
|
||||||
}
|
}
|
||||||
|
|
||||||
let listeners = useCallbacks<NavigationCallback>();
|
let listeners = useEvent<NavigationCallback>();
|
||||||
let teardowns = useCallbacks<() => void>();
|
let teardowns = useEvent<() => void>();
|
||||||
|
|
||||||
let pauseState: HistoryLocation | null = null;
|
let pauseState: HistoryLocation | null = null;
|
||||||
|
|
||||||
@ -293,8 +277,7 @@ function normalizeBase(base?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TRAILING_SLASH_RE = /\/$/;
|
const TRAILING_SLASH_RE = /\/$/;
|
||||||
export const removeTrailingSlash = (path: string) =>
|
export const removeTrailingSlash = (path: string) => path.replace(TRAILING_SLASH_RE, '');
|
||||||
path.replace(TRAILING_SLASH_RE, '');
|
|
||||||
|
|
||||||
function createCurrentLocation(base: string, location: Location) {
|
function createCurrentLocation(base: string, location: Location) {
|
||||||
const { pathname, search, hash } = location;
|
const { pathname, search, hash } = location;
|
||||||
@ -302,9 +285,7 @@ function createCurrentLocation(base: string, location: Location) {
|
|||||||
// hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
|
// hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
|
||||||
const hashPos = base.indexOf('#');
|
const hashPos = base.indexOf('#');
|
||||||
if (hashPos > -1) {
|
if (hashPos > -1) {
|
||||||
let slicePos = hash.includes(base.slice(hashPos))
|
let slicePos = hash.includes(base.slice(hashPos)) ? base.slice(hashPos).length : 1;
|
||||||
? base.slice(hashPos).length
|
|
||||||
: 1;
|
|
||||||
let pathFromHash = hash.slice(slicePos);
|
let pathFromHash = hash.slice(slicePos);
|
||||||
// prepend the starting slash to hash so the url starts with /#
|
// prepend the starting slash to hash so the url starts with /#
|
||||||
if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash;
|
if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash;
|
||||||
@ -339,10 +320,7 @@ export function createHashHistory(base?: string): RouterHistory {
|
|||||||
|
|
||||||
if (!base.endsWith('#/') && !base.endsWith('#')) {
|
if (!base.endsWith('#/') && !base.endsWith('#')) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`A hash base must end with a "#":\n"${base}" should be "${base.replace(
|
`A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, '#')}".`,
|
||||||
/#.*$/,
|
|
||||||
'#'
|
|
||||||
)}".`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,12 +348,12 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
|||||||
historyStack.push({ location, state });
|
historyStack.push({ location, state });
|
||||||
}
|
}
|
||||||
|
|
||||||
const listeners = useCallbacks<NavigationCallback>();
|
const listeners = useEvent<NavigationCallback>();
|
||||||
|
|
||||||
function triggerListeners(
|
function triggerListeners(
|
||||||
to: HistoryLocation,
|
to: HistoryLocation,
|
||||||
from: HistoryLocation,
|
from: HistoryLocation,
|
||||||
{ direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>
|
{ direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>,
|
||||||
): void {
|
): void {
|
||||||
const info: NavigationInformation = {
|
const info: NavigationInformation = {
|
||||||
direction,
|
direction,
|
||||||
@ -411,7 +389,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
|||||||
current: to,
|
current: to,
|
||||||
},
|
},
|
||||||
data,
|
data,
|
||||||
{ position }
|
{ position },
|
||||||
);
|
);
|
||||||
|
|
||||||
// remove current entry and decrement position
|
// remove current entry and decrement position
|
||||||
@ -428,12 +406,9 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
|||||||
historyStack.splice(position, 1);
|
historyStack.splice(position, 1);
|
||||||
pushStack(prevState.current, prevState);
|
pushStack(prevState.current, prevState);
|
||||||
|
|
||||||
const currentState = Object.assign(
|
const currentState = Object.assign({}, buildState(prevState.current, to, null, false), data, {
|
||||||
{},
|
position: ++position,
|
||||||
buildState(prevState.current, to, null, false),
|
});
|
||||||
data,
|
|
||||||
{ position: ++position }
|
|
||||||
);
|
|
||||||
|
|
||||||
pushStack(to, currentState);
|
pushStack(to, currentState);
|
||||||
},
|
},
|
||||||
@ -442,10 +417,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
|||||||
const direction: NavigationDirection =
|
const direction: NavigationDirection =
|
||||||
delta < 0 ? NavigationDirection.back : NavigationDirection.forward;
|
delta < 0 ? NavigationDirection.back : NavigationDirection.forward;
|
||||||
|
|
||||||
position = Math.max(
|
position = Math.max(0, Math.min(position + delta, historyStack.length - 1));
|
||||||
0,
|
|
||||||
Math.min(position + delta, historyStack.length - 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldTriggerListeners) {
|
if (shouldTriggerListeners) {
|
||||||
triggerListeners(this.location, from, {
|
triggerListeners(this.location, from, {
|
||||||
@ -459,9 +431,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
|||||||
destroy() {
|
destroy() {
|
||||||
listeners.clear();
|
listeners.clear();
|
||||||
position = 0;
|
position = 0;
|
||||||
historyStack = [
|
historyStack = [{ location: '', state: buildState(null, '', null, false, position) }];
|
||||||
{ location: '', state: buildState(null, '', null, false, position) },
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,7 @@
|
|||||||
export { createRouter } from './router';
|
export { createRouter } from './router';
|
||||||
export {
|
export { createBrowserHistory, createHashHistory, createMemoryHistory } from './history';
|
||||||
createBrowserHistory,
|
|
||||||
createHashHistory,
|
|
||||||
createMemoryHistory,
|
|
||||||
} from './history';
|
|
||||||
|
|
||||||
export type { RouterHistory } from './history';
|
export type { RouterHistory } from './history';
|
||||||
export type { NavigationGuard, NavigationHookAfter } from './guard';
|
export type { NavigationGuard, NavigationHookAfter } from './guard';
|
||||||
export type { Router, RouterOptions } from './router';
|
export type { Router, RouterOptions } from './router';
|
||||||
export type { RouteParams, LocationQuery, RouteRecord } from './types';
|
export * from './types';
|
||||||
export type {
|
|
||||||
RouteLocation,
|
|
||||||
RouteLocationRaw,
|
|
||||||
RouteLocationOptions,
|
|
||||||
} from '@alilc/runtime-shared';
|
|
||||||
|
|||||||
@ -1,76 +1,69 @@
|
|||||||
import { type AnyObject, pick } from '@alilc/runtime-shared';
|
import { type PlainObject, type RawLocation } from '@alilc/renderer-core';
|
||||||
import type { RouteRecord, RouteParams } from './types';
|
import { pick } from 'lodash-es';
|
||||||
import {
|
import { createRouteRecordMatcher, type RouteRecordMatcher } from './utils/record-matcher';
|
||||||
createRouteRecordMatcher,
|
|
||||||
type RouteRecordMatcher,
|
|
||||||
} from './utils/record-matcher';
|
|
||||||
import { type PathParserOptions } from './utils/path-parser';
|
import { type PathParserOptions } from './utils/path-parser';
|
||||||
|
|
||||||
export interface MatcherLocationAsPath {
|
import type { RouteRecord, RouteParams, RouteLocationNormalized } from './types';
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
export interface MatcherLocationAsRelative {
|
|
||||||
params?: Record<string, string | string[]>;
|
|
||||||
}
|
|
||||||
export interface MatcherLocationAsName {
|
|
||||||
name: string;
|
|
||||||
params?: RouteParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
export interface RouteRecordNormalized {
|
||||||
* 匹配器的路由参数
|
|
||||||
*/
|
|
||||||
export type MatcherLocationRaw =
|
|
||||||
| MatcherLocationAsPath
|
|
||||||
| MatcherLocationAsName
|
|
||||||
| MatcherLocationAsRelative;
|
|
||||||
|
|
||||||
export type RouteRecordNormalized = Required<
|
|
||||||
Pick<RouteRecord, 'path' | 'page' | 'children'>
|
|
||||||
> & {
|
|
||||||
/**
|
/**
|
||||||
* {@link RouteRecord.name}
|
* {@link RouteRecord.name}
|
||||||
*/
|
*/
|
||||||
name: string | undefined;
|
name: RouteRecord['name'];
|
||||||
|
path: RouteRecord['path'];
|
||||||
|
page: string;
|
||||||
|
meta: PlainObject;
|
||||||
/**
|
/**
|
||||||
* {@link RouteRecord.redirect}
|
* {@link RouteRecord.redirect}
|
||||||
*/
|
*/
|
||||||
redirect: RouteRecord['redirect'] | undefined;
|
redirect: RouteRecord['redirect'];
|
||||||
};
|
children: RouteRecord[];
|
||||||
|
|
||||||
export interface MatcherLocation {
|
|
||||||
name: string | undefined;
|
|
||||||
path: string;
|
|
||||||
params: RouteParams;
|
|
||||||
matched: RouteRecord[];
|
|
||||||
meta: AnyObject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作为 matcher 解析 location 的关键参数及输出内容
|
||||||
|
*/
|
||||||
|
export type MatcherLocation = Pick<
|
||||||
|
RouteLocationNormalized,
|
||||||
|
'name' | 'path' | 'params' | 'matched' | 'meta'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由匹配器
|
||||||
|
*/
|
||||||
export interface RouterMatcher {
|
export interface RouterMatcher {
|
||||||
|
/**
|
||||||
|
* 新增路由记录
|
||||||
|
*/
|
||||||
addRoute: (record: RouteRecord, parent?: RouteRecordMatcher) => void;
|
addRoute: (record: RouteRecord, parent?: RouteRecordMatcher) => void;
|
||||||
|
/**
|
||||||
|
* 删除路由记录
|
||||||
|
*/
|
||||||
removeRoute: {
|
removeRoute: {
|
||||||
(matcher: RouteRecordMatcher): void;
|
(matcher: RouteRecordMatcher): void;
|
||||||
(name: string): void;
|
(name: string): void;
|
||||||
};
|
};
|
||||||
getRoutes: () => RouteRecordMatcher[];
|
/**
|
||||||
|
* 获取所有的路由匹配对象
|
||||||
|
*/
|
||||||
|
getRecordMatchers: () => RouteRecordMatcher[];
|
||||||
|
/**
|
||||||
|
* 获取路由匹配对象
|
||||||
|
*/
|
||||||
getRecordMatcher: (name: string) => RouteRecordMatcher | undefined;
|
getRecordMatcher: (name: string) => RouteRecordMatcher | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a location.
|
* Resolves a location.
|
||||||
* Gives access to the route record that corresponds to the actual path as well as filling the corresponding params objects
|
* 允许访问与实际路径对应的路由记录并加入相应的 params
|
||||||
*
|
*
|
||||||
* @param location - MatcherLocationRaw to resolve to a url
|
* @param location - MatcherLocationRaw to resolve to a url
|
||||||
* @param currentLocation - MatcherLocation of the current location
|
* @param currentLocation - MatcherLocation of the current location
|
||||||
*/
|
*/
|
||||||
resolve: (
|
resolve: (location: RawLocation, currentLocation: MatcherLocation) => MatcherLocation;
|
||||||
location: MatcherLocationRaw,
|
|
||||||
currentLocation: MatcherLocation
|
|
||||||
) => MatcherLocation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRouterMatcher(
|
export function createRouterMatcher(
|
||||||
records: RouteRecord[],
|
records: RouteRecord[],
|
||||||
globalOptions: PathParserOptions
|
globalOptions: PathParserOptions,
|
||||||
): RouterMatcher {
|
): RouterMatcher {
|
||||||
const matchers: RouteRecordMatcher[] = [];
|
const matchers: RouteRecordMatcher[] = [];
|
||||||
const matcherMap = new Map<string, RouteRecordMatcher>();
|
const matcherMap = new Map<string, RouteRecordMatcher>();
|
||||||
@ -80,7 +73,7 @@ export function createRouterMatcher(
|
|||||||
const options: PathParserOptions = Object.assign(
|
const options: PathParserOptions = Object.assign(
|
||||||
{},
|
{},
|
||||||
globalOptions,
|
globalOptions,
|
||||||
pick(record, ['end', 'sensitive', 'strict'])
|
pick(record, ['end', 'sensitive', 'strict']),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果子路由不是绝对路径,则构建嵌套路由的路径。
|
// 如果子路由不是绝对路径,则构建嵌套路由的路径。
|
||||||
@ -88,10 +81,9 @@ export function createRouterMatcher(
|
|||||||
const { path } = normalizedRecord;
|
const { path } = normalizedRecord;
|
||||||
if (parent && path[0] !== '/') {
|
if (parent && path[0] !== '/') {
|
||||||
const parentPath = parent.record.path;
|
const parentPath = parent.record.path;
|
||||||
const connectingSlash =
|
const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/';
|
||||||
parentPath[parentPath.length - 1] === '/' ? '' : '/';
|
|
||||||
normalizedRecord.path =
|
normalizedRecord.path = parent.record.path + (path && connectingSlash + path);
|
||||||
parent.record.path + (path && connectingSlash + path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
|
const matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
|
||||||
@ -130,18 +122,11 @@ export function createRouterMatcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRoutes() {
|
|
||||||
return matchers;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecordMatcher(name: string) {
|
function getRecordMatcher(name: string) {
|
||||||
return matcherMap.get(name);
|
return matcherMap.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolve(
|
function resolve(location: RawLocation, currentLocation: MatcherLocation): MatcherLocation {
|
||||||
location: MatcherLocationRaw,
|
|
||||||
currentLocation: MatcherLocation
|
|
||||||
): MatcherLocation {
|
|
||||||
let matcher: RouteRecordMatcher | undefined;
|
let matcher: RouteRecordMatcher | undefined;
|
||||||
let params: RouteParams = {};
|
let params: RouteParams = {};
|
||||||
let path: MatcherLocation['path'];
|
let path: MatcherLocation['path'];
|
||||||
@ -151,34 +136,32 @@ export function createRouterMatcher(
|
|||||||
matcher = matcherMap.get(location.name);
|
matcher = matcherMap.get(location.name);
|
||||||
|
|
||||||
if (!matcher) {
|
if (!matcher) {
|
||||||
throw new Error(
|
throw new Error(`Router error: no match for ${JSON.stringify(location)}`);
|
||||||
`Router error: no match for ${JSON.stringify(location)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
name = matcher.record.name;
|
name = matcher.record.name;
|
||||||
// 从当前路径与传入的参数中获取 params
|
// 从当前路径与传入的参数中获取 params
|
||||||
params = Object.assign(
|
params = Object.assign(
|
||||||
paramsFromLocation(
|
paramsFromLocation(
|
||||||
currentLocation.params,
|
currentLocation.params ?? {},
|
||||||
matcher.keys
|
matcher.keys
|
||||||
.filter(k => {
|
.filter((k) => {
|
||||||
return !(k.modifier === '?' || k.modifier === '*');
|
return !(k.modifier === '?' || k.modifier === '*');
|
||||||
})
|
})
|
||||||
.map(k => k.name)
|
.map((k) => k.name),
|
||||||
),
|
),
|
||||||
location.params
|
location.params
|
||||||
? paramsFromLocation(
|
? paramsFromLocation(
|
||||||
location.params,
|
location.params,
|
||||||
matcher.keys.map(k => k.name)
|
matcher.keys.map((k) => k.name),
|
||||||
)
|
)
|
||||||
: {}
|
: {},
|
||||||
);
|
);
|
||||||
// throws if cannot be stringified
|
// throws if cannot be stringified
|
||||||
path = matcher.stringify(params);
|
path = matcher.stringify(params);
|
||||||
} else if ('path' in location) {
|
} else if ('path' in location) {
|
||||||
path = location.path;
|
path = location.path;
|
||||||
matcher = matchers.find(m => m.re.test(path));
|
matcher = matchers.find((m) => m.re.test(path));
|
||||||
|
|
||||||
if (matcher) {
|
if (matcher) {
|
||||||
name = matcher.record.name;
|
name = matcher.record.name;
|
||||||
@ -187,13 +170,11 @@ export function createRouterMatcher(
|
|||||||
} else {
|
} else {
|
||||||
matcher = currentLocation.name
|
matcher = currentLocation.name
|
||||||
? matcherMap.get(currentLocation.name)
|
? matcherMap.get(currentLocation.name)
|
||||||
: matchers.find(m => m.re.test(currentLocation.path));
|
: matchers.find((m) => m.re.test(currentLocation.path));
|
||||||
|
|
||||||
if (!matcher) {
|
if (!matcher) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`no match for ${JSON.stringify(location)}, ${JSON.stringify(
|
`no match for ${JSON.stringify(location)}, ${JSON.stringify(currentLocation)}`,
|
||||||
currentLocation
|
|
||||||
)}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,24 +199,23 @@ export function createRouterMatcher(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
records.forEach(r => addRoute(r));
|
records.forEach((r) => addRoute(r));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resolve,
|
resolve,
|
||||||
|
|
||||||
addRoute,
|
addRoute,
|
||||||
removeRoute,
|
removeRoute,
|
||||||
getRoutes,
|
|
||||||
|
getRecordMatchers() {
|
||||||
|
return matchers;
|
||||||
|
},
|
||||||
getRecordMatcher,
|
getRecordMatcher,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function paramsFromLocation(
|
function paramsFromLocation(params: RouteParams, keys: (string | number)[]): RouteParams {
|
||||||
params: RouteParams,
|
|
||||||
keys: (string | number)[]
|
|
||||||
): RouteParams {
|
|
||||||
const newParams = {} as RouteParams;
|
const newParams = {} as RouteParams;
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (key in params) newParams[key] = params[key];
|
if (key in params) newParams[key] = params[key];
|
||||||
}
|
}
|
||||||
@ -243,14 +223,13 @@ function paramsFromLocation(
|
|||||||
return newParams;
|
return newParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeRouteRecord(
|
export function normalizeRouteRecord(record: RouteRecord): RouteRecordNormalized {
|
||||||
record: RouteRecord
|
|
||||||
): RouteRecordNormalized {
|
|
||||||
return {
|
return {
|
||||||
path: record.path,
|
path: record.path,
|
||||||
redirect: record.redirect,
|
redirect: record.redirect,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
page: record.page,
|
page: record.page || '',
|
||||||
|
meta: record['meta'] || {},
|
||||||
children: record.children || [],
|
children: record.children || [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
type RouterSchema,
|
type RouterApi,
|
||||||
useCallbacks,
|
type RouterConfig,
|
||||||
type RouteLocation,
|
type RouteLocation,
|
||||||
type RouteLocationRaw,
|
useEvent,
|
||||||
type RouteLocationOptions,
|
type RawRouteLocation,
|
||||||
noop,
|
type RawLocationOptions,
|
||||||
} from '@alilc/runtime-shared';
|
} from '@alilc/renderer-core';
|
||||||
import {
|
import {
|
||||||
createBrowserHistory,
|
createBrowserHistory,
|
||||||
createHashHistory,
|
createHashHistory,
|
||||||
@ -13,46 +13,42 @@ import {
|
|||||||
type RouterHistory,
|
type RouterHistory,
|
||||||
type HistoryState,
|
type HistoryState,
|
||||||
} from './history';
|
} from './history';
|
||||||
import { createRouterMatcher, type MatcherLocationRaw } from './matcher';
|
import { createRouterMatcher } from './matcher';
|
||||||
import { type PathParserOptions } from './utils/path-parser';
|
import { type PathParserOptions } from './utils/path-parser';
|
||||||
import { parseURL, stringifyURL } from './utils/url';
|
import { parseURL, stringifyURL } from './utils/url';
|
||||||
import { normalizeQuery } from './utils/query';
|
|
||||||
import { isSameRouteLocation } from './utils/helper';
|
import { isSameRouteLocation } from './utils/helper';
|
||||||
import type { RouteParams, RouteRecord } from './types';
|
import type { RouteParams, RouteRecord, RouteLocationNormalized } from './types';
|
||||||
import {
|
import { type NavigationHookAfter, type NavigationGuard, guardToPromiseFn } from './guard';
|
||||||
type NavigationHookAfter,
|
|
||||||
type NavigationGuard,
|
|
||||||
guardToPromiseFn,
|
|
||||||
} from './guard';
|
|
||||||
|
|
||||||
export interface Router {
|
export interface RouterOptions extends RouterConfig, PathParserOptions {
|
||||||
|
routes: RouteRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Router extends RouterApi {
|
||||||
readonly options: RouterOptions;
|
readonly options: RouterOptions;
|
||||||
readonly history: RouterHistory;
|
readonly history: RouterHistory;
|
||||||
|
|
||||||
getCurrentRoute: () => RouteLocation;
|
getCurrentLocation(): RouteLocationNormalized;
|
||||||
|
|
||||||
addRoute: {
|
resolve(
|
||||||
(parentName: string, route: RouteRecord): void;
|
rawLocation: RawRouteLocation,
|
||||||
(route: RouteRecord): void;
|
currentLocation?: RouteLocationNormalized,
|
||||||
};
|
): RouteLocationNormalized;
|
||||||
|
|
||||||
|
addRoute(route: RouteRecord): void;
|
||||||
removeRoute(name: string): void;
|
removeRoute(name: string): void;
|
||||||
hasRoute(name: string): boolean;
|
|
||||||
getRoutes(): RouteRecord[];
|
getRoutes(): RouteRecord[];
|
||||||
|
hasRoute(name: string): boolean;
|
||||||
push: (to: RouteLocationRaw) => void;
|
|
||||||
replace: (to: RouteLocationRaw) => void;
|
|
||||||
|
|
||||||
beforeRouteLeave: (fn: NavigationGuard) => () => void;
|
beforeRouteLeave: (fn: NavigationGuard) => () => void;
|
||||||
afterRouteChange: (fn: NavigationHookAfter) => () => void;
|
afterRouteChange: (fn: NavigationHookAfter) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouterOptions = RouterSchema & PathParserOptions;
|
const START_LOCATION: RouteLocationNormalized = {
|
||||||
|
|
||||||
const START_LOCATION_NORMALIZED: RouteLocation = {
|
|
||||||
path: '/',
|
path: '/',
|
||||||
name: undefined,
|
name: undefined,
|
||||||
params: {},
|
params: {},
|
||||||
query: {},
|
searchParams: undefined,
|
||||||
hash: '',
|
hash: '',
|
||||||
fullPath: '/',
|
fullPath: '/',
|
||||||
matched: [],
|
matched: [],
|
||||||
@ -60,54 +56,50 @@ const START_LOCATION_NORMALIZED: RouteLocation = {
|
|||||||
redirectedFrom: undefined,
|
redirectedFrom: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createRouter(options: RouterOptions): Router {
|
const defaultRouterOptions: RouterOptions = {
|
||||||
const {
|
historyMode: 'browser',
|
||||||
baseName = '/',
|
baseName: '/',
|
||||||
historyMode = 'browser',
|
routes: [],
|
||||||
routes = [],
|
};
|
||||||
...globalOptions
|
|
||||||
} = options;
|
export function createRouter(options: RouterOptions = defaultRouterOptions): Router {
|
||||||
|
const { baseName = '/', historyMode = 'browser', routes = [], ...globalOptions } = options;
|
||||||
const matcher = createRouterMatcher(routes, globalOptions);
|
const matcher = createRouterMatcher(routes, globalOptions);
|
||||||
const routerHistory =
|
const routerHistory =
|
||||||
historyMode === 'hash'
|
historyMode === 'hash'
|
||||||
? createHashHistory(baseName)
|
? createHashHistory(baseName)
|
||||||
: historyMode === 'memory'
|
: historyMode === 'memory'
|
||||||
? createMemoryHistory(baseName)
|
? createMemoryHistory(baseName)
|
||||||
: createBrowserHistory(baseName);
|
: createBrowserHistory(baseName);
|
||||||
|
|
||||||
const beforeGuards = useCallbacks<NavigationGuard>();
|
const beforeGuards = useEvent<NavigationGuard>();
|
||||||
const afterGuards = useCallbacks<NavigationHookAfter>();
|
const afterGuards = useEvent<NavigationHookAfter>();
|
||||||
|
|
||||||
let currentRoute: RouteLocation = START_LOCATION_NORMALIZED;
|
let currentLocation: RouteLocationNormalized = START_LOCATION;
|
||||||
let pendingLocation = currentRoute;
|
let pendingLocation = currentLocation;
|
||||||
|
|
||||||
function resolve(
|
function resolve(
|
||||||
rawLocation: RouteLocationRaw,
|
rawLocation: RawRouteLocation,
|
||||||
currentLocation?: RouteLocation
|
currentLocation?: RouteLocationNormalized,
|
||||||
): RouteLocation & {
|
): RouteLocationNormalized & {
|
||||||
href: string;
|
href: string;
|
||||||
} {
|
} {
|
||||||
currentLocation = Object.assign({}, currentLocation || currentRoute);
|
currentLocation = Object.assign({}, currentLocation || currentLocation);
|
||||||
|
|
||||||
if (typeof rawLocation === 'string') {
|
if (typeof rawLocation === 'string') {
|
||||||
const locationNormalized = parseURL(rawLocation);
|
const locationNormalized = parseURL(rawLocation);
|
||||||
|
const matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
|
||||||
const matchedRoute = matcher.resolve(
|
|
||||||
{ path: locationNormalized.path },
|
|
||||||
currentLocation
|
|
||||||
);
|
|
||||||
|
|
||||||
const href = routerHistory.createHref(locationNormalized.fullPath);
|
const href = routerHistory.createHref(locationNormalized.fullPath);
|
||||||
|
|
||||||
return Object.assign(locationNormalized, matchedRoute, {
|
return Object.assign(locationNormalized, matchedRoute, {
|
||||||
query: locationNormalized.query as any,
|
searchParams: locationNormalized.searchParams,
|
||||||
hash: decodeURIComponent(locationNormalized.hash),
|
hash: decodeURIComponent(locationNormalized.hash),
|
||||||
redirectedFrom: undefined,
|
redirectedFrom: undefined,
|
||||||
href,
|
href,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let matcherLocation: MatcherLocationRaw;
|
let matcherLocation: RawRouteLocation;
|
||||||
|
|
||||||
if ('path' in rawLocation) {
|
if ('path' in rawLocation) {
|
||||||
matcherLocation = { ...rawLocation };
|
matcherLocation = { ...rawLocation };
|
||||||
@ -140,13 +132,13 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
{
|
{
|
||||||
fullPath,
|
fullPath,
|
||||||
hash,
|
hash,
|
||||||
query: normalizeQuery(rawLocation.query) as any,
|
searchParams: rawLocation.searchParams,
|
||||||
},
|
},
|
||||||
matchedRoute,
|
matchedRoute,
|
||||||
{
|
{
|
||||||
redirectedFrom: undefined,
|
redirectedFrom: undefined,
|
||||||
href,
|
href,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,34 +161,33 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getRoutes() {
|
function getRoutes() {
|
||||||
return matcher.getRoutes().map(item => item.record);
|
return matcher.getRecordMatchers().map((item) => item.record);
|
||||||
}
|
}
|
||||||
function hasRoute(name: string) {
|
function hasRoute(name: string) {
|
||||||
return !!matcher.getRecordMatcher(name);
|
return !!matcher.getRecordMatcher(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function push(to: RouteLocationRaw) {
|
function push(to: RawRouteLocation) {
|
||||||
return pushOrRedirect(to);
|
return pushOrRedirect(to);
|
||||||
}
|
}
|
||||||
function replace(to: RouteLocationRaw) {
|
function replace(to: RawRouteLocation) {
|
||||||
return pushOrRedirect({ ...locationAsObject(to), replace: true });
|
return pushOrRedirect({ ...locationAsObject(to) }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function locationAsObject(
|
function locationAsObject(
|
||||||
to: RouteLocationRaw | RouteLocation
|
to: RawRouteLocation | RouteLocation,
|
||||||
): Exclude<RouteLocationRaw, string> | RouteLocation {
|
): Exclude<RawRouteLocation, string> | RouteLocation {
|
||||||
return typeof to === 'string' ? parseURL(to) : { ...to };
|
return typeof to === 'string' ? parseURL(to) : { ...to };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pushOrRedirect(
|
async function pushOrRedirect(
|
||||||
to: RouteLocationRaw | RouteLocation,
|
to: RawRouteLocation | RouteLocation,
|
||||||
redirectedFrom?: RouteLocation
|
replace = false,
|
||||||
|
redirectedFrom?: RouteLocation,
|
||||||
) {
|
) {
|
||||||
const targetLocation = (pendingLocation = resolve(to));
|
const targetLocation = (pendingLocation = resolve(to));
|
||||||
const from = currentRoute;
|
const from = currentLocation;
|
||||||
const data: HistoryState | undefined = (to as RouteLocationOptions).state;
|
const data: HistoryState | undefined = (to as RawLocationOptions).state;
|
||||||
const force: boolean | undefined = (to as RouteLocationOptions).force;
|
|
||||||
const replace = (to as RouteLocationOptions).replace === true;
|
|
||||||
|
|
||||||
const shouldRedirect = getRedirectRecordIfShould(targetLocation);
|
const shouldRedirect = getRedirectRecordIfShould(targetLocation);
|
||||||
if (shouldRedirect) {
|
if (shouldRedirect) {
|
||||||
@ -204,20 +195,17 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
{
|
{
|
||||||
...shouldRedirect,
|
...shouldRedirect,
|
||||||
state: Object.assign({}, data, shouldRedirect.state),
|
state: Object.assign({}, data, shouldRedirect.state),
|
||||||
force,
|
|
||||||
replace,
|
|
||||||
},
|
},
|
||||||
redirectedFrom || targetLocation
|
replace,
|
||||||
|
redirectedFrom || targetLocation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const toLocation = targetLocation as RouteLocation;
|
const toLocation = targetLocation as RouteLocationNormalized;
|
||||||
toLocation.redirectedFrom = redirectedFrom;
|
toLocation.redirectedFrom = redirectedFrom;
|
||||||
|
|
||||||
if (!force && isSameRouteLocation(from, toLocation)) {
|
if (isSameRouteLocation(from, toLocation)) {
|
||||||
throw Error(
|
throw Error('路由错误:重复请求' + JSON.stringify({ to: toLocation, from }));
|
||||||
'路由错误:重复请求' + JSON.stringify({ to: toLocation, from })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return navigateTriggerBeforeGuards(toLocation, from)
|
return navigateTriggerBeforeGuards(toLocation, from)
|
||||||
@ -231,21 +219,21 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRedirectRecordIfShould(to: RouteLocation) {
|
function getRedirectRecordIfShould(
|
||||||
|
to: RouteLocationNormalized,
|
||||||
|
): Exclude<RawRouteLocation, string> | undefined {
|
||||||
const lastMatched = to.matched[to.matched.length - 1];
|
const lastMatched = to.matched[to.matched.length - 1];
|
||||||
|
|
||||||
if (lastMatched?.redirect) {
|
if (lastMatched?.redirect) {
|
||||||
const { redirect } = lastMatched;
|
const { redirect } = lastMatched;
|
||||||
let newTargetLocation =
|
let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect;
|
||||||
typeof redirect === 'function' ? redirect(to) : redirect;
|
|
||||||
|
|
||||||
if (typeof newTargetLocation === 'string') {
|
if (typeof newTargetLocation === 'string') {
|
||||||
newTargetLocation =
|
newTargetLocation =
|
||||||
newTargetLocation.includes('?') || newTargetLocation.includes('#')
|
newTargetLocation.includes('?') || newTargetLocation.includes('#')
|
||||||
? locationAsObject(newTargetLocation)
|
? locationAsObject(newTargetLocation)
|
||||||
: { path: newTargetLocation };
|
: { path: newTargetLocation };
|
||||||
// @ts-expect-error 强制清空参数
|
(newTargetLocation as any).params = {};
|
||||||
newTargetLocation.params = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!('path' in newTargetLocation) && !('name' in newTargetLocation)) {
|
if (!('path' in newTargetLocation) && !('name' in newTargetLocation)) {
|
||||||
@ -254,27 +242,25 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
|
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
{
|
{
|
||||||
query: to.query,
|
searchParams: to.searchParams,
|
||||||
hash: to.hash,
|
hash: to.hash,
|
||||||
// path 存在的时候 清空 params
|
// path 存在的时候 清空 params
|
||||||
params: 'path' in newTargetLocation ? {} : to.params,
|
params: 'path' in newTargetLocation ? {} : to.params,
|
||||||
},
|
},
|
||||||
newTargetLocation
|
newTargetLocation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigateTriggerBeforeGuards(
|
async function navigateTriggerBeforeGuards(
|
||||||
to: RouteLocation,
|
to: RouteLocationNormalized,
|
||||||
from: RouteLocation
|
from: RouteLocationNormalized,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
let guards: ((...args: any[]) => Promise<any>)[] = [];
|
let guards: ((...args: any[]) => Promise<any>)[] = [];
|
||||||
|
|
||||||
const canceledNavigationCheck = async (): Promise<any> => {
|
const canceledNavigationCheck = async (): Promise<any> => {
|
||||||
if (pendingLocation !== to) {
|
if (pendingLocation !== to) {
|
||||||
throw Error(
|
throw Error(`路由错误:重复导航,from: ${from.fullPath}, to: ${to.fullPath}`);
|
||||||
`路由错误:重复导航,from: ${from.fullPath}, to: ${to.fullPath}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
@ -288,31 +274,26 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
}
|
}
|
||||||
if (beforeGuardsList.length > 0) guards.push(canceledNavigationCheck);
|
if (beforeGuardsList.length > 0) guards.push(canceledNavigationCheck);
|
||||||
|
|
||||||
return guards.reduce(
|
return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
|
||||||
(promise, guard) => promise.then(() => guard()),
|
|
||||||
Promise.resolve()
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalizeNavigation(
|
function finalizeNavigation(
|
||||||
toLocation: RouteLocation,
|
toLocation: RouteLocationNormalized,
|
||||||
from: RouteLocation,
|
from: RouteLocationNormalized,
|
||||||
isPush: boolean,
|
isPush: boolean,
|
||||||
replace?: boolean,
|
replace?: boolean,
|
||||||
data?: HistoryState
|
data?: HistoryState,
|
||||||
) {
|
) {
|
||||||
// 重复导航
|
// 重复导航
|
||||||
if (pendingLocation !== toLocation) {
|
if (pendingLocation !== toLocation) {
|
||||||
throw Error(
|
throw Error(`路由错误:重复导航,from: ${from.fullPath}, to: ${toLocation.fullPath}`);
|
||||||
`路由错误:重复导航,from: ${from.fullPath}, to: ${toLocation.fullPath}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果不是第一次启动的话 只需要考虑 push
|
// 如果不是第一次启动的话 只需要考虑 push
|
||||||
const isFirstNavigation = from === START_LOCATION_NORMALIZED;
|
const isFirstNavigation = from === START_LOCATION;
|
||||||
|
|
||||||
if (isPush) {
|
if (isPush) {
|
||||||
if (replace || isFirstNavigation) {
|
if (replace || isFirstNavigation) {
|
||||||
@ -322,7 +303,7 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentRoute = toLocation;
|
currentLocation = toLocation;
|
||||||
// markAsReady();
|
// markAsReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,18 +316,15 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
// 判断是否需要重定向
|
// 判断是否需要重定向
|
||||||
const shouldRedirect = getRedirectRecordIfShould(toLocation);
|
const shouldRedirect = getRedirectRecordIfShould(toLocation);
|
||||||
if (shouldRedirect) {
|
if (shouldRedirect) {
|
||||||
return pushOrRedirect(
|
return pushOrRedirect(shouldRedirect, true, toLocation).catch(() => {});
|
||||||
Object.assign(shouldRedirect, { replace: true }),
|
|
||||||
toLocation
|
|
||||||
).catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingLocation = toLocation;
|
pendingLocation = toLocation;
|
||||||
const from = currentRoute;
|
const from = currentLocation;
|
||||||
|
|
||||||
// 触发路由守卫
|
// 触发路由守卫
|
||||||
navigateTriggerBeforeGuards(toLocation, from)
|
navigateTriggerBeforeGuards(toLocation, from)
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
if (info.delta) {
|
if (info.delta) {
|
||||||
routerHistory.go(-info.delta, false);
|
routerHistory.go(-info.delta, false);
|
||||||
}
|
}
|
||||||
@ -366,25 +344,30 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
guard(toLocation, from);
|
guard(toLocation, from);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(noop);
|
.catch(() => {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// init
|
// init
|
||||||
setupListeners();
|
setupListeners();
|
||||||
if (currentRoute === START_LOCATION_NORMALIZED) {
|
if (currentLocation === START_LOCATION) {
|
||||||
push(routerHistory.location).catch(err => {
|
push(routerHistory.location).catch((err) => {
|
||||||
console.warn('Unexpected error when starting the router:', err);
|
console.warn('Unexpected error when starting the router:', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const go = (delta: number) => routerHistory.go(delta);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options,
|
get options() {
|
||||||
|
return options;
|
||||||
|
},
|
||||||
get history() {
|
get history() {
|
||||||
return routerHistory;
|
return routerHistory;
|
||||||
},
|
},
|
||||||
|
getCurrentLocation: () => currentLocation,
|
||||||
|
|
||||||
getCurrentRoute: () => currentRoute,
|
resolve,
|
||||||
addRoute,
|
addRoute,
|
||||||
removeRoute,
|
removeRoute,
|
||||||
getRoutes,
|
getRoutes,
|
||||||
@ -392,6 +375,9 @@ export function createRouter(options: RouterOptions): Router {
|
|||||||
|
|
||||||
push,
|
push,
|
||||||
replace,
|
replace,
|
||||||
|
back: () => go(-1),
|
||||||
|
forward: () => go(1),
|
||||||
|
go,
|
||||||
|
|
||||||
beforeRouteLeave: beforeGuards.add,
|
beforeRouteLeave: beforeGuards.add,
|
||||||
afterRouteChange: afterGuards.add,
|
afterRouteChange: afterGuards.add,
|
||||||
|
|||||||
@ -1,9 +1,22 @@
|
|||||||
import { type RouteSchema } from '@alilc/runtime-shared';
|
import type {
|
||||||
import { type ParsedQs } from 'qs';
|
RouteRecord as RouterRecordSpec,
|
||||||
import { type PathParserOptions } from './utils/path-parser';
|
RouteLocation,
|
||||||
|
PlainObject,
|
||||||
|
RawRouteLocation,
|
||||||
|
} from '@alilc/renderer-core';
|
||||||
|
import type { PathParserOptions } from './utils/path-parser';
|
||||||
|
|
||||||
export type RouteRecord = RouteSchema & PathParserOptions;
|
export interface RouteRecord extends RouterRecordSpec, PathParserOptions {
|
||||||
|
meta?: PlainObject;
|
||||||
|
redirect?:
|
||||||
|
| string
|
||||||
|
| RawRouteLocation
|
||||||
|
| ((to: RouteLocationNormalized) => string | RawRouteLocation);
|
||||||
|
children?: RouteRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
export type LocationQuery = ParsedQs;
|
export interface RouteLocationNormalized extends RouteLocation {
|
||||||
|
matched: RouteRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
export type RouteParams = Record<string, string | string[]>;
|
export type RouteParams = Record<string, string | string[]>;
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
import {
|
import type { RawRouteLocation } from '@alilc/renderer-core';
|
||||||
type RouteLocation,
|
import type { RouteLocationNormalized } from '../types';
|
||||||
type RouteLocationRaw,
|
|
||||||
} from '@alilc/runtime-shared';
|
|
||||||
|
|
||||||
export function isRouteLocation(route: any): route is RouteLocationRaw {
|
export function isRouteLocation(route: any): route is RawRouteLocation {
|
||||||
return typeof route === 'string' || (route && typeof route === 'object');
|
return typeof route === 'string' || (route && typeof route === 'object');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSameRouteLocation(
|
export function isSameRouteLocation(
|
||||||
a: RouteLocation,
|
a: RouteLocationNormalized,
|
||||||
b: RouteLocation
|
b: RouteLocationNormalized,
|
||||||
): boolean {
|
): boolean {
|
||||||
const aLastIndex = a.matched.length - 1;
|
const aLastIndex = a.matched.length - 1;
|
||||||
const bLastIndex = b.matched.length - 1;
|
const bLastIndex = b.matched.length - 1;
|
||||||
@ -19,15 +17,18 @@ export function isSameRouteLocation(
|
|||||||
aLastIndex === bLastIndex &&
|
aLastIndex === bLastIndex &&
|
||||||
a.matched[aLastIndex] === b.matched[bLastIndex] &&
|
a.matched[aLastIndex] === b.matched[bLastIndex] &&
|
||||||
isSameRouteLocationParams(a.params, b.params) &&
|
isSameRouteLocationParams(a.params, b.params) &&
|
||||||
a.query?.toString() === b.query?.toString() &&
|
a.searchParams?.toString() === b.searchParams?.toString() &&
|
||||||
a.hash === b.hash
|
a.hash === b.hash
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSameRouteLocationParams(
|
export function isSameRouteLocationParams(
|
||||||
a: RouteLocation['params'],
|
a: RouteLocationNormalized['params'],
|
||||||
b: RouteLocation['params']
|
b: RouteLocationNormalized['params'],
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (!a && !b) return true;
|
||||||
|
if (!a || !b) return false;
|
||||||
|
|
||||||
if (Object.keys(a).length !== Object.keys(b).length) return false;
|
if (Object.keys(a).length !== Object.keys(b).length) return false;
|
||||||
|
|
||||||
for (const key in a) {
|
for (const key in a) {
|
||||||
@ -38,14 +39,14 @@ export function isSameRouteLocationParams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSameRouteLocationParamsValue(
|
function isSameRouteLocationParamsValue(
|
||||||
a: string | readonly string[],
|
a: undefined | string | string[],
|
||||||
b: string | readonly string[]
|
b: undefined | string | string[],
|
||||||
): boolean {
|
): boolean {
|
||||||
return Array.isArray(a)
|
return Array.isArray(a)
|
||||||
? isEquivalentArray(a, b)
|
? isEquivalentArray(a, b)
|
||||||
: Array.isArray(b)
|
: Array.isArray(b)
|
||||||
? isEquivalentArray(b, a)
|
? isEquivalentArray(b, a)
|
||||||
: a === b;
|
: a === b;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEquivalentArray<T>(a: readonly T[], b: readonly T[] | T): boolean {
|
function isEquivalentArray<T>(a: readonly T[], b: readonly T[] | T): boolean {
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
import { type AnyObject } from '@alilc/runtime-shared';
|
|
||||||
import type { LocationQuery } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* casting numbers into strings, removing keys with an undefined value and replacing
|
|
||||||
* undefined with null in arrays
|
|
||||||
*
|
|
||||||
* 将数字转换为字符,去除值为 undefined 的 keys,将数组中的 undefined 转换为 null
|
|
||||||
*
|
|
||||||
* @param query - query object to normalize
|
|
||||||
* @returns a normalized query object
|
|
||||||
*/
|
|
||||||
export function normalizeQuery(query: AnyObject | undefined): LocationQuery {
|
|
||||||
const normalizedQuery: AnyObject = {};
|
|
||||||
|
|
||||||
for (const key in query) {
|
|
||||||
const value = query[key];
|
|
||||||
if (value !== undefined) {
|
|
||||||
normalizedQuery[key] = Array.isArray(value)
|
|
||||||
? value.map(v => (v == null ? null : '' + v))
|
|
||||||
: value == null
|
|
||||||
? value
|
|
||||||
: '' + value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedQuery;
|
|
||||||
}
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { type AnyObject } from '@alilc/runtime-shared';
|
/**
|
||||||
import { parse, stringify } from 'qs';
|
* todo: replace to URL API
|
||||||
import { type LocationQuery } from '../types';
|
*/
|
||||||
|
|
||||||
export function parseURL(location: string) {
|
export function parseURL(location: string) {
|
||||||
let path = '';
|
let path = '';
|
||||||
let query: LocationQuery = {};
|
let searchParams: URLSearchParams | undefined;
|
||||||
let searchString = '';
|
let searchString = '';
|
||||||
let hash = '';
|
let hash = '';
|
||||||
|
|
||||||
@ -16,12 +16,9 @@ export function parseURL(location: string) {
|
|||||||
|
|
||||||
if (searchPos > -1) {
|
if (searchPos > -1) {
|
||||||
path = location.slice(0, searchPos);
|
path = location.slice(0, searchPos);
|
||||||
searchString = location.slice(
|
searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
|
||||||
searchPos + 1,
|
|
||||||
hashPos > -1 ? hashPos : location.length
|
|
||||||
);
|
|
||||||
|
|
||||||
query = parse(searchString);
|
searchParams = new URLSearchParams(searchString);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hashPos > -1) {
|
if (hashPos > -1) {
|
||||||
@ -35,16 +32,16 @@ export function parseURL(location: string) {
|
|||||||
return {
|
return {
|
||||||
fullPath: path + (searchString && '?') + searchString + hash,
|
fullPath: path + (searchString && '?') + searchString + hash,
|
||||||
path,
|
path,
|
||||||
query,
|
searchParams,
|
||||||
hash,
|
hash,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyURL(location: {
|
export function stringifyURL(location: {
|
||||||
path: string;
|
path: string;
|
||||||
query?: AnyObject;
|
searchParams?: URLSearchParams;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
}): string {
|
}): string {
|
||||||
const query: string = location.query ? stringify(location.query) : '';
|
const searchStr = location.searchParams ? location.searchParams.toString() : '';
|
||||||
return location.path + (query && '?') + query + (location.hash || '');
|
return location.path + (searchStr && '?') + searchStr + (location.hash || '');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"outDir": "dist",
|
||||||
}
|
"paths": {
|
||||||
|
"@alilc/*": ["runtime/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"lib": ["es2015", "dom"],
|
"lib": ["DOM", "ESNext", "DOM.Iterable"],
|
||||||
// Target latest version of ECMAScript.
|
// Target latest version of ECMAScript.
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
// Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
|
// Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user