开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

web server apache tomcat11-17-

发表于 2024-04-23

前言

整理这个官方翻译的系列,原因是网上大部分的 tomcat 版本比较旧,此版本为 v11 最新的版本。

开源项目

从零手写实现 tomcat minicat 别称【嗅虎】心有猛虎,轻嗅蔷薇。

系列文章

web server apache tomcat11-01-官方文档入门介绍

web server apache tomcat11-02-setup 启动

web server apache tomcat11-03-deploy 如何部署

web server apache tomcat11-04-manager 如何管理?

web server apache tomcat11-06-Host Manager App – Text Interface

web server apache tomcat11-07-Realm Configuration

web server apache tomcat11-08-JNDI Resources

web server apache tomcat11-09-JNDI Datasource

web server apache tomcat11-10-Class Loader

…

DefaultServlet 是什么?

默认的 Servlet 是一个用于提供静态资源以及显示目录列表(如果启用了目录列表)的 Servlet。

声明位置

它在 $CATALINA_BASE/conf/web.xml 中全局声明。默认情况下,它的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<servlet>
<servlet-name>default</servlet-name>
<servlet-class>
org.apache.catalina.servlets.DefaultServlet
</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

...

<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

因此,默认情况下,default servlet 在 web 应用程序启动时加载,目录列表被禁用,并且调试被关闭。

如何更改设置?

DefaultServlet 允许以下 initParameters:

属性 描述
debug 调试级别。除非您是 Tomcat 开发者,否则没有多大用处。截至目前,有用的值为 0、1、11。 [0]
listings 如果没有欢迎文件,是否可以显示目录列表?值可以是 true 或 false [false]
precompressed 如果存在文件的预压缩版本(文件名附加了 .br 或 .gz),并且用户代理支持匹配的内容编码(br 或 gzip),并且启用了此选项,则 Tomcat 将提供预压缩文件。 [false]
readmeFile 如果显示目录列表,则也可以显示 readme 文件。此文件会原样插入,因此它可能包含 HTML。
globalXsltFile 如果要自定义目录列表,可以使用 XSL 转换。此值是一个相对文件名(到 CATALINABASE/conf/或CATALINA_BASE/conf/ 或 CATALINABASE/conf/或CATALINA_HOME/conf/),将用于所有目录列表。此设置可以针对每个上下文和/或每个目录进行覆盖。见下文的 contextXsltFile 和 localXsltFile。
contextXsltFile 您还可以通过配置 contextXsltFile 来根据上下文自定义目录列表。这必须是一个上下文相对路径(例如:/path/to/context.xslt),指向具有 .xsl 或 .xslt 扩展名的文件。这会覆盖 globalXsltFile。如果此值存在但文件不存在,则将使用 globalXsltFile。如果 globalXsltFile 不存在,则将显示默认目录列表。
localXsltFile 您还可以通过配置 localXsltFile 来根据目录自定义目录列表。这必须是目录列表所在目录中具有 .xsl 或 .xslt 扩展名的文件。这会覆盖 globalXsltFile 和 contextXsltFile。如果此值存在但文件不存在,则将使用 contextXsltFile。如果 contextXsltFile 不存在,则将使用 globalXsltFile。如果 globalXsltFile 不存在,则将显示默认目录列表。
input 在读取要提供的资源时的输入缓冲区大小(以字节为单位)。 [2048]
output 在写入要提供的资源时的输出缓冲区大小(以字节为单位)。 [2048]
readonly 此上下文是否为“只读”,因此会拒绝像 PUT 和 DELETE 这样的 HTTP 命令? [true]
fileEncoding 在读取静态资源时要使用的文件编码。 [platform default]
useBomIfPresent 如果静态文件包含字节顺序标记(BOM),是否应该优先使用它来确定文件编码,而不是 fileEncoding。此设置必须是 true(删除 BOM 并优先使用它而不是 fileEncoding)、false(删除 BOM 但不使用它)或 pass-through(不使用 BOM 并且不删除它)。 [true]
sendfileSize 如果使用的连接器支持 sendfile,这表示将使用 sendfile 的最小文件大小(以 KiB 为单位)。使用负值始终禁用 sendfile。 [48]
useAcceptRanges 如果为真,则会在响应适当时设置 Accept-Ranges 标头。 [true]
showServerInfo 在启用目录列表时,是否应向客户端发送响应中呈现服务器信息。 [true]
sortListings 服务器是否应该对目录中的列表进行排序。 [false]
sortDirectoriesFirst 服务器是否应该在所有文件之前列出所有目录。 [false]
allowPartialPut 服务器是否应将具有 Range 标头的 HTTP PUT 请求视为部分 PUT?请注意,虽然 RFC 7233 澄清了 Range 标头仅对 GET 请求有效,但 RFC 9110(废除了 RFC 7233)现在允许部分 PUT。 [true]
directoryRedirectStatusCode 当进行目录重定向(缺少尾部斜杠)时,使用此作为 HTTP 响应代码。 [302]

(部分属性已省略)

如何自定义目录列表?

您可以使用 localXsltFile、contextXsltFile 或 globalXsltFile,DefaultServlet 将创建一个 XML 文档,并根据 XSLT 文件中提供的值运行它进行 XSL 转换。首先检查 localXsltFile,然后是 contextXsltFile,最后是 globalXsltFile。如果没有配置 XSLT 文件,则使用默认行为。

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<listing>
<entries>
<entry type='file|dir' urlPath='aPath' size='###' date='gmt date'>
fileName1
</entry>
<entry type='file|dir' urlPath='aPath' size='###' date='gmt date'>
fileName2
</entry>
...
</entries>
<readme></readme>
</listing>

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
xml复制代码<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0">

<xsl:output method="html" html-version="5.0"
encoding="UTF-8" indent="no"
doctype-system="about:legacy-compat"/>

