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

上上一期連接——也就是本文的基礎,參考KOA,5步手寫一款粗糙的web框架html

上一期連接——有關Router的實現思路,這份Koa的簡易Router手敲指南請收下git

本文參考倉庫:點我github

上一期科普了Router,咱們能夠爲每一張頁面配置一個路由,可是咱們不可能每一個router.get(path,(ctx,next)=>{ctx.body=...})都直接寫html,這樣代碼也太難維護了。因而出現了模版這個東西,模版主要是用來管理頁面的。每個html都放入一個單獨的文件中,這樣不管是調用仍是複用都很方便。這裏我用了ejs的語法,來寫這個模版引擎的中間件。web

那麼,咱們從最簡單的靜態頁面開始吧~正則表達式

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這個同步方法,而沒有用異步讀取的方法。bash

STEP 2 封裝一箇中間件View

這裏,咱們新建立一個名爲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"))
複製代碼

都是可行的,由於我建立的是標準的中間件啊~

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();
    }
}
複製代碼
相關文章
相關標籤/搜索