前言
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 里,watch 、computed 、各个生命周期......而且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步:
- 获取当前组件库所有样式
- 定义要替换的样式
- 原样式用正则替换新的
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
}
- 替换后的样式写到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
,会在提交代码的时候要求用户填写所有的必填字段。
使用流程如下
- 全局安装Commitizen(课程中使用了固定版本号)
npm install -g commitizen@4.2.4
- 安装配置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搜索,或者在其他前端开发群里问问,当然这都是方法,但是自己没有形成一些选择的方法论
课程中给的建议是
- 开源协议:MIT或者BSD协议的开源项目
- 功能:满足基本要求(这个一般都会去看)
- issue:查看作者维护程度,看最新一次的issue距今多久了,更新的频次如何,如果不咋更新,那遇到问题可能不能及时处理
- 文档:越详细越好,最好有中文文档
- 国产的:是的话当然最好
6. 其他一些细节
6.1 登录退出逻辑
登录退出一般就直接清除token/浏览器缓存,课程里做了一个细化,其实应该做到两部分:
- 主动退出
这个就是用户点击"退出登录",清除token/浏览器缓存,跳转登录页面
- 被动退出
-
token失效:这个一般会被axios响应拦截的401打回到登录界面
-
登录超时:这个需要在登录成功的时候记录一下时间戳,然后在响应拦截器里面判断是否超时,超时触发清除token/浏览器缓存的逻辑,并跳转登录页面
6.2 keep alive缓存处理
keep alive可以很好地缓存Vue里面组件的状态,但是当我们对组件内部数据更新的时候,显然我们是不想获取这个缓存的状态的,而是应该更新数据(例如创建了新数据,我们应该重新请求接口,拿到最新的数据)
Vue3里面用的是onActivated这个生命周期钩子
js
import { ref, onActivated } from 'vue'
// 处理导入用户后数据不重新加载的问题
onActivated(getListData)
总结
这门课程前前后后学了一个月有余,期间因为自己下班后回娘家蹭饭/打鼓/聚餐等等各种事情偶尔会有不学习的情况,好在学习的习惯还能维持住。课程总的来说还是有一些收获的,后面也逐步尝试把其中学到的东西融合到公司的项目里。