Hello everyone, I am Xiao Yu. As the final (part) of the manual implementation compiler, mischievously changed a title. Review the previous two:

  • Through the PEG API and explain the official case to understand the basic usage of this tool;

  • We practiced an example of writing templates in Chinese and finally parsing them into AST to deepen our understanding of THE PEG API.

As mentioned at the end of the previous article, after compiling into the AST, you need to transform and eventually generate code. The optimize tag static node and generate generated code are included in the template compilation of Vue. At Babel do node traverse with @babel/traverse and generate code with @babel/generator. Today, Xiao Yu will combine the GENERATED AST with the VUE framework to generate the browser’s real DOM, so as to practice the process of AST generate code.

The target

There are four ways to combine vUE to generate:

  1. throughASTgeneraterenderFunction string (this article details this way, other just interested children shoes can try to practice);
  2. By transforming theASTTo generate thevueVNodeThe structure of the;
  3. throughASTgenerateSFCIn thetemplate;
  4. throughASTTo encapsulate a setpatchLogic, byDOM-APITo deal with;

Introduces a

Children’s shoes who have not read the series may not be very clear about the situation, here is a brief mention. We have the following Chinese template for the middle part:

< drop-down box value = "tomato" > < option value = "tomato" > tomato < / options > < option value = "banana" > banana < / options > < / a drop-down box >Copy the code

The AST structure generated by zh-template-Compiler is as follows:

{
  "type": 1."tag": "Drop-down box"."attrs": [{"isBind": false."name": "Value"."value": "Tomato"}]."children": [{"type": 1."tag": "Options"."attrs": [{"isBind": false."name": "Value"."value": "Tomato"}]."children": [
        "Tomato"] {},"type": 1."tag": "Options"."attrs": [{"isBind": false."name": "Value"."value": "Banana"}]."children": [
        "Banana"]]}}Copy the code

To generate a DOM that can be displayed in a browser, we need HTML code that looks like this (this can be done in conjunction with any UI component library, but for the sake of this article, we don’t want to complicate the scene) :

<select value="Tomato">
  <option value="Tomato">tomato</option>
  <option value="Banana">banana</option>
</select>
Copy the code

Render (DOM) render (vue) render (DOM) render (vue)

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>zh-template-compiler</title>
</head>
<body>
  <div id="app"></div>
  <script src=".. /node_modules/vue/dist/vue.global.js"></script>
  <script>
    const { createApp, h, ref } = Vue

    const app = createApp({     
      render (_ctx) {
        return h('select', {
          value: 'tomato'
        }, [
          h('option', { value: 'tomato' }, 'tomato'),
          h('option', { value: 'banana' }, 'banana')
        ])
      }
    })

    app.mount('#app')
  </script>
</body>
</html>
Copy the code

Now that we can display this, our task is more detailed and explicit, converting the AST to the render function:

  1. Chinese and label mapping, “drop down box” intoselect, “Options” is converted tooption;
  2. attrsConvert to attributes on the tag,nameEqual to the valueattrAll generatedvalue= “xx”;
  3. childrenPerform the first and second steps above recursively;

We convert the AST into code snippets and now we’re done with the analysis, and we’re ready to roll out the code.

test

Before this step is directly attached to the test code, that may be for the problem of thinking and writing the test code does not have much guidance. In this paper, I will write the test step by step from simple to difficult. The test tool I use is ANTfu. Vitest ranked ninth in the list of 2021 test framework within two weeks, which is very easy to use and fast to pit.

First of all, the single test must be written from the minimum scenario. For 🌰 in this paper, the minimum AST is:

const ast: NODE = {
  type: 1.tag: "Drop-down box".attrs: [].children: []}Copy the code

render (_ctx) {
    return h('select'}, {})Copy the code

Here comes the first test case:

describe("Chinese AST generates render function".() = > {
  test("Single node without attributes".() = > {
    const ast: NODE = {
      type: 1.tag: "Drop-down box".attrs: [].children: []
    }

    expect(generate(ast)).toBe(`render (_ctx) { return h('select', {})}`)})})Copy the code

render (_ctx) {
    return h('select', {value: 'tomato'})}Copy the code

Here comes the second test case

test('Single node with attributes'.() = > {
  const ast: NODE = {
    type: 1.tag: 'Drop-down box'.attrs: [{isBind: false.name: 'value'.value: 'tomato'}].children: []
  }

  expect(generate(ast)).toBe(` render (_ctx) {return h (' select '{" value ":" tomatoes "})} `)})Copy the code

The third use case naturally considers children, and the value of attrs in the second test case should be discarded. When writing children, since children support strings and node types, we should first consider the text scenario according to the principle of simplicity and depth:

// Chinese template< options > Tomato </ options >// Corresponding HTML code
<option>tomato</option>

// Corresponding render function
render (_ctx) {
    return h('option', {}, 'tomato')}// Generated test case code
