MapStruct这么用,同事也开始模仿
前言
hi,大家好,我是大鱼七成饱。
前几天同事review我的代码,发现mapstruct有这么多好用的技巧,遇到POJO转换的问题经常过来沟通。考虑到不可能每次都一对一,所以我来梳理五个场景,谁在过来问,直接甩出总结。
环境准备
由于日常使用都是spring,所以后面的示例都是在springboot框架中运行的。关键pom依赖如下:
<properties>
<java.version>1.8</java.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.30</org.projectlombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
场景一:常量转换
这是最简单的一个场景,比如需要设置字符串、整形和长整型的常量,有的又需要日期,或者新建类型。下面举个例子,演示如何转换
//实体类
@Data
public class Source {
private String stringProp;
private Long longProp;
}
@Data
public class Target {
private String stringProperty;
private long longProperty;
private String stringConstant;
private Integer integerConstant;
private Long longWrapperConstant;
private Date dateConstant;
}
- 设置字符串常量
- 设置long常量
- 设置java内置类型默认值,比如date
那么mapper这么设置就可以
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface SourceTargetMapper {
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1l")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001L")
@Mapping(target = "dateConstant", dateFormat = "yyyy-MM-dd", constant = "2023-09-")
Target sourceToTarget(Source s);
}
解释下,constant用来设置常量值,source的值如果没有设置,则会使用defaultValue的值,日期可以按dateFormat解析。
Talk is cheap, show me the code !废话不多说,自动生成的转换类如下:
@Component
public class SourceTargetMapperImpl implements SourceTargetMapper {
public SourceTargetMapperImpl() {
}
public Target sourceToTarget(Source s) {
if (s == null) {
return null;
} else {
Target target = new Target();
if (s.getStringProp() != null) {
target.setStringProperty(s.getStringProp());
} else {
target.setStringProperty("undefined");
}
if (s.getLongProp() != null) {
target.setLongProperty(s.getLongProp());
} else {
target.setLongProperty(-1L);
}
target.setStringConstant("Constant Value");
target.setIntegerConstant(14);
target.setLongWrapperConstant(3001L);
try {
target.setDateConstant((new SimpleDateFormat("dd-MM-yyyy")).parse("09-01-2014"));
return target;
} catch (ParseException var4) {
throw new RuntimeException(var4);
}
}
}
}
是不是一目了然
场景二:转换中调用表达式
比如id不存在使用UUID生成一个,或者使用已有参数新建一个对象作为属性。当然可以用after mapping,qualifiedByName等实现,感觉还是不够优雅,这里介绍个雅的(代码少点的)。
实体类如下:
@Data
public class CustomerDto {
public Long id;
public String customerName;
private String format;
private Date time;
}
@Data
public class Customer {
private String id;
private String name;
private TimeAndFormat timeAndFormat;
}
@Data
public class TimeAndFormat {
private Date time;
private String format;
public TimeAndFormat(Date time, String format) {
this.time = time;
this.format = format;
}
}
Dto转customer,加创建TimeAndFormat作为属性,mapper实现如下:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, imports = UUID.class)
public interface CustomerMapper {
@Mapping(target = "timeAndFormat",
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
@Mapping(target = "id", source = "id", defaultExpression = "java( UUID.randomUUID().toString() )")
Customer toCustomer(CustomerDto s);
}
解释下,id为空则走默认的defaultExpression,通过imports引入,java括起来调用。新建对象直接new TimeAndFormat。有的小伙伴喜欢用qualifiedByName自定义方法,可以对比下,哪个合适用哪个,都能调用转换方法。
生成代码如下:
@Component
public class CustomerMapperImpl implements CustomerMapper {
public CustomerMapperImpl() {
}
public Customer toCustomer(CustomerDto s) {
if (s == null) {
return null;
} else {
Customer customer = new Customer();
if (s.getId() != null) {
customer.setId(String.valueOf(s.getId()));
} else {
customer.setId(UUID.randomUUID().toString());
}
customer.setTimeAndFormat(new TimeAndFormat(s.getTime(), s.getFormat()));
return customer;
}
}
}
场景三:类共用属性,如何复用
比如下面的Bike和车辆类,都有id和creationDate属性,我又不想重复写mapper属性注解
public class Bike {
/**
* 唯一id
*/
private String id;
private Date creationDate;
/**
* 品牌
*/
private String brandName;
}
public class Car {
/**
* 唯一id
*/
private String id;
private Date creationDate;
/**
* 车牌号
*/
private String chepaihao;
}
解决起来很简单,写个共用的注解,使用的时候引入就可以,示例如下:
//通用注解
@Retention(RetentionPolicy.CLASS)
//自动生成当前日期
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
//忽略id
@Mapping(target = "id", ignore = true)
public @interface ToEntity { }
//使用
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface TransportationMapper {
@ToEntity
@Mapping( target = "brandName", source = "brand")
Bike map(BikeDto source);
@ToEntity
@Mapping( target = "chepaihao", source = "plateNo")
Car map(CarDto source);
}
这里Retention修饰ToEntity注解,表示ToEntity注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期,辅助生成mapper实现类。上面定义了creationDate和id的转换规则,新建日期,忽略id。
生成的mapper实现类如下:
@Component
public class TransportationMapperImpl implements TransportationMapper {
public TransportationMapperImpl() {
}
public Bike map(BikeDto source) {
if (source == null) {
return null;
} else {
Bike bike = new Bike();
bike.setBrandName(source.getBrand());
bike.setCreationDate(new Date());
return bike;
}
}
public Car map(CarDto source) {
if (source == null) {
return null;
} else {
Car car = new Car();
car.setChepaihao(source.getPlateNo());
car.setCreationDate(new Date());
return car;
}
}
}
坚持一下,还剩俩场景,剩下的俩更有意思
场景四:lombok和mapstruct冲突了
啥冲突?用了builder注解后,mapstuct转换不出来了。哎,这个问题困扰了我那同事两天时间。
解决方案如下:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
加上lombok-mapstruct-binding就可以了,看下生成的效果:
@Builder
@Data
public class Person {
private String name;
}
@Data
public class PersonDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface PersonMapper {
Person map(PersonDto dto);
}
@Component
public class PersonMapperImpl implements PersonMapper {
public PersonMapperImpl() {
}
public Person map(PersonDto dto) {
if (dto == null) {
return null;
} else {
Person.PersonBuilder person = Person.builder();
person.name(dto.getName());
return person.build();
}
}
}
从上面可以看到,mapstruct匹配到了lombok的builder方法。
场景五:说个难点的,转换的时候,如何注入springBean
有时候转换方法比不是静态的,他可能依赖spring bean,这个如何导入?
这个使用需要使用抽象方法了,上代码:
@Component
public class SimpleService {
public String formatName(String name) {
return "您的名字是:" + name;
}
}
@Data
public class Student {
private String name;
}
@Data
public class StudentDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class StudentMapper {
@Autowired
protected SimpleService simpleService;
@Mapping(target = "name", expression = "java(simpleService.formatName(source.getName()))")
public abstract StudentDto map(StudentDto source);
}
接口是不支持注入的,但是抽象类可以,所以采用抽象类解决,后面expression直接用皆可以了,生成mapperimpl如下:
@Component
public class StudentMapperImpl extends StudentMapper {
public StudentMapperImpl() {
}
public StudentDto map(StudentDto source) {
if (source == null) {
return null;
} else {
StudentDto studentDto = new StudentDto();
studentDto.setName(this.simpleService.formatName(source.getName()));
return studentDto;
}
}
}
思考
以上场景肯定还有其他解决方案,遵循合适的原则就可以。驾驭不了的代码,可能带来更多问题,先简单实现,后续在迭代优化可能适合更多的业务场景。
本文示例代码放在了github,需要的朋友请关注公众号大鱼七成饱,回复关键词MapStruct使用即可获得。
来源:juejin.cn/post/7297222349731627046
安卓开发转做鸿蒙后-开篇
一、为什么转做鸿蒙
本人从事安卓开发已近十年,大部分时间还是在不停的需求迭代,或者一遍遍优化各种轮子,自己的职业生涯已经进入了瓶颈期,同时现有工作也很难让自己产生成就感。正好年初有机会转入鸿蒙开发团队,虽然清楚肯定少不了加班,最终也不一定会有预期中的产出,还是希望自己能有一些新东西的刺激和积累。
二、App鸿蒙化的回顾
本人所在公司差不多算是中厂,C端App日活大概有个几百万,各部门团队大概有30人+,历时半年多的时间,差不多完成了全部功能70%左右。前期主要是个人自学及各种培训、前期调研、App基础库的排期、业务排期、开发上架等几个环节。
1、基础库
- 网络库
- 图片库
- 埋点库
- 路由库
- 公共组件
- 崩溃监控
- 打包构建
2、业务排期
- 业务拆分优先级
- 分期迭代开发测试
三、跟安卓相比的差异性
1、ArkUI和Android布局
- Android控件习惯于宽高自适应,ArkUI中部分子组件会超过容器组件区域,所以部分组件需要控制宽度
- Android是命令式UI比较简单直接,ArkUI是声明式,需要重点关注状态管理的合理使用
- Android列表重复相对简单,ArkUI中List懒加载和组件复用使用比较繁琐
- Android基于Java可以通过继承抽取一些公共能力,ArkUI组件无法进行继承
2、鸿蒙开发便捷的一面
1、问题的反馈和响应比较及时,华为技术支持比较到位。
2、应用市场对性能要求和各类适配要求比较高,倒逼开发提高自己的开发能力。
3、跟安卓比提供了各种相对完善的组件,避免了开发者需要进行各种封装
- 路由库
- 网络库
- 图片库
- 扫码
- 人脸识别
- picker
- 统一拖拽
- 预加载服务
- 应用接续
- 智能填充
- 意图框架
- AI语音识别
3、鸿蒙开发不便的一面
- ArkTS文档不够完善,没有从0到1的完整学习流程
- ArkUI部分组件使用繁琐
- DevEco-Studio的稳定性需要提升
- 组件渲染性能需要提升,
四、跨平台方案
- RN
- Flutter
- ArkUI-X
ArkUI-X作为鸿蒙主推的跨平台框架,主要问题是生态的建立和稳定性。所以还是要基于公司基建的完善程度和技术生态进行选择。同时由于鸿蒙的加入,适配3个OS系统的成本提高,公司为降本提效会加快跨平台技术的接入和推进。后续还是需要熟悉跨平台开发的技术。
五、知识体系(待完善)
1、ArkTS应用
1、应用程序包结构(hap、har、hsp)
2、整体架构
3、开发模型
2、ArkTs
1、基本语法
2、方舟字节码
3、容器类库
4、并发
3、ArkUI
1、基本语法
2、声明式UI描述
3、自定义组件
4、装饰器
5、状态管理
6、渲染控制
4、Stage模型
1、应用配置文件
2、应用组件
3、后台任务
4、进程模块
5、线程模型
5、性能优化
1、冷启动
2、响应时延
3、完成时延
4、滑动帧率
5、包大小
来源:juejin.cn/post/7409877909999026217
拼多多冷启真的秒开
背景
最近在使用拼多多购物,除了价格比较香之外,每次冷启打开的体验非常好,作为一个Android开发不免好奇 简单分析记录一下
冷启数据
体验好,让我想到了郭德纲的那句话"全靠同行的衬托",那找几个同行过来对比下,这里使用淘宝、京东、闲鱼,从点击开始图标开始录个屏直接数秒,
测试手机是 华为 Mate 60
我这个样本比较少,机器性能也比较好,仅仅是个人对比,不代表大众使用的真实情况
可以粗略的看几个常见app的冷启对比下,这里录屏使用的剪映来分析
帧率为 每秒30帧,后面会涉及一些时间换算
拼多多
无广告冷启动 从点击图标到到首页完整展示 大概花了 29帧,
1000ms * 29/30 约为 0.96s
,太惊人了,基本冷启秒开
拼多多可能真的没有开屏广告,我印象中没有见过拼多多的开屏广告
淘宝
无广告冷启东 从点击图标到到首页完整展示
大概花了 1s+21帧,
1000ms+ 21/30*1000ms = 1.7s
还可以
淘宝可能没有开屏广告,或者非常克制,我刷了十几次都没有见到开屏广告
京东
无广告冷启京东
从点击图标到到首页完整展示
大概花了 1s+28帧,
1000ms + 28/30*1000ms 约为 1.93s
也是不错的
不过京东的开屏有开屏广告,但是做了用户频控,刷了几次就没了,这里仅对比无广告冷启开屏
闲鱼
毕竟是国内最大的二手平台(虽然现在小商家也特别多),而且是flutter深度使用者,看看它的表现
大概花了** 2s+10帧**
2000ms+ 10/30*1000ms 约为 2.3s
从上面数据来看,怪不得 我使用拼多多之后,打开app 确实比较舒服,因为我就是奔着买东西去的,越快到购物页面越舒服的。或许这就是极致的用户体验吧
首屏细节
拼多多的首页数据咋这么快就准备好了,网络耗时应该也有呢,应该是它提前准备好了数据
我们来实操验证下
- 切后台的截图
我们记住 手枪、去虾线、行李箱、停电 这几个卡片
冷启打开之后首先展示的是 还是切后台之前的数据
紧接着网络数据到了做了一次屏幕刷新
到这里大概就明白了,冷启使用上次feeds流的数据,先让用户看到数据,然后等新数据请求到之后再刷新页面就好
为了严谨点,把缓存数据清除的话,那么肯定首次冷启白屏,ok最后再验证一下
此时冷启白茫茫的一片,看来拼多多的策略还是让用户尽快进应用优先,或者这里并没有刻意设计🤔,都是先进首页有缓存就使用 没有的话就等网络数据,毕竟这种情况也只是新用户或者缓存数据过期才会这样
因此这里我可以得出把这种缓存优先的技术方案也可以学习学习,看看我们自己的app是不是可以复用一下,绩效这不就来了吗🤔
首页 = 数据 + UI
数据是使用缓存,UI也能吧一些UI组件提前预加载,不过这里也无法判断 是否预加载了首页UI🤔
开屏无广告
我目前在字节就是搞广告的,所以对广告稍微敏感些,开屏广告是一个很棒收入来源,特别是合约广告这种,之前应用冷启时间长,有时候其实是故意抽出一些时间来等待冷启的开屏广告,
但是我试了很多次,确实没看过拼多多的开屏广告,不过从这个结果来看 肯定是 经过严密的ab实验,不过拼多多在开屏广告上确实比较克制,
关于现在互联网的计算广告业务还是蛮有意思的比如 广告类型有 开屏、原生、激励、插屏、横幅,sdk类型有单个adn或者聚合广告sdk,有时间再单独分享几篇。
冷启优化一些常见手段
冷启动往往是大型应用的必争之地
- 实打实的提升用户体验
- 可能会带来一些GMV的转化
拼多多技术是应该是有些东西的,但是非常低调,属于人狠话不多那种,也没找到他们的方案。这里结合自身经验聊聊这块,主要是以下4个阶段结合技术手段做优化
Application attachBaseContext
这个阶段由于 Applicaiton Context 赋值等问题,一般不会有太多的业务代码,可能的耗时会在低版本机器4.x机器比较多,首次由于MultiDex.install耗时
dex 的指令格式设计并不完善,单个 dex 文件中引用的 Java 方法总数不能超过 65536 个,在方法数超过 65536 的情况下,将拆分成多个 dex。一般情况下 Dalvik 虚拟机只能执行经过优化后的 odex 文件,在 4.x 设备上为了提升应用安装速度,其在安装阶段仅会对应用的首个 dex 进行优化。对于非首个 dex 其会在首次运行调用 MultiDex.install 时进行优化,而这个优化是非常耗时的,这就造成了 4.x 设备上首次启动慢的问题。
可以使用一些开源方案,比如 github.com/bytedance/B…
不过 这里优化难度比较大,roi的话 看看app低版本的机型占比再做决定
ContentProvider
这里要注意检查 ContentProvider,特别是一些sdk在 AndroidManifest 里面注册了自己的 xxSDkProvider,然后在 xxSDkProvider 的 onCreate 方面里面进行初始化,确实调用者不需要自己初始化了,可却增加了启动耗时,
我们可以打开 Apk,看一下最终merge的 AndroidManiest 里面有多少 provider,看一下是否有这样的骚操作,往往这里容易忽视,这种情况可以使用谷歌App Startup来收敛ContentProvider
Application 优化
- 精简Application 中的启动任务
- 基于进程进行任务排布,比如常见的push进程、webview进程
西瓜视频 在冷启优化就将 push、小程序、sandboxed这几个进程做了优化拿到一些不错的收益mp.weixin.qq.com/s/v23jEhF9k…
搞进程难度大风险高
- 启动链路任务编排
这里需要先梳理启动链路,做成1任务编排,
- 比如之前串2.2行的,搞成并行初始化
- 核心任务做有向无环图(DGA)编排,非核心的延迟初始化
idlehandler是个好东西。
关于初始化DGA框架有不少框架,谷歌官方也有个 App Startup,感兴趣可以研究下
首页优化
首页是用户感知到的第一个页面,也是冷启优化的关键,前面也提过 首页 = 数据 + UI
- 数据 可以使用缓存
- UI的话 通常是xml解析优化,或者预加载
在性能较差的手机上,xml inflate 的时间可能在 200 到 500 毫秒之间。自定义控件和较深的 UI 层级会加重这个解析耗时。
一些框架比如x2c,或者AsyncLayoutInflater 可以帮助我们在UI这里做做文章
- 插件化
把非核心模块做成插件,使用时候下载使用,一劳永逸,不过插件化也有各种弊端
后台任务优化
主线程相关耗时的优化,事实上除了主线程直接的耗时,后台任务的耗时也是会影响到我们的启动速度的,因为它们会抢占我们前台任务的 cpu、io 等资源,导致前台任务的执行时间变长,因此我们在优化前台耗时的同时也需要优化我们的后台任务
- 减少后台线程不必要的任务的执行,特别是一些重 CPU、IO 的任务;
- 对启动阶段线程数进行收敛,防止过多的并发任务抢占主线程资源,同时也可以避免频繁的线程间调度降低并发效率
- GC 抑制
触发 GC 后可能会抢占我们的 cpu 资源甚至导致我们的线程被挂起,如果启动过程中存在大量的 GC,那么我们的启动速度将会受到比较大的影响。通过hook手段在启动阶段去抑制部分类型的 GC,以达到减少 GC 的目的。这个就比较高端了,也是只在一些大厂文章里面见过。
OK 本期就到这里了
来源:juejin.cn/post/7331607384932876326
请不要自己写,Spring Boot非常实用的内置功能
在 Spring Boot 框架中,内置了许多实用的功能,这些功能可以帮助开发者高效地开发和维护应用程序。
松哥来和大家列举几个。
一 请求数据记录
Spring Boot提供了一个内置的日志记录解决方案,通过 AbstractRequestLoggingFilter
可以记录请求的详细信息。
AbstractRequestLoggingFilter
有两个不同的实现类,我们常用的是 CommonsRequestLoggingFilter
。
通过 CommonsRequestLoggingFilter
开发者可以自定义记录请求的参数、请求体、请求头和客户端信息。
启用方式很简单,加个配置就行了:
@Configuration
public class RequestLoggingConfig {
@Bean
public CommonsRequestLoggingFilter logFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setIncludeHeaders(true);
filter.setIncludeClientInfo(true);
filter.setAfterMessagePrefix("REQUEST ");
return filter;
}
}
接下来需要配置日志级别为 DEBUG,就可以详细记录请求信息:
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
二 请求/响应包装器
2.1 什么是请求和响应包装器
在 Spring Boot 中,请求和响应包装器是用于增强原生 HttpServletRequest
和 HttpServletResponse
对象的功能。这些包装器允许开发者在请求处理过程中拦截和修改请求和响应数据,从而实现一些特定的功能,如请求内容的缓存、修改、日志记录,以及响应内容的修改和增强。
请求包装器
ContentCachingRequestWrapper
:这是 Spring 提供的一个请求包装器,用于缓存请求的输入流。它允许多次读取请求体,这在需要多次处理请求数据(如日志记录和业务处理)时非常有用。
响应包装器
ContentCachingResponseWrapper
:这是 Spring 提供的一个响应包装器,用于缓存响应的输出流。它允许开发者在响应提交给客户端之前修改响应体,这在需要对响应内容进行后处理(如添加额外的头部信息、修改响应体)时非常有用。
2.2 使用场景
- 请求日志记录:在处理请求之前和之后记录请求的详细信息,包括请求头、请求参数和请求体。
- 修改请求数据:在请求到达控制器之前修改请求数据,例如添加或修改请求头。
- 响应内容修改:在响应发送给客户端之前修改响应内容,例如添加或修改响应头,或者对响应体进行签名。
- 性能测试:通过缓存请求和响应数据,可以进行性能测试,而不影响实际的网络 I/O 操作。
2.3 具体用法
请求包装器的使用
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class RequestWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
// 可以在这里处理请求数据
byte[] body = requestWrapper.getContentAsByteArray();
// 处理body,例如记录日志
//。。。
filterChain.doFilter(requestWrapper, response);
}
}
响应包装器的使用
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class ResponseWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
filterChain.doFilter(request, responseWrapper);
// 可以在这里处理响应数据
byte[] body = responseWrapper.getContentAsByteArray();
// 处理body,例如添加签名
responseWrapper.setHeader("X-Signature", "some-signature");
// 必须调用此方法以将响应数据发送到客户端
responseWrapper.copyBodyToResponse();
}
}
在上面的案例中,OncePerRequestFilter
确保过滤器在一次请求的生命周期中只被调用一次,这对于处理请求和响应数据尤为重要,因为它避免了在请求转发或包含时重复处理数据。
通过使用请求和响应包装器,开发者可以在不改变原有业务逻辑的情况下,灵活地添加或修改请求和响应的处理逻辑。
三 单次过滤器
3.1 OncePerRequestFilter
OncePerRequestFilter
是 Spring 框架提供的一个过滤器基类,它继承自 Filter
接口。这个过滤器具有以下特点:
- 单次执行:
OncePerRequestFilter
确保在一次请求的生命周期内,无论请求如何转发(forwarding)或包含(including),过滤器逻辑只执行一次。这对于避免重复处理请求或响应非常有用。 - 内置支持:它内置了对请求和响应包装器的支持,使得开发者可以方便地对请求和响应进行包装和处理。
- 简化代码:通过继承
OncePerRequestFilter
,开发者可以减少重复代码,因为过滤器的执行逻辑已经由基类管理。 - 易于扩展:开发者可以通过重写
doFilterInternal
方法来实现自己的过滤逻辑,而不需要关心过滤器的注册和执行次数。
3.2 OncePerRequestFilter 使用场景
- 请求日志记录:在请求处理之前和之后记录请求的详细信息,如请求头、请求参数和请求体,而不希望在请求转发时重复记录。
- 请求数据修改:在请求到达控制器之前,对请求数据进行预处理或修改,例如添加或修改请求头,而不希望这些修改在请求转发时被重复应用。
- 响应数据修改:在响应发送给客户端之前,对响应数据进行后处理或修改,例如添加或修改响应头,而不希望这些修改在请求包含时被重复应用。
- 安全控制:实现安全控制逻辑,如身份验证、授权检查等,确保这些逻辑在一次请求的生命周期内只执行一次。
- 请求和响应的包装:使用
ContentCachingRequestWrapper
和ContentCachingResponseWrapper
等包装器来缓存请求和响应数据,以便在请求处理过程中多次读取或修改数据。 - 性能监控:在请求处理前后进行性能监控,如记录处理时间,而不希望这些监控逻辑在请求转发时被重复执行。
- 异常处理:在请求处理过程中捕获和处理异常,确保异常处理逻辑只执行一次,即使请求被转发到其他处理器。
通过使用 OncePerRequestFilter
,开发者可以确保过滤器逻辑在一次请求的生命周期内只执行一次,从而避免重复处理和潜在的性能问题。这使得 OncePerRequestFilter
成为处理复杂请求和响应逻辑时的一个非常有用的工具。
OncePerRequestFilter
的具体用法松哥就不举例了,第二小节已经介绍过了。
四 AOP 三件套
在 Spring 框架中,AOP(面向切面编程)是一个强大的功能,它允许开发者在不修改源代码的情况下,对程序的特定部分进行横向切入。AopContext
、AopUtils
和 ReflectionUtils
是 Spring AOP 中提供的几个实用类。
我们一起来看下。
4.1 AopContext
AopContext
是 Spring 框架中的一个类,它提供了对当前 AOP 代理对象的访问,以及对目标对象的引用。
AopContext
主要用于获取当前代理对象的相关信息,以及在 AOP 代理中进行一些特定的操作。
常见方法有两个:
getTargetObject()
: 获取当前代理的目标对象。currentProxy()
: 获取当前的代理对象。
其中第二个方法,在防止同一个类中注解失效的时候,可以通过该方法获取当前类的代理对象。
举个栗子:
public void noTransactionTask(String keyword){ // 注意这里 调用了代理类的方法
((YourClass) AopContext.currentProxy()).transactionTask(keyword);
}
@Transactional
void transactionTask(String keyword) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) { //logger
//error tracking
}
System.out.println(keyword);
}
同一个类中两个方法,noTransactionTask 方法调用 transactionTask 方法,为了使事务注解不失效,就可以使用 AopContext.currentProxy() 去获取当前代理对象。
4.2 AopUtils
AopUtils
提供了一些静态方法来处理与 AOP 相关的操作,如获取代理对象、获取目标对象、判断代理类型等。
常见方法有三个:
getTargetObject()
: 从代理对象中获取目标对象。isJdkDynamicProxy(Object obj)
: 判断是否是 JDK 动态代理。isCglibProxy(Object obj)
: 判断是否是 CGLIB 代理。
举个栗子:
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
public class AopUtilsExample {
public static void main(String[] args) {
MyService myService = ...
// 假设 myService 已经被代理
if (AopUtils.isCglibProxy(myService)) {
System.out.println("这是一个 CGLIB 代理对象");
}
}
}
4.3 ReflectionUtils
ReflectionUtils
提供了一系列反射操作的便捷方法,如设置字段值、获取字段值、调用方法等。这些方法封装了 Java 反射 API 的复杂性,使得反射操作更加简单和安全。
常见方法:
makeAccessible(Field field)
: 使私有字段可访问。getField(Field field, Object target)
: 获取对象的字段值。invokeMethod(Method method, Object target, Object... args)
: 调用对象的方法。
举个栗子:
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.Map;
public class ReflectionUtilsExample {
public static void main(String[] args) throws Exception {
ExampleBean bean = new ExampleBean();
bean.setMapAttribute(new HashMap<>());
Field field = ReflectionUtils.findField(ExampleBean.class, "mapAttribute");
ReflectionUtils.makeAccessible(field);
Object value = ReflectionUtils.getField(field, bean);
System.out.println(value);
}
static class ExampleBean {
private Map<String, String> mapAttribute;
public void setMapAttribute(Map<String, String> mapAttribute) {
this.mapAttribute = mapAttribute;
}
}
}
还有哪些实用内置类呢?欢迎小伙伴们留言~
来源:juejin.cn/post/7417630844100231206
VirtualList虚拟列表
首先感谢
Vue3 封装不定高虚拟列表 hooks,复用性更好!这篇文章提供的一些思路,在此基础作者进一步对相关代码进行了一些性能上的优化(解决了通过鼠标操作滚动条时的卡顿)。因为项目没有用到ts,就先去掉了。
hooks
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
export default function useVirtualList(config) {
// 获取元素
let actualHeightContainerEl = null,
translateContainerEl = null,
scrollContainerEl = null;
// 数据源,便于后续直接访问
let dataSource = [];
onMounted(() => {
actualHeightContainerEl = document.querySelector(
config.actualHeightContainer
);
scrollContainerEl = document.querySelector(config.scrollContainer);
translateContainerEl = document.querySelector(config.translateContainer);
});
// 数据源发生变动
watch(
() => config.data.value,
(newValue) => {
// 更新数据源
dataSource = newValue;
// 计算需要渲染的数据
updateRenderData();
}
);
/*
更新相关逻辑
*/
// 更新实际高度
let flag = false;
const updateActualHeight = (oldValue, value) => {
let actualHeight = 0;
if (flag) {
// 修复偏差
actualHeight =
actualHeightContainerEl.offsetHeight -
(oldValue || config.itemHeight) +
value;
} else {
// 首次渲染
flag = true;
for (let i = 0; i < dataSource.length; i++) {
actualHeight += getItemHeightFromCache(i);
}
}
actualHeightContainerEl.style.height = `${actualHeight}px`;
};
// 缓存已渲染元素的高度
const RenderedItemsCache = {};
const RenderedItemsCacheProxy = new Proxy(RenderedItemsCache, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 更新实际高度
updateActualHeight(oldValue, value);
return result;
},
});
// 更新已渲染列表项的缓存高度
const updateRenderedItemCache = (index) => {
// 当所有元素的实际高度更新完毕,就不需要重新计算高度
const shouldUpdate =
Reflect.ownKeys(RenderedItemsCacheProxy).length < dataSource.length;
if (!shouldUpdate) return;
nextTick(() => {
// 获取所有列表项元素(size条数)
const Items = Array.from(document.querySelectorAll(config.itemContainer));
// 进行缓存(通过下标作为key)
for (let i = 0; i < Items.length; i++) {
const el = Reflect.get(Items, i);
const itemIndex = index + i;
if (!Reflect.get(RenderedItemsCacheProxy, itemIndex)) {
Reflect.set(RenderedItemsCacheProxy, itemIndex, el.offsetHeight);
}
}
});
};
// 获取缓存高度,无缓存,取配置项的 itemHeight
const getItemHeightFromCache = (index) => {
const val = Reflect.get(RenderedItemsCacheProxy, index);
return val === void 0 ? config.itemHeight : val;
};
// 实际渲染的数据
const actualRenderData = ref([]);
// 更新实际渲染数据
const updateRenderData = (scrollTop = 0) => {
let startIndex = 0;
let offsetHeight = 0;
for (let i = 0; i < dataSource.length; i++) {
offsetHeight += getItemHeightFromCache(i);
// 第几个以上进行隐藏
if (offsetHeight >= scrollTop - (config.offset || 0)) {
startIndex = i;
break;
}
}
// 计算得出的渲染数据
actualRenderData.value = dataSource
.slice(startIndex, startIndex + config.size)
.map((data, idx) => {
return {
key: startIndex + idx + 1, // 为了在vue的for循环中绑定唯一key值
data,
};
});
// 缓存最新的列表项高度
updateRenderedItemCache(startIndex);
updateOffset(offsetHeight - getItemHeightFromCache(startIndex));
};
// 更新偏移值
const updateOffset = (offset) => {
translateContainerEl.style.transform = `translateY(${offset}px)`;
};
/*
注册事件、销毁事件
*/
// 滚动事件
const handleScroll = (e) =>
// 渲染正确的数据
updateRenderData(e.target.scrollTop);
// 注册滚动事件
onMounted(() => {
scrollContainerEl?.addEventListener("scroll", handleScroll);
});
// 移除滚动事件
onBeforeUnmount(() => {
scrollContainerEl?.removeEventListener("scroll", handleScroll);
});
return { actualRenderData };
}
vue
<script setup>
import { ref } from "vue";
import useVirtualList from "../utils/useVirtualList.js"; // 上面封装的hooks文件
import list from "../json/index.js"; // 造的数据模拟
const tableData = ref([]);
// 模拟异步请求
setTimeout(() => {
tableData.value = list;
}, 0);
const { actualRenderData } = useVirtualList({
data: tableData, // 列表项数据
scrollContainer: ".scroll-container", // 滚动容器
actualHeightContainer: ".actual-height-container", // 渲染实际高度的容器
translateContainer: ".translate-container", // 需要偏移的目标元素,
itemContainer: ".item", // 列表项
itemHeight: 400, // 列表项的大致高度
size: 10, // 单次渲染数量
offset: 200, // 偏移量
});
</script>
<template>
<div>
<h2>virtualList 不固定高度虚拟列表</h2>
<ul class="scroll-container">
<div class="actual-height-container">
<div class="translate-container">
<li
v-for="item in actualRenderData"
:key="item.key"
class="item"
:class="[{ 'is-odd': item.key % 2 }]"
>
<div class="item-title">第{{ item.key }}条:</div>
<div>{{ item.data }}</div>
</li>
</div>
</div>
</ul>
</div>
</template>
<style scoped>
* {
list-style: none;
padding: 0;
margin: 0;
}
.scroll-container {
border: 1px solid #000;
width: 1000px;
height: 200px;
overflow: auto;
}
.item {
border: 1px solid #ccc;
padding: 20px;
display: flex;
flex-wrap: wrap;
word-break: break-all;
}
.item.is-odd {
background-color: rgba(0, 0, 0, 0.1);
}
</style>
来源:juejin.cn/post/7425598941859102730
谈谈我做 Electron 应用的这一两年
大家好,我是徐徐。今天和大家谈谈我做 Electron 桌面端应用的这一两年,把一些经验和想法分享给大家。
前言
入职现在这家公司三年了,刚进公司的时候是 21 年年初,那时候会做一些稍微复杂的后台管理系统以及一些简单的 C 端 SDK。准备开始做 Electron 项目是因为我所在的是安全部门,急需一款桌面管控软件来管理(监控)员工的电脑安全以及入网准入,可以理解为一款零信任的桌面软件。其实之前公司也有一款安全管控的软件,但是Windows 和 Mac是分端构建的,而且维护成本极高,Windows 是使用的 C#, Mac 是用的 Objective-C,开发和发版效率低下,最后在研发老大的同意下,我和另外一个同事开始研究如何用 Electron 这个框架来做一款桌面端软件。
我们发起这个项目大概是在 21 年年底,Windows 版本上线是在上海疫情封城期间,2022年4月份的时候,疫情结束后由于事业部业务方向的调整,又被抽调到了另外一个组去做一个 C 端的创业项目,后面项目结束了,又回来做 Electron 相关的工作直到现在,之所以是一两年,其实就是这个时间线。
对桌面端开发的一些看法
如果你是前端的话,多一门桌面端开发的技能也不是坏事,相当是你的一个亮点,进可攻,退可守。因为桌面端开发到后期的架构可以非常的复杂,不亚于服务端(chromium 就是一个例子),当然也取决于你所应对的场景的挑战,如果所做的产品跟普通前端无异,那也不能说是一个亮点,但是如果你的工作已经触及到一些操作系统的底层,那肯定是一个亮点。
当然也有人说,做桌面端可能就路越走越窄了,但是我想说的是深度和广度其实也可以理解为一个维度,对于技术人来说,知道得越多就行,因为到后期你要成为某个方面的专家,就是可能会非常深入某一块,换一种思路其实也是叫知道得越多。所以,我觉得前端能有做桌面端的机会也是非常好的,即拓展了自己的技能,还能深入底层,因为现阶段由于业务方向的需要,我已经开始看 chromium 源码了,前端的老祖宗。当然,以上这些只代表自己的观点,大家自行斟酌。
谈谈 Electron
其实刚刚工作前两年我就知道这个框架了,当时也做过小 demo,而且还在当时的团队里面分享过这个技术,但是当时对这门技术的认知是非常浅薄的,就知道前端可以做桌面端了,好厉害,大概就停留在这个层面。后面在真正需要用到这门技术去做一个企业级的桌面应用的时候才去真正调研了一下这个框架,然后才发现它真的非常强大,强大到几乎可以实现你桌面端的任何需求。网上关于 Electron 与其他框架的对比实在是太多了,Google 或者 Baidu 都能找到你想要的答案,好与不好完全看自己的业务场景以及自己所在团队的情况。
谈谈自己的感受,什么情况下可以用这门框架
- 追求效率,节省人力财力
- 团队前端居多
- UI交互多
什么情况下不适合这门框架呢?
- 包体积限制
- 性能消耗较高的应用
- 多窗口应用
我们当时的情况就是要追求效率,双端齐头并进,所以最后经过综合对比,选择了 Electron。毕竟 Vscode 就是用它做的,给了我们十足的信心和勇气,一点都不虚。
一图抵千字,我拿出这张图你自己就有所判断了,还是那句话,仁者见仁,智者见智,完全看自己情况。
图片来源:http://www.electronjs.org/apps
技术整体架构
这里我画了一张我所从事 Electron 产品的整体技术架构图。
整个项目基于 Vite 开发构建的,基础设施就是常见的安全策略,然后加上一些本地存储方案,外加一个外部插件,这个插件是用 Tauri 做的 Webview,至于为什么要做这个插件我会在后面的段落说明。应用层面的框架主要是分三个大块,下面主要是为了构建一些基础底座,然后将架构进行分层设计,添加一些原生扩展,上面就是基础的应用管理和 GUI 相关的东西,有了这个整体的框架在后面实现一些业务场景的时候就会变得易如反掌(夸张了一点,因为有的技术细节真的很磨人😐)。
当然这里只是一个整体的架构图,其实还有很多技术细节的流程图以及业务场景图并没有在这里体现出来,不过我也会挑选一些方案在后面的篇幅里面做出相应的讲解。
挑战和方案
桌面端开发会遇到一些挑战,这些挑战大部分来源于特殊的业务场景,框架只能解决一些比较常见的应用场景,当然不仅仅是桌面端,其实移动端或者是 Web 端我相信大家都会遇到或多或少的挑战,我这里遇到的一些挑战和响应的方案不一定适合你,只是做单纯的记录分享,如果有帮助到你,我很开心。下面我挑选软件升级更新,任务队列设计,性能检测优化以及一些特殊的需求这几个方面来聊聊相应的挑战和方案。
软件升级更新
桌面端的软件更新升级是桌面端开发中非常重要的一环,一个好的商业产品必须有稳定好用的解决方案。桌面端的升级跟 C 端 App 的升级其实也是差不多的思路,虽然我所做的产品是公司内部人使用,但是用户也是你面向公司所有用户的,所以跟 C 端产品的解决思路其实是无异的。
升级更新主要是需要做到定向灰度。这个功能是非常重要的,应该大部分的应用都有定向灰度的功能,所以我们为了让软件能够平滑升级,第一步就是实现定向灰度,达到效果可回收,性能可监控,错误可告警的目的。定向更新的功能实现了之后,后面有再多的功能需要实现都有基础保障了,下面是更新相关的能力图。
整个更新模块的设计是分为两大块,一块是后台管理系统,一块是客户端。后台管理系统主要是维护相应的策略配置,比如哪些设备需要定向更新,哪些需要自动更新,不需要更新的白名单以及更新后是需要提醒用户相应的更新功能还是就静默更新。客户端主要就是拉取相应的策略,然后执行相应的更新动作。
由于我们的软件是比较特殊的一个产品,他是需要长期保活的,Mac 端上了文件锁是无法删除的。所以我们在执行更新的时候和常规的软件更新是不一样的,软件的更新下载是利用了 electron-update 相应的钩子,但是安装的时候并没有使用相应的钩子函数,而是自己研究了 electron 的更新源码后做了自己的更新脚本。 因为electron 的更新它自己也会注册一个保活的更新任务的服务,但是这个和我们的文件锁和保活是冲突的,所以是需要禁用掉它的保活服务,完成自己的更新。
整体来说,这一块是花了很多时间去研究的,windows 还好,没有破坏其整个生命周期,傻瓜式的配置一下electron-update 相关的函数钩子就可以了。Mac 的更新花了很多时间,因为破坏了文件的生命周期,再加上保活任务,所以会对 electron-update 的更新钩子进行毁灭性的破坏,最后也只能研究其源码然后自己去实现特殊场景下的更新了。
任务队列设计
任务模块的实现在我们这个软件里面也是非常重要的一环,因为客户端会跑非常多的定时任务。刚开始研发这个产品的时候其实还好,定时任务屈指可数,但是随着长时间的迭代,端上要执行的任务越来越多,每个任务的触发时间,触发条件都不一样,以及还要考虑任务的并发情况和对性能的影响,所以在中后期我们对整个任务模块都做了相应的改造。
下面是整个任务模块的核心能力图。
业界也有一些任务相关的开源工具包,比如 node-schedule、node-cron、cron,这些都是很优秀的库,但是我在使用过程中发现他们好像不具备并发限制的场景,比如有很多任务我们在开始设置的时候都会有个时间间隔,这些任务的时间间隔都是可以在后台随意配置的,如果端上不做并发限制会导致一个问题,就是用户某一瞬间会觉得电脑非常卡。
比如你有 4 个 10 分钟间隔的任务 和 2 个五分钟间隔的任务,那么到某一个时间段,他最大并发可能就是 6,如果刚好这 6 个任务都是非常耗费 CPU 的任务,那他们一起执行的时候就会导致整个终端CPU 飙升,导致用户感觉卡顿,这样就会收到相应的 Diss。
安全类的软件产品其实有的时候不需要太过醒目,后台默默运行就行,所以我们的宗旨就是稳定运行,不超载。为此我们就自己实现了相应的任务队列模式,然后去控制任务并发。其实底层逻辑也不难,就是一个 setInterval 的函数,然后不断的创建销毁,读取队列的函数,执行相应的函数。
性能优化
Electron相关的性能优化其实网上也有非常多的文章,我这里说说我的实践和感受。
首先,性能优化你需要优化什么?这个就是你的出发点了,我们要解决一个问题,首先得知道问题的现状,如果你都不知道现在的性能是什么样子,如何去优化呢?所以发现问题是性能优化的最重要的一步。
这里就推荐两个工具,一个是chrome dev-tool,一个是electron 的 inspector,第一个可以观测渲染进程相关的性能情况,第二个可以观测主进程相关的性能情况。
具体可参考以下网址:
有了工具之后我们就需要用工具去分析一些数据和问题,这里面最重要的就是内存相关的分析,你通过内存相关的分析可以看到 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);
});
一些特殊的需求
做这个产品也遇到一些特殊的需求,有的需求还挺磨人的,这里也和大家分享一下。
- 保活和文件锁
作为一个前端,桌面端的保活和文件锁这种需求基本是之前不可能接触到的,为了做这个需求也去了解了一下业界的实现,其实实现都还好,主要是它会带来一些问题,诸如打包构建需要自定义前置脚本和后置脚本,root 用户环境下 mac 端无法输入中文,上面提到的用 Tauri 构建一个 webview 组件就是为了解决 root 用户无法输入中文的场景。
- 静默安装应用
这个需求也是很绝的一个需求。我想如果是做常规的前端开发,估计一般都不会遇到这种需求,你需要从头到尾实现一个下载器,一个软件安装器,而且还要双端适配,不仅如此,还需要实现 exe、zip、dmg、pkg 等各种软件格式的安装,里面包含重试机制,断点下载,队列下载等各种技术细节。当时接到这个需求头也特别大,不过技术方案做出来后感觉也还好,再复杂的需求只要能理清思路,其实都可以慢慢解决。
- VPN 和 访问记录监控
这种需求对一个前端来说更是无从入手,但是好在之前有老版本的 VPN 做参考,就是根据相应的代码翻译一遍也能实现,大部分可以用命令行解决。至于访问记录监控这个玩意咋说呢,客户端做其实也挺费神的,如果不借助第三方的开源框架,自己是非常难实现的,所以这种就是需要疯狂的翻国外的网站,就GitHub,Stackoverflow啰,总有一款适合你,这里就不具体说明了。
- 进程禁用
违规进程禁用其实在安全软件的应用场景是非常常见的,它需要实时性,而且对性能要求很高,一个是不能影响用户正常使用,还要精准杀掉后台配置的违规进程,这个地方其实也是做了很多版优化,但是最后的感觉还是觉得任务队列有性能瓶颈,无法达到要求,现阶段我们也在想用另外的方式去改造,要么就是上全局钩子,要么就是直接把相应的进程文件上锁或者改文件权限。
上面所提到的需求只是一小部例子,还好很多奇奇怪怪的需求没有举例,这些奇怪的需求就像小怪物,不断挑战我的边界,让我也了解和学习和很多奇奇怪怪的知识,有的时候我就会发出这样的感叹:我去,还能这样?
结语
洋洋洒洒,不知不觉已经写了 5000 字了,其实做 Electron 桌面端应用的这一两年自我感觉还是成长了不少,不管是技术方面还是产品设计方面,自己的能力都有所提升。但是同样会遇到瓶颈,就是一个东西一直做一直做,到后面创新会比较难,取得的成就也会慢慢变少。
另外就是安全类的桌面端产品在整个软件开发的里其实是非常冷门的一个领域,他有他的独特性,也有相应的价值,他需要默默的运行,稳定的运行,出问题可以监控到,该提醒的时候提醒用户。你说他低调吧,有时候也挺高调的,真的不好定论,你说没影响力吧,有的时候没他还真不行。让用户不反感这种软件,拥抱这种软件其实挺难的。从一个前端开发的视角来看,桌面端的体验的确很重要,不管是流畅度还是美观度,都不能太差,这也是我们现阶段追求的一个点,就是不断提升用户体验。
路漫漫其修远兮,吾将上下而求索。前端开发这条路的确很长,如果你想朝某个方面深度发展,你会发现边界是非常难触达的,当然也看所处的环境和对应的机遇,就从技术来说的话,前端的天花板也可以很高,不管是桌面端,服务端,移动端,Web端,每个方向前端的天花板都非常难触摸到。
最后,祝大家在自己的领域越来越深,早日触摸到天花板。
原文链接
来源:juejin.cn/post/7399100662610395147
为什么程序员的社会地位不高?
互联网时代,程序员承担着数字世界构建和技术发展的大任,如此重要,为什么存在感不高,社会地位不高呢?
知乎上针对这个问题也有过讨论,分享给大家。
http://www.zhihu.com/question/58…
回答1
什么是社会地位?
社会地位可以简化成,一个人可支配社会资源的数量,例如:
医生 医疗资源
教师 教育资源
...
而程序员可支配的社会资源只有他自己。从这一点上说,程序员和工人没有本质上的区别。
时代的红利成就了这个职业,抛弃它的时候,一样不会留情。
回答2
程序员作为一种社会职业,既没有政府职能部门的公权力,又没有有钱人的一掷千金,挣得也都是辛苦钱,何来社会地位高不高一说,无非就是资本的韭菜罢了。
回答3
这个问题我曾经思考过很久。按知乎的习惯,先问是不是,再问为什么。
首先说“是不是”。
按大家的直觉也好,或者现有的各个社区讨论来看,程序员的社会地位肯定不是高的。
最多有人说程序员的社会地位和其他职业一样高,但没见过谁说程序员的社会地位能高过GWY,医生,老师的。这么说来,“是不是”这个问题已经基本没有大的争议——在公众认知内,程序员的社会地位的确不高。
再来就是“为什么”。
这个为什么是我想了很久了,如果单独拿程序员和某个职业/行业比较,可以有很多个维度的对比,但如果想把大部分的职业/行业进行对比,需要找一个更有共性的比较方式,或者说是能归纳出比较重要的影响因素。对此,我归纳出来的最主要因素是“自由裁量权”。
这里的“自由裁量权”,又分为两个维度:
第一个是权力本身影响后果的大小,比如影响10块钱和影响10亿元的大小肯定不一样;
第二个是权力影响的范围,比如影响一个区和影响全国肯定不一样。
这里举电视剧《人民的名义》里面的人物来说明这一点。
第一个剧中是京州市副市长兼光明区区委书记丁义珍。丁义珍是“负责土地划批,矿产资源整合,还有老城改造”,这里无论是土地划批给某开发商,或者矿产资源交给哪个煤老板开挖,对于这些开发商和煤老板,都一笔稳赚不赔的买卖。而剧中的丁义珍在具体能把这块地或者这片矿批给谁上面,有很大的自由裁量权,也就是说,他能在规则范围之内,把地给批了。于是各个房地产开发商老板,煤矿老板都要找丁义珍去批地批矿,自然丁义珍社会地位就高了。
第二个是京州市城市银行副行长欧阳菁。作为银行副行长,很多带款她拥有最终决定权。是放贷或者不放贷,放贷放给哪个企业,她拥有决定权力,甚至还能影响汉东农信社的决定。比如在蔡成功申请六千万的带款的事情上,欧阳菁一直阻挠,甚至打电话让汉东农信社不给蔡成功带款。为什么以前能贷给蔡成功,而这次不行了呢,那是因为之前每次过桥贷蔡成功都给欧阳菁50万好处费,而这次没有。
从以上两个例子可以出,无论是在ZF,还是银行这种企业里,当官至一定地位时,就拥有了影响社会面的一定量的自由裁量权。无论是丁义珍还是欧阳菁,他们的自由裁量权总体上还是在规则之内运行的,没有明显超出规则之外。要不是赵德汉被查,丁义珍还没那么快会被抓以至于后面要逃亡国外。而欧阳菁如果不是因为侯亮平下来查山水集团等案子,也不会露出马脚。
在最开始说了,自由裁量权除了影响的后果大小,还有涉及面的大小,比如丁义珍和欧阳菁的影响力,主要还是在京州市之内,出了京州市,尤其是出了汉东省,他们也影响不到啥。而剧中的第一个出场的贪官赵德汉,就有影响全国资源项目的审核权,这就是影响面的区别了。所以才有那个全国各地都有人找赵德汉,在他办公室门口排队的事情了。
通过《人民的名义》这三个例子,自由裁量权的影响力和影响面应该都有一定的了解了。
那么我们回过头来看现实中的程序员,这个职业带来的对于社会影响的自由裁量权,可以看出是非常小的,影响面也非常不适合操作。
首先,程序员可以决定程序的技术架构和代码,但很难影响其功能。真正决定功能是怎么样的,是产品经理(网站、APP类)、策划(游戏类)、甲方(to B和to G类),程序员本身几乎没有话语权,即没有自由裁量权,更多地是执行权。即使程序员做到了manager,或者技术VP,甚至CTO,对于这些功能特性的影响都是有限的。
比如说你是某游戏的技术leader,过年了你侄子在玩这款游戏,他希望你帮他的角色属性全部乘以10,你也是做不到的,甚至在内部评审阶段都被砍了。从影响面的角度来看,如果程序员是做某个APP的,他没法影响同公司另一款APP怎么做,更别说影响别的公司的APP怎么做。用通俗点的话来说,无论是社会上的陌生人,还是亲戚朋友,希望找程序员去做一些其职业内能自由裁量的内容从而获益,是很难的。这也就是程序员社会不高的主要原因。
同理,按照这个框架,我们能分析其他的一些职业的社会地位,同时也能看到一些职业除了稳定之外,还有自由裁量权这一微妙的东西,让不少人甘愿去追逐。
全文完
或许,这些讨论,并不能改变现实。
我觉得我们要思考的是:
社会地位的标准到底是什么?
技术人如今的社会地位,合理不合理?
技术人做什么,能够改变自己的社会地位?
来源:juejin.cn/post/7425807410764546098
聊聊try...catch 与 then...catch
处理错误的两种方式:try...catch
与 then
、catch
在前端编程中,错误和异常处理是保证代码健壮性和用户体验的重要环节。JavaScript 提供了多种方式来处理错误,其中最常见的两种是 try...catch
和 Promise 的 then
、catch
,但是什么时候该用try...catch
,什么时候该用then
、catch
呢,下面将详细探讨这两种机制的区别及其适用场景。
一、try...catch
try...catch
是一种用于捕获和处理同步代码中异常的机制。其基本结构如下:
try {
// 可能会抛出异常的代码
} catch (error) {
// 处理异常
}
使用场景:
- 主要用于同步代码,尤其是在需要处理可能抛出的异常时。
- 适用于函数调用、操作对象、数组等传统代码中。
示例:
function divide(a, b) {
try {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
} catch (error) {
console.error(error.message);
}
}
divide(4, 0); // 输出: Cannot divide by zero
在这个例子中,如果 b
为零,则会抛出一个错误,并被 catch
块捕获。
二、then
和 catch
在处理异步操作时,使用 Promise 的 then
和 catch
方法是更加常见的做法。其结构如下:
someAsyncFunction()
.then(result => {
// 处理成功的结果
})
.catch(error => {
// 处理错误
});
使用场景:
- 主要用于处理异步操作,例如网络请求、文件读取等。
- 可以串联多个 Promise 操作,清晰地处理成功和错误。
示例:
function fetchData() {
return new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
const success = Math.random() > 0.5; // 随机决定成功或失败
if (success) {
resolve("Data fetched successfully");
} else {
reject("Failed to fetch data");
}
}, 1000);
});
}
fetchData()
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error);
});
在这个示例中,fetchData
函数模拟了一个异步操作,通过 Promise 来处理结果和错误。
三、async/await
与 try...catch
为了使异步代码更具可读性,JavaScript 引入了 async/await
语法。结合 try...catch
,可以让异步错误处理更加简洁:
async function fetchDataWithAwait() {
try {
const result = await fetchData();
console.log(result);
} catch (error) {
console.error(error);
}
}
fetchDataWithAwait();
总结
try...catch
:适合于同步代码,能够捕获代码块中抛出的异常。then
和catch
:用于处理 Promise 的结果和错误,适合异步操作。async/await
结合try...catch
:提供了清晰的异步错误处理方式,增强了代码的可读性。
在实际开发中,选择哪种方式取决于代码的性质(同步或异步)以及个人或团队的编码风格。
往期推荐
怎么进行跨组件通信,教你如何使用provide 和 inject🔥
来源:juejin.cn/post/7418133347543121939
用零宽字符来隐藏代码
什么是零宽度字符
一种不可打印的Unicode字符,在浏览器等环境不可见,但是真是存在,获取字符串长度时也会占位置,表示某一种控制功能的字符。
常见的零宽字符有:
空格符:格式为U+null00B,用于较长字符的换行分隔;
非断空格符:格式为U+FEFF,用于阻止特定位置的换行分隔;
连字符:格式为U+null00D,用于阿拉伯文与印度语系等文字中,使不会发生连字的字符间产生连字效果;
断字符:格式为U+200C,用于阿拉伯文、德文、印度语系等文字中,阻止会发生连字的字符间的连字效果;
左至右符:格式为U+200E,用于在混合文字方向的多种语言文本中,规定排版文字书写方向为左至右;
右至左符:格式为U+200F : 用于在混合文字方向的多种语言文本中,规定排版文字书写方向为右至左;
使用零宽字符给信息加密
(function(window) {
var rep = { // 替换用的数据,使用了4个零宽字符代理二进制
'00': '\u200b',
'0null': '\u200c',
'null0': '\u200d',
'nullnull': '\uFEFF'
};
function hide(str) {
str = str.replace(/[^\x00-\xff]/g, function(a) { // 转码 Latin-null 编码以外的字符。
return escape(a).replace('%', '\\');
});
str = str.replace(/[\s\S]/g, function(a) { // 处理二进制数据并且进行数据替换
a = a.charCodeAt().toString(2);
a = a.length < 8 ? Array(9 - a.length).join('0') + a : a;
return a.replace(/../g, function(a) {
return rep[a];
});
});
return str;
}
var tpl = '("@code".replace(/.{4}/g,function(a){var rep={"\u200b":"00","\u200c":"0null","\u200d":"null0","\uFEFF":"nullnull"};return String.fromCharCode(parseInt(a.replace(/./g, function(a) {return rep[a]}),2))}))';
window.hider = function(code, type) {
var str = hide(code); // 生成零宽字符串
str = tpl.replace('@code', str); // 生成模版
if (type === 'eval') {
str = 'eval' + str;
} else {
str = 'Function' + str + '()';
}
return str;
}
})(window);
var code = hider('测试一下');
console.log(code);
直接复制到项目中可以使用,我们现在来试试
var code = hider('测试一下');
console.log(code);
结果如下:
实际用法
功能用途
这个技术可以应用到很多领域,非常具有实用性。
比如:代码加密、数据加密、文字隐藏、内容保密、隐形水印,等等。
原理介绍
实现字符串隐形,技术原理是“零宽字符”。
在编程实现隐形字符功能时,先将字符串转为二进制,再将二进制中的1转换为\u200b;0转换为\u200c;空格转换为\u200d,最后使用\ufeff 零宽度非断空格符作分隔符。这几种unicode字符都是不可见的,因此最终转化完成并组合后,就会形成一个全不可见的“隐形”字符串。
功能源码
function text_2_binary(text){
return text.split('').map(function(char){ return char.charCodeAt(0).toString(2)}).join(' ');
}
function binary_2_hidden_text(binary){
return binary.split('').map(function (binary_num){
var num = parseInt(binary_num, 10);
if (num === 1) {
return '\u200b';
} else if(num===0) {
return '\u200c';
}
return '\u200d';
}).join('\ufeff')
}
var text = "jshaman是专业且强大的JS代码混淆加密工具";
var binary_text = text_2_binary(text);
var hidden_text = binary_2_hidden_text(binary_text);
console.log("原始字符串:",text);
console.log("二进制:",binary_text);
console.log("隐藏字符:",hidden_text,"隐藏字符长度:",hidden_text.length);
隐型还原
接下来介绍“隐形”后的内容如何还原。
在了解上文内容之后,知道了字符隐形的原理,再结合源代码可知:还原隐形内容,即进行逆操作:将隐形的unicode编码转化成二进制,再将二进制转成原本字符。
直接给出源码:
function hidden_text_2_binary(string){
return string.split('\ufeff').map(function(char){
if (char === '\u200b') {
return '1';
} else if(char === '\u200c') {
return '0';
}
return ' ';
}).join('')
}
function binary_2_Text(binaryStr){
var text = ""
binaryStr.split(' ').map(function(num){
text += String.fromCharCode(parseInt(num, 2));
}).join('');
return text.toString();
}
console.log("隐形字符转二进制:",hidden_text_2_binary(hidden_text));
console.log("二进制转原始字符:",binary_2_Text(hidden_text_2_binary(hidden_text)));
运行效果
如果在代码中直接提供“隐形”字符内容,比如ajax通信时,将“隐形”字符由后端传给前端,并用以上解密方法还原,那么这种方式传递的内容会是非常隐秘的。
但还是存在一个安全问题:他人查看JS源码,能看到解密函数,这可能引起加密方法泄露、被人推导出加密、解密方法。
前端的js想做到纯粹的加密目前是不可能的,因为 JavaScript 是一种在客户端执行的脚本语言,其代码需要在浏览器或其他 JavaScript 运行环境中解释和执行,由于需要将 JavaScript 代码发送到客户端,并且在客户端环境中执行,所以无法完全避免代码的逆向工程和破解。
来源:juejin.cn/post/7356208563101220915
前端如何生成临时链接?
前言
前端基于文件上传需要有生成临时可访问链接的能力,我们可以通过URL.createObjectURL
和FileReader.readAsDataUR
API来实现。
URL.createObjectURL()
URL.createObjectURL()
静态方法会创建一个 DOMString
,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document
绑定。这个新的URL 对象表示指定的 File
对象或 Blob
对象。
1. 语法
let objectURL = URL.createObjectURL(object);
2. 参数
用于创建 URL 的 File
对象、Blob
对象或者 MediaSource
对象。
3. 返回值
一个DOMString
包含了一个对象URL,该URL可用于指定源 object的内容。
4. 示例
"file" id="file">
document.querySelector('#file').onchange = function (e) {
console.log(e.target.files[0])
console.log(URL.createObjectURL(e.target.files[0]))
}
将上方console控制台打印的blob文件资源地址粘贴到浏览器中
blob:http://localhost:8080/1ece2bb1-b426-4261-89e8-c3bec43a4020
URL.revokeObjectURL()
在每次调用 createObjectURL()
方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL()
方法来释放。
浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。
1. 语法
window.URL.revokeObjectURL(objectURL);
2. 参数 objectURL
一个 DOMString
,表示通过调用 URL.createObjectURL()
方法返回的 URL 对象。
3. 返回值
undefined
4. 示例
"file" id="file">
<img id="img1" style="width: 200px;height: auto" />
<img id="img2" style="width: 200px;height: auto" />
document.querySelector('#file').onchange = function (e) {
const file = e.target.files[0]
const URL1 = URL.createObjectURL(file)
console.log(URL1)
document.querySelector('#img1').src = URL1
URL.revokeObjectURL(URL1)
const URL2 = URL.createObjectURL(file)
console.log(URL2)
document.querySelector('#img2').src = URL2
}
与FileReader.readAsDataURL(file)区别
1. 主要区别
- 通过
FileReader.readAsDataURL(file)
可以获取一段data:base64
的字符串 - 通过
URL.createObjectURL(blob)
可以获取当前文件的一个内存URL
2. 执行时机
createObjectURL
是同步执行(立即的)FileReader.readAsDataURL
是异步执行(过一段时间)
3. 内存使用
createObjectURL
返回一段带hash
的url
,并且一直存储在内存中,直到document
触发了unload
事件(例如:document close
)或者执行revokeObjectURL
来释放。FileReader.readAsDataURL
则返回包含很多字符的base64
,并会比blob url
消耗更多内存,但是在不用的时候会自动从内存中清除(通过垃圾回收机制)
4. 优劣对比
- 使用
createObjectURL
可以节省性能并更快速,只不过需要在不使用的情况下手动释放内存 - 如果不在意设备性能问题,并想获取图片的
base64
,则推荐使用FileReader.readAsDataURL
来源:juejin.cn/post/7333236033038778409
小程序海报绘制方案(原生,Uniapp,Taro)
背景
- 小程序海报绘制方案有很多,但是大多数都是基于canvas的,而且都是自己封装的,不够通用,不够灵活,不够简单,不够好用。
- 本方使用一个开源的小程序海报绘制,非常灵活,扩展性非常好,仅布局就能得到一张海报。
准备工作
安装依赖,也可以把源码下载到本地,源码地址。
npm install wxml2canvas
布局
无论哪种方案,布局都是一致的,需要注意一些暂未支持的属性:
- 变形:transform,但是节点元素使能读取此属性,但是库不支持,所以不要使用
- 圆角,border-radius,同上,不要使用,圆形图片有特定的属性去实现,除此之外无法实现其他类型的圆角
布局示例:
注意,除了uniapp,原生和Taro要使用原生组件的方式绘制canvas,因为Taro不支持data-xx的属性绑定方式,这一点很糟糕
<!-- 外层wrap用于fixed定位,使得整个布局离屏,离屏canvas暂未支持 -->
<view class='wrap'>
<!-- canvas id,一会 new 的时候需要 -->
<canvas canvas-id="poster-canvas"></canvas>
<view class="container">
<view data-type="text" data-text="测试文字绘制" class='text'>测试文字绘制</view>
<image data-type="image" data-src="https://img.yzcdn.cn/vant/cat.jpeg" class='image'></image>
<image data-type="radius-image" data-src="https://img.yzcdn.cn/vant/cat.jpeg" class='radius-image'></image>
</view>
</view>
原生小程序
import Wxml2Canvas from 'wxml2canvas'
Component({
methods: {
paint() {
wx.showLoading({ title: '生成海报' });
// 创建绘制实例
const drawInstance = new Wxml2canvas({
// 组件的this指向,组件内使用必传
obj: this,
// 画布宽高
width: 275,
height: 441,
// canvas-id
element: 'poster-canvas',
// 画布背景色
background: '#f0f0f0',
// 成功回调
finish: (url) => {
console.log('生成的海报url,开发者工具点击可预览', url);
wx.hideLoading();
},
// 失败回调
error: (err) => {
console.error(err);
wx.hideLoading();
},
});
// 节点数据
const data = {
list: [
{
// 此方式固定 wxml
type: 'wxml',
class: '.text', // draw_canvas指定待绘制的元素
limit: '.container', // 限定绘制元素的范围,取指定元素与它的相对位置计算
}
{
// 此方式固定 wxml
type: 'wxml',
class: '.image', // draw_canvas指定待绘制的元素
limit: '.container', // 限定绘制元素的范围,取指定元素与它的相对位置计算
}
{
// 此方式固定 wxml
type: 'wxml',
class: '.radius-image', // draw_canvas指定待绘制的元素
limit: '.container', // 限定绘制元素的范围,取指定元素与它的相对位置计算
}
]
}
// 调用绘制方法
drawInstance.draw(data);
}
}
})
Uniapp
uniapp 主要讲Vue3的版本,因为涉及 this,需要获取 this 以及时机
import { getCurrentInstance} from 'vue';
// 调用时机 setup内,不能在其他时机
// @see https://github.com/dcloudio/uni-app/issues/3174
const instance = getCurrentInstance();
function paint() {
uni.showLoading({ title: '生成海报' });
const drawInstance = new Wxml2Canvas({
width: 290, // 宽, 以iphone6为基准,传具体数值,其他机型自动适配
height: 430, // 高
element: 'poster-canvas', // canvas-id
background: '#f0f0f0',
obj: instance,
finish(url: string) {
console.log('生成的海报url,开发者工具点击可预览', url);
uni.hideLoading();
},
error(err: Error) {
console.error(err);
uni.hideLoading();
},
});
// 节点数据
const data = {
list: [
{
// 此方式固定 wxml
type: 'wxml',
class: '.text', // draw_canvas指定待绘制的元素
}
{
// 此方式固定 wxml
type: 'wxml',
class: '.image', // draw_canvas指定待绘制的元素
}
{
// 此方式固定 wxml
type: 'wxml',
class: '.radius-image', // draw_canvas指定待绘制的元素
}
]
}
// 调用绘制方法
drawInstance.draw(data);
}
Taro
Taro 比较特殊,框架层面的设计缺陷导致了 Taro 组件增加的 data-xx
属性在编译的时候是会清除的,因此Taro要使用这个库要用原生小程序的方式编写组件。
代码和原生的一样,只是要用原生的方式编写组件,然后在 Taro 中使用。
参考原生的代码,原生小程序js参考这
假设原生组件名为 draw-poster
,那么首先需要再Taro的页面中引入这个组件,然后在页面中使用这个组件,然后在组件中使用这个库。
export default {
navigationBarTitleText: '',
usingComponents: {
'draw-poster': '../../components/draw-poster/index',
},
};
const draw = useCallback(() => {
const { page } = Taro.getCurrentInstance();
// 拿到目标组件实例调用里面的方法
const instance = page!.selectComponent('#draw_poster');
// 调用原生组件绘制方法
instance.paint();
}, []);
return <draw-poster id="draw_poster"/>
总结
对比原生的canvas绘制方案,布局的方式获取节点的方式都是一样的,只是绘制的时候不一样,原生的是直接绘制到canvas上,而这个库是先把布局转换成canvas,然后再绘制到canvas上,所以这个库的性能会比原生的差一些,但是这个库的优势在于布局的方式,不需要自己去计算位置,只需要布局,然后调用绘制方法就可以了,非常方便,而且扩展性非常好,可以自己扩展一些布局方式,比如说flex布局,grid布局等等,这些都是可以的,只需要在布局的时候把布局转换成canvas的布局就可以了,这个库的布局方式是参考的微信小程序的布局方式,所以布局的时候可以参考微信小程序的布局方式,这样就可以很方便的布局了。
来源:juejin.cn/post/7300460850010521654
还在用轮询、websocket查询大屏数据?sse用起来
常见的大屏数据请求方式
1、http请求轮询:使用定时器每隔多少时间去请求一次数据。优点:简单,传参方便。缺点:数据更新不实时,浪费服务器资源(一直请求,但是数据并不更新)
2、websocket:使用websocket实现和服务器长连接,服务器向客户端推送大屏数据。优点:长连接,客户端不用主动去请求数据,节约服务器资源(不会一直去请求数据,也不会一直去查数据库),数据更新及时,浏览器兼容较好(web、h5、小程序一般都支持)。缺点:有点大材小用,一般大屏数据只需要查询数据不需要向服务端发送消息,还要处理心跳、重连等问题。
3、sse:基于http协议,将一次性返回数据包改为流式返回数据。优点:sse使用http协议,兼容较好、sse轻量,使用简单、sse默认支持断线重连、支持自定义响应事件。缺点:浏览器原生的EventSource不支持设置请求头,需要使用第三方包去实现(event-source-polyfill)、需要后端设置接口的响应头Content-Type: text/event-stream
sse和websocket的区别
- websocket支持双向通信,服务端和客户端可以相互通信。sse只支持服务端向客户端发送数据。
- websocket是一种新的协议。sse则是基于http协议的。
- sse默认支持断线重连机制。websocket需要自己实现断线重连。
- websocket整体较重,较为复杂。sse较轻,简单易用。
Websocket和SSE分别适用于什么业务场景?
根据sse的特点(轻量、简单、单向通信)更适用于大屏的数据查询,业务应用上查询全局的一些数据,比如消息通知、未读消息等。
根据websocket的特点(双向通信)更适用于聊天功能的开发
前端代码实现
sse的前端的代码非常简单
const initSse = () => {
const source = new EventSource(`/api/wisdom/terminal/stats/change/notify/test`);
// 这里的stats_change要和后端返回的数据结构里的event要一致
source.addEventListener('stats_change', function (event: any) {
const types = JSON.parse(event.data).types;
});
// 如果event返回的是message 数据监听也可以这样监听
// source.onmessage =function (event) {
// var data = event.data;
// };
// 下面这两个监听也可以写成addEventListener的形式
source.onopen = function () {
console.log('SSE 连接已打开');
};
// 处理连接错误
source.onerror = function (error: any) {
console.error('SSE 连接错误:', error);
};
setSseSource(source);
};
// 关闭连接
sseSource.close();
这种原生的sse连接是不能设置请求头的,但是在业务上接口肯定是要鉴权需要传递token的,那么怎么办呢? 我们可以使用event-source-polyfill这个库
const source = new EventSourcePolyfill(`/api/wisdom/terminal/stats/change/notify/${companyId}`, {
headers: {
Authorization: sessionStorage.get(StorageKey.TOKEN) || storage.get(StorageKey.TOKEN),
COMPANYID: storage.get(StorageKey.COMPANYID),
COMPANYTYPE: 1,
CT: 13
}
});
//其它的事件监听和原生的是一样
后端代码实现
后端最关键的是设置将响应头的Content-Type设置为text/event-stream、Cache-Control设置为no-cache、Connection设置为keep-alive。每次发消息需要在消息体结尾用"/n/n"进行分割,一个消息体有多个字段每个字段的结尾用"/n"分割。
var http = require("http");
http.createServer(function (req, res) {
var fileName = "." + req.url;
if (fileName === "./stream") {
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("event: connecttime\n");
res.write("data: " + (new Date()) + "\n\n");
res.write("data: " + (new Date()) + "\n\n");
interval = setInterval(function () {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);
req.connection.addListener("close", function () {
clearInterval(interval);
}, false);
}
}).listen(8844, "127.0.0.1");
其它开发中遇到的问题
我在开发调试中用的是umi,期间遇到个问题就是sse连接上了但是在控制台一直没有返回消息,后端那边又是正常发出了的,灵异的是在后端把服务干掉的一瞬间可以看到控制台一下接到好多消息。我便怀疑是umi的代理有问题,然后我就去翻umi的文档,看到了下面的东西:
一顿操作之后正常
来源:juejin.cn/post/7424908830902042658
你的团队是“活”的吗?
最近有同学离职,让我突然思考一个话题。
之前在腾讯,内部转岗叫做活水,是希望通过内部转岗,盘活团队。让团队保持一定的人员流动性,让个人与团队双向奔赴,满足各自的需要。因此,我们都希望,团队是活水,而不是一潭死水。
为什么团队要保持一定的人员流动性呢?
- “优”胜“劣”汰。这里不是指恶意竞争和卷。而是通过一定的人员流动性,有进有出,从而找到更加适合团队的人。找到跟团队价值观一致的,志同道合的成员。而跟团队匹配度不是很高的人,可以去寻找更加适合自己的团队和岗位,这对于双方都是有好处的。
- 激活团队。当一个团队保持稳定太久,就会有点思想固化,甚至落后了。这时候,需要通过一些新鲜血液,带来不同的思想和经验,来激活团队,这就像鲶鱼一样。
那想要形成一个“活”的团队,需要什么条件呢?
- 薪资待遇要好。首先是基本福利待遇要高于业界平均水平。其次,绩效激励是有想象空间的。如果没有这个条件,那人员流动肯定是入不敷出的,优秀的人都被挖跑了。
- 团队专业。团队在业界有一定的影响力,在某一方面的专业技术和产出保持业界领先。这个条件隐含了一个信息,就是团队所在业务是有挑战的,因为技术产出一般都是依赖于业务的,没有业务实践和验证,是做不出优秀的技术产出的。因此,待遇好、有技术成长、有职业发展空间,这三者是留住人才的主要手段。
- 梯队完整。在有了前面 2 个条件之后,就有了吸引人才的核心资源了。那接下来就需要有一个完整的梯队。因为资源是有限的,团队资源只能分配到有限人手里,根据最经典的 361,待遇和职业发展空间最多只能覆盖 3 成,技术成长再多覆盖 3 成人已经不错了。那剩下的 4 成人怎么办?所以,团队需要有一些相对稳定的人,他们能完成安排的事情,不出错,也不需要他们卷起来。
这是我当前的想法,我想我还需要更多的经验和讨论的。
那我目前的团队是“活”的吗?答案是否定的。
首先,过去一年,公司的招聘被锁了,内部转岗也基本转不动。薪资待遇就更不用说了。整个环境到处都充斥着“躺”的氛围。
其次,团队专业度一般,在金融业务,前端的发挥空间极其有限。我也只能尽自己所能,帮大家寻求一些技术成长的空间,但还是很有限。
最后,梯队还没有完整,还在建设中,不过也是步履维艰。因为前两个条件限制,别说吸引优秀人才了,能不能保住都是个问题。
最近公司开始放开招聘了,但还不是大面积的,不过还是有希望可以给有人员流失的团队补充 hc 的。但比较难受的是,这个 hc 不是过我的手的,哈哈,又有种听天由命的感觉。
这就是我最近的一个随想,那么,你的团队是“活”的吗?
----------------【END】----------------
【往期文章】
《程序员职场工具库》必须及格的职场工具 —— PPT 系列1
《程序员职场工具库》高效工作的神器 —— checklist
欢迎关注公众号【潜龙在渊灬】(点此扫码关注),收获程序员职场相关经验、提升工作效率和职场效能、结交更多人脉。
来源:juejin.cn/post/7298347391164383259
工作两年,本地git分支达到了惊人的361个,该怎么快速清理呢?
说在前面
不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录,便于后续需求回顾和需求回退。而我平时本地分支都不怎么清理,这就导致了我这两年来本地分支的数量达到了惊人的361个,所以便开始写了这个可以批量删除分支的命令行工具。
功能设计
我们希望可以通过命令行命令的方式来进行交互,快速获取本地分支列表及各分支的最后提交时间和合并状态,在控制台选择我们想要删除的分支。
功能实现
1、命令行交互获取相关参数
这里我们使用@jyeontu/j-inquirer
模块来完成命令行交互功能,@jyeontu/j-inquirer
模块除了支持inquirer
模块的所有交互类型,还扩展了文件选择器、文件夹选择器及多级选择器交互类型,具体介绍可以查看文档:http://www.npmjs.com/package/@jy…
(1)获取操作分支类型
我们的分支分为本地分支和远程分支,这里我们可以选择我们需要操作的分支类型,选择列表为:"本地分支"、"远程分支"、"本地+远程"
。
(2)获取远程仓库名(remote)
我们可以输入自己git的远程仓库名,默认为origin
。
(3)获取生产分支名
我们需要判断各分支是否已经合并到生产分支,所以需要输入自己项目的生产分支名,默认为develop
。
相关代码
const branchListOptions = [
{
type: "list",
message: "请选择要操作的分支来源:",
name: "branchType",
choices: ["本地分支", "远程分支", "本地+远程"],
},
{
type: "input",
message: "请输入远程仓库名(默认为origin):",
name: "gitRemote",
default: "origin",
},
{
type: "input",
message: "请输入生产分支名(默认为develop):",
name: "devBranch",
default: "develop",
},
];
const res = await doInquirer(branchListOptions);
2、命令行输出进度条
在分支过多的时候,获取分支信息的时间也会较长,所以我们需要在控制台中打印相关进度,避免用户以为控制台卡死了,如下图:
3、git操作
(1)获取git本地分支列表
想要获取当前仓库的所有的本地分支,我们可以使用git branch
命令来获取:
function getLocalBranchList() {
const command = "git branch";
const currentBranch = getCurrentBranch();
let branch = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branch = branch
.split("、")
.filter(
(item) => item !== "" && !item.includes("->") && item !== currentBranch
);
return branch;
}
(2)获取远程仓库分支列表
想要获取当前仓库的所有的远程分支,我们可以使用git ls-remote --heads origin
命令来获取,git ls-remote --heads origin
命令将显示远程仓库 origin
中所有分支的引用信息。其中,每一行显示一个引用,包括提交哈希值和引用的全名(格式为 refs/heads/
)。
示例输出可能如下所示:
Copy Code
refs/heads/master
refs/heads/develop
refs/heads/feature/xyz
其中,
是每个分支最新提交的哈希值。
function getRemoteList(gitRemote) {
const command = `git ls-remote --heads ${gitRemote}`;
let branchList = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branchList = branchList
.split("、")
.filter((item) => item.includes("refs/heads/"))
.map((branch) => {
return gitRemote + "/" + branch.split("refs/heads/")[1];
});
return branchList;
}
(3)获取各分支详细信息
我们想要在每个分支后面显示该分支最后提交时间和是否已经合并到生产分支,这两个信息可以作为我们判断该分支是否要删除的一个参考。
- 获取分支最后提交时间
git show -s --format=%ci
命令用于查看 指定 分支最新提交的提交时间。其中,--format=%ci
用于指定输出格式为提交时间。
在 Git 中,git show
命令用于显示某次提交的详细信息,包括作者、提交时间、修改内容等。通过使用 -s
参数,我们只显示提交摘要信息,而不显示修改内容。
git show -s --format=%ci develop
命令将显示 develop
分支最新提交的提交时间。输出格式为 ISO 8601 标准的时间戳,例如 2023-10-22 16:41:47 +0800
。
function getBranchLastCommitTime(branchName) {
try {
const command = `git show -s --format=%ci ${branchName}`;
const result = child_process.execSync(command).toString();
const date = result.split(" ");
return date[0] + " " + date[1];
} catch (err) {
return "未获取到时间";
}
}
- 判断分支是否合并到生产分支
git branch --contains
命令用于查找包含指定分支(
)的所有分支。
在 Git 中,git branch
命令用于管理分支。通过使用 --contains
参数,我们可以查找包含指定提交或分支的所有分支。
git branch --contains
命令将列出包含
的所有分支。输出结果将显示每个分支的名称以及指定分支是否为当前分支。
示例输出可能如下所示:
Copy Code
develop
* feature/xyz
bugfix/123
其中,*
标记表示当前所在的分支,我们只需要判断输出的分支中是否存在生产分支即可:
function isMergedCheck(branch) {
try {
const command = `git branch --contains ${branch}`;
const result = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
const mergedList = result.split("、");
return mergedList.includes(gitInfoObj.devBranch)
? `已合并到${gitInfoObj.devBranch}`
: "";
} catch (err) {
return "未获取到合并状态";
}
}
(4)删除选中分支
选完分支后我们就该来删除分支了,删除分支的命令大家应该就比较熟悉了吧
- git branch -D
git branch -D
命令用于强制删除指定的分支(
)。该命令会删除本地仓库中的指定分支,无法恢复已删除的分支。
- git push
:
git push
命令用于删除远程仓库
中的指定分支(
)。这个命令通过推送一个空分支到远程仓库的
分支来实现删除操作。
async function doDeleteBranch(branchList) {
const deleteBranchList = await getDeleteBranch(branchList);
if (!deleteBranchList) return;
console.log("正在删除分支");
progressBar.run(0);
deleteBranchList.forEach((branch, index) => {
let command = `git branch -D ${branch}`;
if (branch.includes("/")) {
const tmp = branch.split("/");
command = `git push ${tmp[0]} :${tmp[1]}`;
}
child_process.execSync(command);
progressBar.run(Math.floor(((index + 1) / deleteBranchList.length) * 100));
});
console.log("");
console.log("已删除分支:" + deleteBranchList);
}
可以看到我们的分支瞬间就清爽了很多。
使用
该工具已经发布到 npm 上,可以直接通过命令npm i -g jyeontu
进行安装,安装完后在控制台中输入jyeontu git
即可进行操作。
源码
该工具的源码也已经开源,有兴趣的同学可以到Gitee上查看:Gitee地址
来源:juejin.cn/post/7292635075304964123
程序员攻占小猿口算,炸哭小学生!
小学生万万没想到,做个加减乘除的口算练习题,都能被大学生、博士生、甚至是程序员大佬们暴打!
最近这款拥有 PK 功能的《小猿口算》App 火了,谁能想到,本来一个很简单的小学生答题 PK,竟然演变为了第四次忍界大战!
刚开始还是小学生友好 PK,后面突然涌入一波大学生来踢馆,被网友称为 “大学生炸鱼”;随着战况愈演愈烈,硕士生和博士生也加入了战场,直接把小学生学习软件玩成了电子竞技游戏,谁说大一就不是一年级了?这很符合当代大学生的精神状态。
然而,突然一股神秘力量出现,是程序员带着科技加入战场! 自动答题一秒一道 ,让小学生彻底放弃,家长们也无可奈何,只能在 APP 下控诉严查外挂。
此时很多人还没有意识到,小学生口算 PK,已经演变为各大高校和程序员之间的算法学术交流竞赛!
各路大神连夜改进算法,排行榜上的数据也是越发离谱,甚至卷到了 0.1 秒一道题!
算法的演示效果,可以看我发的 B 站视频。
接口也是口,算法也是算,这话没毛病。
这时,官方不得不出手来保护小学生了,战况演变为官方和广大程序员的博弈。短短几天,GitHub 上开源的口算脚本就有好几页,程序员大神们还找到了多种秒速答题的方案。
官方刚搞了加密,程序员网友马上就成功解密,以至于 网传 官方不得不高价招募反爬算法工程师,我建议直接把这些开源大佬招进去算了。
实现方法
事情经过就是这样,我相信朋友们也很好奇秒答题目背后的实现原理吧,这里我以 GitHub 排名最高的几个脚本项目为例,分享 4 种实现方法。当然,为了给小学生更好的学习体验,这里我就不演示具体的操作方法了,反正很快也会被官方打压下去。
方法 1、OCR 识别 + 模拟操作
首先使用模拟器在电脑上运行 App,运用 Python 读取界面上特定位置的题目,然后运用 OCR 识别技术将题目图片识别为文本并输入给算法程序来答题,最后利用 Python 的 pyautogui 库来模拟人工点击和输入答案。
这种方法比较好理解,应用范围也最广,但缺点是识别效果有限,如果题目复杂一些,准确度就不好保证了。
详见开源仓库:github.com/ChaosJulien…
方法 2、抓包获取题目和答案
通过 Python 脚本抓取 App 的网络请求包,从中获取题目和答案,然后通过 ADB(Android Debug Bridge)模拟滑动操作来自动填写答案。然而,随着官方升级接口并加密数据,这种方法已经失效。
详见开源仓库:github.com/cr4n5/XiaoY…
方法 3、抓包 + 修改答案
这个方法非常暴力!首先通过抓包工具拦截口算 App 获取题目数据和答案的网络请求,然后修改请求体中的答案全部为 “1”,这样就可以通过 ADB 模拟操作,每次都输入 1 就能快速完成答题。 根据测试可以达到接近 0 秒的答题时间!
但是这个方法只对练习场有效,估计是练习场的答题逻辑比较简单,且没有像 PK 场那样的复杂校验。
详见开源仓库:github.com/cr4n5/XiaoY…
方法 4、修改 PK 场的 JavaScript 文件
这种方法就更暴力了!在 PK 场模式下,修改 App 内部的 JavaScript 文件来更改答题逻辑。通过分析 JavaScript 响应中的 isRight
函数,找到用于判定答案正确与否的逻辑,然后将其替换为 true,强制所有答案都判定为正确,然后疯狂点点点就行了。
详见开源仓库:github.com/cr4n5/XiaoY…
能这么做是因为 App 在开发时采用了混合 App 架构,一些功能是使用 WebView 来加载网页内容的。而且由于 PK 场答题逻辑是在前端进行验证,而非所有请求都发送到服务器进行校验,才能通过直接修改前端 JS 文件绕过题目验证。
官方反制
官方为了保护小学生学习的体验,也是煞费苦心。
首先加强了用户身份验证和管理,防止大学生炸鱼小学生;并且为了照顾大学生朋友,还开了个 “巅峰对决” 模式,让俺们也可以同实力竞技 PK。
我建议再增加一个程序员模式,也给爱玩算法的程序员一个竞技机会。
其实从技术的角度,要打击上述的答题脚本,并不难。比如检测 App 运行环境,发现是模拟器就限制答题;通过改变题目的显示方式来对抗 OCR 识别;通过随机展示部分 UI, 让脚本无法轻易通过硬编码的坐标点击正确的答案;还可以通过分析用户的答题速度和操作模式来识别脚本,比如答题速度快于 0.1 秒的用户,显然已经超越了人类的极限。
0.0 秒的这位朋友,是不是有点过分(强大)了?
但最关键的一点是,目前 App 的判题逻辑是在前端负责处理的,意味着题目答案的验证可以在本地进行,而不必与服务器通信,这就给了攻击者修改前端文件的机会。虽然官方通过接口加密和行为分析等手段加强了防御,但治标不治本,还是将判题逻辑转移到服务端,会更可靠。
当然,业务流程改起来哪有那么快呢?
不过现在的局面也不错,大学生朋友快乐了,程序员玩爽了,口算 App 流量赢麻了,可谓是皆大欢喜!
等等,好像有哪里不对。。。别再欺负我们的小学生啦!
来源:juejin.cn/post/7425121392738140214
第二届OpenHarmony竞赛训练营颁奖 ——创新驱动,培育未来科技人才
在科技日新月异的时代背景下,OpenAtom OpenHarmony(以下简称“OpenHarmony”)竞赛训练营2024年再度扬帆起航,为高校学子们提供了一个展现创新才能、深入探索前沿技术的广阔舞台。在10月12日以“技术引领筑生态,万物智联创未来”为主题的第三届OpenHarmony技术大会上,OpenHarmony项目群工作委员会(PMC)和OpenHarmony项目群技术指导委员会(TSC)的专家出席仪式并为10个获奖团队颁奖。
OpenHarmony项目群技术指导委员会(TSC)委员张荣超为特别创新奖团队颁奖
OpenHarmony项目群技术指导委员会(TSC)委员贾宁
OpenHarmony项目群工作委员会(PMC)执行总监陶铭为三等奖团队颁奖
OpenHarmony项目群技术指导委员会(TSC)委员臧斌宇,
OpenHarmony项目群工作委员会(PMC)执行主席柳晓见为二等奖团队获得者颁奖
OpenHarmony项目群工作委员会(PMC)主席、华为终端BG软件部总裁龚体,OpenHarmony项目群技术指导委员会(TSC)主席、华为Fellow、华为基础软件首席科学家陈海波为一等奖获奖团队和最佳指导老师颁奖。
OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。OpenHarmony竞赛训练营始终致力于引导高校学生将理论知识与实际应用相结合,推动OpenHarmony产学研用的深度融合。通过精心设计的赛题,以OpenHarmony为核心技术底座,让学生们在解决实际问题的过程中,不断提升自己的技术水平和创新能力。
训练营延续使用实战竞赛+赋能培训的模式,邀请了OpenHarmony行业专家、TSC领域专家、AI领域专家和高校老师,为参赛者提供技术指导、培训和作品评审。OpenHarmony鼓励学生积极参与 OpenHarmony开源社区和相关技术交流平台,充分利用社区丰富的资源,包括文档、代码、开发工具等。同时,配备了专业的社区助手,随时为学生们解答技术难题,促进知识共享和技术交流。与此同时,来自OpenHarmony项目群技术指导委员会、生态、内核、编译优化、视窗等领域,以及各大高校的专家和学者凭借丰富的经验和专业知识,为学生们提供了宝贵的建议和指导,帮助学生们不断优化作品,提高竞争力。
赋能培训环节依然是训练营的重要组成部分。今年的赋能培训内容更加丰富多样,包括前沿技术讲座、赛题深度解读、成功案例分享、开发实战演练、作品要求与评分规则详解等。通过这些培训,学生们不仅能够深入了解OpenHarmony技术体系,还能掌握最新的开发方法和技巧,为作品的创作打下坚实的基础。
为了激励学生们的创新热情,本次训练营设置了丰厚的奖项与奖金。一等奖50000元、二等奖20000元、三等奖10000元,潜力无限奖5000元以及最佳指导老师奖3000元、特别创新奖10000元。这些奖项由行业领军人物、知名专家学者和企业代表颁发,充分体现了对学生们创新成果的高度认可和鼓励。
OpenHarmony 竞赛训练营已经成为培养创新人才、推动OpenHarmony生态发展的重要平台。展望未来,主办方表示将继续加大对训练营的投入和支持,不断丰富赛题内容,拓展合作领域,提高训练营的影响力和吸引力。同时,也希望更多的高校师生能够参与到 OpenHarmony 的开发和应用中来,共同为我国信息技术产业的发展贡献力量。相信在各方的共同努力下,OpenHarmony竞赛训练营将不断创造新的辉煌,为培养未来科技人才、推动科技创新发挥更大的作用。
收起阅读 »融合大模型技术,激发开发新动力,IDE分论坛成功举办
在当今的数字化浪潮中,软件开发是企业和组织技术架构的核心部分。2024年10月12日,第三届OpenHarmony技术大会的IDE分论坛在上海世博中心举行。论坛聚焦于探讨如何利用IDE工具技术提升OpenAtom OpenHarmony(以下简称“OpenHarmony”)应用的开发效率和软件质量,旨在构建一个开放且前瞻性的以IDE为核心的软件开发工具交流平台。
在本次分论坛中,与会嘉宾深入探讨了应用开发技术与工具的工程化解决方案,以及大模型技术与软件开发工具的深度融合,以全面提升OpenHarmony应用开发的效率和质量。通过分享OpenHarmony应用的优秀开发实践和学术前沿的软件开发工具探索,分论坛旨在帮助开发者在OpenHarmony生态中找到更高质量的IDE开发工具方案。
该分论坛由华为软件IDE实验室主任蒋奕和复旦大学计算机学院副院长彭鑫担任本论坛出品人,并由蒋奕主持。在活动中,华为软件IDE实验室主任蒋奕、华为 DevEco Studio 高级技术专家陈晓闯、北京趣拿软件科技有限公司移动端开发总监邹德文、飞书OpenHarmony架构师夏恩龙、中国工商银行软件开发中心研究员赵海强、深圳开鸿数字产业发展有限公司开源社区开发部开发工程师胡瑞涛、百度在线网络技术(北京)有限公司智能研发团队高级经理彭云鹏、北京航天航空大学教授石琳、DeepWisdom创始人兼CEO吴承霖、复旦大学计算机学院副院长彭鑫等嘉宾,分别就各自专业领域的最新进展和实践进行了深入的分享和讨论。
华为软件IDE实验室主任蒋奕发表了主题为“智慧化IDE助力OpenHarmony应用开发探索与实践”的演讲。蒋奕介绍了智慧化IDE在工程级代码生成技术上的突破,这些技术不仅提升了开发效率,还降低了开发门槛,加速了应用的OpenHarmony化进程。华为软件IDE实验室在AI加持的工程级代码生成技术、少语料代码生成技术方面进行了探索,赋能了OpenHarmony UI代码生成、元服务卡片生成及仓颉代码生成开发工具集,致力于打造极简开发体验。
(华为软件IDE实验室主任蒋奕发言)
华为技术专家陈晓闯先生对OpenHarmony应用开发工具DevEco Studio进行了深入介绍。陈晓闯强调了DevEco开发套件的核心特性,包括高效编码、调试、快速构建应用程序等,可以帮助开发者简化开发流程,提升开发效率。陈晓闯展示了DevEco Studio许多功能,包括高效编码、调试、快速构建应用程序、性能调优、代码静态检测等能力,以及如何帮助开发者专注于业务逻辑的实现,从而提高代码编写的效率和应用的整体体验。
(华为技术专家陈晓闯发言)
每次旅行不仅是目的地的探索,也可以是科技体验的旅程。北京趣拿软件科技有限公司移动端开发总监邹德文分享了“去哪儿OpenHarmony跨端技术落地实践”。邹德文讲述了去哪儿网在OpenHarmony平台上采用React Native、Flutter等跨端技术栈,实现了应用的高效跨平台运行能力。邹德文还提到了AI工具在生成目标平台代码方面的应用,这大幅提高了开发效率和应用稳定性,这种跨端技术栈在OpenHarmony化过程中发挥了重要作用。
(北京趣拿软件科技有限公司移动端开发总监邹德文发言)
飞书OpenHarmony架构师夏恩龙分享了“飞书的OpenHarmony化之旅”。夏恩龙详细介绍了飞书企业级应用在OpenHarmony上的适配与升级过程,展示了如何通过一次开发实现多端部署,为用户提供全新的办公体验。夏恩龙强调了飞书与OpenHarmony的合作,不仅提升了办公效率,还引领了智慧协同的新潮流。
(飞书OpenHarmony架构师夏恩龙发言)
中国工商银行软件开发中心互联网金融研究团队的研究员赵海强,在分论坛上介绍了“中国工商银行移动端用户体验提升支撑工具实践”。赵海强探讨了在竞争激烈的APP市场中,如何通过加强底层基础支撑和构建辅助工具,实现APP研发全生命周期体验质量控制。赵海强分享了工商银行在UI一致性、性能、体验、用户友好提示、业务流程交互等方面的研发工具,这些工具在需求、设计、开发、测试各阶段帮助及时发现潜在问题,从而提升工商银行移动端应用的用户体验。
(中国工商银行软件开发中心互联网金融研究团队的研究员赵海强发言)
深圳开鸿数字产业发展有限公司的开源社区开发部开发工程师胡瑞涛,分享了“开发者必备的应用开发工具”。胡瑞涛介绍了全栈开发工具链如何为OpenHarmony生态提供技术支持,强调了这些工具在提升开发效率和生态创新能力方面的重要性。胡瑞涛指出,这些工具不仅简化了开发流程,还推动了新硬件和服务模式的发展,为开发者提供了高效、便捷的开发环境,加速了OpenHarmony在各领域的应用和普及。
(深圳开鸿数字产业发展有限公司的开源社区开发部开发工程师胡瑞涛发言)
在人工智能时代,软件研发范式正在经历的变革。百度在线网络技术(北京)有限公司智能研发团队高级经理彭云鹏,带来了“人工智能原生软件研发新范式”的主题分享。彭云鹏阐述了如何利用AI工具提升研发效率。彭云鹏提到,百度在这一领域的探索和实践,包括代码生成工具Comate的应用,已经实现了全公司35%的新增代码由AI生成,这一比例还在持续增长。
(百度在线网络技术(北京)有限公司智能研发团队高级经理彭云鹏发言)
连续参加两年IDE分论坛的北京航天航空大学教授石琳,分享了“基于智能IDE的开发者个性化数据理解”的主题。石琳探讨了IDE作为开发者编程的主要场所,其中蕴含的丰富个性化数据对于提升大模型的理解能力、实现复杂软件自动化的重要性。石琳提出,通过深入挖掘和理解开发者的编程偏好和项目环境信息,可以助力大模型更好地理解开发者的意图,从而在人机协同的范式中实现从简单代码生成到复杂软件自动化的突破。
(北京航天航空大学教授石琳发言)
DeepWisdom创始人兼CEO吴承霖在分论坛上介绍了“MetaGPT: Coding Through Chat With Agents”。吴承霖展示了如何通过自然语言编程简化开发过程,使编程变得像聊天一样简单。吴承霖提出的MetaGPT框架通过多智能体协同工作,利用自然语言编程重塑了传统IDE模式,显著提升了开发效率。吴承霖还探讨了MetaGPT在代码转译方面的应用,尤其是其对OpenHarmony生态系统创新的推动作用,旨在优化开发流程和增强团队协作。
(DeepWisdom创始人兼CEO吴承霖发言)
复旦大学计算机学院副院长、教授彭鑫,分享了“基于大模型的人机协作生成式应用开发”的主题。彭鑫探讨了大模型技术如何触发软件智能化开发的质变,提出了从软件开发自身规律出发,探索人机协作的智能化开发模式的必要性。彭鑫强调了将演进式设计、特定领域语言(DSL)以及有效的代码审视与反馈与大模型的代码生成能力相结合,形成更高层次上的智能化开发能力的重要性。
(复旦大学计算机学院副院长、教授彭鑫发言)
第三届OpenHarmony技术大会的IDE分论坛的圆满落幕,为开发者社群搭建了一个宝贵的交流舞台。与会者深入探讨了IDE在OpenHarmony应用开发中的关键作用。论坛集中讨论了如何利用IDE提高开发效率、软件质量和用户体验。嘉宾们分享了他们在工程化解决方案、大模型技术与软件开发工具融合方面的见解和经验。此次分论坛的讨论不仅为开发者提供了宝贵的实践指导,还激励了更多开发者以更迅速、更深入的方式投身于OpenHarmony生态,携手促进其蓬勃发展。
收起阅读 »第三届OpenHarmony技术大会星光璀璨,致谢社区贡献者
10月12日,在上海举办的第三届OpenHarmony技术大会上,32家高校OpenHarmony技术俱乐部璀璨亮相,30家高校OpenHarmony开发者协会盛大启幕。还分别致谢了年度星光TSG(技术专家组)、TSG星光贡献者和星光OpenHarmony技术俱乐部、星光导师、星光贡献者、星光活动等OpenHarmony社区贡献者,大会同步举行了授牌仪式。
为致谢取得丰硕成果的TSG团队、OpenHarmony技术俱乐部团队及个人,本次大会特别举办了星光团队和星光个人授牌仪式。
共授牌4个星光TSG,分别是安全及机密计算TSG、跨平台应用开发框架TSG、编程语言TSG、通信互联TSG。
授牌10位TSG星光贡献者,分别是编程语言TSG王学智、跨平台应用开发框架TSG晏国淇、安全及机密计算TSG王季、Web3标准TSG Wenjing Chu、机器人TSG巴延兴、IDE TSG刘芳、并发与协同TSG Diogo Behrens、应用开发工程技术TSG程帅、智能数据管理TSG李永坤、通信互联TSG李锋。
授牌5个“技术突破”星光OpenHarmony技术俱乐部,分别是来自上海交通大学、北京航空航天大学、北京理工大学、兰州大学、华中科技大学的OpenHarmony技术俱乐部。
授牌5个“
授牌11个“活力引领”星光OpenHarmony技术俱乐部,分别是来自中山大学、东南大学、西安交通大学、华南理工大学、武汉大学、南开大学、南昌大学、重庆大学、复旦大学、浙江大学、厦门大学的OpenHarmony技术俱乐部。
授牌5位星光导师,分别是上海交通大学OpenHarmony技术俱乐部夏虞斌、北京邮电大学OpenHarmony技术俱乐部邹仕洪、北京航空航天大学OpenHarmony技术俱乐部黎立、电子科技大学OpenHarmony技术俱乐部唐佐林、兰州大学OpenHarmony技术俱乐部周庆国。
授牌5位OpenHarmony技术俱乐部星光贡献者,分别是东南大学OpenHarmony技术俱乐部李光伟、北京航空航天大学OpenHarmony技术俱乐部陈岱杭、兰州大学OpenHarmony技术俱乐部王天一、华中科技大学OpenHarmony技术俱乐部刘浩毅、湖南大学OpenHarmony技术俱乐部银天杨。
授牌3项OpenHarmony技术俱乐部星光活动,分别是西安电子科技大学OpenHarmony技术俱乐部出品的“红色筑梦·智汇未来”基础软件开源生态研讨会暨OpenHarmony城市技术论坛延安站活动、上海交通大学OpenHarmony技术俱乐部出品的ASPLOS 2024-OpenHarmony国际学术教程会、厦门大学OpenHarmony技术俱乐部出品的海峡开源人才培养研讨会暨厦门大学OpenHarmony技术俱乐部成立仪式。
收起阅读 »OpenHarmony统一互联PMC启动孵化
在2024年10月12日于上海举办的第三届OpenHarmony技术大会上,OpenHarmony统一互联PMC(项目群项目管理委员会)正式启动孵化。
OpenHarmony统一互联PMC 致力于解决OpenAtom OpenHarmony(以下简称“OpenHarmony”)设备跨操作系统、跨厂家之间的互联互通互操作问题,聚焦HarmonyOS之间、不同OpenHarmony厂商之间,以及与三方OS之间的设备连接,从建底座、定标准、搭平台三个维度构筑统一互联的技术底座。OpenHarmony统一互联PMC孵化范围包括OneConnect应用和组件、OneConnect云侧配套,包括OpenHarmony通用互联应用、OpenHarmony图库分享组件、OpenHarmony文件管理器分享组件、OpenHarmony投屏分享组件、OpenHarmony设备侧业务控制联动组件、统一互联物模型服务器、统一互联认证服务器等。
据了解,OpenHarmony统一互联PMC主要通过共建项目的方式运作,其中共建项目1.0分为3个子项目,分别包括富对瘦设备控制、富对富投屏、富对富文件互传,3个子项目均在交付中;共建项目2.0分为4个子项目,分别包括富对瘦设备控制2.0、富对富投屏2.0、富对富文件互传2.0、分布式摄像头,现已完成场景确定及相关需求分析。OpenHarmony统一互联PMC生态伙伴由最初的华为与7家生态伙伴扩增到25家。
在启动仪式上,还发布了OpenHarmony统一互联系列标准2.0。该系列标准作为统一互联PMC所孵化的设备互联、数据互通、业务互操作相关解决方案的技术沉淀,为教育、金融、交通、政务、医疗等行业形成统一互联互通提供了基础标准参考。
本次发布的标准共计六篇,不仅包含了富对瘦设备之间设备控制、设备联动场景,还涵盖了富对富设备之间的投屏、文件分享等常用业务。可以预见,OpenHarmony统一互联技术标准的发展,将助力打造真正的OpenHarmony物联网生态,实现设备之间的无缝连接,提供更流畅、更安全的用户体验。
收起阅读 »第三届OpenHarmony技术大会应用开发工程技术分论坛成功举行
OpenAtom OpenHarmony(以下简称OpenHarmony)生态的繁荣,需要构建服务于千行万业的应用生态,提供高效的应用开发工程技术和完备的软件工程能力成为推动OpenHarmony应用生态高效、低成本可持续发展的关键因素。2024年10月12日下午第三届OpenHarmony技术大会应用开发工程技术分论坛在上海成功举行。该分论坛围绕前沿的应用开发技术与移动软件工程能力,在人机物融合的智能系统及应用新形态、应用业务逻辑分析和安全检测技术、开发者自动化测试、Qt/Flutter框架新技术、大型应用构建和持续集成能力等议题展开深入探讨与经验分享。
OpenHarmony应用开发工程技术TSG主任任晗;北京航空航天大学教授、博士生导师史晓华作为应用开发工程技术分论坛出品人出席本次活动。复旦大学计算机科学技术学院副院长、教授彭鑫;中国科学院计算技术研究所研究员李炼;华东师范大学教授苏亭;复旦大学青年副研究员张晓寒;Qt资深方案工程师雒少华;华为高级技术专家邵甜鸽;华为技术专家武超;深圳开鸿数字产业发展有限公司架构设计工程师丁力出席本论坛并发表演讲。OpenHarmony应用开发工程技术TSG主任任晗主持了整场会议。
(OpenHarmony应用开发工程技术TSG主任任晗主持会议)
(复旦大学计算机科学技术学院副院长、教授彭鑫发言)
中国科学院计算技术研究所研究员李炼聚焦高层语义的自适应分析方法与工具展开分享。应用层的大部分安全性问题以及性能和功能问题都需要深入理解高层的应用逻辑语义。但这些高层语义和应用具体实现密切相关,往往无法进行通用的定义。那么如何通过自动或半自动的方法推断高层应用语义,以及严格表述这些语义信息?如何实现高效且易于扩展的高层语义分析工具?针对上述问题,李炼提出可以通过声明式方法定义高层语义,并扩展现有工具以自动检测自定义语义,从而兼顾可扩展性、效率和精度展开讨论。他指出,通过自动或半自动高层语义推断以及自适应分析方法与工具,可以解决灵活多变的应用层逻辑问题。
(中国科学院计算技术研究所研究员李炼发言)
华东师范大学教授苏亭分享了面向OpenHarmony应用的开发者自动化测试技术新范式。苏亭指出,保障OpenHarmony应用稳定和正确运行是OpenHarmony生态发展的重要目标。然而,与其他现有移动平台应用(如安卓、iOS等)相比,OpenHarmony应用在编程语言、开发特性、架构设计等方面有着显著的不同,这为设计和构建OpenHarmony应用自动化测试技术带来了挑战。鉴于此,苏亭教授介绍了其所带领的研究小组在OpenHarmony应用自动化测试方面的探索和工程化实践,并介绍了基于代码功能地图的OpenHarmony应用增强遍历测试技术和基于性质的OpenHarmony应用异常测试技术。
(华东师范大学教授苏亭发言)
“安全不是选项,而是必需”,复旦大学青年副研究员张晓寒在《移动应用业务安全研究与生态治理》的演讲中强调。本次论坛他带来了在移动应用业务安全方面开展的相关研究与实践成果,并与与会者共同探讨了基于移动应用逆向、程序分析、深度学习与大模型等技术形成的一套应用业务安全分析思路和方法。同时,张晓寒重点分享了团队在移动应用认证安全、端侧风控、应用行为理解、敏感行为感知等方面进行的学术探索,汇报了在漏洞挖掘与治理、应用生态治理等方面进行的尝试和实践。他的相关研究曾获华为优秀技术成果奖、CNVD最具价值漏洞等荣誉。
(复旦大学青年副研究员张晓寒发言)
Qt资深方案工程师雒少华在本次论坛中以《Qt携手OpenHarmony:共创软件新生态的适配之旅》为主题,深入剖析Qt框架如何高效适配OpenHarmony操作系统,展现其在软件生态构建中的关键角色;探讨Qt跨平台技术的独特优势,在OpenHarmony环境下的应用创新,以及如何促进开发者快速迁移,加速软件生态的繁荣。雒少华展望道:“在OpenHarmony的沃土上,Qt绽放新生,共绘软件生态的宏伟蓝图。”
(Qt资深方案工程师雒少华发言)
Flutter作为今年来流行的跨平台开发框架,在全球范围内获得了广泛的应用和认可。OpenHarmony系统如果能成功融入 Flutter 生态系统,将会对OpenHarmony生态产生深远影响。会有什么影响呢?华为技术专家邵甜鸽对此给予了解答。邵甜鸽认为:Flutter 的跨平台能力可以极大减少伙伴的开发和维护成本,且可以使应用快速迁移到OpenHarmony平台,迅速丰富OpenHarmony应用生态。Flutter的自渲染引擎可以有效保证在不同平台上的一致性用户体验,通过优化 Flutter 在OpenHarmony系统上的性能,进一步实现极致流畅的用户体验。Flutter 的广泛使用和社区支持吸引了更多的开发者加入OpenHarmony生态,其丰富的共享资源和插件可以提高开发效率,帮助OpenHarmony快速建立起应用生态,提升竞争力。
(华为高级技术专家邵甜鸽发言)
华为技术专家武超在本次演讲中分享了OpenHarmony大型工程的依赖管理与多产物构建的经验。为与会者介绍OpenHarmony系统依赖管理的几种最常见模式和相应的技术,讲解构建系统的几个核心概念和顶层的领域模型,并分享了OpenHarmony系统上的多产品、多环境、多设备的多目标构建工程能力。
(华为技术专家武超发言)
会议最后,深圳开鸿数字产业发展有限公司架构设计工程师丁力以《OpenHarmony应用开发持续集成工程能力构建》为主题做了报告分享。他指出,持续集成构建、gerrit管控代码、代码门禁集成增量编译、静态检查、单元测试等多种管控措施,是全力构筑好版本质量管控的首道防线。丁力分别从持续集成工具链的整体架构和流程架构两个方面,介绍了深开鸿软件工程团队在此方面的实践探索,并着重分享了OpenHarmony应用开发从编译构建、代码检查、到功能测试的持续集成能力关键技术。
(深圳开鸿数字产业发展有限公司架构设计工程师丁力发言)
应用开发工程技术分论坛通过实际案例和技术分享,旨在帮助开发者在OpenHarmony生态中找到最优的工程方案。OpenHarmony项目技术指导委员会应用开发工程技术TSG致力于构建一个开放且前瞻性的应用工程技术交流平台,为开发者提供从工程指导到模板应用的全方位支持,推动高质量OpenHarmony应用的开发与生态建设。通过共同探索和实践,打造一个高效、安全、高质量的OpenHarmony应用开发平台。
收起阅读 »啊?两个vite项目怎么共用一个端口号啊
问题:
最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后:
该项目的端口号为5173,但是此时我再次通过vite的官方搭建一个react+ts+vite项目:npm create vite@latest react_demos -- --template react-ts
,之后通过npm run dev
启动项目,发现端口号并没有更新
:
这是什么原因呢?
寻因:
查阅官方文档,我发现:
那么我主动在vite.config.ts中添加这个配置:
正常来说,会出现这个报错:
但是此时结果依然为:
我百思不得不得其解,于是再次查阅官方文档:
我寻思这也与文档描述不一致啊,于是我再次尝试,思考是不是vite版本号的问题
,两个项目的版本号分别为:
我决定创建一个4版本的项目npm create vite@^4.1.4 react_demos3 -- --template react-ts
结果发现,还是有这个问题,跟版本号没有关系
,于是我又耐心继续看官方文档,看到了这个配置:
我抱着试试的态度,在其中一个vite项目中添加这个配置:
发现,果然是这个配置的锅,当其中一个项目host配置为0.0.0.0时,vite不会自动尝试更新端口号
难道vite的端口监测机制与host也有关?
结果:
不甘心的我再次进行尝试,将两个项目的host都设置成:
vite会自动尝试更新端口号
原来如此,vite的端口号检测机制在对比端口号之前,会先对比host,由于我的微前端项目中设置了host,而新建的项目中没有设置host,新建的项目host默认值为localhost对比不成功,vite不会自动尝试下一个可用端口,而是共用一个端口
总结:
在遇到问题时,要多多去猜,去想各种可能,并且最重要的是去尝试各种可能,还要加上积极去翻阅官方文档,问题一定会得到解决的;哪怕不能解决,那也会在尝试中,学到很多东西
来源:juejin.cn/post/7319699173740363802
还搞不明白浏览器缓存?
一:前言
浏览器缓存与浏览器储存是不一样的,友友们不要混淆,关于浏览器储存,具体可以看这篇文章 : 一篇打通浏览器储存
这里大概介绍一下:
cookies | localStorage | sessionStorage | IndexedDB |
---|---|---|---|
服务端设置 | 一直存在 | 页面关闭就消失 | 一直存在 |
4K | 5M | 5M | 无限大 |
自动携带在http请求头中 | 不参与后端 | 不参与后端 | 不参与后端 |
默认不允许跨域,但可以设置 | 可跨域 | 可跨域 | 可跨域 |
二:强缓存
强缓存是指浏览器在请求资源时,如果本地有符合条件的缓存,那么浏览器会直接使用缓存而不会向服务器发送新的请求。这可以通过设置 Cache-Control
或 Expires
响应头来实现。
2.1:Cache-Control 头详解
Cache-Control
是一个非常强大的HTTP头部字段,它包含多个指令,用以控制缓存的行为:
- max-age:指定从响应生成时间开始,该资源可被缓存的最大时间(秒数)。
- s-maxage:类似于
max-age
,但仅对共享缓存(如代理服务器)有效。 - public:表明响应可以被任何缓存存储,即使响应通常是私有的。
- private:表明响应只能被单个用户缓存,不能被共享缓存存储。
- no-cache:强制缓存在使用前必须先验证资源是否仍然新鲜。
- no-store:禁止缓存该响应,每次请求都必须获取最新数据。
- must-revalidate:一旦资源过期,必须重新验证其有效性。
例如,通过设置 Cache-Control: max-age=86400
,可以告诉浏览器这个资源可以在本地缓存24小时。在这段时间内,如果再次访问相同URL,浏览器将直接使用缓存中的副本,而不与服务器通信。
2.2:Expires 头
Expires
是一个较旧的头部字段,用于设定资源过期的具体日期和时间。尽管现在推荐使用 Cache-Control
,但在某些情况下,Expires
仍然是有效的。Expires
的值是一个绝对的时间点,而不是相对时间。例如:
Expires: Wed, 09 Oct 2024 18:29:00 GMT
2.3:浏览器默认行为
当用户通过地址栏直接请求资源时,浏览器通常会自动添加 Cache-Control: no-cache
到请求头中。这意味着即使资源已经存在于缓存中,浏览器也会尝试重新验证资源新鲜度,以确保用户看到的是最新的内容。
三:协商缓存
协商缓存发生在资源的缓存条目已过期或设置了 no-cache
指令的情况下。这时,浏览器会向服务器发送请求,并携带上次请求时收到的一些信息,以便服务器决定是否返回完整响应或只是确认没有更新。
3.1:Last-Modified/If-Modified-Since
后端服务器可以为每个资源设置 Last-Modified
头部,表示资源最后修改的时间。当下一次请求同一资源时,浏览器会在请求头中加入 If-Modified-Since
字段,其值为上次接收到的 Last-Modified
值。服务器检查这个时间戳,如果资源自那以后没有改变,则返回304 Not Modified状态码,指示浏览器使用缓存中的版本。
3.2:ETag/If--Match
ETag 提供了一种更精确的方法来检测资源是否发生变化。它是基于文件内容计算出的一个唯一标识符。当客户端请求资源时,服务器会在响应头中提供一个 ETag
值。下次请求时,浏览器会发送 If--Match
头部,包含之前接收到的 ETag
。如果资源未改变,服务器同样返回304状态码;如果有变化,则返回完整的资源及新的 ETag
值。
3.3:比较 Last-Modified 和 ETag
虽然 Last-Modified
简单易用,但它基于时间戳,可能会受到时钟同步问题的影响。相比之下,ETag
更加准确,因为它依赖于资源的实际内容。然而,ETag
计算可能需要更多的服务器处理能力。
四:缓存选择
合理的缓存策略能够显著提升网站性能和用户体验。例如,静态资源(如图片、CSS、JavaScript文件)适合设置较长的缓存时间,而动态内容则需谨慎对待,避免缓存不适当的信息。
- 使用工具如 Chrome DevTools 来分析页面加载时间和缓存效果。
- 对不同类型的资源设置合适的
Cache-Control
参数。 - 注意安全性和隐私保护,确保敏感数据不会被错误地缓存。
五:使用示例
- 引入必要的模块:导入
http
,path
,fs
和mime
模块。 - 创建HTTP服务器:使用
http.createServer
创建一个HTTP服务器。 - 处理请求:
- 根据请求的URL生成文件路径。
- 检查文件是否存在。
- 如果是目录,指向该目录下的
index.html
文件。
- 处理协商缓存:
- 获取请求头中的
If-Modified-Since
字段。 - 比较
If-Modified-Since
与文件的最后修改时间。
- 获取请求头中的
- 读取文件并发送响应:
- 读取文件内容。
- 设置响应头(包括
Content-Type
,Cache-Control
,Last-Modified
,ETag
)。 - 发送响应体。
- 启动服务器:监听3000端口并启动服务器。
server.js:
const http = require('http'); // 引入HTTP模块
const path = require('path'); // 引入路径处理模块
const fs = require('fs'); // 引入文件系统模块
const mime = require('mime'); // 引入MIME类型模块
// 创建一个HTTP服务器
const server = http.createServer((req, res) => {
// console.log(req.url); // /index.html // /assets/image/logo.png
// 根据请求的URL生成文件路径
let filePath = path.resolve(__dirname, path.join('www', req.url));
// 检查文件或目录是否存在
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath); // 获取该路径对应的资源状态信息
// console.log(stats);
const isDir = stats.isDirectory(); // 判断是否是文件夹
const { ext } = path.parse(filePath); // 获取文件扩展名
if (isDir) {
// 如果是目录,则指向该目录下的 index.html 文件
filePath = path.join(filePath, 'index.html');
}
// +++++ 获取前端请求头中的if-modified-since
const timeStamp = req.headers['if-modified-since']; // 获取请求头中的 If-Modified-Since 字段
let status = 200; // 默认响应状态码为200
if (timeStamp && Number(timeStamp) === stats.mtimeMs) { // 如果 If-Modified-Since 存在且与文件最后修改时间相同
status = 304; // 设置响应状态码为304,表示资源未变更
}
// 如果不是目录且文件存在
if (!isDir && fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath); // 读取文件内容
res.writeHead(status, {
'Content-type': mime.getType(ext), // 设置 Content-Type 头
'cache-control': 'max-age=86400', // 设置缓存控制为一天
// 'last-modified': stats.mtimeMs, // 资源最新修改时间(可选)
// 'etag': '由文件内容生成的hash' // 文件指纹(可选)
});
res.end(content); // 发送文件内容作为响应体
}
}
});
// 启动服务器,监听3000端口
server.listen(3000, () => {
console.log('listening on port 3000');
});r.listen(3000, () => {
console.log('listening on port 3000');
});
index.html:
<body>
<h1>midsummer</h1>
<img src="assets/image/1.png" alt="">
</body>
项目结构如下图,友友们自行准备一张图片,将项目npm init -y
初始化为后端项目,之后下载mime@3包,在终端输入npx nodemon server.js
运行起来,在浏览器中查看http://localhost:3000/index.html ,观察效果。在检查中的网络里看缓存效果,同时友友们可以更改图片或者缓存方式,体验下不同的浏览器缓存方式
来源:juejin.cn/post/7423298788873142326
告别axios,这个库让你爱上前端分页!
嗨,我们又见面了!
今天咱们聊聊前端分页加载那些事儿。你有没有遇到过这样的烦恼:在做分页的时候,要手动维护各种状态,比如页码、每页显示数量、总数据量等等,还要处理各种边界情况,哎呀妈呀,真是太麻烦了!
那么,有没有什么好办法能让我们从这些繁琐的工作中解脱出来呢?这时候,alovajs就派上用场了!
alovajs:轻量级请求策略库
alovajs是一个轻量级的请求策略库,它可以帮助我们轻松处理分页请求。它支持开发者使用声明式实现各种复杂的请求,比如请求共享、分页请求、表单提交、断点续传等等。使用alovajs,我们可以用很少的代码就实现高效、流畅的请求功能。比如,在Vue中,你可以这样使用alovajs进行分页请求:
const alovaInstance = createAlova({
// VueHook用于创建ref状态,包括请求状态loading、响应数据data、请求错误对象error等
statesHook: VueHook,
requestAdapter: GlobalFetch(),
responded: response => response.json()
});
const { loading, data, error } = useRequest(
alovaInstance.Get('https://api.alovajs.org/profile', {
params: {
id: 1
}
})
);
看到了吗?只需要几行代码,alovajs就帮我们处理了分页请求的各种细节,我们再也不用手动维护那些繁琐的状态了!
对比axios,alovajs的优势
和axios相比,alovajs有哪些优势呢?首先,alovajs与React、Vue等现代前端框架深度融合,可以自动管理请求相关数据,大大提高了开发效率。其次,alovajs在性能方面做了很多优化,比如默认开启了内存缓存和请求共享,这些都能显著提高请求性能,提升用户体验的同时还能降低服务端的压力。最后,alovajs的体积更小,压缩后只有4kb+,相比之下,axios则有11+kb。
总之,如果你想在分页加载方面做得更轻松、更高效,alovajs绝对值得一试!
来源:juejin.cn/post/7331924057925533746
如何用AI两小时上线自己的小程序
ChatGPT这个轰动全球的产品自问世以来,已经过了将近2年的时间,各行各业的精英们如火如荼的将AI能力应用到自己生产的产品中来。
为分担人类的部分工作,AI还具有非常大的想象空间,例如对于一个程序员来说,使用AI生成快速生成自己的小程序,相信在AI能力与开发工具融合从可用性到易用性普及以后,会变成一个“习以为常”的操作。
App or 小程序?
在APP开发与小程序开发技术路径之间,本人选择了轻应用的技术开发路线,主要是相信“效率为王”,高产才能给自己赚取更高的收益。
好了,选定方向以后,接下来就是技能的学习和深入。AI的效率之高和学习成本之低,在技能深耕让我想到了是否能借助AI做更多的尝试,比如零基础开发一个页面,甚至一个小程序?
说干就干,开始着手进行准备工作:开发什么应用好呢?要不就一个简单的电商小程序吧。
一、准备工作
最开始的开始,我们先要找一个开发工具,既能帮助我们可视化的开发小程序的,又有可以接收prompt的AI能力。找度娘搜索了下,发现一款产品:FinClip的开发者工具(FinClip IDE)。
二、生成小程序
首先,随意输入一句话的提示词:
「创建一个product页面,每个product项有名称描述和单价」,看看能得出怎样的结果。
结果还是比较让人意外的,只是简单的prompt,就能得到下图的页面布局和结构,看来FinClip这个产品设计者也是很用心的,非常懂开发者的“痛”。
正所谓一个好的电影,70%都要靠导演和编导的构思,一个好的应用程序也不例外,如果要利用好AI能力,就需要有更详细的prompt规划,例如一些结构(如下),大家感兴趣的可以多尝试下:
- 内容(什么类型的小程序):XXXXXXX
- 布局(小程序的主要页面都有什么,按钮、图片之类的):XXXXXX
- 交互(页面上用户的使用操作):XXXXXXX
如果prompt出来的效果并不能一次性的调整到位,FinClip的这个开发者工具还能局部修改页面代码,加上小程序页面的实时预览功能,就能够让一个开发小白尽可能的在成本输出之前进行多次调整,不得不说还是非常方便的。
其他有趣的功能,就是对于一个小程序开发小白来说,很有可能就连小程序开发语法和技术都不熟练,如何能够基于产品已有的开发文档,更便捷的进行知识提取,FinClip也通过一个AI agent连通了自有的小程序开发的知识连起来,让使用的开发者能够更好的对开发知识进行检索。
三、小结
从idea到上线,只花了2个小时,整个流程中,除了手动调整样式的数值,没有写一行代码,全部由AI能力,结合prompt帮助我完成。
这只是一次很浅层的探索案例,对我个人来说只是在小程序技能深入学习前的一个小实践,很有可能,对于熟练的前端开发来说可能就是一个小时工作量,但在这里分享的目的,是为了分享下所谓的拥抱新技术所带来的好处,与此同时,也是给大家带来一点小焦虑,正所谓“不进则退”,很多经验可能自己埋头积累并不能获得质的飞跃,最终可能自己是个"井底之蛙",花大力气却换来了小惊喜,还不如拥抱变化,使用新技术快速提升自己的工作技能。
共勉。
来源:juejin.cn/post/7423279449915293707
我为什么要搓一个useRequest
背景
- 在日常开发网络请求过程中,为了维护loading和error状态开发大量重复代码
- 对于竞态问题,要么不处理,要么每个需要请求的地方都要写重复逻辑
- 图表接口数据量大,甚至单接口响应就足以达到数十兆字节,而一个页面有数十个这样的请求,响应时间长,需要能够取消网络请求
以上逻辑,每个人的解法各不相同。为了解决上述问题,统一处理逻辑,需要一个能够统一管理网络请求状态的工具。
调研
首先想到的当然不是自己搓轮子,而是在社区上寻找是否已有解决方案。果不其然,找到了一些方案。
对于React,有像react-query这样的老前辈,功能全面,大而重;有像SWR这样的中流砥柱,受到社区广泛追捧;有像ahooks的useRequest这样的小清新,功能够用,小而美。
而对于Vue,一开始还真没让我找到类似的解决方案,后续进一步查找,发现有一个外国哥们仿造react-qeury仿写了一个vue-query,同时了解到雷达团队正是用的这一套解决方案,便又更深入了解了一下,发现这个库已经不维护了......进而了解到@tanstack/query,好家伙,这玩意胃口大得把react-query和vue-query都吃进去了,甚至svelte也不放过。继续找,发现有个哥们写了一个vue-request库,差不多类似于ahooks的useRequest,不错。然后经典的vue-use库也看了下,有一个useFetch方法,比较鸡肋,只适用于Fetch请求。
上述的社区库都相当不错,但对于我来说都太重了,功能繁多,而且在使用上,几个query都需要花费大量心智在缓存key上,太难用了。而ahooks和vue-request提供的useRequest的高阶函数,是比较符合我的胃口的,但是我还是嫌他们功能太多了。最关键的是,上述所有方案都没有达到我最主要的目的,能够真正取消网络请求。
因此,自己动手,丰衣足食。
动手
说干就干,搓一个咱自己的useRequest。
首先,定义useRequest的接口:
export declare const useRequest: <P extends unknown[], R>(request: (signal: AbortSignal, ...args: P) => Promise<R>, options?: IUseRequestOptions<R> | undefined) => {
result: ShallowRef<R | null>;
loading: ShallowRef<boolean>;
error: ShallowRef<Error | null>;
run: (...args: P) => Promise<Error | R>;
forceRun: (...args: P) => Promise<Error | R>;
cancel: () => boolean;
};
然后定义三个响应式状态,这里之所以用shallowRef,是考虑到部分请求结果可能很深,如果用ref会导致性能很差。
const result = shallowRef<IResult | null>(null);
const loading = shallowRef(false);
const error = shallowRef<Error | null>(null);
定义普通变量,在useRequest内部使用,不要在内部实现读取响应式变量(PS:踩过坑了,有个页面用watchEffect,loading状态一变就发请求,导致无线循环):
let abortController = new AbortController();
let isFetching = false;
然后定义run函数,如果有进行中的请求就取消掉:
const run = async (...args: IParams) => {
if (mergedOptions.cancelLastRequest && isFetching) {
cancel();
}
abortController = new AbortController();
setLoadingState(true);
const res = await runRequest(...args);
return res;
};
const runRequest = async (...args: IParams) => {
const currentAbortController = abortController;
try {
const res = await request(currentAbortController.signal, ...args);
if (currentAbortController.signal.aborted) {
return new Error('canceled');
}
handleSuccess(res);
return res;
} catch (error) {
if (currentAbortController.signal.aborted) {
return new Error('canceled');
}
handleError(error as Error);
return error as Error;
}
};
另外暴露出cancel方法:
const cancel = () => {
if (isFetching) {
mergedOptions.onCancel?.();
setLoadingState(false);
abortController.abort('cancel request');
return true;
}
return false;
};
在组件卸载时也取消掉未完成的请求:
onScopeDispose(() => {
if (mergedOptions.cancelOnDispose && isFetching) {
cancel();
}
});
以上,就是最基础版的useRequest实现,想要了解更多,欢迎直接阅读useRequest源码,核心代码一共也就一百来行。看完再把star一点,诶嘿,美滋滋。
产出
- useRequest源码
- useRequest使用文档
- 本次文章分享
收益
业务贡献
- 提供响应式的result、loading、error状态
- 内置缓存逻辑
- 内置错误重试逻辑
- 内置竞态处理逻辑
- 兼容 Vue 2 & 3
- 兼容 Axios & Fetch
- 取消网络请求
个人成长
- 学会如何编写一个基本的Vue工具库
- 了解如何用vite打包,并且带上类型文件
- 学会如何使用vue-demi兼容Vue2 & Vue3
- 学会如何用VuePress编写文档,过程中没少看源码
- 学会如何在npm上发包并维护
- 之前用jest写过测试,这次尝试了一下vitest,体感不错,过程中暴露不少代码问题
- 通过这个项目将以往所学的部分知识串联起来
参考
来源:juejin.cn/post/7293786784126255131
14 款超赞的代码片段生成工具😍(程序员必备)
在本文中,我将介绍 14 款代码片段图片生成器,每款工具都具备独特功能,能够满足不同需求,帮助你将代码转化为精美、易于分享的视觉内容。无论你是追求简约设计、高度自定义,还是想要生成动态代码片段,希望这篇文章能帮助你找到合适的工具,提升代码展示的效果。
CodeImage
CodeImage
是一个开源项目,为希望全面控制代码片段外观的开发者提供了丰富的自定义选项。它提供了多种窗口和边框设置、丰富的字体和主题选择,非常适合创建专业外观的代码视觉效果。
价格:免费
Codetoimg
Codetoimg
提供了现代化的用户界面,用于生成代码片段图片,并配有便捷的参数控制功能,操作简单直观。对于希望简单工作流程的开发者来说,这是一个绝佳选择。只需添加代码,调整几个滑块或开关,几秒钟内即可导出图片。
价格:免费
ShowCode
ShowCode
允许开发者通过横向标签布局创建高质量、可分享的代码图片,同时提供多种自定义选项。在左侧的代码编辑器中进行更改时,ShowCode
会为你提供即时预览。此外,它还配备了一个免费且不限使用次数的API
,非常方便实用。
价格:免费
Carbon
Carbon
是一款广受欢迎的工具,帮助开发者创建精美的代码片段。它提供了丰富的主题和字体选择,因其简洁清晰的视觉效果而备受青睐。支持多种编程语言,适用于所有希望将代码可视化的程序员,具备极高的通用性。
价格:免费
Ray.so
RaySo
是一款出色的工具,拥有直观的用户界面和色彩鲜艳、现代感十足的背景,简化了代码片段图片的创建过程。提供了暗模式和酷炫的渐变背景,非常适合需要为社交媒体或演示创建时尚代码图片的开发者。
价格:免费
Snappify
Snappify
以其强大的功能脱颖而出,不仅允许用户创建静态图片,还能生成动态代码片段,并提供丰富的自定义选项,满足更精细的展示需求。非常适合那些希望让代码具备视觉交互效果的用户,是展示代码的全方位解决方案。
价格:免费 + 3 个高级功能付费计划
Chalk.ist
Chalk.ist
是一款专为使代码片段视觉效果更具吸引力的工具,提供多种自定义选项,并支持添加多个代码块,增强展示灵活性。支持多种主题,并允许用户自定义背景,非常适合那些希望在输出效果上拥有更多创意和控制的开发者。
价格:免费
CodePNG
CodePNG
是一款极简风格的代码片段图片生成器,适合那些希望工作流程简洁、专注于任务的开发者使用。提供下拉菜单选择主题、编程语言和窗口控制,用户还可以选择自定义背景,并自由启用或禁用行号,进一步简化代码图片生成过程。
价格:免费
Pika Code
Pika Code
是一款帮助开发者创建精美代码视觉效果的工具。它允许用户完全编辑背景图案,灵活调整代码片段的外观,增强视觉吸引力。特别适合那些希望在保持专业美感的同时,创造独特代码片段的开发者使用。
价格:免费 + 1 个高级功能付费计划
Code to Image
Code to Image
以其简洁性脱颖而出,开发者可以通过自定义字体、颜色和阴影,轻松创建美观的代码图片。其用户友好的界面使其成为那些希望简单设置但仍能生成高质量图片的用户的理想选择。
价格:免费
HackReels
HackReels
是一款将代码片段转换为动画视频的工具,而非静态图片,非常适合在社交媒体平台上吸引观众的注意力。这一功能使HackReels
成为开发者展示互动代码片段或通过动态视觉效果分享代码教程的理想工具。
价格:免费 + 3 个高级功能付费计划
Codebit
Codebit
是另一个用于创建视觉上吸引人的代码片段动画的工具,动画的顺序通过Markdown
格式进行定义,非常适合那些希望以简洁方式生成代码动画的开发者。它非常适合开发者或教育者通过多步骤的方式解释某些编码概念。您可以将动画导出为MP4
视频文件,便于分享和展示。
价格:免费 + 2 个高级功能付费计划
CodeSnap
CodeSnap
是一款Visual Studio Code
扩展,允许开发者直接从编辑器中捕获高质量的代码图片,非常方便实用。与VS Code
的无缝集成使开发者能够即时将代码转化为美观的图片,而无需离开IDE
,非常适合注重效率的开发者。
价格:免费
Polacode
Polacode
是另一款实用的VS Code
扩展,在代码编辑器中直接生成代码片段图片时表现出色,方便开发者快速创建视觉效果优雅的代码图片。它使用简单,非常适合那些希望节省时间、不必切换到浏览器的开发者,能够快速将代码转换为可分享的视觉效果。
价格:免费
通过以上工具,您可以根据需求,找到最适合的代码片段生成器,轻松创建视觉效果出众的代码展示内容!
来源:juejin.cn/post/7424045557067907113
Mac 备忘录妙用
之前使用 Windows 的过程中,最痛苦的事是没有一款可以满足我快速进行记录的应用
基本都得先打开该笔记软件,然后创建新笔记,最后才能输入,这么多步骤太麻烦了
在切换到 MacOS 之后,让我惊喜的就是自带的备忘录,只需要简单地把鼠标移动到屏幕右下角,就可以创建一篇快速备忘录
Amazing!
这种方式叫做触发角,触发角可以在「系统设置 » 桌面与程序坞 » 触发角」设置:
四个触发角分别可以自由设置:
除了触发角,快捷键【 fn(🌐) + Q】同样能创建一篇快速备忘录
还有一个问题是,触发角 or 快捷键默认会打开上一次编辑的备忘录,如果想要每次都创建一篇新的快速备忘录的话,可以在设置这里:
把「始终回到上个快速备忘录」取消勾选
备忘录支持大部分高频的文本样式,选取文本后,在头部导航栏 Aa 这里做修改样式:
也能支持 check 清单:
表格功能比较弱鸡,就一个简单的表格,什么合并、冻结等高级功能都没有
另外还有图片、链接,这里就不再赘述。
备忘录默认支持文件夹分类,另外还支持标签分类,只需要在备忘录中使用井号(#
)加上对应文字,Mac 即会生成对应的标签清单:
之前在浏览网页的时候,特别想高亮某些内容,同时做一些拓展记录,安装过插件 Weava Highlighter,但是不好用,每次只要选中文字就 Weava 就会弹出,特别烦人。
没想到 Mac 备忘录居然原生支持这个功能
在 Safari 中,可以选择想要收藏的内容,右键「添加到快速备忘录」
创建快速备忘录之后,选中的这句话在 Safari 中会被高亮:
在最新的 MacOS 15 中更新中,备忘录新支持了录音功能:
并且还支持实时的语言转文本,但目前又又又又仅支持英语
库克的母语是英语,我的母语是无语 😅
另外,还新增了高亮颜色,分别有紫色、粉色、橙色、薄荷色和蓝色,不得不说,这几种颜色确实还挺好看的
最有用的功能当属于这个数学功能了
直接输入像是 (27/3)^2=
或者 47*96=
算式,备忘录会自动计算结果:
还支持自定义变量:
总体来说,Mac 的备忘录还算是一个不错的笔记软件,虽然缺乏像 Notion 的文档目录结构和块编辑的一些先进笔记能力,但它有着原生的支持,能够满足快速记录和基础编辑的需求
One more thing 👇
来源:juejin.cn/post/7424901430371696679
shadcn/ui 一个真·灵活的组件库
当前主流组件库的问题
我之前使用过很多组件库,比如 MUI,AntDesign,ElementUI,等等。他们都是很出名的组件库。
优点就不说了。他们的缺点是不灵活。
不灵活有 2 个原因。
生态不开放
第 1 个不灵活的原因是我感觉选了一家之后,就得一用到底,没有办法使用其他派系的组件了,比如我觉得 MUI 中的表格不好,Ant Design 的表格好,但是我无法在 MUI 中使用 AntDesign 的表格组件,因为在一个项目中无法同时使用 Mui 和 AntDesign。
无法使用的原因组件库把样式和组件库绑定在一起了,MUI 和 AntD 的样式又是不兼容的。使用了一个组件库,就需要套一个 ConfigProvider 或 ThemeProvider, 套上之后,就把地盘占领了,其他组件库就没法再套了。
修改不方便
第 2 个不灵活的原因要修改定制二次开发一个组件时感觉很麻烦,成本很高。有时需要看很多文档才能找到怎么修改,有时直接就无法修改。
Headless UI
为了解决组件库不灵活的问题,出现了无头组件库( headless ui ),不过 headless ui 虽然灵活却不方便。
如果要写一个按钮,按钮的各种状态都需要自己来关心,那还不如直接用大而全的组件库。大部分场景中,方便的优先级还是大于灵活的。
这也是为什么 radix-ui 一开始做了一个 headless 组件库,http://www.radix-ui.com/primitives , 后来又做了一个带主题的组件库
shadcn/ui
shadcn/ui 的优势正是解决了上面两个问题,同时又尽量保留了组件库使用方便的优势。
真·灵活
shadcn/ui 给人的感觉没有什么负担,因为 shadcn/ui ,主打一个按需加载,真·按需加载,加载组件的方式是通过命令把代码加到你的项目中,而不是从依赖中引用代码,所以代码不在外部依赖 node_modules 中,直接在自己的项目源代码中,不会有依赖了重重的一坨代码的感觉。因为都是源代码,这样你可以直接随意修改,二次开发。实际场景中,通常是不需要修改的,在偶尔需要修改是也很灵活,大不了复制粘贴一份,总比“明明知道怎么实现却无法实现强”。
拥抱 Tailwindcss
shadcn/ui 使用了 tailwindcss 作为样式方案,这样可以复制 tailwindcss 生态中的其他代码,比如 tailspark.co/ 和 flowbite.com/ ,一下子生态就大了很多,而且修改起来也方便。
方便
通过官方封装组件 + CLI 命令,灵活的同时并没有明显降低效率
比如要使用按钮组件,ui.shadcn.com/docs/compon… , 直接通过一行命令添加后就可以使用了 npx shadcn-ui@latest add button
总结
总结 shancn/ui 的优点
- 组件代码直接在项目源代码中,将灵活做到极致
- 拥抱 tailwindcss 生态
- 灵活的同时并没有明显降低效率
来源:juejin.cn/post/7382747688112783360
小红书路由处理大揭秘
起因
前两天看到小红书网页版的这个效果,感觉挺神奇的:


