开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Crash 插件开发指南 简介 基本构成 常用 API 集

发表于 2024-04-12

简介

crash-utility 开源项目是用于解析具体版本 Linux 内核 Core 的工具,基于 GDB 之上的程序,支持原始 GDB 指令以及特定的 Linux 模块解析,支持格式化内核数据结构、反汇编源代码、内核堆栈回溯等。对于开发者感兴趣解析的模块,可自定义增加插件模块解析。

基本构成

插件开发构成 描述
defs.h 文件 开发必备头文件,提供 crash-utility 环境下的 API 函数声明。
void attribute((constructor)) demo_init(void) demo 插件模块初始化入口,其它插件开发则替换 demo 字符串即可。
void attribute((destructor)) demo_fini(void) 在 crash-utility 环境下移除 demo 插件时自动调用。
static struct command_table_entry command_table[] = { “demo”, cmd_demo, help_demo, 0 },{ NULL }register_extension(command_table); 用于初始化入口注册插件函数。(关键)command_table_entry 结构体由 { 指令名称,指令主函数,指令帮助信息,…… } 组成。
void cmd_demo(void) 命令行传入参数已被 crash-utility 自身处理过,如果需使用 getopt,getopt_long 函数解析参数,使用全局变量 ( argcnt 、args ) 。
char *help_demo[] = {“demo”, // 指令名称”demo info”, // 指令具体描述”demo [-p] “, // 指令概要”version 1.0”, // 具体描述信息NULL}; 该字符串用于在 crash-utility 中输入 help demo 时返回。
{ ARM64,X86_64,…… } 编译宏定义 必须在编译脚本中确定目标架构,会影响 defs.h 部分函数及宏。
-rdynamic 编译 cflags 定义 添加到动态符号表

自定义插件,以下全局变量,以及宏需重新定义,扩展新的结构体成员。

结构体偏移、大小解析相关
extern struct offset_table offset_table;extern struct size_table size_table; 全局结构体成员偏移表全局结构体以及成员大小表
#define OFFSET(X) (… …) 与 offset_table 绑定,获取指定结构体成员偏移量
#define SIZE(X) (… …) 与 size_table 绑定,获取指定结构体或成员大小
#define VALID_MEMBER(X) (… …) 与 offset_table 绑定,校验成员是否有定义
#define ASSIGN_OFFSET(X) (… …) 与 offset_table 绑定,获取指定结构体成员偏移量,不校验
#define MEMBER_OFFSET_INIT(X, Y, Z) (… …) 初始化 offset_table 表成员偏移量
#define ASSIGN_SIZE(X) (… …) 与 size_table 绑定,获取指定结构体或成员大小,不校验
#define MEMBER_SIZE_INIT(X, Y, Z) (… …) 初始化 size_table 表成员大小
#define STRUCT_SIZE_INIT(X, Y) (… …) 初始化 size_table 表结构体大小
命令行处理以及终端输出
extern FILE *fp; 全局变量,fprintf(fp, “xxx\n”); 命令行输出
extern char *args[MAXARGS];extern int argcnt;extern int argerrs; 插件主函数的参数集。

常用 API 集

错误处理 描述
#define FAULT_ON_ERROR (0x1) 错误终止,并输出错误信息
#define RETURN_ON_ERROR (0x2) 错误不终止,但输出错误信息
#define QUIET (0x4) 错误终止且不输出错误信息
内存相关 描述
int readmem(ulonglong, int, void *, long, char *, ulong); 参数含义 { 地址,地址类型,结果集,错误信息,错误处理 }地址类型: { KVADDR,UVADDR,PHYSADDR }例子:ulong value;readmem(0xFFFFC9000D35BCD0, KVADDR, &value, sizeof(void*), “read value err!”, FAULT_ON_ERROR);
#define GETBUF(X) getbuf((long)(X)) 申请内存,可用 malloc 替代。
#define FREEBUF(X) freebuf((char *)(X)) 释放内存,与 GETBUF 成对存在。
int kvtop(struct task_context *, ulong, physaddr_t *, int); 内核态虚拟地址转物理地址
int uvtop(struct task_context *, ulong, physaddr_t *, int); 用户态虚拟地址转物理地址
机器属性相关 描述
extern struct machdep_table *machdep; 全局变量描述目标机器的具体信息,如 va_bits、kimage_text等, 可用 help -m 查看。
#define PAGESIZE() (machdep->pagesize) 目标机器页表大小
#define PAGESHIFT() (machdep->pageshift) 目标机器页表移位
Task 相关 描述
extern struct task_table task_table, *tt; 全局变量 tt,保存 task 的信息。
#define CURRENT_CONTEXT() (tt->current) 当前 task context,可用命令 set 查看
#define CURRENT_TASK() (tt->current->task)
#define CURRENT_PID() (tt->current->pid)
#define CURRENT_COMM() (tt->current->comm)
int set_context(ulong, ulong); 设置当前 task context。
ulong pid_to_task(ulong); pid 转 task_struct 地址
ulong task_to_pid(ulong); task_struct 地址转 pid
#define RUNNING_TASKS() (tt->running_tasks)
#define FIRST_CONTEXT() (tt->context_array) 所有 task ,可用 ps 查看。首个 task context 地址,一般用于遍历所有 task 时使用。
符号相关 描述
ulong symbol_value(char *);ulong symbol_value_module(char *, char *); 获取符号地址在 xxx.ko 模块中获取符号地址
int symbol_exists(char *s);int kernel_symbol_exists(char *s); 判断符号是否存在
struct syment *value_search(ulong, ulong *);struct syment *value_search_module(ulong, ulong *); 查询地址是某个符号。
链表、树等遍历函数。、 描述
struct list_data {ulong flags;ulong start;long member_offset;long list_head_offset;ulong end;ulong searchfor;char **structname;int structname_args;char *header;ulong *list_ptr;int (*callback_func)(void *, void *);void *callback_data;long struct_list_offset;}; flags:#define VERBOSE 1#define LIST_ALLOCATE (VERBOSE << 10)LIST_ALLOCATE 该 flags 参数会为 list_ptr 分配对应内存,并将遍历得到的所有成员填充到该内存地址上,最后开发者需要用 FREEBUF 将其释放。VERBOSE 该 flags 参数会在遍历的同时打印输出结构。start: 遍历的起始地址member_offset: 链表节点在结构体中的偏移地址
int do_list(struct list_data *); 可用于 list_head, hlist_head 数据结构的遍历。
int hq_open(void);int hq_close(void); hash_table 启用备份遍历数据,crash-utility 提供的遍历函数都会使用 hq_enter(value) 将数据备份到此处。
int retrieve_list(ulong *buf, int); 拷贝 hash_table 数据到 buf 内存。
int do_rdtree(struct tree_data *); 可用 radix_tree 数据结构的遍历。
int do_rbtree(struct tree_data *); 可用 rb_root 数据结构的遍历。
int do_xatree(struct tree_data *); 可用 xarray 数据结构的遍历。
Int do_mptree(struct tree_data *); 可用 maple 数据结构的遍历。
ulong do_maple_tree(ulong, int, struct list_pair *); 可用 maple 数据结构的遍历。
#define MAPLE_TREE_COUNT (1) MAPLE_TREE_COUNT 仅统计数量
#define MAPLE_TREE_SEARCH (2) MAPLE_TREE_SEARCH 检索
#define MAPLE_TREE_DUMP (3) MAPLE_TREE_DUMP 遍历同时打印结果
#define MAPLE_TREE_GATHER (4) MAPLE_TREE_GATHER 遍历结果拷贝到 list_pair 内存
#define MAPLE_TREE_DUMP_CB (5)
ulong do_xarray(ulong, int, struct list_pair *); 与 do_maple_tree 同理,用于 xarray 数据结构的遍历。
#define XARRAY_COUNT (1)
#define XARRAY_SEARCH (2)
#define XARRAY_DUMP (3)
#define XARRAY_GATHER (4)
#define XARRAY_DUMP_CB (5)
ulong do_radix_tree(ulong, int, struct list_pair *); 同理。
#define RADIX_TREE_COUNT (1)
#define RADIX_TREE_SEARCH (2)
#define RADIX_TREE_DUMP (3)
#define RADIX_TREE_GATHER (4)
#define RADIX_TREE_DUMP_CB (5)

案例参考

开源项目: github.com/Penguin38/O… ,该项目目前已支持转储进程 Core 文件,支持解析 zram,shmem 交换页内存。关于转储细节均与 OpenCoreSDK 一致,内核版本 5.10 ~ 6.1,其它版本未做测试,性能比 gcore 插件快上许多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
lua复制代码crash> mod -s zram zram.ko
crash> mod -s zsmalloc zsmalloc.ko

crash> extend output/arm64/linux-parser.so
output/arm64/linux-parser.so: shared object loaded

crash> lp help core
Usage: core -p <PID> [--output|-o <FILE_PATH>] [option]
Option:
--zram: decompress zram page
--shmem: decompress shared memory on zram page
--filter|-f: filter vma flags
Filter Vma:
0x01: filter-special-vma
0x02: filter-file-vma
0x04: filter-shared-vma
0x08: filter-sanitizer-shadow-vma
0x10: filter-non-read-vma
Example:
lp core -p 1 --zram --shmem -f 0x1b

crash> lp core -p 1515 --zram --shmem -f 0x18
Saved [1515.core].

结合开源项目: github.com/Penguin38/O… 进行解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
perl复制代码core-parser> top 20 -d --app
>>> '_ZN3art7Runtime9instance_E' = 0x79f9959430
Address Allocations ShallowSize NativeSize ClassName
TOTAL 1181869 68339645 56853669
------------------------------------------------------------
0x6f3a2840 180908 11229040 0 java.lang.String
0x6f3472b8 102778 7739252 0 java.lang.Object[]
0x6fc69090 101915 10599160 0 android.app.usage.UsageEvents$Event
0x6f3f1030 86669 1733380 0 java.util.ArrayList
0x6fb3c3b8 53835 861360 0 android.content.ComponentName
0x6fd00a58 43321 7277928 0 android.content.pm.parsing.component.ParsedActivity
0x6f3872e0 34173 2902220 0 int[]
0x6f349a80 24709 889524 0 sun.misc.Cleaner
0x6fb46d18 24574 614350 0 android.util.ArrayMap
0x6f5ae0f8 24421 586104 0 libcore.util.NativeAllocationRegistry$CleanerThunk
0x6f3afb08 19696 472704 0 java.util.HashMap$Node
0x6f3471d8 18612 148896 0 java.lang.Object
0x6f3875f8 17757 2258552 0 long[]
0x71b0b278 16557 3112716 0 android.os.perfdebug.MessageMonitorImpl$MessageMonitorInfoImpl
0x13413020 16557 198684 0 android.os.perfdebug.MessageMonitorImpl$MessageMonitorInfoImpl$1
0x6fdab5b8 15481 247696 0 android.util.Pair
0x137a9b70 14856 267408 0 com.android.server.pm.permission.LegacyPermissionState$PermissionState
0x6fced578 14055 1068180 0 android.content.pm.parsing.component.ParsedIntentInfo
0x6f3ccd40 13621 544840 0 java.util.HashMap
0x6fdab698 13437 408880 0 android.util.Pair[]

目前公版的状态,未来可能会支持功能,可翻阅历史文章,精力有限不定期更新。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

意难平!面试小米,一步之遥 1电商系统开发中都遇到了

发表于 2024-04-12

1d4e7c154ca0792c7fb8b8e987cb45e.jpg
面试训练营的同学,前几天面试小米,都三面了,本来以为稳了,但没想到最后还是挂了。

按往年的经验来看,小米相比与其他互联网大厂来说,面试的难度比较低,而且薪资也比较低,所以拿到 Offer 的概率还是很大的。

但今年这个情况,这位同学还是硕士身份,最后还是挂在三面了,确实挺可惜的。

那咱们今天来看看,最近小米面试的真题吧,带大家看看小米的面试难度。

1.电商系统开发中都遇到了哪些问题?

电商系统遇到的常见问题:

  1. 高并发 -> 缓存、限流、熔断。
  2. 库存问题:
    1. 超卖:限流、分布式锁、Redis Lua(先判断库存 -> 减库存 + 加订单)、使用数据库 update 操作库存。
    2. 少卖:及时对账系统 -> 人工介入处理。
  3. 弱网支付问题:支付平台提供回调方法进行支付状态的刷新、手动触发主动出查询得到支付状态、定时任务进行刷新。
  4. 刷单:添加图形验证码、IP 黑名单。
  5. 重复订单问题:业务实现幂等性判断、数据库的唯一约束。

2.怎么用Redis实现秒杀业务?

Redis Lua(先判断库存 -> 减库存 + 加订单)。

3.为什么Redis单线程执行还这么快?

原因有以下几个:

  1. 基于内存操作。
  2. Redis 瓶颈是在内存和带宽,而不是在 CPU 操作。
  3. Redis 使用多路复用的 IO。
  4. 避免多线程切换和过度争抢。

4.Redis如何扫描前缀相同的key?

Redis 查询前缀相同的 Key 实现方式:

  1. keys -> keys user*。
  2. scan -> scan 0 math user* count 100。

5.Redis的keys和scan有什么区别?

keys 查询所有的匹配数据,scan 是可以查询部分数据。

6.如何使用Redis实现分布式锁?

分布式锁实现:

  1. 原始方式:setnx key true(死锁)/set key true ex 30 nx(锁误删)-> 锁重入问题。
  2. Redisson 框架实现方式:Redis + Lua 实现。

7.长连接的好处与坏处?

长连接优点:

  1. 避免了重复创建和销毁连接。
  2. 提高传输效率。
  3. 实现实时传输。
  4. 节省了系统资源的浪费。

长连接缺点:

  1. 占用了更多的资源。
  2. 增加了服务器的压力。
  3. 对网络环境的要求是比较高。

8.TCP和HTTP有什么区别?

TCP VS HTTP:

  1. 层级不同:HTTP 属于应用层协议;TCP 属于传输层的协议。
  2. 数据传输方式不同:HTTP 传输文本或二进制文件;TCP 传输的是数据流。

9.说说HTTPS的执行流程?

HTTPS 执行流程:

  1. 客户端(浏览器)发送 HTTPS 请求到服务器端。
  2. 服务器端使用非对称加密,产生一个公钥,将公钥和 CA 证书给到客户端。
  3. 客户端验证 CA 证书真伪,使用对称加密生成一个共享秘钥,使用刚才的公钥将共享秘钥进行加密,将加密信息发送给服务器端。
  4. 服务器端得到信息之后,使用非对称加密私钥进行解密,得到对称加密的共享秘钥进行通讯。

10.说说ThreadLocal底层实现?

ThreadLocal 底层是通过 ThreadLocalMap 实现,key -> ThreadLocal;value -> 需要存储的值。

11.ThreadLocal父线程和子线程的数据传递?

new InheritableThreadLocal();

12.为什么HashMap是线程不安全的?

HashMap 不是线程安全的原因:

  1. JDK 1.8 之前 -> 链表死循环问题、数据覆盖的问题。
  2. JDK 1.8 之后 -> 数据覆盖的问题。

13.synchronized和volatile有什么区别?

synchronized VS volatile:

  1. 保证线程安全不同:synchronized 能保证线程安全;volatile 不能保证线程安全。
  2. 底层实现不同:
    1. synchronized 底层实现 -> JVM Monitor(监视器)-> C++ ObjectMonitor -> 操作系统 Mutex Lock。
    2. volatile 底层实现 -> 可见性(MESI 协议)、顺序性(内存屏障)。

14.说说ThreadPoolExecutor的参数?

ThreadPoolExecutor 参数:

  1. 核心线程数。
  2. 最大线程数(核心线程数+临时线程数)。
  3. 临时线程在空闲时间可以存活的最大时间。
  4. 参数三的单位描述。
  5. 任务队列。
  6. 线程池的创建工厂。
  7. 拒绝策略。

15.说说ThreadPoolExecutor运行机制?

运行机制(当任务来了之后的执行流程):

  1. 判断核心线程数是否已满;如果未满创建核心线程执行任务;如果满了执行后续操作。
  2. 判断任务队列是否已满;如果未满将任务添加到队列;如果满了执行后续流程。
  3. 判断最大线程数是否已满;如果未满创建临时线程执行任务;如果满了执行后续流程。
  4. 执行拒绝策略(内置4种拒绝策略+自定义的拒绝策略)。

16.RocketMQ和Kafka有什么区别?

RocketMQ VS Kafka 主要区别:

  1. 吐吞量区别:Kafka 吞吐量最高,到达单机百万级的吞吐;RocketMQ 吐吞量是十万到百万级。
  2. 功能上区别:RocketMQ 本身支持死信队列、延迟队列、支持消息推和拉取消息;Kafka 本身不支持死信队列、延迟队列、消息获取方式只能是消费者自己来拉取。

17.说说RocketMQ的重试机制?

RocketMQ 重试机制:

  1. 生产者重试:生成者发送消息的类型有同步发送、异步发送、单次发送(oneway),但模式为同步发送、异步发送才有消息重试机制。
  2. 消费者重试:消费者模式有广播模式和集群模式;广播模式不会进行消息重试,它只会记录警告信息;集群模式会进行消息重试(通过延迟任务来实现消息重试,默认情况下如果重试超过 16 次就会将此消息存到死信队列)。

18.说说MySQL的事务?

事务特性:

  1. 一致性
  2. 持久化
  3. 原子性
  4. 隔离性
    1. 读未提交:存在脏读、不可重复读和幻读问题。
    2. 读已提交:不存在脏读问题,但依然存在不可重复读和幻读的问题。
    3. 可重复读(MySQL 默认的隔离级别):不存在脏读问题、不可重复读,依旧存在幻读问题。
    4. 串行化:不存在脏读问题、不可重复读、幻读问题,但执行效率不高。

19.说说MySQL的优化?explain有什么用?

MySQL 优化措施:

  1. 使用合适的查询语句,不要所有的都使用 select *。
  2. 使用合适的数据类型。
  3. 正确使用索引:
    1. 该创建索引的创建索引。
    2. 查询时一定要正确的触发索引。
  4. 数据量大分库分表。
  5. 使用分布式数据库,例如 TiDB。

20.SpringCloud有哪些组件?

Spring Cloud 包含的组件:

  1. 注册中心:Spring Cloud Alibaba Nacos、Spring Cloud Netflix Euruka。
  2. 配置中心:Spring Cloud Alibaba Nacos。
  3. 网关:Spring Cloud Gateway。
  4. 负载均衡器:Spring Cloud LoadBalancer。
  5. Restful 请求:Spring Cloud Openfeign。
  6. 限流、熔断:Spring Cloud Alibaba Sentinel。
  7. 分布式锁事务:Spring Cloud Alibaba Seata。
  8. 分布式链路追踪系统:Skywalking。

21.Nacos和Euruka的区别?

Nacos VS Euruka 主要区别:

  1. 功能上不同:Nacos 既提供注册中心又提供配置中心;Euruka 只有注册中心。
  2. 通讯方式不同:Nacos 通讯方式是依靠 Netty 实现的长连接;Euruka 短连接+定时任务。
  3. CAP 理论支持不同:Nacos 默认是 AP 模式,但它支持手动设置 CP 模式;Euruka 只支持 AP 模式。
  4. 健康检查机制不同:Nacos 健康检查机制(默认临时实例),每 5s 上报一次健康状况;15s 未上报任务不健康;30s 会剔除此实例;Euruka 健康检查机制,每 30s 上报一次健康状况;60s 未收到认为不健康;90s 未收到剔除此实例。

22.Nacos怎么保证数据一致性的?

Nacos 通过 Raft 实现 Leader 节点的选举,由 Leader 节点将数据同步给所有的普通节点,以保证数据的一致性的。

23.反向代理是什么?

反向代理定义:用于客户端和服务器端进行通讯,代理服务器端的一种代理,可以隐藏真实的服务器地址。反向代理的经典实现就是 Nginx。
优点:

  1. 反向代理服务器可以有缓存,增加查询的效率。
  2. 可以实现负载均衡,从而提升服务器的 QPS,实现服务器的水平扩展。
  3. 反向代理通常自带健康检查机制,可以帮你正确的访问健康的服务实例。
  4. 隐藏真实服务器的地址,一定程度的保证了服务器的安全性。

24.Spring事务是怎么实现的?

Spring 事务实现方式:

  1. 编程式事务
  2. 声明式事务:动态代理实现 -> 1.拦截目标方法;2.在方法执行前开启事务;3.如果方法执行期间出现异常回滚事务;4.在方法执行完之后没有异常,提交事务。

25.SpringBoot自动装配的实现流程?

Spring Boot 实现流程:

  1. 启动 Spring Boot 项目时,执行 @SpringBootApplication 注解类。
  2. 去框架资源文件 spring.factories 读取需要自动装配的类。
  3. 查找自动装配类的 @Configuration + @Condition 条件注解,如果满足自动装配的条件,将此类通过反射机制创建,并放到 IoC 容器,反之则不创建。

最后

想要此面试题视频解析的同学,加我微信:GG_Stone【备注:小米】,免费获取小米面试解析视频。

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

拖动❓元素拖动、列表拖动、表格拖动(列与行)🍊🍊🍊

发表于 2024-04-12

写在开头

各位友友好吖❗

清明已过,下个期待的假期就是五一了,小长假即将到来,假期行程已经可以开始规划囖。😉

不过,对于是大小周的小编来说,整个四月都将是单休,瞬间开心减一半。。。

近来,小编断断续续抽空看了一部名叫 国王排名 动漫,还挺好看的。😁

还把几年不换的微信头像换成了主角的头像,Em……从”蜡笔小新”变成了”波吉王子”。

e890ed7f09eb00e7c11dfac317f3404.jpg
0B9BA257.jpg
dc9d9a8e29708635b56e03ad8ba3135c_1.jpg
新头像,新心情,愉悦。😎

回到正题,本章将分享一些关于 Javascript 中拖动的内容,探索拖动过程的奥秘。👏

元素拖动

刚开始,咱们循序渐进,先来实现一个最简单的功能,让一个元素变成可拖动元素。

布局与样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
html复制代码<!DOCTYPE html>
<html>
<head>
<title>元素拖动</title>
<style>
#drag {
width: 100px;
height: 100px;
border: 1px solid #cbd5e0;
display: flex;
justify-content: center;
align-items: center;
cursor: move;
user-select: none;
position: absolute;
}
</style>
</head>
<body>
<div id="drag">橙某人</div>
</body>

