简介

模板引擎是数据和模板相相结合的最优解决方案,模板引擎的主要功能使模板视图配合数据渲染页面。除了模板引擎还有es6的字符串模板,字符串拼接还有更久远的Dom API。
示例:

  1. // mustache模板引擎
  2. const str = `
  3. <div>
  4. <div>${aaa}</div>
  5. </div>
  6. `;
  7. const data = {
  8. aaa: "你好"
  9. };
  10. const tplStr = mustache.render(str, data);
  11. XXX.innerHTML = str; // 插入视图
  12. // Es6 字符串模板
  13. const aaa = "你好";
  14. const str = `<div>
  15. <div>${aaa}</div>
  16. </div>`; // <div><div>你好</div></div>
  17. XXX.innerHTML = str; // 插入视图
  18. // 字符串拼接
  19. const aaa = "你好";
  20. let str += "<div>";
  21. str += "<div>"+ aaa +"</div>";
  22. str += "</div>"; // <div><div>你好</div></div>
  23. XXX.innerHTML = str;
  24. // dom api
  25. const aaa = "你好";
  26. const div1 = document.createElement("div");
  27. const div2 = document.createElement("div");
  28. div2.innerText = aaa;
  29. div1.appendChild(div2);
  30. XXX.appendChild(div1); // 插入视图

根据以上案例实现效果一致,但dom api不具有实践意义,太过于繁琐。 而字符串拼接则格式不够优雅,模板少量还能接受,多的话不只是编写麻烦,看起来很混乱。 es6字符串模板处理简单字符串无压力, 但如果模板中出现判断是否显示某节点, 循环等操作的话,还是很麻烦。 这个时候模板引擎的功能则体现出来了,模板引擎可以在模板上编写一些判断、循环等操作,例如现在很火的vue,vue的视图就是相当于一个模板引擎。v-if、v-for和大括号表达式等基本实现模板引擎的基本功能。

mustache是一个比较初版的模板引擎插件, 后续出现的模板引擎或多或少都有借鉴mustache的思想。

1、入口文件

  1. import parseTemplateTokens from "./parseTemplateTokens";
  2. import renderTmplate from "./renderTmplate";
  3. export default {
  4. render(templateStr, data){
  5. // 将字符串模板解析成tokens
  6. const tokens = parseTemplateTokens(templateStr);
  7. // 将tokens和数据相结合拼接处处理后的dom字符串
  8. const domStr = renderTmplate(tokens, data);
  9. return domStr;
  10. }
  11. }

2、将模板字符串转成tokens数组

tokens是根据模板的内容拆解出文本节点、元素节点的一个数组,将非标识符(非{{ 或 }})包含的节点识别为元素节点, 被{{ 或 }} 包含的节点为文本节点。
转成tokens需要有以下两个步骤:
1、需要一个方法将元素节点和文本节点识别并拆分(Scanner)得到无序的tokens数组, 何为无序的tokens数组,既是没有将模板中的循环和逻辑判断归类。
2、转成有序tokens的方法(nestTokens), 有序的tokens则是将# 和 /之间的数据进行归类折叠。

  1. /**
  2. * 将模板字符串转成tokens数组
  3. * 无序的tokens
  4. * [
  5. [ "text", "<div class='list'>" ],
  6. [ "#", " arr" ],
  7. [ "text", "<div class='item'><div class='title'>" ],
  8. [ "name", " title " ],
  9. [ "text", "</div><div class='item-list'>" ],
  10. [ "#", " item.data" ],
  11. [ "text", "<div class='item-item'>" ],
  12. [ "name", " a " ],
  13. [ "text", "</div>" ],
  14. [ "/", " item.data" ],
  15. [ "text", "</div></div> " ],
  16. [ "/", " arr" ],
  17. [ "text", "</div>" ]
  18. ]
  19. */
  20. import Scanner from "./Scanner";
  21. import nestTokens from "./nestTokens";
  22. export default templateStr => {
  23. const tokens = [];
  24. const scanner = new Scanner(templateStr);
  25. let words;
  26. while (!scanner.eos()) {
  27. // 收集开始标记出现的文字
  28. words = scanner.scanUtil("{{");
  29. if(words !== ''){
  30. tokens.push(["text", words]);
  31. }
  32. scanner.scan("{{");
  33. words = scanner.scanUtil("}}");
  34. if(words !== ''){
  35. if(words[0] === "#"){
  36. tokens.push(["#", words.substring(1)]);
  37. }else if(words[0] === "/"){
  38. tokens.push(["/", words.substring(1)]);
  39. }else{
  40. tokens.push(["name", words]);
  41. }
  42. }
  43. scanner.scan("}}");
  44. }
  45. return nestTokens(tokens);
  46. }

