Grtsinry43 的前端札记
NO. 0121 手记 2025.01.21

2025.1 最近的一些事情,解决的一些问题

浏览 199 喜欢 0 评论 1

简单说说 reactCompiler

Next.js 15,React.js 19 更新挺久了,随之一并带来的是 reactCompiler 等待了不知多久的 beta 版本,简单试了下 Next.js 集成,体验还不错,具体步骤就看 官方文档

简单来说装一个 babel 插件

shell
pnpm install babel-plugin-react-compiler

然后开 experimental 功能就行了,在 next.config.ts

typescript
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  experimental: {
    reactCompiler: true,
  },
}
 
export default nextConfig

开了之后,它会自动优化我们的代码,比如 useMemo useCallback 这种缓存的使用,会尽量避免多次频繁重新渲染,组件频繁清空数据整个渲染和挂载,…emm 怎么说,感觉 React 也开始借鉴 Vue 了,变成自动档了,可以自己优化了

显示 ip 归属地的实现

最近用 maxmindgeoip2 库补全我 ipv6 归属地的显示,正好说一下这个的完整实现。

xml
<!--这个仅支持ipv4-->
<dependency>
    <groupId>org.lionsoul</groupId>
    <artifactId>ip2region</artifactId>
     <version>2.7.0</version>
</dependency>

<!--支持ipv4 ipv6,但是感觉效果不是很好-->
<dependency>
    <groupId>com.maxmind.geoip2</groupId>
    <artifactId>geoip2</artifactId>
    <version>4.2.1</version>
</dependency>

首先到其开源仓库下载数据集:ip2region

ip2region.xdb 放好在 src/main/resources

同样如法炮制,下载 maxmind 的数据集,由于其商业版本收费,我们只能选择:GeoLite.mmdb

拿到 GeoLite2-City.mmdbGeoLite2-ASN.mmdb

我们可以写一个 IPLocationUtil

java
package com.grtsinry43.grtblog.util;

import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.model.AsnResponse;
import com.maxmind.geoip2.model.CityResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.lionsoul.ip2region.xdb.Searcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileCopyUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * Utility class for getting address by IP using GeoIP2 library.
 *
 * @date 2024/11/2 23:58
 * @description 热爱可抵岁月漫长
 */
public class IPLocationUtil {

    private static final Logger logger = LoggerFactory.getLogger(IPLocationUtil.class);
    private static final String LOCAL_IP = "127.0.0.1";
    private static final String LOCAL_IPV6 = "::1";

    // 这里根据需要初始化数据库搜索实例
    private static final Searcher searcher;
    private static DatabaseReader dbReader;
    private static DatabaseReader asnReader;

    static {
        try {
            InputStream cityDbStream = IPLocationUtil.class.getResourceAsStream("/GeoLite2-City.mmdb");
            InputStream asnDbStream = IPLocationUtil.class.getResourceAsStream("/GeoLite2-ASN.mmdb");
            if (cityDbStream == null || asnDbStream == null) {
                throw new IOException("GeoIP2 database files not found in classpath");
            }
            dbReader = new DatabaseReader.Builder(cityDbStream).build();
            asnReader = new DatabaseReader.Builder(asnDbStream).build();
            InputStream ris = IPLocationUtil.class.getResourceAsStream("/ip2region.xdb");
            byte[] dbBinStr = FileCopyUtils.copyToByteArray(ris);
            searcher = Searcher.newWithBuffer(dbBinStr);
            logger.debug("ip2region 数据库加载成功");
            logger.debug("GeoIP2 数据库加载成功");
        } catch (IOException e) {
            logger.error("无法创建数据库读取器: {}", e.getMessage());
            throw new RuntimeException("无法加载 GeoIP2 数据库", e);
        }
    }

