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>

helloweilei
130 声望2 粉丝

菜鸟一枚,各位道友多多指教!