1 快速开始

00.启动
    tienchin-ui
    npm use 20.10.0
    npm install
    npm run dev
    npm run build:prod

01.日期类型
    网盘视频目录  对应源码链接
    20220508    v20220508
    20220514    v20220514
    20220521    v20220521
    20220527    v20220527
    20220603    v20220603
    20220611    v20220611
    20220619    v20220619
    20220625    v20220625
    20220702    v20220702
    20220709    v20220709
    20220716    v20220716
    20220723    v20220723
    20220730    v20220730
    20220806    v20220806
    20220813    v20220813
    20220820    v20220820
    20220827    v20220827
    20220924    v20220924
    20221001    v20221001
    20221015    v20221015
    20221019    v20221019
    20221029    v20221029
    20221105    v20221105
    20221119    v20221119
    20221126    v20221126
    20221217    v20221217
    20230121    v20230121
    20230211    v20230211
    20230225    v20230225
    20230311    v20230311
    20230318    v20230318
    v0.0.1      v0.0.1

02.视频目录
    000.开篇.mp4
    001.运行RuoYi-Vue.mp4
    002.代码格式化.mp4
    003.项目结构大改造.mp4
    004.项目改造完善.mp4
    005.项目结构分析.mp4
    006.验证码响应结果分析.mp4
    007.验证码生成接口分析.mp4
    008.验证码配置分析.mp4
    009.验证码的校验.mp4
    010.登录流程分析.mp4
    011.登录JWT校验.mp4
    012.SpringSecurity登录配置分析.mp4
    013.自定义多数据源思路分析.mp4
    014.自定义多数据源-1.mp4
    015.自定义多数据源-2.mp4
    016.手动实现网页上切换数据源.mp4
    017.RateLimiter注解简介.mp4
    018.自定义限流注解-1.mp4
    019.自定义限流注解-2.mp4
    020.RuoYi脚手架限流注解分析.mp4
    021.幂等性实现的6中思路梳理.mp4
    022.实现JSON格式参数多次读取.mp4
    023.防止请求重复提交.mp4
    024.防止接口重复提交注解分析.mp4
    025.数据权限注解介绍.mp4
    026.数据权限案例准备工作.mp4
    027.权限注解实现思路分析.mp4
    028.自定义数据权限注解@DataScope.mp4
    029.数据权限过滤角色数据.mp4
    030.数据权限过滤用户数据.mp4
    031.数据权限注解总结.mp4
    032.操作日志记录.mp4
    033.修改日志方法名称.mp4
    034.理解Aware接口.mp4
    035.自定义注解+AOP整理.mp4
    036.TienChin细化到按钮的权限实现思路.mp4
    037.理解TienChin项目中的权限注解.mp4
    038.角色和权限概念梳理.mp4
    039.Spring Security中角色和权限的区别.mp4
    040.SpringSecurity中的权限处理逻辑.mp4
    041.SpringSecurity中使用权限通配符.mp4
    042.SpringSecurity另一种权限判断方式.mp4
    043.自定义权限表达式.mp4
    044.使用POSTMAN测试项目接口.mp4
    045.自定义TienChin项目权限判断表达式.mp4
    046.登录鉴权流程梳理.mp4
    047.运行RuoYi-Vue3.mp4
    048.动态菜单实现思路.mp4
    049.动态菜单JSON分析.mp4
    050.动态菜单的path问题.mp4
    051.服务端查询当前登录用户菜单.mp4
    052.服务端构建动态菜单.mp4
    053.动态菜单实现思路梳理.mp4
    054.Vue3中的动态菜单递归渲染.mp4
    055.前端固定路由定义.mp4
    056.前端轻量级状态管理框架Pinia.mp4
    057.前端网络请求封装思路.mp4
    058.前端登录请求执行流程.mp4
    059.前端动态菜单加载思路.mp4
    060.动态菜单为什么不能存在localStorage.mp4
    061.动态菜单为什么不能存在localStorage-2.mp4
    062.前端路由导航守卫源码分析.mp4
    063.前端动态菜单加载四个核心变量.mp4
    064.routes变量多级菜单铺平.mp4
    065.component字符串转对象.mp4
    066.前端多级菜单铺平.mp4
    067.过滤前端本地动态路由.mp4
    068.前端generateRoutes方法.mp4
    069.前端回调地狱.mp4
    070.Promise初体验.mp4
    071.then方法的各种情况.mp4
    072.Promise中的catch代码块.mp4
    073.Promise中的finally代码块.mp4
    074.Promise中的静态方法.mp4
    075.TienChin项目Vue3中的Promise.mp4
    076.Vue3中的变量定义方式.mp4
    077.Vue3中方法的定义.mp4
    078.Vue3中钩子函数的定义.mp4
    079.Vue3中的计算属性.mp4
    080.Vue3中的watch函数.mp4
    081.Vue3中的ref和reactive.mp4
    082.Vue3中的setup函数.mp4
    083.Vue3中自定义全局方法.mp4
    084.Vue3中router和store的调用.mp4
    085.插件和全局方法的区别.mp4
    086.在Vue3中定义一个插件.mp4
    087.在插件中注册全局组件.mp4
    088.在插件中自定义全局指令.mp4
    089.Vue3自定义插件时传入参数.mp4
    090.自定义插件中的provide和inject.mp4
    091.什么是Vue中的指令.mp4
    092.Vue3自定义局部指令.mp4
    093.Vue3全局自定义指令.mp4
    094.Vue3自定义指令同时传递两个参数.mp4
    095.自定义插件传递动态参数.mp4
    096.Vue3中自定义权限指令.mp4
    097.Vite简介.mp4
    098.创建一个基于Vite的项目.mp4
    099.Vite项目安装vue-router.mp4
    100.Vue3方法自动导入插件.mp4
    101.Vite中省略组件后缀.mp4
    102.Vue3简化组件名称配置.mp4
    103.【workflow】状态机解决流程问题.mp4
    104.【workflow】报销审批流程.mp4
    105.【workflow】笔记本电脑生产流程.mp4
    106.【workflow】三大主流工作流.mp4
    107.【workflow】BPMN流程图规范.mp4
    108.【workflow】BPMN流程图元素.mp4
    109.【workflow】常见的流程绘制工具梳理.mp4
    110.【workflow】使用IDEA插件绘制流程图.mp4
    111.【workflow】分析流程图的XML文件.mp4
    112.【workflow】flowable-ui两种安装方式.mp4
    113.【workflow】flowable-ui四大核心功能.mp4
    114.【workflow】flowable-ui身份管理.mp4
    115.【workflow】flowable-ui管理员功能.mp4
    116.【workflow】flowable-ui建模器应用程序-报销流程介绍.mp4
    117.【workflow】flowable-ui建模器应用程序-绘制流程图.mp4
    118.【workflow】flowable-ui建模器应用程序-填写报销材料.mp4
    119.【workflow】flowable-ui建模器应用程序-小于等于1000审批流程.mp4
    120.【workflow】flowable-ui建模器应用程序-大于1000审批流程.mp4
    121.【workflow】flowable-ui建模器应用程序-流程图下载.mp4
    122.【workflow】flowable-ui建模器应用程序-创建流程应用.mp4
    123.【workflow】flowable-ui建模器应用程序-细节梳理.mp4
    124.【workflow】flowable-ui建模器应用程序-流程监控.mp4
    125.【workflow】flowable源码目录结构.mp4
    126.【workflow】flowable源码编译.mp4
    127.【workflow】flowable源码启动.mp4
    128.【workflow】flowable源码接入MySQL数据库.mp4
    129.【workflow】flowable源码接口分析.mp4
    130.【workflow】flowable添加用户.mp4
    131.【workflow】flowable修改和删除用户.mp4
    132.【workflow】flowable查询用户.mp4
    133.【workflow】flowable用户组的添加与删除.mp4
    134.【workflow】flowable用户组的更新与查询.mp4
    135.【workflow】flowable查看表详细信息.mp4
    136.【workflow】flowable流程自动部署.mp4
    137.【workflow】flowable流程自动升级.mp4
    138.【workflow】flowable修改流程定义的分类.mp4
    139.【workflow】flowable流程自动部署配置.mp4
    140.【workflow】flowable手动部署流程.mp4
    141.【workflow】flowable查询API.mp4
    142.【workflow】flowable自定义流程定义查询SQL.mp4
    143.【workflow】flowable自定义流程部署查询SQL.mp4
    144.【workflow】flowable删除流程定义.mp4
    145.【workflow】flowable流程实例与执行实例.mp4
    146.【workflow】flowable启动一个流程实例.mp4
    147.【workflow】flowable另一种流程启动方式.mp4
    148.【workflow】flowable流程执行.mp4
    149.【workflow】flowable判断流程是否执行结束.mp4
    150.【workflow】flowable查看运行活动节点.mp4
    151.【workflow】flowable删除流程实例.mp4
    156.【workflow】flowable中的租户.mp4
    157.【workflow】flowable中的ReceiveTask.mp4
    158.【workflow】UserTask直接指定处理人.mp4
    159.【workflow】UserTask委派或者自己处理.mp4
    160.【workflow】通过变量指定UserTask处理人.mp4
    161.【workflow】通过监听器指定UserTask处理人.mp4
    162.【workflow】设置UserTask处理人为流程发起人.mp4
    163.【workflow】设置UserTask候选人.mp4
    164.【workflow】UserTask认领任务.mp4
    165.【workflow】通过变量或者监听器为UserTask设置处理人.mp4
    166.【workflow】UserTask任务回退.mp4
    167.【workflow】UserTask候选人的添加与删除.mp4
    168.【workflow】UserTask按角色分配.mp4
    169.【workflow】UserTask通过变量设置角色.mp4
    170.【workflow】通过监听器配置ServiceTask.mp4
    171.【workflow】ServiceTask监听器类设置属性.mp4
    172.【workflow】ServiceTask委托表达式.mp4
    173.【workflow】ServiceTask表达式.mp4
    174.【workflow】脚本任务之JavaScript.mp4
    175.【workflow】脚本任务之Groovy.mp4
    176.【workflow】脚本任务之Juel.mp4
    177.【workflow】流程网关之排他网关.mp4
    178.【workflow】流程网关之并行网关.mp4
    179.【workflow】流程网关之包容网关.mp4
    180.【workflow】全局流程变量-启动时设置.mp4
    181.【workflow】全局流程变量-Task设置.mp4
    182.【workflow】全局流程变量-完成任务时设置.mp4
    183.【workflow】全局流程变量-通过执行实例设置.mp4
    184.【workflow】本地流程变量-1.mp4
    185.【workflow】本地流程变量-2.mp4
    186.【workflow】临时流程变量.mp4
    187.【workflow】流程历史信息-环境准备.mp4
    188.【workflow】流程历史信息-历史流程.mp4
    189.【workflow】流程历史信息-历史任务.mp4
    190.【workflow】流程历史信息-历史活动.mp4
    191.【workflow】流程历史信息-历史变量.mp4
    192.【workflow】流程历史信息-历史日志.mp4
    193.【workflow】流程历史信息-历史权限.mp4
    194.【workflow】流程历史信息-自定义SQL.mp4
    195.【workflow】流程历史信息-日志级别.mp4
    196.【workflow】流程定义定时激活.mp4
    197.【workflow】流程定义定时挂起.mp4
    198.【workflow】定时任务表分析.mp4
    199.【workflow】流程表单分类.mp4
    200.【workflow】动态表单定义.mp4
    201.【workflow】查询启动节点上的表单定义.mp4
    202.【workflow】启动带表单的实例.mp4
    203.【workflow】查询UserTask上的表单.mp4
    204.【workflow】动态表单的保存与完成.mp4
    205.【workflow】开发外置表单.mp4
    206.【workflow】部署带外置表单的流程.mp4
    207.【workflow】查看流程启动节点上的外置表单.mp4
    208.【workflow】带外置表单的流程审批.mp4
    209.【workflow】JSON格式的外置表单.mp4
    210.【workflow】根据流程定义绘制流程图.mp4
    211.【workflow】根据流程实例绘制流程图 Audio Extracted.pkf
    211.【workflow】根据流程实例绘制流程图 Audio Extracted.wav
    211.【workflow】根据流程实例绘制流程图.mp4
    212.【workflow】根据流程历史绘制流程图.mp4
    213.【workflow】综合实践-项目介绍.mp4
    214.【workflow】综合实践-绘制流程图.mp4
    215.【workflow】综合实践-用户体系问题.mp4
    216.【workflow】综合实践-工程创建.mp4
    217.【workflow】综合实践-创建用户表.mp4
    218.【workflow】综合实践-自定义用户登录.mp4
    219.【workflow】综合实践-服务类开发.mp4
    220.【workflow】综合实践-流程部署.mp4
    221.【workflow】综合实践-提交请假申请.mp4
    222.【workflow】综合实践-开发请假页面.mp4
    223.【workflow】综合实践-选择审批人.mp4
    224.【workflow】综合实践-提交请假申请.mp4
    225.【workflow】综合实践-待审批流程接口.mp4
    226.【workflow】综合实践-待审批流程页面.mp4
    227.【workflow】综合实践-添加流程实例ID.mp4
    228.【workflow】综合实践-返回流程实时进度.mp4
    229.【workflow】综合实践-展示流程实时进度.mp4
    230.【workflow】综合实践-当前用户待审批的任务.mp4
    231.【workflow】综合实践-网页展示待审批任务.mp4
    232.【workflow】综合实践-请假任务审批 Audio Extracted.pkf
    232.【workflow】综合实践-请假任务审批 Audio Extracted.wav
    232.【workflow】综合实践-请假任务审批.mp4
    233.【workflow】综合实践-查看请假历史接口.mp4
    234.【workflow】综合实践-页面展示历史请假.mp4
    235.【workflow】综合实践-查看流程进度图.mp4
    236.TienChin系统功能介绍.mp4
    237.配置系统菜单.mp4
    238.创建菜单页面.mp4
    239.引入MyBatisPlus.mp4
    240.渠道管理-表创建.mp4
    241.渠道管理-渠道类型.mp4
    242.渠道管理-工程创建.mp4
    243.渠道管理-查看渠道接口.mp4
    244.渠道管理-前端展示渠道信息.mp4
    245.渠道管理-配置字典常量.mp4
    246.渠道管理-字典原理分析.mp4
    247.渠道管理-权限分配.mp4
    248.渠道管理-添加渠道.mp4
    249.渠道管理-配置校验失败信息.mp4
    250.渠道管理-添加渠道页面开发.mp4
    251.渠道管理-更新渠道接口开发.mp4
    252.渠道管理-更新渠道.mp4
    253.渠道管理-删除渠道.mp4
    254.渠道管理-渠道搜索.mp4
    255.渠道管理-渠道分页.mp4
    256.渠道管理-渠道导出.mp4
    257.渠道管理-导入渠道弹框.mp4
    258.渠道管理-渠道导入.mp4
    259.渠道管理-渠道页面完善.mp4
    260.活动管理-准备工作.mp4
    261.活动管理-工程创建.mp4
    262.活动管理-活动列表展示.mp4
    263.活动管理-活动状态完善.mp4
    264.活动管理-添加活动接口.mp4
    265.活动管理-设置活动的默认状态.mp4
    266.活动管理-添加活动页面.mp4
    267.活动管理-完成添加活动.mp4
    268.活动管理-完善添加活动.mp4
    269.活动管理-修改活动接口.mp4
    270.活动管理-修改活动.mp4
    271.活动管理-删除活动.mp4
    272.活动管理-搜索活动.mp4
    273.活动管理-活动导出.mp4
    274.课程管理-数据表创建.mp4
    275.课程管理-创建工程.mp4
    276.课程管理-配置课程字典.mp4
    277.课程管理-展示课程列表.mp4
    278.课程管理-添加课程接口.mp4
    279.课程管理-添加课程页面.mp4
    280.课程管理-课程更新接口.mp4
    281.课程管理-课程更新页面.mp4
    282.课程管理-删除课程.mp4
    283.课程管理-课程搜索.mp4
    284.课程管理-课程导出.mp4
    285.线索管理-表创建.mp4
    286.线索管理-工程创建.mp4
    287.线索管理-线索录入接口.mp4
    288.线索管理-添加线索对话框.mp4
    289.线索管理-添加线索之渠道下拉框.mp4
    290.线索管理-添加线索对话框之活动信息.mp4
    291.线索管理-添加线索.mp4
    292.线索管理-线索摘要信息实体类.mp4
    293.线索管理-完善线索添加接口.mp4
    294.线索管理-线索数据接口.mp4
    295.线索管理-展示线索数据.mp4
    296.线索管理-设置下次跟进时间.mp4
    297.线索管理-线索分配页面.mp4
    298.线索管理-线索分配数据完善.mp4
    299.线索管理-线索分配页面完善.mp4
    300.线索管理-线索分配接口.mp4
    301.线索管理-线索分配接口数据校验.mp4
    302.线索管理-线索分配前后端对接.mp4
    303.线索管理-线索分配表单重置.mp4
    304.线索管理-线索分配表单校验.mp4
    305.线索管理-查看和跟进图标.mp4
    306.线索管理-创建线索详情页面.mp4
    307.线索管理-线索详情接口.mp4
    308.线索管理-线索详情接口完善.mp4
    309.线索管理-线索详情页面结构.mp4
    310.线索管理-线索详情数据请求.mp4
    311.线索管理-线索详情页面-1.mp4
    312.线索管理-线索详情页面-2.mp4
    313.线索管理-线索详情页面-3.mp4
    314.线索管理-前端线索跟进.mp4
    315.线索管理-线索跟进服务端.mp4
    316.线索管理-线索跟进完善.mp4
    317.线索管理-线索跟进记录接口.mp4
    318.线索管理-线索跟进记录页面.mp4
    319.线索管理-无效线索字典.mp4
    320.线索管理-无效线索弹框.mp4
    321.线索管理-无效线索前端数据校验.mp4
    322.线索管理-无效线索接口.mp4
    323.线索管理-无效线索前后端对接.mp4
    324.线索管理-无效线索接口完善.mp4
    325.线索管理-无效线索页面完善-1.mp4
    326.线索管理-无效线索页面完善-2.mp4
    327.线索管理-线索修改.mp4
    328.线索管理-线索修改数据校验.mp4
    329.线索管理-线索删除.mp4
    330.线索管理-线索搜索页面.mp4
    331.线索管理-线索搜索接口.mp4
    332.线索管理-线索搜索完成.mp4
    333.线索管理-线索跟进按钮.mp4
    334.线索管理-线索分配按钮.mp4
    335.商机管理-数据表设计.mp4
    336.商机管理-权限设计.mp4
    337.商机管理-模块创建.mp4
    338.商机管理-线索转商机.mp4
    339.商机管理-线索转商机页面.mp4
    340.商机管理-商机展示接口.mp4
    341.商机管理-商机展示页面.mp4
    342.商机管理-添加商机接口.mp4
    343.商机管理-添加商机页面.mp4
    344.商机管理-线索转商机完善.mp4
    345.商机管理-接口数据校验.mp4
    346.商机管理-商机分配.mp4
    347.商机管理-商机详情页面跳转.mp4
    348.商机管理-商机详情页面绘制.mp4
    349.商机管理-商机详情-客户资料.mp4
    350.商机管理-商机详情-客户意向.mp4
    351.商机管理-商机详情-沟通记录.mp4
    352.商机管理-商机详情-页面变量.mp4
    353.商机管理-商机详情-数据字典.mp4
    354.商机管理-商机详情-课程下拉框.mp4
    355.商机管理-商机详情-省市县下拉框.mp4
    356.商机管理-商机详情-数据展示.mp4
    357.商机管理-商机详情-数据展示-2.mp4
    358.商机管理-商机跟进.mp4
    359.商机管理-商机跟进数据校验.mp4
    360.商机管理-商机跟踪记录.mp4
    361.商机管理-前端删除无用方法.mp4
    362.商机管理-修改商机.mp4
    363.商机管理-BUG修复.mp4
    364.商机管理-删除商机.mp4
    365.商机管理-商机搜索.mp4
    366.合同管理-表创建.mp4
    367.合同管理-工程创建.mp4
    368.合同管理-字典和权限.mp4
    369.合同管理-项目引入flowable.mp4
    370.合同管理-绘制流程图并部署.mp4
    371.合同管理-添加合同页面.mp4
    372.合同管理-合同原件上传接口.mp4
    373.合同管理-合同原件上传页面.mp4
    374.合同管理-合同原件上传页面完善.mp4
    375.合同管理-合同原件删除.mp4
    376.合同管理-合同原件大小限制.mp4
    377.合同管理-合同审批部门.mp4
    378.合同管理-添加合同页面.mp4
    379.合同管理-添加合同页面完善.mp4
    380.合同管理-添加合同页面完善-2.mp4
    381.合同管理-服务端添加合同数据.mp4
    382.合同管理-根据手机号码自动补充用户名.mp4
    383.合同管理-根据手机号码自动补全信息.mp4
    384.合同管理-启动合同审批流程.mp4
    385.合同管理-查询未审批合同.mp4
    386.合同管理-查看合同详情.mp4
    387.合同管理- WORD转PDF.mp4
    388.合同管理-前端展示PDF.mp4
    389.合同管理-查询已提交任务列表.mp4
    390.合同管理-合同审批.mp4
    391.合同管理-单独标记驳回合同.mp4
    392.合同管理-展示已审批完成合同列表.mp4
    393.合同管理-完善已审批合同列表.mp4
    394.合同管理-修改合同前端展示.mp4
    395.合同管理-修改合同后端完成.mp4
    396.合同管理-补充信息.mp4
    397.统计分析-前端选项卡.mp4
    398.统计分析-前端引入vue-echarts.mp4
    399.统计分析-线索分析前端页面完善.mp4
    400.统计分析-返回线索增量统计数据.mp4
    401.统计分析-返回线索总量统计数据.mp4
    402.统计分析-线索分析前后端对接.mp4
    403.统计分析-线索分析表格数据展示.mp4
    404.统计分析-商机分析.mp4
    405.统计分析-渠道分析.mp4
    406.统计分析-渠道分析完善.mp4
    407.统计分析-活动分析.mp4
    408.转派管理.mp4
    【号外001】.登录流程解析.mp4
    【号外002】.分布式事务开篇.mp4
    【号外003】.分布式事务seata三个核心概念.mp4
    【号外004】.分布式事务seata四种事务模式.mp4
    【号外005】.分布式事务seata中的at模式.mp4
    【号外006】.分布式事务安装seata-server.mp4
    【号外007】.分布式事务seata-at模式实战.mp4
    【号外008】.分布式事务seata-at模式总结.mp4
    【号外009】.多数据源如何处理事务问题.mp4
    【号外010】.分布式事务seata-at模式补充.mp4
    【号外011】.分布式事务seata-tcc模式简介.mp4
    【号外012】.分布式事务seata-tcc模式实战-1.mp4
    【号外013】.分布式事务seata-tcc模式实战-2.mp4
    【号外014】.分布式事务seata-tcc模式实战-3.mp4
    【号外015】.分布式事务seata-tcc模式实战-4.mp4
    【号外016】.分布式事务seata-tcc总结.mp4
    【号外017】.分布式事务seata-xa简介.mp4
    【号外018】.MySQL中的XA事务实践.mp4
    【号外019】.分布式事务seata-xa模式实战-1.mp4
    【号外020】.分布式事务seata-xa模式实战-2.mp4
    【号外021】.分布式事务总结-1.mp4
    【号外022】.分布式事务总结-2.mp4
    【号外023】.动态代理的两种实现方式.mp4
    【号外024】.解决多数据源注解失效问题.mp4

