背景

一个应用随着业务的发展往往会变得越来越复杂和臃肿,不同应用之间又往往存在不同维度的共性。组件化能很好的解决上面的问题:

  • 通过将复杂和臃肿的应用横向拆分成一个个业务组件
  • 通过把存在共性的不同应用纵向提炼抽象成一个个基础组件

另外,Malagu 框架本身也是一个特别复杂的项目,Malagu 框架同样可以基于自身的组件机制来实现,让 Malagu 框架更具活力。

目标

  • 可配置性
  • 可扩展性
  • 可编排性
  • 简单易用性
  • 可积累性
  • 可分享性

组件内部结构

组件内部结构图 (2).svg

说明:

  • 长方形表示组件属性,包含前端属性、后端属性和公共属性
  • 红色三角形表示前端代码
  • 蓝色三角形表示后端代码
  • 红色和蓝色三角形交集橙色三角形表示公共代码
  • 前端代码可以访问公共代码、公共属性和前端属性
  • 后端代码可以访问公共代码、公共属性和后端属性
  • 公共代码只能访问公共属性

组件目录结构

下面是一个前后端一体化组件的代码目录结构,组件也是一个标准 node 模块,在根目录可以放置组件属性配置文件,如下面的 malagu.yml 文件,模式属性配置文件也是放在根目录位置。源代码目录划分成三个目录:前端代码目录(browser)、公共代码目录(common)和后端代码目录(node)。您可以更加具体情况,增删目录,如果组件是一个纯后端组件,browser 和 common 目录可以不需要。组件目录结构中的源代码目录不会强制您按照下面的方式来组织,只不过官方提供的基础组件会按照下面的方式来组织,当然,如果需要开发自己的组件,框架推荐使用与我们相同的组织方式。

  1. .
  2. ├── malagu.yml # 组件配置【可选】
  3. ├── package.json
  4. ├── src
  5. ├── browser # 前端代码目录【可选】
  6. ├── application-lifecycle.ts
  7. ├── module.ts
  8. └── user.view.tsx
  9. ├── common # 公共代码目录【可选】
  10. ├── index.ts
  11. └── user-protocol.ts
  12. └── node # 后端代码目录【可选】
  13. ├── entity
  14. ├── index.ts
  15. └── user.ts
  16. ├── module.ts
  17. └── user-service.ts
  18. └── tsconfig.json

组件形态

组件包含四种种形态:

  • 前端组件【运行时】
  • 后端组件【运行时】
  • 一体化组件【运行时】
  • 命令行组件【编译时】

其中运行时组件,如下图所示:

组件形态.svg

组件之间关系

组件之间关系.svg

说明:

  • 组件之间存在依赖关系
  • 组件之间的依赖关系决定了组件的加载顺序和组件配置优先级,被依赖的组件先于依赖者加载且配置属性优先级低

组件属性

配置方式

组件属性通过 yaml 文件来配置,默认在组件项目的根目录下加载 malagu.yml 属性文件,当 malagu.yml 配置了属性 mode: test ,则尝试加载根目录的 malagu-test.yml 属性文件,规则是: malagu-[mode].yml

配置文件分类如下:

  • 公共属性文件:malagu.yml
  • 模式属性文件:malagu-[mode].yml

模式属性

模式属性,在特定模式下才会生效的属性。模式与环境的关系是多对多的关系,即一个环境对应着一个或多个模式,不同环境之间可以复用同一个的模式。

模式的本质是告诉框架如何加载合并组件的配置文件,以及配置文件的加载顺序(优先加载的属性文件的属性优先级越低)。

有两种方式指定模式

  1. 属性文件中配置 mode
  2. 命令行指定选项 --mode,-m ,支持模式的命令有:
    1. malagu serve
    2. malagu build
    3. malagu deploy

示例一:通过属性文件配置

  1. # 指定单个模式,该模式下,会尝试加载所有组件的 malagu.yml 和 malagu-prod.yml
  2. mode: prod
  3. #指定多模式,该模式下,会尝试加载所有组件的 malagu.yml、malagu-prod1.yml 和 malagu-prod2.yml
  4. mode: [prod1, prod2]

示例二:通过命令行选项配置

  1. # 指定单个模式,该模式下,会尝试加载所有组件的 malagu.yml 和 malagu-prod.yml
  2. malagu deploy -m prod
  3. #指定多模式,该模式下,会尝试加载所有组件的 malagu.yml、malagu-prod1.yml 和 malagu-prod2
  4. malagu deploy -m prod1,prod2

