2024年11月

业余时间用 .net core 写了一个在线客服系统。我把这款业余时间写的小系统丢在网上,陆续有人找我要私有化版本,我都给了,毕竟软件业的初衷就是免费和分享。

后来我索性就发了一个100%私有化版直接给大家下载,方便大家自己部署。同时,为了方便暂时不想自己私有化部署的朋友使用,我免费搭建了一个在线环境给大家使用。

今天我查了下在线使用环境的数据库,累计的处理消息条数居然达到了创纪录的 489933 条!! 48 万余条!!

我十分震惊这个数量,因为这个在线环境我挂上去之后除了正常版本更新,平时基本不管,也不需要维护什么的,今天想起来看一下,居然处理了这么多消息!

虽然系统完全是免费的,免费给客户使用,但是达到这样一个成绩,自己的小系统有这个的用户,也很欣慰。

有朋友说:如果一个程序员编写的系统 有人用,就已经超过了 99% 的程序员,哈哈,欣慰了。


我是怎么做到的

系统架构

服务端程序除了提供一般数据增删改查能力之外,重要的是需要实现稳定的消息中件间,稳定的站点状态、客服状态、访客状态等上下文数据在内存中的管理,以及稳定的TCP/IP长连接维护机制。
综合考虑技术指标和开发效率,我采用了 .net core 做为服务端程序,SQL Server 做为数据库的方案。
.net core 目前已经具备了完善的技术指标,并且能够提供其它开发平台无法比拟的开发效率。得益于整个 .net 技术体系,使我靠一己之力完成整个系统成为可能。

数据库我采用了 SQL Server ,它能够借助 Entity Framework Core 与 .net core 无缝融合,提供无与伦比的开发效率。同时,在数据处理的性能上,完全能够满足各项技术指标的要求,在日常使用和运维上,也提供了 Oracle 和 MySQL 无法比拟的便利性和工作效率。

客服端程序我使用了 WPF 框架。相比 WinForms 程序,WPF 提供了更高的开发效率和更完备的基础框架,使得画面呈现、数据处理、模块解耦更容易。同样,基于 .net 体系的 WPF 所提供的极高的开发效率是其它开发平台无法提供的。对于需要一天十几个小时稳定运行不退出的程序而言,原生客户端程序的稳定性也是基于 Web 页面的程序所无法相比的。


在上文中,我们提到系统可以横向扩展,具备弹性扩容的能力。在初期可以以较低的配置环境运行,用户增加时,可以在不影响原有结构的基础上,无感的横向扩展。亦或因服务规模的减小而收缩。

下面我将从系统的整体结构来阐述这个问题:

从上图可以看出这是一个典型的分布式部署系统。客户网站中嵌入的 JavaScript 文件在运行是地,首先连接路由服务器,路由服务器保存了一张表,每个站点所应该使用的应用服务器地址。就好像DNS服务器一样,告诉客户网站你应该向哪里发起真正的连接请求。接着,客户网站中嵌入的 JavaScript 程序向对应的应用服务器发起连接,上报访客状态,接收服务器指令。
那么很容易理解,我们只需要扩展我们的应用服务器数量,就可以轻松的增加系统的承载能力。反之也可以进行缩减操作。

在处理客户的私有化部署需求时,我们也可以根据容易的实际需要,取消路由服务器、取消文件服务器、取消CDN内容分发网络。以一个简单的方案来应对访问量不高的中小企业需求。在这个基础上,我们还可以借助现在的云服务器,实现弹性配置,以一个较低配置的服务器开始运行,甚至将数据库和缓存服务完全部署在一台服务器上使用,在使用过程中发现确有需要,先增加单台服务器的配置,不能满足需求的,再分离数据库和缓存服务,还不能满足要求的,则增加路由服务器,增加应用服务器。

消息传输

首先我们回顾一下 TCP 协议,TCP 报文格式一般如下所示:
其中的 ACK ,表示对报文是否送达的一个回应。
在这里插入图片描述
ACK是TCP标头中的标志和字段。 发送一个消息至少需要一个标头,再加上所有较低层的内容。

下图则显示了 TCP 通信时,客户端和服务端之间报文传送的过程。
从图中可以看到,发出的消息,和回应的消息,都会有一个编号,如:#1、#2
在ACK报文回应时,它回附带上所收到的报文的编号,那么发送端只需根据收到的ACK报文中的编号,就能判定报文是否送达,已经所送达的数据包。如果在一定时间内,没有收到回应的ACK消息,则发送端会在一定时间内重新尝试发送。
在这里插入图片描述

网络异常的处理

这种情况最好处理。因为客户端程序异常退出会直接引发 ConnectionReset 的 Socket 异常。我们只需要在服务端捕获这个异常进行处理即可:

 public bool Send(byte[] data)
        {
            // 连接已经断开了
            try
            {
                _networkStream.Write(data, 0, data.Length);
            }
            catch (Exception ex)
            {
                OnDisconnected(ex);
                return false;
            }

            return true;
        }

