手写简易版mustache(javascript模板编译)

195 阅读1分钟

第一步、解析模板转成token

// 假设模板是这样得
const templateStr1 = "我今天买了一个{{thing}},心情还蛮{{mood}}的"

// 数据是这样的 
const data = {
  thing: "玩具",
  mood: "开心",
}

// 将模板转化成tokens ==> [ ["text","我今天买了一个"],["name","thing"].....以此类推
function templateToTokens(templateStr) {
  console.log(templateStr)
  let tokens = []
  let pos = 0
  //   尾巴字符串
  let tail = templateStr

  //   当尾巴字符串为空时,结束循环
  while (tail !== '') {
    console.log(tail,"进来了")
    let idx = tail.indexOf("{{")
    // 说明内部已经都是text
    if (idx < 0) {
      tokens.push(["text", tail])
      tail = ""
      break
    }
    let text = tail.substring(pos, idx)
    tokens.push(["text", text])
    tail = tail.slice(idx + 2)

    idx = tail.indexOf("}}")
    let name = tail.substring(pos, idx)
    tokens.push(["name", name])
    tail = tail.slice(idx + 2)
  }
  return tokens
}


// 但是遇到复杂情况就行不通了,例如模板字符串为
const tempStr2 = `
     <ol>
         {{#student}}
             <li>{{name}}</li>
             <li>{{age}}</li>
             <li>{{sex}}</li>
         {{/student}}
     </ol>
`

// 此时应该对特殊符号#,/进行判断

// 这个时候需要再检索到"}}"字符时添加判断,因为这前面一定时一个name或者特殊符号
if (name.startsWith("#")) {
      tokens.push(["#", name.substring(1)])
    } else if (name.startsWith("/")) {
      tokens.push(["/", name.substring(1)])
    } else {
      tokens.push(["name", name])
    }
 }
 
// 最后出来的tokens成功一半了,

第二步、折叠数组

当模板字符串解析成token后,的数组结构是 
[['text''\n <ol>\n '],
['#''student'],
['text''\n <li>'],
...]

这并不符合我们的预期,我们需要将数组这折叠成为
[
    ['text''\n <ol>\n '],
    ['#''student',[
            ['text''\n <li>'],
            ....
        ]],
    ['text''\n </ol>\n '],
]

function nestedTokens(tokens) {
  let resultTokens = []

  //   栈队列
  let section = []

  //  收集器,用于收集每次集合
  let colloctor = resultTokens
  for (let i = 0; i < tokens.length; i++) {
    let token = tokens[i]

    if (token[0] === "#") {
      // 入栈
      section.push(token)
      //   同时结果结果数组仍然需要当前项
      colloctor.push(token)
      // 遇到#应该收集器变成当前#数组内的第2项的收集器
      colloctor = token[2] = []
    } else if (token[0] === "/") {
      // 出栈
      section.pop()

      // 出栈时要判断收集器里是否还有东西,如果没有则重新指向原来的收集器
      colloctor = section.length > 0 ? section[section.length - 1][2] : resultTokens
    } else {
      // 正常内容
      colloctor.push(token)
    }
  }
  return resultTokens
}

最后一步,变成dom字符串

function renderTemplate(tokens, data) {
  // 我们只需要处理当前层级,其余的因为结构相同,可以递归处理
  console.log(tokens, data, "tokens,data")
  let resultStr = ""

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

    if (token[0] == "text") {
      // 如果是text,我们就不需要处理直接加到结果字符串当中
      resultStr += token[1]
    } else if (token[0] == "#") {
      //
      const tempData = lookup(data, token[1])
      console.log(tempData, "tempData")
      let tempStr = ""
      for (let j = 0; j < tempData.length; j++) {
        let oneData = tempData[j]

        // console.log(tempStr, "tempStr",renderTemplate(token[2], oneData))
        tempStr += renderTemplate(token[2], oneData)
      }

      resultStr += tempStr
    } else {
      // 这里肯定是name了,我们只需要替换掉name值即可,加一个.属性是预防当前数据可能不是对象,模板当中可能为"."
      resultStr += lookup({ ...data, ".": data }, token[1])
    }
  }

  console.log(resultStr, "resultStr")
  return resultStr
}

在过程中会发现一个问题就是处理当token[0]是name的时候,我们需要去数据去寻找所对应的值,所以单独封装一个函数lookup去获取这个值

function lookup(data, keyName) {
  let result = data
  if (keyName.indexOf(".") > 0) {
    // 说明是keyName是a.b.c的格式
    let keys = keyName.split(".")

    for (let i = 0; i < keys; i++) {
      let key = keys[i]

      result = result[key]
    }
  } else {
    return result[keyName]
  }
}