# 正则表达式简单实现

  • 最简单的实现则是利用 replace 方法,进行正则匹配

  • /\{\{\s*(\w+)\s*\}\}/g 我是利用这个正则来匹配符合,括号里面允许有多余空格

// 1. 定义模板
const templateStr = `<h2>我喜欢吃{{peach}}, {{ lain }}也喜欢吃 {{peach}}吗?</h2>`
// 2. 定义数据
const data = {
  lain: 'lain',
  peach: '桃子'
}
// 3. 封装匹配符合 {{}} 语法的字符串
function render(templateStr, data) {
  return templateStr.replace(/\{\{\s*(\w+)\s*\}\}/g, function (findStr, $1) {
    console.log(data[$1]);
    return data[$1]
  })
}
// 4. 返回新的字符串
const newStr = render(templateStr, data)
console.log(newStr) // <h2 > 我喜欢吃桃子,lain 也喜欢吃 桃子吗?</h2>

# 什么是 tokens

  • tokens 是一个 JS的嵌套数组 ,也就是 模板字符串JS表示

  • 它是抽象语法树、"虚拟节点" 等等的开山鼻祖

  • 例如普通模板字符串: <h2>我喜欢吃{{peach}}, {{lain}}也喜欢吃{{peach}}吗?</h2>

  • tokens: 由一个个 token 组成

[
  ["test", "<h2>我喜欢吃"], // token
  ["name", "peach"],       // token
  ["test", ", "],          // token
  ["name", "lain"],        // token
  ["test", "也喜欢吃"],     // token
  ["name", "peach"],       // token
  ["test ", "吗?</h2>"],   // token
]
  • 循环嵌套模板字符串
