十分钟打造一个极简的模板“引擎”

273 阅读3分钟

维基百科是这样解释网页模板:网页模板(Web Template)是将资料转译到一个描述显示格式的版面上,使得内容与表现方式可以分离。

何为模板,造一个印章,比如皇帝的玉玺,再加上一点染色物体就可以批改奏折了。这里的玉玺就是模板,染色物体就是内容,使用不同颜色的物体即可渲染出与五颜六色的内容了。

话题扯远了,进入正题。怎么用js打造一个模板引擎呢?想一想,这里给出几种思路。

  • 第一种就是类似vue、react,使用virtual DOM,将模板里的数据抽象出来管理
  • 第二种就是拼接字符串,这种适用于静态界面,首屏加载速度极快

在这里我们选择第二种方案,毕竟第一种并不是一时半会就能做出来(可能这辈子都没可能,不过要积极的去理解它们的思想,有很多可以借鉴的地方)。

话不多说,直接上效果图

image.png

在图片中可以发现,最终编写的代码是支持异步语法的,也就是说内容可以是远端内容,可以运行在浏览器侧。

现在开始具体的编码工作,第一步当然是技术选型啦。我觉得技术选型最主要还是看需求,我们的需求很简单,能够基于已有模板生成合理的DOM语法。还有一点就是能够从中学习与巩固typescript(个人感觉typescript最终会面向所有前端开发者)。所以,我们选择基于typescript来做开发语言。接下来就是选择测试框架,我们这里选择 mocha,搭配 chai 断言库。

我感觉写下去就变成从零开始编写一个极简的模板“引擎”,所以测试类的代码在这篇文章里就不多说,简单带过。接下来讲讲如何将一个模板字符串<div>{{ return 1 }}</div> 渲染成为<div>1</div>

这里我们借用js的一个特性,就是 new Function,它可以将一个字符串编译成一个函数,作用域为全局,所以这里就限制了{{}}里面不能引用局部变量,不过可以将new Function换成eval函数。那么,我们只需要拿到{{}}内容,然后去执行得到最终的值,再拼接即可完成模板内容的过程。

如何拿到{{}}里面的内容呢?我们可以用状态机、正则的方式。这里我们选择用正则的形式去实现,具体代码如下。

import { line } from "./units"

type Match = any[]

interface Options {
  mini: boolean
}

export class Render implements RenderType {
  private readonly options: Options

  constructor(
    options: Options = {
      mini: false
    }
  ) {
    this.options = options
  }

  output(html: string): string {
    const { mini } = this.options
    return mini ? html.replace(trim, ''): html
  }

  render(template?: string): Promise<string> | string {

    const tp = template || this.html
    if (typeof tp === 'undefined') {
      return ''
    }

    const matchArray = this.compiler(tp)
    const placeholder: string = '!%'

    let output: string = ''

    let sync: boolean = false
    let syncArray: Array<Promise<any>> = []

    for (let item of matchArray) {
      if (isString(item)) {
        output += item
      }
      if (isFunction(item)) {
        try {
          const cb = item()
          if (cb instanceof Promise) {
            sync = true
            syncArray.push(cb)
            output += placeholder
          } else {
            output += cb
          }
        } catch (e) {

        }
      }
    }

    if (sync) {
      return Promise.all(syncArray).then(value => {
        let html: string = ''
        output.split(placeholder).forEach((item, index) => {
          html += (item + (value[index] || ''))
        })
        return this.output(html)
      })
    }

    return this.output(output)
  }

  compiler(template: string): Match {

    if (typeof template === 'undefined') {
      console.warn('render function error: not find html template')
    }

    let matchArray: Match = []

    let matchString: RegExpMatchArray | null
    let tp: string = template.replace(line, ' ')

    while((matchString = match.exec(tp))) {
      const index: any = matchString.index
      const script: Script = matchString[1]
      const scriptBlock: Script = matchString[0]

      index !== 0 && matchArray.push(tp.slice(0, index))
      matchArray.push(new Function(script))

      tp = tp.substring(index + scriptBlock.length)
      matchString = match.exec(tp)
    }

    matchArray.push(tp)

    return matchArray
  }
}

compiler函数就是将字符串解析成由字符串和函数组成的数组,然后使用render函数编译生成DOM内容。所有代码可以去我仓库查看

github.com/Linkontoask…

友情链接:

  • gyron.cc 一个很简单的前端框架