上上一期連接——也就是本文的基礎,參考KOA,5步手寫一款粗糙的web框架html
上一期連接——有關Router的實現思路,這份Koa的簡易Router手敲指南請收下git
本文參考倉庫:點我github
上一期科普了Router,咱們能夠爲每一張頁面配置一個路由,可是咱們不可能每一個router.get(path,(ctx,next)=>{ctx.body=...})
都直接寫html
,這樣代碼也太難維護了。因而出現了模版這個東西,模版主要是用來管理頁面的。每個html
都放入一個單獨的文件中,這樣不管是調用仍是複用都很方便。這裏我用了ejs的語法,來寫這個模版引擎的中間件。web
那麼,咱們從最簡單的靜態頁面開始吧~正則表達式
調用文件不是一件難事,只須要讀取,而後賦值給ctx.body
便可:數組
const fs=require("fs")
const path=require("path")
let indexTPL=fs.readFileSync(path.join(__dirname,"/pages/template.ejs"),"utf-8")
ctx.body=indexTPL;
複製代碼
這裏我先以邏輯爲主,因此我用了readFileSync
這個同步方法,而沒有用異步讀取的方法。bash
這裏,咱們新建立一個名爲View中間件,專門用於模板嵌套。app
const fs=require("fs")
const path=require("path")
function View(path){
let tpl="";
return async (ctx,next)=>{
tpl = fs.readFileSync(path.join(__dirname,path),"utf-8")
ctx.body= tpl
await next();
}
}
複製代碼
而後咱們就能夠直接在項目中應用這個中間件了。框架
let view=require("./Views")
let router=new Router()
router.get("/",view("/pages/template.ejs"))
複製代碼
或者異步
app.use(view("/pages/template.ejs"))
複製代碼
都是可行的,由於我建立的是標準的中間件啊~
咱們爲何要用模板!固然是爲了動態頁啊!因此咱們須要替換模板標籤<%=參數名%>
爲咱們須要值。同時模板也須要支持一些函數,好比數組循環填充列表。
那麼第一步,咱們須要的就是將這個標籤提取出來,而後替換成咱們特有的標籤<!--operator 1-->
這個能夠自定義一個特別的標籤用於佔位符。
你們沒聽錯,提取,替換!因此正則表達式
是躲不過了,他已經在虐個人路上了……
由於單純的賦值和執行函數差異比較大,因此我把他們分開識別。若是你們有更好的方法,記得推薦給我。(正則渣渣瑟瑟發抖)
let allTags=[];
function getTags(){
//先取出須要執行的函數,也就是不帶"="的一對標籤,放入數組,而且,將執行函數這一塊替換成佔位符。
let operators = tpl.match(/<%(?!=)([\s\S]*?)%>([\s\S]*?)<%(?!=)([\s\S]*?)%>/ig)||[]
operators.forEach((element,index )=> {
tpl=tpl.replace(element,`<!--operator ${index}-->`)
});
//再取出含有「=」的專門的賦值標籤,怕和執行函數中的賦值標籤搞混,因此這邊我分開執行了
let tags=tpl.match(/<%=([\s\S]*?)%>/ig)||[]
tags.forEach((element,index) => {
tpl=tpl.replace(element,`<!--operator ${index+operators.length}-->`)
});
//給我一個整套的待替換數組
allTags=[...operators,...tags];
}
複製代碼
重頭戲來了,如今咱們要進行模板替換了,要換成咱們傳入的值。這裏須要注意的就是咱們將allTags
逐個替換成可執行的js文本,而後執行js,生成的字符串暫存於數組之中。等執行完畢,再將以前的<!--operator 1-->
佔位符替換掉。
這裏須要注意的是,咱們先把賦值的標籤<%=%>
去除,變成${}
,就像下方這樣:
let str="let tmpl=`<p>字符串模板:${test}</p> <ul> <li>for循環</li> <% for(let user of users){ %> <li>${user}</li> <% } %> </ul>` return tmpl"
複製代碼
而後咱們再把可執行的函數的<%%>去除,首尾加上```閉合字符串,就像下方這樣:
let str="let tmpl=`<p>字符串模板:${test}</p> <ul> <li>for循環</li>` for(let user of users){ tmpl+=`<li>${user}</li>` } `</ul>` return tmpl"
複製代碼
可是這是字符串啊,這個時候咱們要藉助一個方法Function 構造函數
咱們能夠new一個Function,而後將字符串變成能夠執行的js。
Function的語法是這樣的new Function ([arg1[, arg2[, ...argN]],] functionBody)
,再字符串以前能夠聲明無數個參數,那麼咱們就藉助...
三個幫咱們把Object
變成單個參數放進去就能夠了。
舉個例子:
let data={
test:"admin",
users:[1,2,3]
}
複製代碼
上方對象,咱們用Object.keys(data)
,提取字段名,而後利用三點擴展運算符...
,變成test,users
new Function(...Object.keys(data),方法字符串)
複製代碼
也就等同於
new Function(test,users,方法字符串)
複製代碼
咱們合併下上方的字符串,這個可執行的模板js就是這樣的,怎麼樣是否是好理解了?
function xxx(test,users){
let tmpl=`<p>字符串模板:${test}</p>
<ul>
<li>for循環</li>`
for(let user of users){
tmpl+=`<li>${user}</li>`
}
`</ul>`
return tmpl;
}
複製代碼
感受要變成可執行的js,原理不難,就是拼合起來很複雜。
下方是完整的執行代碼:
function render(){
//獲取標籤
getTags();
//開始組合每一個標籤中的內容,而後將文本變成可執行的js
allTags=allTags.map((e,i)=>{
let str = `let tmpl=''\r\n`;
str += 'tmpl+=`\r\n';
str += e
//先替換賦值標籤
str = str.replace(/<%=([\s\S]*?)%>/ig,function () {
return '${'+arguments[1]+'}'
})
//再替換函數方法,記得別忘了首位的"`"這個閉合標籤
str = str.replace(/<%([\s\S]*?)%>/ig,function () {
return '`\r\n'+arguments[1] +"\r\ntmpl+=`"
})
str += '`\r\n return tmpl';
//提取object的key值,用於function的參數
let keys=Object.keys(data);
let fnStr = new Function(...keys,str);
return fnStr(...keys.map((k)=>data[k]));
})
allTags.forEach((element,index )=> {
tpl=tpl.replace(`<!--operator ${index}-->`,element)
});
}
複製代碼
將readFile
變成一個Promise
,而後放入中間件中await
一下,這樣就能夠實現異步了~
若是不瞭解async/await,科普傳送門。
const util=require("util")
const fs=require("fs")
const path=require("path")
let readFile=util.promisify(fs.readFile)
function view(p,data){
let tpl="";
let allTags=[];
function getTags(){
//略
}
function render(){
//略
}
return async (ctx,next)=>{
tpl = await readFile(path.join(__dirname,p),"utf-8")
//別忘了運行render(),替換模板標籤
render();
ctx.body=tpl;
await next();
}
}
複製代碼