2 RuoYi脚手架

2.1 自定义动态数据源

1.自定义一个注解 @DataSource,将来可以将该注解加在 service 层方法或者类上面,表示方法或者类中的所有方法都使用某一个数据源。
2.对于第一步,如果某个方法上面有 @DataSource  注解,那么就将该方法需要使用的数据源名称存入到 ThreadLocal。
3.自定义切面,在切面中解析 @DataSource  注解,当一个方法或者类上面有 @DataSource  注解的时候,将 @DataSource  注解所标记的数据源存入到 ThreadLocal 中。
4.最后,当 Mapper 执行的时候,需要 DataSource,他会自动去 AbstractRoutingDataSource 类中查找需要的数据源,我们只需要在 AbstractRoutingDataSource 中返回 ThreadLocal  中的值即可。

2.2 处理幂等性的思路

1.Token机制:
  a.首先客户端请求服务端,获取一个 token,每一次请求都获取到一个全新的 token(当然这个 token 会有一个超时时间),将 token 存入 redis 中,然后将 token 返回给客户端。
  b.客户端将来携带刚刚返回的 token 去请求一个接口。
  c.服务端收到请求后,分为两种情况:
    ⅰ.如果 token 在 redis 中,直接删除该 token,然后继续处理业务请求。
    ⅱ.如果 token 不在 redis 中,说明 token 过期或者当前业务已经执行过了,那么此时就不执行业务逻辑。
  d.优势:实现简单。
  e.劣势:多了一个获取 token 的过程。