<xsl:template match="listing">
<html>
<head>
<title>
Sample Directory Listing For
<xsl:value-of select="@directory"/>
</title>
<style>
h1 {color : white;background-color : #0086b2;}
h3 {color : white;background-color : #0086b2;}
body {font-family : sans-serif,Arial,Tahoma;
color : black;background-color : white;}
b {color : white;background-color : #0086b2;}
a {color : black;} HR{color : #0086b2;}
table td { padding: 5px; }
</style>
</head>
<body>
<h1>Sample Directory Listing For
<xsl:value-of select="@directory"/>
</h1>
<hr style="height: 1px;" />
<table style="width: 100%;">
<tr>
<th style="text-align: left;">Filename</th>
<th style="text-align: center;">Size</th>
<th style="text-align: right;">Last Modified</th>
</tr>
<xsl:apply-templates select="entries"/>
</table>
<xsl:apply-templates select="readme"/>
<hr style="height: 1px;" />
<h3>Apache Tomcat/11.0</h3>
</body>
</html>
</xsl:template>


<xsl:template match="entries">
<xsl:apply-templates select="entry"/>
</xsl:template>

<xsl:template match="readme">
<hr style="height: 1px;" />
<pre><xsl:apply-templates/></pre>
</xsl:template>

<xsl:template match="entry">
<tr>
<td style="text-align: left;">
<xsl:variable name="urlPath" select="@urlPath"/>
<a href="{$urlPath}">
<pre><xsl:apply-templates/></pre>
</a>
</td>
<td style="text-align: right;">
<pre><xsl:value-of select="@size"/></pre>
</td>
<td style="text-align: right;">
<pre><xsl:value-of select="@date"/></pre>
</td>
</tr>
</xsl:template>

</xsl:stylesheet>

如何保护目录列表?

对于每个个别的 web 应用程序,可以使用 web.xml。请参阅 Servlet 规范的安全部分。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

web server apache tomcat11-16-

发表于 2024-04-23

前言

整理这个官方翻译的系列,原因是网上大部分的 tomcat 版本比较旧,此版本为 v11 最新的版本。

开源项目

从零手写实现 tomcat minicat 别称【嗅虎】心有猛虎,轻嗅蔷薇。

系列文章

web server apache tomcat11-01-官方文档入门介绍

web server apache tomcat11-02-setup 启动

web server apache tomcat11-03-deploy 如何部署

web server apache tomcat11-04-manager 如何管理?

web server apache tomcat11-06-Host Manager App – Text Interface

web server apache tomcat11-07-Realm Configuration

web server apache tomcat11-08-JNDI Resources

web server apache tomcat11-09-JNDI Datasource

web server apache tomcat11-10-Class Loader

…

介绍

Tomcat 使用 JMX MBeans 技术来实现对 Tomcat 的可管理性。

Catalina 的 JMX MBeans 描述在每个包中的 mbeans-descriptors.xml 文件中。

您需要为自定义组件添加 MBean 描述,以避免出现 “ManagedBean is not found” 异常。

添加 MBean 描述

您也可以在与其描述的类文件相同的包中的 mbeans-descriptors.xml 文件中为自定义组件添加 MBean 描述。

mbeans-descriptors.xml 的允许语法由 DTD 文件定义。

自定义 LDAP 认证 Realm 的条目可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<mbean         name="LDAPRealm"
className="org.apache.catalina.mbeans.ClassNameMBean"
description="Custom LDAPRealm"
domain="Catalina"
group="Realm"
type="com.myfirm.mypackage.LDAPRealm">

<attribute name="className"
description="Fully qualified class name of the managed object"
type="java.lang.String"
writeable="false"/>

<attribute name="debug"
description="The debugging detail level for this component"
type="int"/>
.
.
.

</mbean>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字节面试:如何解决MQ消息积压问题?

发表于 2024-04-23

MQ(Message Queue)消息积压问题指的是在消息队列中累积了大量未处理的消息,导致消息队列中的消息积压严重,超出系统处理能力,影响系统性能和稳定性的现象。

1.消息积压是哪个环节的问题?

MQ 执行有三大阶段:

  1. 消息生产阶段。
  2. 消息存储阶段。
  3. 消息消费阶段。

很显然,消息堆积是出现在第三个消息消费阶段的。

2.如何解决?

消息积压问题的处理取决于消息积压的类型,例如,消息积压是突发性消息积压问题?还是缓慢持续增长的消息积压问题?不同的问题的解决方案略有不同,接下来我们一起来看。

2.1 突发性消息积压问题

突发性消息积压问题的解决思路是:先快速解决掉消息积压问题,然后再排查问题制定相应的解决方案,所以我们可以使用以下手段进行处理:

  1. 水平扩容消费者(添加消费者数量)解决消息积压问题。
  2. 使用限流手段,限制生产者生产消息的速度。
  3. 通过日志或监控分析消息积压的问题,如果是消费代码出现的问题,优化代码提升消费速度。

2.2 缓慢持续增长的消息积压问题

缓慢持续增长的消息积压问题,则是使用监控机制早早发现问题,然后快速排查和定位消息积压问题予以解决。

3.总体解决方案

总的来说,消息积压问题的解决方案有以下几个:

  1. 水平扩展消费者:消费者数量增多,则可以并行提升消息消费的速度,从而避免消息积压的问题。
  2. 优化消费者处理速度:提升消费者的消费速度也可以避免消息积压的问题,它的解决方案有:
    • 优化消费者处理消息的逻辑,减少不必要的计算和 I/O 操作。
    • 对于可以并行处理的任务,使用多线程或异步处理来提高吞吐量。
  3. 限流生产者和使用背压机制:
    • 在生产者端实施限流策略,确保消息产生的速度不会超过系统的处理能力。
    • 使用背压机制,即当消息队列达到某个阈值时,通知生产者降低发送速率或暂停发送。
  4. 使用死信队列:在消费者处理消息出现失败或超时的情况下,加入消息重试机制或将异常消息放入死信队列,避免异常消息一直占用队列资源。
  5. 监控和告警:设置合理的告警阈值,当消息积压达到一定程度时及时发出告警,以便快速响应和处理。

课后思考

在 Kafka 中,水平扩展消费者一定要解决消息积压的问题吗?为什么?

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

邂逅MySQL及环境搭建

发表于 2024-04-23

一.数据库的出现与发展


🧙技术的出现往往是为了解决某些问题,数据库系统的出现也是这样,中国有句俗语“好记性不如烂笔头”这其实在一定程度上面反映了,人们对信息记录的需求,在远古时代,人们就懂得了这个道理,那个时候人们使用龟甲和兽骨记录信息,这些用于记录的文字就是我们所说的甲骨文,随着社会和技术的发展,人们发明了纸张,开始在纸张上面通过较现代的文字来记录信息,到现代科学家们发明了计算机,并且在计算机中建立了文件系统,可以通过文件名和日期等搜索有组织的文件,来实现对大数据量的信息进行管理,但是渐渐的发现文件管理的方式并不能满足他们的需求,因为文件系统有很多缺陷,数据的组织和查询等复杂操作很难实现~因此就有了数据库。

二.为什么需要数据库


🦊文件系统的缺点,为什么不使用文件系统来管理软件中的数据?

  1. 很难以合适的方式组织数据(多表之间的关系合理组织)。
  2. 并且对数据进行增删改查的复杂操作(虽然一些简单的确实可以),并且保证操作的原子性。
  3. 很难进行数据共享,比如一个数据库需要对多个程序服务,如何进行很好的数据共享。
  4. 需要考虑如何对数据库进行高效的备份,迁移,恢复等操作。

👽任何的软件系统都需要存储大量的数据,这些数据通常是非常复杂和庞大的。

  1. 比如用户的信息包括年龄,性别,地址,身份证号出生日期等等。
  2. 比如商品信息包含商品名称,描述,价格,分类标签,商品图片等。
  3. 比如歌曲信息包括歌曲的名称,歌手,专辑,歌曲时长,歌曲信息,封面图片等等。

🥴数据库通俗来讲就是一个用来存储数据的仓库,本质上也是一个软件,一个程序。

三.数据库都有哪些?


🔔我们通常会把数据库分为两类:关系型数据库和非关系型数据库。

😭关系型数据库:MySQL,Oracle,DB2,SQL Server,Postage SQL等。

  1. 关系型数据库通常我们会创建很多个二维数据表;
  2. 数据表之间相互关联起来,形成一对一,一对多,多对多的关系。
  3. 之后可以利用SQL在多张表中查询我们所需的数据。

😊非关系型数据库:MongoDB,Redis,Memcached,HBase等;

  1. 非关系型数据库的英文其实是Not only SQL,也简称为NoSQL;
  2. 相对而言非关系型数据库简单一点,存储数据也更加自由。
  3. NoSQL的是基于Key-Value的对应关系,并且查询过程中不需要经过SQL解析。

🎯如何选择他们哪?具体的选择会根据不同的项目进行综合的分析。

  1. 目前公司后端开发(Node,Java,Go)还是以关系型数据库为主的。
  2. 比较常用到关系型数据库的场景是在爬取大量数据进行存储的时候,会比较常见。

四.认识MySQL


👽MySQL原本是一个开源的数据库,原开发者为瑞典的MySQLAB公司,在2008年被Sun公司收购,在2009年,Sun被Oracle收购,所以目前MySQL归属于Oracle。

🎪MySQL是一个关系型数据库,其实本质上就是一个软件,一个程序。

  1. 这个程序管理着多个数据库;
  2. 每个数据库可以有多张表;
  3. 每张表可以有多条数据;

😶‍🌫️MySQL的数据组织方式如下:

👽MySQL的下载与安装:MySQL安装教程

🦊客户端-服务器架构:MySQL的架构基本方式是客户端-服务器的架构,客户就相当于我们使用的微信,服务器就相当于微信的服务器端,MySQL的客户端包含很多种,包括MySQL交互终端,Navicat都是客户端。

五.终端操作MySQL的方式


🥴在cmd中直接输入MySQL然后点击MySQL提供的本地终端,输入密码连接。

然后我们就可以其他命令show databases;来查看MySQL中的的数据库有哪些。

🤡直接在cmd终端中输入命令的同时输入用户名和密码来进行连接,比较方便快捷

🔔MySQL中的默认数据库,我们从上面使用show databases;中展示了许多数据库,有些数据库并不是我们自己创建的,那么这些数据库是做什么用的哪?

  1. information_schema:信息数据库,其中包括MySQL在维护的其他数据库,表,列,访问权限等信息。
  2. performance_schema:性能数据库,记录着MySQL Server数据库引擎在运行过程中的一些性能消耗相关的信息。
  3. mysql:用于存储数据库管理者的用户信息,权限信息等一些日志信息。
  4. sys:相当于一个简易版的performace_schema,将性能数据库中的数据汇总成更容易理解的形式。

🚨如果你平时开发中如果感觉数据库的查询比较慢的时候,可以通过性能数据库的信息来定位问题。

六.终端创建数据库表


😭在终端创建数据库,首先连接MySQL数据库,然后在交互终端中输入如下信息,数据库名为music_db

1
create database music_db;

🎯创建完毕之后需要使用这个数据库

1
use music_db;

😊当我们使用了这个数据库后就可以在这个数据库中进行相应的操作了,我们来建张表。

1
2
3
4
create table t_singer(
name varchar(10),
age int
);

🎪我们如何进行查看这个表哪?使用如下命令就可以查看刚才建的表了。

1
show tables;

七.GUI工具介绍


🥴我们会发现在终端操作数据库有很多不方便的地方:语句写出来没有高亮,并且不会有任何的提示, 复杂的语句分成多行,格式看起来并不美观,很容易出现错误,终端中查看所有的数据库或者表非常的不直观和不方便;

😶‍🌫️所以在开发中,我们可以借助于一些GUI工具来帮助我们连接上数据库,之后进行操作。

👽常见的MySQL的GUI工具有很多,这里推荐几款。

  1. Navicat:个人最喜欢的一款工具,目前收费。
  2. SQLYog:一款免费的SQL工具。
  3. TablePlus:常用的功能都有,但是会有一些限制,比如只能开两个标签页。

🤡推荐使用Navicat,文章中也会使用它,因为比较好用界面优美,功能强大,希望大家支持Navicat正版,后续还可以试试DBeaver也是很好用的工具。并且开源免费。Navicat安装教程

八.Navicat的基本使用


🦊首先我们打开Navicat然后使用Navicat连接本地MySQL数据库,进行相关数据库内容的操作。

🧙然后输入我们的个人信息,连接名自己定义就可以,其他东西不要乱动,然后测试连接,连接成功后就是如下:

😊我们以后写SQL基本不会直接在黑框终端中书写,因为容易出错,且难以查看,我们会直接在Navicat中书写。

我们可以在如上图的内容中建议查询的内容,可以在里面编写SQL语句,用来创建表,执行SQL等等。

其实就是我们在黑框中输入的内容,下载可以直接在这里写就可以了,其实和黑框终端中一样

九.阶段自我测试


🧙基本练习内容,分别通过终端和Navicat分别练习一遍,达到不看文章中的代码熟练写出来,并且保证正确。

  1. 通过终端创建数据库,命名为TestDB;
  2. 然后使用进入这个数据库;
  3. 在当前数据库中创建一个表,表名为testTable;
  4. 查看这个表中的内容。
  5. 查看使用了哪个数据库。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JS 没有枚举,但 JS 可以创建枚举

发表于 2024-04-23

大家好,这里是大家的林语冰。欢迎持续关注“前端俱乐部”。

每周的星期(周一、周二二、…、周日)、每年的季节(春夏秋冬)和基本方位(东西南北)等等,都是有限值的集合。

当变量的值来自一组有限的预定义常量时,此乃枚举的用武之地。枚举使我们规避“魔术数字”和“魔术字符串”等反模式。

大多数编程语言都原生支持枚举数据类型。虽然目前 JS 自己并不支持,但好在 TS 内置了枚举。

有趣的是,当我们将 TS 编译为 JS 之后,就会发现 TS 的枚举其实也是用原生 JS 来模拟的。

本文共享的是,在 JS 中创建枚举的若干方案及其利弊:

  • 基于普通对象的枚举
  • 枚举值类型
  • 基于 Object.freeze() 的枚举
  • 基于 Proxy 的枚举
  • 基于类的枚举

00-wall.png

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 4 Ways to Create an Enum in JavaScript。

基于普通对象的枚举

枚举是一种定义一组有限命名常量的数据结构。每个常量都可以通过其名称读写。

让我们考虑一下猫猫的体积:Small、Medium 和 Large。

在 JS 中创建枚举的一种简单方法(尽管不是最佳实践),是使用普通 JS 对象。

1
2
3
4
5
6
7
8
js复制代码const Sizes = {
Small: '薛定谔',
Medium: '盯裆猫',
Large: '龙猫'
}
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

Sizes 是一个基于普通 JS 对象的枚举,它有 3 个命名常量:

  • Sizes.Small
  • Sizes.Medium
  • Sizes.Large

Sizes 也是一个字符串枚举,因为命名常量的值是字符串:

  • '薛定谔'
  • '盯裆猫'
  • '龙猫'

要读写命名常量的值,请使用对象属性操作符。举个栗子,Sizes.Medium 的值是 '盯裆猫'。

枚举更具可读性、更直观,且消除了“魔术字符串”或“魔术数字”的滥用。

利弊

普通对象枚举之所以有吸引力,是因为它十分简单:只需定义一个键值对象,枚举就欧了。

但在大型代码库中,我们可能会意外修改枚举对象,这会影响 App 的运行时。

1
2
3
4
js复制代码const size1 = Sizes.Medium
const size2 = (Sizes.Medium = '柴郡猫') // 意外修改!

console.log(size1 === Sizes.Medium) // false

Sizes.Medium 枚举值被意外修改。

size1 使用 Sizes.Medium 初始化时,不再等于 Sizes.Medium!

基于普通对象的枚举无法规避此类意外修改。

让我们瞄一下字符串和 Symbol 枚举,以及如何冻结枚举对象,从而规避意外修改。

枚举值类型

除了字符串类型之外,枚举的值还可以是数字:

1
2
3
4
5
6
7
8
js复制代码const Sizes = {
Small: 0,
Medium: 1,
Large: 2
}
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

上述示例中的 Sizes 枚举是数字枚举,因为值为数字:0、1、2。

我们还可以创建 Symbol 枚举:

1
2
3
4
5
6
7
8
js复制代码const Sizes = {
Small: Symbol('薛定谔'),
Medium: Symbol('盯裆猫'),
Large: Symbol('龙猫')
}
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

使用 Symbol 的福利在于,Symbol 都独一无二。这意味着,我们必须使用枚举本身来比较枚举:

1
2
3
4
js复制代码const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true
console.log(mySize === Symbol('盯裆猫')) // false

使用 Symbol 枚举的短板在于,JSON.stringify() 会将 Symbol 序列化为 null、undefined,或者跳过 Symbol 值的属性:

1
2
3
4
5
6
7
8
js复制代码const str1 = JSON.stringify(Sizes.Small)
console.log(str1) // undefined

const str2 = JSON.stringify([Sizes.Small])
console.log(str2) // '[null]'

const str3 = JSON.stringify({ size: Sizes.Small })
console.log(str3) // '{}'

下述示例中,我会使用字符串枚举。但大家可以按需使用任意值类型。

如果大家不受限于枚举值类型,那么优先选择字符串即可。字符串比数字和 Symbol 更易调试。

基于 Object.freeze() 的枚举

保护枚举对象免遭修改的优秀方案之一是冻结它。当对象被冻结时,您无法修改该对象,或者向该对象添加新属性。换而言之,该对象变为只读对象。

在 JS 中,Object.freeze() 工具函数可以冻结对象。让我们冻结 Sizes 枚举:

1
2
3
4
5
6
7
8
js复制代码const Sizes = Object.freeze({
Small: '薛定谔',
Medium: '盯裆猫',
Large: '龙猫'
})
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

const Sizes = Object.freeze({ ... }) 创建一个冻结对象。即使对象被冻结,我们也可以自由读写枚举值:const mySize = Sizes.Medium。

利弊

如果枚举属性被意外修改,那么 JS 会报错(严格模式下):

1
2
js复制代码const size1 = Sizes.Medium
const size2 = (Sizes.Medium = 'foo') // 报错

语句 const size2 = Sizes.Medium = 'foo' 对 Sizes.Medium 属性意外赋值。

因为 Sizes 是一个冻结对象,JS(严格模式下)会报错:

1
md复制代码TypeError: Cannot assign to read only property 'Medium' of object <Object>

冻结对象枚举可以规避意外修改。

不过,还有一个问题。如果我们不小心拼错了枚举常量,那么结果会变成 undefined:

1
js复制代码console.log(Sizes.Med1um) // undefined

Med1um 是 Medium 的错误拼写,Sizes.Med1um 表达式结果为 undefined,而不是抛出有关不存在的枚举常量的错误。

让我们瞄一下基于 Proxy 的枚举如何解决此问题。

基于 Proxy 的枚举

一个有趣的、也是我最爱的实现是基于 Proxy 的枚举。

Proxy 是一种特殊对象,它包装一个对象,修改对原始对象的操作行为。Proxy 不会改变原始对象的结构。

枚举代理拦截枚举对象上的读写操作,并且:

  • 访问不存在的枚举值时会报错
  • 变更枚举对象属性时会报错

下面是一个工厂函数的实现,它接受一个普通的枚举对象,并返回一个 Proxy 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码// enum.js
export function Enum(baseEnum) {
return new Proxy(baseEnum, {
get(target, name) {
if (!baseEnum.hasOwnProperty(name)) {
throw new Error(`此枚举中不存在 ${name} 枚举值`)
}
return baseEnum[name]
},
set(target, name, value) {
throw new Error('无法向此枚举中添加新的枚举值')
}
})
}

Proxy 的 get() 方法会拦截读取操作,如果属性不存在就会报错。

set() 方法拦截存写操作,且只是为了报错。它旨在保护枚举对象规避存写操作的影响。

让我们将 Sizes 对象枚举包装到 Proxy 中:

1
2
3
4
5
6
7
8
9
10
js复制代码import { Enum } from './enum'

const Sizes = Enum({
Small: '薛定谔',
Medium: '盯裆猫',
Large: '龙猫'
})
const mySize = Sizes.Medium

console.log(mySize === Sizes.Medium) // true

代理枚举的工作方式与普通对象枚举一毛一样。

利弊

虽然但是,代理枚举不会被意外重写,或读写不存在的枚举常量:

1
2
js复制代码const size1 = Sizes.Med1um // 报错:常量不存在
const size2 = (Sizes.Medium = '柴郡猫') // 报错:只读枚举

Sizes.Med1um 会报错,因为枚举中不存在 Med1um 常量名。

Sizes.Medium = '柴郡猫' 会报错,因为枚举属性被修改。

代理枚举的短板在于,我们必须导入 Enum 工厂函数,并将枚举对象包装进去。

基于类的枚举

创建枚举的另一种有趣方法是使用 class。

基于类的枚举包含一组静态字段,其中每个静态字段代表一个常量名枚举。每个枚举常量的值本身就是该类的一个实例。

我们使用 Sizes 类来实现枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码class Sizes {
static Small = new Sizes('薛定谔')
static Medium = new Sizes('盯裆猫')
static Large = new Sizes('龙猫')
#value

constructor(value) {
this.#value = value
}

toString() {
return this.#value
}
}

const mySize = Sizes.Small

console.log(mySize === Sizes.Small) // true
console.log(mySize instanceof Sizes) // true

Sizes 是代表枚举的类。枚举常量是类中的静态字段,比如 static Small = new Sizes('薛定谔')。

Sizes 类的每个实例还有一个私有字段 #value,它表示枚举的原始值。

基于类的枚举的福利之一在于,能够在运行时使用 instanceof 操作符确定该值是否为枚举。举个栗子,mySize instanceof Sizes 的计算结果为 true,因为 mySize 是一个枚举值。

基于类的枚举的比较是基于实例的(普通枚举、冻结枚举或代理枚举则是原始比较):

1
2
3
js复制代码const mySize = Sizes.Small

console.log(mySize === new Sizes('small')) // false

Sizes.Small 不等于 new Sizes('薛定谔')。

Sizes.Small 和 new Sizes('薛定谔') 即使具有相同的 #value,也是不同的对象实例。

利弊

基于类的枚举无法规避重写,或读写不存在的常量命名枚举。

1
2
js复制代码const size1 = Sizes.medium // 允许读写不存在的枚举值
const size2 = (Sizes.Medium = 'foo') // 枚举值允许意外重写

但我们可以控制新实例的创建,举个栗子,通过计算构造函数内创建的实例数量。如果创建的实例超过 3 个就报错。

当然了,尽量简化枚举的实现。枚举是简单的数据结构。

总结

JS 中创建枚举有 4 种方案。

最简单的方法是使用普通 JS 对象:

1
2
3
4
5
js复制代码const MyEnum = {
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
}

普通对象枚举适合小型项目或快速演示。

如果想保护枚举对象规避意外重写,第二个选项是冻结普通对象:

1
2
3
4
5
js复制代码const MyEnum = Object.freeze({
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
})

冻结对象枚举适用于希望确保枚举不会意外修改的中大型项目。

第三种选择是 Proxy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码// enum.js
export function Enum(baseEnum) {
return new Proxy(baseEnum, {
get(target, name) {
if (!baseEnum.hasOwnProperty(name)) {
throw new Error(`"${name}" value does not exist in the enum`)
}
return baseEnum[name]
},
set(target, name, value) {
throw new Error('Cannot add a new value to the enum')
}
})
}

// index.js
import { Enum } from './enum'

const MyEnum = Enum({
Option1: 'option1',
Option2: 'option2',
Option3: 'option3'
})

代理枚举适用于中大型项目,更好地保护枚举规避重写,或者读写不存在的命名常量。

代理枚举是我的个人偏好。

第四个选项是使用基于类的枚举,其中每个命名常量都是该类的一个实例,并存储为该类的静态属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码class MyEnum {
static Option1 = new MyEnum('option1')
static Option2 = new MyEnum('option2')
static Option3 = new MyEnum('option3')
#value

constructor(value) {
this.#value = value
}

toString() {
return this.#value
}
}

如果您喜欢类,基于类的枚举也能奏效。虽然但是,基于类的枚举的鲁棒性低于冻结或代理枚举。

欢迎持续关注“前端俱乐部”!坚持阅读,自律打卡,每天一次,进步一点。

谢谢大家的点赞,掰掰~

26-cat.gif

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

开源了!免费合规国内可用的 OpenAI-API 来了

发表于 2024-04-23

你还在烦恼不能自主构建和调试AI模型吗?你还在为自己的AI项目寻找合适的工具和框架吗?如果你已有一定的AI和python基础,我有一个好消息要告诉你:lang2openai已经开源了!这个开源项目让你可以按照openai的方式使用所有模型。下面让我们深度了解一下lang2openai。

前言

在详细介绍lang2openai之前,让我们先来了解一下背景知识。当前市场上有许多API适配工具,比如one-api,但它们往往存在的问题是复杂性高、依赖多。lang2openai则是基于LangChain实现了统一接口标准的服务提供。这个项目是由AI小智创建和维护的。大家可以自由获取和使用这个资源。

让我们通过以下链接获取这个项目:github.com/q2wxec/lang…

接下来,我将带你一步步走进lang2openai项目的世界,从它的设计理念谈起,再到如何部署和使用它,最后探索哪些模型已得到支持。

项目概览

lang2openai是一个用于标准化适配的项目,它主要侧重于将llm(large language model),embedding,rerank等实现统一到一个标准接口。这样做的好处是显而易见的,你可以通过一套接口与不同的模型进行交互,无需为每个模型编写特定的代码。

为何选择lang2openai

这个项目的优势有以下几点:

  • 简单性:只进行接口协议转换,无数据库或中间件依赖。
  • RAG支持:除了对话适配,还进行了embedding和rerank的接口适配。
  • 可进化性:基于LangChain标准,支持所有LangChain或厂商适配的模型。
  • function calling:基于提示词工程实现的功能调用适配。

如何使用lang2openai

使用lang2openai相当直观。你首先需要将代码下载到本地,配置key。然后根据DEPLOY.md中的说明设置你的项目。

安装教程

安装过程并不复杂,以下是基本的步骤:

  1. 复制GitHub仓库到本地。
  2. 安装必要的Python库。
  3. 配置你的API key和其他相关参数。
  4. 启动服务。

接口示例

lang2openai提供了一系列的API端点,包括:

  • /v1/completions:触发语言模型的文本完成。
  • /v1/chat/completions:用于处理聊天形式的请求。
  • /v1/embeddings:提供文本的嵌入表示。
  • /v1/rerank:对一组文档进行重排序。

我们可以使用curl或任何HTTP客户端来与这些端点进行交互。

总结

无论你是AI项目的初学者,还是寻找新工具的专业开发者,lang2openai都将是你的好帮手。现在就通过github.com/q2wxec/lang…获取lang2openai,当然也希望你能点上star,项目后续将会持续迭代更新,开始你的AI之旅吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Llama 3 开源了「GitHub 热点速览」

发表于 2024-04-23

近日,Meta(原 Facebook)开源了他们公司的新一代大模型 Llama 3,虽然目前只放出了 8B 和 70B 两个版本,但是在评估结果上已经优于 Claude 3 Sonnet、Mistral Medium 和 GPT-3.5 等大模型。Meta 官方还表示,这些只是开胃菜,更强的 400B 参数的模型已经在训练中了,预计几个月后将和大家见面(开源与否尚不明确)。

说回上周的热门开源项目,最近基于 LLM 构建知识库的开源项目很火,但我一个都没收录。因为如果是本地起大模型效果不好,请求大模型的 API 又不免费,所以我找到了一个 OpenAI API 反向代理开源项目,可用来实现免费白嫖 OpenAI API。内容也是知识库的关键,Reader 能够将网页内容转化成 LLM 友好的文本。对于没有编程基础的小伙伴,这有一个 30-Days-Of-Python 的开源教程,学它!当然,学习之余也可以娱乐一下,比如试试‘无名杀’,这是一款类似于三国杀的开源卡牌游戏。

  • 本文目录
      1. 开源新闻
        • 1.1 Meta 开源 Llama 3 大模型
      1. 开源热搜项目
        • 2.1 OpenAI API 免费反向代理:ChatGPT
        • 2.2 一门新兴的系统级编程语言:Zig
        • 2.3 将网页内容转化成 LLM 友好的文本:Reader
        • 2.4 三国杀类型的卡牌游戏:noname
        • 2.5 为期 30 天的 Python 编程挑战:30-Days-Of-Python
      1. HelloGitHub 热评
        • 3.1 自定义 Windows 任务栏透明度的小工具:TranslucentTB
        • 3.2 跨平台的手写笔记和绘图应用:Rnote
      1. 结尾
  1. 开源新闻

1.1 Meta 开源 Llama 3 大模型

今年初,扎克伯格就曾公开解释过:Meta 为什么开源 Llama 模型?

  1. 改进模型:开源可以借助社区的力量持续提升模型的质量,因为社区的反馈和审查有助于安全性和运行效率的提升,而这对每个人都有益。
  2. 产品发展:虽然开源并不排除将模型转化为商业产品的可能性,开源模型的领导者可以将社区创新整合进自家产品中,提高产品竞争力。
  3. 行业标准:开源软件有潜力成为行业标准,从而促进技术发展和统一性。
  4. 吸引人才:由于开发者和研究人员更倾向于参与开源项目,开源策略可以帮助公司吸引和招聘到行业内的优秀人才。

最新发布的 Llama 3 相较于 Llama 2 在参数规模、训练数据集、模型架构(GQA)、性能、多语言支持、推理和代码生成方面都有所提升,但现在对中文支持的不是很好,而且 meta.ai 上用的还是 Llama 2。

GitHub 地址:github.com/meta-llama/…

  1. 开源热搜项目

2.1 OpenAI API 免费反向代理:ChatGPT

主语言:TypeScript,Star:3.7k,周增长:1k

虽然现在无需登陆就可以免费使用 ChatGPT(gpt-3.5-turbo 模型),但如果是想用接口的话还要收费的。该项目就是基于免费的 ChatGPT 网站服务,将其转化成免费的 ChatGPT API,接口返回和官方一致,支持 Docker 部署。需要注意的是部署的服务器,要在 OpenAI 服务支持的国家和地区。

GitHub 地址→github.com/PawanOsman/…

2.2 一门新兴的系统级编程语言:Zig

主语言:Zig,Star:30k,周增长:300

这是一种命令式、通用、静态类型、编译的系统编程语言,注重性能、安全和可读性。它支持编译时泛型与反射、交叉编译以及手动存储器管理,目标为改进 C 语言,可以轻松地和 C 语言的代码库配合工作。Zig 简洁且直接,没有隐式控制流、没有隐式内存分配、没有预处理器、没有宏,特别适合用于开发编译器、操作系统内核、桌面应用、性能敏感的应用、嵌入式系统等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
zig复制代码const std = @import("std");
const parseInt = std.fmt.parseInt;

test "parse integers" {
const input = "123 67 89,99";
const ally = std.testing.allocator;

var list = std.ArrayList(u32).init(ally);
// Ensure the list is freed at scope exit.
// Try commenting out this line!
defer list.deinit();

var it = std.mem.tokenizeAny(u8, input, " ,");
while (it.next()) |num| {
const n = try parseInt(u32, num, 10);
try list.append(n);
}

const expected = [_]u32{ 123, 67, 89, 99 };

for (expected, list.items) |exp, actual| {
try std.testing.expectEqual(exp, actual);
}
}

GitHub 地址→github.com/ziglang/zig

2.3 将网页内容转化成 LLM 友好的文本:Reader

主语言:TypeScript,Star:3k,周增长:2.5k

该项目可以将指定的 URL 内容转化为干净、LLM 友好的文本,从而提高 Agent 和 RAG 系统的输入质量,可作为构建知识库的一环。我试用了一下,内容提取效果不错,但是对于需要登陆才能访问的 URL 就不行了。

GitHub 地址→github.com/jina-ai/rea…

2.4 三国杀类型的卡牌游戏:noname

主语言:JavaScript,Star:1.9k,周增长:100

「无名杀」是一款以三国为背景的卡牌策略游戏,它完全免费、无广告,玩法和三国杀一样,但自由度高很多,有海量武将可供玩家选择,还支持自制武将和技能,提供了身份、国战、斗地主、塔防、单挑、联机等游戏模式。

GitHub 地址→github.com/libccy/nona…

2.5 为期 30 天的 Python 编程挑战:30-Days-Of-Python

主语言:Python,Star:31k,周增长:600

该项目是帮助人们在 30 天内学会 Python 编程语言,通过每天的练习和学习,逐渐掌握 Python 的基础和进阶知识,全部完成可能需要超过 100 天。该教程适合想要快速入门 Python 的初学者,或者对 Python 有一定了解,想通过实践深入理解 Python 的开发者。

GitHub 地址→github.com/Asabeneh/30…

  1. HelloGitHub 热评

在这个章节,将会分享下本周 HelloGitHub 网站上的热门开源项目,欢迎与我们分享你上手这些开源项目后的使用体验。

3.1 自定义 Windows 任务栏透明度的小工具:TranslucentTB

主语言:C++

该项目是采用 C++ 开发的用于调整 Windows 任务栏透明度的工具,它体积小、免费、简单易用,支持 5 种任务栏状态、6 种动态模式、Windows 10/11 操作系统。

项目详情→hellogithub.com/repository/…

3.2 跨平台的手写笔记和绘图应用:Rnote

主语言:Rust

这是一款用 Rust 和 GTK4 编写的绘图应用,可用于绘制草图、手写笔记和注释文档等。它支持导入/导出 PDF 和图片文件,以及无限画布、拖放、自动保存等功能。适用于 Windows、Linux 和 macOS 系统,需要搭配手写板使用。

项目详情→hellogithub.com/repository/…

  1. 结尾

在结束本周「GitHub 热点速递」的精彩内容后,希望这些开源项目能够对大家有所启发,帮助你们找到新的工具、学习资源或是娱乐项目。如果看完这些还不过瘾,可以通过阅读「往期回顾」的内容,找到更多热门开源项目。

往期回顾

  • 一周涨 15k Star 的开源项目
  • 拥抱开源更省钱

以上为本周的「GitHub 热点速递」全部内容,如果你发现其他好玩、有趣的 GitHub 项目,就来 HelloGitHub 和大家一起分享吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

go语言如何实现协程的抢占式调度的?

发表于 2024-04-23

写在文章开头

go语言通过GMP模型实现协程并发,为了避免单协程持续持有线程导致线程队列中的其他协程饥饿问题,设计者提出了一个抢占式调度机制,本文会基于一个简单的代码示例对抢占式调度过程进行深入讲解剖析。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

详解协程抢占式调度

函数调用间进行抢占式调度

假设我们现在有这样一个协程,它会进行函数嵌套调用,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
scss复制代码func foo1() {
 fmt.Println("foo1调用foo2")
 foo2()
}

func foo2() {
 fmt.Println("foo2调用foo3")
 foo3()
}

func foo3() {
 fmt.Println("foo3")
}

func main() {
 //设置WaitGroup等待协程运行结束
 var wg sync.WaitGroup
 wg.Add(1)
 //通过协程调用foo1
 go func() {
  defer wg.Done()
  foo1()
 }()
 //等待协程运行结束
 wg.Wait()
}

我们给出运行结果:

1
2
3
复制代码foo1调用foo2
foo2调用foo3
foo3

基于这段代码示例,我们通过这段指令获取plan9汇编码:

1
go复制代码go build -gcflags -S main.go

可以看到在foo1插入runtime.morestack_noctxt方法,该方法是用于检查当前协程是否有足够的堆栈空间以保证函数的正常调用,基于这一点,go就会在进行这部检查时顺带检查协程的执行时长,一旦超过10ms该方法就会将协程设置为标记可被抢占:

1
less复制代码 0x0061 00097 (F:\github\test\main.go:8) CALL    runtime.morestack_noctxt(SB)

如下图,我们的调用的函数都会被插入一个morestack通过这个标记判断当前协程执行耗时,一旦发现超过10ms则会直接通过抢占式调度的方法g0协程直接调用schedule方法获取另外的协程进行调用:

这一点我们可以在asm_amd64.s看到morestack的newstack的代码,而newstack就是实现抢占式调度的核心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码TEXT runtime·morestack(SB),NOSPLIT,$0-0
 // Cannot grow scheduler stack (m->g0).
 get_tls(CX)
 MOVQ g(CX), BX
 MOVQ g_m(BX), BX
 MOVQ m_g0(BX), SI
 CMPQ g(CX), SI
 JNE 3(PC)
 CALL runtime·badmorestackg0(SB)
 CALL runtime·abort(SB)

    //......
    //函数调用前会调用newstack进行抢占式的检查
 CALL runtime·newstack(SB)
 CALL runtime·abort(SB) // crash if newstack returns
 RET

上述的newstack方法在stack.go中,如果当前协程可被抢占则会调用gopreempt_m回到g0调用schedule方法从协程队列中拿到新的协程执行任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码
func newstack() {
 preempt := stackguard0 == stackPreempt


 //如果preempt 为true,则直接当前协程被标记为抢占直接调用gopreempt_m让出线程执行权
 if preempt {
  if gp == thisg.m.g0 {
   throw("runtime: preempt g0")
  }
  //......

  // Act like goroutine called runtime.Gosched.
  gopreempt_m(gp) // never return
 }
}

基于系统调用发起信号的抢占式调度

假设我们的协程没有进行额外的函数调用,是否就意味着当前协程的线程不能被抢占呢?很明显不是这样:

  1. 网络传输过程中需要发送某些紧急消息希望通过已有连接迅速将消息通知给对端时,就会产生SIGURG信号,go语言就会在收到此信号时触发抢占式调度。

  1. 进行GC工作时像目标线程发送信号由此实现抢占式调度。

对于第一点我们可以在signal_unix.go的sighandler方法得以印证,可以看到它会判断sig 是否为_SIGURG若是则调用doSigPreempt进行抢占式调度

1
2
3
4
5
6
7
8
9
10
scss复制代码func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
 
 //如果传入的信号为_SIGURG则调用doSigPreempt回到schedule实现抢占式调度
 if sig == sigPreempt && debug.asyncpreemptoff == 0 && !delayedSignal {
  // Might be a preemption signal.
  doSigPreempt(gp, c)
  
 }
 //......
}

doSigPreempt会通过调用asyncPreempt最终执行到preempt.go的asyncPreempt2调用到和上文函数调用抢占式调度方法gopreempt_m回到schedule方法从而完成抢占式调度:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码func doSigPreempt(gp *g, ctxt *sigctxt) {
 //......
 if wantAsyncPreempt(gp) {
  if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {
   // 调用asyncPreempt内部会得到一个和上文函数调用时抢占式调度的方法gopreempt_m的调用从而回到schedule方法
   ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc)
  }
 }

 //......
}

小结

以上便是笔者关于go语言中协程抢占式调度的所有内容,希望对你有帮助。

我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

参考

TCP 带外数据(即紧急模式的发送和接受) :blog.csdn.net/liushengxi_…

Linux(程序设计):59—SIGHUP、SIGPIPE、SIGURG信号处理(附SIGURG信号处理普通数据与外带数据案例):blog.51cto.com/u_15346415/…

本文使用 markdown.com.cn 排版

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

九九归一,这九个实用的Javascript小功能不学着傍身吗

发表于 2024-04-23

写在开头

哈喽!各位早上好呀,现2024年4月18日早上9点整,天气晴朗。☀

开始步入夏日了?最近几天都好闷热,每次来到公司基本都是”汗流浃背”,Em…可能小编有点虚?。。。不承认,好在公司已经开放空调了,凉快。❄

绿阴不减来时路,添得黄鹂四五声。

上周,写了篇 拖动❓元素拖动、列表拖动、表格拖动(列与行)🍊🍊🍊 的文章,反响好像还行,好久没有超过50个赞了🥳,感谢各位友友的宝贵一赞。👍

其实,很早之前,小编就开始进入佛系状态了,写文章随心所欲,想写就写,想写什么就写什么,不想写也就作罢,就…….很躺平👻,快乐无限。

不过呢,每个月还是会积极参与 金石计划征文活动 ,毕竟还能赚点小钱,补贴上每个月的交通费,何乐而不为呢,是吧是吧。而且这过程也能积累、记录、沉淀一些技术知识点,就挺好,这里也感谢掘金❤有这么一个活动来激励创作者,当然,也鼓励你、你们、他、他们快来参加叭。😉

扯远了!回到正题,本章将分享一些关于 Javascript 实用小功能,请诸君按需食用。(✪ω✪)

手动将文本复制到剪贴板

效果如下:

04182.gif
(开胃小菜,会的可以跳过,继续往下瞧。😐)

具体逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
html复制代码<!DOCTYPE html>
<html>
<head>
<title>手动将文本复制到剪贴板</title>
</head>
<body>
<input id="input" />
<button id="button">复制到剪贴板</button>
<script>
document.addEventListener('DOMContentLoaded', () => {
const input = document.querySelector('#input');
const button = document.querySelector('button');
button.addEventListener('click', () => {
const text = input.value;
copy(text);
});
function copy(text) {
const textareaElement = document.createElement('textarea');
// 隐藏textarea
textareaElement.style.position = 'fixed';
textareaElement.style.left = '-9999px';
textareaElement.style.top = `${document.documentElement.scrollTop}px`;
// 将需要设置的文本加到textarea中
textareaElement.value = text;
document.body.appendChild(textareaElement);
// 聚焦
textareaElement.focus();
// 选中文本
textareaElement.select();
try {
// 手动执行复制操作
document.execCommand('copy');
} catch (err) {
console.error('复制出错啦!');
} finally {
document.body.removeChild(textareaElement);
}
}
});
</script>
</body>
</html>

原理嘛,也简单,利用了一个文本域,借用它的选中能力,再去执行复制API document.execcommand 就行。

不过这个API已经准备被弃用,虽然还能用,但是未来某天可能就GG掉了。🤡

已弃用: 不再推荐使用该特性。虽然一些浏览器仍然支持它,但也许已从相关的 web 标准中移除,也许正准备移除或出于兼容性而保留。请尽量不要使用该特性,并更新现有的代码;参见本页面底部的兼容性表格以指导你作出决定。请注意,该特性随时可能无法正常工作。

那么,替换方案呢?肯定有!

可以瞧瞧 阮一峰 大神的总结,很全面,肯定有你想要且喜欢的。

现有操作剪贴板一共有三种方式:

  • document.execCommand
  • clipboard API
  • copy事件、paste事件、cut事件

而咱们的案例可以这样子修改:

1
2
3
4
javascript复制代码button.addEventListener('click', (e) => {
const text = input.value;
navigator.clipboard.writeText(text);
});

一行即可解决。👻

在复制的文本添加自定义内容

Em……这个是剪贴板的另一种常见应用形式,效果如下:

04183.gif
很简单啦,顺嘴一提。😗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
javascript复制代码<!DOCTYPE html>
<html>
<head>
<title>在复制的文本添加自定义内容</title>
</head>
<body>
<div id="content">随便整点内容就行........</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const contentElement = document.getElementById('content');
// 监听 copy 事件
contentElement.addEventListener('copy', e => {
// 获取复制的内容
const originalText = window.getSelection().toString();
// 没有选中文本
if (!originalText) return;
// 阻止copy事件的默认行为,防止没加上自定义信息,就将原始文本复制到剪贴板上
e.preventDefault();
const clipboardData = e.clipboardData;
// 将内容塞进剪贴板中
clipboardData.setData('text/plain', `${originalText}\n\n你小子?又来复制什么???\n作者:橙某人\n链接:https://juejin.cn/user/1908407919184670/posts`);
});
});
</script>
</body>
</html>