就是它同一个url对应了两种不同的页面。
上面这个是从列表页点开一个文章的时候,浏览器的路由变了,但是页面没有发生跳转,而是以一个弹窗的模式显示文章,底下我们还能看到列表。
但是当我们把这个url发送给别人,或者刷新浏览器后,同一个url会显示为下面这一个文章详情页,这样就避免了查看详情的时候还需要加载背后的列表。并且小红书的列表和详情是有对应关系(hero效果),但是列表页是随机排列的,如果要加载列表后再加载详情,就很难定位到文章在列表中的位置(随机推荐逻辑就很难改),而且还会影响性能。
前两天看到小红书网页版的这个效果,感觉挺神奇的:
就是它同一个url对应了两种不同的页面。
上面这个是从列表页点开一个文章的时候,浏览器的路由变了,但是页面没有发生跳转,而是以一个弹窗的模式显示文章,底下我们还能看到列表。
但是当我们把这个url发送给别人,或者刷新浏览器后,同一个url会显示为下面这一个文章详情页,这样就避免了查看详情的时候还需要加载背后的列表。并且小红书的列表和详情是有对应关系(hero效果),但是列表页是随机排列的,如果要加载列表后再加载详情,就很难定位到文章在列表中的位置(随机推荐逻辑就很难改),而且还会影响性能。
思考
解决方案我跟小伙伴思考了很久(基于vue-router),一开始我想的是通过路由守卫来控制,如果from来自列表,to就不跳转;如果from不是列表,则to跳转。但是这个方案会导致路由出现问题,因为如果没有跳转,则路由也不会变化。
另一个小伙伴想的是在路由表上,复用相同的组件,并使用keepAlive控制,来达到组件重用的目的。但是这个逻辑页有问题,keepAlive是路由的重用,其实不是组件的重用。
但当真正写起代码,才发现我们根本是想太多,其实解决方案简单到不足100行。
解决方案我跟小伙伴思考了很久(基于vue-router),一开始我想的是通过路由守卫来控制,如果from来自列表,to就不跳转;如果from不是列表,则to跳转。但是这个方案会导致路由出现问题,因为如果没有跳转,则路由也不会变化。
另一个小伙伴想的是在路由表上,复用相同的组件,并使用keepAlive控制,来达到组件重用的目的。但是这个逻辑页有问题,keepAlive是路由的重用,其实不是组件的重用。
但当真正写起代码,才发现我们根本是想太多,其实解决方案简单到不足100行。
代码
第一步:搭建项目
这里我采用vite来搭建项目,其实小红书这种网站需要考虑SEO的需求,应该会采用nuxt或者next等同构解决方案,这里我们简化了一下,只考虑路由的变化,所以也就不使用nuxt来搭建项目了。
这里我采用vite来搭建项目,其实小红书这种网站需要考虑SEO的需求,应该会采用nuxt或者next等同构解决方案,这里我们简化了一下,只考虑路由的变化,所以也就不使用nuxt来搭建项目了。
第二步,加入vue-router
routes.ts
import { RouteRecordRaw } from "vue-router";
export const routes: RouteRecordRaw[] = [
{
path: "/",
redirect: '/home'
},
{
path: "/home",
name: "Home",
component: () => import("./Home.vue"),
children: [
{
path: ':id',
name: "Detail",
component: () => import('./Detail.vue'),
}
]
},
]
router.ts
import {createRouter, createWebHistory} from "vue-router";
import { routes } from './routes.ts'
export const router = createRouter({
history: createWebHistory(),
routes,
})
文件结构:

我习惯吧routes和router分开两个文件,一个专心做路由表的编辑,另一个就可以专门做路由器(router)和路由守卫的编辑。
代码结构其实很简单,为了缩减代码量,我直接把page组件跟router放在一起了。
简单解释一下:
routes.ts 文件中我写了三个路由,一个是根路由/
,一个是列表/home
,一个是详情页Detail,这里使用了一个相对路由:id
的小技巧,待会你们就会知道为什么要这样了。
routes.ts
import { RouteRecordRaw } from "vue-router";
export const routes: RouteRecordRaw[] = [
{
path: "/",
redirect: '/home'
},
{
path: "/home",
name: "Home",
component: () => import("./Home.vue"),
children: [
{
path: ':id',
name: "Detail",
component: () => import('./Detail.vue'),
}
]
},
]
router.ts
import {createRouter, createWebHistory} from "vue-router";
import { routes } from './routes.ts'
export const router = createRouter({
history: createWebHistory(),
routes,
})
文件结构:
我习惯吧routes和router分开两个文件,一个专心做路由表的编辑,另一个就可以专门做路由器(router)和路由守卫的编辑。
代码结构其实很简单,为了缩减代码量,我直接把page组件跟router放在一起了。
简单解释一下:
routes.ts 文件中我写了三个路由,一个是根路由/
,一个是列表/home
,一个是详情页Detail,这里使用了一个相对路由:id
的小技巧,待会你们就会知道为什么要这样了。
第三步,编写Home.vue
<template>
<div>
<div class="text-red-700">Homediv>
<div class="w-full flex flex-wrap gap-3">
<router-link v-for="item in dataList" :to="`/home/${item.id}`">
<img :src="item.url" alt="">
router-link>
div>
<el-dialog title="Detail" v-model="dialogVisible">
<router-view>router-view>
el-dialog>
div>
template>
<script setup lang="ts">
import {computed, ref} from "vue";
import {useRoute, useRouter} from "vue-router";
import axios from "axios";
import {randomSize} from "../utils/randomSize.ts";
const route = useRoute()
const router = useRouter()
const lastRoute = computed(() => route.matched[route.matched.length - 1])
const dialogVisible = computed({
get() {
return lastRoute.value.name == 'Detail'
},
set(val) {
if (!val) {
router.go(-1)
}
},
})
const dataList = ref([])
const loading = ref(false)
function getList() {
loading.value = true
const data = localStorage.getItem('imageData')
if (!data) {
axios.get('https://picsum.photos/v2/list')
.then(({data}) => setDataList(data))
.then(data => localStorage.setItem('imageData', JSON.stringify(data)))
.finally(() => {
loading.value = false
})
} else {
setDataList(JSON.parse(data))
}
}
getList()
function setDataList(data) {
dataList.value = data.map(item => ({
id: item.url.split('/').pop(),
url: randomSize(item.download_url)
}))
return data
}
script>
这里重点看两个地方:
- template里需要有显示
detail
视图的地方,因为Home.vue除了要显示列表,还需要显示弹窗中的Detail,所以我把列表做成了router-link,并且把router-view放在了dialog里。(这里借助了tailwindcss和element-plus)

