【山竹记账前端-vue】4.组件封装与插槽
大纲链接 §
[toc]
组件封装与插槽:减少代码的重复
1. 快速完成四个欢迎页面 ⇧
参照 @/modules/welcome/Welcome1stPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import {defineComponent} from 'vue'
import pig from '@/assets/icons/pig.svg'
import {RouterLink} from 'vue-router'
export const Welcome1stPage = defineComponent({
name: 'Welcome1stPage',
setup(/*props, ctx*/) {
return () => (
<>
<img
src={pig}
alt="pig"
class="w-128px h-130px mt-25%"/>
<div class="description flex flex-col items-center text-[2em]">
<h2>会挣钱</h2>
<h2>还要会省钱</h2>
</div>
<div class="go-next text-primaryColor mb-84px text-[2em] font-bold">
<RouterLink to="/welcome/2">下一页</RouterLink>
</div>
</>
)
},
})
|
@/modules/welcome/Welcome2ndPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import {defineComponent} from 'vue'
import clock from '@/assets/icons/clock.svg'
import {RouterLink} from 'vue-router'
export const Welcome2ndPage = defineComponent({
name: 'Welcome2ndPage',
setup(/*props, ctx*/) {
return () => (
<>
<img
src={clock}
alt="clock"
class="w-128px h-130px mt-25%"/>
<div class="description flex flex-col items-center text-[2em]">
<h2>每日提醒</h2>
<h2>不会遗漏每一笔账单</h2>
</div>
<div class="go-next text-primaryColor mb-84px text-[2em] font-bold">
<RouterLink to="/welcome/3">下一页</RouterLink>
</div>
</>
)
},
})
|
@/modules/welcome/Welcome3rdPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import {defineComponent} from 'vue'
import chart from '@/assets/icons/chart.svg'
import {RouterLink} from 'vue-router'
export const Welcome3rdPage = defineComponent({
name: 'Welcome3rdPage',
setup(/*props, ctx*/) {
return () => (
<>
<img
src={chart}
alt="chart"
class="w-128px h-130px mt-25%"/>
<div class="description flex flex-col items-center text-[2em]">
<h2>数据可视化</h2>
<h2>收支一目了然</h2>
</div>
<div class="go-next text-primaryColor mb-84px text-[2em] font-bold">
<RouterLink to="/welcome/4">下一页</RouterLink>
</div>
</>
)
},
})
|
@/modules/welcome/Welcome4thPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import {defineComponent} from 'vue'
import cloud from '@/assets/icons/cloud.svg'
import {RouterLink} from 'vue-router'
export const Welcome4thPage = defineComponent({
name: 'Welcome4thPage',
setup(/*props, ctx*/) {
return () => (
<>
<img
src={cloud}
alt="cloud"
class="w-128px h-130px mt-25%"/>
<div class="description flex flex-col items-center text-[2em]">
<h2>云备份</h2>
<h2>再也不怕数据丢失</h2>
</div>
<div class="go-next text-primaryColor mb-84px text-[2em] font-bold">
<RouterLink to="/start">开启应用</RouterLink>
</div>
</>
)
},
})
|
2. TSX插槽语法 ⇧
注意: 在 jsx 中,应该使用 v-slots 代替 v-slot
2.1. 默认插槽与后备内容 ⇧
SlotProvider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
|
import {defineComponent} from 'vue'
export const SlotProvider = defineComponent({
name: 'SlotProvider',
setup(_props, {slots}) {
return () => (
<>
<h1>{slots.default ? slots.default() : '默认内容'}</h1>
</>
)
},
})
|
SlotConsumer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import {defineComponent} from 'vue'
import {SlotProvider} from './SlotProvider.tsx'
export const SlotConsumer = defineComponent({
name: 'SlotConsumer',
setup() {
return () => (
<>
<SlotProvider/>
<SlotProvider>自定义内容覆盖</SlotProvider>
</>
)
},
})
|
2.2. 具名插槽与后备内容 ⇧
SlotProvider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import {defineComponent} from 'vue'
export const SlotProvider = defineComponent({
name: 'SlotProvider',
setup(_props, {slots}) {
return () => (
<>
<h2>{slots.bar?.() ?? slots.default?.() ?? '保底后备内容'}</h2>
{/* 等价于 */}
{
slots.bar?.()
? slots.bar?.()
: slots.default?.()
? slots.default?.()
: <span>保底后备内容</span>
}
</>
)
},
})
|
slots.default?.() 为插槽消费组件提供的后备内容,也可能未提供
SlotConsumer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import {defineComponent} from 'vue'
import {SlotProvider} from './SlotProvider.tsx'
export const SlotConsumer = defineComponent({
name: 'SlotConsumer',
setup() {
const slots = {
bar: () => <span>Bar的内容</span>,
}
return () => (
<>
{/* 显示 保底后备内容 */}
<SlotProvider/>
{/* 显示 default默认后备内容 */}
<SlotProvider v-slots={null}>
<div>default默认后备内容</div>
</SlotProvider>
{/* 显示 具名插槽Bar的内容 */}
<SlotProvider v-slots={slots}>
<div>默认后备内容</div>
</SlotProvider>
{/* 另外的写法:显示 具名插槽Bar的内容 */}
<SlotProvider>
{{slots}}
</SlotProvider>
{/* 当 `vueJsx({ enableObjectSlots: true, })` 不是 `false` 时,可以直接使用对象插槽 */}
<SlotProvider>
{{
bar: () => <span>Bar的内容</span>,
}}
</SlotProvider>
</>
)
},
})
|
2.3. 多个具名插槽 ⇧
SlotProvider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import {defineComponent} from 'vue'
export const SlotProvider = defineComponent({
name: 'SlotProvider',
setup(_props, {slots}) {
return () => (
<>
<h1>{slots.default?.() ?? '默认default内容'}</h1>
<h2>{slots.bar?.() ?? '默认bar内容'}</h2>
<h2>{slots.foo?.() ?? '默认foo内容'}</h2>
</>
)
},
})
|
SlotConsumer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import {defineComponent} from 'vue'
import {SlotProvider} from './SlotProvider.tsx'
export const SlotConsumer = defineComponent({
name: 'SlotConsumer',
setup() {
const slots = {
default: () => <span>default</span>,
bar: () => <span>bar</span>,
foo: () => <span>foo</span>,
}
return () => (
<>
<SlotProvider v-slots={null}/>
<SlotProvider v-slots={slots}/>
{/* 当 `vueJsx({ enableObjectSlots: true, })` 不是 `false` 时,可以直接使用对象插槽 */}
<XxWelcome
v-slots={{
default: () => <span>default</span>,
bar: () => <span>bar</span>,
foo: () => <span>foo</span>,
}}
/>
</>
)
},
})
|
2.4. 动态插槽名 ⇧
SlotProvider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import {defineComponent} from 'vue'
export const SlotProvider = defineComponent({
name: 'SlotProvider',
setup(_props, {slots}) {
// 1. 获取所有传进来的插槽名称数组 (例如: ['header', 'default', 'footer'])
const slotNames = Object.keys(slots)
console.log('slots_______________________')
console.log(slots)
console.log(slotNames)
console.log('_______________________slots')
return () => (
slotNames.length === 0
? <>默认空</>
: <>
{slotNames.map((slotName) => {
console.log('正在渲染插槽:', slotName);
console.log('正在渲染插槽:', slots[slotName]?.());
return (
<div key={slotName} class={`slot-wrapper-${slotName}`}>
{/* 执行对应的插槽函数,并传入兜底内容 */}
{slots[slotName]?.() ?? '该插槽无内容'}
</div>
)
})}
</>
)
},
})
|
SlotConsumer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import {defineComponent} from 'vue'
import {SlotProvider} from './SlotProvider.tsx'
export const SlotConsumer = defineComponent({
name: 'SlotConsumer',
setup() {
const slots = {
default: () => <span>default</span>,
bar: () => <span>bar</span>,
foo: null,
duo: () => null
}
return () => (
<>
<SlotProvider v-slots={null} />
<SlotProvider v-slots={slots} />
</>
)
},
})
|
2.5. 作用域插槽传参 ⇧
SlotProvider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import {defineComponent} from 'vue'
export const SlotProvider = defineComponent({
name: 'SlotProvider',
setup(_props, {slots}) {
return () => (
<>
<h1>{slots.default?.({msg: 'default内容'})}</h1>
<h2>{slots.foo?.({title: 'foo内容'})}</h2>
</>
)
},
})
|
SlotConsumer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import {defineComponent, type SlotsType} from 'vue'
import type { JSX } from 'vue/jsx-runtime'
import {SlotProvider} from './SlotProvider.tsx'
export const SlotConsumer = defineComponent({
name: 'SlotConsumer',
// 可选 🌟 使用 SlotsType 声明组件支持的插槽及其接收的参数类型
slots: Object as SlotsType<{
default: (props: { msg: string }) => JSX.Element;
foo: (props: { title: string }) => JSX.Element;
}>,
setup() {
const slots = {
default: (props: { msg: string, }) => <span>{props.msg}</span>,
foo: (props: { title: string, }) => <span>{props.title}</span>,
}
return () => (
<>
<XxWelcome v-slots={slots} />
</>
)
},
})
|
在 TypeScript 中使用 tsconfig.json:
1
2
3
4
5
|
{
"compilerOptions": {
"jsx": "preserve"
}
}
|
参考
3. 消除重复,封装 WelcomeLayout 组件 ⇧
代码重构基本原则

