上上一期連結——也就是本文的基礎,參考KOA,5步手寫一款粗糙的web框架
上一期連結——有關Router的實現思路,這份Koa的簡易Router手敲指南請收下
本文參考倉庫:點我
上一期科普了Router,我們可以為每一張頁面配置一個路由,但是我們不可能每個router.get(path,(ctx,next)=>{ctx.body=...})
都直接寫html
,這樣程式碼也太難維護了。於是出現了模版這個東西,模版主要是用來管理頁面的。每一個html
都放入一個單獨的檔案中,這樣無論是呼叫還是複用都很方便。這裡我用了ejs的語法,來寫這個模版引擎的中介軟體。
那麼,我們從最簡單的靜態頁面開始吧~
STEP 1 靜態頁面呼叫
呼叫檔案不是一件難事,只需要讀取,然後賦值給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
這個同步方法,而沒有用非同步讀取的方法。
STEP 2 封裝一箇中介軟體View
這裡,我們新建立一個名為View中介軟體,專門用於模板巢狀。
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"))
複製程式碼
都是可行的,因為我建立的是標準的中介軟體啊~
STEP 3 提取模板標籤
我們為什麼要用模板!當然是為了動態頁啊!所以我們需要替換模板標籤<%=引數名%>
為我們需要值。同時模板也需要支援一些函式,比如陣列迴圈填充列表。
那麼第一步,我們需要的就是將這個標籤提取出來,然後替換成我們特有的標籤<!--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];
}
複製程式碼
STEP 4 替換模板標籤
重頭戲來了,現在我們要進行模板替換了,要換成我們傳入的值。這裡需要注意的就是我們將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)
});
}
複製程式碼
STEP + 如果想用非同步的方式讀取檔案,我推薦:
將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();
}
}
複製程式碼