如何开发一个chrome 扩展
前言
最近开发一个涉及到很多颜色转换的工作,每次都搜索打开一个新页面在线转换,十分麻烦,于是想着开发一个颜色转换的浏览器插件,每次点击即可使用。
查看Chrome插件开发的文档developer.chrome.com/docs/extens… ,从头开始开发一个插件还是比较麻烦且原始的。搜索网上资料,发现了2个工具
- CRXJS: github.com/crxjs/chrom…
- Plasmo: github.com/PlasmoHQ/pl…
- WXT: github.com/wxt-dev/wxt
最后选择了WXT,因为它用起来更方便,且支持多浏览器。
WXT是什么
WXT号称下一代浏览器扩展开发框架
,免费、开源、易用且支持多种浏览器。
这段文字是关于WXT框架的介绍,它是一个用于构建浏览器扩展的开源框架。下面是对文中提到的几个关键点的解释:
- WXT有自己一套约定的框架,为开发者提供了一套标准化的做法,有助于保持项目的一致性,使得新手能够更容易地理解和接手项目。
- 基于项目文件结构自动生成manifest。manifest是浏览器扩展的配置文件,定义了扩展的名称、版本、权限等信息。WXT框架能够根据项目结构自动创建这个文件,简化开发过程。
- 单文件配置Entrypoint,比如背景脚本或内容脚本,这样可以更直观地管理和维护代码。
- WXT提供了开箱即用的TypeScript支持,并且改进了浏览器API的类型定义。TypeScript是一种强类型语言,它在JavaScript的基础上增加了类型系统,有助于在开发过程中捕捉到潜在的错误。
- 输出文件的路径最小化,这意味着WXT在构建扩展时会优化文件路径,减少runtime的path长度,可以提高扩展的加载速度和性能。
WXT 安装&开发
我们直接脚手架开一个项目
pnpm dlx wxt@latest init wxt-demo
cd wxt-demo
pnpm install
套用官网的一个图
不过我选的react,生成的工作目录文件如下
调试运行pnpm dev
,WXT直接开了一个无头浏览器,可以实时看到效果
颜色转换开发
因为我的需求比较简单,实现各种颜色的转换,页面UI就直接使用antd,样式直接Inline。代码如下:
<>
<header style={pageLayoutStyle}>
<p style={{ fontSize: '1.5rem', textAlign: 'center', fontWeight: 'medium' }}>Color Converter</p>
<p>This tool helps you convert colors between different color formats.</p>
</header>
<main style={pageLayoutStyle}>
<div>
<p style={{ fontSize: '1.2rem', textAlign: 'left' }}>Enter a color:</p>
</div>
<Input
suffix={
<ColorPicker
defaultValue={defaultColor}
value={hex === '' ? defaultColor : hex}
styles={{ popupOverlayInner: { position: 'absolute', left: '50%', transform: 'translate(-100%, -50%)' } }}
onChangeComplete={(color) => {
const str = (color.toRgbString())
}} />
}
placeholder={defaultColor}
autoFocus={true}
onChange={(e) => {
const str = (e.target.value)
}} />
<div>
<p style={{ fontSize: '1.2rem', textAlign: 'left' }}>Results</p>
</div>
{contextHolder}
<Input addonBefore="RGB" value={rgb} suffix={<CopyOutlined onClick={() => { copyToClipboard(rgb) }} />} readOnly={true} defaultValue="" />
<Input addonBefore="HEX" value={hex} suffix={<CopyOutlined onClick={() => { copyToClipboard(hex) }} />} readOnly={true} defaultValue="" style={{ marginTop: '8px' }} />
<Input addonBefore="HSL" value={hsl} suffix={<CopyOutlined onClick={() => { copyToClipboard(hsl) }} />} readOnly={true} defaultValue="" style={{ marginTop: '8px' }} />
<Input addonBefore="HSV" value={hsv} suffix={<CopyOutlined onClick={() => { copyToClipboard(hsv) }} />} readOnly={true} defaultValue="" style={{ marginTop: '8px' }} />
<Input addonBefore="CMYK" value={cmyk} suffix={<CopyOutlined onClick={() => { copyToClipboard(cmyk) }} />} readOnly={true} defaultValue="" style={{ marginTop: '8px' }} />
</main>
</>
新建color.ts,定义几个变量名称和方法名称,一路按tab,AI自动补全了代码。代码太长就不贴出来了,主要是颜色的正则匹配和转换。同上面UI绑定后,最终实现效果如下:
在调试firefox时,遇到一个小坑:content.ts中需要至少有一个匹配matches,否则会直接退出提示插件invalid。
发布
WXT发布也比较简单,直接运行 pnpm zip
就会构建chrome的扩展压缩包,发布firefox只需要pnpm zip:firefox
。在ouput目录下就会生成对应产物。
不过记得在打包前修改wxt.config.ts,添加名称、版本、描述等。如:
export default defineConfig({
modules: ['@wxt-dev/module-react'],
manifest: {
version: '1.0.0',
name: 'color-converter',
description: 'A color converter tool',
}
});
最后完整代码见github: github.com/xckevin/col…
现在插件也已经上架了市场,欢迎下载:
- chrome: chromewebstore.google.com/detail/colo…
- firefox: addons.mozilla.org/en-US/firef…
来源:juejin.cn/post/7425803259443019815
从2s优化到0.1s,我用了这5步
前言
分类树
查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中。
但就是这样一个简单的分类树查询功能,我们却优化了5
次。
到底是怎么回事呢?
苏三的免费刷题网站:http://www.susan.net.cn 里面:面试八股文、BAT面试真题、工作内推、工作经验分享、技术专栏等等什么都有,欢迎收藏和转发。
背景
我们的网站使用了SpringBoot
推荐的模板引擎:Thymeleaf
,进行动态渲染。
它是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的应用开发。
它提供了一个用于整合SpringMVC的可选模块,在应用开发中,我们可以使用Thymeleaf来完全代替JSP或其他模板引擎,如Velocity\FreeMarker等。
前端开发写好Thymeleaf的模板文件,调用后端接口获取数据,进行动态绑定,就能把想要的内容展示给用户。
由于当时这个是从0-1的新项目,为了开快速开发功能,我们第一版接口,直接从数据库中查询分类
数据,组装成分类树
,然后返回给前端。
通过这种方式,简化了数据流程,快速把整个页面功能调通了。
第1次优化
我们将该接口部署到dev环境,刚开始没啥问题。
随着开发人员添加的分类越来越多,很快就暴露出性能瓶颈。
我们不得不做优化了。
我们第一个想到的是:加Redis缓存
。
流程图如下:于是暂时这样优化了一下:
- 用户访问接口获取分类树时,先从Redis中查询数据。
- 如果Redis中有数据,则直接数据。
- 如果Redis中没有数据,则再从数据库中查询数据,拼接成分类树返回。
- 将从数据库中查到的分类树的数据,保存到Redis中,设置过期时间5分钟。
- 将分类树返回给用户。
我们在Redis中定义一个了key,value是一个分类树的json格式转换成了字符串,使用简单的key/value形式保存数据。
经过这样优化之后,dev环境的联调和自测顺利完成了。
第2次优化
我们将这个功能部署到st环境了。
刚开始测试同学没有发现什么问题,但随着后面不断地深入测试,隔一段时间就出现一次首页访问很慢的情况。
于是,我们马上进行了第2次优化。
我们决定使用Job
定期异步
更新分类树到Redis中,在系统上线之前,会先生成一份数据。
当然为了保险起见,防止Redis在哪条突然挂了,之前分类树同步写入Redis的逻辑还是保留。
于是,流程图改成了这样:增加了一个job每隔5分钟执行一次,从数据库中查询分类数据,封装成分类树,更新到Redis缓存中。
其他的流程保持不变。
此外,Redis的过期时间之前设置的5分钟,现在要改成永久。
通过这次优化之后,st环境就没有再出现过分类树查询的性能问题了。
第3次优化
测试了一段时间之后,整个网站的功能快要上线了。
为了保险起见,我们需要对网站首页做一次压力测试。
果然测出问题了,网站首页最大的qps是100多,最后发现是每次都从Redis获取分类树导致的网站首页的性能瓶颈。
我们需要做第3次优化。
该怎么优化呢?
答:加内存缓存。
如果加了内存缓存,就需要考虑数据一致性问题。
内存缓存是保存在服务器节点上的,不同的服务器节点更新的频率可能有点差异,这样可能会导致数据的不一致性。
但分类本身是更新频率比较低的数据,对于用户来说不太敏感,即使在短时间内,用户看到的分类树有些差异,也不会对用户造成太大的影响。
因此,分类树这种业务场景,是可以使用内存缓存的。
于是,我们使用了Spring推荐的caffine
作为内存缓存。
改造后的流程图如下:
- 用户访问接口时改成先从本地缓存分类数查询数据。
- 如果本地缓存有,则直接返回。
- 如果本地缓存没有,则从Redis中查询数据。
- 如果Redis中有数据,则将数据更新到本地缓存中,然后返回数据。
- 如果Redis中也没有数据(说明Redis挂了),则从数据库中查询数据,更新到Redis中(万一Redis恢复了呢),然后更新到本地缓存中,返回返回数据。
需要注意的是,需要改本地缓存设置一个过期时间,这里设置的5分钟,不然的话,没办法获取新的数据。
这样优化之后,再次做网站首页的压力测试,qps提升到了500多,满足上线要求。
最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
添加苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。
第4次优化
之后,这个功能顺利上线了。
使用了很长一段时间没有出现问题。
两年后的某一天,有用户反馈说,网站首页有点慢。
我们排查了一下原因发现,分类树的数据太多了,一次性返回了上万个分类。
原来在系统上线的这两年多的时间内,运营同学在系统后台增加了很多分类。
我们需要做第4次优化。
这时要如何优化呢?
限制分类树的数量?
答:也不太现实,目前这个业务场景就是有这么多分类,不能让用户选择不到他想要的分类吧?
这时我们想到最快的办法是开启nginx
的GZip
功能。
让数据在传输之前,先压缩一下,然后进行传输,在用户浏览器
中,自动解压,将真实的分类树数据展示给用户。
之前调用接口返回的分类树有1MB的大小,优化之后,接口返回的分类树的大小是100Kb,一下子缩小了10倍。
这样简单的优化之后,性能提升了一些。
第5次优化
经过上面优化之后,用户很长一段时间都没有反馈性能问题。
但有一天公司同事在排查Redis中大key的时候,揪出了分类树。之前的分类树使用key/value的结构保存数据的。
我们不得不做第5次优化。
为了优化在Redis中存储数据的大小,我们首先需要对数据进行瘦身。
只保存需要用到的字段。
例如:
@AllArgsConstructor
@Data
public class Category {
private Long id;
private String name;
private Long parentId;
private Date inDate;
private Long inUserId;
private String inUserName;
private List children;
}
像这个分类对象中inDate、inUserId和inUserName字段是可以不用保存的。
修改自动名称。
例如:
@AllArgsConstructor
@Data
public class Category {
/**
* 分类编号
*/
@JsonProperty("i")
private Long id;
/**
* 分类层级
*/
@JsonProperty("l")
private Integer level;
/**
* 分类名称
*/
@JsonProperty("n")
private String name;
/**
* 父分类编号
*/
@JsonProperty("p")
private Long parentId;
/**
* 子分类列表
*/
@JsonProperty("c")
private List children;
}
由于在一万多条数据中,每条数据的字段名称是固定的,他们的重复率太高了。
由此,可以在json序列化时,改成一个简短的名称,以便于返回更少的数据大小。
这还不够,需要对存储的数据做压缩。
之前在Redis中保存的key/value,其中的value是json格式的字符串。
其实RedisTemplate
支持,value保存byte数组
。
先将json字符串数据用GZip
工具类压缩成byte数组,然后保存到Redis中。
再获取数据时,将byte数组转换成json字符串,然后再转换成分类树。
这样优化之后,保存到Redis中的分类树的数据大小,一下子减少了10倍,Redis的大key问题被解决了。
性能优化问题,无论在面试,还是工作中,都会经常遇到。
来源:juejin.cn/post/7425382886297600050
课表拖拽(一)拖拽实现
最近接到一个任务,要求实现一个课表拖拽的功能,支持快速修改个人日程时间。项目采用taro框架。
基于性能的抉择
container采用grid布局,7colum+12row,共84个单元格
拖拽的方式有两种
盒子跟随手指,并实时显示松手后落入的位置,松手时寻找一个离手指最近的单元格放入
盒子实时在格子之内,根据手指位置实时计算填入的格子,将盒子放入
哪一种性能更高??????
显然第一种方案在第二种方案上多了盒子实时跟随手指这个额外操作,性能不占优势。
catch-move避免滑动穿透
因为课表支持左右滑动查看自己每一周的课程安排,采用了一个Swiper
包裹在container之外,在滑动时会带动Swiper的滑动,那该怎么办?????????
不妨请教学长,经过学长的指导,告诉了我一个api
不得不感慨阅读官方文档的重要性(老实了,以后必须多看官方文档)
如何根据手指的位置,计算所在单元格
const unitwith = 350 / 7;
const unitheight = 600 / 12;
先得到了每一个单元格的宽高
然后通过滑动的事件对象可以获取当前的(x,y),那么动态设置grid样式就可以实现
const getGridPositionByXY = (xp, yp) => {
return `gridColumn:${Math.floor(xp / unitwith)} ;gridRow:${Math.floor(
yp / unitheight
)}/${Math.floor(yp / unitheight) + 2}`;
};
handleTouchMove函数的实现
我们需要两个响应式的变量x,y
,通过在handleTouchMove
函数中修改x,y
来带动style的修改
const [x, setX] = useState(350 / 7);
const [y, setY] = useState(600 / 12);
const handleTouchMove =(e) => {
setX(e.changedTouches[0].clientX + 50);
setY(e.changedTouches[0].clientY + 50);
};
性能优化(节流)
节流:在一定时间内,无论函数被触发多少次,函数只会在固定的时间间隔内执行一次
为防止handleTouchMove
的触发频率太高,我们采用节流函数来让它在固定时间内只执行一次
function Throttle(fn, delay) {
let timer = null;
return function () {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
};
}
const handleTouchMove = Throttle((e) => {
setX(e.changedTouches[0].clientX + 50);
setY(e.changedTouches[0].clientY + 50);
}, 10);
提升交互性
我们可以让用户长按激活,随后才能滑动,并且在激活的时候触发震动
直接贴完整代码在这里
import { View, Swiper, SwiperItem } from "@tarojs/components";
import { useState, useRef } from "react";
import Taro, { useLoad } from "@tarojs/taro";
import "./index.css";
function Throttle(fn, delay) {
let timer = null;
return function () {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
};
}
export default function Index() {
const [isLongPress, setIsLongPress] = useState(false);
useLoad(() => {
console.log("Page loaded.");
});
const timer = useRef(null);
const [x, setX] = useState(350 / 7);
const [y, setY] = useState(600 / 12);
// const [StartPosition, setStartPosition] = useState({ x: 0, y: 0 });
const unitwith = 350 / 7;
const unitheight = 600 / 12;
const getGridPositionByXY = (xp, yp) => {
return `gridColumn:${Math.floor(xp / unitwith)} ;gridRow:${Math.floor(
yp / unitheight
)}/${Math.floor(yp / unitheight) + 2}`;
};
const handleTouchMove = Throttle((e) => {
if (!isLongPress) return;
setX(e.changedTouches[0].clientX + 50);
setY(e.changedTouches[0].clientY + 50);
}, 10);
return (
<View className='index'>
<Swiper circular style={{ width: "100vw", height: "100vh" }}>
<SwiperItem>
<view className='container'>
<view
style={getGridPositionByXY(x, y)}
className={`items-1 ${isLongPress ? "pressActive" : ""}`}
catch-move
onTouchStart={() => {
timer.current = setTimeout(() => {
setIsLongPress(true);
Taro.vibrateShort();
// console.log("长按");
}, 1000);
}}
onTouchMove={handleTouchMove}
onTouchEnd={() => {
clearTimeout(timer.current);
setIsLongPress(false);
}}
></view>
<view className="items-2">2</view>
</view>
</SwiperItem>
<SwiperItem>
<view className="container">
<view className="items-1">no</view>
</view>
</SwiperItem>
<SwiperItem>
<view className="container">
<view className="items-1" catch-move></view>
</view>
</SwiperItem>
<SwiperItem>
<view className="container">
<view className="items-1" catch-move></view>
</view>
</SwiperItem>
<SwiperItem>
<view className="container">
<view className="items-1" catch-move></view>
</view>
</SwiperItem>
</Swiper>
</View>
);
}
//index.css
.container {
width: 700px;
height: 1200px;
background-color: #ccc;
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: repeat(12, 1fr);
}
.griditems-1 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: cadetblue;
}
.griditems-2 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: aquamarine;
}
.griditems-3 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: burlywood;
}
.griditems-4 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: darkcyan;
}
.griditems-5 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: darkgoldenrod;
}
.items-1 {
grid-column: 1; /* 从第1列开始,到第2列结束 */
grid-row: 1 / 4;
border-radius: 10px;
border: 1px solid #fff;
background-color: burlywood;
}
.items-2 {
grid-column: 3;
grid-row: 1 / 4;
border-radius: 10px;
border: 1px solid #fff;
background-color: burlywood;
}
.pressActive {
border-radius: 10px;
border: 1px solid #fff;
background-color: #fff;
opacity: 0.5;
}
下一期将介绍如何控制方块不重合,以及在展开后方块的处理和对多方块的情况怎么单独管理每一个方块的情况
来源:juejin.cn/post/7425562027412815882
车机系统与Android的关系
前言:搞懂 Android 系统和汽车到底有什么关系。
一、基本概念
1、Android Auto
1)是什么
- Android Atuo 是一个 Android 端的 app,专门为驾驶环境设计的;
- 运行环境:需要在 Android 5.0 或者更高版本的系统,并且需要 Google 地图和 Google Play 音乐应用;
2)功能
- Android Atuo 可以用来将 Android 设备上的部分功能映射到汽车屏幕上;
- 满足了很多人在开车时会使用手机的需求;
2、Google Assistant
- Google 将 GoofleAssistant 集成到 AndroidAuto 中;
- 交互方式有键盘、触摸、语音等;
- 对于汽车来说,语音无疑是比触摸更好的交互方式;
- 在驾驶环境中,语音交换存在的优势
- 用户不改变自身的物理姿势,这种交互方式不影响驾驶员对驾驶的操作;
- 有需要多次触摸的交互时,可能只需要一条语音就可以完成;
- 语音交互不存在入口的层次嵌套,数据更加扁平;
- 优秀的语音系统可以利用对话的上下文完成任务,避免用户重复输入;
3、Android Automotive
1、Android Auto 和 Android Automotive 的区别
- Android Auto 是以手机为中心的
- 好处:数据和应用始终是一致的,不存在需要数据同步的问题,手机上装的软件和已有数据,接到汽车上就直接有了;
- 坏处:每次都需要拿出手机,汽车只是作为手机的一个外设;这种模式不便于对于汽车本身的控制和相关数据的获取;
- Android Automotive
- 如果将系统直接内置于汽车中,会大大提升用户体验;
- Android Automotive 就是面向这个方向进行设计的;
- 一旦将系统内置于汽车,可以完成的功能就会大大增加;例如,直接在中控触摸屏上调整座椅和空调;同时,系统也能获取更多关于汽车的信息,例如:油耗水平、刹车使用等;
加两张中控和仪表的图片
4、App
1)App 的开发
- Android Auto 目前仅支持两类第三方应用
- 音频应用:允许用户浏览和播放汽车中的音乐和语音内容;
- 消息应用:通过 text-to-speech 朗读消息并通过语音输入回复消息;
2)App 的设计
- Google 专门为 Android Auto 上的 UI 设计做了一个指导网站:Auto UI guidelines;
- 基本指导原则(车机交互系统的借鉴)
- Android Auto 上的互动步调必须由驾驶员控制;
- 汽车界面上的触摸目标必须足够大,以便可以轻松地浏览和点击;
- 适当的私彩对比可以帮助驾驶员快速解读信息并做出决定;
- 应用必须支持夜间模式,因为过高的强度可能会干扰注意力;
- Roboto 字体在整个系统中用于保持一致性并帮助提高可读性;
- 通过触摸来进行分页应用用来作为滑动翻页的补充;
- 有节制地使用动画来描述两个状态间的变化;
二、源码和架构
1、Android Automative的整体架构
- Android Automative 的源码包含在 AOSP 中;
- Android Automative 是在原先 Android的 系统架构上增加了一些与车相关的(图中虚线框中绿色背景的)模块;
- Car App:包括 OEM 和第三方开发的 App;
- OEM:就是汽车厂商利用自身掌握的核心技术负责设计和开发新产品,而具体的生产制造任务则通过合同订购的方式委托给同类产品的其他厂家进行,最终产品会贴上汽车厂商自己的品牌商标。这种生产方式被称为定牌生产合作,俗称“贴牌”。承接这种加工任务的制造商就被称为OEM厂商,其生产的产品就是OEM产品;
- Car API:提供给汽车 App 特有的接口;
- Car Service:系统中与车相关的服务;
- Vehicle Network Service:汽车的网络服务;
- Vehicle HAL:汽车的硬件抽象层描述;
- Car App:包括 OEM 和第三方开发的 App;
1)Car App
- /car_product/build/car.mk 这个文件中列出了汽车系统中专有的模块;
- 列表中,首字母大写的模块基本上都是汽车系统中专有的 App;
- App的源码都位于 /platform/packages/services/Car/ 目录下
# Automotive specific packages
PRODUCT_PACKAGES += \
vehicle_monitor_service \
CarService \
CarTrustAgentService \
CarDialerApp \
CarRadioApp \
OverviewApp \
CarLensPickerApp \
LocalMediaPlayer \
CarMediaApp \
CarMessengerApp \
CarHvacApp \
CarMapsPlaceholder \
CarLatinIME \
CarUsbHandler \
android.car \
libvehiclemonitor-native \
2)Car API
- 开发汽车专有的App自然需要专有的API;
- 这些API对于其他平台(例如手机和平板)通常是没有意义的;
- 所以这些API没有包含在Android Framework SDK中;
- 下图列出了所有的 Car API;
- android.car:包含了与车相关的基本API。例如:车辆后视镜,门,座位,窗口等。
- cabin:座舱相关API。
- hvac:通风空调相关API。(hvac是Heating, ventilation and air conditioning的缩写)
- property:属性相关API。
- radio:收音机相关API。
- pm:应用包相关API。
- render:渲染相关API。
- menu:车辆应用菜单相关API。
- annotation:包含了两个注解。
- app
- cluster:仪表盘相关API。
- content
- diagnostic:包含与汽车诊断相关的API。
- hardware:车辆硬件相关API。
- input:输入相关API。
- media:多媒体相关API。
- navigation:导航相关API。
- settings:设置相关API。
- vms:汽车监测相关API。
3)Car Service
- Car Service并非一个服务,而是一系列的服务。这些服务都在ICarImpl.java构造函数中列了出来;
public ICarImpl(Context serviceContext, IVehicle vehicle, SystemInterface systemInterface,
CanBusErrorNotifier errorNotifier) {
mContext = serviceContext;
mHal = new VehicleHal(vehicle);
mSystemActivityMonitoringService = new SystemActivityMonitoringService(serviceContext);
mCarPowerManagementService = new CarPowerManagementService(
mHal.getPowerHal(), systemInterface);
mCarSensorService = new CarSensorService(serviceContext, mHal.getSensorHal());
mCarPackageManagerService = new CarPackageManagerService(serviceContext, mCarSensorService,
mSystemActivityMonitoringService);
mCarInputService = new CarInputService(serviceContext, mHal.getInputHal());
mCarProjectionService = new CarProjectionService(serviceContext, mCarInputService);
mGarageModeService = new GarageModeService(mContext, mCarPowerManagementService);
mCarInfoService = new CarInfoService(serviceContext, mHal.getInfoHal());
mAppFocusService = new AppFocusService(serviceContext, mSystemActivityMonitoringService);
mCarAudioService = new CarAudioService(serviceContext, mHal.getAudioHal(),
mCarInputService, errorNotifier);
mCarCabinService = new CarCabinService(serviceContext, mHal.getCabinHal());
mCarHvacService = new CarHvacService(serviceContext, mHal.getHvacHal());
mCarRadioService = new CarRadioService(serviceContext, mHal.getRadioHal());
mCarNightService = new CarNightService(serviceContext, mCarSensorService);
mInstrumentClusterService = new InstrumentClusterService(serviceContext,
mAppFocusService, mCarInputService);
mSystemStateControllerService = new SystemStateControllerService(serviceContext,
mCarPowerManagementService, mCarAudioService, this);
mCarVendorExtensionService = new CarVendorExtensionService(serviceContext,
mHal.getVendorExtensionHal());
mPerUserCarServiceHelper = new PerUserCarServiceHelper(serviceContext);
mCarBluetoothService = new CarBluetoothService(serviceContext, mCarCabinService,
mCarSensorService, mPerUserCarServiceHelper);
if (FeatureConfiguration.ENABLE_VEHICLE_MAP_SERVICE) {
mVmsSubscriberService = new VmsSubscriberService(serviceContext, mHal.getVmsHal());
mVmsPublisherService = new VmsPublisherService(serviceContext, mHal.getVmsHal());
}
mCarDiagnosticService = new CarDiagnosticService(serviceContext, mHal.getDiagnosticHal());
4)Car Tool
a、VMS
- VMS全称是Vehicle Monitor Service。正如其名称所示,这个服务用来监测其他进程;
- 在运行时,这个服务是一个独立的进程,在init.car.rc中有关于它的配置
service vms /system/bin/vehicle_monitor_service
class core
user root
group root
critical
on boot
start vms
- 这是一个Binder服务,并提供了C++和Java的Binder接口用来供其他模块使用;
来源:juejin.cn/post/7356981730765291558
离了大谱,和HR互怼后被开了!
世界之大无奇不有,不靠谱的人见多了,但是不靠谱的公司还是第一次见。
今天故事的主角是某上市公司,其号称为中国电声行业的龙头企业,名字这里就不说了。
故事发生的背景是某大学生秋招找工作,投递和面试的是这家公司的嵌入式开发,但最终却被分到了 IT 部门(猜测应该是 IT 支持岗)。
于是这位哥们儿不服,就找到了这家公司的 HR,但却没想到被 HR 怒怼,并被质问“你配做嵌入式开发吗?”、“我可没有你这么闲”等极具人身攻击的词汇,以下是聊天截图:
当然,故事的结局也大快人心,这哥们儿把他和 HR 的天截图反馈给了官方,于是不出意外,这位 HR 很就被光速开除了(可以看出满满的求生欲),如下图所示:
人在做天在看,不是不报时候未到。打工人何必为难打工人呢?这下好了,小伙子的事情解决了,HR 可倒好,还得重新找工作。
但博主在评论区看到这家公司随意更改应聘者的岗位也是常规操作了,有个哥们儿说他投入的是算法但被直接干到行政去了,离了大谱:
合着找工作这件事,也能像报考大学的志愿一样,上不了某个专业,还可以滑到另一个专业?这也是让我开眼了,大家怎么看?欢迎评论区讨论留言。
本文已收录到我的面试小站 http://www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。
来源:juejin.cn/post/7426315840222593062
第三届OpenHarmony技术大会应用生态实践分论坛成功举办
开放、兼容、安全是OpenAtom OpenHarmony(以下简称“OpenHarmony”)生态蓬勃发展的重要要素。随着用户需求的多元化,对应用生态的需求迫切增加。2024年10月12日,第三届OpenHarmony技术大会的应用生态实践分论坛在上海世博中心举行,来自不同领域的技术专家聚焦于通过应用开发适配的实战经验,剖析OpenHarmony系统特性,为应用的开发和迁移提供经验和参考。与会嘉宾的分享内容涵盖了OpenHarmony应用开发的多个方面。话题覆盖了从不同应用的OpenHarmony开发经验,到高校应用开发实践案例再到跨平台框架的实践经验和项目优化经验等多个关键主题。通过典型的实战经验分享,分析系统的能力和特性,帮助开发者了解应用开发的最新技术和实践,共同推进应用生态的技术共建、生态共享。
OpenHarmony社区应用工作组组长(代)闫诗文、社区代码共建组组长林志南等专业人士担任出品人,OpenHarmony社区应用工作组运营王霞为分论坛主持人。演讲嘉宾包括:华为三方库和跨平台系统架构师潘锦玲、企查查科技股份有限公司技术专家汤嘉琪、武汉初心科技有限公司(石墨文档)技术总监饶欣、北京风行在线技术有限公司高级技术专家韩超、中国科学院软件研究所高级工程师郑森文、江苏润开鸿数字科技有限公司技术专家徐建国、上海交通大学副教授吴明瑜、华为 ArkUI - X 跨平台系统架构师刘龙、深圳开鸿数字产业发展有限公司 OS 框架开发工程师宫跃纪、华为开源技术专家王晔晖、华为 OpenHarmony 跨平台框架专家高迪。
作为开场嘉宾,华为的三方库和跨平台系统架构师潘锦玲详细介绍了OpenHarmony开源三方库和跨平台框架的技术分类、技术地图以及OpenHarmony化进程等内容,带领与会者对应用三方库和跨平台框架进行了全面了解。
(华为三方库和跨平台系统架构师潘锦玲)
企查查科技股份有限公司的技术专家汤嘉琪在分论坛上分享了“企查查HarmonyOS Next应用适配实战”。在适配过程中,企查查面对了一系列技术挑战。汤嘉琪深入剖析在适配中遇到的技术难题及应对方案,分享了企查查在HarmonyOS Next适配过程中的最新进展与架构设计,讨论实际适配经验以及新系统带来的业务生态创新体验。企查查通过与华为的紧密合作,成功成为垂直领域首家通过KCP7准出标准的企业,并成功在HarmonyOS Next上完成商用级还原。
(企查查科技股份有限公司的技术专家汤嘉琪)
武汉初心科技有限公司(石墨文档)的技术总监饶欣,在分论坛上分享了“石墨文档基于 OpenHarmony 端云一体实践”。饶欣介绍了石墨文档如何利用OpenHarmony操作系统的能力及特性,设计并实现了端云一体化的Office办公软件。他详细阐述了端云一体化的通讯机制、安全机制,以及零注入无感云端站点客户端集成方案,展示了石墨文档在提升OpenHarmony应用开发效率方面的创新实践。
(武汉初心科技有限公司(石墨文档)技术总监饶欣)
大屏端视频用户对于直观流畅操作体验有强烈的需求。北京风行在线技术有限公司的高级技术专家韩超分享了“橙瓣-风行大屏视频产品在OpenHarmony中的开发实践”,讲述了风行如何依托自身在长视频领域的资源积累,率先完成大屏端视频类应用的OpenHarmony化。韩超详细介绍了在具体实践中,利用OpenHarmony的技术特性实现高效开发,以及基于ArkTS Gird布局特性,快速实现瀑布流各类基础行列模版中的应用,这些实践显著节省了开发时间和运营成本。通过ArkUI的动态化加载能力,为用户带来流畅的大屏操作体验。
(北京风行在线技术有限公司的高级技术专家韩超)
“OpenHarmony作为智能终端操作系统,面向RISC-V的应用生态支持以及多端应用生态环境将会是万物智联蓬勃发展的土壤,这需要南北向的开发者一起倾力打造。”中国科学院软件研究所的OpenHarmony项目群工作委员会委员、高级工程师郑森文探讨了“面向RISC-V的OpenHarmony多端应用生态及挑战”。郑森文指出,OpenHarmony对RISC-V架构的应用生态支持至关重要,分享了将ARM平台开发的OpenHarmony应用迁移到RISC-V架构上的策略。同时,也展示了中国科学院软件研究所在PC端形态上的应用需求研究和取得的成果。
(中国科学院软件研究所的OpenHarmony项目群工作委员会委员、高级工程师郑森文)
江苏润开鸿数字科技有限公司的技术专家徐建国,在分论坛上介绍了“思否社区应用开发实践”。徐建国分享了思否社区作为开发者交流平台,在OpenHarmony系统上的应用开发和适配经验。徐建国详细阐述了在构建高性能的MarkDown插件、封装第三方极验包、构建高性能UI、状态管理等方面的实践,以及这些实践如何助力同类社区完成OpenHarmony迁移。徐建国的分享为开发者提供了实用的技术参考和经验分享。
(江苏润开鸿数字科技有限公司的技术专家徐建国)
上海交通大学副教授吴明瑜,在分论坛上分享了“基于OpenHarmony的智能制造软件栈构建实践与探索”。吴明瑜阐述了OpenHarmony在智能制造领域的潜力,展示了其团队如何在OpenHarmony上构建软件化控制系统,实现控制逻辑的弹性部署和更新。他详细介绍了如何通过与云侧能力的整合,实现对制造现场的远程监控,以及这些研究成果如何在真实产线中部署,为智能制造系统提供经验和参考。
(上海交通大学副教授吴明瑜)
华为ArkUI-X跨平台架构系统师刘龙则介绍了“OpenHarmony跨平台框架ArkUI-X实践及思考”。 刘龙讲述了ArkUI-X跨平台框架的工作原理,并分享了跨平台应用开发的前沿佳实践,强调码多平台开发降本增效。
(华为ArkUI-X跨平台架构系统师刘龙)
深圳开鸿数字产业发展有限公司OS框架开发工程师宫跃纪的演讲主题为“OpenHarmony应用性能分析优化项目实践”。 宫跃纪分享了如何从系统层性能调度优化到应用层UI渲染、模块动态加载等优化方案,并结合实际案例分享实践经验。
(深圳开鸿数字产业发展有限公司OS框架开发工程师宫跃纪)
OpenHarmony北向生态面临适配大量安全、先进的第三方开源软件挑战。华为开源技术专家王晔晖认为,这需要构建可持续的管理机制与能力,构筑高价值第三方开源软件供应链。王晔晖介绍了“应用三方库和跨平台框架治理平台”,详细阐述了如何通过构建OpenHarmony应用三方库与跨平台框架(TPC)发行版及治理平台,形成TPC选型、孵化、发行版维护工程落地,通过持续呈现与跟踪、度量TPC平台上的多维度信息,能给持续提升开发者体验和生态繁荣。
(华为开源技术专家王晔晖)
本次分论坛的最后,华为OpenHarmony跨平台框架专家高迪分享了关于Flutter Impeller-Vulkan OpenHarmony化方案和性能优化实践的内容。高迪认为Impeller渲染引擎是Flutter保持优势竞争力的核心组件,高迪深入解析了将Impeller渲染引擎应用到OpenHarmony系统中所面临的跨平台兼容性和性能挑战,并分享了相应的优化方案和实践经验。
(华为OpenHarmony跨平台框架专家高迪)
通过本次分论坛的内容分享,专家们对OpenHarmony应用生态的发展做了总结和展望,大家一致认为OpenHarmony的应用生态在技术共建、生态共享方面将迎来更大的发展机遇,OpenHarmony已经在多个领域展现出强大的潜力,开源社区和企业的紧密合作是OpenHarmony生态建设的关键,这种跨界合作不仅增强了OpenHarmony的应用能力,实现了应用生态的蓬勃发展,而且为整个产业链创造了更多的创新机会。
收起阅读 »第三届OpenHarmony技术大会通信互联分论坛圆满举行
长期以来,通信互联行业面临着包括设备兼容性差、缺乏统一的通信标准、数据安全和隐私保护问题、设备管理和维护成,高等挑战。随着物联网设备数量的增加,如何实现高效、稳定的设备间通信,成为行业研究重要方向。OpenAtom OpenHarmony(以下简称“OpenHarmony”)作为开源的操作系统,带来了一种全新的技术架构和解决方案,其统一互联的技术底座为设备间的无缝协作提供了可能。
2024年10月12日,第三届OpenHarmony技术大会通信互联分论坛在上海世博中心举行。该分论坛主要面向OpenHarmony设备开发者、解决方案合作伙伴、设备集成商以及高校和科研机构。分论坛活动从下午2点持续到5点,与会嘉宾就OpenHarmony的通信互联技术进行了全面的分享与讨论。聚焦于探讨社区伙伴如何基于OpenHarmony统一互联构建创新性的行业实践,主题内容涵盖了从基础协议到实际应用的多个层面,旨在推动技术的进一步发展和行业的应用实践。
分论坛由OpenHarmony通信互联TSG主任、华为OpenHarmony网络协议首席架构师李杰和OpenHarmony统一互联PMC(筹)负责人吕鑫担任出品人,并在分论坛进行内容分享。
OpenHarmony项目群工作委员会执行主席、华为终端BG软件部副总裁柳晓见,西安交通大学OpenHarmony技术俱乐部主任、副教授李昊,上海海思解决方案首席架构师姚亚群,中国移动杭州研究院家庭IoT产品部副总经理施超,中国科学院软件研究所高级工程师陈美汝,鸿湖万联(江苏)科技发展有限公司高级架构师韩琰,深圳开鸿数字产业发展有限公司高级工程师张芳舵,江苏润开鸿数字科技有限公司研发总监张勇赛,湖南开鸿智谷数字产业发展有限公司研发中心产品研发部部长蔡志刚,深圳华龙讯达信息技术股份有限公司交互技术研究所所长张喜权,华为分享技术专家王春风等13位嘉宾出席活动并发表演讲。
OpenHarmony项目群工作委员会执行主席、华为终端BG软件部副总裁柳晓见带来以《OpenHarmony统一互联打造万物互联的智能世界》为主题的开篇致辞,柳晓见强调了OpenHarmony在构建智能世界中的关键作用,以及通过统一互联技术实现设备间的无缝协作的重要意义。柳晓见指出,OpenHarmony正加速成为千行百业的数字底座,技术架构领先,打造先进智能终端操作系统体验,基于OpenHarmony的商用发行版和商用设备覆盖千行百业,已成为智能终端操作系统根社区,OpenHarmony设备统一互联标准为统一生态助力,面向未来,OpenHarmony将赋能更多终端形态,基于OpenHarmony统一互联打造智能世界。
(OpenHarmony项目群工作委员会执行主席、华为终端BG软件部副总裁柳晓见发言)
要构建一个万物互联的智能世界,就要降低不同设备间的通信协议,降低信息交互的门槛。华为2012实验室专家、OpenHarmony通信互联TSG主任和华为OpenHarmony网络协议首席架构师李杰,详细介绍了“OpenHarmony下一代通信互联技术演进以及通途极简协议构建策略”。李杰探讨了通途极简协议如何为OpenHarmony通信互联技术底座提供支持,以及如何面对无线高抖动、终端平台能力差异和功耗约束等挑战以及无限通信标准向更高带宽、极致低时延、高可靠等方面的演进趋势,基于OpenHarmony统一互联社区开源部分协议和竞争力,赋能社区伙伴更高效的实现通信互联。
(华为2012实验室专家、OpenHarmony通信互联TSG主任和
华为OpenHarmony网络协议首席架构师李杰发言)
在OpenHarmony 5.0版本之前,即使通过XTS认证,但跨厂商OH设备之间不互通,HOS设备和OH设备系统不互通,这成为有共同连接诉求的伙伴之间的阻碍。在此背景下,OpenHarmony统一互联PMC应运而生。OpenHarmony统一互联PMC(筹)负责人吕鑫介绍了OpenHarmony统一互联PMC建立初衷、共建进展并阐释了建设目标。从启动孵化共建项目,聚焦OH和HOS之间,以及OH不同设备厂家之间设备的互联互通,包括富对瘦设备控制、富对富投屏、富对富文件互传这3大类应用场景,到继续丰富拓展到业务接续、分布式摄像头等场景,后续逐步扩展到跟三方OS之间的互联互通,OpenHarmony统一互联PMC致力于打造一个坚实的互联互通互操作的数字底座,通过共建共享共成长的方式,夯实联接能力,丰富应用场景,实现设备间的万物互联。
(OpenHarmony统一互联PMC(筹)负责人吕鑫发言)
西安交通大学OpenHarmony技术俱乐部的主任、副教授李昊介绍了“面向多模态场景的可编程网络协议栈技术”。李昊对比了创新可编程协议栈技术相比传统网络协议栈在多样化传输协议和动态协议层结构方面的优势,探讨了全可编程的网络体系结构在满足千行百业对网络灵活性和性能需求方面的重要价值,并阐述了可编程协议栈技术通过高级抽象大幅简化开发流程、提升系统性能,及通过编译器的中间表示层进行优化,在保持高性能的同时,实现灵活扩展,在多变网络环境中具有广泛适用性的突出特点。
(西安交通大学OpenHarmony技术俱乐部的主任、副教授李昊发言)
上海海思分享了基于OpenHarmony统一互联标准构筑智能终端芯片方案底座。上海海思面向“消费电子、智慧家庭、汽车电子”三大场景,打造5+2智能终端解决方案,覆盖音视频和联接等领域。5+2解决方案携手OpenHarmony,从底层软硬芯一体的深度优化,并基于系统软总线实现跨终端场景的媒体处理、感知能力共享,带来星闪指向遥控、穿戴手表信息流转、分布式内容共享等更便捷的跨设备互联互通的应用场景,并在未来提供更丰富的OpenHarmony生态场景解决方案。
(上海海思解决方案首席架构师发言)
智能家居已成为近年来家装领域的热门话题,一个能够洞悉居住者需求并主动提供服务的家,标志着我们向更智能化的生活方式迈进。中国移动杭州研究院家庭IoT产品部副总经理施超分享了中国移动基于OpenHarmony统一互联打造高品质全屋智能解决方案的创新案例。施超指出,在家庭IoT领域,操作系统的碎片化和互联互通的难题日益凸显。中国移动基于OpenHarmony构建的“移鸿”操作系统,旨在覆盖家庭网络设备、感知设备、娱乐设备、中控设备和算力设备等全量终端,实现家庭算力、存储、网络资源的共享和统一调度。
(中国移动杭州研究院家庭IoT产品部副总经理施超发言)
中国科学院软件研究所高级工程师陈美汝,探讨了“面向OpenHarmony PC形态的统一互联挑战”。陈美汝表示,“在OpenHarmony全场景时代下,统一互联为PC设备带来了新的可能性,同时也面临着硬件异构性和软件生态局限等挑战”。她分享了中国科学院软件研究所如何面向OpenHarmony PC形态打造智慧互联与智能协同生态,为用户提供高效任务处理与无缝操作智能化体验,服务PC形态下的远程协同、办公教育等场景。
(中国科学院软件研究所高级工程师陈美汝发言)
商用液晶显示行业过去一直存在配网过程繁琐、连接标准不统一的问题。鸿湖万联(江苏)科技发展有限公司高级架构师韩琰,介绍了“OpenHarmony统一互联赋能商显行业跨端融合”。韩琰阐述了SwanLinkOS 5商业发行版如何集成统一互联技术底座,实现不同形态设备的连接组网、设备控制、文件互传,赋能商显行业跨端融合,提升用户体验,打通不同设备之间的信息鸿沟,从而实现不同设备之间的高效互联互通。
(鸿湖万联(江苏)科技发展有限公司高级架构师韩琰)
深圳开鸿数字产业发展有限公司的高级工程师张芳舵,分享了“OpenHarmony星闪统一互联赋能燃气行业智慧化控制新篇章”。张芳舵介绍了星闪技术发展历程和性能特点,以及该公司基于OpenHarmony打造的统一物联智能软件商业发行版KaihongOS。据悉,深开鸿面向千行百业提供全量系统化部署“KaihongOS+星闪”技术服务,KaihongOS旨在让设备互联更安全、更智能。
(深圳开鸿数字产业发展有限公司的高级工程师张芳舵发言)
江苏润开鸿数字科技有限公司研发总监张勇赛,介绍了“OpenHarmony统一互联构建能源行业数字底座”。能源行业设备类型多样、厂商众多、通信协议复杂,导致设备互联困难、维护成本高。张勇赛分享了HiHopeOS如何在OpenHarmony统一互联技术基础上,构建能源行业设备互联的整体解决方案,通过增加多协议转换、拖拉组网、统一平台等系统能力,有效解决行业痛点。
(江苏润开鸿数字科技有限公司研发总监张勇赛发言)
湖南开鸿智谷数字产业发展有限公司研发中心产品研发部部长蔡志刚向现场观众进行“分布式相机OpenHarmony统一互联实践”的分享。蔡志刚围绕智慧城市、智慧家庭、智慧园区等商业化场景,基于上海海思不同芯片不同设备,展示了如何通过OpenHarmony的核心软件能力实现互联互通,为未来OpenHarmony生态商业化拓展提供了丰富的案例。
(湖南开鸿智谷数字产业发展有限公司研发中心产品研发部部长蔡志刚发言)
深圳华龙讯达信息技术股份有限公司交互技术研究所所长张喜权,介绍了“轻工行业OpenHarmony统一互联创新应用实践”。张喜权阐述了基于Openharmony工业操作系统和华龙讯达自主可控的工业自动化平台,如何在制造业的智能化应用场景中进行研究和实践。他提到,这一数字工厂解决方案以Openharmony操作系统为核心,整合了先进的智能制造技术,为制造企业提供全面的数字化转型支持。
(深圳华龙讯达信息技术股份有限公司交互技术研究所所长张喜权发言)
作为最后一位登台嘉宾,华为的分享技术专家王春风带来了“OpenHarmony统一互联2.0文件互传规范技术分享”。王春风讨论了OpenHarmony在设备互联互通方面面临的挑战,包括生态大屏、广告牌等与HarmonyOS设备间的文件互传需求。OpenHarmony统一互联文件规范,以及如何基于社区开源部分协议和竞争力,赋能社区伙伴更高效地实现统一互联。
(华为的分享技术专家王春风发言)
第三届OpenHarmony技术大会的通信互联分论坛汇聚了行业专家、学者以及技术从业者,共同探讨了OpenHarmony在通信互联领域的最新进展和未来趋势。论坛中,嘉宾们深度分享了从基础的网络协议到实际的行业应用,再到操作系统的创新实践,内容涵盖了智能终端、家庭IoT、燃气行业、能源行业以及轻工行业的数字化转型。这些分享不仅展示了OpenHarmony技术在多个领域的广泛应用,也反映了其在推动行业创新中的重要作用。
通过深入的讨论和技术展示,分论坛明确了OpenHarmony统一互联技术在解决设备互联互通问题上的巨大潜力。随着技术的不断成熟和应用场景的不断拓展,OpenHarmony已经成为连接不同设备和行业的重要桥梁。随着更多行业伙伴的加入和技术创新的持续推进,OpenHarmony将有望进一步提升其在全场景智能互联中的影响力,为用户带来更加丰富和便捷的智能体验。
分论坛最后,OpenHarmony项目群工作委员会执行主席、华为终端BG软件部副总裁柳晓见为OpenHarmony统一互联非凡伙伴进行了授牌,致谢他们在OpenHarmony统一互联共建中的突出贡献。此次授牌的非凡伙伴包括:深圳开鸿数字产业发展有限公司,鸿湖万联(江苏)科技发展有限公司,湖南开鸿智谷数字产业发展有限公司,鼎桥通信技术有限公司,山东亚华电子股份有限公司,广东九联开鸿科技发展有限公司,深圳鸿信智联数字科技有限公司。
此外,柳晓见还为OpenHarmony统一互联先锋专家授牌,以表达对专家们在OpenHarmony统一互联共建中敢于担责,表现突出,起到先锋模范作用的致谢,此处被授予OpenHarmony统一互联先锋专家包括:刘永保、张国荣、沈春萍、韩琰、陈施、王从鼎、江哲凯、王东东、徐昌、侯乐武、白政锋、喻绍强、李贵、胡孝东、庞敏、王纪睿、王小松、刁月磊、刘浩、张坤、李昊、高伟、张振。(排名不分先后)
收起阅读 »你是否遇到过断网检测的需求?
你也碰到断网检测的需求啦?
一般的断网检测需求,大部分都是在用户网络状态不好或者网络掉线时,我们给用户一个提示或者引导界面,防止用户不知道自己卡了在那里一直等待。
方案1,轮询请求
直接说最有效的方案,我们通过轮训请求来检测网络是否可用,比如加载图片或者访问接口等...
下面我以加载图片为例,搞个小demo:
- 首先尽可能的找一个小的图片,不要让图片的请求堵塞我们的其他功能使用。
推荐一个图片在线的压缩的网站: https://www.yalijuda.com/ ,然后把图片上到内部的服务器上
- 既然搞了,就搞个通用的,使用
tsup
我们搞个npm包,然后想一想我们要搞的功能,先把入口函数的出入参类型定了。我们既然要做轮训请求图片,我们首先需要一个图片地址,然后请求后的回调事件,甚至可能需要一些控制参数,那我们的入口代码也就有了。
const request = (imgUrl) => {
// do something
};
type CheckNetworkOptionsType = {
interval: number; // 循环时间 单位ms
};
type CheckNetworkType = (
imgUrl: string, // 图片url
callback: (isOnline: boolean) => void, // 回调,返回网络是否在线
options?: CheckNetworkOptionsType, // 配置项
) => void;
const checkNetwork: CheckNetworkType = (imgUrl, callback, options) => {
const { interval = 30_000 } = options || {};
const timer = setInterval(() => {
request(imgUrl)
}, interval);
return timer
};
export default checkNetwork
- 接下来我们要考虑一下如何进行请求,我们需要一个创一个
promise
和img标签
,resove
出去onload
和onerror
对应的在线和离线状态。
const request = (imgUrl) => {
return new Promise((resolve) => {
let imgRef = null;
let isRespond = false;
imgRef = document.createElement('img');
imgRef.onerror = () => {
isRespond = true;
resolve(false);
return '';
};
imgRef.onload = () => {
isRespond = true;
resolve(true);
return '';
};
imgRef.src = `${imgUrl}?time=${new Date().toLocaleString()}`;
});
};
type CheckNetworkOptionsType = {
interval: number; // 循环时间 单位ms
};
type CheckNetworkType = (
imgUrl: string, // 图片url
callback: (isOnline: boolean) => void, // 回调,返回网络是否在线
options?: CheckNetworkOptionsType, // 配置项
) => void;
const checkNetwork: CheckNetworkType = (imgUrl, callback, options) => {
const { interval = 30_000 } = options || {};
const timer = setInterval(async () => {
const status = (await request(imgUrl)) as boolean;
callback(status);
}, interval);
return timer
};
export default checkNetwork
- 这样基本的功能似乎就差不多了,但是感觉好像少点什么?比如服务器就是返回图片资源慢?那我们是不是可以加个超时时间?又或者是不是可以可以让用户手动取消循环?
const request = (imgUrl) => {
return new Promise((resolve) => {
let imgRef = null;
let isRespond = false;
imgRef = document.createElement('img');
imgRef.onerror = () => {
isRespond = true;
resolve(false);
return '';
};
imgRef.onload = () => {
isRespond = true;
resolve(true);
return '';
};
// 加个参数,防止浏览器缓存
imgRef.src = `${imgUrl}?time=${new Date().toLocaleString()}`;
});
};
type CheckNetworkOptionsType = {
interval: number; // 循环时间 单位ms
};
type CheckNetworkType = (
imgUrl: string, // 图片url
callback: (isOnline: boolean) => void, // 回调,返回网络是否在线
options?: CheckNetworkOptionsType, // 配置项
) => void;
const checkNetwork: CheckNetworkType = (imgUrl, callback, options) => {
const { interval = 30_000 } = options || {};
const timer = setInterval(async () => {
const status = (await request(imgUrl)) as boolean;
callback(status);
}, interval);
return timer
};
export default checkNetwork
- 完整的代码就完整了,具体用的时间还是建议大家关键模块来进行断网检测。不要一个后台配置表单都弄检测,这种完全可以在提交表单的时候接口响应进行处理,断网检测一般都是用在需要实时监控之类的。不多说了,我们来体验下:
6. 没问题,一切ok,发个包,就叫network-watcher
吧,欢迎大家star!
github.com/waltiu/netw…
方案2,直接调api
首先说下这种方案不推荐,其一浏览器兼容性有问题,其二只能检测到是否网络有连接,但是不能检测是否可用,这种实用性真的很差。
浏览器也提供了navigator.onLine
和 navigator.connection
可以直接查询网络状态,我们可以监听网络状态的变化。
window.addEventListener('online',function () {
alert("正常上网");
})
window.addEventListener('offline',function () {
alert('无网络');
})
这种实用性真的很差,用户网络连接但是没有网或者网很慢,实际上都会影响用户体验!!
用户的体验永远是NO.1
来源:juejin.cn/post/7299671709476700212
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