这段时间跟着线上课程完成了一个项目:商医通(基于Vue3+TypeScript的医院挂号平台)。具体我就不过多地介绍其具体功能以及详细的实现步骤了,感兴趣的小伙伴直接:传送门 。该文章我就分享一下在该项目中学习到的一些知识点,以及一些前端小伙伴比较常接触到的功能。
目录
- 项目介绍
- 项目实现效果如下:
- 项目的技术选型如下:
- 项目总结:
- 一、Vue3中的一些基础语法?
- 二、Vite构建工具如何使用?
- 三、TypeScript的基础语法有哪些?
- 四、如何使用Vue-router进行路由的配置?
- 五、如何使用Pinia来进行数据状态的管理?
- 六、element-plus组件库该如何使用?
- 七、如何对axios进行二次封装来实现向服务器请求数据?
项目介绍
项目实现效果如下:
项目的技术选型如下:
项目总结:
一、Vue3中的一些基础语法?
官方文档
由于该项目是使用Vue3实现的,因此需要具备Vue3的一些基础知识,但是Vue3中的知识点不会介绍太多,主要介绍在这个项目中接触到的比较多的部分知识。
1.Vue3与Vue2创建实例方法的不同:
//Vue3中的写法:
import {createAPP} from 'vue'
import App from './App.vue'
createApp(App).mount("#app")
//Vue2中的写法:
import Vue from 'vue'
import App from './App.vue'
new Vue({
el:"#app",
render:h=>h(APP)
})
2.Vue3中setup的作用:
理解:Vue3.0中一个新的配置项,值为一个函数,setup是所有Composition API(组合API)“ 表演的舞台 ”。组件中所用到的:数据、方法等等,均要配置在setup中。
setup函数的两种返回值:若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)若返回一个渲染函数:则可以自定义渲染内容。(了解)
注意点:尽量不要与Vue2.x配置混用。Vue2.x配置(data、methos、computed…)中可以访问到setup中的属性、方法。但在setup中不能访问到Vue2.x配置(data、methos、computed…)。如果有重名, setup优先。setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
3.Vu3中ref函数的作用:
作用: 定义一个响应式的数据。语法: const xxx = ref(initValue)
。创建一个包含响应式数据的引用对象(reference对象,简称ref对象)。JS中操作数据: xxx.value
。模板中读取数据: 不需要.value,直接:<div>{{xxx}}</div>
备注:接收的数据可以是:基本类型、也可以是对象类型。基本类型的数据:响应式依然是靠Object.defineProperty()
的get
与set
完成的。对象类型的数据:内部 “ 求助 ” 了Vue3.0中的一个新函数—— reactive
函数。
4.Vue3中的reactive函数:
作用: 定义一个对象类型的响应式数据(基本类型不要用它,要用ref
函数)语法:const 代理对象= reactive(源对象)
接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)
reactive定义的响应式数据是“深层次的”。内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
5.reactive与ref的比较:
从定义数据角度对比:ref用来定义:基本类型数据。reactive用来定义:对象(或数组)类型数据。
备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过reactive
转为代理对象。
从原理角度对比:ref通过Object.defineProperty()
的get
与set
来实现响应式(数据劫持)。reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
从使用角度对比:ref定义的数据:操作数据需要.value
,读取数据时模板中直接读取不需要.value
。reactive定义的数据:操作数据与读取数据:均不需要.value
。
6.setup的两个注意点:
setup执行的时机,在beforeCreate之前执行一次,this是undefined。
setup的参数:props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。context:上下文对象。attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于 this.$attrs
。slots: 收到的插槽内容, 相当于 this.$slots
。emit: 分发自定义事件的函数, 相当于 this.$emit
。
二、Vite构建工具如何使用?
1.Vite官网
2.为什么使用Vite:
一句话总结:使用vite构建项目,启动的速度要比使用webpack构建更快。
之前浏览器是不支持ES Modules的,为了在让浏览器能够运行我们写的代码(es6语法、.jsx/.vue文件),我们需要使用打包工具,例如webpack,来实现代码的转换和优化的过程;
在浏览器支持ES Modules后,import、export、<script type='modules'>
等成为vite出现的条件;
vite 主要对应的场景是开发模式,它只启动一台静态页面的服务器,对文件代码不打包,服务器会根据客户端的请求加载不同的模块处理;
底层实现上,Vite 是基于 esbuild 预构建依赖的。当声明一个script标签类型为module时,浏览器将对其内部的import引用发起HTTP请求获取模块内容;
Vite 劫持了这些请求,并在后端进行相应的处理(如处理.ts文件为.js文件),然后再返回给浏览器;
由于浏览器只会对用到的模块发起 HTTP 请求,所以 Vite 没必要对项目里所有的文件先打包后返回,而是只编译浏览器发起 HTTP 请求的模块即可。也就实现了所谓的按需加载。
三、TypeScript的基础语法有哪些?
一般在做项目都会使用到TypeScript,因为其有更加严格的类型限制,易于后期维护,同时也很适合于Vue3一同开发。具体基础知识可以看我之前发布的文章:就是这里
四、如何使用Vue-router进行路由的配置?
在项目中比较常见的场景为:通过点击左侧的导航栏来实现切换不同的页面。主要是在一级路由中配置多个二级的路由来进行实现。以本次项目中的医院页面为例:其具体的文件存放位置如下图:一级路由一般存放在src文件夹下建立的pages文件夹下,一级路由命名为hospital,其下的二级路由为close,detail,notice等。
首先在各自的二级路由中编写各自页面的内容,以search页面为例子:
<template>
<div>
<div class="info">
<h1 >{{ hospitalStore.hospitalInfo.hospital?.hosname }}停诊信息</h1>
<el-empty description="暂无信息"/>
</div>
</div>
</template>
<script setup lang="ts">
import useDetailStore from '@/store/modules/hospitalDetail';
let hospitalStore = useDetailStore();
</script>
<style scoped lang="scss">
.info{
h1{
text-align: center;
font-size: 30px;
}
}
</style>
在一级路由组件的文件中,需要使用来存放其二级路由相应的内容,而左侧的导航栏,为每一个导航绑定一个点击事件,该事件能够实现路由的跳转,通过传入相应的路径参数来实现通过点击不同的内容而显示相应的内容。
<template>
<div class="hospital">
<!-- 左侧导航区域 -->
<div class="menu">
<div class="top">
<el-icon><HomeFilled/></el-icon>
<span>/ 医院信息</span>
</div>
<el-menu :default-active="$route.path" class="el-menu-vertical-demo">
<el-menu-item index="/hospital/register" @click="changeActive('/hospital/register')">
<el-icon><icon-menu /></el-icon>
<span>预约挂号</span>
</el-menu-item>
<el-menu-item index="/hospital/detail" @click="changeActive('/hospital/detail')">
<el-icon>
<document />
</el-icon>
<span>医院详情</span>
</el-menu-item>
<el-menu-item index="/hospital/notice" @click="changeActive('/hospital/notice')">
<el-icon>
<setting />
</el-icon>
<span>预约通知</span>
</el-menu-item>
<el-menu-item index="/hospital/close" @click="changeActive('/hospital/close')">
<el-icon>
<InfoFilled />
</el-icon>
<span>停诊信息</span>
</el-menu-item>
<el-menu-item index="/hospital/search" @click="changeActive('/hospital/search')">
<el-icon>
<Search />
</el-icon>
<span>查询/取消</span>
</el-menu-item>
</el-menu>
</div>
<!-- 右侧内容展示区域:路由组件展示位置 -->
<div class="content">
<router-view></router-view>
</div>
</div>
</template>
<script setup lang="ts">
import {onMounted} from 'vue';
import useDetailStore from '@/store/modules/hospitalDetail';
// 获取仓库对象
let detailStore =useDetailStore();
import {
Document,
Menu as IconMenu,
InfoFilled,
Setting,
Search,
HomeFilled,
} from '@element-plus/icons-vue';
import { useRouter,useRoute } from 'vue-router';
let $router=useRouter();
let $route=useRoute();
const changeActive=(path:string)=>{
$router.push({path,query:{hoscode:$route.query.hoscode}})
};
onMounted(()=>{
detailStore.getHospital($route.query.hoscode as string);
// 获取某一个医院科室的数据
detailStore.getDeparment($route.query.hoscode as string);
})
</script>
最后还需要创建一个router文件来专门对路由进行配置,以上展现的部分相应的路由配置如下:
import { createRouter, createWebHistory } from "vue-router";
export default createRouter({
// 路由模式
history: createWebHistory(),
routes: [
{
path: '/hospital',
component: () => import('@/pages/hospital/index.vue'),
children: [
{
path: 'register',
component: () => import('@/pages/hospital/register/index.vue')
},
{
path: 'detail',
component: () => import('@/pages/hospital/detail/index.vue')
},
{
path: 'notice',
component: () => import('@/pages/hospital/notice/index.vue')
},
{
path: 'close',
component: () => import('@/pages/hospital/close/index.vue')
},
{
path: 'search',
component: () => import('@/pages/hospital/search/index.vue')
},
{
path: 'register_step1',
component: () => import('@/pages/hospital/register/register_step1.vue')
},
{
path: 'register_step2',
component: () => import('@/pages/hospital/register/register_step2.vue')
}
]
]
})
五、如何使用Pinia来进行数据状态的管理?
Pinia与Vuex的作用一样,它们都充当存储数据的作用,存储在Pinia中数据允许我们在各个组件中使用。
npm install pinia
下载之后在main.js中进行引入:
import { createPinia } from "pinia";
const pinia = createPinia();
app.use(pinia);
然后再src下创建一个store文件夹用于存放各种store。以用户为例,在store文件夹下创建一个user.ts文件。
import {defineStore} from "pinia";
import {reqCode,reqUserLogin} from '@/api/hospital';
import type {LoginData,UserLoginResponseData,UserInfo} from '@/api/hospital/type'
import type { UserState } from "./interface";
import {GET_TOKEN} from '../../utils/user'
const useUserStore =defineStore('User',{
state:():UserState=>{
return{
visiable:false,
code:'',
userInfo:JSON.parse(localStorage.getItem('USERINFO') as string)||{}
}
},
actions:{
// 获取验证码的方法
async getCode(phone:string){
let result:any= await reqCode(phone);
if(result.code==200){
this.code=result.data;
return 'ok';
}else{
return Promise.reject(new Error(result.message))
}
},
//用户手机号码登录方法
async userLogin(loginData:LoginData){
let result:UserLoginResponseData= await reqUserLogin(loginData);
if(result.code==200){
this.userInfo=result.data;
//本地存储持久化存储用户信息
localStorage.setItem('USERINFO',JSON.stringify(this.userInfo));
return 'ok';
}else{
return Promise.reject(new Error(result.message))
}
},
//退出登录方法
logout(){
//清空仓库的数据
this.userInfo={name:'',token:''};
//清空本地存储的数据
localStorage.removeItem('USERINFO');
},
queryState(){
let timer=setInterval(()=>{
if(GET_TOKEN()){
this.visiable=false;
this.userInfo=JSON.parse(GET_TOKEN() as string);
clearInterval(timer);
}
},1000);
}
},
getters:{
}
});
export default useUserStore;
由于Pinia不是持久化的存储,因此为了让数据在页面刷新之后不会出现数据丢失的问题,使用了localStorage来对仓库中的数据进行持久化的存储。
在其他的组件中使用时,只需要使用以下的方式进行读取即可。
<template>
<p class="login" @click="login" v-if="!userStore.userInfo.name">登录/注册</p>
</template>
<script setup lang="ts">
import useUserStore from '@/store/modules/user';
let userStore = useUserStore();
</script>
state和getters属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。那么,如果我们有业务代码的话,最好就是写在actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法。
六、element-plus组件库该如何使用?
相应官网:一个 Vue 3 UI 框架 | Element Plus (element-plus.org)
首先需要先导入其对应的包在main.js中导入如下内容:(Element Plus 组件 默认 使用英语,如果你希望使用中文,可以导入国际化文件。)
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 国际化文件
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
app.use(ElementPlus,{
locale: zhCn,
});
然后只需要搜素你所需要的内容,然后根据文档的提示将其引入到你自己的组件中即可。例如想要实现如下的搜素效果:
首先在官网中搜索search输入框,之后会出现各种各样的输入框,选择具有远程搜索功能的输入框,点击相应的代码查看,将相应的代码如下到自己的组件中,通过查看官方提供的代码以及API功能介绍进行补充以及修改:
<el-autocomplete
v-model="state"
:fetch-suggestions="querySearchAsync"
placeholder="Please input"
@select="handleSelect"
/>
v-model属性为选中项绑定值,fetch-suggestions获取输入建议的方法, 仅当你的输入建议数据 resolve 时,通过调用 callback(data:[])
来返回它。也就是在这个方法中当输入框输入完数据之后就回调用它,此时就需要在这个方法中获取到输入框中需要的数据,在通过传给回调函数,让其展现在相应的搜索框下方。该方法具有两个参数:(queryString: string, cb: (arg: any) => void),第一个为数据的内容,第二个为回调函数。
const loadAll = () => {
return [
{ value: 'vue', link: 'https://github.com/vuejs/vue' },
{ value: 'element', link: 'https://github.com/ElemeFE/element' },
]
}
通过官网例子提供的代码我们知道在若需要在搜索框出现,则需要参数名为value,而我们通过发送请求返回来的数据参数名并不是value,因此后续我们还需要对数据进行处理之后才能够给回调函数。 @select="handleSelect"为选择其中的某一项时会触发的事件。
因此我们结合自己的项目编写的代码如下:
<template>
<div class="search">
<el-autocomplete @select="goDetail" :trigger-on-focus="false" clearable placeholder="请输入医院名称" v-model="hosname"
:fetch-suggestions="fetchData" />
<el-button type="primary" size="default" :icon="Search">搜索</el-button>
</div>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue';
import { useRouter } from 'vue-router';
import { ref } from 'vue';
import { reqHospitalInfo } from '@/api/home';
import type { HospitalInfo } from '@/api/home/type';
// 创建路由对象
let $router = useRouter();
// 收集搜素的关键字
let hosname = ref<string>('');
// 顶部组件的回调
const fetchData = async (keyword: string, cb: any) => {
let result: HospitalInfo = await reqHospitalInfo(keyword);
let showData = result.data.map(item => {
return {
value: item.hosname,
hoscode: item.hoscode
}
})
cb(showData);
}
// 点击某一个推荐项
const goDetail = (item: any) => {
$router.push({ path: '/hospital/register' ,query:{hoscode:item.hoscode}})
}
</script>
因此其他的样式以及布局也可以通过在其中寻找相应的组件来完成,这样可以减少很多的开发时间。
七、如何对axios进行二次封装来实现向服务器请求数据?
对原生的axios进行二次封装
//对于axios函数库进行二次封装?
//你工作的时候是否axios进行二次封装?二次封装的目的是什么那?
//目的:1,利用axios请求、响应拦截器功能
//目的2:请求拦截器,一般可以在请求头中携带公共的参数:token
//目的3:响应拦截器,可以简化服务器返回的数据,处理http网络错误
import axios from 'axios';
//引入用户相关的仓库
import useUserStore from '@/store/modules/user';
//@ts-ignore
import { ElMessage } from 'element-plus';
//利用axios.create方法创建一个axios实例:可以设置基础路径、超时的时间的设置
const request = axios.create({
baseURL: '/api',//请求的基础路径的设置
timeout: 5000//超时的时间的设置,超出五秒请求就是失败的
});
//请求拦截器
request.interceptors.request.use((config) => {
//获取用户仓库
let userStore = useUserStore();
//token:公共参数,如果用户登录了需要携带
if (userStore.userInfo.token) {
config.headers.token = userStore.userInfo.token;
}
//config:请求拦截器回调注入的对象(配置对象),配置对象的身上最终要的一件事情headers属性
//可以通过请求头携带公共参数-token
return config;
});
//响应拦截器
request.interceptors.response.use((response) => {
//响应拦截器成功的回调,一般会进行简化数据
return response.data;
}, (error) => {
//处理http网络错误
let status = error.response.status;
switch (status) {
case 404:
//错误提示信息
ElMessage({
type: 'error',
message: '请求失败路径出现问题'
})
break;
case 500 | 501 | 502 | 503 | 504 | 505:
ElMessage({
type: 'error',
message: '服务器挂了'
})
break;
case 401:
ElMessage({
type: 'error',
message: '参数有误'
})
break;
}
return Promise.reject(new Error(error.message))
});
//务必对外暴露axios
export default request;
对请求封装成相应的函数
import request from "@/utils/request";
import { get } from "http";
import type{HospitalResponseData,HospitalLevelAndRegionResponseData,HospitalInfo} from './type'
enum API{
// 获取已有医院数据接口
HOSPITAL_URL='/hosp/hospital/',
// 获取医院等级与地区接口
HOSPITALLEVELANDREGION_URL='/cmn/dict/findByDictCode/',
// 根据关键词搜素医院
HOSPITALINFO_URL='/hosp/hospital/findByHosname/'
}
export const reqHospital = (page:number,limit:number,hostype='',districtCode='')=>request.get<any,HospitalResponseData>(API.HOSPITAL_URL+`${page}/${limit}?hostype=${hostype}&districtCode=${districtCode}`)
export const reqHospitalLevelAndRegion=(dictCode:string)=>request.get<any,HospitalLevelAndRegionResponseData>(API.HOSPITALLEVELANDREGION_URL+dictCode);
export const reqHospitalInfo=(hosname:string)=>request.get<any,HospitalInfo>(API.HOSPITALINFO_URL+hosname)
在对应的地方进行调用(可以在onMounted函数中编写方法,若需要多次的调用建议先封装成一个函数,在onMounted中进行调用)
为何使用async以及await与axios进行结合,因为axios异步请求会出现请求的数据来不及渲染到页面的情况,因此使用async以及await来处理异步请求。
onMounted(() => {
getLevel();
});
const getLevel = async () => {
let result: HospitalLevelAndRegionResponseData = await reqHospitalLevelAndRegion('HosType')
if (result.code == 200) {
levelArr.value = result.data;
}
};
好啦!本次文章就分享到这里了,希望对你有一定的帮助!