项目中有一个需求是这样的:有一个轮播列表组件,提供工具栏,可以让用户通过配置来调整列表颜色、边框、大小和动画等各项参数。每次保存配置的时候重新渲染,刷新列表样式。所以这里列表的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
}))