- Column address: Front-end compilation and engineering
- Jouryjc
The previous article we understand the basic use of PEg. js, forget the children’s shoes suggested review, for this article’s edible effect will be better!
All talk and no action is worth learning. So this article implements a compiler (muck, toy, joy).
demand
We know that Vue’s template does not support Chinese tag names, as in this code:
< = "tomato" drop-down box to select values: data = "{" a list" : [{" name ":" π
", "id" : "tomatoes"}, {" name ":" π ", "id" : "banana"}], "total" : 2}"> < subcomponent ></ subcomponent ></ drop-down box >Copy the code
Results generated using AstExplorer:
As you can see, vue-template-Compiler recognizes the < drop-down box > component as text. The requirement is to compile the above code into the same AST as the other components. Let’s take a look at the component AST compiled correctly:
We focus on the four attributes type, Tag, children, and attrs, and the other fields are additional information. Since this article focuses on compilation logic, other fields can be added based on peg.js actions, so we won’t go into details.
Let’s implement zh-template-Compiler in the figure above.
Analysis of the
Based on the above requirements, we can analyze and get the morphology and grammar we need to identify:
- Correctly identify component parent-child relationships; in
vue2
Template compilation, through the re and stack to maintain the relationship between the start tag and the end tag, no contact with children can go toTemplate compilationUnderstanding.PEG.js
Can be directly through the rules to match; - Component property matching; Be able to place the
props
Identify aast
In thename
εvalue
And can distinguish between static and dynamic properties (v-bind
); For complex typesvalue
Objects, for example, that are expected to behave better than just strings; - The component name and property name can only contain Chinese;
The test case
We tend to use single tests to understand the smallest and most fine-grained features of a framework, and the same goes for combing a scene.
For the first requirement of the above analysis, we can write the following use cases (π self-closing component logic is not handled oh, interested children can fork the project to practice) :
const { parse } = require('.. /src/zh-template-compiler')
describe('zh-template-compiler'.() = > {
test('Component without attributes'.() = > {
const template = '< component >
'
const ast = parse(template)
expect(ast.attrs.length).toBe(0)
expect(ast.children.length).toBe(0)
expect(ast.type).toBe(1)
expect(ast.tag).toBe('components')
})
test('Include child components'.() = > {
const template = '< component >< subcomponent >
'
const ast = parse(template)
expect(ast).toMatchSnapshot()
})
test('Contains multiple child components'.() = > {
const template = '< component >< first sub-component >
'
const ast = parse(template)
expect(ast).toMatchSnapshot()
})
test('Contains multiple layers of child components'.() = > {
const template = '< component >< subcomponent >< grandchild component >
'
const ast = parse(template)
expect(ast).toMatchSnapshot()
})
})
Copy the code
The second requirement is to identify props and distinguish between static and dynamic:
describe('zh-template-compiler'.() = > {
/ /... Follow the above code
test('Components with static properties'.() = > {
const template = '< component properties =" value ">
'
const ast = parse(template)
const attr = ast.attrs
expect(attr.length).toBe(1)
expect(attr[0]).toEqual({
isBind: false.name: "Properties".value: "Value"
})
})
test('Components with dynamic properties'.() = > {
const template = '< component: Property =" value ">
'
const ast = parse(template)
const attr = ast.attrs
expect(attr.length).toBe(1)
expect(attr[0]).toEqual({
isBind: true.name: "Properties".value: "Value"
})
})
test('Complex attribute values'.() = > {
const template = ` < = "tomato" drop-down box to select values: data = "{" a list" : [{" name ":" π
", "id" : "tomatoes"}, {" name ":" π ", "id" : "banana"}], "total" : 2}"> < subcomponent >
'
const ast = parse(template)
expect(ast).toMatchSnapshot()
})
test('Components with static + dynamic properties'.() = > {
const template = '< component static property =" value of static property ": Dynamic property =" value of dynamic property ">
'
const ast = parse(template)
const attrs = ast.attrs
expect(attrs.length).toBe(2)
expect(attrs).toEqual([{
isBind: false.name: "Static properties".value: "Static property values"
}, {
isBind: true.name: "Dynamic properties".value: "Values of dynamic properties"})})})Copy the code
Finally, component and attribute names that can only contain Chinese use cases are simpler:
describe('zh-template-compiler'.() = > {
/ /... Follow the above code
test('Component name can contain only Chinese characters'.() = > {
const template = '< component 1>
'
try {
parse(template)
} catch (e) {
console.log(e)
expect(e.message).toBe('Expected ":", ">", or [δΈ-ιΎ₯] but "1" found.')
}
})
test('Attribute names can contain only Chinese characters'.() = > {
const template = '< component property 1=" value 1">
'
try {
parse(template)
} catch (e) {
console.log(e)
expect(e.message).toBe('Expected '=' or [0-9] but '1' found.')}})})Copy the code
coding
Start by writing entry rules:
Program
= program:Tag {
return program;
}
Copy the code
Remember that –allowed-start-rules is configured earlier. If not, the first rule is implemented by default. Next comes the core rule definition:
// A complete template definition
// ws is a whitespace. You can type as many whitespace characters as you like before starting the tag
// StartTag, StartTag matching
// children: (Tag*) key, key, key!! Repeatedly matches the Tag rule.
// EndTag, EndTag matching
// The last action is the key, you can do any judgment, formatting when you get the matching information
// If the component name does not match the start tag and end tag, an error must be reported. Vue2 maintains this relationship through the stack, and you can see that peg.js is much simpler.
Tag
= ws
start:StartTag
children: (Tag*)
end:EndTag
ws
{
if(start.tag ! == end.tag) {throw Error('Start tag and end tag do not match')}return {
...start,
children
}
}
// Start tags and attributes
// Component :$zh = [u4e00-\u9fa5]+ Matches more than one Chinese character. If you don't add the $, you get an array of matches. If you forget this grammar, go back to the previous chapter
// attrsList: (...) * Matches any attR and stores attrList
// attrs: attrs matches individual component properties
// Finally, the action process returns an object with type = 1, the same as VNode in VUe2, representing the component type
StartTag
= "<"
ws
component:$zh
attrList: (
ws
attrs:Attrs
ws
{
if (attrs.name) {
return attrs
}
}
)*
">" {
return {
type: 1.tag: component,
attrs: attrList
}
}
// End tag
EndTag
= "< /"
component:$zh
">"
{
return {
tag: component
}
}
// Matches Chinese characters
zh = [\u4e00-\u9fa5]+
// Matches the component properties
// isBind:name_separator ? Matches: returns the corresponding string, and then returns NULL
// attrName:$zh+ The attribute name is a String in Chinese
/ / quotation_mark quotes
// attrValue:($zh/JSON_text) the value of the attrValue attribute can be a Chinese or a JSON text. JSON_text uses the definition in the previous article.
// When all matches are complete, return the matching object
Attrs
= isBind:name_separator ?
attrName:$zh+
"="
quotation_mark
attrValue:(
$zh / JSON_text
)
quotation_mark {
if (attrName) {
let hasVbind = isBind ? true : false
return {
isBind: hasVbind,
name: attrName,
value: attrValue
}
}
}
Copy the code
The core rule definition is as shown in the above code, the difficulty is to parse the sub-components, by using the idea of rule recursion (similar to function recursion) to solve it becomes so easy.
validation
Finally, generate a compiler with the above rules:
npx pegjs -o zh-template-compiler.js src/zh-template-compiler.pegjs
Copy the code
π° at the beginning of the article produces the following AST results:
{
"type": 1."tag": "Drop-down box"."attrs": [{"isBind": false."name": "Selected value"."value": "Tomato"
},
{
"isBind": true."name": "Data"."value": {
"list": [{"Name": "π
"."id": "Tomato"
},
{
"Name": "π"."id": "Banana"}]."total": 2}}]."children": [{"type": 1."tag": "Subcomponent"."attrs": []."children": []}]}Copy the code
The result of executing the test case is shown below:
The simplest Chinese template compiler is complete. With this exercise, you will become more familiar with the basics of PEg.js and will be able to use it to solve everyday development problems. If you want to further refine the compiler, fork the zh-template-compiler
The next article will generate actual drop-down boxes on the page based on AST results. What would you do?
Pay attention to our
Everyone’s support is our motivation to continue to move forward, come to pay attention to our deep trust in the front end team ~
In the meantime, if you are interested, please join us and send your resume to [email protected].