評(píng)論放鏈接好像會(huì)被吞挑社,demo地址:https://gitee.com/zqw127/vue3-jsx-project
之前我用vue2實(shí)現(xiàn)了一版,地址為:vue中使用遞歸組件實(shí)現(xiàn)無限級(jí)菜單
想要實(shí)現(xiàn)的功能就是根據(jù)路由信息自動(dòng)生成對(duì)應(yīng)的菜單攀涵。
這次用vue3+jsx再實(shí)現(xiàn)一版酥泞,思路沒有變化伞鲫,但是寫起來基本完全不同了戏售,主要變化有:
1.composition api寫法與vue2中的區(qū)別
2.使用jsx+ts
3.router變化
最主要的變化還是第二個(gè)拗秘,下面我會(huì)把涉及到的內(nèi)容以我的理解講出來卓嫂,如果有理解更到位的大佬,歡迎指教哦聘殖。當(dāng)然最基礎(chǔ)的jsx與ts用法下面就不說了。至于jsx寫法與傳統(tǒng)vue文件寫法兩者的優(yōu)缺點(diǎn)行瑞,網(wǎng)上的大佬已經(jīng)說了很多了奸腺,主要看自己喜歡,我覺得可以不用血久,但不能問起來說不出(懂的·都懂)突照。
vue中使用jsx
1.安裝@vue/babel-plugin-jsx
npm run @vue/babel-plugin-jsx --save
2.在項(xiàng)目的babel.config.js中的plugins添加,下面是在腳手架生成文件基礎(chǔ)下添加:
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: ['@vue/babel-plugin-jsx']
}
3.創(chuàng)建tsx文件以及用法
使用ts寫jsx的文件就是tsx文件氧吐,用tsx文件來代替我們平常寫的vue文件讹蘑,下面是兩種文件的區(qū)別:
.vue文件:
<template>
<div>{{bar}}</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const bar = ref('hello')
return {
bar
}
}
})
</script>
.tsx文件:
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const bar = ref('hello')
return () => {
return <div>{bar.value}</div>
}
}
})
上面就是簡(jiǎn)單的用法區(qū)別末盔,當(dāng)setup返回一個(gè)函數(shù)時(shí),就是返回一個(gè)render函數(shù)座慰,這個(gè)函數(shù)返回的內(nèi)容就當(dāng)作組件的模板使用陨舱,下面是一些jsx在vue中基礎(chǔ)用法:
v-bind或props:
<div data={data}></div> // 變量與js表達(dá)式需要用一對(duì)大括號(hào)引起來
v-for:
{
[1,2,3].map((item) => {
return <div key={item}>{item}<div>
})
}
v-if:
{ flag ? <div>超人鴨<div> : null }
v-on:
const fn = () => {
.....
}
<div onClick={fn}>點(diǎn)擊</div>
需要傳遞參數(shù):
<div onClick={() => { fn(111) }}>點(diǎn)擊</div>
class:
<div class="a">超人鴨</div>
基礎(chǔ)的用法就是這些,還有v-model版仔、v-show我們的插件內(nèi)部已經(jīng)實(shí)現(xiàn)了游盲,可以直接用,具體可以看@vue/babel-plugin-jsx蛮粮,插槽的用法在下面會(huì)說到益缎。
功能介紹以及實(shí)現(xiàn)思路
上面說到,實(shí)現(xiàn)的功能就是根據(jù)路由信息使用遞歸組件實(shí)現(xiàn)無限級(jí)菜單然想,其實(shí)就是去生成菜單組件莺奔,先看看生成的菜單:
菜單其實(shí)只有兩種狀態(tài):
1.菜單目錄,下面還有子菜單的变泄,展現(xiàn)出來就是點(diǎn)擊可以收縮子菜單項(xiàng)(上圖中的用戶管理令哟、菜單1、菜單1-2)
2.菜單項(xiàng)杖刷,就是沒有下一級(jí)了励饵,點(diǎn)擊可以跳轉(zhuǎn)到具體頁(yè)面。
整個(gè)菜單就是由這兩種組件組成滑燃,我使用element-ui中的導(dǎo)航組件來實(shí)現(xiàn):
菜單目錄:el-submenu
菜單項(xiàng):el-menu-item
而菜單目錄中可以任意嵌套菜單目錄和菜單項(xiàng)役听,el-submenu也是可以的。
注:vue3中用的是element-plus哦表窘,如果對(duì)這個(gè)組件不熟悉建議先看一下文檔哦典予,導(dǎo)航組件文檔
然后是路由信息
下面是在腳手架生成的 router/index.ts文件中改 routes對(duì)象而已,其他配置不動(dòng)乐严。
export const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'LAYOUT_VIEW',
component: Layout,
meta: { isLogin: true, hidden: true },
redirect: '/user'
},
{
path: '/user',
name: 'USER_MANAGE',
component: Layout,
meta: { title: '用戶管理', icon: 'el-icon-s-tools', alwaysShow: true },
redirect: '/user/info',
children: [
{
path: 'info',
name: 'USER_INFO',
component: () => import('@/views/userInfo/index'),
meta: { title: '用戶信息' }
}
]
},
{
path: '/test1',
name: 'TEST1',
component: Layout,
meta: { title: '菜單1', icon: 'el-icon-s-tools', alwaysShow: true },
children: [
{
path: 'test1-1',
name: 'TEST1-1',
component: () => import('@/views/test.vue'),
meta: { title: '菜單1-1' }
},
{
path: 'test1-2',
name: 'TEST1-2',
component: () => import('@/views/test.vue'),
meta: { title: '菜單1-2', alwaysShow: true },
children: [
{
path: 'test1-2-1',
name: 'TEST1-2-1',
component: () => import('@/views/test.vue'),
meta: { title: '菜單1-2-1' }
}
]
}
]
},
{
path: '/test2',
name: 'TEST2',
component: Layout,
meta: { title: '菜單2', icon: 'el-icon-s-tools', alwaysShow: false },
children: [
{
path: 'test2-1',
name: 'TEST2-1',
component: () => import('@/views/test.vue'),
meta: { title: '菜單2-1' }
}
]
},
{
path: '/login',
name: 'LOGIN',
component: () => import('@/views/login/index.vue'),
meta: { isLogin: false, hidden: true }
}
]
這個(gè)路由信息就對(duì)應(yīng)了上面生成的菜單組件瘤袖。用來生成菜單組件最主要的邏輯就是根據(jù)路由有沒有children屬性,如果有昂验,那就是菜單目錄捂敌,對(duì)應(yīng)el-submenu,如果沒有既琴,那就是菜單項(xiàng)占婉,對(duì)應(yīng)el-menu-item。其中layout就是一個(gè)放著<router-view>的布局組件甫恩,然后關(guān)鍵信息都在每個(gè)路由的meta中:
- title代表菜單的名稱
- icon就是圖標(biāo)的類名逆济,這里用了element自帶的
- hidden就表示不在菜單中顯示,比如登錄路由,404頁(yè)等
- alwaysShow是一個(gè)額外的邏輯奖慌,當(dāng)路由的children只有一項(xiàng)時(shí)抛虫,默認(rèn)是直接展示菜單項(xiàng)的,就是不展示那種可以收縮的菜單目錄简僧,只有配置了這個(gè)alwaysShow屬性后才作菜單目錄渲染建椰。當(dāng)然這是我自己的邏輯,可以隨意改涎劈。
我這樣寫就要求每個(gè)路由信息都要有meta對(duì)象广凸。接下來就可以拿著這個(gè)路由信息去生成組件了。
編寫組件
文件結(jié)構(gòu):
因?yàn)閑lement的菜單組件最外層是一個(gè)<el-menu></el-menu>蛛枚,所以在index.tsx中編寫外部包裹的組件谅海,sidebarItem就是真正實(shí)現(xiàn)遞歸邏輯的組件。
先看index.tsx:
import { defineComponent, computed } from 'vue'
import '../style/sidebar.scss'
import { routes } from '@/router/index' // 將在router中定義的routes引入
import { useRoute } from 'vue-router'
import SidebarItem from './sidebarItem'
export default defineComponent({
setup () {
// 過濾掉第一層不顯示的路由蹦浦,比如登錄路由
const isShowRoutes = computed(() => {
return routes.filter((item) => {
return !item.meta!.hidden
})
})
// 當(dāng)前路由的路徑扭吁,為和el-menu的高亮項(xiàng)對(duì)應(yīng)
const currentPath = computed(() => {
return useRoute().path
})
return () => {
return <div class="layout-sidebar-wrapper">
<el-scrollbar style="height:100%">
<el-menu default-active={currentPath.value}
backgroundColor="#304156"
text-color="#bfcbd9"
unique-opened={false}
active-text-color="#409EFF"
collapse-transition={false}
mode="vertical">
{
isShowRoutes.value.map((route) => {
return <SidebarItem item={route}
basePath={route.path}
key={route.path}>
</SidebarItem>
})
}
</el-menu>
</el-scrollbar>
</div>
}
}
})
這代碼塊沒高亮看著挺難受,下面把幾個(gè)代碼解釋一下以及與vue2寫法的區(qū)別
上面也說到在setup返回一個(gè)函數(shù)就是渲染函數(shù)盲镶,在里面返回組件的模板侥袜,其他邏輯沒變。
使用路由溉贿,需要在vue-router中引入:
vue2獲取當(dāng)前路由的信息:
this.$route
vue3:
import { useRoute } from 'vue-router'
const route = useRoute()
vue2路由跳轉(zhuǎn):
this.$router.push('/')
or
this.$router.push({
path: '/'
})
vue3:
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/')
計(jì)算屬性:
vue2:
computed: {
currentPath() {
return this.$route.path
}
}
vue3:
import { computed } from 'vue'
const currentPath = computed(() => {
return useRoute().path
})
使用需要:
currentPath.value
類型斷言枫吧,ts中的一個(gè)語(yǔ)法,因?yàn)閠s不確定我們每個(gè)路由都有meta信息宇色,所以上面第一個(gè)計(jì)算屬性中的item.meta可能為undefined九杂,所以在后面加上一個(gè)!表示我們確定這個(gè)屬性一定存在:
item.meta!.hidden
最后將isShowRoutes去遍歷渲染<SidebarItem>組件,就相當(dāng)于v-for宣蠕,<SidebarItem>組件要接收一個(gè)item也就是一個(gè)路由對(duì)象信息例隆,一個(gè)basePath就是每一個(gè)路由信息的基礎(chǔ)路徑這兩個(gè)props。這里使用tsx就有一個(gè)明顯的優(yōu)點(diǎn)抢蚀,會(huì)對(duì)props作類型校驗(yàn)镀层。具體可以自己去試一下。
sidebarItem.tsx
import { defineComponent, PropType } from 'vue'
import { RouteRecordRaw, useRouter } from 'vue-router'
import path from 'path'
const SidebarItem = defineComponent({
name: 'SidebarItem',
props: {
item: {
type: Object as PropType<RouteRecordRaw>,
required: true
},
basePath: {
type: String,
required: true
}
},
setup (props) {
const router = useRouter()
let data: Partial<RouteRecordRaw> = { // 存儲(chǔ)當(dāng)前路由的處理后的信息
}
const resolvePath = (routePath: string): string => {
return path.resolve(props.basePath, routePath)
}
const navigation = (path: string) => {
router.push(path)
}
return () => {
const handleRoute = () => {
const { item } = props
// 最后一層的情況皿曲,渲染菜單項(xiàng)
if (!item.children) {
data = { ...item, path: '' }
return <el-menu-item onClick={() => { navigation(resolvePath(data.path!)) }} index={resolvePath(data.path!)}>
<span>{data.meta!.title}</span>
</el-menu-item>
}
// 把當(dāng)前這一層路由中的children的hidden過濾點(diǎn)
const showingChildren = item.children.filter((item) => {
return item.meta && !item.meta.hidden
})
item.children = showingChildren
// 如果當(dāng)前路由只有一個(gè)children并且這個(gè)children沒有children屬性唱逢,并且沒有設(shè)置alwaysShow這個(gè)屬性,當(dāng)菜單項(xiàng)渲染
if (showingChildren.length === 1 && !showingChildren[0].children && (item.meta && !item.meta.alwaysShow)) {
data = showingChildren[0]
return <el-menu-item index={resolvePath(data.path!)} onClick={() => { navigation(resolvePath(data.path!)) }}>
<span>{data.meta!.title}</span>
</el-menu-item>
}
const slots = {
title: () => {
return <div>
{item.meta!.icon ? <i class={item.meta!.icon}></i> : null}
<span>{item.meta!.title ? item.meta!.title : '未定義菜單名稱'}</span>
</div>
}
}
// 有children屬性屋休,沒其他特殊情況坞古,作菜單目錄渲染,遞歸引用
return <el-submenu index={resolvePath(item.path)} v-slots={slots}>
{item.children.map((child) => {
return <SidebarItem item={child} basePath={resolvePath(child.path)} key={child.path}></SidebarItem>
})}
</el-submenu>
}
return <div>{handleRoute()}</div>
}
}
})
export default SidebarItem
這個(gè)組件就比較復(fù)雜了博投,下面從上到下把幾塊解釋一下:
- PropType
vue中定義props的類型只能定義js的基本類型,對(duì)于Object盯蝴、Array毅哗、Function之類的類型在ts檢驗(yàn)中基本等于沒用听怕,所以需要PropType來定義具體的類型
用法:
item: {
在PropType的<>填入具體的類型
type: Object as PropType<RouteRecordRaw>,
required: true
}
RouteRecordRaw是路由信息的類型,是vue-router自帶的虑绵,把它引入就行:
import { RouteRecordRaw } from 'vue-router'
- Partial
Partial是ts的一個(gè)語(yǔ)法尿瞭,將傳入的類型全部變成可選的,來避免一些麻煩
用法:
interface Type{
a: string;
b: number;
}
let c: Partial<Type> = {}
此時(shí)c的類型就是:
{
a?: string;
b?: number;
}
-
遞歸組件
我們?cè)趘ue2中使用遞歸組件只要聲明了name屬性就可以直接在template中使用翅睛,但是在tsx中声搁,使用遞歸組件需要明確的定義:
const SidebarItem = defineComponent({
setup(){
return () => {
return <SidebarItem></SidebarItem>
}
}
})
最后別忘了把組件導(dǎo)出:
export default SidebarItem
-
插槽
image.png
上面這一塊的就是插槽的引入,先看看我們?cè)静宀鄣膶懛ú斗ⅲ苯涌磂lement官方對(duì)el-submenu的寫法:
<el-submenu index="1">
<template v-slot:title>
<i class="el-icon-location"></i>
<span>導(dǎo)航一</span>
</template>
<el-menu-item index="1-1">選項(xiàng)1</el-menu-item>
<el-menu-item index="1-2">選項(xiàng)2</el-menu-item>
<el-menu-item index="1-3">選項(xiàng)3</el-menu-item>
</el-submenu>
這就是我們之前的寫法疏旨,el-submenu就是有一個(gè)叫title的具名插槽,換成tsx寫法就是我上面那種扎酷,通過傳入一個(gè)對(duì)象檐涝,對(duì)象里面配置各個(gè)函數(shù),每個(gè)函數(shù)返回要插入的dom法挨,具體可以看jsx的介紹文檔谁榜,各種用法都挺詳細(xì)的:
-
注意點(diǎn)
上面有一個(gè)navigation函數(shù),作用就是點(diǎn)擊菜單項(xiàng)進(jìn)行路由跳轉(zhuǎn)凡纳,我一開始是這樣寫的:
const navigation = (path: string) => {
const router = useRouter()
router.push(path)
}
就是將路由的聲明放在方法中窃植,但是這樣router會(huì)變成undefined,需要放在setup外層聲明荐糜,可能是跟執(zhí)行的時(shí)機(jī)有關(guān)巷怜,setup會(huì)在相當(dāng)于vue2的beforeCreate和create之間的這個(gè)時(shí)期執(zhí)行,但是和路由具體的關(guān)系我還不太清楚狞尔,待我去研究一哈丛版。
這樣整個(gè)功能就實(shí)現(xiàn)了,之后只需要在路由添加配置路由信息偏序,菜單就會(huì)自動(dòng)生成页畦。歡迎指教哦。
補(bǔ)充:jsx中子組件怎么寫插槽
上面在實(shí)現(xiàn)功能中講到了在jsx中插槽如何使用研儒,但都是在使用別人的插槽豫缨,emmm也就是一直插別的組件,也就是在父組件中使用插槽端朵。那我們?nèi)绾卧趈sx定義自己的插槽呢好芭,其實(shí)就是在子組件中寫插槽。
- 先看看之前在.vue文件中如何寫插槽:
默認(rèn)插槽:
在子組件:
<div>
<span>children</span>
<slot></slot>
</div>
在父組件:
<children>
哈哈哈
</children>
哈哈哈就會(huì)填充到子組件的slot標(biāo)簽里面
具名插槽
在子組件:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<div>
<slot></slot>
</div>
</div>
在父組件:
<children>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</children>
v-slot 只能添加在 <template>標(biāo)簽上
任何沒有被包裹在帶有 v-slot 的 <template> 中的內(nèi)容都會(huì)被視為默認(rèn)插槽的內(nèi)容冲呢,比如上面的兩個(gè)p標(biāo)簽舍败,就會(huì)渲染在默認(rèn)插槽中。
默認(rèn)插槽其實(shí)有個(gè)default的名字,所以上面的父組件也可以寫成:
<children>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
</children>
作用域插槽
作用域插槽就是子組件通過插槽把子組件里面的變量傳遞給父組件
在子組件里面:
<span>
<slot name="user" v-bind:user="user">
</slot>
</span>
data(){
return: {
user: {
name: '超人鴨'
}
}
}
在父組件中:
<children>
// slotProps是自己起的名字邻薯,可以隨便起
<template v-slot:user="slotProps">
{{ slotProps.user.name}}
</template>
</children>
在子組件中的slot標(biāo)簽都可以綁定很多變量裙戏,vue會(huì)把這些變量都放在一個(gè)對(duì)象上,然后在父組件上通過這個(gè)對(duì)象去點(diǎn)出來綁定的變量厕诡,既然是一個(gè)對(duì)象累榜,那就可以解構(gòu),所以上面父組件的寫法可以變成下面這樣:
在父組件中:
<children>
<template v-slot:user="{user}">
{{ user.name}}
</template>
</children>
上面就是在.vue文件中使用插槽的寫法灵嫌,下面直接看在jsx中如何寫插槽:
默認(rèn)插槽
在子組件:
export default defineComponent({
setup (props, { slots }) {
return () => {
return <div>{slots}</div>
}
}
})
在setup的第二個(gè)參數(shù)是一個(gè)對(duì)象壹罚,里面有一個(gè)slots,感覺它就是為了jsx寫法而存在的(個(gè)人目前見解)
父組件的用法上面已經(jīng)說到了寿羞,這里再寫一遍吧猖凛,默認(rèn)插槽其實(shí)有個(gè)被省略的名字:default ,所以父組件中對(duì)默認(rèn)插槽有兩種用法:
1.
<children>
<div>超人鴨</div>
</children>
2.
const slots = {
default: () => {
return <div>超人鴨</div>
}
}
<children v-slots={slots}>
</children>
具名插槽:
在子組件
export default defineComponent({
setup (props, { slots }) {
return () => {
return <div>
<div>{slots}</div>
// 這里需要做個(gè)判斷稠曼,只有存在才去渲染
<div>{slots.header ? slots.header() : null}</div>
</div>
}
}
})
在父組件:
const slots = {
default: () => {
return <div>超人鴨</div>
},
header: () => {
return <div>header</div>
}
}
<children v-slots={slots}>
</children>
使用jsx后其實(shí)比較清晰形病,父組件傳遞給子組件的插槽其實(shí)是個(gè)函數(shù),然后在子組件里面執(zhí)行霞幅,渲染函數(shù)返回的dom漠吻,還有一個(gè)注意的地方,在子組件中寫slots要單獨(dú)包裹一個(gè)標(biāo)簽司恳。
作用域插槽:
上面在說具名插槽的時(shí)候說到途乃,插槽其實(shí)就是子組件中去執(zhí)行父組件傳遞進(jìn)來的函數(shù),那把父組件中要使用子組件中的變量只需要把變量放在函數(shù)的參數(shù)上就可以扔傅。
在子組件:
export default defineComponent({
setup (props, { slots }) {
const list = reactive(
['1', '2', '3']
)
return () => {
return <div>
<div>{slots}</div>
<div>{slots.header ? slots.header() : null}</div>
<div>{slots.list ? slots.list(list) : null}</div>
</div>
}
}
})
在父組件:
const slots = {
default: () => {
return <div>超人鴨</div>
},
header: () => {
return <div>header</div>
},
list: (list: Array<any>) => {
return list.map((item) => {
return <div>{item}</div>
})
}
}
<children v-slots={slots}>
</children>
渲染: