Builder设计模式的目的就是将一个复杂对象的构建与表示分离,使同样的构建过程可以有不同的表示。
例如有一个XML文件解析程序,一种需求可能将一个xml文件转换成json格式的数据,而另一个需求需要将xml文件转换成csv文本形式。一种可能的实现方式就是实现一个独立的xml文本解析器,解析器从前往后解析文本,遇到不同的标签格式时通知特定的构建器构建结果。解析完成时由构建器返回最终的构建结果。
下面我们以创建一个简单的模板引擎为例了解builder模式:
关于模板引擎大家一定很熟悉,比如Mustache、pug、ejs,understore的template方法,这些模板的使用通常都是传入一段模板自字符串,返回一个编译好的函数,通过一个上下文对象作为参数调用这个函数就可以得到一段html文本字符串。
比如pug的使用方式如下(例子来自于pug中文文档):
const pug = require('pug');
const compiledFunction = pug.compileFile('template.pug');
console.log(compiledFunction({name: '李莉'}));
// "<p>李莉的 Pug 代码!</p>"
// 渲染另外一组数据
console.log(compiledFunction({name: '张伟'}));
// "<p>张伟的 Pug 代码!</p>"
接下来我们来写一个自己的模板解析工具:
假设现在有一个这样的模板字符串:
const tpl = `
<p>{{name}}</p>
{{if isVip}}
<p>Welcome!</p>
{{/endif}}
`;
在上面的模板中{{}}表示一个插值,{{if isVip}} ...{{/endif}}表示一个条件编译语句,只有在isVip为true时,之间的html内容才会被渲染。
首先定义一个解析器:
class Parser {
static parse(tpl, builder) {
const htmlReg = /^s*([^{}]+)/;
const interpReg = /^s*{{s*(w+?)s*}}/;
const ifReg = /^s*{{ifs+(w+?)s*}}(.*?){{/endif}}/;
// 去除换行和多余的空格
tpl = tpl.replace(/n+/g, '').replace(/s{2,}/g, ' ');
while(tpl.length) {
// 普通标签
if (htmlReg.test(tpl)) {
const match = tpl.match(htmlReg);
const label = match[0];
tpl = tpl.substr(label.length);
// html += (html === '' ? '' : '+') + JSON.stringify(label.trim());
builder.html(label.trim());
} else if(ifReg.test(tpl)) { // 条件语句
const match = tpl.match(ifReg);
const label = match[0];
tpl = tpl.substr(label.length);
builder.condition(...match.slice(1, 3).map(s => s.trim()));
} else if(interpReg.test(tpl)) { // 插值语句
const match = tpl.match(interpReg);
const label = match[0];
const name = match[1];
tpl = tpl.substr(label.length);
// html += '+' + '$$option.' + name;
builder.interpolation(name);
} else {
break;
}
}
}
}
解析器的有一个parse方法,该方法接受两个参数,一个待解析的模板字符串以及一个构建器,构建器需要提供至少三个方法html,interpolattion,condition这三个方法分别用于处理解析器解析出来的普通html、插值表达式、以及条件语句。按照相同的方式可以扩展出其他更多的控制语句。
下面时builder类的实现:
class Builder {
constructor(ctxName = '$$option') {
this._result = '';
this._ctxName = ctxName;
}
result() {
return this._result;
}
html(html) {
this._result += (this._result === '' ? '' : '+') + JSON.stringify(html);
}
condition(name, content) {
this._result += this._result === '' ? '' : '+';
this._result += `
(${this._ctxName}.${name}?${JSON.stringify(content)}:${JSON.stringify('')})
`;
}
interpolation(name) {
this._result += (this._result === '' ? '' : '+') + `${this._ctxName}.` + name;
}
}
Builder类的result方法返回最终得到的代码字符串。
结合上面的两个类,实现的编译函数如下:
function compile(tpl) {
const builder = new Builder();
Parser.parse(tpl, builder);
return new Function('$$option', `return ${builder.result()}`);
}
compile函数首先构建一个builder实例,通过该实例调用parse方法,最终返回一个函数用于执行从builder得到的代码字符串。
接下来测试一下这个程序:
const template = compile(tpl);
console.log(template({name: '小明', isVip: true}));
执行结果如下:
<p>小明</p><p>Welcome!</p>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。