N久之前我写过一篇文章讲到了简易上手vue3.x并配合ant-design-vue做了一些学习。当时文章的结尾我说随着3.x版本的发展市面上一定会出现很多不错的UI框架。结果没多久element-plus出现了。前段时间,尤大带货的Naive UI也引起了广大开发者的兴趣。
所以我也抱着兴趣开始研究起它的文档,正好之前也没有真正接触过vite,也是结合Vite学习一波怎样用Vite+Naive搭建一个简易的前端项目。
安装部分
vite的安装和新建项目不赘述,这里安装Naive的时候需要注意,Naive使用了vfonts,需要一并安装,并在main.ts中引入其通用字体和等宽字体
import 'vfonts/Lato.css' // 通用字体
import 'vfonts/FiraCode.css' // 等宽字体
复制代码
另外还需要安装vicons图标库,这是Naive默认使用的图标库,其他需要注意的暂无。
布局部分
文档给出了很多布局样例,你只需要选一个自己需要的粘贴就行,我选择的是一个头部与侧边侧边栏固定,右侧滚动可以切换主题的布局。需要注意的是这套方案上想要实现上述效果,其最外层的父元素需要设置样式position为relative。头部的父层n-layout和侧边栏的父层n-layout需要配置position="absolute"
且必须用文档所说的这个方式配置,自己通过样式添加position:absolute是不行的。
<div class="container"> <!-- container设置了position:relative -->
<n-layout position="absolute">
<Header class="header" :inverted="inverted" @toggleInverd="toggleInverd" />
<n-layout has-sider position="absolute" class="content">
<Sidebar :inverted="inverted" />
<n-layout content-style="padding: 24px;">
<Breadcrumb />
<n-message-provider>
<router-view></router-view>
</n-message-provider>
</n-layout>
</n-layout>
</n-layout>
</div>
复制代码
侧边栏与路由
侧边栏内的内容需要通过路由动态生成出来,Naive的侧边栏可以结合n-layout-sider和menu组件开发,Naive的menu组件可以说是一个亮点,但也有一些桎梏。在使用其他的UI组件开发侧边的时候通常我们还需要单独开发一个子组件用于放置链接,标题等,然后利用组件的递归功能生成出基于路由产生的组件。
但是Naive的menu则是另一种大道至简的思想,你只需要调用组件,然后按照样例的方式将列表内容添加即可生成菜单,没有什么子组件,也不存在递归嵌套。
<n-menu
:inverted="inverted"
:collapsed-width="64"
:collapsed-icon-size="22"
@update:value="handleUpdateValue" // 路由跳转通过方法实现
:options="navs"
:value="active"
/>
复制代码
那么,对于开发者而言,只需要将路由列表导入进来,贴上去就好了,但是需要注意,也可能是我没仔细看文档。这里菜单中最后显示的名字和图表,必须在数据列表中就叫llabel和icon。否则菜单组件将不能正常显示,同时,每一个列表项需要有一个唯一的key,这可以和路由中的name同名。
// routes.js
import {
h,
} from "vue";
import Home from '@/components/Home.vue'
import {
NIcon
} from "naive-ui";
import {
BookOutline as BookIcon,
PersonOutline as PersonIcon,
WineOutline as WineIcon,
} from "vicons/ionicons-v5";
function renderIcon(icon) {
return () => h(NIcon, null, {
default: () => h(icon)
});
}
const routes = [{
path: '/',
name: 'Index',
key: 'Index',
redirect: '/dashboard/index',
hidden: true,
label: "首页",
icon: renderIcon(BookIcon), // icon是通过函数生成的
},
// 首页
{
path: '/dashboard',
name: 'Dashboard',
key: 'Dashboard',
redirect: '/dashboard/index',
component: Home,
label: "首页",
icon: renderIcon(BookIcon),
meta: {
label: "首页",
},
children: [{
path: "/dashboard/index",
name: 'DashboardIndex',
key: 'DashboardIndex',
component: () => import('../views/HelloWorld.vue'),
label: "Hello",
icon: renderIcon(BookIcon),
meta: {
label: "Hello",
}
}, {
path: "/dashboard/page1",
name: 'DashboardPage1',
key: 'DashboardPage1',
component: () => import('../views/Page1.vue'),
label: "第一页",
icon: renderIcon(PersonIcon),
meta: {
label: "第一页",
}
}]
},
// 第二页
{
path: "/users",
name: 'Users',
key: 'Users',
redirect: '/users/index',
component: Home,
label: "账户",
icon: renderIcon(WineIcon),
meta: {
label: "账户",
},
children: [{
path: '/users/index',
name: 'UsersIndex',
key: 'UsersIndex',
component: () => import('../views/Page2.vue'),
label: "第二页",
icon: renderIcon(WineIcon),
meta: {
label: "第二页",
},
}],
},
]
export default routes;
复制代码
但如果仅仅是把路由列表按照文档指定的样子引过来就可以直接使用那也是不太可能的。这之后有个问题摆在面前,我并不希望展示路由列表里所有的链接,此时要怎么办?按照传统的办法我可以给不需要展示的列添加一个自定义的hidden属性,然后在开发组件的时候判断为hidden则不展示。但是Naive的menu组件高度封装,也没有提供这样的判断是无法办到过滤展示的。
所以比较好的办法是拷贝一份路由列表,在拷贝的路由列表上做相关判断。但是如果你使用最简单的JSON.parse(JSON.stringify(list))
这样的形式拷贝新的问题又会出现,你会发现列表虽然过滤了,但是图标没有了。
这是因为icon是通过h函数生成出来的,它的类型是一个函数,JSON的办法是拷贝不了函数,所以你需要实现一个真正意义上的深拷贝方法,然后用深拷贝去复制一份路由表做过滤,深拷贝的实现思路有很多,网上抄一下,不在赘述。
import { useRouter, useRoute } from "vue-router";
import routes from "@/router/routes";
import { deepClone } from "@/utils/index";
let active = ref("");
function filterNavs(ary) {
ary.forEach((item, index) => {
if (item.children) {
filterNavs(item.children);
} else {
if (item.hidden) {
ary.splice(index, 1);
}
}
});
return ary;
}
...
setup() {
...
const route = useRoute();
const navs = filterNavs(deepClone(routes));
// 路由跳转需通过代码实现
const handleUpdateValue = (key, item) => {
active.value = key;
router.push({ path: item.path });
};
return {
navs,
handleUpdateValue,
...
}
}
复制代码
那么怎样实现路由跳转呢,因为无法通过以前的方式添加link形式跳转,menu组件给了一个update的事件方法,在该事件中可以配置跳转路由实现跳转,具体见上面的代码。
配合axios实现全局提示
最后再来说一下全局提示,大部分情况下我们希望全局封装axios然后请求成功或失败的时候产生一个提示消息。首先需要全局引入axios,我在axios的基础上封装了一个自己的http = $http;` 但是在ts的环境下是不被允许的,我对ts不熟,也是找了其他的博客看的怎么配置。
// main.ts
import $http from "./api/axios"
//全局配置
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$http: Function,
}
}
const app = createApp(App);
app.config.globalProperties.$http = $http;
复制代码
紧接着我像之前一样在具体的代码中使用const { ctx } = getCurrentInstance();
和ctx.$http
去执行代码却发现提示我http添加进来。叕在进行了若干百度之后发现如果是在ts的环境下定义的全局vue方法被添加到了appContext里,需要在这里去调用相关方法。
import { defineComponent, getCurrentInstance } from "vue";
export default defineComponent({
setup() {
const { appContext } = getCurrentInstance();
const { $http } = appContext.config.globalProperties;
$http({ url: "/test" });
},
});
复制代码
此时错误提示可以走到了,但是要怎样展示message消息,这里也是我不太理解Naive作者的意图,在Naive里面像Mesage, Notification这样的提示类组件需要在外层嵌套一个父层
使用前提
如果你想使用信息,你需要把调用其方法的组件放在 **n-message-provider**
内部并且使用 **useMessage**
去获取 API。
像Ant-design-vue只需要直接引入Message就可以了,这里我没有想明白为什么要套一层。 考虑到提示这种东西我应该会在各个页面都用到,所以我把其放在了router-view的外层
<n-message-provider>
<router-view></router-view>
</n-message-provider>
复制代码
那么,容器准备好了,但此时若是参考ant-design-vue的经验直接使用引入的message会提示message的相应方法为undefined。
因为Naive的useMessage必须是在一个确定的dom结构之内,或者说至少需要在setup函数里才能触发,但是我是在自己封装的axios函数里引用的,在哪里触发才不会有问题?实际山我在调用http方法里调用useMessage就可以了,这样会有一个弊端不过暂时我没想到怎么处理,那就是每次调用$http都会产生一个useMessage的实例,这个后续再想吧。
// 自定义的axios
import {
useMessage
} from 'naive-ui';
...
function $axios(options:any) {
...
}
function $http(options:any, success:any, failure:any) {
// 创造message方法的实例
const message = useMessage();
return $axios({
...
})
.then(res => {
...
message.success(res.message, {
duration: 5000
});
})
.catch(err => {
...
message.error(res.message, {
duration: 5000
});
})
}
复制代码
这样就可以正常展示出全局的axios错误提示了
总结
不得不说,Naive UI是一个使用体验极佳的Vue3.x的UI框架,官方文档里也处处透露出作者饱含诗意的文艺气息,随着Naive被越来越多的开发者注意,后续一定会发现若干的不足,也一定会迎来几次较大更新,让我们共同祝愿Naive UI能够越走越好。