input随着输入内容自动增长-宽度

效果如下:

04184.gif
也是一种常见功能吧,我们来瞧瞧具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
html复制代码<!DOCTYPE html>
<html>
<head>
<title>input随着输入内容自动增长-宽度</title>
</head>
<body>
<input id="input" type="text" style="border: 1px solid #cbd5e0; min-width: 80px; padding: 10px;" />
<script>
document.addEventListener('DOMContentLoaded', () => {
// 创建一个div元素
const fakeElement = document.createElement('div');
// 给它设置样式,用户不可见的
fakeElement.style.position = 'absolute';
fakeElement.style.top = '0';
fakeElement.style.left = '-9999px';
fakeElement.style.overflow = 'hidden';
fakeElement.style.visibility = 'hidden';
fakeElement.style.whiteSpace = 'nowrap';
fakeElement.style.height = '0';
// 获取input元素
const inputElement = document.getElementById('input');
// 获取input元素的样式
const styles = window.getComputedStyle(inputElement);
// 将input的样式同步到div中
// 字体相关的
fakeElement.style.fontFamily = styles.fontFamily;
fakeElement.style.fontSize = styles.fontSize;
fakeElement.style.fontStyle = styles.fontStyle;
fakeElement.style.fontWeight = styles.fontWeight;
fakeElement.style.letterSpacing = styles.letterSpacing;
fakeElement.style.textTransform = styles.textTransform;
// 边框与内边距
fakeElement.style.borderLeftWidth = styles.borderLeftWidth;
fakeElement.style.borderRightWidth = styles.borderRightWidth;
fakeElement.style.paddingLeft = styles.paddingLeft;
fakeElement.style.paddingRight = styles.paddingRight;

document.body.appendChild(fakeElement);
// 先执行一次,保持div与input相同的初始宽度
setWidth();
// 监听input的输入
inputElement.addEventListener('input', e => {
setWidth(inputElement.value || inputElement.getAttribute('placeholder'));
});
// 计算div宽度并同步给input上
function setWidth(text = '') {
fakeElement.innerHTML = text.replace(/\s/g, '&' + 'nbsp;');
const fakeElementStyles = window.getComputedStyle(fakeElement);
inputElement.style.width = fakeElementStyles.width;
};
});
</script>
</body>
</html>

