2024年7月

Nginx是一个高性能的HTTP和反向代理服务器,它在全球范围内被广泛使用,因其高性能、稳定性、丰富的功能以及低资源消耗而受到青睐。今天V哥从5个方面来介绍 Nginx 性能调优的具体策略,希望对兄弟们有帮助,废话不多说,马上开整。

1. 系统层面:

  • 调整内核参数:例如,增加系统文件描述符的限制、TCP连接队列的大小等。
  • 网络优化:可以使用TCP Fast Open、选择更高效的网络协议等。

2. Nginx配置:

  • Worker进程数:通常设置为等于服务器的CPU核心数。
  • 连接数:通过调整worker_connections参数,可以增加每个Worker进程可以打开的连接数。
  • 使用HTTP/2:HTTP/2提供了更好的性能,包括头部压缩和服务器推送等功能。

3. 缓存利用:

  • 启用文件缓存:Nginx可以将静态文件缓存在服务器本地,减少磁盘I/O操作。
  • 使用代理缓存:例如,使用Nginx作为反向代理服务器时,可以缓存后端服务器的响应内容。

4. 压缩:

  • 启用Gzip压缩可以减少数据传输量,提高响应速度。

5. 负载均衡策略:

  • 根据服务器的性能和负载情况,选择合适的负载均衡策略,如轮询、最少连接数、IP哈希等。

下面 V 哥针对每个点结合业务场景来详细介绍,老铁们坐稳了,要发车了。

1. 系统层面

Nginx性能调优可以从多个层面进行,这里我们重点讨论系统层面的调优步骤。系统层面的调优主要是对操作系统进行优化,以更好地支持Nginx的高并发处理能力。

1. 内核参数调整

内核参数的调整可以通过修改/etc/sysctl.conf文件来实现,这些参数会影响网络栈的行为。

  • net.core.somaxconn:这个参数设置了一个系统范围内所有监听Socket的未完成连接队列的最大长度。调高这个值可以避免在高峰期出现"connection refused"的错误。
sysctl -w net.core.somaxconn=65535
  • net.ipv4.tcp_max_syn_backlog:这个参数设置了TCP连接建立时,SYN队列的最大长度。在高负载情况下,增加这个值可以防止SYN洪水攻击。
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
  • net.ipv4.ip_local_port_range:这个参数定义了本地端口范围,增加这个范围可以允许更多的并发连接。
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
  • net.ipv4.tcp_fin_timeout:这个参数决定了系统回收TCP连接的时间。减小这个值可以加快回收速度,释放资源。
sysctl -w net.ipv4.tcp_fin_timeout=30
  • net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_tw_recycle:这两个参数控制了TCP连接的TIME_WAIT状态。开启它们可以更快地重用和回收处于TIME_WAIT状态的连接。
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_tw_recycle=1

2. 文件描述符限制

增加系统允许的文件描述符数量,以便Nginx可以打开更多的连接。

  • 用户级限制:修改/etc/security/limits.conf文件,增加Nginx用户(通常是www-data或nginx)的nofile限制。
nginx soft nofile 65535
nginx hard nofile 65535
  • 系统级限制:修改/proc/sys/fs/file-max文件,增加整个系统的最大文件描述符数。
echo 65535 > /proc/sys/fs/file-max

3. 网络栈优化

  • 禁用Syn Cookies:在高峰期,可以临时禁用Syn Cookies,以提高TCP连接的建立速度。
sysctl -w net.ipv4.tcp_syncookies=0
  • 调整TCP Keepalive:通过调整net.ipv4.tcp_keepalive_time等参数,可以优化长连接的管理。
sysctl -w net.ipv4.tcp_keepalive_time=600

4. 硬件和架构优化

  • 使用SSD存储:使用固态硬盘可以显著提高I/O性能,尤其是对于需要频繁读写文件的Nginx服务器。
  • 网络硬件:使用高带宽和低延迟的网络硬件,如万兆以太网,可以提高数据传输速度。
  • 负载均衡:如果服务器面临极高的负载,可以考虑使用硬件负载均衡器来分担Nginx的压力。

5. 操作系统选择和优化

  • 选择合适的操作系统:不同的操作系统对网络性能的支持不同,选择一个适合高并发网络服务的操作系统是很有帮助的。
  • 关闭不必要的系统服务:减少系统运行的进程和服务,可以释放更多的系统资源给Nginx使用。

在进行系统层面的调优时,需要注意平衡性能提升和系统稳定性之间的关系。过度的优化可能会导致系统不稳定,甚至出现资源耗尽的情况。因此,建议在调优过程中进行充分的测试,并根据服务器的具体情况进行调整。

2. Nginx配置

Nginx的性能调优可以通过修改其配置文件nginx.conf来实现。以下是一些具体的调优步骤和建议:

1. Worker进程数

Nginx使用多进程模型,通常情况下,Worker进程的数量应该设置为等于服务器的CPU核心数。这样可以最大化利用多核CPU的性能。

worker_processes auto;  # 或者指定具体的进程数,如 worker_processes 4;

2. Worker连接数

每个Worker进程可以同时处理的最大连接数由worker_connections指令设置。这个值应该根据服务器的内存大小和系统负载来合理设置。

events {
    worker_connections  4096;  # 根据服务器能力适当调整
}

3. 使用HTTP/2

如果客户端支持,可以启用HTTP/2,它提供了更好的性能,包括头部压缩和服务器推送等功能。

listen 443 ssl http2;

4. 文件描述符限制

增加Nginx可以打开的文件描述符数量,以处理更多的并发连接。

worker_rlimit_nofile 65535;

5. 启用Gzip压缩

压缩可以减少数据传输量,提高响应速度。

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

6. 缓存配置

配置浏览器缓存,减少重复请求。

location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 30d;
    add_header Cache-Control "public";
}

7. 静态资源处理

对于静态资源,可以使用sendfile快速传输文件。

location /static/ {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay off;
}

8. 负载均衡

如果有多台后端服务器,可以使用Nginx的负载均衡功能。

upstream backend {
    server backend1.example.com;
    server backend2.example.com;
    server backend3.example.com;
    keepalive 64;  # 保持活动的连接数
}