对于这种情况,我们只需要检测 Socket 对象的 Connected 属性。
但是需要特别注意:Socket 对象的 Connected 属性获取从 Socket 最后一个 i/o 操作到的的连接状态。 当它返回时 false , Socket 要么从未连接,要么不再处于连接状态。当 Socket 从另一个线程断开连接时,它可能会在操作中止后返回。
如果需要确定连接的当前状态,请发出非阻止的零字节发送调用。 如果调用成功返回或引发 WAEWOULDBLOCK 错误代码 (10035) ,则套接字仍处于连接状态;否则,将不再连接套接字。

我们可以通过实现一个定时心跳,来对网络链路进行检测:

_heartbeatTimer = new Timer((state) =>
            {
                HeartbeatMessage heartbeatMessage = new HeartbeatMessage();
                Send(heartbeatMessage);

            }, null, 3000, 3000);

在定时器发送心跳时,如果网络链路中断,我们可以收到以下消息:

 private void _socketClient_Disconnected(object sender, EventArgs e)
        {
            if (_heartbeatTimer != null)
                _heartbeatTimer.Dispose();

            if (_socketClient != null)
            {
                _socketClient.Close();
                _socketClient = null;
            }
        }

只需针对 Disconnected 事件,进行处理,将两端的状态,置于等待即可。


简介下这个 .net 开发的小系统

https://kf.shengxunwei.com/

  • 可以追踪正在访问网站或使用 APP 的所有访客,收集他们的浏览情况,使客服能够主动出击,施展话术,促进成单。
    访* 客端在 PC 支持所有新老浏览器。包括不支持 WebSocket 的 IE8 也能正常使用。
  • 移动端支持所有手机浏览器、APP、各大平台的公众号对接。
  • 支持访客信息互通,可传输访客标识、名称和其它任意信息到客服系统。
  • 具备一线专业技术水平,网络中断,拔掉网线,手机飞行模式,不丢消息。同类软件可以按视频方式对比测试。

希望能够打造:
开放、开源、共享。努力打造 .net 社区的一款优秀开源产品。

钟意的话请给个赞支持一下吧,谢谢~

大家好,我是程序员鱼皮。今天来聊聊 Nginx 技术,这是一个企业项目必用,但是却经常被程序员忽略的技术。学好 Nginx,可以助你在求职中脱颖而出。

或许你会想:“Nginx 不就是用来部署网站的服务器嘛?这有何难?”

但其实这不过是九牛一毛罢了,Nginx 的实用操作和使用技巧还多着呢,下面这篇文章,就带大家轻松入门 Nginx、并且循序渐进地学习 Nginx 真正的用法!

推荐观看本文对应的视频版本,有更多操作演示哦:
https://bilibili.com/video/BV1TW1LYkE59

一、Nginx 入门 - 牛刀小试

首先要了解什么是 Nginx?注意读音,是 Engine X,而不是恩静因克斯。

根据官方定义,它是世界上最受欢迎的 Web 服务器、高性能负载均衡器、反向代理、API 网关和内容缓存。

虽然听不懂,但是感觉很厉害的样子。

简单来说,Nginx 不仅能部署网站,而且相比其他的 Web 服务器,它能够用更少的资源,同时处理更多用户的请求,让网站速度更快更稳定,这也是企业选择 Nginx 的原因。

下面我们就牛刀小试,用 Nginx 启动一个网站!

1、Nginx 安装

首先我们需要安装 Nginx ,先到官网中根据操作系统下载一个稳定版本的压缩包,下载完成之后解压一下。

如果是 Windows 系统,双击 exe 文件启动即可;如果是 Mac 或 Linux 系统,可以打开终端并进入该目录,手动编译安装后执行 Nginx 命令启动。

当然也可以使用第三方的包管理工具,比如 Chocolatey(Windows)、Homebrew(Mac)、Yum(Linux)。

或者使用现成的服务器运维面板,比如宝塔 Linux,可以傻瓜式一键安装:

2、修改网页文件

启动成功后,我们访问本机域名
localhost:80
(80 为默认端口,可以省略),就可以看到 Nginx 为我们提供的默认网站了。

那如果想自己修改网页内容,怎么办呢?

我们要找到 Nginx 的大脑,也就是配置文件。进入配置目录
conf
,就可以看到配置文件
nginx.conf
了。配置文件由块和指令组成,可以通过修改配置实现各种功能,比如通过 location 块和 root 指令配置网站文件的根路径:

我们找到这个 index.html 文件,修改网页的内容并保存:

重新访问就可以看到效果啦!

看到这里,恭喜你,已经超过 30% 的程序员了!

二、Nginx 常用操作 - 明劲

下面,我们要成为 Nginx 明劲武者。所谓明劲,就是要熟悉 Nginx 的基本配置和常用操作,能够满足企业开发中的大多数需求,如果你的目标是开发岗,那么学完下面这些就足够找工作了。

1、静态文件服务

我们开发好的网站,通常包含像 HTML、CSS、JavaScript、图片等文件,由于这些文件的内容在存储时是固定的,被称为静态文件。

如果你要让别人访问到开发好的网站,只把网站文件放到服务器上还是不够的,还需要一个 Web 服务器,能够接受用户的访问请求,并找到对应位置的文件进行响应。

Nginx 最基本的功能,就是作为 Web 服务器提供静态文件服务。

打开 Nginx 的配置文件
nginx.conf
,添加 location 块,用于根据请求地址处理请求。比如我们通过 root 指令定义静态文件根目录,通过 index 指令定义默认首页文件:

server {
 listen       80;
 server_name localhost;

 location / {
   root /tmp/nginx/html;  # 指定静态文件根目录
   index index.html;  # 默认首页
}
}

保存配置,然后执行
nginx -s reload
命令来重载配置,再次访问网站时就会返回刚配置的目录下的首页文件。

企业项目中,需要为特定路径定义不同的处理规则,location 块的配置会更复杂。支持根据请求路径的特定部分、正则表达式等进行匹配,比如到特定目录去寻找图片:

server {
 listen 80;                          # 监听 80 端口
 server_name example.com;            # 指定域名

 # 根路径的配置,返回静态文件
 location / {
   root /var/www/html;             # 指向静态文件的根目录
   index index.html;               # 默认首页文件
   try_files $uri $uri/ =404;     # 如果文件不存在,则返回 404
}

 # 处理以 /images/ 开头的请求
 location /images/ {
   root /var/www/assets/images/;  # 指向图片目录
}

 # 正则匹配,处理以 .php 结尾的请求
 location \.php$ {
   include fastcgi_params;          # 包含 FastCGI 参数
   fastcgi_pass 127.0.0.1:9000;    # 将请求转发到 FastCGI 处理程序
   fastcgi_index index.php;         # 设置 FastCGI 的默认索引文件
   fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;  # 定义脚本文件名
}
}

2、反向代理

Nginx 的另一个常用功能是用作反向代理服务器。什么是反向代理呢?一句话:Nginx 作为中介,帮后端服务器接受请求。

反向代理有什么作用呢?

首先是请求转发和解决跨域。比如在 location 块中添加 proxy_pass 配置,可以将 Nginx 在 80 端口收到的 /api 路径的请求转发到本地 8080 端口的后端服务。

server {
   listen 80;  # 监听 80 端口
   server_name localhost;  # 替换为你的域名或 IP 地址

   location /api {
       proxy_pass http://localhost:8080;  # 代理到本地的 8080 端口
  }
}

这样就隐藏了后端服务器的 IP 地址,让客户端完全感知不到后端服务器的存在,更加安全。

而且还能让前端和后端的域名统一,解决了跨域问题。

反向代理还可以用于实现负载均衡。由于企业项目的流量巨大,通常需要有多台后端服务器。Nginx 可以作为高性能网关,统一接收请求,并将请求按照特定规则转发到不同的后端服务器进行处理,从而分散了请求压力,避免单一服务器过载。

在 Nginx 中实现负载均衡非常简单,首先通过 upstream 块定义了一个名为 backend 的服务器组,其中包含两个后端服务器,然后通过反向代理配置将请求转发到这个服务器组即可:

upstream backend {
 server localhost:8080;  # 第一个后端服务器
 server localhost:8081;  # 第二个后端服务器
}

server {
 listen 80;
 server_name localhost;

 location /api {
   proxy_pass http://backend;  # 代理到负载均衡的后端服务器
}
}

这样每次都访问同一个地址,会交替返回两种不同的内容,这是因为 Nginx 的默认负载均衡算法是轮询,请求会被平均转发到两个不同的服务进行处理。

反向代理还有更多的作用,比如缓存常见请求的响应、减少后端负担,集中处理 SSL 加密、认证和日志记录等功能,后面会依次讲解。

3、改写请求和响应

第三个 Nginx 的常用功能是改写请求和响应。在请求到达服务器或响应返回给客户端之前,Nginx 可以对其进行修改。

改写请求与响应有什么作用呢?有几个比较典型的场景:

1)控制浏览器缓存

首先,
设置响应头
可以帮助我们控制浏览器缓存。通过 Nginx 的
add_header
指令,可以为响应添加自定义的 HTTP 头部,从而指导浏览器如何处理缓存。比如设置缓存有效期为 30 天:

location /images/ {
   root /tmp/nginx/html;
   expires 30d;  # 设置缓存有效期为 30 天
   add_header Cache-Control "public";  # 设置缓存头
}

这样,当用户访问图片时,浏览器会在本地缓存这些图片,下次访问时就不用访问服务器了,提高速度并减少对服务器的请求。

2)重定向

请求重定向允许我们将请求从一个地址自动引导到另一个地址,常见的应用场景包括将 HTTP 请求重定向到 HTTPS,或者将旧地址重定向到新地址。

在 Nginx 中,可以使用
return
指令 + 302 状态码配置重定向:

location /old-page {
   return 302 https://codefather.cn  # 重定向到新页面
}

当用户访问某个过期页面时,会被重定向自动跳转到新网站。

3)URI 重写

比重定向更高级一些,Nginx 提供了
rewrite
指令,支持正则表达式,可以非常灵活地将请求重写为不同的路径或网站。比如将
/api/v1/users
的请求重写为
/api/users

location /api/v1/ {
   rewrite ^/api/v1/(.*)$ /api/$1 break;  # 将 /api/v1/ 的请求重写为 /api/
}