2.去重表(主要是利用 MySQL 的唯一索引机制来实现的)
  a.客户端请求服务端,服务端将这次的请求信息(请求地址、参数。。。)存入到一个 MySQL 去重表中,这个去重表要根据这次请求的某个特殊字段建立唯一索引或者主键索引。
  b.判断是否插入成功:
    ⅰ.成功:继续完成业务功能。
    ⅱ.失败:表示业务已经执行过了,这次就不执行业务了。
  c.存在的问题:MySQL 的容错性会影响业务、高并发环境可能效率低。

3.用Redis的setnx
  a.客户端请求服务端,服务端将能代表本次请求唯一性的业务字段,通过 setnx 的方式存入 redis,并设置超时时间。
  b.判断 setnx 是否成功:
    ⅰ.成功:继续处理业务。
    ⅱ.失败:表示业务已经执行过了。

4.设置状态字段
  要处理的数据,有一个状态字段。

5.锁机制:
  a.乐观锁:数据库中增加版本号字段,每次更新都根据版本号来判断。更新之前先去查询要更新记录的版本号,第二步更新的时候,将版本号也作为查询条件。
    ⅰ.select version from xxx where id=xxx;
    ⅱ.update xxx set xxx=xxx where xx=xx and version=xxx。
  b.悲观锁:
    ⅰ.假设每一次拿数据都会被修改,所以直接上排他锁就行了。
    ⅱ.start;
       select * from xxx where xxx for update;
       update xxx
       commit;

2.3 登录问题梳理

1.登录的时候,会经过一个过滤器叫做 SecurityContextPersistenceFilter,当用户登录成功后,
会将用户信息存入 SecurityContextHolder 中(SecurityContextHolder 默认底层是将用户数据存入到 ThreadLocal 中的),
然后在登录请求结束的时候,在 SecurityContextPersistenceFilter 过滤器中,
会将 SecurityContextHolder 中的用户信息读取出来存入到 HttpSession 中。

2.以后每次用户发起请求的时候,都会经过 SecurityContextPersistenceFilter,
在这个过滤器中,系统会从 HttpSession 中读取出来当前登录的用户信息并存入 SecurityContextHolder 中。
接下来进行后续的业务处理,在后续的处理中,凡是需要获取当前用户信息的,都从 SecurityContextHolder 中直接获取。
当当前请求结束的时候,就会将 SecurityContextHolder 中的信息清除,下一次请求来的时候,重复步骤2。

2.4 自定义注解+AOP

在系统运行的时候,动态的向系统中添加代码的行为,就是面向切面编程 AOP。
● 前置通知:在目标方法执行之前执行。
● 后置通知:在目标方法执行之后执行。
● 异常通知:当目标方法抛出异常的时候执行。
● 返回通知:当目标方法有返回值的时候执行。
● 环绕通知:这是一个集大成者,包含了上面的四种情况。

在实际项目中,更多的是通过自定义注解+AOP解决各种项目问题:
1.事务处理。
2.接口限流处理:通过一个前置通知,在目标方法执行之前,统计目标方法在给定的时间窗内已经被调用了多少次了,如果超过流量限制,就禁止直行。
3.接口幂等性处理:通过一个前置通知,在目标方法执行之前,先去统计当前请求在给定的时间内是否已经执行过了,如果已经执行过了,那么本次就拒绝执行。
4.多数据源切换:通过一个前置通知,在目标方法执行之前,切换系统的数据源,这样,当目标方法执行的时候,就能够获取到切换之后的数据源了。
5.日志记录:通过一个返回通知或者异常通知,当目标方法执行出错的时候或者执行有返回值的时候,通过一个异步任务,将日志记录下来。
6.数据权限的处理:通过一个前置通知,在目标方法执行之前,添加 SQL 条件,这些条件最终会被添加到 SQL 语句中,进而实现数据过滤。

2.5 权限中的概念梳理

Permission 是一个个具体的权限,例如可以删除一个用户、可以添加一个用户等等,这些都是操作权限。
多个权限合并在一起,就是一个角色。
Shiro 中,当要控制权限的时候,框架本身中是有两个概念的:
● Role
● Permission

但是在 Spring Security 中,反映到代码上,并无明确的 Role 和 Permission:
● 当前用户类,要实现 UserDetails 接口,在这个接口中,如果要返回用户角色/权限的话,调用 getAuthorities 方法。此时,getAuthorities 方法究竟是返回角色还是返回权限呢?理论上来说,这里其实返回角色还是返回权限,都是 OK 的。但是由于角色是权限的集合,所以我们可以拿着用户的角色,去查询用户的权限,然后这个地方返回权限会更合理一些。
● 例如创建一个用户的时候,给用户设置角色还是设置权限,最终的都是调用同步一个方法,只是角色里边多了一个 ROLE_前缀而已。

以 Spring Security 官方的用户创建为例:
@Configuration
public class SecurityConfig {

    @Bean
    UserDetailsService us() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("javaboy").password("{noop}123")
                //给用户设置角色
                .roles("admin")
                //给 javaboy 设置两个权限,可以添加/删除用户的权限
                .authorities("system:user:add","system:user:delete")
                .build());
        return manager;
    }
}

在这里,虽然我们可以为用户设置 role 或者 权限,但是,在代码层面,这两个的区别仅仅只是 role 的字符串额外带有一个 ROLE_前缀。
当用户登录成功之后,我们去获取用户权限的时候,Spring Security 会自动根据权限和角色字符串的区别,给我们返回用户的权限(角色是不会返回的):

@RestController
public class HelloController {

    @GetMapping("/hello")
    public void hello() {
        Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        for (GrantedAuthority authority : authorities) {
            System.out.println("authority = " + authority);
            //authority = system:user:add
            //authority = system:user:delete
        }
    }
}

上面这个返回也是符合逻辑的。因为一般来说,权限才会控制用户具体的操作,角色一般是不控制用户具体的操作,角色仅仅只是用户权限的一个集合而已。

权限注解 @PreAuthorize("hasPermission('/add','system:user:add')")中,里边的 hasPermission('/add','system:user:add')实际上就是 SpEL 表达式,但是这个执行的方法没有指定这个方法是哪个对象中的方法,所以只有一种可能,这个方法是这里执行的 SpEL 的 RootObject 中的方法(SecurityExpressionRoot)。
system:user:*表示具备对用户的所有权限.
在 Spring Security 中,注解中,判断权限和判断角色的逻辑是一模一样的,唯一的区别在于角色有一个 ROLE 前缀,而权限没有这个前缀。

@Override
public final boolean hasAuthority(String authority) {
    return hasAnyAuthority(authority);
}
@Override
public final boolean hasAnyAuthority(String... authorities) {
    return hasAnyAuthorityName(null, authorities);
}
@Override
public final boolean hasRole(String role) {
    return hasAnyRole(role);
}
@Override
public final boolean hasAnyRole(String... roles) {
    return hasAnyAuthorityName(this.defaultRolePrefix, roles);
}
/**
无论是判断角色还是判断权限,最终调用的都是 hasAnyAuthorityName,区别主要在于第一个参数,判断
权限的时候,第一个参数为 null,因为权限没有前缀;判断第二个角色的时候,第一个参数有前缀,前缀
为 ROLE_。这是这两个唯一的区别。
*/
private boolean hasAnyAuthorityName(String prefix, String... roles) {
    Set<String> roleSet = getAuthoritySet();
    for (String role : roles) {
        String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
        if (roleSet.contains(defaultedRole)) {
            return true;
        }
    }
    return false;
}

2.6 登录、授权流程梳理

01.登录流程梳理
    a.登录请求
        登录请求,直接发送给登录接口 /login,具体的处理方法位于 org.javaboy.tienchin.web.controller.system.SysLoginController#login。
        a. 调用 `authenticationManager.authenticate` 方法执行登录操作。这个登录操作,最终会调用到 org.javaboy.tienchin.framework.web.service.UserDetailsServiceImpl#loadUserByUsername方法进行用户登录的认证,认证成功之后会返回一个 LoginUser,这个 LoginUser 中包含用户的基本信息,包括根据用户 id 从数据库中查询到的用户权限。
        b. 接下来创建登录令牌,所谓的另外,实际上是一个 JWT 字符串,具体的生成过程如下:
            ⅰ. 先获取一个经过处理的 UUID。
            ⅱ. 以 uuid 为 key,登录成功的用户 LoginUser 为 value,将之存储到 Redis 中。
            ⅲ. 生成一个 JWT 字符串,这个 JWT 字符串的内容就只有第一步获取到的 UUID。
    b.其他请求
        以后所有的登录之外的请求,只要需要认证,都会经过 org.javaboy.tienchin.framework.security.filter.JwtAuthenticationTokenFilter类,这个类核心功能就是根据用户登录时候的 JWT 字符串,去 Redis 中查询到登录用户对象,并存入到 SecurityContextHolder 中。
        1. 以后其他请求来的时候,必须携带上 JWT 字符串,携带方式就是将 JWT 字符串放入到请求头中,不携带的话,就认证不通过。
        2. 在 JwtAuthenticationTokenFilter 过滤器中,会直接进行 JWT 字符串的处理,根据 JWT 字符串解析出当前登录的用户,具体的处理逻辑在 `org.javaboy.tienchin.framework.web.service.TokenService#getLoginUser` 方法中:
          a. 先从请求头中提取出 JWT 字符串。
          b. 使用 JWT 解析这个 JWT 字符串。
          c. 根据解析后的  JWT 字符串,再提取出 JWT 中的 token,然后根据这个 token 去 redis 中查询到当前登录的用户对象。
          d. 用于用户对象存储在 Redis 中,有过期时间,这里拿到之后,刷新一下当前用户在 Redis 中的过期时间。
          e. 最后,将当前登录用户对象存入到 SecurityContextHolder 中。

