vscode外掛liveserver增加對thymeleaf模板的簡單支援
背景
vscode+liveserver開發時,多個頁面引用的公用靜態資源在每個頁面都寫一個遍比較麻煩,想讓liveserver支援簡單的thymeleaf語法,只要能把公用資源抽出來單獨放到一個檔案中宣告即可。
網上找了一下,沒有現成的功能,為方便自己使用,修改了一個liveserver外掛。
其它人也可能會遇到同樣的需求,這裡把程式碼貼出來
實用方式
只有兩個簡單的js檔案,同時簡單修改一下liveserver外掛的index.js檔案即可
liveserver外掛的位置:
C:\Users\*\.vscode\extensions\ritwickdey.liveserver-5.7.9\node_modules\live-server
(具體路徑,由於機器配置以及使用的liveserver版不同,可能不一樣)
- 把
thymeleaf-handler.js
和thymeleaf-parser.js
拷到外掛目錄中的live-server
目錄中 - 修改
index.js
檔案
liveserver目錄結構:
專案結構
include.html
- env.txt 用於放置環境變裡,在頁面中可使用th:text來引用
- include.html 用於放置公共引用,僅支援th:fragment
index.html
env.txt
輸出到瀏覽器的結果
原始碼
//index.js
var fs = require('fs'),
connect = require('connect'),
serveIndex = require('serve-index'),
logger = require('morgan'),
WebSocket = require('faye-websocket'),
path = require('path'),
url = require('url'),
http = require('http'),
send = require('send'),
open = require('opn'),
es = require("event-stream"),
os = require('os'),
chokidar = require('chokidar'),
httpProxy = require('http-proxy'),
HandlerStream = require('./thymeleaf-handler');//這裡增加一行對thymeleaf-handler的引用
......
//在後面找到inject函式
function inject(stream) {
if (injectTag) {
// We need to modify the length given to browser
var len = GET_INJECTED_CODE().length + res.getHeader('Content-Length');
res.setHeader('Content-Length', len);
var originalPipe = stream.pipe;
stream.pipe = function (resp) {
originalPipe.call(stream, es.replace(new RegExp(injectTag, "i"), GET_INJECTED_CODE() + injectTag))
.pipe(new HandlerStream(root,res)).pipe(resp);// 修改這一句,把原來的 .pipe(resp) 修改為 .pipe(new HandlerStream(root,res)).pipe(resp);
};
}
}
以下為thymeleaf-handler.js
和thymeleaf-parser.js
的原始碼
//thymeleaf-handler
const fs = require('fs');
const path = require('path')
const stream = require('stream');
let thymeleafParser = require("./thymeleaf-parser");
class HandlerStream extends stream.Transform{
constructor (root,res) {
super();
this.root = root;
this.body = "";
this.res = res;
this.byteLen = 0;
}
_parse(){
return thymeleafParser(this.root,this.body);
}
_transform (chunk, encoding, callback){
this.body += chunk;
this.byteLen += chunk.byteLength;
callback()
}
_getBufferLen(v){
const buf = Buffer.from(v, 'utf8');
return buf.length;
}
_flush (callback){
let newBoday = this._parse();
let newLen = this._getBufferLen(newBoday);
var len = (newLen - this.byteLen) + this.res.getHeader('Content-Length');
this.res.setHeader('Content-Length', len);
this.push(newBoday);
callback();
}
}
module.exports = HandlerStream;
//thymeleaf-parser
const fs = require('fs');
const path = require('path');
class Fragment{
constructor(name,content){
this.paramName = null;
this.name = null;
this.decodeName(name);
//替換對上下檔案的引用
this.content = content.replace("[[@{/}]]","/");
let parser = new Parser(this.content);
parser.parseSrc()
.parseHref();
this.content = parser.value;
}
decodeName(name){
let r = /"?(.+?)\((.+)\)"?/;
let m = r.exec(name);
if(m){
this.name = m[1];
this.paramName = m[2];
}
else if(/"(.+)"/.test(name)){
this.name = name.slice(1,-1);
}
else
this.name = name;
}
getContent(param){
if(param && this.paramName){
return this.content.replace(`\${${this.paramName}}`,param)
}
else
return this.content;
}
}
class Parser{
constructor(value){
this.value = value;
}
parseSrc(){
this.value = this.value.replace(/th:src="@\{(.+?)\}"/g,'src="$1"');
return this;
}
parseHref(){
this.value = this.value.replace(/th:href="@\{(.+?)\}"/g,'href="$1"');
return this;
}
parseText(env){
let reg = /<(div|a|input|span|button|p|title)(.*?) th:text="(.+?)"(.*?)><\/\1>/g;
let textBlocks = [];
let m = reg.exec(this.value);
while(m){
m[0];
m[2];
textBlocks.push({
tagContent: m[0],
tagName: m[1],
attrs:[m[2],m[4]],
value: m[3]
});
m = reg.exec(this.value);
}
reg = /\$\{(.+)\}/;
for(let b of textBlocks){
m = reg.exec(b.value);
if(m && env.getValue(m[1])){
b.value = env.getValue(m[1]);
}
let tag = `<${b.tagName}${b.attrs[0]}${b.attrs[1]}>${b.value}</${b.tagName}>`;
this.value = this.value.replace(b.tagContent,tag);
}
return this;
}
parseBlock(fragments){
function removeBrackets(v){
if(!v) return v;
let m = /\('(.+)'\)/.exec(v);
if(m){
return m[1];
}
else{
return "";
}
}
//<th:block th:include="include :: header('示例')"/>
let reg = /<th:block th:include="include\s*::\s*([a-zA-z0-9_]+?)(\(.+\))?"\s*\/>/g;
let blocks = [];
let m = reg.exec(this.value);
while(m){
blocks.push({
tag: m[0],
name: m[1],
param: removeBrackets(m[2])
});
m = reg.exec(this.value);
}
for(let block of blocks){
let fragment = fragments[block.name];
if(fragment){
this.value = this.value.replace(block.tag,fragment.getContent(block.param));
}
}
return this;
}
}
class Evn{
constructor(){
this.values = {};
}
load(root){
let envString = readFile(path.resolve(root,"env.txt"))
let lines = envString.split('\n');
let r = /(.+?)=(.+)/;
for(let l of lines){
l = l.trim();
if(l.startsWith("#") || !l) continue;
let m = r.exec(l);
if(m){
this.values[m[1].trim()] = m[2].trim();
}
}
}
getValue(key){
if(this.values[key]){
return this.values[key];
}
else
return null;
}
}
function parseTemplate(template){{
let fragmentReg = /<(div|head|section)\s+th:fragment=(.*?)>(.*?)<\/\1>/gs;
let fragments = {};
let m = fragmentReg.exec(template);
while(m){
let fragment = new Fragment(m[2],m[3]);
fragments[fragment.name] = fragment;
m = fragmentReg.exec(template);
}
return fragments;
}}
function readFile(fileName){
if(fs.existsSync(fileName)){
return fs.readFileSync(fileName).toString("utf8");
}
else
return "";
}
function readTemplate(root){
return readFile(path.resolve(root,"include.html"));
}
function parse(root,html){
let fragments = parseTemplate(readTemplate(root));
let env = new Evn();
env.load(root);
let parser = new Parser(html);
parser.parseSrc()
.parseHref()
.parseBlock(fragments)
.parseText(env);
return parser.value;
}
module.exports = parse