如何动态渲染 .vue 文件

写在开头

你可能用过 jsfiddle 或 jsbin 之类的网站,在里面你可以用 CDN 的形式引入 Vue.js,然后在线写示例。不过,这类网站主要是一个 html,里面包含 js、css 部分,渲染则是用 iframe 嵌入你编写的 html,并实时更新。在这些网站写示例,是不能直接写 .vue 文件的,因为没法进行编译。

来看另一个网站 iView Run ,它是能够在线编写一个标准的 .vue 文件,并及时渲染的,它也预置了 iView 环境,你可以使用 iView 组件库全部的组件。现在,我们就来实现这样一个能够动态渲染 .vue 文件的 Display 组件,我们会用到 extend 和 $mount

接口设计

一个常规的 .vue 文件一般都会包含 3 个部分:

  • <template>:组件的模板;
  • <script>:组件的选项,不包含 el
  • <style>:CSS 样式。

回忆一下用 extend 来构造一个组件实例,它的选项 template 其实就是上面 <template> 的内容,其余选项对应的是 <script>,样式 <style>事实上与 Vue.js 无关,我们可以先不管。这样的话,当拿到一个 .vue 的文件(整体其实是字符串),只需要把 <template><script><style> 使用正则分割,把对应的部分传递给 extend 创建的实例就可以。Display 是一个功能型的组件,没有交互和事件,只需要一个 prop:code 将 .vue 的内容传递过来

实现

在 src/components 目录下创建 display 目录,并新建 display.vue 文件,基本结构如下:

<template>
  <div ref="display"></div>
</template>
<script>
export default {
    props: {
      code: {
        type: String,
        default: ''
      }
    },
    data () {
      return {
        html: '',
        js: '',
        css: ''
      }
    },
  }
</script>

父级传递 code 后,将其分割,并保存在 data 的 html、js、css 中,后续使用。

我们使用正则,基于 <> 和 </> 的特性进行分割:

// display.vue,部分代码省略
export default {
  methods: {
    getSource (source, type) {
      const regex = new RegExp(`<${type}[^>]*>`);
      let openingTag = source.match(regex);

      if (!openingTag) return'';
      else openingTag = openingTag[0];

      return source.slice(source.indexOf(openingTag) + openingTag.length, source.lastIndexOf(`</${type}>`));
    },
    splitCode () {
      const script = this.getSource(this.code, 'script').replace(/export default/, 'return ');
      const style = this.getSource(this.code, 'style');
      const template = '<div id="app">' + this.getSource(this.code, 'template') + '</div>';

      this.js = script;
      this.css = style;
      this.html = template;
    },
  }
}

getSource 方法接收两个参数:

  • source:.vue 文件代码,即 props: code;
  • type:分割的部分,也就是 template、script、style。

分割后,返回的内容不再包含 <template> 等标签,直接是对应的内容,在 splitCode 方法中,把分割好的代码分别赋值给 data 中声明的 html、js、css。有两个细节需要注意:

  1. .vue 的 <script> 部分一般都是以 export default 开始的,可以看到在 splitCode 方法中将它替换为了 return,这个在后文会做解释,当前只要注意,我们分割完的代码,仍然是字符串;
  2. 在分割的 <template> 外层套了一个 <div id="app">,这是为了容错,有时使用者传递的 code 可能会忘记在外层包一个节点,没有根节点的组件,是会报错的。

准备好这些基础工作后,就可以用 extend 渲染组件了,在这之前,我们先思考一个问题:上文说到,当前的 this.js 是字符串,而 extend 接收的选项可不是字符串,而是一个对象类型,那就要先把 this.js 转为一个对象。我们可以使用new Function 或者eval函数来实现这个功能。

<template>
  <div ref="display"></div>
</template>
<script>

//注意,这里需要引入vue,独立构建(进行编译)
import Vue from 'vue/dist/vue.js';
  
  exportdefault {
    data () {
      return {
        component: null
      }
    },
    methods: {
      renderCode () {
        this.splitCode();

        if (this.html !== '' && this.js !== '') {
          const parseStrToFunc = new Function(this.js)();

          parseStrToFunc.template =  this.html;
          const Component = Vue.extend( parseStrToFunc );
          this.component = new Component().$mount();

          this.$refs.display.appendChild(this.component.$el);
        }
      }
    },
    mounted () {
      this.renderCode();
    }
  }
</script>

extend 构造的实例通过 $mount 渲染后,挂载到了组件唯一的一个节点 <div ref="display"> 上。

现在 html 和 js 都有了,还剩下 css。加载 css 没有什么奇技淫巧,就是创建一个 <style> 标签,然后把 css 写进去,再插入到页面的 <head>中,这样 css 就被浏览器解析了。为了便于后面在 this.code 变化或组件销毁时移除动态创建的 <style> 标签,我们给每个 style 标签加一个随机 id 用于标识。

在 src/utils 目录下新建 random_str.js 文件,并写入以下内容:

//此函数的作用是从指定的 a-zA-Z0-9 中随机生成 32 位的字符串。
export default function(len=32){
    const $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
    const maxPos = $chars.length;
    let str = '';
    for (let i = 0; i < len; i++) {
        str += $chars.charAt(Math.floor(Math.random() * maxPos));
    }
    return str;
}

补全 renderCode 方法:

import randomStr from'../../utils/random_str.js';

export default {
  data () {
    return {
      id: randomStr()
    }
  },
  methods: {
    renderCode () {
      if (this.html !== '' && this.js !== '') {
        // ...if (this.css !== '') {
          const style = document.createElement('style');
          style.type = 'text/css';
          style.id = this.id;
          style.innerHTML = this.css;
          document.getElementsByTagName('head')[0].appendChild(style);
        }
      }
    }
  }
}

当 Display 组件销毁时,也要手动销毁 extend 创建的实例以及上面的 css:

//display.vue,部分代码省略
export default {
  methods: {
    destroyCode () {
      const $target = document.getElementById(this.id);
      if ($target) $target.parentNode.removeChild($target);

      if (this.component) {
        this.$refs.display.removeChild(this.component.$el);
        this.component.$destroy();
        this.component = null;
      }
    }
  },
  beforeDestroy () {
    this.destroyCode();
  }
}

当 this.code 更新时,整个过程要重新来一次,所以要对 code 进行 watch 监听:

export default {
  watch: {
    code () {
      this.destroyCode();
      this.renderCode();
    }
  }
}

以上就是 Display 组件的所有内容。我们来看一下完整的组件

<template>
    <div ref="display"></div>
</template>
<script>
import randomStr from '../../utils/random_str.js';
import Vue from 'vue/dist/vue.js';
export default {
    props:{
        code:{
            type:String,
            default:""
        }
    },
    data(){
        return {
            html:"",
            js:"",
            css:"",
            id:randomStr()
        }
    },
    mounted(){
        this.renderCode();
    },
    methods:{
        // 分割
        getSource(source,type){
            const regex=new RegExp(`<${type}[^>]*>`);
            // 标签匹配
            let openingTag=source.match(regex);

            if(!openingTag){
                return '';
            }else{
                openingTag = openingTag[0];
            }
            //返回的结果剔除了script等标签
            return source.slice(source.indexOf(openingTag) + openingTag.length, source.lastIndexOf(`</${type}>`));
        },
        //具体化分割
        splitCode(){
            const script = this.getSource(this.code, 'script').replace(/export default/, 'return ');
            const style = this.getSource(this.code, 'style');
            const template = '<div id="app">' + this.getSource(this.code, 'template') + '</div>';

            this.js = script;
            this.css = style;
            this.html = template;
        },
        // 渲染,挂载
        renderCode(){
            this.splitCode();
            if(this.html!==''&&this.js!==''){
                const parseStrToFunc=new Function(this.js)();

                parseStrToFunc.template =  this.html;
                const Component = Vue.extend( parseStrToFunc );
                this.component = new Component().$mount();
                this.$refs.display.appendChild(this.component.$el);

                if(this.css!==''){
                    const style = document.createElement('style');
                    style.type = 'text/css';
                    style.id = this.id;
                    style.innerHTML = this.css;
                    document.getElementsByTagName('head')[0].appendChild(style);
                }
            }
        },
        // 当display组件销毁时,也要手动销毁extend创建的实例以及上面的css
        destroyCode(){
            // 销毁style
            const $target = document.getElementById(this.id);
            if($target) {
                $target.parentNode.removeChild($target);
            }
            if(this.component){
                this.$refs.display.removeChild(this.component.$el);
                this.component.$destroy();
                 this.component = null;
            }

        }
    },
    // 当this.code更新时,整个过程需要重来一次,所以要对code进行监听
    watch:{
        code () {
            this.destroyCode();
            this.renderCode();
        }
    },
    beforeDestroy(){
        this.destroyCode();
    }
}
</script>
<style>
    
</style>

使用

新建一条路由,并在 src/views 下新建页面 display.vue 来使用 Display 组件,这里我们把要渲染的code提取到default-code.js中

const code =
`<template>
    <div>
        <input v-model="message">
        {{ message }}
    </div>
</template>
<script>
    export default {
        data () {
            return {
                message: ''
            }
        }
    }
</script>`;

export default code;
<template>
    <div>
        <h3>动态渲染 .vue 文件的组件—— Display</h3>
        <dis :code="code"></dis>
    </div>
</template>
<script>
import dis from '../components/display/display.vue';
import defaultCode from '../components/display/default-code.js'
export default {
    data(){
        return {
            code:defaultCode
        }
    },

    components:{
        dis
    }
}
</script>

好了,到此结束。本文由作者收集整理。

 

 

 

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值