开始
简介
Ecode 是一个代码生成器,用于生成业务代码,让用户只专注于业务逻辑本身,而不用理会关于依赖引用和一些细枝末节的语法处理,最终得到一个完整且干净的应用。
Ecode主要利用TS面向对象的语法特性作为编译器前端,在此基础上,你可以使用for循环、模块导入导出等手段实现代码的复用。
该方案等侵入型非常低,它的最小编译单位是组件,因此可以在不改变项目架构的情况下使用。
目前Ecode仅支持Angular工程代码的生成。
快速上手
安装
npm i -g ecodecc
# or
yarn add ecodecc --global
# or
pnpm add -g ecodecc
配置文件
配置文件目前仅支持默认读取,无法指定,因此命名必须为ecode.config.js
。编译运行会以配置文件为参照路径,因此需要在配置文件中指定入口和出口(日后支持更多配置),目前配置文件支持的选项不多,仅支持入口和出口:
// ecode.config.js
module.exports = {
entry: './views',
output: './src/app/pages',
};
入口一般指一个文件夹,这个文件夹中的所有文件,最终都会按照一定的依赖规则,编译至output
所指定的文件夹中。
书写第一个模块
// views/first.ts
import { Module, Component } from 'ecode/dist/render'
export default new Module({
id: 'hello',
children: [
new Component({
id: 'member',
children: []
})
]
})
编译
编译命令是 ecode go [path]
, path是可选参数,指向 ecode.config.js
,默认指向命令行的执行目录。
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FirstRoutingModule } from './first-routing.module'
import { MemberComponent } from './member.component'
@NgModule({
imports: [FirstRoutingModule, CommonModule],
declarations: [MemberComponent],
exports: [],
providers: []
})
export class FirstModule {}
封装一个简单的组件
这是一个真实的、用于业务上的组件类,它仅仅是对于antd的分割线组件进行简单的二次封装,根据antd的官方文档可知,要使用divider组件,必须引用NzDividerModule,并注册到我们工程的相关module中。
分割线的使用很直观,也不存在嵌套的关系,因此,children只需要传一个空数组即可。在Render函数中也仅仅需要返回几个最简单的属性,其中imports属性会用于构建依赖关系。
import { Render } from 'ecode/dist/render';
export class Line extends Render {
constructor() {
super({ children: [] });
}
render() {
return {
type: 'element',
imports: [
{
target: [{ name: 'NzDividerModule', type: 'module' }],
from: 'ng-zorro-antd/divider',
},
],
template: `<nz-divider></nz-divider>`,
data: [],
methods: [],
};
}
}
生成后的文件
生成的文件中包含Angular模块最基础的几个部分(上述例子中未必会完整体现)包含:
- 引用;如NgModule、CommonModule等Angular Module必须依赖的模块。
- 注册;该模块下辖的Component、Service等部件,并合理地注册。
使用
一个完整组件创建和使用的过程如下:
- 新建文件,引入Render基类。
- 封装出一个元素类。
- 在类的constructor函数中接收你希望传入的几类API,有几个API是约定俗成的,如id、children等。将这些必要的数据传入super函数中,其他API可以由用户自定义其行为,例如放置在模板中或将其渲染成数据。
基础概念
为了增强业务表达能力,需要介绍一下几个基本概念。
组件的实例化
组件需要实例化后才能够使用,每个组件都是一个类,每个类会带有自己的方法,这些方法主要是为了针对性地减少信息量。
在封装一个类时,需要让它继承Render类,Render类中已经包含了几种常见的基本逻辑,例如自动维护事件和回调函数之间的联系。
在使用业务中使用这个组件时,尽可能通过实例方法或静态方法来使用。
为什么除了实例方法外还需要静态方法?
因为在实际业务中,有可能会出现组件逻辑相互引用的场景,如果组件未能合理地初始化,使用指令时会报错,因此为了让前端语法得以运行,有需要的话,需要有静态方法作为弥补。
指令
指令是操作组件的方式,一般建议设计得比较贴合人类的直观感受:
-
如我们通过指令唤起一个弹框:Modal.wakeUp()
-
如我们通过指令清空一个表单:Form.reset()
-
如我们通过指令发起一个请求:httpS.send('foo')
指令中会包含完整的逻辑,如非空判断、错误判断等。
Callback数据结构
Callback数据结构专注于描述业务逻辑;
在形式上,它是一个数组,一般而言是一维的,根据需要也可以是多维的;
Callback中的每一个元素,要么是指令(这些指令在编译时会运行并返回字符串),要么是字符串,当然,字符串一般仅用于弥补指令能力的不足,不是推荐的做法。
由于指令生成的代码会可能有重复的部分,因而在部分场景下,可能需要将其封闭在一个作用域内,这时可以使用中括号[]
将一部分逻辑约束在一个block内。
引入
ecode既包含一个编译器,也包含多个导出的API。这些API的使用方式是基本是一致的,都是通过实例化后传入父元素的children字段中,只是每个元素由于复杂度和职能不同,在实例化时传入的参数不一致,幸好TS能够给予比较详细的类型提示,因此不需要死记硬背。
ecode仅提供了几个重要且基本的类,内部封装了自身所需要的逻辑关系,但仅仅是这样还不足以应付业务,因此在业务中需要基于Render类自行封装。接下来一一介绍:
Module类
建议作为最小生成单位来编译,即在一个文件中导出一个Module实例。
在实例化时,它需要设置两个属性
- id
- children;它直属的
children
成员类型需要是Module
或Component
Component 类
尽管建议将Module作为最小的编译单位,但Component也可以作为最小的编译单位,即一个文件中导出一个Component实例,之所以不推荐是因为如此一来,就需要用户手动处理与Component相关的依赖关系了。举个例子:一个UI组件foo需要依赖一个fooModule的模块,用户需要手动将fooModule注册到父Module文件中,生成后的组件才能够正常地使用。
在实例化时,它需要设置多个属性:
- id
- imports
- children
- init
Render 类
我们一般不会直接使用render类,但会使用继承Render类来使用,当然,需要保持必要的一致性:
首先,由于它继承自Render类,因而它需要有一个constructor函数并包含一个super方法,向super传入正确的属性。
其次,需要包含一个render方法,该方法返回一个对象,包含以下几个属性:
- type;用于声明当前的组件类型
- imports;是个对象数组,含有target、from
- template; 用于
- data;
- methods;
导出
嵌套
几个简单的例子
Canvas
Line
Button
自定义组件
Service
# import
@Component({
selector: '# name',
template: `# template`
})
export class #nameComponent {
# data
# create
constructor(# inject) {
# data
# create
}
ngOnInit() {
# init
}
#methods
}