整个过程原理大致是,将 input 实时输入的内容同步给一个容器,计算容器的宽度,再将容器宽度同步回 input 就能完成如上的效果啦。😁

这个小功能关键点是检测内容的宽度,而在 JS 中检测内容宽度一般用两种方式,其一,使用假元素,也就是我们上面使用的形式;另一种就是使用 canvas 的 measureText 方法。

大致过程如下:

1
2
3
4
5
6
7
javascript复制代码function measureWidth(text, font) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = font;
const metrics = context.measureText(text);
return metrics.width;
};

textarea随着输入内容自动增长-高度

效果如下:

04185.gif
也是比较常见的功能了,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html复制代码<!DOCTYPE html>
<html>
<head>
<title>textarea随着输入内容自动增长-高度</title>
</head>
<body>
<textarea id="textarea" style="width: 16rem;border: 1px solid rgb(203 213 225);"></textarea>
<script>
document.addEventListener('DOMContentLoaded', () => {
const textareaEle = document.getElementById('textarea');
textareaEle.addEventListener('input', () => {
textareaEle.style.height = 'auto';
textareaEle.style.height = `${textareaEle.scrollHeight}px`;
});
});
</script>
</body>
</html>

呃……两行代码就能完成😲,完全没难度,一个小技巧。

先将文本域的高度重置成 auto,这样我们就能使用 scrollHeight 获取内容的实际高度,然后,将文本域的高度设置成 scrollHeight 的值,这就能让文本域自动扩展适配内容了。

