- A+
所属分类:Web前端
vscode插件liveserver增加对thymeleaf模板的简单支持
背景
vscode+liveserver开发时,多个页面引用的公用静态资源在每个页面都写一个遍比较麻烦,想让liveserver支持简单的thymeleaf语法,只要能把公用资源抽出来单独放到一个文件中声明即可。
网上找了一下,没有现成的功能,为方便自己使用,修改了一个liveserver插件。
其它人也可能会遇到同样的需求,这里把代码贴出来
实用方式
只有两个简单的js文件,同时简单修改一下liveserver插件的index.js文件即可
liveserver插件的位置:
C:Users*.vscodeextensionsritwickdey.liveserver-5.7.9node_moduleslive-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="includes*::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