简易模板编译工具

180 阅读2分钟

项目中有一个需求是这样的:有一个轮播列表组件,提供工具栏,可以让用户通过配置来调整列表颜色、边框、大小和动画等各项参数。每次保存配置的时候重新渲染,刷新列表样式。所以这里列表的html和css将会有很多从配置文件获取而来的变量,而不是像以前渲染出来的模板一样固定数值。 利用ES6的template string可以很容易的做到,在js中动态渲染html:

const list = [
    {
        name: '四毛',
        description: '可爱'
    },
    {
        name: '八毛',
        description: '也可爱'
    }
]
let items = ``
for (const i of list) {
    items += `<li>${i.name}:${i.description}</li>`
}
const content = `
    <ul>
        ${items}
    </ul>
`
$(this.$container).html(content)

但是这样写结构样式和js逻辑没有分离,增加了编码和维护成本。希望的是可以把html和css放在模板中,编译成字符串的形式,给js使用。

思路

开始之前,我们选定轻便简单的gulp来构建文件。

首先,我们想要拥有单独的html模板文件,并且做到结构和样式分离。简单的方法就是在<style>标签里写样式,再用 gulp-inline-css 将css处理成内联样式。

第二步,带有样式的html模板文件要怎么变成js字符串呢?可以利用 gulp-html-to-js 将html处理成js。这个插件可以一同处理多个html模板,最终生成的js中,是这样的形式:

module.exports = Object.create(null)
module.exports['index.html'] = '<p>Hello world!</p>'

这样,在js中就能导入相应的模板字符串。 以上两步,gulpfile.js只需要这样处理:

const inlineCss = require('gulp-inline-css')
const html2js = require('gulp-html-to-js')

function templates () {
    return gulp.src('src/templates/*.html')
        .pipe(inlineCss())
        .pipe(html2js({concat: 'templates.js'}))
        .pipe(gulp.dest('./'))
}

exports.templates = templates

第三步,处理变量。以上的模板处理出来是一个字符串,但是我们需要利用配置文件动态改变样式,直接字符串模板显然不符合需求。我们可以这样做:将module.exports出来的内容处理成一个function,参数是配置文件数据,返回值是模板字符串和变量的拼接。形如:

function (config, data) {
  return '<p style="color:' +  config.color  + '">' + data.value + '</p>'
}

这样,可以将带有变量的html抽离出去,构建成js,再动态import,做到结构、样式、逻辑分离。现在最重要的一个问题,就是怎么模板变成拼接的字符串。

处理变量

通过 gulp-html-to-js 处理之后的文件, 我们通过 through2 拿到文件流。将文件流转成字符串,之后就可以对字符串操作。提取module.exports出来的内容,用正则将原本的模板转化成function。利用 through2 可以提取文件的文本内容,处理完我们想要的操作之后,记得字符串转回去流。

function templates () {
    return gulp.src('src/templates/*.html')
        .pipe(inlineCss())
        .pipe(html2js({concat: 'templates.js'}))
        .pipe(through.obj(function (file, encode, cb) {
            //  流转化成字符串
            let result = file.contents.toString()
            // 正则拿到“module.exports['index.html'] = '内容'” 导出的内容
            result = result.replace(/(module.exports\['.+?'\] = )('[\s\S]*?(?<!\\)')/g, function(match, p1, p2){
               // 将原来的字符串模板处理成方法,parse函数下文解释
               // return p1 + parse(p2)
            })
            file.contents = new Buffer(result) // 字符串转成流
            // 提供给下个pipe使用
            this.push(file)
            cb()
         }))
        .pipe(gulp.dest('./'))
}

在模板中,需要动态变更的变量,设置一个插值语法,比如常用的<%= %>

<p><%= data.value %><p>

我们要将这个模板变成:

function (config, data) {
  return '<p>' + data.value + '</p>'
}

可以使用正则替换:

function parse (content) {
    const evalExpr = /<%=(.+?)%>/g
    content = content
        .replace(evalExpr, "' + $1 +'" ))
    let compile = `function (config, data) {
        return '${content}'
        }`
    return compile
}

可以得到:

"function (config, data) {
      return '<p>' +  data.value  +'<p>'
  }"

我们还希望模板能处理简单的循环或者是判断语句,这里用<% %>来放置js语句。可以看到整个编译函数的核心就是将插值的变量/语句提取出来,与其他的进行字符串拼接。这里参考阮一峰老师提到过的编译函数 来优化一下代码。 写一个拼接函数:

 let output = ''
 const echo = function (html) {
    output += html
 }

上文中正则插值替换,就变成

echo('<p>' )
echo( data.value)
echo( '<p>'//正则
content = content.replace(evalExpr, `')\n echo(${expr})\n echo('` ))

对于js语句也是相同的处理方法。 有一点需要注意的是,我们生成的字符串都是用单引号,如果在表达式中需要用到字符串(也会有一个单引号),会被加上转义符,这里还要将转义符去掉。 对于css的变量,如果使用<的插值,css处理的插件会报错。这里我们使用{%= %}来进行插值,在内联处理之后,再转化成<,就可以不用另外对css进行处理了。 以下就是完整的编译函数。

function parse (content) {
    const cssExpr = /\\'\{(%[\s\S]+?%)\}\\'/g
    const evalExpr = /<%=(.+?)%>/g
    const expr = /<%([\s\S]+?)%>/g
    content = content
        .replace(cssExpr, "<$1>")
        .replace(evalExpr, function(match, p) {
            const expr = p.replace(/\\'/g, "'") // 处理被转义的单引号
            return `')\n echo(${expr})\n echo('`
        })
        .replace(expr, function(match, p) {
            const expr = p.replace(/\\'/g, "'")
            return `')\n ${expr}\n echo('`
        })
    let compile = `function (config, data) {
        let output = ''
        const echo = function (html) {
             output += html
        }
        echo(${content})
        return output
        }`
    return compile
}

这就完成了,我们可以这样书写html和css

content.html:

<% for (let count = 0; count < config.score.number; count++){%>
    <% if (count < 10) {%>
        <img src="<%= config.score.highlight %>" class="score-img"/>
        <% } else {%>
        <img src="<%= config.score.defaultIcon %>" class="score-img"/>
    <% }%>
<% }%>

<style>
.origin-list-flip-with-score .item-number {
    width: '{%= config.orderNumber.width %}'px;
    margin: '{%= config.nameArea.position === 'left' ? '0 5px 0 0' : '0 0 0 5px' %}';
}
</style>

在js中这样使用

import html from './templates.js'
$(this.$container).html(html['content.html'](this.style, {
    listData: this.listData
}))