中后台解决方案学习心得

前言

2023年底,算是工作以后最冷的冬天了。不止是气温,更是工作环境。从这一年的12月中旬,我开始了新一轮的前端技术的学习,1.15终于断断续续学完了这一门中后台解决方案课程。

这篇学习心得记录了我在这门课程中的一些新的收获,主要包括Vue3的使用代码提交规范工具三方库选择等等。

1. Vue3使用

由于我本身工作的技术栈以React为主,Vue也只有接近一年的2.0的使用,所以这次Vue3的学习也是给我眼前一亮的感觉。

1.1 Vue2到Vue3的变化

由于是这门课第一次接触Vue3,根据我之前用React和Vue2的直观感觉来说,Vue3的composition api确实让Vue变得更像React

以前的Vue2,响应式数据在data 里,方法在methods 里,watchcomputed各个生命周期......而且data还必须声明成一个函数,返回值中写变量

而实际上项目的代码并不是简简单单按照变量、方法、生命周期这样来划分的,大部分情况下,某几个变量和某个方法关联,属于某个功能,这样就导致每次找代码逻辑都要翻滚动条,很麻烦

Vue3最新的Composition API,用了类似于React Hooks的函数式写法,声明一个响应式数据,用ref,声明一个方法,直接声明就好了,生命周期和computed、watch监听,直接在需要的时候声明一个函数就行

这是我在这门课程学习中最直观的感受

1.2 自定义vue指令

项目中用到了RBAC权限控制,权限精确到了页面的按钮。

巧合的是,最近公司的项目,我自己也用Nest + React自己做了完整的前后端页面和接口,所以RBAC模型还算熟悉。而页面级别的权限控制,也是直接用动态路由渲染即可,逻辑上并没有大的难点。

但是页面按钮,如果使用v-show或者v-if,那在展示的逻辑上又要添加冗长的代码。为了增加复用性,课程里使用了自定义指令v-permission。

directives文件夹中添加一个permission.js,用来处理按钮的展示/隐藏

js 复制代码
import store from '@/store'

// 两个参数,第一个是该指令绑定在哪个DOM元素,第二个是绑定时传入的参数
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);
    }
    // 更新后也要调用
    update(el, binding) {
        checkPermission(el, binding);
    }
}

完成后,在directives/index中绑定指令

js 复制代码
import permission from './permission';

export default (app) => {
    app.directive('permission', permission);
}

最后还是得在main.js里绑定到全局Vue上

js 复制代码
// 指令
import installDirective from '@/directives';

const app = createApp(App);
installDirective(app);

1.3 逻辑复用

之前Vue2里面,我曾经用过mixins来实现部分组件/页面的数据/方法复用,但是这个复用直观上感觉并不好,引用的时候要在mixins中的数组里添加,而且在有参数需要传入的时候,处理起来也挺麻烦

这个项目中,在使用动态表格的时候,做了一部分功能拆分,将原本写在一个vue文件中的逻辑,拆分到了多个js文件中,再在vue文件中导入传参,这样也方便了逻辑复用

js 复制代码
// index.vue
import { dynamicData, selectDynamicLabel, tableColumns } from './dynamic';
import { tableRef, initSortable } from './sortable';
js 复制代码
// dynamic.js
import DynamicData from './DynamicData';
import { ref, watch } from 'vue';
import { watchSwitchLang } from '@/utils/i18n';

// 暴露动态列数据
export const dynamicData = ref(DynamicData());

// 被勾选的动态列数据
export const selectDynamicLabel = ref([]);
// 默认全选
const initSelectDynamicLabel = () => {
  selectDynamicLabel.value = dynamicData.value.map((item) => item.label);
};
initSelectDynamicLabel();
// 国际化
watchSwitchLang(() => {
  dynamicData.value = DynamicData();
  initSelectDynamicLabel();
});

// table列数据
......
js 复制代码
// sortable.js
import { ref } from 'vue';
import Sortable from 'sortablejs';
import i18n from '@/i18n';
import { ElMessage } from 'element-plus';
import { articleSort } from '@/api/article';

// 排序相关
export const tableRef = ref(null);

/**
 * 初始化排序
 */
export const initSortable = (tableData, cb) => {
  ......
};

2. 批量导入文件

之前在引入文件的时候,经常会创建个index.js文件,在这个文件内部做一个批量导入,避免写n多个import

js 复制代码
import { DepartmentTree, ExportExcel, Pagination, Table } from "src/components";

但是这个index.js文件的导入往往会写成这个鬼样子

