使用EJS模板自动生成代码

想象一下有这样的一种场景:你的工程中会用到各种动物类来代表每一种动物,所以每次添加新的动物种类的时候你就需要手动写一个动物类。如果只有几个动物还好说,但是如果动物不断增加到几十个甚至是上百个呢?难道你就要重复上百遍地写这种无聊的代码吗?不会偷懒的程序员不是好的程序员,在本文中我们就来研究一下如何使用EJS模板结合NodeJS来批量地自动生成代码,从而解放我们的双手来偷懒。

EJS 是什么

EJS是一套简单的模板语言,可以让开发者使用JavaScript 代码生成 HTML 页面,具有快速开发、语法简单、执行迅速等特点。EJS支持多种标签,在我们的模板中将会主要应用这样的标签<%= EJS %>,标签内的内容会在JS中执行,我们可以在里面添加if等语句,也可以将JS中的变量应用到模板中。更多详细的信息可以进入官网查看,在本文中不再赘述。

如何自动生成代码

下面进入本文的正题,如何来自动生成代码。总体来说我们需要配置一个Json文件、编写EJS模板文件、编写NodeJS程序读入Json和EJS文件并生成代码文件这三步。

Json配置文件

首先我们需要一个Json格式的配置文件,在配置文件中定义好我们需要生成的类以及每个类的一些具体属性配置,如下所示一个Json中我们定义了6种动物,每个动物有名字、腿的数量和是否可以飞三个属性。我们将会遍历所有的动物对象并根据它们的这三个属性来生成具体的动物类。配置文件保存为animals.json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"version": 1,
"animals": [
{
"name": "Dog",
"legs": 4,
"canFly": false,
},
{
"name": "Eagle",
"legs": 2,
"canFly": true,
},
{
"name": "Ant",
"legs": 6,
"canFly": false,
},
{
"name": "Spider",
"legs": 8,
"canFly": false,
},
{
"name": "Whale",
"legs": 0,
"canFly": false,
},
{
"name": "Tuna",
"legs": 0,
"canFly": false,
}
]
}

编写EJS模板文件

在编写EJS文件之前,我们先定义好Animal接口,从而我们生成的动物类都可以实现这个接口。在Animal接口中,我们定义了getLegsNumbercanFly两个方法,生成的动物类将会根据我们之前的配置来实现这两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Animal {
/**
* Get the legs number of this animal.
*
* @return the legs number.
*/

fun getLegsNumber(): Integer

/**
* Whether this animal can fly or not.
*
* @return true if this animal can fly, otherwise false.
*/

fun canFly(): Boolean
}

接下来我们来编写EJS模板文件,在文件的头部我们用<%= %>标签来定义三个属性,其中locals是我们将会传入到模板文件中的动物的Json对象。模板文件的实现和写一个正常的类差不多,需要我们写入类的整体实现并加入注释,但是要将关键部位替换成<%= %>标签应用上前面定义好的属性。如下所示,一个简单的Animal类实现了Animal接口中的两个方法,其返回值替换成我们之前定义好的legscanFly属性,而类名我们用name属性来代替,这样生成的类就会拥有我们在Json中定义的属性。模板文件保存为Animal.ejs文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%
const name = locals.name
const legs = locals.legs
const canFly = locals.canFly
-%>

/**
* Class for animal <%- name %>
*/
class <%- name %>() : Animal {
/**
* Get the legs number of this animal.
*
* @return the legs number.
*/
override fun getLegsNumber(): Integer = <%- legs %>

/**
* Whether this animal can fly or not.
*
* @return true if this animal can fly, otherwise false.
*/
override fun canFly(): Boolean = <%- canFly %>
}
}

编写NodeJS代码

配置文件和模板文件都准备好了,下面就是编写NodeJS代码读取配置文件和模板文件并生成代码文件了。首先我们定义一个writeIfModified方法用来对比新旧代码并将我们新生成的代码写入到指定的文件中。然后读取Json文件到animalCollections中,接下来读取EJS模板模板文件并使用ejs库来编译。最后我们遍历所有的动物对象,将其应用到编译的ejs对象中并生成对应的代码。NodeJS代码保存为animalParser.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const ejs = require('ejs');
global.writeIfModified = function (filename, newContent) {
try {
const oldContent = fs.readFileSync(filename, 'utf8');
if (oldContent == newContent) {
console.warn(`* Skipping file '${filename}' because it is up-to-date`);
return;
}
} catch (err) {
}
if (['0', 'false'].indexOf(process.env.DRY_RUN || '0') !== -1) {
fs.writeFileSync(filename, newContent);
}
console.warn(`* Updating outdated file '${filename}'`);
};