test('Nodes with text children'.() = > {
  const ast: NODE = {
    type: 1.tag: 'options'.attrs: [].children: ['tomato']
  }

  expect(generate(ast)).toBe(Render (_ctx) {return h('option', {}, 'tomato ')}')})Copy the code

There is one detail in the above thought process, why not use a “drop down box”? Suddenly switched to “options”? With this in mind, one might be tempted to check the MDN documentation out of curiosity and supplement one’s basic knowledge along the way. Isn’t that the charm of unit testing?

After writing children is the text case, let’s write children is the node case:

// Chinese template< drop-down box >< options ></ Options ></ drop-down box >// Corresponding HTML code
<select>
  <option></option>
</select>

// Corresponding render function
render (_ctx) {
    return h('select', {}, [h('option'}, {})])// Generated test case code
 test('Nodes with node children only'.() = > {
   const ast: NODE = {
     type: 1.tag: 'Drop-down box'.attrs: [].children: [{type: 1.tag: 'options'.attrs: [].children: [
         ]
       }
     ]
   }

   expect(generate(ast)).toBe(`render (_ctx) { return h('select', {}, [h('option', {})])}`)})Copy the code

At this point, all the basic types of individual attributes are considered. The following is the test code between the combination of attributes, which is not listed one by one. Permutation and combination, the collocation of all attributes should be considered completely, and the code is given directly:

describe("Ast Generator".() = > {

  // Omit the above analyzed use cases...

  test('Node with two types of children'.() = > {
    const ast: NODE = {
      type: 1.tag: 'Drop-down box'.attrs: [].children: [{type: 1.tag: 'options'.attrs: [].children: [
            'tomato'
          ]
        }
      ]
    }

    expect(generate(ast)).toBe(` render (_ctx) {return h (' select '{} and [h (' option, {},' tomato)])} `)
  })

  test('Tagged child node'.() = > {
    const ast: NODE = {
      type: 1.tag: 'Drop-down box'.attrs: [].children: [{type: 1.tag: 'options'.attrs: [{isBind: false.name: 'value'.value: 'tomato'}].children: [
            'tomato'
          ]
        }
      ]
    }

    expect(generate(ast)).toBe(` render (_ctx) {return h (' select '{} and [h (" option ", "value" : "tomato", attach' tomato)])} `)
  })

  test('Node with 2 tagged children'.() = > {
    const ast: NODE = {
      type: 1.tag: 'Drop-down box'.attrs: [].children: [{type: 1.tag: 'options'.attrs: [].children: [
            'tomato'] {},type: 1.tag: 'options'.attrs: [].children: [
            'banana'
          ]
        }
      ]
    }

    expect(generate(ast)).toBe(` render (_ctx) {return h (' select '{} and [h (' option, {},' tomato), h (' option, {}, "banana")])} `)})});Copy the code

When the test case is written, run the test:

vitest -c vite.config.ts -u
Copy the code

Because we didn’t write a single line of code, we’re all red:

coding

With unit testing, we will focus our attention on the code, this link we just need to write the code well, will not appear while thinking about requirements while writing code, found in the middle of the unmet requirements and all kinds of complementary logic dilemma. Most of the time, everyone writes beautiful code first hand, but because of subsequent supplemental requirements scenarios and iterations, it becomes a “mountain of shit.”

Go back to the requirements in 🌰 and go through the AST in depth to generate the code. The core code is as follows:

function generateItem (node: NODE) {
  const { attrs, tag, } = node

  // Get the specific HTML tag according to the Chinese tag
  const dom = getTag(tag)
  // Get a specific DOM attribute based on the Chinese attribute name
  const props = generateAttrs(attrs)

  return `h('${dom}', The ${JSON.stringify(props)}`
}

export function generate (ast: NODE) :string {
  let code = `render (_ctx) { return `

  function dfs (node: NODE) {
    if(! node) {return;
    }

    let str = generateItem(node)
    let children = node.children
    let len = children.length

    // Text condition
    if (len === 1 && typeof children[0= = ='string') {
      str += `, '${children[0]}') `
    // There are no child nodes
    } else if (len === 0) {
      str += ') '
    } else {
      // Array of child nodes
      let childrenArr = []
      for (let item of children) {
        childrenArr.push(dfs(item as NODE))
      }

      str += `, [${childrenArr.join(', ')}]) `
    }

    return str
  }

  code += `${dfs(ast)}} `
  return code
}
Copy the code

The code is relatively simple, and those who are interested can go to Github to view the overall code. The whole process is similar to the DOM rendering process of VNode through patch in VUE. After testing, all test cases turned green:

Compiler and generate are all ok, then the whole DEMO, combine the two:

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  </style>
  <title>zh-template-compiler</title>
</head>

<body>
  <div id="app"></div>
  <script src=".. /packages/parser/dist/zh-template-compiler.global.js"></script>
  <script src=".. /packages/generate/dist/zh-template-generate.global.js"></script>
  <script src=".. /node_modules/vue/dist/vue.global.js"></script>
  <script>
    
    const template = ` < drop-down box value = "tomato" > < option value = "tomato" > tomato < / options > < option value = "banana" > banana < / options > < / a drop-down box > `;
    const ast = zhTemplateCompiler.parse(template)

    const { createApp, h, ref } = Vue

    const app = createApp({
      render (_ctx) {
        const fn = new Function(`return ${zhTemplateGen.generate(ast)}`)

        return fn()
      }
    })
    app.mount('#app')
  </script>
</body>

</html>
Copy the code

Finally run the HTML:

conclusion

As the last article in the compiler series, the AST generated by the Chinese template in the middle part was traversed and the final render code was generated, basically going through three steps of parse, traverse and generate. In addition to the use of simple and interesting examples to help understand, there are a lot of hot technologies, such as PNPM, Vitest; Finally, there are some common development tips, such as step-by-step instructions for TDD, how to organize the workspace using PNPM, and so on.