Js literal 模板編譯
編譯的工作是用來簡化書寫過程,或是進行一些常用功能的封裝,以利開發者使用。
模板編譯主要是讓我們能夠在字串內,進行一些變數替換、判斷的運作,實作上常用正則表達式去進行比對處理,而比對後的處理就是我們本篇的關注重點。
簡單編譯
從簡單開始,用最直覺的比對替換來進行:
function template(str, data) {
let ret = String(str);
// 遍歷 key,替換字串
for (let item in data) {
if (data.hasOwnProperty(item)) {
var re = new RegExp('{{' + item + '}}', 'g');
ret = ret.replace(re, data[item]);
}
}
return ret;
}
這個方式可以應用於一些簡單的場合,但當需要更複雜的功能時,就無法使用了。
new Function 函數
對於上面的正則比對編譯,其實 ES6 本身的 literal template 就可以完美地做到了:
function template(data) {
return `Hello ${data.name}, I am ${data.age} years old.`;
}
而這麽做就可以支援上面做不到的 data.user.name
這種串聯寫法。
現在我們知道函數拿來處理字串很方便,而 JavaScript 本身有一個函數物件叫做 Function
正好適合用來做這件事, 他的基本用法如下:
const fn = new Function('x', 'y', 'return x + y');
new Function
可以用來創造函數,最後面接受一個字串,進行 return 動作。我們可以善用此特點來處理比對到的資料進行替換。
const template = function(str) {
const re = /\{\{\s*([^\}]+)?\s*\}\}/g;
str = str.replace(re, 'data.$1');
return new Function('data', `return "${str}";`);
};
t('Hello {{ name }}'); // 函數會回傳 return "Hello data.name"
很明顯這樣會變成整個字串回傳,我們需要進行區分,修改如下:
const template = function(str) {
const re = /\{\{\s*([^\}]+)?\s*\}\}/g;
str = str.replace(re, '" + data.$1 + "');
return new Function('data', `return "${str}";`);
};
t('Hello {{ name }}'); // return "Hello " + data.name + "";
OK!! 到這裡後已經完成 80% 了,目前編譯字串要先手動執行一次 template
函數,可以把他封裝如下自動化:
const template = function(str) {
const re = /\{\{\s*([^\}]+)?\s*\}\}/g;
str = str.replace(re, '" + data.$1 + "');
return new Function('data', `return "${str}";`);
};
const render = function(str, data) {
str = String(str);
let fn = template(str);
return fn(data);
};
render('Hello {{ name }}', { name: 'Johnny' });
// Hello Johnny
進階字符串處理
最後為這個純字符串替換函數加上一些功能,包含資料缺失處理、比對 emps 輸出
資料缺失
當資料缺失時,上面的函數會直接顯示 undefined 於畫面中,可以在 replace
替換時進行判斷:
const template = function(str) {
const re = /\{\{\s*([^\}]+)?\s*\}\}/g;
str = str.replace(re, '" + (data.$1 ? data.$1 : '') + "');
return new Function('data', `return "${str}";`);
};
// ...
現在資料缺失,就會直接返回空白了。
比對 emps 輸出
為了知道總共編譯了多少個對象,且對象的 key 分別是誰,我們需要在編譯後取得相關的資料,實作如下:
const removeWrapper = function(arr) {
let ret = [];
arr.forEach((exp) => {
ret.push(exp.replace(/[\{|\}]/g, '').trim());
});
return ret;
};
const render = function(str, data) {
str = String(str);
let exps = null;
const template = function(str) {
const re = /\{\{\s*([^\}]+)?\s*\}\}/g;
exps = removeWrapper(str.match(re)); // 提取符合的字串後移除大括號
str = str.replace(re, '" + data.$1 + "');
return new Function('data', `return "${str}";`);
};
let fn = template(str);
return {
exps,
value: fn(data)
};
};
render('Hello {{ name }}', { name: 'Johnny' });
/*
{
exps: ['name'],
value: 'Hello Johnny'
}
*/