2.1、根据标识区分文本节点和元素节点

  1. /**
  2. * 扫描器类
  3. */
  4. export default class Scanner {
  5. constructor(templateStr){
  6. this.templateStr = templateStr; // 原始模板
  7. this.pos = 0; // 指针
  8. this.tail = templateStr; // 指针扫描后的模板
  9. }
  10. // 跳过指定内容
  11. scan(tag){
  12. // 判断指定内容是否在当前字符串第一位
  13. if(this.tail.indexOf(tag) === 0){
  14. this.pos += tag.length;
  15. this.tail = this.templateStr.substring(this.pos);
  16. }
  17. }
  18. // 让指针进行扫描,知道遇见内容结束,并且能够返回结束之前的文字
  19. scanUtil(stopTag) {
  20. // 记录初始pos的值
  21. const pos_backup = this.pos;
  22. // 当模板扫描未结束 并且 指定内容在当前字符串内不为空则继续循环扫描
  23. while(!this.eos() && this.tail.indexOf(stopTag) !== 0){
  24. this.pos++;
  25. // 不断更新扫描后的模板
  26. this.tail = this.templateStr.substring(this.pos);
  27. }
  28. return this.templateStr.substring(pos_backup, this.pos);
  29. }
  30. // 判断模板是否扫描结束
  31. eos(){
  32. return this.pos >= this.templateStr.length;
  33. }
  34. }

2.2、转成有序tokens

  1. /**
  2. * 将#与/之间的tokens折叠起来
  3. */
  4. /**
  5. * 例子:
  6. * 原结构
  7. * [
  8. [ "text", "<div class='list'>" ],
  9. [ "#", " arr" ],
  10. [ "text", "<div class='item'><div class='title'>" ],
  11. [ "name", " title " ],
  12. [ "text", "</div><div class='item-list'>" ],
  13. [ "#", " item.data" ],
  14. [ "text", "<div class='item-item'>" ],
  15. [ "name", " a " ],
  16. [ "text", "</div>" ],
  17. [ "/", " item.data" ],
  18. [ "text", "</div></div> " ],
  19. [ "/", " arr" ],
  20. [ "text", "</div>" ]
  21. ]
  22. * 转化后
  23. [
  24. [ "text", "<div class='list'>" ],
  25. [ "#", " arr",
  26. [
  27. [ "text", "<div class='item'><div class='title'>" ],
  28. [ "name", " title " ],
  29. [ "text", "</div><div class='item-list'>" ],
  30. [ "#", " item.data",
  31. [
  32. [ "text", "<div class='item-item'>" ],
  33. [ "name", " a " ],
  34. [ "text", "</div>" ]
  35. ]
  36. ],
  37. [ "text", "</div></div>" ]
  38. ]
  39. ],
  40. [ "text", "</div>" ]
  41. ]
  42. */
  43. export default tokens => {
  44. const nestTokens = [];
  45. // 收集器,默认指向nestTokens结果数组
  46. // 收集器会变化, 如遇到#的时候,收集器会指向这个token下标为2的新数组
  47. let collector = nestTokens,
  48. // 栈,存放当前遇到#后的嵌套层级
  49. sections = [],
  50. token,
  51. i = 0,
  52. len = tokens.length;
  53. for(; i < len; i++){
  54. token = tokens[i];
  55. switch (token[0]) {
  56. case "#":
  57. // 收集器添加["#", "***"]
  58. collector.push(token);
  59. // 栈新增嵌套层级
  60. sections.push(token);
  61. // 修改收集器指向
  62. collector = token[2] = [];
  63. break;
  64. case "/":
  65. // 出栈,返回上一层
  66. sections.pop();
  67. // 修改收集器指向,如栈有嵌套层级,则指向栈的最后一位层级的下标为2的数组,否则则指向返回结果tokens
  68. collector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokens;
  69. break;
  70. default:
  71. collector.push(token);
  72. break;
  73. }
  74. }
  75. return nestTokens;
  76. }