server {
    location / {
        proxy_pass http://backend;
    }
}

9. 优化SSL/TLS

如果使用SSL/TLS,可以优化相关配置以提高性能。

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

10. 日志配置

减少不必要的日志记录,可以减少磁盘I/O和CPU使用。

access_log off;  # 或者将日志写入内存文件系统

在进行Nginx配置调优时,应该逐步进行,每次更改后都要进行测试,以确保更改确实提高了性能,并且没有引入新的问题。此外,不同的应用场景可能需要不同的优化策略,因此最好根据实际情况进行调整。

3. 缓存利用

Nginx缓存利用是提高性能的重要手段,它可以通过减少服务器负载、降低响应时间和减少网络带宽使用来显著提升用户体验。以下是缓存利用的具体调优步骤:

1. 启用Nginx内置缓存

Nginx内置了多种缓存机制,如文件系统缓存、内存缓存等。可以通过配置nginx.conf来启用这些缓存。

  • 文件系统缓存(HTTP缓存)

在HTTP模块中,可以使用proxy_cache指令启用代理缓存。

http {
    # 定义缓存区域
    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        location / {
            proxy_cache my_cache;  # 使用定义的缓存区域
            proxy_cache_valid 200 302 10m;  # 对200和302响应缓存10分钟
            proxy_cache_valid 404 1m;  # 对404响应缓存1分钟
            proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;  # 在指定情况下使用过期缓存
        }
    }
}
  • 内存缓存(FastCGI缓存)

如果使用FastCGI(如与PHP-FPM配合),可以使用fastcgi_cache指令启用缓存。

http {
    # 定义FastCGI缓存区域
    fastcgi_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m;

    server {
        location ~ \.php$ {
            fastcgi_cache my_cache;  # 使用定义的缓存区域
            fastcgi_cache_key $request_uri;  # 定义缓存键
            fastcgi_cache_valid 200 30m;  # 对200响应缓存30分钟
        }
    }
}

2. 配置浏览器缓存

通过设置HTTP头,可以指示浏览器缓存静态资源。

location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 30d;  # 设置资源的过期时间
    add_header Cache-Control "public";  # 指示资源可以被浏览器和代理缓存
}

3. 使用CDN

内容分发网络(CDN)可以缓存静态资源,并将其分发到全球各地的节点,用户可以从最近的节点获取内容,从而减少延迟。

4. 配置反向代理缓存

如果Nginx作为反向代理服务器,可以配置它来缓存后端服务器的响应。

location / {
    proxy_pass http://backend;
    proxy_cache my_cache;  # 使用定义的缓存区域
    proxy_cache_valid 200 30m;  # 对200响应缓存30分钟
}

5. 监控和调整缓存性能

  • 使用Nginx的统计模块(如ngx_http_stub_status_module)来监控缓存命中率和缓存大小。
  • 根据监控数据调整缓存大小和过期时间,以优化性能。

6. 清理缓存

定期清理缓存,以释放空间和提高缓存效率。可以使用Nginx的缓存清理功能或编写脚本来实现。

在进行缓存调优时,需要注意以下几点:

  • 缓存大小和过期时间需要根据实际情况进行调整,以找到最佳平衡点。
  • 监控缓存命中率,以确保缓存配置有效地提高了性能。
  • 确保缓存内容的一致性,特别是在动态内容缓存时。

通过合理利用缓存,可以显著提高Nginx的性能,减少服务器负载,并提高用户体验。

4. 压缩

Nginx中的压缩功能可以通过Gzip模块来实现,它可以在服务器端对响应数据进行压缩,以减少传输数据的大小,从而提高加载速度,尤其是在带宽有限的情况下。以下是压缩的具体调优步骤:

1. 启用Gzip压缩

首先,确保在Nginx中启用了Gzip模块。这通常是通过在nginx.conf文件的http块中添加以下指令来完成的:

http {
    gzip on;  # 启用Gzip压缩
}

2. 设置压缩级别

gzip_comp_level指令用于设置压缩级别,级别越高,压缩率越大,但CPU消耗也越高。通常设置为1到9之间的值,其中1是最快的压缩,9是最高压缩率。推荐设置为6,这是一个性能和压缩率的平衡点。

http {
    gzip_comp_level 6;  # 设置压缩级别
}

3. 选择要压缩的内容类型

使用gzip_types指令指定要压缩的MIME类型。通常,文本文件(如HTML、CSS、JavaScript)和XML文件可以从压缩中受益。

http {
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}

4. 设置最小压缩大小

使用gzip_min_length指令设置触发压缩的最小文件大小。对于小文件,压缩可能不会带来太多好处,而且会增加CPU负担。

http {
    gzip_min_length 256;  # 只有大于256字节的响应才会被压缩
}

5. 禁用IE6的压缩

老版本的IE浏览器(IE6及以下)可能不支持Gzip压缩,可以通过gzip_disable指令禁用对这些浏览器的压缩。

http {
    gzip_disable "MSIE [1-6]\.";  # 禁用IE6及以下版本的Gzip压缩
}

6. 配置浏览器缓存

虽然压缩可以减少服务器带宽使用,但它会增加CPU负载。为了减轻服务器压力,可以配置浏览器缓存,让浏览器存储压缩后的内容,减少重复请求。

location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 30d;  # 设置资源的过期时间
    add_header Cache-Control "public";  # 指示资源可以被浏览器和代理缓存
}

7. 测试和监控

  • 在应用这些配置后,使用工具如curl或在线Gzip检测服务来测试压缩是否生效。
  • 监控服务器的CPU使用情况和响应时间,以确保压缩带来的性能提升大于CPU负载的增加。

通过合理配置Gzip压缩,可以在不显著增加服务器负载的情况下,显著减少网络传输数据的大小,提高用户体验。在进行压缩调优时,需要根据服务器的CPU能力和网络条件来平衡压缩级别和最小压缩大小,以达到最佳的性能。

5. 负载均衡策略

Nginx作为高性能的HTTP和反向代理服务器,提供了多种负载均衡策略,可以帮助分散流量,提高网站的可用性和性能。以下是负载均衡策略的具体调优步骤:

1. 选择合适的负载均衡策略