要让一个元素可拖动,我们需要处理三个事件:

  • mousedown:按下。
  • mousemove:移动。
  • mouseup:释放。

三者都是老演员了,相信每个前端人都识得啦😋,具体详情可以点链接上MDN查阅。

具体逻辑过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
html复制代码<script>
document.addEventListener('DOMContentLoaded', () => {
const drag = document.getElementById('drag');
// 添加鼠标按下事件
drag.addEventListener('mousedown', mouseDownHandler);
// 记录鼠标坐标信息
let x = 0;
let y = 0;
// 鼠标按下事件
function mouseDownHandler(e) {
// 记录鼠标初始位置
x = e.clientX;
y = e.clientY;
// 添加鼠标移动与释放事件
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
// 鼠标移动事件
function mouseMoveHandler(e) {
// 计算鼠标拖动的距离
const dx = e.clientX - x;
const dy = e.clientY - y;
// 将拖动距离赋值给目标元素
drag.style.top = `${drag.offsetTop + dy}px`;
drag.style.left = `${drag.offsetLeft + dx}px`;
// 不断记录鼠标上一个位置
x = e.clientX;
y = e.clientY;
};
// 鼠标释放事件
function mouseUpHandler() {
// 重置相关变量
x = 0;
y = 0;
// 移除事件
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
});
</script>

offsetTop:元素到 offsetParent 顶部的距离。

offsetParent:距离元素最近的一个具有定位的祖宗元素(relative,absolute,fixed),若祖宗都不符合条件,offsetParent 为 body。

效果:

04091.gif
大致原理过程:计算鼠标拖动的横向与纵向距离,再结合元素本身的 offsetTop 与 offsetLeft 信息,就能实现元素的拖动。

没几行代码,应该不难哈,都写上详细的注释啦。

上面用 clientX/Y 获取鼠标的位置信息,那用 pageX/Y 可以不呢❓

clientX:提供了鼠标指针相对于浏览器视口(即当前可见的页面部分)左上角的水平坐标。不论页面是否滚动,clientX 的值都是相对于视口的。

pageX:提供了鼠标指针相对于整个页面左上角的水平坐标,包括了任何由于滚动而不可见的部分。当你滚动页面时,pageX 的值会改变,因为它考虑了滚动的距离。

简而言之,就是要不要考虑滚动条的问题,如果你想要获取鼠标指针相对于整个页面的位置,应该使用 pageX。如果你只关心鼠标指针在当前视口内的位置,那么 clientX 就足够了。

网上找了个图,可以瞧瞧:
image.png

列表拖动

简单的整完,咱们开始上点强度💀,这次要做的功能如下:

04092.gif
看着可能稍微有点复杂,但实际还好啦,我们一步一步来完成。

一样,先把布局与样式整一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
html复制代码<!DOCTYPE html>
<html>
<head>
<title>列表拖动</title>
<style>
.item {
width: 200px;
height: 60px;
border: 1px solid #cbd5e0;
display: flex;
justify-content: center;
align-items: center;
cursor: move;
user-select: none;
margin: 10px 0;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="list">
<div class="item">第一个元素</div>
<div class="item">第二个元素</div>
<div class="item">第三个元素</div>
<div class="item">第四个元素</div>
<div class="item">第五个元素</div>
</div>
</body>

接下来,让每个元素变成可拖动元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
html复制代码<script>
document.addEventListener('DOMContentLoaded', () => {
const list = document.getElementById('list');
list.querySelectorAll('.item').forEach(item => {
// 批量添加事件
item.addEventListener('mousedown', mouseDownHandler);
});
// 记录鼠标在拖动元素上的位置信息
let x = 0;
let y = 0;
// 记录当前的拖动元素
let draggingElement;

function mouseDownHandler(e) {
// 记录拖动元素
draggingElement = e.target;
// 计算鼠标在拖动元素上的位置信息
const rect = draggingElement.getBoundingClientRect();
x = e.clientX - rect.left;
y = e.clientY - rect.top;
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
function mouseMoveHandler(e) {
// 计算拖动元素的最新位置
const left = e.clientX - x;
const top = e.clientY - y;
// 将移动距离赋值给目标元素
draggingElement.style.position = 'absolute';
draggingElement.style.top = `${top}px`;
draggingElement.style.left = `${left}px`;
};
function mouseUpHandler() {
// 布局恢复原样(列表布局肯定一直是一个样啦)
draggingElement.style.removeProperty('top');
draggingElement.style.removeProperty('left');
draggingElement.style.removeProperty('position');
x = 0;
y = 0;
draggingElement = null;
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
}
});
</script>

与前面讲的差不多,熟悉的配方。😗

稍微有一点区别是,将元素变成可拖动的逻辑与前面讲的不太一致了。😓

这里用上了 getBoundingClientRect API,其作用是为了优化前面在 mouseMoveHandler 函数中,需要不断去记录鼠标上一个位置的繁琐过程。

大概二者的区别如下:

1️⃣ 拖动元素的位置 = 拖动元素原本位置 + 拖动距离

2️⃣ 拖动元素的位置 = 根据鼠标最新位置直接计算拖动元素的最新位置 = 鼠标最新位置 - 鼠标在拖动元素上的距离

鼠标在拖动元素上的距离:

image.png

效果如下:

04093.gif
现在每个元素都能拖动了,只是还没有加上交换的逻辑。

但,这看着是不是有点奇怪?当拖动一个元素,列表下面的元素就顶上来了,这与咱们一开始看到的效果不太一致吖❗

这是因为缺少了一个占位元素,当在拖动元素时,需要自动插入一个占位元素,保持列表布局不会变化,拖动交换元素时,也应该是占位元素与其他元素进行交换,拖动结束时,再将占位元素给删除,将位置让给拖动元素。

下面,我们来看看如何解决这个占位元素的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
javascript复制代码<!DOCTYPE html>
<html>
<head>
<style>
...
/* 占位元素样式 */
.placeholder {
box-sizing: border-box;
background-color: #edf2f7;
margin: 10px 0;
border: 2px dashed #cbd5e0;
}
</style>
</head>

<script>
document.addEventListener('DOMContentLoaded', () => {
...
let draggingElement;
// 站位元素
let placeholder;
// 是否正在拖动中
let isDraggingStarted = false;

function mouseMoveHandler(e) {
const draggingRect = draggingElement.getBoundingClientRect();
// 仅在移动中初次创建一次
if (!isDraggingStarted) {
isDraggingStarted = true;
// 创建占位元素
placeholder = document.createElement('div');
// 给占位元素添加class
placeholder.classList.add('placeholder');
// 插入占位元素
draggingElement.parentNode.insertBefore(placeholder, draggingElement.nextSibling);
// 保持占位元素与拖动元素一样大小
placeholder.style.width = draggingRect.width + 'px';
placeholder.style.height = draggingRect.height + 'px';
}

const left = e.clientX - x;
...
};
function mouseUpHandler() {
// 拖动结束清除占位元素
placeholder && placeholder.parentNode.removeChild(placeholder);
isDraggingStarted = false;

draggingElement.style.removeProperty('top');
...
};
});
</script>

加了大概十行代码,都是比较基础的DOM操作。👀

瞧瞧效果:

04101.gif
有那味了。🤡

最后,咱们差一步了,就是根据拖动方向进行元素之间的交换。

看到”拖动方向”加粗没😯?这是关键点,我们要如何知道拖动元素是往上还是往下呢❓并且交换元素位置的时机如何把握呢❓

看如下图,假设了中间三个元素的中心点坐标分别如下图。

image.png
当我们拖动第三个元素,往上,第三个元素的中心点坐标会不断变化,可能会变成(10,19)、(10,18)、(10,17)、……。

当继续往上拖动,第三个元素中心点坐标变成(10,9)时,它的纵坐标比第二个元素中心点纵坐标小了,这个时候就需要交换位置❗

根据这个原理过程,咱们先来写一个判断拖动方向的函数,如下:

1
2
3
4
5
6
7
8
9
javascript复制代码// 检测拖动元素是否向上拖动
function isAbove(nodeA, nodeB) {
const rectA = nodeA.getBoundingClientRect();
const rectB = nodeB.getBoundingClientRect();
// 计算中心点纵坐标
const centerPointA = rectA.top + rectA.height / 2;
const centerPointB = rectB.top + rectB.height / 2;
return centerPointA < centerPointB;
};

应该很好理解吧?🌟

交换元素的过程,咱们也可以单独写一个函数,如下:

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码// 交换两个相邻的元素位置
function swap(nodeA, nodeB) {
// 获取父节点,为后续插入提供一个支点
const parentA = nodeA.parentNode;
// 获取参照节点
const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;
// 将A节点移动到参照节点之前
nodeB.parentNode.insertBefore(nodeA, nodeB);
// 将B节点移动到参照节点之前
parentA.insertBefore(nodeB, siblingA);
};

虽然只有短短四行代码,但这个时候就非常考验你的 Javascript 基础了。

(当然,这种东西你也可以直接问GPT,捡现成的。👻)

最后,结合具体逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javascript复制代码function mouseMoveHandler() {
...

// 获取拖动时的上下元素
const prevEle = draggingElement.previousElementSibling;
const nextEle = placeholder.nextElementSibling;
// 向上移动(没上一个元素了,代表到边界了,不用处理)
if (prevEle && isAbove(draggingElement, prevEle)) {
// 占位元素要先与拖动元素交换位置❗
swap(placeholder, draggingElement);
// 占位元素与上一个元素交换位置
swap(placeholder, prevEle);
return;
}
// 向下移动
if (nextEle && !isAbove(draggingElement, nextEle)) {
swap(nextEle, placeholder);
swap(nextEle, draggingElement);
}
}

我们仅需在 mouseMoveHandler 函数中添加交换逻辑即可。

这里要注意”占位元素要先与拖动元素交换位置”,可能你会有疑问?不是直接交换占位元素与上一个元素(或下一个元素)就行咩?😢

我们可以看看实际的DOM结构,第二个元素与占位元素中间还隔着拖动元素呢,注意我们是要交换两个相邻的元素,不是随便两个相隔遥远的元素哦。

image.png

好,到此完毕,列表拖动就完成啦。👻

表格拖动-列

接下来要做的是表格上的拖动,也是比较常见的功能了,话不多说,先看效果图:

04102.gif
做之前咱们先来分析一波,由于我们要拖动的是列,是竖着纵向排列的,而表格可是按照横向进行布局的❗

表格的布局结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
html复制代码<table id="table" class="table">
<thead>
<tr>
<th>序号</th>
<th>日期</th>
<th>姓名</th>
<th>省份</th>
<th>城市</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>2024-04-01</td>
<td>小小红</td>
<td>广东</td>
<td>广州市</td>
<td>广东省广州市xxxxxx</td>
</tr>
<tr>
<td>2</td>
<td>2024-04-02</td>
<td>小小绿</td>
<td>广东</td>
<td>深圳市</td>
<td>广东省深圳市xxxxxx</td>
</tr>
<tr>
<td>3</td>
<td>2024-04-03</td>
<td>小小黄</td>
<td>广东</td>
<td>中山市</td>
<td>广东省中山市xxxxx</td>
</tr>
<tr>
<td>4</td>
<td>2024-04-04</td>
<td>小小白</td>
<td>广东</td>
<td>佛山市</td>
<td>广东省佛山市xxxx</td>
</tr>
<tr>
<td>5</td>
<td>2024-04-05</td>
<td>小小黑</td>
<td>广东</td>
<td>汕头市</td>
<td>广东省汕头市xxxxxx</td>
</tr>
</tbody>
</table>

image.png
这…可不利于我们拖动处理呀❗我们要拖动的列必须是一个整体、是整个块,要是跨元素就麻烦了。

🌟这里咱们就要换个思路了,在要开始拖动时,动态创建一个纵向的列表,列表的每一子项就是表格的列,其实就是将表格转成我们上面已经讲过的列表拖动来进行操作;然后隐藏原表格,操作这个新列表,当拖动结束的时候,我们再通过列表的索引信息来交换表格的格子就行啦,是不是手拿把掐。😁(注意是拖动列表的项!!!)

那咱们先来看看如何动态创建出这个列表叭。😉

相关 HTML 结构就是上面那个表格布局了,没了。

相关 CSS 样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
html复制代码<!DOCTYPE html>
<html>
<head>
<style>
.table {
border: 1px solid #ccc;
border-collapse: collapse;
}
.table th,
.table td {
border: 1px solid #ccc;
}
.table th,
.table td {
padding: 10px;
text-align: center;
box-sizing: border-box;
}
.draggable {
cursor: move;
user-select: none;
}
.list {
border-left: 1px solid #ccc;
border-top: 1px solid #ccc;
display: flex;
}
.list__table {
border-collapse: collapse;
border: none;
}
.list__table th,
.list__table td {
border: 1px solid #ccc;
border-left: none;
border-top: none;
box-sizing: border-box;
padding: 10px;
text-align: center;
color: red; /* 为了演示,加个红色 */
}
.placeholder {
background-color: #edf2f7;
border: 2px dashed #cbd5e0;
box-sizing: border-box;
}
.dragging {
background: #fff;
border-left: 1px solid #ccc;
border-top: 1px solid #ccc;
z-index: 999;
}
</style>
</head>

上面列出了所有样式,对照着看看就行,样式不是重点,但是其中值得关注的是动态创建的列表,它的 border 是如何变成和表格的一样?因为稍微缺失一个格子的边框,可能就会造成列表和原表格不重合,容易露馅,这点你可以仔细琢磨一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
javascript复制代码<script>
document.addEventListener('DOMContentLoaded', () => {
const table = document.getElementById('table');
table.querySelectorAll('th').forEach(headerCell => {
// 给可拖动元素添加一个样式类
headerCell.classList.add('draggable');
headerCell.addEventListener('mousedown', mouseDownHandler);
});

let draggingElement;
// 记录拖动列的索引
let draggingColumnIndex;
let x = 0;
let y = 0;
let isDraggingStarted = false;

function mouseDownHandler(e) {
// 找到拖动列的索引
draggingColumnIndex = [].slice.call(table.querySelectorAll('th')).indexOf(e.target);
x = e.clientX - e.target.offsetLeft;
y = e.clientY - e.target.offsetTop;
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
function mouseMoveHandler(e) {
if (!isDraggingStarted) {
isDraggingStarted = true;
// 动态创建列表
createList();
}
}
// 创建一个表格列表
function createList() {
const rect = table.getBoundingClientRect();
list = document.createElement('div');
list.classList.add('list');
// 覆盖在表格上,如果是在局部,需要在共同的父元素上加上relative,小编这里父元素是body,就不用了。
list.style.position = 'absolute';
list.style.left = rect.left + 'px';
list.style.top = rect.top + 'px';
// 列表插入文档中
table.parentNode.insertBefore(list, table);
// 隐藏原表格
table.style.visibility = 'hidden';
// 获取表格所有的格子
const originalCells = [].slice.call(table.querySelectorAll('tbody td'));
// 获取表格的表头格子
const originalHeaderCells = [].slice.call(table.querySelectorAll('th'));
const numColumns = originalHeaderCells.length;
// 循环列
originalHeaderCells.forEach((headerCell, headerIndex) => {
const { width } = window.getComputedStyle(headerCell);
// 创建列表子项,也就是新的列
const item = document.createElement('div');
item.classList.add('draggable');
// 子项是一个只有单列的表格,这就是上面样式中提到的列表的border如何保持和表格的边框一样的技巧
const newTable = document.createElement('table');
newTable.setAttribute('class', 'list__table');
newTable.style.width = width;
// 子项表格的表头
const th = headerCell.cloneNode(true);
let newRow = document.createElement('tr');
newRow.appendChild(th);
newTable.appendChild(newRow);
// 子项表格的数据,在所有格子中找到属于这一列的格子
const cells = originalCells.filter((c, idx) => {
// 代入几个格子索引算算就清楚啦😉
return (idx - headerIndex) % numColumns === 0;
});
// 找到这列的格子后,给格子加上对应列的宽度,再把它们包装成一个行tr,再插入就可以了
cells.forEach(cell => {
const newCell = cell.cloneNode(true);
newCell.style.width = width + 'px';
newRow = document.createElement('tr');
newRow.appendChild(newCell);
newTable.appendChild(newRow);
});
// 把子项表格追加到新列中,再把新列追加到列表中,完事
item.appendChild(newTable);
list.appendChild(item);
});
}
function mouseUpHandler() {
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
}
});
</script>

很熟悉吧,代码中很多都是前面讲过的了。

重点只有 createList 函数,它的作用就是创建一个与表格一样的列表,外观是一致的,只是与表格不同的是,它的布局是纵向的,就这么简单,详细的可以瞧瞧代码过程。👻

image.png

现在列表有了,操作列表的拖动这块咱熟呀,直接整上。

仅需改动 mouseMoveHandler 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
javascript复制代码function mouseMoveHandler(e) {
if (!isDraggingStarted) {
isDraggingStarted = true;
createList();
// 通过索引获取拖动元素
draggingElement = [].slice.call(list.children)[draggingColumnIndex];
draggingElement.classList.add('dragging');
// 继续创建占位元素
placeholder = document.createElement('div');
placeholder.classList.add('placeholder');
draggingElement.parentNode.insertBefore(placeholder, draggingElement.nextSibling);
// 因为是flex布局,不用设置高度也可以
placeholder.style.width = draggingElement.offsetWidth + 'px';
}

// 和元素拖动的过程一样
draggingElement.style.position = 'absolute';
draggingElement.style.top = (draggingElement.offsetTop + e.clientY - y) + 'px';
draggingElement.style.left = (draggingElement.offsetLeft + e.clientX - x) + 'px';
x = e.clientX;
y = e.clientY;

// 交换元素,与列表拖动的一样
const prevEle = draggingElement.previousElementSibling;
const nextEle = placeholder.nextElementSibling;
if (prevEle && isOnLeft(draggingElement, prevEle)) {
swap(placeholder, draggingElement);
swap(placeholder, prevEle);
return;
}
if (nextEle && isOnLeft(nextEle, draggingElement)) { // 元素换个位置而已,用!取反也是一样的
swap(nextEle, placeholder);
swap(nextEle, draggingElement);
}
}
function swap(nodeA, nodeB) {
const parentA = nodeA.parentNode;
const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;
nodeB.parentNode.insertBefore(nodeA, nodeB);
parentA.insertBefore(nodeB, siblingA);
};
function isOnLeft(nodeA, nodeB) {
const rectA = nodeA.getBoundingClientRect();
const rectB = nodeB.getBoundingClientRect();
const centerPointA = rectA.left + rectA.width / 2;
const centerPointB = rectB.left + rectB.width / 2;
return centerPointA < centerPointB;
};

新增加的 swap 与 isOnLeft 函数在列表拖动的时候都讲过啦,这里就不多说了。

做到这里,你的表格(列表)应该是可以正常拖动了,只是拖动后的效果还能不真正同步到表格中而已,差最后一步,咱也给它加上、加上。😀

咱们仅需要改动 mouseUpHandler 函数,在拖动结束的时候将列表子项的索引信息同步回原表格上,然后把列表移除就可以了。

具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
javascript复制代码function mouseUpHandler() {
// 移除占位元素
placeholder && placeholder.parentNode.removeChild(placeholder);
// 恢复拖动元素样式
draggingElement.classList.remove('dragging');
draggingElement.style.removeProperty('top');
draggingElement.style.removeProperty('left');
draggingElement.style.removeProperty('position');
// 获取拖动元素最后的索引
const endColumnIndex = [].slice.call(list.children).indexOf(draggingElement);
isDraggingStarted = false;
// 移除创建的列表
list.parentNode.removeChild(list);
// 将列表的信息同步到原表格中
table.querySelectorAll('tr').forEach(row => {
// 获取每一行的格子
const cells = [].slice.call(row.querySelectorAll('th, td'));
if (draggingColumnIndex > endColumnIndex) { // 往左拖动
// 将目标格子(cells[draggingColumnIndex])放到最新的位置上
cells[endColumnIndex].parentNode.insertBefore(
cells[draggingColumnIndex], cells[endColumnIndex]);
}else { // 往右拖动
cells[endColumnIndex].parentNode.insertBefore(
cells[draggingColumnIndex], cells[endColumnIndex].nextSibling);
}
});
// 恢复原表格的展示
table.style.removeProperty('visibility');
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};

应该不是很难哈,都写上了详细的注释。

好啦,就这么多,到此,咱们就完成了开头看到的表格列拖动的效果了。👻👻👻

表格拖动-行

既然讲了表格的列拖动了,那么行的拖动肯定也是不能落下啦。😁

不过现在我们有了前面的基础,这个不是洒洒水?有手就行?

HTML 结构不变。

CSS 样式略微调整一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
css复制代码...
.list {
/* border-left: 1px solid #ccc; */
border-top: 1px solid #ccc;
/* display: flex; */
}
.list__table th,
.list__table td {
border: 1px solid #ccc;
/* border-left: none; */
border-top: none;
box-sizing: border-box;
padding: 10px;
text-align: center;
color: red;
}
...

主要还是 JS 逻辑部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
javascript复制代码<script>
document.addEventListener('DOMContentLoaded', () => {
const table = document.getElementById('table');
table.querySelectorAll('tr').forEach((row, index) => {
// 表格不能拖动,跳过
if (index === 0) return;
// 第一列的第一个格子才能拖动
const firstCell = row.firstElementChild;
firstCell.classList.add('draggable');
firstCell.addEventListener('mousedown', mouseDownHandler);
});

// 记录拖动的行索引
let draggingColumnIndex;
let x = 0;
let y = 0;
let isDraggingStarted = false;
let list;
let draggingElement;
let placeholder;

function mouseDownHandler(e) {
const originalRow = e.target.parentNode;
draggingRowIndex = [].slice.call(table.querySelectorAll('tr')).indexOf(originalRow);
x = e.clientX;
y = e.clientY;
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
function mouseMoveHandler(e) {
if (!isDraggingStarted) {
isDraggingStarted = true;
createList();
draggingElement = [].slice.call(list.children)[draggingRowIndex];
draggingElement.classList.add('dragging');
placeholder = document.createElement('div');
placeholder.classList.add('placeholder');
draggingElement.parentNode.insertBefore(placeholder, draggingElement.nextSibling);
placeholder.style.height = draggingElement.offsetHeight + 'px';
}
draggingElement.style.position = 'absolute';
draggingElement.style.top = (draggingElement.offsetTop + e.clientY - y) + 'px';
draggingElement.style.left = (draggingElement.offsetLeft + e.clientX - x) + 'px';
x = e.clientX;
y = e.clientY;
const prevEle = draggingElement.previousElementSibling;
const nextEle = placeholder.nextElementSibling;
if (prevEle && prevEle.previousElementSibling && isAbove(draggingElement, prevEle)) {
swap(placeholder, draggingElement);
swap(placeholder, prevEle);
return;
}
if (nextEle && isAbove(nextEle, draggingElement)) {
swap(nextEle, placeholder);
swap(nextEle, draggingElement);
}
};
function createList() {
const rect = table.getBoundingClientRect();
const width = window.getComputedStyle(table).width;
list = document.createElement('div');
list.classList.add('list');
list.style.position = 'absolute';
list.style.left = rect.left + 'px';
list.style.top = rect.top + 'px';
table.parentNode.insertBefore(list, table);
table.style.visibility = 'hidden';
// 循环行
table.querySelectorAll('tr').forEach(row => {
const item = document.createElement('div');
item.classList.add('draggable');
// 子项是一个只有一行的表格,这就是上面样式中提到的列表的border如何保持和表格的边框一样的技巧
const newTable = document.createElement('table');
newTable.setAttribute('class', 'list__table');
newTable.style.width = width;
const newRow = document.createElement('tr');
const cells = [].slice.call(row.children);
cells.forEach(cell => {
const newCell = cell.cloneNode(true);
// 每个格子还是原来格子的宽度
newCell.style.width = window.getComputedStyle(cell).width;
newRow.appendChild(newCell);
});
newTable.appendChild(newRow);
item.appendChild(newTable);
list.appendChild(item);
});
};
function swap(nodeA, nodeB) {
// ... 一样的,不写了
};
function isAbove(nodeA, nodeB) {
const rectA = nodeA.getBoundingClientRect();
const rectB = nodeB.getBoundingClientRect();
const centerPointA = rectA.top + rectA.height / 2;
const centerPointB = rectB.top + rectB.height / 2;
return centerPointA < centerPointB;
};
function mouseUpHandler() {
placeholder && placeholder.parentNode.removeChild(placeholder);
draggingElement.classList.remove('dragging');
draggingElement.style.removeProperty('top');
draggingElement.style.removeProperty('left');
draggingElement.style.removeProperty('position');
const endRowIndex = [].slice.call(list.children).indexOf(draggingElement);
isDraggingStarted = false;
list.parentNode.removeChild(list);
let rows = [].slice.call(table.querySelectorAll('tr'));
// 行的交换就简单了,直接整行换就行了
if (draggingRowIndex > endRowIndex) {
rows[endRowIndex].parentNode.insertBefore(
rows[draggingRowIndex],
rows[endRowIndex]
);
}else {
rows[endRowIndex].parentNode.insertBefore(
rows[draggingRowIndex],
rows[endRowIndex].nextSibling
);
}
table.style.removeProperty('visibility');
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
});
</script>

大部分还是与上面讲过的一样,有一些略微差别而已,需要注意方向相关的就可以了。

还有就是动态创建的列表变成如下的样子了:

image.png

最后,放个效果图:

04111.gif
完整源码


传送门 👈👈👈


至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JWT重放漏洞如何攻防?你的系统安全吗? 1、初识JWT重放

发表于 2024-04-11
大家好,我是石头~


在数字化浪潮席卷全球的今天,JSON Web Token(JWT)作为身份验证的利器,已成为众多Web应用的首选方案。


然而,正如硬币有两面,JWT的强大功能背后也隐藏着潜在的安全风险,其中“重放攻击”便是不容忽视的一环。


那么,究竟什么是JWT重放漏洞?它如何威胁我们的系统安全?又该如何有效修复?


今天,我们就来深入剖析这个问题,让你的系统固若金汤。

view.png

1、初识JWT重放漏洞:一场时空穿越的游戏

首先,让我们对JWT重放漏洞有一个直观的认识。


想象一下,你是一名电影导演,正在拍摄一部关于时间旅行的科幻大片。

2023_0508_7f39c218g00rubohh007od200dw005qg00it007q.gif

主角手握一枚神秘的“时光令牌”(JWT),凭借它能自由穿梭于过去和未来。


然而,一旦这枚令牌落入反派手中,他们便可以随意模仿主角的行为,引发一系列混乱。


这就是JWT重放漏洞的现实写照。


具体来说,JWT是由服务器签发的一种包含用户信息的加密令牌,客户端通过携带此令牌访问受保护资源。


重放攻击就是攻击者截获并重新发送一个有效的JWT,利用其未过期的有效性,冒充合法用户执行操作,如同电影中的反派复制主角的“时光令牌”,在不同时间、不同场景中重复使用。

v2-3baabff114b824104dc8b1d2fb1d646e_r.jpg

2、重放漏洞的危害有多深?

面对JWT重放漏洞,你的系统可能面临以下严峻挑战:
  • 权限滥用:攻击者可以重复使用已获取的JWT,进行越权操作,如非法登录、篡改数据、甚至进行金融交易等敏感操作,严重侵犯用户权益,破坏系统秩序。
  • 资源耗尽:大规模的重放攻击可能导致服务器处理大量无效请求,占用系统资源,降低服务性能,甚至引发拒绝服务攻击(DoS)。
  • 业务逻辑漏洞放大:如果系统存在业务逻辑缺陷,如订单状态更新、积分兑换等操作未做严格的幂等性控制,重放攻击可能会被恶意利用,进一步扩大损失。

u=338495599,3858702345&fm=253&fmt=auto&app=138&f=JPEG.webp

3、如何有效修复JWT重放漏洞?

明白了JWT重放漏洞的危害,我们接下来就要探讨如何构筑防线,让系统无懈可击。

t014036d529ad859760.jpg

以下是一些建议性的防御措施:

时间戳与有效期结合

为JWT添加一个“发行时间”(iat) claim,并结合“过期时间”(exp)进行双重防护。


服务器在验证JWT时,除了检查exp是否未过期,还要确保当前时间与iat之间的时间差在合理范围内(例如,允许的最大时延)。


这样可以防止攻击者长时间保存JWT并伺机重放。

使用一次性Token(Nonce)

在特定操作(如重要交易、密码修改等)中,引入一次性Token(Nonce)。


服务器在生成JWT时附带一个随机生成且仅使用一次的Nonce值,客户端在请求时需同时提交该值。


服务器在验证JWT时,除了常规校验外,还需确认Nonce未被使用过。如此一来,即使JWT被截获,攻击者也无法重复使用。

实施滑动窗口策略

滑动窗口是一种更精细的时间戳验证机制。


设定一个固定的时间窗口(如5分钟),只允许在这个窗口内的JWT有效。每验证一个JWT,就将窗口向前滑动,抛弃窗口外的所有JWT。


这种动态调整的方式,能够有效抵御基于时间的重放攻击。

增强业务逻辑防护

对敏感操作进行严格的幂等性设计,确保同一操作无论执行多少次,结果都保持一致,从而减少重放攻击的影响。


例如,订单支付操作应确保同一笔订单不能被多次支付;用户密码修改后,旧密码应立即失效。

引入Token黑名单机制

对于已撤销或过期的JWT,将其加入服务器端的黑名单,并定期更新。


当接收到请求时,除了常规验证外,还应检查JWT是否存在于黑名单中。


虽然该方法会增加一定的存储和计算开销,但对于高安全要求的场景,不失为一种有效的补充防护手段。

4、安全无小事,行动起来!

JWT重放漏洞虽隐匿却危害甚大,但只要我们理解其原理,采取针对性的防御措施,就能有效封堵这一安全隐患。


记住,安全无小事,每一处细节都关乎系统的生死存亡。


审视你的系统,看看是否有被重放攻击“趁虚而入”的可能,及时打上补丁,让每一次验证、每一次交互都充满信任与安全感。


同时,欢迎在评论区分享你的防护经验或提出疑问,让我们共同探讨,共筑网络安全长城。

20190430163854_7114.jpg

**MORE | 更多精彩文章**

  • H5推送,为什么都用WebSocket?
  • 别再这么写POST请求了~
  • 揭秘布谷鸟过滤器:一场数据过滤中的“鸠占鹊巢”大戏!
  • JWT:你真的了解它吗?
  • JWT vs Session:到底哪个才是你的菜?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MyBatis-Plus 还手写 Join 联表查询?一个依

发表于 2024-04-11

图片

众所周知,Mybatis Plus 封装的 mapper 不支持 join,如果需要支持就必须自己去实现。

但是对于大部分的业务场景来说,都需要多表 join,要不然就没必要采用关系型数据库了。

那么有没有一种不通过硬 SQL 的形式,通过框架提供 join 能力呢?答案是,可以有。

使用方法

安装

  • Maven
1
2
3
4
5
xml复制代码<dependency>
    <groupId>com.github.yulichang</groupId>
    <artifactId>mybatis-plus-join-boot-starter</artifactId>
    <version>1.4.4</version>
</dependency>
  • Gradle
1
arduino复制代码implementation 'com.github.yulichang:mybatis-plus-join-boot-starter:1.4.4'

或者 clone 代码到本地执行 mvn install,再引入以上依赖。

注意:mybatis plus version >= 3.4.0。

使用

  • mapper继承MPJBaseMapper (必选)
  • service继承MPJBaseService (可选)
  • serviceImpl继承MPJBaseServiceImpl (可选)

核心类MPJLambdaWrapper和MPJQueryWrapper

MPJLambdaWrapper用法

简单的三表查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
php复制代码class test {
    @Resource
    private UserMapper userMapper;

    void testJoin() {
        //和Mybatis plus一致,MPJLambdaWrapper的泛型必须是主表的泛型,并且要用主表的Mapper来调用
        MPJLambdaWrapper<UserDO> wrapper = new MPJLambdaWrapper<UserDO>()
                .selectAll(UserDO.class)//查询user表全部字段
                .select(UserAddressDO::getTel)//查询user_address tel 字段
                .selectAs(UserAddressDO::getAddress, UserDTO::getUserAddress)//别名 t.address AS userAddress
                .select(AreaDO::getProvince, AreaDO::getCity)
                .leftJoin(UserAddressDO.class, UserAddressDO::getUserId, UserDO::getId)
                .leftJoin(AreaDO.class, AreaDO::getId, UserAddressDO::getAreaId)
                .eq(UserDO::getId, 1)
                .like(UserAddressDO::getTel, "1")
                .gt(UserDO::getId, 5);

        //连表查询 返回自定义ResultType
        List<UserDTO> list = userMapper.selectJoinList(UserDTO.class, wrapper);

        //分页查询 (需要启用 mybatis plus 分页插件)
        Page<UserDTO> listPage = userMapper.selectJoinPage(new Page<>(2, 10), UserDTO.class, wrapper);
    }
}

对应sql

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码SELECT  
    t.id, t.name, t.sex, t.head_img, 
    t1.tel, t1.address AS userAddress,
    t2.province, t2.city 
FROM 
    user t 
    LEFT JOIN user_address t1 ON t1.user_id = t.id 
    LEFT JOIN area t2 ON t2.id = t1.area_id 
WHERE (
    t.id = ? 
    AND t1.tel LIKE ? 
    AND t.id > ?)

说明:

  • UserDTO.class 查询结果返回类(resultType)
  • selectAll() 查询指定实体类的全部字段
  • select() 查询指定的字段,支持可变参数,同一个select只能查询相同表的字段
  • selectAs() 字段别名查询,用于数据库字段与业务实体类属性名不一致时使用
  • leftJoin() 参数说明;第一个参数: 参与连表的实体类class 第二个参数: 连表的ON字段,这个属性必须是第一个参数实体类的属性 第三个参数: 参与连表的ON的另一个实体类属性
  • 默认主表别名是t,其他的表别名以先后调用的顺序使用t1,t2,t3….
  • 条件查询,可以查询主表以及参与连接的所有表的字段,全部调用mp原生的方法,正常使用没有sql注入风险

MPJLambdaWrapper 还有很多其他的功能

  • 简单的SQL函数使用:https://gitee.com/best_handsome/mybatis-plus-join/wikis/selectFunc()?sort_id=4082479
  • ON语句多条件支持:https://gitee.com/best_handsome/mybatis-plus-join/wikis/leftJoin?sort_id=3496671

等效于ResultMap

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<resultMap id="xxxxxxxx" type="com.github.yulichang.join.dto.UserDTO">
    <result property="id" column="id"/>
    <result property="name" column="name"/>
    <!--其他属性省略-->
    <collection property="addressList" javaType="java.util.List"
                ofType="com.github.yulichang.join.entity.UserAddressDO">
        <id property="id" column="mpj_id"/>
        <result property="address" column="address"/>
        <result property="userId" column="user_id"/>
        <!--其他属性省略-->
    </collection>
</resultMap>

MPJLambdaWrapper其他功能

  • 一对一,一对多使用:ylctmh.com/pages/core/…
  • 简单的SQL函数使用:ylctmh.com/pages/core/…
  • ON语句多条件支持:ylctmh.com/pages/core/…

String形式用法(MPJQueryWrapper)

简单的连表查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
csharp复制代码class test {
    @Resource
    private UserMapper userMapper;

    void testJoin() {
        MPJQueryWrapper wrapper = new MPJQueryWrapper<UserDO>()
                .selectAll(UserDO.class)
                .select("addr.tel", "addr.address", "a.province")
                .leftJoin("user_address addr on t.id = addr.user_id")
                .rightJoin("area a on addr.area_id = a.id")
                .like("addr.tel", "1")
                .le("a.province", "1");

        //列表查询
        List<UserDTO> list = userMapper.selectJoinList(UserDTO.class, wrapper);

        //分页查询 (需要启用 mybatis plus 分页插件)
        Page<UserDTO> listPage = userMapper.selectJoinPage(new Page<>(1, 10), UserDTO.class, wrapper);
    }
}

对应sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码SELECT 
    t.id,
    t.name,
    t.sex,
    t.head_img,
    addr.tel,
    addr.address,
    a.province
FROM 
    user t
    LEFT JOIN user_address addr on t.id = addr.user_id
    RIGHT JOIN area a on addr.area_id = a.id
WHERE (
    addr.tel LIKE ?
    AND a.province <= ?)

说明:

  • UserDTO.class 查询结果类(resultType)
  • selectAll(UserDO.class) 查询主表全部字段(主表实体类)默认主表别名 “t”
  • select() mp的select策略是覆盖,以最后一次为准,这里的策略是追加,可以一直select 主表字段可以用lambda,会自动添加表别名,主表别名默认是 t ,非主表字段必须带别名查询
  • leftJoin() rightJoin() innerJoin() 传sql片段 格式 (表 + 别名 + 关联条件)
  • 条件查询,可以查询主表以及参与连接的所有表的字段,全部调用mp原生的方法,正常使用没有sql注入风险

还可以这么操作,但不建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
csharp复制代码class test {
    @Resource
    private UserMapper userMapper;

    void testJoin() {
        List<UserDTO> list = userMapper.selectJoinList(UserDTO.class,
                new MPJQueryWrapper<UserDO>()
                        .selectAll(UserDO.class)
                        .select("addr.tel", "addr.address")
                        //行列转换
                        .select("CASE t.sex WHEN '男' THEN '1' ELSE '0' END AS sex")
                        //求和函数
                        .select("sum(a.province) AS province")
                        //自定义数据集
                        .leftJoin("(select * from user_address) addr on t.id = addr.user_id")
                        .rightJoin("area a on addr.area_id = a.id")
                        .like("addr.tel", "1")
                        .le("a.province", "1")
                        .orderByDesc("addr.id"));
    }
}

对应sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码SELECT 
    t.id,
    t.name,
    t.sex,
    t.head_img,
    addr.tel,
    addr.address,
    CASE t.sex WHEN '男' THEN '1' ELSE '0' END AS sex,
    sum(a.province) AS province
FROM 
    user t
    LEFT JOIN (select * from user_address) addr on t.id = addr.user_id
    RIGHT JOIN area a on addr.area_id = a.id
WHERE (
    addr.tel LIKE ?
    AND a.province <= ?)
ORDER BY
    addr.id DESC

这样,我们就能和使用 Mybatis Plus 一样进行表关联操作了!

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利**!**

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我用这6个问题,就验证了2年Java面试者的真实性

发表于 2024-04-11

一、背景

现在又到了金三银四的面试季,想要换新工作的人非常多,最近团队也开始面试几个后端开发人员,在面试前其实已经发现某些简历可能存在一些包装水分和或者不合适,但是还是抱着试一试的态度,万一人家有一些可以让我们感觉还行的条件呢,比如沟通能力、思考能力、协作能力、理解能力等。而在这个过程中,持续了10年+的现象,到现在依然还存在,就是在2-3年这个区间内的程序员,存在一些水分包装的现象,可能并没有实际的干过企业的核心的开发工作就来面试了,本文分享的也是这个.

二、简历筛选

面试前团队收到了8份求职者的简历,已经经过了HR的初筛,然后到了我们这里,认真仔细分析和看了一会儿,我发现这个社会还是老样子,招聘3年的,收到的简历明面上是3年,实际7个是22年毕业的(还差3个月满2年,很多公司都是以毕业时间为准的),1个是21年毕业的,不得不说2-3年的出来找工作的,还是和过去10年一样,水分很大的占了一定的市场。所以真实的情况下可能不止我遇到这个现象,可能其他企业的面试官也遇到了类似的现象。这8个算不是优秀简历,也暴露了很多问题,我总结的如下:

(1)、2个人可能存在培训机构项目包装水分或项目没做过的包装水分

(2)、2个人的简历上不认真,毫无价值和存在错误技术名称和汉字

(3)、2个人的简历项目技术栈老旧,居然还写struts2,非目前主要核心java技术

(4)、2个人的简历,中规中举,比较一般,但是项目应该真实,但是入职后可能需要人带一带

这几份简历共性的问题也不少,比如真的没有体现出来自己简历是认真打磨过的,自己花时间整理的,有的空白区域很多,有的负责的描述依然是使用XXX技术实现aaa,有时候会经常看到有1-4年的小伙伴投递简历说找不到面试,其实这个区间水分非常大,如果想脱颖而出,用心找工作还是非常重要的。以前有位大佬说过:同样的简历,肯定会选那个更加细腻的,排版好看的。也更愿意相信,对自己有严格要求的,写出的代码质量也很高。

最后我们选择了一个可能存在包装的水分的求职者,毕竟他的技术栈和内容与我们公司的需求匹配度更高,万一是包装者中的顶尖梯队的人呢。

三、面试问题

面试开始了,我可能会从几个问题来验证他的真实性和能力,然后再深入问一问。

先来看下简历截图吧大家:

1、问题1:请说说你最做的这个项目组的人员情况和项目微服务的数量吧?

求职者回答:他自己的把研发团队的所有角色都说了一遍,产品经理、项目经理、前端、后端、UI、还说了架构师,同时介绍项目是共享打印机,介绍项目业务的说的时候也是共享打印机,同时微服务的数量是30个。

当他说出团队人员中存在架构师和数量是30个的时候,我开始有些怀疑了,感觉他这个体量和经验和公司环境应该不足以支持他的描述,有架构师的团队不应该是这样的简历和描述,同时自己口述的项目名称和简历上的不一致

2、问题2:既然你们微服务拆分的这么细,这么多,那你说说如果1个接口中,调用了A/B/C三个服务,C失败了怎么办?

这个问题回答的并不好,通过他的回答可以分析出来,他可能没做过一个接口中,调用多个服务的情况,即使有,可能也没有考虑过失败的业务处理方案等

3、问题3:说说你对集群和分布式的理解?

这位兄弟前期说的不清楚,后期突然说的很八股话了,此时我对他产生了一点怀疑,开始在远程视频会议中关注他的行为和动作,我之所以问他这个,我觉得是很多真没有工作过的人,对这个问题的理解有些不清晰,果然让我产生了怀疑。

感觉当面试官问到对XXX理解这种类似的问题的时候,确实很多人对于这种简单的问题说的不太明白,毕竟没有实际企业项目历练,不太容易用别人能够听懂的方式讲述出来,同时自己也没有阅读和学习大量的理论,就出去面试了。

4、问题4:为什么要用RabbitMQ不用其他的消息中间件,MQ重复消费如何解决

这位兄弟始终没有回答出来重复消费兜底的策略:用数据库唯一索引保障幂等性和兜底。

他说了用redis判断、数据库里面加个字段、说用行锁,期间我也问到并发情况下是否安全,一直没有想到数据库的唯一索引。

此时从心理和行为学上看,哥们的手放在了头上,出现了明显的不会和思考,同时眼镜中出现了一点反光和白色的内容,我开始关注他的接下来的行为。

其实,我感觉很多人都在简历上写RabbitMQ,尤其是没有过工作经验的新手或低经验的同学,我始终觉得这个价值不大,面试官也不太容易问到原理和源码类的内容,既然是面试Java工程师,为啥不谢谢RocketMQ呢,只要稍微看下源码和原理就已经能够在面试中领先大多数人了,看下源码后,不仅面试能提高通过率,还能对自己的技术有提高。

5、问题5:我看他写了订单和支付的功能和业务,我问请说说回调接口和预支付单创建接口如何保障安全问题、并发问题、重复支付、重复回调问题

这位兄弟的这个问题彻底的让我觉得他写的业务确实没怎么深入做过,很多写支付订单的业务的面试者的简历,我可能都会问他类似的问题,这个问题如果说的足够透彻,那应该是真的做过,开发者做过支付和订单相关业务的,应该会比较理解,为了保证支付回调、取消订单、预支付相关接口的安全并发问题,通常可以用分布式锁来解决,很多人包括这个兄弟一直没有说出来,我也想引导他。应该是自己开发或者学习的时候,只调用API走了个流程而已。

在之前学习支付和订单业务的时候,看到这个很有价值的图,很多没有做过企业级支付开发功能的小伙伴基本不知道用分布式锁解决支付回调、支付预支付、支付取消阶段需要用分布式锁进行控制,同事很多IT培训机构的老师在讲课的时候,也基本不会太关注真实的企业工作中的各种细节问题

图片分享给大家:

其实,我觉的这个问题的有一部分在于,很多面试者和开发者无法申请个人的支付平台商户,导致一直没有去主动性的学习支付业务和功能,其实国内已经有一些支持个人签约的支付平台了,比如蓝兔支付,云狗支付。之前分享过一篇文章:

个人账号如何用微信支付开发,试试蓝兔支付吧

之前还封装了一个基于蓝兔支付的SDK开源项目:

github.com/wuchubuzai2…

6、问题6:问他微服务注册nacos下,A如何调用的B服务和C服务

此时他真的说不清楚了,表达的意思就是A服务如果调用B服务和C服务需要通过网关来调用,然后我在会议中发现他的眼镜又存在白色的闪光,反射的闪光中存在了明显的下拉框的数据加载,我在仔细一看这位兄弟竟然边说边百度搜索,然后哥们最终也答出来OpenFeign了,瞬间回想起相关的话术回复了,面试到这里我就结束了,这位兄弟到了面试结束后,也不会知道,我一直能通过远程视频会议看到他的眼镜反光出来的自己屏幕的相关信息,殊不知早已在我的掌控之下,被我发现了。

在面试前,我就非常确信是有问题的简历,但是还是面面看吧,果然不太合适。他的这个简历的非常明显的问题在于:

1、个人经验和项目经验的技术栈过于新、豪华,存在包装风险

2、项目使用了某IT培训机构中的出现的自研框架elegent,个人怀疑他是搞的那个培训机构的项目包装的(不过这个自研框架也是值得探究一番的,地址如下:gitee.com/myelegent/p…

3、其次就是简历中,描述职责的时候,没有用STAR法则或SMART原则来量化来描述,而全是技术类的使用XXX技术实现AAA这种。

四、简历共性问题

最近面试别人的过程中,发现的简历共性问题如下:

1、包装的面试者或者低经验的面试者,确实对自己做的项目业务说不清楚或者缺少自己的想法,面试问问什么选这个,为什么用这个,经常回答是leader搞的,Leader选择的,同事做的,简历上用纯技术描述项目职责,即:使用XXX技术实现aaa,回答的时候也确实过少结合业务来描述

2、常年感觉自己做的项目都是CRUD类型的面试者,有些在项目职责中基本没有表达任何技术名称和解决思路,一股脑的介绍自己介绍的功能和模块,面试官一问,问题更大。

3、简历上的排版明显有问题的面试者,本周公司收到的几份简历中有2-4分都存在很多的错别字之类的,比如mongodb写成manogdb\mangdb的,大小写也看着不舒服的等等,简历上能明显看出来之前做的业务都是边缘功能的,老旧的技术依然写的,没有任何包装的,这样的简历确实不想给面试机会,因为和别人的对比一目了然

4、我觉得更多的是这种情况比较危险,就是1、2、3、4这个年限经验段的,感觉有些人的水平就是很相似的,只是年限不同而已,简历也很大差不差,这也是现在的IT界的工作城市、公司环境、个人认知不齐影响的,再加上2-3年是最容易出现一些包装着,让2-3年这个区间段的不容易找工作,面试公司也不容易筛选出好简历。AI出现一年多了,现在的大部分简历非常明显都是没有经过AI润色的,感觉很多人确实都不怎么用AI呢。

后来面试别人又发现,有个小伙子真胆大,当着远程会议的面,现场鼠标单击和键盘敲击明显的搜索结果的,殊不知他的鼠标声音我这里也能听到,还边吃东西边面试。

五、AI建议的简历内容

现在AI大火,但是很多人写简历真的没有体现出来自己的价值和经验,写的水平还是以前的样子。没有用心认真打磨的简历,既然如此那么企业看到你的简历后,为啥要为你的价值和经验买单呢。

对于简历中的描述项目职责应该着重描述技术实现点还是结合实际业务描述呢?

我最近也问了下AI,感觉挺有道理的,参考如下:

平衡技术与业务描述:简历中描述项目职责时应该既关注技术实现点,也结合实际业务描述。通过平衡技术和业务描述、突出技术亮点、结合实际业务场景、简洁明了地表达以及量化成果,你可以更好地展示自己的能力和价值,增加面试成功的几率。 简历应该既展示你的技术能力,也展示你对业务的理解。你可以从项目的业务需求出发,描述为了解决这些业务需求,你采用了哪些技术方案和实现细节。这样既能体现你的技术实力,也能让面试官了解你对业务的敏感度。

突出技术亮点:在描述项目职责时,可以着重突出你在项目中使用的关键技术、解决的技术难题以及实现的创新点。这些亮点能够体现你的技术能力和解决问题的能力,是面试官非常关注的部分。

结合实际业务场景:除了技术实现点,也要描述项目在实际业务中的应用场景、解决的问题以及带来的效果。这样能够让面试官更好地理解你在项目中的角色和价值,也能展示你对业务的深入理解和实践经验。

简洁明了:在描述项目职责时,要注意言简意赅,避免冗长的句子和复杂的词汇。用简洁的语言清晰地表达你的职责和成果,让面试官能够快速了解你的能力和价值。

量化成果:如果可能的话,尽量用具体的数据或成果来量化你的贡献。例如,你可以描述项目上线后带来的用户增长、性能提升或成本降低等具体成果,这样更具说服力。

希望通过本文的分享,让面试者可以更加注重自己的简历编写内容,面试时避免做的事情。以及面试官应该具备一定的简历筛选能力。

如果你喜欢这篇文章,可以关注、收藏、分享、评论哦

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

看不懂来打我,vue3如何将template编译成rende

发表于 2024-04-11

本文是 vue3编译原理揭秘 的第 8 篇,和该系列的其他文章一起服用效果更佳。

  1. vue3的宏到底是什么东西?
  2. Vue 3 的 setup语法糖到底是什么东西?
  3. 看不懂来打我,vue3的.vue文件(SFC)编译过程
  4. 为什么defineProps宏函数不需要从vue中import导入?
  5. 天天用defineEmits宏函数,竟然不知道编译后是vue2的选项式API?
  6. 面试官:只知道v-model是:modelValue和@onUpdate语法糖,那你可以走了
  7. defineModel是否破坏了vue3的单向数据流呢?
  8. 看不懂来打我,vue3如何将template编译成render函数
  9. 面试官:来说说vue3是怎么处理内置的v-for、v-model等指令?
  10. 面试官:在原生input上面使用v-model和组件上面使用有什么区别?

前言

在之前的 看不懂来打我,vue3的.vue文件(SFC)编译过程 文章中我们讲过了vue文件是如何编译成js文件,通过那篇文章我们知道了,template编译为render函数底层就是调用了@vue/compiler-sfc包暴露出来的compileTemplate函数。由于文章篇幅有限,我们没有去深入探索compileTemplate函数是如何将template模块编译为render函数,在这篇文章中我们来了解一下。

@vue下面的几个包

先来介绍一下本文中涉及到vue下的几个包,分别是:@vue/compiler-sfc、@vue/compiler-dom、@vue/compiler-core。

  • @vue/compiler-sfc:用于编译vue的SFC文件,这个包依赖vue下的其他包,比如@vue/compiler-dom和@vue/compiler-core。这个包一般是给vue-loader 和 @vitejs/plugin-vue使用的。
  • @vue/compiler-dom:这个包专注于浏览器端的编译,处理浏览器dom相关的逻辑都在这里面。
  • @vue/compiler-core:从名字你也能看出来这个包是vue编译部分的核心,提供了通用的编译逻辑,不管是浏览器端还是服务端编译最终都会走到这个包里面来。

先来看个流程图

先来看一下我画的template模块编译为render函数这一过程的流程图,让你对整个流程有个大概的印象,后面的内容看着就不费劲了。如下图:
full-progress

从上面的流程图可以看到整个流程可以分为7步:

  • 执行@vue/compiler-sfc包的compileTemplate函数,里面会调用同一个包的doCompileTemplate函数。
  • 执行@vue/compiler-sfc包的doCompileTemplate函数,里面会调用@vue/compiler-dom包中的compile函数。
  • 执行@vue/compiler-dom包中的compile函数,里面会对options进行了扩展,塞了一些处理dom的转换函数进去。分别塞到了options.nodeTransforms数组和options.directiveTransforms对象中。然后以扩展后的options去调用@vue/compiler-core包的baseCompile函数。
  • 执行@vue/compiler-core包的baseCompile函数,在这个函数中主要分为4部分。第一部分为检查传入的source是不是html字符串,如果是就调用同一个包下的baseParse函数生成模版AST抽象语法树。否则就直接使用传入的模版AST抽象语法树。此时node节点中还有v-for、v-model等指令。这里的模版AST抽象语法树结构和template模块中的代码结构是一模一样的,所以说模版AST抽象语法树就是对template模块中的结构进行描述。
  • 第二部分为执行getBaseTransformPreset函数拿到@vue/compiler-core包中内置的nodeTransforms和directiveTransforms转换函数。
  • 第三部分为将传入的options.nodeTransforms、options.directiveTransforms分别和本地的nodeTransforms、directiveTransforms进行合并得到一堆新的转换函数,和模版AST抽象语法树一起传入到transform函数中执行,就会得到转换后的javascript AST抽象语法树。在这一过程中v-for、v-model等指令已经被转换函数给处理了。得到的javascript AST抽象语法树的结构和将要生成的render函数的结构是一模一样的,所以说javascript AST抽象语法树就是对render函数的结构进行描述。
  • 第四部分为由于已经拿到了和render函数的结构一模一样的javascript AST抽象语法树,只需要在generate函数中遍历javascript AST抽象语法树进行字符串拼接就可以得到render函数了。

关注公众号:【前端欧阳】,解锁我更多vue原理文章。还可以加我微信,让你的朋友圈多一位对vue有深入理解的人。也可以通过微信给我说你想看哪些vue原理文章,我会根据大家的反馈进行创作。

@vue/compiler-sfc包的compileTemplate函数

还是同样的套路,我们通过debug一个简单的demo来搞清楚compileTemplate函数是如何将template编译成render函数的。demo代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<template>
<input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const msgList = ref([
{
id: 1,
value: "",
},
{
id: 2,
value: "",
},
{
id: 3,
value: "",
},
]);
</script>

看不懂来打我,vue3的.vue文件(SFC)编译过程 文章中我们已经知道了在使用vite的情况下template编译为render函数是在node端完成的。所以我们需要启动一个debug终端,才可以在node端打断点。这里以vscode举例,首先我们需要打开终端,然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。
debug-terminal

compileTemplate函数在node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js文件中,找到compileTemplate函数打上断点,然后在debug终端中执行yarn dev(这里是以vite举例)。在浏览器中访问 http://localhost:5173/,此时断点就会走到compileTemplate函数中了。在我们这个场景中compileTemplate函数简化后的代码非常简单,代码如下:

1
2
3
scss复制代码function compileTemplate(options) {
return doCompileTemplate(options);
}

@vue/compiler-sfc包的doCompileTemplate函数

我们接着将断点走进doCompileTemplate函数中,看看里面的代码是什么样的,简化后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码import * as CompilerDOM from '@vue/compiler-dom'

function doCompileTemplate({
source,
ast: inAST,
compiler
}) {
const defaultCompiler = CompilerDOM;
compiler = compiler || defaultCompiler;
let { code, ast, preamble, map } = compiler.compile(inAST || source, {
// ...省略传入的options
});
return { code, ast, preamble, source, errors, tips, map };
}

在doCompileTemplate函数中代码同样也很简单,我们在debug终端中看看compiler、source、inAST这三个变量的值是长什么样的。如下图:
doCompileTemplate

从上图中我们可以看到此时的compiler变量的值为undefined,source变量的值为template模块中的代码,inAST的值为由template模块编译而来的AST抽象语法树。不是说好的要经过parse函数处理后才会得到AST抽象语法树,为什么这里就已经有了AST抽象语法树?不要着急接着向下看,后面我会解释。

由于这里的compiler变量的值为undefined,所以compiler会被赋值为CompilerDOM。而CompilerDOM就是@vue/compiler-dom包中暴露的所有内容。执行compiler.compile函数,就是执行@vue/compiler-dom包中的compile函数。compile函数接收的第一个参数为inAST || source,从这里我们知道第一个参数既可能是AST抽象语法树,也有可能是template模块中的html代码字符串。compile函数的返回值对象中的code字段就是编译好的render函数,然后return出去。

@vue/compiler-dom包中的compile函数

我们接着将断点走进@vue/compiler-dom包中的compile函数,发现代码同样也很简单,简化后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javascript复制代码import {
baseCompile,
} from '@vue/compiler-core'

function compile(src, options = {}) {
return baseCompile(
src,
Object.assign({}, parserOptions, options, {
nodeTransforms: [
...DOMNodeTransforms,
...options.nodeTransforms || []
],
directiveTransforms: shared.extend(
{},
DOMDirectiveTransforms,
options.directiveTransforms || {}
)
})
);
}

从上面的代码中可以看到这里的compile函数也不是具体实现的地方,在这里调用的是@vue/compiler-core包的baseCompile函数。看到这里你可能会有疑问,为什么不在上一步的doCompileTemplate函数中直接调用@vue/compiler-core包的baseCompile函数,而是要从@vue/compiler-dom包中绕一圈再来调用呢baseCompile函数呢?

答案是baseCompile函数是一个处于@vue/compiler-core包中的API,而@vue/compiler-core可以运行在各种 JavaScript 环境下,比如浏览器端、服务端等各个平台。baseCompile函数接收这些平台专有的一些options,而我们这里的demo是浏览器平台。所以才需要从@vue/compiler-dom包中绕一圈去调用@vue/compiler-core包中的baseCompile函数传入一些浏览器中特有的options。在上面的代码中我们看到使用DOMNodeTransforms数组对options中的nodeTransforms属性进行了扩展,使用DOMDirectiveTransforms对象对options中的directiveTransforms属性进行了扩展。

我们先来看看DOMNodeTransforms数组:

1
2
3
ini复制代码const DOMNodeTransforms = [
transformStyle
];

options对象中的nodeTransforms属性是一个数组,里面包含了许多transform转换函数用于处理AST抽象语法树。经过@vue/compiler-dom的compile函数处理后nodeTransforms数组中多了一个处理style的transformStyle函数。这里的transformStyle是一个转换函数用于处理dom上面的style,比如style="color: red"。

我们再来看看DOMDirectiveTransforms对象:

1
2
3
4
5
6
7
8
yaml复制代码const DOMDirectiveTransforms = {
cloak: compilerCore.noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel,
on: transformOn,
show: transformShow
};

options对象中的directiveTransforms属性是一个对象,经过@vue/compiler-dom的compile函数处理后directiveTransforms对象中增加了处理v-cloak、v-html、v-text、v-model、v-on、v-show等指令的transform转换函数。很明显我们这个demo中input标签上面的v-model指令就是由这里的transformModel转换函数处理。

你发现了没,不管是nodeTransforms数组还是directiveTransforms对象,增加的transform转换函数都是处理dom相关的。经过@vue/compiler-dom的compile函数处理后,再调用baseCompile函数就有了处理dom相关的转换函数了。

@vue/compiler-core包的baseCompile函数

继续将断点走进vue/compiler-core包的baseCompile函数,简化后的baseCompile函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
scss复制代码function baseCompile(
source: string | RootNode,
options: CompilerOptions = {},
): CodegenResult {
const ast = isString(source) ? baseParse(source, options) : source

const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()

transform(
ast,
Object.assign({}, options, {
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: Object.assign(
{},
directiveTransforms,
options.directiveTransforms || {}, // user transforms
),
}),
)

return generate(ast, options)
}

我们先来看看baseCompile函数接收的参数,第一个参数为source,类型为string | RootNode。这句话的意思是接收的source变量可能是html字符串,也有可能是html字符串编译后的AST抽象语法树。再来看看第二个参数options,我们这里只关注options.nodeTransforms数组属性和options.directiveTransforms对象属性,这两个里面都是存了一堆转换函数,区别就是一个是数组,一个是对象。

我们再来看看返回值类型CodegenResult,定义如下:

1
2
3
4
5
6
go复制代码interface CodegenResult {
code: string
preamble: string
ast: RootNode
map?: RawSourceMap
}

从类型中我们可以看到返回值对象中的code属性就是编译好的render函数,而这个返回值就是最后调用generate函数返回的。

明白了baseCompile函数接收的参数和返回值,我们再来看函数内的代码。主要分为四块内容:

  • 拿到由html字符串转换成的AST抽象语法树。
  • 拿到由一堆转换函数组成的nodeTransforms数组,和拿到由一堆转换函数组成的directiveTransforms对象。
  • 执行transform函数,使用合并后的nodeTransforms中的所有转换函数处理AST抽象语法树中的所有node节点,使用合并后的directiveTransforms中的转换函数对会生成props的指令进行处理,得到处理后的javascript AST抽象语法树。
  • 调用generate函数根据上一步处理后的javascript AST抽象语法树进行字符串拼接,拼成render函数。

获取AST抽象语法树

我们先来看第一块的内容,代码如下:

1
scss复制代码const ast = isString(source) ? baseParse(source, options) : source

如果传入的source是html字符串,那就调用baseParse函数根据html字符串生成对应的AST抽象语法树,如果传入的就是AST抽象语法树那么就直接赋值给ast变量。为什么这里有这两种情况呢?

原因是baseCompile函数可以被直接调用,也可以像我们这样由vite的@vitejs/plugin-vue包发起,经过层层调用后最终执行baseCompile函数。在我们这个场景中,在前面我们就知道了走进compileTemplate函数之前就已经有了编译后的AST抽象语法树,所以这里不会再调用baseParse函数去生成AST抽象语法树了。那么又是什么时候生成的AST抽象语法树呢?

在之前的 看不懂来打我,vue3的.vue文件(SFC)编译过程 文章中我们讲了调用createDescriptor函数会将vue代码字符串转换为descriptor对象,descriptor对象中拥有template属性、scriptSetup属性、styles属性,分别对应vue文件中的template模块、<script setup>模块、<style>模块。如下图:
progress-createDescriptor
createDescriptor函数在生成template属性的时候底层同样也会调用@vue/compiler-core包的baseParse函数,将template模块中的html字符串编译为AST抽象语法树。

所以在我们这个场景中走到baseCompile函数时就已经有了AST抽象语法树了,其实底层都调用的是@vue/compiler-core包的baseParse函数。

获取转换函数

接着将断点走到第二块内容处,代码如下:

1
scss复制代码const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()

从上面的代码可以看到getBaseTransformPreset函数的返回值是一个数组,对返回的数组进行解构,数组的第一项赋值给nodeTransforms变量,数组的第二项赋值给directiveTransforms变量。

将断点走进getBaseTransformPreset函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
csharp复制代码function getBaseTransformPreset() {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
transformFilter,
trackVForSlotScopes,
transformExpression
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
];
}

从上面的代码中不难看出由getBaseTransformPreset函数的返回值解构出来的nodeTransforms变量是一个数组,数组中包含一堆transform转换函数,比如处理v-once、v-if、v-memo、v-for等指令的转换函数。很明显我们这个demo中input标签上面的v-for指令就是由这里的transformFor转换函数处理。

同理由getBaseTransformPreset函数的返回值解构出来的directiveTransforms变量是一个对象,对象中包含处理v-on、v-bind、v-model指令的转换函数。

经过这一步的处理我们就拿到了由一系列转换函数组成的nodeTransforms数组,和由一系列转换函数组成的directiveTransforms对象。看到这里我想你可能有一些疑问,为什么nodeTransforms是数组,directiveTransforms却是对象呢?为什么有的指令转换转换函数是在nodeTransforms数组中,有的却是在directiveTransforms对象中呢?别着急,我们下面会讲。

transform函数

接着将断点走到第三块内容,transform函数处,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码transform(
ast,
Object.assign({}, options, {
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: Object.assign(
{},
directiveTransforms,
options.directiveTransforms || {}, // user transforms
),
}),
)

调用transform函数时传入了两个参数,第一个参数为当前的AST抽象语法树,第二个参数为传入的options,在options中我们主要看两个属性:nodeTransforms数组和directiveTransforms对象。

nodeTransforms数组由两部分组成,分别是上一步拿到的nodeTransforms数组,和之前在options.nodeTransforms数组中塞进去的转换函数。

directiveTransforms对象就不一样了,如果上一步拿到的directiveTransforms对象和options.directiveTransforms对象拥有相同的key,那么后者就会覆盖前者。以我们这个例子举例:在上一步中拿到的directiveTransforms对象中有key为model的处理v-model指令的转换函数,但是我们在@vue/compiler-dom包中的compile函数同样也给options.directiveTransforms对象中塞了一个key为model的处理v-model指令的转换函数。那么@vue/compiler-dom包中的v-model转换函数就会覆盖上一步中定义的v-model转换函数,那么@vue/compiler-core包中v-model转换函数是不是就没用了呢?答案是当然有用,在@vue/compiler-dom包中的v-model转换函数会手动调用@vue/compiler-core包中v-model转换函数。这样设计的目的是对于一些指令的处理支持不同的平台传入不同的转换函数,并且在这些平台中也可以手动调用@vue/compiler-core包中提供的指令转换函数,根据手动调用的结果再针对各自平台进行一些特别的处理。

我们先来回忆一下前面demo中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<template>
<input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const msgList = ref([
{
id: 1,
value: "",
},
{
id: 2,
value: "",
},
{
id: 3,
value: "",
},
]);
</script>

接着在debug终端中看看执行transform函数前的AST抽象语法树是什么样的,如下图:
AST

从上图中可以看到AST抽象语法树根节点下面只有一个children节点,这个children节点对应的就是input标签。在input标签上面有三个props,分别对应的是input标签上面的v-for指令、:key属性、v-model指令。说明在生成AST抽象语法树的阶段不会对指令进行处理,而是当做普通的属性一样使用正则匹配出来,然后塞到props数组中。

既然在生成AST抽象语法树的过程中没有对v-model、v-for等指令进行处理,那么又是在什么时候处理的呢?答案是在执行transform函数的时候处理的,在transform函数中会递归遍历整个AST抽象语法树,在遍历每个node节点时都会将nodeTransforms数组中的所有转换函数按照顺序取出来执行一遍,在执行时将当前的node节点和上下文作为参数传入。经过nodeTransforms数组中全部的转换函数处理后,vue提供的许多内置指令、语法糖、内置组件等也就被处理了,接下来只需要执行generate函数生成render函数就行了。

nodeTransforms数组

nodeTransforms 主要是对 node节点 进行操作,可能会替换或者移动节点。每个node节点都会将nodeTransforms数组中的转换函数按照顺序全部执行一遍,比如处理v-if指令的transformIf转换函数就要比处理v-for指令的transformFor函数先执行。所以nodeTransforms是一个数组,而且数组中的转换函数的顺序还是有讲究的。

在我们这个demo中input标签上面的v-for指令是由nodeTransforms数组中的transformFor转换函数处理的,很简单就可以找到transformFor转换函数。在函数开始的地方打一个断点,代码就会走到这个断点中,在debug终端上面看看此时的node节点是什么样的,如下图:
before-transformFor

从上图中可以看到在执行transformFor转换函数之前的node节点和上一张图打印的node节点是一样的。

我们在执行完transformFor转换函数的地方打一个断点,看看执行完transformFor转换函数后node节点变成什么样了,如下图:
after-transformFor

从上图我们可以看到经过transformFor转换函数处理后当前的node节点已经变成了一个新的node节点,而原来的input的node节点变成了这个节点的children子节点。新节点的source.content里存的是v-for="item in msgList"中的msgList变量。新节点的valueAlias.content里存的是v-for="item in msgList"中的item。我们发现input子节点的props数组现在只有两项了,原本的v-for指令的props经过transformFor转换函数的处理后已经被消费掉了,所以就只有两项了。

看到这里你可能会有疑问,为什么执行transform函数后会将AST抽象语法树的结构都改变了呢?

这样做的目的是在后续的generate函数中递归遍历AST抽象语法树时,只想进行字符串拼接就可以拼成render函数。这里涉及到模版AST抽象语法树和Javascript AST抽象语法树的概念。

我们来回忆一下template模块中的代码:

1
2
3
xml复制代码<template>
<input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>

template模版经过parse函数拿到AST抽象语法树,此时的AST抽象语法树的结构和template模版的结构是一模一样的,所以我们称之为模版AST抽象语法树。模版AST抽象语法树其实就是描述template模版的结构。如下图:
template-AST

我们再来看看生成的render函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
php复制代码function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(true), _createElementBlock(
_Fragment,
null,
_renderList($setup.msgList, (item) => {
return _withDirectives((_openBlock(), _createElementBlock("input", {
key: item.id,
"onUpdate:modelValue": ($event) => item.value = $event
}, null, 8, _hoisted_1)), [
[_vModelText, item.value]
]);
}),
128
/* KEYED_FRAGMENT */
);
}

很明显模版AST抽象语法树无法通过简单的字符串拼接就可以拼成上面的render函数,所以我们需要一个结构和上面的render函数一模一样的Javascript AST抽象语法树,Javascript AST抽象语法树的作用就是描述render函数的结构。如下图:
javascript-AST

上面这个Javascript AST抽象语法树就是执行transform函数时根据模版AST抽象语法树生成的。有了Javascript AST抽象语法树后再来执行generate函数时就可以只进行简单的字符串拼接,就能得到render函数了。

directiveTransforms对象

directiveTransforms对象的作用是对指令进行转换,给node节点生成对应的props。比如给子组件上面使用了v-model指令,经过directiveTransforms对象中的transformModel转换函数处理后,v-mode节点上面就会多两个props属性:modelValue和onUpdate:modelValue属性。directiveTransforms对象中的转换函数不会每次都全部执行,而是要node节点中有对应的指令,才会执行指令的转换函数。所以directiveTransforms是对象,而不是数组。

那为什么有的指令转换函数在directiveTransforms对象中,有的又在nodeTransforms数组中呢?

答案是在directiveTransforms对象中的指令全部都是会给node节点生成props属性的,那些不生成props属性的就在nodeTransforms数组中。

很容易就可以找到@vue/compiler-dom包的transformModel函数,然后打一个断点,让断点走进transformModel函数中,如下图:
transformModel

从上面的图中我们可以看到在@vue/compiler-dom包的transformModel函数中会调用@vue/compiler-core包的transformModel函数,拿到返回的baseResult对象后再一些其他操作后直接return baseResult。从左边的call stack调用栈中我们可以看到transformModel函数是由一个buildProps函数调用的,看名字你应该猜到了buildProps函数的作用是生成props属性的。点击Step Out将断点跳出transformModel函数,走进buildProps函数中,可以看到buildProps函数中调用transformModel函数的代码如下图:
buildProps

从上图中可以看到,name变量的值为model。context.directiveTransforms[name]的返回值就是transformModel函数,所以执行directiveTransform(prop, node, context)其实就是在执行transformModel函数。在debug终端中可以看到返回的props2是一个数组,里面存的是v-model指令被处理后生成的props属性。props属性数组中只有一项是onUpdate:modelValue属性,看到这里有的小伙伴会疑惑了v-model指令不是会生成modelValue和onUpdate:modelValue两个属性,为什么这里只有一个呢?答案是只有给自定义组件上面使用v-model指令才会生成modelValue和onUpdate:modelValue两个属性,对于这种原生input标签是不需要生成modelValue属性的,因为input标签本身是不接收名为modelValue属性,接收的是value属性。

其实transform函数中的内容是非常复杂的,里面包含了vue提供的指令、filter、slot等功能的处理逻辑。transform函数的设计高明之处就在于插件化,将处理这些功能的transform转换函数以插件的形式插入的,这样逻辑就会非常清晰了。比如我想看v-model指令是如何实现的,我只需要去看对应的transformModel转换函数就行了。又比如哪天vue需要实现一个v-xxx指令,要实现这个指令只需要增加一个transformXxx的转换函数就行了。

generate函数

经过上一步transform函数的处理后,已经将描述模版结构的模版AST抽象语法树转换为了描述render函数结构的Javascript AST抽象语法树。在前面我们已经讲过了Javascript AST抽象语法树就是描述了最终生成render函数的样子。所以在generate函数中只需要递归遍历Javascript AST抽象语法树,通过字符串拼接的方式就可以生成render函数了。

将断点走到执行generate函数前,看看这会儿的Javascript AST抽象语法树是什么样的,如下图:
before-generate

从上面的图中可以看到Javascript AST和模版AST的区别主要有两个:

  • node节点中多了一个codegenNode属性,这个属性中存了许多node节点信息,比如codegenNode.props中就存了key和onUpdate:modelValue属性的信息。在generate函数中遍历每个node节点时就会读取这个codegenNode属性生成render函数
  • 模版AST中根节点下面的children节点就是input标签,但是在这里Javascript AST中却是根节点下面的children节点,再下面的children节点才是input标签。多了一层节点,在前面的transform函数中我们已经讲了多的这层节点是由v-for指令生成的,用于给v-for循环出来的多个节点当父节点。

将断点走到generate函数执行之后,可以看到已经生成render函数啦,如下图:
after-generate

总结

现在我们再来看看最开始讲的流程图,我想你应该已经能将整个流程串起来了。如下图:
full-progress

将template编译为render函数可以分为7步:

  • 执行@vue/compiler-sfc包的compileTemplate函数,里面会调用同一个包的doCompileTemplate函数。这一步存在的目的是作为一个入口函数给外部调用。
  • 执行@vue/compiler-sfc包的doCompileTemplate函数,里面会调用@vue/compiler-dom包中的compile函数。这一步存在的目的是入口函数的具体实现。
  • 执行@vue/compiler-dom包中的compile函数,里面会对options进行了扩展,塞了一些处理dom的转换函数进去。给options.nodeTransforms数组中塞了处理style的转换函数,和给options.directiveTransforms对象中塞了处理v-cloak、v-html、v-text、v-model、v-on、v-show等指令的转换函数。然后以扩展后的options去调用@vue/compiler-core包的baseCompile函数。
  • 执行@vue/compiler-core包的baseCompile函数,在这个函数中主要分为4部分。第一部分为检查传入的source是不是html字符串,如果是就调用同一个包下的baseParse函数生成模版AST抽象语法树。否则就直接使用传入的模版AST抽象语法树。此时node节点中还有v-for、v-model等指令,并没有被处理掉。这里的模版AST抽象语法树的结构和template中的结构一模一样,模版AST抽象语法树是对template中的结构进行描述。
  • 第二部分为执行getBaseTransformPreset函数拿到@vue/compiler-core包中内置的nodeTransforms和directiveTransforms转换函数。nodeTransforms数组中的为一堆处理node节点的转换函数,比如处理v-on指令的transformOnce转换函数、处理v-if指令的transformIf转换函数。directiveTransforms对象中存的是对一些“会生成props的指令”进行转换的函数,用于给node节点生成对应的props。比如处理v-model指令的transformModel转换函数。
  • 第三部分为将传入的options.nodeTransforms、options.directiveTransforms分别和本地的nodeTransforms、directiveTransforms进行合并得到一堆新的转换函数。其中由于nodeTransforms是数组,所以在合并的过程中会将options.nodeTransforms和nodeTransforms中的转换函数全部合并进去。由于directiveTransforms是对象,如果directiveTransforms对象和options.directiveTransforms对象拥有相同的key,那么后者就会覆盖前者。然后将合并的结果和模版AST抽象语法树一起传入到transform函数中执行,就可以得到转换后的javascript AST抽象语法树。在这一过程中v-for、v-model等指令已经被转换函数给处理了。得到的javascript AST抽象语法树的结构和render函数的结构一模一样,javascript AST抽象语法树就是对render函数的结构进行描述。
  • 第四部分为由于已经拿到了和render函数的结构一模一样的javascript AST抽象语法树,只需要在generate函数中遍历javascript AST抽象语法树进行字符串拼接就可以得到render函数了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

美团一面,面试官让介绍AQS原理并手写一个同步器,直接凉了

发表于 2024-04-10

写在开头

今天在牛客上看到了一个帖子,一个网友吐槽美团一面上来就让手撕同步器,没整出来,结果面试直接凉凉。
image.png
就此联想到一周前写的一篇关于AQS知识点解析的博文,当时也曾埋下伏笔说后面会根据AQS的原理实现一个自定义的同步器,那今天就来把这个坑给填上哈。

常用的AQS架构同步器类

自定义同步器实现步骤

在上一篇文章中我们就已经提过了AQS是基于 模版方法模式 的,我们基于此的自定义同步器设计一般需要如下两步:

1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法;
2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

在模版方法模式下,有个很重要的东西,那就是“钩子方法”,这是一种抽象类中的方法,一般使用 protected 关键字修饰,可以给与默认实现,空方法居多,其内容逻辑由子类实现,为什么不适用抽象方法呢?因为,抽象方法需要子类全部实现,增加大量代码冗余!

Ok,有了这层理论知识,我们去看看Java中根据AQS实现的同步工具类有哪些吧

Semaphore(信号量)

在前面我们讲过的synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量,它并不能保证线程安全。

我们下面来看一个关于Semaphore的使用示例:

【代码示例1】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码public class Test {
private final Semaphore semaphore;

/**
* 构造方法初始化信号量
* @param limit
*/
public Test(int limit) {
this.semaphore = new Semaphore(limit);
}

public void useResource() {
try {
semaphore.acquire();
// 使用资源
System.out.println("资源use:" + Thread.currentThread().getName());
Thread.sleep(1000); // 模拟资源使用时间
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println("资源release:" + Thread.currentThread().getName());
}
}

public static void main(String[] args) {
// 限制3个线程同时访问资源
Test pool = new Test(3);

for (int i = 0; i < 4; i++) {
new Thread(pool::useResource).start();
}
}
}

输出:

1
2
3
4
5
6
7
8
java复制代码资源use:Thread-1
资源use:Thread-0
资源use:Thread-2
资源release:Thread-0
资源release:Thread-1
资源release:Thread-2
资源use:Thread-3
资源release:Thread-3

由此结果可看出,我们成功的将同时访问共享资源的线程数限制在了不超过3个的级别,这里面涉及到了Semaphore的两个主要方法:acquire()和release()

① acquire():获取许可
跟进这个方法后,我们会发现其内部调用了AQS的一个final 方法acquireSharedInterruptibly(),这个方法中又调用了tryAcquireShared(arg)放,作为AQS中的钩子方法,这个方法的实现在Semaphore的两个静态内部类 FairSync(公平模式) 和 NonfairSync(非公平模式) 中。
在这里插入图片描述

② release():释放许可
同样跟入这个方法,里面用了AQS的releaseShared(),而在这个方法内也毫无疑问的用了tryReleaseShared(int arg)这个钩子方法,原理同上,不再冗释。

【补充】
此外,在Semaphore中还有一个Sync的内部类,提供nonfairTryAcquireShared()自旋获取资源,以及tryReleaseShared(int releases),共享方式尝试释放资源。

除了Semaphore(信号量)外,基于AQS实现的还有CountDownLatch (倒计时器)、CyclicBarrier(循环栅栏),本来想在一篇文章中讲完的,但感觉篇幅上会非常长,遂放弃,后面分篇学习吧。

手写一个同步器!

好了,有了上面的一系列学习,我们现在来手撕一个自定义的同步器吧,原理都一样滴,开始前,先贴上AQS中的几个钩子方法,防止待会忘记,哈哈!

【钩子方法】

1
2
3
4
5
6
7
8
9
10
java复制代码//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)
//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()

写一个基于AQS的互斥锁,同一时刻只允许一个线程获取资源。

步骤一:

首先,我们在第一步,我们定义一个互斥锁类OnlySyncByAQS,在类中我们同样写一个静态内部类去继承AbstractQueuedSynchronizer,在内部类中,我们重写AQS的tryAcquire方法,独占方式,尝试获取资源;重写tryRelease()尝试释放资源,这俩为主要方法!

然后我们再进一步封装成lock()与unlock()的上锁与解锁方法,并在里面通过模版方法模式,去调用AQS中的acquire()和release(),从而去调到我们对模版方法的实现。

【代码示例2】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
java复制代码public class OnlySyncByAQS {

private final Sync sync = new Sync();

/**
* 获取许可,给资源上锁
*/
public void lock() {
sync.acquire(1);
}

/**
* 释放许可,解锁
*/
public void unlock() {
sync.release(1);
}

/**
* 判断是否独占
* @return
*/
public boolean isLocked() {
return sync.isHeldExclusively();
}

/**
* 静态内部类,继承AQS,重写钩子方法
*/
private static class Sync extends AbstractQueuedSynchronizer {

/**
* 重写AQS的tryAcquire方法,独占方式,尝试获取资源。
*/
@Override
protected boolean tryAcquire(int arg) {
//CAS 尝试更改状态
if (compareAndSetState(0, 1)) {
//独占模式下,设置锁的持有者为当前线程,来自于AOS
setExclusiveOwnerThread(Thread.currentThread());
System.out.println(Thread.currentThread().getName()+"获取锁成功");
return true;
}
System.out.println(Thread.currentThread().getName()+"获取锁失败");
return false;
}

/**
* 独占方式。尝试释放资源,成功则返回true,失败则返回false。
* @param arg
* @return
*/
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
//置空锁的持有者
setExclusiveOwnerThread(null);
//改状态为0,未锁定状态
setState(0);
System.out.println(Thread.currentThread().getName()+"释放锁成功!");
return true;
}

/**
* 判断该线程是否正在独占资源,返回state=1
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}

}

步骤二:

第二步,我们写一个测试类去调用这个自定义的互斥锁。

【代码示例2】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码public class Test {

private OnlySyncByAQS onlySyncByAQS = new OnlySyncByAQS();

public void use(){
onlySyncByAQS.lock();
try {
//休眠1秒获取使用共享资源
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
onlySyncByAQS.unlock();
}
}

public static void main(String[] args) throws InterruptedException {
Test test = new Test();
//多线程竞争资源,每次仅一个线程拿到锁
for (int i = 0; i < 3; i++) {
new Thread(()->{
test.use();
}).start();
}
}
}

输出:

1
2
3
4
5
6
7
8
9
10
java复制代码Thread-0获取锁成功
Thread-1获取锁失败
Thread-2获取锁失败
Thread-1获取锁失败
Thread-1获取锁失败
Thread-0释放锁成功!
Thread-1获取锁成功
Thread-1释放锁成功!
Thread-2获取锁成功
Thread-2释放锁成功!

由输出结果可以看出作为互斥锁,每次仅一个线程可以获取到锁资源,其他线程会不断尝试获取并失败,直至该线程释放锁资源!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

在这里插入图片描述
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nocode/Doc,可视化+ 零代码打造下一代文件编辑器

发表于 2024-04-10

hi,大家好,我是徐小夕,之前和大家分享了《用零代码的思维设计文档引擎》的内容,承诺大家在10号上线内测版可视化文档编辑器——Nocode/Doc。 今天如约而至,接下来就和大家分享一下可视化文档引擎的最新内容和技术理念。

  • 内测地址: http://wep.turntip.cn
  • 文档案例: http://wep.turntip.cn/design/doc?id=d1711853506463&uid=wep_251711700015023

建议大家在电脑端体验测试,可以更全面的感受可视化文档编辑器的魅力。

接下来我会从以下几个方面来介绍Nocode/Doc:

  • 核心功能
  • 技术实现
  • 性能体验
  • 后期规划

功能亮点介绍

上篇文章介绍了文档的很多实用的组件和功能, 今天我介绍一下最新迭代的能力。

1. 文档支持思维导图编辑

没错,用过飞书和钉钉的朋友也许很熟悉,我们可以在文档里内嵌思维导图,让自己的思路更清晰生动。目前我实现的Nocode/Doc文档引擎也支持了思维导图的编辑,大家可以直接在文档中编写思维导图,当然,有了这个能力,后期我会和更多系统场景打通, 实现业务级别的思维导图动态渲染。

2. 文档支持内嵌白板,展示更多原型,设计稿等效果

产品经理写的原型,如果用文档轻松展示出来,是不是很酷炫呢?

3. 内容分享权限

和市面上常用文档类似,Nocode/Doc 也支持访问权限设置,我们可以设置:

  • 仅自己可见
  • 公开
  • 密码访问

这样我们就能实现更多内容资产的配置模式, 最近很火的知识付费,是不是也可以用它来轻松实现呢?

4. 内嵌表单问卷玩法,轻松收集文档数据

我们可以和橙子试卷打通,做好表单支持直接内嵌到文档中,这样就可以实现在文档里做信息收集的能力了:

技术实现

目前整个系统我采用世界最流行的解决方案和框架Nextjs + React, UI库采用世界使用量最高的Ant Design, 目前是5.0版本,还有我本人很多原生js实现的可视化方案和组件, 后续产品稳定之后会出一系列技术分享,和大家分享底层实现的技术方案。

如果对可视化零代码感兴趣的朋友也可以在趣谈前端中看到我往期的技术分享,相信对你会非常有帮助。也欢迎随时和我交流。

性能体验

目前我用飞书文档,钉钉文档,以及我做的Nocode/Doc, 在相同文档内容下,1w字长文档,Nocode/Doc 渲染的速度还是非常快的, 基本1-3s内能打开,大家也可以体验一下。

后期规划

我写了个大纲, 感兴趣的朋友可以参考一下。

规划文档:http://wep.turntip.cn/design/doc?id=d1712737394357&uid=wep_251711700015023

大致如下:

反馈 & 内测

欢迎大家提出优化建议,如果遇到bug或者好的想法,欢迎在评论区留言反馈,会有小惊喜哦~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Garnet,缓存的新选择! 什么是Garnet? 特性和优

发表于 2024-04-10

号外号外!

  1. redis不再“开源”;
  2. 微软开源了Garnet

什么是Garnet?

Garnet是微软推出的一款远程缓存存储系统,旨在为开发者提供高性能、可靠性和可伸缩性的缓存解决方案。它采用了现代化的架构和技术,具有高度可定制性和灵活性,适用于各种规模和类型的应用场景。

特性和优势

  1. 高性能

Garnet采用了高效的缓存算法和数据结构,以实现快速的数据访问和响应。它支持并发访问和高吞吐量,能够处理大规模的请求流量。
Garnet通过智能缓存策略,将热点数据(经常访问的数据)存储在用户附近的节点上,从而减少了数据传输的时间和距离,实现了低延迟访问。
2. 可靠性

Garnet具有强大的数据保护和容错机制,能够确保数据的持久性和一致性。它支持数据备份、复制和故障转移,有效地降低了数据丢失和系统故障的风险。
3. 可扩展性

Garnet的架构设计具有良好的水平扩展性,可以轻松地扩展到数百甚至数千台服务器。它支持动态添加和移除节点,能够根据需求灵活调整集群规模。
4. 多种数据类型支持:

除了常规的键值对存储之外,Garnet还支持多种数据类型,包括列表、集合、哈希表等,满足了不同应用场景的需求。
5. 丰富的功能

Garnet提供了丰富的功能和工具,包括监控、调优、故障排除等,帮助开发者更好地管理和运维缓存系统。

高性能

对于一个中间件来说,大家最关心的应该就是性能到底怎么样?基准测试的结果,总体还是表现不错的。

具体性能测试详情可以查看链接: Evaluating Garnet’s Performance Benefits

实际应用的过程中效果到底咋样当然还需要我们在通过代码来验证一下。

下面我写了一个简单的例子,直接循环往garnet和redis里面写入和读取数据。通过计算时间来对比一下两者的差距到底有多大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
C#复制代码[HttpGet]
[ActionTitle(Name = "测试Garnet")]
[Route("test.svc")]
public void Start()
{
// Redis connection
var redis = ConnectionMultiplexer.Connect("localhost");
var redisDb = redis.GetDatabase();

// Garnet connection
var garnet = new GarnetClient("localhost", 3278);

// Test data
string key = "test_key";
string value = "test_value";

// Test with Redis
var redisStartTime = DateTime.Now;
for( int i = 0; i < 1000; i++ ) {
redisDb.StringSet(key + i, value + i);
}
var redisEndTime = DateTime.Now;
Console.WriteLine($"StringSet Time taken by Redis: {(redisEndTime - redisStartTime).TotalMilliseconds} ms");

// Test with Garnet
var garnetStartTime = DateTime.Now;
for( int i = 0; i < 1000; i++ ) {
garnet.StringSet(key + i, value + i, null);
}
var garnetEndTime = DateTime.Now;
Console.WriteLine($"StringSet Time taken by Garnet: {(garnetEndTime - garnetStartTime).TotalMilliseconds} ms");

// Test with Redis
redisStartTime = DateTime.Now;
for( int i = 0; i < 1000; i++ ) {
string a = redisDb.StringGet(key + i).ToString();
}
redisEndTime = DateTime.Now;
Console.WriteLine($"StringGet Time taken by Redis: {(redisEndTime - redisStartTime).TotalMilliseconds} ms");

// Test with Garnet
garnetStartTime = DateTime.Now;
for( int i = 0; i < 1000; i++ ) {
garnet.StringGet(key + i, null);
}
garnetEndTime = DateTime.Now;
Console.WriteLine($"StringGet Time taken by Garnet: {(garnetEndTime - garnetStartTime).TotalMilliseconds} ms");
}

最后运行的结果还是挺振奋人心的!

image.png

兼容性

当然在进行中间件选择的时候,切换的成本也是重点要纳入考虑的。“白嫖一时爽,重构火葬场!”这种事情显然是我们不愿意看到的。

这个时候我们在官网看到这样一句话

Garnet 并不是要成为 Redis 100% 完美的替代品,而是应该将其视为一个足够接近的起点,以确保对您重要的功能的兼容性。 Garnet 确实可以在未经修改的情况下与许多 Redis 客户端一起使用(我们特别对 Garnet 进行了StackExchange.Redis很好的测试),因此入门非常容易。

于是,我们再整个demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
C#复制代码[HttpGet]
[ActionTitle(Name = "测试Garnet连接")]
[Route("connect.svc")]
public void Connect()
{
// Redis connection
var redis = ConnectionMultiplexer.Connect("localhost");
var redisDb = redis.GetDatabase();

// Garnet connection
var garnet = ConnectionMultiplexer.Connect("localhost:3278");
var garnetDb = garnet.GetDatabase();

// Test data
string key = "test_key";

// Test with Redis
string a = redisDb.StringGet(key + 1).ToString();
Console.WriteLine($"StringGet by Redis : {a}");

// Test with Garnet
string b = garnetDb.StringGet(key + 1).ToString();
Console.WriteLine($"StringGet by Garnet : {b}");
}

微软 诚不我欺!

在不动客户端代码的情况下,我们能够非常平滑的切换到Garnet

image.png

redis桌面客户端也能直接连接Garnet

image.png

日志和诊断

Garnet 提供了丰富的日志和诊断特性,以帮助开发人员监视和调试其应用程序的性能和行为。以下是 Garnet 日志和诊断特性的主要内容:

  1. 详细日志记录:Garnet 具有灵活的日志记录功能,可以记录各种级别的日志消息,包括信息、警告和错误。这些日志消息可以帮助开发人员了解系统的运行情况,识别潜在的问题并进行故障排除。
  2. 性能指标:Garnet 还提供了丰富的性能指标,可以帮助开发人员监视系统的性能状况。这些指标可以包括各种关键性能指标,如请求响应时间、吞吐量、延迟等。
  3. 异常跟踪:Garnet 具有异常跟踪功能,可以捕获和记录应用程序中的异常情况。这些异常跟踪信息可以帮助开发人员快速定位和修复问题。
  4. 诊断工具:Garnet 还提供了一系列诊断工具,用于分析和调试系统的行为。这些工具可以帮助开发人员深入了解系统的内部工作原理,并识别潜在的性能瓶颈和问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
C#复制代码static void Main(string[] args)
{
try
{

var loggerFactory = LoggerFactory.Create(x =>
{
x.ClearProviders();
x.SetMinimumLevel(LogLevel.Trace);
x.AddZLoggerConsole(options =>
{
options.UsePlainTextFormatter(formatter =>
{
formatter.SetPrefixFormatter($"[{0}]", (in MessageTemplate template, in LogInfo info) => template.Format(info.Category));
});
});
});


using var server = new GarnetServer(args, loggerFactory);

// Optional: register custom extensions
RegisterExtensions(server);

// Start the server
server.Start();

Thread.Sleep(Timeout.Infinite);
}
catch (Exception ex)
{
Console.WriteLine($"Unable to initialize server due to exception: {ex.Message}");
}
}

image.png

自定义命令

想要在 Redis 上执行一些复杂的执行是很常见的,在 Redis 的情况下,我们过去常常使用 Lua 脚本来处理它,但使用 Garnet,我们可以在 C# 中实现和合并自定义命令。 如果你不知道 LUA 是否在性能方面,或者如果你想做一些 LUA 做不到的相当复杂的事情,你可以使用它而不会有任何性能劣势。 更好的是,服务器端提供的扩展命令遵循 RESP,因此客户端可以从 PHP 或 Go 调用它们,而不仅仅是针对 C#。

因此,让我们立即创建一个名为“TEST”的自定义命令。命令很简单,直接返回一个“Hello Garnet!”

注册自定义命令本身非常简单,只需添加一个实现的类,或者与命令名称一起添加即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Garnet复制代码// 测试自定义命令
server.Register.NewTransactionProc("TEST", 0, () => new TestCustomCommand());


sealed class TestCustomCommand : CustomTransactionProcedure
{
/// <summary>
/// No transactional phase, skip Prepare
/// </summary>
public override bool Prepare<TGarnetReadApi>(TGarnetReadApi api, ArgSlice input)
=> false;

/// <summary>
/// Main will not be called because Prepare returns false
/// </summary>
public override void Main<TGarnetApi>(TGarnetApi api, ArgSlice input, ref MemoryResult<byte> output)
=> throw new InvalidOperationException();

/// <summary>
/// Finalize reads two keys (non-transactionally) and return their values as an array of bulk strings
/// </summary>
public override void Finalize<TGarnetApi>(TGarnetApi api, ArgSlice input, ref MemoryResult<byte> output)
{
WriteSimpleString(ref output, "Hello Garnet!");
}
}

同时还是因为Garnet遵循 RESP,因此客户端不是专用于 C#,也不是 Garnet 客户端独有的。 所以我们可以直接在redis客户端调用Garnet的自定义命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Garnet复制代码 [HttpGet]
[ActionTitle(Name = "测试Garnet 自定义命令")]
[Route("custom-command.svc")]
public async void Test()
{
// Garnet connection
var garnet = new GarnetClient("localhost", 3278);
garnet.Connect();
// Test data
string result = await garnet.ExecuteForStringResultAsync("TEST");

Console.WriteLine($"Garnet Custom Command Result: {result}");

// 采用redis客户端连接
var client = ConnectionMultiplexer.Connect("localhost:3278");
var db = client.GetDatabase();

RedisResult redisResult = db.Execute("TEST");

Console.WriteLine($"Redis Client Custom Command Result: {((string?)redisResult)}");
}

最后我们可以看到用两种客户端执行自定义命令都输出了同样的结果

image.png

结语

Garnet作为微软最新推出的远程缓存存储系统,为开发者提供了一种全新的选择。它具有高性能、可靠性和可伸缩性的特性,适用于各种规模和类型的应用场景。通过使用Garnet,开发者可以更好地提升应用的性能和用户体验,实现业务的快速发展和持续创新。

参考文档

  • Garnet 开发入门

更多一手讯息,可关注公众号:ITProHub

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…404142…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%