Rendering persisted dynamic templates in VueJS

Viewed 87

I'm exploring ways of rendering dynamic VueJS templates that are persisted in a database. The effect I'm looking to achieve is similar to the Liquid templating engine, but with VueJS components. The templates will then be later rendered by a SSR framework, like NuxtJS.

For example, here's a Cart template, which somebody can edit and save to the database.

<div>
  <div v-for="product in products" :key="product.id">
    <h3>{{ product.title }}</h3>
    <button @click="REMOVE_PRODUCT(product.id)">X</button>
  </div>
</div>

I have experimented with some things like

v-html

  • Cannot add event listeners directly to elements ( see button in example ), as well as attributes
  • Reactivity doesn't work very efficiently, as V-HTML runs on every render and replaces the html content of the parent element
  • Cannot use other components, like router-link

Using the runtime compiler

  • Larger bundle ( runtime compiler ) and performance hits ( runtime compilation ), though I'm sure any solution would involve these.

Generic problems with any implementation

  • Guard against bad actors - I would like to whitelist actions and variables they can access in a given template, to protect the server against, for example, recursive functions, which would halt the server.

I'm sure that there are a lot more potential problems that inherently come with the desire of rendering this way, so feel free to add to the list in your answers.

What I'm currently experimenting and thinking about is using the Vue Compiler in an Editor component ( the component which the users will use to edit their templates ), which can be seen here.

The example template is compiled to this AST

{
"type": 1,
"tag": "div",
"attrsList": [],
"attrsMap": {},
"rawAttrsMap": {},
"children": [
    {
        "type": 1,
        "tag": "div",
        "attrsList": [],
        "attrsMap": {
            "v-for": "product in products",
            ":key": "product.id"
        },
        "rawAttrsMap": {},
        "parent": "[循环引用]",
        "children": [
            {
                "type": 1,
                "tag": "h3",
                "attrsList": [],
                "attrsMap": {},
                "rawAttrsMap": {},
                "parent": "[循环引用]",
                "children": [
                    {
                        "type": 2,
                        "expression": "_s(product.title)",
                        "tokens": [
                            {
                                "@binding": "product.title"
                            }
                        ],
                        "text": "{{ product.title }}",
                        "static": false
                    }
                ],
                "plain": true,
                "static": false,
                "staticRoot": false
            },
            {
                "type": 3,
                "text": " ",
                "static": true
            },
            {
                "type": 1,
                "tag": "button",
                "attrsList": [
                    {
                        "name": "@click",
                        "value": "REMOVE_PRODUCT(product.id)"
                    }
                ],
                "attrsMap": {
                    "@click": "REMOVE_PRODUCT(product.id)"
                },
                "rawAttrsMap": {},
                "parent": "[循环引用]",
                "children": [
                    {
                        "type": 3,
                        "text": "X",
                        "static": true
                    }
                ],
                "plain": false,
                "hasBindings": true,
                "events": {
                    "click": {
                        "value": "REMOVE_PRODUCT(product.id)",
                        "dynamic": false
                    }
                },
                "static": false,
                "staticRoot": false
            }
        ],
        "for": "products",
        "alias": "product",
        "key": "product.id",
        "plain": false,
        "static": false,
        "staticRoot": false,
        "forProcessed": true
    }
],
"plain": true,
"static": false,
"staticRoot": false

}

I'm thinking about grabbing the raw AST by the Vue compiler and transform it slightly ( validations for whitelisted methods / variables accessed ) and then persist in the database.

Then, I can create a Vue component, which given an AST and some dynamic data ( like a product ), would render a dynamic template.

Something to the lines of

ASTCompiler.vue
export default {
  props: {
    ast: Object,
    data: Object,
  },
  methods: {
    astToRenderFunction(ast) {
      // Recursively transform the AST to a `render`
      // function
      return function(h) {
        return h(ast.tag, { attrs: ast.attrsMap }, ...)
      }
    }
  },
  beforeCreate() {
    const { ast } = this.$options.props
    this.$options.methods.compiledRenderFunction = 
      this.$options.methods.astToRenderFunction(ast)
  },
  render (h) {
    return this.compiledRenderFunction(h)
  },
}

This is pseudo-code and a lot of stuff is missing here for the sake of simplicity, like proxied access from template to the data prop and additional variable scopes, like in v-for cycles. I'm sure the actual implementation is a lot more involved than the contrived example, but it seems to be my best bet, at least theory-wise.

0 Answers
Related