Nginx支持多种负载均衡策略,包括:

  • 轮询(Round Robin):这是默认的策略,请求按时间顺序逐一分配到不同的服务器,如果服务器宕机,自动剔除。
  • 最少连接(Least Connections):分配到当前连接数最少的服务器。
  • IP哈希(IP Hash):根据请求的来源IP地址,将请求分配到固定的服务器,可以在一定程度上保持用户的会话状态。
  • 权重(Weight):指定不同服务器的权重,权重越高,分配的请求越多。

在nginx.conf的upstream块中定义负载均衡策略:

upstream backend {
    server backend1.example.com weight=3;
    server backend2.example.com;
    server backend3.example.com backup;  # 备用服务器
    ip_hash;  # 使用IP哈希策略
}

2. 配置健康检查

使用health_check模块可以对后端服务器进行健康检查,以确保流量只被定向到健康的服务器。

server {
    location / {
        proxy_pass http://backend;
        health_check interval=10 fails=3 passes=2;
    }
}

3. 使用持久连接

对于HTTP/1.1客户端,可以使用持久连接(Keepalive)来减少建立和关闭连接的次数。

upstream backend {
    server backend1.example.com;
    keepalive 32;  # 保持活动的连接数
}

4. 调整超时时间

适当调整超时时间,以确保Nginx能够快速响应后端服务器的变化。

proxy_connect_timeout 5s;  # 设置与后端服务器建立连接的超时时间
proxy_read_timeout 60s;    # 设置从后端服务器读取响应的超时时间
proxy_send_timeout 5s;     # 设置向后端服务器发送请求的超时时间

5. 限制连接数

为了避免单个客户端占用过多资源,可以限制每个客户端的连接数。

limit_conn_zone $binary_remote_addr zone=mylimit:10m;
limit_conn mylimit 5;  # 每个IP地址最多允许5个同时连接

6. 监控和调整

  • 使用Nginx的统计模块(如ngx_http_stub_status_module)来监控负载均衡的性能。
  • 根据监控数据调整负载均衡策略和服务器权重,以优化性能。

7. 考虑使用第三方负载均衡解决方案

对于大型应用或需要更多高级功能的场景,可以考虑使用商业负载均衡器或云服务提供的负载均衡解决方案。

通过合理配置负载均衡策略,可以有效分散流量,提高网站的可用性和性能。在进行负载均衡调优时,需要根据服务器的具体情况和实际需求来选择合适的策略,并进行持续的监控和调整。

最后

以上 V 哥总结的5个方面关于 Nginx性能调优的策略,实际应用中需要结合实际项目的情况来测试,希望对你有帮助,欢迎关注威哥爱编程,技术路上我们一起成长。

在Spring Boot中,监听和处理事件是一种常用的模式,用于在应用程序的不同部分之间传递信息。Spring 的事件发布/订阅模型允许我们创建自定义事件,并在这些事件发生时由注册的监听器进行处理。这里,我将提供一个简单的Spring Boot应用程序示例,其中将包括事件的定义、事件的发布以及事件的监听。

1. Spring Boot应用程序示例

1.1 步骤 1: 创建Spring Boot项目

首先,我们可以使用Spring Initializr(
https://start.spring.io/)来快速生成一个新的Spring
Boot项目。在项目中添加
Spring Web
依赖,因为我们将使用一个简单的REST API来触发事件发布。

1.2 步骤 2: 定义事件

首先,我们定义一个简单的事件类。这个类将作为事件对象在应用程序中传递。

import org.springframework.context.ApplicationEvent;  
  
public class CustomEvent extends ApplicationEvent {  
  
    private final String message;  
  
    public CustomEvent(Object source, String message) {  
        super(source);  
        this.message = message;  
    }  
  
    public String getMessage() {  
        return message;  
    }  
}

1.3 步骤 3: 创建事件监听器

然后,我们定义一个监听器来监听上面定义的事件。

import org.springframework.context.event.EventListener;  
import org.springframework.stereotype.Component;  
  
@Component  
public class CustomEventListener {  
  
    @EventListener  
    public void handleCustomEvent(CustomEvent event) {  
        System.out.println("Received custom event - " + event.getMessage());  
        // 在这里可以执行更多操作,比如发送邮件、更新数据库等  
    }  
}

1.4 步骤 4: 发布事件

现在,我们需要一个方式来发布事件。通常,这会在业务逻辑代码中完成,但为了简单起见,我们将通过REST API来触发事件的发布。

首先,在我们的Spring Boot应用中添加一个控制器。

import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.context.ApplicationEventPublisher;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestParam;  
import org.springframework.web.bind.annotation.RestController;  
  
@RestController  
public class EventController {  
  
    @Autowired  
    private ApplicationEventPublisher eventPublisher;  
  
    @PostMapping("/publish")  
    public String publishEvent(@RequestParam String message) {  
        CustomEvent customEvent = new CustomEvent(this, message);  
        eventPublisher.publishEvent(customEvent);  
        return "Event published with message: " + message;  
    }  
}

1.5 步骤 5: 运行我们的Spring Boot应用

现在,我们可以运行我们的Spring Boot应用。一旦应用启动,我们可以使用Postman或curl命令来触发事件发布。

bash复制代码

curl -X POST http://localhost:8080/publish?message=Hello%20Spring%20Events

我们将在控制台看到输出,表明
CustomEventListener
已经接收并处理了事件。

1.6 总结

以上就是在Spring Boot中监听和处理自定义事件的一个完整示例。通过定义事件、创建监听器并发布事件,我们可以在不同的组件或服务之间轻松地传递信息。这种模式在微服务架构中尤其有用,因为它支持松耦合的通信方式。

2. 更详细的Spring Boot代码示例

当然,我会给出一个更详细的Spring Boot代码示例,该示例包含了完整的项目结构、配置以及必要的类来展示如何定义事件、监听事件以及通过REST API发布事件。

2.1 项目结构

假设我们的项目结构如下:

src/  
|-- main/  
|   |-- java/  
|   |   |-- com/  
|   |   |   |-- example/  
|   |   |       |-- demo/  
|   |   |           |-- DemoApplication.java  
|   |   |           |-- CustomEvent.java  
|   |   |           |-- CustomEventListener.java  
|   |   |           |-- EventController.java  
|   |-- resources/  
|       |-- application.properties  
|  
|-- pom.xml

2.2
pom.xml

首先,确保我们的
pom.xml
文件中包含了Spring Boot的起步依赖(starter)和Spring Web依赖:

<dependencies>  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
  
    <!-- Optional, but recommended -->  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-test</artifactId>  
        <scope>test</scope>  
    </dependency>  
</dependencies>  
  
<properties>  
    <java.version>11</java.version>  
    <spring-boot.version>2.5.4</spring-boot.version>  
</properties>  
  
<parent>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>${spring-boot.version}</version>  
</parent>

2.3
DemoApplication.java

这是Spring Boot的主应用类:

package com.example.demo;  
  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
  
@SpringBootApplication  
public class DemoApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(DemoApplication.class, args);  
    }  
}

