本文是不才在學習Vue和Bootstrap過程當中遇到問題解決的一些思路,主要描述了項目搭建,組件封裝、獲取、編輯、更新的一步步實現,一些解決方案也沒找到正確的官方API,還請大拿們多多提點。
旨在經過項目的形式同時學習Vue和Bootstrap,實現一個在線配置頁面的功能。經過Bootstrap封裝好的組件樣式提供界面須要的組件,經過Vue實現組件狀態更改及頁面渲染。javascript
https://github.com/shixia226/bootstrap-vue-designercss
組件模塊區
提供可用於拖拽到編輯區的全部組件,分類別展現html
該功能與本學習目的關聯不強,且其主要拖拽功能比較花時間,暫且擱置
頁面編輯區
提供全部已添加到頁面的組件的編輯預覽,並提供組件增,刪,排版,選中功能vue
增,刪,排版功能能夠與模板區的拖拽功能結合,一樣暫時擱置
基本的項目搭建,建立index.html
, index.js
配置好webpack
java
<!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"] } }] } };
Bootstrap樣式引入node
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
Vue框架引入webpack
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
運行git
//node webpack-dev-server --port=9926 //Browser http://localhost:9926/
<span class="badge badge-light badge-pill">9</span>
widget-badge
.Vue.component('widget-badge', { template: `<span :class="['badge', theme ? 'badge-' + theme : '', pill ? 'badge-pill' : '']">{{text}}</span>`, props: ['theme', 'pill', 'text'] });
<div class="app"> <widget-badge></widget-badge> </div>
new Vue({ el: '.app' })
以上步驟後刷新瀏覽器應該是能夠看到組件效果了,但該組件的全部屬性都是在標籤內寫死的,沒法在編輯頁面動態設置github
每一個可變屬性加一個編輯項,對應屬性名name="vpropA"
, 取值爲當前屬性值:value="vpropsA"
,全部的編輯項所有定義屬性 editor 上。web
沒找到對應獲取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原本就是經過狀態更新的方式更改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> `, ... }
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; } } } }