@项目简历
项目介绍
- 刀锋电影管理端是刀锋电影App与刀锋电影小程序的中后台管理端程序;
- 包含用户管理、数据看板、在映影片管理、预告管理、城市管理、影院管理、订单管理、运营管理、售后客服等模块;
- 运营人员通过该系统可以创建与维护系统用户、管理用户权限、发布影片与影院信息、创建运营活动、跟踪用户订单、提供售后服务等;
技术栈
- Vue全家桶: Vue3 + TypeScript + Vue-Router + Vuex + Element-Plus + Axios
- 自定义脚手架 sto + mock框架 mongo-expresser ;
个人负责模块
- 用户管理、数据看板、在映影片管理、预告管理;
项目亮点
- 脚手架模板搭建项目:使用自定义脚手架sto拉取github的Vue中后台项目模板vue-manger创建项目,实现Vue3+TS+vite+axios+vuex+elementplus+路由结构+常用hook与指令的自动化集成;
- 使用升级版的mock技术mongo-expresser作为前端的mock服务器,实现任意模块的CRUD接口全自动生成+静态资源服务+权限校验+上传服务;
- 项目形成自定义组件库super-ep,包含PageHeader页头组件 + EpTable通用表格渲染组件 + BtnGroup通用按钮组 + EpDialog确认弹窗组件;
- 逻辑复用:实时滚动位置获取钩子useScroll、网络通信钩子useAxios、按钮鉴权指令v-auth、元素互动指令v-change;
- 性能优化:全局状态缓存于Vuex + 每5分钟自动刷新数据 + 动态组件与KeepAlive缓存组件状态;
- 权限管理:后端接口管理 + 动态渲染登录用户的权限菜单 + 按钮权限管理;
- 数据可视化:使用Echarts框架实现实时票房排行、实时在线用户数、实时观影人数、城市票房排行等数据的动态可视化呈现;
@项目面试
说一说你最近的项目;
- 项目是干嘛用的:刀锋电影管理端是刀锋电影App与刀锋电影小程序的中后台管理端程序,C端用户通过小程序/App浏览电影、选择影院、订票选座位、领优惠券、发起客服咨询;运营人员通过该B端系统可以创建与维护系统用户、管理用户权限、发布影片与影院信息、创建运营活动、跟踪用户订单、提供售后服务等;
- 技术栈是什么: 主要用到Vue全家桶 + 自定义脚手架项目模板 + 内部mock解决方案mongo-expresser;
- 有哪些模块:项目模块包含用户管理、数据看板、在映影片管理、预告管理、城市管理、影院管理、订单管理、运营管理、售后客服等,我主要负责用户管理、数据看板、在映影片管理、预告管理这几个模块;
- 项目现状:目前1.0版本已经上线,运行稳定,2.0暂无计划;
开发周期怎样?团队有几个人?
- 202205~202211共计6个月;项目组前3后6,自己任项目组前端组长/核心开发人员;
讲一讲这个项目研发的全流程?
- 立项:产品经理产出
需求原型
(一套非常简陋的页面雏形+跳转关系,产品经理开发原型的工具:PC端Axure,手机端墨刀),美术产出页面效果图
; - 前后对接接口:前后端主程内部磋商,最后由服务端提供
接口文档
(doc),包含所有接口的:接口名 + 服务端路由地址 + 访问权限(角色数组) + 请求方法 + 请求参数(GET/DELETE的查询参数 + POST/PUT请求体与格式) + 返回数据格式(通常是JSON,通常有code + msg + data + err) - 前后分头开发:前端使用自建的mock数据和mock服务器;常见的mock解决方案:纯GET使用json-server,如果有POST/DELETE/PUT,最常用的是fast-mock,也可以使用自行构建的mock方案,比如mongo-expresser;
- 前端创建
工程+仓库
:脚手架拉取代码模板,模板为一套自动集成了[Vue3+TS+vite+axios+vuex+elementplus+常用路由表+常用hook+常用指令
]的工程,创建master+dev+test分支,后续在dev分支上合并特性分支,在test分支上做测试, 在master分支上合并test分支打包上线; - 专人开发MVC里面的M层,即axios的业务层;
- 典型的页面开发流程:静态页布局 + 拆分为组件 + AJAX通信获取数据 + 渲染页面 + 开发交互 + 封装整理形成可复用组件/hook/指令等;
- 合龙提交测试:开发人员不断开发+自测,一轮需求开发完毕后将各个特性分支合并到dev分支,然后用test分支合并dev分支,在test分支上打包,丢于测试服务器,通知测试人员测试;
- 递归修改BUG:测试人员将BUG提交于
禅道/Jira等BUG管理系统
(超级简单,无需担心),指派给前端组长,由前端组长转派给组员,组员修复后标记为已修改; - 打包上线:测试通过后(至少要解决
致命+严重+一般
三个级别的所有BUG,不严重
的可以马虎点,测试组长要在特定版本号上签字画押),然后将测试完毕的test分支合并到master分支,(可打tag),打包上传到生产服务器与用户见面; - 上线后维护:产品团队给出新一轮需求,同时生产环境下用户反馈了新的BUG,同时前后端的日志共同收集上来的BUG,共同促成版本升级的需要,重新执行接口-开发-测试-上线流程;
- 特别介绍自动集成
CI/CD
(ContinuousDeveloping持续开发 + ContinuousIntegration持续集成),一旦master分支有新的提交,GitHook会自动触发运维脚本git pull + npm build + ftp dist目录 生产服务器项目部署目录进行覆盖,用户不知不觉就被安排了,这一套体系CTO/运维负责人所搭建,与你无关,你只是一个被动的渣渣使用者;
为什么选择Vue3?Vue3相比Vue2你觉得好用在哪?
@底层响应式原理更新
- 使用Proxy替代Vue2的数据劫持侦听/Object.defineProperty;
- Proxy机制:数据的每个叶子节点背后都是一个代理对象,访问数据一律通过其代理,这样一旦数据被更新,代理会直接通知相应的视图更新,而无需递归比较整棵数据树,从而极大地提升了响应式性能;
- 数据劫持机制的缺点:
- 性能垃圾,因为需要递归比较新老数据的所有叶子节点;
- 只能监听数据的get与set操作,这样当数据地址不发生变化时(例如删除对象的一个key,原地修改数组)数据的修改是无法被侦听的;(做了很多复杂的周边处理才填上这个坑)
- 底层响应式原理的替换并不直接体现在编码上;
@逻辑复用机制更新
- Vue2的主要(跨组件)逻辑复用方式是mixin;
- mixin就是个垃圾,全局和局部到处都可以定义,相互冲突和覆盖,难以追踪和调试,因此并不好用;
- Vue3的组合式API(compositionAPI)可以将响应式数据的定义逻辑、生命周期回调逻辑、数据侦听逻辑全部组合到【自定义Hook】中,轻松在组件间共享各种业务逻辑而不产生任何副作用;
- 独立的响应式数据的定义:ref定义单个地址、reactive定义响应式对象、toRefs将响应式对象打散为多个ref集合在一起的普通对象;
- 独立的生命周期逻辑:setup本身充当创建阶段,另外六大生命周期回调都以普通回调函数的方式存在:例如onMounted(()=>{组件挂载时做什么}),它们分别是:
- onBeforeMount/onMounted
- onBeforeUpdate/onUpdated
- onBeforeUnmount/onUnmounted;
- PS:在Vue2中卸载阶段的两个生命周期分别是beforeDestroy/destroyed;
- PS:注意keepalive管理的缓存实例有激活/失活两个生命周期activated/deactivated,它们在Vue3中对应的名字为: onActivated/onDeactivated;
- 独立的数据侦听逻辑:
const xxx = computed(()=>基于一手数据换算而来的二手数据)
用于创建独立存在的计算属性;const unwatch = watch(()=>侦听项,(nv,ov)=>副作用逻辑,{config})
和watchEffect(()=>自动收集依赖的副作用逻辑)
用于创建独立的数据变化侦听和副作用逻辑;
项目是如何实现跨域的?
- 开发阶段:通过配置vite的热更新服务器实现跨域,其原理是访问本地的/api,开发服务器会自动代理到服务器的后端地址,服务器之间的互访是不受浏览器同源策略(CORS policy)限制的,跨域得以实现;
- 生产阶段:使用Nginx替代开发服务器实现跨域,跨域原理与开发阶段相同;
- 非代理跨域方式:服务端需要进行配合,具体方式包括:服务端允许跨域 + JSONP;
你的项目是怎么做的权限管理的?
- 后台接口权限:axios的响应拦截器里发现返回码是401/403,一脚踹到登录页;
- 路由权限A方案:加载完整路由表,加全局守卫判断meta,无权限时一脚踹到登录页;
- 路由权限B方案:先挂载公共路由表,登录后再router.addRoute动态挂载权限路由表;
- 菜单权限:由服务端返回菜单(或前端登录后从本地调取对应菜单)并动态渲染;
- 菜单权限注意事项:用户通过地址栏强行访问前端路由时,需要路由权限策略来加以补充;
- 按钮权限:v-if没权限就不显示;或者通过自定义指令v-auth在元素或组件挂载时判断到无权限就直接删除当前按钮; PS:各种权限控制策略的优劣比较
- 接口控制策略 是服务端天然存在的,前端是否做配套处置看具体业务需求;
- 全路由表策略 适用于比较小型的工程;
- 半路由表策略(先加载公共路由表,登录后再动态补充加载权限路由表),适用于中大型项目,先期只加载一部分路由表能提升一些性能;
- 无论全路由策略还是半路由策略,由于全局守卫的存在,性能都好不到哪去;
- 菜单策略 一次性完成大面积的访问权限控制,作为一种宏观权限控制策略,个人认为要比路由控制更可取;
- 按钮显隐策略 适合打扫一些边边角角的地方,是一种有效且必要的补充机制;
你的项目在性能方面有哪些优化措施?
参见 Vue性能优化
讲一讲axios的封装思路;
参见 有没有封装过axios
讲一下某模块的实现细节;
以添加一条电影数据为例:
- 用户在热映列表头部点击新增按钮,进入新增页;
- 新增页加载一个空白的电影数据表单,预先按照指定的空白电影数据JSON模板;
- 用户修改该表单中的各个数据项,表单的所有数据项通过双向数据绑定的方式,将电影的所有字段同步到一个响应式对象filmForm中;
- 对于电影海报和电影演职人员头像,采用立即上传的方式,使用的是ElementPlus中提供的Upload组件;
- 最后用户点击提交,将表单数据格式化为后台所需要的数据格式,具体细节为:将演职人员头像由[{name,url}]格式化为[{name,role,avtarAddress}],将海报数据由[{name,url}]格式化为poster-string形式,再通过ajax的POST请求将表单数据发送服务端,等待服务端返回;
- 如果数据提交成功,导航调回热映列表页;
讲一讲某组件的封装思路;
组件的整体封装思路请参考 如何封装自己的组件
- 此处以通用表格组件EpTable为例说一说该组件的具体封装思路:
- 使用ElementPlus的Table组件将热映数据的所有字段以硬编码的方式渲染出来;
- 发现这些字段的格式高度统一,无非是sortable控制当前字段可否排序,fixed控制当前字段列是否固定,prop定义当前列要渲染数据的哪个字段,label定义列标题,width定义列宽,formatter定义当前字段的加工处理方式,default插槽定义当前列的复杂渲染方式;
- 最终以配置的方式将表格所有字段的渲染方式处理为一个props名曰cols,其值为一个数组,对每个要处理的字段以一个对象的形式进行渲染配置,cols的格式如下:[{sortable:true,fixed:false,prop:"premierAt",label:"上映日期",formatter:row=>new Date(row.premierAt*1000).toLocalString().replaceAll("/","-")}...]
- 对于需要渲染图片的列,覆盖自定义插槽内容,从列的default插槽中获取作用域数据row,即当前行,然后再默认插槽中配置一个与当前列prop的新的作用域插槽,例如poster,由父组件覆盖poster插槽,从插槽中拿到当前行的作用域数据row,在插槽中插入图片,src即为row.poster;
- 最后将最后一列的操作按钮(编辑+删除)固定写死;
- 以上主要讲是是一个核心props名曰cols的配置方式,该组件还提供了events事件和便捷操作API;
- 当用户在特定行点击删除的时候,EpTable会给父组件发送自定义事件deleteItem,携带的载荷为当前行row,由父组件处理事件,将数据中相同id的item予以删除;
- 用户在表格中执行多选,EpTable知道用户实时选中了哪些行,以API名曰getSelectedItems(),父组件通过refEpTable获取EpTable实例在进一步通过调用refEpTable.value.getSelectedItems()获取用户实时选中的所有行,再执行批量操作;
讲一讲某hook的封装思路;
以【获取页面组件的实时滚动位置钩子useScroll】为例:
- 该组件能实时获取页面的滚动位置,即调用useScroll()会到响应式数据scrollTop;
- hook内要定义响应式输入const scrollTop = ref(0);
- 需要将真正在滚动的元素作为参数传递给该hook:const scrollTop = useScroll(scrollingElement)
- 组件挂载时对其根元素添加scroll事件,组件卸载时移除该DOM事件监听器,以避免内存泄露;
- 在scroll事件监听器中,实时获取根元素的scrollToP,同步给ref数据scrollTop:
const scrollHandler = (e) => {
scrollTop.value = root.scrollTop
}
- 最终调用该hook的组件得到的就是其根元素实时滚动的位置这一响应式数据,其余的什么都不用管;
讲一讲某指令的封装思路;
- v-change指令可以在元素被鼠标覆盖的时候改变样式,鼠标移出的时候恢复默认样式;
- 使用方式:
<button v-change="{color:'cyan'}">Click Me</button>
- 指令作用的目标元素所在的组件被挂载时,创建mouseover事件侦听,在其内部将应用binding.value定义的所有样式,在应用该样式之前,要先缓存在元素变化之前的样式在指令对象的defaultStyle中;
- 同时注册该元素的mouseout事件侦听,在其中恢复事先缓存好的该元素的默认样式;
- 指令作用的目标元素所在的组件被卸载时,移除该元素的mouseover与mouseout事件侦听;
开发过程遇到哪些坑?
- 坑指数2/10:代码正确但界面就是不配合,重启开发服务器;
- 坑指数10/10:组件做切换动画时,有一个组件的根布局是ul,里面的item是根据数据动态渲染的,正常需要做切换动画的组件需要唯一根元素,但这里的Ul元素明明是一个根元素,但只要
<transition name="xx" mode="out-in">...</transiton>
一给,该页面就白屏并且同时所有其它参与切换的页面也失效,最终解决方式,在ul外再套一层div,解决方案百度了也没有,猜测可能是ul反复被重新差量渲染状态不稳定,因此加套一层相对稳定的div元素在外面就解决了该问题; - 坑指数4/10:早些年在配置Nginx代理的时候给代理前缀
/api
没有加结尾的/,导致前端路由无效,最终加上收尾的/即/api/
即解决问题; - 坑指数3/10:keepalive缓存组件,导致详情页每次都渲染同一个影片数据,最终解决方案:给keepalive加excluede='[Detail]'将详情页排除出keepalive的管理栈;
- 坑指数10/10:在Vue3拿到网络返回数据的时候,无法在实例对象上挂载同名数据,即
this.cities = cities
无效,在Vue2中有效;