这样一来,后端就不用再关注
/api/v1/
的存在了,这种方法在网站迁移或者结构调整时非常有用。大家也不用去记忆改写的具体语法,随用随查就行。


看到这里,恭喜你,超过 60% 的程序员了。

三、Nginx 高级操作 - 暗劲

下面,我们要成为 Nginx 的暗劲高手。所谓暗劲,又分为 2 种境界。

  • 熟悉 Nginx 的各种特性和高级配置,能更快速地配置和管理 Nginx,为小圆满

  • 熟悉 Nginx 工具和模块生态,能够灵活运用 Nginx 进行架构设计、并巧妙地解决各种需求,为大圆满。

暗劲境界的高手,挑战大厂开发、架构师、高级系统管理员岗位,不成问题。

1、Nginx 高级配置

我们先挑战小圆满,Nginx 的配置项实在是太多了,这里我就挑选几个相对实用的来讲解。

1)日志记录

为了分析网站流量、用户行为和报错信息,我们可以开启 Nginx 日志功能。分为访问日志和错误日志。

访问日志会记录所有请求的信息,更全面,可以通过修改 access_log 指令调整日志存储路径:

http {
 log_format custom_format 'yupi $remote_addr - $remote_user [$time_local] "$request" '
                         '$status $body_bytes_sent "$http_referer" '
                         '"$http_user_agent" "$http_x_forwarded_for"';
 
 access_log /rap/access.log custom_format;  # 配置访问日志

 server {
   listen       80;
   server_name localhost;
 
   location / {
     root /tmp/nginx/html;  # 指定静态文件根目录
     index index.html;  # 默认首页
  }
}
}

而错误日志仅记录 Nginx 在处理请求时遇到的问题,错误又分为 8 个级别:

可以为不同的级别指定不同的日志输出路径:

access_log /rap/access.log custom_format;  # 配置访问日志
error_log /rap/error.log error;  # 配置错误日志

开启日志功能后,就能直接在文件中查看日志了。

2)访问控制

如果有恶意用户攻击我们的网站,怎么办?

莫慌,Nginx 提供了访问控制功能,可以使用
allow

deny
指令对 IP 访问进行限制,比如不让 127.0.0.1 这个 IP 访问:

server {
   listen 80;
   server_name localhost;

   location / {
       # 拒绝特定 IP 地址
       deny 127.0.0.1;
       # 除了写具体 ip 外,也可以写网段
       deny 192.168.1.0/24;
       # 允许所有其他 IP 地址
       allow all;
       proxy_pass http://localhost:8081;
   
  }
}

这样一来,攻击者就访问不了网站了!

3)限流

为了保护网站,我们还可以使用 Nginx 的限流功能。比如下面这段配置,通过定义请求限流区域并应用于根路径,限制每个 IP 地址在一分钟内最多只能发送 2 个请求。

# 定义限流区域,使用客户端的二进制 IP 地址作为唯一标识
# zone=one:10m 表示创建一个名为 "one" 的内存区域,大小为 10MB
# rate=2r/m 表示每个 IP 地址每分钟最多允许 2 个请求
limit_req_zone $binary_remote_addr zone=one:10m rate=2r/m;

server {
 listen 80;  # 监听 80 端口,接收 HTTP 请求
 server_name localhost;  # 设置服务器名称为 localhost

 location / {  # 配置根路径的请求处理
   # 应用限流配置,使用之前定义的 "one" 区域
   # burst=10 表示可以允许最多 10 个额外请求超出正常限速
   # nodelay 表示在突发请求情况下,这 10 个请求将立即被处理,不会被延迟
   limit_req zone=one burst=10 nodelay;

   # 将请求转发到本地的 8080 端口
   proxy_pass http://localhost:8080;  # 反向代理请求到后端服务
}
}

这样后端服务就不被流量激增影响,能够提高系统的稳定性。

4)虚拟主机

在企业开发中,我们为了节省成本,经常会在同一台服务器上部署多个网站项目,这时就需要使用 Nginx 的虚拟主机功能了。

每个网站通常就是一个虚拟主机,会有一个 server_name 名称对应访问网站的域名,比如我这里配置 2 台虚拟主机:

# 虚拟主机1
server {
   listen 80;                           # 监听 80 端口
   server_name localhost;               # 配置域名为 example.com

   root /tmp/nginx/html;            # 网站根目录
   index localhost.html;                # 默认首页
}

# 虚拟主机2
server {
   listen 80;                           # 监听 80 端口
   server_name 127.0.0.1;               # 配置域名为 another.com

   root /tmp/nginx/html;            # 网站根目录
   index 127.html;                      # 默认首页
}

配置虚拟主机后,Nginx 就能够根据请求的域名找到对应的网站配置,并处理请求。

5)其他

除了上面这些,还有很多企业开发中可能会用到的 Nginx 高级配置和技巧。

比如可以:

  • 通过后端响应缓存配置,让 Nginx 直接从缓存中读取数据来响应请求,这样能够显著提升性能、减少服务器压力。

  • 通过正向代理的设置,Nginx 可以作为 “跳板机”,帮客户端发起请求,从而访问原本无法直接访问的资源。

  • 通过自定义错误页面,能够给用户提供更友好的错误提示信息。

