Blazor和Vue對比學習(基礎1.7):傳遞UI片斷,slot和RenderFragment

functionMC 發表於 2022-05-16
Vue

元件開發模式,帶來了複用、靈活、效能等優勢,但也增加了元件之間資料傳遞的繁雜。不像傳統的頁面開發模式,一個ViewModel搞定整個頁面資料。

元件之間的資料傳遞,是學習元件開發,必須要攻克的難關。這個章節,我們將一起學習如何將UI片斷傳遞給子元件。子元件的UI片斷,由父元件來提供,子元件接收到後直接渲染,這種場景的使用範圍還是比較多的。我們之前對自定義元件的操作,一直都是在標籤屬性的位置,從來沒有在標籤體內容的位置搞過【<Child>這個位置</Child>】。這個位置,就是為傳遞UI模板片斷準備的。Vue使用slot來接收,Blazor使用RenderFragment來接收。這兩個使用的差異還是很大,【<slot></slot>】是元件標籤,在檢視層中使用;【RenderFragment ChildContent{get;set}】是屬性,在邏輯層使用。我們通過以下幾節,來一起學習。

 

匿名傳遞一個UI片斷

具名傳遞多個UI片斷

UI片斷的資料作用域(父子作用域資料):以建立一個簡單的自定義表格元件為例

 

一、匿名傳遞一個模板片斷

//Vue=====================================

//父元件:可以傳遞任意UI片斷,包括響應式資料
<template>
  <div class="parent">
    //傳遞文字
    <Child>普通文字</Child>
    //傳遞響應式資料
    <Child>響應式資料:{{msg}}</Child>
    //傳遞任意模板片斷,可以是原生HTML標籤,也可以是自定義元件標籤
    <Child>
      <h5>任意HTML標籤和自定義元件</h5>
      <Other></Other>
    </Child> 
    //如果不傳遞,則預設顯示子元件中slot標籤體內容
    <Child></Child> 
  </div>
</template>

<script setup>
import Child from './components/Child.vue'
import Other from './components/Other.vue'
import {ref} from 'vue'
const msg = ref('響應式資料')
</script>

 

//子元件
<template>
<div class="child">
    //子元件的插槽,相當於一個佔位符
    <slot>
        <span>標籤體內容不傳入值時,預設顯示這個模板片斷</span> 
    </slot>
</div>
</template>
//Blazor
//父元件:可以傳遞任意UI片斷
<Child>普通文字</Child>
<Child>響應式資料:@msg</Child>
<Child>
    <h5>任意HTML標籤和自定義元件標籤</h5>
    <Counter></Counter>
</Child>

@code {
    private string msg = "響應式資料";
}


//子元件:使用RenderFragment型別屬性接收。注意,匿名片斷,屬性名稱必須為ChildContent,這是命名約定
<div>@ChildContent</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}


//如果父元件未傳入UI片斷,如<Child></Child>。子元件如何設定預設值?
//子元件需要使用完整屬性的方式來接收
//完整屬性的私有欄位部分,設定預設值。RenderFragment是委託型別,需要傳入一個回撥,引數型別為RenderTreeBuilder,名稱必須為__builder。先記住語法,後面會解釋
private RenderFragment childContent = (RenderTreeBuilder __builder) =>
{
    <h1>父元件如果不傳入UI,則預設顯示這句話</h1>
};

[Parameter]
public RenderFragment? ChildContent
{
    get => childContent;
    set => childContent = value;
}

 

  

 

二、具名傳遞多個片斷

//Vue=====================================

//父元件。
//使用template標籤來包裝模板片斷,並使用【v-slot:片斷名稱】來命名
//命名還可以簡寫為【#片斷名稱】
//未具名的片斷,會按先後順序統一傳入到未命名的slot中。

<template>
  <div class="parent">
    <Child>
        <h5>這是未具名的模板片斷(1)</h5>
        <template v-slot:header><h2>header片斷</h2></template>
        <template #content><h3>content片斷,#是【v-slot:】的簡寫</h3></template>
        <template #footer><h4>footer片斷</h4></template>
        <h5>這是未具名的模板片斷(2)</h5>
    </Child>
