手写mustache模板引擎(简易版)

450 阅读4分钟

前言

第一次使用掘金写文章,今天来分享一下手写mustache这个编译的模板引擎!!!那么接下来就是来说说这个是如何手写的呢?

详细过程

render()函数

首先呢 ,我们得有一个render函数 来帮助我们编译模板,那么这个render函数该如何写呢?

import templateTokens from './tokens.js';
import templateRener from './templateRener.js'
window.gx_mustache = {
    //模板字符串转换tokens数组   => tokens数组变为dom字符串
    render(templateStr, data) {
        //将模板字符串转换tokens数组
        let tokens = templateTokens(templateStr);
        // 让tokens数组变为dom字符串
        let domStr = templateRener(tokens, data);
        //返回
        return domStr;
    }
};

然后可以通过调用来生成我们的模板字符串咯

gx_mustache.render(templateStr, data); //templateStr为模板字符串  data为数据

//然后就可以挂载到contain容器咯
var contain = document.getElementById("contain");
 contain.innerHTML = domStr;

上面templateTokens 、templateRener又是什么呢? 这里说一下

templateTokens()  //这个是将模板字符串转换tokens数组

templateRener()   //这个是将tokens数组变为dom字符串

templateTokens() 的结果

templateRener() 的结果

那么这俩个函数又是怎样的呢? 首先来看

templateTokens() 函数

export default function templateTokens(templateStr) {
    var tokens = [];
    // 创建扫描器
    var scanner = new Scanner(templateStr);
    var words;
    // 让扫描器工作
    while (!scanner.eos()) {
        // 收集开始标记出现之前的文字
        words = scanner.scanUtil('{{');
        if (words != '') {
            // 尝试写一下去掉空格,智能判断是普通文字的空格,还是标签中的空格
            // 标签中的空格不能去掉,比如<div class="box">不能去掉class前面的空格
            let isInJJH = false;
            // 空白字符串
            var _words = '';
            for (let i = 0; i < words.length; i++) {
                // 判断是否在标签里
                if (words[i] == '<') {
                    isInJJH = true;
                } else if (words[i] == '>') {
                    isInJJH = false;
                }
                // 如果这项不是空格,拼接上
                if (!/\s/.test(words[i])) {
                    _words += words[i];
                } else {
                    // 如果这项是空格,只有当它在标签内的时候,才拼接上
                    if (isInJJH) {
                        _words += ' ';
                    }
                }
            }
            // 存起来,去掉空格
            tokens.push(['text', _words]);
        }
        // 过双大括号
        scanner.scan('{{');
        // 收集开始标记出现之前的文字
        words = scanner.scanUtil('}}');
        if (words != '') {
            // 这个words就是{{}}中间的东西。判断一下首字符
            if (words[0] == '#') {
                // 存起来,从下标为1的项开始存,因为下标为0的项是#
                tokens.push(['#', words.substring(1)]);
            } else if (words[0] == '/') {
                // 存起来,从下标为1的项开始存,因为下标为0的项是/
                tokens.push(['/', words.substring(1)]);
            } else {
                // 存起来
                tokens.push(['name', words]);
            }
        }
        // 过双大括号
        scanner.scan('}}');
    }

    // 返回折叠收集的tokens
    return nestTokens(tokens);

}

这里使用了一个扫描类Scanner来帮助我们找到这些模板字符串

Scanner类

export default class Scanner {
    constructor(templateStr) {
        //将模板字符串写到实例上
        this.templateStr = templateStr;
        // 定义一个指针
        this.pos = 0;
        //定义一个尾指针
        this.tail = templateStr;

    }
    // 扫描指定跳过,然后继续扫描
    scan(tag) {
        if (this.tail.indexOf(tag) == 0) {
            this.pos += tag.length;

            //尾巴也要变
            this.tail = this.templateStr.substring(this.pos);
        }
    }
    //扫描
    // stopTag停止标记
    scanUtil(stopTag) {
        //记录一下pos的值,一开始不一定是0
        const back_pos = this.pos;
        while (this.tail.indexOf(stopTag) != 0 && !this.eos()) {
            this.pos++;
            //后边是它的尾巴
            this.tail = this.templateStr.substring(this.pos);
        }
        // 循环返回扫描的内容
        return this.templateStr.substring(back_pos, this.pos)
    }
   //指针是否到头
    eos(){
        return this.pos >= this.templateStr.length
    }


}