02.鉴权流程梳理
    a.当用户登录成功的时候,就已经把用户的权限信息保存到 LoginUser 中了,当每次请求到达的时候,都会在 JwtAuthenticationTokenFilter 过滤器中,
        重新获取到用户的基本信息(包括用户的权限)存入到 SecurityContextHolder 中。
    b.以后,用户访问某一个接口,或者某一个方法的时候,我们需要进行权限控制的时候,直接通过 @PreAuthorize("hasPermission('system:dict:export')")注解去执行即可,
        这个注解中,会获取到当前用户的角色信息,并和需要的角色信息进行比对。

2.7 动态菜单加载思路

整体思路
    1.当用户登录成功之后,前端会自动发送一个请求,去后端查询当前这个登录成功的用户的动态菜单。具体的思路就是根据当前登录成功的用户 id,去 sys_user_role 表中查询到这个用户的角色 id,然后在根据角色 id 去 sys_role_menu 表中查询到菜单 id,然后再根据菜单 id 去 sys_menu 表中查询到具体的菜单数据。
    2.前端定义了一个前置路由导航守卫,当发生页面跳转的时候,这个路由导航守卫会监听到所有的页面跳转,监听到之后,会去检查是否需要服务端返回的动态菜单数据,如果需要的话,就去服务端加载,加载到之后,渲染侧边栏菜单,同时将菜单项都加入到 router 中。

服务端动态菜单JSON
    1.去数据看中查询菜单数据
        /**
         * 根据用户ID查询菜单
         *
         * @param userId 用户名称
         * @return 菜单列表
         */
        @Override
        public List<SysMenu> selectMenuTreeByUserId(Long userId) {
            List<SysMenu> menus = null;
            if (SecurityUtils.isAdmin(userId)) {
                menus = menuMapper.selectMenuTreeAll();
            } else {
                menus = menuMapper.selectMenuTreeByUserId(userId);
            }
            return getChildPerms(menus, 0);
        }
        如果当前用户是 admin,说明是超级管理员,那么此时直接查询所有的菜单数据即可。
        如果不是 admin,那么就根据当前登录用户的 id 去查询菜单数据即可。
        menus 是查询到的没有进行菜单层级处理的一个集合。
        getChildPerms 方法则是通过一个递归操作,将菜单的层级管理给建立起来。
    2.将查到的菜单数据,构造为前端需要的JSON格式
        /**
         * 构建前端路由所需要的菜单
         *
         * @param menus 菜单列表
         * @return 路由列表
         */
        @Override
        public List<RouterVo> buildMenus(List<SysMenu> menus) {
            List<RouterVo> routers = new LinkedList<RouterVo>();
            for (SysMenu menu : menus) {
                RouterVo router = new RouterVo();
                router.setHidden("1".equals(menu.getVisible()));
                router.setName(getRouteName(menu));
                router.setPath(getRouterPath(menu));
                router.setComponent(getComponent(menu));
                router.setQuery(menu.getQuery());
                router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
                List<SysMenu> cMenus = menu.getChildren();
                if (!cMenus.isEmpty() && cMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType())) {
                    router.setAlwaysShow(true);
                    router.setRedirect("noRedirect");
                    router.setChildren(buildMenus(cMenus));
                } else if (isMenuFrame(menu)) {
                    router.setMeta(null);
                    List<RouterVo> childrenList = new ArrayList<RouterVo>();
                    RouterVo children = new RouterVo();
                    children.setPath(menu.getPath());
                    children.setComponent(menu.getComponent());
                    children.setName(StringUtils.capitalize(menu.getPath()));
                    children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
                    children.setQuery(menu.getQuery());
                    childrenList.add(children);
                    router.setChildren(childrenList);
                } else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) {
                    router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
                    router.setPath("/");
                    List<RouterVo> childrenList = new ArrayList<RouterVo>();
                    RouterVo children = new RouterVo();
                    String routerPath = innerLinkReplaceEach(menu.getPath());
                    children.setPath(routerPath);
                    children.setComponent(UserConstants.INNER_LINK);
                    children.setName(StringUtils.capitalize(routerPath));
                    children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
                    childrenList.add(children);
                    router.setChildren(childrenList);
                }
                routers.add(router);
            }
            return routers;
        }
        从这段代码中,可以看出来,返回的动态菜单 JSON 一共分为了四种情况。
        常规数据:
        1.可见性
        2.name:正常来说,name 都是 path 首字母大写;特殊情况就是如果当前类型为 C 并且是一级菜单而且还不是外链,这个时候会自动为这个菜单项生成一个 parent,这个 parent 的 name 为空字符串。
        3.path:
          a. 不是一级菜单,并且还是一个内链打开外网(在当前系统中,新开一个选项卡,打开外部链接):去除掉 path 中的 http 或者 https 即可。
          b. 非外链并且是一级目录并且是 M 类型,此时 path 就在数据库查出来的 path 前加上 /。
          c. 非外链并且是一级目录并且是 C 类型,此时 path 就是 /。
          d. 其他情况就直接返沪菜单项即可。
          e. 对于正常的菜单数据而言,parent 实际上就是走第二个 if,children 实际上是不会进入到任何分支中,直接返回的。
        4.component:
          a. Layout:项目的主页面。
          b. Inner_Link:展示外链的组件。
          c. Parent_View:如果一个二级菜单还有子菜单,那么这个二级菜单对应的 component 就是 Parent_View。
          d. 首先定义了一个默认的 component,就是 Layout。
          e. 如果当前菜单项有 component,并且当前菜单项还不是一个内部跳转的菜单,那么这个 component 就是从数据库中实际查询到的 component。
          f. 如果自己没有 component,并且还不是一级菜单,并且还是一个内链(想在当前系统中打开外部链接),设置默认的组件为 Inner_Link。
          g. 如果自己没有 component,并且还有子菜单,那么就设置当前菜单的 component 为 Parent_View。
        5.query、meta 都按实际情况来。
        6.如果当前菜单有 children,那么递归处理 children。
        7.如果当前菜单是 C 类型的,并且还不是外链还是一级菜单,那么就自动给这个菜单项加一个 children(menu_type 中的第一种情况)。
        8.如果是一级菜单并且想在内部选项卡中展示一个外部页面,对应 is_frame 中的第一种情况,也就是会自动生成一个 children。
        9.三个分支都没进来,说明就没有 children,is_frame 中的第二种情况,menu_type 中的第二种情况。

2.8 Vue3中的数据加载

TienChin中的动态菜单加载:
    1.用户数据以及动态菜单数据,都是保存在 Pinia 中,Pinia 中的数据特点,就是当用户按了浏览器刷新按钮或者 F5 按钮之后,Pinia 中的数据会丢失。所以,我们需要确保当用户按了浏览器刷新按钮之后,要主动的重新加载一次动态菜单数据。
    2.加载的思路,就是利用全局前置导航守卫,当发生页面跳转的时候,通过全局前置导航守卫可以监听到所有的页面跳转,监听到之后,先去判断 Pinia 中的菜单数据是否还在,如果还在,说明当前跳转就是一次普普通通的页面跳转,否则说明用户是按了 F5 进行的页面跳转,那么此时就要先去加载浏览器的菜单数据了。

为什么要把菜单数据存到Pinia/Vuex中?
    不能存到 sessionStorage 或者 localStorage 中吗?
    首先要明白,服务端返回的动态菜单实际上是有两方面的作用:
    1.渲染左侧的菜单栏,这个好说,哪怕你把菜单数据存到 sessionStorage、localStorage 或者是 Cookie 中,将来都是可以渲染出来的。
    2.添加到路由中,这个路由,说白了,这个路由其实是一个内存对象,从服务端加载到菜单数据之后,我们会将菜单数据动态的加到 router 中,这样,点击左边的菜单项,右边才会进行页面跳转,但是,这个 router 中的数据,是保存在内存中的。这就意味着,如果用户点击了浏览器刷新按钮,或者用户点击了 F5 按钮,都会导致 router 中的数据丢失。如果 router 中的数据丢失了,那么就无法进行页面跳转了
    所以说白了,其实就是 router 这个对象,限制了动态菜单数据,他要求必须在浏览器刷新之后(或者按 F5 之后)重新加载一次动态菜单,否则,浏览器刷新之后,router 中没东西了,没东西就没法进行页面跳转了。

假设动态菜单存到sessionStorage中:
    1.动态菜单渲染是没问题的。
    2.router:按了 F5 之后,router 中的数据就没了,此时虽然 sessionStorage 中有数据,但是 router 中没有数据,跳转不了。那么能不能把 sessionStorage 中的数据拿出来放到 router 中呢?想拿是可以的!但是有一个问题:什么时候拿?
      a.监听浏览器的刷新事件。
      b.在导航守卫里边去处理。

2.9 Promise

01.回调地狱
    a.介绍
        登录->获取用户信息->获取用户菜单。
    b.如果用 Ajax 来写,伪代码如下:
        $.ajax({
            url:'/login',
            data:loginForm,
            success:function (data) {
                //登录成功
                $.ajax({
                    url:'/getInfo',
                    success:function (data) {
                        //获取用户信息成功
                        $.ajax({
                            url:'/getMenus',
                            success:function (data) {
                                //获取动态菜单成功
                            }
                        })
                    }
                })
            }
        })
        -----------------------------------------------------------------------------------------------------
        原生的 Ajax,异步任务执行的逻辑和处理的逻辑写在了一起,导致如果有多个网络请求,并且多个请求存在依赖关系,就会造成回调地狱。
        我们希望能够将异步任务执行的代码和处理的代码分离开,能够实现这一需求的工具就是 Promise。

02.Promise
    a.介绍
        登录->获取用户信息->获取用户菜单。
    b.代码
        /**
         * 登录
         * @param resolve
         * @param reject
         */
        function login(resolve, reject) {
            setTimeout(() => {
                //生成一个随机数
                let number = Math.random();
                if (number > 0.5) {
                    //登录成功,利用 resolve 函数将登录成功的结果扔出去
                    resolve("login success");
                } else {
                    //登录失败,利用 reject 函数将登录失败的结果扔出去
                    reject("login error")
                }
            }, 1000);
        }
        function getInfo(resolve, reject) {
            setTimeout(() => {
                //生成一个随机数
                let number = Math.random();
                if (number > 0.5) {
                    //登录成功,利用 resolve 函数将登录成功的结果扔出去
                    resolve("getInfo success");
                } else {
                    //登录失败,利用 reject 函数将登录失败的结果扔出去
                    reject("getInfo error")
                }
            }, 1000);
        }
        function getMenus(resolve, reject) {
            setTimeout(() => {
                //生成一个随机数
                let number = Math.random();
                if (number > 0.5) {
                    //登录成功,利用 resolve 函数将登录成功的结果扔出去
                    resolve("getMenus success");
                } else {
                    //登录失败,利用 reject 函数将登录失败的结果扔出去
                    reject("getMenus error")
                }
            }, 1000);
        }
        new Promise(login).then(data => {
            //如果 login 将来执行成功(就是 resolve 方法抛出执行结果的时候)
            console.log("login:", data);
            return new Promise(getInfo);
        }).then(data=>{
            //getInfo 执行成功,会进入到这里
            console.log("getInfo:", data);
            return new Promise(getMenus);
        }).then(data=>{
            console.log("getMenus:", data);
        }).catch(err=>{
            //如果上面任意一个出异常了,都会进入到最近的 catch 代码块中
            console.error("err:", err);
        })
    c.说明
        Promise 中,执行成功,就通过 resolve 将成功的结果抛出去;执行失败,就通过 reject 将失败的结果抛出去。在 Promise 中,只管抛出执行结果即可。
        如果是 resolve 执行了,则进入到接下来的 then 中对异步任务的执行结果进行处理;如果是 reject 执行了,则进入到 catch 代码块中对异常的结果进行处理。