此外,Nginx 支持 WebSocket、HTTPS 和 HTTP/2 等多种协议,还可以配置 Gzip 压缩来减少传输的数据量,进一步优化性能。

最后,Nginx 自身也支持一系列性能调优的配置,比如工作进程与连接数配置,可以从容应对高并发和大流量场景。

worker_processes auto; # 自动检测 CPU 核心数,设置工作进程数

events {
   worker_connections 2048; # 每个工作进程的最大连接数
}

2、Nginx 工具和模块生态

想成为 Nginx 大圆满高手,就要懂得利用工具和生态,比如可视化工具、模块和开源项目。

首先,Nginx 的配置和运维对初学者来说可能比较复杂,这时可以利用 Nginx 官方推出的 Nginx Amplify、轻量级的 Nginx-UI 或者宝塔 Linux 服务器管理面板等可视化工具,通过图形界面来更直观地查看配置、分析流量和性能指标,从而提高操作和运维效率。

Nginx UI 面板

其次,Nginx 的功能并不是一成不变的,我们可以通过各种各样的模块来扩展它的能力,比如我们常用于健康检查的 nginx_upstream_check_module 模块、实现 JavaScript 语言扩展的 njs 模块。

但手动安装模块的过程是比较繁琐的,需要下载源码并进行编译。

这种情况下,我们就可以选择 OpenResty 这样一个基于 Nginx 的高性能 Web 平台,它集成了大量模块、依赖项和 Lua 脚本库,能够让你直接在 Nginx 里开发复杂的业务逻辑,充分利用 Nginx 的非阻塞 I/O 模型来提升应用的性能,适合超高并发的场景。

比如下图是网上的一个基于 OpenResty 实现的灰度发布架构:

四、Nginx 原理 - 化劲

想要突破为化劲强者,你需要去理解 Nginx 的核心原理,甚至是去钻研那晦涩难懂的 C 语言源码。

当然,为了应对面试,现在很多程序员迫不得已朝着化劲强者进发。

原理的学习就不是几分钟的视频能搞定的了,但是我可以帮大家划划重点。

  • 负载均衡机制

  • 事件驱动模型

  • 请求处理流程

  • 多进程架构

  • 进程间通信机制

  • 限流机制

  • 缓存机制

  • 压缩机制

  • 资源复用

能搞懂这些,并且融会贯通,你就能够更自如地优化 Nginx 的性能和可用性等等,也就超过 99% 的程序员了。

当然,编程是学不完的,真正的 Nginx 绝世高手,可以给 Nginx 贡献代码,甚至是自立门户、手写 Nginx 的竞品。

我相信看到这里的小伙伴中肯定会出现绝世高手~

结尾

最后,我把这份 Nginx 学习路线文字版、以及常问的面试题都放到了自己的小博客,还有更多学习路线也可免费获取。

希望对大家有帮助,学会的话也还请给本文一个点赞支持哦~

更多编程学习资源


title: Nuxt.js 应用中的 components:extend 事件钩子详解
date: 2024/11/1
updated: 2024/11/1
author:
cmdragon

excerpt:
components:extend 是 Nuxt.js 中的一个生命周期钩子,允许开发者扩展新的组件到项目中。通过这个钩子,开发者可以动态地添加额外的组件,从而增强项目的功能和灵活性。

categories:

  • 前端开发

tags:

  • Nuxt
  • 组件
  • 钩子
  • 动态
  • 扩展
  • 生命周期
  • Vue


image
image

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

components:extend
钩子详解

components:extend
是 Nuxt.js 中的一个生命周期钩子,允许开发者扩展新的组件到项目中。通过这个钩子,开发者可以动态地添加额外的组件,从而增强项目的功能和灵活性。


目录

  1. 概述
  2. components:extend 钩子的详细说明
  3. 具体使用示例
  4. 应用场景
  5. 注意事项
  6. 关键要点
  7. 总结


1. 概述

components:extend
钩子使开发者能够在 Nuxt.js 项目中动态地添加新的组件。这种灵活性使得项目能够根据需求进行扩展和修改,适应不同的功能需求。

2. components:extend 钩子的详细说明

2.1 钩子的定义与作用

  • 定义
    :
    components:extend
    是 Nuxt.js 的一个钩子,用于扩展和添加新的组件至项目中。
  • 作用
    : 使开发者可以在项目中动态地添加新增的组件,增加应用的功能性和灵活性。

2.2 调用时机

  • 执行环境
    : 在组件解析时触发,适合进行组件的扩展和添加。
  • 挂载时机
    : 该钩子在应用启动前被调用,确保新的组件设置在应用运行之前生效。

2.3 参数说明

  • components
    : 该参数包含当前项目中的组件配置信息,开发者能够对其进行添加、修改或删除操作。

3. 具体使用示例

3.1 扩展组件示例

// plugins/componentsExtend.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('components:extend', (components) => {
    // 扩展新的组件
    components.push({
      name: 'CustomComponent',
      path: './components/CustomComponent.vue'
    });

    console.log('Extended components:', components);
  });
});

在这个示例中,我们使用
components:extend
钩子向现有的组件列表中添加了一个新的组件
CustomComponent
。这个组件位于
./components/CustomComponent.vue
文件中,可以在项目中随意使用。