</div>
</template>

<script setup>
import Child from './components/Child.vue'
</script>

 
//子元件。使用【name="片斷名稱"】,來接收具名的片斷。
<template>
<div class="child">
    <h5>下面顯示header片斷</h5>
    <slot name="header"></slot>
    <h5>下面顯示content片斷</h5>
    <slot name="content"></slot>
    <h5>下面顯示footer片斷</h5>
    <slot name="footer"></slot>
    <h5>下面顯示未具名片斷</h5>
    <slot></slot> //這個插槽負責接受所有未具名的模板片斷
</div>
</template>
//Blazor====================================
//父元件。
//直接使用標籤方式傳遞,標籤名就是子元件的RenderFragment屬性名。
//Blazor的具名片斷,語法比Vue更簡潔
<Child>
    <Header>
        <h1>這裡是Header片斷</h1>
    </Header>
    <Body>
        <h3>這裡是Body片斷</h3>
    </Body>
    <Footer>
        <h5>這裡是Footer片斷</h5>
    </Footer>
</Child>

@code {
}


//子元件
//具備的RenderFragment可以任意命名。匿名則只能使用ChildContent
<div>@Header</div>
<div>@Body</div>
<div>@Footer</div>

@code {
    [Parameter]
    public RenderFragment? Header { get; set; }
    [Parameter]
    public RenderFragment? Body { get; set; }
    [Parameter]
    public RenderFragment? Footer { get; set; }
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}

 

 

 

三、UI片斷的資料作用域(父子作用域資料):以建立一個簡單的自定義表格元件為例

正常情況下,傳遞UI片斷時,父子元件的資料作用域是相互隔離的,但有時候我們需要父子元件之間的資料能夠串通,比如:

 

 1、子元件需要使用父元件的資料:這個比較簡單,通過屬性傳遞,我們們都學過

1Vue<Child :sources = peoples></Child>

2Blazor<Child sources = @peoples></Child>

 

2、父元件需要使用子元件的資料:這個比較麻煩,Vue還好點,Blazor會比較複雜。我們詳細說一下:

//Vue=====================================
//使用slot的屬性傳遞,記住套路就可以,還是比較簡單

//匿名插槽情況
//父元件:使用【v-slot="slotProps"】接收,其中slotProps是命名約定,不能修改
<template>
    <Child v-slot="slotProps">
    {{slotProps.msg1}}{{slotProps.msg2}}
    </Child>
</template>
<script setup>
import Child from './components/Child.vue'
</script>

 
//子元件:通過屬性msg和count傳遞多個值
<template>
<div>
    <slot :msg1=" 'hello' " :msg2=" 'world' "></slot>
</div>
</template>


//具名插槽情況
<Child>
    <template #header="headerProps">
        {{ headerProps.msg }}
    </template>
    <template #footer="footerProps">
        {{ footerProps.msg }}
    </template>
</MyComponent>
//子元件傳值
<slot name="header" msg="‘這是header傳的值’"></slot>
<slot name="header" msg="‘這是footer傳的值’"></slot>
//Blazor===================================
//需要使用到泛型RenderFragment,會比較晦澀一些。沒關係,我們先從認識RenderFragment的本質開始。
// RenderFragment的本質是一個委託, 它將 RenderTreeBuilder 作為委託入參,從而完成UI的渲染。

//我們先看一下以下四種Razor文件的等效寫法:


//第一種:這是一個簡單的Razor檔案
<div>hello world</div>


//第二種:這個檔案如果使用RenderFragment來實現,可以等價於:
<div>@HelloContent</div>
@code{
    private RenderFragment HelloContent=(RenderTreeBuilder builder)=>{
        builder.OpenElement(0,"div");
        builder.AddContent(1, "hello world");
        builder.CloseElement();
    };
}


//第三種:上面的寫法比較繁索,RenderFragment的回撥裡還可以直接寫HTML
//注意這種方式,引數名稱必須為__builder,這是約定名稱
//看到第三種寫法,能不能領會到從父元件傳遞子元件的實質?
//父元件傳遞UI片斷時,這個UI片斷就是進入到RenderFragment的回撥裡
<div>@HelloContent</div>
@code {
    private RenderFragment HelloContent = (RenderTreeBuilder __builder) =>
    {
        <div>hello world </div>
    };
}


