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

石俠發表於2018-06-21

本文主要描述不才在學習Vue和Bootstrap中遇到的關於插槽的問題及解決方案

插槽理解

vue官網和很多熱心朋友又很多關於預設插槽,具名插槽,插槽作用域相關的解釋,我就不重複搬過來佔用篇幅了,這裡簡單的談談我個人的淺見。

  • 插槽就是預留位置
    插槽其實就是定義元件時預留的一些位置,將使用元件時元件內額外的一些標籤寫入到對應的預留位置就是插槽的作用了。
  • 插槽適用於不固定內容的元件
    正是因為無法預見元件內部所有可能的標籤或內容,所以乾脆留一個空缺,全權交給使用者,想填啥就用啥。
  • 具名插槽適用於具有一定結構且有多處不固定內容的元件
    此時在定義元件時其實就是預留了多個空缺位置且分別命名(如果只有一個當然也可以採用具名插槽,但肯定不如預設插槽,只會徒增配置成本)。然後在使用時將內容分成多塊分別命名成預留空缺位置的名字。

元件理解

為什麼要定義元件?
答:定義元件是一種封裝的形式,使用最簡單的標籤及屬性配置表達一大段比較豐富的結構效果及一些資料和事件。

Bootstrap元件-Collapse(Accordion example)

官網HTML程式碼

