npm install rtt --save-dev
最近一段时间在修改自己的个人在线简历. 这个在线简历用到了css3来制作3D的旋转效果, 因此会有兼容性问题, 针对于不支持css3的3D透视的浏览器, 比如 IE, 360等等, 我使用的是另一套css文件兼容. 针对于移动端浏览器, 尽管基本都是webkit内核, 但经测试发现3D效果并不流畅, 因此移动端是识别userAgent切换到另一套非3D页面. 因为没用任何数据库, 那么问题就来了, 移动端和pc端两套页面是共用的一套数据, 我想到的方法有两个: 一是页面加载之后用ajax请求同一个数据文件, 然后js在页面显示的时候填充数据, 但这个方法要增加http请求, 万一碰到需要请求超多数据文件的时候呢? 加载速度自然要受影响. 方法二在html中直接写,这样不会增加多余的请求, 但是万一里面的数据要修改一下呢, 多个页面你去慢慢找吧...... 因此就想实现自己的一套模板替换工具, 像php那样插入, 然后就能直接打印出来. 正好最近在学nodejs, 它可以操作文件, 对于我们jser来说犹如神器! 于是我就想模拟php插入标签方式实现一个简单的模板替换工具rtt ---- replace templete tool, 通过nodejs来操作html文件, 然后替换输出到目标文件
在html插入<%%>类型的标签, 然后里面可以使用js代码:
<div class="face info"> <% for(var i=0, m=data.info.length; i<m; i++){ %> <div class="info-kind"> <h2><%=data.info[i].title%></h2> <p><%=data.info[i].content%></p> </div> <% } %> </div>
替换之后, 生成内容如下:
<div class="face info"> <div class="info-kind"> <h2>工作经历</h2> <p>###########</p> </div> <div class="info-kind"> <h2>兴趣爱好</h2> <p>###########</p> </div> <div class="info-kind"> <h2>英语水平</h2> <p>###########</p> </div> ...... </div>
实现模板替换工具
如何实现一个这种工具? 其实原理非常简单, 就是用运行在node中的js读取文件, 会用到 fs.readFileSync 方法, 然后将得到的字符串用正则进行匹配, 通过<%%>作为定界符, js就可以区分开哪些是直接输出, 哪些是需要经过运算, 最后用fs.writeFileSync方法输出. 因为之前自己实现过一个复杂dom选择器, 所以一下就想到使用正则断开特殊字符, 然后放到一个数组中, 进行遍历.
新建一个rtt.js的文件:
var fs = require('fs'); //获取fs模块 var srcHtml = fs.readFileSync('a.src.html'); //读取a.src.html文件数据 srcHtml = srcHtml.toString('utf-8'); //将读取到的二进制数据转成字符串 srcHtml = srcHtml.replace(/[\r\n\t]+/g, ''); //去掉换行和tab //本可以和上面合并, 方便观看分开写, 去掉html和js注释 srcHtml = srcHtml.replace(/<!--.*?-->|\/\*.*?\*\//g, ''); //将srcHtml用<%%>断开 //如果是 abc<% alert(1); %>def 会输出['abc', '<% alert(1); %>', 'def'] var arr = srcHtml.match(/<%={0,2}.*?%>|.*?(?=<%)|.*?(?=$)/g)
因此得到的arr是类似于 ['<div>', '<% data.name %>', '</div>', '<% data.age %>', '<ul>' ........ ]
最后需要通过eval才能读取<%%>中的js语句, 所以需要设置一个用于eval的s变量, s变量中有res变量最终会被eval出来,在遍历数组的过程中不断迭代, 用于输出
var s = "res='';"; for(var i=0, m=arr.length; i<m; i++){ /*遍历数组的时候, 进行判断, 如果是<%开头的, 则是js语句, 如果不是则直接输出*/ if(arr[i].indexOf('<%') === 0 ){ if(arr[i].charAt(2) === '=' && arr[i].charAt(3) !== '=' ){ //如果是js变量的时候 s += 'res+=' + arr[i] + ';' ; } else { //js语句的时候 s += arr[i]; } } else { s += 'res+="' + arr[i] + '";' ; } } eval(s); //生成res变量, 并执行 fs.writeFileSync(a.html, res); //将res变量输出到目标文件
现在, 一个简单的模板替换工具就完成了, 每次的arr[i]还需要检验, 转义这里就不赘述了, 声明一个函数调用即可.
添加include函数用来引入其他文件内容
上面的模板替换只允许替换变量, 通过数组生成多个指定标签, 但我们经常需要引入一些外部的模块文件, php中的require和include非常好用, 但现在是nodejs.
比如该说a.src.html中引入了m.tpl.html, m.tpl.html又引入了x.tpl.js和y.tpl.html, y.tpl.html中又引入了z.tpl.json.
原理也没那么难,首先将上面的代码封装成一个函数.
/* src --- 源文件路径 dest --- 生成文件路径 dataPath --- json数据路径 dataName --- src文件中的对象名字 delimiter --- 定界符 */ function rtt(src, dest, dataPath, dataName, delimiter){ //设置对象名字默认为dataPath的文件名 dataName = dataName || dataPath.split('/')[dataPath.split('/').length-1].replace(/\.[a-z0-9]+$/gi, ''); //设置定界符默认是<%%>, 然后在中间分隔开保存成['<%', '%>'] delimiter = delimiter || '<%%>'; delimiter = (function(){ var n = Math.floor(delimiter.length/2); return [delimiter.substr(0, n), delimiter.substr(-n)]; })(); //调用replace函数, 最终输出得到替换之后的字符串, 生成到dest文件 //这里大部分替换逻辑已经封装到了replace中, replace只读取源文件生成目标最终字符串 //因为replace中会使用的rtt中的各种变量, 因此使用闭包形式书写 fs.writeFileSync(dest, replace(src)); console.log('compile finished!'); //打印'compile finished' function replace(src){
//传入一个src路径, 返回替换之后的字符串 ...... } }
replace大部分和上上面试一样的, 只是封装成了一个函数, 然后进行了include扩展:
function replace(src){ eval("var "+dataName+" =" + data); var s = "res='';", fs = require("fs"), location = getLocation(src); //读取源文件html, 并得到字符串, 然后将字符串中的\r\n\t var srcHtml = fs.readFileSync(src).toString('utf-8').replace(/[\r\n\t]+/g, ''); //去掉html注释, js注释/**/类型 srcHtml = srcHtml.replace(/<!--.*?-->|\/\*.*?\*\//g, ''); //分割html文件中的字符串 var reg = new RegExp(delimiter[0] + "={0,2}.*?"+delimiter[1]+"|.*?(?="+delimiter[0]+")|.*?(?=$)", "g"); var arr = srcHtml.match(reg); for(var i=0, m=arr.length; i<m; i++){ if(arr[i].indexOf('<%') === 0){ if(arr[i].charAt(2)==='=' && arr[i].charAt(3) !== '=' ){ //js变量的时候 s += 'res+=' + trim(arr[i]) + ';'; } else if(arr[i].charAt(2)==='=' && arr[i].charAt(3) === '='){ //js变量的时候, 如果是两个等号, 则将其中<替换成< 将> 替换成> s += 'res+=' + esHtml(trim(arr[i])) + ';'; } else if(/^<%\s*include\(.*\)\s*;?\s*%>$/.test(arr[i])){ //如果识别到是include('abc.html')类型格式, 则递归调用replace函数, 传入abc.html var newPath = genPath(src, arr[i].replace(/.+['"](.+)['"].+/g, function(a, b){ return b; })); s += 'res+="' + es(replace( newPath )) + '";'; } else{ //js语句的时候 s += trim(arr[i]); } } else{ s += 'res+="' + es(arr[i]) + '";'; } } eval(s); //返回eval得到的变量res return res; }
上面的replace函数中用到了递归来实现多层引用, 但是有个问题, 如果 a.src.html 中引用了 tpl/b.html, tpl/b.html 又引用了 ./css/c.css, 那么路径就会出现问题, 因为被引用的文件中, 引用其他文件的路径是相对于他自己, 而node读取文件的位置是src文件所在的位置, 如图所示:
当node执行到include('css/c.html')的时候, 会根据src文件的位置, 读取/css/c.css文件, 因此会出错, 所以需要使用函数来生成新的相对于src的路径, 才能得到/tpl/css/c.css文件
//传入一个文件路径,返回其所在目录 function getLocation(src){ if(src.charAt(src.length-1) === '/') return src; else if(src.indexOf('/') === 0) return './'; else if(src.indexOf('/') < 0) return './'; else{ var temp = src.split('/'); temp.pop(); return temp.join('/')+'/'; } }
然后还需要生成新路径的函数:
//传入当前文件路径, 和include的文件路径, 生成相对于index的相对路径 function genPath(cur, dest){ if(dest.charAt(0) !== '.' && dest.charAt(0) !== '/'){ return getLocation(cur) + dest.replace(/^\//, ''); } else if(dest.indexOf('./') === 0){ return getLocation(cur) + dest.replace(/^\.\//, ''); } else{ //如果目标路径是 ../../../a/b/c.html类型 var cur = getLocation(cur).match(/[^\/]+\//g); var m = cur.length; var n = dest.match(/\.\.\//g).length; if(m>n){ return cur.slice(0, m-n).join('') + dest.replace(/\.\.\//g, ''); } else if(m === n) return './' + dest.replace(/\.\.\//g, ''); else if(m < n){ for(var res='', i=0; i<n-m; i++) res+='../'; return res + dest.replace(/\.\.\//g, ''); } } }
至此, 一个可以引用其他文件的模板已经完成了, 可以通过npm install rtt --save-dev 进行安装, 然后新建一个replace.js文件, 输入:
var rtt = require('rtt'); rtt('a.src.html' , 'a.html', 'data.json');
就可以讲a.src.html源文件中<%%>包含的使用的data命名空间输出到a.html, 允许多层引入文件, 注意: <%include('');%>
由于本人学习nodejs时间不长, 有任何问题请各位指出, 多谢啦