基於JavaScript寫一款EJS模板引擎

1. 起因

部門最近的一次分享中,有人提出來要實現一個ejs模板引擎,突然發現之前似乎從來都沒有考慮過這個問題,一直都是直接拿過來用的。那就動手實現一下吧。本文主要介紹ejs的簡單使用,並非全部實現,其中涉及到options配置的部分直接省略瞭。如有不對請指出,最後歡迎點贊 + 收藏。

2. 基本語法實現

定義render函數,接收html字符串,和data參數。

const render = (ejs = '', data = {}) => {

}

事例模板字符串如下:

<body>
    <div><%= name %></div>
    <div><%= age %></div>
</body>

可以使用正則將<%= name %>匹配出來,隻保留name。這裡借助ES6的模板字符串。將name用${}包裹起來。

props中第2個值就是匹配到的變量。直接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函數

這裡得到的html是一個模板字符串。可以通過Function將字符串編程可執行的函數。當然這裡也可以使用eval,隨你。

<body>
    <div>${ name }</div>
    <div>${ age }</div>
</body>

Function是一個構造函數,實例化後返回一個真正的函數,構造函數的最後一個參數是函數體的字符串,前面的參數都為形式參數。比如這裡傳入形參name,函數體通過console.log打印一句話。

const func = new Function('name', 'console.log("我是通過Function構建的函數,我叫:" + name)');
// 執行函數,傳入參數
func('yindong'); // 我是通過Function構建的函數,我叫:yindong

利用Function的能力可以將html模板字符串執行返回。函數字符串編寫return,返回一個拼裝好的模板字符串、

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函數中props[1]的實際上是變量名稱,也就是name和age,可以替換成data[props[1].trim()],不過這樣寫會有一些問題,偷個懶利用with代碼塊的特性。

with語句用於擴展一個語句的作用域鏈。換句人話來說就是在with語句中使用的變量都會先在with中尋找,找不到才會向上尋找。

比如這裡定義一個age數字和data對象,data中包含一個name字符串。with包裹的代碼塊中輸出的name會先在data中尋找,age在data中並不存在,則會向上尋找。當然這個特性也是一個with不推薦使用的原因,因為不確定with語句中出現的變量是否是data中。

const age = 18;
const data = {
    name: 'yindong'
}

with(data) {
    console.log(name);
    console.log(age);
}

這裡使用with改造一下getHtml函數。函數體用with包裹起來,data就是傳入的參數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 = {}) => {
    // 優化一下代碼,直接用$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語句

這裡擴展一下ejs,加上一個arr.join語句。

<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);

可以發現ejs也是可以正常編譯的。因為模板字符串支持arr.join語法,輸出:

<body>
    <div>yindong</div>
    <div>18</div>
    <div>1--2--3--4</div>
</body>

如果ejs中包含forEach語句,就比較復雜瞭。此時render函數就無法正常解析。

<body>
    <div><%= name %></div>
    <div><%= age %></div>
    <% arr.forEach((item) => {%>
        <div><%= item %></div>
    <%})%>
</body>

這裡分兩步來處理。仔細觀察可以發現,使用變量值得方式存在=號,而語句是沒有=號的。可以對ejs字符串進行第一步處理,將<%=變量替換成對應的變量,也就是原本的render函數代碼不變。

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>

第二步比較繞一點,可以將上面的字符串處理成多個字符串拼接。簡單舉例,將a加上arr.forEach的結果再加上c轉換為,str存儲a,再拼接arr.forEach每項結果,再拼接c。這樣就可以獲得正確的字符串瞭。

// 原始字符串
retrun `
    a
    <% arr.forEach((item) => {%>
        item
    <%})%>
    c
`
// 拼接後的
let str;
str = `a`;

arr.forEach((item) => {
    str += item;
});

str += c;

return str;

在第一步的結果上使用/<%(.*?)%>/g正則匹配出<%%>中間的內容,也就是第二步。

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);
}

替換後得到的字符串長成這個樣子。

<body>
    <div>${ name }</div>
    <div>${ age }</div>
    `
 arr.forEach((item) => {
 str += `
        <div>${ item }</div>
    `
})
 str += `
</body>

添加換行會更容易看一些。可以發現,第一部分是缺少首部`的字符串,第二部分是用str存儲瞭forEach循環內容的完整js部分,並且可執行。第三部分是缺少尾部`的字符串。

<body>
    <div>${ name }</div>
    <div>${ age }</div>
    `

// 第二部分
 arr.forEach((item) => {
 str += `
        <div>${ item }</div>
    `
})

// 第三部分
 str += `
</body>

處理一下將字符串補齊,在第一部分添加let str = `,這樣就是一個完整的字串瞭,第二部分不需要處理,會再第一部分基礎上拼接上第二部分的執行結果,第三部分需要在結尾出拼接`; return str; 也就是補齊尾部的模板字符串,並且通過return返回str完整字符串。

// 第一部分

let str = `<body>
    <div>${ name }</div>
    <div>${ age }</div>
    `

// 第二部分
 arr.forEach((item) => {
 str += `
        <div>${ item }</div>
    `
})

// 第三部分
 str += `
</body>
`;

return str;

這部分邏輯可以在getHtml函數中添加,首先在with中定義str用於存儲第一部分的字符串,尾部通過return返回str字符串。

const getHtml = (html, data) => {
    const func = new Function('data', ` with(data) { let str = \`${html}\`; return str; }`);
    return func(data);
}

這樣就可以實現執行ejs語句瞭。

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);

輸出結果:

<body>
    <div>yindong</div>
    <div>18</div>

        <div>1</div>

        <div>2</div>

        <div>3</div>

        <div>4</div>

</body>

6. 標簽轉義

<%=會對傳入的html進行轉義,這裡編寫一個escapeHTML轉義函數。

const escapeHTML = (str) => {
    if (typeof str === 'string') {
        return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/ /g, "&nbsp;").replace(/"/g, """).replace(/'/g, "'");
    } else {
        return str;
    }
}

變量替換的時候使用escapeHTML函數處理變量。這裡通過\s*去掉空格。為瞭避免命名沖突,這裡將escapeHTML改造成自執行函數,函數參數為$1變量名。

const render = (ejs = '', data = {}) => {
    // 替換轉移變量
    // 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/ /g, "&nbsp;").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函數不變。

const getHtml = (html, data) => {
    const func = new Function('data', `with(data) { var str = \`${html}\`; return str; }`);
    return func(data);
}

<%-會保留原本格式輸出,隻需要再加一條不使用escapeHTML函數處理的就可以瞭。

const render = (ejs = '', data = {}) => {
    // 替換轉義變量
    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>&lt;div&gt;escapeHTML&lt;/div&gt;</div>
</body>

至此一個簡單的ejs模板解釋器就寫完瞭。

到此這篇關於基於JavaScript寫一款EJS模板引擎的文章就介紹到這瞭,更多相關寫一款EJS模板引擎內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: