如何动态渲染 .vue 文件
写在开头你可能用过jsfiddle或jsbin之类的网站,在里面你可以用 CDN 的形式引入 Vue.js,然后在线写示例。不过,这类网站主要是一个 html,里面包含 js、css 部分,渲染则是用 iframe 嵌入你编写的 html,并实时更新。在这些网站写示例,是不能直接写.vue文件的,因为没法进行编译。来看另一个网站iView Run,它是能够在线编写一个标准的...
写在开头
你可能用过 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。有两个细节需要注意:
- .vue 的
<script>
部分一般都是以export default
开始的,可以看到在 splitCode 方法中将它替换为了return
,这个在后文会做解释,当前只要注意,我们分割完的代码,仍然是字符串; - 在分割的
<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>
好了,到此结束。本文由作者收集整理。
更多推荐
所有评论(0)