js 复制代码
import Breadcrumb from "./Breadcrumb";
import Calendar from "./Calendar";
import DepartmentTree from "./Tree/DepartmentTree";
import Detail from "./Detail";
import DynamicForm from "./Form/DynamicForm";
import EditForm from "./Form/EditForm";
import ExportExcel from "./ExportExcel";
import GroupForm from "./Form/GroupForm";
import MenuTree from "./Tree/MenuTree";
import Pagination from "./Pagination";
import SearchForm from "./Form/SearchForm";
import Schedular from "./Schedular";
import Table from "./Table";
import UploadButton from "./Upload/UploadButton";
import UploadImage from "./Upload/UploadImage";
import UploadStep from "./Upload/UploadStep";

export {
  Breadcrumb,
  Calendar,
  DepartmentTree,
  Detail,
  DynamicForm,
  EditForm,
  ExportExcel,
  GroupForm,
  MenuTree,
  Pagination,
  SearchForm,
  Schedular,
  Table,
  UploadButton,
  UploadImage,
  UploadStep,
};

这可就头疼了,每次添加一个新的component都要在index.js先引入再导出?这不太麻烦了嘛!

课程里用到了webpack 里的require.context()将svg文件进行批量导入(官方文档)

js 复制代码
// 这里创建了一个上下文,三个参数分别为:搜索的文件目录,是否递归查找子目录,匹配文件的正则表达式
const svgRequire = require.context('./svg', false, /\.svg$/);
// 这个函数返回一个 require 函数,可以接受一个request参数,用于require导入
// 可以用require.keys()获取所有svg图标,然后把每个图标传递给require函数,类似于import XXX的效果
svgRequire.keys().forEach((svgIcon) => svgRequire(svgIcon));

3. 动态换肤

说实话这是这门课程中我学起来觉得最麻烦的一个章节,因为逻辑上要考虑element-plus组件库/自定义组件两部分,尤其是element-plus组件库换肤最为麻烦,这里主要记录的就是这部分逻辑。

element-plus动态换肤的原理分4步:

  1. 获取当前组件库所有样式
  2. 定义要替换的样式
  3. 原样式用正则替换新的
js 复制代码
// 获取element-plus默认样式表
const getOriginalStyle = async () => {
  const version = require('element-plus/package.json').version
  // 这个地方根据版本不同,要去node_modules中查看文件具体路径
  const url = `https://unpkg.com/element-plus@${version}/dist/index.css`
  const { data } = await axios(url)
  // 把获取到的数据筛选为原样式模板
  return getStyleTemplate(data)
}

// 根据主题色,获取到新的色值
export const generateColors = primary => {
  if (!primary) return
  const colors = {
    primary
  }
  Object.keys(formula).forEach(key => {
    const value = formula[key].replace(/primary/g, primary)
    colors[key] = '#' + rgbHex(color.convert(value))
  })
  return colors
}

export const generateNewStyle = async primaryColor => {
  const colors = generateColors(primaryColor)
  let cssText = await getOriginalStyle()

  // 遍历生成的样式表,在 CSS 的原样式中进行全局替换
  Object.keys(colors).forEach(key => {
    cssText = cssText.replace(
      new RegExp('(:|\\s+)' + key, 'g'),
      '$1' + colors[key]
    )
  })

  return cssText
}
  1. 替换后的样式写到style标签,用优先级来替换掉原有样式
js 复制代码
export const writeNewStyle = elNewStyle => {
  const style = document.createElement('style')
  style.innerText = elNewStyle
  document.head.appendChild(style)
}

在组件中,要把最新的主题色保存到vuex中

js 复制代码
const comfirm = async () => {
  // 1.1 获取主题色
  const newStyleText = await generateNewStyle(mColor.value)
  // 1.2 写入最新主题色
  writeNewStyle(newStyleText)
  // 2. 保存最新的主题色
  store.commit('theme/setMainColor', mColor.value)
  // 3. 关闭 dialog
  closed()
}

4. 代码提交规范

4.1 代码提交规范工具Commitizen

之前在一门NodeJS的课程中,学习到过git提交规范,大概就是每次提交,都要用类似如下的格式

<提交类型>: <提交内容>

feat: 新增XXX功能,XXX接口联调

fix: 修复XXX

对我个人来说,学习了,应用了,OK,以后我基本上能保证用这种格式提交代码。但是对于团队开发来说,其他人总有不知道提交规范的,甚至我自己难免也会有遗忘的时候,那最好有一个自动化 的工具来规范提交。Commitizen就是这样的一个工具,它提供了一个git cz的指令替代git commit,会在提交代码的时候要求用户填写所有的必填字段

使用流程如下

  1. 全局安装Commitizen(课程中使用了固定版本号)

npm install -g commitizen@4.2.4

  1. 安装配置cz-customizable插件

先下载cz-customizable

npm i cz-customizable@6.3.0 --save-dev

完成后在package.json做配置

json 复制代码
"config": {
    "commitizen": {
        "path": "node_modules/cz-customizable"
    }
}

根目录配置.cz-config.js提示文件

