使用Java自己简单搭建内网穿透
作者:cloudy491思路内网穿透是一种网络技术,适用于需要远程访问本地部署服务的场景,比如你在家里搭建了一个网站或者想远程访问家里的电脑。由于本地部署的设备使用私有IP地址,无法直接被外部访问,因此需要通过公网IP实现访问。通常可以通过购买云服务器获取一个公网IP来实现这一目的。实际上,内网穿透的原理是将位于公司或其他工作地点的私有IP数据发送到云服务器(公网IP),再从云服务器发送到家里的设备(私有IP)。从私有IP到公网IP的连接是相对简单的,但是从公网IP到私有IP就比较麻烦,因为公网IP无法直接找到私有IP。为了解决这个问题,我们可以让私有IP主动连接公网IP。这样,一旦私有IP连接到了公网IP,公网IP就知道了私有IP的存在,它们之间建立了连接关系。当公网IP收到访问请求时,就会通知私有IP有访问请求,并要求私有IP连接到公网IP。这样一来,公网IP就建立了两个连接,一个是用于访问的连接,另一个是与私有IP之间的连接。最后,通过这两个连接之间的数据交换,实现了远程访问本地部署服务的目的。代码操作打开IDEA创建一个mave项目,删除掉src,创建两个模块client和service,一个是在本地的运行,一个是在云服务器上运行的,这边socket(tcp)连接,我使用的是AIO,AIO的函数回调看起来好复杂。先编写service服务端,创建两个ServerSocket服务,一个是监听16000的,用来外来连接的,另一是监听16088是用来client访问的,也就是给service和client之间交互用的。先讲一个extListener他是监听16000,当有外部请求来时,也就是在公司访问时,先判断registerChannel是不是有client和service,没有就关闭连接。有的话就下发指令告诉client有访问了赶快给我连接,连接会存在channelQueue队列里,拿到连接后,两个连接交换数据就行。private static final int extPort = 16000; private static final int clintPort = 16088; private static AsynchronousSocketChannel registerChannel; static BlockingQueue[removed] channelQueue = new LinkedBlockingQueue[removed](); public static void main(String[] args) throws IOException { final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("localhost", clintPort)); listener.accept(null, new CompletionHandler[removed]() { public void completed(AsynchronousSocketChannel ch, Void att) { // 接受连接,准备接收下一个连接 listener.accept(null, this); // 处理连接 clintHandle(ch); } public void failed(Throwable exc, Void att) { exc.printStackTrace(); } }); final AsynchronousServerSocketChannel extListener = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("localhost", extPort)); extListener.accept(null, new CompletionHandler[removed]() { private Future[removed] writeFuture; public void completed(AsynchronousSocketChannel ch, Void att) { // 接受连接,准备接收下一个连接 extListener.accept(null, this); try { //判断是否有注册连接 if(registerChannel==null || !registerChannel.isOpen()){ try { ch.close(); } catch (IOException e) { e.printStackTrace(); } return; } //下发指令告诉需要连接 ByteBuffer bf = ByteBuffer.wrap(new byte[]{1}); if(writeFuture != null){ writeFuture.get(); } writeFuture = registerChannel.write(bf); AsynchronousSocketChannel take = channelQueue.take(); //clint连接失败的 if(take == null){ ch.close(); return; } //交换数据 exchangeDataHandle(ch,take); } catch (Exception e) { e.printStackTrace(); } } public void failed(Throwable exc, Void att) { exc.printStackTrace(); } }); Scanner in = new Scanner(System.in); in.nextLine(); } 看看clintHandle方法是怎么存进channelQueue里的,很简单client发送0,就认为他是注册的连接,也就交互的连接直接覆盖registerChannel,发送1的话就是用来交换数据的,扔到channelQueue,发送2就异常的连接。private static void clintHandle(AsynchronousSocketChannel ch) { final ByteBuffer buffer = ByteBuffer.allocate(1); ch.read(buffer, null, new CompletionHandler[removed]() { public void completed(Integer result, Void attachment) { buffer.flip(); byte b = buffer.get(); if (b == 0) { registerChannel = ch; } else if(b == 1){ channelQueue.offer(ch); }else{ //clint连接不到 channelQueue.add(null); } } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); } 再编写client客户端,dstHost和dstPort是用来连接service的ip和端口,看起来好长,实际上就是client连接service,第一个连接成功后向service发送了个0告诉他是注册的连接,用来交换数据。当这个连接收到service发送的1时,就会创建新的连接去连接service。private static final String dstHost = "192.168.1.10"; private static final int dstPort = 16088; private static final String srcHost = "localhost"; private static final int srcPort = 3389; public static void main(String[] args) throws IOException { System.out.println("dst:"+dstHost+":"+dstPort); System.out.println("src:"+srcHost+":"+srcPort); //使用aio final AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); client.connect(new InetSocketAddress(dstHost, dstPort), null, new CompletionHandler[removed]() { public void completed(Void result, Void attachment) { //连接成功 byte[] bt = new byte[]{0}; final ByteBuffer buffer = ByteBuffer.wrap(bt); client.write(buffer, null, new CompletionHandler[removed]() { public void completed(Integer result, Void attachment) { //读取数据 final ByteBuffer buffer = ByteBuffer.allocate(1); client.read(buffer, null, new CompletionHandler[removed]() { public void completed(Integer result, Void attachment) { buffer.flip(); if (buffer.get() == 1) { //发起新的连 try { createNewClient(); } catch (IOException e) { throw new RuntimeException(e); } } buffer.clear(); // 这里再次调用读取操作,实现循环读取 client.read(buffer, null, this); } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); Scanner in = new Scanner(System.in); in.nextLine(); } createNewClient方法,尝试连接本地服务,如果失败就发送2,成功就发送1,这个会走 service的clintHandle方法,成功的话就会让两个连接交换数据。private static void createNewClient() throws IOException { final AsynchronousSocketChannel dstClient = AsynchronousSocketChannel.open(); dstClient.connect(new InetSocketAddress(dstHost, dstPort), null, new CompletionHandler[removed]() { public void completed(Void result, Void attachment) { //尝试连接本地服务 final AsynchronousSocketChannel srcClient; try { srcClient = AsynchronousSocketChannel.open(); srcClient.connect(new InetSocketAddress(srcHost, srcPort), null, new CompletionHandler[removed]() { public void completed(Void result, Void attachment) { byte[] bt = new byte[]{1}; final ByteBuffer buffer = ByteBuffer.wrap(bt); Future[removed] write = dstClient.write(buffer); try { write.get(); //交换数据 exchangeData(srcClient, dstClient); exchangeData(dstClient, srcClient); } catch (Exception e) { closeChannels(srcClient, dstClient); } } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); //失败 byte[] bt = new byte[]{2}; final ByteBuffer buffer = ByteBuffer.wrap(bt); dstClient.write(buffer); } }); } catch (IOException e) { e.printStackTrace(); //失败 byte[] bt = new byte[]{2}; final ByteBuffer buffer = ByteBuffer.wrap(bt); dstClient.write(buffer); } } public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); } 下面是exchangeData交换数据方法,看起好麻烦,效果就类似IOUtils.copy(InputStream,OutputStream),一个流写入另一个流。private static void exchangeData(AsynchronousSocketChannel ch1, AsynchronousSocketChannel ch2) { try { final ByteBuffer buffer = ByteBuffer.allocate(1024); ch1.read(buffer, null, new CompletionHandler<Integer, CompletableFuture[removed]>() { public void completed(Integer result, CompletableFuture[removed] readAtt) { CompletableFuture[removed] future = new CompletableFuture[removed](); if (result == -1 || buffer.position() == 0) { // 处理连接关闭的情况或者没有数据可读的情况 try { readAtt.get(3,TimeUnit.SECONDS); } catch (Exception e) { e.printStackTrace(); } closeChannels(ch1, ch2); return; } buffer.flip(); CompletionHandler readHandler = this; ch2.write(buffer, future, new CompletionHandler<Integer, CompletableFuture[removed]>() { @Override public void completed(Integer result, CompletableFuture[removed] writeAtt) { if (buffer.hasRemaining()) { // 如果未完全写入,则继续写入 ch2.write(buffer, writeAtt, this); } else { writeAtt.complete(1); // 清空buffer并继续读取 buffer.clear(); if(ch1.isOpen()){ ch1.read(buffer, writeAtt, readHandler); } } } @Override public void failed(Throwable exc, CompletableFuture[removed] attachment) { if(!(exc instanceof AsynchronousCloseException)){ exc.printStackTrace(); } closeChannels(ch1, ch2); } }); } public void failed(Throwable exc, CompletableFuture[removed] attachment) { if(!(exc instanceof AsynchronousCloseException)){ exc.printStackTrace(); } closeChannels(ch1, ch2); } }); } catch (Exception ex) { ex.printStackTrace(); closeChannels(ch1, ch2); } } private static void closeChannels(AsynchronousSocketChannel ch1, AsynchronousSocketChannel ch2) { if (ch1 != null && ch1.isOpen()) { try { ch1.close(); } catch (IOException e) { e.printStackTrace(); } } if (ch2 != null && ch2.isOpen()) { try { ch2.close(); } catch (IOException e) { e.printStackTrace(); } } } [removed]>>机会语言:Java、Js、测试、python、ios、安卓、C++等)!>测试我这边就用虚拟机来测试,用云服务器就比较麻烦,得登录账号,增加开放端口规则,上传代码。我这边用Hyper-V快速创建了虚拟机,创建一个windows 10 MSIX系统,安装JDK8,下载地址:www.azul.com/downloads/?… 。怎样把本地编译好的class放到虚拟机呢,虚拟机是可以访问主机ip的,我们可以弄一个web的文件目录下载给虚拟机访问,人生苦短我用pyhton,下面python简单代码if __name__ == '__main__': #定义服务器的端口PORT=8000# 创建请求处理程序 Handler = http.server.SimpleHTTPRequestHandler # 设置工作目录 os.chdir("C:\netTunnlDemo\client\target") # 创建服务器 with socketserver.TCPServer(("", PORT), Handler) as httpd: print(f"服务启动在端口 {PORT}") httpd.serve_forever() 到class的目录下运行cmd,执行java -cp . org.example.Main,windows 默认远程端口3389。最后效果#畅聊专区#总结使用AIO导致代码长,逻辑并不复杂,完整代码,供个人学习:断续/netTunnlDemo (gitee.com)
科叼
5 8 开源硬件平台
Flutter 又要凉了? Flock 是什么东西?
今天突然看到这个消息,突然又有一种熟悉的味道,看来这个月 Flutter “又要凉一次了”:[removed]起因 flutter foundation 决定 fork Flutter 并推出 Flock 分支用于自建维护,理由是:foundation 推测 Flutter 团队的劳动力短缺,因为 Flutter 需要维护 Android、iOS、Mac、Window、Linux、Web 等平台,但是 Flutter团队的规模仅略有增加。在 foundation 看来,保守估计全球至少有 100 万 Flutter 相关开发者,而 Flutter 团队的规模大概就只有 50+ 人,这个比例并不健康。问题在于这个数据推测就很迷,没有数据来源的推测貌似全靠“我认为”。。。。另外 foundation 做这个决定,还因为 Flutter 官方团队对其 6 个支持的平台中,有 3 个处于维护模式(Window、Mac、Linux),所以他们无法接受桌面端的现场,因为他们认为桌面端很可能是 Flutter 最大的未开发价值。关于这点目前 PC 端支持确实缓慢,但也并没有完全停止,如果关注 PC issue 的应该看到, Mac 的 PlatformView 和 WebView 支持近期才初步落地。而让 foundation 最无法忍受的是,issue 的处理还有 pr 的 merge 有时候甚至可能会积累数年之久。事实上这点确实成立,因为 Flutter 在很多功能上都十分保守,同时 issue 量大且各平台需求多等原因,很多能力的支持时间跨度多比较长,例如[Row/Column 即将支持 Flex spacing]、[宏编程支持]、[支持P3色域] 等这些都是持续了很久才被 merge 的 feature 。所以 Flutter 的另外一个支持途径是来自社区 PR,但是 foundation 表示 Flutter 的代码 Review 和审计工作缓慢,并且沟通困难,想法很难被认可等,让 foundation 无法和 Flutter 官方有效沟通。总结起来,在 foundation 的角度是,Flutter 官方团队维护 Flutter 不够尽心尽力。所以他们决定,创建 Flutter 分支,并称为 Flock:意寓位 “Flutter+”。不过 foundation 表示,他们其实并不想也不打算分叉 Flutter 社区,Flock 将始终与 Flutter 保持同步。Flock 的重点是添加重要的错误修复和全新的社区功能支持,例如 Flutter 团队不做的,或者短期不会实现:并且 Flock 的目的是招募一个比 Flutter 团队大得多的 PR 审查团队,从而加快 PR 的审计和推进。所以看起来貌似这是一件好事,那么为什么解读会是“崩盘”和“内斗”?大概还是 Flutter 到时间凉了,毕竟刚刚过完Fluter 是十周年生日 ,凉一凉也挺好的。作者:恋猫de小郭出处:juejin.cn/post/7431032490284236839
科叼
2 14 开源硬件平台
为什么 Java 大佬都不推荐使用 keySet() 遍历HashMap?
#畅聊专区#在Java编程中,HashMap 是一种非常常见的数据结构。我们经常需要对其中的键值对进行遍历。通常有多种方法可以遍历 HashMap,其中一种方法是使用 keySet() 方法。然而,很多Java大佬并不推荐这种方法。为什么呢?keySet() 方法的工作原理首先,让我们来看一下 keySet() 方法是如何工作的。keySet() 方法返回 HashMap 中所有键的集合 (Set[removed])。然后我们可以使用这些键来获取相应的值。代码示例如下: // 创建一个HashMap并填充数据 Map[removed] map = new HashMap[removed](); map.put("apple", 1); map.put("banana", 2); map.put("cherry", 3); // 使用keySet()方法遍历HashMap for (String key : map.keySet()) { // 通过键获取相应的值 Integer value = map.get(key); System.out.println("Key: " + key + ", Value: " + value); } 这个代码看起来没什么问题,但在性能和效率上存在一些隐患。keySet() 方法的缺点1、 多次哈希查找:如上面的代码所示,使用 keySet() 方法遍历时,需要通过键去调用 map.get(key) 方法来获取值。这意味着每次获取值时,都需要进行一次哈希查找操作。如果 HashMap 很大,这种方法的效率就会明显降低。2、 额外的内存消耗:keySet() 方法会生成一个包含所有键的集合。虽然这个集合是基于 HashMap 的键的视图,但仍然需要额外的内存开销来维护这个集合的结构。如果 HashMap 很大,这个内存开销也会变得显著。3、 代码可读性和维护性:使用 keySet() 方法的代码可能会让人误解,因为它没有直接表现出键值对的关系。在大型项目中,代码的可读性和维护性尤为重要。[removed]更好的选择:entrySet() 方法相比之下,使用 entrySet() 方法遍历 HashMap 是一种更好的选择。entrySet() 方法返回的是 HashMap 中所有键值对的集合 (Set<Map.Entry[removed]>)。通过遍历这个集合,我们可以直接获取每个键值对,从而避免了多次哈希查找和额外的内存消耗。下面是使用 entrySet() 方法的示例代码: // 创建一个HashMap并填充数据 Map[removed] map = new HashMap[removed](); map.put("apple", 1); map.put("banana", 2); map.put("cherry", 3); // 使用entrySet()方法遍历HashMap for (Map.Entry[removed] entry : map.entrySet()) { // 直接获取键和值 String key = entry.getKey(); Integer value = entry.getValue(); System.out.println("Key: " + key + ", Value: " + value); } entrySet() 方法的优势1、 避免多次哈希查找:在遍历过程中,我们可以直接从 Map.Entry 对象中获取键和值,而不需要再次进行哈希查找,提高了效率。2、 减少内存消耗:entrySet() 方法返回的是 HashMap 内部的一个视图,不需要额外的内存来存储键的集合。3、 提高代码可读性:entrySet() 方法更直观地表现了键值对的关系,使代码更加易读和易维护。性能比较我们来更深入地解析性能比较,特别是 keySet() 和 entrySet() 方法在遍历 HashMap 时的性能差异。主要性能问题1、 多次哈希查找: 使用 keySet() 方法遍历 HashMap 时,需要通过键调用 map.get(key) 方法获取值。这意味着每次获取值时都需要进行一次哈希查找操作。哈希查找虽然时间复杂度为 O(1),但在大量数据下,频繁的哈希查找会累积较高的时间开销。2、 额外的内存消耗: keySet() 方法返回的是一个包含所有键的集合。虽然这个集合是基于 HashMap 的键的视图,但仍然需要额外的内存来维护这个集合的结构。更高效的选择:entrySet() 方法相比之下,entrySet() 方法返回的是 HashMap 中所有键值对的集合 (Set<Map.Entry[removed]>)。通过遍历这个集合,我们可以直接获取每个键值对,避免了多次哈希查找和额外的内存消耗。性能比较示例让我们通过一个具体的性能比较示例来详细说明: import java.util.HashMap; import java.util.Map; public class HashMapTraversalComparison { public static void main(String[] args) { // 创建一个大的HashMap Map[removed] map = new HashMap[removed](); for (int i = 0; i < 1000000; i++) { map.put("key" + i, i); } // 测试keySet()方法的性能 long startTime = System.nanoTime(); // 记录开始时间 for (String key : map.keySet()) { Integer value = map.get(key); // 通过键获取值 } long endTime = System.nanoTime(); // 记录结束时间 System.out.println("keySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒"); // 测试entrySet()方法的性能 startTime = System.nanoTime(); // 记录开始时间 for (Map.Entry[removed] entry : map.entrySet()) { String key = entry.getKey(); // 直接获取键 Integer value = entry.getValue(); // 直接获取值 } endTime = System.nanoTime(); // 记录结束时间 System.out.println("entrySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒"); } } 深度解析性能比较示例1、 创建一个大的 HashMap: Map[removed] map = new HashMap[removed](); for (int i = 0; i < 1000000; i++) { map.put("key" + i, i); } 创建一个包含100万个键值对的 HashMap。键 为 "key" + i,值 为 i。这个 HashMap 足够大,可以明显展示两种遍历方法的性能差异。2、 测试 keySet() 方法的性能: long startTime = System.nanoTime(); // 记录开始时间 for (String key : map.keySet()) { Integer value = map.get(key); // 通过键获取值 } long endTime = System.nanoTime(); // 记录结束时间 System.out.println("keySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒"); 使用 keySet() 方法获取所有键,并遍历这些键。在每次迭代中,通过 map.get(key) 方法获取值。记录开始时间和结束时间,计算遍历所需的总时间。3、 测试 entrySet() 方法的性能:startTime = System.nanoTime(); // 记录开始时间 for (Map.Entry[removed] entry : map.entrySet()) { String key = entry.getKey(); // 直接获取键 Integer value = entry.getValue(); // 直接获取值 } endTime = System.nanoTime(); // 记录结束时间 System.out.println("entrySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒"); 使用 entrySet() 方法获取所有键值对,并遍历这些键值对。在每次迭代中,直接从 Map.Entry 对象中获取键和值。记录开始时间和结束时间,计算遍历所需的总时间。性能结果分析假设上述代码的运行结果如下: keySet() 方法遍历时间: 1200000000 纳秒 entrySet() 方法遍历时间: 800000000 纳秒 可以看出,使用 entrySet() 方法的遍历时间明显短于 keySet() 方法。这主要是因为:1、 避免了多次哈希查找: 使用 keySet() 方法时,每次获取值都需要进行一次哈希查找。而使用 entrySet() 方法时,键和值直接从 Map.Entry 对象中获取,无需再次查找。2、 减少了内存消耗: 使用 keySet() 方法时,额外生成了一个包含所有键的集合。而使用 entrySet() 方法时,返回的是 HashMap 内部的一个视图,无需额外的内存开销。小结一下通过性能比较示例,我们可以清楚地看到 entrySet() 方法在遍历 HashMap 时的效率优势。使用 entrySet() 方法不仅能避免多次哈希查找,提高遍历效率,还能减少内存消耗。综上所述,在遍历 HashMap 时,entrySet() 方法是更优的选择。几种高效的替代方案除了 entrySet() 方法外,还有其他几种高效的替代方案,可以用于遍历 HashMap。以下是几种常见的高效替代方案及其优缺点分析:1. 使用 entrySet() 方法我们已经讨论过,entrySet() 方法是遍历 HashMap 时的一个高效选择。它直接返回键值对的集合,避免了多次哈希查找,减少了内存开销。import java.util.HashMap; import java.util.Map; public class EntrySetTraversal { public static void main(String[] args) { Map[removed] map = new HashMap[removed](); map.put("apple", 1); map.put("banana", 2); map.put("cherry", 3); for (Map.Entry[removed] entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println("Key: " + key + ", Value: " + value); } } } 2. 使用 forEach 方法从 Java 8 开始,Map 接口提供了 forEach 方法,可以直接对每个键值对进行操作。这种方式利用了 lambda 表达式,代码更简洁,可读性强。 import java.util.HashMap; import java.util.Map; public class ForEachTraversal { public static void main(String[] args) { Map[removed] map = new HashMap[removed](); map.put("apple", 1); map.put("banana", 2); map.put("cherry", 3); map.forEach((key, value) -> { System.out.println("Key: " + key + ", Value: " + value); }); } } 3. 使用 iterator 方法另一种遍历 HashMap 的方法是使用迭代器 (Iterator)。这种方法适用于需要在遍历过程中对集合进行修改的情况,比如删除某些元素。 import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class IteratorTraversal { public static void main(String[] args) { Map[removed] map = new HashMap[removed](); map.put("apple", 1); map.put("banana", 2); map.put("cherry", 3); Iterator<Map.Entry[removed]> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry[removed] entry = iterator.next(); String key = entry.getKey(); Integer value = entry.getValue(); System.out.println("Key: " + key + ", Value: " + value); } } } 4. 使用 Streams APIJava 8 引入了 Streams API,可以结合 stream() 方法和 forEach 方法来遍历 HashMap。这种方法可以对集合进行更复杂的操作,比如过滤、映射等。 import java.util.HashMap; import java.util.Map; public class StreamTraversal { public static void main(String[] args) { Map[removed] map = new HashMap[removed](); map.put("apple", 1); map.put("banana", 2); map.put("cherry", 3); map.entrySet().stream().forEach(entry -> { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println("Key: " + key + ", Value: " + value); }); } } 优缺点分析entrySet() 方法:优点:避免多次哈希查找,减少内存消耗,代码简单明了。缺点:没有特定缺点,在大多数情况下是最佳选择。forEach 方法:优点:代码简洁,可读性强,充分利用 lambda 表达式。缺点:仅适用于 Java 8 及以上版本。iterator 方法:优点:适用于需要在遍历过程中修改集合的情况,如删除元素。缺点:代码稍显繁琐,不如 entrySet() 和 forEach 方法直观。Streams API 方法:优点:支持复杂操作,如过滤、映射等,代码简洁。缺点:仅适用于 Java 8 及以上版本,性能在某些情况下可能不如 entrySet() 和 forEach。结论在遍历 HashMap 时,entrySet() 方法是一个高效且广泛推荐的选择。对于更现代的代码风格,forEach 方法和 Streams API 提供了简洁且强大的遍历方式。如果需要在遍历过程中修改集合,可以使用 iterator 方法。根据具体需求选择合适的遍历方法,可以显著提高代码的效率和可读性。作者:架构师专栏出处:juejin.cn/post/7393663398406799372
科叼
0 3 开源硬件平台
Lodash已过时,试试它吧!
作者:芝士加开篇之前,先提两个问题:你知道 Radash 吗?Radash 会取代 Lodash 吗?认识 Radash相信大家都知道Lodash, 这个 JavaScript 工具库从2012至今,已经存在长达12年的时间,它在github上的 star 数超过 58.6k, 在 npm 上每周的下载量已超过 5200 万。最初,Lodash 的运行情况很好,帮助开发人员编写了简洁、可维护的 JavaScript 代码。然而,由于近两年没有针对最新 JavaScript 函数进行重大更新,开发人员在使用 Lodash 时开始面临一些挑战,在这样的背景下,Radash 应运而生,以其现代化的特性和对TypeScript的友好支持,逐渐成为开发者的新宠。在本文中,我将详细讨论 Lodash 中的问题以及 Radash 如何解决这些问题,从而回答提出的问题:Radash 会取代 Lodash 吗?[removed]Lodash 面临的问题随着 JavaScript 语言的不断进化和新特性的引入,Lodash 的一些功能开始显得不再那么必要。Lodash 函数过时了随着ES6及后续版本的推出,JavaScript 引入了许多新的语言特性,如可选链(?.)和空值合并(??),使得一些 Lodash 的函数显得多余。在 ES6 之前,如果你想安全地访问对象的嵌套属性,可以使用 Lodash 的 _.get 函数来避免可能的 undefined 错误。例如:// 假设我们有一个对象,我们想访问 `a.b.c` 属性  const obj = {     a: {         b: {             c: 'Hello'          }        }  };  // 使用 Lodash 的 _.get 来安全地获取值  const value = _.get(obj, 'a.b.c', 'Default'); console.log(value);  // 输出: 'Hello' 如果 obj 中的任何中间属性是 undefined 或 null,_.get 将返回提供的默认值 'Default',而不是抛出错误。然而,随着 ES6 引入了可选链操作符 ?.,我们现在可以更简洁地实现同样的功能,而不需要 Lodash:// 使用可选链操作符来安全地访问嵌套属性 const value = obj?.a?.b?.c || 'Default'; console.log(value); // 输出: 'Hello' 同样的,像 .filter、.map 和 _.size 这样的函数也变得多余了。并且,在性能方面,像可选链?.这样的特性远远超过了 Lodash 函数,可选链的性能几乎 Lodash 的 _.get 函数的两倍(根据能测量工具:measurethat.net的测试结果)。源码可读性差实话说,上面所谈到的,作为开发者的你也许可以接受,但是 Lodash 源码的学习成本真的很高,这可能才是我们的底线。这是 Lodash源码我们不应该为了了解一个单行函数是如何工作的,而去翻阅 15000 行代码,为了学习 API。几年前,相信大家经常看到过这样解读 Lodash 的文章。以前,我也曾花费不少时间挖掘源代码,学习每一个 API,记录每一个函数调用,理解得足够透彻,为了能够回答一个简单的面试问题,比如 isNumber 函数是如何工作的?Radash 的崛起Radash,这个新兴的工具库,以其现代化的设计和对TypeScript的原生支持,迅速吸引了开发者的注意。虽然 Radash 是新产品,但它在 GitHub 上的 star 数已超过 2.8K,拥有 99 个 Forks,每周的 NPM 下载量超过 7.6 万。您可以使用 NPM 或 Yarn 轻松安装 Radash。Radash 的特点包括:零依赖:Radash 不依赖于任何第三方库,使得项目更加轻量级。TypeScript友好:Radash 完全使用TypeScript编写,提供了准确的类型定义。现代化功能:Radash 去除了 Lodash 中一些过时的函数,并引入了许多新的实用功能。易于理解和维护:Radash 的源代码易于理解,对新手友好。值得👍的是,源代码的维护真的将新人的易懂性放在首位。在大多数情况下,如果你想使用 Radash 函数,但又不想安装,你可以直接从 GitHub 复制。例如下面这段源代码:export const unique = [removed]( array: readonly T[], toKey?: (item: T) => K ): T[] => { const valueMap = array.reduce((acc, item) => { const key = toKey ? toKey(item) : (item as any as string | number | symbol) if (acc[key]) return acc acc[key] = item return acc }, {} as Record[removed]) return Object.values(valueMap) } 定义了一个名为 unique 的泛型函数,目的是从输入数组中提取唯一的元素。函数接受两个参数:一个类型为 readonly T[] 的只读数组 array,以及一个可选的映射函数 toKey, 相信很多初级的开发者都可以看懂。另外,如果你项目只需要一个unique函数,完全可以将源码复制到自己的工具文件中来使用。目前 Radash 已经提供 90多个实用函数。下面我们将介绍几个特别实用的函数:tryit()tryit 函数可能是我最喜欢的 Radash 函数。tryit 函数可以包装一个函数,将其转换为错误优先函数。适用于异步和同步功能。import {tryit} form "radash" const [err, user] = await tryit(api.users.userInfo)(userId) 我发现它是对代码整洁最大提升的代码。不再需要为了尝试某些操作而分叉控制流。不再需要在 try 块外部创建一个可变的 let 变量,在里面设置它,然后在之后检查它。range()range() 可以取代传统的循环。例如,假设您需要打印从 1 到 5 的数据,如果使用传统的 for 循环,就会像下面这样:for (let i = 1; i [removed] i * 2, 2); console.log(myList); // 输出: [2, 4, 6, 8, 10] 在这个例子中,list()函数创建了一个从2开始,每次增加2,到10结束的列表。counting()counting()函数用于统计类数组集合中各类元素的数量。它接收一个对象数组和一个回调函数,通过回调函数定义计数条件。 import { counting } from 'radash'; const items = [ { category: 'A' }, { category: 'B' }, { category: 'A' }, { category: 'C' } ]; const counts = counting(items, (item) => item.category); console.log(counts); // 输出: { A: 2, B: 1, C: 1 } 在项目中,我发现自己使用counting的次数比自己想象得要多。除了上面这些有特色的方法,还有很多实用的方法, 例如:节流 throttle 和防抖 debounce类型判断方法,如isArray()、isString()、isNumber()等对象操作 pick、omit、clone等还有很多非常实用的方法, 大家可以通过官网查阅:在这篇文章中,讨论了不喜欢 Lodash 的原因,以及可能倾向 Radash 等替代品的原因。但是,与新的竞争对手相比,Lodash 仍然拥有庞大的用户群,并在许多大型项目中得到广泛应用。新项目中你会选择 Radash吗?参考文章:medium.com/exobase/lod…
科叼
1 4 开源硬件平台
谈谈我做 Electron 应用的这一两年
#畅聊专区#作者:前端徐徐(同公众号)今天和大家谈谈我做 Electron 桌面端应用的这一两年,把一些经验和想法分享给大家。对桌面端开发的一些看法如果你是前端的话,多一门桌面端开发的技能也不是坏事,相当是你的一个亮点,进可攻,退可守。当然也有人说,做桌面端可能就路越走越窄了,但是我想说的是深度和广度其实也可以理解为一个维度,对于技术人来说,知道得越多就行,因为到后期你要成为某个方面的专家,就是可能会非常深入某一块,换一种思路其实也是叫知道得越多。所以,我觉得前端能有做桌面端的机会也是非常好的,即拓展了自己的技能,还能深入底层,因为现阶段由于业务方向的需要,我已经开始看 chromium 源码了,前端的老祖宗。当然,以上这些只代表自己的观点,大家自行斟酌。[removed].谈谈 Electron其实刚刚工作前两年我就知道这个框架了,当时也做过小 demo,而且还在当时的团队里面分享过这个技术,但是当时对这门技术的认知是非常浅薄的,就知道前端可以做桌面端了,好厉害,大概就停留在这个层面。后面在真正需要用到这门技术去做一个企业级的桌面应用的时候才去真正调研了一下这个框架,然后才发现它真的非常强大,强大到几乎可以实现你桌面端的任何需求。网上关于 Electron 与其他框架的对比实在是太多了,Google 或者 Baidu 都能找到你想要的答案,好与不好完全看自己的业务场景以及自己所在团队的情况。谈谈自己的感受,什么情况下可以用这门框架追求效率,节省人力财力团队前端居多UI交互多什么情况下不适合这门框架呢?包体积限制性能消耗较高的应用多窗口应用我们当时的情况就是要追求效率,双端齐头并进,所以最后经过综合对比,选择了 Electron。毕竟 Vscode 就是用它做的,给了我们十足的信心和勇气,一点都不虚。一图抵千字,我拿出这张图你自己就有所判断了,还是那句话,仁者见仁,智者见智,完全看自己情况。技术整体架构这里我画了一张我所从事 Electron 产品的整体技术架构图。 整个项目基于 Vite 开发构建的,基础设施就是常见的安全策略,然后加上一些本地存储方案,外加一个外部插件,这个插件是用 Tauri 做的 Webview,至于为什么要做这个插件我会在后面的段落说明。应用层面的框架主要是分三个大块,下面主要是为了构建一些基础底座,然后将架构进行分层设计,添加一些原生扩展,上面就是基础的应用管理和 GUI 相关的东西,有了这个整体的框架在后面实现一些业务场景的时候就会变得易如反掌(夸张了一点,因为有的技术细节真的很磨人😐)。挑战和方案桌面端开发会遇到一些挑战,这些挑战大部分来源于特殊的业务场景,框架只能解决一些比较常见的应用场景,当然不仅仅是桌面端,其实移动端或者是 Web 端我相信大家都会遇到或多或少的挑战,我这里遇到的一些挑战和响应的方案不一定适合你,只是做单纯的记录分享,如果有帮助到你,我很开心。下面我挑选软件升级更新,任务队列设计,性能检测优化以及一些特殊的需求这几个方面来聊聊相应的挑战和方案。软件升级更新升级更新主要是需要做到定向灰度。这个功能是非常重要的,应该大部分的应用都有定向灰度的功能,所以我们为了让软件能够平滑升级,第一步就是实现定向灰度,达到效果可回收,性能可监控,错误可告警的目的。定向更新的功能实现了之后,后面有再多的功能需要实现都有基础保障了,下面是更新相关的能力图。整个更新模块的设计是分为两大块,一块是后台管理系统,一块是客户端。后台管理系统主要是维护相应的策略配置,比如哪些设备需要定向更新,哪些需要自动更新,不需要更新的白名单以及更新后是需要提醒用户相应的更新功能还是就静默更新。客户端主要就是拉取相应的策略,然后执行相应的更新动作。整体来说,这一块是花了很多时间去研究的,windows 还好,没有破坏其整个生命周期,傻瓜式的配置一下electron-update 相关的函数钩子就可以了。Mac 的更新花了很多时间,因为破坏了文件的生命周期,再加上保活任务,所以会对 electron-update 的更新钩子进行毁灭性的破坏,最后也只能研究其源码然后自己去实现特殊场景下的更新了。任务队列设计任务模块的实现在我们这个软件里面也是非常重要的一环,因为客户端会跑非常多的定时任务。刚开始研发这个产品的时候其实还好,定时任务屈指可数,但是随着长时间的迭代,端上要执行的任务越来越多,每个任务的触发时间,触发条件都不一样,以及还要考虑任务的并发情况和对性能的影响,所以在中后期我们对整个任务模块都做了相应的改造。下面是整个任务模块的核心能力图。 性能优化Electron相关的性能优化其实网上也有非常多的文章,我这里说说我的实践和感受。首先,性能优化你需要优化什么?这个就是你的出发点了,我们要解决一个问题,首先得知道问题的现状,如果你都不知道现在的性能是什么样子,如何去优化呢?所以发现问题是性能优化的最重要的一步。这里就推荐两个工具,一个是chrome dev-tool,一个是electron 的 inspector,第一个可以观测渲染进程相关的性能情况,第二个可以观测主进程相关的性能情况。具体可参考以下网址:developer.chrome.com/docs/devtoo…www.electronjs.org/zh/docs/lat…有了工具之后我们就需要用工具去分析一些数据和问题,这里面最重要的就是内存相关的分析,你通过内存相关的分析可以看到 CPU 占用高的动作,以及提前检测出内存泄漏的风险。只要把这两个关键的东西抓住了,应用的稳定性就可以得到保障了,我的经验就是每次发布之前都会跑一遍内存快照,内存没有异常才进行发布动作,内存泄漏是最后的底线。我说说我大概的操作步骤。通过Performance确认大体的溢出位置使用Memory进行细粒度的问题分析根据heap snapshot,判断内存溢出的代码位置调试相应的代码块循环往复上面的步骤上面的步骤在主进程和渲染进程都适用,每一步实际操作在这里就不详细展开了,主要是提供一个思路和方法,因为 dev-tool 的面板东西非常多,扩展开来都可以当一个专题了。然后我再说说桌面端什么地方可能会内存泄漏或者溢出,下面这些都是我血和泪的教训。创建的子进程没有及时销毁:如果子进程在完成任务后未被正确终止,这些进程会继续运行并占用系统资源,导致内存泄漏和资源浪费。假设你的 Electron 应用启动了一个子进程来执行某些计算任务,但在计算完成后未调用 childProcess.kill() 或者未确保子进程已正常退出,那么这些子进程会一直存在,占用系统内存。 const { spawn } = require('child_process'); const child = spawn('someCommand'); child.on('exit', () => { console.log('Child process exited'); }); // 未正确终止子进程可能导致内存泄漏 HTTP 请求时间过长没有正确处理:长时间未响应的 HTTP 请求如果没有设定超时机制,会使得这些请求占用内存资源,导致内存泄漏。在使用 fetch 或 axios 进行 HTTP 请求时,如果服务器长时间不响应且没有设置超时处理,内存会被这些未完成的请求占用。 const fetch = require('node-fetch'); fetch('https://example.com/long-request') .then(response => response.json()) .catch(error => console.error('Error:', error)); // 应该设置请求超时 const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, 5000); // 5秒超时 fetch('https://example.com/long-request', { signal: controller.signal }) .then(response => response.json()) .catch(error => console.error('Error:', error)); 事件处理器没有移除未正确移除不再需要的事件处理器会导致内存一直被占用,因为这些处理器仍然存在并监听事件。在添加事件监听器后,未在适当时机移除它们会导致内存泄漏。 const handleEvent = () => { console.log('Event triggered'); }; window.addEventListener('resize', handleEvent); // 在不再需要时移除事件监听器 window.removeEventListener('resize', handleEvent); 定时任务未被正确销毁未在适当时候清除不再需要的定时任务(如 setInterval)会导致内存持续占用。使用 setInterval 创建的定时任务,如果未在不需要时清除,会导致内存泄漏。 const intervalId = setInterval(() => { console.log('Interval task running'); }, 1000); // 在适当时机清除定时任务 clearInterval(intervalId); JavaScript 对象未正确释放长时间保留不再使用的 JavaScript 对象会导致内存占用无法释放,特别是当这些对象被全局变量或闭包引用时。创建了大量对象但未在适当时机将它们置为 null 或解除引用。 let bigArray = new Array(1000000).fill('data'); // 当不再需要时,应释放内存 bigArray = null; 窗口实例未被正确销毁未关闭或销毁不再使用的窗口实例会继续占用内存资源,即使用户已经关闭了窗口界面。创建了一个新的 BrowserWindow 实例,但在窗口关闭后未销毁它。 const { BrowserWindow } = require('electron'); let win = new BrowserWindow({ width: 800, height: 600 }); win.on('closed', () => { win = null; }); // 应确保在窗口关闭时正确释放资源 大文件或大数据量的处理处理大文件或大量数据时,如果没有进行内存优化和分批处理,会导致内存溢出和性能问题。在读取一个大文件时,未采用流式处理,而是一次性加载整个文件到内存中。 const fs = require('fs'); // 不推荐的方式:一次性读取大文件 fs.readFile('largeFile.txt', (err, data) => { if (err) throw err; console.log(data); }); // 推荐的方式:流式读取大文件 const readStream = fs.createReadStream('largeFile.txt'); readStream.on('data', (chunk) => { console.log(chunk); }); 祝大家在自己的领域越来越深,早日触摸到天花板。
科叼
9 12 开源硬件平台
前端:为什么 try catch 能捕捉 await 后 Promise 的错误?
作者:21Pilots一次代码CR引发的困惑“你这块的代码,没有做异常捕获呀,要是抛出了异常,可能会影响后续的代码流程”。这是一段出自组内代码CR群的聊天记录。代码类似如下: const asyncErrorThrow = () => { return new Promise((resolve, reject) => { // 业务代码... // 假设这里抛出了错误 throw new Error('抛出错误'); // 业务代码... }) } const testFun = async () => { await asyncErrorThrow(); console.log("async 函数中的后续流程"); // 不会执行 } testFun(); 在 testFun 函数中,抛出错误后,await 函数中后续流程不会执行。仔细回想一下,在我的前端日常开发中,对于错误捕获,还基本停留在使用 Promise时用 catch 捕获一下 Promise 中抛出的错误或者 reject,或者最基本的,在使用 JSON.parse、JSON.stringfy等容易出错的方法中,使用 try..catch... 方法捕获一下可能出现的错误。后来,这个同学将代码改成了:const asyncErrorThrow = () => { return new Promise((resolve, reject) => { // 业务代码... throw new Error('抛出错误'); // 业务代码... }) } const testFun = async () => { try { await asyncErrorThrow(); console.log("async 函数中的后续流程"); // 不会执行 } catch (error) { console.log("若错误发生 async 函数中的后续流程"); // 会执行 } } testFun(); 而这次不同的是,这段修改后的代码中使用了 try...catch...来捕获 async...await... 函数中的错误,这着实让我有些困惑,让我来写的话,我可能会在 await 函数的后面增加一个 catch:await asyncErrorThrow().catch(error => {})。因为我之前已经对 try..catch 只能捕获发生在当前执行上下文的错误(或者简单理解成同步代码的错误)有了一定的认知,但是 async...await... 其实还是异步的代码,只不过用的是同步的写法,为啥用在这里就可以捕获到错误了呢?在查阅了相当多的资料之后,才清楚了其中的一些原理。[removed]Promise 中的错误我们都知道,一个 Promise 必然处于以下几种状态之一:待定(pending):初始状态,既没有被兑现,也没有被拒绝。已兑现(fulfilled):意味着操作成功完成。已拒绝(rejected):意味着操作失败。当一个 Promise 被 reject 时,该 Promise 会变为 rejected 状态,控制权将移交至最近的 rejection 处理程序。最常见的 rejection 处理程序就是 catch handler或者 then 函数的第二个回调函数。而如果在 Promise 中抛出了一个错误。这个 Promise 会直接变成 rejected 状态,控制权移交至最近的 error 处理程序。 const function myExecutorFunc = () => { // 同步代码 throw new Error(); }; new Promise(myExecutorFunc); Promise 的构造函数需要传入的 Executor 函数参数,实际上是一段同步代码。在我们 new 一个新的 Promise 时,这个 Executor 就会立即被塞入到当前的执行上下文栈中进行执行。但是,在 Executor 中 throw 出的错误,并不会被外层的 try...catch 捕获到。 const myExecutorFunc = () => { // 同步代码 throw new Error(); }; try { new Promise(myExecutorFunc); } catch (error) { console.log('不会执行: ', error); } console.log('会执行的'); // 打印 其原因是因为,在 Executor 函数执行的过程中,实际上有一个隐藏的机制,当同步抛出错误时,相当于执行了 reject 回调,让该 Promise 进入 rejected 状态。而错误不会影响到外层的代码执行。 const myExecutorFunc = () => { throw new Error(); // 等同于 reject(new Error()); }; new Promise(myExecutorFunc); console.log('会执行的'); // 打印 同理 then 回调函数也是这样的,抛出的错误同样会变成 reject。在一个普通脚本执行中,我们知道抛出一个错误,如果没有被捕获掉,会影响到后续代码的执行,而在 Promise 中,这个错误不会影响到外部代码的执行。对于 Promise 没有被捕获的错误,我们可以通过特定的事件处理函数来观察到。new Promise(function() { throw new Error(""); }); // 没有用来处理 error 的 catch // Web 标准实现 window.addEventListener('unhandledrejection', function(event) { console.log(event); // 可以在这里采取其他措施,如日志记录或应用程序关闭 }); // Node 下的实现 process.on('unhandledRejection', (event) => { console.log(event); // 可以在这里采取其他措施,如日志记录或应用程序关闭 }); Promise 是这样实现的,我们可以想一想为什么要这样实现。我看到一个比较好的回答是这个:传送门。我也比较赞成他的说法,我觉得,Promise 的诞生是为了解决异步函数过多而形成的回调地狱,使用了微任务的底层机制来实现异步链式调用。理论上是可以将同步的错误向上冒泡抛出然后用 try...catch... 接住的,异步的一些错误用 catch handler 统一处理,但是这样做的话会使得 Promise 的错误捕获使用起来不够直观,如果同步的错误也进行 reject 的话,实际上我们处理错误的方式就可以统一成 Promise catch handler 了,这样其实更直观也更容易让开发者理解和编写代码。async await 的问题那么回到我们最开始的问题,在这个里面,为什么 try catch 能够捕获到错误? jconst asyncErrorThrow = () => { return new Promise((resolve, reject) => { // 业务代码... throw new Error('抛出错误'); // 业务代码... }) } const testFun = async () => { try { await asyncErrorThrow(); console.log("async 函数中的后续流程"); // 不会执行 } catch (error) { console.log("若错误发生 async 函数中的后续流程"); // 会执行 } } testFun(); 我思考了很久,最后还是从黄玄大佬的知乎回答中窥见的一部分原理。这...难道就是浏览器底层帮我们处理的事儿吗,不然也没法解释了。唯一能够解释的事就是,async await 原本就是为了让开发者使用同步的写法编写异步代码,目的是消除过多的 Promise 调用链,我们在使用 async await 时,最好就是不使用 .catch 来捕获错误了,而直接能使用同步的 try...catch... 语法来捕获错误。即使 .catch 也能做同样的事情。只是说,代码编写风格统一性的问题让我们原本能之间用同步语法捕获的错误,就不需要使用 .catch 链式调用了,否则代码风格看起来会有点“异类”。这就是为什么 async MDN 中会有这样一句解释:参考文档:《使用Promise进行错误治理》- zh.javascript.info/promise-err…《为什么try catch能捕捉 await 后 promise 错误? 和执行栈有关系吗?》www.zhihu.com/question/52…
科叼
0 3 开源硬件平台
我用这10招,能减少了70%的BUG
作者:苏三说技术出处:juejin.cn/post/7358310951479427083前言对于大部分程序员来说,主要的工作时间是在开发和修复BUG。有可能修改了一个BUG,会导致几个新BUG的产生,不断循环。那么,有没有办法能够减少BUG,保证代码质量,提升工作效率?答案是肯定的。如果能做到,我们多出来的时间,多摸点鱼,做点自己喜欢的事情,不香吗?这篇文章跟大家一起聊聊减少代码BUG的10个小技巧,希望对你会有所帮助。1 找个好用的开发工具在日常工作中,找一款好用的开发工具,对于开发人员来说非常重要。不光可以提升开发效率,更重要的是它可以帮助我们减少BUG。有些好的开发工具,比如:idea中,对于包没有引入,会在相关的类上面标红。并且idea还有自动补全的功能,可以有效减少我们在日常开发的过程中,有些单词手动输入的时候敲错的情况发生。2 引入Findbugs插件Findbugs是一款Java静态代码分析工具,它专注于寻找真正的缺陷或者潜在的性能问题,它可以帮助java工程师提高代码质量以及排除隐含的缺陷。Findbugs运用Apache BCEL 库分析类文件,而不是源代码,将字节码与一组缺陷模式进行对比以发现可能的问题。可以直接在idea中安装FindBugs插件:之后可以选择分析哪些代码:分析结果:点击对应的问题项,可以找到具体的代码行,进行修复。Findbugs的检测器已增至300多条,被分为不同的类型,常见的类型如下:Correctness:这种归类下的问题在某种情况下会导致bug,比如错误的强制类型转换等。Bad practice:这种类别下的代码违反了公认的最佳实践标准,比如某个类实现了equals方法但未实现hashCode方法等。Multithreaded correctness:关注于同步和多线程问题。Performance:潜在的性能问题。Security:安全相关。Dodgy:Findbugs团队认为该类型下的问题代码导致bug的可能性很高。< 顺便内推几个技术大厂的机会,前、后端or测试,感兴趣可以试试>3 引入CheckStyle插件CheckStyle作为检验代码规范的插件,除了可以使用配置默认给定的开发规范,如Sun、Google的开发规范之外,还可以使用像阿里的开发规范的插件。目前国内用的比较多的是阿里的代码开发规范,我们可以直接通过idea下载插件:如果想检测某个文件:可以看到结果:阿里巴巴规约扫描包括:OOP规约并发处理控制语句命名规约常量定义注释规范Alibaba Java Coding Guidelines 专注于Java代码规范,目的是让开发者更加方便、快速规范代码格式。该插件在扫描代码后,将不符合规约的代码按 Blocker、Critical、Major 三个等级显示出来,并且大部分可以自动修复。它还基于Inspection机制提供了实时检测功能,编写代码的同时也能快速发现问题。4 用SonarQube扫描代码SonarQube是一种自动代码审查工具,用于检测代码中的错误,漏洞和代码格式上的问题。它可以与用户现有的工作流程集成,以实现跨项目分支和提取请求的连续代码检查,同时也提供了可视化的管理页面,用于查看检测出的结果。SonarQube通过配置的代码分析规则,从可靠性、安全性、可维护性、覆盖率、重复率等方面分析项目,风险等级从A~E划分为5个等级;同时,SonarQube可以集成pmd、findbugs、checkstyle等插件来扩展使用其他规则来检验代码质量。一般推荐它跟Jenkins集成,做成每天定时扫描项目中test分支中的代码问题。5 用Fortify扫描代码Fortify 是一款广泛使用的静态应用程序安全测试(SAST)工具。它具有代码扫描、漏斗扫描和渗透测试等功能。它的设计目的是有效地检测和定位源代码中的漏洞。它能帮助开发人员识别和修复代码中的安全漏洞。Fortify的主要功能:静态代码分析:它会对源代码进行静态分析,找出可能导致安全漏洞的代码片段。它能识别多种类型的安全漏洞,如 SQL 注入、跨站脚本(XSS)、缓冲区溢出等。数据流分析:它不仅分析单个代码文件,还跟踪应用程序的数据流。这有助于找到更复杂的漏洞,如未经验证的用户输入在应用程序中的传播路径。漏洞修复建议:发现潜在的安全漏洞时,它会为开发人员提供修复建议。集成支持:它可以与多种持续集成(CI)工具(如 Jenkins)和应用生命周期管理(ALM)工具(如 Jira)集成,实现自动化的代码扫描和漏洞跟踪。报告和度量:它提供了丰富的报告功能,帮助团队了解项目的安全状况和漏洞趋势。使用Fortify扫描代码的结果:一般推荐它跟Jenkins集成,定期扫描项目中test分支中的代码安全问题。6 写单元测试有些小伙伴可能会问:写单元测试可以减少代码的BUG?答案是肯定的。我之前有同事,使用的测试驱动开发模式,开发一个功能模块之前,先把单元测试写好,然后再真正的开发业务代码。后面发现他写的代码速度很快,而且代码质量很高,是一个开发牛人。如果你后期要做系统的代码重构,你只是重写了相关的业务代码,但业务逻辑并没有修改。这时,因为有了之前写好的单位测试,你会发现测试起来非常方便。可以帮你减少很多BUG。7 功能自测功能自测,是程序员的基本要求。但有些程序员自测之后,BUG还是比较多,而有些程序员自测之后,BUG非常少,这是什么原因呢?可能有些人比较粗心,有些人比较细心。其实更重要的是测试的策略。有些人喜欢把所有相关的功能都开发完,然后一起测试。这种情况下,相当于一个黑盒测试,需要花费大量的时间,梳理业务逻辑才能测试完整,大部分情况下,开发人员是没法测试完整的,可能会有很多bug测试不出来。这种做法是没有经过单元测试,直接进行了集成测试。看似节省了很多单元测试的时间,但其实后面修复BUG的时间可能会花费更多。比较推荐的自测方式是:一步一个脚印。比如:你写了一个工具类的一个方法,就测试一下。如果这个方法中,调用了另外一个关键方法,我们可以先测试一下这个关键方法。这样可以写出BUG更少的代码。8 自动化测试有些公司引入了自动化测试的功能。有专门的程序,每天都会自动测试,保证系统的核心流程没有问题。因为我们的日常开发中,经常需要调整核心流程的代码。不可能每调整一次,都需要把所有的核心流程都测试一遍吧,这样会浪费大量的时间,而且也容易遗漏一些细节。如果引入了自动化测试的功能,可以帮助我们把核心流程都测试一下。避免代码重构,或者修改核心流程,测试时间不够,或者测试不完全的尴尬。自动化测试,可以有效的减少核心流程调整,或者代码重构中的BUG。9 代码review很多公司都有代码review机制。我之前也参与多次代码review的会议,发现代码review确实可以找出很多BUG。比如:一些代码的逻辑错误,语法的问题,不规范的命名等。这样问题通过组内的代码review一般可以检查出来。有些国外的大厂,采用结对编程的模式。同一个组的两个人A和B一起开发,开发完之后,A reivew B的代码,同时B review A的代码。因为同组的A和B对项目比较熟,对对方开发的功能更有了解,可以快速找出对外代码中的一些问题。能够有效减少一些BUG。10 多看别人的踩坑分享如果你想减少日常工作中的代码BUG,或者线上事故,少犯错,少踩坑。经常看别人真实的踩坑分享,是一个非常不错的选择,可以学到一些别人的工作经验,帮助你少走很多弯路。网上有许多博主写过自己的踩坑记录,大家可以上网搜一下。最后说一句,本文总结了10种减少代码BUG的小技巧,但我们要根据实际情况选择使用,并非所有的场景都适合。
科叼
8 13 开源硬件平台
40亿QQ号,如何去重?
作者:BLACK595前言首先我们来看看如果要存储40亿QQ号需要多少内存?我们使用无符号整数存储,一个整数需要4个字节,那么40亿需要4*4000000000/1024/1024/1024≈15G,在业务中我们往往需要更多的空间。而且在Java中并不存在无符号整形,只有几个操作无符号的静态方法。1GB = 1024MB,1MB = 1024KB,1KB = 1024B, 1B = 8b很显然这种存储是不太优雅的,对于这种大数据量的去重,我们可以使用位图Bitmap。​ 顺手推几个技术大厂的机会,前、后端or测试,感兴#畅聊专区#趣就试试 BitmapBitmap,位图,首先看它的名字,比特map,首先我们听到map,一般都有去重的功能,bitmap听名字就像使用bit存储的map。确实,位图是使用bit数组表示的,它只存储0或者1,因此我们可以把全部的QQ号放到位图中,当index位置为1时表示已经存在。假如我们要判断2924357571是否存在,那么我们只需要看index为2924357571的值是否为1,如果为1则表示已经存在。位图使用1个比特表示一个数是否存在,那么使用无符号整数表示QQ号,4字节2^32-1是4294967295,内存需要4294967295/8/1024/1024≈512MB。使用Java编程时,我们使用位图一般是通过的redis,在redis中位图常用的是以下三个命其他作用大数据量去重,Bitmap其极致的空间用在大数据量去重非常合适的,除了QQ号去重,我们还可以用在比如订单号去重;爬取网站时URL去重,爬过的就不爬取了。数据统计,比如在线人员统计,将在线人员id为偏移值,为1表示在线;视频统计,将全部视频的id为偏移存储到Bitmap中。布隆过滤器(BloomFilter),布隆过滤器的基础就是使用的位图,只不过布隆过滤器使用了多个哈希函数处理,只有当全部的哈希都为1,才表示这个值存在。布隆过滤器布隆过滤器一般会使用多个哈希函数,计算出对应的hash对应多个位图下标值,如果都为1,表示这个值存在。例如hutool工具中布隆过滤器的实现类BitMapBloomFilter默认就提供了5个哈希函数。 public BitMapBloomFilter(int m) { int mNum =NumberUtil.div(String.valueOf(m), String.valueOf(5)).intValue(); long size = mNum * 1024 * 1024 * 8; filters = new BloomFilter[]{ new DefaultFilter(size), new ELFFilter(size), new JSFilter(size), new PJWFilter(size), new SDBMFilter(size) }; } 优点:相较位图,布隆过滤器使用多个hash算法,我们就可以给字符串或对象存进去计算hash了,不像位图一样只能使用整形数字看偏移位置是否为1。缺点:可能产生哈希冲突,如果判断某个位置值为1,那么可能是产生了哈希冲突,所以,布隆过滤器会有一定误差。
科叼
1 8 开源硬件平台
探究 width:100%与width:auto区别
作者:秋天的一阵风一、 width属性介绍width 属性用于设置元素的宽度。width 默认设置内容区域的宽度,但如果box-sizing  属性被设置为 border-box,就转而设置边框区域的宽度。#畅聊专区#(顺便推几个技术大厂的机会,前、后端or测试,感兴趣就试试试试 )二、 话不多说,直接上代码看效果 三、 分析比较我们给parent设置了padding:20px 内边距,给两个child都设置了margin:20px的外边距。child1的width属性是auto,child2的width属性是100%。很明显地看到两个child的不同表现,child1的宽度是可以适应的,不会溢出其父元素。child1最终的宽度值:540px=600px(父元素宽度)−20px(child1外边距)∗2−10px∗2(child1边框值)−0px(child1内边距)child1最终的宽度值: 540px = 600px(父元素宽度) - 20px (child1 外边距) * 2 - 10px *2 (child1 边框值) - 0px (child1 内边距) child1最终的宽度值:540px=600px(父元素宽度)−20px(child1外边距)∗2−10px∗2(child1边框值)−0px(child1内边距)而child2的宽度则是和父元素一样大最终溢出了其父元素。child2最终的宽度值:600px=600px(父元素宽度)child2最终的宽度值: 600px = 600px(父元素宽度) child2最终的宽度值:600px=600px(父元素宽度)四、 结论width:100% : 子元素的 content 撑满父元素的content,如果子元素还有 padding、border等属性,或者是在父元素上设置了边距和填充,都有可能会造成子元素区域溢出显示;width:auto : 是子元素的 content+padding+border+margin 等撑满父元素的 content 区域。所以,在开发中尽量还是选择 width:auto ,因为当从边距、填充或边框添加额外空间时,它将尽可能努力保持元素与其父容器的宽度相同。而width:100%将使元素与父容器一样宽。额外的间距将添加到元素的大小,而不考虑父元素。这通常会导致问题。
科叼
5 5 开源硬件平台
不够理解import和require导入的区别被diss惨了
作者:天天鸭前言在真实工作中,估计import和require大家经常见到,如果做前端业务代码,那么import更是随处可见了。但我们都是直接去使用,但是这两种方式的区别是什么呢?应用场景有什么区别呢?大部分能说出来import是ES6规范,而require是CommonJS规范,然后面试官深入问你两者编译规则有啥不一样?然后就不知道了本文一次性对import和require的模块基本概念、编译规则、基本用法差异、生态支持和性能对比等5个方面一次理清总结好,下次遇到这种问题直接举一反三。(顺便吆喝一句,技术大厂,前后端测试捞人,感兴趣来看这里) 一、模块基本概念 require: 是CommonJS模块规范,主要应用于Node.js环境。 import:是ES6模块规范,主要应用于现代浏览器和现代js开发(适用于例如各种前端框架)。 二、编译规则 require: require 执行时会把导入的模块进行缓存,下次再调用会返回同一个实例。 在CommonJS模块规范中,require默认是同步的。当我们在某个模块中使用require调用时,会等待调用完成才接着往下执行,如下例子所示。模块A代码 console.log('我是模块A的1...'); const moduleB = require('./myModuleB'); console.log('我是模块A的2'); 模块B代码 console.log('我是模块B...'); 打印顺序,会按顺序同步执行 // 我是模块A的1... // 我是模块B... // 我是模块A的2...  注意:require并非绝对是同步执行,例如在Webpack中能使用 require.ensure 来进行异步加载模块。 import:在ES6模块规范中,import默认是静态编译的,也就是在编译过程就已经确认了导入的模块是啥,因此默认是同步的。import有引用提升置顶效果,也就是放在何处都会默认在最前面。但是...., 通过import()动态引入是异步的哦,并且是在执行中加载的。 import()在真实业务中是很常见的,例如路由组件的懒加载component: () => import('@/components/dutest.vue')和动态组件const MyTest = await import('@/components/MyTest.vue');等等,import() 执行返回的是一个 Promise,所以经常会配合async/await一起用。三、基本用法差异 require: 一般不直接用于前端框架,是用于 Node.js 环境和一些前端构建工具(例如:Webpack)中1. 导入模块(第三方库) 在Node.js中经常要导入各种模块,用require可以导入模块是最常见的。例如导入一个os模块 const os = require('os'); // 使用 os.platform() 2. 导入本地写好的模块 假设我本地项目有一个名为 utils.js 的本地文件,文件里面导出一个add函数 module.exports = {   add: (a, b) => a + b, }; 在其它文件中导入并使用上面的模块 const { add } = require('../test/utils'); // 使用 add(2, 3); import: 一般都是应用于现在浏览器和各种主流前端框架(例如:Vue\react)1. 静态引入(项目中最常用) 这种情况一般适用于确定的模块关系,是在编译时解析 2. 动态引入 其实就是使用import()函数去返回一个 Promise,在Promise回调函数里面处理加载相关,例如路由的懒加载。 {   path: '/',   name: 'test',   component: () => import('@/components/dutest.vue') }, 或者动态引入一些文件(或者本地的JSON文件)  四、生态支持 require:Node.js14 之前是默认模块系统。目前的浏览器基本是不原生支持 CommonJS,都是需要通过构建工具(如 Webpack )转换才行。并且虽然目前市面上CommonJS依然广泛使用,但基本都是比较老的库,感觉被逐渐过渡了。import:import是ES6规范,并且Node.js在Node.js12开始支持ES6,Node.js14 之后是默认选项。目前现代浏览器和主流的框架(Vue、React)都支持原生ES6,大多数现代库也是,因此import是未来主流。五、性能对比ES6 支持 Tree Shaking摇树优化,因此可以更好地去除一些没用的代码,能很好减小打包体积。 所以import有更好的性能。import()能动态导入模块性能更好,而require不支持动态导入。小结对比下来发现,import不但有更好性能,而且还是Node.js14之后的默认,会是主流趋势。至此我感觉足够能举一反三了,如有哪里写的不对或者有更好建议欢迎大佬指点一二啊。
科叼
1 11 硬创社
前端项目公共组件封装思想(Vue)
作者:安静的搬砖人1. 通用组件(表单搜索 + 表格展示 + 分页器)在项目当中我们总会遇到这样的页面:页面顶部是一个表单筛选项,下面是一个表格展示数据。表格下方是一个分页器,这样的页面在我们的后台管理系统中经常所遇到,有时候可能不止一个页面,好几个页面的结构都是这种。本人记得,在 react 中的高级组件库中有这么一个组件,就实现了这么一个效果。就拿这个页面来说我们实现一下组件封装的思想:1. 首先把每个页面的公共部分抽出来,比如标题等,用 props 或者插槽的形式传入到组件中进行展示 2. 可以里面数据的双向绑定实现跟新的效果 3. 设置自定义函数传递给父组件要做上面事情1. 将公共的部分抽离出来TableContainer组件      
       
         
           
             
{{ title }}
             
                 
                                 
             
           
         
       
     
         
  这里的话利用了具名插槽插入了 navbar、table 组件,title 通过 props 的属性传入到子组件当中。进行展示,父组件                         当然这是一个非常非常简单的组件封装案例接下来我们看一个高级一点的组件封装父组件 父组件传递给子组件各种必要的属性:total(总共多少条数据)、page (当前多少页)、limit(每页多少条数据)、pageSizes(选择每页大小数组)子组件 这里的 page.sync、limit.sync 目的就是为了实现数据的双向绑定,computed 中监听 page 和 limit 的变化,子组件接收的数据通过 computed 生成的 currentPage 通过 sync 绑定到了 el-pagination 中, 点击分页器的时候会改变 currentPage 此时会调用 set 函数设置新的值,通过代码 this.$emit(update:page,value) 更新父组件中的值,实现双向的数据绑定本文是作者在闲暇的时间随便记录一下, 若有错误请指正,多多包涵。感谢支持(顺便吆喝一声,技术大厂内推,前后端测试捞人)!
科叼
6 10 硬创社
前端,就是你,赶紧通知用户刷新页面去!
前言 老板:新的需求不是上线了嘛,怎么用户看到的还是老的页面呀 窝囊废:让用户刷新一下页面,或者清一下缓存 老板:那我得告诉用户,刷新一下页面,或者清一下缓存,才能看到新的页面呀,感觉用户体验不好啊,不能直接刷新页面嘛? 窝囊废:可以解决(OS:一点改的必要没有,用户全是大聪明) 产品介绍 c端需要经常进行一些文案调整,一些老版的文字字眼可能会导致一些舆论问题,所以就需要更新之后刷新页面,让用户看到新的页面。 思考问题为什么产生 项目是基于vue的spa应用,通过nginx代理静态资源,配置了index.html协商缓存,js、css等静态文件 Cache-Control,按正常前端重新部署后, 用户 重新访问系统,已经是最新的页面。 但是绝大部份用户都是访问页面之后一直停留在此页面,这时候前端部署后,用户就无法看到新的页面,需要用户刷新页面。 产生问题 如果后端接口有更新,前端重新部署后,用户访问老的页面,可能会导致接口报错。 如果前端部署后,用户访问老的页面,可能无法看到新的页面,需要用户刷新页面,用户体验不好。 出现线上bug,修复完后,用户依旧访问老的页面,仍会遇到bug。 [removed] 解决方案 前后端配合解决 WebSocket SSE(Server-Send-Event) 纯前端方案 以下示例均以vite+vue3为例; 轮询html Etag/Last-Modified 在App.vue中添加如下代码 const oldHtmlEtag = ref(); const timer = ref(); const getHtmlEtag = async () => { const { protocol, host } = window.location; const res = await fetch(​${protocol}//${host}​, { headers: { "Cache-Control": "no-cache", }, }); return res.headers.get("Etag"); };oldHtmlEtag.value = await getHtmlEtag(); clearInterval(timer.value); timer.value = setInterval(async () => { const newHtmlEtag = await getHtmlEtag(); console.log("---new---", newHtmlEtag); if (newHtmlEtag !== oldHtmlEtag.value) { Modal.destroyAll(); Modal.confirm({ title: "检测到新版本,是否更新?", content: "新版本内容:", okText: "更新", cancelText: "取消", onOk: () => { window.location.reload(); }, }); } }, 30000); versionData.json 自定义plugin,项目根目录创建/plugins/vitePluginCheckVersion.ts import path from "path"; import fs from "fs"; export function checkVersion(version: string) { return { name: "vite-plugin-check-version", buildStart() { const now = new Date().getTime(); const version = { version: now, }; const versionPath = path.join(__dirname, "../public/versionData.json"); fs.writeFileSync(versionPath, JSON.stringify(version), "utf8", (err) => { if (err) { console.log("写入失败"); } else { console.log("写入成功"); } }); }, }; } 在vite.config.ts中引入插件 import { checkVersion } from "./plugins/vitePluginCheckVersion"; plugins: [ vue(), checkVersion(), ] 在App.vue中添加如下代码 const timer = ref() const checkUpdate = async () => { let res = await fetch('/versionData.json', { headers: { 'Cache-Control': 'no-cache', }, }).then((r) => r.json()) if (!localStorage.getItem('demo_version')) { localStorage.setItem('demo_version', res.version) } else { if (res.version !== localStorage.getItem('demo_version')) { localStorage.setItem('demo_version', res.version) Modal.confirm({ title: '检测到新版本,是否更新?', content: '新版本内容:' + res.content, okText: '更新', cancelText: '取消', onOk: () => { window.location.reload() }, }) } } }onMounted(()=>{ clearInterval(timer.value) timer.value = setInterval(async () => { checkUpdate() }, 30000) }) plugin-web-update-notification Use // vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { webUpdateNotice } from '@plugin-web-update-notification/vite'// https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), webUpdateNotice({ logVersion: true, }), ] }) 作者:李暖阳啊 原文:juejin.cn/post/7439905609312403483
科叼
0 0 开源硬件平台