# bosssoft-train-user-permission-centre-front-end **Repository Path**: chufeng-official/bosssoft-train-user-permission-centre-front-end ## Basic Information - **Project Name**: bosssoft-train-user-permission-centre-front-end - **Description**: 前端项目 - **Primary Language**: HTML - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 10 - **Created**: 2022-11-29 - **Last Updated**: 2023-01-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 整改说明 ## 功能方面 ### 图片文字 已修改。 ### 角色字段改为默认启用 已修改。 ### 排序问题 用户表格数据查询的后端有问题,修改后updatedTime字段没有变化,所以无法按照更新时间排序。 但是角色表格数据就没有这个问题,修改后会按照更新时间,将最近修改的角色排到第一位。 所以我只能通过修改前端的表格数据的方式修改排序,页面刷新之后就不能保证排序了。 ## 代码方面 ### 分支规范 已修改。 ### 开发要求 已将多处Promise对象的连续同步使用改为使用`Promise.all`。 已新增`BosssoftTree.vue`组件。 ### 代码可读 #### 属性引号 这个属于我个人的代码风格,如果项目规范要求我必须改为不加引号,我可以改,这当然没有问题,但是我觉得我有必要阐述一下为什么我要加引号。 加引号主要有两个原因: 1. 便于浏览一个对象中存在哪些属性; 2. 便于区分属性的类型。 首先说第一个原因,在VSCode中,在使用对象字面量的时候,写对象的属性时VSCode会弹出代码提示,列出以我们写出的字符串为前缀的属性名,就像这样: ![代码规范](./整改/代码规范1.png) 在这个例子中,当我们输入了`c`,VSCode就会提示出vue组件对象中以`c`开头的所有属性:`comments`、`components`、`computed`和`created`,然而,如果我们想知道它所有的属性名,那么该怎么办呢? 解决办法就是输入`'`,就像这样: ![代码规范](./整改/代码规范2.png) 这时当我们再次输入`回车`时,当前高亮选择的项就会自动写入编辑器的光标处,非常方便。 这个功能的好处在于,如果我们使用的对象的类型是明确的,我们可以在不看文档的情况下知道它所有符合类型定义的属性名。 然后说第二个原因,在`JavaScript`中,对象字面量的属性名和变量名相同时是可以省略的,就像这样: ```JavaScript const a = 0 const object = { a: a, b: 2, } ``` 等价于 ```JavaScript const a = 0 const object = { a, b: 2, } ``` 两个a在这个例子中是可以合并的,非常方便。 这种写法非常常用,因为很多时候我们需要从后端的json数据中取出一部分字段,并且绝大多数字段的名字是相同的,这时我们就可以用对象解构和属性简写的语法实现,就像这样: ```JavaScript const object = { 'dataArray': data.map(({ birthday, code, company, department, id, name, position, roleList, sex, status, tel, }) => { const role = roleList?.map(({ 'name': roleName, }) => { return roleName }).join(' ') return { 'birthday': birthday?.slice(0, 10), code, 'company': company?.name, 'department': department?.name, id, name, 'position': position?.name, role, 'sex': ['男', '女'][sex], status, tel, } }), 'total': Number(total), } ``` 使用省略写法时,属性名是一定不能带有`''`的,而普通属性名是可以带也可以不带的。在上述例子中,我用`''`做了区分,可以很清楚地看到,带有`''`的属性名是普通的、需要写属性值的属性,而没有带`''`的是简写属性,是和同名变量绑定的。 上面的这个例子是一个比较复杂的例子,在这里使用`''`作为属性区分的方式,非常清晰,一目了然,我认为对于后续的阅读和维护都有很大的便利。 #### 文件注释和组件注释的格式 关于文件的创建人、创建时间、更新时间等,我个人认为没有必要写,因为Git中会记录这些信息,而且避免了人为篡改或者失误写错的情况。 关于组件的功能注释,我写了,只不过没有写在文件顶部,我写在了`export default {`上面,就像这样: ```JavaScript /** * # 博思树 * 用于构建树形查询组件 */ export default { 'name': 'BosssoftTree', // ... } ``` 因为写在这里,当鼠标悬停在导入项上时,VSCode就会弹出友好的提示信息,就像这样: ![代码规范](./整改/代码规范3.png) 并且,VSCode注释是支持解析`markdown`格式的。 而如果写在组件文件的开头,那么就没有这样的效果。 #### 组件和函数的功能注释 功能方面的注释确实比较简略,主要是由于我准备期末考试和治疗新冠肺炎这两件事耽误了将近两周时间,为了尽快地实现项目功能,就没有在注释的内容上花费太多精力 不过在注释的格式和JSDoc类型标注上我下了一定的功夫,因为我认为类型在一定程度上能供体现功能,起到代码自解释的效果。 由于该项目中不要求使用`TypeScript`,所以我在注释中使用JSDoc语法对类型进行标注,配合上VSCode的提示功能,使得组件和函数的查看和使用更加方便。 #### 全局变量和局部变量之间的权衡 在该项目中,我把所有的数据都放在了页面根组件中作为“全局”的变量使用,没有将其放在组件内部或者函数内部作为局部变量,原因是在这个项目中,每个组件不完全是一个独立的存在,它们之间的数据是需要共享的,例如`UserDataTable.vue`的表格数据,会被其它组件的事件所改变,所以表格的数据不能写在组件内部,而只能写在组件外部,也就是`index.vue`里,和其它组件处于同一个外层,便于其它组件操作。 而有的组件虽然不是必须和其它组件共享数据,但是它需要维护一个临时状态,例如`ViewManager.vue`,它需要存储“需要显示的表格列”这样的信息,同时还要根据这个信息维护一个“需要勾选的多选框”这样的临时信息,当用户取消时,临时信息应该消失,恢复到之前的状态。 基于以上两个需求,为了保持组件行为的一致性,我采取了以下模式设计这两个页面的所有组件: 在`index.vue`中调用`ajax`接口获取数据,将该数据保存在`index.vue`中的`data`中,再通过组件的`props`传给组件。组件在内部创建一个用于显示的临时副本,当需要在组件内修改数据时,修改这个副本,只有当用户保存或者提交时,才触发事件,将数据传给父组件`index.vue`,然后修改`index.vue`的`data`中的数据,之后根据需要,在父组件`index.vue`中调用`ajax`接口发送数据;如果用户没有保存就退出了该组件,那就舍弃副本的信息,直到下次打开该组件时,组件内的数据又会被父组件传入的数据同步。 这个模式主要遵循以下两个原则: 1. 所有的`ajax`方法都是由**页面**维护的而非页面的**组件**维护的,都写在`index.vue`中; 2. 所有可变的数据都存储在组件以外,不在组件内写封闭的数据。在组件中虽然会维护一个副本,但是这个副本在组件打开和关闭(或者根据需要)时,都会与父组件传入的数据进行同步,不存在封闭的数据。 这样写主要有以下好处: 1. 即使页面中各组件在形式上差异很大,但是父子组件之间通信的写法都是相似的,可以很方便地互相参考、复制粘贴; 2. 可以很方便地实现“不保存就回退”的功能。 3. 组件之间的数据互用更方便,例如,编辑用户数据时`UserDataForm`需要用到用户的`id`,但是用户`id`只有`UserDataTable`才能提供,这时只需要将`index.vue`中的`data`作为桥梁传递`id`数据即可 4. 大多数函数不需要参数(该点存疑,我展开讨论)。 参数看似给函数提供了额外的信息,提高了封装度,其实函数参数越多,可能出现的代码冲突、变量命名的复杂度和开发心智负担也越大。 Vue中`methods`中的函数是非常特殊的一个存在,不能把它和一般的函数相提并论。Vue之所以提供了一个`methods`来定义函数,而不是让我们在`export default {`上面定义函数,就是因为`methods`中的函数都需要调用`this`,而且必须是绑定了Vue组件实例的`this`。所以它一定是在内部使用了Vue实例上的属性,而这个属性是可能被其它代码改变的,所以它几乎不会是一个函数式编程意义上的**纯函数**(我个人习惯把纯函数写在`export default {`上面或者写在方法内部作为临时函数),所以也就很难说这个函数的“封装性”、“复用性”或者“内聚程度”有多好。既然它一定要用Vue实例上的属性,那就干脆不要混用,除了事件处理函数以外,大多数函数我都让它取消了参数,避免函数参数和Vue实例属性的混用,防止它给我一种它是纯函数的错觉。 当然,这个模式带来的麻烦也是显而易见的,那就是显著地提高了`index.vue`整体的复杂度,对于此我没有什么很好的解决方法,希望以后能够学习到更好地组件设计模式 #### Promise `Promise`对象可以通过`then` `catch`调用,也可以通过`async`和`await`改造成同步代码,它们本质上是一样的,就看代码怎么写。 我个人偏向于多使用`async`和`await`,因为这样可以使代码可以轻易地和其它同步代码结合使用,当同步代码的执行高度依赖于异步代码的执行结果时,`async`和`await`语法要比Promise链的写法更简洁 一般而言,不使用`async`和`await`主要是怕它们阻塞页面加载,这个主要看该Promise的执行结果的用途,如果接下来的代码没它不行,那么就算用的是`then` `catch`,该阻塞还是得阻塞 在该项目中,顶层函数基本上都是事件回调,它们之间是不会阻塞的,每个函数只负责自己内部的逻辑,不会去阻塞其它事件回调 使用`async`和`await`最大的好处还是错误处理和使用控制流语句更加简单且符合直觉,减少开发者的心智负担,比如这个例子: ```JavaScript try { if (await batchRemove(load)) { $message({ 'message': '删除成功', 'type': 'success', }) return } } catch (error) { console.error(error) } throw new Error('删除用户数据时出错') ``` 在这个例子中,我希望如果`batchRemove`执行成功且返回值为`true`,那么就弹出提示框,并且结束当前所在的外层函数;如果出现错误或者返回值为`false`,那么就抛出自定义错误`'删除用户数据时出错'`,并且如果是出错的情况,还需要在控制台输出这个错误。 如果使用`then` `catch`的写法,大概需要写成这样: ```JavaScript batchRemove(load).then((result) => { if (!result) { throw new Error('删除用户数据时出错') } $message({ 'message': '删除成功', 'type': 'success', }) }).catch((error) => { console.error(error) throw new Error('删除用户数据时出错') }) ``` 看似两者实现了同一个功能,但是其实是由区别的: 我希望如果执行成功且返回`false`时,虽然要让它抛出自定义错误,但是不希望它在控制台输出,使用`try` `catch`时的实现方法也很简单,只要把`throw`语句写在`try` `catch`语句之外就行了,这样灵活性强并且代码清晰容易理解,一眼就能看明白 但是在上述的`then` `catch`代码中,第三行处抛出的错误是会被自己的`catch`捕获的,这就导致会重复执行控制台输出,不满足我们的需求,要解决这个问题,我们就必须另行判断,增加了不必要的复杂度,就像这样: ```JavaScript batchRemove(load).then((result) => { if (!result) { throw result } $message({ 'message': '删除成功', 'type': 'success', }) }).catch((error) => { if (error instanceof Error) { console.error(error) } throw new Error('删除用户数据时出错') }) ``` 除了`throw`语句控制流使用不灵活以外,`return`控制流也不灵活,在`then` `catch`的回调函数中,`return`只能中断自己,而无法中断上层函数,但是`try` `catch`就可以。 ### 其它 #### 公共组件的抽象问题 我觉得本项目中我写的页面中的`组件`其实都很难称之为`组件`,因为它们的共同点太少了,差异点太多了,就算抽象出来,我们再使用时还是需要写很多个性化的配置,几乎等同于直接使用element-ui重写了,所以我觉得它们更多应该算是“页面的一部分”而非“组件”,只有抽象到element-ui那种通用程度的我觉得才算组件,或者`BosssoftTree`这种的也可以算是组件