数据源引擎设计

核心原理

考虑之后的扩展性和兼容性,核心分为了 2 类包,一个是 datasource-engine ,另一个是 datasource-engine-x-handler ,x 的意思其实是对应数据源的 type,比如说 datasource-engine-mtop-handler,也就是说我们会将真正的请求工具放在 handler 里面去处理,engine 在使用的时候由使用方自身来决定需要注册哪些 handler,这样的目的有 2 个,一个是如果将所有的 handler 都放到一个包,对于端上来说这个包过大,有一些浪费资源和损耗性能的问题,另一个是如果有新的类型的数据源出现,只需要按照既定的格式去新增一个对应的 handler 处理器即可,达到了高扩展性的目的;

数据源引擎设计 - 图1

DataSourceEngine

  • engine: engine 主要分 2 类,一类是面向 render 引擎的,可以从 engine/interpret 引入,一类是面向出码或者说直接单纯使用数据源引擎的场景,可以从 engine/runtime 引入,代码如下
  1. import { createInterpret, createRuntime } from '@alilc/lowcode-datasource-engine';

create 方法定义如下

  1. interface IDataSourceEngineFactory {
  2. create(dataSource: DataSource, context: Omit<IRuntimeContext, 'dataSourceMap' | 'reloadDataSource'>, extraConfig?: {
  3. requestHandlersMap: RequestHandlersMap;
  4. [key: string]: any;
  5. }): IDataSourceEngine;
  6. }

create 接收三个参数,第一个是 DataSource,对于运行时渲染和出码来说,DataSource 的定义分别如下:

  1. /**
  2. * 数据源对象--运行时渲染
  3. * @see https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
  4. */
  5. export interface DataSource {
  6. list: DataSourceConfig[];
  7. dataHandler?: JSFunction;
  8. }
  9. /**
  10. * 数据源对象
  11. * @see https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
  12. */
  13. export interface DataSourceConfig {
  14. id: string;
  15. isInit: boolean | JSExpression;
  16. type: string;
  17. requestHandler?: JSFunction;
  18. dataHandler?: JSFunction;
  19. options?: {
  20. uri: string | JSExpression;
  21. params?: JSONObject | JSExpression;
  22. method?: string | JSExpression;
  23. isCors?: boolean | JSExpression;
  24. timeout?: number | JSExpression;
  25. headers?: JSONObject | JSExpression;
  26. [option: string]: CompositeValue;
  27. };
  28. [otherKey: string]: CompositeValue;
  29. }

但是对于出码来说,create 和 DataSource 定义如下:

  1. export interface IRuntimeDataSourceEngineFactory {
  2. create(dataSource: RuntimeDataSource, context: Omit<IRuntimeContext, 'dataSourceMap' | 'reloadDataSource'>, extraConfig?: {
  3. requestHandlersMap: RequestHandlersMap;
  4. [key: string]: any;
  5. }): IDataSourceEngine;
  6. }
  7. export interface RuntimeOptionsConfig {
  8. uri: string;
  9. params?: Record<string, unknown>;
  10. method?: string;
  11. isCors?: boolean;
  12. timeout?: number;
  13. headers?: Record<string, unknown>;
  14. shouldFetch?: () => boolean;
  15. [option: string]: unknown;
  16. }
  17. export declare type RuntimeOptions = () => RuntimeOptionsConfig; // 考虑需要动态获取值的情况,这里在运行时会真正的转为一个 function
  18. export interface RuntimeDataSourceConfig {
  19. id: string;
  20. isInit: boolean;
  21. type: string;
  22. requestHandler?: () => {};
  23. dataHandler: (data: unknown, err?: Error) => {};
  24. options?: RuntimeOptions;
  25. [otherKey: string]: unknown;
  26. }
  27. /**
  28. * 数据源对象
  29. * @see https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
  30. */
  31. export interface RuntimeDataSource {
  32. list: RuntimeDataSourceConfig[];
  33. dataHandler?: (dataMap: DataSourceMap) => void;
  34. }

2 者的区别还是比较明显的,一个是带 js 表达式一类的字符串,另一个是真正转为直接可以运行的 js 代码,对于出码来说,转为可执行的 js 代码的过程是出码自身负责的,对于渲染引擎来说,它只能接受到初始的 schema json 所以需要数据源引擎来做转化

  • context:数据源引擎内部有一些使用了 this 的表达式,这些表达式需要求值的时候依赖上下文,因此需要将当前的上下文丢给数据源引擎,另外在 handler 里面去赋值的时候,也会用到诸如 setState 这种上下文里面的 api,当然,这个是可选的,我们后面再说。
  1. /**
  2. * 运行时上下文--暂时是参考 react,当然可以自己构建,完全没问题
  3. */
  4. export interface IRuntimeContext<TState extends object = Record<string, unknown>> {
  5. /** 当前容器的状态 */
  6. readonly state: TState;
  7. /** 设置状态(浅合并) */
  8. setState(state: Partial<TState>): void;
  9. /** 自定义的方法 */
  10. [customMethod: string]: any;
  11. /** 数据源, key 是数据源的 ID */
  12. dataSourceMap: Record<string, IRuntimeDataSource>;
  13. /** 重新加载所有的数据源 */
  14. reloadDataSource(): Promise<void>;
  15. /** 页面容器 */
  16. readonly page: IRuntimeContext & {
  17. readonly props: Record<string, unknown>;
  18. };
  19. /** 低代码业务组件容器 */
  20. readonly component: IRuntimeContext & {
  21. readonly props: Record<string, unknown>;
  22. };
  23. }
  • extraConfig:这个字段是为了留着扩展用的,除了一个必填的字段 requestHandlersMap
  1. export declare type RequestHandler<T = unknown> = (ds: RuntimeDataSourceConfig, context: IRuntimeContext) => Promise<RequestResult<T>>;
  2. export declare type RequestHandlersMap = Record<string, RequestHandler>;

RequestHandlersMap 是一个把数据源以及对应的数据源 handler 关联起来的桥梁,它的 key 对应的是数据源 DataSourceConfig 的 type,比如 mtop/http/jsonp … ,每个类型的数据源在真正使用的时候会调用对应的 type-handler,并将当前的参数和上下文带给对应的 handler。

create 调用结束后,可以获取到一个 DataSourceEngine 实例

  1. export interface IDataSourceEngine {
  2. /** 数据源, key 是数据源的 ID */
  3. dataSourceMap: Record<string, IRuntimeDataSource>;
  4. /** 重新加载所有的数据源 */
  5. reloadDataSource(): Promise<void>;
  6. }