2.4
CustomEvent.java

这是自定义事件类:

package com.example.demo;  
  
import org.springframework.context.ApplicationEvent;  
  
public class CustomEvent extends ApplicationEvent {  
  
    private final String message;  
  
    public CustomEvent(Object source, String message) {  
        super(source);  
        this.message = message;  
    }  
  
    public String getMessage() {  
        return message;  
    }  
}

2.5
CustomEventListener.java

这是事件监听器类:

package com.example.demo;  
  
import org.springframework.context.event.EventListener;  
import org.springframework.stereotype.Component;  
  
@Component  
public class CustomEventListener {  
  
    @EventListener  
    public void handleCustomEvent(CustomEvent event) {  
        System.out.println("Received custom event - " + event.getMessage());  
        // 在这里可以执行更多操作,比如发送邮件、更新数据库等  
    }  
}

2.6
EventController.java

这是REST控制器类,用于发布事件:

package com.example.demo;  
  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.context.ApplicationEventPublisher;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestParam;  
import org.springframework.web.bind.annotation.RestController;  
  
@RestController  
public class EventController {  
  
    @Autowired  
    private ApplicationEventPublisher eventPublisher;  
  
    @PostMapping("/publish")  
    public String publishEvent(@RequestParam String message) {  
        CustomEvent customEvent = new CustomEvent(this, message);  
        eventPublisher.publishEvent(customEvent);  
        return "Event published with message: " + message;  
    }  
}

2.7
application.properties

这是一个空的
application.properties
文件,但我们可以在这里添加任何Spring Boot配置。

2.8 运行和测试

(1)运行
DemoApplication.java
来启动Spring Boot应用。

(2)使用Postman或curl命令向
http://localhost:8080/publish?message=Hello%20Spring%20Events
发送POST请求。

(3)查看控制台输出,当我们向
/publish
端点发送POST请求时,Spring Boot应用会捕获到这个请求,并通过
EventController
中的
publishEvent
方法发布一个
CustomEvent
。这个事件随后被
CustomEventListener
捕获并处理,我们会在控制台上看到类似这样的输出:

复制代码

Received custom event - Hello Spring Events

这表明我们的事件监听器成功接收到了事件,并执行了相应的逻辑(在这个例子中是打印了一条消息)。

2.9 完整测试

为了完整地测试这个功能,我们可以使用Postman或者curl命令行工具来发送HTTP POST请求。以下是使用curl命令的示例:

bash复制代码

curl -X POST http://localhost:8080/publish?message=Hello%20Spring%20Events

我们应该会收到一个响应,内容是:

复制代码

Event published with message: Hello Spring Events

同时,我们的Spring Boot应用的控制台上也会显示事件被接收的消息。

2.10 总结

这个示例展示了如何在Spring Boot应用中定义自定义事件、发布事件以及监听事件。这是Spring事件驱动编程模型的一个简单应用,它允许我们以解耦的方式在应用的不同部分之间传递信息。在这个例子中,我们创建了一个简单的REST API来触发事件的发布,但这只是事件发布方式的一种。在更复杂的应用中,事件可能由多种不同的源触发,包括其他REST API调用、数据库更新、定时任务等。

通过利用Spring的事件监听和发布机制,我们可以轻松地构建出更加模块化和可维护的应用,因为我们可以在不修改监听器代码的情况下添加新的事件源,或者在不修改事件源代码的情况下添加新的监听器。这种解耦的方式使得应用更加灵活和可扩展。


title: Nuxt框架中内置组件详解及使用指南(三)
date: 2024/7/8
updated: 2024/7/8
author:
cmdragon

excerpt:
摘要:“Nuxt 3框架中
组件的深度使用教程,包括如何使用这两个组件进行页面导航和加载指示的自定义配置与实战示例。”

categories:

  • 前端开发

tags:

  • Nuxt3
  • 组件
  • NuxtLink
  • 导航
  • 链接
  • 加载
  • 自定义


image

image

扫描
二维码
关注或者微信搜一搜:
编程智域 前端至全栈交流与成长

Nuxt 3 中
<NuxtLink>
组件的使用指南与示例

NuxtLink 是 Nuxt.js 框架提供的一种组件,用于在 Vue.js 应用程序中创建链接。它结合了 Vue Router 的强大功能与 HTML
<a>
标签的简洁性,使得在 Nuxt.js 应用中创建内部和外部链接变得轻而易举。NuxtLink
组件能够智能地处理链接的优化,如预加载、默认属性等,为开发者提供了一种高效、灵活的链接解决方案。

在 Nuxt.js 应用中,使用 NuxtLink 组件创建链接非常简单。只需要将
<NuxtLink>
标签包裹在你想要链接的文本或内容上,然后指定
to
属性即可。下面是一个创建内部链接的示例:

<template>
  <NuxtLink to="/about">
    关于页面
  </NuxtLink>
</template>

对于外部链接,只需将链接地址作为
to
属性的值即可:

<template>
  <NuxtLink to="https://nuxtjs.org">
    Nuxt 网站
  </NuxtLink>
</template>

