分享我遇到的一些面试题
1.vue中如何封装一个可复用的input组件?
首先构建一个基础的输入框组件实现双向数据绑定和事件监听功能,这里使用vue3语法
<!-- -->
<template>
<div class="base-input">
<label v-if="label">{{ label }}</label>
<div class="input-container">
<input
:type="type"
:value="modelValue"
@input="$emit('update:modelValue',$event.target.value)"
@blur="$emit('blur',$event)"
@focus="$emit('focus',$event)"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:class="{'has-error':hasError}"
>
</div>
<p v-if="hasError" class="error-message">{{ errorMsg }}</p>
</div>
</template>
<script setup lang="ts">
import {computed ,toRefs} from 'vue'
const props=defineProps({
modelValue:{
type:[String,Number],
default:''
},
label:{
type:String,
default:''
},
type:{
type:String,
default:'text',
validator: (value:any) => ['text', 'password', 'number', 'email', 'tel'].includes(value)
},
placeholder:{
type:String,
default:''
},
disabled:{
type:Boolean,
default:false
},
readonly:{
type:Boolean,
default:false
},
error:{
type:Boolean,
default:false
},
errorMsg:{
type:String,
default:''
}
})
const $emits=defineEmits(['update:modelValue','blur','focus'])
const {error,errorMsg}=toRefs(props)//转成响应式数据
const hasError=computed(()=>error.value&&errorMsg.value)//计算属性逻辑或判断是否有error或者errorMsg之中的任一个
</script>
<style scoped>
.base-input {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.input-container {
position: relative;
display: flex;
align-items: center;
}
.input-container > input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-container > input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
}
.has-error input {
border-color: #ef4444;
}
.error-message {
color: #ef4444;
font-size: 0.875rem;
margin-top: 0.25rem;
}
</style>
面试中问到类似封装input组件其实就是问如何实现双向数据绑定以及对v-model的理解
关于v-model,v-model是v-bind (:)与v-on (@)的语法糖
<input v-model="modelValue">
等价于
<input :value="modelValue" @input="modelValue=$event.target.value">
封装input组件时,用到组件通信中props传参以及$emit触发事件传参,前者defineProps一般用于父传子,其传参是单向数据流,子中无法修改,因此我们可以说v-model是单向数据流,defineEmits一般用于子传父,绑定事件触发时传参。
叙述这个问题的时候要说出来封装input时要用defineProps接收传来的参数并且用defineEmits绑定事件触发时更新内容完成输入框双向数据绑定。
到这一步有可能会追问双向数据绑定的原理,首先我们要知道什么是双向数据绑定,其实就是数据的变化能自动反映到UI(视图层),同时 UI 的变化也能自动更新数据
vue中双向数据绑定的原理:响应式系统(数据劫持) + 发布--订阅者模式
- Vue 2.x:使用Object
.defineProperty()
拦截对象属性的getter/setter
。 - Vue 3.x:使用
Proxy
对象代理整个对象,提供更强大的拦截能力。
通过Object.defineProperty()
(Vue 2)或 Proxy
(Vue 3)对数据对象的属性进行劫持,拦截属性的读取(get)和修改(set)操作,当数据发生变化时,watcher类(订阅者)监听到变化并调用其update()更新函数,实现数据变化与视图更新是同步的。这里就不分享源码案例了,因为我也不太会。。。
扩展:当数据变化时,vue不会直接操作DOM,而是会用到虚拟DOM(用js对象形式表示的dom结构),通过比较新旧虚拟DOM的差异计算出来是哪块发生变化,之后批量更新需要变化的真实DOM节点,也就是虚拟 DOM diff 和 DOM 更新(计算出需要变化的dom结构进行patch更新而不改变其他部分),这样能确保高效更新。
另外提一嘴,数据发生变化时,原生js程序能迅速发现是哪块DOM结构发生了变化,因此框架是要慢于原生js程序(虚拟dom,其存在的意义也是缓解基于框架设计模式下处理数据效率低下的问题)
引入使用查看效果,这里触发的监听事件是校验输入内容与手机号是否正确
<!-- -->
<template>
<div class="container">
<h2>使用封装好的组件</h2>
<br><hr><br>
<BaseInput
v-model="username"
placeholder="请输入用户名"
label="用户名"
/>
<p>输入框内容为:{{ username }}</p>
<br><hr><br>
<BaseInput
v-model="inputcontent"
label="校验输入内容不能为空"
:error="testerror"
:errorMsg="testerrorMsg"
@blur="validateInput"
/>
<br>
<BaseInput
v-model="phonenumber"
placeholder="请输入手机号"
label="校验手机号"
:error="phonenumbererror"
:errorMsg="phonenumbererrorMsg"
@blur="validatehonenumber"
/>
<br><hr><br>
</div>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import BaseInput from '../components/BaseInput.vue';
const username=ref('')
const inputcontent=ref('')
const testerror=ref(false)
const testerrorMsg=ref('')
const validateInput=()=>{
if(inputcontent.value===''){
testerror.value=true
testerrorMsg.value='输入内容不能为空'
}else{
testerror.value=false
testerrorMsg.value=''
}
}
const phonenumber=ref('')
const phonenumbererror=ref(false)
const phonenumbererrorMsg=ref('')
const validatehonenumber=()=>{
//校验手机号的正则表达式
const phonereg=/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;
if(!phonereg.test(phonenumber.value)){//此时未通过校验
phonenumbererror.value=true
phonenumbererrorMsg.value='手机号格式不正确'
}else{
phonenumbererror.value=false
phonenumbererrorMsg.value=''
}
}
</script>
<style scoped></style>
2.v-if与v-show的区别
v-if与v-show都用于控制元素的显示与隐藏,v-if是条件性渲染,当绑定内容为false时,元素会被完全移除(触发生命周期钩子中的卸载),不保留在DOM中,当绑定元素为true时,会重新挂载;v-show是条件性显示,元素始终存在于DOM中,当绑定条件为false时,相当于display:none隐藏了元素(仅是样式的隐藏,元素仍然存在)
根据特性可以总结v-if一般用于不会频繁切换的场景中而v-show与之相反且v-if可以绑定到<template>模板标签上来实现多个元素的条件渲染而v-show不可以,v-if可以节省内存,避免不必要的DOM元素存在,v-show仅是css样式的改变,性能开销很小,绑定内容又false变为true时速度相较于v-if会更快一点
3.项目中如何二次封装axios发送网络请求
直接举例,创建一个axiosrequest.ts文件
import axios from 'axios'
import { ElMessage } from 'element-plus'//为了错误提示更美观引入elementplus插件的elmessage
//1.创建一个axios实例
let axiosrequest =axios.create({
baseURL:'/api',
timeout:5000
})
//2.设置请求拦截器
axiosrequest.interceptors.request.use((config) => {
//config是请求的配置对象
//可以在这里设置请求头,在发请求时携带一些公共参数
return config//这个请求拦截器配置对象必须返回
})
//3.设置响应拦截器
axiosrequest.interceptors.response.use((res) => {
return res.data
},(err: any) => {//失败的回调
let fmessage = '';
let fstatus = err.response.status;
switch (fstatus) {
case 400:
fmessage = '请求参数错误'
break;
case 401:
fmessage = '未授权,请登录'
break;
case 403:
fmessage = '拒绝访问或权限不足'
break;
case 404:
fmessage = '请求资源不存在,请求地址错误'
break;
case 500:
fmessage = '服务器内部错误'
break;
case 503:
fmessage = '服务器正在维护'
break;
default:
fmessage = '未知错误即网络出现问题'
break;
}
ElMessage({
type: 'error',
message: fmessage
})
return Promise.reject(new Error(err.message))//返回一个失败的promise
})
export default axiosrequest//默认暴露这个实例
4.如何实现登录流程(具体一点)
1.api文件统一处理接口请求,在接口详情页引入封装好的网络请求工具,配合接口地址定义方法并导出
import axiosrequest from '@/utils/axiosrequest'
export const userLogin=(data:any)=>{//发送登录请求方法
return axiosrequest.post('/api/reqlogin',data)
}
2.之后在仓库中引入api文件中暴露出来的登陆方法
import {userLogin} from '@/api/user'
仓库中actions下定义异步方法来处理登录请求
actions:{
async userlogin(data: any){
let res:any=await userLogin(data)
if(res.status==200){
//登录成功,根据接口文档处理res
this.token=res.data.token as string//state中的token值赋值为返回过来的token
set_token(res.data.token as string)//本地存储返回过来的token
this.userinfo=res.data.userinfo//接收用户信息
return 'ok'
}else{
//登录失败,返回错误信息(失败的promise)
return Promise.reject(new Error(res.data.msg))
}
}
}
3.页面引入仓库对象发送登录请求
<template>
<el-input
placeholder="请输入账号"
type="username"
clearable
v-model="loginForm.username"
></el-input>
<el-input
placeholder="请输入密码"
type="password"
show-password
v-model="loginForm.password"
></el-input>
<el-button
type="primary"
size="default"
style="width: 100%"
@click="reqlogin"
>登录</el-button>
</template>
<script setup lang="ts">
import {ref,reactive} from 'vue'
import useUserLoginStore from "@/store/user.ts";
const userLoginStore = useUserLoginStore();
const loginForm = reactive({
username: "",
password: "",
});
const reqlogin=()=>{
userLoginStore.userlogin(loginForm);
}
</script>
5.如何实现登录过某个网页后下次再进入仍是登陆状态?
通过持久化存储用户身份凭证实现,这个问题其实就是问token与localstorage本地存储,token相当于用户身份证,登陆完成后会返回登陆成功的信息,信息内部包含有token的值(也就是常说的jwt),当登录之后下次再登录时前端在请求头中携带token,服务器验证token有效性。现定义一个工具文件处理token
export const set_token=(token:string)=>{//封装保存token的方法并且暴露出去
return localStorage.setItem('TOKEN',token)//
}
// token是用户身份的唯一标识
export const get_token=()=>{//封装获取token的方法并且暴露出去
return localStorage.getItem('TOKEN')
}
export const remove_token=()=>{//封装删除token的方法并且暴露出去
return localStorage.removeItem('TOKEN')
}
在仓库中存储token之后在封装好的axios请求工具的请求拦截器中配置
import useUserLoginStore from '@/store/user'
//2.设置请求拦截器
axiosrequest.interceptors.request.use((config)=>{
const userStore = useUserLoginStore()
if(userStore.token){//能保证token公共参数发请求之前携带给请求头用于判定用户是否登录
config.headers.token=userStore.token
}
return config//必须返回config请求拦截器配置对象
})
// 页面加载时验证登录状态 ,用仓库也可以实现,这里为了方便理解就直观一点
import {get_token} from '@/utils/token'
onMounted(()=>{
const token=get_token('token');
if (token) {
// 验证 Token 有效性(如调用用户信息接口)
checkUser(token)
} else {
router.push('/login');//token没有或者过期跳转登录页
}
})
6.同一个用户开两个用户中心页面,当一个页面退出登陆之后,另一个页面会有何变化?
就是问退出登陆之后要做的事情,一个页面退出登录,另一个页面状态不会立刻发生变化(保持原样),但是此时再点其他内容(如支付等)会跳转登陆页面,原因是退出登陆之后会调退出登陆方法,要清除token,此时再点击其他需要登录才能接触的操作时会跳转到登录页
以上问题都是口述,理解记忆大致流程能流畅叙述即可
持续更新。。