Vue & Bootstrap 結合學習筆記(一)

石俠發表於2018-06-20

本文是不才在學習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;
            }
        }
    }
}

相關文章