//第四種:RenderFragment還有一種泛型方式RenderFragment<T>,可以實現傳遞資料到UI片斷
//下例中,將字串world,做為引數傳遞到渲染片斷裡。
<div>@HelloContent("world")</div>
@code {
    private RenderFragment<string> HelloContent = (msg) => (RenderTreeBuilder builder) =>
    {
        builder.OpenElement(0, "div");
        builder.AddContent(1, "hello " + msg);
        builder.CloseElement();
    };
}

 

//最後,我們通過父子元件傳遞UI片斷的方式,來實現hello world
//父元件
<Child T="string"> //指定泛型型別
    <div>
        @context //context代表為子元件的msg
    </div>
</Child>

@code {
}
 
//子元件
@typeparam T

@ChildContent((T)msg) //將msg強制轉化為T泛型

@code {
    [Parameter]
    public RenderFragment<T>? ChildContent { get; set; }
    private object msg = "hello world";
}

 

 

3、最後,我們通過一個自定義表格元件的案例,來結束UI片斷傳遞的學習。這個案例中,我們希望實現如下功能:

①自定義表格元件的名稱叫MyTable

②資料來源在使用元件時傳入

③表格的列數和列名,也在使用元件時再確定

//Vue=====================================
//定義兩個具名插槽THead和TBody
//以屬性方式傳入資料來源peoples
//TBody插槽,將行資料傳回到父元件使用

//父元件
<template>
    <MyTable :items = "peoples">
        <template #THead>
            <th>ID</th>
            <th>姓名</th>
        </template>
        <template #TBody="TBodyProps">
            <td>{{TBodyProps.item.id}}</td>
            <td>{{TBodyProps.item.name}}</td>
        </template>
    </MyTable>
</template>

<script setup>
import MyTable from './components/MyTable.vue'
import {ref} from 'vue'
const peoples = ref([
    {id:1,name:"functionMC"},
    {id:2,name:"GongFU"},
    {id:3,name:"TaiJi"}
])
</script>


//子元件
<template>
    <table>
        <thead>
            <slot name="THead"></slot>
        </thead>
        <tbody>
            <tr v-for="item in props.items">
            <slot name="TBody" :TBodyProps="item"></slot>
            </tr>
        </tbody>
    </table>
</template>

<script setup>
import {ref} from 'vue'
const props = defineProps(['items'])
</script>

 

//Blazor====================================

//父元件
//使用THead和Tbody兩個具名UI片斷
//【T="People"】指定子元件的泛型
//context為子元件【@TBody(item)】中的item

<MyTable T="People" TItems="@peoples">
    <THead>
        <th>ID</th>
        <th>姓名</th>
    </THead>
    <TBody>
        <td>@context.Id</td>
        <td>@context.Name</td>
    </TBody>
</MyTable>

@code {
    private List<People> peoples = new List<People>
    {
        new People{Id=1,Name="functionMC"},
        new People{Id=2,Name="Shine"},
        new People{Id=3,Name="Billing"}
    };

    private class People
    {
        public int Id { get; set; }
        public string? Name { get; set; }
    }
}


//子元件
//定義了一個泛型T,資料來源及其型別,以及行的型別,都應該由父元件傳入
//通過【@TBody(item)】,將子元件的item傳回去
//其實子元件的item來源於父元件的TItems,資料的流轉有兩個過程:
//①父元件將所有人peoples傳遞給子元件
//②子元件又將一個人people傳遞給父元件
@typeparam T

<table>
    <thead>
        @THead
    </thead>
    <tbody>
        @foreach (var item in TItems)
        {
            if (TItems is not null)
            {
                <tr>@TBody(item)</tr>
            }
        }
    </tbody>
</table>

@code {
    [Parameter]
    public List<T>? TItems { get; set; }
    [Parameter]
    public RenderFragment? THead { get; set; }
    [Parameter]
    public RenderFragment<T>? TBody { get; set; }
}