image.png
将页面部分内容全屏化


效果如下:

04186.gif
具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
html复制代码<!DOCTYPE html>
<html>
<head>
<title>将页面部分内容全屏化</title>
</head>
<body>
<p>.......</p>
<div id="container" style="background-color: #fff;border: 1px solid red;">
<p>.......</p>
<button id="full-screen-btn">全屏观看</button>
</div>
<p>.......</p>
<script>
document.addEventListener('DOMContentLoaded', () => {
const containerElement = document.getElementById('container');
const fullScreenButton = document.getElementById('full-screen-btn');
fullScreenButton.addEventListener('click', () => {
// 判断全屏模式是否可用
if (document.fullscreenEnabled) {
// 判断是记否是全屏模式
if (document.fullscreenElement) {
// 退出全屏
document.exitFullscreen();
}else {
// 进入全屏
containerElement.requestFullscreen();
}
}
});
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
fullScreenButton.innerHTML = '全屏观看';
} else {
fullScreenButton.innerHTML = '退出全屏';
}
});
});
</script>
</body>
</html>

这个案例主要考验咱们对 JS 中的一些API使用是否熟练,可以自个瞅瞅哈。👻

  • document.fullscreenEnabled:检测全屏模式是否可用。
  • document.fullscreenElement:返回已经被全屏化的元素,如果没有则返回 null。
  • document.exitFullscreen:退出全屏。
  • element.requestFullscreen:将元素全屏化,注意是异步的。
  • fullscreenchange:监听浏览器进入或退出全屏模式后立即触发。