另外还使用了nestTokens函数来折叠数组

*nestTokens()函数

/* 
    函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项
*/
export default function nestTokens(tokens) {
    // 结果数组
    var nestedTokens = [];
    // 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
    var sections = [];
    // 收集器,天生指向nestedTokens结果数组,引用类型值,所以指向的是同一个数组
    // 收集器的指向会变化,当遇见#的时候,收集器会指向这个token的下标为2的新数组
    var collector = nestedTokens;

    for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i];
        // console.log(token);

        //token[0] 指的是# / text name这些
        //token[1] 指的的word
        //token[2] 指的是有子级
        switch (token[0]) {
            case '#':
                // 收集器中放入这个token
                collector.push(token);
                // console.log(collector);
                // 入栈
                sections.push(token);
                // 收集器要换人。给token添加下标为2的项,并且让收集器指向它
                collector = token[2] = [];
                break;
            case '/':
                // 出栈。pop()会返回刚刚弹出的项
                sections.pop();
                // 改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组
                collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
                break;
            default:
                // 甭管当前的collector是谁,可能是结果nestedTokens,也可能是某个token的下标为2的数组,甭管是谁,推入collctor即可。
                collector.push(token);
        }
    }

    return nestedTokens;
};

那么templateTokens函数就这样完成咯。

下面来看看另一个函数templateRener() 这个是将tokens数组变为dom字符串

templateRener()函数

import lookup from './lookup';
import parseArray from './parseArray'
// 让tokens数组变为dom字符串
export default function templateRener(tokens, data) {
    // 结果字符串
    var resultStr = '';
    // 遍历tokens
    for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i];
        // 看类型
        if (token[0] == 'text') {
            // 拼起来
            resultStr += token[1];
        } else if (token[0] == 'name') {
            // 如果是name类型,那么就直接使用它的值,当然要用lookup
            // 因为防止这里是“a.b.c”有逗号的形式
            resultStr += lookup(data, token[1]);
        } else if (token[0] == '#') {
            resultStr += parseArray(token, data);
        }
    }

    return resultStr;
}

这个函数也借助了俩个函数lookup()和parseArray()来完成的

下面来看看lookup()函数

export default function lookup(obj, keyName) {
    //首先判断keyName是否含有.
    if (keyName.indexOf('.') != -1 && keyName != '.') {
        //有就拆分
        let keys = keyName.split(".");
        let temp = obj;
        for (let i; i < keys.length; i++) {
            temp = temp[keys[i]];
        }
        //循环结束后返回temp
        return temp;
    }
    //没有. 就直接使用
    return obj[keyName];
}

然后就是parseArray()函数

export default function parseArray(token, data) {
    // 得到整体数据data中这个数组要使用的部分
    var v = lookup(data, token[1]);
    // 结果字符串
    var resultStr = '';
    // 遍历v数组,v一定是数组
    // 注意,下面这个循环可能是整个包中最难思考的一个循环
    // 它是遍历数据,而不是遍历tokens。数组中的数据有几条,就要遍历几条。
    for(let i = 0 ; i < v.length; i++) {
        // 这里要补一个“.”属性
        // 拼接
        resultStr += renderTemplate(token[2], {
            ...v[i],
            '.': v[i]
        });
    }
    return resultStr;
};

最后 一个简易版的mustache模板引擎就这样做好咯 下面来测试一下

 // 模板
        var templateStr = `
        <div>
                <ul>
                    {{#students}}
                    <li>
                        学生{{name}}的爱好是
                        <ol>
                            {{#hobbies}}
                            <li class = "xixi">{{.}}</li>
                            {{/hobbies}}
                        </ol>
                    </li>
                    {{/students}}
                </ul>
            </div>
        `;
        // 数据
        var data = {
            students: [
                { 'name': '故心', 'hobbies': ['编程', '游泳'] },
                { 'name': '故心心', 'hobbies': ['看书', '弹琴', '画画'] },
                { 'name': '故心要加油', 'hobbies': ['锻炼'] }
            ]
        };
        // 调用render方法
        var domStr = gx_mustache.render(templateStr, data);
        // console.log(domStr);
        

        //挂载到contain容器汇总
        var contain = document.getElementById("contain");
        contain.innerHTML = domStr;

结果

哈 ,完成啦,给自己一个赞,继续加油。