mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-02-06 21:45:38 +00:00
feat: add router
This commit is contained in:
parent
fb5de6441d
commit
03f7c76284
@ -1513,7 +1513,6 @@ webpack.config.js # 项目工程配置,包含插件配置及自定义 webpack
|
||||
| -------------- | ---------------------------------- | ------ | ------ | ------ | ---------------------------------------------- |
|
||||
| path | 当前解析后的路径 | String | - | - | 必填 |
|
||||
| hash | 当前路径的 hash 值,以 # 开头 | String | - | - | 必填 |
|
||||
| href | 当前的全部路径 | String | - | - | 必填 |
|
||||
| params | 匹配到的路径参数 | Object | - | - | 必填 |
|
||||
| query | 当前的路径 query 对象 | Object | - | - | 必填,代表当前地址的 search 属性的对象 |
|
||||
| name | 匹配到的路由记录名 | String | - | - | 选填 |
|
||||
|
||||
@ -6,8 +6,10 @@
|
||||
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
||||
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
||||
"license": "MIT",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "",
|
||||
"build": "tsc",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
|
||||
@ -22,7 +22,7 @@ export interface AppBase {
|
||||
*/
|
||||
export interface AppContext {
|
||||
schema: AppSchema;
|
||||
config: Map<string, any>;
|
||||
config: PlainObject;
|
||||
appScope: CodeScope;
|
||||
packageManager: PackageManager;
|
||||
boosts: AppBoostsManager;
|
||||
@ -35,14 +35,14 @@ type AppCreator<O, T extends AppBase> = (
|
||||
|
||||
export type App<T extends AppBase = AppBase> = {
|
||||
schema: Project;
|
||||
config: Map<string, any>;
|
||||
config: PlainObject;
|
||||
readonly boosts: AppBoosts;
|
||||
|
||||
use(plugin: Plugin): Promise<void>;
|
||||
} & T;
|
||||
|
||||
/**
|
||||
* 创建应用
|
||||
* 创建 createApp 的辅助函数
|
||||
* @param schema
|
||||
* @param options
|
||||
* @returns
|
||||
@ -55,9 +55,9 @@ export function createAppFunction<O extends AppOptionsBase, T extends AppBase =
|
||||
}
|
||||
|
||||
return async (options) => {
|
||||
const { schema, appScopeValue = {} } = options;
|
||||
const { schema, appScopeValue } = options;
|
||||
const appSchema = createAppSchema(schema);
|
||||
const appConfig = new Map<string, any>();
|
||||
const appConfig = {};
|
||||
const packageManager = createPackageManager();
|
||||
const appScope = createScope({
|
||||
...appScopeValue,
|
||||
|
||||
@ -1,136 +1,42 @@
|
||||
import { CreateContainerOptions, createContainer } from '../container';
|
||||
import { createCodeRuntime, createScope } from '../code-runtime';
|
||||
import { throwRuntimeError } from '../utils/error';
|
||||
import { validateContainerSchema } from '../validator/schema';
|
||||
import { type CreateContainerOptions, createContainer, type Container } from '../container';
|
||||
import type { PlainObject, InstanceStateApi } from '../types';
|
||||
|
||||
export interface ComponentOptionsBase<C> {
|
||||
componentsTree: RootSchema;
|
||||
componentsRecord: Record<string, C | Package>;
|
||||
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;
|
||||
export type CreateComponentBaseOptions<T extends string> = Omit<
|
||||
CreateContainerOptions<T>,
|
||||
'stateCreator'
|
||||
>;
|
||||
|
||||
/**
|
||||
* 创建 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) => {
|
||||
const finalOptions = Object.assign({}, defaultOptions, componentOptions);
|
||||
const { supCodeScope, initScopeValue = {}, dataSourceCreator } = finalOptions;
|
||||
const {
|
||||
supCodeScope,
|
||||
initScopeValue = {},
|
||||
dataSourceCreator,
|
||||
componentsTree,
|
||||
} = componentOptions;
|
||||
|
||||
const codeRuntimeScope =
|
||||
supCodeScope?.createSubScope(initScopeValue) ?? createScope(initScopeValue);
|
||||
const codeRuntime = createCodeRuntime(codeRuntimeScope);
|
||||
|
||||
const container: Container = {
|
||||
get codeScope() {
|
||||
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;
|
||||
},
|
||||
};
|
||||
const container = createContainer<InstanceT, LifeCycleNameT>({
|
||||
supCodeScope,
|
||||
initScopeValue,
|
||||
stateCreator,
|
||||
dataSourceCreator,
|
||||
componentsTree,
|
||||
});
|
||||
|
||||
return componentCreator(container, componentOptions);
|
||||
};
|
||||
|
||||
@ -76,17 +76,20 @@ export function createCodeRuntime(scopeOrValue: PlainObject = {}): CodeRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CodeScope {
|
||||
readonly value: PlainObject;
|
||||
export interface CodeScope<T extends PlainObject = PlainObject, K extends keyof T = keyof T> {
|
||||
readonly value: T;
|
||||
|
||||
inject(name: string, value: any, force?: boolean): void;
|
||||
setValue(value: PlainObject, replace?: boolean): void;
|
||||
createSubScope(initValue?: PlainObject): CodeScope;
|
||||
inject(name: K, value: T[K], force?: boolean): void;
|
||||
setValue(value: T, replace?: boolean): void;
|
||||
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 proxyValue = new Proxy(Object.create(null), {
|
||||
|
||||
const proxyValue: T = new Proxy(Object.create(null), {
|
||||
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 {
|
||||
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 = {
|
||||
const scope: CodeScope<T, K> = {
|
||||
get value() {
|
||||
// dev return value
|
||||
return proxyValue;
|
||||
},
|
||||
inject,
|
||||
inject(name, value, force = false): void {
|
||||
if (innerScope.value[name] && !force) {
|
||||
return;
|
||||
}
|
||||
innerScope.value[name] = value;
|
||||
},
|
||||
setValue(value, replace = false) {
|
||||
if (replace) {
|
||||
innerScope.value = { ...value };
|
||||
@ -134,7 +125,13 @@ export function createScope(initValue: PlainObject = {}): CodeScope {
|
||||
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 });
|
||||
|
||||
@ -4,6 +4,7 @@ import type {
|
||||
ComponentTree,
|
||||
InstanceDataSourceApi,
|
||||
InstanceStateApi,
|
||||
NodeType,
|
||||
} from './types';
|
||||
import { type CodeScope, type CodeRuntime, createCodeRuntime, createScope } from './code-runtime';
|
||||
import { isJSFunction } from './utils/type-guard';
|
||||
@ -48,7 +49,13 @@ export interface CreateContainerOptions<LifeCycleNameT extends string> {
|
||||
export function createContainer<InstanceT, LifeCycleNameT extends string>(
|
||||
options: CreateContainerOptions<LifeCycleNameT>,
|
||||
): Container<InstanceT, LifeCycleNameT> {
|
||||
const { componentsTree, supCodeScope, initScopeValue, stateCreator, dataSourceCreator } = options;
|
||||
const {
|
||||
componentsTree,
|
||||
supCodeScope,
|
||||
initScopeValue = {},
|
||||
stateCreator,
|
||||
dataSourceCreator,
|
||||
} = options;
|
||||
|
||||
validContainerSchema(componentsTree);
|
||||
|
||||
@ -151,7 +158,6 @@ export function createContainer<InstanceT, LifeCycleNameT extends string>(
|
||||
|
||||
createWidgets<Element>() {
|
||||
if (!componentsTree.children) return [];
|
||||
|
||||
return componentsTree.children.map((item) => createWidget<Element>(item));
|
||||
},
|
||||
};
|
||||
|
||||
@ -5,11 +5,14 @@ export { createCodeRuntime, createScope } from './code-runtime';
|
||||
export { definePlugin } from './plugin';
|
||||
export { createWidget } from './widget';
|
||||
export { createContainer } from './container';
|
||||
export { createHookStore, useEvent } from './utils/hook';
|
||||
export * from './utils/type-guard';
|
||||
export * from './utils/value';
|
||||
export * from './widget';
|
||||
|
||||
/* --------------- types ---------------- */
|
||||
export * from './types';
|
||||
export type { CodeRuntime, CodeScope } from './code-runtime';
|
||||
export type { Plugin, PluginSetupContext } from './plugin';
|
||||
export type { PackageManager, PackageLoader } from './package';
|
||||
export type { Container } from './container';
|
||||
export type { Widget, TextWidget, ComponentWidget } from './widget';
|
||||
export type { Container, CreateContainerOptions } from './container';
|
||||
|
||||
@ -24,9 +24,11 @@ export interface PackageManager {
|
||||
/** 解析组件映射 */
|
||||
resolveComponentMaps(componentMaps: ComponentMap[]): void;
|
||||
/** 获取组件映射对象,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;
|
||||
}
|
||||
|
||||
@ -11,9 +11,9 @@ export interface AppSchema {
|
||||
addComponentsMap(componentName: ComponentMap): void;
|
||||
removeComponentsMap(componentName: string): void;
|
||||
|
||||
getPages(): PageConfig[];
|
||||
addPage(page: PageConfig): void;
|
||||
removePage(id: string): void;
|
||||
getPageConfigs(): PageConfig[];
|
||||
addPageConfig(page: PageConfig): void;
|
||||
removePageConfig(id: string): void;
|
||||
|
||||
getByKey<K extends keyof Project>(key: K): Project[K] | undefined;
|
||||
updateByKey<K extends keyof Project>(
|
||||
@ -55,14 +55,14 @@ export function createAppSchema(schema: Project): AppSchema {
|
||||
removeArrayItem(schemaRef.componentsMap, 'componentName', componentName);
|
||||
},
|
||||
|
||||
getPages() {
|
||||
getPageConfigs() {
|
||||
return schemaRef.pages ?? [];
|
||||
},
|
||||
addPage(page) {
|
||||
addPageConfig(page) {
|
||||
schemaRef.pages ??= [];
|
||||
addArrayItem(schemaRef.pages, page, 'id');
|
||||
},
|
||||
removePage(id) {
|
||||
removePageConfig(id) {
|
||||
schemaRef.pages ??= [];
|
||||
removeArrayItem(schemaRef.pages, 'id', id);
|
||||
},
|
||||
@ -72,8 +72,7 @@ export function createAppSchema(schema: Project): AppSchema {
|
||||
},
|
||||
updateByKey(key, updater) {
|
||||
const value = schemaRef[key];
|
||||
|
||||
schemaRef[key] = typeof updater === 'function' ? updater(value) : updater;
|
||||
schemaRef[key] = typeof updater === 'function' ? (updater as any)(value) : updater;
|
||||
},
|
||||
|
||||
find(predicate) {
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { Package } from './specs/asset-spec';
|
||||
import { Project, ComponentMap } from './specs/lowcode-spec';
|
||||
import { ComponentTree } from './specs/lowcode-spec';
|
||||
|
||||
export interface ProCodeComponent extends Package {
|
||||
package: string;
|
||||
type: 'proCode';
|
||||
}
|
||||
|
||||
export interface LowCodeComponent extends Package {
|
||||
export interface LowCodeComponent extends Omit<Package, 'schema'> {
|
||||
id: string;
|
||||
type: 'lowCode';
|
||||
componentName: string;
|
||||
schema: Project;
|
||||
schema: ComponentTree;
|
||||
}
|
||||
|
||||
@ -189,20 +189,28 @@ export interface ComponentTreeNodeProps {
|
||||
/** 组件内联样式 */
|
||||
style?: JSONObject | JSExpression;
|
||||
/** 组件 ref 名称 */
|
||||
ref?: string | JSExpression;
|
||||
ref?: string;
|
||||
|
||||
[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
|
||||
* 用于描述物料开发过程中,自定义扩展或引入的第三方工具类(例如:lodash 及 moment),增强搭建基础协议的扩展性,提供通用的工具类方法的配置方案及调用 API。
|
||||
*/
|
||||
export interface Util {
|
||||
name: string;
|
||||
type: 'npm' | 'function';
|
||||
content: ComponentMap | JSFunction;
|
||||
}
|
||||
export type Util = NPMUtil | FunctionUtil;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
type: 'JSSlot';
|
||||
value: 1;
|
||||
value: ComponentTreeNode | ComponentTreeNode[];
|
||||
params?: string[];
|
||||
}
|
||||
|
||||
|
||||
@ -50,10 +50,16 @@ export interface InstanceDataSourceApi {
|
||||
reloadDataSource: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用级别的公共函数或第三方扩展
|
||||
*/
|
||||
export interface UtilsApi {
|
||||
utils: Record<string, AnyFunction>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化相关 API
|
||||
*/
|
||||
export interface IntlApi {
|
||||
/**
|
||||
* 返回语料字符串
|
||||
@ -72,4 +78,106 @@ export interface IntlApi {
|
||||
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';
|
||||
|
||||
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 {
|
||||
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(
|
||||
obj: any,
|
||||
predicate: (obj: any) => boolean,
|
||||
processor: (node: any, paths: Array<string | number>) => any
|
||||
processor: (node: any, paths: Array<string | number>) => any,
|
||||
): any {
|
||||
const innerProcess = (target: any, paths: Array<string | number>): any => {
|
||||
if (Array.isArray(target)) {
|
||||
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 (predicate(target)) {
|
||||
|
||||
@ -1,35 +1,32 @@
|
||||
import type { NodeType, ComponentTreeNode, ComponentTreeNodeProps } from './types';
|
||||
import { isJSExpression, isI18nNode } from './utils/type-guard';
|
||||
import { guid } from './utils/guid';
|
||||
|
||||
export class Widget<Data, Element> {
|
||||
protected _raw: Data;
|
||||
protected proxyElements: Element[] = [];
|
||||
protected renderObject: Element | undefined;
|
||||
|
||||
constructor(data: Data) {
|
||||
this._raw = data;
|
||||
constructor(public raw: Data) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
protected init() {}
|
||||
|
||||
get raw() {
|
||||
return this._raw;
|
||||
get key(): string {
|
||||
return (this.raw as any)?.id ?? `${guid()}`;
|
||||
}
|
||||
|
||||
setRenderObject(el: Element) {
|
||||
this.renderObject = el;
|
||||
}
|
||||
getRenderObject() {
|
||||
return this.renderObject;
|
||||
mapRenderObject(mapper: (widget: Widget<Data, Element>) => Element | undefined) {
|
||||
this.renderObject = mapper(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
addProxyELements(el: Element) {
|
||||
this.proxyElements.push(el);
|
||||
this.proxyElements.unshift(el);
|
||||
}
|
||||
|
||||
build(builder: (elements: Element[]) => Element) {
|
||||
return builder(this.proxyElements);
|
||||
build<C>(builder: (elements: Element[]) => C): C {
|
||||
return builder(this.renderObject ? [...this.proxyElements, this.renderObject] : []);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,11 +50,11 @@ export class ComponentWidget<E = unknown> extends Widget<ComponentTreeNode, E> {
|
||||
private _propsValue: ComponentTreeNodeProps = {};
|
||||
|
||||
protected init() {
|
||||
if (this._raw.props) {
|
||||
this._propsValue = this._raw.props;
|
||||
if (this.raw.props) {
|
||||
this._propsValue = this.raw.props;
|
||||
}
|
||||
if (this._raw.children) {
|
||||
this._children = this._raw.children.map((child) => createWidget<E>(child));
|
||||
if (this.raw.children) {
|
||||
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;
|
||||
}
|
||||
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() {
|
||||
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) {
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,10 @@
|
||||
"bugs": "https://github.com/alibaba/lowcode-engine/issues",
|
||||
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
||||
"license": "MIT",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "",
|
||||
"build": "tsc",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -20,8 +22,11 @@
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@alilc/runtime-router": "1.0.0-beta.0",
|
||||
"@testing-library/react": "^14.2.0",
|
||||
"@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-dom": "^18.2.22",
|
||||
"jsdom": "^24.0.0"
|
||||
|
||||
@ -1,34 +1,47 @@
|
||||
import {
|
||||
type App,
|
||||
type RenderBase,
|
||||
type AppBase,
|
||||
createAppFunction,
|
||||
type AppOptionsBase,
|
||||
} from '@alilc/renderer-core';
|
||||
import { type ComponentType } from 'react';
|
||||
import { type Root, createRoot } from 'react-dom/client';
|
||||
import { createRouter } from '@alilc/runtime-router';
|
||||
import { createRenderer } from '../renderer';
|
||||
import AppComponent from '../components/app';
|
||||
import { intlPlugin } from '../plugins/intl';
|
||||
import { globalUtilsPlugin } from '../plugins/utils';
|
||||
import { initRouter } from '../router';
|
||||
import { createIntl } from '../runtime-api/intl';
|
||||
import { createRuntimeUtils } from '../runtime-api/utils';
|
||||
|
||||
export interface AppOptions extends AppOptionsBase {
|
||||
dataSourceCreator: DataSourceCreator;
|
||||
dataSourceCreator: any;
|
||||
faultComponent?: ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface ReactRender extends RenderBase {}
|
||||
export interface ReactRender extends AppBase {}
|
||||
|
||||
export type ReactApp = App<ReactRender>;
|
||||
|
||||
export const createApp = createAppFunction<AppOptions, ReactRender>(async (context, options) => {
|
||||
const renderer = createRenderer();
|
||||
const appContext = { ...context, renderer };
|
||||
const { schema, packageManager, appScope, boosts } = context;
|
||||
|
||||
initRouter(appContext);
|
||||
// router
|
||||
// todo: transform config
|
||||
const router = createRouter(schema.getByKey('router') as any);
|
||||
|
||||
options.plugins ??= [];
|
||||
options.plugins!.unshift(globalUtilsPlugin, intlPlugin);
|
||||
appScope.inject('router', router);
|
||||
|
||||
// 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
|
||||
if (options.faultComponent) {
|
||||
@ -37,6 +50,8 @@ export const createApp = createAppFunction<AppOptions, ReactRender>(async (conte
|
||||
context.config.set('dataSourceCreator', options.dataSourceCreator);
|
||||
|
||||
let root: Root | undefined;
|
||||
const renderer = createRenderer();
|
||||
const appContext = { ...context, renderer };
|
||||
|
||||
const reactRender: ReactRender = {
|
||||
async mount(el) {
|
||||
@ -56,7 +71,7 @@ export const createApp = createAppFunction<AppOptions, ReactRender>(async (conte
|
||||
};
|
||||
|
||||
return {
|
||||
renderBase: reactRender,
|
||||
appBase: reactRender,
|
||||
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 { createComponent } from '../api/createComponent';
|
||||
import { createComponent } from '../component';
|
||||
import Route from './route';
|
||||
import { createRouterProvider } from './router-view';
|
||||
|
||||
export default function App({ context }: { context: AppContextObject }) {
|
||||
const { schema, config, renderer, packageManager, appScope } = context;
|
||||
@ -15,8 +16,7 @@ export default function App({ context }: { context: AppContextObject }) {
|
||||
|
||||
if (Component?.devMode === 'lowCode') {
|
||||
const componentsMap = schema.getComponentsMaps();
|
||||
const componentsRecord =
|
||||
packageManager.getComponentsNameRecord<any>(componentsMap);
|
||||
const componentsRecord = packageManager.getComponentsNameRecord<any>(componentsMap);
|
||||
|
||||
const Layout = createComponent({
|
||||
componentsTree: Component.schema,
|
||||
@ -24,6 +24,7 @@ export default function App({ context }: { context: AppContextObject }) {
|
||||
|
||||
dataSourceCreator: config.get('dataSourceCreator'),
|
||||
supCodeScope: appScope,
|
||||
intl: appScope.value.intl,
|
||||
});
|
||||
|
||||
return Layout;
|
||||
@ -54,5 +55,11 @@ export default function App({ context }: { context: AppContextObject }) {
|
||||
}, 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 { createComponent } from '../api/createComponent';
|
||||
import { PAGE_EVENTS } from '../events';
|
||||
import { createComponent } from '../component';
|
||||
|
||||
export interface OutletProps {
|
||||
pageSchema: PageSchema;
|
||||
componentsTree?: PageContainerSchema | undefined;
|
||||
pageConfig: PageConfig;
|
||||
componentsTree?: ComponentTree | undefined;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default function Outlet({ pageSchema, componentsTree }: OutletProps) {
|
||||
const { schema, config, packageManager, appScope, boosts } = useAppContext();
|
||||
const { schema, config, packageManager, appScope } = useAppContext();
|
||||
const { type = 'lowCode' } = pageSchema;
|
||||
|
||||
if (type === 'lowCode' && componentsTree) {
|
||||
const componentsMap = schema.getComponentsMaps();
|
||||
const componentsRecord =
|
||||
packageManager.getComponentsNameRecord<any>(componentsMap);
|
||||
const componentsRecord = packageManager.getComponentsNameRecord<any>(componentsMap);
|
||||
|
||||
const LowCodeComponent = createComponent({
|
||||
supCodeScope: appScope,
|
||||
dataSourceCreator: config.get('dataSourceCreator'),
|
||||
componentsTree,
|
||||
componentsRecord,
|
||||
|
||||
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
|
||||
);
|
||||
},
|
||||
intl: appScope.value.intl,
|
||||
});
|
||||
|
||||
return <LowCodeComponent />;
|
||||
|
||||
@ -1,28 +1,21 @@
|
||||
import { usePageSchema } from '../context/router';
|
||||
import { usePageConfig } from '../context/router';
|
||||
import { useAppContext } from '../context/app';
|
||||
import RouteOutlet from './outlet';
|
||||
|
||||
export default function Route(props: any) {
|
||||
const { schema, renderer } = useAppContext();
|
||||
const pageSchema = usePageSchema();
|
||||
const Outlet = renderer.getOutlet();
|
||||
const pageConfig = usePageConfig();
|
||||
const Outlet = renderer.getOutlet() ?? RouteOutlet;
|
||||
|
||||
if (Outlet && pageSchema) {
|
||||
if (Outlet && pageConfig) {
|
||||
let componentsTree;
|
||||
const { type = 'lowCode', treeId } = pageSchema;
|
||||
const { type = 'lowCode', mappingId } = pageConfig;
|
||||
|
||||
if (type === 'lowCode') {
|
||||
componentsTree = schema
|
||||
.getComponentsTrees()
|
||||
.find(item => item.id === treeId);
|
||||
componentsTree = schema.getComponentsTrees().find((item) => item.id === mappingId);
|
||||
}
|
||||
|
||||
return (
|
||||
<Outlet
|
||||
{...props}
|
||||
pageSchema={pageSchema}
|
||||
componentsTree={componentsTree}
|
||||
/>
|
||||
);
|
||||
return <Outlet {...props} pageConfig={pageConfig} componentsTree={componentsTree} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@ -1,28 +1,24 @@
|
||||
import { type Router } from '@alilc/runtime-router';
|
||||
import { useState, useLayoutEffect, useMemo, type ReactNode } from 'react';
|
||||
import {
|
||||
RouterContext,
|
||||
RouteLocationContext,
|
||||
PageSchemaContext,
|
||||
} from '../context/router';
|
||||
import { RouterContext, RouteLocationContext, PageConfigContext } from '../context/router';
|
||||
import { useAppContext } from '../context/app';
|
||||
|
||||
export const createRouterProvider = (router: Router) => {
|
||||
return function RouterProvider({ children }: { children?: ReactNode }) {
|
||||
const { schema } = useAppContext();
|
||||
const [location, setCurrentLocation] = useState(router.getCurrentRoute());
|
||||
const [location, setCurrentLocation] = useState(router.getCurrentLocation());
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const remove = router.afterRouteChange(to => setCurrentLocation(to));
|
||||
const remove = router.afterRouteChange((to) => setCurrentLocation(to));
|
||||
return () => remove();
|
||||
}, []);
|
||||
|
||||
const pageSchema = useMemo(() => {
|
||||
const pages = schema.getPages();
|
||||
const pages = schema.getPageConfigs();
|
||||
const matched = location.matched[location.matched.length - 1];
|
||||
|
||||
if (matched) {
|
||||
const page = pages.find(item => matched.page === item.id);
|
||||
const page = pages.find((item) => matched.page === item.id);
|
||||
return page;
|
||||
}
|
||||
|
||||
@ -32,9 +28,7 @@ export const createRouterProvider = (router: Router) => {
|
||||
return (
|
||||
<RouterContext.Provider value={router}>
|
||||
<RouteLocationContext.Provider value={location}>
|
||||
<PageSchemaContext.Provider value={pageSchema}>
|
||||
{children}
|
||||
</PageSchemaContext.Provider>
|
||||
<PageConfigContext.Provider value={pageSchema}>{children}</PageConfigContext.Provider>
|
||||
</RouteLocationContext.Provider>
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
export interface AppContextObject extends AppContextType {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Router, type RouteLocation } from '@alilc/runtime-router';
|
||||
import { type PageSchema } from '@alilc/runtime-shared';
|
||||
import { type Router, type RouteLocationNormalized } from '@alilc/runtime-router';
|
||||
import { type PageConfig } from '@alilc/renderer-core';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export const RouterContext = createContext<Router>({} as any);
|
||||
@ -8,10 +8,10 @@ RouterContext.displayName = 'RouterContext';
|
||||
|
||||
export const useRouter = () => useContext(RouterContext);
|
||||
|
||||
export const RouteLocationContext = createContext<RouteLocation>({
|
||||
export const RouteLocationContext = createContext<RouteLocationNormalized>({
|
||||
name: undefined,
|
||||
path: '/',
|
||||
query: {},
|
||||
searchParams: undefined,
|
||||
params: {},
|
||||
hash: '',
|
||||
fullPath: '/',
|
||||
@ -24,10 +24,8 @@ RouteLocationContext.displayName = 'RouteLocationContext';
|
||||
|
||||
export const useRouteLocation = () => useContext(RouteLocationContext);
|
||||
|
||||
export const PageSchemaContext = createContext<PageSchema | undefined>(
|
||||
undefined
|
||||
);
|
||||
export const PageConfigContext = createContext<PageConfig | 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,
|
||||
type Plugin,
|
||||
type PluginSetupContext,
|
||||
} from '@alilc/runtime-core';
|
||||
} from '@alilc/renderer-core';
|
||||
import { type ComponentType, type PropsWithChildren } from 'react';
|
||||
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(
|
||||
messages: Record<string, Record<string, string>>,
|
||||
defaultLocale: string
|
||||
defaultLocale: string,
|
||||
) {
|
||||
const allMessages = createSignal(messages);
|
||||
const currentLocale = createSignal(defaultLocale);
|
||||
const currentMessages = computed(
|
||||
() => allMessages.value[currentLocale.value]
|
||||
);
|
||||
const currentMessages = computed(() => allMessages.value[currentLocale.value]);
|
||||
|
||||
return {
|
||||
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 {
|
||||
ref,
|
||||
computed,
|
||||
@ -114,7 +115,7 @@ function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set<u
|
||||
});
|
||||
} else if (isPlainObject(value)) {
|
||||
for (const key in value) {
|
||||
traverse(value[key], depth, currentDepth, seen);
|
||||
traverse((value as PlainObject)[key], depth, currentDepth, seen);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
|
||||
@ -1,32 +1,26 @@
|
||||
import {
|
||||
type AnyObject,
|
||||
processValue,
|
||||
type AnyFunction,
|
||||
type PlainObject,
|
||||
type JSExpression,
|
||||
isJsExpression,
|
||||
} from '@alilc/runtime-shared';
|
||||
import { processValue } from '@alilc/runtime-core';
|
||||
import {
|
||||
type ComponentType,
|
||||
memo,
|
||||
forwardRef,
|
||||
type ForwardRefRenderFunction,
|
||||
type PropsWithChildren,
|
||||
} from 'react';
|
||||
isJSExpression,
|
||||
} from '@alilc/renderer-core';
|
||||
import { type ComponentType, memo, forwardRef, type PropsWithChildren, createElement } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
||||
import { computed, watch } from '../signals';
|
||||
|
||||
export interface ReactiveStore<Snapshot = AnyObject> {
|
||||
export interface ReactiveStore<Snapshot = PlainObject> {
|
||||
value: Snapshot;
|
||||
onStateChange: AnyFunction | null;
|
||||
subscribe: (onStoreChange: () => void) => () => void;
|
||||
getSnapshot: () => Snapshot;
|
||||
}
|
||||
|
||||
function createReactiveStore<Snapshot = AnyObject>(
|
||||
function createReactiveStore<Snapshot = PlainObject>(
|
||||
target: Record<string, any>,
|
||||
valueGetter: (expr: JSExpression) => any
|
||||
valueGetter: (expr: JSExpression) => any,
|
||||
): ReactiveStore<Snapshot> {
|
||||
let isFlushing = false;
|
||||
let isFlushPending = false;
|
||||
@ -34,25 +28,21 @@ function createReactiveStore<Snapshot = AnyObject>(
|
||||
const cleanups: Array<() => void> = [];
|
||||
const waitPathToSetValueMap = new Map();
|
||||
|
||||
const initValue = processValue(
|
||||
target,
|
||||
isJsExpression,
|
||||
(node: JSExpression, paths) => {
|
||||
const computedValue = computed(() => valueGetter(node));
|
||||
const unwatch = watch(computedValue, newValue => {
|
||||
waitPathToSetValueMap.set(paths, newValue);
|
||||
const initValue = processValue(target, isJSExpression, (node: JSExpression, paths) => {
|
||||
const computedValue = computed(() => valueGetter(node));
|
||||
const unwatch = watch(computedValue, (newValue) => {
|
||||
waitPathToSetValueMap.set(paths, newValue);
|
||||
|
||||
if (!isFlushPending && !isFlushing) {
|
||||
isFlushPending = true;
|
||||
Promise.resolve().then(genValue);
|
||||
}
|
||||
});
|
||||
if (!isFlushPending && !isFlushing) {
|
||||
isFlushPending = true;
|
||||
Promise.resolve().then(genValue);
|
||||
}
|
||||
});
|
||||
|
||||
cleanups.push(unwatch);
|
||||
cleanups.push(unwatch);
|
||||
|
||||
return computedValue.value;
|
||||
}
|
||||
);
|
||||
return computedValue.value;
|
||||
});
|
||||
|
||||
const genValue = () => {
|
||||
isFlushPending = false;
|
||||
@ -89,7 +79,7 @@ function createReactiveStore<Snapshot = AnyObject>(
|
||||
return () => {
|
||||
store.onStateChange = null;
|
||||
|
||||
cleanups.forEach(c => c());
|
||||
cleanups.forEach((c) => c());
|
||||
cleanups.length = 0;
|
||||
};
|
||||
},
|
||||
@ -102,35 +92,32 @@ function createReactiveStore<Snapshot = AnyObject>(
|
||||
}
|
||||
|
||||
interface ReactiveOptions {
|
||||
target: AnyObject;
|
||||
target: PlainObject;
|
||||
valueGetter: (expr: JSExpression) => any;
|
||||
forwardRef?: boolean;
|
||||
}
|
||||
|
||||
export function reactive<TProps extends AnyObject = AnyObject>(
|
||||
WrappedComponent: ForwardRefRenderFunction<PropsWithChildren<TProps>>,
|
||||
{ target, valueGetter, forwardRef: forwardRefOption = true }: ReactiveOptions
|
||||
) {
|
||||
export function reactive<TProps extends PlainObject = PlainObject>(
|
||||
WrappedComponent: ComponentType<TProps>,
|
||||
{ target, valueGetter, forwardRef: forwardRefOption = true }: ReactiveOptions,
|
||||
): ComponentType<PlainObject> {
|
||||
const store = createReactiveStore(target, valueGetter);
|
||||
|
||||
function WrapperComponent(props: any, ref: any) {
|
||||
const actualProps = useSyncExternalStore(
|
||||
store.subscribe,
|
||||
store.getSnapshot
|
||||
);
|
||||
return <WrappedComponent {...props} {...actualProps} ref={ref} />;
|
||||
const actualProps = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
||||
|
||||
return createElement(WrappedComponent, {
|
||||
...props,
|
||||
...actualProps,
|
||||
ref,
|
||||
});
|
||||
}
|
||||
|
||||
const componentName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||
const componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||
const displayName = `Reactive(${componentName})`;
|
||||
|
||||
const _Reactived = forwardRefOption
|
||||
? forwardRef(WrapperComponent)
|
||||
: WrapperComponent;
|
||||
const Reactived = memo(_Reactived) as unknown as ComponentType<
|
||||
PropsWithChildren<TProps>
|
||||
>;
|
||||
const _Reactived = forwardRefOption ? forwardRef(WrapperComponent) : WrapperComponent;
|
||||
const Reactived = memo(_Reactived) as unknown as ComponentType<PropsWithChildren<TProps>>;
|
||||
|
||||
Reactived.displayName = WrappedComponent.displayName = displayName;
|
||||
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"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",
|
||||
"homepage": "https://github.com/alibaba/lowcode-engine/#readme",
|
||||
"license": "MIT",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "",
|
||||
"build": "tsc",
|
||||
"test": "vitest"
|
||||
},
|
||||
"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 {
|
||||
type RouteLocation,
|
||||
type RouteLocationRaw,
|
||||
} from '@alilc/runtime-shared';
|
||||
import { RawRouteLocation } from '@alilc/renderer-core';
|
||||
import { type RouteLocationNormalized } from './types';
|
||||
import { isRouteLocation } from './utils/helper';
|
||||
|
||||
export type NavigationHookAfter = (
|
||||
to: RouteLocation,
|
||||
from: RouteLocation
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
) => any;
|
||||
|
||||
export type NavigationGuardReturn =
|
||||
| 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;
|
||||
}
|
||||
export type NavigationGuardReturn = undefined | Error | RawRouteLocation | boolean;
|
||||
|
||||
/**
|
||||
* Navigation guard.
|
||||
*/
|
||||
export interface NavigationGuard {
|
||||
(to: RouteLocation, from: RouteLocation, next: NavigationGuardNext):
|
||||
| NavigationGuardReturn
|
||||
| Promise<NavigationGuardReturn>;
|
||||
(
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
): NavigationGuardReturn | Promise<NavigationGuardReturn>;
|
||||
}
|
||||
|
||||
export function guardToPromiseFn(
|
||||
guard: NavigationGuard,
|
||||
to: RouteLocation,
|
||||
from: RouteLocation
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
): () => Promise<void> {
|
||||
return () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const next: NavigationGuardNext = (
|
||||
valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback | Error
|
||||
) => {
|
||||
const next = (valid?: boolean | RawRouteLocation | Error) => {
|
||||
if (valid === false) {
|
||||
reject();
|
||||
} else if (valid instanceof Error) {
|
||||
reject(valid);
|
||||
} else if (isRouteLocation(valid)) {
|
||||
// todo
|
||||
// reject(
|
||||
// createRouterError<NavigationRedirectError>(
|
||||
// ErrorTypes.NAVIGATION_GUARD_REDIRECT,
|
||||
// {
|
||||
// from: to,
|
||||
// to: valid
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
// todo reject (error)
|
||||
reject();
|
||||
} else {
|
||||
resolve();
|
||||
@ -74,55 +40,10 @@ export function guardToPromiseFn(
|
||||
};
|
||||
|
||||
// 使用 Promise.resolve 包装允许它与异步和同步守卫一起工作
|
||||
const guardReturn = guard.call(
|
||||
null,
|
||||
to,
|
||||
from,
|
||||
canOnlyBeCalledOnce(next, to, from)
|
||||
);
|
||||
let guardCall = Promise.resolve(guardReturn);
|
||||
const guardReturn = guard.call(null, to, from);
|
||||
|
||||
if (guard.length <= 2) guardCall = guardCall.then(next);
|
||||
if (guard.length > 2) {
|
||||
const message = `The "next" callback was never called inside of ${
|
||||
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));
|
||||
return Promise.resolve(guardReturn)
|
||||
.then(next)
|
||||
.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 HistoryLocation = string;
|
||||
@ -23,7 +23,7 @@ export type NavigationInformation = {
|
||||
export type NavigationCallback = (
|
||||
to: HistoryLocation,
|
||||
from: HistoryLocation,
|
||||
info: NavigationInformation
|
||||
info: NavigationInformation,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
@ -94,7 +94,7 @@ function buildState(
|
||||
current: HistoryLocation,
|
||||
forward: HistoryLocation | null,
|
||||
replaced = false,
|
||||
position = window.history.length
|
||||
position = window.history.length,
|
||||
): RouterHistoryState {
|
||||
return {
|
||||
name: '__ROUTER_STATE__',
|
||||
@ -110,25 +110,14 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
||||
const finalBase = normalizeBase(base);
|
||||
const { history, location } = window;
|
||||
|
||||
let currentLocation: HistoryLocation = createCurrentLocation(
|
||||
finalBase,
|
||||
location
|
||||
);
|
||||
let currentLocation: HistoryLocation = createCurrentLocation(finalBase, location);
|
||||
let historyState: RouterHistoryState = history.state;
|
||||
|
||||
if (!historyState) {
|
||||
doDomHistoryEvent(
|
||||
currentLocation,
|
||||
buildState(null, currentLocation, null, true),
|
||||
true
|
||||
);
|
||||
doDomHistoryEvent(currentLocation, buildState(null, currentLocation, null, true), true);
|
||||
}
|
||||
|
||||
function doDomHistoryEvent(
|
||||
to: HistoryLocation,
|
||||
state: RouterHistoryState,
|
||||
replace: boolean
|
||||
) {
|
||||
function doDomHistoryEvent(to: HistoryLocation, state: RouterHistoryState, replace: boolean) {
|
||||
// 处理 hash 情况下的 url
|
||||
const hashIndex = finalBase.indexOf('#');
|
||||
const url =
|
||||
@ -150,7 +139,7 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
||||
history.state,
|
||||
buildState(historyState.back, to, historyState.forward, true),
|
||||
data,
|
||||
{ position: historyState.position }
|
||||
{ position: historyState.position },
|
||||
);
|
||||
|
||||
doDomHistoryEvent(to, state, true);
|
||||
@ -158,14 +147,9 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
||||
}
|
||||
|
||||
function push(to: HistoryLocation, data?: HistoryState) {
|
||||
const currentState: RouterHistoryState = Object.assign(
|
||||
{},
|
||||
historyState,
|
||||
history.state,
|
||||
{
|
||||
forward: to,
|
||||
}
|
||||
);
|
||||
const currentState: RouterHistoryState = Object.assign({}, historyState, history.state, {
|
||||
forward: to,
|
||||
});
|
||||
|
||||
// 防止当前浏览器的 state 被修改先 replace 一次
|
||||
// 将上次的state 的 forward 修改为 to
|
||||
@ -175,15 +159,15 @@ export function createBrowserHistory(base?: string): RouterHistory {
|
||||
{},
|
||||
buildState(currentLocation, to, null),
|
||||
{ position: currentState.position + 1 },
|
||||
data
|
||||
data,
|
||||
);
|
||||
|
||||
doDomHistoryEvent(to, state, false);
|
||||
currentLocation = to;
|
||||
}
|
||||
|
||||
let listeners = useCallbacks<NavigationCallback>();
|
||||
let teardowns = useCallbacks<() => void>();
|
||||
let listeners = useEvent<NavigationCallback>();
|
||||
let teardowns = useEvent<() => void>();
|
||||
|
||||
let pauseState: HistoryLocation | null = null;
|
||||
|
||||
@ -293,8 +277,7 @@ function normalizeBase(base?: string) {
|
||||
}
|
||||
|
||||
const TRAILING_SLASH_RE = /\/$/;
|
||||
export const removeTrailingSlash = (path: string) =>
|
||||
path.replace(TRAILING_SLASH_RE, '');
|
||||
export const removeTrailingSlash = (path: string) => path.replace(TRAILING_SLASH_RE, '');
|
||||
|
||||
function createCurrentLocation(base: string, location: Location) {
|
||||
const { pathname, search, hash } = location;
|
||||
@ -302,9 +285,7 @@ function createCurrentLocation(base: string, location: Location) {
|
||||
// hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
|
||||
const hashPos = base.indexOf('#');
|
||||
if (hashPos > -1) {
|
||||
let slicePos = hash.includes(base.slice(hashPos))
|
||||
? base.slice(hashPos).length
|
||||
: 1;
|
||||
let slicePos = hash.includes(base.slice(hashPos)) ? base.slice(hashPos).length : 1;
|
||||
let pathFromHash = hash.slice(slicePos);
|
||||
// prepend the starting slash to hash so the url starts with /#
|
||||
if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash;
|
||||
@ -339,10 +320,7 @@ export function createHashHistory(base?: string): RouterHistory {
|
||||
|
||||
if (!base.endsWith('#/') && !base.endsWith('#')) {
|
||||
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 });
|
||||
}
|
||||
|
||||
const listeners = useCallbacks<NavigationCallback>();
|
||||
const listeners = useEvent<NavigationCallback>();
|
||||
|
||||
function triggerListeners(
|
||||
to: HistoryLocation,
|
||||
from: HistoryLocation,
|
||||
{ direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>
|
||||
{ direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>,
|
||||
): void {
|
||||
const info: NavigationInformation = {
|
||||
direction,
|
||||
@ -411,7 +389,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
||||
current: to,
|
||||
},
|
||||
data,
|
||||
{ position }
|
||||
{ position },
|
||||
);
|
||||
|
||||
// remove current entry and decrement position
|
||||
@ -428,12 +406,9 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
||||
historyStack.splice(position, 1);
|
||||
pushStack(prevState.current, prevState);
|
||||
|
||||
const currentState = Object.assign(
|
||||
{},
|
||||
buildState(prevState.current, to, null, false),
|
||||
data,
|
||||
{ position: ++position }
|
||||
);
|
||||
const currentState = Object.assign({}, buildState(prevState.current, to, null, false), data, {
|
||||
position: ++position,
|
||||
});
|
||||
|
||||
pushStack(to, currentState);
|
||||
},
|
||||
@ -442,10 +417,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
||||
const direction: NavigationDirection =
|
||||
delta < 0 ? NavigationDirection.back : NavigationDirection.forward;
|
||||
|
||||
position = Math.max(
|
||||
0,
|
||||
Math.min(position + delta, historyStack.length - 1)
|
||||
);
|
||||
position = Math.max(0, Math.min(position + delta, historyStack.length - 1));
|
||||
|
||||
if (shouldTriggerListeners) {
|
||||
triggerListeners(this.location, from, {
|
||||
@ -459,9 +431,7 @@ export function createMemoryHistory(base = ''): RouterHistory {
|
||||
destroy() {
|
||||
listeners.clear();
|
||||
position = 0;
|
||||
historyStack = [
|
||||
{ location: '', state: buildState(null, '', null, false, position) },
|
||||
];
|
||||
historyStack = [{ location: '', state: buildState(null, '', null, false, position) }];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,16 +1,7 @@
|
||||
export { createRouter } from './router';
|
||||
export {
|
||||
createBrowserHistory,
|
||||
createHashHistory,
|
||||
createMemoryHistory,
|
||||
} from './history';
|
||||
export { createBrowserHistory, createHashHistory, createMemoryHistory } from './history';
|
||||
|
||||
export type { RouterHistory } from './history';
|
||||
export type { NavigationGuard, NavigationHookAfter } from './guard';
|
||||
export type { Router, RouterOptions } from './router';
|
||||
export type { RouteParams, LocationQuery, RouteRecord } from './types';
|
||||
export type {
|
||||
RouteLocation,
|
||||
RouteLocationRaw,
|
||||
RouteLocationOptions,
|
||||
} from '@alilc/runtime-shared';
|
||||
export * from './types';
|
||||
|
||||
@ -1,76 +1,69 @@
|
||||
import { type AnyObject, pick } from '@alilc/runtime-shared';
|
||||
import type { RouteRecord, RouteParams } from './types';
|
||||
import {
|
||||
createRouteRecordMatcher,
|
||||
type RouteRecordMatcher,
|
||||
} from './utils/record-matcher';
|
||||
import { type PlainObject, type RawLocation } from '@alilc/renderer-core';
|
||||
import { pick } from 'lodash-es';
|
||||
import { createRouteRecordMatcher, type RouteRecordMatcher } from './utils/record-matcher';
|
||||
import { type PathParserOptions } from './utils/path-parser';
|
||||
|
||||
export interface MatcherLocationAsPath {
|
||||
path: string;
|
||||
}
|
||||
export interface MatcherLocationAsRelative {
|
||||
params?: Record<string, string | string[]>;
|
||||
}
|
||||
export interface MatcherLocationAsName {
|
||||
name: string;
|
||||
params?: RouteParams;
|
||||
}
|
||||
import type { RouteRecord, RouteParams, RouteLocationNormalized } from './types';
|
||||
|
||||
/**
|
||||
* 匹配器的路由参数
|
||||
*/
|
||||
export type MatcherLocationRaw =
|
||||
| MatcherLocationAsPath
|
||||
| MatcherLocationAsName
|
||||
| MatcherLocationAsRelative;
|
||||
|
||||
export type RouteRecordNormalized = Required<
|
||||
Pick<RouteRecord, 'path' | 'page' | 'children'>
|
||||
> & {
|
||||
export interface RouteRecordNormalized {
|
||||
/**
|
||||
* {@link RouteRecord.name}
|
||||
*/
|
||||
name: string | undefined;
|
||||
name: RouteRecord['name'];
|
||||
path: RouteRecord['path'];
|
||||
page: string;
|
||||
meta: PlainObject;
|
||||
/**
|
||||
* {@link RouteRecord.redirect}
|
||||
*/
|
||||
redirect: RouteRecord['redirect'] | undefined;
|
||||
};
|
||||
|
||||
export interface MatcherLocation {
|
||||
name: string | undefined;
|
||||
path: string;
|
||||
params: RouteParams;
|
||||
matched: RouteRecord[];
|
||||
meta: AnyObject;
|
||||
redirect: RouteRecord['redirect'];
|
||||
children: RouteRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 作为 matcher 解析 location 的关键参数及输出内容
|
||||
*/
|
||||
export type MatcherLocation = Pick<
|
||||
RouteLocationNormalized,
|
||||
'name' | 'path' | 'params' | 'matched' | 'meta'
|
||||
>;
|
||||
|
||||
/**
|
||||
* 路由匹配器
|
||||
*/
|
||||
export interface RouterMatcher {
|
||||
/**
|
||||
* 新增路由记录
|
||||
*/
|
||||
addRoute: (record: RouteRecord, parent?: RouteRecordMatcher) => void;
|
||||
/**
|
||||
* 删除路由记录
|
||||
*/
|
||||
removeRoute: {
|
||||
(matcher: RouteRecordMatcher): void;
|
||||
(name: string): void;
|
||||
};
|
||||
getRoutes: () => RouteRecordMatcher[];
|
||||
/**
|
||||
* 获取所有的路由匹配对象
|
||||
*/
|
||||
getRecordMatchers: () => RouteRecordMatcher[];
|
||||
/**
|
||||
* 获取路由匹配对象
|
||||
*/
|
||||
getRecordMatcher: (name: string) => RouteRecordMatcher | undefined;
|
||||
|
||||
/**
|
||||
* 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 currentLocation - MatcherLocation of the current location
|
||||
*/
|
||||
resolve: (
|
||||
location: MatcherLocationRaw,
|
||||
currentLocation: MatcherLocation
|
||||
) => MatcherLocation;
|
||||
resolve: (location: RawLocation, currentLocation: MatcherLocation) => MatcherLocation;
|
||||
}
|
||||
|
||||
export function createRouterMatcher(
|
||||
records: RouteRecord[],
|
||||
globalOptions: PathParserOptions
|
||||
globalOptions: PathParserOptions,
|
||||
): RouterMatcher {
|
||||
const matchers: RouteRecordMatcher[] = [];
|
||||
const matcherMap = new Map<string, RouteRecordMatcher>();
|
||||
@ -80,7 +73,7 @@ export function createRouterMatcher(
|
||||
const options: PathParserOptions = Object.assign(
|
||||
{},
|
||||
globalOptions,
|
||||
pick(record, ['end', 'sensitive', 'strict'])
|
||||
pick(record, ['end', 'sensitive', 'strict']),
|
||||
);
|
||||
|
||||
// 如果子路由不是绝对路径,则构建嵌套路由的路径。
|
||||
@ -88,10 +81,9 @@ export function createRouterMatcher(
|
||||
const { path } = normalizedRecord;
|
||||
if (parent && path[0] !== '/') {
|
||||
const parentPath = parent.record.path;
|
||||
const connectingSlash =
|
||||
parentPath[parentPath.length - 1] === '/' ? '' : '/';
|
||||
normalizedRecord.path =
|
||||
parent.record.path + (path && connectingSlash + path);
|
||||
const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/';
|
||||
|
||||
normalizedRecord.path = parent.record.path + (path && connectingSlash + path);
|
||||
}
|
||||
|
||||
const matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
|
||||
@ -130,18 +122,11 @@ export function createRouterMatcher(
|
||||
}
|
||||
}
|
||||
|
||||
function getRoutes() {
|
||||
return matchers;
|
||||
}
|
||||
|
||||
function getRecordMatcher(name: string) {
|
||||
return matcherMap.get(name);
|
||||
}
|
||||
|
||||
function resolve(
|
||||
location: MatcherLocationRaw,
|
||||
currentLocation: MatcherLocation
|
||||
): MatcherLocation {
|
||||
function resolve(location: RawLocation, currentLocation: MatcherLocation): MatcherLocation {
|
||||
let matcher: RouteRecordMatcher | undefined;
|
||||
let params: RouteParams = {};
|
||||
let path: MatcherLocation['path'];
|
||||
@ -151,34 +136,32 @@ export function createRouterMatcher(
|
||||
matcher = matcherMap.get(location.name);
|
||||
|
||||
if (!matcher) {
|
||||
throw new Error(
|
||||
`Router error: no match for ${JSON.stringify(location)}`
|
||||
);
|
||||
throw new Error(`Router error: no match for ${JSON.stringify(location)}`);
|
||||
}
|
||||
|
||||
name = matcher.record.name;
|
||||
// 从当前路径与传入的参数中获取 params
|
||||
params = Object.assign(
|
||||
paramsFromLocation(
|
||||
currentLocation.params,
|
||||
currentLocation.params ?? {},
|
||||
matcher.keys
|
||||
.filter(k => {
|
||||
.filter((k) => {
|
||||
return !(k.modifier === '?' || k.modifier === '*');
|
||||
})
|
||||
.map(k => k.name)
|
||||
.map((k) => k.name),
|
||||
),
|
||||
location.params
|
||||
? paramsFromLocation(
|
||||
location.params,
|
||||
matcher.keys.map(k => k.name)
|
||||
matcher.keys.map((k) => k.name),
|
||||
)
|
||||
: {}
|
||||
: {},
|
||||
);
|
||||
// throws if cannot be stringified
|
||||
path = matcher.stringify(params);
|
||||
} else if ('path' in location) {
|
||||
path = location.path;
|
||||
matcher = matchers.find(m => m.re.test(path));
|
||||
matcher = matchers.find((m) => m.re.test(path));
|
||||
|
||||
if (matcher) {
|
||||
name = matcher.record.name;
|
||||
@ -187,13 +170,11 @@ export function createRouterMatcher(
|
||||
} else {
|
||||
matcher = currentLocation.name
|
||||
? matcherMap.get(currentLocation.name)
|
||||
: matchers.find(m => m.re.test(currentLocation.path));
|
||||
: matchers.find((m) => m.re.test(currentLocation.path));
|
||||
|
||||
if (!matcher) {
|
||||
throw new Error(
|
||||
`no match for ${JSON.stringify(location)}, ${JSON.stringify(
|
||||
currentLocation
|
||||
)}`
|
||||
`no match for ${JSON.stringify(location)}, ${JSON.stringify(currentLocation)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -218,24 +199,23 @@ export function createRouterMatcher(
|
||||
};
|
||||
}
|
||||
|
||||
records.forEach(r => addRoute(r));
|
||||
records.forEach((r) => addRoute(r));
|
||||
|
||||
return {
|
||||
resolve,
|
||||
|
||||
addRoute,
|
||||
removeRoute,
|
||||
getRoutes,
|
||||
|
||||
getRecordMatchers() {
|
||||
return matchers;
|
||||
},
|
||||
getRecordMatcher,
|
||||
};
|
||||
}
|
||||
|
||||
function paramsFromLocation(
|
||||
params: RouteParams,
|
||||
keys: (string | number)[]
|
||||
): RouteParams {
|
||||
function paramsFromLocation(params: RouteParams, keys: (string | number)[]): RouteParams {
|
||||
const newParams = {} as RouteParams;
|
||||
|
||||
for (const key of keys) {
|
||||
if (key in params) newParams[key] = params[key];
|
||||
}
|
||||
@ -243,14 +223,13 @@ function paramsFromLocation(
|
||||
return newParams;
|
||||
}
|
||||
|
||||
export function normalizeRouteRecord(
|
||||
record: RouteRecord
|
||||
): RouteRecordNormalized {
|
||||
export function normalizeRouteRecord(record: RouteRecord): RouteRecordNormalized {
|
||||
return {
|
||||
path: record.path,
|
||||
redirect: record.redirect,
|
||||
name: record.name,
|
||||
page: record.page,
|
||||
page: record.page || '',
|
||||
meta: record['meta'] || {},
|
||||
children: record.children || [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import {
|
||||
type RouterSchema,
|
||||
useCallbacks,
|
||||
type RouterApi,
|
||||
type RouterConfig,
|
||||
type RouteLocation,
|
||||
type RouteLocationRaw,
|
||||
type RouteLocationOptions,
|
||||
noop,
|
||||
} from '@alilc/runtime-shared';
|
||||
useEvent,
|
||||
type RawRouteLocation,
|
||||
type RawLocationOptions,
|
||||
} from '@alilc/renderer-core';
|
||||
import {
|
||||
createBrowserHistory,
|
||||
createHashHistory,
|
||||
@ -13,46 +13,42 @@ import {
|
||||
type RouterHistory,
|
||||
type HistoryState,
|
||||
} from './history';
|
||||
import { createRouterMatcher, type MatcherLocationRaw } from './matcher';
|
||||
import { createRouterMatcher } from './matcher';
|
||||
import { type PathParserOptions } from './utils/path-parser';
|
||||
import { parseURL, stringifyURL } from './utils/url';
|
||||
import { normalizeQuery } from './utils/query';
|
||||
import { isSameRouteLocation } from './utils/helper';
|
||||
import type { RouteParams, RouteRecord } from './types';
|
||||
import {
|
||||
type NavigationHookAfter,
|
||||
type NavigationGuard,
|
||||
guardToPromiseFn,
|
||||
} from './guard';
|
||||
import type { RouteParams, RouteRecord, RouteLocationNormalized } from './types';
|
||||
import { 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 history: RouterHistory;
|
||||
|
||||
getCurrentRoute: () => RouteLocation;
|
||||
getCurrentLocation(): RouteLocationNormalized;
|
||||
|
||||
addRoute: {
|
||||
(parentName: string, route: RouteRecord): void;
|
||||
(route: RouteRecord): void;
|
||||
};
|
||||
resolve(
|
||||
rawLocation: RawRouteLocation,
|
||||
currentLocation?: RouteLocationNormalized,
|
||||
): RouteLocationNormalized;
|
||||
|
||||
addRoute(route: RouteRecord): void;
|
||||
removeRoute(name: string): void;
|
||||
hasRoute(name: string): boolean;
|
||||
getRoutes(): RouteRecord[];
|
||||
|
||||
push: (to: RouteLocationRaw) => void;
|
||||
replace: (to: RouteLocationRaw) => void;
|
||||
hasRoute(name: string): boolean;
|
||||
|
||||
beforeRouteLeave: (fn: NavigationGuard) => () => void;
|
||||
afterRouteChange: (fn: NavigationHookAfter) => () => void;
|
||||
}
|
||||
|
||||
export type RouterOptions = RouterSchema & PathParserOptions;
|
||||
|
||||
const START_LOCATION_NORMALIZED: RouteLocation = {
|
||||
const START_LOCATION: RouteLocationNormalized = {
|
||||
path: '/',
|
||||
name: undefined,
|
||||
params: {},
|
||||
query: {},
|
||||
searchParams: undefined,
|
||||
hash: '',
|
||||
fullPath: '/',
|
||||
matched: [],
|
||||
@ -60,54 +56,50 @@ const START_LOCATION_NORMALIZED: RouteLocation = {
|
||||
redirectedFrom: undefined,
|
||||
};
|
||||
|
||||
export function createRouter(options: RouterOptions): Router {
|
||||
const {
|
||||
baseName = '/',
|
||||
historyMode = 'browser',
|
||||
routes = [],
|
||||
...globalOptions
|
||||
} = options;
|
||||
const defaultRouterOptions: RouterOptions = {
|
||||
historyMode: 'browser',
|
||||
baseName: '/',
|
||||
routes: [],
|
||||
};
|
||||
|
||||
export function createRouter(options: RouterOptions = defaultRouterOptions): Router {
|
||||
const { baseName = '/', historyMode = 'browser', routes = [], ...globalOptions } = options;
|
||||
const matcher = createRouterMatcher(routes, globalOptions);
|
||||
const routerHistory =
|
||||
historyMode === 'hash'
|
||||
? createHashHistory(baseName)
|
||||
: historyMode === 'memory'
|
||||
? createMemoryHistory(baseName)
|
||||
: createBrowserHistory(baseName);
|
||||
? createMemoryHistory(baseName)
|
||||
: createBrowserHistory(baseName);
|
||||
|
||||
const beforeGuards = useCallbacks<NavigationGuard>();
|
||||
const afterGuards = useCallbacks<NavigationHookAfter>();
|
||||
const beforeGuards = useEvent<NavigationGuard>();
|
||||
const afterGuards = useEvent<NavigationHookAfter>();
|
||||
|
||||
let currentRoute: RouteLocation = START_LOCATION_NORMALIZED;
|
||||
let pendingLocation = currentRoute;
|
||||
let currentLocation: RouteLocationNormalized = START_LOCATION;
|
||||
let pendingLocation = currentLocation;
|
||||
|
||||
function resolve(
|
||||
rawLocation: RouteLocationRaw,
|
||||
currentLocation?: RouteLocation
|
||||
): RouteLocation & {
|
||||
rawLocation: RawRouteLocation,
|
||||
currentLocation?: RouteLocationNormalized,
|
||||
): RouteLocationNormalized & {
|
||||
href: string;
|
||||
} {
|
||||
currentLocation = Object.assign({}, currentLocation || currentRoute);
|
||||
currentLocation = Object.assign({}, currentLocation || currentLocation);
|
||||
|
||||
if (typeof rawLocation === 'string') {
|
||||
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);
|
||||
|
||||
return Object.assign(locationNormalized, matchedRoute, {
|
||||
query: locationNormalized.query as any,
|
||||
searchParams: locationNormalized.searchParams,
|
||||
hash: decodeURIComponent(locationNormalized.hash),
|
||||
redirectedFrom: undefined,
|
||||
href,
|
||||
});
|
||||
}
|
||||
|
||||
let matcherLocation: MatcherLocationRaw;
|
||||
let matcherLocation: RawRouteLocation;
|
||||
|
||||
if ('path' in rawLocation) {
|
||||
matcherLocation = { ...rawLocation };
|
||||
@ -140,13 +132,13 @@ export function createRouter(options: RouterOptions): Router {
|
||||
{
|
||||
fullPath,
|
||||
hash,
|
||||
query: normalizeQuery(rawLocation.query) as any,
|
||||
searchParams: rawLocation.searchParams,
|
||||
},
|
||||
matchedRoute,
|
||||
{
|
||||
redirectedFrom: undefined,
|
||||
href,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -169,34 +161,33 @@ export function createRouter(options: RouterOptions): Router {
|
||||
}
|
||||
}
|
||||
function getRoutes() {
|
||||
return matcher.getRoutes().map(item => item.record);
|
||||
return matcher.getRecordMatchers().map((item) => item.record);
|
||||
}
|
||||
function hasRoute(name: string) {
|
||||
return !!matcher.getRecordMatcher(name);
|
||||
}
|
||||
|
||||
function push(to: RouteLocationRaw) {
|
||||
function push(to: RawRouteLocation) {
|
||||
return pushOrRedirect(to);
|
||||
}
|
||||
function replace(to: RouteLocationRaw) {
|
||||
return pushOrRedirect({ ...locationAsObject(to), replace: true });
|
||||
function replace(to: RawRouteLocation) {
|
||||
return pushOrRedirect({ ...locationAsObject(to) }, true);
|
||||
}
|
||||
|
||||
function locationAsObject(
|
||||
to: RouteLocationRaw | RouteLocation
|
||||
): Exclude<RouteLocationRaw, string> | RouteLocation {
|
||||
to: RawRouteLocation | RouteLocation,
|
||||
): Exclude<RawRouteLocation, string> | RouteLocation {
|
||||
return typeof to === 'string' ? parseURL(to) : { ...to };
|
||||
}
|
||||
|
||||
async function pushOrRedirect(
|
||||
to: RouteLocationRaw | RouteLocation,
|
||||
redirectedFrom?: RouteLocation
|
||||
to: RawRouteLocation | RouteLocation,
|
||||
replace = false,
|
||||
redirectedFrom?: RouteLocation,
|
||||
) {
|
||||
const targetLocation = (pendingLocation = resolve(to));
|
||||
const from = currentRoute;
|
||||
const data: HistoryState | undefined = (to as RouteLocationOptions).state;
|
||||
const force: boolean | undefined = (to as RouteLocationOptions).force;
|
||||
const replace = (to as RouteLocationOptions).replace === true;
|
||||
const from = currentLocation;
|
||||
const data: HistoryState | undefined = (to as RawLocationOptions).state;
|
||||
|
||||
const shouldRedirect = getRedirectRecordIfShould(targetLocation);
|
||||
if (shouldRedirect) {
|
||||
@ -204,20 +195,17 @@ export function createRouter(options: RouterOptions): Router {
|
||||
{
|
||||
...shouldRedirect,
|
||||
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;
|
||||
|
||||
if (!force && isSameRouteLocation(from, toLocation)) {
|
||||
throw Error(
|
||||
'路由错误:重复请求' + JSON.stringify({ to: toLocation, from })
|
||||
);
|
||||
if (isSameRouteLocation(from, toLocation)) {
|
||||
throw Error('路由错误:重复请求' + JSON.stringify({ to: 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];
|
||||
|
||||
if (lastMatched?.redirect) {
|
||||
const { redirect } = lastMatched;
|
||||
let newTargetLocation =
|
||||
typeof redirect === 'function' ? redirect(to) : redirect;
|
||||
let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect;
|
||||
|
||||
if (typeof newTargetLocation === 'string') {
|
||||
newTargetLocation =
|
||||
newTargetLocation.includes('?') || newTargetLocation.includes('#')
|
||||
? locationAsObject(newTargetLocation)
|
||||
: { path: newTargetLocation };
|
||||
// @ts-expect-error 强制清空参数
|
||||
newTargetLocation.params = {};
|
||||
(newTargetLocation as any).params = {};
|
||||
}
|
||||
|
||||
if (!('path' in newTargetLocation) && !('name' in newTargetLocation)) {
|
||||
@ -254,27 +242,25 @@ export function createRouter(options: RouterOptions): Router {
|
||||
|
||||
return Object.assign(
|
||||
{
|
||||
query: to.query,
|
||||
searchParams: to.searchParams,
|
||||
hash: to.hash,
|
||||
// path 存在的时候 清空 params
|
||||
params: 'path' in newTargetLocation ? {} : to.params,
|
||||
},
|
||||
newTargetLocation
|
||||
newTargetLocation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function navigateTriggerBeforeGuards(
|
||||
to: RouteLocation,
|
||||
from: RouteLocation
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
): Promise<any> {
|
||||
let guards: ((...args: any[]) => Promise<any>)[] = [];
|
||||
|
||||
const canceledNavigationCheck = async (): Promise<any> => {
|
||||
if (pendingLocation !== to) {
|
||||
throw Error(
|
||||
`路由错误:重复导航,from: ${from.fullPath}, to: ${to.fullPath}`
|
||||
);
|
||||
throw Error(`路由错误:重复导航,from: ${from.fullPath}, to: ${to.fullPath}`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
@ -288,31 +274,26 @@ export function createRouter(options: RouterOptions): Router {
|
||||
}
|
||||
if (beforeGuardsList.length > 0) guards.push(canceledNavigationCheck);
|
||||
|
||||
return guards.reduce(
|
||||
(promise, guard) => promise.then(() => guard()),
|
||||
Promise.resolve()
|
||||
);
|
||||
return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeNavigation(
|
||||
toLocation: RouteLocation,
|
||||
from: RouteLocation,
|
||||
toLocation: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
isPush: boolean,
|
||||
replace?: boolean,
|
||||
data?: HistoryState
|
||||
data?: HistoryState,
|
||||
) {
|
||||
// 重复导航
|
||||
if (pendingLocation !== toLocation) {
|
||||
throw Error(
|
||||
`路由错误:重复导航,from: ${from.fullPath}, to: ${toLocation.fullPath}`
|
||||
);
|
||||
throw Error(`路由错误:重复导航,from: ${from.fullPath}, to: ${toLocation.fullPath}`);
|
||||
}
|
||||
|
||||
// 如果不是第一次启动的话 只需要考虑 push
|
||||
const isFirstNavigation = from === START_LOCATION_NORMALIZED;
|
||||
const isFirstNavigation = from === START_LOCATION;
|
||||
|
||||
if (isPush) {
|
||||
if (replace || isFirstNavigation) {
|
||||
@ -322,7 +303,7 @@ export function createRouter(options: RouterOptions): Router {
|
||||
}
|
||||
}
|
||||
|
||||
currentRoute = toLocation;
|
||||
currentLocation = toLocation;
|
||||
// markAsReady();
|
||||
}
|
||||
|
||||
@ -335,18 +316,15 @@ export function createRouter(options: RouterOptions): Router {
|
||||
// 判断是否需要重定向
|
||||
const shouldRedirect = getRedirectRecordIfShould(toLocation);
|
||||
if (shouldRedirect) {
|
||||
return pushOrRedirect(
|
||||
Object.assign(shouldRedirect, { replace: true }),
|
||||
toLocation
|
||||
).catch(() => {});
|
||||
return pushOrRedirect(shouldRedirect, true, toLocation).catch(() => {});
|
||||
}
|
||||
|
||||
pendingLocation = toLocation;
|
||||
const from = currentRoute;
|
||||
const from = currentLocation;
|
||||
|
||||
// 触发路由守卫
|
||||
navigateTriggerBeforeGuards(toLocation, from)
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
if (info.delta) {
|
||||
routerHistory.go(-info.delta, false);
|
||||
}
|
||||
@ -366,25 +344,30 @@ export function createRouter(options: RouterOptions): Router {
|
||||
guard(toLocation, from);
|
||||
}
|
||||
})
|
||||
.catch(noop);
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// init
|
||||
setupListeners();
|
||||
if (currentRoute === START_LOCATION_NORMALIZED) {
|
||||
push(routerHistory.location).catch(err => {
|
||||
if (currentLocation === START_LOCATION) {
|
||||
push(routerHistory.location).catch((err) => {
|
||||
console.warn('Unexpected error when starting the router:', err);
|
||||
});
|
||||
}
|
||||
|
||||
const go = (delta: number) => routerHistory.go(delta);
|
||||
|
||||
return {
|
||||
options,
|
||||
get options() {
|
||||
return options;
|
||||
},
|
||||
get history() {
|
||||
return routerHistory;
|
||||
},
|
||||
getCurrentLocation: () => currentLocation,
|
||||
|
||||
getCurrentRoute: () => currentRoute,
|
||||
resolve,
|
||||
addRoute,
|
||||
removeRoute,
|
||||
getRoutes,
|
||||
@ -392,6 +375,9 @@ export function createRouter(options: RouterOptions): Router {
|
||||
|
||||
push,
|
||||
replace,
|
||||
back: () => go(-1),
|
||||
forward: () => go(1),
|
||||
go,
|
||||
|
||||
beforeRouteLeave: beforeGuards.add,
|
||||
afterRouteChange: afterGuards.add,
|
||||
|
||||
@ -1,9 +1,22 @@
|
||||
import { type RouteSchema } from '@alilc/runtime-shared';
|
||||
import { type ParsedQs } from 'qs';
|
||||
import { type PathParserOptions } from './utils/path-parser';
|
||||
import type {
|
||||
RouteRecord as RouterRecordSpec,
|
||||
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[]>;
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
import {
|
||||
type RouteLocation,
|
||||
type RouteLocationRaw,
|
||||
} from '@alilc/runtime-shared';
|
||||
import type { RawRouteLocation } from '@alilc/renderer-core';
|
||||
import type { RouteLocationNormalized } from '../types';
|
||||
|
||||
export function isRouteLocation(route: any): route is RouteLocationRaw {
|
||||
export function isRouteLocation(route: any): route is RawRouteLocation {
|
||||
return typeof route === 'string' || (route && typeof route === 'object');
|
||||
}
|
||||
|
||||
export function isSameRouteLocation(
|
||||
a: RouteLocation,
|
||||
b: RouteLocation
|
||||
a: RouteLocationNormalized,
|
||||
b: RouteLocationNormalized,
|
||||
): boolean {
|
||||
const aLastIndex = a.matched.length - 1;
|
||||
const bLastIndex = b.matched.length - 1;
|
||||
@ -19,15 +17,18 @@ export function isSameRouteLocation(
|
||||
aLastIndex === bLastIndex &&
|
||||
a.matched[aLastIndex] === b.matched[bLastIndex] &&
|
||||
isSameRouteLocationParams(a.params, b.params) &&
|
||||
a.query?.toString() === b.query?.toString() &&
|
||||
a.searchParams?.toString() === b.searchParams?.toString() &&
|
||||
a.hash === b.hash
|
||||
);
|
||||
}
|
||||
|
||||
export function isSameRouteLocationParams(
|
||||
a: RouteLocation['params'],
|
||||
b: RouteLocation['params']
|
||||
a: RouteLocationNormalized['params'],
|
||||
b: RouteLocationNormalized['params'],
|
||||
): boolean {
|
||||
if (!a && !b) return true;
|
||||
if (!a || !b) return false;
|
||||
|
||||
if (Object.keys(a).length !== Object.keys(b).length) return false;
|
||||
|
||||
for (const key in a) {
|
||||
@ -38,14 +39,14 @@ export function isSameRouteLocationParams(
|
||||
}
|
||||
|
||||
function isSameRouteLocationParamsValue(
|
||||
a: string | readonly string[],
|
||||
b: string | readonly string[]
|
||||
a: undefined | string | string[],
|
||||
b: undefined | string | string[],
|
||||
): boolean {
|
||||
return Array.isArray(a)
|
||||
? isEquivalentArray(a, b)
|
||||
: Array.isArray(b)
|
||||
? isEquivalentArray(b, a)
|
||||
: a === b;
|
||||
? isEquivalentArray(b, a)
|
||||
: a === b;
|
||||
}
|
||||
|
||||
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';
|
||||
import { type LocationQuery } from '../types';
|
||||
/**
|
||||
* todo: replace to URL API
|
||||
*/
|
||||
|
||||
export function parseURL(location: string) {
|
||||
let path = '';
|
||||
let query: LocationQuery = {};
|
||||
let searchParams: URLSearchParams | undefined;
|
||||
let searchString = '';
|
||||
let hash = '';
|
||||
|
||||
@ -16,12 +16,9 @@ export function parseURL(location: string) {
|
||||
|
||||
if (searchPos > -1) {
|
||||
path = location.slice(0, searchPos);
|
||||
searchString = location.slice(
|
||||
searchPos + 1,
|
||||
hashPos > -1 ? hashPos : location.length
|
||||
);
|
||||
searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
|
||||
|
||||
query = parse(searchString);
|
||||
searchParams = new URLSearchParams(searchString);
|
||||
}
|
||||
|
||||
if (hashPos > -1) {
|
||||
@ -35,16 +32,16 @@ export function parseURL(location: string) {
|
||||
return {
|
||||
fullPath: path + (searchString && '?') + searchString + hash,
|
||||
path,
|
||||
query,
|
||||
searchParams,
|
||||
hash,
|
||||
};
|
||||
}
|
||||
|
||||
export function stringifyURL(location: {
|
||||
path: string;
|
||||
query?: AnyObject;
|
||||
searchParams?: URLSearchParams;
|
||||
hash?: string;
|
||||
}): string {
|
||||
const query: string = location.query ? stringify(location.query) : '';
|
||||
return location.path + (query && '?') + query + (location.hash || '');
|
||||
const searchStr = location.searchParams ? location.searchParams.toString() : '';
|
||||
return location.path + (searchStr && '?') + searchStr + (location.hash || '');
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
}
|
||||
"outDir": "dist",
|
||||
"paths": {
|
||||
"@alilc/*": ["runtime/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"lib": ["es2015", "dom"],
|
||||
"lib": ["DOM", "ESNext", "DOM.Iterable"],
|
||||
// Target latest version of ECMAScript.
|
||||
"target": "esnext",
|
||||
// Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user