1
本文是不才在学习Vue和Bootstrap过程中遇到问题解决的一些思路,主要描述了项目搭建,组件封装、获取、编辑、更新的一步步实现,一些解决方案也没找到正确的官方API,还请大拿们多多提点。

项目介绍

旨在通过项目的形式同时学习Vue和Bootstrap,实现一个在线配置页面的功能。通过Bootstrap封装好的组件样式提供界面需要的组件,通过Vue实现组件状态更改及页面渲染。

项目地址

https://github.com/shixia226/bootstrap-vue-designer

项目设计

  • 组件模块区
    提供可用于拖拽到编辑区的所有组件,分类别展示

    该功能与本学习目的关联不强,且其主要拖拽功能比较花时间,暂且搁置
  • 页面编辑区
    提供所有已添加到页面的组件的编辑预览,并提供组件增,删,排版,选中功能

    增,删,排版功能可以与模板区的拖拽功能结合,同样暂时搁置
  • 组件配置区
    提供具体组件内部状态查看及更改功能

项目搭建

  1. 基本的项目搭建,创建index.html, index.js配置好webpack

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>Vue Demo</title>
    </head>
    <body>
        <script src="../index.js"></script>
    </body>
    </html>
    module.exports = {
        entry: './index.js',
        output: {
            filename: 'index.js'
        },
        module: {
            rules: [{
                test: /^[^.]+\.scss$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            }, {
                test: /(\.js|\.vue)$/,
                exclude: /(node_modules|bower_components)(?!.*webpack-dev-server)/,
                loader: 'babel-loader',
                query: {
                    "presets": ["env"]
                }
            }]
        }
    };
  2. Bootstrap样式引入

    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  3. Vue框架引入

    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  4. 运行

    //node
    webpack-dev-server --port=9926
    //Browser
    http://localhost:9926/

第一个组件Badage

Bootstrap官网例子:

<span class="badge badge-light badge-pill">9</span>

组件分析

  • badge-light 样式可以替换成badge-primary等,可以设置成属性变量用于选择哪个颜色;
  • badge-pill 样式有和无表现是不一样的,可以设置属性变量用于控制要不该样式;
  • 9 文本内容作为最终的展示内容,可以设置成属性变量;
  • 组件名取 widget-badge.

Vue组件封装

Vue.component('widget-badge', {
    template: `<span :class="['badge', theme ? 'badge-' + theme : '', pill ? 'badge-pill' : '']">{{text}}</span>`,
    props: ['theme', 'pill', 'text']
});

组件展示

html

<div class="app">
    <widget-badge></widget-badge>
</div>

js

new Vue({
    el: '.app'
})

组件配置

以上步骤后刷新浏览器应该是可以看到组件效果了,但该组件的所有属性都是在标签内写死的,无法在编辑页面动态设置

动态属性

  • vue 中 props 属性是不允许动态更改的,一般都只能更改 data 中的属性值,所以需要把 props 中的所有可变属性拷贝一份到 data 中,且命名上不能相同,所以在此先规定 data 中的所有属性都以字母'v'开头;
  • 每个可变属性加一个编辑项,对应属性名name="vpropA", 取值为当前属性值:value="vpropsA",所有的编辑项全部定义属性 editor 上。

    没找到对应获取editor属性值的API,但通过分析vue对象发现可以通过vue实例vm.$options.editor获取到该定义值,暂且先就这么用着。

组件封装更改如下:

Vue.component('widget-badge', {
    template: `<span :class="['badge', 'badge-' + vtheme, vpill ? 'badge-pill' : '']">{{vtext}}</span>`,
    props: ['theme', 'pill', 'text'],
    editor: `
        <input name="vtheme" :value="vtheme" />
        <input name="vpill" :value="vpill" />
        <input name="vtext" :value="vtext" />
    `,
    data() {
        return {
            vtheme: this.theme || 'secondary',
            vpill: this.pill,
            vtext: this.text || 'Badge'
        }
    }
});

