随着公司项目越作越复杂,因前期团队对 Vue 使用经验不足,致使留下比较多坑。再这样下去项目会变成愈来愈难以维护,因而我对主管说:“主管,我想重构”,便有了此次的重构经历。通过对项目分析,主要存在如下问题:css
本项目是一个金融类项目,采用可视化的资产架构描述方式,并根据资产架构生成税务报告。使用 Vue 全家桶进行业务开发,并在 Element UI 基础上进行定制化,可视化建模使用 mxGraph
项目出现全局样式满天飞的状况,有如下缘由html
组件销毁后,Vue是不会删除对应样式标签的,因此组件内样式不写 scoped 存在污染全局样式的风险。vue
为了解决第一个问题,此次重构的作法是,坚定全部组件都使用 scoped。须要覆写子组件时使用深度做用选择器解决。这样仅不会污染全局样式,还对子组件覆写样式一目了然。node
对于弹窗这类确实要做用到全局的样式,咱们统一写在命名为 global.scss 的文件,并使用 BEM 规范命名。ios
对于在组件间复用的样式,分模块地放到 modules 文件夹下,组件内使用时再用 @import 导入。git
来看看重构后的 style 文件夹长这个样子程序员
全局样式样式只剩下 nomalize.css、一些自定义的 reset、element-ui 的默认样式、上文提到的 global,还有就是图标。github
未重构前,全局基础组件放置在 components/common 文件夹,业务组件与其余未归类的组件全放在 components 文件下,看起来很是混乱。编程
经重构后,将组件分为五类: business、common、function、slot,还有一种就是为某个页面特定提供的,下面会提到。element-ui
business 为业务组件跟业务有耦合,可在页面间复用,但不适用于其余项目。而 slot、function、common 这几类是可脱离当前项目使用的。 slot、function 与 common 同样,不一样的是 common 使用频率很是高是全局注册的。而 slot、function 是局部注册使用的。slot 的特别之处在于,这类组件只提供一个样式外壳,无太多交互,能很好地被其余组件利用。
像下图所示其余 Panel 组件均可以复用 slot目录下的 Panel 组件。
此次重构经我总结后得出应该在这两种状况下建立组件
不可复用的,纯粹为了减小某个页面代码,使 template 结构更清晰。
像下面 NodeDetai 页面分离出来的 components 就是上面提到的不可复用组件。
咱们系统核心页面就是画图页。该页面共三个组件,左侧的元素面板、右侧的节点面板、右侧的线条面板。交互与大多可视化建模软件相同,用户将左侧元素拖拽到画布,从节点拖出线条链接到另外一个节点,当用户在画布上选择节点时右侧面板显示节点相关操做,选择线条亦然(同一时刻只能选择一个节点或者一条线条)。与 draw.io 有点类似,但咱们作的不是绘图应用。
通过我对该页面代码进行浏览后发现,该页面之全部这么多代码是由于,在编写方法过程当中咱们会习惯性地将大的方法拆分红小的方法,结果这些小的方法都堆在 methods,致使 template 事件处理函数很是不显眼。因此此次重构目标就是删除 methods 对象中除页面初始化方法外的全部非事件处理方法。也就是说 methods 对象中的每一个方法都应该对应一个 template 事件处理。
那么问题来了拆分出来的小方法不放在 methods ,该放到什么地方?根据我对画布页面代码分析,发现这个页面其实只对有三个东西进行操做:架构、节点、线条。因而按照这个思路独立出有三个 js 文件,将 this 看成参数传入到各自的模块,用来操做 vm 对象。同时将 js 从 vue 文件中独立出来。重构以后该页面目录长成了这个样子
js/index.js 是页面的 vm 对象,重构后代码由原来的1000多行精简成了300多行,提升了可维护性。
这个页面js还存在一个问题,大量的 if/else 判断。这里先扯一点 mxGraph 的东西,在 mxGraph 中节点与线条都统称为 Cell,当节点或线条被删除时 mxGraph 会派发一个 CELL_REMOVE 事件,可是这个 Cell 是节点仍是线条仍是要程序员本身去判断的,这也致使了咱们系统出现了不少下面这样的判断语句
functoin syncRemove(cell) { // 判断是节点仍是线条 const cellIsVertex = cell.vertex; if(cellIsVertex){ // 执行删除节点 } else { // 执行删除线条 } }
通过我思考许久后得出两个方案
使用面向接口编程。上文提到我将该页面的交互分红有三个模块:节点、线条、架构,既然节点、线条有相同的操做,那么他们应该实现共同的接口。因而乎将代码改形成这样子
// vertexOp.js const vertexOp = { // ********* // Interface // ********* handleActive(vertex) { // 节点被点出时该执行的操做 }, async syncAdd(vertex) {}, syncRemove(vertex) {}, // Others .... } // edgeOp.js const edgeOp = { // ********* // Interface // ********* handleActive(edge) {}, async syncAdd(edge) {}, syncRemove(edge) {}, // Others .... }
当用户选择不一样 Cell 的时候,只须要在选择事件处理器中作一次判断便可。
// index.js let opContex = null; let activeCell = null; const listenSelectionChange = ()=> { activeCell = graph.getSelectionCell(); const cellIsVertex = activeCell.vertex; if(cellIsVertex){ opContex = vertexOp; } else { opContex = edgeOp; } } const handleRemoveEvent = ()=> { contexOp.syncRemove(activeCell); }
这个页面再一个问题是,出现这多零散的方法调用。好比像下面的需求
需求:当用户作了任何改变架构外观的操做都将当前架构截图同步到服务端用做该架构的封面展现。
重构前作法:
具体作法是对请求进行命名,而后在每一个请求发送完成时使用全局 eventBus 发送一个请求完成事件。事件处理器只须要根据请求名称判断是否须要截图发送请求。代码思路以下
// api层 // api/nodes.js import http from '@/config/axios/index'; export default { all: data => http('/nodes', data, 'GET'), one: id => http(`/nodes/${id}`, 'GET'), save: data => http('/nodes', data, 'POST', 'nodes-save'), del: id => http(`/nodes/${id}`, 'DELETE', 'nodes-del'), // ..... }; // 封装 axios // config/axios/index.js export default function (...args) { const url = args[0]; let data; let method; let name; // 参数重载 if (args.length === 2) { method = args[1]; } else if (args.length === 3) { if (_.isString(args[1]) && _.isString(args[2])) { method = args[1]; name = args[2]; } else { data = args[1]; method = args[2]; } } else if (args.length === 4) { data = args[1]; method = args[2]; name = args[3]; } else { throw new Error('http support max 4 args'); } if (_.isNil(name)) { // 没有命名的请求,默认命名为当前时间戳 name = String(Date.now()); } else { // 有命名的请求,为了保证请求的惟一性,加上时间戳后缀 name = `${name}__${Date.now()}`; } return $axios({ url, data, method }, name); } async function $axios(initialOptions, requestName) { const options = getOptions(initialOptions); initialOptions.requestName = requestName; requestManager.addReq({ name: requestName, config: initialOptions, }); try { const response = await axios(options); requestManager.popReq({ name: requestName, response, }); return response.data; } catch (error) { // 保证即使请求出错也要使该请求弹出队列 requestManager.popReq({ name: requestName, error, }); return {}; } } // 发送请求相关事件 // requestManager.js import Vue from 'vue'; const $bus = Vue.prototype.$bus; class RequestManager { constructor() { this._updateRequests = []; } addReq(req) { if (req.config.method.toLowerCase() === 'get') { return; } this._updateRequests.push(req); $bus.$emit('before-modify-req-send', req); } popReq({ name, response }) { if (response && response.config.method.toLowerCase() === 'get') { return; } const idx = this._updateRequests.findIndex(item => item.name === name); if (idx >= 0) { this._updateRequests.splice(idx, 1); $bus.$emit('modify-req-finished', name, response); if (this._updateRequests.length === 0) { $bus.$emit('modify-req-empty'); } } } } // RequestManager是一个全局的单例对象 export default new RequestManager();
最终只须要对请求进行拦截,就能够大量减小零散的方法调用
// xxx.vue this.$bus.$on('modify-req-finished', (name, response) => { const reqs = ['c-transitions-updateRatio', 'c-transitions-save', 'c-transitions-del', /*..........*/]; const reqName = name.split('__')[0]; if (reqs.includes(reqName)) { // 截图,发送请求 } });
除此以外请求拦截还适用于这个场景: 当用户作了操做,实时提示用户操做保存中,保存完成后提示用户操做已保存。
使用请求拦截很是轻松完成这个功能,只须要监听发送请求事件、请求队列清空事件作相应提示便可。
此次重构一人完成,用时一星期,作了以下工做
使用以下方式拆分大文件
感悟比较深的是,网上虽然不少文章教你怎样用 Vue 作好项目,但实际状况仍是要从项目自身出发,本身必定要对项目进行思考,我相信没有适合全部项目的 "Vue最佳实践"。只要不断对项目进行思考、改进,才能找到最适合自身项目的架构方式。