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小時內與您取得聯系。