元件開發模式,帶來了複用、靈活、效能等優勢,但也增加了元件之間資料傳遞的繁雜。不像傳統的頁面開發模式,一個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、子元件需要使用父元件的資料:這個比較簡單,通過屬性傳遞,我們們都學過
(1)Vue:<Child :sources = “peoples”></Child>
(2)Blazor:<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; } }