03.then
    a.then 方法参数
        then 方法中,可以传递两个回调函数,第一个是成功的回调函数,第二个是失败的回调函数,如果没有传递第二个参数,那么可以在 catch 中处理失败。
        -----------------------------------------------------------------------------------------------------
        /**
         * 登录
         * @param resolve
         * @param reject
         */
        function login(resolve, reject) {
            setTimeout(() => {
                //生成一个随机数
                let number = Math.random();
                if (number > 0.5) {
                    //登录成功,利用 resolve 函数将登录成功的结果扔出去
                    resolve("login success");
                } else {
                    //登录失败,利用 reject 函数将登录失败的结果扔出去
                    reject("login error")
                }
            }, 1000);
        }
        new Promise(login).then(data => {
            //如果 login 将来执行成功(就是 resolve 方法抛出执行结果的时候)
            console.log("login:", data);
        },err=>{
            console.error("err:", err);
        })
    b.then 方法返回值
        a.返回 Promise
            前面的例子,返回的就是 Promise。这样的话,下一个 then 他的主语其实就是上一个 then 返回的 Promise。
            -------------------------------------------------------------------------------------------------
            A.then(data=>{xxxx;return B}).then(data=>{xxx})
            由于在 A 的 then 中返回了 B,所以第二个 then 的主语,其实是 B,不是 A(假设 A 和 B 都是 Promise 对象)。
        b.返回字符串
            then 方法中可以就返回一个字符串,此时可以一直 then 下去,所有的 then 方法此时都是同一个主语。
            -------------------------------------------------------------------------------------------------
            /**
             * 登录
             * @param resolve
             * @param reject
             */
            function login(resolve, reject) {
                setTimeout(() => {
                    //生成一个随机数
                    let number = Math.random();
                    if (number > 0.5) {
                        //登录成功,利用 resolve 函数将登录成功的结果扔出去
                        resolve("login success");
                    } else {
                        //登录失败,利用 reject 函数将登录失败的结果扔出去
                        reject("login error")
                    }
                }, 1000);
            }
            new Promise(login).then(data => {
                //如果 login 将来执行成功(就是 resolve 方法抛出执行结果的时候)
                console.log("then1:", data);
                return data;
            }).then(data=>{
                console.log("then2:", data);
                return data;
            }).then(data=>{
                console.log("then3:", data);
            }).catch(err=>{
                console.error("err:", err);
            })
            -------------------------------------------------------------------------------------------------
            此时,三个 then 都是同一个主语,因为在 then 中没有返回新的 Promise 对象。每一个 then 的参数,都是上一个 then 的返回值。
        c.抛出异常
            then 方法中也可以抛出异常。
            -------------------------------------------------------------------------------------------------
            /**
             * 登录
             * @param resolve
             * @param reject
             */
            function login(resolve, reject) {
                setTimeout(() => {
                    //生成一个随机数
                    let number = Math.random();
                    if (number > 0.5) {
                        //登录成功,利用 resolve 函数将登录成功的结果扔出去
                        resolve("login success");
                    } else {
                        //登录失败,利用 reject 函数将登录失败的结果扔出去
                        reject("login error")
                    }
                }, 1000);
            }
            new Promise(login).then(data => {
                //如果 login 将来执行成功(就是 resolve 方法抛出执行结果的时候)
                console.log("then1:", data);
                return data;
            }).then(data=>{
                console.log("then2:", data);
                return data;
            }).then(data=>{
                throw new Error("出错啦");
            }).catch(err=>{
                console.error("err:", err);
            })
            -------------------------------------------------------------------------------------------------
            then 中还可以直接抛出异常,抛出异常就进入到 catch 中了。

04.catch
    a.介绍
        catch 并不是必须的,也可以直接在 then 中写两个回调函数,
        第二个回调函数就是用来处理 catch 的,不过更多情况下,都是单独写 catch 的。
    b.进入到 catch 中,分为两种情况:
        1. Promise 在执行的过程中,通过 reject 返回数据。
        2. then 中抛出异常。
        进入 catch 的时候,都是就近进入。

05.finally
    a.介绍
        无论是最终执行了 then 还是 catch,反正都会进入到 finally 代码块中:
        /**
         * 登录
         * @param resolve
         * @param reject
         */
        function login(resolve, reject) {
            setTimeout(() => {
                //生成一个随机数
                let number = Math.random();
                if (number > 0.5) {
                    //登录成功,利用 resolve 函数将登录成功的结果扔出去
                    resolve("login success");
                } else {
                    //登录失败,利用 reject 函数将登录失败的结果扔出去
                    reject("login error")
                }
            }, 1000);
        }
        new Promise(login).then(data => {
            //如果 login 将来执行成功(就是 resolve 方法抛出执行结果的时候)
            console.log("then1:", data);
            return data;
        }).then(data=>{
            console.log("then2:", data);
            return data;
        }).then(data=>{
            // throw new Error("出错啦");
        }).catch(err=>{
            console.error("err1:", err);
        }).finally(()=>{
            console.log("over!")
        })
    b.区别
        这个类似于 Java 中的 finally,就是前面无论是 catch 还是 then,反正 finally 都会执行。
        不过不同于 Java 中的 finally,前端的 finally 执行完成之后,还可以继续 then:
        -----------------------------------------------------------------------------------------------------
        /**
         * 登录
         * @param resolve
         * @param reject
         */
        function login(resolve, reject) {
            setTimeout(() => {
                //生成一个随机数
                let number = Math.random();
                if (number > 0.5) {
                    //登录成功,利用 resolve 函数将登录成功的结果扔出去
                    resolve("login success");
                } else {
                    //登录失败,利用 reject 函数将登录失败的结果扔出去
                    reject("login error")
                }
            }, 1000);
        }
        new Promise(login).then(data => {
            //如果 login 将来执行成功(就是 resolve 方法抛出执行结果的时候)
            console.log("then1:", data);
            return data;
        }).then(data=>{
            console.log("then2:", data);
            return data;
        }).then(data=>{
            // throw new Error("出错啦");
        }).catch(err=>{
            console.error("err1:", err);
        }).finally(()=>{
            console.log("over!")
        }).then(()=>{
            console.log("========")
        })

06.静态方法
    a.Promise.resolve()
        返回一个带有给定值解析后的 Promise 对象:
        -----------------------------------------------------------------------------------------------------
        let p1 = Promise.resolve("hello promise!");
        p1.then(data=>{
            console.log('data', data);
        }).catch(err=>{
            //除非 then 中抛出异常,就会进入到 catch 中,否则不会进入到 catch 中
        })
    b.Promise.reject()
        返回一个带有 reject 原因的 Promise 对象:
        -----------------------------------------------------------------------------------------------------
        function resolved() {
            console.log("resolved")
        }
        function rejected(err) {
            console.log("err:", err);
        }
        let p1 = Promise.reject("出错啦");
        // p1.then(resolved, rejected);
        p1.then(resolved).catch(rejected);
        -----------------------------------------------------------------------------------------------------
        这个时候,p1 只会进入到 catch 中。
    c.Promise.all()
        这个静态方法接受多个 Promise 对象,并且只返回一个 Promise 实例,all 方法中会传入多个 Promise 实例,他会等所有的 Promise 对象都 resolve 之后,才会进入到 then 中,否则只要参数中的 Promise 中有一个对象 reject 了,就会进入到 catch 中。
        简而言之一句话,所有的 Promise 都成功,进入 then,有一个 Promise 失败,进入 catch。
        -----------------------------------------------------------------------------------------------------
        let p1 = Promise.resolve('javaboy');
        let p2 = 88;
        let p3 = new Promise((resolve, reject) => {
            setTimeout(resolve, 3000, "hello javaboy");
        })
        Promise.all([p1,p2,p3]).then(data=>{
            // p1\p2\p3 都是 resolve,就会进入到 then 中
            console.log("data:", data);
        }).catch(err=>{
            //p1\p2\p3 有一个 reject,就会进入到 catch 中
            console.error("err:", err);
        })
        all 方法能够帮助我们确保多个异步任务同时执行成功。
    d.Promise.race()
        这个也可以接受多个 Promise 对象,一旦参数中的 Promise 对象 resolve 或者 reject,就会进入到 resolve 或者 reject 中。
        简而言之,参数中只要有一个执行成功或者失败,就 OK,不会等其他的 Promise 了,其他的执行慢的 Promise 对象就直接被抛弃了。
        let p1 = new Promise((resolve, reject) => {
            setTimeout(reject, 300, "one");
        })
        let p2 = new Promise((resolve, reject) => {
            setTimeout(resolve, 200, "two");
        })
        Promise.race([p1,p2]).then(data=>{
            console.log("data:", data);
        }).catch(err=>{
            console.error("err:", err);
        })
        由于 p2 执行的快,所以最终进入到 then 中的就是 p2,p1 被舍弃了。

2.10 Vue3和Vue2的差异

01.变量定义
    a.例如 Vue2 中,我们定义变量,可以按照如下方式:
        <template>
            <div>hello 01!</div>
            <h1>{{msg}}</h1>
        </template>

        <script>
            export default {
                name: "My01",
                data(){
                    return{
                        msg: "hello javaboy!"
                    }
                }
            }
        </script>

        <style scoped>

        </style>
    b.以上代码,在 Vue3 中,可以按照如下方式来写:
        <template>
            <div>
                <div>hello 01!</div>
                <h1>{{msg}}</h1>
                <input type="text" v-model="msg">
            </div>
        </template>

        <script>

            import {ref} from 'vue';

            export default {
                name: "My02",
                /**
                 * 我们以前在 Vue2 中定义的各种变量、方法、生命周期钩子函数等等,现在统一都在 setup 中进行定义
                 *
                 * 需要注意的是,所有定义的变量,方法等,都需要返回之后才可以使用
                 */
                setup() {
                    //注意,直接这样写,这个变量不是响应式数据
                    // let msg = "hello vue3";
                    let msg = ref("hello vue3");
                    return {msg};
                }
            }
        </script>

        <style scoped>

        </style>
    c.两个要点:
        1.变量定义,需要用到 ref 函数,该函数,直接从 vue 中导入,否则直接定义的变量不具备响应式的特性。
        2.所有定义的变量、方法等,都需要 return,不 return,用不了。

02.方法定义
    a.介绍
        在 Vue2 中,我们一般是将方法定义在 methods 节点中,但是在 Vue3 中,我们将方法定义在 setup 方法中,
        尤其要注意,方法定义完成之后,必须要返回方法名,否则方法用不了。
    b.代码
        <template>
            <div>
                <div>hello 01!</div>
                <h1>{{msg}}</h1>
                <input type="text" v-model="msg">
                <button @click="doLogin('zhangsan','123')">登录</button>
            </div>
        </template>

        <script>

            import {ref} from 'vue';

            export default {
                name: "My02",
                /**
                 * 我们以前在 Vue2 中定义的各种变量、方法、生命周期钩子函数等等,现在统一都在 setup 中进行定义
                 *
                 * 需要注意的是,所有定义的变量,方法等,都需要返回之后才可以使用
                 */
                setup() {
                    //注意,直接这样写,这个变量不是响应式数据
                    // let msg = "hello vue3";
                    let msg = ref("hello vue3");
                    const doLogin=(username,password)=>{
                        console.log(username);
                        console.log(password);
                    }
                    return {msg,doLogin};
                }
            }
        </script>

        <style scoped>

        </style>
    c.说明
        如上,像定义一个变量一样去定义方法,方法定义完成之后,一定要返回。