同步两个元素之间的滚动

这个案例源于小编提交 Git 代码时,查看相关代码的前后情况,那时编辑器的同步滚动引起了我的注意,就想着写个DEMO玩玩看。😗

同步滚动的应用情况还是非常常见的,如小编此时正在用的掘金Markdown编辑器、处理并排翻译项目时等等吧,总之,同步滚动可以给我们提供更便捷的工作效率。

下面咱们来一步一步实现这个小功能案例,先整上布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
html复制代码<!DOCTYPE html>
<html>
<head>
<title>同步两个元素之间的滚动</title>
<style>
#container {
display: flex;
border: 1px solid rgb(203, 213, 225);
height: 520px;
}
#left,
#right {
flex: 1;
overflow-y: auto;
}
.child {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
}
.child:not(:last-child) {
border-bottom: 1px solid rgb(203, 213, 225);
}
</style>
</head>
<body>
<div id="container">
<div id="left"></div>
<div id="right"></div>
</div>
<script>
const container = document.getElementById("container");
const left = document.getElementById("left");
const right = document.getElementById("right");

createChildElement(left);
createChildElement(right);

// 在给定范围内随机生成一个数字
function randomInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 生成子元素
function createChildElement(parent) {
// 要生成多少个子元素
const childNum = randomInteger(50, 80);
console.log('子元素有多少个:', childNum)
// 批量创建子元素
Array(childNum).fill(0).forEach((_, index) => {
const div = document.createElement("div");
div.classList.add("child");
div.innerHTML = `${index + 1}`;
parent.appendChild(div);
});
}
</script>
</body>
</html>

Em…😐没啥难,就是通过 JS 动态随机生成了一些子元素,方便我们后续的测试,大概整出来的效果如下:

image.png

现在左右两边各自滚自己的,互不干扰。

059807BB.gif
而我们要如何来同步两边的滚动呢?很简单!咱们只要给两边加上监听器(scroll),当用户滚动其中一边,咱们更新另一边就可以。

具体实现过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javascript复制代码left.addEventListener('scroll', e => {
syncScroll(left, right);
});
right.addEventListener('scroll', e => {
syncScroll(right, left);
});
// 同步滚动, scrolledElement为滚动的元素, element为需要同步滚动的元素
function syncScroll(scrolledElement, element) {
const top = scrolledElement.scrollTop;
const left = scrolledElement.scrollLeft;
element.scrollTo({
behavior: 'instant',
top,
left,
});
};

效果:

04191.gif

我们用了 Element.scrollTo API 来进行滚动操作,看起来是不是还不错?😉

不过,这还没完,这里还存在两个问题🔉:

  • 可能造成无限滚动循环:因为我们左右两边都监听了 scroll 事件,如果用户滚动了左边,我们去通过 Element.scrollTo API滚动右边,这里其实右边也会触发 scroll 事件,那么它又会去同步滚动左边,这就会造成一个死循环的无限滚动了。而为什么动态图中看起来很正常呢?这是因为咱们在 behavior 参数上使用了 instant 值,如果你换成 smooth 值,这个问题就比较容易复现出来。

image.png

  • 总高度滚动不同步:仔细瞧动图,你会发现左边还没滚动到底的时候,右边就已经到底了,这可不符合我们同步滚动的需求呀❗造成这个原因是两边的总高度不一致,虽然现在子元素高度都是一样的,但是子元素个数是随机的,未来也可能是子元素高度不一样高,反正就是两边的总高度可能会不一样高,那么滚动就不可能完全同步。

对于这两个问题,咱们来逐一击破💣。

要解决无限滚动循环问题,我们可以先暂时将未滚动元素的事件监听器给先移除了,等滚动结束后再加回来,Em…说着…很简单😑,但…具体要如何做呢?

且看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
javascript复制代码const boxs = [left, right];
boxs.forEach(item => {
item.addEventListener('scroll', handleScroll);
});

function handleScroll(e) {
const scrolledElement = e.target;
boxs.filter(item => item !== scrolledElement).forEach(noScrolledElement => {
// 移除未滚动元素的事件监听器
noScrolledElement.removeEventListener("scroll", handleScroll);
// 执行同步滚动
syncScroll(scrolledElement, noScrolledElement);
// 在下次重绘之前加回监听事件
window.requestAnimationFrame(() => {
noScrolledElement.addEventListener("scroll", handleScroll);
});
});
}

function syncScroll(scrolledElement, element) {
...
};

这里用到了一个 requestAnimationFrame API来判断再次添加回事件监听器的时机,按它的API介绍”要求浏览器在下次重绘之前调用”,也就是它会在下一次重绘之前被调用。

而滚动操作咱们使用了 Element.scrollTo API,它通常情况下,仅会引起重绘,因为滚动并不改变元素的布局。但是,如果滚动导致某些依赖于滚动位置的计算(如计算动态加载的内容或响应滚动事件而改变样式的元素)发生,那么它也可能间接引起回流,这时可能就要考虑加一个”宏任务”(setTimeout)来判断时机了。

那么,无限滚动循环问题咱们就如此解决掉了😎,其实关键点是滚动结束时机的把控,这会涉及重绘、回流、事件循环等 JS 的知识点。

而另一个总高度滚动不同步问题呢❓改动不大,先直接贴上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码function syncScroll(scrolledElement, element) {
// 计算 滚动距离 占 总可滚动距离 的滚动比例
const scrolledPercent = scrolledElement.scrollTop / (scrolledElement.scrollHeight - scrolledElement.clientHeight);
// 通过 滚动比例 与 总可滚动距离 就能推出实际滚动距离
const top = scrolledPercent * (element.scrollHeight - element.clientHeight);
// top能懂,left就能明白
const scrolledWidthPercent = scrolledElement.scrollLeft / (scrolledElement.scrollWidth - scrolledElement.clientWidth);
const left = scrolledWidthPercent * (element.scrollWidth - element.clientWidth);
element.scrollTo({
behavior: 'instant',
top,
left,
});
};

开始咱们是直接将滚动距离 scrolledElement.scrollTop 同步到另一个未滚动元素上。

但是当元素具有不同的高度时,滚动位置可能会变得不同步。要解决这个问题,咱们只能计算出滚动比例再去推导出不同元素的实际需要滚动的距离。

image.png

好了,通过这些修改,我们的同步滚动功能现在可以处理具有不同数量块的多个可滚动元素,同时保持其滚动位置完美同步,完美收工。🥳

当前时间2024年4月18日下午5点22分,文章还没写完,快下班了😔,咋搞❗❗❗ 看来今天怕是写不完了,本来打算是随便水水得了😆,不知不觉中间又扩展很多出来,自己又写得慢吞吞,唉。

打印图片

图片打印听起来好像挺高大上😲,其实不然。

在浏览器中,提供了打印的方法 window.print(),这个方法不需要任何参数,直接调用即可。