<ul>
  {{#arr}}
    <li>
      <span>{{.}}</span>
    </li>
  {{/arr}}
</ul>
  • 由下面一个个 token 组成 tokens:
[
  ['text', '<ul>'],
  [ '#', 'arr', [
    ['text', '<li><span>'],
    ['name', '.'],
    ['text', '</span></li>']
  ]],
  ['text', '</ul>']
]
  • mustache 库底层重点要做两个事情:
    • 将模板字符串编译为token形式
    • 将tokens结合数据,解析为dom字符串

# 使用 webpack 和 webpack-dev-server 构建

  • ​ 模块化打包工具有 webpack (webpack-dev-server)、rollup、Parcel 等
  • mustache 官方库使用rollup进行模块化打包 ,而我们使用今天使用 webpack (webpack-dev-server)进行模块化打包 ,这是因为 webpack (webpack-dev-server) 能让我们更方便地在 浏览器中 (而不是node.js环境中) 实时调式程序,相比 node.js 控制后台, 览器控制台更好用 ,比如能够点击展开数组地每项
  • 生成库是 UMD 的,这意味着它可以同时在 node.js 环境中使用,也可以在浏览器环境中使用。实现 UMD 不难,只需要一个 通用头 即可

# webpack 所需配置文件

  • package.json
{
  "name": "mustache",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.44.2",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0"
  }
}
  • webpack.config.js
const path = require('path')
module.exports = {
  // 模式,开发
  mode: 'development',
  // 入口
  entry: './src/index.js',
  // 打包到什么文件
  output: {
    filename: 'bundle.js'
  },
  // 配置 webpack-dev-server
  devServer: {
    hot: true,
    // 静态文件根目录
    contentBase: path.join(__dirname),
    // 不压缩
    compress: false,
    // 端口号
    port: 8080,
    // 虚拟打包路径
    publicPath: '/dist/'
  }
}
  • HTML 引入
<script src="/dist/bundle.js"></script>

# Scanner

  • 扫描器类
export default class Scanner {
  constructor(template) {
    // 1. 定义 指针 尾巴 与 模板 变量
    this.pos 	= 0 // 指针
    this.tail = template // 尾巴
    this.template = template // 模板
  }
  // 2. 作用与越过指定 tag 
  scan(tag) {
    // 2.1 所以需要将 pos 与 tag 长度进行相加
    this.pos += tag.length
    // 2.2 所以需要将 并取 pos 之后的字符串
    this.tail = this.template.substring(this.pos)
  }
  // 3. 让指针进行扫描,遇到指定 stopTag 结束,并返回之间路过的文字
  scanUntil(stopTag) {
    // 3.1 保存上次 this.pos 位置
    let pos_backup = this.pos
    // 3.2 当 pos 小于 template 长度 并且尾巴不是 stopTag, 说没没有扫描到 stopTag 才会继续循环  
    while (!this.eos() && this.tail.indexOf(stopTag) != 0) {
      // 3.3 每循环依次让指针 ++
      this.pos++
      // 3.4 获取 this.template 下标 this.pos 的字符串
      this.tail = this.template.substring(this.pos)
    }
    // 4. 返回开始到当前指针之前的字符串
    return this.template.substring(pos_backup, this.pos)
  }
  // 5. 判断 pos 是否到 template 最后了
  eos() {
    return this.pos >= this.template.length 
  }
}

# parseTemplateToTokens

  • 将模板字符串转为tokens数组
import Scanner from "./Scanner"
import nestTokens from "./nestTokens"
export default function parseTemplateToTokens(template) {
  // 1. 定义数组 将模板字符串转为 tokens
  const tokens = []
  // 2. 创建 Scanner 实例
  const scanner = new Scanner(template)
  // 3. 接收每次返回的字符串
  let words = ''
  // 4. 匹配去掉空格
  function trimWords(words) {
    return words.replace(/\s+(<)|(>)\s+/g, '$1$2')
  }
  // 3. 判断 this.tail 与 this.template 长度是否一致 否则一直循环
  while (!scanner.eos()) {
    // 3.1 扫描 {{  返回之前的字符串 text 类型
    words = scanner.scanUntil('{{')
    // 3.2 追入 tokens
    if(words) tokens.push(['text', trimWords(words)])
    // 3.3 越过 {{
    scanner.scan('{{')
    // 3.4 扫描 }}  返回之前的字符串 非 text 类型
    words = scanner.scanUntil('}}')
    // 4. 追入 tokens
    if (words) {
      // 4.1 判断为 '#' 类型 
      if (words[0] == '#') tokens.push(['#', trimWords(words.substring(1))])
        
      // 4.2 判断为 '/' 类型
      else if (words[0] == '/') tokens.push(['/', trimWords(words.substring(1))])
        
      // 4.3 判断为 'name' 类型 
      else tokens.push(['name', words])
    }
    
    // 4.4 越过 }}
    scanner.scan('}}')
  }
  // 5. 返回 tokens
  return nestTokens(tokens)
}

# nestTokens

  • 折叠tokens,将#与/之间的tokens整合起来,作为下标为2的项
export default function nestTokens(tokens) {
  // 1. 定义最终 tokens 数组 
  const nestTokens = []
  // 2. 定义栈结构,存放小 tokens
  let sections = []
  // 3. 收藏器 主要作用是需要巧妙地引用地址
  let collector = nestTokens
  // 4. 遍历原 tokens
  for (let i = 0; i < tokens.length; i++){
    // 4.1 取出一个个小 token
    let token = tokens[i]
    // 5. 进行判断类型
    switch (token[0]) {
      // 5.1 如果为 '#' 类型代表也是 tokens 需要黄 `# 与 /` 之间地数据放入 token [2]
      case '#':
        // 5.2 先同时将 token 追加入 数组中
        collector.push(token)
        sections.push(token)
        // 5.3 这里是非常巧妙地利用了引用地址类型 token [2] 地址 给到了 collector
        // 代表接下来 collector 进行 push 数据时 都是往 token [2] 追加的
        collector = token[2] = []
        break;
      
      case '/':
        // 6. 遇到 `/` 代表需要进行弹栈 
        sections.pop()
        // 7. 判断 sections 中是否还有数组 有的话将数组地址给 collector
        // 例如 [[1,2], [3,4]] 上面 pop 操作将 [3, 4] 弹出后剩下 [[1,2]], 那么此时 collector 地址 === [1,2] 地址 
        // 而 [1, 2] 地址也就是栈结构形式的一个个数组 
        // 8. 否则返回 nestTokens 而 nestTokens 被 collector 引用,所以返回的也是 collector
        collector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokens
        break;
      default:
        // 9. `text` 类型为普通文本 之间追加进入就可以了
        collector.push(token)
    }
  }
  // 10. 结果返回
  return nestTokens
}
  • 下面我补上分析数组引用的思路图

nestTokens

  • 与代码对照图

nestTokens-code

# renderTemplate

  • 将tokens数组变为DOM字符串
import lookup from "./lookup";
import parseToken from "./parseToken";
export default function renderTemplate(tokens, data) {
  // 1. 定义结果字符串
  let resultStr = ''
  // 2. 遍历 tokens
  for (let i = 0; i < tokens.length; i++){
    // 3. 循环获取 token
    let token = tokens[i]
    // 4. 从 token [0] 第一项判断是什么类型 
    switch (token[0]) {    
      // 4.1 'text' 类型就行普通文本 直接相加即可
      case 'text':
        resultStr += token[1]
        break;
      
      // 4.2 'name' 则是 {{}} 里面的,代表是变量 通过 lookup 函数获取变量对应的值
      case 'name':
        resultStr += lookup(data, token[1]) 
        break;
      
      // 4.3 '#' 代表也是 tokens, 需要将 tokens 再转为 token 进行链接 
      case '#':
        // 5. 所以创建一个递归函数 parseToken
        resultStr += parseToken(token, data)
        break;
    }
  }
  
  // 6. 返回字符串
  return resultStr
}

# lookup

  • 在对象中寻找连续点符号的keyName属性

  • 举个栗子:

const dataObj = {
  a: {
    b: {
      c: 100
    }
  }
}
lookup(dataObj, 'a.b.c') // 100
  • lookup 函数
export default function lookup(dataObj, keyName) {
  // 1. 判断 keyName 本身不是。并且 keyName 中没有以。连接地属性名 
  // 栗子:a.b.c
  if (keyName != '.' && keyName.indexOf('.') != -1) {
    // 2. 将上面 a.b.c 变成 ['a', 'b', 'c']
    let keys = keyName.split('.')
    // 3. 定义一个临时对象保存每次属性拿到的值 
    let tempObj = dataObj
    // 4. 循环拿到值 举个栗子 tempObj = {a: {b: {c: 100} } }
    // tempObj = dataObj[a] -> tempObj2 = tempObj[b] -> tempObj3 = tempObj2[c]
    for (let i = 0; i < keys.length; i++) tempObj = tempObj[keys[i]]
      
    // 5. 返回结果
    return tempObj
  }
  // 6. 如果本身就是。属性或者没有。连接的属性的话 就直接取值返回
  return dataObj[keyName]
}

# parseToken

  • 处理 tokens 数组, 结合 parseTemplateToTokens 实现递归
  • 注意函数参数时 token!不是 tokens
  • token 是一个简单的 ['#', 'friends', []]
import lookup from "./lookup"
import renderTemplate from "./renderTemplate";
export default function parseToken(token, data) {
  // 1. 定义变量接收数据对象
  const v = lookup(data, token[1])
  
  // 2. 作为结果字符串
  let resultStr = ''
  // 3. 遍历数据的长度 递归次数与数据的长度一致
  for (let i = 0; i < v.length; i++){
    resultStr += renderTemplate(token[2], {
      ...v[i],
      '.': v
    })
  }
  // 4. 返回结果字符串
  return resultStr
}

# index

  • 将模板转字符串转化为 tokens
  • 将 tokens 渲染为 DOMString 结构
  • 将 DOMString 返回
import Scanner from './Scanner.js'
import parseTemplateToTokens from './parseTemplateToTokens.js'
import renderTemplate from './renderTemplate.js'
global.TemplateEngine = {
  render(template, data) {
    // 1. 将模板转字符串转化为 tokens
    const tokens = parseTemplateToTokens(template)
    
    // 2. 将 tokens 渲染为 DOMString 结构
    const domStr = renderTemplate(tokens, data)
    // 3. 将 DOMString 返回
    return domStr
  }
}
  • 总结一下:这里尽量使用了 switch , 而没有使用 if else if ,是因为我认为 switch 效率会比 if else if

# 测试代码

  • 我简单实现了 mustache ,的基本功能,下面进行测试,您也可以自行测试

  • 如果您不了解用法,可以搜索我之前写的 JavaScript Mustache 模板引擎用法

  • 下面进行 . 测试

const templateStr =
  `<ul>
    {%#123;#arr}%#125;
      <li>
        {%#123;.}%#125;
      </li>
    {%#123;/arr}%#125;
  <ul>
`
const data = {
  arr: ['苹果', '西瓜', '樱桃']
}
const domStr = TemplateEngine.render(templateStr, data)
console.log(domStr);
container.innerHTML = domStr
  • 循环嵌套数组测试
<div id="container"></div>
<script src="./dist/bundle.js"></script>
<script>
  // 1. 定义模板字符串
  const templateStr =
    `<ul>
      {{#arr}}
        <li class="a">
          <span>姓名:{{name}}</span>
          <span>年龄:{{age}}</span>
          <ol>
            {{#freiends}}
              <li>freiends: {{.}}</li>
            {{/freiends}}
          </ol>
        </li>
      {{/arr}}
    <ul>
  `
  // 2. 定义数据
  const data = {
    name: 'lain',
    age: 16,
    arr: [
      { tag: 'li', name: 'lain', age: 16, freiends: ['saber', '樱岛麻衣'] },
      { tag: 'li', name: 'saber', age: 17, freiends: ['樱岛麻衣', '稚名真白'] },
      { tag: 'li', name: '樱岛麻衣', age: 16, freiends: ['稚名真白', 'lain'] },
      { tag: 'li', name: '稚名真白', age: 17, freiends: ['lain', 'saber'] }
    ]
  }
  // 3. 返回遍历渲染完的新字符串
  const domStr = TemplateEngine.render(templateStr, data)
  console.log(domStr)	
    
  // 4. 进行挂载 id 属性可以不用进行获取  
  container.innerHTML = domStr
</script>

# Mustache 源码

  • 这里放个官网地址以供参考
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.1.0/mustache.js"></script>