03.钩子函数
    a.在 Vue2 中,定义钩子函数,直接定义对应的方法名即可:
        <template>
            <div>hello 01!</div>
            <h1>{{msg}}</h1>
        </template>

        <script>
            export default {
                name: "My01",
                data(){
                    return{
                        msg: "hello javaboy!"
                    }
                },
                mounted() {
                    console.log("=====Vue2=====mounted()==========")
                }
            }
        </script>

        <style scoped>

        </style>
    b.但是,在 Vue3 中,由于所有的东西都是在 setup 中定义的,包括钩子函数。
        Vue3 中定义钩子函数:
        <template>
            <div>
                <div>hello 01!</div>
                <h1>{{msg}}</h1>
                <input type="text" v-model="msg">
                <button @click="doLogin('zhangsan','123')">登录</button>
            </div>
        </template>

        <script>

            import {ref} from 'vue';
            //使用钩子函数时,首先导入钩子函数
            import {onMounted} from 'vue';

            export default {
                name: "My02",
                /**
                 * 我们以前在 Vue2 中定义的各种变量、方法、生命周期钩子函数等等,现在统一都在 setup 中进行定义
                 *
                 * 需要注意的是,所有定义的变量,方法等,都需要返回之后才可以使用
                 */
                setup() {
                    //注意,直接这样写,这个变量不是响应式数据
                    // let msg = "hello vue3";
                    let msg = ref("hello vue3");
                    const doLogin=(username,password)=>{
                        console.log(username);
                        console.log(password);
                    }
                    //调用钩子函数,并传入回调函数
                    //另外需要注意,这个钩子函数不需要返回
                    onMounted(()=>{
                        console.log("My02 初始化了。。。")
                    })
                    return {msg,doLogin};
                }
            }
        </script>

        <style scoped>

        </style>
    c.说明
        首先从 vue 中导入钩子函数。
        在 setup 方法中去定义钩子函数的逻辑。
        在 return 中,不需要返回钩子函数。
    d.钩子函数对照:
        Vue2            Vue3
        mounted         onMounted
        beforeUpdate    onBeforeUpdate
        updated         OnUpdated
        beforeUnmount   OnBeforeUnmounted
        unmounted       OnUnmounted
        errorCapture    OnErrorCapture
        renderTracked   OnRenderTracked
        renderTriggered OnRenderTriggered
        activated       OnActivated
        deactivated     OnDeactivated

04.computed 属性
    a.介绍
        计算属性和钩子函数比较类似,计算属性使用步骤:
        1. 从 vue 中导入计算属性函数。
        2. 定义计算属性。
        3. 在 return 中返回计算属性值。
    b.代码
        <template>
            <div>
                <div>hello 01!</div>
                <h1>{{msg}}</h1>
                <input type="text" v-model="msg">
                <button @click="doLogin('zhangsan','123')">登录</button>
                <div>{{currentTime}}</div>
            </div>
        </template>

        <script>

            import {ref} from 'vue';
            //使用钩子函数时,首先导入钩子函数
            //计算属性的使用,也需要首先导入计算属性
            import {onMounted,computed} from 'vue';

            export default {
                name: "My02",
                /**
                 * 我们以前在 Vue2 中定义的各种变量、方法、生命周期钩子函数等等,现在统一都在 setup 中进行定义
                 *
                 * 需要注意的是,所有定义的变量,方法等,都需要返回之后才可以使用
                 */
                setup() {
                    //注意,直接这样写,这个变量不是响应式数据
                    // let msg = "hello vue3";
                    let msg = ref("hello vue3");
                    const doLogin=(username,password)=>{
                        console.log(username);
                        console.log(password);
                    }
                    //调用钩子函数,并传入回调函数
                    //另外需要注意,这个钩子函数不需要返回
                    onMounted(()=>{
                        console.log("My02 初始化了。。。")
                    })
                    //现在就可以通过计算属性去定义一个变量了
                    const currentTime=computed(()=>{
                        return Date.now();
                    })
                    //注意,计算属性需要在 return 中返回
                    return {msg,doLogin,currentTime};
                }
            }
        </script>

        <style scoped>

        </style>
    c.计算属性更新的例子:
        <template>
            <div>
                <div>hello 01!</div>
                <h1>{{msg}}</h1>
                <input type="text" v-model="msg">
                <button @click="doLogin('zhangsan','123')">登录</button>
                <div>{{currentTime}}</div>
            </div>
        </template>

        <script>

            import {ref} from 'vue';
            //使用钩子函数时,首先导入钩子函数
            //计算属性的使用,也需要首先导入计算属性
            import {onMounted,computed} from 'vue';

            export default {
                name: "My02",
                /**
                 * 我们以前在 Vue2 中定义的各种变量、方法、生命周期钩子函数等等,现在统一都在 setup 中进行定义
                 *
                 * 需要注意的是,所有定义的变量,方法等,都需要返回之后才可以使用
                 */
                setup() {
                    //注意,直接这样写,这个变量不是响应式数据
                    // let msg = "hello vue3";
                    let msg = ref("hello vue3");
                    let age = ref(99);
                    const doLogin=(username,password)=>{
                        console.log(username);
                        console.log(password);
                        age.value++;
                        msg.value = 'hello javaboy!';
                    }
                    //调用钩子函数,并传入回调函数
                    //另外需要注意,这个钩子函数不需要返回
                    onMounted(()=>{
                        console.log("My02 初始化了。。。")
                    })
                    //现在就可以通过计算属性去定义一个变量了
                    const currentTime=computed(()=>{
                        age.value++;
                        return Date.now();
                    })
                    //注意,计算属性需要在 return 中返回
                    return {msg,doLogin,currentTime,age};
                }
            }
        </script>

        <style scoped>

        </style>
    d.说明
        由于生成计算属性 currentTime 依赖 age 变量,所以当 age 变量发生变化的时候,计算属性会自动更新,否则计算属性将一直使用缓存中的数据(age 没有发生变化的情况)。
        另外还有一点,就是定义的变量入 age、msg 等 ,在 HTML 节点中,直接使用 age、msg,但是如果是在方法中操作这些变量,则一定要使用 age.value 或者 msg.value 去操作这些变量。

05.watch 函数
    a.Vue3 中,watch 函数的写法:
        <template>
            <div>
                <div>hello 01!</div>
                <h1>{{msg}}</h1>
                <input type="text" v-model="msg">
                <button @click="doLogin('zhangsan','123')">登录</button>
                <div>{{currentTime}}</div>
            </div>
        </template>

        <script>

            import {ref} from 'vue';
            //使用钩子函数时,首先导入钩子函数
            //计算属性的使用,也需要首先导入计算属性
            import {onMounted,computed,watch} from 'vue';

            export default {
                name: "My02",
                /**
                 * 我们以前在 Vue2 中定义的各种变量、方法、生命周期钩子函数等等,现在统一都在 setup 中进行定义
                 *
                 * 需要注意的是,所有定义的变量,方法等,都需要返回之后才可以使用
                 */
                setup() {
                    //注意,直接这样写,这个变量不是响应式数据
                    // let msg = "hello vue3";
                    let msg = ref("hello vue3");
                    let age = ref(99);
                    const doLogin=(username,password)=>{
                        console.log(username);
                        console.log(password);
                        age.value++;
                        msg.value = 'hello javaboy!';
                    }
                    //调用钩子函数,并传入回调函数
                    //另外需要注意,这个钩子函数不需要返回
                    onMounted(()=>{
                        console.log("My02 初始化了。。。")
                    })
                    //现在就可以通过计算属性去定义一个变量了
                    const currentTime=computed(()=>{
                        age.value++;
                        return Date.now();
                    })
                    watch(age,(newValue,oldValue)=>{
                        console.log("newValue", newValue);
                        console.log("oldValue", oldValue);
                    })
                    //注意,计算属性需要在 return 中返回
                    return {msg,doLogin,currentTime,age};
                }
            }
        </script>

        <style scoped>

        </style>
    b.说明
        1.先从 vue 中导入 watch 函数。
        2.在 setup 中去监控变量,第一个参数是要监控的变量,第二个参数则是一个回调函数,回调函数的参数就是所监控变量值的变化。

06.ref 和 reactive
    a.代码
        <template>
            <div>
                <div>{{age}}</div>
                <div>{{book.name}}</div>
                <div>{{book.author}}</div>
            </div>
        </template>

        <script>

            import {ref, reactive} from 'vue';

            export default {
                name: "My03",
                setup() {
                    const age = ref(99);
                    const book = reactive({
                        name: "三国演义",
                        author: '罗贯中'
                    });
                    return {age,book};
                }
            }
        </script>

        <style scoped>

        </style>
    b.说明
        一般来说,我们通过 ref 来定义一个变量,一般都是原始数据类型,例如 String、Number、BigInt、Boolean 等,通过 reactive 来定义一个对象。
        如上面的案例所示,定义了 book 对象之后,接下来的访问中,通过 book.xxx 就可以访问到 book 中的属性值了。
        但是,假设我们在定义的时候,定义的是 book 对象,但是我们访问的时候,却希望能够直接按照属性来访问,此时可以直接展开变量,但是,如果直接通过三个点去展开变量,会导致变量的响应式特点失效,此时,我们可以通过 toRefs 函数,让变量恢复响应式的特点:
        <template>
            <div>
                <div>{{age}}</div>
                <div>{{name}}</div>
                <div>{{author}}</div>
                <button @click="updateBookInfo">更新图书信息</button>
            </div>
        </template>

        <script>

            import {ref, reactive,toRefs} from 'vue';

            export default {
                name: "My03",
                setup() {
                    const age = ref(99);
                    const book = reactive({
                        name: "三国演义",
                        author: '罗贯中'
                    });
                    const updateBookInfo = ()=>{
                        //修改书名,注意,在 vue3 中,现在方法中访问变量,不再需要 this
                        book.name = '三国演义123';
                    }
                    //如果直接这样写,会导致响应式失效,通过 toRefs 函数,可以解决这个问题
                    return {age,...toRefs(book),updateBookInfo};
                }
            }
        </script>

        <style scoped>

        </style>
    c.总结:
        1. ref 定义原始数据类型;reactive 定义对象。
        2. 如果用到了对象展开,那么需要用到 toRefs 函数将对象中的属性变为响应式。
        3. 在 vue3 中,定义的变量、函数等等,在使用的时候,都不需要 this。

07.setup
    a.介绍
        1.写的时候,容易忘记返回变量或者方法等等。
        2.写法有点臃肿。
    b.代码
        <template>
            <div>
                <div>{{age}}</div>
                <div>{{book.name}}</div>
                <div>{{book.author}}</div>
                <div>{{name}}</div>
                <div>{{author}}</div>
                <button @click="updateBookInfo">更新图书信息</button>
            </div>
        </template>

        <!--直接在 script 节点中定义 setup 属性,然后,script 节点就像以前 jquery 写法一样-->
        <script setup>

            import {ref, reactive, toRefs} from 'vue';

            const age = ref(99);
            const book = reactive({
                name: "三国演义",
                author: '罗贯中'
            });
            const updateBookInfo = () => {
                //修改书名,注意,在 vue3 中,现在方法中访问变量,不再需要 this
                book.name = '三国演义123';
            }
            //展开的变量
            const {name, author} = toRefs(book);
        </script>

        <style scoped>

        </style>
        现在,就直接在 script 节点中,增加 setup 属性,然后 script 节点中定义的变量名、方法名等等,默认就会自动返回,我们只需要定义即可。

2.11 Vue3全局方法、插件、指令