- 为了控制弹窗的显隐,我定义了一个dialogVisible计算对象,他的get来自router.matched列表中最后一个路由(最终命中的路由)是否为Detail,如果为Detail,就true,否则为false;它的set我们只需要处理false的情况,当false的时候,路由回退1。(其实是用push/replace还是用go我是有点纠结的,但是我看到小红书这里是用的回退,所以我也就用回退了,虽然回退在这种使用场景中存在一定的隐患)

<template>
<div>
<div class="text-red-700">Homediv>
<div class="w-full flex flex-wrap gap-3">
<router-link v-for="item in dataList" :to="`/home/${item.id}`">
<img :src="item.url" alt="">
router-link>
div>
<el-dialog title="Detail" v-model="dialogVisible">
<router-view>router-view>
el-dialog>
div>
template>
<script setup lang="ts">
import {computed, ref} from "vue";
import {useRoute, useRouter} from "vue-router";
import axios from "axios";
import {randomSize} from "../utils/randomSize.ts";
const route = useRoute()
const router = useRouter()
const lastRoute = computed(() => route.matched[route.matched.length - 1])
const dialogVisible = computed({
get() {
return lastRoute.value.name == 'Detail'
},
set(val) {
if (!val) {
router.go(-1)
}
},
})
const dataList = ref([])
const loading = ref(false)
function getList() {
loading.value = true
const data = localStorage.getItem('imageData')
if (!data) {
axios.get('https://picsum.photos/v2/list')
.then(({data}) => setDataList(data))
.then(data => localStorage.setItem('imageData', JSON.stringify(data)))
.finally(() => {
loading.value = false
})
} else {
setDataList(JSON.parse(data))
}
}
getList()
function setDataList(data) {
dataList.value = data.map(item => ({
id: item.url.split('/').pop(),
url: randomSize(item.download_url)
}))
return data
}
script>
这里重点看两个地方:
- template里需要有显示
detail
视图的地方,因为Home.vue除了要显示列表,还需要显示弹窗中的Detail,所以我把列表做成了router-link,并且把router-view放在了dialog里。(这里借助了tailwindcss和element-plus)
- 为了控制弹窗的显隐,我定义了一个dialogVisible计算对象,他的get来自router.matched列表中最后一个路由(最终命中的路由)是否为Detail,如果为Detail,就true,否则为false;它的set我们只需要处理false的情况,当false的时候,路由回退1。(其实是用push/replace还是用go我是有点纠结的,但是我看到小红书这里是用的回退,所以我也就用回退了,虽然回退在这种使用场景中存在一定的隐患)
剩下的代码就是获取数据相关的,我借用了picsum的接口(获取demo图片),并且我也没有做小红书的瀑布流(毕竟还是有点难度的,等有空了再做个仿小红书瀑布流来水一篇文章)。
Detail.vue
的代码就不贴了,它没有太多技术含量。
大概的页面效果是这样的:这里我就没有做数据加载优化之类功能了。(代码尽量简短)
我们可以看到,当点击详情的时候,浏览器右下角是有显示对应的路由,点开之后浏览器地址栏也变化了,详情内容在弹窗中出现,是我们想要的效果。
但是此时如果刷新页面,页面还是会一样先加载列表页,然后以Dialog显示详情。
刷新只显示详情
怎么做到刷新的时候只显示Detail页面而不显示列表页呢?我很快有一个想法:在路由表(routes.ts)的下面再增加一个路由,让它的路由路径跟详情的一样,这样刷新的时候会不会能够匹配到这个新路由呢?
// route.ts
export const routes = [
...
{
path: '/home/:id',
name: "DetailId",
component: () => import('./Detail.vue')
}
]
这个路由跟Home是同级的,使用了绝对路径来标记path(这就是上面detail采用相对路径的原因),同时为了避免name冲突,我换了一个name,component还是使用Detail.vue(这里我后来发现其实也可以使用其他的组件,其实真正起作用的是path,而不是component)。
但是不行,不论是将这个路由放在Home前面还是Home后面,都没法做到小红书的那种效果,放在home前面会导致从列表页直接跳转到详情页,不会在弹窗中显示;放在home后面又会因为匹配优先级的问题,匹配不到底下的DetailId
解决方案
但是前面的思考还是给了我灵感,添加一个路由守卫
是不是就可以解决问题呢?于是我添加了这样一个全局路由守卫:
// router.ts
router.beforeEach((to, from) => {
if (to.name === 'Detail') {
if (from.name === 'Home') {
return true
} else {
return { name: 'DetailId', params: to.params }
}
}
})
这个守卫的作用是,当发生路由跳转时,如果to为Detail,则判断from是否为Home,如果from为Home,则可以正常跳转,如果from不为Home,则说明是刷新或者链接打开,这时跳转至DetailId页面,并且params保持不变。
短短十行代码,就解决了问题。
可以看到,正常从列表显示详情还是会正常从弹窗中显示,而如果此时刷新页面,就会直接进入到详情页面。
如此我们成功的模仿了小红书的路由逻辑。
总结
其实做完效果才会发现代码非常简单无非就是一个路由守卫,一个弹窗显示,加一起不到一百行代码。代码地址我贴在下方了,希望对大家有帮助。
来源:juejin.cn/post/7343883765540962355
入职2个月,我写了一个VSCode插件解决团队遗留的any问题
背景
团队项目用的是React Ts,接口定义使用Yapi
。
但是项目中很多旧代码为了省事,都是写成 any
,导致在使用的时候没有类型提示,甚至在迭代的时候还发现了不少因为传参导致的bug。
举个例子
表格分页接口定义的参数是 pageSize
和 offset
,但是代码里传的却是 size
和 offset
,导致每次都是全量拉数据,然而因为测试环境数据量少,完全没测出来。
在这种项目背景下,大致过了一个月,结合自己试用期的目标(我也不想搞啊......),想通过一个工具来快速解决这类问题。
团队项目用的是React Ts,接口定义使用Yapi
。
但是项目中很多旧代码为了省事,都是写成 any
,导致在使用的时候没有类型提示,甚至在迭代的时候还发现了不少因为传参导致的bug。
举个例子
表格分页接口定义的参数是 pageSize
和 offset
,但是代码里传的却是 size
和 offset
,导致每次都是全量拉数据,然而因为测试环境数据量少,完全没测出来。
在这种项目背景下,大致过了一个月,结合自己试用期的目标(我也不想搞啊......),想通过一个工具来快速解决这类问题。
目标
把代码中接口的 any
替换成 Yapi
上定义的类型,减少因为传参导致的bug数量。
把代码中接口的 any
替换成 Yapi
上定义的类型,减少因为传参导致的bug数量。
交互流程
设计
鉴于当前项目中接口数量庞大(eslint扫出来包含any的接口有768个),手动逐一审查并替换类型显得既不现实又效率低下。
显然需要一种更加高效且可靠的方法来解决。
因为组内基本上都是使用 VSCode
开发,因此最终决定开发一个 VSCode
插件来实现类型的替换。
考虑到直接扫描整个项目进行替换风险较大,因此最终是 按文件维度,针对当前打开的文件执行替换。
整个插件分为3个命令:
- 单个接口替换
- 整个文件所有接口替换
- 新增接口