分析有变化的部分
使用插槽 与 使用Props 对比
- 插槽更灵活,使用子组件的形式传任意内容
- 使用Props更简洁,但对数据一致性要求更高;需要在标签里传属性
参考
- vue 插槽官方与生态最通用的称呼
- 插槽模板组件 / 定义插槽的组件:称为 子组件 (Child Component) 或 “提供者” (Provider / Slot Provider)
- 或称为 骨架组件 (Skeleton Component)、外壳组件 (Shell Component) 或 容器组件 (Container Component)
- 只负责外层抽象的搭建布局、处理通用逻辑、提供状态,不关心具体的视觉呈现与具体数据
- 消费插槽的组件 / 使用插槽的组件:称为 父组件 (Parent Component) 或 “消费者” (Consumer / Slot Consumer)
- 或称为 内容组件 (Content Component)、视图组件 (View Component) 或 注入组件 (Injector Component)
- 决定了具体数据最终以什么样的 UI 样式呈现出来,即内部的内容由父组件提供
XxWelcome.tsx 作为组件插槽入口 ⇧
创建 @/modules/welcome/XxWelcome.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import {defineComponent} from 'vue'
export const XxWelcome = defineComponent({
name: 'XxWelcome',
setup(_props, _ctx) {
const {slots} = _ctx
console.log('slots_______________________')
console.log(slots)
console.log('_______________________slots')
return () => (
<>
{slots?.icon?.()}
{<div class="description flex flex-col items-center text-[2em]">{slots?.title?.()}</div>}
<div class="go-next text-primaryColor mb-84px text-[2em] font-bold">
{slots?.buttons?.()}
</div>
</>
)
},
})
|
- 使用具名插槽占位
- 原本图标,改为插槽占位
{slots?.icon?.()}
- 原本文字描述,改为插槽占位
{slots?.title?.()}
- 原本跳转,改为插槽占位
{slots?.buttons?.()}
- 无默认插槽
slots?.default?.()
- 直接调用具名插槽方法即可,可传实参
改造 Welcome1stPage.tsx ⇧
@/modules/welcome/Welcome1stPage.tsx
- 默认的配置即
vueJsx({ enableObjectSlots: true, }),,可省略
- 当
enableObjectSlots 不是 false 时,可以直接使用对象插槽
可解构出插槽作用域中的形参使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import {defineComponent} from 'vue'
import pig from '@/assets/icons/pig.svg'
import {RouterLink} from 'vue-router'
import {XxWelcome} from '@/modules/welcome/XxWelcome.tsx'
export const Welcome1stPage = defineComponent({
name: 'Welcome1stPage',
setup(/*props, ctx*/) {
return () => (
<XxWelcome
v-slots={{
icon: () => <img
src={pig}
alt="pig"
class="w-128px h-130px mt-25%"/>,
title: () => (
<>
<h2>会挣钱</h2>
<h2>还要会省钱</h2>
</>
),
buttons: () => <RouterLink to="/welcome/2">下一页</RouterLink>,
}}
/>
)
},
})
|
重命名 XxWelcometsx 为 WelcomeLayout.tsx
改造 Welcome2ndPage.tsx ⇧
@/modules/welcome/Welcome2ndPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import { defineComponent } from 'vue'
import clock from '@/assets/icons/clock.svg'
import { RouterLink } from 'vue-router'
import { WelcomeLayout } from '@/modules/welcome/WelcomeLayout.tsx'
export const Welcome2ndPage = defineComponent({
name: 'Welcome2ndPage',
setup(/*props, ctx*/) {
return () => (
<WelcomeLayout>
{{
icon: () => <img src={clock} alt="clock" class="w-128px h-130px mt-25%" />,
title: () => (
<>
<h2>每日提醒</h2>
<h2>不会遗漏每一笔账单</h2>
</>
),
buttons: () => <RouterLink to="/welcome/3">下一页</RouterLink>,
}}
</WelcomeLayout>
)
},
})
|
4. 还可以优化 ⇧
直接使用无状态的函数式组件 @/views/WelcomeView.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import type {FunctionalComponent} from 'vue'
import { RouterView } from 'vue-router'
import { cn } from 'cn'
import s from '@/modules/welcome/WelcomeView.module.scss'
import logo from '@/assets/icons/mangosteen.svg'
export const WelcomeView: FunctionalComponent = () => (
<div class={s.wrapper}>
<header class={s.title}>
<img src={logo} alt="logo" />
<h1>山竹记账</h1>
</header>
<main
class={cn([
'bg-welcomeCardBg mb-62px ml-16px mr-16px rounded-lg',
'flex flex-col flex-grow items-center justify-around',
])}>
<RouterView />
</main>
<section class={s['to-last-page']}>跳过</section>
</div>
)
WelcomeView.displayName = 'WelcomeView'
|
@/modules/welcome/Welcome1stPage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import type {FunctionalComponent} from 'vue'
import pig from '@/assets/icons/pig.svg'
import { RouterLink } from 'vue-router'
import { WelcomeLayout } from '@/modules/welcome/WelcomeLayout.tsx'
export const Welcome1stPage: FunctionalComponent = () => (
<WelcomeLayout
v-slots={{
icon: () => <img src={pig} alt="pig" class="w-128px h-130px mt-25%" />,
title: () => (
<>
<h2>会挣钱</h2>
<h2>还要会省钱</h2>
</>
),
buttons: () => <RouterLink to="/welcome/2">下一页</RouterLink>,
}}
/>
)
Welcome1stPage.displayName = 'Welcome1stPage'
|
添加 Welcome1stPage.displayName = 'Welcome1stPage' 防止报警告
1
2
3
|
# main.ts:12 [VUE_ROUTER_R0030] Component "default" in record with path "/welcome" is a function that does not return a Promise. This will break in production if not fixed.
# ├▶ fix: Return a dynamic import (`() => import("./MyPage.vue")`) from the function, or add a `displayName` if it is a functional component.
# ╰▶ see: https://router.vuejs.org/guide/advanced/lazy-loading.html
|
·未完待续·
参考文章
相关文章
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名