简介
模板引擎是数据和模板相相结合的最优解决方案,模板引擎的主要功能使模板视图配合数据渲染页面。除了模板引擎还有es6的字符串模板,字符串拼接还有更久远的Dom API。
示例:
// mustache模板引擎const str = `<div><div>${aaa}</div></div>`;const data = {aaa: "你好"};const tplStr = mustache.render(str, data);XXX.innerHTML = str; // 插入视图// Es6 字符串模板const aaa = "你好";const str = `<div><div>${aaa}</div></div>`; // <div><div>你好</div></div>XXX.innerHTML = str; // 插入视图// 字符串拼接const aaa = "你好";let str += "<div>";str += "<div>"+ aaa +"</div>";str += "</div>"; // <div><div>你好</div></div>XXX.innerHTML = str;// dom apiconst aaa = "你好";const div1 = document.createElement("div");const div2 = document.createElement("div");div2.innerText = aaa;div1.appendChild(div2);XXX.appendChild(div1); // 插入视图
根据以上案例实现效果一致,但dom api不具有实践意义,太过于繁琐。 而字符串拼接则格式不够优雅,模板少量还能接受,多的话不只是编写麻烦,看起来很混乱。 es6字符串模板处理简单字符串无压力, 但如果模板中出现判断是否显示某节点, 循环等操作的话,还是很麻烦。 这个时候模板引擎的功能则体现出来了,模板引擎可以在模板上编写一些判断、循环等操作,例如现在很火的vue,vue的视图就是相当于一个模板引擎。v-if、v-for和大括号表达式等基本实现模板引擎的基本功能。
mustache是一个比较初版的模板引擎插件, 后续出现的模板引擎或多或少都有借鉴mustache的思想。
1、入口文件
import parseTemplateTokens from "./parseTemplateTokens";import renderTmplate from "./renderTmplate";export default {render(templateStr, data){// 将字符串模板解析成tokensconst tokens = parseTemplateTokens(templateStr);// 将tokens和数据相结合拼接处处理后的dom字符串const domStr = renderTmplate(tokens, data);return domStr;}}
2、将模板字符串转成tokens数组
tokens是根据模板的内容拆解出文本节点、元素节点的一个数组,将非标识符(非{{ 或 }})包含的节点识别为元素节点, 被{{ 或 }} 包含的节点为文本节点。
转成tokens需要有以下两个步骤:
1、需要一个方法将元素节点和文本节点识别并拆分(Scanner)得到无序的tokens数组, 何为无序的tokens数组,既是没有将模板中的循环和逻辑判断归类。
2、转成有序tokens的方法(nestTokens), 有序的tokens则是将# 和 /之间的数据进行归类折叠。
/*** 将模板字符串转成tokens数组* 无序的tokens* [[ "text", "<div class='list'>" ],[ "#", " arr" ],[ "text", "<div class='item'><div class='title'>" ],[ "name", " title " ],[ "text", "</div><div class='item-list'>" ],[ "#", " item.data" ],[ "text", "<div class='item-item'>" ],[ "name", " a " ],[ "text", "</div>" ],[ "/", " item.data" ],[ "text", "</div></div> " ],[ "/", " arr" ],[ "text", "</div>" ]]*/import Scanner from "./Scanner";import nestTokens from "./nestTokens";export default templateStr => {const tokens = [];const scanner = new Scanner(templateStr);let words;while (!scanner.eos()) {// 收集开始标记出现的文字words = scanner.scanUtil("{{");if(words !== ''){tokens.push(["text", words]);}scanner.scan("{{");words = scanner.scanUtil("}}");if(words !== ''){if(words[0] === "#"){tokens.push(["#", words.substring(1)]);}else if(words[0] === "/"){tokens.push(["/", words.substring(1)]);}else{tokens.push(["name", words]);}}scanner.scan("}}");}return nestTokens(tokens);}
2.1、根据标识区分文本节点和元素节点
/*** 扫描器类*/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);}}// 让指针进行扫描,知道遇见内容结束,并且能够返回结束之前的文字scanUtil(stopTag) {// 记录初始pos的值const pos_backup = this.pos;// 当模板扫描未结束 并且 指定内容在当前字符串内不为空则继续循环扫描while(!this.eos() && this.tail.indexOf(stopTag) !== 0){this.pos++;// 不断更新扫描后的模板this.tail = this.templateStr.substring(this.pos);}return this.templateStr.substring(pos_backup, this.pos);}// 判断模板是否扫描结束eos(){return this.pos >= this.templateStr.length;}}
2.2、转成有序tokens
/*** 将#与/之间的tokens折叠起来*//*** 例子:* 原结构* [[ "text", "<div class='list'>" ],[ "#", " arr" ],[ "text", "<div class='item'><div class='title'>" ],[ "name", " title " ],[ "text", "</div><div class='item-list'>" ],[ "#", " item.data" ],[ "text", "<div class='item-item'>" ],[ "name", " a " ],[ "text", "</div>" ],[ "/", " item.data" ],[ "text", "</div></div> " ],[ "/", " arr" ],[ "text", "</div>" ]]* 转化后[[ "text", "<div class='list'>" ],[ "#", " arr",[[ "text", "<div class='item'><div class='title'>" ],[ "name", " title " ],[ "text", "</div><div class='item-list'>" ],[ "#", " item.data",[[ "text", "<div class='item-item'>" ],[ "name", " a " ],[ "text", "</div>" ]]],[ "text", "</div></div>" ]]],[ "text", "</div>" ]]*/export default tokens => {const nestTokens = [];// 收集器,默认指向nestTokens结果数组// 收集器会变化, 如遇到#的时候,收集器会指向这个token下标为2的新数组let collector = nestTokens,// 栈,存放当前遇到#后的嵌套层级sections = [],token,i = 0,len = tokens.length;for(; i < len; i++){token = tokens[i];switch (token[0]) {case "#":// 收集器添加["#", "***"]collector.push(token);// 栈新增嵌套层级sections.push(token);// 修改收集器指向collector = token[2] = [];break;case "/":// 出栈,返回上一层sections.pop();// 修改收集器指向,如栈有嵌套层级,则指向栈的最后一位层级的下标为2的数组,否则则指向返回结果tokenscollector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokens;break;default:collector.push(token);break;}}return nestTokens;}
3、让tokens数组变为dom字符串
在得到tokens数组后,下一步是将tokens和数据相结合,转成dom字符串。
转成dom字符串有以下步骤:
1、对元素节点直接并入dom字符串中
2、将tokens中对应的文本节点找到对应的data字段获取到该字段的属性,然后并入dom字符串中,此时模板中编写的data字段有可能会出现 XXX.XXX.XXX, 这时需要一个方法处理该情况(lookup)。
3、处理tokens中的逻辑节点(#)的方法, parseArray。
/*** 让tokens数组变为dom字符串*/import lookup from "./lookup";import parseArray from "./parseArray";export default function renderTmplate(tokens, data){let template = "";for(let i = 0; i < tokens.length; i++){let token = tokens[i];switch (token[0]) {case "text":template += token[1];break;case "name":template += lookup(data, token[1]);break;default:template += parseArray(token, data);break;}}return template;}
3.1、根据token的文本节点的属性值获取data中的属性
/*** 功能是可以在dataObj对象中,寻找用连续点符号的keyName属性* 例如: dataObj* {* a: {* b: {* c: 1* }* }* }** keyName a.b.c*/export default (dataObj, keyName) => {const keyStr = keyName.trim();if(keyStr.indexOf(".") !== -1 && keyStr !== "."){const keys = keyStr.split(".");let temp = dataObj;for(let i = 0; i < keys.length; i++){temp = temp[keys[i]];}return temp;}// 如无点符号return dataObj[keyStr] || "";};
3.2、处理逻辑节点数组,结合renderTemplate实现递归
/*** 处理数组,结合renderTemplate实现递归* 注意,这个函数收的参数是token, 而不是tokens!* token是 ["#", "XXX", [...]]*/import renderTmplate from "./renderTmplate";export default (token, data) => {const v = data[token[1].trim()];let str = "";// # 的字段可以是循环,也可以判断语句if(v instanceof Array){for(let i = 0; i < v.length; i++){str += renderTmplate(token[2], {...v[i],"item": v[i],".": v[i]});}}if(typeof v === "boolean" && v){str += renderTmplate(token[2], data);}return str;}
使用方式
import fang_mustache from "./index.js";const str = `<div class="list">{{# arr}}<div class="item"><div class="title">{{ title }}</div><div class="item-list">{{# data}}<div class="item-item">{{ item.a }}</div><div class="item-item">{{ item.b }}</div><div class="item-item">{{ index }}</div>{{/ data}}</div></div>{{/ arr}}{{# isShow}}<div>你好</div>{{/ isShow}}</div>`;const data = {isShow: true,arr: [{title: "O(∩_∩)O",data: [{a: "测测测测",b: "1111111111111"}]},{title: "hahahahahahaha",data: [{a: "王晓",b: "222222222222222"}]}]};const str = fang_mustache.render(str, data); // 即可获取到编译后的字符串
