本文是不才在學習Vue和Bootstrap過程中遇到問題解決的一些思路,主要描述了專案搭建,元件封裝、獲取、編輯、更新的一步步實現,一些解決方案也沒找到正確的官方API,還請大拿們多多提點。
專案介紹
旨在通過專案的形式同時學習Vue和Bootstrap,實現一個線上配置頁面的功能。通過Bootstrap封裝好的元件樣式提供介面需要的元件,通過Vue實現元件狀態更改及頁面渲染。
專案地址
https://github.com/shixia226/bootstrap-vue-designer
專案設計
-
元件模組區
提供可用於拖拽到編輯區的所有元件,分類別展示該功能與本學習目的關聯不強,且其主要拖拽功能比較花時間,暫且擱置
-
頁面編輯區
提供所有已新增到頁面的元件的編輯預覽,並提供元件增,刪,排版,選中功能增,刪,排版功能可以與模板區的拖拽功能結合,同樣暫時擱置
- 元件配置區
提供具體元件內部狀態檢視及更改功能
專案搭建
-
基本的專案搭建,建立
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"] } }] } };
-
Bootstrap樣式引入
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
-
Vue框架引入
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
-
執行
//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;
}
}
}
}