elocity中常常会写出#foreach #if #else #end等语句,
但由于模板文件中html本身就带有缩进,所以最终的缩进,并不符合velocity语句的含义。当主要针对velocity逻辑阅读时,很不方便
没有处理格式代码:
模板代码:
模板代码截图
生成html文件截图:
#if产生缩进后的截图
解决办法:
“#if#foreach#else#end”这些语法,不用任何缩进。并且行尾加##注释,表示不解析后面空格
定格写,末尾加##注释
取消缩进后结果
velocity-wiki地址
https://cwiki.apache.org/confluence/display/velocity/VelocityWhitespaceTruncatedByLineComment
解决办法截图
言
“我不是在教你学坏,而是教你如何提高生产效率。” ----------- 牛顿
人类社会能够一直进步发展出现在的文明世界,最大的一个原因就是这个世界上懒人居多,懒人为了偷懒就需要提高生产效率,效率提高节省下来的时间才能创造出艺术、娱乐以及更高效率的科学技术。程序员们如何提高生产效率?创造一个自己为自己干活!
今天给大家介绍一个代码生成神器Velocity,Velocity作为一款基于Java的强大模板引擎,其拥有简洁的设计和强大的功能,新手也能很快上手。从此以后你就可以摆脱无聊且繁杂的crud代码,给自己留下诗和远方的闲暇时光。
效果展示
话不多说,以下截图是我利用Velocity写的一个代码生成的工具,支持Mysql或Pgsql两种数据库。新项目开发的时候,你只需要设计好你的表结构,在界面上填写包名、地址后缀、模块名等信息即可。剩下的事情交给Velocity,它会帮你完成从controller、service、dao的所有通用接口。
生成代码压缩包里的目录结构,前后端代码都有。
以下是适用于Mybatis框架的Velocity模板生成的代码文件。Velocity的优点之一就是它将生成代码需要的数据模型与模板解耦,所以对模板的修改可以做到非常的丝滑,数据模型做好了基本上不用做大的调整,剩下的就是DIY自己的模板文件。
也许有人会质疑说,idea中不是有很多代码生成插件吗?我认为别人写的总归没有那么灵活,我自己决定使用Velocity也是因为工作中遇到了一个开源项目需要二开,在使用别人的个性化框架期间,一个一个新建Java文件太费鼠标了,所以我才决定自己写一套模板。
Velocity
Velocity的核心理念是遵循Model-View-Controller(MVC)设计模式,它致力于将视图逻辑与业务逻辑彻底分离。它允许前端开发者使用一种专门的语法来引用预定义好的数据模型,而无需直接编写Java代码。这种设计不仅极大地简化了前端开发者的工作量,也使得后端开发者能够专注于后端逻辑的优化与实现,两者并行工作,极大地提升了开发效率。
Maven 依赖如下:
<properties>
<velocity-tools-version>2.0</velocity-tools-version>
</properties>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-tools</artifactId>
<version>${velocity-tools-version}</version>
</dependency>
Velocity原理
Velocity引擎的工作流程大致如下:
Hello Velocity World!
<html>
<body>
#set( $foo = "Velocity" )
Hello $foo World!
</body>
<html>
输出结果:
Hello Velocity World!
代码实际上很简单,声明了一个 foo 的变量,给它赋值 "Velocity", 在 body 中显示 “Hello $foo World! ”,Velocity 会将 foo 变量的值替换为 "Velocity"。
以上就是一个使用 Velocity 模板语言(VTL) 的简单应用。VTL 主要是采用引用的方式将动态的内容嵌入到输出文件里面。
建议点赞+收藏+关注,方便以后复习查阅。
语法
VTL 语法主要分为四种:
注释
单行注释:
## 我是IT果果日记。
多行注释:
#*
我是IT果果日记
我来自东土大唐
要去往西天拜佛求经
*#
引用
引用的简单语法可以写作 ` $+标识符 ` 的格式。标识符必须以字母开头并区分大小写,剩下的标识符由以下几种类型组成:
引用的变量都会处理成字符串对象,例如一个整形对象,Velocity 会调用其 toString() 方法将其转化为字符串。
Velocity 里的引用分为三种类型:
变量的引用:
$itguoguo
属性的引用:
## 这里的属性Money既可以是属性的引用,也可以是getMoney()方法的引用,由Velocity来决定是哪一个引用。
$itguoguo.Money
方法的引用:
## 方法的引用格式:$+标识符+方法体
$itguoguo.getMoney()
$itguoguo.setMoney( "1,0000,0000¥" )
$itguoguo.setWifies( ["刘亦菲", "董璇", "夏思凝"] )
此外还有两种特殊的引用:形式引用(Formal Reference Notation)和安静引用(Quiet Reference Notation)。
形式引用常用于引用与文本相邻的情况。例如,我要显示"I have one billionRMB",我可以写成"I have one $unitRMB"。我的本意是想通过引用变量unit动态的显示RMB的数量,结果因为unit和RMB相邻导致Velocity将unitRMB视作了引用变量。这种情况就可以使用形式引用。像酱紫的 "I have one ${unit}RMB"。
## 形式引用
${itguoguo.getMoney()}
${itguoguo.setMoney( "1,0000,0000¥" )}
${itguoguo.setWifies( ["刘亦菲", "董璇", "夏思凝"] )}
安静引用常用于引用变量不存在的情况。
## 普通引用,Velocity返回值:$money
<input type="text" name="money" value="$money"/>
## 安静引用,Velocity返回空字符串
<input type="text" name="money" value="$!money"/>
控制
If / ElseIf / Else 代码示例
## foo为空或者false,表达式判定为假,不会输出内容
#if( $foo )
<div>Hello ITGuoGuo!</div>
#end
## If / ElseIf / Else
#if( $foo < 5 )
<div>up</div>
#elseif( $foo == 5 )
<div>down</div>
#elseif( $bar == 6 )
<div>left</div>
#else
<div>right</div>
#end
Velocity 将在第一个为真的表达式停止并输出内容。
&& / || / ! 代码示例
#if( $foo && $bar )
<div>逻辑与</div>
#end
#if( $foo || $bar )
<div>逻辑或</div>
#end
#if( !$foo )
<div>逻辑非</div>
#end
逻辑非与安静引用里的 ! 容易混淆,逻辑非的 ! 用在 $ 之前,安静引用的 ! 用在 $ 之后。
foreach 代码示例
## names 可以是一个列表或数组
<ul>
#foreach( $name in $names )
<li>$name</li>
#end
</ul>
## people 可以是一个键值对
<ul>
#foreach( $key in $people.keySet() )
<li>Key: $key -> Value: $people.get($key)</li>
#end
</ul>
## Velocity 还可以在循环里使用计数缺省变量 $velocityCount,从1开始计数。
<table>
#foreach( $person in $people )
<tr><td>$velocityCount</td>
<td>$person.Name</td></tr>
#end
</table>
Velocity 默认为 foreach 指令提供了计数变量 $velocityCount ,从1开始计数。也可以在 velocity.properties 文件中配置计数起始的位置。例如下面的配置:
## 循环计数变量名默认 velocityCount
directive.foreach.counter.name = velocityCount
## 循环计数默认起始位从0开始
directive.foreach.counter.initial.value = 0
包含/解析
include 代码示例
#include( "ItGuoGuo.txt" )
ItGuoGuo.txt 文件将被插入到 #include 指令被定义的位置;
ItGuoGuo.txt 文件不会被 Velocity 解析,如果需要的引入文件被解析可以使用 parse 指令;
include 引入的文件必须是配置参数 TEMPLATE_ROOT 所定义目录下的,默认为当前目录。
## 引入多个文件使用逗号分隔
#include( "ItGuoGuo1.gif","ItGuoGuo2.txt","ItGuoGuo3.htm" )
## 引用文件名最好使用变量,例如 $ItGuoGuoReference
#include( "ItGuoGuo.txt", $ItGuoGuoReference )
parse 代码示例
#parse( "ItGuoGuo.vm" )
ItGuoGuo.vm 将被 Velocity 解析,即 ItGuoGuo.vm 可以是静态文件也可以是动态文件;
parse 指令只有一个参数;
parse 引入的文件必须是配置参数 TEMPLATE_ROOT 所定义目录下的,默认为当前目录。
下面是由两个 vm 文件共同完成的逻辑,首先是主文件 main.vm:
main.vm 倒计时开始
#set( $cnt = 10 )
#parse( "parsed.vm" )
main.vm 倒计时完成!
主文件申明了一个 cnt 变量,将在 parsed.vm 文件中做递减处理:
$cnt
#set( $cnt = $cnt - 1 )
#if( $cnt > 0 )
#parse( "parsed.vm" )
#else
parsed.vm 倒计时完成!
#end
程序首先进入 main.vm 申明一个 cnt 变量,通过引入 parsed.vm 文件递归执行递减操作,当 cnt 变量递减为0时打印 “parsed.vm 倒计时完成!” ,最后回到 main.vm 打印 “main.vm 倒计时完成!”
停止
stop 代码示例
#stop
stop 指令会停止模板引擎的执行,通常用作代码调试。
宏
macro 指令示例
#macro( emptytd )
<tr><td></td></tr>
#end
#emptytd()
定义一个名为 emptytd 的宏,然后执行它,就会显示一行空表格。
#macro( colorrows $color $texts )
#foreach( $text in $texts )
<tr><td bgcolor=$color>$text</td></tr>
#end
#end
定义一个名为 colorrows 的宏,第一个参数表示颜色,第二个参数表示表格里的内容。
#set( $list = ["one","two","three","four","five"] )
#set( $color = "red" )
<table>
#tablerows( $color $list )
</table>
调用 colorrows 这个宏,传递两个参数,注意变量 $list 替换了 $texts,输出如下:
#set( $list = ["one","two","three","four","five"] )
#set( $color = "red" )
<table>
#tablerows( $color $list )
</table>
宏一般用来在多个模板中共享,这样可以减少模板内的重复工作量,也减少了出错的机率。
转义
Velocity 的指令使用 $ 和 # 开头,如果在模板里需要使用这两个特殊字符,需要做转义处理。
I have $1000000000!
I have $money!
00000000 不需要做转义,因为引用变量名必须是大写或这小写字母开头。$money 是否需要转义要看你想输出 money 这个变量还是 “money” 这个字符串。
#set( $money = "1,000,000,000" )
I have $money!
I have \$money!
输出结果如下:
I have 1,000,000,000!
I have $money!
还有一种情况是引用变量的值需要转义,可以使用 Velocity 的扩展工具 EscapeTool,Maven 依赖如下:
<dependency>
<groupId>org.apache.velocity.tools</groupId>
<artifactId>velocity-tools-generic</artifactId>
<version>3.1</version>
</dependency>
代码生成
前面提到过 Velocity 生成代码的原理是将数据模型和模板合并,输出指定格式的文件。下面我们就来自己动手利用 Velocity 写一个生成代码的小工具。
数据模型
首先需要定义自己的数据模型,因为我们写的这个工具是用来生成 crud 代码的,所以数据模型里的字段主要就是表的元信息。表的元信息实体类如下:
列的元信息实体类如下:
定义好数据模型之后,我们需要调用数据库查找这些信息。以 Mysql 为例:
模板文件
多个模板文件可以共用一套数据模型,当前我们以生成实体类文件为例,实体模板如下:
合并生成
最后是将数据模型合并到模板文件,生成代码文件:
参考
velocity中文文档 https://wizardforcel.gitbooks.io/velocity-doc/content/1.html
建议点赞+收藏+关注,方便以后复习查阅。
#头条创作挑战赛#
导读
近期一次需求开发涉及到了Java Veloctiy,由于当前vue项目无法在本地编译运行velocity,且开发成本过高,如果需要调试则要发布后才能看到效果。之前有类似的案例,遂尝试继续采用这种方式解决,但是通过比对认为该方案迁移到本项目中的成本较高,且需要改动大量配置文件。
基于以上,我们便尝试将veloctiy本地工程化,尝试跟vue和webpack的结合,实现热更等开发常用需求。
本文把velocity工程化的心路历程记录下来,主要给大家描述我们在解决问题过程中的一些感悟,同时文中也详细介绍了具体方案。
2
背景
本文提到的“Velocity”指的是Java Velocity,后面全部以“velocity”代称。
本文不对Velocity基础概念做详细介绍,默认阅读本文的读者具备velocity基础相关知识。如果确有不清楚的地方,可依据参考文献提供的文档或搜索引擎进一步了解。
3
现状
随着前端技术的发展,页面的开发逐步形成了前后端分离的开发模式,形成了以vue、react等主流前端框架为主的MVVM模式。
回首再面对这些基于Velocity的旧系统,无论是后端还是前端人员维护,都会存在诸多问题:
1. RD不熟悉前端开发模式,需要花大量时间学习前端js和框架。
2. Velocity渲染依赖Java环境,FE需要花费大量精力学习Maven工程、环境配置,且前端MVC框架版本老,开发效率低。
原有项目涉及面较广,基于Velocity开发的情况分散在不同业务系统中,随着业务需求的迭代,维护成本越来越高。解决方案一般是针对高频迭代的业务模块进行前后端分离开发,但这样会带来新的问题:
1. 使用新项目重构原有项目成本高
2. 覆盖面不广
3. 周期长
4. 重构本身不能为业务带来更高的价值
如下图,我们在本地开发velocity模版页面时,目前基本处于盲写阶段,不能即时查看,造成开发时间大量浪费在部署和联调中。
基于以上问题,我们团队在维护的时候探索一种新的方式,旨在提升开发效率,解决本地编译运行velocity模版。
4
实现
以我们团队维护的本地服务大类页为例。
该项目基于vue,近期一次需求迭代中,需要把部分页面内容前置到服务端交由java预渲染,然而本地开发目前不支持模拟java 环境运行velocity模版。因此需把velocity和vue进行整合,降低开发成本,让FE无痛开发,不论是编译还是打包上线都跟普通vue项目无差异。
基于此目标,需要实现一个velocity+mvc+mvvm的混合架构,该架构需具备两个能力:
1. 把velocity模版页面进行本地渲染,然后跟vue的index.html模版页面进行整合
2.让vue把dom挂载到整合后的模版页面中
现在思考第一个问题,如何实现上面的架构?回顾刚才我们描述的架构需要具备的能力,可以分为两段:
第一段是velocity+mvc,上线后由服务端执行
第二段是mvvm,上线后由客户端执行
问题进一步明确,基于以上两段,我们需要做的事可以分为3步:
1. 本地实现一套velocity模版渲染引擎,实现velocity页面的渲染和mock数据填充
2. 把渲染好的velocity模版整合到vue的模版文件index.html中
3. 把整合了velocity页面的vue模版文件index.html交给vue进行dom挂载
好了,到此,我们不再停留在理论层面,而是需要考虑怎么实现以上三步。由于后面步骤依赖前面步骤的产物,所以我们按顺序实现。
现在思考第二个问题,velocity渲染引擎应该具备什么能力?
根据第一步的描述,该引擎需要在前端本地编译运行velocity模版页面,同时支持mock数据的渲染。
总结一下就是两点:1 渲染velocity 2 支持mock数据
目前本地渲染velocity的主流方式是在本地运行一套java服务,但是当前需求开发伙伴对java不太了解,并且一些配置项对我们来说较为复杂,这与我们在现状中描述的问题不谋而合,这条路不适合我们。换个思路,java能跑服务,js不能吗?答案很明显,可以,是nodejs。虽然对java不熟悉,但是nodejs我们熟啊。
通过调研,发现nodejs有类似java velocity的模版引擎,能运行velocity模版,同时能支持mock数据。接下来尝试把该引擎接入项目中。
下面是接入引擎时尝试的两种方式:
方法一:在vue执行入口文件index.js前启动node服务,然后通过在node服务中使用引擎提供的编译和运行方法把velocity模版页面加载好供后续使用。
方法二:把该引擎写在webpack的loader中,随着webpack的执行逻辑编译velocity。
如果大家对vue本地运行时各个页面加载的顺序有了解,应该能看出来方法一存在问题,我们逐个实现,看看问题在哪里。
以下代码为当时在本地调试时临时编写,仅用于帮助大家理解思路。不具有参考性。核心代码会加注释。
step1: 改造vue入口文件index.js
// setup中启动node服务,编译velocity并生成一个html文件到指定文件夹下
import './setup';
// entry是原有vue的index.js入口文件内容
import './entry';
step2: 实现setup.js
import Axios from 'axios';
// 该方法向本地启动的node服务发送请求,告诉node服务需要重新编译模版
(() => {
new Promise((resolve, reject) => {
Axios.get('initdom')
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
});
})();
step3: 实现node服务
// node服务在本地手动启动
// 这里需要在全局安装velocityjs包,用于后续命令执行
const http = require('http');
const fs = require('fs');
const { exec } = require('child_process');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
if (req.url === '/initdom') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
// 该命令用于执行velocity模版页面,并且生成html文件到执行目录下
exec('velocity ./index.vm', { encoding: 'utf-8' }, (error, stdout, stderr) => {
if (error) {
console.error(error);
}
if (stderr) {
console.error(stderr);
}
const isExist = fs.existsSync('模版文件地址');
if (isExist) {
console.log('file is exist');
fs.rmSync('生成html文件地址');
}
console.log('file is rewriting');
fs.writeFileSync('生成html文件地址', stdout);
console.log('file is rewrited');
// html文件生成成功后,通知前端,执行后续逻辑
res.end('模版编译成功');
});
} else {
console.log('url is not correct!!!');
}
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
step4: 本地启动node服务和vue项目
// 启动node服务
node node.js
// 启动vue
npm run dev
至此,方法一整个流程实现,项目启动后确实也符合我们的预期,但是前文提到的问题在哪里?大家知道,我们平时本地开发中很重要的一个需求是热更新。当我改动mock数据后发现,生成的html文件中的mock数据确实变动了,但是我需要刷新两次浏览器才能在页面中体现出来,也就是说热更生效了,但又没完全生效。
问题在于vue读取html文件的时机早于html文件生成的时机,那么能把html文件生成的逻辑再前置吗?基于方法一很明显不行,而且方法一需要本地启动node服务,通过请求接口的方式实现服务启动,这很不优雅,因此考虑换个方式。
我们知道webpack提供了自定义loader的机制,允许我们创建loader预处理指定的文件。
vue cli支持通过webpack chain添加新的loader。
回顾方法一遇到的问题,尝试用webpack loader实现。
方法二具体实现:
下图为整体流程图:
实现方法二之前我们再思考一个问题,随着项目逐步迭代,可能会对velocity模版文件进行拆分,同时一些外部文件也需要注入到最后的html文件中,而且后续还会逐步把代码进行升级。好消息是velocity提供了#parse和#include能力,配合script和api接口我们能够实现velocity+vue的单文件组件化模式。
下图为文件基本结构:
基于这个结构,我们搭建一套前端沙盒环境,通过在本地json文件中保存mock服务端的数据,然后开发velocity-loader引擎来解析velocity模版,嵌入到webpack loader中,实时解析文件并注入mock数据到模版中,解决方法一中遇到的热更问题。如下图。
下面贴上方法二实现的相关代码。
step1: 配置webpack chain
config.module.rule('velocity')
// 只处理.vm结尾的文件
.test(/.vm$/)
.exclude.add(/node_modules/).end()
.use('html-loader')
.loader('html-loader')
.end()
.use('velocity-loader')
.loader(path.resolve(__dirname, './v-loader.js'))
.options({
basePath: path.join(__dirname, 'src'),
});
step2: 实现velocity-loader引擎
这里只写部分核心代码,其他的代码为其他配置项,跟本文关系不大。
module.exports = function (content) {
const callback = this.async();
const filePath = this.resourcePath;
const fileDirPath = path.dirname(filePath);
watcher = this.addDependency;
// mock文件名称这里写死,用于描述,实际可进行配置
const mockPath = path.join(fileDirPath, 'mock.js');
let mock = {};
// 判断mock文件是否存在,如果存在则进行监听,实现热更
if (fs.existsSync(mockPath)) {
watcher(mockPath);
delete require.cache[mockPath];
// eslint-disable-next-line global-require
mock = require(mockPath);
}
// 使用compile解析velocity模版
content = new Compile(parse(content), {
escape: false,
}).render(mock, macros(filePath, {}, mock));
// 返回html内容给webpack,进行下一步处理
callback(null, content);
};
至此,我们解决了本地编译运行velocity模版的问题,也实现了mock数据和热更,提升了开发效率和开发体验,维护成本并没有增加。
5
总结
其实到现在关于本地编译运行velocity已经有很多方案且大都成熟,只是我们要么直接使用了现有的方案,要么没遇到这种情况,写这篇文章的目的也是想把自己在解决问题时的思路描述出来,做个总结。
参考文献
[1] velocity官网:https://velocity.apache.org/
[2] velocityjs官网: https://github.com/shepherdwind/velocity.js
[3] webpack 官网:https://www.webpackjs.com/loaders/
[4] vue官网:https://cli.vuejs.org/zh/guide/webpack.html
作者简介
雷鸣生:LBG-FE
来源:微信公众号:58技术
出处:https://mp.weixin.qq.com/s/LqBf6H-6o88tMfl59IRPbw
*请认真填写需求信息,我们会在24小时内与您取得联系。