01.自定义全局方法
    a.Vue2 中定义全局方法
        在 Vue2 中,自定义全局方法的思路如下:
        Vue.prototype.postRequest = postRequest;
        -----------------------------------------------------------------------------------------------------
        通过 Vue.prototype 将一个方法挂载为全局方法,这样,在具体的 .vue 文件中,我们就可以通过 this 来引用这个全局方法了:
        this.postRequest('/doLogin', this.loginForm).then(resp => {
            this.loading = false;
            if (resp) {
                this.$store.commit('INIT_CURRENTHR', resp.obj);
                window.sessionStorage.setItem("user", JSON.stringify(resp.obj));
                let path = this.$route.query.redirect;
                this.$router.replace((path == '/' || path == undefined) ? '/home' : path);
            }else{
                this.vcUrl = '/verifyCode?time='+new Date();
            }
        })
        -----------------------------------------------------------------------------------------------------
        在 Vue2 中,我们可以将一个方法挂载为全局方法。
        Vue3 这个写法则完全变了:
        定义的方式变了,不再是 Vue.prototype。
        引用的方式变了,因为在 Vue3 中,没法直接通过 this 去引用全局方法了。
    b.Vue3 中定义全局方法
        首先来看方法定义:
        /**
         * Vue3 中定义全局方法
         */
        app.config.globalProperties.sayHello=()=>{
            console.log("hello javaboy!");
        }
        -----------------------------------------------------------------------------------------------------
        定义好之后,需要引用,方式如下:
        <template>
            <div>
                <div>{{age}}</div>
                <div>{{book.name}}</div>
                <div>{{book.author}}</div>
                <div>{{name}}</div>
                <div>{{author}}</div>
                <button @click="updateBookInfo">更新图书信息</button>
                <button @click="btnClick">ClickMe</button>
            </div>
        </template>

        <!--直接在 script 节点中定义 setup 属性,然后,script 节点就像以前 jquery 写法一样-->
        <script setup>

            //getCurrentInstance 方法可以获取到当前的 Vue 对象
            import {ref, reactive, toRefs, onMounted, getCurrentInstance} from 'vue';

            //来自该方法的 proxy 对象则相当于之前的 this
            const {proxy} = getCurrentInstance();

            const age = ref(99);
            const book = reactive({
                name: "三国演义",
                author: '罗贯中'
            });
            const updateBookInfo = () => {
                //修改书名,注意,在 vue3 中,现在方法中访问变量,不再需要 this
                book.name = '三国演义123';
            }
            //展开的变量
            const {name, author} = toRefs(book);
            onMounted(() => {
                console.log(this);
            })
            const btnClick = () => {
                //想在这里调用全局方法
                proxy.sayHello();
            }
        </script>

        <style scoped>

        </style>
        -----------------------------------------------------------------------------------------------------
        首先需要导入 getCurrentInstance() 方法。
        从第一步导入的方法中,提取出 proxy 对象,这个 proxy 对象就类似于之前在 Vue2 中用的 this。
        接下来,通过 proxy 对象就可以去引用全局方法了。

02.自定义插件
    a.介绍
        一些工具方法可以定义为全局方法,如果这个全局的工具,不仅仅是一个工具方法,里边还包含了一些页面等,那么此时,全局方法就不适用了。这个时候我们需要定义插件。
        上述全局方法定义,可以理解为是一个简单的插件。
        Vue2 和 Vue3 中自定义插件的流程基本上都差不多,但是,插件内部的钩子函数不一样。
    b.注册全局组件
        首先定义一个组件:
        <template>
            <div>
                <div>
                    <a href="http://www.javaboy.org">javaboy</a>
                </div>
                <div>
                    <a href="http://www.itboyhub.com">itboyhub</a>
                </div>
            </div>
        </template>

        <script>
            export default {
                name: "MyBanner"
            }
        </script>

        <style scoped>

        </style>
        -----------------------------------------------------------------------------------------------------
        接下来,就可以在插件中导入组件并注册:
        // 在这里定义插件
        //在插件中,可以引入 vue 组件,并注册(这里的注册,就相当于全局注册)
        import MyBanner from "@/components/MyBanner";

        export default {
            /**
             * @param app 这个就是 Vue 对象
             * @param options 这是一个可选参数
             *
             * 当项目启动的时候,插件方法就会自动执行
             */
            install: (app, options) => {
                console.log("这是我的第一个插件")
                //在这里完成组件的注册,注意,这是一个全局注册
                app.component('my-banner', MyBanner);
            }
        }
        -----------------------------------------------------------------------------------------------------
        最后,就可以在项目的任意位置使用这个组件了:
        <template>
          <nav>
            <router-link to="/">Home</router-link> |
            <router-link to="/about">About</router-link>
          </nav>
          <div>
            <!--这里就可以直接使用插件中注册的全局组件了-->
            <my-banner></my-banner>
          </div>
          <router-view/>
        </template>

        <style>
        #app {
          font-family: Avenir, Helvetica, Arial, sans-serif;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
          text-align: center;
          color: #2c3e50;
        }

        nav {
          padding: 30px;
        }

        nav a {
          font-weight: bold;
          color: #2c3e50;
        }

        nav a.router-link-exact-active {
          color: #42b983;
        }
        </style>
    c.注册全局指令
        首先注册全局指令:
        // 在这里定义插件
        //在插件中,可以引入 vue 组件,并注册(这里的注册,就相当于全局注册)
        import MyBanner from "@/components/MyBanner";

        export default {
            /**
             * @param app 这个就是 Vue 对象
             * @param options 这是一个可选参数
             *
             * 当项目启动的时候,插件方法就会自动执行
             */
            install: (app, options) => {
                console.log("这是我的第一个插件")
                //在这里完成组件的注册,注意,这是一个全局注册
                app.component('my-banner', MyBanner);
                //自定义指令,第一个参数是自定义指令的名称,第二个参数自定义指令的逻辑
                //el 表示添加这个自定义指令的节点
                //binding 中包含了自定义指令的参数
                app.directive('font-size', (el, binding, vnode) => {
                    let size = 18;
                    //binding.arg 获取到的就是 small 或者 large
                    switch (binding.arg) {
                        case "small":
                            size = 14;
                            break;
                        case "large":
                            size = 36;
                            break;
                        default:
                            break;
                    }
                    //为使用了 v-font-size 指令的标签设置 font-size 的大小
                    el.style.fontSize = size + 'px';

                })
            }
        }
        -----------------------------------------------------------------------------------------------------
        然后就可以在任意地方去使用这个全局指令了:
        <template>
            <div>
                <div>
                    <a href="http://www.javaboy.org" v-font-size:large>javaboy</a>
                </div>
                <div>
                    <!--使用自定义指令,去指定文本的大小,指令的名字就是 font-size-->
                    <a href="http://www.itboyhub.com" v-font-size:small>itboyhub</a>
                </div>
            </div>
        </template>

        <script>
            export default {
                name: "MyBanner"
            }
        </script>

        <style scoped>

        </style>
    d.参数传递
        自定义插件的时候,可以通过 options 传递参数到插件中:
        // 在这里定义插件
        //在插件中,可以引入 vue 组件,并注册(这里的注册,就相当于全局注册)
        import MyBanner from "@/components/MyBanner";

        export default {
            /**
             * @param app 这个就是 Vue 对象
             * @param options 这是一个可选参数
             *
             * 当项目启动的时候,插件方法就会自动执行
             */
            install: (app, options) => {
                console.log("这是我的第一个插件")
                //在这里完成组件的注册,注意,这是一个全局注册
                app.component('my-banner', MyBanner);
                //自定义指令,第一个参数是自定义指令的名称,第二个参数自定义指令的逻辑
                //el 表示添加这个自定义指令的节点
                //binding 中包含了自定义指令的参数
                app.directive('font-size', (el, binding, vnode) => {
                    let size = 18;
                    //binding.arg 获取到的就是 small 或者 large
                    switch (binding.arg) {
                        case "small":
                            size = options.fontSize.small;
                            break;
                        case "large":
                            size = options.fontSize.large;
                            break;
                        default:
                            break;
                    }
                    //为使用了 v-font-size 指令的标签设置 font-size 的大小
                    el.style.fontSize = size + 'px';

                })
            }
        }
        -----------------------------------------------------------------------------------------------------
        options.fontSize.small 就是插件在引用的时候传入的参数。
        createApp(App)
            //安装插件
            .use(plugins,{
                fontSize:{
                    small: 6,
                    large: 64
                }
            })
            .use(store)
            .use(router)
            .mount('#app')
    e.provide 和 inject
        可以通过 provide 去定义一个方法,然后在需要使用的使用,通过 inject 去注入这个方法然后使用。
        方法定义:
        // 在这里定义插件
        //在插件中,可以引入 vue 组件,并注册(这里的注册,就相当于全局注册)
        import MyBanner from "@/components/MyBanner";

        export default {
            /**
             * @param app 这个就是 Vue 对象
             * @param options 这是一个可选参数
             *
             * 当项目启动的时候,插件方法就会自动执行
             */
            install: (app, options) => {
                console.log("这是我的第一个插件")
                //在这里完成组件的注册,注意,这是一个全局注册
                app.component('my-banner', MyBanner);
                //自定义指令,第一个参数是自定义指令的名称,第二个参数自定义指令的逻辑
                //el 表示添加这个自定义指令的节点
                //binding 中包含了自定义指令的参数
                app.directive('font-size', (el, binding, vnode) => {
                    let size = 18;
                    //binding.arg 获取到的就是 small 或者 large
                    switch (binding.arg) {
                        case "small":
                            size = options.fontSize.small;
                            break;
                        case "large":
                            size = options.fontSize.large;
                            break;
                        default:
                            break;
                    }
                    //为使用了 v-font-size 指令的标签设置 font-size 的大小
                    el.style.fontSize = size + 'px';
                })

                const clickMe = () => {
                    console.log("========clickMe========")
                }
                //这里相当于是注册方法
                app.provide('clickMe', clickMe);
            }
        }
        注意定义的时候,方法要写在 install 中。
        -----------------------------------------------------------------------------------------------------
        方法使用:
        <template>
            <div>
                <div>
                    <a href="http://www.javaboy.org" v-font-size:large>javaboy</a>
                </div>
                <div>
                    <!--使用自定义指令,去指定文本的大小,指令的名字就是 font-size-->
                    <a href="http://www.itboyhub.com" v-font-size:small>itboyhub</a>
                </div>
                <div>
                    <button @click="btnClick">ClickMe</button>
                </div>
            </div>
        </template>

        <script>
            import {inject} from 'vue';

            export default {
                name: "MyBanner",
                mounted() {
                    //注入 clickMe 函数
                    const clickMe = inject('clickMe');
                    clickMe();
                },
                methods: {
                    btnClick() {
                    }
                }
            }
        </script>

        <style scoped>

        </style>