4. 应用场景

  1. 功能扩展
    : 在需要时动态地添加新组件,以加强项目的功能。
  2. 共享组件
    : 针对多个模块或页面创建共享的组件,从而提高代码重用率。
  3. 模块化设计
    : 在构建大型应用时,根据需求动态创建并扩展组件。

5. 注意事项

  • 组件命名
    : 确保添加的组件不会与已有组件冲突,适当使用命名空间。
  • 性能考虑
    : 动态添加多个组件可能会影响性能,应合理组织组件结构。
  • 团队协作
    : 与团队成员沟通,确保对新增组件的了解和使用。

6. 关键要点

  • components:extend
    钩子为 Nuxt.js 开发者提供了一种动态扩展组件的方式。
  • 通过合理利用此钩子,可以提高项目的灵活性和可维护性。

7. 总结

components:extend
钩子使得 Nuxt.js 项目可以灵活地添加新的组件,提升了应用的扩展性。通过有效地管理组件,开发者能够创建更加模块化和可维护的项目结构。

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
,阅读完整的文章:
Nuxt.js 应用中的 components:extend 事件钩子详解 | cmdragon's Blog

往期文章归档:

1.简介

随着Unity6的发布,URP17也已经可以上手使用,相对旧的版本改动较大的是加入了

RenderGraph、STP、Foveated rendering、GPU Resident Drawer等功能,部分功能只需要开关参数即可使用,

而GRD更像是Gpu driven管线下的SRP Batches升级,RenderGraph相较于HDRP之前使用的版本换了一套API。

最大的不同是,使用URP17编写Feature时,必须依赖于RenderGraph进行编写,接下来就来介绍一下。

1.1 相关Demo

目前URP17比较容易找到的学习Demo如下:

2.RenderGraph

打开任意URP的示例场景查看,RenderGraphView上各图标含义如下:

  1. 说明这是一个外部置入的RenderTexture
  2. 红色方块说明存在写入操作
  3. 绿色方块指存在读取操作(红绿方块说明读写操作)
  4. 该图标说明标记了全局RenderTexture

而顶部表明当前渲染一帧的各个Pass,左侧是各类RT。

URP17同时保留了旧的Feature逻辑与RenderGraph逻辑(打开任意pass文件为例):

public classDistortTunnelPass_Tunnel : ScriptableRenderPass
{
classPassData
{
publicRenderer tunnelObject;publicMaterial tunnelMaterial;
}
#pragma warning disable 618, 672 //Type or member is obsolete, Member overrides obsolete member //Unity calls the Configure method in the Compatibility mode (non-RenderGraph path) public override voidConfigure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescripor)
{
}
//Unity calls the Execute method in the Compatibility mode public override void Execute(ScriptableRenderContext context, refRenderingData renderingData)
{
}
#pragma warning restore 618, 672 //Unity calls the RecordRenderGraph method to add and configure one or more render passes in the render graph system. public override voidRecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
}
}

参考时忽略掉Configure和Execute的逻辑,执行逻辑关注RecordRenderGraph函数。

2.1 操作方式的改变

在RenderGraph中,之前的RTHandle由于不在该系统中托管,进入RenderGraph的材质都需要调用API进行转换,

转换为RendeGraph的RT后,无需考虑释放操作:

RenderTextureDescriptor textureProperties = new RenderTextureDescriptor(Screen.width, Screen.height, RenderTextureFormat.Default, 0);
TextureHandle textureHandle
= UniversalRenderer.CreateRenderGraphTexture(renderGraph, textureProperties, "My texture", false);

相关文档:

https://docs.unity3d.com/Manual/urp/render-graph-create-a-texture.html

此外RenderGraph对于空调用的pass,也会剔除进行优化,使用者需要手动标记以防止被剔除。

2.1 RecordRenderGraph

在该函数内可组织渲染逻辑,pass相关的逻辑需放在对应的代码块中,例如:

using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out_))
{
builder.UseTexture(rt1);
builder.SetRenderAttachment(resourceData.activeColorTexture,
0);

builder.SetRenderFunc
<PassData>((data, context) =>{
MaterialPropertyBlock materialPropertyBlock
= newMaterialPropertyBlock();
materialPropertyBlock.SetTexture(
"_BlitTexture", rt1);
materialPropertyBlock.SetVector(
"_BlitScaleBias", new Vector4(1, 1, 0, 0));

context.cmd.DrawProcedural(Matrix4x4.identity, material,
0, MeshTopology.Triangles, 3, 1, materialPropertyBlock);
});
}

URP提供了多种RenderPass,例如处理光栅化相关逻辑使用RasterRenderPass组织相关逻辑。

在RenderPass的代码块中可使用builder对象配置RenderTarget、标记材质的读写等

而具体的pass绘制逻辑则在SetRenderFunc代码块中。

RecordRenderGraph内可以调用多次AddRenderPass,但URP并没有整理旧API的代码和相关工具类,

以至于容易使用旧的API导致报错,这点需要注意。

3.编写Feature

3.1 Blit与SetTarget

