功能实现之简单的文本渲染引擎

某日突发奇想想到了一个页面渲染引擎的实现方法,这篇文章用来讲述其简单的原理

实现的功能

将下列字符串编译成正确的结果

1
2
3
4
5

<% for(let i = 0 ;i< this.l;i++){ %>
<li><%= i%></li>
<% } %>

l=2 时变成

1
2
3

<li>0</li><li>1</li>

实现过程

截取内容

通过截取开始符号和结束符号的方式,我们可以截取 渲染文本渲染脚本 交替的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

let arr = [str];
while (true){
let str = arr.pop();

let start = str.search('<%');
let end = str.search('%>');

if (start === -1){
arr.push(str)
break
}

let bstr = str.substring(0, start);
let instr = str.substring(start + 2, end);
str = str.substring(end + 2);

arr.push(bstr)
arr.push(instr)
arr.push(str)
}

return arr;

解析截取的数组

我们看过程可以看出 0,2,4... 都是用于渲染的文本 1,3,5... 是用于渲染的脚本。我们可以使用构建函数脚本的方式来实现渲染。

同时判断一下开头为等号的执行变量的赋值操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

let renderBuf = `let renderout = ''\n`;
arr.forEach((data, index) => {
if (index % 2 === 0 && data){
renderBuf += `renderout += "${data.trim()}"\n`
} else {
if (data[0] === '='){
renderBuf += `renderout += ${data.substr(1).trim()}\n`
} else {
renderBuf += `${data.trim()}\n`
}
}
})
renderBuf += `return renderout`;

于是就构建了我们的脚本-。-

运行脚本

1
2
3
4

let x = new Function('locals', renderBuf);
x.call(data)

这里使用 call 来导入环境变量,使用 this 来获取脚步外传入的值

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

function render(str, data){
let arr = [str];
while (true){
let str = arr.pop();

let start = str.search('<%');
let end = str.search('%>');

if (start === -1){
arr.push(str)
break
}

let bstr = str.substring(0, start);
let instr = str.substring(start + 2, end);
str = str.substring(end + 2);

arr.push(bstr)
arr.push(instr)
arr.push(str)
}

let renderBuf = `let renderout = ''\n`;
arr.forEach((data, index) => {
if (index % 2 === 0 && data){
renderBuf += `renderout += "${data.trim()}"\n`
} else {
if (data[0] === '='){
renderBuf += `renderout += ${data.substr(1).trim()}\n`
} else {
renderBuf += `${data.trim()}\n`
}
}
})
renderBuf += `return renderout`;

let x = new Function('locals', renderBuf);
return x.call(data)
}

这样就实现了一个特别简单的渲染引擎,当然这个还很简陋,
我们只需要进行如下改进,就可以实现一个更加好用的简单的页面渲染引擎了(其实就是个渲染引擎)

  1. 去除 \n

    由于可能在编写的时候为了可读性而换行,去除 \n 可以避免一些莫名其妙的问题

  2. 自定义开闭标识

    仅仅是支持着玩~

  3. 添加默认值

  4. 增加没有闭合的错误抛出

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

function render(str, data = {}, options = {}){
let {
startSign = "<%",
endSign = "%>"
} = options;


str = str.replace('\n', '');
let arr = [str];
while (true){
let str = arr.pop();

let start = str.search(startSign);
let end = str.search(endSign);

if (start === -1){
arr.push(str)
break
} else if (end === -1){
throw "no close !"
}

let bstr = str.substring(0, start);
let instr = str.substring(start + startSign.length, end);
str = str.substring(end + endSign.length);

arr.push(bstr)
arr.push(instr)
arr.push(str)
}

let renderBuf = `let renderout = ''\n`;
arr.forEach((data, index) => {
if (index % 2 === 0 && data){
renderBuf += `renderout += "${data.trim()}"\n`
} else {
if (data[0] === '='){
renderBuf += `renderout += ${data.substr(1).trim()}\n`
} else {
renderBuf += `${data.trim()}\n`
}
}
})
renderBuf += `return renderout`;

let renderFun = new Function('locals', renderBuf);
return renderFun.call(data)
}

后记

本来这个功能很早就想过实现,但当时没有想通,直到今天(2017-5-17)在写字符串模版信息提取时,写着写着写歪了,写成了模版渲染,我的内心是崩溃的。

END

2017-05-17 完成

2017-01-03 立项