    /**
     * 获取用户真实 IP 地址
     *
     * @param request HttpServletRequest
     * @return 用户真实 IP 地址
     */
    public static String getIp(HttpServletRequest request) {
        String ipAddress = request.getHeader("X-Original-Forwarded-For");

        if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("X-Forwarded-For");
        }
        if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if (LOCAL_IP.equals(ipAddress) || LOCAL_IPV6.equals(ipAddress)) {
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    logger.error("获取IP地址失败: {}", e.getMessage());
                }
            }
        }

        if (ipAddress != null && ipAddress.length() > 15) {
            int commaIndex = ipAddress.indexOf(",");
            if (commaIndex > 0) {
                ipAddress = ipAddress.substring(0, commaIndex);
            }
        }
        return LOCAL_IPV6.equals(ipAddress) ? LOCAL_IP : ipAddress;
    }

    /**
     * 根据 IP 获取 ip2region 信息
     *
     * @param ip IP 地址
     * @return 解析后的信息
     */
    public static String getIp2region(String ip) {
        if (searcher == null) {
            logger.error("搜索器未初始化");
            return null;
        }

        try {
            InetAddress ipAddress = InetAddress.getByName(ip);
            // 判断是 ipv4 还是 ipv6
            if (ipAddress instanceof java.net.Inet4Address) {
                String ipInfo = searcher.search(ip);
                if (!StringUtils.isEmpty(ipInfo)) {
                    return ipInfo.replace("|0", "").replace("0|", "").replace("|", "");
                }
            } else if (ipAddress instanceof java.net.Inet6Address) {
                CityResponse response = dbReader.city(ipAddress);
                AsnResponse asnResponse = asnReader.asn(ipAddress);
                String isp = asnResponse.getAutonomousSystemOrganization();
                if (StringUtils.isBlank(isp)) {
                    isp = "未知运营商";
                } else if (isp.startsWith("China Telecom")) {
                    isp = "电信";
                } else if (isp.startsWith("China Unicom")) {
                    isp = "联通";
                } else if (isp.startsWith("China Mobile")) {
                    isp = "移动";
                } else if (isp.startsWith("CERNET2 IX")) {
                    isp = "教育网";
                }
                return response.getCountry().getNames().get("zh-CN") + response.getMostSpecificSubdivision().getNames().get("zh-CN") +
                        response.getCity().getNames().get("zh-CN") + isp;
            }
        } catch (Exception e) {
            logger.error("解析IP信息失败: {}", e.getMessage());
        }
        return null;
    }
}

还有一件事情哦,如果你使用 nginx 或 docker 部署,很可能会出现拿不到 ip 的情况,比如 nginx 中,我们要转发好访客的 ip

conf
location /api/v1 {
        rewrite ^/api/v1/(.*)$ /$1 break;
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

评论效果

全站更新通知的实现

这个是我最近加的一点点小功能,希望给站点加一点活力吧,毕竟我这都没几个人看 (让我难受的很啊)

双向通知当然是 websocket 的能力,而我们加到 socket 连接之后,如何告诉其他组件呢?

对了!可以使用 MessageChannel,可以看下 MDN 的文档

这是浏览器的一个原生能力,我们在两个端口 port1 port2 之间可以实现消息的传递,通过在一个端口发送消息,一个端口监听消息,我们就可以实现很多功能啦(有点像 electron 的 ipc,node 的 emitter)

我们就先创建一个公共实例:

typescript
// frontend/src/utils/channel.ts
const channel = new MessageChannel();
export default channel;

在socket收到事件时候:

typescript
// 用于发送更新通知
        newSocket.on("updateNotification", (content) => {
            console.log(content);
            channel.port2.postMessage({
                content: content,
                publishAt: new Date().toISOString(),
            });
        });

另一面便可以处理啦:

typescript
useEffect(() => {
    // 这里是为了消息定时关闭
        let timer: NodeJS.Timeout;
        channel.port1.onmessage = (event) => {
            const res = event.data;
            if (res) {
                setNotification(res);
                setShow(true);
                timer = setTimeout(() => setShow(false), 10000);
            }
        };

        return () => clearTimeout(timer);
    }, []);

通知效果

喜欢 0
手记
2025.01.21
Loading comments...