当即通过属性文件配置,又通过命令行选项配置时,框架将两者配置进行合并,命令行选项配置的模式优先级比属性文件配置高。

模式属性优先级规则

  • malagu.yml < malagu-[mode].yml
  • 多模式情况下,写在前面的模式优先级低,例如模式为 prod1,prod2,则 prod2 的属性优先级大于 prod1 的属性
  • 属性文件配置的模式 < 命令行选项配置的模式

属性划分

在属性文件中可以配置:

  • 公共属性
  • 前端属性
  • 后端属性

其中, frontend 下面的都是前端属性; backend 下面的都是后端属性,其他的为公共属性。

  1. 前端属性示例
  1. frontend:
  2. malagu:
  3. foo: bar
  1. 后端属性示例
  1. backend:
  2. malagu:
  3. foo: bar
  1. 公共属性示例
  1. malagu:
  2. foo: bar

还有一种维度的划分:

  • 编译时属性
  • 运行时属性

属性优先级规则

泛化原则

泛化的属性优先级低(以下规则优先级由高到底):

  • 前端(后端)属性 > 公共属性
  • 模式属性文件 > 公共属性文件

依赖原则

被依赖的组件属性优先级低:

  • 依赖者属性 > 被依赖者属性

当以上两个原则存在冲突时:

  • 泛化原则 > 依赖原则

属性规范

  • 前后端代码都需要访问的属性设计成公共属性
  • 公共代码只访问公共属性
  • 属性层级设计:[业务名].[组件名].[属性]
  • 属性命名规则:采用小驼峰(推荐)
  • 属性最好提供默认值

属性模板变量

属性值可以使用表达式来引用其他的属性值或者环境变量值。

  1. 引用其他属性值 ```yaml port: 3000 host: localhost url: ‘https://${host}:${port}
  1. 2. 引用环境变量值
  2. ```yaml
  3. password: ${env.PASSWORD}
  1. 默认值设置

    1. password: '${env.PASSWORD?:123456}'
  2. 忽略表达式计算

如下属性配置,因为 test 节点存在属性 _ignoreEl 为 true,则 test 节点下的属性或者属性的属性的值不在进行表达式计算。

  1. test:
  2. a: '${b > 0 ? true: false}'
  3. _ignoreEl: true

上面的方案是对某个一个节点进行表达式计算忽略,如何只对单一属性值就行表达式忽略呢?我们可以通过转义符实现。如下:

  1. test:
  2. a: '\\${b > 0 ? true: false}'
  1. 运行时表达式

上面的写法是在项目编译期间计算表达式的值,如果您需要在应用运行时取运行时的环境变量的话,如下:

  1. password: '${{env.PASSWORD?:123456}}'
  1. 运行时正则表达式配置

属性文件不太方便配置正则表达式值,框架特意为此提供专门的转换函数,方便开发者在属性文件中配置正则表达式,如下:

  1. origin: ${{'.*\.google\.com$'|regexp}}

表达式更多语法规则请看:Jexl

组件使用

一个组件就是一个 npm 包,组件的使用与 npm 包的使用是一样的。但是,对两个平级的组件而言, package.json 中的依赖顺序会决定组件的加载顺序。

组件加载

Malagu 推荐使用容器来管理对象之间的依赖,也正是把对象放到容器中托管,才让组件具备极强的扩展性,让组件适用更多的场景。对于一个 Malagu 的应用,可能有一个或者多个组件组成,想让应用能够正常跑起来,就必须按照合理的加载顺序,分别加载组件中需要托管到容器的对象。

组件加载机制如下图:

组件加载.svg

从上图可以看出,组件的对象在 module 中声明后,只需要把组件按依赖关系的进行拓扑排序,被依赖的组件优先加载,平级组件按照先后顺序加载,容器按照拓扑排序后的顺序加载 module ,最终构建出整个完成的容器。

组件扩展

扩展方法

自定义一个组件,且依赖需要被扩展的组件,通过属性覆盖、实现扩展接口和直接替换组件内部实现来扩展或改变组件的行为。

扩展策略

  • 属性扩展,基于组件属性优先级规则
  • 接口扩展,组件将可变的逻辑抽象为接口,子组件通过实现该接口达到扩展组件的目的
  • 实现扩展,组件内的任何实现(实现没有抽象成接口),子组件都可以通过 rebind 的方式替换掉

扩展能力来源

  • 组件属性优先级规则
  • 组件的代码实现都托管于容器(IOC)