鉴于当前项目中接口数量庞大(eslint扫出来包含any的接口有768个),手动逐一审查并替换类型显得既不现实又效率低下。
显然需要一种更加高效且可靠的方法来解决。
因为组内基本上都是使用 VSCode
开发,因此最终决定开发一个 VSCode
插件来实现类型的替换。
考虑到直接扫描整个项目进行替换风险较大,因此最终是 按文件维度,针对当前打开的文件执行替换。
整个插件分为3个命令:
- 单个接口替换
- 整个文件所有接口替换
- 新增接口
整体设计
插件按功能划分为6个模块:

插件按功能划分为6个模块:
环境检测
Easy Yapi需要和Yapi服务器交互,需要用户提供Yapi Project相关的信息,因此需要填写配置文件(由使用者提供)。
插件执行命令时会对配置文件内的信息进行检测。
Easy Yapi需要和Yapi服务器交互,需要用户提供Yapi Project相关的信息,因此需要填写配置文件(由使用者提供)。
插件执行命令时会对配置文件内的信息进行检测。
缓存接口列表
从性能上考虑,一次批量替换后,会缓存当前Yapi项目所有接口的定义到cache文件中,下次替换不会重新请求。
从性能上考虑,一次批量替换后,会缓存当前Yapi项目所有接口的定义到cache文件中,下次替换不会重新请求。
接口捕获
不管是单个接口替换还是整个文件接口替换都需要先捕获接口,这里是通过将代码转成AST来实现。


不管是单个接口替换还是整个文件接口替换都需要先捕获接口,这里是通过将代码转成AST来实现。
类型生成
将接口定义转化成TS类型,通过循环+递归拼接字符串生成类型。
为什么不直接使用Yapi自带的ts类型?
- 命名问题,Yapi自带的ts类型命名过于简单粗暴,就是直接把接口路径拼接起来
- 有的字段因为粗心带了空格,最后还需要手动修改一遍类型