3、让tokens数组变为dom字符串

在得到tokens数组后,下一步是将tokens和数据相结合,转成dom字符串。
转成dom字符串有以下步骤:
1、对元素节点直接并入dom字符串中
2、将tokens中对应的文本节点找到对应的data字段获取到该字段的属性,然后并入dom字符串中,此时模板中编写的data字段有可能会出现 XXX.XXX.XXX, 这时需要一个方法处理该情况(lookup)。
3、处理tokens中的逻辑节点(#)的方法, parseArray。

  1. /**
  2. * 让tokens数组变为dom字符串
  3. */
  4. import lookup from "./lookup";
  5. import parseArray from "./parseArray";
  6. export default function renderTmplate(tokens, data){
  7. let template = "";
  8. for(let i = 0; i < tokens.length; i++){
  9. let token = tokens[i];
  10. switch (token[0]) {
  11. case "text":
  12. template += token[1];
  13. break;
  14. case "name":
  15. template += lookup(data, token[1]);
  16. break;
  17. default:
  18. template += parseArray(token, data);
  19. break;
  20. }
  21. }
  22. return template;
  23. }

3.1、根据token的文本节点的属性值获取data中的属性

  1. /**
  2. * 功能是可以在dataObj对象中,寻找用连续点符号的keyName属性
  3. * 例如: dataObj
  4. * {
  5. * a: {
  6. * b: {
  7. * c: 1
  8. * }
  9. * }
  10. * }
  11. *
  12. * keyName a.b.c
  13. */
  14. export default (dataObj, keyName) => {
  15. const keyStr = keyName.trim();
  16. if(keyStr.indexOf(".") !== -1 && keyStr !== "."){
  17. const keys = keyStr.split(".");
  18. let temp = dataObj;
  19. for(let i = 0; i < keys.length; i++){
  20. temp = temp[keys[i]];
  21. }
  22. return temp;
  23. }
  24. // 如无点符号
  25. return dataObj[keyStr] || "";
  26. };

3.2、处理逻辑节点数组,结合renderTemplate实现递归

  1. /**
  2. * 处理数组,结合renderTemplate实现递归
  3. * 注意,这个函数收的参数是token, 而不是tokens!
  4. * token是 ["#", "XXX", [...]]
  5. */
  6. import renderTmplate from "./renderTmplate";
  7. export default (token, data) => {
  8. const v = data[token[1].trim()];
  9. let str = "";
  10. // # 的字段可以是循环,也可以判断语句
  11. if(v instanceof Array){
  12. for(let i = 0; i < v.length; i++){
  13. str += renderTmplate(token[2], {
  14. ...v[i],
  15. "item": v[i],
  16. ".": v[i]
  17. });
  18. }
  19. }
  20. if(typeof v === "boolean" && v){
  21. str += renderTmplate(token[2], data);
  22. }
  23. return str;
  24. }

使用方式

  1. import fang_mustache from "./index.js";
  2. const str = `
  3. <div class="list">
  4. {{# arr}}
  5. <div class="item">
  6. <div class="title">{{ title }}</div>
  7. <div class="item-list">
  8. {{# data}}
  9. <div class="item-item">{{ item.a }}</div>
  10. <div class="item-item">{{ item.b }}</div>
  11. <div class="item-item">{{ index }}</div>
  12. {{/ data}}
  13. </div>
  14. </div>
  15. {{/ arr}}
  16. {{# isShow}}
  17. <div>你好</div>
  18. {{/ isShow}}
  19. </div>
  20. `;
  21. const data = {
  22. isShow: true,
  23. arr: [
  24. {
  25. title: "O(∩_∩)O",
  26. data: [
  27. {
  28. a: "测测测测",
  29. b: "1111111111111"
  30. }
  31. ]
  32. },
  33. {
  34. title: "hahahahahahaha",
  35. data: [
  36. {
  37. a: "王晓",
  38. b: "222222222222222"
  39. }
  40. ]
  41. }
  42. ]
  43. };
  44. const str = fang_mustache.render(str, data); // 即可获取到编译后的字符串