前言
模板引擎的作用就是將模板渲染成html,html = render(template,data)
,常見的js模板引擎有Pug,Nunjucks,Mustache等。網上一些製作模板引擎的文章大部分是用正規表示式做一些hack工作,看完能收穫的東西很少。本文將使用編譯原理那套理論來打造自己的模板引擎。之前玩過一年Django,還是偏愛那套模板引擎,這次就打算自己用js寫一個,就叫jstemp
預覽功能
寫一個庫,不可能一次性把所有功能全部實現,所以我們第一版就挑一些比較核心的功能
var jstemp = require('jstemp');
// 渲染變數
jstemp.render('{{value}}', {value: 'hello world'});// hello world
// 渲染if/elseif/else表示式
jstemp.render('{% if value1 %}hello{% elseif value %}world{% else %}byebye{% endif %}', {value: 'hello world'});// world
// 渲染列表
jstemp.render('{%for item : list %}{{item}}{%endfor%}', {list:[1, 2, 3]});// 123複製程式碼
詞法分析
詞法分析就是將字串分割成一個一個有意義的token,每個token都有它要表達的意義,供語法分析器去建AST。
jstemp的token型別如下
{
EOF: 0, // 檔案結束
Character: 1, // 字串
Variable: 2, // 變數開始{{
VariableName: 3, // 變數名
IfStatement: 4,// if 語句
IfCondition: 5,// if 條件
ElseIfStatement: 6,// else if 語句
ElseStatement: 7,// else 語句
EndTag: 8,// }},%}這種閉合標籤
EndIfStatement: 9,// endif標籤
ForStatement: 10,// for 語句
ForItemName: 11,// for item 的變數名
ForListName: 12,// for list 的變數名
EndForStatement: 13// endfor 標籤
};複製程式碼
一般來說,詞法分析有幾種方法(歡迎補充)
- 使用正規表示式
- 使用開源庫解析,如ohm,yacc,lex
- 自己寫有窮狀態自動機進行解析
作者本著自虐的心理,採取了第三種方法。
舉例說明有窮狀態自動機,解析<p>{{value}}</p>
的過程
- Init 狀態
- 遇到<,轉Char狀態
- 直到遇到{轉化為LeftBrace,返回一個token
- 再遇{轉Variable狀態,返回一個token
- 解析value,直到}},再返回一個token
- }}後再轉狀態,再返回token,轉init狀態
結果是{type:Character,value:'<p>'}
,{type:Variable}
,{type:VariableName, valueName: 'value'}
,{type:EndTag}
,{type:Character,value:'</p>'}
這五個token。(當然如果你喜歡,可以把{{value}}
當作一個token,但是我這裡分成了五個)。最後因為考慮到空格和if/elseif/else,for等情況,狀態機又複雜了許多。
程式碼的話就是一個迴圈加一堆switch 轉化狀態(特別很累,也很容易出錯),有一些情況我也沒考慮全。截一部分程式碼下來看
nextToken() {
Tokenizer.currentToken = '';
while (this.baseoffset < this.template.length) {
switch (this.state) {
case Tokenizer.InitState:
if (this.template[this.baseoffset] === '{') {
this.state = Tokenizer.LeftBraceState;
this.baseoffset++;
}
else if (this.template[this.baseoffset] === '\\') {
this.state = Tokenizer.EscapeState;
this.baseoffset++;
}
else {
this.state = Tokenizer.CharState;
Tokenizer.currentToken += this.template[this.baseoffset++];
}
break;
case Tokenizer.CharState:
if (this.template[this.baseoffset] === '{') {
this.state = Tokenizer.LeftBraceState;
this.baseoffset++;
return TokenType.Character;
}
else if (this.template[this.baseoffset] === '\\') {
this.state = Tokenizer.EscapeState;
this.baseoffset++;
}
else {
Tokenizer.currentToken += this.template[this.baseoffset++];
}
break;
case Tokenizer.LeftBraceState:
if (this.template[this.baseoffset] === '{') {
this.baseoffset++;
this.state = Tokenizer.BeforeVariableState;
return TokenType.Variable;
}
else if (this.template[this.baseoffset] === '%') {
this.baseoffset++;
this.state = Tokenizer.BeforeStatementState;
}
else {
this.state = Tokenizer.CharState;
Tokenizer.currentToken += '{' + this.template[this.baseoffset++];
}
break;
// ...此處省去無數case
default:
console.log(this.state, this.template[this.baseoffset]);
throw Error('錯誤的語法');
}
}
if (this.state === Tokenizer.InitState) {
return TokenType.EOF;
}
else if (this.state === Tokenizer.CharState) {
this.state = Tokenizer.InitState;
return TokenType.Character;
}
else {
throw Error('錯誤的語法');
}
}複製程式碼
語法分析
當我們將字串序列化成一個個token後,就需要建AST樹。樹的根節點rootNode為一個childNodes陣列用來連線子節點
let rootNode = {childNodes:[]}複製程式碼
字串節點
{
type:'character',
value:'123'
}複製程式碼
變數節點
{
type:'variable',
valueName: 'name'
}複製程式碼
if 表示式的節點和for表示式節點可以巢狀其他語句,所以要多一個childNodes陣列來裝語句內的表示式,childNodes 可以裝任意的node,然後我們解析的時候遞迴向下解析。elseifNodes 裝elseif/else 節點,解析的時候,當if的conditon為false的時候,按順序取elseifNodes陣列裡的節點,誰的condition為true,就執行誰的childNodes,然後返回結果。
// if node
{
type:'if',
condition: '',
elseifNodes: [],
childNodes:[],
}
// elseif node
{
type: 'elseif',// 其實這個屬性沒用
condition: '',
childNodes:[]
}
// else node
{
type: 'elseif',// 其實這個屬性沒用
condition: true,
childNodes:[]
}複製程式碼
for節點
{
type:'for',
itemName: '',
listName: '',
childNodes: []
}複製程式碼
舉例:
let template = `
<p>how to</p>
{%for num : list %}
let say{{num.num}}
{%endfor%}
{%if obj%}
{{obj.test}}
{%else%}
hello world
{%endif%}
`;
// AST樹為
let rootNode = {
childNode:[
{
type:'char',
value: '<p>how to</p>'
},
{
type:'for',
itemName: 'num',
listName: 'list',
childNodes:[
{
type:'char',
value:'let say',
},
{
type: 'variable',
valueName: 'num.num'
}
]
},
{
type:'if',
condition: 'obj',
childNodes: [
{
type: 'variable',
valueName: 'obj.test'
}
],
elseifNodes: [
{
type: 'elseif',
condition:true,
childNodes:[
{
type: 'char',
value: 'hello world'
}
]
}
]
}
]
}複製程式碼
具體建樹邏輯可以看程式碼
解析AST樹
解析變數節點
從rootNode節點開始解析
let html = '';
for (let node of rootNode.childNodes) {
html += calStatement(env, node);
}複製程式碼
calStatement為所有語句的解析入口
function calStatement(env, node) {
let html = '';
switch (node.type) {
case NodeType.Character:
html += node.value;
break;
case NodeType.Variable:
html += calVariable(env, node.valueName);
break;
case NodeType.IfStatement:
html += calIfStatement(env, node);
break;
case NodeType.ForStatement:
html += calForStatement(env, node);
break;
default:
throw Error('未知node type');
}
return html;
}複製程式碼
解析變數
// env為資料變數如{value:'hello world'},valueName為變數名
function calVariable(env, valueName) {
if (!valueName) {
return '';
}
let result = env;
for (let name of valueName.split('.')) {
result = result[name];
}
return result;
}複製程式碼
解析if 語句及condition 條件
// 目前只支援變數值判斷,不支援||,&&,<=之類的表示式
function calConditionStatement(env, condition) {
if (typeof condition === 'string') {
return !!calVariable(env, condition);
}
return !!condition;
}
function calIfStatement(env, node) {
let status = calConditionStatement(env, node.condition);
let result = '';
if (status) {
for (let childNode of node.childNodes) {
// 遞迴向下解析子節點
result += calStatement(env, childNode);
}
return result;
}
for (let elseifNode of node.elseifNodes) {
let elseIfStatus = calConditionStatement(env, elseifNode.condition);
if (elseIfStatus) {
for (let childNode of elseifNode.childNodes) {
// 遞迴向下解析子節點
result += calStatement(env, childNode);
}
return result;
}
}
return result;
}複製程式碼
解析for節點
function calForStatement(env, node) {
let result = '';
let obj = {};
let name = node.itemName.split('.')[0];
for (let item of env[node.listName]) {
obj[name] = item;
let statementEnv = Object.assign(env, obj);
for (let childNode of node.childNodes) {
// 遞迴向下解析子節點
result += calStatement(statementEnv, childNode);
}
}
return result;
}複製程式碼
結束語
目前的實現的jstemp功能還比較單薄,存在以下不足:
- 不支援模板繼承
- 不支援過濾器
- condition表示式支援有限
- 錯誤提示不夠完善
- 單元測試,持續整合沒有完善
...
未來將一步步完善,另外無恥求個star
github地址