从前有句俗话“切RT的性能消耗相当于半个pass”,Unity SRP在几个版本的升级都在逐渐强调不切RenderTarget直接绘制,

如Cockpit Demo的屏幕空间描边。

3.2 屏幕模糊Demo

下面通过屏幕模糊Demo案例,演示URP17下pass的编写。

通过外部EnqueuePass的方式,在场景中通过控制器脚本添加该Pass,

MyBlurSceneController.cs:

usingUnityEngine;usingUnityEngine.Rendering.Universal;usingUnityEngine.Rendering;public classMyBlurSceneController : MonoBehaviour
{
publicMaterial material;
[Range(
2, 15)] public int blurPasses = 3;
[Range(
0, 4)] public int downSample = 0;
[Range(
0.0f, 10f)] public float offset = 0.2f;public RenderPassEvent injectionPoint =RenderPassEvent.BeforeRenderingPostProcessing;public int injectionPointOffset = 0;public ScriptableRenderPassInput inputRequirements =ScriptableRenderPassInput.Color;public CameraType cameraType =CameraType.Game;privateMyBlurPass mMyBlurPass;private voidOnEnable()
{
SetupPass();

RenderPipelineManager.beginCameraRendering
+=OnBeginCamera;
}
private voidOnDisable()
{
RenderPipelineManager.beginCameraRendering
-=OnBeginCamera;
}
public virtual voidSetupPass()
{
mMyBlurPass
= newMyBlurPass();

mMyBlurPass.renderPassEvent
= injectionPoint +injectionPointOffset;
mMyBlurPass.material
=material;

mMyBlurPass.ConfigureInput(inputRequirements);
}
public virtual voidOnBeginCamera(ScriptableRenderContext ctx, Camera cam)
{
if (mMyBlurPass == null || material == null)return;if ((cam.cameraType & cameraType) == 0) return;

mMyBlurPass.blurPasses
=blurPasses;
mMyBlurPass.downSample
=downSample;
mMyBlurPass.offset
=offset;

cam.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(mMyBlurPass);
}
}

MyBlurPass.cs:

usingUnityEngine;usingUnityEngine.Rendering.RenderGraphModule;usingUnityEngine.Rendering;usingUnityEngine.Rendering.Universal;usingUnityEngine.Rendering.RenderGraphModule.Util;public classMyBlurPass : ScriptableRenderPass
{
public classPassData
{
publicTextureHandle tempRt1;publicTextureHandle tempRt2;
}
publicMaterial material;
[Range(
2, 15)] public int blurPasses = 3;
[Range(
1, 4)] public int downSample = 1;
[Range(
0.0f, 10f)] public float offset = 0.2f;public override voidRecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
var resourceData = frameData.Get<UniversalResourceData>();var passData = newPassData();var w = Screen.width >>downSample;var h = Screen.height >>downSample;

RenderTextureDescriptor textureProperties
= new RenderTextureDescriptor(w, h, RenderTextureFormat.Default, 0);
passData.tempRt1
= UniversalRenderer.CreateRenderGraphTexture(renderGraph, textureProperties, "MyBlurPassTempRt1", false);

textureProperties
= new RenderTextureDescriptor(w, h, RenderTextureFormat.Default, 0);
passData.tempRt2
= UniversalRenderer.CreateRenderGraphTexture(renderGraph, textureProperties, "MyBlurPassTempRt2", false);var rt1 =passData.tempRt1;var rt2 =passData.tempRt2;//将屏幕RT Blit到rt1上 var para = new RenderGraphUtils.BlitMaterialParameters(resourceData.activeColorTexture, rt1, material, 0);
renderGraph.AddBlitPass(para,
"MyBlurPassBlitFirst");

material.SetFloat(
"_SampleOffset", offset);//模糊迭代 for (int i = 0; i < blurPasses - 1; ++i)
{
para
= new RenderGraphUtils.BlitMaterialParameters(rt1, rt2, material, 0);
renderGraph.AddBlitPass(para, $
"MyBlurPassBlit_{i}");var tmp =rt1;
rt1
=rt2;
rt2
=tmp;
}
//通过直接绘制的方式,将模糊RT绘制到屏幕上 using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out_))
{
builder.UseTexture(rt1);
builder.SetRenderAttachment(resourceData.activeColorTexture,
0);

builder.SetRenderFunc
<PassData>((data, context) =>{
MaterialPropertyBlock materialPropertyBlock
= newMaterialPropertyBlock();
materialPropertyBlock.SetTexture(
"_BlitTexture", rt1);
materialPropertyBlock.SetVector(
"_BlitScaleBias", new Vector4(1, 1, 0, 0));

context.cmd.DrawProcedural(Matrix4x4.identity, material,
0, MeshTopology.Triangles, 3, 1, materialPropertyBlock);
});
}
}
}

接着在ShaderGraph中连出模糊的逻辑,注意Blit对应的参数_BlitTexture、_BlitScaleBias:

最后在场景中挂载控制器以及材质球,即可使用该模糊Pass。

一、基本概念

Java NIO 是 Java 1.4 引入的,用于处理高速、高并发的 I/O 操作。与传统的阻塞 I/O 不同,NIO 支持非阻塞 I/O 和选择器,可以更高效地管理多个通道。

