KOA的簡易模板引擎實現方式

小美娜娜發表於2018-08-29

上上一期連結——也就是本文的基礎,參考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();
    }
}
複製程式碼

相關文章