<div id="accordion">
  <div class="card">
    <div class="card-header" id="headingOne">
      <h5 class="mb-0">
        <button class="btn btn-link" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
          Collapsible Group Item #1
        </button>
      </h5>
    </div>

    <div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion">
      <div class="card-body">
        Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven`t heard of them accusamus labore sustainable VHS.
      </div>
    </div>
  </div>
  <div class="card">
    <div class="card-header" id="headingTwo">
      <h5 class="mb-0">
        <button class="btn btn-link collapsed" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
          Collapsible Group Item #2
        </button>
      </h5>
    </div>
    <div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordion">
      <div class="card-body">
        Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven`t heard of them accusamus labore sustainable VHS.
      </div>
    </div>
  </div>
  <div class="card">
    <div class="card-header" id="headingThree">
      <h5 class="mb-0">
        <button class="btn btn-link collapsed" data-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
          Collapsible Group Item #3
        </button>
      </h5>
    </div>
    <div id="collapseThree" class="collapse" aria-labelledby="headingThree" data-parent="#accordion">
      <div class="card-body">
        Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven`t heard of them accusamus labore sustainable VHS.
      </div>
    </div>
  </div>
</div>
  • 該示例程式碼比較長
  • 結構是很有規律的,很容易發現幾個共同的結構:card, card-header, card-body…
  • 文案是具體的展示資訊,也是我們希望元件能夠配置的

最精簡的元件配置

<widget-collapse>
    <div header="Collapsible Group Item #1">
        Anim pariatur ...
    </div>
    <div header="Collapsible Group Item #2">
        Anim pariatur ...
    </div>
    <div header="Collapsible Group Item #3">
        Anim pariatur ...
    </div>
</widget-collapse>
  • header一般文字就夠用了,所以直接配置在標籤屬性上
  • content, 如`Anim pariatur …`我們預想其可能是其他的一個或多個元件甚至大段html(會有標籤),此時放到屬性內就不合適了,配置起來超級麻煩

元件封裝

  • 獲取header簡單。遍歷$children從其attrs物件中可以讀取到,放到data中的一個陣列裡,採用v-for指令即可很容易渲染出來
  • 獲取content不容易。$children都是虛擬DOM,反向變成html的API沒找到;再者就算找到了,同header一樣遍歷渲染出來,也只是當成字串渲染出來,加上v-html指令,標準的HTML標籤倒是確實渲染出來了,可已定義的元件標籤未能正確解析渲染(不知道vue有沒有相應的API方法解決)。
  • 插槽<slot></slot>剛好可以解決內容中的標籤解析問題,但它會將所有的內容(上例中3個div)都解析到一起無法拆分
  • 具名插槽<slot name="..."></slot>可以解決內容無法拆分的問題。那我們就把內容中的每一個都標一個name, name取值要滿足內容個數不定的條件,所以最好採用和序號有關的,剛好可以在v-for指令中匹配到。
  • 問題解決!

元件定義

Vue.component(`widget-collapse`, {
    template: `<div>
        <div class="card" v-for="(item, index) in vitems">
            <div class="card-header">
                <h5 class="mb-0">
                    <button class="btn btn-link" data-toggle="collapse">{{item.header}}</button>
                </h5>
            </div>

            <div :class="[`collapse`]">
                <div class="card-body">
                    <slot :name="index"></slot>
                </div>
            </div>
        </div>
    </div>`,
    data() {
        let children = this.$slots, items = [];
        for (let i in children) {
            let node = children[i][0];
            if (node.tag) {
                items[i] = ({
                    header: VTool.attr(node, `header`) || (`Item ` + items.length),
                })
            }
        }
        return {
            vitems: items
        }
    }
})

元件使用

<widget-collapse>
    <div header="Collapsible Group Item #1" slot="0">
        Anim pariatur ...
    </div>
    <div header="Collapsible Group Item #2" slot="1">
        Anim pariatur ...
    </div>
    <div header="Collapsible Group Item #3" slot="2">
        Anim pariatur ...
    </div>
</widget-collapse>

元件優化

使用時每個節點都寫一個slot太累贅,容易寫錯,刪除某個節點或調整節點順序後更改麻煩,能否程式動態幫忙新增該slot屬性?
答:未找到對應API,但對比預設與具名插槽的$slots資料物件發現key分別為default和具名name

動態增加$slots屬性值

Vue.component(`widget-collapse`, {
    template: `<div>
            <div class="card" v-for="(item, index) in vitems">
                <div class="card-header">
                    <h5 class="mb-0">
                        <button class="btn btn-link" data-toggle="collapse">{{item.header}}</button>
                    </h5>
                </div>

                <div :class="[`collapse`]">
                    <div class="card-body">
                        <slot :name="item.id"></slot>
                    </div>
                </div>
            </div>
        </div>`,
    data() {
        let children = this.$slots.default, items = [];
        for (let i = 0, len = children.length; i < len; i++) {
            let node = children[i];
            if (node.tag) {
                let id = VTool.random();
                this.$slots[id] = node;
                items.push({
                    id: id,
                    header: VTool.attr(node, `header`) || (`Item ` + items.length),
                })
            }
        }
        return {
            vitems: items
        }
    }
})

刪除使用時的插槽命名

<widget-collapse>
    <div header="Collapsible Group Item #1">
        Anim pariatur ...
    </div>
    <div header="Collapsible Group Item #2">
        Anim pariatur ...
    </div>
    <div header="Collapsible Group Item #3">
        Anim pariatur ...
    </div>
</widget-collapse>

同步更新

  • 以上設定基本初始化是沒有任何問題了,但在vue物件更新時會更改$slots物件
    (經跟蹤程式碼查詢到updateChildComponent方法內在更新下層元件時執行了vm.$slots = resolveSlots(renderChildren, parentVnode.context);覆蓋了之前縮處理過的$slots,且會在vm._render方法中重新渲染,且在此之前沒有公佈任何事件,那就暴力覆蓋重寫了)
Vue.component(`widget-collapse`, {
    ...
    created() {
        let _render = this._render;
        this._render = function() {
            let $slots = this.$slots;
            for (let name in $slots) {
                if (name !== `default`) {
                    return _render.apply(this, arguments);
                }
            }
            // 此時肯定是重新new的$slots, 重寫對應關係
            let children = this.$slots.default, items = [];
            for (let i = 0, len = children.length; i < len; i++) {
                let node = children[i];
                if (node.tag) {
                    let id = VTool.random();
                    this.$slots[id] = node;
                    items.push({
                        id: id,
                        header: VTool.attr(node, `header`) || (`Item ` + items.length),
                    })
                }
            }
            this.vitems = items;
            return _render.apply(this, arguments);
        }
    },
    ...
})

本文對於vue插槽的處理辦法略顯暴力,但的確解決了我遇到的問題,各位大拿有更好的經驗還請不吝賜教,拜謝!

相關文章