At the beginning

In my previous article, I described the implementation of a code preview tool for online editing (portal: Quick build a code online edit preview tool), to achieve THE CSS, HTML, JS editing, but for the demo scene, vUE single file is also a better way to organize code, at least the author often write a variety of demo while writing vUE project, but it is not too convenient to share, Because a single file can not be directly run to see the effect, based on this, the author decided to add a vUE single file editing and preview function on the basis of the previous.

Ps. If you haven’t read the previous article, here’s a quick overview of the project: The basic framework uses ve3. X, the building tool uses Vite, and the code editor uses Monaco-Editor. The basic principle is to splice CSS, JS and HTML into a complete HTML string and throw it into iframe for preview.

In addition, there are some drawbacks in the project:

1. Vite does not support the CommonJS module. .

2. The three-party modules are currently placed under the public folder of the project and loaded as static resources on demand:

In addition, the Monaco Editor’s built-in module system conflicts with defined/require, so we need to manually modify each three-party module to support only global objects. For example:

The basic idea

To preview a single vUE file, you need to convert it to CSS, JS, and HTML that the browser can understand. The first thought was to use vue-loader for conversion, but after looking at its documentation, I found that it still had to be used with Webpack. However, THE author found a supporting module vue-template-Compiler, which provided some methods. A parseComponent method can be used to parse a single vue file and output the contents of each part. The output structure is as follows:

So the idea is clear:

1. HTML part, fixed structure:

<div id="app"></div>
Copy the code

2. CSS part, first determine whether to use CSS preprocessor, if yes, use the corresponding parser to convert to CSS, and then through the style tag inserted into the page.

In the 3.js part, taking the vue2.x version as an example, we finally need to generate the following structure:

new Vue({
    el: '#app'.template: ' '.// Part of the template
    / /... The other options
})
Copy the code

Other options are the script. Content parsed by vue-template-compiler, but the single file is basically in the form of export default {}. The template option is simply the content of template.content.

The idea here is to use Babel to convert export default {} to new Vue, and then add el and template attributes. This can be done by writing a Babel plug-in.

Install and use vue-template-compiler

First of all, the VUe-template-compiler module will also be placed in the public folder, so where is its browser version? We can install NPM I vue-template-compiler, and then find it in node_modules and find a file in it:

This is what we want, just copy it to the public folder (and comment out its module export of course), then delete the module, then we can use it through the global object:

// code is the vue single file content string
let componentData = window.VueTemplateCompiler.parseComponent(code)
// Process the content of style, script, template, and finally generate CSS string, JS string, HTML string
parseVueComponentData(componentData)
Copy the code

Generating HTML Strings

For the HTML section, we need to write a div to mount the vue instance:

const parseVueComponentData = async (data) => {
    // HTML simply renders a node to which the vue instance is mounted
    let htmlStr = `<div id="app"></div>`
    
    return {
        html: htmlStr
    }
}
Copy the code

Generating CSS Strings

If you don’t use a CSS preprocessor, you can simply return the style content, otherwise you need to use the corresponding preprocessor to convert it to CSS first:

const parseVueComponentData = async (data) => {
    / / compile CSS
    let cssStr = []
    // Vue can have multiple style blocks in a single file, so parsed styles is an array
    for(let i = 0; i < data.styles.length; i++) {
        let style = data.styles[i]
        // If CSS preprocessors are used, the lang field is not empty
        let preprocessor = style.lang || 'css'
        if(preprocessor ! = ='css') {
            The // load method loads the corresponding tripartite parsing module, as described in the previous article
            await load([preprocessor])
        }
        // CSS methods are parsed using the corresponding parser. See the previous article
        let cssData = await css(preprocessor, style.content)
        // Add the parsed CSS string to the result array
        cssStr.push(cssData)
    }
    return {
        // Finally concatenate the CSS of multiple style blocks into one
        css: cssStr.join('\r\n')}}Copy the code

The CSS above will call the corresponding CSS preprocessor parsing module to compile, such as less processing as follows:

const css = (preprocessor, code) = > {
    return new Promise((resolve, reject) = > {
        switch (preprocessor) {
            case 'css':
                resolve(code)
                break;
            case 'less':
                window.less.render(code)
                    .then((output) = > {
                        resolve(output.css)
                    },
                    (error) = > {
                        reject(error)
                    });
                break; }})}Copy the code

Generate JS string

We will compile the contents of the script section using Babel:

