「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
🖼️序言
最近在用 vue3
和 ts
捣鼓一些小工具,发现平常开发中一个很常见的需求就是,数据列表的渲染。现在重新学习,发现我在学 vue2
时的很多设计规范和逻辑都考虑的不是特别妥当。
因此,写下这篇文章,记录组件设计中数据列表渲染和全局头部的设计。
一起来学习吧~🙆
📻一、ColumnList列表数据渲染
1、设计稿抢先知
在了解功能实现之前,我们先来看看原型图,看我们想要实现的数据列表是怎么样的。如下图所示:
大家可以先了解一下,我们待会所要实现内容的效果图。
2、数据构思
了解完具体的效果图之后呢,现在我们要开始来干活啦!
首先我们需要先构思这个组件所需要的数据有哪一些呢?
这个组件所需要的数据,首先是每一行数据它自己唯一的 id
,其次就是标题 title
,还有一个是头像 avatar
,最后一个是每个标题对应的文字描述 description
。
分析完成之后,我们现在在 vue3
项目下的 src|components
文件夹下新建一个文件,命名为 ColumnList.vue
。之后再编写这段业务代码。具体代码如下:
<template>
<div></div>
</template>
<script lang="ts"> import { computed, defineComponent, PropType } from 'vue' //用ts写一个接口,存放列表数据的属性 export interface ColumnProps { id: number; title: string; avatar?: string; description: string; } export default defineComponent({ name: 'ColumnList', props: { //将接口的内容赋值给list数组,方便接收父组件传来的数据 list: { type: Array as PropType<ColumnProps[]>, required: true } } }) </script>
<style lang="scss" scoped>
</style>
3、视图数据绑定
现在,对数据构思完毕之后,我们是还没有取到任何数据可以渲染的,相当于是一个空的 ColumnList
。但是我们已经有了接口的属性内容,所以我们先来把数据绑定到视图当中。具体代码如下:
<template>
<div class="row">
<div v-for="column in columnList" :key="column.id" class="col-4 mb-3">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<img :src="column.avatar" :alt="column.title" class="rounded-circle border border-light w-25 my-3">
<h5 class="title">{{column.title}}</h5>
<p class="card-text text-left">{{column.description}}</p>
<a href="#" class="btn btn-outline-primary">进入专栏</a>
</div>
</div>
</div>
</div>
</template>
<script lang="ts"> import { computed, defineComponent, PropType } from 'vue' export interface ColumnProps { id: number; title: string; avatar?: string; description: string; } export default defineComponent({ name: 'ColumnList', props: { list: { type: Array as PropType<ColumnProps[]>, required: true } } }) </script>
<style lang="scss" scoped>
</style>
注: 这里我用到的是 bootstrap
的样式库,所以 css
方面不做过多的编写,大家有需要可以到官方中文文档进行查看,也可以自己进行样式设计。
到此,我们就完成了第一轮的数据绑定。接下来我们在父组件中,进行数据传递。
4、数据传递
我们在vue3项目中的 src
文件夹下的 App.vue
中来进行数据传递。具体代码如下:
<template>
<div class="container">
<column-list :list="list"></column-list>
</div>
</template>
<script lang="ts"> import { defineComponent } from 'vue' //在根文件下引入bootstrap import 'bootstrap/dist/css/bootstrap.min.css' //引入子组件 import ColumnList, { ColumnProps } from './components/ColumnList.vue' //制造子组件的接口数据 const testData: ColumnProps[] = [ { id: 1, title: 'test1专栏', description: '众所周知, js 是一门弱类型语言,并且规范较少。这就很容易导致在项目上线之前我们很难发现到它的错误,等到项目一上线,浑然不觉地,bug就UpUp了。于是,在过去的这两年,ts悄悄的崛起了。 本专栏将介绍关于ts的一些学习记录。' avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg' }, { id: 2, title: 'test2专栏', description: '众所周知, js 是一门弱类型语言,并且规范较少。这就很容易导致在项目上线之前我们很难发现到它的错误,等到项目一上线,浑然不觉地,bug就UpUp了。于是,在过去的这两年,ts悄悄的崛起了。 本专栏将介绍关于ts的一些学习记录。', avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg' } ] export default defineComponent({ name: 'App', components: { ColumnList }, setup () { return { list: testData } } }) </script>
<style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
现在,我们来看下此时浏览器的运行效果:
大家可以看到,通过以上的代码编写,数据正常的传递并运行成功了。
5、挠头情况
看到这里,感觉整个组件的设计还挺尽善尽美的。但是呢,大家有没有想过有一种特殊情况,假设后端传来的数据中,有一行数据里面,没有头像avatar的值。那这个时候,如果我们前期没有考虑清楚有可能遇到的各种情况,程序估计很容易地就报错了。
所以我们还要做的一件事情就是,当收不到头像的数据时,我们要给它加一张初始化的图片,以至于保持列表内容一致。
现在我们来对 ColumnList.vue
文件进行改造,具体代码如下:
<template>
<div class="row">
<div v-for="column in columnList" :key="column.id" class="col-4 mb-3">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<img :src="column.avatar" :alt="column.title" class="rounded-circle border border-light w-25 my-3">
<h5 class="title">{{column.title}}</h5>
<p class="card-text text-left">{{column.description}}</p>
<a href="#" class="btn btn-outline-primary">进入专栏</a>
</div>
</div>
</div>
</div>
</template>
<script lang="ts"> import { computed, defineComponent, PropType } from 'vue' //用ts写一个接口,存放列表数据的属性 export interface ColumnProps { id: number; title: string; avatar?: string; description: string; } export default defineComponent({ name: 'ColumnList', props: { //将接口的内容赋值给list数组,方便接收父组件传来的数据 list: { type: Array as PropType<ColumnProps[]>, required: true } }, //将props传递给setup setup(props) { const columnList = computed(() => { //遍历list数组数据的每一行 return props.list.map(column => { //当遇到当前行数据没有头像时 if (!column.avatar) { //赋予初始化图片 column.avatar = require('@/assets/logo.png') } return column }) }) return { columnList } } }) </script>
<style lang="scss" scoped>
</style>
继续,我们把 App.vue
中 testData
的数据进行删减。具体代码如下:
<template>
<div class="container">
<column-list :list="list"></column-list>
</div>
</template>
<script lang="ts"> //制造子组件的接口数据 const testData: ColumnProps[] = [ { id: 1, title: 'test1专栏', description: '众所周知, js 是一门弱类型语言,并且规范较少。这就很容易导致在项目上线之前我们很难发现到它的错误,等到项目一上线,浑然不觉地,bug就UpUp了。于是,在过去的这两年,ts悄悄的崛起了。 本专栏将介绍关于ts的一些学习记录。' //avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg' }, { id: 2, title: 'test2专栏', description: '众所周知, js 是一门弱类型语言,并且规范较少。这就很容易导致在项目上线之前我们很难发现到它的错误,等到项目一上线,浑然不觉地,bug就UpUp了。于是,在过去的这两年,ts悄悄的崛起了。 本专栏将介绍关于ts的一些学习记录。', avatar: 'https://img0.baidu.com/it/u=3101694723,748884042&fm=26&fmt=auto&gp=0.jpg' } ]
大家定位到 testData
中的 avatar
那一行,我们把第一个数据的 avatar
属性进行注释。现在,我们来看下浏览器的效果:
大家可以看到,缺 avatar
属性时,按照我们预期的,浏览器自动显示了我们预先初始化的图片。这样,不论从组件结构设计还是从代码逻辑结构设计来说,是不是感觉可扩展性又增强了许多。
☎️二、GlobalHeader全局Header
1、设计稿抢先看
写完 columnList
组件,我们用一个新的组件来强化这种设计方法。接下来我们来写一个新的组件,GlobalHeader
,即全局头部。先来看下我们要实现的效果图。详情见下图:
2、数据构思
了解完具体的效果图之后呢,同样地,我们先来构思这个组件所需要的数据有哪一些。
这个组件所需要的数据,首先是针对每一个用户的,所以它每个用户拥有自己唯一的 id
,其次就是用户名 name
,最后一个是 是否登录 isLogin
。
分析完成之后,我们现在在 vue3
项目下的 src|components
文件夹下新建一个文件,命名为 GlobalHeader.vue
。之后编写这段业务代码。具体代码如下:
<template>
<div></div>
</template>
<script lang="ts"> import { defineComponent, PropType } from 'vue' //用ts写一个接口,存放列表数据的属性 //name和id加?表示是可选项 export interface UserProps{ isLogin: boolean; name?: string; id?: number; } export default defineComponent({ name: 'GlobalHeader', props: { //将接口的内容赋值给user对象,方便接收父组件传来的数据 user: { type: Object as PropType<UserProps>, required: true } } }) </script>
<style lang="scss" scoped>
</style>
3、视图数据绑定
现在,对数据构思完毕之后,我们来把数据绑定到视图当中。具体代码如下:
<template>
<nav class="navbar navbar-dark bg-primary justify-content-between mb-4 px-4">
<a class="navbar-brand" href="#">周一专栏</a>
<ul v-if="!user.isLogin" class="list-inline mb-0">
<li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">登录</a></li>
<li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">注册</a></li>
</ul>
<ul v-else class="list-inline mb-0">
<li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">欢迎你 {{user.name}}</a></li>
</ul>
</nav>
</template>
<script lang="ts"> import { computed, defineComponent, PropType } from 'vue' export interface ColumnProps { id: number; title: string; avatar?: string; description: string; } export default defineComponent({ name: 'ColumnList', props: { list: { type: Array as PropType<ColumnProps[]>, required: true } } }) </script>
<style lang="scss" scoped>
</style>
4、数据传递
现在,我们在 vue3
项目中的 src
文件夹下的 App.vue
中来进行数据传递。具体代码如下:
<template>
<div class="container">
<global-header :user="user"></global-header>
</div>
</template>
<script lang="ts"> import { defineComponent } from 'vue' //在根文件下引入bootstrap import 'bootstrap/dist/css/bootstrap.min.css' //引入子组件 import GlobalHeader, { UserProps } from './components/GlobalHeader.vue' //制造子组件的接口数据 const currentUser: UserProps = { isLogin: false, name: 'Monday' } export default defineComponent({ name: 'App', components: { GlobalHeader }, setup () { return { user: currentUser } } }) </script>
<style lang="scss" scoped>
</style>
当前 isLogin
的状态我们是设置成 false
。现在,我们来看下此时浏览器的运行效果:
大家可以看到,当前状态为 false
,所以 header
的右边显示的是登录和注册两个按钮,如预期所料。
现在,我们来把 isLogin
的状态改为 true
,具体代码如下:
const currentUser: UserProps = {
isLogin: true,
name: 'Monday'
}
此时我们来看下浏览器的显示效果,如下图所示:
现在,可以看到,当 isLogin
为 true
时,表示用户成功登录了。所以 header
的右方显示的是 欢迎你 Monday 的字样,也如我们预期所料。
📸三、Dropdown下拉菜单
看完上面的内容,大家心里是否有一个疑惑,我们 header
最右方的下拉菜单还没有实现。不着急,接下来我们就来设计这个组件。
1、组件基本功能
我们现在先来设计下这个组件的基本功能。首先在 vue3
项目的 src|components
文件夹下,新增一个 .vue
文件,命名为 Dropdown.vue
。之后编写该文件的代码,具体代码如下:
<template>
<div class="dropdown">
<!-- 下拉菜单标题 -->
<a href="#" class="btn btn-outline-light my-2dropdown-toggle" @click.prevent="toggleOpen()">
{{title}}
</a>
<!-- 下拉菜单内容 -->
<ul class="dropdown-menu" :style="{ display: 'block' }" v-if="isOpen">
<li class="dropdown-item">
<a href="#">新建文章</a>
</li>
<li class="dropdown-item">
<a href="#">编辑资料</a>
</li>
</ul>
</div>
</template>
<script lang="ts"> import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue' export default defineComponent({ name: 'Dropdown', props: { title: { type: String, required: true } }, setup() { const isOpen = ref(false) //点击后打开菜单 const toggleOpen = () => { isOpen.value = !isOpen.value } return { isOpen, toggleOpen } } }) </script>
<style lang="scss" scoped>
</style>
继续,我们来改造下刚刚的 GlobalHeader.vue
文件。具体代码如下:
<template>
<nav class="navbar navbar-dark bg-primary justify-content-between mb-4 px-4">
<a class="navbar-brand" href="#">周一专栏</a>
<ul v-if="!user.isLogin" class="list-inline mb-0">
<li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">登录</a></li>
<li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">注册</a></li>
</ul>
<ul v-else class="list-inline mb-0">
<li class="list-inline-item">
<dropdown :title="`欢迎你 ${user.name}`"></dropdown>
</li>
</ul>
</nav>
</template>
<script lang="ts"> import { defineComponent, PropType } from 'vue' import Dropdown from './Dropdown.vue' export interface UserProps{ isLogin: boolean; name?: string; id?: number; } export default defineComponent({ name: 'GlobalHeader', components: { Dropdown }, props: { user: { type: Object as PropType<UserProps>, required: true } } }) </script>
<style lang="scss" scoped>
</style>
现在,我们来看下浏览器的显示效果:
2、自定义菜单内容DropdownItem
现在,我们已经完成了组件的基本功能。但是细心的小伙伴已经发现,下拉菜单没有办法自定义,因为它被写成固定的了。还有一个问题就是,点击其他区域我们不能收起菜单,这间接地,用户体验好像就没有那么好了。所以呢,有需求了,我们就来完成需求。现在,我们就来解决上述所说的两个问题。
同样地,在vue3项目中的 src|components
文件夹下添加一个 .vue
文件,命名为 DropdownItem.vue
。具体代码如下:
<template>
<li class="dropdown-option" :class="{'is-disabled': disabled}" >
<!-- 定义一个插槽,供给父组件使用 -->
<slot></slot>
</li>
</template>
<script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ props: { //禁用状态属性 disabled: { type: Boolean, default: false } } }) </script>
<style> /* 注意:*表示两个类下面的所有元素 */ .dropdown-option.is-disabled * { color: #6c757d; /* 不让其点击,将pointer-events设置为none */ pointer-events: none; background: transparent; } </style>
接下来我们来将之前写死的内容进行该打造,大家定位到 Dropdown.vue
文件,具体代码如下:
<template>
<div class="dropdown">
<!-- 下拉菜单标题 -->
<a href="#" class="btn btn-outline-light my-2dropdown-toggle" @click.prevent="toggleOpen()">
{{title}}
</a>
<!-- 下拉菜单内容 -->
<ul v-if="isOpen" class="dropdown-menu" :style="{ display: 'block' }">
<slot></slot>
</ul>
</div>
</template>
<script lang="ts"> import { defineComponent, ref } from 'vue' export default defineComponent({ name: 'Dropdown', props: { title: { type: String, required: true } }, setup() { const isOpen = ref(false) //点击后打开菜单 const toggleOpen = () => { isOpen.value = !isOpen.value } return { isOpen, toggleOpen } } }) </script>
<style lang="scss" scoped>
</style>
通过以上代码我们可以了解到,将插槽放到 dropdown-menu
中去。
现在,到了最后一步,我们来看把它引入 GlobalHeader.vue
文件中。具体代码如下:
<template>
<nav class="navbar navbar-dark bg-primary justify-content-between mb-4 px-4">
<a class="navbar-brand" href="#">周一专栏</a>
<ul v-if="!user.isLogin" class="list-inline mb-0">
<li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">登录</a></li>
<li class="list-inline-item"><a href="#" class="btn btn-outline-light my-2">注册</a></li>
</ul>
<ul v-else class="list-inline mb-0">
<li class="list-inline-item">
<dropdown :title="`欢迎你 ${user.name}`">
<drop-down-item><a href="#" class="dropdown-item">新建文章</a></drop-down-item>
<drop-down-item disabled><a href="#" class="dropdown-item">编辑资料</a></drop-down-item>
<drop-down-item><a href="#" class="dropdown-item">退出登陆</a></drop-down-item>
</dropdown>
</li>
</ul>
</nav>
</template>
<script lang="ts"> import { defineComponent, PropType } from 'vue' import Dropdown from './Dropdown.vue' import DropDownItem from './DropDownItem.vue' export interface UserProps{ isLogin: boolean; name?: string; id?: number; } export default defineComponent({ name: 'GlobalHeader', components: { Dropdown, DropDownItem }, props: { user: { type: Object as PropType<UserProps>, required: true } } }) </script>
<style lang="scss" scoped>
</style>
此时,我们来看下浏览器的显示效果:
大家可以看到,此时的编辑资料已经变成了灰色并且无法进行点击和跳转。同时,自定义菜单的内容也一一显示了出来。
到这里,这一步的内容我们就完成啦!接下来我们继续升华这个组件,让它的用户体验更为极致。
3、组件点击外部区域自动隐藏
大家可以联想到平常自己点击各大网站时的场景,当点击菜单外部区域时,组件是不是就会自动隐藏。所以,接下来我们就来实现这个功能。
首先我们要明确,需要完成的两个任务:
- 在
onMounted
时候添加click
事件,在onUnmounted
的时候将事件删除; - 拿到
Dropdown
的DOM
元素,从而判断点击的内容是否被这个元素包含。
接下来我们定位到 Dropdown.vue
文件,继续升级改造。具体代码如下:
<template>
<div class="dropdown" ref="dropdownRef">
<a href="#" class="btn btn-outline-light my-2 dropdown-toggle" @click.prevent="toggleOpen()">
{{title}}
</a>
<ul v-if="isOpen" class="dropdown-menu" :style="{ display: 'block' }">
<slot></slot>
</ul>
</div>
</template>
<script lang="ts"> import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue' export default defineComponent({ name: 'DropDown', props: { title: { type: String, required: true } }, setup() { const isOpen = ref(false) // 获取ref的dow节点 const dropdownRef = ref<null | HTMLElement>(null) const toggleOpen = () => { isOpen.value = !isOpen.value } const handler = (e: MouseEvent) => { if (dropdownRef.value) { // 用contains来判断是否包含另外一个dow节点 // 当点击的不是当前区域的节点,并且菜单是打开的 if (!dropdownRef.value.contains(e.target as HTMLElement) && isOpen.value) { isOpen.value = false } } } onMounted(() => { document.addEventListener('click', handler) }) onUnmounted(() => { document.removeEventListener('click', handler) }) return { isOpen, toggleOpen, dropdownRef, handler } } }) </script>
<style lang="scss" scoped>
</style>
此时我们来看下浏览器的显示效果:
大家可以看到,改造完 dropdown
的逻辑后,如我们预期所料的,当我们点击组件外部区域时,组件也自动隐藏了。
4、自定义函数
到这里,整个 GlobalHeader
组件可以说是相对比较完美了。但是大家有没有发现,在设计 dropdown
组件时,dropdown.vue
的代码好像看起来还稍微有一点点冗余。
这个时候我们可以考虑把它的逻辑抽离出来,单独放到一个自定义函数里面。接下来一起来实现一下吧~
首先我们在 vue3
项目中的 src
文件夹下,新建一个文件夹,命名为 hooks
。之后在 hooks
下新建一个文件,命名为 useClickOutside.ts
。 useClickOutside
的具体代码如下:
import { ref, onMounted, onUnmounted, Ref } from "vue";
const useClickOutside = (elementRef: Ref<null | HTMLElement>) => {
const isClickOutside = ref(false)
const handler = (e: MouseEvent) => {
if (elementRef.value){
if(elementRef.value.contains(e.target as HTMLElement)){
isClickOutside.value = true
}else{
isClickOutside.value = false
}
}
}
onMounted( () => {
document.addEventListener('click', handler)
})
onUnmounted(() => {
document.removeEventListener('click', handler)
})
return isClickOutside
}
export default useClickOutside
抽离完代码后,我们继续把 Dropdown.vue
化繁为简。具体代码如下:
<template>
<div class="dropdown" ref="dropdownRef">
<a href="#" class="btn btn-outline-light my-2 dropdown-toggle" @click.prevent="toggleOpen()">
{{title}}
</a>
<ul v-if="isOpen" class="dropdown-menu" :style="{ display: 'block' }">
<slot></slot>
</ul>
</div>
</template>
<script lang="ts"> import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue' import useClickOutside from '../hooks/useClickOutside' export default defineComponent({ name: 'DropDown', props: { title: { type: String, required: true } }, setup() { const isOpen = ref(false) // 获取ref的dow节点 const dropdownRef = ref<null | HTMLElement>(null) const toggleOpen = () => { isOpen.value = !isOpen.value } const isClickOutside = useClickOutside(dropdownRef) if (isOpen.value && isClickOutside) { isOpen.value = false } watch(isClickOutside, () => { if (isOpen.value && isClickOutside.value) { isOpen.value = false } }) return { isOpen, toggleOpen, dropdownRef } } }) </script>
<style lang="ts" scoped>
</style>
此时浏览器的显示效果如下:
大家可以看到,最终的显示效果也是一样的。但是呢,通过代码逻辑抽离,我们整个组件的设计看起来也更加完美,可扩展性也变得更高。
5、联合效果
最后,我们把上面所学的GlobalHeader和Columnist结合起来,来看一下一体化的效果。详情见下图:
以上就是关于 ColumnList
和 GlobalHeader
两个组件的实现方式。不知道大家是否还意犹未尽呢~
后面还会持续出关于组件设计的文章,欢迎关注~
🛒四、结束语
讲到这里,关于组件 GlobalHeader
和 ColumnList
的设计就结束啦!在设计组件的时候呢,要特别考虑组件的可扩展性。如果一个组件在写的时候,感觉没什么复用度,那么这个时候可能就得思考下,是不是哪个环节出现问题了。多问自己为什么,多问自己这个组件是否能抽离的更好。
以上就是本文的全部内容!如有疑问或文章有误欢迎评论区留言或公众号后台加我微信交流~
关注公众号星期一研究室,第一时间关注学习干货,更多精选专栏待你解锁~
如果这篇文章对你有用,记得留下脚印再走哦~
我们下期见!🥂🥂🥂
🐣彩蛋 One More Thing
基础知识回顾
软件推荐
这里给大家推荐文章用到的一个画图软件:Axure RP 9
Axure RP 旨在用于画低保真原型图,对于开发者来说也是极其友好的。丰富的控件库和动画交互,可以满足日常画图的大部分需求。
安利一波~
👋👋👋
今天的文章组件库实战 | 用vue3+ts实现全局Header和列表数据渲染ColumnList分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22245.html