- 实际项目中有一层请求中间件,可能最终需要的类型只有data那一层,而Yapi定义的是整个类型
将接口定义转化成TS类型,通过循环+递归拼接字符串生成类型。
为什么不直接使用Yapi自带的ts类型?
- 命名问题,Yapi自带的ts类型命名过于简单粗暴,就是直接把接口路径拼接起来
- 有的字段因为粗心带了空格,最后还需要手动修改一遍类型
- 实际项目中有一层请求中间件,可能最终需要的类型只有data那一层,而Yapi定义的是整个类型
代码插入
- 将生成的类型插入文件中
// 检查文件是否存在
if (fs.existsSync(targetFilePath)) {
const currentContent = fs.readFileSync(targetFilePath);
if (!currentContent.includes(typeName)) { // 判断类型是否已存在
try {
fs.appendFileSync(targetFilePath, content); // 追加内容
editor.document.save(); // 调用vscode api保存文件
return true;
} catch (err: any) {
......
return false;
}
} else {
......
return false;
}
} else { // 文件不存在,创建并写入类型
try {
fs.writeFileSync(targetFilePath, content);
editor.document.save();
return true;
} catch (err: any) {
......
}
}
- 替换原有函数字符串
const nextFnStr = functionText
.replace(/(\w+:\s*)(any)/, (_, $1) => {
if (query.apiReq) {
return `${$1}${query.typeName}`;
}
// 没参数
else {
return "";
}
})
.replace(/Promise<([a-zA-Z0-9_]+|any)>/ , (_, $1) => {
if (res?.apiRes) {
return `Promise<${res?.typeName}>`;
}
return `Promise` ;
})
.replace(/,\s*\{\s*params\s*\}/, (_) => {
// 对于没有参数的case, 应该删除参数
if (!query.apiReq) {
return "";
}
return _;
});
- 调用vscode api替换函数字符串
const startPosition = new vscode.Position(functionStartLine - 1, 0); // 减1因为VS Code的行数是从0开始的
const endPosition = new vscode.Position(
functionEndLine - 1,
document.lineAt(functionEndLine - 1).text.length
);
const textRange = new vscode.Range(startPosition, endPosition);
const editApplied = await editor.edit((editBuilder) => {
editBuilder.replace(textRange, nextFnStr);
});
......
- 引入类型, 插入import语句
const document = editor.document;
const fullText = document.getText(); // 调用vscode api 拿到当前文件字符串
// 匹配单引号或双引号,并确保结束引号与开始引号相匹配
const importRegex =
/(import\s+(type\s+)?\{\s*[^}]*)(}\s+from\s+(['"])\.\/types(\.ts)?['"]);?/g;
let matchIndex = fullText.search(importRegex); // 使用search得到全局匹配的起始索引
if (matchIndex !== -1) {
// 已经有类型语句
let matchText = fullText.match(importRegex)?.[0]; // 获取完整的匹配文本
// 去重,如果 import { a, b } from './types'中已经有typeNames中的类型,则不需要重复引入
// existingTypes = ['a', 'b']
const existingTypes = (
/\{\s*([^}]+)\s*\}/g.exec(matchText)?.[1] as string
)
.split(",")
.map((v) => v.trim());
const uniqueTypeNames = typeNames.filter(
(v) => !existingTypes.includes(v)
);
// 将生成的类型插入原有的import type语句中
// 例如: import { a } from './types'
// 生成了类型 b c 则变成 import { a, b, c } from './types'
let updatedImport = matchText?.replace(
importRegex,
(_, group1, group2, group3) => {
// group1 对应 $1,即 import 语句到第一个 "}" 之前的所有内容
// group3 对应 $3,即 "}" 到语句末尾的部分
return `${
(group1.trim() as string).endsWith(",") ? group1 : `${group1}, `
}${uniqueTypeNames.join(", ")} ${group3}`;
}
);
// 计算确切的起始和结束位置
let startPos = document.positionAt(matchIndex);
let endPos = document.positionAt(matchIndex + matchText.length);
let range = new vscode.Range(startPos, endPos);
// 替换
await editor.edit((editBuilder) => {
editBuilder.replace(range, updatedImport as string);
});
} else {
// 直接插入import type
await editor.edit((editBuilder) => {
editBuilder.insert(
new vscode.Position(0, 0),
`import type { ${typeNames.join(",")} } from './types';\n`
);
});
}
// importStr导入语句需要进行判断再导入
// 例如:import request from '@service/request';
if (importStr && requestName) {
const importStatementRegex = new RegExp(
`import\\s+(?:\\{\\s*${requestName}\\s*\\}|${requestName})\\s+from\\s+['"]([^'"]+)['"];?`
);
const match = importStatementRegex.exec(editor.document.getText());
// 当前文件没有这个语句,插入
if (!match) {
await editor.edit((editBuilder) => {
editBuilder.insert(new vscode.Position(0, 0), `${importStr};\n`);
});
}
}
- 将生成的类型插入文件中
// 检查文件是否存在
if (fs.existsSync(targetFilePath)) {
const currentContent = fs.readFileSync(targetFilePath);
if (!currentContent.includes(typeName)) { // 判断类型是否已存在
try {
fs.appendFileSync(targetFilePath, content); // 追加内容
editor.document.save(); // 调用vscode api保存文件
return true;
} catch (err: any) {
......
return false;
}
} else {
......
return false;
}
} else { // 文件不存在,创建并写入类型
try {
fs.writeFileSync(targetFilePath, content);
editor.document.save();
return true;
} catch (err: any) {
......
}
}
- 替换原有函数字符串
const nextFnStr = functionText
.replace(/(\w+:\s*)(any)/, (_, $1) => {
if (query.apiReq) {
return `${$1}${query.typeName}`;
}
// 没参数
else {
return "";
}
})
.replace(/Promise<([a-zA-Z0-9_]+|any)>/ , (_, $1) => {
if (res?.apiRes) {
return `Promise<${res?.typeName}>`;
}
return `Promise` ;
})
.replace(/,\s*\{\s*params\s*\}/, (_) => {
// 对于没有参数的case, 应该删除参数
if (!query.apiReq) {
return "";
}
return _;
});
const startPosition = new vscode.Position(functionStartLine - 1, 0); // 减1因为VS Code的行数是从0开始的
const endPosition = new vscode.Position(
functionEndLine - 1,
document.lineAt(functionEndLine - 1).text.length
);
const textRange = new vscode.Range(startPosition, endPosition);
const editApplied = await editor.edit((editBuilder) => {
editBuilder.replace(textRange, nextFnStr);
});
......
const document = editor.document;
const fullText = document.getText(); // 调用vscode api 拿到当前文件字符串
// 匹配单引号或双引号,并确保结束引号与开始引号相匹配
const importRegex =
/(import\s+(type\s+)?\{\s*[^}]*)(}\s+from\s+(['"])\.\/types(\.ts)?['"]);?/g;
let matchIndex = fullText.search(importRegex); // 使用search得到全局匹配的起始索引
if (matchIndex !== -1) {
// 已经有类型语句
let matchText = fullText.match(importRegex)?.[0]; // 获取完整的匹配文本
// 去重,如果 import { a, b } from './types'中已经有typeNames中的类型,则不需要重复引入
// existingTypes = ['a', 'b']
const existingTypes = (
/\{\s*([^}]+)\s*\}/g.exec(matchText)?.[1] as string
)
.split(",")
.map((v) => v.trim());
const uniqueTypeNames = typeNames.filter(
(v) => !existingTypes.includes(v)
);
// 将生成的类型插入原有的import type语句中
// 例如: import { a } from './types'
// 生成了类型 b c 则变成 import { a, b, c } from './types'
let updatedImport = matchText?.replace(
importRegex,
(_, group1, group2, group3) => {
// group1 对应 $1,即 import 语句到第一个 "}" 之前的所有内容
// group3 对应 $3,即 "}" 到语句末尾的部分
return `${
(group1.trim() as string).endsWith(",") ? group1 : `${group1}, `
}${uniqueTypeNames.join(", ")} ${group3}`;
}
);
// 计算确切的起始和结束位置
let startPos = document.positionAt(matchIndex);
let endPos = document.positionAt(matchIndex + matchText.length);
let range = new vscode.Range(startPos, endPos);
// 替换
await editor.edit((editBuilder) => {
editBuilder.replace(range, updatedImport as string);
});
} else {
// 直接插入import type
await editor.edit((editBuilder) => {
editBuilder.insert(
new vscode.Position(0, 0),
`import type { ${typeNames.join(",")} } from './types';\n`
);
});
}
// importStr导入语句需要进行判断再导入
// 例如:import request from '@service/request';
if (importStr && requestName) {
const importStatementRegex = new RegExp(
`import\\s+(?:\\{\\s*${requestName}\\s*\\}|${requestName})\\s+from\\s+['"]([^'"]+)['"];?`
);
const match = importStatementRegex.exec(editor.document.getText());
// 当前文件没有这个语句,插入
if (!match) {
await editor.edit((editBuilder) => {
editBuilder.insert(new vscode.Position(0, 0), `${importStr};\n`);
});
}
}
总结
开发这个插件自己学到了不少东西,团队也用上了,有同学给了插件使用的反馈。
最后,试用期过了。
不过,新公司ppt文化是真的很重!!!
来源:juejin.cn/post/7423649211190591488
Seata解决分布式的四种方案
前言:
Seata是一款开源的分布式事务解决方案,提供高性能和简单易用的服务,确保微服务架构下的数据一致性, 本文章将依照本人的实际研究所得展开讲述,若有差错,敬请批评,还望海涵~
一、什么是分布式事务?
在分布式项目中,因为服务的拆分,每个服务单独管理自己的数据库。而一个业务操作往往涉及多个服务的数据落库,所以会出现某个服务出现业务异常而出现数据不一致的问题,为了避免数据库的数据不一致问题,分布式服务提供了全局事务,每个微服务作为分支事务,正是因为有全局事务的存在就可以保证了所有的分支事务能够同时成功或同时失败,能正确的进行数据的落库或回滚。
二、Seata解决分布式的四种方案
AT
第一阶段,在AT模式中TC(事务协调者)为包含在其中的多个RM(资源管理者)注册全局事务,然后调用分支事务注册到TC中,接着RM执行sql并提交到undo log(数据快照)中,数据此时处于一种中间状态(软状态),其中包含了事务执行前的旧数据,和事务执行之后的新数据,为保证数据一致性提供了保障。接着RM报告TC事务的执行状态。
第二阶段,TC判断事务是否全部执行成功,并对RM进行对应的提交事务或者回滚事务的通知。最后由TM(应用程序)提交或者回滚全局事务,TC再次进行检查分支事务的状态。
优点:AT模式以分二阶段提交事务,弥补了XA模型资源占用周期过长的缺陷,性能也得到了进一步的加强。
缺点:舍弃了XA的资源占用的同时,也不能保证事务的强一致性,会出现数据在转化过程中提前被用户访问的情况,导致用户得到的是旧数据。
Seata的AT模式的执行流程
XA :
XA模式主要特点为两阶段事务提交,
第一阶段,TC(事务协调者)会通知每个分支事务RM(每个微服务)做好执行事务的准备工作,RM返回就绪信息。在此阶段事务执行但不提交,但是持有数据库锁,占用了数据库的连接。
第二阶段,TC(事务的协调者)会根据第一阶段的执行报告来进行下一步判断,若所有分支事务都能执行成功,那就提交事务,数据落库,否则回滚事务。
优点:1、保证了数据的强一致性,满足ACID原则
2、支持常用的数据库,实现简单,没有代码侵入
应用场景:银行业务、金融行业
缺点:1、若事务因为第一阶段分支事务的失败而长时间等待则会导致资源长时间不得释放,业务无法快速实现响应的问题。
2、依赖关系型数据库实现
TCC
TCC模式的核心思想是通过三个阶段来确保分布式事务的一致性:
在Try阶段中系统会检查业务是否可以执行,并操作对资源进行预留,但依旧没有真正操作和使用资源。
在Confirm阶段,预留资源在此阶段会真正的使用
在Cancel阶段,释放掉锁定的资源
优点:TCC模式对资源的锁定预留保证了强一致性
缺点:TCC模式需要额外的网络通信和预留的预留,导致性能开销大。
SAGA
Saga模式是一种分布式事务处理模式,它将一个大的事务分解为多个小的事务(子事务),每个子事务都是独立的本地事务,可以独立提交或回滚。如果在执行过程中某个子事务失败,Saga模式会触发补偿事务(Compensation Transaction)来撤销之前已经提交的子事务的影响,从而保持数据的一致性。
来源:juejin.cn/post/7424901256151728166
MySQL9.0.0爆重大Bug!不要升级,不要升级...
前言
2024年7月1推出了最新的MySQL9.0.0创新版本,但是由于存在重大BUG,MySQL在7月23日重新发布了新版本.
1.重大bug
7/11日开源数据库软件服务商percona发布MySQL9.0.0重大BUG警告
本次涉及的是3个版本如下
MySQL 8.0.38
MySQL 8.4.1
MySQL 9.0.0
简而言之,如果你创建了大量的表,比如10000个,mysql守护进程将在重启时崩溃。
DELIMITER //
CREATE PROCEDURE CreateTables()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 10001 DO
SET @tableName := CONCAT('mysql', i);
SET @stmt := CONCAT('CREATE TABLE ', @tableName, ' (id INT PRIMARY KEY AUTO_INCREMENT, data VARCHAR(100));');
PREPARE createTable FROM @stmt;
EXECUTE createTable;
DEALLOCATE PREPARE createTable;
SET i := i + 1;
END WHILE;
END //
DELIMITER ;
CALL CreateTables();
于是我也在之前安装的环境做了下测试,确实存在当创建的表达到10000个后重启实例,就能看到实例启动失败。
2.修复版本
目前三个存在Bug的版本已经无法下载了,以下是之前MySQL9.0.0的截图
7 月 23 日之后
MySQL9.0.0和MySQL 8.4.1 和 MySQL 8.0.38
这三个版本已经无法从历史归档中下载了
目前发布的新版本如下,测试发现确实修复了bug
MySQL 9.0.1
MySQL 8.4.2
MySQL 8.0.39
3.版本升级的风险
数据库版本升级是一项重要的维护工作,但同时也伴随着一定的风险,确保升级过程顺利进行,同时保证业务的连续性和数据的安全性至关重要。
4.总结
本来以为这次 MySQL9.0会有一些王炸的新特性,结果呢,本次除了修复了 100 多个 Bug 之外,几乎没啥对开发者有帮助的点,结果还出现了重大bug.
来源:juejin.cn/post/7395022563976740902
未登录也能知道你是谁?浏览器指纹了解一下!
引言
大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢?
本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。
浏览器指纹
浏览器指纹是指通过浏览器的特征来唯一标识用户身份的一种技术。
它通过记录用户浏览器的一些基本信息,包括操作系统、浏览器类型、浏览器版本、屏幕分辨率、字体、颜色深度、插件、时间戳等,通过这些信息,可以唯一标识用户身份。
应用场景
其实浏览器指纹这类的技术已经被运用的很广泛了,通常都是用在一些网站用途上,比如:
- 资讯等网站:精准推送一些你感兴趣的资讯给你看
- 购物网站: 精确推送一些你近期浏览量比较多的商品展示给你看
- 广告投放: 有一些网站是会有根据你的喜好,去投放不同的广告给你看的,大家在一些网站上经常会看到广告投放吧?
- 网站防刷: 有了浏览器指纹,就可以防止一些恶意用户的恶意刷浏览量,因为后端可以通过浏览器指纹认得这些恶意用户,所以可以防止这些用户的恶意行为
- 网站统计: 通过浏览器指纹,网站可以统计用户的访问信息,比如用户的地理位置、访问时间、访问频率等,从而更好的为用户提供服务
如何获取浏览器指纹
指纹算法有很多,这里介绍一个网站 https://browserleaks.com/
上面介绍了很多种指纹,可以根据自己的需要选择。
这里我们看一看canvas,可以看到光靠一个canvas的信息区分,就可以做到15万用户只有7个是重复的,如果结合其他信息,那么就可以做到更精准的识别。
canvas指纹
canvas
指纹的原理就是通过 canvas
生成一张图片,然后将图片的像素点信息记录下来,作为指纹信息。
不同的浏览器、操作系统、cpu、显卡等等,画出来的 canvas 是不一样的,甚至可能是唯一的。
具体步骤如下:
- 用canvas 绘制一个图像,在画布上渲染图像的方式可能因web浏览器、操作系统、图形卡和其他因素而异,从而生成可用于创建指纹的唯一图像。在画布上呈现文本的方式也可能因不同web浏览器和操作系统使用的字体渲染设置和抗锯齿算法而异。
- 要从画布生成签名,我们需要通过调用
toDataURL()
函数从应用程序的内存中提取像素。此函数返回表示二进制图像文件的base64
编码字符串。然后,我们可以计算该字符串的MD5
哈希来获得画布指纹。或者,我们可以从IDAT块
中提取CRC校验和
,IDAT块
位于每个PNG
文件末尾的16到12个字节处,并将其用作画布指纹。
我们来看看结果,可以知道,无论是否在无痕模式下,都可以生成相同的 canvas
指纹。
换台设备试试
其他浏览器指纹
除了canvas
,还有很多其他的浏览器指纹,比如:
WebGL 指纹
WebGL(Web图形库)
是一个 JavaScript API
,可在任何兼容的 Web
浏览器中渲染高性能的交互式 3D
和 2D
图形,而无需使用插件。
WebGL
通过引入一个与 OpenGL ES 2.0
非常一致的 API
来做到这一点,该 API
可以在 HTML5
元素中使用。
这种一致性使 API
可以利用用户设备提供的硬件图形加速。
网站可以利用 WebGL
来识别设备指纹,一般可以用两种方式来做到指纹生产:
WebGL 报告
——完整的 WebGL
浏览器报告表是可获取、可被检测的。在一些情况下,它会被转换成为哈希值以便更快地进行分析。
WebGL 图像
——渲染和转换为哈希值的隐藏 3D
图像。由于最终结果取决于进行计算的硬件设备,因此此方法会为设备及其驱动程序的不同组合生成唯一值。这种方式为不同的设备组合和驱动程序生成了唯一值。
可以通过 Browserleaks test
检测网站来查看网站可以通过该 API
获取哪些信息。
产生 WebGL
指纹原理是首先需要用着色器(shaders)
绘制一个梯度对象,并将这个图片转换为Base64
字符串。
然后枚举 WebGL
所有的拓展和功能,并将他们添加到 Base64
字符串上,从而产生一个巨大的字符串,这个字符串在每台设备上可能是非常独特的。
例如 fingerprint2js
库的 WebGL
指纹生产方式:
HTTP标头
每当浏览器向服务器发送请求时,它会附带一个HTTP标头,其中包含了诸如浏览器类型、操作系统、语言偏好等信息。
这些信息可以帮助网站优化用户体验,但同时也能用来识别和追踪用户。
屏幕分辨率
屏幕分辨率指的是浏览器窗口的大小和设备屏幕的能力,这个参数因用户设备的不同而有所差异,为浏览器指纹提供了又一个独特的数据点。
时区
用户设备的本地时间和日期设置可以透露其地理位置信息,这对于需要提供地区特定内容的服务来说是很有价值的。
浏览器插件
用户安装的插件列表是非常独特的,可以帮助形成识别个体的浏览器指纹。
音频和视频指纹
通过分析浏览器处理音频和视频的方式,网站可以获取关于用户设备音频和视频硬件的信息,这也可以用来构建用户的浏览器指纹。
那么如何防止浏览器指纹呢?
先讲结论,成本比较高,一般人不会使用。
现在开始实践,根据上述的原理,我们知道了如何生成一个浏览器指纹,我们只需要它在获取toDataURL
时,修改其中的内容,那么结果就回产生差异,从而无法通过浏览器指纹进行识别。
那么,我们如何修改toDataURL
的内容呢?
我们不知道它会在哪里调用,所以我们只能通过修改它的原型链来修改。
又或者使用专门的指纹浏览,该浏览器可以随意切换js版本等信息来造成无序的随机值。
修改 toDataURL
第三方指纹库
FingerprintJS
FingerprintJS
是一个源代码可用的客户端浏览器指纹库,用于查询浏览器属性并从中计算散列访问者标识符。
与cookie
和本地存储不同,指纹在匿名/私人模式下保持不变,即使浏览器数据被清除。
ClientJS Library
ClientJS
是另一个常用的JavaScript
库,它通过检测浏览器的多个属性来生成指纹。
该库提供了易于使用的接口,适用于多种浏览器指纹应用场景。
来源:juejin.cn/post/7382344353069088803
告别 VSCode:VSCodium 自由开发之旅
Visual Studio Code (VSCode)
是一个由微软开发的免费源代码编辑器,它在开发者社区中非常受欢迎。然而,对于那些寻求完全开源替代方案的人来说,VSCodium 成为了一个不错的选择。下面我将撰写一篇关于 VSCodium 的文章,并阐述它与 VSCode 的主要区别。
VSCodium:自由且开放的开发环境
VSCodium 是一个基于 Visual Studio Code(简称 VSCode)的开源版本控制集成开发环境(IDE)。它不仅提供了与 VSCode 相同的强大功能,还去除了专有软件组件,确保了软件的自由度和透明度。
VSCodium 的特点
- 完全开源:VSCodium 是完全基于 MIT 许可证发布的,这意味着它的源代码是完全公开的,用户可以自由地查看、修改和分发其副本。
- 无数据收集:与 VSCode 不同的是,VSCodium 默认不会发送遥测数据或任何其他信息到微软服务器,这对于注重隐私的开发者来说是一个重要的优势。
- 社区驱动:由于其开源性质,VSCodium 可以通过社区贡献来改进和发展,任何人都可以参与到项目的维护和发展中来。
- 跨平台支持:VSCodium 支持 Windows、macOS 和 Linux 操作系统,为不同平台上的开发者提供了一致的- - 使用体验。
VSCodium 与 VSCode 的主要区别
尽管 VSCodium 和 VSCode 在功能上非常相似,但它们之间存在一些关键差异:
许可证与所有权:
- VSCode 虽然也是免费的,但它使用的是专有的“源码可用”许可证,并且由微软拥有和维护。
- VSCodium 则是完全开源的,遵循 MIT 许可证。
数据收集:
- VSCode 默认会收集一些使用数据,虽然这些数据主要用于改善产品,但对于部分用户来说可能是一个隐私问题。
- VSCodium 去除了所有与数据收集相关的功能,保证用户的隐私不受侵犯。
扩展生态系统:
- VSCode 有一个庞大的官方市场,其中包含了大量的插件和扩展,这使得它成为很多开发者的首选。
- VSCodium 使用相同的扩展格式,但由于其相对较小的用户基数,某些插件可能首先发布在 VSCode 市场上。
更新和支持:
- VSCode 得到了微软的强大支持,通常会有更频繁的更新和新特性发布。
- VSCodium 的更新频率取决于社区贡献者的工作,虽然它通常会紧跟 VSCode 的步伐,但在某些情况下可能会稍微滞后。
下载和安装
使用
与vscode基本相同。
结论
选择 VSCodium 还是 VSCode 主要取决于个人的需求和价值观。如果你重视隐私并且希望使用完全开源的工具,那么 VSCodium 将是一个很好的选择。如果你更看重官方支持以及广泛的插件生态系统,那么 VSCode 仍然是一个强大的开发工具。无论选择哪一个,你都将获得一个功能强大、灵活且易于使用的 IDE。
来源:juejin.cn/post/7424908830902485044
Tauri2.0 发布!不止于桌面!这次的“王炸”是移动端支持
开发桌面应用已经不再是唯一的战场,随着移动设备的普及,跨平台开发成了趋势。最近,Tauri带来了一个让开发者眼前一亮的功能——移动端支持。是的,Tauri不仅能开发轻量级桌面应用,还可以打通移动平台。这就像是它的“王炸”,在保持轻量化和高效的同时,直接扩展到移动端,瞬间吊打许多传统框架。
接下来,我们一起来看看Tauri的移动端支持,如何让它成为开发者的新宠。
1. 从桌面到移动:跨平台的真正意义
首先,我们要明确一点,Tauri的核心竞争力就是跨平台开发。过去它的焦点主要集中在Windows、macOS和Linux三大桌面系统上,能够让你用前端技术快速开发出轻量级桌面应用。如今,它打破了这一界限,开始支持iOS和Android移动端,这让开发者可以在一个统一的框架下,写出同时适配桌面和移动的应用。
对于开发者来说,这意味着更高的开发效率——你不需要为不同的系统做繁琐的适配工作,也不必纠结于不同平台的特性差异。只需要一个代码库,Tauri就能帮你搞定桌面和移动端,真正实现“一次开发,多端运行”。
2. 移动端支持的背后:依然轻量化
说到跨平台框架,大家可能会想到Flutter或者React Native,它们也支持移动端开发,但往往伴随着较大的应用体积和较高的资源消耗。Tauri则依然保持了它一贯的轻量化特性。通过依赖系统自带的WebView,Tauri的应用体积相对其他框架要小得多。
举个例子,同样是一个展示信息的应用,使用Tauri开发的移动端应用安装包可能比Flutter小很多。对用户来说,这种轻量化带来的优势显而易见:更快的下载速度、更少的存储空间占用,尤其适合那些存储空间紧张的设备。
3. 全平台一致的开发体验
Tauri的另一大优势就是它对开发者友好的体验。如果你是前端开发者,你可以继续使用你熟悉的前端技术栈(例如React、Vue、Svelte等),几乎没有学习成本。现在,你不仅能用这些技术来开发桌面应用,还可以直接迁移到移动端,这样的开发一致性大大降低了学习和维护成本。
而且,Tauri的API不仅支持桌面系统的功能调用,现在也在逐步支持移动端的特性。这意味着你可以在同一个代码库中同时调用桌面和移动端的系统功能,而不必为不同的设备写不同的代码。
例如,你可以通过Tauri提供的API来访问手机的传感器、相机等功能,未来它还将进一步扩展对移动端特性的支持。
import { open } from '@tauri-apps/plugin-dialog';
// when using `"withGlobalTauri": true`, you may use
// const { open } = window.__TAURI__.dialog;
// Open a dialog
const file = await open({
multiple: false,
directory: false,
});
console.log(file);
// Prints file path or URI
上面的代码不仅可以在桌面应用中运行,未来也能扩展到移动端,实现类似的功能调用。这种开发体验的统一性,对于希望快速上线跨平台应用的开发者来说,绝对是个福音。
4. Tauri在移动端的性能表现
性能问题总是开发者最关心的点。和Flutter这类框架不同,Tauri并没有选择单独构建一个跨平台的UI框架,而是借助操作系统自带的WebView,这样不仅保证了轻量化,还能依托于WebView的优化,带来较好的性能表现。
在移动端,Tauri同样依赖于系统的WebView,这意味着你不必担心额外的资源开销。只要用户的系统WebView版本足够新,应用的启动速度和渲染性能都能得到保障。而且,Tauri团队还在不断优化移动端的支持,未来的性能提升值得期待。
5. 为什么选择Tauri开发移动端应用?
总结一下,如果你是一名前端开发者,或者希望在一个框架下同时支持桌面和移动端,Tauri无疑是一个非常有吸引力的选择。它不仅帮助你用熟悉的前端技术快速开发应用,还通过轻量化设计和跨平台支持,解决了传统框架的种种痛点。
无论是应用体积、性能还是开发体验,Tauri都提供了一个高效且轻量的解决方案。对于那些追求高效开发、同时需要支持桌面和移动端的项目,Tauri的移动端支持就是它的“王炸”。
一些思考:
随着Tauri逐步扩展对移动端的支持,它正从一个桌面应用开发框架,进化为一个真正的全平台开发工具。如果你正在寻找一个轻量化的跨平台解决方案,或者想让你的应用跑在更多设备上,不妨试试Tauri,说不定这就是你一直在找的那个“它”。但是,如果说你的团队准备做一个现象级的产品,可能目前还并不适合,因为 tarui 团队自身看起来也并没有敲定移动端的完美方案,可能后续还会有所调整,追求性能、安全性的同时,还有一个更重要的事情是,兼容性,稳定性,你觉得呢?欢迎留言,掰扯掰扯。
来源:juejin.cn/post/7420980084361625600
一分钟学会 Rust 生成 JWT和校验
JWT 在 web 开发中作用毋庸置疑。下面我们快速的了解 jwt 在 rust 中的使用。
JWT 基础知识
JWT 是一种用于安全传输信息的轻量级、无状态的标准,适用于身份验证和信息交换场景。通过签名验证和自包含的声明,它能够高效地传递用户信息,同时减少服务器负担。
JWT 组成
Header.Payload.Signature
- Header: 类型(typ)和算法(alg, 指定 hash 签名算法),Header 对象会被比 base64URL 编码。
- Payload:负载部分(事件传递的数据),可以分为三类:
注册 Claims
、公共 Claims
以及私有 Claims
,Payload 也会被 base64URL 编码。 - Signature: 签名部分,用于验证消息的完整性,并确保消息在传输过程中未被篡改。
他们直接通过点 .
链接
依赖
[dependencies]
chrono = "0.4.38"
dotenv = "0.15.0"
jsonwebtoken = { version = "9.3.0", features = [] }
serde = { version = "1.0.210", features = ["derive"] }
env 文件
使用 dotenv 将 jwt 需要 secet 放在 env 文件中
JWT_SECRET=your_secret_key
token 的创建和解析
生成解析 token 需要:
- Algorithm 算法
- Claims 有效负载数据。
- chrono 世间处理
实现
jwt 创建和解析十分重要,我们将 jwt 的单独封装到 utils/jwt.rs下
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey, Algorithm, TokenData};
use serde::{Serialize, Deserialize};
use chrono::{Utc, Duration};
use std::error::Error;
use std::env;
// Define the claims structure
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
sub: String, // Subject (user ID, email, etc.)
exp: usize, // Expiration time (as UTC timestamp)
}
pub fn create_jwt(sub: &str) -> Result<String, Box<dyn Error>> {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
// Define some claims
let my_claims = Claims {
sub: sub.to_owned(),
exp: (Utc::now() + Duration::hours(24)).timestamp() as usize, // JWT expires in 24 hours
};
// Encoding the token
let token = encode(
&Header::default(),
&my_claims,
&EncodingKey::from_secret(secret.as_ref()),
)?;
Ok(token)
}
pub fn verify_jwt(token: &str) -> Result<TokenData<Claims>, Box<dyn Error>> {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_ref()),
&Validation::new(Algorithm::HS256),
)?;
Ok(token_data)
}
- create_jwt 对外暴露,接受 sub 作为参数,传递给 Claims 结构体。然后使用 encode 进行加密即可。
- verify_jwt 一般是用在请求拦截器中,请求头中 headers 获取 token 进行解析。
小结
本文主要讲解 jwt 在 rust 中使用。本质就是 Claims 对象和 token 创建与解析。我们需要一些 web 组件像 jsonwebtoken、chrono、dotenv 和 serde 等。JWT 往往单独的放在一个单独的 utils 模块中方便能使用时调用。
来源:juejin.cn/post/7424901483987304463
如何为上传文件取一个唯一的文件名
作者:陈杰
背景
古茗内部有一个 CDN 文件上传平台,用户在平台上传文件时,会将文件上传至阿里云 OSS 对象存储,并将 OSS 链接转换成 CDN 链接返回给用户,即可通过 CDN 链接访问到文件资源。我们对 CDN 文件的缓存策略是持久化强缓存(Cache-Control: public, max-age=31536000
),这就要求所有上传文件的文件名都是唯一的,否则就有文件被覆盖的风险。有哪些方式可以保证文件名全局唯一?
唯一命名方式
方式一:使用时间戳+随机数
这是我们最容易想到的一种方式:
const name = Date.now() + Math.random().toString().slice(2, 6);
// '17267354922380490'
使用时间戳,加上 4 位随机数,已经可以 99.99999% 保证不会存在文件名重复。可以稍微优化一下:
const name = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
// 'm191x7bii63s'
将时间戳和随机数分别转换成 36 进制,以减少字符串长度。通过上面一步优化可以将字符长度从 17 位减少至 12 位。
使用时间戳+随机数作为文件名的优势是简单粗暴,基本上可以满足述求;但是有极小概率存在文件名冲突的可能。
方式二:使用文件 MD5 值
生成文件的 MD5 Hash 摘要值,在 node 中代码示例如下:
const crypto = require('crypto');
const name = crypto.createHash('md5').update([file]).digest('hex');
// 'f668bd04d1a6cfc29378e24829cddba9'
文件的 MD5 Hash 值可以当成文件指纹,每个文件都会生成唯一的 hash 值(有极小的概率会 hash 碰撞,可以忽略)。使用 MD5 Hash 值作为文件名还可以避免相同文件重复上传;但是缺点是文件名较长。
方式三:使用 UUID
UUID (通用唯一识别码) 是用于计算机体系中以识别信息的一个标识符,重复的概率接近零,可以忽略不计。生成的 UUID 大概长这样:279e573f-c787-4a84-bafb-dfdc98f445cc。
使用 UUID 作为文件名的缺点也是文件名较长。
最终方案
从上述的几种命名方式可以看出,每种方式都有各种的优缺点,直接作为 OSS 的文件命名都不是很满意(期望 CDN 链接尽可能简短)。所以我们通过优化时间戳+随机数方式来作为最终方案版本。
本质上还是基于时间戳、随机数 2 部分来组成文件名,但是有以下几点优化:
- 由于 CDN 链接区分大小写,可以充分利用 数字+大写字母+小写字母(一共 62 个字符),也就是可以转成 62 进制,来进一步缩短字符长度
- 时间戳数字的定义是,当前时间减去 1970-01-01 的毫秒数。显然在 2024 年的今天,这个数字是非常大的。对此,可以使用 当前时间减去 2024-01-01 的毫秒数 来优化,这会大幅减少时间戳数字大小(2024-01-01 这个时间点是固定的,而且必须是功能上线前的一个时间点,确保不会减出负数)
示例代码如下:
/**
* 10 进制整数转 62 进制
*/
function integerToBase62(value) {
const base62Chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const base62 = base62Chars.length;
value = parseInt(value);
if (isNaN(value) || !value) {
return String(value);
}
let prefix = '';
if (value < 0) {
value = -value;
prefix = '-';
}
let result = '';
while (value > 0) {
const remainder = value % base62;
result = base62Chars[remainder] + result;
value = Math.floor(value / base62);
}
return prefix + result || '0';
}
const part1 = integerToBase62(Date.now() - new Date('2024-01-01').getTime()); // 'OkLdmK'
const part2 = integerToBase62(Math.random() * 1000000).slice(-4); // '3hLT'
const name = part1 + part2; // 'OkLdmK3hLT'
最终文件名字符长度减少到 10 位。但是始终感觉给 4 位随机数太浪费了,于是想着能否在保证唯一性的同时,还能减少随机数的位数。那就只能看看时间戳部分还能不能压榨一下。
只要能保证同一毫秒内只生成一个文件的文件名,就可以保证这个文件名是唯一的,这样的话,随机数部分都可以不要了,所以可以做如下优化:
// 伪代码
async function getFileName() {
// 等待锁释放,并发调用时保证至少等待 1ms
await waitLockRelease();
return integerToBase62(Date.now() - new Date('2024-01-01').getTime());
}
const name = await getFileName();
// 'OkLdmK'
由于 node 服务线上是多实例部署,所以 waitLockRelease
方法是基于 Redis 来实现多进程间加锁,保证多进程间创建的文件名也是唯一的。与此同时,还额外加上了一位随机数,来做冗余设计。最终将文件名字符长度减少至 7 位,且可以 100% 保证唯一性!
总结
看似非常简单的一个问题,想要处理的比较严谨和完美,其实也不太容易,甚至引入了 62 进制编码及加锁逻辑的处理。希望本文的分享能给大家带来收获!
来源:juejin.cn/post/7424901430378545164
哪位 iOS 开发还不知道,没有权限也能发推送?
这里每天分享一个 iOS 的新知识,快来关注我吧
前言
在 iOS App 开发中,推送通知是一个非常有效地触答和吸引用户的措施,通知可以成为让用户保持用户的参与度。
但大家都知道,苹果上每个 App 想要发推送给用户,都需要首先申请对应的权限,只有用户明确点了允许之后才可以。
大部分的 App 都是在启动时直接申请权限,这样的话,用户可能会因为不了解 App 的情况而拒绝授权,就会导致 App 无法发送通知。
其实在 iOS 12 中有个方案叫做临时通知。这功能允许应用在没有申请到权限的情况下发送通知。
今天就来聊聊这个不为人知的隐藏功能。
请求临时授权
要请求临时授权,我们需要使用与请求完全授权相同的方法 requestAuthorization(options:completionHandler:)
,但需要添加 provisional
选项。
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge, .provisional]) { isSuccess, error in
if let error {
print("Error requesting notification authorization: \(error)")
} else if isSuccess {
print("Requesting notification authorization is successed")
} else {
print("Requesting notification authorization is failed")
}
}
如果不加 provisional
选项,那么当你调用这个方法时,会直接弹出授权弹窗:
加 provisional
选项后这段代码不会触发对话框来提示用户允许通知。它会在首次调用时静默地授予我们的应用通知权限。
由于用户无感知,所以我们不必等待合适的时机来请求授权,可以在应用一启动时就调用。
发送通知
为了展示我们应用通知对用户的确是有价值的,我们可以开始通过本地或远程通知来定位用户。这里我们将发送一个本地通知作为示例,但如果你想尝试远程推送通知,可以查看我之前的几篇文章。
为了测试临时通知流程,以下是发送一个将在设置后 10 秒触发的本地通知的示例:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge, .provisional]) { isSuccess, error in
if let error {
print("Error requesting notification authorization: \(error)")
} else if isSuccess {
print("Requesting notification authorization is successed")
self.scheduleTestNotification()
} else {
print("Requesting notification authorization is failed")
}
}
return true
}
func scheduleTestNotification() {
let content = UNMutableNotificationContent()
content.title = "发现新事物!"
content.body = "点击探索你还未尝试的功能。"
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: 10,
repeats: false
)
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error scheduling notification: \(error)")
}
}
}
启动 App 后,我们退回到后台,等待 10 秒后,会看到我们发的通知已经出现在了通知中心中了。
此时可以看到这条通知中下边会出现两个按钮,如果用户想继续接受,就会点击继续接收按钮,如果不想继续接受,就会点击停止按钮。
如果用户点了停止按钮,那么就相当于我们应用的通知权限被用户拒绝了,相反的,如果用户点击了继续接收按钮,那么就相当于我们应用的通知权限被用户接受了。
鼓励用户完全授权
因此这条通知决定了用户是否继续接收我们 App 的通知,那么我们就需要慎重考虑这条通知的文案和时机,在用户体验到我们通知的好处之后,再发送这个通知,这样用户大概率就会选择继续接收通知。
如果用户仍然选择拒绝授权,我们还可以在 App 内的合适位置引导用户到设置页面去手动开启。
我这里写一个简单的示例,大家可以参考,先判断是否有权限,然后引导用户去设置页面。
class EnableNotificationsViewController: UIViewController {
private let titleLabel: UILabel = {
let label = UILabel()
label.text = "启用通知提示"
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 20, weight: .bold)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let descriptionLabel: UILabel = {
let label = UILabel()
label.text = "启用通知横幅和声音,保持最新了解我们的应用提供的一切。"
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let settingsButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("去设置", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .systemBlue
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.backgroundColor = .white
view.addSubview(titleLabel)
view.addSubview(descriptionLabel)
view.addSubview(settingsButton)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
settingsButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 30),
settingsButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
settingsButton.widthAnchor.constraint(equalToConstant: 120),
settingsButton.heightAnchor.constraint(equalToConstant: 44)
])
settingsButton.addTarget(self, action: #selector(openSettings), for: .touchUpInside)
}
@objc private func openSettings() {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
}
// 检查通知权限
func checkNotificationAuthorization() {
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { settings in
if settings.authorizationStatus == .authorized {
print("Notification authorization is authorized")
} else {
print("Notification authorization is not authorized")
}
}
}
最后
在我们的应用中实现临时通知是一种吸引用户的好方法,这其实也是苹果推荐的做法,创建一种尊重用户偏好的非侵入性通知体验,同时展示你应用通知的价值。
希望这篇文章对你有所帮助,如果你喜欢这篇文章,欢迎点赞、收藏、评论和转发,我们下期再见。
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!
来源:juejin.cn/post/7424335565121093672
签字板很难吗?纯 JS 实现一个!
前段时间有位同学问我:“公司项目中需要增加一个签字板的功能”,问我如何进行实现。
我说:“这种功能很简单呀,目前市面上有很多开源的库,比如:signature_pad
就可以直接引入实现”。
但是,该同学说自己公司的项目比较特殊,尽量不要使用 第三方的库,所以想要自己实现,那怎么办呢?
没办法!只能帮他实现一个了.
签字板实现逻辑
签字板的功能实现其实并不复杂,核心是 基于 canvas 的 2d
绘制能力,监听用户 鼠标 或者 手指 的移动行为,完成对应的 线绘制。
所以,想要实现签字板那么必须要有一个 canvas
,先看 html 的实现部分:
html
<body>
<canvas id="signature-pad" width="400" height="200">canvas>
<div class="controls">
<select id="stroke-style">
<option value="pen">钢笔option>
<option value="brush">毛笔option>
select>
<button id="clear">清空button>
div>
<script src="script.js">script>
body>
我们可以基于以上代码完成 HTML 布局,核心是两个内容:
canvas
画布:它是完成签字板的关键controls
控制器:通过它可以完成 画笔切换 以及 清空画布 的功能
css
css 相对比较简单,大家可以根据自己的需求进行调整就可以了,以下是 css 大家可以作为参考:
* {
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
width: 100vw;
background-color: #f0f0f0;
overflow: hidden;
}
canvas {
border: 1px solid #000;
background-color: #fff;
}
.controls {
margin-top: 10px;
display: flex;
gap: 10px;
}
button,
select {
padding: 5px 10px;
cursor: pointer;
}
js
js 部分是整个签字板的核心,我们需要在这里考虑较多的内容,比如:
- 为了绘制更加平滑,我们需要使用
ctx.quadraticCurveTo
方法完成平滑过渡 - 为了解决移动端手指滑动滚动条的问题,我们需要在
move
事件中通过e.preventDefault()
取消默认行为 - 为了完成画笔切换,我们需要监听
select
的change
事件,从而修改ctx.lineWidth
画笔
最终得到的 js 代码如下所示(代码中提供的详细的注释):
document.addEventListener('DOMContentLoaded', function () {
// 获取 canvas 元素和其 2D 上下文
var canvas = document.getElementById('signature-pad')
var ctx = canvas.getContext('2d')
var drawing = false // 标志是否正在绘制
var lastX = 0,
lastY = 0 // 保存上一个点的坐标
var strokeStyle = 'pen' // 初始笔画样式
// 开始绘制的函数
function startDrawing(e) {
e.preventDefault() // 阻止默认行为,避免页面滚动
drawing = true // 设置为正在绘制
ctx.beginPath() // 开始新路径
// 记录初始点的位置
const { offsetX, offsetY } = getEventPosition(e)
lastX = offsetX
lastY = offsetY
ctx.moveTo(offsetX, offsetY) // 移动画笔到初始位置
}
// 绘制过程中的函数
function draw(e) {
e.preventDefault() // 阻止默认行为,避免页面滚动
if (!drawing) return // 如果不是在绘制,直接返回
// 获取当前触点位置
const { offsetX, offsetY } = getEventPosition(e)
// 使用贝塞尔曲线进行平滑过渡绘制
ctx.quadraticCurveTo(
lastX,
lastY,
(lastX + offsetX) / 2,
(lastY + offsetY) / 2
)
ctx.stroke() // 实际绘制路径
// 更新上一个点的位置
lastX = offsetX
lastY = offsetY
}
// 停止绘制的函数
function stopDrawing(e) {
e.preventDefault() // 阻止默认行为
drawing = false // 结束绘制状态
}
// 获取事件中触点的相对位置
function getEventPosition(e) {
// 鼠标事件或者触摸事件中的坐标
const offsetX = e.offsetX || e.touches[0].clientX - canvas.offsetLeft
const offsetY = e.offsetY || e.touches[0].clientY - canvas.offsetTop
return { offsetX, offsetY }
}
// 鼠标事件绑定
canvas.addEventListener('mousedown', startDrawing) // 鼠标按下开始绘制
canvas.addEventListener('mousemove', draw) // 鼠标移动时绘制
canvas.addEventListener('mouseup', stopDrawing) // 鼠标抬起停止绘制
canvas.addEventListener('mouseout', stopDrawing) // 鼠标移出画布停止绘制
// 触摸事件绑定
canvas.addEventListener('touchstart', startDrawing) // 触摸开始绘制
canvas.addEventListener('touchmove', draw) // 触摸移动时绘制
canvas.addEventListener('touchend', stopDrawing) // 触摸结束时停止绘制
canvas.addEventListener('touchcancel', stopDrawing) // 触摸取消时停止绘制
// 清除画布的功能
document.getElementById('clear').addEventListener('click', function () {
ctx.clearRect(0, 0, canvas.width, canvas.height) // 清空整个画布
})
// 修改笔画样式的功能
document
.getElementById('stroke-style')
.addEventListener('change', function (e) {
strokeStyle = e.target.value // 获取选中的笔画样式
updateStrokeStyle() // 更新样式
})
// 根据 strokeStyle 更新笔画样式
function updateStrokeStyle() {
if (strokeStyle === 'pen') {
ctx.lineWidth = 2 // 细线条
ctx.lineCap = 'round' // 线条末端圆角
} else if (strokeStyle === 'brush') {
ctx.lineWidth = 5 // 粗线条
ctx.lineCap = 'round' // 线条末端圆角
}
}
// 初始化默认的笔画样式
updateStrokeStyle()
})
以上就是 纯JS实现签字板的完整代码,大家可以直接组合代码进行使用,最终展示的结果如下:
来源:juejin.cn/post/7424498500890705935
蓝牙耳机丢了,我花几分钟写了一个小程序,找到了!
你是否曾经经历过蓝牙耳机不知道丢到哪里去的困扰?特别是忙碌的早晨,准备出门时才发现耳机不见了,整个心情都被影响。幸运的是,随着技术的进步,我们可以利用一些简单的小程序和蓝牙技术轻松找到丢失的耳机。今天,我要分享的是我如何通过一个自制的小程序,利用蓝牙发现功能,成功定位自己的耳机。这不仅是一次有趣的技术尝试,更是对日常生活中类似问题的一个智能化解决方案。
1. 蓝牙耳机丢失的困扰
现代生活中,蓝牙耳机几乎是每个人的必备品。然而,耳机的体积小、颜色常常与周围环境融为一体,导致丢失的情况时有发生。传统的寻找方式依赖于我们对耳机放置地点的记忆,但往往不尽人意。这时候,如果耳机还保持在开机状态,我们就可以借助蓝牙技术进行定位。然而,市场上大部分设备并没有自带这类功能,而我们完全可以通过编写小程序实现。
2. 蓝牙发现功能的原理
蓝牙发现功能是通过设备之间的信号传输进行连接和识别的。当一个蓝牙设备处于开机状态时,它会周期性地广播自己的信号,周围的蓝牙设备可以接收到这些信号并进行配对。这个过程的背后其实是信号的强度和距离的关系。当我们在手机或其他设备上扫描时,能够检测到耳机的存在,但并不能直接告诉我们耳机的具体位置。此时,我们可以通过信号强弱来推测耳机的大概位置。
3. 实现步骤:从构想到实践
有了这个想法后,我决定动手实践。首先,我使用微信小程序作为开发平台,利用其内置的蓝牙接口实现设备扫描功能。具体步骤如下:
- • 环境搭建:选择微信小程序作为平台主要因为其开发简便且自带蓝牙接口支持。
- • 蓝牙接口调用:调用
wx.openBluetoothAdapter
初始化蓝牙模块,确保设备的蓝牙功能开启。 - • 设备扫描:通过
wx.startBluetoothDevicesDiscovery
函数启动设备扫描,并使用wx.onBluetoothDeviceFound
监听扫描结果。 - • 信号强度分析:通过读取蓝牙信号强度(RSSI),结合多次扫描的数据变化,推测设备的距离,最终帮助定位耳机。
在代码的实现过程中,信号强度的变化尤为重要。根据RSSI值的波动,我们可以判断耳机是在靠近还是远离,并通过走动测试信号的变化,逐渐缩小搜索范围。
下面是我使用 Taro 实现的全部代码:import React, { useState, useEffect } from "react";
import Taro, { useReady } from "@tarojs/taro";
import { View, Text } from "@tarojs/components";
import { AtButton, AtIcon, AtProgress, AtList, AtListItem } from "taro-ui";
import "./index.scss";
const BluetoothEarphoneFinder = () => {
const [isSearching, setIsSearching] = useState(false);
const [devices, setDevices] = useState([]);
const [nearestDevice, setNearestDevice] = useState(null);
const [isBluetoothAvailable, setIsBluetoothAvailable] = useState(false);
const [trackedDevice, setTrackedDevice] = useState(null);
useEffect(() => {
if (isSearching) {
startSearch();
} else {
stopSearch();
}
}, [isSearching]);
useEffect(() => {
if (devices.length > 0) {
const nearest = trackedDevice
? devices.find((d) => d.deviceId === trackedDevice.deviceId)
: devices[0];
setNearestDevice(nearest || null);
} else {
setNearestDevice(null);
}
}, [devices, trackedDevice]);
const startSearch = () => {
const startDiscovery = () => {
setIsBluetoothAvailable(true);
Taro.startBluetoothDevicesDiscovery({
success: () => {
Taro.onBluetoothDeviceFound((res) => {
const newDevices = res.devices.map((device) => ({
name: device.name || "未知设备",
deviceId: device.deviceId,
rssi: device.RSSI,
}));
setDevices((prevDevices) => {
const updatedDevices = [...prevDevices];
newDevices.forEach((newDevice) => {
const index = updatedDevices.findIndex(
(d) => d.deviceId === newDevice.deviceId
);
if (index !== -1) {
updatedDevices[index] = newDevice;
} else {
updatedDevices.push(newDevice);
}
});
return updatedDevices.sort((a, b) => b.rssi - a.rssi);
});
});
},
fail: (error) => {
console.error("启动蓝牙设备搜索失败:", error);
Taro.showToast({
title: "搜索失败,请重试",
icon: "none",
});
setIsSearching(false);
},
});
};
Taro.openBluetoothAdapter({
success: startDiscovery,
fail: (error) => {
if (error.errMsg.includes("already opened")) {
startDiscovery();
} else {
console.error("初始化蓝牙适配器失败:", error);
Taro.showToast({
title: "蓝牙初始化失败,请检查蓝牙是否开启",
icon: "none",
});
setIsSearching(false);
setIsBluetoothAvailable(false);
}
},
});
};
const stopSearch = () => {
if (isBluetoothAvailable) {
Taro.stopBluetoothDevicesDiscovery({
complete: () => {
Taro.closeBluetoothAdapter({
complete: () => {
setIsBluetoothAvailable(false);
},
});
},
});
}
};
const getSignalStrength = (rssi) => {
if (rssi >= -50) return 100;
if (rssi <= -100) return 0;
return Math.round(((rssi + 100) / 50) * 100);
};
const getDirectionGuide = (rssi) => {
if (rssi >= -50) return "非常接近!你已经找到了!";
if (rssi >= -70) return "很近了,继续朝这个方向移动!";
if (rssi >= -90) return "正确方向,但还需要继续寻找。";
return "信号较弱,尝试改变方向。";
};
const handleDeviceSelect = (device) => {
setTrackedDevice(device);
Taro.showToast({
title: `正在跟踪: ${device.name}`,
icon: "success",
duration: 2000,
});
};
return (
"bluetooth-finder">
{isSearching && (
"loading-indicator">
"loading-3" size="30" color="#6190E8" />
"loading-text">搜索中...Text>
View>
)}
{nearestDevice && (
"nearest-device">
"device-name">{nearestDevice.name}Text>
{getSignalStrength(nearestDevice.rssi)}
status="progress"
isHidePercent
/>
"direction-guide">
{getDirectionGuide(nearestDevice.rssi)}
Text>
View>
)}
"device-list">
{devices.map((device) => (
{device.deviceId}
title={device.name}
note={`${device.rssi} dBm`}
extraText={
trackedDevice && trackedDevice.deviceId === device.deviceId
? "跟踪中"
: ""
}
arrow="right"
onClick={() => handleDeviceSelect(device)}
/>
))}
AtList>
View>
"action-button">
type="primary"
circle
onClick={() => setIsSearching(!isSearching)}
>
{isSearching ? "停止搜索" : "开始搜索"}
AtButton>
View>
View>
);
};
export default BluetoothEarphoneFinder;
嘿嘿,功夫不负苦心人,我最终通过自己的小程序找到了我的蓝牙耳机。
我将我的小程序发布到了微信小程序上,目前已经通过审核,可以直接使用了。搜索老码宝箱 即可体验。
顺带还加了非常多的小工具,而且里面还有非常多日常可能会用到的工具,有些还非常有意思。
比如
绘制函数图
每日一言
汇率转换(实时)
BMI 计算
简易钢琴
算一卦
这还不是最重要的
最重要的是,这里的工具是会不断增加的,而且,更牛皮的是,你还可以给作者提需求,增加你想要的小工具,作者是非常欢迎一起讨论的。有朝一日,你也希望你的工具也出现在这个小程序上,被千万人使用吧。
4. 实际应用与优化空间
这个小程序的实际效果超出了我的预期。我能够通过它快速找到丢失的耳机,整个过程不到几分钟时间。然而,值得注意的是,由于蓝牙信号会受到环境干扰,例如墙体、金属物等,导致信号强度并不总是精确。在后续的优化中,我计划加入更多的信号处理算法,例如利用三角定位技术,结合多个信号源来提高定位精度。此外,还可以考虑在小程序中加入可视化的信号强度图,帮助用户更直观地了解耳机的大致方位。
一些思考:
蓝牙耳机定位这个小程序的开发,展示了技术在日常生活中的强大应用潜力。虽然这个项目看似简单,但背后的原理和实现过程非常具有教育意义。通过这次尝试,我们可以看到,借助开源技术和简单的编程能力,我们能够解决许多日常生活中的实际问题。
参考资料:
- 微信小程序官方文档:developers.weixin.qq.com
- 蓝牙信号强度(RSSI)与距离关系的研究:http://www.bluetooth.com
- 个人开发者经验分享: 利用蓝牙发现功能定位设备
来源:juejin.cn/post/7423610485180727332
opentype.js 使用与文字渲染
大家好,我是前端西瓜哥。
opentype.js 是一个 JavaScript 库,支持浏览器和 Node.js,可以解析字体文件,拿到字体信息,并提供一些渲染方法。
虽然名字叫做 opentype.js,但除了可以解析 OpenType,也可以解析 TrueType。
支持常见的字体类型,比如 WOFF, OTF, TTF。像是 AutoCAD 的 shx 就不支持了。
本文使用的 opentype.js 版本为 1.3.4
加载文字
加载文件字体为二进制数据,然后使用 opentype.js 解析:
import opentype from 'opentype.js'
const buffer = await fetch('./SourceHanSansCN-Normal.otf').then(
(res) => res.arrayBuffer(),
);
const font = opentype.parse(buffer);
需要注意的是,woff2 字体是用 Brotli 压缩过的文件,需要你额外用解压库做解压。
opentype.js 没有提供对应解压 Brotli 的能力,倒是提供了 Inflate 解压能力,所以可以解析 woff 字体。
font 这个对象保存了很多属性。
比如所有的 glyph(字形)、一些 table(表)、字体的信息(字体名、设计师等)等等。
获取字形(glyph)信息
glyph 为单个需要渲染的字形,是渲染的最小单位。
const glyph = font.charToGlyph('i')
另外 stringToGlyphs
方法会返回一个 glyph 数组。
const glyphs = font.stringToGlyph('abcd');
获取文字轮廓(path)
getPaths 计算得到一段字符串中每个 glyph 的轮廓数据。传入的坐标值为文字的左下角位置和文字大小。
const x = 60;
const y = 60;
const fontSize = 24;
const text = '前端西瓜哥/Ab1';
const textPaths = font.getPaths(text, x, y, fontSize);
textPaths 是一个 path 数组。
字符串长度为 9,产生了 9 个 glyph(字形),所以一共有 9 个 path 对象。
形状的表达使用了经典的 SVG 的 Path 命令,对应着 command 属性。
TrueType 字体的曲线使用二阶贝塞尔曲线(对应 Q 命令);而 OpenType 支持三阶贝塞尔曲线(对应 C 命令)。
渲染
我们有了 Path 数据,就能渲染 SVG 或 Canvas。
当然这个 OpenType.js 专门暴露了方法给我们,不用自己折腾做这层转换实现。
Canvas
基于 Canvas 2D 的方式绘制文字。
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
// ...
font.draw(ctx, text, x, y, fontSize);
渲染效果:
如果使用的字体找不到对应的字形,比如只支持英文的字体,但是却想要渲染中文字符。
此时 opentype.js 会帮你显示一个 豆腐块(“tofu” glyph)。豆腐块是取自字体自己设定的 glyph,不同字体的豆腐块还长得不一样,挺有意思的。
辅助点和线
字体是基于直线和贝塞尔曲线控制点形成的轮廓线进行表达的,我们可以绘制字体的控制点:
font.drawPoints(ctx, text, x, y, fontSize);
对文字做度量(metrics)得到尺寸数据。蓝色线为包围盒,绿色线为前进宽度。
font.drawMetrics(ctx, text, x, y, fontSize);
SVG
Path 实例有方法可以转为 SVG 中 Path 的 pathData 字符串。(Glyph 对象也支持这些方法)
path 长这个样子:
"M74.5920 47.6640L74.5920 57.5040L76.1040 57.5040L76.1040 47.6640ZM79.4640 46.9200L79.4640 59.8080C79.4640 60.1440 79.3440 60.2400 78.9600 60.2640C78.5520 60.2880 77.2320 60.2880 75.7440 60.2400C75.9840 60.6720 76.2480 61.3440 76.3200 61.7760Z"
拿到一段字符串对应的 path。
const textPath = font.getPath(text, x, y, fontSize);
const pathData = textPath.toPathData(4); // 4 为小数精度
// 创建 path 元素,指定 d 属性,添加到 SVG 上...
渲染结果。
另外还有一个 getPaths 方法,会返回一个 path 数组,里面是每个 glyph 的 path。
也可以直接拿到一个 path 元素的字符串形式。
path.toSVG(4)
会返回类似这样的字符串:
<path d="M74.5920 47.6640L74.5920 57.5040L76.1040 57.5040L76.1040 47.6640ZM79.4640 46.9200L79.4640 59.8080C79.4640 60.1440 79.3440 60.2400 78.9600 60.2640C78.5520 60.2880 77.2320 60.2880 75.7440 60.2400C75.9840 60.6720 76.2480 61.3440 76.3200 61.7760Z"/>
连字(ligature)
连字(合字、Ligatrue),指的是能够将多个字符组成成一个字符(glyph)。如:
像是 FiraCode 编程字体,该字体会将一些符号进行连字。
opentype.js 虽然说自己支持连字(Support for ligatures),但实际测试发现 API 好像并不会做处理。
用法为:
const textPath = font.getPath(text, x, y, fontSize, {
features: { liga: true },
});
字距(kerning)
两个 glyph 的距离如果为 0,会因为负空间不均匀,导致视觉上的失衡。
此时字体设计师就会额外调整特定 glyph 之间的字距(kerning),使其空间布局保持均衡。如下图:
opentype.js 可以帮我们获取两个 glyph 之间的字距。
const leftGlyph = font.charToGlyph('A');
const rightGlyph = font.charToGlyph('V');
font.getKerningValue(leftGlyph, rightGlyph)
// -15
返回值为 -15。代表右侧的字形 V 需往左移动 15 的距离。
结尾
本文简单介绍了 opentype.js 的一些用法,更多用法可以阅读官方文档。
不过你大概发现里面有某些方法对不上号,应该是迟迟未发布的 2.0.0 版本的文档。所以正确做法是切为 1.3.4 分支阅读 README.md 文档。
我是前端西瓜哥,关注我学习更多前端知识。
来源:juejin.cn/post/7424906244215455780
Java已死,大模型才是未来?
引言
在数字技术的浪潮中,编程语言始终扮演着至关重要的角色。Java,自1995年诞生以来,便以其跨平台的特性和丰富的生态系统,成为了全球范围内开发者们最为青睐的编程语言之一
然而,随着技术的不断进步和新兴语言的崛起,近年来,“Java已死”的论调开始不绝于耳。尤其是在大模型技术迅猛发展的今天,Java的地位似乎更加岌岌可危。然而,事实真的如此吗?Java的春天,真的已经渐行渐远了吗?本文将从多个维度深入探讨Java的现状、大模型技术的影响,以及Java与大模型融合的可能性,为读者提供一个更为全面和深入的视角。
Java的辉煌历史与稳健地位
Java,作为Sun Microsystems在1995年推出的编程语言,一经问世便凭借其独特的跨平台特性和丰富的生态系统,迅速在全球范围内赢得了广泛的认可和应用。从最初的Java Applet,到后来的Java Web开发、Java EE企业级应用,再到如今的Android应用开发、大数据处理等领域,Java都展现出了其强大的生命力和广泛的应用前景。
在最新的TIOBE编程语言排行榜上,Java长期位居前列,这足以证明其在开发界的重要地位。而在中国这个拥有庞大IT市场的国家中,Java更是受到了广泛的关注和追捧。无论是大型企业还是初创公司,Java都成为了其首选的开发语言之一。这背后,是Java的跨平台特性、丰富的库和框架、强大的社区支持等多方面的优势所共同铸就的。
然而,随着技术的不断进步和新兴语言的崛起,Java也面临着一些挑战和质疑。
一些人认为,Java的语法过于繁琐、性能不够优越、新兴语言如Python、Go等更加轻便灵活。这些观点在一定程度上反映了Java在某些方面的不足和局限性。
但是,我们也不能忽视Java在企业级应用、Web开发、大数据处理等领域的深厚积累和广泛应用。这些领域对Java的稳定性和可靠性有着极高的要求,而Java正是凭借其在这方面的优势,赢得了众多企业和开发者的青睐。
大模型技术的崛起与影响
近年来,随着人工智能和机器学习技术的飞速发展,大模型技术逐渐成为了人工智能领域的一大热点,可谓是百家争鸣。大模型技术通过构建庞大的神经网络模型,实现对海量数据的深度学习和处理,从而在各种应用场景中取得了令人瞩目的成果。
在自然语言处理领域,大模型技术通过训练庞大的语言模型,实现了对自然语言的深入理解和生成。这使得机器能够更加智能地处理人类的语言信息,从而实现更加自然和流畅的人机交互。在图像处理领域,大模型技术也展现出了强大的能力。通过训练庞大的卷积神经网络模型,机器能够实现对图像的精准识别和分析,从而在各种应用场景中发挥出巨大的作用。
大模型技术的崛起对软件开发产生了深远的影响。
首先,大模型技术为开发者提供了更加高级别的抽象和智能化解决方案。这使得开发者能够更加专注于核心业务逻辑的实现,而无需过多关注底层技术的细节。其次,大模型技术降低了AI应用的开发门槛。传统的AI应用开发需要深厚的数学和编程基础,而大模型技术则通过提供易于使用的工具和框架,使得开发者能够更加方便地构建和部署AI应用。最后,大模型技术推动了软件开发的智能化升级。从需求分析、设计到开发、测试和维护等各个环节都在经历着智能化的变革,这使得软件开发过程更加高效和智能。
Java与大模型的融合与变革
在大模型技术崛起的背景下,Java作为一种成熟且广泛应用的编程语言,自然也在探索与大模型技术的融合之路。事实上,Java与大模型的融合已经取得了不少进展和成果。
首先,Java社区对于大模型技术的支持和探索已经初见成效。一些开源项目和框架在Java环境中实现了深度学习和大模型技术的支持,如Deeplearning4j、ND4J等。这些项目和框架为Java开发者提供了丰富的工具和资源,使得他们能够更加方便地构建和部署基于大模型的应用。
其次,Java自身的特性和优势也为其与大模型的融合提供了有力的支持。Java作为一种面向对象的语言,具有强大的抽象能力和封装性,这使得它能够更好地处理大模型中的复杂数据结构和算法。同时,Java的跨平台特性也使得基于Java的大模型应用能够在不同的操作系统和硬件平台上运行,从而提高了应用的兼容性和可移植性。
最后,Java与大模型的融合也推动了软件开发的智能化升级。在需求分析阶段,大模型技术可以通过对海量数据的学习和分析,帮助开发者更加准确地把握用户需求和市场趋势。在设计阶段,大模型技术可以通过对已有设计的分析和优化,提高设计的合理性和效率。在开发阶段,大模型技术可以为开发者提供智能化的编程辅助和错误检查功能,从而提高开发效率和代码质量。在测试和维护阶段,大模型技术可以通过对应用的持续监控和分析,及时发现和修复潜在的问题和缺陷。
未来趋势与展望
随着AI和机器学习技术的不断发展,大模型技术将在未来继续发挥重要的作用。而Java作为一种成熟且广泛应用的编程语言,也将继续在大模型时代发挥其独特的优势和作用。
首先,Java将继续优化其性能和语法,提高开发者的开发效率和代码质量。同时,Java还将加强对大模型技术的支持和整合,为开发者提供更加全面和强大的工具和框架。
其次,Java将与更多新兴技术进行融合和创新。例如,随着云计算和边缘计算的兴起,Java将加强与这些技术的融合,推动云计算和边缘计算应用的发展。此外,Java还将与物联网、区块链等新兴技术进行深度融合,开拓新的应用领域和市场空间。
最后,Java将继续发挥其在企业级应用、Web开发、大数据处理等领域的优势,为各行各业提供更加稳定、可靠、安全的解决方案。同时,Java也将积极拥抱开源文化和社区文化,与全球开发者共同推动Java生态系统的繁荣和发展。
总之,Java作为一种历久弥新的编程语言巨头,将在大模型时代继续发挥其独特的优势和作用。通过与大模型技术的深度融合与创新,Java将引领编程世界的潮流,为各行各业带来更加智能化和自动化的解决方案。让我们共同期待Java在未来的辉煌!
写在最后
我不禁要感慨Java这一编程语言的深厚底蕴和持久魅力。它不仅是一段技术史,更是无数开发者智慧与汗水的结晶。在大模型时代,Java也会以其独特的稳定性和可靠性,持续为各行各业提供着坚实的支撑。正如历史的河流永不停息,Java也在不断地进化与创新,与新兴技术深度融合,共同推动着科技发展的浪潮。让我们携手前行,继续书写Java的辉煌篇章,为构建更加智能、更加美好的未来贡献力量。
来源:juejin.cn/post/7419967609451675700
架构师之道:为什么需要架构师
在聊架构师这个角色之前,我们得先搞清楚一件事:行业里对这个职位的看法其实挺模糊的。回顾一下,过去在一些大公司,有那么一段时间,架构师被视作一个专职的角色。但现在,情况有所变化,这个称呼渐渐退回到了“工程师”、“专家”或“研究员”这类更加技术性的职位名称里。换句话说,那些曾经被冠以“架构师”头衔的人,现在可能更多的是以工程师或研究的身份出现。
但这并不意味着架构师这个角色就消失了。事实上,在我的个人工作经验中,遇到的所谓“架构师”五花八门。特别是在一些小团队中,项目经理可能也会自封为架构师。这里的“架构师”,更多的时候不是一个官方职位,而是根据项目需要,某人暂时扮演的一个角色。
如果你想了解架构师到底是什么,先得接受一个事实:在当前的技术领域,架构师这个角色还没有一个清晰且统一的定义。它更像是一个根据项目情况变化的角色,而不是一个固定的职业路径。这也就意味着,成为一个架构师,与其说是达到某个职位的高度,不如说是在特定情境下,扮演的一个必要角色。
1、架构师的定义
架构师:任何复杂结构的设计人员。
架构师这个概念是从建筑业借鉴过来的。实际上,如果我们将“Software Architect”直译成中文,它意味着“软件建筑师”。这不仅仅是一个简单的名字借用;在很多方面,软件架构师的角色确实与建筑师有着相似之处。为了深入理解这种联系,我曾经翻阅了不少关于建筑设计的书籍(比如,《建筑的永恒之道》是一本极好的参考资料),通过这些学习,我发现软件架构与建筑设计之间不仅有着历史上的联系,它们的发展轨迹在某些方面也可能朝着相同的方向前进。
- 一脉相承:无论是传统的建筑师还是现代的软件架构师,他们的核心职责都是为了构建一个宏大的设计蓝图,确保在需求方和实施团队之间架起一座沟通的桥梁。
- **分道扬镳:**这种分歧主要是因为两个领域发展阶段的不同。建筑行业有数千年的实践历史和几百年的理论基础,已经发展成为一个高度模式化的领域。相比之下,软件架构作为一个领域的历史还不足二十年,仍然处于快速发展和变化之中。在这个阶段,软件架构师更多的是关注于技术的选择和实现方式,而不是设计的美感,这也是为什么软件架构师通常被看作是高级工程师,而不是设计师。
- 殊途同归:尽管如此,计算机科学的发展历程也证明了技术的持续抽象和模式化。从面向服务的架构(SOA)到物联网(IoT),再到“如果这个,那么那个”(IFTTT)的编程理念,我们已经开始看到软件领域向着建筑业已经达到的模块化水平迈进。随着技术的发展,软件架构师的工作越来越多地涉及到决定“要做什么”,而不仅仅是“怎么做”。这种变化预示着,未来软件架构师可能真正成为一个关注设计本身的职业,大学中甚至可能开设专门的“软件架构”专业。
当然,要实现这样的转变,我们这一代技术人员面临着巨大的挑战。我们需要像建筑行业的先驱那样,不断地规范化技术实践,形成设计模式,同时还需要建立一套既考虑架构美学又不忽视功能设计的统一标准。这是一条漫长而艰难的道路,但正如建筑领域所展现的那样,通过不懈努力,最终能够达到的成就是无限的。
2、架构师的职责
在软件行业的早期,"架构师"这个职位并不存在。那时候,大家都是程序员,也许会有一个领头的,称之为"主程序员"。但随着时间的推移,计算机技术飞速发展,软件开始渗透到生活的方方面面,不仅覆盖面广,而且复杂度大增。现在,拥有数百万甚至数千万行代码的软件系统已经变得司空见惯。随着软件日益复杂,开发者面临的挑战也与日俱增,因为人脑处理信息的能力终究是有限的。为了应对这些挑战,软件开发工具和方法也在不断进化,从汇编语言到高级编程语言,从基本的函数编程到复杂的框架,从面向过程到面向对象,从设计模式到架构模式,这一切都在展示着人类在软件工具开发上不断追求"封装"和"抽象"。
在这个抽象和封装的进程中,架构设计可谓达到了顶峰。作为架构师,不再需要过分纠结于编程语言、函数或设计模式等具体细节,而是要从一个更高的视角,全面考虑整个软件系统的设计,确保技术方案的合理性、需求的完整实现,以及与商业目标的契合度——这些构成了架构师的技术职责。
随着行业的不断发展,软件项目参与的角色和人员也变得越来越多样化,不仅仅局限于程序员和需求方,还扩展到了技术、产品、设计、商务、项目管理等多个团队。同时,技术团队内部的分工也越发细化,形成了前端、后端、测试、运维、技术支持等多个专业领域。在这种背景下,架构师成为了技术团队与产品、设计等非技术团队之间的桥梁,负责协调不同团队间的沟通,确保技术与业务的有效结合。作为技术团队的领导者,架构师需要勾画出整个项目的蓝图,明确各个环节的边界,引导各个专业领域的团队成员协同工作,共同完成软件系统的构建和发布——这就是架构师的组织职责。
2.1、架构师的技术职责
讨论软件架构师和建筑师的角色时,我们常常会发现两者之间存在着引人入胜的相似性和关键性的差异。这种比较不仅帮助我们理解软件架构师的角色,还揭示了软件开发过程中的独特挑战和机遇。
让我们来看看那两个在建筑领域根深蒂固,但在软件架构界至少目前不完全适用的基本理念:
- 职业路径的差异:在建筑领域,成为一名建筑设计师通常不需要经历建筑工人或工程师的角色。相反,软件架构师的成长路径几乎总是从软件工程师开始的,通过深入实践中积累经验和技术深度,逐渐演化成为能够担当架构设计重任的专家。这种差异反映了软件行业对于实际编码和项目经验的高度重视。
- 职责范围的差异:建筑学与工程学之间存在明确的分工——建筑师负责概念化设计,即决定要建造什么,而工程师解决实现问题,即如何建造。软件架构师则通常需要兼顾这两方面,他们不仅定义软件的功能和外观,还必须深入到技术实现的关键部分,确保设计的可行性和实用性。
这两个差异引出了软件架构师的三大技术职责,主要分为三大块:抽象设计、非功能设计以及关键技术设计。每一项都对成功的软件开发至关重要。
抽象设计的艺术:架构师的任务是在不同的抽象层次上自由地分析需求,每个层次或视角都为我们提供了一个独特的视图。这些视图不仅相互验证,而且共同组成了一个完整的设计蓝图。抽象设计可以从两个维度来看:
- 垂直维度:这里我们从顶层的企业架构到底层的系统架构,分别关注不同层面的需求和决策。比如,CTO更关心企业架构,因为它关系到公司整体的IT战略方向;产品经理和运维团队则更关注应用架构,涉及产品的业务流程和部署问题;而研发团队则深入到系统架构,专注于具体系统的设计和框架。
- 水平维度:针对特定业务,架构设计可以进一步细化为业务架构、数据架构、技术架构和应用架构。这些视角涵盖了从业务流程分析到技术选型的全方位设计。架构师和产品经理合作确定业务的核心领域模型;数据架构师设计数据模型;技术架构师选定技术栈;应用架构师规划应用的架构布局。
这样的划分使得每个角色都能在其专业领域内发挥最大的作用,同时确保整体设计的协调一致。架构设计的目的是为了确保技术解决方案能够精准地匹配业务需求,正如不同类型的桥梁设计师面对的挑战各不相同,软件架构的设计也需要根据业务领域的特性来定制。每个业务领域的独特性要求架构设计必须具有灵活性和创新性,以实现最佳的业务支持。
非功能需求的分析:架构的真正价值体现在对非功能性需求的满足上。这不仅仅是关于软件能做什么,更重要的是它如何做得好。我们谈到的非功能性需求包括软件系统的可靠性、扩展性、可测性、数据一致性、安全性和性能等方面。在真实世界的约束条件下,如成本、运行环境的限制,往往难以同时满足所有这些需求。
这就要求架构师进行精细的权衡。例如,在算法设计中可能需要在时间和空间之间做出选择,或者在系统性能和可靠性之间找到平衡点。有时,这种权衡甚至触及到学术领域,例如CAP理论就是关于在一致性、可用性和分区容错性之间做权衡的经典案例。架构师的工作就是在这些多维度的需求中找到最优解,确保系统在满足核心需求的同时,保持良好的性能和可用性。
关键技术设计:架构师的角色并不仅限于宏观设计。正如建筑师不仅关心建筑的整体外观,还会深入到细节设计一样,软件架构师也需要关注那些对系统整体质量有重大影响的关键技术细节。拿高迪的巴塞罗那圣家堂为例,连一把椅子的设计都不放过,每个细节都被赋予了深思熟虑的考虑。
在软件架构中,这意味着对系统中的关键组件进行详尽的设计,不仅是功能实现,更包括如何实现这些功能的具体技术选型、性能优化、安全策略等。架构师需要深入到系统的内部,确保每一个关键点都经得起考验,无论是在系统扩展、数据处理还是安全性方面。通过这样的细节关注,架构师确保软件不仅在今天有效,也能面对未来的挑战。
2.2、架构师的组织职责
架构师,作为企业中的一个核心角色,担当着“边界人”的重要职责。他们不仅是技术决策的制定者,也是不同角色和团队之间沟通协调的桥梁。
架构师与业务、产品团队的合作
在现实世界里,每个软件系统背后都有一个问题需要解决。简单地说,这就是软件存在的理由。但问题的解决并不只是随便写写代码就行,而是需要深入理解业务本身。这就是为什么,当一个软件的商业模式明确后,架构师要和业务、产品团队紧密地工作在一起。他们的目标是什么呢?是确定软件系统应该如何支撑业务,也就是说,他们需要设计出一个既能解决当前问题,又能支持未来业务发展的架构和领域模型。
这里的“架构”和“领域模型”其实就是把复杂的业务逻辑分解成一个个更容易理解和实施的部分。这种分解的好坏,直接影响到软件是否只能解决眼前的问题,还是能成为一个真正能随着业务成长的产品。
但要注意,业务和产品团队与架构师之间的关系并不总是那么简单。他们既是合作伙伴,又可能是谈判桌上的对手,尤其是在外包项目中。这时,架构师的角色不仅仅是技术决策者,更是需要在业务需求和技术实现之间找到平衡点的关键人物。简而言之,架构师的任务是确保软件既能满足当前的业务需求,又能灵活适应未来的发展。
架构师与技术团队的合作
在与技术团队的合作中,架构师的角色不仅仅是技术的引领者,更是团队合作的枢纽和策略制定者。直接切入重点,我们看到架构师在研发阶段的作用不仅限于构建技术框架和确定开发边界,还包括对项目中关键的非功能性需求——比如系统的性能、可靠性和安全性——进行精准的设计和实现。这意味着架构师不仅需要具备宏观的视野,将不同的研发团队和业务领域有序地编织在一起,还需要深入到技术细节中,亲自确保这些非功能需求能够得到满足。
在部署阶段,架构师与运维团队的合作变得尤为关键。他们需要共同评估如何在确保系统满足所有预定非功能需求的同时,实现成本和性能的最优平衡。这涉及到复杂的决策过程,如选择合适的硬件资源、决定是否采用CDN以提高性能、如何确保系统的高可靠性以及部署安全策略等。架构师在这一过程中扮演的是策略家和协调者的角色,旨在设计出一个既经济又高效的部署方案。
站在技术团队的角度,架构师的定位呈现出一种动态平衡。一方面,深耕于技术团队让架构师能够更深入地理解产品和业务需求,从而做出更加精准的技术设计和决策。另一方面,保持适当的独立和客观视角使得架构师能够从更宏观的层面审视和规划软件架构,避免过分陷入具体技术细节而失去整体的协调和控制。架构师需要在深入与独立之间找到合适的定位,确保既不脱离技术团队的实际,又能保持必要的全局视角。
除了技术设计和决策,架构师还承担着重要的组织职能——团队培养。架构师通过制定关键技术方案,不仅展示了技术领导力,还为团队成员提供了学习和成长的机会。这要求架构师既要有足够的技术洞察力亲自解决核心问题,又要给予团队足够的空间和信任,让他们在实践中学习和成长,即使这意味着需要承担一定的风险和责任。架构师的这一角色不仅是技术领导者,更是教练和导师,引导团队不断前进,提升技术实力。
综上所述,架构师与技术团队的协作是一场精心设计的平衡游戏,需要架构师在保证技术先进性和系统稳定性的同时,促进团队的协作与成长。架构师必须在技术的深度与广度、团队内部与外部的定位、以及领导与培养之间精准把握,以确保既能实现高效的技术创新,又能维护和促进团队的整体协作和发展。
和其他角色的协作
想象一下,一个架构师不仅仅是坐在电脑前写代码的技术人员,他其实更像是一个大指挥官。他的任务是什么呢?是确保软件项目从开始到结束都能顺利进行。这听起来简单,实际上却涉及到很多方面。
架构师需要和谁合作?首先是产品和技术团队,这个不用说,毕竟软件是由他们一起打造的。但这还不够,架构师还要和项目经理合作,确保项目按时按质完成。还有外部客户,他们是软件的最终用户,架构师需要理解他们的需求。甚至连公司财务部门也逃不过架构师的合作名单,毕竟软件项目的预算和成本也是非常关键的部分。
架构师的角色远不止是技术实施那么简单,他必须与所有相关方保持沟通和协调,从技术方案的角度出发,确保每个人的需求都得到满足。这就是架构师作为技术方案总负责人的真正含义:他是连接所有点的线,确保这些点能够形成一个完整的、成功的项目。
如何沟通
沟通是团队合作的基石,而对于架构师来说,沟通的艺术不仅仅是说话和写字那么简单。他们需要的是一种更高效、更直观的沟通方式——图表。为什么呢?因为图表能够跨越语言和专业的界限,让复杂的概念变得易于理解。
对不同的团队,架构师使用不同的图表作为沟通工具。比如,和产品团队沟通时,架构师会用业务架构图、用例图和领域模型图来说明软件要解决的业务问题和如何解决。这些图表帮助产品团队理解软件的业务价值和功能范围。
当转向研发团队,架构师则切换到应用架构图、组件图和时序图。这些工具帮助研发人员把握软件的内部结构和各部分如何协同工作。
对于运维团队,架构师又会用部署架构图来说明软件如何在实际环境中部署和运行。这样运维团队就能更好地理解和准备所需的资源和配置。
图表的力量在于它们提供了一个共同的语言,让所有人都能理解软件的设计和运作原理,无论他们的专业背景如何。同时,图表还能将设计文档化,便于传承和未来参考,确保软件的长期成功。简而言之,架构师通过使用图表作为沟通的桥梁,不仅促进了团队之间的理解和合作,也为软件的成功奠定了基础。
3、架构师的成长
在探讨架构师的角色时,我们首先要明确一点:架构师的职责直接定义了他们必须具备的能力。这意味着,作为架构师,不仅需要掌握广泛的技术知识,成为一个全面的技术专家,同时还要精通沟通与协作技巧。这样的定位要求架构师在技术领域有深入的理解和广泛的视野,能够看到技术如何服务于业务目标;另一方面,他们还需要具备出色的人际交往能力,能够有效地与团队成员、利益相关者进行沟通和协作,确保技术解决方案的顺利实施。简而言之,架构师的角色是技术与沟通能力的完美结合体,他们在将复杂概念分解成易于理解的部分方面发挥着关键作用,确保所有人都能跟上项目的进展。
所以,如果我们要总结架构师成长的路径,其实可以看作是两个主要方向:
3.1、技术层面
作为架构师,你的主战场是抽象建模,但战斗前的准备不能少,那就是深入了解你的业务领域。只有当你对业务有深刻的理解时,你才能高效地进行抽象和建模,并能够提炼出通用的设计方法。回想起几年前,我看到我们公司首席架构师的书单时,明白了这一点。尽管我们那时仅是金融领域边缘的一家支付公司,他的书单上却涵盖了银彳亍卡组织介绍、零售银行业务分析等领域。
另外,架构师不仅需要理解业务,还得对涉及的技术领域有广泛甚至深入的知识。对于互联网行业的架构师而言,这包括从编程语言、算法、数据库,到网络协议、分布式系统、服务器、中间件、IDC等各个层面。简而言之,架构师既是技术团队的门面,也是解决外部技术问题的关键人。除了技术的广度,深度同样重要,架构师对关键技术模块的设计应具备权威性见解。这样的角色定位,要求架构师既是全面的技术探索者,也是业务领域的深度分析师。
3.2、组织和个人成长层面
架构师站在技术与业务的十字路口,不仅需要精通各自的语言,更要在沟通中架起桥梁。这意味着,架构师的能力远不止于技术深度,还包括能够以口头和书面(特别是通过标准化图表)的形式,清晰、准确地传达设计思路和决策逻辑。这样的沟通技巧对于确保团队成员、利益相关者和客户之间的顺畅交流至关重要。
架构师的工作本质上是一场不断的权衡和平衡艺术,涉及技术选型、团队合作方式、人才培养、任务分配,以及如何在商业需求与成本控制、产品需求与技术能力之间找到最佳匹配点。这种持续的权衡过程不仅展现了架构师的策略思维,也是他们价值的体现。与工程师的角色相比,架构师更需要适应并接受不完美的解决方案和在给定条件下的近似精确,这往往是因为现实世界的复杂性和资源的限制。
从工程师到架构师的转变,意味着从追求代码的完美到追求系统设计和决策的优化平衡。这个过程中,架构师需要发展出对业务敏感性,深入理解业务背后的逻辑和需求,并以此为基础设计出既符合技术发展又服务于业务目标的架构方案。同时,架构师还要在技术前沿不断学习和探索,确保所采用的技术方案既前瞻性又实用,能够支撑业务的长期发展。
来源:juejin.cn/post/7361752279718297652
当一个程序员决定脱下孔乙己的长衫
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
在探索个人IP半年的时间里,自己利用业余时间学习写作,输出内容。
为爱发电半年了,我认为是时候做出点改变了。
行动、破除了一切焦虑
在起步之初,我就暗暗下定决心,在前半年的时间里,我不去思考如何变现,而是专注开阔眼界,锻炼写作能力。
上半年接触了很多做自媒体的朋友,很多都在做知识付费,各种各样的项目让人眼花撩乱,更是可以付费和他们学习。看着他们朋友圈不断发出的日入xxx、月入xxx的信息,说实话很难让人不心动。
当然,心动的同时,伴随你的一定还有焦虑。我加入了十几个群,群里面又不断的有着分享,一天需要看的消息内容可能有上千条。
正是自己给自己定下的锻炼写作能力的目标,和前半年不去思考如何变现的决定,让我在日常生活中,需要花大量的时间阅读、思考、输出,加上下班后还要看娃、看书,因此时间有限,做完该做的事情后,剩下的时间所剩无几,很多群、朋友圈我几乎没有时间去看了。
很庆幸自己给自己做了这个要求,不然我一定会在繁多的信息和项目中迷失自己。
行动,能够破除很多焦虑。
现在半年过去了,这段时间里,用心学习了不少东西,比如标题、框架、内容,最重要的是养成了持续输出的习惯,从自己过去的一些经历,到自己想要学习的东西,已经写了39篇文章,掘金的创作等级终于到了lv5,挺开心的。
复盘问题
半年时间,总结自己存在的问题,大概有两个点。
- 内容缺乏深度,主要是阅历不足,这个短时间内很难提升。
- 更新速度较慢,最快也需要集中精力两天,才能打磨好一个自己满意的文章。
我对自己输出的文章,是有自己的一个要求在里面的,比如不能标题党,内容框架要好,要有深度,还不能只罗列知识,要有自己的经历与感悟。
这导致我的输出速度较低,因为当你要讲明白一个观点、思维的时候,你一定会发现自己有不了解的地方,然后就要去重新学习、思考、整合。
但个人的见识、思维在短时间内不可能有突破性的提升,所以写出来的东西,一定只停留在我当下的能力阶段,吸引到的人也有限。自媒体就是要不断增加自己的更新速度,才能在这个信息爆炸的时代,吸引到更多人的注意力。
我还是蛮羡慕那些商业、技术大佬,能够吸引到几十万的粉丝,但如果按照目前的增长速度,错过平台红利期的我,或许几年后也无法积累这么多粉丝。
没有用户,没有这么多认可我的人,我做自媒体的目的:成为一个自由职业者,也就无法达到了。
现在,是时候去尝试一下,做一些能够赚钱的项目了。
脱下孔乙己的长衫
我决定深度参与一个项目,去做之前我认为没有价值的,写公众号爆文。
先介绍一下公众号爆文。
项目很简单,就是在公众号发布用户喜欢的内容,比如热点、公众人物、情感等文章,公众号平台会推荐,如果被推荐了,阅读量提升,你可以开通流量主,文章会自动插入广告,那么文中的广告,就会给你带来收益。
之前自己的一篇文章在1w左右阅读的情况下,收入大概在50元左右。文章在被推荐的情况下,如果达到10w+,单篇文章收入会在5、600左右,是一个门槛比较低的副业。
说实话,做这个决定很难,内心内耗的两个点:
- 对流量文这件事情,并不是很认可。我之所以用心去写每一篇文章,就是因为看过太多靠标题、热点,写出的没营养的文章了,我认为这完全就是在浪费每一个读者的时间。
- 如果仅看单篇文章的收益,我之前1w+阅读的文章,至少花费了我4、5天的业余时间,最终几十元的收益,对于一个程序员来讲,性价比极低,我瞧不上。
但我最近从思想上想清楚了上面的两个问题:
- 事实是最重要的。既然一些流量文能够达到10w+的阅读量,这正是代表了他符合用户、平台的需求。我或许觉着他毫无价值,但是有很多读者就是会认为阅读这类文章很爽,很休闲。毕竟不是谁都能踏下心来,在手机上阅读一篇大几千字满是干活的文章的。
- 我之所以瞧不上,是因为我认为收益较低。但这并不妨碍有不少人,通过大量的发布文章,可以每天都写出10w+的文章来,那么一天的收益至少在500以上,那么一个月至少破万。你看,当量积累到一个程度,那你很难忽视它能够达到的高度了。
而我现在遇到的问题,正好是无法获得更多的流量,无法提升自己的更新速度。结合我自媒体的方向,通过这个项目都可以打开我的思路,帮助我提升用户视角、提升效率。
所以,不能只是瞧不起那些只有流量、没有深度的流量文了,应该去尝试下场干干,而且我很笃定,这件事情,可以收获许多,不仅是在金钱,还有方法与认知。
说在最后
好了,文章到这里就要结束了。
所以你看,程序员想要在业余时间赚到一点工资以外的收入,其实真的挺难的, 不仅是行动上需要牺牲休闲、陪家人的时间,思维上面的卡点,一样重要。
但同时也很沮丧,或许务实主义确实是无数过来人想要告诉我们的真理。但是当自己不再这么的理想主义,我只能觉着我自己成熟了,但并不能感到高兴。
以上,就是我半年来的一个小小的复盘,与后续的计划。
来源:juejin.cn/post/7381349596637102089
setTimeout是准时的吗?
引言
最近在一些论坛上,有人讨论 setTimeout
的准确性。因此,我进行了探索,以解答这个问题。结果发现,setTimeout
并不完全可靠,因为它是一个宏任务。所指定的时间实际上是将任务放入主线程队列的时间,而不是任务实际执行的时间。
`setTimeout(callback, 进入主线程的时间)`
因此,何时执行回调取决于主线程上待处理的任务数量。
演示
这段代码使用一个计数器来记录每次 setTimeout
的调用。设定的间隔时间乘以计数次数,理想情况下应等于预期的延迟。通过以下示例,可以检查我们计时器的准确性。
function time () {
var speed = 50, // 间隔
count = 1 // 计数
start = new Date().getTime();
function instance() {
var ideal=(count * speed),
real = (new Date().getTime() - start);
count++;
console.log(count + '理想值------------------------:', ideal); // 记录理想值
console.log(count + '真实值------------------------:', real); // 记录理想值
var diff = (real - ideal);
console.log(count + '差值------------------------:', diff); // 差值
// 小于5执行
if (count < 5) {
window.setTimeout(function(){
instance();
}, speed);
};
};
window.setTimeout(function () {
instance();
}, speed);
};
打印1
我们可以在 setTimeout
执行之前加入额外的代码逻辑,然后再观察这个差值。
...
window.setTimeout(function(){
instance();
}, speed);
for(var a = 1, i = 0; i < 10000000; i++) {
a *= (i + 1);
};
...
打印2
可以看出,这大大增加了误差。随着时间的推移,setTimeout
实际执行的时间与理想时间之间的差距会不断扩大,这并不是我们所期望的结果。在实际应用中,例如倒计时和动画,这种时间偏差会导致不理想的效果。
如何实现更精准的 setTimeout
?
requestAnimationFrame
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,回调函数执行次数通常是每秒60次,也就是每16.7ms 执行一次,但是并不一定保证为 16.7 ms。
我们用requestAnimationFrame
模拟 setTimeout
function setTimeout2(cb, delay) {
const startTime = Date.now();
function loop() {
const now = Date.now();
if (now - startTime >= delay) {
cb();
} else {
requestAnimationFrame(loop);
}
}
requestAnimationFrame(loop);
};
打印3
貌似误差问题还是没有得到解决,因此这个方案还是不行。
while
想得到准确的,我们第一反应就是如果我们能够主动去触发,获取到最开始的时间,以及不断去轮询当前时间,如果差值是预期的时间,那么这个定时器肯定是准确的,那么用while
可以实现这个功能。
function time2(time) {
const startTime = Date.now();
function checkTime() {
const now = Date.now();
if (now - startTime >= time) {
console.log('误差', now - startTime - time);
} else {
setTimeout(checkTime, 1); // 每毫秒检查一次
}
}
checkTime();
}
time2(5000);
误差存在是 2
, 甚至为 0
, 但使用 while(true)
会导致 CPU 占用率极高,因为它会持续循环而不进行任何等待,会使得页面进入卡死状态,这样的结果显然是不合适的。
setTimeout 系统时间补偿
这个方案是在 Stack Overflow
看到的一个方案,我们来看看此方案和原方案的区别。
当每一次定时器执行时后,都去获取系统的时间来进行修正,虽然每次运行可能会有误差,但是通过系统时间对每次运行的修复,能够让后面每一次时间都得到一个补偿。
function time () {
var speed = 50, // 间隔
count = 1 // 计数
start = new Date().getTime();
function instance() {
var ideal=(count * speed),
real = (new Date().getTime() - start);
count++;
console.log(count + '理想值------------------------:', ideal); // 记录理想值
console.log(count + '真实值------------------------:', real); // 记录理想值
var diff = (real - ideal);
console.log(count + '差值------------------------:', diff); // 差值
// 5次后不再执行
if (count < 5) {
window.setTimeout(function(){
instance();
}, (speed - diff));
};
};
window.setTimeout(function () {
instance();
}, speed);
};
打印4
结论
多次尝试后,是非常稳定的,误差微乎其微,几乎可以忽略不计,因此通过系统的时间补偿,能使 setTimeout
变得更加准时。
来源:juejin.cn/post/7420059840971980834
开发了优惠促销功能,产品再也不汪汪叫了。。。
背景
大家好,我是程序员 cq。
最近快国庆节了,很多平台为了促销都开始发放优惠券,连上海都发放了足足 5 亿元的消费券,当知道这个消息的时候,我差点都忘了自己没钱的事了。
言归正传,我们 网站 也准备做一个优惠券的功能,正好借此机会给大家分享下我们是怎么做的优惠券,下面是某一次需求评审会的对话:
产品小 y:最近国庆节快到了,咱们网站也要像其他网站学习,给用户发放优惠券,把我们的价格打下来!
开发小 c:我才不做,你直接把会员价格下调不就好了?要啥优惠券,不做不做。
产品小 y:诶,你这个小同志,我下调价格是简单,但是我下调之后怎么统计有多少人看了我们的商品没买呢(转化率)?
开发小 c:你统计这个干什么,让用户感觉到优惠不就行了?
产品小 y:我统计转化率肯定是有意义的呀,我给不同的推广渠道发放不同的优惠券,可以得到不同渠道的转化率,如果有的渠道转化率很差,那我下次就不在这个渠道里推广了,有的渠道转化率很好,那我后面就要在这个渠道里加大推广量。还有就是我直接改价要手动改,假期人工改价格很难保证准时开始活动。
开发小 c:好像有道理啊,而且直接改价格也会让用户感觉平台的价格不稳定,价格经常变动,导致用户总是处于观望的状态。
技术方案
需求分析
既然上面需求评审确定了我们要做优惠券功能,那我们就要先梳理下我们要做什么东西。
首先就是优惠券的优惠能力,我们第一期就做的简单些,只用完成直减券,也就是购买会员的时候可以直接抵扣金额的消费券。当然实际上优惠券不光有直减券,还有满减券、折扣券等等,我们的网站也不需要那么复杂,能做一个直减券就够应对会员降价的功能了。
其次就是优惠券的开始时间和结束时间,便于控制优惠的开始时间和结束时间。
最后还有对应的使用条件,比如我们的优惠券只允许新用户使用,便于拉新,之类的。
那我们就可以确定这个优惠券只需要有减免价格、优惠券名称、开始结束时间以及使用条件就好了。
这时候网站运营同学听到我的喃喃低语,头突然伸过来。
他高呼:你这样可不行啊,光有这些可不够,我还要有优惠券的浏览量、领取量、使用量算转化率的嘞!
我心神一动,喔喔喔,那这样的话就可以做一个漏斗:浏览量 > 领取量 > 使用量,「使用量 / 领取量」就可以得到这个优惠券的转化率,也就知道了这个营销渠道的效果。
想到这里,我果断开口,好好好,就宠你。
具体实现
ok,我们确定了我们要做的内容,也就是要给会员商品做一个优惠券,其中要可以控制减免的金额、优惠券的浏览量、领取量、使用量、还有优惠券的开始时间、结束时间以及使用条件。
业务流程
明确了要做的需求,那么我们就可以确定下我们的整个业务流程:
技术实现
其实上述流程中最复杂的地方还是用户领取优惠券和使用优惠券购买后的处理逻辑,下面我带大家看下我们都是怎么做的。
首先我们要对领取优惠券做幂等,确保用户只在第一次领取优惠券的时候可以领取成功,后续的领取都返回已领取。要实现这个效果,我们需要对用户领取优惠券加锁,也就是下面这样:
String lockKey = "coupon_receive_lock:" + userId + ":" + couponId;
synchronized (lockKey.intern()) {
couponService.receiveCouponInner(loginUser, coupon);
}
但是由于我们公司是一个分布式的服务,所以本地锁其实是无效的,正好我们的服务用到了 redis,而且还引入了 redisson,那么就可以直接使用 redisson 的分布式锁,
String lockKey = RedisKeyConstant.COUPON_RECEIVE_LOCK + userId + ":" + couponId;
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock();
return couponService.receiveCouponInner(loginUser, coupon);
} finally {
lock.unlock();
}
但是这样写就太 low 了,我不如直接封装一个方法,让后续再使用分布式锁更方便:
public T blockExecute(String lockKey, Supplier supplier ) {
RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + lockKey);
try {
lock.lock();
// 执行方法
return supplier.get();
} finally {
lock.unlock();
}
}
这样,我们在使用分布式锁的时候就更方便了:
String lockKey = RedisKeyConstant.COUPON_RECEIVE_LOCK + userId + ":" + couponId;
return redisLockUtil.blockExecute(lockKey,
() -> couponService.receiveCouponInner(loginUser, coupon));
很好,在完成需求的时候又做了个基建工作,不愧是我。
其次就是在用户领取优惠券之后,我们的优惠券库存就应该对应的 -1,同时领取量对应 +1,那么这个 sql 就是这样的:
update coupon set leftNum = leftNum - 1, receiveNum = receiveNum + 1 where id = ?
这里可以确定数据不会出现混乱,因为在执行更新操作的时候,mysql 会有行锁,所以所有领取的用户都会在这里进行排队操作。
但是这样反而会出现问题,如果一瞬间领取优惠券的用户量激增,大家都在排队等库存量 - 1,就导致领取优惠券的时候用户会感觉很卡,也就是我们常说的热点行问题,不过像很多云厂商,都会针对热点行进行优化,比如某某云的 Inventory Hint 就可以实现快速提交 / 回滚事务,提高吞吐量,详细文章可以看下这篇文章:Inventory Hint,如果并发量真的很大的话,可以考虑用 redis 实现库存的 - 1,不过按照以往的经验来说不用 redis 也可以扛得住,这里就没必要再搞那么复杂了。
最后在领取优惠券之后,我们要接收来自支付中心的回调,根据用户的支付信息给用户赋予对应的会员权限,同时设置用户领取的优惠券为已使用,这样用户就不能再使用这个优惠券了,同时我们也可以关联查询到优惠券的使用量让运营同学进行分析。
了解领取优惠券的具体实现之后,后面的开发就简单了,这里就不再细说了,属于是商业机密了(其实就是代码写的太烂)。
优惠券到底带来了什么
优惠券的技术方案完成后,我们就应该深思一下,优惠券对我们来说到底有什么用,可以给我们带来什么好处?
对于用户而言自然是比之前便宜了许多,更省钱;用户觉得自己赚了,性价比高。
对于运营而言,可以对运营效果进行评估,比如销售变化、客户流失、获客成本等进而调整后续的营销活动;查看不同营销渠道表现,确定不同营销渠道优惠券效果;还可以利用历史数据预测未来的销售趋势,就像我们国庆节的优惠券就是参考了去年的优惠券使用情况来定的。
这也就是为什么运营同学会对优惠券这么痴迷了。
最后
通过这个优惠券功能的开发,其实大家也发现了,从最开始的直接改价到最后的优惠券,我们要做的不光是对商品的减免,还要做运营相关的功能。所以对于开发者而言,很多功能其实不仅要关注代码怎么写,更要考虑实现这个功能的意义,如果没意义,我就不用做了(bushi)。
了解功能实现的意义之后,我们身为开发就应该本能的想到,如何确定这个功能有多少人用,如何埋点,也就是为后续的运营计划以及后续的开发计划做准备。
来源:juejin.cn/post/7419598962346328105
MQTT vs HTTP:谁更适合物联网?
前言
随着物联网(IoT)技术的飞速发展中,其应用规模和使用场景正在持续扩大,但它关键的流程仍然是围绕数据传输来进行的,因此设备通信协议选择至关重要。
作为两种主要的通信协议,MQTT 协议和 HTTP 协议各自拥有独特的优势和应用场景:MQTT 完全围绕物联网设计,拥有更灵活的使用方式,和诸多专为物联网场景设计的特性;而 HTTP 的诞生比它更早,并且被广泛应用在各类非物联网应用中,用户可能拥有更加丰富的开发和使用经验。
本文将深入探讨在物联网环境下,MQTT 和 HTTP 的不同特性、应用场景以及它们在实际应用中的表现。通过对这两种协议的比较分析,我们可以更好地理解如何根据具体需求选择合适的通信协议,以优化物联网系统的性能和可靠性。
MQTT 是什么
MQTT 是一种基于发布/订阅模式的轻量级消息传输协议,针对性地解决了物联网设备网络环境复杂而不可靠、内存和闪存容量小、处理器能力有限的问题,可以用极少的代码为联网设备提供实时可靠的消息服务。
在典型的 MQTT 使用方式中,所有需要通信的客户端(通常是硬件设备和应用服务)与同一个 MQTT 服务器(MQTT Broker)建立 TCP 长连接。发送消息的客户端(发布者)与接收消息的客户端(订阅者)不需要建立直接的连接,而是通过 MQTT 服务器实现消息的路由和分发工作。
实现这一操作的关键在于另一个概念 —— **主题(Topic),**主题是 MQTT 进行消息路由的基础,它类似 URL 路径,使用斜杠 /
进行分层,比如 sensor/1/temperature
。订阅者订阅感兴趣的主题,当发布者向这个主题发布消息时,消息将按照主题进行转发。
一个主题可以有多个订阅者,服务器会将该主题下的消息转发给所有订阅者;一个主题也可以有多个发布者,服务将按照消息到达的顺序转发。同一个客户端,既能作为发布者,也能作为订阅者,双方根据主题进行通信,因此 MQTT 能够实现一对一、一对多、多对一的双向通信。
HTTP 是什么
HTTP 是一种基于请求/响应模式的应用层协议,尽管它主要针对传统的客户端-服务器架构而设计,但它在物联网应用中同样扮演着重要角色。
特别说明的是,本文对比的 HTTP 特指传统的请求/响应模式用例,基于 HTTP 协议扩展实现的 WebSocket 与 Server-Sent Events 协议不参与对比。
在典型的 HTTP 使用方式中,客户端(通常是浏览器或其他网络应用)向服务器发送请求以获取资源或提交数据,服务器接收到请求后,需要处理请求并返回响应,例如将提交的数据保存到数据库中,等待另一个客户端来请求获取。
HTTP 协议使用 URL 来标识资源路径,类似于 MQTT 中的主题(Topic)。例如,HTTP 请求中的 URL 可能是 http://example.com/api/sensor
,这与 MQTT 中的 sensor/1/temperature
主题有相似的分层结构。
HTTP 每次通信都通过独立的请求和响应流程完成,因此它需要额外的开销,并且两个客户端之间无法直接通信,在实时性上稍有欠缺。
资源消耗对比
MQTT 和 HTTP 都是非常简单的协议,许多物联网硬件设备和嵌入式系统都同时提供了对两者的支持。实时上资源体积与运行内存通常不会限制两者的使用,但 MQTT 设计初衷和使用特性是针对物联网设计,因此长期使用中,它具有更小的资源消耗。
首先,MQTT 在连接方面具有较低的开销。MQTT 将协议本身占用的额外消耗最小化,消息头部最小只需要占用 2 个字节,连接建立时的握手过程相对简单,可稳定运行在带宽受限的网络环境下。
一旦建立连接,客户端和服务器之间可以保持长时间的持久连接,多个消息可以在同一连接上传输,从而减少了频繁建立和断开连接的开销。以向 topic/1
主题发布 HelloWorld
内容为例,其报文信息如下:
字段 | 大小(字节) | 描述 |
---|---|---|
固定头部 | 1 | 固定为 0b0011xxxx |
主题长度 | 2 | 0x00 0x08 |
主题 | 9 | "topic/1" |
消息内容长度 | 2 | "HelloWorld"长度 |
消息内容 | 10 | "HelloWorld"内容 |
合计:24 |
HTTP 在每个请求-响应周期中都需要建立和断开连接,会带来额外的服务器资源使用。相对来说,HTTP 协议较为复杂,消息头部较大。同时,由于它是无状态协议,因此每次连接时客户端都需要携带额外的身份信息,这会进一步增加带宽消耗。
以向 http://localhost:3000/topic
URL 传输 HelloWorld
内容为例,在不携带身份凭证的情况下,其报文信息如下:
字段 | 大小(字节) | 描述 |
---|---|---|
请求行 | 17 | POST /topic HTTP/1.1 |
Host | 20 | Host: localhost:3000 |
Content-Type | 24 | Content-Type: text/plain |
Content-Length | 18 | Content-Length: 10 |
空行 | 2 | 用于分隔请求头和请求体 |
请求体 | 10 | HelloWorld 内容 |
合计:91 字节 |
总结:
- MQTT 的连接开销较低,连接建立简单,报文头较小,适用于需要频繁通信或保持持久连接的场景。
- 相比之下,HTTP 需要在每次请求-响应周期中建立和关闭连接,报文头较大,在网络带宽有限的情况下可能会增加传输延迟和负担。
在报文尺寸和连接开销方面,MQTT 通常比 HTTP 更为高效,特别是在需要频繁通信、保持长连接或网络带宽有限的物联网场景下。
安全性对比
MQTT 和 HTTP 两者都是基于 TCP 的协议,并且在协议设计上都充分考虑了安全性。
SSL/TLS 加密
两者都能支持通过 SSL/TLS 进行加密通信:
- 可以保护数据在传输过程中的机密性和完整性;
- 可以防止数据被窃听、篡改或伪造。
多样化的认证授权机制
- MQTT 提供了用户名/密码认证,可以扩展支持 JWT 认证,也支持客户端和服务器之间的 X.509 证书认证;在授权方面,可以支持基于主题的发布订阅授权检查,取决于MQTT 服务器的实现,。
- HTTP 则提供了更灵活的选项,包括基本认证(Basic Auth)、令牌认证(Token Auth)、OAuth 认证;可以通过应用层的权限控制机制,通过访问令牌(Access Token)、会话管理等来控制资源的访问权限。
物联网特性对比
MQTT 协议是专为物联网而设计的通讯协议,内置了丰富的物联网场景特性,能够有效地帮助用户实现设备间稳定可靠的通讯、实时数据传输功能,满足灵活的业务场景需求。
断线重连与持久会话
MQTT 支持持久连接和断线重连,确保设备与服务器之间的稳定通信,即使在网络不稳定的情况下也能保持连接。客户端可以选择是否创建持久会话,在断线重连时恢复之前的会话状态,确保消息不会丢失。
QoS 控制
MQTT 提供三种 QoS 等级:
- QoS 0:最多一次传递,消息可能会丢失。
- QoS 1:至少一次传递,消息可能重复。
- QoS 2:只有一次传递,消息保证不丢失也不重复。
客户端可根据需求选择适当的 QoS 等级,确保消息传递的可靠性。
多个客户端可以订阅相同的主题,接收相同的消息,适用于多个设备间共享数据或订阅相同事件的场景。
服务器可以保留指定主题最新的消息,当新的订阅者连接时立即发送,确保新订阅者获取最新数据。
客户端可以设置遗嘱消息,当客户端异常断开连接时,服务器会发布遗嘱消息,通知其他订阅者客户端已离线。
可以设置消息的过期时间,确保消息在一定时间内被消费,避免过期消息对系统造成不必要的负担。
尽管 HTTP 是 Web 应用中使用最广泛的协议之一,基于成熟的工具链和功能设计经验用户可以实现一些特性,但需要额外的开发工作。在物联网场景下,由于 MQTT 协议原生内置了许多适用于物联网的特性,使用 MQTT 可以降低开发成本,提高通信效率,更适合于物联网应用的需求。
对比总结
总而言之,MQTT 和 HTTP 在通信模型和物联网特性上有显著的区别:
- MQTT 基于发布订阅模型,HTTP 基于请求响应,因此 MQTT 支持双工通信。
- MQTT 可实时推送消息,但 HTTP 需要通过轮询获取数据更新。
- MQTT 是有状态的,但是 HTTP 是无状态的。
- MQTT 可从连接异常断开中恢复,HTTP 无法实现此目标。
- MQTT 支持更多开箱即用的物联网功能,HTTP 则没有针对性的设计。
这些差异将直接影响它们物联网中的使用场景选择:
- 实时通信: MQTT 在实时性要求较高的场景下更为适用。由于其基于发布/订阅模型,设备可以实时推送消息给服务器或其他设备,而不需要等待请求。例如,实时监测传感器数据、实时控制设备等场景下,MQTT 可以提供更快的响应速度。
- 轻量且频繁的通信: 对于带宽和资源有限的环境,MQTT 通常比 HTTP 更加高效。MQTT 不需要频繁建立连接,且消息头相对较小,通信开销较低;而 HTTP 同步的请求/响应模式则显得效率低下,每次通信都需要完整的请求和响应头,导致带宽和资源的浪费。
- 网络波动的场景: MQTT 支持客户端与服务器之间的持久连接,并且能够从连接异常中恢复,这意味着即使网络断开,设备重新连接后也能够恢复通信。而 HTTP 是无状态的,每次通信都是独立的,无法实现断线恢复。
另一个想法:MQTT 与 HTTP 集成使用
到目前为止,我们讨论的都是在物联网设备上更应该选择哪个协议的问题。实际上,在一个复杂的物联网应用中,不仅有硬件设备,还涉及到其他客户端角色和业务流程。MQTT 和 HTTP 作为物联网和互联网中最广泛使用的两种协议,在许多场景下可以互相补充使用,提高系统的效率和灵活性。
例如,在一个典型的车联网应用中,用户侧更适合使用 HTTP 协议:用户可以通过 App 中的"打开车门"按钮来控制停在车库中的汽车。这个过程中,App 与服务器之间并不是双向通信,使用 HTTP 也能实现更复杂和灵活的安全与权限检查。而服务器到车辆之间则依赖实时的双向通信:车辆需要确保任何时候都能够响应来自用户的操作。
车辆可以通过 MQTT 协议周期性的上报自身状态,服务器将其保存下来,当用户需要获取时,在 App 上通过 HTTP 协议完成请求即可。
在知名的 MQTT 服务器 EMQX 中,可以轻松、灵活地实现 MQTT 协议和 HTTP 协议的集成,从而实现这一过程。
EMQX 是一款大规模分布式 MQTT 物联网接入平台,为高可靠、高性能的物联网实时数据移动、处理和集成提供动力,助力企业快速构建物联网时代的关键应用。
HTTP → MQTT:
应用系统通过调用 EMQX 提供的 API,将 HTTP 请求转换为 MQTT 消息发送到指定设备,实现应用系统向设备发送控制指令或通知。
curl -X POST 'http://localhost:18083/api/v5/publish' \
-H 'Content-Type: application/json' \
-u '<appkey>:<secret>'
-d '{
"payload_encoding": "plain",
"topic": "cmd/{CAR_TYPE}/{VIN}",
"qos": 1,
"payload": "{ \"oper\": \"unlock\" }",
"retain": false
}'
MQTT → HTTP:
当设备发送 MQTT 消息到 EMQX 时,通过 EMQX 提供的 Webhook 可以将消息转发到 HTTP 服务器,实现设备数据的即时传输到应用系统。
配置界面如下:
在未来版本中,EMQX 还将提供提供扩展功能,能够将实时的 MQTT 消息保存到内置的消息队列(Message Queue)和流(Stream)中,并允许用户通过 HTTP 拉取的方式进行消费,更好地支持复杂的物联网应用场景,提供更强大的消息处理能力。
总结
总的来说,选择 MQTT 还是 HTTP 取决于具体的应用需求和场景特点。如果需要实时性好、双向通信、资源占用低的通信方式,可以选择 MQTT;只有简单的请求/响应通信,例如物联网客户端数据采集上报、主动拉取服务器数据,或者迫切希望使用现有的 Web 基础设施,那么可以选择 HTTP。
来源:juejin.cn/post/7423605871840608267
工作两年以来,被磨圆滑了,心智有所成长……
刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。
一路走来,磕磕绊绊,几年来,我总结了工作上的思考……
工作思考
- 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争吵。只有和气相处,我们才能推动工作的进展。
- 在讨论具体问题之前,先进行一些预备性的交流。情绪应放在第一位,工作讨论放在第二位。如果对方情绪不好,最好选择另一个时间再进行讨论。
- 在与他人交流时要保持初学者的态度和需求,不要用技术去怼人。
- 进入新团队先提升自己在团队的业务能力,对整个系统有足够的了解,不要怕问问题和学习。不要新入职就想毁天灭地,指手画脚 ”这里的设计不合理,那里有性能瓶颈“。
- 在各个事情上,都要比别人多了解一点。对于关键的事情要精通,对于其他事情也要多花一点时间去投入。
- 遇到困难时,先自己思考和尝试解决,然后再请教他人。不要机械地提问,也不要埋头一直搞而不主动提问。但如果是新入职,可以例外,多提问总没有坏处,但要在思考的基础上提问。
- 当向他人求助时,首先要清晰地阐述自己正在面临的问题、目标、已尝试的方法以及所需要的帮助和紧迫程度。所有的方面都要有所涉及。在提问之前,最好加上一句是否可以帮忙,这样对解决问题是否有帮助更加明确。因为别
- 一定有时间来帮助你,即使有时间,你也不一定找对了人。
- 在明确软件产品要解决的业务问题之前,先了解自己负责的那部分与业务的对应关系。
- 主要核心问题一定要提前叙述清楚,不要等别人问
- 要始终坚持追踪事情的进展,与与自己有交互的队友讨论接口,并关注他们的进度,以确保协调一致。
- 要主动向队友述说自己的困难,在项目延期或遇到困难时,要主动求助同事或领导,是否能分配部分工作给其他人,不要全部自己承担。
- 如果预计任务需要延期,要提前告知领导。如果有进展,也要及时向领导汇报。
- 如果无法参加会议但是自己是会议的重要参与者,一定要提前告知领导自己的进度、计划和想法,最好以书面形式或电话告知。如果可以远程参加,可以选择电话参加。除非有极其重要的事情,务必参加会议。不要假设别人都知道你的进度和想法。
- 要少说话,多做事。在开会时,不要凭借想当然的想法,可以询问其他小组的细节,但不要妄自揣测别人的细节,以为自己是对的。否则会被批评。
- 程序员如果经验丰富,很容易产生自我感觉良好的情绪。要避免这种情况,我们必须使用自己没有使用过的东西,并进行充分的测试,这样才能减少问题的出现。要提前考虑好所有细节,不要认为没有问题就不加考虑。要给自己留出处理问题的时间,并及时反馈并寻求帮助。
- 当与他人交流时,要始终保持有始有终的态度,特别是当寻求他人帮助时,最后一定要确认OK。要胆大心细,不要害怕犯错,要有成果,要快速并提高效率,不择手段地追求快速,并对结果负责。工作一定要完成闭环,要记事情要好,记住重要的事情并使用备忘录记录待办事项。
- 每完成一个项目后,应该回顾一下使用了什么知识、技能和工具。要总结并记录下这些,并与之前积累的知识和技能进行关联。如果发生了错误,也要记录下来,并将经验进行总结。
- 每天早上先思考今天要做什么,列出1、2、3,然后每天晚上下班时回顾已完成的任务、未完成的任务以及遇到的问题。
- 如果有待办事项没有立即处理,一定要用工具记录下来,不要心存侥幸以为自己能记住。
代码编写和技术问题
- 在代码编写过程中要认真对待,对于代码审核之前,要自己好好检查,给人一种可靠的感觉。
- 对于代码审核,不要过于苛刻,要容忍个人的发挥。
- 在提交代码给测试之前,应该先自行进行测试验证通过。
- 如果接口没有做到幂等性,那就会给未来的人工运维增加困难。当数据存在多份副本时,例如容量信息和上下游同时存在的资源,需要评估数据不一致的可能性以及解决方法。可以考虑通过数据校准或严格的代码编写来保证最终的一致性,或者考虑只在一方保存数据或以一方的数据为准。一旦出现数据不一致,则以其中一方的数据为准,无需人为干预即可自动达到数据再次一致。
- 要学会横向和纵向分割隔离系统,明确系统的边界,这样可以更好地进行并发合作开发和运维,提高效率。各个子系统应该独立变化,新的设计要考虑向后兼容性和上下游兼容性问题,包括上线期间的新老版本兼容。在设计评审阶段就应该重视这些问题。
- 如果在代码审查中无法发现业务问题或代码风格问题,不妨重点关注日志的打印是否合理和是否存在bug。
- 在依赖某个服务或与其他服务共享时,要确认该服务是否要废弃、是否是系统的瓶颈,以及是否可以自己进行改造或寻找更优的提供者。
- 使用缓存时注意预热,以防止开始使用时大量的缓存未命中导致数据库负载过高。
- 在使用rpc和mq、共享数据库、轮询、进程间通信和服务间通信时,要根据情况做出选择,并注意不要产生依赖倒置。
- 在接口有任何变动时,务必通过书面和口头确认。在这方面,要多沟通,尽量详细,以避免出现严重问题!毕竟,软件系统非常复杂,上下游之间的理解难以保持一致。
- 尽可能使用批量接口,并考虑是否需要完全批量查询。当批量接口性能较差时,设置适当的最大数量,并考虑客户端支持将批量接口聚合查询。批量接口往往是tp99最高的接口。
- 对于系统重要设计和功能,要考虑降级预案,并加入一些开关来满足安全性和性能需求。
- 如果数据不一致,可以考虑对比两方的不一致数据并打印错误日志,例如es/db等。
- 在系统设计之前,要充分调研其他人的设计,了解背景和现状。
- 废弃的代码应立即删除,如果以后需要,可以从git中找回。如果实在不想删除,也要注释掉!特别是对外的rpc、http接口,不使用的要立即删除,保持代码简洁。接手项目的人不熟悉背景情况,很难判断这段废弃代码的意义,容易造成混乱和浪费时间。要努力将其和其他有效代码联系起来,但这很困难。
- 在代码中要有详尽的日志记录!但是必须有条理和规范,只打印关键部分。对于执行的定时任务,应该打印足够详细的统计结果。最好使用简洁明了的日志,只记录最少量但最详细的信息,反馈程序的执行路径。
- 如果接口调用失败或超时,应该如何处理?幂等和重试如何处理?
当你写下一行代码前
- 要明确这行代码可能出现的异常情况以及如何处理,是将异常隔离、忽略还是单独处理,以防遗漏某些异常。
- 需要确保该行代码的输入是否已进行校验,并考虑校验可能引发的异常。
- 需要思考由谁调用该代码,会涉及哪些上游调用,并确定向调用者提供什么样的预期结果。
- 需要确定是否调用了一个方法或接口,以及该调用是否会阻塞或是异步的,并考虑对性能的影响。
- 需要评估该行代码是否可以进行优化,是否可以复用。
- 如果该行代码是控制语句,考虑是否能简化控制流程是否扁平。
- 对于日志打印或与主要逻辑无关的输出或报警,是否需要多加关注,因为它们可能还是很重要的。
- 如果代码是set等方法,也要仔细检查,避免赋错属性。IDE可能会有误提示,因为属性名前缀类似,set方法容易赋值错误。
当你设计一个接口时
- 接口的语义应该足够明确,避免出现过于综合的上帝接口
- 如果语义不明确,需要明确上下游的期望和需求。有些需求可以选择不提供给上游调用。
- 对于接口超时的处理,可以考虑重试和幂等性。在创建和删除接口时要确定是否具有幂等性,同时,幂等后返回的数据是否和首次请求一致也需要考虑。
- 接口是否需要防止并发,以及是否成为性能瓶颈也需要考虑。
- 设计接口时要确保调用方能够完全理解,如果他对接口的理解有问题,就需要重新设计接口。这一点非常关键,可以通过邮件确认或者面对面交流来确保调用方理解得清楚。
- 在开发过程中,需要定期关注队友的开发进度,了解他们是否已经使用了接口以及是否遇到了问题。这个原则适用于所有的上下游和相关方,包括产品和测试人员。要想清楚如何对接口进行测试,并与测试人员明确交流。
- 最好自己整理好测试用例,不要盲目地指望测试人员能发现所有的bug。
- 需要考虑是否需要批量处理这个接口,以减少rpc请求的次数。但即使是批量处理,也要注意一次批处理最多处理多少条记录,不要一次性处理全部记录,避免由于网络阻塞或批量处理时间过长导致上游调用超时,需要适度控制批量处理的规模。
来源:juejin.cn/post/7306025036656787475
CSS实现一个故障时钟效果
起因
最近公司事情不是太多,我趁着这段时间在网上学习一些Cool~
的效果。今天我想和大家分享一个故障时钟的效果。很多时候,一个效果开始看起来很难,但是当你一步步摸索之后,就会发现其实它们只是由一些简单的效果组合而成的。
什么是故障效果(Glitch)
"glitch" 效果是一种模拟数字图像或视频信号中出现的失真、干扰或故障的视觉效果。它通常表现为图像的一部分或整体闪烁、抖动、扭曲、重叠或变形。这种效果常常被用来传达技术故障、数字崩溃、未来主义、复古风格等主题,也经常在艺术作品、音乐视频、电影、广告和网页设计中使用。Glitch 效果通常通过调整图像、视频或音频的编码、解码或播放过程中的参数来实现。 来自ChatGPT
可以看到关键的表现为一部分或整体闪烁、抖动、扭曲、重叠或变形
,所以我们应该重点关注用CSS
实现整体闪烁、抖动、扭曲、重叠或变形
CSS
实现闪烁
Glitch 闪烁通常是指图像或视频中出现的突然的、不规则的、瞬间的明暗变化或闪烁效果
那么我们有没有办法通过CSS
来实现上述的效果,答案是通过随机不规则的clip-path
来实现!
我们先来看看clip-path
的定义与用法
clip-path
CSS 属性使用裁剪方式创建元素的可显示区域。区域内的部分显示,区域外的隐藏。
/* <basic-shape> values */
clip-path: inset(100px 50px);
clip-path: circle(50px at 0 100px);
clip-path: ellipse(50px 60px at 0 10% 20%);
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
clip-path: path(
"M0.5,1 C0.5,1,0,0.7,0,0.3 A0.25,0.25,1,1,1,0.5,0.3 A0.25,0.25,1,1,1,1,0.3 C1,0.7,0.5,1,0.5,1 Z"
);
再想想所谓的Glitch
故障闪烁时的效果是不是就是部分画面被切掉了~
span {
display: block;
position: relative;
font-size: 128px;
line-height: 1;
animation: clock 1s infinite linear alternate-reverse;
}
@keyframes clock {
0%{
clip-path: inset(0px 0px calc(100% - 10px) 0);
}
100%{
clip-path: inset(calc(100% - 10px) 0px 0px 0);
}
}
此时的效果如下:
啥啥啥,这看着是什么呀根本不像闪烁效果嘛,先别急,想想我们闪烁效果的定义突然的、不规则的、瞬间的明暗变化
,此时因为我们是在切割整体元素,如果我们再后面再重叠一个正常元素!
span {
display: block;
position: relative;
font-size: 128px;
line-height: 1;
//animation: clock 1s infinite linear alternate-reverse;
&:before{
display: block;
content: attr(data-time);
position: absolute;
top: 0;
color: $txt-color;
background: $bg-color;
overflow: hidden;
width: 720px;
height: 128px;
}
&:before {
left: -2px;
animation: c2 1s infinite linear alternate-reverse;
}
}
@keyframes c2 {
0%{
clip-path: inset(0px 0px calc(100% - 10px) 0);
}
100%{
clip-path: inset(calc(100% - 10px) 0px 0px 0);
}
}
可以看到通过手动偏移了-2px
后然后不断剪裁元素已经有了一定的闪烁效果,但是目前的闪烁效果过于呆滞死板,我们通过scss
的随机函数优化一下效果。
@keyframes c2 {
@for $i from 0 through 20 {
#{percentage($i / 20)} {
$y1: random(100);
$y2: random(100);
clip-path: polygon(0% $y1 * 1px, 100% $y1 * 1px, 100% $y2 * 1px, 0% $y2 * 1px);
}
}
23% {
transform: scaleX(0.8);
}
}
此时效果如下
可以看到闪烁的效果已经很强烈了,我们依葫芦画瓢再叠加一个元素上去使得故障效果再强烈一些。
span {
display: block;
position: relative;
font-size: 128px;
line-height: 1;
&:before,
&:after {
display: block;
content: attr(data-time);
position: absolute;
top: 0;
color: $txt-color;
background: $bg-color;
overflow: hidden;
width: 720px;
height: 128px;
}
&:before {
left: calc(-#{$offset-c2});
text-shadow: #{$lay-c2} 0 #{$color-c2};
animation: c2 1s infinite linear alternate-reverse;
}
&:after {
left: #{$offset-c1};
text-shadow: calc(-#{$lay-c1}) 0 #{$color-c1};
animation: c1 2s infinite linear alternate-reverse;
}
}
此时我们已经通过:before
和:after
叠加了相同的元素并且一个设置蓝色一个设置红色,让故障效果更真实!
CSS
实现扭曲效果
上述的效果已经非常贴近我们传统意义上理解的Glitch
效果了,但是还差了一点就是通常表现为图像的一部分或整体闪烁、抖动、扭曲、重叠或变形
中的扭曲
和变形
,碰巧的是CSS
实现这个效果非常容易,来看看~
skewX()
函数定义了一个转换,该转换将元素倾斜到二维平面上的水平方向。它的结果是一个<transform-function>
数据类型。
Cool~
最后一块拼图也被补上了~~
@keyframes is-off {
0%, 50%, 80%, 85% {
opacity: 1;
}
56%, 57%, 81%, 84% {
opacity: 0;
}
58% {
opacity: 1;
}
71%, 73% {
transform: scaleY(1) skewX(0deg);
}
72% {
transform: scaleY(3) skewX(-60deg);
}
91%, 93% {
transform: scaleX(1) scaleY(1) skewX(0deg);
color: $txt-color;
}
92% {
transform: scaleX(1.5) scaleY(0.2) skewX(80deg);
color: green;
}
}
来看看完整的效果和代码吧!
结语
春风若有怜花意,可否许我再少年。
感谢
Glitch Clock
来源:juejin.cn/post/7355302255409184807
解决小程序web-view两个恶心问题
1.web-view覆盖层问题
问题由来
web-view
是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。
所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。
解决办法
web-view内部使用cover-view
,调整cover-view的样式即可覆盖在web-view上。
cover-view
覆盖在原生组件上的文本视图。
app-vue和小程序框架,渲染引擎是webview的。但为了优化体验,部分组件如map、video、textarea、canvas通过原生控件实现,原生组件层级高于前端组件(类似flash层级高于div)。为了能正常覆盖原生组件,设计了cover-view。
支持的平台:
App | H5 | 微信小程序 | 支付宝小程序 | 百度小程序 |
---|
具体实现
<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
</view>
</template>
.close-view{
position: fixed;
z-index: 99999;
top: 30rpx;
left: 45vw;
.close-icon{
width: 100rpx;
height: 80rpx;
}
}
代码说明:
这里的案例是一个关闭按钮图标悬浮在webview上,点击图标可以关闭当前预览的webview。
注意
仅仅真机上才生效,开发者工具上是看不到效果的,如果要调整覆盖层的样式,可以先把web-view标签注释了,写完样式没问题再释放web-view标签。
2.web-view导航栏返回
问题由来
- 小程序端 web-view 组件一定有原生导航栏,下面一定是全屏的 web-view 组件,navigationStyle: custom 对 web-view 组件无效。
场景
用户在嵌套的webview里填写表单,不小心按到导航栏的返回了,就全没了。
解决办法
使用page-container容器,点击到返回的时候,给个提示。
page-container
页面容器。
小程序如果在页面内进行复杂的界面设计(如在页面内弹出半屏的弹窗、在页面内加载一个全屏的子页面等),用户进行返回操作会直接离开当前页面,不符合用户预期,预期应为关闭当前弹出的组件。 为此提供“假页”容器组件,效果类似于 popup
弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。返回操作包括三种情形,右滑手势、安卓物理返回键和调用 navigateBack
接口。
具体实现
<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
<!--这里这里,就这一句-->
<page-container :show="isShow" :overlay="false" @beforeleave="beforeleave"></page-container>
</view>
</template>
export default {
data() {
return {
isShow: true
}
},
methods: {
beforeleave(){
this.isShow = false
uni.showToast({
title: '别点这里',
icon: 'none',
duration: 3000
})
}
}
}
结语
算是小完美的解决了吧,这里记录一下,看看就行,勿喷。
连夜更新安卓cover-view失效问题
由于之前一直用ios测试的,今晚才发现这个问题
解决办法
cover-view, cover-image{
visibility: visible!important;
z-index: 99999;
}
继续连夜更新cover-view在安卓上的问题
如果cover-view的展示是通过v-if控制的,后续通过v-if显示时会出现问题
解决方案
将v-if换成v-show,一换一个不吱声,必然好使!
来源:juejin.cn/post/7379960023407198220