背景:我做了一个后台管理项目,要写在简历上,那么
- 有哪些功能可以写
- 如何向面试官说清楚这些功能
涉及到的功能点
大功能
- 登录登出,登录鉴权方案设计与实现
- 国际化方案设计与实现
- 用户角色的权限访问控制方案设计与实现
小功能
- 利用githooks实现代码提交规范检查
- 动态面包屑实现
动态更换主题颜色功能(暂没搞定)- 全屏功能实现
- 利用wangEditor插件实现富文本编辑组件
- 引导页功能实现
问:一些小功能,没什么大难度的功能点,可不可以写到简历上?
答:如果你的亮点功能不够多,可以写上去,理由:
1)至少能体现你做过什么。
2)这些功能虽然不是啥难点,但面试官如果他自己刚好做过,那他可能会有兴趣问一下
总体回答思路
回答的思路是:
- 先描述这个功能的具体需求,需求都说不清楚是大忌,用简洁有条理的话描述一遍
- 然后才是描述如何实现的,按照实现步骤,分一二三点说清思路,想到什么说什么是大忌
很多功能其实项目原本已经有了,所以面试回答为什么你做了这些功能,理由可以是:
- 公司新开了一个项目,需要新做这个功能
- 已有功能但是交接给你维护,你做了维护和迭代,所以你懂
具体问题回答(针对面试)
一:问:说一下你的登录登出功能怎么做的
需求
- 登录
- 登录鉴权,同一个浏览器不需要重复登录
- 登出,主动登出和被动登出
回答提纲思路
登录
- 有一个登录界面,密码一般不是明文,是md5加密
- 发送登录接口,返回token,存到vuex和localStorage
- 跳转页面到首页,通常会发送获得用户信息的接口
- 往后每次发送接口,都会把token带给后端,写在请求头里面
登录鉴权(页面关闭后再次打开)
- 从localStorage拿token,判断这个页面是否登录过,拿不到token说明没登录过,直接重定向到login页面,这个判断一般写在全局路由守卫里面
登出
- 登出分为主动登出和被动登出(token过期或者单点登录被挤下)
- 主动登出发送登出接口就行,被动登出,通常后台的接口会返回token过期的状态码,然后前端要做登出的相关逻辑,这个逻辑一般写在发送接口的拦截器里
- 登出需要做的事情有:a)清空全局store和localStorage里的token。b)清空用户信息,包括权限路由等,这些信息一般都保存在全局store里面。c)跳转到登录页
面试完整回答(口语化)
- 主要需求有登录,登出以及登录鉴权功能,登出可能是主动登出和被动登出,被动登出就是token过期,或者单点登录账号被挤下,以及登录鉴权,就是同一个设备上,页面再次打开不需要重复登录。
- 具体实现:首先我们有一个登录页面,输入账号密码,密码一般使用md5加密的,不会明文。
- 然后发送登录接口,后台返回登录成功的状态码,同时把token返回给前端,这个token,第一要存到localStorage里面,第二要存到vuex里面,存到localStorage是为了关闭页面后再次打开不需要重复登录,如果项目没有这个需求,则可以不存
- 然后这个登录成功以后,要做两件事,第一就是发送接口获取当前账号的基本信息,比如名称啊工号啊还有当前账号有哪些权限啊,第二就是跳转到首页。
- 到此,我们的登录已经做完了,接着我们要来做第二个功能,就是在token没有过期之前,不用重复登录的功能,这个功能怎么做呢,首先我们在进入页面之前需要判断当前这个账号登录过没有,这个判断应该写在公用的路由守卫里面,如果登录了,则放行进入页面,如果没有登录,则统一重定向到登录页。怎么判断有没有登录过呢,就看我们的localStorage里面有没有token,有就是登录过了,没有就是没登录过。
- 接着我们要实现登出功能,登出分为主动登出和被动登出。主动登出就是发个接口退出,被动登出通常是指token过期了,或者单点登录账号被挤掉了,此时发送任意接口,后台都会给我报一个未登录的状态码,我们通过这个判断这个状态码来做登出操作,那么这个判断,因为是公用的,通常可以写在发送接口的拦截器里面。
- 那么,我们登出需要做的事情有:a)删除localSorage里面的token。b)清除的当前账号的公共信息userInfo,比如当前账号的的姓名工号,以及权限路由等等,这些通常是存到vuex里面,要清空掉。第三,就是跳转到登录页面。
- 到此我们整个登录登出的功能已经做完了
关键代码实现
登录
登录这个接口通常写在vuex里面,在login页面调用
const loading = ref(false)
const store = userStore()
const hangleLogin = () => {
loading.value = true
store.dispatch('login', loginForm.value).then(() => {
loading.value = false
}).catch(() => {
loading.value = false
})
}
把获得到的token,要同时存到lcoalStorage和vuex
// vuex文件
export default createStore({
state: {
// 每次打开页面之前,都先从localStorage里面先尝试拿token,这一步直接写在vuex里面
token: getItem('token') || '',
},
mutations: {
// 存到vue和存到localStorage
setToken(state, token) {
state.token = token
setItem('token', token)
}
},
actions: {
lgoin(context, userInfo) {
const { username, password } = userInfo
return new Promise((res, rej) => {
login({
username,
password: md5(password)
}).then((data) => {
res(data)
// 存到vue和存到localStorage
this.commit('setToken', data.token)
}).catch((err) => {
rej(err)
})
})
}
},
})
每次发送接口,都带上token,通常写在ajax的拦截器里面
// 请求拦截器
service.interceptors.request.use(
config => {
// 在这个位置需要统一的去注入token
if (store.getters.token) {
// 如果token存在 注入token
config.headers.Authorization = `Bearer ${store.getters.token}`
}
return config // 必须返回配置
},
error => {
return Promise.reject(error)
}
)
登录鉴权,同一个设备不需要重复登录,一般都写在全局路由钩子里面beforeEach,如果登录了,则不允许进入login页面,如果没有登录,则重定向到login页面
router.beforeEach(async (to, from, next) => {
if (store.getters.token) {
if (to.path === '/login') {
next('/')
} else {
if (!store.getters.hasUserInfo) {
// 获取用户信息接口
await store.dispatch('getUserInfo')
}
}
} else {
// 没有token的情况下,可以进入白名单
if (whiteList.indexOf(to.path) > -1) {
next()
} else {
next('/login')
}
}
})
登出
清除用户信息,清除token,跳转到登录页面
// vuex 的actions
logout() {
this.commit('setUserInfo', {})
this.commit('setToken', '')
removeAllItem()
router.push('/login')
}
在接口里面判断是否token超时或者被挤下,通常写在拦截器的返回里面
// 响应拦截器
service.interceptors.response.use(
response => {
// todo...
},
error => {
// 处理 token 超时问题
if (
error.response &&
error.response.data &&
error.response.data.code === 401
) {
// token超时
store.dispatch('user/logout')
}
ElMessage.error(error.message) // 提示错误信息
return Promise.reject(error)
}
)
二:问:说一下你们国际化怎么做的
需求
- 第一,我们有一个地方,有一个按钮,可以切换语言,切换完以后立即生效
- 我们希望页面关闭以后再次打开,仍然是刚刚切换完以后得语言,也就是同一个设备上能记得住
回答提纲思路
- 建立一个language文件夹,放入不同语言的字典
- 建立一个控制语言的变量lang,这个变量要同时存在store里面和localStorage里面
- 页面初始化时候从store拿lang这个变量,确认显示语言
- 切换语言的时候,要更新lang这个变量到localStorage和store
- 后台返回报错提示用什么语言,可以在接口的请求头里面告诉后台
- 选第三方插件要注意要选支持国际化的
面试完整回答(口语化)
原理
他其实就是每一种语言都有一个字典/语言包,然后有一个变量来控制当前应该访问哪个字典,就是访问哪一种语言,然后同一个一个函数去访问。
国际化这个功能并不复杂,我们自己写都能写的出来,但是在实际开发中,通常我们会用到一个叫i18n的插件去做国际化这件事。
实现步骤
- 通常我们会建立一个叫lang的文件夹,这里面存放不同语言的字典,通常是英文一个文件,中文一个文件。如果项目比较大,还可以是按照业务模块,每个模块一个文件夹,然后里面是中英文字典。
- 我们需要一个变量,来控制当前浏览器上显示什么语言,这个变量我们可以把它命名为language,然后这个变量应该同时存到localStorage里面和vuex里面。存到localStorage里面是为了在页面关闭以后再次打开,依然能显示已经设置语言,存到vuex里面,为什么方便获取。
- 我们在页面初始化的时候,会先从localStorage里面查找控制语言的变量,来判断显示哪种语言,如果没查到,说明是第一次登录嘛,那么就给一个默认值,通常设置为中文,这个值要同时写入到localStorage里面和vuex里面。这样下一次页面打开,就能查到了。
- 然后我们会有一个地方是切换语言的按钮,那么这个按钮切换语言的时候,要注意要同时更新存在localStorage里面的语言变量和vuex里面的语言变量,两个都要更新。
- 最后一个关键点,我们前端的语言控制做完了,然后有时候,后台的接口会报一些提示,这个提示也要根据不同的语言来做切换,那么这个怎么做,通常的做法是,我们可以在请求头里面,传一个字段,用来告诉后台应该返回什么语言,这样每次发送接口,后来都有办法知道,应该返回什么语言的报错提示。
- 项目既然做了国际化,那么选择第三方插件,比如富文本编辑器的时候,也要考虑国际化功能
代码演示
太简单了,不写了
三:问:说一下你们用户角色的权限控制怎么做的
这个权限控制技术上其实并没有什么难度,核心就在于你能不能把业务说清楚。复杂在业务,不在技术。
需求核心
核心需求就两句话:
- 每一个账号都有一个或者多个角色(比如管理员,普通员工),每一个角色都拥有不同的权限(比如访问不同的页面,操作不同的按钮)
- 我们需要针对不同权限的账号显示出不同的页面功能。
对于后台管理系统,我们需要两个页面:
- 查询所有账号,并且给账号分配角色的表格页面。
- 查询所有角色,并且给每个角色分配不同权限的页面(通常是路由和按钮)。
对于权限的控制在页面的上的具体体现,主要有两个:
- 不同的账号能访问不同的路由页面,我们要对当前的账号做路由控制,只允许访问有权限的路由页面。
- 不同的账号能操作不同的按钮,我们要对当前的账号只显示出有权限操作的按钮,没权限的操作的按钮不显示。
回答提纲思路
- 针对后台管理,需要有分配角色页面以及给角色分配权限的页面
- 建立私有路由表和公有路由表
- 在路由钩子
beforeEach里面,发送接口获取用户信息,后端会返回当前用户可以访问哪些路由页面,我们根据后端的返回数据做过滤,最后通过addRoute Api动态添加到路由表中。 - 退出登录记得重置路由表
- 针对按钮权限,后端会返回当前账号可以访问哪些按钮,前端写一个
v-permission指定,指定传一个标识参数,指定根据这个参数,去查找当前用户操作这个按钮。
具体实现步骤(口语化)
- 首先对于后台管理系统,我们要有给账号分配角色以及给角色分配权限的两个页面,这两个知识增删改查的页面,没什么好说的。
- 对于路由的权限控制和按钮的权限控制如何做,
- 我们先来看路由,通常会有两种方案,一种方案是,在页面初始化请求用户信息的时候,后台把这个用户能访问哪些路由页面,全部告诉前端了,后台返回了一个数组,数组里面有所有的路由;另一种方案是,后台只告诉前端这个用户有什么角色,然后要前端自己去过滤路由,过滤出有权限查看的路由。
- 这两个方案具体怎么做,不管选用哪一种方案,我们首先都要建立一个私有路由表和公有有路由表。私有路由表,就是根据不同权限的角色,生成不同的路由表。
- 然后,在进入页面的时候,我们在路由钩子里面
beforeEach这个钩子里面,会去发送接口获取用户信息数据,此时就能拿到和用户权限相关的信息。 - 如果是后端已经返回了完整的需要显示哪些路由,那么我们把这些路由动态添加的我们的路由表中,通过
addRoute这个Api就能动态添加路由。当然后端通常返回的路由数据结构是不能直接用的,我们需要做二次组装。 - 如果后端只返回了当前用户是什么角色,没有返回应该显示哪些内容,那么判断用户可以访问哪些路由就要前端来做,我们就是要根据当前账号的角色去过滤出可以访问的路由。这种方案的话,通常,我们在配置私有路由表的时候,会在每个路由对象的mate字段里面,写一个角色的字段,这个字段记录了当前路由可以被哪些角色访问,然后等后端返回了角色信息的时候,就可以根据后端返回的角色信息和私有路由表里面配置的角色信息做一个匹配,做这么一个过滤的过程,最终得到结果以后再动态添加的路由表中。
- 路由权限配置就是这个么过程,最后我们在退出登录的时候,记得要删这个私有路由表(
removeRoute),完结。(注意用户信息等不能存到localStorage里面,因为随时会改变,必须保证刷新页面就要重新获取) - 接下去我们要实现按钮的权限,这个比较简单,通常是写一个指定来完成。通常后端会返回当前账号可以操作哪些按钮,就是返回一个数组。然后,我们定义一个v-permission这个指定,这个指定接收一个参数,这个参数就是表明当前按钮是一个什么功能,然后这个指定在执行的时候,会去查,后端返回的按钮权限列表里面,包不包含当前这个按钮,如果包含,按钮显示,如果不包含,删掉当前按钮。
- 完结
核心代码
按钮指定部分
import store from '@/store'
function checkPermission(el, binding) {
// 获取绑定的值,此处为权限
const { value } = binding
// 获取所有的功能指令
const points = store.getters.userInfo.permission.points
// 当传入的指令集为数组时
if (value && value instanceof Array) {
// 匹配对应的指令
const hasPermission = points.some(point => {
return value.includes(point)
})
// 如果无法匹配,则表示当前用户无该指令,那么删除对应的功能按钮
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
// eslint-disabled-next-line
throw new Error('v-permission value is ["admin","editor"]')
}
}
export default {
// 在绑定元素的父组件被挂载后调用
mounted(el, binding) {
checkPermission(el, binding)
},
// 在包含组件的 VNode 及其子组件的 VNode 更新后调用
update(el, binding) {
checkPermission(el, binding)
}
}
// 注册指定
import permission from './permission'
export default (app) => {
...
app.directive('permission', permission)
}
四:以下小功能实现
利用githooks实现代码提交规范检查
需求描述:
一般项目都已经做了这样的规范约束了,但是也有一些缺乏维护的项目可能没有去做这个功能。
最主要的就两个:
- 我们希望在提交commit之前,自动检验代码哪里有格式不规范的地方,如果有格式不规范的地方的,则不让commit提交,并且只要求对有修改的文件做检验,没修改过的,不校验。
- 除了对代码的格式检验以外,我们希望对commit信息本身也做格式约定,如果不符合规范的,依然不让提交。
实现:
- 首先代码格式规范,我们在eslint里面配置。
- git commit的信息的提交规范,也可以配置(在commitlint.config.js文件)
- 主要就是在commit的提交之前要做校验,那就是要用到git的钩子函数(
git在执行某个事件之前或之后进行一些其他额外的操作),我主要是用到pre-commit这个钩子函数,这个钩子函数会在git commit执行前执行 - 具体做法的话要用到两个插件husky 和 commitlint。commitlint:用于检查提交信息。husky:是
git hooks工具。根据文档去配置这两个工具就行。略。。。
说说你的全屏功能怎么做的
全屏功能本来就有原生api去支持,可以针对整个文档全屏或者是针对某一个元素全屏。
Document.exitFullscreen():该方法用于请求从全屏模式切换到窗口模式Element.requestFullscreen():该方法用于请求浏览器(user agent)将特定元素(甚至延伸到它的后代元素)置为全屏模式 用原生api的问题是,如果针对某一个元素进行全屏,那么空白的地方,可能会出现黑边,因为空白的地方没有任何元素。所以我们一般不会直接使用原生的全屏api,而是用一个库screenfull
动态面包屑实现
动态面包屑是指,我们的面包屑会随着当前路由的变化自动改变,而不是在每个页面都写死对应的面包屑。写死的面包屑的缺点就是不好扩展和维护。
面包屑这个UI组件其实各大ui框架都有,直接用就行。实现动态面包屑的关键点就是让数据变成动态的,跟着路由变化的。
- 核心是要监听路由的变化,每一次路由变化的时候,获取当前路由的从上到下的所有路由对象。重新渲染面包屑数据,就可以了。
- 所以我们可以写一个共用的面包屑组件,在这个组件里面,通过watch,监听路由变化,当路由变化后,重新获取当前路由从上到下的所有路由对象,然后重新渲染就可以了,核心api是
route.matched,这个api就是获取当前路由从上到下的所有路由对象的,返回的是一个数组。
核心代码:
监听路由变化,变化时,获取当前路由的所有上下级路由对象
const route = useRoute()
// 生成数组数据
const breadcrumbData = ref([])
const getBreadcrumbData = () => {
breadcrumbData.value = route.matched.filter((item) => {
return item.meta && item.meta.title
})
}
// 监听路由变化时触发
watch(
route,
() => {
getBreadcrumbData()
},
{
immediate: true
}
)
利用wangEditor插件实现富文本组件
富文本库较多,功能大同小异
注意点:
选择富文本要选择文档详细的,另外要符合自己项目要求的,比如项目有国际化的需求,那么选择富文本库就要有这个功能的
。
富文本最终生成的内容都是html格式的内容。
引导页功能实现
需求描述:
引导页,就是高亮页面当中的某一处区域,然后用户点击下一步高亮下一个区域。市面上有很多轮子,我是用driver.js的的插件实现的。通常引导页,是引导新用户去查看我们的页面有什么功能,一般不会出现第二次,所以一般会有接口告诉前端这个用户是新用户老用户。
本身用这个插件并没有什么困难,就是给需要高亮的元素写一个id,然后去做配置就可以了。但是在实际开发过程中也遇到了一点问题:
- 一个是:我们需要高亮一个表格,告诉用户这个表格是干什么用的,但是这个用户是新用户,这个表格还是空的没有数据,导致用户看不出来这个表格是干什么的,就是实际上页面的内容是不可控的,不一定有我们要的东西。
- 第二个是:我们引导了用户看A页面,然后想跳到B页面继续引导,这个时候怎么办,我们引导页没有这个功能,甚至这个用户还是个新用户,不一定有真的有权限看B页面。
针对这两个问题,就是页面内容不可控的问题,其实要看每个项目的复杂度具体分析,我当时的解决方案是,为引导页功能专门开发了一个页面,在给用户做引导的时候,实际上是用这个假页面做展示,等引导完了再跳到真实的页面。