const animalCollections = require('./animals.json');

const animalKotlin = ejs.compile(fs.readFileSync('./Animal.ejs', 'utf8'), {strict: true});

for (const animal of animalCollections.animals) {
writeIfModified(`./generated/${animal.name}.kt`, animalKotlin(animal));
}

生成代码

终于到了最后一步来生成代码了,我们在命令行用运行命令npm install && node animalParser.js,如果没有出错的话我们将会在generated目录下看到生成了六个类文件

1
2
3
4
5
6
Ant
Dog
Eagle
Spider
Tuna
Whale

打开Ant文件来看一下生成的代码,和手动写的代码完全没有区别。如果还需要生成新的类,只需要在配置文件中添加,然后一条命令生成即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Class for animal Ant
*/

class Ant() : Animal {
/**
* Get the legs number of this animal.
*
* @return the legs number.
*/

override fun getLegsNumber(): Integer = 6

/**
* Whether this animal can fly or not.
*
* @return true if this animal can fly, otherwise false.
*/

override fun canFly(): Boolean = false
}

进一步改进

更细致的分类

之前我们所有的类都是直接实现了Animal接口,接下来我们想加入更细致的分类如蚂蚁属于昆虫,狗属于哺乳动物等。所以我们需要在配置文件中加入types的定义:

1
2
3
4
5
6
"types": [
"Mammal",
"Bird",
"Fish",
"Insect"
]

并给每个动物对象添加type属性,如Dog在更新后会拥有4个属性:

1
2
3
4
5
6
{
"name": "Dog",
"legs": 4,
"canFly": false,
"type": "Mammal"
}

不同的种类意味着不同的接口,所以我们需要添加一个新的模板文件来生成对应类型的接口文件,新的接口将会继承Animal接口。这个新的模板文件我们保存为AnimalType.ejs文件

1
2
3
4
5
6
7
<%
const type = locals
-%>
/**
* Interface for animal <%- type %>
*/
interface <%- type %>() : Animal

同时我们还需要更新之前的类模板文件,让新生成的类都实现新的类型接口

1
class <%- name %>() : <%- type %> {

最后我们还是需要将新的模板文件应用到JS程序中,在程序中添加如下的代码来生成对应的接口文件

1
2
3
4
5
6

const animalType = ejs.compile(fs.readFileSync('./AnimalType.ejs', 'utf8'), {strict: true});

for (const type of animalCollections.types) {
writeIfModified(`./generated/types/${type}.kt`, animalType(type));
}

运行代码后我们将会生成4个新的接口文件

1
2
3
4
Bird
Fish

Insect
Mammal

而新生成的类文件也不再直接实现Animal接口,而是实现其所对应的接口

1
class Ant() : Insect {

加入条件判断

虽然我们生成了所有的类文件,但是有时候会有些特殊的业务逻辑需要我们根据一定的条件来生成代码。如我们在Animal接口中加入了一个新的方法返回当前的动物是否可以游泳而且这项属性并没有配置在Json文件中。

1
2
3
4
5
6
/**
* Whether this animal can swim or not.
*
* @return true is this animal can swim, otherwise false.
*/
fun canSwim(): Boolean

已知这六种动物中鱼类和狗、鲸鱼会游泳,其它的不会,所以我们需要在模板文件中加入同样的判断逻辑来生成对应的代码。需要注意的就是所有的判断逻辑代码都需要包含在<%= %>标签中。

1
2
3
4
5
6
7
    override fun canSwim(): Boolean {
<% if (type == 'Fish' || name == 'Whale' || name == 'Dog') { -%>
return true
<% } else { -%>
return false
<% } -%>
}

再次运行生成代码命令,我们可以所有的鱼类和鲸鱼、狗的canSwim方法都会返回 true,而其它的种类将会返回false

总结

在本文中,我们了解了如何使用EJS和NodeJS来自动生成Kotlin代码。不仅仅是Kotlin,同样的方法我们也可以生成 c++、Java、Python等语言的代码,同时我们还可以添加新的模板来生成对应的单元测试代码。
以上就是本文的全部内容,完整的代码可以移步到Github查看。
signature.png