js 复制代码
module.exports = {
  // 可选类型
  types: [
    { value: 'feat', name: 'feat:     新功能' },
    { value: 'fix', name: 'fix:      修复' },
    { value: 'docs', name: 'docs:     文档变更' },
    { value: 'style', name: 'style:    代码格式(不影响代码运行的变动)' },
    {
      value: 'refactor',
      name: 'refactor: 重构(既不是增加feature,也不是修复bug)'
    },
    { value: 'perf', name: 'perf:     性能优化' },
    { value: 'test', name: 'test:     增加测试' },
    { value: 'chore', name: 'chore:    构建过程或辅助工具的变动' },
    { value: 'revert', name: 'revert:   回退' },
    { value: 'build', name: 'build:    打包' }
  ],
  // 消息步骤
  messages: {
    type: '请选择提交类型:',
    customScope: '请输入修改范围(可选):',
    subject: '请简要描述提交(必填):',
    body: '请输入详细描述(可选):',
    footer: '请输入要关闭的issue(可选):',
    confirmCommit: '确认使用以上信息提交?(y/n/e/h)'
  },
  // 跳过问题
  skipQuestions: ['body', 'footer'],
  // subject文字长度默认是72
  subjectLimit: 72
}

后续就可以用 git cz 替代 git commit

但是还是有问题,有可能我们还是会一不小心用git commit提交,那这不就功亏一篑了吗?

为了解决这个问题,得使用Git Hooks检查提交信息,不符合要求的不允许提交

4.2 Git Hooks

Git Hooks是git 在执行某个事件前后进行的其他一些操作,有点类似于前端中的生命周期

因为我们是在代码提交前 操作,所以用到的是commit-msg钩子

为了在项目中监听Git Hooks并执行特定操作,需要安装上husky

npm install husky --save-dev

npx husky install

这时候在项目的根目录下会生成一个.husky文件夹

接下来用命令在package.json中生成prepare指令(也可以手动添加),并执行

npm set-script prepare "husky install"

npm run prepare

执行成功后,用命令 给husky添加一个commit-msg阶段的执行指令(这里不能手动添加,会导致后面husky无法监听)

npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'

完成之后,下次提交时候就必须用git cz的提交规范了,否则提交不予通过

5. 第三方库的选择

关于第三方库,之前自己在开发过程中,往往直接自己在npm搜索,或者在其他前端开发群里问问,当然这都是方法,但是自己没有形成一些选择的方法论

课程中给的建议是

  1. 开源协议:MIT或者BSD协议的开源项目
  2. 功能:满足基本要求(这个一般都会去看)
  3. issue:查看作者维护程度,看最新一次的issue距今多久了,更新的频次如何,如果不咋更新,那遇到问题可能不能及时处理
  4. 文档:越详细越好,最好有中文文档
  5. 国产的:是的话当然最好

6. 其他一些细节

6.1 登录退出逻辑

登录退出一般就直接清除token/浏览器缓存,课程里做了一个细化,其实应该做到两部分:

  1. 主动退出

这个就是用户点击"退出登录",清除token/浏览器缓存,跳转登录页面

  1. 被动退出
  • token失效:这个一般会被axios响应拦截的401打回到登录界面

  • 登录超时:这个需要在登录成功的时候记录一下时间戳,然后在响应拦截器里面判断是否超时,超时触发清除token/浏览器缓存的逻辑,并跳转登录页面

6.2 keep alive缓存处理

keep alive可以很好地缓存Vue里面组件的状态,但是当我们对组件内部数据更新的时候,显然我们是不想获取这个缓存的状态的,而是应该更新数据(例如创建了新数据,我们应该重新请求接口,拿到最新的数据)

Vue3里面用的是onActivated这个生命周期钩子

js 复制代码
import { ref, onActivated } from 'vue'

// 处理导入用户后数据不重新加载的问题
onActivated(getListData)

总结

这门课程前前后后学了一个月有余,期间因为自己下班后回娘家蹭饭/打鼓/聚餐等等各种事情偶尔会有不学习的情况,好在学习的习惯还能维持住。课程总的来说还是有一些收获的,后面也逐步尝试把其中学到的东西融合到公司的项目里。

相关推荐
码农爱java6 小时前
设计模式--抽象工厂模式【创建型模式】
java·设计模式·面试·抽象工厂模式·原理·23种设计模式·java 设计模式
Jiude6 小时前
算法题题解记录——双变量问题的 “枚举右,维护左”
python·算法·面试
长安051112 小时前
面试经典题目:LeetCode134_加油站
c++·算法·面试
正在绘制中13 小时前
Java重要面试名词整理(一):MySQL&JVM&Tomcat
java·开发语言·面试
沉默王二13 小时前
虾皮开的很高,还有签字费。
后端·面试·github
Do14 小时前
时间系列三:实现毫秒级倒计时
前端·javascript·面试
小蜗牛慢慢爬行14 小时前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
ThisIsClark15 小时前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
程序猿进阶16 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
LCG元1 天前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展