2025.1 最近的一些事情,解决的一些问题
简单说说 reactCompiler
Next.js 15,React.js 19 更新挺久了,随之一并带来的是 reactCompiler 等待了不知多久的 beta 版本,简单试了下 Next.js 集成,体验还不错,具体步骤就看 官方文档
简单来说装一个 babel 插件
SHELL1pnpm install babel-plugin-react-compiler
然后开 experimental 功能就行了,在 next.config.ts
TYPESCRIPT1import type { NextConfig } from 'next' 2 3const nextConfig: NextConfig = { 4 experimental: { 5 reactCompiler: true, 6 }, 7} 8 9export default nextConfig
开了之后,它会自动优化我们的代码,比如 useMemo
useCallback
这种缓存的使用,会尽量避免多次频繁重新渲染,组件频繁清空数据整个渲染和挂载,...emm 怎么说,感觉 React 也开始借鉴 Vue 了,变成自动档了,可以自己优化了
显示 ip 归属地的实现
最近用 maxmind
的 geoip2
库补全我 ipv6 归属地的显示,正好说一下这个的完整实现。
XML1<!--这个仅支持ipv4--> 2<dependency> 3 <groupId>org.lionsoul</groupId> 4 <artifactId>ip2region</artifactId> 5 <version>2.7.0</version> 6</dependency> 7 8<!--支持ipv4 ipv6,但是感觉效果不是很好--> 9<dependency> 10 <groupId>com.maxmind.geoip2</groupId> 11 <artifactId>geoip2</artifactId> 12 <version>4.2.1</version> 13</dependency>
首先到其开源仓库下载数据集:ip2region
将 ip2region.xdb
放好在 src/main/resources
同样如法炮制,下载 maxmind 的数据集,由于其商业版本收费,我们只能选择:GeoLite.mmdb
拿到 GeoLite2-City.mmdb
和 GeoLite2-ASN.mmdb
我们可以写一个 IPLocationUtil
JAVA1package com.grtsinry43.grtblog.util; 2 3import com.maxmind.geoip2.DatabaseReader; 4import com.maxmind.geoip2.model.AsnResponse; 5import com.maxmind.geoip2.model.CityResponse; 6import jakarta.servlet.http.HttpServletRequest; 7import org.apache.commons.lang3.StringUtils; 8import org.lionsoul.ip2region.xdb.Searcher; 9import org.slf4j.Logger; 10import org.slf4j.LoggerFactory; 11import org.springframework.util.FileCopyUtils; 12 13import java.io.File; 14import java.io.IOException; 15import java.io.InputStream; 16import java.net.InetAddress; 17import java.net.UnknownHostException; 18 19/** 20 * Utility class for getting address by IP using GeoIP2 library. 21 * 22 * @date 2024/11/2 23:58 23 * @description 热爱可抵岁月漫长 24 */ 25public class IPLocationUtil { 26 27 private static final Logger logger = LoggerFactory.getLogger(IPLocationUtil.class); 28 private static final String LOCAL_IP = "127.0.0.1"; 29 private static final String LOCAL_IPV6 = "::1"; 30 31 // 这里根据需要初始化数据库搜索实例 32 private static final Searcher searcher; 33 private static DatabaseReader dbReader; 34 private static DatabaseReader asnReader; 35 36 static { 37 try { 38 InputStream cityDbStream = IPLocationUtil.class.getResourceAsStream("/GeoLite2-City.mmdb"); 39 InputStream asnDbStream = IPLocationUtil.class.getResourceAsStream("/GeoLite2-ASN.mmdb"); 40 if (cityDbStream == null || asnDbStream == null) { 41 throw new IOException("GeoIP2 database files not found in classpath"); 42 } 43 dbReader = new DatabaseReader.Builder(cityDbStream).build(); 44 asnReader = new DatabaseReader.Builder(asnDbStream).build(); 45 InputStream ris = IPLocationUtil.class.getResourceAsStream("/ip2region.xdb"); 46 byte[] dbBinStr = FileCopyUtils.copyToByteArray(ris); 47 searcher = Searcher.newWithBuffer(dbBinStr); 48 logger.debug("ip2region 数据库加载成功"); 49 logger.debug("GeoIP2 数据库加载成功"); 50 } catch (IOException e) { 51 logger.error("无法创建数据库读取器: {}", e.getMessage()); 52 throw new RuntimeException("无法加载 GeoIP2 数据库", e); 53 } 54 } 55 56 /** 57 * 获取用户真实 IP 地址 58 * 59 * @param request HttpServletRequest 60 * @return 用户真实 IP 地址 61 */ 62 public static String getIp(HttpServletRequest request) { 63 String ipAddress = request.getHeader("X-Original-Forwarded-For"); 64 65 if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) { 66 ipAddress = request.getHeader("X-Forwarded-For"); 67 } 68 if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) { 69 ipAddress = request.getRemoteAddr(); 70 if (LOCAL_IP.equals(ipAddress) || LOCAL_IPV6.equals(ipAddress)) { 71 try { 72 InetAddress inet = InetAddress.getLocalHost(); 73 ipAddress = inet.getHostAddress(); 74 } catch (UnknownHostException e) { 75 logger.error("获取IP地址失败: {}", e.getMessage()); 76 } 77 } 78 } 79 80 if (ipAddress != null && ipAddress.length() > 15) { 81 int commaIndex = ipAddress.indexOf(","); 82 if (commaIndex > 0) { 83 ipAddress = ipAddress.substring(0, commaIndex); 84 } 85 } 86 return LOCAL_IPV6.equals(ipAddress) ? LOCAL_IP : ipAddress; 87 } 88 89 /** 90 * 根据 IP 获取 ip2region 信息 91 * 92 * @param ip IP 地址 93 * @return 解析后的信息 94 */ 95 public static String getIp2region(String ip) { 96 if (searcher == null) { 97 logger.error("搜索器未初始化"); 98 return null; 99 } 100 101 try { 102 InetAddress ipAddress = InetAddress.getByName(ip); 103 // 判断是 ipv4 还是 ipv6 104 if (ipAddress instanceof java.net.Inet4Address) { 105 String ipInfo = searcher.search(ip); 106 if (!StringUtils.isEmpty(ipInfo)) { 107 return ipInfo.replace("|0", "").replace("0|", "").replace("|", ""); 108 } 109 } else if (ipAddress instanceof java.net.Inet6Address) { 110 CityResponse response = dbReader.city(ipAddress); 111 AsnResponse asnResponse = asnReader.asn(ipAddress); 112 String isp = asnResponse.getAutonomousSystemOrganization(); 113 if (StringUtils.isBlank(isp)) { 114 isp = "未知运营商"; 115 } else if (isp.startsWith("China Telecom")) { 116 isp = "电信"; 117 } else if (isp.startsWith("China Unicom")) { 118 isp = "联通"; 119 } else if (isp.startsWith("China Mobile")) { 120 isp = "移动"; 121 } else if (isp.startsWith("CERNET2 IX")) { 122 isp = "教育网"; 123 } 124 return response.getCountry().getNames().get("zh-CN") + response.getMostSpecificSubdivision().getNames().get("zh-CN") + 125 response.getCity().getNames().get("zh-CN") + isp; 126 } 127 } catch (Exception e) { 128 logger.error("解析IP信息失败: {}", e.getMessage()); 129 } 130 return null; 131 } 132}
还有一件事情哦,如果你使用 nginx 或 docker 部署,很可能会出现拿不到 ip 的情况,比如 nginx 中,我们要转发好访客的 ip
CONF1location /api/v1 { 2 rewrite ^/api/v1/(.*)$ /$1 break; 3 proxy_pass http://127.0.0.1:8080; 4 proxy_set_header Host $http_host; 5 proxy_set_header X-Real-IP $remote_addr; 6 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 7 proxy_set_header X-Forwarded-Proto $scheme; 8 }
全站更新通知的实现
这个是我最近加的一点点小功能,希望给站点加一点活力吧,毕竟我这都没几个人看 (让我难受的很啊)
双向通知当然是 websocket 的能力,而我们加到 socket 连接之后,如何告诉其他组件呢?
对了!可以使用 MessageChannel
,可以看下 MDN 的文档
这是浏览器的一个原生能力,我们在两个端口 port1
port2
之间可以实现消息的传递,通过在一个端口发送消息,一个端口监听消息,我们就可以实现很多功能啦(有点像 electron 的 ipc,node 的 emitter)
我们就先创建一个公共实例:
TYPESCRIPT1// frontend/src/utils/channel.ts 2const channel = new MessageChannel(); 3export default channel;
在socket收到事件时候:
TYPESCRIPT1// 用于发送更新通知 2 newSocket.on("updateNotification", (content) => { 3 console.log(content); 4 channel.port2.postMessage({ 5 content: content, 6 publishAt: new Date().toISOString(), 7 }); 8 });
另一面便可以处理啦:
TYPESCRIPT1useEffect(() => { 2 // 这里是为了消息定时关闭 3 let timer: NodeJS.Timeout; 4 channel.port1.onmessage = (event) => { 5 const res = event.data; 6 if (res) { 7 setNotification(res); 8 setShow(true); 9 timer = setTimeout(() => setShow(false), 10000); 10 } 11 }; 12 13 return () => clearTimeout(timer); 14 }, []);