模板引擎:将数据变为视图的最优雅的解决方案

模板引擎的发展:

  1. 纯DOM方法
  2. 数组的join方法
  3. ES6模板字符串
  4. 模板引擎

    mustache的基本使用:

  • 在vue中使用是由两个大括号将变量包裹起来
    1. <p>{{name}}</p>

    实现原理:

    将模板字符串转化为tokens ,数据结合 tokens 解析为 dom 字符串。

实现代码:

本案例主要实现mustache库,非Vue的mustache

第一步:将模板字符串转换为Tokens

scanner.js

  • 用于扫描模板字符串,找到tag内的变量,与tag外的变量

    1. // 扫描器
    2. export default class Scanner {
    3. constructor(template) {
    4. this.template = template
    5. this.pos = 0 // 记录当前扫描的字符串的索引
    6. this.tail = template // 记录剩下未扫描的字符串,刚开始是模板字符串
    7. }
    8. // 跳过标记
    9. scan(tag) {
    10. if (this.tail.indexOf(tag) == 0) {
    11. this.pos += tag.length // 跳过标记
    12. this.tail = this.template.slice(this.pos) // 重新获取剩下的字符串
    13. }
    14. }
    15. // 扫描直到标记
    16. scanUtil(tag) {
    17. let pos_start = this.pos // 记录从哪开始扫描
    18. // 如果未扫描到标记就继续扫描,直到标记
    19. while (!this.eos() && this.tail.indexOf(tag) !== 0) {
    20. this.pos++
    21. this.tail = this.template.slice(this.pos)
    22. }
    23. return this.template.slice(pos_start, this.pos) // 返回标记前的所有字符
    24. }
    25. // end of string 判断是否扫描结束
    26. eos() {
    27. return this.pos >= this.template.length
    28. }
    29. }

    getToken.js

  • 循环使用扫描器,直到将模板字符串全部扫描结束

    1. import Scanner from "./scanner" // 引入扫描器
    2. import nestToken from "./nestToken" // 整合扫描后的tokens
    3. // 将字符串转换为tokens
    4. export default function getTokens(template) {
    5. let tokens = []
    6. let word // 记录每次扫描后的结果
    7. // 实例化扫描器
    8. let scanner = new Scanner(template)
    9. // 循环扫描,直到扫描完整个模板字符串
    10. while (!scanner.eos()) {
    11. word = scanner.scanUtil("{{") // 获取“{{”前的字符串
    12. tokens.push(["text", word]) // “{{”前的字符串是text
    13. scanner.scan("{{")
    14. if (!scanner.eos()) {
    15. word = scanner.scanUtil("}}") // 获取括号内的字符串
    16. if (word[0] == "#") { // 如果括号内第一个字符是#号,表示要循环,单独处理
    17. tokens.push(["#", word.slice(1)])
    18. } else if (word[0] == "/") { // 如果括号内第一个字符是/,表示循环结束,单独处理
    19. tokens.push(["/", word.slice(1)])
    20. } else {
    21. tokens.push(["name", word]) // 其他情况,就是name
    22. }
    23. scanner.scan("}}")
    24. }
    25. }
    26. return nestToken(tokens) // 整合扫描后得到的tokens
    27. }

    nestToken.js

  • 整合折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项

    1. // 折叠tokens
    2. export default function nestToken(tokens) {
    3. let nestToken = []
    4. let sections = [] // 栈
    5. let collector = nestToken // 收集器,默认指向nestToken
    6. // 循环tokens
    7. for (let i = 0; i < tokens.length; i++) {
    8. const token = tokens[i];
    9. switch (token[0]) {
    10. case "#": // 如果第一个参数是#,则需要折叠
    11. // 入栈
    12. sections.push(token)
    13. // 收集
    14. collector.push(token)
    15. // 改变收集器指向
    16. collector = token[2] = []
    17. break;
    18. case "/":
    19. // 出栈
    20. sections.pop()
    21. // 改变收集器执行,如果栈中还有数据,指向最后一个,栈中没有数据的话指向nestToken
    22. collector = sections.length > 0 ? sections[sections.length - 1][2] : nestToken
    23. break;
    24. default:
    25. collector.push(token)
    26. break;
    27. }
    28. }
    29. return nestToken
    30. }

    第二步:数据结合tokens解析为Dom字符串

    margeTokenAndData.js

  • 让tokens数组变为dom字符串

    1. import lookdata from "./lookdata"; // 引入对象查找方法
    2. // 将tokens转换为DOM字符串
    3. export default function margeTokenAndData(tokens, data) {
    4. let resultStr = ""
    5. // 循环tokens
    6. for (let i = 0; i < tokens.length; i++) {
    7. const token = tokens[i];
    8. if (token[0] == "text") { // 第一个是text不做处理
    9. resultStr += token[1]
    10. } else if (token[0] == "name") { // 第一个是name,需要赋值
    11. resultStr += lookdata(data, token[1])
    12. } else if (token[0] == "#") { // 第一个是#号,需要循环数组
    13. let arr = lookdata(data, token[1])
    14. // 循环数组,并且递归转换模板字符串
    15. arr.forEach(obj => {
    16. resultStr += margeTokenAndData(token[2], obj)
    17. });
    18. }
    19. }
    20. return resultStr
    21. }

    lookdata.js

  • 查找对象的某个属性

  • 解决:

    • obj[a.b.c],无法获取的问题
    • 如果是遍历数组,mustache中是{{.}},就是直接返回这个obj
      1. export default function lookdata(obj, parms) {
      2. if (parms == ".") return obj // 如果参数只有一个点,直接返回该对象
      3. if (parms.includes(".")) { // 参数如果是a.b.c,则分割参数后,依次获取
      4. let arr = parms.split(".")
      5. return arr.reduce((pre, item) => pre[item], obj)
      6. }
      7. return obj[parms] // 如果参数不满足以上逻辑,则可以返回obj[parms]
      8. }

      第三步:向外暴露模板引擎与其render方法

      index.js

  • 整合以上方法,向外暴露模板引擎与其render方法

    1. import getTokens from "./getToken"
    2. import margeTokenAndData from "./margeTokenAndData"
    3. // 挂载到window对象上向外暴露
    4. window.ylzTE = {
    5. render(template, data) {
    6. let tokens = getTokens(template) // 获取tokens
    7. return margeTokenAndData(tokens, data) // 将tokens与数据结合为dom字符串
    8. }
    9. }
    10. /*-----------------------------------------------------------------------------*/
    11. // 测试代码
    12. let template = `
    13. <h1>
    14. <p>{{group.name}}成员的基本信息</p>
    15. <ul>
    16. {{#arr}}
    17. <br>
    18. <li>姓名:{{name}}</li>
    19. <li>年龄:{{age}}</li>
    20. <li>性别:{{sex}}</li>
    21. <ul>爱好:
    22. {{#hobbies}}
    23. <ol>{{.}}</ol>
    24. {{/hobbies}}
    25. </ul>
    26. {{/arr}}
    27. </ul>
    28. </h1>
    29. `
    30. let data = {
    31. group: {
    32. name: "三组"
    33. },
    34. arr: [
    35. { name: "张三", age: 19, sex: "男", hobbies: ["篮球", "足球"] },
    36. { name: "李四", age: 23, sex: "女", hobbies: ["拼图", "看电视剧", "购物"] },
    37. { name: "王五", age: 43, sex: "男", hobbies: ["打游戏"] },
    38. { name: "赵六", age: 22, sex: "男", hobbies: ["跑步", "股票"] }
    39. ]
    40. }
    41. const box = document.getElementById("box")
    42. box.innerHTML = ylzTE.render(template, data)