NuxtLink 组件提供了丰富的属性来增强链接的交互性和功能:

  • target
    属性

    :允许指定链接在新标签页中打开,如
    target="_blank"
  • rel
    属性

    :用于设置链接的 rel 属性,如
    rel="noopener noreferrer"
    ,适用于外部链接。
  • noRel
    属性

    :当链接需要外部链接的行为但不需要 rel 属性时,可以使用此属性。
  • activeClass

    exactActiveClass

    :用于自定义活动链接和精确活动链接的类,帮助实现更丰富的视觉效果。
  • replace
    属性

    :控制链接点击时是否替换当前页面历史记录。
  • ariaCurrentValue
    属性

    :用于设置活动链接的 aria-current 属性,提高无障碍访问性。
  • external
    属性

    :强制将链接视为外部链接或内部链接。
  • prefetch

    noPrefetch

    :控制是否为即将进入视口的链接预加载资源。
  • prefetchedClass
    :应用于已预加载链接的类。
  • custom
    属性

    :允许完全自定义链接的渲染方式和导航行为。

覆盖默认值

如果你希望自定义 NuxtLink 的某些行为,可以通过创建自定义组件并使用
defineNuxtLink
函数来实现。这使得你可以根据特定需求调整链接的默认设置。

示例代码:创建自定义链接组件

// components/MyNuxtLink.vue
export default defineNuxtLink({
  componentName: 'MyNuxtLink',
  externalRelAttribute: 'noopener', // 自定义外部链接的 rel 属性
  activeClass: 'my-active-class', // 自定义活动链接类
  exactActiveClass: 'my-exact-active-class', // 自定义精确活动链接类
  prefetchedClass: 'my-prefetched-class', // 自定义预加载链接类
  trailingSlash: 'remove', // 自定义尾部斜杠行为
})

完整示例:

  1. 项目结构
    - 你的 Nuxt.js 项目应该有以下的文件和目录结构:
my-nuxt-app/
├── components/
│   └── MyNuxtLink.vue
├── pages/
│   ├── index.vue
│   └── about.vue
├── nuxt.config.js
└── package.json

  1. 创建自定义链接组件
    (
    components/MyNuxtLink.vue
    ):
<template>
  <NuxtLink
    :to="to"
    :target="target"
    :rel="rel"
    :class="{'my-active-class': isActive, 'my-exact-active-class': isExactActive}"
  >
    <slot></slot>
  </NuxtLink>
</template>

<script>
export default defineNuxtLink({
  componentName: 'MyNuxtLink',
  externalRelAttribute: 'noopener noreferrer',
  activeClass: 'my-active-class',
  exactActiveClass: 'my-exact-active-class',
  prefetchedClass: 'my-prefetched-class',
  trailingSlash: 'remove'
});
</script>

<style>
.my-active-class {
  color: red;
}

.my-exact-active-class {
  font-weight: bold;
}
</style>
  1. 首页
    (
    pages/index.vue
    ):
<template>
  <div>
    <h1>欢迎来到首页</h1>
    <MyNuxtLink to="/about">关于页面</MyNuxtLink>
    <MyNuxtLink to="https://nuxtjs.org" target="_blank" external>访问 Nuxt.js 官网</MyNuxtLink>
  </div>
</template>

<script>
export default {
  components: {
    MyNuxtLink: () => import('~/components/MyNuxtLink.vue')
  }
}
</script>
  1. 关于页面
    (
    pages/about.vue
    ):
<template>
  <div>
    <h1>关于我们</h1>
    <MyNuxtLink to="/">返回首页</MyNuxtLink>
  </div>
</template>

<script>
export default {
  components: {
    MyNuxtLink: () => import('~/components/MyNuxtLink.vue')
  }
}
</script>
  1. 配置文件
    (
    nuxt.config.js
    ):
export default {
  components: true, // 启用自动导入组件
  // 其他配置...
}

  1. 启动项目
    - 在项目根目录下运行以下命令启动开发服务器:
npm run dev

打开浏览器并访问
http://localhost:3000
,你应该能看到首页,并且可以通过自定义的 MyNuxtLink 组件导航到“关于页面”或者打开新标签页访问
Nuxt.js 官网。

Nuxt 3 中
<NuxtLoadingIndicator>
组件的使用指南与示例

Nuxt Loading Indicator 是 Nuxt.js 应用程序中一个实用的组件,用于在页面加载或导航时显示加载进度条。这不仅提升用户体验,还能为用户显示应用程序正在执行的操作,从而减少不确定性。

如何使用 Nuxt Loading Indicator?

在 Nuxt.js 应用中,要使用 Nuxt Loading Indicator,首先需要在你的
app.vue
或任何布局文件中引入并添加此组件。以下是一个简单的示例:

<template>
  <NuxtLayout>
    <div>
      <NuxtLoadingIndicator /> <!-- 这里是加载指示器的位置 -->
      <NuxtPage />
    </div>
  </NuxtLayout>
</template>

Nuxt Loading Indicator 的关键属性

  1. color
    :设置进度条的颜色。默认为黑色,你可以根据需要调整颜色。
  2. height
    :进度条的高度,以像素为单位。默认值为 3px。
  3. duration
    :进度条显示的持续时间,以毫秒为单位。默认为 2000 毫秒。
  4. throttle
    :进度条出现和隐藏的节流时间,以毫秒为单位。默认为 200 毫秒。

如何自定义 Nuxt Loading Indicator

Nuxt Loading Indicator 支持通过默认插槽传递自定义 HTML 或组件,允许你根据特定需求定制进度条的外观和行为。

示例代码

假设你想要创建一个更自定义的进度条,可以使用以下代码:

<template>
  <NuxtLayout>
    <div>
      <NuxtLoadingIndicator
        :color="customColor"
        :height="customHeight"
        :duration="customDuration"
        :throttle="customThrottle"
      >
        <!-- 自定义内容 -->
        <div class="custom-loading-text">加载中...</div>
      </NuxtLoadingIndicator>
      <NuxtPage />
    </div>
  </NuxtLayout>
</template>

<script>
export default {
  data() {
    return {
      customColor: '#FF5733', // 自定义颜色
      customHeight: 5, // 自定义高度
      customDuration: 1500, // 自定义持续时间
      customThrottle: 500, // 自定义节流时间
    };
  },
};
</script>