不过,它是将整个网页进行打印,这倒是与我们只想打印图片的需求有点差异,不过问题不大,咱们可以利用一个 iframe 来解决,iframe 里面就放一张图片就行嘛,这也是当前浏览器上打印局部内容的主流方案。

就不卖关子了😄,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
html复制代码<!DOCTYPE html>
<html>
<head>
<title>打印图片</title>
</head>
<body>
<div style="display: flex; flex-direction: column; justify-content: center;align-items: center;">
<img id="image" style="width: 300px;margin-bottom: 10px;" src="https://p9-passport.byteacctimg.com/img/user-avatar/958fa7d9d487975fe84bc62298b8bc47~120x120.awebp" />
<button id="print">打印图片</button>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const printBtn = document.getElementById('print');
printBtn.addEventListener('click', function () {
const image = document.getElementById('image');
print(image);
});

function print(image) {
// 创建一个iframe
const iframe = document.createElement('iframe');
// 设置样式
iframe.style.height = 0;
iframe.style.visibility = 'hidden';
iframe.style.width = 0;
// 等同于src属性,只是src是路径,srcdoc是HTML代码
iframe.setAttribute('srcdoc', '<html><body></body></html>');
// 插入iframe
document.body.appendChild(iframe);
// iframe加载完
iframe.addEventListener('load', () => {
// 克隆图片元素,防止相互干扰
const imageClone = image.cloneNode();
imageClone.style.maxWidth = '100%';
// 访问iframe的body
const body = iframe.contentDocument.body;
body.style.textAlign = 'center';
body.appendChild(imageClone);
// 等待图片加载完
imageClone.addEventListener('load', () => {
// 打印,等同window.print()
iframe.contentWindow.print();
});
});
// iframe.contentWindow返回iframe的window对象
iframe.contentWindow.addEventListener('afterprint', () => {
// 在关联的文档开始打印或关闭打印预览后,将触发 afterprint 事件。
iframe.parentNode.removeChild(iframe);
});
}
});
</script>
</body>
</html>

就一个方法,标有详细的注解,这里就不多说啦,看就完事。👻

贴两张图瞅瞅叭。

整个网页打印:

image.png
只打印图片:

image.png
可调整大小的视图


效果如下:

04221.gif
看这个案例的具体实现之前,小编建议你先稍微瞅瞅另一篇文章 拖动❓元素拖动、列表拖动、表格拖动(列与行)🍊🍊🍊 ,看完之后,这个案例对你来说就是手到擒来的事情,没吹🙅,真是这样。

结构与样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
html复制代码<!DOCTYPE html>
<html>
<head>
<title>可调整大小的视图</title>
<style>
.container {
width: 100%;
height: 500px;
border: 1px solid #cbd5e0;
display: flex;
}
.line[data-direction='horizontal'] {
width: 2px;
height: 100%;
background-color: #cbd5e0;
cursor: ew-resize;
}
.line[data-direction='vertical'] {
height: 2px;
width: 100%;
background-color: #cbd5e0;
cursor: ns-resize;
}
.left {
width: 25%;
align-items: center;
display: flex;
justify-content: center;
}
.right {
flex: 1;
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
}
.top {
width: 100%;
height: 200px;
align-items: center;
display: flex;
justify-content: center;
}
.bottom {
width: 100%;
flex: 1;
align-items: center;
display: flex;
justify-content: center;
}
</style>
</head>
<body>
<div class="container">
<div class="left">左边菜单</div>
<div class="line" data-direction="horizontal"></div>
<div class="right">
<div class="top">上面内容</div>
<div class="line" data-direction="vertical"></div>
<div class="bottom">下面内容</div>
</div>
</div>
</body>
</html>

HTML+CSS 就没什么好说的了,主要咱们是来看看逻辑方面是如何做的。😀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
javascript复制代码document.addEventListener('DOMContentLoaded', () => {
// 给 拖动元素(line) 批量添加监听事件
document.querySelectorAll('.line').forEach(line => {
resizable(line);
});

function resizable(line) {
// 容器信息
const containerRect = line.parentNode.getBoundingClientRect();
// 获取拖动方向
const direction = line.getAttribute('data-direction') || 'horizontal';
// 获取相邻元素
const prevSibling = line.previousElementSibling;
const nextSibling = line.nextElementSibling;
// 相关位置信息
let x = 0;
let y = 0;
let prevSiblingHeight = 0;
let prevSiblingWidth = 0;
// 拖动元素添加鼠标按下事件
line.addEventListener('mousedown', mouseDownHandler);

function mouseDownHandler(e) {
// 获取鼠标当前位置
x = e.clientX;
y = e.clientY;
// 获取拖动元素的上个元素的宽高
const rect = prevSibling.getBoundingClientRect();
prevSiblingHeight = rect.height;
prevSiblingWidth = rect.width;
// 监听鼠标的移动与释放事件
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
}
function mouseMoveHandler(e) {
// 获取拖动距离
const dx = e.clientX - x;
const dy = e.clientY - y;
switch (direction) {
case 'vertical':
// 垂直拖动时,top元素宽度 = (原高度 + 拖动距离) * 100 / 容器总高度
const h = ((prevSiblingHeight + dy) * 100) / containerRect.height;
prevSibling.style.height = h + '%';
break;
case 'horizontal':
default:
// 水平拖动时,left元素宽度 = (原宽度 + 拖动距离) * 100 / 容器总宽度
const w = ((prevSiblingWidth + dx) * 100) / containerRect.width;
prevSibling.style.width = w + '%';
break;
}
// 更改相关样式
const cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize';
line.style.cursor = cursor;
document.body.style.cursor = cursor;
prevSibling.style.userSelect = 'none';
prevSibling.style.pointerEvents = 'none';
nextSibling.style.userSelect = 'none';
nextSibling.style.pointerEvents = 'none';
};
function mouseUpHandler() {
// 相关样式、事件重置回来
line.style.removeProperty('cursor');
document.body.style.removeProperty('cursor');
prevSibling.style.removeProperty('user-select');
prevSibling.style.removeProperty('pointer-events');
nextSibling.style.removeProperty('user-select');
nextSibling.style.removeProperty('pointer-events');
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
}
});

父窗口与iframe之间的通信

效果如下:

04222.gif
Em……我想作为一个前端切图仔,你多少也有所了解,当前,在 JS 中要实现父窗口与 iframe 之间的通信,我们基本都会选择 message 事件与 postMessage 方法相结合的形式。

只不过现在越来越少会使用 iframe 了吧😷, 毕竟还有更优的方案可以选择-微前端。

但是呢……Em……没事的话,还是可以学学着玩的。😉

咱们新建 index.html 与 iframe.html 两个页面,具体过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
html复制代码<!DOCTYPE html>
<html>
<head>
<title>Window</title>
<style>
#box {
height: 200px;
border: 1px solid #cbd5e0;
padding: 10px;
box-sizing: border-box;
margin-top: 10px;
}
</style>
</head>
<body>
<button id="button">发现消息给iframe</button>
<div id="box"></div>
<br />
<iframe id="iframe" src="./iframe.html" style="border: none; height: 300px; width: 100%"></iframe>
<script>
document.addEventListener('DOMContentLoaded', () => {
const box = document.getElementById('box');
const iframe = document.getElementById('iframe');
// 监听message事件
window.addEventListener('message', e => {
const data = e.data;
const time = new Date(data.time).toLocaleTimeString();
box.innerHTML = `收到消息: "${data.message}" ,时间为 "${time}"<br>` + box.innerHTML;
});
document.getElementById('button').addEventListener('click', () => {
const message = {
message: '我从window那边发过来的',
time: Date.now(),
};
// 将信息发送给iframe
iframe.contentWindow.postMessage(message, '*');
});
});
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
html复制代码<!DOCTYPE html>
<html>
<head>
<title>iframe</title>
<style>
#box {
height: 200px;
border: 1px solid #cbd5e0;
padding: 10px;
box-sizing: border-box;
margin-top: 10px;
}
</style>
</head>
<body>
<button id="button">发现消息给window</button>
<div id="box"></div>
(这里是一个iframe)
<script>
document.addEventListener('DOMContentLoaded', () => {
const box = document.getElementById('box');
window.addEventListener('message', e => {
const data = e.data;
const time = new Date(data.time).toLocaleTimeString();
box.innerHTML = `收到消息: "${data.message}" ,时间为 "${time}"<br>` + box.innerHTML;
});
document.getElementById('button').addEventListener('click', () => {
const message = {
message: '我从iframe那边发过来的',
time: Date.now(),
};
window.parent.postMessage(message, '*');
});
});
</script>
</body>
</html>

很简单,但也有一些注意事项:

  • 总是在发送和接收消息时验证消息来源,以确保安全。
  • 如果父窗口和 iframe 在同一域下,可以省略 event.origin 的检查。
  • postMessage 方法的第二个参数(目标源)可以是具体的源(如一个域名),也可以是'*',表示任何源都可以,但这会带来安全隐患。

至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

前端开发中也会遇到并发问题 1 前言 2 并发请求的场景

发表于 2024-04-23
  1. 前言

在前端开发中,咱们经常得同时向服务器发好几个请求,特别是要获取多个数据资源或者执行多个任务的时候。这篇文章呢,主要是想跟大家分享一些我在实际项目中碰到的前端并发请求问题,还有我是怎么解决这些问题的。希望这些经验能对大家有所帮助哈!

  1. 并发请求的场景

咱们聊聊前端并发请求的场景吧,不限于以下几种:

  1. 页面初始化时加载多个数据资源:想象一下,你打开一个网页,它得从服务器那里拿好多东西,比如用户信息、列表数据、配置信息等等。这些请求可能是一起发出去的,这就是页面初始化时的并发请求。
  2. 依赖数据的级联请求:有时候一个请求的结果能告诉咱们下一步该做什么。如果这个链条很长,那同时进行的请求可就多了。
  3. 批量操作:用户执行批量操作(如批量下载、批量更新)时,前端可能会并发地发出多个请求以提高效率。
  4. 实时数据更新:在需要实时更新数据的应用中(如股票交易平台),可能会用到轮询(polling)或WebSocket等技术,这也可能导致并发请求。
  5. 第三方服务的集成:在集成多个第三方服务(如社交媒体分享、地图服务等)时,可能需要并发地请求这些服务的API。

总的来说,前端并发请求的场景真是五花八门,但都是为了给咱们提供更好的用户体验。

  1. 遇到的问题