const parseVueComponentData = async (data, parseVueScriptPlugin) => {
    // Babel compiles and modifies ast by writing plug-ins
    let jsStr = data.script ? window.Babel.transform(data.script.content, {
        presets: [
            'es2015'.'es2016'.'es2017',].plugins: [
            / / the plugin
            parseVue2ScriptPlugin(data)
        ]
    }).code : ' '

    return {
        js: jsStr
    }
}
Copy the code

The Babel plugin is a function that takes a Babel object as a parameter and then needs to return an object. We can access the AST node in the visitor property of that object and make some modifications. Each function in the visitor receives two parameters: Path and state, path represents the object connected between two nodes, including node information and some operation methods. For detailed documentation of plug-in development, please refer to: Plugin-handbook.

The basic structure is as follows:

const parseVue2ScriptPlugin = (data) = > {
    return function (babel) {
        let t = babel.types
        return {
            visitor: {}}}}Copy the code

Convert the export default syntax

We need to convert the form of export default {} to the form of new Vue. To do this, we can use astExplorer to see the difference between the AST of the two structures:

We can see that the yellow parts are the same, but the outer nodes are different, so we can access the ExportDefaultDeclaration node and replace it with an ExpressionStatement. It is also easy to create a new node. Refer to the AST and babel-types documentation.

const parseVue2ScriptPlugin = (data) = > {
    return function (babel) {
        let t = babel.types
        return {
            visitor: {
                // Access the export default node and convert export Default to new Vue
                ExportDefaultDeclaration(path) {
                    // Replace itself
                    path.replaceWith(
                        Create an expression statement
                        t.expressionStatement(
                            // Create a new expression
                            t.newExpression(
                                // Create an identifier named Vue, the function name
                                t.identifier('Vue'),
                                // Function arguments
                                [
                                    // The argument is the node corresponding to the declaration attribute of the ExportDefaultDeclaration node
                                    path.get('declaration').node
                                ]
                            )
                        )
                    );
                }
            }
        }
    }
}
Copy the code

The effect is as follows:

We haven’t added the el and template attributes yet, so try using the AST tool first:

Obviously we need to access the ObjectExpression node and add two nodes to its Properties property. The first thing that comes to mind is this:

const parseVue2ScriptPlugin = (data) = > {
    return function (babel) {
        let t = babel.types
        return {
            visitor: {
                ExportDefaultDeclaration(path) {
                    // ...
                },
                ObjectExpression(path) {
                    // If the parent node is a new statement, add properties for that node
                    if (path.parent && path.parent.type === 'NewExpression' ) {
                        path.node.properties.push(
                            // el
                            t.objectProperty(
                                t.identifier('el'),
                                t.stringLiteral('#app')),// template
                            t.objectProperty(
                                t.identifier('template'),
                                t.stringLiteral(data.template.content)
                            )
                        )
                    }
                }
            }
        }
    }
}
Copy the code

What’s the problem with this? Suppose the code we want to convert looks like this:

new Vue({});
export default {
    created() {
        new Vue({});
    },
    data() {
        return {
            msg: "Hello world!"}; },mounted() {
        newVue({}); }};Copy the code

We should have just wanted to add these two attributes to the export Default object, but the actual effect is as follows:

We can see that all the objects of the new statement have been modified, which is obviously not what we want. What is the correct way to do this? We should recursively iterate over the ExportDefaultDeclaration node immediately after replacing it, and stop traversing as soon as we add these two attributes:

const parseVue2ScriptPlugin = (data) = > {
    return function (babel) {
        let t = babel.types
        return {
            visitor: {
                ExportDefaultDeclaration(path) {
                    // export default -> new Vue
                    // ...
                    // Add el and template attributes
                    path.traverse({
                        ObjectExpression(path2) {
                            if (path2.parent && path2.parent.type === 'NewExpression' ) {
                                path2.node.properties.push(
                                    // el
                                    t.objectProperty(
                                        t.identifier('el'),
                                        t.stringLiteral('#app')),// template
                                    t.objectProperty(
                                        t.identifier('template'),
                                        t.stringLiteral(data.template.content)
                                    ),
                                )
                                path2.stop()
                            }
                        }
                    });
                }
            }
        }
    }
}
Copy the code

The effect is as follows:

Now that the HTML, JS, and CSS sections have been processed, we can put them into a complete HTML string and throw it into an iframe to preview the result:

Converts the module.exports syntax

Module. exports = {} / / exports = {} / / module. Exports = {} / / module. Exports = {} / / module. Exports = {} / / module.

Module. exports is an ExpressionStatement, so we just need to visit the AssignmentExpression node and replace it with the newExpression node of new Vue:

const parseVue2ScriptPlugin = (data) = > {
    return function (babel) {
        let t = babel.types
        return {
            visitor: {
                // Parse the export default module syntax
                ExportDefaultDeclaration(path) {
                    // ...
                },
                // exports module syntax
                AssignmentExpression(path) {
                    try {
                        let objectNode = path.get('left.object.name')
                        let propertyNode = path.get('left.property.name')
                        if (
                            objectNode 
                            && objectNode.node === 'module' 
                            && propertyNode 
                            && propertyNode.node === 'exports'
                        ) {
                            path.replaceWith(
                                t.newExpression(
                                    t.identifier('Vue'),
                                    [
                                        path.get('right').node
                                    ]
                                )
                            )
                            // Add el and template attributes
                            // same logic as above}}catch (error) {
                        console.log(error)
                    }
                }
            }
        }
    }
}
Copy the code

Of course, this can go wrong if there are multiple module.exports = {} statements, but this scenario should be rare so we’ll leave it at that.

Other tools of practice

There are several tools available to support the loading and use of. Vue files on the browser side, such as http-vue-loader, as follows:

<! doctypehtml>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/http-vue-loader"></script>
  </head>

  <body>
    <div id="my-app">
      <my-component></my-component>
    </div>

    <script type="text/javascript">
      new Vue({
        el: '#my-app'.components: {
          'my-component': httpVueLoader('my-component.vue')}});</script>
  </body>
</html>
Copy the code

Next, we will implement it again according to its principle.

Leaving styles aside, let’s look at the basic HTML and JS parts:

const parseVueComponentData2 = (data) = > {
    let htmlStr = ` 
      
`
// Converts the/in the vue single file string to \/ because if not, the browser will mistake the template tag for the actual page tag, resulting in page parsing errors let jsStr = ` new Vue({ el: '#app', components: { 'vue-component': VueLoader(\`${data.replaceAll('/'.'\ \ /')}\ `)}}); ` return { html: htmlStr, js: jsStr, css: ' '}}Copy the code

You can see that we are using the vue single file as a component this time, and then we will implement a global method VueLoader that receives the contents of the single file and returns a component option object.

Instead of using vue-template-compiler, we parse it ourselves by creating a new HTML document, throwing the contents of the vUE single file into the body node of the document, and then iterating through the children of the body node to determine the parts based on the tag name:

// Global method
window.VueLoader = (str) = > {
    let {
        templateEl,
        scriptEl,
        styleEls
    } = parseBlock(str)
}

// Parse out the various parts of the vue single file and return the corresponding node
const parseBlock = (str) = > {
    // Create a new HTML document
    let doc = document.implementation.createHTMLDocument(' ');
    // Add the contents of the vue single file under the body node
    doc.body.innerHTML = str
    let templateEl = null
    let scriptEl = null
    let styleEls = []
    // Iterate over the children of the body node
    for (let node = doc.body.firstChild; node; node = node.nextSibling) {
        switch (node.nodeName) {
            case 'TEMPLATE':
                templateEl = node
                break;
            case 'SCRIPT':
                scriptEl = node
                break;
            case 'STYLE':
                styleEls.push(node)
                break; }}return {
        templateEl,
        scriptEl,
        styleEls
    }
}
Copy the code

Next we parse the contents of the script block, and we will eventually return an option object, which looks like this:

{
    name: 'vue-component',
    data () {
        return {
            msg: 'Hello world! '}},template: ' '
}
Copy the code

Module. exports object can be manually created and then accessed by script runtime, which is equivalent to assigning this option object to the module.exports object we defined.

window.VueLoader = (str) = > {
    // ...
    let options = parseScript(scriptEl)
}

/ / parse the script
const parseScript = (el) = > {
    let str = el.textContent
    let module = {
        exports: {}}let fn = new Function('exports'.'module', str)
    fn(module.exports, module)
    return module.exports
}
Copy the code

Add the template option and component name to the object, and return the object:

window.VueLoader = (str) = > {
    // ...
    options.template = parseTemplate(templateEl)
    options.name = 'vue-component'
    return options
}

// Returns the template content string
const parseTemplate = (el) = > {
    return el.innerHTML
}
Copy the code

The CSS parsing is the same as before, but http-VUe-loader also implements style scoped processing.

One disadvantage of this tool is that it does not support the Export Default module syntax.

Refer to the link

Wanglin2.github. IO /code-run-on… .

For the complete code, please go to the project repository: github.com/wanglin2/co… .

Quickly build a code online edit preview tool

astexplorer

http-vue-loader

Babel plugin-handbook

vue-template-compiler

babel-types