<style scoped>
.custom-loading-text {
  color: #fff;
  font-size: 18px;
  text-align: center;
  margin-top: 50px;
}
</style>

完整示例:

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
,阅读完整的文章:
Nuxt框架中内置组件详解及使用指南(三) | cmdragon's Blog

往期文章归档:

前言

最近我在做知识星球中的商品秒杀系统,昨天遇到了一个诡异的json反序列化问题,感觉挺有意思的,现在拿出来跟大家一起分享一下,希望对你会有所帮助。

案发现场

我最近在做知识星球中的商品秒杀系统,写了一个filter,获取用户请求的header中获取JWT的token信息。

然后根据token信息,获取到用户信息。

在转发到业务接口之前,将用户信息设置到用户上下文当中。

这样接口中的业务代码,就能通过用户上下文,获取到当前登录的用户信息了。

我们的token和用户信息,为了性能考虑都保存到了Redis当中。

用户信息是一个json字符串。

当时在用户登录接口中,将用户实体,使用fastjson工具,转换成了字符串:

JSON.toJSONString(userDetails);

保存到了Redis当中。

然后在filter中,通过一定的key,获取Redis中的字符串,反序列化成用户实体。

使用的同样是fastjson工具:

JSON.parseObject(json, UserEntity.class);

但在反序列化的过程中,filter抛异常了:
com.alibaba.fastjson.JSONException: illegal identifier : \pos 1, line 1, column 2{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}

2 分析问题

我刚开始以为是json数据格式有问题。

将json字符串复制到在线json工具:
https://www.sojson.com
,先去掉化之后,再格式数据,发现json格式没有问题:

然后写了一个专门的测试类,将日志中打印的json字符串复制到json变量那里,使用JSON.parseObject方法,将json字符串转换成Map对象:

public class Test {

    public static void main(String[] args) {
        String json = "{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}";
        Map map = JSON.parseObject(json, Map.class);
        // 输出解析后的 JSON 对象
        System.out.println(map);
    }
}

执行结果:

{password=$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe, credentialsNonExpired=true, roles=["admin"], accountNonExpired=true, id=13, authorities=[{"authority":"admin"}], enabled=true, accountNonLocked=true, username=admin}

竟然转换成功了。

这就让我有点懵逼了。。。

为什么相同的json字符串,在Test类中能够正常解析,而在filter当中却不行?

当时怕搞错了,debug了一下filter,发现获取到的json数据,跟Test类中的一模一样:

带着一脸的疑惑,我做了下面的测试。

莫非是反序列化工具有bug?

3 改成gson工具

我尝试了一下将json的反序列化工具改成google的gson,代码如下:

 Map map = new Gson().fromJson(userJson, Map.class);

运行之后,报了一个新的异常:
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 2 path $

这里提示json字符串中包含了:
$


$
是特殊字符,password是做了加密处理的,里面包含
$

.
,这两种特殊字符。

为了快速解决问题,我先将这两个特字符替换成空字符串:

json = json.replace("$","").replace(".","");

日志中打印出的json中的password,已经不包含这两个特殊字符了:

2a10o3XfeGr0SHStAwLuJRW6ykE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe

但调整之后代码报了下面的异常:
com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Expected name at line 1 column 2 path $.

跟刚刚有点区别,但还是有问题。

4 改成jackson工具

我又尝试了一下json的反序列化工具,改成Spring自带的的jackson工具,代码如下:

ObjectMapper objectMapper = new ObjectMapper();
try {
    Map map = objectMapper.readValue(json, Map.class);
} catch (JsonProcessingException e) {
    e.printStackTrace();
}

