基于JavaScript寫(xiě)一款EJS模板引擎
1. 起因
部門(mén)最近的一次分享中,有人提出來(lái)要實(shí)現(xiàn)一個(gè)ejs模板引擎,突然發(fā)現(xiàn)之前似乎從來(lái)都沒(méi)有考慮過(guò)這個(gè)問(wèn)題,一直都是直接拿過(guò)來(lái)用的。那就動(dòng)手實(shí)現(xiàn)一下吧。本文主要介紹ejs
的簡(jiǎn)單使用,并非全部實(shí)現(xiàn),其中涉及到options
配置的部分直接省略了。如有不對(duì)請(qǐng)指出,最后歡迎點(diǎn)贊 + 收藏。
2. 基本語(yǔ)法實(shí)現(xiàn)
定義render
函數(shù),接收html
字符串,和data
參數(shù)。
const render = (ejs = '', data = {}) => { }
事例模板字符串如下:
<body> ? ? <div><%= name %></div> ? ? <div><%= age %></div> </body>
可以使用正則將<%= name %>
匹配出來(lái),只保留name
。這里借助ES6的模板字符串。將name用${}包裹起來(lái)。
props中第2個(gè)值就是匹配到的變量。直接props[1]替換。
[ ? '<%= name %>', ? ' name ', ? 16, ? '<body>\n ? ?<div><%= name %></div>\n ? ?<div><%= age %></div>\n</body>' ]
const render = (ejs = '', data = {}) => { ? ? const html = ejs.replace(/<%=(.*?)%>/g, (...props) => { ? ? ? ? return '${' + props[1] + '}'; ? ? ? ? // return data[props[1].trim()]; ? ? }); }
3. Function函數(shù)
這里得到的html是一個(gè)模板字符串??梢酝ㄟ^(guò)Function
將字符串編程可執(zhí)行的函數(shù)。當(dāng)然這里也可以使用eval,隨你。
<body> ? ? <div>${ name }</div> ? ? <div>${ age }</div> </body>
Function
是一個(gè)構(gòu)造函數(shù),實(shí)例化后返回一個(gè)真正的函數(shù),構(gòu)造函數(shù)的最后一個(gè)參數(shù)是函數(shù)體的字符串,前面的參數(shù)都為形式參數(shù)。比如這里傳入形參name,函數(shù)體通過(guò)console.log
打印一句話(huà)。
const func = new Function('name', 'console.log("我是通過(guò)Function構(gòu)建的函數(shù),我叫:" + name)'); // 執(zhí)行函數(shù),傳入?yún)?shù) func('yindong'); // 我是通過(guò)Function構(gòu)建的函數(shù),我叫:yindong
利用Function
的能力可以將html模板字符串執(zhí)行返回。函數(shù)字符串編寫(xiě)return,返回一個(gè)拼裝好的模板字符串、
const getHtml = (html, data) => { ? ? const func = new Function('data', `return \`${html}\`;`); ? ? return func(data); ? ? // return eval(`((data) => { ?return \`${html}\`; })(data)`) } const render = (ejs = '', data = {}) => { ? ? const html = ejs.replace(/<%=(.*?)%>/g, (...props) => { ? ? ? ? return '${' + props[1] + '}'; ? ? }); ? ? return getHtml(html, data); }
4 with
這里render函數(shù)中props[1]的實(shí)際上是變量名稱(chēng),也就是name和age,可以替換成data[props[1].trim()],不過(guò)這樣寫(xiě)會(huì)有一些問(wèn)題,偷個(gè)懶利用with代碼塊的特性。
with語(yǔ)句用于擴(kuò)展一個(gè)語(yǔ)句的作用域鏈。換句人話(huà)來(lái)說(shuō)就是在with語(yǔ)句中使用的變量都會(huì)先在with中尋找,找不到才會(huì)向上尋找。
比如這里定義一個(gè)age數(shù)字和data對(duì)象,data中包含一個(gè)name字符串。with包裹的代碼塊中輸出的name會(huì)先在data中尋找,age在data中并不存在,則會(huì)向上尋找。當(dāng)然這個(gè)特性也是一個(gè)with不推薦使用的原因,因?yàn)椴淮_定with語(yǔ)句中出現(xiàn)的變量是否是data中。
const age = 18; const data = { ? ? name: 'yindong' } with(data) { ? ? console.log(name); ? ? console.log(age); }
這里使用with
改造一下getHtml
函數(shù)。函數(shù)體用with包裹起來(lái),data就是傳入的參數(shù)data,這樣with體中的所有使用的變量都從data中查找了。
const getHtml = (html, data) => { ? ? const func = new Function('data', `with(data) { return \`${html}\`; }`); ? ? return func(data); ? ? // return eval(`((data) => { with(data) { return \`${html}\`; } })(data)`) } const render = (ejs = '', data = {}) => { ? ? // 優(yōu)化一下代碼,直接用$1替代props[1]; ? ? // const html = ejs.replace(/<%=(.*?)%>/g, (...props) => { ? ? // ? ? return '${' + props[1] + '}'; ? ? // }); ? ? const html = ejs.replace(/<%=(.*?)%>/gi, '${$1}'); ? ? return getHtml(html, data); }
這樣就可以打印出真是的html了。
<body> ? ? <div>yindong</div> ? ? <div>18</div> </body>
5. ejs語(yǔ)句
這里擴(kuò)展一下ejs,加上一個(gè)arr.join語(yǔ)句。
<body> ? ? <div><%= name %></div> ? ? <div><%= age %></div> ? ? <div><%= arr.join('--') %></div> </body>
const data = { ? ? name: "yindong", ? ? age: 18, ? ? arr: [1, 2, 3, 4] } const html = fs.readFileSync('./html.ejs', 'utf-8'); const getHtml = (html, data) => { ? ? const func = new Function('data', ` with(data) { return \`${html}\`; }`); ? ? return func(data); } const render = (ejs = '', data = {}) => { ? ? const html = html = ejs.replace(/<%=(.*?)%>/gi, '${$1}'); ? ? return getHtml(html, data); } const result = render(html, data); console.log(result);
可以發(fā)現(xiàn)ejs也是可以正常編譯的。因?yàn)槟0遄址С謅rr.join語(yǔ)法,輸出:
<body> ? ? <div>yindong</div> ? ? <div>18</div> ? ? <div>1--2--3--4</div> </body>
如果ejs中包含forEach語(yǔ)句,就比較復(fù)雜了。此時(shí)render
函數(shù)就無(wú)法正常解析。
<body> ? ? <div><%= name %></div> ? ? <div><%= age %></div> ? ? <% arr.forEach((item) => {%> ? ? ? ? <div><%= item %></div> ? ? <%})%> </body>
這里分兩步來(lái)處理。仔細(xì)觀察可以發(fā)現(xiàn),使用變量值得方式存在=號(hào),而語(yǔ)句是沒(méi)有=號(hào)的??梢詫?duì)ejs字符串進(jìn)行第一步處理,將<%=變量替換成對(duì)應(yīng)的變量,也就是原本的render
函數(shù)代碼不變。
const render = (ejs = '', data = {}) => { ? ? const html = ejs.replace(/<%=(.*?)%>/gi, '${$1}'); ? ? console.log(html); }
<body> ? ? <div>${ name }</div> ? ? <div>${ age }</div> ? ? <% arr.forEach((item) => {%> ? ? ? ? <div>${ item }</div> ? ? <%})%> </body>
第二步比較繞一點(diǎn),可以將上面的字符串處理成多個(gè)字符串拼接。簡(jiǎn)單舉例,將a加上arr.forEach
的結(jié)果再加上c轉(zhuǎn)換為,str存儲(chǔ)a,再拼接arr.forEach
每項(xiàng)結(jié)果,再拼接c。這樣就可以獲得正確的字符串了。
// 原始字符串 retrun ` ? ? a ? ? <% arr.forEach((item) => {%> ? ? ? ? item ? ? <%})%> ? ? c ` // 拼接后的 let str; str = `a`; arr.forEach((item) => { ? ? str += item; }); str += c; return str;
在第一步的結(jié)果上使用/<%(.*?)%>/g
正則匹配出<%%>中間的內(nèi)容,也就是第二步。
const render = (ejs = '', data = {}) => { ? ? // 第一步 ? ? let html = ejs.replace(/<%=(.*?)%>/gi, '${$1}'); ? ? // 第二步 ? ? html = html.replace(/<%(.*?)%>/g, (...props) => { ? ? ? ? return '`\r\n' + props[1] + '\r\n str += `'; ? ? }); ? ? console.log(html); }
替換后得到的字符串長(zhǎng)成這個(gè)樣子。
<body> ? ? <div>${ name }</div> ? ? <div>${ age }</div> ? ? ` ?arr.forEach((item) => { ?str += ` ? ? ? ? <div>${ item }</div> ? ? ` }) ?str += ` </body>
添加換行會(huì)更容易看一些??梢园l(fā)現(xiàn),第一部分是缺少首部`的字符串,第二部分是用str存儲(chǔ)了forEach
循環(huán)內(nèi)容的完整js部分,并且可執(zhí)行。第三部分是缺少尾部`的字符串。
<body> ? ? <div>${ name }</div> ? ? <div>${ age }</div> ? ? ` // 第二部分 ?arr.forEach((item) => { ?str += ` ? ? ? ? <div>${ item }</div> ? ? ` }) // 第三部分 ?str += ` </body>
處理一下將字符串補(bǔ)齊,在第一部分添加let str = `,這樣就是一個(gè)完整的字串了,第二部分不需要處理,會(huì)再第一部分基礎(chǔ)上拼接上第二部分的執(zhí)行結(jié)果,第三部分需要在結(jié)尾出拼接`; return str; 也就是補(bǔ)齊尾部的模板字符串,并且通過(guò)return返回str完整字符串。
// 第一部分 let str = `<body> ? ? <div>${ name }</div> ? ? <div>${ age }</div> ? ? ` // 第二部分 ?arr.forEach((item) => { ?str += ` ? ? ? ? <div>${ item }</div> ? ? ` }) // 第三部分 ?str += ` </body> `; return str;
這部分邏輯可以在getHtml
函數(shù)中添加,首先在with中定義str用于存儲(chǔ)第一部分的字符串,尾部通過(guò)return返回str字符串。
const getHtml = (html, data) => { ? ? const func = new Function('data', ` with(data) { let str = \`${html}\`; return str; }`); ? ? return func(data); }
這樣就可以實(shí)現(xiàn)執(zhí)行ejs語(yǔ)句了。
const data = { ? ? name: "yindong", ? ? age: 18, ? ? arr: [1, 2, 3, 4], ? ? html: '<div>html</div>', ? ? escape: '<div>escape</div>' } const html = fs.readFileSync('./html.ejs', 'utf-8'); const getHtml = (html, data) => { ? ? const func = new Function('data', ` with(data) { var str = \`${html}\`; return str; }`); ? ? return func(data); } const render = (ejs = '', data = {}) => { ? ? // 替換所有變量 ? ? let html = ejs.replace(/<%=(.*?)%>/gi, '${$1}'); ? ? // 拼接字符串 ? ? html = html.replace(/<%(.*?)%>/g, (...props) => { ? ? ? ? return '`\r\n' + props[1] + '\r\n str += `'; ? ? }); ? ? return getHtml(html, data); } const result = render(html, data); console.log(result);
輸出結(jié)果:
<body>
<div>yindong</div>
<div>18</div><div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</body>
6. 標(biāo)簽轉(zhuǎn)義
<%=會(huì)對(duì)傳入的html進(jìn)行轉(zhuǎn)義,這里編寫(xiě)一個(gè)escapeHTML轉(zhuǎn)義函數(shù)。
const escapeHTML = (str) => { ? ? if (typeof str === 'string') { ? ? ? ? return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ").replace(/"/g, """).replace(/'/g, "'"); ? ? } else { ? ? ? ? return str; ? ? } }
變量替換的時(shí)候使用escapeHTML
函數(shù)處理變量。這里通過(guò)\s*去掉空格。為了避免命名沖突,這里將escapeHTML
改造成自執(zhí)行函數(shù),函數(shù)參數(shù)為$1變量名。
const render = (ejs = '', data = {}) => { ? ? // 替換轉(zhuǎn)移變量 ? ? // let html = ejs.replace(/<%=\s*(.*?)\s*%>/gi, '${escapeHTML($1)}'); ? ? let html = ejs.replace(/<%=\s*(.*?)\s*%>/gi, `\${ ? ? ? ? ((str) => { ? ? ? ? ? ? if (typeof str === 'string') { ? ? ? ? ? ? ? ? return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ").replace(/"/g, """).replace(/'/g, "'"); ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? return str; ? ? ? ? ? ? } ? ? ? ? })($1) ? ? }`); ? ? // 拼接字符串 ? ? html = html.replace(/<%(.*?)%>/g, (...props) => { ? ? ? ? return '`\r\n' + props[1] + '\r\n str += `'; ? ? }); ? ? return getHtml(html, data); }
getHtml
函數(shù)不變。
const getHtml = (html, data) => { ? ? const func = new Function('data', `with(data) { var str = \`${html}\`; return str; }`); ? ? return func(data); }
<%-會(huì)保留原本格式輸出,只需要再加一條不使用escapeHTML
函數(shù)處理的就可以了。
const render = (ejs = '', data = {}) => { ? ? // 替換轉(zhuǎn)義變量 ? ? let html = ejs.replace(/<%=\s*(.*?)\s*%>/gi, '${escapeHTML($1)}'); ? ? // 替換其余變量 ? ? html = html.replace(/<%-(.*?)%>/gi, '${$1}'); ? ? // 拼接字符串 ? ? html = html.replace(/<%(.*?)%>/g, (...props) => { ? ? ? ? return '`\r\n' + props[1] + '\r\n str += `'; ? ? }); ? ? return getHtml(html, data, escapeHTML); }
輸出樣式:
<body>
<div>yindong</div>
<div>18</div><div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div><div>escapeHTML</div></div>
</body>
至此一個(gè)簡(jiǎn)單的ejs模板解釋器就寫(xiě)完了。
到此這篇關(guān)于基于JavaScript寫(xiě)一款EJS模板引擎的文章就介紹到這了,更多相關(guān)寫(xiě)一款EJS模板引擎內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- node+express+ejs制作簡(jiǎn)單頁(yè)面上手指南
- Node.js的路由、EJS模板引擎、GET和POST請(qǐng)求講解
- koa2使用ejs和nunjucks作為模板引擎的使用
- 詳解在express站點(diǎn)中使用ejs模板引擎
- node+express+ejs使用模版引擎做的一個(gè)示例demo
- Node.js的Web模板引擎ejs的入門(mén)使用教程
- node.js 使用ejs模板引擎時(shí)后綴換成.html
- Express 框架中使用 EJS 模板引擎并結(jié)合 silly-datetime 庫(kù)進(jìn)行日期格式化的實(shí)現(xiàn)方法
相關(guān)文章
Firefox+FireBug使JQuery的學(xué)習(xí)更加輕松愉快
FireBug是FireFox下最強(qiáng)大的調(diào)試插件.利用它,可以讓JQuery的學(xué)習(xí)過(guò)程更加輕松愉快.2010-01-01JS判斷頁(yè)面是否出現(xiàn)滾動(dòng)條的方法
這篇文章主要介紹了JS判斷頁(yè)面是否出現(xiàn)滾動(dòng)條的方法,涉及javascript針對(duì)頁(yè)面元素的讀取與判定實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07JavaScript事件學(xué)習(xí)小結(jié)(二)js事件處理程序
這篇文章主要介紹了JavaScript事件學(xué)習(xí)小結(jié)(二)js事件處理程序的相關(guān)資料,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06javascript時(shí)間戳和日期字符串相互轉(zhuǎn)換代碼(超簡(jiǎn)單)
下面小編就為大家?guī)?lái)一篇javascript時(shí)間戳和日期字符串相互轉(zhuǎn)換代碼(超簡(jiǎn)單)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-06-06學(xué)習(xí)javascript面向?qū)ο?理解javascript原型和原型鏈
這篇文章主要介紹了javascript原型和原型鏈,學(xué)習(xí)javascript面向?qū)ο?,感興趣的小伙伴們可以參考一下2016-01-01用file標(biāo)簽實(shí)現(xiàn)多圖文件上傳預(yù)覽
本文介紹了用file標(biāo)簽實(shí)現(xiàn)多圖文件上傳預(yù)覽的方法。具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-02-02