03.自定义指令
    a.两种作用域
        自定义指令有两种作用域,可以是全局自定义指令(当前项目的任何地方都可以使用),
        也可以是局部自定义指令(就能在定义的 .vue 文件中使用)。
    b.局部自定义指令
        自定义一个指令,要求如下:
        1. 指令的名字为 onceClick。
        2. 指令在定义的时候,可以加上一个时间参数。
        3. 这个指令一般加在 button 上,表示 button 在点击完成之后,多长时间内,无法再次点击。
        4. <button v-onceClick="10000">BTN01</button>表示这个按钮点击之后,会处于禁用状态,10s 之后才可以再次点击,防止用户发送重复请求。
        -----------------------------------------------------------------------------------------------------
        自定义局部指令:
        <template>
            <div>
                <div>{{a}}</div>
                <div>
                    <button @click="btnClick" v-once-click="10000">ClickMe</button>
                </div>
            </div>
        </template>

        <script>
            import {ref} from 'vue';

            export default {
                setup() {
                    const a = ref(1);
                    const btnClick = () => {
                        a.value++;
                    }
                    return {a, btnClick}
                },
                directives:{
                    onceClick:{
                        /**
                         * 首先,自定义指令的钩子函数一共有七个:https://cn.vuejs.org/guide/reusability/custom-directives.html#directive-hooks
                         * 钩子函数中有参数,这个参数基本和 Vue2 中的自定义指令时候的参数含义一致,一共四个参数
                         * @param el 就是自定义指令所绑定的元素
                         * @param binding 各种传递的参数都在 binding 上
                         * @param vNode Vue 编译生成的虚拟节点
                         */
                        mounted(el, binding, vNode) {
                            el.addEventListener('click',()=>{
                                if (!el.disabled) {
                                    //当发生点击事件的时候,如果 el 没有被禁用,那么就让他禁用 10s 钟
                                    //禁用
                                    el.disabled = true;
                                    //延迟执行
                                    setTimeout(() => {
                                        el.disabled = false;
                                    }, binding.value || 3000);
                                }
                            });
                        }
                    }
                }
            }
        </script>

        <style scoped>

        </style>
        -----------------------------------------------------------------------------------------------------
        两种传参方式:
        v-once-click:10000,那么获取参数就是 binding.arg。
        v-once-click="10000",那么获取参数就是 binding.value。
    c.全局自定义指令
        const app = createApp(App);
        /**
         * 这里定义的,就是一个全局指令
         */
        app.directive('onceClick2', {
            /**
             * 首先,自定义指令的钩子函数一共有七个:https://cn.vuejs.org/guide/reusability/custom-directives.html#directive-hooks
             * 钩子函数中有参数,这个参数基本和 Vue2 中的自定义指令时候的参数含义一致,一共四个参数
             * @param el 就是自定义指令所绑定的元素
             * @param binding 各种传递的参数都在 binding 上
             * @param vNode Vue 编译生成的虚拟节点
             */
            mounted(el, binding, vNode) {
                el.addEventListener('click', () => {
                    if (!el.disabled) {
                        //当发生点击事件的时候,如果 el 没有被禁用,那么就让他禁用 10s 钟
                        //禁用
                        el.disabled = true;
                        //延迟执行
                        setTimeout(() => {
                            el.disabled = false;
                        }, binding.value || 3000);
                    }
                });
            }
        });
        -----------------------------------------------------------------------------------------------------
        全局自定义指令和局部自定义指令的唯一区别在全局自定义指令写在 main.js 中
        (也可以单独写一个 js 文件用来定义各种全局自定义指令,然后在 main.js 中引入这个自定义的指令的文件)。
        指令的具体的逻辑,定义方式都是一样的。全局自定义指令可以应用在当前项目的任何地方。
    d.同时传递两个参数
        假设,v-onceClick2 这个指令的时间参数,其中的时间单位可以自己传递:
        <button @click="btnClick" v-onceClick3:ms="10000">ClickMe</button>
        -----------------------------------------------------------------------------------------------------
        全局指令的定义:
        /**
         * 这里定义的,就是一个全局指令
         */
        app.directive('onceClick3', {
            /**
             * 首先,自定义指令的钩子函数一共有七个:https://cn.vuejs.org/guide/reusability/custom-directives.html#directive-hooks
             * 钩子函数中有参数,这个参数基本和 Vue2 中的自定义指令时候的参数含义一致,一共四个参数
             * @param el 就是自定义指令所绑定的元素
             * @param binding 各种传递的参数都在 binding 上
             * @param vNode Vue 编译生成的虚拟节点
             */
            mounted(el, binding, vNode) {
                el.addEventListener('click', () => {
                    if (!el.disabled) {
                        //当发生点击事件的时候,如果 el 没有被禁用,那么就让他禁用 10s 钟
                        //禁用
                        el.disabled = true;
                        let time = binding.value;
                        if (binding.arg == 's') {
                            //使用指令的时候,单位是秒,这里转为 ms,因为 setTimeout 中的时间是 ms
                            time = time * 1000;
                        }
                        //延迟执行
                        setTimeout(() => {
                            el.disabled = false;
                        }, time);
                    }
                });
            }
        });
        -----------------------------------------------------------------------------------------------------
        处理这个指令的时候,先看一下指令的 binding.arg,如果这个为 s,说明使用指令的时候传递的参数是 s,
        而 setTimeout 中的参数是 ms,所以乘上 1000。
    e.传递动态参数
        自定义指令中,还可以传递动态参数,方式如下:
        首先定义一个变量:
        setup() {
            const a = ref(1);
            const timeUnit = ref('s');
            const btnClick = () => {
                a.value++;
            }
            return {a, btnClick, timeUnit}
        },
        timeUnit 就是我们要使用的时间单位,但是注意他是一个变量。
        然后在页面中,就可以直接使用这个变量了:
        <button @click="btnClick" v-onceClick3:[timeUnit]="3">ClickMe</button>
    f.自定义权限指令
        用户登录成功之后,会从服务端获取到自己的权限以及角色信息,自定义权限指令,则是比较一下用户是否具备某一个空间所需要的角色/权限。
        自定义全局的权限控制指令:
        // 这个表示当前用户所具备的权限信息,正常来说,这个数组应该是从服务端获取
        const usersPermissions = ['user'];

        app.directive('hasPermission', {
            mounted(el, binding, vNode) {
                //获取组件所需要的权限 <button v-hasPermission="[user:add]">添加用户</button>
                //此时,拿到的 value 就是一个数组:[user:add]
                const {value} = binding;  //const value = binding.value;
                //接下来就是判断 usersPermissions 数组中是否包含 value 数组中的值
                let f=usersPermissions.some(p=>{
                    //如果这里都返回 false,f 就是 false,如果这里有一个是 true,则 f 就是 true
                    return p.indexOf(value[0]) !== -1;
                })
                if (!f) {
                    //说明当前用户不具备所需要的权限,那么就把 el 从 DOM 树中移除掉
                    el.parentNode && el.parentNode.removeChild(el);
                }

            }
        })
        -----------------------------------------------------------------------------------------------------
        两个地方:
        1. 遍历数组的时候,使用的 some 函数。
        2. 从 DOM 树中移除一个元素。
        -----------------------------------------------------------------------------------------------------
        用法如下:
        <button @click="btnClick" v-onceClick2="10000" v-hasPermission="['user2']">ClickMe</button>

2.12 Vite

01.介绍
    Vite 是一个前端构建工具,类似于我们在 Vue2 中使用的 Webpack。
    Vite 相比于 Webapck,最大的优势在于快!
    Spring Boot 热加载原理:Spring Boot 中提供了两个类加载器:
    ● base classloader:这个主要用来加载各种第三方的类(各种依赖中的类,这些类的特点是不会变)。
    ● restart classloader:这个用来加载我们自己写的类。
    Vite 快的思路和 Spring Boot 热加载基本上是一致的。在 Vite 中,将项目的模块分为两种:
    ● 依赖(变化小,大部分时间都是不变的)
    ● 源码(经常变化的是源码)

02.常见方法
    a.介绍
        1. 注意,需要 nodejs 版本大于 14.18。如果电脑上已经有基于 nodejs 的项目,建议通过 nvm 来安装多个 nodejs 版本,然后去处理依赖问题。
        2. 需要注意,要根据不同的 npm 版本,使用不同的创建命令 https://cn.vitejs.dev/guide/#scaffolding-your-first-vite-project。
    b.创建三个步骤:
        npm create vite@latest vue3_03 --template vue
        cd vue3_03
        npm install
        npm run dev
    c.方法自动导入
        在 Vue3 中,当我们要使用某一个方法的时候,必须先导入这个方法,以页面跳转为例,有如下两种跳转方式:
        -----------------------------------------------------------------------------------------------------
        第一种写法,首先从 vue 中导入 getCurrentInstance 方法,然后获取 proxy 对象,这个 proxy 对象就相当于之前 Vue2 中的 this。
        import {getCurrentInstance} from 'vue';
        const {proxy} = getCurrentInstance()
        function goUserMana() {
          proxy.$router.push('/um');
        }
        -----------------------------------------------------------------------------------------------------
        第二种写法,直接从 vue-router 中导入 router 对象,然后调用 router 对象实现页面的跳转:
        import {useRouter} from 'vue-router';
        const router = useRouter();
        function goLogMana() {
          router.push("/lm");
        }
        -----------------------------------------------------------------------------------------------------
        无论哪种方式,都需要先导入方法/组件,然后才能使用。
        如果使用了 vite,那么我们可以通过 unplugin-auto-import 插件来实现方法的自动导入。
    d.unplugin-auto-import插件使用步骤
        安装插件:
        npm install unplugin-auto-import -D
        -----------------------------------------------------------------------------------------------------
        配置插件(vue3_03/vite.config.js)
        import {defineConfig} from 'vite'
        import vue from '@vitejs/plugin-vue'
        import AutoImport from 'unplugin-auto-import/vite'

        // https://vitejs.dev/config/
        export default defineConfig({
            plugins: [vue(), AutoImport({
                imports: ['vue']
            })]
        })
        imports: ['vue']配置的含义是,凡是 vue 中提供的方法,现在都不需要导入就可以直接使用了。
        -----------------------------------------------------------------------------------------------------
        如果还想自动导入 vue-router 中的配置,方式如下:
        import {defineConfig} from 'vite'
        import vue from '@vitejs/plugin-vue'
        import AutoImport from 'unplugin-auto-import/vite'

        // https://vitejs.dev/config/
        export default defineConfig({
            plugins: [vue(), AutoImport({
                imports: ['vue','vue-router']
            })]
        })
        一般来说,一些使用频率特别高的组件中的方法(例如 vue、vue-router、vuex/pinia),
        可以通过这种方式来导入,一些冷门的组件方法,建议还是自己手动导入。
    e.组件后缀问题
        在 Vite 中,当需要导入一个组件的时候,
        默认情况下,必须加上 .vue 后缀,无论是在 router 中还是在普通的 .vue 文件中,
        都需要加上这个后缀。但是很多小伙伴习惯了 webpack 中不写后缀,那么通过配置可以解决这个问题。
        -----------------------------------------------------------------------------------------------------
        import {defineConfig} from 'vite'
        import vue from '@vitejs/plugin-vue'
        import AutoImport from 'unplugin-auto-import/vite'

        // https://vitejs.dev/config/
        export default defineConfig({
            plugins: [vue(), AutoImport({
                imports: ['vue','vue-router']
            })],
          resolve:{
            extensions: ['.vue','.js','.json','.ts']
          }
        })
    f.组件名称问题
        之前,我们可以通过如下方式来自定义组件的名称:
        <script>
            export default {
                name: "LogMana222"
            }
        </script>
        -----------------------------------------------------------------------------------------------------
        当我们将 setup 加在 script 节点中时,就没法去设置 name 属性了,此时,如果还需要设置 name 属性,那么可以通过如下方式来设置:
        <script>
            export default {
                name: 'my-app'
            }
        </script>
        -----------------------------------------------------------------------------------------------------
        新加一个 script 标签,里边只写 export default 即可,然后配置一下组件名称即可。
        通过插件可以简化这个操作。
        -----------------------------------------------------------------------------------------------------
        首先安装插件:
        npm install vite-plugin-vue-setup-extend -D
        -----------------------------------------------------------------------------------------------------
        接下来配置组件:
        import {defineConfig} from 'vite'
        import vue from '@vitejs/plugin-vue'
        import AutoImport from 'unplugin-auto-import/vite'
        import VueSetupExtend from 'vite-plugin-vue-setup-extend'

        // https://vitejs.dev/config/
        export default defineConfig({
            plugins: [vue(), AutoImport({
                imports: ['vue', 'vue-router']
            }),
                VueSetupExtend()],
            resolve: {
                extensions: ['.vue', '.js', '.json', '.ts']
            }
        })
        -----------------------------------------------------------------------------------------------------
        配置完成后,接下来,就可以直接在 script 节点中定义组件的名称了:
        <script setup name="javaboy-app">
            // This starter template is using Vue 3 <script setup> SFCs
            // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
            import HelloWorld from './components/HelloWorld.vue'
            // import {getCurrentInstance} from 'vue';

            const {proxy} = getCurrentInstance()

            function goUserMana() {
                proxy.$router.push('/um');
            }

            // import {useRouter} from 'vue-router';

            const router = useRouter();

            function goLogMana() {
                router.push("/lm");
            }
        </script>