调整之后,反序列化还是报错:
com.fasterxml.jackson.core.JsonParseException: Unexpected character ('\' (code 92)): was expecting double-quote to start field name

3种反序列化工具都不行,说明应该不是fastjson的bug导致的当前json字符串,反序列化失败。

到底是什么问题呢?

5 转义

之前的数据,我在仔细看了看。

里面是对双引号,是使用了转义的,具体是这样做的:
\"

莫非还是这个转义的问题?

其实我之前已经注意到了转义的问题,但使用Test类测试过,没有问题。

当时的代码是这样的:

public class Test {

    public static void main(String[] args) {
        String json = "{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}";
        Map map = JSON.parseObject(json, Map.class);
        // 输出解析后的 JSON 对象
        System.out.println(map);
    }
}

里面也包含了一些转义字符。

我带着试一试的心态,接下来,打算将转义字符去掉。

看看原始的json字符串,解析有没有问题。

怎么去掉转义字符呢?

手写工具类,感觉不太好,可能会写漏一些特殊字符的场景。

我想到了org.apache.commons包下的StringEscapeUtils类,它里面的unescapeJava方法,可以轻松去掉Java代码中的转义字符。

于是,我调整了一下代码:

json = StringEscapeUtils.unescapeJava(json);
JSON.parseObject(json, UserEntity.class);

这样处理之后,发现反序列化成功了。

总结

这个问题最终发现还是
转义
的问题。

那么,之前Test类中json字符串,也使用了转义,为什么没有问题?

当时的代码是这样的:

public class Test {

    public static void main(String[] args) {
        String json = "{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}";
        Map map = JSON.parseObject(json, Map.class);
        System.out.println(map);
    }
}

但在filter中的程序,在读取到这个json字符串之后,发现该字符串中包含了
\
转义符号,程序自动把它变成了
\\\

调整一下Test类的main方法,改成三个斜杠的json字符串:

public static void main(String[] args) {
    String json = "{\\\"accountNonExpired\\\":true,\\\"accountNonLocked\\\":true,\\\"authorities\\\":[{\\\"authority\\\":\\\"admin\\\"}],\\\"credentialsNonExpired\\\":true,\\\"enabled\\\":true,\\\"id\\\":13,\\\"password\\\":\\\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\\\",\\\"roles\\\":[\\\"admin\\\"],\\\"username\\\":\\\"admin\\\"}";
    Map map = JSON.parseObject(json, Map.class);
    System.out.println(map);
}

执行结果:
Exception in thread "main" com.alibaba.fastjson.JSONException: illegal identifier : \pos 1, line 1, column 2{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}
抛出了跟文章最开始一样的异常。

说明其实就是转义的问题。

之前,我将项目的日志中的json字符串,复制到idea的Test的json变量中,当时将最外层的双引号一起复制过来了,保存的是1个斜杠的数据。

这个操作把我误导了。

而后面从在线的json工具中,把相同的json字符串,复制到idea的Test的json变量中,在双引号当中粘贴数据,保存的却是3个斜杠的数据,它会自动转义。

让我意识到了问题。

好了,下次如果遇到类似的问题,可以直接使用org.apache.commons包下的StringEscapeUtils类,先去掉转义,再反序列化,这样可以快速解决问题。

此外,这次使用了3种不同的反序列化工具,也看到了其中的一些差异。

如果你对日常工作中的一些坑,比较感兴趣,可以看看我的技术专栏《
程序员最常见的100个问题
》,里面有很多干货,还是非常值得一看的。

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

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

Vue 处理异步加载顺序问题:在Konva中确保文本在Konva之上显示

在使用Konva开发应用时,我们经常会遇到需要将文本绘制在图片之上的情况。一个常见的问题是,由于图像加载是异步的,文本有时会显示在图片下方。这篇博客将总结如何正确处理这种异步加载顺序问题。

我之前写过一篇博客,主要是为了说明如何通过父子组件来控制Konva组件间的先后绘制顺序,利用了Vue的生命周期(
自定义父子组件mounted执行顺序
)。这种方法适用于将Konva组件分开到不同的Vue组件中,通过Vue的生命周期来确保正确的绘制顺序。然而,这种方法并不适用于所有情况,比如说必须在同一个文件中编写,一起调用,或者都在setup语法糖中等情况。本文将探讨在这些情况下如何确保文本在图片之上显示。

问题描述

我们希望在绘制图片后,再在图片上方绘制文本。一个简单的代码片段如下:

for (let i = 0; i < 4; i++) {
    const geometry = {
        x: 100 + (i % 2 === 0 ? 0 : 150),
        y: 50 + (i < 2 ? 0 : 100),
        width: 100,
        height: 100,
    };

    addWidgetImgToLayer(geometry, './img/b.png', true, group, 'click', () => {}, () => {
        addShapesToLayer(geometry, 'text', false, group, `${i + 1}`);
    });
}

初步实现

我们实现了两个函数:
addWidgetImgToLayer
用于加载图片,
addShapesToLayer
用于添加Shapes(这里添加了Text)。

const addWidgetImgToLayer = (geometry, src, listening = false, parent, eventType, eventFunc, callback) => {
    const layer = konvaStore.layers['consoleLayer'];
    const imageObj = new Image();
    let konvaImage = null;

    imageObj.onload = () => {
        konvaImage = new Konva.Image({
            ...geometry,
            image: imageObj,
            listening: listening,
        });
        if (eventType && eventFunc) {
            konvaImage.on(eventType, eventFunc);
        }
        if (parent) {
            parent.add(konvaImage);
        } else {
            layer.add(konvaImage);
        }
        layer.batchDraw();
        if (callback) {
            callback();
        }
    };
    imageObj.src = src;
    return imageObj;
};

const addShapesToLayer = (geometry, type, listening = false, parent, text) => {
    const layer = konvaStore.layers['consoleLayer'];
    let shape = null;
    if (type === 'text') {
        shape = new Konva.Text({
            ...geometry,
            listening: listening,
            text: text,
            fontSize: 20,
            align: 'center',
            verticalAlign: 'middle',
        });
    }
    if (parent) {
        parent.add(shape);
    } else {
        layer.add(shape);
    }
    layer.batchDraw();
    return shape;
};

异步问题

这里的关键在于
imageObj.onload
回调函数。图片加载是异步的,代码不会等待图片加载完成才执行接下来的语句。因此,必须确保回调函数在图片加载完成后才执行添加文本的操作。

尝试1:直接回调

我们首先尝试使用回调来确保顺序执行:

for (let i = 0; i < 4; i++) {
    const geometry = {
        x: 100 + (i % 2 === 0 ? 0 : 150),
        y: 50 + (i < 2 ? 0 : 100),
        width: 100,
        height: 100,
    };

    addWidgetImgToLayer(geometry, './img/b.png', true, group, 'click', () => {}, () => {
        addShapesToLayer(geometry, 'text', false, group, `${i + 1}`);
    });
}

然而,这种方式下,我们遇到了无法传递参数的问题。在回调函数中,无法访问循环中的变量
geometry

group

尝试2:在函数中写回调

我们尝试在
addWidgetImgToLayer
函数中编写回调,但放在了
imageObj.onload
之外:

const addWidgetImgToLayer = (geometry, src, listening = false, parent, eventType, eventFunc, callback) => {
    const layer = konvaStore.layers['consoleLayer'];
    const imageObj = new Image();
    let konvaImage = null;

    imageObj.onload = () => {
        konvaImage = new Konva.Image({
            ...geometry,
            image: imageObj,
            listening: listening,
        });
        if (eventType && eventFunc) {
            konvaImage.on(eventType, eventFunc);
        }
        if (parent) {
            parent.add(konvaImage);
        } else {
            layer.add(konvaImage);
        }
        layer.batchDraw();
    };
    
    if (callback) {
        callback();
    }
    
    imageObj.src = src;
    return imageObj;
};

这导致了文本依然在图片下方的问题,因为回调函数立即执行,而不是等待图片加载完成再执行。分析发现,问题依然是异步加载的问题,即使解决了参数传递问题,这种方法也不能正确得到想要的结果。

尝试3:将回调移至
imageObj.onload
内部

我们意识到了JavaScript的异步执行机制。在函数嵌套的情况下,异步函数会在调用堆栈清空后才执行。我们需要确保回调函数在图片加载完成后才执行。

const addWidgetImgToLayer = (geometry, src, listening = false, parent, eventType, eventFunc, callback) => {
    const layer = konvaStore.layers['consoleLayer'];
    const imageObj = new Image();
    let konvaImage = null;

    imageObj.onload = () => {
        konvaImage = new Konva.Image({
            ...geometry,
            image: imageObj,
            listening: listening,
        });
        if (eventType && eventFunc) {
            konvaImage.on(eventType, eventFunc);
        }
        if (parent) {
            parent.add(konvaImage);
        } else {
            layer.add(konvaImage);
        }
        layer.batchDraw();
        
        if (callback) {
            callback();
        }
    };
    imageObj.src = src;
    return imageObj;
};

// 使用 addWidgetImgToLayer 并确保回调在图片加载完成后执行
for (let i = 0; i < 4; i++) {
    const geometry = {
        x: 100 + (i % 2 === 0 ? 0 : 150),
        y: 50 + (i < 2 ? 0 : 100),
        width: 100,
        height: 100,
    };

    // 这里可以使用两种不同的实现方式,现在是比较简洁的格式,下面会详细说明
    addWidgetImgToLayer(geometry, './img/b.png', true, group, 'click', () => {}, () => {
        addShapesToLayer(geometry, 'text', false, group, `${i + 1}`);
    });
}

比较两种实现方式

在解决这个问题时,我们还可以采用两种不同的实现方式:

方式1:使用立即执行函数表达式(IIFE)

addWidgetImgToLayer(dispBtnGeometrys[i], './img/b.png', true, group, 'click', () => { }, (function (geo, grp, idx) {
    return function () {
        addShapesToLayer(geo, 'text', false, grp, `${idx}`);
    };
})(dispBtnGeometrys[i], group, i + 1));

这种写法使用了一个立即执行函数表达式(
IIFE
),这个函数会立即执行并返回一个新的函数。通过这种方式,可以捕获循环中的当前变量状态,并在异步回调中使用。

方式2:直接传递回调

for (let i = 0; i < 4; i++) {
    let geometry = {
        x: 100 + (i % 2 === 0 ? 0 : 150),
        y: 50 + (i < 2 ? 0 : 100),
        width: 100,
        height: 100,
    };

    addWidgetImgToLayer(geometry, './img/b.png', true, group, 'click', () => {}, () => {
        addShapesToLayer(geometry, 'text', false, group, `${i + 1}`);
    });
}

这种写法直接传递了一个回调函数给
addWidgetImgToLayer
。通过使用
let
声明来确保每次循环迭代中创建一个新的块作用域,变量捕获是正确的。

主要区别

  1. 变量捕获


    • 第一种写法通过IIFE来捕获当前循环迭代中的变量状态,确保异步回调中使用的是当前迭代的值。
    • 第二种写法直接传递回调,如果使用
      let
      声明或其他方法,能正确捕获每次迭代中的变量状态。
  2. 代码可读性


    • 第一种写法较为复杂,但能够确保异步回调中的变量正确捕获当前状态。

    • 第二种写法更简洁,但需要确保使用
      let
      声明或其他方法正确捕获变量,而不能使用var(此处使用到的是循环变量
      i
      )。


      在JavaScript中,
      var
      声明的变量在函数作用域内是共享的

      ,这意味着在异步回调中,它们的值可能会变成最后一次迭代的值。这会导致我们期望的结果不正确。而
      let
      声明的变量在每次循环迭代中都会创建一个新的块作用域,从而确保异步回调中捕获的是当前迭代的值。

      示例解释

      使用
      var
      声明的问题

      for (var i = 0; i < 4; i++) {
          const geometry = {
              x: 100 + (i % 2 === 0 ? 0 : 150),
              y: 50 + (i < 2 ? 0 : 100),
              width: 100,
              height: 100,
          };
      
          addWidgetImgToLayer(geometry, './img/b.png', true, group, 'click', () => {}, () => {
              console.log(i);  // 可能会输出4,而不是期望的0, 1, 2, 3,可能在其他位置被修改过了,它们指向同一个地址
              addShapesToLayer(geometry, 'text', false, group, `${i + 1}`);
          });
      }
      

      因为
      var
      声明的变量
      i
      在函数作用域内是共享的(有点像Python),所以在异步回调中,
      i
      的值可能是循环结束时的值(4),而不是期望的0, 1, 2, 3。

      使用
      let
      声明解决问题

      for (let i = 0; i < 4; i++) {
          let geometry = {
              x: 100 + (i % 2 === 0 ? 0 : 150),
              y: 50 + (i < 2 ? 0 : 100),
              width: 100,
              height: 100,
          };
      
          addWidgetImgToLayer(geometry, './img/b.png', true, group, 'click', () => {}, () => {
              console.log(i);  // 输出期望的0, 1, 2, 3,使用let声明变量不会出现问题
              addShapesToLayer(geometry, 'text', false, group, `${i + 1}`);
          });
      }
      

      let
      声明的变量
      i
      在每次循环迭代中都会创建一个新的块作用域,因此在异步回调中,
      i
      的值是当前迭代的值,确保输出的是期望的0, 1, 2, 3。

      其他捕获变量的方法

      另一种确保变量捕获正确的方法是使用立即执行函数表达式(IIFE):

      for (var i = 0; i < 4; i++) {
          (function(i) {
              let geometry = {
                  x: 100 + (i % 2 === 0 ? 0 : 150),
                  y: 50 + (i < 2 ? 0 : 100),
                  width: 100,
                  height: 100,
              };
      
              addWidgetImgToLayer(geometry, './img/b.png', true, group, 'click', () => {}, () => {
                  console.log(i);  // 输出期望的0, 1, 2, 3,可以理解为使用IIFE的话开辟空间事会先对使用到的数据进行了值拷贝
                  addShapesToLayer(geometry, 'text', false, group, `${i + 1}`);
              });
          })(i);
      }
      

      通过IIFE,每次循环迭代都会创建一个新的函数作用域,确保异步回调中的变量是当前迭代的值。

      结论

      在第二种写法中,通过使用
      let
      声明或IIFE,确保异步回调函数中捕获的变量值是当前迭代的值,而不是循环结束后的值。这是确保变量正确捕获和代码正确执行的关键。

总结

在处理Konva中的异步加载顺序问题时,确保在图像加载完成后再添加其他元素是关键。通过将回调函数放在
imageObj.onload
中,并正确处理变量捕获,我们可以确保文本总是绘制在图片之上。这不仅解决了显示顺序的问题,也为未来的调试提供了明确的方向。