并发请求确实能让应用更快更高效地响应,但也会带来一些挑战:

  1. 性能问题:如果请求太多,服务器可能会感到压力山大,导致响应变慢,甚至影响稳定性。
  2. 资源竞争:浏览器同时发出的请求数量是有限的,超出限制的话,请求就得排队等待,这样可能会让一些紧急的请求也变慢。
  3. 前端资源利用:并发处理大量请求可能会占用大量的前端资源(如内存和CPU),导致页面响应变慢,影响用户体验。
  4. 数据一致性和同步问题:如果多个请求同时修改同一个数据源,可能会出现数据不一致的情况,需要仔细设计数据同步和状态管理逻辑。
  5. 错误处理复杂化:并发请求的错误处理也更复杂,得考虑怎么集中处理错误,还有部分请求成功、部分失败的情况。

所以,虽然并发请求有很多好处,但也要好好考虑这些挑战,才能确保应用稳定、高效地运行。

  1. 解决方案

咱们来聊聊怎么解决这些问题吧!其实有几个小技巧可以试试看。

  • 限制并发数量:使用队列或Promise.allSettled等方式限制同时进行的请求数量。
  • 使用缓存:我们可以把请求结果缓存起来,这样就不用每次都去麻烦服务器啦。
  • 服务端优化:服务器端也可以优化一下,比如用负载均衡、缓存等方式来提高处理并发请求的能力。
  • 合并请求:如果可能的话,我们还可以把多个请求合并成一个,这样并发数量就减少了。
  • 优先级控制:我们还可以给不同的请求设置优先级,确保重要的请求能先得到处理。

把这些小技巧都用上,我们的应用就能更流畅,用户体验也会变得更好!

  1. 案例

5.1 Tab 快速切换竞态条件导致的访问顺序问题

功能描述: 在异常运单列表页面,你会看到4个选项卡,分别是“全部”、“一级”、“二级”和“三级”。每当你切换选项卡时,系统都会根据你的选择重新发送请求,并在收到响应后更新页面上的数据。

8E0F33FE-EBAA-4EB2-A71C-E90CD5AD60CC.png

问题复现: 想象一下,你像个探险家,一级一级地闯过迷宫,每次点击都像是在探索新的领域。你按顺序发起了三个请求:RequestA、RequestB、RequestC,但网络这个小调皮却把你的响应顺序给搞乱了,变成了Response A、Response C、ResponseB。这可怎么办?页面上的显示和真实的数据就不匹配了,就像你探险时,地图和实际路线不一致一样,真是让人头疼啊!

这种场景还挺多的,比如用户在下单页面上查看费用信息,他们和页面上的表单互动时,那些费用信息就会实时变动,还会调用一些接口。挺酷的吧!

解决方案:想要解决实时数据频繁更新导致的问题?别担心,我有个小妙招!你可以用一个计数标志来搞定。这样,问题就迎刃而解啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码// 使用一个计数变量,每次请求前后进行记录,只有数量一致,才读取数据,避免先发的请求后到达,导致页面查询条件与发起的请求不一致  

export function createSequencedRequest(requestFunc) {
  let requestCount = 0; // 记录当前发起了多少次请求

  const sequencedRequest = (...args) => {
    const currentRequestCount = ++requestCount; // 记录某个请求的排序
    return requestFunc(...args).then(r => {
      if (currentRequestCount === requestCount) {
        return r;
      }
      // 如果请求的顺序不正确,返回一个特殊的值或者错误
      throw new Error('Request order is incorrect');
    });
  };

  return sequencedRequest;
}

5.2 批量下单并发过多失败问题

功能描述: 用户上传了一个Excel表格,里面填满了订单信息。服务端会解析这些数据,然后把结果反馈给前端展示给用户看。接下来,用户需要为每一个订单补充一些材料。最后,前端会批量调用下单接口,完成所有订单的下单操作。

F9F3D4A1-32B9-42CC-82DA-45AF8DE23CE2.png

问题描述: 你知道吗,当普通用户一下子提交几十个单时,他们的浏览器会同时发出几十个请求。这有点像是交通堵塞,请求太多,路就堵了,后面的请求就得等更久才能得到回应。为了避免这种情况,我们在项目中使用了Axios来处理请求,并设置了超时时间,这样请求就不会一直等下去,也不会占用太多资源。

在Chrome浏览器里,如果一下子发出超过6个请求,后面的请求就会因为超时而被取消。所以,为了避免这种情况,我们可以试着分批次下载,或者优化一下我们的请求策略。

1C0D13D4-B412-489E-B145-7C2DF182DF8C.png

由于上线时间问题,这里其实后端提供一个批量下单的接口更加合理。但类似这种情况有大文件分片上传,也会同时发起多个分片请求。

解决方案: 通过控制并发的数量解决问题,每次发起6个请求,只要有一个请求成功响应后,就可以新增一个请求,通过请求队列进行控制。

58D8B506-6B85-4BCD-87EA-3D7917D040F3.png
下面这个函数,就是用来控制流量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
js复制代码export const pLimit = concurrency => {  
  if (!((Number.isInteger(concurrency) || concurrency === Infinity) && concurrency > 0)) {
    throw new TypeError('`concurrency` 必须是大于零的整数或者 Infinity');
  }

  const queue = []; // 用于存储待执行的任务
  let activeCount = 0; // 表示当前正在执行的任务数量

  /*
   * 函数用于处理任务执行完成后的逻辑,将 activeCount 减一,并从任务队列中取出下一个任务执行
   */
  const next = () => {
    activeCount--;

    if (queue.length > 0) {
      queue.shift()(); // 出队
    }
  };

  /**
   * 执行异步任务,并在任务完成后执行 resolve,同时调用 next 函数
   */
  const run = async (fn, resolve, ...args) => {
    activeCount++;

    const result = (async () => fn(...args))();

    resolve(result);

    try {
      await result;
    } catch {}

    next();
  };
  /**
   * enqueue 函数用于将任务添加到队列中,并在当前任务数量未达到并发数上限时立即执行 
   */
  const enqueue = (fn, resolve, ...args) => {
    // 入队
    queue.push(run.bind(null, fn, resolve, ...args));

    (async () => {
      await Promise.resolve();
      // 立即执行
      if (activeCount < concurrency && queue.length > 0) {
        queue.shift()();
      }
    })();
  };

  /**
   * generator 函数是一个 Promise 化的包装函数,用于生成异步任务的 Promise 对象,并将任务添加到队列中
   */
  const generator = (fn, ...args) =>
    new Promise(resolve => {
      enqueue(fn, resolve, ...args);
    });

  // 通过 Object.defineProperties 方法定义了 generator 函数的几个属性
  Object.defineProperties(generator, {
    activeCount: {
      get: () => activeCount,
    },
    pendingCount: {
      get: () => queue.length,
    },
    clearQueue: {
      value: () => {
        queue.length = 0;
      },
    },
  });

  return generator;
};

在 pLimit 函数中,generator 函数的设计是为了将异步任务包装成一个 Promise 对象,并将其添加到任务队列中。为了正确处理异步任务的完成状态,需要在异步任务执行完成后调用其对应的 resolve 函数。

在 run 函数中,我们执行了异步任务 fn,然后调用了 resolve 函数,这样可以在异步任务完成后,将结果传递给对应的 Promise 对象。但是,由于 resolve 函数是外部传入的,我们不能直接在 run 函数中调用它,因为在异步任务执行完成后,run 函数已经执行完毕并且无法再直接访问到 resolve 函数。

为了解决这个问题,我们通过闭包的方式,将 resolve 函数传递给 run 函数,并将其保存在 enqueue 函数中调用 run 函数时。这样就能确保在异步任务执行完成后,调用正确的 resolve 函数,将结果传递给对应的 Promise 对象。

使用例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码// 引入 pLimit 函数  
const { pLimit } = require('./pLimit');

// 创建一个限流器,限制同时执行的任务数量为 2
const limit = pLimit(2);

// 模拟一个异步任务,返回一个 Promise,其中的延时代表任务执行的耗时
const asyncTask = async (index) => {
    console.log(`Task ${index} started`);
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log(`Task ${index} finished`);
};

// 执行一组任务
const runTasks = async () => {
    const tasks = [];
    for (let i = 1; i <= 5; i++) {
        tasks.push(limit(() => asyncTask(i)));
    }
    await Promise.all(tasks);
};

// 执行测试
runTasks();

上面的解决方案抽象起来就是限流,限流的核心原理是控制系统在单位时间内能够处理的请求或任务数量,以保护系统不被过载。在实际应用中,限流通常应用于网络通信、接口调用、任务调度等场景。

限流要考虑以下几点:

  1. 并发控制:其实,限流就是为了让系统不要处理太多的请求或任务,避免它累垮了。通过限制同时进行的数量,咱们可以让系统轻松一些,这样它就能更稳定、更可靠地工作了。
  2. 算法选择: 选择合适的限流算法是限流实现的关键。常见的限流算法包括令牌桶算法、漏桶算法、计数器算法等。这些算法在实现上各有特点,例如令牌桶算法可以平滑限制请求的流量,漏桶算法可以平滑处理突发流量等。
  3. 计数器和队列: 限流通常使用计数器和队列来跟踪和控制请求或任务的数量。计数器用于统计当前活跃的请求数量或任务数量,而队列则用于暂存超出限流阈值的请求或任务,以便后续处理。
  4. 动态调整: 限流策略通常需要根据系统负载和性能动态调整。例如,当系统负载较低时可以适当放宽限流策略,以提高系统的响应速度;当系统负载较高时则需要加强限流策略,以保护系统不被过载。
  5. 错误处理: 在限流过程中,需要考虑如何处理请求被拒绝的情况。合适的错误处理策略可以提高系统的用户体验和容错能力,例如返回友好的错误提示、重试机制等。

总的来说,限流的核心原理是通过控制系统的并发数量、选择合适的限流算法、使用计数器和队列进行跟踪和控制、动态调整限流策略以及合理处理被拒绝请求等方式,保护系统不被过载,确保系统的稳定性和可靠性。

借鉴后端的限流原理,我们来实现在前端控制并发请求数。

总结

不只是后端开发者要头疼并发问题,有时候前端也需要掌握一些并发的解决方案。比如说,在需要批量操作或者实时数据更新的场景下,前端也会遇到性能、数据一致性等头疼的问题。不过别担心,我们可以通过限流、优先级等方案来解决这些问题,可以考虑一些开源方案比如 p-limit、react query、swr 等。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…313233…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%