async/await 你可能正在将异步写成同步
前言你是否察觉到自己随手写的异步函数,实际却是“同步”的效果!正文以一个需求为例:获取给定目录下的全部文件,返回所有文件的路径数组。第一版思路很简单:读取目录内容,如果是文件,添加进结果数组,如果还是目录,我们递归执行。import path from 'node:path'
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'
async function findFiles(root) {
if (!existsSync(root)) return
const rootStat = await fs.stat(root)
if (rootStat.isFile()) return [root]
const result = []
const find = async (dir) => {
const files = await fs.readdir(dir)
for (let file of files) {
file = path.resolve(dir, file)
const stat = await fs.stat(file)
if (stat.isFile()) {
result.push(file)
} else if (stat.isDirectory()) {
await find(file)
}
}
}
await find(root)
return result
}
机智的你是否已经发现了问题?我们递归查询子目录的过程是不需要等待上一个结果的,但是第 20 行代码,只有查询完一个子目录之后才会查询下一个,显然让并发的异步,变成了顺序的“同步”执行。那我们去掉 20 行的 await 是不是就可以了,当然不行,这样的话 await find(root) 在没有完全遍历目录之前就会立刻返回,我们无法拿到正确的结果。思考一下,怎么修改它呢?......让我们看第二版代码。[removed]第二版import path from 'node:path'
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'
async function findFiles(root) {
if (!existsSync(root)) return
const rootStat = await fs.stat(root)
if (rootStat.isFile()) return [root]
const result = []
const find = async (dir) => {
const task = (await fs.readdir(dir)).map(async (file) => {
file = path.resolve(dir, file)
const stat = await fs.stat(file)
if (stat.isFile()) {
result.push(file)
} else if (stat.isDirectory()) {
await find(file)
}
})
return Promise.all(task)
}
await find(root)
return result
}
我们把每个子目录内容的查询作为独立的任务,扔给 Promise.all 执行,就是这个简单的改动,性能得到了质的提升,让我们看看测试,究竟能差多少。对比测试console.time('v1')
const files1 = await findFiles1('D:\\Videos')
console.timeEnd('v1')
console.time('v2')
const files2 = await findFiles2('D:\\Videos')
console.timeEnd('v2')
console.log(files1?.length, files2?.length)
版本二快了三倍不止,如果是并发的接口请求被不小心搞成了顺序执行,差距比这还要夸张。——转载自作者:justorez
我说数据分页用Limit,面试官直接让我回去等消息
最近我碰到了一个挺有趣的“小插曲”,大概是这样的:现在有一个社交应用,在聊天界面中,用户可以通过下滑页面来不断加载历史消息。我当时想不就一个分页,这么简单的需求怎么能难倒我这个练习时长两年半的SQL boy,我直接一个安一个limit上去直接就把这个问题解决了,写出来的SQL大概是这样的:select * from message order by create_time desc limit n_page*50 50;
看了两眼,好像没有什么问题,然后就跑去忙别的了。问题浮现等到了下午,我忙完了手上的其他需求,准备开始对这个功能进行一个简单的压测,然后就可以美美的把代码提交,去博德之门里扔几把骰子。但是随着压测的进行,我发现这个接口的响应时间波动比较剧烈,一开始我还没当回事,认为是数据库压力太大导致的性能下 降,但是紧接着我就发现了这些性能较差的接口都具有较大的n_page,同时普遍耗时都在其他接口的10倍以上这一下可就不能当做没看见了(悲伤痛哭)。算了,发现问题那就解决问题,于是经过我的一番查找,很快我就发现,原来这是全是深分页惹的祸!深分页问题那么什么是深分页问题呢? 在MySQL的limit中:limit 100,10,MySQL会根据查询条件去存储引擎层找到前110条记录,然后在server层丢弃前100条记录取最后10条 这样先扫描完再丢弃的记录相当于白找,深分页问题指的就是这种场景(当limit的偏移量过大时会导致性能开销)。 那么要如何解决这个问题呢,现在比较多的实践是使用游标分页。游标分页游标分页的思想说起来也简单,既然采用limit 100,10这样的分页方式会导致MySQL从头开始扫描,从而扫描到没用的前100条记录,那么如果我们能够直接从第101条开始扫描,扫描10条,那么这个问题不就解决了。那么应该怎么实现呢,简单,在扫描时加一个条件不就得了select * from message where create_time<上一次扫描到的最后一条记录的create_time order by create_time desc limit 10;
实际上游标翻页只是通过额外添加一个条件来改变MySQL扫描的起始位置。这也导致了游标翻页的使用必须要满足两个前提:需要一个列来记录上一次查询到的最大值,并且这个列要是有序的(比如我们上面用到的create_time)每一次的查询会依赖上一次查询的最大值,因此,分页查询需要时连续的,不能进行跳页(比如查完第一页然后跳过第二页直接查第三页)接下来让我们来做一个简单实验,验证一下游标分页的有效性。为了凸显效果,我们直接给表里整进去100万数据(这会花费很多时间,你可以自行减少记录数量)CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`empty_body` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT NOW(),
PRIMARY KEY (`id`)
)ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='测试表';
DROP PROCEDURE IF EXISTS my_insert;
create procedure my_insert()
BEGIN
declare i int default 1;
set autocommit = 0;
while i [removed]1000000 limit 0,10;
可以看到,使用游标翻页的速度远远快于直接使用limit进行分页。被忽略的问题在上面我们证明了游标分页可以解决limit分页带来的性能问题,但是其实还有隐藏的,limit分页无法解决的问题:消息重复问题。让我们思考一下,如果我们在使用limit分页时,有新消息到来会怎么样。没错!由于limit分页是根据和最新记录的差值来计算记录位置的,所以如果有新消息到达,那么在读取下一页的时候,就有可能读取到和上一页重复的消息。但是由于游标翻页采用的是固定的游标,即使有新消息到达也不会读取到重复的消息。[removed]总结游标分页非常适合于深分页的场景,能够大大减少需要的记录数量,但是由于游标依赖于上一次查询,所以无法实现跳页,并且只有作为游标的列有序的情况下才能使用游标。而且这个解决方案可以很容易的迁移到项目中任何需要分页的地方,非常适合作为面试时的技术亮点。——转载自作者:单木
这样代码命名,总不会被同事蛐蛐了吧
1. 引言....又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是草头底下一个来回的回字么?”孔乙己显出极高兴的样子,将两个指头的长指甲敲着柜台,点头说,“对呀对呀!……回字有四样写法,你知道么?”我愈不耐烦了,努着嘴走远。孔乙己刚用指甲蘸了酒,想在柜上写字,见我毫不热心,便又叹一口气,显出极惋惜的样子针对于同一个代码变量或者函数方法,张三可能认为可以叫 xxx,李四可能摇头说 不不不,得叫 yyyy ,好的命名让人如沐春风,原来是这个意思;坏的代码命名,同事可能会眉头紧锁,然后送你两斤熏鸡骨头让你炖汤比如隔壁小组新来的一个同事,对字符串命名就用 s,对于布尔值的命名就用 b,然后他的主管说他的变量名起的跟他人一样。如何做到信雅达的命名,让同事不会再背后蛐蛐,我是这样想的。[removed]2. 代码整洁之道2.1 团队规范“我在上家公司都是这样命名的,在这里我也要这样命名”小组里张三给 Service 起的名字叫 UserService 实现类是 UserServiceImpl;小组里李四给 Service 起的名字叫 CustomerService 实现类 CustomerServiceImpl你跳出来出来说,统统不对,接口需要区分对待 得叫 IUserService 和 ICustomerService但是组里成员都不习惯往接口类加个 I;或许这就是 E 人编码吧,不能写 I(我承认这个梗有点烂)双拳难敌四手,亲,这边建议你按照 UserService 和 CustomerService 起名这只是个简单的例子,还有就是你认为 4 就是 for,2 就是 to,如果小组内的成员表示认可你的想法,那你就尽管大胆的使用,但是小组成员要是没有这一点习惯,建议还是老老实实 for 和 to,毕竟你没有一票否决权诸如此类的还有 request -> req、response -> resp 等以下所有的代码命名建议都不能打破团队规范这一条大原则2.2 统一业务词汇在各行各业中,基于业务属性,我们都有一些专业术语,对于专业术语的命名往往在设计领域模型的时候已经确定下来,建议有一份业务词汇来规范团队同学命名,或者以数据库字段为准比如在保险行业中,我们有保费(premium)、保单(policy)、被保人(assured)等,针对于这些业务词汇,务必需要统一。被保人就是 assured 不是 Insured Person2.2 名副其实“语义一定要清晰,不然后续接手的人根本看不懂,我的这个函数名是用来对订单进行删除操作,然后进行 MQ 消息推送的,我准备给他起名为 deleteOrderByCustomerIdAndSendMqMessage”对,函数名很长很清晰,虽然我的屏幕很宽,但是针对于这样的命名,我觉得不可取,函数名和函数一样应该尽量短小,当你的命名极其长的时候你需要考虑简化你的命名,或者 考虑你的函数是否遵循到了单一职责。bad😭
deleteOrderByCustomerIdAndSendMqMessage(String userId)
good🤭
deleteOrder(String userId)
sendMq()
我们在做阅读理解的时候,需要结合上下文来作答,同样,我们的命名需要让下一个做阅读理解的人感受到我们的上下文含义。在我们删除订单的时候,假设我们需要用到订单的 ID,那么我们的命名需要是 orderId = 123,而不是 id = 123bad😭 这个 id 指代的是什么,订单ID 还是用户 ID
id = 123
good🤭
deleteOrder(String userId)
orderId = 123
人靠衣装马靠鞍,变量类型需“平安”,我们在起名的时候需要对的起自己的名字bad😭 tm的喵,我以为是个 list
String idList = "1,2,3"
good🤭
List[removed] idList = ImmutabList.of("1", "2", "3")
默认我的同事的英文水平只有四级,我们变量命名的时候,尽量取一些大众化的词汇,可以四级词汇,切莫六级词汇bad😭
actiivityOrchestrater
good🤭 活动策划人
actiivityPlanner
普通函数、方法命名以动词开头bad😭
messageSend
good🤭
sendMessage
减少介词链接,考虑使用 形容词+名词productListForSpecialOffer -> specialOfferProductList
productListForNewArrival -> newArrivalProductList
productListFromHenan -> henanProductList
productListWithGiftBox -> withGiftBoxProductList \ giftBoxedProductList
productListWithoutGiftBox -> withoutGiftBoxProductList \ noGiftBoxProductList \ unGiftBoxedProductList
消除无意义的前后缀单词: userInfo、userData,info 和 data 的含义过于宽泛,没有实质性意义所以我们可以不用写。或者诸如在 UserService 类中,我们可以可以尝试将 selectUserList 更换为 selectList,因为我们调用的时候,上下文一定是 userService.selectList,阅读者是可以感受到我们的语义的userInfo -> user
userService.selectUserList -> userService.selectList
做有意义的方法名的区分:在我刚入职的时候,有一个 OrderService 中,存在 4个方法,enableOrder、enableOrderV2、enableOrderV3、enableOrderV4,我问组里的同事,有什么区别,他们告诉我,现在各个外部服务用的不同,不知道有啥区别。所以为了避免给类似我这样的菜鸟产生歧义,建议在方法起名的时候做好区分,以免埋坑——转载自作者:isysc1
工作中 Spring Boot 五大实用小技巧,来看看你掌握了几个?
0. 引入Spring Boot 以其简化配置、快速开发和微服务支持等特点,成为了 Java 开发的首选框架。本文将结合我在实际工作中遇到的问题,分享五个高效的 Spring Boot 的技巧。希望这些技巧能对你有所帮助。1. Spring Boot 执行初始化逻辑1.1 背景项目的某次更新,数据库中的某张表新增了一个字段,且与业务有关联,需要对新建的字段根据对应的业务进行赋值操作。一种解决方案就是,更新前手动写 SQL 更新字段的值,但这样做的效率太低,而且每给不同环境更新一次,就需要手动执行一次,容易出错且效率低。另一种方案则是在项目启动时进行初始化操作,完成字段对应值的更新,这种方案效率更高且不容易出错。1.2 实现Spring Boot 提供了多种方案用于项目启动后执行初始化的逻辑。实现CommandLineRunner接口,重写run方法。@Slf4j
@Component
public class InitListen implements CommandLineRunner {
@Overridepublic void run(String... args) {// 初始化相关逻辑...}
}
实现ApplicationRunner接口,重写run方法。@Slf4j
@Component
public class InitListen implements ApplicationRunner {
@Overridepublic void run(ApplicationArguments args) {// 初始化相关逻辑...}
}
实现ApplicationListener接口@Slf4j
@Configuration
public class StartClientListener implements ApplicationListener[removed] {
@Overridepublic void onApplicationEvent(ContextRefreshedEvent arg0) {// 初始化逻辑}
}
针对于上述这个需求,如何实现仅更新一次字段的值?可在数据库字典表中设置一个更新标识字段,每次执行初始化逻辑之前,校验判断下字典中的这个值,确认是否已经更新,如果已经更新,就不需要再执行更新操作了。2. Spring Boot 动态控制数据源的加载2.1 背景期望通过在application.yml文件中,添加一个开关来控制是否加载数据库。2.2 实现启动类上添加注解 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }),代表禁止 Spring Boot 自动注入数据源。新建 DataSourceConfig配置类,用于初始化数据源。在DataSourceConfig配置类上添加条件注解 @ConditionalOnProperty(name = "spring.datasource.enabled", havingValue = "true",代表只有当 spring.datasource.enabled 为 true时,加载数据库,其余情况不加载数据库。仓库类 XxxRepository 的注入,需要使用注解 @Autowired(required = false)[removed]3. Spring Boot 根据不同环境加载配置文件3.1 背景实际开发工作中,针对同一个项目,可能会存在开发环境、测试环境、正式环境等,不同环境的配置内容可能会不一致,如:数据库、Redis等等。期望在项目在启动时能够针对不同的环境来加载不同的配置文件。3.2 实现Spring 提供 Profiles 特性,通过启动时设置指令-Dspring.profiles.active指定加载的配置文件,同一个配置文件中不同的配置使用---来区分。启动 jar 包时执行命令:java -jar test.jar -Dspring.profiles.active=dev
-Dspring.profiles.active=dev代表激活 profiles 为 dev 的相关配置。##用---区分环境,不同环境获取不同配置---# 开发环境
spring:
profiles: dev
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 命名空间为默认,所以不需要写命名空间
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
extension-configs[0]:
data-id: database-base.yaml
group: DEFAULT_GROUP
refresh: true
extension-configs[1]:
# 本地单机Redis
data-id: redis-base-auth.yaml
group: DEFAULT_GROUP
refresh: true
extension-configs[2]:
data-id: master-base-auth.yaml
group: DEFAULT_GROUP
refresh: true
---
#测试环境
spring:
profiles: test
cloud:
nacos:
discovery:
server-addr: 192.168.0.111:8904
# 测试环境注册的命名空间
namespace: b80b921d-cd74-4f22-8025-333d9b3d0e1d
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
extension-configs[0]:
data-id: database-base-test.yaml
group: DEFAULT_GROUP
refresh: true
extension-configs[1]:
data-id: redis-base-test.yaml
group: DEFAULT_GROUP
refresh: true
extension-configs[2]:
data-id: master-auth-test.yaml
group: DEFAULT_GROUP
refresh: true
---
# 生产环境
spring:
profiles: prod
cloud:
nacos:
discovery:
server-addr: 192.168.0.112:8848
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
extension-configs[0]:
# 生产环境
data-id: database-auth.yaml
group: DEFAULT_GROUP
refresh: true
extension-configs[1]:
# 生产环境
data-id: redis-base-auth.yaml
group: DEFAULT_GROUP
refresh: true
extension-configs[2]:
data-id: master-base-auth.yaml
group: DEFAULT_GROUP
refresh: true
也可以定义多个配置文件,如在application.yml中定义和环境无关的配置,而application-{profile}.yml则根据环境做不同区分,如在 application-dev.yml 中定义开发环境相关配置、application-test.yml 中定义测试环境相关配置。启动时指定环境命令同上,仍为:java -jar test.jar -Dspring.profiles.active=dev
参考资料zhuanlan.zhihu.com/p/646593227cloud.tencent.com/developer/a…——转载自作者:离开地球表面_99
接口不能对外暴露怎么办?
在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。面对这样的情况,我们该如何实现呢?1. 内外网接口微服务隔离将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。2. 网关 + redis 实现白名单机制在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。3. 方案三 网关 + AOP相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。[removed]具体实操下面就方案三,进行具体的代码演示。首先在网关侧,需要对进来的请求header添加外网标识符: from=public@Component
public class AuthFilter implements GlobalFilter, Ordered {@Overridepublic Mono [removed] filter ( ServerWebExchange exchange, GatewayFilterChain chain ) {return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate().header('id', '').header('from', 'public').build()).build());
}
@Overridepublic int getOrder () {return 0;}}
接着,编写内外网访问权限判断的AOP和注解@Aspect
@Component
@Slf4j
public class OnlyIntranetAccessAspect {
@Pointcut ( '@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnClass () {}
@Pointcut ( '@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnMethed () {
}
@Before ( value = 'onlyIntranetAccessOnMethed() || onlyIntranetAccessOnClass()' )
public void before () {
HttpServletRequest hsr = (( ServletRequestAttributes ) RequestContextHolder.getRequestAttributes()) .getRequest ();
String from = hsr.getHeader ( 'from' );
if ( !StringUtils.isEmpty( from ) && 'public'.equals ( from )) {
log.error ( 'This api is only allowed invoked by intranet source' );
throw new MMException ( ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR);
}
}
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OnlyIntranetAccess {
}
最后,在只能内网访问的接口上加上@OnlyIntranetAccess注解即可@GetMapping ( '/role/add' )
@OnlyIntranetAccess
public String onlyIntranetAccess() {
return '该接口只允许内部服务调用';
}
4. 网关路径匹配
在DailyMart项目中我采用的是第四种:即在网关中进行路径匹配。
该方案中我们将内网访问的接口全部以前缀/pv开头,然后在网关过滤器中根据路径找到具体校验器,如果是/pv访问的路径则直接提示禁止外部访问。
使用网关路径匹配方案不仅可以应对内网接口的问题,还可以扩展到其他校验机制上。
譬如,有的接口需要通过access_token进行校验,有的接口需要校验api_key 和 api_secret,为了应对这种不同的校验场景,只需要再实现一个校验类即可,由不同的子类实现不同的校验逻辑,扩展非常方便。
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。——转自作者:程序员蜗牛
十分钟学会WebSocket
WebSocket简介WebSocket是一种在客户端和服务器之间实现双向通信的网络协议。它通过在单个TCP连接上提供全双工通信功能,使得服务器可以主动向客户端推送数据,而不需要客户端发起请求。WebSocket与HTTP的区别与传统的HTTP协议相比,WebSocket具有以下几个显著的区别:双向通信:WebSocket支持客户端和服务器之间的实时双向通信,而HTTP协议是单向请求-响应模式。低延迟:由于WebSocket使用长连接,避免了HTTP的连接建立和断开过程,可以降低通信延迟。更少的数据传输:WebSocket头部信息相对较小,减少了数据传输的开销。跨域支持:WebSocket可以轻松跨域,而HTTP需要通过CORS等机制来实现。WebSocket的工作原理WebSocket的握手过程和HTTP有所不同。客户端通过发送特定的HTTP请求进行握手,服务器收到请求后进行验证,如果验证通过,则会建立WebSocket连接。建立连接后,客户端和服务器之间可以通过WebSocket发送和接收消息,可以使用文本、二进制数据等进行通信。WebSocket的应用场景WebSocket的实时双向通信特性使得它在许多应用场景中发挥重要作用,例如:即时聊天:WebSocket可以实现实时的聊天功能,用户可以发送和接收消息,实现快速、低延迟的聊天体验。实时数据更新:对于需要实时更新数据的应用,如股票行情、实时监控等,WebSocket可以将数据实时推送给客户端,确保数据的及时更新。在线游戏:在线游戏需要实时的双向通信,WebSocket可以提供稳定的通信通道,支持实时交互和多人游戏。WebSocket的使用以下是使用JavaScript与WebSocket建立连接的示例代码:
var Socket = new WebSocket("url, [protocol]");
以上代码中的第一个参数url, 指定连接的 URL。第二个参数protocol是可选的,指定了可接受的子协议。WebSocket 属性以下是 WebSocket 对象的属性。属性描述Socket.readyState只读属性readyState表示连接状态,可以是以下值:0-表示连接尚未建立。1-表示连接已建立,可以进行通信。2-表示连接正在进行关闭。3-表示连接已经关闭或者连接不能打开。Socket.bufferedAmount只读属性bufferedAmount已被send()放入正在队列中等待传输,但是还没有发出的UTF-8文本字节数。0-表示连接尚未建立。1-表示连接已建立,可以进行通信。2-表示连接正在进行关闭。3-表示连接已经关闭或者连接不能打开。WebSocket 事件以下是 WebSocket 对象的相关事件。事件事件处理程序描述openSocket.onopen连接建立时触发messageSocket.onmessage客户端接收服务端数据时触发errorSocket.onerror通信发生错误时触发closeSocket.onclose连接关闭时触发下面是相关示例代码:
Socket.onopen = function() {
//连接建立时触发
console.log("WebSocket连接已建立");
};
Socket.onmessage = function(event) {
//客户端接收服务端数据时触发
var message = event.data;
console.log("收到消息:" + message);
};
Socket.onerror = function() {
//通信发生错误时触发
console.log("WebSocket连接发生了错误");
};
Socket.onclose = function() {
//连接关闭时触发
console.log("WebSocket连接已关闭");
};
WebSocket 方法以下是 WebSocket 对象的相关方法。方法描述Socket.send()使用连接发送数据Socket.close()关闭连接
//发送一条消息
Socket.send('你好')
//关闭WebSocket连接
Socket.close()
WebSocket 除了发送和接收文本消息外,还支持发送和接收二进制数据。对于发送二进制数据,可以使用 send() 方法传递一个 ArrayBuffer 或 Blob 对象,例如:
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, 1234);
socket.send(buffer);
在接收二进制数据时,可以通过 event.data 获取到 ArrayBuffer 对象,然后进行处理。WebSocket的心跳机制WebSocket的心跳机制是一种用于保持WebSocket连接的稳定性和活跃性的方法。心跳机制的目的是定期发送小的探测消息,以确保连接仍然有效,如果连接断开或出现问题,可以及时发现并采取措施。下面是WebSocket心跳机制的详细步骤和相关代码示例:定义心跳间隔:为了定期发送心跳消息,你需要定义一个心跳间隔,通常以毫秒为单位。在示例中,我们将心跳间隔设置为30秒。
const heartbeatInterval = 30000; // 30秒
定义心跳消息:你需要定义用于发送心跳的消息内容。这通常是一个简单的字符串,如"heartbeat",但可以根据应用的需求自定义。
const heartbeatMessage = 'heartbeat';
设置心跳定时器:一旦WebSocket连接打开,你可以使用setInterval函数设置一个定时器,以便每隔一段时间发送心跳消息。
let heartbeat;
socket.addEventListener('open', () => {
console.log('WebSocket连接已打开');
heartbeat = setInterval(() => {
socket.send(heartbeatMessage);
}, heartbeatInterval);
});
处理心跳消息:当你接收到来自服务器的消息时,你需要检查它是否是心跳消息。这可以通过比较接收到的消息内容和心跳消息的内容来实现。
socket.addEventListener('message', (event) => {
const message = event.data;
if (message === heartbeatMessage) {
console.log('接收到心跳消息');
// 在这里可以执行一些处理心跳消息的操作
} else {
console.log('接收到其他消息:', message);
// 处理其他类型的消息
}
});
清除心跳定时器:当WebSocket连接关闭时,你应该清除之前设置的心跳定时器,以防止继续发送心跳消息。
socket.addEventListener('close', () => {
console.log('WebSocket连接已关闭');
clearInterval(heartbeat);
});
通过这些步骤,你可以实现WebSocket的心跳机制,确保连接的持续稳定,以适应长时间的通信需求。如果连接断开或出现问题,你可以根据需要添加进一步的错误处理机制。WebSocket 的安全性和跨域问题如何处理?WebSocket 支持通过 wss:// 前缀建立加密的安全连接,使用 TLS/SSL 加密通信,确保数据的安全性。在使用加密连接时,服务器需要配置相应的证书。对于跨域问题,WebSocket 遵循同源策略,只能与同源的服务器建立连接。如果需要与不同域的服务器通信,可以使用 CORS(跨域资源共享)来进行跨域访问控制。有哪些好用的客户端WebSocket第三方库Socket.io-client:Socket.io 是一个流行的实时通信库,它提供了客户端 JavaScript 库,可用于在浏览器中与 Socket.io 服务器建立 WebSocket 连接。它支持自动重连、事件处理等功能,用于构建实时应用非常方便。ReconnectingWebSocket:ReconnectingWebSocket 是一个带有自动重连功能的 WebSocket 客户端库,可以很好地处理网络连接断开和重新连接的情况,适合用于浏览器端的 WebSocket 开发。SockJS-client:SockJS 提供了一个浏览器端的 JavaScript 客户端库,用于与 SockJS 服务器建立连接。它可以在不支持 WebSocket 的浏览器上自动降级到其他传输方式,具有良好的兼容性。RxJS WebSocketSubject:RxJS 是一个流式编程库,它提供了 WebSocketSubject 类,可以将 WebSocket 转换为可观察对象,方便进行响应式编程。autobahn.js:autobahn.js 是一个用于实现 WebSocket 和 WAMP(Web Application Messaging Protocol)的客户端库,在浏览器中可以方便地使用它来与 WAMP 路由进行通信。这些库都提供了良好的接口封装和功能特性,可以根据项目需求选择适合的库来进行浏览器端的 WebSocket 开发。[removed]
mysql for update是锁表还是锁行
转载至 www.infrastack.cn 在并发一致性控制场景中,我们常常用for update悲观锁来进行一致性的保证,但是如果不了解它的机制,就进行使用,很容易出现事故,比如for update进行了锁表导致其他请求只能等待,从而拖垮系统,因此了解它的原理是非常必要的,下面我们通过一系列示例进行测试,来看看到底是什么场景下锁表什么场景下锁行验证示例说明创建一个账户表,插入基础数据,以唯一索引、普通索引、主键、普通字段4 个维度进行select ... for update查询,查看是进行锁表还是锁行表创建创建一个账户表,指定account_no为唯一索引、id为主键、user_no为普通字段、curreny为普通索引
CREATE TABLE `account_info` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'ID' ,
`account_no` int NOT NULL COMMENT '账户编号',
`user_no` varchar(32) NOT NULL COMMENT '用户 Id',
`currency` varchar(10) NOT NULL COMMENT '币种',
`amount` DECIMAL(10,2) NOT NULL COMMENT '金额',
`freeze_amount` DECIMAL(10,2) NOT NULL COMMENT '冻结金额',
`create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
`update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uni_idx_account_no` (`account_no`) ,
KEY `idx_currency_` (`currency`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='账户信息表';
插入基础数据
insert into account_info values (1,1,'ur001','RMB',100,0,now(),now());
insert into account_info values (2,2,'ur002','RMB',1000,0,now(),now());
insert into account_info values (3,3,'ur002','DOLLAR',200,0,now(),now());
根据主键查询在事务 1 中,根据主键id=1 进行 for update查询时,事务2、事务 3 都进行阻塞,而事务 4 由于更新的id=2 所以成功,因此判定,根据主键进行 for update 查询时是行锁根据唯一索引查询在事务 1 中,根据唯一索引account_no=1 进行 for update查询时,事务2、事务 3 都进行阻塞,而事务 4 由于更新的account_no=2 所以成功,因此判定,根据唯一索引进行 for update 查询时是行锁根据普通索引查询在事务 1 中,根据普通索引currency='RMB' 进行 for update查询时,事务2、事务 3 都进行阻塞,而事务 4 由于更新的currency='DOLLAR`所以成功,因此判定,根据普通索引进行 for update 查询时是行锁根据普通字段查询在事务 1 中,根据普通字段user_no='ur001' 进行 for update查询时,事务2、事务 3 都进行阻塞,而事务 4查询的是user_no='ur002'也进行阻塞,因此判定,根据普通字段进行 for update 查询时是表锁[removed]总结如果查询条件是索引/主键字段,那么select ..... for update会进行行锁如果查询条件是普通字段(没有索引/主键),那么select ..... for update会进行锁表,这点一定要注意。
draw.io:让你的流程图动起来
前言你可能会经常看到一些精美的流程图、架构图或是网络图等,并且看到他们的线条能够流动。但你可能疑惑这样的图是如何画出来的,今天介绍这样一个工具,用来帮助我们画出一些好看的动态图片。先来看下整体效果:首页进入首页之后,可以看到一个精美的图片示例,这意味着如果你有类似的做图需求,你可以尝试使用这个工具来进行制作。我们可以使用网页版,也可以下载桌面版应用。这里我们尝试使用网页版进行演示,点击start按钮创建一个图。内容创作进入创作页面后,我们选择一个空白的页面模板。可以看到,这和我们平时常见的作图软件的界面就极其相似了,左侧是图形选择区域,右侧是画布,通过将图形拖动到画布上来构建我们的流程图。线条流动当我们在画布上完成了一些绘制之后,我们期望能够让线条流动起来,使它看起来更加动态(这也是我们使用这个工具的原因)。要完成这一步,我们只需要对线条进行配置即可,如下:此时,你就可以看到画布上的线条动了起来。[removed]其他有意思的能力元素手绘风除了让线条流动起来外,我们还可以让图上的元素编程手绘风格。操作如下:添加更多图形通过添加图形的方式,能够在画布中增加更多预设的图形,基本能满足我们各种图的诉最后对我而言这个工具最吸引我的功能在于让流程图动起来。如果你也想画出好看的流程图、架构图、网络图等,不妨试一试吧。转自作者:风生水气
我将fabricjs换为了leaferjs
作者:雾恋序言别因今天的懒惰,让明天的您后悔。输出文章的本意并不是为了得到赞美,而是为了让自己能够学会总结思考;当然,如果有幸能够给到你一点点灵感或者思考,那么我这篇文章的意义将无限放大。背景前段时间学习了fabric也写了几篇文章,最近在学习和了解fabric的时候发现另一个图形化插件leaferjs,在经过两天的调研以后我觉得学习leaferjs是一个不错的选择,理由如下:国产,不得不说国产必须支持他的渲染速度以及流畅度确实很高(至少在我目前做的这些功能来看)因为是国产化所以文档都是中文,学习成本相对较低leaferjs有点像quill拓展性比较强,很多东西都是插件化leaferjs很多api都是封装好了的,使用起来更加的方便快捷leaferjs简介LeaferJS 是一款好用的 Canvas 引擎,革新的开发体验,可用于高效绘图 、UI 交互(小游戏、互动应用、组态)、图形编辑。提供了丰富的 UI 绘图元素,和开箱即用的功能,如自动布局、图形编辑、SVG 导出等,方便与 PS、 Figma、Sketch 等产品进行对接。并为跨平台开发提供了统一的交互事件,如拖拽、旋转、缩放手势等。主要场景有:leafer-draw 绘图场景leafer-game 游戏开发leafer-editor 图形编辑器主要版本是:web 版本woker 版本node 版本小程序版本综上所述:leaferjs适用场景很大,大部分场景都是可以实现的,但是现在市面上商业化较少,大家学习以及做一些自己的项目是没问题的。说了这么多可能大家没有直观的感受,先去看看性能测试吧!,然后我们进行对比查看。源码@ 设计工作室 design-workshop[removed]标尺fabricjs 是没有对应的插件,有一些插件可以使用,但是没有很好的适配不友好;而leaferjs有对应的插件leafer-x-ruler 直接可以使用,对于画布也很适配流畅度拉满。以下是示例代码:import { App } from 'leafer-ui';
import '@leafer-in/editor';
import { Ruler } from 'leafer-x-ruler';
this.app = new App({
view: canvas || 'canvas',
ground: { type: 'draw' },
tree: {},
editor: {
lockRatio: 'corner',
// stroke: '#6f4593',
// skewable: false,
hover: true,
// 选中框中间点
middlePoint: {
cornerRadius: 10,
width: 10,
height: 10,
},
// 选中框样式
rotatePoint: {
width: 10,
height: 10,
fill: '#fff',
},
},
sky: { type: 'draw', usePartRender: false },
});
this.ruler = new Ruler(this.app, {
enabled: true,
theme: 'dark',
});
自定义画布我们很多时候都是想自定义画布大小的,比如:手机大小、pc大小等等。fabric画布fabric如果要创建画布的话需要通过矩形创建一个剪切板,然后超出画板就不展示;如以下代码:const initWorkspace = (item?: IOption) => {
const { width, height } = item || option.value;
fabric.Object.prototype.objectCaching = false;
const workspaceData = new fabric.Rect({
fill: 'rgba(255,255,255, 0.1)',
width,
height,
id: 'workspace',
death: true,
selectable: false,
left: 0,
top: 0,
// 设置边框
// strokeDashArray: [5, 5],
// stroke: '#ff0000',
strokeWidth: 0,
rx: 5, // 圆角
ry: 5, // 圆角
});
// 超出画布不展示
workspaceData.clone((cloned: any) => {
canvas.value.clipPath = cloned;
});
canvas.value.add(workspaceData);
canvas.value.renderAll();
// 保存对象
workspace.value = workspaceData;
};
eaferjs官方元素: Frame: 创建画板。继承自 Box,默认白色背景、会裁剪掉超出宽高的内容,类似于 HTML5 中的页面。我们从一下代码中可以看出来:leaferjs创建一个画布真的超级简单,直接调用api即可。workspace.value = new Frame({
width: 1920,
height: 1080,
// overflow: 'hide',
editable: false,
lockRatio: true,
x: 0,
y: 0,
resizeChildren: true,
});
// 添加画布到app
app.tree.add(workspace.value);
总结以上是一些简单的示例代码,感谢关注。
前端,就是你,赶紧通知用户刷新页面去!
前言
老板:新的需求不是上线了嘛,怎么用户看到的还是老的页面呀
窝囊废:让用户刷新一下页面,或者清一下缓存
老板:那我得告诉用户,刷新一下页面,或者清一下缓存,才能看到新的页面呀,感觉用户体验不好啊,不能直接刷新页面嘛?
窝囊废:可以解决(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
使用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)
为什么 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
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…
谈谈我做 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);
});
祝大家在自己的领域越来越深,早日触摸到天花板。
前端:为什么 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…
我用这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的小技巧,但我们要根据实际情况选择使用,并非所有的场景都适合。
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,那么可能是产生了哈希冲突,所以,布隆过滤器会有一定误差。
探究 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%将使元素与父容器一样宽。额外的间距将添加到元素的大小,而不考虑父元素。这通常会导致问题。
不够理解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之后的默认,会是主流趋势。至此我感觉足够能举一反三了,如有哪里写的不对或者有更好建议欢迎大佬指点一二啊。
前端项目公共组件封装思想(Vue)
作者:安静的搬砖人1. 通用组件(表单搜索 + 表格展示 + 分页器)在项目当中我们总会遇到这样的页面:页面顶部是一个表单筛选项,下面是一个表格展示数据。表格下方是一个分页器,这样的页面在我们的后台管理系统中经常所遇到,有时候可能不止一个页面,好几个页面的结构都是这种。本人记得,在 react 中的高级组件库中有这么一个组件,就实现了这么一个效果。就拿这个页面来说我们实现一下组件封装的思想:1. 首先把每个页面的公共部分抽出来,比如标题等,用 props 或者插槽的形式传入到组件中进行展示 2. 可以里面数据的双向绑定实现跟新的效果 3. 设置自定义函数传递给父组件要做上面事情1. 将公共的部分抽离出来TableContainer组件
这里的话利用了具名插槽插入了 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) 更新父组件中的值,实现双向的数据绑定本文是作者在闲暇的时间随便记录一下, 若有错误请指正,多多包涵。感谢支持(顺便吆喝一声,技术大厂内推,前后端测试捞人)!{{ title }}