属性配置面板

  • 点击不同的组件要展示对应的(不同的)配置面板

根据点击元素获取所属vue组件

vue本来就是通过状态更新的方式更改dom的,所以很少有dom相关的api,又只得分析vue实例里的数据,发现$children好像就是直接下级组件的一个集合,且$children每一项里都又一个$el的属性对应到实际DOM元素
function getVueCmp(vm, elem) {
    let pelems = [],
        $root = vm.$el;
    while (elem !== $root) {
        pelems.push(elem);
        elem = elem.parentNode;
    }
    return getVueCmpByPelem(vm, pelems);
}
function getVueCmpByPelem(vm, pelems) {
    let $children = vm.$children;
    if ($children) {
        for (let i = 0, len = $children.length; i < len; i++) {
            let vcmp = $children[i],
                $el = vcmp.$el,
                idx = pelems.indexOf($el);
            if (idx !== -1) {
                pelems.length = idx;
                return getVueCmpByPelem(vcmp, pelems);
            }
        }
    }
    return vm;
}

增加点击事件

<div class="app" @click="showPpt">
    <widget-badge></widget-badge>
</div>

获取组件实时数据

根据前面的数据命名规则直接遍历$data中所有以字母'v'开头的属性
function getVueCmpData(vcmp) {
    if (!vcmp) return {};
    let $data = vcmp.$data,
        data = {};
    let names = Object.getOwnPropertyNames($data);
    for (let i = 0, len = names.length; i < len; i++) {
        let name = names[i];
        if (name.charAt(0) === 'v') {
            data[name.substr(1)] = $data[name];
        }
    }
    return data;
}

数据更新

在vue根节点上设置全局监听事件,然后在属性值中定义$emit方法触发该监听事件
  • 根节点设置监听事件,并将监听结果反馈到当前选中的组件上
created() {
    this.$on('changeppt', function(name, value) {
        if (vcmp) {
            let names = name.split('.'),
                data = vcmp,
                len = names.length - 1;
            for (let i = 0; i < len; i++) {
                data = data[names[i]];
            }
            data[names[len]] = value;
        }
    })
}
  • 封装编辑器的输入框为组件如下:
Vue.component('editor-text', {
    template: `<input v-model="vvalue" @change="$root.$emit('changeppt', name, vvalue)">`,
    props: ['name', 'value'],
    data() {
        return {
            vvalue: this.value
        }
    }      
})
  • 更改编辑器配置如下
{
    ...
    /*
    editor: `
        <input name="vtheme" :value="vtheme" />
        <input name="vpill" :value="vpill" />
        <input name="vtext" :value="vtext" />
    `,
    */
    editor: `
        <editor-text name="vtheme" :value="theme" ></editor-text>
        <input name="vpill" :value="pill" ></editor-text>
        <input name="vtext" :value="text" ></editor-text>
    `,
    ...
}

vue最终初始化更改如下

new Vue({
    el: '.app',
    data: {
        pptCmp: undefined
    },
    watch: {
        pptCmp(vcmp) {
            new Vue({
                el: '.ppt',
                template: '<div class="ppt">' + (vcmp ? vcmp.$options.editor || '' : '') + '</div>',
                data() {
                    return getVueCmpData(vcmp, true);
                },
                created() {
                    this.$on('changeppt', function(name, value) {
                        if (vcmp) {
                            let names = name.split('.'),
                                data = vcmp,
                                len = names.length - 1;
                            for (let i = 0; i < len; i++) {
                                data = data[names[i]];
                            }
                            data[names[len]] = value;
                        }
                    })
                }
            })
        }
    },
    methods: {
        showPpt: function(evt) {
            let elem = evt.target;
            if (!document.querySelector('.ppt').contains(elem)) {
                let vcmp = getVueCmp(this, elem);
                if (vcmp === this.$root) {
                    vcmp = null;
                }
                this.pptCmp = vcmp;
            }
        }
    }
}

石侠
27 声望0 粉丝

尽心做到极致