二、核心组件

  1. 通道(Channel)
    • Channel
      是 NIO 中用于读取和写入数据的主要接口,提供双向数据传输的能力。
    • 常见的通道实现:
      • FileChannel
        :用于文件的读写操作。
      • SocketChannel
        :用于 TCP 网络通信。
      • ServerSocketChannel
        :用于监听 TCP 连接的服务器端通道。
      • DatagramChannel
        :用于 UDP 网络通信。
  2. 缓冲区(Buffer)
    • Buffer
      是 NIO 中用于存储数据的容器。与传统的流不同,NIO 通过缓冲区进行数据的读写。
    • 常见的缓冲区类型:
      • ByteBuffer
        :处理字节数据。
      • CharBuffer
        :处理字符数据。
      • IntBuffer

        LongBuffer
        等:处理整型和长整型数据。
    • 缓冲区有三个重要的属性:
      • position
        :当前缓冲区的读写位置。
      • limit
        :可以读取或写入的最大数据量。
      • capacity
        :缓冲区的总容量。
  3. 选择器(Selector)
    • Selector
      是 NIO 的核心组件之一,允许单个线程监控多个通道的事件。
    • 通过选择器,可以处理多个连接而不需要为每个连接都创建一个线程。
    • Selector 的工作流程:
      • 注册通道(Channel)到选择器。
      • 选择感兴趣的通道(如可读、可写、连接等)。
      • 处理就绪的通道。

三、底层实现

  1. 文件描述符

    NIO 底层仍然依赖操作系统的文件描述符。每个通道对应一个文件描述符,用于直接与操作系统进行交互。

  2. 事件驱动

    NIO 使用事件驱动的机制。选择器会调用操作系统的底层 API(如 epoll、kqueue)来获取就绪事件。这种机制允许线程在等待事件时处于睡眠状态,从而减少 CPU 资源的消耗。

四、设计原理

  1. 非阻塞 IO

    NIO 允许通道在没有可用数据时不阻塞线程。线程可以继续执行其他操作,适合处理高并发请求。

  2. 选择性处理

    使用选择器,可以选择性地处理就绪通道,避免了为每个连接创建一个线程的开销。

  3. 适应性强

    NIO 的设计使得它可以处理各种数据源(如文件、网络等),提高了灵活性。

五、底层原理

  1. 内存管理

    NIO 的缓冲区(Buffer)底层使用
    java.nio.HeapByteBuffer

    java.nio.DirectByteBuffer
    ,后者直接在 JVM 之外分配内存,减少了与 JVM 堆内存的交互开销,提升了 I/O 性能,特别是在大数据量传输时。

  2. 内存映射文件(Memory-Mapped File)

    NIO 的
    FileChannel
    支持内存映射文件,允许将文件映射到内存。这种方式使得文件内容可以像数组一样直接操作,大幅提升了文件读取和写入的速度,特别适用于大文件处理和高性能数据库实现。

  3. 选择器的实现

    选择器的实现通常基于操作系统提供的高效 I/O 多路复用机制,如 Linux 的
    epoll
    或 Windows 的
    IOCP
    。这些机制使得 NIO 能够在处理大量并发连接时表现优异。了解这些底层实现的机制,能够帮助开发者在不同操作系统上优化性能。

六、使用场景

  • 高性能 Web 服务器

    NIO 适合构建高性能的 Web 服务器,如 Netty 框架,利用其事件驱动和异步非阻塞的特性,可以处理数万并发连接,而不需要为每个连接创建一个线程。

  • 实时数据处理

    在需要实时处理大量数据的应用(如金融交易系统、在线游戏等),NIO 提供的低延迟和高吞吐量使其成为理想选择。

  • 跨平台的网络通信

    NIO 的通道和选择器机制提供了跨平台的网络通信能力,开发者可以轻松构建支持多种操作系统的网络应用。

  • 高并发网络应用

    NIO 适用于需要处理大量并发连接的应用,例如聊天服务器、HTTP 服务器和在线游戏等。

  • 异步文件处理

    使用
    AsynchronousFileChannel
    进行异步文件读写操作,适合需要高性能的文件处理场景。

七、性能特点

  1. 降低上下文切换

    NIO 的非阻塞特性降低了线程切换的开销,特别是在高并发情况下,提高了应用的吞吐量。

  2. 内存映射文件

    NIO 支持内存映射文件,可以将文件直接映射到内存,这种方式可以提高对大文件的访问速度。

  3. 减少资源占用

    由于使用选择器管理多个通道,NIO 可以减少对系统资源(如线程和内存)的占用,提高整体性能。

八、示例代码

以下是一个简单的 NIO 服务器示例,使用选择器处理客户端连接:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.SelectionKey;

public class NioServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            selector.select(); // 阻塞,直到有事件发生
            for (SelectionKey key : selector.selectedKeys()) {
                if (key.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    int bytesRead = socketChannel.read(buffer);
                    if (bytesRead == -1) {
                        socketChannel.close();
                    } else {
                        buffer.flip();
                        // 处理数据...
                        socketChannel.write(buffer);
                    }
                }
            }
            selector.selectedKeys().clear(); // 清除已处理的事件
        }
    }
}