Blazor和Vue对比学习(基础-7):传递UI片断,slot和RenderFragment

组件开发模式,带来了复用、灵活、性能等优势,但也增加了组件之间数据传递的繁杂。不像传统的页面开发模式,一个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; }
}

 

posted @ 2022-05-16 21:34  functionMC  阅读(926)  评论(1编辑  收藏  举报